From a6a0ebc8da53fcbd030993ec133c98d09813190a Mon Sep 17 00:00:00 2001 From: xzl Date: Tue, 7 May 2024 17:34:49 +0800 Subject: [PATCH] feat: update tomcat10 to 10.1.23 Apache Tomcat 10 - Servlet and JSP engine -- core libraries Issue: https://github.com/deepin-community/sig-deepin-sysdev-team/issues/547 Log: update repo --- .editorconfig | 31 + BUILDING.txt | 604 ++ CONTRIBUTING.md | 165 + KEYS | 562 ++ LICENSE | 1143 +++ MERGE.txt | 78 + NOTICE | 68 + README.md | 80 +- RELEASE-NOTES | 173 + RUNNING.txt | 467 ++ bin/catalina-tasks.xml | 39 + bin/catalina.bat | 357 + bin/catalina.sh | 646 ++ bin/ciphers.bat | 58 + bin/ciphers.sh | 60 + bin/configtest.bat | 58 + bin/configtest.sh | 60 + bin/daemon.sh | 270 + bin/digest.bat | 58 + bin/digest.sh | 60 + bin/makebase.bat | 114 + bin/makebase.sh | 115 + bin/migrate.bat | 58 + bin/migrate.sh | 60 + bin/service.bat | 230 + bin/setclasspath.bat | 95 + bin/setclasspath.sh | 105 + bin/shutdown.bat | 58 + bin/shutdown.sh | 60 + bin/startup.bat | 58 + bin/startup.sh | 60 + bin/tool-wrapper.bat | 90 + bin/tool-wrapper.sh | 132 + bin/version.bat | 58 + bin/version.sh | 60 + build.properties.default | 352 + build.properties.release | 54 + build.xml | 4378 +++++++++++ conf/catalina.policy | 263 + conf/catalina.properties | 221 + conf/context.xml | 31 + conf/jaspic-providers.xml | 23 + conf/jaspic-providers.xsd | 53 + conf/logging.properties | 79 + conf/server.xml | 162 + conf/tomcat-users.xml | 56 + conf/tomcat-users.xsd | 59 + conf/web.xml | 4752 ++++++++++++ debian/README.Debian | 61 + debian/ant.properties | 15 + debian/changelog | 1172 ++- debian/clean | 4 + debian/compat | 1 - debian/context/docs.xml | 20 + debian/context/examples.xml | 5 + debian/context/host-manager.xml | 20 + debian/context/manager.xml | 20 + debian/control | 170 +- debian/copyright | 411 +- debian/default.template | 23 + debian/default_root/META-INF/context.xml | 2 + debian/default_root/index.html | 29 + debian/libexec/tomcat-locate-java.sh | 50 + debian/libexec/tomcat-start.sh | 25 + debian/libexec/tomcat-update-policy.sh | 23 + debian/libtomcat10-embed-java.manifest | 2 + debian/libtomcat10-embed-java.poms | 4 + debian/libtomcat10-java.lintian-overrides | 2 + debian/libtomcat10-java.manifest | 2 + debian/libtomcat10-java.poms | 32 + debian/logging.properties | 58 + debian/logrotate.template | 10 + debian/maven.rules | 36 + ...oy-webapps-target-from-deploy-target.patch | 30 + debian/patches/0005-skip-test-failures.patch | 22 + ....security.policy-file-in-catalina.sh.patch | 42 + debian/patches/0010-debianize-build-xml.patch | 33 + ...ok-for-build-properties-in-user-home.patch | 22 + debian/patches/0018-fix-manager-webapp.patch | 77 + .../0019-add-distribution-to-error-page.patch | 34 + .../0021-dont-test-unsupported-ciphers.patch | 63 + .../0023-disable-shutdown-by-socket.patch | 20 + .../patches/0024-systemd-log-formatter.patch | 122 + ...25-invalid-configuration-exit-status.patch | 23 + .../0026-easymock4-compatibility.patch | 153 + debian/patches/disable-jacoco.patch | 46 + .../exclude-TestJNDIRealmIntegration.patch | 22 + debian/patches/series | 14 + debian/policy/01system.policy | 51 + debian/policy/02debian.policy | 10 + debian/policy/03catalina.policy | 67 + debian/policy/04webapps.policy | 94 + debian/policy/50local.policy | 42 + debian/policy/examples/10examples.policy | 3 + debian/rsyslog/tomcat10.conf | 7 + debian/rules | 35 +- debian/setenv.sh | 12 + debian/sysusers/tomcat10.conf | 7 + debian/tomcat10-admin.install | 4 + debian/tomcat10-common.docs | 2 + debian/tomcat10-common.install | 3 + debian/tomcat10-common.links | 27 + debian/tomcat10-docs.doc-base | 9 + debian/tomcat10-docs.install | 2 + debian/tomcat10-docs.links | 1 + debian/tomcat10-docs.lintian-overrides | 9 + debian/tomcat10-examples.install | 4 + debian/tomcat10-instance-create | 139 + debian/tomcat10-instance-create.1 | 24 + debian/tomcat10-user.install | 6 + debian/tomcat10-user.manpages | 1 + debian/tomcat10.cron.daily | 26 + debian/tomcat10.dirs | 6 + debian/tomcat10.install | 11 + debian/tomcat10.links | 4 + debian/tomcat10.postinst | 78 + debian/tomcat10.postrm.in | 92 + debian/tomcat10.service | 43 + debian/tomcat10.tmpfiles | 6 + debian/watch | 3 + java/jakarta/annotation/Generated.java | 50 + java/jakarta/annotation/ManagedBean.java | 39 + java/jakarta/annotation/Nonnull.java | 32 + java/jakarta/annotation/Nullable.java | 32 + java/jakarta/annotation/PostConstruct.java | 35 + java/jakarta/annotation/PreDestroy.java | 35 + java/jakarta/annotation/Priority.java | 37 + java/jakarta/annotation/Resource.java | 88 + java/jakarta/annotation/Resources.java | 39 + .../annotation/security/DeclareRoles.java | 37 + java/jakarta/annotation/security/DenyAll.java | 33 + .../annotation/security/PermitAll.java | 33 + .../annotation/security/RolesAllowed.java | 37 + java/jakarta/annotation/security/RunAs.java | 37 + .../annotation/sql/DataSourceDefinition.java | 122 + .../annotation/sql/DataSourceDefinitions.java | 35 + java/jakarta/ejb/EJB.java | 40 + java/jakarta/ejb/EJBs.java | 29 + java/jakarta/el/ArrayELResolver.java | 163 + java/jakarta/el/BeanELResolver.java | 369 + java/jakarta/el/BeanNameELResolver.java | 160 + java/jakarta/el/BeanNameResolver.java | 87 + java/jakarta/el/CompositeELResolver.java | 279 + java/jakarta/el/ELClass.java | 33 + java/jakarta/el/ELContext.java | 382 + java/jakarta/el/ELContextEvent.java | 36 + java/jakarta/el/ELContextListener.java | 26 + java/jakarta/el/ELException.java | 62 + java/jakarta/el/ELManager.java | 88 + java/jakarta/el/ELProcessor.java | 340 + java/jakarta/el/ELResolver.java | 159 + java/jakarta/el/EvaluationListener.java | 54 + java/jakarta/el/Expression.java | 38 + java/jakarta/el/ExpressionFactory.java | 378 + java/jakarta/el/FunctionMapper.java | 37 + java/jakarta/el/ImportHandler.java | 480 ++ java/jakarta/el/LambdaExpression.java | 87 + java/jakarta/el/ListELResolver.java | 159 + java/jakarta/el/LocalStrings.properties | 53 + java/jakarta/el/LocalStrings_cs.properties | 52 + java/jakarta/el/LocalStrings_de.properties | 52 + java/jakarta/el/LocalStrings_es.properties | 52 + java/jakarta/el/LocalStrings_fr.properties | 53 + java/jakarta/el/LocalStrings_ja.properties | 53 + java/jakarta/el/LocalStrings_ko.properties | 52 + java/jakarta/el/LocalStrings_pt_BR.properties | 52 + java/jakarta/el/LocalStrings_ru.properties | 52 + java/jakarta/el/LocalStrings_zh_CN.properties | 52 + java/jakarta/el/MapELResolver.java | 140 + java/jakarta/el/MethodExpression.java | 82 + java/jakarta/el/MethodInfo.java | 88 + java/jakarta/el/MethodNotFoundException.java | 38 + java/jakarta/el/MethodReference.java | 133 + .../jakarta/el/PropertyNotFoundException.java | 38 + .../el/PropertyNotWritableException.java | 38 + java/jakarta/el/ResourceBundleELResolver.java | 124 + java/jakarta/el/StandardELContext.java | 203 + java/jakarta/el/StaticFieldELResolver.java | 194 + java/jakarta/el/TypeConverter.java | 60 + java/jakarta/el/Util.java | 862 +++ java/jakarta/el/ValueExpression.java | 87 + java/jakarta/el/ValueReference.java | 43 + java/jakarta/el/VariableMapper.java | 27 + java/jakarta/mail/Authenticator.java | 23 + java/jakarta/mail/PasswordAuthentication.java | 24 + java/jakarta/mail/Session.java | 30 + .../mail/internet/InternetAddress.java | 24 + java/jakarta/mail/internet/MimeMessage.java | 34 + java/jakarta/mail/internet/MimePart.java | 21 + .../mail/internet/MimePartDataSource.java | 24 + .../persistence/PersistenceContext.java | 37 + .../persistence/PersistenceContextType.java | 22 + .../persistence/PersistenceContexts.java | 29 + .../persistence/PersistenceProperty.java | 30 + java/jakarta/persistence/PersistenceUnit.java | 31 + .../jakarta/persistence/PersistenceUnits.java | 29 + .../persistence/SynchronizationType.java | 22 + .../security/auth/message/AuthException.java | 54 + .../security/auth/message/AuthStatus.java | 37 + .../security/auth/message/ClientAuth.java | 66 + .../security/auth/message/MessageInfo.java | 32 + .../security/auth/message/MessagePolicy.java | 85 + .../security/auth/message/ServerAuth.java | 65 + .../callback/CallerPrincipalCallback.java | 57 + .../message/callback/CertStoreCallback.java | 40 + .../callback/GroupPrincipalCallback.java | 42 + .../callback/PasswordValidationCallback.java | 65 + .../message/callback/PrivateKeyCallback.java | 122 + .../message/callback/SecretKeyCallback.java | 62 + .../message/callback/TrustStoreCallback.java | 37 + .../auth/message/config/AuthConfig.java | 32 + .../message/config/AuthConfigFactory.java | 183 + .../message/config/AuthConfigProvider.java | 30 + .../auth/message/config/ClientAuthConfig.java | 29 + .../message/config/ClientAuthContext.java | 22 + .../message/config/RegistrationListener.java | 22 + .../auth/message/config/ServerAuthConfig.java | 29 + .../message/config/ServerAuthContext.java | 22 + .../auth/message/module/ClientAuthModule.java | 33 + .../auth/message/module/ServerAuthModule.java | 33 + java/jakarta/servlet/AsyncContext.java | 161 + java/jakarta/servlet/AsyncEvent.java | 119 + java/jakarta/servlet/AsyncListener.java | 67 + java/jakarta/servlet/DispatcherType.java | 52 + java/jakarta/servlet/Filter.java | 105 + java/jakarta/servlet/FilterChain.java | 44 + java/jakarta/servlet/FilterConfig.java | 65 + java/jakarta/servlet/FilterRegistration.java | 77 + java/jakarta/servlet/GenericFilter.java | 91 + java/jakarta/servlet/GenericServlet.java | 203 + .../servlet/HttpConstraintElement.java | 117 + .../servlet/HttpMethodConstraintElement.java | 68 + java/jakarta/servlet/LocalStrings.properties | 26 + .../servlet/LocalStrings_cs.properties | 16 + .../servlet/LocalStrings_de.properties | 22 + .../servlet/LocalStrings_es.properties | 23 + .../servlet/LocalStrings_fr.properties | 26 + .../servlet/LocalStrings_ja.properties | 26 + .../servlet/LocalStrings_ko.properties | 26 + .../servlet/LocalStrings_ru.properties | 16 + .../servlet/LocalStrings_zh_CN.properties | 26 + .../servlet/MultipartConfigElement.java | 126 + java/jakarta/servlet/ReadListener.java | 50 + java/jakarta/servlet/Registration.java | 105 + java/jakarta/servlet/RequestDispatcher.java | 260 + java/jakarta/servlet/Servlet.java | 124 + java/jakarta/servlet/ServletConfig.java | 63 + java/jakarta/servlet/ServletConnection.java | 83 + .../servlet/ServletContainerInitializer.java | 47 + java/jakarta/servlet/ServletContext.java | 938 +++ .../servlet/ServletContextAttributeEvent.java | 72 + .../ServletContextAttributeListener.java | 58 + java/jakarta/servlet/ServletContextEvent.java | 47 + .../servlet/ServletContextListener.java | 50 + java/jakarta/servlet/ServletException.java | 79 + java/jakarta/servlet/ServletInputStream.java | 106 + java/jakarta/servlet/ServletOutputStream.java | 273 + java/jakarta/servlet/ServletRegistration.java | 99 + java/jakarta/servlet/ServletRequest.java | 472 ++ .../servlet/ServletRequestAttributeEvent.java | 73 + .../ServletRequestAttributeListener.java | 58 + java/jakarta/servlet/ServletRequestEvent.java | 60 + .../servlet/ServletRequestListener.java | 46 + .../servlet/ServletRequestWrapper.java | 476 ++ java/jakarta/servlet/ServletResponse.java | 320 + .../servlet/ServletResponseWrapper.java | 251 + .../servlet/ServletSecurityElement.java | 139 + java/jakarta/servlet/SessionCookieConfig.java | 184 + java/jakarta/servlet/SessionTrackingMode.java | 39 + .../jakarta/servlet/UnavailableException.java | 108 + java/jakarta/servlet/WriteListener.java | 43 + .../servlet/annotation/HandlesTypes.java | 39 + .../servlet/annotation/HttpConstraint.java | 63 + .../annotation/HttpMethodConstraint.java | 68 + .../servlet/annotation/MultipartConfig.java | 64 + .../servlet/annotation/ServletSecurity.java | 85 + .../jakarta/servlet/annotation/WebFilter.java | 110 + .../servlet/annotation/WebInitParam.java | 55 + .../servlet/annotation/WebListener.java | 47 + .../servlet/annotation/WebServlet.java | 102 + .../descriptor/JspConfigDescriptor.java | 44 + .../JspPropertyGroupDescriptor.java | 121 + .../servlet/descriptor/TaglibDescriptor.java | 39 + java/jakarta/servlet/http/Cookie.java | 548 ++ java/jakarta/servlet/http/HttpFilter.java | 87 + java/jakarta/servlet/http/HttpServlet.java | 1072 +++ .../servlet/http/HttpServletMapping.java | 49 + .../servlet/http/HttpServletRequest.java | 542 ++ .../http/HttpServletRequestWrapper.java | 384 + .../servlet/http/HttpServletResponse.java | 535 ++ .../http/HttpServletResponseWrapper.java | 247 + java/jakarta/servlet/http/HttpSession.java | 197 + .../http/HttpSessionActivationListener.java | 46 + .../http/HttpSessionAttributeListener.java | 55 + .../servlet/http/HttpSessionBindingEvent.java | 110 + .../http/HttpSessionBindingListener.java | 52 + .../servlet/http/HttpSessionEvent.java | 44 + .../servlet/http/HttpSessionIdListener.java | 41 + .../servlet/http/HttpSessionListener.java | 47 + .../servlet/http/HttpUpgradeHandler.java | 39 + .../servlet/http/LocalStrings.properties | 31 + .../servlet/http/LocalStrings_de.properties | 18 + .../servlet/http/LocalStrings_es.properties | 25 + .../servlet/http/LocalStrings_fr.properties | 31 + .../servlet/http/LocalStrings_ja.properties | 31 + .../servlet/http/LocalStrings_ko.properties | 31 + .../http/LocalStrings_zh_CN.properties | 31 + java/jakarta/servlet/http/MappingMatch.java | 50 + java/jakarta/servlet/http/Part.java | 124 + java/jakarta/servlet/http/PushBuilder.java | 175 + java/jakarta/servlet/http/WebConnection.java | 48 + java/jakarta/servlet/http/package.html | 30 + java/jakarta/servlet/jsp/ErrorData.java | 85 + java/jakarta/servlet/jsp/HttpJspPage.java | 53 + .../servlet/jsp/JspApplicationContext.java | 77 + java/jakarta/servlet/jsp/JspContext.java | 261 + java/jakarta/servlet/jsp/JspEngineInfo.java | 44 + java/jakarta/servlet/jsp/JspException.java | 89 + java/jakarta/servlet/jsp/JspFactory.java | 140 + java/jakarta/servlet/jsp/JspPage.java | 78 + java/jakarta/servlet/jsp/JspTagException.java | 75 + java/jakarta/servlet/jsp/JspWriter.java | 392 + .../servlet/jsp/LocalStrings.properties | 16 + .../servlet/jsp/LocalStrings_de.properties | 16 + .../servlet/jsp/LocalStrings_es.properties | 16 + .../servlet/jsp/LocalStrings_fr.properties | 16 + .../servlet/jsp/LocalStrings_ja.properties | 16 + .../servlet/jsp/LocalStrings_ko.properties | 16 + .../servlet/jsp/LocalStrings_zh_CN.properties | 16 + java/jakarta/servlet/jsp/PageContext.java | 445 ++ .../servlet/jsp/SkipPageException.java | 71 + java/jakarta/servlet/jsp/el/ELException.java | 76 + .../servlet/jsp/el/ELParseException.java | 49 + java/jakarta/servlet/jsp/el/Expression.java | 53 + .../servlet/jsp/el/ExpressionEvaluator.java | 94 + .../servlet/jsp/el/FunctionMapper.java | 43 + .../jsp/el/ImplicitObjectELResolver.java | 639 ++ .../servlet/jsp/el/ImportELResolver.java | 147 + .../servlet/jsp/el/NotFoundELResolver.java | 129 + .../jsp/el/ScopedAttributeELResolver.java | 193 + .../servlet/jsp/el/VariableResolver.java | 47 + java/jakarta/servlet/jsp/el/package.html | 37 + java/jakarta/servlet/jsp/package.html | 29 + java/jakarta/servlet/jsp/resources/jspxml.dtd | 192 + java/jakarta/servlet/jsp/resources/jspxml.xsd | 515 ++ .../servlet/jsp/tagext/BodyContent.java | 115 + java/jakarta/servlet/jsp/tagext/BodyTag.java | 134 + .../servlet/jsp/tagext/BodyTagSupport.java | 152 + .../servlet/jsp/tagext/DynamicAttributes.java | 44 + .../servlet/jsp/tagext/FunctionInfo.java | 77 + .../servlet/jsp/tagext/IterationTag.java | 88 + .../servlet/jsp/tagext/JspFragment.java | 79 + .../servlet/jsp/tagext/JspIdConsumer.java | 30 + java/jakarta/servlet/jsp/tagext/JspTag.java | 26 + java/jakarta/servlet/jsp/tagext/PageData.java | 43 + .../jakarta/servlet/jsp/tagext/SimpleTag.java | 121 + .../servlet/jsp/tagext/SimpleTagSupport.java | 190 + java/jakarta/servlet/jsp/tagext/Tag.java | 225 + .../servlet/jsp/tagext/TagAdapter.java | 150 + .../servlet/jsp/tagext/TagAttributeInfo.java | 254 + java/jakarta/servlet/jsp/tagext/TagData.java | 142 + .../servlet/jsp/tagext/TagExtraInfo.java | 131 + .../servlet/jsp/tagext/TagFileInfo.java | 75 + java/jakarta/servlet/jsp/tagext/TagInfo.java | 396 + .../servlet/jsp/tagext/TagLibraryInfo.java | 273 + .../jsp/tagext/TagLibraryValidator.java | 112 + .../servlet/jsp/tagext/TagSupport.java | 278 + .../servlet/jsp/tagext/TagVariableInfo.java | 97 + .../servlet/jsp/tagext/TryCatchFinally.java | 86 + .../servlet/jsp/tagext/ValidationMessage.java | 68 + .../servlet/jsp/tagext/VariableInfo.java | 230 + .../jsp/tagext/doc-files/BodyTagProtocol.gif | Bin 0 -> 4647 bytes .../tagext/doc-files/IterationTagProtocol.gif | Bin 0 -> 3762 bytes .../jsp/tagext/doc-files/TagProtocol.gif | Bin 0 -> 4152 bytes .../jsp/tagext/doc-files/VariableInfo-1.gif | Bin 0 -> 2306 bytes java/jakarta/servlet/jsp/tagext/package.html | 47 + java/jakarta/servlet/package.html | 30 + java/jakarta/servlet/resources/XMLSchema.dtd | 418 ++ java/jakarta/servlet/resources/datatypes.dtd | 219 + java/jakarta/servlet/resources/j2ee_1_4.xsd | 1582 ++++ .../resources/j2ee_web_services_1_1.xsd | 465 ++ .../j2ee_web_services_client_1_1.xsd | 319 + .../servlet/resources/jakartaee_10.xsd | 3073 ++++++++ .../jakarta/servlet/resources/jakartaee_9.xsd | 3073 ++++++++ .../resources/jakartaee_web_services_2_0.xsd | 551 ++ .../jakartaee_web_services_client_2_0.xsd | 714 ++ java/jakarta/servlet/resources/javaee_5.xsd | 2103 ++++++ java/jakarta/servlet/resources/javaee_6.xsd | 2431 +++++++ java/jakarta/servlet/resources/javaee_7.xsd | 3105 ++++++++ java/jakarta/servlet/resources/javaee_8.xsd | 3105 ++++++++ .../resources/javaee_web_services_1_2.xsd | 755 ++ .../resources/javaee_web_services_1_3.xsd | 578 ++ .../resources/javaee_web_services_1_4.xsd | 579 ++ .../javaee_web_services_client_1_2.xsd | 586 ++ .../javaee_web_services_client_1_3.xsd | 744 ++ .../javaee_web_services_client_1_4.xsd | 744 ++ java/jakarta/servlet/resources/jsp_2_0.xsd | 282 + java/jakarta/servlet/resources/jsp_2_1.xsd | 73 + java/jakarta/servlet/resources/jsp_2_2.xsd | 398 + java/jakarta/servlet/resources/jsp_2_3.xsd | 396 + java/jakarta/servlet/resources/jsp_3_0.xsd | 365 + java/jakarta/servlet/resources/jsp_3_1.xsd | 378 + .../jakarta/servlet/resources/web-app_2_2.dtd | 581 ++ .../jakarta/servlet/resources/web-app_2_3.dtd | 1003 +++ .../jakarta/servlet/resources/web-app_2_4.xsd | 1208 ++++ .../jakarta/servlet/resources/web-app_2_5.xsd | 945 +++ .../jakarta/servlet/resources/web-app_3_0.xsd | 281 + .../jakarta/servlet/resources/web-app_3_1.xsd | 333 + .../jakarta/servlet/resources/web-app_4_0.xsd | 372 + .../jakarta/servlet/resources/web-app_5_0.xsd | 342 + .../jakarta/servlet/resources/web-app_6_0.xsd | 342 + .../servlet/resources/web-common_3_0.xsd | 1584 ++++ .../servlet/resources/web-common_3_1.xsd | 1481 ++++ .../servlet/resources/web-common_4_0.xsd | 1481 ++++ .../servlet/resources/web-common_5_0.xsd | 1450 ++++ .../servlet/resources/web-common_6_0.xsd | 1507 ++++ .../servlet/resources/web-fragment_3_0.xsd | 281 + .../servlet/resources/web-fragment_3_1.xsd | 347 + .../servlet/resources/web-fragment_4_0.xsd | 347 + .../servlet/resources/web-fragment_5_0.xsd | 316 + .../servlet/resources/web-fragment_6_0.xsd | 316 + .../resources/web-jsptaglibrary_1_1.dtd | 207 + .../resources/web-jsptaglibrary_1_2.dtd | 478 ++ .../resources/web-jsptaglibrary_2_0.xsd | 983 +++ .../resources/web-jsptaglibrary_2_1.xsd | 229 + .../resources/web-jsptaglibrary_3_0.xsd | 1109 +++ .../resources/web-jsptaglibrary_3_1.xsd | 1109 +++ java/jakarta/servlet/resources/xml.xsd | 97 + .../transaction/HeuristicCommitException.java | 30 + .../transaction/HeuristicMixedException.java | 30 + .../HeuristicRollbackException.java | 30 + .../InvalidTransactionException.java | 30 + .../transaction/NotSupportedException.java | 30 + .../transaction/RollbackException.java | 30 + java/jakarta/transaction/Status.java | 30 + java/jakarta/transaction/Synchronization.java | 23 + java/jakarta/transaction/SystemException.java | 38 + java/jakarta/transaction/Transaction.java | 38 + .../transaction/TransactionManager.java | 38 + .../TransactionRequiredException.java | 30 + .../TransactionRolledbackException.java | 30 + .../TransactionSynchronizationRegistry.java | 33 + java/jakarta/transaction/UserTransaction.java | 32 + java/jakarta/websocket/ClientEndpoint.java | 36 + .../websocket/ClientEndpointConfig.java | 141 + java/jakarta/websocket/CloseReason.java | 120 + java/jakarta/websocket/ContainerProvider.java | 58 + java/jakarta/websocket/DecodeException.java | 55 + java/jakarta/websocket/Decoder.java | 63 + .../DefaultClientEndpointConfig.java | 88 + .../websocket/DeploymentException.java | 30 + java/jakarta/websocket/EncodeException.java | 38 + java/jakarta/websocket/Encoder.java | 59 + java/jakarta/websocket/Endpoint.java | 48 + java/jakarta/websocket/EndpointConfig.java | 29 + java/jakarta/websocket/Extension.java | 31 + java/jakarta/websocket/HandshakeResponse.java | 30 + java/jakarta/websocket/MessageHandler.java | 41 + java/jakarta/websocket/OnClose.java | 27 + java/jakarta/websocket/OnError.java | 27 + java/jakarta/websocket/OnMessage.java | 28 + java/jakarta/websocket/OnOpen.java | 27 + java/jakarta/websocket/PongMessage.java | 32 + java/jakarta/websocket/RemoteEndpoint.java | 223 + java/jakarta/websocket/SendHandler.java | 22 + java/jakarta/websocket/SendResult.java | 39 + java/jakarta/websocket/Session.java | 184 + java/jakarta/websocket/SessionException.java | 35 + .../jakarta/websocket/WebSocketContainer.java | 123 + .../server/DefaultServerEndpointConfig.java | 92 + .../websocket/server/HandshakeRequest.java | 53 + java/jakarta/websocket/server/PathParam.java | 32 + .../server/ServerApplicationConfig.java | 46 + .../websocket/server/ServerContainer.java | 65 + .../websocket/server/ServerEndpoint.java | 45 + .../server/ServerEndpointConfig.java | 233 + java/jakarta/xml/ws/WebServiceRef.java | 39 + java/jakarta/xml/ws/WebServiceRefs.java | 29 + java/org/apache/catalina/AccessLog.java | 109 + java/org/apache/catalina/AsyncDispatcher.java | 39 + java/org/apache/catalina/Authenticator.java | 57 + java/org/apache/catalina/Cluster.java | 85 + java/org/apache/catalina/Contained.java | 45 + java/org/apache/catalina/Container.java | 523 ++ java/org/apache/catalina/ContainerEvent.java | 96 + .../apache/catalina/ContainerListener.java | 38 + .../org/apache/catalina/ContainerServlet.java | 44 + java/org/apache/catalina/Context.java | 2060 ++++++ .../apache/catalina/CredentialHandler.java | 46 + .../apache/catalina/DistributedManager.java | 50 + java/org/apache/catalina/Engine.java | 86 + java/org/apache/catalina/Executor.java | 22 + java/org/apache/catalina/Globals.java | 281 + java/org/apache/catalina/Group.java | 117 + java/org/apache/catalina/Host.java | 280 + java/org/apache/catalina/JmxEnabled.java | 52 + java/org/apache/catalina/Lifecycle.java | 319 + java/org/apache/catalina/LifecycleEvent.java | 80 + .../apache/catalina/LifecycleException.java | 72 + .../apache/catalina/LifecycleListener.java | 39 + java/org/apache/catalina/LifecycleState.java | 65 + java/org/apache/catalina/Loader.java | 121 + java/org/apache/catalina/Manager.java | 498 ++ java/org/apache/catalina/Pipeline.java | 135 + java/org/apache/catalina/Realm.java | 276 + java/org/apache/catalina/Role.java | 72 + java/org/apache/catalina/Server.java | 268 + java/org/apache/catalina/Service.java | 152 + java/org/apache/catalina/Session.java | 376 + java/org/apache/catalina/SessionEvent.java | 98 + .../apache/catalina/SessionIdGenerator.java | 61 + java/org/apache/catalina/SessionListener.java | 38 + java/org/apache/catalina/Store.java | 134 + java/org/apache/catalina/StoreManager.java | 38 + .../catalina/ThreadBindingListener.java | 29 + java/org/apache/catalina/TomcatPrincipal.java | 98 + .../apache/catalina/TrackedWebResource.java | 24 + java/org/apache/catalina/User.java | 172 + java/org/apache/catalina/UserDatabase.java | 223 + java/org/apache/catalina/Valve.java | 126 + java/org/apache/catalina/WebResource.java | 171 + java/org/apache/catalina/WebResourceRoot.java | 527 ++ java/org/apache/catalina/WebResourceSet.java | 156 + java/org/apache/catalina/Wrapper.java | 374 + .../ant/AbstractCatalinaCommandTask.java | 84 + .../catalina/ant/AbstractCatalinaTask.java | 348 + .../ant/BaseRedirectorHelperTask.java | 373 + java/org/apache/catalina/ant/DeployTask.java | 194 + .../apache/catalina/ant/FindLeaksTask.java | 61 + .../catalina/ant/JKStatusUpdateTask.java | 415 ++ java/org/apache/catalina/ant/JMXGetTask.java | 100 + .../org/apache/catalina/ant/JMXQueryTask.java | 88 + java/org/apache/catalina/ant/JMXSetTask.java | 123 + java/org/apache/catalina/ant/ListTask.java | 53 + java/org/apache/catalina/ant/ReloadTask.java | 47 + .../apache/catalina/ant/ResourcesTask.java | 80 + .../apache/catalina/ant/ServerinfoTask.java | 45 + .../org/apache/catalina/ant/SessionsTask.java | 65 + .../catalina/ant/SslConnectorCiphersTask.java | 45 + java/org/apache/catalina/ant/StartTask.java | 46 + java/org/apache/catalina/ant/StopTask.java | 46 + .../apache/catalina/ant/ThreaddumpTask.java | 45 + .../org/apache/catalina/ant/UndeployTask.java | 46 + .../apache/catalina/ant/ValidatorTask.java | 111 + java/org/apache/catalina/ant/VminfoTask.java | 45 + java/org/apache/catalina/ant/antlib.xml | 81 + java/org/apache/catalina/ant/catalina.tasks | 40 + java/org/apache/catalina/ant/jmx/Arg.java | 39 + .../ant/jmx/JMXAccessorCondition.java | 229 + .../ant/jmx/JMXAccessorConditionBase.java | 182 + .../ant/jmx/JMXAccessorCreateTask.java | 179 + .../ant/jmx/JMXAccessorEqualsCondition.java | 80 + .../catalina/ant/jmx/JMXAccessorGetTask.java | 119 + .../ant/jmx/JMXAccessorInvokeTask.java | 181 + .../ant/jmx/JMXAccessorQueryTask.java | 166 + .../catalina/ant/jmx/JMXAccessorSetTask.java | 195 + .../catalina/ant/jmx/JMXAccessorTask.java | 722 ++ .../ant/jmx/JMXAccessorUnregisterTask.java | 85 + java/org/apache/catalina/ant/jmx/antlib.xml | 46 + .../apache/catalina/ant/jmx/jmxaccessor.tasks | 25 + java/org/apache/catalina/ant/jmx/package.html | 77 + java/org/apache/catalina/ant/package.html | 103 + .../authenticator/AuthenticatorBase.java | 1296 ++++ .../authenticator/BasicAuthenticator.java | 291 + .../catalina/authenticator/Constants.java | 88 + .../authenticator/DigestAuthenticator.java | 766 ++ .../authenticator/FormAuthenticator.java | 768 ++ .../authenticator/LocalStrings.properties | 94 + .../authenticator/LocalStrings_cs.properties | 26 + .../authenticator/LocalStrings_de.properties | 28 + .../authenticator/LocalStrings_es.properties | 44 + .../authenticator/LocalStrings_fr.properties | 94 + .../authenticator/LocalStrings_ja.properties | 94 + .../authenticator/LocalStrings_ko.properties | 77 + .../LocalStrings_pt_BR.properties | 20 + .../authenticator/LocalStrings_ru.properties | 16 + .../LocalStrings_zh_CN.properties | 76 + .../authenticator/NonLoginAuthenticator.java | 104 + .../authenticator/SSLAuthenticator.java | 206 + .../catalina/authenticator/SavedRequest.java | 193 + .../catalina/authenticator/SingleSignOn.java | 590 ++ .../authenticator/SingleSignOnEntry.java | 196 + .../authenticator/SingleSignOnListener.java | 61 + .../authenticator/SingleSignOnSessionKey.java | 117 + .../authenticator/SpnegoAuthenticator.java | 496 ++ .../jaspic/AuthConfigFactoryImpl.java | 646 ++ .../jaspic/CallbackHandlerImpl.java | 132 + .../jaspic/LocalStrings.properties | 35 + .../jaspic/LocalStrings_cs.properties | 18 + .../jaspic/LocalStrings_de.properties | 18 + .../jaspic/LocalStrings_es.properties | 20 + .../jaspic/LocalStrings_fr.properties | 35 + .../jaspic/LocalStrings_ja.properties | 35 + .../jaspic/LocalStrings_ko.properties | 35 + .../jaspic/LocalStrings_pt_BR.properties | 16 + .../jaspic/LocalStrings_ru.properties | 18 + .../jaspic/LocalStrings_zh_CN.properties | 35 + .../authenticator/jaspic/MessageInfoImpl.java | 78 + .../PersistentProviderRegistrations.java | 281 + .../jaspic/SimpleAuthConfigProvider.java | 88 + .../jaspic/SimpleServerAuthConfig.java | 147 + .../jaspic/SimpleServerAuthContext.java | 72 + .../authenticator/mbeans-descriptors.xml | 300 + .../catalina/authenticator/package.html | 54 + .../connector/ClientAbortException.java | 70 + .../apache/catalina/connector/Connector.java | 1144 +++ .../catalina/connector/CoyoteAdapter.java | 1315 ++++ .../catalina/connector/CoyoteInputStream.java | 305 + .../connector/CoyoteOutputStream.java | 197 + .../catalina/connector/CoyotePrincipal.java | 68 + .../catalina/connector/CoyoteReader.java | 207 + .../catalina/connector/CoyoteWriter.java | 320 + .../catalina/connector/InputBuffer.java | 689 ++ .../connector/LocalStrings.properties | 98 + .../connector/LocalStrings_cs.properties | 30 + .../connector/LocalStrings_de.properties | 27 + .../connector/LocalStrings_es.properties | 64 + .../connector/LocalStrings_fr.properties | 98 + .../connector/LocalStrings_ja.properties | 98 + .../connector/LocalStrings_ko.properties | 93 + .../connector/LocalStrings_pt_BR.properties | 16 + .../connector/LocalStrings_ru.properties | 26 + .../connector/LocalStrings_zh_CN.properties | 97 + .../catalina/connector/OutputBuffer.java | 869 +++ .../apache/catalina/connector/Request.java | 3251 +++++++++ .../catalina/connector/RequestFacade.java | 858 +++ .../apache/catalina/connector/Response.java | 1629 +++++ .../catalina/connector/ResponseFacade.java | 488 ++ .../catalina/connector/mbeans-descriptors.xml | 245 + .../catalina/core/AccessLogAdapter.java | 63 + .../catalina/core/ApplicationContext.java | 1274 ++++ .../core/ApplicationContextFacade.java | 854 +++ .../catalina/core/ApplicationDispatcher.java | 953 +++ .../catalina/core/ApplicationFilterChain.java | 314 + .../core/ApplicationFilterConfig.java | 358 + .../core/ApplicationFilterFactory.java | 293 + .../core/ApplicationFilterRegistration.java | 204 + .../catalina/core/ApplicationHttpRequest.java | 939 +++ .../core/ApplicationHttpResponse.java | 363 + .../catalina/core/ApplicationMapping.java | 117 + .../apache/catalina/core/ApplicationPart.java | 165 + .../catalina/core/ApplicationPushBuilder.java | 427 ++ .../catalina/core/ApplicationRequest.java | 180 + .../catalina/core/ApplicationResponse.java | 183 + .../core/ApplicationServletRegistration.java | 219 + .../core/ApplicationSessionCookieConfig.java | 248 + .../catalina/core/AprLifecycleListener.java | 423 ++ java/org/apache/catalina/core/AprStatus.java | 60 + .../catalina/core/AsyncContextImpl.java | 592 ++ .../catalina/core/AsyncListenerWrapper.java | 80 + java/org/apache/catalina/core/Constants.java | 25 + .../apache/catalina/core/ContainerBase.java | 1367 ++++ .../core/ContextNamingInfoListener.java | 120 + .../catalina/core/DefaultInstanceManager.java | 771 ++ .../catalina/core/FrameworkListener.java | 121 + .../catalina/core/JniLifecycleListener.java | 87 + .../core/JreMemoryLeakPreventionListener.java | 214 + .../catalina/core/LocalStrings.properties | 336 + .../catalina/core/LocalStrings_cs.properties | 57 + .../catalina/core/LocalStrings_de.properties | 60 + .../catalina/core/LocalStrings_es.properties | 179 + .../catalina/core/LocalStrings_fr.properties | 330 + .../catalina/core/LocalStrings_ja.properties | 330 + .../catalina/core/LocalStrings_ko.properties | 306 + .../core/LocalStrings_pt_BR.properties | 33 + .../catalina/core/LocalStrings_ru.properties | 60 + .../core/LocalStrings_zh_CN.properties | 313 + .../catalina/core/NamingContextListener.java | 1237 ++++ .../core/OpenSSLLifecycleListener.java | 233 + .../core/PropertiesRoleMappingListener.java | 145 + .../core/RestrictedFilters.properties | 16 + .../core/RestrictedListeners.properties | 15 + .../core/RestrictedServlets.properties | 19 + .../apache/catalina/core/StandardContext.java | 6347 ++++++++++++++++ .../catalina/core/StandardContextValve.java | 92 + .../apache/catalina/core/StandardEngine.java | 468 ++ .../catalina/core/StandardEngineValve.java | 76 + .../apache/catalina/core/StandardHost.java | 885 +++ .../catalina/core/StandardHostValve.java | 378 + .../catalina/core/StandardPipeline.java | 459 ++ .../apache/catalina/core/StandardServer.java | 1121 +++ .../apache/catalina/core/StandardService.java | 658 ++ .../catalina/core/StandardThreadExecutor.java | 415 ++ .../core/StandardVirtualThreadExecutor.java | 215 + .../apache/catalina/core/StandardWrapper.java | 1497 ++++ .../catalina/core/StandardWrapperFacade.java | 102 + .../catalina/core/StandardWrapperValve.java | 338 + .../ThreadLocalLeakPreventionListener.java | 137 + .../catalina/core/mbeans-descriptors.xml | 1782 +++++ .../catalina/deploy/LocalStrings.properties | 25 + .../deploy/LocalStrings_cs.properties | 17 + .../deploy/LocalStrings_de.properties | 17 + .../deploy/LocalStrings_es.properties | 18 + .../deploy/LocalStrings_fr.properties | 25 + .../deploy/LocalStrings_ja.properties | 25 + .../deploy/LocalStrings_ko.properties | 25 + .../deploy/LocalStrings_pt_BR.properties | 16 + .../deploy/LocalStrings_zh_CN.properties | 25 + .../catalina/deploy/NamingResourcesImpl.java | 1209 ++++ .../catalina/deploy/mbeans-descriptors.xml | 124 + .../filters/AddDefaultCharsetFilter.java | 140 + .../apache/catalina/filters/Constants.java | 65 + .../apache/catalina/filters/CorsFilter.java | 999 +++ .../filters/CsrfPreventionFilter.java | 690 ++ .../filters/CsrfPreventionFilterBase.java | 135 + .../catalina/filters/ExpiresFilter.java | 1591 ++++ .../catalina/filters/FailedRequestFilter.java | 115 + .../apache/catalina/filters/FilterBase.java | 73 + .../filters/HttpHeaderSecurityFilter.java | 272 + .../catalina/filters/LocalStrings.properties | 81 + .../filters/LocalStrings_cs.properties | 30 + .../filters/LocalStrings_de.properties | 32 + .../filters/LocalStrings_es.properties | 44 + .../filters/LocalStrings_fr.properties | 81 + .../filters/LocalStrings_ja.properties | 81 + .../filters/LocalStrings_ko.properties | 71 + .../filters/LocalStrings_pt_BR.properties | 25 + .../filters/LocalStrings_ru.properties | 22 + .../filters/LocalStrings_zh_CN.properties | 73 + .../catalina/filters/RateLimitFilter.java | 237 + .../catalina/filters/RemoteAddrFilter.java | 65 + .../catalina/filters/RemoteCIDRFilter.java | 236 + .../catalina/filters/RemoteHostFilter.java | 64 + .../catalina/filters/RemoteIpFilter.java | 1331 ++++ .../catalina/filters/RequestDumperFilter.java | 272 + .../catalina/filters/RequestFilter.java | 239 + .../filters/RestCsrfPreventionFilter.java | 251 + .../filters/SessionInitializerFilter.java | 58 + .../filters/SetCharacterEncodingFilter.java | 141 + .../catalina/filters/WebdavFixFilter.java | 122 + .../apache/catalina/ha/CatalinaCluster.java | 114 + .../apache/catalina/ha/ClusterDeployer.java | 96 + .../apache/catalina/ha/ClusterListener.java | 104 + .../apache/catalina/ha/ClusterManager.java | 99 + .../apache/catalina/ha/ClusterMessage.java | 33 + .../catalina/ha/ClusterMessageBase.java | 51 + .../apache/catalina/ha/ClusterRuleSet.java | 184 + .../apache/catalina/ha/ClusterSession.java | 39 + java/org/apache/catalina/ha/ClusterValve.java | 41 + .../ha/authenticator/ClusterSingleSignOn.java | 205 + .../ClusterSingleSignOnListener.java | 34 + .../ha/authenticator/LocalStrings.properties | 17 + .../authenticator/LocalStrings_cs.properties | 16 + .../authenticator/LocalStrings_de.properties | 16 + .../authenticator/LocalStrings_es.properties | 16 + .../authenticator/LocalStrings_fr.properties | 17 + .../authenticator/LocalStrings_ja.properties | 17 + .../authenticator/LocalStrings_ko.properties | 17 + .../LocalStrings_zh_CN.properties | 17 + .../ha/authenticator/mbeans-descriptors.xml | 64 + .../catalina/ha/backend/CollectedInfo.java | 119 + .../ha/backend/HeartbeatListener.java | 235 + .../ha/backend/LocalStrings.properties | 33 + .../ha/backend/LocalStrings_de.properties | 16 + .../ha/backend/LocalStrings_fr.properties | 33 + .../ha/backend/LocalStrings_ja.properties | 33 + .../ha/backend/LocalStrings_ko.properties | 33 + .../ha/backend/LocalStrings_zh_CN.properties | 33 + .../catalina/ha/backend/MultiCastSender.java | 84 + .../org/apache/catalina/ha/backend/Proxy.java | 29 + .../apache/catalina/ha/backend/Sender.java | 43 + .../apache/catalina/ha/backend/TcpSender.java | 213 + .../ha/context/LocalStrings.properties | 19 + .../ha/context/LocalStrings_fr.properties | 19 + .../ha/context/LocalStrings_ja.properties | 19 + .../ha/context/LocalStrings_ko.properties | 19 + .../ha/context/LocalStrings_zh_CN.properties | 19 + .../ha/context/ReplicatedContext.java | 233 + .../catalina/ha/deploy/FarmWarDeployer.java | 752 ++ .../ha/deploy/FileChangeListener.java | 25 + .../catalina/ha/deploy/FileMessage.java | 91 + .../ha/deploy/FileMessageFactory.java | 362 + .../ha/deploy/LocalStrings.properties | 61 + .../ha/deploy/LocalStrings_cs.properties | 25 + .../ha/deploy/LocalStrings_de.properties | 27 + .../ha/deploy/LocalStrings_es.properties | 31 + .../ha/deploy/LocalStrings_fr.properties | 61 + .../ha/deploy/LocalStrings_ja.properties | 61 + .../ha/deploy/LocalStrings_ko.properties | 60 + .../ha/deploy/LocalStrings_pt_BR.properties | 19 + .../ha/deploy/LocalStrings_ru.properties | 22 + .../ha/deploy/LocalStrings_zh_CN.properties | 60 + .../catalina/ha/deploy/UndeployMessage.java | 65 + .../apache/catalina/ha/deploy/WarWatcher.java | 223 + .../catalina/ha/deploy/mbeans-descriptors.xml | 54 + java/org/apache/catalina/ha/package.html | 23 + .../catalina/ha/session/BackupManager.java | 272 + .../ha/session/ClusterManagerBase.java | 228 + .../ha/session/ClusterSessionListener.java | 107 + .../catalina/ha/session/DeltaManager.java | 1373 ++++ .../catalina/ha/session/DeltaRequest.java | 445 ++ .../catalina/ha/session/DeltaSession.java | 922 +++ .../ha/session/JvmRouteBinderValve.java | 398 + .../ha/session/LocalStrings.properties | 92 + .../ha/session/LocalStrings_cs.properties | 25 + .../ha/session/LocalStrings_de.properties | 26 + .../ha/session/LocalStrings_es.properties | 81 + .../ha/session/LocalStrings_fr.properties | 92 + .../ha/session/LocalStrings_ja.properties | 92 + .../ha/session/LocalStrings_ko.properties | 90 + .../ha/session/LocalStrings_pt_BR.properties | 17 + .../ha/session/LocalStrings_ru.properties | 16 + .../ha/session/LocalStrings_zh_CN.properties | 90 + .../ha/session/ReplicatedSessionListener.java | 28 + .../catalina/ha/session/SessionMessage.java | 107 + .../ha/session/SessionMessageImpl.java | 175 + .../ha/session/mbeans-descriptors.xml | 634 ++ .../org/apache/catalina/ha/tcp/Constants.java | 28 + .../catalina/ha/tcp/LocalStrings.properties | 45 + .../ha/tcp/LocalStrings_cs.properties | 21 + .../ha/tcp/LocalStrings_de.properties | 20 + .../ha/tcp/LocalStrings_es.properties | 34 + .../ha/tcp/LocalStrings_fr.properties | 45 + .../ha/tcp/LocalStrings_ja.properties | 45 + .../ha/tcp/LocalStrings_ko.properties | 42 + .../ha/tcp/LocalStrings_pt_BR.properties | 16 + .../ha/tcp/LocalStrings_ru.properties | 19 + .../ha/tcp/LocalStrings_zh_CN.properties | 42 + .../catalina/ha/tcp/ReplicationValve.java | 611 ++ .../catalina/ha/tcp/SendMessageData.java | 63 + .../catalina/ha/tcp/SimpleTcpCluster.java | 878 +++ .../catalina/ha/tcp/mbeans-descriptors.xml | 157 + .../catalina/loader/JdbcLeakPrevention.java | 67 + .../catalina/loader/LocalStrings.properties | 70 + .../loader/LocalStrings_cs.properties | 19 + .../loader/LocalStrings_de.properties | 21 + .../loader/LocalStrings_es.properties | 41 + .../loader/LocalStrings_fr.properties | 70 + .../loader/LocalStrings_ja.properties | 70 + .../loader/LocalStrings_ko.properties | 70 + .../loader/LocalStrings_pt_BR.properties | 17 + .../loader/LocalStrings_ru.properties | 20 + .../loader/LocalStrings_zh_CN.properties | 70 + .../loader/ParallelWebappClassLoader.java | 73 + .../apache/catalina/loader/ResourceEntry.java | 38 + .../catalina/loader/WebappClassLoader.java | 69 + .../loader/WebappClassLoaderBase.java | 2658 +++++++ .../apache/catalina/loader/WebappLoader.java | 623 ++ .../catalina/loader/mbeans-descriptors.xml | 123 + .../apache/catalina/manager/Constants.java | 146 + .../catalina/manager/DummyProxySession.java | 222 + .../catalina/manager/HTMLManagerServlet.java | 1369 ++++ .../catalina/manager/JMXProxyServlet.java | 313 + .../apache/catalina/manager/JspHelper.java | 223 + .../catalina/manager/LocalStrings.properties | 198 + .../manager/LocalStrings_cs.properties | 37 + .../manager/LocalStrings_de.properties | 97 + .../manager/LocalStrings_es.properties | 121 + .../manager/LocalStrings_fr.properties | 196 + .../manager/LocalStrings_ja.properties | 198 + .../manager/LocalStrings_ko.properties | 196 + .../manager/LocalStrings_pt.properties | 16 + .../manager/LocalStrings_pt_BR.properties | 23 + .../manager/LocalStrings_ru.properties | 172 + .../manager/LocalStrings_zh_CN.properties | 195 + .../catalina/manager/ManagerServlet.java | 1673 +++++ .../manager/StatusManagerServlet.java | 348 + .../catalina/manager/StatusTransformer.java | 1083 +++ .../catalina/manager/host/Constants.java | 92 + .../manager/host/HTMLHostManagerServlet.java | 570 ++ .../manager/host/HostManagerServlet.java | 658 ++ .../manager/host/LocalStrings.properties | 90 + .../manager/host/LocalStrings_cs.properties | 29 + .../manager/host/LocalStrings_de.properties | 40 + .../manager/host/LocalStrings_es.properties | 83 + .../manager/host/LocalStrings_fr.properties | 90 + .../manager/host/LocalStrings_ja.properties | 90 + .../manager/host/LocalStrings_ko.properties | 89 + .../host/LocalStrings_pt_BR.properties | 20 + .../manager/host/LocalStrings_ru.properties | 89 + .../host/LocalStrings_zh_CN.properties | 89 + .../catalina/manager/util/SessionUtils.java | 261 + .../org/apache/catalina/mapper/Constants.java | 26 + .../catalina/mapper/LocalStrings.properties | 36 + .../mapper/LocalStrings_cs.properties | 19 + .../mapper/LocalStrings_de.properties | 19 + .../mapper/LocalStrings_es.properties | 22 + .../mapper/LocalStrings_fr.properties | 36 + .../mapper/LocalStrings_ja.properties | 36 + .../mapper/LocalStrings_ko.properties | 36 + .../mapper/LocalStrings_pt_BR.properties | 16 + .../mapper/LocalStrings_zh_CN.properties | 36 + java/org/apache/catalina/mapper/Mapper.java | 1655 +++++ .../catalina/mapper/MapperListener.java | 512 ++ .../apache/catalina/mapper/MappingData.java | 62 + .../catalina/mapper/WrapperMappingInfo.java | 54 + .../catalina/mapper/mbeans-descriptors.xml | 40 + .../apache/catalina/mbeans-descriptors.xml | 171 + .../catalina/mbeans/BaseCatalinaMBean.java | 46 + .../catalina/mbeans/ClassNameMBean.java | 41 + .../catalina/mbeans/ConnectorMBean.java | 92 + .../catalina/mbeans/ContainerMBean.java | 222 + .../mbeans/ContextEnvironmentMBean.java | 61 + .../apache/catalina/mbeans/ContextMBean.java | 195 + .../mbeans/ContextResourceLinkMBean.java | 127 + .../catalina/mbeans/ContextResourceMBean.java | 130 + .../mbeans/DataSourceUserDatabaseMBean.java | 378 + .../GlobalResourcesLifecycleListener.java | 235 + .../apache/catalina/mbeans/GroupMBean.java | 137 + .../catalina/mbeans/LocalStrings.properties | 67 + .../mbeans/LocalStrings_fr.properties | 67 + .../mbeans/LocalStrings_ja.properties | 67 + .../mbeans/LocalStrings_ko.properties | 65 + .../mbeans/LocalStrings_zh_CN.properties | 65 + .../apache/catalina/mbeans/MBeanDumper.java | 233 + .../apache/catalina/mbeans/MBeanFactory.java | 847 +++ .../apache/catalina/mbeans/MBeanUtils.java | 762 ++ .../mbeans/MemoryUserDatabaseMBean.java | 36 + .../catalina/mbeans/NamingResourcesMBean.java | 285 + .../org/apache/catalina/mbeans/RoleMBean.java | 44 + .../apache/catalina/mbeans/ServiceMBean.java | 129 + .../mbeans/SparseUserDatabaseMBean.java | 370 + .../org/apache/catalina/mbeans/UserMBean.java | 182 + .../catalina/mbeans/mbeans-descriptors.xml | 317 + .../realm/AuthenticatedUserRealm.java | 44 + .../apache/catalina/realm/CombinedRealm.java | 422 ++ .../catalina/realm/DataSourceRealm.java | 560 ++ .../realm/DigestCredentialHandlerBase.java | 347 + .../catalina/realm/GenericPrincipal.java | 352 + .../catalina/realm/JAASCallbackHandler.java | 217 + .../catalina/realm/JAASMemoryLoginModule.java | 377 + java/org/apache/catalina/realm/JAASRealm.java | 622 ++ java/org/apache/catalina/realm/JNDIRealm.java | 3178 ++++++++ .../catalina/realm/LocalStrings.properties | 118 + .../catalina/realm/LocalStrings_cs.properties | 31 + .../catalina/realm/LocalStrings_de.properties | 48 + .../catalina/realm/LocalStrings_es.properties | 77 + .../catalina/realm/LocalStrings_fr.properties | 118 + .../catalina/realm/LocalStrings_ja.properties | 118 + .../catalina/realm/LocalStrings_ko.properties | 112 + .../realm/LocalStrings_pt_BR.properties | 25 + .../catalina/realm/LocalStrings_ru.properties | 21 + .../realm/LocalStrings_zh_CN.properties | 112 + .../apache/catalina/realm/LockOutRealm.java | 364 + .../apache/catalina/realm/MemoryRealm.java | 247 + .../apache/catalina/realm/MemoryRuleSet.java | 119 + .../realm/MessageDigestCredentialHandler.java | 182 + .../realm/NestedCredentialHandler.java | 62 + java/org/apache/catalina/realm/NullRealm.java | 38 + java/org/apache/catalina/realm/RealmBase.java | 1593 ++++ .../realm/SecretKeyCredentialHandler.java | 105 + .../catalina/realm/UserDatabaseRealm.java | 354 + .../realm/X509SubjectDnRetriever.java | 30 + .../catalina/realm/X509UsernameRetriever.java | 33 + .../catalina/realm/mbeans-descriptors.xml | 483 ++ java/org/apache/catalina/realm/package.html | 30 + .../apache/catalina/security/Constants.java | 24 + .../security/DeployXmlPermission.java | 37 + .../catalina/security/LocalStrings.properties | 31 + .../security/LocalStrings_de.properties | 16 + .../security/LocalStrings_es.properties | 22 + .../security/LocalStrings_fr.properties | 31 + .../security/LocalStrings_ja.properties | 28 + .../security/LocalStrings_ko.properties | 24 + .../security/LocalStrings_zh_CN.properties | 24 + .../catalina/security/SecurityClassLoad.java | 203 + .../catalina/security/SecurityConfig.java | 142 + .../catalina/security/SecurityListener.java | 247 + .../catalina/security/SecurityUtil.java | 370 + .../TLSCertificateReloadListener.java | 177 + .../apache/catalina/servlets/CGIServlet.java | 1771 +++++ .../catalina/servlets/DefaultServlet.java | 2892 ++++++++ .../catalina/servlets/LocalStrings.properties | 58 + .../servlets/LocalStrings_cs.properties | 21 + .../servlets/LocalStrings_de.properties | 24 + .../servlets/LocalStrings_es.properties | 36 + .../servlets/LocalStrings_fr.properties | 58 + .../servlets/LocalStrings_ja.properties | 58 + .../servlets/LocalStrings_ko.properties | 56 + .../servlets/LocalStrings_pt_BR.properties | 19 + .../servlets/LocalStrings_ru.properties | 19 + .../servlets/LocalStrings_zh_CN.properties | 56 + .../catalina/servlets/WebdavServlet.java | 2527 +++++++ .../org/apache/catalina/servlets/package.html | 33 + .../apache/catalina/session/Constants.java | 46 + .../catalina/session/DataSourceStore.java | 733 ++ .../apache/catalina/session/FileStore.java | 354 + .../catalina/session/LocalStrings.properties | 103 + .../session/LocalStrings_cs.properties | 25 + .../session/LocalStrings_de.properties | 32 + .../session/LocalStrings_es.properties | 69 + .../session/LocalStrings_fr.properties | 103 + .../session/LocalStrings_ja.properties | 103 + .../session/LocalStrings_ko.properties | 94 + .../session/LocalStrings_pt_BR.properties | 19 + .../session/LocalStrings_ru.properties | 21 + .../session/LocalStrings_zh_CN.properties | 94 + .../apache/catalina/session/ManagerBase.java | 1320 ++++ .../catalina/session/PersistentManager.java | 49 + .../session/PersistentManagerBase.java | 1017 +++ .../catalina/session/StandardManager.java | 422 ++ .../catalina/session/StandardSession.java | 1482 ++++ .../session/StandardSessionFacade.java | 123 + .../apache/catalina/session/StoreBase.java | 276 + .../TooManyActiveSessionsException.java | 51 + .../catalina/session/mbeans-descriptors.xml | 411 ++ java/org/apache/catalina/session/package.html | 63 + .../ssi/ByteArrayServletOutputStream.java | 85 + .../catalina/ssi/ExpressionParseTree.java | 475 ++ .../catalina/ssi/ExpressionTokenizer.java | 195 + .../catalina/ssi/LocalStrings.properties | 46 + .../catalina/ssi/LocalStrings_fr.properties | 46 + .../catalina/ssi/LocalStrings_ja.properties | 46 + .../catalina/ssi/LocalStrings_ko.properties | 46 + .../ssi/LocalStrings_zh_CN.properties | 46 + .../catalina/ssi/ResponseIncludeWrapper.java | 171 + java/org/apache/catalina/ssi/SSICommand.java | 45 + .../apache/catalina/ssi/SSIConditional.java | 142 + .../catalina/ssi/SSIConditionalState.java | 40 + java/org/apache/catalina/ssi/SSIConfig.java | 61 + java/org/apache/catalina/ssi/SSIEcho.java | 79 + java/org/apache/catalina/ssi/SSIExec.java | 84 + .../catalina/ssi/SSIExternalResolver.java | 70 + java/org/apache/catalina/ssi/SSIFilter.java | 160 + java/org/apache/catalina/ssi/SSIFlastmod.java | 74 + java/org/apache/catalina/ssi/SSIFsize.java | 123 + java/org/apache/catalina/ssi/SSIInclude.java | 65 + java/org/apache/catalina/ssi/SSIMediator.java | 350 + java/org/apache/catalina/ssi/SSIPrintenv.java | 59 + .../org/apache/catalina/ssi/SSIProcessor.java | 328 + java/org/apache/catalina/ssi/SSIServlet.java | 213 + .../ssi/SSIServletExternalResolver.java | 544 ++ .../catalina/ssi/SSIServletRequestUtil.java | 56 + java/org/apache/catalina/ssi/SSISet.java | 66 + .../ssi/SSIStopProcessingException.java | 39 + java/org/apache/catalina/ssi/package.html | 32 + .../catalina/startup/AddPortOffsetRule.java | 43 + .../startup/Authenticators.properties | 21 + .../apache/catalina/startup/Bootstrap.java | 605 ++ .../org/apache/catalina/startup/Catalina.java | 1023 +++ .../CatalinaBaseConfigurationSource.java | 165 + .../catalina/startup/CatalinaProperties.java | 144 + .../startup/CertificateCreateRule.java | 69 + .../catalina/startup/ClassLoaderFactory.java | 316 + .../catalina/startup/ConnectorCreateRule.java | 127 + .../apache/catalina/startup/Constants.java | 58 + .../catalina/startup/ContextConfig.java | 2786 +++++++ .../catalina/startup/ContextRuleSet.java | 199 + .../startup/CopyParentClassLoaderRule.java | 77 + .../startup/CredentialHandlerRuleSet.java | 91 + .../apache/catalina/startup/EngineConfig.java | 111 + .../catalina/startup/EngineRuleSet.java | 98 + .../apache/catalina/startup/ExpandWar.java | 388 + .../catalina/startup/FailedContext.java | 1437 ++++ .../catalina/startup/HomesUserDatabase.java | 108 + .../apache/catalina/startup/HostConfig.java | 2009 ++++++ .../apache/catalina/startup/HostRuleSet.java | 97 + .../startup/LifecycleListenerRule.java | 132 + .../catalina/startup/ListenerCreateRule.java | 125 + .../catalina/startup/LocalStrings.properties | 195 + .../startup/LocalStrings_cs.properties | 40 + .../startup/LocalStrings_de.properties | 51 + .../startup/LocalStrings_es.properties | 118 + .../startup/LocalStrings_fr.properties | 195 + .../startup/LocalStrings_ja.properties | 196 + .../startup/LocalStrings_ko.properties | 186 + .../startup/LocalStrings_pt_BR.properties | 22 + .../startup/LocalStrings_ru.properties | 42 + .../startup/LocalStrings_zh_CN.properties | 185 + .../startup/MimeTypeMappings.properties | 1029 +++ .../catalina/startup/NamingRuleSet.java | 105 + .../catalina/startup/PasswdUserDatabase.java | 119 + .../apache/catalina/startup/RealmRuleSet.java | 86 + .../SafeForkJoinWorkerThreadFactory.java | 44 + .../catalina/startup/SetNextNamingRule.java | 128 + java/org/apache/catalina/startup/Tomcat.java | 1267 ++++ java/org/apache/catalina/startup/Tool.java | 238 + .../apache/catalina/startup/UserConfig.java | 424 ++ .../apache/catalina/startup/UserDatabase.java | 65 + .../startup/VersionLoggerListener.java | 131 + .../catalina/startup/WebAnnotationSet.java | 355 + .../catalina/startup/WebappServiceLoader.java | 239 + .../catalina/startup/mbeans-descriptors.xml | 164 + .../storeconfig/CatalinaClusterSF.java | 95 + .../storeconfig/CertificateStoreAppender.java | 39 + .../catalina/storeconfig/ChannelSF.java | 77 + .../catalina/storeconfig/ConnectorSF.java | 75 + .../storeconfig/ConnectorStoreAppender.java | 307 + .../catalina/storeconfig/Constants.java | 23 + .../storeconfig/CredentialHandlerSF.java | 87 + .../storeconfig/GlobalNamingResourcesSF.java | 71 + .../catalina/storeconfig/IStoreConfig.java | 131 + .../catalina/storeconfig/IStoreFactory.java | 34 + .../catalina/storeconfig/InterceptorSF.java | 84 + .../catalina/storeconfig/JarScannerSF.java | 56 + .../apache/catalina/storeconfig/LoaderSF.java | 84 + .../storeconfig/LocalStrings.properties | 44 + .../storeconfig/LocalStrings_fr.properties | 44 + .../storeconfig/LocalStrings_ja.properties | 44 + .../storeconfig/LocalStrings_ko.properties | 43 + .../storeconfig/LocalStrings_pt_BR.properties | 16 + .../storeconfig/LocalStrings_zh_CN.properties | 43 + .../catalina/storeconfig/ManagerSF.java | 96 + .../storeconfig/NamingResourcesSF.java | 105 + .../catalina/storeconfig/OpenSSLConfSF.java | 44 + .../storeconfig/PersistentManagerSF.java | 62 + .../apache/catalina/storeconfig/RealmSF.java | 94 + .../catalina/storeconfig/SSLHostConfigSF.java | 60 + .../apache/catalina/storeconfig/SenderSF.java | 54 + .../storeconfig/StandardContextSF.java | 367 + .../storeconfig/StandardEngineSF.java | 93 + .../catalina/storeconfig/StandardHostSF.java | 106 + .../storeconfig/StandardServerSF.java | 87 + .../storeconfig/StandardServiceSF.java | 68 + .../catalina/storeconfig/StoreAppender.java | 387 + .../catalina/storeconfig/StoreConfig.java | 336 + .../StoreConfigLifecycleListener.java | 163 + .../storeconfig/StoreContextAppender.java | 182 + .../storeconfig/StoreDescription.java | 377 + .../storeconfig/StoreFactoryBase.java | 205 + .../storeconfig/StoreFactoryRule.java | 123 + .../catalina/storeconfig/StoreFileMover.java | 215 + .../catalina/storeconfig/StoreLoader.java | 172 + .../catalina/storeconfig/StoreRegistry.java | 225 + .../storeconfig/WatchedResourceSF.java | 53 + .../storeconfig/WebResourceRootSF.java | 84 + .../storeconfig/WrapperLifecycleSF.java | 53 + .../storeconfig/WrapperListenerSF.java | 54 + .../storeconfig/mbeans-descriptors.xml | 96 + .../catalina/storeconfig/server-registry.xml | 482 ++ .../apache/catalina/tribes/ByteMessage.java | 102 + java/org/apache/catalina/tribes/Channel.java | 473 ++ .../catalina/tribes/ChannelException.java | 188 + .../catalina/tribes/ChannelInterceptor.java | 189 + .../catalina/tribes/ChannelListener.java | 61 + .../catalina/tribes/ChannelMessage.java | 107 + .../catalina/tribes/ChannelReceiver.java | 93 + .../apache/catalina/tribes/ChannelSender.java | 82 + .../apache/catalina/tribes/ErrorHandler.java | 41 + .../org/apache/catalina/tribes/Heartbeat.java | 33 + .../apache/catalina/tribes/JmxChannel.java | 60 + .../catalina/tribes/ManagedChannel.java | 76 + java/org/apache/catalina/tribes/Member.java | 158 + .../catalina/tribes/MembershipListener.java | 39 + .../catalina/tribes/MembershipProvider.java | 38 + .../catalina/tribes/MembershipService.java | 164 + .../catalina/tribes/MessageListener.java | 32 + .../tribes/RemoteProcessException.java | 46 + java/org/apache/catalina/tribes/UniqueId.java | 79 + .../catalina/tribes/group/AbsoluteOrder.java | 126 + .../tribes/group/ChannelCoordinator.java | 375 + .../tribes/group/ChannelInterceptorBase.java | 245 + .../tribes/group/ExtendedRpcCallback.java | 45 + .../catalina/tribes/group/GroupChannel.java | 847 +++ .../tribes/group/GroupChannelMBean.java | 62 + .../tribes/group/InterceptorPayload.java | 31 + .../tribes/group/LocalStrings.properties | 29 + .../tribes/group/LocalStrings_cs.properties | 16 + .../tribes/group/LocalStrings_de.properties | 18 + .../tribes/group/LocalStrings_es.properties | 20 + .../tribes/group/LocalStrings_fr.properties | 29 + .../tribes/group/LocalStrings_ja.properties | 29 + .../tribes/group/LocalStrings_ko.properties | 29 + .../group/LocalStrings_pt_BR.properties | 16 + .../tribes/group/LocalStrings_ru.properties | 18 + .../group/LocalStrings_zh_CN.properties | 29 + .../catalina/tribes/group/Response.java | 52 + .../catalina/tribes/group/RpcCallback.java | 45 + .../catalina/tribes/group/RpcChannel.java | 312 + .../catalina/tribes/group/RpcMessage.java | 110 + .../interceptors/DomainFilterInterceptor.java | 164 + .../DomainFilterInterceptorMBean.java | 29 + .../interceptors/EncryptInterceptor.java | 620 ++ .../interceptors/EncryptInterceptorMBean.java | 31 + .../FragmentationInterceptor.java | 256 + .../FragmentationInterceptorMBean.java | 29 + .../group/interceptors/GzipInterceptor.java | 296 + .../interceptors/GzipInterceptorMBean.java | 79 + .../interceptors/LocalStrings.properties | 105 + .../interceptors/LocalStrings_cs.properties | 59 + .../interceptors/LocalStrings_de.properties | 33 + .../interceptors/LocalStrings_es.properties | 61 + .../interceptors/LocalStrings_fr.properties | 105 + .../interceptors/LocalStrings_ja.properties | 105 + .../interceptors/LocalStrings_ko.properties | 105 + .../LocalStrings_pt_BR.properties | 38 + .../interceptors/LocalStrings_ru.properties | 26 + .../LocalStrings_zh_CN.properties | 104 + .../MessageDispatchInterceptor.java | 333 + .../MessageDispatchInterceptorMBean.java | 46 + .../interceptors/NonBlockingCoordinator.java | 891 +++ .../group/interceptors/OrderInterceptor.java | 354 + .../group/interceptors/SimpleCoordinator.java | 118 + .../StaticMembershipInterceptor.java | 261 + .../StaticMembershipInterceptorMBean.java | 26 + .../interceptors/TcpFailureDetector.java | 445 ++ .../interceptors/TcpFailureDetectorMBean.java | 46 + .../interceptors/TcpPingInterceptor.java | 206 + .../interceptors/TcpPingInterceptorMBean.java | 27 + .../interceptors/ThroughputInterceptor.java | 169 + .../ThroughputInterceptorMBean.java | 54 + .../TwoPhaseCommitInterceptor.java | 150 + .../apache/catalina/tribes/io/BufferPool.java | 96 + .../catalina/tribes/io/ChannelData.java | 380 + .../io/DirectByteArrayOutputStream.java | 59 + .../catalina/tribes/io/ListenCallback.java | 39 + .../tribes/io/LocalStrings.properties | 27 + .../tribes/io/LocalStrings_cs.properties | 22 + .../tribes/io/LocalStrings_de.properties | 21 + .../tribes/io/LocalStrings_es.properties | 23 + .../tribes/io/LocalStrings_fr.properties | 27 + .../tribes/io/LocalStrings_ja.properties | 27 + .../tribes/io/LocalStrings_ko.properties | 27 + .../tribes/io/LocalStrings_pt_BR.properties | 18 + .../tribes/io/LocalStrings_ru.properties | 16 + .../tribes/io/LocalStrings_zh_CN.properties | 27 + .../catalina/tribes/io/ObjectReader.java | 173 + .../catalina/tribes/io/ReplicationStream.java | 178 + .../catalina/tribes/io/XByteBuffer.java | 617 ++ .../catalina/tribes/jmx/JmxRegistry.java | 159 + .../tribes/jmx/LocalStrings.properties | 21 + .../tribes/jmx/LocalStrings_es.properties | 16 + .../tribes/jmx/LocalStrings_fr.properties | 21 + .../tribes/jmx/LocalStrings_ja.properties | 21 + .../tribes/jmx/LocalStrings_ko.properties | 21 + .../tribes/jmx/LocalStrings_zh_CN.properties | 21 + .../catalina/tribes/membership/Constants.java | 34 + .../tribes/membership/LocalStrings.properties | 71 + .../membership/LocalStrings_cs.properties | 24 + .../membership/LocalStrings_de.properties | 28 + .../membership/LocalStrings_es.properties | 27 + .../membership/LocalStrings_fr.properties | 71 + .../membership/LocalStrings_ja.properties | 71 + .../membership/LocalStrings_ko.properties | 65 + .../membership/LocalStrings_pt_BR.properties | 16 + .../membership/LocalStrings_ru.properties | 18 + .../membership/LocalStrings_zh_CN.properties | 65 + .../tribes/membership/McastService.java | 550 ++ .../tribes/membership/McastServiceImpl.java | 746 ++ .../tribes/membership/McastServiceMBean.java | 60 + .../tribes/membership/MemberImpl.java | 673 ++ .../tribes/membership/Membership.java | 331 + .../membership/MembershipProviderBase.java | 73 + .../membership/MembershipServiceBase.java | 143 + .../tribes/membership/StaticMember.java | 79 + .../membership/StaticMembershipProvider.java | 416 ++ .../membership/StaticMembershipService.java | 256 + .../StaticMembershipServiceMBean.java | 44 + .../cloud/AbstractStreamProvider.java | 150 + .../cloud/CertificateStreamProvider.java | 82 + .../cloud/CloudMembershipProvider.java | 186 + .../cloud/CloudMembershipService.java | 273 + .../cloud/CloudMembershipServiceMBean.java | 40 + .../cloud/DNSMembershipProvider.java | 195 + .../cloud/InsecureStreamProvider.java | 35 + .../cloud/KubernetesMembershipProvider.java | 239 + .../membership/cloud/LocalStrings.properties | 40 + .../cloud/LocalStrings_cs.properties | 18 + .../cloud/LocalStrings_de.properties | 23 + .../cloud/LocalStrings_es.properties | 21 + .../cloud/LocalStrings_fr.properties | 40 + .../cloud/LocalStrings_ja.properties | 40 + .../cloud/LocalStrings_ko.properties | 33 + .../cloud/LocalStrings_pt_BR.properties | 18 + .../cloud/LocalStrings_zh_CN.properties | 33 + .../membership/cloud/StreamProvider.java | 34 + .../membership/cloud/TokenStreamProvider.java | 60 + .../tribes/membership/mbeans-descriptors.xml | 164 + java/org/apache/catalina/tribes/package.html | 88 + .../tribes/tipis/AbstractReplicatedMap.java | 1745 +++++ .../tribes/tipis/LazyReplicatedMap.java | 234 + .../tribes/tipis/LocalStrings.properties | 57 + .../tribes/tipis/LocalStrings_cs.properties | 23 + .../tribes/tipis/LocalStrings_de.properties | 20 + .../tribes/tipis/LocalStrings_es.properties | 23 + .../tribes/tipis/LocalStrings_fr.properties | 57 + .../tribes/tipis/LocalStrings_ja.properties | 57 + .../tribes/tipis/LocalStrings_ko.properties | 52 + .../tipis/LocalStrings_pt_BR.properties | 21 + .../tribes/tipis/LocalStrings_ru.properties | 16 + .../tipis/LocalStrings_zh_CN.properties | 52 + .../catalina/tribes/tipis/ReplicatedMap.java | 298 + .../tribes/tipis/ReplicatedMapEntry.java | 138 + .../tribes/transport/AbstractRxTask.java | 70 + .../tribes/transport/AbstractSender.java | 355 + .../catalina/tribes/transport/Constants.java | 41 + .../catalina/tribes/transport/DataSender.java | 33 + .../tribes/transport/LocalStrings.properties | 25 + .../transport/LocalStrings_cs.properties | 17 + .../transport/LocalStrings_de.properties | 16 + .../transport/LocalStrings_es.properties | 20 + .../transport/LocalStrings_fr.properties | 25 + .../transport/LocalStrings_ja.properties | 25 + .../transport/LocalStrings_ko.properties | 24 + .../transport/LocalStrings_pt_BR.properties | 16 + .../transport/LocalStrings_zh_CN.properties | 24 + .../tribes/transport/MultiPointSender.java | 32 + .../tribes/transport/PooledSender.java | 234 + .../tribes/transport/ReceiverBase.java | 621 ++ .../transport/ReplicationTransmitter.java | 144 + .../catalina/tribes/transport/RxTaskPool.java | 155 + .../tribes/transport/SenderState.java | 95 + .../transport/nio/LocalStrings.properties | 62 + .../transport/nio/LocalStrings_cs.properties | 26 + .../transport/nio/LocalStrings_de.properties | 24 + .../transport/nio/LocalStrings_es.properties | 34 + .../transport/nio/LocalStrings_fr.properties | 62 + .../transport/nio/LocalStrings_ja.properties | 62 + .../transport/nio/LocalStrings_ko.properties | 58 + .../transport/nio/LocalStrings_ru.properties | 18 + .../nio/LocalStrings_zh_CN.properties | 58 + .../tribes/transport/nio/NioReceiver.java | 487 ++ .../transport/nio/NioReceiverMBean.java | 78 + .../transport/nio/NioReplicationTask.java | 362 + .../tribes/transport/nio/NioSender.java | 424 ++ .../transport/nio/ParallelNioSender.java | 436 ++ .../transport/nio/PooledParallelSender.java | 71 + .../nio/PooledParallelSenderMBean.java | 68 + .../apache/catalina/tribes/util/Arrays.java | 248 + .../catalina/tribes/util/ExceptionUtils.java | 42 + .../catalina/tribes/util/ExecutorFactory.java | 133 + .../catalina/tribes/util/Jre14Compat.java | 60 + .../catalina/tribes/util/JreCompat.java | 55 + .../tribes/util/LocalStrings.properties | 27 + .../tribes/util/LocalStrings_cs.properties | 18 + .../tribes/util/LocalStrings_de.properties | 18 + .../tribes/util/LocalStrings_es.properties | 19 + .../tribes/util/LocalStrings_fr.properties | 25 + .../tribes/util/LocalStrings_ja.properties | 25 + .../tribes/util/LocalStrings_ko.properties | 25 + .../tribes/util/LocalStrings_pt_BR.properties | 16 + .../tribes/util/LocalStrings_zh_CN.properties | 25 + .../org/apache/catalina/tribes/util/Logs.java | 27 + .../catalina/tribes/util/StringManager.java | 262 + .../tribes/util/TcclThreadFactory.java | 66 + .../catalina/tribes/util/UUIDGenerator.java | 94 + .../apache/catalina/users/AbstractGroup.java | 168 + .../apache/catalina/users/AbstractRole.java | 115 + .../apache/catalina/users/AbstractUser.java | 225 + java/org/apache/catalina/users/Constants.java | 31 + .../users/DataSourceUserDatabase.java | 1639 +++++ .../users/DataSourceUserDatabaseFactory.java | 169 + .../apache/catalina/users/GenericGroup.java | 190 + .../apache/catalina/users/GenericRole.java | 109 + .../apache/catalina/users/GenericUser.java | 265 + .../catalina/users/LocalStrings.properties | 33 + .../catalina/users/LocalStrings_cs.properties | 16 + .../catalina/users/LocalStrings_de.properties | 17 + .../catalina/users/LocalStrings_es.properties | 26 + .../catalina/users/LocalStrings_fr.properties | 33 + .../catalina/users/LocalStrings_ja.properties | 33 + .../catalina/users/LocalStrings_ko.properties | 32 + .../catalina/users/LocalStrings_ru.properties | 16 + .../users/LocalStrings_zh_CN.properties | 32 + .../apache/catalina/users/MemoryGroup.java | 71 + .../org/apache/catalina/users/MemoryRole.java | 67 + .../org/apache/catalina/users/MemoryUser.java | 105 + .../catalina/users/MemoryUserDatabase.java | 925 +++ .../users/MemoryUserDatabaseFactory.java | 115 + .../catalina/users/SparseUserDatabase.java | 29 + .../catalina/users/mbeans-descriptors.xml | 504 ++ .../apache/catalina/util/CharsetMapper.java | 135 + .../util/CharsetMapperDefault.properties | 17 + .../org/apache/catalina/util/ContextName.java | 201 + .../util/CustomObjectInputStream.java | 195 + java/org/apache/catalina/util/DOMWriter.java | 165 + .../catalina/util/ErrorPageSupport.java | 104 + java/org/apache/catalina/util/IOTools.java | 110 + .../apache/catalina/util/Introspection.java | 180 + .../apache/catalina/util/LifecycleBase.java | 426 ++ .../catalina/util/LifecycleMBeanBase.java | 236 + .../catalina/util/LocalStrings.properties | 52 + .../catalina/util/LocalStrings_cs.properties | 21 + .../catalina/util/LocalStrings_de.properties | 27 + .../catalina/util/LocalStrings_es.properties | 31 + .../catalina/util/LocalStrings_fr.properties | 52 + .../catalina/util/LocalStrings_ja.properties | 52 + .../catalina/util/LocalStrings_ko.properties | 50 + .../util/LocalStrings_pt_BR.properties | 16 + .../catalina/util/LocalStrings_ru.properties | 20 + .../util/LocalStrings_zh_CN.properties | 50 + java/org/apache/catalina/util/NetMask.java | 309 + java/org/apache/catalina/util/NetMaskSet.java | 162 + .../apache/catalina/util/ParameterMap.java | 256 + .../org/apache/catalina/util/RequestUtil.java | 60 + .../org/apache/catalina/util/ResourceSet.java | 182 + java/org/apache/catalina/util/ServerInfo.java | 143 + .../catalina/util/ServerInfo.properties | 19 + .../apache/catalina/util/SessionConfig.java | 111 + .../catalina/util/SessionIdGeneratorBase.java | 327 + .../util/StandardSessionIdGenerator.java | 65 + java/org/apache/catalina/util/Strftime.java | 252 + java/org/apache/catalina/util/StringUtil.java | 43 + java/org/apache/catalina/util/TLSUtil.java | 50 + .../catalina/util/TimeBucketCounter.java | 221 + .../apache/catalina/util/ToStringUtil.java | 62 + java/org/apache/catalina/util/TomcatCSS.java | 32 + java/org/apache/catalina/util/URLEncoder.java | 193 + java/org/apache/catalina/util/XMLWriter.java | 249 + .../valves/AbstractAccessLogValve.java | 1908 +++++ .../catalina/valves/AccessLogValve.java | 682 ++ .../org/apache/catalina/valves/Constants.java | 38 + .../valves/CrawlerSessionManagerValve.java | 290 + .../catalina/valves/ErrorReportValve.java | 472 ++ .../valves/ExtendedAccessLogValve.java | 805 +++ .../catalina/valves/HealthCheckValve.java | 109 + .../catalina/valves/JDBCAccessLogValve.java | 657 ++ .../catalina/valves/JsonAccessLogValve.java | 271 + .../catalina/valves/JsonErrorReportValve.java | 109 + .../valves/LoadBalancerDrainingValve.java | 232 + .../catalina/valves/LocalStrings.properties | 165 + .../valves/LocalStrings_cs.properties | 37 + .../valves/LocalStrings_de.properties | 48 + .../valves/LocalStrings_es.properties | 85 + .../valves/LocalStrings_fr.properties | 165 + .../valves/LocalStrings_ja.properties | 166 + .../valves/LocalStrings_ko.properties | 152 + .../valves/LocalStrings_pt_BR.properties | 22 + .../valves/LocalStrings_ru.properties | 53 + .../valves/LocalStrings_zh_CN.properties | 151 + .../catalina/valves/PersistentValve.java | 457 ++ .../valves/ProxyErrorReportValve.java | 226 + .../catalina/valves/RemoteAddrValve.java | 62 + .../catalina/valves/RemoteCIDRValve.java | 251 + .../catalina/valves/RemoteHostValve.java | 57 + .../apache/catalina/valves/RemoteIpValve.java | 952 +++ .../catalina/valves/RequestFilterValve.java | 430 ++ java/org/apache/catalina/valves/SSLValve.java | 195 + .../catalina/valves/SemaphoreValve.java | 237 + .../valves/StuckThreadDetectionValve.java | 396 + .../org/apache/catalina/valves/ValveBase.java | 227 + .../catalina/valves/mbeans-descriptors.xml | 664 ++ java/org/apache/catalina/valves/package.html | 28 + .../valves/rewrite/InternalRewriteMap.java | 122 + .../valves/rewrite/LocalStrings.properties | 33 + .../valves/rewrite/LocalStrings_fr.properties | 33 + .../valves/rewrite/LocalStrings_ja.properties | 33 + .../valves/rewrite/LocalStrings_ko.properties | 24 + .../rewrite/LocalStrings_zh_CN.properties | 26 + .../valves/rewrite/QuotedStringTokenizer.java | 139 + .../rewrite/RandomizedTextRewriteMap.java | 94 + .../catalina/valves/rewrite/Resolver.java | 39 + .../catalina/valves/rewrite/ResolverImpl.java | 368 + .../catalina/valves/rewrite/RewriteCond.java | 239 + .../catalina/valves/rewrite/RewriteMap.java | 76 + .../catalina/valves/rewrite/RewriteRule.java | 577 ++ .../catalina/valves/rewrite/RewriteValve.java | 818 +++ .../catalina/valves/rewrite/Substitution.java | 348 + .../valves/rewrite/mbeans-descriptors.xml | 40 + .../webresources/AbstractArchiveResource.java | 316 + .../AbstractArchiveResourceSet.java | 335 + .../webresources/AbstractFileResourceSet.java | 250 + .../webresources/AbstractResource.java | 105 + .../webresources/AbstractResourceSet.java | 138 + .../AbstractSingleArchiveResource.java | 52 + .../AbstractSingleArchiveResourceSet.java | 150 + .../apache/catalina/webresources/Cache.java | 340 + .../catalina/webresources/CachedResource.java | 627 ++ .../ClasspathURLStreamHandler.java | 49 + .../catalina/webresources/DirResourceSet.java | 302 + .../catalina/webresources/EmptyResource.java | 172 + .../webresources/EmptyResourceSet.java | 177 + .../catalina/webresources/ExtractingRoot.java | 103 + .../catalina/webresources/FileResource.java | 288 + .../webresources/FileResourceSet.java | 168 + .../catalina/webresources/JarContents.java | 140 + .../catalina/webresources/JarResource.java | 42 + .../webresources/JarResourceRoot.java | 162 + .../catalina/webresources/JarResourceSet.java | 61 + .../catalina/webresources/JarWarResource.java | 94 + .../webresources/JarWarResourceSet.java | 263 + .../webresources/LocalStrings.properties | 61 + .../webresources/LocalStrings_cs.properties | 20 + .../webresources/LocalStrings_de.properties | 16 + .../webresources/LocalStrings_es.properties | 22 + .../webresources/LocalStrings_fr.properties | 61 + .../webresources/LocalStrings_ja.properties | 61 + .../webresources/LocalStrings_ko.properties | 61 + .../LocalStrings_pt_BR.properties | 16 + .../webresources/LocalStrings_ru.properties | 18 + .../LocalStrings_zh_CN.properties | 61 + .../catalina/webresources/StandardRoot.java | 860 +++ .../webresources/TomcatJarInputStream.java | 60 + .../TomcatURLStreamHandlerFactory.java | 164 + .../webresources/TrackedInputStream.java | 112 + .../webresources/VirtualResource.java | 44 + .../catalina/webresources/WarResource.java | 43 + .../catalina/webresources/WarResourceSet.java | 57 + .../webresources/mbeans-descriptors.xml | 106 + .../catalina/webresources/war/Handler.java | 46 + .../webresources/war/WarURLConnection.java | 79 + java/org/apache/coyote/AbstractProcessor.java | 1020 +++ .../apache/coyote/AbstractProcessorLight.java | 191 + java/org/apache/coyote/AbstractProtocol.java | 1234 ++++ java/org/apache/coyote/ActionCode.java | 267 + java/org/apache/coyote/ActionHook.java | 37 + java/org/apache/coyote/Adapter.java | 82 + .../apache/coyote/AsyncContextCallback.java | 47 + java/org/apache/coyote/AsyncStateMachine.java | 529 ++ .../apache/coyote/BadRequestException.java | 68 + java/org/apache/coyote/CloseNowException.java | 50 + java/org/apache/coyote/CompressionConfig.java | 313 + java/org/apache/coyote/Constants.java | 98 + .../apache/coyote/ContinueResponseTiming.java | 78 + java/org/apache/coyote/ErrorState.java | 86 + java/org/apache/coyote/InputBuffer.java | 50 + .../org/apache/coyote/LocalStrings.properties | 73 + .../apache/coyote/LocalStrings_cs.properties | 27 + .../apache/coyote/LocalStrings_de.properties | 29 + .../apache/coyote/LocalStrings_es.properties | 29 + .../apache/coyote/LocalStrings_fr.properties | 73 + .../apache/coyote/LocalStrings_ja.properties | 73 + .../apache/coyote/LocalStrings_ko.properties | 70 + .../coyote/LocalStrings_pt_BR.properties | 16 + .../apache/coyote/LocalStrings_ru.properties | 20 + .../coyote/LocalStrings_zh_CN.properties | 70 + java/org/apache/coyote/OutputBuffer.java | 48 + java/org/apache/coyote/Processor.java | 114 + java/org/apache/coyote/ProtocolException.java | 41 + java/org/apache/coyote/ProtocolHandler.java | 257 + java/org/apache/coyote/Request.java | 864 +++ java/org/apache/coyote/RequestGroupInfo.java | 156 + java/org/apache/coyote/RequestInfo.java | 264 + java/org/apache/coyote/Response.java | 781 ++ java/org/apache/coyote/UpgradeProtocol.java | 98 + java/org/apache/coyote/UpgradeToken.java | 57 + .../coyote/ajp/AbstractAjpProtocol.java | 282 + java/org/apache/coyote/ajp/AjpMessage.java | 406 ++ .../apache/coyote/ajp/AjpNio2Protocol.java | 51 + .../org/apache/coyote/ajp/AjpNioProtocol.java | 50 + java/org/apache/coyote/ajp/AjpProcessor.java | 1341 ++++ java/org/apache/coyote/ajp/Constants.java | 192 + .../apache/coyote/ajp/LocalStrings.properties | 37 + .../coyote/ajp/LocalStrings_cs.properties | 18 + .../coyote/ajp/LocalStrings_de.properties | 20 + .../coyote/ajp/LocalStrings_es.properties | 25 + .../coyote/ajp/LocalStrings_fr.properties | 37 + .../coyote/ajp/LocalStrings_ja.properties | 37 + .../coyote/ajp/LocalStrings_ko.properties | 34 + .../coyote/ajp/LocalStrings_pt_BR.properties | 16 + .../coyote/ajp/LocalStrings_ru.properties | 16 + .../coyote/ajp/LocalStrings_zh_CN.properties | 35 + .../http11/AbstractHttp11JsseProtocol.java | 63 + .../coyote/http11/AbstractHttp11Protocol.java | 802 +++ java/org/apache/coyote/http11/Constants.java | 157 + .../http11/HeadersTooLargeException.java | 41 + .../coyote/http11/Http11InputBuffer.java | 1237 ++++ .../coyote/http11/Http11Nio2Protocol.java | 59 + .../coyote/http11/Http11NioProtocol.java | 78 + .../coyote/http11/Http11OutputBuffer.java | 569 ++ .../apache/coyote/http11/Http11Processor.java | 1435 ++++ .../coyote/http11/HttpOutputBuffer.java | 39 + .../org/apache/coyote/http11/InputFilter.java | 81 + .../coyote/http11/LocalStrings.properties | 58 + .../coyote/http11/LocalStrings_cs.properties | 22 + .../coyote/http11/LocalStrings_de.properties | 22 + .../coyote/http11/LocalStrings_es.properties | 30 + .../coyote/http11/LocalStrings_fr.properties | 58 + .../coyote/http11/LocalStrings_ja.properties | 58 + .../coyote/http11/LocalStrings_ko.properties | 57 + .../http11/LocalStrings_pt_BR.properties | 19 + .../coyote/http11/LocalStrings_ru.properties | 22 + .../http11/LocalStrings_zh_CN.properties | 57 + .../apache/coyote/http11/OutputFilter.java | 49 + .../http11/filters/BufferedInputFilter.java | 188 + .../http11/filters/ChunkedInputFilter.java | 659 ++ .../http11/filters/ChunkedOutputFilter.java | 209 + .../http11/filters/GzipOutputFilter.java | 170 + .../http11/filters/IdentityInputFilter.java | 232 + .../http11/filters/IdentityOutputFilter.java | 132 + .../http11/filters/LocalStrings.properties | 34 + .../http11/filters/LocalStrings_cs.properties | 18 + .../http11/filters/LocalStrings_de.properties | 18 + .../http11/filters/LocalStrings_es.properties | 18 + .../http11/filters/LocalStrings_fr.properties | 34 + .../http11/filters/LocalStrings_ja.properties | 34 + .../http11/filters/LocalStrings_ko.properties | 27 + .../filters/LocalStrings_pt_BR.properties | 16 + .../http11/filters/LocalStrings_ru.properties | 17 + .../filters/LocalStrings_zh_CN.properties | 27 + .../filters/SavedRequestInputFilter.java | 112 + .../http11/filters/VoidInputFilter.java | 123 + .../http11/filters/VoidOutputFilter.java | 81 + .../upgrade/InternalHttpUpgradeHandler.java | 50 + .../http11/upgrade/LocalStrings.properties | 36 + .../http11/upgrade/LocalStrings_cs.properties | 20 + .../http11/upgrade/LocalStrings_de.properties | 18 + .../http11/upgrade/LocalStrings_es.properties | 20 + .../http11/upgrade/LocalStrings_fr.properties | 36 + .../http11/upgrade/LocalStrings_ja.properties | 36 + .../http11/upgrade/LocalStrings_ko.properties | 36 + .../http11/upgrade/LocalStrings_ru.properties | 18 + .../upgrade/LocalStrings_zh_CN.properties | 36 + .../UpgradeApplicationBufferHandler.java | 45 + .../http11/upgrade/UpgradeGroupInfo.java | 133 + .../coyote/http11/upgrade/UpgradeInfo.java | 96 + .../http11/upgrade/UpgradeProcessorBase.java | 105 + .../upgrade/UpgradeProcessorExternal.java | 145 + .../upgrade/UpgradeProcessorInternal.java | 116 + .../upgrade/UpgradeServletInputStream.java | 270 + .../upgrade/UpgradeServletOutputStream.java | 286 + .../coyote/http2/AbstractNonZeroStream.java | 63 + .../apache/coyote/http2/AbstractStream.java | 165 + java/org/apache/coyote/http2/ByteUtil.java | 107 + .../coyote/http2/ConnectionException.java | 34 + .../coyote/http2/ConnectionSettingsBase.java | 233 + .../coyote/http2/ConnectionSettingsLocal.java | 105 + .../http2/ConnectionSettingsRemote.java | 41 + java/org/apache/coyote/http2/Constants.java | 44 + java/org/apache/coyote/http2/Flags.java | 49 + java/org/apache/coyote/http2/FrameType.java | 117 + .../org/apache/coyote/http2/HPackHuffman.java | 585 ++ java/org/apache/coyote/http2/HeaderSink.java | 43 + java/org/apache/coyote/http2/Hpack.java | 216 + .../org/apache/coyote/http2/HpackDecoder.java | 490 ++ .../org/apache/coyote/http2/HpackEncoder.java | 402 ++ .../apache/coyote/http2/HpackException.java | 33 + .../apache/coyote/http2/Http2AsyncParser.java | 343 + .../http2/Http2AsyncUpgradeHandler.java | 558 ++ java/org/apache/coyote/http2/Http2Error.java | 54 + .../apache/coyote/http2/Http2Exception.java | 41 + .../coyote/http2/Http2OutputBuffer.java | 77 + java/org/apache/coyote/http2/Http2Parser.java | 833 +++ .../apache/coyote/http2/Http2Protocol.java | 393 + .../coyote/http2/Http2UpgradeHandler.java | 2096 ++++++ .../coyote/http2/LocalStrings.properties | 184 + .../coyote/http2/LocalStrings_cs.properties | 39 + .../coyote/http2/LocalStrings_de.properties | 43 + .../coyote/http2/LocalStrings_es.properties | 57 + .../coyote/http2/LocalStrings_fr.properties | 184 + .../coyote/http2/LocalStrings_ja.properties | 184 + .../coyote/http2/LocalStrings_ko.properties | 180 + .../http2/LocalStrings_pt_BR.properties | 20 + .../coyote/http2/LocalStrings_ru.properties | 20 + .../http2/LocalStrings_zh_CN.properties | 179 + .../apache/coyote/http2/RecycledStream.java | 69 + .../org/apache/coyote/http2/SendfileData.java | 33 + java/org/apache/coyote/http2/Setting.java | 72 + java/org/apache/coyote/http2/Stream.java | 1503 ++++ .../apache/coyote/http2/StreamException.java | 37 + .../apache/coyote/http2/StreamProcessor.java | 556 ++ .../apache/coyote/http2/StreamRunnable.java | 37 + .../coyote/http2/StreamStateMachine.java | 252 + .../coyote/http2/WindowAllocationManager.java | 241 + java/org/apache/coyote/mbeans-descriptors.xml | 92 + java/org/apache/el/ExpressionFactoryImpl.java | 83 + java/org/apache/el/LocalStrings.properties | 56 + java/org/apache/el/LocalStrings_es.properties | 37 + java/org/apache/el/LocalStrings_fr.properties | 56 + java/org/apache/el/LocalStrings_ja.properties | 56 + java/org/apache/el/LocalStrings_ko.properties | 56 + .../apache/el/LocalStrings_zh_CN.properties | 56 + java/org/apache/el/MethodExpressionImpl.java | 329 + .../apache/el/MethodExpressionLiteral.java | 134 + java/org/apache/el/ValueExpressionImpl.java | 293 + .../org/apache/el/ValueExpressionLiteral.java | 137 + java/org/apache/el/lang/ELArithmetic.java | 405 ++ java/org/apache/el/lang/ELSupport.java | 766 ++ .../org/apache/el/lang/EvaluationContext.java | 188 + .../org/apache/el/lang/ExpressionBuilder.java | 333 + .../apache/el/lang/FunctionMapperFactory.java | 70 + .../apache/el/lang/FunctionMapperImpl.java | 187 + .../el/lang/LambdaExpressionNestedState.java | 51 + .../apache/el/lang/VariableMapperFactory.java | 56 + .../apache/el/lang/VariableMapperImpl.java | 65 + java/org/apache/el/parser/ArithmeticNode.java | 37 + java/org/apache/el/parser/AstAnd.java | 46 + java/org/apache/el/parser/AstAssign.java | 50 + .../apache/el/parser/AstBracketSuffix.java | 38 + java/org/apache/el/parser/AstChoice.java | 48 + .../el/parser/AstCompositeExpression.java | 56 + .../apache/el/parser/AstConcatenation.java | 46 + .../el/parser/AstDeferredExpression.java | 56 + java/org/apache/el/parser/AstDiv.java | 41 + java/org/apache/el/parser/AstDotSuffix.java | 49 + .../el/parser/AstDynamicExpression.java | 56 + java/org/apache/el/parser/AstEmpty.java | 59 + java/org/apache/el/parser/AstEqual.java | 41 + java/org/apache/el/parser/AstFalse.java | 38 + .../apache/el/parser/AstFloatingPoint.java | 67 + java/org/apache/el/parser/AstFunction.java | 225 + java/org/apache/el/parser/AstGreaterThan.java | 47 + .../apache/el/parser/AstGreaterThanEqual.java | 47 + java/org/apache/el/parser/AstIdentifier.java | 238 + java/org/apache/el/parser/AstInteger.java | 68 + .../apache/el/parser/AstLambdaExpression.java | 151 + .../apache/el/parser/AstLambdaParameters.java | 43 + java/org/apache/el/parser/AstLessThan.java | 47 + .../apache/el/parser/AstLessThanEqual.java | 47 + java/org/apache/el/parser/AstListData.java | 50 + .../el/parser/AstLiteralExpression.java | 65 + java/org/apache/el/parser/AstMapData.java | 56 + java/org/apache/el/parser/AstMapEntry.java | 26 + .../apache/el/parser/AstMethodParameters.java | 53 + java/org/apache/el/parser/AstMinus.java | 41 + java/org/apache/el/parser/AstMod.java | 41 + java/org/apache/el/parser/AstMult.java | 41 + java/org/apache/el/parser/AstNegative.java | 84 + java/org/apache/el/parser/AstNot.java | 47 + java/org/apache/el/parser/AstNotEqual.java | 41 + java/org/apache/el/parser/AstNull.java | 44 + java/org/apache/el/parser/AstOr.java | 46 + java/org/apache/el/parser/AstPlus.java | 41 + java/org/apache/el/parser/AstSemicolon.java | 49 + java/org/apache/el/parser/AstSetData.java | 51 + java/org/apache/el/parser/AstString.java | 75 + java/org/apache/el/parser/AstTrue.java | 38 + java/org/apache/el/parser/AstValue.java | 392 + java/org/apache/el/parser/BooleanNode.java | 37 + java/org/apache/el/parser/ELParser.html | 223 + java/org/apache/el/parser/ELParser.java | 3962 ++++++++++ java/org/apache/el/parser/ELParser.jjt | 587 ++ .../apache/el/parser/ELParserConstants.java | 201 + .../el/parser/ELParserTokenManager.java | 2339 ++++++ .../el/parser/ELParserTreeConstants.java | 97 + .../apache/el/parser/JJTELParserState.java | 124 + java/org/apache/el/parser/Node.java | 89 + java/org/apache/el/parser/NodeVisitor.java | 24 + java/org/apache/el/parser/ParseException.java | 198 + .../apache/el/parser/SimpleCharStream.java | 483 ++ java/org/apache/el/parser/SimpleNode.java | 222 + java/org/apache/el/parser/Token.java | 133 + java/org/apache/el/parser/TokenMgrError.java | 148 + java/org/apache/el/stream/Optional.java | 76 + java/org/apache/el/stream/Stream.java | 522 ++ .../el/stream/StreamELResolverImpl.java | 114 + java/org/apache/el/util/ConcurrentCache.java | 59 + java/org/apache/el/util/ExceptionUtils.java | 56 + java/org/apache/el/util/MessageFactory.java | 80 + java/org/apache/el/util/ReflectionUtil.java | 617 ++ java/org/apache/el/util/Validation.java | 108 + java/org/apache/jasper/Constants.java | 101 + .../apache/jasper/EmbeddedServletOptions.java | 968 +++ java/org/apache/jasper/JasperException.java | 50 + java/org/apache/jasper/JspC.java | 1797 +++++ .../apache/jasper/JspCompilationContext.java | 769 ++ java/org/apache/jasper/Options.java | 371 + java/org/apache/jasper/TrimSpacesOption.java | 24 + .../apache/jasper/compiler/AntCompiler.java | 493 ++ .../jasper/compiler/AttributeParser.java | 333 + .../jasper/compiler/BeanRepository.java | 75 + .../org/apache/jasper/compiler/Collector.java | 217 + java/org/apache/jasper/compiler/Compiler.java | 623 ++ .../jasper/compiler/DefaultErrorHandler.java | 119 + .../jasper/compiler/ELFunctionMapper.java | 331 + .../apache/jasper/compiler/ELInterpreter.java | 46 + .../jasper/compiler/ELInterpreterFactory.java | 107 + java/org/apache/jasper/compiler/ELNode.java | 269 + java/org/apache/jasper/compiler/ELParser.java | 589 ++ .../jasper/compiler/EncodingDetector.java | 218 + .../jasper/compiler/ErrorDispatcher.java | 514 ++ .../apache/jasper/compiler/ErrorHandler.java | 76 + .../org/apache/jasper/compiler/Generator.java | 4158 +++++++++++ .../compiler/ImplicitTagLibraryInfo.java | 203 + .../apache/jasper/compiler/JDTCompiler.java | 541 ++ .../jasper/compiler/JarScannerFactory.java | 50 + .../apache/jasper/compiler/JasperTagInfo.java | 63 + .../jasper/compiler/JavacErrorDetail.java | 230 + .../org/apache/jasper/compiler/JspConfig.java | 544 ++ .../jasper/compiler/JspDocumentParser.java | 1550 ++++ .../org/apache/jasper/compiler/JspReader.java | 654 ++ .../jasper/compiler/JspRuntimeContext.java | 614 ++ java/org/apache/jasper/compiler/JspUtil.java | 944 +++ .../org/apache/jasper/compiler/Localizer.java | 87 + java/org/apache/jasper/compiler/Mark.java | 145 + .../NewlineReductionServletWriter.java | 54 + java/org/apache/jasper/compiler/Node.java | 2595 +++++++ .../apache/jasper/compiler/PageDataImpl.java | 750 ++ java/org/apache/jasper/compiler/PageInfo.java | 779 ++ java/org/apache/jasper/compiler/Parser.java | 1812 +++++ .../jasper/compiler/ParserController.java | 596 ++ .../jasper/compiler/ScriptingVariabler.java | 157 + .../apache/jasper/compiler/ServletWriter.java | 172 + .../org/apache/jasper/compiler/SmapInput.java | 39 + .../apache/jasper/compiler/SmapStratum.java | 397 + java/org/apache/jasper/compiler/SmapUtil.java | 834 +++ .../jasper/compiler/StringInterpreter.java | 49 + .../compiler/StringInterpreterFactory.java | 178 + .../apache/jasper/compiler/TagConstants.java | 115 + .../jasper/compiler/TagFileProcessor.java | 711 ++ .../jasper/compiler/TagLibraryInfoImpl.java | 409 ++ .../jasper/compiler/TagPluginManager.java | 293 + .../apache/jasper/compiler/TextOptimizer.java | 131 + java/org/apache/jasper/compiler/TldCache.java | 188 + .../org/apache/jasper/compiler/Validator.java | 1947 +++++ .../jasper/compiler/tagplugin/TagPlugin.java | 36 + .../compiler/tagplugin/TagPluginContext.java | 136 + java/org/apache/jasper/el/ELContextImpl.java | 153 + .../apache/jasper/el/ELContextWrapper.java | 89 + java/org/apache/jasper/el/ELResolverImpl.java | 126 + .../jasper/el/ExpressionEvaluatorImpl.java | 62 + java/org/apache/jasper/el/ExpressionImpl.java | 44 + .../apache/jasper/el/FunctionMapperImpl.java | 37 + .../apache/jasper/el/JasperELResolver.java | 310 + java/org/apache/jasper/el/JspELException.java | 28 + .../apache/jasper/el/JspMethodExpression.java | 165 + .../jasper/el/JspMethodNotFoundException.java | 28 + .../el/JspPropertyNotFoundException.java | 30 + .../el/JspPropertyNotWritableException.java | 29 + .../apache/jasper/el/JspValueExpression.java | 177 + .../jasper/el/VariableResolverImpl.java | 37 + .../ELInterpreterTagSetters.java | 269 + .../optimizations/StringInterpreterEnum.java | 37 + .../jasper/resources/LocalStrings.properties | 435 ++ .../resources/LocalStrings_cs.properties | 103 + .../resources/LocalStrings_de.properties | 120 + .../resources/LocalStrings_es.properties | 339 + .../resources/LocalStrings_fr.properties | 435 ++ .../resources/LocalStrings_ja.properties | 433 ++ .../resources/LocalStrings_ko.properties | 421 ++ .../resources/LocalStrings_pt.properties | 16 + .../resources/LocalStrings_pt_BR.properties | 69 + .../resources/LocalStrings_ru.properties | 58 + .../resources/LocalStrings_zh_CN.properties | 420 ++ .../jasper/runtime/BodyContentImpl.java | 682 ++ .../apache/jasper/runtime/ExceptionUtils.java | 60 + .../apache/jasper/runtime/HttpJspBase.java | 91 + .../runtime/InstanceManagerFactory.java | 38 + .../runtime/JspApplicationContextImpl.java | 138 + .../jasper/runtime/JspContextWrapper.java | 678 ++ .../apache/jasper/runtime/JspFactoryImpl.java | 215 + .../jasper/runtime/JspFragmentHelper.java | 64 + .../jasper/runtime/JspRuntimeLibrary.java | 1113 +++ .../jasper/runtime/JspSourceDependent.java | 39 + .../jasper/runtime/JspSourceDirectives.java | 27 + .../jasper/runtime/JspSourceImports.java | 30 + .../apache/jasper/runtime/JspWriterImpl.java | 608 ++ .../jasper/runtime/PageContextImpl.java | 740 ++ .../runtime/ProtectedFunctionMapper.java | 152 + .../ServletResponseWrapperInclude.java | 78 + .../apache/jasper/runtime/TagHandlerPool.java | 182 + .../jasper/security/SecurityClassLoad.java | 64 + .../apache/jasper/security/SecurityUtil.java | 41 + .../jasper/servlet/JasperInitializer.java | 117 + .../apache/jasper/servlet/JasperLoader.java | 169 + .../jasper/servlet/JspCServletContext.java | 778 ++ .../org/apache/jasper/servlet/JspServlet.java | 412 ++ .../jasper/servlet/JspServletWrapper.java | 615 ++ .../apache/jasper/servlet/TldPreScanned.java | 57 + .../org/apache/jasper/servlet/TldScanner.java | 416 ++ .../jasper/servlet/mbeans-descriptors.xml | 48 + .../apache/jasper/tagplugins/jstl/Util.java | 360 + .../jasper/tagplugins/jstl/core/Catch.java | 71 + .../jasper/tagplugins/jstl/core/Choose.java | 34 + .../jasper/tagplugins/jstl/core/ForEach.java | 349 + .../tagplugins/jstl/core/ForTokens.java | 118 + .../jasper/tagplugins/jstl/core/If.java | 50 + .../jasper/tagplugins/jstl/core/Import.java | 380 + .../tagplugins/jstl/core/Otherwise.java | 32 + .../jasper/tagplugins/jstl/core/Out.java | 127 + .../jasper/tagplugins/jstl/core/Param.java | 76 + .../jasper/tagplugins/jstl/core/Redirect.java | 82 + .../jasper/tagplugins/jstl/core/Remove.java | 44 + .../jasper/tagplugins/jstl/core/Set.java | 178 + .../jasper/tagplugins/jstl/core/Url.java | 100 + .../jasper/tagplugins/jstl/core/When.java | 49 + .../jasper/tagplugins/jstl/tagPlugins.xml | 63 + .../jasper/util/FastRemovalDequeue.java | 326 + .../jasper/util/UniqueAttributesImpl.java | 120 + java/org/apache/juli/AsyncFileHandler.java | 233 + .../apache/juli/ClassLoaderLogManager.java | 761 ++ java/org/apache/juli/DateFormatCache.java | 193 + java/org/apache/juli/FileHandler.java | 559 ++ java/org/apache/juli/JdkLoggerFormatter.java | 120 + java/org/apache/juli/OneLineFormatter.java | 292 + java/org/apache/juli/VerbatimFormatter.java | 35 + java/org/apache/juli/WebappProperties.java | 57 + .../org/apache/juli/logging/DirectJDKLog.java | 185 + java/org/apache/juli/logging/Log.java | 249 + .../logging/LogConfigurationException.java | 71 + java/org/apache/juli/logging/LogFactory.java | 251 + java/org/apache/juli/logging/package.html | 37 + java/org/apache/naming/AbstractRef.java | 87 + .../naming/ContextAccessController.java | 128 + java/org/apache/naming/ContextBindings.java | 308 + java/org/apache/naming/EjbRef.java | 103 + java/org/apache/naming/HandlerRef.java | 110 + .../org/apache/naming/LocalStrings.properties | 30 + .../apache/naming/LocalStrings_cs.properties | 16 + .../apache/naming/LocalStrings_de.properties | 21 + .../apache/naming/LocalStrings_es.properties | 30 + .../apache/naming/LocalStrings_fr.properties | 30 + .../apache/naming/LocalStrings_ja.properties | 30 + .../apache/naming/LocalStrings_ko.properties | 30 + .../apache/naming/LocalStrings_ru.properties | 19 + .../naming/LocalStrings_zh_CN.properties | 30 + java/org/apache/naming/LookupRef.java | 53 + java/org/apache/naming/NameParserImpl.java | 54 + java/org/apache/naming/NamingContext.java | 1009 +++ .../NamingContextBindingsEnumeration.java | 130 + .../naming/NamingContextEnumeration.java | 97 + java/org/apache/naming/NamingEntry.java | 63 + java/org/apache/naming/ResourceEnvRef.java | 50 + java/org/apache/naming/ResourceLinkRef.java | 68 + java/org/apache/naming/ResourceRef.java | 118 + java/org/apache/naming/SelectorContext.java | 797 ++ java/org/apache/naming/ServiceRef.java | 144 + java/org/apache/naming/StringManager.java | 183 + java/org/apache/naming/TransactionRef.java | 59 + .../apache/naming/factory/BeanFactory.java | 233 + java/org/apache/naming/factory/Constants.java | 48 + .../naming/factory/DataSourceLinkFactory.java | 149 + .../org/apache/naming/factory/EjbFactory.java | 78 + .../apache/naming/factory/FactoryBase.java | 141 + .../naming/factory/LocalStrings.properties | 38 + .../naming/factory/LocalStrings_cs.properties | 18 + .../naming/factory/LocalStrings_es.properties | 20 + .../naming/factory/LocalStrings_fr.properties | 38 + .../naming/factory/LocalStrings_ja.properties | 38 + .../naming/factory/LocalStrings_ko.properties | 38 + .../factory/LocalStrings_pt_BR.properties | 16 + .../factory/LocalStrings_zh_CN.properties | 38 + .../apache/naming/factory/LookupFactory.java | 150 + .../naming/factory/MailSessionFactory.java | 153 + .../apache/naming/factory/OpenEjbFactory.java | 89 + .../naming/factory/ResourceEnvFactory.java | 47 + .../naming/factory/ResourceFactory.java | 88 + .../naming/factory/ResourceLinkFactory.java | 170 + .../naming/factory/SendMailFactory.java | 127 + .../naming/factory/TransactionFactory.java | 47 + java/org/apache/naming/factory/package.html | 21 + .../webservices/LocalStrings.properties | 16 + .../webservices/LocalStrings_de.properties | 16 + .../webservices/LocalStrings_fr.properties | 16 + .../webservices/LocalStrings_ja.properties | 16 + .../webservices/LocalStrings_ko.properties | 16 + .../webservices/LocalStrings_zh_CN.properties | 16 + .../factory/webservices/ServiceProxy.java | 148 + .../webservices/ServiceRefFactory.java | 353 + .../naming/java/javaURLContextFactory.java | 120 + java/org/apache/naming/java/package.html | 21 + java/org/apache/naming/package.html | 21 + java/org/apache/tomcat/ContextBind.java | 60 + java/org/apache/tomcat/InstanceManager.java | 51 + .../tomcat/InstanceManagerBindings.java | 35 + .../tomcat/InstrumentableClassLoader.java | 78 + java/org/apache/tomcat/Jar.java | 136 + java/org/apache/tomcat/JarScanFilter.java | 38 + java/org/apache/tomcat/JarScanType.java | 23 + java/org/apache/tomcat/JarScanner.java | 45 + .../org/apache/tomcat/JarScannerCallback.java | 67 + .../apache/tomcat/PeriodicEventListener.java | 24 + .../apache/tomcat/SimpleInstanceManager.java | 68 + .../org/apache/tomcat/buildutil/CheckEol.java | 181 + .../tomcat/buildutil/ForceUtcTimeZone.java | 30 + .../tomcat/buildutil/MimeTypeMappings.java | 70 + .../tomcat/buildutil/RepeatableArchive.java | 133 + .../org/apache/tomcat/buildutil/Txt2Html.java | 176 + java/org/apache/tomcat/buildutil/Utils.java | 59 + .../buildutil/translate/BackportBase.java | 65 + .../buildutil/translate/BackportEnglish.java | 69 + .../translate/BackportTranslations.java | 68 + .../tomcat/buildutil/translate/Constants.java | 32 + .../tomcat/buildutil/translate/Import.java | 111 + .../tomcat/buildutil/translate/Utils.java | 204 + .../tomcat/dbcp/dbcp2/AbandonedTrace.java | 228 + .../tomcat/dbcp/dbcp2/BasicDataSource.java | 2610 +++++++ .../dbcp/dbcp2/BasicDataSourceFactory.java | 453 ++ .../dbcp/dbcp2/BasicDataSourceMXBean.java | 38 + .../tomcat/dbcp/dbcp2/ConnectionFactory.java | 36 + .../dbcp/dbcp2/ConnectionFactoryFactory.java | 76 + .../apache/tomcat/dbcp/dbcp2/Constants.java | 48 + .../dbcp2/DataSourceConnectionFactory.java | 111 + .../tomcat/dbcp/dbcp2/DataSourceMXBean.java | 353 + .../dbcp2/DelegatingCallableStatement.java | 1382 ++++ .../dbcp/dbcp2/DelegatingConnection.java | 1032 +++ .../dbcp2/DelegatingDatabaseMetaData.java | 1938 +++++ .../dbcp2/DelegatingPreparedStatement.java | 718 ++ .../dbcp/dbcp2/DelegatingResultSet.java | 2082 ++++++ .../dbcp/dbcp2/DelegatingStatement.java | 811 +++ .../dbcp/dbcp2/DriverConnectionFactory.java | 84 + .../tomcat/dbcp/dbcp2/DriverFactory.java | 80 + .../dbcp2/DriverManagerConnectionFactory.java | 148 + .../tomcat/dbcp/dbcp2/Jdbc41Bridge.java | 488 ++ .../dbcp/dbcp2/LifetimeExceededException.java | 44 + .../tomcat/dbcp/dbcp2/ListException.java | 56 + .../tomcat/dbcp/dbcp2/LocalStrings.properties | 26 + .../dbcp/dbcp2/LocalStrings_de.properties | 16 + .../dbcp/dbcp2/LocalStrings_fr.properties | 26 + .../dbcp/dbcp2/LocalStrings_ja.properties | 26 + .../dbcp/dbcp2/LocalStrings_ko.properties | 26 + .../dbcp/dbcp2/LocalStrings_zh_CN.properties | 26 + .../tomcat/dbcp/dbcp2/ObjectNameWrapper.java | 104 + .../apache/tomcat/dbcp/dbcp2/PStmtKey.java | 681 ++ .../dbcp/dbcp2/PoolableCallableStatement.java | 111 + .../tomcat/dbcp/dbcp2/PoolableConnection.java | 402 ++ .../dbcp/dbcp2/PoolableConnectionFactory.java | 766 ++ .../dbcp/dbcp2/PoolableConnectionMXBean.java | 67 + .../dbcp/dbcp2/PoolablePreparedStatement.java | 125 + .../tomcat/dbcp/dbcp2/PoolingConnection.java | 615 ++ .../tomcat/dbcp/dbcp2/PoolingDataSource.java | 256 + .../tomcat/dbcp/dbcp2/PoolingDriver.java | 260 + .../tomcat/dbcp/dbcp2/SQLExceptionList.java | 56 + .../dbcp/dbcp2/SwallowedExceptionLogger.java | 61 + java/org/apache/tomcat/dbcp/dbcp2/Utils.java | 257 + .../dbcp2/cpdsadapter/ConnectionImpl.java | 307 + .../dbcp2/cpdsadapter/DriverAdapterCPDS.java | 814 +++ .../dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java | 113 + .../cpdsadapter/PooledConnectionImpl.java | 730 ++ .../dbcp/dbcp2/cpdsadapter/package-info.java | 79 + .../datasources/CPDSConnectionFactory.java | 484 ++ .../dbcp/dbcp2/datasources/CharArray.java | 84 + .../datasources/InstanceKeyDataSource.java | 1327 ++++ .../InstanceKeyDataSourceFactory.java | 340 + .../KeyedCPDSConnectionFactory.java | 380 + .../datasources/PerUserPoolDataSource.java | 1197 +++ .../PerUserPoolDataSourceFactory.java | 95 + .../dbcp/dbcp2/datasources/PoolKey.java | 67 + .../datasources/PooledConnectionAndInfo.java | 77 + .../datasources/PooledConnectionManager.java | 68 + .../datasources/SharedPoolDataSource.java | 230 + .../SharedPoolDataSourceFactory.java | 44 + .../dbcp/dbcp2/datasources/UserPassKey.java | 119 + .../dbcp/dbcp2/datasources/package-info.java | 183 + .../dbcp2/managed/BasicManagedDataSource.java | 256 + .../DataSourceXAConnectionFactory.java | 259 + .../managed/LocalXAConnectionFactory.java | 384 + .../dbcp/dbcp2/managed/ManagedConnection.java | 327 + .../dbcp/dbcp2/managed/ManagedDataSource.java | 92 + .../managed/PoolableManagedConnection.java | 91 + .../PoolableManagedConnectionFactory.java | 110 + .../dbcp2/managed/SynchronizationAdapter.java | 36 + .../dbcp2/managed/TransactionContext.java | 205 + .../managed/TransactionContextListener.java | 34 + .../dbcp2/managed/TransactionRegistry.java | 153 + .../dbcp2/managed/XAConnectionFactory.java | 54 + .../dbcp/dbcp2/managed/package-info.java | 40 + .../tomcat/dbcp/dbcp2/package-info.java | 131 + .../apache/tomcat/dbcp/pool2/BaseObject.java | 45 + .../tomcat/dbcp/pool2/BaseObjectPool.java | 127 + .../apache/tomcat/dbcp/pool2/DestroyMode.java | 32 + .../tomcat/dbcp/pool2/KeyedObjectPool.java | 334 + .../dbcp/pool2/KeyedPooledObjectFactory.java | 172 + .../apache/tomcat/dbcp/pool2/ObjectPool.java | 224 + .../apache/tomcat/dbcp/pool2/PoolUtils.java | 1848 +++++ .../tomcat/dbcp/pool2/PooledObject.java | 358 + .../dbcp/pool2/PooledObjectFactory.java | 162 + .../tomcat/dbcp/pool2/PooledObjectState.java | 83 + .../pool2/SwallowedExceptionListener.java | 36 + .../apache/tomcat/dbcp/pool2/TrackedUse.java | 52 + .../tomcat/dbcp/pool2/UsageTracking.java | 36 + .../dbcp/pool2/impl/AbandonedConfig.java | 366 + .../pool2/impl/BaseGenericObjectPool.java | 2082 ++++++ .../dbcp/pool2/impl/BaseObjectPoolConfig.java | 959 +++ .../tomcat/dbcp/pool2/impl/CallStack.java | 54 + .../dbcp/pool2/impl/CallStackUtils.java | 85 + .../pool2/impl/DefaultEvictionPolicy.java | 54 + .../dbcp/pool2/impl/DefaultPooledObject.java | 343 + .../pool2/impl/DefaultPooledObjectInfo.java | 117 + .../impl/DefaultPooledObjectInfoMBean.java | 106 + .../dbcp/pool2/impl/EvictionConfig.java | 198 + .../dbcp/pool2/impl/EvictionPolicy.java | 42 + .../tomcat/dbcp/pool2/impl/EvictionTimer.java | 233 + .../pool2/impl/GenericKeyedObjectPool.java | 1755 +++++ .../impl/GenericKeyedObjectPoolConfig.java | 198 + .../impl/GenericKeyedObjectPoolMXBean.java | 323 + .../dbcp/pool2/impl/GenericObjectPool.java | 1195 +++ .../pool2/impl/GenericObjectPoolConfig.java | 158 + .../pool2/impl/GenericObjectPoolMXBean.java | 294 + .../impl/InterruptibleReentrantLock.java | 54 + .../dbcp/pool2/impl/LinkedBlockingDeque.java | 1477 ++++ .../tomcat/dbcp/pool2/impl/NoOpCallStack.java | 54 + .../tomcat/dbcp/pool2/impl/PoolImplUtils.java | 233 + .../dbcp/pool2/impl/PooledSoftReference.java | 96 + .../pool2/impl/SecurityManagerCallStack.java | 122 + .../pool2/impl/SoftReferenceObjectPool.java | 447 ++ .../dbcp/pool2/impl/ThrowableCallStack.java | 87 + java/org/apache/tomcat/jni/Buffer.java | 35 + .../tomcat/jni/CertificateVerifier.java | 34 + java/org/apache/tomcat/jni/FileInfo.java | 30 + java/org/apache/tomcat/jni/Library.java | 167 + .../tomcat/jni/LibraryNotFoundError.java | 39 + java/org/apache/tomcat/jni/Pool.java | 44 + java/org/apache/tomcat/jni/SSL.java | 599 ++ java/org/apache/tomcat/jni/SSLConf.java | 110 + java/org/apache/tomcat/jni/SSLContext.java | 440 ++ java/org/apache/tomcat/jni/Sockaddr.java | 30 + java/org/apache/tomcat/util/Diagnostics.java | 699 ++ .../apache/tomcat/util/ExceptionUtils.java | 71 + .../tomcat/util/IntrospectionUtils.java | 622 ++ .../tomcat/util/LocalStrings.properties | 40 + .../tomcat/util/LocalStrings_cs.properties | 18 + .../tomcat/util/LocalStrings_de.properties | 19 + .../tomcat/util/LocalStrings_es.properties | 19 + .../tomcat/util/LocalStrings_fr.properties | 40 + .../tomcat/util/LocalStrings_ja.properties | 40 + .../tomcat/util/LocalStrings_ko.properties | 35 + .../tomcat/util/LocalStrings_pt_BR.properties | 17 + .../tomcat/util/LocalStrings_ru.properties | 17 + .../tomcat/util/LocalStrings_zh_CN.properties | 35 + .../apache/tomcat/util/MultiThrowable.java | 99 + .../util/XReflectionIntrospectionUtils.java | 51 + java/org/apache/tomcat/util/bcel/Const.java | 232 + .../classfile/AnnotationElementValue.java | 39 + .../util/bcel/classfile/AnnotationEntry.java | 70 + .../util/bcel/classfile/Annotations.java | 52 + .../bcel/classfile/ArrayElementValue.java | 49 + .../bcel/classfile/ClassElementValue.java | 35 + .../bcel/classfile/ClassFormatException.java | 64 + .../util/bcel/classfile/ClassParser.java | 264 + .../tomcat/util/bcel/classfile/Constant.java | 102 + .../util/bcel/classfile/ConstantClass.java | 52 + .../util/bcel/classfile/ConstantDouble.java | 52 + .../util/bcel/classfile/ConstantFloat.java | 52 + .../util/bcel/classfile/ConstantInteger.java | 52 + .../util/bcel/classfile/ConstantLong.java | 52 + .../util/bcel/classfile/ConstantPool.java | 154 + .../util/bcel/classfile/ConstantUtf8.java | 62 + .../util/bcel/classfile/ElementValue.java | 151 + .../util/bcel/classfile/ElementValuePair.java | 50 + .../util/bcel/classfile/EnumElementValue.java | 34 + .../tomcat/util/bcel/classfile/JavaClass.java | 131 + .../bcel/classfile/SimpleElementValue.java | 75 + .../tomcat/util/bcel/classfile/Utility.java | 64 + .../util/bcel/classfile/package-info.java | 21 + .../apache/tomcat/util/bcel/package-info.java | 22 + .../apache/tomcat/util/buf/AbstractChunk.java | 182 + java/org/apache/tomcat/util/buf/Ascii.java | 106 + .../apache/tomcat/util/buf/Asn1Parser.java | 181 + .../apache/tomcat/util/buf/Asn1Writer.java | 94 + .../apache/tomcat/util/buf/B2CConverter.java | 259 + .../tomcat/util/buf/ByteBufferHolder.java | 54 + .../tomcat/util/buf/ByteBufferUtils.java | 125 + .../org/apache/tomcat/util/buf/ByteChunk.java | 910 +++ .../apache/tomcat/util/buf/C2BConverter.java | 206 + .../org/apache/tomcat/util/buf/CharChunk.java | 649 ++ .../apache/tomcat/util/buf/CharsetCache.java | 235 + .../apache/tomcat/util/buf/CharsetUtil.java | 58 + .../util/buf/EncodedSolidusHandling.java | 51 + java/org/apache/tomcat/util/buf/HexUtils.java | 124 + .../tomcat/util/buf/LocalStrings.properties | 42 + .../util/buf/LocalStrings_cs.properties | 16 + .../util/buf/LocalStrings_de.properties | 16 + .../util/buf/LocalStrings_es.properties | 18 + .../util/buf/LocalStrings_fr.properties | 42 + .../util/buf/LocalStrings_ja.properties | 42 + .../util/buf/LocalStrings_ko.properties | 35 + .../util/buf/LocalStrings_pt_BR.properties | 16 + .../util/buf/LocalStrings_zh_CN.properties | 35 + .../apache/tomcat/util/buf/MessageBytes.java | 656 ++ .../apache/tomcat/util/buf/StringCache.java | 739 ++ .../apache/tomcat/util/buf/StringUtils.java | 121 + java/org/apache/tomcat/util/buf/UDecoder.java | 255 + java/org/apache/tomcat/util/buf/UEncoder.java | 163 + java/org/apache/tomcat/util/buf/UriUtil.java | 255 + .../apache/tomcat/util/buf/Utf8Encoder.java | 234 + java/org/apache/tomcat/util/buf/package.html | 37 + .../tomcat/util/codec/binary/Base64.java | 661 ++ .../tomcat/util/codec/binary/BaseNCodec.java | 478 ++ .../util/codec/binary/LocalStrings.properties | 20 + .../codec/binary/LocalStrings_fr.properties | 20 + .../codec/binary/LocalStrings_ja.properties | 20 + .../codec/binary/LocalStrings_ko.properties | 19 + .../binary/LocalStrings_zh_CN.properties | 19 + .../tomcat/util/codec/binary/StringUtils.java | 116 + .../util/codec/binary/package-info.java | 21 + .../collections/CaseInsensitiveKeyMap.java | 208 + .../util/collections/ConcurrentCache.java | 59 + .../ManagedConcurrentWeakHashMap.java | 271 + .../util/collections/SynchronizedQueue.java | 105 + .../util/collections/SynchronizedStack.java | 105 + .../tomcat/util/compat/Jre16Compat.java | 100 + .../tomcat/util/compat/Jre19Compat.java | 84 + .../tomcat/util/compat/Jre21Compat.java | 82 + .../tomcat/util/compat/Jre22Compat.java | 49 + .../apache/tomcat/util/compat/JreCompat.java | 231 + .../tomcat/util/compat/JrePlatform.java | 57 + .../apache/tomcat/util/compat/JreVendor.java | 49 + .../util/compat/LocalStrings.properties | 28 + .../util/compat/LocalStrings_fr.properties | 24 + .../util/compat/LocalStrings_ja.properties | 24 + .../util/compat/LocalStrings_ko.properties | 21 + .../util/compat/LocalStrings_zh_CN.properties | 21 + .../tomcat/util/descriptor/Constants.java | 25 + .../util/descriptor/DigesterFactory.java | 212 + .../util/descriptor/InputSourceUtil.java | 47 + .../tomcat/util/descriptor/LocalResolver.java | 162 + .../util/descriptor/LocalStrings.properties | 21 + .../descriptor/LocalStrings_es.properties | 17 + .../descriptor/LocalStrings_fr.properties | 21 + .../descriptor/LocalStrings_ja.properties | 21 + .../descriptor/LocalStrings_ko.properties | 21 + .../descriptor/LocalStrings_zh_CN.properties | 21 + .../util/descriptor/XmlErrorHandler.java | 75 + .../util/descriptor/XmlIdentifiers.java | 96 + .../descriptor/tagplugin/TagPluginParser.java | 89 + .../descriptor/tld/ImplicitTldRuleSet.java | 86 + .../descriptor/tld/LocalStrings.properties | 16 + .../descriptor/tld/LocalStrings_fr.properties | 16 + .../descriptor/tld/LocalStrings_ja.properties | 16 + .../descriptor/tld/LocalStrings_ko.properties | 16 + .../tld/LocalStrings_zh_CN.properties | 16 + .../util/descriptor/tld/TagFileXml.java | 79 + .../tomcat/util/descriptor/tld/TagXml.java | 124 + .../tomcat/util/descriptor/tld/TaglibXml.java | 124 + .../tomcat/util/descriptor/tld/TldParser.java | 100 + .../util/descriptor/tld/TldResourcePath.java | 171 + .../util/descriptor/tld/TldRuleSet.java | 409 ++ .../util/descriptor/tld/ValidatorXml.java | 44 + .../util/descriptor/tld/package-info.java | 21 + .../descriptor/web/ApplicationParameter.java | 120 + .../tomcat/util/descriptor/web/Constants.java | 41 + .../util/descriptor/web/ContextEjb.java | 161 + .../descriptor/web/ContextEnvironment.java | 130 + .../util/descriptor/web/ContextHandler.java | 214 + .../util/descriptor/web/ContextLocalEjb.java | 160 + .../util/descriptor/web/ContextResource.java | 188 + .../descriptor/web/ContextResourceEnvRef.java | 97 + .../descriptor/web/ContextResourceLink.java | 122 + .../util/descriptor/web/ContextService.java | 357 + .../descriptor/web/ContextTransaction.java | 87 + .../tomcat/util/descriptor/web/ErrorPage.java | 163 + .../tomcat/util/descriptor/web/FilterDef.java | 211 + .../tomcat/util/descriptor/web/FilterMap.java | 225 + .../web/FragmentJarScannerCallback.java | 161 + .../util/descriptor/web/Injectable.java | 25 + .../util/descriptor/web/InjectionTarget.java | 54 + .../web/JspConfigDescriptorImpl.java | 46 + .../util/descriptor/web/JspPropertyGroup.java | 115 + .../web/JspPropertyGroupDescriptorImpl.java | 157 + .../descriptor/web/LocalStrings.properties | 62 + .../descriptor/web/LocalStrings_cs.properties | 21 + .../descriptor/web/LocalStrings_de.properties | 20 + .../descriptor/web/LocalStrings_es.properties | 31 + .../descriptor/web/LocalStrings_fr.properties | 62 + .../descriptor/web/LocalStrings_ja.properties | 62 + .../descriptor/web/LocalStrings_ko.properties | 61 + .../web/LocalStrings_pt_BR.properties | 16 + .../descriptor/web/LocalStrings_ru.properties | 20 + .../web/LocalStrings_zh_CN.properties | 61 + .../util/descriptor/web/LoginConfig.java | 226 + .../descriptor/web/MessageDestination.java | 158 + .../descriptor/web/MessageDestinationRef.java | 132 + .../util/descriptor/web/MultipartDef.java | 136 + .../util/descriptor/web/NamingResources.java | 40 + .../util/descriptor/web/ResourceBase.java | 234 + .../descriptor/web/SecurityCollection.java | 407 ++ .../descriptor/web/SecurityConstraint.java | 784 ++ .../util/descriptor/web/SecurityRoleRef.java | 85 + .../util/descriptor/web/ServletDef.java | 274 + .../util/descriptor/web/SessionConfig.java | 123 + .../descriptor/web/TaglibDescriptorImpl.java | 77 + .../util/descriptor/web/WebRuleSet.java | 1511 ++++ .../tomcat/util/descriptor/web/WebXml.java | 2362 ++++++ .../util/descriptor/web/WebXmlParser.java | 155 + .../util/descriptor/web/XmlEncodingBase.java | 46 + .../descriptor/web/mbeans-descriptors.xml | 113 + .../tomcat/util/descriptor/web/package.html | 26 + .../AbstractObjectCreationFactory.java | 78 + .../tomcat/util/digester/ArrayStack.java | 141 + .../tomcat/util/digester/CallMethodRule.java | 447 ++ .../tomcat/util/digester/CallParamRule.java | 206 + .../apache/tomcat/util/digester/Digester.java | 2110 ++++++ .../util/digester/DocumentProperties.java | 33 + .../digester/EnvironmentPropertySource.java | 78 + .../util/digester/FactoryCreateRule.java | 177 + .../util/digester/LocalStrings.properties | 37 + .../util/digester/LocalStrings_de.properties | 16 + .../util/digester/LocalStrings_es.properties | 17 + .../util/digester/LocalStrings_fr.properties | 37 + .../util/digester/LocalStrings_ja.properties | 37 + .../util/digester/LocalStrings_ko.properties | 36 + .../util/digester/LocalStrings_ru.properties | 16 + .../digester/LocalStrings_zh_CN.properties | 36 + .../util/digester/ObjectCreateRule.java | 170 + .../util/digester/ObjectCreationFactory.java | 58 + .../org/apache/tomcat/util/digester/Rule.java | 164 + .../apache/tomcat/util/digester/RuleSet.java | 47 + .../apache/tomcat/util/digester/Rules.java | 87 + .../tomcat/util/digester/RulesBase.java | 208 + .../ServiceBindingPropertySource.java | 157 + .../tomcat/util/digester/SetNextRule.java | 170 + .../util/digester/SetPropertiesRule.java | 128 + .../util/digester/SystemPropertySource.java | 51 + .../apache/tomcat/util/digester/package.html | 1188 +++ .../tomcat/util/file/ConfigFileLoader.java | 59 + .../tomcat/util/file/ConfigurationSource.java | 170 + java/org/apache/tomcat/util/file/Matcher.java | 240 + java/org/apache/tomcat/util/file/package.html | 26 + .../util/http/ConcurrentDateFormat.java | 72 + .../tomcat/util/http/CookieProcessor.java | 55 + .../tomcat/util/http/CookieProcessorBase.java | 84 + .../tomcat/util/http/FastHttpDateFormat.java | 183 + .../apache/tomcat/util/http/HeaderUtil.java | 51 + .../tomcat/util/http/LocalStrings.properties | 45 + .../util/http/LocalStrings_cs.properties | 27 + .../util/http/LocalStrings_de.properties | 27 + .../util/http/LocalStrings_es.properties | 27 + .../util/http/LocalStrings_fr.properties | 45 + .../util/http/LocalStrings_ja.properties | 45 + .../util/http/LocalStrings_ko.properties | 43 + .../util/http/LocalStrings_pt_BR.properties | 17 + .../util/http/LocalStrings_ru.properties | 18 + .../util/http/LocalStrings_zh_CN.properties | 43 + .../apache/tomcat/util/http/MimeHeaders.java | 524 ++ .../apache/tomcat/util/http/Parameters.java | 517 ++ .../apache/tomcat/util/http/RequestUtil.java | 210 + .../apache/tomcat/util/http/ResponseUtil.java | 172 + .../util/http/Rfc6265CookieProcessor.java | 291 + .../tomcat/util/http/SameSiteCookies.java | 64 + .../apache/tomcat/util/http/ServerCookie.java | 64 + .../tomcat/util/http/ServerCookies.java | 94 + .../fileupload/ByteArrayOutputStream.java | 227 + .../fileupload/DeferredFileOutputStream.java | 227 + .../tomcat/util/http/fileupload/FileItem.java | 208 + .../util/http/fileupload/FileItemFactory.java | 46 + .../util/http/fileupload/FileItemHeaders.java | 74 + .../fileupload/FileItemHeadersSupport.java | 48 + .../http/fileupload/FileItemIterator.java | 101 + .../util/http/fileupload/FileItemStream.java | 102 + .../util/http/fileupload/FileUpload.java | 77 + .../util/http/fileupload/FileUploadBase.java | 532 ++ .../http/fileupload/FileUploadException.java | 55 + .../util/http/fileupload/FileUtils.java | 309 + .../tomcat/util/http/fileupload/IOUtils.java | 249 + .../fileupload/InvalidFileNameException.java | 62 + .../util/http/fileupload/MultipartStream.java | 987 +++ .../util/http/fileupload/ParameterParser.java | 340 + .../http/fileupload/ProgressListener.java | 37 + .../util/http/fileupload/RequestContext.java | 54 + .../fileupload/ThresholdingOutputStream.java | 226 + .../util/http/fileupload/UploadContext.java | 39 + .../http/fileupload/disk/DiskFileItem.java | 625 ++ .../fileupload/disk/DiskFileItemFactory.java | 205 + .../http/fileupload/disk/package-info.java | 51 + .../impl/FileCountLimitExceededException.java | 50 + .../fileupload/impl/FileItemIteratorImpl.java | 351 + .../fileupload/impl/FileItemStreamImpl.java | 218 + .../impl/FileSizeLimitExceededException.java | 94 + .../impl/FileUploadIOException.java | 63 + .../impl/IOFileUploadException.java | 62 + .../impl/InvalidContentTypeException.java | 61 + .../http/fileupload/impl/SizeException.java | 75 + .../impl/SizeLimitExceededException.java | 43 + .../http/fileupload/impl/package-info.java | 21 + .../util/http/fileupload/package-info.java | 86 + .../servlet/ServletRequestContext.java | 115 + .../http/fileupload/servlet/package-info.java | 45 + .../util/http/fileupload/util/Closeable.java | 41 + .../fileupload/util/FileItemHeadersImpl.java | 86 + .../fileupload/util/LimitedInputStream.java | 166 + .../util/http/fileupload/util/Streams.java | 150 + .../fileupload/util/mime/MimeUtility.java | 279 + .../fileupload/util/mime/ParseException.java | 38 + .../util/mime/QuotedPrintableDecoder.java | 112 + .../fileupload/util/mime/RFC2231Utility.java | 154 + .../fileupload/util/mime/package-info.java | 22 + .../http/fileupload/util/package-info.java | 23 + java/org/apache/tomcat/util/http/package.html | 30 + .../util/http/parser/AcceptEncoding.java | 74 + .../util/http/parser/AcceptLanguage.java | 78 + .../util/http/parser/Authorization.java | 136 + .../tomcat/util/http/parser/ContentRange.java | 108 + .../tomcat/util/http/parser/Cookie.java | 304 + .../tomcat/util/http/parser/EntityTag.java | 93 + .../apache/tomcat/util/http/parser/Host.java | 140 + .../tomcat/util/http/parser/HttpParser.java | 1049 +++ .../util/http/parser/LocalStrings.properties | 60 + .../http/parser/LocalStrings_cs.properties | 20 + .../http/parser/LocalStrings_de.properties | 24 + .../http/parser/LocalStrings_es.properties | 22 + .../http/parser/LocalStrings_fr.properties | 60 + .../http/parser/LocalStrings_ja.properties | 60 + .../http/parser/LocalStrings_ko.properties | 60 + .../http/parser/LocalStrings_pt_BR.properties | 16 + .../http/parser/LocalStrings_ru.properties | 20 + .../http/parser/LocalStrings_zh_CN.properties | 55 + .../tomcat/util/http/parser/MediaType.java | 172 + .../util/http/parser/MediaTypeCache.java | 64 + .../tomcat/util/http/parser/Priority.java | 92 + .../tomcat/util/http/parser/Ranges.java | 125 + .../tomcat/util/http/parser/SkipResult.java | 23 + .../util/http/parser/StructuredField.java | 598 ++ .../tomcat/util/http/parser/TokenList.java | 113 + .../tomcat/util/http/parser/Upgrade.java | 106 + .../apache/tomcat/util/json/JSONFilter.java | 140 + .../apache/tomcat/util/json/JSONParser.java | 655 ++ .../apache/tomcat/util/json/JSONParser.jjt | 384 + .../tomcat/util/json/JSONParserConstants.java | 122 + .../util/json/JSONParserTokenManager.java | 869 +++ .../tomcat/util/json/JavaCharStream.java | 641 ++ .../tomcat/util/json/LocalStrings.properties | 16 + .../util/json/LocalStrings_de.properties | 16 + .../util/json/LocalStrings_fr.properties | 16 + .../util/json/LocalStrings_ja.properties | 16 + .../util/json/LocalStrings_ko.properties | 16 + .../util/json/LocalStrings_ru.properties | 16 + .../util/json/LocalStrings_zh_CN.properties | 16 + .../tomcat/util/json/ParseException.java | 212 + java/org/apache/tomcat/util/json/Token.java | 147 + .../tomcat/util/json/TokenMgrError.java | 163 + .../apache/tomcat/util/log/CaptureLog.java | 49 + .../tomcat/util/log/SystemLogHandler.java | 278 + .../tomcat/util/log/UserDataHelper.java | 149 + .../tomcat/util/modeler/AttributeInfo.java | 167 + .../util/modeler/BaseAttributeFilter.java | 164 + .../tomcat/util/modeler/BaseModelMBean.java | 952 +++ .../modeler/BaseNotificationBroadcaster.java | 184 + .../tomcat/util/modeler/FeatureInfo.java | 81 + .../util/modeler/LocalStrings.properties | 52 + .../util/modeler/LocalStrings_fr.properties | 52 + .../util/modeler/LocalStrings_ja.properties | 52 + .../util/modeler/LocalStrings_ko.properties | 48 + .../modeler/LocalStrings_zh_CN.properties | 48 + .../tomcat/util/modeler/ManagedBean.java | 581 ++ .../util/modeler/NoDescriptorRegistry.java | 407 ++ .../tomcat/util/modeler/NotificationInfo.java | 157 + .../tomcat/util/modeler/OperationInfo.java | 175 + .../tomcat/util/modeler/ParameterInfo.java | 55 + .../apache/tomcat/util/modeler/Registry.java | 749 ++ .../tomcat/util/modeler/RegistryMBean.java | 118 + java/org/apache/tomcat/util/modeler/Util.java | 34 + .../util/modeler/mbeans-descriptors.dtd | 246 + .../modeler/modules/LocalStrings.properties | 16 + .../modules/LocalStrings_fr.properties | 16 + .../modules/LocalStrings_ja.properties | 16 + .../MbeansDescriptorsDigesterSource.java | 182 + .../MbeansDescriptorsIntrospectionSource.java | 390 + .../util/modeler/modules/ModelerSource.java | 44 + .../tomcat/util/modeler/modules/package.html | 59 + .../apache/tomcat/util/modeler/package.html | 248 + .../tomcat/util/net/AbstractEndpoint.java | 1593 ++++ .../tomcat/util/net/AbstractJsseEndpoint.java | 240 + java/org/apache/tomcat/util/net/Acceptor.java | 223 + .../util/net/ApplicationBufferHandler.java | 48 + .../org/apache/tomcat/util/net/Constants.java | 43 + .../apache/tomcat/util/net/DispatchType.java | 38 + .../org/apache/tomcat/util/net/IPv6Utils.java | 252 + .../tomcat/util/net/LocalStrings.properties | 173 + .../util/net/LocalStrings_cs.properties | 38 + .../util/net/LocalStrings_de.properties | 41 + .../util/net/LocalStrings_es.properties | 68 + .../util/net/LocalStrings_fr.properties | 174 + .../util/net/LocalStrings_ja.properties | 174 + .../util/net/LocalStrings_ko.properties | 166 + .../util/net/LocalStrings_pt_BR.properties | 21 + .../util/net/LocalStrings_ru.properties | 30 + .../util/net/LocalStrings_zh_CN.properties | 168 + .../apache/tomcat/util/net/Nio2Channel.java | 308 + .../apache/tomcat/util/net/Nio2Endpoint.java | 1739 +++++ .../apache/tomcat/util/net/NioChannel.java | 277 + .../apache/tomcat/util/net/NioEndpoint.java | 1782 +++++ .../apache/tomcat/util/net/SSLContext.java | 53 + .../apache/tomcat/util/net/SSLHostConfig.java | 855 +++ .../util/net/SSLHostConfigCertificate.java | 347 + .../tomcat/util/net/SSLImplementation.java | 82 + .../tomcat/util/net/SSLSessionManager.java | 28 + .../apache/tomcat/util/net/SSLSupport.java | 164 + java/org/apache/tomcat/util/net/SSLUtil.java | 82 + .../apache/tomcat/util/net/SSLUtilBase.java | 586 ++ .../tomcat/util/net/SecureNio2Channel.java | 1296 ++++ .../tomcat/util/net/SecureNioChannel.java | 904 +++ .../tomcat/util/net/SendfileDataBase.java | 54 + .../util/net/SendfileKeepAliveState.java | 39 + .../apache/tomcat/util/net/SendfileState.java | 37 + .../util/net/ServletConnectionImpl.java | 55 + .../tomcat/util/net/SocketBufferHandler.java | 234 + .../apache/tomcat/util/net/SocketEvent.java | 73 + .../tomcat/util/net/SocketProcessorBase.java | 60 + .../tomcat/util/net/SocketProperties.java | 503 ++ .../tomcat/util/net/SocketWrapperBase.java | 1521 ++++ .../util/net/TLSClientHelloExtractor.java | 444 ++ .../apache/tomcat/util/net/WriteBuffer.java | 147 + .../util/net/jsse/JSSEImplementation.java | 54 + .../tomcat/util/net/jsse/JSSEKeyManager.java | 122 + .../tomcat/util/net/jsse/JSSESSLContext.java | 106 + .../tomcat/util/net/jsse/JSSESupport.java | 228 + .../apache/tomcat/util/net/jsse/JSSEUtil.java | 157 + .../util/net/jsse/LocalStrings.properties | 30 + .../util/net/jsse/LocalStrings_cs.properties | 16 + .../util/net/jsse/LocalStrings_de.properties | 19 + .../util/net/jsse/LocalStrings_es.properties | 18 + .../util/net/jsse/LocalStrings_fr.properties | 30 + .../util/net/jsse/LocalStrings_ja.properties | 30 + .../util/net/jsse/LocalStrings_ko.properties | 24 + .../net/jsse/LocalStrings_pt_BR.properties | 16 + .../util/net/jsse/LocalStrings_ru.properties | 18 + .../net/jsse/LocalStrings_zh_CN.properties | 24 + .../apache/tomcat/util/net/jsse/PEMFile.java | 695 ++ .../tomcat/util/net/mbeans-descriptors.xml | 473 ++ .../util/net/openssl/LocalStrings.properties | 67 + .../net/openssl/LocalStrings_cs.properties | 20 + .../net/openssl/LocalStrings_de.properties | 33 + .../net/openssl/LocalStrings_es.properties | 27 + .../net/openssl/LocalStrings_fr.properties | 67 + .../net/openssl/LocalStrings_ja.properties | 67 + .../net/openssl/LocalStrings_ko.properties | 64 + .../net/openssl/LocalStrings_pt_BR.properties | 17 + .../net/openssl/LocalStrings_ru.properties | 20 + .../net/openssl/LocalStrings_zh_CN.properties | 64 + .../tomcat/util/net/openssl/OpenSSLConf.java | 37 + .../util/net/openssl/OpenSSLConfCmd.java | 43 + .../util/net/openssl/OpenSSLContext.java | 662 ++ .../util/net/openssl/OpenSSLEngine.java | 1473 ++++ .../net/openssl/OpenSSLImplementation.java | 41 + .../net/openssl/OpenSSLSessionContext.java | 148 + .../util/net/openssl/OpenSSLSessionStats.java | 126 + .../util/net/openssl/OpenSSLStatus.java | 69 + .../tomcat/util/net/openssl/OpenSSLUtil.java | 138 + .../net/openssl/OpenSSLX509Certificate.java | 192 + .../net/openssl/ciphers/Authentication.java | 33 + .../util/net/openssl/ciphers/Cipher.java | 5117 +++++++++++++ .../util/net/openssl/ciphers/Encryption.java | 42 + .../net/openssl/ciphers/EncryptionLevel.java | 27 + .../util/net/openssl/ciphers/KeyExchange.java | 36 + .../openssl/ciphers/LocalStrings.properties | 18 + .../ciphers/LocalStrings_de.properties | 16 + .../ciphers/LocalStrings_es.properties | 16 + .../ciphers/LocalStrings_fr.properties | 18 + .../ciphers/LocalStrings_ja.properties | 18 + .../ciphers/LocalStrings_ko.properties | 17 + .../ciphers/LocalStrings_zh_CN.properties | 17 + .../net/openssl/ciphers/MessageDigest.java | 27 + .../OpenSSLCipherConfigurationParser.java | 910 +++ .../util/net/openssl/ciphers/Protocol.java | 43 + .../openssl/panama/LocalStrings.properties | 101 + .../openssl/panama/LocalStrings_fr.properties | 98 + .../openssl/panama/LocalStrings_ja.properties | 98 + .../net/openssl/panama/OpenSSLContext.java | 1421 ++++ .../net/openssl/panama/OpenSSLEngine.java | 1697 +++++ .../openssl/panama/OpenSSLImplementation.java | 42 + .../net/openssl/panama/OpenSSLLibrary.java | 447 ++ .../openssl/panama/OpenSSLSessionContext.java | 155 + .../openssl/panama/OpenSSLSessionStats.java | 128 + .../util/net/openssl/panama/OpenSSLUtil.java | 108 + .../panama/OpenSSLX509Certificate.java | 192 + .../SSL_CTX_set_alpn_select_cb$cb.java | 70 + .../SSL_CTX_set_cert_verify_callback$cb.java | 66 + .../SSL_CTX_set_tmp_dh_callback$dh.java | 67 + .../openssl/SSL_CTX_set_verify$callback.java | 66 + .../openssl/SSL_set_info_callback$cb.java | 66 + .../util/openssl/SSL_set_verify$callback.java | 66 + .../apache/tomcat/util/openssl/openssl_h.java | 6404 +++++++++++++++++ .../util/openssl/openssl_h_Compatibility.java | 120 + .../tomcat/util/openssl/openssl_h_Macros.java | 293 + .../tomcat/util/openssl/pem_password_cb.java | 81 + .../apache/tomcat/util/res/StringManager.java | 269 + .../util/scan/AbstractInputStreamJar.java | 284 + .../apache/tomcat/util/scan/Constants.java | 36 + .../apache/tomcat/util/scan/JarFactory.java | 71 + .../tomcat/util/scan/JarFileUrlJar.java | 224 + .../tomcat/util/scan/JarFileUrlNestedJar.java | 65 + .../tomcat/util/scan/LocalStrings.properties | 27 + .../util/scan/LocalStrings_cs.properties | 19 + .../util/scan/LocalStrings_de.properties | 19 + .../util/scan/LocalStrings_es.properties | 21 + .../util/scan/LocalStrings_fr.properties | 27 + .../util/scan/LocalStrings_ja.properties | 27 + .../util/scan/LocalStrings_ko.properties | 27 + .../util/scan/LocalStrings_pt_BR.properties | 17 + .../util/scan/LocalStrings_ru.properties | 16 + .../util/scan/LocalStrings_zh_CN.properties | 27 + .../util/scan/NonClosingJarInputStream.java | 48 + .../tomcat/util/scan/ReferenceCountedJar.java | 146 + .../util/scan/StandardJarScanFilter.java | 261 + .../tomcat/util/scan/StandardJarScanner.java | 516 ++ java/org/apache/tomcat/util/scan/UrlJar.java | 49 + java/org/apache/tomcat/util/scan/package.html | 27 + .../security/ConcurrentMessageDigest.java | 127 + .../apache/tomcat/util/security/Escape.java | 165 + .../tomcat/util/security/KeyStoreUtil.java | 72 + .../util/security/LocalStrings.properties | 19 + .../util/security/LocalStrings_fr.properties | 19 + .../util/security/LocalStrings_ja.properties | 19 + .../util/security/LocalStrings_ko.properties | 19 + .../security/LocalStrings_zh_CN.properties | 19 + .../tomcat/util/security/MD5Encoder.java | 70 + .../tomcat/util/security/PermissionCheck.java | 43 + .../util/security/PrivilegedGetTccl.java | 37 + .../PrivilegedSetAccessControlContext.java | 67 + .../util/security/PrivilegedSetTccl.java | 42 + .../apache/tomcat/util/threads/Constants.java | 30 + .../util/threads/InlineExecutorService.java | 84 + .../tomcat/util/threads/LimitLatch.java | 169 + .../util/threads/LocalStrings.properties | 25 + .../util/threads/LocalStrings_es.properties | 16 + .../util/threads/LocalStrings_fr.properties | 25 + .../util/threads/LocalStrings_ja.properties | 25 + .../util/threads/LocalStrings_ko.properties | 19 + .../threads/LocalStrings_zh_CN.properties | 19 + .../util/threads/ResizableExecutor.java | 44 + .../threads/ScheduledThreadPoolExecutor.java | 142 + .../threads/StopPooledThreadException.java | 31 + .../apache/tomcat/util/threads/TaskQueue.java | 119 + .../tomcat/util/threads/TaskThread.java | 73 + .../util/threads/TaskThreadFactory.java | 71 + .../util/threads/ThreadPoolExecutor.java | 2359 ++++++ .../util/threads/VirtualThreadExecutor.java | 98 + .../ObjectReflectionPropertyInspector.java | 277 + .../ReflectionLessCodeGenerator.java | 273 + .../util/xreflection/ReflectionProperty.java | 133 + .../util/xreflection/SetPropertyClass.java | 443 ++ .../websocket/AsyncChannelGroupUtil.java | 144 + .../tomcat/websocket/AsyncChannelWrapper.java | 48 + .../AsyncChannelWrapperNonSecure.java | 112 + .../websocket/AsyncChannelWrapperSecure.java | 556 ++ .../websocket/AuthenticationException.java | 35 + .../tomcat/websocket/AuthenticationType.java | 63 + .../tomcat/websocket/Authenticator.java | 124 + .../websocket/AuthenticatorFactory.java | 67 + .../tomcat/websocket/BackgroundProcess.java | 26 + .../websocket/BackgroundProcessManager.java | 144 + .../tomcat/websocket/BasicAuthenticator.java | 63 + .../websocket/ClientEndpointHolder.java | 28 + .../apache/tomcat/websocket/Constants.java | 155 + .../apache/tomcat/websocket/DecoderEntry.java | 38 + .../tomcat/websocket/DigestAuthenticator.java | 157 + .../tomcat/websocket/EndpointClassHolder.java | 57 + .../tomcat/websocket/EndpointHolder.java | 56 + .../tomcat/websocket/FutureToSendHandler.java | 109 + .../tomcat/websocket/LocalStrings.properties | 149 + .../websocket/LocalStrings_cs.properties | 32 + .../websocket/LocalStrings_de.properties | 36 + .../websocket/LocalStrings_es.properties | 45 + .../websocket/LocalStrings_fr.properties | 149 + .../websocket/LocalStrings_ja.properties | 149 + .../websocket/LocalStrings_ko.properties | 145 + .../websocket/LocalStrings_pt_BR.properties | 24 + .../websocket/LocalStrings_ru.properties | 40 + .../websocket/LocalStrings_zh_CN.properties | 145 + .../websocket/MessageHandlerResult.java | 41 + .../websocket/MessageHandlerResultType.java | 23 + .../apache/tomcat/websocket/MessagePart.java | 81 + .../tomcat/websocket/PerMessageDeflate.java | 491 ++ .../tomcat/websocket/PojoClassHolder.java | 63 + .../apache/tomcat/websocket/PojoHolder.java | 60 + .../ReadBufferOverflowException.java | 34 + .../tomcat/websocket/Transformation.java | 100 + .../websocket/TransformationFactory.java | 41 + .../websocket/TransformationResult.java | 36 + java/org/apache/tomcat/websocket/Util.java | 640 ++ .../websocket/WrappedMessageHandler.java | 25 + .../tomcat/websocket/WsContainerProvider.java | 29 + .../apache/tomcat/websocket/WsExtension.java | 46 + .../websocket/WsExtensionParameter.java | 40 + .../apache/tomcat/websocket/WsFrameBase.java | 982 +++ .../tomcat/websocket/WsFrameClient.java | 227 + .../tomcat/websocket/WsHandshakeResponse.java | 56 + .../tomcat/websocket/WsIOException.java | 40 + .../tomcat/websocket/WsPongMessage.java | 39 + .../websocket/WsRemoteEndpointAsync.java | 78 + .../websocket/WsRemoteEndpointBase.java | 62 + .../websocket/WsRemoteEndpointBasic.java | 74 + .../websocket/WsRemoteEndpointImplBase.java | 1283 ++++ .../websocket/WsRemoteEndpointImplClient.java | 83 + .../apache/tomcat/websocket/WsSession.java | 1067 +++ .../websocket/WsWebSocketContainer.java | 1112 +++ .../tomcat/websocket/pojo/Constants.java | 29 + .../websocket/pojo/LocalStrings.properties | 45 + .../websocket/pojo/LocalStrings_cs.properties | 18 + .../websocket/pojo/LocalStrings_de.properties | 20 + .../websocket/pojo/LocalStrings_es.properties | 22 + .../websocket/pojo/LocalStrings_fr.properties | 45 + .../websocket/pojo/LocalStrings_ja.properties | 45 + .../websocket/pojo/LocalStrings_ko.properties | 45 + .../pojo/LocalStrings_pt_BR.properties | 16 + .../websocket/pojo/LocalStrings_ru.properties | 16 + .../pojo/LocalStrings_zh_CN.properties | 45 + .../websocket/pojo/PojoEndpointBase.java | 151 + .../websocket/pojo/PojoEndpointClient.java | 46 + .../websocket/pojo/PojoEndpointServer.java | 48 + .../pojo/PojoMessageHandlerBase.java | 119 + .../pojo/PojoMessageHandlerPartialBase.java | 73 + .../pojo/PojoMessageHandlerPartialBinary.java | 33 + .../pojo/PojoMessageHandlerPartialText.java | 32 + .../pojo/PojoMessageHandlerWholeBase.java | 132 + .../pojo/PojoMessageHandlerWholeBinary.java | 113 + .../pojo/PojoMessageHandlerWholePong.java | 45 + .../pojo/PojoMessageHandlerWholeText.java | 119 + .../websocket/pojo/PojoMethodMapping.java | 663 ++ .../tomcat/websocket/pojo/PojoPathParam.java | 67 + .../tomcat/websocket/pojo/package-info.java | 21 + .../tomcat/websocket/server/Constants.java | 33 + .../DefaultServerEndpointConfigurator.java | 84 + .../websocket/server/LocalStrings.properties | 40 + .../server/LocalStrings_cs.properties | 16 + .../server/LocalStrings_de.properties | 21 + .../server/LocalStrings_es.properties | 21 + .../server/LocalStrings_fr.properties | 40 + .../server/LocalStrings_ja.properties | 40 + .../server/LocalStrings_ko.properties | 40 + .../server/LocalStrings_zh_CN.properties | 40 + .../tomcat/websocket/server/UpgradeUtil.java | 345 + .../tomcat/websocket/server/UriTemplate.java | 169 + .../websocket/server/WsContextListener.java | 49 + .../tomcat/websocket/server/WsFilter.java | 78 + .../websocket/server/WsFrameServer.java | 204 + .../websocket/server/WsHandshakeRequest.java | 182 + .../server/WsHttpUpgradeHandler.java | 265 + .../websocket/server/WsMappingResult.java | 43 + .../WsPerSessionServerEndpointConfig.java | 82 + .../server/WsRemoteEndpointImplServer.java | 404 ++ .../apache/tomcat/websocket/server/WsSci.java | 143 + .../websocket/server/WsServerContainer.java | 464 ++ .../websocket/server/WsSessionListener.java | 36 + .../websocket/server/WsWriteTimeout.java | 103 + .../tomcat/websocket/server/package-info.java | 21 + modules/cxf/.gitignore | 12 + modules/cxf/pom.xml | 172 + .../src/main/java/tomcat/cxf/JsonBean.java | 28 + .../cxf/src/main/resources/META-INF/beans.xml | 27 + .../main/resources/META-INF/web-fragment.xml | 39 + modules/jdbc-pool/LICENSE | 201 + modules/jdbc-pool/NOTICE | 6 + modules/jdbc-pool/build.properties.default | 107 + modules/jdbc-pool/build.xml | 547 ++ modules/jdbc-pool/doc/changelog.xml | 133 + modules/jdbc-pool/doc/jdbc-pool.xml | 993 +++ modules/jdbc-pool/doc/package.xsl | 258 + modules/jdbc-pool/doc/project.xml | 31 + modules/jdbc-pool/pom.xml | 160 + modules/jdbc-pool/resources/MANIFEST.MF | 22 + .../naming/GenericNamingResourcesFactory.java | 239 + .../tomcat/jdbc/pool/ClassLoaderUtil.java | 61 + .../tomcat/jdbc/pool/ConnectionPool.java | 1632 +++++ .../apache/tomcat/jdbc/pool/DataSource.java | 148 + .../tomcat/jdbc/pool/DataSourceFactory.java | 598 ++ .../tomcat/jdbc/pool/DataSourceProxy.java | 1178 +++ .../jdbc/pool/DisposableConnectionFacade.java | 98 + .../tomcat/jdbc/pool/FairBlockingQueue.java | 538 ++ .../tomcat/jdbc/pool/JdbcInterceptor.java | 240 + .../jdbc/pool/MultiLockFairBlockingQueue.java | 542 ++ .../tomcat/jdbc/pool/PoolConfiguration.java | 913 +++ .../jdbc/pool/PoolExhaustedException.java | 56 + .../tomcat/jdbc/pool/PoolProperties.java | 1038 +++ .../tomcat/jdbc/pool/PoolUtilities.java | 38 + .../tomcat/jdbc/pool/PooledConnection.java | 908 +++ .../jdbc/pool/PooledConnectionMBean.java | 42 + .../tomcat/jdbc/pool/ProxyConnection.java | 161 + .../tomcat/jdbc/pool/StatementFacade.java | 157 + .../tomcat/jdbc/pool/TrapException.java | 82 + .../apache/tomcat/jdbc/pool/Validator.java | 37 + .../apache/tomcat/jdbc/pool/XADataSource.java | 36 + .../AbstractCreateStatementInterceptor.java | 168 + .../pool/interceptor/AbstractQueryReport.java | 248 + .../pool/interceptor/ConnectionState.java | 183 + .../interceptor/QueryTimeoutInterceptor.java | 58 + .../pool/interceptor/ResetAbandonedTimer.java | 106 + .../interceptor/ResetAbandonedTimerMBean.java | 21 + .../pool/interceptor/SlowQueryReport.java | 503 ++ .../pool/interceptor/SlowQueryReportJmx.java | 303 + .../interceptor/SlowQueryReportJmxMBean.java | 23 + .../jdbc/pool/interceptor/StatementCache.java | 402 ++ .../pool/interceptor/StatementCacheMBean.java | 27 + .../StatementDecoratorInterceptor.java | 306 + .../pool/interceptor/StatementFinalizer.java | 117 + .../pool/interceptor/mbeans-descriptors.xml | 40 + .../tomcat/jdbc/pool/jmx/ConnectionPool.java | 910 +++ .../jdbc/pool/jmx/ConnectionPoolMBean.java | 87 + .../apache/tomcat/jdbc/pool/jmx/JmxUtil.java | 67 + .../tomcat/jdbc/pool/mbeans-descriptors.xml | 409 ++ .../org/apache/tomcat/jdbc/bugs/Bug51582.java | 105 + .../org/apache/tomcat/jdbc/bugs/Bug53367.java | 177 + .../org/apache/tomcat/jdbc/bugs/Bug54225.java | 64 + .../org/apache/tomcat/jdbc/bugs/Bug54227.java | 65 + .../org/apache/tomcat/jdbc/bugs/Bug54978.java | 66 + .../tomcat/jdbc/pool/PoolPropertiesTest.java | 54 + .../jdbc/pool/ShouldForceReconnectTest.java | 140 + .../pool/interceptor/InduceSlowQuery.java | 71 + .../StatementCounterInterceptor.java | 65 + .../pool/interceptor/TestInterceptor.java | 54 + .../jdbc/test/AbandonPercentageTest.java | 117 + .../jdbc/test/AlternateUsernameTest.java | 151 + .../jdbc/test/Async0IdleTestBug50477.java | 43 + .../tomcat/jdbc/test/BorrowWaitTest.java | 71 + .../org/apache/tomcat/jdbc/test/Bug50571.java | 38 + .../org/apache/tomcat/jdbc/test/Bug50805.java | 54 + .../tomcat/jdbc/test/CheckOutThreadTest.java | 411 ++ .../tomcat/jdbc/test/ConnectCountTest.java | 289 + .../tomcat/jdbc/test/CreateTestTable.java | 138 + .../tomcat/jdbc/test/DefaultProperties.java | 72 + .../tomcat/jdbc/test/DefaultTestCase.java | 264 + .../tomcat/jdbc/test/EqualsHashCodeTest.java | 74 + .../apache/tomcat/jdbc/test/FairnessTest.java | 271 + .../tomcat/jdbc/test/JmxPasswordTest.java | 70 + .../tomcat/jdbc/test/MultipleCloseTest.java | 71 + .../tomcat/jdbc/test/PoolCleanerTest.java | 140 + .../tomcat/jdbc/test/PoolPurgeTest.java | 86 + .../jdbc/test/SimplePOJOAsyncExample.java | 85 + .../tomcat/jdbc/test/SimplePOJOExample.java | 75 + .../tomcat/jdbc/test/StarvationTest.java | 114 + .../jdbc/test/StatementFinalizerTest.java | 56 + .../tomcat/jdbc/test/TestAsyncQueue.java | 91 + .../tomcat/jdbc/test/TestConcurrency.java | 216 + .../tomcat/jdbc/test/TestConnectionState.java | 154 + .../tomcat/jdbc/test/TestException.java | 50 + .../apache/tomcat/jdbc/test/TestGCClose.java | 38 + .../tomcat/jdbc/test/TestGetConnection.java | 35 + .../jdbc/test/TestInterceptorShortName.java | 44 + .../TestJdbcInterceptorConfigParsing.java | 174 + .../test/TestQueryTimeoutInterceptor.java | 51 + .../jdbc/test/TestSizePreservation.java | 126 + .../jdbc/test/TestSlowQueryComparator.java | 121 + .../tomcat/jdbc/test/TestSlowQueryReport.java | 336 + .../tomcat/jdbc/test/TestStatementCache.java | 209 + .../tomcat/jdbc/test/TestSuspectTimeout.java | 46 + .../apache/tomcat/jdbc/test/TestTimeout.java | 94 + .../tomcat/jdbc/test/TestValidation.java | 647 ++ .../jdbc/test/TestValidationQueryTimeout.java | 260 + .../tomcat/jdbc/test/TwoDataSources.java | 61 + .../tomcat/jdbc/test/driver/Connection.java | 320 + .../tomcat/jdbc/test/driver/Driver.java | 88 + .../tomcat/jdbc/test/driver/ResultSet.java | 1223 ++++ .../tomcat/jdbc/test/driver/Statement.java | 1320 ++++ modules/owb/.gitignore | 13 + modules/owb/pom.xml | 149 + .../OpenWebBeansContextLifecycleListener.java | 143 + .../tomcat/OpenWebBeansInstanceManager.java | 142 + .../web/tomcat/OpenWebBeansListener.java | 81 + .../tomcat/OpenWebBeansSecurityFilter.java | 67 + .../web/tomcat/OpenWebBeansSecurityValve.java | 59 + .../webbeans/web/tomcat/TomcatPlugin.java | 102 + .../web/tomcat/TomcatSecurityService.java | 104 + .../openwebbeans/openwebbeans.properties | 20 + ...he.webbeans.spi.plugins.OpenWebBeansPlugin | 17 + .../web/tomcat/LocalStrings.properties | 19 + modules/stuffed/Dockerfile | 61 + modules/stuffed/DockerfileGraal | 41 + modules/stuffed/README.md | 88 + modules/stuffed/conf/.gitignore | 4 + modules/stuffed/pom.xml | 128 + modules/stuffed/tomcat-jni.json | 7 + modules/stuffed/tomcat-reflection.json | 51 + modules/stuffed/tomcat-resource.json | 85 + modules/stuffed/tomcat.yaml | 51 + modules/stuffed/webapp-jspc.ant.xml | 64 + modules/stuffed/webapps/.gitignore | 4 + res/META-INF/annotations-api.jar.manifest | 11 + res/META-INF/bootstrap.jar.manifest | 11 + res/META-INF/default.license | 202 + res/META-INF/default.manifest | 9 + res/META-INF/default.notice | 5 + res/META-INF/default/.gitignore | 24 + res/META-INF/el-api.jar.manifest | 11 + .../services/jakarta.el.ExpressionFactory | 16 + res/META-INF/jasper-el.jar/web-fragment.xml | 26 + ...akarta.servlet.ServletContainerInitializer | 16 + res/META-INF/jasper.jar/web-fragment.xml | 26 + res/META-INF/jaspic-api.jar.manifest | 11 + res/META-INF/jsp-api.jar.manifest | 11 + res/META-INF/servlet-api.jar.license | 858 +++ res/META-INF/servlet-api.jar.manifest | 11 + res/META-INF/servlet-api.jar.notice | 31 + ...akarta.servlet.ServletContainerInitializer | 16 + .../jakarta.websocket.ContainerProvider | 16 + ...t.server.ServerEndpointConfig$Configurator | 16 + .../tomcat-websocket.jar/web-fragment.xml | 26 + res/META-INF/websocket-api.jar.manifest | 11 + .../websocket-client-api.jar.manifest | 11 + res/bnd/annotations-api.jar.tmp.bnd | 43 + res/bnd/build-defaults.bnd | 39 + res/bnd/catalina-ha.jar.tmp.bnd | 35 + res/bnd/catalina-ssi.jar.tmp.bnd | 29 + res/bnd/catalina-storeconfig.jar.tmp.bnd | 28 + res/bnd/catalina-tribes.jar.tmp.bnd | 41 + res/bnd/catalina.jar.tmp.bnd | 68 + res/bnd/el-api.jar.tmp.bnd | 51 + res/bnd/jasper-el.jar.tmp.bnd | 37 + res/bnd/jasper.jar.tmp.bnd | 45 + res/bnd/jaspic-api.jar.tmp.bnd | 43 + res/bnd/jsp-api.jar.tmp.bnd | 43 + res/bnd/servlet-api.jar.tmp.bnd | 47 + res/bnd/spec-defaults.bnd | 21 + res/bnd/tomcat-api.jar.tmp.bnd | 28 + res/bnd/tomcat-coyote.jar.tmp.bnd | 55 + res/bnd/tomcat-dbcp.jar.tmp.bnd | 34 + res/bnd/tomcat-embed-core.jar.tmp.bnd | 144 + res/bnd/tomcat-embed-el.jar.tmp.bnd | 61 + res/bnd/tomcat-embed-jasper.jar.tmp.bnd | 60 + res/bnd/tomcat-embed-websocket.jar.tmp.bnd | 62 + res/bnd/tomcat-jni.jar.tmp.bnd | 28 + res/bnd/tomcat-juli.jar.tmp.bnd | 30 + res/bnd/tomcat-util-scan.jar.tmp.bnd | 34 + res/bnd/tomcat-util.jar.tmp.bnd | 40 + res/bnd/tomcat-websocket.jar.tmp.bnd | 35 + res/bnd/websocket-api.jar.tmp.bnd | 56 + res/bnd/websocket-client-api.jar.tmp.bnd | 46 + res/checkstyle/checkstyle.xml | 137 + res/checkstyle/header-al2.txt | 19 + res/checkstyle/jakarta-checkstyle.xml | 37 + res/checkstyle/jakarta-import-control.xml | 77 + res/checkstyle/org-checkstyle.xml | 37 + res/checkstyle/org-import-control.xml | 195 + res/checkstyle/test-checkstyle.xml | 106 + res/deployer/build.xml | 117 + res/graal/README.md | 40 + res/graal/build-tomcat-native-image.sh | 62 + res/graal/graal-measure.sh | 58 + .../native-image/native-image.properties | 16 + .../native-image/tomcat-jni.json | 12 + .../native-image/tomcat-reflection.json | 66 + .../native-image/tomcat-resource.json | 57 + .../native-image/native-image.properties | 16 + .../native-image/tomcat-reflection.json | 3 + .../native-image/tomcat-resource.json | 6 + .../native-image/native-image.properties | 16 + .../native-image/tomcat-reflection.json | 2 + .../native-image/tomcat-resource.json | 9 + .../native-image/native-image.properties | 16 + .../native-image/tomcat-reflection.json | 2 + .../native-image/tomcat-resource.json | 42 + .../native-image/native-image.properties | 16 + .../native-image/tomcat-reflection.json | 8 + .../native-image/tomcat-resource.json | 7 + res/ide-support/coding-style.txt | 30 + .../eclipse/clean-up-asf-tomcat.xml | 160 + res/ide-support/eclipse/eclipse.classpath | 36 + res/ide-support/eclipse/eclipse.project | 33 + .../eclipse/formatting-asf-tomcat.xml | 416 ++ .../eclipse/java-compiler-errors-warnings.txt | 132 + .../org.eclipse.jdt.core.prefs.properties | 20 + res/ide-support/eclipse/start-tomcat.launch | 29 + res/ide-support/eclipse/stop-tomcat.launch | 29 + res/ide-support/idea/ant.xml | 22 + res/ide-support/idea/codeStyles/Project.xml | 30 + .../idea/codeStyles/codeStyleConfig.xml | 22 + res/ide-support/idea/compiler.xml | 30 + res/ide-support/idea/copyright/Tomcat.xml | 23 + .../idea/copyright/profiles_settings.xml | 20 + res/ide-support/idea/externalDependencies.xml | 28 + .../inspectionProfiles/Project_Default.xml | 28 + res/ide-support/idea/misc.xml | 22 + res/ide-support/idea/modules.xml | 24 + res/ide-support/idea/tomcat.iml | 152 + res/ide-support/idea/workspace.xml | 34 + res/ide-support/netbeans/README.txt | 143 + .../netbeans/nb-tomcat-build.properties | 50 + .../netbeans/nb-tomcat-project.properties | 33 + res/ide-support/netbeans/nb-tomcat.xml | 147 + res/ide-support/netbeans/project.xml | 197 + res/install-win/INSTALLLICENSE | 1143 +++ res/install-win/Uninstall.exe.sig | Bin 0 -> 10202 bytes res/install-win/header.bmp | Bin 0 -> 10198 bytes res/install-win/side_left.bmp | Bin 0 -> 345238 bytes res/install-win/tomcat-installer.exe.sig | Bin 0 -> 10202 bytes res/install-win/tomcat-users_1.xml | 20 + res/install-win/tomcat-users_2.xml | 35 + res/install-win/tomcat.ico | Bin 0 -> 21630 bytes res/install-win/tomcat.nsi | 1388 ++++ res/maven/README.txt | 39 + res/maven/mvn-pub.xml | 599 ++ res/maven/mvn.properties.default | 65 + res/maven/mvn.properties.release | 27 + res/maven/tomcat-annotations-api.pom | 35 + res/maven/tomcat-api.pom | 43 + res/maven/tomcat-catalina-ant.pom | 49 + res/maven/tomcat-catalina-ha.pom | 79 + res/maven/tomcat-catalina.pom | 97 + res/maven/tomcat-coyote.pom | 61 + res/maven/tomcat-dbcp.pom | 43 + res/maven/tomcat-el-api.pom | 35 + res/maven/tomcat-embed-core.pom | 43 + res/maven/tomcat-embed-el.pom | 35 + res/maven/tomcat-embed-jasper.pom | 54 + res/maven/tomcat-embed-programmatic.pom | 35 + res/maven/tomcat-embed-websocket.pom | 49 + res/maven/tomcat-i18n-cs.pom | 35 + res/maven/tomcat-i18n-de.pom | 35 + res/maven/tomcat-i18n-es.pom | 35 + res/maven/tomcat-i18n-fr.pom | 35 + res/maven/tomcat-i18n-ja.pom | 35 + res/maven/tomcat-i18n-ko.pom | 35 + res/maven/tomcat-i18n-pt-BR.pom | 35 + res/maven/tomcat-i18n-ru.pom | 35 + res/maven/tomcat-i18n-zh-CN.pom | 35 + res/maven/tomcat-jasper-el.pom | 44 + res/maven/tomcat-jasper.pom | 84 + res/maven/tomcat-jaspic-api.pom | 35 + res/maven/tomcat-jdbc.pom | 43 + res/maven/tomcat-jni.pom | 35 + res/maven/tomcat-jsp-api.pom | 49 + res/maven/tomcat-juli.pom | 35 + res/maven/tomcat-servlet-api.pom | 83 + res/maven/tomcat-ssi.pom | 43 + res/maven/tomcat-storeconfig.pom | 73 + res/maven/tomcat-tribes.pom | 43 + res/maven/tomcat-util-scan.pom | 58 + res/maven/tomcat-util.pom | 43 + res/maven/tomcat-websocket-api.pom | 43 + res/maven/tomcat-websocket-client-api.pom | 35 + res/maven/tomcat-websocket.pom | 61 + res/maven/tomcat.pom | 36 + res/openssl/README.md | 42 + res/openssl/addlicense.sh | 22 + res/openssl/license.header | 17 + res/openssl/openssl-tomcat.conf | 369 + res/openssl/openssl.h | 31 + res/rat/rat-excludes.txt | 256 + res/scripts/check-mime.pl | 471 ++ res/spotbugs/filter-false-positives.xml | 2662 +++++++ res/welcome.bin.html | 86 + res/welcome.main.html | 86 + ...akarta.servlet.ServletContainerInitializer | 17 + test/conf/TesterRewriteMapB.txt | 24 + test/conf/TesterRewriteMapC.txt | 24 + test/conf/jaspic-test-01.xml | 22 + test/conf/jaspic-test-02.xml | 26 + test/conf/jaspic-test-04.xml | 23 + test/deployment/broken.war | Bin 0 -> 1507 bytes test/deployment/context.jar | Bin 0 -> 489 bytes test/deployment/context.war | Bin 0 -> 565 bytes test/deployment/context.xml | 17 + test/deployment/contextCopyXMLFalse.war | Bin 0 -> 733 bytes test/deployment/contextCopyXMLTrue.war | Bin 0 -> 732 bytes test/deployment/contextUnpackWARFalse.war | Bin 0 -> 583 bytes test/deployment/contextUnpackWARTrue.war | Bin 0 -> 582 bytes test/deployment/dir with spaces/context.jar | Bin 0 -> 489 bytes test/deployment/dir with spaces/context.war | Bin 0 -> 565 bytes .../dirContext/META-INF/context.xml | 17 + test/deployment/dirContext/index.html | 22 + test/deployment/dirNoContext/index.html | 22 + test/deployment/noContext.war | Bin 0 -> 240 bytes test/jakarta/el/TestArrayELResolver.java | 522 ++ test/jakarta/el/TestBeanELResolver.java | 1066 +++ .../TestBeanELResolverVarargsInvocation.java | 118 + test/jakarta/el/TestBeanNameELResolver.java | 586 ++ test/jakarta/el/TestCompositeELResolver.java | 37 + test/jakarta/el/TestELContext.java | 169 + test/jakarta/el/TestELProcessor.java | 207 + test/jakarta/el/TestELResolver.java | 129 + test/jakarta/el/TestEvaluationListener.java | 132 + test/jakarta/el/TestImportHandler.java | 268 + .../el/TestImportHandlerStandardPackages.java | 131 + test/jakarta/el/TestListELResolver.java | 366 + test/jakarta/el/TestMapELResolver.java | 295 + test/jakarta/el/TestMethodReference.java | 68 + .../el/TestResourceBundleELResolver.java | 303 + .../jakarta/el/TestStaticFieldELResolver.java | 459 ++ test/jakarta/el/TestUtil.java | 56 + test/jakarta/el/TesterBean.java | 72 + test/jakarta/el/TesterBeanNameResolver.java | 82 + test/jakarta/el/TesterClass.java | 47 + .../TesterCompositeELResolverPerformance.java | 52 + test/jakarta/el/TesterELContext.java | 45 + test/jakarta/el/TesterELResolverOne.java | 31 + test/jakarta/el/TesterELResolverTwo.java | 31 + test/jakarta/el/TesterEvaluationListener.java | 79 + test/jakarta/el/TesterFunctions.java | 66 + .../el/TesterImportHandlerPerformance.java | 49 + .../servlet/TestSessionCookieConfig.java | 56 + .../annotation/TestServletSecurity.java | 106 + .../TestServletSecurityMappings.java | 203 + .../http/HttpServletDoHeadBaseTest.java | 373 + test/jakarta/servlet/http/TestCookie.java | 214 + .../http/TestCookieRFC6265Validator.java | 40 + .../jakarta/servlet/http/TestHttpServlet.java | 590 ++ .../TestHttpServletDoHeadValidWrite0.java | 57 + .../TestHttpServletDoHeadValidWrite1.java | 57 + .../TestHttpServletDoHeadValidWrite1023.java | 57 + .../TestHttpServletDoHeadValidWrite1024.java | 57 + .../TestHttpServletDoHeadValidWrite1025.java | 57 + .../TestHttpServletDoHeadValidWrite511.java | 57 + .../TestHttpServletDoHeadValidWrite512.java | 57 + .../TestHttpServletDoHeadValidWrite513.java | 57 + .../TestHttpServletResponseSendError.java | 366 + .../http/TesterHttpServletPerformance.java | 70 + test/jakarta/servlet/jsp/TestPageContext.java | 37 + .../servlet/jsp/TesterPageContext.java | 195 + .../servlet/jsp/el/TestImportELResolver.java | 55 + .../jsp/el/TestScopedAttributeELResolver.java | 39 + ...rScopedAttributeELResolverPerformance.java | 51 + .../resources/TestSchemaValidation.java | 175 + .../TesterContainerProviderPerformance.java | 59 + .../apache/catalina/ant/TestDeployTask.java | 124 + .../authenticator/ResponseDescriptor.java | 59 + .../TestAuthInfoResponseHeaders.java | 170 + .../TestAuthenticatorBaseCorsPreflight.java | 177 + .../authenticator/TestBasicAuthParser.java | 536 ++ .../TestDigestAuthenticator.java | 388 + .../TestDigestAuthenticatorAlgorithms.java | 279 + .../authenticator/TestFormAuthenticatorA.java | 711 ++ .../authenticator/TestFormAuthenticatorB.java | 520 ++ .../authenticator/TestFormAuthenticatorC.java | 522 ++ ...tJaspicCallbackHandlerInAuthenticator.java | 187 + .../TestNonLoginAndBasicAuthenticator.java | 606 ++ .../TestSSOnonLoginAndBasicAuthenticator.java | 679 ++ ...TestSSOnonLoginAndDigestAuthenticator.java | 501 ++ .../TesterCallbackHandlerImpl.java | 36 + .../TesterDigestAuthenticatorPerformance.java | 298 + .../jaspic/TestAuthConfigFactoryImpl.java | 446 ++ .../TestPersistentProviderRegistrations.java | 135 + .../jaspic/TestSimpleServerAuthConfig.java | 74 + .../jaspic/TesterMessageInfo.java | 54 + .../jaspic/TesterServerAuthModuleA.java | 63 + .../connector/TestClientReadTimeout.java | 129 + .../catalina/connector/TestConnector.java | 220 + .../catalina/connector/TestCoyoteAdapter.java | 427 ++ .../TestCoyoteAdapterCanonicalization.java | 235 + .../TestCoyoteAdapterRequestFuzzing.java | 167 + .../connector/TestCoyoteInputStream.java | 72 + .../connector/TestCoyoteOutputStream.java | 293 + .../catalina/connector/TestInputBuffer.java | 160 + .../connector/TestKeepAliveCount.java | 143 + .../connector/TestMaxConnections.java | 159 + .../catalina/connector/TestOutputBuffer.java | 214 + .../catalina/connector/TestRequest.java | 966 +++ .../catalina/connector/TestResponse.java | 1004 +++ .../connector/TestResponsePerformance.java | 86 + .../catalina/connector/TestSendFile.java | 248 + .../connector/TesterRequestPerformance.java | 47 + .../catalina/connector/test_content.txt | 19 + .../catalina/core/TestApplicationContext.java | 331 + ...plicationContextFacadeSecurityManager.java | 148 + ...pplicationContextGetRequestDispatcher.java | 524 ++ ...plicationContextGetRequestDispatcherB.java | 585 ++ ...TestApplicationContextStripPathParams.java | 66 + .../core/TestApplicationFilterConfig.java | 68 + .../core/TestApplicationHttpRequest.java | 357 + .../catalina/core/TestApplicationMapping.java | 375 + .../core/TestApplicationPushBuilder.java | 72 + .../TestApplicationSessionCookieConfig.java | 139 + .../catalina/core/TestAsyncContextImpl.java | 3218 +++++++++ .../core/TestAsyncContextImplDispatch.java | 151 + ...estAsyncContextImplListenerOnComplete.java | 182 + .../core/TestAsyncContextStateChanges.java | 377 + .../core/TestContextNamingInfoListener.java | 119 + .../core/TestDefaultInstanceManager.java | 90 + .../core/TestNamingContextListener.java | 174 + .../TestPropertiesRoleMappingListener.java | 169 + .../catalina/core/TestStandardContext.java | 1069 +++ .../core/TestStandardContextAliases.java | 117 + .../core/TestStandardContextResources.java | 280 + .../core/TestStandardContextValve.java | 262 + .../catalina/core/TestStandardHostValve.java | 260 + .../catalina/core/TestStandardService.java | 58 + .../catalina/core/TestStandardWrapper.java | 485 ++ .../core/TestSwallowAbortedUploads.java | 485 ++ ...sterApplicationHttpRequestPerformance.java | 52 + ...sterDefaultInstanceManagerPerformance.java | 97 + .../catalina/core/TesterTldListener.java | 59 + .../filters/TestAddCharSetFilter.java | 179 + .../catalina/filters/TestCorsFilter.java | 1315 ++++ .../filters/TestCsrfPreventionFilter.java | 215 + .../filters/TestCsrfPreventionFilter2.java | 89 + .../catalina/filters/TestExpiresFilter.java | 524 ++ .../catalina/filters/TestRateLimitFilter.java | 168 + .../filters/TestRemoteCIDRFilter.java | 150 + .../catalina/filters/TestRemoteIpFilter.java | 871 +++ .../filters/TestRestCsrfPreventionFilter.java | 342 + .../TestRestCsrfPreventionFilter2.java | 352 + .../catalina/filters/TesterFilterChain.java | 32 + .../catalina/filters/TesterFilterConfigs.java | 278 + .../filters/TesterHttpServletRequest.java | 463 ++ .../filters/TesterHttpServletResponse.java | 376 + .../ha/context/TestReplicatedContext.java | 73 + .../catalina/ha/session/TestDeltaRequest.java | 73 + test/org/apache/catalina/loader/EchoTag.java | 46 + .../catalina/loader/MyAnnotatedServlet.java | 40 + .../catalina/loader/TestVirtualContext.java | 338 + .../loader/TestVirtualWebappLoader.java | 108 + .../loader/TestWebappClassLoader.java | 175 + ...stWebappClassLoaderExecutorMemoryLeak.java | 130 + .../TestWebappClassLoaderMemoryLeak.java | 121 + .../loader/TestWebappClassLoaderWeaving.java | 411 ++ .../loader/TesterNeverWeavedClass.java | 24 + .../catalina/loader/TesterUnweavedClass.java | 24 + ...ebappClassLoaderThreadLocalMemoryLeak.java | 200 + .../manager/TestStatusTransformer.java | 81 + .../apache/catalina/mapper/TestMapper.java | 549 ++ .../catalina/mapper/TestMapperListener.java | 117 + .../mapper/TestMapperPerformance.java | 73 + .../catalina/mapper/TestMapperWebapps.java | 270 + .../catalina/mbeans/TestRegistration.java | 261 + .../nonblocking/TestNonBlockingAPI.java | 1700 +++++ .../TesterAjpNonBlockingClient.java | 105 + .../catalina/realm/TestGenericPrincipal.java | 87 + .../apache/catalina/realm/TestJNDIRealm.java | 200 + .../TestJNDIRealmAttributeValueEscape.java | 86 + .../TestJNDIRealmConvertToHexEscape.java | 70 + .../realm/TestJNDIRealmIntegration.java | 315 + .../catalina/realm/TestMemoryRealm.java | 38 + .../TestMessageDigestCredentialHandler.java | 61 + .../apache/catalina/realm/TestRealmBase.java | 792 ++ .../realm/TestSecretKeyCredentialHandler.java | 84 + .../catalina/realm/TesterPrincipal.java | 66 + .../realm/TesterPrincipalNonSerializable.java | 63 + .../realm/TesterServletSecurity01.java | 35 + .../security/TestSecurityClassLoad.java | 27 + .../DefaultServletEncodingBaseTest.java | 294 + .../servlets/ServletOptionsBaseTest.java | 164 + .../TestCGIServletCmdLineArguments.java | 102 + .../catalina/servlets/TestDefaultServlet.java | 674 ++ ...tDefaultServletEncodingPassThroughBom.java | 27 + .../TestDefaultServletEncodingWithBom.java | 27 + .../TestDefaultServletEncodingWithoutBom.java | 27 + .../TestDefaultServletIfMatchRequests.java | 204 + .../servlets/TestDefaultServletOptions.java | 60 + .../servlets/TestDefaultServletPut.java | 198 + .../TestDefaultServletRangeRequests.java | 176 + .../catalina/servlets/TestWebdavServlet.java | 198 + .../TestWebdavServletOptionCollection.java | 62 + .../TestWebdavServletOptionsFile.java | 62 + .../TestWebdavServletOptionsUnknown.java | 62 + .../apache/catalina/session/Benchmarks.java | 361 + .../catalina/session/FileStoreTest.java | 100 + .../session/TestPersistentManager.java | 159 + .../TestPersistentManagerIntegration.java | 220 + .../catalina/session/TestStandardSession.java | 154 + .../TestStandardSessionIntegration.java | 107 + .../apache/catalina/session/TesterStore.java | 90 + .../catalina/ssi/TestExpressionParseTree.java | 191 + .../catalina/ssi/TestRegExpCapture.java | 96 + .../catalina/startup/BytesStreamer.java | 40 + .../startup/DuplicateMappingParamFilter.java | 46 + .../startup/DuplicateMappingParamServlet.java | 48 + .../catalina/startup/EmbeddedTomcat.java | 97 + .../catalina/startup/ExpectationClient.java | 51 + .../catalina/startup/FastNonSecureRandom.java | 60 + .../catalina/startup/LoggingBaseTest.java | 158 + .../startup/NoMappingParamServlet.java | 43 + .../apache/catalina/startup/ParamFilter.java | 49 + .../apache/catalina/startup/ParamServlet.java | 43 + .../catalina/startup/SimpleHttpClient.java | 505 ++ .../catalina/startup/TestBootstrap.java | 180 + .../catalina/startup/TestContextConfig.java | 206 + .../startup/TestContextConfigAnnotation.java | 362 + .../TestHostConfigAutomaticDeploymentA.java | 482 ++ .../TestHostConfigAutomaticDeploymentB.java | 687 ++ .../TestHostConfigAutomaticDeploymentC.java | 1162 +++ .../apache/catalina/startup/TestListener.java | 109 + .../catalina/startup/TestMultipartConfig.java | 187 + .../apache/catalina/startup/TestTomcat.java | 643 ++ .../startup/TestTomcatClassLoader.java | 108 + .../catalina/startup/TestTomcatNoServer.java | 89 + .../startup/TestTomcatStandalone.java | 107 + .../startup/TestWebappServiceLoader.java | 231 + .../catalina/startup/TesterMapRealm.java | 55 + .../catalina/startup/TesterServlet.java | 56 + .../TesterServletContainerInitializer1.java | 38 + .../TesterServletContainerInitializer2.java | 38 + .../startup/TesterServletEncodeUrl.java | 59 + .../startup/TesterServletWithAnnotations.java | 75 + .../TesterServletWithLifeCycleMethods.java | 58 + .../catalina/startup/TomcatBaseTest.java | 935 +++ .../catalina/startup/service-config.txt | 20 + .../startup/web-1lifecyclecallback.xml | 32 + .../apache/catalina/startup/web-1ordering.xml | 27 + .../startup/web-2lifecyclecallback.xml | 32 + .../apache/catalina/startup/web-2ordering.xml | 30 + .../catalina/startup/web-fragment-1name.xml | 25 + .../startup/web-fragment-1ordering.xml | 29 + .../catalina/startup/web-fragment-2name.xml | 26 + .../startup/web-fragment-2ordering.xml | 34 + .../catalina/tribes/TesterMulticast.java | 137 + .../apache/catalina/tribes/TesterUtil.java | 52 + .../catalina/tribes/demos/ChannelCreator.java | 238 + .../tribes/demos/CoordinationDemo.java | 438 ++ .../catalina/tribes/demos/EchoRpcTest.java | 208 + .../tribes/demos/IntrospectionUtils.java | 212 + .../catalina/tribes/demos/LoadTest.java | 410 ++ .../apache/catalina/tribes/demos/MapDemo.java | 557 ++ .../tribes/demos/MembersWithProperties.java | 122 + .../group/TestGroupChannelMemberArrival.java | 196 + .../group/TestGroupChannelOptionFlag.java | 108 + .../TestGroupChannelSenderConnections.java | 186 + .../group/TestGroupChannelStartStop.java | 155 + .../EncryptionInterceptorBaseTest.java | 185 + .../TestDomainFilterInterceptor.java | 134 + .../interceptors/TestEncryptInterceptor.java | 377 + .../TestEncryptInterceptorLargeHeap.java | 47 + .../interceptors/TestGzipInterceptor.java | 58 + .../TestNonBlockingCoordinator.java | 185 + .../interceptors/TestOrderInterceptor.java | 213 + .../interceptors/TestTcpFailureDetector.java | 197 + .../catalina/tribes/io/TestChannelData.java | 33 + .../catalina/tribes/io/TestXByteBuffer.java | 41 + .../TestMemberImplSerialization.java | 107 + .../tribes/membership/TestMembership.java | 68 + .../membership/cloud/TestKubernetesJson.java | 286 + .../catalina/tribes/test/NioSenderTest.java | 110 + .../catalina/tribes/test/TribesTestSuite.java | 53 + .../test/channel/TestChannelConfig.java | 70 + .../test/channel/TestDataIntegrity.java | 219 + .../test/channel/TestMulticastPackages.java | 277 + .../channel/TestRemoteProcessException.java | 166 + .../tribes/test/channel/TestUdpPackages.java | 323 + .../test/transport/SocketNioReceive.java | 98 + .../tribes/test/transport/SocketNioSend.java | 106 + .../test/transport/SocketNioValidateSend.java | 102 + .../tribes/test/transport/SocketReceive.java | 90 + .../tribes/test/transport/SocketSend.java | 74 + .../test/transport/SocketTribesReceive.java | 103 + .../test/transport/SocketValidateReceive.java | 120 + .../users/DataSourceUserDatabaseTests.java | 237 + .../users/MemoryUserDatabaseTests.java | 259 + .../apache/catalina/util/TestContextName.java | 244 + .../util/TestContextNameExtractFromPath.java | 70 + .../org/apache/catalina/util/TestNetMask.java | 176 + .../apache/catalina/util/TestNetMaskSet.java | 62 + .../catalina/util/TestParameterMap.java | 310 + .../apache/catalina/util/TestServerInfo.java | 30 + .../catalina/util/TestTimeBucketCounter.java | 78 + .../apache/catalina/util/TestURLEncoder.java | 56 + .../apache/catalina/valves/Benchmarks.java | 489 ++ .../TestAbstractAccessLogValveEscape.java | 73 + .../catalina/valves/TestAccessLogValve.java | 341 + .../TestAccessLogValveDateFormatCache.java | 91 + .../TestCrawlerSessionManagerValve.java | 214 + .../catalina/valves/TestErrorReportValve.java | 266 + .../valves/TestExtendedAccessLogValve.java | 63 + .../valves/TestLoadBalancerDrainingValve.java | 341 + .../catalina/valves/TestPatternTokenizer.java | 32 + .../catalina/valves/TestPersistentValve.java | 97 + .../catalina/valves/TestRemoteIpValve.java | 1226 ++++ .../valves/TestRequestFilterValve.java | 396 + .../apache/catalina/valves/TestSSLValve.java | 356 + .../valves/TestStuckThreadDetectionValve.java | 158 + .../catalina/valves/TesterAccessLogValve.java | 123 + .../rewrite/TestQuotedStringTokenizer.java | 71 + .../valves/rewrite/TestResolverSSL.java | 182 + .../valves/rewrite/TestRewriteValve.java | 905 +++ .../valves/rewrite/TesterRewriteMapA.java | 46 + .../AbstractTestFileResourceSet.java | 82 + .../webresources/AbstractTestResourceSet.java | 552 ++ .../AbstractTestResourceSetMount.java | 75 + .../TestAbstractArchiveResource.java | 77 + .../TestAbstractArchiveResourceSet.java | 115 + .../webresources/TestCachedResource.java | 134 + .../TestClasspathUrlStreamHandler.java | 44 + .../webresources/TestDirResourceSet.java | 93 + .../TestDirResourceSetInternal.java | 89 + .../webresources/TestDirResourceSetMount.java | 85 + .../TestDirResourceSetReadOnly.java | 65 + .../TestDirResourceSetVirtual.java | 107 + .../webresources/TestFileResource.java | 45 + .../webresources/TestFileResourceSet.java | 72 + .../TestFileResourceSetReadOnly.java | 72 + .../TestFileResourceSetVirtual.java | 77 + .../webresources/TestJarContents.java | 93 + .../TestJarInputStreamWrapper.java | 141 + .../webresources/TestJarResourceSet.java | 69 + .../TestJarResourceSetInternal.java | 69 + .../webresources/TestJarResourceSetMount.java | 60 + .../webresources/TestJarWarResourceSet.java | 56 + .../webresources/TestResourceJars.java | 51 + .../webresources/TestStandardRoot.java | 94 + .../TestTomcatURLStreamHandlerFactory.java | 43 + ...terAbstractFileResourceSetPerformance.java | 94 + .../webresources/TesterWebResourceRoot.java | 160 + .../webresources/war/TestHandler.java | 81 + .../war/TestHandlerIntegration.java | 52 + .../war/TestWarURLConnection.java | 52 + .../apache/coyote/TestCompressionConfig.java | 78 + test/org/apache/coyote/TestIoTimeouts.java | 241 + test/org/apache/coyote/TestRequest.java | 168 + test/org/apache/coyote/TestResponse.java | 146 + .../apache/coyote/ajp/SimpleAjpClient.java | 417 ++ .../coyote/ajp/TestAbstractAjpProcessor.java | 1142 +++ .../apache/coyote/ajp/TesterAjpMessage.java | 199 + .../coyote/http11/TestHttp11InputBuffer.java | 779 ++ .../http11/TestHttp11InputBufferCRLF.java | 215 + .../coyote/http11/TestHttp11OutputBuffer.java | 139 + .../coyote/http11/TestHttp11Processor.java | 1915 +++++ .../filters/TestChunkedInputFilter.java | 624 ++ .../http11/filters/TestGzipOutputFilter.java | 84 + .../http11/filters/TesterOutputBuffer.java | 128 + .../coyote/http11/upgrade/TestUpgrade.java | 531 ++ .../upgrade/TestUpgradeInternalHandler.java | 284 + .../apache/coyote/http2/Http2TestBase.java | 1537 ++++ test/org/apache/coyote/http2/TestAsync.java | 275 + .../apache/coyote/http2/TestAsyncError.java | 169 + .../apache/coyote/http2/TestAsyncFlush.java | 153 + .../apache/coyote/http2/TestAsyncTimeout.java | 235 + .../org/apache/coyote/http2/TestByteUtil.java | 39 + .../coyote/http2/TestCancelledUpload.java | 175 + .../apache/coyote/http2/TestFlowControl.java | 144 + test/org/apache/coyote/http2/TestHpack.java | 150 + .../http2/TestHttp2ConnectionTimeouts.java | 37 + .../http2/TestHttp2InitialConnection.java | 164 + .../apache/coyote/http2/TestHttp2Limits.java | 597 ++ .../coyote/http2/TestHttp2Section_3_2.java | 173 + .../coyote/http2/TestHttp2Section_3_5.java | 67 + .../coyote/http2/TestHttp2Section_4_1.java | 68 + .../coyote/http2/TestHttp2Section_4_2.java | 131 + .../coyote/http2/TestHttp2Section_4_3.java | 89 + .../coyote/http2/TestHttp2Section_5_1.java | 434 ++ .../coyote/http2/TestHttp2Section_5_2.java | 117 + .../coyote/http2/TestHttp2Section_5_5.java | 93 + .../coyote/http2/TestHttp2Section_6_1.java | 187 + .../coyote/http2/TestHttp2Section_6_2.java | 104 + .../coyote/http2/TestHttp2Section_6_3.java | 82 + .../coyote/http2/TestHttp2Section_6_4.java | 75 + .../coyote/http2/TestHttp2Section_6_5.java | 123 + .../coyote/http2/TestHttp2Section_6_6.java | 127 + .../coyote/http2/TestHttp2Section_6_7.java | 80 + .../coyote/http2/TestHttp2Section_6_8.java | 84 + .../coyote/http2/TestHttp2Section_6_9.java | 273 + .../coyote/http2/TestHttp2Section_8_1.java | 482 ++ .../coyote/http2/TestHttp2Timeouts.java | 105 + .../coyote/http2/TestHttp2UpgradeHandler.java | 220 + .../apache/coyote/http2/TestHttpServlet.java | 59 + .../apache/coyote/http2/TestLargeUpload.java | 164 + test/org/apache/coyote/http2/TestRfc9218.java | 182 + test/org/apache/coyote/http2/TestStream.java | 137 + .../coyote/http2/TestStreamProcessor.java | 589 ++ .../coyote/http2/TestStreamQueryString.java | 173 + .../coyote/http2/TesterHttp2Parser.java | 92 + test/org/apache/el/TestELEvaluation.java | 257 + test/org/apache/el/TestELInJsp.java | 515 ++ test/org/apache/el/TestExpressionFactory.java | 48 + .../apache/el/TestMethodExpressionImpl.java | 747 ++ .../apache/el/TestValueExpressionImpl.java | 330 + test/org/apache/el/TesterBeanA.java | 66 + test/org/apache/el/TesterBeanAA.java | 30 + test/org/apache/el/TesterBeanAAA.java | 22 + test/org/apache/el/TesterBeanB.java | 52 + test/org/apache/el/TesterBeanBB.java | 30 + test/org/apache/el/TesterBeanBBB.java | 22 + test/org/apache/el/TesterBeanC.java | 42 + test/org/apache/el/TesterBeanD.java | 40 + test/org/apache/el/TesterBeanEnum.java | 30 + test/org/apache/el/TesterBeanF.java | 82 + test/org/apache/el/TesterBeanG.java | 41 + test/org/apache/el/TesterBeanH.java | 28 + test/org/apache/el/TesterBeanI.java | 29 + test/org/apache/el/TesterBeanJ.java | 50 + test/org/apache/el/TesterEnum.java | 27 + test/org/apache/el/TesterFunctions.java | 102 + test/org/apache/el/lang/TestELArithmetic.java | 148 + test/org/apache/el/lang/TestELSupport.java | 501 ++ test/org/apache/el/lang/TesterBean.java | 21 + test/org/apache/el/lang/TesterType.java | 30 + .../apache/el/lang/TesterTypeEditorBase.java | 79 + .../apache/el/lang/TesterTypeEditorError.java | 25 + .../el/lang/TesterTypeEditorNoError.java | 25 + .../el/lang/TesterVariableMapperImpl.java | 55 + test/org/apache/el/parser/TestAstAnd.java | 53 + test/org/apache/el/parser/TestAstAssign.java | 83 + test/org/apache/el/parser/TestAstChoice.java | 32 + .../el/parser/TestAstConcatenation.java | 122 + .../el/parser/TestAstFloatingPoint.java | 46 + .../org/apache/el/parser/TestAstFunction.java | 62 + .../apache/el/parser/TestAstIdentifier.java | 46 + test/org/apache/el/parser/TestAstInteger.java | 52 + .../el/parser/TestAstLambdaExpression.java | 236 + .../org/apache/el/parser/TestAstListData.java | 87 + test/org/apache/el/parser/TestAstMapData.java | 105 + test/org/apache/el/parser/TestAstNot.java | 46 + test/org/apache/el/parser/TestAstOr.java | 53 + .../apache/el/parser/TestAstSemicolon.java | 66 + test/org/apache/el/parser/TestAstSetData.java | 84 + test/org/apache/el/parser/TestELParser.java | 229 + .../el/parser/TestELParserPerformance.java | 92 + test/org/apache/el/parser/TesterBeanA.java | 29 + test/org/apache/el/parser/TesterBeanB.java | 30 + test/org/apache/el/parser/TesterBeanC.java | 49 + .../el/stream/TestCollectionOperations.java | 777 ++ .../apache/el/util/TestMessageFactory.java | 67 + .../apache/el/util/TestReflectionUtil.java | 87 + .../org/apache/el/util/TestStrings.properties | 19 + test/org/apache/el/util/Tester.java | 52 + test/org/apache/jasper/TestJspC.java | 162 + .../jasper/TestJspCompilationContext.java | 74 + test/org/apache/jasper/compiler/Dumper.java | 218 + .../jasper/compiler/TestAttributeParser.java | 167 + .../apache/jasper/compiler/TestCompiler.java | 192 + .../compiler/TestELInterpreterFactory.java | 97 + .../apache/jasper/compiler/TestELParser.java | 322 + .../jasper/compiler/TestEncodingDetector.java | 94 + .../apache/jasper/compiler/TestGenerator.java | 982 +++ .../apache/jasper/compiler/TestJspConfig.java | 332 + .../compiler/TestJspDocumentParser.java | 220 + .../apache/jasper/compiler/TestJspReader.java | 34 + .../apache/jasper/compiler/TestJspUtil.java | 49 + .../compiler/TestJspUtilMakeJavaPackage.java | 63 + test/org/apache/jasper/compiler/TestNode.java | 73 + .../jasper/compiler/TestNodeIntegration.java | 37 + .../apache/jasper/compiler/TestParser.java | 269 + .../TestParserNoStrictWhitespace.java | 47 + .../compiler/TestScriptingVariabler.java | 95 + .../jasper/compiler/TestSmapStratum.java | 57 + .../compiler/TestTagLibraryInfoImpl.java | 56 + .../jasper/compiler/TestTagPluginManager.java | 66 + .../apache/jasper/compiler/TestValidator.java | 278 + .../org/apache/jasper/compiler/TesterTag.java | 27 + .../jasper/compiler/TesterTagPlugin.java | 31 + .../jasper/compiler/TesterValidator.java | 98 + .../jasper/el/TestJasperELResolver.java | 117 + .../TestELInterpreterTagSetters.java | 555 ++ .../TestStringInterpreterTagSetters.java | 120 + .../jasper/runtime/TestCustomHttpJspPage.java | 62 + .../jasper/runtime/TestJspContextWrapper.java | 85 + .../jasper/runtime/TestJspRuntimeLibrary.java | 143 + .../jasper/runtime/TestJspWriterImpl.java | 57 + .../jasper/runtime/TestPageContextImpl.java | 140 + .../org/apache/jasper/runtime/TesterBean.java | 278 + .../jasper/runtime/TesterHttpJspBase.java | 70 + .../TesterTagHandlerPoolPerformance.java | 91 + .../apache/jasper/runtime/TesterTypeA.java | 42 + .../jasper/runtime/TesterTypeAEditor.java | 30 + .../apache/jasper/runtime/TesterTypeB.java | 25 + .../jasper/servlet/TestJasperInitializer.java | 71 + .../servlet/TestJspCServletContext.java | 181 + .../apache/jasper/servlet/TestJspServlet.java | 109 + .../apache/jasper/servlet/TestTldScanner.java | 125 + .../tagplugins/jstl/core/AbstractTestTag.java | 59 + .../tagplugins/jstl/core/TestForEach.java | 58 + .../jasper/tagplugins/jstl/core/TestOut.java | 59 + .../jasper/tagplugins/jstl/core/TestSet.java | 54 + .../jasper/util/TestFastRemovalDequeue.java | 197 + .../juli/TestAsyncFileHandlerOverflow.java | 151 + .../juli/TestClassLoaderLogManager.java | 194 + test/org/apache/juli/TestDateFormatCache.java | 109 + test/org/apache/juli/TestFileHandler.java | 136 + .../juli/TestFileHandlerNonRotatable.java | 83 + .../juli/TestOneLineFormatterPerformance.java | 85 + test/org/apache/juli/TestThreadNameCache.java | 61 + ...sterOneLineFormatterMillisPerformance.java | 74 + .../juli/logging-non-rotatable.properties | 16 + test/org/apache/naming/TestEnvEntry.java | 136 + test/org/apache/naming/TestNamingContext.java | 104 + test/org/apache/naming/TesterEnvEntry.java | 33 + .../apache/naming/TesterInjectionServlet.java | 68 + .../naming/factory/TestBeanFactory.java | 67 + .../org/apache/naming/factory/TesterBean.java | 41 + .../naming/resources/TestNamingContext.java | 366 + .../naming/resources/TestWarDirContext.java | 130 + .../naming/resources/TesterFactory.java | 49 + .../apache/naming/resources/TesterObject.java | 35 + .../buildutil/translate/TestFixedStrings.java | 68 + .../tomcat/buildutil/translate/TestUtils.java | 79 + test/org/apache/tomcat/jni/TesterSSL.java | 58 + .../tomcat/unittest/TesterBug66582.java | 21 + .../apache/tomcat/unittest/TesterContext.java | 1335 ++++ .../apache/tomcat/unittest/TesterCounter.java | 40 + .../apache/tomcat/unittest/TesterData.java | 37 + .../apache/tomcat/unittest/TesterHost.java | 392 + .../unittest/TesterLeakingServlet1.java | 54 + .../unittest/TesterLeakingServlet2.java | 58 + .../unittest/TesterLogValidationFilter.java | 90 + .../apache/tomcat/unittest/TesterRequest.java | 142 + .../tomcat/unittest/TesterResponse.java | 73 + .../tomcat/unittest/TesterServletContext.java | 352 + .../unittest/TesterSessionCookieConfig.java | 110 + .../unittest/TesterThreadScopedHolder.java | 32 + .../unittest/TesterThreadedPerformance.java | 81 + .../apache/tomcat/unittest/tags/Bug53545.java | 23 + .../tomcat/util/TestIntrospectionUtils.java | 159 + .../tomcat/util/bcel/TesterPerformance.java | 82 + .../org/apache/tomcat/util/buf/TestAscii.java | 65 + .../tomcat/util/buf/TestB2CConverter.java | 143 + .../apache/tomcat/util/buf/TestByteChunk.java | 182 + .../util/buf/TestByteChunkLargeHeap.java | 60 + .../apache/tomcat/util/buf/TestCharChunk.java | 80 + .../util/buf/TestCharChunkLargeHeap.java | 55 + .../tomcat/util/buf/TestCharsetCache.java | 78 + .../util/buf/TestCharsetCachePerformance.java | 142 + .../tomcat/util/buf/TestCharsetUtil.java | 92 + .../apache/tomcat/util/buf/TestHexUtils.java | 75 + .../tomcat/util/buf/TestMessageBytes.java | 69 + .../util/buf/TestMessageBytesConversion.java | 207 + .../util/buf/TestMessageBytesIntegration.java | 106 + .../util/buf/TestMessageBytesPerformance.java | 116 + .../tomcat/util/buf/TestStringCache.java | 100 + .../tomcat/util/buf/TestStringUtils.java | 77 + .../apache/tomcat/util/buf/TestUDecoder.java | 241 + .../apache/tomcat/util/buf/TestUEncoder.java | 45 + .../apache/tomcat/util/buf/TestUriUtil.java | 67 + .../apache/tomcat/util/buf/TestUriUtil24.java | 24 + .../apache/tomcat/util/buf/TestUriUtil26.java | 24 + .../apache/tomcat/util/buf/TestUriUtil2A.java | 24 + .../apache/tomcat/util/buf/TestUriUtil40.java | 24 + .../util/buf/TestUriUtilIsAbsoluteURI.java | 77 + test/org/apache/tomcat/util/buf/TestUtf8.java | 412 ++ .../tomcat/util/buf/TesterUriUtilBase.java | 136 + .../TestCaseInsensitiveKeyMap.java | 215 + .../collections/TestSynchronizedQueue.java | 117 + .../collections/TestSynchronizedStack.java | 119 + .../TesterPerformanceSynchronizedQueue.java | 107 + .../TesterPerformanceSynchronizedStack.java | 107 + .../util/descriptor/TestLocalResolver.java | 125 + .../descriptor/tld/TestImplicitTldParser.java | 59 + .../util/descriptor/tld/TestTldParser.java | 173 + .../util/descriptor/web/TestFilterDef.java | 45 + .../web/TestJspConfigDescriptorImpl.java | 51 + .../descriptor/web/TestJspPropertyGroup.java | 35 + .../TestJspPropertyGroupDescriptorImpl.java | 50 + .../web/TestSecurityConstraint.java | 452 ++ .../util/descriptor/web/TestServletDef.java | 45 + .../util/descriptor/web/TestWebRuleSet.java | 155 + .../util/descriptor/web/TestWebXml.java | 632 ++ .../descriptor/web/TestWebXmlOrdering.java | 708 ++ .../util/file/TestConfigFileLoader.java | 67 + .../util/http/TestConcurrentDateFormat.java | 51 + .../tomcat/util/http/TestCookieParsing.java | 230 + .../http/TestCookieProcessorGeneration.java | 297 + .../TestCookieProcessorGenerationHttp.java | 86 + .../apache/tomcat/util/http/TestCookies.java | 297 + .../http/TestHeaderUtiltoPrintableString.java | 83 + .../tomcat/util/http/TestMimeHeaders.java | 110 + .../util/http/TestMimeHeadersIntegration.java | 181 + .../tomcat/util/http/TestParameters.java | 318 + .../util/http/TestRequestUtilNormalize.java | 77 + .../util/http/TestRequestUtilSameOrigin.java | 113 + .../tomcat/util/http/TestResponseUtil.java | 253 + .../tomcat/util/http/TestSameSiteCookies.java | 116 + .../util/http/TesterCookiesPerformance.java | 75 + .../TesterFastHttpDateFormatPerformance.java | 68 + .../http/TesterParametersPerformance.java | 132 + .../util/http/parser/TestAcceptLanguage.java | 358 + .../http/parser/TestAuthorizationDigest.java | 515 ++ .../util/http/parser/TestHttpParser.java | 189 + .../util/http/parser/TestHttpParserHost.java | 286 + .../util/http/parser/TestMediaType.java | 327 + .../tomcat/util/http/parser/TestPriority.java | 37 + .../tomcat/util/http/parser/TestRanges.java | 147 + .../util/http/parser/TestTokenList.java | 222 + .../tomcat/util/http/parser/TestUpgrade.java | 86 + .../http/parser/TesterHostPerformance.java | 76 + .../parser/TesterHttpWgStructuredField.java | 130 + .../http/parser/TesterParserPerformance.java | 143 + .../tomcat/util/json/TestJSONFilter.java | 90 + .../tomcat/util/net/TestClientCert.java | 221 + .../tomcat/util/net/TestClientCertTls13.java | 132 + .../apache/tomcat/util/net/TestCustomSsl.java | 78 + .../util/net/TestCustomSslTrustManager.java | 159 + .../apache/tomcat/util/net/TestIPv6Utils.java | 144 + .../tomcat/util/net/TestSSLHostConfig.java | 112 + .../util/net/TestSSLHostConfigCompat.java | 336 + .../net/TestSSLHostConfigIntegration.java | 91 + .../util/net/TestSocketBufferHandler.java | 139 + test/org/apache/tomcat/util/net/TestSsl.java | 380 + .../util/net/TestTLSClientHelloExtractor.java | 89 + .../tomcat/util/net/TestXxxEndpoint.java | 116 + .../apache/tomcat/util/net/TesterSupport.java | 667 ++ test/org/apache/tomcat/util/net/ca-cert.pem | 38 + test/org/apache/tomcat/util/net/ca.jks | Bin 0 -> 1766 bytes .../tomcat/util/net/jsse/TestPEMFile.java | 142 + .../util/net/jsse/TesterBug50640SslImpl.java | 40 + .../net/jsse/key-encrypted-pkcs1-aes256.pem | 18 + .../net/jsse/key-encrypted-pkcs1-des-cbc.pem | 18 + .../jsse/key-encrypted-pkcs1-des-ede3-cbc.pem | 18 + ...ted-pkcs8-hmacsha1default-des-ede3-cbc.pem | 54 + ...encrypted-pkcs8-hmacsha256-aes-128-cbc.pem | 54 + ...encrypted-pkcs8-hmacsha256-aes-256-cbc.pem | 18 + ...ncrypted-pkcs8-hmacsha256-des-ede3-cbc.pem | 54 + .../apache/tomcat/util/net/jsse/key-password | 1 + .../apache/tomcat/util/net/jsse/key-pkcs1.pem | 15 + test/org/apache/tomcat/util/net/key-password | 1 + .../apache/tomcat/util/net/keystore-info.txt | 28 + .../apache/tomcat/util/net/keystore-password | 1 + .../tomcat/util/net/localhost-ec-cert.pem | 86 + .../tomcat/util/net/localhost-ec-key.pem | 8 + .../apache/tomcat/util/net/localhost-ec.jks | Bin 0 -> 1372 bytes .../tomcat/util/net/localhost-rsa-cert.pem | 105 + .../tomcat/util/net/localhost-rsa-copy1.jks | Bin 0 -> 2685 bytes .../tomcat/util/net/localhost-rsa-key.pem | 28 + .../apache/tomcat/util/net/localhost-rsa.jks | Bin 0 -> 4403 bytes .../util/net/openssl/TestOpenSSLConf.java | 148 + .../util/net/openssl/ciphers/TestCipher.java | 1113 +++ .../TestOpenSSLCipherConfigurationParser.java | 618 ++ ...tOpenSSLCipherConfigurationParserOnly.java | 113 + .../net/openssl/ciphers/TesterOpenSSL.java | 404 ++ test/org/apache/tomcat/util/net/user1.jks | Bin 0 -> 4386 bytes .../tomcat/util/res/TestStringManager.java | 151 + test/org/apache/tomcat/util/scan/FooSCI.java | 37 + .../util/scan/TestAbstractInputStreamJar.java | 59 + .../tomcat/util/scan/TestClassParser.java | 58 + .../tomcat/util/scan/TestJarScanner.java | 46 + .../util/scan/TestStandardJarScanner.java | 81 + .../security/SecurityManagerBaseTest.java | 50 + .../security/TestConcurrentMessageDigest.java | 40 + .../tomcat/util/security/TestEscape.java | 56 + .../tomcat/util/threads/TestLimitLatch.java | 236 + .../websocket/TestPerMessageDeflate.java | 205 + .../org/apache/tomcat/websocket/TestUtil.java | 445 ++ .../websocket/TestWebSocketFrameClient.java | 220 + .../TestWebSocketFrameClientSSL.java | 300 + .../apache/tomcat/websocket/TestWsFrame.java | 61 + .../websocket/TestWsPingPongMessages.java | 89 + .../websocket/TestWsRemoteEndpoint.java | 241 + .../tomcat/websocket/TestWsSession.java | 107 + .../websocket/TestWsSessionSuspendResume.java | 243 + .../tomcat/websocket/TestWsSubprotocols.java | 114 + .../websocket/TestWsWebSocketContainer.java | 665 ++ ...stWsWebSocketContainerGetOpenSessions.java | 391 + .../TestWsWebSocketContainerSSL.java | 169 + ...ContainerSessionExpiryContainerClient.java | 91 + ...ContainerSessionExpiryContainerServer.java | 107 + ...ebSocketContainerSessionExpirySession.java | 96 + ...TestWsWebSocketContainerTimeoutClient.java | 119 + ...TestWsWebSocketContainerTimeoutServer.java | 191 + .../tomcat/websocket/TesterAsyncTiming.java | 87 + .../websocket/TesterBlockWebSocketSCI.java | 43 + .../TesterConnectionLimitPerformance.java | 108 + .../tomcat/websocket/TesterEchoServer.java | 228 + .../websocket/TesterFirehoseServer.java | 194 + .../websocket/TesterMessageCountClient.java | 239 + .../websocket/TesterWebSocketClientProxy.java | 189 + .../websocket/TesterWsClientAutobahn.java | 201 + .../TesterWsWebSocketContainerWithProxy.java | 53 + .../tomcat/websocket/WebSocketBaseTest.java | 70 + .../WsWebSocketContainerBaseTest.java | 47 + .../websocket/pojo/TestEncodingDecoding.java | 789 ++ .../websocket/pojo/TestPojoEndpointBase.java | 147 + .../websocket/pojo/TestPojoMethodMapping.java | 142 + .../tomcat/websocket/pojo/TesterUtil.java | 62 + .../server/TestAsyncMessagesPerformance.java | 165 + .../websocket/server/TestClassLoader.java | 150 + .../tomcat/websocket/server/TestClose.java | 328 + .../websocket/server/TestCloseBug58624.java | 167 + .../websocket/server/TestKeyHeader.java | 92 + .../tomcat/websocket/server/TestShutdown.java | 107 + .../websocket/server/TestSlowClient.java | 98 + .../websocket/server/TestUriTemplate.java | 234 + ...estWsRemoteEndpointImplServerDeadlock.java | 221 + .../server/TestWsServerContainer.java | 315 + .../server/TesterEndpointConfig.java | 54 + .../websocket/server/TesterWsClient.java | 148 + .../TesterWsRemoteEndpointImplServer.java | 161 + test/tld/implicit-bad.tld | 30 + test/tld/implicit-good.tld | 25 + test/tld/listener.tld | 29 + test/tld/tags11.tld | 37 + test/tld/tags12.tld | 37 + test/tld/tags20.tld | 37 + test/tld/tags21.tld | 37 + test/tld/test.tld | 93 + test/util/TestCookieFilter.java | 93 + test/util/a/Foo.java | 23 + test/util/b/Foo.java | 23 + test/webapp-2.2/WEB-INF/tags11.tld | 36 + test/webapp-2.2/WEB-INF/tags12.tld | 36 + test/webapp-2.2/WEB-INF/tags20.tld | 37 + test/webapp-2.2/WEB-INF/tags21.tld | 37 + test/webapp-2.2/WEB-INF/web.xml | 32 + test/webapp-2.2/el-as-literal.jsp | 22 + test/webapp-2.2/tld-versions.jsp | 29 + test/webapp-2.3/WEB-INF/tags11.tld | 36 + test/webapp-2.3/WEB-INF/tags12.tld | 36 + test/webapp-2.3/WEB-INF/tags20.tld | 37 + test/webapp-2.3/WEB-INF/tags21.tld | 37 + test/webapp-2.3/WEB-INF/web.xml | 32 + test/webapp-2.3/el-as-literal.jsp | 22 + test/webapp-2.3/tld-versions.jsp | 29 + test/webapp-2.4/WEB-INF/tags11.tld | 37 + test/webapp-2.4/WEB-INF/tags12.tld | 37 + test/webapp-2.4/WEB-INF/tags20.tld | 37 + test/webapp-2.4/WEB-INF/tags21.tld | 37 + test/webapp-2.4/WEB-INF/web.xml | 33 + test/webapp-2.4/el-as-literal.jsp | 22 + test/webapp-2.4/tld-versions.jsp | 29 + test/webapp-2.5/WEB-INF/tags11.tld | 37 + test/webapp-2.5/WEB-INF/tags12.tld | 37 + test/webapp-2.5/WEB-INF/tags20.tld | 37 + test/webapp-2.5/WEB-INF/tags21.tld | 37 + test/webapp-2.5/WEB-INF/web.xml | 33 + test/webapp-2.5/el-as-literal.jsp | 21 + test/webapp-2.5/tld-versions.jsp | 29 + test/webapp-3.0/WEB-INF/listener.tld | 29 + test/webapp-3.0/WEB-INF/tags11.tld | 37 + test/webapp-3.0/WEB-INF/tags12.tld | 37 + test/webapp-3.0/WEB-INF/tags20.tld | 37 + test/webapp-3.0/WEB-INF/tags21.tld | 37 + test/webapp-3.0/WEB-INF/web.xml | 36 + test/webapp-3.0/el-as-literal.jsp | 21 + test/webapp-3.0/tld-versions.jsp | 29 + test/webapp-3.1/WEB-INF/tags11.tld | 37 + test/webapp-3.1/WEB-INF/tags12.tld | 37 + test/webapp-3.1/WEB-INF/tags20.tld | 37 + test/webapp-3.1/WEB-INF/tags21.tld | 37 + test/webapp-3.1/WEB-INF/web.xml | 36 + test/webapp-3.1/el-as-literal.jsp | 21 + test/webapp-3.1/tld-versions.jsp | 29 + test/webapp-4.0/WEB-INF/tags11.tld | 37 + test/webapp-4.0/WEB-INF/tags12.tld | 37 + test/webapp-4.0/WEB-INF/tags20.tld | 37 + test/webapp-4.0/WEB-INF/tags21.tld | 37 + test/webapp-4.0/WEB-INF/web.xml | 36 + test/webapp-4.0/el-as-literal.jsp | 21 + test/webapp-4.0/tld-versions.jsp | 29 + test/webapp-5.0/WEB-INF/tags11.tld | 37 + test/webapp-5.0/WEB-INF/tags12.tld | 37 + test/webapp-5.0/WEB-INF/tags20.tld | 37 + test/webapp-5.0/WEB-INF/tags21.tld | 37 + test/webapp-5.0/WEB-INF/tags30.tld | 37 + test/webapp-5.0/WEB-INF/web.xml | 36 + test/webapp-5.0/el-as-literal.jsp | 21 + test/webapp-5.0/tld-versions.jsp | 31 + test/webapp-6.0/WEB-INF/tags11.tld | 37 + test/webapp-6.0/WEB-INF/tags12.tld | 37 + test/webapp-6.0/WEB-INF/tags20.tld | 37 + test/webapp-6.0/WEB-INF/tags21.tld | 37 + test/webapp-6.0/WEB-INF/tags30.tld | 37 + test/webapp-6.0/WEB-INF/tags31.tld | 37 + test/webapp-6.0/WEB-INF/web.xml | 36 + test/webapp-6.0/el-as-literal.jsp | 21 + test/webapp-6.0/tld-versions.jsp | 33 + .../WEB-INF/lib/resources.jar | Bin 0 -> 16055 bytes .../WEB-INF/web.xml | 33 + test/webapp-fragments/'singlequote2.jsp | 19 + .../WEB-INF/classes/#Bug51584.txt | 0 .../classes/META-INF/resources/resourceG.jsp | 21 + .../WEB-INF/lib/resources.jar | Bin 0 -> 19989 bytes .../WEB-INF/lib/resources2.jar | Bin 0 -> 17002 bytes test/webapp-fragments/WEB-INF/web.xml | 179 + test/webapp-fragments/bug51396.jsp | 21 + test/webapp-fragments/folder/resourceC.jsp | 21 + test/webapp-fragments/folder/resourceE.jsp | 20 + test/webapp-fragments/jndi.jsp | 31 + test/webapp-fragments/resourceA.jsp | 21 + test/webapp-fragments/warDirContext.jsp | 21 + .../example/prefixed-role-mapping.properties | 17 + .../com/example/role-mapping.properties | 17 + .../WEB-INF/prefixed-role-mapping.properties | 17 + .../WEB-INF/role-mapping.properties | 17 + test/webapp-role-mapping/admin.txt | 18 + test/webapp-role-mapping/unmapped.txt | 18 + test/webapp-role-mapping/user.txt | 18 + ...akarta.servlet.ServletContainerInitializer | 16 + test/webapp-servletsecurity-a/WEB-INF/web.xml | 48 + test/webapp-servletsecurity-b/WEB-INF/web.xml | 43 + test/webapp-servletsecurity-b/protected.jsp | 23 + test/webapp-servletsecurity-b/unprotected.jsp | 23 + .../target/WEB-INF/C.tld | 37 + .../resources/rsrc/resourceE.properties | 16 + .../WEB-INF/classes/rsrc/resourceC.properties | 16 + .../src/main/lib/META-INF/B.tld | 37 + .../src/main/lib/rsrc/resourceD.properties | 16 + .../src/main/misc/resourceI.properties | 16 + .../src/main/webapp-a/WEB-INF/A.tld | 37 + .../WEB-INF/classes/rsrc/resourceA.properties | 16 + .../src/main/webapp-a/WEB-INF/lib/rsrc.jar | Bin 0 -> 1936 bytes .../src/main/webapp-a/WEB-INF/web.xml | 24 + .../webapp-a/classpathGetResourceAsStream.jsp | 32 + .../classpathGetResourceUrlThenGetStream.jsp | 33 + .../main/webapp-a/classpathGetResources.jsp | 25 + .../src/main/webapp-a/contextGetRealPath.jsp | 28 + .../src/main/webapp-a/contextGetResource.jsp | 36 + .../main/webapp-a/contextGetResourcePaths.jsp | 27 + .../main/webapp-a/rsrc/resourceF.properties | 16 + .../src/main/webapp-a/testTlds.jsp | 26 + .../src/main/webapp-b/WEB-INF/D.tld | 37 + .../classes/rsrc-2/resourceK.properties | 16 + .../WEB-INF/classes/rsrc/resourceG.properties | 16 + .../main/webapp-b/rsrc-2/resourceJ.properties | 16 + .../main/webapp-b/rsrc/resourceF.properties | 17 + .../main/webapp-b/rsrc/resourceH.properties | 16 + .../target/classes/rsrc/resourceB.properties | 16 + test/webapp/404.html | 1 + test/webapp/WEB-INF/bug53545.tld | 29 + test/webapp/WEB-INF/bugs.tld | 191 + .../WEB-INF/classes/META-INF/bug55807.tld | 37 + .../WEB-INF/classes/META-INF/bug64373.tld | 31 + .../META-INF/org.apache.jasper/tagPlugins.xml | 23 + .../classes/META-INF/tags/bug64373.tag | 17 + .../classes/org/apache/tomcat/Bug58096.class | Bin 0 -> 285 bytes .../classes/org/apache/tomcat/Bug58096.java | 24 + test/webapp/WEB-INF/jsp/bug53574.jsp | 21 + test/webapp/WEB-INF/lib/test-lib.jar | Bin 0 -> 1943 bytes test/webapp/WEB-INF/tag-setters.tld | 245 + test/webapp/WEB-INF/tags/bug42390.tag | 18 + test/webapp/WEB-INF/tags/bug43400.tag | 18 + test/webapp/WEB-INF/tags/bug48668.tagx | 26 + test/webapp/WEB-INF/tags/bug49297.tag | 21 + test/webapp/WEB-INF/tags/bug54012.tag | 20 + test/webapp/WEB-INF/tags/bug55198.tagx | 26 + test/webapp/WEB-INF/tags/bug56265.tagx | 24 + test/webapp/WEB-INF/tags/bug58178.tag | 32 + test/webapp/WEB-INF/tags/bug58178b.tag | 20 + test/webapp/WEB-INF/tags/bug62453.tag | 26 + test/webapp/WEB-INF/tags/bug65390.tag | 20 + test/webapp/WEB-INF/tags/circular01.tag | 25 + test/webapp/WEB-INF/tags/circular02.tag | 25 + test/webapp/WEB-INF/tags/dobody.tagx | 26 + .../WEB-INF/tags/echo-deferred-method.tag | 19 + test/webapp/WEB-INF/tags/echo-deferred.tag | 17 + test/webapp/WEB-INF/tags/echo-double.tag | 20 + test/webapp/WEB-INF/tags/echo-long.tag | 20 + test/webapp/WEB-INF/tags/echo-noel.tag | 19 + test/webapp/WEB-INF/tags/echo.tag | 19 + .../tags/error-on-el-not-found-false.tag | 18 + .../tags/error-on-el-not-found-true.tag | 18 + test/webapp/WEB-INF/tags/forward.tag | 18 + test/webapp/WEB-INF/tags/implicit.tld | 23 + test/webapp/WEB-INF/tags/invoke.tagx | 24 + test/webapp/WEB-INF/tags/jsp-root.tagx | 21 + test/webapp/WEB-INF/tags/no-jsp-root.tagx | 19 + test/webapp/WEB-INF/tags/setters.tag | 22 + .../WEB-INF/tags/variable-from-attr.tag | 21 + test/webapp/WEB-INF/tags/variable.tag | 23 + test/webapp/WEB-INF/test.tld | 64 + test/webapp/WEB-INF/web.xml | 338 + test/webapp/annotations.jsp | 28 + test/webapp/bug36923.jsp | 24 + test/webapp/bug42390.jsp | 18 + test/webapp/bug42565.jsp | 38 + test/webapp/bug43nnn/bug43400.jsp | 24 + test/webapp/bug44994.jsp | 25 + test/webapp/bug45nnn/bug45015a.jsp | 32 + test/webapp/bug45nnn/bug45015b.jsp | 23 + test/webapp/bug45nnn/bug45015c.jsp | 23 + test/webapp/bug45nnn/bug45427.jsp | 39 + test/webapp/bug45nnn/bug45451.jspf | 38 + test/webapp/bug45nnn/bug45451a.jsp | 26 + test/webapp/bug45nnn/bug45451b.jsp | 19 + test/webapp/bug45nnn/bug45451c.jsp | 19 + test/webapp/bug45nnn/bug45451d.jspx | 42 + test/webapp/bug45nnn/bug45451e.jsp | 19 + test/webapp/bug45nnn/bug45511.jsp | 24 + test/webapp/bug46381.jsp | 28 + test/webapp/bug46596.jsp | 25 + test/webapp/bug47331.jsp | 23 + test/webapp/bug47413.jsp | 51 + test/webapp/bug47977.jspx | 26 + test/webapp/bug48nnn/bug48112.jsp | 24 + test/webapp/bug48nnn/bug48616.jsp | 21 + test/webapp/bug48nnn/bug48616b.jsp | 31 + test/webapp/bug48nnn/bug48627.jsp | 24 + test/webapp/bug48nnn/bug48668a.jsp | 60 + test/webapp/bug48nnn/bug48668b.jsp | 26 + test/webapp/bug48nnn/bug48701-TVI-NFA.jsp | 20 + test/webapp/bug48nnn/bug48701-TVI-NG.jsp | 20 + test/webapp/bug48nnn/bug48701-UseBean.jsp | 19 + test/webapp/bug48nnn/bug48701-VI.jsp | 20 + test/webapp/bug48nnn/bug48701-fail.jsp | 19 + test/webapp/bug48nnn/bug48827.jspx | 30 + test/webapp/bug49nnn/bug49196.jsp | 26 + .../webapp/bug49nnn/bug49297DuplicateAttr.jsp | 23 + .../bug49nnn/bug49297MultipleImport1.jsp | 28 + .../bug49nnn/bug49297MultipleImport2.jsp | 29 + .../bug49297MultiplePageEncoding1.jsp | 24 + .../bug49297MultiplePageEncoding2.jsp | 23 + .../bug49297MultiplePageEncoding3.jsp | 24 + .../bug49297MultiplePageEncoding4.jsp | 23 + test/webapp/bug49nnn/bug49297NoSpace.jsp | 23 + .../webapp/bug49nnn/bug49297NoSpaceStrict.jsp | 23 + test/webapp/bug49nnn/bug49297Tag.jsp | 23 + test/webapp/bug49nnn/bug49464-cp1252.txt | 1 + test/webapp/bug49nnn/bug49464-ibm850.txt | 1 + test/webapp/bug49nnn/bug49464-iso-8859-1.txt | 1 + test/webapp/bug49nnn/bug49464-utf-8-bom.txt | 1 + test/webapp/bug49nnn/bug49464-utf-8.txt | 1 + test/webapp/bug49nnn/bug49555.jsp | 18 + test/webapp/bug49nnn/bug49726a.jsp | 24 + test/webapp/bug49nnn/bug49726b.jsp | 23 + test/webapp/bug49nnn/bug49799.jsp | 40 + test/webapp/bug53257/foo bar.jsp | 21 + test/webapp/bug53257/foo bar.txt | 1 + test/webapp/bug53257/foo bar/foobar.jsp | 21 + test/webapp/bug53257/foo bar/foobar.txt | 1 + test/webapp/bug53257/foo#bar.jsp | 21 + test/webapp/bug53257/foo#bar.txt | 1 + test/webapp/bug53257/foo%bar.jsp | 21 + test/webapp/bug53257/foo%bar.txt | 1 + test/webapp/bug53257/foo&bar.jsp | 21 + test/webapp/bug53257/foo&bar.txt | 1 + test/webapp/bug53257/foo+bar.jsp | 21 + test/webapp/bug53257/foo+bar.txt | 1 + test/webapp/bug53257/foo;bar.jsp | 21 + test/webapp/bug53257/foo;bar.txt | 1 + test/webapp/bug53257/index.jsp | 36 + test/webapp/bug5nnnn/bug50408.jsp | 26 + test/webapp/bug5nnnn/bug51544.jsp | 26 + test/webapp/bug5nnnn/bug52335.jsp | 26 + test/webapp/bug5nnnn/bug53387.shtml | 27 + test/webapp/bug5nnnn/bug53465.jsp | 29 + test/webapp/bug5nnnn/bug53467].jsp | 21 + test/webapp/bug5nnnn/bug53545.html | 21 + test/webapp/bug5nnnn/bug53545.jsp | 34 + test/webapp/bug5nnnn/bug53986.jsp | 22 + test/webapp/bug5nnnn/bug54011.jsp | 25 + test/webapp/bug5nnnn/bug54012.jsp | 24 + test/webapp/bug5nnnn/bug54144.jsp | 35 + test/webapp/bug5nnnn/bug54241a.jsp | 28 + test/webapp/bug5nnnn/bug54241b.jsp | 34 + test/webapp/bug5nnnn/bug54242.jsp | 29 + test/webapp/bug5nnnn/bug54338.jsp | 35 + test/webapp/bug5nnnn/bug54801a.jspx | 23 + test/webapp/bug5nnnn/bug54801b.jspx | 23 + test/webapp/bug5nnnn/bug54821a.jspx | 21 + test/webapp/bug5nnnn/bug54821b.jspx | 21 + test/webapp/bug5nnnn/bug54888.jsp | 27 + test/webapp/bug5nnnn/bug55198.jsp | 27 + test/webapp/bug5nnnn/bug55262-coda.jspf | 17 + test/webapp/bug5nnnn/bug55262-prelude.jspf | 17 + test/webapp/bug5nnnn/bug55262.jsp | 17 + test/webapp/bug5nnnn/bug55642a.jsp | 21 + test/webapp/bug5nnnn/bug55642b.jsp | 18 + test/webapp/bug5nnnn/bug55807.jsp | 30 + test/webapp/bug5nnnn/bug56029.jspx | 26 + test/webapp/bug5nnnn/bug56147.jsp | 33 + test/webapp/bug5nnnn/bug56265.jsp | 30 + test/webapp/bug5nnnn/bug56334and56561.jspx | 68 + test/webapp/bug5nnnn/bug56529.jsp | 24 + test/webapp/bug5nnnn/bug56581.jsp | 27 + test/webapp/bug5nnnn/bug56612.jsp | 17 + test/webapp/bug5nnnn/bug57141.jsp | 26 + test/webapp/bug5nnnn/bug57142.jsp | 24 + test/webapp/bug5nnnn/bug57441.jsp | 22 + test/webapp/bug5nnnn/bug57601.jsp | 18 + test/webapp/bug5nnnn/bug57601.txt | 16 + test/webapp/bug5nnnn/bug58096.jsp | 17 + test/webapp/bug5nnnn/bug58178.jsp | 30 + test/webapp/bug5nnnn/bug58178b.jsp | 21 + test/webapp/bug5nnnn/bug58178c.jsp | 63 + test/webapp/bug5nnnn/bug58444a.jsp | 23 + test/webapp/bug5nnnn/bug58444b.jsp | 23 + test/webapp/bug66609/_listing.xslt | 90 + test/webapp/bug66609/a&a.txt | 1 + test/webapp/bug66609/b'b.txt | 1 + test/webapp/bug6nnnn/bug60032.jsp | 24 + test/webapp/bug6nnnn/bug60431.jsp | 26 + test/webapp/bug6nnnn/bug61854.jsp | 24 + test/webapp/bug6nnnn/bug62453.jsp | 24 + test/webapp/bug6nnnn/bug63359a.jsp | 202 + test/webapp/bug6nnnn/bug64373.jsp | 23 + test/webapp/bug6nnnn/bug64872-bigdecimal.jsp | 30 + test/webapp/bug6nnnn/bug64872-biginteger.jsp | 30 + test/webapp/bug6nnnn/bug64872-boolean.jsp | 37 + test/webapp/bug6nnnn/bug64872-byte.jsp | 32 + test/webapp/bug6nnnn/bug64872-character.jsp | 30 + test/webapp/bug6nnnn/bug64872-double.jsp | 30 + test/webapp/bug6nnnn/bug64872-float.jsp | 30 + test/webapp/bug6nnnn/bug64872-integer.jsp | 30 + test/webapp/bug6nnnn/bug64872-long.jsp | 30 + .../bug6nnnn/bug64872-primitive-boolean.jsp | 37 + .../bug6nnnn/bug64872-primitive-byte.jsp | 32 + .../bug6nnnn/bug64872-primitive-character.jsp | 30 + .../bug6nnnn/bug64872-primitive-double.jsp | 30 + .../bug6nnnn/bug64872-primitive-float.jsp | 30 + .../bug6nnnn/bug64872-primitive-integer.jsp | 30 + .../bug6nnnn/bug64872-primitive-long.jsp | 30 + .../bug6nnnn/bug64872-primitive-short.jsp | 30 + test/webapp/bug6nnnn/bug64872-short.jsp | 30 + test/webapp/bug6nnnn/bug64872-string.jsp | 33 + test/webapp/bug6nnnn/bug64872-timeunit.jsp | 30 + test/webapp/bug6nnnn/bug64872b-timeunit.jsp | 29 + test/webapp/bug6nnnn/bug65377.jsp | 39 + test/webapp/bug6nnnn/bug65390-empty.jsp | 17 + test/webapp/bug6nnnn/bug65390.jsp | 24 + test/webapp/bug6nnnn/bug66441.jsp | 23 + test/webapp/bug6nnnn/bug66582.jsp | 22 + test/webapp/bug6nnnn/bug69303.txt | 18 + test/webapp/echo-params.jsp | 33 + test/webapp/el-method.jsp | 38 + test/webapp/el-misc-no-quote-attribute-el.jsp | 44 + .../el-misc-with-quote-attribute-el.jsp | 44 + test/webapp/index.html | 24 + test/webapp/index.html.br | Bin 0 -> 367 bytes test/webapp/index.html.gz | Bin 0 -> 562 bytes .../jsp/doc-version-invalid/document-0.4.jspx | 25 + .../jsp/doc-version-invalid/document-1.1.jspx | 25 + .../doc-version-invalid/document-1.2.1.jspx | 25 + .../jsp/doc-version-invalid/document-1.3.jspx | 25 + .../jsp/doc-version-invalid/document-1.9.jspx | 25 + .../jsp/doc-version-invalid/document-2.4.jspx | 25 + .../jsp/doc-version-invalid/document-3.2.jspx | 25 + .../jsp/doc-version-invalid/document-4.0.jspx | 25 + .../jsp/doc-version-invalid/document-5.4.jspx | 25 + .../jsp/doc-version-valid/document-1.2.jspx | 25 + .../jsp/doc-version-valid/document-2.0.jspx | 25 + .../jsp/doc-version-valid/document-2.1.jspx | 25 + .../jsp/doc-version-valid/document-2.2.jspx | 25 + .../jsp/doc-version-valid/document-2.3.jspx | 25 + .../jsp/doc-version-valid/document-3.0.jspx | 25 + .../jsp/doc-version-valid/document-3.1.jspx | 25 + test/webapp/jsp/encoding/README.txt | 40 + .../jsp/encoding/bom-none-prolog-none.jsp | 17 + .../jsp/encoding/bom-none-prolog-none.jspx | 21 + .../jsp/encoding/bom-none-prolog-utf16be.jspx | Bin 0 -> 2010 bytes .../jsp/encoding/bom-none-prolog-utf16le.jspx | Bin 0 -> 2010 bytes .../jsp/encoding/bom-none-prolog-utf8.jspx | 21 + .../jsp/encoding/bom-utf16be-prolog-none.jsp | Bin 0 -> 1764 bytes .../jsp/encoding/bom-utf16be-prolog-none.jspx | Bin 0 -> 1970 bytes .../encoding/bom-utf16be-prolog-utf16be.jspx | Bin 0 -> 2010 bytes .../encoding/bom-utf16be-prolog-utf16le.jspx | Bin 0 -> 2010 bytes .../jsp/encoding/bom-utf16be-prolog-utf8.jspx | Bin 0 -> 2004 bytes .../jsp/encoding/bom-utf16le-prolog-none.jsp | Bin 0 -> 1764 bytes .../jsp/encoding/bom-utf16le-prolog-none.jspx | Bin 0 -> 1970 bytes .../encoding/bom-utf16le-prolog-utf16be.jspx | Bin 0 -> 2010 bytes .../encoding/bom-utf16le-prolog-utf16le.jspx | Bin 0 -> 2010 bytes .../jsp/encoding/bom-utf16le-prolog-utf8.jspx | Bin 0 -> 2004 bytes .../jsp/encoding/bom-utf8-prolog-none.jsp | 17 + .../jsp/encoding/bom-utf8-prolog-none.jspx | 21 + .../jsp/encoding/bom-utf8-prolog-utf16be.jspx | 21 + .../jsp/encoding/bom-utf8-prolog-utf16le.jspx | 21 + .../jsp/encoding/bom-utf8-prolog-utf8.jspx | 21 + test/webapp/jsp/encoding/bug60769a.jspx | 21 + test/webapp/jsp/encoding/bug60769b.jspx | 21 + test/webapp/jsp/error.jsp | 22 + test/webapp/jsp/errorOnELNotFound/default.jsp | 21 + .../page-directive-false.jsp | 22 + .../errorOnELNotFound/page-directive-true.jsp | 22 + .../jsp/errorOnELNotFound/tag-file-false.jsp | 23 + .../jsp/errorOnELNotFound/tag-file-true.jsp | 23 + .../jsp/errorOnELNotFound/web-xml-false.jsp | 21 + .../jsp/errorOnELNotFound/web-xml-true.jsp | 21 + test/webapp/jsp/forward.jsp | 17 + test/webapp/jsp/generator/attribute-01.jsp | 27 + test/webapp/jsp/generator/attribute-02.jsp | 36 + test/webapp/jsp/generator/attribute-03.jsp | 24 + test/webapp/jsp/generator/attribute-04.jsp | 62 + test/webapp/jsp/generator/beaninfo-01.jsp | 18 + .../jsp/generator/break-el-interpreter.jsp | 19 + .../generator/break-string-interpreter.jsp | 19 + test/webapp/jsp/generator/circular-01.jsp | 18 + test/webapp/jsp/generator/customtag-02.jsp | 23 + test/webapp/jsp/generator/customtag-03.jsp | 19 + test/webapp/jsp/generator/customtag-04.jsp | 19 + .../jsp/generator/deferred-method-01.jsp | 24 + .../jsp/generator/deferred-method-02.jsp | 24 + test/webapp/jsp/generator/dobody-01.jsp | 20 + test/webapp/jsp/generator/element-01.jsp | 25 + test/webapp/jsp/generator/forward-01.jsp | 21 + test/webapp/jsp/generator/forward-02.jsp | 22 + test/webapp/jsp/generator/forward-03.jsp | 20 + test/webapp/jsp/generator/forward-04.jsp | 18 + test/webapp/jsp/generator/include-01.jsp | 24 + .../jsp/generator/info-conflict-none.jsp | 18 + test/webapp/jsp/generator/info-conflict.jsp | 18 + test/webapp/jsp/generator/info.jsp | 17 + test/webapp/jsp/generator/invoke-01.jsp | 20 + test/webapp/jsp/generator/jsp-id.jsp | 19 + test/webapp/jsp/generator/jsp-id.jspx | 23 + test/webapp/jsp/generator/plugin-01.jspx | 54 + .../jsp/generator/scriptingvariables-01.jsp | 25 + .../jsp/generator/scriptingvariables-02.jsp | 24 + test/webapp/jsp/generator/setproperty-01.jsp | 32 + test/webapp/jsp/generator/setters-01.jsp | 18 + test/webapp/jsp/generator/single-threaded.jsp | 17 + test/webapp/jsp/generator/templatetext-01.jsp | 283 + test/webapp/jsp/generator/templatetext-02.jsp | 19 + .../jsp/generator/try-catch-finally-01.jsp | 22 + .../jsp/generator/try-catch-finally-02.jsp | 18 + test/webapp/jsp/generator/usebean-01.jsp | 20 + test/webapp/jsp/generator/usebean-02.jsp | 20 + test/webapp/jsp/generator/usebean-03.jsp | 38 + test/webapp/jsp/generator/usebean-04.jsp | 20 + test/webapp/jsp/generator/usebean-05.jsp | 20 + test/webapp/jsp/generator/usebean-06.jsp | 20 + test/webapp/jsp/generator/usebean-07.jsp | 20 + test/webapp/jsp/generator/usebean-08.jsp | 30 + .../generator/variable-from-attr-nested.jsp | 21 + .../variable-tagfile-from-attr-nested.jsp | 21 + .../jsp/generator/variable-tagfile-nested.jsp | 28 + .../jsp/generator/variable-tei-nested.jsp | 25 + test/webapp/jsp/generator/x-powered-by.jsp | 18 + test/webapp/jsp/generator/xml-doctype-01.jspx | 23 + test/webapp/jsp/generator/xml-doctype-02.jspx | 24 + test/webapp/jsp/generator/xml-prolog-01.jspx | 22 + test/webapp/jsp/generator/xml-prolog-02.jspx | 22 + test/webapp/jsp/generator/xml-prolog-tag.jspx | 21 + test/webapp/jsp/includeThenForward.jsp | 19 + test/webapp/jsp/ok.html | 5 + test/webapp/jsp/pageContext1.jsp | 35 + test/webapp/jsp/pageContext2.jsp | 18 + test/webapp/jsp/session.jsp | 18 + test/webapp/jsp/tagFileInJar.jsp | 18 + test/webapp/jsp/test.jsp | 18 + test/webapp/jsp/trim-spaces-extended.jsp | 49 + test/webapp/script-expr.jsp | 34 + test/webapp/valid.jspx | 22 + test/webapp/valid.xsd | 20 + test/webapp/welcome-files/index.jsp | 21 + test/webapp/welcome-files/sub/.gitignore | 24 + test/webresources/dir1-internal.jar | Bin 0 -> 960 bytes test/webresources/dir1.jar | Bin 0 -> 778 bytes test/webresources/dir1/META-INF/MANIFEST.MF | 2 + test/webresources/dir1/d1/d1-f1.txt | 0 test/webresources/dir1/d2/d2-f1.txt | 0 test/webresources/dir1/f1.txt | 0 test/webresources/dir1/f2.txt | 0 test/webresources/dir2/d1/.ignore-me.txt | 21 + test/webresources/dir2/d2/.ignore-me.txt | 21 + test/webresources/dir3/.ignore-me.txt | 21 + test/webresources/non-static-resources.jar | Bin 0 -> 1681 bytes test/webresources/war-url-connection.war | Bin 0 -> 868 bytes webapps/ROOT/WEB-INF/web.xml | 30 + webapps/ROOT/asf-logo-wide.svg | 295 + webapps/ROOT/bg-button.png | Bin 0 -> 713 bytes webapps/ROOT/bg-middle.png | Bin 0 -> 1918 bytes webapps/ROOT/bg-nav.png | Bin 0 -> 1401 bytes webapps/ROOT/bg-upper.png | Bin 0 -> 3103 bytes webapps/ROOT/favicon.ico | Bin 0 -> 21630 bytes webapps/ROOT/index.jsp | 219 + webapps/ROOT/tomcat.css | 398 + webapps/ROOT/tomcat.svg | 967 +++ webapps/docs/META-INF/context.xml | 21 + webapps/docs/WEB-INF/jsp/403.jsp | 44 + webapps/docs/WEB-INF/web.xml | 35 + webapps/docs/aio.xml | 87 + webapps/docs/annotationapi/index.html | 34 + webapps/docs/api/index.html | 34 + webapps/docs/appdev/build.xml.txt | 508 ++ webapps/docs/appdev/deployment.xml | 249 + webapps/docs/appdev/index.xml | 79 + webapps/docs/appdev/installation.xml | 103 + webapps/docs/appdev/introduction.xml | 88 + webapps/docs/appdev/processes.xml | 290 + webapps/docs/appdev/project.xml | 48 + webapps/docs/appdev/sample/docs/README.txt | 17 + webapps/docs/appdev/sample/index.html | 55 + webapps/docs/appdev/sample/sample.war | Bin 0 -> 4704 bytes .../appdev/sample/src/mypackage/Hello.java | 83 + .../docs/appdev/sample/web/WEB-INF/web.xml | 40 + webapps/docs/appdev/sample/web/hello.jsp | 37 + .../docs/appdev/sample/web/images/tomcat.gif | Bin 0 -> 2066 bytes webapps/docs/appdev/sample/web/index.html | 39 + webapps/docs/appdev/source.xml | 289 + webapps/docs/appdev/web.xml.txt | 165 + webapps/docs/apr.xml | 120 + webapps/docs/architecture/index.xml | 69 + webapps/docs/architecture/overview.xml | 138 + webapps/docs/architecture/project.xml | 45 + webapps/docs/architecture/requestProcess.xml | 74 + .../requestProcess/authentication-process.png | Bin 0 -> 42682 bytes .../requestProcess/request-process.png | Bin 0 -> 109471 bytes webapps/docs/architecture/startup.xml | 73 + .../architecture/startup/serverStartup.pdf | Bin 0 -> 46175 bytes .../architecture/startup/serverStartup.txt | 139 + webapps/docs/balancer-howto.xml | 55 + webapps/docs/building.xml | 267 + webapps/docs/cdi.xml | 187 + webapps/docs/cgi-howto.xml | 168 + webapps/docs/changelog.xml | 4551 ++++++++++++ webapps/docs/class-loader-howto.xml | 294 + webapps/docs/cluster-howto.xml | 707 ++ webapps/docs/comments.xml | 118 + webapps/docs/config/ajp.xml | 892 +++ webapps/docs/config/automatic-deployment.xml | 548 ++ webapps/docs/config/cluster-channel.xml | 146 + webapps/docs/config/cluster-deployer.xml | 109 + webapps/docs/config/cluster-interceptor.xml | 320 + webapps/docs/config/cluster-listener.xml | 67 + webapps/docs/config/cluster-manager.xml | 293 + webapps/docs/config/cluster-membership.xml | 327 + webapps/docs/config/cluster-receiver.xml | 167 + webapps/docs/config/cluster-sender.xml | 176 + webapps/docs/config/cluster-valve.xml | 172 + webapps/docs/config/cluster.xml | 214 + webapps/docs/config/context.xml | 1481 ++++ webapps/docs/config/cookie-processor.xml | 148 + webapps/docs/config/credentialhandler.xml | 219 + webapps/docs/config/engine.xml | 264 + webapps/docs/config/executor.xml | 152 + webapps/docs/config/filter.xml | 2027 ++++++ webapps/docs/config/globalresources.xml | 284 + webapps/docs/config/host.xml | 689 ++ webapps/docs/config/http.xml | 1792 +++++ webapps/docs/config/http2.xml | 260 + webapps/docs/config/index.xml | 96 + webapps/docs/config/jar-scan-filter.xml | 191 + webapps/docs/config/jar-scanner.xml | 147 + webapps/docs/config/jaspic.xml | 203 + webapps/docs/config/listeners.xml | 654 ++ webapps/docs/config/loader.xml | 149 + webapps/docs/config/manager.xml | 612 ++ webapps/docs/config/project.xml | 98 + webapps/docs/config/realm.xml | 1116 +++ webapps/docs/config/resources.xml | 371 + webapps/docs/config/server.xml | 159 + webapps/docs/config/service.xml | 119 + webapps/docs/config/sessionidgenerator.xml | 131 + webapps/docs/config/systemprops.xml | 393 + webapps/docs/config/valve.xml | 2678 +++++++ webapps/docs/connectors.xml | 80 + webapps/docs/default-servlet.xml | 342 + webapps/docs/deployer-howto.xml | 353 + webapps/docs/developers.xml | 79 + webapps/docs/elapi/index.html | 34 + webapps/docs/graal.xml | 229 + webapps/docs/host-manager-howto.xml | 239 + webapps/docs/html-host-manager-howto.xml | 212 + webapps/docs/html-manager-howto.xml | 539 ++ webapps/docs/images/add.gif | Bin 0 -> 1037 bytes webapps/docs/images/asf-logo.svg | 226 + webapps/docs/images/code.gif | Bin 0 -> 394 bytes webapps/docs/images/cors-flowchart.png | Bin 0 -> 86555 bytes webapps/docs/images/design.gif | Bin 0 -> 608 bytes webapps/docs/images/docs-stylesheet.css | 303 + webapps/docs/images/docs.gif | Bin 0 -> 261 bytes webapps/docs/images/fix.gif | Bin 0 -> 345 bytes webapps/docs/images/fonts/OpenSans400.woff | Bin 0 -> 21956 bytes .../docs/images/fonts/OpenSans400italic.woff | Bin 0 -> 21092 bytes webapps/docs/images/fonts/OpenSans600.woff | Bin 0 -> 22604 bytes .../docs/images/fonts/OpenSans600italic.woff | Bin 0 -> 21252 bytes webapps/docs/images/fonts/OpenSans700.woff | Bin 0 -> 22748 bytes .../docs/images/fonts/OpenSans700italic.woff | Bin 0 -> 21184 bytes webapps/docs/images/fonts/fonts.css | 54 + webapps/docs/images/tomcat.gif | Bin 0 -> 2066 bytes webapps/docs/images/tomcat.png | Bin 0 -> 5103 bytes webapps/docs/images/update.gif | Bin 0 -> 627 bytes webapps/docs/images/void.gif | Bin 0 -> 43 bytes webapps/docs/index.xml | 231 + webapps/docs/introduction.xml | 304 + webapps/docs/jasper-howto.xml | 520 ++ webapps/docs/jaspicapi/index.html | 34 + .../docs/jndi-datasource-examples-howto.xml | 682 ++ webapps/docs/jndi-resources-howto.xml | 1204 ++++ webapps/docs/jspapi/index.html | 34 + webapps/docs/logging.xml | 430 ++ webapps/docs/manager-howto.xml | 1483 ++++ webapps/docs/maven-jars.xml | 59 + webapps/docs/mbeans-descriptors-howto.xml | 86 + webapps/docs/monitoring.xml | 1198 +++ webapps/docs/project.xml | 109 + webapps/docs/proxy-howto.xml | 145 + webapps/docs/realm-howto.xml | 1127 +++ webapps/docs/rewrite.xml | 823 +++ webapps/docs/security-howto.xml | 598 ++ webapps/docs/security-manager-howto.xml | 256 + webapps/docs/servletapi/index.html | 34 + webapps/docs/setup.xml | 195 + webapps/docs/ssi-howto.xml | 440 ++ webapps/docs/ssl-howto.xml | 672 ++ webapps/docs/tomcat-docs.xsl | 451 ++ webapps/docs/tribes/developers.xml | 37 + webapps/docs/tribes/faq.xml | 37 + webapps/docs/tribes/interceptors.xml | 37 + webapps/docs/tribes/introduction.xml | 273 + .../leader-election-initiate-election.dia | Bin 0 -> 2732 bytes .../leader-election-initiate-election.jpg | Bin 0 -> 33664 bytes .../leader-election-message-arrives.dia | Bin 0 -> 5796 bytes .../leader-election-message-arrives.jpg | Bin 0 -> 111284 bytes webapps/docs/tribes/membership.xml | 37 + webapps/docs/tribes/project.xml | 58 + webapps/docs/tribes/setup.xml | 37 + webapps/docs/tribes/status.xml | 37 + webapps/docs/tribes/transport.xml | 37 + webapps/docs/virtual-hosting-howto.xml | 145 + webapps/docs/web-socket-howto.xml | 189 + webapps/docs/websocketapi/index.html | 34 + webapps/docs/windows-auth-howto.xml | 342 + webapps/docs/windows-service-howto.xml | 585 ++ webapps/examples/META-INF/context.xml | 23 + .../WEB-INF/classes/CookieExample.java | 140 + .../WEB-INF/classes/HelloWorldExample.java | 79 + .../WEB-INF/classes/LocalStrings.properties | 48 + .../classes/LocalStrings_cs.properties | 22 + .../classes/LocalStrings_de.properties | 28 + .../classes/LocalStrings_es.properties | 48 + .../classes/LocalStrings_fr.properties | 48 + .../classes/LocalStrings_ja.properties | 48 + .../classes/LocalStrings_ko.properties | 48 + .../classes/LocalStrings_pt.properties | 48 + .../classes/LocalStrings_pt_BR.properties | 16 + .../classes/LocalStrings_ru.properties | 47 + .../classes/LocalStrings_zh_CN.properties | 48 + .../WEB-INF/classes/RequestHeaderExample.java | 184 + .../WEB-INF/classes/RequestInfoExample.java | 118 + .../WEB-INF/classes/RequestParamExample.java | 111 + .../WEB-INF/classes/ServletToJsp.java | 39 + .../WEB-INF/classes/SessionExample.java | 147 + .../WEB-INF/classes/async/Async0.java | 69 + .../WEB-INF/classes/async/Async1.java | 60 + .../WEB-INF/classes/async/Async2.java | 66 + .../WEB-INF/classes/async/Async3.java | 39 + .../async/AsyncStockContextListener.java | 44 + .../classes/async/AsyncStockServlet.java | 144 + .../WEB-INF/classes/async/Stockticker.java | 212 + .../examples/WEB-INF/classes/cal/Entries.java | 63 + .../examples/WEB-INF/classes/cal/Entry.java | 52 + .../WEB-INF/classes/cal/JspCalendar.java | 152 + .../WEB-INF/classes/cal/TableBean.java | 106 + .../WEB-INF/classes/checkbox/CheckTest.java | 30 + .../WEB-INF/classes/colors/ColorGameBean.java | 114 + .../compressionFilters/CompressionFilter.java | 224 + .../CompressionFilterTestServlet.java | 65 + .../CompressionResponseStream.java | 448 ++ .../CompressionServletResponseWrapper.java | 295 + .../WEB-INF/classes/dates/JspCalendar.java | 155 + .../examples/WEB-INF/classes/error/Smart.java | 30 + .../classes/examples/ExampleTagBase.java | 74 + .../WEB-INF/classes/examples/FooTag.java | 87 + .../classes/examples/FooTagExtraInfo.java | 36 + .../WEB-INF/classes/examples/LogTag.java | 63 + .../WEB-INF/classes/examples/ValuesTag.java | 79 + .../classes/filters/ExampleFilter.java | 99 + .../classes/http2/SimpleImagePush.java | 59 + .../classes/jsp2/examples/BookBean.java | 42 + .../classes/jsp2/examples/FooBean.java | 34 + .../classes/jsp2/examples/ValuesBean.java | 50 + .../classes/jsp2/examples/el/Functions.java | 45 + .../examples/simpletag/EchoAttributesTag.java | 56 + .../examples/simpletag/FindBookSimpleTag.java | 44 + .../simpletag/HelloWorldSimpleTag.java | 32 + .../examples/simpletag/RepeatSimpleTag.java | 42 + .../examples/simpletag/ShuffleSimpleTag.java | 85 + .../examples/simpletag/TileSimpleTag.java | 46 + .../classes/listeners/ContextListener.java | 139 + .../classes/listeners/SessionListener.java | 161 + .../classes/nonblocking/ByteCounter.java | 142 + .../classes/nonblocking/NumberWriter.java | 148 + .../WEB-INF/classes/num/NumberGuessBean.java | 98 + .../WEB-INF/classes/sessions/DummyCart.java | 66 + .../classes/trailers/ResponseTrailers.java | 68 + .../WEB-INF/classes/util/CookieFilter.java | 85 + .../WEB-INF/classes/util/HTMLFilter.java | 69 + .../classes/validators/DebugValidator.java | 83 + .../classes/websocket/ExamplesConfig.java | 66 + .../websocket/chat/ChatAnnotation.java | 109 + .../classes/websocket/drawboard/Client.java | 231 + .../websocket/drawboard/DrawMessage.java | 253 + .../drawboard/DrawboardContextListener.java | 32 + .../drawboard/DrawboardEndpoint.java | 236 + .../classes/websocket/drawboard/Room.java | 497 ++ .../wsmessages/AbstractWebsocketMessage.java | 25 + .../wsmessages/BinaryWebsocketMessage.java | 34 + .../wsmessages/CloseWebsocketMessage.java | 24 + .../wsmessages/StringWebsocketMessage.java | 34 + .../websocket/echo/EchoAnnotation.java | 75 + .../websocket/echo/EchoAsyncAnnotation.java | 128 + .../classes/websocket/echo/EchoEndpoint.java | 80 + .../websocket/echo/EchoStreamAnnotation.java | 75 + .../classes/websocket/echo/servers.json | 20 + .../classes/websocket/snake/Direction.java | 21 + .../classes/websocket/snake/Location.java | 73 + .../classes/websocket/snake/Snake.java | 150 + .../websocket/snake/SnakeAnnotation.java | 142 + .../classes/websocket/snake/SnakeTimer.java | 115 + webapps/examples/WEB-INF/jsp/403.jsp | 44 + webapps/examples/WEB-INF/jsp/debug-taglib.tld | 54 + .../examples/WEB-INF/jsp/example-taglib.tld | 107 + .../WEB-INF/jsp/jsp2-example-taglib.tld | 124 + .../examples/WEB-INF/tags/displayProducts.tag | 55 + webapps/examples/WEB-INF/tags/helloWorld.tag | 17 + webapps/examples/WEB-INF/tags/panel.tag | 29 + webapps/examples/WEB-INF/web.xml | 423 ++ webapps/examples/index.html | 30 + webapps/examples/jsp/async/async1.jsp | 28 + webapps/examples/jsp/async/async3.jsp | 25 + webapps/examples/jsp/async/index.jsp | 69 + webapps/examples/jsp/cal/cal1.jsp | 94 + webapps/examples/jsp/cal/cal2.jsp | 45 + webapps/examples/jsp/cal/calendar.html | 43 + webapps/examples/jsp/cal/login.html | 47 + webapps/examples/jsp/checkbox/CheckTest.html | 56 + webapps/examples/jsp/checkbox/check.html | 38 + webapps/examples/jsp/checkbox/checkresult.jsp | 65 + webapps/examples/jsp/checkbox/cresult.html | 34 + .../examples/jsp/colors/ColorGameBean.html | 116 + webapps/examples/jsp/colors/clr.html | 34 + webapps/examples/jsp/colors/colors.html | 47 + webapps/examples/jsp/colors/colrs.jsp | 70 + webapps/examples/jsp/dates/date.html | 31 + webapps/examples/jsp/dates/date.jsp | 41 + webapps/examples/jsp/error/er.html | 31 + webapps/examples/jsp/error/err.jsp | 44 + webapps/examples/jsp/error/error.html | 37 + webapps/examples/jsp/error/errorpge.jsp | 25 + webapps/examples/jsp/forward/forward.jsp | 33 + webapps/examples/jsp/forward/fwd.html | 30 + webapps/examples/jsp/forward/one.jsp | 23 + webapps/examples/jsp/forward/two.html | 23 + webapps/examples/jsp/images/code.gif | Bin 0 -> 292 bytes webapps/examples/jsp/images/execute.gif | Bin 0 -> 1242 bytes webapps/examples/jsp/images/return.gif | Bin 0 -> 1231 bytes webapps/examples/jsp/include/foo.html | 17 + webapps/examples/jsp/include/foo.jsp | 17 + webapps/examples/jsp/include/inc.html | 30 + webapps/examples/jsp/include/include.jsp | 30 + webapps/examples/jsp/index.html | 361 + .../jsp/jsp2/el/basic-arithmetic.html | 30 + .../examples/jsp/jsp2/el/basic-arithmetic.jsp | 88 + .../jsp/jsp2/el/basic-comparisons.html | 30 + .../jsp/jsp2/el/basic-comparisons.jsp | 116 + webapps/examples/jsp/jsp2/el/composite.html | 31 + webapps/examples/jsp/jsp2/el/composite.jsp | 110 + webapps/examples/jsp/jsp2/el/functions.html | 32 + webapps/examples/jsp/jsp2/el/functions.jsp | 67 + .../jsp/jsp2/el/implicit-objects.html | 31 + .../examples/jsp/jsp2/el/implicit-objects.jsp | 90 + .../jsp/jsp2/jspattribute/jspattribute.html | 37 + .../jsp/jsp2/jspattribute/jspattribute.jsp | 46 + .../jsp/jsp2/jspattribute/shuffle.html | 37 + .../jsp/jsp2/jspattribute/shuffle.jsp | 90 + webapps/examples/jsp/jsp2/jspx/basic.html | 31 + webapps/examples/jsp/jsp2/jspx/basic.jspx | 48 + .../examples/jsp/jsp2/jspx/svgexample.html | 46 + .../examples/jsp/jsp2/jspx/textRotate.html | 32 + webapps/examples/jsp/jsp2/jspx/textRotate.jpg | Bin 0 -> 26729 bytes .../examples/jsp/jsp2/jspx/textRotate.jspx | 53 + webapps/examples/jsp/jsp2/misc/coda.jspf | 21 + webapps/examples/jsp/jsp2/misc/config.html | 35 + webapps/examples/jsp/jsp2/misc/config.jsp | 32 + .../examples/jsp/jsp2/misc/dynamicattrs.html | 33 + .../examples/jsp/jsp2/misc/dynamicattrs.jsp | 44 + webapps/examples/jsp/jsp2/misc/prelude.jspf | 21 + webapps/examples/jsp/jsp2/simpletag/book.html | 37 + webapps/examples/jsp/jsp2/simpletag/book.jsp | 55 + .../examples/jsp/jsp2/simpletag/hello.html | 33 + webapps/examples/jsp/jsp2/simpletag/hello.jsp | 31 + .../examples/jsp/jsp2/simpletag/repeat.html | 33 + .../examples/jsp/jsp2/simpletag/repeat.jsp | 39 + webapps/examples/jsp/jsp2/tagfiles/hello.html | 33 + webapps/examples/jsp/jsp2/tagfiles/hello.jsp | 35 + webapps/examples/jsp/jsp2/tagfiles/panel.html | 33 + webapps/examples/jsp/jsp2/tagfiles/panel.jsp | 58 + .../examples/jsp/jsp2/tagfiles/products.html | 33 + .../examples/jsp/jsp2/tagfiles/products.jsp | 54 + webapps/examples/jsp/jsptoserv/hello.jsp | 26 + .../examples/jsp/jsptoserv/jsptoservlet.jsp | 23 + webapps/examples/jsp/jsptoserv/jts.html | 36 + webapps/examples/jsp/num/numguess.html | 34 + webapps/examples/jsp/num/numguess.jsp | 69 + .../examples/jsp/security/protected/error.jsp | 25 + .../examples/jsp/security/protected/index.jsp | 163 + .../examples/jsp/security/protected/login.jsp | 38 + webapps/examples/jsp/sessions/DummyCart.html | 56 + webapps/examples/jsp/sessions/carts.html | 53 + webapps/examples/jsp/sessions/carts.jsp | 43 + webapps/examples/jsp/sessions/crt.html | 34 + webapps/examples/jsp/simpletag/foo.html | 30 + webapps/examples/jsp/simpletag/foo.jsp | 38 + webapps/examples/jsp/snp/snoop.html | 31 + webapps/examples/jsp/snp/snoop.jsp | 56 + webapps/examples/jsp/tagplugin/choose.html | 36 + webapps/examples/jsp/tagplugin/choose.jsp | 54 + webapps/examples/jsp/tagplugin/foreach.html | 36 + webapps/examples/jsp/tagplugin/foreach.jsp | 54 + webapps/examples/jsp/tagplugin/howto.html | 45 + webapps/examples/jsp/tagplugin/if.html | 36 + webapps/examples/jsp/tagplugin/if.jsp | 47 + webapps/examples/jsp/tagplugin/notes.html | 41 + webapps/examples/jsp/xml/xml.html | 31 + webapps/examples/jsp/xml/xml.jsp | 70 + webapps/examples/servlets/cookies.html | 61 + webapps/examples/servlets/helloworld.html | 50 + webapps/examples/servlets/images/code.gif | Bin 0 -> 292 bytes webapps/examples/servlets/images/execute.gif | Bin 0 -> 1242 bytes webapps/examples/servlets/images/return.gif | Bin 0 -> 1231 bytes webapps/examples/servlets/index.html | 193 + .../servlets/nonblocking/bytecounter.html | 32 + webapps/examples/servlets/reqheaders.html | 49 + webapps/examples/servlets/reqinfo.html | 68 + webapps/examples/servlets/reqparams.html | 82 + webapps/examples/servlets/sessions.html | 70 + webapps/examples/websocket/chat.xhtml | 136 + webapps/examples/websocket/drawboard.xhtml | 899 +++ webapps/examples/websocket/echo.xhtml | 184 + webapps/examples/websocket/index.xhtml | 32 + webapps/examples/websocket/snake.xhtml | 266 + webapps/host-manager/META-INF/context.xml | 24 + webapps/host-manager/WEB-INF/jsp/401.jsp | 71 + webapps/host-manager/WEB-INF/jsp/403.jsp | 90 + webapps/host-manager/WEB-INF/jsp/404.jsp | 62 + webapps/host-manager/WEB-INF/manager.xml | 30 + webapps/host-manager/WEB-INF/web.xml | 148 + webapps/host-manager/css/manager.css | 141 + webapps/host-manager/images/asf-logo.svg | 226 + webapps/host-manager/images/tomcat.svg | 967 +++ webapps/host-manager/index.jsp | 18 + webapps/manager/META-INF/context.xml | 24 + webapps/manager/WEB-INF/jsp/401.jsp | 80 + webapps/manager/WEB-INF/jsp/403.jsp | 100 + webapps/manager/WEB-INF/jsp/404.jsp | 63 + .../manager/WEB-INF/jsp/connectorCerts.jsp | 92 + .../manager/WEB-INF/jsp/connectorCiphers.jsp | 92 + .../WEB-INF/jsp/connectorTrustedCerts.jsp | 92 + webapps/manager/WEB-INF/jsp/sessionDetail.jsp | 197 + webapps/manager/WEB-INF/jsp/sessionsList.jsp | 170 + webapps/manager/WEB-INF/web.xml | 212 + webapps/manager/css/manager.css | 141 + webapps/manager/images/asf-logo.svg | 226 + webapps/manager/images/tomcat.svg | 967 +++ webapps/manager/index.jsp | 18 + webapps/manager/status.xsd | 84 + webapps/manager/xform.xsl | 140 + 4549 files changed, 818829 insertions(+), 39 deletions(-) create mode 100644 .editorconfig create mode 100644 BUILDING.txt create mode 100644 CONTRIBUTING.md create mode 100644 KEYS create mode 100644 LICENSE create mode 100644 MERGE.txt create mode 100644 NOTICE create mode 100644 RELEASE-NOTES create mode 100644 RUNNING.txt create mode 100644 bin/catalina-tasks.xml create mode 100755 bin/catalina.bat create mode 100755 bin/catalina.sh create mode 100755 bin/ciphers.bat create mode 100755 bin/ciphers.sh create mode 100755 bin/configtest.bat create mode 100755 bin/configtest.sh create mode 100755 bin/daemon.sh create mode 100755 bin/digest.bat create mode 100755 bin/digest.sh create mode 100755 bin/makebase.bat create mode 100755 bin/makebase.sh create mode 100755 bin/migrate.bat create mode 100755 bin/migrate.sh create mode 100755 bin/service.bat create mode 100755 bin/setclasspath.bat create mode 100755 bin/setclasspath.sh create mode 100755 bin/shutdown.bat create mode 100755 bin/shutdown.sh create mode 100755 bin/startup.bat create mode 100755 bin/startup.sh create mode 100755 bin/tool-wrapper.bat create mode 100755 bin/tool-wrapper.sh create mode 100755 bin/version.bat create mode 100755 bin/version.sh create mode 100644 build.properties.default create mode 100644 build.properties.release create mode 100644 build.xml create mode 100644 conf/catalina.policy create mode 100644 conf/catalina.properties create mode 100644 conf/context.xml create mode 100644 conf/jaspic-providers.xml create mode 100644 conf/jaspic-providers.xsd create mode 100644 conf/logging.properties create mode 100644 conf/server.xml create mode 100644 conf/tomcat-users.xml create mode 100644 conf/tomcat-users.xsd create mode 100644 conf/web.xml create mode 100644 debian/README.Debian create mode 100644 debian/ant.properties create mode 100644 debian/clean delete mode 100644 debian/compat create mode 100644 debian/context/docs.xml create mode 100644 debian/context/examples.xml create mode 100644 debian/context/host-manager.xml create mode 100644 debian/context/manager.xml create mode 100644 debian/default.template create mode 100644 debian/default_root/META-INF/context.xml create mode 100644 debian/default_root/index.html create mode 100644 debian/libexec/tomcat-locate-java.sh create mode 100755 debian/libexec/tomcat-start.sh create mode 100755 debian/libexec/tomcat-update-policy.sh create mode 100644 debian/libtomcat10-embed-java.manifest create mode 100644 debian/libtomcat10-embed-java.poms create mode 100644 debian/libtomcat10-java.lintian-overrides create mode 100644 debian/libtomcat10-java.manifest create mode 100644 debian/libtomcat10-java.poms create mode 100644 debian/logging.properties create mode 100644 debian/logrotate.template create mode 100644 debian/maven.rules create mode 100644 debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch create mode 100644 debian/patches/0005-skip-test-failures.patch create mode 100644 debian/patches/0009-Use-java.security.policy-file-in-catalina.sh.patch create mode 100644 debian/patches/0010-debianize-build-xml.patch create mode 100644 debian/patches/0013-dont-look-for-build-properties-in-user-home.patch create mode 100644 debian/patches/0018-fix-manager-webapp.patch create mode 100644 debian/patches/0019-add-distribution-to-error-page.patch create mode 100644 debian/patches/0021-dont-test-unsupported-ciphers.patch create mode 100644 debian/patches/0023-disable-shutdown-by-socket.patch create mode 100644 debian/patches/0024-systemd-log-formatter.patch create mode 100644 debian/patches/0025-invalid-configuration-exit-status.patch create mode 100644 debian/patches/0026-easymock4-compatibility.patch create mode 100644 debian/patches/disable-jacoco.patch create mode 100644 debian/patches/exclude-TestJNDIRealmIntegration.patch create mode 100644 debian/patches/series create mode 100644 debian/policy/01system.policy create mode 100644 debian/policy/02debian.policy create mode 100644 debian/policy/03catalina.policy create mode 100644 debian/policy/04webapps.policy create mode 100644 debian/policy/50local.policy create mode 100644 debian/policy/examples/10examples.policy create mode 100644 debian/rsyslog/tomcat10.conf create mode 100755 debian/setenv.sh create mode 100644 debian/sysusers/tomcat10.conf create mode 100644 debian/tomcat10-admin.install create mode 100644 debian/tomcat10-common.docs create mode 100644 debian/tomcat10-common.install create mode 100644 debian/tomcat10-common.links create mode 100644 debian/tomcat10-docs.doc-base create mode 100644 debian/tomcat10-docs.install create mode 100644 debian/tomcat10-docs.links create mode 100644 debian/tomcat10-docs.lintian-overrides create mode 100644 debian/tomcat10-examples.install create mode 100644 debian/tomcat10-instance-create create mode 100644 debian/tomcat10-instance-create.1 create mode 100644 debian/tomcat10-user.install create mode 100644 debian/tomcat10-user.manpages create mode 100644 debian/tomcat10.cron.daily create mode 100644 debian/tomcat10.dirs create mode 100644 debian/tomcat10.install create mode 100644 debian/tomcat10.links create mode 100644 debian/tomcat10.postinst create mode 100644 debian/tomcat10.postrm.in create mode 100644 debian/tomcat10.service create mode 100644 debian/tomcat10.tmpfiles create mode 100644 debian/watch create mode 100644 java/jakarta/annotation/Generated.java create mode 100644 java/jakarta/annotation/ManagedBean.java create mode 100644 java/jakarta/annotation/Nonnull.java create mode 100644 java/jakarta/annotation/Nullable.java create mode 100644 java/jakarta/annotation/PostConstruct.java create mode 100644 java/jakarta/annotation/PreDestroy.java create mode 100644 java/jakarta/annotation/Priority.java create mode 100644 java/jakarta/annotation/Resource.java create mode 100644 java/jakarta/annotation/Resources.java create mode 100644 java/jakarta/annotation/security/DeclareRoles.java create mode 100644 java/jakarta/annotation/security/DenyAll.java create mode 100644 java/jakarta/annotation/security/PermitAll.java create mode 100644 java/jakarta/annotation/security/RolesAllowed.java create mode 100644 java/jakarta/annotation/security/RunAs.java create mode 100644 java/jakarta/annotation/sql/DataSourceDefinition.java create mode 100644 java/jakarta/annotation/sql/DataSourceDefinitions.java create mode 100644 java/jakarta/ejb/EJB.java create mode 100644 java/jakarta/ejb/EJBs.java create mode 100644 java/jakarta/el/ArrayELResolver.java create mode 100644 java/jakarta/el/BeanELResolver.java create mode 100644 java/jakarta/el/BeanNameELResolver.java create mode 100644 java/jakarta/el/BeanNameResolver.java create mode 100644 java/jakarta/el/CompositeELResolver.java create mode 100644 java/jakarta/el/ELClass.java create mode 100644 java/jakarta/el/ELContext.java create mode 100644 java/jakarta/el/ELContextEvent.java create mode 100644 java/jakarta/el/ELContextListener.java create mode 100644 java/jakarta/el/ELException.java create mode 100644 java/jakarta/el/ELManager.java create mode 100644 java/jakarta/el/ELProcessor.java create mode 100644 java/jakarta/el/ELResolver.java create mode 100644 java/jakarta/el/EvaluationListener.java create mode 100644 java/jakarta/el/Expression.java create mode 100644 java/jakarta/el/ExpressionFactory.java create mode 100644 java/jakarta/el/FunctionMapper.java create mode 100644 java/jakarta/el/ImportHandler.java create mode 100644 java/jakarta/el/LambdaExpression.java create mode 100644 java/jakarta/el/ListELResolver.java create mode 100644 java/jakarta/el/LocalStrings.properties create mode 100644 java/jakarta/el/LocalStrings_cs.properties create mode 100644 java/jakarta/el/LocalStrings_de.properties create mode 100644 java/jakarta/el/LocalStrings_es.properties create mode 100644 java/jakarta/el/LocalStrings_fr.properties create mode 100644 java/jakarta/el/LocalStrings_ja.properties create mode 100644 java/jakarta/el/LocalStrings_ko.properties create mode 100644 java/jakarta/el/LocalStrings_pt_BR.properties create mode 100644 java/jakarta/el/LocalStrings_ru.properties create mode 100644 java/jakarta/el/LocalStrings_zh_CN.properties create mode 100644 java/jakarta/el/MapELResolver.java create mode 100644 java/jakarta/el/MethodExpression.java create mode 100644 java/jakarta/el/MethodInfo.java create mode 100644 java/jakarta/el/MethodNotFoundException.java create mode 100644 java/jakarta/el/MethodReference.java create mode 100644 java/jakarta/el/PropertyNotFoundException.java create mode 100644 java/jakarta/el/PropertyNotWritableException.java create mode 100644 java/jakarta/el/ResourceBundleELResolver.java create mode 100644 java/jakarta/el/StandardELContext.java create mode 100644 java/jakarta/el/StaticFieldELResolver.java create mode 100644 java/jakarta/el/TypeConverter.java create mode 100644 java/jakarta/el/Util.java create mode 100644 java/jakarta/el/ValueExpression.java create mode 100644 java/jakarta/el/ValueReference.java create mode 100644 java/jakarta/el/VariableMapper.java create mode 100644 java/jakarta/mail/Authenticator.java create mode 100644 java/jakarta/mail/PasswordAuthentication.java create mode 100644 java/jakarta/mail/Session.java create mode 100644 java/jakarta/mail/internet/InternetAddress.java create mode 100644 java/jakarta/mail/internet/MimeMessage.java create mode 100644 java/jakarta/mail/internet/MimePart.java create mode 100644 java/jakarta/mail/internet/MimePartDataSource.java create mode 100644 java/jakarta/persistence/PersistenceContext.java create mode 100644 java/jakarta/persistence/PersistenceContextType.java create mode 100644 java/jakarta/persistence/PersistenceContexts.java create mode 100644 java/jakarta/persistence/PersistenceProperty.java create mode 100644 java/jakarta/persistence/PersistenceUnit.java create mode 100644 java/jakarta/persistence/PersistenceUnits.java create mode 100644 java/jakarta/persistence/SynchronizationType.java create mode 100644 java/jakarta/security/auth/message/AuthException.java create mode 100644 java/jakarta/security/auth/message/AuthStatus.java create mode 100644 java/jakarta/security/auth/message/ClientAuth.java create mode 100644 java/jakarta/security/auth/message/MessageInfo.java create mode 100644 java/jakarta/security/auth/message/MessagePolicy.java create mode 100644 java/jakarta/security/auth/message/ServerAuth.java create mode 100644 java/jakarta/security/auth/message/callback/CallerPrincipalCallback.java create mode 100644 java/jakarta/security/auth/message/callback/CertStoreCallback.java create mode 100644 java/jakarta/security/auth/message/callback/GroupPrincipalCallback.java create mode 100644 java/jakarta/security/auth/message/callback/PasswordValidationCallback.java create mode 100644 java/jakarta/security/auth/message/callback/PrivateKeyCallback.java create mode 100644 java/jakarta/security/auth/message/callback/SecretKeyCallback.java create mode 100644 java/jakarta/security/auth/message/callback/TrustStoreCallback.java create mode 100644 java/jakarta/security/auth/message/config/AuthConfig.java create mode 100644 java/jakarta/security/auth/message/config/AuthConfigFactory.java create mode 100644 java/jakarta/security/auth/message/config/AuthConfigProvider.java create mode 100644 java/jakarta/security/auth/message/config/ClientAuthConfig.java create mode 100644 java/jakarta/security/auth/message/config/ClientAuthContext.java create mode 100644 java/jakarta/security/auth/message/config/RegistrationListener.java create mode 100644 java/jakarta/security/auth/message/config/ServerAuthConfig.java create mode 100644 java/jakarta/security/auth/message/config/ServerAuthContext.java create mode 100644 java/jakarta/security/auth/message/module/ClientAuthModule.java create mode 100644 java/jakarta/security/auth/message/module/ServerAuthModule.java create mode 100644 java/jakarta/servlet/AsyncContext.java create mode 100644 java/jakarta/servlet/AsyncEvent.java create mode 100644 java/jakarta/servlet/AsyncListener.java create mode 100644 java/jakarta/servlet/DispatcherType.java create mode 100644 java/jakarta/servlet/Filter.java create mode 100644 java/jakarta/servlet/FilterChain.java create mode 100644 java/jakarta/servlet/FilterConfig.java create mode 100644 java/jakarta/servlet/FilterRegistration.java create mode 100644 java/jakarta/servlet/GenericFilter.java create mode 100644 java/jakarta/servlet/GenericServlet.java create mode 100644 java/jakarta/servlet/HttpConstraintElement.java create mode 100644 java/jakarta/servlet/HttpMethodConstraintElement.java create mode 100644 java/jakarta/servlet/LocalStrings.properties create mode 100644 java/jakarta/servlet/LocalStrings_cs.properties create mode 100644 java/jakarta/servlet/LocalStrings_de.properties create mode 100644 java/jakarta/servlet/LocalStrings_es.properties create mode 100644 java/jakarta/servlet/LocalStrings_fr.properties create mode 100644 java/jakarta/servlet/LocalStrings_ja.properties create mode 100644 java/jakarta/servlet/LocalStrings_ko.properties create mode 100644 java/jakarta/servlet/LocalStrings_ru.properties create mode 100644 java/jakarta/servlet/LocalStrings_zh_CN.properties create mode 100644 java/jakarta/servlet/MultipartConfigElement.java create mode 100644 java/jakarta/servlet/ReadListener.java create mode 100644 java/jakarta/servlet/Registration.java create mode 100644 java/jakarta/servlet/RequestDispatcher.java create mode 100644 java/jakarta/servlet/Servlet.java create mode 100644 java/jakarta/servlet/ServletConfig.java create mode 100644 java/jakarta/servlet/ServletConnection.java create mode 100644 java/jakarta/servlet/ServletContainerInitializer.java create mode 100644 java/jakarta/servlet/ServletContext.java create mode 100644 java/jakarta/servlet/ServletContextAttributeEvent.java create mode 100644 java/jakarta/servlet/ServletContextAttributeListener.java create mode 100644 java/jakarta/servlet/ServletContextEvent.java create mode 100644 java/jakarta/servlet/ServletContextListener.java create mode 100644 java/jakarta/servlet/ServletException.java create mode 100644 java/jakarta/servlet/ServletInputStream.java create mode 100644 java/jakarta/servlet/ServletOutputStream.java create mode 100644 java/jakarta/servlet/ServletRegistration.java create mode 100644 java/jakarta/servlet/ServletRequest.java create mode 100644 java/jakarta/servlet/ServletRequestAttributeEvent.java create mode 100644 java/jakarta/servlet/ServletRequestAttributeListener.java create mode 100644 java/jakarta/servlet/ServletRequestEvent.java create mode 100644 java/jakarta/servlet/ServletRequestListener.java create mode 100644 java/jakarta/servlet/ServletRequestWrapper.java create mode 100644 java/jakarta/servlet/ServletResponse.java create mode 100644 java/jakarta/servlet/ServletResponseWrapper.java create mode 100644 java/jakarta/servlet/ServletSecurityElement.java create mode 100644 java/jakarta/servlet/SessionCookieConfig.java create mode 100644 java/jakarta/servlet/SessionTrackingMode.java create mode 100644 java/jakarta/servlet/UnavailableException.java create mode 100644 java/jakarta/servlet/WriteListener.java create mode 100644 java/jakarta/servlet/annotation/HandlesTypes.java create mode 100644 java/jakarta/servlet/annotation/HttpConstraint.java create mode 100644 java/jakarta/servlet/annotation/HttpMethodConstraint.java create mode 100644 java/jakarta/servlet/annotation/MultipartConfig.java create mode 100644 java/jakarta/servlet/annotation/ServletSecurity.java create mode 100644 java/jakarta/servlet/annotation/WebFilter.java create mode 100644 java/jakarta/servlet/annotation/WebInitParam.java create mode 100644 java/jakarta/servlet/annotation/WebListener.java create mode 100644 java/jakarta/servlet/annotation/WebServlet.java create mode 100644 java/jakarta/servlet/descriptor/JspConfigDescriptor.java create mode 100644 java/jakarta/servlet/descriptor/JspPropertyGroupDescriptor.java create mode 100644 java/jakarta/servlet/descriptor/TaglibDescriptor.java create mode 100644 java/jakarta/servlet/http/Cookie.java create mode 100644 java/jakarta/servlet/http/HttpFilter.java create mode 100644 java/jakarta/servlet/http/HttpServlet.java create mode 100644 java/jakarta/servlet/http/HttpServletMapping.java create mode 100644 java/jakarta/servlet/http/HttpServletRequest.java create mode 100644 java/jakarta/servlet/http/HttpServletRequestWrapper.java create mode 100644 java/jakarta/servlet/http/HttpServletResponse.java create mode 100644 java/jakarta/servlet/http/HttpServletResponseWrapper.java create mode 100644 java/jakarta/servlet/http/HttpSession.java create mode 100644 java/jakarta/servlet/http/HttpSessionActivationListener.java create mode 100644 java/jakarta/servlet/http/HttpSessionAttributeListener.java create mode 100644 java/jakarta/servlet/http/HttpSessionBindingEvent.java create mode 100644 java/jakarta/servlet/http/HttpSessionBindingListener.java create mode 100644 java/jakarta/servlet/http/HttpSessionEvent.java create mode 100644 java/jakarta/servlet/http/HttpSessionIdListener.java create mode 100644 java/jakarta/servlet/http/HttpSessionListener.java create mode 100644 java/jakarta/servlet/http/HttpUpgradeHandler.java create mode 100644 java/jakarta/servlet/http/LocalStrings.properties create mode 100644 java/jakarta/servlet/http/LocalStrings_de.properties create mode 100644 java/jakarta/servlet/http/LocalStrings_es.properties create mode 100644 java/jakarta/servlet/http/LocalStrings_fr.properties create mode 100644 java/jakarta/servlet/http/LocalStrings_ja.properties create mode 100644 java/jakarta/servlet/http/LocalStrings_ko.properties create mode 100644 java/jakarta/servlet/http/LocalStrings_zh_CN.properties create mode 100644 java/jakarta/servlet/http/MappingMatch.java create mode 100644 java/jakarta/servlet/http/Part.java create mode 100644 java/jakarta/servlet/http/PushBuilder.java create mode 100644 java/jakarta/servlet/http/WebConnection.java create mode 100644 java/jakarta/servlet/http/package.html create mode 100644 java/jakarta/servlet/jsp/ErrorData.java create mode 100644 java/jakarta/servlet/jsp/HttpJspPage.java create mode 100644 java/jakarta/servlet/jsp/JspApplicationContext.java create mode 100644 java/jakarta/servlet/jsp/JspContext.java create mode 100644 java/jakarta/servlet/jsp/JspEngineInfo.java create mode 100644 java/jakarta/servlet/jsp/JspException.java create mode 100644 java/jakarta/servlet/jsp/JspFactory.java create mode 100644 java/jakarta/servlet/jsp/JspPage.java create mode 100644 java/jakarta/servlet/jsp/JspTagException.java create mode 100644 java/jakarta/servlet/jsp/JspWriter.java create mode 100644 java/jakarta/servlet/jsp/LocalStrings.properties create mode 100644 java/jakarta/servlet/jsp/LocalStrings_de.properties create mode 100644 java/jakarta/servlet/jsp/LocalStrings_es.properties create mode 100644 java/jakarta/servlet/jsp/LocalStrings_fr.properties create mode 100644 java/jakarta/servlet/jsp/LocalStrings_ja.properties create mode 100644 java/jakarta/servlet/jsp/LocalStrings_ko.properties create mode 100644 java/jakarta/servlet/jsp/LocalStrings_zh_CN.properties create mode 100644 java/jakarta/servlet/jsp/PageContext.java create mode 100644 java/jakarta/servlet/jsp/SkipPageException.java create mode 100644 java/jakarta/servlet/jsp/el/ELException.java create mode 100644 java/jakarta/servlet/jsp/el/ELParseException.java create mode 100644 java/jakarta/servlet/jsp/el/Expression.java create mode 100644 java/jakarta/servlet/jsp/el/ExpressionEvaluator.java create mode 100644 java/jakarta/servlet/jsp/el/FunctionMapper.java create mode 100644 java/jakarta/servlet/jsp/el/ImplicitObjectELResolver.java create mode 100644 java/jakarta/servlet/jsp/el/ImportELResolver.java create mode 100644 java/jakarta/servlet/jsp/el/NotFoundELResolver.java create mode 100644 java/jakarta/servlet/jsp/el/ScopedAttributeELResolver.java create mode 100644 java/jakarta/servlet/jsp/el/VariableResolver.java create mode 100644 java/jakarta/servlet/jsp/el/package.html create mode 100644 java/jakarta/servlet/jsp/package.html create mode 100644 java/jakarta/servlet/jsp/resources/jspxml.dtd create mode 100644 java/jakarta/servlet/jsp/resources/jspxml.xsd create mode 100644 java/jakarta/servlet/jsp/tagext/BodyContent.java create mode 100644 java/jakarta/servlet/jsp/tagext/BodyTag.java create mode 100644 java/jakarta/servlet/jsp/tagext/BodyTagSupport.java create mode 100644 java/jakarta/servlet/jsp/tagext/DynamicAttributes.java create mode 100644 java/jakarta/servlet/jsp/tagext/FunctionInfo.java create mode 100644 java/jakarta/servlet/jsp/tagext/IterationTag.java create mode 100644 java/jakarta/servlet/jsp/tagext/JspFragment.java create mode 100644 java/jakarta/servlet/jsp/tagext/JspIdConsumer.java create mode 100644 java/jakarta/servlet/jsp/tagext/JspTag.java create mode 100644 java/jakarta/servlet/jsp/tagext/PageData.java create mode 100644 java/jakarta/servlet/jsp/tagext/SimpleTag.java create mode 100644 java/jakarta/servlet/jsp/tagext/SimpleTagSupport.java create mode 100644 java/jakarta/servlet/jsp/tagext/Tag.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagAdapter.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagAttributeInfo.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagData.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagExtraInfo.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagFileInfo.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagInfo.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagLibraryInfo.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagLibraryValidator.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagSupport.java create mode 100644 java/jakarta/servlet/jsp/tagext/TagVariableInfo.java create mode 100644 java/jakarta/servlet/jsp/tagext/TryCatchFinally.java create mode 100644 java/jakarta/servlet/jsp/tagext/ValidationMessage.java create mode 100644 java/jakarta/servlet/jsp/tagext/VariableInfo.java create mode 100644 java/jakarta/servlet/jsp/tagext/doc-files/BodyTagProtocol.gif create mode 100644 java/jakarta/servlet/jsp/tagext/doc-files/IterationTagProtocol.gif create mode 100644 java/jakarta/servlet/jsp/tagext/doc-files/TagProtocol.gif create mode 100644 java/jakarta/servlet/jsp/tagext/doc-files/VariableInfo-1.gif create mode 100644 java/jakarta/servlet/jsp/tagext/package.html create mode 100644 java/jakarta/servlet/package.html create mode 100644 java/jakarta/servlet/resources/XMLSchema.dtd create mode 100644 java/jakarta/servlet/resources/datatypes.dtd create mode 100644 java/jakarta/servlet/resources/j2ee_1_4.xsd create mode 100644 java/jakarta/servlet/resources/j2ee_web_services_1_1.xsd create mode 100644 java/jakarta/servlet/resources/j2ee_web_services_client_1_1.xsd create mode 100644 java/jakarta/servlet/resources/jakartaee_10.xsd create mode 100644 java/jakarta/servlet/resources/jakartaee_9.xsd create mode 100644 java/jakarta/servlet/resources/jakartaee_web_services_2_0.xsd create mode 100644 java/jakarta/servlet/resources/jakartaee_web_services_client_2_0.xsd create mode 100644 java/jakarta/servlet/resources/javaee_5.xsd create mode 100644 java/jakarta/servlet/resources/javaee_6.xsd create mode 100644 java/jakarta/servlet/resources/javaee_7.xsd create mode 100644 java/jakarta/servlet/resources/javaee_8.xsd create mode 100644 java/jakarta/servlet/resources/javaee_web_services_1_2.xsd create mode 100644 java/jakarta/servlet/resources/javaee_web_services_1_3.xsd create mode 100644 java/jakarta/servlet/resources/javaee_web_services_1_4.xsd create mode 100644 java/jakarta/servlet/resources/javaee_web_services_client_1_2.xsd create mode 100644 java/jakarta/servlet/resources/javaee_web_services_client_1_3.xsd create mode 100644 java/jakarta/servlet/resources/javaee_web_services_client_1_4.xsd create mode 100644 java/jakarta/servlet/resources/jsp_2_0.xsd create mode 100644 java/jakarta/servlet/resources/jsp_2_1.xsd create mode 100644 java/jakarta/servlet/resources/jsp_2_2.xsd create mode 100644 java/jakarta/servlet/resources/jsp_2_3.xsd create mode 100644 java/jakarta/servlet/resources/jsp_3_0.xsd create mode 100644 java/jakarta/servlet/resources/jsp_3_1.xsd create mode 100644 java/jakarta/servlet/resources/web-app_2_2.dtd create mode 100644 java/jakarta/servlet/resources/web-app_2_3.dtd create mode 100644 java/jakarta/servlet/resources/web-app_2_4.xsd create mode 100644 java/jakarta/servlet/resources/web-app_2_5.xsd create mode 100644 java/jakarta/servlet/resources/web-app_3_0.xsd create mode 100644 java/jakarta/servlet/resources/web-app_3_1.xsd create mode 100644 java/jakarta/servlet/resources/web-app_4_0.xsd create mode 100644 java/jakarta/servlet/resources/web-app_5_0.xsd create mode 100644 java/jakarta/servlet/resources/web-app_6_0.xsd create mode 100644 java/jakarta/servlet/resources/web-common_3_0.xsd create mode 100644 java/jakarta/servlet/resources/web-common_3_1.xsd create mode 100644 java/jakarta/servlet/resources/web-common_4_0.xsd create mode 100644 java/jakarta/servlet/resources/web-common_5_0.xsd create mode 100644 java/jakarta/servlet/resources/web-common_6_0.xsd create mode 100644 java/jakarta/servlet/resources/web-fragment_3_0.xsd create mode 100644 java/jakarta/servlet/resources/web-fragment_3_1.xsd create mode 100644 java/jakarta/servlet/resources/web-fragment_4_0.xsd create mode 100644 java/jakarta/servlet/resources/web-fragment_5_0.xsd create mode 100644 java/jakarta/servlet/resources/web-fragment_6_0.xsd create mode 100644 java/jakarta/servlet/resources/web-jsptaglibrary_1_1.dtd create mode 100644 java/jakarta/servlet/resources/web-jsptaglibrary_1_2.dtd create mode 100644 java/jakarta/servlet/resources/web-jsptaglibrary_2_0.xsd create mode 100644 java/jakarta/servlet/resources/web-jsptaglibrary_2_1.xsd create mode 100644 java/jakarta/servlet/resources/web-jsptaglibrary_3_0.xsd create mode 100644 java/jakarta/servlet/resources/web-jsptaglibrary_3_1.xsd create mode 100644 java/jakarta/servlet/resources/xml.xsd create mode 100644 java/jakarta/transaction/HeuristicCommitException.java create mode 100644 java/jakarta/transaction/HeuristicMixedException.java create mode 100644 java/jakarta/transaction/HeuristicRollbackException.java create mode 100644 java/jakarta/transaction/InvalidTransactionException.java create mode 100644 java/jakarta/transaction/NotSupportedException.java create mode 100644 java/jakarta/transaction/RollbackException.java create mode 100644 java/jakarta/transaction/Status.java create mode 100644 java/jakarta/transaction/Synchronization.java create mode 100644 java/jakarta/transaction/SystemException.java create mode 100644 java/jakarta/transaction/Transaction.java create mode 100644 java/jakarta/transaction/TransactionManager.java create mode 100644 java/jakarta/transaction/TransactionRequiredException.java create mode 100644 java/jakarta/transaction/TransactionRolledbackException.java create mode 100644 java/jakarta/transaction/TransactionSynchronizationRegistry.java create mode 100644 java/jakarta/transaction/UserTransaction.java create mode 100644 java/jakarta/websocket/ClientEndpoint.java create mode 100644 java/jakarta/websocket/ClientEndpointConfig.java create mode 100644 java/jakarta/websocket/CloseReason.java create mode 100644 java/jakarta/websocket/ContainerProvider.java create mode 100644 java/jakarta/websocket/DecodeException.java create mode 100644 java/jakarta/websocket/Decoder.java create mode 100644 java/jakarta/websocket/DefaultClientEndpointConfig.java create mode 100644 java/jakarta/websocket/DeploymentException.java create mode 100644 java/jakarta/websocket/EncodeException.java create mode 100644 java/jakarta/websocket/Encoder.java create mode 100644 java/jakarta/websocket/Endpoint.java create mode 100644 java/jakarta/websocket/EndpointConfig.java create mode 100644 java/jakarta/websocket/Extension.java create mode 100644 java/jakarta/websocket/HandshakeResponse.java create mode 100644 java/jakarta/websocket/MessageHandler.java create mode 100644 java/jakarta/websocket/OnClose.java create mode 100644 java/jakarta/websocket/OnError.java create mode 100644 java/jakarta/websocket/OnMessage.java create mode 100644 java/jakarta/websocket/OnOpen.java create mode 100644 java/jakarta/websocket/PongMessage.java create mode 100644 java/jakarta/websocket/RemoteEndpoint.java create mode 100644 java/jakarta/websocket/SendHandler.java create mode 100644 java/jakarta/websocket/SendResult.java create mode 100644 java/jakarta/websocket/Session.java create mode 100644 java/jakarta/websocket/SessionException.java create mode 100644 java/jakarta/websocket/WebSocketContainer.java create mode 100644 java/jakarta/websocket/server/DefaultServerEndpointConfig.java create mode 100644 java/jakarta/websocket/server/HandshakeRequest.java create mode 100644 java/jakarta/websocket/server/PathParam.java create mode 100644 java/jakarta/websocket/server/ServerApplicationConfig.java create mode 100644 java/jakarta/websocket/server/ServerContainer.java create mode 100644 java/jakarta/websocket/server/ServerEndpoint.java create mode 100644 java/jakarta/websocket/server/ServerEndpointConfig.java create mode 100644 java/jakarta/xml/ws/WebServiceRef.java create mode 100644 java/jakarta/xml/ws/WebServiceRefs.java create mode 100644 java/org/apache/catalina/AccessLog.java create mode 100644 java/org/apache/catalina/AsyncDispatcher.java create mode 100644 java/org/apache/catalina/Authenticator.java create mode 100644 java/org/apache/catalina/Cluster.java create mode 100644 java/org/apache/catalina/Contained.java create mode 100644 java/org/apache/catalina/Container.java create mode 100644 java/org/apache/catalina/ContainerEvent.java create mode 100644 java/org/apache/catalina/ContainerListener.java create mode 100644 java/org/apache/catalina/ContainerServlet.java create mode 100644 java/org/apache/catalina/Context.java create mode 100644 java/org/apache/catalina/CredentialHandler.java create mode 100644 java/org/apache/catalina/DistributedManager.java create mode 100644 java/org/apache/catalina/Engine.java create mode 100644 java/org/apache/catalina/Executor.java create mode 100644 java/org/apache/catalina/Globals.java create mode 100644 java/org/apache/catalina/Group.java create mode 100644 java/org/apache/catalina/Host.java create mode 100644 java/org/apache/catalina/JmxEnabled.java create mode 100644 java/org/apache/catalina/Lifecycle.java create mode 100644 java/org/apache/catalina/LifecycleEvent.java create mode 100644 java/org/apache/catalina/LifecycleException.java create mode 100644 java/org/apache/catalina/LifecycleListener.java create mode 100644 java/org/apache/catalina/LifecycleState.java create mode 100644 java/org/apache/catalina/Loader.java create mode 100644 java/org/apache/catalina/Manager.java create mode 100644 java/org/apache/catalina/Pipeline.java create mode 100644 java/org/apache/catalina/Realm.java create mode 100644 java/org/apache/catalina/Role.java create mode 100644 java/org/apache/catalina/Server.java create mode 100644 java/org/apache/catalina/Service.java create mode 100644 java/org/apache/catalina/Session.java create mode 100644 java/org/apache/catalina/SessionEvent.java create mode 100644 java/org/apache/catalina/SessionIdGenerator.java create mode 100644 java/org/apache/catalina/SessionListener.java create mode 100644 java/org/apache/catalina/Store.java create mode 100644 java/org/apache/catalina/StoreManager.java create mode 100644 java/org/apache/catalina/ThreadBindingListener.java create mode 100644 java/org/apache/catalina/TomcatPrincipal.java create mode 100644 java/org/apache/catalina/TrackedWebResource.java create mode 100644 java/org/apache/catalina/User.java create mode 100644 java/org/apache/catalina/UserDatabase.java create mode 100644 java/org/apache/catalina/Valve.java create mode 100644 java/org/apache/catalina/WebResource.java create mode 100644 java/org/apache/catalina/WebResourceRoot.java create mode 100644 java/org/apache/catalina/WebResourceSet.java create mode 100644 java/org/apache/catalina/Wrapper.java create mode 100644 java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java create mode 100644 java/org/apache/catalina/ant/AbstractCatalinaTask.java create mode 100644 java/org/apache/catalina/ant/BaseRedirectorHelperTask.java create mode 100644 java/org/apache/catalina/ant/DeployTask.java create mode 100644 java/org/apache/catalina/ant/FindLeaksTask.java create mode 100644 java/org/apache/catalina/ant/JKStatusUpdateTask.java create mode 100644 java/org/apache/catalina/ant/JMXGetTask.java create mode 100644 java/org/apache/catalina/ant/JMXQueryTask.java create mode 100644 java/org/apache/catalina/ant/JMXSetTask.java create mode 100644 java/org/apache/catalina/ant/ListTask.java create mode 100644 java/org/apache/catalina/ant/ReloadTask.java create mode 100644 java/org/apache/catalina/ant/ResourcesTask.java create mode 100644 java/org/apache/catalina/ant/ServerinfoTask.java create mode 100644 java/org/apache/catalina/ant/SessionsTask.java create mode 100644 java/org/apache/catalina/ant/SslConnectorCiphersTask.java create mode 100644 java/org/apache/catalina/ant/StartTask.java create mode 100644 java/org/apache/catalina/ant/StopTask.java create mode 100644 java/org/apache/catalina/ant/ThreaddumpTask.java create mode 100644 java/org/apache/catalina/ant/UndeployTask.java create mode 100644 java/org/apache/catalina/ant/ValidatorTask.java create mode 100644 java/org/apache/catalina/ant/VminfoTask.java create mode 100644 java/org/apache/catalina/ant/antlib.xml create mode 100644 java/org/apache/catalina/ant/catalina.tasks create mode 100644 java/org/apache/catalina/ant/jmx/Arg.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorCondition.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorConditionBase.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorCreateTask.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorEqualsCondition.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorGetTask.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorInvokeTask.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorQueryTask.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorSetTask.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorTask.java create mode 100644 java/org/apache/catalina/ant/jmx/JMXAccessorUnregisterTask.java create mode 100644 java/org/apache/catalina/ant/jmx/antlib.xml create mode 100644 java/org/apache/catalina/ant/jmx/jmxaccessor.tasks create mode 100644 java/org/apache/catalina/ant/jmx/package.html create mode 100644 java/org/apache/catalina/ant/package.html create mode 100644 java/org/apache/catalina/authenticator/AuthenticatorBase.java create mode 100644 java/org/apache/catalina/authenticator/BasicAuthenticator.java create mode 100644 java/org/apache/catalina/authenticator/Constants.java create mode 100644 java/org/apache/catalina/authenticator/DigestAuthenticator.java create mode 100644 java/org/apache/catalina/authenticator/FormAuthenticator.java create mode 100644 java/org/apache/catalina/authenticator/LocalStrings.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/authenticator/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/authenticator/NonLoginAuthenticator.java create mode 100644 java/org/apache/catalina/authenticator/SSLAuthenticator.java create mode 100644 java/org/apache/catalina/authenticator/SavedRequest.java create mode 100644 java/org/apache/catalina/authenticator/SingleSignOn.java create mode 100644 java/org/apache/catalina/authenticator/SingleSignOnEntry.java create mode 100644 java/org/apache/catalina/authenticator/SingleSignOnListener.java create mode 100644 java/org/apache/catalina/authenticator/SingleSignOnSessionKey.java create mode 100644 java/org/apache/catalina/authenticator/SpnegoAuthenticator.java create mode 100644 java/org/apache/catalina/authenticator/jaspic/AuthConfigFactoryImpl.java create mode 100644 java/org/apache/catalina/authenticator/jaspic/CallbackHandlerImpl.java create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/authenticator/jaspic/MessageInfoImpl.java create mode 100644 java/org/apache/catalina/authenticator/jaspic/PersistentProviderRegistrations.java create mode 100644 java/org/apache/catalina/authenticator/jaspic/SimpleAuthConfigProvider.java create mode 100644 java/org/apache/catalina/authenticator/jaspic/SimpleServerAuthConfig.java create mode 100644 java/org/apache/catalina/authenticator/jaspic/SimpleServerAuthContext.java create mode 100644 java/org/apache/catalina/authenticator/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/authenticator/package.html create mode 100644 java/org/apache/catalina/connector/ClientAbortException.java create mode 100644 java/org/apache/catalina/connector/Connector.java create mode 100644 java/org/apache/catalina/connector/CoyoteAdapter.java create mode 100644 java/org/apache/catalina/connector/CoyoteInputStream.java create mode 100644 java/org/apache/catalina/connector/CoyoteOutputStream.java create mode 100644 java/org/apache/catalina/connector/CoyotePrincipal.java create mode 100644 java/org/apache/catalina/connector/CoyoteReader.java create mode 100644 java/org/apache/catalina/connector/CoyoteWriter.java create mode 100644 java/org/apache/catalina/connector/InputBuffer.java create mode 100644 java/org/apache/catalina/connector/LocalStrings.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/connector/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/connector/OutputBuffer.java create mode 100644 java/org/apache/catalina/connector/Request.java create mode 100644 java/org/apache/catalina/connector/RequestFacade.java create mode 100644 java/org/apache/catalina/connector/Response.java create mode 100644 java/org/apache/catalina/connector/ResponseFacade.java create mode 100644 java/org/apache/catalina/connector/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/core/AccessLogAdapter.java create mode 100644 java/org/apache/catalina/core/ApplicationContext.java create mode 100644 java/org/apache/catalina/core/ApplicationContextFacade.java create mode 100644 java/org/apache/catalina/core/ApplicationDispatcher.java create mode 100644 java/org/apache/catalina/core/ApplicationFilterChain.java create mode 100644 java/org/apache/catalina/core/ApplicationFilterConfig.java create mode 100644 java/org/apache/catalina/core/ApplicationFilterFactory.java create mode 100644 java/org/apache/catalina/core/ApplicationFilterRegistration.java create mode 100644 java/org/apache/catalina/core/ApplicationHttpRequest.java create mode 100644 java/org/apache/catalina/core/ApplicationHttpResponse.java create mode 100644 java/org/apache/catalina/core/ApplicationMapping.java create mode 100644 java/org/apache/catalina/core/ApplicationPart.java create mode 100644 java/org/apache/catalina/core/ApplicationPushBuilder.java create mode 100644 java/org/apache/catalina/core/ApplicationRequest.java create mode 100644 java/org/apache/catalina/core/ApplicationResponse.java create mode 100644 java/org/apache/catalina/core/ApplicationServletRegistration.java create mode 100644 java/org/apache/catalina/core/ApplicationSessionCookieConfig.java create mode 100644 java/org/apache/catalina/core/AprLifecycleListener.java create mode 100644 java/org/apache/catalina/core/AprStatus.java create mode 100644 java/org/apache/catalina/core/AsyncContextImpl.java create mode 100644 java/org/apache/catalina/core/AsyncListenerWrapper.java create mode 100644 java/org/apache/catalina/core/Constants.java create mode 100644 java/org/apache/catalina/core/ContainerBase.java create mode 100644 java/org/apache/catalina/core/ContextNamingInfoListener.java create mode 100644 java/org/apache/catalina/core/DefaultInstanceManager.java create mode 100644 java/org/apache/catalina/core/FrameworkListener.java create mode 100644 java/org/apache/catalina/core/JniLifecycleListener.java create mode 100644 java/org/apache/catalina/core/JreMemoryLeakPreventionListener.java create mode 100644 java/org/apache/catalina/core/LocalStrings.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/core/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/core/NamingContextListener.java create mode 100644 java/org/apache/catalina/core/OpenSSLLifecycleListener.java create mode 100644 java/org/apache/catalina/core/PropertiesRoleMappingListener.java create mode 100644 java/org/apache/catalina/core/RestrictedFilters.properties create mode 100644 java/org/apache/catalina/core/RestrictedListeners.properties create mode 100644 java/org/apache/catalina/core/RestrictedServlets.properties create mode 100644 java/org/apache/catalina/core/StandardContext.java create mode 100644 java/org/apache/catalina/core/StandardContextValve.java create mode 100644 java/org/apache/catalina/core/StandardEngine.java create mode 100644 java/org/apache/catalina/core/StandardEngineValve.java create mode 100644 java/org/apache/catalina/core/StandardHost.java create mode 100644 java/org/apache/catalina/core/StandardHostValve.java create mode 100644 java/org/apache/catalina/core/StandardPipeline.java create mode 100644 java/org/apache/catalina/core/StandardServer.java create mode 100644 java/org/apache/catalina/core/StandardService.java create mode 100644 java/org/apache/catalina/core/StandardThreadExecutor.java create mode 100644 java/org/apache/catalina/core/StandardVirtualThreadExecutor.java create mode 100644 java/org/apache/catalina/core/StandardWrapper.java create mode 100644 java/org/apache/catalina/core/StandardWrapperFacade.java create mode 100644 java/org/apache/catalina/core/StandardWrapperValve.java create mode 100644 java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java create mode 100644 java/org/apache/catalina/core/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/deploy/LocalStrings.properties create mode 100644 java/org/apache/catalina/deploy/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/deploy/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/deploy/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/deploy/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/deploy/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/deploy/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/deploy/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/deploy/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/deploy/NamingResourcesImpl.java create mode 100644 java/org/apache/catalina/deploy/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/filters/AddDefaultCharsetFilter.java create mode 100644 java/org/apache/catalina/filters/Constants.java create mode 100644 java/org/apache/catalina/filters/CorsFilter.java create mode 100644 java/org/apache/catalina/filters/CsrfPreventionFilter.java create mode 100644 java/org/apache/catalina/filters/CsrfPreventionFilterBase.java create mode 100644 java/org/apache/catalina/filters/ExpiresFilter.java create mode 100644 java/org/apache/catalina/filters/FailedRequestFilter.java create mode 100644 java/org/apache/catalina/filters/FilterBase.java create mode 100644 java/org/apache/catalina/filters/HttpHeaderSecurityFilter.java create mode 100644 java/org/apache/catalina/filters/LocalStrings.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/filters/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/filters/RateLimitFilter.java create mode 100644 java/org/apache/catalina/filters/RemoteAddrFilter.java create mode 100644 java/org/apache/catalina/filters/RemoteCIDRFilter.java create mode 100644 java/org/apache/catalina/filters/RemoteHostFilter.java create mode 100644 java/org/apache/catalina/filters/RemoteIpFilter.java create mode 100644 java/org/apache/catalina/filters/RequestDumperFilter.java create mode 100644 java/org/apache/catalina/filters/RequestFilter.java create mode 100644 java/org/apache/catalina/filters/RestCsrfPreventionFilter.java create mode 100644 java/org/apache/catalina/filters/SessionInitializerFilter.java create mode 100644 java/org/apache/catalina/filters/SetCharacterEncodingFilter.java create mode 100644 java/org/apache/catalina/filters/WebdavFixFilter.java create mode 100644 java/org/apache/catalina/ha/CatalinaCluster.java create mode 100644 java/org/apache/catalina/ha/ClusterDeployer.java create mode 100644 java/org/apache/catalina/ha/ClusterListener.java create mode 100644 java/org/apache/catalina/ha/ClusterManager.java create mode 100644 java/org/apache/catalina/ha/ClusterMessage.java create mode 100644 java/org/apache/catalina/ha/ClusterMessageBase.java create mode 100644 java/org/apache/catalina/ha/ClusterRuleSet.java create mode 100644 java/org/apache/catalina/ha/ClusterSession.java create mode 100644 java/org/apache/catalina/ha/ClusterValve.java create mode 100644 java/org/apache/catalina/ha/authenticator/ClusterSingleSignOn.java create mode 100644 java/org/apache/catalina/ha/authenticator/ClusterSingleSignOnListener.java create mode 100644 java/org/apache/catalina/ha/authenticator/LocalStrings.properties create mode 100644 java/org/apache/catalina/ha/authenticator/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/ha/authenticator/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/ha/authenticator/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/ha/authenticator/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/ha/authenticator/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/ha/authenticator/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/ha/authenticator/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/ha/authenticator/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/ha/backend/CollectedInfo.java create mode 100644 java/org/apache/catalina/ha/backend/HeartbeatListener.java create mode 100644 java/org/apache/catalina/ha/backend/LocalStrings.properties create mode 100644 java/org/apache/catalina/ha/backend/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/ha/backend/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/ha/backend/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/ha/backend/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/ha/backend/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/ha/backend/MultiCastSender.java create mode 100644 java/org/apache/catalina/ha/backend/Proxy.java create mode 100644 java/org/apache/catalina/ha/backend/Sender.java create mode 100644 java/org/apache/catalina/ha/backend/TcpSender.java create mode 100644 java/org/apache/catalina/ha/context/LocalStrings.properties create mode 100644 java/org/apache/catalina/ha/context/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/ha/context/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/ha/context/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/ha/context/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/ha/context/ReplicatedContext.java create mode 100644 java/org/apache/catalina/ha/deploy/FarmWarDeployer.java create mode 100644 java/org/apache/catalina/ha/deploy/FileChangeListener.java create mode 100644 java/org/apache/catalina/ha/deploy/FileMessage.java create mode 100644 java/org/apache/catalina/ha/deploy/FileMessageFactory.java create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/ha/deploy/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/ha/deploy/UndeployMessage.java create mode 100644 java/org/apache/catalina/ha/deploy/WarWatcher.java create mode 100644 java/org/apache/catalina/ha/deploy/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/ha/package.html create mode 100644 java/org/apache/catalina/ha/session/BackupManager.java create mode 100644 java/org/apache/catalina/ha/session/ClusterManagerBase.java create mode 100644 java/org/apache/catalina/ha/session/ClusterSessionListener.java create mode 100644 java/org/apache/catalina/ha/session/DeltaManager.java create mode 100644 java/org/apache/catalina/ha/session/DeltaRequest.java create mode 100644 java/org/apache/catalina/ha/session/DeltaSession.java create mode 100644 java/org/apache/catalina/ha/session/JvmRouteBinderValve.java create mode 100644 java/org/apache/catalina/ha/session/LocalStrings.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/ha/session/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/ha/session/ReplicatedSessionListener.java create mode 100644 java/org/apache/catalina/ha/session/SessionMessage.java create mode 100644 java/org/apache/catalina/ha/session/SessionMessageImpl.java create mode 100644 java/org/apache/catalina/ha/session/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/ha/tcp/Constants.java create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/ha/tcp/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/ha/tcp/ReplicationValve.java create mode 100644 java/org/apache/catalina/ha/tcp/SendMessageData.java create mode 100644 java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java create mode 100644 java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/loader/JdbcLeakPrevention.java create mode 100644 java/org/apache/catalina/loader/LocalStrings.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/loader/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/loader/ParallelWebappClassLoader.java create mode 100644 java/org/apache/catalina/loader/ResourceEntry.java create mode 100644 java/org/apache/catalina/loader/WebappClassLoader.java create mode 100644 java/org/apache/catalina/loader/WebappClassLoaderBase.java create mode 100644 java/org/apache/catalina/loader/WebappLoader.java create mode 100644 java/org/apache/catalina/loader/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/manager/Constants.java create mode 100644 java/org/apache/catalina/manager/DummyProxySession.java create mode 100644 java/org/apache/catalina/manager/HTMLManagerServlet.java create mode 100644 java/org/apache/catalina/manager/JMXProxyServlet.java create mode 100644 java/org/apache/catalina/manager/JspHelper.java create mode 100644 java/org/apache/catalina/manager/LocalStrings.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_pt.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/manager/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/manager/ManagerServlet.java create mode 100644 java/org/apache/catalina/manager/StatusManagerServlet.java create mode 100644 java/org/apache/catalina/manager/StatusTransformer.java create mode 100644 java/org/apache/catalina/manager/host/Constants.java create mode 100644 java/org/apache/catalina/manager/host/HTMLHostManagerServlet.java create mode 100644 java/org/apache/catalina/manager/host/HostManagerServlet.java create mode 100644 java/org/apache/catalina/manager/host/LocalStrings.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/manager/host/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/manager/util/SessionUtils.java create mode 100644 java/org/apache/catalina/mapper/Constants.java create mode 100644 java/org/apache/catalina/mapper/LocalStrings.properties create mode 100644 java/org/apache/catalina/mapper/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/mapper/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/mapper/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/mapper/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/mapper/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/mapper/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/mapper/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/mapper/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/mapper/Mapper.java create mode 100644 java/org/apache/catalina/mapper/MapperListener.java create mode 100644 java/org/apache/catalina/mapper/MappingData.java create mode 100644 java/org/apache/catalina/mapper/WrapperMappingInfo.java create mode 100644 java/org/apache/catalina/mapper/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/mbeans/BaseCatalinaMBean.java create mode 100644 java/org/apache/catalina/mbeans/ClassNameMBean.java create mode 100644 java/org/apache/catalina/mbeans/ConnectorMBean.java create mode 100644 java/org/apache/catalina/mbeans/ContainerMBean.java create mode 100644 java/org/apache/catalina/mbeans/ContextEnvironmentMBean.java create mode 100644 java/org/apache/catalina/mbeans/ContextMBean.java create mode 100644 java/org/apache/catalina/mbeans/ContextResourceLinkMBean.java create mode 100644 java/org/apache/catalina/mbeans/ContextResourceMBean.java create mode 100644 java/org/apache/catalina/mbeans/DataSourceUserDatabaseMBean.java create mode 100644 java/org/apache/catalina/mbeans/GlobalResourcesLifecycleListener.java create mode 100644 java/org/apache/catalina/mbeans/GroupMBean.java create mode 100644 java/org/apache/catalina/mbeans/LocalStrings.properties create mode 100644 java/org/apache/catalina/mbeans/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/mbeans/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/mbeans/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/mbeans/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/mbeans/MBeanDumper.java create mode 100644 java/org/apache/catalina/mbeans/MBeanFactory.java create mode 100644 java/org/apache/catalina/mbeans/MBeanUtils.java create mode 100644 java/org/apache/catalina/mbeans/MemoryUserDatabaseMBean.java create mode 100644 java/org/apache/catalina/mbeans/NamingResourcesMBean.java create mode 100644 java/org/apache/catalina/mbeans/RoleMBean.java create mode 100644 java/org/apache/catalina/mbeans/ServiceMBean.java create mode 100644 java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java create mode 100644 java/org/apache/catalina/mbeans/UserMBean.java create mode 100644 java/org/apache/catalina/mbeans/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/realm/AuthenticatedUserRealm.java create mode 100644 java/org/apache/catalina/realm/CombinedRealm.java create mode 100644 java/org/apache/catalina/realm/DataSourceRealm.java create mode 100644 java/org/apache/catalina/realm/DigestCredentialHandlerBase.java create mode 100644 java/org/apache/catalina/realm/GenericPrincipal.java create mode 100644 java/org/apache/catalina/realm/JAASCallbackHandler.java create mode 100644 java/org/apache/catalina/realm/JAASMemoryLoginModule.java create mode 100644 java/org/apache/catalina/realm/JAASRealm.java create mode 100644 java/org/apache/catalina/realm/JNDIRealm.java create mode 100644 java/org/apache/catalina/realm/LocalStrings.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/realm/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/realm/LockOutRealm.java create mode 100644 java/org/apache/catalina/realm/MemoryRealm.java create mode 100644 java/org/apache/catalina/realm/MemoryRuleSet.java create mode 100644 java/org/apache/catalina/realm/MessageDigestCredentialHandler.java create mode 100644 java/org/apache/catalina/realm/NestedCredentialHandler.java create mode 100644 java/org/apache/catalina/realm/NullRealm.java create mode 100644 java/org/apache/catalina/realm/RealmBase.java create mode 100644 java/org/apache/catalina/realm/SecretKeyCredentialHandler.java create mode 100644 java/org/apache/catalina/realm/UserDatabaseRealm.java create mode 100644 java/org/apache/catalina/realm/X509SubjectDnRetriever.java create mode 100644 java/org/apache/catalina/realm/X509UsernameRetriever.java create mode 100644 java/org/apache/catalina/realm/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/realm/package.html create mode 100644 java/org/apache/catalina/security/Constants.java create mode 100644 java/org/apache/catalina/security/DeployXmlPermission.java create mode 100644 java/org/apache/catalina/security/LocalStrings.properties create mode 100644 java/org/apache/catalina/security/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/security/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/security/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/security/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/security/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/security/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/security/SecurityClassLoad.java create mode 100644 java/org/apache/catalina/security/SecurityConfig.java create mode 100644 java/org/apache/catalina/security/SecurityListener.java create mode 100644 java/org/apache/catalina/security/SecurityUtil.java create mode 100644 java/org/apache/catalina/security/TLSCertificateReloadListener.java create mode 100644 java/org/apache/catalina/servlets/CGIServlet.java create mode 100644 java/org/apache/catalina/servlets/DefaultServlet.java create mode 100644 java/org/apache/catalina/servlets/LocalStrings.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/servlets/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/servlets/WebdavServlet.java create mode 100644 java/org/apache/catalina/servlets/package.html create mode 100644 java/org/apache/catalina/session/Constants.java create mode 100644 java/org/apache/catalina/session/DataSourceStore.java create mode 100644 java/org/apache/catalina/session/FileStore.java create mode 100644 java/org/apache/catalina/session/LocalStrings.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/session/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/session/ManagerBase.java create mode 100644 java/org/apache/catalina/session/PersistentManager.java create mode 100644 java/org/apache/catalina/session/PersistentManagerBase.java create mode 100644 java/org/apache/catalina/session/StandardManager.java create mode 100644 java/org/apache/catalina/session/StandardSession.java create mode 100644 java/org/apache/catalina/session/StandardSessionFacade.java create mode 100644 java/org/apache/catalina/session/StoreBase.java create mode 100644 java/org/apache/catalina/session/TooManyActiveSessionsException.java create mode 100644 java/org/apache/catalina/session/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/session/package.html create mode 100644 java/org/apache/catalina/ssi/ByteArrayServletOutputStream.java create mode 100644 java/org/apache/catalina/ssi/ExpressionParseTree.java create mode 100644 java/org/apache/catalina/ssi/ExpressionTokenizer.java create mode 100644 java/org/apache/catalina/ssi/LocalStrings.properties create mode 100644 java/org/apache/catalina/ssi/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/ssi/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/ssi/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/ssi/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/ssi/ResponseIncludeWrapper.java create mode 100644 java/org/apache/catalina/ssi/SSICommand.java create mode 100644 java/org/apache/catalina/ssi/SSIConditional.java create mode 100644 java/org/apache/catalina/ssi/SSIConditionalState.java create mode 100644 java/org/apache/catalina/ssi/SSIConfig.java create mode 100644 java/org/apache/catalina/ssi/SSIEcho.java create mode 100644 java/org/apache/catalina/ssi/SSIExec.java create mode 100644 java/org/apache/catalina/ssi/SSIExternalResolver.java create mode 100644 java/org/apache/catalina/ssi/SSIFilter.java create mode 100644 java/org/apache/catalina/ssi/SSIFlastmod.java create mode 100644 java/org/apache/catalina/ssi/SSIFsize.java create mode 100644 java/org/apache/catalina/ssi/SSIInclude.java create mode 100644 java/org/apache/catalina/ssi/SSIMediator.java create mode 100644 java/org/apache/catalina/ssi/SSIPrintenv.java create mode 100644 java/org/apache/catalina/ssi/SSIProcessor.java create mode 100644 java/org/apache/catalina/ssi/SSIServlet.java create mode 100644 java/org/apache/catalina/ssi/SSIServletExternalResolver.java create mode 100644 java/org/apache/catalina/ssi/SSIServletRequestUtil.java create mode 100644 java/org/apache/catalina/ssi/SSISet.java create mode 100644 java/org/apache/catalina/ssi/SSIStopProcessingException.java create mode 100644 java/org/apache/catalina/ssi/package.html create mode 100644 java/org/apache/catalina/startup/AddPortOffsetRule.java create mode 100644 java/org/apache/catalina/startup/Authenticators.properties create mode 100644 java/org/apache/catalina/startup/Bootstrap.java create mode 100644 java/org/apache/catalina/startup/Catalina.java create mode 100644 java/org/apache/catalina/startup/CatalinaBaseConfigurationSource.java create mode 100644 java/org/apache/catalina/startup/CatalinaProperties.java create mode 100644 java/org/apache/catalina/startup/CertificateCreateRule.java create mode 100644 java/org/apache/catalina/startup/ClassLoaderFactory.java create mode 100644 java/org/apache/catalina/startup/ConnectorCreateRule.java create mode 100644 java/org/apache/catalina/startup/Constants.java create mode 100644 java/org/apache/catalina/startup/ContextConfig.java create mode 100644 java/org/apache/catalina/startup/ContextRuleSet.java create mode 100644 java/org/apache/catalina/startup/CopyParentClassLoaderRule.java create mode 100644 java/org/apache/catalina/startup/CredentialHandlerRuleSet.java create mode 100644 java/org/apache/catalina/startup/EngineConfig.java create mode 100644 java/org/apache/catalina/startup/EngineRuleSet.java create mode 100644 java/org/apache/catalina/startup/ExpandWar.java create mode 100644 java/org/apache/catalina/startup/FailedContext.java create mode 100644 java/org/apache/catalina/startup/HomesUserDatabase.java create mode 100644 java/org/apache/catalina/startup/HostConfig.java create mode 100644 java/org/apache/catalina/startup/HostRuleSet.java create mode 100644 java/org/apache/catalina/startup/LifecycleListenerRule.java create mode 100644 java/org/apache/catalina/startup/ListenerCreateRule.java create mode 100644 java/org/apache/catalina/startup/LocalStrings.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/startup/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/startup/MimeTypeMappings.properties create mode 100644 java/org/apache/catalina/startup/NamingRuleSet.java create mode 100644 java/org/apache/catalina/startup/PasswdUserDatabase.java create mode 100644 java/org/apache/catalina/startup/RealmRuleSet.java create mode 100644 java/org/apache/catalina/startup/SafeForkJoinWorkerThreadFactory.java create mode 100644 java/org/apache/catalina/startup/SetNextNamingRule.java create mode 100644 java/org/apache/catalina/startup/Tomcat.java create mode 100644 java/org/apache/catalina/startup/Tool.java create mode 100644 java/org/apache/catalina/startup/UserConfig.java create mode 100644 java/org/apache/catalina/startup/UserDatabase.java create mode 100644 java/org/apache/catalina/startup/VersionLoggerListener.java create mode 100644 java/org/apache/catalina/startup/WebAnnotationSet.java create mode 100644 java/org/apache/catalina/startup/WebappServiceLoader.java create mode 100644 java/org/apache/catalina/startup/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/storeconfig/CatalinaClusterSF.java create mode 100644 java/org/apache/catalina/storeconfig/CertificateStoreAppender.java create mode 100644 java/org/apache/catalina/storeconfig/ChannelSF.java create mode 100644 java/org/apache/catalina/storeconfig/ConnectorSF.java create mode 100644 java/org/apache/catalina/storeconfig/ConnectorStoreAppender.java create mode 100644 java/org/apache/catalina/storeconfig/Constants.java create mode 100644 java/org/apache/catalina/storeconfig/CredentialHandlerSF.java create mode 100644 java/org/apache/catalina/storeconfig/GlobalNamingResourcesSF.java create mode 100644 java/org/apache/catalina/storeconfig/IStoreConfig.java create mode 100644 java/org/apache/catalina/storeconfig/IStoreFactory.java create mode 100644 java/org/apache/catalina/storeconfig/InterceptorSF.java create mode 100644 java/org/apache/catalina/storeconfig/JarScannerSF.java create mode 100644 java/org/apache/catalina/storeconfig/LoaderSF.java create mode 100644 java/org/apache/catalina/storeconfig/LocalStrings.properties create mode 100644 java/org/apache/catalina/storeconfig/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/storeconfig/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/storeconfig/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/storeconfig/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/storeconfig/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/storeconfig/ManagerSF.java create mode 100644 java/org/apache/catalina/storeconfig/NamingResourcesSF.java create mode 100644 java/org/apache/catalina/storeconfig/OpenSSLConfSF.java create mode 100644 java/org/apache/catalina/storeconfig/PersistentManagerSF.java create mode 100644 java/org/apache/catalina/storeconfig/RealmSF.java create mode 100644 java/org/apache/catalina/storeconfig/SSLHostConfigSF.java create mode 100644 java/org/apache/catalina/storeconfig/SenderSF.java create mode 100644 java/org/apache/catalina/storeconfig/StandardContextSF.java create mode 100644 java/org/apache/catalina/storeconfig/StandardEngineSF.java create mode 100644 java/org/apache/catalina/storeconfig/StandardHostSF.java create mode 100644 java/org/apache/catalina/storeconfig/StandardServerSF.java create mode 100644 java/org/apache/catalina/storeconfig/StandardServiceSF.java create mode 100644 java/org/apache/catalina/storeconfig/StoreAppender.java create mode 100644 java/org/apache/catalina/storeconfig/StoreConfig.java create mode 100644 java/org/apache/catalina/storeconfig/StoreConfigLifecycleListener.java create mode 100644 java/org/apache/catalina/storeconfig/StoreContextAppender.java create mode 100644 java/org/apache/catalina/storeconfig/StoreDescription.java create mode 100644 java/org/apache/catalina/storeconfig/StoreFactoryBase.java create mode 100644 java/org/apache/catalina/storeconfig/StoreFactoryRule.java create mode 100644 java/org/apache/catalina/storeconfig/StoreFileMover.java create mode 100644 java/org/apache/catalina/storeconfig/StoreLoader.java create mode 100644 java/org/apache/catalina/storeconfig/StoreRegistry.java create mode 100644 java/org/apache/catalina/storeconfig/WatchedResourceSF.java create mode 100644 java/org/apache/catalina/storeconfig/WebResourceRootSF.java create mode 100644 java/org/apache/catalina/storeconfig/WrapperLifecycleSF.java create mode 100644 java/org/apache/catalina/storeconfig/WrapperListenerSF.java create mode 100644 java/org/apache/catalina/storeconfig/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/storeconfig/server-registry.xml create mode 100644 java/org/apache/catalina/tribes/ByteMessage.java create mode 100644 java/org/apache/catalina/tribes/Channel.java create mode 100644 java/org/apache/catalina/tribes/ChannelException.java create mode 100644 java/org/apache/catalina/tribes/ChannelInterceptor.java create mode 100644 java/org/apache/catalina/tribes/ChannelListener.java create mode 100644 java/org/apache/catalina/tribes/ChannelMessage.java create mode 100644 java/org/apache/catalina/tribes/ChannelReceiver.java create mode 100644 java/org/apache/catalina/tribes/ChannelSender.java create mode 100644 java/org/apache/catalina/tribes/ErrorHandler.java create mode 100644 java/org/apache/catalina/tribes/Heartbeat.java create mode 100644 java/org/apache/catalina/tribes/JmxChannel.java create mode 100644 java/org/apache/catalina/tribes/ManagedChannel.java create mode 100644 java/org/apache/catalina/tribes/Member.java create mode 100644 java/org/apache/catalina/tribes/MembershipListener.java create mode 100644 java/org/apache/catalina/tribes/MembershipProvider.java create mode 100644 java/org/apache/catalina/tribes/MembershipService.java create mode 100644 java/org/apache/catalina/tribes/MessageListener.java create mode 100644 java/org/apache/catalina/tribes/RemoteProcessException.java create mode 100644 java/org/apache/catalina/tribes/UniqueId.java create mode 100644 java/org/apache/catalina/tribes/group/AbsoluteOrder.java create mode 100644 java/org/apache/catalina/tribes/group/ChannelCoordinator.java create mode 100644 java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java create mode 100644 java/org/apache/catalina/tribes/group/ExtendedRpcCallback.java create mode 100644 java/org/apache/catalina/tribes/group/GroupChannel.java create mode 100644 java/org/apache/catalina/tribes/group/GroupChannelMBean.java create mode 100644 java/org/apache/catalina/tribes/group/InterceptorPayload.java create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/tribes/group/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/group/Response.java create mode 100644 java/org/apache/catalina/tribes/group/RpcCallback.java create mode 100644 java/org/apache/catalina/tribes/group/RpcChannel.java create mode 100644 java/org/apache/catalina/tribes/group/RpcMessage.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/GzipInterceptorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/OrderInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/SimpleCoordinator.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetectorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/TcpPingInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/TcpPingInterceptorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptorMBean.java create mode 100644 java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java create mode 100644 java/org/apache/catalina/tribes/io/BufferPool.java create mode 100644 java/org/apache/catalina/tribes/io/ChannelData.java create mode 100644 java/org/apache/catalina/tribes/io/DirectByteArrayOutputStream.java create mode 100644 java/org/apache/catalina/tribes/io/ListenCallback.java create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/tribes/io/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/io/ObjectReader.java create mode 100644 java/org/apache/catalina/tribes/io/ReplicationStream.java create mode 100644 java/org/apache/catalina/tribes/io/XByteBuffer.java create mode 100644 java/org/apache/catalina/tribes/jmx/JmxRegistry.java create mode 100644 java/org/apache/catalina/tribes/jmx/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/jmx/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/jmx/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/jmx/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/jmx/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/jmx/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/membership/Constants.java create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/tribes/membership/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/membership/McastService.java create mode 100644 java/org/apache/catalina/tribes/membership/McastServiceImpl.java create mode 100644 java/org/apache/catalina/tribes/membership/McastServiceMBean.java create mode 100644 java/org/apache/catalina/tribes/membership/MemberImpl.java create mode 100644 java/org/apache/catalina/tribes/membership/Membership.java create mode 100644 java/org/apache/catalina/tribes/membership/MembershipProviderBase.java create mode 100644 java/org/apache/catalina/tribes/membership/MembershipServiceBase.java create mode 100644 java/org/apache/catalina/tribes/membership/StaticMember.java create mode 100644 java/org/apache/catalina/tribes/membership/StaticMembershipProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/StaticMembershipService.java create mode 100644 java/org/apache/catalina/tribes/membership/StaticMembershipServiceMBean.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/CertificateStreamProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/CloudMembershipProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/CloudMembershipService.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/CloudMembershipServiceMBean.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/DNSMembershipProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/InsecureStreamProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/membership/cloud/StreamProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java create mode 100644 java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/tribes/package.html create mode 100644 java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java create mode 100644 java/org/apache/catalina/tribes/tipis/LazyReplicatedMap.java create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/tribes/tipis/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/tipis/ReplicatedMap.java create mode 100644 java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java create mode 100644 java/org/apache/catalina/tribes/transport/AbstractRxTask.java create mode 100644 java/org/apache/catalina/tribes/transport/AbstractSender.java create mode 100644 java/org/apache/catalina/tribes/transport/Constants.java create mode 100644 java/org/apache/catalina/tribes/transport/DataSender.java create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/tribes/transport/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/transport/MultiPointSender.java create mode 100644 java/org/apache/catalina/tribes/transport/PooledSender.java create mode 100644 java/org/apache/catalina/tribes/transport/ReceiverBase.java create mode 100644 java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java create mode 100644 java/org/apache/catalina/tribes/transport/RxTaskPool.java create mode 100644 java/org/apache/catalina/tribes/transport/SenderState.java create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/transport/nio/NioReceiver.java create mode 100644 java/org/apache/catalina/tribes/transport/nio/NioReceiverMBean.java create mode 100644 java/org/apache/catalina/tribes/transport/nio/NioReplicationTask.java create mode 100644 java/org/apache/catalina/tribes/transport/nio/NioSender.java create mode 100644 java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java create mode 100644 java/org/apache/catalina/tribes/transport/nio/PooledParallelSender.java create mode 100644 java/org/apache/catalina/tribes/transport/nio/PooledParallelSenderMBean.java create mode 100644 java/org/apache/catalina/tribes/util/Arrays.java create mode 100644 java/org/apache/catalina/tribes/util/ExceptionUtils.java create mode 100644 java/org/apache/catalina/tribes/util/ExecutorFactory.java create mode 100644 java/org/apache/catalina/tribes/util/Jre14Compat.java create mode 100644 java/org/apache/catalina/tribes/util/JreCompat.java create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings.properties create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/tribes/util/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/tribes/util/Logs.java create mode 100644 java/org/apache/catalina/tribes/util/StringManager.java create mode 100644 java/org/apache/catalina/tribes/util/TcclThreadFactory.java create mode 100644 java/org/apache/catalina/tribes/util/UUIDGenerator.java create mode 100644 java/org/apache/catalina/users/AbstractGroup.java create mode 100644 java/org/apache/catalina/users/AbstractRole.java create mode 100644 java/org/apache/catalina/users/AbstractUser.java create mode 100644 java/org/apache/catalina/users/Constants.java create mode 100644 java/org/apache/catalina/users/DataSourceUserDatabase.java create mode 100644 java/org/apache/catalina/users/DataSourceUserDatabaseFactory.java create mode 100644 java/org/apache/catalina/users/GenericGroup.java create mode 100644 java/org/apache/catalina/users/GenericRole.java create mode 100644 java/org/apache/catalina/users/GenericUser.java create mode 100644 java/org/apache/catalina/users/LocalStrings.properties create mode 100644 java/org/apache/catalina/users/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/users/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/users/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/users/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/users/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/users/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/users/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/users/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/users/MemoryGroup.java create mode 100644 java/org/apache/catalina/users/MemoryRole.java create mode 100644 java/org/apache/catalina/users/MemoryUser.java create mode 100644 java/org/apache/catalina/users/MemoryUserDatabase.java create mode 100644 java/org/apache/catalina/users/MemoryUserDatabaseFactory.java create mode 100644 java/org/apache/catalina/users/SparseUserDatabase.java create mode 100644 java/org/apache/catalina/users/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/util/CharsetMapper.java create mode 100644 java/org/apache/catalina/util/CharsetMapperDefault.properties create mode 100644 java/org/apache/catalina/util/ContextName.java create mode 100644 java/org/apache/catalina/util/CustomObjectInputStream.java create mode 100644 java/org/apache/catalina/util/DOMWriter.java create mode 100644 java/org/apache/catalina/util/ErrorPageSupport.java create mode 100644 java/org/apache/catalina/util/IOTools.java create mode 100644 java/org/apache/catalina/util/Introspection.java create mode 100644 java/org/apache/catalina/util/LifecycleBase.java create mode 100644 java/org/apache/catalina/util/LifecycleMBeanBase.java create mode 100644 java/org/apache/catalina/util/LocalStrings.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/util/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/util/NetMask.java create mode 100644 java/org/apache/catalina/util/NetMaskSet.java create mode 100644 java/org/apache/catalina/util/ParameterMap.java create mode 100644 java/org/apache/catalina/util/RequestUtil.java create mode 100644 java/org/apache/catalina/util/ResourceSet.java create mode 100644 java/org/apache/catalina/util/ServerInfo.java create mode 100644 java/org/apache/catalina/util/ServerInfo.properties create mode 100644 java/org/apache/catalina/util/SessionConfig.java create mode 100644 java/org/apache/catalina/util/SessionIdGeneratorBase.java create mode 100644 java/org/apache/catalina/util/StandardSessionIdGenerator.java create mode 100644 java/org/apache/catalina/util/Strftime.java create mode 100644 java/org/apache/catalina/util/StringUtil.java create mode 100644 java/org/apache/catalina/util/TLSUtil.java create mode 100644 java/org/apache/catalina/util/TimeBucketCounter.java create mode 100644 java/org/apache/catalina/util/ToStringUtil.java create mode 100644 java/org/apache/catalina/util/TomcatCSS.java create mode 100644 java/org/apache/catalina/util/URLEncoder.java create mode 100644 java/org/apache/catalina/util/XMLWriter.java create mode 100644 java/org/apache/catalina/valves/AbstractAccessLogValve.java create mode 100644 java/org/apache/catalina/valves/AccessLogValve.java create mode 100644 java/org/apache/catalina/valves/Constants.java create mode 100644 java/org/apache/catalina/valves/CrawlerSessionManagerValve.java create mode 100644 java/org/apache/catalina/valves/ErrorReportValve.java create mode 100644 java/org/apache/catalina/valves/ExtendedAccessLogValve.java create mode 100644 java/org/apache/catalina/valves/HealthCheckValve.java create mode 100644 java/org/apache/catalina/valves/JDBCAccessLogValve.java create mode 100644 java/org/apache/catalina/valves/JsonAccessLogValve.java create mode 100644 java/org/apache/catalina/valves/JsonErrorReportValve.java create mode 100644 java/org/apache/catalina/valves/LoadBalancerDrainingValve.java create mode 100644 java/org/apache/catalina/valves/LocalStrings.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/valves/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/valves/PersistentValve.java create mode 100644 java/org/apache/catalina/valves/ProxyErrorReportValve.java create mode 100644 java/org/apache/catalina/valves/RemoteAddrValve.java create mode 100644 java/org/apache/catalina/valves/RemoteCIDRValve.java create mode 100644 java/org/apache/catalina/valves/RemoteHostValve.java create mode 100644 java/org/apache/catalina/valves/RemoteIpValve.java create mode 100644 java/org/apache/catalina/valves/RequestFilterValve.java create mode 100644 java/org/apache/catalina/valves/SSLValve.java create mode 100644 java/org/apache/catalina/valves/SemaphoreValve.java create mode 100644 java/org/apache/catalina/valves/StuckThreadDetectionValve.java create mode 100644 java/org/apache/catalina/valves/ValveBase.java create mode 100644 java/org/apache/catalina/valves/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/valves/package.html create mode 100644 java/org/apache/catalina/valves/rewrite/InternalRewriteMap.java create mode 100644 java/org/apache/catalina/valves/rewrite/LocalStrings.properties create mode 100644 java/org/apache/catalina/valves/rewrite/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/valves/rewrite/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/valves/rewrite/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/valves/rewrite/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/valves/rewrite/QuotedStringTokenizer.java create mode 100644 java/org/apache/catalina/valves/rewrite/RandomizedTextRewriteMap.java create mode 100644 java/org/apache/catalina/valves/rewrite/Resolver.java create mode 100644 java/org/apache/catalina/valves/rewrite/ResolverImpl.java create mode 100644 java/org/apache/catalina/valves/rewrite/RewriteCond.java create mode 100644 java/org/apache/catalina/valves/rewrite/RewriteMap.java create mode 100644 java/org/apache/catalina/valves/rewrite/RewriteRule.java create mode 100644 java/org/apache/catalina/valves/rewrite/RewriteValve.java create mode 100644 java/org/apache/catalina/valves/rewrite/Substitution.java create mode 100644 java/org/apache/catalina/valves/rewrite/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/webresources/AbstractArchiveResource.java create mode 100644 java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java create mode 100644 java/org/apache/catalina/webresources/AbstractFileResourceSet.java create mode 100644 java/org/apache/catalina/webresources/AbstractResource.java create mode 100644 java/org/apache/catalina/webresources/AbstractResourceSet.java create mode 100644 java/org/apache/catalina/webresources/AbstractSingleArchiveResource.java create mode 100644 java/org/apache/catalina/webresources/AbstractSingleArchiveResourceSet.java create mode 100644 java/org/apache/catalina/webresources/Cache.java create mode 100644 java/org/apache/catalina/webresources/CachedResource.java create mode 100644 java/org/apache/catalina/webresources/ClasspathURLStreamHandler.java create mode 100644 java/org/apache/catalina/webresources/DirResourceSet.java create mode 100644 java/org/apache/catalina/webresources/EmptyResource.java create mode 100644 java/org/apache/catalina/webresources/EmptyResourceSet.java create mode 100644 java/org/apache/catalina/webresources/ExtractingRoot.java create mode 100644 java/org/apache/catalina/webresources/FileResource.java create mode 100644 java/org/apache/catalina/webresources/FileResourceSet.java create mode 100644 java/org/apache/catalina/webresources/JarContents.java create mode 100644 java/org/apache/catalina/webresources/JarResource.java create mode 100644 java/org/apache/catalina/webresources/JarResourceRoot.java create mode 100644 java/org/apache/catalina/webresources/JarResourceSet.java create mode 100644 java/org/apache/catalina/webresources/JarWarResource.java create mode 100644 java/org/apache/catalina/webresources/JarWarResourceSet.java create mode 100644 java/org/apache/catalina/webresources/LocalStrings.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_cs.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_de.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_es.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_fr.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_ja.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_ko.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_ru.properties create mode 100644 java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/catalina/webresources/StandardRoot.java create mode 100644 java/org/apache/catalina/webresources/TomcatJarInputStream.java create mode 100644 java/org/apache/catalina/webresources/TomcatURLStreamHandlerFactory.java create mode 100644 java/org/apache/catalina/webresources/TrackedInputStream.java create mode 100644 java/org/apache/catalina/webresources/VirtualResource.java create mode 100644 java/org/apache/catalina/webresources/WarResource.java create mode 100644 java/org/apache/catalina/webresources/WarResourceSet.java create mode 100644 java/org/apache/catalina/webresources/mbeans-descriptors.xml create mode 100644 java/org/apache/catalina/webresources/war/Handler.java create mode 100644 java/org/apache/catalina/webresources/war/WarURLConnection.java create mode 100644 java/org/apache/coyote/AbstractProcessor.java create mode 100644 java/org/apache/coyote/AbstractProcessorLight.java create mode 100644 java/org/apache/coyote/AbstractProtocol.java create mode 100644 java/org/apache/coyote/ActionCode.java create mode 100644 java/org/apache/coyote/ActionHook.java create mode 100644 java/org/apache/coyote/Adapter.java create mode 100644 java/org/apache/coyote/AsyncContextCallback.java create mode 100644 java/org/apache/coyote/AsyncStateMachine.java create mode 100644 java/org/apache/coyote/BadRequestException.java create mode 100644 java/org/apache/coyote/CloseNowException.java create mode 100644 java/org/apache/coyote/CompressionConfig.java create mode 100644 java/org/apache/coyote/Constants.java create mode 100644 java/org/apache/coyote/ContinueResponseTiming.java create mode 100644 java/org/apache/coyote/ErrorState.java create mode 100644 java/org/apache/coyote/InputBuffer.java create mode 100644 java/org/apache/coyote/LocalStrings.properties create mode 100644 java/org/apache/coyote/LocalStrings_cs.properties create mode 100644 java/org/apache/coyote/LocalStrings_de.properties create mode 100644 java/org/apache/coyote/LocalStrings_es.properties create mode 100644 java/org/apache/coyote/LocalStrings_fr.properties create mode 100644 java/org/apache/coyote/LocalStrings_ja.properties create mode 100644 java/org/apache/coyote/LocalStrings_ko.properties create mode 100644 java/org/apache/coyote/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/coyote/LocalStrings_ru.properties create mode 100644 java/org/apache/coyote/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/coyote/OutputBuffer.java create mode 100644 java/org/apache/coyote/Processor.java create mode 100644 java/org/apache/coyote/ProtocolException.java create mode 100644 java/org/apache/coyote/ProtocolHandler.java create mode 100644 java/org/apache/coyote/Request.java create mode 100644 java/org/apache/coyote/RequestGroupInfo.java create mode 100644 java/org/apache/coyote/RequestInfo.java create mode 100644 java/org/apache/coyote/Response.java create mode 100644 java/org/apache/coyote/UpgradeProtocol.java create mode 100644 java/org/apache/coyote/UpgradeToken.java create mode 100644 java/org/apache/coyote/ajp/AbstractAjpProtocol.java create mode 100644 java/org/apache/coyote/ajp/AjpMessage.java create mode 100644 java/org/apache/coyote/ajp/AjpNio2Protocol.java create mode 100644 java/org/apache/coyote/ajp/AjpNioProtocol.java create mode 100644 java/org/apache/coyote/ajp/AjpProcessor.java create mode 100644 java/org/apache/coyote/ajp/Constants.java create mode 100644 java/org/apache/coyote/ajp/LocalStrings.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_cs.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_de.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_es.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_fr.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_ja.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_ko.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_ru.properties create mode 100644 java/org/apache/coyote/ajp/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/coyote/http11/AbstractHttp11JsseProtocol.java create mode 100644 java/org/apache/coyote/http11/AbstractHttp11Protocol.java create mode 100644 java/org/apache/coyote/http11/Constants.java create mode 100644 java/org/apache/coyote/http11/HeadersTooLargeException.java create mode 100644 java/org/apache/coyote/http11/Http11InputBuffer.java create mode 100644 java/org/apache/coyote/http11/Http11Nio2Protocol.java create mode 100644 java/org/apache/coyote/http11/Http11NioProtocol.java create mode 100644 java/org/apache/coyote/http11/Http11OutputBuffer.java create mode 100644 java/org/apache/coyote/http11/Http11Processor.java create mode 100644 java/org/apache/coyote/http11/HttpOutputBuffer.java create mode 100644 java/org/apache/coyote/http11/InputFilter.java create mode 100644 java/org/apache/coyote/http11/LocalStrings.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_cs.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_de.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_es.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_fr.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_ja.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_ko.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_ru.properties create mode 100644 java/org/apache/coyote/http11/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/coyote/http11/OutputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/BufferedInputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/ChunkedInputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/GzipOutputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/IdentityInputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/IdentityOutputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_cs.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_de.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_es.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_fr.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_ja.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_ko.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_ru.properties create mode 100644 java/org/apache/coyote/http11/filters/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/coyote/http11/filters/SavedRequestInputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/VoidInputFilter.java create mode 100644 java/org/apache/coyote/http11/filters/VoidOutputFilter.java create mode 100644 java/org/apache/coyote/http11/upgrade/InternalHttpUpgradeHandler.java create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings.properties create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings_cs.properties create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings_de.properties create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings_es.properties create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings_fr.properties create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings_ja.properties create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings_ko.properties create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings_ru.properties create mode 100644 java/org/apache/coyote/http11/upgrade/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/coyote/http11/upgrade/UpgradeApplicationBufferHandler.java create mode 100644 java/org/apache/coyote/http11/upgrade/UpgradeGroupInfo.java create mode 100644 java/org/apache/coyote/http11/upgrade/UpgradeInfo.java create mode 100644 java/org/apache/coyote/http11/upgrade/UpgradeProcessorBase.java create mode 100644 java/org/apache/coyote/http11/upgrade/UpgradeProcessorExternal.java create mode 100644 java/org/apache/coyote/http11/upgrade/UpgradeProcessorInternal.java create mode 100644 java/org/apache/coyote/http11/upgrade/UpgradeServletInputStream.java create mode 100644 java/org/apache/coyote/http11/upgrade/UpgradeServletOutputStream.java create mode 100644 java/org/apache/coyote/http2/AbstractNonZeroStream.java create mode 100644 java/org/apache/coyote/http2/AbstractStream.java create mode 100644 java/org/apache/coyote/http2/ByteUtil.java create mode 100644 java/org/apache/coyote/http2/ConnectionException.java create mode 100644 java/org/apache/coyote/http2/ConnectionSettingsBase.java create mode 100644 java/org/apache/coyote/http2/ConnectionSettingsLocal.java create mode 100644 java/org/apache/coyote/http2/ConnectionSettingsRemote.java create mode 100644 java/org/apache/coyote/http2/Constants.java create mode 100644 java/org/apache/coyote/http2/Flags.java create mode 100644 java/org/apache/coyote/http2/FrameType.java create mode 100644 java/org/apache/coyote/http2/HPackHuffman.java create mode 100644 java/org/apache/coyote/http2/HeaderSink.java create mode 100644 java/org/apache/coyote/http2/Hpack.java create mode 100644 java/org/apache/coyote/http2/HpackDecoder.java create mode 100644 java/org/apache/coyote/http2/HpackEncoder.java create mode 100644 java/org/apache/coyote/http2/HpackException.java create mode 100644 java/org/apache/coyote/http2/Http2AsyncParser.java create mode 100644 java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java create mode 100644 java/org/apache/coyote/http2/Http2Error.java create mode 100644 java/org/apache/coyote/http2/Http2Exception.java create mode 100644 java/org/apache/coyote/http2/Http2OutputBuffer.java create mode 100644 java/org/apache/coyote/http2/Http2Parser.java create mode 100644 java/org/apache/coyote/http2/Http2Protocol.java create mode 100644 java/org/apache/coyote/http2/Http2UpgradeHandler.java create mode 100644 java/org/apache/coyote/http2/LocalStrings.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_cs.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_de.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_es.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_fr.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_ja.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_ko.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_ru.properties create mode 100644 java/org/apache/coyote/http2/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/coyote/http2/RecycledStream.java create mode 100644 java/org/apache/coyote/http2/SendfileData.java create mode 100644 java/org/apache/coyote/http2/Setting.java create mode 100644 java/org/apache/coyote/http2/Stream.java create mode 100644 java/org/apache/coyote/http2/StreamException.java create mode 100644 java/org/apache/coyote/http2/StreamProcessor.java create mode 100644 java/org/apache/coyote/http2/StreamRunnable.java create mode 100644 java/org/apache/coyote/http2/StreamStateMachine.java create mode 100644 java/org/apache/coyote/http2/WindowAllocationManager.java create mode 100644 java/org/apache/coyote/mbeans-descriptors.xml create mode 100644 java/org/apache/el/ExpressionFactoryImpl.java create mode 100644 java/org/apache/el/LocalStrings.properties create mode 100644 java/org/apache/el/LocalStrings_es.properties create mode 100644 java/org/apache/el/LocalStrings_fr.properties create mode 100644 java/org/apache/el/LocalStrings_ja.properties create mode 100644 java/org/apache/el/LocalStrings_ko.properties create mode 100644 java/org/apache/el/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/el/MethodExpressionImpl.java create mode 100644 java/org/apache/el/MethodExpressionLiteral.java create mode 100644 java/org/apache/el/ValueExpressionImpl.java create mode 100644 java/org/apache/el/ValueExpressionLiteral.java create mode 100644 java/org/apache/el/lang/ELArithmetic.java create mode 100644 java/org/apache/el/lang/ELSupport.java create mode 100644 java/org/apache/el/lang/EvaluationContext.java create mode 100644 java/org/apache/el/lang/ExpressionBuilder.java create mode 100644 java/org/apache/el/lang/FunctionMapperFactory.java create mode 100644 java/org/apache/el/lang/FunctionMapperImpl.java create mode 100644 java/org/apache/el/lang/LambdaExpressionNestedState.java create mode 100644 java/org/apache/el/lang/VariableMapperFactory.java create mode 100644 java/org/apache/el/lang/VariableMapperImpl.java create mode 100644 java/org/apache/el/parser/ArithmeticNode.java create mode 100644 java/org/apache/el/parser/AstAnd.java create mode 100644 java/org/apache/el/parser/AstAssign.java create mode 100644 java/org/apache/el/parser/AstBracketSuffix.java create mode 100644 java/org/apache/el/parser/AstChoice.java create mode 100644 java/org/apache/el/parser/AstCompositeExpression.java create mode 100644 java/org/apache/el/parser/AstConcatenation.java create mode 100644 java/org/apache/el/parser/AstDeferredExpression.java create mode 100644 java/org/apache/el/parser/AstDiv.java create mode 100644 java/org/apache/el/parser/AstDotSuffix.java create mode 100644 java/org/apache/el/parser/AstDynamicExpression.java create mode 100644 java/org/apache/el/parser/AstEmpty.java create mode 100644 java/org/apache/el/parser/AstEqual.java create mode 100644 java/org/apache/el/parser/AstFalse.java create mode 100644 java/org/apache/el/parser/AstFloatingPoint.java create mode 100644 java/org/apache/el/parser/AstFunction.java create mode 100644 java/org/apache/el/parser/AstGreaterThan.java create mode 100644 java/org/apache/el/parser/AstGreaterThanEqual.java create mode 100644 java/org/apache/el/parser/AstIdentifier.java create mode 100644 java/org/apache/el/parser/AstInteger.java create mode 100644 java/org/apache/el/parser/AstLambdaExpression.java create mode 100644 java/org/apache/el/parser/AstLambdaParameters.java create mode 100644 java/org/apache/el/parser/AstLessThan.java create mode 100644 java/org/apache/el/parser/AstLessThanEqual.java create mode 100644 java/org/apache/el/parser/AstListData.java create mode 100644 java/org/apache/el/parser/AstLiteralExpression.java create mode 100644 java/org/apache/el/parser/AstMapData.java create mode 100644 java/org/apache/el/parser/AstMapEntry.java create mode 100644 java/org/apache/el/parser/AstMethodParameters.java create mode 100644 java/org/apache/el/parser/AstMinus.java create mode 100644 java/org/apache/el/parser/AstMod.java create mode 100644 java/org/apache/el/parser/AstMult.java create mode 100644 java/org/apache/el/parser/AstNegative.java create mode 100644 java/org/apache/el/parser/AstNot.java create mode 100644 java/org/apache/el/parser/AstNotEqual.java create mode 100644 java/org/apache/el/parser/AstNull.java create mode 100644 java/org/apache/el/parser/AstOr.java create mode 100644 java/org/apache/el/parser/AstPlus.java create mode 100644 java/org/apache/el/parser/AstSemicolon.java create mode 100644 java/org/apache/el/parser/AstSetData.java create mode 100644 java/org/apache/el/parser/AstString.java create mode 100644 java/org/apache/el/parser/AstTrue.java create mode 100644 java/org/apache/el/parser/AstValue.java create mode 100644 java/org/apache/el/parser/BooleanNode.java create mode 100644 java/org/apache/el/parser/ELParser.html create mode 100644 java/org/apache/el/parser/ELParser.java create mode 100644 java/org/apache/el/parser/ELParser.jjt create mode 100644 java/org/apache/el/parser/ELParserConstants.java create mode 100644 java/org/apache/el/parser/ELParserTokenManager.java create mode 100644 java/org/apache/el/parser/ELParserTreeConstants.java create mode 100644 java/org/apache/el/parser/JJTELParserState.java create mode 100644 java/org/apache/el/parser/Node.java create mode 100644 java/org/apache/el/parser/NodeVisitor.java create mode 100644 java/org/apache/el/parser/ParseException.java create mode 100644 java/org/apache/el/parser/SimpleCharStream.java create mode 100644 java/org/apache/el/parser/SimpleNode.java create mode 100644 java/org/apache/el/parser/Token.java create mode 100644 java/org/apache/el/parser/TokenMgrError.java create mode 100644 java/org/apache/el/stream/Optional.java create mode 100644 java/org/apache/el/stream/Stream.java create mode 100644 java/org/apache/el/stream/StreamELResolverImpl.java create mode 100644 java/org/apache/el/util/ConcurrentCache.java create mode 100644 java/org/apache/el/util/ExceptionUtils.java create mode 100644 java/org/apache/el/util/MessageFactory.java create mode 100644 java/org/apache/el/util/ReflectionUtil.java create mode 100644 java/org/apache/el/util/Validation.java create mode 100644 java/org/apache/jasper/Constants.java create mode 100644 java/org/apache/jasper/EmbeddedServletOptions.java create mode 100644 java/org/apache/jasper/JasperException.java create mode 100644 java/org/apache/jasper/JspC.java create mode 100644 java/org/apache/jasper/JspCompilationContext.java create mode 100644 java/org/apache/jasper/Options.java create mode 100644 java/org/apache/jasper/TrimSpacesOption.java create mode 100644 java/org/apache/jasper/compiler/AntCompiler.java create mode 100644 java/org/apache/jasper/compiler/AttributeParser.java create mode 100644 java/org/apache/jasper/compiler/BeanRepository.java create mode 100644 java/org/apache/jasper/compiler/Collector.java create mode 100644 java/org/apache/jasper/compiler/Compiler.java create mode 100644 java/org/apache/jasper/compiler/DefaultErrorHandler.java create mode 100644 java/org/apache/jasper/compiler/ELFunctionMapper.java create mode 100644 java/org/apache/jasper/compiler/ELInterpreter.java create mode 100644 java/org/apache/jasper/compiler/ELInterpreterFactory.java create mode 100644 java/org/apache/jasper/compiler/ELNode.java create mode 100644 java/org/apache/jasper/compiler/ELParser.java create mode 100644 java/org/apache/jasper/compiler/EncodingDetector.java create mode 100644 java/org/apache/jasper/compiler/ErrorDispatcher.java create mode 100644 java/org/apache/jasper/compiler/ErrorHandler.java create mode 100644 java/org/apache/jasper/compiler/Generator.java create mode 100644 java/org/apache/jasper/compiler/ImplicitTagLibraryInfo.java create mode 100644 java/org/apache/jasper/compiler/JDTCompiler.java create mode 100644 java/org/apache/jasper/compiler/JarScannerFactory.java create mode 100644 java/org/apache/jasper/compiler/JasperTagInfo.java create mode 100644 java/org/apache/jasper/compiler/JavacErrorDetail.java create mode 100644 java/org/apache/jasper/compiler/JspConfig.java create mode 100644 java/org/apache/jasper/compiler/JspDocumentParser.java create mode 100644 java/org/apache/jasper/compiler/JspReader.java create mode 100644 java/org/apache/jasper/compiler/JspRuntimeContext.java create mode 100644 java/org/apache/jasper/compiler/JspUtil.java create mode 100644 java/org/apache/jasper/compiler/Localizer.java create mode 100644 java/org/apache/jasper/compiler/Mark.java create mode 100644 java/org/apache/jasper/compiler/NewlineReductionServletWriter.java create mode 100644 java/org/apache/jasper/compiler/Node.java create mode 100644 java/org/apache/jasper/compiler/PageDataImpl.java create mode 100644 java/org/apache/jasper/compiler/PageInfo.java create mode 100644 java/org/apache/jasper/compiler/Parser.java create mode 100644 java/org/apache/jasper/compiler/ParserController.java create mode 100644 java/org/apache/jasper/compiler/ScriptingVariabler.java create mode 100644 java/org/apache/jasper/compiler/ServletWriter.java create mode 100644 java/org/apache/jasper/compiler/SmapInput.java create mode 100644 java/org/apache/jasper/compiler/SmapStratum.java create mode 100644 java/org/apache/jasper/compiler/SmapUtil.java create mode 100644 java/org/apache/jasper/compiler/StringInterpreter.java create mode 100644 java/org/apache/jasper/compiler/StringInterpreterFactory.java create mode 100644 java/org/apache/jasper/compiler/TagConstants.java create mode 100644 java/org/apache/jasper/compiler/TagFileProcessor.java create mode 100644 java/org/apache/jasper/compiler/TagLibraryInfoImpl.java create mode 100644 java/org/apache/jasper/compiler/TagPluginManager.java create mode 100644 java/org/apache/jasper/compiler/TextOptimizer.java create mode 100644 java/org/apache/jasper/compiler/TldCache.java create mode 100644 java/org/apache/jasper/compiler/Validator.java create mode 100644 java/org/apache/jasper/compiler/tagplugin/TagPlugin.java create mode 100644 java/org/apache/jasper/compiler/tagplugin/TagPluginContext.java create mode 100644 java/org/apache/jasper/el/ELContextImpl.java create mode 100644 java/org/apache/jasper/el/ELContextWrapper.java create mode 100644 java/org/apache/jasper/el/ELResolverImpl.java create mode 100644 java/org/apache/jasper/el/ExpressionEvaluatorImpl.java create mode 100644 java/org/apache/jasper/el/ExpressionImpl.java create mode 100644 java/org/apache/jasper/el/FunctionMapperImpl.java create mode 100644 java/org/apache/jasper/el/JasperELResolver.java create mode 100644 java/org/apache/jasper/el/JspELException.java create mode 100644 java/org/apache/jasper/el/JspMethodExpression.java create mode 100644 java/org/apache/jasper/el/JspMethodNotFoundException.java create mode 100644 java/org/apache/jasper/el/JspPropertyNotFoundException.java create mode 100644 java/org/apache/jasper/el/JspPropertyNotWritableException.java create mode 100644 java/org/apache/jasper/el/JspValueExpression.java create mode 100644 java/org/apache/jasper/el/VariableResolverImpl.java create mode 100644 java/org/apache/jasper/optimizations/ELInterpreterTagSetters.java create mode 100644 java/org/apache/jasper/optimizations/StringInterpreterEnum.java create mode 100644 java/org/apache/jasper/resources/LocalStrings.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_cs.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_de.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_es.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_fr.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_ja.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_ko.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_pt.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_ru.properties create mode 100644 java/org/apache/jasper/resources/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/jasper/runtime/BodyContentImpl.java create mode 100644 java/org/apache/jasper/runtime/ExceptionUtils.java create mode 100644 java/org/apache/jasper/runtime/HttpJspBase.java create mode 100644 java/org/apache/jasper/runtime/InstanceManagerFactory.java create mode 100644 java/org/apache/jasper/runtime/JspApplicationContextImpl.java create mode 100644 java/org/apache/jasper/runtime/JspContextWrapper.java create mode 100644 java/org/apache/jasper/runtime/JspFactoryImpl.java create mode 100644 java/org/apache/jasper/runtime/JspFragmentHelper.java create mode 100644 java/org/apache/jasper/runtime/JspRuntimeLibrary.java create mode 100644 java/org/apache/jasper/runtime/JspSourceDependent.java create mode 100644 java/org/apache/jasper/runtime/JspSourceDirectives.java create mode 100644 java/org/apache/jasper/runtime/JspSourceImports.java create mode 100644 java/org/apache/jasper/runtime/JspWriterImpl.java create mode 100644 java/org/apache/jasper/runtime/PageContextImpl.java create mode 100644 java/org/apache/jasper/runtime/ProtectedFunctionMapper.java create mode 100644 java/org/apache/jasper/runtime/ServletResponseWrapperInclude.java create mode 100644 java/org/apache/jasper/runtime/TagHandlerPool.java create mode 100644 java/org/apache/jasper/security/SecurityClassLoad.java create mode 100644 java/org/apache/jasper/security/SecurityUtil.java create mode 100644 java/org/apache/jasper/servlet/JasperInitializer.java create mode 100644 java/org/apache/jasper/servlet/JasperLoader.java create mode 100644 java/org/apache/jasper/servlet/JspCServletContext.java create mode 100644 java/org/apache/jasper/servlet/JspServlet.java create mode 100644 java/org/apache/jasper/servlet/JspServletWrapper.java create mode 100644 java/org/apache/jasper/servlet/TldPreScanned.java create mode 100644 java/org/apache/jasper/servlet/TldScanner.java create mode 100644 java/org/apache/jasper/servlet/mbeans-descriptors.xml create mode 100644 java/org/apache/jasper/tagplugins/jstl/Util.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Catch.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Choose.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/ForEach.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/ForTokens.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/If.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Import.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Otherwise.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Out.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Param.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Redirect.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Remove.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Set.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/Url.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/core/When.java create mode 100644 java/org/apache/jasper/tagplugins/jstl/tagPlugins.xml create mode 100644 java/org/apache/jasper/util/FastRemovalDequeue.java create mode 100644 java/org/apache/jasper/util/UniqueAttributesImpl.java create mode 100644 java/org/apache/juli/AsyncFileHandler.java create mode 100644 java/org/apache/juli/ClassLoaderLogManager.java create mode 100644 java/org/apache/juli/DateFormatCache.java create mode 100644 java/org/apache/juli/FileHandler.java create mode 100644 java/org/apache/juli/JdkLoggerFormatter.java create mode 100644 java/org/apache/juli/OneLineFormatter.java create mode 100644 java/org/apache/juli/VerbatimFormatter.java create mode 100644 java/org/apache/juli/WebappProperties.java create mode 100644 java/org/apache/juli/logging/DirectJDKLog.java create mode 100644 java/org/apache/juli/logging/Log.java create mode 100644 java/org/apache/juli/logging/LogConfigurationException.java create mode 100644 java/org/apache/juli/logging/LogFactory.java create mode 100644 java/org/apache/juli/logging/package.html create mode 100644 java/org/apache/naming/AbstractRef.java create mode 100644 java/org/apache/naming/ContextAccessController.java create mode 100644 java/org/apache/naming/ContextBindings.java create mode 100644 java/org/apache/naming/EjbRef.java create mode 100644 java/org/apache/naming/HandlerRef.java create mode 100644 java/org/apache/naming/LocalStrings.properties create mode 100644 java/org/apache/naming/LocalStrings_cs.properties create mode 100644 java/org/apache/naming/LocalStrings_de.properties create mode 100644 java/org/apache/naming/LocalStrings_es.properties create mode 100644 java/org/apache/naming/LocalStrings_fr.properties create mode 100644 java/org/apache/naming/LocalStrings_ja.properties create mode 100644 java/org/apache/naming/LocalStrings_ko.properties create mode 100644 java/org/apache/naming/LocalStrings_ru.properties create mode 100644 java/org/apache/naming/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/naming/LookupRef.java create mode 100644 java/org/apache/naming/NameParserImpl.java create mode 100644 java/org/apache/naming/NamingContext.java create mode 100644 java/org/apache/naming/NamingContextBindingsEnumeration.java create mode 100644 java/org/apache/naming/NamingContextEnumeration.java create mode 100644 java/org/apache/naming/NamingEntry.java create mode 100644 java/org/apache/naming/ResourceEnvRef.java create mode 100644 java/org/apache/naming/ResourceLinkRef.java create mode 100644 java/org/apache/naming/ResourceRef.java create mode 100644 java/org/apache/naming/SelectorContext.java create mode 100644 java/org/apache/naming/ServiceRef.java create mode 100644 java/org/apache/naming/StringManager.java create mode 100644 java/org/apache/naming/TransactionRef.java create mode 100644 java/org/apache/naming/factory/BeanFactory.java create mode 100644 java/org/apache/naming/factory/Constants.java create mode 100644 java/org/apache/naming/factory/DataSourceLinkFactory.java create mode 100644 java/org/apache/naming/factory/EjbFactory.java create mode 100644 java/org/apache/naming/factory/FactoryBase.java create mode 100644 java/org/apache/naming/factory/LocalStrings.properties create mode 100644 java/org/apache/naming/factory/LocalStrings_cs.properties create mode 100644 java/org/apache/naming/factory/LocalStrings_es.properties create mode 100644 java/org/apache/naming/factory/LocalStrings_fr.properties create mode 100644 java/org/apache/naming/factory/LocalStrings_ja.properties create mode 100644 java/org/apache/naming/factory/LocalStrings_ko.properties create mode 100644 java/org/apache/naming/factory/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/naming/factory/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/naming/factory/LookupFactory.java create mode 100644 java/org/apache/naming/factory/MailSessionFactory.java create mode 100644 java/org/apache/naming/factory/OpenEjbFactory.java create mode 100644 java/org/apache/naming/factory/ResourceEnvFactory.java create mode 100644 java/org/apache/naming/factory/ResourceFactory.java create mode 100644 java/org/apache/naming/factory/ResourceLinkFactory.java create mode 100644 java/org/apache/naming/factory/SendMailFactory.java create mode 100644 java/org/apache/naming/factory/TransactionFactory.java create mode 100644 java/org/apache/naming/factory/package.html create mode 100644 java/org/apache/naming/factory/webservices/LocalStrings.properties create mode 100644 java/org/apache/naming/factory/webservices/LocalStrings_de.properties create mode 100644 java/org/apache/naming/factory/webservices/LocalStrings_fr.properties create mode 100644 java/org/apache/naming/factory/webservices/LocalStrings_ja.properties create mode 100644 java/org/apache/naming/factory/webservices/LocalStrings_ko.properties create mode 100644 java/org/apache/naming/factory/webservices/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/naming/factory/webservices/ServiceProxy.java create mode 100644 java/org/apache/naming/factory/webservices/ServiceRefFactory.java create mode 100644 java/org/apache/naming/java/javaURLContextFactory.java create mode 100644 java/org/apache/naming/java/package.html create mode 100644 java/org/apache/naming/package.html create mode 100644 java/org/apache/tomcat/ContextBind.java create mode 100644 java/org/apache/tomcat/InstanceManager.java create mode 100644 java/org/apache/tomcat/InstanceManagerBindings.java create mode 100644 java/org/apache/tomcat/InstrumentableClassLoader.java create mode 100644 java/org/apache/tomcat/Jar.java create mode 100644 java/org/apache/tomcat/JarScanFilter.java create mode 100644 java/org/apache/tomcat/JarScanType.java create mode 100644 java/org/apache/tomcat/JarScanner.java create mode 100644 java/org/apache/tomcat/JarScannerCallback.java create mode 100644 java/org/apache/tomcat/PeriodicEventListener.java create mode 100644 java/org/apache/tomcat/SimpleInstanceManager.java create mode 100644 java/org/apache/tomcat/buildutil/CheckEol.java create mode 100644 java/org/apache/tomcat/buildutil/ForceUtcTimeZone.java create mode 100644 java/org/apache/tomcat/buildutil/MimeTypeMappings.java create mode 100644 java/org/apache/tomcat/buildutil/RepeatableArchive.java create mode 100644 java/org/apache/tomcat/buildutil/Txt2Html.java create mode 100644 java/org/apache/tomcat/buildutil/Utils.java create mode 100644 java/org/apache/tomcat/buildutil/translate/BackportBase.java create mode 100644 java/org/apache/tomcat/buildutil/translate/BackportEnglish.java create mode 100644 java/org/apache/tomcat/buildutil/translate/BackportTranslations.java create mode 100644 java/org/apache/tomcat/buildutil/translate/Constants.java create mode 100644 java/org/apache/tomcat/buildutil/translate/Import.java create mode 100644 java/org/apache/tomcat/buildutil/translate/Utils.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/Constants.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/ListException.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/Utils.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java create mode 100644 java/org/apache/tomcat/dbcp/dbcp2/package-info.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/BaseObject.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/DestroyMode.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/ObjectPool.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/PoolUtils.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/PooledObject.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/TrackedUse.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/UsageTracking.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/CallStackUtils.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/SecurityManagerCallStack.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java create mode 100644 java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java create mode 100644 java/org/apache/tomcat/jni/Buffer.java create mode 100644 java/org/apache/tomcat/jni/CertificateVerifier.java create mode 100644 java/org/apache/tomcat/jni/FileInfo.java create mode 100644 java/org/apache/tomcat/jni/Library.java create mode 100644 java/org/apache/tomcat/jni/LibraryNotFoundError.java create mode 100644 java/org/apache/tomcat/jni/Pool.java create mode 100644 java/org/apache/tomcat/jni/SSL.java create mode 100644 java/org/apache/tomcat/jni/SSLConf.java create mode 100644 java/org/apache/tomcat/jni/SSLContext.java create mode 100644 java/org/apache/tomcat/jni/Sockaddr.java create mode 100644 java/org/apache/tomcat/util/Diagnostics.java create mode 100644 java/org/apache/tomcat/util/ExceptionUtils.java create mode 100644 java/org/apache/tomcat/util/IntrospectionUtils.java create mode 100644 java/org/apache/tomcat/util/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/MultiThrowable.java create mode 100644 java/org/apache/tomcat/util/XReflectionIntrospectionUtils.java create mode 100644 java/org/apache/tomcat/util/bcel/Const.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/Annotations.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ArrayElementValue.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ClassElementValue.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ClassFormatException.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ClassParser.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/Constant.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ConstantUtf8.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ElementValue.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/ElementValuePair.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/EnumElementValue.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/JavaClass.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/Utility.java create mode 100644 java/org/apache/tomcat/util/bcel/classfile/package-info.java create mode 100644 java/org/apache/tomcat/util/bcel/package-info.java create mode 100644 java/org/apache/tomcat/util/buf/AbstractChunk.java create mode 100644 java/org/apache/tomcat/util/buf/Ascii.java create mode 100644 java/org/apache/tomcat/util/buf/Asn1Parser.java create mode 100644 java/org/apache/tomcat/util/buf/Asn1Writer.java create mode 100644 java/org/apache/tomcat/util/buf/B2CConverter.java create mode 100644 java/org/apache/tomcat/util/buf/ByteBufferHolder.java create mode 100644 java/org/apache/tomcat/util/buf/ByteBufferUtils.java create mode 100644 java/org/apache/tomcat/util/buf/ByteChunk.java create mode 100644 java/org/apache/tomcat/util/buf/C2BConverter.java create mode 100644 java/org/apache/tomcat/util/buf/CharChunk.java create mode 100644 java/org/apache/tomcat/util/buf/CharsetCache.java create mode 100644 java/org/apache/tomcat/util/buf/CharsetUtil.java create mode 100644 java/org/apache/tomcat/util/buf/EncodedSolidusHandling.java create mode 100644 java/org/apache/tomcat/util/buf/HexUtils.java create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/buf/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/buf/MessageBytes.java create mode 100644 java/org/apache/tomcat/util/buf/StringCache.java create mode 100644 java/org/apache/tomcat/util/buf/StringUtils.java create mode 100644 java/org/apache/tomcat/util/buf/UDecoder.java create mode 100644 java/org/apache/tomcat/util/buf/UEncoder.java create mode 100644 java/org/apache/tomcat/util/buf/UriUtil.java create mode 100644 java/org/apache/tomcat/util/buf/Utf8Encoder.java create mode 100644 java/org/apache/tomcat/util/buf/package.html create mode 100644 java/org/apache/tomcat/util/codec/binary/Base64.java create mode 100644 java/org/apache/tomcat/util/codec/binary/BaseNCodec.java create mode 100644 java/org/apache/tomcat/util/codec/binary/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/codec/binary/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/codec/binary/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/codec/binary/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/codec/binary/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/codec/binary/StringUtils.java create mode 100644 java/org/apache/tomcat/util/codec/binary/package-info.java create mode 100644 java/org/apache/tomcat/util/collections/CaseInsensitiveKeyMap.java create mode 100644 java/org/apache/tomcat/util/collections/ConcurrentCache.java create mode 100644 java/org/apache/tomcat/util/collections/ManagedConcurrentWeakHashMap.java create mode 100644 java/org/apache/tomcat/util/collections/SynchronizedQueue.java create mode 100644 java/org/apache/tomcat/util/collections/SynchronizedStack.java create mode 100644 java/org/apache/tomcat/util/compat/Jre16Compat.java create mode 100644 java/org/apache/tomcat/util/compat/Jre19Compat.java create mode 100644 java/org/apache/tomcat/util/compat/Jre21Compat.java create mode 100644 java/org/apache/tomcat/util/compat/Jre22Compat.java create mode 100644 java/org/apache/tomcat/util/compat/JreCompat.java create mode 100644 java/org/apache/tomcat/util/compat/JrePlatform.java create mode 100644 java/org/apache/tomcat/util/compat/JreVendor.java create mode 100644 java/org/apache/tomcat/util/compat/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/compat/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/compat/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/compat/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/compat/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/descriptor/Constants.java create mode 100644 java/org/apache/tomcat/util/descriptor/DigesterFactory.java create mode 100644 java/org/apache/tomcat/util/descriptor/InputSourceUtil.java create mode 100644 java/org/apache/tomcat/util/descriptor/LocalResolver.java create mode 100644 java/org/apache/tomcat/util/descriptor/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/descriptor/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/descriptor/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/descriptor/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/descriptor/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/descriptor/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/descriptor/XmlErrorHandler.java create mode 100644 java/org/apache/tomcat/util/descriptor/XmlIdentifiers.java create mode 100644 java/org/apache/tomcat/util/descriptor/tagplugin/TagPluginParser.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/ImplicitTldRuleSet.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/descriptor/tld/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/descriptor/tld/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/descriptor/tld/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/descriptor/tld/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/descriptor/tld/TagFileXml.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/TagXml.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/TaglibXml.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/TldParser.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/TldResourcePath.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/TldRuleSet.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/ValidatorXml.java create mode 100644 java/org/apache/tomcat/util/descriptor/tld/package-info.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ApplicationParameter.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/Constants.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextEjb.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextEnvironment.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextHandler.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextLocalEjb.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextResource.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextResourceEnvRef.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextResourceLink.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextService.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ContextTransaction.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ErrorPage.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/FilterDef.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/FilterMap.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/FragmentJarScannerCallback.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/Injectable.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/InjectionTarget.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/JspConfigDescriptorImpl.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/JspPropertyGroup.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/JspPropertyGroupDescriptorImpl.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/descriptor/web/LoginConfig.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/MessageDestination.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/MessageDestinationRef.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/MultipartDef.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/NamingResources.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ResourceBase.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/SecurityCollection.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/SecurityConstraint.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/SecurityRoleRef.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/ServletDef.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/SessionConfig.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/TaglibDescriptorImpl.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/WebRuleSet.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/WebXml.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/WebXmlParser.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/XmlEncodingBase.java create mode 100644 java/org/apache/tomcat/util/descriptor/web/mbeans-descriptors.xml create mode 100644 java/org/apache/tomcat/util/descriptor/web/package.html create mode 100644 java/org/apache/tomcat/util/digester/AbstractObjectCreationFactory.java create mode 100644 java/org/apache/tomcat/util/digester/ArrayStack.java create mode 100644 java/org/apache/tomcat/util/digester/CallMethodRule.java create mode 100644 java/org/apache/tomcat/util/digester/CallParamRule.java create mode 100644 java/org/apache/tomcat/util/digester/Digester.java create mode 100644 java/org/apache/tomcat/util/digester/DocumentProperties.java create mode 100644 java/org/apache/tomcat/util/digester/EnvironmentPropertySource.java create mode 100644 java/org/apache/tomcat/util/digester/FactoryCreateRule.java create mode 100644 java/org/apache/tomcat/util/digester/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/digester/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/digester/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/digester/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/digester/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/digester/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/digester/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/digester/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/digester/ObjectCreateRule.java create mode 100644 java/org/apache/tomcat/util/digester/ObjectCreationFactory.java create mode 100644 java/org/apache/tomcat/util/digester/Rule.java create mode 100644 java/org/apache/tomcat/util/digester/RuleSet.java create mode 100644 java/org/apache/tomcat/util/digester/Rules.java create mode 100644 java/org/apache/tomcat/util/digester/RulesBase.java create mode 100644 java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java create mode 100644 java/org/apache/tomcat/util/digester/SetNextRule.java create mode 100644 java/org/apache/tomcat/util/digester/SetPropertiesRule.java create mode 100644 java/org/apache/tomcat/util/digester/SystemPropertySource.java create mode 100644 java/org/apache/tomcat/util/digester/package.html create mode 100644 java/org/apache/tomcat/util/file/ConfigFileLoader.java create mode 100644 java/org/apache/tomcat/util/file/ConfigurationSource.java create mode 100644 java/org/apache/tomcat/util/file/Matcher.java create mode 100644 java/org/apache/tomcat/util/file/package.html create mode 100644 java/org/apache/tomcat/util/http/ConcurrentDateFormat.java create mode 100644 java/org/apache/tomcat/util/http/CookieProcessor.java create mode 100644 java/org/apache/tomcat/util/http/CookieProcessorBase.java create mode 100644 java/org/apache/tomcat/util/http/FastHttpDateFormat.java create mode 100644 java/org/apache/tomcat/util/http/HeaderUtil.java create mode 100644 java/org/apache/tomcat/util/http/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/http/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/http/MimeHeaders.java create mode 100644 java/org/apache/tomcat/util/http/Parameters.java create mode 100644 java/org/apache/tomcat/util/http/RequestUtil.java create mode 100644 java/org/apache/tomcat/util/http/ResponseUtil.java create mode 100644 java/org/apache/tomcat/util/http/Rfc6265CookieProcessor.java create mode 100644 java/org/apache/tomcat/util/http/SameSiteCookies.java create mode 100644 java/org/apache/tomcat/util/http/ServerCookie.java create mode 100644 java/org/apache/tomcat/util/http/ServerCookies.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/ByteArrayOutputStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/DeferredFileOutputStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItem.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemFactory.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemHeadersSupport.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemIterator.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileItemStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileUpload.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileUploadException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/FileUtils.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/IOUtils.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/MultipartStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/ParameterParser.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/ProgressListener.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/RequestContext.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/ThresholdingOutputStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/UploadContext.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/disk/package-info.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/FileCountLimitExceededException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/FileItemIteratorImpl.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/FileItemStreamImpl.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/FileSizeLimitExceededException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/FileUploadIOException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/IOFileUploadException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/InvalidContentTypeException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/SizeException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/SizeLimitExceededException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/impl/package-info.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/package-info.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/servlet/package-info.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/Closeable.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/Streams.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/mime/package-info.java create mode 100644 java/org/apache/tomcat/util/http/fileupload/util/package-info.java create mode 100644 java/org/apache/tomcat/util/http/package.html create mode 100644 java/org/apache/tomcat/util/http/parser/AcceptEncoding.java create mode 100644 java/org/apache/tomcat/util/http/parser/AcceptLanguage.java create mode 100644 java/org/apache/tomcat/util/http/parser/Authorization.java create mode 100644 java/org/apache/tomcat/util/http/parser/ContentRange.java create mode 100644 java/org/apache/tomcat/util/http/parser/Cookie.java create mode 100644 java/org/apache/tomcat/util/http/parser/EntityTag.java create mode 100644 java/org/apache/tomcat/util/http/parser/Host.java create mode 100644 java/org/apache/tomcat/util/http/parser/HttpParser.java create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/http/parser/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/http/parser/MediaType.java create mode 100644 java/org/apache/tomcat/util/http/parser/MediaTypeCache.java create mode 100644 java/org/apache/tomcat/util/http/parser/Priority.java create mode 100644 java/org/apache/tomcat/util/http/parser/Ranges.java create mode 100644 java/org/apache/tomcat/util/http/parser/SkipResult.java create mode 100644 java/org/apache/tomcat/util/http/parser/StructuredField.java create mode 100644 java/org/apache/tomcat/util/http/parser/TokenList.java create mode 100644 java/org/apache/tomcat/util/http/parser/Upgrade.java create mode 100644 java/org/apache/tomcat/util/json/JSONFilter.java create mode 100644 java/org/apache/tomcat/util/json/JSONParser.java create mode 100644 java/org/apache/tomcat/util/json/JSONParser.jjt create mode 100644 java/org/apache/tomcat/util/json/JSONParserConstants.java create mode 100644 java/org/apache/tomcat/util/json/JSONParserTokenManager.java create mode 100644 java/org/apache/tomcat/util/json/JavaCharStream.java create mode 100644 java/org/apache/tomcat/util/json/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/json/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/json/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/json/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/json/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/json/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/json/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/json/ParseException.java create mode 100644 java/org/apache/tomcat/util/json/Token.java create mode 100644 java/org/apache/tomcat/util/json/TokenMgrError.java create mode 100644 java/org/apache/tomcat/util/log/CaptureLog.java create mode 100644 java/org/apache/tomcat/util/log/SystemLogHandler.java create mode 100644 java/org/apache/tomcat/util/log/UserDataHelper.java create mode 100644 java/org/apache/tomcat/util/modeler/AttributeInfo.java create mode 100644 java/org/apache/tomcat/util/modeler/BaseAttributeFilter.java create mode 100644 java/org/apache/tomcat/util/modeler/BaseModelMBean.java create mode 100644 java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java create mode 100644 java/org/apache/tomcat/util/modeler/FeatureInfo.java create mode 100644 java/org/apache/tomcat/util/modeler/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/modeler/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/modeler/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/modeler/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/modeler/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/modeler/ManagedBean.java create mode 100644 java/org/apache/tomcat/util/modeler/NoDescriptorRegistry.java create mode 100644 java/org/apache/tomcat/util/modeler/NotificationInfo.java create mode 100644 java/org/apache/tomcat/util/modeler/OperationInfo.java create mode 100644 java/org/apache/tomcat/util/modeler/ParameterInfo.java create mode 100644 java/org/apache/tomcat/util/modeler/Registry.java create mode 100644 java/org/apache/tomcat/util/modeler/RegistryMBean.java create mode 100644 java/org/apache/tomcat/util/modeler/Util.java create mode 100644 java/org/apache/tomcat/util/modeler/mbeans-descriptors.dtd create mode 100644 java/org/apache/tomcat/util/modeler/modules/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/modeler/modules/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/modeler/modules/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java create mode 100644 java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsIntrospectionSource.java create mode 100644 java/org/apache/tomcat/util/modeler/modules/ModelerSource.java create mode 100644 java/org/apache/tomcat/util/modeler/modules/package.html create mode 100644 java/org/apache/tomcat/util/modeler/package.html create mode 100644 java/org/apache/tomcat/util/net/AbstractEndpoint.java create mode 100644 java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java create mode 100644 java/org/apache/tomcat/util/net/Acceptor.java create mode 100644 java/org/apache/tomcat/util/net/ApplicationBufferHandler.java create mode 100644 java/org/apache/tomcat/util/net/Constants.java create mode 100644 java/org/apache/tomcat/util/net/DispatchType.java create mode 100644 java/org/apache/tomcat/util/net/IPv6Utils.java create mode 100644 java/org/apache/tomcat/util/net/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/net/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/net/Nio2Channel.java create mode 100644 java/org/apache/tomcat/util/net/Nio2Endpoint.java create mode 100644 java/org/apache/tomcat/util/net/NioChannel.java create mode 100644 java/org/apache/tomcat/util/net/NioEndpoint.java create mode 100644 java/org/apache/tomcat/util/net/SSLContext.java create mode 100644 java/org/apache/tomcat/util/net/SSLHostConfig.java create mode 100644 java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java create mode 100644 java/org/apache/tomcat/util/net/SSLImplementation.java create mode 100644 java/org/apache/tomcat/util/net/SSLSessionManager.java create mode 100644 java/org/apache/tomcat/util/net/SSLSupport.java create mode 100644 java/org/apache/tomcat/util/net/SSLUtil.java create mode 100644 java/org/apache/tomcat/util/net/SSLUtilBase.java create mode 100644 java/org/apache/tomcat/util/net/SecureNio2Channel.java create mode 100644 java/org/apache/tomcat/util/net/SecureNioChannel.java create mode 100644 java/org/apache/tomcat/util/net/SendfileDataBase.java create mode 100644 java/org/apache/tomcat/util/net/SendfileKeepAliveState.java create mode 100644 java/org/apache/tomcat/util/net/SendfileState.java create mode 100644 java/org/apache/tomcat/util/net/ServletConnectionImpl.java create mode 100644 java/org/apache/tomcat/util/net/SocketBufferHandler.java create mode 100644 java/org/apache/tomcat/util/net/SocketEvent.java create mode 100644 java/org/apache/tomcat/util/net/SocketProcessorBase.java create mode 100644 java/org/apache/tomcat/util/net/SocketProperties.java create mode 100644 java/org/apache/tomcat/util/net/SocketWrapperBase.java create mode 100644 java/org/apache/tomcat/util/net/TLSClientHelloExtractor.java create mode 100644 java/org/apache/tomcat/util/net/WriteBuffer.java create mode 100644 java/org/apache/tomcat/util/net/jsse/JSSEImplementation.java create mode 100644 java/org/apache/tomcat/util/net/jsse/JSSEKeyManager.java create mode 100644 java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java create mode 100644 java/org/apache/tomcat/util/net/jsse/JSSESupport.java create mode 100644 java/org/apache/tomcat/util/net/jsse/JSSEUtil.java create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/net/jsse/PEMFile.java create mode 100644 java/org/apache/tomcat/util/net/mbeans-descriptors.xml create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLConf.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLImplementation.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLSessionContext.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLSessionStats.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java create mode 100644 java/org/apache/tomcat/util/net/openssl/OpenSSLX509Certificate.java create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/Authentication.java create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/Cipher.java create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/Encryption.java create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/EncryptionLevel.java create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/KeyExchange.java create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/MessageDigest.java create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java create mode 100644 java/org/apache/tomcat/util/net/openssl/ciphers/Protocol.java create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/OpenSSLImplementation.java create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/OpenSSLSessionContext.java create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/OpenSSLSessionStats.java create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java create mode 100644 java/org/apache/tomcat/util/net/openssl/panama/OpenSSLX509Certificate.java create mode 100644 java/org/apache/tomcat/util/openssl/SSL_CTX_set_alpn_select_cb$cb.java create mode 100644 java/org/apache/tomcat/util/openssl/SSL_CTX_set_cert_verify_callback$cb.java create mode 100644 java/org/apache/tomcat/util/openssl/SSL_CTX_set_tmp_dh_callback$dh.java create mode 100644 java/org/apache/tomcat/util/openssl/SSL_CTX_set_verify$callback.java create mode 100644 java/org/apache/tomcat/util/openssl/SSL_set_info_callback$cb.java create mode 100644 java/org/apache/tomcat/util/openssl/SSL_set_verify$callback.java create mode 100644 java/org/apache/tomcat/util/openssl/openssl_h.java create mode 100644 java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java create mode 100644 java/org/apache/tomcat/util/openssl/openssl_h_Macros.java create mode 100644 java/org/apache/tomcat/util/openssl/pem_password_cb.java create mode 100644 java/org/apache/tomcat/util/res/StringManager.java create mode 100644 java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java create mode 100644 java/org/apache/tomcat/util/scan/Constants.java create mode 100644 java/org/apache/tomcat/util/scan/JarFactory.java create mode 100644 java/org/apache/tomcat/util/scan/JarFileUrlJar.java create mode 100644 java/org/apache/tomcat/util/scan/JarFileUrlNestedJar.java create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/util/scan/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/scan/NonClosingJarInputStream.java create mode 100644 java/org/apache/tomcat/util/scan/ReferenceCountedJar.java create mode 100644 java/org/apache/tomcat/util/scan/StandardJarScanFilter.java create mode 100644 java/org/apache/tomcat/util/scan/StandardJarScanner.java create mode 100644 java/org/apache/tomcat/util/scan/UrlJar.java create mode 100644 java/org/apache/tomcat/util/scan/package.html create mode 100644 java/org/apache/tomcat/util/security/ConcurrentMessageDigest.java create mode 100644 java/org/apache/tomcat/util/security/Escape.java create mode 100644 java/org/apache/tomcat/util/security/KeyStoreUtil.java create mode 100644 java/org/apache/tomcat/util/security/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/security/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/security/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/security/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/security/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/security/MD5Encoder.java create mode 100644 java/org/apache/tomcat/util/security/PermissionCheck.java create mode 100644 java/org/apache/tomcat/util/security/PrivilegedGetTccl.java create mode 100644 java/org/apache/tomcat/util/security/PrivilegedSetAccessControlContext.java create mode 100644 java/org/apache/tomcat/util/security/PrivilegedSetTccl.java create mode 100644 java/org/apache/tomcat/util/threads/Constants.java create mode 100644 java/org/apache/tomcat/util/threads/InlineExecutorService.java create mode 100644 java/org/apache/tomcat/util/threads/LimitLatch.java create mode 100644 java/org/apache/tomcat/util/threads/LocalStrings.properties create mode 100644 java/org/apache/tomcat/util/threads/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/util/threads/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/util/threads/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/util/threads/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/util/threads/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/util/threads/ResizableExecutor.java create mode 100644 java/org/apache/tomcat/util/threads/ScheduledThreadPoolExecutor.java create mode 100644 java/org/apache/tomcat/util/threads/StopPooledThreadException.java create mode 100644 java/org/apache/tomcat/util/threads/TaskQueue.java create mode 100644 java/org/apache/tomcat/util/threads/TaskThread.java create mode 100644 java/org/apache/tomcat/util/threads/TaskThreadFactory.java create mode 100644 java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java create mode 100644 java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java create mode 100644 java/org/apache/tomcat/util/xreflection/ObjectReflectionPropertyInspector.java create mode 100644 java/org/apache/tomcat/util/xreflection/ReflectionLessCodeGenerator.java create mode 100644 java/org/apache/tomcat/util/xreflection/ReflectionProperty.java create mode 100644 java/org/apache/tomcat/util/xreflection/SetPropertyClass.java create mode 100644 java/org/apache/tomcat/websocket/AsyncChannelGroupUtil.java create mode 100644 java/org/apache/tomcat/websocket/AsyncChannelWrapper.java create mode 100644 java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java create mode 100644 java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java create mode 100644 java/org/apache/tomcat/websocket/AuthenticationException.java create mode 100644 java/org/apache/tomcat/websocket/AuthenticationType.java create mode 100644 java/org/apache/tomcat/websocket/Authenticator.java create mode 100644 java/org/apache/tomcat/websocket/AuthenticatorFactory.java create mode 100644 java/org/apache/tomcat/websocket/BackgroundProcess.java create mode 100644 java/org/apache/tomcat/websocket/BackgroundProcessManager.java create mode 100644 java/org/apache/tomcat/websocket/BasicAuthenticator.java create mode 100644 java/org/apache/tomcat/websocket/ClientEndpointHolder.java create mode 100644 java/org/apache/tomcat/websocket/Constants.java create mode 100644 java/org/apache/tomcat/websocket/DecoderEntry.java create mode 100644 java/org/apache/tomcat/websocket/DigestAuthenticator.java create mode 100644 java/org/apache/tomcat/websocket/EndpointClassHolder.java create mode 100644 java/org/apache/tomcat/websocket/EndpointHolder.java create mode 100644 java/org/apache/tomcat/websocket/FutureToSendHandler.java create mode 100644 java/org/apache/tomcat/websocket/LocalStrings.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/websocket/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/websocket/MessageHandlerResult.java create mode 100644 java/org/apache/tomcat/websocket/MessageHandlerResultType.java create mode 100644 java/org/apache/tomcat/websocket/MessagePart.java create mode 100644 java/org/apache/tomcat/websocket/PerMessageDeflate.java create mode 100644 java/org/apache/tomcat/websocket/PojoClassHolder.java create mode 100644 java/org/apache/tomcat/websocket/PojoHolder.java create mode 100644 java/org/apache/tomcat/websocket/ReadBufferOverflowException.java create mode 100644 java/org/apache/tomcat/websocket/Transformation.java create mode 100644 java/org/apache/tomcat/websocket/TransformationFactory.java create mode 100644 java/org/apache/tomcat/websocket/TransformationResult.java create mode 100644 java/org/apache/tomcat/websocket/Util.java create mode 100644 java/org/apache/tomcat/websocket/WrappedMessageHandler.java create mode 100644 java/org/apache/tomcat/websocket/WsContainerProvider.java create mode 100644 java/org/apache/tomcat/websocket/WsExtension.java create mode 100644 java/org/apache/tomcat/websocket/WsExtensionParameter.java create mode 100644 java/org/apache/tomcat/websocket/WsFrameBase.java create mode 100644 java/org/apache/tomcat/websocket/WsFrameClient.java create mode 100644 java/org/apache/tomcat/websocket/WsHandshakeResponse.java create mode 100644 java/org/apache/tomcat/websocket/WsIOException.java create mode 100644 java/org/apache/tomcat/websocket/WsPongMessage.java create mode 100644 java/org/apache/tomcat/websocket/WsRemoteEndpointAsync.java create mode 100644 java/org/apache/tomcat/websocket/WsRemoteEndpointBase.java create mode 100644 java/org/apache/tomcat/websocket/WsRemoteEndpointBasic.java create mode 100644 java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java create mode 100644 java/org/apache/tomcat/websocket/WsRemoteEndpointImplClient.java create mode 100644 java/org/apache/tomcat/websocket/WsSession.java create mode 100644 java/org/apache/tomcat/websocket/WsWebSocketContainer.java create mode 100644 java/org/apache/tomcat/websocket/pojo/Constants.java create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_pt_BR.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_ru.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoEndpointClient.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoEndpointServer.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerBase.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBase.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBinary.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialText.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBase.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBinary.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholePong.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java create mode 100644 java/org/apache/tomcat/websocket/pojo/PojoPathParam.java create mode 100644 java/org/apache/tomcat/websocket/pojo/package-info.java create mode 100644 java/org/apache/tomcat/websocket/server/Constants.java create mode 100644 java/org/apache/tomcat/websocket/server/DefaultServerEndpointConfigurator.java create mode 100644 java/org/apache/tomcat/websocket/server/LocalStrings.properties create mode 100644 java/org/apache/tomcat/websocket/server/LocalStrings_cs.properties create mode 100644 java/org/apache/tomcat/websocket/server/LocalStrings_de.properties create mode 100644 java/org/apache/tomcat/websocket/server/LocalStrings_es.properties create mode 100644 java/org/apache/tomcat/websocket/server/LocalStrings_fr.properties create mode 100644 java/org/apache/tomcat/websocket/server/LocalStrings_ja.properties create mode 100644 java/org/apache/tomcat/websocket/server/LocalStrings_ko.properties create mode 100644 java/org/apache/tomcat/websocket/server/LocalStrings_zh_CN.properties create mode 100644 java/org/apache/tomcat/websocket/server/UpgradeUtil.java create mode 100644 java/org/apache/tomcat/websocket/server/UriTemplate.java create mode 100644 java/org/apache/tomcat/websocket/server/WsContextListener.java create mode 100644 java/org/apache/tomcat/websocket/server/WsFilter.java create mode 100644 java/org/apache/tomcat/websocket/server/WsFrameServer.java create mode 100644 java/org/apache/tomcat/websocket/server/WsHandshakeRequest.java create mode 100644 java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java create mode 100644 java/org/apache/tomcat/websocket/server/WsMappingResult.java create mode 100644 java/org/apache/tomcat/websocket/server/WsPerSessionServerEndpointConfig.java create mode 100644 java/org/apache/tomcat/websocket/server/WsRemoteEndpointImplServer.java create mode 100644 java/org/apache/tomcat/websocket/server/WsSci.java create mode 100644 java/org/apache/tomcat/websocket/server/WsServerContainer.java create mode 100644 java/org/apache/tomcat/websocket/server/WsSessionListener.java create mode 100644 java/org/apache/tomcat/websocket/server/WsWriteTimeout.java create mode 100644 java/org/apache/tomcat/websocket/server/package-info.java create mode 100644 modules/cxf/.gitignore create mode 100644 modules/cxf/pom.xml create mode 100644 modules/cxf/src/main/java/tomcat/cxf/JsonBean.java create mode 100644 modules/cxf/src/main/resources/META-INF/beans.xml create mode 100644 modules/cxf/src/main/resources/META-INF/web-fragment.xml create mode 100644 modules/jdbc-pool/LICENSE create mode 100644 modules/jdbc-pool/NOTICE create mode 100644 modules/jdbc-pool/build.properties.default create mode 100644 modules/jdbc-pool/build.xml create mode 100644 modules/jdbc-pool/doc/changelog.xml create mode 100644 modules/jdbc-pool/doc/jdbc-pool.xml create mode 100644 modules/jdbc-pool/doc/package.xsl create mode 100644 modules/jdbc-pool/doc/project.xml create mode 100644 modules/jdbc-pool/pom.xml create mode 100644 modules/jdbc-pool/resources/MANIFEST.MF create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/naming/GenericNamingResourcesFactory.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ClassLoaderUtil.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSource.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceFactory.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceProxy.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DisposableConnectionFacade.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/FairBlockingQueue.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/JdbcInterceptor.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/MultiLockFairBlockingQueue.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolConfiguration.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolExhaustedException.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolUtilities.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnectionMBean.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ProxyConnection.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/StatementFacade.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/TrapException.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/Validator.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XADataSource.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ConnectionState.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/QueryTimeoutInterceptor.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimer.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimerMBean.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReportJmx.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReportJmxMBean.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCacheMBean.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementFinalizer.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/mbeans-descriptors.xml create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/ConnectionPool.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/ConnectionPoolMBean.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/JmxUtil.java create mode 100644 modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/mbeans-descriptors.xml create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug51582.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug53367.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54225.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54227.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54978.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/PoolPropertiesTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/ShouldForceReconnectTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/InduceSlowQuery.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCounterInterceptor.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestInterceptor.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/AbandonPercentageTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/AlternateUsernameTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Async0IdleTestBug50477.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/BorrowWaitTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Bug50571.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Bug50805.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/CheckOutThreadTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/ConnectCountTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/CreateTestTable.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/DefaultProperties.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/DefaultTestCase.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/EqualsHashCodeTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/FairnessTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/JmxPasswordTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/MultipleCloseTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/PoolCleanerTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/PoolPurgeTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/SimplePOJOAsyncExample.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/SimplePOJOExample.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/StarvationTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/StatementFinalizerTest.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestAsyncQueue.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestConcurrency.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestConnectionState.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestException.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestGCClose.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestGetConnection.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestInterceptorShortName.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestJdbcInterceptorConfigParsing.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestQueryTimeoutInterceptor.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSizePreservation.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSlowQueryComparator.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSlowQueryReport.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestStatementCache.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSuspectTimeout.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestTimeout.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestValidation.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestValidationQueryTimeout.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TwoDataSources.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Connection.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Driver.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/ResultSet.java create mode 100644 modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Statement.java create mode 100644 modules/owb/.gitignore create mode 100644 modules/owb/pom.xml create mode 100644 modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansContextLifecycleListener.java create mode 100644 modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansInstanceManager.java create mode 100644 modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansListener.java create mode 100644 modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansSecurityFilter.java create mode 100644 modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansSecurityValve.java create mode 100644 modules/owb/src/main/java/org/apache/webbeans/web/tomcat/TomcatPlugin.java create mode 100644 modules/owb/src/main/java/org/apache/webbeans/web/tomcat/TomcatSecurityService.java create mode 100644 modules/owb/src/main/resources/META-INF/openwebbeans/openwebbeans.properties create mode 100644 modules/owb/src/main/resources/META-INF/services/org.apache.webbeans.spi.plugins.OpenWebBeansPlugin create mode 100644 modules/owb/src/main/resources/org/apache/webbeans/web/tomcat/LocalStrings.properties create mode 100644 modules/stuffed/Dockerfile create mode 100644 modules/stuffed/DockerfileGraal create mode 100644 modules/stuffed/README.md create mode 100644 modules/stuffed/conf/.gitignore create mode 100644 modules/stuffed/pom.xml create mode 100644 modules/stuffed/tomcat-jni.json create mode 100644 modules/stuffed/tomcat-reflection.json create mode 100644 modules/stuffed/tomcat-resource.json create mode 100644 modules/stuffed/tomcat.yaml create mode 100644 modules/stuffed/webapp-jspc.ant.xml create mode 100644 modules/stuffed/webapps/.gitignore create mode 100644 res/META-INF/annotations-api.jar.manifest create mode 100644 res/META-INF/bootstrap.jar.manifest create mode 100644 res/META-INF/default.license create mode 100644 res/META-INF/default.manifest create mode 100644 res/META-INF/default.notice create mode 100644 res/META-INF/default/.gitignore create mode 100644 res/META-INF/el-api.jar.manifest create mode 100644 res/META-INF/jasper-el.jar/services/jakarta.el.ExpressionFactory create mode 100644 res/META-INF/jasper-el.jar/web-fragment.xml create mode 100644 res/META-INF/jasper.jar/services/jakarta.servlet.ServletContainerInitializer create mode 100644 res/META-INF/jasper.jar/web-fragment.xml create mode 100644 res/META-INF/jaspic-api.jar.manifest create mode 100644 res/META-INF/jsp-api.jar.manifest create mode 100644 res/META-INF/servlet-api.jar.license create mode 100644 res/META-INF/servlet-api.jar.manifest create mode 100644 res/META-INF/servlet-api.jar.notice create mode 100644 res/META-INF/tomcat-websocket.jar/services/jakarta.servlet.ServletContainerInitializer create mode 100644 res/META-INF/tomcat-websocket.jar/services/jakarta.websocket.ContainerProvider create mode 100644 res/META-INF/tomcat-websocket.jar/services/jakarta.websocket.server.ServerEndpointConfig$Configurator create mode 100644 res/META-INF/tomcat-websocket.jar/web-fragment.xml create mode 100644 res/META-INF/websocket-api.jar.manifest create mode 100644 res/META-INF/websocket-client-api.jar.manifest create mode 100644 res/bnd/annotations-api.jar.tmp.bnd create mode 100644 res/bnd/build-defaults.bnd create mode 100644 res/bnd/catalina-ha.jar.tmp.bnd create mode 100644 res/bnd/catalina-ssi.jar.tmp.bnd create mode 100644 res/bnd/catalina-storeconfig.jar.tmp.bnd create mode 100644 res/bnd/catalina-tribes.jar.tmp.bnd create mode 100644 res/bnd/catalina.jar.tmp.bnd create mode 100644 res/bnd/el-api.jar.tmp.bnd create mode 100644 res/bnd/jasper-el.jar.tmp.bnd create mode 100644 res/bnd/jasper.jar.tmp.bnd create mode 100644 res/bnd/jaspic-api.jar.tmp.bnd create mode 100644 res/bnd/jsp-api.jar.tmp.bnd create mode 100644 res/bnd/servlet-api.jar.tmp.bnd create mode 100644 res/bnd/spec-defaults.bnd create mode 100644 res/bnd/tomcat-api.jar.tmp.bnd create mode 100644 res/bnd/tomcat-coyote.jar.tmp.bnd create mode 100644 res/bnd/tomcat-dbcp.jar.tmp.bnd create mode 100644 res/bnd/tomcat-embed-core.jar.tmp.bnd create mode 100644 res/bnd/tomcat-embed-el.jar.tmp.bnd create mode 100644 res/bnd/tomcat-embed-jasper.jar.tmp.bnd create mode 100644 res/bnd/tomcat-embed-websocket.jar.tmp.bnd create mode 100644 res/bnd/tomcat-jni.jar.tmp.bnd create mode 100644 res/bnd/tomcat-juli.jar.tmp.bnd create mode 100644 res/bnd/tomcat-util-scan.jar.tmp.bnd create mode 100644 res/bnd/tomcat-util.jar.tmp.bnd create mode 100644 res/bnd/tomcat-websocket.jar.tmp.bnd create mode 100644 res/bnd/websocket-api.jar.tmp.bnd create mode 100644 res/bnd/websocket-client-api.jar.tmp.bnd create mode 100644 res/checkstyle/checkstyle.xml create mode 100644 res/checkstyle/header-al2.txt create mode 100644 res/checkstyle/jakarta-checkstyle.xml create mode 100644 res/checkstyle/jakarta-import-control.xml create mode 100644 res/checkstyle/org-checkstyle.xml create mode 100644 res/checkstyle/org-import-control.xml create mode 100644 res/checkstyle/test-checkstyle.xml create mode 100644 res/deployer/build.xml create mode 100644 res/graal/README.md create mode 100755 res/graal/build-tomcat-native-image.sh create mode 100755 res/graal/graal-measure.sh create mode 100644 res/graal/tomcat-embed-core/native-image/native-image.properties create mode 100644 res/graal/tomcat-embed-core/native-image/tomcat-jni.json create mode 100644 res/graal/tomcat-embed-core/native-image/tomcat-reflection.json create mode 100644 res/graal/tomcat-embed-core/native-image/tomcat-resource.json create mode 100644 res/graal/tomcat-embed-el/native-image/native-image.properties create mode 100644 res/graal/tomcat-embed-el/native-image/tomcat-reflection.json create mode 100644 res/graal/tomcat-embed-el/native-image/tomcat-resource.json create mode 100644 res/graal/tomcat-embed-jasper/native-image/native-image.properties create mode 100644 res/graal/tomcat-embed-jasper/native-image/tomcat-reflection.json create mode 100644 res/graal/tomcat-embed-jasper/native-image/tomcat-resource.json create mode 100644 res/graal/tomcat-embed-programmatic/native-image/native-image.properties create mode 100644 res/graal/tomcat-embed-programmatic/native-image/tomcat-reflection.json create mode 100644 res/graal/tomcat-embed-programmatic/native-image/tomcat-resource.json create mode 100644 res/graal/tomcat-embed-websocket/native-image/native-image.properties create mode 100644 res/graal/tomcat-embed-websocket/native-image/tomcat-reflection.json create mode 100644 res/graal/tomcat-embed-websocket/native-image/tomcat-resource.json create mode 100644 res/ide-support/coding-style.txt create mode 100644 res/ide-support/eclipse/clean-up-asf-tomcat.xml create mode 100644 res/ide-support/eclipse/eclipse.classpath create mode 100644 res/ide-support/eclipse/eclipse.project create mode 100644 res/ide-support/eclipse/formatting-asf-tomcat.xml create mode 100644 res/ide-support/eclipse/java-compiler-errors-warnings.txt create mode 100644 res/ide-support/eclipse/org.eclipse.jdt.core.prefs.properties create mode 100644 res/ide-support/eclipse/start-tomcat.launch create mode 100644 res/ide-support/eclipse/stop-tomcat.launch create mode 100644 res/ide-support/idea/ant.xml create mode 100644 res/ide-support/idea/codeStyles/Project.xml create mode 100644 res/ide-support/idea/codeStyles/codeStyleConfig.xml create mode 100644 res/ide-support/idea/compiler.xml create mode 100644 res/ide-support/idea/copyright/Tomcat.xml create mode 100644 res/ide-support/idea/copyright/profiles_settings.xml create mode 100644 res/ide-support/idea/externalDependencies.xml create mode 100644 res/ide-support/idea/inspectionProfiles/Project_Default.xml create mode 100644 res/ide-support/idea/misc.xml create mode 100644 res/ide-support/idea/modules.xml create mode 100644 res/ide-support/idea/tomcat.iml create mode 100644 res/ide-support/idea/workspace.xml create mode 100644 res/ide-support/netbeans/README.txt create mode 100644 res/ide-support/netbeans/nb-tomcat-build.properties create mode 100644 res/ide-support/netbeans/nb-tomcat-project.properties create mode 100644 res/ide-support/netbeans/nb-tomcat.xml create mode 100644 res/ide-support/netbeans/project.xml create mode 100644 res/install-win/INSTALLLICENSE create mode 100644 res/install-win/Uninstall.exe.sig create mode 100644 res/install-win/header.bmp create mode 100644 res/install-win/side_left.bmp create mode 100644 res/install-win/tomcat-installer.exe.sig create mode 100644 res/install-win/tomcat-users_1.xml create mode 100644 res/install-win/tomcat-users_2.xml create mode 100644 res/install-win/tomcat.ico create mode 100644 res/install-win/tomcat.nsi create mode 100644 res/maven/README.txt create mode 100644 res/maven/mvn-pub.xml create mode 100644 res/maven/mvn.properties.default create mode 100644 res/maven/mvn.properties.release create mode 100644 res/maven/tomcat-annotations-api.pom create mode 100644 res/maven/tomcat-api.pom create mode 100644 res/maven/tomcat-catalina-ant.pom create mode 100644 res/maven/tomcat-catalina-ha.pom create mode 100644 res/maven/tomcat-catalina.pom create mode 100644 res/maven/tomcat-coyote.pom create mode 100644 res/maven/tomcat-dbcp.pom create mode 100644 res/maven/tomcat-el-api.pom create mode 100644 res/maven/tomcat-embed-core.pom create mode 100644 res/maven/tomcat-embed-el.pom create mode 100644 res/maven/tomcat-embed-jasper.pom create mode 100644 res/maven/tomcat-embed-programmatic.pom create mode 100644 res/maven/tomcat-embed-websocket.pom create mode 100644 res/maven/tomcat-i18n-cs.pom create mode 100644 res/maven/tomcat-i18n-de.pom create mode 100644 res/maven/tomcat-i18n-es.pom create mode 100644 res/maven/tomcat-i18n-fr.pom create mode 100644 res/maven/tomcat-i18n-ja.pom create mode 100644 res/maven/tomcat-i18n-ko.pom create mode 100644 res/maven/tomcat-i18n-pt-BR.pom create mode 100644 res/maven/tomcat-i18n-ru.pom create mode 100644 res/maven/tomcat-i18n-zh-CN.pom create mode 100644 res/maven/tomcat-jasper-el.pom create mode 100644 res/maven/tomcat-jasper.pom create mode 100644 res/maven/tomcat-jaspic-api.pom create mode 100644 res/maven/tomcat-jdbc.pom create mode 100644 res/maven/tomcat-jni.pom create mode 100644 res/maven/tomcat-jsp-api.pom create mode 100644 res/maven/tomcat-juli.pom create mode 100644 res/maven/tomcat-servlet-api.pom create mode 100644 res/maven/tomcat-ssi.pom create mode 100644 res/maven/tomcat-storeconfig.pom create mode 100644 res/maven/tomcat-tribes.pom create mode 100644 res/maven/tomcat-util-scan.pom create mode 100644 res/maven/tomcat-util.pom create mode 100644 res/maven/tomcat-websocket-api.pom create mode 100644 res/maven/tomcat-websocket-client-api.pom create mode 100644 res/maven/tomcat-websocket.pom create mode 100644 res/maven/tomcat.pom create mode 100644 res/openssl/README.md create mode 100755 res/openssl/addlicense.sh create mode 100644 res/openssl/license.header create mode 100644 res/openssl/openssl-tomcat.conf create mode 100644 res/openssl/openssl.h create mode 100644 res/rat/rat-excludes.txt create mode 100755 res/scripts/check-mime.pl create mode 100644 res/spotbugs/filter-false-positives.xml create mode 100644 res/welcome.bin.html create mode 100644 res/welcome.main.html create mode 100644 test/META-INF/services/jakarta.servlet.ServletContainerInitializer create mode 100644 test/conf/TesterRewriteMapB.txt create mode 100644 test/conf/TesterRewriteMapC.txt create mode 100644 test/conf/jaspic-test-01.xml create mode 100644 test/conf/jaspic-test-02.xml create mode 100644 test/conf/jaspic-test-04.xml create mode 100644 test/deployment/broken.war create mode 100644 test/deployment/context.jar create mode 100644 test/deployment/context.war create mode 100644 test/deployment/context.xml create mode 100644 test/deployment/contextCopyXMLFalse.war create mode 100644 test/deployment/contextCopyXMLTrue.war create mode 100644 test/deployment/contextUnpackWARFalse.war create mode 100644 test/deployment/contextUnpackWARTrue.war create mode 100644 test/deployment/dir with spaces/context.jar create mode 100644 test/deployment/dir with spaces/context.war create mode 100644 test/deployment/dirContext/META-INF/context.xml create mode 100644 test/deployment/dirContext/index.html create mode 100644 test/deployment/dirNoContext/index.html create mode 100644 test/deployment/noContext.war create mode 100644 test/jakarta/el/TestArrayELResolver.java create mode 100644 test/jakarta/el/TestBeanELResolver.java create mode 100644 test/jakarta/el/TestBeanELResolverVarargsInvocation.java create mode 100644 test/jakarta/el/TestBeanNameELResolver.java create mode 100644 test/jakarta/el/TestCompositeELResolver.java create mode 100644 test/jakarta/el/TestELContext.java create mode 100644 test/jakarta/el/TestELProcessor.java create mode 100644 test/jakarta/el/TestELResolver.java create mode 100644 test/jakarta/el/TestEvaluationListener.java create mode 100644 test/jakarta/el/TestImportHandler.java create mode 100644 test/jakarta/el/TestImportHandlerStandardPackages.java create mode 100644 test/jakarta/el/TestListELResolver.java create mode 100644 test/jakarta/el/TestMapELResolver.java create mode 100644 test/jakarta/el/TestMethodReference.java create mode 100644 test/jakarta/el/TestResourceBundleELResolver.java create mode 100644 test/jakarta/el/TestStaticFieldELResolver.java create mode 100644 test/jakarta/el/TestUtil.java create mode 100644 test/jakarta/el/TesterBean.java create mode 100644 test/jakarta/el/TesterBeanNameResolver.java create mode 100644 test/jakarta/el/TesterClass.java create mode 100644 test/jakarta/el/TesterCompositeELResolverPerformance.java create mode 100644 test/jakarta/el/TesterELContext.java create mode 100644 test/jakarta/el/TesterELResolverOne.java create mode 100644 test/jakarta/el/TesterELResolverTwo.java create mode 100644 test/jakarta/el/TesterEvaluationListener.java create mode 100644 test/jakarta/el/TesterFunctions.java create mode 100644 test/jakarta/el/TesterImportHandlerPerformance.java create mode 100644 test/jakarta/servlet/TestSessionCookieConfig.java create mode 100644 test/jakarta/servlet/annotation/TestServletSecurity.java create mode 100644 test/jakarta/servlet/annotation/TestServletSecurityMappings.java create mode 100644 test/jakarta/servlet/http/HttpServletDoHeadBaseTest.java create mode 100644 test/jakarta/servlet/http/TestCookie.java create mode 100644 test/jakarta/servlet/http/TestCookieRFC6265Validator.java create mode 100644 test/jakarta/servlet/http/TestHttpServlet.java create mode 100644 test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite0.java create mode 100644 test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1.java create mode 100644 test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1023.java create mode 100644 test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1024.java create mode 100644 test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1025.java create mode 100644 test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite511.java create mode 100644 test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite512.java create mode 100644 test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite513.java create mode 100644 test/jakarta/servlet/http/TestHttpServletResponseSendError.java create mode 100644 test/jakarta/servlet/http/TesterHttpServletPerformance.java create mode 100644 test/jakarta/servlet/jsp/TestPageContext.java create mode 100644 test/jakarta/servlet/jsp/TesterPageContext.java create mode 100644 test/jakarta/servlet/jsp/el/TestImportELResolver.java create mode 100644 test/jakarta/servlet/jsp/el/TestScopedAttributeELResolver.java create mode 100644 test/jakarta/servlet/jsp/el/TesterScopedAttributeELResolverPerformance.java create mode 100644 test/jakarta/servlet/resources/TestSchemaValidation.java create mode 100644 test/jakarta/websocket/TesterContainerProviderPerformance.java create mode 100644 test/org/apache/catalina/ant/TestDeployTask.java create mode 100644 test/org/apache/catalina/authenticator/ResponseDescriptor.java create mode 100644 test/org/apache/catalina/authenticator/TestAuthInfoResponseHeaders.java create mode 100644 test/org/apache/catalina/authenticator/TestAuthenticatorBaseCorsPreflight.java create mode 100644 test/org/apache/catalina/authenticator/TestBasicAuthParser.java create mode 100644 test/org/apache/catalina/authenticator/TestDigestAuthenticator.java create mode 100644 test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java create mode 100644 test/org/apache/catalina/authenticator/TestFormAuthenticatorA.java create mode 100644 test/org/apache/catalina/authenticator/TestFormAuthenticatorB.java create mode 100644 test/org/apache/catalina/authenticator/TestFormAuthenticatorC.java create mode 100644 test/org/apache/catalina/authenticator/TestJaspicCallbackHandlerInAuthenticator.java create mode 100644 test/org/apache/catalina/authenticator/TestNonLoginAndBasicAuthenticator.java create mode 100644 test/org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java create mode 100644 test/org/apache/catalina/authenticator/TestSSOnonLoginAndDigestAuthenticator.java create mode 100644 test/org/apache/catalina/authenticator/TesterCallbackHandlerImpl.java create mode 100644 test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java create mode 100644 test/org/apache/catalina/authenticator/jaspic/TestAuthConfigFactoryImpl.java create mode 100644 test/org/apache/catalina/authenticator/jaspic/TestPersistentProviderRegistrations.java create mode 100644 test/org/apache/catalina/authenticator/jaspic/TestSimpleServerAuthConfig.java create mode 100644 test/org/apache/catalina/authenticator/jaspic/TesterMessageInfo.java create mode 100644 test/org/apache/catalina/authenticator/jaspic/TesterServerAuthModuleA.java create mode 100644 test/org/apache/catalina/connector/TestClientReadTimeout.java create mode 100644 test/org/apache/catalina/connector/TestConnector.java create mode 100644 test/org/apache/catalina/connector/TestCoyoteAdapter.java create mode 100644 test/org/apache/catalina/connector/TestCoyoteAdapterCanonicalization.java create mode 100644 test/org/apache/catalina/connector/TestCoyoteAdapterRequestFuzzing.java create mode 100644 test/org/apache/catalina/connector/TestCoyoteInputStream.java create mode 100644 test/org/apache/catalina/connector/TestCoyoteOutputStream.java create mode 100644 test/org/apache/catalina/connector/TestInputBuffer.java create mode 100644 test/org/apache/catalina/connector/TestKeepAliveCount.java create mode 100644 test/org/apache/catalina/connector/TestMaxConnections.java create mode 100644 test/org/apache/catalina/connector/TestOutputBuffer.java create mode 100644 test/org/apache/catalina/connector/TestRequest.java create mode 100644 test/org/apache/catalina/connector/TestResponse.java create mode 100644 test/org/apache/catalina/connector/TestResponsePerformance.java create mode 100644 test/org/apache/catalina/connector/TestSendFile.java create mode 100644 test/org/apache/catalina/connector/TesterRequestPerformance.java create mode 100644 test/org/apache/catalina/connector/test_content.txt create mode 100644 test/org/apache/catalina/core/TestApplicationContext.java create mode 100644 test/org/apache/catalina/core/TestApplicationContextFacadeSecurityManager.java create mode 100644 test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java create mode 100644 test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherB.java create mode 100644 test/org/apache/catalina/core/TestApplicationContextStripPathParams.java create mode 100644 test/org/apache/catalina/core/TestApplicationFilterConfig.java create mode 100644 test/org/apache/catalina/core/TestApplicationHttpRequest.java create mode 100644 test/org/apache/catalina/core/TestApplicationMapping.java create mode 100644 test/org/apache/catalina/core/TestApplicationPushBuilder.java create mode 100644 test/org/apache/catalina/core/TestApplicationSessionCookieConfig.java create mode 100644 test/org/apache/catalina/core/TestAsyncContextImpl.java create mode 100644 test/org/apache/catalina/core/TestAsyncContextImplDispatch.java create mode 100644 test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java create mode 100644 test/org/apache/catalina/core/TestAsyncContextStateChanges.java create mode 100644 test/org/apache/catalina/core/TestContextNamingInfoListener.java create mode 100644 test/org/apache/catalina/core/TestDefaultInstanceManager.java create mode 100644 test/org/apache/catalina/core/TestNamingContextListener.java create mode 100644 test/org/apache/catalina/core/TestPropertiesRoleMappingListener.java create mode 100644 test/org/apache/catalina/core/TestStandardContext.java create mode 100644 test/org/apache/catalina/core/TestStandardContextAliases.java create mode 100644 test/org/apache/catalina/core/TestStandardContextResources.java create mode 100644 test/org/apache/catalina/core/TestStandardContextValve.java create mode 100644 test/org/apache/catalina/core/TestStandardHostValve.java create mode 100644 test/org/apache/catalina/core/TestStandardService.java create mode 100644 test/org/apache/catalina/core/TestStandardWrapper.java create mode 100644 test/org/apache/catalina/core/TestSwallowAbortedUploads.java create mode 100644 test/org/apache/catalina/core/TesterApplicationHttpRequestPerformance.java create mode 100644 test/org/apache/catalina/core/TesterDefaultInstanceManagerPerformance.java create mode 100644 test/org/apache/catalina/core/TesterTldListener.java create mode 100644 test/org/apache/catalina/filters/TestAddCharSetFilter.java create mode 100644 test/org/apache/catalina/filters/TestCorsFilter.java create mode 100644 test/org/apache/catalina/filters/TestCsrfPreventionFilter.java create mode 100644 test/org/apache/catalina/filters/TestCsrfPreventionFilter2.java create mode 100644 test/org/apache/catalina/filters/TestExpiresFilter.java create mode 100644 test/org/apache/catalina/filters/TestRateLimitFilter.java create mode 100644 test/org/apache/catalina/filters/TestRemoteCIDRFilter.java create mode 100644 test/org/apache/catalina/filters/TestRemoteIpFilter.java create mode 100644 test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java create mode 100644 test/org/apache/catalina/filters/TestRestCsrfPreventionFilter2.java create mode 100644 test/org/apache/catalina/filters/TesterFilterChain.java create mode 100644 test/org/apache/catalina/filters/TesterFilterConfigs.java create mode 100644 test/org/apache/catalina/filters/TesterHttpServletRequest.java create mode 100644 test/org/apache/catalina/filters/TesterHttpServletResponse.java create mode 100644 test/org/apache/catalina/ha/context/TestReplicatedContext.java create mode 100644 test/org/apache/catalina/ha/session/TestDeltaRequest.java create mode 100644 test/org/apache/catalina/loader/EchoTag.java create mode 100644 test/org/apache/catalina/loader/MyAnnotatedServlet.java create mode 100644 test/org/apache/catalina/loader/TestVirtualContext.java create mode 100644 test/org/apache/catalina/loader/TestVirtualWebappLoader.java create mode 100644 test/org/apache/catalina/loader/TestWebappClassLoader.java create mode 100644 test/org/apache/catalina/loader/TestWebappClassLoaderExecutorMemoryLeak.java create mode 100644 test/org/apache/catalina/loader/TestWebappClassLoaderMemoryLeak.java create mode 100644 test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java create mode 100644 test/org/apache/catalina/loader/TesterNeverWeavedClass.java create mode 100644 test/org/apache/catalina/loader/TesterUnweavedClass.java create mode 100644 test/org/apache/catalina/loader/TesterWebappClassLoaderThreadLocalMemoryLeak.java create mode 100644 test/org/apache/catalina/manager/TestStatusTransformer.java create mode 100644 test/org/apache/catalina/mapper/TestMapper.java create mode 100644 test/org/apache/catalina/mapper/TestMapperListener.java create mode 100644 test/org/apache/catalina/mapper/TestMapperPerformance.java create mode 100644 test/org/apache/catalina/mapper/TestMapperWebapps.java create mode 100644 test/org/apache/catalina/mbeans/TestRegistration.java create mode 100644 test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java create mode 100644 test/org/apache/catalina/nonblocking/TesterAjpNonBlockingClient.java create mode 100644 test/org/apache/catalina/realm/TestGenericPrincipal.java create mode 100644 test/org/apache/catalina/realm/TestJNDIRealm.java create mode 100644 test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java create mode 100644 test/org/apache/catalina/realm/TestJNDIRealmConvertToHexEscape.java create mode 100644 test/org/apache/catalina/realm/TestJNDIRealmIntegration.java create mode 100644 test/org/apache/catalina/realm/TestMemoryRealm.java create mode 100644 test/org/apache/catalina/realm/TestMessageDigestCredentialHandler.java create mode 100644 test/org/apache/catalina/realm/TestRealmBase.java create mode 100644 test/org/apache/catalina/realm/TestSecretKeyCredentialHandler.java create mode 100644 test/org/apache/catalina/realm/TesterPrincipal.java create mode 100644 test/org/apache/catalina/realm/TesterPrincipalNonSerializable.java create mode 100644 test/org/apache/catalina/realm/TesterServletSecurity01.java create mode 100644 test/org/apache/catalina/security/TestSecurityClassLoad.java create mode 100644 test/org/apache/catalina/servlets/DefaultServletEncodingBaseTest.java create mode 100644 test/org/apache/catalina/servlets/ServletOptionsBaseTest.java create mode 100644 test/org/apache/catalina/servlets/TestCGIServletCmdLineArguments.java create mode 100644 test/org/apache/catalina/servlets/TestDefaultServlet.java create mode 100644 test/org/apache/catalina/servlets/TestDefaultServletEncodingPassThroughBom.java create mode 100644 test/org/apache/catalina/servlets/TestDefaultServletEncodingWithBom.java create mode 100644 test/org/apache/catalina/servlets/TestDefaultServletEncodingWithoutBom.java create mode 100644 test/org/apache/catalina/servlets/TestDefaultServletIfMatchRequests.java create mode 100644 test/org/apache/catalina/servlets/TestDefaultServletOptions.java create mode 100644 test/org/apache/catalina/servlets/TestDefaultServletPut.java create mode 100644 test/org/apache/catalina/servlets/TestDefaultServletRangeRequests.java create mode 100644 test/org/apache/catalina/servlets/TestWebdavServlet.java create mode 100644 test/org/apache/catalina/servlets/TestWebdavServletOptionCollection.java create mode 100644 test/org/apache/catalina/servlets/TestWebdavServletOptionsFile.java create mode 100644 test/org/apache/catalina/servlets/TestWebdavServletOptionsUnknown.java create mode 100644 test/org/apache/catalina/session/Benchmarks.java create mode 100644 test/org/apache/catalina/session/FileStoreTest.java create mode 100644 test/org/apache/catalina/session/TestPersistentManager.java create mode 100644 test/org/apache/catalina/session/TestPersistentManagerIntegration.java create mode 100644 test/org/apache/catalina/session/TestStandardSession.java create mode 100644 test/org/apache/catalina/session/TestStandardSessionIntegration.java create mode 100644 test/org/apache/catalina/session/TesterStore.java create mode 100644 test/org/apache/catalina/ssi/TestExpressionParseTree.java create mode 100644 test/org/apache/catalina/ssi/TestRegExpCapture.java create mode 100644 test/org/apache/catalina/startup/BytesStreamer.java create mode 100644 test/org/apache/catalina/startup/DuplicateMappingParamFilter.java create mode 100644 test/org/apache/catalina/startup/DuplicateMappingParamServlet.java create mode 100644 test/org/apache/catalina/startup/EmbeddedTomcat.java create mode 100644 test/org/apache/catalina/startup/ExpectationClient.java create mode 100644 test/org/apache/catalina/startup/FastNonSecureRandom.java create mode 100644 test/org/apache/catalina/startup/LoggingBaseTest.java create mode 100644 test/org/apache/catalina/startup/NoMappingParamServlet.java create mode 100644 test/org/apache/catalina/startup/ParamFilter.java create mode 100644 test/org/apache/catalina/startup/ParamServlet.java create mode 100644 test/org/apache/catalina/startup/SimpleHttpClient.java create mode 100644 test/org/apache/catalina/startup/TestBootstrap.java create mode 100644 test/org/apache/catalina/startup/TestContextConfig.java create mode 100644 test/org/apache/catalina/startup/TestContextConfigAnnotation.java create mode 100644 test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentA.java create mode 100644 test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentB.java create mode 100644 test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentC.java create mode 100644 test/org/apache/catalina/startup/TestListener.java create mode 100644 test/org/apache/catalina/startup/TestMultipartConfig.java create mode 100644 test/org/apache/catalina/startup/TestTomcat.java create mode 100644 test/org/apache/catalina/startup/TestTomcatClassLoader.java create mode 100644 test/org/apache/catalina/startup/TestTomcatNoServer.java create mode 100644 test/org/apache/catalina/startup/TestTomcatStandalone.java create mode 100644 test/org/apache/catalina/startup/TestWebappServiceLoader.java create mode 100644 test/org/apache/catalina/startup/TesterMapRealm.java create mode 100644 test/org/apache/catalina/startup/TesterServlet.java create mode 100644 test/org/apache/catalina/startup/TesterServletContainerInitializer1.java create mode 100644 test/org/apache/catalina/startup/TesterServletContainerInitializer2.java create mode 100644 test/org/apache/catalina/startup/TesterServletEncodeUrl.java create mode 100644 test/org/apache/catalina/startup/TesterServletWithAnnotations.java create mode 100644 test/org/apache/catalina/startup/TesterServletWithLifeCycleMethods.java create mode 100644 test/org/apache/catalina/startup/TomcatBaseTest.java create mode 100644 test/org/apache/catalina/startup/service-config.txt create mode 100644 test/org/apache/catalina/startup/web-1lifecyclecallback.xml create mode 100644 test/org/apache/catalina/startup/web-1ordering.xml create mode 100644 test/org/apache/catalina/startup/web-2lifecyclecallback.xml create mode 100644 test/org/apache/catalina/startup/web-2ordering.xml create mode 100644 test/org/apache/catalina/startup/web-fragment-1name.xml create mode 100644 test/org/apache/catalina/startup/web-fragment-1ordering.xml create mode 100644 test/org/apache/catalina/startup/web-fragment-2name.xml create mode 100644 test/org/apache/catalina/startup/web-fragment-2ordering.xml create mode 100644 test/org/apache/catalina/tribes/TesterMulticast.java create mode 100644 test/org/apache/catalina/tribes/TesterUtil.java create mode 100644 test/org/apache/catalina/tribes/demos/ChannelCreator.java create mode 100644 test/org/apache/catalina/tribes/demos/CoordinationDemo.java create mode 100644 test/org/apache/catalina/tribes/demos/EchoRpcTest.java create mode 100644 test/org/apache/catalina/tribes/demos/IntrospectionUtils.java create mode 100644 test/org/apache/catalina/tribes/demos/LoadTest.java create mode 100644 test/org/apache/catalina/tribes/demos/MapDemo.java create mode 100644 test/org/apache/catalina/tribes/demos/MembersWithProperties.java create mode 100644 test/org/apache/catalina/tribes/group/TestGroupChannelMemberArrival.java create mode 100644 test/org/apache/catalina/tribes/group/TestGroupChannelOptionFlag.java create mode 100644 test/org/apache/catalina/tribes/group/TestGroupChannelSenderConnections.java create mode 100644 test/org/apache/catalina/tribes/group/TestGroupChannelStartStop.java create mode 100644 test/org/apache/catalina/tribes/group/interceptors/EncryptionInterceptorBaseTest.java create mode 100644 test/org/apache/catalina/tribes/group/interceptors/TestDomainFilterInterceptor.java create mode 100644 test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java create mode 100644 test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorLargeHeap.java create mode 100644 test/org/apache/catalina/tribes/group/interceptors/TestGzipInterceptor.java create mode 100644 test/org/apache/catalina/tribes/group/interceptors/TestNonBlockingCoordinator.java create mode 100644 test/org/apache/catalina/tribes/group/interceptors/TestOrderInterceptor.java create mode 100644 test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetector.java create mode 100644 test/org/apache/catalina/tribes/io/TestChannelData.java create mode 100644 test/org/apache/catalina/tribes/io/TestXByteBuffer.java create mode 100644 test/org/apache/catalina/tribes/membership/TestMemberImplSerialization.java create mode 100644 test/org/apache/catalina/tribes/membership/TestMembership.java create mode 100644 test/org/apache/catalina/tribes/membership/cloud/TestKubernetesJson.java create mode 100644 test/org/apache/catalina/tribes/test/NioSenderTest.java create mode 100644 test/org/apache/catalina/tribes/test/TribesTestSuite.java create mode 100644 test/org/apache/catalina/tribes/test/channel/TestChannelConfig.java create mode 100644 test/org/apache/catalina/tribes/test/channel/TestDataIntegrity.java create mode 100644 test/org/apache/catalina/tribes/test/channel/TestMulticastPackages.java create mode 100644 test/org/apache/catalina/tribes/test/channel/TestRemoteProcessException.java create mode 100644 test/org/apache/catalina/tribes/test/channel/TestUdpPackages.java create mode 100644 test/org/apache/catalina/tribes/test/transport/SocketNioReceive.java create mode 100644 test/org/apache/catalina/tribes/test/transport/SocketNioSend.java create mode 100644 test/org/apache/catalina/tribes/test/transport/SocketNioValidateSend.java create mode 100644 test/org/apache/catalina/tribes/test/transport/SocketReceive.java create mode 100644 test/org/apache/catalina/tribes/test/transport/SocketSend.java create mode 100644 test/org/apache/catalina/tribes/test/transport/SocketTribesReceive.java create mode 100644 test/org/apache/catalina/tribes/test/transport/SocketValidateReceive.java create mode 100644 test/org/apache/catalina/users/DataSourceUserDatabaseTests.java create mode 100644 test/org/apache/catalina/users/MemoryUserDatabaseTests.java create mode 100644 test/org/apache/catalina/util/TestContextName.java create mode 100644 test/org/apache/catalina/util/TestContextNameExtractFromPath.java create mode 100644 test/org/apache/catalina/util/TestNetMask.java create mode 100644 test/org/apache/catalina/util/TestNetMaskSet.java create mode 100644 test/org/apache/catalina/util/TestParameterMap.java create mode 100644 test/org/apache/catalina/util/TestServerInfo.java create mode 100644 test/org/apache/catalina/util/TestTimeBucketCounter.java create mode 100644 test/org/apache/catalina/util/TestURLEncoder.java create mode 100644 test/org/apache/catalina/valves/Benchmarks.java create mode 100644 test/org/apache/catalina/valves/TestAbstractAccessLogValveEscape.java create mode 100644 test/org/apache/catalina/valves/TestAccessLogValve.java create mode 100644 test/org/apache/catalina/valves/TestAccessLogValveDateFormatCache.java create mode 100644 test/org/apache/catalina/valves/TestCrawlerSessionManagerValve.java create mode 100644 test/org/apache/catalina/valves/TestErrorReportValve.java create mode 100644 test/org/apache/catalina/valves/TestExtendedAccessLogValve.java create mode 100644 test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java create mode 100644 test/org/apache/catalina/valves/TestPatternTokenizer.java create mode 100644 test/org/apache/catalina/valves/TestPersistentValve.java create mode 100644 test/org/apache/catalina/valves/TestRemoteIpValve.java create mode 100644 test/org/apache/catalina/valves/TestRequestFilterValve.java create mode 100644 test/org/apache/catalina/valves/TestSSLValve.java create mode 100644 test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java create mode 100644 test/org/apache/catalina/valves/TesterAccessLogValve.java create mode 100644 test/org/apache/catalina/valves/rewrite/TestQuotedStringTokenizer.java create mode 100644 test/org/apache/catalina/valves/rewrite/TestResolverSSL.java create mode 100644 test/org/apache/catalina/valves/rewrite/TestRewriteValve.java create mode 100644 test/org/apache/catalina/valves/rewrite/TesterRewriteMapA.java create mode 100644 test/org/apache/catalina/webresources/AbstractTestFileResourceSet.java create mode 100644 test/org/apache/catalina/webresources/AbstractTestResourceSet.java create mode 100644 test/org/apache/catalina/webresources/AbstractTestResourceSetMount.java create mode 100644 test/org/apache/catalina/webresources/TestAbstractArchiveResource.java create mode 100644 test/org/apache/catalina/webresources/TestAbstractArchiveResourceSet.java create mode 100644 test/org/apache/catalina/webresources/TestCachedResource.java create mode 100644 test/org/apache/catalina/webresources/TestClasspathUrlStreamHandler.java create mode 100644 test/org/apache/catalina/webresources/TestDirResourceSet.java create mode 100644 test/org/apache/catalina/webresources/TestDirResourceSetInternal.java create mode 100644 test/org/apache/catalina/webresources/TestDirResourceSetMount.java create mode 100644 test/org/apache/catalina/webresources/TestDirResourceSetReadOnly.java create mode 100644 test/org/apache/catalina/webresources/TestDirResourceSetVirtual.java create mode 100644 test/org/apache/catalina/webresources/TestFileResource.java create mode 100644 test/org/apache/catalina/webresources/TestFileResourceSet.java create mode 100644 test/org/apache/catalina/webresources/TestFileResourceSetReadOnly.java create mode 100644 test/org/apache/catalina/webresources/TestFileResourceSetVirtual.java create mode 100644 test/org/apache/catalina/webresources/TestJarContents.java create mode 100644 test/org/apache/catalina/webresources/TestJarInputStreamWrapper.java create mode 100644 test/org/apache/catalina/webresources/TestJarResourceSet.java create mode 100644 test/org/apache/catalina/webresources/TestJarResourceSetInternal.java create mode 100644 test/org/apache/catalina/webresources/TestJarResourceSetMount.java create mode 100644 test/org/apache/catalina/webresources/TestJarWarResourceSet.java create mode 100644 test/org/apache/catalina/webresources/TestResourceJars.java create mode 100644 test/org/apache/catalina/webresources/TestStandardRoot.java create mode 100644 test/org/apache/catalina/webresources/TestTomcatURLStreamHandlerFactory.java create mode 100644 test/org/apache/catalina/webresources/TesterAbstractFileResourceSetPerformance.java create mode 100644 test/org/apache/catalina/webresources/TesterWebResourceRoot.java create mode 100644 test/org/apache/catalina/webresources/war/TestHandler.java create mode 100644 test/org/apache/catalina/webresources/war/TestHandlerIntegration.java create mode 100644 test/org/apache/catalina/webresources/war/TestWarURLConnection.java create mode 100644 test/org/apache/coyote/TestCompressionConfig.java create mode 100644 test/org/apache/coyote/TestIoTimeouts.java create mode 100644 test/org/apache/coyote/TestRequest.java create mode 100644 test/org/apache/coyote/TestResponse.java create mode 100644 test/org/apache/coyote/ajp/SimpleAjpClient.java create mode 100644 test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java create mode 100644 test/org/apache/coyote/ajp/TesterAjpMessage.java create mode 100644 test/org/apache/coyote/http11/TestHttp11InputBuffer.java create mode 100644 test/org/apache/coyote/http11/TestHttp11InputBufferCRLF.java create mode 100644 test/org/apache/coyote/http11/TestHttp11OutputBuffer.java create mode 100644 test/org/apache/coyote/http11/TestHttp11Processor.java create mode 100644 test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java create mode 100644 test/org/apache/coyote/http11/filters/TestGzipOutputFilter.java create mode 100644 test/org/apache/coyote/http11/filters/TesterOutputBuffer.java create mode 100644 test/org/apache/coyote/http11/upgrade/TestUpgrade.java create mode 100644 test/org/apache/coyote/http11/upgrade/TestUpgradeInternalHandler.java create mode 100644 test/org/apache/coyote/http2/Http2TestBase.java create mode 100644 test/org/apache/coyote/http2/TestAsync.java create mode 100644 test/org/apache/coyote/http2/TestAsyncError.java create mode 100644 test/org/apache/coyote/http2/TestAsyncFlush.java create mode 100644 test/org/apache/coyote/http2/TestAsyncTimeout.java create mode 100644 test/org/apache/coyote/http2/TestByteUtil.java create mode 100644 test/org/apache/coyote/http2/TestCancelledUpload.java create mode 100644 test/org/apache/coyote/http2/TestFlowControl.java create mode 100644 test/org/apache/coyote/http2/TestHpack.java create mode 100644 test/org/apache/coyote/http2/TestHttp2ConnectionTimeouts.java create mode 100644 test/org/apache/coyote/http2/TestHttp2InitialConnection.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Limits.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_3_2.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_3_5.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_4_1.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_4_2.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_4_3.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_5_1.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_5_2.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_5_5.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_1.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_2.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_3.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_4.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_5.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_6.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_7.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_8.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_6_9.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Section_8_1.java create mode 100644 test/org/apache/coyote/http2/TestHttp2Timeouts.java create mode 100644 test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java create mode 100644 test/org/apache/coyote/http2/TestHttpServlet.java create mode 100644 test/org/apache/coyote/http2/TestLargeUpload.java create mode 100644 test/org/apache/coyote/http2/TestRfc9218.java create mode 100644 test/org/apache/coyote/http2/TestStream.java create mode 100644 test/org/apache/coyote/http2/TestStreamProcessor.java create mode 100644 test/org/apache/coyote/http2/TestStreamQueryString.java create mode 100644 test/org/apache/coyote/http2/TesterHttp2Parser.java create mode 100644 test/org/apache/el/TestELEvaluation.java create mode 100644 test/org/apache/el/TestELInJsp.java create mode 100644 test/org/apache/el/TestExpressionFactory.java create mode 100644 test/org/apache/el/TestMethodExpressionImpl.java create mode 100644 test/org/apache/el/TestValueExpressionImpl.java create mode 100644 test/org/apache/el/TesterBeanA.java create mode 100644 test/org/apache/el/TesterBeanAA.java create mode 100644 test/org/apache/el/TesterBeanAAA.java create mode 100644 test/org/apache/el/TesterBeanB.java create mode 100644 test/org/apache/el/TesterBeanBB.java create mode 100644 test/org/apache/el/TesterBeanBBB.java create mode 100644 test/org/apache/el/TesterBeanC.java create mode 100644 test/org/apache/el/TesterBeanD.java create mode 100644 test/org/apache/el/TesterBeanEnum.java create mode 100644 test/org/apache/el/TesterBeanF.java create mode 100644 test/org/apache/el/TesterBeanG.java create mode 100644 test/org/apache/el/TesterBeanH.java create mode 100644 test/org/apache/el/TesterBeanI.java create mode 100644 test/org/apache/el/TesterBeanJ.java create mode 100644 test/org/apache/el/TesterEnum.java create mode 100644 test/org/apache/el/TesterFunctions.java create mode 100644 test/org/apache/el/lang/TestELArithmetic.java create mode 100644 test/org/apache/el/lang/TestELSupport.java create mode 100644 test/org/apache/el/lang/TesterBean.java create mode 100644 test/org/apache/el/lang/TesterType.java create mode 100644 test/org/apache/el/lang/TesterTypeEditorBase.java create mode 100644 test/org/apache/el/lang/TesterTypeEditorError.java create mode 100644 test/org/apache/el/lang/TesterTypeEditorNoError.java create mode 100644 test/org/apache/el/lang/TesterVariableMapperImpl.java create mode 100644 test/org/apache/el/parser/TestAstAnd.java create mode 100644 test/org/apache/el/parser/TestAstAssign.java create mode 100644 test/org/apache/el/parser/TestAstChoice.java create mode 100644 test/org/apache/el/parser/TestAstConcatenation.java create mode 100644 test/org/apache/el/parser/TestAstFloatingPoint.java create mode 100644 test/org/apache/el/parser/TestAstFunction.java create mode 100644 test/org/apache/el/parser/TestAstIdentifier.java create mode 100644 test/org/apache/el/parser/TestAstInteger.java create mode 100644 test/org/apache/el/parser/TestAstLambdaExpression.java create mode 100644 test/org/apache/el/parser/TestAstListData.java create mode 100644 test/org/apache/el/parser/TestAstMapData.java create mode 100644 test/org/apache/el/parser/TestAstNot.java create mode 100644 test/org/apache/el/parser/TestAstOr.java create mode 100644 test/org/apache/el/parser/TestAstSemicolon.java create mode 100644 test/org/apache/el/parser/TestAstSetData.java create mode 100644 test/org/apache/el/parser/TestELParser.java create mode 100644 test/org/apache/el/parser/TestELParserPerformance.java create mode 100644 test/org/apache/el/parser/TesterBeanA.java create mode 100644 test/org/apache/el/parser/TesterBeanB.java create mode 100644 test/org/apache/el/parser/TesterBeanC.java create mode 100644 test/org/apache/el/stream/TestCollectionOperations.java create mode 100644 test/org/apache/el/util/TestMessageFactory.java create mode 100644 test/org/apache/el/util/TestReflectionUtil.java create mode 100644 test/org/apache/el/util/TestStrings.properties create mode 100644 test/org/apache/el/util/Tester.java create mode 100644 test/org/apache/jasper/TestJspC.java create mode 100644 test/org/apache/jasper/TestJspCompilationContext.java create mode 100644 test/org/apache/jasper/compiler/Dumper.java create mode 100644 test/org/apache/jasper/compiler/TestAttributeParser.java create mode 100644 test/org/apache/jasper/compiler/TestCompiler.java create mode 100644 test/org/apache/jasper/compiler/TestELInterpreterFactory.java create mode 100644 test/org/apache/jasper/compiler/TestELParser.java create mode 100644 test/org/apache/jasper/compiler/TestEncodingDetector.java create mode 100644 test/org/apache/jasper/compiler/TestGenerator.java create mode 100644 test/org/apache/jasper/compiler/TestJspConfig.java create mode 100644 test/org/apache/jasper/compiler/TestJspDocumentParser.java create mode 100644 test/org/apache/jasper/compiler/TestJspReader.java create mode 100644 test/org/apache/jasper/compiler/TestJspUtil.java create mode 100644 test/org/apache/jasper/compiler/TestJspUtilMakeJavaPackage.java create mode 100644 test/org/apache/jasper/compiler/TestNode.java create mode 100644 test/org/apache/jasper/compiler/TestNodeIntegration.java create mode 100644 test/org/apache/jasper/compiler/TestParser.java create mode 100644 test/org/apache/jasper/compiler/TestParserNoStrictWhitespace.java create mode 100644 test/org/apache/jasper/compiler/TestScriptingVariabler.java create mode 100644 test/org/apache/jasper/compiler/TestSmapStratum.java create mode 100644 test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java create mode 100644 test/org/apache/jasper/compiler/TestTagPluginManager.java create mode 100644 test/org/apache/jasper/compiler/TestValidator.java create mode 100644 test/org/apache/jasper/compiler/TesterTag.java create mode 100644 test/org/apache/jasper/compiler/TesterTagPlugin.java create mode 100644 test/org/apache/jasper/compiler/TesterValidator.java create mode 100644 test/org/apache/jasper/el/TestJasperELResolver.java create mode 100644 test/org/apache/jasper/optimizations/TestELInterpreterTagSetters.java create mode 100644 test/org/apache/jasper/optimizations/TestStringInterpreterTagSetters.java create mode 100644 test/org/apache/jasper/runtime/TestCustomHttpJspPage.java create mode 100644 test/org/apache/jasper/runtime/TestJspContextWrapper.java create mode 100644 test/org/apache/jasper/runtime/TestJspRuntimeLibrary.java create mode 100644 test/org/apache/jasper/runtime/TestJspWriterImpl.java create mode 100644 test/org/apache/jasper/runtime/TestPageContextImpl.java create mode 100644 test/org/apache/jasper/runtime/TesterBean.java create mode 100644 test/org/apache/jasper/runtime/TesterHttpJspBase.java create mode 100644 test/org/apache/jasper/runtime/TesterTagHandlerPoolPerformance.java create mode 100644 test/org/apache/jasper/runtime/TesterTypeA.java create mode 100644 test/org/apache/jasper/runtime/TesterTypeAEditor.java create mode 100644 test/org/apache/jasper/runtime/TesterTypeB.java create mode 100644 test/org/apache/jasper/servlet/TestJasperInitializer.java create mode 100644 test/org/apache/jasper/servlet/TestJspCServletContext.java create mode 100644 test/org/apache/jasper/servlet/TestJspServlet.java create mode 100644 test/org/apache/jasper/servlet/TestTldScanner.java create mode 100644 test/org/apache/jasper/tagplugins/jstl/core/AbstractTestTag.java create mode 100644 test/org/apache/jasper/tagplugins/jstl/core/TestForEach.java create mode 100644 test/org/apache/jasper/tagplugins/jstl/core/TestOut.java create mode 100644 test/org/apache/jasper/tagplugins/jstl/core/TestSet.java create mode 100644 test/org/apache/jasper/util/TestFastRemovalDequeue.java create mode 100644 test/org/apache/juli/TestAsyncFileHandlerOverflow.java create mode 100644 test/org/apache/juli/TestClassLoaderLogManager.java create mode 100644 test/org/apache/juli/TestDateFormatCache.java create mode 100644 test/org/apache/juli/TestFileHandler.java create mode 100644 test/org/apache/juli/TestFileHandlerNonRotatable.java create mode 100644 test/org/apache/juli/TestOneLineFormatterPerformance.java create mode 100644 test/org/apache/juli/TestThreadNameCache.java create mode 100644 test/org/apache/juli/TesterOneLineFormatterMillisPerformance.java create mode 100644 test/org/apache/juli/logging-non-rotatable.properties create mode 100644 test/org/apache/naming/TestEnvEntry.java create mode 100644 test/org/apache/naming/TestNamingContext.java create mode 100644 test/org/apache/naming/TesterEnvEntry.java create mode 100644 test/org/apache/naming/TesterInjectionServlet.java create mode 100644 test/org/apache/naming/factory/TestBeanFactory.java create mode 100644 test/org/apache/naming/factory/TesterBean.java create mode 100644 test/org/apache/naming/resources/TestNamingContext.java create mode 100644 test/org/apache/naming/resources/TestWarDirContext.java create mode 100644 test/org/apache/naming/resources/TesterFactory.java create mode 100644 test/org/apache/naming/resources/TesterObject.java create mode 100644 test/org/apache/tomcat/buildutil/translate/TestFixedStrings.java create mode 100644 test/org/apache/tomcat/buildutil/translate/TestUtils.java create mode 100644 test/org/apache/tomcat/jni/TesterSSL.java create mode 100644 test/org/apache/tomcat/unittest/TesterBug66582.java create mode 100644 test/org/apache/tomcat/unittest/TesterContext.java create mode 100644 test/org/apache/tomcat/unittest/TesterCounter.java create mode 100644 test/org/apache/tomcat/unittest/TesterData.java create mode 100644 test/org/apache/tomcat/unittest/TesterHost.java create mode 100644 test/org/apache/tomcat/unittest/TesterLeakingServlet1.java create mode 100644 test/org/apache/tomcat/unittest/TesterLeakingServlet2.java create mode 100644 test/org/apache/tomcat/unittest/TesterLogValidationFilter.java create mode 100644 test/org/apache/tomcat/unittest/TesterRequest.java create mode 100644 test/org/apache/tomcat/unittest/TesterResponse.java create mode 100644 test/org/apache/tomcat/unittest/TesterServletContext.java create mode 100644 test/org/apache/tomcat/unittest/TesterSessionCookieConfig.java create mode 100644 test/org/apache/tomcat/unittest/TesterThreadScopedHolder.java create mode 100644 test/org/apache/tomcat/unittest/TesterThreadedPerformance.java create mode 100644 test/org/apache/tomcat/unittest/tags/Bug53545.java create mode 100644 test/org/apache/tomcat/util/TestIntrospectionUtils.java create mode 100644 test/org/apache/tomcat/util/bcel/TesterPerformance.java create mode 100644 test/org/apache/tomcat/util/buf/TestAscii.java create mode 100644 test/org/apache/tomcat/util/buf/TestB2CConverter.java create mode 100644 test/org/apache/tomcat/util/buf/TestByteChunk.java create mode 100644 test/org/apache/tomcat/util/buf/TestByteChunkLargeHeap.java create mode 100644 test/org/apache/tomcat/util/buf/TestCharChunk.java create mode 100644 test/org/apache/tomcat/util/buf/TestCharChunkLargeHeap.java create mode 100644 test/org/apache/tomcat/util/buf/TestCharsetCache.java create mode 100644 test/org/apache/tomcat/util/buf/TestCharsetCachePerformance.java create mode 100644 test/org/apache/tomcat/util/buf/TestCharsetUtil.java create mode 100644 test/org/apache/tomcat/util/buf/TestHexUtils.java create mode 100644 test/org/apache/tomcat/util/buf/TestMessageBytes.java create mode 100644 test/org/apache/tomcat/util/buf/TestMessageBytesConversion.java create mode 100644 test/org/apache/tomcat/util/buf/TestMessageBytesIntegration.java create mode 100644 test/org/apache/tomcat/util/buf/TestMessageBytesPerformance.java create mode 100644 test/org/apache/tomcat/util/buf/TestStringCache.java create mode 100644 test/org/apache/tomcat/util/buf/TestStringUtils.java create mode 100644 test/org/apache/tomcat/util/buf/TestUDecoder.java create mode 100644 test/org/apache/tomcat/util/buf/TestUEncoder.java create mode 100644 test/org/apache/tomcat/util/buf/TestUriUtil.java create mode 100644 test/org/apache/tomcat/util/buf/TestUriUtil24.java create mode 100644 test/org/apache/tomcat/util/buf/TestUriUtil26.java create mode 100644 test/org/apache/tomcat/util/buf/TestUriUtil2A.java create mode 100644 test/org/apache/tomcat/util/buf/TestUriUtil40.java create mode 100644 test/org/apache/tomcat/util/buf/TestUriUtilIsAbsoluteURI.java create mode 100644 test/org/apache/tomcat/util/buf/TestUtf8.java create mode 100644 test/org/apache/tomcat/util/buf/TesterUriUtilBase.java create mode 100644 test/org/apache/tomcat/util/collections/TestCaseInsensitiveKeyMap.java create mode 100644 test/org/apache/tomcat/util/collections/TestSynchronizedQueue.java create mode 100644 test/org/apache/tomcat/util/collections/TestSynchronizedStack.java create mode 100644 test/org/apache/tomcat/util/collections/TesterPerformanceSynchronizedQueue.java create mode 100644 test/org/apache/tomcat/util/collections/TesterPerformanceSynchronizedStack.java create mode 100644 test/org/apache/tomcat/util/descriptor/TestLocalResolver.java create mode 100644 test/org/apache/tomcat/util/descriptor/tld/TestImplicitTldParser.java create mode 100644 test/org/apache/tomcat/util/descriptor/tld/TestTldParser.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestFilterDef.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestJspConfigDescriptorImpl.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestJspPropertyGroup.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestJspPropertyGroupDescriptorImpl.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestSecurityConstraint.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestServletDef.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestWebRuleSet.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestWebXml.java create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestWebXmlOrdering.java create mode 100644 test/org/apache/tomcat/util/file/TestConfigFileLoader.java create mode 100644 test/org/apache/tomcat/util/http/TestConcurrentDateFormat.java create mode 100644 test/org/apache/tomcat/util/http/TestCookieParsing.java create mode 100644 test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java create mode 100644 test/org/apache/tomcat/util/http/TestCookieProcessorGenerationHttp.java create mode 100644 test/org/apache/tomcat/util/http/TestCookies.java create mode 100644 test/org/apache/tomcat/util/http/TestHeaderUtiltoPrintableString.java create mode 100644 test/org/apache/tomcat/util/http/TestMimeHeaders.java create mode 100644 test/org/apache/tomcat/util/http/TestMimeHeadersIntegration.java create mode 100644 test/org/apache/tomcat/util/http/TestParameters.java create mode 100644 test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java create mode 100644 test/org/apache/tomcat/util/http/TestRequestUtilSameOrigin.java create mode 100644 test/org/apache/tomcat/util/http/TestResponseUtil.java create mode 100644 test/org/apache/tomcat/util/http/TestSameSiteCookies.java create mode 100644 test/org/apache/tomcat/util/http/TesterCookiesPerformance.java create mode 100644 test/org/apache/tomcat/util/http/TesterFastHttpDateFormatPerformance.java create mode 100644 test/org/apache/tomcat/util/http/TesterParametersPerformance.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestAcceptLanguage.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestAuthorizationDigest.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestHttpParser.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestHttpParserHost.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestMediaType.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestPriority.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestRanges.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestTokenList.java create mode 100644 test/org/apache/tomcat/util/http/parser/TestUpgrade.java create mode 100644 test/org/apache/tomcat/util/http/parser/TesterHostPerformance.java create mode 100644 test/org/apache/tomcat/util/http/parser/TesterHttpWgStructuredField.java create mode 100644 test/org/apache/tomcat/util/http/parser/TesterParserPerformance.java create mode 100644 test/org/apache/tomcat/util/json/TestJSONFilter.java create mode 100644 test/org/apache/tomcat/util/net/TestClientCert.java create mode 100644 test/org/apache/tomcat/util/net/TestClientCertTls13.java create mode 100644 test/org/apache/tomcat/util/net/TestCustomSsl.java create mode 100644 test/org/apache/tomcat/util/net/TestCustomSslTrustManager.java create mode 100644 test/org/apache/tomcat/util/net/TestIPv6Utils.java create mode 100644 test/org/apache/tomcat/util/net/TestSSLHostConfig.java create mode 100644 test/org/apache/tomcat/util/net/TestSSLHostConfigCompat.java create mode 100644 test/org/apache/tomcat/util/net/TestSSLHostConfigIntegration.java create mode 100644 test/org/apache/tomcat/util/net/TestSocketBufferHandler.java create mode 100644 test/org/apache/tomcat/util/net/TestSsl.java create mode 100644 test/org/apache/tomcat/util/net/TestTLSClientHelloExtractor.java create mode 100644 test/org/apache/tomcat/util/net/TestXxxEndpoint.java create mode 100644 test/org/apache/tomcat/util/net/TesterSupport.java create mode 100644 test/org/apache/tomcat/util/net/ca-cert.pem create mode 100644 test/org/apache/tomcat/util/net/ca.jks create mode 100644 test/org/apache/tomcat/util/net/jsse/TestPEMFile.java create mode 100644 test/org/apache/tomcat/util/net/jsse/TesterBug50640SslImpl.java create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes256.pem create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-des-cbc.pem create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-des-ede3-cbc.pem create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha1default-des-ede3-cbc.pem create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-aes-128-cbc.pem create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-aes-256-cbc.pem create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-des-ede3-cbc.pem create mode 100644 test/org/apache/tomcat/util/net/jsse/key-password create mode 100644 test/org/apache/tomcat/util/net/jsse/key-pkcs1.pem create mode 100644 test/org/apache/tomcat/util/net/key-password create mode 100644 test/org/apache/tomcat/util/net/keystore-info.txt create mode 100644 test/org/apache/tomcat/util/net/keystore-password create mode 100644 test/org/apache/tomcat/util/net/localhost-ec-cert.pem create mode 100644 test/org/apache/tomcat/util/net/localhost-ec-key.pem create mode 100644 test/org/apache/tomcat/util/net/localhost-ec.jks create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa-cert.pem create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa-copy1.jks create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa-key.pem create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa.jks create mode 100644 test/org/apache/tomcat/util/net/openssl/TestOpenSSLConf.java create mode 100644 test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java create mode 100644 test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java create mode 100644 test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParserOnly.java create mode 100644 test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java create mode 100644 test/org/apache/tomcat/util/net/user1.jks create mode 100644 test/org/apache/tomcat/util/res/TestStringManager.java create mode 100644 test/org/apache/tomcat/util/scan/FooSCI.java create mode 100644 test/org/apache/tomcat/util/scan/TestAbstractInputStreamJar.java create mode 100644 test/org/apache/tomcat/util/scan/TestClassParser.java create mode 100644 test/org/apache/tomcat/util/scan/TestJarScanner.java create mode 100644 test/org/apache/tomcat/util/scan/TestStandardJarScanner.java create mode 100644 test/org/apache/tomcat/util/security/SecurityManagerBaseTest.java create mode 100644 test/org/apache/tomcat/util/security/TestConcurrentMessageDigest.java create mode 100644 test/org/apache/tomcat/util/security/TestEscape.java create mode 100644 test/org/apache/tomcat/util/threads/TestLimitLatch.java create mode 100644 test/org/apache/tomcat/websocket/TestPerMessageDeflate.java create mode 100644 test/org/apache/tomcat/websocket/TestUtil.java create mode 100644 test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java create mode 100644 test/org/apache/tomcat/websocket/TestWebSocketFrameClientSSL.java create mode 100644 test/org/apache/tomcat/websocket/TestWsFrame.java create mode 100644 test/org/apache/tomcat/websocket/TestWsPingPongMessages.java create mode 100644 test/org/apache/tomcat/websocket/TestWsRemoteEndpoint.java create mode 100644 test/org/apache/tomcat/websocket/TestWsSession.java create mode 100644 test/org/apache/tomcat/websocket/TestWsSessionSuspendResume.java create mode 100644 test/org/apache/tomcat/websocket/TestWsSubprotocols.java create mode 100644 test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java create mode 100644 test/org/apache/tomcat/websocket/TestWsWebSocketContainerGetOpenSessions.java create mode 100644 test/org/apache/tomcat/websocket/TestWsWebSocketContainerSSL.java create mode 100644 test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpiryContainerClient.java create mode 100644 test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpiryContainerServer.java create mode 100644 test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpirySession.java create mode 100644 test/org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutClient.java create mode 100644 test/org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutServer.java create mode 100644 test/org/apache/tomcat/websocket/TesterAsyncTiming.java create mode 100644 test/org/apache/tomcat/websocket/TesterBlockWebSocketSCI.java create mode 100644 test/org/apache/tomcat/websocket/TesterConnectionLimitPerformance.java create mode 100644 test/org/apache/tomcat/websocket/TesterEchoServer.java create mode 100644 test/org/apache/tomcat/websocket/TesterFirehoseServer.java create mode 100644 test/org/apache/tomcat/websocket/TesterMessageCountClient.java create mode 100644 test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java create mode 100644 test/org/apache/tomcat/websocket/TesterWsClientAutobahn.java create mode 100644 test/org/apache/tomcat/websocket/TesterWsWebSocketContainerWithProxy.java create mode 100644 test/org/apache/tomcat/websocket/WebSocketBaseTest.java create mode 100644 test/org/apache/tomcat/websocket/WsWebSocketContainerBaseTest.java create mode 100644 test/org/apache/tomcat/websocket/pojo/TestEncodingDecoding.java create mode 100644 test/org/apache/tomcat/websocket/pojo/TestPojoEndpointBase.java create mode 100644 test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java create mode 100644 test/org/apache/tomcat/websocket/pojo/TesterUtil.java create mode 100644 test/org/apache/tomcat/websocket/server/TestAsyncMessagesPerformance.java create mode 100644 test/org/apache/tomcat/websocket/server/TestClassLoader.java create mode 100644 test/org/apache/tomcat/websocket/server/TestClose.java create mode 100644 test/org/apache/tomcat/websocket/server/TestCloseBug58624.java create mode 100644 test/org/apache/tomcat/websocket/server/TestKeyHeader.java create mode 100644 test/org/apache/tomcat/websocket/server/TestShutdown.java create mode 100644 test/org/apache/tomcat/websocket/server/TestSlowClient.java create mode 100644 test/org/apache/tomcat/websocket/server/TestUriTemplate.java create mode 100644 test/org/apache/tomcat/websocket/server/TestWsRemoteEndpointImplServerDeadlock.java create mode 100644 test/org/apache/tomcat/websocket/server/TestWsServerContainer.java create mode 100644 test/org/apache/tomcat/websocket/server/TesterEndpointConfig.java create mode 100644 test/org/apache/tomcat/websocket/server/TesterWsClient.java create mode 100644 test/org/apache/tomcat/websocket/server/TesterWsRemoteEndpointImplServer.java create mode 100644 test/tld/implicit-bad.tld create mode 100644 test/tld/implicit-good.tld create mode 100644 test/tld/listener.tld create mode 100644 test/tld/tags11.tld create mode 100644 test/tld/tags12.tld create mode 100644 test/tld/tags20.tld create mode 100644 test/tld/tags21.tld create mode 100644 test/tld/test.tld create mode 100644 test/util/TestCookieFilter.java create mode 100644 test/util/a/Foo.java create mode 100644 test/util/b/Foo.java create mode 100644 test/webapp-2.2/WEB-INF/tags11.tld create mode 100644 test/webapp-2.2/WEB-INF/tags12.tld create mode 100644 test/webapp-2.2/WEB-INF/tags20.tld create mode 100644 test/webapp-2.2/WEB-INF/tags21.tld create mode 100644 test/webapp-2.2/WEB-INF/web.xml create mode 100644 test/webapp-2.2/el-as-literal.jsp create mode 100644 test/webapp-2.2/tld-versions.jsp create mode 100644 test/webapp-2.3/WEB-INF/tags11.tld create mode 100644 test/webapp-2.3/WEB-INF/tags12.tld create mode 100644 test/webapp-2.3/WEB-INF/tags20.tld create mode 100644 test/webapp-2.3/WEB-INF/tags21.tld create mode 100644 test/webapp-2.3/WEB-INF/web.xml create mode 100644 test/webapp-2.3/el-as-literal.jsp create mode 100644 test/webapp-2.3/tld-versions.jsp create mode 100644 test/webapp-2.4/WEB-INF/tags11.tld create mode 100644 test/webapp-2.4/WEB-INF/tags12.tld create mode 100644 test/webapp-2.4/WEB-INF/tags20.tld create mode 100644 test/webapp-2.4/WEB-INF/tags21.tld create mode 100644 test/webapp-2.4/WEB-INF/web.xml create mode 100644 test/webapp-2.4/el-as-literal.jsp create mode 100644 test/webapp-2.4/tld-versions.jsp create mode 100644 test/webapp-2.5/WEB-INF/tags11.tld create mode 100644 test/webapp-2.5/WEB-INF/tags12.tld create mode 100644 test/webapp-2.5/WEB-INF/tags20.tld create mode 100644 test/webapp-2.5/WEB-INF/tags21.tld create mode 100644 test/webapp-2.5/WEB-INF/web.xml create mode 100644 test/webapp-2.5/el-as-literal.jsp create mode 100644 test/webapp-2.5/tld-versions.jsp create mode 100644 test/webapp-3.0/WEB-INF/listener.tld create mode 100644 test/webapp-3.0/WEB-INF/tags11.tld create mode 100644 test/webapp-3.0/WEB-INF/tags12.tld create mode 100644 test/webapp-3.0/WEB-INF/tags20.tld create mode 100644 test/webapp-3.0/WEB-INF/tags21.tld create mode 100644 test/webapp-3.0/WEB-INF/web.xml create mode 100644 test/webapp-3.0/el-as-literal.jsp create mode 100644 test/webapp-3.0/tld-versions.jsp create mode 100644 test/webapp-3.1/WEB-INF/tags11.tld create mode 100644 test/webapp-3.1/WEB-INF/tags12.tld create mode 100644 test/webapp-3.1/WEB-INF/tags20.tld create mode 100644 test/webapp-3.1/WEB-INF/tags21.tld create mode 100644 test/webapp-3.1/WEB-INF/web.xml create mode 100644 test/webapp-3.1/el-as-literal.jsp create mode 100644 test/webapp-3.1/tld-versions.jsp create mode 100644 test/webapp-4.0/WEB-INF/tags11.tld create mode 100644 test/webapp-4.0/WEB-INF/tags12.tld create mode 100644 test/webapp-4.0/WEB-INF/tags20.tld create mode 100644 test/webapp-4.0/WEB-INF/tags21.tld create mode 100644 test/webapp-4.0/WEB-INF/web.xml create mode 100644 test/webapp-4.0/el-as-literal.jsp create mode 100644 test/webapp-4.0/tld-versions.jsp create mode 100644 test/webapp-5.0/WEB-INF/tags11.tld create mode 100644 test/webapp-5.0/WEB-INF/tags12.tld create mode 100644 test/webapp-5.0/WEB-INF/tags20.tld create mode 100644 test/webapp-5.0/WEB-INF/tags21.tld create mode 100644 test/webapp-5.0/WEB-INF/tags30.tld create mode 100644 test/webapp-5.0/WEB-INF/web.xml create mode 100644 test/webapp-5.0/el-as-literal.jsp create mode 100644 test/webapp-5.0/tld-versions.jsp create mode 100644 test/webapp-6.0/WEB-INF/tags11.tld create mode 100644 test/webapp-6.0/WEB-INF/tags12.tld create mode 100644 test/webapp-6.0/WEB-INF/tags20.tld create mode 100644 test/webapp-6.0/WEB-INF/tags21.tld create mode 100644 test/webapp-6.0/WEB-INF/tags30.tld create mode 100644 test/webapp-6.0/WEB-INF/tags31.tld create mode 100644 test/webapp-6.0/WEB-INF/web.xml create mode 100644 test/webapp-6.0/el-as-literal.jsp create mode 100644 test/webapp-6.0/tld-versions.jsp create mode 100644 test/webapp-fragments-empty-absolute-ordering/WEB-INF/lib/resources.jar create mode 100644 test/webapp-fragments-empty-absolute-ordering/WEB-INF/web.xml create mode 100644 test/webapp-fragments/'singlequote2.jsp create mode 100644 test/webapp-fragments/WEB-INF/classes/#Bug51584.txt create mode 100644 test/webapp-fragments/WEB-INF/classes/META-INF/resources/resourceG.jsp create mode 100644 test/webapp-fragments/WEB-INF/lib/resources.jar create mode 100644 test/webapp-fragments/WEB-INF/lib/resources2.jar create mode 100644 test/webapp-fragments/WEB-INF/web.xml create mode 100644 test/webapp-fragments/bug51396.jsp create mode 100644 test/webapp-fragments/folder/resourceC.jsp create mode 100644 test/webapp-fragments/folder/resourceE.jsp create mode 100644 test/webapp-fragments/jndi.jsp create mode 100644 test/webapp-fragments/resourceA.jsp create mode 100644 test/webapp-fragments/warDirContext.jsp create mode 100644 test/webapp-role-mapping/WEB-INF/classes/com/example/prefixed-role-mapping.properties create mode 100644 test/webapp-role-mapping/WEB-INF/classes/com/example/role-mapping.properties create mode 100644 test/webapp-role-mapping/WEB-INF/prefixed-role-mapping.properties create mode 100644 test/webapp-role-mapping/WEB-INF/role-mapping.properties create mode 100644 test/webapp-role-mapping/admin.txt create mode 100644 test/webapp-role-mapping/unmapped.txt create mode 100644 test/webapp-role-mapping/user.txt create mode 100644 test/webapp-sci/WEB-INF/classes/META-INF/services/jakarta.servlet.ServletContainerInitializer create mode 100644 test/webapp-servletsecurity-a/WEB-INF/web.xml create mode 100644 test/webapp-servletsecurity-b/WEB-INF/web.xml create mode 100644 test/webapp-servletsecurity-b/protected.jsp create mode 100644 test/webapp-servletsecurity-b/unprotected.jsp create mode 100644 test/webapp-virtual-library/target/WEB-INF/C.tld create mode 100644 test/webapp-virtual-library/target/WEB-INF/classes/META-INF/resources/rsrc/resourceE.properties create mode 100644 test/webapp-virtual-library/target/WEB-INF/classes/rsrc/resourceC.properties create mode 100644 test/webapp-virtual-webapp/src/main/lib/META-INF/B.tld create mode 100644 test/webapp-virtual-webapp/src/main/lib/rsrc/resourceD.properties create mode 100644 test/webapp-virtual-webapp/src/main/misc/resourceI.properties create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/A.tld create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/classes/rsrc/resourceA.properties create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/lib/rsrc.jar create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/web.xml create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResourceAsStream.jsp create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResourceUrlThenGetStream.jsp create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResources.jsp create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/contextGetRealPath.jsp create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/contextGetResource.jsp create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/contextGetResourcePaths.jsp create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/rsrc/resourceF.properties create mode 100644 test/webapp-virtual-webapp/src/main/webapp-a/testTlds.jsp create mode 100644 test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/D.tld create mode 100644 test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc-2/resourceK.properties create mode 100644 test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc/resourceG.properties create mode 100644 test/webapp-virtual-webapp/src/main/webapp-b/rsrc-2/resourceJ.properties create mode 100644 test/webapp-virtual-webapp/src/main/webapp-b/rsrc/resourceF.properties create mode 100644 test/webapp-virtual-webapp/src/main/webapp-b/rsrc/resourceH.properties create mode 100644 test/webapp-virtual-webapp/target/classes/rsrc/resourceB.properties create mode 100644 test/webapp/404.html create mode 100644 test/webapp/WEB-INF/bug53545.tld create mode 100644 test/webapp/WEB-INF/bugs.tld create mode 100644 test/webapp/WEB-INF/classes/META-INF/bug55807.tld create mode 100644 test/webapp/WEB-INF/classes/META-INF/bug64373.tld create mode 100644 test/webapp/WEB-INF/classes/META-INF/org.apache.jasper/tagPlugins.xml create mode 100644 test/webapp/WEB-INF/classes/META-INF/tags/bug64373.tag create mode 100644 test/webapp/WEB-INF/classes/org/apache/tomcat/Bug58096.class create mode 100644 test/webapp/WEB-INF/classes/org/apache/tomcat/Bug58096.java create mode 100644 test/webapp/WEB-INF/jsp/bug53574.jsp create mode 100644 test/webapp/WEB-INF/lib/test-lib.jar create mode 100644 test/webapp/WEB-INF/tag-setters.tld create mode 100644 test/webapp/WEB-INF/tags/bug42390.tag create mode 100644 test/webapp/WEB-INF/tags/bug43400.tag create mode 100644 test/webapp/WEB-INF/tags/bug48668.tagx create mode 100644 test/webapp/WEB-INF/tags/bug49297.tag create mode 100644 test/webapp/WEB-INF/tags/bug54012.tag create mode 100644 test/webapp/WEB-INF/tags/bug55198.tagx create mode 100644 test/webapp/WEB-INF/tags/bug56265.tagx create mode 100644 test/webapp/WEB-INF/tags/bug58178.tag create mode 100644 test/webapp/WEB-INF/tags/bug58178b.tag create mode 100644 test/webapp/WEB-INF/tags/bug62453.tag create mode 100644 test/webapp/WEB-INF/tags/bug65390.tag create mode 100644 test/webapp/WEB-INF/tags/circular01.tag create mode 100644 test/webapp/WEB-INF/tags/circular02.tag create mode 100644 test/webapp/WEB-INF/tags/dobody.tagx create mode 100644 test/webapp/WEB-INF/tags/echo-deferred-method.tag create mode 100644 test/webapp/WEB-INF/tags/echo-deferred.tag create mode 100644 test/webapp/WEB-INF/tags/echo-double.tag create mode 100644 test/webapp/WEB-INF/tags/echo-long.tag create mode 100644 test/webapp/WEB-INF/tags/echo-noel.tag create mode 100644 test/webapp/WEB-INF/tags/echo.tag create mode 100644 test/webapp/WEB-INF/tags/error-on-el-not-found-false.tag create mode 100644 test/webapp/WEB-INF/tags/error-on-el-not-found-true.tag create mode 100644 test/webapp/WEB-INF/tags/forward.tag create mode 100644 test/webapp/WEB-INF/tags/implicit.tld create mode 100644 test/webapp/WEB-INF/tags/invoke.tagx create mode 100644 test/webapp/WEB-INF/tags/jsp-root.tagx create mode 100644 test/webapp/WEB-INF/tags/no-jsp-root.tagx create mode 100644 test/webapp/WEB-INF/tags/setters.tag create mode 100644 test/webapp/WEB-INF/tags/variable-from-attr.tag create mode 100644 test/webapp/WEB-INF/tags/variable.tag create mode 100644 test/webapp/WEB-INF/test.tld create mode 100644 test/webapp/WEB-INF/web.xml create mode 100644 test/webapp/annotations.jsp create mode 100644 test/webapp/bug36923.jsp create mode 100644 test/webapp/bug42390.jsp create mode 100644 test/webapp/bug42565.jsp create mode 100644 test/webapp/bug43nnn/bug43400.jsp create mode 100644 test/webapp/bug44994.jsp create mode 100644 test/webapp/bug45nnn/bug45015a.jsp create mode 100644 test/webapp/bug45nnn/bug45015b.jsp create mode 100644 test/webapp/bug45nnn/bug45015c.jsp create mode 100644 test/webapp/bug45nnn/bug45427.jsp create mode 100644 test/webapp/bug45nnn/bug45451.jspf create mode 100644 test/webapp/bug45nnn/bug45451a.jsp create mode 100644 test/webapp/bug45nnn/bug45451b.jsp create mode 100644 test/webapp/bug45nnn/bug45451c.jsp create mode 100644 test/webapp/bug45nnn/bug45451d.jspx create mode 100644 test/webapp/bug45nnn/bug45451e.jsp create mode 100644 test/webapp/bug45nnn/bug45511.jsp create mode 100644 test/webapp/bug46381.jsp create mode 100644 test/webapp/bug46596.jsp create mode 100644 test/webapp/bug47331.jsp create mode 100644 test/webapp/bug47413.jsp create mode 100644 test/webapp/bug47977.jspx create mode 100644 test/webapp/bug48nnn/bug48112.jsp create mode 100644 test/webapp/bug48nnn/bug48616.jsp create mode 100644 test/webapp/bug48nnn/bug48616b.jsp create mode 100644 test/webapp/bug48nnn/bug48627.jsp create mode 100644 test/webapp/bug48nnn/bug48668a.jsp create mode 100644 test/webapp/bug48nnn/bug48668b.jsp create mode 100644 test/webapp/bug48nnn/bug48701-TVI-NFA.jsp create mode 100644 test/webapp/bug48nnn/bug48701-TVI-NG.jsp create mode 100644 test/webapp/bug48nnn/bug48701-UseBean.jsp create mode 100644 test/webapp/bug48nnn/bug48701-VI.jsp create mode 100644 test/webapp/bug48nnn/bug48701-fail.jsp create mode 100644 test/webapp/bug48nnn/bug48827.jspx create mode 100644 test/webapp/bug49nnn/bug49196.jsp create mode 100644 test/webapp/bug49nnn/bug49297DuplicateAttr.jsp create mode 100644 test/webapp/bug49nnn/bug49297MultipleImport1.jsp create mode 100644 test/webapp/bug49nnn/bug49297MultipleImport2.jsp create mode 100644 test/webapp/bug49nnn/bug49297MultiplePageEncoding1.jsp create mode 100644 test/webapp/bug49nnn/bug49297MultiplePageEncoding2.jsp create mode 100644 test/webapp/bug49nnn/bug49297MultiplePageEncoding3.jsp create mode 100644 test/webapp/bug49nnn/bug49297MultiplePageEncoding4.jsp create mode 100644 test/webapp/bug49nnn/bug49297NoSpace.jsp create mode 100644 test/webapp/bug49nnn/bug49297NoSpaceStrict.jsp create mode 100644 test/webapp/bug49nnn/bug49297Tag.jsp create mode 100644 test/webapp/bug49nnn/bug49464-cp1252.txt create mode 100644 test/webapp/bug49nnn/bug49464-ibm850.txt create mode 100644 test/webapp/bug49nnn/bug49464-iso-8859-1.txt create mode 100644 test/webapp/bug49nnn/bug49464-utf-8-bom.txt create mode 100644 test/webapp/bug49nnn/bug49464-utf-8.txt create mode 100644 test/webapp/bug49nnn/bug49555.jsp create mode 100644 test/webapp/bug49nnn/bug49726a.jsp create mode 100644 test/webapp/bug49nnn/bug49726b.jsp create mode 100644 test/webapp/bug49nnn/bug49799.jsp create mode 100644 test/webapp/bug53257/foo bar.jsp create mode 100644 test/webapp/bug53257/foo bar.txt create mode 100644 test/webapp/bug53257/foo bar/foobar.jsp create mode 100644 test/webapp/bug53257/foo bar/foobar.txt create mode 100644 test/webapp/bug53257/foo#bar.jsp create mode 100644 test/webapp/bug53257/foo#bar.txt create mode 100644 test/webapp/bug53257/foo%bar.jsp create mode 100644 test/webapp/bug53257/foo%bar.txt create mode 100644 test/webapp/bug53257/foo&bar.jsp create mode 100644 test/webapp/bug53257/foo&bar.txt create mode 100644 test/webapp/bug53257/foo+bar.jsp create mode 100644 test/webapp/bug53257/foo+bar.txt create mode 100644 test/webapp/bug53257/foo;bar.jsp create mode 100644 test/webapp/bug53257/foo;bar.txt create mode 100644 test/webapp/bug53257/index.jsp create mode 100644 test/webapp/bug5nnnn/bug50408.jsp create mode 100644 test/webapp/bug5nnnn/bug51544.jsp create mode 100644 test/webapp/bug5nnnn/bug52335.jsp create mode 100644 test/webapp/bug5nnnn/bug53387.shtml create mode 100644 test/webapp/bug5nnnn/bug53465.jsp create mode 100644 test/webapp/bug5nnnn/bug53467].jsp create mode 100644 test/webapp/bug5nnnn/bug53545.html create mode 100644 test/webapp/bug5nnnn/bug53545.jsp create mode 100644 test/webapp/bug5nnnn/bug53986.jsp create mode 100644 test/webapp/bug5nnnn/bug54011.jsp create mode 100644 test/webapp/bug5nnnn/bug54012.jsp create mode 100644 test/webapp/bug5nnnn/bug54144.jsp create mode 100644 test/webapp/bug5nnnn/bug54241a.jsp create mode 100644 test/webapp/bug5nnnn/bug54241b.jsp create mode 100644 test/webapp/bug5nnnn/bug54242.jsp create mode 100644 test/webapp/bug5nnnn/bug54338.jsp create mode 100644 test/webapp/bug5nnnn/bug54801a.jspx create mode 100644 test/webapp/bug5nnnn/bug54801b.jspx create mode 100644 test/webapp/bug5nnnn/bug54821a.jspx create mode 100644 test/webapp/bug5nnnn/bug54821b.jspx create mode 100644 test/webapp/bug5nnnn/bug54888.jsp create mode 100644 test/webapp/bug5nnnn/bug55198.jsp create mode 100644 test/webapp/bug5nnnn/bug55262-coda.jspf create mode 100644 test/webapp/bug5nnnn/bug55262-prelude.jspf create mode 100644 test/webapp/bug5nnnn/bug55262.jsp create mode 100644 test/webapp/bug5nnnn/bug55642a.jsp create mode 100644 test/webapp/bug5nnnn/bug55642b.jsp create mode 100644 test/webapp/bug5nnnn/bug55807.jsp create mode 100644 test/webapp/bug5nnnn/bug56029.jspx create mode 100644 test/webapp/bug5nnnn/bug56147.jsp create mode 100644 test/webapp/bug5nnnn/bug56265.jsp create mode 100644 test/webapp/bug5nnnn/bug56334and56561.jspx create mode 100644 test/webapp/bug5nnnn/bug56529.jsp create mode 100644 test/webapp/bug5nnnn/bug56581.jsp create mode 100644 test/webapp/bug5nnnn/bug56612.jsp create mode 100644 test/webapp/bug5nnnn/bug57141.jsp create mode 100644 test/webapp/bug5nnnn/bug57142.jsp create mode 100644 test/webapp/bug5nnnn/bug57441.jsp create mode 100644 test/webapp/bug5nnnn/bug57601.jsp create mode 100644 test/webapp/bug5nnnn/bug57601.txt create mode 100644 test/webapp/bug5nnnn/bug58096.jsp create mode 100644 test/webapp/bug5nnnn/bug58178.jsp create mode 100644 test/webapp/bug5nnnn/bug58178b.jsp create mode 100644 test/webapp/bug5nnnn/bug58178c.jsp create mode 100644 test/webapp/bug5nnnn/bug58444a.jsp create mode 100644 test/webapp/bug5nnnn/bug58444b.jsp create mode 100644 test/webapp/bug66609/_listing.xslt create mode 100644 test/webapp/bug66609/a&a.txt create mode 100644 test/webapp/bug66609/b'b.txt create mode 100644 test/webapp/bug6nnnn/bug60032.jsp create mode 100644 test/webapp/bug6nnnn/bug60431.jsp create mode 100644 test/webapp/bug6nnnn/bug61854.jsp create mode 100644 test/webapp/bug6nnnn/bug62453.jsp create mode 100644 test/webapp/bug6nnnn/bug63359a.jsp create mode 100644 test/webapp/bug6nnnn/bug64373.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-bigdecimal.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-biginteger.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-boolean.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-byte.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-character.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-double.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-float.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-integer.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-long.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-primitive-boolean.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-primitive-byte.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-primitive-character.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-primitive-double.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-primitive-float.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-primitive-integer.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-primitive-long.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-primitive-short.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-short.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-string.jsp create mode 100644 test/webapp/bug6nnnn/bug64872-timeunit.jsp create mode 100644 test/webapp/bug6nnnn/bug64872b-timeunit.jsp create mode 100644 test/webapp/bug6nnnn/bug65377.jsp create mode 100644 test/webapp/bug6nnnn/bug65390-empty.jsp create mode 100644 test/webapp/bug6nnnn/bug65390.jsp create mode 100644 test/webapp/bug6nnnn/bug66441.jsp create mode 100644 test/webapp/bug6nnnn/bug66582.jsp create mode 100644 test/webapp/bug6nnnn/bug69303.txt create mode 100644 test/webapp/echo-params.jsp create mode 100644 test/webapp/el-method.jsp create mode 100644 test/webapp/el-misc-no-quote-attribute-el.jsp create mode 100644 test/webapp/el-misc-with-quote-attribute-el.jsp create mode 100644 test/webapp/index.html create mode 100644 test/webapp/index.html.br create mode 100644 test/webapp/index.html.gz create mode 100644 test/webapp/jsp/doc-version-invalid/document-0.4.jspx create mode 100644 test/webapp/jsp/doc-version-invalid/document-1.1.jspx create mode 100644 test/webapp/jsp/doc-version-invalid/document-1.2.1.jspx create mode 100644 test/webapp/jsp/doc-version-invalid/document-1.3.jspx create mode 100644 test/webapp/jsp/doc-version-invalid/document-1.9.jspx create mode 100644 test/webapp/jsp/doc-version-invalid/document-2.4.jspx create mode 100644 test/webapp/jsp/doc-version-invalid/document-3.2.jspx create mode 100644 test/webapp/jsp/doc-version-invalid/document-4.0.jspx create mode 100644 test/webapp/jsp/doc-version-invalid/document-5.4.jspx create mode 100644 test/webapp/jsp/doc-version-valid/document-1.2.jspx create mode 100644 test/webapp/jsp/doc-version-valid/document-2.0.jspx create mode 100644 test/webapp/jsp/doc-version-valid/document-2.1.jspx create mode 100644 test/webapp/jsp/doc-version-valid/document-2.2.jspx create mode 100644 test/webapp/jsp/doc-version-valid/document-2.3.jspx create mode 100644 test/webapp/jsp/doc-version-valid/document-3.0.jspx create mode 100644 test/webapp/jsp/doc-version-valid/document-3.1.jspx create mode 100644 test/webapp/jsp/encoding/README.txt create mode 100644 test/webapp/jsp/encoding/bom-none-prolog-none.jsp create mode 100644 test/webapp/jsp/encoding/bom-none-prolog-none.jspx create mode 100644 test/webapp/jsp/encoding/bom-none-prolog-utf16be.jspx create mode 100644 test/webapp/jsp/encoding/bom-none-prolog-utf16le.jspx create mode 100644 test/webapp/jsp/encoding/bom-none-prolog-utf8.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf16be-prolog-none.jsp create mode 100644 test/webapp/jsp/encoding/bom-utf16be-prolog-none.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf16be-prolog-utf16be.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf16be-prolog-utf16le.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf16be-prolog-utf8.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf16le-prolog-none.jsp create mode 100644 test/webapp/jsp/encoding/bom-utf16le-prolog-none.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf16le-prolog-utf16be.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf16le-prolog-utf16le.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf16le-prolog-utf8.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf8-prolog-none.jsp create mode 100644 test/webapp/jsp/encoding/bom-utf8-prolog-none.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf8-prolog-utf16be.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf8-prolog-utf16le.jspx create mode 100644 test/webapp/jsp/encoding/bom-utf8-prolog-utf8.jspx create mode 100644 test/webapp/jsp/encoding/bug60769a.jspx create mode 100644 test/webapp/jsp/encoding/bug60769b.jspx create mode 100644 test/webapp/jsp/error.jsp create mode 100644 test/webapp/jsp/errorOnELNotFound/default.jsp create mode 100644 test/webapp/jsp/errorOnELNotFound/page-directive-false.jsp create mode 100644 test/webapp/jsp/errorOnELNotFound/page-directive-true.jsp create mode 100644 test/webapp/jsp/errorOnELNotFound/tag-file-false.jsp create mode 100644 test/webapp/jsp/errorOnELNotFound/tag-file-true.jsp create mode 100644 test/webapp/jsp/errorOnELNotFound/web-xml-false.jsp create mode 100644 test/webapp/jsp/errorOnELNotFound/web-xml-true.jsp create mode 100644 test/webapp/jsp/forward.jsp create mode 100644 test/webapp/jsp/generator/attribute-01.jsp create mode 100644 test/webapp/jsp/generator/attribute-02.jsp create mode 100644 test/webapp/jsp/generator/attribute-03.jsp create mode 100644 test/webapp/jsp/generator/attribute-04.jsp create mode 100644 test/webapp/jsp/generator/beaninfo-01.jsp create mode 100644 test/webapp/jsp/generator/break-el-interpreter.jsp create mode 100644 test/webapp/jsp/generator/break-string-interpreter.jsp create mode 100644 test/webapp/jsp/generator/circular-01.jsp create mode 100644 test/webapp/jsp/generator/customtag-02.jsp create mode 100644 test/webapp/jsp/generator/customtag-03.jsp create mode 100644 test/webapp/jsp/generator/customtag-04.jsp create mode 100644 test/webapp/jsp/generator/deferred-method-01.jsp create mode 100644 test/webapp/jsp/generator/deferred-method-02.jsp create mode 100644 test/webapp/jsp/generator/dobody-01.jsp create mode 100644 test/webapp/jsp/generator/element-01.jsp create mode 100644 test/webapp/jsp/generator/forward-01.jsp create mode 100644 test/webapp/jsp/generator/forward-02.jsp create mode 100644 test/webapp/jsp/generator/forward-03.jsp create mode 100644 test/webapp/jsp/generator/forward-04.jsp create mode 100644 test/webapp/jsp/generator/include-01.jsp create mode 100644 test/webapp/jsp/generator/info-conflict-none.jsp create mode 100644 test/webapp/jsp/generator/info-conflict.jsp create mode 100644 test/webapp/jsp/generator/info.jsp create mode 100644 test/webapp/jsp/generator/invoke-01.jsp create mode 100644 test/webapp/jsp/generator/jsp-id.jsp create mode 100644 test/webapp/jsp/generator/jsp-id.jspx create mode 100644 test/webapp/jsp/generator/plugin-01.jspx create mode 100644 test/webapp/jsp/generator/scriptingvariables-01.jsp create mode 100644 test/webapp/jsp/generator/scriptingvariables-02.jsp create mode 100644 test/webapp/jsp/generator/setproperty-01.jsp create mode 100644 test/webapp/jsp/generator/setters-01.jsp create mode 100644 test/webapp/jsp/generator/single-threaded.jsp create mode 100644 test/webapp/jsp/generator/templatetext-01.jsp create mode 100644 test/webapp/jsp/generator/templatetext-02.jsp create mode 100644 test/webapp/jsp/generator/try-catch-finally-01.jsp create mode 100644 test/webapp/jsp/generator/try-catch-finally-02.jsp create mode 100644 test/webapp/jsp/generator/usebean-01.jsp create mode 100644 test/webapp/jsp/generator/usebean-02.jsp create mode 100644 test/webapp/jsp/generator/usebean-03.jsp create mode 100644 test/webapp/jsp/generator/usebean-04.jsp create mode 100644 test/webapp/jsp/generator/usebean-05.jsp create mode 100644 test/webapp/jsp/generator/usebean-06.jsp create mode 100644 test/webapp/jsp/generator/usebean-07.jsp create mode 100644 test/webapp/jsp/generator/usebean-08.jsp create mode 100644 test/webapp/jsp/generator/variable-from-attr-nested.jsp create mode 100644 test/webapp/jsp/generator/variable-tagfile-from-attr-nested.jsp create mode 100644 test/webapp/jsp/generator/variable-tagfile-nested.jsp create mode 100644 test/webapp/jsp/generator/variable-tei-nested.jsp create mode 100644 test/webapp/jsp/generator/x-powered-by.jsp create mode 100644 test/webapp/jsp/generator/xml-doctype-01.jspx create mode 100644 test/webapp/jsp/generator/xml-doctype-02.jspx create mode 100644 test/webapp/jsp/generator/xml-prolog-01.jspx create mode 100644 test/webapp/jsp/generator/xml-prolog-02.jspx create mode 100644 test/webapp/jsp/generator/xml-prolog-tag.jspx create mode 100644 test/webapp/jsp/includeThenForward.jsp create mode 100644 test/webapp/jsp/ok.html create mode 100644 test/webapp/jsp/pageContext1.jsp create mode 100644 test/webapp/jsp/pageContext2.jsp create mode 100644 test/webapp/jsp/session.jsp create mode 100644 test/webapp/jsp/tagFileInJar.jsp create mode 100644 test/webapp/jsp/test.jsp create mode 100644 test/webapp/jsp/trim-spaces-extended.jsp create mode 100644 test/webapp/script-expr.jsp create mode 100644 test/webapp/valid.jspx create mode 100644 test/webapp/valid.xsd create mode 100644 test/webapp/welcome-files/index.jsp create mode 100644 test/webapp/welcome-files/sub/.gitignore create mode 100644 test/webresources/dir1-internal.jar create mode 100644 test/webresources/dir1.jar create mode 100644 test/webresources/dir1/META-INF/MANIFEST.MF create mode 100644 test/webresources/dir1/d1/d1-f1.txt create mode 100644 test/webresources/dir1/d2/d2-f1.txt create mode 100644 test/webresources/dir1/f1.txt create mode 100644 test/webresources/dir1/f2.txt create mode 100644 test/webresources/dir2/d1/.ignore-me.txt create mode 100644 test/webresources/dir2/d2/.ignore-me.txt create mode 100644 test/webresources/dir3/.ignore-me.txt create mode 100644 test/webresources/non-static-resources.jar create mode 100644 test/webresources/war-url-connection.war create mode 100644 webapps/ROOT/WEB-INF/web.xml create mode 100644 webapps/ROOT/asf-logo-wide.svg create mode 100644 webapps/ROOT/bg-button.png create mode 100644 webapps/ROOT/bg-middle.png create mode 100644 webapps/ROOT/bg-nav.png create mode 100644 webapps/ROOT/bg-upper.png create mode 100644 webapps/ROOT/favicon.ico create mode 100644 webapps/ROOT/index.jsp create mode 100644 webapps/ROOT/tomcat.css create mode 100644 webapps/ROOT/tomcat.svg create mode 100644 webapps/docs/META-INF/context.xml create mode 100644 webapps/docs/WEB-INF/jsp/403.jsp create mode 100644 webapps/docs/WEB-INF/web.xml create mode 100644 webapps/docs/aio.xml create mode 100644 webapps/docs/annotationapi/index.html create mode 100644 webapps/docs/api/index.html create mode 100644 webapps/docs/appdev/build.xml.txt create mode 100644 webapps/docs/appdev/deployment.xml create mode 100644 webapps/docs/appdev/index.xml create mode 100644 webapps/docs/appdev/installation.xml create mode 100644 webapps/docs/appdev/introduction.xml create mode 100644 webapps/docs/appdev/processes.xml create mode 100644 webapps/docs/appdev/project.xml create mode 100644 webapps/docs/appdev/sample/docs/README.txt create mode 100644 webapps/docs/appdev/sample/index.html create mode 100644 webapps/docs/appdev/sample/sample.war create mode 100644 webapps/docs/appdev/sample/src/mypackage/Hello.java create mode 100644 webapps/docs/appdev/sample/web/WEB-INF/web.xml create mode 100644 webapps/docs/appdev/sample/web/hello.jsp create mode 100644 webapps/docs/appdev/sample/web/images/tomcat.gif create mode 100644 webapps/docs/appdev/sample/web/index.html create mode 100644 webapps/docs/appdev/source.xml create mode 100644 webapps/docs/appdev/web.xml.txt create mode 100644 webapps/docs/apr.xml create mode 100644 webapps/docs/architecture/index.xml create mode 100644 webapps/docs/architecture/overview.xml create mode 100644 webapps/docs/architecture/project.xml create mode 100644 webapps/docs/architecture/requestProcess.xml create mode 100644 webapps/docs/architecture/requestProcess/authentication-process.png create mode 100644 webapps/docs/architecture/requestProcess/request-process.png create mode 100644 webapps/docs/architecture/startup.xml create mode 100644 webapps/docs/architecture/startup/serverStartup.pdf create mode 100644 webapps/docs/architecture/startup/serverStartup.txt create mode 100644 webapps/docs/balancer-howto.xml create mode 100644 webapps/docs/building.xml create mode 100644 webapps/docs/cdi.xml create mode 100644 webapps/docs/cgi-howto.xml create mode 100644 webapps/docs/changelog.xml create mode 100644 webapps/docs/class-loader-howto.xml create mode 100644 webapps/docs/cluster-howto.xml create mode 100644 webapps/docs/comments.xml create mode 100644 webapps/docs/config/ajp.xml create mode 100644 webapps/docs/config/automatic-deployment.xml create mode 100644 webapps/docs/config/cluster-channel.xml create mode 100644 webapps/docs/config/cluster-deployer.xml create mode 100644 webapps/docs/config/cluster-interceptor.xml create mode 100644 webapps/docs/config/cluster-listener.xml create mode 100644 webapps/docs/config/cluster-manager.xml create mode 100644 webapps/docs/config/cluster-membership.xml create mode 100644 webapps/docs/config/cluster-receiver.xml create mode 100644 webapps/docs/config/cluster-sender.xml create mode 100644 webapps/docs/config/cluster-valve.xml create mode 100644 webapps/docs/config/cluster.xml create mode 100644 webapps/docs/config/context.xml create mode 100644 webapps/docs/config/cookie-processor.xml create mode 100644 webapps/docs/config/credentialhandler.xml create mode 100644 webapps/docs/config/engine.xml create mode 100644 webapps/docs/config/executor.xml create mode 100644 webapps/docs/config/filter.xml create mode 100644 webapps/docs/config/globalresources.xml create mode 100644 webapps/docs/config/host.xml create mode 100644 webapps/docs/config/http.xml create mode 100644 webapps/docs/config/http2.xml create mode 100644 webapps/docs/config/index.xml create mode 100644 webapps/docs/config/jar-scan-filter.xml create mode 100644 webapps/docs/config/jar-scanner.xml create mode 100644 webapps/docs/config/jaspic.xml create mode 100644 webapps/docs/config/listeners.xml create mode 100644 webapps/docs/config/loader.xml create mode 100644 webapps/docs/config/manager.xml create mode 100644 webapps/docs/config/project.xml create mode 100644 webapps/docs/config/realm.xml create mode 100644 webapps/docs/config/resources.xml create mode 100644 webapps/docs/config/server.xml create mode 100644 webapps/docs/config/service.xml create mode 100644 webapps/docs/config/sessionidgenerator.xml create mode 100644 webapps/docs/config/systemprops.xml create mode 100644 webapps/docs/config/valve.xml create mode 100644 webapps/docs/connectors.xml create mode 100644 webapps/docs/default-servlet.xml create mode 100644 webapps/docs/deployer-howto.xml create mode 100644 webapps/docs/developers.xml create mode 100644 webapps/docs/elapi/index.html create mode 100644 webapps/docs/graal.xml create mode 100644 webapps/docs/host-manager-howto.xml create mode 100644 webapps/docs/html-host-manager-howto.xml create mode 100644 webapps/docs/html-manager-howto.xml create mode 100644 webapps/docs/images/add.gif create mode 100644 webapps/docs/images/asf-logo.svg create mode 100644 webapps/docs/images/code.gif create mode 100644 webapps/docs/images/cors-flowchart.png create mode 100644 webapps/docs/images/design.gif create mode 100644 webapps/docs/images/docs-stylesheet.css create mode 100644 webapps/docs/images/docs.gif create mode 100644 webapps/docs/images/fix.gif create mode 100644 webapps/docs/images/fonts/OpenSans400.woff create mode 100644 webapps/docs/images/fonts/OpenSans400italic.woff create mode 100644 webapps/docs/images/fonts/OpenSans600.woff create mode 100644 webapps/docs/images/fonts/OpenSans600italic.woff create mode 100644 webapps/docs/images/fonts/OpenSans700.woff create mode 100644 webapps/docs/images/fonts/OpenSans700italic.woff create mode 100644 webapps/docs/images/fonts/fonts.css create mode 100644 webapps/docs/images/tomcat.gif create mode 100644 webapps/docs/images/tomcat.png create mode 100644 webapps/docs/images/update.gif create mode 100644 webapps/docs/images/void.gif create mode 100644 webapps/docs/index.xml create mode 100644 webapps/docs/introduction.xml create mode 100644 webapps/docs/jasper-howto.xml create mode 100644 webapps/docs/jaspicapi/index.html create mode 100644 webapps/docs/jndi-datasource-examples-howto.xml create mode 100644 webapps/docs/jndi-resources-howto.xml create mode 100644 webapps/docs/jspapi/index.html create mode 100644 webapps/docs/logging.xml create mode 100644 webapps/docs/manager-howto.xml create mode 100644 webapps/docs/maven-jars.xml create mode 100644 webapps/docs/mbeans-descriptors-howto.xml create mode 100644 webapps/docs/monitoring.xml create mode 100644 webapps/docs/project.xml create mode 100644 webapps/docs/proxy-howto.xml create mode 100644 webapps/docs/realm-howto.xml create mode 100644 webapps/docs/rewrite.xml create mode 100644 webapps/docs/security-howto.xml create mode 100644 webapps/docs/security-manager-howto.xml create mode 100644 webapps/docs/servletapi/index.html create mode 100644 webapps/docs/setup.xml create mode 100644 webapps/docs/ssi-howto.xml create mode 100644 webapps/docs/ssl-howto.xml create mode 100644 webapps/docs/tomcat-docs.xsl create mode 100644 webapps/docs/tribes/developers.xml create mode 100644 webapps/docs/tribes/faq.xml create mode 100644 webapps/docs/tribes/interceptors.xml create mode 100644 webapps/docs/tribes/introduction.xml create mode 100644 webapps/docs/tribes/leader-election-initiate-election.dia create mode 100644 webapps/docs/tribes/leader-election-initiate-election.jpg create mode 100644 webapps/docs/tribes/leader-election-message-arrives.dia create mode 100644 webapps/docs/tribes/leader-election-message-arrives.jpg create mode 100644 webapps/docs/tribes/membership.xml create mode 100644 webapps/docs/tribes/project.xml create mode 100644 webapps/docs/tribes/setup.xml create mode 100644 webapps/docs/tribes/status.xml create mode 100644 webapps/docs/tribes/transport.xml create mode 100644 webapps/docs/virtual-hosting-howto.xml create mode 100644 webapps/docs/web-socket-howto.xml create mode 100644 webapps/docs/websocketapi/index.html create mode 100644 webapps/docs/windows-auth-howto.xml create mode 100644 webapps/docs/windows-service-howto.xml create mode 100644 webapps/examples/META-INF/context.xml create mode 100644 webapps/examples/WEB-INF/classes/CookieExample.java create mode 100644 webapps/examples/WEB-INF/classes/HelloWorldExample.java create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_cs.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_de.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_es.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_fr.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_ja.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_ko.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_pt.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_pt_BR.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_ru.properties create mode 100644 webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties create mode 100644 webapps/examples/WEB-INF/classes/RequestHeaderExample.java create mode 100644 webapps/examples/WEB-INF/classes/RequestInfoExample.java create mode 100644 webapps/examples/WEB-INF/classes/RequestParamExample.java create mode 100644 webapps/examples/WEB-INF/classes/ServletToJsp.java create mode 100644 webapps/examples/WEB-INF/classes/SessionExample.java create mode 100644 webapps/examples/WEB-INF/classes/async/Async0.java create mode 100644 webapps/examples/WEB-INF/classes/async/Async1.java create mode 100644 webapps/examples/WEB-INF/classes/async/Async2.java create mode 100644 webapps/examples/WEB-INF/classes/async/Async3.java create mode 100644 webapps/examples/WEB-INF/classes/async/AsyncStockContextListener.java create mode 100644 webapps/examples/WEB-INF/classes/async/AsyncStockServlet.java create mode 100644 webapps/examples/WEB-INF/classes/async/Stockticker.java create mode 100644 webapps/examples/WEB-INF/classes/cal/Entries.java create mode 100644 webapps/examples/WEB-INF/classes/cal/Entry.java create mode 100644 webapps/examples/WEB-INF/classes/cal/JspCalendar.java create mode 100644 webapps/examples/WEB-INF/classes/cal/TableBean.java create mode 100644 webapps/examples/WEB-INF/classes/checkbox/CheckTest.java create mode 100644 webapps/examples/WEB-INF/classes/colors/ColorGameBean.java create mode 100644 webapps/examples/WEB-INF/classes/compressionFilters/CompressionFilter.java create mode 100644 webapps/examples/WEB-INF/classes/compressionFilters/CompressionFilterTestServlet.java create mode 100644 webapps/examples/WEB-INF/classes/compressionFilters/CompressionResponseStream.java create mode 100644 webapps/examples/WEB-INF/classes/compressionFilters/CompressionServletResponseWrapper.java create mode 100644 webapps/examples/WEB-INF/classes/dates/JspCalendar.java create mode 100644 webapps/examples/WEB-INF/classes/error/Smart.java create mode 100644 webapps/examples/WEB-INF/classes/examples/ExampleTagBase.java create mode 100644 webapps/examples/WEB-INF/classes/examples/FooTag.java create mode 100644 webapps/examples/WEB-INF/classes/examples/FooTagExtraInfo.java create mode 100644 webapps/examples/WEB-INF/classes/examples/LogTag.java create mode 100644 webapps/examples/WEB-INF/classes/examples/ValuesTag.java create mode 100644 webapps/examples/WEB-INF/classes/filters/ExampleFilter.java create mode 100644 webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/BookBean.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/FooBean.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/ValuesBean.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/el/Functions.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/EchoAttributesTag.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/FindBookSimpleTag.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/HelloWorldSimpleTag.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/RepeatSimpleTag.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/ShuffleSimpleTag.java create mode 100644 webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/TileSimpleTag.java create mode 100644 webapps/examples/WEB-INF/classes/listeners/ContextListener.java create mode 100644 webapps/examples/WEB-INF/classes/listeners/SessionListener.java create mode 100644 webapps/examples/WEB-INF/classes/nonblocking/ByteCounter.java create mode 100644 webapps/examples/WEB-INF/classes/nonblocking/NumberWriter.java create mode 100644 webapps/examples/WEB-INF/classes/num/NumberGuessBean.java create mode 100644 webapps/examples/WEB-INF/classes/sessions/DummyCart.java create mode 100644 webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java create mode 100644 webapps/examples/WEB-INF/classes/util/CookieFilter.java create mode 100644 webapps/examples/WEB-INF/classes/util/HTMLFilter.java create mode 100644 webapps/examples/WEB-INF/classes/validators/DebugValidator.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/ExamplesConfig.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/chat/ChatAnnotation.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardContextListener.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/CloseWebsocketMessage.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/echo/EchoAnnotation.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/echo/EchoAsyncAnnotation.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/echo/EchoStreamAnnotation.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/echo/servers.json create mode 100644 webapps/examples/WEB-INF/classes/websocket/snake/Direction.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/snake/Location.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/snake/Snake.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/snake/SnakeAnnotation.java create mode 100644 webapps/examples/WEB-INF/classes/websocket/snake/SnakeTimer.java create mode 100644 webapps/examples/WEB-INF/jsp/403.jsp create mode 100644 webapps/examples/WEB-INF/jsp/debug-taglib.tld create mode 100644 webapps/examples/WEB-INF/jsp/example-taglib.tld create mode 100644 webapps/examples/WEB-INF/jsp/jsp2-example-taglib.tld create mode 100644 webapps/examples/WEB-INF/tags/displayProducts.tag create mode 100644 webapps/examples/WEB-INF/tags/helloWorld.tag create mode 100644 webapps/examples/WEB-INF/tags/panel.tag create mode 100644 webapps/examples/WEB-INF/web.xml create mode 100644 webapps/examples/index.html create mode 100644 webapps/examples/jsp/async/async1.jsp create mode 100644 webapps/examples/jsp/async/async3.jsp create mode 100644 webapps/examples/jsp/async/index.jsp create mode 100644 webapps/examples/jsp/cal/cal1.jsp create mode 100644 webapps/examples/jsp/cal/cal2.jsp create mode 100644 webapps/examples/jsp/cal/calendar.html create mode 100644 webapps/examples/jsp/cal/login.html create mode 100644 webapps/examples/jsp/checkbox/CheckTest.html create mode 100644 webapps/examples/jsp/checkbox/check.html create mode 100644 webapps/examples/jsp/checkbox/checkresult.jsp create mode 100644 webapps/examples/jsp/checkbox/cresult.html create mode 100644 webapps/examples/jsp/colors/ColorGameBean.html create mode 100644 webapps/examples/jsp/colors/clr.html create mode 100644 webapps/examples/jsp/colors/colors.html create mode 100644 webapps/examples/jsp/colors/colrs.jsp create mode 100644 webapps/examples/jsp/dates/date.html create mode 100644 webapps/examples/jsp/dates/date.jsp create mode 100644 webapps/examples/jsp/error/er.html create mode 100644 webapps/examples/jsp/error/err.jsp create mode 100644 webapps/examples/jsp/error/error.html create mode 100644 webapps/examples/jsp/error/errorpge.jsp create mode 100644 webapps/examples/jsp/forward/forward.jsp create mode 100644 webapps/examples/jsp/forward/fwd.html create mode 100644 webapps/examples/jsp/forward/one.jsp create mode 100644 webapps/examples/jsp/forward/two.html create mode 100644 webapps/examples/jsp/images/code.gif create mode 100644 webapps/examples/jsp/images/execute.gif create mode 100644 webapps/examples/jsp/images/return.gif create mode 100644 webapps/examples/jsp/include/foo.html create mode 100644 webapps/examples/jsp/include/foo.jsp create mode 100644 webapps/examples/jsp/include/inc.html create mode 100644 webapps/examples/jsp/include/include.jsp create mode 100644 webapps/examples/jsp/index.html create mode 100644 webapps/examples/jsp/jsp2/el/basic-arithmetic.html create mode 100644 webapps/examples/jsp/jsp2/el/basic-arithmetic.jsp create mode 100644 webapps/examples/jsp/jsp2/el/basic-comparisons.html create mode 100644 webapps/examples/jsp/jsp2/el/basic-comparisons.jsp create mode 100644 webapps/examples/jsp/jsp2/el/composite.html create mode 100644 webapps/examples/jsp/jsp2/el/composite.jsp create mode 100644 webapps/examples/jsp/jsp2/el/functions.html create mode 100644 webapps/examples/jsp/jsp2/el/functions.jsp create mode 100644 webapps/examples/jsp/jsp2/el/implicit-objects.html create mode 100644 webapps/examples/jsp/jsp2/el/implicit-objects.jsp create mode 100644 webapps/examples/jsp/jsp2/jspattribute/jspattribute.html create mode 100644 webapps/examples/jsp/jsp2/jspattribute/jspattribute.jsp create mode 100644 webapps/examples/jsp/jsp2/jspattribute/shuffle.html create mode 100644 webapps/examples/jsp/jsp2/jspattribute/shuffle.jsp create mode 100644 webapps/examples/jsp/jsp2/jspx/basic.html create mode 100644 webapps/examples/jsp/jsp2/jspx/basic.jspx create mode 100644 webapps/examples/jsp/jsp2/jspx/svgexample.html create mode 100644 webapps/examples/jsp/jsp2/jspx/textRotate.html create mode 100644 webapps/examples/jsp/jsp2/jspx/textRotate.jpg create mode 100644 webapps/examples/jsp/jsp2/jspx/textRotate.jspx create mode 100644 webapps/examples/jsp/jsp2/misc/coda.jspf create mode 100644 webapps/examples/jsp/jsp2/misc/config.html create mode 100644 webapps/examples/jsp/jsp2/misc/config.jsp create mode 100644 webapps/examples/jsp/jsp2/misc/dynamicattrs.html create mode 100644 webapps/examples/jsp/jsp2/misc/dynamicattrs.jsp create mode 100644 webapps/examples/jsp/jsp2/misc/prelude.jspf create mode 100644 webapps/examples/jsp/jsp2/simpletag/book.html create mode 100644 webapps/examples/jsp/jsp2/simpletag/book.jsp create mode 100644 webapps/examples/jsp/jsp2/simpletag/hello.html create mode 100644 webapps/examples/jsp/jsp2/simpletag/hello.jsp create mode 100644 webapps/examples/jsp/jsp2/simpletag/repeat.html create mode 100644 webapps/examples/jsp/jsp2/simpletag/repeat.jsp create mode 100644 webapps/examples/jsp/jsp2/tagfiles/hello.html create mode 100644 webapps/examples/jsp/jsp2/tagfiles/hello.jsp create mode 100644 webapps/examples/jsp/jsp2/tagfiles/panel.html create mode 100644 webapps/examples/jsp/jsp2/tagfiles/panel.jsp create mode 100644 webapps/examples/jsp/jsp2/tagfiles/products.html create mode 100644 webapps/examples/jsp/jsp2/tagfiles/products.jsp create mode 100644 webapps/examples/jsp/jsptoserv/hello.jsp create mode 100644 webapps/examples/jsp/jsptoserv/jsptoservlet.jsp create mode 100644 webapps/examples/jsp/jsptoserv/jts.html create mode 100644 webapps/examples/jsp/num/numguess.html create mode 100644 webapps/examples/jsp/num/numguess.jsp create mode 100644 webapps/examples/jsp/security/protected/error.jsp create mode 100644 webapps/examples/jsp/security/protected/index.jsp create mode 100644 webapps/examples/jsp/security/protected/login.jsp create mode 100644 webapps/examples/jsp/sessions/DummyCart.html create mode 100644 webapps/examples/jsp/sessions/carts.html create mode 100644 webapps/examples/jsp/sessions/carts.jsp create mode 100644 webapps/examples/jsp/sessions/crt.html create mode 100644 webapps/examples/jsp/simpletag/foo.html create mode 100644 webapps/examples/jsp/simpletag/foo.jsp create mode 100644 webapps/examples/jsp/snp/snoop.html create mode 100644 webapps/examples/jsp/snp/snoop.jsp create mode 100644 webapps/examples/jsp/tagplugin/choose.html create mode 100644 webapps/examples/jsp/tagplugin/choose.jsp create mode 100644 webapps/examples/jsp/tagplugin/foreach.html create mode 100644 webapps/examples/jsp/tagplugin/foreach.jsp create mode 100644 webapps/examples/jsp/tagplugin/howto.html create mode 100644 webapps/examples/jsp/tagplugin/if.html create mode 100644 webapps/examples/jsp/tagplugin/if.jsp create mode 100644 webapps/examples/jsp/tagplugin/notes.html create mode 100644 webapps/examples/jsp/xml/xml.html create mode 100644 webapps/examples/jsp/xml/xml.jsp create mode 100644 webapps/examples/servlets/cookies.html create mode 100644 webapps/examples/servlets/helloworld.html create mode 100644 webapps/examples/servlets/images/code.gif create mode 100644 webapps/examples/servlets/images/execute.gif create mode 100644 webapps/examples/servlets/images/return.gif create mode 100644 webapps/examples/servlets/index.html create mode 100644 webapps/examples/servlets/nonblocking/bytecounter.html create mode 100644 webapps/examples/servlets/reqheaders.html create mode 100644 webapps/examples/servlets/reqinfo.html create mode 100644 webapps/examples/servlets/reqparams.html create mode 100644 webapps/examples/servlets/sessions.html create mode 100644 webapps/examples/websocket/chat.xhtml create mode 100644 webapps/examples/websocket/drawboard.xhtml create mode 100644 webapps/examples/websocket/echo.xhtml create mode 100644 webapps/examples/websocket/index.xhtml create mode 100644 webapps/examples/websocket/snake.xhtml create mode 100644 webapps/host-manager/META-INF/context.xml create mode 100644 webapps/host-manager/WEB-INF/jsp/401.jsp create mode 100644 webapps/host-manager/WEB-INF/jsp/403.jsp create mode 100644 webapps/host-manager/WEB-INF/jsp/404.jsp create mode 100644 webapps/host-manager/WEB-INF/manager.xml create mode 100644 webapps/host-manager/WEB-INF/web.xml create mode 100644 webapps/host-manager/css/manager.css create mode 100644 webapps/host-manager/images/asf-logo.svg create mode 100644 webapps/host-manager/images/tomcat.svg create mode 100644 webapps/host-manager/index.jsp create mode 100644 webapps/manager/META-INF/context.xml create mode 100644 webapps/manager/WEB-INF/jsp/401.jsp create mode 100644 webapps/manager/WEB-INF/jsp/403.jsp create mode 100644 webapps/manager/WEB-INF/jsp/404.jsp create mode 100644 webapps/manager/WEB-INF/jsp/connectorCerts.jsp create mode 100644 webapps/manager/WEB-INF/jsp/connectorCiphers.jsp create mode 100644 webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp create mode 100644 webapps/manager/WEB-INF/jsp/sessionDetail.jsp create mode 100644 webapps/manager/WEB-INF/jsp/sessionsList.jsp create mode 100644 webapps/manager/WEB-INF/web.xml create mode 100644 webapps/manager/css/manager.css create mode 100644 webapps/manager/images/asf-logo.svg create mode 100644 webapps/manager/images/tomcat.svg create mode 100644 webapps/manager/index.jsp create mode 100644 webapps/manager/status.xsd create mode 100644 webapps/manager/xform.xsl diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..486f834 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +# 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. +root = true + +[*] +indent_style = space +trim_trailing_whitespace = true + +[*.java] +indent_size = 4 +max_line_length = 120 +ij_java_block_brace_style = end_of_line +ij_java_else_on_new_line = false + +[*.{txt,md}] +max_line_length = 80 + +[*.xml] +indent_size = 2 \ No newline at end of file diff --git a/BUILDING.txt b/BUILDING.txt new file mode 100644 index 0000000..fc440f2 --- /dev/null +++ b/BUILDING.txt @@ -0,0 +1,604 @@ +================================================================================ + 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. +================================================================================ + + ==================================================== + Building The Apache Tomcat @VERSION_MAJOR_MINOR@ Servlet/JSP Container + ==================================================== + +This project contains the source code for Tomcat @VERSION_MAJOR_MINOR@, a container that +implements the Jakarta Servlet @SERVLET_SPEC_VERSION@, JSP @JSP_SPEC_VERSION@, EL @EL_SPEC_VERSION@, WebSocket @WEBSOCKET_SPEC_VERSION@ and +Authentication @JASPIC_SPEC_VERSION@ specifications from the Jakarta EE project at Eclipse +. + +Note: If you just need to run Apache Tomcat, it is not necessary to build +it. You may simply download a binary distribution. It is cross-platform. +Read RUNNING.txt for the instruction on how to run it. + +In order to build a binary distribution version of Apache Tomcat from a +source distribution, do the following: + + +(1) Download and Install a Java Development Kit + + 1. If the JDK is already installed, skip to (2). + + 2. Download a version @BUILD_JAVA_VERSION@ or later of Java Development Kit (JDK) release (use + the latest update available for your chosen version) from one of: + + http://www.oracle.com/technetwork/java/javase/downloads/index.html + http://openjdk.java.net/install/index.html + or another JDK vendor. + + Note regarding later versions of Java: + + As documented elsewhere, one of components in Apache Tomcat includes + a private copy of the Apache Commons DBCP 2 library. + + The JDBC interfaces implemented by DBCP frequently change in non-backwards + compatible ways between versions of the Java SE specification. Therefore, + it is likely that DBCP 2 will only compile with the specific version of Java + listed above and that compilation will fail if a later version of Java is + used. + + See Apache Commons DBCP 2 project web site for more details on + available versions of the library and its requirements, + + https://commons.apache.org/dbcp/ + + 3. Install the JDK according to the instructions included with the release. + + 4. Set an environment variable JAVA_HOME to the pathname of the directory + into which you installed the JDK release. + + +(2) Install Apache Ant version @ANT_VERSION_REQUIRED@ or later on your computer. + + 1. If Apache Ant version @ANT_VERSION_REQUIRED@ or later is already installed on your + computer, skip to (3). + + 2. Download a binary distribution of Ant from: + + https://ant.apache.org/bindownload.cgi + + 3. Unpack the binary distribution into a convenient location so that the + Ant release resides in its own directory (conventionally named + "apache-ant-[version]"). + + For the purposes of the remainder of this document, the symbolic name + "${ant.home}" is used to refer to the full pathname of the release + directory. + + 4. Create an ANT_HOME environment variable to point the directory + ${ant.home}. + + 5. Modify the PATH environment variable to include the directory + ${ant.home}/bin in its list. This makes the "ant" command line script + available, which will be used to actually perform the build. + + +(3) Building Tomcat @VERSION_MAJOR_MINOR@ + +(3.1) Checkout or obtain the source code for Tomcat @VERSION_MAJOR_MINOR@ + +Clone the source using git, then checkout a specific major branch or +main for the latest code development, or download and unpack a source +package. + + * Tomcat GitHub repository URL: + + https://github.com/apache/tomcat + + * Source packages can be downloaded from: + + https://tomcat.apache.org/download-@VERSION_MAJOR@.cgi + +The location where the source has been placed will be further referred as +${tomcat.source}. + +The Tomcat local build process does not modify line-endings. The svn repository +is configured so that all files will be checked out with the line-ending +appropriate for the current platform. When using a source package you should +ensure that you use the source package that has the appropriate line-ending +for your platform: + + zip -> CRLF + tar.gz -> LF + +Note that the release build process does modify line-endings to ensure that +each release package has the appropriate line-endings. + +(3.2) Building + + 1. The build is controlled by creating a ${tomcat.source}/build.properties + file. + + It is recommended to always create the file, because of unfortunate + default value of base.path property. You may start with the following + content for the file: + + # ----- Default Base Path for Dependent Packages ----- + # Replace this path with the directory path where dependencies binaries + # should be downloaded + base.path=/home/me/some-place-to-download-to + + 2. Configure base.path property by adding it to the + ${tomcat.source}/build.properties file. + + The base.path property specifies the place where Tomcat dependencies + required by the build are downloaded. It is recommended to place this + directory outside of the source tree, so that you do not waste your + time re-downloading the libraries. + +* NOTE: The default value of the base.path property configures the build script + to download the libraries required to build Tomcat to the + ${user.home}/tomcat-build-libs directory. + +* NOTE: Users accessing the Internet through a proxy must use the properties + file to indicate to Ant the proxy configuration. + + The following properties should be added to the ${tomcat.source}/build.properties + file. + + proxy.use=true + proxy.host=proxy.domain + proxy.port=8080 + proxy.user=username + proxy.password=password + + See Apache Ant documentation for the task for details. + + 3. Go to the sources directory and run Ant: + + cd ${tomcat.source} + ant + + This will execute the "deploy" target in build.xml. + + Once the build has completed successfully, a usable Tomcat installation + will have been produced in the ${tomcat.source}/output/build directory, + and can be started and stopped with the usual scripts. + + Note that the build includes Tomcat documentation, which can be found + in the output/build/webapps/docs directory. + + The path of the output directory can be controlled by specifying the + "tomcat.output" property in the build.properties file. + +* NOTE: Do not run the build as the root user. Building and running Tomcat + does not require root privileges. + + +(4) Updating sources and rebuilding + +It is recommended that you regularly update the downloaded Tomcat @VERSION_MAJOR_MINOR@ +sources using your git client. + +For a quick rebuild of only modified code you can use: + + cd ${tomcat.source} + ant + + +(5) Special builds + +There are several targets in Tomcat build files that are useful to be +called separately. They build components that you may want to build +quickly, or ones that are included in the full release and are not built +during the default "deploy" build. + +(5.1) Building documentation + +The documentation web application is built during the default "deploy" +build. + +It can be built quickly by using the following commands: + + cd ${tomcat.source} + ant build-docs + +The output of this command will be found in the following directory: + + output/build/webapps/docs + + +The API documentation (Javadoc) is built during a "release" build. It is +easy to build it separately by using the following commands: + + cd ${tomcat.source} + ant javadoc + +The output of this command will be found in the following directories: + + output/dist/webapps/docs/api + output/dist/webapps/docs/elapi + output/dist/webapps/docs/jspapi + output/dist/webapps/docs/servletapi + + +(5.2) Building the extras (commons-logging, webservices etc.) + +These components are documented on the "Additional Components" +(extras.html) page of documentation. They are built during a "release" +build. + +You can build them by using the following commands: + + cd ${tomcat.source} + ant extras + +(5.3) Building the embedded packages + +These are built during a "release" build. + +You can build them by using the following commands: + + cd ${tomcat.source} + ant embed + + +(6) Building a full release (as provided via the ASF download pages) + + A full release includes the Windows installer which requires a Windows + environment to be available to create it. If not building in a Windows + environment, the build scripts assume that Wine is available. If this is not + the case, the skip.installer property may be set to skip the creation of the + Windows installer. + + Provided that Wine is available on non-Windows platforms, a full release + build may be made on Windows, Linux or MacOS. + + 1. Configure GPG, if needed + + If the released artifacts have to be cryptographically signed with a + PGP signature, like the official ASF releases are, the following + property can be added to the build.properties file: + + # Location of GPG executable (used only for releases) + gpg.exec=/path/to/gpg + + You do not need it if you do not plan to sign the release. + + If "gpg.exec" property does not point to an existing file, it will be + ignored and this feature will be deactivated. + + You will be prompted for the GPG passphrase when the release build + starts, unless "gpg.passphrase" property is set. + + 2. If building the Windows installer + + If running the build in a UAC enabled environment, building the Windows + installer requires elevated privileges. The simplest way to do this is to + open the command prompt used for the build with the "Run as administrator" + option. + + 3. Configure the code signing service + + ASF committers performing official releases will need to configure the code + signing service so that the Windows installer is signed during the build + process. The following properties need to be added to the build.properties + file: + + # Location of GPG executable (used only for releases) + gpg.exec=/path/to/gpg + # Code signing of Windows installer + do.codesigning=true + codesigning.storepass=request-via-pmc + + Release managers will be provided with the necessary credentials by the PMC. + + 4. Build the release: + + Apache Tomcat releases are fully reproducible. + + Release managers producing release builds must follow the following + procedure: + + cd ${tomcat.source} + ant pre-release + ant release + git commit -a -m "Tag " + git tag + git push origin + ant release + git reset --hard HEAD~1 + + The output from either 'ant release' call may be uploaded as the official + release since they will be identical. It is recommended that the output from + the second call is used. + + Anyone wishing to reproduce an official build must do so from an official + source release. The build tool chain defined in build.properties.release + must be used to reproduce an official build. Once unpacked to + ${tomcat.source}, the following steps must be followed + + cd ${tomcat.source} + ant release + + Following the same steps without using the defined build tool chain will + create a release that is functionally the same as an official release but + not bit for bit identical. + + +(7) Tests + +(7.1) Running Tomcat tests + +Tomcat includes a number of junit tests. The tests are not run when a +release is built. There is separate command to run them. + +To run the testsuite use the following command: + + cd ${tomcat.source} + ant test + +It is advisable to redirect output of the above command to a file for later +inspection. + +The JUnit reports generated by the tests will be written to the following +directory: + + output/build/logs + +By default the testsuite is run twice to test the 2 different implementations +of Tomcat connectors: NIO and NIO2. (If you are not familiar with Tomcat +connectors, see config/http.html in documentation for details). + +The 2 runs are activated and deactivated individually by the following +properties, which all are "true" by default: + + execute.test.nio=true + execute.test.nio2=true + +The SSL tests will be run twice. Once with the JSSE implementation and once +with the OpenSSL implementation. The OpenSSL implementation can only can be +tested if Tomcat-Native library binaries are found by the testsuite. The +"test.apr.loc" property specifies the directory where the library binaries are +located. + +By default the "test.apr.loc" property specifies the following location: + + output/build/bin/ + +If you are on Windows and want to test the OpenSSL TLS implementation you can +put the tcnative-2.dll file into ${tomcat.source}/bin/ and it will be +copied into the above directory when the build runs. + +The unit tests include tests of the clustering functionality which require +multicast to be enabled. There is a simple application provided in the Tomcat +test source (org.apache.catalina.tribes.TesterMulticast) that can be used to +check if a machine supports multicast. Notes on enabling multicast for different +operating systems are provided in the Javadoc for that class. + + +(7.2) Running a single test + +It is possible to run a single JUnit test class by adding the "test.entry" +property to the build.properties file. The property specifies the name of +the test class. + +For example: + + test.entry=org.apache.catalina.util.TestServerInfo + +It is possible to further limit such run to a number of selected test +methods by adding "test.entry.methods" property. The property specifies a +comma-separated list of test case methods. + +For example: + + test.entry=org.apache.el.lang.TestELArithmetic + test.entry.methods=testMultiply01,testMultiply02 + + +(7.3) Running a set of tests + +It is possible to run a set of JUnit test classes by adding the "test.name" +property to the build.properties file. The property specifies an Ant +includes pattern for the fileset of test class files to run. + +The default value is "**/Test*.java", so all test classes are being +executed (with few exceptions - see build.xml for several exclude patterns). + +You can include multiple patterns by concatenating them with a comma (",") +as the separator. + +For example: + + test.name=**/TestSsl.java,**/TestWebSocketFrameClientSSL.java + +You can exclude specific JUnit test classes by adding the "test.exclude" +property to the build.properties file. The property specifies an Ant +excludes pattern for the fileset of test class files to exclude form the run. +The default value is empty, so no classes are excluded. The syntax is the same +as for the property "test.name". + + +(7.4) Other configuration options + + 1. It is possible to configure the directory where JUnit reports are + written to. It is configured by "test.reports" property. The default + value is + + output/build/logs + + 2. It is possible to enable generation of access log file when the tests + are run. This is off by default and can be enabled by the following + property: + + test.accesslog=true + + The "access_log." file will be written to the same directory as + JUnit reports, + + output/build/logs + + 3. The testsuite respects logging configuration as configured by + ${tomcat.source}/conf/logging.properties + + The log files will be written to the temporary directory used by the + tests, + + output/test-tmp/logs + + 4. It is possible to configure formatter used by JUnit reports. + Configuration properties are "junit.formatter.type", + "junit.formatter.extension" and "junit.formatter.usefile". + + For example the following property deactivates generation of separate report + files: + + junit.formatter.usefile=false + + 5. It is possible to speed up testing by letting JUnit to run several + tests in parallel. + + This is configured by setting "test.threads" property. The recommended + value is one thread per core. + + 6. Optional support is provided for the Cobertura code coverage tool. + +NOTE: Cobertura is licensed under GPL v2 with parts of it being under + Apache License v1.1. See https://cobertura.github.io/cobertura/ for details. + Using it during Tomcat build is optional and is off by default. + + Cobertura can be enabled using the following properties: + + test.cobertura=true + test.threads=1 + + Using Cobertura currently requires setting test.threads configuration + property to the value of 1. Setting that property to a different value + will deactivate code coverage. + + The report files by default are written to + + output/coverage + + 7. The performance tests are written to run on reasonably powerful machines + (such as a developer may use day to day) assuming no other resource hungry + processes are running. + + Performance tests may be an absolute test (how long to complete a number + of iterations of an operation or set of operations) or may be a relative + test (how long two or more different approaches take to generate the same + result). The absolute tests may be destructive in that they run until the + system runs out of resources. + + Where there is no benefit in running an absolute performance test as part + of a standard test run, the test will be excluded by naming it + Tester*Performance.java. + + The relative tests are included as part of a standard test run however, + where the assumptions made about host capabilities are not true (e.g. on + CI systems running in virtual machine) the tests may be deactivated by + using the following property: + + test.excludePerformance=true + + 8. Some tests are require large heaps (e.g. 8GB). The CI systems used by the + project either cannot support heaps of this size or do not support them by + default. These tests are therefore disabled by default and may be enabled by + using the following property: + + test.includeLargeHeap=true + + 9. Some tests include checks that the access log valve entries are as expected. + These checks include timings. On slower / loaded systems these checks will + often fail. The checks may be relaxed by using the following property: + + test.relaxTiming=true + + 10. It is known that some platforms (e.g. OSX El Capitan) require IPv4 to + be the default for the multicast tests to work. This is configured by + the following property: + + java.net.preferIPv4Stack=true + + 11. By default the output of unit tests is sent to the console and can be + quite verbose. The output can be deactivated by setting the property: + + test.verbose=false + +(8) Source code checks + +(8.1) Checkstyle + +NOTE: Checkstyle is licensed under LGPL. Using Checkstyle during Tomcat + build is optional and is off by default. + + See http://checkstyle.sourceforge.net/ for more information. + +Tomcat comes with a Checkstyle configuration that tests its source code +for certain conventions, like presence of the license header. + +To enable Checkstyle, add the following property to build.properties file: + + execute.validate=true + +Once Checkstyle is enabled, the check will be performed automatically +during the build. The check is run before compilation of the source code. + +To speed-up repeated runs of this check, a cache is configured. The cache +is located in the following directory: + + output/res/checkstyle + +It is possible to run the check separately by calling the "validate" +target. The command is: + + cd ${tomcat.source} + ant -Dexecute.validate=true validate + + +(8.2) SpotBugs + +NOTE: SpotBugs is licensed under LGPL. Using SpotBugs during Tomcat build is + optional and is off by default. + + See https://spotbugs.github.io/ for more information. + +To enable SpotBugs, add the following property to build.properties file: + + execute.spotbugs=true + +To compile Tomcat classes and generate a SpotBugs report, call the +"spotbugs" target. For example: + + cd ${tomcat.source} + ant -Dexecute.spotbugs=true spotbugs + +The report file by default is written to + + output/spotbugs + + +(8.3) End-of-line conventions check + +You usually would not need to run this check. You can skip this section. + +Apache Tomcat project has convention that all of its textual source files, +stored in the Git repository, use Unix style LF line endings. + +This test is used by developers to check that the source code adheres to +this convention. It verifies that the ends of lines in textual files are +appropriate. The idea is to run this check regularly and notify developers +when an inconsistency is detected. + +The command to run this test is: + + cd ${tomcat.source} + ant validate-eoln diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0ffe20f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,165 @@ +# Contributing to Apache Tomcat + +Firstly, thanks for your interest in contributing! I hope that this will be a +pleasant experience for you, and that you will return to continue +contributing. + +Please visit our [Get Involved page](https://tomcat.apache.org/getinvolved.html) +for more information on how to contribute. + +## Code of Conduct + +This project and everyone participating in it are governed by the Apache +software Foundation's +[Code of Conduct](https://www.apache.org/foundation/policies/conduct.html). By +participating, you are expected to adhere to this code. If you are aware of +unacceptable behavior, please visit the +[Reporting Guidelines page](https://www.apache.org/foundation/policies/conduct.html#reporting-guidelines) +and follow the instructions there. + +## How Can I Contribute? + +Most of the contributions that we receive are code contributions, but you can +also contribute to the documentation, wiki, etc., or simply report solid bugs +for us to fix. + +### Reporting Bugs + +Please review our [guide](https://tomcat.apache.org/bugreport.html) on how to +submit a bug report. This page also has links to other resources to assist +you. + +### Reporting Translation improvements + +Apache Tomcat project uses POEditor for managing the localization files. +Please see more at https://cwiki.apache.org/confluence/x/vIPzBQ + +### Your First Code Contribution + +### Trouble Deciding How to Contribute? + +Unsure where to begin contributing to Tomcat? You can start by taking a look at +the issues marked 'Beginner', link below. Please note that the Beginner keyword +is pretty new to the project, so if there aren't any issues in the filter feel +free to ask on the [dev list](https://tomcat.apache.org/lists.html#tomcat-dev). + +* [Beginner issues](https://bz.apache.org/bugzilla/buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=NEEDINFO&keywords=Beginner&keywords_type=allwords&list_id=160824&product=Tomcat%207&product=Tomcat%208.5&product=Tomcat%209&query_format=advanced) - +issues which should only require a few lines of code, and a test or two to +resolve. + +The list above shows all bugs that are marked 'Beginner' and are open in the +currently supported Tomcat versions (7, 8.5, and 9). + +If you prefer C over Java, you may also take a look at the tomcat-native and +Tomcat Connectors products in Bugzilla. + +### How to Provide Your First Patch + +Excited yet? This section will guide you through providing a patch to the +committers of the project for review and acceptance. + +##### Choose Your Method of Submission + +You can provide a patch in one of the following ways (in order of preference): + +* GitHub Pull Request +* Patch attachment to the Bugzilla issue +* Email the patch to the developer list. This is not preferred, but if no bug +is associated with the patch, or you would like a developer review, an email +may be appropriate. + +##### Get the Sources + +Now that you've chosen how you want to submit a patch, you need to get the +source code. + +###### Download The Source Distribution + +This method works if you want to submit a patch via email, but +the difference in using the sources distribution and a VCS is that you have to +manually generate the patch file by using diff. If this is what you want, you +can download the sources from the "Source Code Distributions" section of the +Download Page. There is one such page for every major Tomcat version: + +- [Tomcat 10](https://tomcat.apache.org/download-10.cgi) +- [Tomcat 9](https://tomcat.apache.org/download-90.cgi) +- [Tomcat 8](https://tomcat.apache.org/download-80.cgi) +- [Tomcat 7](https://tomcat.apache.org/download-70.cgi) + +##### Manual Patch Generation + +If you have chosen to attach a patch to the Bugzilla issue (or email +one), then you'll need to download the sources as noted above, make your +desired changes and then manually generate your patch using diff (or any +other tool). + +##### GitHub + +To submit a GitHub Pull Request you'll need to fork the +[repository](https://github.com/apache/tomcat), clone your fork to do the work: + +``` +$ git clone https://github.com/$USERNAME/tomcat.git +``` + +and then push your changes, and submit a Pull Request via the GitHub UI. + +#### Submitting Your Patch! + +After you've chosen your method of submission, retrieved the sources, and +fixed the issue it's time to submit your work. At this point, just follow +the method of submission you chose earlier. + +* GitHub PR - after resolving the issue in your local fork and pushing to your +copy of the repository, open a GitHub PR for review. +* Bugzilla attachment - attach the patch to the Bugzilla issue +* Email - again, not preferred, but you may send an email to the developer list +with a patch attached for review. + +#### Waiting For Feedback + +It may take a while for committers to review. Please be patient during this +time as all committers are volunteers on the project. If a significant amount +of time has lapsed since your submission, such as a couple of months, feel free +to either update your BZ, PR, or email the dev list with a message to bump your +issue. Sometimes things get lost in all the work and we need a reminder :smile: + +## IDE Support + +Special IDE support for +[Eclipse](https://www.eclipse.org/ide/), +[IntelliJ IDEA](https://www.jetbrains.com/idea/) and +[NetBeans](https://netbeans.org/) +is provided through special ant targets: + +```bash +ant ide-eclipse +``` +```bash +ant ide-intellij +``` +```bash +ant ide-netbeans +``` + +Just execute the ant target for your IDE after checking out the sources +to set up the appropriate configuration files. +Also make sure to re-execute the target after switching branches or +after pulling upstream changes in order to keep your IDE configurations in sync. + +## Style Guide + +Apache Tomcat has very loosely defined coding conventions, but the following +guidelines will be useful: + +* Use spaces for indenting, not tabs +* 100 char line width for Java source, 80 char line width for documentation +source (.txt, .xml) +* Java source: { at end of line, 4 space indents +* XML source: 2 space indents + +## Did we miss something? + +Have you reviewed this guide and found it lacking? Or are you confused about +some particular step? If so, please let us know! Or better yet, submit a PR to +address the issue :wink: diff --git a/KEYS b/KEYS new file mode 100644 index 0000000..6b7231f --- /dev/null +++ b/KEYS @@ -0,0 +1,562 @@ +This file contains the PGP&GPG keys of various Apache developers. +Please don't use them for email unless you have to. Their main +purpose is code signing. + +Apache users: pgp < KEYS +Apache developers: + (pgpk -ll && pgpk -xa ) >> this file. + or + (gpg --fingerprint --list-sigs + && gpg --armor --export ) >> this file. + +Apache developers: please ensure that your key is also available via the +PGP keyservers (such as pgpkeys.mit.edu). + + +pub 4096R/2F6059E7 2009-09-18 + Key fingerprint = A9C5 DF4D 22E9 9998 D987 5A51 10C0 1C5A 2F60 59E7 +uid Mark E D Thomas +sub 4096R/5E763BEC 2009-09-18 + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: GPGTools - http://gpgtools.org + +mQINBEq0DukBEAD4jovHOPJDxoD+JnO1Go2kiwpgRULasGlrVKuSUdP6wzcaqWmX +pqtOJKKwW2MQFQLmg7nQ9RjJwy3QCbKNDJQA/bwbQT1F7WzTCz2S6vxC4zxKck4t +6RZBq2dJsYKF0CEh6ZfY4dmKvhq+3istSoFRdHYoOPGWZpuRDqfZPdGm/m335/6K +GH59oysn1NE7a2a+kZzjBSEgv23+l4Z1Rg7+fpz1JcdHSdC2Z+ZRxML25eVatRVz +4yvDOZItqDURP24zWOodxgboldV6Y88C3v/7KRR+1vklzkuA2FqF8Q4r/2f0su7M +UVviQcy29y/RlLSDTTYoVlCZ1ni14qFU7Hpw43KJtgXmcUwq31T1+SlXdYjNJ1aF +kUi8BjCHDcSgE/IReKUanjHzm4XSymKDTeqqzidi4k6PDD4jyHb8k8vxi6qT6Udn +lcfo5NBkkUT1TauhEy8ktHhbl9k60BvvMBP9l6cURiJg1WS77egI4P/82oPbzzFi +GFqXyJKULVgxtdQ3JikCpodp3f1fh6PlYZwkW4xCJLJucJ5MiQp07HAkMVW5w+k8 +Xvuk4i5quh3N+2kzKHOOiQCDmN0sz0XjOE+7XBvM1lvz3+UarLfgSVmW8aheLd7e +aIl5ItBk8844ZJ60LrQ+JiIqvqJemxyIM6epoZvY5a3ZshZpcLilC5hW8QARAQAB +tCJNYXJrIEUgRCBUaG9tYXMgPG1hcmt0QGFwYWNoZS5vcmc+iQI3BBMBCgAhBQJK +tA7pAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEBDAHFovYFnn2YgQAKN6 +FLG/I1Ij3PUlC/XNlhasQxPeE3w2OvttweOQPYkblJ9nHtGH5pNqG2/qoGShlpI0 +4jJy9GxWKOo7NV4v7M0mbVlCXVgjdlvMFWdL7lnocggwJAFejQcYlVtxyhu4m50L +BvBunEhxCbQcKnnWmkB7Ocm0Ictaqjc9rCc1F/aNhVMUpJ0zG1kyTp9hxvN6TbCQ +lacMx5ocTWzL0zn6QZhbUfrYwfxYJmSnkVYZOYzXIXIsLN5sJ9Q4P8tjY4qWgd+b +QvOqPWrkzL9LVRnGOrSYIsoM5zWdoj1g1glMzK/ZqJdRqqqBhe6FYTbXipz8oX8i +mCebcaxZnfLhGiqqX+yDa3YUwDiqom+sZOc0iXGvKkqltPLpNeF0MVT7aZjalsQ/ +v2Ysb24RQl9FfjfWmvT8ZPWz8Kore1AI4UcIIgFVtM+zuLlL9CIsGjg+gHDE2dhZ +DY0qfizlHL9CoAWUDM3pIfxM2V4BRn1xO+j/mModhjmYLZvnFVz4KGkNO7wRkofA +ANIWYo3WI5x83BGDH371t3NRrrpSSFP0XpQX6/Leaj2j6U6puABL2qBxhscsO6ch +c3u4/+019ff+peZVsc9ttcTQXsKIujmMb8p2sk5usmv6PKVX3oW/RAxpbVHU5kZ5 +px1Hq7mMQdZfLs5ff4YymXBH02z4/RmSzPam0Xb5iEYEEBEKAAYFAkq0IlsACgkQ +b7IeiTPGAkN0nQCfUkbSwYiPeKQg6n2w/DuE91bVWLgAninl71+xlXNMZ+n6kBCX +On7R8UCpiQIcBBABCgAGBQJKtCKAAAoJEDGBeFpSfIEkwqkP/37xinx1zPlX9XZ4 +S9ShAl5+H/ZDvqJ45eJvHcxsym8/Go5nT3n0ny4wgjFQjG+X49wk/P0/rwG32xlk +/3tLLGmQA0MQwBCd9F9Mq3gLof09sgB6qyr9N4PfPHmsnkae4vmsS9cd5pXB0Gyy +b03rveedxUjD/joOrCfA28VIyF2yrCqiANr1dJruS0ieTuK9BskY++BoADtlwxhh +OGEEV/xAMggA20oWtCoib0cPZAj0exi3c+s3E2mUaBI7Ycy2yKpztXImb0cPsd6v +h0YpymXIl2OY1XPV8G5vUVwS4Ojs94lR4ozDXkl7UCPEW356SWVNPpDUTwxE37mt +TKqgsWjwEmfXU+N2rh9pqZY5RApFmKRgxOmF0BTG8ml9d3U49KAk4VUGIav+Fy32 +4M82Ka+mG7ZqgoBuc5nnEMmVYO4Zhh1yEt8d3lKRl3jRbmvoxvxQRRGUYrfEkCxd +XhspVh1OD+ZcY7V592OoUmsi4a6LZHrRirUuz9t4tVNctPtraDtClczEw1QdAseJ +Z/oTQrKVQ9mzXOeFdVjLOdXrAuLagcr/ENa8QlYxulal5YqN7pqRM5DQbZiVY2+o +wgsNDh2s8LyArJuOJdt0KTGiFz3i2tx77JOVKkXBRl155dnhN+NuxW22Itf2okMM +Y5vwnQ22rpa6PUXLwmf/lL9SC6//iEYEEBECAAYFAkr7Bl4ACgkQEy5J1OQe3H5o +QACdGWeT0hXpsWvi8LG4smhXgqzCKdEAn0ec6xU4cBJJ9e/DRPzj9S7nqZFHiEYE +EBEKAAYFAkrzvDUACgkQmx/anzwGIjEsXACg6MZYvM+2cATmO1/SeAChCmzuPDoA +oLVG30rJdaAm2GiA5oMNLOdxf1SviQIcBBABCAAGBQJK82GzAAoJEIWPxMT0OFaj +8QgP/i6E4jje3nEn6c+/2CTXuSP7Q9L5EsPNnMinKDi+3ksnLOAOTGZZoaekb7Q1 +v1rEQCIwxcGDUynsh7xr7pafky/taYouoVDWdvyy6BC7itQ3DkynHV5AwL1sazmp +itGcmmLWs9sDYyjgMYnFAVxgoQnFEmU5lgZXI3bnAp5qRhPHPlFEiraki0qFP4z+ +gb7ljRC58xH3Id/3Zv8fxO4cVH6hX4KHDINEa/KTwRJCMvd7tol/IojkWxH73IMH +cvQHrxV3tRGfQD67TJTP9jvqfm34edIsF9hjd473y4aKNgt0G+h3g3ND+K7+ECn2 +XfYi6xWlMlP4TQOGKMln9P0QHrs1qv63iBO/D/rd/MfFj1pKl888DXBIQ6ytO3iL +DPNuRF2M9T4Rjy10nDXHnigs6jktg5hWKzXSnLKPmRmT9c+UCmym9c16044bKnwK +Abi8aOdXI4IkOLTedX8qZsQ3L8rSXMPpIqlfmLfDzvY1rXyO7loFRW76cfZE2Rkv +ZzjSOpgC4kWtaxXPJZI/gfDpUQFKFFU98dmDCRgFJXpVcRIMWaZ1iGT8MRVBos98 +M4JBX3//ftLNoTFQS5V1Ks3TEOrK80FdQXGLLXpz3cpfh3MoXLz+yjTMSkehhF1Q +X7JFBP/aYk5Nv08qaQJwPYvXNqyFLgk6Dg6dLOkJ/U+AboK7iQIcBBABCgAGBQJK +82DsAAoJEJmgMCnd+hmeW2MP/34UZZwx2v/RNONuCCMVaPqDxlAWaNlKOjx4s4ej +DtyyNaSB4zKOqrcLNdZUr1YlcrNduRHjrt9ue17lhhg/zfBWiau4PG/LZp6Yhhv2 +OAsRpUd6djcWW8InjDKG57Ks65dReZgOmd37VNYUZFlDMTtRgk002e75l8Kr1diZ +qHWQ97eHCWQrCmQSe6mRGIUMiMzqlzCK01BhJvM5fyhCigseuIWwYaLO0MYyGANq +qvDfp3iCoH1f7e0MhBT7fYXAM1Vebx5OAeU8Sm2NyeVSVXNmEsh+fPdPX3HjDhsU +0ht0LsZLsf+oktes+iavQ/wWJ3TVADHiC2sOcckcnG7hbI9MhV8IC5XkCfubVpR6 +5RTbCbyJAQQQ/AEwAsTtYaJIYPx+W8xh9W1qC3CJbfwNAJgYP3f2f9wOM3ha9zb+ +AevBV1+tWUNOlGg9uM1c1MS7e5ifSF52TIsZpsxyJE7kwW1C+rLYwvyFMP+5pfm+ +/IMUEVLnlOAndo542RtGgVJtoTyA/JHh4rVimf6a9AdpDMG4eNWzzLX7C54FbEOS ++zecBiJLsbwW1GoYatlNrrhDAkZSNsmAJjCkmK3gf5EjRAUVYOsh9oVGoi9iDMQi +K1OsymMTsBF9MYNkdU3Hz04uanIbMpZuB0WFvi6d3cMXwnhCdyxSTMmgl5SizXPX +M6AniQIcBBABCgAGBQJK87unAAoJECBchnPcdCx8joYQAMm3NgBPLxKtVxzRFBYN +VuhrQpWYkkC+eVEmwIimvcf/cJmgImu6M9YdJvfsNcZ9510/qm9MF0QrPYeIgkK9 +INJwuNPM1dCR9N9z+Odxt+G6ZPW4wK0pHq+CIWMBJe83+szfMi1X/t4fa5/9kV7t +4tTSIrwMmmclm0trn2U84xwAsAjTdWH8OFBISSu1W0TEHnYU4x21i4Vcvu75KYJB +zbGawunsbyTcf8xw3GKg6kHpNpaQwkU78nBhRMKYxEqW3p9ar/eDg/BbdD1TaGk3 +N8Ej370Us9dhcBeRR0u5tYmMsbcI5r8W0smBFmuJzvHmi1q4p5SHYI2yRyHHxr2G +AE/LGGrEvwJmmsNAfYsoT4z+C+DLwijawkeQETB9A3fPsd4y0NTmCYKY9hdTwIhS +jdcP9lLIqSjQuNwG4b2hUdUf+XlW/4EuK41SllLTgz9osliU2Xzzx1wO0z2FWfUv +pdJro+PAqO9RrBw7F4M4gJ84JzECritS40WAeO4fDj5vw/oPP4sedZOwJ9O3VvAq +ibaWxdJZ8XclnxquD/OMCMaV9rK46bHv66x0HLXopfDhbI+oPuzYkpQaubxAVt0k +ttzBqEnBBqGazuvkxvfqjt6byix/Pee5jAGrUYHEjAiNUHYrzwFy1PK6TOoDLww4 +en6UgMqMO/xYsqKyYlERnyHsiQIcBBABCgAGBQJK+zaeAAoJEDWVOV6z2OG65aUP +/1gXindkH7yiYnvBIyk72M2s9KV/msWjoGeHcOy+XNfj+NSseLun88wf+2guh3z+ +v+MM5eUFtTSiJfftOkFIp+SQyEBAtAgf4iiwA8jKHiffiJ0fP3qLxhaO4t8uaSbg +cbgmsYbtD6iuBH20kHZbK9FO5Bl6FcQ8Xy4YOi300LiPa3vR2Rza6kgpCqlZ0d+A +qlgUZ7X+Zdm2RSew5jXsRB5K9jPxbYUaJUJgUAcIkUnnnhk1zN1/3yi4mLM6eEL8 +7efoZfgLRbd6UZvE+nKEwpjU1ocI9a8dHpr48PVOpSu/bNLVs+LhEHfFQcrZx6FO +yuR/J7CuMw/DyNICY2denzMhmqYNVaveGTEZQn7RHPIV/KEsw8AyPv3pFX7uz0km +ISMJCmMQRRpW/Hl80H7vuFyQQUcNpCvYBii46+Mf3qCKVIhUwNZ92ytRQ1lrC7P+ +Qe7iCYHgVX1F749i13qp+EZTU63Zz6AfFA+P4LdUmmGCFvAhJ8HTlOS75b3eryOg +vUBLnSAXHQDbFQ3Ku3sOkCVVbXkHO4aGFNlEaQeatBtonCFmx9CDkEgdxUd777D3 +i/jll4DLOJkwd6AFAOzivuQqq6Rn5XO7QrCTD6gw5+yeRPZIf7cq6PHTkHplt1pm +FCpfUkVSpOdX0F++al/vSvyAK/XHRSETpjxrZ80deGNOiQQcBBABCAAGBQJK/hs7 +AAoJEIqviNbYTkGuHYgf/jmm4EqMBNvUDUVxBkMCHFFURIdQO+xQnuB4So5p/XVf +c55WKDPG/4fCvcB+sritcJa+FdPFT4EzxMDzbgnbsfoVuF27WV5yQzgTNUiHszYe +X45oFF30fh9vPKGfjzy4zeQzzgGzAlyijjBXdAEgN0TXkxdMCiVxrK4TAve41ZEH +bS9lmtFCXrX7SBoZhOTSVfGUDaxb/5FVgP/GhtG35wtFtDlaca3W0x8Wl0kyRMb2 +NPjO/h/VYhoSDlJGBEGotCafY3tqdwEcnfTcz2mv0Y0ASI6aaCN/BVegkCO1Q5uI +EpTa6YPiRJqZqvjUlocy/LjNzmcDa/11Ai8DUd9wHFVEPBPRc6Xz+jbi9VqsN07E +S1FCnOr+YIh1E7dsKENdnWmRW/vJjsyzLGcU5G7xzxZAyqHRPB6v1MX3HdAQL91z +V62LpvGHynGHJbQQclKv/3PKCu/IM2XAOVbHhakPNmT/1+ceoeLgTRD9WL7/6lrM +bYFCxVfNNYAUqkxo52bV/TBVfLvgsrNyhfpPGJ3pqOP5IaWzMcoi+hyrFv4LZYdL +8r7SNvkuX7z+WeYzPbj3gPg5lS9YjFYOuvr63DMPzsWng4MZqVSxiB/BAvGAvJwd +bDPqmHjL4cctxQOV4yPoHEacR8S6Ajg1tAdc0Xn2LFcErLnsmNz0nuBACYNb8cOE +v6UD12ovs1jLXK2KY0QcrQ23lJi/9awpoLgPcbwRFplUnXsNrsOMl9n+0CquSXAq +uoynmWYGvDgFE5TT6V2IVTTasPToxb32+TegfiLFI+rwaQPFgP11whxVrs2a07yw +QWT5P7UgX6t6veedw6udqzDJ/kRYjZhoV7aUfj05stRGIsRxzahRhRoybRG0RCBO +rYILg40pKIvAlN/wwpxKwj3tdgsAxkdCfatYJGH9HP5DLYS14tgHwyt9/7xbl6d9 +aswj1gBK0cciy2i+tBp5UoMKrIa3dijYY2IGLca7thXz3WfWYd9qEYa+rkbjvj3H +vuF6tnNlg9n893Xx1nHA016TyUzwyOGhIXvctKTpoPP59AC9WCG6GbFoZHG6uyR+ +d5T0eTBApbUxO814wEOL+Ux9BvQHEimuUsv8ib9WYPGr4A4f+vI/4MOKNRro7tRv +eVfIIemXwYX691Yum+d6ndVcUPOTZw9i952fwX2NdyNimk8svWO4fXI9IlU9FEy3 +El//1bJgxxE670Y2/3uKhziNu2b3YEEyTh5i0geiFJQjFggIQVCgYhS2KhkBxJJ5 +p+4NI3s/i8H6TMn7gHxmh8gf2QhcdIXoBypj554vDuqKS8JZd4uyhyNmSCex/4m3 +Xho4utrGZDoo1ZOrQvPLQ+fZxdcpqPVjdI5p2vpKXeiJAhwEEAECAAYFAks9za0A +CgkQ7Thz9dMmJyJhbA//cEIpU0T4dqP8fkYpff4cuAbOmgDlQtH8BlJVUYDAXaL+ +TTwx3sdjPbj5lIsl+6vVEHQRgftgZy2TMTnrz5OZdDKmxqAc3ae4qr4yjPZNzqBg +7FalLsUYve0KGqOM5WhOWOq25MRyjD3IEARuMpc0SRmggNZj2Pke/bByvs/EJUx0 +KAktsWHDQYPkutsh809lbpTLPUsc1cpw394gj7EESKv+jWe0rOlB3TuWexrD7RgH +RenDYa7lJIFZdtWX1vYAhJJVzeq5D8nMffTvppXgAHQPmNwK7Ys6i7mYChvp/k2i +At2BDISVEtssnP+joZjrAE+8HXqlDFXnR+Y6YjW5i/+/sKbdySe6ZZEgvlmw/4vD +zZXGabwj4WFRrmR2bLKcsrut+VtnVIlTZ0QqV/UcVcqBp/4vuW6Dq68NVdbXKe8+ +cAXFQHTfbpXQ8G0PTcvMsrQBMkfFBeYvi96UvQIbVF/dxW43eyQR8El8E3Z+ECNO +2GAkI7wNAU5HcfL9HNyw1X3nMx/NZ2qgaMnQdUzVgEk56/d0ub4TyE2mdNvINa4C +DvsHWjjz5QOqdA/2qZDKv6qpea/ZHAE8gt0fqSVY8rTJYhjvuO6CMeU2BDPp4YNU ++iQzMnAE1se0DeX32t5Ry+dS2DrrAXQK5Q56vZfbIUdsmzB4Fxis/lDt+L0lKquJ +AhwEEAEKAAYFAkslSR0ACgkQMXxt+Dx3Bc9pwA/8C6q6iGBCgNEHz0R6x9GAhmgU +ib7Gqu5XajASaH9A1Zd0sT4gBcFxdY3boUeDU9nr8a+wTPRCN4K7RZJVL4RzWnyf +HNBVzFlck996FztO1gsR4yS2NcuMxGTc7fc8I0s216nr2pwJmP4HzF8QVeLGsCbm +MIfylKXCriqaoBAcf+jRBGzQrqn0U3SqQfzkD/rGXKpDkZgS2ynI230JWKWqemsL +EhODDEWaz+nSOko3pASPNs4RbL4g21sFqJjy7u+BIKnmdwQkxVLBMj7MHRftnP8/ +JyJOlO0TgmoX3pB4QVC+xVz34S2Sfgeo5M+YtPkRl4DEBe5F2K9rEQt3XBBW79qz +aogtawbhjFpbKy1Hkp9CaoWmGg6bJV2NyXj2CWjrWeLwIlWgWDemYLiX90zlwTby +ubUQI8/2O52f3QFtO5G6Dap593ReYC8ZKQOPvbhkEKaipJbSAYwakNQAhqGZXnYh +FVwpdOFOnO3uwv+1JrUJgobXLGx1WX4BZwgZBqvGZXHnFkAaH4lzinwdHBNCgvTc +j3d3WpPoVUgpxxbd2pmYL5iT9jTnBviH+VmxDObgikWMMWbLIl2cCcwhhUB3SFe/ +emyKG27nwY/BAneez9qeWBrm9YtcQpONXRb8ct3N6mV9fh3SAKUBJ/8YQeFtrhm7 +fLyM7J6FDUikqqm5X0uIRgQQEQIABgUCTc6p/QAKCRALA65uTiRRfGzaAJ9zSN2x +ZS8+lr72Dy3ui3w4YiFalACgrGvpN1BuW0jAjqqQwZAVg+IqM/CIygQQEQIAigUC +TNK0Qx8cSmltIEphZ2llbHNraSA8amltQGFwYWNoZS5vcmc+IBxKaW0gSmFnaWVs +c2tpIDxqaW1AamFndU5FVC5jb20+HxxKaW0gSmFnaWVsc2tpIDxqaW1AamltamFn +LmNvbT4iHEppbSBKYWdpZWxza2kgPGppbWpAY292YWxlbnQubmV0PgAKCRCLOmAf +CMl15XBxAKDZ5PuM6hG8AHDNZaG+xyUOO00QZwCgkuvUBDrrb78TZweYttGPXB5P +7B6JARwEEAECAAYFAkwjU4gACgkQzl51YrnSm9IDQAgA0A7zvnzcxbG8298qiUWG +wnl/vH+ZqA7RkBQlyjvZuB8MadKWK6kxq6sSgyttOelw2jBpZ57LXl+9C/8s09/R +kfWpgQJK9J7oPm8dXiJjwuTgkr8dxQIuFLgiLHvwVUR9tPHw7xr/w8LaZiTvHqop +MMVfhV+TMB8EoR7G40PnaVlmahy2hrOJK2VkYNIoaKAWmApGAcZInM+aT/BWth7X +Ya1QxGxr1QDerM6XiygfdjRKJgrTPrxCBrRZ5ooYOnH4xxwqiTlWnesvmzxA+ipM +FPFpzU3NWIVqeFrb0hDSVE+jGoE8Tr5bujy2rHrWkcGmFYt2Mis316+6/3MVXBzn +lokCGwQQAQoABgUCS87t3AAKCRD46bjF0BjmsT6TD/dk0AS53pTGh2onWjpKJUOJ +XIDlSq99wY1A4cE8sNDYRnAlOWjsYocN2ds0u0vcc84q4DpnwGE40iaRaeD0ik1p +gBFexl0OTmYBkhzc/6TxS2EXQ7eQBl2a47cOXU15jtRHkfTJW1wddzXCby5mjUa5 +FH6RYxkK7s/2ZlAFpPpDj5Lh0dYezvEYaNk7tFPhl9DJnRKYi00XGFcypyU8eeEu +GQ6YItx0iccFVc82On5M+1YW4dg1aViXrXwTHcmWkfz5r0WVzRvAS99hwWcNmu8U +XfolKeqtO/VOU2aUmRRd9XiJMeJ0vmJZBwFRbSH5gLKbiMEAxOHb5tIViP/EoQkI +ZPwke7bLD8QefkwFW29c+U9bX5cIoLh8SEGCXIkrB+FrO/++VWN7Yt+SBtcA98rp +ejVgbEu/laV7rXmpq6+7EUlaUgjk99ddNHqvKTSIbAcfHm7sWCQm5hcLSDEXUBp3 +wimuKJbO/gu6Kbf3RVBzcIr3zz9M+tFBzp9UHHbraA2J1+gP4PickU/lBbvd3S+0 +E9X/vhInCPHlbZCJDilfa5xcUW1AL36OutPRO23PRLBqhEVlyDuUxG7gaHsm2QjF +x8hlx4ZTtfN91wl1KWmhi6Um6uZOselNTsxouRoel868GsbN+2HS+/oHGZt87DEm +bZSCGvlLuLM4OyZQGfQMiQIcBBABAgAGBQJNzqqEAAoJEDxcCZ7pYzSqXHYQAKVu +x81368OzRdnOkce14esVtGj77lEdlDt3HYJgCBo3D6QiEzTHOnQB5AtKpOq73rqx +XqljAOBHt4x07ibL5HoTulmfxRsFdFOCXqYtnkBNVAF6nrotCBcKkSnNFhfSqqMr +eC8ifKjkzbWHi5Q1ERsXnKzZXht50EyMj9Urw/WAM+kFyDLTKqVIUueKQVSQrv6v +uFIUBNQLKmqVvf9OSn67jqM6jCidwH95BLFrJHDUtxTpJq4GkfK4qN3sVBjExCRY +kNfAKKLlewKILGl7Xc+TYZuRQR8/MM6VZ2lMnphL53YgVT8KuWPYhJnk/8Rt3cBS +vuxXDd5w02xHucoqPH7X3UjEJ0fhPlmIxAErJWKojRy/aja7S0jjvKPX9AIh3DO3 +dgTAVdWnL/ur5qq29ak0PI6xMX03mhx+oNW61n6N1opSKsdgdcwWO7iA9trOtHhL +7X0zm4I0zBCEB+fjpxipX7XwD8GXhG7r/ohHl8iaD6VVf81kPeol+ecmFrfm6s9F +TIlQ2gVI7ZC0IWq/VmX2pD3G7LlufOL2lz9fEH+Sn8im/XpV2kMreno79Cy0mQmX +UAkSoogiWxDt33T8uXANC78wlixmPy1xVhLy1/5G1ICYbzgE6Ce1D13TGnXar+OU +87hP96ppmbYEgwQZs3dAcbxpgeyisv/A/p1jcnkdiQIcBBABCgAGBQJMdpNqAAoJ +EOE2CIoYJL3BZZcP/3un/2Cs1CwPe47u0wOtHuKEqnYCzarpy5dw3ucIh+3PFeul +nVcaFmP516FFlQSsFVtWf2Gnvqz0fPMoqiXR1wyAGN9venc70hvo33MJ/K2ATG2S +ttNgVAGMmxRsNsYPhrx7jv8ud7Pbld5ZTKn8eQP0qIeMzDECP2yQ+WpT8R31SWAB +N4i4y0ivyMJpWMndkCXPehSLQcuydufVcOHlvvCBUBaR+6o6qzn5gvZBITHUCZhl +jNMWzFqiAhQyUzTHB7PBfEWd3Jy0eSA8VcY835IgOPnzbn87f8uxKs7sTlff9cHa +Ut8usMClKpZQCHiO8KZ0Ulmcg3Ex20nVBth7+ce3Ggduv3syi2sWnlNtFl9vF0bF +Fx0OdLbJZdiBSYji5WGx1oBYQsCT5SuDArOngaTGJyvPodj1rB+4XwzIqnQpuJZj +A3y/j+C+ur3rEgNUzCdMpNGkN6SIjpbPGOqXosVy8xpkwtoOVocR4dUD18uqjpMD +N8d/NAxd1nBkv3AjQ3wQd/32ROs5JIkH47AI1kJ5JymZKma0qSz4NTweTFp6jMCE +b4AjfsePnV+mUxAaOIu+QApkhKjjKwXqokWfay/maAJvyS7va2ANYEKCrNoNZUL4 +Oh6vNCr0WGrrzcbK1sXSzqYMvf0asYoCbYA6F6gxuqKdNjlLaRb7s/XPvadniQIc +BBABCgAGBQJN0A4UAAoJEAjjDy2TKbhQsXEP/RlUMOvS5sfRvqqYtJiStJEOwNM+ +0z13wVIlA5rNywvGJcUAwdOjRxlZSnyjBe2+nZnFIvFcZw0/36+q/zCorwDD7OiG +axNk/xDvIhUj5u26v0RvjhDsbOwC0f1uddV0SDX4VDy6UNy5BW7YtkP6t0kGxWPN +0Ze269Y9vlWaFxdmNPLEoC6czU+oAjETeJYuHImHc2LAyTvwrw8K92Rk2WTXPXdu +txLTT0uNOQDk38DaiG0V9Yaqwq9+7wI/19QRvVAk4d7d4TBHoS9fngDLw83GC8P7 +qymM94h+bidJSYyQbjfGsNg8VVlx6sQ3V7rECb2uRmH5u+6eOsZBMZNFB7C4xio8 +ON6brHlmtHeIhGHiqd9vwQbhsS0T+Ixvkfblmp9of/EidAWAq/JbPuEJeR6n0P4P +UetKZ3ordZYKdh/bAXjnbrcucglEa7dYPwkIC2k9oLwzEWYayq2UPPS+Dm5vzmNP +ba/1Z/Wv2K+SffyOe2N/QYuSPlSUUYcaBcbY16Lbz3kW656qDGpxoU470Xy+xiTW +fQCfsvwT4HM0AxwVWmaOPp/Lu8lHDOgqZ+DhSTY7LRgXfXe4IVu2lw9t1QUdLKBt +QN+V8r0G5q4pFUzBC0zKck73LNd86ueuNvZp3dCFyEk8P2RPB5TG1rM0IcZc4PKv +tGvMHLLxuyX4IU/8iQJSBBABAgA8BQJM0s2oNRxUaG9tYXMgRHVkemlhayAoQ09E +RSBTSUdOSU5HIEtFWSkgPHRvbWR6QGFwYWNoZS5vcmc+AAoJEOpNytxNyqiPuSIP +/jM/q2a8cHJaFx6aW6vCJHeANgp8N3NKmaQAXj0WleNIG7iskhAb30wDGAvD8O0I +YO+tPRRxdp3QfzafP8vGECOhsKNG2pX9VUwXaLB7w+miRXbxD+yAbbo+4jSqXI4N +AYoR+tschgEVwyQZeMw0sntLPsQzRDMs1HXli/a5YZeUvQHu5HZlKmxRkZyAH9la +/nAzNWZH7n/+vLfUvwe1/susA2NubsGiHK4H62gsfXeU2ns5wcj/RM642KpQl3uy +hv593dx3IB/ze42jCbkVKcDorYOGEPccbLm1IQRTQxwoje7xpdpFnkNLVh51SnAh +02PC+pzvcMd4TtY2A2SWyEA3Jp/yvJsTd6vdSMR1VcJBvfyCN41b6cCiciKgDOMD +xLx1+0c6A7ZUqqgivMEalZsEQPJaD6Kn9X0jjh+aJR3Bhx/LGOSJUHa72gizLrv5 +hLVCA0PuiaP0dHHYiuch6OuZgwww3DUhsYQOKmXvOqoIztGSjVTAB8D9pbbxlyuF +Hih1hbg7T5q2tLTxb5JrlW4G4+0AoOybe4gEkPubf9JdzA2RuQD5vN7krpmiVlgJ +PDaG9pYUE+FqwiJsuDH2xyjNksZtCFyNbSAexPFY4GbuN8TMveOh/1FUspUTERp7 +hF91WIg93+AIzyIpQc1ryL1cnAzBF4uQ62T4mUR9JNIWiQJaBBABAgBEBQJM0rjY +PRxBbnRvaW5lIExldnktTGFtYmVydCAoQ09ERSBTSUdOSU5HIEtFWSkgPGFudG9p +bmVAYXBhY2hlLm9yZz4ACgkQXvrZ/oKn+81T6hAAmePBc4tVmcPvBl3iAiPD0UKa +bMa3tVYWLh9LDkaBnQOXiPHYfZCpACfnXfZLCg469xEEVrgpRl2aJHjEypm6txg7 +kGKcLkozKLxRRw52LYVuYO99CYXoTdmTZpsoC/CrkXpJXPl153zY80eZ/P4XNrxY +p8Pys5VrpvRibjURphwhDGTEHJKel6/so5mI7axuRxgd3fBUs9wUmTTB3h2cN0hK +mysMFmYjn5vG+WdJWmB4wRPDwz6pMRGKX9lVfdgs9RwH2YoeY1zCc1CXw2I8ox+X +KsD5OLyowCnGRftnPylTCqWx0LXko1xF7nY3+CN4P/ghh+0BRUiiVzT0pbzdhEVc +S7cXOLm99wXrDRzVcgrp4TGkaJSRjP7WWQ1KHsAI1lZLQyhOEdYex32+U+66bCjp +FiiBPpX2zdu/lXpFUIwmjNjEbXb7rK0Uk3lToF/QrNTaPnttrDFQNTOmGp/4+PmI +Acz2PkGcb+Osr6k1mx3f9p87saZMaGZXx9Vl+60w1d4PSJRqvawqHpAyMYEvgzjx +c3EtcnwlXI07qk8nmCl1XudlS5KHeJ5axWLEDNJCvEuJGy4moaFllwmTUtxDzRK/ +zG6QSJEPfH8VZiEl6HBpgzlBB5aToxc+Wy7UpGzwaNrA6KvwSaB+jSxrZYtHbwBq +2U3ttmamynBF1izPuOeJAn4EEAECAGgFAksM3mcgHFNhbmRlciBUZW1tZSA8c2Fu +ZGVyQHRlbW1lLm5ldD4iHFNhbmRlciBUZW1tZSA8c2N0ZW1tZUBhcGFjaGUub3Jn +Ph0cU2FuZGVyIFRlbW1lIDxzYW5kZXJAbWUuY29tPgAKCRCbuGOw9Ru4igbUD/9+ ++F4uqkO+F6g1QNiyFM47K1NaFLga/lNp72mOOIlZMORXpBJGeL9DhbDvN5KIQqR4 +I5wCONGtzeU8P7M+uLapFZmofROABlACUeVhOPx8fOCeNz9xU4f33dKJJnAsIfis +SMMyJR9Xz/IbNWvM6Kb7hU+L86G2cZPD8uvZ7oHyY4wZWtjTHM0ne/usodJffxIj +oQFhn0vAC8hBNWStI8UUih+n+iqvvXA2cB9v3Ni3VjQOGLKSZ5Ke2jpKdYTl5zST +frHfjgjGvIdkWz0hbVMzL+urU9oIChBK6Nr3DxY/XvO8PmFGh7V+M9C/gahLCrNz +fD3vKGi7OYkp6dODPnREpOSZ7elGO5mGQnSosqzrLb7kw/vEobBbxsYDwxxl+ONZ +8yxUaBpvRIrwqNEwb1YxlMCLnpRs7TJxxitq+zF7F0MxB5sqdLHSNqxcfbxpTflz +Tn/H9h34aF7f4Qkn276XKIomhge4su7jxHNH/G9gTk6oicdalBFRMFwBRpaOivno +FmSGa1Yt7C1mBBQkRXjw52ZRx8MHKnccNNgpk5Xc7+VT9nqmoulq1JfzLsrtu4D1 +TaleWOVN4X4FYk3S2VWxLV5dhmuCy7hP0ZsXOTBjLXC5wMxJcp7I3IlIx39a6U7O +vhWbguPxyXR3AjBUsMRfO0pGZKXxDI3N+yHOWAtDcokCkwQQAQIAfQUCTNK0KDUc +SmltIEphZ2llbHNraSAoUmVsZWFzZSBTaWduaW5nIEtleSkgPGppbUBhcGFjaGUu +b3JnPiAcSmltIEphZ2llbHNraSA8amltQGphZ3VORVQuY29tPh8cSmltIEphZ2ll +bHNraSA8amltQGppbWphZy5jb20+AAoJEDTqduZ5FIWo/wEP/immECQXMIMQ9zj7 +4RU2R6YV7Slzc3YATfjwOgOwzQ3qF+UTSHui7eiYoHOwRK0OTKW6fe7bh9/XeWLB +sglUl+RNuvCNHAQNg9/p7x9mJL05e3OnGpVLVnq/yQezCWo1GWIPxodwmoyI2C3S +iUapfKN1q0Ml6nwZOyHsgNCFYDVhmtEnIcK7/PXLpeYTpTBuXETZZQY++XyZDiVI +le2W3uxrY0k8zVPvGSXGIh2iHtAuCrJoNKY+webEe5QVNoAYnNyu5de+50gpQOr+ +wYsvuf7S1MAPAX2L48U/JvpBfDikZR8qujvCDAAe0bnom/Ov/eWtnONi4hpsD4T3 +kcElxWOph3mEm1YUksGmoWE0FvE6o7r04XF+VrIA/YwcgC5wGgGmYFU4DR384hMZ +1EMAsd8cKdYBja/PA4BTcA6Vc0uJVFaB8o117m7r7sHKRsD7CHbbKcK8ZtRNDQa3 +SzTMqLhsRDRl5wp9Bwq6l++BKPpvEnpbs8SdW+TxrxpM/0aIhQ6ZS5RKJvhZK3Ap +3cgYBVW23CwPtVc+40/t8XqBRRlzbLdfWhEC+FU4f9RHS+DQRFSxl/xXeLtDBH75 +1WbhkRIRTktQxapEDuRFfd02Oeo356/r0TYUQHL4bwYEbTBm+HO1KJn64BCaUMOU +pW881K8SHQaHMr1iiQUpvMkTcofRiQKpBBABAgCTBQJM0uOxIBxTYW5kZXIgVGVt +bWUgPHNhbmRlckB0ZW1tZS5uZXQ+IhxTYW5kZXIgVGVtbWUgPHNjdGVtbWVAYXBh +Y2hlLm9yZz4dHFNhbmRlciBUZW1tZSA8c2FuZGVyQG1lLmNvbT4qHFNhbmRlciBU +ZW1tZSA8c2N0ZW1tZUBrZXlzaW50aGVjbG91ZC5jb20+AAoJEJu4Y7D1G7iKG0YP +/3AweZFnchITlhDdEkozl8M89VZGTBqfU6Rg7mNxL/HDnJRtBBGXmSQ5NLygX+kS +PKci3KRbjbeVEmEcWs/ZfzxN4gR+6y8ohUOaA0UDrZYctYkzG1d7HbkcM1lmvtde +Gelj1j/+eu4LRu5h9oczkqRnWV5xnqz7xsEH9Rrcgm39RpNzFf43Joo25iqdVGk4 +yBVjoHQsnD1qZwdZQ0EHxYozNkGfa6j7VXAzCYVstfRMPGKrYH/xRg2jNy750z/p +apgT3GXbpvcB9Z3gj2LRn+FKLxoH2WLf2DabdzyZz9KXfnG4ZeOd2BrFXexKT2MQ +TqJIzIAmXU2XgXK2fHeofqTcKD63qgbg2EBg2m4/RvRgO81kCuO6Sq5UVhtyYSk6 +X5O/Z3O4WEtyp2bePWC6tP3x/5ks8OlUDF7Rw2AOUCyU96aa/C3MwrV/cJrbESbo +ZJCnSsn0DMj+96J0qsm49N6nj9xo1VPWE1kfl2+K2uW89IBun5/pTDE5C3m9Qvff +HSTMDY37bVwfoCzfNiOUa+BDQLM7gkjATzbmvY6d4UMnXjQ9GKp/v2Nsz+FV8HJ8 +RQI8yvPLI8hbleX0EJpvrUsLllZR76/IWxoNy7J1KiYD7yuqSoa3clCTPw8EVaAF +q5g64uFMHAADsFSWaID8umypkhH0imS8J4XwKPJfnCroiEYEEBEKAAYFAkzSGcIA +CgkQkdPrePiuutNirgCZAZorRS7z/JqyPwb2a4NgRrSSWXkAoIQxA4OqQQNgmJ/9 +rHQqbuJxkfwBiEYEEhECAAYFAlD3wYIACgkQn5i5pTzaZheV6wCfTLPa0d7TcY6Y +9AdwJfDJXqHDUCkAoJzDpnJgn6VIpXnaSQVAFbxre+VFiQEcBBABAgAGBQJPHwj7 +AAoJEKv3lK+rsTuD3AgH/2YNKl+dNEj/vzvQmvsCQTWXf3OycOiMVKj6nwoEGNzC +/1QTXdTZy+8ZaOg+dLoGnHMuj0TdwgjGPiN5uIWeLss9FIr2BkCJusi0sCINDTA+ +l0qduOY+mhvkw0xJ0SueR8Qn+PK7rxQwLVsScyw5n0mrPyt0ws4BZTiVdXglJxvQ +gseXjSGhgpwPKB7OKlpqvu7wyXe8Nnbxj369yJqZbyDZ6I6Sy4FjhBQGN+woRTyV +LlOoHLaIdstQZ6onUc+LpQj9mZuKt9L7AYJS7FBXRnLLeBdN7sghzotI+/SOv3dz +yX1Q1C7OgItTzgMqtfJKpRlnD7FtOhO4XYg+Sonzs0SJAhwEEAECAAYFAk3T3jsA +CgkQLAdUss4GkvPLsA/+MO159xG0ro1RUswS6xJ3xQms60TNsCuXE/Ty3EXWUV/t +KRcP8sHOcqaGkxwx0BHbUWCHiau7u34IRS6sacHHDcNfZDBpkHbqz+/1uTfODsKT +c0wje0PWWO+XUZTgYUTvcg9cmWqCvkEBoaXhoeYnse0E5bUO94h2gvxzMzs50PV7 ++VBK5VAI7o8b+RQ+pO2Roh8AoE8NNAzMLzZ2kg8lPhd0ibtOxR1zq5RVedLKX+hN +/k+Met/PbJb1CqzccFn6VAmrx76zJPwWO4faicU7UC6v07knmEPfBPyHMo3VdRQj +fAOCt4gmD6/29HuzgLgMNQ/jsVgxTSBymormd2LRUPLMpN+QcB+dZqYaSFi0JSA4 +wmWeIqrXT1PDIWL8XzlEelgZIape6gZcXl9/MpI6BC5IpJFDMYcRHBtSw2UO3jXr +uSilM7ly27FJViWtClIn0ZwjC3MHRExkOkc5uiNbR0DitgDIoIhe673y3NV9VphU +oY4vBFyYM9Q3P8RRS/QWqNw6zdWi2NZHLh9g8CMgxxSirSCmk5oVYSeYczL8v2bq +ceOLEUtkcM7wLQ5Vp6ALT1dLoxdIk2EzZ2Dg8kdjxTvh7XbApxe4AQ4CDPRO6Wef +gOgZJ95xVbKOZtGwpVsz7CJiK38k/Nuv7U7SXhBN5gzSn8Zi+v8UMhhrUn1ffpmJ +AhwEEAECAAYFAk3g95sACgkQGFJGyIKJNBe0ww//T5mReFaaKxP6Rt2rp8NTLkRO +lUk7dIrtljMJ54wGAlFk1CEVM2z68iiJXvHebHYOtj7pkq0kj85WzgZ7vjjXL8cW +HDiMY30fYLmzPj5Wu+qJyWmoo33F0pe6bTee0d5SyAaJ5O8UQ2PoXwJfFghp8iCt +jDhwqXMaz1mXOeFP2gVZD3lp32SrCpmBHwsTn8KsVdtmsY1FmFeIoEZCFTj7ct7I +wpjvCZ1GIBLh5iWECiW7uB31IjK5txCxQvs7Vzb7No4wvzxzDLKbF2O5iht+Zf9e +d+JVWnjf4limSDoZI9zo0e1bFwf4ud85UIsDa6kw0rc+VyiUu35+qn4/hg742pEv +c5BRTFfpQGVJno7QlqxWxYUAGDi/axHh279UGu5L0r/0sCnte/YDPupnIvM5cdei +5dpliN6MjZMLF9efLsP40dSUSAFXwMDutISXktWqSpnaSyqVxFgfieLRKCdXeUO5 +6LOBPD6ahw5SJIpr8H5Q3PA0/s7ipH85VLtgmxq4QCkXDdxFG9+Aj7RNjdlmKoBV +F6kPHgQtnYL9wdlvyk+G65FirZKD17N45UqU5Jpe+4fjQVg8rABPYn6f9iBsxrNa +yLECEn1WPjILwUp+afIaB0/Dl6leF/IjtmKQw2geJf2i6J0YbPn8F10c+9r71k+e +23A58eKO0fm2tHrX/v6JAhwEEAEIAAYFAk3iiCsACgkQQPReIWFFn3qvPA/9EDue +G9lfEJgz6BItHbVHC+CwKrBDqORayojJdgJPk7ugUO62iEITA54aClc2q/YK8wL4 ++AhmnCdp/8cQaC/KdyEQt+SHHh0faqnXcdTDPdXCkb4iwhssue+sUyBTAVo1Jwal +HjYGXqcy9AeuSXjnOIWCHeSoy4EMrfUngyTSIRJPgMh6xvDF7QIQx2mD3UPTrIM6 +iQK/TqZgr5VBZwRX+qgv0NERnTRfJoYG45WLrywF3WXa0JSpTjskg4s08U0zkD7r +5l/Rk1Q6teyc9RWEEmxBJhYCpSX/dwdm3cb/pMOMwiQD/m80c3+2r0STGrjjmo0m +5A42CLKWHD/4yL/Iw8e34Fhsu0KJsxZwgM9R2ov0Pm8fnKva0hUS+UPdn4/lEXQr +86RCGHyQrXDg1W70FHxlr+WAaaWHp83MeZ1CnjB3901Sj/L5URn9kHXLqDgo0ZRs +m/3P+S/T7PU9PJM1OWsaTViHsbT8dm1R9/QJq5y+KdcDI9ZlH5teG94bhY0pR935 +JXn5rkm1A7vtDIou4snrE7rg4rEGHFeRUO7NmneKaEH8s/LZmUb5mEh3NoY6NCjk ++z+dUw1IWaavCPZa8EN1PSjYudg6wRXOC7GioczLPSzD3tzS7N0rudOme5YGx/lK +U9XRaYMG2VSGJCuS/imSOkoAyOkMwelPH4GGAq6JAhwEEgEIAAYFAk8IouwACgkQ +xodfNUHO/eBW6RAAio14ecYMboqD3oJgS6RYd0wxPm2pfvyvi9NEYGdqYwZi8feo +GnZE4NeXeYZyHVR+5GHt6XIzwzUOvvZ4J0VN5AA9xnvtIrypScM/Z6OZzDLfq+PE +LNuWePT8XAJYFRFP/ic0y3TPdPixww7ZQTOjgBXgIgDAHCbJ+fAhhi70MkCpTfd5 +AEPJU4PhoXxckhdIx4DorvMlI87RpwZbMaKGDZxWew0eedUpPoGoCjzZ1Gwso7Fd +nCu5deIttEVP+wStiGahMn4VAWI0zjuUU8EpWW0vZ9dpiGQtJReiBzvH7NTYNTCG +MpodlqV9TR3HG2WYjAw22u7wWhat1HB+WIED5hzNI+x8NbnH2IT+3nISvnApzHMz +nIZTrIYyYCatZ3mPr18R/eUUoHjaJSo+IyfokqD5lvSnvogLlOjkPXF0HECBz6pF +SuEZWxw/Y9b0DEofqSdopqYpssxhSMgsbfk9wiAVuO0oAZ187cxlsaeYnxZzVM9z +q0SfwJ3PpiafUvVLTHPC8PWNNRsbsOWZwlhWr4l6LJ8G2owSFODk6+GZ3oHfzRsX +ApqBV5Q78094HusVdcN9eehnnPJREgxVRGmESzHiv9cbV3xXnHVcBlhtYmboUEVz +k1Yg342KeaW5/AWR6xsTAZRE1aJdNcghtYrQj5Fd99XfsN8RUGJ7H8H6r0eJBBwE +EAEKAAYFAk51z2QACgkQ7bjAgqbuaQhjDR/7Bdipk4r53BrRQS3XTzRGVl35RdE5 +0vUc/11pMHkpg6/1HQrVY2msNBDY+R2tUzuvdH1Z1J0Xc0yBQ2WLWW+pORYnnx2c +hVrrVDFTrtwxkvzS98K9ZJ3q8glSYWEh9KgwvUjCcoKE4FO2fy57ZLYKNuzY9PC5 +euEHUR6jFluGPHGD3CweUc49GN+6nfSXnrIzaLHRKanNc2Vv4nIMTXzqoXd3BeHp +8869EpD6oFD0eB0CuS22AuOdYhtPB24ajTbZv+jSHYlUlKZtmFqG+Jba8tmxV73d +j1xlLrtisrQtrQMmaFsK4sWdrI2FasfvjB40x0u1McPf+eH8YiVYqL9pshbDAR6b +qbVazhTGDtEu80WwSTU8C+cuy9QvE4hClhxLBdGFG4/o3ANvsQZz0CetYBUojJEg +6q7K8BoVZ3My2S8UWO23warFkS1UWanpSPu5GD8qaGiMVAaG6gML3lMKb/PbAldl +cBhqSIpB/3gpfuGrE/z+N4r2nQbXAI2G8+4YgTahsuj0775GtzGpZRAZxSsbZDim +l4n6KfNeET2Zx4vY+kbAX4IxyP/fytk6cqBPrG8e/hdVcMl5MdHSVIfX9Z6KzV6V +drED83LNUCrCt7YQGyu7JcBLk4ytXUow+MxnOOpHKaBiq56wmlO5mwfA3UexysI5 +hxogAVm+w9SoB1hPuZKUyfOyd3xP3Ao2p5LVhF84NIgroNZ3HhxZGnb8xMSNjR4J +eeiYJ0Yl0j3O+xWYfrLOvY1GxEevMQbDgMw6WDWaevC4qsp7co/wxSZLi7f2gNwR +i2YPX2Kg3jKoCBuM0HRCZplnZATq7kCROcZKGvm9o0zGoUNKzh3yqU4A3hWOc4kj +TYrqHT5o1yAOFJtGvFixPmZ0L3c4+U4iVt1Pmo5i3AQKq8/qi9OU0F91Rzkf1Saz +zy+lGqFR+RFfoXIinsoTHbZFbCTP0yfW64f93tk7dSTp4M3Z08MSJfnyS7SRH8iR +isJbfJ62DZgA3gc95Sc79qb1p9WAq/JPzNLQaaq92Th823sVCZ0k5EpqV5vLBOtA +7zKns1ByNsTr1EGGnYyLXcJxgxTtP29kHs7Ia2ZKV3QYFgPdP4QseQdvntYkMN1f +cLs1Lv3JiMp96RMSywSF0tp9iMbVJPUu8uBvU4zXlrVAnUFNKcrORxRIkFDp7WAO +LqkI959Mc8Eerb9CWc/Axh94Rla194Zfpp1ExY5KOCnATGEMP1NU8CVCecVukn/f +yzRzNfCO3VbI6qvgKHCmCPwdXQAsHJiCtNgO70yp0zivQWNGtuiU5ar3lfX5neih +UFTZz1j3yM3EHySQIXHZR3OTbTmVgTZ+7Mp6c2AxuOGAdUQ9Cj5DhkOJuokBHAQQ +AQgABgUCV4TFlgAKCRD0ziNk+2lnsgQPB/0c27VXpwGNZTc7adptXsJAuPgNTIcz +upNwovUnJqC5OZnTyF3F0bQw051voZyoS+MuPk+hSG3pPuVFWhmszQaB+9S9yjrD +Yj4UPeucAkYQGNGwTRe1e7lRrmWguEGSUbHxAGLR3VuFgxeY3Tbera2jyI2k5Oxc +uV7xp9zmNCBvje3nzaIG69fHq+vkemMmdmYusrorZ4kXNmM+vNFIzJWxv0KRb4mm +fDaWGQdH2c7MnqMfAjkTYW70Nm4DcI+8XNsyaQvodkmUKONvjCHneoed+0OS/5MU +V+HLxzoX8bLRw2YIR4GxhQaxixkPgvyQWtIcWpsx8NH3o/PVsdPTQtwPiQIcBBAB +AgAGBQJVBs+xAAoJECS2Qt9ZftCVMtEP/15sUVyYjDBlB6+3WlSLuKp5LXr3L1Vb +YFqA4LfNGzDdY9C4XrxE6G1ZdCK/mYqfFeZjijw7ZoFr+O2GSW1/rr/KOrZu8rtx +J1YWjyds3ebBqS7thBMPomgJpqODUTBSxNxpeDMoOT7DYn1b9mQUrKVlqZnfl+R3 +zBCu+sj5U10b9duEOu3w15AHYSxZwFRATMwssGOzpeH46U1YgGiUZpyVF6WfWUuH +GaYkOdS143H4FehIYxiQIoozeSstFACQNKmJ9bOl9cN8qEcVbXCt+6p9sBQoZnYX +znOwLWnylwdzc5ch6RzBoCxYTthUNOAWVPFiK6OpJwtxUZAJG954o3cYIj9fRP3Q +Ie3gplu8oVF3GJBgUnWZSxsEYddxmWLc2/pTtZdIkoT49eflOslfH99ySXWQh/TQ +878FBwugCifQazivlYHY8WmJ6/xRsLXPD5H5vH0dzxZmnDI7UEshyPczYHNYXdsC +QIYstm/zjfm7CmoOR0oPUUIHwpadeaVPh6sbPXq9VU09OcEOibfdWNvsg07MWbuj +knmIV9+zoBUMj76vpBE776/aXoaAeNWUgK05CUMDLOekICevzu81iVxXYHl+46+H +RGZ7XkvOpkkgjbg+M9xId8q3PjzmGYQYr+Lg+6h3TodkGCQdAPYWCwJh0ZzZE3WZ +RPx3Ae/oX7HOiQIcBBMBCgAGBQJUaz58AAoJECsRil+hXzC5S0gP/Ri70XuaPUp/ +/CXmpwoTdIksSCtbXf/RCMYvpRcWlTctLaJVPTHtzMO5zoRvN1M5P6AEZfPcckCo +HlnbHNJFHxoRaGsKTjLUu2r9FzFZ/3QGOOXemMte1B+33nKiRaW+yVlNht2+ZuQa +JB/X1Ieg1n1qkMV3z6jDEkDuDL/w3bCLmc5/fRWYRK79N7rsJmYqIkZj0kBjQGGz +ElDctTq+6yhKHv9pKHbR84c5Yf144TxR+8rMayv6zaBXCYbso1HdYWIRba2Z0yRB +212YMt4kDk1uxVW6Fc8Jn0JWunHF1pbdkvbQBYUxs57kSp54cEUtdMZtowKN/SMU +UCwTcoEDHt2r6P6eOJC1TkOjtHKRNLsjzTmP2398APhXyisr206XCOGDeDO3wODj +pIFkr2gTjGRQKRNy8JHvI1+6KCrLg/eSrkVpsXxzzPaZxUjW4GM0o+XU/8QGNhiw +IFccOmjHBNfSE/suDKkULqLDuVW3RFDdiSd/pIUPTAk331SYWHo1Bh8C3ViJQBkE +kL9zflvdlyHp7dlpiLuQrfD3SAPJdw7liPdJ6AlCoCFv63I9MDAnoZgGhsZHEAwL +PUwrReHmBZCrq4gRux0EOuJsoYZVkW918O/4K0wgC8FqYc7/JYgQkLRNEe0nRrI6 +lcLJuqNwH+NwjyqoUe5uXU3SzUGVCmu/iQIiBBMBCgAMBQJXQVf2BYMHhh+AAAoJ +ECbR5vsOCGsNzU0P/1oBRmDNViNgy6S9FGYCmZnkYKyzmvRPBRbK6hDSCdGgLtSB +AdFv6hVPXprzOty9kZzpupMck8ETdNRL6ar007Vkl137iGc+OfKujh4z9F46iLvA +0INcg9Ei4suAO4NsVYAa7jz6AHorx3gFRRsmLR8AGtLdy+EMoXQzZAQGrMUGyNsY +SwMEsdJeO74XczkmfHpLQiFRAZjv+RHmJe6IUfkWF5iYoX7rDfvz+vKhjASED4Z0 +h3xChou9mX5Ujm7KxGdEiRAYp7TRAqxbpw7mHtgYrXa5wSlCVGCDNnhGQ1ke6LS6 +97kAAyuR5clBQF/owH89VDM+7ETFVkGUVAtjiqzpnA3/YGzWXhOABxDASmWKqXpp +uxkTAcH2+uBxPFrWRkItGTdM+vDZtN0VqCI2lfqZ93RkUKmrKvAEFTbyRaMpPCH3 +pTFS0DhbCeRoyqJxKC5Fo1teqeuTvFGIysnuWC0SNgQDjLB15RJkMX+Qfc3mvqvY +qiX9RM1ni3jf7IhnXljXRu2Yg/QpogjWa1R62EVqyX1EpLpz0AYBWrO6EvYyF1tM +99IFLglvKTwKYhcUb00ujhmobZ0kvakrMaLei0OTeTWScFPRtzeXJLN6Iu5MtOvI +k+FVXfqajUM/R1wwmS2jv8l9k6kgz3xCJKieitZEyeBACAcHN4x4LpDD9cJYiQIi +BBMBCgAMBQJYGMcvBYMHhh+AAAoJEKI41NSYIPRnv2sQAJ12YZnfA93fxOPDcHWn +eTVQBnhAfX62f5Mt2lUGs2ejzqHugznnt7LbYVYGl0e4w01bPVwSEd7Og6IBPR6o +wx9CPr/aW4lKlxDSKFKsBSgjnBEZSos3GjPxPsgyvsgfP/8g1zZ2b873lNvB4wK8 +3VAJ17JB3DhtC/ZpIEiQq7EveSoYKqWr7Av+Vl0vohziDEf+x76Gp/Z29BbQp2Ug +DsUkNTks6/WSYEJnmHoyXrSRkqFKUXN/IXqys+PdtEKfZP7hHUdtxEisvjqnYFgD +bUYNAHoMDceYRP0Y5s0YC6RZ+CuKel70pN30RykLDdoEz7xiWw84ayAZZJCqMUJ2 +9S61rSztouJ6HR1ueZtT4BK6v/PkgjHUYUx/QjKOjBdo/oE5toUw9FTk3Cd6LVxr +I11gRJcM8cMpeeeMrotveIXBvOSBbUDalTlBkXYSq/2+VxdzWdEqUptv8KRAZBfU +lHOsft8o4lNxL7koBJNggULk+INAJcg+NNlp8ufPddYPvxhZTModx99SW6LY/YlV +uhoK7kXPMKzX1yILdGm1g2sFirQnAHNntHVwqvFN4/X3mGY4/jn7ebNGQKr/o05m +vu8iIwOqbEpeERgsaazI01a5S2ICydX9eWU63hx5rG0CIXapjUsbV900MELcB34O +RHqn5dLpfevue863JGUxPGMjiQEzBBABCAAdFiEEr3zKM9T0WtSOOvGE/Sshck2M +1IAFAlsXD4sACgkQ/Sshck2M1ICdkwf/Y6gAIBctZSb5VwXgwZ/9WVa8m+otcNna +Bs/Uf2txmblNHMQb1vaTyRkRpfz/YirxwfpJFoNouZQaf+Bbl/5ll8xIE1/UGeGD +GzXgjXxs+EJy1Lac4voAf191dGJrPGQ+sJsq/FUqk9Hij2A0/nxajh96WIIRZjbZ +mtbRBicNmifojIti2Iw3vCtu5kPMYSKPd4NIkQj6vg68wUhVObHPdO9DdpUQz5Z9 +YDu7fdhg0xespNvAhYWvHBBmWatTTNISRg54ueo8kLCCtXU18ho/2fUWHoyjfno3 +PZBOr95NaDRXnEqT9iYT0ewlVflciBBbKrVWWWEt8ke/NLJHluvbtokCMwQQAQgA +HRYhBEW+vuyVCr0Fzw71w1CgTQw7ZRfyBQJbN4eOAAoJEFCgTQw7ZRfyEtAP/jNN +2+Sg9Rb3pCL92moMiUklNXBWgxXWfan2QnbOifcfJ7loZvC6uvFkM3O0Nnn5Hwa4 +lZ9Nkb6kUFKitEi1FVihOrVmdS86oxN27+71rLc82qSUqY1EkmhfIggdh0hqXYoy +GnUjsiL3LMHQxBmNoEsO88Wf73+Nh0G60eOPFABArA7EmAdciwW+1V3BTGNMTl62 +l9vtc2YBT9QQw+avanSNbH1AXcYcYJ2bIGZ+O/C0aeMskW5ixhFFMoTWmleZUnlH +vB78vtgv1RC4VXyxrJppbHr7/sS/x2R/8lOle3HefDhxPm16yYwlNBkjnCQyp+0B +1QfeTrmMEsQcUnEvc+bH/YaNTdqbuHoQx9QSDfDZ77d6xE7owdfa8jwLVFdcULjn +IVHfxNXJGBgnpRJU1xKCGNoxtf8xuuG1g1I7K/niFwOU6dcAzHseZ+rk04VqZnlZ +PcNW5+ec/yZSl4o1Upm1xI0+f5NuEWA4IC5l91kykCbVCxddlX4qsi/C6GuFlkbG +ss8JNnHG/pvqGS3JXIGDe8DRF1lb3QVSxytq2N+qoQmhDU/GLEhwrOpLu0inUknw +msUBa5pVMeFKd24OqSOQ1v80euEt+YOW9/gVAxE+y9iSD2qonNw88ivjQmdOwJaO +GwCpDc2kU9fomfC4xc/A3pA8Sc9bI+213tgoMSQxiQEzBBABCAAdFiEEAP4HRkFI +6sfyXBjniwh32RvyOqoFAly3Ic8ACgkQiwh32RvyOqoxbgf/dPt1DU5O06Y/acuf +PzJgY8Hvc2k6Hy8T9YegGFjSXLjEkFqaHJ5rhIRUPoLPcgzj7+aLy+bEqzIuSMkl +NJR0Wtp+rn5tqY8Vt+5BJYACTPrbnFTBaYO6KEygqOsa4kSLg7mNEe9eO2q5HyJX +dnCnMnqKRsjq6lGax+BVSGDfuCQZhrYgyhykkfrkHvaAet7KE/iqO9av2btFE0yH +jpe73QUiaacQePfuMyO5WBIXqfkk1SPEtHn44elF5KGscdMB+VvCo2aMdBXgMAUL +9/EXP1cimnJp5vbEYF+x2M+bp+NTPTqiwuDb6ENDqDdmCIDjFgqM7EgsnpBcC+Q5 +WNrZLbkCDQRKtA7pARAAwjRJMLOWK6AZm7vO/PV39NOoE5eS8w/x3bd7AKfYgnz4 +LnDvpe1PsW6NVx0zCUMBFX0vkcd0W2i2ERvoVOxbiS0Af+TWggzUbqsOSh8kLSVB +/s6POCKqnzMxvGjknR4Ncq9sSh+EE5oEDjQbv1tMRGZma6Ok42DcJJNqcFytsriJ +mT1DsvpitahfFpt4U7ZDxPhRUjRSGnhw6Expsf9EYrvyu3TSU6wtE5UaZ9iunetM +wed2GE3PtA2Eg8gdBbqV4gMf/lxBp90O3jYtgVesOdL+a+dUD/M6bYhX5THxSjQH +1fMUuTLXkHffGEuaqnfyz6N4EuRxT0Gki9JN0Uwpb+30DR3GRapr9DlqYses5tp6 +WMYarEwxnkmudv7l3oVVxeSbm2BYnzEi6WxlWana5huYa9nMnMbIxYmNMyTmkYrZ +jfyVmzhi4sK3DeLpCjchZ7RRuYz2hZyXcfax38iTXhfXIL/SZWXhcSelqiAIZSjr +h9yvP6ctEjxOmThX0aNGFMb4duSv7IjnDy5utd2jscmO2H0PDBNr4J+yNJgLYPWp +vmBQ2mxqo/N/aHcGXc2b9k9plB58mxUyRQbjFhlimLLWA0unmRJobqWz71CpA7oP +5jvoHaPqUihfWEugzOUbQnUzSauDWWOdMqQW+UUo/iDRz6HCKdlfww0288krLusA +EQEAAYkCHwQYAQoACQUCSrQO6QIbDAAKCRAQwBxaL2BZ5y3tD/4t+KCuXBNi5alB +CExHEzveMdRF9FJrSqJEX0NwGFivF3hQ/HJkrcu9oTJC/tXNFf/+EHOd0lMiyFl5 +PBSlhe4XS988rgapUW+ee9tQmAt+RgP40fdKdJNb6+9NYGmrdnDUzlQtP+h/XBOc +mF0/szK/U0oigg8DjYYUm5gCWXOl9H4LJgg+yOcVCOVa4oTf1sdAmQba1xlMhOIY +BWmEhqbWZpGOS59XvpyNfOQXWu26S8HACBqyPZ2LVV4H+9cmxinTz7RX1yKD17nL +Z/fTOzZ1gYTbhg5rNmFpDgu3nlgU8SpGQ1kd70ZkcudgehsUe1EpPyl7O8qhj5H0 +/3OAmRXzrq2VF17gtz7zpntA0JqsBMbSaK5qBuBcurLhBT634WDIoE8u5Em1Uwjg +TI0Cx/lPxRTbIb4PfjP2b2ik/tJaUbwUrhuZ4LAtGztMVrF4W+qnx9oed4OFXMBb +wgS+SH6oAHlGwpxhhzXBlqZsHXm+w+2oazWUhxFFGEe5U245GEtNf0AznBMDWTqg +0SCVEDjlKt+e9tVXkTpHYWZjGbRZbEHkCbFqKhq0KP5BGInFZTFToI5jjszmuX0W +/yKpRpQZ+GuJnt4VrYSy7TMvjjhIpuhDY57VUwUIkz/2Kq8Vg2wpGg+29nvcGOTd +yZUcTCEB33B2jQ9z0XUEp+6B2F5iZQ== +=4AB7 +-----END PGP PUBLIC KEY BLOCK----- +pub rsa4096 2012-05-08 [SCEA] + 5C3C 5F3E 314C 8662 92F3 59A8 F3AD 5C94 A67F 707E +uid [ultimate] Christopher Schultz +uid [ultimate] Christopher Schultz +uid [ultimate] Christopher Schultz +uid [ultimate] Christopher Schultz +sub rsa4096 2012-05-08 [SEA] + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBE+pgz4BEADd7qAWgqXcNltlB3aow0UneRmNSVjHKgekgs0ZXxG9l50Athks +r/3bL/ygbxFB00JcM9W+UxLhKHiMSyzfeBHn9l9wAlLFKs0S91KXTUnRwGFtvgst +vGROoqPgTVREklnmyW/KpzOwqSrQ5xHcogaT+XWlXmRbtFypi52Z5HGWlFWWgwx0 +vKBWHmQayPtCif0v1RDxfdV9zziodn0TnpfBQsEgf9TDAjkNT8f0ecwTnhSihTDm +1W5HCK7Pm5DfUtree1Oh6Ncz2ljlUO0b3Lai9pX48eZOj7WQXPefkcv2AoUvdELk +QKw3klM5YNXbXPf1KAjky+q4DQ1ydD6LkK+9cI3STeMesTlk/tytOsaN2NH2k87s +EpcumbH0AcmPFEnIYUfm4KzWdKlYA6mbV3Pk3tHSuayyJovjh/7Y7BG9p2l7D60r +49hzrTPG8VxNkSliNLcSjI3QjYpfhSlqmqXyVKzdzirK1HPr1xfJStigRpLP9nWa +rZjoXng9N0etGwtH/8roeDPYA8x9ba1KXy/1g/i+RLx2ms+rueCpnFZxU3GZNUSp +RfpdUbwCN3Zm1w5Z6SI8X2aSnWWeYzU6HMsV+P4PROnFsgxDeOpyWhyEaaVLXQtO +YwcHneHbn56vSG50TkAuHs5kk/3/YDPSsqjsUPOuhKgFMh3iqMTh5DMdSwARAQAB +tDJDaHJpc3RvcGhlciBTY2h1bHR6IDxjaHJpc0BjaHJpc3RvcGhlcnNjaHVsdHou +bmV0PokCOgQTAQgAJAIbLwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAUCT6mETgIZ +AQAKCRDzrVyUpn9wflkxD/9IsahRqHTV/hH5nuPqVO692cQqHvPtMPO6lDb4909V +N5T1i+1hFr80P0KVDL6EI78lDBJ2TThWI0o5vFdmsRlei59wsgTvkKTph5QwwOWl +7OyzUDX3WbKhkNQdGf4I+/g/1s2bHaRoG30ELdL7cwUPCPrW0KQwBy7Rtr0Wbduj +KOw9b/UcgyXEOE1wNcorq/E1o5/6BRYIcFQOO4sjHjGcChOpSg5ms4zbs+Xv3gOt +LrbmOPRTXdvBxwJA6kkfQFHvI42kXYghTdqhBVPnHYPqUeavRsb+Yz3ghkZhj35i +GfaGyXNwFBikCYjzIaj44NOkT1pU50MgIbjSJ+xoHnC20T942kekqp6wzqUM19Pa +9ohsEdA1Sf6/A7RmpZRrxSIY02ZVnGccnVjglnylVcnxrNAZC3ebxCeZPQ09FBR0 +Uqlsrdt7A3hlEP2FaoMTSa+hYqfWBGB7uZhcJZIsZspxm8J0txeOzYNSFDl7mF13 +4ShRsq6dpSugCdcdeSWKliBzq0U8sIabOFLMxM0hbwkn2RG4OaurJLWXQf+7IhA/ +J8TizjkbdxLmR2PiTiVtrx484mpWpbF8po/em0q/reFnL+JtOM6qlJE/Q4B6Pfkc +hhU5vKPfmGw98t9guyw5G8YSR1rR+SOowHg4T/i2Rezz1idKmoFpPdNFRPlOAC+d +67QpQ2hyaXN0b3BoZXIgU2NodWx0eiA8Y3NjaHVsdHpAY2hhZGlzLmNvbT6JAjcE +EwEIACECGy8CHgECF4AFAk+piA0FCwkIBwMFFQoJCAsFFgIDAQAACgkQ861clKZ/ +cH7G3w/9E5VNELFHPVnfyel41FINbXBR0XzP2A2OfFyDIM3HHPm3AT/AMfxMpUc1 +5NJZTYBX8y8/m56fBNp9+Me5HswU6SJ3bQQ16aHtwW4/cXkwShEkzLbFstABXPIv +eQXexyx+4F7EJa1zPqSt4ZMT4QYrybKvrIsnJ8dnyxy1BU5UoZe43vnkK8jxG0j8 +ZiJh9rYKcz+Xg3FeTwBK5laERQ60CldYELSjOD65unrJHmUmgDhrrnzT/8kkP7D0 +ETD40MMvAq4xfTk4QrXbELiMl6I9yfYgssnWBMRk7Gi7zwgG+Vdh7/ysdfqkVQHB +55SdC7akSelReq04becUe2L6TGVkqDKgrgfaoFwvie5hBSBdiB8QyX51wWeVDp8X +a25zRGdLiNvkkezNxEln/eRFZZc6QVuXqflNu+GqCF4EGGtRMsabzUbi+kGI/moG +3+ikyzIXUvmfrlzryv7ViTQ3/qLfMyBGwMrNdqyB66l3TOWJtK8r8C6GCinhaHU2 +OiYeCesG2Sc/Od8qWQbJu3o3vBohjF3cUpz2NUi7wrcT6jQmG8LGFwfpiT2xgpiW +OJkK9eMOym/GJhzFf5ruE5vBtoy46xZFaMknaBDOvqEmMCjbiVslg7cLQUROiW+4 +rRBgIdkOnwLytNqb88dyHPAnfM0fvdG9imhujADohG5RWxqa5MS0KENocmlzdG9w +aGVyIFNjaHVsdHogPHNjaHVsdHpAYXBhY2hlLm9yZz6JAjcEEwEIACEFAk+phHAC +Gy8FCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQ861clKZ/cH4C+g//bLtwxjk7 +oEqfYnLWzPGMzrOayaQzL2mSwrLkTiWbaK/C/Hcv8BPNic9eXYNEZRIinmSjdF0y +qNZDinjAEGAcj1ktIk3bnCZbinYkDf62G5JqCEFlolOZ0QUl8mINfU0g82LwcIq1 +mafRaRDaemyfxxSIdU31Kc7lRUEAIM7Lm+5q2ItsouVmk1x2qkkUYcF1YqndkDpe +lAxVwOI3qYrQuPiSgMlOWIx7u1ds1Izyo28PJ+/9Xm2vy5O1z0QmZCEjFRo0kDyU +tyKOAB9NSIDOWcFOIo6Q3J5L92WrtKxgQQtrvB9b7rgUHr7o7LIzwZUf66wfalB+ +8ieLnJl6HWWEIEDIxDN7Zac21JnXjpUKQR1Pap6pLKXn2tGTaZMtXwudHOQ+1Es/ +dr4KjKZJyszYcp5w27sLy69Hoxe0JDunbiYFOPVkV3YCJ4p9MNoXd1gxhgpSAwmU +WSAH4AdDeV5SYPCNlfnBQS5TJw+lfDuMMPxjDt4caDAOR8yFMOI7s6jbWfSHFq4X +qVRPjoRw6iwSOzudLGu83WLfhLT7bHA3wnYKznHU/zIYwIGi9kFeg2r7HLhdRcXo +oWD2KINPzRJNV4iLLHE26aW9lJiH/N138LBjTEwst/3I+61BpVEm8efED+a3ZoLE +8xWQtwrC1WYev6URqo/W/dXzjkVa8V0jM+i0QENocmlzdG9waGVyIFNjaHVsdHog +PGNocmlzdG9waGVyLnNjaHVsdHpAYWx1bW5pLnJvc2UtaHVsbWFuLmVkdT6JAjcE +EwEIACECGy8CHgECF4AFAk+piBUFCwkIBwMFFQoJCAsFFgIDAQAACgkQ861clKZ/ +cH6ZOBAAuhvgseU3EZjXdCRnJDOJhgpsUbjnTnKHAKc/xxdFlRyJuKIg7zn/ZjND +rycwi5DXXZeeor6FpxaBFu2QbHuAnRkHSGwhU7DS/BpXHcOGtYVptyaPVZ1ARoDD +Wo1n20e5f5lDuBRmszYp4CX6ISgPN0EWVSrsGMP03FXH7DNEd0nqF4O7L82J9ay9 +rIlvDOWBI9hN7MSAT+JXPdbE20ouWaQWkAFxiXgnhLbfXEqLE/T85SKD+QrNKcU8 +msy+liEheAZPBtHJs3LoEn6MAYVbUYD7U4BELBus9VlmkCkR7bn1mSJXdQAWkVMF +AmG6HfbY8p6oZPbKYXkMWJOnOXSG7jWlslvJ4oX8v5omKE8IANRMOaKtxqRQkezn +0i/f892ug5hAGHGUSQ4e+C8fWnyf6ryUzij3sVlI42KcIHRO39kGrzOo3Hv2Y75c +c6xtkkaPiF7+RJd1uegVABUbs7xVdYRCInWwQqQyybwkhUhL6H9hlTTq2E3bf7YW +8Q7fd8j7Yqw7geG7aLgpsqieX5rmNmieFnKln5r7DeRAVu0PnlHAkEsqNmluh3th +KJv1RbOK28SW91425OrRI6G/5DVXRhbsXOFfi6tEAn3kVe2lg/gMu/QsBCfvVx3j +SiyipbhUGZ6GAsyGanTT4Bk0HQ7zjYaevMjFE8M7cTR79O7P25G5Ag0ET6mDPgEQ +AMxCLVac73IGQfn2lFoueJSaAPBp1cVxHjkGPu8JPz+YvRrS9FFVwud49hfOwQ1V +xmpSoFYxFXTWNwssfnSdqLOuYI8XemStfM3Oje2GQWdNFzVPZ08+VyJmcx5+FZnl +2DEKyjv/fNZwjT5VeglXYBoPcJdSI+UofEkk2/JFkqNZ4Mfl1+MiTVG30od9sjD1 +RiO2XKGS5vYHYEzjp7hJHnhrP5T72DftQr/2Lb5D8m0jklknR8SFVOCJKOleyJDN +Wpo3cCn6HoPgi4WbG2O1SwM1bRr2hec+K7SbQn034W2A9MEvGnxAXL7HsNR3bWA0 ++QCaqI3mQRg7mOyQoAC3Nxwst+Gc5PpotasL65uYvwgTLiL48QdjJM4R0DVA9tH8 +3UXKCIMOCSM26mS5TqVRXoQ18yk/4eb4flL+7Okc+YD+YtEZHIIcNNJkQb7jfJqU +Jy7eIWC9tnDYf7ZNCxcXWu2Wj3obu7oOBORRAdCH5tSldfD+yqX7vLYki8+65fvs +e96OLr+G5NWZ4zv4xbMdCixsKlcUdu+mSMLFeFqS2LyeX0dJKqnf0zilxkhANHhs +Vk3m/3zbZGHpkVaGrAdfq6o4f0c9KdtKPHpZKg4ac84azDcxDGJC8kru56Vgnllu +DXetPe/jhvQQizvp3RpgHLqXbuljBI9lJVrEMbgmAj4LABEBAAGJBD4EGAEIAAkF +Ak+pgz4CGy4CKQkQ861clKZ/cH7BXSAEGQEIAAYFAk+pgz4ACgkQHPApP6U8pFju +lg/9F5QBuA+BsM87jn4ewxunJvyNL5gEhCZGOYIUrVuBlG2KLIEv27co/2D1s1Ye +B1jL7gOmGCgrs/wjN4d+HUduV5h6lHTsiTgpzCd6bHb+WLMrYJxHbyDWQC5Q3QV9 +xrH8rYEtCp1YwHPdi3yTsRwLGX+MxN0lNvBTF+4woHqjt2K75B7GfHc84MN0GE9C +dzXGH5H5WwzwVwQxq73VLnmv8Ohde6XLXUXbe4xNyYixathfXoVzkrCMHnFJalWI +gkbsW5Q+HCeoCjJ9MvM5ZVuBsiG3//pjS5KSqDD2J6bQ73BKI5bh7cG5EnxeQMIC +W5uqX7PWtr6RgVZKPnQVxoeP096jWMXhhALCNKbBCbtxGTfXWL/2Tm+vQ1CeksBC +qfy+5VOwvB6C8fzKSrbiTvTkubrgIXQUohqn2jfcz9jcT10sJ3sVStnscv4ebFHr +w7JwRB5ssXuRkUqyIpcooy7ZWIatluxveaaMSBWbiw2/fEfu1UfWJfbjmepEQSZs +6fV9qAfDAx6CnuEBLokcf+fwzcQfItkKE1jfwX5bk0n1aTua340l5WlLhTp6Lga1 +nZm2gXYPuO5uCBhM9dvWXX/sviEnsUOj9emWNaGMWJW0EmhMJNPkyeAdArrzB8Dx +d0gx9i3kcfpitjr8J1bpb49vlnVv6yFYMIcGypgS4+NEflKuMw/+IkcSeTxdcp48 +UaaNyM6f2a2Fqz4LQYNI1GfA3ZQIkRwhRj6XHCvEbwVl5rcxeF0LFd/WjwanBN1u +iHE5yDKw4upp6nmpaX4lKvuERjX0RTfcLYFYmS8vxUVou+pPr6PyZqe/yvSW5Rl+ +PA9iS/RV9MOT15KROGm5mNTgEkFIsKCeJ9WrMcBKJ0mlnmGyO1+SKnb3rxsn2HUp ++FRMsHa2BrVo7FpWfwz6Hz8LCG0FxmOqPsIPZQwJNi9p1U2tOV7sjqKFD8Ciw9Fq +yziMS5nG1b/7YCRtN/7iOIg4rUqH0yp6cxPsXaXxSMsNTG/DzuFiYtIKR5pi1Gxv +ADC0vvMEgxcCAOE10BcvbLl18y7rszC/huYfTKOQ+quR7CHufZYXcJO6BD2SdTVi +3y2r0xDUIOTm0tmE9SmhbJBccYLXFNGgcrQMU45IBkQfFLp15TSXgrkyAFMV8ONT +/d1eCoYoHOXRDhtZBA6KtHQJQBOwGy+lc3PxqPlMIr7VIpd7FwAzsjPT0yYyzZ8n +vtldMsEJ3CdQBurMSAqTys7/KGD+scLqxLipxfAwFhBxME/hW9u+yHb9b/LjBr5b +aXKHu6JRNDvk0VLTBMLRKeIOJvMptaySP8n8F7R5EvEHFQQPS0anFhJ2tVT5U9aZ +NwUqv9cUGhf+Di0nAX1diWxfd7DdEi8= +=6zfV +-----END PGP PUBLIC KEY BLOCK----- diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c3277f --- /dev/null +++ b/LICENSE @@ -0,0 +1,1143 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + + + +APACHE TOMCAT SUBCOMPONENTS: + +Apache Tomcat includes a number of subcomponents with separate copyright notices +and license terms. Your use of these subcomponents is subject to the terms and +conditions of the following licenses. + + +For the Eclipse JDT Core Batch Compiler (ecj-x.x.x.jar) component and the +following Jakarta EE Schemas: +- jakartaee_9.xsd +- jakartaee_10.xsd +- jakarta_web-services_2_0.xsd +- jakarta_web-services_client_2_0.xsd +- jsp_3_0.xsd +- jsp_3_1.xsd +- web-app_5_0.xsd +- web-app_6_0.xsd +- web-commonn_5_0.xsd +- web-commonn_6_0.xsd +- web-fragment_5_0.xsd +- web-fragment_6_0.xsd +- web-jsptaglibrary_3_0.xsd +- web-jsptaglibrary_3_1.xsd + +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + + +For the Windows Installer component: + + * All NSIS source code, plug-ins, documentation, examples, header files and + graphics, with the exception of the compression modules and where + otherwise noted, are licensed under the zlib/libpng license. + * The zlib compression module for NSIS is licensed under the zlib/libpng + license. + * The bzip2 compression module for NSIS is licensed under the bzip2 license. + * The lzma compression module for NSIS is licensed under the Common Public + License version 1.0. + +zlib/libpng license + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +bzip2 license + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + 3. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 4. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. + +Julian Seward, Cambridge, UK. + +jseward@acm.org +Common Public License version 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation +distributed under this Agreement, and b) in the case of each subsequent +Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are +distributed by that particular Contributor. A Contribution 'originates' from a +Contributor if it was added to the Program by such Contributor itself or anyone +acting on such Contributor's behalf. Contributions do not include additions to +the Program which: (i) are separate modules of software distributed in +conjunction with the Program under their own license agreement, and (ii) are not +derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free copyright license to +reproduce, prepare derivative works of, publicly display, publicly perform, +distribute and sublicense the Contribution of such Contributor, if any, and such +derivative works, in source code and object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed +Patents to make, use, sell, offer to sell, import and otherwise transfer the +Contribution of such Contributor, if any, in source code and object code form. +This patent license shall apply to the combination of the Contribution and the +Program if, at the time the Contribution is added by the Contributor, such +addition of the Contribution causes such combination to be covered by the +Licensed Patents. The patent license shall not apply to any other combinations +which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the licenses to +its Contributions set forth herein, no assurances are provided by any +Contributor that the Program does not infringe the patent or other intellectual +property rights of any other entity. Each Contributor disclaims any liability to +Recipient for claims brought by any other entity based on infringement of +intellectual property rights or otherwise. As a condition to exercising the +rights and licenses granted hereunder, each Recipient hereby assumes sole +responsibility to secure any other intellectual property rights needed, if any. +For example, if a third party patent license is required to allow Recipient to +distribute the Program, it is Recipient's responsibility to acquire that license +before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient copyright +rights in its Contribution, if any, to grant the copyright license set forth in +this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its +own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties and +conditions, express and implied, including warranties or conditions of title and +non-infringement, and implied warranties or conditions of merchantability and +fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and consequential +damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are offered by +that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such Contributor, +and informs licensees how to obtain it in a reasonable manner on or through a +medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the +Program. + +Each Contributor must identify itself as the originator of its Contribution, if +any, in a manner that reasonably allows subsequent Recipients to identify the +originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, if +a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, damages +and costs (collectively "Losses") arising from claims, lawsuits and other legal +actions brought by a third party against the Indemnified Contributor to the +extent caused by the acts or omissions of such Commercial Contributor in +connection with its distribution of the Program in a commercial product +offering. The obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property infringement. In order +to qualify, an Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial Contributor to +control, and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may participate in +any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If that +Commercial Contributor then makes performance claims, or offers warranties +related to Product X, those performance claims and warranties are such +Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a court +requires any other Contributor to pay any damages as a result, the Commercial +Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each +Recipient is solely responsible for determining the appropriateness of using and +distributing the Program and assumes all risks associated with its exercise of +rights under this Agreement, including but not limited to the risks and costs of +program errors, compliance with applicable laws, damage to or loss of data, +programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS +GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable +law, it shall not affect the validity or enforceability of the remainder of the +terms of this Agreement, and without further action by the parties hereto, such +provision shall be reformed to the minimum extent necessary to make such +provision valid and enforceable. + +If Recipient institutes patent litigation against a Contributor with respect to +a patent applicable to software (including a cross-claim or counterclaim in a +lawsuit), then any patent licenses granted by that Contributor to such Recipient +under this Agreement shall terminate as of the date such litigation is filed. In +addition, if Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or hardware) +infringes such Recipient's patent(s), then such Recipient's rights granted under +Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue and +survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to time. +No one other than the Agreement Steward has the right to modify this Agreement. +IBM is the initial Agreement Steward. IBM may assign the responsibility to serve +as the Agreement Steward to a suitable separate entity. Each new version of the +Agreement will be given a distinguishing version number. The Program (including +Contributions) may always be distributed subject to the version of the Agreement +under which it was received. In addition, after a new version of the Agreement +is published, Contributor may elect to distribute the Program (including its +Contributions) under the new version. Except as expressly stated in Sections +2(a) and 2(b) above, Recipient receives no rights or licenses to the +intellectual property of any Contributor under this Agreement, whether +expressly, by implication, estoppel or otherwise. All rights in the Program not +expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial in +any resulting litigation. + +Special exception for LZMA compression module + +Igor Pavlov and Amir Szekely, the authors of the LZMA compression module for +NSIS, expressly permit you to statically or dynamically link your code (or bind +by name) to the files from the LZMA compression module for NSIS without +subjecting your linked code to the terms of the Common Public license version +1.0. Any modifications or additions to files from the LZMA compression module +for NSIS, however, are subject to the terms of the Common Public License version +1.0. + + +For the following XML Schemas for Java EE Deployment Descriptors: + - javaee_5.xsd + - javaee_web_services_1_2.xsd + - javaee_web_services_client_1_2.xsd + - javaee_6.xsd + - javaee_web_services_1_3.xsd + - javaee_web_services_client_1_3.xsd + - jsp_2_2.xsd + - web-app_3_0.xsd + - web-common_3_0.xsd + - web-fragment_3_0.xsd + - javaee_7.xsd + - javaee_web_services_1_4.xsd + - javaee_web_services_client_1_4.xsd + - jsp_2_3.xsd + - web-app_3_1.xsd + - web-common_3_1.xsd + - web-fragment_3_1.xsd + - javaee_8.xsd + - web-app_4_0.xsd + - web-common_4_0.xsd + - web-fragment_4_0.xsd + +COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 + +1. Definitions. + + 1.1. Contributor. means each individual or entity that creates or contributes + to the creation of Modifications. + + 1.2. Contributor Version. means the combination of the Original Software, + prior Modifications used by a Contributor (if any), and the + Modifications made by that particular Contributor. + + 1.3. Covered Software. means (a) the Original Software, or (b) Modifications, + or (c) the combination of files containing Original Software with files + containing Modifications, in each case including portions thereof. + + 1.4. Executable. means the Covered Software in any form other than Source + Code. + + 1.5. Initial Developer. means the individual or entity that first makes + Original Software available under this License. + + 1.6. Larger Work. means a work which combines Covered Software or portions + thereof with code not governed by the terms of this License. + + 1.7. License. means this document. + + 1.8. Licensable. means having the right to grant, to the maximum extent + possible, whether at the time of the initial grant or subsequently + acquired, any and all of the rights conveyed herein. + + 1.9. Modifications. means the Source Code and Executable form of any of the + following: + + A. Any file that results from an addition to, deletion from or + modification of the contents of a file containing Original Software + or previous Modifications; + + B. Any new file that contains any part of the Original Software or + previous Modification; or + + C. Any new file that is contributed or otherwise made available under + the terms of this License. + + 1.10. Original Software. means the Source Code and Executable form of + computer software code that is originally released under this License. + + 1.11. Patent Claims. means any patent claim(s), now owned or hereafter + acquired, including without limitation, method, process, and apparatus + claims, in any patent Licensable by grantor. + + 1.12. Source Code. means (a) the common form of computer software code in + which modifications are made and (b) associated documentation included + in or with such code. + + 1.13. You. (or .Your.) means an individual or a legal entity exercising + rights under, and complying with all of the terms of, this License. For + legal entities, .You. includes any entity which controls, is controlled + by, or is under common control with You. For purposes of this + definition, .control. means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + +2. License Grants. + + 2.1. The Initial Developer Grant. + + Conditioned upon Your compliance with Section 3.1 below and subject to + third party intellectual property claims, the Initial Developer hereby + grants You a world-wide, royalty-free, non-exclusive license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by Initial Developer, to use, reproduce, modify, display, + perform, sublicense and distribute the Original Software (or + portions thereof), with or without Modifications, and/or as part of + a Larger Work; and + + (b) under Patent Claims infringed by the making, using or selling of + Original Software, to make, have made, use, practice, sell, and + offer for sale, and/or otherwise dispose of the Original Software + (or portions thereof). + + (c) The licenses granted in Sections 2.1(a) and (b) are effective on the + date Initial Developer first distributes or otherwise makes the + Original Software available to a third party under the terms of this + License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is granted: + (1) for code that You delete from the Original Software, or (2) for + infringements caused by: (i) the modification of the Original + Software, or (ii) the combination of the Original Software with + other software or devices. + + 2.2. Contributor Grant. + + Conditioned upon Your compliance with Section 3.1 below and subject to third + party intellectual property claims, each Contributor hereby grants You a + world-wide, royalty-free, non-exclusive license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by Contributor to use, reproduce, modify, display, + perform, sublicense and distribute the Modifications created by such + Contributor (or portions thereof), either on an unmodified basis, + with other Modifications, as Covered Software and/or as part of a + Larger Work; and + + (b) under Patent Claims infringed by the making, using, or selling of + Modifications made by that Contributor either alone and/or in + combination with its Contributor Version (or portions of such + combination), to make, use, sell, offer for sale, have made, and/or + otherwise dispose of: (1) Modifications made by that Contributor (or + portions thereof); and (2) the combination of Modifications made by + that Contributor with its Contributor Version (or portions of such + combination). + + (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on + the date Contributor first distributes or otherwise makes the + Modifications available to a third party. + + (d) Notwithstanding Section 2.2(b) above, no patent license is granted: + (1) for any code that Contributor has deleted from the Contributor + Version; (2) for infringements caused by: (i) third party + modifications of Contributor Version, or (ii) the combination of + Modifications made by that Contributor with other software (except + as part of the Contributor Version) or other devices; or (3) under + Patent Claims infringed by Covered Software in the absence of + Modifications made by that Contributor. + +3. Distribution Obligations. + + 3.1. Availability of Source Code. + Any Covered Software that You distribute or otherwise make available in + Executable form must also be made available in Source Code form and that + Source Code form must be distributed only under the terms of this License. + You must include a copy of this License with every copy of the Source Code + form of the Covered Software You distribute or otherwise make available. + You must inform recipients of any such Covered Software in Executable form + as to how they can obtain such Covered Software in Source Code form in a + reasonable manner on or through a medium customarily used for software + exchange. + + 3.2. Modifications. + The Modifications that You create or to which You contribute are governed + by the terms of this License. You represent that You believe Your + Modifications are Your original creation(s) and/or You have sufficient + rights to grant the rights conveyed by this License. + + 3.3. Required Notices. + You must include a notice in each of Your Modifications that identifies + You as the Contributor of the Modification. You may not remove or alter + any copyright, patent or trademark notices contained within the Covered + Software, or any notices of licensing or any descriptive text giving + attribution to any Contributor or the Initial Developer. + + 3.4. Application of Additional Terms. + You may not offer or impose any terms on any Covered Software in Source + Code form that alters or restricts the applicable version of this License + or the recipients. rights hereunder. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability obligations to + one or more recipients of Covered Software. However, you may do so only on + Your own behalf, and not on behalf of the Initial Developer or any + Contributor. You must make it absolutely clear that any such warranty, + support, indemnity or liability obligation is offered by You alone, and + You hereby agree to indemnify the Initial Developer and every Contributor + for any liability incurred by the Initial Developer or such Contributor as + a result of warranty, support, indemnity or liability terms You offer. + + 3.5. Distribution of Executable Versions. + You may distribute the Executable form of the Covered Software under the + terms of this License or under the terms of a license of Your choice, + which may contain terms different from this License, provided that You are + in compliance with the terms of this License and that the license for the + Executable form does not attempt to limit or alter the recipient.s rights + in the Source Code form from the rights set forth in this License. If You + distribute the Covered Software in Executable form under a different + license, You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial Developer + or Contributor. You hereby agree to indemnify the Initial Developer and + every Contributor for any liability incurred by the Initial Developer or + such Contributor as a result of any such terms You offer. + + 3.6. Larger Works. + You may create a Larger Work by combining Covered Software with other code + not governed by the terms of this License and distribute the Larger Work + as a single product. In such a case, You must make sure the requirements + of this License are fulfilled for the Covered Software. + +4. Versions of the License. + + 4.1. New Versions. + Sun Microsystems, Inc. is the initial license steward and may publish + revised and/or new versions of this License from time to time. Each + version will be given a distinguishing version number. Except as provided + in Section 4.3, no one other than the license steward has the right to + modify this License. + + 4.2. Effect of New Versions. + You may always continue to use, distribute or otherwise make the Covered + Software available under the terms of the version of the License under + which You originally received the Covered Software. If the Initial + Developer includes a notice in the Original Software prohibiting it from + being distributed or otherwise made available under any subsequent version + of the License, You must distribute and make the Covered Software + available under the terms of the version of the License under which You + originally received the Covered Software. Otherwise, You may also choose + to use, distribute or otherwise make the Covered Software available under + the terms of any subsequent version of the License published by the + license steward. + + 4.3. Modified Versions. + When You are an Initial Developer and You want to create a new license for + Your Original Software, You may create and use a modified version of this + License if You: (a) rename the license and remove any references to the + name of the license steward (except to note that the license differs from + this License); and (b) otherwise make it clear that the license contains + terms which differ from this License. + +5. DISCLAIMER OF WARRANTY. + + COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN .AS IS. BASIS, WITHOUT + WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT + LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, + MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK + AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD + ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL + DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY + SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN + ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED + HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +6. TERMINATION. + + 6.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to + cure such breach within 30 days of becoming aware of the breach. + Provisions which, by their nature, must remain in effect beyond the + termination of this License shall survive. + + 6.2. If You assert a patent infringement claim (excluding declaratory + judgment actions) against Initial Developer or a Contributor (the + Initial Developer or Contributor against whom You assert such claim + is referred to as .Participant.) alleging that the Participant + Software (meaning the Contributor Version where the Participant is a + Contributor or the Original Software where the Participant is the + Initial Developer) directly or indirectly infringes any patent, then + any and all rights granted directly or indirectly to You by such + Participant, the Initial Developer (if the Initial Developer is not + the Participant) and all Contributors under Sections 2.1 and/or 2.2 + of this License shall, upon 60 days notice from Participant terminate + prospectively and automatically at the expiration of such 60 day + notice period, unless if within such 60 day period You withdraw Your + claim with respect to the Participant Software against such + Participant either unilaterally or pursuant to a written agreement + with Participant. + + 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end + user licenses that have been validly granted by You or any + distributor hereunder prior to termination (excluding licenses + granted to You by any distributor) shall survive termination. + +7. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING + NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY + OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF + ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, + INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT + LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, + COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR + LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF + SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR + DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY.S NEGLIGENCE TO THE EXTENT + APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS + EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +8. U.S. GOVERNMENT END USERS. + + The Covered Software is a .commercial item,. as that term is defined in 48 + C.F.R. 2.101 (Oct. 1995), consisting of .commercial computer software. (as + that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and commercial + computer software documentation. as such terms are used in 48 C.F.R. 12.212 + (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 + through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered + Software with only those rights set forth herein. This U.S. Government Rights + clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or + provision that addresses Government rights in computer software under this + License. + +9. MISCELLANEOUS. + + This License represents the complete agreement concerning subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. This License shall be governed by the law of the jurisdiction + specified in a notice contained within the Original Software (except to the + extent applicable law, if any, provides otherwise), excluding such + jurisdiction's conflict-of-law provisions. Any litigation relating to this + License shall be subject to the jurisdiction of the courts located in the + jurisdiction and venue specified in a notice contained within the Original + Software, with the losing party responsible for costs, including, without + limitation, court costs and reasonable attorneys. fees and expenses. The + application of the United Nations Convention on Contracts for the + International Sale of Goods is expressly excluded. Any law or regulation + which provides that the language of a contract shall be construed against + the drafter shall not apply to this License. You agree that You alone are + responsible for compliance with the United States export administration + regulations (and the export control laws and regulation of any other + countries) when You use, distribute or otherwise make available any Covered + Software. + +10. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is responsible + for claims and damages arising, directly or indirectly, out of its + utilization of rights under this License and You agree to work with Initial + Developer and Contributors to distribute such responsibility on an equitable + basis. Nothing herein is intended or shall be deemed to constitute any + admission of liability. + + NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION + LICENSE (CDDL) + + The code released under the CDDL shall be governed by the laws of the State + of California (excluding conflict-of-law provisions). Any litigation relating + to this License shall be subject to the jurisdiction of the Federal Courts of + the Northern District of California and the state courts of the State of + California, with venue lying in Santa Clara County, California. + diff --git a/MERGE.txt b/MERGE.txt new file mode 100644 index 0000000..c3e81ab --- /dev/null +++ b/MERGE.txt @@ -0,0 +1,78 @@ +================================================================================ + 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. +================================================================================ + +Apache Tomcat re-uses code from a number of other Apache projects. There are +several reasons why depending on the binary releases of those projects is not +ideal. These include: +- potential conflicts if a web application ships with the same JAR +- a large JAR where Tomcat only depends on a small fraction + + +GIT +=== + +Updates from Git are applied manually via patch files. Patch files are generated +using: +git diff : HEAD: > temp.patch +The most recently merged SHA1 for the component below should be updated after +the patch file has been applied and committed + +BCEL +---- +Unused code is removed +Sub-tree: +src/main/java/org/apache/bcel +The SHA1 ID / tag for the most recent commit to be merged to Tomcat is: +rel/commons-bcel-6.8.2 (2024-02-25) + +Codec +----- +Unused code is removed +Sub-tree: +src/main/java/org/apache/commons/codec +The SHA1 ID / tag for the most recent commit to be merged to Tomcat is: +rel/commons-codec-1.16.1 (2024-02-09) +Note: Only classes required for Base64 encoding/decoding. The rest are removed. + +FileUpload +---------- +Unused code is removed +Branch: 1.x +Sub-tree: +src/main/java/org/apache/commons/fileupload2 +The SHA1 ID / tag for the most recent commit to be merged to Tomcat is: +7a8c3241cfa8d036452cd4fc3f92d57cff189bca (2023-09-16) + +Note: Tomcat's copy of fileupload also includes classes copied manually from + Commons IO. + +DBCP +---- +Pool2 +Unused classes removed +Sub-tree +src/main/java/org/apache/commons/pool2 +The SHA1 ID / tag for the most recent commit to be merged to Tomcat is: +rel/commons-pool-2.12.0 (2023-09-30) + +DBCP2 +No unused code removed +Sub-tree +src/main/java/org/apache/commons/dbcp2 +src/main/resources/org/apache/commons/dbcp2 +The SHA1 ID / tag for the most recent commit to be merged to Tomcat is: +b1e0c86d101aa43029625eb191aaee4306911702 (2023-03-08) diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..dc7f59d --- /dev/null +++ b/NOTICE @@ -0,0 +1,68 @@ +Apache Tomcat +Copyright 1999-2024 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +This software contains code derived from netty-native +developed by the Netty project +(https://netty.io, https://github.com/netty/netty-tcnative/) +and from finagle-native developed at Twitter +(https://github.com/twitter/finagle). + +This software contains code derived from jgroups-kubernetes +developed by the JGroups project (http://www.jgroups.org/). + +The Windows Installer is built with the Nullsoft +Scriptable Install System (NSIS), which is +open source software. The original software and +related information is available at +http://nsis.sourceforge.net. + +Java compilation software for JSP pages is provided by the Eclipse +JDT Core Batch Compiler component, which is open source software. +The original software and related information is available at +https://www.eclipse.org/jdt/core/. + +org.apache.tomcat.util.json.JSONParser.jj is a public domain javacc grammar +for JSON written by Robert Fischer. +https://github.com/RobertFischer/json-parser + +For portions of the Tomcat JNI OpenSSL API and the OpenSSL JSSE integration +The org.apache.tomcat.jni and the org.apache.tomcat.net.openssl packages +are derivative work originating from the Netty project and the finagle-native +project developed at Twitter +* Copyright 2014 The Netty Project +* Copyright 2014 Twitter + +For portions of the Tomcat cloud support +The org.apache.catalina.tribes.membership.cloud package contains derivative +work originating from the jgroups project. +https://github.com/jgroups-extras/jgroups-kubernetes +Copyright 2002-2018 Red Hat Inc. + +The original XML Schemas for Java EE Deployment Descriptors: + - javaee_5.xsd + - javaee_web_services_1_2.xsd + - javaee_web_services_client_1_2.xsd + - javaee_6.xsd + - javaee_web_services_1_3.xsd + - javaee_web_services_client_1_3.xsd + - jsp_2_2.xsd + - web-app_3_0.xsd + - web-common_3_0.xsd + - web-fragment_3_0.xsd + - javaee_7.xsd + - javaee_web_services_1_4.xsd + - javaee_web_services_client_1_4.xsd + - jsp_2_3.xsd + - web-app_3_1.xsd + - web-common_3_1.xsd + - web-fragment_3_1.xsd + - javaee_8.xsd + - web-app_4_0.xsd + - web-common_4_0.xsd + - web-fragment_4_0.xsd + +may be obtained from: +http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/index.html diff --git a/README.md b/README.md index 9ebb840..107df4d 100644 --- a/README.md +++ b/README.md @@ -1 +1,79 @@ -# template-repository \ No newline at end of file +## Welcome to Apache Tomcat! + +### What Is It? + +The Apache Tomcat® software is an open source implementation of the Jakarta +Servlet, Jakarta Pages, Jakarta Expression Language and Jakarta WebSocket +technologies. The Jakarta Servlet, Jakarta Server Pages, Jakarta Expression Language and +Jakarta WebSocket specifications are developed as part of the +[Jakarta EE Platform](https://jakarta.ee/specifications/). + +The Apache Tomcat software is developed in an open and participatory +environment and released under the +[Apache License version 2](https://www.apache.org/licenses/). The Apache Tomcat +project is intended to be a collaboration of the best-of-breed developers from +around the world. We invite you to participate in this open development +project. To learn more about getting involved, +[click here](https://tomcat.apache.org/getinvolved.html) or keep reading. + +Apache Tomcat software powers numerous large-scale, mission-critical web +applications across a diverse range of industries and organizations. Some of +these users and their stories are listed on the +[PoweredBy wiki page](https://cwiki.apache.org/confluence/display/TOMCAT/PoweredBy). + +Apache Tomcat, Tomcat, Apache, the Apache feather, and the Apache Tomcat +project logo are trademarks of the Apache Software Foundation. + +### Get It + +For every major Tomcat version there is one download page containing +links to the latest binary and source code downloads, but also +links for browsing the download directories and archives: +- [Tomcat 11](https://tomcat.apache.org/download-11.cgi) +- [Tomcat 10](https://tomcat.apache.org/download-10.cgi) +- [Tomcat 9](https://tomcat.apache.org/download-90.cgi) + +To facilitate choosing the right major Tomcat version one, we have provided a +[version overview page](https://tomcat.apache.org/whichversion.html). + +### Documentation + +The documentation available as of the date of this release is +included in the docs webapp which ships with tomcat. You can access that webapp +by starting tomcat and visiting in your browser. +The most up-to-date documentation for each version can be found at: +- [Tomcat 11](https://tomcat.apache.org/tomcat-11.0-doc/) +- [Tomcat 10](https://tomcat.apache.org/tomcat-10.1-doc/) +- [Tomcat 9](https://tomcat.apache.org/tomcat-9.0-doc/) + +### Installation + +Please see [RUNNING.txt](RUNNING.txt) for more info. + +### Licensing + +Please see [LICENSE](LICENSE) for more info. + +### Support and Mailing List Information + +* Free community support is available through the +[tomcat-users](https://tomcat.apache.org/lists.html#tomcat-users) email list and +a dedicated [IRC channel](https://tomcat.apache.org/irc.html) (#tomcat on +Freenode). + +* If you want freely available support for running Apache Tomcat, please see the +resources page [here](https://tomcat.apache.org/findhelp.html). + +* If you want to be informed about new code releases, bug fixes, +security fixes, general news and information about Apache Tomcat, please +subscribe to the +[tomcat-announce](https://tomcat.apache.org/lists.html#tomcat-announce) email +list. + +* If you have a concrete bug report for Apache Tomcat, please see the +instructions for reporting a bug +[here](https://tomcat.apache.org/bugreport.html). + +### Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for more info. diff --git a/RELEASE-NOTES b/RELEASE-NOTES new file mode 100644 index 0000000..3d648e7 --- /dev/null +++ b/RELEASE-NOTES @@ -0,0 +1,173 @@ +================================================================================ + 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. +================================================================================ + + + Apache Tomcat Version @VERSION@ + Release Notes + + +========= +CONTENTS: +========= + +* Dependency Changes +* API Stability +* Bundled APIs +* Web application reloading and static fields in shared libraries +* Security manager URLs +* Symlinking static resources +* Viewing the Tomcat Change Log +* Cryptographic software notice +* When all else fails + + +=================== +Dependency Changes: +=================== +Tomcat @VERSION_MAJOR_MINOR@ is designed to run on Java @MIN_JAVA_VERSION@ and later. + + +============== +API Stability: +============== + +The public interfaces for the following classes are fixed and will not be +changed at all during the remaining lifetime of the @VERSION_MAJOR@.x series: +- All classes in the jakarta namespace + +The public interfaces for the following classes may be added to in order to +resolve bugs and/or add new features. No existing interface method will be +removed or changed although it may be deprecated. +- org.apache.catalina.* (excluding sub-packages) + +Note: As Tomcat @VERSION_MAJOR@ matures, the above list will be added to. The list is not + considered complete at this time. + +The remaining classes are considered part of the Tomcat internals and may change +without notice between point releases. + + +============= +Bundled APIs: +============= +A standard installation of Tomcat @VERSION_MAJOR_MINOR@ makes all of the following APIs available +for use by web applications (by placing them in "lib"): +* annotations-api.jar (Annotations package) +* catalina.jar (Tomcat Catalina implementation) +* catalina-ant.jar (Tomcat Catalina Ant tasks) +* catalina-ha.jar (High availability package) +* catalina-ssi.jar (Server-side Includes module) +* catalina-storeconfig.jar (Generation of XML configuration from current state) +* catalina-tribes.jar (Group communication) +* ecj-@JDT_VERSION@.jar (Eclipse JDT Java compiler) +* el-api.jar (EL 5.0 API) +* jasper.jar (Jasper 2 Compiler and Runtime) +* jasper-el.jar (Jasper 2 EL implementation) +* jsp-api.jar (JSP 3.1 API) +* servlet-api.jar (Servlet 6.0 API) +* tomcat-api.jar (Interfaces shared by Catalina and Jasper) +* tomcat-coyote.jar (Tomcat connectors and utility classes) +* tomcat-dbcp.jar (package renamed database connection pool based on Commons DBCP 2) +* tomcat-jdbc.jar (Tomcat's database connection pooling solution) +* tomcat-jni.jar (Interface to the native component of the APR/native connector) +* tomcat-util.jar (Various utilities) +* tomcat-websocket.jar (WebSocket 2.1 implementation) +* websocket-api.jar (WebSocket 2.1 API) +* websocket-client-api.jar (WebSocket 2.1 Client API) + +You can make additional APIs available to all of your web applications by +putting unpacked classes into a "classes" directory (not created by default), +or by placing them in JAR files in the "lib" directory. + +To override the XML parser implementation or interfaces, use the upgradeable +modules feature. + + +================================================================ +Web application reloading and static fields in shared libraries: +================================================================ +Some shared libraries (many are part of the JDK) keep references to objects +instantiated by the web application. To avoid class loading related problems +(ClassCastExceptions, messages indicating that the classloader +is stopped, etc.), the shared libraries state should be reinitialized. + +Something which might help is to avoid putting classes which would be +referenced by a shared static field in the web application classloader, +and putting them in the shared classloader instead (JARs should be put in the +"lib" folder, and classes should be put in the "classes" folder). + + +====================== +Security manager URLs: +====================== +In order to grant security permissions to JARs located inside the +web application repository, use URLs of the following format +in your policy file: + +file:${catalina.base}/webapps/examples/WEB-INF/lib/driver.jar + + +============================ +Symlinking static resources: +============================ +By default, Unix symlinks will not work when used in a web application to link +resources located outside the web application root directory. + +This behavior is optional, and the "allowLinking" flag may be used to deactivate +the check. + + +============================== +Viewing the Tomcat Change Log: +============================== +The full change log is available from https://tomcat.apache.org and is also +included in the documentation web application. + + +============================= +Cryptographic software notice +============================= +This distribution includes cryptographic software. The country in +which you currently reside may have restrictions on the import, +possession, use, and/or re-export to another country, of +encryption software. BEFORE using any encryption software, please +check your country's laws, regulations and policies concerning the +import, possession, or use, and re-export of encryption software, to +see if this is permitted. See for more +information. + +The U.S. Government Department of Commerce, Bureau of Industry and +Security (BIS), has classified this software as Export Commodity +Control Number (ECCN) 5D002.C.1, which includes information security +software using or performing cryptographic functions with asymmetric +algorithms. The form and manner of this Apache Software Foundation +distribution makes it eligible for export under the License Exception +ENC Technology Software Unrestricted (TSU) exception (see the BIS +Export Administration Regulations, Section 740.13) for both object +code and source code. + +The following provides more details on the included cryptographic +software: + - Tomcat includes code designed to work with JSSE + - Tomcat includes code designed to work with OpenSSL + + +==================== +When all else fails: +==================== +See the FAQ +https://tomcat.apache.org/faq/ diff --git a/RUNNING.txt b/RUNNING.txt new file mode 100644 index 0000000..1e25052 --- /dev/null +++ b/RUNNING.txt @@ -0,0 +1,467 @@ +================================================================================ + 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. +================================================================================ + + =================================================== + Running The Apache Tomcat @VERSION_MAJOR_MINOR@ Servlet/JSP Container + =================================================== + +Apache Tomcat @VERSION_MAJOR_MINOR@ requires a Java Standard Edition Runtime +Environment (JRE) version @MIN_JAVA_VERSION@ or later. + +============================= +Running With JRE @MIN_JAVA_VERSION@ Or Later +============================= + +(1) Download and Install a Java SE Runtime Environment (JRE) + +(1.1) Download a Java SE Runtime Environment (JRE), + release version @MIN_JAVA_VERSION@ or later, from + http://www.oracle.com/technetwork/java/javase/downloads/index.html + +(1.2) Install the JRE according to the instructions included with the + release. + + You may also use a full Java Development Kit (JDK) rather than just + a JRE. + + +(2) Download and Install Apache Tomcat + +(2.1) Download a binary distribution of Tomcat from: + + https://tomcat.apache.org/ + +(2.2) Unpack the binary distribution so that it resides in its own + directory (conventionally named "apache-tomcat-[version]"). + + For the purposes of the remainder of this document, the name + "CATALINA_HOME" is used to refer to the full pathname of that + directory. + +NOTE: As an alternative to downloading a binary distribution, you can +create your own from the Tomcat source code, as described in +"BUILDING.txt". You can either + + a) Do the full "release" build and find the created distribution in the + "output/release" directory and then proceed with unpacking as above, or + + b) Do a simple build and use the "output/build" directory as + "CATALINA_HOME". Be warned that there are some differences between the + contents of the "output/build" directory and a full "release" + distribution. + + +(3) Configure Environment Variables + +Tomcat is a Java application and does not use environment variables directly. +Environment variables are used by the Tomcat startup scripts. The scripts use +the environment variables to prepare the command that starts Tomcat. + +(3.1) Set CATALINA_HOME (required) and CATALINA_BASE (optional) + +The CATALINA_HOME environment variable should be set to the location of the +root directory of the "binary" distribution of Tomcat. + +The Tomcat startup scripts have some logic to set this variable +automatically if it is absent, based on the location of the startup script +in *nix and on the current directory in Windows. That logic might not work +in all circumstances, so setting the variable explicitly is recommended. + +The CATALINA_BASE environment variable specifies location of the root +directory of the "active configuration" of Tomcat. It is optional. It +defaults to be equal to CATALINA_HOME. + +Using distinct values for the CATALINA_HOME and CATALINA_BASE variables is +recommended to simplify further upgrades and maintenance. It is documented +in the "Multiple Tomcat Instances" section below. + + +(3.2) Set JRE_HOME or JAVA_HOME (required) + +These variables are used to specify location of a Java Runtime +Environment or of a Java Development Kit that is used to start Tomcat. + +The JRE_HOME variable is used to specify location of a JRE. The JAVA_HOME +variable is used to specify location of a JDK. + +Using JAVA_HOME provides access to certain additional startup options that +are not allowed when JRE_HOME is used. + +If both JRE_HOME and JAVA_HOME are specified, JRE_HOME is used. + +The recommended place to specify these variables is a "setenv" script. See +below. + + +(3.3) Other variables (optional) + +Other environment variables exist, besides the four described above. +See the comments at the top of catalina.bat or catalina.sh scripts for +the list and a description of each of them. + +One frequently used variable is CATALINA_OPTS. It allows specification of +additional options for the java command that starts Tomcat. + +See the Java documentation for the options that affect the Java Runtime +Environment. + +See the "System Properties" page in the Tomcat Configuration Reference for +the system properties that are specific to Tomcat. + +A similar variable is JAVA_OPTS. It is used less frequently. It allows +specification of options that are used both to start and to stop Tomcat as well +as for other commands. + +Note: Do not use JAVA_OPTS to specify memory limits. You do not need much +memory for a small process that is used to stop Tomcat. Those settings +belong to CATALINA_OPTS. + +Another frequently used variable is CATALINA_PID (on *nix only). It +specifies the location of the file where process id of the forked Tomcat +java process will be written. This setting is optional. It will activate +the following features: + + * better protection against duplicate start attempts and + * allows forceful termination of Tomcat process when it does not react to + the standard shutdown command. + + +(3.4) Using the "setenv" script (optional, recommended) + +Apart from CATALINA_HOME and CATALINA_BASE, all environment variables can +be specified in the "setenv" script. The script is placed either into +CATALINA_BASE/bin or into CATALINA_HOME/bin directory and is named +setenv.bat (on Windows) or setenv.sh (on *nix). The file has to be +readable. + +By default the setenv script file is absent. If the script file is present +both in CATALINA_BASE and in CATALINA_HOME, the one in CATALINA_BASE is +preferred. + +For example, to configure the JRE_HOME and CATALINA_PID variables you can +create the following script file: + +On Windows, %CATALINA_BASE%\bin\setenv.bat: + + set "JRE_HOME=%ProgramFiles%\Java\jre@MIN_JAVA_VERSION@" + exit /b 0 + +On *nix, $CATALINA_BASE/bin/setenv.sh: + + JRE_HOME=/usr/java/latest + CATALINA_PID="/run/tomcat.pid" + + +The CATALINA_HOME and CATALINA_BASE variables cannot be configured in the +setenv script, because they are used to locate that file. + +All the environment variables described here and the "setenv" script are +used only if you use the standard scripts to launch Tomcat. For example, if +you have installed Tomcat as a service on Windows, the service wrapper +launches Java directly and does not use the script files. + + +(4) Start Up Tomcat + +(4.1) Tomcat can be started by executing one of the following commands: + + On Windows: + + %CATALINA_HOME%\bin\startup.bat + + or + + %CATALINA_HOME%\bin\catalina.bat start + + On *nix: + + $CATALINA_HOME/bin/startup.sh + + or + + $CATALINA_HOME/bin/catalina.sh start + +(4.2) After startup, the default web applications included with Tomcat will be + available by visiting: + + http://localhost:8080/ + +(4.3) Further information about configuring and running Tomcat can be found in + the documentation included here, as well as on the Tomcat web site: + + https://tomcat.apache.org/ + + +(5) Shut Down Tomcat + +(5.1) Tomcat can be shut down by executing one of the following commands: + + On Windows: + + %CATALINA_HOME%\bin\shutdown.bat + + or + + %CATALINA_HOME%\bin\catalina.bat stop + + On *nix: + + $CATALINA_HOME/bin/shutdown.sh + + or + + $CATALINA_HOME/bin/catalina.sh stop + +================================================== +Advanced Configuration - Multiple Tomcat Instances +================================================== + +In many circumstances, it is desirable to have a single copy of a Tomcat +binary distribution shared among multiple users on the same server. To make +this possible, you can set the CATALINA_BASE environment variable to the +directory that contains the files for your 'personal' Tomcat instance. + +When running with a separate CATALINA_HOME and CATALINA_BASE, the files +and directories are split as following: + +In CATALINA_BASE: + + * bin - Only the following files: + + * setenv.sh (*nix) or setenv.bat (Windows), + * tomcat-juli.jar + + The setenv scripts were described above. The tomcat-juli library + is documented in the Logging chapter in the User Guide. + + * conf - Server configuration files (including server.xml) + + * lib - Libraries and classes, as explained below + + * logs - Log and output files + + * webapps - Automatically loaded web applications + + * work - Temporary working directories for web applications + + * temp - Directory used by the JVM for temporary files (java.io.tmpdir) + + +In CATALINA_HOME: + + * bin - Startup and shutdown scripts + + The following files will be used only if they are absent in + CATALINA_BASE/bin: + + setenv.sh (*nix), setenv.bat (Windows), tomcat-juli.jar + + * lib - Libraries and classes, as explained below + +In the default configuration the JAR libraries and classes both in +CATALINA_BASE/lib and in CATALINA_HOME/lib will be added to the common +classpath, but the ones in CATALINA_BASE will be added first and thus will +be searched first. + +The idea is that you may leave the standard Tomcat libraries in +CATALINA_HOME/lib and add other ones such as database drivers into +CATALINA_BASE/lib. + +In general it is advised to never share libraries between web applications, +but put them into WEB-INF/lib directories inside the applications. See +Classloading documentation in the User Guide for details. + + +It might be useful to note that the values of CATALINA_HOME and +CATALINA_BASE can be referenced in the XML configuration files processed +by Tomcat as ${catalina.home} and ${catalina.base} respectively. + +For example, the standard manager web application can be kept in +CATALINA_HOME/webapps/manager and loaded into CATALINA_BASE by copying +its deployment descriptor into the desired virtual host: + + * Copy the CATALINA_HOME/webapps/manager/META-INF/context.xml + file as CATALINA_BASE/conf/Catalina/localhost/manager.xml + + * Add docBase attribute as shown below. + +The file will look like the following: + + + + + + + +See Deployer chapter in User Guide and Context and Host chapters in the +Configuration Reference for more information on contexts and web +application deployment. + + +================ +Troubleshooting +================ + +There are only really 2 things likely to go wrong during the stand-alone +Tomcat install: + +(1) The most common hiccup is when another web server (or any process for that + matter) has laid claim to port 8080. This is the default HTTP port that + Tomcat attempts to bind to at startup. To change this, open the file: + + $CATALINA_HOME/conf/server.xml + + and search for '8080'. Change it to a port that isn't in use, and is + greater than 1024, as ports less than or equal to 1024 require superuser + access to bind under UNIX. + + Restart Tomcat and you're in business. Be sure that you replace the "8080" + in the URL you're using to access Tomcat. For example, if you change the + port to 1977, you would request the URL http://localhost:1977/ in your + browser. + +(2) The 'localhost' machine isn't found. This could happen if you're behind a + proxy. If that's the case, make sure the proxy configuration for your + browser knows that you shouldn't be going through the proxy to access the + "localhost". + + In Firefox, this is under Tools/Preferences -> Advanced/Network -> + Connection -> Settings..., and in Internet Explorer it is Tools -> + Internet Options -> Connections -> LAN Settings. + + +==================== +Optional Components +==================== + +The following optional components may be included with the Apache Tomcat binary +distribution. If they are not included, you can install them separately. + + 1. Apache Tomcat Native library + + 2. Apache Commons Daemon service launcher + +Both of them are implemented in C language and as such have to be compiled +into binary code. The binary code will be specific for a platform and CPU +architecture and it must match the Java Runtime Environment executables +that will be used to launch Tomcat. + +The Windows-specific binary distributions of Apache Tomcat include binary +files for these components. On other platforms you would have to look for +binary versions elsewhere or compile them yourself. + +If you are new to Tomcat, do not bother with these components to start with. +If you do use them, do not forget to read their documentation. + + +Apache Tomcat Native library +----------------------------- + +It is a library that allows to use the OpenSSL variant of the TLS implementation +for the HTTP connector in Apache Tomcat. It is built around OpenSSL and Apache +Portable Runtime (APR) libraries. + +This feature was especially important in the old days when Java performance +was poor. It is less important nowadays, but it is still used and respected +by many. See Tomcat documentation for more details. + +For further reading: + + - Apache Tomcat documentation + + * Documentation for APR/Native library in the Tomcat User's Guide + + https://tomcat.apache.org/tomcat-@VERSION_MAJOR_MINOR@-doc/apr.html + + * Documentation for the HTTP connector in the Tomcat Configuration Reference + + https://tomcat.apache.org/tomcat-@VERSION_MAJOR_MINOR@-doc/config/http.html + + - Apache Tomcat Native project home + + https://tomcat.apache.org/native-doc/ + + - Other projects + + * OpenSSL + + https://www.openssl.org/ + + * Apache Portable Runtime + + https://apr.apache.org/ + + * Apache HTTP Server + + https://httpd.apache.org/ + +To deactivate Apache Tomcat Native library: + + - To deactivate Apache Tomcat Native library when it is installed, or + - To remove the warning that is logged during Tomcat startup when the + library is not installed: + + Edit the "conf/server.xml" file and remove "AprLifecycleListener" from + it. + +The binary file of Apache Tomcat Native library is usually named + + - "tcnative-2.dll" on Windows + - "libtcnative-2.so" on *nix systems + + +Apache Commons Daemon +---------------------- + +Apache Commons Daemon project provides wrappers that can be used to +install Apache Tomcat as a service on Windows or as a daemon on *nix +systems. + +The Windows-specific implementation of Apache Commons Daemon is called +"procrun". The *nix-specific one is called "jsvc". + +For further reading: + + - Apache Commons Daemon project + + https://commons.apache.org/daemon/ + + - Apache Tomcat documentation + + * Installing Apache Tomcat + + https://tomcat.apache.org/tomcat-@VERSION_MAJOR_MINOR@-doc/setup.html + + * Windows Service How-To + + https://tomcat.apache.org/tomcat-@VERSION_MAJOR_MINOR@-doc/windows-service-howto.html + +The binary files of Apache Commons Daemon in Apache Tomcat distributions +for Windows are named: + + - "tomcat@VERSION_MAJOR@.exe" + - "tomcat@VERSION_MAJOR@w.exe" + +These files are renamed copies of "prunsrv.exe" and "prunmgr.exe" from +Apache Commons Daemon distribution. The file names have a meaning: they are +used as the service name to register the service in Windows, as well as the +key name to store distinct configuration for this installation of +"procrun". If you would like to install several instances of Tomcat @VERSION_MAJOR_MINOR@ +in parallel, you have to further rename those files, using the same naming +scheme. diff --git a/bin/catalina-tasks.xml b/bin/catalina-tasks.xml new file mode 100644 index 0000000..c7c9c28 --- /dev/null +++ b/bin/catalina-tasks.xml @@ -0,0 +1,39 @@ + + + + + + Catalina Ant Manager, JMX and JSPC Tasks + + + + + + + + + + + + + + + diff --git a/bin/catalina.bat b/bin/catalina.bat new file mode 100755 index 0000000..9c55ae9 --- /dev/null +++ b/bin/catalina.bat @@ -0,0 +1,357 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Start/Stop Script for the CATALINA Server +rem +rem For supported commands call "catalina.bat help" or see the usage section +rem towards the end of this file. +rem +rem Environment Variable Prerequisites +rem +rem Do not set the variables in this script. Instead put them into a script +rem setenv.bat in CATALINA_BASE/bin to keep your customizations separate. +rem +rem WHEN RUNNING TOMCAT AS A WINDOWS SERVICE: +rem Note that the environment variables that affect the behavior of this +rem script will have no effect at all on Windows Services. As such, any +rem local customizations made in a CATALINA_BASE/bin/setenv.bat script +rem will also have no effect on Tomcat when launched as a Windows Service. +rem The configuration that controls Windows Services is stored in the Windows +rem Registry, and is most conveniently maintained using the "tomcat@VERSION_MAJOR@w.exe" +rem maintenance utility. +rem +rem CATALINA_HOME May point at your Catalina "build" directory. +rem +rem CATALINA_BASE (Optional) Base directory for resolving dynamic portions +rem of a Catalina installation. If not present, resolves to +rem the same directory that CATALINA_HOME points to. +rem +rem CATALINA_OPTS (Optional) Java runtime options used when the "start", +rem "run" or "debug" command is executed. +rem Include here and not in JAVA_OPTS all options, that should +rem only be used by Tomcat itself, not by the stop process, +rem the version command etc. +rem Examples are heap size, GC logging, JMX ports etc. +rem +rem CATALINA_TMPDIR (Optional) Directory path location of temporary directory +rem the JVM should use (java.io.tmpdir). Defaults to +rem %CATALINA_BASE%\temp. +rem +rem JAVA_HOME Must point at your Java Development Kit installation. +rem Required to run the with the "debug" argument. +rem +rem JRE_HOME Must point at your Java Runtime installation. +rem Defaults to JAVA_HOME if empty. If JRE_HOME and JAVA_HOME +rem are both set, JRE_HOME is used. +rem +rem JAVA_OPTS (Optional) Java runtime options used when any command +rem is executed. +rem Include here and not in CATALINA_OPTS all options, that +rem should be used by Tomcat and also by the stop process, +rem the version command etc. +rem Most options should go into CATALINA_OPTS. +rem +rem JPDA_TRANSPORT (Optional) JPDA transport used when the "jpda start" +rem command is executed. The default is "dt_socket". +rem +rem JPDA_ADDRESS (Optional) Java runtime options used when the "jpda start" +rem command is executed. The default is localhost:8000. +rem +rem JPDA_SUSPEND (Optional) Java runtime options used when the "jpda start" +rem command is executed. Specifies whether JVM should suspend +rem execution immediately after startup. Default is "n". +rem +rem JPDA_OPTS (Optional) Java runtime options used when the "jpda start" +rem command is executed. If used, JPDA_TRANSPORT, JPDA_ADDRESS, +rem and JPDA_SUSPEND are ignored. Thus, all required jpda +rem options MUST be specified. The default is: +rem +rem -agentlib:jdwp=transport=%JPDA_TRANSPORT%, +rem address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND% +rem +rem JSSE_OPTS (Optional) Java runtime options used to control the TLS +rem implementation when JSSE is used. Default is: +rem "-Djdk.tls.ephemeralDHKeySize=2048" +rem +rem CATALINA_LOGGING_CONFIG (Optional) Override Tomcat's logging config file +rem Example (all one line) +rem set CATALINA_LOGGING_CONFIG=-Djava.util.logging.config.file="%CATALINA_BASE%\conf\logging.properties" +rem +rem LOGGING_MANAGER (Optional) Override Tomcat's logging manager +rem Example (all one line) +rem set LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager +rem +rem TITLE (Optional) Specify the title of Tomcat window. The default +rem TITLE is Tomcat if it's not specified. +rem Example (all one line) +rem set TITLE=Tomcat.Cluster#1.Server#1 [%DATE% %TIME%] +rem --------------------------------------------------------------------------- + +setlocal + +rem Suppress Terminate batch job on CTRL+C +if not ""%1"" == ""run"" goto mainEntry +if "%TEMP%" == "" goto mainEntry +if exist "%TEMP%\%~nx0.run" goto mainEntry +echo Y>"%TEMP%\%~nx0.run" +if not exist "%TEMP%\%~nx0.run" goto mainEntry +echo Y>"%TEMP%\%~nx0.Y" +call "%~f0" %* <"%TEMP%\%~nx0.Y" +rem Use provided errorlevel +set RETVAL=%ERRORLEVEL% +del /Q "%TEMP%\%~nx0.Y" >NUL 2>&1 +exit /B %RETVAL% +:mainEntry +del /Q "%TEMP%\%~nx0.run" >NUL 2>&1 + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome + +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +rem Copy CATALINA_BASE from CATALINA_HOME if not defined +if not "%CATALINA_BASE%" == "" goto gotBase +set "CATALINA_BASE=%CATALINA_HOME%" +:gotBase + +rem Ensure that neither CATALINA_HOME nor CATALINA_BASE contains a semi-colon +rem as this is used as the separator in the classpath and Java provides no +rem mechanism for escaping if the same character appears in the path. Check this +rem by replacing all occurrences of ';' with '' and checking that neither +rem CATALINA_HOME nor CATALINA_BASE have changed +if "%CATALINA_HOME%" == "%CATALINA_HOME:;=%" goto homeNoSemicolon +echo Using CATALINA_HOME: "%CATALINA_HOME%" +echo Unable to start as CATALINA_HOME contains a semicolon (;) character +goto end +:homeNoSemicolon + +if "%CATALINA_BASE%" == "%CATALINA_BASE:;=%" goto baseNoSemicolon +echo Using CATALINA_BASE: "%CATALINA_BASE%" +echo Unable to start as CATALINA_BASE contains a semicolon (;) character +goto end +:baseNoSemicolon + +rem Ensure that any user defined CLASSPATH variables are not used on startup, +rem but allow them to be specified in setenv.bat, in rare case when it is needed. +set CLASSPATH= + +rem Get standard environment variables +if not exist "%CATALINA_BASE%\bin\setenv.bat" goto checkSetenvHome +call "%CATALINA_BASE%\bin\setenv.bat" +goto setenvDone +:checkSetenvHome +if exist "%CATALINA_HOME%\bin\setenv.bat" call "%CATALINA_HOME%\bin\setenv.bat" +:setenvDone + +rem Get standard Java environment variables +if exist "%CATALINA_HOME%\bin\setclasspath.bat" goto okSetclasspath +echo Cannot find "%CATALINA_HOME%\bin\setclasspath.bat" +echo This file is needed to run this program +goto end +:okSetclasspath +call "%CATALINA_HOME%\bin\setclasspath.bat" %1 +if errorlevel 1 goto end + +rem Add on extra jar file to CLASSPATH +rem Note that there are no quotes as we do not want to introduce random +rem quotes into the CLASSPATH +if "%CLASSPATH%" == "" goto emptyClasspath +set "CLASSPATH=%CLASSPATH%;" +:emptyClasspath +set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar" + +if not "%CATALINA_TMPDIR%" == "" goto gotTmpdir +set "CATALINA_TMPDIR=%CATALINA_BASE%\temp" +:gotTmpdir + +rem Add tomcat-juli.jar to classpath +rem tomcat-juli.jar can be over-ridden per instance +if not exist "%CATALINA_BASE%\bin\tomcat-juli.jar" goto juliClasspathHome +set "CLASSPATH=%CLASSPATH%;%CATALINA_BASE%\bin\tomcat-juli.jar" +goto juliClasspathDone +:juliClasspathHome +set "CLASSPATH=%CLASSPATH%;%CATALINA_HOME%\bin\tomcat-juli.jar" +:juliClasspathDone + +if not "%JSSE_OPTS%" == "" goto gotJsseOpts +set "JSSE_OPTS=-Djdk.tls.ephemeralDHKeySize=2048" +:gotJsseOpts +set "JAVA_OPTS=%JAVA_OPTS% %JSSE_OPTS%" + +rem Register custom URL handlers +rem Do this here so custom URL handles (specifically 'war:...') can be used in the security policy +set "JAVA_OPTS=%JAVA_OPTS% -Djava.protocol.handler.pkgs=org.apache.catalina.webresources" + +if not "%CATALINA_LOGGING_CONFIG%" == "" goto noJuliConfig +set CATALINA_LOGGING_CONFIG=-Dnop +if not exist "%CATALINA_BASE%\conf\logging.properties" goto noJuliConfig +set CATALINA_LOGGING_CONFIG=-Djava.util.logging.config.file="%CATALINA_BASE%\conf\logging.properties" +:noJuliConfig + +if not "%LOGGING_MANAGER%" == "" goto noJuliManager +set LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager +:noJuliManager + +rem Configure module start-up parameters +set "JAVA_OPTS=%JAVA_OPTS% --add-opens=java.base/java.lang=ALL-UNNAMED" +set "JAVA_OPTS=%JAVA_OPTS% --add-opens=java.base/java.io=ALL-UNNAMED" +set "JAVA_OPTS=%JAVA_OPTS% --add-opens=java.base/java.util=ALL-UNNAMED" +set "JAVA_OPTS=%JAVA_OPTS% --add-opens=java.base/java.util.concurrent=ALL-UNNAMED" +set "JAVA_OPTS=%JAVA_OPTS% --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED" + +rem ----- Execute The Requested Command --------------------------------------- + +echo Using CATALINA_BASE: "%CATALINA_BASE%" +echo Using CATALINA_HOME: "%CATALINA_HOME%" +echo Using CATALINA_TMPDIR: "%CATALINA_TMPDIR%" +if ""%1"" == ""debug"" goto use_jdk +echo Using JRE_HOME: "%JRE_HOME%" +goto java_dir_displayed +:use_jdk +echo Using JAVA_HOME: "%JAVA_HOME%" +:java_dir_displayed +echo Using CLASSPATH: "%CLASSPATH%" +echo Using CATALINA_OPTS: "%CATALINA_OPTS%" + +set _EXECJAVA="%_RUNJAVA%" +set MAINCLASS=org.apache.catalina.startup.Bootstrap +set ACTION=start +set SECURITY_POLICY_FILE= +set DEBUG_OPTS= +set JPDA= + +if not ""%1"" == ""jpda"" goto noJpda +set JPDA=jpda +if not "%JPDA_TRANSPORT%" == "" goto gotJpdaTransport +set JPDA_TRANSPORT=dt_socket +:gotJpdaTransport +if not "%JPDA_ADDRESS%" == "" goto gotJpdaAddress +set JPDA_ADDRESS=localhost:8000 +:gotJpdaAddress +if not "%JPDA_SUSPEND%" == "" goto gotJpdaSuspend +set JPDA_SUSPEND=n +:gotJpdaSuspend +if not "%JPDA_OPTS%" == "" goto gotJpdaOpts +set JPDA_OPTS=-agentlib:jdwp=transport=%JPDA_TRANSPORT%,address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND% +:gotJpdaOpts +shift +:noJpda + +if ""%1"" == ""debug"" goto doDebug +if ""%1"" == ""run"" goto doRun +if ""%1"" == ""start"" goto doStart +if ""%1"" == ""stop"" goto doStop +if ""%1"" == ""configtest"" goto doConfigTest +if ""%1"" == ""version"" goto doVersion + +echo Usage: catalina ( commands ... ) +echo commands: +echo debug Start Catalina in a debugger +echo debug -security Debug Catalina with a security manager +echo jpda start Start Catalina under JPDA debugger +echo run Start Catalina in the current window +echo run -security Start in the current window with security manager +echo start Start Catalina in a separate window +echo start -security Start in a separate window with security manager +echo stop Stop Catalina +echo configtest Run a basic syntax check on server.xml +echo version What version of tomcat are you running? +goto end + +:doDebug +shift +set _EXECJAVA="%_RUNJDB%" +set DEBUG_OPTS=-sourcepath "%CATALINA_HOME%\..\..\java" +if not ""%1"" == ""-security"" goto execCmd +shift +echo Using Security Manager +set "SECURITY_POLICY_FILE=%CATALINA_BASE%\conf\catalina.policy" +goto execCmd + +:doRun +shift +if not ""%1"" == ""-security"" goto execCmd +shift +echo Using Security Manager +set "SECURITY_POLICY_FILE=%CATALINA_BASE%\conf\catalina.policy" +goto execCmd + +:doStart +shift +if "%TITLE%" == "" set TITLE=Tomcat +set _EXECJAVA=start "%TITLE%" "%_RUNJAVA%" +if not ""%1"" == ""-security"" goto execCmd +shift +echo Using Security Manager +set "SECURITY_POLICY_FILE=%CATALINA_BASE%\conf\catalina.policy" +goto execCmd + +:doStop +shift +set ACTION=stop +set CATALINA_OPTS= +goto execCmd + +:doConfigTest +shift +set ACTION=configtest +set CATALINA_OPTS= +goto execCmd + +:doVersion +%_EXECJAVA% %JAVA_OPTS% -classpath "%CATALINA_HOME%\lib\catalina.jar" org.apache.catalina.util.ServerInfo +goto end + + +:execCmd +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +rem Execute Java with the applicable properties +if not "%JPDA%" == "" goto doJpda +if not "%SECURITY_POLICY_FILE%" == "" goto doSecurity +%_EXECJAVA% %CATALINA_LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% +goto end +:doSecurity +%_EXECJAVA% %CATALINA_LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -classpath "%CLASSPATH%" -Djava.security.manager -Djava.security.policy=="%SECURITY_POLICY_FILE%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% +goto end +:doJpda +if not "%SECURITY_POLICY_FILE%" == "" goto doSecurityJpda +%_EXECJAVA% %CATALINA_LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %JPDA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% +goto end +:doSecurityJpda +%_EXECJAVA% %CATALINA_LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %JPDA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -classpath "%CLASSPATH%" -Djava.security.manager -Djava.security.policy=="%SECURITY_POLICY_FILE%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION% +goto end + +:end diff --git a/bin/catalina.sh b/bin/catalina.sh new file mode 100755 index 0000000..32f87ff --- /dev/null +++ b/bin/catalina.sh @@ -0,0 +1,646 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Control Script for the CATALINA Server +# +# For supported commands call "catalina.sh help" or see the usage section at +# the end of this file. +# +# Environment Variable Prerequisites +# +# Do not set the variables in this script. Instead put them into a script +# setenv.sh in CATALINA_BASE/bin to keep your customizations separate. +# +# CATALINA_HOME May point at your Catalina "build" directory. +# +# CATALINA_BASE (Optional) Base directory for resolving dynamic portions +# of a Catalina installation. If not present, resolves to +# the same directory that CATALINA_HOME points to. +# +# CATALINA_OUT (Optional) Full path to a file where stdout and stderr +# will be redirected. +# Default is $CATALINA_BASE/logs/catalina.out +# +# CATALINA_OUT_CMD (Optional) Command which will be executed and receive +# as its stdin the stdout and stderr from the Tomcat java +# process. If CATALINA_OUT_CMD is set, the value of +# CATALINA_OUT will be used as a named pipe. +# No default. +# Example (all one line) +# CATALINA_OUT_CMD="/usr/bin/rotatelogs -f $CATALINA_BASE/logs/catalina.out.%Y-%m-%d.log 86400" +# +# CATALINA_OPTS (Optional) Java runtime options used when the "start", +# "run" or "debug" command is executed. +# Include here and not in JAVA_OPTS all options, that should +# only be used by Tomcat itself, not by the stop process, +# the version command etc. +# Examples are heap size, GC logging, JMX ports etc. +# +# CATALINA_TMPDIR (Optional) Directory path location of temporary directory +# the JVM should use (java.io.tmpdir). Defaults to +# $CATALINA_BASE/temp. +# +# JAVA_HOME Must point at your Java Development Kit installation. +# Required to run the with the "debug" argument. +# +# JRE_HOME Must point at your Java Runtime installation. +# Defaults to JAVA_HOME if empty. If JRE_HOME and JAVA_HOME +# are both set, JRE_HOME is used. +# +# JAVA_OPTS (Optional) Java runtime options used when any command +# is executed. +# Include here and not in CATALINA_OPTS all options, that +# should be used by Tomcat and also by the stop process, +# the version command etc. +# Most options should go into CATALINA_OPTS. +# +# JPDA_TRANSPORT (Optional) JPDA transport used when the "jpda start" +# command is executed. The default is "dt_socket". +# +# JPDA_ADDRESS (Optional) Java runtime options used when the "jpda start" +# command is executed. The default is localhost:8000. +# +# JPDA_SUSPEND (Optional) Java runtime options used when the "jpda start" +# command is executed. Specifies whether JVM should suspend +# execution immediately after startup. Default is "n". +# +# JPDA_OPTS (Optional) Java runtime options used when the "jpda start" +# command is executed. If used, JPDA_TRANSPORT, JPDA_ADDRESS, +# and JPDA_SUSPEND are ignored. Thus, all required jpda +# options MUST be specified. The default is: +# +# -agentlib:jdwp=transport=$JPDA_TRANSPORT, +# address=$JPDA_ADDRESS,server=y,suspend=$JPDA_SUSPEND +# +# JSSE_OPTS (Optional) Java runtime options used to control the TLS +# implementation when JSSE is used. Default is: +# "-Djdk.tls.ephemeralDHKeySize=2048" +# +# CATALINA_PID (Optional) Path of the file which should contains the pid +# of the catalina startup java process, when start (fork) is +# used +# +# CATALINA_LOGGING_CONFIG (Optional) Override Tomcat's logging config file +# Example (all one line) +# CATALINA_LOGGING_CONFIG="-Djava.util.logging.config.file=$CATALINA_BASE/conf/logging.properties" +# +# LOGGING_MANAGER (Optional) Override Tomcat's logging manager +# Example (all one line) +# LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" +# +# UMASK (Optional) Override Tomcat's default UMASK of 0027 +# +# USE_NOHUP (Optional) If set to the string true the start command will +# use nohup so that the Tomcat process will ignore any hangup +# signals. Default is "false" unless running on HP-UX in which +# case the default is "true" +# ----------------------------------------------------------------------------- + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +os400=false +hpux=false +case "`uname`" in +CYGWIN*) cygwin=true;; +Darwin*) darwin=true;; +OS400*) os400=true;; +HP-UX*) hpux=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +# Get standard environment variables +PRGDIR=`dirname "$PRG"` + +# Only set CATALINA_HOME if not already set +[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd` + +# Copy CATALINA_BASE from CATALINA_HOME if not already set +[ -z "$CATALINA_BASE" ] && CATALINA_BASE="$CATALINA_HOME" + +# Ensure that any user defined CLASSPATH variables are not used on startup, +# but allow them to be specified in setenv.sh, in rare case when it is needed. +CLASSPATH= + +if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then + . "$CATALINA_BASE/bin/setenv.sh" +elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then + . "$CATALINA_HOME/bin/setenv.sh" +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$JRE_HOME" ] && JRE_HOME=`cygpath --unix "$JRE_HOME"` + [ -n "$CATALINA_HOME" ] && CATALINA_HOME=`cygpath --unix "$CATALINA_HOME"` + [ -n "$CATALINA_BASE" ] && CATALINA_BASE=`cygpath --unix "$CATALINA_BASE"` + [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# Ensure that neither CATALINA_HOME nor CATALINA_BASE contains a colon +# as this is used as the separator in the classpath and Java provides no +# mechanism for escaping if the same character appears in the path. +case $CATALINA_HOME in + *:*) echo "Using CATALINA_HOME: $CATALINA_HOME"; + echo "Unable to start as CATALINA_HOME contains a colon (:) character"; + exit 1; +esac +case $CATALINA_BASE in + *:*) echo "Using CATALINA_BASE: $CATALINA_BASE"; + echo "Unable to start as CATALINA_BASE contains a colon (:) character"; + exit 1; +esac + +# For OS400 +if $os400; then + # Set job priority to standard for interactive (interactive - 6) by using + # the interactive priority - 6, the helper threads that respond to requests + # will be running at the same priority as interactive jobs. + COMMAND='chgjob job('$JOBNAME') runpty(6)' + system $COMMAND + + # Enable multi threading + export QIBM_MULTI_THREADED=Y +fi + +# Get standard Java environment variables +if $os400; then + # -r will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + . "$CATALINA_HOME"/bin/setclasspath.sh +else + if [ -r "$CATALINA_HOME"/bin/setclasspath.sh ]; then + . "$CATALINA_HOME"/bin/setclasspath.sh + else + echo "Cannot find $CATALINA_HOME/bin/setclasspath.sh" + echo "This file is needed to run this program" + exit 1 + fi +fi + +# Add on extra jar files to CLASSPATH +if [ ! -z "$CLASSPATH" ] ; then + CLASSPATH="$CLASSPATH": +fi +CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar + +if [ -z "$CATALINA_OUT" ] ; then + CATALINA_OUT="$CATALINA_BASE"/logs/catalina.out +fi + +if [ -z "$CATALINA_TMPDIR" ] ; then + # Define the java.io.tmpdir to use for Catalina + CATALINA_TMPDIR="$CATALINA_BASE"/temp +fi + +# Add tomcat-juli.jar to classpath +# tomcat-juli.jar can be over-ridden per instance +if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then + CLASSPATH=$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar +else + CLASSPATH=$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar +fi + +# Bugzilla 37848: When no TTY is available, don't output to console +have_tty=0 +if [ -t 0 ]; then + have_tty=1 +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + JAVA_HOME=`cygpath --absolute --windows "$JAVA_HOME"` + JRE_HOME=`cygpath --absolute --windows "$JRE_HOME"` + CATALINA_HOME=`cygpath --absolute --windows "$CATALINA_HOME"` + CATALINA_BASE=`cygpath --absolute --windows "$CATALINA_BASE"` + CATALINA_TMPDIR=`cygpath --absolute --windows "$CATALINA_TMPDIR"` + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +if [ -z "$JSSE_OPTS" ] ; then + JSSE_OPTS="-Djdk.tls.ephemeralDHKeySize=2048" +fi +JAVA_OPTS="$JAVA_OPTS $JSSE_OPTS" + +# Register custom URL handlers +# Do this here so custom URL handles (specifically 'war:...') can be used in the security policy +JAVA_OPTS="$JAVA_OPTS -Djava.protocol.handler.pkgs=org.apache.catalina.webresources" + +# Set juli LogManager config file if it is present and an override has not been issued +if [ -z "$CATALINA_LOGGING_CONFIG" ]; then + if [ -r "$CATALINA_BASE"/conf/logging.properties ]; then + CATALINA_LOGGING_CONFIG="-Djava.util.logging.config.file=$CATALINA_BASE/conf/logging.properties" + else + # Bugzilla 45585 + CATALINA_LOGGING_CONFIG="-Dnop" + fi +fi + +if [ -z "$LOGGING_MANAGER" ]; then + LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" +fi + +# Set UMASK unless it has been overridden +if [ -z "$UMASK" ]; then + UMASK="0027" +fi +umask $UMASK + +# Make the umask available when using the org.apache.catalina.security.SecurityListener +JAVA_OPTS="$JAVA_OPTS -Dorg.apache.catalina.security.SecurityListener.UMASK=`umask`" + +if [ -z "$USE_NOHUP" ]; then + if $hpux; then + USE_NOHUP="true" + else + USE_NOHUP="false" + fi +fi +unset _NOHUP +if [ "$USE_NOHUP" = "true" ]; then + _NOHUP="nohup" +fi + +# Add the module start-up parameters required by Tomcat +JAVA_OPTS="$JAVA_OPTS --add-opens=java.base/java.lang=ALL-UNNAMED" +JAVA_OPTS="$JAVA_OPTS --add-opens=java.base/java.io=ALL-UNNAMED" +JAVA_OPTS="$JAVA_OPTS --add-opens=java.base/java.util=ALL-UNNAMED" +JAVA_OPTS="$JAVA_OPTS --add-opens=java.base/java.util.concurrent=ALL-UNNAMED" +JAVA_OPTS="$JAVA_OPTS --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED" + +# ----- Execute The Requested Command ----------------------------------------- + +# Bugzilla 37848: only output this if we have a TTY +if [ $have_tty -eq 1 ]; then + echo "Using CATALINA_BASE: $CATALINA_BASE" + echo "Using CATALINA_HOME: $CATALINA_HOME" + echo "Using CATALINA_TMPDIR: $CATALINA_TMPDIR" + if [ "$1" = "debug" ] ; then + echo "Using JAVA_HOME: $JAVA_HOME" + else + echo "Using JRE_HOME: $JRE_HOME" + fi + echo "Using CLASSPATH: $CLASSPATH" + echo "Using CATALINA_OPTS: $CATALINA_OPTS" + if [ ! -z "$CATALINA_PID" ]; then + echo "Using CATALINA_PID: $CATALINA_PID" + fi +fi + +if [ "$1" = "jpda" ] ; then + if [ -z "$JPDA_TRANSPORT" ]; then + JPDA_TRANSPORT="dt_socket" + fi + if [ -z "$JPDA_ADDRESS" ]; then + JPDA_ADDRESS="localhost:8000" + fi + if [ -z "$JPDA_SUSPEND" ]; then + JPDA_SUSPEND="n" + fi + if [ -z "$JPDA_OPTS" ]; then + JPDA_OPTS="-agentlib:jdwp=transport=$JPDA_TRANSPORT,address=$JPDA_ADDRESS,server=y,suspend=$JPDA_SUSPEND" + fi + CATALINA_OPTS="$JPDA_OPTS $CATALINA_OPTS" + shift +fi + +if [ "$1" = "debug" ] ; then + if $os400; then + echo "Debug command not available on OS400" + exit 1 + else + shift + if [ "$1" = "-security" ] ; then + if [ $have_tty -eq 1 ]; then + echo "Using Security Manager" + fi + shift + eval exec "\"$_RUNJDB\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \ + -classpath "$CLASSPATH" \ + -sourcepath "$CATALINA_HOME"/../../java \ + -Djava.security.manager \ + -Djava.security.policy=="$CATALINA_BASE"/conf/catalina.policy \ + -Dcatalina.base="$CATALINA_BASE" \ + -Dcatalina.home="$CATALINA_HOME" \ + -Djava.io.tmpdir="$CATALINA_TMPDIR" \ + org.apache.catalina.startup.Bootstrap "$@" start + else + eval exec "\"$_RUNJDB\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \ + -classpath "$CLASSPATH" \ + -sourcepath "$CATALINA_HOME"/../../java \ + -Dcatalina.base="$CATALINA_BASE" \ + -Dcatalina.home="$CATALINA_HOME" \ + -Djava.io.tmpdir="$CATALINA_TMPDIR" \ + org.apache.catalina.startup.Bootstrap "$@" start + fi + fi + +elif [ "$1" = "run" ]; then + + shift + if [ "$1" = "-security" ] ; then + if [ $have_tty -eq 1 ]; then + echo "Using Security Manager" + fi + shift + eval exec "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Djava.security.manager \ + -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ + org.apache.catalina.startup.Bootstrap "$@" start + else + eval exec "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ + org.apache.catalina.startup.Bootstrap "$@" start + fi + +elif [ "$1" = "start" ] ; then + + if [ ! -z "$CATALINA_PID" ]; then + if [ -f "$CATALINA_PID" ]; then + if [ -s "$CATALINA_PID" ]; then + echo "Existing PID file found during start." + if [ -r "$CATALINA_PID" ]; then + PID=`cat "$CATALINA_PID"` + ps -p $PID >/dev/null 2>&1 + if [ $? -eq 0 ] ; then + echo "Tomcat appears to still be running with PID $PID. Start aborted." + echo "If the following process is not a Tomcat process, remove the PID file and try again:" + ps -f -p $PID + exit 1 + else + echo "Removing/clearing stale PID file." + rm -f "$CATALINA_PID" >/dev/null 2>&1 + if [ $? != 0 ]; then + if [ -w "$CATALINA_PID" ]; then + cat /dev/null > "$CATALINA_PID" + else + echo "Unable to remove or clear stale PID file. Start aborted." + exit 1 + fi + fi + fi + else + echo "Unable to read PID file. Start aborted." + exit 1 + fi + else + rm -f "$CATALINA_PID" >/dev/null 2>&1 + if [ $? != 0 ]; then + if [ ! -w "$CATALINA_PID" ]; then + echo "Unable to remove or write to empty PID file. Start aborted." + exit 1 + fi + fi + fi + fi + fi + + shift + if [ -z "$CATALINA_OUT_CMD" ] ; then + touch "$CATALINA_OUT" + else + if [ ! -e "$CATALINA_OUT" ]; then + if ! mkfifo "$CATALINA_OUT"; then + echo "cannot create named pipe $CATALINA_OUT. Start aborted." + exit 1 + fi + elif [ ! -p "$CATALINA_OUT" ]; then + echo "$CATALINA_OUT exists and is not a named pipe. Start aborted." + exit 1 + fi + $CATALINA_OUT_CMD <"$CATALINA_OUT" & + fi + if [ "$1" = "-security" ] ; then + if [ $have_tty -eq 1 ]; then + echo "Using Security Manager" + fi + shift + eval $_NOHUP "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Djava.security.manager \ + -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ + org.apache.catalina.startup.Bootstrap "$@" start \ + >> "$CATALINA_OUT" 2>&1 "&" + + else + eval $_NOHUP "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ + org.apache.catalina.startup.Bootstrap "$@" start \ + >> "$CATALINA_OUT" 2>&1 "&" + + fi + + if [ ! -z "$CATALINA_PID" ]; then + echo $! > "$CATALINA_PID" + fi + + echo "Tomcat started." + +elif [ "$1" = "stop" ] ; then + + shift + + SLEEP=5 + if [ ! -z "$1" ]; then + echo $1 | grep "[^0-9]" >/dev/null 2>&1 + if [ $? -gt 0 ]; then + SLEEP=$1 + shift + fi + fi + + FORCE=0 + if [ "$1" = "-force" ]; then + shift + FORCE=1 + fi + + if [ ! -z "$CATALINA_PID" ]; then + if [ -f "$CATALINA_PID" ]; then + if [ -s "$CATALINA_PID" ]; then + kill -0 `cat "$CATALINA_PID"` >/dev/null 2>&1 + if [ $? -gt 0 ]; then + echo "PID file found but either no matching process was found or the current user does not have permission to stop the process. Stop aborted." + exit 1 + fi + else + echo "PID file is empty and has been ignored." + fi + else + echo "\$CATALINA_PID was set but the specified file does not exist. Is Tomcat running? Stop aborted." + exit 1 + fi + fi + + eval "\"$_RUNJAVA\"" $LOGGING_MANAGER "$JAVA_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ + org.apache.catalina.startup.Bootstrap "$@" stop + + # stop failed. Shutdown port disabled? Try a normal kill. + if [ $? != 0 ]; then + if [ ! -z "$CATALINA_PID" ]; then + echo "The stop command failed. Attempting to signal the process to stop through OS signal." + kill -15 `cat "$CATALINA_PID"` >/dev/null 2>&1 + fi + fi + + if [ ! -z "$CATALINA_PID" ]; then + if [ -f "$CATALINA_PID" ]; then + while [ $SLEEP -ge 0 ]; do + kill -0 `cat "$CATALINA_PID"` >/dev/null 2>&1 + if [ $? -gt 0 ]; then + rm -f "$CATALINA_PID" >/dev/null 2>&1 + if [ $? != 0 ]; then + if [ -w "$CATALINA_PID" ]; then + cat /dev/null > "$CATALINA_PID" + # If Tomcat has stopped don't try and force a stop with an empty PID file + FORCE=0 + else + echo "The PID file could not be removed or cleared." + fi + fi + echo "Tomcat stopped." + break + fi + if [ $SLEEP -gt 0 ]; then + sleep 1 + fi + if [ $SLEEP -eq 0 ]; then + echo "Tomcat did not stop in time." + if [ $FORCE -eq 0 ]; then + echo "PID file was not removed." + fi + echo "To aid diagnostics a thread dump has been written to standard out." + kill -3 `cat "$CATALINA_PID"` + fi + SLEEP=`expr $SLEEP - 1 ` + done + fi + fi + + KILL_SLEEP_INTERVAL=5 + if [ $FORCE -eq 1 ]; then + if [ -z "$CATALINA_PID" ]; then + echo "Kill failed: \$CATALINA_PID not set" + else + if [ -f "$CATALINA_PID" ]; then + PID=`cat "$CATALINA_PID"` + echo "Killing Tomcat with the PID: $PID" + kill -9 $PID + while [ $KILL_SLEEP_INTERVAL -ge 0 ]; do + kill -0 `cat "$CATALINA_PID"` >/dev/null 2>&1 + if [ $? -gt 0 ]; then + rm -f "$CATALINA_PID" >/dev/null 2>&1 + if [ $? != 0 ]; then + if [ -w "$CATALINA_PID" ]; then + cat /dev/null > "$CATALINA_PID" + else + echo "The PID file could not be removed." + fi + fi + echo "The Tomcat process has been killed." + break + fi + if [ $KILL_SLEEP_INTERVAL -gt 0 ]; then + sleep 1 + fi + KILL_SLEEP_INTERVAL=`expr $KILL_SLEEP_INTERVAL - 1 ` + done + if [ $KILL_SLEEP_INTERVAL -lt 0 ]; then + echo "Tomcat has not been killed completely yet. The process might be waiting on some system call or might be UNINTERRUPTIBLE." + fi + fi + fi + fi + +elif [ "$1" = "configtest" ] ; then + + eval "\"$_RUNJAVA\"" $LOGGING_MANAGER "$JAVA_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ + org.apache.catalina.startup.Bootstrap configtest + result=$? + if [ $result -ne 0 ]; then + echo "Configuration error detected!" + fi + exit $result + +elif [ "$1" = "version" ] ; then + + eval "\"$_RUNJAVA\"" "$JAVA_OPTS" \ + -classpath "\"$CATALINA_HOME/lib/catalina.jar\"" \ + org.apache.catalina.util.ServerInfo + +else + + echo "Usage: catalina.sh ( commands ... )" + echo "commands:" + if $os400; then + echo " debug Start Catalina in a debugger (not available on OS400)" + echo " debug -security Debug Catalina with a security manager (not available on OS400)" + else + echo " debug Start Catalina in a debugger" + echo " debug -security Debug Catalina with a security manager" + fi + echo " jpda start Start Catalina under JPDA debugger" + echo " run Start Catalina in the current window" + echo " run -security Start in the current window with security manager" + echo " start Start Catalina in a separate window" + echo " start -security Start in a separate window with security manager" + echo " stop Stop Catalina, waiting up to 5 seconds for the process to end" + echo " stop n Stop Catalina, waiting up to n seconds for the process to end" + echo " stop -force Stop Catalina, wait up to 5 seconds and then use kill -KILL if still running" + echo " stop n -force Stop Catalina, wait up to n seconds and then use kill -KILL if still running" + echo " configtest Run a basic syntax check on server.xml - check exit code for result" + echo " version What version of tomcat are you running?" + echo "Note: Waiting for the process to end and use of the -force option require that \$CATALINA_PID is defined" + exit 1 + +fi diff --git a/bin/ciphers.bat b/bin/ciphers.bat new file mode 100755 index 0000000..914181b --- /dev/null +++ b/bin/ciphers.bat @@ -0,0 +1,58 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Script to digest password using the algorithm specified +rem --------------------------------------------------------------------------- + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\tool-wrapper.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome +if exist "%CATALINA_HOME%\bin\tool-wrapper.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +set "EXECUTABLE=%CATALINA_HOME%\bin\tool-wrapper.bat" + +rem Check that target executable exists +if exist "%EXECUTABLE%" goto okExec +echo Cannot find "%EXECUTABLE%" +echo This file is needed to run this program +goto end +:okExec + +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +call "%EXECUTABLE%" org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser %CMD_LINE_ARGS% + +:end diff --git a/bin/ciphers.sh b/bin/ciphers.sh new file mode 100755 index 0000000..d4a87b5 --- /dev/null +++ b/bin/ciphers.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Script to digest password using the algorithm specified +# ----------------------------------------------------------------------------- + +# Better OS/400 detection: see Bugzilla 31132 +os400=false +case "`uname`" in +OS400*) os400=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +PRGDIR=`dirname "$PRG"` +EXECUTABLE=tool-wrapper.sh + +# Check that target executable exists +if $os400; then + # -x will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + eval +else + if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then + echo "Cannot find $PRGDIR/$EXECUTABLE" + echo "The file is absent or does not have execute permission" + echo "This file is needed to run this program" + exit 1 + fi +fi + +exec "$PRGDIR"/"$EXECUTABLE" org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser "$@" diff --git a/bin/configtest.bat b/bin/configtest.bat new file mode 100755 index 0000000..c26b65d --- /dev/null +++ b/bin/configtest.bat @@ -0,0 +1,58 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Configuration test script for the CATALINA Server +rem --------------------------------------------------------------------------- + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat" + +rem Check that target executable exists +if exist "%EXECUTABLE%" goto okExec +echo Cannot find "%EXECUTABLE%" +echo This file is needed to run this program +goto end +:okExec + +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +call "%EXECUTABLE%" configtest %CMD_LINE_ARGS% + +:end diff --git a/bin/configtest.sh b/bin/configtest.sh new file mode 100755 index 0000000..9a8ebff --- /dev/null +++ b/bin/configtest.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Configuration Test Script for the CATALINA Server +# ----------------------------------------------------------------------------- + +# Better OS/400 detection: see Bugzilla 31132 +os400=false +case "`uname`" in +OS400*) os400=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +PRGDIR=`dirname "$PRG"` +EXECUTABLE=catalina.sh + +# Check that target executable exists +if $os400; then + # -x will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + eval +else + if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then + echo "Cannot find $PRGDIR/$EXECUTABLE" + echo "The file is absent or does not have execute permission" + echo "This file is needed to run this program" + exit 1 + fi +fi + +exec "$PRGDIR"/"$EXECUTABLE" configtest "$@" diff --git a/bin/daemon.sh b/bin/daemon.sh new file mode 100755 index 0000000..b3a3363 --- /dev/null +++ b/bin/daemon.sh @@ -0,0 +1,270 @@ +#!/bin/sh + +# 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. +# +# ----------------------------------------------------------------------------- +# Commons Daemon wrapper script. +# ----------------------------------------------------------------------------- + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +DIRNAME="`dirname "$PRG"`" +PROGRAM="`basename "$PRG"`" +while [ ".$1" != . ] +do + case "$1" in + --java-home ) + JAVA_HOME="$2" + shift; shift; + continue + ;; + --catalina-home ) + CATALINA_HOME="$2" + shift; shift; + continue + ;; + --catalina-base ) + CATALINA_BASE="$2" + shift; shift; + continue + ;; + --catalina-pid ) + CATALINA_PID="$2" + shift; shift; + continue + ;; + --tomcat-user ) + TOMCAT_USER="$2" + shift; shift; + continue + ;; + --service-start-wait-time ) + SERVICE_START_WAIT_TIME="$2" + shift; shift; + continue + ;; + * ) + break + ;; + esac +done +# OS specific support (must be 'true' or 'false'). +cygwin=false; +darwin=false; +case "`uname`" in + CYGWIN*) + cygwin=true + ;; + Darwin*) + darwin=true + ;; +esac + +# Use the maximum available, or set MAX_FD != -1 to use that +test ".$MAX_FD" = . && MAX_FD="maximum" +# Setup parameters for running the jsvc +# +test ".$TOMCAT_USER" = . && TOMCAT_USER=tomcat +# Set JAVA_HOME to working JDK or JRE +# If not set we'll try to guess the JAVA_HOME +# from java binary if on the PATH +# +if [ -z "$JAVA_HOME" ]; then + JAVA_BIN="`which java 2>/dev/null || type java 2>&1`" + while [ -h "$JAVA_BIN" ]; do + ls=`ls -ld "$JAVA_BIN"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + JAVA_BIN="$link" + else + JAVA_BIN="`dirname "$JAVA_BIN"`/$link" + fi + done + test -x "$JAVA_BIN" && JAVA_HOME="`dirname "$JAVA_BIN"`" + test ".$JAVA_HOME" != . && JAVA_HOME=`cd "$JAVA_HOME/.." >/dev/null; pwd` +else + JAVA_BIN="$JAVA_HOME/bin/java" +fi + +# Only set CATALINA_HOME if not already set +test ".$CATALINA_HOME" = . && CATALINA_HOME=`cd "$DIRNAME/.." >/dev/null; pwd` +test ".$CATALINA_BASE" = . && CATALINA_BASE="$CATALINA_HOME" +test ".$CATALINA_MAIN" = . && CATALINA_MAIN=org.apache.catalina.startup.Bootstrap +# If not explicitly set, look for jsvc in CATALINA_BASE first then CATALINA_HOME +if [ -z "$JSVC" ]; then + JSVC="$CATALINA_BASE/bin/jsvc" + if [ ! -x "$JSVC" ]; then + JSVC="$CATALINA_HOME/bin/jsvc" + fi +fi +# Set the default service-start wait time if necessary +test ".$SERVICE_START_WAIT_TIME" = . && SERVICE_START_WAIT_TIME=10 + +# Ensure that any user defined CLASSPATH variables are not used on startup, +# but allow them to be specified in setenv.sh, in rare case when it is needed. +CLASSPATH= +JAVA_OPTS= +if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then + . "$CATALINA_BASE/bin/setenv.sh" +elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then + . "$CATALINA_HOME/bin/setenv.sh" +fi + +# Add on extra jar files to CLASSPATH +test ".$CLASSPATH" != . && CLASSPATH="$CLASSPATH:" +CLASSPATH="$CLASSPATH$CATALINA_HOME/bin/bootstrap.jar:$CATALINA_HOME/bin/commons-daemon.jar" + +test ".$CATALINA_OUT" = . && CATALINA_OUT="$CATALINA_BASE/logs/catalina-daemon.out" +test ".$CATALINA_TMP" = . && CATALINA_TMP="$CATALINA_BASE/temp" + +# Add tomcat-juli.jar to classpath +# tomcat-juli.jar can be over-ridden per instance +if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then + CLASSPATH="$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar" +else + CLASSPATH="$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar" +fi + +# Set juli LogManager config file if it is present and an override has not been issued +if [ -z "$CATALINA_LOGGING_CONFIG" ]; then + if [ -r "$CATALINA_BASE/conf/logging.properties" ]; then + CATALINA_LOGGING_CONFIG="-Djava.util.logging.config.file=$CATALINA_BASE/conf/logging.properties" + else + # Bugzilla 45585 + CATALINA_LOGGING_CONFIG="-Dnop" + fi +fi + +test ".$LOGGING_MANAGER" = . && LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" +JAVA_OPTS="$JAVA_OPTS $LOGGING_MANAGER" + +# Set -pidfile +test ".$CATALINA_PID" = . && CATALINA_PID="$CATALINA_BASE/logs/catalina-daemon.pid" + +# Increase the maximum file descriptors if we can +if [ "$cygwin" = "false" ]; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ "$?" -eq 0 ]; then + # Darwin does not allow RLIMIT_INFINITY on file soft limit + if [ "$darwin" = "true" ] && [ "$MAX_FD_LIMIT" = "unlimited" ]; then + MAX_FD_LIMIT=`/usr/sbin/sysctl -n kern.maxfilesperproc` + fi + test ".$MAX_FD" = ".maximum" && MAX_FD="$MAX_FD_LIMIT" + ulimit -n $MAX_FD + if [ "$?" -ne 0 ]; then + echo "$PROGRAM: Could not set maximum file descriptor limit: $MAX_FD" + fi + else + echo "$PROGRAM: Could not query system maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# Set UMASK unless it has been overridden +if [ -z "$UMASK" ]; then + UMASK="0027" +fi +umask $UMASK + +# ----- Execute The Requested Command ----------------------------------------- +case "$1" in + run ) + shift + eval exec "\"$JSVC\"" $* \ + "$JSVC_OPTS" \ + -java-home "\"$JAVA_HOME\"" \ + -pidfile "\"$CATALINA_PID\"" \ + -wait $SERVICE_START_WAIT_TIME \ + -umask $UMASK \ + -nodetach \ + -outfile "\"&1\"" \ + -errfile "\"&2\"" \ + -classpath "\"$CLASSPATH\"" \ + "\"$CATALINA_LOGGING_CONFIG\"" "$JAVA_OPTS" "$CATALINA_OPTS" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMP\"" \ + $CATALINA_MAIN + exit $? + ;; + start ) + eval "\"$JSVC\"" \ + "$JSVC_OPTS" \ + -java-home "\"$JAVA_HOME\"" \ + -user $TOMCAT_USER \ + -pidfile "\"$CATALINA_PID\"" \ + -wait $SERVICE_START_WAIT_TIME \ + -umask $UMASK \ + -outfile "\"$CATALINA_OUT\"" \ + -errfile "\"&1\"" \ + -classpath "\"$CLASSPATH\"" \ + "\"$CATALINA_LOGGING_CONFIG\"" "$JAVA_OPTS" "$CATALINA_OPTS" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMP\"" \ + $CATALINA_MAIN + exit $? + ;; + stop ) + eval "\"$JSVC\"" \ + "$JSVC_OPTS" \ + -stop \ + -pidfile "\"$CATALINA_PID\"" \ + -classpath "\"$CLASSPATH\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMP\"" \ + $CATALINA_MAIN + exit $? + ;; + version ) + "$JSVC" \ + -java-home "$JAVA_HOME" \ + -pidfile "$CATALINA_PID" \ + -classpath "$CLASSPATH" \ + -errfile "&2" \ + -version \ + -check \ + $CATALINA_MAIN + if [ "$?" = 0 ]; then + "$JAVA_BIN" \ + -classpath "$CATALINA_HOME/lib/catalina.jar" \ + org.apache.catalina.util.ServerInfo + fi + exit $? + ;; + * ) + echo "Unknown command: '$1'" + echo "Usage: $PROGRAM ( commands ... )" + echo "commands:" + echo " run Start Tomcat without detaching from console" + echo " start Start Tomcat" + echo " stop Stop Tomcat" + echo " version What version of commons daemon and Tomcat" + echo " are you running?" + exit 1 + ;; +esac diff --git a/bin/digest.bat b/bin/digest.bat new file mode 100755 index 0000000..2c1ffae --- /dev/null +++ b/bin/digest.bat @@ -0,0 +1,58 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Script to digest password using the algorithm specified +rem --------------------------------------------------------------------------- + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\tool-wrapper.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome +if exist "%CATALINA_HOME%\bin\tool-wrapper.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +set "EXECUTABLE=%CATALINA_HOME%\bin\tool-wrapper.bat" + +rem Check that target executable exists +if exist "%EXECUTABLE%" goto okExec +echo Cannot find "%EXECUTABLE%" +echo This file is needed to run this program +goto end +:okExec + +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +call "%EXECUTABLE%" -server org.apache.catalina.realm.RealmBase %CMD_LINE_ARGS% + +:end diff --git a/bin/digest.sh b/bin/digest.sh new file mode 100755 index 0000000..62ed5d0 --- /dev/null +++ b/bin/digest.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Script to digest password using the algorithm specified +# ----------------------------------------------------------------------------- + +# Better OS/400 detection: see Bugzilla 31132 +os400=false +case "`uname`" in +OS400*) os400=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +PRGDIR=`dirname "$PRG"` +EXECUTABLE=tool-wrapper.sh + +# Check that target executable exists +if $os400; then + # -x will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + eval +else + if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then + echo "Cannot find $PRGDIR/$EXECUTABLE" + echo "The file is absent or does not have execute permission" + echo "This file is needed to run this program" + exit 1 + fi +fi + +exec "$PRGDIR"/"$EXECUTABLE" -server org.apache.catalina.realm.RealmBase "$@" diff --git a/bin/makebase.bat b/bin/makebase.bat new file mode 100755 index 0000000..bce8b2f --- /dev/null +++ b/bin/makebase.bat @@ -0,0 +1,114 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem This script creates the directory structure required for running Tomcat +rem in a separate directory by pointing %CATALINA_BASE% to it. It copies the +rem conf directory from %CATALINA_HOME%, and creates empty directories for +rem bin, lib, logs, temp, webapps, and work. +rem +rem If the file %CATALINA_HOME%/bin/setenv.sh exists then it is copied to +rem the target directory as well. +rem +rem Usage: makebase [-w | --webapps] + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome + +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto EOF +:okHome + +rem first arg is the target directory +set BASE_TGT=%1 + +if %BASE_TGT%.==. ( + rem target directory not provided; exit + echo Usage: makebase ^ + goto :EOF +) + +set COPY_WEBAPPS=false + +rem parse args +for %%a in (%*) do ( + if "%%~a"=="--webapps" ( + set COPY_WEBAPPS=true + ) + if "%%~a"=="-w" ( + set COPY_WEBAPPS=true + ) +) + +if exist %BASE_TGT% ( + rem target directory exists + echo Target directory exists + + rem exit if target directory is not empty + for /F %%i in ('dir /b %BASE_TGT%\*.*') do ( + echo Target directory is not empty + goto :EOF + ) +) else ( + rem create the target directory + mkdir %BASE_TGT% +) + +rem create empty directories +for %%d in (bin, conf, lib, logs, temp, webapps, work) do ( + mkdir %BASE_TGT%\%%d +) + +if "%COPY_WEBAPPS%" == "true" ( + echo Copying webapps + robocopy %CATALINA_HOME%\webapps %BASE_TGT%\webapps /E > nul + rem copy conf directory recursively + robocopy %CATALINA_HOME%\conf %BASE_TGT%\conf /E > nul +) else ( + rem copy conf directory without subdirectories and suppress warning + robocopy %CATALINA_HOME%\conf %BASE_TGT%\conf > nul + rem create empty ROOT directory + mkdir %BASE_TGT%\webapps\ROOT +) + +rem copy setenv.bat if exists +robocopy %CATALINA_HOME%\bin %BASE_TGT%\bin setenv.bat > nul + +echo Created CATALINA_BASE directory at %BASE_TGT% + +echo. +echo You can launch the new instance by running: +echo set CATALINA_HOME=%CATALINA_HOME% +echo set CATALINA_BASE=%BASE_TGT% +echo %%CATALINA_HOME%%/bin/catalina.bat run + +echo. +echo Attention: The ports in conf\server.xml might be bound by a +echo different instance. Please review your config files +echo and update them where necessary. +echo. + +:EOF diff --git a/bin/makebase.sh b/bin/makebase.sh new file mode 100755 index 0000000..2b6eada --- /dev/null +++ b/bin/makebase.sh @@ -0,0 +1,115 @@ +#!/bin/sh + +# 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. + +# This script creates the directory structure required for running Tomcat +# in a separate directory by pointing $CATALINA_BASE to it. It copies the +# conf directory from $CATALINA_HOME, and creates empty directories for +# bin, lib, logs, temp, webapps, and work. +# +# If the file $CATALINA_HOME/bin/setenv.sh exists then it is copied to +# the target directory as well. +# +# Usage: makebase [-w | --webapps] + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +# Get standard environment variables +PRGDIR=`dirname "$PRG"` + +# Only set CATALINA_HOME if not already set +[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd` + +# first arg is the target directory +BASE_TGT=$1 + +if [ -z "$BASE_TGT" ]; then + # target directory not provided; exit + echo "Usage: makebase " + exit 1 +fi + +COPY_WEBAPPS=false + +# parse args +while [ "$1" != "" ]; do + case $1 in + -w | --webapps) + COPY_WEBAPPS=true + ;; + esac + shift +done + +if [ -d "$BASE_TGT" ]; then + # target directory exists + echo "Target directory exists" + + # exit if target directory is not empty + [ "`ls -A "$BASE_TGT"`" ] && \ + echo "Target directory is not empty" && \ + exit 1 +else + # create the target directory + mkdir -p "$BASE_TGT" +fi + +for dir in bin conf lib logs temp webapps work; +do + # create empty directories + mkdir "$BASE_TGT/$dir" +done + +if [ "$COPY_WEBAPPS" = true ]; then + echo "Copying webapps" + cp -r "$CATALINA_HOME/webapps" "$BASE_TGT/" + # copy conf directory recursively + cp -r "$CATALINA_HOME/conf" "$BASE_TGT/" +else + # copy conf directory without subdirectories and suppress warning + cp "${CATALINA_HOME}/conf"/* "$BASE_TGT/conf" 2> /dev/null + # create empty ROOT directory + mkdir "$BASE_TGT/webapps/ROOT" +fi + +# copy setenv.sh if exists +[ -f "$CATALINA_HOME/bin/setenv.sh" ] && \ + cp "$CATALINA_HOME/bin/setenv.sh" "$BASE_TGT/bin/" + +echo "Created CATALINA_BASE directory at $BASE_TGT" + +echo +echo "You can launch the new instance by running:" +echo " export CATALINA_HOME=$CATALINA_HOME" +echo " export CATALINA_BASE=$BASE_TGT" +echo " \$CATALINA_HOME/bin/catalina.sh run" + +echo +echo "Attention: The ports in conf/server.xml might be bound by a " +echo " different instance. Please review your config files " +echo " and update them as necessary." +echo diff --git a/bin/migrate.bat b/bin/migrate.bat new file mode 100755 index 0000000..62c2854 --- /dev/null +++ b/bin/migrate.bat @@ -0,0 +1,58 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Script call Apache Tomcat Migration Tool for Jakarta EE +rem --------------------------------------------------------------------------- + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\tool-wrapper.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome +if exist "%CATALINA_HOME%\bin\tool-wrapper.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +set "EXECUTABLE=%CATALINA_HOME%\bin\tool-wrapper.bat" + +rem Check that target executable exists +if exist "%EXECUTABLE%" goto okExec +echo Cannot find "%EXECUTABLE%" +echo This file is needed to run this program +goto end +:okExec + +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +call "%EXECUTABLE%" -server org.apache.tomcat.jakartaee.MigrationCLI %CMD_LINE_ARGS% + +:end diff --git a/bin/migrate.sh b/bin/migrate.sh new file mode 100755 index 0000000..d454207 --- /dev/null +++ b/bin/migrate.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Script call Apache Tomcat Migration Tool for Jakarta EE +# ----------------------------------------------------------------------------- + +# Better OS/400 detection: see Bugzilla 31132 +os400=false +case "`uname`" in +OS400*) os400=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +PRGDIR=`dirname "$PRG"` +EXECUTABLE=tool-wrapper.sh + +# Check that target executable exists +if $os400; then + # -x will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + eval +else + if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then + echo "Cannot find $PRGDIR/$EXECUTABLE" + echo "The file is absent or does not have execute permission" + echo "This file is needed to run this program" + exit 1 + fi +fi + +exec "$PRGDIR"/"$EXECUTABLE" -server org.apache.tomcat.jakartaee.MigrationCLI "$@" diff --git a/bin/service.bat b/bin/service.bat new file mode 100755 index 0000000..be6eca7 --- /dev/null +++ b/bin/service.bat @@ -0,0 +1,230 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem NT Service Install/Uninstall script +rem +rem Usage: service.bat install/remove [service_name [--rename]] [--user username] +rem +rem Options +rem install Install the service using default settings. +rem remove Remove the service from the system. +rem +rem service_name (optional) The name to use for the service. If not specified, +rem Tomcat@VERSION_MAJOR@ is used as the service name. +rem +rem --rename (optional) Rename tomcat@VERSION_MAJOR@.exe and tomcat@VERSION_MAJOR@w.exe to match +rem the non-default service name. +rem +rem username (optional) The name of the OS user to use to install/remove +rem the service (not the name of the OS user the +rem service will run as). If not specified, the current +rem user is used. +rem --------------------------------------------------------------------------- + +setlocal + +set "SELF=%~dp0%service.bat" + +set DEFAULT_SERVICE_NAME=Tomcat@VERSION_MAJOR@ +set SERVICE_NAME=%DEFAULT_SERVICE_NAME% + +set "CURRENT_DIR=%cd%" + +rem Parse the arguments +if "x%1x" == "xx" goto displayUsage +set SERVICE_CMD=%1 +shift +if "x%1x" == "xx" goto checkEnv +:checkUser +if "x%1x" == "x/userx" goto runAsUser +if "x%1x" == "x--userx" goto runAsUser +set SERVICE_NAME=%1 +shift +if "x%1x" == "xx" goto checkEnv +if "x%1x" == "x--renamex" ( + set RENAME=%1 + shift +) +if "x%1x" == "xx" goto checkEnv +goto checkUser +:runAsUser +shift +if "x%1x" == "xx" goto displayUsage +set SERVICE_USER=%1 +shift +runas /env /savecred /user:%SERVICE_USER% "%COMSPEC% /K \"%SELF%\" %SERVICE_CMD% %SERVICE_NAME%" +exit /b 0 + +rem Check the environment +:checkEnv + +rem Guess CATALINA_HOME if not defined +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%cd%" +if exist "%CATALINA_HOME%\bin\%DEFAULT_SERVICE_NAME%.exe" goto gotHome +if exist "%CATALINA_HOME%\bin\%SERVICE_NAME%.exe" goto gotHome +rem CD to the upper dir +cd .. +set "CATALINA_HOME=%cd%" +:gotHome +if exist "%CATALINA_HOME%\bin\%DEFAULT_SERVICE_NAME%.exe" ( + set "EXECUTABLE=%CATALINA_HOME%\bin\%DEFAULT_SERVICE_NAME%.exe" + goto okHome +) +if exist "%CATALINA_HOME%\bin\%SERVICE_NAME%.exe" ( + set "EXECUTABLE=%CATALINA_HOME%\bin\%SERVICE_NAME%.exe" + goto okHome +) +if "%DEFAULT_SERVICE_NAME%"== "%SERVICE_NAME%" ( + echo The file %DEFAULT_SERVICE_NAME%.exe was not found... +) else ( + echo Neither the %DEFAULT_SERVICE_NAME%.exe file nor the %SERVICE_NAME%.exe file was found... +) +echo Either the CATALINA_HOME environment variable is not defined correctly or +echo the incorrect service name has been used. +echo Both the CATALINA_HOME environment variable and the correct service name +echo are required to run this program. +exit /b 1 +:okHome +cd "%CURRENT_DIR%" + +rem Make sure prerequisite environment variables are set +if not "%JRE_HOME%" == "" goto gotJreHome +if not "%JAVA_HOME%" == "" goto gotJavaHome +echo Neither the JAVA_HOME nor the JRE_HOME environment variable is defined +echo Service will try to guess them from the registry. +goto okJava + +:gotJavaHome +rem No JRE given, check if JAVA_HOME is usable as JRE_HOME +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHomeAsJre +rem Use JAVA_HOME as JRE_HOME +set "JRE_HOME=%JAVA_HOME%" +goto okJava + +:noJavaHomeAsJre +echo The JAVA_HOME environment variable is not defined correctly. +echo JAVA_HOME=%JAVA_HOME% +echo NB: JAVA_HOME should point to a JDK not a JRE. +exit /b 1 + +:gotJreHome +rem Check if we have a usable JRE +if not exist "%JRE_HOME%\bin\java.exe" goto noJreHome +goto okJava + +:noJreHome +rem Needed at least a JRE +echo The JRE_HOME environment variable is not defined correctly +echo JRE_HOME=%JRE_HOME% +echo This environment variable is needed to run this program +exit /b 1 + +:okJava +if not "%CATALINA_BASE%" == "" goto gotBase +set "CATALINA_BASE=%CATALINA_HOME%" + +:gotBase +rem Process the requested command +if /i %SERVICE_CMD% == install goto doInstall +if /i %SERVICE_CMD% == remove goto doRemove +if /i %SERVICE_CMD% == uninstall goto doRemove +echo Unknown parameter "%SERVICE_CMD%" +:displayUsage +echo. +echo Usage: service.bat install/remove [service_name [--rename]] [--user username] +exit /b 1 + +:doRemove +rem Remove the service +echo Removing the service '%SERVICE_NAME%' ... +echo Using CATALINA_BASE: "%CATALINA_BASE%" + +"%EXECUTABLE%" //DS//%SERVICE_NAME% ^ + --LogPath "%CATALINA_BASE%\logs" +if not errorlevel 1 goto removed +echo Failed removing '%SERVICE_NAME%' service +exit /b 1 +:removed +echo The service '%SERVICE_NAME%' has been removed +if exist "%CATALINA_HOME%\bin\%SERVICE_NAME%.exe" ( + rename "%SERVICE_NAME%.exe" "%DEFAULT_SERVICE_NAME%.exe" + rename "%SERVICE_NAME%w.exe" "%DEFAULT_SERVICE_NAME%w.exe" +) +exit /b 0 + +:doInstall +rem Install the service +echo Installing the service '%SERVICE_NAME%' ... +echo Using CATALINA_HOME: "%CATALINA_HOME%" +echo Using CATALINA_BASE: "%CATALINA_BASE%" +echo Using JRE_HOME: "%JRE_HOME%" + +rem Try to use the server jvm +set "JVM=%JRE_HOME%\bin\server\jvm.dll" +if exist "%JVM%" goto foundJvm +rem Try to use the client jvm +set "JVM=%JRE_HOME%\bin\client\jvm.dll" +if exist "%JVM%" goto foundJvm +echo Warning: Neither 'server' nor 'client' jvm.dll was found at JRE_HOME. +set JVM=auto +:foundJvm +echo Using JVM: "%JVM%" + +set "CLASSPATH=%CATALINA_HOME%\bin\bootstrap.jar;%CATALINA_BASE%\bin\tomcat-juli.jar" +if not "%CATALINA_HOME%" == "%CATALINA_BASE%" set "CLASSPATH=%CLASSPATH%;%CATALINA_HOME%\bin\tomcat-juli.jar" + +if "%SERVICE_STARTUP_MODE%" == "" set SERVICE_STARTUP_MODE=manual +if "%JvmMs%" == "" set JvmMs=128 +if "%JvmMx%" == "" set JvmMx=256 + +if exist "%CATALINA_HOME%\bin\%DEFAULT_SERVICE_NAME%.exe" ( + if "x%RENAME%x" == "x--renamex" ( + rename "%DEFAULT_SERVICE_NAME%.exe" "%SERVICE_NAME%.exe" + rename "%DEFAULT_SERVICE_NAME%w.exe" "%SERVICE_NAME%w.exe" + set "EXECUTABLE=%CATALINA_HOME%\bin\%SERVICE_NAME%.exe" + ) +) + +"%EXECUTABLE%" //IS//%SERVICE_NAME% ^ + --Description "Apache Tomcat @VERSION@ Server - https://tomcat.apache.org/" ^ + --DisplayName "Apache Tomcat @VERSION_MAJOR_MINOR@ %SERVICE_NAME%" ^ + --Install "%EXECUTABLE%" ^ + --LogPath "%CATALINA_BASE%\logs" ^ + --StdOutput auto ^ + --StdError auto ^ + --Classpath "%CLASSPATH%" ^ + --Jvm "%JVM%" ^ + --StartMode jvm ^ + --StopMode jvm ^ + --StartPath "%CATALINA_HOME%" ^ + --StopPath "%CATALINA_HOME%" ^ + --StartClass org.apache.catalina.startup.Bootstrap ^ + --StopClass org.apache.catalina.startup.Bootstrap ^ + --StartParams start ^ + --StopParams stop ^ + --JvmOptions "-Dcatalina.home=%CATALINA_HOME%;-Dcatalina.base=%CATALINA_BASE%;-Djava.io.tmpdir=%CATALINA_BASE%\temp;-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager;-Djava.util.logging.config.file=%CATALINA_BASE%\conf\logging.properties;%JvmArgs%" ^ + --JvmOptions9 "--add-opens=java.base/java.lang=ALL-UNNAMED#--add-opens=java.base/java.io=ALL-UNNAMED#--add-opens=java.base/java.util=ALL-UNNAMED#--add-opens=java.base/java.util.concurrent=ALL-UNNAMED#--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED" ^ + --Startup "%SERVICE_STARTUP_MODE%" ^ + --JvmMs "%JvmMs%" ^ + --JvmMx "%JvmMx%" +if not errorlevel 1 goto installed +echo Failed installing '%SERVICE_NAME%' service +exit /b 1 +:installed +echo The service '%SERVICE_NAME%' has been installed. +exit /b 0 \ No newline at end of file diff --git a/bin/setclasspath.bat b/bin/setclasspath.bat new file mode 100755 index 0000000..ded6ac1 --- /dev/null +++ b/bin/setclasspath.bat @@ -0,0 +1,95 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Set JAVA_HOME or JRE_HOME if not already set, ensure any provided settings +rem are valid and consistent with the selected start-up options. +rem --------------------------------------------------------------------------- + +rem Make sure prerequisite environment variables are set + +rem In debug mode we need a real JDK (JAVA_HOME) +if ""%1"" == ""debug"" goto needJavaHome + +rem Otherwise either JRE or JDK are fine +if not "%JRE_HOME%" == "" goto gotJreHome +if not "%JAVA_HOME%" == "" goto gotJavaHome +echo Neither the JAVA_HOME nor the JRE_HOME environment variable is defined +echo At least one of these environment variable is needed to run this program +goto exit + +:needJavaHome +rem Check if we have a usable JDK +if "%JAVA_HOME%" == "" goto noJavaHome +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome +if not exist "%JAVA_HOME%\bin\jdb.exe" goto noJavaHome +if not exist "%JAVA_HOME%\bin\javac.exe" goto noJavaHome +set "JRE_HOME=%JAVA_HOME%" +goto okJava + +:noJavaHome +echo The JAVA_HOME environment variable is not defined correctly. +echo JAVA_HOME=%JAVA_HOME% +echo It is needed to run this program in debug mode. +echo NB: JAVA_HOME should point to a JDK not a JRE. +goto exit + +:gotJavaHome +rem No JRE given, check if JAVA_HOME is usable as JRE_HOME +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHomeAsJre +rem Use JAVA_HOME as JRE_HOME +set "JRE_HOME=%JAVA_HOME%" +goto okJava + +:noJavaHomeAsJre +echo The JAVA_HOME environment variable is not defined correctly. +echo JAVA_HOME=%JAVA_HOME% +echo NB: JAVA_HOME should point to a JDK not a JRE. +goto exit + +:gotJreHome +rem Check if we have a usable JRE +if not exist "%JRE_HOME%\bin\java.exe" goto noJreHome +goto okJava + +:noJreHome +rem Needed at least a JRE +echo The JRE_HOME environment variable is not defined correctly +echo JRE_HOME=%JRE_HOME% +echo This environment variable is needed to run this program +goto exit + +:okJava +rem Don't override _RUNJAVA if the user has set it previously +if not "%_RUNJAVA%" == "" goto gotRunJava +rem Set standard command for invoking Java. +rem Also note the quoting as JRE_HOME may contain spaces. +set "_RUNJAVA=%JRE_HOME%\bin\java.exe" +:gotRunJava + +rem Don't override _RUNJDB if the user has set it previously +rem Also note the quoting as JAVA_HOME may contain spaces. +if not "%_RUNJDB%" == "" goto gotRunJdb +set "_RUNJDB=%JAVA_HOME%\bin\jdb.exe" +:gotRunJdb + +goto end + +:exit +exit /b 1 + +:end +exit /b 0 diff --git a/bin/setclasspath.sh b/bin/setclasspath.sh new file mode 100755 index 0000000..f56b2aa --- /dev/null +++ b/bin/setclasspath.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Set JAVA_HOME or JRE_HOME if not already set, ensure any provided settings +# are valid and consistent with the selected start-up options. +# ----------------------------------------------------------------------------- + +# Make sure prerequisite environment variables are set +if [ -z "$JAVA_HOME" ] && [ -z "$JRE_HOME" ]; then + if $darwin; then + # Bugzilla 54390 + if [ -x '/usr/libexec/java_home' ] ; then + export JAVA_HOME=`/usr/libexec/java_home` + # Bugzilla 37284 (reviewed). + elif [ -d "/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home" ]; then + export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home" + fi + else + JAVA_PATH=`which java 2>/dev/null` + if [ "x$JAVA_PATH" != "x" ]; then + JAVA_PATH=`dirname "$JAVA_PATH" 2>/dev/null` + JRE_HOME=`dirname "$JAVA_PATH" 2>/dev/null` + fi + if [ "x$JRE_HOME" = "x" ]; then + # XXX: Should we try other locations? + if [ -x /usr/bin/java ]; then + JRE_HOME=/usr + fi + fi + fi + if [ -z "$JAVA_HOME" ] && [ -z "$JRE_HOME" ]; then + echo "Neither the JAVA_HOME nor the JRE_HOME environment variable is defined" + echo "At least one of these environment variable is needed to run this program" + exit 1 + fi +fi +if [ -z "$JAVA_HOME" ] && [ "$1" = "debug" ]; then + echo "JAVA_HOME should point to a JDK in order to run in debug mode." + exit 1 +fi + +# If we're running under jdb, we need a full jdk. +if [ "$1" = "debug" ] ; then + if [ "$os400" = "true" ]; then + if [ ! -x "$JAVA_HOME"/bin/java ] || [ ! -x "$JAVA_HOME"/bin/javac ]; then + echo "The JAVA_HOME environment variable is not defined correctly" + echo "JAVA_HOME=$JAVA_HOME" + echo "This environment variable is needed to run this program" + echo "NB: JAVA_HOME should point to a JDK not a JRE" + exit 1 + fi + else + if [ ! -x "$JAVA_HOME"/bin/java ] || [ ! -x "$JAVA_HOME"/bin/jdb ] || [ ! -x "$JAVA_HOME"/bin/javac ]; then + echo "The JAVA_HOME environment variable is not defined correctly" + echo "JAVA_HOME=$JAVA_HOME" + echo "This environment variable is needed to run this program" + echo "NB: JAVA_HOME should point to a JDK not a JRE" + exit 1 + fi + fi +fi + +if [ -z "$JRE_HOME" ]; then + # JAVA_HOME_MUST be set + if [ ! -x "$JAVA_HOME"/bin/java ]; then + echo "The JAVA_HOME environment variable is not defined correctly" + echo "JAVA_HOME=$JAVA_HOME" + echo "This environment variable is needed to run this program" + echo "NB: JAVA_HOME should point to a JDK not a JRE" + exit 1 + fi + JRE_HOME="$JAVA_HOME" +else + if [ ! -x "$JRE_HOME"/bin/java ]; then + echo "The JRE_HOME environment variable is not defined correctly" + echo "JRE_HOME=$JRE_HOME" + echo "This environment variable is needed to run this program" + exit 1 + fi +fi + +# Set standard commands for invoking Java, if not already set. +if [ -z "$_RUNJAVA" ]; then + _RUNJAVA="$JRE_HOME"/bin/java +fi +if [ "$os400" != "true" ]; then + if [ -z "$_RUNJDB" ]; then + _RUNJDB="$JAVA_HOME"/bin/jdb + fi +fi diff --git a/bin/shutdown.bat b/bin/shutdown.bat new file mode 100755 index 0000000..d7040e3 --- /dev/null +++ b/bin/shutdown.bat @@ -0,0 +1,58 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Stop script for the CATALINA Server +rem --------------------------------------------------------------------------- + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat" + +rem Check that target executable exists +if exist "%EXECUTABLE%" goto okExec +echo Cannot find "%EXECUTABLE%" +echo This file is needed to run this program +goto end +:okExec + +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +call "%EXECUTABLE%" stop %CMD_LINE_ARGS% + +:end diff --git a/bin/shutdown.sh b/bin/shutdown.sh new file mode 100755 index 0000000..cd0c97d --- /dev/null +++ b/bin/shutdown.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Stop script for the CATALINA Server +# ----------------------------------------------------------------------------- + +# Better OS/400 detection: see Bugzilla 31132 +os400=false +case "`uname`" in +OS400*) os400=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +PRGDIR=`dirname "$PRG"` +EXECUTABLE=catalina.sh + +# Check that target executable exists +if $os400; then + # -x will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + eval +else + if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then + echo "Cannot find $PRGDIR/$EXECUTABLE" + echo "The file is absent or does not have execute permission" + echo "This file is needed to run this program" + exit 1 + fi +fi + +exec "$PRGDIR"/"$EXECUTABLE" stop "$@" diff --git a/bin/startup.bat b/bin/startup.bat new file mode 100755 index 0000000..8c8f663 --- /dev/null +++ b/bin/startup.bat @@ -0,0 +1,58 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Start script for the CATALINA Server +rem --------------------------------------------------------------------------- + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat" + +rem Check that target executable exists +if exist "%EXECUTABLE%" goto okExec +echo Cannot find "%EXECUTABLE%" +echo This file is needed to run this program +goto end +:okExec + +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +call "%EXECUTABLE%" start %CMD_LINE_ARGS% + +:end diff --git a/bin/startup.sh b/bin/startup.sh new file mode 100755 index 0000000..7b10287 --- /dev/null +++ b/bin/startup.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Start Script for the CATALINA Server +# ----------------------------------------------------------------------------- + +# Better OS/400 detection: see Bugzilla 31132 +os400=false +case "`uname`" in +OS400*) os400=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +PRGDIR=`dirname "$PRG"` +EXECUTABLE=catalina.sh + +# Check that target executable exists +if $os400; then + # -x will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + eval +else + if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then + echo "Cannot find $PRGDIR/$EXECUTABLE" + echo "The file is absent or does not have execute permission" + echo "This file is needed to run this program" + exit 1 + fi +fi + +exec "$PRGDIR"/"$EXECUTABLE" start "$@" diff --git a/bin/tool-wrapper.bat b/bin/tool-wrapper.bat new file mode 100755 index 0000000..e9320d8 --- /dev/null +++ b/bin/tool-wrapper.bat @@ -0,0 +1,90 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Wrapper script for command line tools +rem +rem Environment Variable Prerequisites +rem +rem CATALINA_HOME May point at your Catalina "build" directory. +rem +rem TOOL_OPTS (Optional) Java runtime options. +rem +rem JAVA_HOME Must point at your Java Development Kit installation. +rem Using JRE_HOME instead works as well. +rem +rem JRE_HOME Must point at your Java Runtime installation. +rem Defaults to JAVA_HOME if empty. If JRE_HOME and JAVA_HOME +rem are both set, JRE_HOME is used. +rem +rem JAVA_OPTS (Optional) Java runtime options. +rem --------------------------------------------------------------------------- + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\tool-wrapper.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome +if exist "%CATALINA_HOME%\bin\tool-wrapper.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +rem Ensure that any user defined CLASSPATH variables are not used on startup, +rem but allow them to be specified in setenv.bat, in rare case when it is needed. +set CLASSPATH= + +rem Get standard environment variables +if exist "%CATALINA_HOME%\bin\setenv.bat" call "%CATALINA_HOME%\bin\setenv.bat" + +rem Get standard Java environment variables +if exist "%CATALINA_HOME%\bin\setclasspath.bat" goto okSetclasspath +echo Cannot find "%CATALINA_HOME%\bin\setclasspath.bat" +echo This file is needed to run this program +goto end +:okSetclasspath +call "%CATALINA_HOME%\bin\setclasspath.bat" %1 +if errorlevel 1 goto end + +rem Add on extra jar files to CLASSPATH +rem Note that there are no quotes as we do not want to introduce random +rem quotes into the CLASSPATH +if "%CLASSPATH%" == "" goto emptyClasspath +set "CLASSPATH=%CLASSPATH%;" +:emptyClasspath +set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar;%CATALINA_HOME%\bin\tomcat-juli.jar;%CATALINA_HOME%\lib\servlet-api.jar;%CATALINA_HOME%\lib\tomcat-util.jar" + +set JAVA_OPTS=%JAVA_OPTS% -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager + +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +"%_RUNJAVA%" %JAVA_OPTS% %TOOL_OPTS% -classpath "%CLASSPATH%" -Dcatalina.home="%CATALINA_HOME%" org.apache.catalina.startup.Tool %CMD_LINE_ARGS% + +:end diff --git a/bin/tool-wrapper.sh b/bin/tool-wrapper.sh new file mode 100755 index 0000000..5a7f4b2 --- /dev/null +++ b/bin/tool-wrapper.sh @@ -0,0 +1,132 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Wrapper script for command line tools +# +# Environment Variable Prerequisites +# +# CATALINA_HOME May point at your Catalina "build" directory. +# +# TOOL_OPTS (Optional) Java runtime options. +# +# JAVA_HOME Must point at your Java Development Kit installation. +# Using JRE_HOME instead works as well. +# +# JRE_HOME Must point at your Java Runtime installation. +# Defaults to JAVA_HOME if empty. If JRE_HOME and JAVA_HOME +# are both set, JRE_HOME is used. +# +# JAVA_OPTS (Optional) Java runtime options. +# ----------------------------------------------------------------------------- + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +os400=false +case "`uname`" in +CYGWIN*) cygwin=true;; +Darwin*) darwin=true;; +OS400*) os400=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +# Get standard environment variables +PRGDIR=`dirname "$PRG"` + +# Only set CATALINA_HOME if not already set +[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd` + +# Ensure that any user defined CLASSPATH variables are not used on startup, +# but allow them to be specified in setenv.sh, in rare case when it is needed. +CLASSPATH= + +if [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then + . "$CATALINA_HOME/bin/setenv.sh" +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$JRE_HOME" ] && JRE_HOME=`cygpath --unix "$JRE_HOME"` + [ -n "$CATALINA_HOME" ] && CATALINA_HOME=`cygpath --unix "$CATALINA_HOME"` + [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For OS400 +if $os400; then + # Set job priority to standard for interactive (interactive - 6) by using + # the interactive priority - 6, the helper threads that respond to requests + # will be running at the same priority as interactive jobs. + COMMAND='chgjob job('$JOBNAME') runpty(6)' + system $COMMAND + + # Enable multi threading + export QIBM_MULTI_THREADED=Y +fi + +# Get standard Java environment variables +if $os400; then + # -r will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + . "$CATALINA_HOME"/bin/setclasspath.sh +else + if [ -r "$CATALINA_HOME"/bin/setclasspath.sh ]; then + . "$CATALINA_HOME"/bin/setclasspath.sh + else + echo "Cannot find $CATALINA_HOME/bin/setclasspath.sh" + echo "This file is needed to run this program" + exit 1 + fi +fi + +# Add on extra jar files to CLASSPATH +if [ ! -z "$CLASSPATH" ] ; then + CLASSPATH="$CLASSPATH": +fi +CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar:"$CATALINA_HOME"/bin/tomcat-juli.jar:"$CATALINA_HOME"/lib/servlet-api.jar:"$CATALINA_HOME"/lib/tomcat-util.jar + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + JAVA_HOME=`cygpath --absolute --windows "$JAVA_HOME"` + JRE_HOME=`cygpath --absolute --windows "$JRE_HOME"` + CATALINA_HOME=`cygpath --absolute --windows "$CATALINA_HOME"` + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +JAVA_OPTS="$JAVA_OPTS -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" + +# ----- Execute The Requested Command ----------------------------------------- + +eval exec "\"$_RUNJAVA\"" "$JAVA_OPTS" "$TOOL_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + org.apache.catalina.startup.Tool "$@" diff --git a/bin/version.bat b/bin/version.bat new file mode 100755 index 0000000..6c807bb --- /dev/null +++ b/bin/version.bat @@ -0,0 +1,58 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem --------------------------------------------------------------------------- +rem Version script for the CATALINA Server +rem --------------------------------------------------------------------------- + +setlocal + +rem Guess CATALINA_HOME if not defined +set "CURRENT_DIR=%cd%" +if not "%CATALINA_HOME%" == "" goto gotHome +set "CATALINA_HOME=%CURRENT_DIR%" +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +cd .. +set "CATALINA_HOME=%cd%" +cd "%CURRENT_DIR%" +:gotHome +if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome +echo The CATALINA_HOME environment variable is not defined correctly +echo This environment variable is needed to run this program +goto end +:okHome + +set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat" + +rem Check that target executable exists +if exist "%EXECUTABLE%" goto okExec +echo Cannot find "%EXECUTABLE%" +echo This file is needed to run this program +goto end +:okExec + +rem Get remaining unshifted command line arguments and save them in the +set CMD_LINE_ARGS= +:setArgs +if ""%1""=="""" goto doneSetArgs +set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1 +shift +goto setArgs +:doneSetArgs + +call "%EXECUTABLE%" version %CMD_LINE_ARGS% + +:end diff --git a/bin/version.sh b/bin/version.sh new file mode 100755 index 0000000..1cb19bd --- /dev/null +++ b/bin/version.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# 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. + +# ----------------------------------------------------------------------------- +# Version Script for the CATALINA Server +# ----------------------------------------------------------------------------- + +# Better OS/400 detection: see Bugzilla 31132 +os400=false +case "`uname`" in +OS400*) os400=true;; +esac + +# resolve links - $0 may be a softlink +PRG="$0" + +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +PRGDIR=`dirname "$PRG"` +EXECUTABLE=catalina.sh + +# Check that target executable exists +if $os400; then + # -x will Only work on the os400 if the files are: + # 1. owned by the user + # 2. owned by the PRIMARY group of the user + # this will not work if the user belongs in secondary groups + eval +else + if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then + echo "Cannot find $PRGDIR/$EXECUTABLE" + echo "The file is absent or does not have execute permission" + echo "This file is needed to run this program" + exit 1 + fi +fi + +exec "$PRGDIR"/"$EXECUTABLE" version "$@" diff --git a/build.properties.default b/build.properties.default new file mode 100644 index 0000000..0efb352 --- /dev/null +++ b/build.properties.default @@ -0,0 +1,352 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +# build.properties.default +# +# This file provides the defaults for build properties that are likely to: +# - change over time such as those related to dependencies +# - be ones that individual developers will wish to customise. +# +# To customise the build, create a build.properties file in the top-level +# source directory (where this file is located) and use it to define new +# values for the build properties you wish to change. In addition to +# changing any of the properties defined in this file, you can change any +# build property defined in build.xml. More information is available in +# BUILDING.txt. +# ----------------------------------------------------------------------------- + +# ----- Version Control Flags ----- +version.major=10 +version.minor=1 +version.build=23 +version.patch=0 +version.suffix= +version.dev=-dev + +# ----- Build tools ----- +ant.version.required=1.10.2 + +# ----- Build control flags ----- +compile.debug=true +# Do not pass -deprecation (-Xlint:deprecation) flag to javac +compile.deprecation=false + +# ----- Documentation properties ----- +git.branch=10.1.x + +# ----- Code quality tools +# Note enabling validation uses Checkstyle which is LGPL licensed +execute.validate=false + +# Note the JaCoCo code coverage tool is EPLv2 licensed +# Enabling code coverage extends the time taken to run the tests by ~50% +test.coverage=false + +# Note the SpotBugs is LGPL licensed +execute.spotbugs=false + +# Javadoc - warnings are disabled as they are noisy with Java 18+ and CheckStyle +# works better for Tomcat. +javadoc.failonerror=true +javadoc.failonwarning=false + +# ----- Test configuration ----- +execute.test.nio=true +execute.test.nio2=true +# Stop testing if a failure occurs +test.haltonfailure=false +# Activate AccessLog during testing +test.accesslog=false +# Display the tests output on the console +test.verbose=true + +# Number of parallel threads to use for testing. The recommended value is one +# thread per core. +test.threads=1 + +# Some platforms (e.g. OSX El Capitan) require IPv4 to be the default for the +# multicast tests to work +java.net.preferIPv4Stack=false + +# ----- Release build settings ----- +# Location of GPG executable +gpg.exec=/path/to/gpg + +# Code signing of Windows installer +# See https://infra.apache.org/digicert-use.html for setup instructions +do.codesigning=false +codesigning.alias=Tomcat-PMC-cert-2023-11 +codesigning.digest=SHA-512 +codesigning.storetype=DIGICERTONE +# Set codesigning.storepass in build.properties with the following syntax +#codesigning.storepass=|/path/to/Certificate_pkcs12.p12| + +# ----- Settings to control downloading of files ----- +execute.download=true +trydownload.httpusecaches=true + +# ----- Default base path for dependencies ----- +# Please note this path must be absolute, not relative, +# as it is referenced with different working directory +# contexts by the various build scripts. +base.path=${user.home}/tomcat-build-libs + +# ---- Download locations for dependencies ----- +base-apache.loc.1=https://dlcdn.apache.org +base-apache.loc.2=https://archive.apache.org/dist +base-commons.loc.1=${base-apache.loc.1}/commons +base-commons.loc.2=${base-apache.loc.2}/commons +base-tomcat.loc.1=${base-apache.loc.1}/tomcat +base-tomcat.loc.2=${base-apache.loc.2}/tomcat + +base-gh.loc=https://github.com +base-sf.loc=https://downloads.sourceforge.net +# repo.maven.apache.org is the same as repo2.maven.org +base-maven.loc=https://repo.maven.apache.org/maven2 + +# Mirror, was used when there were problems with the main SF downloads site +# base-sf.loc=https://sunet.dl.sourceforge.net + +# ----- Webservices - JAX RPC ----- +jaxrpc-lib.version=1.1-rc4 +jaxrpc-lib.checksum.enabled=true +jaxrpc-lib.checksum.algorithm=MD5|SHA-1 +jaxrpc-lib.checksum.value=4bebba22a4cdb9f68e16c45129770333|fe9371d33dc3e1646d4d13bde19614283eb998b1 +jaxrpc-lib.home=${base.path}/jaxrpc-${jaxrpc-lib.version} +jaxrpc-lib.jar=${jaxrpc-lib.home}/geronimo-spec-jaxrpc-${jaxrpc-lib.version}.jar +jaxrpc-lib.loc=${base-maven.loc}/geronimo-spec/geronimo-spec-jaxrpc/${jaxrpc-lib.version}/geronimo-spec-jaxrpc-${jaxrpc-lib.version}.jar + +# ----- Webservices - WSDL4J ----- +wsdl4j-lib.version=1.6.3 +wsdl4j-lib.checksum.enabled=true +wsdl4j-lib.checksum.algorithm=MD5|SHA-1 +wsdl4j-lib.checksum.value=cfc28d89625c5e88589aec7a9aee0208|6d106a6845a3d3477a1560008479312888e94f2f +wsdl4j-lib.home=${base.path}/wsdl4j-${wsdl4j-lib.version} +wsdl4j-lib.jar=${wsdl4j-lib.home}/wsdl4j-${wsdl4j-lib.version}.jar +wsdl4j-lib.loc=${base-maven.loc}/wsdl4j/wsdl4j/${wsdl4j-lib.version}/wsdl4j-${wsdl4j-lib.version}.jar + +# ----- Eclipse JDT, version 4.7 or later -----# +# See https://cwiki.apache.org/confluence/display/TOMCAT/Managing+Tomcat%27s+Dependency+on+the+Eclipse+JDT+Core+Batch+Compiler +# +# 4.27 is the latest release that runs on Java 11 +# Later versions can be used but the official builds need to use 4.27 +# +# Checksum is from "SHA512 Checksums for 4.27" link at +# https://download.eclipse.org/eclipse/downloads/drops4/R-4.27-202303020300/ +# https://download.eclipse.org/eclipse/downloads/drops4/R-4.27-202303020300/checksum/eclipse-4.27-SUMSSHA512 +# +jdt.version=4.27 +jdt.release=R-4.27-202303020300 +jdt.checksum.enabled=true +jdt.checksum.algorithm=SHA-512 +jdt.checksum.value=69b57e12aa7f1339fe86fdb82f8fe9a104ff4d5e887448a779059e4f0626c43af0f1539569d9669c3f3add54fce6447e0bdcec93ee52ad25bf9697f7ac59ca7f +jdt.home=${base.path}/ecj-${jdt.version} +jdt.jar=${jdt.home}/ecj-${jdt.version}.jar +# The download will be moved to the archive area eventually. We are taking care of that in advance. +jdt.loc.1=http://archive.eclipse.org/eclipse/downloads/drops4/${jdt.release}/ecj-${jdt.version}.jar +jdt.loc.2=http://download.eclipse.org/eclipse/downloads/drops4/${jdt.release}/ecj-${jdt.version}.jar + +# ----- Tomcat native library ----- +tomcat-native.version=2.0.7 +tomcat-native-openssl.version=3.0.13 +tomcat-native.src.checksum.enabled=true +tomcat-native.src.checksum.algorithm=SHA-512 +tomcat-native.src.checksum.value=625b334271494f7c86c06d6a8c6d13c06d9d6094f65cccbdc8d3df13ee2aae4cb42ad326e20bcbf8a8f141240111778991882f9bf87793b09a2920433d6c8c85 +tomcat-native.win.checksum.enabled=true +tomcat-native.win.checksum.algorithm=SHA-512 +tomcat-native.win.checksum.value=5473e62b3743c776442656abad3ef7edb1aa086ed3231a72efab91b700cbf14ec1bdb619a8303ceafb5304ce6b756e61411e8b2d7cbf4c4b1f0c285fe8aaa77b +tomcat-native.home=${base.path}/tomcat-native-${tomcat-native.version} +tomcat-native.tar.gz=${tomcat-native.home}/tomcat-native.tar.gz +tomcat-native.loc.1=${base-tomcat.loc.1}/tomcat-connectors/native/${tomcat-native.version}/source/tomcat-native-${tomcat-native.version}-src.tar.gz +tomcat-native.loc.2=${base-tomcat.loc.2}/tomcat-connectors/native/${tomcat-native.version}/source/tomcat-native-${tomcat-native.version}-src.tar.gz +tomcat-native.win.1=${base-tomcat.loc.1}/tomcat-connectors/native/${tomcat-native.version}/binaries/tomcat-native-${tomcat-native.version}-openssl-${tomcat-native-openssl.version}-win32-bin.zip +tomcat-native.win.2=${base-tomcat.loc.2}/tomcat-connectors/native/${tomcat-native.version}/binaries/tomcat-native-${tomcat-native.version}-openssl-${tomcat-native-openssl.version}-win32-bin.zip + +# ----- NSIS, version 3.0 or later ----- +nsis.version=3.09 +nsis.checksum.enabled=true +nsis.checksum.algorithm=MD5|SHA-1 +nsis.checksum.value=2953f6074bcc4711b439a666eafbb91b|586855a743a6e0ade203d8758af303a48ee0716b +nsis.home=${base.path}/nsis-${nsis.version} +nsis.exe=${nsis.home}/makensis.exe +nsis.arch.dir=x86-unicode/ +nsis.installoptions.dll=${nsis.home}/Plugins/${nsis.arch.dir}InstallOptions.dll +nsis.nsexec.dll=${nsis.home}/Plugins/${nsis.arch.dir}nsExec.dll +nsis.nsisdl.dll=${nsis.home}/Plugins/${nsis.arch.dir}NSISdl.dll +nsis.system.dll=${nsis.home}/Plugins/${nsis.arch.dir}System.dll +nsis.nsdialogs.dll=${nsis.home}/Plugins/${nsis.arch.dir}nsDialogs.dll +nsis.loc=${base-sf.loc}/nsis/nsis-${nsis.version}.zip + +# ----- Commons Daemon, version 1.2.0 or later ----- +commons-daemon.version=1.3.4 + +# checksum for commons-daemon-1.3.4-bin.tar.gz +commons-daemon.bin.checksum.enabled=true +commons-daemon.bin.checksum.algorithm=SHA-512 +commons-daemon.bin.checksum.value=adc301fe9c7e50c5ed71c6775c8c41c33a369a05c30785ccb81209089603ae66563e958b466c99fc5cd27c12625bb7def68d7d91933aa8739eb645af37f3d03e + +# checksums for commons-daemon-1.3.4-native-src.tar.gz, commons-daemon-1.3.4-bin-windows.zip +commons-daemon.native.src.checksum.enabled=true +commons-daemon.native.src.checksum.algorithm=SHA-512 +commons-daemon.native.src.checksum.value=3c10ca72fc0eb7f755c0b5452bb6d5e8b42d8f363767ffcd9a6f0883026e688ea7dff50ea05e2675a7cdf9f413cb8012ee6b79e16dfc1cd4d83bd775ea10216c +commons-daemon.native.win.checksum.enabled=true +commons-daemon.native.win.checksum.algorithm=SHA-512 +commons-daemon.native.win.checksum.value=57a59d402dd0a1c99ed5da062b4616d54679e4208abec8b25742f5bf3ec1ee6b5187bc830edeaa218766215371b5519ce0a7186325c929c86b567a3078aa7555 + +commons-daemon.home=${base.path}/commons-daemon-${commons-daemon.version} +commons-daemon.jar=${commons-daemon.home}/commons-daemon-${commons-daemon.version}.jar +commons-daemon.native.win.home=${commons-daemon.home}/windows +commons-daemon.native.win.mgr.exe=${commons-daemon.native.win.home}/prunmgr.exe +commons-daemon.native.src.tgz=${commons-daemon.home}/commons-daemon-${commons-daemon.version}-native-src.tar.gz +commons-daemon.native.win.zip=${commons-daemon.home}/commons-daemon-${commons-daemon.version}-bin-windows-signed.zip +commons-daemon.bin.loc.1=${base-commons.loc.1}/daemon/binaries/commons-daemon-${commons-daemon.version}-bin.tar.gz +commons-daemon.bin.loc.2=${base-commons.loc.2}/daemon/binaries/commons-daemon-${commons-daemon.version}-bin.tar.gz +commons-daemon.native.src.loc.1=${base-commons.loc.1}/daemon/source/commons-daemon-${commons-daemon.version}-native-src.tar.gz +commons-daemon.native.src.loc.2=${base-commons.loc.2}/daemon/source/commons-daemon-${commons-daemon.version}-native-src.tar.gz +commons-daemon.native.win.loc.1=${base-commons.loc.1}/daemon/binaries/windows/commons-daemon-${commons-daemon.version}-bin-windows.zip +commons-daemon.native.win.loc.2=${base-commons.loc.2}/daemon/binaries/windows/commons-daemon-${commons-daemon.version}-bin-windows.zip + +# ----- JUnit Unit Test Suite, version 4.11 or later ----- +junit.version=4.13.2 +junit.checksum.enabled=true +junit.checksum.algorithm=MD5|SHA-1 +junit.checksum.value=d98a9a02a99a9acd22d7653cbcc1f31f|8ac9e16d933b6fb43bc7f576336b8f4d7eb5ba12 +junit.home=${base.path}/junit-${junit.version} +junit.jar=${junit.home}/junit-${junit.version}.jar +junit.loc=${base-maven.loc}/junit/junit/${junit.version}/junit-${junit.version}.jar + +# ----- Hamcrest Library, used by JUnit, version 1.3 or later ---- +hamcrest.version=2.2 +hamcrest.checksum.enabled=true +hamcrest.checksum.algorithm=SHA-512 +hamcrest.checksum.value=6b1141329b83224f69f074cb913dbff6921d6b8693ede8d2599acb626481255dae63de42eb123cbd5f59a261ac32faae012be64e8e90406ae9215543fbca5546 +hamcrest.home=${base.path}/hamcrest-${hamcrest.version} +hamcrest.jar=${hamcrest.home}/hamcrest-${hamcrest.version}.jar +hamcrest.loc=${base-maven.loc}/org/hamcrest/hamcrest/${hamcrest.version}/hamcrest-${hamcrest.version}.jar + +# ----- EasyMock, version 3.2 or later ----- +easymock.version=4.3 +easymock.checksum.enabled=true +easymock.checksum.algorithm=MD5|SHA-1 +easymock.checksum.value=f4d141b8c32c022def9089ee4f890c90|b0dbe2df1a71b8115835561f46a8f06cb168a94f +easymock.home=${base.path}/easymock-${easymock.version} +easymock.jar=${easymock.home}/easymock-${easymock.version}.jar +easymock.loc=${base-maven.loc}/org/easymock/easymock/${easymock.version}/easymock-${easymock.version}.jar + +# ----- cglib, used by EasyMock, version 2.2 or later ----- +cglib.version=3.3.0 +cglib.checksum.enabled=true +cglib.checksum.algorithm=SHA-512 +cglib.checksum.value=faa1d2121e87ae69e179e3aae217accd0834e0da716b91a029fd526e192612e71675f2740bedf48e23ef1edc45f672a2be1b3e78bbfb1ad59c96dd3d2feeedba +cglib.home=${base.path}/cglib-${cglib.version} +cglib.jar=${cglib.home}/cglib-nodep-${cglib.version}.jar +cglib.loc=${base-maven.loc}/cglib/cglib-nodep/${cglib.version}/cglib-nodep-${cglib.version}.jar + +# ----- objenesis, used by EasyMock, version 1.2 or later ----- +objenesis.version=3.3 +objenesis.checksum.enabled=true +objenesis.checksum.algorithm=MD5|SHA-1 +objenesis.checksum.value=ab0e0b2ab81affdd7f38bcc60fd85571|1049c09f1de4331e8193e579448d0916d75b7631 +objenesis.home=${base.path}/objenesis-${objenesis.version} +objenesis.jar=${objenesis.home}/objenesis-${objenesis.version}.jar +objenesis.loc=${base-maven.loc}/org/objenesis/objenesis/${objenesis.version}/objenesis-${objenesis.version}.jar + +# ----- UnboundID, used by unit tests, version 5.1.4 or later ----- +unboundid.version=6.0.11 +unboundid.checksum.enabled=true +unboundid.checksum.algorithm=SHA-512 +unboundid.checksum.value=4bb1dc4adef77fd124d1b184556c44b44945fc69f62662c62f46cff9a6792c24ed385c6a01854797ec069df42286ba51b3d1e3c7a6ee9ee4a3e69908850ffa36 +unboundid.home=${base.path}/unboundid-${unboundid.version} +unboundid.jar=${unboundid.home}/unboundid-ldapsdk-${unboundid.version}.jar +unboundid.loc=${base-maven.loc}/com/unboundid/unboundid-ldapsdk/${unboundid.version}/unboundid-ldapsdk-${unboundid.version}.jar + +# ----- Checkstyle, version 6.16 or later ----- +checkstyle.version=10.14.1 +checkstyle.checksum.enabled=true +checkstyle.checksum.algorithm=SHA-512 +checkstyle.checksum.value=59c734883c7770429ef5f977f4139724da86caa932fb365a186e1bf47b5a6e04c718c1dba8ed383c0979a594586c608af1aa30bbec6f4c444c08c3009473e245 +checkstyle.home=${base.path}/checkstyle-${checkstyle.version} +checkstyle.jar=${checkstyle.home}/checkstyle-${checkstyle.version}-all.jar +checkstyle.loc=${base-gh.loc}/checkstyle/checkstyle/releases/download/checkstyle-${checkstyle.version}/checkstyle-${checkstyle.version}-all.jar + +# ----- JaCoCo code coverage tool ----- +jacoco.version=0.8.11 +jacoco.checksum.enabled=true +jacoco.checksum.algorithm=MD5|SHA-1 +jacoco.checksum.value=2e4992dc1d63a86cdcb5084f9a5b8ebc|027b1d840385543736a3a2c3652fa67ba39025d2 +jacoco.home=${base.path}/jacoco-${jacoco.version} +jacoco.jar=${jacoco.home}/lib/jacocoant.jar +jacoco.loc=${base-maven.loc}/org/jacoco/jacoco/${jacoco.version}/jacoco-${jacoco.version}.zip + +# ----- SpotBugs (originally FindBugs) ----- +spotbugs.version=4.8.3 +spotbugs.checksum.enabled=true +spotbugs.checksum.algorithm=SHA-512 +spotbugs.checksum.value=cf12a31f67d07e4da7d0e8ec7b3e9abcc891aae62af5e95a91c27f1f29470042d9b5d57e2cb2ec2aa07349313661e41b51de8442b17ec972b1f066bb36e77603 +spotbugs.home=${base.path}/spotbugs-${spotbugs.version} +spotbugs.jar=${spotbugs.home}/lib/spotbugs-ant.jar +spotbugs.loc=${base-maven.loc}/com/github/spotbugs/spotbugs/${spotbugs.version}/spotbugs-${spotbugs.version}.tgz + +# ----- bnd, version 6.3.0 or later ----- +# ----- provides OSGI metadata for JARs ----- +bnd.version=7.0.0 +bnd.checksum.enabled=true +bnd.checksum.algorithm=MD5|SHA-1 +bnd.checksum.value=654776477ed942fc53f581fec66e253a|9937f6b7528628964a4ab8e50ba6b964d0310bce + +bnd.home=${base.path}/bnd-${bnd.version} +bnd.jar=${bnd.home}/biz.aQute.bnd-${bnd.version}.jar +bnd.loc=${base-maven.loc}/biz/aQute/bnd/biz.aQute.bnd/${bnd.version}/biz.aQute.bnd-${bnd.version}.jar + +# ----- Tomcat Migration Tool for Jakarta EE ----- +migration-lib.version=1.0.8 +migration-lib.checksum.enabled=true +migration-lib.checksum.algorithm=MD5|SHA-1 +migration-lib.checksum.value=bc5265465d7c641bbd5c9f2b057decc1|56eb518000183b5f3eface92fb9e9ccd1cbaee09 + +migration-lib.home=${base.path}/migration-${migration-lib.version} +migration-lib.jar=${migration-lib.home}/jakartaee-migration-${migration-lib.version}-shaded.jar +migration-lib.loc=${base-maven.loc}/org/apache/tomcat/jakartaee-migration/${migration-lib.version}/jakartaee-migration-${migration-lib.version}-shaded.jar + +# ----- JSign, version 4.1 or later ----- +jsign.version=6.0 +jsign.checksum.enabled=true +jsign.checksum.algorithm=MD5|SHA-1 +jsign.checksum.value=c14fe256b5bc42dc6934d3ce7b659cdf|d2f1a60711c3b51123f84cd9e04dd9d482d95f5e + +jsign.home=${base.path}/jsign-${jsign.version} +jsign.jar=${jsign.home}/jsign-${jsign.version}.jar +jsign.loc=${base-maven.loc}/net/jsign/jsign/${jsign.version}/jsign-${jsign.version}.jar + +# ----- Derby, used by unit tests ----- +derby.version=10.17.1.0 +derby.checksum.enabled=true +derby.checksum.algorithm=MD5|SHA-1 +derby.checksum.value=0665c8f3365fca01eb639e41f7685991|e90e61e8ee731614a9bafd3d81155e09fff5e80c +derby-shared.checksum.enabled=true +derby-shared.checksum.algorithm=MD5|SHA-1 +derby-shared.checksum.value=ce2d7164d5cda8ac3a1ede81023814d4|e6eac60d1b80b3781dff97ccef88fa131043f2a5 +derby-tools.checksum.enabled=true +derby-tools.checksum.algorithm=MD5|SHA-1 +derby-tools.checksum.value=ea7b7cba09a4056219e888bcdc1a3bb7|6d1a4e5e0f5c26516abbba85ece081506b9ad2e1 + +derby.home=${base.path}/derby-${derby.version} +derby.jar=${derby.home}/derby-${derby.version}.jar +derby.loc=${base-maven.loc}/org/apache/derby/derby/${derby.version}/derby-${derby.version}.jar +derby-shared.jar=${derby.home}/derby-shared-${derby.version}.jar +derby-shared.loc=${base-maven.loc}/org/apache/derby/derbyshared/${derby.version}/derbyshared-${derby.version}.jar +derby-tools.jar=${derby.home}/derby-tools-${derby.version}.jar +derby-tools.loc=${base-maven.loc}/org/apache/derby/derbytools/${derby.version}/derbytools-${derby.version}.jar diff --git a/build.properties.release b/build.properties.release new file mode 100644 index 0000000..80d2fff --- /dev/null +++ b/build.properties.release @@ -0,0 +1,54 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + +# This file was auto-generated by the pre-release Ant target. + +# Any unwanted settings may be over-ridden in a build.properties file located +# in the same directory as this file. + +# Set the version-dev to "" (empty string) as this is not a development release. +version.dev= + +# Ensure consistent timestamps for reproducible builds. +ant.tstamp.now.iso=2024-04-16T12:17:18Z + +# Enable insertion of detached signatures into the Windows installer. +do.codesigning=true + +# Re-use the same GPG executable. +gpg.exec=/opt/homebrew/bin/gpg + +# Reproducible builds require the use of the build tools defined below. The +# vendors (where appropriate) and versions must match exactly for a reproducible +# build since this data is embedded in various files, particularly JAR file +# manifests, as part of the build process. +# +# Apache Ant: Apache Ant(TM) version 1.10.14 compiled on August 16 2023 +# +# Java Name: OpenJDK 64-Bit Server VM +# Java Vendor: Eclipse Adoptium +# Java Version: 22+36 + +# The following is provided for information only. Builds will be repeatable +# whether or not the build environment is consistent with this information. +# +# OS: aarch64 Mac OS X 14.4.1 +# File encoding: UTF-8 +# +# Release Manager: schultz +release-java-version=22+36 +release-ant-version=1.10.14 diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..7c4cbfa --- /dev/null +++ b/build.xml @@ -0,0 +1,4378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Generating Reflection Less Introspection to: ${xreflect.directory} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Testsuites with skipped tests:${line.separator}
+ ${toString:test.result.skippedtests} + + + + + +
+ +
Testsuites with failed tests:${line.separator}
+ ${toString:test.result.failedtests} + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Apache Tomcat ${version} native binaries for Win64 AMD64/EMT64 platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JAVA VERSION 22 OR NEWER IS REQUIRED FOR RELEASE + + + + + + + + + + + + + + + + + + + # ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + +# This file was auto-generated by the pre-release Ant target. + +# Any unwanted settings may be over-ridden in a build.properties file located +# in the same directory as this file. + +# Set the version-dev to "" (empty string) as this is not a development release. +version.dev= + +# Ensure consistent timestamps for reproducible builds. +ant.tstamp.now.iso=${tstamp.iso.release} + +# Enable insertion of detached signatures into the Windows installer. +do.codesigning=true + +# Re-use the same GPG executable. +gpg.exec=${gpg.exec} + +# Reproducible builds require the use of the build tools defined below. The +# vendors (where appropriate) and versions must match exactly for a reproducible +# build since this data is embedded in various files, particularly JAR file +# manifests, as part of the build process. +# +# Apache Ant: ${ant.version} +# +# Java Name: ${java.vm.name} +# Java Vendor: ${java.vm.vendor} +# Java Version: ${java.vm.version} + +# The following is provided for information only. Builds will be repeatable +# whether or not the build environment is consistent with this information. +# +# OS: ${os.arch} ${os.name} ${os.version} +# File encoding: ${file.encoding} +# +# Release Manager: ${release.asfusername} +release-java-version=${java.vm.version} +release-ant-version=${antversion} + + # ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + +# This file was auto-generated by the pre-release Ant target. + +# Remove "-dev" from the version since this is not a development release. +maven.asf.release.deploy.version=${version.major}.${version.minor}.${version.build}${version.suffix} + +# Re-use the same GPG executable. +gpg.exec=${gpg.exec} + +# Set the user name to use to upload the artefacts to Nexus. +asf.ldap.username=${release.asfusername} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +gpg.exec.available=${gpg.exec.available} +gpg.exec=${gpg.exec} + Enter GPG passphrase + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Checksum check failure for ${name} (@{file}). + Algorithm: @{algorithm} + Expected value: @{value} + Actual values: + SHA-512: ${value.sha512} + SHA-384: ${value.sha384} + SHA-256: ${value.sha256} + SHA-1: ${value.sha1} + MD5: ${value.md5} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Eclipse project files created. +Read the Building page on the Apache Tomcat documentation site for details on how to configure your Eclipse workspace. + + + + + + + + + + + + + + + IntelliJ IDEA project directory created. +The SDK was set to "${build.java.version}" so make sure that your IDE has an SDK with that name, +or update the Project Settings accordingly. + + + + + + + + + + NetBeans project files already exist and have been protected! +Use the "ide-netbeans-replace" target if you wish to overwrite them. + + + + + + + + + + + + + + + + + + + + + + + NetBeans project files created. +Read the Building page on the Apache Tomcat documentation site for details on how to customise your NetBeans project. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${contents} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Unable to locate release hash for @{basefile} + + + + + + + + + + + + + + + Signature MATCH for @{src-or-bin}/@{basefile} + +Signature mismatch for @{src-or-bin}/@{basefile}: + + + + + + + + + + + +This does not appear to be a copy of a released tag; no build.properties.release file exists. + + + + +It appears there are no build artifacts to verify. Please run 'ant release' first. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +One or more signatures failed. + +Considering using a tool such as "diffoscope"[1] to inspect the differences +between the official release and your local-build. There may be trivial +differences that should not be considered troublesome. + +For example: +$ wget https://dist.apache.org/repos/dist/release/tomcat/tomcat-${version.major}/v${version}/src/apache-tomcat-${version}-src.tar.gz + or, for a release candidate: +$ wget https://dist.apache.org/repos/dist/dev/tomcat/tomcat-${version.major}/v${version}/src/apache-tomcat-${version}-src.tar.gz + then: +$ diffoscope apache-tomcat-${version}-src.tar.gz output/release/v${version}/src/apache-tomcat-${version}-src.tar.gz + + +[1] https://diffoscope.org/ + + + + SSSS SSSS + S SS SS S + SS S S SS + SS S S SS + SS SS S SS + SS S S SS + SSS SS SS SSS + SS SS SS SS + SS SS SS SS + SS S S SS + SS SSSSSSS SSSSSSS SS + SSSSS SSS SSS SSSS + SSS SSSS SS SS SSSS SS + SS SSS SSS S S SSS SSS S + S SSS SS SS SSS S + S SSSSSS SS SS SSSSSS S + S SS SSSSSSS SSSSSSS SS S + S SSSS SS SS SSSS S + SS SS SSSSSSSSS SSSSSSSSS SS S + SSSS SSSS S S SSSS SS + SSSSS SSSSSS SS SS SSSSSS SSSS + SS SS SSSSSS SSSSSS SS SS + SSS SSS S S SSS SSS + SSSSSSSSSS SSSSSSSSS + + + +All signatures are verified; the build appears to be 100% reproducible. + + +All (important) signatures are verified; the build appears to be reproducible. + +The fulldocs bundle appears to be different than that of the release. +It is likely due to this JDK bug: + + https://bugs.openjdk.org/browse/JDK-8306980 + + + + + + + + + + + + + + + + + + + + + + + +Release toolchain versions do not match local toolchain: + +Release Java: ${release-java-version} +Local Java: ${java.vm.version} +Release Ant: ${release-ant-version} +Local Ant: ${antversion} + +You may not be able to verify that this build is reproducible. + +Re-run with -Dcheck-release-toolchain=false to disable this check. + + Local toolchain versions match release toolchain (Java ${release-java-version}, Ant ${release-ant-version}). + + +
diff --git a/conf/catalina.policy b/conf/catalina.policy new file mode 100644 index 0000000..6a82bcb --- /dev/null +++ b/conf/catalina.policy @@ -0,0 +1,263 @@ +// 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. + +// ============================================================================ +// catalina.policy - Security Policy Permissions for Tomcat +// +// This file contains a default set of security policies to be enforced (by the +// JVM) when Catalina is executed with the "-security" option. In addition +// to the permissions granted here, the following additional permissions are +// granted to each web application: +// +// * Read access to the web application's document root directory +// * Read, write and delete access to the web application's working directory +// ============================================================================ + + +// ========== SYSTEM CODE PERMISSIONS ========================================= + + +// These permissions apply to javac +grant codeBase "file:${java.home}/lib/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to all shared system extensions +grant codeBase "file:${java.home}/jre/lib/ext/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to javac when ${java.home} points at $JAVA_HOME/jre +grant codeBase "file:${java.home}/../lib/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to all shared system extensions when +// ${java.home} points at $JAVA_HOME/jre +grant codeBase "file:${java.home}/lib/ext/-" { + permission java.security.AllPermission; +}; + +// This permission is required when using javac to compile JSPs +grant codeBase "jrt:/jdk.compiler" { + permission java.security.AllPermission; +}; + + +// ========== CATALINA CODE PERMISSIONS ======================================= + +// These permissions apply to the daemon code +grant codeBase "file:${catalina.home}/bin/commons-daemon.jar" { + permission java.security.AllPermission; +}; + +// These permissions apply to the logging API +// Note: If tomcat-juli.jar is in ${catalina.base} and not in ${catalina.home}, +// update this section accordingly. +// grant codeBase "file:${catalina.base}/bin/tomcat-juli.jar" {..} +grant codeBase "file:${catalina.home}/bin/tomcat-juli.jar" { + permission java.io.FilePermission + "${java.home}${file.separator}lib${file.separator}logging.properties", "read"; + + permission java.io.FilePermission + "${catalina.base}${file.separator}conf${file.separator}logging.properties", "read"; + permission java.io.FilePermission + "${catalina.base}${file.separator}logs", "read, write"; + permission java.io.FilePermission + "${catalina.base}${file.separator}logs${file.separator}*", "read, write, delete"; + + permission java.lang.RuntimePermission "shutdownHooks"; + permission java.lang.RuntimePermission "getClassLoader"; + permission java.lang.RuntimePermission "setContextClassLoader"; + + permission java.lang.management.ManagementPermission "monitor"; + + permission java.util.logging.LoggingPermission "control"; + + permission java.util.PropertyPermission "java.util.logging.config.class", "read"; + permission java.util.PropertyPermission "java.util.logging.config.file", "read"; + permission java.util.PropertyPermission "org.apache.juli.AsyncMaxRecordCount", "read"; + permission java.util.PropertyPermission "org.apache.juli.AsyncOverflowDropType", "read"; + permission java.util.PropertyPermission "org.apache.juli.ClassLoaderLogManager.debug", "read"; + permission java.util.PropertyPermission "catalina.base", "read"; + + // Note: To enable per context logging configuration, permit read access to + // the appropriate file. Be sure that the logging configuration is + // secure before enabling such access. + // E.g. for the examples web application (uncomment and unwrap + // the following to be on a single line): + // permission java.io.FilePermission "${catalina.base}${file.separator} + // webapps${file.separator}examples${file.separator}WEB-INF + // ${file.separator}classes${file.separator}logging.properties", "read"; +}; + +// These permissions apply to the server startup code +grant codeBase "file:${catalina.home}/bin/bootstrap.jar" { + permission java.security.AllPermission; +}; + +// These permissions apply to the servlet API classes +// and those that are shared across all class loaders +// located in the "lib" directory +grant codeBase "file:${catalina.home}/lib/-" { + permission java.security.AllPermission; +}; + + +// If using a per instance lib directory, i.e. ${catalina.base}/lib, +// then the following permission will need to be uncommented +// grant codeBase "file:${catalina.base}/lib/-" { +// permission java.security.AllPermission; +// }; + + +// ========== WEB APPLICATION PERMISSIONS ===================================== + + +// These permissions are granted by default to all web applications +// In addition, a web application will be given a read FilePermission +// for all files and directories in its document root. +grant { + // Required for JNDI lookup of named JDBC DataSource's and + // javamail named MimePart DataSource used to send mail + permission java.util.PropertyPermission "java.home", "read"; + permission java.util.PropertyPermission "java.naming.*", "read"; + permission java.util.PropertyPermission "javax.sql.*", "read"; + + // OS Specific properties to allow read access + permission java.util.PropertyPermission "os.name", "read"; + permission java.util.PropertyPermission "os.version", "read"; + permission java.util.PropertyPermission "os.arch", "read"; + permission java.util.PropertyPermission "file.separator", "read"; + permission java.util.PropertyPermission "path.separator", "read"; + permission java.util.PropertyPermission "line.separator", "read"; + + // JVM properties to allow read access + permission java.util.PropertyPermission "java.version", "read"; + permission java.util.PropertyPermission "java.vendor", "read"; + permission java.util.PropertyPermission "java.vendor.url", "read"; + permission java.util.PropertyPermission "java.class.version", "read"; + permission java.util.PropertyPermission "java.specification.version", "read"; + permission java.util.PropertyPermission "java.specification.vendor", "read"; + permission java.util.PropertyPermission "java.specification.name", "read"; + + permission java.util.PropertyPermission "java.vm.specification.version", "read"; + permission java.util.PropertyPermission "java.vm.specification.vendor", "read"; + permission java.util.PropertyPermission "java.vm.specification.name", "read"; + permission java.util.PropertyPermission "java.vm.version", "read"; + permission java.util.PropertyPermission "java.vm.vendor", "read"; + permission java.util.PropertyPermission "java.vm.name", "read"; + + // Required for OpenJMX + permission java.lang.RuntimePermission "getAttribute"; + + // Allow read of JAXP compliant XML parser debug + permission java.util.PropertyPermission "jaxp.debug", "read"; + + // All JSPs need to be able to read this package + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.tomcat"; + + // Precompiled JSPs need access to these packages. + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.jasper.el"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.jasper.runtime"; + permission java.lang.RuntimePermission + "accessClassInPackage.org.apache.jasper.runtime.*"; + + // Applications using WebSocket need to be able to access these packages + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.tomcat.websocket"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.tomcat.websocket.server"; +}; + + +// The Manager application needs access to the following packages to support the +// session display functionality. It also requires the custom Tomcat +// DeployXmlPermission to enable the use of META-INF/context.xml +// These settings support the following configurations: +// - default CATALINA_HOME == CATALINA_BASE +// - CATALINA_HOME != CATALINA_BASE, per instance Manager in CATALINA_BASE +// - CATALINA_HOME != CATALINA_BASE, shared Manager in CATALINA_HOME +grant codeBase "file:${catalina.base}/webapps/manager/-" { + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.ha.session"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager.util"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.util"; + permission org.apache.catalina.security.DeployXmlPermission "manager"; +}; +grant codeBase "file:${catalina.home}/webapps/manager/-" { + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.ha.session"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager.util"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.util"; + permission org.apache.catalina.security.DeployXmlPermission "manager"; +}; + +// The Host Manager application needs the custom Tomcat DeployXmlPermission to +// enable the use of META-INF/context.xml +// These settings support the following configurations: +// - default CATALINA_HOME == CATALINA_BASE +// - CATALINA_HOME != CATALINA_BASE, per instance Host Manager in CATALINA_BASE +// - CATALINA_HOME != CATALINA_BASE, shared Host Manager in CATALINA_HOME +grant codeBase "file:${catalina.base}/webapps/host-manager/-" { + permission org.apache.catalina.security.DeployXmlPermission "host-manager"; +}; +grant codeBase "file:${catalina.home}/webapps/host-manager/-" { + permission org.apache.catalina.security.DeployXmlPermission "host-manager"; +}; + + +// You can assign additional permissions to particular web applications by +// adding additional "grant" entries here, based on the code base for that +// application, /WEB-INF/classes/, or /WEB-INF/lib/ jar files. +// +// Different permissions can be granted to JSP pages, classes loaded from +// the /WEB-INF/classes/ directory, all jar files in the /WEB-INF/lib/ +// directory, or even to individual jar files in the /WEB-INF/lib/ directory. +// +// For instance, assume that the standard "examples" application +// included a JDBC driver that needed to establish a network connection to the +// corresponding database and used the scrape taglib to get the weather from +// the NOAA web server. You might create a "grant" entries like this: +// +// The permissions granted to the context root directory apply to JSP pages. +// grant codeBase "file:${catalina.base}/webapps/examples/-" { +// permission java.net.SocketPermission "dbhost.mycompany.com:5432", "connect"; +// permission java.net.SocketPermission "*.noaa.gov:80", "connect"; +// }; +// +// The permissions granted to the context WEB-INF/classes directory +// grant codeBase "file:${catalina.base}/webapps/examples/WEB-INF/classes/-" { +// }; +// +// The permission granted to your JDBC driver +// grant codeBase "jar:file:${catalina.base}/webapps/examples/WEB-INF/lib/driver.jar!/-" { +// permission java.net.SocketPermission "dbhost.mycompany.com:5432", "connect"; +// }; +// The permission granted to the scrape taglib +// grant codeBase "jar:file:${catalina.base}/webapps/examples/WEB-INF/lib/scrape.jar!/-" { +// permission java.net.SocketPermission "*.noaa.gov:80", "connect"; +// }; + +// To grant permissions for web applications using packed WAR files, use the +// Tomcat specific WAR url scheme. +// +// The permissions granted to the entire web application +// grant codeBase "war:file:${catalina.base}/webapps/examples.war*/-" { +// }; +// +// The permissions granted to a specific JAR +// grant codeBase "war:file:${catalina.base}/webapps/examples.war*/WEB-INF/lib/foo.jar" { +// }; \ No newline at end of file diff --git a/conf/catalina.properties b/conf/catalina.properties new file mode 100644 index 0000000..edf1a90 --- /dev/null +++ b/conf/catalina.properties @@ -0,0 +1,221 @@ +# 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. + +# +# List of comma-separated packages that start with or equal this string +# will cause a security exception to be thrown when +# passed to checkPackageAccess unless the +# corresponding RuntimePermission ("accessClassInPackage."+package) has +# been granted. +package.access=sun.,org.apache.catalina.,org.apache.coyote.,org.apache.jasper.,org.apache.tomcat. +# +# List of comma-separated packages that start with or equal this string +# will cause a security exception to be thrown when +# passed to checkPackageDefinition unless the +# corresponding RuntimePermission ("defineClassInPackage."+package) has +# been granted. +# +# by default, no packages are restricted for definition, and none of +# the class loaders supplied with the JDK call checkPackageDefinition. +# +package.definition=sun.,java.,org.apache.catalina.,org.apache.coyote.,\ +org.apache.jasper.,org.apache.naming.,org.apache.tomcat. + +# +# +# List of comma-separated paths defining the contents of the "common" +# classloader. Prefixes should be used to define what is the repository type. +# Path may be relative to the CATALINA_HOME or CATALINA_BASE path or absolute. +# If left as blank,the JVM system loader will be used as Catalina's "common" +# loader. +# Examples: +# "foo": Add this folder as a class repository +# "foo/*.jar": Add all the JARs of the specified folder as class +# repositories +# "foo/bar.jar": Add bar.jar as a class repository +# +# Note: Values are enclosed in double quotes ("...") in case either the +# ${catalina.base} path or the ${catalina.home} path contains a comma. +# Because double quotes are used for quoting, the double quote character +# may not appear in a path. +common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar" + +# +# List of comma-separated paths defining the contents of the "server" +# classloader. Prefixes should be used to define what is the repository type. +# Path may be relative to the CATALINA_HOME or CATALINA_BASE path or absolute. +# If left as blank, the "common" loader will be used as Catalina's "server" +# loader. +# Examples: +# "foo": Add this folder as a class repository +# "foo/*.jar": Add all the JARs of the specified folder as class +# repositories +# "foo/bar.jar": Add bar.jar as a class repository +# +# Note: Values may be enclosed in double quotes ("...") in case either the +# ${catalina.base} path or the ${catalina.home} path contains a comma. +# Because double quotes are used for quoting, the double quote character +# may not appear in a path. +server.loader= + +# +# List of comma-separated paths defining the contents of the "shared" +# classloader. Prefixes should be used to define what is the repository type. +# Path may be relative to the CATALINA_BASE path or absolute. If left as blank, +# the "common" loader will be used as Catalina's "shared" loader. +# Examples: +# "foo": Add this folder as a class repository +# "foo/*.jar": Add all the JARs of the specified folder as class +# repositories +# "foo/bar.jar": Add bar.jar as a class repository +# Please note that for single jars, e.g. bar.jar, you need the URL form +# starting with file:. +# +# Note: Values may be enclosed in double quotes ("...") in case either the +# ${catalina.base} path or the ${catalina.home} path contains a comma. +# Because double quotes are used for quoting, the double quote character +# may not appear in a path. +shared.loader= + +# Default list of JAR files that should not be scanned using the JarScanner +# functionality. This is typically used to scan JARs for configuration +# information. JARs that do not contain such information may be excluded from +# the scan to speed up the scanning process. This is the default list. JARs on +# this list are excluded from all scans. The list must be a comma separated list +# of JAR file names. +# The list of JARs to skip may be over-ridden at a Context level for individual +# scan types by configuring a JarScanner with a nested JarScanFilter. +# The JARs listed below include: +# - Tomcat Bootstrap JARs +# - Tomcat API JARs +# - Catalina JARs +# - Jasper JARs +# - Tomcat JARs +# - Common non-Tomcat JARs +# - Test JARs (JUnit, Cobertura and dependencies) +tomcat.util.scan.StandardJarScanFilter.jarsToSkip=\ +annotations-api.jar,\ +ant-junit*.jar,\ +ant-launcher*.jar,\ +ant*.jar,\ +asm-*.jar,\ +aspectj*.jar,\ +bcel*.jar,\ +biz.aQute.bnd*.jar,\ +bootstrap.jar,\ +catalina-ant.jar,\ +catalina-ha.jar,\ +catalina-ssi.jar,\ +catalina-storeconfig.jar,\ +catalina-tribes.jar,\ +catalina.jar,\ +cglib-*.jar,\ +cobertura-*.jar,\ +commons-beanutils*.jar,\ +commons-codec*.jar,\ +commons-collections*.jar,\ +commons-compress*.jar,\ +commons-daemon.jar,\ +commons-dbcp*.jar,\ +commons-digester*.jar,\ +commons-fileupload*.jar,\ +commons-httpclient*.jar,\ +commons-io*.jar,\ +commons-lang*.jar,\ +commons-logging*.jar,\ +commons-math*.jar,\ +commons-pool*.jar,\ +derby-*.jar,\ +dom4j-*.jar,\ +easymock-*.jar,\ +ecj-*.jar,\ +el-api.jar,\ +geronimo-spec-jaxrpc*.jar,\ +h2*.jar,\ +ha-api-*.jar,\ +hamcrest-*.jar,\ +hibernate*.jar,\ +httpclient*.jar,\ +icu4j-*.jar,\ +jakartaee-migration-*.jar,\ +jasper-el.jar,\ +jasper.jar,\ +jaspic-api.jar,\ +jaxb-*.jar,\ +jaxen-*.jar,\ +jaxws-rt-*.jar,\ +jdom-*.jar,\ +jetty-*.jar,\ +jmx-tools.jar,\ +jmx.jar,\ +jsp-api.jar,\ +jstl.jar,\ +jta*.jar,\ +junit-*.jar,\ +junit.jar,\ +log4j*.jar,\ +mail*.jar,\ +objenesis-*.jar,\ +oraclepki.jar,\ +org.hamcrest.core_*.jar,\ +org.junit_*.jar,\ +oro-*.jar,\ +servlet-api-*.jar,\ +servlet-api.jar,\ +slf4j*.jar,\ +taglibs-standard-spec-*.jar,\ +tagsoup-*.jar,\ +tomcat-api.jar,\ +tomcat-coyote.jar,\ +tomcat-dbcp.jar,\ +tomcat-i18n-*.jar,\ +tomcat-jdbc.jar,\ +tomcat-jni.jar,\ +tomcat-juli-adapters.jar,\ +tomcat-juli.jar,\ +tomcat-util-scan.jar,\ +tomcat-util.jar,\ +tomcat-websocket.jar,\ +tools.jar,\ +unboundid-ldapsdk-*.jar,\ +websocket-api.jar,\ +websocket-client-api.jar,\ +wsdl4j*.jar,\ +xercesImpl.jar,\ +xml-apis.jar,\ +xmlParserAPIs-*.jar,\ +xmlParserAPIs.jar,\ +xom-*.jar + +# Default list of JAR files that should be scanned that overrides the default +# jarsToSkip list above. This is typically used to include a specific JAR that +# has been excluded by a broad file name pattern in the jarsToSkip list. +# The list of JARs to scan may be over-ridden at a Context level for individual +# scan types by configuring a JarScanner with a nested JarScanFilter. +tomcat.util.scan.StandardJarScanFilter.jarsToScan=\ +log4j-taglib*.jar,\ +log4j-jakarta-web*.jar,\ +log4javascript*.jar,\ +slf4j-taglib*.jar + +# String cache configuration. +tomcat.util.buf.StringCache.byte.enabled=true +#tomcat.util.buf.StringCache.char.enabled=true +#tomcat.util.buf.StringCache.trainThreshold=500000 +#tomcat.util.buf.StringCache.cacheSize=5000 + +# Disable use of some privilege blocks Tomcat doesn't need since calls to the +# code in question are always already inside a privilege block +org.apache.el.GET_CLASSLOADER_USE_PRIVILEGED=false diff --git a/conf/context.xml b/conf/context.xml new file mode 100644 index 0000000..0a7cfac --- /dev/null +++ b/conf/context.xml @@ -0,0 +1,31 @@ + + + + + + + + WEB-INF/web.xml + WEB-INF/tomcat-web.xml + ${catalina.base}/conf/web.xml + + + + diff --git a/conf/jaspic-providers.xml b/conf/jaspic-providers.xml new file mode 100644 index 0000000..cdebf87 --- /dev/null +++ b/conf/jaspic-providers.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/conf/jaspic-providers.xsd b/conf/jaspic-providers.xsd new file mode 100644 index 0000000..1004a11 --- /dev/null +++ b/conf/jaspic-providers.xsd @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/conf/logging.properties b/conf/logging.properties new file mode 100644 index 0000000..e8b7b16 --- /dev/null +++ b/conf/logging.properties @@ -0,0 +1,79 @@ +# 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. + +handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler + +.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +1catalina.org.apache.juli.AsyncFileHandler.level = FINE +1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina. +1catalina.org.apache.juli.AsyncFileHandler.maxDays = 90 +1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8 + +2localhost.org.apache.juli.AsyncFileHandler.level = FINE +2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost. +2localhost.org.apache.juli.AsyncFileHandler.maxDays = 90 +2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8 + +3manager.org.apache.juli.AsyncFileHandler.level = FINE +3manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +3manager.org.apache.juli.AsyncFileHandler.prefix = manager. +3manager.org.apache.juli.AsyncFileHandler.maxDays = 90 +3manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8 + +4host-manager.org.apache.juli.AsyncFileHandler.level = FINE +4host-manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +4host-manager.org.apache.juli.AsyncFileHandler.prefix = host-manager. +4host-manager.org.apache.juli.AsyncFileHandler.maxDays = 90 +4host-manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8 + +java.util.logging.ConsoleHandler.level = FINE +java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter +java.util.logging.ConsoleHandler.encoding = UTF-8 + + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.AsyncFileHandler + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.AsyncFileHandler + +# For example, set the org.apache.catalina.util.LifecycleBase logger to log +# each component that extends LifecycleBase changing state: +#org.apache.catalina.util.LifecycleBase.level = FINE + +# To see debug messages in TldLocationsCache, uncomment the following line: +#org.apache.jasper.compiler.TldLocationsCache.level = FINE + +# To see debug messages for HTTP/2 handling, uncomment the following line: +#org.apache.coyote.http2.level = FINE + +# To see debug messages for WebSocket handling, uncomment the following line: +#org.apache.tomcat.websocket.level = FINE diff --git a/conf/server.xml b/conf/server.xml new file mode 100644 index 0000000..2beafa4 --- /dev/null +++ b/conf/server.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/tomcat-users.xml b/conf/tomcat-users.xml new file mode 100644 index 0000000..86b2a4a --- /dev/null +++ b/conf/tomcat-users.xml @@ -0,0 +1,56 @@ + + + + + + + + diff --git a/conf/tomcat-users.xsd b/conf/tomcat-users.xsd new file mode 100644 index 0000000..6a3446c --- /dev/null +++ b/conf/tomcat-users.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/web.xml b/conf/web.xml new file mode 100644 index 0000000..085d84e --- /dev/null +++ b/conf/web.xml @@ -0,0 +1,4752 @@ + + + + + + + + + + + + + + + + + UTF-8 + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + org.apache.catalina.servlets.DefaultServlet + + debug + 0 + + + listings + false + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jsp + org.apache.jasper.servlet.JspServlet + + fork + false + + + xpoweredBy + false + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + / + + + + + jsp + *.jsp + *.jspx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30 + + + + + + + + + + + + + 123 + application/vnd.lotus-1-2-3 + + + 3dml + text/vnd.in3d.3dml + + + 3ds + image/x-3ds + + + 3g2 + video/3gpp2 + + + 3gp + video/3gpp + + + 7z + application/x-7z-compressed + + + aab + application/x-authorware-bin + + + aac + audio/x-aac + + + aam + application/x-authorware-map + + + aas + application/x-authorware-seg + + + abs + audio/x-mpeg + + + abw + application/x-abiword + + + ac + application/pkix-attr-cert + + + acc + application/vnd.americandynamics.acc + + + ace + application/x-ace-compressed + + + acu + application/vnd.acucobol + + + acutc + application/vnd.acucorp + + + adp + audio/adpcm + + + aep + application/vnd.audiograph + + + afm + application/x-font-type1 + + + afp + application/vnd.ibm.modcap + + + ahead + application/vnd.ahead.space + + + ai + application/postscript + + + aif + audio/x-aiff + + + aifc + audio/x-aiff + + + aiff + audio/x-aiff + + + aim + application/x-aim + + + air + application/vnd.adobe.air-application-installer-package+zip + + + ait + application/vnd.dvb.ait + + + ami + application/vnd.amiga.ami + + + anx + application/annodex + + + apk + application/vnd.android.package-archive + + + appcache + text/cache-manifest + + + application + application/x-ms-application + + + apr + application/vnd.lotus-approach + + + arc + application/x-freearc + + + art + image/x-jg + + + asc + application/pgp-signature + + + asf + video/x-ms-asf + + + asm + text/x-asm + + + aso + application/vnd.accpac.simply.aso + + + asx + video/x-ms-asf + + + atc + application/vnd.acucorp + + + atom + application/atom+xml + + + atomcat + application/atomcat+xml + + + atomsvc + application/atomsvc+xml + + + atx + application/vnd.antix.game-component + + + au + audio/basic + + + avi + video/x-msvideo + + + avx + video/x-rad-screenplay + + + aw + application/applixware + + + axa + audio/annodex + + + axv + video/annodex + + + azf + application/vnd.airzip.filesecure.azf + + + azs + application/vnd.airzip.filesecure.azs + + + azw + application/vnd.amazon.ebook + + + bat + application/x-msdownload + + + bcpio + application/x-bcpio + + + bdf + application/x-font-bdf + + + bdm + application/vnd.syncml.dm+wbxml + + + bed + application/vnd.realvnc.bed + + + bh2 + application/vnd.fujitsu.oasysprs + + + bin + application/octet-stream + + + blb + application/x-blorb + + + blorb + application/x-blorb + + + bmi + application/vnd.bmi + + + bmp + image/bmp + + + body + text/html + + + book + application/vnd.framemaker + + + box + application/vnd.previewsystems.box + + + boz + application/x-bzip2 + + + bpk + application/octet-stream + + + btif + image/prs.btif + + + bz + application/x-bzip + + + bz2 + application/x-bzip2 + + + c + text/x-c + + + c11amc + application/vnd.cluetrust.cartomobile-config + + + c11amz + application/vnd.cluetrust.cartomobile-config-pkg + + + c4d + application/vnd.clonk.c4group + + + c4f + application/vnd.clonk.c4group + + + c4g + application/vnd.clonk.c4group + + + c4p + application/vnd.clonk.c4group + + + c4u + application/vnd.clonk.c4group + + + cab + application/vnd.ms-cab-compressed + + + caf + audio/x-caf + + + cap + application/vnd.tcpdump.pcap + + + car + application/vnd.curl.car + + + cat + application/vnd.ms-pki.seccat + + + cb7 + application/x-cbr + + + cba + application/x-cbr + + + cbr + application/x-cbr + + + cbt + application/x-cbr + + + cbz + application/x-cbr + + + cc + text/x-c + + + cct + application/x-director + + + ccxml + application/ccxml+xml + + + cdbcmsg + application/vnd.contact.cmsg + + + cdf + application/x-cdf + + + cdkey + application/vnd.mediastation.cdkey + + + cdmia + application/cdmi-capability + + + cdmic + application/cdmi-container + + + cdmid + application/cdmi-domain + + + cdmio + application/cdmi-object + + + cdmiq + application/cdmi-queue + + + cdx + chemical/x-cdx + + + cdxml + application/vnd.chemdraw+xml + + + cdy + application/vnd.cinderella + + + cer + application/pkix-cert + + + cfs + application/x-cfs-compressed + + + cgm + image/cgm + + + chat + application/x-chat + + + chm + application/vnd.ms-htmlhelp + + + chrt + application/vnd.kde.kchart + + + cif + chemical/x-cif + + + cii + application/vnd.anser-web-certificate-issue-initiation + + + cil + application/vnd.ms-artgalry + + + cla + application/vnd.claymore + + + class + application/java + + + clkk + application/vnd.crick.clicker.keyboard + + + clkp + application/vnd.crick.clicker.palette + + + clkt + application/vnd.crick.clicker.template + + + clkw + application/vnd.crick.clicker.wordbank + + + clkx + application/vnd.crick.clicker + + + clp + application/x-msclip + + + cmc + application/vnd.cosmocaller + + + cmdf + chemical/x-cmdf + + + cml + chemical/x-cml + + + cmp + application/vnd.yellowriver-custom-menu + + + cmx + image/x-cmx + + + cod + application/vnd.rim.cod + + + com + application/x-msdownload + + + conf + text/plain + + + cpio + application/x-cpio + + + cpp + text/x-c + + + cpt + application/mac-compactpro + + + crd + application/x-mscardfile + + + crl + application/pkix-crl + + + crt + application/x-x509-ca-cert + + + cryptonote + application/vnd.rig.cryptonote + + + csh + application/x-csh + + + csml + chemical/x-csml + + + csp + application/vnd.commonspace + + + css + text/css + + + cst + application/x-director + + + csv + text/csv + + + cu + application/cu-seeme + + + curl + text/vnd.curl + + + cww + application/prs.cww + + + cxt + application/x-director + + + cxx + text/x-c + + + dae + model/vnd.collada+xml + + + daf + application/vnd.mobius.daf + + + dart + application/vnd.dart + + + dataless + application/vnd.fdsn.seed + + + davmount + application/davmount+xml + + + dbk + application/docbook+xml + + + dcr + application/x-director + + + dcurl + text/vnd.curl.dcurl + + + dd2 + application/vnd.oma.dd2+xml + + + ddd + application/vnd.fujixerox.ddd + + + deb + application/x-debian-package + + + def + text/plain + + + deploy + application/octet-stream + + + der + application/x-x509-ca-cert + + + dfac + application/vnd.dreamfactory + + + dgc + application/x-dgc-compressed + + + dib + image/bmp + + + dic + text/x-c + + + dir + application/x-director + + + dis + application/vnd.mobius.dis + + + dist + application/octet-stream + + + distz + application/octet-stream + + + djv + image/vnd.djvu + + + djvu + image/vnd.djvu + + + dll + application/x-msdownload + + + dmg + application/x-apple-diskimage + + + dmp + application/vnd.tcpdump.pcap + + + dms + application/octet-stream + + + dna + application/vnd.dna + + + doc + application/msword + + + docm + application/vnd.ms-word.document.macroenabled.12 + + + docx + application/vnd.openxmlformats-officedocument.wordprocessingml.document + + + dot + application/msword + + + dotm + application/vnd.ms-word.template.macroenabled.12 + + + dotx + application/vnd.openxmlformats-officedocument.wordprocessingml.template + + + dp + application/vnd.osgi.dp + + + dpg + application/vnd.dpgraph + + + dra + audio/vnd.dra + + + dsc + text/prs.lines.tag + + + dssc + application/dssc+der + + + dtb + application/x-dtbook+xml + + + dtd + application/xml-dtd + + + dts + audio/vnd.dts + + + dtshd + audio/vnd.dts.hd + + + dump + application/octet-stream + + + dv + video/x-dv + + + dvb + video/vnd.dvb.file + + + dvi + application/x-dvi + + + dwf + model/vnd.dwf + + + dwg + image/vnd.dwg + + + dxf + image/vnd.dxf + + + dxp + application/vnd.spotfire.dxp + + + dxr + application/x-director + + + ecelp4800 + audio/vnd.nuera.ecelp4800 + + + ecelp7470 + audio/vnd.nuera.ecelp7470 + + + ecelp9600 + audio/vnd.nuera.ecelp9600 + + + ecma + application/ecmascript + + + edm + application/vnd.novadigm.edm + + + edx + application/vnd.novadigm.edx + + + efif + application/vnd.picsel + + + ei6 + application/vnd.pg.osasli + + + elc + application/octet-stream + + + emf + application/x-msmetafile + + + eml + message/rfc822 + + + emma + application/emma+xml + + + emz + application/x-msmetafile + + + eol + audio/vnd.digital-winds + + + eot + application/vnd.ms-fontobject + + + eps + application/postscript + + + epub + application/epub+zip + + + es3 + application/vnd.eszigno3+xml + + + esa + application/vnd.osgi.subsystem + + + esf + application/vnd.epson.esf + + + et3 + application/vnd.eszigno3+xml + + + etx + text/x-setext + + + eva + application/x-eva + + + evy + application/x-envoy + + + exe + application/octet-stream + + + exi + application/exi + + + ext + application/vnd.novadigm.ext + + + ez + application/andrew-inset + + + ez2 + application/vnd.ezpix-album + + + ez3 + application/vnd.ezpix-package + + + f + text/x-fortran + + + f4v + video/x-f4v + + + f77 + text/x-fortran + + + f90 + text/x-fortran + + + fbs + image/vnd.fastbidsheet + + + fcdt + application/vnd.adobe.formscentral.fcdt + + + fcs + application/vnd.isac.fcs + + + fdf + application/vnd.fdf + + + fe_launch + application/vnd.denovo.fcselayout-link + + + fg5 + application/vnd.fujitsu.oasysgp + + + fgd + application/x-director + + + fh + image/x-freehand + + + fh4 + image/x-freehand + + + fh5 + image/x-freehand + + + fh7 + image/x-freehand + + + fhc + image/x-freehand + + + fig + application/x-xfig + + + flac + audio/flac + + + fli + video/x-fli + + + flo + application/vnd.micrografx.flo + + + flv + video/x-flv + + + flw + application/vnd.kde.kivio + + + flx + text/vnd.fmi.flexstor + + + fly + text/vnd.fly + + + fm + application/vnd.framemaker + + + fnc + application/vnd.frogans.fnc + + + for + text/x-fortran + + + fpx + image/vnd.fpx + + + frame + application/vnd.framemaker + + + fsc + application/vnd.fsc.weblaunch + + + fst + image/vnd.fst + + + ftc + application/vnd.fluxtime.clip + + + fti + application/vnd.anser-web-funds-transfer-initiation + + + fvt + video/vnd.fvt + + + fxp + application/vnd.adobe.fxp + + + fxpl + application/vnd.adobe.fxp + + + fzs + application/vnd.fuzzysheet + + + g2w + application/vnd.geoplan + + + g3 + image/g3fax + + + g3w + application/vnd.geospace + + + gac + application/vnd.groove-account + + + gam + application/x-tads + + + gbr + application/rpki-ghostbusters + + + gca + application/x-gca-compressed + + + gdl + model/vnd.gdl + + + geo + application/vnd.dynageo + + + gex + application/vnd.geometry-explorer + + + ggb + application/vnd.geogebra.file + + + ggs + application/vnd.geogebra.slides + + + ggt + application/vnd.geogebra.tool + + + ghf + application/vnd.groove-help + + + gif + image/gif + + + gim + application/vnd.groove-identity-message + + + gml + application/gml+xml + + + gmx + application/vnd.gmx + + + gnumeric + application/x-gnumeric + + + gph + application/vnd.flographit + + + gpx + application/gpx+xml + + + gqf + application/vnd.grafeq + + + gqs + application/vnd.grafeq + + + gram + application/srgs + + + gramps + application/x-gramps-xml + + + gre + application/vnd.geometry-explorer + + + grv + application/vnd.groove-injector + + + grxml + application/srgs+xml + + + gsf + application/x-font-ghostscript + + + gtar + application/x-gtar + + + gtm + application/vnd.groove-tool-message + + + gtw + model/vnd.gtw + + + gv + text/vnd.graphviz + + + gxf + application/gxf + + + gxt + application/vnd.geonext + + + gz + application/x-gzip + + + h + text/x-c + + + h261 + video/h261 + + + h263 + video/h263 + + + h264 + video/h264 + + + hal + application/vnd.hal+xml + + + hbci + application/vnd.hbci + + + hdf + application/x-hdf + + + hh + text/x-c + + + hlp + application/winhlp + + + hpgl + application/vnd.hp-hpgl + + + hpid + application/vnd.hp-hpid + + + hps + application/vnd.hp-hps + + + hqx + application/mac-binhex40 + + + htc + text/x-component + + + htke + application/vnd.kenameaapp + + + htm + text/html + + + html + text/html + + + hvd + application/vnd.yamaha.hv-dic + + + hvp + application/vnd.yamaha.hv-voice + + + hvs + application/vnd.yamaha.hv-script + + + i2g + application/vnd.intergeo + + + icc + application/vnd.iccprofile + + + ice + x-conference/x-cooltalk + + + icm + application/vnd.iccprofile + + + ico + image/x-icon + + + ics + text/calendar + + + ief + image/ief + + + ifb + text/calendar + + + ifm + application/vnd.shana.informed.formdata + + + iges + model/iges + + + igl + application/vnd.igloader + + + igm + application/vnd.insors.igm + + + igs + model/iges + + + igx + application/vnd.micrografx.igx + + + iif + application/vnd.shana.informed.interchange + + + imp + application/vnd.accpac.simply.imp + + + ims + application/vnd.ms-ims + + + in + text/plain + + + ink + application/inkml+xml + + + inkml + application/inkml+xml + + + install + application/x-install-instructions + + + iota + application/vnd.astraea-software.iota + + + ipfix + application/ipfix + + + ipk + application/vnd.shana.informed.package + + + irm + application/vnd.ibm.rights-management + + + irp + application/vnd.irepository.package+xml + + + iso + application/x-iso9660-image + + + itp + application/vnd.shana.informed.formtemplate + + + ivp + application/vnd.immervision-ivp + + + ivu + application/vnd.immervision-ivu + + + jad + text/vnd.sun.j2me.app-descriptor + + + jam + application/vnd.jam + + + jar + application/java-archive + + + java + text/x-java-source + + + jisp + application/vnd.jisp + + + jlt + application/vnd.hp-jlyt + + + jnlp + application/x-java-jnlp-file + + + joda + application/vnd.joost.joda-archive + + + jpe + image/jpeg + + + jpeg + image/jpeg + + + jpg + image/jpeg + + + jpgm + video/jpm + + + jpgv + video/jpeg + + + jpm + video/jpm + + + js + text/javascript + + + jsf + text/plain + + + json + application/json + + + jsonml + application/jsonml+json + + + jspf + text/plain + + + kar + audio/midi + + + karbon + application/vnd.kde.karbon + + + kfo + application/vnd.kde.kformula + + + kia + application/vnd.kidspiration + + + kml + application/vnd.google-earth.kml+xml + + + kmz + application/vnd.google-earth.kmz + + + kne + application/vnd.kinar + + + knp + application/vnd.kinar + + + kon + application/vnd.kde.kontour + + + kpr + application/vnd.kde.kpresenter + + + kpt + application/vnd.kde.kpresenter + + + kpxx + application/vnd.ds-keypoint + + + ksp + application/vnd.kde.kspread + + + ktr + application/vnd.kahootz + + + ktx + image/ktx + + + ktz + application/vnd.kahootz + + + kwd + application/vnd.kde.kword + + + kwt + application/vnd.kde.kword + + + lasxml + application/vnd.las.las+xml + + + latex + application/x-latex + + + lbd + application/vnd.llamagraphics.life-balance.desktop + + + lbe + application/vnd.llamagraphics.life-balance.exchange+xml + + + les + application/vnd.hhe.lesson-player + + + lha + application/x-lzh-compressed + + + link66 + application/vnd.route66.link66+xml + + + list + text/plain + + + list3820 + application/vnd.ibm.modcap + + + listafp + application/vnd.ibm.modcap + + + lnk + application/x-ms-shortcut + + + log + text/plain + + + lostxml + application/lost+xml + + + lrf + application/octet-stream + + + lrm + application/vnd.ms-lrm + + + ltf + application/vnd.frogans.ltf + + + lvp + audio/vnd.lucent.voice + + + lwp + application/vnd.lotus-wordpro + + + lzh + application/x-lzh-compressed + + + m13 + application/x-msmediaview + + + m14 + application/x-msmediaview + + + m1v + video/mpeg + + + m21 + application/mp21 + + + m2a + audio/mpeg + + + m2v + video/mpeg + + + m3a + audio/mpeg + + + m3u + audio/x-mpegurl + + + m3u8 + application/vnd.apple.mpegurl + + + m4a + audio/mp4 + + + m4b + audio/mp4 + + + m4r + audio/mp4 + + + m4u + video/vnd.mpegurl + + + m4v + video/mp4 + + + ma + application/mathematica + + + mac + image/x-macpaint + + + mads + application/mads+xml + + + mag + application/vnd.ecowin.chart + + + maker + application/vnd.framemaker + + + man + text/troff + + + mar + application/octet-stream + + + mathml + application/mathml+xml + + + mb + application/mathematica + + + mbk + application/vnd.mobius.mbk + + + mbox + application/mbox + + + mc1 + application/vnd.medcalcdata + + + mcd + application/vnd.mcd + + + mcurl + text/vnd.curl.mcurl + + + mdb + application/x-msaccess + + + mdi + image/vnd.ms-modi + + + me + text/troff + + + mesh + model/mesh + + + meta4 + application/metalink4+xml + + + metalink + application/metalink+xml + + + mets + application/mets+xml + + + mfm + application/vnd.mfmp + + + mft + application/rpki-manifest + + + mgp + application/vnd.osgeo.mapguide.package + + + mgz + application/vnd.proteus.magazine + + + mid + audio/midi + + + midi + audio/midi + + + mie + application/x-mie + + + mif + application/x-mif + + + mime + message/rfc822 + + + mj2 + video/mj2 + + + mjp2 + video/mj2 + + + mjs + text/javascript + + + mk3d + video/x-matroska + + + mka + audio/x-matroska + + + mks + video/x-matroska + + + mkv + video/x-matroska + + + mlp + application/vnd.dolby.mlp + + + mmd + application/vnd.chipnuts.karaoke-mmd + + + mmf + application/vnd.smaf + + + mmr + image/vnd.fujixerox.edmics-mmr + + + mng + video/x-mng + + + mny + application/x-msmoney + + + mobi + application/x-mobipocket-ebook + + + mods + application/mods+xml + + + mov + video/quicktime + + + movie + video/x-sgi-movie + + + mp1 + audio/mpeg + + + mp2 + audio/mpeg + + + mp21 + application/mp21 + + + mp2a + audio/mpeg + + + mp3 + audio/mpeg + + + mp4 + video/mp4 + + + mp4a + audio/mp4 + + + mp4s + application/mp4 + + + mp4v + video/mp4 + + + mpa + audio/mpeg + + + mpc + application/vnd.mophun.certificate + + + mpe + video/mpeg + + + mpeg + video/mpeg + + + mpega + audio/x-mpeg + + + mpg + video/mpeg + + + mpg4 + video/mp4 + + + mpga + audio/mpeg + + + mpkg + application/vnd.apple.installer+xml + + + mpm + application/vnd.blueice.multipass + + + mpn + application/vnd.mophun.application + + + mpp + application/vnd.ms-project + + + mpt + application/vnd.ms-project + + + mpv2 + video/mpeg2 + + + mpy + application/vnd.ibm.minipay + + + mqy + application/vnd.mobius.mqy + + + mrc + application/marc + + + mrcx + application/marcxml+xml + + + ms + text/troff + + + mscml + application/mediaservercontrol+xml + + + mseed + application/vnd.fdsn.mseed + + + mseq + application/vnd.mseq + + + msf + application/vnd.epson.msf + + + msh + model/mesh + + + msi + application/x-msdownload + + + msl + application/vnd.mobius.msl + + + msty + application/vnd.muvee.style + + + mts + model/vnd.mts + + + mus + application/vnd.musician + + + musicxml + application/vnd.recordare.musicxml+xml + + + mvb + application/x-msmediaview + + + mwf + application/vnd.mfer + + + mxf + application/mxf + + + mxl + application/vnd.recordare.musicxml + + + mxml + application/xv+xml + + + mxs + application/vnd.triscape.mxs + + + mxu + video/vnd.mpegurl + + + n-gage + application/vnd.nokia.n-gage.symbian.install + + + n3 + text/n3 + + + nb + application/mathematica + + + nbp + application/vnd.wolfram.player + + + nc + application/x-netcdf + + + ncx + application/x-dtbncx+xml + + + nfo + text/x-nfo + + + ngdat + application/vnd.nokia.n-gage.data + + + nitf + application/vnd.nitf + + + nlu + application/vnd.neurolanguage.nlu + + + nml + application/vnd.enliven + + + nnd + application/vnd.noblenet-directory + + + nns + application/vnd.noblenet-sealer + + + nnw + application/vnd.noblenet-web + + + npx + image/vnd.net-fpx + + + nsc + application/x-conference + + + nsf + application/vnd.lotus-notes + + + ntf + application/vnd.nitf + + + nzb + application/x-nzb + + + oa2 + application/vnd.fujitsu.oasys2 + + + oa3 + application/vnd.fujitsu.oasys3 + + + oas + application/vnd.fujitsu.oasys + + + obd + application/x-msbinder + + + obj + application/x-tgif + + + oda + application/oda + + + + odb + application/vnd.oasis.opendocument.database + + + + odc + application/vnd.oasis.opendocument.chart + + + + odf + application/vnd.oasis.opendocument.formula + + + odft + application/vnd.oasis.opendocument.formula-template + + + + odg + application/vnd.oasis.opendocument.graphics + + + + odi + application/vnd.oasis.opendocument.image + + + + odm + application/vnd.oasis.opendocument.text-master + + + + odp + application/vnd.oasis.opendocument.presentation + + + + ods + application/vnd.oasis.opendocument.spreadsheet + + + + odt + application/vnd.oasis.opendocument.text + + + oga + audio/ogg + + + ogg + audio/ogg + + + ogv + video/ogg + + + + ogx + application/ogg + + + omdoc + application/omdoc+xml + + + onepkg + application/onenote + + + onetmp + application/onenote + + + onetoc + application/onenote + + + onetoc2 + application/onenote + + + opf + application/oebps-package+xml + + + opml + text/x-opml + + + oprc + application/vnd.palm + + + opus + audio/ogg + + + org + application/vnd.lotus-organizer + + + osf + application/vnd.yamaha.openscoreformat + + + osfpvg + application/vnd.yamaha.openscoreformat.osfpvg+xml + + + otc + application/vnd.oasis.opendocument.chart-template + + + otf + font/otf + + + + otg + application/vnd.oasis.opendocument.graphics-template + + + + oth + application/vnd.oasis.opendocument.text-web + + + oti + application/vnd.oasis.opendocument.image-template + + + + otp + application/vnd.oasis.opendocument.presentation-template + + + + ots + application/vnd.oasis.opendocument.spreadsheet-template + + + + ott + application/vnd.oasis.opendocument.text-template + + + oxps + application/oxps + + + oxt + application/vnd.openofficeorg.extension + + + p + text/x-pascal + + + p10 + application/pkcs10 + + + p12 + application/x-pkcs12 + + + p7b + application/x-pkcs7-certificates + + + p7c + application/pkcs7-mime + + + p7m + application/pkcs7-mime + + + p7r + application/x-pkcs7-certreqresp + + + p7s + application/pkcs7-signature + + + p8 + application/pkcs8 + + + pas + text/x-pascal + + + paw + application/vnd.pawaafile + + + pbd + application/vnd.powerbuilder6 + + + pbm + image/x-portable-bitmap + + + pcap + application/vnd.tcpdump.pcap + + + pcf + application/x-font-pcf + + + pcl + application/vnd.hp-pcl + + + pclxl + application/vnd.hp-pclxl + + + pct + image/pict + + + pcurl + application/vnd.curl.pcurl + + + pcx + image/x-pcx + + + pdb + application/vnd.palm + + + pdf + application/pdf + + + pfa + application/x-font-type1 + + + pfb + application/x-font-type1 + + + pfm + application/x-font-type1 + + + pfr + application/font-tdpfr + + + pfx + application/x-pkcs12 + + + pgm + image/x-portable-graymap + + + pgn + application/x-chess-pgn + + + pgp + application/pgp-encrypted + + + pic + image/pict + + + pict + image/pict + + + pkg + application/octet-stream + + + pki + application/pkixcmp + + + pkipath + application/pkix-pkipath + + + plb + application/vnd.3gpp.pic-bw-large + + + plc + application/vnd.mobius.plc + + + plf + application/vnd.pocketlearn + + + pls + audio/x-scpls + + + pml + application/vnd.ctc-posml + + + png + image/png + + + pnm + image/x-portable-anymap + + + pnt + image/x-macpaint + + + portpkg + application/vnd.macports.portpkg + + + pot + application/vnd.ms-powerpoint + + + potm + application/vnd.ms-powerpoint.template.macroenabled.12 + + + potx + application/vnd.openxmlformats-officedocument.presentationml.template + + + ppam + application/vnd.ms-powerpoint.addin.macroenabled.12 + + + ppd + application/vnd.cups-ppd + + + ppm + image/x-portable-pixmap + + + pps + application/vnd.ms-powerpoint + + + ppsm + application/vnd.ms-powerpoint.slideshow.macroenabled.12 + + + ppsx + application/vnd.openxmlformats-officedocument.presentationml.slideshow + + + ppt + application/vnd.ms-powerpoint + + + pptm + application/vnd.ms-powerpoint.presentation.macroenabled.12 + + + pptx + application/vnd.openxmlformats-officedocument.presentationml.presentation + + + pqa + application/vnd.palm + + + prc + application/x-mobipocket-ebook + + + pre + application/vnd.lotus-freelance + + + prf + application/pics-rules + + + ps + application/postscript + + + psb + application/vnd.3gpp.pic-bw-small + + + psd + image/vnd.adobe.photoshop + + + psf + application/x-font-linux-psf + + + pskcxml + application/pskc+xml + + + ptid + application/vnd.pvi.ptid1 + + + pub + application/x-mspublisher + + + pvb + application/vnd.3gpp.pic-bw-var + + + pwn + application/vnd.3m.post-it-notes + + + pya + audio/vnd.ms-playready.media.pya + + + pyv + video/vnd.ms-playready.media.pyv + + + qam + application/vnd.epson.quickanime + + + qbo + application/vnd.intu.qbo + + + qfx + application/vnd.intu.qfx + + + qps + application/vnd.publishare-delta-tree + + + qt + video/quicktime + + + qti + image/x-quicktime + + + qtif + image/x-quicktime + + + qwd + application/vnd.quark.quarkxpress + + + qwt + application/vnd.quark.quarkxpress + + + qxb + application/vnd.quark.quarkxpress + + + qxd + application/vnd.quark.quarkxpress + + + qxl + application/vnd.quark.quarkxpress + + + qxt + application/vnd.quark.quarkxpress + + + ra + audio/x-pn-realaudio + + + ram + audio/x-pn-realaudio + + + rar + application/x-rar-compressed + + + ras + image/x-cmu-raster + + + rcprofile + application/vnd.ipunplugged.rcprofile + + + rdf + application/rdf+xml + + + rdz + application/vnd.data-vision.rdz + + + rep + application/vnd.businessobjects + + + res + application/x-dtbresource+xml + + + rgb + image/x-rgb + + + rif + application/reginfo+xml + + + rip + audio/vnd.rip + + + ris + application/x-research-info-systems + + + rl + application/resource-lists+xml + + + rlc + image/vnd.fujixerox.edmics-rlc + + + rld + application/resource-lists-diff+xml + + + rm + application/vnd.rn-realmedia + + + rmi + audio/midi + + + rmp + audio/x-pn-realaudio-plugin + + + rms + application/vnd.jcp.javame.midlet-rms + + + rmvb + application/vnd.rn-realmedia-vbr + + + rnc + application/relax-ng-compact-syntax + + + roa + application/rpki-roa + + + roff + text/troff + + + rp9 + application/vnd.cloanto.rp9 + + + rpss + application/vnd.nokia.radio-presets + + + rpst + application/vnd.nokia.radio-preset + + + rq + application/sparql-query + + + rs + application/rls-services+xml + + + rsd + application/rsd+xml + + + rss + application/rss+xml + + + rtf + application/rtf + + + rtx + text/richtext + + + s + text/x-asm + + + s3m + audio/s3m + + + saf + application/vnd.yamaha.smaf-audio + + + sbml + application/sbml+xml + + + sc + application/vnd.ibm.secure-container + + + scd + application/x-msschedule + + + scm + application/vnd.lotus-screencam + + + scq + application/scvp-cv-request + + + scs + application/scvp-cv-response + + + scurl + text/vnd.curl.scurl + + + sda + application/vnd.stardivision.draw + + + sdc + application/vnd.stardivision.calc + + + sdd + application/vnd.stardivision.impress + + + sdkd + application/vnd.solent.sdkm+xml + + + sdkm + application/vnd.solent.sdkm+xml + + + sdp + application/sdp + + + sdw + application/vnd.stardivision.writer + + + see + application/vnd.seemail + + + seed + application/vnd.fdsn.seed + + + sema + application/vnd.sema + + + semd + application/vnd.semd + + + semf + application/vnd.semf + + + ser + application/java-serialized-object + + + setpay + application/set-payment-initiation + + + setreg + application/set-registration-initiation + + + sfd-hdstx + application/vnd.hydrostatix.sof-data + + + sfs + application/vnd.spotfire.sfs + + + sfv + text/x-sfv + + + sgi + image/sgi + + + sgl + application/vnd.stardivision.writer-global + + + sgm + text/sgml + + + sgml + text/sgml + + + sh + application/x-sh + + + shar + application/x-shar + + + shf + application/shf+xml + + + + sid + image/x-mrsid-image + + + sig + application/pgp-signature + + + sil + audio/silk + + + silo + model/mesh + + + sis + application/vnd.symbian.install + + + sisx + application/vnd.symbian.install + + + sit + application/x-stuffit + + + sitx + application/x-stuffitx + + + skd + application/vnd.koan + + + skm + application/vnd.koan + + + skp + application/vnd.koan + + + skt + application/vnd.koan + + + sldm + application/vnd.ms-powerpoint.slide.macroenabled.12 + + + sldx + application/vnd.openxmlformats-officedocument.presentationml.slide + + + slt + application/vnd.epson.salt + + + sm + application/vnd.stepmania.stepchart + + + smf + application/vnd.stardivision.math + + + smi + application/smil+xml + + + smil + application/smil+xml + + + smv + video/x-smv + + + smzip + application/vnd.stepmania.package + + + snd + audio/basic + + + snf + application/x-font-snf + + + so + application/octet-stream + + + spc + application/x-pkcs7-certificates + + + spf + application/vnd.yamaha.smaf-phrase + + + spl + application/x-futuresplash + + + spot + text/vnd.in3d.spot + + + spp + application/scvp-vp-response + + + spq + application/scvp-vp-request + + + spx + audio/ogg + + + sql + application/x-sql + + + src + application/x-wais-source + + + srt + application/x-subrip + + + sru + application/sru+xml + + + srx + application/sparql-results+xml + + + ssdl + application/ssdl+xml + + + sse + application/vnd.kodak-descriptor + + + ssf + application/vnd.epson.ssf + + + ssml + application/ssml+xml + + + st + application/vnd.sailingtracker.track + + + stc + application/vnd.sun.xml.calc.template + + + std + application/vnd.sun.xml.draw.template + + + stf + application/vnd.wt.stf + + + sti + application/vnd.sun.xml.impress.template + + + stk + application/hyperstudio + + + stl + application/vnd.ms-pki.stl + + + str + application/vnd.pg.format + + + stw + application/vnd.sun.xml.writer.template + + + sub + text/vnd.dvb.subtitle + + + sus + application/vnd.sus-calendar + + + susp + application/vnd.sus-calendar + + + sv4cpio + application/x-sv4cpio + + + sv4crc + application/x-sv4crc + + + svc + application/vnd.dvb.service + + + svd + application/vnd.svd + + + svg + image/svg+xml + + + svgz + image/svg+xml + + + swa + application/x-director + + + swf + application/x-shockwave-flash + + + swi + application/vnd.aristanetworks.swi + + + sxc + application/vnd.sun.xml.calc + + + sxd + application/vnd.sun.xml.draw + + + sxg + application/vnd.sun.xml.writer.global + + + sxi + application/vnd.sun.xml.impress + + + sxm + application/vnd.sun.xml.math + + + sxw + application/vnd.sun.xml.writer + + + t + text/troff + + + t3 + application/x-t3vm-image + + + taglet + application/vnd.mynfc + + + tao + application/vnd.tao.intent-module-archive + + + tar + application/x-tar + + + tcap + application/vnd.3gpp2.tcap + + + tcl + application/x-tcl + + + teacher + application/vnd.smart.teacher + + + tei + application/tei+xml + + + teicorpus + application/tei+xml + + + tex + application/x-tex + + + texi + application/x-texinfo + + + texinfo + application/x-texinfo + + + text + text/plain + + + tfi + application/thraud+xml + + + tfm + application/x-tex-tfm + + + tga + image/x-tga + + + thmx + application/vnd.ms-officetheme + + + tif + image/tiff + + + tiff + image/tiff + + + tmo + application/vnd.tmobile-livetv + + + torrent + application/x-bittorrent + + + tpl + application/vnd.groove-tool-template + + + tpt + application/vnd.trid.tpt + + + tr + text/troff + + + tra + application/vnd.trueapp + + + trm + application/x-msterminal + + + tsd + application/timestamped-data + + + tsv + text/tab-separated-values + + + ttc + font/collection + + + ttf + font/ttf + + + ttl + text/turtle + + + twd + application/vnd.simtech-mindmapper + + + twds + application/vnd.simtech-mindmapper + + + txd + application/vnd.genomatix.tuxedo + + + txf + application/vnd.mobius.txf + + + txt + text/plain + + + u32 + application/x-authorware-bin + + + udeb + application/x-debian-package + + + ufd + application/vnd.ufdl + + + ufdl + application/vnd.ufdl + + + ulw + audio/basic + + + ulx + application/x-glulx + + + umj + application/vnd.umajin + + + unityweb + application/vnd.unity + + + uoml + application/vnd.uoml+xml + + + uri + text/uri-list + + + uris + text/uri-list + + + urls + text/uri-list + + + ustar + application/x-ustar + + + utz + application/vnd.uiq.theme + + + uu + text/x-uuencode + + + uva + audio/vnd.dece.audio + + + uvd + application/vnd.dece.data + + + uvf + application/vnd.dece.data + + + uvg + image/vnd.dece.graphic + + + uvh + video/vnd.dece.hd + + + uvi + image/vnd.dece.graphic + + + uvm + video/vnd.dece.mobile + + + uvp + video/vnd.dece.pd + + + uvs + video/vnd.dece.sd + + + uvt + application/vnd.dece.ttml+xml + + + uvu + video/vnd.uvvu.mp4 + + + uvv + video/vnd.dece.video + + + uvva + audio/vnd.dece.audio + + + uvvd + application/vnd.dece.data + + + uvvf + application/vnd.dece.data + + + uvvg + image/vnd.dece.graphic + + + uvvh + video/vnd.dece.hd + + + uvvi + image/vnd.dece.graphic + + + uvvm + video/vnd.dece.mobile + + + uvvp + video/vnd.dece.pd + + + uvvs + video/vnd.dece.sd + + + uvvt + application/vnd.dece.ttml+xml + + + uvvu + video/vnd.uvvu.mp4 + + + uvvv + video/vnd.dece.video + + + uvvx + application/vnd.dece.unspecified + + + uvvz + application/vnd.dece.zip + + + uvx + application/vnd.dece.unspecified + + + uvz + application/vnd.dece.zip + + + vcard + text/vcard + + + vcd + application/x-cdlink + + + vcf + text/x-vcard + + + vcg + application/vnd.groove-vcard + + + vcs + text/x-vcalendar + + + vcx + application/vnd.vcx + + + vis + application/vnd.visionary + + + viv + video/vnd.vivo + + + vob + video/x-ms-vob + + + vor + application/vnd.stardivision.writer + + + vox + application/x-authorware-bin + + + vrml + model/vrml + + + vsd + application/vnd.visio + + + vsf + application/vnd.vsf + + + vss + application/vnd.visio + + + vst + application/vnd.visio + + + vsw + application/vnd.visio + + + vtu + model/vnd.vtu + + + vxml + application/voicexml+xml + + + w3d + application/x-director + + + wad + application/x-doom + + + wasm + application/wasm + + + wav + audio/x-wav + + + wax + audio/x-ms-wax + + + + wbmp + image/vnd.wap.wbmp + + + wbs + application/vnd.criticaltools.wbs+xml + + + wbxml + application/vnd.wap.wbxml + + + wcm + application/vnd.ms-works + + + wdb + application/vnd.ms-works + + + wdp + image/vnd.ms-photo + + + weba + audio/webm + + + webm + video/webm + + + webp + image/webp + + + wg + application/vnd.pmi.widget + + + wgt + application/widget + + + wks + application/vnd.ms-works + + + wm + video/x-ms-wm + + + wma + audio/x-ms-wma + + + wmd + application/x-ms-wmd + + + wmf + application/x-msmetafile + + + + wml + text/vnd.wap.wml + + + + wmlc + application/vnd.wap.wmlc + + + + wmls + text/vnd.wap.wmlscript + + + + wmlsc + application/vnd.wap.wmlscriptc + + + wmv + video/x-ms-wmv + + + wmx + video/x-ms-wmx + + + wmz + application/x-msmetafile + + + woff + font/woff + + + woff2 + font/woff2 + + + wpd + application/vnd.wordperfect + + + wpl + application/vnd.ms-wpl + + + wps + application/vnd.ms-works + + + wqd + application/vnd.wqd + + + wri + application/x-mswrite + + + wrl + model/vrml + + + wsdl + application/wsdl+xml + + + wspolicy + application/wspolicy+xml + + + wtb + application/vnd.webturbo + + + wvx + video/x-ms-wvx + + + x32 + application/x-authorware-bin + + + x3d + model/x3d+xml + + + x3db + model/x3d+binary + + + x3dbz + model/x3d+binary + + + x3dv + model/x3d+vrml + + + x3dvz + model/x3d+vrml + + + x3dz + model/x3d+xml + + + xaml + application/xaml+xml + + + xap + application/x-silverlight-app + + + xar + application/vnd.xara + + + xbap + application/x-ms-xbap + + + xbd + application/vnd.fujixerox.docuworks.binder + + + xbm + image/x-xbitmap + + + xdf + application/xcap-diff+xml + + + xdm + application/vnd.syncml.dm+xml + + + xdp + application/vnd.adobe.xdp+xml + + + xdssc + application/dssc+xml + + + xdw + application/vnd.fujixerox.docuworks + + + xenc + application/xenc+xml + + + xer + application/patch-ops-error+xml + + + xfdf + application/vnd.adobe.xfdf + + + xfdl + application/vnd.xfdl + + + xht + application/xhtml+xml + + + xhtml + application/xhtml+xml + + + xhvml + application/xv+xml + + + xif + image/vnd.xiff + + + xla + application/vnd.ms-excel + + + xlam + application/vnd.ms-excel.addin.macroenabled.12 + + + xlc + application/vnd.ms-excel + + + xlf + application/x-xliff+xml + + + xlm + application/vnd.ms-excel + + + xls + application/vnd.ms-excel + + + xlsb + application/vnd.ms-excel.sheet.binary.macroenabled.12 + + + xlsm + application/vnd.ms-excel.sheet.macroenabled.12 + + + xlsx + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + + + xlt + application/vnd.ms-excel + + + xltm + application/vnd.ms-excel.template.macroenabled.12 + + + xltx + application/vnd.openxmlformats-officedocument.spreadsheetml.template + + + xlw + application/vnd.ms-excel + + + xm + audio/xm + + + xml + application/xml + + + xo + application/vnd.olpc-sugar + + + xop + application/xop+xml + + + xpi + application/x-xpinstall + + + xpl + application/xproc+xml + + + xpm + image/x-xpixmap + + + xpr + application/vnd.is-xpr + + + xps + application/vnd.ms-xpsdocument + + + xpw + application/vnd.intercon.formnet + + + xpx + application/vnd.intercon.formnet + + + xsl + application/xml + + + xslt + application/xslt+xml + + + xsm + application/vnd.syncml+xml + + + xspf + application/xspf+xml + + + xul + application/vnd.mozilla.xul+xml + + + xvm + application/xv+xml + + + xvml + application/xv+xml + + + xwd + image/x-xwindowdump + + + xyz + chemical/x-xyz + + + xz + application/x-xz + + + yang + application/yang + + + yin + application/yin+xml + + + z + application/x-compress + + + z1 + application/x-zmachine + + + z2 + application/x-zmachine + + + z3 + application/x-zmachine + + + z4 + application/x-zmachine + + + z5 + application/x-zmachine + + + z6 + application/x-zmachine + + + z7 + application/x-zmachine + + + z8 + application/x-zmachine + + + zaz + application/vnd.zzazz.deck+xml + + + zip + application/zip + + + zir + application/vnd.zul + + + zirz + application/vnd.zul + + + zmm + application/vnd.handheld-entertainment+xml + + + + + + + + + + + + + + + + + + index.html + index.htm + index.jsp + + + diff --git a/debian/README.Debian b/debian/README.Debian new file mode 100644 index 0000000..007b608 --- /dev/null +++ b/debian/README.Debian @@ -0,0 +1,61 @@ +Migrating from previous Tomcat packages +--------------------------------------- + + * Refer to the upstream migration guides for application compatibility + and configuration changes: + - http://tomcat.apache.org/migration-7.html + - http://tomcat.apache.org/migration-8.html + - http://tomcat.apache.org/migration-85.html + - http://tomcat.apache.org/migration-9.html + - http://tomcat.apache.org/migration-10.html + + * Before the version 9 the Debian packages for Tomcat each created their + own tomcat users. You may need to recursively update your application + directories to be owned by the tomcat user. This user will no longer + change for the future upgrades. + + +Getting started +--------------- + + * After installing the tomcat10 package, the server should be accessible + at http://localhost:8080/ + + * The default port 8080 can be changed by modifying the port of the + connector in /etc/tomcat10/server.xml. Privileged ports (such as 80 or 443) + can be used with no extra configuration. + + * If you install tomcat10-admin, you have to define an admin account to + access the manager interface. Edit /etc/tomcat10/tomcat-users.xml and + follow the instructions in the comments. The Tomcat manager will be + accessible at http://localhost:8080/manager/html + + * Tomcat is not running under a Java security manager by default. If you + expose your Tomcat instance to the internet, please consider editing + your /etc/default/tomcat10 file and set SECURITY_MANAGER="true", then + adjust policy files in /etc/tomcat10/policy.d/ as explained in + https://tomcat.apache.org/tomcat-10.0-doc/security-manager-howto.html + + * Tomcat is sandboxed by systemd and only has write access to the following + directories: + - /var/lib/tomcat10/conf/Catalina (actually /etc/tomcat10/Catalina) + - /var/lib/tomcat10/logs (actually /var/log/tomcat10) + - /var/lib/tomcat10/webapps + - /var/lib/tomcat10/work (actually /var/cache/tomcat10) + + If write access to other directories is required the service settings + have to be overridden. This is done by creating an override.conf file + in /etc/systemd/system/tomcat10.service.d/ containing: + + [Service] + ReadWritePaths=/path/to/the/directory/ + + The service has to be restarted afterward with: + + systemctl daemon-reload + systemctl restart tomcat10 + + * To run more than one Tomcat instance on your server, install the package + tomcat10-user and run the tomcat10-instance-create utility. + You should remove the tomcat10 package if you don't want Tomcat to + start as a daemon at boot time. diff --git a/debian/ant.properties b/debian/ant.properties new file mode 100644 index 0000000..7eaa45c --- /dev/null +++ b/debian/ant.properties @@ -0,0 +1,15 @@ +compile.debug=true +execute.validate=false + +exist=true +bnd.jar=/usr/share/java/bnd.jar +bndlib.jar=/usr/share/java/bndlib.jar +jaxrpc-lib.jar=/usr/share/java/jaxrpc-api.jar +jdt.jar=/usr/share/java/eclipse-jdt-core.jar +junit.jar=/usr/share/java/junit4.jar +cglib.jar=/usr/share/java/cglib-nodep.jar +easymock.jar=/usr/share/java/easymock.jar +hamcrest.jar=/usr/share/java/hamcrest-core.jar +objenesis.jar=/usr/share/java/objenesis.jar +wsdl4j-lib.jar=/usr/share/java/wsdl4j.jar +migration-lib.jar=/usr/share/tomcat-jakartaee-migration/lib/jakartaee-migration-shaded.jar diff --git a/debian/changelog b/debian/changelog index bad88e2..272296a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,1171 @@ -template-repository (1.0-1) unstable; urgency=medium +tomcat10 (10.1.23-1) unstable; urgency=medium - * Initial release + * New upstream version 10.1.23 + * Refresh the patches. + * Declare compliance with Debian Policy 4.7.0. - -- Tsic404 Sat, 28 Jan 2023 13:46:49 +0800 + -- Markus Koschany Thu, 18 Apr 2024 22:34:59 +0200 + +tomcat10 (10.1.20-1) unstable; urgency=high + + * New upstream version 10.1.20. + - Fix CVE-2024-24549: Denial of Service due to improper input validation + vulnerability. (Closes: #1066878) + - Fix CVE-2024-23672: Denial of Service via incomplete cleanup + vulnerability. (Closes: #1066877) + * Remove obsolete dependency on lsb-base from tomcat10 binary package. + + -- Markus Koschany Sat, 06 Apr 2024 13:43:19 +0200 + +tomcat10 (10.1.16-1) unstable; urgency=medium + + * New upstream version 10.1.16. + - Fix CVE-2023-46589: potential request smuggling. (Closes: #1057082) + + -- Markus Koschany Sun, 03 Dec 2023 13:31:22 +0100 + +tomcat10 (10.1.15-1) unstable; urgency=medium + + [ Emmanuel Bourg ] + * Build depend on the versionless tomcat-jakartaee-migration jar. + (Closes: #1054705) + + [ Markus Koschany ] + * New upstream version 10.1.15. + + -- Markus Koschany Fri, 27 Oct 2023 22:40:52 +0200 + +tomcat10 (10.1.14-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Tue, 10 Oct 2023 09:44:59 +0200 + +tomcat10 (10.1.13-1) unstable; urgency=medium + + * New upstream version 10.1.13. + * Refresh the patches. + + -- Markus Koschany Fri, 01 Sep 2023 13:47:35 +0200 + +tomcat10 (10.1.10-1) unstable; urgency=medium + + * New upstream version 10.1.10. + + -- Markus Koschany Sun, 18 Jun 2023 18:16:33 +0200 + +tomcat10 (10.1.9-1) experimental; urgency=medium + + * New upstream version 10.1.9. + + -- Markus Koschany Tue, 16 May 2023 11:32:58 +0200 + +tomcat10 (10.1.8-1) experimental; urgency=medium + + * New upstream version 10.1.8. + + -- Markus Koschany Tue, 25 Apr 2023 21:54:20 +0200 + +tomcat10 (10.1.7-1) experimental; urgency=medium + + * New upstream version 10.1.7. + + -- Markus Koschany Wed, 12 Apr 2023 00:27:16 +0200 + +tomcat10 (10.1.6-1) unstable; urgency=medium + + * New upstream version 10.1.6 + * Tighten dependency on libtcnative-1. + * Refresh the patches. + + -- Markus Koschany Thu, 23 Feb 2023 12:49:47 +0100 + +tomcat10 (10.1.5-2) unstable; urgency=medium + + * Install and link to websocket-client-api.jar. Fixes ClassNotFoundException. + Thanks to Jorge Moraleda for the report. (Closes: #1030869) + + -- Markus Koschany Sun, 12 Feb 2023 19:27:47 +0100 + +tomcat10 (10.1.5-1) unstable; urgency=medium + + * New upstream version. + * Drop tomcat-jakartaee-migration.patch. Fixed upstream. + * Refresh the patches. + + -- Markus Koschany Wed, 01 Feb 2023 00:13:09 +0100 + +tomcat10 (10.0.27-1) unstable; urgency=medium + + * New upstream version. + * Release to unstable. + * Make tomcat9 and tomcat10 co-installable. (Closes: #965006) + * Tomcat 10 can be built from source again. (Closes: #1028632) + * Add myself to Uploaders. + * Declare compliance with Debian Policy 4.6.2. + + -- Markus Koschany Mon, 16 Jan 2023 22:51:22 +0100 + +tomcat10 (10.0.0~M7-1) experimental; urgency=medium + + * New upstream release + - Refreshed the patches + - Fixed the compatibility with the version of bnd in Debian + * Set the generic version of the Maven artifacts to 10.x (Fixes: #965006) + * Grant write access on /var/log/tomcat10 to the adm group + + -- Emmanuel Bourg Tue, 14 Jul 2020 20:14:44 +0200 + +tomcat10 (10.0.0~M6-1) experimental; urgency=medium + + * New upstream release + - Refreshed the patches + - Renamed the package to tomcat10 + - Migrate the taglibs-standard libraries to the Jakarta EE namespace + + -- Emmanuel Bourg Mon, 15 Jun 2020 09:51:05 +0200 + +tomcat9 (9.0.70-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Mon, 05 Dec 2022 18:50:40 +0100 + +tomcat9 (9.0.68-1) unstable; urgency=medium + + * New upstream release + * Look for OpenJDK 17 and up to 21 when starting the server (Closes: #1020948) + * Simplified the Maven rules + + -- Emmanuel Bourg Sat, 08 Oct 2022 13:53:36 +0200 + +tomcat9 (9.0.67-1) unstable; urgency=medium + + * Team upload. + + [ Thorsten Glaser ] + * Fix a Policy violation in the Depends of bin:tomcat9 + + [ Emmanuel Bourg ] + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Tue, 27 Sep 2022 00:49:00 +0200 + +tomcat9 (9.0.65-1) unstable; urgency=medium + + * Team upload. + * New upstream version 9.0.65. + + -- Markus Koschany Fri, 12 Aug 2022 12:56:06 +0200 + +tomcat9 (9.0.64-2) unstable; urgency=medium + + * Fallback to the default log formatter when systemd isn't used + * Depend on systemd-sysusers and systemd-tmpfiles instead of systemd + * Depend on libeclipse-jdt-core-java (>= 3.26.0) + + -- Emmanuel Bourg Tue, 21 Jun 2022 14:59:03 +0200 + +tomcat9 (9.0.64-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Standards-Version updated to 4.6.1 + + -- Emmanuel Bourg Mon, 20 Jun 2022 15:17:59 +0200 + +tomcat9 (9.0.63-1) unstable; urgency=medium + + * Team upload. + * New upstream version 9.0.63. + - Fix CVE-2022-29885: Improve documentation for the EncryptInterceptor and + do not claim it protects against all risks associated with running over + any untrusted network. + + -- Markus Koschany Fri, 13 May 2022 14:04:35 +0200 + +tomcat9 (9.0.62-1) unstable; urgency=medium + + * Team upload. + * New upstream version 9.0.62. + * Drop 0027-java11-compilation.patch because it is apparently no longer + required. + * Refresh disable-jacoco.patch for new release. + * Depend on java11-runtime-headless because Java 8 is no longer supported. + Thanks to Per Lundberg for the report. (Closes: #1006647) + + -- Markus Koschany Fri, 29 Apr 2022 23:10:59 +0200 + +tomcat9 (9.0.58-1) unstable; urgency=medium + + * Team upload. + * New upstream version 9.0.58. + * Add disable-jacoco.patch and remove the dependency on jacoco when running + the test suite. + + -- Markus Koschany Wed, 09 Feb 2022 15:51:20 +0100 + +tomcat9 (9.0.55-1) unstable; urgency=medium + + * Team upload. + * New upstream version 9.0.55. + + -- Markus Koschany Mon, 15 Nov 2021 22:12:42 +0100 + +tomcat9 (9.0.54-1) unstable; urgency=medium + + * Team upload. + * New upstream version 9.0.54. + - Fix CVE-2021-42340: + The fix for bug 63362 introduced a memory leak. The object introduced to + collect metrics for HTTP upgrade connections was not released for + WebSocket connections once the connection was closed. This created a + memory leak that, over time, could lead to a denial of service via an + OutOfMemoryError. + * Update 0010-debianize-build-xml.patch and depend on the setup-bnd task to + prevent a FTBFS when building the tests. This replaces the workaround by + setting addOSGi to false. + Thanks to Aurimas Fišeras for the report. + + -- Markus Koschany Fri, 22 Oct 2021 21:59:08 +0200 + +tomcat9 (9.0.53-1) unstable; urgency=medium + + * Team upload. + * New upstream version 9.0.53. + - Drop security patches. Fixed upstream. + - Fix CVE-2021-41079: + Apache Tomcat did not properly validate incoming TLS packets. When Tomcat + was configured to use NIO+OpenSSL or NIO2+OpenSSL for TLS, a specially + crafted packet could be used to trigger an infinite loop resulting in a + denial of service. + * Declare compliance with Debian Policy 4.6.0. + * Set the fileOwner of catalina.out to tomcat explicitly. + Thanks to Adam Cecile for the report. (Closes: #987179) + * Refresh 0021-dont-test-unsupported-ciphers.patch + * tomcat9.cron.daily: Set maxdepth to 1 so that log files of custom + applications in subdirectories of /var/log/tomcat9 are not compressed. + Thanks to Ludovic Pouzenc for the report. (Closes: #982961) + * Exclude TestJNDIRealmIntegration because of missing dependencies. + + -- Markus Koschany Fri, 24 Sep 2021 15:37:51 +0200 + +tomcat9 (9.0.43-3) unstable; urgency=medium + + * Team upload. + * CVE-2021-30640: Fix NullPointerException. + If no userRoleAttribute is specified in the user's Realm configuration its + default value will be null. This will cause a NPE in the methods + doFilterEscaping and doAttributeValueEscaping. This is upstream bug + https://bz.apache.org/bugzilla/show_bug.cgi?id=65308 + + -- Markus Koschany Tue, 10 Aug 2021 17:17:56 +0200 + +tomcat9 (9.0.43-2) unstable; urgency=medium + + * Team upload. + + [ mirabilos ] + * fix /var/log/tomcat9 permissions + fixup for commit 51128fe9fb2d4d0b56be675d845cf92e4301a6c3 + + [ Markus Koschany ] + * Fix CVE-2021-30640: + A vulnerability in the JNDI Realm of Apache Tomcat allows an attacker to + authenticate using variations of a valid user name and/or to bypass some of + the protection provided by the LockOut Realm. + * Fix CVE-2021-33037: + Apache Tomcat did not correctly parse the HTTP transfer-encoding request + header in some circumstances leading to the possibility to request + smuggling when used with a reverse proxy. Specifically: - Tomcat + incorrectly ignored the transfer encoding header if the client declared it + would only accept an HTTP/1.0 response; - Tomcat honoured the identify + encoding; and - Tomcat did not ensure that, if present, the chunked + encoding was the final encoding. + (Closes: #991046) + + -- Markus Koschany Sat, 07 Aug 2021 00:11:43 +0200 + +tomcat9 (9.0.43-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Rotate the catalina.out log file with the tomcat user (Closes: #971583) + * Switch to debhelper level 13 + + -- Emmanuel Bourg Tue, 02 Feb 2021 20:23:51 +0100 + +tomcat9 (9.0.41-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Standards-Version updated to 4.5.1 + + -- Emmanuel Bourg Wed, 09 Dec 2020 16:03:00 +0100 + +tomcat9 (9.0.40-1) unstable; urgency=medium + + [ Emmanuel Bourg ] + * New upstream release + - Refreshed the patches + * Changed the home directory of the tomcat user to /var/lib/tomcat + (Closes: #926338) + + [ Vincent McIntyre ] + * Automatically export the JAVA_HOME environment variable when the value + is defined in /etc/defaults/tomcat9 (Closes: #966338) + + -- Emmanuel Bourg Tue, 24 Nov 2020 08:21:29 +0100 + +tomcat9 (9.0.39-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * tomcat9-user now depends on netcat-openbsd instead of netcat + (Closes: #966158) + + -- Emmanuel Bourg Mon, 12 Oct 2020 17:16:57 +0200 + +tomcat9 (9.0.38-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Wed, 16 Sep 2020 16:04:03 +0200 + +tomcat9 (9.0.37-3) unstable; urgency=medium + + * control: Bump build-dep on bnd, drop bnd compat and re-export patches. + (Closes: #964433) + + -- Timo Aaltonen Thu, 06 Aug 2020 18:59:11 +0300 + +tomcat9 (9.0.37-2) unstable; urgency=medium + + * d/p/0029-fix-regression-in-bz64540.patch: Re-export util.net.jsse + and util.modeler.modules. (Closes: #964433) + + -- Timo Aaltonen Tue, 28 Jul 2020 14:09:13 +0300 + +tomcat9 (9.0.37-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + - Fixed the compatibility with the version of bnd in Debian + * Restored execute permission on /var/log/tomcat9 to the adm group + + -- Emmanuel Bourg Mon, 06 Jul 2020 22:39:32 +0200 + +tomcat9 (9.0.36-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Grant write access on /var/log/tomcat9 to the adm group (LP: #1861881) + + -- Emmanuel Bourg Tue, 23 Jun 2020 11:47:47 +0200 + +tomcat9 (9.0.35-1) unstable; urgency=medium + + * New upstream release + - Fixes CVE-2020-9484: Remote Code Execution via session persistence (Closes: #961209) + - Refreshed the patches + + -- Emmanuel Bourg Thu, 21 May 2020 15:50:03 +0200 + +tomcat9 (9.0.34-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Depend on libeclipse-jdt-core-java (>= 3.18.0) + * Switch to debhelper level 12 + + -- Emmanuel Bourg Mon, 27 Apr 2020 00:36:59 +0200 + +tomcat9 (9.0.31-1) unstable; urgency=medium + + * New upstream release + - Fixes CVE-2019-10072: Denial of Service (Closes: #930872) + - Fixes CVE-2019-12418: Local Privilege Escalation + - Fixes CVE-2019-17563: Session fixation attack + - Fixes CVE-2019-17569: HTTP Request Smuggling + - Fixes CVE-2020-1935: HTTP Request Smuggling + - Fixes CVE-2020-1938: AJP Request Injection (Closes: #952437) + - Fixes CATALINA_PID handling in catalina.sh (Closes: #948553) + - Refreshed the patches + - Fixed the compilation with Java 11 + * Moved the RequiresMountsFor directive in the service file + to the Unit section (Closes: #942316) + * Tightened the dependency on systemd (Closes: #931997) + * Standards-Version updated to 4.5.0 + + -- Emmanuel Bourg Mon, 24 Feb 2020 23:37:00 +0100 + +tomcat9 (9.0.27-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Standards-Version updated to 4.4.1 + + -- Emmanuel Bourg Mon, 14 Oct 2019 11:31:50 +0200 + +tomcat9 (9.0.24-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Thu, 22 Aug 2019 13:55:14 +0200 + +tomcat9 (9.0.22-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Track and download the new releases from GitHub + * Standards-Version updated to 4.4.0 + + -- Emmanuel Bourg Fri, 12 Jul 2019 15:01:28 +0200 + +tomcat9 (9.0.16-4) unstable; urgency=medium + + * Team upload. + + [ Emmanuel Bourg ] + * Fixed CVE-2019-0221: The SSI printenv command echoes user provided data + without escaping and is, therefore, vulnerable to XSS. SSI is disabled + by default (Closes: #929895) + + [ Thorsten Glaser ] + * Remove -XX:+UseG1GC from standard JAVA_OPTS; the JRE chooses + a suitable GC automatically anyway (Closes: #925928) + * Correct the ownership and permissions on the log directory: + group adm and setgid (Closes: #925929) + * Make the startup script honour the (renamed) $SECURITY_MANAGER + * debian/libexec/tomcat-locate-java.sh: Remove shebang and make + not executable as this is only ever sourced (makes no sense otherwise) + + [ Christian Hänsel ] + * Restored the variable expansion in /etc/default/tomcat9 (Closes: #926319) + + -- Emmanuel Bourg Thu, 13 Jun 2019 23:26:12 +0200 + +tomcat9 (9.0.16-3) unstable; urgency=medium + + * Removed read/write access to /var/lib/solr (Closes: #923299) + * Removed the broken catalina-ws.jar and catalina-jmx-remote.jar + symlinks in /usr/share/tomcat9/lib/ + + -- Emmanuel Bourg Tue, 26 Feb 2019 09:31:13 +0100 + +tomcat9 (9.0.16-2) unstable; urgency=medium + + * Team upload. + * tomcat9.service: Permit read and write access to /var/lib/solr too. + (Closes: #919638) + + -- Markus Koschany Mon, 18 Feb 2019 20:58:51 +0100 + +tomcat9 (9.0.16-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + - Install the new Chinese, Czech, German, Korean and Portuguese translations + - No longer build the extra WS and JMX jars + * Standards-Version updated to 4.3.0 + + -- Emmanuel Bourg Fri, 08 Feb 2019 08:26:48 +0100 + +tomcat9 (9.0.14-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Create the /var/log/tomcat9/ and /var/cache/tomcat9/ directories + at install time (Closes: #915791) + * Tightened the dependency on systemd + + -- Emmanuel Bourg Wed, 12 Dec 2018 13:45:52 +0100 + +tomcat9 (9.0.13-2) unstable; urgency=medium + + * Install the tomcat-embed-* artifacts with the 9.x version (Closes: #915578) + * Modified the dependencies required for creating the tomcat user + (adduser is replaced by systemd) (Closes: #915586) + * Fixed the tomcat-jasper pom to reference the ECJ dependency + from libeclipse-jdt-core-java + * Removed the redundant ReadWritePaths options in the service file for the log + and cache directories (Thanks to Lennart Poettering for the suggestion) + + -- Emmanuel Bourg Wed, 05 Dec 2018 10:04:52 +0100 + +tomcat9 (9.0.13-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + - Renamed the package to tomcat9 + - Removed the libservlet3.1-java package. From now on the Servlet API + is packaged in a separate package independent from Tomcat. + - Depend on libeclipse-jdt-core-java (>= 3.14.0) instead of libecj-java + - Updated the policy files in /etc/tomcat8/policy.d/ + - Use the OSGi metadata generated by the upstream build + - Deploy the Tomcat artifacts in the Maven repository with the 9.x version + - Updated the README file + * Removed the SysV init script + * Restart the server automatically on failures + * Use a fixed non-configurable user 'tomcat' to run the server + * Removed the debconf integration. The user being now unmodifiable, + the remaining configuration parameter JAVA_OPTS can be edited in + /etc/default/tomcat9 + * No longer add the 'common', 'server' and 'shared' directories under + CATALINA_HOME and CATALINA_BASE to the classpath. Extra jar files should go + to the 'lib' directory. + * Let Tomcat handle the rotation of its log files with the maxDays parameter + of the valves and log handlers instead of relying on a cron job + * Renamed the TOMCAT_SECURITY parameter to SECURITY_MANAGER in the service + configuration file + * Simplified the postinst script by using systemd-sysusers to create + the 'tomcat' user + * No longer create the /etc/tomcat9/Catalina/localhost directory at install + time and let Tomcat create it automatically + * Let systemd automatically create /var/log/tomcat9 and /var/cache/tomcat9 + * Prevent Tomcat from writing outside of /var/log/tomcat9, /var/cache/tomcat9, + /var/lib/tomcat9/webapps and /etc/tomcat9/Catalina by default. This can be + overridden (see the README file). + * Build and install the extra jar catalina-ws.jar + * No longer recommend libcommons-pool-java and libcommons-dbcp-java since + Tomcat already embeds its own version of these libraries + * Support three-way merge when upgrading the configuration files + * Use the G1 garbage collector by default instead of Concurrent Mark Sweep + * The setenv.sh script in tomcat9-user and the service startup script now + share the same JDK detection logic + + -- Emmanuel Bourg Wed, 28 Nov 2018 15:06:00 +0100 + +tomcat8 (8.5.35-3) UNRELEASED; urgency=medium + + * Team upload. + * Updated the version required for libtcnative-1 (>= 1.2.18) + * Install the Russian translation added in Tomcat 8.5.33 + + -- Emmanuel Bourg Tue, 20 Nov 2018 14:38:01 +0100 + +tomcat8 (8.5.35-2) unstable; urgency=medium + + * Team upload. + * Fixed the build failure with Easymock 4 (Closes: #913402) + + -- Emmanuel Bourg Mon, 12 Nov 2018 10:52:08 +0100 + +tomcat8 (8.5.35-1) unstable; urgency=medium + + * Team upload. + + [ Thomas Opfer ] + * Removed old version requirement for package ant-optional that is not + required any more. + + [ Emmanuel Bourg ] + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Thu, 08 Nov 2018 23:40:00 +0100 + +tomcat8 (8.5.34-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Mon, 10 Sep 2018 14:31:03 +0200 + +tomcat8 (8.5.33-1) unstable; urgency=medium + + * Team upload. + * New upstream version 8.5.33. + - Tomcat compiles to Java 7 bytecode and passes release=7 to javac now. + This ensures backwards compatibility with older JREs. (Closes: #906447) + * Declare compliance with Debian Policy 4.2.1. + * Refresh 0025-invalid-configuration-exit-status.patch. + + -- Markus Koschany Mon, 27 Aug 2018 13:41:16 +0200 + +tomcat8 (8.5.32-2) unstable; urgency=medium + + * Team upload. + * Added a systemd service file (Closes: #832151, #817909) + * Look for the Java runtime in the paths used by java-package >= 0.61 + (/usr/lib/jvm/oracle-java-{jre,jdk}-*) (Closes: #894318) + * Install catalina.policy in the tomcat8-user package to be able to run + custom instances with a security manager (Closes: #736321) + * Disabled the shutdown port (8005) by default + * Updated the policy files in /etc/tomcat8/policy.d/ + * Added the missing Maven rules to use the 8.x generic version for + tomcat-jaspic-api, tomcat-storeconfig and tomcat-util-scan + * Set the gecos field when creating the tomcat8 user + * No longer set JSSE_HOME in the init script (JSSE is enabled by default) + * Standards-Version updated to 4.2.0 + + -- Emmanuel Bourg Thu, 09 Aug 2018 17:53:44 +0200 + +tomcat8 (8.5.32-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Mon, 25 Jun 2018 14:51:50 +0200 + +tomcat8 (8.5.31-1) unstable; urgency=medium + + * Team upload. + * New upstream release + * Build with ant/1.10.3-2 and the automatic 'release' attribute restoring + the backward compatibility with Java 7 (Closes: #895866) + * Search for Java 10 and 11 runtimes + * Don't follow the symlinks when setting the owner of the /var/log/tomcat8 + and /var/cache/tomcat8 directories in the postinst script + * Use salsa.debian.org Vcs-* URLs + + -- Emmanuel Bourg Thu, 14 Jun 2018 13:32:46 +0200 + +tomcat8 (8.5.30-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Standards-Version updated to 4.1.4 + + -- Emmanuel Bourg Thu, 12 Apr 2018 09:49:28 +0200 + +tomcat8 (8.5.29-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Mon, 12 Mar 2018 16:43:57 +0100 + +tomcat8 (8.5.28-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + - Disabled the tests checking the ARIA cipher since it isn't enabled + by default in OpenSSL + * Standards-Version updated to 4.1.3 + * Switch to debhelper level 11 + * Use a secure URL for checking and downloading the new releases + * No longer parse dpkg-parsechangelog in debian/rules + + -- Emmanuel Bourg Fri, 16 Feb 2018 13:43:01 +0100 + +tomcat8 (8.5.24-2) unstable; urgency=medium + + * Team upload. + * Removed the setDefaultAsyncSendTimeout method mistakenly added to + javax.websocket.WebSocketContainer in the version 8.5.24 (Closes: #884046) + + -- Emmanuel Bourg Thu, 14 Dec 2017 12:35:33 +0100 + +tomcat8 (8.5.24-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Standards-Version updated to 4.1.2 + + -- Emmanuel Bourg Fri, 01 Dec 2017 09:20:18 +0100 + +tomcat8 (8.5.23-1) unstable; urgency=medium + + * Team upload. + * New upstream release + * Standards-Version updated to 4.1.1 + + -- Emmanuel Bourg Fri, 13 Oct 2017 00:01:48 +0200 + +tomcat8 (8.5.21-1) unstable; urgency=medium + + * Team upload. + + [ Emmanuel Bourg ] + * New upstream release + - Refreshed the patches + - Disabled Checkstyle + * Changed the Class-Path manifest entry of tomcat8-jasper.jar to use + the specification jars from libtomcat8-java instead of libservlet3.1-java + (Closes: #867247) + + [ Miguel Landaeta ] + * Remove myself from uploaders. (Closes: #871892) + * Update copyright info. + + -- Emmanuel Bourg Wed, 20 Sep 2017 10:06:56 +0200 + +tomcat8 (8.5.16-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Standards-Version updated to 4.0.0 + + -- Emmanuel Bourg Mon, 26 Jun 2017 16:03:53 +0200 + +tomcat8 (8.5.15-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Wed, 21 Jun 2017 13:00:44 +0200 + +tomcat8 (8.5.14-2) unstable; urgency=high + + * Team upload. + * Fixed CVE-2017-5664: Static error pages can be overwritten if the + DefaultServlet is configured to permit writes (Closes: #864447) + + -- Emmanuel Bourg Thu, 08 Jun 2017 12:28:34 +0200 + +tomcat8 (8.5.14-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Removed the CVE patches (fixed in this release) + + -- Emmanuel Bourg Mon, 08 May 2017 00:17:52 +0200 + +tomcat8 (8.5.12-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Tue, 18 Apr 2017 09:53:23 +0200 + +tomcat8 (8.5.11-2) unstable; urgency=medium + + * Team upload. + * Fix the following security vulnerabilities (Closes: #860068): + Thanks to Salvatore Bonaccorso for the report. + - CVE-2017-5647: + A bug in the handling of the pipelined requests when send file was used + resulted in the pipelined request being lost when send file processing of + the previous request completed. This could result in responses appearing + to be sent for the wrong request. For example, a user agent that sent + requests A, B and C could see the correct response for request A, the + response for request C for request B and no response for request C. + - CVE-2017-5648: + It was noticed that some calls to application listeners did not use the + appropriate facade object. When running an untrusted application under a + SecurityManager, it was therefore possible for that untrusted application + to retain a reference to the request or response object and thereby access + and/or modify information associated with another web application. + - CVE-2017-5650: + The handling of an HTTP/2 GOAWAY frame for a connection did not close + streams associated with that connection that were currently waiting for a + WINDOW_UPDATE before allowing the application to write more data. These + waiting streams each consumed a thread. A malicious client could therefore + construct a series of HTTP/2 requests that would consume all available + processing threads. + - CVE-2017-5651: + The refactoring of the HTTP connectors for 8.5.x onwards, introduced a + regression in the send file processing. If the send file processing + completed quickly, it was possible for the Processor to be added to the + processor cache twice. This could result in the same Processor being used + for multiple requests which in turn could lead to unexpected errors and/or + response mix-up. + * debian/control: tomcat8: Fix Lintian error and depend on lsb-base. + + -- Markus Koschany Wed, 12 Apr 2017 09:58:46 +0200 + +tomcat8 (8.5.11-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Recommend Java 8 in /etc/default/tomcat8 + + -- Emmanuel Bourg Tue, 17 Jan 2017 15:09:30 +0100 + +tomcat8 (8.5.9-2) unstable; urgency=medium + + * Team upload. + * Require Java 8 or higher (Closes: #848612) + + -- Emmanuel Bourg Mon, 19 Dec 2016 15:35:19 +0100 + +tomcat8 (8.5.9-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Restored the classloading from the common, server and shared directories + under CATALINA_BASE (Closes: #847137) + * Fixed the installation error when JAVA_OPTS in /etc/default/tomcat8 + contains the '%' character (Closes: #770911) + + -- Emmanuel Bourg Thu, 08 Dec 2016 22:26:36 +0100 + +tomcat8 (8.5.8-2) unstable; urgency=medium + + * Team upload. + * Upload to unstable. + * No longer make /etc/tomcat8/Catalina/localhost writable by the tomcat8 user + in the postinst script (Closes: #845393) + * The tomcat8 user is no longer removed when the package is purged + (Closes: #845385) + * Compress and remove the access log files with a .txt extension + (Closes: #845661) + * Added the delaycompress option to the logrotate configuration + of catalina.out (Closes: #843135) + * Changed the home directory for the tomcat8 user from /usr/share/tomcat8 + to /var/lib/tomcat8 (Closes: #833261) + * Aligned the logging configuration with the upstream one + * Set the proper permissions for /etc/tomcat8/jaspic-providers.xml + * Install the new library jaspic-api.jar + * Install the Maven artifacts for tomcat-storeconfig + * Simplified debian/rules + + -- Emmanuel Bourg Thu, 01 Dec 2016 18:41:14 +0100 + +tomcat8 (8.5.8-1) experimental; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + - Tomcat no longer builds tomcat-embed-logging-juli.jar + - Updated the policy files + - Added a NEWS file detailing the major changes in Tomcat 8.5.x + * Enabled the APR library loading by default (required for HTTP/2 support) + * Promoted libtcnative-1 from suggested to recommended dependency + * Enabled the APR tests + * Fixed the test failure with TestStandardContextAliases + * Added a link to the Tomcat 8.5 migration guide in README.Debian + * Adapted debian/orig-tar.sh to download the 8.5.x releases + + -- Emmanuel Bourg Thu, 17 Nov 2016 23:54:35 +0100 + +tomcat8 (8.0.39-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Tue, 15 Nov 2016 15:37:48 +0100 + +tomcat8 (8.0.38-2) unstable; urgency=high + + * Team upload. + * CVE-2016-1240 follow-up: + - The previous init.d fix was vulnerable to a race condition that could + be exploited to make any existing file writable by the tomcat user. + Thanks to Paul Szabo for the report and the fix. + - The catalina.policy file generated on startup was affected by a similar + vulnerability that could be exploited to overwrite any file on the system. + Thanks to Paul Szabo for the report. + * Install the extra jar catalina-jmx-remote.jar (Closes: #762916) + * Added the new libtomcat8-embed-java package containing the libraries + for embedding Tomcat into other applications. + * Switch to debhelper level 10 + + -- Emmanuel Bourg Fri, 28 Oct 2016 01:17:23 +0200 + +tomcat8 (8.0.38-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Hardened the init.d script, thanks to Paul Szabo (Closes: #840685) + * Fixed the OSGi metadata for tomcat8-jasper.jar and tomcat8-jasper-el.jar + * Depend on libcglib-nodep-java instead of libcglib3-java + * Removed the unused Lintian overrides + + -- Emmanuel Bourg Wed, 19 Oct 2016 11:01:03 +0200 + +tomcat8 (8.0.37-1) unstable; urgency=medium + + * Team upload. + * New upstream release + * Removed 0001-set-UTF-8-as-default-character-encoding.patch (fixed upstream) + + -- Emmanuel Bourg Mon, 19 Sep 2016 09:37:33 +0200 + +tomcat8 (8.0.36-3) unstable; urgency=high + + * Team upload. + * Fixed CVE-2016-1240: A flaw in the init.d startup script allows local + attackers who have gained access to the server in the context of the + tomcat user through a vulnerability in a web application to replace + the catalina.out file with a symlink to an arbitrary file on the system, + potentially leading to a root privilege escalation. + Thanks to Dawid Golunski for the report. + * Removed the default 128M heap limit (LP: #568823) + * Depend on taglibs-standard instead of jakarta-taglibs-standard + + -- Emmanuel Bourg Wed, 14 Sep 2016 10:20:28 +0200 + +tomcat8 (8.0.36-2) unstable; urgency=medium + + * Team upload. + * Do not unconditionally overwrite files in /etc/tomcat8 anymore. + (Closes: #825786) + * Change file permissions to 640 for Debian files in /etc/tomcat8. + + -- Markus Koschany Tue, 02 Aug 2016 10:50:42 +0200 + +tomcat8 (8.0.36-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + - Depend on libecj-java (>= 3.11.0) + * Standards-Version updated to 3.9.8 (no changes) + * Use a secure Vcs-Git URL + + -- Emmanuel Bourg Tue, 14 Jun 2016 14:34:46 +0200 + +tomcat8 (8.0.32-1) unstable; urgency=medium + + * Team upload. + * New upstream release + * Fixed a warning in catalina.out caused by an incorrect path + for the root context (Closes: #808378) + * Standards-Version updated to 3.9.7 (no changes) + + -- Emmanuel Bourg Mon, 21 Dec 2015 11:20:10 +0100 + +tomcat8 (8.0.30-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Use LC_ALL instead of LANG to format the date and make the documentation + reproducible on the builders + + -- Emmanuel Bourg Fri, 18 Dec 2015 11:44:06 +0100 + +tomcat8 (8.0.28-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Fixed a localized date in the documentation to improve the reproducibility + + -- Emmanuel Bourg Mon, 19 Oct 2015 11:12:07 +0200 + +tomcat8 (8.0.26-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * Changed the authbind configuration to allow IPv6 connections (LP: #1443041) + * Fixed an upgrade error when /etc/tomcat8/tomcat-users.xml is removed + (LP: #1010791) + * Fixed a minor HTML error in the default index.html file (LP: #1236132) + + -- Emmanuel Bourg Mon, 24 Aug 2015 00:30:40 +0200 + +tomcat8 (8.0.24-1) unstable; urgency=medium + + * Team upload. + * New upstream release + - Refreshed the patches + * debian/rules: Use an english locale when generating the documentation + to improve the reproducibility + + -- Emmanuel Bourg Wed, 08 Jul 2015 17:42:14 +0200 + +tomcat8 (8.0.23-1) unstable; urgency=medium + + * New upstream release + * debian/rules: Set the 'year' and 'today-iso-8601' build variables + to improve the reproducibility + + -- Emmanuel Bourg Tue, 26 May 2015 16:04:01 +0200 + +tomcat8 (8.0.22-2) unstable; urgency=medium + + * Replaced the date in ServerInfo.properties with the latest date + in debian/changelog to make the build reproducible + * debian/rules: + - Modified to use the dh sequencer + - Simplified the ant invocation and moved some properties + to debian/ant.properties + - Do not set the version.* properties already defined + in build.properties.default + - Renamed T_VER to VERSION + - Removed the RWFILES and RWLOC variables + - Merged the ANT_ARGS and ANT_INVOKE variables + - No longer remove the long gone .svn directories under + /usr/share/tomcat8/webapps/default_root + - Let dh_fixperms set the permissions instead of calling chmod +x + - Use debian/tomcat8-user.manpages instead of calling dh_installman + - Updated the copyright year in the Javadoc + - Simplified the call to mh_install + + -- Emmanuel Bourg Thu, 07 May 2015 14:13:30 +0200 + +tomcat8 (8.0.22-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + - No longer install tomcat-spdy.jar (removed upstream) + * Removed the timestamp from the Javadoc of the Servlet API + to make the build reproducible + + -- Emmanuel Bourg Wed, 06 May 2015 09:30:38 +0200 + +tomcat8 (8.0.21-2) unstable; urgency=medium + + * Upload to unstable. + + -- Miguel Landaeta Fri, 01 May 2015 12:41:13 -0300 + +tomcat8 (8.0.21-1) experimental; urgency=medium + + * New upstream release + - Refreshed the patches + * debian/orig-tar.sh: Exclude the taglibs-standard-*.jar files + from the upstream tarball + * Support the JVMs installed by the older versions of java-package (<< 0.52) + and the oracle-java-installer packages from webupd8 (Closes: #769166) + + -- Emmanuel Bourg Mon, 30 Mar 2015 19:40:22 +0200 + +tomcat8 (8.0.18-1) experimental; urgency=medium + + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Tue, 27 Jan 2015 22:54:00 +0100 + +tomcat8 (8.0.17-1) experimental; urgency=medium + + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Mon, 19 Jan 2015 09:58:16 +0100 + +tomcat8 (8.0.15-1) experimental; urgency=medium + + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Mon, 08 Dec 2014 23:59:10 +0100 + +tomcat8 (8.0.14-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Build depend on libcglib3-java instead of libcglib-java + * Standards-Version updated to 3.9.6 (no changes) + + -- Emmanuel Bourg Mon, 29 Sep 2014 13:23:43 +0200 + +tomcat8 (8.0.12-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Fixed the tomcat8-examples configuration (Closes: #753372) + * No longer create the common/server/shared directories under + /var/lib/tomcat8, and use a unique lib directory as documented + upstream since Tomcat 6. The old directories are still supported + if inherited from a previous installation (Closes: #754386) + * Depend on libecj-java >= 3.10.0 to support the new Java 8 syntax in JSPs + * Install the missing tomcat-dbcp.jar in libtomcat8-java and use it as + the default JDBC pool implementation instead of Commons DBCP. + * Removed the obsolete patch 0012-java7-compat.patch + * Tightened the build dependency on junit4 (>= 4.11) + * Build the Javadoc with the JDK specified by the JAVA_HOME variable + instead of the default JDK (this fixes a build failure when backporting + to Wheezy) + * Removed the note about the authbind IPv6 incompatibility + in /etc/defaults/tomcat8 + + -- Emmanuel Bourg Wed, 17 Sep 2014 16:23:52 +0200 + +tomcat8 (8.0.9-1) unstable; urgency=medium + + [ Emmanuel Bourg ] + * New upstream release + - Refreshed the patches + * Search for OpenJDK 8 and Oracle JDKs when starting the server + * Removed the dependency on the non existent java-7-runtime package + * Fixed a link still pointing to the Tomcat 7 documentation in README.Debian + * Updated the version required for libtcnative-1 (>= 1.1.30) + + [ tony mancill ] + * Update README.Debian with information about migration guides. + + -- Emmanuel Bourg Tue, 24 Jun 2014 21:28:37 +0200 + +tomcat8 (8.0.8-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + + -- Emmanuel Bourg Thu, 22 May 2014 13:01:55 +0200 + +tomcat8 (8.0.5-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + - Disabled Java 8 support in JSPs (requires an Eclipse compiler update) + * Fixed the name of the doc-base file for libservlet3.1-java (Closes: #746338) + * Update email addresses of maintainers. + + -- Emmanuel Bourg Tue, 29 Apr 2014 10:22:45 +0200 + +tomcat8 (8.0.3-1) unstable; urgency=medium + + [ Emmanuel Bourg ] + * Team upload. + * New upstream release (Closes: #722675) + - Updated the version of the Servlet, JSP and EL APIs + - Switched to Java 7 + - Updated the watch file to match the Tomcat 8 releases + - Refreshed the patches + - Updated debian/copyright, documented the xsd files licensed under the CDDL + - Installed the new jars (spdy, jni, websocket, websocket-api, storeconfig) + - Updated the artifactId of the specification jars to include + the new javax prefix + - Added the javax.websocket-api artifact to libservlet3.1-java + - New build dependency on cglib, easymock and objenesis + * Added a patch to include the name of the distribution on the error pages + * Use XZ compression for the upstream tarball + * debian/control: + - Replaced Sun Microsystems with Oracle in the packages descriptions + - Mentioned 'Apache Tomcat' in the packages descriptions + - Standards-Version updated to 3.9.5 (no changes) + * Deploy the Tomcat artifacts in the Maven repository with the 8.x version + instead of 'debian' to avoid conflicts with other versions of Tomcat. + * Hard coded the versions in the poms in debian/javaxpoms to fix the version + of the dependencies for jsp-api + * Renamed the jars in /usr/share/java to tomcat8-xxx to avoid conflicts + with other versions of Tomcat + * Added the missing descriptions to the patches + * Added a patch to ignore the failing tests + * Moved the tomcat-{servlet|jsp|el}-api artifacts from libservlet3.1-java + to libtomcat8-java and changed their versions to the Tomcat version instead + of the specification version. + * Removed libservlet3.1-java.links defining the tomcat-* links + in /usr/share/java with the specifications versions + * The symlinks to /usr/share/tomcat8/lib are no longer split between the two + packages libtomcat8-java and tomcat8-common. tomcat8-common assembles all + the jars required by Tomcat (tomcat jars + dbcp + pool). libtomcat8-java + deploys only the jars in /usr/share/java and the Maven artifacts in + /usr/share/maven-repo. + * Added the EL and WebSocket APIs to libservlet3.1-java-doc + * Added a Lintian override for the incompatible-java-bytecode-format warning + since Tomcat requires Java 7 + * Added a Lintian override to clear the codeless-jar warnings + on the tomcat-i18n jars instead of a patch turning them into zip files. + * Removed 0011-fix-classpath-lintian-warnings.patch and specified + the classpath of jasper.jar in libtomcat8-java.manifest instead. + + [ tony mancill ] + * Include tomcat-util-scan.jar in the libtomcat8-java package. + * Remove debian/NEWS (inapplicable to this release). + * Prune debian/changelog to only contain tomcat8 entries. + + -- Emmanuel Bourg Sat, 15 Mar 2014 23:23:14 +0100 diff --git a/debian/clean b/debian/clean new file mode 100644 index 0000000..b8c506f --- /dev/null +++ b/debian/clean @@ -0,0 +1,4 @@ +output/ +webapps/examples/WEB-INF/lib/*.jar +debian/tomcat10.postrm +debian/poms/ diff --git a/debian/compat b/debian/compat deleted file mode 100644 index b4de394..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -11 diff --git a/debian/context/docs.xml b/debian/context/docs.xml new file mode 100644 index 0000000..2e03ac7 --- /dev/null +++ b/debian/context/docs.xml @@ -0,0 +1,20 @@ + + + diff --git a/debian/context/examples.xml b/debian/context/examples.xml new file mode 100644 index 0000000..38bcde2 --- /dev/null +++ b/debian/context/examples.xml @@ -0,0 +1,5 @@ + + + + diff --git a/debian/context/host-manager.xml b/debian/context/host-manager.xml new file mode 100644 index 0000000..478600f --- /dev/null +++ b/debian/context/host-manager.xml @@ -0,0 +1,20 @@ + + + diff --git a/debian/context/manager.xml b/debian/context/manager.xml new file mode 100644 index 0000000..98d19ae --- /dev/null +++ b/debian/context/manager.xml @@ -0,0 +1,20 @@ + + + diff --git a/debian/control b/debian/control index cb7c4a0..9a020b6 100644 --- a/debian/control +++ b/debian/control @@ -1,15 +1,157 @@ -Source: template-repository -Section: unknown +Source: tomcat10 +Section: java Priority: optional -Maintainer: Tsic404 -Build-Depends: debhelper (>= 11) -Standards-Version: 4.1.3 -Homepage: https://github.com/deepin-community/template-repository -#Vcs-Browser: https://salsa.debian.org/debian/deepin-community-template-repository -#Vcs-Git: https://salsa.debian.org/debian/deepin-community-template-repository.git - -Package: template-repository -Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends} -Description: - +Maintainer: Debian Java Maintainers +Uploaders: + tony mancill , + Emmanuel Bourg , + Markus Koschany +Build-Depends: + ant-optional, + bnd (>= 5.0.1), + debhelper-compat (= 13), + default-jdk, + javahelper, + junit4 (>= 4.11), + libcglib-nodep-java, + libeasymock-java (>= 3.0), + libeclipse-jdt-core-java (>= 3.26.0), + libhamcrest-java (>= 1.3), + libjaxrpc-api-java, + libobjenesis-java, + libtaglibs-standard-impl-java, + libtaglibs-standard-spec-java, + libtcnative-1 (>= 1.2.35), + libwsdl4j-java, + lsb-release, + maven-repo-helper, + tomcat-jakartaee-migration (>= 1.0.7-2~) +Standards-Version: 4.7.0 +Vcs-Git: https://salsa.debian.org/java-team/tomcat10.git +Vcs-Browser: https://salsa.debian.org/java-team/tomcat10 +Homepage: http://tomcat.apache.org + +Package: tomcat10-common +Architecture: all +Depends: + default-jre-headless | java11-runtime-headless | java11-runtime, + libtomcat10-java (>= ${source:Version}), + ${misc:Depends} +Description: Apache Tomcat 10 - Servlet and JSP engine -- common files + Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) + specifications from Oracle, and provides a "pure Java" HTTP web + server environment for Java code to run. + . + This package contains common files needed by the tomcat10 and tomcat10-user + packages (Tomcat 10 scripts and libraries). + +Package: tomcat10 +Architecture: all +Depends: + systemd (>= 238) | systemd-sysusers, + systemd (>= 238) | systemd-tmpfiles, + tomcat10-common (>= ${source:Version}), + ucf, + ${misc:Depends} +Recommends: + libtcnative-1 (>= 1.2.18) +Suggests: + tomcat10-admin (>= ${source:Version}), + tomcat10-docs (>= ${source:Version}), + tomcat10-examples (>= ${source:Version}), + tomcat10-user (>= ${source:Version}) +Description: Apache Tomcat 10 - Servlet and JSP engine + Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) + specifications from Oracle, and provides a "pure Java" HTTP web + server environment for Java code to run. + . + This package contains only the startup scripts for the system-wide daemon. + No documentation or web applications are included here, please install + the tomcat10-docs and tomcat10-examples packages if you want them. + Install tomcat10-user instead of this package if you don't want Tomcat to + start as a service. + +Package: tomcat10-user +Architecture: all +Depends: + netcat-openbsd, + tomcat10-common (>= ${source:Version}), + ${misc:Depends} +Suggests: + tomcat10 (>= ${source:Version}), + tomcat10-admin (>= ${source:Version}), + tomcat10-docs (>= ${source:Version}), + tomcat10-examples (>= ${source:Version}) +Description: Apache Tomcat 10 - Servlet and JSP engine -- tools to create user instances + Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) + specifications from Oracle, and provides a "pure Java" HTTP web + server environment for Java code to run. + . + This package contains files needed to create a user Tomcat instance. + This user Tomcat instance can be started and stopped using the scripts + provided in the Tomcat instance directory. + +Package: libtomcat10-java +Architecture: all +Depends: + libeclipse-jdt-core-java (>= 3.26.0), + ${misc:Depends} +Suggests: + tomcat10 (>= ${source:Version}) +Description: Apache Tomcat 10 - Servlet and JSP engine -- core libraries + Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) + specifications from Oracle, and provides a "pure Java" HTTP web + server environment for Java code to run. + . + This package contains the Tomcat core classes which can be used by other + Java applications to embed Tomcat. + +Package: libtomcat10-embed-java +Architecture: all +Depends: + libeclipse-jdt-core-java (>= 3.26.0), + ${misc:Depends} +Description: Apache Tomcat 10 - Servlet and JSP engine -- embed libraries + Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) + specifications from Oracle, and provides a "pure Java" HTTP web + server environment for Java code to run. + . + This package contains the libraries required to embed Tomcat into Java + applications. + +Package: tomcat10-admin +Architecture: all +Depends: + tomcat10-common (>= ${source:Version}), + ${misc:Depends} +Description: Apache Tomcat 10 - Servlet and JSP engine -- admin web applications + Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) + specifications from Oracle, and provides a "pure Java" HTTP web + server environment for Java code to run. + . + This package contains the administrative web interfaces. + +Package: tomcat10-examples +Architecture: all +Depends: + tomcat10-common (>= ${source:Version}), + ${misc:Depends} +Description: Apache Tomcat 10 - Servlet and JSP engine -- example web applications + Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) + specifications from Oracle, and provides a "pure Java" HTTP web + server environment for Java code to run. + . + This package contains the default Tomcat example webapps. + +Package: tomcat10-docs +Section: doc +Architecture: all +Depends: + tomcat10-common (>= ${source:Version}), + ${misc:Depends} +Description: Apache Tomcat 10 - Servlet and JSP engine -- documentation + Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) + specifications from Oracle, and provides a "pure Java" HTTP web + server environment for Java code to run. + . + This package contains the online documentation web application. diff --git a/debian/copyright b/debian/copyright index f5c805e..9dd9288 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,22 +1,395 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: template-repository -Source: https://github.com/deepin-community/template-repository +Upstream-Name: Apache Tomcat +Source: https://tomcat.apache.org/download-10.cgi +Files-Excluded: */taglibs-standard-*.jar Files: * -Copyright: 2023 Tsic404 -License: GPL-2+ - This package is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - . - This package is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - . - You should have received a copy of the GNU General Public License - along with this program. If not, see - . - On Debian systems, the complete text of the GNU General - Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". +Copyright: 2000-2024, The Apache Software Foundation. + 2002, International Business Machines Corporation. +License: Apache-2.0 + +Files: java/jakarta/servlet/resources/javaee_5.xsd + java/jakarta/servlet/resources/javaee_6.xsd + java/jakarta/servlet/resources/javaee_7.xsd + java/jakarta/servlet/resources/javaee_8.xsd + java/jakarta/servlet/resources/javaee_web_services_1_2.xsd + java/jakarta/servlet/resources/javaee_web_services_1_3.xsd + java/jakarta/servlet/resources/javaee_web_services_1_4.xsd + java/jakarta/servlet/resources/javaee_web_services_client_1_2.xsd + java/jakarta/servlet/resources/javaee_web_services_client_1_3.xsd + java/jakarta/servlet/resources/javaee_web_services_client_1_4.xsd + java/jakarta/servlet/resources/jsp_2_2.xsd + java/jakarta/servlet/resources/jsp_2_3.xsd + java/jakarta/servlet/resources/web-app_3_0.xsd + java/jakarta/servlet/resources/web-app_3_1.xsd + java/jakarta/servlet/resources/web-app_4_0.xsd + java/jakarta/servlet/resources/web-common_3_0.xsd + java/jakarta/servlet/resources/web-common_3_1.xsd + java/jakarta/servlet/resources/web-common_4_0.xsd + java/jakarta/servlet/resources/web-fragment_3_0.xsd + java/jakarta/servlet/resources/web-fragment_3_1.xsd + java/jakarta/servlet/resources/web-fragment_4_0.xsd +Copyright: 2003-2009, Sun Microsystems, Inc. + 2009-2017, Oracle and/or its affiliates +License: CDDL + +Files: debian/* +Copyright: 2008,2011, Canonical Ltd. + 2008-2010, Thierry Carrez + 2008, Paul Cager + 2009, Damien Raude-Morvan + 2009-2010, Torsten Werner + 2009-2010, Ludovic Claude + 2009-2010, Niels Thykier + 2010, Marcus Better + 2010-2014, tony mancill + 2011, Ernesto Hernández-Novich + 2011-2014, James Page + 2011-2015, Miguel Landaeta + 2013, Jakub Adam + 2013-2014, Gianfranco Costamagna + 2013-2018, Emmanuel Bourg + 2016-2024, Markus Koschany +License: Apache-2.0 + +License: Apache-2.0 + On Debian systems, the full text of the Apache-2.0 license + can be found in the file '/usr/share/common-licenses/Apache-2.0' + +License: CDDL + COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 + . + 1. Definitions. + . + 1.1. "Contributor" means each individual or entity that + creates or contributes to the creation of Modifications. + 1.2. "Contributor Version" means the combination of the + Original Software, prior Modifications used by a + Contributor (if any), and the Modifications made by that + particular Contributor. + 1.3. "Covered Software" means (a) the Original Software, or + (b) Modifications, or (c) the combination of files + containing Original Software with files containing + Modifications, in each case including portions thereof. + 1.4. "Executable" means the Covered Software in any form + other than Source Code. + 1.5. "Initial Developer" means the individual or entity + that first makes Original Software available under this + License. + 1.6. "Larger Work" means a work which combines Covered + Software or portions thereof with code not governed by the + terms of this License. + 1.7. "License" means this document. + 1.8. "Licensable" means having the right to grant, to the + maximum extent possible, whether at the time of the initial + grant or subsequently acquired, any and all of the rights + conveyed herein. + 1.9. "Modifications" means the Source Code and Executable + form of any of the following: + A. Any file that results from an addition to, + deletion from or modification of the contents of a + file containing Original Software or previous + Modifications; + B. Any new file that contains any part of the + Original Software or previous Modification; or + C. Any new file that is contributed or otherwise made + available under the terms of this License. + 1.10. "Original Software" means the Source Code and + Executable form of computer software code that is + originally released under this License. + 1.11. "Patent Claims" means any patent claim(s), now owned + or hereafter acquired, including without limitation, + method, process, and apparatus claims, in any patent + Licensable by grantor. + 1.12. "Source Code" means (a) the common form of computer + software code in which modifications are made and (b) + associated documentation included in or with such code. + 1.13. "You" (or "Your") means an individual or a legal + entity exercising rights under, and complying with all of + the terms of, this License. For legal entities, "You" + includes any entity which controls, is controlled by, or is + under common control with You. For purposes of this + definition, "control" means (a) the power, direct or + indirect, to cause the direction or management of such + entity, whether by contract or otherwise, or (b) ownership + of more than fifty percent (50%) of the outstanding shares + or beneficial ownership of such entity. + . + 2. License Grants. + . + 2.1. The Initial Developer Grant. + Conditioned upon Your compliance with Section 3.1 below and + subject to third party intellectual property claims, the + Initial Developer hereby grants You a world-wide, + royalty-free, non-exclusive license: + (a) under intellectual property rights (other than + patent or trademark) Licensable by Initial Developer, + to use, reproduce, modify, display, perform, + sublicense and distribute the Original Software (or + portions thereof), with or without Modifications, + and/or as part of a Larger Work; and + (b) under Patent Claims infringed by the making, + using or selling of Original Software, to make, have + made, use, practice, sell, and offer for sale, and/or + otherwise dispose of the Original Software (or + portions thereof). + (c) The licenses granted in Sections 2.1(a) and (b) + are effective on the date Initial Developer first + distributes or otherwise makes the Original Software + available to a third party under the terms of this + License. + (d) Notwithstanding Section 2.1(b) above, no patent + license is granted: (1) for code that You delete from + the Original Software, or (2) for infringements + caused by: (i) the modification of the Original + Software, or (ii) the combination of the Original + Software with other software or devices. + 2.2. Contributor Grant. + Conditioned upon Your compliance with Section 3.1 below and + subject to third party intellectual property claims, each + Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + (a) under intellectual property rights (other than + patent or trademark) Licensable by Contributor to + use, reproduce, modify, display, perform, sublicense + and distribute the Modifications created by such + Contributor (or portions thereof), either on an + unmodified basis, with other Modifications, as + Covered Software and/or as part of a Larger Work; and + (b) under Patent Claims infringed by the making, + using, or selling of Modifications made by that + Contributor either alone and/or in combination with + its Contributor Version (or portions of such + combination), to make, use, sell, offer for sale, + have made, and/or otherwise dispose of: (1) + Modifications made by that Contributor (or portions + thereof); and (2) the combination of Modifications + made by that Contributor with its Contributor Version + (or portions of such combination). + (c) The licenses granted in Sections 2.2(a) and + 2.2(b) are effective on the date Contributor first + distributes or otherwise makes the Modifications + available to a third party. + (d) Notwithstanding Section 2.2(b) above, no patent + license is granted: (1) for any code that Contributor + has deleted from the Contributor Version; (2) for + infringements caused by: (i) third party + modifications of Contributor Version, or (ii) the + combination of Modifications made by that Contributor + with other software (except as part of the + Contributor Version) or other devices; or (3) under + Patent Claims infringed by Covered Software in the + absence of Modifications made by that Contributor. + . + 3. Distribution Obligations. + . + 3.1. Availability of Source Code. + Any Covered Software that You distribute or otherwise make + available in Executable form must also be made available in + Source Code form and that Source Code form must be + distributed only under the terms of this License. You must + include a copy of this License with every copy of the + Source Code form of the Covered Software You distribute or + otherwise make available. You must inform recipients of any + such Covered Software in Executable form as to how they can + obtain such Covered Software in Source Code form in a + reasonable manner on or through a medium customarily used + for software exchange. + 3.2. Modifications. + The Modifications that You create or to which You + contribute are governed by the terms of this License. You + represent that You believe Your Modifications are Your + original creation(s) and/or You have sufficient rights to + grant the rights conveyed by this License. + 3.3. Required Notices. + You must include a notice in each of Your Modifications + that identifies You as the Contributor of the Modification. + You may not remove or alter any copyright, patent or + trademark notices contained within the Covered Software, or + any notices of licensing or any descriptive text giving + attribution to any Contributor or the Initial Developer. + 3.4. Application of Additional Terms. + You may not offer or impose any terms on any Covered + Software in Source Code form that alters or restricts the + applicable version of this License or the recipients' + rights hereunder. You may choose to offer, and to charge a + fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Software. + However, you may do so only on Your own behalf, and not on + behalf of the Initial Developer or any Contributor. You + must make it absolutely clear that any such warranty, + support, indemnity or liability obligation is offered by + You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred + by the Initial Developer or such Contributor as a result of + warranty, support, indemnity or liability terms You offer. + 3.5. Distribution of Executable Versions. + You may distribute the Executable form of the Covered + Software under the terms of this License or under the terms + of a license of Your choice, which may contain terms + different from this License, provided that You are in + compliance with the terms of this License and that the + license for the Executable form does not attempt to limit + or alter the recipient's rights in the Source Code form + from the rights set forth in this License. If You + distribute the Covered Software in Executable form under a + different license, You must make it absolutely clear that + any terms which differ from this License are offered by You + alone, not by the Initial Developer or Contributor. You + hereby agree to indemnify the Initial Developer and every + Contributor for any liability incurred by the Initial + Developer or such Contributor as a result of any such terms + You offer. + 3.6. Larger Works. + You may create a Larger Work by combining Covered Software + with other code not governed by the terms of this License + and distribute the Larger Work as a single product. In such + a case, You must make sure the requirements of this License + are fulfilled for the Covered Software. + . + 4. Versions of the License. + . + 4.1. New Versions. + Sun Microsystems, Inc. is the initial license steward and + may publish revised and/or new versions of this License + from time to time. Each version will be given a + distinguishing version number. Except as provided in + Section 4.3, no one other than the license steward has the + right to modify this License. + 4.2. Effect of New Versions. + You may always continue to use, distribute or otherwise + make the Covered Software available under the terms of the + version of the License under which You originally received + the Covered Software. If the Initial Developer includes a + notice in the Original Software prohibiting it from being + distributed or otherwise made available under any + subsequent version of the License, You must distribute and + make the Covered Software available under the terms of the + version of the License under which You originally received + the Covered Software. Otherwise, You may also choose to + use, distribute or otherwise make the Covered Software + available under the terms of any subsequent version of the + License published by the license steward. + 4.3. Modified Versions. + When You are an Initial Developer and You want to create a + new license for Your Original Software, You may create and + use a modified version of this License if You: (a) rename + the license and remove any references to the name of the + license steward (except to note that the license differs + from this License); and (b) otherwise make it clear that + the license contains terms which differ from this License. + . + 5. DISCLAIMER OF WARRANTY. + . + COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" + BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED + SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR + PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND + PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY + COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE + INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF + ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF + WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS + DISCLAIMER. + . + 6. TERMINATION. + . + 6.1. This License and the rights granted hereunder will + terminate automatically if You fail to comply with terms + herein and fail to cure such breach within 30 days of + becoming aware of the breach. Provisions which, by their + nature, must remain in effect beyond the termination of + this License shall survive. + 6.2. If You assert a patent infringement claim (excluding + declaratory judgment actions) against Initial Developer or + a Contributor (the Initial Developer or Contributor against + whom You assert such claim is referred to as "Participant") + alleging that the Participant Software (meaning the + Contributor Version where the Participant is a Contributor + or the Original Software where the Participant is the + Initial Developer) directly or indirectly infringes any + patent, then any and all rights granted directly or + indirectly to You by such Participant, the Initial + Developer (if the Initial Developer is not the Participant) + and all Contributors under Sections 2.1 and/or 2.2 of this + License shall, upon 60 days notice from Participant + terminate prospectively and automatically at the expiration + of such 60 day notice period, unless if within such 60 day + period You withdraw Your claim with respect to the + Participant Software against such Participant either + unilaterally or pursuant to a written agreement with + Participant. + 6.3. In the event of termination under Sections 6.1 or 6.2 + above, all end user licenses that have been validly granted + by You or any distributor hereunder prior to termination + (excluding licenses granted to You by any distributor) + shall survive termination. + . + 7. LIMITATION OF LIABILITY. + . + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE + INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF + COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE + LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR + CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT + LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK + STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL + INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT + APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO + NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR + CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT + APPLY TO YOU. + . + 8. U.S. GOVERNMENT END USERS. + . + The Covered Software is a "commercial item," as that term is + defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial + computer software" (as that term is defined at 48 C.F.R. ¤ + 252.227-7014(a)(1)) and "commercial computer software + documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. + 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 + through 227.7202-4 (June 1995), all U.S. Government End Users + acquire Covered Software with only those rights set forth herein. + This U.S. Government Rights clause is in lieu of, and supersedes, + any other FAR, DFAR, or other clause or provision that addresses + Government rights in computer software under this License. + . + 9. MISCELLANEOUS. + . + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the + extent necessary to make it enforceable. This License shall be + governed by the law of the jurisdiction specified in a notice + contained within the Original Software (except to the extent + applicable law, if any, provides otherwise), excluding such + jurisdiction's conflict-of-law provisions. Any litigation + relating to this License shall be subject to the jurisdiction of + the courts located in the jurisdiction and venue specified in a + notice contained within the Original Software, with the losing + party responsible for costs, including, without limitation, court + costs and reasonable attorneys' fees and expenses. The + application of the United Nations Convention on Contracts for the + International Sale of Goods is expressly excluded. Any law or + regulation which provides that the language of a contract shall + be construed against the drafter shall not apply to this License. + You agree that You alone are responsible for compliance with the + United States export administration regulations (and the export + control laws and regulation of any other countries) when You use, + distribute or otherwise make available any Covered Software. + . + 10. RESPONSIBILITY FOR CLAIMS. + . + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or + indirectly, out of its utilization of rights under this License + and You agree to work with Initial Developer and Contributors to + distribute such responsibility on an equitable basis. Nothing + herein is intended or shall be deemed to constitute any admission + of liability. diff --git a/debian/default.template b/debian/default.template new file mode 100644 index 0000000..f75801b --- /dev/null +++ b/debian/default.template @@ -0,0 +1,23 @@ +# The home directory of the Java development kit (JDK). You need at least +# JDK version 8. If JAVA_HOME is not set, some common directories for +# OpenJDK and the Oracle JDK are tried. +#JAVA_HOME=/usr/lib/jvm/java-8-openjdk + +# You may pass JVM startup parameters to Java here. If you run Tomcat with +# Java 8 instead of 9 or newer, add "-XX:+UseG1GC" to select a suitable GC. +# If unset, the default options will be: -Djava.awt.headless=true +JAVA_OPTS="-Djava.awt.headless=true" + +# To enable remote debugging uncomment the following line. +# You will then be able to use a Java debugger on port 8000. +#JAVA_OPTS="${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n" + +# Java compiler to use for translating JavaServer Pages (JSPs). You can use all +# compilers that are accepted by Ant's build.compiler property. +#JSP_COMPILER=javac + +# Enable the Java security manager? (true/false, default: false) +#SECURITY_MANAGER=true + +# Whether to compress logfiles older than today's +#LOGFILE_COMPRESS=1 diff --git a/debian/default_root/META-INF/context.xml b/debian/default_root/META-INF/context.xml new file mode 100644 index 0000000..00e2ff6 --- /dev/null +++ b/debian/default_root/META-INF/context.xml @@ -0,0 +1,2 @@ + diff --git a/debian/default_root/index.html b/debian/default_root/index.html new file mode 100644 index 0000000..1f3d3fe --- /dev/null +++ b/debian/default_root/index.html @@ -0,0 +1,29 @@ + + + + + Apache Tomcat + + + +

It works !

+ +

If you're seeing this page via a web browser, it means you've setup Tomcat successfully. Congratulations!

+ +

This is the default Tomcat home page. It can be found on the local filesystem at: /var/lib/tomcat10/webapps/ROOT/index.html

+ +

Tomcat veterans might be pleased to learn that this system instance of Tomcat is installed with CATALINA_HOME in /usr/share/tomcat10 and CATALINA_BASE in /var/lib/tomcat10, following the rules from /usr/share/doc/tomcat10-common/RUNNING.txt.gz.

+ +

You might consider installing the following packages, if you haven't already done so:

+ +

tomcat10-docs: This package installs a web application that allows to browse the Tomcat 10 documentation locally. Once installed, you can access it by clicking here.

+ +

tomcat10-examples: This package installs a web application that allows to access the Tomcat 10 Servlet and JSP examples. Once installed, you can access it by clicking here.

+ +

tomcat10-admin: This package installs two web applications that can help managing this Tomcat instance. Once installed, you can access the manager webapp and the host-manager webapp.

+ +

NOTE: For security reasons, using the manager webapp is restricted to users with role "manager-gui". The host-manager webapp is restricted to users with role "admin-gui". Users are defined in /etc/tomcat10/tomcat-users.xml.

+ + + diff --git a/debian/libexec/tomcat-locate-java.sh b/debian/libexec/tomcat-locate-java.sh new file mode 100644 index 0000000..2603a15 --- /dev/null +++ b/debian/libexec/tomcat-locate-java.sh @@ -0,0 +1,50 @@ +# +# Script looking for a Java runtime suitable for running Tomcat +# +# The script looks for the default JRE/JDK, OpenJDK and Oracle JDK +# as packaged by java-package. The Java runtime found is exported +# in the JAVA_HOME environment variable. +# + +set -e + +# Find the Java runtime if JAVA_HOME isn't already defined +if [ -z "$JAVA_HOME" ]; then + # This function sets the variable JDK_DIRS + find_jdks() + { + for java_version in 21 20 19 17 11 8 + do + for jvmdir in /usr/lib/jvm/java-${java_version}-openjdk-* \ + /usr/lib/jvm/jdk-${java_version}-oracle-* \ + /usr/lib/jvm/jre-${java_version}-oracle-* \ + /usr/lib/jvm/java-${java_version}-oracle \ + /usr/lib/jvm/oracle-java${java_version}-jdk-* \ + /usr/lib/jvm/oracle-java${java_version}-jre-* + do + if [ -d "${jvmdir}" ] + then + JDK_DIRS="${JDK_DIRS} ${jvmdir}" + fi + done + done + } + + # The first existing directory is used for JAVA_HOME + JDK_DIRS="/usr/lib/jvm/default-java" + find_jdks + + # Look for the right JVM to use + for jdir in $JDK_DIRS; do + if [ -r "$jdir/bin/java" -a -z "${JAVA_HOME}" ]; then + JAVA_HOME="$jdir" + fi + done +fi + +if [ -z "$JAVA_HOME" ]; then + echo "<2>No JDK or JRE found - Please set the JAVA_HOME variable or install the default-jdk package" + exit 1 +fi + +export JAVA_HOME diff --git a/debian/libexec/tomcat-start.sh b/debian/libexec/tomcat-start.sh new file mode 100755 index 0000000..b5de208 --- /dev/null +++ b/debian/libexec/tomcat-start.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# +# Startup script for Apache Tomcat with systemd +# + +set -e + +# Load the service settings +. /etc/default/tomcat10 + +# Find the Java runtime and set JAVA_HOME +. /usr/libexec/tomcat10/tomcat-locate-java.sh + +# Set the JSP compiler if configured in the /etc/default/tomcat10 file +[ -n "$JSP_COMPILER" ] && JAVA_OPTS="$JAVA_OPTS -Dbuild.compiler=\"$JSP_COMPILER\"" + +export JAVA_OPTS + +# Enable the Java security manager? +SECURITY="" +[ "$SECURITY_MANAGER" = "true" ] && SECURITY="-security" + + +# Start Tomcat +cd $CATALINA_BASE && exec $CATALINA_HOME/bin/catalina.sh run $SECURITY diff --git a/debian/libexec/tomcat-update-policy.sh b/debian/libexec/tomcat-update-policy.sh new file mode 100755 index 0000000..a91e879 --- /dev/null +++ b/debian/libexec/tomcat-update-policy.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Script regenerating the catalina.policy file from the collection +# of files in /etc/tomcat10/policy.d/ +# +# This script is run as root by systemd before starting Tomcat. +# + +set -e + +if [ ! -d "$CATALINA_BASE/conf" ]; then + echo "<2>Invalid CATALINA_BASE, configuration files not found: $CATALINA_BASE" + exit 1 +fi + +# Regenerate the catalina.policy file +POLICY_CACHE="$CATALINA_BASE/policy/catalina.policy" +umask 022 +rm -rf "$CATALINA_BASE/policy" +mkdir "$CATALINA_BASE/policy" +echo "// AUTO-GENERATED FILE from /etc/tomcat10/policy.d/" > "$POLICY_CACHE" +echo "" >> "$POLICY_CACHE" +cat $CATALINA_BASE/conf/policy.d/*.policy >> "$POLICY_CACHE" diff --git a/debian/libtomcat10-embed-java.manifest b/debian/libtomcat10-embed-java.manifest new file mode 100644 index 0000000..98ecf91 --- /dev/null +++ b/debian/libtomcat10-embed-java.manifest @@ -0,0 +1,2 @@ +usr/share/java/tomcat10-embed-jasper.jar: + Class-Path: eclipse-jdt-core.jar diff --git a/debian/libtomcat10-embed-java.poms b/debian/libtomcat10-embed-java.poms new file mode 100644 index 0000000..fbc1ddd --- /dev/null +++ b/debian/libtomcat10-embed-java.poms @@ -0,0 +1,4 @@ +debian/poms/tomcat-embed-core.pom --java-lib --usj-name=tomcat10-embed-core --artifact=output/embed/tomcat-embed-core.jar +debian/poms/tomcat-embed-el.pom --java-lib --usj-name=tomcat10-embed-el --artifact=output/embed/tomcat-embed-el.jar +debian/poms/tomcat-embed-jasper.pom --java-lib --usj-name=tomcat10-embed-jasper --artifact=output/embed/tomcat-embed-jasper.jar +debian/poms/tomcat-embed-websocket.pom --java-lib --usj-name=tomcat10-embed-websocket --artifact=output/embed/tomcat-embed-websocket.jar diff --git a/debian/libtomcat10-java.lintian-overrides b/debian/libtomcat10-java.lintian-overrides new file mode 100644 index 0000000..9aabd22 --- /dev/null +++ b/debian/libtomcat10-java.lintian-overrides @@ -0,0 +1,2 @@ +# The i18n jars contain only properties files +libtomcat10-java: codeless-jar usr/share/java/tomcat10-i18n* diff --git a/debian/libtomcat10-java.manifest b/debian/libtomcat10-java.manifest new file mode 100644 index 0000000..427eefa --- /dev/null +++ b/debian/libtomcat10-java.manifest @@ -0,0 +1,2 @@ +usr/share/java/tomcat10-jasper.jar: + Class-Path: eclipse-jdt-core.jar tomcat10-el-api.jar tomcat10-servlet-api.jar tomcat10-jsp-api.jar diff --git a/debian/libtomcat10-java.poms b/debian/libtomcat10-java.poms new file mode 100644 index 0000000..576e44e --- /dev/null +++ b/debian/libtomcat10-java.poms @@ -0,0 +1,32 @@ +debian/poms/tomcat-annotations-api.pom --java-lib --usj-name=tomcat10-annotations-api --artifact=output/build/lib/annotations-api.jar +debian/poms/tomcat-api.pom --java-lib --usj-name=tomcat10-api --artifact=output/build/lib/tomcat-api.jar +debian/poms/tomcat-catalina.pom --java-lib --usj-name=tomcat10-catalina --artifact=output/build/lib/catalina.jar +debian/poms/tomcat-catalina-ant.pom --java-lib --usj-name=tomcat10-catalina-ant --artifact=output/build/lib/catalina-ant.jar +debian/poms/tomcat-catalina-ha.pom --java-lib --usj-name=tomcat10-catalina-ha --artifact=output/build/lib/catalina-ha.jar +debian/poms/tomcat-coyote.pom --java-lib --usj-name=tomcat10-coyote --artifact=output/build/lib/tomcat-coyote.jar +debian/poms/tomcat-dbcp.pom --java-lib --usj-name=tomcat10-dbcp --artifact=output/build/lib/tomcat-dbcp.jar +debian/poms/tomcat-el-api.pom --java-lib --usj-name=tomcat10-el-api --artifact=output/build/lib/el-api.jar +debian/poms/tomcat-i18n-cs.pom --java-lib --usj-name=tomcat10-i18n-cs --artifact=output/build/lib/tomcat-i18n-cs.jar +debian/poms/tomcat-i18n-de.pom --java-lib --usj-name=tomcat10-i18n-de --artifact=output/build/lib/tomcat-i18n-de.jar +debian/poms/tomcat-i18n-es.pom --java-lib --usj-name=tomcat10-i18n-es --artifact=output/build/lib/tomcat-i18n-es.jar +debian/poms/tomcat-i18n-fr.pom --java-lib --usj-name=tomcat10-i18n-fr --artifact=output/build/lib/tomcat-i18n-fr.jar +debian/poms/tomcat-i18n-ja.pom --java-lib --usj-name=tomcat10-i18n-ja --artifact=output/build/lib/tomcat-i18n-ja.jar +debian/poms/tomcat-i18n-ko.pom --java-lib --usj-name=tomcat10-i18n-ko --artifact=output/build/lib/tomcat-i18n-ko.jar +debian/poms/tomcat-i18n-pt-BR.pom --java-lib --usj-name=tomcat10-i18n-pt-BR --artifact=output/build/lib/tomcat-i18n-pt-BR.jar +debian/poms/tomcat-i18n-ru.pom --java-lib --usj-name=tomcat10-i18n-ru --artifact=output/build/lib/tomcat-i18n-ru.jar +debian/poms/tomcat-i18n-zh-CN.pom --java-lib --usj-name=tomcat10-i18n-zh-CN --artifact=output/build/lib/tomcat-i18n-zh-CN.jar +debian/poms/tomcat-jasper.pom --java-lib --usj-name=tomcat10-jasper --artifact=output/build/lib/jasper.jar +debian/poms/tomcat-jasper-el.pom --java-lib --usj-name=tomcat10-jasper-el --artifact=output/build/lib/jasper-el.jar +debian/poms/tomcat-jaspic-api.pom --java-lib --usj-name=tomcat10-jaspic-api --artifact=output/build/lib/jaspic-api.jar +debian/poms/tomcat-jdbc.pom --java-lib --usj-name=tomcat10-jdbc --artifact=output/jdbc-pool/tomcat-jdbc.jar +debian/poms/tomcat-jni.pom --java-lib --usj-name=tomcat10-jni --artifact=output/build/lib/tomcat-jni.jar +debian/poms/tomcat-jsp-api.pom --java-lib --usj-name=tomcat10-jsp-api --artifact=output/build/lib/jsp-api.jar +debian/poms/tomcat-juli.pom --java-lib --usj-name=tomcat10-juli --artifact=output/build/bin/tomcat-juli.jar +debian/poms/tomcat-servlet-api.pom --java-lib --usj-name=tomcat10-servlet-api --artifact=output/build/lib/servlet-api.jar +debian/poms/tomcat-storeconfig.pom --java-lib --usj-name=tomcat10-storeconfig --artifact=output/build/lib/catalina-storeconfig.jar +debian/poms/tomcat-tribes.pom --java-lib --usj-name=tomcat10-tribes --artifact=output/build/lib/catalina-tribes.jar +debian/poms/tomcat-util.pom --java-lib --usj-name=tomcat10-util --artifact=output/build/lib/tomcat-util.jar +debian/poms/tomcat-util-scan.pom --java-lib --usj-name=tomcat10-util-scan --artifact=output/build/lib/tomcat-util-scan.jar +debian/poms/tomcat-websocket.pom --java-lib --usj-name=tomcat10-websocket --artifact=output/build/lib/tomcat-websocket.jar +debian/poms/tomcat-websocket-api.pom --java-lib --usj-name=tomcat10-websocket-api --artifact=output/build/lib/websocket-api.jar +debian/poms/tomcat-websocket-client-api.pom --java-lib --usj-name=tomcat10-websocket-client-api --artifact=output/build/lib/websocket-client-api.jar diff --git a/debian/logging.properties b/debian/logging.properties new file mode 100644 index 0000000..37fa30d --- /dev/null +++ b/debian/logging.properties @@ -0,0 +1,58 @@ +# 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. + +handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler + +.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +1catalina.org.apache.juli.AsyncFileHandler.level = FINE +1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina. +1catalina.org.apache.juli.AsyncFileHandler.maxDays = 90 + +2localhost.org.apache.juli.AsyncFileHandler.level = FINE +2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost. +2localhost.org.apache.juli.AsyncFileHandler.maxDays = 90 + +java.util.logging.ConsoleHandler.level = FINE +java.util.logging.ConsoleHandler.formatter = org.apache.juli.SystemdFormatter + + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler + +# For example, set the org.apache.catalina.util.LifecycleBase logger to log +# each component that extends LifecycleBase changing state: +#org.apache.catalina.util.LifecycleBase.level = FINE + +# To see debug messages in TldLocationsCache, uncomment the following line: +#org.apache.jasper.compiler.TldLocationsCache.level = FINE + +# To see debug messages for HTTP/2 handling, uncomment the following line: +#org.apache.coyote.http2.level = FINE + +# To see debug messages for WebSocket handling, uncomment the following line: +#org.apache.tomcat.websocket.level = FINE diff --git a/debian/logrotate.template b/debian/logrotate.template new file mode 100644 index 0000000..4d0848a --- /dev/null +++ b/debian/logrotate.template @@ -0,0 +1,10 @@ +/var/log/tomcat10/catalina.out { + copytruncate + weekly + rotate 52 + compress + delaycompress + missingok + create 640 tomcat adm + su tomcat adm +} diff --git a/debian/maven.rules b/debian/maven.rules new file mode 100644 index 0000000..843c570 --- /dev/null +++ b/debian/maven.rules @@ -0,0 +1,36 @@ +# Publish 10.x instead of 'debian' artifacts to avoid conflicts with other versions of Tomcat + +org.apache.tomcat tomcat * s/.*/10.x/ +org.apache.tomcat tomcat-annotations-api * s/.*/10.x/ +org.apache.tomcat tomcat-api * s/.*/10.x/ +org.apache.tomcat tomcat-catalina * s/.*/10.x/ +org.apache.tomcat tomcat-catalina-ant * s/.*/10.x/ +org.apache.tomcat tomcat-catalina-ha * s/.*/10.x/ +org.apache.tomcat tomcat-catalina-jmx-remote * s/.*/10.x/ +org.apache.tomcat tomcat-catalina-ws * s/.*/10.x/ +org.apache.tomcat tomcat-coyote * s/.*/10.x/ +org.apache.tomcat tomcat-dbcp * s/.*/10.x/ +org.apache.tomcat tomcat-el-api * s/.*/10.x/ +org.apache.tomcat tomcat-i18n-* * s/.*/10.x/ +org.apache.tomcat tomcat-jasper * s/.*/10.x/ +org.apache.tomcat tomcat-jasper-el * s/.*/10.x/ +org.apache.tomcat tomcat-jaspic-api * s/.*/10.x/ +org.apache.tomcat tomcat-jdbc * s/.*/10.x/ +org.apache.tomcat tomcat-jni * s/.*/10.x/ +org.apache.tomcat tomcat-jsp-api * s/.*/10.x/ +org.apache.tomcat tomcat-juli * s/.*/10.x/ +org.apache.tomcat tomcat-servlet-api * s/.*/10.x/ +org.apache.tomcat tomcat-storeconfig * s/.*/10.x/ +org.apache.tomcat tomcat-tribes * s/.*/10.x/ +org.apache.tomcat tomcat-util * s/.*/10.x/ +org.apache.tomcat tomcat-util-scan * s/.*/10.x/ +org.apache.tomcat tomcat-websocket * s/.*/10.x/ +org.apache.tomcat tomcat-websocket-api * s/.*/10.x/ +org.apache.tomcat tomcat-websocket-client-api * s/.*/10.x/ + +org.eclipse.jdt s/ecj/org.eclipse.jdt.core/ * s/.*/debian/ + +org.apache.tomcat.embed tomcat-embed-core * s/.*/10.x/ +org.apache.tomcat.embed tomcat-embed-el * s/.*/10.x/ +org.apache.tomcat.embed tomcat-embed-jasper * s/.*/10.x/ +org.apache.tomcat.embed tomcat-embed-websocket * s/.*/10.x/ diff --git a/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch b/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch new file mode 100644 index 0000000..c5edc19 --- /dev/null +++ b/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch @@ -0,0 +1,30 @@ +From: Debian Java Maintainers +Date: Mon, 28 Jun 2010 21:32:35 +0200 +Subject: [PATCH] split deploy-webapps target from deploy target + +--- + build.xml | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +--- a/build.xml ++++ b/build.xml +@@ -1494,7 +1494,7 @@ + + + +- + + +@@ -1528,6 +1528,10 @@ + + + ++ ++ ++ ++ + + + diff --git a/debian/patches/0005-skip-test-failures.patch b/debian/patches/0005-skip-test-failures.patch new file mode 100644 index 0000000..6ee43f2 --- /dev/null +++ b/debian/patches/0005-skip-test-failures.patch @@ -0,0 +1,22 @@ +From: Emmanuel Bourg +Date: Mon, 16 Jan 2023 23:22:18 +0100 +Subject: Ignore the failing tests + +Forwarded: not-needed +--- + build.xml | 2 ++ + 1 file changed, 2 insertions(+) + +--- a/build.xml ++++ b/build.xml +@@ -1970,8 +1970,10 @@ + + + ++ + + + +Date: Mon, 28 Jun 2010 21:53:50 +0200 +Subject: [PATCH] Use java.security.policy file in catalina.sh Make sure + catalina.sh uses the Debian/Ubuntu java.security.policy file location when + Tomcat is started with a security manager. + +Bug-Ubuntu: https://bugs.launchpad.net/bugs/591802 +Bug-Debian: http://bugs.debian.org/585379 +Forwarded: not-needed +--- + bin/catalina.sh | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +--- a/bin/catalina.sh ++++ b/bin/catalina.sh +@@ -348,7 +348,7 @@ if [ "$1" = "debug" ] ; then + -classpath "$CLASSPATH" \ + -sourcepath "$CATALINA_HOME"/../../java \ + -Djava.security.manager \ +- -Djava.security.policy=="$CATALINA_BASE"/conf/catalina.policy \ ++ -Djava.security.policy=="$CATALINA_BASE"/policy/catalina.policy \ + -Dcatalina.base="$CATALINA_BASE" \ + -Dcatalina.home="$CATALINA_HOME" \ + -Djava.io.tmpdir="$CATALINA_TMPDIR" \ +@@ -375,7 +375,7 @@ elif [ "$1" = "run" ]; then + eval exec "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Djava.security.manager \ +- -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \ ++ -Djava.security.policy=="\"$CATALINA_BASE/policy/catalina.policy\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ +@@ -454,7 +454,7 @@ elif [ "$1" = "start" ] ; then + eval $_NOHUP "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \ + -classpath "\"$CLASSPATH\"" \ + -Djava.security.manager \ +- -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \ ++ -Djava.security.policy=="\"$CATALINA_BASE/policy/catalina.policy\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ diff --git a/debian/patches/0010-debianize-build-xml.patch b/debian/patches/0010-debianize-build-xml.patch new file mode 100644 index 0000000..1b786bc --- /dev/null +++ b/debian/patches/0010-debianize-build-xml.patch @@ -0,0 +1,33 @@ +From: James Pages +Date: Mon, 27 Sep 2021 16:45:13 +0200 +Subject: Disable usage of embedded library copies + +Forwarded: no +Last-Update: 2011-05-16 +--- + build.xml | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +--- a/build.xml ++++ b/build.xml +@@ -1004,7 +1004,7 @@ + + + +- ++ + + +- ++ + + + diff --git a/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch b/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch new file mode 100644 index 0000000..05afc04 --- /dev/null +++ b/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch @@ -0,0 +1,22 @@ +From: Jakub Adam +Date: Mon, 16 Jan 2023 23:22:18 +0100 +Subject: Don't look for build.properties in the user home directory. + +Forwarded: not-needed + +This directory doesn't exist on the builders and the attempt to load +a property file there causes a build failure. +--- + build.xml | 1 - + 1 file changed, 1 deletion(-) + +--- a/build.xml ++++ b/build.xml +@@ -847,7 +847,6 @@ + + +- + + + diff --git a/debian/patches/0018-fix-manager-webapp.patch b/debian/patches/0018-fix-manager-webapp.patch new file mode 100644 index 0000000..9df8601 --- /dev/null +++ b/debian/patches/0018-fix-manager-webapp.patch @@ -0,0 +1,77 @@ +From: "ubuntu@iam.tj" +Date: Mon, 16 Jan 2023 23:22:18 +0100 +Subject: This patch changes the manager path from webapps/manager to + +Bug-Ubuntu: https://bugs.launchpad.net/bugs/1128067 +Reviewed-By: Gianfranco Costamagna +Last-Update: 2013-08-01 + +../tomcat10-admin/manager +--- + conf/catalina.policy | 10 +++++----- + webapps/docs/manager-howto.xml | 2 +- + webapps/host-manager/WEB-INF/manager.xml | 2 +- + 3 files changed, 7 insertions(+), 7 deletions(-) + +--- a/conf/catalina.policy ++++ b/conf/catalina.policy +@@ -188,7 +188,7 @@ grant { + // - default CATALINA_HOME == CATALINA_BASE + // - CATALINA_HOME != CATALINA_BASE, per instance Manager in CATALINA_BASE + // - CATALINA_HOME != CATALINA_BASE, shared Manager in CATALINA_HOME +-grant codeBase "file:${catalina.base}/webapps/manager/-" { ++grant codeBase "file:${catalina.base}/../tomcat10-admin/manager/-" { + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.ha.session"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager"; +@@ -196,7 +196,7 @@ grant codeBase "file:${catalina.base}/we + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.util"; + permission org.apache.catalina.security.DeployXmlPermission "manager"; + }; +-grant codeBase "file:${catalina.home}/webapps/manager/-" { ++grant codeBase "file:${catalina.home}/../tomcat10-admin/manager/-" { + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.ha.session"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager"; +@@ -211,10 +211,10 @@ grant codeBase "file:${catalina.home}/we + // - default CATALINA_HOME == CATALINA_BASE + // - CATALINA_HOME != CATALINA_BASE, per instance Host Manager in CATALINA_BASE + // - CATALINA_HOME != CATALINA_BASE, shared Host Manager in CATALINA_HOME +-grant codeBase "file:${catalina.base}/webapps/host-manager/-" { ++grant codeBase "file:${catalina.base}/../tomcat10-admin/host-manager/-" { + permission org.apache.catalina.security.DeployXmlPermission "host-manager"; + }; +-grant codeBase "file:${catalina.home}/webapps/host-manager/-" { ++grant codeBase "file:${catalina.home}/../tomcat10-admin/host-manager/-" { + permission org.apache.catalina.security.DeployXmlPermission "host-manager"; + }; + +@@ -260,4 +260,4 @@ grant codeBase "file:${catalina.home}/we + // + // The permissions granted to a specific JAR + // grant codeBase "war:file:${catalina.base}/webapps/examples.war*/WEB-INF/lib/foo.jar" { +-// }; +\ No newline at end of file ++// }; +--- a/webapps/docs/manager-howto.xml ++++ b/webapps/docs/manager-howto.xml +@@ -74,7 +74,7 @@ configuration file in the + $CATALINA_BASE/conf/[enginename]/[hostname] folder. Here is an + example:

+ ++ docBase="${catalina.home}/../tomcat10-admin/manager"> + + +- + diff --git a/debian/patches/0019-add-distribution-to-error-page.patch b/debian/patches/0019-add-distribution-to-error-page.patch new file mode 100644 index 0000000..aa2f7cd --- /dev/null +++ b/debian/patches/0019-add-distribution-to-error-page.patch @@ -0,0 +1,34 @@ +From: Yolanda Robla +Date: Mon, 16 Jan 2023 23:22:18 +0100 +Subject: Adds the name of the distribution to the version of Tomcat + +Forwarded: not-needed +Bug-Debian: http://bugs.debian.org/729840 + +reported on the error pages (i.e. 'Apache Tomcat/8.0.x (Debian)') +--- + build.xml | 1 + + java/org/apache/catalina/util/ServerInfo.properties | 2 +- + 2 files changed, 2 insertions(+), 1 deletion(-) + +--- a/build.xml ++++ b/build.xml +@@ -284,6 +284,7 @@ + + + ++ + + + +--- a/java/org/apache/catalina/util/ServerInfo.properties ++++ b/java/org/apache/catalina/util/ServerInfo.properties +@@ -13,7 +13,7 @@ + # See the License for the specific language governing permissions and + # limitations under the License. + +-server.info=Apache Tomcat/@VERSION@ ++server.info=Apache Tomcat/@VERSION@ (@TOMCAT_DISTRIBUTION@) + server.number=@VERSION_NUMBER@ + server.built=@VERSION_BUILT@ + server.built.iso=@VERSION_BUILT_ISO@ diff --git a/debian/patches/0021-dont-test-unsupported-ciphers.patch b/debian/patches/0021-dont-test-unsupported-ciphers.patch new file mode 100644 index 0000000..bc184ae --- /dev/null +++ b/debian/patches/0021-dont-test-unsupported-ciphers.patch @@ -0,0 +1,63 @@ +From: Markus Koschany +Date: Fri, 24 Sep 2021 14:52:24 +0200 +Subject: 0021-dont-test-unsupported-ciphers + +Don't check the IDEA cipher during the tests since it is disabled in Debian (see #327739) +Also ignore ARIA which is disabled by default in OpenSSL. + +Forwarded: not-needed +--- + .../tomcat/util/net/openssl/ciphers/TestCipher.java | 2 +- + .../ciphers/TestOpenSSLCipherConfigurationParser.java | 2 +- + .../tomcat/util/net/openssl/ciphers/TesterOpenSSL.java | 18 ++++++++++++++++++ + 3 files changed, 20 insertions(+), 2 deletions(-) + +--- a/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java ++++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java +@@ -76,7 +76,7 @@ public class TestCipher { + // OpenSSL does not include ECDH/ECDHE ciphers in all and there is no + // EC alias. Use aRSA. + // OpenSSL 1.0.0 onwards does not include eNULL in all. +- Set availableCipherSuites = TesterOpenSSL.getOpenSSLCiphersAsSet("ALL:eNULL:aRSA"); ++ Set availableCipherSuites = TesterOpenSSL.getOpenSSLCiphersAsSet("ALL:eNULL:aRSA:!ARIA"); + + Set expectedCipherSuites = new HashSet<>(); + for (Cipher cipher : Cipher.values()) { +--- a/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java ++++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java +@@ -573,7 +573,7 @@ public class TestOpenSSLCipherConfigurat + + private void testSpecification(String specification) throws Exception { + // Filter out cipher suites that OpenSSL does not implement +- String openSSLCipherList = TesterOpenSSL.getOpenSSLCiphersAsExpression(specification); ++ String openSSLCipherList = TesterOpenSSL.getOpenSSLCiphersAsExpression(specification + ":!ARIA"); + List jsseCipherListFromOpenSSL = + OpenSSLCipherConfigurationParser.parseExpression(openSSLCipherList); + List jsseCipherListFromParser = +--- a/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java ++++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java +@@ -105,6 +105,24 @@ public class TesterOpenSSL { + unimplemented.add(Cipher.SSL2_RC4_128_EXPORT40_WITH_MD5); + unimplemented.add(Cipher.SSL2_IDEA_128_CBC_WITH_MD5); + unimplemented.add(Cipher.SSL2_DES_192_EDE3_CBC_WITH_MD5); ++ unimplemented.add(Cipher.TLS_RSA_WITH_IDEA_CBC_SHA); ++ unimplemented.add(Cipher.TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256); ++ unimplemented.add(Cipher.TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384); ++ unimplemented.add(Cipher.TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256); ++ unimplemented.add(Cipher.TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384); ++ unimplemented.add(Cipher.TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256); ++ unimplemented.add(Cipher.TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384); ++ unimplemented.add(Cipher.TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256); ++ unimplemented.add(Cipher.TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384); ++ unimplemented.add(Cipher.TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256); ++ unimplemented.add(Cipher.TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384); ++ unimplemented.add(Cipher.TLS_PSK_WITH_ARIA_128_GCM_SHA256); ++ unimplemented.add(Cipher.TLS_PSK_WITH_ARIA_256_GCM_SHA384); ++ unimplemented.add(Cipher.TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256); ++ unimplemented.add(Cipher.TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384); ++ unimplemented.add(Cipher.TLS_RSA_WITH_ARIA_128_GCM_SHA256); ++ unimplemented.add(Cipher.TLS_RSA_WITH_ARIA_256_GCM_SHA384); ++ + // These were removed in 1.1.0 so won't be available from that + // version onwards. + unimplemented.add(Cipher.TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA); diff --git a/debian/patches/0023-disable-shutdown-by-socket.patch b/debian/patches/0023-disable-shutdown-by-socket.patch new file mode 100644 index 0000000..00179cb --- /dev/null +++ b/debian/patches/0023-disable-shutdown-by-socket.patch @@ -0,0 +1,20 @@ +From: Emmanuel Bourg +Date: Mon, 16 Jan 2023 23:22:18 +0100 +Subject: Disables the shutdown port (8005) by default + +Forwarded: no +--- + conf/server.xml | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/conf/server.xml ++++ b/conf/server.xml +@@ -19,7 +19,7 @@ + define subcomponents such as "Valves" at this level. + Documentation at /docs/config/server.html + --> +- ++ + + + + ++ +
+ + diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000..51b628b --- /dev/null +++ b/debian/patches/series @@ -0,0 +1,14 @@ +0004-split-deploy-webapps-target-from-deploy-target.patch +0005-skip-test-failures.patch +0009-Use-java.security.policy-file-in-catalina.sh.patch +0010-debianize-build-xml.patch +0013-dont-look-for-build-properties-in-user-home.patch +0018-fix-manager-webapp.patch +0019-add-distribution-to-error-page.patch +0023-disable-shutdown-by-socket.patch +0024-systemd-log-formatter.patch +0025-invalid-configuration-exit-status.patch +0026-easymock4-compatibility.patch +0021-dont-test-unsupported-ciphers.patch +exclude-TestJNDIRealmIntegration.patch +disable-jacoco.patch diff --git a/debian/policy/01system.policy b/debian/policy/01system.policy new file mode 100644 index 0000000..499fbd9 --- /dev/null +++ b/debian/policy/01system.policy @@ -0,0 +1,51 @@ +// 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. + +// ============================================================================ +// catalina.policy - Security Policy Permissions for Tomcat +// +// This file contains a default set of security policies to be enforced (by the +// JVM) when Catalina is executed with the "-security" option. In addition +// to the permissions granted here, the following additional permissions are +// granted to each web application: +// +// * Read access to the web application's document root directory +// * Read, write and delete access to the web application's working directory +// ============================================================================ + + +// ========== SYSTEM CODE PERMISSIONS ========================================= + + +// These permissions apply to javac +grant codeBase "file:${java.home}/lib/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to all shared system extensions +grant codeBase "file:${java.home}/jre/lib/ext/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to javac when ${java.home] points at $JAVA_HOME/jre +grant codeBase "file:${java.home}/../lib/-" { + permission java.security.AllPermission; +}; + +// These permissions apply to all shared system extensions when +// ${java.home} points at $JAVA_HOME/jre +grant codeBase "file:${java.home}/lib/ext/-" { + permission java.security.AllPermission; +}; diff --git a/debian/policy/02debian.policy b/debian/policy/02debian.policy new file mode 100644 index 0000000..582c47c --- /dev/null +++ b/debian/policy/02debian.policy @@ -0,0 +1,10 @@ +// These permissions apply to all JARs from Debian packages +grant codeBase "file:/usr/share/java/-" { + permission java.security.AllPermission; +}; +grant codeBase "file:/usr/share/maven-repo/-" { + permission java.security.AllPermission; +}; +grant codeBase "file:/usr/share/ant/lib/-" { + permission java.security.AllPermission; +}; diff --git a/debian/policy/03catalina.policy b/debian/policy/03catalina.policy new file mode 100644 index 0000000..9b1c028 --- /dev/null +++ b/debian/policy/03catalina.policy @@ -0,0 +1,67 @@ +// ========== CATALINA CODE PERMISSIONS ======================================= + + +// These permissions apply to the daemon code +grant codeBase "file:${catalina.home}/bin/commons-daemon.jar" { + permission java.security.AllPermission; +}; + +// These permissions apply to the logging API +// Note: If tomcat-juli.jar is in ${catalina.base} and not in ${catalina.home}, +// update this section accordingly. +// grant codeBase "file:${catalina.base}/bin/tomcat-juli.jar" {..} +grant codeBase "file:${catalina.home}/bin/tomcat-juli.jar" { + permission java.io.FilePermission + "${java.home}${file.separator}lib${file.separator}logging.properties", "read"; + + permission java.io.FilePermission + "${catalina.base}${file.separator}conf${file.separator}logging.properties", "read"; + permission java.io.FilePermission + "${catalina.base}${file.separator}logs", "read, write"; + permission java.io.FilePermission + "${catalina.base}${file.separator}logs${file.separator}*", "read, write, delete"; + + permission java.lang.RuntimePermission "shutdownHooks"; + permission java.lang.RuntimePermission "getClassLoader"; + permission java.lang.RuntimePermission "setContextClassLoader"; + + permission java.lang.management.ManagementPermission "monitor"; + + permission java.util.logging.LoggingPermission "control"; + + permission java.util.PropertyPermission "java.util.logging.config.class", "read"; + permission java.util.PropertyPermission "java.util.logging.config.file", "read"; + permission java.util.PropertyPermission "org.apache.juli.AsyncLoggerPollInterval", "read"; + permission java.util.PropertyPermission "org.apache.juli.AsyncMaxRecordCount", "read"; + permission java.util.PropertyPermission "org.apache.juli.AsyncOverflowDropType", "read"; + permission java.util.PropertyPermission "org.apache.juli.ClassLoaderLogManager.debug", "read"; + permission java.util.PropertyPermission "catalina.base", "read"; + + // Note: To enable per context logging configuration, permit read access to + // the appropriate file. Be sure that the logging configuration is + // secure before enabling such access. + // E.g. for the examples web application (uncomment and unwrap + // the following to be on a single line): + // permission java.io.FilePermission "${catalina.base}${file.separator} + // webapps${file.separator}examples${file.separator}WEB-INF + // ${file.separator}classes${file.separator}logging.properties", "read"; +}; + +// These permissions apply to the server startup code +grant codeBase "file:${catalina.home}/bin/bootstrap.jar" { + permission java.security.AllPermission; +}; + +// These permissions apply to the servlet API classes +// and those that are shared across all class loaders +// located in the "lib" directory +grant codeBase "file:${catalina.home}/lib/-" { + permission java.security.AllPermission; +}; + + +// If using a per instance lib directory, i.e. ${catalina.base}/lib, +// then the following permission will need to be uncommented +// grant codeBase "file:${catalina.base}/lib/-" { +// permission java.security.AllPermission; +// }; diff --git a/debian/policy/04webapps.policy b/debian/policy/04webapps.policy new file mode 100644 index 0000000..39335f0 --- /dev/null +++ b/debian/policy/04webapps.policy @@ -0,0 +1,94 @@ +// ========== WEB APPLICATION PERMISSIONS ===================================== + + +// These permissions are granted by default to all web applications +// In addition, a web application will be given a read FilePermission +// for all files and directories in its document root. +grant { + // Required for JNDI lookup of named JDBC DataSource's and + // javamail named MimePart DataSource used to send mail + permission java.util.PropertyPermission "java.home", "read"; + permission java.util.PropertyPermission "java.naming.*", "read"; + permission java.util.PropertyPermission "javax.sql.*", "read"; + + // OS Specific properties to allow read access + permission java.util.PropertyPermission "os.name", "read"; + permission java.util.PropertyPermission "os.version", "read"; + permission java.util.PropertyPermission "os.arch", "read"; + permission java.util.PropertyPermission "file.separator", "read"; + permission java.util.PropertyPermission "path.separator", "read"; + permission java.util.PropertyPermission "line.separator", "read"; + + // JVM properties to allow read access + permission java.util.PropertyPermission "java.version", "read"; + permission java.util.PropertyPermission "java.vendor", "read"; + permission java.util.PropertyPermission "java.vendor.url", "read"; + permission java.util.PropertyPermission "java.class.version", "read"; + permission java.util.PropertyPermission "java.specification.version", "read"; + permission java.util.PropertyPermission "java.specification.vendor", "read"; + permission java.util.PropertyPermission "java.specification.name", "read"; + + permission java.util.PropertyPermission "java.vm.specification.version", "read"; + permission java.util.PropertyPermission "java.vm.specification.vendor", "read"; + permission java.util.PropertyPermission "java.vm.specification.name", "read"; + permission java.util.PropertyPermission "java.vm.version", "read"; + permission java.util.PropertyPermission "java.vm.vendor", "read"; + permission java.util.PropertyPermission "java.vm.name", "read"; + + // Required for OpenJMX + permission java.lang.RuntimePermission "getAttribute"; + + // Allow read of JAXP compliant XML parser debug + permission java.util.PropertyPermission "jaxp.debug", "read"; + + // All JSPs need to be able to read this package + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.tomcat"; + + // Precompiled JSPs need access to these packages. + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.jasper.el"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.jasper.runtime"; + permission java.lang.RuntimePermission + "accessClassInPackage.org.apache.jasper.runtime.*"; + + // Applications using WebSocket need to be able to access these packages + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.tomcat.websocket"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.tomcat.websocket.server"; +}; + + +// The Manager application needs access to the following packages to support the +// session display functionality. It also requires the custom Tomcat +// DeployXmlPermission to enable the use of META-INF/context.xml +// These settings support the following configurations: +// - default CATALINA_HOME == CATALINA_BASE +// - CATALINA_HOME != CATALINA_BASE, per instance Manager in CATALINA_BASE +// - CATALINA_HOME != CATALINA_BASE, shared Manager in CATALINA_HOME +grant codeBase "file:${catalina.base}/../tomcat10-admin/manager/-" { + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.ha.session"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager.util"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.util"; + permission org.apache.catalina.security.DeployXmlPermission "manager"; +}; +grant codeBase "file:${catalina.home}/../tomcat10-admin/manager/-" { + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.ha.session"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager.util"; + permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.util"; + permission org.apache.catalina.security.DeployXmlPermission "manager"; +}; + +// The Host Manager application needs the custom Tomcat DeployXmlPermission to +// enable the use of META-INF/context.xml +// These settings support the following configurations: +// - default CATALINA_HOME == CATALINA_BASE +// - CATALINA_HOME != CATALINA_BASE, per instance Host Manager in CATALINA_BASE +// - CATALINA_HOME != CATALINA_BASE, shared Host Manager in CATALINA_HOME +grant codeBase "file:${catalina.base}/../tomcat10-admin/host-manager/-" { + permission org.apache.catalina.security.DeployXmlPermission "host-manager"; +}; +grant codeBase "file:${catalina.home}/../tomcat10-admin/host-manager/-" { + permission org.apache.catalina.security.DeployXmlPermission "host-manager"; +}; diff --git a/debian/policy/50local.policy b/debian/policy/50local.policy new file mode 100644 index 0000000..4c177b4 --- /dev/null +++ b/debian/policy/50local.policy @@ -0,0 +1,42 @@ +// You can assign additional permissions to particular web applications by +// adding additional "grant" entries here, based on the code base for that +// application, /WEB-INF/classes/, or /WEB-INF/lib/ jar files. +// +// Different permissions can be granted to JSP pages, classes loaded from +// the /WEB-INF/classes/ directory, all jar files in the /WEB-INF/lib/ +// directory, or even to individual jar files in the /WEB-INF/lib/ directory. +// +// For instance, assume that the standard "examples" application +// included a JDBC driver that needed to establish a network connection to the +// corresponding database and used the scrape taglib to get the weather from +// the NOAA web server. You might create a "grant" entries like this: +// +// The permissions granted to the context root directory apply to JSP pages. +// grant codeBase "file:${catalina.base}/webapps/examples/-" { +// permission java.net.SocketPermission "dbhost.mycompany.com:5432", "connect"; +// permission java.net.SocketPermission "*.noaa.gov:80", "connect"; +// }; +// +// The permissions granted to the context WEB-INF/classes directory +// grant codeBase "file:${catalina.base}/webapps/examples/WEB-INF/classes/-" { +// }; +// +// The permission granted to your JDBC driver +// grant codeBase "jar:file:${catalina.base}/webapps/examples/WEB-INF/lib/driver.jar!/-" { +// permission java.net.SocketPermission "dbhost.mycompany.com:5432", "connect"; +// }; +// The permission granted to the scrape taglib +// grant codeBase "jar:file:${catalina.base}/webapps/examples/WEB-INF/lib/scrape.jar!/-" { +// permission java.net.SocketPermission "*.noaa.gov:80", "connect"; +// }; + +// To grant permissions for web applications using packed WAR files, use the +// Tomcat specific WAR url scheme. +// +// The permissions granted to the entire web application +// grant codeBase "war:file:${catalina.base}/webapps/examples.war*/-" { +// }; +// +// The permissions granted to a specific JAR +// grant codeBase "war:file:${catalina.base}/webapps/examples.war*/WEB-INF/lib/foo.jar" { +// }; diff --git a/debian/policy/examples/10examples.policy b/debian/policy/examples/10examples.policy new file mode 100644 index 0000000..6fa2dee --- /dev/null +++ b/debian/policy/examples/10examples.policy @@ -0,0 +1,3 @@ +grant codeBase "file:${catalina.home}/bin/tomcat-juli.jar" { + permission java.io.FilePermission "/usr/share/tomcat10-examples/examples/WEB-INF/classes/logging.properties", "read"; +}; diff --git a/debian/rsyslog/tomcat10.conf b/debian/rsyslog/tomcat10.conf new file mode 100644 index 0000000..8291559 --- /dev/null +++ b/debian/rsyslog/tomcat10.conf @@ -0,0 +1,7 @@ +# Send Tomcat messages to catalina.out when using systemd +$template TomcatFormat,"[%timegenerated:::date-year%-%timegenerated:::date-month%-%timegenerated:::date-day% %timegenerated:::date-hour%:%timegenerated:::date-minute%:%timegenerated:::date-second%] [%syslogseverity-text%]%msg%\n" + +:programname, startswith, "tomcat10" { + action(type="omfile" file="/var/log/tomcat10/catalina.out" Template="TomcatFormat" fileOwner="tomcat" fileCreateMode="0640") + stop +} diff --git a/debian/rules b/debian/rules index 2d33f6a..7e20352 100755 --- a/debian/rules +++ b/debian/rules @@ -1,4 +1,37 @@ #!/usr/bin/make -f +include /usr/share/dpkg/pkg-info.mk + +JAVA_HOME := /usr/lib/jvm/default-java + %: - dh $@ + dh $@ --with maven-repo-helper --with javahelper + +override_dh_auto_build: + dh_auto_build -- -Ddistribution.name=$(shell lsb_release -si) embed-jars deploy + + # Prepare the Maven artifacts + mkdir -p debian/poms + cp res/maven/*.pom debian/poms + perl -p -i -e 's/\@MAVEN.DEPLOY.VERSION\@/$(DEB_VERSION_UPSTREAM)/' debian/poms/*.pom + + # Migrate the taglibs-standard libraries to the Jakarta EE namespace + mkdir -p webapps/examples/WEB-INF/lib + cp /usr/share/java/taglibs-standard-spec.jar webapps/examples/WEB-INF/lib/taglibs-standard-spec-1.2.5.jar + cp /usr/share/java/taglibs-standard-impl.jar webapps/examples/WEB-INF/lib/taglibs-standard-impl-1.2.5.jar + javax2jakarta -profile=TOMCAT webapps/examples/WEB-INF/lib/taglibs-standard-spec-1.2.5.jar webapps/examples/WEB-INF/lib/taglibs-standard-spec-1.2.5-migrated-0.0.1.jar + javax2jakarta -profile=TOMCAT webapps/examples/WEB-INF/lib/taglibs-standard-impl-1.2.5.jar webapps/examples/WEB-INF/lib/taglibs-standard-impl-1.2.5-migrated-0.0.1.jar + rm webapps/examples/WEB-INF/lib/taglibs-standard-spec-*.jar + +override_dh_auto_test: +ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) + dh_auto_build -- test -Dtest.apr.loc=/usr/lib/$(shell dpkg-architecture --query DEB_BUILD_MULTIARCH) -Dtest.verbose=false +endif + +override_dh_install-indep: + dh_install -i --exclude=.bat --exclude=Thumbs.db + + # update the checksum for the root webapp + unset rwmd5sum \ + && rwmd5sum=`cat debian/default_root/index.html debian/default_root/META-INF/context.xml | md5sum - 2>/dev/null | cut -d " " -f1` \ + && sed "s/\@ROOT_WEBAPP_MD5SUM\@/$$rwmd5sum/" debian/tomcat10.postrm.in > debian/tomcat10.postrm diff --git a/debian/setenv.sh b/debian/setenv.sh new file mode 100755 index 0000000..e564a14 --- /dev/null +++ b/debian/setenv.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# + +CATALINA_HOME=/usr/share/tomcat10 + +# Find the Java runtime and set JAVA_HOME +. /usr/libexec/tomcat10/tomcat-locate-java.sh + +# Default Java options +if [ -z "$JAVA_OPTS" ]; then + JAVA_OPTS="-Djava.awt.headless=true" +fi diff --git a/debian/sysusers/tomcat10.conf b/debian/sysusers/tomcat10.conf new file mode 100644 index 0000000..6c78a06 --- /dev/null +++ b/debian/sysusers/tomcat10.conf @@ -0,0 +1,7 @@ +# +# sysusers.d snippet for creating the tomcat user automatically +# at install time. See sysusers.d(5) for details. +# + +#Type Name ID GECOS Home directory Shell +u tomcat - "Apache Tomcat" /var/lib/tomcat /usr/sbin/nologin diff --git a/debian/tomcat10-admin.install b/debian/tomcat10-admin.install new file mode 100644 index 0000000..ace3222 --- /dev/null +++ b/debian/tomcat10-admin.install @@ -0,0 +1,4 @@ +debian/context/host-manager.xml /etc/tomcat10/Catalina/localhost/ +debian/context/manager.xml /etc/tomcat10/Catalina/localhost/ +output/build/webapps/host-manager /usr/share/tomcat10-admin/ +output/build/webapps/manager /usr/share/tomcat10-admin/ diff --git a/debian/tomcat10-common.docs b/debian/tomcat10-common.docs new file mode 100644 index 0000000..e951ee6 --- /dev/null +++ b/debian/tomcat10-common.docs @@ -0,0 +1,2 @@ +RELEASE-NOTES +RUNNING.txt diff --git a/debian/tomcat10-common.install b/debian/tomcat10-common.install new file mode 100644 index 0000000..8be3fc2 --- /dev/null +++ b/debian/tomcat10-common.install @@ -0,0 +1,3 @@ +bin/* /usr/share/tomcat10/bin/ +debian/libexec/tomcat-locate-java.sh /usr/libexec/tomcat10/ +output/build/bin/bootstrap.jar /usr/share/tomcat10/bin/ diff --git a/debian/tomcat10-common.links b/debian/tomcat10-common.links new file mode 100644 index 0000000..1c00809 --- /dev/null +++ b/debian/tomcat10-common.links @@ -0,0 +1,27 @@ +/usr/share/java/tomcat10-annotations-api.jar /usr/share/tomcat10/lib/annotations-api.jar +/usr/share/java/tomcat10-api.jar /usr/share/tomcat10/lib/tomcat-api.jar +/usr/share/java/tomcat10-catalina-ant.jar /usr/share/tomcat10/lib/catalina-ant.jar +/usr/share/java/tomcat10-catalina-ha.jar /usr/share/tomcat10/lib/catalina-ha.jar +/usr/share/java/tomcat10-catalina.jar /usr/share/tomcat10/lib/catalina.jar +/usr/share/java/tomcat10-coyote.jar /usr/share/tomcat10/lib/tomcat-coyote.jar +/usr/share/java/tomcat10-dbcp.jar /usr/share/tomcat10/lib/tomcat-dbcp.jar +/usr/share/java/tomcat10-el-api.jar /usr/share/tomcat10/lib/el-api.jar +/usr/share/java/tomcat10-i18n-es.jar /usr/share/tomcat10/lib/tomcat-i18n-es.jar +/usr/share/java/tomcat10-i18n-fr.jar /usr/share/tomcat10/lib/tomcat-i18n-fr.jar +/usr/share/java/tomcat10-i18n-ja.jar /usr/share/tomcat10/lib/tomcat-i18n-ja.jar +/usr/share/java/tomcat10-i18n-ru.jar /usr/share/tomcat10/lib/tomcat-i18n-ru.jar +/usr/share/java/tomcat10-jasper-el.jar /usr/share/tomcat10/lib/jasper-el.jar +/usr/share/java/tomcat10-jasper.jar /usr/share/tomcat10/lib/jasper.jar +/usr/share/java/tomcat10-jaspic-api.jar /usr/share/tomcat10/lib/jaspic-api.jar +/usr/share/java/tomcat10-jdbc.jar /usr/share/tomcat10/lib/tomcat-jdbc.jar +/usr/share/java/tomcat10-jni.jar /usr/share/tomcat10/lib/tomcat-jni.jar +/usr/share/java/tomcat10-jsp-api.jar /usr/share/tomcat10/lib/jsp-api.jar +/usr/share/java/tomcat10-juli.jar /usr/share/tomcat10/bin/tomcat-juli.jar +/usr/share/java/tomcat10-servlet-api.jar /usr/share/tomcat10/lib/servlet-api.jar +/usr/share/java/tomcat10-storeconfig.jar /usr/share/tomcat10/lib/catalina-storeconfig.jar +/usr/share/java/tomcat10-tribes.jar /usr/share/tomcat10/lib/catalina-tribes.jar +/usr/share/java/tomcat10-util-scan.jar /usr/share/tomcat10/lib/tomcat-util-scan.jar +/usr/share/java/tomcat10-util.jar /usr/share/tomcat10/lib/tomcat-util.jar +/usr/share/java/tomcat10-websocket-api.jar /usr/share/tomcat10/lib/websocket-api.jar +/usr/share/java/tomcat10-websocket-client-api.jar /usr/share/tomcat10/lib/websocket-client-api.jar +/usr/share/java/tomcat10-websocket.jar /usr/share/tomcat10/lib/tomcat-websocket.jar diff --git a/debian/tomcat10-docs.doc-base b/debian/tomcat10-docs.doc-base new file mode 100644 index 0000000..163a2ea --- /dev/null +++ b/debian/tomcat10-docs.doc-base @@ -0,0 +1,9 @@ +Document: tomcat10 +Title: Apache Tomcat 10 Documentation +Author: Apache Software Foundation +Abstract: Documentation bundle for Apache Tomcat 10 Servlet/JSP container. +Section: System/Administration + +Format: HTML +Index: /usr/share/doc/tomcat10-docs/docs/index.html +Files: /usr/share/doc/tomcat10-docs/docs/* diff --git a/debian/tomcat10-docs.install b/debian/tomcat10-docs.install new file mode 100644 index 0000000..78d943c --- /dev/null +++ b/debian/tomcat10-docs.install @@ -0,0 +1,2 @@ +debian/context/docs.xml /etc/tomcat10/Catalina/localhost/ +output/build/webapps/docs /usr/share/tomcat10-docs/ diff --git a/debian/tomcat10-docs.links b/debian/tomcat10-docs.links new file mode 100644 index 0000000..45e3394 --- /dev/null +++ b/debian/tomcat10-docs.links @@ -0,0 +1 @@ +/usr/share/tomcat10-docs/docs /usr/share/doc/tomcat10-docs/docs diff --git a/debian/tomcat10-docs.lintian-overrides b/debian/tomcat10-docs.lintian-overrides new file mode 100644 index 0000000..1c6ee2d --- /dev/null +++ b/debian/tomcat10-docs.lintian-overrides @@ -0,0 +1,9 @@ +# The documentation is packaged as a web application and is intended to be served +# from Tomcat at http://localhost:8080/docs, not read directly from /usr/share/doc +tomcat10-docs: package-contains-documentation-outside-usr-share-doc + +# 'docs' is the name of the web application shipped upstream +tomcat10-docs: unusual-documentation-package-name + +# Web font used within a web application +tomcat10-docs: font-outside-font-dir diff --git a/debian/tomcat10-examples.install b/debian/tomcat10-examples.install new file mode 100644 index 0000000..f4f385b --- /dev/null +++ b/debian/tomcat10-examples.install @@ -0,0 +1,4 @@ +debian/context/examples.xml /etc/tomcat10/Catalina/localhost/ +debian/policy/examples/*.policy /etc/tomcat10/policy.d/ +output/build/webapps/examples /usr/share/tomcat10-examples/ +webapps/examples/WEB-INF/lib/taglibs-standard-*.jar /usr/share/tomcat10-examples/examples/WEB-INF/lib/ diff --git a/debian/tomcat10-instance-create b/debian/tomcat10-instance-create new file mode 100644 index 0000000..b199716 --- /dev/null +++ b/debian/tomcat10-instance-create @@ -0,0 +1,139 @@ +#!/bin/sh +# Script to create a CATALINA_BASE directory for your own tomcat + +PROG=`basename $0` +TARGET="" +HPORT=8080 +CPORT=8005 +CWORD="SHUTDOWN" +warned=0 +warnlowport=0 + +usage() { + echo "Usage: $PROG [options] " + echo " directoryname: name of the tomcat instance directory to create" + echo "Options:" + echo " -h, --help Display this help message" + echo " -p httpport HTTP port to be used by Tomcat (default is $HPORT)" + echo " -c controlport Server shutdown control port (default is $CPORT)" + echo " -w magicword Word to send to trigger shutdown (default is $CWORD)" +} + +checkport() { + type=$1 + port=$2 + # Fail if port is non-numeric + num=`expr ${port} + 1 2> /dev/null` + if [ $? != 0 ] || [ $num -lt 2 ]; then + echo "Error: ${type} port '${port}' is not a valid TCP port number." + exit 1 + fi + + # Fail if port is above 65535 + if [ ${port} -gt 65535 ]; then + echo "Error: ${type} port ${port} is above TCP port numbers (> 65535)." + exit 1 + fi + + # Warn if port is below 1024 (once) + if [ ${warnlowport} -eq 0 ]; then + if [ ${port} -lt 1024 ]; then + echo "Warning: ports below 1024 are reserved to the super-user." + warnlowport=1 + warned=1 + fi + fi + + # Warn if port appears to be in use + if nc localhost "${port}" -z > /dev/null; then + echo "Warning: ${type} port ${port} appears to be in use." + warned=1 + fi +} + +if [ "$#" -lt 1 ]; then + usage + exit 1 +fi +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + usage + exit 0 +fi + + + +while getopts ":p:c:w:h" options; do + case $options in + p ) HPORT=$OPTARG ;; + c ) CPORT=$OPTARG ;; + w ) CWORD=$OPTARG ;; + h ) usage;; + * ) echo "Error: Unknown parameter '$OPTARG'." + exit 1;; + esac +done + +shift $(($OPTIND - 1)) +TARGET=$1 +shift +echo "You are about to create a Tomcat instance in directory '$TARGET'" + +# Fail if no target specified +if [ -z "${TARGET}" ]; then + echo "Error: No target directory specified (use -d)." + exit 1 +fi + +# Fail if ports are the same +if [ "${HPORT}" = "${CPORT}" ]; then + echo "Error: HTTP port and control port must be different." + exit 1 +fi + +# Fail if target directory already exists +if [ -d "${TARGET}" ]; then + echo "Error: Target directory already exists." + exit 1 +fi + +# Check ports +checkport HTTP "${HPORT}" +checkport Control "${CPORT}" + +# Ask for confirmation if warnings were printed out +if [ ${warned} -eq 1 ]; then + echo "Type to continue, to abort." + read answer +fi + +mkdir -p "${TARGET}" + +FULLTARGET=`cd "${TARGET}" > /dev/null && pwd` + +mkdir "${TARGET}/conf" +mkdir "${TARGET}/logs" +mkdir "${TARGET}/webapps" +mkdir "${TARGET}/work" +mkdir "${TARGET}/temp" +cp -r /usr/share/tomcat10/skel/* "${TARGET}" + +sed -i -e "s/Connector port=\"8080\"/Connector port=\"${HPORT}\"/;s/Server port=\"-1\" shutdown=\"SHUTDOWN\"/Server port=\"${CPORT}\" shutdown=\"${CWORD}\"/" "${TARGET}/conf/server.xml" + +cat > "${TARGET}/bin/startup.sh" << EOT +#!/bin/sh +export CATALINA_BASE="${FULLTARGET}" +/usr/share/tomcat10/bin/startup.sh +echo "Tomcat started" +EOT + +cat > "${TARGET}/bin/shutdown.sh" << EOT +#!/bin/sh +export CATALINA_BASE="${FULLTARGET}" +/usr/share/tomcat10/bin/shutdown.sh +echo "Tomcat stopped" +EOT + +chmod a+x "${TARGET}/bin/startup.sh" "${TARGET}/bin/shutdown.sh" +echo "* New Tomcat instance created in ${TARGET}" +echo "* You might want to edit default configuration in ${TARGET}/conf" +echo "* Run ${TARGET}/bin/startup.sh to start your Tomcat instance" diff --git a/debian/tomcat10-instance-create.1 b/debian/tomcat10-instance-create.1 new file mode 100644 index 0000000..806d9db --- /dev/null +++ b/debian/tomcat10-instance-create.1 @@ -0,0 +1,24 @@ +.TH "TOMCAT10-INSTANCE-CREATE" "2" "Feb 2010" "" "" +.SH "NAME" +tomcat10\-instance\-create \- creates a Tomcat 10 instance +.SH "SYNOPSIS" +.B tomcat10\-instance\-create [\fIOPTIONS\fR] \fIDIRECTORYNAME\fR +.SH "DESCRIPTION" +The +.B tomcat10\-instance\-create +script creates a directory with all required Tomcat 10 CATALINA_BASE elements so that a separate Tomcat 10 instance with its own configuration, libraries or web applications can be run by a user. bin/startup.sh and bin/shutdown.sh scripts are also generated to allow the instance to be started and stopped. +.TP +.B DIRECTORYNAME +The name of the directory where the instance will be created. It should not exist and will be created automatically. +.SH "OPTIONS" +.TP +.B \-p HTTPPORT +The TCP port to use for the default HTTP connector in the instance. The default port is 8080. +.TP +.B \-c CONTROLPORT +The TCP port to use for Tomcat shutdown control port. The default port is 8005. +.TP +.B \-w MAGICWORD +The magic word that sent to the control port will trigger the shutdown of the Tomcat instance. The default word is SHUTDOWN. +.SH "AUTHOR" +This man page was written by Thierry Carrez and is copyright (C) 2008 Canonical Ltd. diff --git a/debian/tomcat10-user.install b/debian/tomcat10-user.install new file mode 100644 index 0000000..139b884 --- /dev/null +++ b/debian/tomcat10-user.install @@ -0,0 +1,6 @@ +conf/*.xml /usr/share/tomcat10/skel/conf/ +conf/catalina.policy /usr/share/tomcat10/skel/policy/ +conf/catalina.properties /usr/share/tomcat10/skel/conf/ +debian/logging.properties /usr/share/tomcat10/skel/conf/ +debian/setenv.sh /usr/share/tomcat10/skel/bin/ +debian/tomcat10-instance-create /usr/bin/ diff --git a/debian/tomcat10-user.manpages b/debian/tomcat10-user.manpages new file mode 100644 index 0000000..708ccd2 --- /dev/null +++ b/debian/tomcat10-user.manpages @@ -0,0 +1 @@ +debian/tomcat10-instance-create.1 diff --git a/debian/tomcat10.cron.daily b/debian/tomcat10.cron.daily new file mode 100644 index 0000000..6414f13 --- /dev/null +++ b/debian/tomcat10.cron.daily @@ -0,0 +1,26 @@ +#!/bin/sh + +NAME=tomcat10 +DEFAULT=/etc/default/$NAME +LOGEXT="log txt" + +# The following variables can be overwritten in $DEFAULT + +# Whether to compress logfiles older than today's +LOGFILE_COMPRESS=1 + +# Overwrite settings from default file +if [ -f "$DEFAULT" ]; then + . "$DEFAULT" +fi + +if [ -d /var/log/$NAME ]; then + for EXT in $LOGEXT; do + # Compress the log files + if [ $LOGFILE_COMPRESS = 1 ]; then + find /var/log/$NAME/ -name \*.$EXT -daystart -mtime +0 -print0 -maxdepth 1 \ + | xargs --no-run-if-empty -0 gzip -9 + EXT=$EXT.gz + fi + done +fi diff --git a/debian/tomcat10.dirs b/debian/tomcat10.dirs new file mode 100644 index 0000000..741e394 --- /dev/null +++ b/debian/tomcat10.dirs @@ -0,0 +1,6 @@ +etc/logrotate.d +etc/tomcat10/Catalina +var/cache/tomcat10/ +var/lib/tomcat10/lib +var/lib/tomcat10/webapps +var/log/tomcat10/ diff --git a/debian/tomcat10.install b/debian/tomcat10.install new file mode 100644 index 0000000..f4c2999 --- /dev/null +++ b/debian/tomcat10.install @@ -0,0 +1,11 @@ +conf/*.xml /usr/share/tomcat10/etc/ +conf/catalina.properties /usr/share/tomcat10/etc/ +debian/default.template /usr/share/tomcat10/ +debian/default_root /usr/share/tomcat10-root/ +debian/libexec/tomcat-start.sh /usr/libexec/tomcat10/ +debian/libexec/tomcat-update-policy.sh /usr/libexec/tomcat10/ +debian/logging.properties /usr/share/tomcat10/etc/ +debian/logrotate.template /usr/share/tomcat10/ +debian/policy/*.policy /etc/tomcat10/policy.d/ +debian/rsyslog/* /etc/rsyslog.d/ +debian/sysusers/*.conf /usr/lib/sysusers.d/ diff --git a/debian/tomcat10.links b/debian/tomcat10.links new file mode 100644 index 0000000..03fdf13 --- /dev/null +++ b/debian/tomcat10.links @@ -0,0 +1,4 @@ +/etc/tomcat10 /var/lib/tomcat10/conf +/usr/share/doc/tomcat10-common/README.Debian /usr/share/doc/tomcat10/README.Debian +/var/cache/tomcat10 /var/lib/tomcat10/work +/var/log/tomcat10 /var/lib/tomcat10/logs diff --git a/debian/tomcat10.postinst b/debian/tomcat10.postinst new file mode 100644 index 0000000..9d64d53 --- /dev/null +++ b/debian/tomcat10.postinst @@ -0,0 +1,78 @@ +#!/bin/sh +# +# Post installation script for Tomcat +# + +set -e + +TOMCAT_USER="tomcat" +TOMCAT_GROUP="tomcat" + +CONFFILES="tomcat-users.xml web.xml server.xml logging.properties context.xml catalina.properties jaspic-providers.xml" + +case "$1" in + configure) + # Create the tomcat user as defined in /usr/lib/sysusers.d/tomcat10.conf + systemd-sysusers + + # Install the configuration files + for conffile in $CONFFILES; + do + ucf --debconf-ok --three-way /usr/share/tomcat10/etc/$conffile /etc/tomcat10/$conffile + + # Configuration files should not be modifiable by the tomcat user, as this can be + # a security issue (an attacker may insert code in a webapp and rewrite the tomcat + # configuration) but those files should be readable by tomcat, so we set the group + # to tomcat. + if [ -f "/etc/tomcat10/$conffile" ]; then + chown root:$TOMCAT_GROUP /etc/tomcat10/$conffile + chmod 640 /etc/tomcat10/$conffile + fi + done + + # Install /etc/logrotate.d/tomcat10 + ucf --debconf-ok --three-way /usr/share/tomcat10/logrotate.template /etc/logrotate.d/tomcat10 + + # Install /etc/default/tomcat10 + ucf --debconf-ok --three-way /usr/share/tomcat10/default.template /etc/default/tomcat10 + + # Install the policy files for the security manager. These files should not be modifiable + # by the tomcat user. Only diverge from default permissions for known Debian files. + chown root:$TOMCAT_GROUP /etc/tomcat10/policy.d + for policyfile in 01system.policy 02debian.policy 03catalina.policy 04webapps.policy 50local.policy; + do + if [ -f "/etc/tomcat10/policy.d/$policyfile" ]; then + chown root:$TOMCAT_GROUP /etc/tomcat10/policy.d/$policyfile + chmod 640 /etc/tomcat10/policy.d/$policyfile + fi + done + + # Grant the tomcat group read/write access to /var/lib/tomcat10/lib. This allows + # a non-root administrator in the tomcat group to add jar files to the classpath + # (for example database drivers). + chown -Rh $TOMCAT_USER:$TOMCAT_GROUP /var/lib/tomcat10/lib + + # Grant the tomcat group read/write access to the /etc/tomcat10/Catalina directory + # to write the hosts/contexts configuration files. + chown -Rh root:$TOMCAT_GROUP /etc/tomcat10/Catalina + chmod 775 /etc/tomcat10/Catalina + + # Grant read/write access to tomcat to the webapps directory (needed for extracting + # war files and for deploying webapps by non-root administrators in the tomcat group). + chown -Rh $TOMCAT_USER:$TOMCAT_GROUP /var/lib/tomcat10/webapps + chmod 775 /var/lib/tomcat10/webapps + + # Grant read/write access to tomcat to the log and cache directories + chown -Rh $TOMCAT_USER:adm /var/log/tomcat10/ + chmod 2770 /var/log/tomcat10/ + chown -Rh $TOMCAT_USER:$TOMCAT_GROUP /var/cache/tomcat10/ + chmod 750 /var/cache/tomcat10/ + ;; +esac + +# Install the default root webapp if there isn't one already +if [ ! -d /var/lib/tomcat10/webapps/ROOT ]; then + cp -r /usr/share/tomcat10-root/default_root /var/lib/tomcat10/webapps/ROOT +fi + +#DEBHELPER# diff --git a/debian/tomcat10.postrm.in b/debian/tomcat10.postrm.in new file mode 100644 index 0000000..03d2752 --- /dev/null +++ b/debian/tomcat10.postrm.in @@ -0,0 +1,92 @@ +#!/bin/sh +# +# Post removal script for Tomcat +# + +set -e + +#DEBHELPER# + +CONFFILES="tomcat-users.xml web.xml server.xml logging.properties context.xml catalina.properties jaspic-providers.xml" + +case "$1" in + remove) + # Remove the ROOT webapp if not modified + RWLOC="/var/lib/tomcat10/webapps/ROOT" + RWFILES="$RWLOC/index.html $RWLOC/META-INF/context.xml" + if [ "`(cat $RWFILES | md5sum -) 2>/dev/null | cut -d ' ' -f 1`" \ + = "@ROOT_WEBAPP_MD5SUM@" ] ; then + rm $RWFILES + rmdir --ignore-fail-on-non-empty \ + /var/lib/tomcat10/webapps/ROOT/META-INF \ + /var/lib/tomcat10/webapps/ROOT \ + /var/lib/tomcat10/webapps \ + /var/lib/tomcat10 || true + fi + + # Remove CATALINA_BASE/lib if not empty + if [ -d /var/lib/tomcat10/lib ] && [ -z "`(find var/lib/tomcat10/lib/classes -type f)`" ] ; then + rmdir --ignore-fail-on-non-empty \ + /var/lib/tomcat10/lib/classes \ + /var/lib/tomcat10/lib || true + fi + + # Remove the cache files (compiled JSP files) + if [ -d "/var/cache/tomcat10" ] ; then + rm -rf /var/cache/tomcat10 + fi + + # Remove the auto-generated catalina.policy file + if [ -d "/var/lib/tomcat10/policy" ] ; then + rm -rf /var/lib/tomcat10/policy + fi + ;; + + purge) + # Ignore errors during purge + set +e + + # Remove the configuration files + rm -rf /etc/logrotate.d/tomcat10 + rm -rf /etc/default/tomcat10 + for conffile in $CONFFILES; + do + rm -f /etc/tomcat10/$conffile + done + + # Unregister the configuration files from ucf + if which ucf >/dev/null; then + ucf --purge /etc/logrotate.d/tomcat10 + ucf --purge /etc/default/tomcat10 + for conffile in $CONFFILES; + do + ucf --purge /etc/tomcat10/$conffile + done + fi + + # Remove the log files + rm -rf /var/log/tomcat10 + + # Remove the temp directory + rm -rf /var/lib/tomcat10/temp + + # Remove the CATALINA_BASE directory if empty + if [ -d "/var/lib/tomcat10" ] ; then + rmdir --ignore-fail-on-non-empty /var/lib/tomcat10 + fi + + # Remove the configuration directories + rmdir --ignore-fail-on-non-empty /etc/tomcat10/policy.d /etc/tomcat10/Catalina/* /etc/tomcat10/Catalina /etc/tomcat10 + + set -e + ;; + + upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + # Nothing to do here + ;; + + *) + echo "$0 called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac diff --git a/debian/tomcat10.service b/debian/tomcat10.service new file mode 100644 index 0000000..6556d4f --- /dev/null +++ b/debian/tomcat10.service @@ -0,0 +1,43 @@ +# +# Systemd unit file for Apache Tomcat +# + +[Unit] +Description=Apache Tomcat 10 Web Application Server +Documentation=https://tomcat.apache.org/tomcat-10.0-doc/index.html +After=network.target +RequiresMountsFor=/var/log/tomcat10 /var/lib/tomcat10 + +[Service] + +# Configuration +Environment="CATALINA_HOME=/usr/share/tomcat10" +Environment="CATALINA_BASE=/var/lib/tomcat10" +Environment="CATALINA_TMPDIR=/tmp" +Environment="JAVA_OPTS=-Djava.awt.headless=true" + +# Lifecycle +Type=simple +ExecStartPre=+/usr/libexec/tomcat10/tomcat-update-policy.sh +ExecStart=/bin/sh /usr/libexec/tomcat10/tomcat-start.sh +SuccessExitStatus=143 +Restart=on-abort + +# Logging +SyslogIdentifier=tomcat10 + +# Security +User=tomcat +Group=tomcat +PrivateTmp=yes +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true +CacheDirectory=tomcat10 +CacheDirectoryMode=750 +ProtectSystem=strict +ReadWritePaths=/etc/tomcat10/Catalina/ +ReadWritePaths=/var/lib/tomcat10/webapps/ +ReadWritePaths=/var/log/tomcat10/ + +[Install] +WantedBy=multi-user.target diff --git a/debian/tomcat10.tmpfiles b/debian/tomcat10.tmpfiles new file mode 100644 index 0000000..c038b20 --- /dev/null +++ b/debian/tomcat10.tmpfiles @@ -0,0 +1,6 @@ +# Tomcat log directory permissions + +# See tmpfiles.d(5) for details + +# Type Path Mode UID GID Age Argument +d /var/log/tomcat10 2770 tomcat adm - diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..43145c7 --- /dev/null +++ b/debian/watch @@ -0,0 +1,3 @@ +version=4 +opts="mode=git,repack,compression=xz,uversionmangle=s/-(alpha|beta|RC|M)/~$1/" \ +https://github.com/apache/tomcat refs/tags/(10\.1\..*) diff --git a/java/jakarta/annotation/Generated.java b/java/jakarta/annotation/Generated.java new file mode 100644 index 0000000..4997cdf --- /dev/null +++ b/java/jakarta/annotation/Generated.java @@ -0,0 +1,50 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to identify generated code. + * + * @since Common Annotations 1.0 + */ +@Documented +@Target({ ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, + ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE }) +@Retention(RetentionPolicy.SOURCE) +public @interface Generated { + /** + * @return The name of the code generator. It is recommended that the fully qualified name of the code generator is + * used. + */ + String[] value(); + + /** + * @return The date the code was generated + */ + String date() default ""; + + /** + * @return Additional comments (if any) related to the code generation + */ + String comments() default ""; +} diff --git a/java/jakarta/annotation/ManagedBean.java b/java/jakarta/annotation/ManagedBean.java new file mode 100644 index 0000000..7a839d8 --- /dev/null +++ b/java/jakarta/annotation/ManagedBean.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Identifies a POJO as a managed bean. + * + * @since Common Annotations 1.1 + * + * @deprecated This will be removed no earlier than Jakarta EE 11. Use CDI beans instead. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Deprecated(since = "2.1.1", forRemoval = true) +public @interface ManagedBean { + /** + * @return Name of the managed bean + */ + String value() default ""; +} diff --git a/java/jakarta/annotation/Nonnull.java b/java/jakarta/annotation/Nonnull.java new file mode 100644 index 0000000..b29d54e --- /dev/null +++ b/java/jakarta/annotation/Nonnull.java @@ -0,0 +1,32 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Added to indicate an element may not be null. + * + * @since Common Annotations 2.1 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface Nonnull { + +} diff --git a/java/jakarta/annotation/Nullable.java b/java/jakarta/annotation/Nullable.java new file mode 100644 index 0000000..de6be0e --- /dev/null +++ b/java/jakarta/annotation/Nullable.java @@ -0,0 +1,32 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Added to indicate an element may be null. + * + * @since Common Annotations 2.1 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface Nullable { + +} diff --git a/java/jakarta/annotation/PostConstruct.java b/java/jakarta/annotation/PostConstruct.java new file mode 100644 index 0000000..8f32f4f --- /dev/null +++ b/java/jakarta/annotation/PostConstruct.java @@ -0,0 +1,35 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Added to a method required to be called after the object has been constructed and before it used. + * + * @since Common Annotations 1.0 + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PostConstruct { + // No attributes +} diff --git a/java/jakarta/annotation/PreDestroy.java b/java/jakarta/annotation/PreDestroy.java new file mode 100644 index 0000000..3635f76 --- /dev/null +++ b/java/jakarta/annotation/PreDestroy.java @@ -0,0 +1,35 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Added to a method required to be called once the object is no longer required and before it is discarded. + * + * @since Common Annotations 1.0 + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PreDestroy { + // No attributes +} diff --git a/java/jakarta/annotation/Priority.java b/java/jakarta/annotation/Priority.java new file mode 100644 index 0000000..8fd399c --- /dev/null +++ b/java/jakarta/annotation/Priority.java @@ -0,0 +1,37 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used to define the order in which classes or parameters should be used. The annotated class or parameter defines how + * the prority value is used. + * + * @since Common Annotations 1.2 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface Priority { + + /** + * @return the int value + */ + int value(); +} diff --git a/java/jakarta/annotation/Resource.java b/java/jakarta/annotation/Resource.java new file mode 100644 index 0000000..a86d4cf --- /dev/null +++ b/java/jakarta/annotation/Resource.java @@ -0,0 +1,88 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a resource required by the application. Annotated classes will be used as resources. Annotated fields + * and/or methods will have resources injected. + * + * @since Common Annotations 1.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(Resources.class) +public @interface Resource { + + /** + * The AuthenticationType, either CONTAINER or APPLICATION + */ + enum AuthenticationType { + /** + * Container authentication + */ + CONTAINER, + /** + * Application authentication + */ + APPLICATION + } + + /** + * @return a String with the name of the resource + */ + String name() default ""; + + /** + * Uses generics since Common Annotations 1.2. + * + * @return The type for instances of this resource + */ + Class type() default Object.class; + + /** + * @return the AuthenticationType of the resource default CONTAINER + */ + AuthenticationType authenticationType() default AuthenticationType.CONTAINER; + + /** + * @return true (default) if the resource is shareable, or false if not + */ + boolean shareable() default true; + + /** + * @return a string with the description for the resource + */ + String description() default ""; + + /** + * @return a string with the mappedName of the resource + */ + String mappedName() default ""; + + /** + * @since Common Annotations 1.1 + * + * @return The name of the entry, if any, to use for this resource + */ + String lookup() default ""; +} diff --git a/java/jakarta/annotation/Resources.java b/java/jakarta/annotation/Resources.java new file mode 100644 index 0000000..6ade76d --- /dev/null +++ b/java/jakarta/annotation/Resources.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used when multiple {@link Resource} annotations are required. + * + * @since Common Annotations 1.0 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Resources { + + /** + * @return a Resource[] with the value of this Resources + */ + Resource[] value(); +} diff --git a/java/jakarta/annotation/security/DeclareRoles.java b/java/jakarta/annotation/security/DeclareRoles.java new file mode 100644 index 0000000..3762af6 --- /dev/null +++ b/java/jakarta/annotation/security/DeclareRoles.java @@ -0,0 +1,37 @@ +/* + * 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 jakarta.annotation.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since Common Annotations 1.0 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DeclareRoles { + + /** + * @return a String[] with the roles + */ + String[] value(); +} diff --git a/java/jakarta/annotation/security/DenyAll.java b/java/jakarta/annotation/security/DenyAll.java new file mode 100644 index 0000000..347d5e6 --- /dev/null +++ b/java/jakarta/annotation/security/DenyAll.java @@ -0,0 +1,33 @@ +/* + * 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 jakarta.annotation.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since Common Annotations 1.0 + */ +@Documented +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface DenyAll { + // No attributes +} diff --git a/java/jakarta/annotation/security/PermitAll.java b/java/jakarta/annotation/security/PermitAll.java new file mode 100644 index 0000000..0e0c784 --- /dev/null +++ b/java/jakarta/annotation/security/PermitAll.java @@ -0,0 +1,33 @@ +/* + * 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 jakarta.annotation.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since Common Annotations 1.0 + */ +@Documented +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface PermitAll { + // No attributes +} diff --git a/java/jakarta/annotation/security/RolesAllowed.java b/java/jakarta/annotation/security/RolesAllowed.java new file mode 100644 index 0000000..7092d01 --- /dev/null +++ b/java/jakarta/annotation/security/RolesAllowed.java @@ -0,0 +1,37 @@ +/* + * 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 jakarta.annotation.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since Common Annotations 1.0 + */ +@Documented +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface RolesAllowed { + + /** + * @return a String[] of the allowed roles + */ + String[] value(); +} diff --git a/java/jakarta/annotation/security/RunAs.java b/java/jakarta/annotation/security/RunAs.java new file mode 100644 index 0000000..d5029b7 --- /dev/null +++ b/java/jakarta/annotation/security/RunAs.java @@ -0,0 +1,37 @@ +/* + * 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 jakarta.annotation.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since Common Annotations 1.0 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface RunAs { + + /** + * @return a String with the value for RunAs + */ + String value(); +} diff --git a/java/jakarta/annotation/sql/DataSourceDefinition.java b/java/jakarta/annotation/sql/DataSourceDefinition.java new file mode 100644 index 0000000..9e5c46d --- /dev/null +++ b/java/jakarta/annotation/sql/DataSourceDefinition.java @@ -0,0 +1,122 @@ +/* + * 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 jakarta.annotation.sql; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since Common Annotations 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(DataSourceDefinitions.class) +public @interface DataSourceDefinition { + + /** + * @return the className + */ + String className(); + + /** + * @return the name + */ + String name(); + + /** + * @return the description + */ + String description() default ""; + + /** + * @return the url + */ + String url() default ""; + + /** + * @return the user + */ + String user() default ""; + + /** + * @return the password + */ + String password() default ""; + + /** + * @return database name + */ + String databaseName() default ""; + + /** + * @return the port number + */ + int portNumber() default -1; + + /** + * @return the server name + */ + String serverName() default "localhost"; + + /** + * @return the isolation level + */ + int isolationLevel() default -1; + + /** + * @return true if the data source is transactional + */ + boolean transactional() default true; + + /** + * @return the initial pool size + */ + int initialPoolSize() default -1; + + /** + * @return the max pool size + */ + int maxPoolSize() default -1; + + /** + * @return the min pool size + */ + int minPoolSize() default -1; + + /** + * @return the max idle time + */ + int maxIdleTime() default -1; + + /** + * @return the max statements + */ + int maxStatements() default -1; + + /** + * @return a String[] with the properties + */ + String[] properties() default {}; + + /** + * @return the login timeout + */ + int loginTimeout() default 0; +} diff --git a/java/jakarta/annotation/sql/DataSourceDefinitions.java b/java/jakarta/annotation/sql/DataSourceDefinitions.java new file mode 100644 index 0000000..d40e357 --- /dev/null +++ b/java/jakarta/annotation/sql/DataSourceDefinitions.java @@ -0,0 +1,35 @@ +/* + * 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 jakarta.annotation.sql; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @since Common Annotations 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DataSourceDefinitions { + + /** + * @return a DataSourceDefinition[] + */ + DataSourceDefinition[] value(); +} diff --git a/java/jakarta/ejb/EJB.java b/java/jakarta/ejb/EJB.java new file mode 100644 index 0000000..cd5b247 --- /dev/null +++ b/java/jakarta/ejb/EJB.java @@ -0,0 +1,40 @@ +/* + * 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 jakarta.ejb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) + +public @interface EJB { + String name() default ""; + + String description() default ""; + + @SuppressWarnings("rawtypes") // Can't use Class because API needs to match specification + Class beanInterface() default Object.class; + + String beanName() default ""; + + String mappedName() default ""; + + String lookup() default ""; +} diff --git a/java/jakarta/ejb/EJBs.java b/java/jakarta/ejb/EJBs.java new file mode 100644 index 0000000..5f2647c --- /dev/null +++ b/java/jakarta/ejb/EJBs.java @@ -0,0 +1,29 @@ +/* + * 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 jakarta.ejb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) + +public @interface EJBs { + EJB[] value(); +} diff --git a/java/jakarta/el/ArrayELResolver.java b/java/jakarta/el/ArrayELResolver.java new file mode 100644 index 0000000..8a432e5 --- /dev/null +++ b/java/jakarta/el/ArrayELResolver.java @@ -0,0 +1,163 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.lang.reflect.Array; +import java.util.Iterator; +import java.util.Objects; + +/** + * Standard ELResolver for working with arrays. + */ +public class ArrayELResolver extends ELResolver { + + private final boolean readOnly; + + /** + * Creates a writable instance of the standard array resolver. + */ + public ArrayELResolver() { + this.readOnly = false; + } + + /** + * Creates an instance of the standard array resolver. + * + * @param readOnly {@code true} if the created instance should be read-only otherwise false. + */ + public ArrayELResolver(boolean readOnly) { + this.readOnly = readOnly; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base != null && base.getClass().isArray()) { + context.setPropertyResolved(base, property); + try { + int idx = coerce(property); + checkBounds(base, idx); + } catch (IllegalArgumentException e) { + // ignore + } + /* + * The resolver may have been created in read-only mode but the array and its elements will always be + * read-write. + */ + if (readOnly) { + return null; + } + return base.getClass().getComponentType(); + } + + return null; + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base != null && base.getClass().isArray()) { + context.setPropertyResolved(base, property); + int idx = coerce(property); + if (idx < 0 || idx >= Array.getLength(base)) { + return null; + } + return Array.get(base, idx); + } + + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + + if (base != null && base.getClass().isArray()) { + context.setPropertyResolved(base, property); + + if (this.readOnly) { + throw new PropertyNotWritableException( + Util.message(context, "resolverNotWritable", base.getClass().getName())); + } + + int idx = coerce(property); + checkBounds(base, idx); + if (value != null && !Util.isAssignableFrom(value.getClass(), base.getClass().getComponentType())) { + throw new ClassCastException(Util.message(context, "objectNotAssignable", value.getClass().getName(), + base.getClass().getComponentType().getName())); + } + Array.set(base, idx, value); + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base != null && base.getClass().isArray()) { + context.setPropertyResolved(base, property); + try { + int idx = coerce(property); + checkBounds(base, idx); + } catch (IllegalArgumentException e) { + // ignore + } + } + + return this.readOnly; + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base != null && base.getClass().isArray()) { + return Integer.class; + } + return null; + } + + private static void checkBounds(Object base, int idx) { + if (idx < 0 || idx >= Array.getLength(base)) { + throw new PropertyNotFoundException(new ArrayIndexOutOfBoundsException(idx).getMessage()); + } + } + + private static int coerce(Object property) { + if (property instanceof Number) { + return ((Number) property).intValue(); + } + if (property instanceof Character) { + return ((Character) property).charValue(); + } + if (property instanceof Boolean) { + return ((Boolean) property).booleanValue() ? 1 : 0; + } + if (property instanceof String) { + return Integer.parseInt((String) property); + } + throw new IllegalArgumentException(property != null ? property.toString() : "null"); + } + +} diff --git a/java/jakarta/el/BeanELResolver.java b/java/jakarta/el/BeanELResolver.java new file mode 100644 index 0000000..0d8ad14 --- /dev/null +++ b/java/jakarta/el/BeanELResolver.java @@ -0,0 +1,369 @@ +/* + * 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 jakarta.el; + +import java.beans.BeanInfo; +import java.beans.FeatureDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Standard ELResolver for working with JavaBeans. + */ +public class BeanELResolver extends ELResolver { + + private static final int CACHE_SIZE; + private static final String CACHE_SIZE_PROP = "org.apache.el.BeanELResolver.CACHE_SIZE"; + + static { + if (System.getSecurityManager() == null) { + CACHE_SIZE = Integer.getInteger(CACHE_SIZE_PROP, 1000).intValue(); + } else { + CACHE_SIZE = AccessController + .doPrivileged((PrivilegedAction) () -> Integer.getInteger(CACHE_SIZE_PROP, 1000)) + .intValue(); + } + } + + private final boolean readOnly; + + private final ConcurrentCache cache = new ConcurrentCache<>(CACHE_SIZE); + + /** + * Creates a writable instance of the standard JavaBean resolver. + */ + public BeanELResolver() { + this.readOnly = false; + } + + /** + * Creates an instance of the standard JavaBean resolver. + * + * @param readOnly {@code true} if the created instance should be read-only otherwise false. + */ + public BeanELResolver(boolean readOnly) { + this.readOnly = readOnly; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + if (base == null || property == null) { + return null; + } + + context.setPropertyResolved(base, property); + BeanProperty beanProperty = property(context, base, property); + + if (readOnly || beanProperty.isReadOnly(base)) { + return null; + } + + return beanProperty.getPropertyType(); + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + if (base == null || property == null) { + return null; + } + + context.setPropertyResolved(base, property); + Method m = this.property(context, base, property).read(context, base); + try { + return m.invoke(base, (Object[]) null); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + Util.handleThrowable(cause); + throw new ELException( + Util.message(context, "propertyReadError", base.getClass().getName(), property.toString()), cause); + } catch (Exception e) { + throw new ELException(e); + } + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + if (base == null || property == null) { + return; + } + + context.setPropertyResolved(base, property); + + if (this.readOnly) { + throw new PropertyNotWritableException( + Util.message(context, "resolverNotWritable", base.getClass().getName())); + } + + Method m = this.property(context, base, property).write(context, base); + try { + m.invoke(base, value); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + Util.handleThrowable(cause); + throw new ELException( + Util.message(context, "propertyWriteError", base.getClass().getName(), property.toString()), cause); + } catch (Exception e) { + throw new ELException(e); + } + } + + @Override + public Object invoke(ELContext context, Object base, Object method, Class[] paramTypes, Object[] params) { + Objects.requireNonNull(context); + if (base == null || method == null) { + return null; + } + + ExpressionFactory factory = ELManager.getExpressionFactory(); + + String methodName = factory.coerceToType(method, String.class); + + // Find the matching method + Method matchingMethod = Util.findMethod(context, base.getClass(), base, methodName, paramTypes, params); + + Object[] parameters = + Util.buildParameters(context, matchingMethod.getParameterTypes(), matchingMethod.isVarArgs(), params); + + Object result = null; + try { + result = matchingMethod.invoke(base, parameters); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new ELException(e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + Util.handleThrowable(cause); + throw new ELException(cause); + } + + context.setPropertyResolved(base, method); + return result; + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + if (base == null || property == null) { + return false; + } + + context.setPropertyResolved(base, property); + return this.readOnly || this.property(context, base, property).isReadOnly(base); + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + if (base == null) { + return null; + } + + try { + BeanInfo info = Introspector.getBeanInfo(base.getClass()); + PropertyDescriptor[] pds = info.getPropertyDescriptors(); + for (PropertyDescriptor pd : pds) { + pd.setValue(RESOLVABLE_AT_DESIGN_TIME, Boolean.TRUE); + pd.setValue(TYPE, pd.getPropertyType()); + } + return Arrays.asList((FeatureDescriptor[]) pds).iterator(); + } catch (IntrospectionException e) { + // + } + + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base != null) { + return Object.class; + } + + return null; + } + + static final class BeanProperties { + private final Map properties; + + private final Class type; + + BeanProperties(Class type) throws ELException { + this.type = type; + this.properties = new HashMap<>(); + try { + BeanInfo info = Introspector.getBeanInfo(this.type); + PropertyDescriptor[] pds = info.getPropertyDescriptors(); + for (PropertyDescriptor pd : pds) { + this.properties.put(pd.getName(), new BeanProperty(type, pd)); + } + /* + * Populating from any interfaces solves two distinct problems: 1. When running under a security + * manager, classes may be unaccessible but have accessible interfaces. 2. It causes default methods to + * be included. + */ + populateFromInterfaces(type); + } catch (IntrospectionException ie) { + throw new ELException(ie); + } + } + + private void populateFromInterfaces(Class aClass) throws IntrospectionException { + Class interfaces[] = aClass.getInterfaces(); + if (interfaces.length > 0) { + for (Class ifs : interfaces) { + BeanInfo info = Introspector.getBeanInfo(ifs); + PropertyDescriptor[] pds = info.getPropertyDescriptors(); + for (PropertyDescriptor pd : pds) { + if (!this.properties.containsKey(pd.getName())) { + this.properties.put(pd.getName(), new BeanProperty(this.type, pd)); + } + } + populateFromInterfaces(ifs); + } + } + Class superclass = aClass.getSuperclass(); + if (superclass != null) { + populateFromInterfaces(superclass); + } + } + + private BeanProperty get(ELContext ctx, String name) { + BeanProperty property = this.properties.get(name); + if (property == null) { + throw new PropertyNotFoundException(Util.message(ctx, "propertyNotFound", type.getName(), name)); + } + return property; + } + + private Class getType() { + return type; + } + } + + static final class BeanProperty { + private final Class type; + + private final Class owner; + + private final PropertyDescriptor descriptor; + + private Method read; + + private Method write; + + BeanProperty(Class owner, PropertyDescriptor descriptor) { + this.owner = owner; + this.descriptor = descriptor; + this.type = descriptor.getPropertyType(); + } + + public Class getPropertyType() { + return this.type; + } + + public boolean isReadOnly(Object base) { + return this.write == null && + (null == (this.write = Util.getMethod(this.owner, base, descriptor.getWriteMethod()))); + } + + private Method write(ELContext ctx, Object base) { + if (this.write == null) { + this.write = Util.getMethod(this.owner, base, descriptor.getWriteMethod()); + if (this.write == null) { + throw new PropertyNotWritableException(Util.message(ctx, "propertyNotWritable", + new Object[] { owner.getName(), descriptor.getName() })); + } + } + return this.write; + } + + private Method read(ELContext ctx, Object base) { + if (this.read == null) { + this.read = Util.getMethod(this.owner, base, descriptor.getReadMethod()); + if (this.read == null) { + throw new PropertyNotFoundException(Util.message(ctx, "propertyNotReadable", + new Object[] { owner.getName(), descriptor.getName() })); + } + } + return this.read; + } + } + + private BeanProperty property(ELContext ctx, Object base, Object property) { + Class type = base.getClass(); + String prop = property.toString(); + + BeanProperties props = this.cache.get(type.getName()); + if (props == null || type != props.getType()) { + props = new BeanProperties(type); + this.cache.put(type.getName(), props); + } + + return props.get(ctx, prop); + } + + private static final class ConcurrentCache { + + private final int size; + private final Map eden; + private final Map longterm; + + ConcurrentCache(int size) { + this.size = size; + this.eden = new ConcurrentHashMap<>(size); + this.longterm = new WeakHashMap<>(size); + } + + public V get(K key) { + V value = this.eden.get(key); + if (value == null) { + synchronized (longterm) { + value = this.longterm.get(key); + } + if (value != null) { + this.eden.put(key, value); + } + } + return value; + } + + public void put(K key, V value) { + if (this.eden.size() >= this.size) { + synchronized (longterm) { + this.longterm.putAll(this.eden); + } + this.eden.clear(); + } + this.eden.put(key, value); + } + + } +} diff --git a/java/jakarta/el/BeanNameELResolver.java b/java/jakarta/el/BeanNameELResolver.java new file mode 100644 index 0000000..0f74353 --- /dev/null +++ b/java/jakarta/el/BeanNameELResolver.java @@ -0,0 +1,160 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.util.Iterator; +import java.util.Objects; + +/** + * @since EL 3.0 + */ +public class BeanNameELResolver extends ELResolver { + + private final BeanNameResolver beanNameResolver; + + public BeanNameELResolver(BeanNameResolver beanNameResolver) { + this.beanNameResolver = beanNameResolver; + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + if (base != null || !(property instanceof String)) { + return null; + } + + String beanName = (String) property; + + if (beanNameResolver.isNameResolved(beanName)) { + try { + Object result = beanNameResolver.getBean(beanName); + context.setPropertyResolved(base, property); + return result; + } catch (Throwable t) { + Util.handleThrowable(t); + throw new ELException(t); + } + } + + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + if (base != null || !(property instanceof String)) { + return; + } + + String beanName = (String) property; + + boolean isResolved = context.isPropertyResolved(); + + boolean isReadOnly; + try { + isReadOnly = isReadOnly(context, base, property); + } catch (Throwable t) { + Util.handleThrowable(t); + throw new ELException(t); + } finally { + context.setPropertyResolved(isResolved); + } + + if (isReadOnly) { + throw new PropertyNotWritableException(Util.message(context, "beanNameELResolver.beanReadOnly", beanName)); + } + + if (beanNameResolver.isNameResolved(beanName) || beanNameResolver.canCreateBean(beanName)) { + try { + beanNameResolver.setBeanValue(beanName, value); + context.setPropertyResolved(base, property); + } catch (Throwable t) { + Util.handleThrowable(t); + throw new ELException(t); + } + } + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + if (base != null || !(property instanceof String)) { + return null; + } + + String beanName = (String) property; + + try { + if (beanNameResolver.isNameResolved(beanName)) { + Class result = beanNameResolver.getBean(beanName).getClass(); + context.setPropertyResolved(base, property); + + /* + * No resolver level isReadOnly property for this resolver + */ + if (beanNameResolver.isReadOnly((String) property)) { + return null; + } + + return result; + } + } catch (Throwable t) { + Util.handleThrowable(t); + throw new ELException(t); + } + + return null; + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + if (base != null || !(property instanceof String)) { + // Return value undefined + return false; + } + + String beanName = (String) property; + + if (beanNameResolver.isNameResolved(beanName)) { + boolean result; + try { + result = beanNameResolver.isReadOnly(beanName); + } catch (Throwable t) { + Util.handleThrowable(t); + throw new ELException(t); + } + context.setPropertyResolved(base, property); + return result; + } + + // Return value undefined + return false; + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return String.class; + } +} diff --git a/java/jakarta/el/BeanNameResolver.java b/java/jakarta/el/BeanNameResolver.java new file mode 100644 index 0000000..17754a1 --- /dev/null +++ b/java/jakarta/el/BeanNameResolver.java @@ -0,0 +1,87 @@ +/* + * 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 jakarta.el; + +/** + * Base implementation that provides a minimal default implementation that is intended to be extended by application + * developers. + * + * @since EL 3.0 + */ +public abstract class BeanNameResolver { + + /** + * Can this resolver resolve the given bean name? + * + * @param beanName The bean name to resolve + * + * @return This default implementation always returns false + */ + public boolean isNameResolved(String beanName) { + return false; + } + + + /** + * Returns the named bean. + * + * @param beanName The bean name to return + * + * @return This default implementation always returns null + */ + public Object getBean(String beanName) { + return null; + } + + + /** + * Sets a value of a bean of the given name. If the named bean does not exist and {@link #canCreateBean} returns + * true then a bean is created with the given value. + * + * @param beanName The name of the bean to be set/create + * @param value The value of the bean to set/create + * + * @throws PropertyNotWritableException if the bean is read only + */ + public void setBeanValue(String beanName, Object value) throws PropertyNotWritableException { + throw new PropertyNotWritableException(); + } + + + /** + * Is the named bean read-only? + * + * @param beanName The name of the bean of interest + * + * @return true if the bean is read only, otherwise false + */ + public boolean isReadOnly(String beanName) { + return true; + } + + + /** + * Is it permitted to create a bean of the given name? + * + * @param beanName The name of the bean of interest + * + * @return true if the bean may be created, otherwise false + */ + public boolean canCreateBean(String beanName) { + return false; + } +} diff --git a/java/jakarta/el/CompositeELResolver.java b/java/jakarta/el/CompositeELResolver.java new file mode 100644 index 0000000..6c8ced4 --- /dev/null +++ b/java/jakarta/el/CompositeELResolver.java @@ -0,0 +1,279 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.util.HashSet; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; + +public class CompositeELResolver extends ELResolver { + + private static final Class SCOPED_ATTRIBUTE_EL_RESOLVER; + private static final Set KNOWN_NON_TYPE_CONVERTING_RESOLVERS = new HashSet<>(); + static { + Class clazz = null; + try { + clazz = Class.forName("jakarta.servlet.jsp.el.ScopedAttributeELResolver"); + } catch (ClassNotFoundException e) { + // Ignore. This is expected if using the EL stand-alone + } + SCOPED_ATTRIBUTE_EL_RESOLVER = clazz; + + // EL API Resolvers + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add(ArrayELResolver.class.getName()); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add(BeanELResolver.class.getName()); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add(BeanNameELResolver.class.getName()); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add(ListELResolver.class.getName()); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add(MapELResolver.class.getName()); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add(ResourceBundleELResolver.class.getName()); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add(StaticFieldELResolver.class.getName()); + // JSP API Resolvers - referenced by name to avoid creating dependency + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add("jakarta.servlet.jsp.el.ImplicitObjectELResolver"); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add("jakarta.servlet.jsp.el.ImportELResolver"); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add("jakarta.servlet.jsp.el.NotFoundELResolver"); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add("jakarta.servlet.jsp.el.ScopedAttributeELResolver"); + // Tomcat internal resolvers - referenced by name to avoid creating dependency + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add("org.apache.jasper.el.JasperELResolver$GraalBeanELResolver"); + KNOWN_NON_TYPE_CONVERTING_RESOLVERS.add("org.apache.el.stream.StreamELResolverImpl"); + } + + private int resolversSize; + private ELResolver[] resolvers; + + /* + * Use a separate array for ELResolvers that might implement type conversion as a performance optimisation. See + * https://bz.apache.org/bugzilla/show_bug.cgi?id=68119 + */ + private int typeConvertersSize; + private ELResolver[] typeConverters; + + public CompositeELResolver() { + resolversSize = 0; + resolvers = new ELResolver[8]; + + typeConvertersSize = 0; + typeConverters = new ELResolver[0]; + } + + public void add(ELResolver elResolver) { + Objects.requireNonNull(elResolver); + + /* + * resolversSize should never be larger than resolvers.length. If it ever is, the code will fail when execution + * reaches System.arraycopy with an IndexOutOfBoundsException. + */ + if (resolversSize >= resolvers.length) { + ELResolver[] nr = new ELResolver[resolversSize * 2]; + System.arraycopy(resolvers, 0, nr, 0, resolversSize); + resolvers = nr; + } + resolvers[resolversSize++] = elResolver; + + if (KNOWN_NON_TYPE_CONVERTING_RESOLVERS.contains(elResolver.getClass().getName())) { + // Performance optimisation. ELResolver known not to perform type conversion + return; + } + + if (typeConvertersSize == 0) { + typeConverters = new ELResolver[1]; + } else if (typeConvertersSize == typeConverters.length) { + ELResolver[] expandedTypeConverters = new ELResolver[typeConvertersSize * 2]; + System.arraycopy(typeConverters, 0, expandedTypeConverters, 0, typeConvertersSize); + typeConverters = expandedTypeConverters; + } + typeConverters[typeConvertersSize++] = elResolver; + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + context.setPropertyResolved(false); + int sz = resolversSize; + for (int i = 0; i < sz; i++) { + Object result = resolvers[i].getValue(context, base, property); + if (context.isPropertyResolved()) { + return result; + } + } + return null; + } + + @Override + public Object invoke(ELContext context, Object base, Object method, Class[] paramTypes, Object[] params) { + context.setPropertyResolved(false); + int sz = this.resolversSize; + for (int i = 0; i < sz; i++) { + Object obj = this.resolvers[i].invoke(context, base, method, paramTypes, params); + if (context.isPropertyResolved()) { + return obj; + } + } + return null; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + context.setPropertyResolved(false); + int sz = this.resolversSize; + for (int i = 0; i < sz; i++) { + Class type = this.resolvers[i].getType(context, base, property); + if (context.isPropertyResolved()) { + if (SCOPED_ATTRIBUTE_EL_RESOLVER != null && + SCOPED_ATTRIBUTE_EL_RESOLVER.isAssignableFrom(resolvers[i].getClass())) { + // Special case since + // jakarta.servlet.jsp.el.ScopedAttributeELResolver will + // always return Object.class for type + Object value = resolvers[i].getValue(context, base, property); + if (value != null) { + return value.getClass(); + } + } + return type; + } + } + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + context.setPropertyResolved(false); + int sz = this.resolversSize; + for (int i = 0; i < sz; i++) { + this.resolvers[i].setValue(context, base, property, value); + if (context.isPropertyResolved()) { + return; + } + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + context.setPropertyResolved(false); + int sz = this.resolversSize; + for (int i = 0; i < sz; i++) { + boolean readOnly = this.resolvers[i].isReadOnly(context, base, property); + if (context.isPropertyResolved()) { + return readOnly; + } + } + return false; + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return new FeatureIterator(context, base, this.resolvers, this.resolversSize); + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + Class commonType = null; + int sz = this.resolversSize; + for (int i = 0; i < sz; i++) { + Class type = this.resolvers[i].getCommonPropertyType(context, base); + if (type != null && (commonType == null || commonType.isAssignableFrom(type))) { + commonType = type; + } + } + return commonType; + } + + @Override + public T convertToType(ELContext context, Object obj, Class type) { + context.setPropertyResolved(false); + int sz = typeConvertersSize; + for (int i = 0; i < sz; i++) { + T result = this.typeConverters[i].convertToType(context, obj, type); + if (context.isPropertyResolved()) { + return result; + } + } + return null; + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + private static final class FeatureIterator implements Iterator { + + private final ELContext context; + + private final Object base; + + private final ELResolver[] resolvers; + + private final int size; + + private Iterator itr; + + private int idx; + + private FeatureDescriptor next; + + FeatureIterator(ELContext context, Object base, ELResolver[] resolvers, int size) { + this.context = context; + this.base = base; + this.resolvers = resolvers; + this.size = size; + + this.idx = 0; + this.guaranteeIterator(); + } + + private void guaranteeIterator() { + while (this.itr == null && this.idx < this.size) { + this.itr = this.resolvers[this.idx].getFeatureDescriptors(this.context, this.base); + this.idx++; + } + } + + @Override + public boolean hasNext() { + if (this.next != null) { + return true; + } + if (this.itr != null) { + while (this.next == null && itr.hasNext()) { + this.next = itr.next(); + } + } else { + return false; + } + if (this.next == null) { + this.itr = null; + this.guaranteeIterator(); + } + return hasNext(); + } + + @Override + public FeatureDescriptor next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + FeatureDescriptor result = this.next; + this.next = null; + return result; + + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/java/jakarta/el/ELClass.java b/java/jakarta/el/ELClass.java new file mode 100644 index 0000000..0d00302 --- /dev/null +++ b/java/jakarta/el/ELClass.java @@ -0,0 +1,33 @@ +/* + * 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 jakarta.el; + +/** + * @since EL 3.0 + */ +public class ELClass { + + private final Class clazz; + + public ELClass(Class clazz) { + this.clazz = clazz; + } + + public Class getKlass() { + return clazz; + } +} diff --git a/java/jakarta/el/ELContext.java b/java/jakarta/el/ELContext.java new file mode 100644 index 0000000..12840d1 --- /dev/null +++ b/java/jakarta/el/ELContext.java @@ -0,0 +1,382 @@ +/* + * 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 jakarta.el; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +public abstract class ELContext { + + private Locale locale; + + private Map,Object> map; + + private boolean resolved; + + private ImportHandler importHandler = null; + + private List listeners; + + private Deque> lambdaArguments = new ArrayDeque<>(); + + public ELContext() { + this.resolved = false; + } + + public void setPropertyResolved(boolean resolved) { + this.resolved = resolved; + } + + /** + * Mark the given property as resolved and notify any interested listeners. + * + * @param base The base object on which the property was found + * @param property The property that was resolved + * + * @since EL 3.0 + */ + public void setPropertyResolved(Object base, Object property) { + setPropertyResolved(true); + notifyPropertyResolved(base, property); + } + + public boolean isPropertyResolved() { + return this.resolved; + } + + /** + * Add an object to this EL context under the given key. + * + * @param key The key under which to store the object + * @param contextObject The object to add + * + * @throws NullPointerException If the supplied key or context is null + */ + public void putContext(Class key, Object contextObject) { + Objects.requireNonNull(key); + Objects.requireNonNull(contextObject); + + if (this.map == null) { + this.map = new HashMap<>(); + } + + this.map.put(key, contextObject); + } + + /** + * Obtain the context object for the given key. + * + * @param key The key of the required context object + * + * @return The value of the context object associated with the given key + * + * @throws NullPointerException If the supplied key is null + */ + public Object getContext(Class key) { + Objects.requireNonNull(key); + if (this.map == null) { + return null; + } + return this.map.get(key); + } + + public abstract ELResolver getELResolver(); + + /** + * Obtain the ImportHandler for this ELContext, creating one if necessary. This method is not thread-safe. + * + * @return the ImportHandler for this ELContext. + * + * @since EL 3.0 + */ + public ImportHandler getImportHandler() { + if (importHandler == null) { + importHandler = new ImportHandler(); + } + return importHandler; + } + + public abstract FunctionMapper getFunctionMapper(); + + public Locale getLocale() { + return this.locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public abstract VariableMapper getVariableMapper(); + + /** + * Register an EvaluationListener with this ELContext. + * + * @param listener The EvaluationListener to register + * + * @since EL 3.0 + */ + public void addEvaluationListener(EvaluationListener listener) { + if (listeners == null) { + listeners = new ArrayList<>(); + } + + listeners.add(listener); + } + + /** + * Obtain the list of registered EvaluationListeners. + * + * @return A list of the EvaluationListener registered with this ELContext + * + * @since EL 3.0 + */ + public List getEvaluationListeners() { + return listeners == null ? Collections.emptyList() : listeners; + } + + /** + * Notify interested listeners that an expression will be evaluated. + * + * @param expression The expression that will be evaluated + * + * @since EL 3.0 + */ + public void notifyBeforeEvaluation(String expression) { + if (listeners == null) { + return; + } + + for (EvaluationListener listener : listeners) { + try { + listener.beforeEvaluation(this, expression); + } catch (Throwable t) { + Util.handleThrowable(t); + // Ignore - no option to log + } + } + } + + /** + * Notify interested listeners that an expression has been evaluated. + * + * @param expression The expression that was evaluated + * + * @since EL 3.0 + */ + public void notifyAfterEvaluation(String expression) { + if (listeners == null) { + return; + } + + for (EvaluationListener listener : listeners) { + try { + listener.afterEvaluation(this, expression); + } catch (Throwable t) { + Util.handleThrowable(t); + // Ignore - no option to log + } + } + } + + /** + * Notify interested listeners that a property has been resolved. + * + * @param base The object on which the property was resolved + * @param property The property that was resolved + * + * @since EL 3.0 + */ + public void notifyPropertyResolved(Object base, Object property) { + if (listeners == null) { + return; + } + + for (EvaluationListener listener : listeners) { + try { + listener.propertyResolved(this, base, property); + } catch (Throwable t) { + Util.handleThrowable(t); + // Ignore - no option to log + } + } + } + + /** + * Determine if the specified name is recognised as the name of a lambda argument. + * + * @param name The name of the lambda argument + * + * @return true if the name is recognised as the name of a lambda argument, otherwise + * false + * + * @since EL 3.0 + */ + public boolean isLambdaArgument(String name) { + for (Map arguments : lambdaArguments) { + if (arguments.containsKey(name)) { + return true; + } + } + return false; + } + + /** + * Obtain the value of the lambda argument with the given name. + * + * @param name The name of the lambda argument + * + * @return The value of the specified argument + * + * @since EL 3.0 + */ + public Object getLambdaArgument(String name) { + for (Map arguments : lambdaArguments) { + Object result = arguments.get(name); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Called when starting to evaluate a lambda expression so that the arguments are available to the EL context during + * evaluation. + * + * @param arguments The arguments in scope for the current lambda expression. + * + * @since EL 3.0 + */ + public void enterLambdaScope(Map arguments) { + lambdaArguments.push(arguments); + } + + /** + * Called after evaluating a lambda expression to signal that the arguments are no longer required. + * + * @since EL 3.0 + */ + public void exitLambdaScope() { + lambdaArguments.pop(); + } + + /** + * Coerce the supplied object to the requested type. + * + * @param The type to which the object should be coerced + * @param obj The object to be coerced + * @param type The type to which the object should be coerced + * + * @return An instance of the requested type. + * + * @throws ELException If the conversion fails + * + * @since EL 3.0 + */ + public T convertToType(Object obj, Class type) { + + boolean originalResolved = isPropertyResolved(); + setPropertyResolved(false); + try { + ELResolver resolver = getELResolver(); + if (resolver != null) { + T result = resolver.convertToType(this, obj, type); + if (isPropertyResolved()) { + return result; + } + } + } finally { + setPropertyResolved(originalResolved); + } + + if (obj instanceof LambdaExpression && isFunctionalInterface(type)) { + ((LambdaExpression) obj).setELContext(this); + } + + return ELManager.getExpressionFactory().coerceToType(obj, type); + } + + + /* + * Copied from org.apache.el.lang.ELSupport - keep in sync + */ + static boolean isFunctionalInterface(Class type) { + + if (!type.isInterface()) { + return false; + } + + boolean foundAbstractMethod = false; + Method[] methods = type.getMethods(); + for (Method method : methods) { + if (Modifier.isAbstract(method.getModifiers())) { + // Abstract methods that override one of the public methods + // of Object don't count + if (overridesObjectMethod(method)) { + continue; + } + if (foundAbstractMethod) { + // Found more than one + return false; + } else { + foundAbstractMethod = true; + } + } + } + return foundAbstractMethod; + } + + + /* + * Copied from org.apache.el.lang.ELSupport - keep in sync + */ + private static boolean overridesObjectMethod(Method method) { + // There are three methods that can be overridden + if ("equals".equals(method.getName())) { + if (method.getReturnType().equals(boolean.class)) { + if (method.getParameterCount() == 1) { + if (method.getParameterTypes()[0].equals(Object.class)) { + return true; + } + } + } + } else if ("hashCode".equals(method.getName())) { + if (method.getReturnType().equals(int.class)) { + if (method.getParameterCount() == 0) { + return true; + } + } + } else if ("toString".equals(method.getName())) { + if (method.getReturnType().equals(String.class)) { + if (method.getParameterCount() == 0) { + return true; + } + } + } + + return false; + } +} diff --git a/java/jakarta/el/ELContextEvent.java b/java/jakarta/el/ELContextEvent.java new file mode 100644 index 0000000..651eeea --- /dev/null +++ b/java/jakarta/el/ELContextEvent.java @@ -0,0 +1,36 @@ +/* + * 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 jakarta.el; + +import java.util.EventObject; + +public class ELContextEvent extends EventObject { + + private static final long serialVersionUID = 1255131906285426769L; + + /** + * @param source The EL context that was the source of this event + */ + public ELContextEvent(ELContext source) { + super(source); + } + + public ELContext getELContext() { + return (ELContext) this.getSource(); + } + +} diff --git a/java/jakarta/el/ELContextListener.java b/java/jakarta/el/ELContextListener.java new file mode 100644 index 0000000..9c7f735 --- /dev/null +++ b/java/jakarta/el/ELContextListener.java @@ -0,0 +1,26 @@ +/* + * 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 jakarta.el; + +/** + * @author Jacob Hookom [jacob/hookom.net] + */ +public interface ELContextListener extends java.util.EventListener { + + void contextCreated(ELContextEvent event); + +} diff --git a/java/jakarta/el/ELException.java b/java/jakarta/el/ELException.java new file mode 100644 index 0000000..ab18aee --- /dev/null +++ b/java/jakarta/el/ELException.java @@ -0,0 +1,62 @@ +/* + * 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 jakarta.el; + +/** + * Represents any of the exception conditions that can arise during expression evaluation. + * + * @since 2.1 + */ +public class ELException extends RuntimeException { + + private static final long serialVersionUID = -6228042809457459161L; + + /** + * Creates an ELException with no detail message + */ + public ELException() { + super(); + } + + /** + * Creates an ELException with the provided detail message. + * + * @param message the detail message + */ + public ELException(String message) { + super(message); + } + + /** + * Creates an ELException with the given cause + * + * @param cause the originating cause of this exception + */ + public ELException(Throwable cause) { + super(cause); + } + + /** + * Creates an ELException with the given detail message and root cause. + * + * @param message the detail message + * @param cause the originating cause of this exception + */ + public ELException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/jakarta/el/ELManager.java b/java/jakarta/el/ELManager.java new file mode 100644 index 0000000..4f55294 --- /dev/null +++ b/java/jakarta/el/ELManager.java @@ -0,0 +1,88 @@ +/* + * 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 jakarta.el; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * @since EL 3.0 + */ +public class ELManager { + + private StandardELContext context = null; + + public static ExpressionFactory getExpressionFactory() { + return Util.getExpressionFactory(); + } + + public StandardELContext getELContext() { + if (context == null) { + context = new StandardELContext(getExpressionFactory()); + } + + return context; + } + + public ELContext setELContext(ELContext context) { + StandardELContext oldContext = this.context; + this.context = new StandardELContext(context); + return oldContext; + } + + public void addBeanNameResolver(BeanNameResolver beanNameResolver) { + getELContext().addELResolver(new BeanNameELResolver(beanNameResolver)); + } + + public void addELResolver(ELResolver resolver) { + getELContext().addELResolver(resolver); + } + + public void mapFunction(String prefix, String function, Method method) { + getELContext().getFunctionMapper().mapFunction(prefix, function, method); + } + + public void setVariable(String variable, ValueExpression expression) { + getELContext().getVariableMapper().setVariable(variable, expression); + } + + public void importStatic(String staticMemberName) throws ELException { + getELContext().getImportHandler().importStatic(staticMemberName); + } + + public void importClass(String className) throws ELException { + getELContext().getImportHandler().importClass(className); + } + + public void importPackage(String packageName) { + getELContext().getImportHandler().importPackage(packageName); + } + + public Object defineBean(String name, Object bean) { + Map localBeans = getELContext().getLocalBeans(); + + if (bean == null) { + return localBeans.remove(name); + } else { + return localBeans.put(name, bean); + } + } + + public void addEvaluationListener(EvaluationListener listener) { + getELContext().addEvaluationListener(listener); + } +} diff --git a/java/jakarta/el/ELProcessor.java b/java/jakarta/el/ELProcessor.java new file mode 100644 index 0000000..02c502d --- /dev/null +++ b/java/jakarta/el/ELProcessor.java @@ -0,0 +1,340 @@ +/* + * 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 jakarta.el; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Set; + +/** + * @since EL 3.0 + */ +public class ELProcessor { + + private static final Set PRIMITIVES = new HashSet<>(); + static { + PRIMITIVES.add("boolean"); + PRIMITIVES.add("byte"); + PRIMITIVES.add("char"); + PRIMITIVES.add("double"); + PRIMITIVES.add("float"); + PRIMITIVES.add("int"); + PRIMITIVES.add("long"); + PRIMITIVES.add("short"); + } + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private final ELManager manager = new ELManager(); + private final ELContext context = manager.getELContext(); + private final ExpressionFactory factory = ELManager.getExpressionFactory(); + + + public ELManager getELManager() { + return manager; + } + + + public T eval(String expression) { + @SuppressWarnings("unchecked") + T result = (T) getValue(expression, Object.class); + return result; + } + + + public T getValue(String expression, Class expectedType) { + ValueExpression ve = factory.createValueExpression(context, bracket(expression), expectedType); + return ve.getValue(context); + } + + + public void setValue(String expression, Object value) { + ValueExpression ve = factory.createValueExpression(context, bracket(expression), Object.class); + ve.setValue(context, value); + } + + + public void setVariable(String variable, String expression) { + if (expression == null) { + manager.setVariable(variable, null); + } else { + ValueExpression ve = factory.createValueExpression(context, bracket(expression), Object.class); + manager.setVariable(variable, ve); + } + } + + + public void defineFunction(String prefix, String function, String className, String methodName) + throws ClassNotFoundException, NoSuchMethodException { + + if (prefix == null || function == null || className == null || methodName == null) { + throw new NullPointerException(Util.message(context, "elProcessor.defineFunctionNullParams")); + } + + // Check the imports + Class clazz = context.getImportHandler().resolveClass(className); + + if (clazz == null) { + clazz = Class.forName(className, true, Util.getContextClassLoader()); + } + + if (!Modifier.isPublic(clazz.getModifiers())) { + throw new ClassNotFoundException( + Util.message(context, "elProcessor.defineFunctionInvalidClass", className)); + } + + MethodSignature sig = new MethodSignature(context, methodName, className); + + if (function.length() == 0) { + function = sig.getName(); + } + + // Only returns public methods. Module access is checked below. + Method methods[] = clazz.getMethods(); + + for (Method method : methods) { + if (!Modifier.isStatic(method.getModifiers())) { + continue; + } + if (!Util.canAccess(null, method)) { + continue; + } + if (method.getName().equals(sig.getName())) { + if (sig.getParamTypeNames() == null) { + // Only a name provided, no signature so map the first + // method declared + manager.mapFunction(prefix, function, method); + return; + } + if (sig.getParamTypeNames().length != method.getParameterTypes().length) { + continue; + } + if (sig.getParamTypeNames().length == 0) { + manager.mapFunction(prefix, function, method); + return; + } else { + Class[] types = method.getParameterTypes(); + String[] typeNames = sig.getParamTypeNames(); + if (types.length == typeNames.length) { + boolean match = true; + for (int i = 0; i < types.length; i++) { + if (i == types.length - 1 && method.isVarArgs()) { + String typeName = typeNames[i]; + if (typeName.endsWith("...")) { + typeName = typeName.substring(0, typeName.length() - 3); + if (!typeName.equals(types[i].getName())) { + match = false; + } + } else { + match = false; + } + } else if (!types[i].getName().equals(typeNames[i])) { + match = false; + break; + } + } + if (match) { + manager.mapFunction(prefix, function, method); + return; + } + } + } + } + } + + throw new NoSuchMethodException( + Util.message(context, "elProcessor.defineFunctionNoMethod", methodName, className)); + } + + + /** + * Map a method to a function name. + * + * @param prefix Function prefix + * @param function Function name + * @param method Method + * + * @throws NullPointerException If any of the arguments are null + * @throws NoSuchMethodException If the method is not static + */ + public void defineFunction(String prefix, String function, Method method) throws NoSuchMethodException { + + if (prefix == null || function == null || method == null) { + throw new NullPointerException(Util.message(context, "elProcessor.defineFunctionNullParams")); + } + + int modifiers = method.getModifiers(); + + // Check for static, public method and module access + if (!Modifier.isStatic(modifiers) || !Util.canAccess(null, method)) { + throw new NoSuchMethodException(Util.message(context, "elProcessor.defineFunctionInvalidMethod", + method.getName(), method.getDeclaringClass().getName())); + } + + manager.mapFunction(prefix, function, method); + } + + + public void defineBean(String name, Object bean) { + manager.defineBean(name, bean); + } + + + private static String bracket(String expression) { + return "${" + expression + "}"; + } + + private static class MethodSignature { + + private final String name; + private final String[] parameterTypeNames; + + MethodSignature(ELContext context, String methodName, String className) throws NoSuchMethodException { + + int paramIndex = methodName.indexOf('('); + + if (paramIndex == -1) { + name = methodName.trim(); + parameterTypeNames = null; + } else { + String returnTypeAndName = methodName.substring(0, paramIndex).trim(); + // Assume that the return type and the name are separated by + // whitespace. Given the use of trim() above, there should only + // be one sequence of whitespace characters. + int wsPos = -1; + for (int i = 0; i < returnTypeAndName.length(); i++) { + if (Character.isWhitespace(returnTypeAndName.charAt(i))) { + wsPos = i; + break; + } + } + if (wsPos == -1) { + throw new NoSuchMethodException(); + } + name = returnTypeAndName.substring(wsPos).trim(); + + String paramString = methodName.substring(paramIndex).trim(); + // We know the params start with '(', check they end with ')' + if (!paramString.endsWith(")")) { + throw new NoSuchMethodException(Util.message(context, + "elProcessor.defineFunctionInvalidParameterList", paramString, methodName, className)); + } + // Trim '(' and ')' + paramString = paramString.substring(1, paramString.length() - 1).trim(); + if (paramString.length() == 0) { + parameterTypeNames = EMPTY_STRING_ARRAY; + } else { + parameterTypeNames = paramString.split(","); + ImportHandler importHandler = context.getImportHandler(); + for (int i = 0; i < parameterTypeNames.length; i++) { + String parameterTypeName = parameterTypeNames[i].trim(); + int dimension = 0; + int bracketPos = parameterTypeName.indexOf('['); + if (bracketPos > -1) { + String parameterTypeNameOnly = parameterTypeName.substring(0, bracketPos).trim(); + while (bracketPos > -1) { + dimension++; + bracketPos = parameterTypeName.indexOf('[', bracketPos + 1); + } + parameterTypeName = parameterTypeNameOnly; + } + boolean varArgs = false; + if (parameterTypeName.endsWith("...")) { + varArgs = true; + dimension = 1; + parameterTypeName = parameterTypeName.substring(0, parameterTypeName.length() - 3).trim(); + } + boolean isPrimitive = PRIMITIVES.contains(parameterTypeName); + if (isPrimitive && dimension > 0) { + // When in an array, class name changes for primitive + switch (parameterTypeName) { + case "boolean": + parameterTypeName = "Z"; + break; + case "byte": + parameterTypeName = "B"; + break; + case "char": + parameterTypeName = "C"; + break; + case "double": + parameterTypeName = "D"; + break; + case "float": + parameterTypeName = "F"; + break; + case "int": + parameterTypeName = "I"; + break; + case "long": + parameterTypeName = "J"; + break; + case "short": + parameterTypeName = "S"; + break; + default: + // Should never happen + break; + } + } else if (!isPrimitive && !parameterTypeName.contains(".")) { + Class clazz = importHandler.resolveClass(parameterTypeName); + if (clazz == null) { + throw new NoSuchMethodException( + Util.message(context, "elProcessor.defineFunctionInvalidParameterTypeName", + parameterTypeNames[i], methodName, className)); + } + parameterTypeName = clazz.getName(); + } + if (dimension > 0) { + // Convert to array form of class name + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < dimension; j++) { + sb.append('['); + } + if (!isPrimitive) { + sb.append('L'); + } + sb.append(parameterTypeName); + if (!isPrimitive) { + sb.append(';'); + } + parameterTypeName = sb.toString(); + } + if (varArgs) { + parameterTypeName += "..."; + } + parameterTypeNames[i] = parameterTypeName; + } + } + } + + } + + public String getName() { + return name; + } + + /** + * @return null if just the method name was specified, an empty List if an empty parameter list was + * specified - i.e. () - otherwise an ordered list of parameter type names + */ + public String[] getParamTypeNames() { + return parameterTypeNames; + } + } +} diff --git a/java/jakarta/el/ELResolver.java b/java/jakarta/el/ELResolver.java new file mode 100644 index 0000000..ee01d31 --- /dev/null +++ b/java/jakarta/el/ELResolver.java @@ -0,0 +1,159 @@ +/* + * 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 jakarta.el; + +import java.util.Iterator; + +/** + * @author Jacob Hookom [jacob/hookom.net] + */ +public abstract class ELResolver { + + public static final String TYPE = "type"; + + public static final String RESOLVABLE_AT_DESIGN_TIME = "resolvableAtDesignTime"; + + /** + * Obtain the value of the given property on the given object using the given context. + * + * @param context The EL context for this evaluation + * @param base The base object on which the property is to be found + * @param property The property whose value is to be returned + * + * @return the value of the provided property + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If the base/property combination provided to the resolver is one that the + * resolver can handle but no match was found or a match was found but was not + * readable + * @throws ELException Wraps any exception throw whilst resolving the property + */ + public abstract Object getValue(ELContext context, Object base, Object property); + + /** + * Invokes a method on the the given object. + * + * @param context The EL context for this evaluation + * @param base The base object on which the method is to be found + * @param method The method to invoke + * @param paramTypes The types of the parameters of the method to invoke + * @param params The parameters with which to invoke the method + * + * @return This default implementation always returns null + * + * @since EL 2.2 + */ + public Object invoke(ELContext context, Object base, Object method, Class[] paramTypes, Object[] params) { + return null; + } + + /** + * Obtain the most generally acceptable type that may be used to set the given property on the given object using + * the given context. + * + * @param context The EL context for this evaluation + * @param base The base object on which the property is to be found + * @param property The property whose type is to be returned + * + * @return the most general type that maybe used to set the provided property or {@code null} if the resolver is + * read-only. + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If the base/property combination provided to the resolver is one that the + * resolver can handle but no match was found or a match was found but was not + * readable + * @throws ELException Wraps any exception throw whilst resolving the property + */ + public abstract Class getType(ELContext context, Object base, Object property); + + /** + * Set the value of the given property on the given object using the given context. + * + * @param context The EL context for this evaluation + * @param base The base object on which the property is to be found + * @param property The property whose value is to be set + * @param value The value to set the property to + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If the base/property combination provided to the resolver is one that the + * resolver can handle but no match was found + * @throws PropertyNotWritableException If the base/property combination provided to the resolver is one that the + * resolver can handle but the property was not writable + * @throws ELException Wraps any exception throw whilst resolving the property + */ + public abstract void setValue(ELContext context, Object base, Object property, Object value); + + /** + * Determine if the given property on the given object is read-only using the given context. + * + * @param context The EL context for this evaluation + * @param base The base object on which the property is to be found + * @param property The property to be checked for read only status + * + * @return true if the identified property is read only, otherwise false + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If the base/property combination provided to the resolver is one that the + * resolver can handle but no match was found + * @throws ELException Wraps any exception throw whilst resolving the property + */ + public abstract boolean isReadOnly(ELContext context, Object base, Object property); + + /** + * Obtain the feature descriptors for the resolvable properties of the given object. + *

+ * The default implementation returns {@code null}. + * + * @param context The context in which the examination takes place + * @param base The object to examine + * + * @return An iterator, possibly empty, of feature descriptors of the given object + * + * @deprecated This method will be removed, without replacement, in EL 6.0 / Tomcat 11. + */ + @Deprecated(forRemoval = true, since = "EL 5.0") + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return null; + } + + /** + * Obtain the most common type that is acceptable for the given base object. + * + * @param context The context in which the examination takes place + * @param base The object to examine + * + * @return {code null} if the most common type cannot be determine, otherwise the most common type + */ + public abstract Class getCommonPropertyType(ELContext context, Object base); + + /** + * Converts the given object to the given type. + * + * @param The type to which the object should be converted + * @param context The EL context for this evaluation + * @param obj The object to convert + * @param type The type to which the object should be converted + * + * @return This default implementation always returns null + * + * @since EL 3.0 + */ + public T convertToType(ELContext context, Object obj, Class type) { + context.setPropertyResolved(false); + return null; + } +} diff --git a/java/jakarta/el/EvaluationListener.java b/java/jakarta/el/EvaluationListener.java new file mode 100644 index 0000000..68c3098 --- /dev/null +++ b/java/jakarta/el/EvaluationListener.java @@ -0,0 +1,54 @@ +/* + * 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 jakarta.el; + +/** + * @since EL 3.0 + */ +public abstract class EvaluationListener { + + /** + * Fired before the evaluation of the expression. + * + * @param context The EL context in which the expression will be evaluated + * @param expression The expression that will be evaluated + */ + public void beforeEvaluation(ELContext context, String expression) { + // NO-OP + } + + /** + * Fired after the evaluation of the expression. + * + * @param context The EL context in which the expression was evaluated + * @param expression The expression that was evaluated + */ + public void afterEvaluation(ELContext context, String expression) { + // NO-OP + } + + /** + * Fired after a property has been resolved. + * + * @param context The EL context in which the property was resolved + * @param base The base object on which the property was resolved + * @param property The property that was resolved + */ + public void propertyResolved(ELContext context, Object base, Object property) { + // NO-OP + } +} diff --git a/java/jakarta/el/Expression.java b/java/jakarta/el/Expression.java new file mode 100644 index 0000000..bc8950a --- /dev/null +++ b/java/jakarta/el/Expression.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.el; + +import java.io.Serializable; + +/** + * + */ +public abstract class Expression implements Serializable { + + private static final long serialVersionUID = -6663767980471823812L; + + public abstract String getExpressionString(); + + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract int hashCode(); + + public abstract boolean isLiteralText(); + +} diff --git a/java/jakarta/el/ExpressionFactory.java b/java/jakarta/el/ExpressionFactory.java new file mode 100644 index 0000000..f1b3720 --- /dev/null +++ b/java/jakarta/el/ExpressionFactory.java @@ -0,0 +1,378 @@ +/* + * 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 jakarta.el; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * @since 2.1 + */ +public abstract class ExpressionFactory { + + private static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); + + private static final String PROPERTY_NAME = "jakarta.el.ExpressionFactory"; + + private static final String PROPERTY_FILE; + + private static final CacheValue nullTcclFactory = new CacheValue(); + private static final Map factoryCache = new ConcurrentHashMap<>(); + + static { + if (IS_SECURITY_ENABLED) { + PROPERTY_FILE = + AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty("java.home") + + File.separator + "lib" + File.separator + "el.properties"); + } else { + PROPERTY_FILE = System.getProperty("java.home") + File.separator + "lib" + File.separator + "el.properties"; + } + } + + /** + * Create a new {@link ExpressionFactory}. The class to use is determined by the following search order: + *

    + *
  1. services API (META-INF/services/jakarta.el.ExpressionFactory)
  2. + *
  3. $JRE_HOME/lib/el.properties - key jakarta.el.ExpressionFactory
  4. + *
  5. jakarta.el.ExpressionFactory
  6. + *
  7. Platform default implementation - org.apache.el.ExpressionFactoryImpl
  8. + *
+ * + * @return the new ExpressionFactory + */ + public static ExpressionFactory newInstance() { + return newInstance(null); + } + + /** + * Create a new {@link ExpressionFactory} passing in the provided {@link Properties}. Search order is the same as + * {@link #newInstance()}. + * + * @param properties the properties to be passed to the new instance (may be null) + * + * @return the new ExpressionFactory + */ + public static ExpressionFactory newInstance(Properties properties) { + ExpressionFactory result = null; + + ClassLoader tccl = Util.getContextClassLoader(); + + CacheValue cacheValue; + Class clazz; + + if (tccl == null) { + cacheValue = nullTcclFactory; + } else { + CacheKey key = new CacheKey(tccl); + cacheValue = factoryCache.get(key); + if (cacheValue == null) { + CacheValue newCacheValue = new CacheValue(); + cacheValue = factoryCache.putIfAbsent(key, newCacheValue); + if (cacheValue == null) { + cacheValue = newCacheValue; + } + } + } + + final Lock readLock = cacheValue.getLock().readLock(); + readLock.lock(); + try { + clazz = cacheValue.getFactoryClass(); + } finally { + readLock.unlock(); + } + + if (clazz == null) { + String className = null; + try { + final Lock writeLock = cacheValue.getLock().writeLock(); + writeLock.lock(); + try { + className = cacheValue.getFactoryClassName(); + if (className == null) { + className = discoverClassName(tccl); + cacheValue.setFactoryClassName(className); + } + if (tccl == null) { + clazz = Class.forName(className); + } else { + clazz = tccl.loadClass(className); + } + cacheValue.setFactoryClass(clazz); + } finally { + writeLock.unlock(); + } + } catch (ClassNotFoundException e) { + throw new ELException(Util.message(null, "expressionFactory.cannotFind", className), e); + } + } + + try { + Constructor constructor = null; + // Do we need to look for a constructor that will take properties? + if (properties != null) { + try { + constructor = clazz.getConstructor(Properties.class); + } catch (SecurityException se) { + throw new ELException(se); + } catch (NoSuchMethodException nsme) { + // This can be ignored + // This is OK for this constructor not to exist + } + } + if (constructor == null) { + result = (ExpressionFactory) clazz.getConstructor().newInstance(); + } else { + result = (ExpressionFactory) constructor.newInstance(properties); + } + + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + Util.handleThrowable(cause); + throw new ELException(Util.message(null, "expressionFactory.cannotCreate", clazz.getName()), e); + } catch (ReflectiveOperationException | IllegalArgumentException e) { + throw new ELException(Util.message(null, "expressionFactory.cannotCreate", clazz.getName()), e); + } + + return result; + } + + /** + * Create a new value expression. + * + * @param context The EL context for this evaluation + * @param expression The String representation of the value expression + * @param expectedType The expected type of the result of evaluating the expression + * + * @return A new value expression formed from the input parameters + * + * @throws NullPointerException If the expected type is null + * @throws ELException If there are syntax errors in the provided expression + */ + public abstract ValueExpression createValueExpression(ELContext context, String expression, Class expectedType); + + public abstract ValueExpression createValueExpression(Object instance, Class expectedType); + + /** + * Create a new method expression instance. + * + * @param context The EL context for this evaluation + * @param expression The String representation of the method expression + * @param expectedReturnType The expected type of the result of invoking the method + * @param expectedParamTypes The expected types of the input parameters + * + * @return A new method expression formed from the input parameters. + * + * @throws NullPointerException If the expected parameters types are null + * @throws ELException If there are syntax errors in the provided expression + */ + public abstract MethodExpression createMethodExpression(ELContext context, String expression, + Class expectedReturnType, Class[] expectedParamTypes); + + /** + * Coerce the supplied object to the requested type. + * + * @param The type to which the object should be coerced + * @param obj The object to be coerced + * @param expectedType The type to which the object should be coerced + * + * @return An instance of the requested type. + * + * @throws ELException If the conversion fails + */ + public abstract T coerceToType(Object obj, Class expectedType); + + /** + * @return This default implementation returns null + * + * @since EL 3.0 + */ + public ELResolver getStreamELResolver() { + return null; + } + + /** + * @return This default implementation returns null + * + * @since EL 3.0 + */ + public Map getInitFunctionMap() { + return null; + } + + /** + * Key used to cache ExpressionFactory discovery information per class loader. The class loader reference is never + * {@code null}, because {@code null} tccl is handled separately. + */ + private static class CacheKey { + private final int hash; + private final WeakReference ref; + + CacheKey(ClassLoader cl) { + hash = cl.hashCode(); + ref = new WeakReference<>(cl); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof CacheKey)) { + return false; + } + ClassLoader thisCl = ref.get(); + if (thisCl == null) { + return false; + } + return thisCl == ((CacheKey) obj).ref.get(); + } + } + + private static class CacheValue { + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private String className; + private WeakReference> ref; + + CacheValue() { + } + + public ReadWriteLock getLock() { + return lock; + } + + public String getFactoryClassName() { + return className; + } + + public void setFactoryClassName(String className) { + this.className = className; + } + + public Class getFactoryClass() { + return ref != null ? ref.get() : null; + } + + public void setFactoryClass(Class clazz) { + ref = new WeakReference<>(clazz); + } + } + + /** + * Discover the name of class that implements ExpressionFactory. + * + * @param tccl {@code ClassLoader} + * + * @return Class name. There is default, so it is never {@code null}. + */ + private static String discoverClassName(ClassLoader tccl) { + String className = null; + + // First services API + className = getClassNameServices(tccl); + if (className == null) { + if (IS_SECURITY_ENABLED) { + className = + AccessController.doPrivileged((PrivilegedAction) ExpressionFactory::getClassNameJreDir); + } else { + // Second el.properties file + className = getClassNameJreDir(); + } + } + if (className == null) { + if (IS_SECURITY_ENABLED) { + className = AccessController + .doPrivileged((PrivilegedAction) ExpressionFactory::getClassNameSysProp); + } else { + // Third system property + className = getClassNameSysProp(); + } + } + if (className == null) { + // Fourth - default + className = "org.apache.el.ExpressionFactoryImpl"; + } + return className; + } + + private static String getClassNameServices(ClassLoader tccl) { + + ExpressionFactory result = null; + + ServiceLoader serviceLoader = ServiceLoader.load(ExpressionFactory.class, tccl); + Iterator iter = serviceLoader.iterator(); + while (result == null && iter.hasNext()) { + result = iter.next(); + } + + if (result == null) { + return null; + } + + return result.getClass().getName(); + } + + private static String getClassNameJreDir() { + File file = new File(PROPERTY_FILE); + if (file.canRead()) { + try (InputStream is = new FileInputStream(file)) { + Properties props = new Properties(); + props.load(is); + String value = props.getProperty(PROPERTY_NAME); + if (value != null && value.trim().length() > 0) { + return value.trim(); + } + } catch (FileNotFoundException e) { + // Should not happen - ignore it if it does + } catch (IOException e) { + throw new ELException(Util.message(null, "expressionFactory.readFailed", PROPERTY_FILE), e); + } + } + return null; + } + + private static String getClassNameSysProp() { + String value = System.getProperty(PROPERTY_NAME); + if (value != null && value.trim().length() > 0) { + return value.trim(); + } + return null; + } + +} diff --git a/java/jakarta/el/FunctionMapper.java b/java/jakarta/el/FunctionMapper.java new file mode 100644 index 0000000..036f12b --- /dev/null +++ b/java/jakarta/el/FunctionMapper.java @@ -0,0 +1,37 @@ +/* + * 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 jakarta.el; + +import java.lang.reflect.Method; + +public abstract class FunctionMapper { + + public abstract Method resolveFunction(String prefix, String localName); + + /** + * Map a method to a function name. + * + * @param prefix Function prefix + * @param localName Function name + * @param method Method + * + * @since EL 3.0 + */ + public void mapFunction(String prefix, String localName, Method method) { + // NO-OP + } +} diff --git a/java/jakarta/el/ImportHandler.java b/java/jakarta/el/ImportHandler.java new file mode 100644 index 0000000..70c9555 --- /dev/null +++ b/java/jakarta/el/ImportHandler.java @@ -0,0 +1,480 @@ +/* + * 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 jakarta.el; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @since EL 3.0 + */ +public class ImportHandler { + + private static final Map> standardPackages = new HashMap<>(); + + static { + // Servlet 6.0 + Set servletClassNames = new HashSet<>(); + // Interfaces + servletClassNames.add("AsyncContext"); + servletClassNames.add("AsyncListener"); + servletClassNames.add("Filter"); + servletClassNames.add("FilterChain"); + servletClassNames.add("FilterConfig"); + servletClassNames.add("FilterRegistration"); + servletClassNames.add("FilterRegistration.Dynamic"); + servletClassNames.add("ReadListener"); + servletClassNames.add("Registration"); + servletClassNames.add("Registration.Dynamic"); + servletClassNames.add("RequestDispatcher"); + servletClassNames.add("Servlet"); + servletClassNames.add("ServletConfig"); + servletClassNames.add("ServletConnection"); + servletClassNames.add("ServletContainerInitializer"); + servletClassNames.add("ServletContext"); + servletClassNames.add("ServletContextAttributeListener"); + servletClassNames.add("ServletContextListener"); + servletClassNames.add("ServletRegistration"); + servletClassNames.add("ServletRegistration.Dynamic"); + servletClassNames.add("ServletRequest"); + servletClassNames.add("ServletRequestAttributeListener"); + servletClassNames.add("ServletRequestListener"); + servletClassNames.add("ServletResponse"); + servletClassNames.add("SessionCookieConfig"); + servletClassNames.add("WriteListener"); + // Classes + servletClassNames.add("AsyncEvent"); + servletClassNames.add("GenericFilter"); + servletClassNames.add("GenericServlet"); + servletClassNames.add("HttpConstraintElement"); + servletClassNames.add("HttpMethodConstraintElement"); + servletClassNames.add("MultipartConfigElement"); + servletClassNames.add("ServletContextAttributeEvent"); + servletClassNames.add("ServletContextEvent"); + servletClassNames.add("ServletInputStream"); + servletClassNames.add("ServletOutputStream"); + servletClassNames.add("ServletRequestAttributeEvent"); + servletClassNames.add("ServletRequestEvent"); + servletClassNames.add("ServletRequestWrapper"); + servletClassNames.add("ServletResponseWrapper"); + servletClassNames.add("ServletSecurityElement"); + // Enums + servletClassNames.add("DispatcherType"); + servletClassNames.add("SessionTrackingMode"); + // Exceptions + servletClassNames.add("ServletException"); + servletClassNames.add("UnavailableException"); + standardPackages.put("jakarta.servlet", servletClassNames); + + // Servlet 6.0 + Set servletHttpClassNames = new HashSet<>(); + // Interfaces + servletHttpClassNames.add("HttpServletMapping"); + servletHttpClassNames.add("HttpServletRequest"); + servletHttpClassNames.add("HttpServletResponse"); + servletHttpClassNames.add("HttpSession"); + servletHttpClassNames.add("HttpSessionActivationListener"); + servletHttpClassNames.add("HttpSessionAttributeListener"); + servletHttpClassNames.add("HttpSessionBindingListener"); + servletHttpClassNames.add("HttpSessionIdListener"); + servletHttpClassNames.add("HttpSessionListener"); + servletHttpClassNames.add("HttpUpgradeHandler"); + servletHttpClassNames.add("Part"); + servletHttpClassNames.add("PushBuilder"); + servletHttpClassNames.add("WebConnection"); + // Classes + servletHttpClassNames.add("Cookie"); + servletHttpClassNames.add("HttpFilter"); + servletHttpClassNames.add("HttpServlet"); + servletHttpClassNames.add("HttpServletRequestWrapper"); + servletHttpClassNames.add("HttpServletResponseWrapper"); + servletHttpClassNames.add("HttpSessionBindingEvent"); + servletHttpClassNames.add("HttpSessionEvent"); + servletHttpClassNames.add("HttpUtils"); + // Enums + servletHttpClassNames.add("MappingMatch"); + standardPackages.put("jakarta.servlet.http", servletHttpClassNames); + + // JSP 3.0 + Set servletJspClassNames = new HashSet<>(); + // Interfaces + servletJspClassNames.add("HttpJspPage"); + servletJspClassNames.add("JspApplicationContext"); + servletJspClassNames.add("JspPage"); + // Classes + servletJspClassNames.add("ErrorData"); + servletJspClassNames.add("JspContext"); + servletJspClassNames.add("JspEngineInfo"); + servletJspClassNames.add("JspFactory"); + servletJspClassNames.add("JspWriter"); + servletJspClassNames.add("PageContext"); + servletJspClassNames.add("Exceptions"); + servletJspClassNames.add("JspException"); + servletJspClassNames.add("JspTagException"); + servletJspClassNames.add("SkipPageException"); + standardPackages.put("jakarta.servlet.jsp", servletJspClassNames); + + Set javaLangClassNames = new HashSet<>(); + // Based on Java 21 EA29 + // Interfaces + javaLangClassNames.add("Appendable"); + javaLangClassNames.add("AutoCloseable"); + javaLangClassNames.add("CharSequence"); + javaLangClassNames.add("Cloneable"); + javaLangClassNames.add("Comparable"); + javaLangClassNames.add("Iterable"); + javaLangClassNames.add("ProcessHandle"); + javaLangClassNames.add("ProcessHandle.Info"); + javaLangClassNames.add("Readable"); + javaLangClassNames.add("Runnable"); + javaLangClassNames.add("StackWalker.StackFrame"); + javaLangClassNames.add("System.Logger"); + javaLangClassNames.add("Thread.Builder"); + javaLangClassNames.add("Thread.Builder.OfPlatform"); + javaLangClassNames.add("Thread.Builder.OfVirtual"); + javaLangClassNames.add("Thread.UncaughtExceptionHandler"); + // Classes + javaLangClassNames.add("Boolean"); + javaLangClassNames.add("Byte"); + javaLangClassNames.add("Character"); + javaLangClassNames.add("Character.Subset"); + javaLangClassNames.add("Character.UnicodeBlock"); + javaLangClassNames.add("Class"); + javaLangClassNames.add("ClassLoader"); + javaLangClassNames.add("ClassValue"); + javaLangClassNames.add("Compiler"); + javaLangClassNames.add("Double"); + javaLangClassNames.add("Enum"); + javaLangClassNames.add("Enum.EnumDesc"); + javaLangClassNames.add("Float"); + javaLangClassNames.add("InheritableThreadLocal"); + javaLangClassNames.add("Integer"); + javaLangClassNames.add("Long"); + javaLangClassNames.add("Math"); + javaLangClassNames.add("Module"); + javaLangClassNames.add("ModuleLayer"); + javaLangClassNames.add("ModuleLayer.Controller"); + javaLangClassNames.add("Number"); + javaLangClassNames.add("Object"); + javaLangClassNames.add("Package"); + javaLangClassNames.add("Process"); + javaLangClassNames.add("ProcessBuilder"); + javaLangClassNames.add("ProcessBuilder.Redirect"); + javaLangClassNames.add("Record"); + javaLangClassNames.add("Runtime"); + javaLangClassNames.add("Runtime.Version"); + javaLangClassNames.add("RuntimePermission"); + javaLangClassNames.add("ScopedValue"); + javaLangClassNames.add("ScopedValue.Carrier"); + javaLangClassNames.add("SecurityManager"); + javaLangClassNames.add("Short"); + javaLangClassNames.add("StackTraceElement"); + javaLangClassNames.add("StackWalker"); + javaLangClassNames.add("StrictMath"); + javaLangClassNames.add("String"); + javaLangClassNames.add("StringBuffer"); + javaLangClassNames.add("StringBuilder"); + javaLangClassNames.add("StringTemplate"); + javaLangClassNames.add("StringTemplate.Processor"); + javaLangClassNames.add("StringTemplate.Processor.Linkage"); + javaLangClassNames.add("System"); + javaLangClassNames.add("System.LoggerFinder"); + javaLangClassNames.add("Thread"); + javaLangClassNames.add("ThreadGroup"); + javaLangClassNames.add("ThreadLocal"); + javaLangClassNames.add("Throwable"); + javaLangClassNames.add("Void"); + // Enums + javaLangClassNames.add("Character.UnicodeScript"); + javaLangClassNames.add("ProcessBuilder.Redirect.Type"); + javaLangClassNames.add("StackWalker.Option"); + javaLangClassNames.add("System.Logger.Level"); + javaLangClassNames.add("Thread.State"); + // Exceptions + javaLangClassNames.add("ArithmeticException"); + javaLangClassNames.add("ArrayIndexOutOfBoundsException"); + javaLangClassNames.add("ArrayStoreException"); + javaLangClassNames.add("ClassCastException"); + javaLangClassNames.add("ClassNotFoundException"); + javaLangClassNames.add("CloneNotSupportedException"); + javaLangClassNames.add("EnumConstantNotPresentException"); + javaLangClassNames.add("Exception"); + javaLangClassNames.add("IllegalAccessException"); + javaLangClassNames.add("IllegalArgumentException"); + javaLangClassNames.add("IllegalCallerException"); + javaLangClassNames.add("IllegalMonitorStateException"); + javaLangClassNames.add("IllegalStateException"); + javaLangClassNames.add("IllegalThreadStateException"); + javaLangClassNames.add("IndexOutOfBoundsException"); + javaLangClassNames.add("InstantiationException"); + javaLangClassNames.add("InterruptedException"); + javaLangClassNames.add("LayerInstantiationException"); + javaLangClassNames.add("MatchException"); + javaLangClassNames.add("NegativeArraySizeException"); + javaLangClassNames.add("NoSuchFieldException"); + javaLangClassNames.add("NoSuchMethodException"); + javaLangClassNames.add("NullPointerException"); + javaLangClassNames.add("NumberFormatException"); + javaLangClassNames.add("ReflectiveOperationException"); + javaLangClassNames.add("RuntimeException"); + javaLangClassNames.add("SecurityException"); + javaLangClassNames.add("StringIndexOutOfBoundsException"); + javaLangClassNames.add("TypeNotPresentException"); + javaLangClassNames.add("UnsupportedOperationException"); + javaLangClassNames.add("WrongThreadException"); + // Errors + javaLangClassNames.add("AbstractMethodError"); + javaLangClassNames.add("AssertionError"); + javaLangClassNames.add("BootstrapMethodError"); + javaLangClassNames.add("ClassCircularityError"); + javaLangClassNames.add("ClassFormatError"); + javaLangClassNames.add("Error"); + javaLangClassNames.add("ExceptionInInitializerError"); + javaLangClassNames.add("IllegalAccessError"); + javaLangClassNames.add("IncompatibleClassChangeError"); + javaLangClassNames.add("InstantiationError"); + javaLangClassNames.add("InternalError"); + javaLangClassNames.add("LinkageError"); + javaLangClassNames.add("NoClassDefFoundError"); + javaLangClassNames.add("NoSuchFieldError"); + javaLangClassNames.add("NoSuchMethodError"); + javaLangClassNames.add("OutOfMemoryError"); + javaLangClassNames.add("StackOverflowError"); + javaLangClassNames.add("ThreadDeath"); + javaLangClassNames.add("UnknownError"); + javaLangClassNames.add("UnsatisfiedLinkError"); + javaLangClassNames.add("UnsupportedClassVersionError"); + javaLangClassNames.add("VerifyError"); + javaLangClassNames.add("VirtualMachineError"); + // Annotation Types + javaLangClassNames.add("Deprecated"); + javaLangClassNames.add("FunctionalInterface"); + javaLangClassNames.add("Override"); + javaLangClassNames.add("SafeVarargs"); + javaLangClassNames.add("SuppressWarnings"); + standardPackages.put("java.lang", javaLangClassNames); + + } + + private Map> packageNames = new ConcurrentHashMap<>(); + private Map classNames = new ConcurrentHashMap<>(); + private Map> clazzes = new ConcurrentHashMap<>(); + private Map> statics = new ConcurrentHashMap<>(); + + + public ImportHandler() { + importPackage("java.lang"); + } + + + public void importStatic(String name) throws ELException { + int lastPeriod = name.lastIndexOf('.'); + + if (lastPeriod < 0) { + throw new ELException(Util.message(null, "importHandler.invalidStaticName", name)); + } + + String className = name.substring(0, lastPeriod); + String fieldOrMethodName = name.substring(lastPeriod + 1); + + Class clazz = findClass(className, true); + + if (clazz == null) { + throw new ELException(Util.message(null, "importHandler.invalidClassNameForStatic", className, name)); + } + + boolean found = false; + + for (Field field : clazz.getFields()) { + if (field.getName().equals(fieldOrMethodName)) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) { + found = true; + break; + } + } + } + + if (!found) { + for (Method method : clazz.getMethods()) { + if (method.getName().equals(fieldOrMethodName)) { + int modifiers = method.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) { + found = true; + break; + } + } + } + } + + if (!found) { + throw new ELException( + Util.message(null, "importHandler.staticNotFound", fieldOrMethodName, className, name)); + } + + Class conflict = statics.get(fieldOrMethodName); + if (conflict != null) { + throw new ELException(Util.message(null, "importHandler.ambiguousStaticImport", name, + conflict.getName() + '.' + fieldOrMethodName)); + } + + statics.put(fieldOrMethodName, clazz); + } + + + public void importClass(String name) throws ELException { + int lastPeriodIndex = name.lastIndexOf('.'); + + if (lastPeriodIndex < 0) { + throw new ELException(Util.message(null, "importHandler.invalidClassName", name)); + } + + String unqualifiedName = name.substring(lastPeriodIndex + 1); + String currentName = classNames.putIfAbsent(unqualifiedName, name); + + if (currentName != null && !currentName.equals(name)) { + // Conflict. Same unqualifiedName, different fully qualified names + throw new ELException(Util.message(null, "importHandler.ambiguousImport", name, currentName)); + } + } + + + public void importPackage(String name) { + // Import ambiguity is handled at resolution, not at import + // Whether the package exists is not checked, + // a) for sake of performance when used in JSPs (BZ 57142), + // b) java.lang.Package.getPackage(name) is not reliable (BZ 57574), + // c) such check is not required by specification. + Set preloaded = standardPackages.get(name); + if (preloaded == null) { + packageNames.put(name, Collections.emptySet()); + } else { + packageNames.put(name, preloaded); + } + } + + + public Class resolveClass(String name) { + if (name == null || name.contains(".")) { + return null; + } + + // Has it been previously resolved? + Class result = clazzes.get(name); + + if (result != null) { + if (NotFound.class.equals(result)) { + return null; + } else { + return result; + } + } + + // Search the class imports + String className = classNames.get(name); + if (className != null) { + Class clazz = findClass(className, true); + if (clazz != null) { + clazzes.put(name, clazz); + return clazz; + } + } + + // Search the package imports - note there may be multiple matches + // (which correctly triggers an error) + for (Map.Entry> entry : packageNames.entrySet()) { + if (!entry.getValue().isEmpty()) { + // Standard package where we know all the class names + if (!entry.getValue().contains(name)) { + // Requested name isn't in the list so it isn't in this + // package so move on to next package. This allows the + // class loader look-up to be skipped. + continue; + } + } + className = entry.getKey() + '.' + name; + Class clazz = findClass(className, false); + if (clazz != null) { + if (result != null) { + throw new ELException( + Util.message(null, "importHandler.ambiguousImport", className, result.getName())); + } + result = clazz; + } + } + if (result == null) { + // Cache NotFound results to save repeated calls to findClass() + // which is relatively slow + clazzes.put(name, NotFound.class); + } else { + clazzes.put(name, result); + } + + return result; + } + + + public Class resolveStatic(String name) { + return statics.get(name); + } + + + private Class findClass(String name, boolean throwException) { + Class clazz; + ClassLoader cl = Util.getContextClassLoader(); + try { + clazz = cl.loadClass(name); + } catch (ClassNotFoundException e) { + return null; + } + + // Class must be public, non-abstract, not an interface and in an + // exported package + int modifiers = clazz.getModifiers(); + if (!Modifier.isPublic(modifiers) || Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) || + !isExported(clazz)) { + if (throwException) { + throw new ELException(Util.message(null, "importHandler.invalidClass", name)); + } else { + return null; + } + } + + return clazz; + } + + + private static boolean isExported(Class type) { + String packageName = type.getPackage().getName(); + Module module = type.getModule(); + return module.isExported(packageName); + } + + + /* + * Marker class used because null values are not permitted in a ConcurrentHashMap. + */ + private static class NotFound { + } +} diff --git a/java/jakarta/el/LambdaExpression.java b/java/jakarta/el/LambdaExpression.java new file mode 100644 index 0000000..fe7d9ca --- /dev/null +++ b/java/jakarta/el/LambdaExpression.java @@ -0,0 +1,87 @@ +/* + * 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 jakarta.el; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class LambdaExpression { + + private final List formalParameters; + private final ValueExpression expression; + private final Map nestedArguments = new HashMap<>(); + private ELContext context = null; + + public LambdaExpression(List formalParameters, ValueExpression expression) { + this.formalParameters = formalParameters; + this.expression = expression; + + } + + public void setELContext(ELContext context) { + this.context = context; + } + + @SuppressWarnings("null") // args[i] can't be null due to earlier checks + public Object invoke(ELContext context, Object... args) throws ELException { + + Objects.requireNonNull(context); + + int formalParamCount = 0; + if (formalParameters != null) { + formalParamCount = formalParameters.size(); + } + + int argCount = 0; + if (args != null) { + argCount = args.length; + } + + if (formalParamCount > argCount) { + throw new ELException(Util.message(context, "lambdaExpression.tooFewArgs", Integer.valueOf(argCount), + Integer.valueOf(formalParamCount))); + } + + // Build the argument map + // Start with the arguments from any outer expressions so if there is + // any overlap the local arguments have priority + Map lambdaArguments = new HashMap<>(nestedArguments); + for (int i = 0; i < formalParamCount; i++) { + lambdaArguments.put(formalParameters.get(i), args[i]); + } + + context.enterLambdaScope(lambdaArguments); + + try { + Object result = expression.getValue(context); + // Make arguments from this expression available to any nested + // expression + if (result instanceof LambdaExpression) { + ((LambdaExpression) result).nestedArguments.putAll(lambdaArguments); + } + return result; + } finally { + context.exitLambdaScope(); + } + } + + public Object invoke(Object... args) { + return invoke(context, args); + } +} diff --git a/java/jakarta/el/ListELResolver.java b/java/jakarta/el/ListELResolver.java new file mode 100644 index 0000000..3cdcd5c --- /dev/null +++ b/java/jakarta/el/ListELResolver.java @@ -0,0 +1,159 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +public class ListELResolver extends ELResolver { + + private final boolean readOnly; + + // TODO - Handle the Lists created by List.of(). Multiple package private + // classes. Java 9 + so a back-port would require JreCompat. + private static final Class UNMODIFIABLE = Collections.unmodifiableList(new ArrayList<>()).getClass(); + + public ListELResolver() { + this.readOnly = false; + } + + public ListELResolver(boolean readOnly) { + this.readOnly = readOnly; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof List) { + context.setPropertyResolved(base, property); + List list = (List) base; + int idx = coerce(property); + if (idx < 0 || idx >= list.size()) { + throw new PropertyNotFoundException(new ArrayIndexOutOfBoundsException(idx).getMessage()); + } + + /* + * Not perfect as a custom list implementation may be read-only but consistent with isReadOnly(). + */ + if (list.getClass() == UNMODIFIABLE || readOnly) { + return null; + } + + return Object.class; + } + + return null; + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof List) { + context.setPropertyResolved(base, property); + List list = (List) base; + int idx = coerce(property); + if (idx < 0 || idx >= list.size()) { + return null; + } + return list.get(idx); + } + + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + + if (base instanceof List) { + context.setPropertyResolved(base, property); + @SuppressWarnings("unchecked") // Must be OK to cast to Object + List list = (List) base; + + if (this.readOnly) { + throw new PropertyNotWritableException( + Util.message(context, "resolverNotWritable", base.getClass().getName())); + } + + int idx = coerce(property); + try { + list.set(idx, value); + } catch (UnsupportedOperationException e) { + throw new PropertyNotWritableException(e); + } catch (IndexOutOfBoundsException e) { + throw new PropertyNotFoundException(e); + } + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof List) { + context.setPropertyResolved(base, property); + List list = (List) base; + try { + int idx = coerce(property); + if (idx < 0 || idx >= list.size()) { + throw new PropertyNotFoundException(new ArrayIndexOutOfBoundsException(idx).getMessage()); + } + } catch (IllegalArgumentException e) { + // ignore + } + return this.readOnly || UNMODIFIABLE.equals(list.getClass()); + } + + return this.readOnly; + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base instanceof List) { // implies base != null + return Integer.class; + } + return null; + } + + private static int coerce(Object property) { + if (property instanceof Number) { + return ((Number) property).intValue(); + } + if (property instanceof Character) { + return ((Character) property).charValue(); + } + if (property instanceof Boolean) { + return ((Boolean) property).booleanValue() ? 1 : 0; + } + if (property instanceof String) { + return Integer.parseInt((String) property); + } + throw new IllegalArgumentException(property != null ? property.toString() : "null"); + } +} diff --git a/java/jakarta/el/LocalStrings.properties b/java/jakarta/el/LocalStrings.properties new file mode 100644 index 0000000..e0bc40d --- /dev/null +++ b/java/jakarta/el/LocalStrings.properties @@ -0,0 +1,53 @@ +# 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. + +beanNameELResolver.beanReadOnly=The bean name [{0}] is read-only + +elProcessor.defineFunctionInvalidClass=The class [{0}] is not public +elProcessor.defineFunctionInvalidMethod=The method [{0}] on class [{1}] is not a public static method +elProcessor.defineFunctionInvalidParameterList=The parameter list [{0}] for method [{1}] on class [{2}] is not valid +elProcessor.defineFunctionInvalidParameterTypeName=The parameter type [{0}] for method [{1}] on class [{2}] is not valid +elProcessor.defineFunctionNoMethod=A public static method [{0}] on class [{1}] could not be found +elProcessor.defineFunctionNullParams=One or more of the input parameters was null + +expressionFactory.cannotCreate=Unable to create ExpressionFactory of type [{0}] +expressionFactory.cannotFind=Unable to find ExpressionFactory of type [{0}] +expressionFactory.readFailed=Failed to read [{0}] + +importHandler.ambiguousImport=The class [{0}] could not be imported as it conflicts with [{1}] which has already been imported +importHandler.ambiguousStaticImport=The static import [{0}] could not be processed as it conflicts with [{1}] which has already been imported +importHandler.classNotFound=The class [{0}] could not be imported as it could not be found +importHandler.invalidClass=The class [{0}] must be public, in an exported package, non-abstract and not an interface +importHandler.invalidClassName=Name of class to import [{0}] must include a package +importHandler.invalidClassNameForStatic=The class [{0}] specified for static import [{1}] is not valid +importHandler.invalidStaticName=Name of static method or field to import [{0}] must include a class +importHandler.staticNotFound=The static import [{0}] could not be found in class [{1}] for import [{2}] + +lambdaExpression.tooFewArgs=Only [{0}] arguments were provided for a lambda expression that requires at least [{1}] + +objectNotAssignable=Unable to add an object of type [{0}] to an array of objects of type [{1}] +propertyNotFound=Property [{1}] not found on type [{0}] +propertyNotReadable=Property [{1}] not readable on type [{0}] +propertyNotWritable=Property [{1}] not writable on type [{0}] +propertyReadError=Error reading [{1}] on type [{0}] +propertyWriteError=Error writing [{1}] on type [{0}] +resolverNotWritable=ELResolver not writable for type [{0}] + +staticFieldELResolver.methodNotFound=No matching public static method named [{0}] found on class [{1}] +staticFieldELResolver.notFound=No public static field named [{0}] was found on exported class [{1}] +staticFieldELResolver.notWritable=Writing to static fields (in this case field [{0}] on class [{1}]) is not permitted + +util.method.ambiguous=Unable to find unambiguous method: {0}.{1}({2}) +util.method.notfound=Method not found: {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_cs.properties b/java/jakarta/el/LocalStrings_cs.properties new file mode 100644 index 0000000..ad96a24 --- /dev/null +++ b/java/jakarta/el/LocalStrings_cs.properties @@ -0,0 +1,52 @@ +# 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. + +beanNameELResolver.beanReadOnly=Název objektu typu bean [{0}] jen pro Ätení + +elProcessor.defineFunctionInvalidClass=Třída [{0}] není veÅ™ejná +elProcessor.defineFunctionInvalidMethod=Metoda [{0}] na třídÄ› [{1}] není veÅ™ejnou statickou metodou +elProcessor.defineFunctionInvalidParameterList=Seznam parametrů [{0}] pro metodu [{1}] na třídÄ› [{2}] není platný +elProcessor.defineFunctionInvalidParameterTypeName=Typ parametru [{0}] pro metodu [{1}] na třídÄ› [{2}] není platný +elProcessor.defineFunctionNoMethod=VeÅ™ejnou statickou metodu [{0}] pro třídu [{1}] nelze nalézt +elProcessor.defineFunctionNullParams=NejménÄ› jeden ze vstupních parametrů byl null + +expressionFactory.cannotCreate=Nelze vytvoÅ™it ExpressionFactory typu [{0}] +expressionFactory.cannotFind=Nelze najít ExpressionFactory typu [{0}] +expressionFactory.readFailed=NezdaÅ™ilo se pÅ™eÄíst [{0}] + +importHandler.ambiguousImport=Třídu [{0}] nelze importovat, protože je v konfliktu s [{1}], který již byl importován +importHandler.ambiguousStaticImport=Statický import [{0}] nelze zpracovat, protože je v konfliktu s [{1}], který již byl importován +importHandler.classNotFound=Třídu [{0}] nelze importovat, protože ji nelze nalézt +importHandler.invalidClass=Třída [{0}] musí být veÅ™ejná, v exportovaném balíku (pro Java 9+), neabstraktní a nesmí být rozhraním +importHandler.invalidClassName=Název třídy pro import [{0}] musí obsahovat balík +importHandler.invalidClassNameForStatic=Třída [{0}] urÄená pro statický import [{1}] není platná +importHandler.invalidStaticName=Název statické metody nebo pole pro import [{0}] musí obsahovat třídu +importHandler.staticNotFound=Statický import [{0}] nebyl nalezen ve třídÄ› [{1}] pro import [{2}] + +lambdaExpression.tooFewArgs=Pouze [{0}] argumentů bylo poskytnuto pro výraz lambda, který vyžaduje alespoň [{1}] + +objectNotAssignable=Nelze pÅ™idat objekt typu [{0}] do pole objektů typu [{1}]. +propertyNotFound=Vlastnost [{1}] nebyla nalezena u typu [{0}] +propertyNotReadable=Vlastnost [{1}] není Äitelná u typu [{0}] +propertyNotWritable=Vlastnost [{1}] není zapisovatelná u typu [{0}] +propertyReadError=Chyba pÅ™i Ätení [{1}] u typu [{0}] +propertyWriteError=Chyba pri zápisu [{1}] u typu [{0}] + +staticFieldELResolver.methodNotFound=Nebyla nalezena žádná odpovídající veÅ™ejná statická metoda s názvem [{0}] ve třídÄ› [{1}] +staticFieldELResolver.notFound=Nebylo nalezeno žádné veÅ™ejné statické pole s názvem [{0}] ve třídÄ› [{1}] (exportované pro Java 9+) +staticFieldELResolver.notWritable=Zápis do statických polí (v tomto případÄ› pole [{0}] pro třídu [{1}]) není povolen + +util.method.ambiguous=Nelze najít jednoznaÄnou metodu: {0}.{1}({2}) +util.method.notfound=Metoda nebyla nalezena: {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_de.properties b/java/jakarta/el/LocalStrings_de.properties new file mode 100644 index 0000000..dfa2c0b --- /dev/null +++ b/java/jakarta/el/LocalStrings_de.properties @@ -0,0 +1,52 @@ +# 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. + +beanNameELResolver.beanReadOnly=Der Beanname [{0}] ist schreibgeschützt. + +elProcessor.defineFunctionInvalidClass=Die Klasse [{0}] ist nicht öffentlich. +elProcessor.defineFunctionInvalidMethod=Die Methode [{0}] in der Klasse [{1}] ist keine öffentliche statische Methode. +elProcessor.defineFunctionInvalidParameterList=Die Parameterliste [{0}] für die Methode [{1}] in der Klasse [{2}] ist nicht gültig. +elProcessor.defineFunctionInvalidParameterTypeName=Der Parametertyp [{0}] für die Methode [{1}] in der Klasse [{2}] ist nicht gültig. +elProcessor.defineFunctionNoMethod=Es wurde keine öffentliche statische Methode [{0}] in der Klasse [{1}] gefunden. +elProcessor.defineFunctionNullParams=Mindestens einer der Eingabeparameter ist null. + +expressionFactory.cannotCreate=Die Ausdruckfactory vom Typ [{0}] konnte nicht erstellt werden. +expressionFactory.cannotFind=Die Ausdruckfactory vom Typ [{0}] konnte nicht gefunden werden. +expressionFactory.readFailed=Fehler beim Lesen von [{0}] + +importHandler.ambiguousImport=Die Klasse [{0}] konnte nicht importiert werden, weil sie mit dem bereits durchgeführten Import von [{1}] in Konflikt steht. +importHandler.ambiguousStaticImport=Der statische Import [{0}] konnte nicht verarbeitet werden, weil er mit dem bereits durchgeführten Import von [{1}] in Konflikt steht. +importHandler.classNotFound=Die Klasse [{0}] konnte nicht importiert werden, weil sie nicht gefunden wurde. +importHandler.invalidClass=Die Klasse [{0}] muss eine öffentliche, in einem exportierten Paket (für Java 9+), nicht abstrakte Klasse und darf keine Schnittstelle sein. +importHandler.invalidClassName=Der Name der zu importierenden Klasse [{0}] muss ein Paket enthalten. +importHandler.invalidClassNameForStatic=Die Klasse [{0}], die für den statischen Import [{1}] angegeben wurde, ist nicht gültig. +importHandler.invalidStaticName=Der Name der zu importierenden statischen Methode bzw. des zu importierenden Felds [{0}] muss eine Klasse enthalten. +importHandler.staticNotFound=Der statische Import [{0}] wurde nicht in der Klasse [{1}] für den Import [{2}] gefunden. + +lambdaExpression.tooFewArgs=Es wurden nur [{0}] Argumente für einen Lambdaausdruck angegeben, der mindestens [{1}] Argumente erfordert. + +objectNotAssignable=Ein Objekt des Typs [{0}] kann keinem Array von Objekten des Typs [{1}] hinzugefügt werden. +propertyNotFound=Die Eigenschaft [{1}] wurde für den Typ [{0}] nicht gefunden +propertyNotReadable=Die Eigenschaft [{1}] ist für den Typ [{0}] nicht lesbar +propertyNotWritable=Die Eigenschaft [{1}] kann nicht für den Typ [{0}] beschrieben werden +propertyReadError=Fehler beim Lesen von [{1}] für den Typ [{0}] +propertyWriteError=Fehler beim Schreiben von [{1}] für den Typ [{0}] + +staticFieldELResolver.methodNotFound=Es wurde keine öffentliche statische Methode mit dem Namen [{0}] in der Klasse [{1}] gefunden. +staticFieldELResolver.notFound=Es wurde kein öffentliches statisches Feld mit dem Namen [{0}] in der (für Java 9+ exportierten) Klasse [{1}] gefunden. +staticFieldELResolver.notWritable=Das Schreiben in statische Felder (in diesem Fall Feld [{0}] in Klasse [{1}]) ist nicht zulässig. + +util.method.ambiguous=Es wurde keine eindeutige Methode gefunden: {0}.{1}({2}) +util.method.notfound=Methode nicht gefunden: {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_es.properties b/java/jakarta/el/LocalStrings_es.properties new file mode 100644 index 0000000..644a47a --- /dev/null +++ b/java/jakarta/el/LocalStrings_es.properties @@ -0,0 +1,52 @@ +# 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. + +beanNameELResolver.beanReadOnly=El nombre de bean [{0}] es de sólo lectura + +elProcessor.defineFunctionInvalidClass=La clase [{0}] no es pública +elProcessor.defineFunctionInvalidMethod=El método [{0}] en la clase [{1}] no es un método estático público +elProcessor.defineFunctionInvalidParameterList=La lista de parámetros [{0}] para el método [{1}] en la clase [{2}] no es válida +elProcessor.defineFunctionInvalidParameterTypeName=El tipo de parámetro [{0}] para el método [{1}] en la clase [{2}] no es válido +elProcessor.defineFunctionNoMethod=No se ha podido encontrar un método estático público [{0}] en la clase [{1}] +elProcessor.defineFunctionNullParams=Uno o varios de los parámetros de entrada era nulo + +expressionFactory.cannotCreate=No se ha podido crear un ExpressionFactory de tipo [{0}] +expressionFactory.cannotFind=No se ha podido encontrar un ExpressionFactory de tipo [{0}] +expressionFactory.readFailed=No se ha podido leer [{0}] + +importHandler.ambiguousImport=La clase [{0}] no se ha podido importar ya que está en conflicto con [{1}], que ya se ha importado +importHandler.ambiguousStaticImport=No se ha podido procesar la importación estática [{0}] ya que está en conflicto con [{1}] que ya se ha importado +importHandler.classNotFound=No se ha podido importar la clase [{0}], ya que no se ha podido importar +importHandler.invalidClass=La clase [{0}] debe ser pública, estar en un paquete exportado (para Java 9+), ser no abstracta y no ser una interfaz +importHandler.invalidClassName=El nombre de la clase para importar [{0}] debe incluir un paquete +importHandler.invalidClassNameForStatic=La clase [{0}] especificada para la importación estática [{1}] no es válida +importHandler.invalidStaticName=El nombre del campo o método estático para importar [{0}] debe incluir una clase +importHandler.staticNotFound=No se ha podido encontrar la importación estática [{0}] en la clase [{1}] para la importación [{2}] + +lambdaExpression.tooFewArgs=Sólo se han proporcionado [{0}] argumentos para una expresión lambda que requiere al menos [{1}] + +objectNotAssignable=No se puede añadir un objeto de tipo [{0}] a una matriz de objetos de tipo [{1}] +propertyNotFound=No se ha encontrado la propiedad [{1}] en el tipo [{0}] +propertyNotReadable=La propiedad [{1}] no se puede leer en el tipo [{0}] +propertyNotWritable=La propiedad [{1}] no se puede escribir en el tipo [{0}] +propertyReadError=Error al leer [{1}] en el tipo [{0}] +propertyWriteError=Error al escribir [{1}] en el tipo [{0}] + +staticFieldELResolver.methodNotFound=No se ha encontrado ningún método estático público coincidente denominado [{0}] en la clase [{1}] +staticFieldELResolver.notFound=No se ha encontrado ningún campo estático público denominado [{0}] en la clase [{1}] (exportada para Java 9+) +staticFieldELResolver.notWritable=No se permite escribir en campos estáticos (en este caso, el campo [{0}] en la clase [{1}]) + +util.method.ambiguous=No se ha podido encontrar el método no ambiguo: {0}.{1}({2}) +util.method.notfound=No se ha encontrado el método: {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_fr.properties b/java/jakarta/el/LocalStrings_fr.properties new file mode 100644 index 0000000..0f3ca9b --- /dev/null +++ b/java/jakarta/el/LocalStrings_fr.properties @@ -0,0 +1,53 @@ +# 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. + +beanNameELResolver.beanReadOnly=Le nom de bean [{0}] est en lecture seule + +elProcessor.defineFunctionInvalidClass=La classe [{0}] n''est pas publique +elProcessor.defineFunctionInvalidMethod=La méthode [{0}] sur la classe [{1}] n''est pas une méthode statique publique +elProcessor.defineFunctionInvalidParameterList=La liste de paramètres [{0}] pour la méthode [{1}] sur la classe [{2}] n''est pas valide +elProcessor.defineFunctionInvalidParameterTypeName=Le type de paramètre [{0}] pour la méthode [{1}] sur la classe [{2}] n''est pas valide +elProcessor.defineFunctionNoMethod=Une méthode statique publique [{0}] sur la classe [{1}] est introuvable +elProcessor.defineFunctionNullParams=Un ou plusieurs des paramètres en entrée avaient une valeur nulle + +expressionFactory.cannotCreate=Impossible de créer ExpressionFactory de type [{0}] +expressionFactory.cannotFind=Impossible de trouver ExpressionFactory de type [{0}] +expressionFactory.readFailed=Echec de lecture de [{0}] + +importHandler.ambiguousImport=.La classe [{0}] n''a pas pu être importée car elle est en conflit avec [{1}] qui est déjà importé +importHandler.ambiguousStaticImport=L''importation statique [{0}] n''a pas pu être traitée car elle est en conflit avec [{1}] qui est déjà importé +importHandler.classNotFound=La classe [{0}] n''a pas pu être importée car elle est introuvable +importHandler.invalidClass=La classe [{0}] doit être publique, dans un package exporté (pour Java 9+), non abstraite et ne doit pas être une interface +importHandler.invalidClassName=Le nom de la classe à importer [{0}] doit inclure un package +importHandler.invalidClassNameForStatic=La classe [{0}] spécifiée pour l''importation statique [{1}] n''est pas valide +importHandler.invalidStaticName=Le nom de la méthode ou de la zone statique à importer [{0}] doit inclure une classe +importHandler.staticNotFound=L''importation statique [{0}] est introuvable dans la classe [{1}] pour l''importation [{2}] + +lambdaExpression.tooFewArgs=Seuls [{0}] arguments ont été fournis pour une expression lambda qui en requiert au moins [{1}] + +objectNotAssignable=Impossible d''ajouter un objet de type [{0}] à un tableau d''objets de type [{1}] +propertyNotFound=Propriété [{1}] introuvable sur le type [{0}] +propertyNotReadable=Propriété [{1}] illisible sur le type [{0}] +propertyNotWritable=Propriété [{1}] non inscriptible sur le type [{0}] +propertyReadError=Erreur de lecture de [{1}] sur le type [{0}] +propertyWriteError=Erreur d''écriture de [{1}] sur le type [{0}] +resolverNotWritable=ELResolver ne peut être utilisé en écriture pour le type [{0}] + +staticFieldELResolver.methodNotFound=Aucune méthode statique publique nommée [{0}] n''a été détectée sur la classe [{1}] +staticFieldELResolver.notFound=Aucune zone statique publique nommée [{0}] n''a été détectée sur la classe (exportée pour Java 9+) [{1}] +staticFieldELResolver.notWritable=L''écriture dans les zones statiques (dans ce cas la zone [{0}] sur la classe [{1}]) n''est pas autorisée + +util.method.ambiguous=Méthode non ambiguë : {0}.{1}({2}) non trouvée +util.method.notfound=Méthode non trouvée : {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_ja.properties b/java/jakarta/el/LocalStrings_ja.properties new file mode 100644 index 0000000..71ff1c0 --- /dev/null +++ b/java/jakarta/el/LocalStrings_ja.properties @@ -0,0 +1,53 @@ +# 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. + +beanNameELResolver.beanReadOnly=Bean å [{0}] ã¯èª­ã¿å–り専用ã§ã™ + +elProcessor.defineFunctionInvalidClass=クラス [{0}] 㯠public ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +elProcessor.defineFunctionInvalidMethod=クラス [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã¯ã€public static メソッドã§ã¯ã‚ã‚Šã¾ã›ã‚“ +elProcessor.defineFunctionInvalidParameterList=クラス [{2}]ã€ãƒ¡ã‚½ãƒƒãƒ‰ [{1}] ã®ãƒ‘ラメーター・リスト [{0}] ã¯ç„¡åŠ¹ã§ã™ã€‚ +elProcessor.defineFunctionInvalidParameterTypeName=クラス [{2}]ã€ãƒ¡ã‚½ãƒƒãƒ‰ [{1}] ã®ãƒ‘ラメーター・タイプ [{0}] ã¯ç„¡åŠ¹ã§ã™ã€‚ +elProcessor.defineFunctionNoMethod=クラス [{1}] ã® public static メソッド [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+elProcessor.defineFunctionNullParams=1 ã¤ä»¥ä¸Šã®å…¥åŠ›ãƒ‘ラメーターãŒãƒŒãƒ«ã§ã—㟠+ +expressionFactory.cannotCreate=タイプ [{0}] ã® ExpressionFactory を作æˆã§ãã¾ã›ã‚“ +expressionFactory.cannotFind=タイプ [{0}] ã® ExpressionFactory ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +expressionFactory.readFailed=[{0}] を読ã¿å–ã‚Œã¾ã›ã‚“ã§ã—㟠+ +importHandler.ambiguousImport=クラス [{0}] ã¯ã€æ—¢ã«ã‚¤ãƒ³ãƒãƒ¼ãƒˆæ¸ˆã¿ã® [{1}] ã¨çŸ›ç›¾ã™ã‚‹ãŸã‚ã€ã‚¤ãƒ³ãƒãƒ¼ãƒˆã§ãã¾ã›ã‚“ã§ã—㟠+importHandler.ambiguousStaticImport=static インãƒãƒ¼ãƒˆ [{0}] ã¯ã€æ—¢ã«ã‚¤ãƒ³ãƒãƒ¼ãƒˆæ¸ˆã¿ã® [{1}] ã¨çŸ›ç›¾ã™ã‚‹ãŸã‚ã€å‡¦ç†ã§ãã¾ã›ã‚“ã§ã—㟠+importHandler.classNotFound=クラス [{0}] ã¯ã€è¦‹ã¤ã‹ã‚‰ãªã‹ã£ãŸãŸã‚ã€ã‚¤ãƒ³ãƒãƒ¼ãƒˆã§ãã¾ã›ã‚“ã§ã—㟠+importHandler.invalidClass=クラス [{0}] ã¯ã€public (Java 9+ 用ã«ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã•ã‚ŒãŸãƒ‘ッケージ内ã§) ã‹ã¤éžæŠ½è±¡ã§ãªã‘ã‚Œã°ãªã‚‰ãšã€ã‚¤ãƒ³ã‚¿ãƒ¼ãƒ•ã‚§ãƒ¼ã‚¹ã§ã‚ã£ã¦ã¯ãªã‚Šã¾ã›ã‚“ +importHandler.invalidClassName=インãƒãƒ¼ãƒˆã™ã‚‹ã‚¯ãƒ©ã‚¹ã®åå‰ [{0}] ã¯ãƒ‘ッケージをå«ã‚€å¿…è¦ãŒã‚ã‚Šã¾ã™ +importHandler.invalidClassNameForStatic=static インãƒãƒ¼ãƒˆ [{1}] ã«æŒ‡å®šã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{0}] ã¯ç„¡åŠ¹ã§ã™ +importHandler.invalidStaticName=インãƒãƒ¼ãƒˆã™ã‚‹ static メソッドã¾ãŸã¯ãƒ•ã‚£ãƒ¼ãƒ«ãƒ‰ã®åå‰ [{0}] ã¯ã€ã‚¯ãƒ©ã‚¹ã‚’å«ã¾ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“ +importHandler.staticNotFound=static インãƒãƒ¼ãƒˆ [{0}] ãŒã€ã‚¤ãƒ³ãƒãƒ¼ãƒˆ [{2}] ã®ã‚¯ãƒ©ã‚¹ [{1}] ã§è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+ +lambdaExpression.tooFewArgs=å°‘ãªãã¨ã‚‚ [{1}] 個ã®å¼•æ•°ã‚’å¿…è¦ã¨ã™ã‚‹ãƒ©ãƒ ãƒ€å¼ã«ã€[{0}] 個ã®å¼•æ•°ã—ã‹æŒ‡å®šã•ã‚Œã¾ã›ã‚“ã§ã—㟠+ +objectNotAssignable=タイプ [{0}] ã®ã‚ªãƒ–ジェクトをタイプ [{1}] ã®ã‚ªãƒ–ジェクトé…列ã«è¿½åŠ ã§ãã¾ã›ã‚“ +propertyNotFound=タイプ [{0}] ã§ãƒ—ロパティー [{1}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +propertyNotReadable=タイプ [{0}] ã§ãƒ—ロパティー [{1}] を読ã¿å–ã‚Œã¾ã›ã‚“ +propertyNotWritable=タイプ [{0}] ã§ãƒ—ロパティー [{1}] を書ãè¾¼ã‚ã¾ã›ã‚“ +propertyReadError=タイプ [{0}] 㧠[{1}] を読ã¿å–り中ã®ã‚¨ãƒ©ãƒ¼ +propertyWriteError=タイプ [{0}] 㧠[{1}] を書ãè¾¼ã¿ä¸­ã®ã‚¨ãƒ©ãƒ¼ +resolverNotWritable=ELResolver ã¯ã‚¿ã‚¤ãƒ— [{0}] ã«å¯¾ã—ã¦æ›¸ãè¾¼ã¿å¯èƒ½ã§ã¯ã‚ã‚Šã¾ã›ã‚“ + +staticFieldELResolver.methodNotFound=クラス [{1}] ã« [{0}] ã¨ã„ã†åå‰ã®ä¸€è‡´ã™ã‚‹ public static メソッドãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+staticFieldELResolver.notFound=(Java 9+ 用ã«ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã•ã‚ŒãŸ) クラス [{1}] ã« public static フィールド [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+staticFieldELResolver.notWritable=static フィールド (ã“ã®å ´åˆã€ã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ•ã‚£ãƒ¼ãƒ«ãƒ‰ [{0}]) ã¸ã®æ›¸ãè¾¼ã¿ã¯è¨±å¯ã•ã‚Œã¾ã›ã‚“ + +util.method.ambiguous=ã‚ã„ã¾ã„ã§ãªã„メソッドãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“: {0}.{1}({2}) +util.method.notfound=メソッドãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“: {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_ko.properties b/java/jakarta/el/LocalStrings_ko.properties new file mode 100644 index 0000000..458e0a8 --- /dev/null +++ b/java/jakarta/el/LocalStrings_ko.properties @@ -0,0 +1,52 @@ +# 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. + +beanNameELResolver.beanReadOnly=Bean ì´ë¦„ [{0}]ì€(는) ì½ê¸° 전용입니다. + +elProcessor.defineFunctionInvalidClass=í´ëž˜ìŠ¤ [{0}]ì´(ê°€) ê³µìš©ì´ ì•„ë‹™ë‹ˆë‹¤. +elProcessor.defineFunctionInvalidMethod=í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì´(ê°€) 공용 ì •ì  ë©”ì†Œë“œê°€ 아닙니다. +elProcessor.defineFunctionInvalidParameterList=í´ëž˜ìŠ¤ [{2}]ì˜ ë©”ì†Œë“œ [{1}]ì— ëŒ€í•œ 매개변수 ëª©ë¡ [{0}]ì´(ê°€) 유효하지 않습니다. +elProcessor.defineFunctionInvalidParameterTypeName=í´ëž˜ìŠ¤ [{2}]ì˜ ë©”ì†Œë“œ [{1}]ì— ëŒ€í•œ 매개변수 유형 [{0}]ì´(ê°€) 유효하지 않습니다. +elProcessor.defineFunctionNoMethod=í´ëž˜ìŠ¤ [{1}]ì˜ ê³µìš© ì •ì  ë©”ì†Œë“œ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +elProcessor.defineFunctionNullParams=하나 ì´ìƒì˜ ìž…ë ¥ 매개변수가 ë„입니다. + +expressionFactory.cannotCreate=[{0}] ìœ í˜•ì˜ ExpressionFactory를 작성할 수 ì—†ìŒ +expressionFactory.cannotFind=[{0}] ìœ í˜•ì˜ ExpressionFactory를 ì°¾ì„ ìˆ˜ ì—†ìŒ +expressionFactory.readFailed=[{0}] ì½ê¸° 실패 + +importHandler.ambiguousImport=ì´ë¯¸ 가져온 [{1}]ê³¼(와) 충ëŒí•˜ë¯€ë¡œ í´ëž˜ìŠ¤ [{0}]ì„(를) 가져올 수 없습니다. +importHandler.ambiguousStaticImport=ì´ë¯¸ 가져온 [{1}]ê³¼(와) 충ëŒí•˜ë¯€ë¡œ ì •ì  ê°€ì ¸ì˜¤ê¸° [{0}]ì„(를) 처리할 수 없습니다. +importHandler.classNotFound=í´ëž˜ìŠ¤ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없으므로 가져올 수 없습니다. +importHandler.invalidClass=[{0}] í´ëž˜ìŠ¤ëŠ” 내보낸 패키지(ìžë°” 9+ìš©), ë¹„ì¶”ìƒ ë° ì¸í„°íŽ˜ì´ìŠ¤ê°€ ì•„ë‹Œ 공용ì´ì–´ì•¼ 합니다. +importHandler.invalidClassName=[{0}]ì„(를) 가져올 í´ëž˜ìŠ¤ì˜ ì´ë¦„ì—는 패키지가 í¬í•¨ë˜ì–´ì•¼ 합니다. +importHandler.invalidClassNameForStatic=ì •ì  ê°€ì ¸ì˜¤ê¸° [{1}]ì— ì§€ì •ëœ í´ëž˜ìŠ¤ [{0}]ì´(ê°€) 유효하지 않습니다. +importHandler.invalidStaticName=[{0}]ì„(를) 가져올 ì •ì  ë©”ì†Œë“œ ë˜ëŠ” í•„ë“œì˜ ì´ë¦„ì—는 í´ëž˜ìŠ¤ê°€ í¬í•¨ë˜ì–´ì•¼ 합니다. +importHandler.staticNotFound=ì •ì  ê°€ì ¸ì˜¤ê¸° [{0}]ì„(를) 가져오기 [{2}]ì˜ í´ëž˜ìŠ¤ [{1}]ì—ì„œ ì°¾ì„ ìˆ˜ 없습니다. + +lambdaExpression.tooFewArgs=최소한 [{1}]개를 필요로 하는 lambda 표현ì‹ì— [{0}]ê°œì˜ ì¸ìˆ˜ë§Œ 제공ë˜ì—ˆìŠµë‹ˆë‹¤. + +objectNotAssignable=[{0}] ìœ í˜•ì˜ ì˜¤ë¸Œì íŠ¸ë¥¼ [{1}] ìœ í˜•ì˜ ì˜¤ë¸Œì íŠ¸ ë°°ì—´ì— ì¶”ê°€í•  수 없습니다. +propertyNotFound=[{1}] íŠ¹ì„±ì´ [{0}] ìœ í˜•ì— ì—†ìŠµë‹ˆë‹¤. +propertyNotReadable=[{0}] 유형ì—ì„œ [{1}] íŠ¹ì„±ì„ ì½ì„ 수 없습니다. +propertyNotWritable=[{1}] íŠ¹ì„±ì´ [{0}] 유형ì—ì„œ 쓰기 가능하지 않습니다. +propertyReadError=[{0}] 유형ì—ì„œ [{1}]ì„(를) ì½ëŠ” ì¤‘ì— ì˜¤ë¥˜ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. +propertyWriteError=[{0}] ìœ í˜•ì— [{1}]ì„(를) 쓰는 ì¤‘ì— ì˜¤ë¥˜ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. + +staticFieldELResolver.methodNotFound=ì¼ì¹˜í•˜ëŠ” 공용 ì •ì  ë©”ì†Œë“œ [{0}]ì´(ê°€) [{1}] í´ëž˜ìŠ¤ì— ì—†ìŒ +staticFieldELResolver.notFound=[{0}](으)ë¡œ ì´ë¦„ ì§€ì •ëœ ê³µìš© ì •ì  í•„ë“œê°€ (Java 9+ì— ëŒ€í•´ 내보낸) í´ëž˜ìŠ¤ [{1}] ì—ì„œ 발견ë˜ì§€ 않았습니다. +staticFieldELResolver.notWritable=[{1}] í´ëž˜ìŠ¤ì˜ ì´ ì¼€ì´ìŠ¤ í•„ë“œ [{0}]ì— ìžˆëŠ” ì •ì  í•„ë“œì— ì“°ê¸°ê°€ 허용ë˜ì§€ ì•ŠìŒ + +util.method.ambiguous=명확한 메소드를 ì°¾ì„ ìˆ˜ ì—†ìŒ: {0}.{1}({2}) +util.method.notfound=메소드를 ì°¾ì„ ìˆ˜ ì—†ìŒ: {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_pt_BR.properties b/java/jakarta/el/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..5dd86dd --- /dev/null +++ b/java/jakarta/el/LocalStrings_pt_BR.properties @@ -0,0 +1,52 @@ +# 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. + +beanNameELResolver.beanReadOnly=O nome do bean [{0}] é somente leitura + +elProcessor.defineFunctionInvalidClass=A classe [{0}] não é pública +elProcessor.defineFunctionInvalidMethod=O método [{0}] na classe [{1}] não é um método estático público +elProcessor.defineFunctionInvalidParameterList=A lista de parâmetros [{0}] do método [{1}] na classe [{2}] não é válida +elProcessor.defineFunctionInvalidParameterTypeName=O tipo de parâmetro [{0}] do método [{1}] na classe [{2}] não é válido +elProcessor.defineFunctionNoMethod=Um método estático público [{0}] na classe [{1}] não pôde ser localizado +elProcessor.defineFunctionNullParams=Um ou mais parâmetros de entrada era nulo + +expressionFactory.cannotCreate=Não foi possível criar ExpressionFactory do tipo [{0}] +expressionFactory.cannotFind=Não foi possível localizar ExpressionFactory do tipo [{0}] +expressionFactory.readFailed=Falha ao ler [{0}] + +importHandler.ambiguousImport=A classe [{0}] não pôde ser importada, pois ela entra em conflito com [{1}] que já foi importado +importHandler.ambiguousStaticImport=A importação estática [{0}] não pôde ser processada, pois ela entra em conflito com [{1}] que já foi importada +importHandler.classNotFound=A classe [{0}] não pôde importada, pois não pôde ser localizada +importHandler.invalidClass=A classe [{0}] deve ser pública, deve estar em um pacote exportado (para Java 9+) e não deve ser abstrata e uma interface +importHandler.invalidClassName=O nome da classe a ser importada [{0}] deve incluir um pacote +importHandler.invalidClassNameForStatic=A classe [{0}] especificada para importação estática [{1}] não é válida +importHandler.invalidStaticName=O nome do método ou campo estático a ser importado [{0}] deve incluir uma classe +importHandler.staticNotFound=A importação estática [{0}] não pôde ser localizada na classe [{1}] para importação [{2}] + +lambdaExpression.tooFewArgs=Somente [{0}] argumentos foram fornecidos para uma expressão lambda que requer pelo menos [{1}] + +objectNotAssignable=Não é possível incluir um objeto do tipo [{0}] em uma matriz de objetos do tipo [{1}] +propertyNotFound=Não foi possível localizar a propriedade [{1}] no tipo [{0}] +propertyNotReadable=Não foi possível ler a propriedade [{1}] no tipo [{0}] +propertyNotWritable=Não foi possível gravar a propriedade [{1}] no tipo [{0}] +propertyReadError=Erro ao ler [{1}] no tipo [{0}] +propertyWriteError=Erro ao gravar [{1}] no tipo [{0}] + +staticFieldELResolver.methodNotFound=Nenhum método estático público correspondente chamado [{0}] localizado na classe [{1}] +staticFieldELResolver.notFound=Nenhum campo público estático denominado [{0}] foi localizado na classe (exportada para Java 9+) [{1}] +staticFieldELResolver.notWritable=Não é permitido gravar nos campos estáticos (nesse caso, o campo [{0}] na classe [{1}]) + +util.method.ambiguous=Não é possível localizar método inequívoco: {0}.{1}({2}) +util.method.notfound=Método não localizado: {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_ru.properties b/java/jakarta/el/LocalStrings_ru.properties new file mode 100644 index 0000000..aec65d5 --- /dev/null +++ b/java/jakarta/el/LocalStrings_ru.properties @@ -0,0 +1,52 @@ +# 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. + +beanNameELResolver.beanReadOnly=Ð˜Ð¼Ñ Ð¾Ð±ÑŠÐµÐºÑ‚Ð° EJB [{0}] доÑтупно только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ + +elProcessor.defineFunctionInvalidClass=КлаÑÑ [{0}] не public +elProcessor.defineFunctionInvalidMethod=Метод [{0}] в клаÑÑе [{1}] не public static +elProcessor.defineFunctionInvalidParameterList=ÐедопуÑтимый ÑпиÑок параметров [{0}] Ð´Ð»Ñ Ð¼ÐµÑ‚Ð¾Ð´Ð° [{1}] в клаÑÑе [{2}] +elProcessor.defineFunctionInvalidParameterTypeName=ÐедопуÑтимый тип параметра [{0}] Ð´Ð»Ñ Ð¼ÐµÑ‚Ð¾Ð´Ð° [{1}] в клаÑÑе [{2}] +elProcessor.defineFunctionNoMethod=Ðе найден метод public static [{0}] в клаÑÑе [{1}] +elProcessor.defineFunctionNullParams=ЧаÑÑ‚ÑŒ входных параметров равна null + +expressionFactory.cannotCreate=Ðе удалоÑÑŒ Ñоздать ExpressionFactory типа [{0}] +expressionFactory.cannotFind=Ðе удалоÑÑŒ найти ExpressionFactory типа [{0}] +expressionFactory.readFailed=Ðе удалоÑÑŒ прочитать [{0}] + +importHandler.ambiguousImport=КлаÑÑ [{0}] не удалоÑÑŒ импортировать, так как он конфликтует Ñ ÑƒÐ¶Ðµ импортированным клаÑÑом [{1}] +importHandler.ambiguousStaticImport=СтатичеÑкий импорт клаÑÑа [{0}] не удалоÑÑŒ обработать, так как он конфликтует Ñ ÑƒÐ¶Ðµ импортированным клаÑÑом [{1}] +importHandler.classNotFound=КлаÑÑ [{0}] не удалоÑÑŒ импортировать, поÑкольку он не найден +importHandler.invalidClass=КлаÑÑ [{0}] должен иметь Ñпецификатор public, находитьÑÑ Ð² ÑкÑпортированном пакете (Ð´Ð»Ñ Java 9+), быть неабÑтрактным и не быть интерфейÑом +importHandler.invalidClassName=Ð˜Ð¼Ñ ÐºÐ»Ð°ÑÑа Ð´Ð»Ñ Ð¸Ð¼Ð¿Ð¾Ñ€Ñ‚Ð° [{0}] должно включать пакет +importHandler.invalidClassNameForStatic=КлаÑÑ [{0}], указанный Ð´Ð»Ñ ÑтатичеÑкого импорта [{1}], недопуÑтимый +importHandler.invalidStaticName=Ð˜Ð¼Ñ Ð¼ÐµÑ‚Ð¾Ð´Ð° или Ð¿Ð¾Ð»Ñ static Ð´Ð»Ñ Ð¸Ð¼Ð¿Ð¾Ñ€Ñ‚Ð° [{0}] должно включать клаÑÑ +importHandler.staticNotFound=СтатичеÑкий импорт [{0}] не найден в клаÑÑе [{1}] Ð´Ð»Ñ Ð¸Ð¼Ð¿Ð¾Ñ€Ñ‚Ð° [{2}] + +lambdaExpression.tooFewArgs=Ð’ лÑмбда-выражение передано только [{0}] аргументов, а требуетÑÑ Ð½Ðµ менее [{1}] + +objectNotAssignable=Ðе удалоÑÑŒ добавить объект типа [{0}] в маÑÑив объектов типа [{1}] +propertyNotFound=СвойÑтво [{1}] не найдено в типе [{0}] +propertyNotReadable=СвойÑтво [{1}] не ÑвлÑетÑÑ Ñ‡Ð¸Ñ‚Ð°ÐµÐ¼Ñ‹Ð¼ в типе [{0}] +propertyNotWritable=СвойÑтво [{1}] не ÑвлÑетÑÑ Ð·Ð°Ð¿Ð¸Ñываемым в типе [{0}] +propertyReadError=Ошибка Ñ‡Ñ‚ÐµÐ½Ð¸Ñ [{1}] в типе [{0}] +propertyWriteError=Ошибка запиÑи [{1}] в типе [{0}] + +staticFieldELResolver.methodNotFound=Ð’ клаÑÑе [{1}] не найден ÑоответÑтвующий метод public static Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ [{0}] +staticFieldELResolver.notFound=Ð’ ÑкÑпортированном клаÑÑе [{1}] не найдено публичное ÑтатичеÑкое поле Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ [{0}] +staticFieldELResolver.notWritable=ЗапиÑÑŒ в поле static (в данном Ñлучае Ñто поле [{0}] в клаÑÑе [{1}]) запрещена + +util.method.ambiguous=Ðе удалоÑÑŒ однозначно найти метод: {0}.{1}({2}) +util.method.notfound=Метод не найден: {0}.{1}({2}) diff --git a/java/jakarta/el/LocalStrings_zh_CN.properties b/java/jakarta/el/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..9b1bcdd --- /dev/null +++ b/java/jakarta/el/LocalStrings_zh_CN.properties @@ -0,0 +1,52 @@ +# 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. + +beanNameELResolver.beanReadOnly=Bean å称 [{0}] 为åªè¯»é¡¹ + +elProcessor.defineFunctionInvalidClass=ç±» [{0}] ä¸æ˜¯å…¬å…±ç±» +elProcessor.defineFunctionInvalidMethod=ç±» [{1}] 上的方法 [{0}] ä¸æ˜¯å…¬å…±é™æ€æ–¹æ³• +elProcessor.defineFunctionInvalidParameterList=ç±» [{2}] 上方法 [{1}] çš„å‚数列表 [{0}] 无效 +elProcessor.defineFunctionInvalidParameterTypeName=ç±» [{2}] 上方法 [{1}] çš„å‚数类型 [{0}] 无效 +elProcessor.defineFunctionNoMethod=找ä¸åˆ°ç±» [{1}] 上的公共é™æ€æ–¹æ³• [{0}] +elProcessor.defineFunctionNullParams=一个或多个输入å‚数为 null + +expressionFactory.cannotCreate=无法创建类型为 [{0}] çš„ ExpressionFactory +expressionFactory.cannotFind=找ä¸åˆ°ç±»åž‹ä¸º [{0}] çš„ ExpressionFactory +expressionFactory.readFailed=æœªèƒ½è¯»å– [{0}] + +importHandler.ambiguousImport=未能导入类 [{0}],因为它和已导入的 [{1}] å†²çª +importHandler.ambiguousStaticImport=未能处ç†é™æ€å¯¼å…¥ [{0}],因为它和已导入的 [{1}] å†²çª +importHandler.classNotFound=未能导入类 [{0}],因为找ä¸åˆ°è¯¥ç±» +importHandler.invalidClass=在导出的包中(对于 Java 9+),类 [{0}] 必须是公用的,éžæŠ½è±¡å¹¶ä¸”éžæŽ¥å£ +importHandler.invalidClassName=è¦å¯¼å…¥çš„类的å称 [{0}] 必须包å«è½¯ä»¶åŒ… +importHandler.invalidClassNameForStatic=为é™æ€å¯¼å…¥ [{1}] 指定的类 [{0}] 无效 +importHandler.invalidStaticName=è¦å¯¼å…¥çš„é™æ€æ–¹æ³•æˆ–字段的å称 [{0}] 必须包å«ç±» +importHandler.staticNotFound=在导入 [{2}] çš„ç±» [{1}] 中找ä¸åˆ°é™æ€å¯¼å…¥ [{0}] + +lambdaExpression.tooFewArgs=å¯¹äºŽè‡³å°‘éœ€è¦ [{1}] çš„ lambda 表达å¼ï¼Œä»…æ供了 [{0}] 个自å˜é‡ + +objectNotAssignable=无法将类型为 [{0}] 的对象添加到类型为 [{1}] 的对象阵列 +propertyNotFound=在类型 [{0}] 上未找到属性 [{1}] +propertyNotReadable=属性 [{1}] 在类型 [{0}] 上ä¸å¯è¯» +propertyNotWritable=属性 [{1}] 在类型 [{0}] 上ä¸å¯å†™ +propertyReadError=在类型 [{0}] ä¸Šè¯»å– [{1}] 时出错 +propertyWriteError=在类型 [{0}] 上写入 [{1}] 时出错 + +staticFieldELResolver.methodNotFound=在类 [{1}] 上,找ä¸åˆ°å为 [{0}] 的匹é…公共é™æ€æ–¹æ³• +staticFieldELResolver.notFound=在(导出的 Java 9+ )类 [{1}] 上找ä¸åˆ°å为 [{0}] 的公共é™æ€å­—段 +staticFieldELResolver.notWritable=ä¸å…许写入é™æ€å­—段(在此情况下,这是类 [{1}] 上的字段 [{0}]) + +util.method.ambiguous=找ä¸åˆ°æ˜Žç¡®çš„方法:{0}.{1}({2}) +util.method.notfound=找ä¸åˆ°æ–¹æ³•ï¼š{0}.{1}({2}) diff --git a/java/jakarta/el/MapELResolver.java b/java/jakarta/el/MapELResolver.java new file mode 100644 index 0000000..5ea2332 --- /dev/null +++ b/java/jakarta/el/MapELResolver.java @@ -0,0 +1,140 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class MapELResolver extends ELResolver { + + private static final Class UNMODIFIABLE = Collections.unmodifiableMap(new HashMap<>()).getClass(); + + private final boolean readOnly; + + public MapELResolver() { + this.readOnly = false; + } + + public MapELResolver(boolean readOnly) { + this.readOnly = readOnly; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof Map) { + context.setPropertyResolved(base, property); + + Map map = (Map) base; + if (readOnly || map.getClass() == UNMODIFIABLE) { + return null; + } + + return Object.class; + } + + return null; + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof Map) { + context.setPropertyResolved(base, property); + return ((Map) base).get(property); + } + + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + + if (base instanceof Map) { + context.setPropertyResolved(base, property); + + if (this.readOnly) { + throw new PropertyNotWritableException( + Util.message(context, "resolverNotWritable", base.getClass().getName())); + } + + try { + @SuppressWarnings("unchecked") // Must be OK + Map map = ((Map) base); + map.put(property, value); + } catch (UnsupportedOperationException e) { + throw new PropertyNotWritableException(e); + } + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof Map) { + context.setPropertyResolved(base, property); + return this.readOnly || UNMODIFIABLE.equals(base.getClass()); + } + + return this.readOnly; + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + if (base instanceof Map) { + Iterator itr = ((Map) base).keySet().iterator(); + List feats = new ArrayList<>(); + Object key; + FeatureDescriptor desc; + while (itr.hasNext()) { + key = itr.next(); + desc = new FeatureDescriptor(); + desc.setDisplayName(key.toString()); + desc.setShortDescription(""); + desc.setExpert(false); + desc.setHidden(false); + desc.setName(key.toString()); + desc.setPreferred(true); + desc.setValue(RESOLVABLE_AT_DESIGN_TIME, Boolean.TRUE); + desc.setValue(TYPE, key.getClass()); + feats.add(desc); + } + return feats.iterator(); + } + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base instanceof Map) { + return Object.class; + } + return null; + } + +} diff --git a/java/jakarta/el/MethodExpression.java b/java/jakarta/el/MethodExpression.java new file mode 100644 index 0000000..e8a91c4 --- /dev/null +++ b/java/jakarta/el/MethodExpression.java @@ -0,0 +1,82 @@ +/* + * 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 jakarta.el; + +public abstract class MethodExpression extends Expression { + + private static final long serialVersionUID = 8163925562047324656L; + + /** + * @param context The EL context for this evaluation + * + * @return Information about the method that this expression resolves to + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If a property/variable resolution failed because no match was found or a match + * was found but was not readable + * @throws MethodNotFoundException If no matching method can be found + * @throws ELException Wraps any exception throw whilst resolving the property + */ + public abstract MethodInfo getMethodInfo(ELContext context); + + /** + * @param context The EL context for this evaluation + * @param params The parameters with which to invoke this method expression + * + * @return The result of invoking this method expression + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If a property/variable resolution failed because no match was found or a match + * was found but was not readable + * @throws MethodNotFoundException If no matching method can be found + * @throws ELException Wraps any exception throw whilst resolving the property or coercion of the + * result to the expected return type fails + */ + public abstract Object invoke(ELContext context, Object[] params); + + /** + * @return This default implementation always returns false + * + * @since EL 3.0 + */ + public boolean isParametersProvided() { + // Expected to be over-ridden by implementation + return false; + } + + /** + * Obtain the {@link MethodReference} for the method to which this method expression resolves. + * + * @param context The EL context for this evaluation + * + * @return This default implementation always returns null + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If a property/variable resolution failed because no match was found or a match + * was found but was not readable + * @throws MethodNotFoundException If no matching method can be found + * @throws ELException Wraps any exception throw whilst resolving the property + * + * @since EL 5.0 + */ + public MethodReference getMethodReference(ELContext context) { + // Expected to be over-ridden by implementation + context.notifyBeforeEvaluation(getExpressionString()); + context.notifyAfterEvaluation(getExpressionString()); + return null; + } +} diff --git a/java/jakarta/el/MethodInfo.java b/java/jakarta/el/MethodInfo.java new file mode 100644 index 0000000..8b977a6 --- /dev/null +++ b/java/jakarta/el/MethodInfo.java @@ -0,0 +1,88 @@ +/* + * 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 jakarta.el; + +import java.util.Arrays; + +public class MethodInfo { + + private final String name; + + private final Class[] paramTypes; + + private final Class returnType; + + public MethodInfo(String name, Class returnType, Class[] paramTypes) { + this.name = name; + this.returnType = returnType; + this.paramTypes = paramTypes; + } + + public String getName() { + return this.name; + } + + public Class getReturnType() { + return this.returnType; + } + + public Class[] getParamTypes() { + return this.paramTypes; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + Arrays.hashCode(paramTypes); + result = prime * result + ((returnType == null) ? 0 : returnType.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MethodInfo other = (MethodInfo) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + if (!Arrays.equals(paramTypes, other.paramTypes)) { + return false; + } + if (returnType == null) { + if (other.returnType != null) { + return false; + } + } else if (!returnType.equals(other.returnType)) { + return false; + } + return true; + } +} diff --git a/java/jakarta/el/MethodNotFoundException.java b/java/jakarta/el/MethodNotFoundException.java new file mode 100644 index 0000000..74446f8 --- /dev/null +++ b/java/jakarta/el/MethodNotFoundException.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.el; + +public class MethodNotFoundException extends ELException { + + private static final long serialVersionUID = -3631968116081480328L; + + public MethodNotFoundException() { + super(); + } + + public MethodNotFoundException(String message) { + super(message); + } + + public MethodNotFoundException(Throwable cause) { + super(cause); + } + + public MethodNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/jakarta/el/MethodReference.java b/java/jakarta/el/MethodReference.java new file mode 100644 index 0000000..30065c5 --- /dev/null +++ b/java/jakarta/el/MethodReference.java @@ -0,0 +1,133 @@ +/* + * 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 jakarta.el; + +import java.lang.annotation.Annotation; +import java.util.Arrays; + +/** + * Provides information about the method to which a method expression resolves. + * + * @since EL 5.0 + */ +public class MethodReference { + + private final Object base; + private final MethodInfo methodInfo; + private final Annotation[] annotations; + private final Object[] evaluatedParameters; + + + public MethodReference(Object base, MethodInfo methodInfo, Annotation[] annotations, Object[] evaluatedParameters) { + this.base = base; + this.methodInfo = methodInfo; + this.annotations = annotations; + this.evaluatedParameters = evaluatedParameters; + } + + + /** + * Obtain the base object on which the method will be invoked. + * + * @return The base object on which the method will be invoked or {@code null} for literal method expressions. + */ + public Object getBase() { + return base; + } + + + /** + * Obtain the {@link MethodInfo} for the {@link MethodExpression} for which this {@link MethodReference} has been + * generated. + * + * @return The {@link MethodInfo} for the {@link MethodExpression} for which this {@link MethodReference} has been + * generated. + */ + public MethodInfo getMethodInfo() { + return this.methodInfo; + } + + + /** + * Obtain the annotations on the method to which the associated expression resolves. + * + * @return The annotations on the method to which the associated expression resolves. If the are no annotations, + * then an empty array is returned. + */ + public Annotation[] getAnnotations() { + return annotations; + } + + + /** + * Obtain the evaluated parameter values that will be passed to the method to which the associated expression + * resolves. + * + * @return The evaluated parameters. + */ + public Object[] getEvaluatedParameters() { + return evaluatedParameters; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(annotations); + result = prime * result + ((base == null) ? 0 : base.hashCode()); + result = prime * result + Arrays.deepHashCode(evaluatedParameters); + result = prime * result + ((methodInfo == null) ? 0 : methodInfo.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MethodReference other = (MethodReference) obj; + if (!Arrays.equals(annotations, other.annotations)) { + return false; + } + if (base == null) { + if (other.base != null) { + return false; + } + } else if (!base.equals(other.base)) { + return false; + } + if (!Arrays.deepEquals(evaluatedParameters, other.evaluatedParameters)) { + return false; + } + if (methodInfo == null) { + if (other.methodInfo != null) { + return false; + } + } else if (!methodInfo.equals(other.methodInfo)) { + return false; + } + return true; + } +} diff --git a/java/jakarta/el/PropertyNotFoundException.java b/java/jakarta/el/PropertyNotFoundException.java new file mode 100644 index 0000000..5c4b484 --- /dev/null +++ b/java/jakarta/el/PropertyNotFoundException.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.el; + +public class PropertyNotFoundException extends ELException { + + private static final long serialVersionUID = -3799200961303506745L; + + public PropertyNotFoundException() { + super(); + } + + public PropertyNotFoundException(String message) { + super(message); + } + + public PropertyNotFoundException(Throwable cause) { + super(cause); + } + + public PropertyNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/jakarta/el/PropertyNotWritableException.java b/java/jakarta/el/PropertyNotWritableException.java new file mode 100644 index 0000000..6339984 --- /dev/null +++ b/java/jakarta/el/PropertyNotWritableException.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.el; + +public class PropertyNotWritableException extends ELException { + + private static final long serialVersionUID = 827987155471214717L; + + public PropertyNotWritableException() { + super(); + } + + public PropertyNotWritableException(String message) { + super(message); + } + + public PropertyNotWritableException(Throwable cause) { + super(cause); + } + + public PropertyNotWritableException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/jakarta/el/ResourceBundleELResolver.java b/java/jakarta/el/ResourceBundleELResolver.java new file mode 100644 index 0000000..55b53f8 --- /dev/null +++ b/java/jakarta/el/ResourceBundleELResolver.java @@ -0,0 +1,124 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.MissingResourceException; +import java.util.Objects; +import java.util.ResourceBundle; + +public class ResourceBundleELResolver extends ELResolver { + + public ResourceBundleELResolver() { + super(); + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof ResourceBundle) { + context.setPropertyResolved(base, property); + + if (property != null) { + try { + return ((ResourceBundle) base).getObject(property.toString()); + } catch (MissingResourceException mre) { + return "???" + property.toString() + "???"; + } + } + } + + return null; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof ResourceBundle) { + context.setPropertyResolved(base, property); + /* + * ResourceBundles are always read-only so fall-through to return null + */ + } + + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + + if (base instanceof ResourceBundle) { + context.setPropertyResolved(base, property); + throw new PropertyNotWritableException( + Util.message(context, "resolverNotWritable", base.getClass().getName())); + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof ResourceBundle) { + context.setPropertyResolved(base, property); + return true; + } + + return false; + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + if (base instanceof ResourceBundle) { + List feats = new ArrayList<>(); + Enumeration e = ((ResourceBundle) base).getKeys(); + FeatureDescriptor feat; + String key; + while (e.hasMoreElements()) { + key = e.nextElement(); + feat = new FeatureDescriptor(); + feat.setDisplayName(key); + feat.setShortDescription(""); + feat.setExpert(false); + feat.setHidden(false); + feat.setName(key); + feat.setPreferred(true); + feat.setValue(RESOLVABLE_AT_DESIGN_TIME, Boolean.TRUE); + feat.setValue(TYPE, String.class); + feats.add(feat); + } + return feats.iterator(); + } + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base instanceof ResourceBundle) { + return String.class; + } + return null; + } + +} diff --git a/java/jakarta/el/StandardELContext.java b/java/jakarta/el/StandardELContext.java new file mode 100644 index 0000000..11121b7 --- /dev/null +++ b/java/jakarta/el/StandardELContext.java @@ -0,0 +1,203 @@ +/* + * 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 jakarta.el; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + * @since EL 3.0 + */ +public class StandardELContext extends ELContext { + + private final ELContext wrappedContext; + private final VariableMapper variableMapper; + private final FunctionMapper functionMapper; + private final CompositeELResolver standardResolver; + private final CompositeELResolver customResolvers; + private final Map localBeans = new HashMap<>(); + + + public StandardELContext(ExpressionFactory factory) { + wrappedContext = null; + variableMapper = new StandardVariableMapper(); + functionMapper = new StandardFunctionMapper(factory.getInitFunctionMap()); + standardResolver = new CompositeELResolver(); + customResolvers = new CompositeELResolver(); + + ELResolver streamResolver = factory.getStreamELResolver(); + + // Add resolvers in order + standardResolver.add(new BeanNameELResolver(new StandardBeanNameResolver(localBeans))); + standardResolver.add(customResolvers); + if (streamResolver != null) { + standardResolver.add(streamResolver); + } + standardResolver.add(new StaticFieldELResolver()); + standardResolver.add(new MapELResolver()); + standardResolver.add(new ResourceBundleELResolver()); + standardResolver.add(new ListELResolver()); + standardResolver.add(new ArrayELResolver()); + standardResolver.add(new BeanELResolver()); + } + + public StandardELContext(ELContext context) { + wrappedContext = context; + variableMapper = context.getVariableMapper(); + functionMapper = context.getFunctionMapper(); + standardResolver = new CompositeELResolver(); + customResolvers = new CompositeELResolver(); + + // Add resolvers in order + standardResolver.add(new BeanNameELResolver(new StandardBeanNameResolver(localBeans))); + standardResolver.add(customResolvers); + // Use resolvers from context from this point on + standardResolver.add(context.getELResolver()); + } + + @Override + public void putContext(Class key, Object contextObject) { + if (wrappedContext == null) { + super.putContext(key, contextObject); + } else { + wrappedContext.putContext(key, contextObject); + } + } + + @Override + public Object getContext(Class key) { + if (wrappedContext == null) { + return super.getContext(key); + } else { + return wrappedContext.getContext(key); + } + } + + @Override + public ELResolver getELResolver() { + return standardResolver; + } + + public void addELResolver(ELResolver resolver) { + customResolvers.add(resolver); + } + + @Override + public FunctionMapper getFunctionMapper() { + return functionMapper; + } + + @Override + public VariableMapper getVariableMapper() { + return variableMapper; + } + + + Map getLocalBeans() { + return localBeans; + } + + + private static class StandardVariableMapper extends VariableMapper { + + private Map vars; + + @Override + public ValueExpression resolveVariable(String variable) { + if (vars == null) { + return null; + } + return vars.get(variable); + } + + @Override + public ValueExpression setVariable(String variable, ValueExpression expression) { + if (vars == null) { + vars = new HashMap<>(); + } + if (expression == null) { + return vars.remove(variable); + } else { + return vars.put(variable, expression); + } + } + } + + + private static class StandardBeanNameResolver extends BeanNameResolver { + + private final Map beans; + + StandardBeanNameResolver(Map beans) { + this.beans = beans; + } + + @Override + public boolean isNameResolved(String beanName) { + return beans.containsKey(beanName); + } + + @Override + public Object getBean(String beanName) { + return beans.get(beanName); + } + + @Override + public void setBeanValue(String beanName, Object value) throws PropertyNotWritableException { + beans.put(beanName, value); + } + + @Override + public boolean isReadOnly(String beanName) { + return false; + } + + @Override + public boolean canCreateBean(String beanName) { + return true; + } + } + + + private static class StandardFunctionMapper extends FunctionMapper { + + private final Map methods = new HashMap<>(); + + StandardFunctionMapper(Map initFunctionMap) { + if (initFunctionMap != null) { + methods.putAll(initFunctionMap); + } + } + + @Override + public Method resolveFunction(String prefix, String localName) { + String key = prefix + ':' + localName; + return methods.get(key); + } + + @Override + public void mapFunction(String prefix, String localName, Method method) { + String key = prefix + ':' + localName; + if (method == null) { + methods.remove(key); + } else { + methods.put(key, method); + } + } + } +} diff --git a/java/jakarta/el/StaticFieldELResolver.java b/java/jakarta/el/StaticFieldELResolver.java new file mode 100644 index 0000000..db70f6e --- /dev/null +++ b/java/jakarta/el/StaticFieldELResolver.java @@ -0,0 +1,194 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Iterator; +import java.util.Objects; + +/** + * @since EL 3.0 + */ +public class StaticFieldELResolver extends ELResolver { + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof ELClass && property instanceof String) { + context.setPropertyResolved(base, property); + + Class clazz = ((ELClass) base).getKlass(); + String name = (String) property; + Exception exception = null; + try { + Field field = clazz.getField(name); + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) && Util.canAccess(null, field)) { + return field.get(null); + } + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { + exception = e; + } + String msg = Util.message(context, "staticFieldELResolver.notFound", name, clazz.getName()); + if (exception == null) { + throw new PropertyNotFoundException(msg); + } else { + throw new PropertyNotFoundException(msg, exception); + } + } + return null; + } + + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + + if (base instanceof ELClass && property instanceof String) { + Class clazz = ((ELClass) base).getKlass(); + String name = (String) property; + + throw new PropertyNotWritableException( + Util.message(context, "staticFieldELResolver.notWritable", name, clazz.getName())); + } + } + + + @Override + public Object invoke(ELContext context, Object base, Object method, Class[] paramTypes, Object[] params) { + Objects.requireNonNull(context); + + if (base instanceof ELClass && method instanceof String) { + context.setPropertyResolved(base, method); + + Class clazz = ((ELClass) base).getKlass(); + String methodName = (String) method; + + if ("".equals(methodName)) { + Constructor match = Util.findConstructor(context, clazz, paramTypes, params); + + Object[] parameters = + Util.buildParameters(context, match.getParameterTypes(), match.isVarArgs(), params); + + Object result = null; + + try { + result = match.newInstance(parameters); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + Util.handleThrowable(cause); + throw new ELException(cause); + } catch (ReflectiveOperationException e) { + throw new ELException(e); + } + return result; + + } else { + // Static method so base should be null + Method match = Util.findMethod(context, clazz, null, methodName, paramTypes, params); + + if (match == null) { + throw new MethodNotFoundException( + Util.message(context, "staticFieldELResolver.methodNotFound", methodName, clazz.getName())); + } + + Object[] parameters = + Util.buildParameters(context, match.getParameterTypes(), match.isVarArgs(), params); + + Object result = null; + try { + result = match.invoke(null, parameters); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new ELException(e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + Util.handleThrowable(cause); + throw new ELException(cause); + } + return result; + } + } + return null; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof ELClass && property instanceof String) { + context.setPropertyResolved(base, property); + + Class clazz = ((ELClass) base).getKlass(); + String name = (String) property; + Exception exception = null; + try { + Field field = clazz.getField(name); + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) && Util.canAccess(null, field)) { + // Resolver is read-only so returns null for resolved fields + return null; + } + } catch (IllegalArgumentException | NoSuchFieldException | SecurityException e) { + exception = e; + } + String msg = Util.message(context, "staticFieldELResolver.notFound", name, clazz.getName()); + if (exception == null) { + throw new PropertyNotFoundException(msg); + } else { + throw new PropertyNotFoundException(msg, exception); + } + } + return null; + } + + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base instanceof ELClass && property instanceof String) { + context.setPropertyResolved(base, property); + } + return true; + } + + + /** + * Always returns null. + * + * @deprecated This method will be removed, without replacement, in EL 6.0 / Tomcat 11. + */ + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return null; + } + + /** + * @return This resolver always returns String.class + */ + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return String.class; + } +} diff --git a/java/jakarta/el/TypeConverter.java b/java/jakarta/el/TypeConverter.java new file mode 100644 index 0000000..bc97a1e --- /dev/null +++ b/java/jakarta/el/TypeConverter.java @@ -0,0 +1,60 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.util.Iterator; + +/** + * @since EL 3.0 + */ +public abstract class TypeConverter extends ELResolver { + + @Override + public Object getValue(ELContext context, Object base, Object property) { + return null; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + // NO-OP + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + return false; + } + + @Deprecated(forRemoval = true, since = "EL 5.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return null; + } + + @Override + public abstract T convertToType(ELContext context, Object obj, Class type); +} diff --git a/java/jakarta/el/Util.java b/java/jakarta/el/Util.java new file mode 100644 index 0000000..b566852 --- /dev/null +++ b/java/jakarta/el/Util.java @@ -0,0 +1,862 @@ +/* + * 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 jakarta.el; + +import java.lang.ref.WeakReference; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +class Util { + + private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + private static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); + + private static final boolean GET_CLASSLOADER_USE_PRIVILEGED; + + static { + if (IS_SECURITY_ENABLED) { + // Defaults to using a privileged block + // When running on Tomcat this will be set to false in + // $CATALINA_BASE/conf/catalina.properties + String value = AccessController.doPrivileged((PrivilegedAction) () -> System + .getProperty("org.apache.el.GET_CLASSLOADER_USE_PRIVILEGED", "true")); + GET_CLASSLOADER_USE_PRIVILEGED = Boolean.parseBoolean(value); + } else { + // No security manager - no need to use a privileged block. + GET_CLASSLOADER_USE_PRIVILEGED = false; + } + } + + + /** + * Checks whether the supplied Throwable is one that needs to be rethrown and swallows all others. + * + * @param t the Throwable to check + */ + static void handleThrowable(Throwable t) { + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + // All other instances of Throwable will be silently swallowed + } + + + static String message(ELContext context, String name, Object... props) { + Locale locale = null; + if (context != null) { + locale = context.getLocale(); + } + if (locale == null) { + locale = Locale.getDefault(); + if (locale == null) { + return ""; + } + } + ResourceBundle bundle = ResourceBundle.getBundle("jakarta.el.LocalStrings", locale); + try { + String template = bundle.getString(name); + if (props != null) { + template = MessageFormat.format(template, props); + } + return template; + } catch (MissingResourceException e) { + return "Missing Resource: '" + name + "' for Locale " + locale.getDisplayName(); + } + } + + + private static final CacheValue nullTcclFactory = new CacheValue(); + private static final Map factoryCache = new ConcurrentHashMap<>(); + + /** + * Provides a per class loader cache of ExpressionFactory instances without pinning any in memory as that could + * trigger a memory leak. + */ + static ExpressionFactory getExpressionFactory() { + + ClassLoader tccl = getContextClassLoader(); + + CacheValue cacheValue = null; + ExpressionFactory factory = null; + + if (tccl == null) { + cacheValue = nullTcclFactory; + } else { + CacheKey key = new CacheKey(tccl); + cacheValue = factoryCache.get(key); + if (cacheValue == null) { + CacheValue newCacheValue = new CacheValue(); + cacheValue = factoryCache.putIfAbsent(key, newCacheValue); + if (cacheValue == null) { + cacheValue = newCacheValue; + } + } + } + + final Lock readLock = cacheValue.getLock().readLock(); + readLock.lock(); + try { + factory = cacheValue.getExpressionFactory(); + } finally { + readLock.unlock(); + } + + if (factory == null) { + final Lock writeLock = cacheValue.getLock().writeLock(); + writeLock.lock(); + try { + factory = cacheValue.getExpressionFactory(); + if (factory == null) { + factory = ExpressionFactory.newInstance(); + cacheValue.setExpressionFactory(factory); + } + } finally { + writeLock.unlock(); + } + } + + return factory; + } + + + /** + * Key used to cache default ExpressionFactory information per class loader. The class loader reference is never + * {@code null}, because {@code null} tccl is handled separately. + */ + private static class CacheKey { + private final int hash; + private final WeakReference ref; + + CacheKey(ClassLoader key) { + hash = key.hashCode(); + ref = new WeakReference<>(key); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof CacheKey)) { + return false; + } + ClassLoader thisKey = ref.get(); + if (thisKey == null) { + return false; + } + return thisKey == ((CacheKey) obj).ref.get(); + } + } + + private static class CacheValue { + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private WeakReference ref; + + CacheValue() { + } + + public ReadWriteLock getLock() { + return lock; + } + + public ExpressionFactory getExpressionFactory() { + return ref != null ? ref.get() : null; + } + + public void setExpressionFactory(ExpressionFactory factory) { + ref = new WeakReference<>(factory); + } + } + + + /* + * This method duplicates code in org.apache.el.util.ReflectionUtil. When making changes keep the code in sync. + */ + static Method findMethod(ELContext context, Class clazz, Object base, String methodName, Class[] paramTypes, + Object[] paramValues) { + + if (clazz == null || methodName == null) { + throw new MethodNotFoundException( + message(null, "util.method.notfound", clazz, methodName, paramString(paramTypes))); + } + + if (paramTypes == null) { + paramTypes = getTypesFromValues(paramValues); + } + + Method[] methods = clazz.getMethods(); + + List> wrappers = Wrapper.wrap(methods, methodName); + + Wrapper result = findWrapper(context, clazz, wrappers, methodName, paramTypes, paramValues); + + return getMethod(clazz, base, result.unWrap()); + } + + /* + * This method duplicates code in org.apache.el.util.ReflectionUtil. When making changes keep the code in sync. + */ + @SuppressWarnings("null") + private static Wrapper findWrapper(ELContext context, Class clazz, List> wrappers, String name, + Class[] paramTypes, Object[] paramValues) { + + Map,MatchResult> candidates = new HashMap<>(); + + int paramCount = paramTypes.length; + + for (Wrapper w : wrappers) { + Class[] mParamTypes = w.getParameterTypes(); + int mParamCount; + if (mParamTypes == null) { + mParamCount = 0; + } else { + mParamCount = mParamTypes.length; + } + + // Check the number of parameters + // Multiple tests to improve readability + if (!w.isVarArgs() && paramCount != mParamCount) { + // Method has wrong number of parameters + continue; + } + if (w.isVarArgs() && paramCount < mParamCount - 1) { + // Method has wrong number of parameters + continue; + } + if (w.isVarArgs() && paramCount == mParamCount && paramValues != null && paramValues.length > paramCount && + !paramTypes[mParamCount - 1].isArray()) { + // Method arguments don't match + continue; + } + if (w.isVarArgs() && paramCount > mParamCount && paramValues != null && paramValues.length != paramCount) { + // Might match a different varargs method + continue; + } + if (!w.isVarArgs() && paramValues != null && paramCount != paramValues.length) { + // Might match a different varargs method + continue; + } + + // Check the parameters match + int exactMatch = 0; + int assignableMatch = 0; + int coercibleMatch = 0; + int varArgsMatch = 0; + boolean noMatch = false; + for (int i = 0; i < mParamCount; i++) { + // Can't be null + if (w.isVarArgs() && i == (mParamCount - 1)) { + if (i == paramCount || (paramValues != null && paramValues.length == i)) { + // Var args defined but nothing is passed as varargs + // Use MAX_VALUE so this matches only if nothing else does + varArgsMatch = Integer.MAX_VALUE; + break; + } + Class varType = mParamTypes[i].getComponentType(); + for (int j = i; j < paramCount; j++) { + if (isAssignableFrom(paramTypes[j], varType)) { + assignableMatch++; + varArgsMatch++; + } else { + if (paramValues == null) { + noMatch = true; + break; + } else { + if (isCoercibleFrom(context, paramValues[j], varType)) { + coercibleMatch++; + varArgsMatch++; + } else { + noMatch = true; + break; + } + } + } + // Don't treat a varArgs match as an exact match, it can + // lead to a varArgs method matching when the result + // should be ambiguous + } + } else { + if (mParamTypes[i].equals(paramTypes[i])) { + exactMatch++; + } else if (paramTypes[i] != null && isAssignableFrom(paramTypes[i], mParamTypes[i])) { + assignableMatch++; + } else { + if (paramValues == null) { + noMatch = true; + break; + } else { + if (isCoercibleFrom(context, paramValues[i], mParamTypes[i])) { + coercibleMatch++; + } else { + noMatch = true; + break; + } + } + } + } + } + if (noMatch) { + continue; + } + + // If a method is found where every parameter matches exactly, + // and no vars args are present, return it + if (exactMatch == paramCount && varArgsMatch == 0) { + return w; + } + + candidates.put(w, new MatchResult(w.isVarArgs(), exactMatch, assignableMatch, coercibleMatch, varArgsMatch, + w.isBridge())); + } + + // Look for the method that has the highest number of parameters where + // the type matches exactly + MatchResult bestMatch = new MatchResult(true, 0, 0, 0, 0, true); + Wrapper match = null; + boolean multiple = false; + for (Map.Entry,MatchResult> entry : candidates.entrySet()) { + int cmp = entry.getValue().compareTo(bestMatch); + if (cmp > 0 || match == null) { + bestMatch = entry.getValue(); + match = entry.getKey(); + multiple = false; + } else if (cmp == 0) { + multiple = true; + } + } + if (multiple) { + if (bestMatch.getExactCount() == paramCount - 1) { + // Only one parameter is not an exact match - try using the + // super class + match = resolveAmbiguousWrapper(candidates.keySet(), paramTypes); + } else { + match = null; + } + + if (match == null) { + // If multiple methods have the same matching number of parameters + // the match is ambiguous so throw an exception + throw new MethodNotFoundException( + message(null, "util.method.ambiguous", clazz, name, paramString(paramTypes))); + } + } + + // Handle case where no match at all was found + if (match == null) { + throw new MethodNotFoundException( + message(null, "util.method.notfound", clazz, name, paramString(paramTypes))); + } + + return match; + } + + + private static String paramString(Class[] types) { + if (types != null) { + StringBuilder sb = new StringBuilder(); + for (Class type : types) { + if (type == null) { + sb.append("null, "); + } else { + sb.append(type.getName()).append(", "); + } + } + if (sb.length() > 2) { + sb.setLength(sb.length() - 2); + } + return sb.toString(); + } + return null; + } + + + /* + * This method duplicates code in org.apache.el.util.ReflectionUtil. When making changes keep the code in sync. + */ + private static Wrapper resolveAmbiguousWrapper(Set> candidates, Class[] paramTypes) { + // Identify which parameter isn't an exact match + Wrapper w = candidates.iterator().next(); + + int nonMatchIndex = 0; + Class nonMatchClass = null; + + for (int i = 0; i < paramTypes.length; i++) { + if (w.getParameterTypes()[i] != paramTypes[i]) { + nonMatchIndex = i; + nonMatchClass = paramTypes[i]; + break; + } + } + + if (nonMatchClass == null) { + // Null will always be ambiguous + return null; + } + + for (Wrapper c : candidates) { + if (c.getParameterTypes()[nonMatchIndex] == paramTypes[nonMatchIndex]) { + // Methods have different non-matching parameters + // Result is ambiguous + return null; + } + } + + // Can't be null + Class superClass = nonMatchClass.getSuperclass(); + while (superClass != null) { + for (Wrapper c : candidates) { + if (c.getParameterTypes()[nonMatchIndex].equals(superClass)) { + // Found a match + return c; + } + } + superClass = superClass.getSuperclass(); + } + + // Treat instances of Number as a special case + Wrapper match = null; + if (Number.class.isAssignableFrom(nonMatchClass)) { + for (Wrapper c : candidates) { + Class candidateType = c.getParameterTypes()[nonMatchIndex]; + if (Number.class.isAssignableFrom(candidateType) || candidateType.isPrimitive()) { + if (match == null) { + match = c; + } else { + // Match still ambiguous + match = null; + break; + } + } + } + } + + return match; + } + + + /* + * This method duplicates code in org.apache.el.util.ReflectionUtil. When making changes keep the code in sync. + */ + static boolean isAssignableFrom(Class src, Class target) { + // src will always be an object + // Short-cut. null is always assignable to an object and in EL null + // can always be coerced to a valid value for a primitive + if (src == null) { + return true; + } + + Class targetClass; + if (target.isPrimitive()) { + if (target == Boolean.TYPE) { + targetClass = Boolean.class; + } else if (target == Character.TYPE) { + targetClass = Character.class; + } else if (target == Byte.TYPE) { + targetClass = Byte.class; + } else if (target == Short.TYPE) { + targetClass = Short.class; + } else if (target == Integer.TYPE) { + targetClass = Integer.class; + } else if (target == Long.TYPE) { + targetClass = Long.class; + } else if (target == Float.TYPE) { + targetClass = Float.class; + } else { + targetClass = Double.class; + } + } else { + targetClass = target; + } + return targetClass.isAssignableFrom(src); + } + + + /* + * This method duplicates code in org.apache.el.util.ReflectionUtil. When making changes keep the code in sync. + */ + private static boolean isCoercibleFrom(ELContext context, Object src, Class target) { + // TODO: This isn't pretty but it works. Significant refactoring would + // be required to avoid the exception. + try { + context.convertToType(src, target); + } catch (ELException e) { + return false; + } + return true; + } + + + private static Class[] getTypesFromValues(Object[] values) { + if (values == null) { + return EMPTY_CLASS_ARRAY; + } + + Class result[] = new Class[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + result[i] = null; + } else { + result[i] = values[i].getClass(); + } + } + return result; + } + + + /* + * This method duplicates code in org.apache.el.util.ReflectionUtil. When making changes keep the code in sync. + */ + static Method getMethod(Class type, Object base, Method m) { + if (m == null || (Modifier.isPublic(type.getModifiers()) && + (Modifier.isStatic(m.getModifiers()) && canAccess(null, m) || canAccess(base, m)))) { + return m; + } + Class[] interfaces = type.getInterfaces(); + Method mp = null; + for (Class iface : interfaces) { + try { + mp = iface.getMethod(m.getName(), m.getParameterTypes()); + mp = getMethod(mp.getDeclaringClass(), base, mp); + if (mp != null) { + return mp; + } + } catch (NoSuchMethodException e) { + // Ignore + } + } + Class sup = type.getSuperclass(); + if (sup != null) { + try { + mp = sup.getMethod(m.getName(), m.getParameterTypes()); + mp = getMethod(mp.getDeclaringClass(), base, mp); + if (mp != null) { + return mp; + } + } catch (NoSuchMethodException e) { + // Ignore + } + } + return null; + } + + + static Constructor findConstructor(ELContext context, Class clazz, Class[] paramTypes, + Object[] paramValues) { + + String methodName = ""; + + if (clazz == null) { + throw new MethodNotFoundException( + message(null, "util.method.notfound", null, methodName, paramString(paramTypes))); + } + + if (paramTypes == null) { + paramTypes = getTypesFromValues(paramValues); + } + + Constructor[] constructors = clazz.getConstructors(); + + List>> wrappers = Wrapper.wrap(constructors); + + Wrapper> wrapper = findWrapper(context, clazz, wrappers, methodName, paramTypes, paramValues); + + Constructor constructor = wrapper.unWrap(); + + if (!Modifier.isPublic(clazz.getModifiers()) || !canAccess(null, constructor)) { + throw new MethodNotFoundException( + message(null, "util.method.notfound", clazz, methodName, paramString(paramTypes))); + } + + return constructor; + } + + + static boolean canAccess(Object base, AccessibleObject accessibleObject) { + try { + return accessibleObject.canAccess(base); + } catch (IllegalArgumentException iae) { + return false; + } + } + + + static Object[] buildParameters(ELContext context, Class[] parameterTypes, boolean isVarArgs, Object[] params) { + Object[] parameters = null; + if (parameterTypes.length > 0) { + parameters = new Object[parameterTypes.length]; + int paramCount; + if (params == null) { + params = EMPTY_OBJECT_ARRAY; + } + paramCount = params.length; + if (isVarArgs) { + int varArgIndex = parameterTypes.length - 1; + // First argCount-1 parameters are standard + for (int i = 0; (i < varArgIndex); i++) { + parameters[i] = context.convertToType(params[i], parameterTypes[i]); + } + // Last parameter is the varargs + Class varArgClass = parameterTypes[varArgIndex].getComponentType(); + final Object varargs = Array.newInstance(varArgClass, (paramCount - varArgIndex)); + for (int i = (varArgIndex); i < paramCount; i++) { + Array.set(varargs, i - varArgIndex, context.convertToType(params[i], varArgClass)); + } + parameters[varArgIndex] = varargs; + } else { + parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + parameters[i] = context.convertToType(params[i], parameterTypes[i]); + } + } + } + return parameters; + } + + + static ClassLoader getContextClassLoader() { + ClassLoader tccl; + if (IS_SECURITY_ENABLED && GET_CLASSLOADER_USE_PRIVILEGED) { + PrivilegedAction pa = new PrivilegedGetTccl(); + tccl = AccessController.doPrivileged(pa); + } else { + tccl = Thread.currentThread().getContextClassLoader(); + } + + return tccl; + } + + + private abstract static class Wrapper { + + public static List> wrap(Method[] methods, String name) { + List> result = new ArrayList<>(); + for (Method method : methods) { + if (method.getName().equals(name)) { + result.add(new MethodWrapper(method)); + } + } + return result; + } + + public static List>> wrap(Constructor[] constructors) { + List>> result = new ArrayList<>(); + for (Constructor constructor : constructors) { + result.add(new ConstructorWrapper(constructor)); + } + return result; + } + + public abstract T unWrap(); + + public abstract Class[] getParameterTypes(); + + public abstract boolean isVarArgs(); + + public abstract boolean isBridge(); + } + + + private static class MethodWrapper extends Wrapper { + private final Method m; + + MethodWrapper(Method m) { + this.m = m; + } + + @Override + public Method unWrap() { + return m; + } + + @Override + public Class[] getParameterTypes() { + return m.getParameterTypes(); + } + + @Override + public boolean isVarArgs() { + return m.isVarArgs(); + } + + @Override + public boolean isBridge() { + return m.isBridge(); + } + } + + private static class ConstructorWrapper extends Wrapper> { + private final Constructor c; + + ConstructorWrapper(Constructor c) { + this.c = c; + } + + @Override + public Constructor unWrap() { + return c; + } + + @Override + public Class[] getParameterTypes() { + return c.getParameterTypes(); + } + + @Override + public boolean isVarArgs() { + return c.isVarArgs(); + } + + @Override + public boolean isBridge() { + return false; + } + } + + /* + * This class duplicates code in org.apache.el.util.ReflectionUtil. When making changes keep the code in sync. + */ + private static class MatchResult implements Comparable { + + private final boolean varArgs; + private final int exactCount; + private final int assignableCount; + private final int coercibleCount; + private final int varArgsCount; + private final boolean bridge; + + MatchResult(boolean varArgs, int exactCount, int assignableCount, int coercibleCount, int varArgsCount, + boolean bridge) { + this.varArgs = varArgs; + this.exactCount = exactCount; + this.assignableCount = assignableCount; + this.coercibleCount = coercibleCount; + this.varArgsCount = varArgsCount; + this.bridge = bridge; + } + + public boolean isVarArgs() { + return varArgs; + } + + public int getExactCount() { + return exactCount; + } + + public int getAssignableCount() { + return assignableCount; + } + + public int getCoercibleCount() { + return coercibleCount; + } + + public int getVarArgsCount() { + return varArgsCount; + } + + public boolean isBridge() { + return bridge; + } + + @Override + public int compareTo(MatchResult o) { + // Non-varArgs always beats varArgs + int cmp = Boolean.compare(o.isVarArgs(), this.isVarArgs()); + if (cmp == 0) { + cmp = Integer.compare(this.getExactCount(), o.getExactCount()); + if (cmp == 0) { + cmp = Integer.compare(this.getAssignableCount(), o.getAssignableCount()); + if (cmp == 0) { + cmp = Integer.compare(this.getCoercibleCount(), o.getCoercibleCount()); + if (cmp == 0) { + // Fewer var args matches are better + cmp = Integer.compare(o.getVarArgsCount(), this.getVarArgsCount()); + if (cmp == 0) { + // The nature of bridge methods is such that it actually + // doesn't matter which one we pick as long as we pick + // one. That said, pick the 'right' one (the non-bridge + // one) anyway. + cmp = Boolean.compare(o.isBridge(), this.isBridge()); + } + } + } + } + } + return cmp; + } + + @Override + public boolean equals(Object o) { + return o == this || (null != o && this.getClass().equals(o.getClass()) && + ((MatchResult) o).getExactCount() == this.getExactCount() && + ((MatchResult) o).getAssignableCount() == this.getAssignableCount() && + ((MatchResult) o).getCoercibleCount() == this.getCoercibleCount() && + ((MatchResult) o).getVarArgsCount() == this.getVarArgsCount() && + ((MatchResult) o).isVarArgs() == this.isVarArgs() && + ((MatchResult) o).isBridge() == this.isBridge()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + assignableCount; + result = prime * result + (bridge ? 1231 : 1237); + result = prime * result + coercibleCount; + result = prime * result + exactCount; + result = prime * result + (varArgs ? 1231 : 1237); + result = prime * result + varArgsCount; + return result; + } + } + + + private static class PrivilegedGetTccl implements PrivilegedAction { + @Override + public ClassLoader run() { + return Thread.currentThread().getContextClassLoader(); + } + } +} diff --git a/java/jakarta/el/ValueExpression.java b/java/jakarta/el/ValueExpression.java new file mode 100644 index 0000000..33b04eb --- /dev/null +++ b/java/jakarta/el/ValueExpression.java @@ -0,0 +1,87 @@ +/* + * 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 jakarta.el; + +public abstract class ValueExpression extends Expression { + + private static final long serialVersionUID = 8577809572381654673L; + + /** + * @param The expected type for the result of evaluating this value expression + * @param context The EL context for this evaluation + * + * @return The result of evaluating this value expression + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If a property/variable resolution failed because no match was found or a match + * was found but was not readable + * @throws ELException Wraps any exception throw whilst resolving a property or variable + */ + public abstract T getValue(ELContext context); + + /** + * @param context The EL context for this evaluation + * @param value The value to set the property to which this value expression refers + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If a property/variable resolution failed because no match was found + * @throws PropertyNotWritableException If a property/variable resolution failed because a match was found but was + * not writable + * @throws ELException Wraps any exception throw whilst resolving a property or variable + */ + public abstract void setValue(ELContext context, Object value); + + /** + * @param context The EL context for this evaluation + * + * @return true if this expression is read only otherwise false + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If a property/variable resolution failed because no match was found or a match + * was found but was not readable + * @throws ELException Wraps any exception throw whilst resolving a property or variable + */ + public abstract boolean isReadOnly(ELContext context); + + /** + * @param context The EL context for this evaluation + * + * @return The type of the result of this value expression + * + * @throws NullPointerException If the supplied context is null + * @throws PropertyNotFoundException If a property/variable resolution failed because no match was found or a match + * was found but was not readable + * @throws ELException Wraps any exception throw whilst resolving a property or variable + */ + public abstract Class getType(ELContext context); + + public abstract Class getExpectedType(); + + /** + * @param context The EL context for this evaluation + * + * @return This default implementation always returns null + * + * @since EL 2.2 + */ + public ValueReference getValueReference(ELContext context) { + // Expected to be over-ridden by implementation + context.notifyBeforeEvaluation(getExpressionString()); + context.notifyAfterEvaluation(getExpressionString()); + return null; + } +} diff --git a/java/jakarta/el/ValueReference.java b/java/jakarta/el/ValueReference.java new file mode 100644 index 0000000..9148f26 --- /dev/null +++ b/java/jakarta/el/ValueReference.java @@ -0,0 +1,43 @@ +/* + * 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 jakarta.el; + +import java.io.Serializable; + +/** + * @since EL 2.2 + */ +public class ValueReference implements Serializable { + + private static final long serialVersionUID = 1L; + + private final Object base; + private final Object property; + + public ValueReference(Object base, Object property) { + this.base = base; + this.property = property; + } + + public Object getBase() { + return base; + } + + public Object getProperty() { + return property; + } +} diff --git a/java/jakarta/el/VariableMapper.java b/java/jakarta/el/VariableMapper.java new file mode 100644 index 0000000..79018e0 --- /dev/null +++ b/java/jakarta/el/VariableMapper.java @@ -0,0 +1,27 @@ +/* + * 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 jakarta.el; + +/** + * + */ +public abstract class VariableMapper { + + public abstract ValueExpression resolveVariable(String variable); + + public abstract ValueExpression setVariable(String variable, ValueExpression expression); +} diff --git a/java/jakarta/mail/Authenticator.java b/java/jakarta/mail/Authenticator.java new file mode 100644 index 0000000..6586675 --- /dev/null +++ b/java/jakarta/mail/Authenticator.java @@ -0,0 +1,23 @@ +/* + * 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 jakarta.mail; + +public class Authenticator { + protected PasswordAuthentication getPasswordAuthentication() { + return null; + } +} diff --git a/java/jakarta/mail/PasswordAuthentication.java b/java/jakarta/mail/PasswordAuthentication.java new file mode 100644 index 0000000..7be7b29 --- /dev/null +++ b/java/jakarta/mail/PasswordAuthentication.java @@ -0,0 +1,24 @@ +/* + * 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 jakarta.mail; + +@SuppressWarnings("unused") // Dummy implementation +public class PasswordAuthentication { + public PasswordAuthentication(String user, String password) { + // Dummy implementation + } +} diff --git a/java/jakarta/mail/Session.java b/java/jakarta/mail/Session.java new file mode 100644 index 0000000..87ff869 --- /dev/null +++ b/java/jakarta/mail/Session.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.mail; + +import java.util.Properties; + +@SuppressWarnings("unused") // Dummy implementation +public class Session { + public static Session getInstance(Properties props, Authenticator auth) { + return null; + } + + public static Session getInstance(Properties props) { + return null; + } +} diff --git a/java/jakarta/mail/internet/InternetAddress.java b/java/jakarta/mail/internet/InternetAddress.java new file mode 100644 index 0000000..aa1ce38 --- /dev/null +++ b/java/jakarta/mail/internet/InternetAddress.java @@ -0,0 +1,24 @@ +/* + * 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 jakarta.mail.internet; + +@SuppressWarnings("unused") // Dummy implementation +public class InternetAddress { + public InternetAddress(String from) { + // Dummy implementation + } +} diff --git a/java/jakarta/mail/internet/MimeMessage.java b/java/jakarta/mail/internet/MimeMessage.java new file mode 100644 index 0000000..39644f0 --- /dev/null +++ b/java/jakarta/mail/internet/MimeMessage.java @@ -0,0 +1,34 @@ +/* + * 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 jakarta.mail.internet; + +import jakarta.mail.Session; + +@SuppressWarnings("unused") // Dummy implementation +public class MimeMessage implements MimePart { + public MimeMessage(Session session) { + // Dummy implementation + } + + public void setFrom(InternetAddress from) { + // Dummy implementation + } + + public void setSubject(String subject) { + // Dummy implementation + } +} diff --git a/java/jakarta/mail/internet/MimePart.java b/java/jakarta/mail/internet/MimePart.java new file mode 100644 index 0000000..7591634 --- /dev/null +++ b/java/jakarta/mail/internet/MimePart.java @@ -0,0 +1,21 @@ +/* + * 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 jakarta.mail.internet; + +public interface MimePart { + // Dummy implementation +} diff --git a/java/jakarta/mail/internet/MimePartDataSource.java b/java/jakarta/mail/internet/MimePartDataSource.java new file mode 100644 index 0000000..a86bae5 --- /dev/null +++ b/java/jakarta/mail/internet/MimePartDataSource.java @@ -0,0 +1,24 @@ +/* + * 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 jakarta.mail.internet; + +@SuppressWarnings("unused") // Dummy implementation +public class MimePartDataSource { + public MimePartDataSource(MimePart part) { + // Dummy implementation + } +} diff --git a/java/jakarta/persistence/PersistenceContext.java b/java/jakarta/persistence/PersistenceContext.java new file mode 100644 index 0000000..78ece9e --- /dev/null +++ b/java/jakarta/persistence/PersistenceContext.java @@ -0,0 +1,37 @@ +/* + * 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 jakarta.persistence; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) + +public @interface PersistenceContext { + String name() default ""; + + String unitName() default ""; + + PersistenceContextType type() default PersistenceContextType.TRANSACTION; + + PersistenceProperty[] properties() default {}; + + SynchronizationType synchronization() default SynchronizationType.SYNCHRONIZED; +} diff --git a/java/jakarta/persistence/PersistenceContextType.java b/java/jakarta/persistence/PersistenceContextType.java new file mode 100644 index 0000000..0e54e3c --- /dev/null +++ b/java/jakarta/persistence/PersistenceContextType.java @@ -0,0 +1,22 @@ +/* + * 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 jakarta.persistence; + +public enum PersistenceContextType { + TRANSACTION, + EXTENDED +} \ No newline at end of file diff --git a/java/jakarta/persistence/PersistenceContexts.java b/java/jakarta/persistence/PersistenceContexts.java new file mode 100644 index 0000000..671d117 --- /dev/null +++ b/java/jakarta/persistence/PersistenceContexts.java @@ -0,0 +1,29 @@ +/* + * 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 jakarta.persistence; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) + +public @interface PersistenceContexts { + PersistenceContext[] value(); +} diff --git a/java/jakarta/persistence/PersistenceProperty.java b/java/jakarta/persistence/PersistenceProperty.java new file mode 100644 index 0000000..3ed05e1 --- /dev/null +++ b/java/jakarta/persistence/PersistenceProperty.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.persistence; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({}) +@Retention(RetentionPolicy.RUNTIME) + +public @interface PersistenceProperty { + String name(); + + String value(); +} diff --git a/java/jakarta/persistence/PersistenceUnit.java b/java/jakarta/persistence/PersistenceUnit.java new file mode 100644 index 0000000..0580f11 --- /dev/null +++ b/java/jakarta/persistence/PersistenceUnit.java @@ -0,0 +1,31 @@ +/* + * 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 jakarta.persistence; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) + +public @interface PersistenceUnit { + String name() default ""; + + String unitName() default ""; +} diff --git a/java/jakarta/persistence/PersistenceUnits.java b/java/jakarta/persistence/PersistenceUnits.java new file mode 100644 index 0000000..3da4da5 --- /dev/null +++ b/java/jakarta/persistence/PersistenceUnits.java @@ -0,0 +1,29 @@ +/* + * 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 jakarta.persistence; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) + +public @interface PersistenceUnits { + PersistenceUnit[] value(); +} diff --git a/java/jakarta/persistence/SynchronizationType.java b/java/jakarta/persistence/SynchronizationType.java new file mode 100644 index 0000000..b1d5c1a --- /dev/null +++ b/java/jakarta/persistence/SynchronizationType.java @@ -0,0 +1,22 @@ +/* + * 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 jakarta.persistence; + +public enum SynchronizationType { + SYNCHRONIZED, + UNSYNCHRONIZED +} diff --git a/java/jakarta/security/auth/message/AuthException.java b/java/jakarta/security/auth/message/AuthException.java new file mode 100644 index 0000000..1a2b652 --- /dev/null +++ b/java/jakarta/security/auth/message/AuthException.java @@ -0,0 +1,54 @@ +/** + * 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 jakarta.security.auth.message; + +import javax.security.auth.login.LoginException; + +public class AuthException extends LoginException { + private static final long serialVersionUID = -1156951780670243758L; + + public AuthException() { + } + + public AuthException(String msg) { + super(msg); + } + + /** + * Construct an instance of AuthException. + * + * @param msg Exception message + * @param cause The cause of the exception + * + * @since Authentication 3.0 + */ + public AuthException(String msg, Throwable cause) { + super(msg); + initCause(cause); + } + + /** + * Construct an instance of AuthException. + * + * @param cause The cause of the exception + * + * @since Authentication 3.0 + */ + public AuthException(Throwable cause) { + initCause(cause); + } +} diff --git a/java/jakarta/security/auth/message/AuthStatus.java b/java/jakarta/security/auth/message/AuthStatus.java new file mode 100644 index 0000000..2e9c10d --- /dev/null +++ b/java/jakarta/security/auth/message/AuthStatus.java @@ -0,0 +1,37 @@ +/** + * 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 jakarta.security.auth.message; + +public class AuthStatus { + + public static final AuthStatus SUCCESS = new AuthStatus("SUCCESS"); + public static final AuthStatus FAILURE = new AuthStatus("FAILURE"); + public static final AuthStatus SEND_SUCCESS = new AuthStatus("SEND_SUCCESS"); + public static final AuthStatus SEND_FAILURE = new AuthStatus("SEND_FAILURE"); + public static final AuthStatus SEND_CONTINUE = new AuthStatus("SEND_CONTINUE"); + + private final String name; + + private AuthStatus(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/java/jakarta/security/auth/message/ClientAuth.java b/java/jakarta/security/auth/message/ClientAuth.java new file mode 100644 index 0000000..c4bf93e --- /dev/null +++ b/java/jakarta/security/auth/message/ClientAuth.java @@ -0,0 +1,66 @@ +/** + * 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 jakarta.security.auth.message; + +import javax.security.auth.Subject; + +public interface ClientAuth { + + /** + * Secure (authenticate) the request. + * + * @param messageInfo The associated request and response + * @param clientSubject The subject that represents the source of the request + * + * @return An AuthStatus instance that represents the result of the authentication + * + * @throws AuthException If the a failure occurred in a manner that prevented the failure from being communicated + * via messageInfo + */ + AuthStatus secureRequest(MessageInfo messageInfo, Subject clientSubject) throws AuthException; + + /** + * Validate a response. + * + * @param messageInfo The associated request and response + * @param clientSubject The subject that represents the recipient of the response + * @param serviceSubject The subject that represents the source of the response + * + * @return An AuthStatus instance that represents the result of the validation + * + * @throws AuthException If the a failure occurred in a manner that prevented the failure from being communicated + * via messageInfo + */ + default AuthStatus validateResponse(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) + throws AuthException { + return AuthStatus.SUCCESS; + } + + /** + * Remove principals and/or credentials from the subject that were previously added by this authentication + * mechanism. + * + * @param messageInfo The associated request and response + * @param subject The subject to clean + * + * @throws AuthException If the a failure occurred + */ + default void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException { + // NO-OP + } +} + diff --git a/java/jakarta/security/auth/message/MessageInfo.java b/java/jakarta/security/auth/message/MessageInfo.java new file mode 100644 index 0000000..eb2e10e --- /dev/null +++ b/java/jakarta/security/auth/message/MessageInfo.java @@ -0,0 +1,32 @@ +/** + * 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 jakarta.security.auth.message; + +import java.util.Map; + +public interface MessageInfo { + + Object getRequestMessage(); + + Object getResponseMessage(); + + void setRequestMessage(Object request); + + void setResponseMessage(Object response); + + Map getMap(); +} diff --git a/java/jakarta/security/auth/message/MessagePolicy.java b/java/jakarta/security/auth/message/MessagePolicy.java new file mode 100644 index 0000000..f58aaf5 --- /dev/null +++ b/java/jakarta/security/auth/message/MessagePolicy.java @@ -0,0 +1,85 @@ +/** + * 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 jakarta.security.auth.message; + +public class MessagePolicy { + + private final TargetPolicy[] targetPolicies; + private final boolean mandatory; + + public MessagePolicy(TargetPolicy[] targetPolicies, boolean mandatory) { + if (targetPolicies == null) { + throw new IllegalArgumentException("targetPolicies is null"); + } + this.targetPolicies = targetPolicies; + this.mandatory = mandatory; + } + + public boolean isMandatory() { + return mandatory; + } + + public TargetPolicy[] getTargetPolicies() { + if (targetPolicies.length == 0) { + return null; + } + return targetPolicies; + } + + public interface ProtectionPolicy { + + String AUTHENTICATE_SENDER = "#authenticateSender"; + String AUTHENTICATE_CONTENT = "#authenticateContent"; + String AUTHENTICATE_RECIPIENT = "#authenticateRecipient"; + + String getID(); + } + + public interface Target { + + Object get(MessageInfo messageInfo); + + void remove(MessageInfo messageInfo); + + void put(MessageInfo messageInfo, Object data); + } + + public static class TargetPolicy { + + private final Target[] targets; + private final ProtectionPolicy protectionPolicy; + + public TargetPolicy(Target[] targets, ProtectionPolicy protectionPolicy) { + if (protectionPolicy == null) { + throw new IllegalArgumentException("protectionPolicy is null"); + } + this.targets = targets; + this.protectionPolicy = protectionPolicy; + } + + public Target[] getTargets() { + if (targets == null || targets.length == 0) { + return null; + } + return targets; + } + + public ProtectionPolicy getProtectionPolicy() { + return protectionPolicy; + } + } +} diff --git a/java/jakarta/security/auth/message/ServerAuth.java b/java/jakarta/security/auth/message/ServerAuth.java new file mode 100644 index 0000000..84141ad --- /dev/null +++ b/java/jakarta/security/auth/message/ServerAuth.java @@ -0,0 +1,65 @@ +/** + * 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 jakarta.security.auth.message; + +import javax.security.auth.Subject; + +public interface ServerAuth { + + /** + * Validate the request. + * + * @param messageInfo The associated request and response + * @param clientSubject The subject that represents the source of the request + * @param serviceSubject The subject that represents the recipient of the request + * + * @return An AuthStatus instance that represents the result of the validation + * + * @throws AuthException If the a failure occurred in a manner that prevented the failure from being communicated + * via messageInfo + */ + AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) + throws AuthException; + + /** + * Secure (authenticate) the response. + * + * @param messageInfo The associated request and response + * @param serviceSubject The subject that represents the source of the response + * + * @return An AuthStatus instance that represents the result of the authentication + * + * @throws AuthException If the a failure occurred in a manner that prevented the failure from being communicated + * via messageInfo + */ + default AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException { + return AuthStatus.SUCCESS; + } + + /** + * Remove principals and/or credentials from the subject that were previously added by this authentication + * mechanism. + * + * @param messageInfo The associated request and response + * @param subject The subject to clean + * + * @throws AuthException If the a failure occurred + */ + default void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException { + // NO-OP + } +} diff --git a/java/jakarta/security/auth/message/callback/CallerPrincipalCallback.java b/java/jakarta/security/auth/message/callback/CallerPrincipalCallback.java new file mode 100644 index 0000000..a7a5f80 --- /dev/null +++ b/java/jakarta/security/auth/message/callback/CallerPrincipalCallback.java @@ -0,0 +1,57 @@ +/** + * 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 jakarta.security.auth.message.callback; + +import java.security.Principal; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; + +/** + * Callback that enables an authentication module to inform the runtime of the call principal or name of the caller + * principal. + */ +public class CallerPrincipalCallback implements Callback { + + private final Subject subject; + private final Principal principal; + private final String name; + + public CallerPrincipalCallback(Subject subject, Principal principal) { + this.subject = subject; + this.principal = principal; + this.name = null; + } + + public CallerPrincipalCallback(Subject subject, String name) { + this.subject = subject; + this.principal = null; + this.name = name; + } + + public Subject getSubject() { + return subject; + } + + public Principal getPrincipal() { + return principal; + } + + public String getName() { + return name; + } +} diff --git a/java/jakarta/security/auth/message/callback/CertStoreCallback.java b/java/jakarta/security/auth/message/callback/CertStoreCallback.java new file mode 100644 index 0000000..5a9455b --- /dev/null +++ b/java/jakarta/security/auth/message/callback/CertStoreCallback.java @@ -0,0 +1,40 @@ +/** + * 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 jakarta.security.auth.message.callback; + +import java.security.cert.CertStore; + +import javax.security.auth.callback.Callback; + +/** + * Callback that enables a runtime to inform authentication modules of the CertStore to use. + */ +public class CertStoreCallback implements Callback { + + private CertStore certStore; + + public CertStoreCallback() { + } + + public void setCertStore(CertStore certStore) { + this.certStore = certStore; + } + + public CertStore getCertStore() { + return certStore; + } +} diff --git a/java/jakarta/security/auth/message/callback/GroupPrincipalCallback.java b/java/jakarta/security/auth/message/callback/GroupPrincipalCallback.java new file mode 100644 index 0000000..ea5d2eb --- /dev/null +++ b/java/jakarta/security/auth/message/callback/GroupPrincipalCallback.java @@ -0,0 +1,42 @@ +/** + * 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 jakarta.security.auth.message.callback; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; + +/** + * Callback that enables an authentication module to inform the runtime of the groups a user is in. + */ +public class GroupPrincipalCallback implements Callback { + + private final Subject subject; + private final String[] groups; + + public GroupPrincipalCallback(Subject subject, String[] groups) { + this.subject = subject; + this.groups = groups; + } + + public Subject getSubject() { + return subject; + } + + public String[] getGroups() { + return groups; + } +} diff --git a/java/jakarta/security/auth/message/callback/PasswordValidationCallback.java b/java/jakarta/security/auth/message/callback/PasswordValidationCallback.java new file mode 100644 index 0000000..9840375 --- /dev/null +++ b/java/jakarta/security/auth/message/callback/PasswordValidationCallback.java @@ -0,0 +1,65 @@ +/** + * 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 jakarta.security.auth.message.callback; + +import java.util.Arrays; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; + +/** + * Callback that enables an authentication module to supply a user name and password (to a runtime?) and determine if + * the result of validation. + */ +public class PasswordValidationCallback implements Callback { + + private final Subject subject; + private final String username; + private char[] password; + private boolean result; + + public PasswordValidationCallback(Subject subject, String username, char[] password) { + this.subject = subject; + this.username = username; + this.password = password; + } + + public Subject getSubject() { + return subject; + } + + public String getUsername() { + return username; + } + + public char[] getPassword() { + return password; + } + + public void clearPassword() { + Arrays.fill(password, (char) 0); + password = new char[0]; + } + + public void setResult(boolean result) { + this.result = result; + } + + public boolean getResult() { + return result; + } +} diff --git a/java/jakarta/security/auth/message/callback/PrivateKeyCallback.java b/java/jakarta/security/auth/message/callback/PrivateKeyCallback.java new file mode 100644 index 0000000..dffcb0b --- /dev/null +++ b/java/jakarta/security/auth/message/callback/PrivateKeyCallback.java @@ -0,0 +1,122 @@ +/** + * 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 jakarta.security.auth.message.callback; + +import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.cert.Certificate; + +import javax.security.auth.callback.Callback; +import javax.security.auth.x500.X500Principal; + +/** + * Callback that enables an authentication module to request a certificate chain and private key from the runtime. The + * information specifying the chain and key may be an alias, a digest, a subject key, or an issuer ID. Other request + * types may be supported. + */ +public class PrivateKeyCallback implements Callback { + + private final Request request; + private Certificate[] chain; + private PrivateKey key; + + public PrivateKeyCallback(Request request) { + this.request = request; + } + + public Request getRequest() { + return request; + } + + public void setKey(PrivateKey key, Certificate[] chain) { + this.key = key; + this.chain = chain; + } + + public PrivateKey getKey() { + return key; + } + + public Certificate[] getChain() { + return chain; + } + + public interface Request { + } + + public static class AliasRequest implements Request { + + private final String alias; + + public AliasRequest(String alias) { + this.alias = alias; + } + + public String getAlias() { + return alias; + } + } + + public static class DigestRequest implements Request { + private final byte[] digest; + private final String algorithm; + + public DigestRequest(byte[] digest, String algorithm) { + this.digest = digest; + this.algorithm = algorithm; + } + + public byte[] getDigest() { + return digest; + } + + public String getAlgorithm() { + return algorithm; + } + } + + public static class SubjectKeyIDRequest implements Request { + + private final byte[] subjectKeyID; + + public SubjectKeyIDRequest(byte[] subjectKeyID) { + this.subjectKeyID = subjectKeyID; + } + + public byte[] getSubjectKeyID() { + return subjectKeyID; + } + } + + public static class IssuerSerialNumRequest implements Request { + private final X500Principal issuer; + private final BigInteger serialNum; + + public IssuerSerialNumRequest(X500Principal issuer, BigInteger serialNum) { + this.issuer = issuer; + this.serialNum = serialNum; + } + + public X500Principal getIssuer() { + return issuer; + } + + public BigInteger getSerialNum() { + return serialNum; + } + } +} diff --git a/java/jakarta/security/auth/message/callback/SecretKeyCallback.java b/java/jakarta/security/auth/message/callback/SecretKeyCallback.java new file mode 100644 index 0000000..95dd488 --- /dev/null +++ b/java/jakarta/security/auth/message/callback/SecretKeyCallback.java @@ -0,0 +1,62 @@ +/** + * 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 jakarta.security.auth.message.callback; + +import javax.crypto.SecretKey; +import javax.security.auth.callback.Callback; + +/** + * A callback enabling an authentication module to request a secret key from the runtime, by supplying an alias. Other + * request types may also be supported. + */ +public class SecretKeyCallback implements Callback { + + private final Request request; + private SecretKey key; + + public SecretKeyCallback(Request request) { + this.request = request; + } + + public Request getRequest() { + return request; + } + + public void setKey(SecretKey key) { + this.key = key; + } + + public SecretKey getKey() { + return key; + } + + public interface Request { + } + + public static class AliasRequest implements Request { + + private final String alias; + + public AliasRequest(String alias) { + this.alias = alias; + } + + public String getAlias() { + return alias; + } + } +} diff --git a/java/jakarta/security/auth/message/callback/TrustStoreCallback.java b/java/jakarta/security/auth/message/callback/TrustStoreCallback.java new file mode 100644 index 0000000..64ab856 --- /dev/null +++ b/java/jakarta/security/auth/message/callback/TrustStoreCallback.java @@ -0,0 +1,37 @@ +/** + * 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 jakarta.security.auth.message.callback; + +import java.security.KeyStore; + +import javax.security.auth.callback.Callback; + +/** + * A Callback enabling an authentication module to request a truststore from the runtime. + */ +public class TrustStoreCallback implements Callback { + + private KeyStore trustStore; + + public void setTrustStore(KeyStore trustStore) { + this.trustStore = trustStore; + } + + public KeyStore getTrustStore() { + return trustStore; + } +} diff --git a/java/jakarta/security/auth/message/config/AuthConfig.java b/java/jakarta/security/auth/message/config/AuthConfig.java new file mode 100644 index 0000000..bd9feed --- /dev/null +++ b/java/jakarta/security/auth/message/config/AuthConfig.java @@ -0,0 +1,32 @@ +/** + * 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 jakarta.security.auth.message.config; + +import jakarta.security.auth.message.MessageInfo; + +public interface AuthConfig { + + String getMessageLayer(); + + String getAppContext(); + + String getAuthContextID(MessageInfo messageInfo); + + void refresh(); + + boolean isProtected(); +} diff --git a/java/jakarta/security/auth/message/config/AuthConfigFactory.java b/java/jakarta/security/auth/message/config/AuthConfigFactory.java new file mode 100644 index 0000000..7ecaa3a --- /dev/null +++ b/java/jakarta/security/auth/message/config/AuthConfigFactory.java @@ -0,0 +1,183 @@ +/** + * 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 jakarta.security.auth.message.config; + +import java.security.AccessController; +import java.security.Permission; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.Security; +import java.security.SecurityPermission; +import java.util.Map; + +import jakarta.security.auth.message.module.ServerAuthModule; + +public abstract class AuthConfigFactory { + + public static final String DEFAULT_FACTORY_SECURITY_PROPERTY = "authconfigprovider.factory"; + public static final String GET_FACTORY_PERMISSION_NAME = "getProperty.authconfigprovider.factory"; + public static final String SET_FACTORY_PERMISSION_NAME = "setProperty.authconfigprovider.factory"; + public static final String PROVIDER_REGISTRATION_PERMISSION_NAME = "setProperty.authconfigfactory.provider"; + + /** + * @deprecated Following JEP 411 + */ + @Deprecated(forRemoval = true) + public static final SecurityPermission getFactorySecurityPermission = + new SecurityPermission(GET_FACTORY_PERMISSION_NAME); + + /** + * @deprecated Following JEP 411 + */ + @Deprecated(forRemoval = true) + public static final SecurityPermission setFactorySecurityPermission = + new SecurityPermission(SET_FACTORY_PERMISSION_NAME); + + /** + * @deprecated Following JEP 411 + */ + @Deprecated(forRemoval = true) + public static final SecurityPermission providerRegistrationSecurityPermission = + new SecurityPermission(PROVIDER_REGISTRATION_PERMISSION_NAME); + + private static final String DEFAULT_JASPI_AUTHCONFIGFACTORYIMPL = + "org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl"; + + private static volatile AuthConfigFactory factory; + + public AuthConfigFactory() { + } + + public static AuthConfigFactory getFactory() { + checkPermission(getFactorySecurityPermission); + if (factory != null) { + return factory; + } + + synchronized (AuthConfigFactory.class) { + if (factory == null) { + final String className = getFactoryClassName(); + try { + factory = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + // Load this class with the same class loader as used for + // this class. Note that the Thread context class loader + // should not be used since that would trigger a memory leak + // in container environments. + if (className.equals("org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl")) { + return new org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl(); + } else { + Class clazz = Class.forName(className); + return (AuthConfigFactory) clazz.getConstructor().newInstance(); + } + }); + } catch (PrivilegedActionException e) { + Exception inner = e.getException(); + if (inner instanceof InstantiationException) { + throw new SecurityException("AuthConfigFactory error:" + inner.getCause().getMessage(), + inner.getCause()); + } else { + throw new SecurityException("AuthConfigFactory error: " + inner, inner); + } + } + } + } + + return factory; + } + + public static synchronized void setFactory(AuthConfigFactory factory) { + checkPermission(setFactorySecurityPermission); + AuthConfigFactory.factory = factory; + } + + public abstract AuthConfigProvider getConfigProvider(String layer, String appContext, + RegistrationListener listener); + + public abstract String registerConfigProvider(String className, Map properties, String layer, + String appContext, String description); + + public abstract String registerConfigProvider(AuthConfigProvider provider, String layer, String appContext, + String description); + + public abstract boolean removeRegistration(String registrationID); + + public abstract String[] detachListener(RegistrationListener listener, String layer, String appContext); + + public abstract String[] getRegistrationIDs(AuthConfigProvider provider); + + public abstract RegistrationContext getRegistrationContext(String registrationID); + + public abstract void refresh(); + + /** + * Convenience method for registering a {@link ServerAuthModule} that should have the same effect as calling + * {@link #registerConfigProvider(AuthConfigProvider, String, String, String)} with the implementation providing the + * appropriate {@link AuthConfigProvider} generated from the provided context. + * + * @param serverAuthModule The {@link ServerAuthModule} to register + * @param context The associated application context + * + * @return A string identifier for the created registration + * + * @since Authentication 3.0 + */ + public abstract String registerServerAuthModule(ServerAuthModule serverAuthModule, Object context); + + /** + * Convenience method for deregistering a {@link ServerAuthModule} that should have the same effect as calling + * {@link AuthConfigFactory#removeRegistration(String)}. + * + * @param context The associated application context + * + * @since Authentication 3.0 + */ + public abstract void removeServerAuthModule(Object context); + + /** + * @deprecated Following JEP 411 + */ + @Deprecated(forRemoval = true) + private static void checkPermission(Permission permission) { + SecurityManager securityManager = System.getSecurityManager(); + if (securityManager != null) { + securityManager.checkPermission(permission); + } + } + + private static String getFactoryClassName() { + String className = AccessController + .doPrivileged((PrivilegedAction) () -> Security.getProperty(DEFAULT_FACTORY_SECURITY_PROPERTY)); + + if (className != null) { + return className; + } + + return DEFAULT_JASPI_AUTHCONFIGFACTORYIMPL; + } + + public interface RegistrationContext { + + String getMessageLayer(); + + String getAppContext(); + + String getDescription(); + + boolean isPersistent(); + } +} diff --git a/java/jakarta/security/auth/message/config/AuthConfigProvider.java b/java/jakarta/security/auth/message/config/AuthConfigProvider.java new file mode 100644 index 0000000..3aa912d --- /dev/null +++ b/java/jakarta/security/auth/message/config/AuthConfigProvider.java @@ -0,0 +1,30 @@ +/** + * 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 jakarta.security.auth.message.config; + +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.AuthException; + +public interface AuthConfigProvider { + + ClientAuthConfig getClientAuthConfig(String layer, String appContext, CallbackHandler handler) throws AuthException; + + ServerAuthConfig getServerAuthConfig(String layer, String appContext, CallbackHandler handler) throws AuthException; + + void refresh(); +} diff --git a/java/jakarta/security/auth/message/config/ClientAuthConfig.java b/java/jakarta/security/auth/message/config/ClientAuthConfig.java new file mode 100644 index 0000000..2421bca --- /dev/null +++ b/java/jakarta/security/auth/message/config/ClientAuthConfig.java @@ -0,0 +1,29 @@ +/** + * 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 jakarta.security.auth.message.config; + +import java.util.Map; + +import javax.security.auth.Subject; + +import jakarta.security.auth.message.AuthException; + +public interface ClientAuthConfig extends AuthConfig { + + ClientAuthContext getAuthContext(String authContextID, Subject clientSubject, Map properties) + throws AuthException; +} diff --git a/java/jakarta/security/auth/message/config/ClientAuthContext.java b/java/jakarta/security/auth/message/config/ClientAuthContext.java new file mode 100644 index 0000000..4321e8a --- /dev/null +++ b/java/jakarta/security/auth/message/config/ClientAuthContext.java @@ -0,0 +1,22 @@ +/** + * 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 jakarta.security.auth.message.config; + +import jakarta.security.auth.message.ClientAuth; + +public interface ClientAuthContext extends ClientAuth { +} diff --git a/java/jakarta/security/auth/message/config/RegistrationListener.java b/java/jakarta/security/auth/message/config/RegistrationListener.java new file mode 100644 index 0000000..0d6f76d --- /dev/null +++ b/java/jakarta/security/auth/message/config/RegistrationListener.java @@ -0,0 +1,22 @@ +/** + * 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 jakarta.security.auth.message.config; + +public interface RegistrationListener { + + void notify(String layer, String appContext); +} diff --git a/java/jakarta/security/auth/message/config/ServerAuthConfig.java b/java/jakarta/security/auth/message/config/ServerAuthConfig.java new file mode 100644 index 0000000..9477d7a --- /dev/null +++ b/java/jakarta/security/auth/message/config/ServerAuthConfig.java @@ -0,0 +1,29 @@ +/** + * 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 jakarta.security.auth.message.config; + +import java.util.Map; + +import javax.security.auth.Subject; + +import jakarta.security.auth.message.AuthException; + +public interface ServerAuthConfig extends AuthConfig { + + ServerAuthContext getAuthContext(String authContextID, Subject serviceSubject, Map properties) + throws AuthException; +} diff --git a/java/jakarta/security/auth/message/config/ServerAuthContext.java b/java/jakarta/security/auth/message/config/ServerAuthContext.java new file mode 100644 index 0000000..6f088fe --- /dev/null +++ b/java/jakarta/security/auth/message/config/ServerAuthContext.java @@ -0,0 +1,22 @@ +/** + * 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 jakarta.security.auth.message.config; + +import jakarta.security.auth.message.ServerAuth; + +public interface ServerAuthContext extends ServerAuth { +} diff --git a/java/jakarta/security/auth/message/module/ClientAuthModule.java b/java/jakarta/security/auth/message/module/ClientAuthModule.java new file mode 100644 index 0000000..b7fa96a --- /dev/null +++ b/java/jakarta/security/auth/message/module/ClientAuthModule.java @@ -0,0 +1,33 @@ +/** + * 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 jakarta.security.auth.message.module; + +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.ClientAuth; +import jakarta.security.auth.message.MessagePolicy; + +public interface ClientAuthModule extends ClientAuth { + + void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler handler, + Map options) throws AuthException; + + Class[] getSupportedMessageTypes(); +} diff --git a/java/jakarta/security/auth/message/module/ServerAuthModule.java b/java/jakarta/security/auth/message/module/ServerAuthModule.java new file mode 100644 index 0000000..8c65873 --- /dev/null +++ b/java/jakarta/security/auth/message/module/ServerAuthModule.java @@ -0,0 +1,33 @@ +/** + * 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 jakarta.security.auth.message.module; + +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.MessagePolicy; +import jakarta.security.auth.message.ServerAuth; + +public interface ServerAuthModule extends ServerAuth { + + void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler handler, + Map options) throws AuthException; + + Class[] getSupportedMessageTypes(); +} diff --git a/java/jakarta/servlet/AsyncContext.java b/java/jakarta/servlet/AsyncContext.java new file mode 100644 index 0000000..7b952e6 --- /dev/null +++ b/java/jakarta/servlet/AsyncContext.java @@ -0,0 +1,161 @@ +/* + * 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 jakarta.servlet; + +/** + * Provides the context for asynchronous request handling + * + * @since Servlet 3.0 + */ +public interface AsyncContext { + + /** + * The attribute name for the URI of the async request + */ + String ASYNC_REQUEST_URI = "jakarta.servlet.async.request_uri"; + + /** + * The attribute name for the Context Path of the async request + */ + String ASYNC_CONTEXT_PATH = "jakarta.servlet.async.context_path"; + + /** + * The attribute name for the Mapping of the async request + */ + String ASYNC_MAPPING = "jakarta.servlet.async.mapping"; + + /** + * The attribute name for the Path Info of the async request + */ + String ASYNC_PATH_INFO = "jakarta.servlet.async.path_info"; + + /** + * The attribute name for the Servlet Path of the async request + */ + String ASYNC_SERVLET_PATH = "jakarta.servlet.async.servlet_path"; + + /** + * The attribute name for the Query String of the async request + */ + String ASYNC_QUERY_STRING = "jakarta.servlet.async.query_string"; + + /** + * @return a reference to the ServletRequest object + */ + ServletRequest getRequest(); + + /** + * @return a reference to the ServletResponse object + */ + ServletResponse getResponse(); + + /** + * @return true if the Request and Response are the original ones + */ + boolean hasOriginalRequestAndResponse(); + + /** + * @throws IllegalStateException if this method is called when the request is not in asynchronous mode. The request + * is in asynchronous mode after + * {@link jakarta.servlet.http.HttpServletRequest#startAsync()} or + * {@link jakarta.servlet.http.HttpServletRequest#startAsync(ServletRequest, ServletResponse)} + * has been called and before {@link #complete()} or any other dispatch() method + * has been called. + */ + void dispatch(); + + /** + * @param path The path to which the request/response should be dispatched relative to the {@link ServletContext} + * from which this async request was started. + * + * @throws IllegalStateException if this method is called when the request is not in asynchronous mode. The request + * is in asynchronous mode after + * {@link jakarta.servlet.http.HttpServletRequest#startAsync()} or + * {@link jakarta.servlet.http.HttpServletRequest#startAsync(ServletRequest, ServletResponse)} + * has been called and before {@link #complete()} or any other dispatch() method + * has been called. + */ + void dispatch(String path); + + /** + * @param path The path to which the request/response should be dispatched relative to the specified + * {@link ServletContext}. + * @param context The {@link ServletContext} to which the request/response should be dispatched. + * + * @throws IllegalStateException if this method is called when the request is not in asynchronous mode. The request + * is in asynchronous mode after + * {@link jakarta.servlet.http.HttpServletRequest#startAsync()} or + * {@link jakarta.servlet.http.HttpServletRequest#startAsync(ServletRequest, ServletResponse)} + * has been called and before {@link #complete()} or any other dispatch() method + * has been called. + */ + void dispatch(ServletContext context, String path); + + /** + * Completes the async request processing and closes the response stream + */ + void complete(); + + /** + * Starts a new thread to process the asynchronous request + * + * @param run a Runnable that the new thread will run + */ + void start(Runnable run); + + /** + * Adds an event listener that will be called for different AsyncEvents fire + * + * @param listener an AsyncListener that will be called with AsyncEvent objects + */ + void addListener(AsyncListener listener); + + /** + * Adds an event listener that will be called when different AsyncEvents fire + * + * @param listener an AsyncListener that will be called with AsyncEvent objects + * @param request the ServletRequest that will be passed with the AsyncEvent + * @param response the ServletResponse that will be passed with the AsyncEvent + */ + void addListener(AsyncListener listener, ServletRequest request, ServletResponse response); + + /** + * Creates and returns an AsyncListener object + * + * @param The type to create that extends AsyncListener + * @param clazz The class to instantiate to create the listener + * + * @return the newly created AsyncListener object + * + * @throws ServletException if the listener cannot be created + */ + T createListener(Class clazz) throws ServletException; + + /** + * Set the timeout. + * + * @param timeout The timeout in milliseconds. 0 or less indicates no timeout. + */ + void setTimeout(long timeout); + + /** + * Get the current timeout. + * + * @return The timeout in milliseconds. 0 or less indicates no timeout. + */ + long getTimeout(); +} diff --git a/java/jakarta/servlet/AsyncEvent.java b/java/jakarta/servlet/AsyncEvent.java new file mode 100644 index 0000000..23d9a6a --- /dev/null +++ b/java/jakarta/servlet/AsyncEvent.java @@ -0,0 +1,119 @@ +/* + * 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 jakarta.servlet; + +/** + * Used to pass data to the methods of {@link AsyncListener}. + * + * @since Servlet 3.0 + */ +public class AsyncEvent { + private final AsyncContext context; + private final ServletRequest request; + private final ServletResponse response; + private final Throwable throwable; + + /** + * Creates an instance using the provide parameters. + * + * @param context The asynchronous context associated with the event + */ + public AsyncEvent(AsyncContext context) { + this.context = context; + this.request = null; + this.response = null; + this.throwable = null; + } + + /** + * Creates an instance using the provide parameters. + * + * @param context The asynchronous context associated with the event + * @param request The request associated with the event + * @param response The response associated with the event + */ + public AsyncEvent(AsyncContext context, ServletRequest request, ServletResponse response) { + this.context = context; + this.request = request; + this.response = response; + this.throwable = null; + } + + /** + * Creates an instance using the provide parameters. + * + * @param context The asynchronous context associated with the event + * @param throwable The throwable associated with the event + */ + public AsyncEvent(AsyncContext context, Throwable throwable) { + this.context = context; + this.throwable = throwable; + this.request = null; + this.response = null; + } + + /** + * Creates an instance using the provide parameters. + * + * @param context The asynchronous context associated with the event + * @param request The request associated with the event + * @param response The response associated with the event + * @param throwable The throwable associated with the event + */ + public AsyncEvent(AsyncContext context, ServletRequest request, ServletResponse response, Throwable throwable) { + this.context = context; + this.request = request; + this.response = response; + this.throwable = throwable; + } + + /** + * Obtain the asynchronous context associated with the event. + * + * @return The asynchronous context associated with the event or {@code null} if one was not specified + */ + public AsyncContext getAsyncContext() { + return context; + } + + /** + * Obtain the request associated with the event. + * + * @return The request associated with the event or {@code null} if one was not specified + */ + public ServletRequest getSuppliedRequest() { + return request; + } + + /** + * Obtain the response associated with the event. + * + * @return The response associated with the event or {@code null} if one was not specified + */ + public ServletResponse getSuppliedResponse() { + return response; + } + + /** + * Obtain the throwable associated with the event. + * + * @return The throwable associated with the event or {@code null} if one was not specified + */ + public Throwable getThrowable() { + return throwable; + } +} diff --git a/java/jakarta/servlet/AsyncListener.java b/java/jakarta/servlet/AsyncListener.java new file mode 100644 index 0000000..a4710d6 --- /dev/null +++ b/java/jakarta/servlet/AsyncListener.java @@ -0,0 +1,67 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; +import java.util.EventListener; + +/** + * Listener for events associated with an {@link AsyncContext}. + * + * @since Servlet 3.0 + */ +public interface AsyncListener extends EventListener { + + /** + * This event is fired after the call to {@link AsyncContext#complete()} has been processed by the container. + * + * @param event Provides access to the objects associated with the event + * + * @throws IOException Should be thrown if an I/O error occurs during the processing of the event + */ + void onComplete(AsyncEvent event) throws IOException; + + /** + * This event is fired if an asynchronous operation times out but before the container takes any action as a result + * of the timeout. + * + * @param event Provides access to the objects associated with the event + * + * @throws IOException Should be thrown if an I/O error occurs during the processing of the event + */ + void onTimeout(AsyncEvent event) throws IOException; + + /** + * This event is fired if an error occurs during an asynchronous operation but before the container takes any action + * as a result of the error. + * + * @param event Provides access to the objects associated with the event + * + * @throws IOException Should be thrown if an I/O error occurs during the processing of the event + */ + void onError(AsyncEvent event) throws IOException; + + /** + * This event is fired if new call is made to {@link ServletRequest#startAsync()} after the completion of the + * {@link AsyncContext} to which this listener was added. + * + * @param event Provides access to the objects associated with the event + * + * @throws IOException Should be thrown if an I/O error occurs during the processing of the event + */ + void onStartAsync(AsyncEvent event) throws IOException; +} diff --git a/java/jakarta/servlet/DispatcherType.java b/java/jakarta/servlet/DispatcherType.java new file mode 100644 index 0000000..8734e50 --- /dev/null +++ b/java/jakarta/servlet/DispatcherType.java @@ -0,0 +1,52 @@ +/* + * 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 jakarta.servlet; + +/** + * Enumeration of dispatcher types. Used both to define filter mappings and by Servlets to determine why they were + * called. + * + * @since Servlet 3.0 + */ +public enum DispatcherType { + + /** + * {@link RequestDispatcher#forward(ServletRequest, ServletResponse)} + */ + FORWARD, + + /** + * {@link RequestDispatcher#include(ServletRequest, ServletResponse)} + */ + INCLUDE, + + /** + * Normal (non-dispatched) requests. + */ + REQUEST, + + /** + * {@link AsyncContext#dispatch()}, {@link AsyncContext#dispatch(String)} and + * {@link AsyncContext#addListener(AsyncListener, ServletRequest, ServletResponse)} + */ + ASYNC, + + /** + * When the container has passed processing to the error handler mechanism such as a defined error page. + */ + ERROR +} diff --git a/java/jakarta/servlet/Filter.java b/java/jakarta/servlet/Filter.java new file mode 100644 index 0000000..063d0de --- /dev/null +++ b/java/jakarta/servlet/Filter.java @@ -0,0 +1,105 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; + +/** + * A filter is an object that performs filtering tasks on either the request to a resource (a servlet or static + * content), or on the response from a resource, or both.
+ *
+ * Filters perform filtering in the doFilter method. Every Filter has access to a FilterConfig object from + * which it can obtain its initialization parameters, a reference to the ServletContext which it can use, for example, + * to load resources needed for filtering tasks. + *

+ * Filters are configured in the deployment descriptor of a web application + *

+ * Examples that have been identified for this design are
+ * 1) Authentication Filters
+ * 2) Logging and Auditing Filters
+ * 3) Image conversion Filters
+ * 4) Data compression Filters
+ * 5) Encryption Filters
+ * 6) Tokenizing Filters
+ * 7) Filters that trigger resource access events
+ * 8) XSL/T filters
+ * 9) Mime-type chain Filter
+ * + * @since Servlet 2.3 + */ +public interface Filter { + + /** + * Called by the web container to indicate to a filter that it is being placed into service. The servlet container + * calls the init method exactly once after instantiating the filter. The init method must complete successfully + * before the filter is asked to do any filtering work. + *

+ * The web container cannot place the filter into service if the init method either: + *

    + *
  • Throws a ServletException
  • + *
  • Does not return within a time period defined by the web container
  • + *
+ * The default implementation is a NO-OP. + * + * @param filterConfig The configuration information associated with the filter instance being initialised + * + * @throws ServletException if the initialisation fails + */ + default void init(FilterConfig filterConfig) throws ServletException { + } + + /** + * The doFilter method of the Filter is called by the container each time a request/response pair is + * passed through the chain due to a client request for a resource at the end of the chain. The FilterChain passed + * in to this method allows the Filter to pass on the request and response to the next entity in the chain. + *

+ * A typical implementation of this method would follow the following pattern:-
+ * 1. Examine the request
+ * 2. Optionally wrap the request object with a custom implementation to filter content or headers for input + * filtering
+ * 3. Optionally wrap the response object with a custom implementation to filter content or headers for output + * filtering
+ * 4. a) Either invoke the next entity in the chain using the FilterChain object + * (chain.doFilter()),
+ * 4. b) or not pass on the request/response pair to the next entity in the filter chain to block + * the request processing
+ * 5. Directly set headers on the response after invocation of the next entity in the filter chain. + * + * @param request The request to process + * @param response The response associated with the request + * @param chain Provides access to the next filter in the chain for this filter to pass the request and response + * to for further processing + * + * @throws IOException if an I/O error occurs during this filter's processing of the request + * @throws ServletException if the processing fails for any other reason + */ + void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException; + + /** + * Called by the web container to indicate to a filter that it is being taken out of service. This method is only + * called once all threads within the filter's doFilter method have exited or after a timeout period has passed. + * After the web container calls this method, it will not call the doFilter method again on this instance of the + * filter.
+ *
+ * This method gives the filter an opportunity to clean up any resources that are being held (for example, memory, + * file handles, threads) and make sure that any persistent state is synchronized with the filter's current state in + * memory. The default implementation is a NO-OP. + */ + default void destroy() { + } +} diff --git a/java/jakarta/servlet/FilterChain.java b/java/jakarta/servlet/FilterChain.java new file mode 100644 index 0000000..ddeeb13 --- /dev/null +++ b/java/jakarta/servlet/FilterChain.java @@ -0,0 +1,44 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; + +/** + * A FilterChain is an object provided by the servlet container to the developer giving a view into the invocation chain + * of a filtered request for a resource. Filters use the FilterChain to invoke the next filter in the chain, or if the + * calling filter is the last filter in the chain, to invoke the resource at the end of the chain. + * + * @see Filter + * + * @since Servlet 2.3 + */ +public interface FilterChain { + + /** + * Causes the next filter in the chain to be invoked, or if the calling filter is the last filter in the chain, + * causes the resource at the end of the chain to be invoked. + * + * @param request the request to pass along the chain. + * @param response the response to pass along the chain. + * + * @throws IOException if an I/O error occurs during the processing of the request + * @throws ServletException if the processing fails for any other reason + */ + void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException; + +} diff --git a/java/jakarta/servlet/FilterConfig.java b/java/jakarta/servlet/FilterConfig.java new file mode 100644 index 0000000..86eb9df --- /dev/null +++ b/java/jakarta/servlet/FilterConfig.java @@ -0,0 +1,65 @@ +/* + * 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 jakarta.servlet; + +import java.util.Enumeration; + +/** + * A filter configuration object used by a servlet container to pass information to a filter during initialization. + * + * @see Filter + * + * @since Servlet 2.3 + */ +public interface FilterConfig { + + /** + * Get the name of the filter. + * + * @return The filter-name of this filter as defined in the deployment descriptor. + */ + String getFilterName(); + + /** + * Returns a reference to the {@link ServletContext} in which the caller is executing. + * + * @return {@link ServletContext} object, used by the caller to interact with its servlet container + * + * @see ServletContext + */ + ServletContext getServletContext(); + + /** + * Returns a String containing the value of the named initialization parameter, or null if + * the parameter does not exist. + * + * @param name String specifying the name of the initialization parameter + * + * @return String containing the value of the initialization parameter + */ + String getInitParameter(String name); + + /** + * Returns the names of the filter's initialization parameters as an Enumeration of String + * objects, or an empty Enumeration if the filter has no initialization parameters. + * + * @return Enumeration of String objects containing the names of the filter's + * initialization parameters + */ + Enumeration getInitParameterNames(); + +} diff --git a/java/jakarta/servlet/FilterRegistration.java b/java/jakarta/servlet/FilterRegistration.java new file mode 100644 index 0000000..919e6b0 --- /dev/null +++ b/java/jakarta/servlet/FilterRegistration.java @@ -0,0 +1,77 @@ +/* + * 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 jakarta.servlet; + +import java.util.Collection; +import java.util.EnumSet; + +/** + * Interface through which a Filter may be further configured. + * + * @since Servlet 3.0 + */ +public interface FilterRegistration extends Registration { + + /** + * Add a mapping for this filter to one or more named Servlets. + * + * @param dispatcherTypes The dispatch types to which this filter should apply + * @param isMatchAfter Should this filter be applied after any mappings defined in the deployment descriptor + * (true) or before? + * @param servletNames Requests mapped to these servlets will be processed by this filter + * + * @throws IllegalArgumentException if the list of servlet names is empty or null + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + void addMappingForServletNames(EnumSet dispatcherTypes, boolean isMatchAfter, + String... servletNames); + + /** + * Gets the currently available servlet name mappings of the Filter represented by this FilterRegistration. + * + * @return a Collection of the Servlet name mappings + */ + Collection getServletNameMappings(); + + /** + * Add a mapping for this filter to one or more URL patterns. + * + * @param dispatcherTypes The dispatch types to which this filter should apply + * @param isMatchAfter Should this filter be applied after any mappings defined in the deployment descriptor + * (true) or before? + * @param urlPatterns The URL patterns to which this filter should be applied + * + * @throws IllegalArgumentException if the list of URL patterns is empty or null + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + void addMappingForUrlPatterns(EnumSet dispatcherTypes, boolean isMatchAfter, String... urlPatterns); + + /** + * Gets the currently available URL pattern mappings of the Filter represented by this FilterRegistration. + * + * @return a Collection of the URL pattern mappings + */ + Collection getUrlPatternMappings(); + + /** + * Interface through which a Filter registered via one of the addFilter methods on ServletContext may be further + * configured. + */ + interface Dynamic extends FilterRegistration, Registration.Dynamic { + // No additional methods + } +} diff --git a/java/jakarta/servlet/GenericFilter.java b/java/jakarta/servlet/GenericFilter.java new file mode 100644 index 0000000..3f2eb9d --- /dev/null +++ b/java/jakarta/servlet/GenericFilter.java @@ -0,0 +1,91 @@ +/* + * 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 jakarta.servlet; + +import java.io.Serializable; +import java.util.Enumeration; + +/** + * Provides a base class that implements the Filter and FilterConfig interfaces to reduce boilerplate when writing new + * filters. + * + * @see jakarta.servlet.Filter + * @see jakarta.servlet.FilterConfig + * + * @since Servlet 4.0 + */ +public abstract class GenericFilter implements Filter, FilterConfig, Serializable { + + private static final long serialVersionUID = 1L; + + /** + * The filter configuration. + */ + private volatile FilterConfig filterConfig; + + + @Override + public String getInitParameter(String name) { + return getFilterConfig().getInitParameter(name); + } + + + @Override + public Enumeration getInitParameterNames() { + return getFilterConfig().getInitParameterNames(); + } + + + /** + * Obtain the FilterConfig used to initialise this Filter instance. + * + * @return The config previously passed to the {@link #init(FilterConfig)} method + */ + public FilterConfig getFilterConfig() { + return filterConfig; + } + + + @Override + public ServletContext getServletContext() { + return getFilterConfig().getServletContext(); + } + + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + this.filterConfig = filterConfig; + init(); + } + + + /** + * Convenience method for sub-classes to save them having to call super.init(config). This is a NO-OP + * by default. + * + * @throws ServletException If an exception occurs that interrupts the Filter's normal operation + */ + public void init() throws ServletException { + // NO-OP + } + + + @Override + public String getFilterName() { + return getFilterConfig().getFilterName(); + } +} diff --git a/java/jakarta/servlet/GenericServlet.java b/java/jakarta/servlet/GenericServlet.java new file mode 100644 index 0000000..caf2ab3 --- /dev/null +++ b/java/jakarta/servlet/GenericServlet.java @@ -0,0 +1,203 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; +import java.util.Enumeration; + +/** + * Defines a generic, protocol-independent servlet. To write an HTTP servlet for use on the Web, extend + * {@link jakarta.servlet.http.HttpServlet} instead. + *

+ * GenericServlet implements the Servlet and ServletConfig interfaces. + * GenericServlet may be directly extended by a servlet, although it's more common to extend a + * protocol-specific subclass such as HttpServlet. + *

+ * GenericServlet makes writing servlets easier. It provides simple versions of the lifecycle methods + * init and destroy and of the methods in the ServletConfig interface. + * GenericServlet also implements the log method, declared in the ServletContext + * interface. + *

+ * To write a generic servlet, you need only override the abstract service method. + */ +public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private transient ServletConfig config; + + /** + * Does nothing. All of the servlet initialization is done by one of the init methods. + */ + public GenericServlet() { + // NOOP + } + + /** + * Called by the servlet container to indicate to a servlet that the servlet is being taken out of service. See + * {@link Servlet#destroy}. + */ + @Override + public void destroy() { + // NOOP by default + } + + /** + * Returns a String containing the value of the named initialization parameter, or null if + * the parameter does not exist. See {@link ServletConfig#getInitParameter}. + *

+ * This method is supplied for convenience. It gets the value of the named parameter from the servlet's + * ServletConfig object. + * + * @param name a String specifying the name of the initialization parameter + * + * @return String a String containing the value of the initialization parameter + */ + @Override + public String getInitParameter(String name) { + return getServletConfig().getInitParameter(name); + } + + /** + * Returns the names of the servlet's initialization parameters as an Enumeration of + * String objects, or an empty Enumeration if the servlet has no initialization + * parameters. See {@link ServletConfig#getInitParameterNames}. + *

+ * This method is supplied for convenience. It gets the parameter names from the servlet's + * ServletConfig object. + * + * @return Enumeration an enumeration of String objects containing the names of the servlet's + * initialization parameters + */ + @Override + public Enumeration getInitParameterNames() { + return getServletConfig().getInitParameterNames(); + } + + /** + * Returns this servlet's {@link ServletConfig} object. + * + * @return ServletConfig the ServletConfig object that initialized this servlet + */ + @Override + public ServletConfig getServletConfig() { + return config; + } + + /** + * Returns a reference to the {@link ServletContext} in which this servlet is running. See + * {@link ServletConfig#getServletContext}. + *

+ * This method is supplied for convenience. It gets the context from the servlet's ServletConfig + * object. + * + * @return ServletContext the ServletContext object passed to this servlet by the init + * method + */ + @Override + public ServletContext getServletContext() { + return getServletConfig().getServletContext(); + } + + /** + * Returns information about the servlet, such as author, version, and copyright. By default, this method returns an + * empty string. Override this method to have it return a meaningful value. See {@link Servlet#getServletInfo}. + * + * @return String information about this servlet, by default an empty string + */ + @Override + public String getServletInfo() { + return ""; + } + + /** + * Called by the servlet container to indicate to a servlet that the servlet is being placed into service. See + * {@link Servlet#init}. + *

+ * This implementation stores the {@link ServletConfig} object it receives from the servlet container for later use. + * When overriding this form of the method, call super.init(config). + * + * @param config the ServletConfig object that contains configuration information for this servlet + * + * @exception ServletException if an exception occurs that interrupts the servlet's normal operation + * + * @see UnavailableException + */ + @Override + public void init(ServletConfig config) throws ServletException { + this.config = config; + this.init(); + } + + /** + * A convenience method which can be overridden so that there's no need to call super.init(config). + *

+ * Instead of overriding {@link #init(ServletConfig)}, simply override this method and it will be called by + * GenericServlet.init(ServletConfig config). The ServletConfig object can still be + * retrieved via {@link #getServletConfig}. + * + * @exception ServletException if an exception occurs that interrupts the servlet's normal operation + */ + public void init() throws ServletException { + // NOOP by default + } + + /** + * Writes the specified message to a servlet log file, prepended by the servlet's name. See + * {@link ServletContext#log(String)}. + * + * @param message a String specifying the message to be written to the log file + */ + public void log(String message) { + getServletContext().log(getServletName() + ": " + message); + } + + /** + * Writes an explanatory message and a stack trace for a given Throwable exception to the servlet log + * file, prepended by the servlet's name. See {@link ServletContext#log(String, Throwable)}. + * + * @param message a String that describes the error or exception + * @param t the java.lang.Throwable error or exception + */ + public void log(String message, Throwable t) { + getServletContext().log(getServletName() + ": " + message, t); + } + + /** + * Called by the servlet container to allow the servlet to respond to a request. See {@link Servlet#service}. + *

+ * This method is declared abstract so subclasses, such as HttpServlet, must override it. + * + * @param req the ServletRequest object that contains the client's request + * @param res the ServletResponse object that will contain the servlet's response + * + * @exception ServletException if an exception occurs that interferes with the servlet's normal operation occurred + * @exception IOException if an input or output exception occurs + */ + @Override + public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; + + /** + * Returns the name of this servlet instance. See {@link ServletConfig#getServletName}. + * + * @return the name of this servlet instance + */ + @Override + public String getServletName() { + return config.getServletName(); + } +} diff --git a/java/jakarta/servlet/HttpConstraintElement.java b/java/jakarta/servlet/HttpConstraintElement.java new file mode 100644 index 0000000..90e3102 --- /dev/null +++ b/java/jakarta/servlet/HttpConstraintElement.java @@ -0,0 +1,117 @@ +/* + * 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 jakarta.servlet; + +import java.util.ResourceBundle; + +import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic; +import jakarta.servlet.annotation.ServletSecurity.TransportGuarantee; + +/** + * Equivalent of {@link jakarta.servlet.annotation.HttpConstraint} for programmatic configuration of security + * constraints. + * + * @since Servlet 3.0 + */ +public class HttpConstraintElement { + + private static final String LSTRING_FILE = "jakarta.servlet.LocalStrings"; + private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + private final EmptyRoleSemantic emptyRoleSemantic;// = EmptyRoleSemantic.PERMIT; + private final TransportGuarantee transportGuarantee;// = TransportGuarantee.NONE; + private final String[] rolesAllowed;// = new String[0]; + + /** + * Default constraint is permit with no transport guarantee. + */ + public HttpConstraintElement() { + // Default constructor + this.emptyRoleSemantic = EmptyRoleSemantic.PERMIT; + this.transportGuarantee = TransportGuarantee.NONE; + this.rolesAllowed = new String[0]; + } + + /** + * Construct a constraint with an empty role semantic. Typically used with {@link EmptyRoleSemantic#DENY}. + * + * @param emptyRoleSemantic The empty role semantic to apply to the newly created constraint + */ + public HttpConstraintElement(EmptyRoleSemantic emptyRoleSemantic) { + this.emptyRoleSemantic = emptyRoleSemantic; + this.transportGuarantee = TransportGuarantee.NONE; + this.rolesAllowed = new String[0]; + } + + /** + * Construct a constraint with a transport guarantee and roles. + * + * @param transportGuarantee The transport guarantee to apply to the newly created constraint + * @param rolesAllowed The roles to associate with the newly created constraint + */ + public HttpConstraintElement(TransportGuarantee transportGuarantee, String... rolesAllowed) { + this.emptyRoleSemantic = EmptyRoleSemantic.PERMIT; + this.transportGuarantee = transportGuarantee; + this.rolesAllowed = rolesAllowed; + } + + /** + * Construct a constraint with an empty role semantic, a transport guarantee and roles. + * + * @param emptyRoleSemantic The empty role semantic to apply to the newly created constraint + * @param transportGuarantee The transport guarantee to apply to the newly created constraint + * @param rolesAllowed The roles to associate with the newly created constraint + * + * @throws IllegalArgumentException if roles are specified when DENY is used + */ + public HttpConstraintElement(EmptyRoleSemantic emptyRoleSemantic, TransportGuarantee transportGuarantee, + String... rolesAllowed) { + if (rolesAllowed != null && rolesAllowed.length > 0 && EmptyRoleSemantic.DENY.equals(emptyRoleSemantic)) { + throw new IllegalArgumentException(lStrings.getString("httpConstraintElement.invalidRolesDeny")); + } + this.emptyRoleSemantic = emptyRoleSemantic; + this.transportGuarantee = transportGuarantee; + this.rolesAllowed = rolesAllowed; + } + + /** + * TODO + * + * @return TODO + */ + public EmptyRoleSemantic getEmptyRoleSemantic() { + return emptyRoleSemantic; + } + + /** + * TODO + * + * @return TODO + */ + public TransportGuarantee getTransportGuarantee() { + return transportGuarantee; + } + + /** + * TODO + * + * @return TODO + */ + public String[] getRolesAllowed() { + return rolesAllowed; + } +} \ No newline at end of file diff --git a/java/jakarta/servlet/HttpMethodConstraintElement.java b/java/jakarta/servlet/HttpMethodConstraintElement.java new file mode 100644 index 0000000..d37c6a6 --- /dev/null +++ b/java/jakarta/servlet/HttpMethodConstraintElement.java @@ -0,0 +1,68 @@ +/* + * 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 jakarta.servlet; + +import java.util.ResourceBundle; + +/** + * Programmatic equivalent of a security constraint defined for a single HTTP method. + * + * @since Servlet 3.0 + */ +public class HttpMethodConstraintElement extends HttpConstraintElement { + + // Can't inherit from HttpConstraintElement as API does not allow it + private static final String LSTRING_FILE = "jakarta.servlet.LocalStrings"; + private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + private final String methodName; + + /** + * Construct an instance for the given HTTP method name and a default {@link HttpConstraintElement}. + * + * @param methodName The HTTP method name + */ + public HttpMethodConstraintElement(String methodName) { + if (methodName == null || methodName.length() == 0) { + throw new IllegalArgumentException(lStrings.getString("httpMethodConstraintElement.invalidMethod")); + } + this.methodName = methodName; + } + + /** + * Construct an instance for the given HTTP method name and {@link HttpConstraintElement}. + * + * @param methodName The HTTP method name + * @param constraint The constraint for the given method + */ + public HttpMethodConstraintElement(String methodName, HttpConstraintElement constraint) { + super(constraint.getEmptyRoleSemantic(), constraint.getTransportGuarantee(), constraint.getRolesAllowed()); + if (methodName == null || methodName.length() == 0) { + throw new IllegalArgumentException(lStrings.getString("httpMethodConstraintElement.invalidMethod")); + } + this.methodName = methodName; + } + + /** + * Obtain the name of the HTTP method for which this constraint was created. + * + * @return The HTTP method name as provided to the constructor + */ + public String getMethodName() { + return methodName; + } +} \ No newline at end of file diff --git a/java/jakarta/servlet/LocalStrings.properties b/java/jakarta/servlet/LocalStrings.properties new file mode 100644 index 0000000..cad5eb7 --- /dev/null +++ b/java/jakarta/servlet/LocalStrings.properties @@ -0,0 +1,26 @@ +# 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. + +err.not_iso8859_1=Not an ISO 8859-1 character: [{0}] + +httpConstraintElement.invalidRolesDeny=Roles may not be specified when using DENY + +httpMethodConstraintElement.invalidMethod=Invalid HTTP method + +value.false=false +value.true=true + +wrapper.nullRequest=Request cannot be null +wrapper.nullResponse=Response cannot be null diff --git a/java/jakarta/servlet/LocalStrings_cs.properties b/java/jakarta/servlet/LocalStrings_cs.properties new file mode 100644 index 0000000..4e9cc8e --- /dev/null +++ b/java/jakarta/servlet/LocalStrings_cs.properties @@ -0,0 +1,16 @@ +# 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. + +httpMethodConstraintElement.invalidMethod=Neplatná HTTP metoda diff --git a/java/jakarta/servlet/LocalStrings_de.properties b/java/jakarta/servlet/LocalStrings_de.properties new file mode 100644 index 0000000..c8a5526 --- /dev/null +++ b/java/jakarta/servlet/LocalStrings_de.properties @@ -0,0 +1,22 @@ +# 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. + +err.not_iso8859_1=Kein ISO 8859-1 Zeichen: [{0}] + +httpMethodConstraintElement.invalidMethod=Ungültige HTTP-Methode + +value.true=wahr + +wrapper.nullResponse=Der Response darf nicht 'null' sein diff --git a/java/jakarta/servlet/LocalStrings_es.properties b/java/jakarta/servlet/LocalStrings_es.properties new file mode 100644 index 0000000..e5348f5 --- /dev/null +++ b/java/jakarta/servlet/LocalStrings_es.properties @@ -0,0 +1,23 @@ +# 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. + +err.not_iso8859_1=No es un carácter ISO 8859-1: [{0}] + +httpConstraintElement.invalidRolesDeny=No se pueden especificar Roles al utilizar DENY (DENEGAR) + +httpMethodConstraintElement.invalidMethod=Método HTTP inválido + +value.false=false +value.true=true diff --git a/java/jakarta/servlet/LocalStrings_fr.properties b/java/jakarta/servlet/LocalStrings_fr.properties new file mode 100644 index 0000000..bd1cdfc --- /dev/null +++ b/java/jakarta/servlet/LocalStrings_fr.properties @@ -0,0 +1,26 @@ +# 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. + +err.not_iso8859_1=[{0}] n''est pas un caractère ISO 8859-1 + +httpConstraintElement.invalidRolesDeny=Des rôles ne peuvent pas être spécifiés lorsque DENY est utilisé + +httpMethodConstraintElement.invalidMethod=Méthode HTTP invalide + +value.false=false +value.true=true + +wrapper.nullRequest=La requête ne peut pas être null +wrapper.nullResponse=La réponse ne peut pas être null diff --git a/java/jakarta/servlet/LocalStrings_ja.properties b/java/jakarta/servlet/LocalStrings_ja.properties new file mode 100644 index 0000000..693dcf5 --- /dev/null +++ b/java/jakarta/servlet/LocalStrings_ja.properties @@ -0,0 +1,26 @@ +# 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. + +err.not_iso8859_1=ISO 8859-1 ã®æ–‡å­—ã§ã¯ã‚ã‚Šã¾ã›ã‚“: [{0}] + +httpConstraintElement.invalidRolesDeny=DENYを使用ã™ã‚‹å ´åˆã€Roleを指定ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 + +httpMethodConstraintElement.invalidMethod=無効ãªHTTPメソッド + +value.false=false +value.true=true + +wrapper.nullRequest=リクエストã«ã¯ null を指定ã§ãã¾ã›ã‚“。 +wrapper.nullResponse=レスãƒãƒ³ã‚¹ã«ã¯ null を指定ã§ãã¾ã›ã‚“。 diff --git a/java/jakarta/servlet/LocalStrings_ko.properties b/java/jakarta/servlet/LocalStrings_ko.properties new file mode 100644 index 0000000..63865cb --- /dev/null +++ b/java/jakarta/servlet/LocalStrings_ko.properties @@ -0,0 +1,26 @@ +# 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. + +err.not_iso8859_1=ISO 8859-1 문ìžê°€ 아닙니다: [{0}] + +httpConstraintElement.invalidRolesDeny=DENY를 사용할 ë•Œì—는 ì—­í• ë“¤ì´ ì§€ì •ë  ìˆ˜ 없습니다. + +httpMethodConstraintElement.invalidMethod=유효하지 ì•Šì€ HTTP 메소드 + +value.false=false +value.true=true + +wrapper.nullRequest=ìš”ì²­ì´ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +wrapper.nullResponse=ì‘ë‹µì´ ë„ì¼ ìˆ˜ëŠ” 없습니다. diff --git a/java/jakarta/servlet/LocalStrings_ru.properties b/java/jakarta/servlet/LocalStrings_ru.properties new file mode 100644 index 0000000..8caf598 --- /dev/null +++ b/java/jakarta/servlet/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +httpMethodConstraintElement.invalidMethod=Ошибочный HTTP метод diff --git a/java/jakarta/servlet/LocalStrings_zh_CN.properties b/java/jakarta/servlet/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..6f983a9 --- /dev/null +++ b/java/jakarta/servlet/LocalStrings_zh_CN.properties @@ -0,0 +1,26 @@ +# 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. + +err.not_iso8859_1=ä¸æ˜¯ISO 8859-1字符:[{0}] + +httpConstraintElement.invalidRolesDeny=使用 DENY æ—¶å¯èƒ½æœªæŒ‡å®šè§’色 + +httpMethodConstraintElement.invalidMethod=无效的HTTP.方法 + +value.false=å¦ +value.true=true + +wrapper.nullRequest=请求ä¸èƒ½ä¸ºç©º +wrapper.nullResponse=å“应ä¸èƒ½ä¸ºç©º diff --git a/java/jakarta/servlet/MultipartConfigElement.java b/java/jakarta/servlet/MultipartConfigElement.java new file mode 100644 index 0000000..a91b30a --- /dev/null +++ b/java/jakarta/servlet/MultipartConfigElement.java @@ -0,0 +1,126 @@ +/* + * 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 jakarta.servlet; + +import jakarta.servlet.annotation.MultipartConfig; + +/** + * The programmatic equivalent of {@link jakarta.servlet.annotation.MultipartConfig} used to configure multi-part + * handling for a Servlet when registering a Servlet via code. + * + * @since Servlet 3.0 + */ +public class MultipartConfigElement { + + private final String location;// = ""; + private final long maxFileSize;// = -1; + private final long maxRequestSize;// = -1; + private final int fileSizeThreshold;// = 0; + + /** + * Create a programmatic multi-part configuration with a specific location and defaults for the remaining + * configuration elements. + * + * @param location The temporary location to store files + */ + public MultipartConfigElement(String location) { + // Keep empty string default if location is null + if (location != null) { + this.location = location; + } else { + this.location = ""; + } + this.maxFileSize = -1; + this.maxRequestSize = -1; + this.fileSizeThreshold = 0; + } + + /** + * Create a programmatic multi-part configuration from the individual configuration elements. + * + * @param location The temporary location to store files + * @param maxFileSize The maximum permitted size for a single file + * @param maxRequestSize The maximum permitted size for a request + * @param fileSizeThreshold The size above which the file is save in the temporary location rather than retained in + * memory. + */ + public MultipartConfigElement(String location, long maxFileSize, long maxRequestSize, int fileSizeThreshold) { + // Keep empty string default if location is null + if (location != null) { + this.location = location; + } else { + this.location = ""; + } + this.maxFileSize = maxFileSize; + this.maxRequestSize = maxRequestSize; + // Avoid threshold values of less than zero as they cause trigger NPEs + // in the Commons FileUpload port for fields that have no data. + if (fileSizeThreshold > 0) { + this.fileSizeThreshold = fileSizeThreshold; + } else { + this.fileSizeThreshold = 0; + } + } + + /** + * Create a programmatic configuration from an annotation. + * + * @param annotation The source annotation to copy to create the programmatic equivalent. + */ + public MultipartConfigElement(MultipartConfig annotation) { + location = annotation.location(); + maxFileSize = annotation.maxFileSize(); + maxRequestSize = annotation.maxRequestSize(); + fileSizeThreshold = annotation.fileSizeThreshold(); + } + + /** + * Obtain the location where temporary files should be stored. + * + * @return the location where temporary files should be stored. + */ + public String getLocation() { + return location; + } + + /** + * Obtain the maximum permitted size for a single file. + * + * @return the maximum permitted size for a single file. + */ + public long getMaxFileSize() { + return maxFileSize; + } + + /** + * Obtain the maximum permitted size for a single request. + * + * @return the maximum permitted size for a single request. + */ + public long getMaxRequestSize() { + return maxRequestSize; + } + + /** + * Obtain the size above which the file is save in the temporary location rather than retained in memory. + * + * @return the size above which the file is save in the temporary location rather than retained in memory. + */ + public int getFileSizeThreshold() { + return fileSizeThreshold; + } +} \ No newline at end of file diff --git a/java/jakarta/servlet/ReadListener.java b/java/jakarta/servlet/ReadListener.java new file mode 100644 index 0000000..448c357 --- /dev/null +++ b/java/jakarta/servlet/ReadListener.java @@ -0,0 +1,50 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; + +/** + * Receives notification of read events when using non-blocking IO. + * + * @since Servlet 3.1 + */ +public interface ReadListener extends java.util.EventListener { + + /** + * Invoked when data is available to read. The container will invoke this method the first time for a request as + * soon as there is data to read. Subsequent invocations will only occur if a call to + * {@link ServletInputStream#isReady()} has returned false and data has subsequently become available to read. + * + * @throws IOException id an I/O error occurs while processing the event + */ + void onDataAvailable() throws IOException; + + /** + * Invoked when the request body has been fully read. + * + * @throws IOException id an I/O error occurs while processing the event + */ + void onAllDataRead() throws IOException; + + /** + * Invoked if an error occurs while reading the request body. + * + * @param throwable The exception that occurred + */ + void onError(Throwable throwable); +} diff --git a/java/jakarta/servlet/Registration.java b/java/jakarta/servlet/Registration.java new file mode 100644 index 0000000..5badfb7 --- /dev/null +++ b/java/jakarta/servlet/Registration.java @@ -0,0 +1,105 @@ +/* + * 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 jakarta.servlet; + +import java.util.Map; +import java.util.Set; + +/** + * Common interface for the registration of Filters and Servlets. + * + * @since Servlet 3.0 + */ +public interface Registration { + + /** + * Obtain the name of the Servlet. + * + * @return the name of the Servlet. + */ + String getName(); + + /** + * Obtain the name of the implementation class for the Servlet. + * + * @return the name of the implementation class for the Servlet. + */ + String getClassName(); + + /** + * Add an initialisation parameter if not already added. + * + * @param name Name of initialisation parameter + * @param value Value of initialisation parameter + * + * @return true if the initialisation parameter was set, false if the initialisation + * parameter was not set because an initialisation parameter of the same name already existed + * + * @throws IllegalArgumentException if name or value is null + * @throws IllegalStateException if the ServletContext associated with this registration has already been + * initialised + */ + boolean setInitParameter(String name, String value); + + /** + * Get the value of an initialisation parameter. + * + * @param name The initialisation parameter whose value is required + * + * @return The value of the named initialisation parameter + */ + String getInitParameter(String name); + + /** + * Add multiple initialisation parameters. If any of the supplied initialisation parameter conflicts with an + * existing initialisation parameter, no updates will be performed. + * + * @param initParameters The initialisation parameters to add + * + * @return The set of initialisation parameter names that conflicted with existing initialisation parameter. If + * there are no conflicts, this Set will be empty. + * + * @throws IllegalArgumentException if any of the supplied initialisation parameters have a null name or value + * @throws IllegalStateException if the ServletContext associated with this registration has already been + * initialised + */ + Set setInitParameters(Map initParameters); + + /** + * Get the names and values of all the initialisation parameters. + * + * @return A Map of initialisation parameter names and associated values keyed by name + */ + Map getInitParameters(); + + /** + * Interface through which a Servlet or Filter registered via one of the addServlet or addFilter methods, + * respectively, on ServletContext may be further configured. + */ + interface Dynamic extends Registration { + + /** + * Mark this Servlet/Filter as supported asynchronous processing. + * + * @param isAsyncSupported Should this Servlet/Filter support asynchronous processing + * + * @throws IllegalStateException if the ServletContext associated with this registration has already been + * initialised + */ + void setAsyncSupported(boolean isAsyncSupported); + } +} diff --git a/java/jakarta/servlet/RequestDispatcher.java b/java/jakarta/servlet/RequestDispatcher.java new file mode 100644 index 0000000..79ff77b --- /dev/null +++ b/java/jakarta/servlet/RequestDispatcher.java @@ -0,0 +1,260 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; + +/** + * Defines an object that receives requests from the client and sends them to any resource (such as a servlet, HTML + * file, or JSP file) on the server. The servlet container creates the RequestDispatcher object, which is + * used as a wrapper around a server resource located at a particular path or given by a particular name. + *

+ * This interface is intended to wrap servlets, but a servlet container can create RequestDispatcher + * objects to wrap any type of resource. + * + * @see ServletContext#getRequestDispatcher(String) + * @see ServletContext#getNamedDispatcher(String) + * @see ServletRequest#getRequestDispatcher(String) + */ +public interface RequestDispatcher { + + /** + * The name of the request attribute that should be set by the container when the + * {@link #forward(ServletRequest, ServletResponse)} method is called. It provides the original value of a + * path-related property of the request. See the chapter "Forwarded Request Parameters" in the Servlet Specification + * for details. + * + * @since Servlet 3.0 + */ + String FORWARD_REQUEST_URI = "jakarta.servlet.forward.request_uri"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #forward(ServletRequest, ServletResponse)} method is called. It provides the original value of a + * path-related property of the request. See the chapter "Forwarded Request Parameters" in the Servlet Specification + * for details. + * + * @since Servlet 3.0 + */ + String FORWARD_CONTEXT_PATH = "jakarta.servlet.forward.context_path"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #forward(ServletRequest, ServletResponse)} method is called. It provides the original value of a + * path-related property of the request. See the chapter "Forwarded Request Parameters" in the Servlet Specification + * for details. + * + * @since Servlet 4.0 + */ + String FORWARD_MAPPING = "jakarta.servlet.forward.mapping"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #forward(ServletRequest, ServletResponse)} method is called. It provides the original value of a + * path-related property of the request. See the chapter "Forwarded Request Parameters" in the Servlet Specification + * for details. + * + * @since Servlet 3.0 + */ + String FORWARD_PATH_INFO = "jakarta.servlet.forward.path_info"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #forward(ServletRequest, ServletResponse)} method is called. It provides the original value of a + * path-related property of the request. See the chapter "Forwarded Request Parameters" in the Servlet Specification + * for details. + * + * @since Servlet 3.0 + */ + String FORWARD_SERVLET_PATH = "jakarta.servlet.forward.servlet_path"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #forward(ServletRequest, ServletResponse)} method is called. It provides the original value of a + * path-related property of the request. See the chapter "Forwarded Request Parameters" in the Servlet Specification + * for details. + * + * @since Servlet 3.0 + */ + String FORWARD_QUERY_STRING = "jakarta.servlet.forward.query_string"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #include(ServletRequest, ServletResponse)} method is called on the {@code RequestDispatcher} obtained by a + * path and not by a name. It provides information on the path that was used to obtain the {@code RequestDispatcher} + * instance for this include call. See the chapter "Included Request Parameters" in the Servlet Specification for + * details. + * + * @since Servlet 3.0 + */ + String INCLUDE_REQUEST_URI = "jakarta.servlet.include.request_uri"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #include(ServletRequest, ServletResponse)} method is called on the {@code RequestDispatcher} obtained by a + * path and not by a name. It provides information on the path that was used to obtain the {@code RequestDispatcher} + * instance for this include call. See the chapter "Included Request Parameters" in the Servlet Specification for + * details. + * + * @since Servlet 3.0 + */ + String INCLUDE_CONTEXT_PATH = "jakarta.servlet.include.context_path"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #include(ServletRequest, ServletResponse)} method is called on the {@code RequestDispatcher} obtained by a + * path and not by a name. It provides information on the path that was used to obtain the {@code RequestDispatcher} + * instance for this include call. See the chapter "Included Request Parameters" in the Servlet Specification for + * details. + * + * @since Servlet 3.0 + */ + String INCLUDE_PATH_INFO = "jakarta.servlet.include.path_info"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #include(ServletRequest, ServletResponse)} method is called on the {@code RequestDispatcher} obtained by a + * path and not by a name. It provides information on the path that was used to obtain the {@code RequestDispatcher} + * instance for this include call. See the chapter "Included Request Parameters" in the Servlet Specification for + * details. + * + * @since Servlet 4.0 + */ + String INCLUDE_MAPPING = "jakarta.servlet.include.mapping"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #include(ServletRequest, ServletResponse)} method is called on the {@code RequestDispatcher} obtained by a + * path and not by a name. It provides information on the path that was used to obtain the {@code RequestDispatcher} + * instance for this include call. See the chapter "Included Request Parameters" in the Servlet Specification for + * details. + * + * @since Servlet 3.0 + */ + String INCLUDE_SERVLET_PATH = "jakarta.servlet.include.servlet_path"; + + /** + * The name of the request attribute that should be set by the container when the + * {@link #include(ServletRequest, ServletResponse)} method is called on the {@code RequestDispatcher} obtained by a + * path and not by a name. It provides information on the path that was used to obtain the {@code RequestDispatcher} + * instance for this include call. See the chapter "Included Request Parameters" in the Servlet Specification for + * details. + * + * @since Servlet 3.0 + */ + String INCLUDE_QUERY_STRING = "jakarta.servlet.include.query_string"; + + /** + * The name of the request attribute that should be set by the container when custom error-handling servlet or JSP + * page is invoked. The value of the attribute is of type {@code java.lang.Throwable}. See the chapter "Error + * Handling" in the Servlet Specification for details. + * + * @since Servlet 3.0 + */ + String ERROR_EXCEPTION = "jakarta.servlet.error.exception"; + + /** + * The name of the request attribute that should be set by the container when custom error-handling servlet or JSP + * page is invoked. The value of the attribute is of type {@code java.lang.Class}. See the chapter "Error Handling" + * in the Servlet Specification for details. + * + * @since Servlet 3.0 + */ + String ERROR_EXCEPTION_TYPE = "jakarta.servlet.error.exception_type"; + + /** + * The name of the request attribute that should be set by the container when custom error-handling servlet or JSP + * page is invoked. The value of the attribute is of type {@code String}. See the chapter "Error Handling" in the + * Servlet Specification for details. + * + * @since Servlet 3.0 + */ + String ERROR_MESSAGE = "jakarta.servlet.error.message"; + + /** + * The name of the request attribute that should be set by the container when custom error-handling servlet or JSP + * page is invoked. The value of the attribute is of type {@code String}. See the chapter "Error Handling" in the + * Servlet Specification for details. + * + * @since Servlet 3.0 + */ + String ERROR_REQUEST_URI = "jakarta.servlet.error.request_uri"; + + /** + * The name of the request attribute that should be set by the container when custom error-handling servlet or JSP + * page is invoked. The value of the attribute is of type {@code String}. See the chapter "Error Handling" in the + * Servlet Specification for details. + * + * @since Servlet 3.0 + */ + String ERROR_SERVLET_NAME = "jakarta.servlet.error.servlet_name"; + + /** + * The name of the request attribute that should be set by the container when custom error-handling servlet or JSP + * page is invoked. The value of the attribute is of type {@code java.lang.Integer}. See the chapter "Error + * Handling" in the Servlet Specification for details. + * + * @since Servlet 3.0 + */ + String ERROR_STATUS_CODE = "jakarta.servlet.error.status_code"; + + /** + * Forwards a request from a servlet to another resource (servlet, JSP file, or HTML file) on the server. This + * method allows one servlet to do preliminary processing of a request and another resource to generate the + * response. + *

+ * For a RequestDispatcher obtained via getRequestDispatcher(), the + * ServletRequest object has its path elements and parameters adjusted to match the path of the target + * resource. + *

+ * forward should be called before the response has been committed to the client (before response body + * output has been flushed). If the response already has been committed, this method throws an + * IllegalStateException. Uncommitted output in the response buffer is automatically cleared before the + * forward. + *

+ * The request and response parameters must be either the same objects as were passed to the calling servlet's + * service method or be subclasses of the {@link ServletRequestWrapper} or {@link ServletResponseWrapper} classes + * that wrap them. + * + * @param request a {@link ServletRequest} object that represents the request the client makes of the servlet + * @param response a {@link ServletResponse} object that represents the response the servlet returns to the client + * + * @exception ServletException if the target resource throws this exception + * @exception IOException if the target resource throws this exception + * @exception IllegalStateException if the response was already committed + */ + void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException; + + /** + * Includes the content of a resource (servlet, JSP page, HTML file) in the response. In essence, this method + * enables programmatic server-side includes. + *

+ * The {@link ServletResponse} object has its path elements and parameters remain unchanged from the caller's. The + * included servlet cannot change the response status code or set headers; any attempt to make a change is ignored. + *

+ * The request and response parameters must be either the same objects as were passed to the calling servlet's + * service method or be subclasses of the {@link ServletRequestWrapper} or {@link ServletResponseWrapper} classes + * that wrap them. + * + * @param request a {@link ServletRequest} object that contains the client's request + * @param response a {@link ServletResponse} object that contains the servlet's response + * + * @exception ServletException if the included resource throws this exception + * @exception IOException if the included resource throws this exception + */ + void include(ServletRequest request, ServletResponse response) throws ServletException, IOException; +} diff --git a/java/jakarta/servlet/Servlet.java b/java/jakarta/servlet/Servlet.java new file mode 100644 index 0000000..838b4f0 --- /dev/null +++ b/java/jakarta/servlet/Servlet.java @@ -0,0 +1,124 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; + +/** + * Defines methods that all servlets must implement. + *

+ * A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web + * clients, usually across HTTP, the HyperText Transfer Protocol. + *

+ * To implement this interface, you can write a generic servlet that extends jakarta.servlet.GenericServlet + * or an HTTP servlet that extends jakarta.servlet.http.HttpServlet. + *

+ * This interface defines methods to initialize a servlet, to service requests, and to remove a servlet from the server. + * These are known as life-cycle methods and are called in the following sequence: + *

    + *
  1. The servlet is constructed, then initialized with the init method. + *
  2. Any calls from clients to the service method are handled. + *
  3. The servlet is taken out of service, then destroyed with the destroy method, then garbage collected + * and finalized. + *
+ *

+ * In addition to the life-cycle methods, this interface provides the getServletConfig method, which the + * servlet can use to get any startup information, and the getServletInfo method, which allows the servlet + * to return basic information about itself, such as author, version, and copyright. + * + * @see GenericServlet + * @see jakarta.servlet.http.HttpServlet + */ +public interface Servlet { + + /** + * Called by the servlet container to indicate to a servlet that the servlet is being placed into service. + *

+ * The servlet container calls the init method exactly once after instantiating the servlet. The + * init method must complete successfully before the servlet can receive any requests. + *

+ * The servlet container cannot place the servlet into service if the init method + *

    + *
  1. Throws a ServletException + *
  2. Does not return within a time period defined by the Web server + *
+ * + * @param config a ServletConfig object containing the servlet's configuration and initialization + * parameters + * + * @exception ServletException if an exception has occurred that interferes with the servlet's normal operation + * + * @see UnavailableException + * @see #getServletConfig + */ + void init(ServletConfig config) throws ServletException; + + /** + * Returns a {@link ServletConfig} object, which contains initialization and startup parameters for this servlet. + * The ServletConfig object returned is the one passed to the init method. + *

+ * Implementations of this interface are responsible for storing the ServletConfig object so that this + * method can return it. The {@link GenericServlet} class, which implements this interface, already does this. + * + * @return the ServletConfig object that initializes this servlet + * + * @see #init + */ + ServletConfig getServletConfig(); + + /** + * Called by the servlet container to allow the servlet to respond to a request. + *

+ * This method is only called after the servlet's init() method has completed successfully. + *

+ * The status code of the response always should be set for a servlet that throws or sends an error. + *

+ * Servlets typically run inside multithreaded servlet containers that can handle multiple requests concurrently. + * Developers must be aware to synchronize access to any shared resources such as files, network connections, and as + * well as the servlet's class and instance variables. More information on multithreaded programming in Java is + * available in the Java tutorial on + * multi-threaded programming. + * + * @param req the ServletRequest object that contains the client's request + * @param res the ServletResponse object that contains the servlet's response + * + * @exception ServletException if an exception occurs that interferes with the servlet's normal operation + * @exception IOException if an input or output exception occurs + */ + void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; + + /** + * Returns information about the servlet, such as author, version, and copyright. + *

+ * The string that this method returns should be plain text and not markup of any kind (such as HTML, XML, etc.). + * + * @return a String containing servlet information + */ + String getServletInfo(); + + /** + * Called by the servlet container to indicate to a servlet that the servlet is being taken out of service. This + * method is only called once all threads within the servlet's service method have exited or after a + * timeout period has passed. After the servlet container calls this method, it will not call the + * service method again on this servlet. + *

+ * This method gives the servlet an opportunity to clean up any resources that are being held (for example, memory, + * file handles, threads) and make sure that any persistent state is synchronized with the servlet's current state + * in memory. + */ + void destroy(); +} diff --git a/java/jakarta/servlet/ServletConfig.java b/java/jakarta/servlet/ServletConfig.java new file mode 100644 index 0000000..f0c750e --- /dev/null +++ b/java/jakarta/servlet/ServletConfig.java @@ -0,0 +1,63 @@ +/* + * 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 jakarta.servlet; + +import java.util.Enumeration; + +/** + * A servlet configuration object used by a servlet container to pass information to a servlet during initialization. + */ +public interface ServletConfig { + + /** + * Returns the name of this servlet instance. The name may be provided via server administration, assigned in the + * web application deployment descriptor, or for an unregistered (and thus unnamed) servlet instance it will be the + * servlet's class name. + * + * @return the name of the servlet instance + */ + String getServletName(); + + /** + * Returns a reference to the {@link ServletContext} in which the caller is executing. + * + * @return a {@link ServletContext} object, used by the caller to interact with its servlet container + * + * @see ServletContext + */ + ServletContext getServletContext(); + + /** + * Returns a String containing the value of the named initialization parameter, or null if + * the parameter does not exist. + * + * @param name a String specifying the name of the initialization parameter + * + * @return a String containing the value of the initialization parameter + */ + String getInitParameter(String name); + + /** + * Returns the names of the servlet's initialization parameters as an Enumeration of + * String objects, or an empty Enumeration if the servlet has no initialization + * parameters. + * + * @return an Enumeration of String objects containing the names of the servlet's + * initialization parameters + */ + Enumeration getInitParameterNames(); +} diff --git a/java/jakarta/servlet/ServletConnection.java b/java/jakarta/servlet/ServletConnection.java new file mode 100644 index 0000000..acdaee1 --- /dev/null +++ b/java/jakarta/servlet/ServletConnection.java @@ -0,0 +1,83 @@ +/* + * 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 jakarta.servlet; + +/** + * Provides information about the connection made to the Servlet container. This interface is intended primarily for + * debugging purposes and as such provides the raw information as seen by the container. Unless explicitly stated + * otherwise in the Javadoc for a method, no adjustment is made for the presence of reverse proxies or similar + * configurations. + * + * @since Servlet 6.0 + */ +public interface ServletConnection { + + /** + * Obtain a unique (within the lifetime of the JVM) identifier string for the network connection to the JVM that is + * being used for the {@code ServletRequest} from which this {@code ServletConnection} was obtained. + *

+ * There is no defined format for this string. The format is implementation dependent. + * + * @return A unique identifier for the network connection + */ + String getConnectionId(); + + /** + * Obtain the name of the protocol as presented to the server after the removal, if present, of any TLS or similar + * encryption. This may not be the same as the protocol seen by the application. For example, a reverse proxy may + * present AJP whereas the application will see HTTP 1.1. + *

+ * If the protocol has an entry in the IANA + * registry for ALPN names then the identification sequence, in string form, must be returned. Registered + * identification sequences MUST only be used for the associated protocol. Return values for other protocols are + * implementation dependent. Unknown protocols should return the string "unknown". + * + * @return The name of the protocol presented to the server after decryption of TLS, or similar encryption, if any. + */ + String getProtocol(); + + /** + * Obtain the connection identifier for the network connection to the server that is being used for the + * {@code ServletRequest} from which this {@code ServletConnection} was obtained as defined by the protocol in use. + * Note that some protocols do not define such an identifier. + *

+ * Examples of protocol provided connection identifiers include: + *

+ *
HTTP 1.x
+ *
None, so the empty string should be returned
+ *
HTTP 2
+ *
None, so the empty string should be returned
+ *
HTTP 3
+ *
The QUIC connection ID
+ *
AJP
+ *
None, so the empty string should be returned
+ *
+ * + * @return The connection identifier if one is defined, otherwise an empty string + */ + String getProtocolConnectionId(); + + /** + * Determine whether or not the incoming network connection to the server used encryption or not. Note that where a + * reverse proxy is used, the application may have a different view as to whether encryption is being used due to + * the use of headers like {@code X-Forwarded-Proto}. + * + * @return {@code true} if the incoming network connection used encryption, otherwise {@code false} + */ + boolean isSecure(); +} \ No newline at end of file diff --git a/java/jakarta/servlet/ServletContainerInitializer.java b/java/jakarta/servlet/ServletContainerInitializer.java new file mode 100644 index 0000000..14e0ec7 --- /dev/null +++ b/java/jakarta/servlet/ServletContainerInitializer.java @@ -0,0 +1,47 @@ +/* + * 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 jakarta.servlet; + +import java.util.Set; + +/** + * ServletContainerInitializers (SCIs) are registered via an entry in the file + * META-INF/services/jakarta.servlet.ServletContainerInitializer that must be included in the JAR file that contains the + * SCI implementation. + *

+ * SCI processing is performed regardless of the setting of metadata-complete. SCI processing can be controlled per JAR + * file via fragment ordering. If absolute ordering is defined, then only the JARs included in the ordering will be + * processed for SCIs. To disable SCI processing completely, an empty absolute ordering may be defined. + *

+ * SCIs register an interest in annotations (class, method or field) and/or types via the + * {@link jakarta.servlet.annotation.HandlesTypes} annotation which is added to the class. + * + * @since Servlet 3.0 + */ +public interface ServletContainerInitializer { + + /** + * Receives notification during startup of a web application of the classes within the web application that matched + * the criteria defined via the {@link jakarta.servlet.annotation.HandlesTypes} annotation. + * + * @param c The (possibly null) set of classes that met the specified criteria + * @param ctx The ServletContext of the web application in which the classes were discovered + * + * @throws ServletException If an error occurs + */ + void onStartup(Set> c, ServletContext ctx) throws ServletException; +} diff --git a/java/jakarta/servlet/ServletContext.java b/java/jakarta/servlet/ServletContext.java new file mode 100644 index 0000000..fe7945c --- /dev/null +++ b/java/jakarta/servlet/ServletContext.java @@ -0,0 +1,938 @@ +/* + * 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 jakarta.servlet; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.descriptor.JspConfigDescriptor; + +/** + * Defines a set of methods that a servlet uses to communicate with its servlet container, for example, to get the MIME + * type of a file, dispatch requests, or write to a log file. + *

+ * There is one context per "web application" per Java Virtual Machine. (A "web application" is a collection of servlets + * and content installed under a specific subset of the server's URL namespace such as /catalog and + * possibly installed via a .war file.) + *

+ * In the case of a web application marked "distributed" in its deployment descriptor, there will be one context + * instance for each virtual machine. In this situation, the context cannot be used as a location to share global + * information (because the information won't be truly global). Use an external resource like a database instead. + *

+ * The ServletContext object is contained within the {@link ServletConfig} object, which the Web server + * provides the servlet when the servlet is initialized. + * + * @see Servlet#getServletConfig + * @see ServletConfig#getServletContext + */ +public interface ServletContext { + + /** + * The name of the ServletContext attribute that holds the temporary file location for the web application. + */ + String TEMPDIR = "jakarta.servlet.context.tempdir"; + + /** + * The name of the ServletContext attribute that holds the ordered list of web fragments for this web application. + * + * @since Servlet 3.0 + */ + String ORDERED_LIBS = "jakarta.servlet.context.orderedLibs"; + + /** + * Return the main path associated with this context. + * + * @return The main context path + * + * @since Servlet 2.5 + */ + String getContextPath(); + + /** + * Returns a ServletContext object that corresponds to a specified URL on the server. + *

+ * This method allows servlets to gain access to the context for various parts of the server, and as needed obtain + * {@link RequestDispatcher} objects from the context. The given path must be begin with "/", is interpreted + * relative to the server's document root and is matched against the context roots of other web applications hosted + * on this container. + *

+ * In a security conscious environment, the servlet container may return null for a given URL. + * + * @param uripath a String specifying the context path of another web application in the container. + * + * @return the ServletContext object that corresponds to the named URL, or null if either none exists + * or the container wishes to restrict this access. + * + * @see RequestDispatcher + */ + ServletContext getContext(String uripath); + + /** + * Returns the major version of the Java Servlet API that this servlet container supports. All implementations that + * comply with Version 6.0 must have this method return the integer 6. + * + * @return 6 + */ + int getMajorVersion(); + + /** + * Returns the minor version of the Servlet API that this servlet container supports. All implementations that + * comply with Version 6.0 must have this method return the integer 0. + * + * @return 0 + */ + int getMinorVersion(); + + /** + * Obtain the major version of the servlet specification for which this web application is implemented. + * + * @return The major version declared in web.xml + * + * @since Servlet 3.0 + */ + int getEffectiveMajorVersion(); + + /** + * Obtain the minor version of the servlet specification for which this web application is implemented. + * + * @return The minor version declared in web.xml + * + * @since Servlet 3.0 + */ + int getEffectiveMinorVersion(); + + /** + * Returns the MIME type of the specified file, or null if the MIME type is not known. The MIME type is + * determined by the configuration of the servlet container, and may be specified in a web application deployment + * descriptor. Common MIME types are "text/html" and "image/gif". + * + * @param file a String specifying the name of a file + * + * @return a String specifying the file's MIME type + */ + String getMimeType(String file); + + /** + * Returns a directory-like listing of all the paths to resources within the web application whose longest sub-path + * matches the supplied path argument. Paths indicating subdirectory paths end with a '/'. The returned paths are + * all relative to the root of the web application and have a leading '/'. For example, for a web application + * containing
+ *
+ * /welcome.html
+ * /catalog/index.html
+ * /catalog/products.html
+ * /catalog/offers/books.html
+ * /catalog/offers/music.html
+ * /customer/login.jsp
+ * /WEB-INF/web.xml
+ * /WEB-INF/classes/com.acme.OrderServlet.class,
+ *
+ * getResourcePaths("/") returns {"/welcome.html", "/catalog/", "/customer/", "/WEB-INF/"}
+ * getResourcePaths("/catalog/") returns {"/catalog/index.html", "/catalog/products.html", "/catalog/offers/"}.
+ * + * @param path the partial path used to match the resources, which must start with a / + * + * @return a Set containing the directory listing, or null if there are no resources in the web application whose + * path begins with the supplied path. + * + * @since Servlet 2.3 + */ + Set getResourcePaths(String path); + + /** + * Returns a URL to the resource that is mapped to a specified path. The path must begin with a "/" and is + * interpreted as relative to the current context root. + *

+ * This method allows the servlet container to make a resource available to servlets from any source. Resources can + * be located on a local or remote file system, in a database, or in a .war file. + *

+ * The servlet container must implement the URL handlers and URLConnection objects that are necessary + * to access the resource. + *

+ * This method returns null if no resource is mapped to the pathname. + *

+ * Some containers may allow writing to the URL returned by this method using the methods of the URL class. + *

+ * The resource content is returned directly, so be aware that requesting a .jsp page returns the JSP + * source code. Use a RequestDispatcher instead to include results of an execution. + *

+ * This method has a different purpose than java.lang.Class.getResource, which looks up resources based + * on a class loader. This method does not use class loaders. + * + * @param path a String specifying the path to the resource + * + * @return the resource located at the named path, or null if there is no resource at that path + * + * @exception MalformedURLException if the pathname is not given in the correct form + */ + URL getResource(String path) throws MalformedURLException; + + /** + * Returns the resource located at the named path as an InputStream object. + *

+ * The data in the InputStream can be of any type or length. The path must be specified according to + * the rules given in getResource. This method returns null if no resource exists at the + * specified path. + *

+ * Meta-information such as content length and content type that is available via getResource method is + * lost when using this method. + *

+ * The servlet container must implement the URL handlers and URLConnection objects necessary to access + * the resource. + *

+ * This method is different from java.lang.Class.getResourceAsStream, which uses a class loader. This + * method allows servlet containers to make a resource available to a servlet from any location, without using a + * class loader. + * + * @param path a String specifying the path to the resource + * + * @return the InputStream returned to the servlet, or null if no resource exists at the + * specified path + */ + InputStream getResourceAsStream(String path); + + /** + * Returns a {@link RequestDispatcher} object that acts as a wrapper for the resource located at the given path. A + * RequestDispatcher object can be used to forward a request to the resource or to include the resource + * in a response. The resource can be dynamic or static. + *

+ * The pathname must begin with a "/" and is interpreted as relative to the current context root. Use + * getContext to obtain a RequestDispatcher for resources in foreign contexts. This method + * returns null if the ServletContext cannot return a RequestDispatcher. + * + * @param path a String specifying the pathname to the resource + * + * @return a RequestDispatcher object that acts as a wrapper for the resource at the specified path, or + * null if the ServletContext cannot return a RequestDispatcher + * + * @see RequestDispatcher + * @see ServletContext#getContext + */ + RequestDispatcher getRequestDispatcher(String path); + + /** + * Returns a {@link RequestDispatcher} object that acts as a wrapper for the named servlet. + *

+ * Servlets (and JSP pages also) may be given names via server administration or via a web application deployment + * descriptor. A servlet instance can determine its name using {@link ServletConfig#getServletName}. + *

+ * This method returns null if the ServletContext cannot return a + * RequestDispatcher for any reason. + * + * @param name a String specifying the name of a servlet to wrap + * + * @return a RequestDispatcher object that acts as a wrapper for the named servlet, or + * null if the ServletContext cannot return a RequestDispatcher + * + * @see RequestDispatcher + * @see ServletContext#getContext + * @see ServletConfig#getServletName + */ + RequestDispatcher getNamedDispatcher(String name); + + /** + * Writes the specified message to a servlet log file, usually an event log. The name and type of the servlet log + * file is specific to the servlet container. + * + * @param msg a String specifying the message to be written to the log file + */ + void log(String msg); + + /** + * Writes an explanatory message and a stack trace for a given Throwable exception to the servlet log + * file. The name and type of the servlet log file is specific to the servlet container, usually an event log. + * + * @param message a String that describes the error or exception + * @param throwable the Throwable error or exception + */ + void log(String message, Throwable throwable); + + /** + * Returns a String containing the real path for a given virtual path. For example, the path + * "/index.html" returns the absolute file path on the server's filesystem would be served by a request for + * "http://host/contextPath/index.html", where contextPath is the context path of this ServletContext.. + *

+ * The real path returned will be in a form appropriate to the computer and operating system on which the servlet + * container is running, including the proper path separators. This method returns null if the servlet + * container cannot translate the virtual path to a real path for any reason (such as when the content is being made + * available from a .war archive). + * + * @param path a String specifying a virtual path + * + * @return a String specifying the real path, or null if the translation cannot be performed + */ + String getRealPath(String path); + + /** + * Returns the name and version of the servlet container on which the servlet is running. + *

+ * The form of the returned string is servername/versionnumber. For example, the JavaServer Web + * Development Kit may return the string JavaServer Web Dev Kit/1.0. + *

+ * The servlet container may return other optional information after the primary string in parentheses, for example, + * JavaServer Web Dev Kit/1.0 (JDK 1.1.6; Windows NT 4.0 x86). + * + * @return a String containing at least the servlet container name and version number + */ + String getServerInfo(); + + /** + * Returns a String containing the value of the named context-wide initialization parameter, or + * null if the parameter does not exist. + *

+ * This method can make available configuration information useful to an entire "web application". For example, it + * can provide a web site administrator's email address or the name of a system that holds critical data. + * + * @param name a String containing the name of the parameter whose value is requested + * + * @return a String containing the value of the initialization parameter + * + * @throws NullPointerException If the provided parameter name is null + * + * @see ServletConfig#getInitParameter + */ + String getInitParameter(String name); + + /** + * Returns the names of the context's initialization parameters as an Enumeration of + * String objects, or an empty Enumeration if the context has no initialization + * parameters. + * + * @return an Enumeration of String objects containing the names of the context's + * initialization parameters + * + * @see ServletConfig#getInitParameter + */ + + Enumeration getInitParameterNames(); + + /** + * Set the given initialisation parameter to the given value. + * + * @param name Name of initialisation parameter + * @param value Value for initialisation parameter + * + * @return true if the call succeeds or false if the call fails because an initialisation + * parameter with the same name has already been set + * + * @throws IllegalStateException If initialisation of this ServletContext has already completed + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * @throws NullPointerException If the provided parameter name is null + * + * @since Servlet 3.0 + */ + boolean setInitParameter(String name, String value); + + /** + * Returns the servlet container attribute with the given name, or null if there is no attribute by + * that name. An attribute allows a servlet container to give the servlet additional information not already + * provided by this interface. See your server documentation for information about its attributes. A list of + * supported attributes can be retrieved using getAttributeNames. + *

+ * The attribute is returned as a java.lang.Object or some subclass. Attribute names should follow the + * same convention as package names. The Jakarta EE platform reserves names matching jakarta.*. + * + * @param name a String specifying the name of the attribute + * + * @return an Object containing the value of the attribute, or null if no attribute exists + * matching the given name + * + * @throws NullPointerException If the provided attribute name is null + * + * @see ServletContext#getAttributeNames + */ + Object getAttribute(String name); + + /** + * Returns an Enumeration containing the attribute names available within this servlet context. Use the + * {@link #getAttribute} method with an attribute name to get the value of an attribute. + * + * @return an Enumeration of attribute names + * + * @see #getAttribute + */ + Enumeration getAttributeNames(); + + /** + * Binds an object to a given attribute name in this servlet context. If the name specified is already used for an + * attribute, this method will replace the attribute with the new to the new attribute. + *

+ * If listeners are configured on the ServletContext the container notifies them accordingly. + *

+ * If a null value is passed, the effect is the same as calling removeAttribute(). + *

+ * Attribute names should follow the same convention as package names. The Jakarta EE platform reserves names + * matching jakarta.*. + * + * @param name a String specifying the name of the attribute + * @param object an Object representing the attribute to be bound + * + * @throws NullPointerException If the provided attribute name is null + */ + void setAttribute(String name, Object object); + + /** + * Removes the attribute with the given name from the servlet context. After removal, subsequent calls to + * {@link #getAttribute} to retrieve the attribute's value will return null. + *

+ * If listeners are configured on the ServletContext the container notifies them accordingly. + * + * @param name a String specifying the name of the attribute to be removed + */ + void removeAttribute(String name); + + /** + * Returns the name of this web application corresponding to this ServletContext as specified in the deployment + * descriptor for this web application by the display-name element. + * + * @return The name of the web application or null if no name has been declared in the deployment descriptor. + * + * @since Servlet 2.3 + */ + String getServletContextName(); + + /** + * Register a servlet implementation for use in this ServletContext. + * + * @param servletName The name of the servlet to register + * @param className The implementation class for the servlet + * + * @return The registration object that enables further configuration + * + * @throws IllegalStateException If the context has already been initialised + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + ServletRegistration.Dynamic addServlet(String servletName, String className); + + /** + * Register a servlet instance for use in this ServletContext. + * + * @param servletName The name of the servlet to register + * @param servlet The Servlet instance to register + * + * @return The registration object that enables further configuration + * + * @throws IllegalStateException If the context has already been initialised + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet); + + /** + * Add servlet to the context. + * + * @param servletName Name of servlet to add + * @param servletClass Class of servlet to add + * + * @return null if the servlet has already been fully defined, else a + * {@link jakarta.servlet.ServletRegistration.Dynamic} object that can be used to further configure the + * servlet + * + * @throws IllegalStateException If the context has already been initialised + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + ServletRegistration.Dynamic addServlet(String servletName, Class servletClass); + + /** + * Add a JSP to the context. + * + * @param jspName The servlet name under which this JSP file should be registered + * @param jspFile The path, relative to the web application root, for the JSP file to be used for this servlet + * + * @return a {@link jakarta.servlet.ServletRegistration.Dynamic} object that can be used to further configure the + * servlet + * + * @since Servlet 4.0 + */ + ServletRegistration.Dynamic addJspFile(String jspName, String jspFile); + + /** + * Create an Servlet instance using the given class. The instance is just created. No initialisation occurs. + * + * @param The type for the given class + * @param c The the class for which an instance should be created + * + * @return The created Servlet instance. + * + * @throws ServletException If the servlet instance cannot be created. + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + T createServlet(Class c) throws ServletException; + + /** + * Obtain the details of the named servlet. + * + * @param servletName The name of the Servlet of interest + * + * @return The registration details for the named Servlet or null if no Servlet has been registered + * with the given name + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + ServletRegistration getServletRegistration(String servletName); + + /** + * Obtain a Map of servlet names to servlet registrations for all servlets registered with this context. + * + * @return A Map of servlet names to servlet registrations for all servlets registered with this context + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + Map getServletRegistrations(); + + /** + * Add filter to context. + * + * @param filterName Name of filter to add + * @param className Name of filter class + * + * @return null if the filter has already been fully defined, else a + * {@link jakarta.servlet.FilterRegistration.Dynamic} object that can be used to further configure the + * filter + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * @throws IllegalStateException If the context has already been initialised + * + * @since Servlet 3.0 + */ + FilterRegistration.Dynamic addFilter(String filterName, String className); + + /** + * Add filter to context. + * + * @param filterName Name of filter to add + * @param filter Filter to add + * + * @return null if the filter has already been fully defined, else a + * {@link jakarta.servlet.FilterRegistration.Dynamic} object that can be used to further configure the + * filter + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * @throws IllegalStateException If the context has already been initialised + * + * @since Servlet 3.0 + */ + FilterRegistration.Dynamic addFilter(String filterName, Filter filter); + + /** + * Add filter to context. + * + * @param filterName Name of filter to add + * @param filterClass Class of filter to add + * + * @return null if the filter has already been fully defined, else a + * {@link jakarta.servlet.FilterRegistration.Dynamic} object that can be used to further configure the + * filter + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * @throws IllegalStateException If the context has already been initialised + * + * @since Servlet 3.0 + */ + FilterRegistration.Dynamic addFilter(String filterName, Class filterClass); + + /** + * Create a Filter instance using the given class. The instance is just created. No initialisation occurs. + * + * @param The type for the given class + * @param c The the class for which an instance should be created + * + * @return The created Filter instance. + * + * @throws ServletException If the Filter instance cannot be created + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + T createFilter(Class c) throws ServletException; + + /** + * TODO SERVLET3 - Add comments + * + * @param filterName TODO + * + * @return TODO + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + FilterRegistration getFilterRegistration(String filterName); + + /** + * @return TODO + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + Map getFilterRegistrations(); + + /** + * @return TODO + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + SessionCookieConfig getSessionCookieConfig(); + + /** + * Configures the available session tracking modes for this web application. + * + * @param sessionTrackingModes The session tracking modes to use for this web application + * + * @throws IllegalArgumentException If sessionTrackingModes specifies {@link SessionTrackingMode#SSL} in + * combination with any other {@link SessionTrackingMode} + * @throws IllegalStateException If the context has already been initialised + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + void setSessionTrackingModes(Set sessionTrackingModes); + + /** + * Obtains the default session tracking modes for this web application. By default {@link SessionTrackingMode#URL} + * is always supported, {@link SessionTrackingMode#COOKIE} is supported unless the cookies attribute + * has been set to false for the context and {@link SessionTrackingMode#SSL} is supported if at least + * one of the connectors used by this context has the attribute secure set to true. + * + * @return The set of default session tracking modes for this web application + * + * @since Servlet 3.0 + */ + Set getDefaultSessionTrackingModes(); + + /** + * Obtains the currently enabled session tracking modes for this web application. + * + * @return The value supplied via {@link #setSessionTrackingModes(Set)} if one was previously set, else return the + * defaults + * + * @since Servlet 3.0 + */ + Set getEffectiveSessionTrackingModes(); + + /** + * TODO SERVLET3 - Add comments + * + * @param className TODO + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + void addListener(String className); + + /** + * TODO SERVLET3 - Add comments + * + * @param TODO + * @param t TODO + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + void addListener(T t); + + /** + * TODO SERVLET3 - Add comments + * + * @param listenerClass TODO + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + void addListener(Class listenerClass); + + /** + * TODO SERVLET3 - Add comments + * + * @param TODO + * @param c TODO + * + * @return TODO + * + * @throws ServletException TODO + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * + * @since Servlet 3.0 + */ + T createListener(Class c) throws ServletException; + + /** + * @return TODO + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + JspConfigDescriptor getJspConfigDescriptor(); + + /** + * Get the web application class loader associated with this ServletContext. + * + * @return The associated web application class loader + * + * @throws SecurityException if access to the class loader is prevented by a SecurityManager + * + * @since Servlet 3.0 + */ + ClassLoader getClassLoader(); + + /** + * Add to the declared roles for this ServletContext. + * + * @param roleNames The roles to add + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * @throws IllegalArgumentException If the list of roleNames is null or empty + * @throws IllegalStateException If the ServletContext has already been initialised + * + * @since Servlet 3.0 + */ + void declareRoles(String... roleNames); + + /** + * Get the primary name of the virtual host on which this context is deployed. The name may or may not be a valid + * host name. + * + * @return The primary name of the virtual host on which this context is deployed + * + * @since Servlet 3.1 + */ + String getVirtualServerName(); + + /** + * Get the default session timeout. + * + * @return The current default session timeout in minutes + * + * @since Servlet 4.0 + */ + int getSessionTimeout(); + + /** + * Set the default session timeout. This method may only be called before the ServletContext is initialised. + * + * @param sessionTimeout The new default session timeout in minutes. + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * @throws IllegalStateException If the ServletContext has already been initialised + * + * @since Servlet 4.0 + */ + void setSessionTimeout(int sessionTimeout); + + /** + * Get the default character encoding for reading request bodies. + * + * @return The character encoding name or {@code null} if no default has been specified + * + * @since Servlet 4.0 + */ + String getRequestCharacterEncoding(); + + /** + * Set the default character encoding to use for reading request bodies. Calling this method will over-ride any + * value set in the deployment descriptor. + * + * @param encoding The name of the character encoding to use + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * @throws IllegalStateException If the ServletContext has already been initialised + * + * @since Servlet 4.0 + */ + void setRequestCharacterEncoding(String encoding); + + /** + * Get the default character encoding for writing response bodies. + * + * @return The character encoding name or {@code null} if no default has been specified + * + * @since Servlet 4.0 + */ + String getResponseCharacterEncoding(); + + /** + * Set the default character encoding to use for writing response bodies. Calling this method will over-ride any + * value set in the deployment descriptor. + * + * @param encoding The name of the character encoding to use + * + * @throws UnsupportedOperationException If called from a + * {@link ServletContextListener#contextInitialized(ServletContextEvent)} + * method of a {@link ServletContextListener} that was not defined in a + * web.xml file, a web-fragment.xml file nor annotated with + * {@link jakarta.servlet.annotation.WebListener}. For example, a + * {@link ServletContextListener} defined in a TLD would not be able to + * use this method. + * @throws IllegalStateException If the ServletContext has already been initialised + * + * @since Servlet 4.0 + */ + void setResponseCharacterEncoding(String encoding); +} diff --git a/java/jakarta/servlet/ServletContextAttributeEvent.java b/java/jakarta/servlet/ServletContextAttributeEvent.java new file mode 100644 index 0000000..6103265 --- /dev/null +++ b/java/jakarta/servlet/ServletContextAttributeEvent.java @@ -0,0 +1,72 @@ +/* + * 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 jakarta.servlet; + +/** + * This is the event class for notifications about changes to the attributes of the servlet context of a web + * application. + * + * @see ServletContextAttributeListener + * + * @since Servlet 2.3 + */ +public class ServletContextAttributeEvent extends ServletContextEvent { + private static final long serialVersionUID = 1L; + + /** + * Attribute name. + */ + private final String name; + + /** + * Attribute value. + */ + private final Object value; + + /** + * Construct a ServletContextAttributeEvent from the given context for the given attribute name and attribute value. + * + * @param source The ServletContext associated with this attribute event + * @param name The name of the servlet context attribute + * @param value The value of the servlet context attribute + */ + public ServletContextAttributeEvent(ServletContext source, String name, Object value) { + super(source); + this.name = name; + this.value = value; + } + + /** + * Return the name of the attribute that changed on the ServletContext. + * + * @return The name of the attribute that changed + */ + public String getName() { + return this.name; + } + + /** + * Returns the value of the attribute that has been added, removed, or replaced. + * + * @return If the attribute was added, this is the value of the attribute. If the attribute was removed, this is the + * value of the removed attribute. If the attribute was replaced, this is the old value of the + * attribute. + */ + public Object getValue() { + return this.value; + } +} diff --git a/java/jakarta/servlet/ServletContextAttributeListener.java b/java/jakarta/servlet/ServletContextAttributeListener.java new file mode 100644 index 0000000..17df62d --- /dev/null +++ b/java/jakarta/servlet/ServletContextAttributeListener.java @@ -0,0 +1,58 @@ +/* + * 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 jakarta.servlet; + +import java.util.EventListener; + +/** + * Implementations of this interface receive notifications of changes to the attribute list on the servlet context of a + * web application. To receive notification events, the implementation class must be configured in the deployment + * descriptor for the web application. + * + * @see ServletContextAttributeEvent + * + * @since Servlet 2.3 + */ +public interface ServletContextAttributeListener extends EventListener { + + /** + * Notification that a new attribute was added to the servlet context. Called after the attribute is added. The + * default implementation is a NO-OP. + * + * @param scae Information about the new attribute + */ + default void attributeAdded(ServletContextAttributeEvent scae) { + } + + /** + * Notification that an existing attribute has been removed from the servlet context. Called after the attribute is + * removed. The default implementation is a NO-OP. + * + * @param scae Information about the removed attribute + */ + default void attributeRemoved(ServletContextAttributeEvent scae) { + } + + /** + * Notification that an attribute on the servlet context has been replaced. Called after the attribute is replaced. + * The default implementation is a NO-OP. + * + * @param scae Information about the replaced attribute + */ + default void attributeReplaced(ServletContextAttributeEvent scae) { + } +} diff --git a/java/jakarta/servlet/ServletContextEvent.java b/java/jakarta/servlet/ServletContextEvent.java new file mode 100644 index 0000000..dc098b6 --- /dev/null +++ b/java/jakarta/servlet/ServletContextEvent.java @@ -0,0 +1,47 @@ +/* + * 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 jakarta.servlet; + +/** + * This is the event class for notifications about changes to the servlet context of a web application. + * + * @see ServletContextListener + * + * @since Servlet 2.3 + */ +public class ServletContextEvent extends java.util.EventObject { + + private static final long serialVersionUID = 1L; + + /** + * Construct a ServletContextEvent from the given context. + * + * @param source - the ServletContext that is sending the event. + */ + public ServletContextEvent(ServletContext source) { + super(source); + } + + /** + * Return the ServletContext that changed. + * + * @return the ServletContext that sent the event. + */ + public ServletContext getServletContext() { + return (ServletContext) super.getSource(); + } +} diff --git a/java/jakarta/servlet/ServletContextListener.java b/java/jakarta/servlet/ServletContextListener.java new file mode 100644 index 0000000..b695635 --- /dev/null +++ b/java/jakarta/servlet/ServletContextListener.java @@ -0,0 +1,50 @@ +/* + * 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 jakarta.servlet; + +import java.util.EventListener; + +/** + * Implementations of this interface receive notifications about changes to the servlet context of the web application + * they are part of. To receive notification events, the implementation class must be configured in the deployment + * descriptor for the web application. + * + * @see ServletContextEvent + * + * @since Servlet 2.3 + */ +public interface ServletContextListener extends EventListener { + + /** + ** Notification that the web application initialization process is starting. All ServletContextListeners are + * notified of context initialization before any filter or servlet in the web application is initialized. The + * default implementation is a NO-OP. + * + * @param sce Information about the ServletContext that was initialized + */ + default void contextInitialized(ServletContextEvent sce) { + } + + /** + ** Notification that the servlet context is about to be shut down. All servlets and filters have been destroyed + * before any ServletContextListeners are notified of context destruction. The default implementation is a NO-OP. + * + * @param sce Information about the ServletContext that was destroyed + */ + default void contextDestroyed(ServletContextEvent sce) { + } +} diff --git a/java/jakarta/servlet/ServletException.java b/java/jakarta/servlet/ServletException.java new file mode 100644 index 0000000..ca8b5f1 --- /dev/null +++ b/java/jakarta/servlet/ServletException.java @@ -0,0 +1,79 @@ +/* + * 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 jakarta.servlet; + +/** + * Defines a general exception a servlet can throw when it encounters difficulty. + */ +public class ServletException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new servlet exception. + */ + public ServletException() { + super(); + } + + /** + * Constructs a new servlet exception with the specified message. The message can be written to the server log + * and/or displayed for the user. + * + * @param message a String specifying the text of the exception message + */ + public ServletException(String message) { + super(message); + } + + /** + * Constructs a new servlet exception when the servlet needs to throw an exception and include a message about the + * "root cause" exception that interfered with its normal operation, including a description message. + * + * @param message a String containing the text of the exception message + * @param rootCause the Throwable exception that interfered with the servlet's normal operation, making + * this servlet exception necessary + */ + public ServletException(String message, Throwable rootCause) { + super(message, rootCause); + } + + /** + * Constructs a new servlet exception when the servlet needs to throw an exception and include a message about the + * "root cause" exception that interfered with its normal operation. The exception's message is based on the + * localized message of the underlying exception. + *

+ * This method calls the getLocalizedMessage method on the Throwable exception to get a + * localized exception message. When subclassing ServletException, this method can be overridden to + * create an exception message designed for a specific locale. + * + * @param rootCause the Throwable exception that interfered with the servlet's normal operation, making + * the servlet exception necessary + */ + public ServletException(Throwable rootCause) { + super(rootCause); + } + + /** + * Returns the exception that caused this servlet exception. + * + * @return the Throwable that caused this servlet exception + */ + public Throwable getRootCause() { + return getCause(); + } +} diff --git a/java/jakarta/servlet/ServletInputStream.java b/java/jakarta/servlet/ServletInputStream.java new file mode 100644 index 0000000..654e724 --- /dev/null +++ b/java/jakarta/servlet/ServletInputStream.java @@ -0,0 +1,106 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Provides an input stream for reading binary data from a client request, including an efficient readLine + * method for reading data one line at a time. With some protocols, such as HTTP POST and PUT, a + * ServletInputStream object can be used to read data sent from the client. + *

+ * A ServletInputStream object is normally retrieved via the {@link ServletRequest#getInputStream} method. + *

+ * This is an abstract class that a servlet container implements. Subclasses of this class must implement the + * java.io.InputStream.read() method. + * + * @see ServletRequest + */ +public abstract class ServletInputStream extends InputStream { + + /** + * Does nothing, because this is an abstract class. + */ + protected ServletInputStream() { + // NOOP + } + + /** + * Reads the input stream, one line at a time. Starting at an offset, reads bytes into an array, until it reads a + * certain number of bytes or reaches a newline character, which it reads into the array as well. + *

+ * This method returns -1 if it reaches the end of the input stream before reading the maximum number of bytes. + * + * @param b an array of bytes into which data is read + * @param off an integer specifying the character at which this method begins reading + * @param len an integer specifying the maximum number of bytes to read + * + * @return an integer specifying the actual number of bytes read, or -1 if the end of the stream is reached + * + * @exception IOException if an input or output exception has occurred + */ + public int readLine(byte[] b, int off, int len) throws IOException { + + if (len <= 0) { + return 0; + } + int count = 0, c; + + while ((c = read()) != -1) { + b[off++] = (byte) c; + count++; + if (c == '\n' || count == len) { + break; + } + } + return count > 0 ? count : -1; + } + + /** + * Has the end of this InputStream been reached? + * + * @return true if all the data has been read from the stream, else false + * + * @since Servlet 3.1 + */ + public abstract boolean isFinished(); + + /** + * Can data be read from this InputStream without blocking? Returns If this method is called and returns false, the + * container will invoke {@link ReadListener#onDataAvailable()} when data is available. + * + * @return true if data can be read without blocking, else false + * + * @since Servlet 3.1 + */ + public abstract boolean isReady(); + + /** + * Sets the {@link ReadListener} for this {@link ServletInputStream} and thereby switches to non-blocking IO. It is + * only valid to switch to non-blocking IO within async processing or HTTP upgrade processing. + * + * @param listener The non-blocking IO read listener + * + * @throws IllegalStateException If this method is called if neither async nor HTTP upgrade is in progress or if the + * {@link ReadListener} has already been set + * @throws NullPointerException If listener is null + * + * @since Servlet 3.1 + */ + public abstract void setReadListener(ReadListener listener); +} diff --git a/java/jakarta/servlet/ServletOutputStream.java b/java/jakarta/servlet/ServletOutputStream.java new file mode 100644 index 0000000..78c9312 --- /dev/null +++ b/java/jakarta/servlet/ServletOutputStream.java @@ -0,0 +1,273 @@ +/* + * 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 jakarta.servlet; + +import java.io.CharConversionException; +import java.io.IOException; +import java.io.OutputStream; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +/** + * Provides an output stream for sending binary data to the client. A ServletOutputStream object is + * normally retrieved via the {@link ServletResponse#getOutputStream} method. + *

+ * This is an abstract class that the servlet container implements. Subclasses of this class must implement the + * java.io.OutputStream.write(int) method. + * + * @see ServletResponse + */ +public abstract class ServletOutputStream extends OutputStream { + + private static final String LSTRING_FILE = "jakarta.servlet.LocalStrings"; + private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + /** + * Does nothing, because this is an abstract class. + */ + protected ServletOutputStream() { + // NOOP + } + + /** + * Writes a String to the client, without a carriage return-line feed (CRLF) character at the end. + * + * @param s the String to send to the client + * + * @exception IOException if an input or output exception occurred + */ + public void print(String s) throws IOException { + if (s == null) { + s = "null"; + } + int len = s.length(); + byte[] buffer = new byte[len]; + + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + + // + // XXX NOTE: This is clearly incorrect for many strings, + // but is the only consistent approach within the current + // servlet framework. It must suffice until servlet output + // streams properly encode their output. + // + if ((c & 0xff00) != 0) { // high order byte must be zero + String errMsg = lStrings.getString("err.not_iso8859_1"); + Object[] errArgs = new Object[1]; + errArgs[0] = Character.valueOf(c); + errMsg = MessageFormat.format(errMsg, errArgs); + throw new CharConversionException(errMsg); + } + buffer[i] = (byte) (c & 0xFF); + } + write(buffer); + } + + /** + * Writes a boolean value to the client, with no carriage return-line feed (CRLF) character at the end. + * + * @param b the boolean value to send to the client + * + * @exception IOException if an input or output exception occurred + */ + public void print(boolean b) throws IOException { + String msg; + if (b) { + msg = lStrings.getString("value.true"); + } else { + msg = lStrings.getString("value.false"); + } + print(msg); + } + + /** + * Writes a character to the client, with no carriage return-line feed (CRLF) at the end. + * + * @param c the character to send to the client + * + * @exception IOException if an input or output exception occurred + */ + public void print(char c) throws IOException { + print(String.valueOf(c)); + } + + /** + * Writes an int to the client, with no carriage return-line feed (CRLF) at the end. + * + * @param i the int to send to the client + * + * @exception IOException if an input or output exception occurred + */ + public void print(int i) throws IOException { + print(String.valueOf(i)); + } + + /** + * Writes a long value to the client, with no carriage return-line feed (CRLF) at the end. + * + * @param l the long value to send to the client + * + * @exception IOException if an input or output exception occurred + */ + public void print(long l) throws IOException { + print(String.valueOf(l)); + } + + /** + * Writes a float value to the client, with no carriage return-line feed (CRLF) at the end. + * + * @param f the float value to send to the client + * + * @exception IOException if an input or output exception occurred + */ + public void print(float f) throws IOException { + print(String.valueOf(f)); + } + + /** + * Writes a double value to the client, with no carriage return-line feed (CRLF) at the end. + * + * @param d the double value to send to the client + * + * @exception IOException if an input or output exception occurred + */ + public void print(double d) throws IOException { + print(String.valueOf(d)); + } + + /** + * Writes a carriage return-line feed (CRLF) to the client. + * + * @exception IOException if an input or output exception occurred + */ + public void println() throws IOException { + print("\r\n"); + } + + /** + * Writes a String to the client, followed by a carriage return-line feed (CRLF). + * + * @param s the String to write to the client + * + * @exception IOException if an input or output exception occurred + */ + public void println(String s) throws IOException { + StringBuilder sb = new StringBuilder(); + sb.append(s); + sb.append("\r\n"); + print(sb.toString()); + } + + /** + * Writes a boolean value to the client, followed by a carriage return-line feed (CRLF). + * + * @param b the boolean value to write to the client + * + * @exception IOException if an input or output exception occurred + */ + public void println(boolean b) throws IOException { + StringBuilder sb = new StringBuilder(); + if (b) { + sb.append(lStrings.getString("value.true")); + } else { + sb.append(lStrings.getString("value.false")); + } + sb.append("\r\n"); + print(sb.toString()); + } + + /** + * Writes a character to the client, followed by a carriage return-line feed (CRLF). + * + * @param c the character to write to the client + * + * @exception IOException if an input or output exception occurred + */ + public void println(char c) throws IOException { + println(String.valueOf(c)); + } + + /** + * Writes an int to the client, followed by a carriage return-line feed (CRLF) character. + * + * @param i the int to write to the client + * + * @exception IOException if an input or output exception occurred + */ + public void println(int i) throws IOException { + println(String.valueOf(i)); + } + + /** + * Writes a long value to the client, followed by a carriage return-line feed (CRLF). + * + * @param l the long value to write to the client + * + * @exception IOException if an input or output exception occurred + */ + public void println(long l) throws IOException { + println(String.valueOf(l)); + } + + /** + * Writes a float value to the client, followed by a carriage return-line feed (CRLF). + * + * @param f the float value to write to the client + * + * @exception IOException if an input or output exception occurred + */ + public void println(float f) throws IOException { + println(String.valueOf(f)); + } + + /** + * Writes a double value to the client, followed by a carriage return-line feed (CRLF). + * + * @param d the double value to write to the client + * + * @exception IOException if an input or output exception occurred + */ + public void println(double d) throws IOException { + println(String.valueOf(d)); + } + + /** + * Checks if a non-blocking write will succeed. If this returns false, it will cause a callback to + * {@link WriteListener#onWritePossible()} when the buffer has emptied. If this method returns false no + * further data must be written until the container calls {@link WriteListener#onWritePossible()}. + * + * @return true if data can be written, else false + * + * @since Servlet 3.1 + */ + public abstract boolean isReady(); + + /** + * Sets the {@link WriteListener} for this {@link ServletOutputStream} and thereby switches to non-blocking IO. It + * is only valid to switch to non-blocking IO within async processing or HTTP upgrade processing. + * + * @param listener The non-blocking IO write listener + * + * @throws IllegalStateException If this method is called if neither async nor HTTP upgrade is in progress or if the + * {@link WriteListener} has already been set + * @throws NullPointerException If listener is null + * + * @since Servlet 3.1 + */ + public abstract void setWriteListener(WriteListener listener); +} diff --git a/java/jakarta/servlet/ServletRegistration.java b/java/jakarta/servlet/ServletRegistration.java new file mode 100644 index 0000000..c9b5385 --- /dev/null +++ b/java/jakarta/servlet/ServletRegistration.java @@ -0,0 +1,99 @@ +/* + * 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 jakarta.servlet; + +import java.util.Collection; +import java.util.Set; + +/** + * Interface through which a Servlet may be further configured. + * + * @since Servlet 3.0 + */ +public interface ServletRegistration extends Registration { + + /** + * Adds a servlet mapping with the given URL patterns for the Servlet represented by this ServletRegistration. If + * any of the specified URL patterns are already mapped to a different Servlet, no updates will be performed. If + * this method is called multiple times, each successive call adds to the effects of the former. The returned set is + * not backed by the ServletRegistration object, so changes in the returned set are not reflected in the + * ServletRegistration object, and vice-versa. + * + * @param urlPatterns The URL patterns that this Servlet should be mapped to + * + * @return the (possibly empty) Set of URL patterns that are already mapped to a different Servlet + * + * @throws IllegalArgumentException if urlPattern is null or empty + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + Set addMapping(String... urlPatterns); + + /** + * Gets the currently available mappings of the Servlet represented by this ServletRegistration. If permitted, any + * changes to the returned Collection must not affect this ServletRegistration. + * + * @return a (possibly empty) Collection of the currently available mappings of the Servlet represented by this + * ServletRegistration + */ + Collection getMappings(); + + /** + * Obtain the name of the user / group under which the Servlet has been configured to run. + * + * @return the name of the user / group or {@code null} if none has been specified + */ + String getRunAsRole(); + + /** + * Interface through which a Servlet registered via one of the addServlet methods on ServletContext may be further + * configured. + */ + interface Dynamic extends ServletRegistration, Registration.Dynamic { + + /** + * Set the loadOnStartup order for the Servlet + * + * @param loadOnStartup The position in the order the Servlet should be started (higher numbers are started + * after lower numbers) + */ + void setLoadOnStartup(int loadOnStartup); + + /** + * Add security constraints to this Servlet. + * + * @param constraint new security constraints for this Servlet + * + * @return urls currently mapped to this registration that are already present in web.xml + */ + Set setServletSecurity(ServletSecurityElement constraint); + + /** + * Set the multi-part configuration for the associated Servlet. To clear the multi-part configuration specify + * null as the new value. + * + * @param multipartConfig The configuration to associate with the Servlet + */ + void setMultipartConfig(MultipartConfigElement multipartConfig); + + /** + * Set the name of the user / group under which the Servlet should be configured to run. + * + * @param roleName name of the user / group or {@code null} if none + */ + void setRunAsRole(String roleName); + } +} diff --git a/java/jakarta/servlet/ServletRequest.java b/java/jakarta/servlet/ServletRequest.java new file mode 100644 index 0000000..7d12677 --- /dev/null +++ b/java/jakarta/servlet/ServletRequest.java @@ -0,0 +1,472 @@ +/* + * 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 jakarta.servlet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; + +/** + * Defines an object to provide client request information to a servlet. The servlet container creates a + * ServletRequest object and passes it as an argument to the servlet's service method. + *

+ * A ServletRequest object provides data including parameter name and values, attributes, and an input + * stream. Interfaces that extend ServletRequest can provide additional protocol-specific data (for + * example, HTTP data is provided by {@link jakarta.servlet.http.HttpServletRequest}. + * + * @see jakarta.servlet.http.HttpServletRequest + */ +public interface ServletRequest { + + /** + * Returns the value of the named attribute as an Object, or null if no attribute of the + * given name exists. + *

+ * Attributes can be set two ways. The servlet container may set attributes to make available custom information + * about a request. For example, for requests made using HTTPS, the attribute + * jakarta.servlet.request.X509Certificate can be used to retrieve information on the certificate of + * the client. Attributes can also be set programmatically using {@link ServletRequest#setAttribute}. This allows + * information to be embedded into a request before a {@link RequestDispatcher} call. + *

+ * Attribute names should follow the same conventions as package names. Names beginning with jakarta.* + * are reserved for use by the Jakarta EE platform. + * + * @param name a String specifying the name of the attribute + * + * @return an Object containing the value of the attribute, or null if the attribute does + * not exist + */ + Object getAttribute(String name); + + /** + * Returns an Enumeration containing the names of the attributes available to this request. This method + * returns an empty Enumeration if the request has no attributes available to it. + * + * @return an Enumeration of strings containing the names of the request's attributes + */ + Enumeration getAttributeNames(); + + /** + * Returns the name of the character encoding used in the body of this request. This method returns + * null if the no character encoding has been specified. The following priority order is used to + * determine the specified encoding: + *

    + *
  1. per request
  2. + *
  3. web application default via the deployment descriptor or + * {@link ServletContext#setRequestCharacterEncoding(String)}
  4. + *
  5. container default via container specific configuration
  6. + *
+ * + * @return a String containing the name of the character encoding, or null if the request + * does not specify a character encoding + */ + String getCharacterEncoding(); + + /** + * Overrides the name of the character encoding used in the body of this request. This method must be called prior + * to reading request parameters or reading input using getReader(). + * + * @param encoding a {@code String} containing the name of the character encoding + * + * @throws UnsupportedEncodingException if this is not a valid encoding + */ + void setCharacterEncoding(String encoding) throws UnsupportedEncodingException; + + /** + * Returns the length, in bytes, of the request body and made available by the input stream, or -1 if the length is + * not known. For HTTP servlets, same as the value of the CGI variable CONTENT_LENGTH. + * + * @return an integer containing the length of the request body or -1 if the length is not known or is greater than + * {@link Integer#MAX_VALUE} + */ + int getContentLength(); + + /** + * Returns the length, in bytes, of the request body and made available by the input stream, or -1 if the length is + * not known. For HTTP servlets, same as the value of the CGI variable CONTENT_LENGTH. + * + * @return a long integer containing the length of the request body or -1 if the length is not known + * + * @since Servlet 3.1 + */ + long getContentLengthLong(); + + /** + * Returns the MIME type of the body of the request, or null if the type is not known. For HTTP + * servlets, same as the value of the CGI variable CONTENT_TYPE. + * + * @return a String containing the name of the MIME type of the request, or null if the type is not + * known + */ + String getContentType(); + + /** + * Retrieves the body of the request as binary data using a {@link ServletInputStream}. Either this method or + * {@link #getReader} may be called to read the body, not both. + * + * @return a {@link ServletInputStream} object containing the body of the request + * + * @exception IllegalStateException if the {@link #getReader} method has already been called for this request + * @exception IOException if an input or output exception occurred + */ + ServletInputStream getInputStream() throws IOException; + + /** + * Returns the value of a request parameter as a String, or null if the parameter does not + * exist. Request parameters are extra information sent with the request. For HTTP servlets, parameters are + * contained in the query string or posted form data. + *

+ * You should only use this method when you are sure the parameter has only one value. If the parameter might have + * more than one value, use {@link #getParameterValues}. + *

+ * If you use this method with a multivalued parameter, the value returned is equal to the first value in the array + * returned by getParameterValues. + *

+ * If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the + * body directly via {@link #getInputStream} or {@link #getReader} can interfere with the execution of this method. + * + * @param name a String specifying the name of the parameter + * + * @return a String representing the single value of the parameter + * + * @see #getParameterValues + */ + String getParameter(String name); + + /** + * Returns an Enumeration of String objects containing the names of the parameters + * contained in this request. If the request has no parameters, the method returns an empty + * Enumeration. + * + * @return an Enumeration of String objects, each String containing the name + * of a request parameter; or an empty Enumeration if the request has no parameters + */ + Enumeration getParameterNames(); + + /** + * Returns an array of String objects containing all of the values the given request parameter has, or + * null if the parameter does not exist. + *

+ * If the parameter has a single value, the array has a length of 1. + * + * @param name a String containing the name of the parameter whose value is requested + * + * @return an array of String objects containing the parameter's values + * + * @see #getParameter + */ + String[] getParameterValues(String name); + + /** + * Returns a java.util.Map of the parameters of this request. Request parameters are extra information sent with the + * request. For HTTP servlets, parameters are contained in the query string or posted form data. + * + * @return an immutable java.util.Map containing parameter names as keys and parameter values as map values. The + * keys in the parameter map are of type String. The values in the parameter map are of type String + * array. + */ + Map getParameterMap(); + + /** + * Returns the name and version of the protocol the request uses in the form + * protocol/majorVersion.minorVersion, for example, HTTP/1.1. For HTTP servlets, the value returned is the + * same as the value of the CGI variable SERVER_PROTOCOL. + * + * @return a String containing the protocol name and version number + */ + String getProtocol(); + + /** + * Returns the name of the scheme used to make this request, for example, http, https, or + * ftp. Different schemes have different rules for constructing URLs, as noted in RFC 1738. + * + * @return a String containing the name of the scheme used to make this request + */ + String getScheme(); + + /** + * Returns the host name of the server to which the request was sent. It is the value of the part before ":" in the + * Host header value, if any, or the resolved server name, or the server IP address. + * + * @return a String containing the name of the server + */ + String getServerName(); + + /** + * Returns the port number to which the request was sent. It is the value of the part after ":" in the + * Host header value, if any, or the server port where the client connection was accepted on. + * + * @return an integer specifying the port number + */ + int getServerPort(); + + /** + * Retrieves the body of the request as character data using a BufferedReader. The reader translates + * the character data according to the character encoding used on the body. Either this method or + * {@link #getInputStream} may be called to read the body, not both. + * + * @return a BufferedReader containing the body of the request + * + * @exception java.io.UnsupportedEncodingException if the character set encoding used is not supported and the text + * cannot be decoded + * @exception IllegalStateException if {@link #getInputStream} method has been called on this request + * @exception IOException if an input or output exception occurred + * + * @see #getInputStream + */ + BufferedReader getReader() throws IOException; + + /** + * Returns the Internet Protocol (IP) address of the client or last proxy that sent the request. For HTTP servlets, + * same as the value of the CGI variable REMOTE_ADDR. + * + * @return a String containing the IP address of the client that sent the request + */ + String getRemoteAddr(); + + /** + * Returns the fully qualified name of the client or the last proxy that sent the request. If the engine cannot or + * chooses not to resolve the hostname (to improve performance), this method returns the dotted-string form of the + * IP address. For HTTP servlets, same as the value of the CGI variable REMOTE_HOST. + * + * @return a String containing the fully qualified name of the client + */ + String getRemoteHost(); + + /** + * Stores an attribute in this request. Attributes are reset between requests. This method is most often used in + * conjunction with {@link RequestDispatcher}. + *

+ * Attribute names should follow the same conventions as package names. Names beginning with jakarta.* + * are reserved for use by the Jakarta EE platform. + *

+ * If the object passed in is null, the effect is the same as calling {@link #removeAttribute}.
+ * It is warned that when the request is dispatched from the servlet resides in a different web application by + * RequestDispatcher, the object set by this method may not be correctly retrieved in the caller + * servlet. + * + * @param name a String specifying the name of the attribute + * @param o the Object to be stored + */ + void setAttribute(String name, Object o); + + /** + * Removes an attribute from this request. This method is not generally needed as attributes only persist as long as + * the request is being handled. + *

+ * Attribute names should follow the same conventions as package names. Names beginning with jakarta.* + * are reserved for use by the Jakarta EE platform. + * + * @param name a String specifying the name of the attribute to remove + */ + void removeAttribute(String name); + + /** + * Returns the preferred Locale that the client will accept content in, based on the Accept-Language + * header. If the client request doesn't provide an Accept-Language header, this method returns the default locale + * for the server. + * + * @return the preferred Locale for the client + */ + Locale getLocale(); + + /** + * Returns an Enumeration of Locale objects indicating, in decreasing order starting with + * the preferred locale, the locales that are acceptable to the client based on the Accept-Language header. If the + * client request doesn't provide an Accept-Language header, this method returns an Enumeration + * containing one Locale, the default locale for the server. + * + * @return an Enumeration of preferred Locale objects for the client + */ + Enumeration getLocales(); + + /** + * Returns a boolean indicating whether this request was made using a secure channel, such as HTTPS. + * + * @return a boolean indicating if the request was made using a secure channel + */ + boolean isSecure(); + + /** + * Returns a {@link RequestDispatcher} object that acts as a wrapper for the resource located at the given path. A + * RequestDispatcher object can be used to forward a request to the resource or to include the resource + * in a response. The resource can be dynamic or static. + *

+ * The pathname specified may be relative, although it cannot extend outside the current servlet context. If the + * path begins with a "/" it is interpreted as relative to the current context root. This method returns + * null if the servlet container cannot return a RequestDispatcher. + *

+ * The difference between this method and {@link ServletContext#getRequestDispatcher} is that this method can take a + * relative path. + * + * @param path a String specifying the pathname to the resource. If it is relative, it must be relative + * against the current servlet. + * + * @return a RequestDispatcher object that acts as a wrapper for the resource at the specified path, or + * null if the servlet container cannot return a RequestDispatcher + * + * @see RequestDispatcher + * @see ServletContext#getRequestDispatcher + */ + RequestDispatcher getRequestDispatcher(String path); + + /** + * Returns the Internet Protocol (IP) source port of the client or last proxy that sent the request. + * + * @return an integer specifying the port number + * + * @since Servlet 2.4 + */ + int getRemotePort(); + + /** + * Returns the host name of the Internet Protocol (IP) interface on which the request was received. + * + * @return a String containing the host name of the IP on which the request was received. + * + * @since Servlet 2.4 + */ + String getLocalName(); + + /** + * Returns the Internet Protocol (IP) address of the interface on which the request was received. + * + * @return a String containing the IP address on which the request was received. + * + * @since Servlet 2.4 + */ + String getLocalAddr(); + + /** + * Returns the Internet Protocol (IP) port number of the interface on which the request was received. + * + * @return an integer specifying the port number + * + * @since Servlet 2.4 + */ + int getLocalPort(); + + /** + * @return TODO + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + ServletContext getServletContext(); + + /** + * @return TODO + * + * @throws IllegalStateException If async is not supported for this request + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + AsyncContext startAsync() throws IllegalStateException; + + /** + * @param servletRequest The ServletRequest with which to initialise the asynchronous context + * @param servletResponse The ServletResponse with which to initialise the asynchronous context + * + * @return TODO + * + * @throws IllegalStateException If async is not supported for this request + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException; + + /** + * @return TODO + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + boolean isAsyncStarted(); + + /** + * @return TODO + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + boolean isAsyncSupported(); + + /** + * Get the current AsyncContext. + * + * @return The current AsyncContext + * + * @throws IllegalStateException if the request is not in asynchronous mode (i.e. @link #isAsyncStarted() is + * {@code false}) + * + * @since Servlet 3.0 + */ + AsyncContext getAsyncContext(); + + /** + * @return TODO + * + * @since Servlet 3.0 TODO SERVLET3 - Add comments + */ + DispatcherType getDispatcherType(); + + /** + * Obtain a unique (within the lifetime of the Servlet container) identifier string for this request. + *

+ * There is no defined format for this string. The format is implementation dependent. + * + * @return A unique identifier for the request + * + * @since Servlet 6.0 + */ + String getRequestId(); + + /** + * Obtain the request identifier for this request as defined by the protocol in use. Note that some protocols do not + * define such an identifier. + *

+ * Examples of protocol provided request identifiers include: + *

+ *
HTTP 1.x
+ *
None, so the empty string should be returned
+ *
HTTP 2
+ *
The stream identifier
+ *
HTTP 3
+ *
The stream identifier
+ *
AJP
+ *
None, so the empty string should be returned
+ *
+ * + * @return The request identifier if one is defined, otherwise an empty string + * + * @since Servlet 6.0 + */ + String getProtocolRequestId(); + + /** + * Obtain details of the network connection to the Servlet container that is being used by this request. The + * information presented may differ from information presented elsewhere in the Servlet API as raw information is + * presented without adjustments for, example, use of reverse proxies that may be applied elsewhere in the Servlet + * API. + * + * @return The network connection details. + * + * @since Servlet 6.0 + */ + ServletConnection getServletConnection(); +} diff --git a/java/jakarta/servlet/ServletRequestAttributeEvent.java b/java/jakarta/servlet/ServletRequestAttributeEvent.java new file mode 100644 index 0000000..d86563a --- /dev/null +++ b/java/jakarta/servlet/ServletRequestAttributeEvent.java @@ -0,0 +1,73 @@ +/* + * 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 jakarta.servlet; + +/** + * This is the event class for notifications of changes to the attributes of the servlet request in an application. + * + * @see ServletRequestAttributeListener + * + * @since Servlet 2.4 + */ +public class ServletRequestAttributeEvent extends ServletRequestEvent { + private static final long serialVersionUID = 1L; + + /** + * Attribute name. + */ + private final String name; + + /** + * Attribute value. + */ + private final Object value; + + /** + * Construct a ServletRequestAttributeEvent giving the servlet context of this web application, the ServletRequest + * whose attributes are changing and the name and value of the attribute. + * + * @param sc the ServletContext that is sending the event. + * @param request the ServletRequest that is sending the event. + * @param name the name of the request attribute. + * @param value the value of the request attribute. + */ + public ServletRequestAttributeEvent(ServletContext sc, ServletRequest request, String name, Object value) { + super(sc, request); + this.name = name; + this.value = value; + } + + /** + * Return the name of the attribute that changed on the ServletRequest. + * + * @return the name of the changed request attribute + */ + public String getName() { + return this.name; + } + + /** + * Returns the value of the attribute that has been added, removed or replaced. If the attribute was added, this is + * the value of the attribute. If the attribute was removed, this is the value of the removed attribute. If the + * attribute was replaced, this is the old value of the attribute. + * + * @return the value of the changed request attribute + */ + public Object getValue() { + return this.value; + } +} diff --git a/java/jakarta/servlet/ServletRequestAttributeListener.java b/java/jakarta/servlet/ServletRequestAttributeListener.java new file mode 100644 index 0000000..2a711f9 --- /dev/null +++ b/java/jakarta/servlet/ServletRequestAttributeListener.java @@ -0,0 +1,58 @@ +/* + * 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 jakarta.servlet; + +import java.util.EventListener; + +/** + * A ServletRequestAttributeListener can be implemented by the developer interested in being notified of request + * attribute changes. Notifications will be generated while the request is within the scope of the web application in + * which the listener is registered. A request is defined as coming into scope when it is about to enter the first + * servlet or filter in each web application, as going out of scope when it exits the last servlet or the first filter + * in the chain. + * + * @since Servlet 2.4 + */ +public interface ServletRequestAttributeListener extends EventListener { + /** + * Notification that a new attribute was added to the servlet request. Called after the attribute is added. The + * default implementation is a NO-OP. + * + * @param srae Information about the new request attribute + */ + default void attributeAdded(ServletRequestAttributeEvent srae) { + } + + /** + * Notification that an existing attribute has been removed from the servlet request. Called after the attribute is + * removed. The default implementation is a NO-OP. + * + * @param srae Information about the removed request attribute + */ + default void attributeRemoved(ServletRequestAttributeEvent srae) { + } + + /** + * Notification that an attribute was replaced on the servlet request. Called after the attribute is replaced. The + * default implementation is a NO-OP. + * + * @param srae Information about the replaced request attribute + */ + default void attributeReplaced(ServletRequestAttributeEvent srae) { + } +} + diff --git a/java/jakarta/servlet/ServletRequestEvent.java b/java/jakarta/servlet/ServletRequestEvent.java new file mode 100644 index 0000000..6a24875 --- /dev/null +++ b/java/jakarta/servlet/ServletRequestEvent.java @@ -0,0 +1,60 @@ +/* + * 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 jakarta.servlet; + +/** + * Events of this kind indicate lifecycle events for a ServletRequest. The source of the event is the ServletContext of + * this web application. + * + * @see ServletRequestListener + * + * @since Servlet 2.4 + */ +public class ServletRequestEvent extends java.util.EventObject { + private static final long serialVersionUID = 1L; + + private final transient ServletRequest request; + + /** + * Construct a ServletRequestEvent for the given ServletContext and ServletRequest. + * + * @param sc the ServletContext of the web application. + * @param request the ServletRequest that is sending the event. + */ + public ServletRequestEvent(ServletContext sc, ServletRequest request) { + super(sc); + this.request = request; + } + + /** + * Get the associated ServletRequest. + * + * @return the ServletRequest that is changing. + */ + public ServletRequest getServletRequest() { + return this.request; + } + + /** + * Get the associated ServletContext. + * + * @return the ServletContext that is changing. + */ + public ServletContext getServletContext() { + return (ServletContext) super.getSource(); + } +} diff --git a/java/jakarta/servlet/ServletRequestListener.java b/java/jakarta/servlet/ServletRequestListener.java new file mode 100644 index 0000000..59d2c4e --- /dev/null +++ b/java/jakarta/servlet/ServletRequestListener.java @@ -0,0 +1,46 @@ +/* + * 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 jakarta.servlet; + +import java.util.EventListener; + +/** + * A ServletRequestListener can be implemented by the developer interested in being notified of requests coming in and + * out of scope in a web component. A request is defined as coming into scope when it is about to enter the first + * servlet or filter in each web application, as going out of scope when it exits the last servlet or the first filter + * in the chain. + * + * @since Servlet 2.4 + */ +public interface ServletRequestListener extends EventListener { + + /** + * The request is about to go out of scope of the web application. The default implementation is a NO-OP. + * + * @param sre Information about the request + */ + default void requestDestroyed(ServletRequestEvent sre) { + } + + /** + * The request is about to come into scope of the web application. The default implementation is a NO-OP. + * + * @param sre Information about the request + */ + default void requestInitialized(ServletRequestEvent sre) { + } +} diff --git a/java/jakarta/servlet/ServletRequestWrapper.java b/java/jakarta/servlet/ServletRequestWrapper.java new file mode 100644 index 0000000..9268905 --- /dev/null +++ b/java/jakarta/servlet/ServletRequestWrapper.java @@ -0,0 +1,476 @@ +/* + * 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 jakarta.servlet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * Provides a convenient implementation of the ServletRequest interface that can be subclassed by developers wishing to + * adapt the request to a Servlet. This class implements the Wrapper or Decorator pattern. Methods default to calling + * through to the wrapped request object. + * + * @see jakarta.servlet.ServletRequest + * + * @since Servlet 2.3 + */ +public class ServletRequestWrapper implements ServletRequest { + private static final String LSTRING_FILE = "jakarta.servlet.LocalStrings"; + private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + private ServletRequest request; + + /** + * Creates a ServletRequest adaptor wrapping the given request object. + * + * @param request The request to wrap + * + * @throws IllegalArgumentException if the request is null + */ + public ServletRequestWrapper(ServletRequest request) { + if (request == null) { + throw new IllegalArgumentException(lStrings.getString("wrapper.nullRequest")); + } + this.request = request; + } + + /** + * Get the wrapped request. + * + * @return the wrapped request object + */ + public ServletRequest getRequest() { + return this.request; + } + + /** + * Sets the request object being wrapped. + * + * @param request The new wrapped request. + * + * @throws IllegalArgumentException if the request is null. + */ + public void setRequest(ServletRequest request) { + if (request == null) { + throw new IllegalArgumentException(lStrings.getString("wrapper.nullRequest")); + } + this.request = request; + } + + /** + * The default behavior of this method is to call getAttribute(String name) on the wrapped request object. + */ + @Override + public Object getAttribute(String name) { + return this.request.getAttribute(name); + } + + /** + * The default behavior of this method is to return getAttributeNames() on the wrapped request object. + */ + @Override + public Enumeration getAttributeNames() { + return this.request.getAttributeNames(); + } + + /** + * The default behavior of this method is to return getCharacterEncoding() on the wrapped request object. + */ + @Override + public String getCharacterEncoding() { + return this.request.getCharacterEncoding(); + } + + /** + * The default behavior of this method is to set the character encoding on the wrapped request object. + */ + @Override + public void setCharacterEncoding(String enc) throws java.io.UnsupportedEncodingException { + this.request.setCharacterEncoding(enc); + } + + /** + * The default behavior of this method is to return getContentLength() on the wrapped request object. + */ + @Override + public int getContentLength() { + return this.request.getContentLength(); + } + + /** + * The default behavior of this method is to return getContentLengthLong() on the wrapped request object. + * + * @since Servlet 3.1 + */ + @Override + public long getContentLengthLong() { + return this.request.getContentLengthLong(); + } + + /** + * The default behavior of this method is to return getContentType() on the wrapped request object. + */ + @Override + public String getContentType() { + return this.request.getContentType(); + } + + /** + * The default behavior of this method is to return getInputStream() on the wrapped request object. + */ + @Override + public ServletInputStream getInputStream() throws IOException { + return this.request.getInputStream(); + } + + /** + * The default behavior of this method is to return getParameter(String name) on the wrapped request object. + */ + @Override + public String getParameter(String name) { + return this.request.getParameter(name); + } + + /** + * The default behavior of this method is to return getParameterMap() on the wrapped request object. + */ + @Override + public Map getParameterMap() { + return this.request.getParameterMap(); + } + + /** + * The default behavior of this method is to return getParameterNames() on the wrapped request object. + */ + @Override + public Enumeration getParameterNames() { + return this.request.getParameterNames(); + } + + /** + * The default behavior of this method is to return getParameterValues(String name) on the wrapped request object. + */ + @Override + public String[] getParameterValues(String name) { + return this.request.getParameterValues(name); + } + + /** + * The default behavior of this method is to return getProtocol() on the wrapped request object. + */ + @Override + public String getProtocol() { + return this.request.getProtocol(); + } + + /** + * The default behavior of this method is to return getScheme() on the wrapped request object. + */ + @Override + public String getScheme() { + return this.request.getScheme(); + } + + /** + * The default behavior of this method is to return getServerName() on the wrapped request object. + */ + @Override + public String getServerName() { + return this.request.getServerName(); + } + + /** + * The default behavior of this method is to return getServerPort() on the wrapped request object. + */ + @Override + public int getServerPort() { + return this.request.getServerPort(); + } + + /** + * The default behavior of this method is to return getReader() on the wrapped request object. + */ + @Override + public BufferedReader getReader() throws IOException { + return this.request.getReader(); + } + + /** + * The default behavior of this method is to return getRemoteAddr() on the wrapped request object. + */ + @Override + public String getRemoteAddr() { + return this.request.getRemoteAddr(); + } + + /** + * The default behavior of this method is to return getRemoteHost() on the wrapped request object. + */ + @Override + public String getRemoteHost() { + return this.request.getRemoteHost(); + } + + /** + * The default behavior of this method is to return setAttribute(String name, Object o) on the wrapped request + * object. + */ + @Override + public void setAttribute(String name, Object o) { + this.request.setAttribute(name, o); + } + + /** + * The default behavior of this method is to call removeAttribute(String name) on the wrapped request object. + */ + @Override + public void removeAttribute(String name) { + this.request.removeAttribute(name); + } + + /** + * The default behavior of this method is to return getLocale() on the wrapped request object. + */ + @Override + public Locale getLocale() { + return this.request.getLocale(); + } + + /** + * The default behavior of this method is to return getLocales() on the wrapped request object. + */ + @Override + public Enumeration getLocales() { + return this.request.getLocales(); + } + + /** + * The default behavior of this method is to return isSecure() on the wrapped request object. + */ + @Override + public boolean isSecure() { + return this.request.isSecure(); + } + + /** + * The default behavior of this method is to return getRequestDispatcher(String path) on the wrapped request object. + */ + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return this.request.getRequestDispatcher(path); + } + + /** + * The default behavior of this method is to return getRemotePort() on the wrapped request object. + * + * @since Servlet 2.4 + */ + @Override + public int getRemotePort() { + return this.request.getRemotePort(); + } + + /** + * The default behavior of this method is to return getLocalName() on the wrapped request object. + * + * @since Servlet 2.4 + */ + @Override + public String getLocalName() { + return this.request.getLocalName(); + } + + /** + * The default behavior of this method is to return getLocalAddr() on the wrapped request object. + * + * @since Servlet 2.4 + */ + @Override + public String getLocalAddr() { + return this.request.getLocalAddr(); + } + + /** + * The default behavior of this method is to return getLocalPort() on the wrapped request object. + * + * @since Servlet 2.4 + */ + @Override + public int getLocalPort() { + return this.request.getLocalPort(); + } + + /** + * The default behavior of this method is to return getServletContext() on the wrapped request object. + * + * @since Servlet 3.0 + */ + @Override + public ServletContext getServletContext() { + return request.getServletContext(); + } + + /** + * The default behavior of this method is to return startAsync() on the wrapped request object. + * + * @throws IllegalStateException If asynchronous processing is not supported for this request or if the request is + * already in asynchronous mode + * + * @since Servlet 3.0 + */ + @Override + public AsyncContext startAsync() throws IllegalStateException { + return request.startAsync(); + } + + /** + * The default behavior of this method is to return startAsync(Runnable) on the wrapped request object. + * + * @param servletRequest The ServletRequest with which to initialise the asynchronous context + * @param servletResponse The ServletResponse with which to initialise the asynchronous context + * + * @throws IllegalStateException If asynchronous processing is not supported for this request or if the request is + * already in asynchronous mode + * + * @since Servlet 3.0 + */ + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + return request.startAsync(servletRequest, servletResponse); + } + + /** + * The default behavior of this method is to return isAsyncStarted() on the wrapped request object. + * + * @since Servlet 3.0 + */ + @Override + public boolean isAsyncStarted() { + return request.isAsyncStarted(); + } + + /** + * The default behavior of this method is to return isAsyncSupported() on the wrapped request object. + * + * @since Servlet 3.0 + */ + @Override + public boolean isAsyncSupported() { + return request.isAsyncSupported(); + } + + /** + * The default behavior of this method is to return getAsyncContext() on the wrapped request object. + * + * @since Servlet 3.0 + */ + @Override + public AsyncContext getAsyncContext() { + return request.getAsyncContext(); + } + + /** + * TODO SERVLET3 - Add comments + * + * @param wrapped The request to compare to the wrapped request + * + * @return true if the request wrapped by this wrapper (or series of wrappers) is the same as the + * supplied request, otherwise false + * + * @since Servlet 3.0 + */ + public boolean isWrapperFor(ServletRequest wrapped) { + if (request == wrapped) { + return true; + } + if (request instanceof ServletRequestWrapper) { + return ((ServletRequestWrapper) request).isWrapperFor(wrapped); + } + return false; + } + + /** + * TODO SERVLET3 - Add comments + * + * @param wrappedType The class to compare to the class of the wrapped request + * + * @return true if the request wrapped by this wrapper (or series of wrappers) is the same type as the + * supplied type, otherwise false + * + * @since Servlet 3.0 + */ + public boolean isWrapperFor(Class wrappedType) { + if (wrappedType.isAssignableFrom(request.getClass())) { + return true; + } + if (request instanceof ServletRequestWrapper) { + return ((ServletRequestWrapper) request).isWrapperFor(wrappedType); + } + return false; + } + + /** + * The default behavior of this method is to call getDispatcherType() on the wrapped request object. + * + * @since Servlet 3.0 + */ + @Override + public DispatcherType getDispatcherType() { + return this.request.getDispatcherType(); + } + + /** + * Gets the request ID for the wrapped request. + * + * @return the request ID for the wrapped request + * + * @since Servlet 6.0 + */ + @Override + public String getRequestId() { + return request.getRequestId(); + } + + /** + * Gets the protocol defined request ID, if any, for the wrapped request. + * + * @return the protocol defined request ID, if any, for the wrapped request + * + * @since Servlet 6.0 + */ + @Override + public String getProtocolRequestId() { + return request.getProtocolRequestId(); + } + + /** + * Gets the connection information for the wrapped request. + * + * @return the connection information for the wrapped request + * + * @since Servlet 6.0 + */ + @Override + public ServletConnection getServletConnection() { + return request.getServletConnection(); + } +} diff --git a/java/jakarta/servlet/ServletResponse.java b/java/jakarta/servlet/ServletResponse.java new file mode 100644 index 0000000..57c82cd --- /dev/null +++ b/java/jakarta/servlet/ServletResponse.java @@ -0,0 +1,320 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Locale; + +/** + * Defines an object to assist a servlet in sending a response to the client. The servlet container creates a + * ServletResponse object and passes it as an argument to the servlet's service method. + *

+ * To send binary data in a MIME body response, use the {@link ServletOutputStream} returned by + * {@link #getOutputStream}. To send character data, use the PrintWriter object returned by + * {@link #getWriter}. To mix binary and text data, for example, to create a multipart response, use a + * ServletOutputStream and manage the character sections manually. + *

+ * The charset for the MIME body response can be specified explicitly or implicitly. The priority order for specifying + * the response body is: + *

    + *
  1. explicitly per request using {@link #setCharacterEncoding} and {@link #setContentType}
  2. + *
  3. implicitly per request using {@link #setLocale}
  4. + *
  5. per web application via the deployment descriptor or + * {@link ServletContext#setRequestCharacterEncoding(String)}
  6. + *
  7. container default via vendor specific configuration
  8. + *
  9. ISO-8859-1
  10. + *
+ * The setCharacterEncoding, setContentType, or setLocale method must be called + * before getWriter and before committing the response for the character encoding to be used. + *

+ * See the Internet RFCs such as RFC 2045 for more information on + * MIME. Protocols such as SMTP and HTTP define profiles of MIME, and those standards are still evolving. + * + * @see ServletOutputStream + */ +public interface ServletResponse { + + /** + * Returns the name of the character encoding (MIME charset) used for the body sent in this response. The charset + * for the MIME body response can be specified explicitly or implicitly. The priority order for specifying the + * response body is: + *

    + *
  1. explicitly per request using {@link #setCharacterEncoding} and {@link #setContentType}
  2. + *
  3. implicitly per request using {@link #setLocale}
  4. + *
  5. per web application via the deployment descriptor or + * {@link ServletContext#setRequestCharacterEncoding(String)}
  6. + *
  7. container default via vendor specific configuration
  8. + *
  9. ISO-8859-1
  10. + *
+ * Calls made to {@link #setCharacterEncoding}, {@link #setContentType} or {@link #setLocale} after + * getWriter has been called or after the response has been committed have no effect on the character + * encoding. If no character encoding has been specified, ISO-8859-1 is returned. + *

+ * See RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt) for more information about character encoding and MIME. + * + * @return a String specifying the name of the character encoding, for example, UTF-8 + */ + String getCharacterEncoding(); + + /** + * Returns the content type used for the MIME body sent in this response. The content type proper must have been + * specified using {@link #setContentType} before the response is committed. If no content type has been specified, + * this method returns null. If a content type has been specified and a character encoding has been explicitly or + * implicitly specified as described in {@link #getCharacterEncoding}, the charset parameter is included in the + * string returned. If no character encoding has been specified, the charset parameter is omitted. + * + * @return a String specifying the content type, for example, text/html; charset=UTF-8, or + * null + * + * @since Servlet 2.4 + */ + String getContentType(); + + /** + * Returns a {@link ServletOutputStream} suitable for writing binary data in the response. The servlet container + * does not encode the binary data. + *

+ * Calling flush() on the ServletOutputStream commits the response. Either this method or {@link #getWriter} may be + * called to write the body, not both. + * + * @return a {@link ServletOutputStream} for writing binary data + * + * @exception IllegalStateException if the getWriter method has been called on this response + * @exception IOException if an input or output exception occurred + * + * @see #getWriter + */ + ServletOutputStream getOutputStream() throws IOException; + + /** + * Returns a PrintWriter object that can send character text to the client. The + * PrintWriter uses the character encoding returned by {@link #getCharacterEncoding}. If the response's + * character encoding has not been specified as described in getCharacterEncoding (i.e., the method + * just returns the default value ISO-8859-1), getWriter updates it to + * ISO-8859-1. + *

+ * Calling flush() on the PrintWriter commits the response. + *

+ * Either this method or {@link #getOutputStream} may be called to write the body, not both. + * + * @return a PrintWriter object that can return character data to the client + * + * @exception java.io.UnsupportedEncodingException if the character encoding returned by + * getCharacterEncoding cannot be used + * @exception IllegalStateException if the getOutputStream method has already been + * called for this response object + * @exception IOException if an input or output exception occurred + * + * @see #getOutputStream + * @see #setCharacterEncoding + */ + PrintWriter getWriter() throws IOException; + + /** + * Sets the character encoding (MIME charset) of the response being sent to the client, for example, to UTF-8. If + * the character encoding has already been set by container default, ServletContext default, {@link #setContentType} + * or {@link #setLocale}, this method overrides it. Calling {@link #setContentType} with the String of + * text/html and calling this method with the String of UTF-8 is equivalent + * with calling setContentType with the String of text/html; charset=UTF-8. + *

+ * This method can be called repeatedly to change the character encoding. This method has no effect if it is called + * after getWriter has been called or after the response has been committed. + *

+ * Containers must communicate the character encoding used for the servlet response's writer to the client if the + * protocol provides a way for doing so. In the case of HTTP, the character encoding is communicated as part of the + * Content-Type header for text media types. Note that the character encoding cannot be communicated + * via HTTP headers if the servlet does not specify a content type; however, it is still used to encode text written + * via the servlet response's writer. + * + * @param charset a String specifying only the character set defined by IANA Character Sets + * (http://www.iana.org/assignments/character-sets) + * + * @see #setContentType #setLocale + * + * @since Servlet 2.4 + */ + void setCharacterEncoding(String charset); + + /** + * Sets the length of the content body in the response In HTTP servlets, this method sets the HTTP Content-Length + * header. + * + * @param len an integer specifying the length of the content being returned to the client; sets the Content-Length + * header + */ + void setContentLength(int len); + + /** + * Sets the length of the content body in the response In HTTP servlets, this method sets the HTTP Content-Length + * header. + * + * @param length an integer specifying the length of the content being returned to the client; sets the + * Content-Length header + * + * @since Servlet 3.1 + */ + void setContentLengthLong(long length); + + /** + * Sets the content type of the response being sent to the client, if the response has not been committed yet. The + * given content type may include a character encoding specification, for example, + * text/html;charset=UTF-8. The response's character encoding is only set from the given content type + * if this method is called before getWriter is called. + *

+ * This method may be called repeatedly to change content type and character encoding. This method has no effect if + * called after the response has been committed. It does not set the response's character encoding if it is called + * after getWriter has been called or after the response has been committed. + *

+ * Containers must communicate the content type and the character encoding used for the servlet response's writer to + * the client if the protocol provides a way for doing so. In the case of HTTP, the Content-Type header + * is used. + * + * @param type a String specifying the MIME type of the content + * + * @see #setLocale + * @see #setCharacterEncoding + * @see #getOutputStream + * @see #getWriter + */ + void setContentType(String type); + + /** + * Sets the preferred buffer size for the body of the response. The servlet container will use a buffer at least as + * large as the size requested. The actual buffer size used can be found using getBufferSize. + *

+ * A larger buffer allows more content to be written before anything is actually sent, thus providing the servlet + * with more time to set appropriate status codes and headers. A smaller buffer decreases server memory load and + * allows the client to start receiving data more quickly. + *

+ * This method must be called before any response body content is written; if content has been written or the + * response object has been committed, this method throws an IllegalStateException. + * + * @param size the preferred buffer size + * + * @exception IllegalStateException if this method is called after content has been written + * + * @see #getBufferSize + * @see #flushBuffer + * @see #isCommitted + * @see #reset + */ + void setBufferSize(int size); + + /** + * Returns the actual buffer size used for the response. If no buffering is used, this method returns 0. + * + * @return the actual buffer size used + * + * @see #setBufferSize + * @see #flushBuffer + * @see #isCommitted + * @see #reset + */ + int getBufferSize(); + + /** + * Forces any content in the buffer to be written to the client. A call to this method automatically commits the + * response, meaning the status code and headers will be written. + * + * @throws IOException if an I/O occurs during the flushing of the response + * + * @see #setBufferSize + * @see #getBufferSize + * @see #isCommitted + * @see #reset + */ + void flushBuffer() throws IOException; + + /** + * Clears the content of the underlying buffer in the response without clearing headers or status code. If the + * response has been committed, this method throws an IllegalStateException. + * + * @see #setBufferSize + * @see #getBufferSize + * @see #isCommitted + * @see #reset + * + * @since Servlet 2.3 + */ + void resetBuffer(); + + /** + * Returns a boolean indicating if the response has been committed. A committed response has already had its status + * code and headers written. + * + * @return a boolean indicating if the response has been committed + * + * @see #setBufferSize + * @see #getBufferSize + * @see #flushBuffer + * @see #reset + */ + boolean isCommitted(); + + /** + * Clears any data that exists in the buffer as well as the status code and headers. If the response has been + * committed, this method throws an IllegalStateException. + * + * @exception IllegalStateException if the response has already been committed + * + * @see #setBufferSize + * @see #getBufferSize + * @see #flushBuffer + * @see #isCommitted + */ + void reset(); + + /** + * Sets the locale of the response, if the response has not been committed yet. It also sets the response's + * character encoding appropriately for the locale, if the character encoding has not been explicitly set using + * {@link #setContentType} or {@link #setCharacterEncoding}, getWriter hasn't been called yet, and the + * response hasn't been committed yet. If the deployment descriptor contains a + * locale-encoding-mapping-list element, and that element provides a mapping for the given locale, that + * mapping is used. Otherwise, the mapping from locale to character encoding is container dependent. + *

+ * This method may be called repeatedly to change locale and character encoding. The method has no effect if called + * after the response has been committed. It does not set the response's character encoding if it is called after + * {@link #setContentType} has been called with a charset specification, after {@link #setCharacterEncoding} has + * been called, after getWriter has been called, or after the response has been committed. + *

+ * Containers must communicate the locale and the character encoding used for the servlet response's writer to the + * client if the protocol provides a way for doing so. In the case of HTTP, the locale is communicated via the + * Content-Language header, the character encoding as part of the Content-Type header for + * text media types. Note that the character encoding cannot be communicated via HTTP headers if the servlet does + * not specify a content type; however, it is still used to encode text written via the servlet response's writer. + * + * @param loc the locale of the response + * + * @see #getLocale + * @see #setContentType + * @see #setCharacterEncoding + */ + void setLocale(Locale loc); + + /** + * Returns the locale specified for this response using the {@link #setLocale} method. Calls made to + * setLocale after the response is committed have no effect. + * + * @return The locale specified for this response using the {@link #setLocale} method. If no locale has been + * specified, the container's default locale is returned. + * + * @see #setLocale + */ + Locale getLocale(); + +} diff --git a/java/jakarta/servlet/ServletResponseWrapper.java b/java/jakarta/servlet/ServletResponseWrapper.java new file mode 100644 index 0000000..e1737ac --- /dev/null +++ b/java/jakarta/servlet/ServletResponseWrapper.java @@ -0,0 +1,251 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * Provides a convenient implementation of the ServletResponse interface that can be subclassed by developers wishing to + * adapt the response from a Servlet. This class implements the Wrapper or Decorator pattern. Methods default to calling + * through to the wrapped response object. + * + * @since Servlet 2.3 + * + * @see jakarta.servlet.ServletResponse + */ +public class ServletResponseWrapper implements ServletResponse { + private static final String LSTRING_FILE = "jakarta.servlet.LocalStrings"; + private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + private ServletResponse response; + + /** + * Creates a ServletResponse adaptor wrapping the given response object. + * + * @param response The response to wrap + * + * @throws java.lang.IllegalArgumentException if the response is null. + */ + public ServletResponseWrapper(ServletResponse response) { + if (response == null) { + throw new IllegalArgumentException(lStrings.getString("wrapper.nullResponse")); + } + this.response = response; + } + + /** + * Return the wrapped ServletResponse object. + * + * @return The wrapped ServletResponse object. + */ + public ServletResponse getResponse() { + return this.response; + } + + /** + * Sets the response being wrapped. + * + * @param response The new response to wrap + * + * @throws java.lang.IllegalArgumentException if the response is null. + */ + public void setResponse(ServletResponse response) { + if (response == null) { + throw new IllegalArgumentException(lStrings.getString("wrapper.nullResponse")); + } + this.response = response; + } + + /** + * The default behavior of this method is to call setCharacterEncoding(String charset) on the wrapped response + * object. + * + * @since Servlet 2.4 + */ + @Override + public void setCharacterEncoding(String charset) { + this.response.setCharacterEncoding(charset); + } + + /** + * The default behavior of this method is to return getCharacterEncoding() on the wrapped response object. + */ + @Override + public String getCharacterEncoding() { + return this.response.getCharacterEncoding(); + } + + /** + * The default behavior of this method is to return getOutputStream() on the wrapped response object. + */ + @Override + public ServletOutputStream getOutputStream() throws IOException { + return this.response.getOutputStream(); + } + + /** + * The default behavior of this method is to return getWriter() on the wrapped response object. + */ + @Override + public PrintWriter getWriter() throws IOException { + return this.response.getWriter(); + } + + /** + * The default behavior of this method is to call setContentLength(int len) on the wrapped response object. + */ + @Override + public void setContentLength(int len) { + this.response.setContentLength(len); + } + + /** + * The default behavior of this method is to call setContentLengthLong(long len) on the wrapped response object. + * + * @since Servlet 3.1 + */ + @Override + public void setContentLengthLong(long length) { + this.response.setContentLengthLong(length); + } + + /** + * The default behavior of this method is to call setContentType(String type) on the wrapped response object. + */ + @Override + public void setContentType(String type) { + this.response.setContentType(type); + } + + /** + * The default behavior of this method is to return getContentType() on the wrapped response object. + * + * @since Servlet 2.4 + */ + @Override + public String getContentType() { + return this.response.getContentType(); + } + + /** + * The default behavior of this method is to call setBufferSize(int size) on the wrapped response object. + */ + @Override + public void setBufferSize(int size) { + this.response.setBufferSize(size); + } + + /** + * The default behavior of this method is to return getBufferSize() on the wrapped response object. + */ + @Override + public int getBufferSize() { + return this.response.getBufferSize(); + } + + /** + * The default behavior of this method is to call flushBuffer() on the wrapped response object. + */ + @Override + public void flushBuffer() throws IOException { + this.response.flushBuffer(); + } + + /** + * The default behavior of this method is to return isCommitted() on the wrapped response object. + */ + @Override + public boolean isCommitted() { + return this.response.isCommitted(); + } + + /** + * The default behavior of this method is to call reset() on the wrapped response object. + */ + @Override + public void reset() { + this.response.reset(); + } + + /** + * The default behavior of this method is to call resetBuffer() on the wrapped response object. + */ + @Override + public void resetBuffer() { + this.response.resetBuffer(); + } + + /** + * The default behavior of this method is to call setLocale(Locale loc) on the wrapped response object. + */ + @Override + public void setLocale(Locale loc) { + this.response.setLocale(loc); + } + + /** + * The default behavior of this method is to return getLocale() on the wrapped response object. + */ + @Override + public Locale getLocale() { + return this.response.getLocale(); + } + + /** + * TODO SERVLET3 - Add comments + * + * @param wrapped The response to compare to the wrapped response + * + * @return true if the response wrapped by this wrapper (or series of wrappers) is the same as the + * supplied response, otherwise false + * + * @since Servlet 3.0 + */ + public boolean isWrapperFor(ServletResponse wrapped) { + if (response == wrapped) { + return true; + } + if (response instanceof ServletResponseWrapper) { + return ((ServletResponseWrapper) response).isWrapperFor(wrapped); + } + return false; + } + + /** + * TODO SERVLET3 - Add comments + * + * @param wrappedType The class to compare to the class of the wrapped response + * + * @return true if the response wrapped by this wrapper (or series of wrappers) is the same type as the + * supplied type, otherwise false + * + * @since Servlet 3.0 + */ + public boolean isWrapperFor(Class wrappedType) { + if (wrappedType.isAssignableFrom(response.getClass())) { + return true; + } + if (response instanceof ServletResponseWrapper) { + return ((ServletResponseWrapper) response).isWrapperFor(wrappedType); + } + return false; + } + +} diff --git a/java/jakarta/servlet/ServletSecurityElement.java b/java/jakarta/servlet/ServletSecurityElement.java new file mode 100644 index 0000000..8c97fe1 --- /dev/null +++ b/java/jakarta/servlet/ServletSecurityElement.java @@ -0,0 +1,139 @@ +/* + * 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 jakarta.servlet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.annotation.HttpMethodConstraint; +import jakarta.servlet.annotation.ServletSecurity; + +/** + * The programmatic equivalent of {@link jakarta.servlet.annotation.ServletSecurity} used to configre security + * constraints for a Servlet. + * + * @since Servlet 3.0 + */ +public class ServletSecurityElement extends HttpConstraintElement { + + private final Map methodConstraints = new HashMap<>(); + + /** + * Use default HttpConstraint. + */ + public ServletSecurityElement() { + super(); + } + + /** + * Use specified HttpConstraintElement. + * + * @param httpConstraintElement The constraint + */ + public ServletSecurityElement(HttpConstraintElement httpConstraintElement) { + this(httpConstraintElement, null); + } + + /** + * Use specific constraints for specified methods and default HttpConstraintElement for all other methods. + * + * @param httpMethodConstraints Method constraints + * + * @throws IllegalArgumentException if a method name is specified more than once + */ + public ServletSecurityElement(Collection httpMethodConstraints) { + super(); + addHttpMethodConstraints(httpMethodConstraints); + } + + + /** + * Use specified HttpConstraintElement as default and specific constraints for specified methods. + * + * @param httpConstraintElement Default constraint + * @param httpMethodConstraints Method constraints + * + * @throws IllegalArgumentException if a method name is specified more than once + */ + public ServletSecurityElement(HttpConstraintElement httpConstraintElement, + Collection httpMethodConstraints) { + super(httpConstraintElement.getEmptyRoleSemantic(), httpConstraintElement.getTransportGuarantee(), + httpConstraintElement.getRolesAllowed()); + addHttpMethodConstraints(httpMethodConstraints); + } + + /** + * Create from an annotation. + * + * @param annotation Annotation to use as the basis for the new instance + * + * @throws IllegalArgumentException if a method name is specified more than once + */ + public ServletSecurityElement(ServletSecurity annotation) { + this(new HttpConstraintElement(annotation.value().value(), annotation.value().transportGuarantee(), + annotation.value().rolesAllowed())); + + List l = new ArrayList<>(); + HttpMethodConstraint[] constraints = annotation.httpMethodConstraints(); + if (constraints != null) { + for (HttpMethodConstraint constraint : constraints) { + HttpMethodConstraintElement e = new HttpMethodConstraintElement(constraint.value(), + new HttpConstraintElement(constraint.emptyRoleSemantic(), constraint.transportGuarantee(), + constraint.rolesAllowed())); + l.add(e); + } + } + addHttpMethodConstraints(l); + } + + /** + * Obtain the collection of security constraints configured for specific methods. + * + * @return The security constraints for specific methods + */ + public Collection getHttpMethodConstraints() { + Collection result = new HashSet<>(methodConstraints.values()); + return result; + } + + /** + * Obtain the collection HTTP methods for which security constraints have been defined. + * + * @return The names of the HTTP methods + */ + public Collection getMethodNames() { + Collection result = new HashSet<>(methodConstraints.keySet()); + return result; + } + + private void addHttpMethodConstraints(Collection httpMethodConstraints) { + if (httpMethodConstraints == null) { + return; + } + for (HttpMethodConstraintElement constraint : httpMethodConstraints) { + String method = constraint.getMethodName(); + if (methodConstraints.containsKey(method)) { + throw new IllegalArgumentException("Duplicate method name: " + method); + } + methodConstraints.put(method, constraint); + } + } +} diff --git a/java/jakarta/servlet/SessionCookieConfig.java b/java/jakarta/servlet/SessionCookieConfig.java new file mode 100644 index 0000000..c977905 --- /dev/null +++ b/java/jakarta/servlet/SessionCookieConfig.java @@ -0,0 +1,184 @@ +/* + * 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 jakarta.servlet; + +import java.util.Map; + +/** + * Configures the session cookies used by the web application associated with the ServletContext from which this + * SessionCookieConfig was obtained. + * + * @since Servlet 3.0 + */ +public interface SessionCookieConfig { + + /** + * Sets the session cookie name. + * + * @param name The name of the session cookie + * + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + void setName(String name); + + /** + * Obtain the name to use for the session cookies. + * + * @return the name to use for session cookies. + */ + String getName(); + + /** + * Sets the domain for the session cookie + * + * @param domain The session cookie domain + * + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + void setDomain(String domain); + + /** + * Obtain the domain to use for session cookies. + * + * @return the domain to use for session cookies. + */ + String getDomain(); + + /** + * Sets the path of the session cookie. + * + * @param path The session cookie path + * + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + void setPath(String path); + + /** + * Obtain the path to use for session cookies. This is normally the context path. + * + * @return The path to use for session cookies. + */ + String getPath(); + + /** + * If called, this method has no effect. + * + * @param comment Ignore + * + * @throws IllegalStateException if the associated ServletContext has already been initialised + * + * @deprecated This is no longer required with RFC 6265 + */ + @Deprecated(since = "Servlet 6.0", forRemoval = true) + void setComment(String comment); + + /** + * With the adoption of support for RFC 6265, this method should no longer be used. + * + * @return always {@code null} + * + * @deprecated This is no longer required with RFC 6265 + */ + @Deprecated(since = "Servlet 6.0", forRemoval = true) + String getComment(); + + /** + * Sets the httpOnly flag for the session cookie. + * + * @param httpOnly The httpOnly setting to use for session cookies + * + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + void setHttpOnly(boolean httpOnly); + + /** + * Will session cookies be created with the httpOnly flag set? + * + * @return {@code true} if the flag should be set, otherwise {@code false} + */ + boolean isHttpOnly(); + + /** + * Sets the secure flag for the session cookie. + * + * @param secure The secure setting to use for session cookies + * + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + void setSecure(boolean secure); + + /** + * Will session cookies be created with the secure flag set? + * + * @return {@code true} if the flag should be set, otherwise {@code false} + */ + boolean isSecure(); + + /** + * Sets the maximum age. + * + * @param MaxAge the maximum age to set + * + * @throws IllegalStateException if the associated ServletContext has already been initialised + */ + void setMaxAge(int MaxAge); + + /** + * Obtain the maximum age to set for a session cookie. + * + * @return the maximum age in seconds + */ + int getMaxAge(); + + /** + * Sets the value for the given session cookie attribute. When a value is set via this method, the value returned by + * the attribute specific getter (if any) must be consistent with the value set via this method. + * + * @param name Name of attribute to set + * @param value Value of attribute + * + * @throws IllegalStateException if the associated ServletContext has already been initialised + * @throws IllegalArgumentException If the attribute name is null or contains any characters not permitted for use + * in Cookie names. + * @throws NumberFormatException If the attribute is known to be numerical but the provided value cannot be + * parsed to a number. + * + * @since Servlet 6.0 + */ + void setAttribute(String name, String value); + + /** + * Obtain the value for a sesison cookie given attribute. Values returned from this method must be consistent with + * the values set and returned by the attribute specific getters and setters in this class. + * + * @param name Name of attribute to return + * + * @return Value of specified attribute + * + * @since Servlet 6.0 + */ + String getAttribute(String name); + + /** + * Obtain the Map of attributes and values (excluding version) for this session cookie. + * + * @return A read-only Map of attributes to values, excluding version. + * + * @since Servlet 6.0 + */ + Map getAttributes(); +} diff --git a/java/jakarta/servlet/SessionTrackingMode.java b/java/jakarta/servlet/SessionTrackingMode.java new file mode 100644 index 0000000..5f6f727 --- /dev/null +++ b/java/jakarta/servlet/SessionTrackingMode.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.servlet; + +/** + * Defines the permitted options for configuring the session tracking mode. + * + * @since Servlet 3.0 + */ +public enum SessionTrackingMode { + /** + * Use HTTP cookies. + */ + COOKIE, + + /** + * Use url rewriting (also known as path parameter) + */ + URL, + + /** + * Use SSL session. + */ + SSL +} diff --git a/java/jakarta/servlet/UnavailableException.java b/java/jakarta/servlet/UnavailableException.java new file mode 100644 index 0000000..3e3a433 --- /dev/null +++ b/java/jakarta/servlet/UnavailableException.java @@ -0,0 +1,108 @@ +/* + * 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 jakarta.servlet; + +/** + * Defines an exception that a servlet or filter throws to indicate that it is permanently or temporarily unavailable. + *

+ * When a servlet or filter is permanently unavailable, something is wrong with it, and it cannot handle requests until + * some action is taken. For example, a servlet might be configured incorrectly, or a filter's state may be corrupted. + * The component should log both the error and the corrective action that is needed. + *

+ * A servlet or filter is temporarily unavailable if it cannot handle requests momentarily due to some system-wide + * problem. For example, a third-tier server might not be accessible, or there may be insufficient memory or disk + * storage to handle requests. A system administrator may need to take corrective action. + *

+ * Servlet containers can safely treat both types of unavailable exceptions in the same way. However, treating temporary + * unavailability effectively makes the servlet container more robust. Specifically, the servlet container might block + * requests to the servlet or filter for a period of time suggested by the exception, rather than rejecting them until + * the servlet container restarts. + */ +public class UnavailableException extends ServletException { + + private static final long serialVersionUID = 1L; + + /** + * Is the issue permanent - i.e. is administrator action required? + */ + private final boolean permanent; + + /** + * The estimate of how long the Servlet will be unavailable. + */ + private final int seconds; + + /** + * Constructs a new exception with a descriptive message indicating that the servlet is permanently unavailable. + * + * @param msg a String specifying the descriptive message + */ + public UnavailableException(String msg) { + super(msg); + seconds = 0; + permanent = true; + } + + /** + * Constructs a new exception with a descriptive message indicating that the servlet is temporarily unavailable and + * giving an estimate of how long it will be unavailable. + *

+ * In some cases, the servlet cannot make an estimate. For example, the servlet might know that a server it needs is + * not running, but not be able to report how long it will take to be restored to functionality. This can be + * indicated with a negative or zero value for the seconds argument. + * + * @param msg a String specifying the descriptive message, which can be written to a log file or + * displayed for the user. + * @param seconds an integer specifying the number of seconds the servlet expects to be unavailable; if zero or + * negative, indicates that the servlet can't make an estimate + */ + public UnavailableException(String msg, int seconds) { + super(msg); + + if (seconds <= 0) { + this.seconds = -1; + } else { + this.seconds = seconds; + } + permanent = false; + } + + /** + * Returns a boolean indicating whether the servlet is permanently unavailable. If so, something is + * wrong with the servlet, and the system administrator must take some corrective action. + * + * @return true if the servlet is permanently unavailable; false if the servlet is + * available or temporarily unavailable + */ + public boolean isPermanent() { + return permanent; + } + + /** + * Returns the number of seconds the servlet expects to be temporarily unavailable. + *

+ * If this method returns a negative number, the servlet is permanently unavailable or cannot provide an estimate of + * how long it will be unavailable. No effort is made to correct for the time elapsed since the exception was first + * reported. + * + * @return an integer specifying the number of seconds the servlet will be temporarily unavailable, or a negative + * number if the servlet is permanently unavailable or cannot make an estimate + */ + public int getUnavailableSeconds() { + return permanent ? -1 : seconds; + } +} diff --git a/java/jakarta/servlet/WriteListener.java b/java/jakarta/servlet/WriteListener.java new file mode 100644 index 0000000..8810012 --- /dev/null +++ b/java/jakarta/servlet/WriteListener.java @@ -0,0 +1,43 @@ +/* + * 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 jakarta.servlet; + +import java.io.IOException; + +/** + * Receives notification of write events when using non-blocking IO. + * + * @since Servlet 3.1 + */ +public interface WriteListener extends java.util.EventListener { + + /** + * Invoked when it it possible to write data without blocking. The container will invoke this method the first time + * for a request as soon as data can be written. Subsequent invocations will only occur if a call to + * {@link ServletOutputStream#isReady()} has returned false and it has since become possible to write data. + * + * @throws IOException if an I/O error occurs while processing this event + */ + void onWritePossible() throws IOException; + + /** + * Invoked if an error occurs while writing the response. + * + * @param throwable The throwable that represents the error that occurred + */ + void onError(Throwable throwable); +} \ No newline at end of file diff --git a/java/jakarta/servlet/annotation/HandlesTypes.java b/java/jakarta/servlet/annotation/HandlesTypes.java new file mode 100644 index 0000000..7dd52b1 --- /dev/null +++ b/java/jakarta/servlet/annotation/HandlesTypes.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to declare an array of application classes which are passed to a + * {@link jakarta.servlet.ServletContainerInitializer}. + * + * @since Servlet 3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface HandlesTypes { + + /** + * @return array of classes + */ + Class[] value(); + +} diff --git a/java/jakarta/servlet/annotation/HttpConstraint.java b/java/jakarta/servlet/annotation/HttpConstraint.java new file mode 100644 index 0000000..e62cbf7 --- /dev/null +++ b/java/jakarta/servlet/annotation/HttpConstraint.java @@ -0,0 +1,63 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic; +import jakarta.servlet.annotation.ServletSecurity.TransportGuarantee; + +/** + * This annotation represents the security constraints that are applied to all requests with HTTP protocol method types + * that are not otherwise represented by a corresponding {@link jakarta.servlet.annotation.HttpMethodConstraint} in a + * {@link jakarta.servlet.annotation.ServletSecurity} annotation. + * + * @since Servlet 3.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface HttpConstraint { + + /** + * The EmptyRoleSemantic determines the behaviour when the rolesAllowed list is empty. + * + * @return empty role semantic + */ + EmptyRoleSemantic value() default EmptyRoleSemantic.PERMIT; + + /** + * Determines whether SSL/TLS is required to process the current request. + * + * @return transport guarantee + */ + TransportGuarantee transportGuarantee() default TransportGuarantee.NONE; + + /** + * The authorized roles' names. The container may discard duplicate role names during processing of the annotation. + * N.B. The String "*" does not have a special meaning if it occurs as a role name. + * + * @return array of names. The array may be of zero length, in which case the EmptyRoleSemantic applies; the + * returned value determines whether access is to be permitted or denied regardless of the identity and + * authentication state in either case, PERMIT or DENY.
+ * Otherwise, when the array contains one or more role names access is permitted if the user a member of + * at least one of the named roles. The EmptyRoleSemantic is not applied in this case. + */ + String[] rolesAllowed() default {}; + +} diff --git a/java/jakarta/servlet/annotation/HttpMethodConstraint.java b/java/jakarta/servlet/annotation/HttpMethodConstraint.java new file mode 100644 index 0000000..376e0c5 --- /dev/null +++ b/java/jakarta/servlet/annotation/HttpMethodConstraint.java @@ -0,0 +1,68 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic; +import jakarta.servlet.annotation.ServletSecurity.TransportGuarantee; + +/** + * Specific security constraints can be applied to different types of request, differentiated by the HTTP protocol + * method type by using this annotation inside the {@link jakarta.servlet.annotation.ServletSecurity} annotation. + * + * @since Servlet 3.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface HttpMethodConstraint { + + /** + * HTTP Protocol method name (e.g. POST, PUT) + * + * @return method name + */ + String value(); + + /** + * The EmptyRoleSemantic determines the behaviour when the rolesAllowed list is empty. + * + * @return empty role semantic + */ + EmptyRoleSemantic emptyRoleSemantic() default EmptyRoleSemantic.PERMIT; + + /** + * Determines whether SSL/TLS is required to process the current request. + * + * @return transport guarantee + */ + TransportGuarantee transportGuarantee() default TransportGuarantee.NONE; + + /** + * The authorized roles' names. The container may discard duplicate role names during processing of the annotation. + * N.B. The String "*" does not have a special meaning if it occurs as a role name. + * + * @return array of names. The array may be of zero length, in which case the EmptyRoleSemantic applies; the + * returned value determines whether access is to be permitted or denied regardless of the identity and + * authentication state in either case, PERMIT or DENY.
+ * Otherwise, when the array contains one or more role names access is permitted if the user a member of + * at least one of the named roles. The EmptyRoleSemantic is not applied in this case. + */ + String[] rolesAllowed() default {}; +} diff --git a/java/jakarta/servlet/annotation/MultipartConfig.java b/java/jakarta/servlet/annotation/MultipartConfig.java new file mode 100644 index 0000000..6e0d692 --- /dev/null +++ b/java/jakarta/servlet/annotation/MultipartConfig.java @@ -0,0 +1,64 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to indicate that the {@link jakarta.servlet.Servlet} on which it is declared expects requests + * to made using the {@code + * multipart/form-data} MIME type.
+ *
+ * {@link jakarta.servlet.http.Part} components of a given {@code + * multipart/form-data} request are retrieved by a Servlet annotated with {@code MultipartConfig} by calling + * {@link jakarta.servlet.http.HttpServletRequest#getPart} or + * {@link jakarta.servlet.http.HttpServletRequest#getParts}.
+ *
+ * E.g. @WebServlet("/upload")}
+ * @MultipartConfig() public class UploadServlet extends + * HttpServlet ... }
+ * + * @since Servlet 3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface MultipartConfig { + + /** + * @return location in which the Container stores temporary files + */ + String location() default ""; + + /** + * @return the maximum size allowed for uploaded files (in bytes) + */ + long maxFileSize() default -1L; + + /** + * @return the maximum size of the request allowed for {@code + * multipart/form-data} + */ + long maxRequestSize() default -1L; + + /** + * @return the size threshold at which the file will be written to the disk + */ + int fileSizeThreshold() default 0; +} diff --git a/java/jakarta/servlet/annotation/ServletSecurity.java b/java/jakarta/servlet/annotation/ServletSecurity.java new file mode 100644 index 0000000..2864407 --- /dev/null +++ b/java/jakarta/servlet/annotation/ServletSecurity.java @@ -0,0 +1,85 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declare this annotation on a {@link jakarta.servlet.Servlet} implementation class to enforce security constraints on + * HTTP protocol requests.
+ * The container applies constraints to the URL patterns mapped to each Servlet which declares this annotation.
+ *
+ * + * @since Servlet 3.0 + */ +@Inherited +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ServletSecurity { + + /** + * Represents the two possible values of the empty role semantic, active when a list of role names is empty. + */ + enum EmptyRoleSemantic { + + /** + * Access MUST be permitted, regardless of authentication state or identity + */ + PERMIT, + + /** + * Access MUST be denied, regardless of authentication state or identity + */ + DENY + } + + /** + * Represents the two possible values of data transport, encrypted or not. + */ + enum TransportGuarantee { + + /** + * User data must not be encrypted by the container during transport + */ + NONE, + + /** + * The container MUST encrypt user data during transport + */ + CONFIDENTIAL + } + + /** + * The default constraint to apply to requests not handled by specific method constraints + * + * @return http constraint + */ + HttpConstraint value() default @HttpConstraint; + + /** + * An array of HttpMethodConstraint objects to which the security constraint will be applied + * + * @return array of http method constraint + */ + HttpMethodConstraint[] httpMethodConstraints() default {}; +} diff --git a/java/jakarta/servlet/annotation/WebFilter.java b/java/jakarta/servlet/annotation/WebFilter.java new file mode 100644 index 0000000..a277d43 --- /dev/null +++ b/java/jakarta/servlet/annotation/WebFilter.java @@ -0,0 +1,110 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.servlet.DispatcherType; + +/** + * The annotation used to declare a Servlet {@link jakarta.servlet.Filter}.
+ *
+ * This annotation will be processed by the container during deployment, the Filter class in which it is found will be + * created as per the configuration and applied to the URL patterns, {@link jakarta.servlet.Servlet}s and + * {@link jakarta.servlet.DispatcherType}s.
+ *
+ * If the name attribute is not defined, the fully qualified name of the class is used.
+ *
+ * At least one URL pattern MUST be declared in either the {@code value} or {@code urlPattern} attribute of the + * annotation, but not both.
+ *
+ * The {@code value} attribute is recommended for use when the URL pattern is the only attribute being set, otherwise + * the {@code urlPattern} attribute should be used.
+ *
+ * The annotated class MUST implement {@link jakarta.servlet.Filter}. E.g. @WebFilter("/path/*")
+ * public class AnExampleFilter implements Filter { ...
+ * + * @since Servlet 3.0 (Section 8.1.2) + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebFilter { + + /** + * @return description of the Filter, if present + */ + String description() default ""; + + /** + * @return display name of the Filter, if present + */ + String displayName() default ""; + + /** + * @return array of initialization params for this Filter + */ + WebInitParam[] initParams() default {}; + + /** + * @return name of the Filter, if present + */ + String filterName() default ""; + + /** + * @return small icon for this Filter, if present + */ + String smallIcon() default ""; + + /** + * @return the large icon for this Filter, if present + */ + String largeIcon() default ""; + + /** + * @return array of Servlet names to which this Filter applies + */ + String[] servletNames() default {}; + + /** + * A convenience method, to allow extremely simple annotation of a class. + * + * @return array of URL patterns + * + * @see #urlPatterns() + */ + String[] value() default {}; + + /** + * @return array of URL patterns to which this Filter applies + */ + String[] urlPatterns() default {}; + + /** + * @return array of DispatcherTypes to which this filter applies + */ + DispatcherType[] dispatcherTypes() default { DispatcherType.REQUEST }; + + /** + * @return asynchronous operation supported by this Filter + */ + boolean asyncSupported() default false; +} diff --git a/java/jakarta/servlet/annotation/WebInitParam.java b/java/jakarta/servlet/annotation/WebInitParam.java new file mode 100644 index 0000000..feb4bf9 --- /dev/null +++ b/java/jakarta/servlet/annotation/WebInitParam.java @@ -0,0 +1,55 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotation used to declare an initialization parameter on a {@link jakarta.servlet.Servlet} or + * {@link jakarta.servlet.Filter}, within a {@link jakarta.servlet.annotation.WebFilter} or + * {@link jakarta.servlet.annotation.WebServlet} annotation.
+ *
+ * E.g. + * &#064;WebServlet(name="TestServlet", urlPatterns={"/test"},initParams={&#064;WebInitParam(name="test", value="true")}) + * public class TestServlet extends HttpServlet { ...
+ * + * @since Servlet 3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebInitParam { + + /** + * @return name of the initialization parameter + */ + String name(); + + /** + * @return value of the initialization parameter + */ + String value(); + + /** + * @return description of the initialization parameter + */ + String description() default ""; +} diff --git a/java/jakarta/servlet/annotation/WebListener.java b/java/jakarta/servlet/annotation/WebListener.java new file mode 100644 index 0000000..aee671b --- /dev/null +++ b/java/jakarta/servlet/annotation/WebListener.java @@ -0,0 +1,47 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotation used to declare a listener for various types of event, in a given web application context.
+ *
+ * The class annotated MUST implement one, (or more), of the following interfaces: + * {@link jakarta.servlet.http.HttpSessionAttributeListener}, {@link jakarta.servlet.http.HttpSessionListener}, + * {@link jakarta.servlet.ServletContextAttributeListener}, {@link jakarta.servlet.ServletContextListener}, + * {@link jakarta.servlet.ServletRequestAttributeListener}, {@link jakarta.servlet.ServletRequestListener} or + * {@link jakarta.servlet.http.HttpSessionIdListener}
+ * E.g. @WebListener
+ * public TestListener implements ServletContextListener {
+ * + * @since Servlet 3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebListener { + + /** + * @return description of the listener, if present + */ + String value() default ""; +} diff --git a/java/jakarta/servlet/annotation/WebServlet.java b/java/jakarta/servlet/annotation/WebServlet.java new file mode 100644 index 0000000..5e1334b --- /dev/null +++ b/java/jakarta/servlet/annotation/WebServlet.java @@ -0,0 +1,102 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to declare the configuration of a {@link jakarta.servlet.Servlet}.
+ * If the name attribute is not defined, the fully qualified name of the class is used.
+ *
+ * At least one URL pattern MUST be declared in either the {@code value} or {@code urlPattern} attribute of the + * annotation, but not both.
+ *
+ * The {@code value} attribute is recommended for use when the URL pattern is the only attribute being set, otherwise + * the {@code urlPattern} attribute should be used.
+ *
+ * The class on which this annotation is declared MUST extend {@link jakarta.servlet.http.HttpServlet}.
+ *
+ * E.g. @WebServlet("/path")}
+ * public class TestServlet extends HttpServlet ... {

+ * E.g. @WebServlet(name="TestServlet", urlPatterns={"/path", "/alt"})
+ * public class TestServlet extends HttpServlet ... {

+ * + * @since Servlet 3.0 (Section 8.1.1) + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebServlet { + + /** + * @return name of the Servlet + */ + String name() default ""; + + /** + * A convenience method, to allow extremely simple annotation of a class. + * + * @return array of URL patterns + * + * @see #urlPatterns() + */ + String[] value() default {}; + + /** + * @return array of URL patterns to which this Filter applies + */ + String[] urlPatterns() default {}; + + /** + * @return load on startup ordering hint + */ + int loadOnStartup() default -1; + + /** + * @return array of initialization params for this Servlet + */ + WebInitParam[] initParams() default {}; + + /** + * @return asynchronous operation supported by this Servlet + */ + boolean asyncSupported() default false; + + /** + * @return small icon for this Servlet, if present + */ + String smallIcon() default ""; + + /** + * @return large icon for this Servlet, if present + */ + String largeIcon() default ""; + + /** + * @return description of this Servlet, if present + */ + String description() default ""; + + /** + * @return display name of this Servlet, if present + */ + String displayName() default ""; +} diff --git a/java/jakarta/servlet/descriptor/JspConfigDescriptor.java b/java/jakarta/servlet/descriptor/JspConfigDescriptor.java new file mode 100644 index 0000000..401b7e9 --- /dev/null +++ b/java/jakarta/servlet/descriptor/JspConfigDescriptor.java @@ -0,0 +1,44 @@ +/* + * 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 jakarta.servlet.descriptor; + +import java.util.Collection; + +/** + * This interface exposes the JSP specific configuration information obtain ed from the deployment descriptors. It is + * primarily provided so that JSP implementations do not have to parse deployment descriptors. + * + * @since Servlet 3.0 + */ +public interface JspConfigDescriptor { + + /** + * Provide the set of tag library descriptors obtained from the <jsp-config> elements in the web application's + * deployment descriptors. + * + * @return the tag library descriptors + */ + Collection getTaglibs(); + + /** + * Provide the set of JSP property groups obtained from the <jsp-config> elements in the web application's + * deployment descriptors. + * + * @return the JSP property groups + */ + Collection getJspPropertyGroups(); +} diff --git a/java/jakarta/servlet/descriptor/JspPropertyGroupDescriptor.java b/java/jakarta/servlet/descriptor/JspPropertyGroupDescriptor.java new file mode 100644 index 0000000..6cd415b --- /dev/null +++ b/java/jakarta/servlet/descriptor/JspPropertyGroupDescriptor.java @@ -0,0 +1,121 @@ +/* + * 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 jakarta.servlet.descriptor; + +import java.util.Collection; + +/** + * Represents the JSP property groups in the deployment descriptors. + * + * @since Servlet 3.0 + */ +public interface JspPropertyGroupDescriptor { + + /** + * Obtain the patterns to which this group applies. + * + * @return the patterns to which this group applies + */ + Collection getUrlPatterns(); + + /** + * Is Expression Language ignored for this group? + * + * @return {@code true} if EL is ignored, otherwise {@code false} + */ + String getElIgnored(); + + /** + * Will the use of an unknown identifier in EL within a JSP page trigger an error for this group? + * + * @return {@code true} if an error will be triggered, otherwise {@code false} + * + * @since Servlet 6.0 + */ + String getErrorOnELNotFound(); + + /** + * Obtain the page encoding for this group. + * + * @return the page encoding for this group + */ + String getPageEncoding(); + + /** + * Is scripting disabled for this group? + * + * @return {@code true} if scripting is disabled, otherwise {@code false} + */ + String getScriptingInvalid(); + + /** + * Should the JSPs in this group be treated as JSP documents? + * + * @return {@code true} if the JSPs should be treated as JSP documents, otherwise {@code false} + */ + String getIsXml(); + + /** + * Obtain the preludes to include for this group. + * + * @return the preludes to include for this group + */ + Collection getIncludePreludes(); + + /** + * Obtain the codas to include for this group. + * + * @return the codas to include for this group. + */ + Collection getIncludeCodas(); + + /** + * Is the deferred El syntax #{...} allowed to be used as a literal in this group? + * + * @return {@code true} if the deferred EL syntax is allowed to be used as a literal, otherwise {@code false} + */ + String getDeferredSyntaxAllowedAsLiteral(); + + /** + * Should the JSPs in this group have template text that only contains whitespace removed? + * + * @return {@code true} if the whitespace be removed, otherwise {@code false} + */ + String getTrimDirectiveWhitespaces(); + + /** + * Obtain the default content type this group of JSP pages.# + * + * @return the default content type this group of JSP pages + */ + String getDefaultContentType(); + + /** + * Obtain the per-page buffer configuration for this group of JSP pages. + * + * @return the per-page buffer configuration for this group of JSP pages + */ + String getBuffer(); + + /** + * Should an error be raised at translation time for a page in this group if the page contains a reference (e.g. a + * tag) to a undeclared namespace. + * + * @return {@code true} if an error should be raised, otherwise {@code false} + */ + String getErrorOnUndeclaredNamespace(); +} diff --git a/java/jakarta/servlet/descriptor/TaglibDescriptor.java b/java/jakarta/servlet/descriptor/TaglibDescriptor.java new file mode 100644 index 0000000..545377d --- /dev/null +++ b/java/jakarta/servlet/descriptor/TaglibDescriptor.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.servlet.descriptor; + +/** + * Represents a taglib descriptor definitions in the deployment descriptor. + * + * @since Servlet 3.0 + */ +public interface TaglibDescriptor { + + /** + * Obtain the URI for the tag library. + * + * @return the URI for the tag library + */ + String getTaglibURI(); + + /** + * Obtain the location of the tag library. + * + * @return the location of the tag library + */ + String getTaglibLocation(); +} diff --git a/java/jakarta/servlet/http/Cookie.java b/java/jakarta/servlet/http/Cookie.java new file mode 100644 index 0000000..22ddc5d --- /dev/null +++ b/java/jakarta/servlet/http/Cookie.java @@ -0,0 +1,548 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.BitSet; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.TreeMap; + +/** + * Creates a cookie, a small amount of information sent by a servlet to a Web browser, saved by the browser, and later + * sent back to the server. A cookie's value can uniquely identify a client, so cookies are commonly used for session + * management. + *

+ * A cookie has a name, a single value, and optional attributes such as a comment, path and domain qualifiers, a maximum + * age, and a version number. Some Web browsers have bugs in how they handle the optional attributes, so use them + * sparingly to improve the interoperability of your servlets. + *

+ * The servlet sends cookies to the browser by using the {@link HttpServletResponse#addCookie} method, which adds fields + * to HTTP response headers to send cookies to the browser, one at a time. The browser is expected to support 50 cookies + * for each domain, 3000 cookies total, and may limit cookie size to 4 KiB each. + *

+ * The browser returns cookies to the servlet by adding fields to HTTP request headers. Cookies can be retrieved from a + * request by using the {@link HttpServletRequest#getCookies} method. Several cookies might have the same name but + * different path attributes. + *

+ * Cookies affect the caching of the Web pages that use them. HTTP 1.0 does not cache pages that use cookies created + * with this class. This class does not support the cache control defined with HTTP 1.1. + *

+ * This class supports both the RFC 6265 specification. + */ +public class Cookie implements Cloneable, Serializable { + + private static final String LSTRING_FILE = "jakarta.servlet.http.LocalStrings"; + private static final ResourceBundle LSTRINGS = ResourceBundle.getBundle(LSTRING_FILE); + + private static final CookieNameValidator validation = new RFC6265Validator(); + + private static final long serialVersionUID = 2L; + + /** + * Cookie name. + */ + private final String name; + + /** + * Cookie value. + */ + private String value; + + /** + * Attributes encoded in the header's cookie fields. + */ + private volatile Map attributes; + + private static final String DOMAIN = "Domain"; + private static final String MAX_AGE = "Max-Age"; + private static final String PATH = "Path"; + private static final String SECURE = "Secure"; + private static final String HTTP_ONLY = "HttpOnly"; + + /** + * Constructs a cookie with a specified name and value. + *

+ * The cookie's name cannot be changed after creation. + *

+ * The value can be anything the server chooses to send. Its value is probably of interest only to the server. The + * cookie's value can be changed after creation with the setValue method. + * + * @param name a String specifying the name of the cookie + * @param value a String specifying the value of the cookie + * + * @throws IllegalArgumentException if the cookie name contains illegal characters + * + * @see #setValue + */ + public Cookie(String name, String value) { + validation.validate(name); + this.name = name; + this.value = value; + } + + + /** + * If called, this method has no effect. + * + * @param purpose ignored + * + * @see #getComment + * + * @deprecated This is no longer required with RFC 6265 + */ + @Deprecated(since = "Servlet 6.0", forRemoval = true) + public void setComment(String purpose) { + // NO-OP + } + + + /** + * With the adoption of support for RFC 6265, this method should no longer be used. + * + * @return always {@code null} + * + * @see #setComment + * + * @deprecated This is no longer required with RFC 6265 + */ + @Deprecated(since = "Servlet 6.0", forRemoval = true) + public String getComment() { + return null; + } + + + /** + * Specifies the domain within which this cookie should be presented. + *

+ * By default, cookies are only returned to the server that sent them. + * + * @param pattern a String containing the domain name within which this cookie is visible + * + * @see #getDomain + */ + public void setDomain(String pattern) { + if (pattern == null) { + setAttributeInternal(DOMAIN, null); + } else { + // IE requires the domain to be lower case (unconfirmed) + setAttributeInternal(DOMAIN, pattern.toLowerCase(Locale.ENGLISH)); + } + } + + + /** + * Returns the domain name set for this cookie. + * + * @return a String containing the domain name + * + * @see #setDomain + */ + public String getDomain() { + return getAttribute(DOMAIN); + } + + + /** + * Sets the maximum age of the cookie in seconds. + *

+ * A positive value indicates that the cookie will expire after that many seconds have passed. Note that the value + * is the maximum age when the cookie will expire, not the cookie's current age. + *

+ * A negative value means that the cookie is not stored persistently and will be deleted when the Web browser exits. + * A zero value causes the cookie to be deleted. + * + * @param expiry an integer specifying the maximum age of the cookie in seconds; if negative, means the cookie is + * not stored; if zero, deletes the cookie + * + * @see #getMaxAge + */ + public void setMaxAge(int expiry) { + setAttributeInternal(MAX_AGE, Integer.toString(expiry)); + } + + + /** + * Returns the maximum age of the cookie, specified in seconds, By default, -1 indicating the cookie + * will persist until browser shutdown. + * + * @return an integer specifying the maximum age of the cookie in seconds; if negative, means the cookie persists + * until browser shutdown + * + * @see #setMaxAge + */ + public int getMaxAge() { + String maxAge = getAttribute(MAX_AGE); + if (maxAge == null) { + return -1; + } else { + return Integer.parseInt(maxAge); + } + } + + + /** + * Specifies a path for the cookie to which the client should return the cookie. + *

+ * The cookie is visible to all the pages in the directory you specify, and all the pages in that directory's + * subdirectories. A cookie's path must include the servlet that set the cookie, for example, /catalog, which + * makes the cookie visible to all directories on the server under /catalog. + * + * @param uri a String specifying a path + * + * @see #getPath + */ + public void setPath(String uri) { + setAttributeInternal(PATH, uri); + } + + + /** + * Returns the path on the server to which the browser returns this cookie. The cookie is visible to all subpaths on + * the server. + * + * @return a String specifying a path that contains a servlet name, for example, /catalog + * + * @see #setPath + */ + public String getPath() { + return getAttribute(PATH); + } + + + /** + * Indicates to the browser whether the cookie should only be sent using a secure protocol, such as HTTPS or SSL. + *

+ * The default value is false. + * + * @param flag if true, sends the cookie from the browser to the server only when using a secure + * protocol; if false, sent on any protocol + * + * @see #getSecure + */ + public void setSecure(boolean flag) { + setAttributeInternal(SECURE, Boolean.toString(flag)); + } + + + /** + * Returns true if the browser is sending cookies only over a secure protocol, or false if + * the browser can send cookies using any protocol. + * + * @return true if the browser uses a secure protocol; otherwise, false + * + * @see #setSecure + */ + public boolean getSecure() { + return Boolean.parseBoolean(getAttribute(SECURE)); + } + + + /** + * Returns the name of the cookie. The name cannot be changed after creation. + * + * @return a String specifying the cookie's name + */ + public String getName() { + return name; + } + + + /** + * Assigns a new value to a cookie after the cookie is created. If you use a binary value, you may want to use + * BASE64 encoding. + *

+ * With Version 0 cookies, values should not contain white space, brackets, parentheses, equals signs, commas, + * double quotes, slashes, question marks, at signs, colons, and semicolons. Empty values may not behave the same + * way on all browsers. + * + * @param newValue a String specifying the new value + * + * @see #getValue + * @see Cookie + */ + public void setValue(String newValue) { + value = newValue; + } + + + /** + * Returns the value of the cookie. + * + * @return a String containing the cookie's present value + * + * @see #setValue + * @see Cookie + */ + public String getValue() { + return value; + } + + + /** + * With the adoption of support for RFC 6265, this method should no longer be used. + * + * @return Always zero + * + * @see #setVersion + * + * @deprecated This is no longer required with RFC 6265 + */ + @Deprecated(since = "Servlet 6.0", forRemoval = true) + public int getVersion() { + return 0; + } + + + /** + * If called, this method has no effect. + * + * @param v Ignored + * + * @see #getVersion + * + * @deprecated This is no longer required with RFC 6265 + */ + @Deprecated(since = "Servlet 6.0", forRemoval = true) + public void setVersion(int v) { + // NO-OP + } + + + /** + * Overrides the standard java.lang.Object.clone method to return a copy of this cookie. + */ + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + + /** + * Sets the flag that controls if this cookie will be hidden from scripts on the client side. + * + * @param httpOnly The new value of the flag + * + * @since Servlet 3.0 + */ + public void setHttpOnly(boolean httpOnly) { + setAttributeInternal(HTTP_ONLY, Boolean.toString(httpOnly)); + } + + + /** + * Gets the flag that controls if this cookie will be hidden from scripts on the client side. + * + * @return true if the cookie is hidden from scripts, else false + * + * @since Servlet 3.0 + */ + public boolean isHttpOnly() { + return Boolean.parseBoolean(getAttribute(HTTP_ONLY)); + } + + + /** + * Sets the value for the given cookie attribute. When a value is set via this method, the value returned by the + * attribute specific getter (if any) must be consistent with the value set via this method. + * + * @param name Name of attribute to set + * @param value Value of attribute + * + * @throws IllegalArgumentException If the attribute name is null or contains any characters not permitted for use + * in Cookie names. + * @throws NumberFormatException If the attribute is known to be numerical but the provided value cannot be + * parsed to a number. + * + * @since Servlet 6.0 + */ + public void setAttribute(String name, String value) { + if (name == null) { + throw new IllegalArgumentException(LSTRINGS.getString("cookie.attribute.invalidName.null")); + } + if (!validation.isToken(name)) { + String msg = LSTRINGS.getString("cookie.attribute.invalidName.notToken"); + throw new IllegalArgumentException(MessageFormat.format(msg, name)); + } + + if (name.equalsIgnoreCase(MAX_AGE)) { + if (value == null) { + setAttributeInternal(MAX_AGE, null); + } else { + // Integer.parseInt throws NFE if required + setMaxAge(Integer.parseInt(value)); + } + } else { + setAttributeInternal(name, value); + } + } + + + private void setAttributeInternal(String name, String value) { + if (attributes == null) { + if (value == null) { + return; + } else { + // Case insensitive keys but retain case used + attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + } + + if (value == null) { + attributes.remove(name); + } else { + attributes.put(name, value); + } + } + + + /** + * Obtain the value for a given attribute. Values returned from this method must be consistent with the values set + * and returned by the attribute specific getters and setters in this class. + * + * @param name Name of attribute to return + * + * @return Value of specified attribute + * + * @since Servlet 6.0 + */ + public String getAttribute(String name) { + if (attributes == null) { + return null; + } else { + return attributes.get(name); + } + } + + + /** + * Obtain the Map of attributes and values (excluding version) for this cookie. + * + * @return A read-only Map of attributes to values, excluding version. + * + * @since Servlet 6.0 + */ + public Map getAttributes() { + if (attributes == null) { + return Collections.emptyMap(); + } else { + return Collections.unmodifiableMap(attributes); + } + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((attributes == null) ? 0 : attributes.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Cookie other = (Cookie) obj; + if (attributes == null) { + if (other.attributes != null) { + return false; + } + } else if (!attributes.equals(other.attributes)) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + if (value == null) { + if (other.value != null) { + return false; + } + } else if (!value.equals(other.value)) { + return false; + } + return true; + } +} + + +class CookieNameValidator { + private static final String LSTRING_FILE = "jakarta.servlet.http.LocalStrings"; + protected static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + protected final BitSet allowed; + + protected CookieNameValidator(String separators) { + allowed = new BitSet(128); + allowed.set(0x20, 0x7f); // any CHAR except CTLs or separators + for (int i = 0; i < separators.length(); i++) { + char ch = separators.charAt(i); + allowed.clear(ch); + } + } + + void validate(String name) { + if (name == null || name.length() == 0) { + throw new IllegalArgumentException(lStrings.getString("err.cookie_name_blank")); + } + if (!isToken(name)) { + String errMsg = lStrings.getString("err.cookie_name_is_token"); + throw new IllegalArgumentException(MessageFormat.format(errMsg, name)); + } + } + + boolean isToken(String possibleToken) { + int len = possibleToken.length(); + + for (int i = 0; i < len; i++) { + char c = possibleToken.charAt(i); + if (!allowed.get(c)) { + return false; + } + } + return true; + } +} + +class RFC6265Validator extends CookieNameValidator { + private static final String RFC2616_SEPARATORS = "()<>@,;:\\\"/[]?={} \t"; + + RFC6265Validator() { + super(RFC2616_SEPARATORS); + } +} \ No newline at end of file diff --git a/java/jakarta/servlet/http/HttpFilter.java b/java/jakarta/servlet/http/HttpFilter.java new file mode 100644 index 0000000..b02a306 --- /dev/null +++ b/java/jakarta/servlet/http/HttpFilter.java @@ -0,0 +1,87 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +/** + * Provides a base class that implements the Filter interface and ensures that the Request and Response are of type + * HttpServletRequest and HttpServletResponse respectively. + */ +public abstract class HttpFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + /** + * {@inheritDoc} This implementation tests the request and response to see if they are instances of + * {@link HttpServletRequest} and {@link HttpServletResponse} respectively. If they are then they are passed to + * {@link #doFilter(HttpServletRequest, HttpServletResponse, FilterChain)}. If not, a {@link ServletException} is + * thrown. + * + * @throws ServletException If either the request or response are not of the expected types or any other error + * occurs + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (!(request instanceof HttpServletRequest)) { + throw new ServletException(request + " not HttpServletRequest"); + } + if (!(response instanceof HttpServletResponse)) { + throw new ServletException(response + " not HttpServletResponse"); + } + doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); + } + + + /** + * The doFilter method of the Filter is called by the container each time a request/response pair is + * passed through the chain due to a client request for a resource at the end of the chain. The FilterChain passed + * in to this method allows the Filter to pass on the request and response to the next entity in the chain. + *

+ * A typical implementation of this method would follow the following pattern:-
+ * 1. Examine the request
+ * 2. Optionally wrap the request object with a custom implementation to filter content or headers for input + * filtering
+ * 3. Optionally wrap the response object with a custom implementation to filter content or headers for output + * filtering
+ * 4. a) Either invoke the next entity in the chain using the FilterChain object + * (chain.doFilter()),
+ * 4. b) or not pass on the request/response pair to the next entity in the filter chain to block + * the request processing
+ * 5. Directly set headers on the response after invocation of the next entity in the filter chain. This default + * implementation simply calls the next filter in the filter chain. + * + * @param request The request to process + * @param response The response associated with the request + * @param chain Provides access to the next filter in the chain for this filter to pass the request and response + * to for further processing + * + * @throws IOException if an I/O error occurs during this filter's processing of the request + * @throws ServletException if the processing fails for any other reason + */ + protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + chain.doFilter(request, response); + } +} diff --git a/java/jakarta/servlet/http/HttpServlet.java b/java/jakarta/servlet/http/HttpServlet.java new file mode 100644 index 0000000..c184041 --- /dev/null +++ b/java/jakarta/servlet/http/HttpServlet.java @@ -0,0 +1,1072 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; + +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.GenericServlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.WriteListener; + + +/** + * Provides an abstract class to be subclassed to create an HTTP servlet suitable for a Web site. A subclass of + * HttpServlet must override at least one method, usually one of these: + *

    + *
  • doGet, if the servlet supports HTTP GET requests + *
  • doPost, for HTTP POST requests + *
  • doPut, for HTTP PUT requests + *
  • doDelete, for HTTP DELETE requests + *
  • init and destroy, to manage resources that are held for the life of the servlet + *
  • getServletInfo, which the servlet uses to provide information about itself + *
+ *

+ * There's almost no reason to override the service method. service handles standard HTTP + * requests by dispatching them to the handler methods for each HTTP request type (the doMethod + * methods listed above). + *

+ * Likewise, there's almost no reason to override the doOptions and doTrace methods. + *

+ * Servlets typically run on multithreaded servers, so be aware that a servlet must handle concurrent requests and be + * careful to synchronize access to shared resources. Shared resources include in-memory data such as instance or class + * variables and external objects such as files, database connections, and network connections. See the + * Java Tutorial on Multithreaded + * Programming for more information on handling multiple threads in a Java program. + */ +public abstract class HttpServlet extends GenericServlet { + + private static final long serialVersionUID = 1L; + + private static final String METHOD_DELETE = "DELETE"; + private static final String METHOD_HEAD = "HEAD"; + private static final String METHOD_GET = "GET"; + private static final String METHOD_OPTIONS = "OPTIONS"; + private static final String METHOD_POST = "POST"; + private static final String METHOD_PUT = "PUT"; + private static final String METHOD_TRACE = "TRACE"; + + private static final String HEADER_IFMODSINCE = "If-Modified-Since"; + private static final String HEADER_LASTMOD = "Last-Modified"; + + private static final String LSTRING_FILE = "jakarta.servlet.http.LocalStrings"; + private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + private static final List SENSITIVE_HTTP_HEADERS = + Arrays.asList("authorization", "cookie", "x-forwarded", "forwarded", "proxy-authorization"); + + /** + * @deprecated May be removed in a future release + * + * @since Servlet 6.0 + */ + @Deprecated(forRemoval = true, since = "Servlet 6.0") + public static final String LEGACY_DO_HEAD = "jakarta.servlet.http.legacyDoHead"; + + private final transient Object cachedAllowHeaderValueLock = new Object(); + + /** + * Cached value of the HTTP {@code Allow} header for this servlet. + */ + private volatile String cachedAllowHeaderValue = null; + + /** + * Cached value read from {@link HttpServlet#LEGACY_DO_HEAD} system property. + */ + private volatile boolean cachedUseLegacyDoHead; + + + /** + * Does nothing, because this is an abstract class. + */ + public HttpServlet() { + // NOOP + } + + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + cachedUseLegacyDoHead = Boolean.parseBoolean(config.getInitParameter(LEGACY_DO_HEAD)); + } + + + /** + * Called by the server (via the service method) to allow a servlet to handle a GET request. + *

+ * Overriding this method to support a GET request also automatically supports an HTTP HEAD request. A HEAD request + * is a GET request that returns no body in the response, only the request header fields. + *

+ * When overriding this method, read the request data, write the response headers, get the response's Writer or + * output stream object, and finally, write the response data. It's best to include content type and encoding. When + * using a PrintWriter object to return the response, set the content type before accessing the + * PrintWriter object. + *

+ * The servlet container must write the headers before committing the response, because in HTTP the headers must be + * sent before the response body. + *

+ * Where possible, set the Content-Length header (with the {@link jakarta.servlet.ServletResponse#setContentLength} + * method), to allow the servlet container to use a persistent connection to return its response to the client, + * improving performance. The content length is automatically set if the entire response fits inside the response + * buffer. + *

+ * When using HTTP 1.1 chunked encoding (which means that the response has a Transfer-Encoding header), do not set + * the Content-Length header. + *

+ * The GET method should be safe, that is, without any side effects for which users are held responsible. For + * example, most form queries have no side effects. If a client request is intended to change stored data, the + * request should use some other HTTP method. + *

+ * The GET method should also be idempotent, meaning that it can be safely repeated. Sometimes making a method safe + * also makes it idempotent. For example, repeating queries is both safe and idempotent, but buying a product online + * or modifying data is neither safe nor idempotent. + *

+ * If the request is incorrectly formatted, doGet returns an HTTP "Bad Request" message. + * + * @param req an {@link HttpServletRequest} object that contains the request the client has made of the servlet + * @param resp an {@link HttpServletResponse} object that contains the response the servlet sends to the client + * + * @exception IOException if an input or output error is detected when the servlet handles the GET request + * @exception ServletException if the request for the GET could not be handled + * + * @see jakarta.servlet.ServletResponse#setContentType + */ + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String msg = lStrings.getString("http.method_get_not_supported"); + sendMethodNotAllowed(req, resp, msg); + } + + + /** + * Returns the time the HttpServletRequest object was last modified, in milliseconds since midnight + * January 1, 1970 GMT. If the time is unknown, this method returns a negative number (the default). + *

+ * Servlets that support HTTP GET requests and can quickly determine their last modification time should override + * this method. This makes browser and proxy caches work more effectively, reducing the load on server and network + * resources. + * + * @param req the HttpServletRequest object that is sent to the servlet + * + * @return a long integer specifying the time the HttpServletRequest object was last + * modified, in milliseconds since midnight, January 1, 1970 GMT, or -1 if the time is not known + */ + protected long getLastModified(HttpServletRequest req) { + return -1; + } + + + /** + *

+ * Receives an HTTP HEAD request from the protected service method and handles the request. The client + * sends a HEAD request when it wants to see only the headers of a response, such as Content-Type or Content-Length. + * The HTTP HEAD method counts the output bytes in the response to set the Content-Length header accurately. + *

+ * If you override this method, you can avoid computing the response body and just set the response headers directly + * to improve performance. Make sure that the doHead method you write is both safe and idempotent (that + * is, protects itself from being called multiple times for one HTTP HEAD request). + *

+ * If the HTTP HEAD request is incorrectly formatted, doHead returns an HTTP "Bad Request" message. + * + * @param req the request object that is passed to the servlet + * @param resp the response object that the servlet uses to return the headers to the client + * + * @exception IOException if an input or output error occurs + * @exception ServletException if the request for the HEAD could not be handled + */ + protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (DispatcherType.INCLUDE.equals(req.getDispatcherType()) || !cachedUseLegacyDoHead) { + doGet(req, resp); + } else { + NoBodyResponse response = new NoBodyResponse(resp); + doGet(req, response); + if (req.isAsyncStarted()) { + req.getAsyncContext().addListener(new NoBodyAsyncContextListener(response)); + } else { + response.setContentLength(); + } + } + } + + + /** + * Called by the server (via the service method) to allow a servlet to handle a POST request. The HTTP + * POST method allows the client to send data of unlimited length to the Web server a single time and is useful when + * posting information such as credit card numbers. + *

+ * When overriding this method, read the request data, write the response headers, get the response's Writer or + * output stream object, and finally, write the response data. It's best to include content type and encoding. When + * using a PrintWriter object to return the response, set the content type before accessing the + * PrintWriter object. + *

+ * The servlet container must write the headers before committing the response, because in HTTP the headers must be + * sent before the response body. + *

+ * Where possible, set the Content-Length header (with the {@link jakarta.servlet.ServletResponse#setContentLength} + * method), to allow the servlet container to use a persistent connection to return its response to the client, + * improving performance. The content length is automatically set if the entire response fits inside the response + * buffer. + *

+ * When using HTTP 1.1 chunked encoding (which means that the response has a Transfer-Encoding header), do not set + * the Content-Length header. + *

+ * This method does not need to be either safe or idempotent. Operations requested through POST can have side + * effects for which the user can be held accountable, for example, updating stored data or buying items online. + *

+ * If the HTTP POST request is incorrectly formatted, doPost returns an HTTP "Bad Request" message. + * + * @param req an {@link HttpServletRequest} object that contains the request the client has made of the servlet + * @param resp an {@link HttpServletResponse} object that contains the response the servlet sends to the client + * + * @exception IOException if an input or output error is detected when the servlet handles the request + * @exception ServletException if the request for the POST could not be handled + * + * @see jakarta.servlet.ServletOutputStream + * @see jakarta.servlet.ServletResponse#setContentType + */ + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String msg = lStrings.getString("http.method_post_not_supported"); + sendMethodNotAllowed(req, resp, msg); + } + + + /** + * Called by the server (via the service method) to allow a servlet to handle a PUT request. The PUT + * operation allows a client to place a file on the server and is similar to sending a file by FTP. + *

+ * When overriding this method, leave intact any content headers sent with the request (including Content-Length, + * Content-Type, Content-Transfer-Encoding, Content-Encoding, Content-Base, Content-Language, Content-Location, + * Content-MD5, and Content-Range). If your method cannot handle a content header, it must issue an error message + * (HTTP 501 - Not Implemented) and discard the request. For more information on HTTP 1.1, see RFC 2616 + * . + *

+ * This method does not need to be either safe or idempotent. Operations that doPut performs can have + * side effects for which the user can be held accountable. When using this method, it may be useful to save a copy + * of the affected URL in temporary storage. + *

+ * If the HTTP PUT request is incorrectly formatted, doPut returns an HTTP "Bad Request" message. + * + * @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet + * @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client + * + * @exception IOException if an input or output error occurs while the servlet is handling the PUT request + * @exception ServletException if the request for the PUT cannot be handled + */ + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String msg = lStrings.getString("http.method_put_not_supported"); + sendMethodNotAllowed(req, resp, msg); + } + + + /** + * Called by the server (via the service method) to allow a servlet to handle a DELETE request. The + * DELETE operation allows a client to remove a document or Web page from the server. + *

+ * This method does not need to be either safe or idempotent. Operations requested through DELETE can have side + * effects for which users can be held accountable. When using this method, it may be useful to save a copy of the + * affected URL in temporary storage. + *

+ * If the HTTP DELETE request is incorrectly formatted, doDelete returns an HTTP "Bad Request" message. + * + * @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet + * @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client + * + * @exception IOException if an input or output error occurs while the servlet is handling the DELETE request + * @exception ServletException if the request for the DELETE cannot be handled + */ + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String msg = lStrings.getString("http.method_delete_not_supported"); + sendMethodNotAllowed(req, resp, msg); + } + + + private void sendMethodNotAllowed(HttpServletRequest req, HttpServletResponse resp, String msg) throws IOException { + String protocol = req.getProtocol(); + // Note: Tomcat reports "" for HTTP/0.9 although some implementations + // may report HTTP/0.9 + if (protocol.length() == 0 || protocol.endsWith("0.9") || protocol.endsWith("1.0")) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); + } else { + resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); + } + } + + + private String getCachedAllowHeaderValue() { + if (cachedAllowHeaderValue == null) { + synchronized (cachedAllowHeaderValueLock) { + if (cachedAllowHeaderValue == null) { + + Method[] methods = getAllDeclaredMethods(this.getClass()); + + // RFC 7230 does not define an order for this header + // This code aims to retain, broadly, the order of method + // tokens returned in earlier versions of this code. If that + // constraint is dropped then the code can be simplified + // further. + + boolean allowGet = false; + boolean allowHead = false; + boolean allowPost = false; + boolean allowPut = false; + boolean allowDelete = false; + + for (Method method : methods) { + switch (method.getName()) { + case "doGet": { + allowGet = true; + allowHead = true; + break; + } + case "doPost": { + allowPost = true; + break; + } + case "doPut": { + allowPut = true; + break; + } + case "doDelete": { + allowDelete = true; + break; + } + default: + // NO-OP + } + + } + + StringBuilder allow = new StringBuilder(); + + if (allowGet) { + allow.append(METHOD_GET); + allow.append(", "); + } + + if (allowHead) { + allow.append(METHOD_HEAD); + allow.append(", "); + } + + if (allowPost) { + allow.append(METHOD_POST); + allow.append(", "); + } + + if (allowPut) { + allow.append(METHOD_PUT); + allow.append(", "); + } + + if (allowDelete) { + allow.append(METHOD_DELETE); + allow.append(", "); + } + + // Options is always allowed + allow.append(METHOD_OPTIONS); + + cachedAllowHeaderValue = allow.toString(); + } + } + } + + return cachedAllowHeaderValue; + } + + + private static Method[] getAllDeclaredMethods(Class c) { + + if (c.equals(HttpServlet.class)) { + return null; + } + + Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass()); + Method[] thisMethods = c.getDeclaredMethods(); + + if ((parentMethods != null) && (parentMethods.length > 0)) { + Method[] allMethods = new Method[parentMethods.length + thisMethods.length]; + System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length); + System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length); + thisMethods = allMethods; + } + + return thisMethods; + } + + + /** + * Called by the server (via the service method) to allow a servlet to handle an OPTIONS request. The + * OPTIONS request determines which HTTP methods the server supports and returns an appropriate header. For example, + * if a servlet overrides doGet, this method returns the following header: + *

+ * Allow: GET, HEAD, TRACE, OPTIONS + *

+ * There's no need to override this method unless the servlet implements new HTTP methods, beyond those implemented + * by HTTP 1.1. + * + * @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet + * @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client + * + * @exception IOException if an input or output error occurs while the servlet is handling the OPTIONS request + * @exception ServletException if the request for the OPTIONS cannot be handled + */ + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String allow = getCachedAllowHeaderValue(); + + // Tomcat specific hack to see if TRACE is allowed + if (TomcatHack.getAllowTrace(req)) { + if (allow.length() == 0) { + allow = METHOD_TRACE; + } else { + allow = allow + ", " + METHOD_TRACE; + } + } + + resp.setHeader("Allow", allow); + } + + + /** + * Called by the server (via the service method) to allow a servlet to handle a TRACE request. A TRACE + * returns the headers sent with the TRACE request to the client, so that they can be used in debugging. There's no + * need to override this method. + * + * @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet + * @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client + * + * @exception IOException if an input or output error occurs while the servlet is handling the TRACE request + * @exception ServletException if the request for the TRACE cannot be handled + */ + protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + int responseLength; + + String CRLF = "\r\n"; + StringBuilder buffer = + new StringBuilder("TRACE ").append(req.getRequestURI()).append(' ').append(req.getProtocol()); + + Enumeration reqHeaderNames = req.getHeaderNames(); + + while (reqHeaderNames.hasMoreElements()) { + String headerName = reqHeaderNames.nextElement(); + // RFC 7231, 4.3.8 - skip 'sensitive' headers + if (!isSensitiveHeader(headerName)) { + Enumeration headerValues = req.getHeaders(headerName); + while (headerValues.hasMoreElements()) { + String headerValue = headerValues.nextElement(); + buffer.append(CRLF).append(headerName).append(": ").append(headerValue); + } + } + } + + buffer.append(CRLF); + + responseLength = buffer.length(); + + resp.setContentType("message/http"); + resp.setContentLength(responseLength); + ServletOutputStream out = resp.getOutputStream(); + out.print(buffer.toString()); + out.close(); + } + + + /** + * Is the provided HTTP request header considered sensitive and therefore should be excluded from the response to a + * {@code TRACE} request? + *

+ * By default, the headers that start with any of the following are considered sensitive: + *

    + *
  • authorization
  • + *
  • cookie
  • + *
  • x-forwarded
  • + *
  • forwarded
  • + *
  • proxy-authorization
  • + *
+ *

+ * Note that HTTP header names are case insensitive. + * + * @param headerName the name of the HTTP request header to test + * + * @return (@code true} if the HTTP request header is considered sensitive and should be excluded from the response + * to a {@code TRACE} request, otherwise {@code false} + */ + private boolean isSensitiveHeader(String headerName) { + String lcHeaderName = headerName.toLowerCase(Locale.ENGLISH); + for (String sensitiveHeaderName : SENSITIVE_HTTP_HEADERS) { + if (lcHeaderName.startsWith(sensitiveHeaderName)) { + return true; + } + } + return false; + } + + + /** + * Receives standard HTTP requests from the public service method and dispatches them to the + * doMethod methods defined in this class. This method is an HTTP-specific version of the + * {@link jakarta.servlet.Servlet#service} method. There's no need to override this method. + * + * @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet + * @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client + * + * @exception IOException if an input or output error occurs while the servlet is handling the HTTP request + * @exception ServletException if the HTTP request cannot be handled + * + * @see jakarta.servlet.Servlet#service + */ + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String method = req.getMethod(); + + if (method.equals(METHOD_GET)) { + long lastModified = getLastModified(req); + if (lastModified == -1) { + // servlet doesn't support if-modified-since, no reason + // to go through further expensive logic + doGet(req, resp); + } else { + long ifModifiedSince; + try { + ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); + } catch (IllegalArgumentException iae) { + // Invalid date header - proceed as if none was set + ifModifiedSince = -1; + } + if (ifModifiedSince < (lastModified / 1000 * 1000)) { + // If the servlet mod time is later, call doGet() + // Round down to the nearest second for a proper compare + // A ifModifiedSince of -1 will always be less + maybeSetLastModified(resp, lastModified); + doGet(req, resp); + } else { + resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + } + + } else if (method.equals(METHOD_HEAD)) { + long lastModified = getLastModified(req); + maybeSetLastModified(resp, lastModified); + doHead(req, resp); + + } else if (method.equals(METHOD_POST)) { + doPost(req, resp); + + } else if (method.equals(METHOD_PUT)) { + doPut(req, resp); + + } else if (method.equals(METHOD_DELETE)) { + doDelete(req, resp); + + } else if (method.equals(METHOD_OPTIONS)) { + doOptions(req, resp); + + } else if (method.equals(METHOD_TRACE)) { + doTrace(req, resp); + + } else { + // + // Note that this means NO servlet supports whatever + // method was requested, anywhere on this server. + // + + String errMsg = lStrings.getString("http.method_not_implemented"); + Object[] errArgs = new Object[1]; + errArgs[0] = method; + errMsg = MessageFormat.format(errMsg, errArgs); + + resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); + } + } + + + /* + * Sets the Last-Modified entity header field, if it has not already been set and if the value is meaningful. Called + * before doGet, to ensure that headers are set before response data is written. A subclass might have set this + * header already, so we check. + */ + private void maybeSetLastModified(HttpServletResponse resp, long lastModified) { + if (resp.containsHeader(HEADER_LASTMOD)) { + return; + } + if (lastModified >= 0) { + resp.setDateHeader(HEADER_LASTMOD, lastModified); + } + } + + + /** + * Dispatches client requests to the protected service method. There's no need to override this method. + * + * @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet + * @param res the {@link HttpServletResponse} object that contains the response the servlet returns to the client + * + * @exception IOException if an input or output error occurs while the servlet is handling the HTTP request + * @exception ServletException if the HTTP request cannot be handled + * + * @see jakarta.servlet.Servlet#service + */ + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + + HttpServletRequest request; + HttpServletResponse response; + + try { + request = (HttpServletRequest) req; + response = (HttpServletResponse) res; + } catch (ClassCastException e) { + throw new ServletException(lStrings.getString("http.non_http")); + } + service(request, response); + } + + + private static class TomcatHack { + + private static final Class REQUEST_FACADE_CLAZZ; + private static final Method GET_ALLOW_TRACE; + + + static { + Method m1 = null; + Class c1 = null; + try { + c1 = Class.forName("org.apache.catalina.connector.RequestFacade"); + m1 = c1.getMethod("getAllowTrace", (Class[]) null); + } catch (ReflectiveOperationException | SecurityException | IllegalArgumentException e) { + // Ignore. Not running on Tomcat. TRACE is always allowed. + } + REQUEST_FACADE_CLAZZ = c1; + GET_ALLOW_TRACE = m1; + } + + public static boolean getAllowTrace(HttpServletRequest req) { + if (REQUEST_FACADE_CLAZZ != null && GET_ALLOW_TRACE != null) { + if (REQUEST_FACADE_CLAZZ.isAssignableFrom(req.getClass())) { + try { + return ((Boolean) GET_ALLOW_TRACE.invoke(req, (Object[]) null)).booleanValue(); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + // Should never happen given the checks in place. + // Ignore + } + } + } + return true; + } + } + + + /* + * A response wrapper for use in (dumb) "HEAD" support. This just swallows that body, counting the bytes in order to + * set the content length appropriately. All other methods delegate to the wrapped HTTP Servlet Response object. + */ + private static class NoBodyResponse extends HttpServletResponseWrapper { + private final NoBodyOutputStream noBodyOutputStream; + private ServletOutputStream originalOutputStream; + private NoBodyPrintWriter noBodyWriter; + private boolean didSetContentLength; + + private NoBodyResponse(HttpServletResponse r) { + super(r); + noBodyOutputStream = new NoBodyOutputStream(this); + } + + private void setContentLength() { + if (!didSetContentLength) { + if (noBodyWriter != null) { + noBodyWriter.flush(); + } + super.setContentLengthLong(noBodyOutputStream.getWrittenByteCount()); + } + } + + + @Override + public void setContentLength(int len) { + super.setContentLength(len); + didSetContentLength = true; + } + + @Override + public void setContentLengthLong(long len) { + super.setContentLengthLong(len); + didSetContentLength = true; + } + + @Override + public void setHeader(String name, String value) { + super.setHeader(name, value); + checkHeader(name); + } + + @Override + public void addHeader(String name, String value) { + super.addHeader(name, value); + checkHeader(name); + } + + @Override + public void setIntHeader(String name, int value) { + super.setIntHeader(name, value); + checkHeader(name); + } + + @Override + public void addIntHeader(String name, int value) { + super.addIntHeader(name, value); + checkHeader(name); + } + + private void checkHeader(String name) { + if ("content-length".equalsIgnoreCase(name)) { + didSetContentLength = true; + } + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + originalOutputStream = getResponse().getOutputStream(); + return noBodyOutputStream; + } + + @Override + public PrintWriter getWriter() throws UnsupportedEncodingException { + + if (noBodyWriter == null) { + noBodyWriter = new NoBodyPrintWriter(noBodyOutputStream, getCharacterEncoding()); + } + return noBodyWriter; + } + + @Override + public void reset() { + super.reset(); + resetBuffer(); + originalOutputStream = null; + } + + @Override + public void resetBuffer() { + noBodyOutputStream.resetBuffer(); + if (noBodyWriter != null) { + noBodyWriter.resetBuffer(); + } + } + } + + + /* + * Servlet output stream that gobbles up all its data. + */ + private static class NoBodyOutputStream extends ServletOutputStream { + + private static final String LSTRING_FILE = "jakarta.servlet.http.LocalStrings"; + private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + private final NoBodyResponse response; + private boolean flushed = false; + private long writtenByteCount = 0; + + private NoBodyOutputStream(NoBodyResponse response) { + this.response = response; + } + + private long getWrittenByteCount() { + return writtenByteCount; + } + + @Override + public void write(int b) throws IOException { + writtenByteCount++; + checkCommit(); + } + + @Override + public void write(byte buf[], int offset, int len) throws IOException { + if (buf == null) { + throw new NullPointerException(lStrings.getString("err.io.nullArray")); + } + + if (offset < 0 || len < 0 || offset + len > buf.length) { + String msg = lStrings.getString("err.io.indexOutOfBounds"); + Object[] msgArgs = new Object[3]; + msgArgs[0] = Integer.valueOf(offset); + msgArgs[1] = Integer.valueOf(len); + msgArgs[2] = Integer.valueOf(buf.length); + msg = MessageFormat.format(msg, msgArgs); + throw new IndexOutOfBoundsException(msg); + } + + writtenByteCount += len; + checkCommit(); + } + + @Override + public boolean isReady() { + // Will always be ready as data is swallowed. + return true; + } + + @Override + public void setWriteListener(WriteListener listener) { + response.originalOutputStream.setWriteListener(listener); + } + + private void checkCommit() throws IOException { + if (!flushed && writtenByteCount > response.getBufferSize()) { + response.flushBuffer(); + flushed = true; + } + } + + private void resetBuffer() { + if (flushed) { + throw new IllegalStateException(lStrings.getString("err.state.commit")); + } + writtenByteCount = 0; + } + } + + + /* + * On reset() and resetBuffer() need to clear the data buffered in the OutputStreamWriter. No easy way to do that so + * NoBodyPrintWriter wraps a PrintWriter than can be thrown away on reset()/resetBuffer() and a new one constructed + * while the application retains a reference to the NoBodyPrintWriter instance. + */ + private static class NoBodyPrintWriter extends PrintWriter { + + private final NoBodyOutputStream out; + private final String encoding; + private PrintWriter pw; + + NoBodyPrintWriter(NoBodyOutputStream out, String encoding) throws UnsupportedEncodingException { + super(out); + this.out = out; + this.encoding = encoding; + + Writer osw = new OutputStreamWriter(out, encoding); + pw = new PrintWriter(osw); + } + + private void resetBuffer() { + out.resetBuffer(); + + Writer osw = null; + try { + osw = new OutputStreamWriter(out, encoding); + } catch (UnsupportedEncodingException e) { + // Impossible. + // The same values were used in the constructor. If this method + // gets called then the constructor must have succeeded so the + // above call must also succeed. + } + pw = new PrintWriter(osw); + } + + @Override + public void flush() { + pw.flush(); + } + + @Override + public void close() { + pw.close(); + } + + @Override + public boolean checkError() { + return pw.checkError(); + } + + @Override + public void write(int c) { + pw.write(c); + } + + @Override + public void write(char[] buf, int off, int len) { + pw.write(buf, off, len); + } + + @Override + public void write(char[] buf) { + pw.write(buf); + } + + @Override + public void write(String s, int off, int len) { + pw.write(s, off, len); + } + + @Override + public void write(String s) { + pw.write(s); + } + + @Override + public void print(boolean b) { + pw.print(b); + } + + @Override + public void print(char c) { + pw.print(c); + } + + @Override + public void print(int i) { + pw.print(i); + } + + @Override + public void print(long l) { + pw.print(l); + } + + @Override + public void print(float f) { + pw.print(f); + } + + @Override + public void print(double d) { + pw.print(d); + } + + @Override + public void print(char[] s) { + pw.print(s); + } + + @Override + public void print(String s) { + pw.print(s); + } + + @Override + public void print(Object obj) { + pw.print(obj); + } + + @Override + public void println() { + pw.println(); + } + + @Override + public void println(boolean x) { + pw.println(x); + } + + @Override + public void println(char x) { + pw.println(x); + } + + @Override + public void println(int x) { + pw.println(x); + } + + @Override + public void println(long x) { + pw.println(x); + } + + @Override + public void println(float x) { + pw.println(x); + } + + @Override + public void println(double x) { + pw.println(x); + } + + @Override + public void println(char[] x) { + pw.println(x); + } + + @Override + public void println(String x) { + pw.println(x); + } + + @Override + public void println(Object x) { + pw.println(x); + } + } + + + /* + * Calls NoBodyResponse.setContentLength() once the async request is complete. + */ + private static class NoBodyAsyncContextListener implements AsyncListener { + + private final NoBodyResponse noBodyResponse; + + NoBodyAsyncContextListener(NoBodyResponse noBodyResponse) { + this.noBodyResponse = noBodyResponse; + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + noBodyResponse.setContentLength(); + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onError(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + // NO-OP + } + } +} diff --git a/java/jakarta/servlet/http/HttpServletMapping.java b/java/jakarta/servlet/http/HttpServletMapping.java new file mode 100644 index 0000000..ae88d32 --- /dev/null +++ b/java/jakarta/servlet/http/HttpServletMapping.java @@ -0,0 +1,49 @@ +/* + * 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 jakarta.servlet.http; + +import jakarta.servlet.annotation.WebServlet; + +/** + * Represents how the request from which this object was obtained was mapped to the associated servlet. + * + * @since Servlet 4.0 + */ +public interface HttpServletMapping { + + /** + * @return The value that was matched or the empty String if not known. + */ + String getMatchValue(); + + /** + * @return The {@code url-pattern} that matched this request or the empty String if not known. + */ + String getPattern(); + + /** + * @return The name of the servlet (as specified in web.xml, {@link WebServlet#name()}, + * {@link jakarta.servlet.ServletContext#addServlet(String, Class)} or one of the other + * addServlet() methods) that the request was mapped to. + */ + String getServletName(); + + /** + * @return The type of match ({@code null} if not known) + */ + MappingMatch getMappingMatch(); +} diff --git a/java/jakarta/servlet/http/HttpServletRequest.java b/java/jakarta/servlet/http/HttpServletRequest.java new file mode 100644 index 0000000..7bb78cb --- /dev/null +++ b/java/jakarta/servlet/http/HttpServletRequest.java @@ -0,0 +1,542 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; + +/** + * Extends the {@link jakarta.servlet.ServletRequest} interface to provide request information for HTTP servlets. + *

+ * The servlet container creates an HttpServletRequest object and passes it as an argument to the servlet's + * service methods (doGet, doPost, etc). + */ +public interface HttpServletRequest extends ServletRequest { + + /** + * String identifier for Basic authentication. Value "BASIC" + */ + String BASIC_AUTH = "BASIC"; + /** + * String identifier for Form authentication. Value "FORM" + */ + String FORM_AUTH = "FORM"; + /** + * String identifier for Client Certificate authentication. Value "CLIENT_CERT" + */ + String CLIENT_CERT_AUTH = "CLIENT_CERT"; + /** + * String identifier for Digest authentication. Value "DIGEST" + */ + String DIGEST_AUTH = "DIGEST"; + + /** + * Returns the name of the authentication scheme used to protect the servlet. All servlet containers support basic, + * form and client certificate authentication, and may additionally support digest authentication. If the servlet is + * not authenticated null is returned. + *

+ * Same as the value of the CGI variable AUTH_TYPE. + * + * @return one of the static members BASIC_AUTH, FORM_AUTH, CLIENT_CERT_AUTH, DIGEST_AUTH (suitable for == + * comparison) or the container-specific string indicating the authentication scheme, or + * null if the request was not authenticated. + */ + String getAuthType(); + + /** + * Returns an array containing all of the Cookie objects the client sent with this request. This method + * returns null if no cookies were sent. + * + * @return an array of all the Cookies included with this request, or null if the request + * has no cookies + */ + Cookie[] getCookies(); + + /** + * Returns the value of the specified request header as a long value that represents a + * Date object. Use this method with headers that contain dates, such as + * If-Modified-Since. + *

+ * The date is returned as the number of milliseconds since January 1, 1970 GMT. The header name is case + * insensitive. + *

+ * If the request did not have a header of the specified name, this method returns -1. If the header can't be + * converted to a date, the method throws an IllegalArgumentException. + * + * @param name a String specifying the name of the header + * + * @return a long value representing the date specified in the header expressed as the number of + * milliseconds since January 1, 1970 GMT, or -1 if the named header was not included with the request + * + * @exception IllegalArgumentException If the header value can't be converted to a date + */ + long getDateHeader(String name); + + /** + * Returns the value of the specified request header as a String. If the request did not include a + * header of the specified name, this method returns null. If there are multiple headers with the same + * name, this method returns the first head in the request. The header name is case insensitive. You can use this + * method with any request header. + * + * @param name a String specifying the header name + * + * @return a String containing the value of the requested header, or null if the request + * does not have a header of that name + */ + String getHeader(String name); + + /** + * Returns all the values of the specified request header as an Enumeration of String + * objects. + *

+ * Some headers, such as Accept-Language can be sent by clients as several headers each with a + * different value rather than sending the header as a comma separated list. + *

+ * If the request did not include any headers of the specified name, this method returns an empty + * Enumeration. The header name is case insensitive. You can use this method with any request header. + * + * @param name a String specifying the header name + * + * @return an Enumeration containing the values of the requested header. If the request does not have + * any headers of that name return an empty enumeration. If the container does not allow access to + * header information, return null + */ + Enumeration getHeaders(String name); + + /** + * Returns an enumeration of all the header names this request contains. If the request has no headers, this method + * returns an empty enumeration. + *

+ * Some servlet containers do not allow servlets to access headers using this method, in which case this method + * returns null + * + * @return an enumeration of all the header names sent with this request; if the request has no headers, an empty + * enumeration; if the servlet container does not allow servlets to use this method, null + */ + Enumeration getHeaderNames(); + + /** + * Returns the value of the specified request header as an int. If the request does not have a header + * of the specified name, this method returns -1. If the header cannot be converted to an integer, this method + * throws a NumberFormatException. + *

+ * The header name is case insensitive. + * + * @param name a String specifying the name of a request header + * + * @return an integer expressing the value of the request header or -1 if the request doesn't have a header of this + * name + * + * @exception NumberFormatException If the header value can't be converted to an int + */ + int getIntHeader(String name); + + /** + * Obtain the mapping information for this request. + * + * @return the mapping information for this request + */ + default HttpServletMapping getHttpServletMapping() { + return new HttpServletMapping() { + + @Override + public String getMatchValue() { + return ""; + } + + @Override + public String getPattern() { + return ""; + } + + @Override + public String getServletName() { + return ""; + } + + @Override + public MappingMatch getMappingMatch() { + return null; + } + }; + } + + /** + * Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT. Same as the + * value of the CGI variable REQUEST_METHOD. + * + * @return a String specifying the name of the method with which this request was made + */ + String getMethod(); + + /** + * Returns any extra path information associated with the URL the client sent when it made this request. The extra + * path information follows the servlet path but precedes the query string and will start with a "/" character. + *

+ * This method returns null if there was no extra path information. + *

+ * The URL will be canonicalized as per section 3.5 of the specification before the path information, if any, is + * extracted. + * + * @return a String, canonicalized by the web container, specifying extra path information that comes + * after the servlet path but before the query string in the request URL; or {@code null} if the URL + * does not have any extra path information + */ + String getPathInfo(); + + /** + * Returns any extra path information after the servlet name but before the query string, and translates it to a + * real path. Same as the value of the CGI variable PATH_TRANSLATED. + *

+ * If the URL does not have any extra path information, this method returns null or the servlet + * container cannot translate the virtual path to a real path for any reason (such as when the web application is + * executed from an archive). The web container does not decode this string. + * + * @return a String specifying the real path, or null if the URL does not have any extra + * path information + */ + String getPathTranslated(); + + /** + * Obtain a builder for generating push requests. {@link PushBuilder} documents how this request will be used as the + * basis for a push request. Each call to this method will return a new instance, independent of any previous + * instance obtained. + * + * @return A builder that can be used to generate push requests based on this request or {@code null} if push is not + * supported. Note that even if a PushBuilder instance is returned, by the time that + * {@link PushBuilder#push()} is called, it may no longer be valid to push a request and the push + * request will be ignored. + * + * @since Servlet 4.0 + */ + default PushBuilder newPushBuilder() { + return null; + } + + /** + * Returns the portion of the request URI that indicates the context of the request. The context path always comes + * first in a request URI. The path starts with a "/" character but does not end with a "/" character. For servlets + * in the default (root) context, this method returns "". The container does not decode this string. + * + * @return a String specifying the portion of the request URI that indicates the context of the request + */ + String getContextPath(); + + /** + * Returns the query string that is contained in the request URL after the path. This method returns + * null if the URL does not have a query string. Same as the value of the CGI variable QUERY_STRING. + * + * @return a String containing the query string or null if the URL contains no query + * string. The value is not decoded by the container. + */ + String getQueryString(); + + /** + * Returns the login of the user making this request, if the user has been authenticated, or null if + * the user has not been authenticated. Whether the user name is sent with each subsequent request depends on the + * browser and type of authentication. Same as the value of the CGI variable REMOTE_USER. + * + * @return a String specifying the login of the user making this request, or null if the + * user login is not known + */ + String getRemoteUser(); + + /** + * Returns a boolean indicating whether the authenticated user is included in the specified logical "role". Roles + * and role membership can be defined using deployment descriptors. If the user has not been authenticated, the + * method returns false. + * + * @param role a String specifying the name of the role + * + * @return a boolean indicating whether the user making this request belongs to a given role; + * false if the user has not been authenticated + */ + boolean isUserInRole(String role); + + /** + * Returns a java.security.Principal object containing the name of the current authenticated user. If + * the user has not been authenticated, the method returns null. + * + * @return a java.security.Principal containing the name of the user making this request; + * null if the user has not been authenticated + */ + java.security.Principal getUserPrincipal(); + + /** + * Returns the session ID specified by the client. This may not be the same as the ID of the current valid session + * for this request. If the client did not specify a session ID, this method returns null. + * + * @return a String specifying the session ID, or null if the request did not specify a + * session ID + * + * @see #isRequestedSessionIdValid + */ + String getRequestedSessionId(); + + /** + * Returns the part of this request's URL from the protocol name up to the query string in the first line of the + * HTTP request. The web container does not decode this String. For example: + * + * + * + * + * + * + * + * + *
Examples of Returned Values
First line of HTTP requestReturned Value
POST /some/path.html HTTP/1.1 + * + * /some/path.html + *
GET http://foo.bar/a.html HTTP/1.0 + * + * /a.html + *
HEAD /xyz?a=b HTTP/1.1 + * + * /xyz + *
+ *

+ * To reconstruct a URL with a scheme and host, use {@link #getRequestURL}. + * + * @return a String containing the part of the URL from the protocol name up to the query string + * + * @see #getRequestURL + */ + String getRequestURI(); + + /** + * Reconstructs the URL the client used to make the request. The returned URL contains a protocol, server name, port + * number, and server path, but it does not include query string parameters. + *

+ * Because this method returns a StringBuffer, not a string, you can modify the URL easily, for + * example, to append query parameters. + *

+ * This method is useful for creating redirect messages and for reporting errors. + * + * @return a StringBuffer object containing the reconstructed URL + */ + StringBuffer getRequestURL(); + + /** + * Returns the part of this request's URL that calls the servlet. This path starts with a "/" character and includes + * either the servlet name or a path to the servlet, but does not include any extra path information or a query + * string. Same as the value of the CGI variable SCRIPT_NAME. + *

+ * The URL will be canonicalized as per section 3.5 of the specification before the path information, if any, is + * extracted. + *

+ * This method will return an empty string ("") if the servlet used to process this request was matched using the + * "/*" pattern. + * + * @return a String, canonicalized by the web container, containing the name or path of the servlet + * being called, as specified in the request URL, or an empty string if the servlet used to process the + * request is matched using the "/*" pattern. + */ + String getServletPath(); + + /** + * Returns the current HttpSession associated with this request or, if there is no current session and + * create is true, returns a new session. + *

+ * If create is false and the request has no valid HttpSession, this method + * returns null. + *

+ * To make sure the session is properly maintained, you must call this method before the response is committed. If + * the container is using cookies to maintain session integrity and is asked to create a new session when the + * response is committed, an IllegalStateException is thrown. + * + * @param create true to create a new session for this request if necessary; false to + * return null if there's no current session + * + * @return the HttpSession associated with this request or null if create is + * false and the request has no valid session + * + * @see #getSession() + */ + HttpSession getSession(boolean create); + + /** + * Returns the current session associated with this request, or if the request does not have a session, creates one. + * + * @return the HttpSession associated with this request + * + * @see #getSession(boolean) + */ + HttpSession getSession(); + + /** + * Changes the session ID of the session associated with this request. This method does not create a new session + * object it only changes the ID of the current session. + * + * @return the new session ID allocated to the session + * + * @see HttpSessionIdListener + * + * @since Servlet 3.1 + */ + String changeSessionId(); + + /** + * Checks whether the requested session ID is still valid. + * + * @return true if this request has an id for a valid session in the current session context; + * false otherwise + * + * @see #getRequestedSessionId + * @see #getSession + */ + boolean isRequestedSessionIdValid(); + + /** + * Checks whether the requested session ID came in as a cookie. + * + * @return true if the session ID came in as a cookie; otherwise, false + * + * @see #getSession + */ + boolean isRequestedSessionIdFromCookie(); + + /** + * Checks whether the requested session ID came in as part of the request URL. + * + * @return true if the session ID came in as part of a URL; otherwise, false + * + * @see #getSession + */ + boolean isRequestedSessionIdFromURL(); + + /** + * Triggers the same authentication process as would be triggered if the request is for a resource that is protected + * by a security constraint. + * + * @param response The response to use to return any authentication challenge + * + * @return true if the user is successfully authenticated and false if not + * + * @throws IOException if the authentication process attempted to read from the request or write to the + * response and an I/O error occurred + * @throws IllegalStateException if the authentication process attempted to write to the response after it had been + * committed + * @throws ServletException if the authentication failed and the caller is expected to handle the failure + * + * @since Servlet 3.0 + */ + boolean authenticate(HttpServletResponse response) throws IOException, ServletException; + + /** + * Authenticate the provided user name and password and then associated the authenticated user with the request. + * + * @param username The user name to authenticate + * @param password The password to use to authenticate the user + * + * @throws ServletException If any of {@link #getRemoteUser()}, {@link #getUserPrincipal()} or + * {@link #getAuthType()} are non-null, if the configured authenticator does not + * support user name and password authentication or if the authentication fails + * + * @since Servlet 3.0 + */ + void login(String username, String password) throws ServletException; + + /** + * Removes any authenticated user from the request. + * + * @throws ServletException If the logout fails + * + * @since Servlet 3.0 + */ + void logout() throws ServletException; + + /** + * Return a collection of all uploaded Parts. + * + * @return A collection of all uploaded Parts. + * + * @throws IOException if an I/O error occurs + * @throws IllegalStateException if size limits are exceeded or no multipart configuration is provided + * @throws ServletException if the request is not multipart/form-data + * + * @since Servlet 3.0 + */ + Collection getParts() throws IOException, ServletException; + + /** + * Gets the named Part or null if the Part does not exist. Triggers upload of all Parts. + * + * @param name The name of the Part to obtain + * + * @return The named Part or null if the Part does not exist + * + * @throws IOException if an I/O error occurs + * @throws IllegalStateException if size limits are exceeded + * @throws ServletException if the request is not multipart/form-data + * + * @since Servlet 3.0 + */ + Part getPart(String name) throws IOException, ServletException; + + /** + * Start the HTTP upgrade process and create and instance of the provided protocol handler class. The connection + * will be passed this instance once the current request/response pair has completed processing. Calling this method + * sets the response status to {@link HttpServletResponse#SC_SWITCHING_PROTOCOLS}. + * + * @param The type of the upgrade handler + * @param httpUpgradeHandlerClass The class that implements the upgrade handler + * + * @return A newly created instance of the specified upgrade handler type + * + * @throws IOException if an I/O error occurred during the upgrade + * @throws ServletException if the given httpUpgradeHandlerClass fails to be instantiated + * + * @since Servlet 3.1 + */ + T upgrade(Class httpUpgradeHandlerClass) + throws IOException, ServletException; + + /** + * Obtain a Map of the trailer fields that is not backed by the request object. + * + * @return A Map of the received trailer fields with all keys lower case or an empty Map if no trailers are present + * + * @since Servlet 4.0 + */ + default Map getTrailerFields() { + return Collections.emptyMap(); + } + + /** + * Are trailer fields ready to be read (there may still be no trailers to read). This method always returns + * {@code true} if the underlying protocol does not support trailer fields. Otherwise, {@code true} is returned once + * all of the following are true: + *

    + *
  • The application has ready all the request data and an EOF has been received or the content-length is + * zero
  • + *
  • All trailer fields, if any, have been received
  • + *
+ * + * @return {@code true} if trailers are ready to be read + * + * @since Servlet 4.0 + */ + default boolean isTrailerFieldsReady() { + return true; + } +} diff --git a/java/jakarta/servlet/http/HttpServletRequestWrapper.java b/java/jakarta/servlet/http/HttpServletRequestWrapper.java new file mode 100644 index 0000000..766488b --- /dev/null +++ b/java/jakarta/servlet/http/HttpServletRequestWrapper.java @@ -0,0 +1,384 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequestWrapper; + +/** + * Provides a convenient implementation of the HttpServletRequest interface that can be subclassed by developers wishing + * to adapt the request to a Servlet. This class implements the Wrapper or Decorator pattern. Methods default to calling + * through to the wrapped request object. + * + * @see jakarta.servlet.http.HttpServletRequest + * + * @since Servlet 2.3 + */ +public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest { + + /** + * Constructs a request object wrapping the given request. + * + * @param request The request to wrap + * + * @throws java.lang.IllegalArgumentException if the request is null + */ + public HttpServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + private HttpServletRequest _getHttpServletRequest() { + return (HttpServletRequest) super.getRequest(); + } + + /** + * The default behavior of this method is to return getAuthType() on the wrapped request object. + */ + @Override + public String getAuthType() { + return this._getHttpServletRequest().getAuthType(); + } + + /** + * The default behavior of this method is to return getCookies() on the wrapped request object. + */ + @Override + public Cookie[] getCookies() { + return this._getHttpServletRequest().getCookies(); + } + + /** + * The default behavior of this method is to return getDateHeader(String name) on the wrapped request object. + */ + @Override + public long getDateHeader(String name) { + return this._getHttpServletRequest().getDateHeader(name); + } + + /** + * The default behavior of this method is to return getHeader(String name) on the wrapped request object. + */ + @Override + public String getHeader(String name) { + return this._getHttpServletRequest().getHeader(name); + } + + /** + * The default behavior of this method is to return getHeaders(String name) on the wrapped request object. + */ + @Override + public Enumeration getHeaders(String name) { + return this._getHttpServletRequest().getHeaders(name); + } + + /** + * The default behavior of this method is to return getHeaderNames() on the wrapped request object. + */ + @Override + public Enumeration getHeaderNames() { + return this._getHttpServletRequest().getHeaderNames(); + } + + /** + * The default behavior of this method is to return getIntHeader(String name) on the wrapped request object. + */ + @Override + public int getIntHeader(String name) { + return this._getHttpServletRequest().getIntHeader(name); + } + + /** + * The default behavior of this method is to return {@link HttpServletRequest#getHttpServletMapping()} on the + * wrapped request object. + * + * @since Servlet 4.0 + */ + @Override + public HttpServletMapping getHttpServletMapping() { + return this._getHttpServletRequest().getHttpServletMapping(); + } + + /** + * The default behavior of this method is to return getMethod() on the wrapped request object. + */ + @Override + public String getMethod() { + return this._getHttpServletRequest().getMethod(); + } + + /** + * The default behavior of this method is to return getPathInfo() on the wrapped request object. + */ + @Override + public String getPathInfo() { + return this._getHttpServletRequest().getPathInfo(); + } + + /** + * The default behavior of this method is to return getPathTranslated() on the wrapped request object. + */ + @Override + public String getPathTranslated() { + return this._getHttpServletRequest().getPathTranslated(); + } + + /** + * The default behavior of this method is to return getContextPath() on the wrapped request object. + */ + @Override + public String getContextPath() { + return this._getHttpServletRequest().getContextPath(); + } + + /** + * The default behavior of this method is to return getQueryString() on the wrapped request object. + */ + @Override + public String getQueryString() { + return this._getHttpServletRequest().getQueryString(); + } + + /** + * The default behavior of this method is to return getRemoteUser() on the wrapped request object. + */ + @Override + public String getRemoteUser() { + return this._getHttpServletRequest().getRemoteUser(); + } + + /** + * The default behavior of this method is to return isUserInRole(String role) on the wrapped request object. + */ + @Override + public boolean isUserInRole(String role) { + return this._getHttpServletRequest().isUserInRole(role); + } + + /** + * The default behavior of this method is to return getUserPrincipal() on the wrapped request object. + */ + @Override + public java.security.Principal getUserPrincipal() { + return this._getHttpServletRequest().getUserPrincipal(); + } + + /** + * The default behavior of this method is to return getRequestedSessionId() on the wrapped request object. + */ + @Override + public String getRequestedSessionId() { + return this._getHttpServletRequest().getRequestedSessionId(); + } + + /** + * The default behavior of this method is to return getRequestURI() on the wrapped request object. + */ + @Override + public String getRequestURI() { + return this._getHttpServletRequest().getRequestURI(); + } + + /** + * The default behavior of this method is to return getRequestURL() on the wrapped request object. + */ + @Override + public StringBuffer getRequestURL() { + return this._getHttpServletRequest().getRequestURL(); + } + + /** + * The default behavior of this method is to return getServletPath() on the wrapped request object. + */ + @Override + public String getServletPath() { + return this._getHttpServletRequest().getServletPath(); + } + + /** + * The default behavior of this method is to return getSession(boolean create) on the wrapped request object. + */ + @Override + public HttpSession getSession(boolean create) { + return this._getHttpServletRequest().getSession(create); + } + + /** + * The default behavior of this method is to return getSession() on the wrapped request object. + */ + @Override + public HttpSession getSession() { + return this._getHttpServletRequest().getSession(); + } + + /** + * The default behavior of this method is to call changeSessionId() on the wrapped request object. + * + * @since Servlet 3.1 + */ + @Override + public String changeSessionId() { + return this._getHttpServletRequest().changeSessionId(); + } + + /** + * The default behavior of this method is to return isRequestedSessionIdValid() on the wrapped request object. + */ + @Override + public boolean isRequestedSessionIdValid() { + return this._getHttpServletRequest().isRequestedSessionIdValid(); + } + + /** + * The default behavior of this method is to return isRequestedSessionIdFromCookie() on the wrapped request object. + */ + @Override + public boolean isRequestedSessionIdFromCookie() { + return this._getHttpServletRequest().isRequestedSessionIdFromCookie(); + } + + /** + * The default behavior of this method is to return isRequestedSessionIdFromURL() on the wrapped request object. + */ + @Override + public boolean isRequestedSessionIdFromURL() { + return this._getHttpServletRequest().isRequestedSessionIdFromURL(); + } + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#authenticate(HttpServletResponse)} on + * the wrapped request object. + * + * @since Servlet 3.0 + */ + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + return this._getHttpServletRequest().authenticate(response); + } + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#login(String, String)} on the wrapped + * request object. + * + * @since Servlet 3.0 + */ + @Override + public void login(String username, String password) throws ServletException { + this._getHttpServletRequest().login(username, password); + } + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#logout()} on the wrapped request + * object. + * + * @since Servlet 3.0 + */ + @Override + public void logout() throws ServletException { + this._getHttpServletRequest().logout(); + } + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#getParts()} on the wrapped request + * object. + * + * @since Servlet 3.0 + */ + @Override + public Collection getParts() throws IOException, ServletException { + return this._getHttpServletRequest().getParts(); + } + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#getPart(String)} on the wrapped + * request object. + * + * @since Servlet 3.0 + */ + @Override + public Part getPart(String name) throws IOException, ServletException { + return this._getHttpServletRequest().getPart(name); + } + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#upgrade(Class)} on the wrapped request + * object. + * + * @since Servlet 3.1 + */ + @Override + public T upgrade(Class httpUpgradeHandlerClass) + throws IOException, ServletException { + return this._getHttpServletRequest().upgrade(httpUpgradeHandlerClass); + } + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#newPushBuilder()} on the wrapped + * request object. + * + * @since Servlet 4.0 + */ + @Override + public PushBuilder newPushBuilder() { + return this._getHttpServletRequest().newPushBuilder(); + } + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#getTrailerFields()} on the wrapped + * request object. + * + * @since Servlet 4.0 + */ + @Override + public Map getTrailerFields() { + return this._getHttpServletRequest().getTrailerFields(); + } + + + /** + * {@inheritDoc} + *

+ * The default behavior of this method is to return {@link HttpServletRequest#isTrailerFieldsReady()} on the wrapped + * request object. + * + * @since Servlet 4.0 + */ + @Override + public boolean isTrailerFieldsReady() { + return this._getHttpServletRequest().isTrailerFieldsReady(); + } +} diff --git a/java/jakarta/servlet/http/HttpServletResponse.java b/java/jakarta/servlet/http/HttpServletResponse.java new file mode 100644 index 0000000..26405e0 --- /dev/null +++ b/java/jakarta/servlet/http/HttpServletResponse.java @@ -0,0 +1,535 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; + +import jakarta.servlet.ServletResponse; + +/** + * Extends the {@link ServletResponse} interface to provide HTTP-specific functionality in sending a response. For + * example, it has methods to access HTTP headers and cookies. + *

+ * The servlet container creates an HttpServletResponse object and passes it as an argument to the + * servlet's service methods (doGet, doPost, etc). + * + * @see jakarta.servlet.ServletResponse + */ +public interface HttpServletResponse extends ServletResponse { + + /** + * Adds the specified cookie to the response. This method can be called multiple times to set more than one cookie. + * + * @param cookie the Cookie to return to the client + */ + void addCookie(Cookie cookie); + + /** + * Returns a boolean indicating whether the named response header has already been set. + * + * @param name the header name + * + * @return true if the named response header has already been set; false otherwise + */ + boolean containsHeader(String name); + + /** + * Encodes the specified URL by including the session ID in it, or, if encoding is not needed, returns the URL + * unchanged. The implementation of this method includes the logic to determine whether the session ID needs to be + * encoded in the URL. For example, if the browser supports cookies, or session tracking is turned off, URL encoding + * is unnecessary. + *

+ * For robust session tracking, all URLs emitted by a servlet should be run through this method. Otherwise, URL + * rewriting cannot be used with browsers which do not support cookies. + * + * @param url the url to be encoded. + * + * @return the encoded URL if encoding is needed; the unchanged URL otherwise. + */ + String encodeURL(String url); + + /** + * Encodes the specified URL for use in the sendRedirect method or, if encoding is not needed, returns + * the URL unchanged. The implementation of this method includes the logic to determine whether the session ID needs + * to be encoded in the URL. Because the rules for making this determination can differ from those used to decide + * whether to encode a normal link, this method is separated from the encodeURL method. + *

+ * All URLs sent to the HttpServletResponse.sendRedirect method should be run through this method. + * Otherwise, URL rewriting cannot be used with browsers which do not support cookies. + * + * @param url the url to be encoded. + * + * @return the encoded URL if encoding is needed; the unchanged URL otherwise. + * + * @see #sendRedirect + */ + String encodeRedirectURL(String url); + + /** + * Sends an error response to the client using the specified status code and clears the output buffer. The server + * defaults to creating the response to look like an HTML-formatted server error page containing the specified + * message, setting the content type to "text/html", leaving cookies and other headers unmodified. If an error-page + * declaration has been made for the web application corresponding to the status code passed in, it will be served + * back in preference to the suggested msg parameter. + *

+ * If the response has already been committed, this method throws an IllegalStateException. After using this method, + * the response should be considered to be committed and should not be written to. + * + * @param sc the error status code + * @param msg the descriptive message + * + * @exception IOException If an input or output exception occurs + * @exception IllegalStateException If the response was committed + */ + void sendError(int sc, String msg) throws IOException; + + /** + * Sends an error response to the client using the specified status code and clears the buffer. This is equivalent + * to calling {@link #sendError(int, String)} with the same status code and null for the message. + * + * @param sc the error status code + * + * @exception IOException If an input or output exception occurs + * @exception IllegalStateException If the response was committed before this method call + */ + void sendError(int sc) throws IOException; + + /** + * Sends a redirect response to the client using the specified redirect location URL with the status code + * {@link #SC_FOUND} 302 (Found), clears the response buffer and commits the response. The response buffer will be + * replaced with a short hypertext note as per RFC 9110. + *

+ * This method has no effect if called from an include. + *

+ * This method accepts both relative and absolute URLs. Absolute URLs passed to this method are used as provided as + * the redirect location URL. Relative URLs are converted to absolute URLs. If converting a relative URL to an absolute URL then: + *

    + *
  • If the location is relative without a leading '/' the container interprets it as relative to the current + * request URI.
  • + *
  • If the location is relative with a leading '/' the container interprets it as relative to the servlet + * container root.
  • + *
  • If the location is relative with two leading '/' the container interprets it as a network-path reference (see + * RFC 3986: Uniform Resource Identifier (URI): Generic Syntax, + * section 4.2 "Relative Reference").
  • + *
+ *

+ * If the response has already been committed, this method throws an IllegalStateException. After using this method, + * the response should be considered to be committed and should not be written to. + * + * @param location the redirect location URL (may be absolute or relative) + * + * @exception IOException If an input or output exception occurs + * @exception IllegalArgumentException If a relative URL is given and cannot be converted into an absolute URL + * @exception IllegalStateException If the response was already committed when this method was called + */ + void sendRedirect(String location) throws IOException; + + /** + * Sets a response header with the given name and date-value. The date is specified in terms of milliseconds since + * the epoch. If the header had already been set, the new value overwrites the previous one. The + * containsHeader method can be used to test for the presence of a header before setting its value. + * + * @param name the name of the header to set + * @param date the assigned date value + * + * @see #containsHeader + * @see #addDateHeader + */ + void setDateHeader(String name, long date); + + /** + * Adds a response header with the given name and date-value. The date is specified in terms of milliseconds since + * the epoch. This method allows response headers to have multiple values. + * + * @param name the name of the header to set + * @param date the additional date value + * + * @see #setDateHeader + */ + void addDateHeader(String name, long date); + + /** + * Sets a response header with the given name and value. If the header had already been set, the new value + * overwrites the previous one. The containsHeader method can be used to test for the presence of a + * header before setting its value. + * + * @param name the name of the header + * @param value the header value If it contains octet string, it should be encoded according to RFC 2047 + * (http://www.ietf.org/rfc/rfc2047.txt) + * + * @see #containsHeader + * @see #addHeader + */ + void setHeader(String name, String value); + + /** + * Adds a response header with the given name and value. This method allows response headers to have multiple + * values. + * + * @param name the name of the header + * @param value the additional header value If it contains octet string, it should be encoded according to RFC 2047 + * (http://www.ietf.org/rfc/rfc2047.txt) + * + * @see #setHeader + */ + void addHeader(String name, String value); + + /** + * Sets a response header with the given name and integer value. If the header had already been set, the new value + * overwrites the previous one. The containsHeader method can be used to test for the presence of a + * header before setting its value. + * + * @param name the name of the header + * @param value the assigned integer value + * + * @see #containsHeader + * @see #addIntHeader + */ + void setIntHeader(String name, int value); + + /** + * Adds a response header with the given name and integer value. This method allows response headers to have + * multiple values. + * + * @param name the name of the header + * @param value the assigned integer value + * + * @see #setIntHeader + */ + void addIntHeader(String name, int value); + + /** + * Sets the status code for this response. This method is used to set the return status code when there is no error + * (for example, for the status codes SC_OK or SC_MOVED_TEMPORARILY). If there is an error, and the caller wishes to + * invoke an error-page defined in the web application, the sendError method should be used instead. + *

+ * The container clears the buffer and sets the Location header, preserving cookies and other headers. + * + * @param sc the status code + * + * @see #sendError + */ + void setStatus(int sc); + + /** + * Get the HTTP status code for this Response. + * + * @return The HTTP status code for this Response + * + * @since Servlet 3.0 + */ + int getStatus(); + + /** + * Return the value for the specified header, or null if this header has not been set. If more than one + * value was added for this name, only the first is returned; use {@link #getHeaders(String)} to retrieve all of + * them. + * + * @param name Header name to look up + * + * @return The first value for the specified header. This is the raw value so if multiple values are specified in + * the first header then they will be returned as a single header value . + * + * @since Servlet 3.0 + */ + String getHeader(String name); + + /** + * Return a Collection of all the header values associated with the specified header name. + * + * @param name Header name to look up + * + * @return The values for the specified header. These are the raw values so if multiple values are specified in a + * single header that will be returned as a single header value. + * + * @since Servlet 3.0 + */ + Collection getHeaders(String name); + + /** + * Get the header names set for this HTTP response. + * + * @return The header names set for this HTTP response. + * + * @since Servlet 3.0 + */ + Collection getHeaderNames(); + + /** + * Configure the supplier of the trailer headers. The supplier will be called in the scope of the thread that + * completes the response.
+ * Trailers that don't meet the requirements of RFC 7230, section 4.1.2 will be ignored.
+ * The default implementation is a NO-OP. + * + * @param supplier The supplier for the trailer headers + * + * @throws IllegalStateException if this method is called when the underlying protocol does not support trailer + * headers or if using HTTP/1.1 and the response has already been committed + * + * @since Servlet 4.0 + */ + default void setTrailerFields(Supplier> supplier) { + // NO-OP + } + + /** + * Obtain the supplier of the trailer headers.
+ * The default implementation returns null. + * + * @return The supplier for the trailer headers + * + * @since Servlet 4.0 + */ + default Supplier> getTrailerFields() { + return null; + } + + /* + * Server status codes; see RFC 7231. + */ + + /** + * Status code (100) indicating the client can continue. + */ + int SC_CONTINUE = 100; + + /** + * Status code (101) indicating the server is switching protocols according to Upgrade header. + */ + int SC_SWITCHING_PROTOCOLS = 101; + + /** + * Status code (200) indicating the request succeeded normally. + */ + int SC_OK = 200; + + /** + * Status code (201) indicating the request succeeded and created a new resource on the server. + */ + int SC_CREATED = 201; + + /** + * Status code (202) indicating that a request was accepted for processing, but was not completed. + */ + int SC_ACCEPTED = 202; + + /** + * Status code (203) indicating that the meta information presented by the client did not originate from the server. + */ + int SC_NON_AUTHORITATIVE_INFORMATION = 203; + + /** + * Status code (204) indicating that the request succeeded but that there was no new information to return. + */ + int SC_NO_CONTENT = 204; + + /** + * Status code (205) indicating that the agent SHOULD reset the document view which caused the request to + * be sent. + */ + int SC_RESET_CONTENT = 205; + + /** + * Status code (206) indicating that the server has fulfilled the partial GET request for the resource. + */ + int SC_PARTIAL_CONTENT = 206; + + /** + * Status code (300) indicating that the requested resource corresponds to any one of a set of representations, each + * with its own specific location. + */ + int SC_MULTIPLE_CHOICES = 300; + + /** + * Status code (301) indicating that the resource has permanently moved to a new location, and that future + * references should use a new URI with their requests. + */ + int SC_MOVED_PERMANENTLY = 301; + + /** + * Status code (302) indicating that the resource has temporarily moved to another location, but that future + * references should still use the original URI to access the resource. This definition is being retained for + * backwards compatibility. SC_FOUND is now the preferred definition. + */ + int SC_MOVED_TEMPORARILY = 302; + + /** + * Status code (302) indicating that the resource reside temporarily under a different URI. Since the redirection + * might be altered on occasion, the client should continue to use the Request-URI for future requests.(HTTP/1.1) To + * represent the status code (302), it is recommended to use this variable. + */ + int SC_FOUND = 302; + + /** + * Status code (303) indicating that the response to the request can be found under a different URI. + */ + int SC_SEE_OTHER = 303; + + /** + * Status code (304) indicating that a conditional GET operation found that the resource was available and not + * modified. + */ + int SC_NOT_MODIFIED = 304; + + /** + * Status code (305) indicating that the requested resource MUST be accessed through the proxy given by the + * Location field. + */ + int SC_USE_PROXY = 305; + + /** + * Status code (307) indicating that the requested resource resides temporarily under a different URI. The temporary + * URI SHOULD be given by the Location field in the response. + */ + int SC_TEMPORARY_REDIRECT = 307; + + /** + * Status code (400) indicating the request sent by the client was syntactically incorrect. + */ + int SC_BAD_REQUEST = 400; + + /** + * Status code (401) indicating that the request requires HTTP authentication. + */ + int SC_UNAUTHORIZED = 401; + + /** + * Status code (402) reserved for future use. + */ + int SC_PAYMENT_REQUIRED = 402; + + /** + * Status code (403) indicating the server understood the request but refused to fulfill it. + */ + int SC_FORBIDDEN = 403; + + /** + * Status code (404) indicating that the requested resource is not available. + */ + int SC_NOT_FOUND = 404; + + /** + * Status code (405) indicating that the method specified in the Request-Line is not allowed + * for the resource identified by the Request-URI. + */ + int SC_METHOD_NOT_ALLOWED = 405; + + /** + * Status code (406) indicating that the resource identified by the request is only capable of generating response + * entities which have content characteristics not acceptable according to the accept headers sent in the request. + */ + int SC_NOT_ACCEPTABLE = 406; + + /** + * Status code (407) indicating that the client MUST first authenticate itself with the proxy. + */ + int SC_PROXY_AUTHENTICATION_REQUIRED = 407; + + /** + * Status code (408) indicating that the client did not produce a request within the time that the server was + * prepared to wait. + */ + int SC_REQUEST_TIMEOUT = 408; + + /** + * Status code (409) indicating that the request could not be completed due to a conflict with the current state of + * the resource. + */ + int SC_CONFLICT = 409; + + /** + * Status code (410) indicating that the resource is no longer available at the server and no forwarding address is + * known. This condition SHOULD be considered permanent. + */ + int SC_GONE = 410; + + /** + * Status code (411) indicating that the request cannot be handled without a defined + * Content-Length. + */ + int SC_LENGTH_REQUIRED = 411; + + /** + * Status code (412) indicating that the precondition given in one or more of the request-header fields evaluated to + * false when it was tested on the server. + */ + int SC_PRECONDITION_FAILED = 412; + + /** + * Status code (413) indicating that the server is refusing to process the request because the request entity is + * larger than the server is willing or able to process. + */ + int SC_REQUEST_ENTITY_TOO_LARGE = 413; + + /** + * Status code (414) indicating that the server is refusing to service the request because the + * Request-URI is longer than the server is willing to interpret. + */ + int SC_REQUEST_URI_TOO_LONG = 414; + + /** + * Status code (415) indicating that the server is refusing to service the request because the entity of the request + * is in a format not supported by the requested resource for the requested method. + */ + int SC_UNSUPPORTED_MEDIA_TYPE = 415; + + /** + * Status code (416) indicating that the server cannot serve the requested byte range. + */ + int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + + /** + * Status code (417) indicating that the server could not meet the expectation given in the Expect request header. + */ + int SC_EXPECTATION_FAILED = 417; + + /** + * Status code (500) indicating an error inside the HTTP server which prevented it from fulfilling the request. + */ + int SC_INTERNAL_SERVER_ERROR = 500; + + /** + * Status code (501) indicating the HTTP server does not support the functionality needed to fulfill the request. + */ + int SC_NOT_IMPLEMENTED = 501; + + /** + * Status code (502) indicating that the HTTP server received an invalid response from a server it consulted when + * acting as a proxy or gateway. + */ + int SC_BAD_GATEWAY = 502; + + /** + * Status code (503) indicating that the HTTP server is temporarily overloaded, and unable to handle the request. + */ + int SC_SERVICE_UNAVAILABLE = 503; + + /** + * Status code (504) indicating that the server did not receive a timely response from the upstream server while + * acting as a gateway or proxy. + */ + int SC_GATEWAY_TIMEOUT = 504; + + /** + * Status code (505) indicating that the server does not support or refuses to support the HTTP protocol version + * that was used in the request message. + */ + int SC_HTTP_VERSION_NOT_SUPPORTED = 505; +} diff --git a/java/jakarta/servlet/http/HttpServletResponseWrapper.java b/java/jakarta/servlet/http/HttpServletResponseWrapper.java new file mode 100644 index 0000000..1f47437 --- /dev/null +++ b/java/jakarta/servlet/http/HttpServletResponseWrapper.java @@ -0,0 +1,247 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; + +import jakarta.servlet.ServletResponseWrapper; + +/** + * Provides a convenient implementation of the HttpServletResponse interface that can be subclassed by developers + * wishing to adapt the response from a Servlet. This class implements the Wrapper or Decorator pattern. Methods default + * to calling through to the wrapped response object. + * + * @since Servlet 2.3 + * + * @see jakarta.servlet.http.HttpServletResponse + */ +public class HttpServletResponseWrapper extends ServletResponseWrapper implements HttpServletResponse { + + /** + * Constructs a response adaptor wrapping the given response. + * + * @param response The response to be wrapped + * + * @throws java.lang.IllegalArgumentException if the response is null + */ + public HttpServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + private HttpServletResponse _getHttpServletResponse() { + return (HttpServletResponse) super.getResponse(); + } + + /** + * The default behavior of this method is to call addCookie(Cookie cookie) on the wrapped response object. + */ + @Override + public void addCookie(Cookie cookie) { + this._getHttpServletResponse().addCookie(cookie); + } + + /** + * The default behavior of this method is to call containsHeader(String name) on the wrapped response object. + */ + @Override + public boolean containsHeader(String name) { + return this._getHttpServletResponse().containsHeader(name); + } + + /** + * The default behavior of this method is to call encodeURL(String url) on the wrapped response object. + */ + @Override + public String encodeURL(String url) { + return this._getHttpServletResponse().encodeURL(url); + } + + /** + * The default behavior of this method is to return encodeRedirectURL(String url) on the wrapped response object. + */ + @Override + public String encodeRedirectURL(String url) { + return this._getHttpServletResponse().encodeRedirectURL(url); + } + + /** + * The default behavior of this method is to call sendError(int sc, String msg) on the wrapped response object. + */ + @Override + public void sendError(int sc, String msg) throws IOException { + this._getHttpServletResponse().sendError(sc, msg); + } + + /** + * The default behavior of this method is to call sendError(int sc) on the wrapped response object. + */ + @Override + public void sendError(int sc) throws IOException { + this._getHttpServletResponse().sendError(sc); + } + + /** + * The default behavior of this method is to call sendRedirect(String location) on the wrapped response object. + */ + @Override + public void sendRedirect(String location) throws IOException { + this._getHttpServletResponse().sendRedirect(location); + } + + /** + * The default behavior of this method is to call setDateHeader(String name, long date) on the wrapped response + * object. + */ + @Override + public void setDateHeader(String name, long date) { + this._getHttpServletResponse().setDateHeader(name, date); + } + + /** + * The default behavior of this method is to call addDateHeader(String name, long date) on the wrapped response + * object. + */ + @Override + public void addDateHeader(String name, long date) { + this._getHttpServletResponse().addDateHeader(name, date); + } + + /** + * The default behavior of this method is to return setHeader(String name, String value) on the wrapped response + * object. + */ + @Override + public void setHeader(String name, String value) { + this._getHttpServletResponse().setHeader(name, value); + } + + /** + * The default behavior of this method is to return addHeader(String name, String value) on the wrapped response + * object. + */ + @Override + public void addHeader(String name, String value) { + this._getHttpServletResponse().addHeader(name, value); + } + + /** + * The default behavior of this method is to call setIntHeader(String name, int value) on the wrapped response + * object. + */ + @Override + public void setIntHeader(String name, int value) { + this._getHttpServletResponse().setIntHeader(name, value); + } + + /** + * The default behavior of this method is to call addIntHeader(String name, int value) on the wrapped response + * object. + */ + @Override + public void addIntHeader(String name, int value) { + this._getHttpServletResponse().addIntHeader(name, value); + } + + /** + * The default behavior of this method is to call setStatus(int sc) on the wrapped response object. + */ + @Override + public void setStatus(int sc) { + this._getHttpServletResponse().setStatus(sc); + } + + /** + * {@inheritDoc} + *

+ * The default implementation is to call {@link HttpServletResponse#getStatus()} on the wrapped + * {@link HttpServletResponse}. + * + * @since Servlet 3.0 + */ + @Override + public int getStatus() { + return this._getHttpServletResponse().getStatus(); + } + + /** + * {@inheritDoc} + *

+ * The default implementation is to call {@link HttpServletResponse#getHeader(String)} on the wrapped + * {@link HttpServletResponse}. + * + * @since Servlet 3.0 + */ + @Override + public String getHeader(String name) { + return this._getHttpServletResponse().getHeader(name); + } + + /** + * {@inheritDoc} + *

+ * The default implementation is to call {@link HttpServletResponse#getHeaders(String)} on the wrapped + * {@link HttpServletResponse}. + * + * @since Servlet 3.0 + */ + @Override + public Collection getHeaders(String name) { + return this._getHttpServletResponse().getHeaders(name); + } + + /** + * {@inheritDoc} + *

+ * The default implementation is to call {@link HttpServletResponse#getHeaderNames()} on the wrapped + * {@link HttpServletResponse}. + * + * @since Servlet 3.0 + */ + @Override + public Collection getHeaderNames() { + return this._getHttpServletResponse().getHeaderNames(); + } + + /** + * {@inheritDoc} + *

+ * The default implementation is to call {@link HttpServletResponse#setTrailerFields(Supplier)} on the wrapped + * {@link HttpServletResponse}. + * + * @since Servlet 4.0 + */ + @Override + public void setTrailerFields(Supplier> supplier) { + this._getHttpServletResponse().setTrailerFields(supplier); + } + + /** + * {@inheritDoc} + *

+ * The default implementation is to call {@link HttpServletResponse#getTrailerFields()} on the wrapped + * {@link HttpServletResponse}. + * + * @since Servlet 4.0 + */ + @Override + public Supplier> getTrailerFields() { + return this._getHttpServletResponse().getTrailerFields(); + } +} diff --git a/java/jakarta/servlet/http/HttpSession.java b/java/jakarta/servlet/http/HttpSession.java new file mode 100644 index 0000000..ecadd1e --- /dev/null +++ b/java/jakarta/servlet/http/HttpSession.java @@ -0,0 +1,197 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.Enumeration; + +import jakarta.servlet.ServletContext; + +/** + * Provides a way to identify a user across more than one page request or visit to a Web site and to store information + * about that user. + *

+ * The servlet container uses this interface to create a session between an HTTP client and an HTTP server. The session + * persists for a specified time period, across more than one connection or page request from the user. A session + * usually corresponds to one user, who may visit a site many times. The server can maintain a session in many ways such + * as using cookies or rewriting URLs. + *

+ * This interface allows servlets to + *

    + *
  • View and manipulate information about a session, such as the session identifier, creation time, and last accessed + * time + *
  • Bind objects to sessions, allowing user information to persist across multiple user connections + *
+ *

+ * When an application stores an object in or removes an object from a session, the session checks whether the object + * implements {@link HttpSessionBindingListener}. If it does, the servlet notifies the object that it has been bound to + * or unbound from the session. Notifications are sent after the binding methods complete. For session that are + * invalidated or expire, notifications are sent after the session has been invalidated or expired. + *

+ * When container migrates a session between VMs in a distributed container setting, all session attributes implementing + * the {@link HttpSessionActivationListener} interface are notified. + *

+ * A servlet should be able to handle cases in which the client does not choose to join a session, such as when cookies + * are intentionally turned off. Until the client joins the session, isNew returns true. If + * the client chooses not to join the session, getSession will return a different session on each request, + * and isNew will always return true. + *

+ * Session information is scoped only to the current web application ( ServletContext), so information + * stored in one context will not be directly visible in another. + * + * @see HttpSessionBindingListener + */ +public interface HttpSession { + + /** + * Returns the time when this session was created, measured in milliseconds since midnight January 1, 1970 GMT. + * + * @return a long specifying when this session was created, expressed in milliseconds since 1/1/1970 + * GMT + * + * @exception IllegalStateException if this method is called on an invalidated session + */ + long getCreationTime(); + + /** + * Returns a string containing the unique identifier assigned to this session. The identifier is assigned by the + * servlet container and is implementation dependent. + * + * @return a string specifying the identifier assigned to this session + * + * @exception IllegalStateException if this method is called on an invalidated session + */ + String getId(); + + /** + * Returns the last time the client sent a request associated with this session, as the number of milliseconds since + * midnight January 1, 1970 GMT, and marked by the time the container received the request. + *

+ * Actions that your application takes, such as getting or setting a value associated with the session, do not + * affect the access time. + * + * @return a long representing the last time the client sent a request associated with this session, + * expressed in milliseconds since 1/1/1970 GMT + * + * @exception IllegalStateException if this method is called on an invalidated session + */ + long getLastAccessedTime(); + + /** + * Returns the ServletContext to which this session belongs. + * + * @return The ServletContext object for the web application + * + * @since Servlet 2.3 + */ + ServletContext getServletContext(); + + /** + * Specifies the time, in seconds, between client requests before the servlet container will invalidate this + * session. A zero or negative time indicates that the session should never timeout. + * + * @param interval An integer specifying the number of seconds + */ + void setMaxInactiveInterval(int interval); + + /** + * Returns the maximum time interval, in seconds, that the servlet container will keep this session open between + * client accesses. After this interval, the servlet container will invalidate the session. The maximum time + * interval can be set with the setMaxInactiveInterval method. A zero or negative time indicates that + * the session should never timeout. + * + * @return an integer specifying the number of seconds this session remains open between client requests + * + * @see #setMaxInactiveInterval + */ + int getMaxInactiveInterval(); + + /** + * Returns the object bound with the specified name in this session, or null if no object is bound + * under the name. + * + * @param name a string specifying the name of the object + * + * @return the object with the specified name + * + * @exception IllegalStateException if this method is called on an invalidated session + */ + Object getAttribute(String name); + + /** + * Returns an Enumeration of String objects containing the names of all the objects bound + * to this session. + * + * @return an Enumeration of String objects specifying the names of all the objects bound + * to this session + * + * @exception IllegalStateException if this method is called on an invalidated session + */ + Enumeration getAttributeNames(); + + /** + * Binds an object to this session, using the name specified. If an object of the same name is already bound to the + * session, the object is replaced. + *

+ * After this method executes, and if the new object implements HttpSessionBindingListener, the + * container calls HttpSessionBindingListener.valueBound. The container then notifies any + * HttpSessionAttributeListeners in the web application. + *

+ * If an object was already bound to this session of this name that implements + * HttpSessionBindingListener, its HttpSessionBindingListener.valueUnbound method is + * called. + *

+ * If the value passed in is null, this has the same effect as calling removeAttribute(). + * + * @param name the name to which the object is bound; cannot be null + * @param value the object to be bound + * + * @exception IllegalStateException if this method is called on an invalidated session + */ + void setAttribute(String name, Object value); + + /** + * Removes the object bound with the specified name from this session. If the session does not have an object bound + * with the specified name, this method does nothing. + *

+ * After this method executes, and if the object implements HttpSessionBindingListener, the container + * calls HttpSessionBindingListener.valueUnbound. The container then notifies any + * HttpSessionAttributeListeners in the web application. + * + * @param name the name of the object to remove from this session + * + * @exception IllegalStateException if this method is called on an invalidated session + */ + void removeAttribute(String name); + + /** + * Invalidates this session then unbinds any objects bound to it. + * + * @exception IllegalStateException if this method is called on an already invalidated session + */ + void invalidate(); + + /** + * Returns true if the client does not yet know about the session or if the client chooses not to join + * the session. For example, if the server used only cookie-based sessions, and the client had disabled the use of + * cookies, then a session would be new on each request. + * + * @return true if the server has created a session, but the client has not yet joined + * + * @exception IllegalStateException if this method is called on an already invalidated session + */ + boolean isNew(); +} diff --git a/java/jakarta/servlet/http/HttpSessionActivationListener.java b/java/jakarta/servlet/http/HttpSessionActivationListener.java new file mode 100644 index 0000000..625fe5e --- /dev/null +++ b/java/jakarta/servlet/http/HttpSessionActivationListener.java @@ -0,0 +1,46 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.EventListener; + +/** + * Objects that are bound to a session may listen to container events notifying them that sessions will be passivated + * and that session will be activated. A container that migrates session between VMs or persists sessions is required to + * notify all attributes bound to sessions implementing HttpSessionActivationListener. + * + * @since Servlet 2.3 + */ +public interface HttpSessionActivationListener extends EventListener { + + /** + * Notification that the session is about to be passivated. The default implementation is a NO-OP. + * + * @param se Information about the session this is about to be passivated + */ + default void sessionWillPassivate(HttpSessionEvent se) { + } + + /** + * Notification that the session has just been activated. The default implementation is a NO-OP. + * + * @param se Information about the session this has just been activated + */ + default void sessionDidActivate(HttpSessionEvent se) { + } +} + diff --git a/java/jakarta/servlet/http/HttpSessionAttributeListener.java b/java/jakarta/servlet/http/HttpSessionAttributeListener.java new file mode 100644 index 0000000..4d00312 --- /dev/null +++ b/java/jakarta/servlet/http/HttpSessionAttributeListener.java @@ -0,0 +1,55 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.EventListener; + +/** + * This listener interface can be implemented in order to get notifications of changes to the attribute lists of + * sessions within this web application. + * + * @since Servlet 2.3 + */ +public interface HttpSessionAttributeListener extends EventListener { + + /** + * Notification that an attribute has been added to a session. Called after the attribute is added. The default + * implementation is a NO-OP. + * + * @param se Information about the added attribute + */ + default void attributeAdded(HttpSessionBindingEvent se) { + } + + /** + * Notification that an attribute has been removed from a session. Called after the attribute is removed. The + * default implementation is a NO-OP. + * + * @param se Information about the removed attribute + */ + default void attributeRemoved(HttpSessionBindingEvent se) { + } + + /** + * Notification that an attribute has been replaced in a session. Called after the attribute is replaced. The + * default implementation is a NO-OP. + * + * @param se Information about the replaced attribute + */ + default void attributeReplaced(HttpSessionBindingEvent se) { + } +} diff --git a/java/jakarta/servlet/http/HttpSessionBindingEvent.java b/java/jakarta/servlet/http/HttpSessionBindingEvent.java new file mode 100644 index 0000000..156a660 --- /dev/null +++ b/java/jakarta/servlet/http/HttpSessionBindingEvent.java @@ -0,0 +1,110 @@ +/* + * 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 jakarta.servlet.http; + +/** + * Events of this type are either sent to an object that implements {@link HttpSessionBindingListener} when it is bound + * or unbound from a session, or to an {@link HttpSessionAttributeListener} that has been configured in the deployment + * descriptor when any attribute is bound, unbound or replaced in a session. + *

+ * The session binds the object by a call to HttpSession.setAttribute and unbinds the object by a call to + * HttpSession.removeAttribute. + * + * @see HttpSession + * @see HttpSessionBindingListener + * @see HttpSessionAttributeListener + */ +public class HttpSessionBindingEvent extends HttpSessionEvent { + + private static final long serialVersionUID = 1L; + + /** + * The name to which the object is being bound or unbound. + */ + private final String name; + + /** + * The object is being bound or unbound. + */ + private final Object value; + + /** + * Constructs an event that notifies an object that it has been bound to or unbound from a session. To receive the + * event, the object must implement {@link HttpSessionBindingListener}. + * + * @param session the session to which the object is bound or unbound + * @param name the name with which the object is bound or unbound + * + * @see #getName() + * @see #getSession() + */ + public HttpSessionBindingEvent(HttpSession session, String name) { + super(session); + this.name = name; + this.value = null; + } + + /** + * Constructs an event that notifies an object that it has been bound to or unbound from a session. To receive the + * event, the object must implement {@link HttpSessionBindingListener}. + * + * @param session the session to which the object is bound or unbound + * @param name the name with which the object is bound or unbound + * @param value the object that is bound or unbound + * + * @see #getName() + * @see #getSession() + * @see #getValue() + */ + public HttpSessionBindingEvent(HttpSession session, String name, Object value) { + super(session); + this.name = name; + this.value = value; + } + + /** + * Get the session that changed. + * + * @return The session that changed + */ + @Override + public HttpSession getSession() { + return super.getSession(); + } + + /** + * Returns the name with which the attribute is bound to or unbound from the session. + * + * @return a string specifying the name with which the object is bound to or unbound from the session + */ + public String getName() { + return name; + } + + /** + * Returns the value of the attribute that has been added, removed or replaced. + * + * @return If the attribute was added (or bound), this is the value of the attribute. If the attribute was removed + * (or unbound), this is the value of the removed attribute. If the attribute was replaced, this is the + * old value of the attribute. + * + * @since Servlet 2.3 + */ + public Object getValue() { + return this.value; + } +} diff --git a/java/jakarta/servlet/http/HttpSessionBindingListener.java b/java/jakarta/servlet/http/HttpSessionBindingListener.java new file mode 100644 index 0000000..5c16e73 --- /dev/null +++ b/java/jakarta/servlet/http/HttpSessionBindingListener.java @@ -0,0 +1,52 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.EventListener; + +/** + * Causes an object to be notified when it is bound to or unbound from a session. The object is notified by an + * {@link HttpSessionBindingEvent} object. This may be as a result of a servlet programmer explicitly unbinding an + * attribute from a session, due to a session being invalidated, or due to a session timing out. + * + * @see HttpSession + * @see HttpSessionBindingEvent + */ +public interface HttpSessionBindingListener extends EventListener { + + /** + * Notifies the object that it is being bound to a session and identifies the session. The default implementation is + * a NO-OP. + * + * @param event the event that identifies the session + * + * @see #valueUnbound + */ + default void valueBound(HttpSessionBindingEvent event) { + } + + /** + * Notifies the object that it is being unbound from a session and identifies the session. The default + * implementation is a NO-OP. + * + * @param event the event that identifies the session + * + * @see #valueBound + */ + default void valueUnbound(HttpSessionBindingEvent event) { + } +} diff --git a/java/jakarta/servlet/http/HttpSessionEvent.java b/java/jakarta/servlet/http/HttpSessionEvent.java new file mode 100644 index 0000000..a61cd06 --- /dev/null +++ b/java/jakarta/servlet/http/HttpSessionEvent.java @@ -0,0 +1,44 @@ +/* + * 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 jakarta.servlet.http; + +/** + * This is the class representing event notifications for changes to sessions within a web application. + * + * @since Servlet 2.3 + */ +public class HttpSessionEvent extends java.util.EventObject { + private static final long serialVersionUID = 1L; + + /** + * Construct a session event from the given source. + * + * @param source The HTTP session where the change took place + */ + public HttpSessionEvent(HttpSession source) { + super(source); + } + + /** + * Get the session that changed. + * + * @return The session that changed + */ + public HttpSession getSession() { + return (HttpSession) super.getSource(); + } +} diff --git a/java/jakarta/servlet/http/HttpSessionIdListener.java b/java/jakarta/servlet/http/HttpSessionIdListener.java new file mode 100644 index 0000000..e48ec3d --- /dev/null +++ b/java/jakarta/servlet/http/HttpSessionIdListener.java @@ -0,0 +1,41 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.EventListener; + +/** + * Implementations of this interface are notified when an {@link HttpSession}'s ID changes. To receive notification + * events, the implementation class must be configured in the deployment descriptor for the web application, annotated + * with {@link jakarta.servlet.annotation.WebListener} or registered by calling an addListener method on the + * {@link jakarta.servlet.ServletContext}. + * + * @see HttpSessionEvent + * @see HttpServletRequest#changeSessionId() + * + * @since Servlet 3.1 + */ +public interface HttpSessionIdListener extends EventListener { + + /** + * Notification that a session ID has been changed. + * + * @param se the notification event + * @param oldSessionId the old session ID + */ + void sessionIdChanged(HttpSessionEvent se, String oldSessionId); +} diff --git a/java/jakarta/servlet/http/HttpSessionListener.java b/java/jakarta/servlet/http/HttpSessionListener.java new file mode 100644 index 0000000..bd3fd17 --- /dev/null +++ b/java/jakarta/servlet/http/HttpSessionListener.java @@ -0,0 +1,47 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.EventListener; + +/** + * Implementations of this interface are notified of changes to the list of active sessions in a web application. To + * receive notification events, the implementation class must be configured in the deployment descriptor for the web + * application. + * + * @see HttpSessionEvent + * + * @since Servlet 2.3 + */ +public interface HttpSessionListener extends EventListener { + + /** + * Notification that a session was created. The default implementation is a NO-OP. + * + * @param se the notification event + */ + default void sessionCreated(HttpSessionEvent se) { + } + + /** + * Notification that a session is about to be invalidated. The default implementation is a NO-OP. + * + * @param se the notification event + */ + default void sessionDestroyed(HttpSessionEvent se) { + } +} diff --git a/java/jakarta/servlet/http/HttpUpgradeHandler.java b/java/jakarta/servlet/http/HttpUpgradeHandler.java new file mode 100644 index 0000000..40930a9 --- /dev/null +++ b/java/jakarta/servlet/http/HttpUpgradeHandler.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.servlet.http; + +/** + * Interface between the HTTP upgrade process and the new protocol. + * + * @since Servlet 3.1 + */ +public interface HttpUpgradeHandler { + + /** + * This method is called once the request/response pair where {@link HttpServletRequest#upgrade(Class)} is called + * has completed processing and is the point where control of the connection passes from the container to the + * {@link HttpUpgradeHandler}. + * + * @param connection The connection that has been upgraded + */ + void init(WebConnection connection); + + /** + * This method is called after the upgraded connection has been closed. + */ + void destroy(); +} diff --git a/java/jakarta/servlet/http/LocalStrings.properties b/java/jakarta/servlet/http/LocalStrings.properties new file mode 100644 index 0000000..f9fbd6c --- /dev/null +++ b/java/jakarta/servlet/http/LocalStrings.properties @@ -0,0 +1,31 @@ +# 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. + +cookie.attribute.invalidName.notToken=Cookie attribute name [{0}] is not valid as it is not a token +cookie.attribute.invalidName.null=Cookie attribute names may not be null + +err.cookie_name_blank=Cookie name may not be null or zero length +err.cookie_name_is_token=Cookie name [{0}] is a reserved token +err.io.indexOutOfBounds=Invalid offset [{0}] and / or length [{1}] specified for array of size [{2}] +err.io.nullArray=Null passed for byte array in write method +err.io.short_read=Short Read +err.state.commit=Not permitted once response has been committed + +http.method_delete_not_supported=HTTP method DELETE is not supported by this URL +http.method_get_not_supported=HTTP method GET is not supported by this URL +http.method_not_implemented=Method [{0}] is not implemented by this Servlet for this URI +http.method_post_not_supported=HTTP method POST is not supported by this URL +http.method_put_not_supported=HTTP method PUT is not supported by this URL +http.non_http=Non HTTP request or response diff --git a/java/jakarta/servlet/http/LocalStrings_de.properties b/java/jakarta/servlet/http/LocalStrings_de.properties new file mode 100644 index 0000000..ace9cd9 --- /dev/null +++ b/java/jakarta/servlet/http/LocalStrings_de.properties @@ -0,0 +1,18 @@ +# 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. + +err.io.nullArray=Null wurde als Byte-Array an die Schreibmethode übergeben + +http.method_not_implemented=Methode [{0}] ist von diesem Servlet für diese URI nicht implementiert diff --git a/java/jakarta/servlet/http/LocalStrings_es.properties b/java/jakarta/servlet/http/LocalStrings_es.properties new file mode 100644 index 0000000..0b7972e --- /dev/null +++ b/java/jakarta/servlet/http/LocalStrings_es.properties @@ -0,0 +1,25 @@ +# 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. + +err.cookie_name_blank=El nombre del Cookie no puede ser nulo o de longitud cero +err.cookie_name_is_token=El nombre de Cookie [{0}] es una palabra reservada +err.io.nullArray=Se pasó un valor Null para el arreglo byte en el método de escritura +err.io.short_read=Lectura Corta + +http.method_delete_not_supported=El Metodo HTTP DELETE no es soportado por esta URL +http.method_get_not_supported=El Metodo HTTP GET no está soportado por esta URL +http.method_not_implemented=El Metodo [{0}] no esta implementado por este servlet para esta URI +http.method_post_not_supported=El Metodo HTTP POST no está soportado por esta URL +http.method_put_not_supported=El Metodo HTTP PUT no está soportado por esta URL diff --git a/java/jakarta/servlet/http/LocalStrings_fr.properties b/java/jakarta/servlet/http/LocalStrings_fr.properties new file mode 100644 index 0000000..bdf7898 --- /dev/null +++ b/java/jakarta/servlet/http/LocalStrings_fr.properties @@ -0,0 +1,31 @@ +# 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. + +cookie.attribute.invalidName.notToken=Le nom de l''attribut [{0}] du cookie n''est pas valide car ce n''est pas un jeton +cookie.attribute.invalidName.null=Le nom d'un attribut de cookie ne peut pas être null + +err.cookie_name_blank=Le nom de cookie ne doit pas être null ou vide +err.cookie_name_is_token=Le nom de cookie [{0}] est un "token" réservé +err.io.indexOutOfBounds=L''offset [{0}] et/ou la longueur [{1}] spécifiés pour la taille du tableau [{2}] sont invalides +err.io.nullArray=Null a été passée comme tableau d'octets à la méthode d'écriture +err.io.short_read=Lecture partielle +err.state.commit=Interdit une fois que la réponse a été commitée + +http.method_delete_not_supported=La méthode HTTP DELETE n'est pas supportée par cette URL +http.method_get_not_supported=La méthode HTTP GET n'est pas supportée par cette URL +http.method_not_implemented=Le méthode [{0}] n''est pas implémentée par ce Servlet pour cette URI +http.method_post_not_supported=La méthode HTTP POST n'est pas supportée par cette URL +http.method_put_not_supported=La méthode HTTP PUT n'est pas supportée par cette URL +http.non_http=Requête ou réponse non HTTP diff --git a/java/jakarta/servlet/http/LocalStrings_ja.properties b/java/jakarta/servlet/http/LocalStrings_ja.properties new file mode 100644 index 0000000..70548e2 --- /dev/null +++ b/java/jakarta/servlet/http/LocalStrings_ja.properties @@ -0,0 +1,31 @@ +# 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. + +cookie.attribute.invalidName.notToken=Cookie属性å[{0}]ã¯ãƒˆãƒ¼ã‚¯ãƒ³ã§ã¯ãªã„ãŸã‚無効ã§ã™ +cookie.attribute.invalidName.null=Cookieã®å±žæ€§åã‚’nullã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ + +err.cookie_name_blank=Cookieåã¯nullã¾ãŸã¯é•·ã•ã‚¼ãƒ­ã§ã‚ã£ã¦ã¯ãªã‚Šã¾ã›ã‚“.\n +err.cookie_name_is_token=クッキーå [{0}] ã¯äºˆç´„済ã®ãƒˆãƒ¼ã‚¯ãƒ³ã§ã™ã€‚ +err.io.indexOutOfBounds=サイズ [{2}] ã®é…列ã«æŒ‡å®šã•ã‚ŒãŸã‚ªãƒ•ã‚»ãƒƒãƒˆ [{0}] ã¾ãŸã¯é•·ã• [{1}] ãŒç„¡åŠ¹ã§ã™ +err.io.nullArray=write メソッドã«æ¸¡ã•ã‚ŒãŸãƒã‚¤ãƒˆé…列㯠null ã§ã™ã€‚ +err.io.short_read=読ã¿è¾¼ã¿ãŒã™ãã«çµ‚ã‚ã‚Šã¾ã—ãŸã€‚ +err.state.commit=レスãƒãƒ³ã‚¹ãŒã‚³ãƒŸãƒƒãƒˆã•ã‚ŒãŸå¾Œã¯è¨±å¯ã•ã‚Œã¾ã›ã‚“ + +http.method_delete_not_supported=HTTPã®DELETEメソッドã¯ã€ã“ã®URLã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +http.method_get_not_supported=HTTPã®GETメソッドã¯ã€ã“ã®URLã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +http.method_not_implemented=メソッド [{0}] 㯠RFC 2068 ã«ã¯å®šç¾©ã•ã‚Œã¦ãŠã‚‰ãšã€ã‚µãƒ¼ãƒ–レット API ã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¾ã›ã‚“ +http.method_post_not_supported=HTTPã®POSTメソッドã¯ã€ã“ã®URLã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +http.method_put_not_supported=HTTPã®PUTメソッドã¯ã€ã“ã®URLã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +http.non_http=リクエスト㌠HTTP リクエストã§ã¯ãªã„ã€ã‚ã‚‹ã„ã¯ãƒ¬ã‚¹ãƒãƒ³ã‚¹ãŒ HTTP レスãƒãƒ³ã‚¹ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 diff --git a/java/jakarta/servlet/http/LocalStrings_ko.properties b/java/jakarta/servlet/http/LocalStrings_ko.properties new file mode 100644 index 0000000..30cdc43 --- /dev/null +++ b/java/jakarta/servlet/http/LocalStrings_ko.properties @@ -0,0 +1,31 @@ +# 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. + +cookie.attribute.invalidName.notToken=쿠키 ì†ì„± ì´ë¦„ [{0}]ì´(ê°€) 토í°ì´ 아니므로 유효하지 않습니다. +cookie.attribute.invalidName.null=쿠키 ì†ì„± ì´ë¦„ë“¤ì€ ë„ ê°’ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. + +err.cookie_name_blank=쿠키 ì´ë¦„ì´ ë„ì´ê±°ë‚˜ 길ì´ê°€ 0ì¸ ë¬¸ìžì—´ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +err.cookie_name_is_token=쿠키 ì´ë¦„ [{0}]ì€(는) ì˜ˆì•½ëœ í† í°ìž…니다. +err.io.indexOutOfBounds=í¬ê¸° [{2}]ì¸ ë°°ì—´ì— ëŒ€í•˜ì—¬, 유효하지 ì•Šì€ offset [{0}] 그리고/ë˜ëŠ” ê¸¸ì´ [{1}]. +err.io.nullArray=write ë©”ì†Œë“œì— ë„ì¸ ë°”ì´íŠ¸ ë°°ì—´ì´ ì „ë‹¬ë˜ì—ˆìŠµë‹ˆë‹¤. +err.io.short_read=Short Read +err.state.commit=ì‘ë‹µì´ í•œë²ˆ ì»¤ë°‹ëœ ì´í›„ì—는 허용ë˜ì§€ 않습니다. + +http.method_delete_not_supported=HTTP 메소드 DELETE는 ì´ URLì— ì˜í•´ 지ì›ë˜ì§€ 않습니다. +http.method_get_not_supported=HTTP 메소드 GETì€ ì´ URLì— ì˜í•´ 지ì›ë˜ì§€ 않습니다. +http.method_not_implemented=ì´ URI를 위한 ì„œë¸”ë¦¿ì€ ë©”ì†Œë“œ [{0}]ì„(를) 구현하지 않았습니다. +http.method_post_not_supported=HTTP ë©”ì†Œë“œì¸ POST는 ì´ URLì— ì˜í•´ 지ì›ë˜ì§€ 않습니다. +http.method_put_not_supported=HTTP 메소드 PUTì€ ì´ URLì— ì˜í•´ 지ì›ë˜ì§€ 않습니다. +http.non_http=HTTP ìš”ì²­ì´ ì•„ë‹ˆê±°ë‚˜, HTTP ì‘ë‹µì´ ì•„ë‹™ë‹ˆë‹¤. diff --git a/java/jakarta/servlet/http/LocalStrings_zh_CN.properties b/java/jakarta/servlet/http/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..04f0508 --- /dev/null +++ b/java/jakarta/servlet/http/LocalStrings_zh_CN.properties @@ -0,0 +1,31 @@ +# 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. + +cookie.attribute.invalidName.notToken=Cookie属性å[{0}]无效,因为它ä¸æ˜¯ä»¤ç‰Œ +cookie.attribute.invalidName.null=Cookie属性å称ä¸èƒ½ä¸ºç©º + +err.cookie_name_blank=Cookieå称ä¸èƒ½ä¸ºnull或零长度 +err.cookie_name_is_token=Cookie name[{0}]是一个ä¿ç•™ä»¤ç‰Œ +err.io.indexOutOfBounds=为大å°ä¸º[{2}]的数组指定的å移é‡[{0}]å’Œ/或长度[{1}]无效。 +err.io.nullArray=Null在write方法中传递给字节数组 +err.io.short_read=短.读 +err.state.commit=å“应一旦被æ交åŽå°±æ²¡æœ‰å‡†è®¸ + +http.method_delete_not_supported=当å‰URLä¸æ”¯æŒHTTPçš„DELETE方法 +http.method_get_not_supported=æ­¤URLä¸æ”¯æŒHttp方法GET +http.method_not_implemented=这个servlet没有为这个URI实现方法[{0}] +http.method_post_not_supported=æ­¤URLä¸æ”¯æŒHttp方法POST +http.method_put_not_supported=æ­¤URLä¸æ”¯æŒHTTP方法PUT +http.non_http=没有HTTP请求或å“应 diff --git a/java/jakarta/servlet/http/MappingMatch.java b/java/jakarta/servlet/http/MappingMatch.java new file mode 100644 index 0000000..c352d36 --- /dev/null +++ b/java/jakarta/servlet/http/MappingMatch.java @@ -0,0 +1,50 @@ +/* + * 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 jakarta.servlet.http; + +/** + * Represents the ways that a request can be mapped to a servlet + * + * @since Servlet 4.0 + */ +public enum MappingMatch { + + /** + * The request was mapped to the servlet via the context root URL pattern of {@code ""}. + */ + CONTEXT_ROOT, + + /** + * The request was mapped to the servlet via the default servlet URL pattern of {@code "/"} . + */ + DEFAULT, + + /** + * The request was mapped to the servlet using an exact URL pattern match. + */ + EXACT, + + /** + * The request was mapped to the servlet using an extension URL pattern match. + */ + EXTENSION, + + /** + * The request was mapped to the servlet using a path URL pattern. + */ + PATH +} diff --git a/java/jakarta/servlet/http/Part.java b/java/jakarta/servlet/http/Part.java new file mode 100644 index 0000000..f60a259 --- /dev/null +++ b/java/jakarta/servlet/http/Part.java @@ -0,0 +1,124 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; + +/** + * This class represents a part as uploaded to the server as part of a multipart/form-data request body. + * The part may represent either an uploaded file or form data. + * + * @since Servlet 3.0 + */ +public interface Part { + + /** + * Obtain an InputStream that can be used to retrieve the contents of the file. + * + * @return An InputStream for the contents of the file + * + * @throws IOException if an I/O occurs while obtaining the stream + */ + InputStream getInputStream() throws IOException; + + /** + * Obtain the content type passed by the browser. + * + * @return The content type passed by the browser or null if not defined. + */ + String getContentType(); + + /** + * Obtain the name of the field in the multipart form corresponding to this part. + * + * @return The name of the field in the multipart form corresponding to this part. + */ + String getName(); + + /** + * If this part represents an uploaded file, gets the file name submitted in the upload. Returns {@code null} if no + * file name is available or if this part is not a file upload. + * + * @return the submitted file name or {@code null}. + * + * @since Servlet 3.1 + */ + String getSubmittedFileName(); + + /** + * Obtain the size of this part. + * + * @return The size of the part if bytes + */ + long getSize(); + + /** + * A convenience method to write an uploaded part to disk. The client code is not concerned with whether or not the + * part is stored in memory, or on disk in a temporary location. They just want to write the uploaded part to a + * file. This method is not guaranteed to succeed if called more than once for the same part. This allows a + * particular implementation to use, for example, file renaming, where possible, rather than copying all of the + * underlying data, thus gaining a significant performance benefit. + * + * @param fileName The location into which the uploaded part should be stored. Relative locations are relative to + * {@link jakarta.servlet.MultipartConfigElement#getLocation()} + * + * @throws IOException if an I/O occurs while attempting to write the part + */ + void write(String fileName) throws IOException; + + /** + * Deletes the underlying storage for a part, including deleting any associated temporary disk file. Although the + * container will delete this storage automatically this method can be used to ensure that this is done at an + * earlier time, thus preserving system resources. + *

+ * Containers are only required to delete the associated storage when the Part instance is garbage collected. Apache + * Tomcat will delete the associated storage when the associated request has finished processing. Behaviour of other + * containers may be different. + * + * @throws IOException if an I/O occurs while attempting to delete the part + */ + void delete() throws IOException; + + /** + * Obtains the value of the specified part header as a String. If there are multiple headers with the same name, + * this method returns the first header in the part. The header name is case insensitive. + * + * @param name Header name + * + * @return The header value or null if the header is not present + */ + String getHeader(String name); + + /** + * Obtain all the values of the specified part header. + * + * @param name The name of the header of interest. The header name is case insensitive. + * + * @return All the values of the specified part header. If the part did not include any headers of the specified + * name, this method returns an empty Collection. + */ + Collection getHeaders(String name); + + /** + * Get the header names provided for this part. + * + * @return a Collection of all the header names provided for this part. + */ + Collection getHeaderNames(); +} diff --git a/java/jakarta/servlet/http/PushBuilder.java b/java/jakarta/servlet/http/PushBuilder.java new file mode 100644 index 0000000..9a10920 --- /dev/null +++ b/java/jakarta/servlet/http/PushBuilder.java @@ -0,0 +1,175 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.Set; + +/** + * Builds a push request based on the {@link HttpServletRequest} from which this builder was obtained. The push request + * will be constructed on the following basis: + *

    + *
  • The request method is set to GET.
  • + *
  • The path will not be set. This must be set explicitly via a call to {@link #path(String)}.
  • + *
  • Conditional, range, expectation, authorization and referer headers will be removed.
  • + *
  • Cookies added to the associated response will be added to the push request unless maxAge <= 0 in which case + * any request cookie with the same name will be removed.
  • + *
  • The referer header will be set to {@link HttpServletRequest#getRequestURL()} plus, if present, the query string + * from {@link HttpServletRequest#getQueryString()}. + *
+ * + * @since Servlet 4.0 + */ +public interface PushBuilder { + + /** + * Specify the HTTP method to use for the push request. + * + * @param method The method to use for the push request + * + * @return This builder instance + * + * @throws IllegalArgumentException if an HTTP method is specified that is known not to be + * cacheable and + * safe. POST, PUT, DELETE, CONNECT, OPTIONS and TRACE will trigger the + * exception. + */ + PushBuilder method(String method); + + /** + * Specifies the query string to use in subsequent push requests generated by a call to {@link #push()}. This will + * be appended to any query string specified in the call to {@link #path(String)}. + * + * @param queryString The query string to use to generate push requests + * + * @return This builder instance + */ + PushBuilder queryString(String queryString); + + /** + * Specifies the session ID to use in subsequent push requests generated by a call to {@link #push()}. The session + * ID will be presented the same way as it is on the original request (cookie or URL parameter). The default is + * determined in the following order: + *
    + *
  • the requested session ID for the originating request
  • + *
  • the session ID generated in the originated request
  • + *
  • {@code null}
  • + *
+ * + * @param sessionId The session ID to use to generate push requests + * + * @return This builder instance + */ + PushBuilder sessionId(String sessionId); + + /** + * Sets an HTTP header on the request. Any existing headers of the same name are first remove. + * + * @param name The name of the header to set + * @param value The value of the header to set + * + * @return This builder instance + */ + PushBuilder setHeader(String name, String value); + + /** + * Adds an HTTP header to the request. + * + * @param name The name of the header to add + * @param value The value of the header to add + * + * @return This builder instance + */ + PushBuilder addHeader(String name, String value); + + /** + * Removes an HTTP header from the request. + * + * @param name The name of the header to remove + * + * @return This builder instance + */ + PushBuilder removeHeader(String name); + + /** + * Sets the URI path to be used for the push request. This must be called before every call to {@link #push()}. If + * the path includes a query string, the query string will be appended to the existing query string (if any) and no + * de-duplication will occur. + * + * @param path Paths beginning with '/' are treated as absolute paths. All other paths are treated as relative to + * the context path of the request used to create this builder instance. The path may include a + * query string. + * + * @return This builder instance + */ + PushBuilder path(String path); + + /** + * Generates the push request and sends it to the client unless pushes are not available for some reason. After + * calling this method the following fields are set to {@code null}: + *
    + *
  • {@code path}
  • + *
  • conditional request headers ({@code if-none-match} and {@code if-modified-since})
  • + *
+ * + * @throws IllegalStateException If this method is called when {@code path} is {@code null} + * @throws IllegalArgumentException If the request to push requires a body + */ + void push(); + + /** + * Obtain the name of the HTTP method that will be used for push requests generated by future calls to + * {@code push()}. + * + * @return The HTTP method to be used for future push requests + */ + String getMethod(); + + /** + * Obtain the query string that will be used for push requests generated by future calls to {@code push()}. + * + * @return The query string that will be appended to push requests. + */ + String getQueryString(); + + /** + * Obtain the session ID that will be used for push requests generated by future calls to {@code push()}. + * + * @return The session that will be used for push requests. + */ + String getSessionId(); + + /** + * @return The current set of names of HTTP headers to be used the next time {@code push()} is called. + */ + Set getHeaderNames(); + + /** + * Obtain a value for the given HTTP header. TODO Servlet 4.0 Clarify the behaviour of this method + * + * @param name The name of the header whose value is to be returned + * + * @return The value of the given header. If multiple values are defined then any may be returned + */ + String getHeader(String name); + + /** + * Obtain the path that will be used for the push request that will be generated by the next call to {@code push()}. + * + * @return The path value that will be associated with the next push request + */ + String getPath(); +} diff --git a/java/jakarta/servlet/http/WebConnection.java b/java/jakarta/servlet/http/WebConnection.java new file mode 100644 index 0000000..f9ff100 --- /dev/null +++ b/java/jakarta/servlet/http/WebConnection.java @@ -0,0 +1,48 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; + +/** + * The interface used by an {@link HttpUpgradeHandler} to interact with an upgraded HTTP connection. + * + * @since Servlet 3.1 + */ +public interface WebConnection extends AutoCloseable { + + /** + * Provides access to the {@link ServletInputStream} for reading data from the client. + * + * @return the input stream + * + * @throws IOException If an I/O occurs while obtaining the stream + */ + ServletInputStream getInputStream() throws IOException; + + /** + * Provides access to the {@link ServletOutputStream} for writing data to the client. + * + * @return the output stream + * + * @throws IOException If an I/O occurs while obtaining the stream + */ + ServletOutputStream getOutputStream() throws IOException; +} \ No newline at end of file diff --git a/java/jakarta/servlet/http/package.html b/java/jakarta/servlet/http/package.html new file mode 100644 index 0000000..b418dc8 --- /dev/null +++ b/java/jakarta/servlet/http/package.html @@ -0,0 +1,30 @@ + + + + + + + +The jakarta.servlet.http package contains a number of classes and interfaces +that describe and define the contracts between a servlet class +running under the HTTP protocol and the runtime environment provided +for an instance of such a class by a conforming servlet container. + + + + diff --git a/java/jakarta/servlet/jsp/ErrorData.java b/java/jakarta/servlet/jsp/ErrorData.java new file mode 100644 index 0000000..82e641d --- /dev/null +++ b/java/jakarta/servlet/jsp/ErrorData.java @@ -0,0 +1,85 @@ +/* + * 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 jakarta.servlet.jsp; + +/** + * Contains information about an error, for error pages. The information contained in this instance is meaningless if + * not used in the context of an error page. To indicate a JSP is an error page, the page author must set the + * isErrorPage attribute of the page directive to "true". + * + * @see PageContext#getErrorData + * + * @since JSP 2.0 + */ +public final class ErrorData { + + private final Throwable throwable; + private final int statusCode; + private final String uri; + private final String servletName; + + /** + * Creates a new ErrorData object. + * + * @param throwable The Throwable that is the cause of the error + * @param statusCode The status code of the error + * @param uri The request URI + * @param servletName The name of the servlet invoked + */ + public ErrorData(Throwable throwable, int statusCode, String uri, String servletName) { + this.throwable = throwable; + this.statusCode = statusCode; + this.uri = uri; + this.servletName = servletName; + } + + /** + * Returns the Throwable that caused the error. + * + * @return The Throwable that caused the error + */ + public Throwable getThrowable() { + return this.throwable; + } + + /** + * Returns the status code of the error. + * + * @return The status code of the error + */ + public int getStatusCode() { + return this.statusCode; + } + + /** + * Returns the request URI. + * + * @return The request URI + */ + public String getRequestURI() { + return this.uri; + } + + /** + * Returns the name of the servlet invoked. + * + * @return The name of the servlet invoked + */ + public String getServletName() { + return this.servletName; + } +} diff --git a/java/jakarta/servlet/jsp/HttpJspPage.java b/java/jakarta/servlet/jsp/HttpJspPage.java new file mode 100644 index 0000000..d7e6d6d --- /dev/null +++ b/java/jakarta/servlet/jsp/HttpJspPage.java @@ -0,0 +1,53 @@ +/* + * 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 jakarta.servlet.jsp; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * The HttpJspPage interface describes the interaction that a JSP Page Implementation Class must satisfy when using the + * HTTP protocol. + *

+ * The behaviour is identical to that of the JspPage, except for the signature of the _jspService method, which is now + * expressible in the Java type system and included explicitly in the interface. + * + * @see JspPage + */ + +public interface HttpJspPage extends JspPage { + + /** + * The _jspService()method corresponds to the body of the JSP page. This method is defined automatically by the JSP + * container and should never be defined by the JSP page author. + *

+ * If a superclass is specified using the extends attribute, that superclass may choose to perform some actions in + * its service() method before or after calling the _jspService() method. See using the extends attribute in the + * JSP_Engine chapter of the JSP specification. + * + * @param request Provides client request information to the JSP. + * @param response Assists the JSP in sending a response to the client. + * + * @throws ServletException Thrown if an error occurred during the processing of the JSP and that the container + * should take appropriate action to clean up the request. + * @throws IOException Thrown if an error occurred while writing the response for this page. + */ + void _jspService(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; +} diff --git a/java/jakarta/servlet/jsp/JspApplicationContext.java b/java/jakarta/servlet/jsp/JspApplicationContext.java new file mode 100644 index 0000000..cdec5b9 --- /dev/null +++ b/java/jakarta/servlet/jsp/JspApplicationContext.java @@ -0,0 +1,77 @@ +/* + * 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 jakarta.servlet.jsp; + +import jakarta.el.ELContextListener; +import jakarta.el.ELResolver; +import jakarta.el.ExpressionFactory; + +/** + *

+ * Stores application-scoped information for the JSP container. + *

+ * + * @since JSP 2.1 + */ +public interface JspApplicationContext { + + /** + * Registers an ELContextListener that will be notified whenever a new ELContext is + * created. + *

+ * At the very least, any ELContext instantiated will have reference to the JspContext + * under JspContext.class. + * + * @param listener The listener to add + */ + void addELContextListener(ELContextListener listener); + + /** + *

+ * Adds an ELResolver to the chain of EL variable and property management within JSP pages and Tag + * files. + *

+ *

+ * JSP has a default set of ELResolvers to chain for all EL evaluation: + *

+ *
    + *
  • ImplicitObjectELResolver
  • + *
  • ELResolver instances registered with this method
  • + *
  • MapELResolver
  • + *
  • ListELResolver
  • + *
  • ArrayELResolver
  • + *
  • BeanELResolver
  • + *
  • ScopedAttributeELResolver
  • + *
+ * + * @param resolver an additional resolver + * + * @throws IllegalStateException if called after the application's ServletContextListeners have been + * initialized. + */ + void addELResolver(ELResolver resolver); + + /** + *

+ * Returns the JSP container's ExpressionFactory implementation for EL use. + *

+ * + * @return an ExpressionFactory implementation + */ + ExpressionFactory getExpressionFactory(); + +} diff --git a/java/jakarta/servlet/jsp/JspContext.java b/java/jakarta/servlet/jsp/JspContext.java new file mode 100644 index 0000000..4135971 --- /dev/null +++ b/java/jakarta/servlet/jsp/JspContext.java @@ -0,0 +1,261 @@ +/* + * 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 jakarta.servlet.jsp; + +import java.util.Enumeration; + +import jakarta.el.ELContext; + +/** + *

+ * JspContext serves as the base class for the PageContext class and abstracts all information that is not + * specific to servlets. This allows for Simple Tag Extensions to be used outside of the context of a request/response + * Servlet. + *

+ * The JspContext provides a number of facilities to the page/component author and page implementor, including: + *

    + *
  • a single API to manage the various scoped namespaces + *
  • a mechanism to obtain the JspWriter for output + *
  • a mechanism to expose page directive attributes to the scripting environment + *
+ *

+ * Methods Intended for Container Generated Code + *

+ * The following methods enable the management of nested JspWriter streams to implement Tag Extensions: + * pushBody() and popBody() + *

+ * Methods Intended for JSP authors + *

+ * Some methods provide uniform access to the diverse objects representing scopes. The implementation must use + * the underlying machinery corresponding to that scope, so information can be passed back and forth between the + * underlying environment (e.g. Servlets) and JSP pages. The methods are: setAttribute(), + * getAttribute(), findAttribute(), removeAttribute(), + * getAttributesScope() and getAttributeNamesInScope(). + *

+ * The following methods provide convenient access to implicit objects: getOut() + *

+ * The following methods provide programmatic access to the Expression Language evaluator: + * getExpressionEvaluator(), getVariableResolver() + * + * @since JSP 2.0 + */ + +public abstract class JspContext { + + /** + * Sole constructor. (For invocation by subclass constructors, typically implicit.) + */ + public JspContext() { + // NOOP by default + } + + /** + * Register the name and value specified with page scope semantics. If the value passed in is null, + * this has the same effect as calling removeAttribute( name, PageContext.PAGE_SCOPE ). + * + * @param name the name of the attribute to set + * @param value the value to associate with the name, or null if the attribute is to be removed from the page scope. + * + * @throws NullPointerException if the name is null + */ + + public abstract void setAttribute(String name, Object value); + + /** + * Register the name and value specified with appropriate scope semantics. If the value passed in is + * null, this has the same effect as calling removeAttribute( name, scope ). + * + * @param name the name of the attribute to set + * @param value the object to associate with the name, or null if the attribute is to be removed from the specified + * scope. + * @param scope the scope with which to associate the name/object + * + * @throws NullPointerException if the name is null + * @throws IllegalArgumentException if the scope is invalid + * @throws IllegalStateException if the scope is PageContext.SESSION_SCOPE but the page that was requested does + * not participate in a session or the session has been invalidated. + */ + + public abstract void setAttribute(String name, Object value, int scope); + + /** + * Returns the object associated with the name in the page scope or null if not found. + * + * @param name the name of the attribute to get + * + * @return the object associated with the name in the page scope or null if not found. + * + * @throws NullPointerException if the name is null + */ + + public abstract Object getAttribute(String name); + + /** + * Return the object associated with the name in the specified scope or null if not found. + * + * @param name the name of the attribute to set + * @param scope the scope with which to associate the name/object + * + * @return the object associated with the name in the specified scope or null if not found. + * + * @throws NullPointerException if the name is null + * @throws IllegalArgumentException if the scope is invalid + * @throws IllegalStateException if the scope is PageContext.SESSION_SCOPE but the page that was requested does + * not participate in a session or the session has been invalidated. + */ + + public abstract Object getAttribute(String name, int scope); + + /** + * Searches for the named attribute in page, request, session (if valid), and application scope(s) in order and + * returns the value associated or null. + * + * @param name the name of the attribute to search for + * + * @return the value associated or null + * + * @throws NullPointerException if the name is null + */ + + public abstract Object findAttribute(String name); + + /** + * Remove the object reference associated with the given name from all scopes. Does nothing if there is no such + * object. + * + * @param name The name of the object to remove. + * + * @throws NullPointerException if the name is null + */ + + public abstract void removeAttribute(String name); + + /** + * Remove the object reference associated with the specified name in the given scope. Does nothing if there is no + * such object. + * + * @param name The name of the object to remove. + * @param scope The scope where to look. + * + * @throws IllegalArgumentException if the scope is invalid + * @throws IllegalStateException if the scope is PageContext.SESSION_SCOPE but the page that was requested does + * not participate in a session or the session has been invalidated. + * @throws NullPointerException if the name is null + */ + + public abstract void removeAttribute(String name, int scope); + + /** + * Get the scope where a given attribute is defined. + * + * @param name the name of the attribute to return the scope for + * + * @return the scope of the object associated with the name specified or 0 + * + * @throws NullPointerException if the name is null + */ + + public abstract int getAttributesScope(String name); + + /** + * Enumerate all the attributes in a given scope. + * + * @param scope the scope to enumerate all the attributes for + * + * @return an enumeration of names ({@link String}) of all the attributes the specified scope + * + * @throws IllegalArgumentException if the scope is invalid + * @throws IllegalStateException if the scope is PageContext.SESSION_SCOPE but the page that was requested does + * not participate in a session or the session has been invalidated. + */ + + public abstract Enumeration getAttributeNamesInScope(int scope); + + /** + * The current value of the out object (a JspWriter). + * + * @return the current JspWriter stream being used for client response + */ + public abstract JspWriter getOut(); + + /** + * Provides programmatic access to the ExpressionEvaluator. The JSP Container must return a valid instance of an + * ExpressionEvaluator that can parse EL expressions. + * + * @return A valid instance of an ExpressionEvaluator. + * + * @since JSP 2.0 + * + * @deprecated As of JSP 2.1, replaced by JspApplicationContext.getExpressionFactory() + */ + @Deprecated + public abstract jakarta.servlet.jsp.el.ExpressionEvaluator getExpressionEvaluator(); + + /** + * Obtain the ELContext for this JSPContext. Each JSPContext has a dedicated ELContext. + * + * @return the ELContext for this JSPContext + */ + public abstract ELContext getELContext(); + + /** + * Returns an instance of a VariableResolver that provides access to the implicit objects specified in the JSP + * specification using this JspContext as the context object. + * + * @return A valid instance of a VariableResolver. + * + * @since JSP 2.0 + * + * @deprecated As of JSP 2.1, replaced by jakarta.el.ELContext.getELResolver() which can be obtained by + * jspContext.getELContext().getELResolver() + */ + @Deprecated + public abstract jakarta.servlet.jsp.el.VariableResolver getVariableResolver(); + + /** + * Return a new JspWriter object that sends output to the provided Writer. Saves the current "out" JspWriter, and + * updates the value of the "out" attribute in the page scope attribute namespace of the JspContext. + *

+ * The returned JspWriter must implement all methods and behave as though it were unbuffered. More specifically: + *

+ *
    + *
  • clear() must throw an IOException
  • + *
  • clearBuffer() does nothing
  • + *
  • getBufferSize() always returns 0
  • + *
  • getRemaining() always returns 0
  • + *
+ * + * @param writer The Writer for the returned JspWriter to send output to. + * + * @return a new JspWriter that writes to the given Writer. + * + * @since JSP 2.0 + */ + public JspWriter pushBody(java.io.Writer writer) { + return null; // XXX to implement + } + + /** + * Return the previous JspWriter "out" saved by the matching pushBody(), and update the value of the "out" attribute + * in the page scope attribute namespace of the JspContext. + * + * @return the saved JspWriter. + */ + public JspWriter popBody() { + return null; // XXX to implement + } +} diff --git a/java/jakarta/servlet/jsp/JspEngineInfo.java b/java/jakarta/servlet/jsp/JspEngineInfo.java new file mode 100644 index 0000000..3ccb77b --- /dev/null +++ b/java/jakarta/servlet/jsp/JspEngineInfo.java @@ -0,0 +1,44 @@ +/* + * 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 jakarta.servlet.jsp; + +/** + * The JspEngineInfo is an abstract class that provides information on the current JSP engine. + */ + +public abstract class JspEngineInfo { + + /** + * Sole constructor. (For invocation by subclass constructors, typically implicit.) + */ + public JspEngineInfo() { + // NOOP by default + } + + /** + * Return the version number of the JSP specification that is supported by this JSP engine. + *

+ * Specification version numbers that consists of positive decimal integers separated by periods ".", for example, + * "2.0" or "1.2.3.4.5.6.7". This allows an extensible number to be used to represent major, minor, micro, etc + * versions. The version number must begin with a number. + *

+ * + * @return the specification version, null is returned if it is not known + */ + + public abstract String getSpecificationVersion(); +} diff --git a/java/jakarta/servlet/jsp/JspException.java b/java/jakarta/servlet/jsp/JspException.java new file mode 100644 index 0000000..f061ecc --- /dev/null +++ b/java/jakarta/servlet/jsp/JspException.java @@ -0,0 +1,89 @@ +/* + * 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 jakarta.servlet.jsp; + +/** + * A generic exception known to the JSP engine; uncaught JspExceptions will result in an invocation of the errorpage + * machinery. + */ +public class JspException extends Exception { + + private static final long serialVersionUID = 1L; + + + /** + * Construct a JspException. + */ + public JspException() { + // NOOP + } + + + /** + * Constructs a new JSP exception with the specified message. The message can be written to the server log and/or + * displayed for the user. + * + * @param msg a String specifying the text of the exception message + */ + public JspException(String msg) { + super(msg); + } + + + /** + * Constructs a new JSPException with the specified detail message and cause. The cause is saved for + * later retrieval by the java.lang.Throwable.getCause() and {@link #getRootCause()} methods. + * + * @see java.lang.Exception#Exception(String, Throwable) + * + * @param message a String containing the text of the exception message + * @param cause the Throwable exception that interfered with the JSP's normal operation, making this + * JSP exception necessary + */ + + public JspException(String message, Throwable cause) { + super(message, cause); + } + + + /** + * Constructs a new JSPException with the specified cause. The cause is saved for later retrieval by + * the java.lang.Throwable.getCause() and {@link #getRootCause()} methods. + * + * @see java.lang.Exception#Exception(Throwable) + * + * @param cause the Throwable exception that interfered with the JSP's normal operation, making the JSP + * exception necessary + */ + + public JspException(Throwable cause) { + super(cause); + } + + + /** + * Returns the exception that caused this JSP exception. + * + * @return the Throwable that caused this JSP exception + * + * @deprecated As of JSP 2.1, replaced by java.lang.Throwable.getCause() + */ + @Deprecated + public Throwable getRootCause() { + return getCause(); + } +} diff --git a/java/jakarta/servlet/jsp/JspFactory.java b/java/jakarta/servlet/jsp/JspFactory.java new file mode 100644 index 0000000..4abc2de --- /dev/null +++ b/java/jakarta/servlet/jsp/JspFactory.java @@ -0,0 +1,140 @@ +/* + * 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 jakarta.servlet.jsp; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +/** + *

+ * The JspFactory is an abstract class that defines a number of factory methods available to a JSP page at runtime for + * the purposes of creating instances of various interfaces and classes used to support the JSP implementation. + *

+ * A conformant JSP Engine implementation will, during it's initialization instantiate an implementation dependent + * subclass of this class, and make it globally available for use by JSP implementation classes by registering the + * instance created with this class via the static setDefaultFactory() method. + *

+ * The PageContext and the JspEngineInfo classes are the only implementation-dependent classes that can be created from + * the factory. + *

+ * JspFactory objects should not be used by JSP page authors. + */ + +public abstract class JspFactory { + + private static volatile JspFactory deflt = null; + + /** + * Sole constructor. (For invocation by subclass constructors, typically implicit.) + */ + public JspFactory() { + // NOOP by default + } + + /** + *

+ * set the default factory for this implementation. It is illegal for any principal other than the JSP Engine + * runtime to call this method. + *

+ * + * @param deflt The default factory implementation + */ + + public static void setDefaultFactory(JspFactory deflt) { + JspFactory.deflt = deflt; + } + + /** + * Returns the default factory for this implementation. + * + * @return the default factory for this implementation + */ + + public static JspFactory getDefaultFactory() { + return deflt; + } + + /** + *

+ * obtains an instance of an implementation dependent jakarta.servlet.jsp.PageContext abstract class for the calling + * Servlet and currently pending request and response. + *

+ *

+ * This method is typically called early in the processing of the _jspService() method of a JSP implementation class + * in order to obtain a PageContext object for the request being processed. + *

+ *

+ * Invoking this method shall result in the PageContext.initialize() method being invoked. The PageContext returned + * is properly initialized. + *

+ *

+ * All PageContext objects obtained via this method shall be released by invoking releasePageContext(). + *

+ * + * @param servlet the requesting servlet + * @param request the current request pending on the servlet + * @param response the current response pending on the servlet + * @param errorPageURL the URL of the error page for the requesting JSP, or null + * @param needsSession true if the JSP participates in a session + * @param buffer size of buffer in bytes, {@link JspWriter#NO_BUFFER} if no buffer, + * {@link JspWriter#DEFAULT_BUFFER} if implementation default. + * @param autoflush should the buffer autoflush to the output stream on buffer overflow, or throw an IOException? + * + * @return the page context + * + * @see jakarta.servlet.jsp.PageContext + */ + + public abstract PageContext getPageContext(Servlet servlet, ServletRequest request, ServletResponse response, + String errorPageURL, boolean needsSession, int buffer, boolean autoflush); + + /** + *

+ * called to release a previously allocated PageContext object. Results in PageContext.release() being invoked. This + * method should be invoked prior to returning from the _jspService() method of a JSP implementation class. + *

+ * + * @param pc A PageContext previously obtained by getPageContext() + */ + public abstract void releasePageContext(PageContext pc); + + /** + *

+ * called to get implementation-specific information on the current JSP engine. + *

+ * + * @return a JspEngineInfo object describing the current JSP engine + */ + + public abstract JspEngineInfo getEngineInfo(); + + /** + *

+ * Obtain the JspApplicationContext instance that was associated within the passed + * ServletContext for this web application. + *

+ * + * @param context the current web application's ServletContext + * + * @return JspApplicationContext instance + * + * @since JSP 2.1 + */ + public abstract JspApplicationContext getJspApplicationContext(ServletContext context); +} diff --git a/java/jakarta/servlet/jsp/JspPage.java b/java/jakarta/servlet/jsp/JspPage.java new file mode 100644 index 0000000..bb9222c --- /dev/null +++ b/java/jakarta/servlet/jsp/JspPage.java @@ -0,0 +1,78 @@ +/* + * 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 jakarta.servlet.jsp; + +import jakarta.servlet.Servlet; + +/** + * The JspPage interface describes the generic interaction that a JSP Page Implementation class must satisfy; pages that + * use the HTTP protocol are described by the HttpJspPage interface. + *

+ * Two plus One Methods + *

+ * The interface defines a protocol with 3 methods; only two of them: jspInit() and jspDestroy() are part of this + * interface as the signature of the third method: _jspService() depends on the specific protocol used and cannot be + * expressed in a generic way in Java. + *

+ * A class implementing this interface is responsible for invoking the above methods at the appropriate time based on + * the corresponding Servlet-based method invocations. + *

+ * The jspInit() and jspDestroy() methods can be defined by a JSP author, but the _jspService() method is defined + * automatically by the JSP processor based on the contents of the JSP page. + *

+ * _jspService() + *

+ * The _jspService()method corresponds to the body of the JSP page. This method is defined automatically by the JSP + * container and should never be defined by the JSP page author. + *

+ * If a superclass is specified using the extends attribute, that superclass may choose to perform some actions in its + * service() method before or after calling the _jspService() method. See using the extends attribute in the JSP_Engine + * chapter of the JSP specification. + *

+ * The specific signature depends on the protocol supported by the JSP page. + * + *

+ * public void _jspService(ServletRequestSubtype request,
+ *                             ServletResponseSubtype response)
+ *        throws ServletException, IOException;
+ * 
+ */ + + +public interface JspPage extends Servlet { + + /** + * The jspInit() method is invoked when the JSP page is initialized. It is the responsibility of the JSP + * implementation (and of the class mentioned by the extends attribute, if present) that at this point invocations + * to the getServletConfig() method will return the desired value. + *

+ * A JSP page can override this method by including a definition for it in a declaration element. + *

+ * A JSP page should redefine the init() method from Servlet. + */ + void jspInit(); + + /** + * The jspDestroy() method is invoked when the JSP page is about to be destroyed. + *

+ * A JSP page can override this method by including a definition for it in a declaration element. + *

+ * A JSP page should redefine the destroy() method from Servlet. + */ + void jspDestroy(); + +} diff --git a/java/jakarta/servlet/jsp/JspTagException.java b/java/jakarta/servlet/jsp/JspTagException.java new file mode 100644 index 0000000..9507c9a --- /dev/null +++ b/java/jakarta/servlet/jsp/JspTagException.java @@ -0,0 +1,75 @@ +/* + * 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 jakarta.servlet.jsp; + +/** + * Exception to be used by a Tag Handler to indicate some unrecoverable error. This error is to be caught by the top + * level of the JSP page and will result in an error page. + */ +public class JspTagException extends JspException { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new JspTagException with the specified message. The message can be written to the server log and/or + * displayed for the user. + * + * @param msg a String specifying the text of the exception message + */ + public JspTagException(String msg) { + super(msg); + } + + /** + * Constructs a new JspTagException with no message. + */ + public JspTagException() { + super(); + } + + /** + * Constructs a new JspTagException when the JSP Tag needs to throw an exception and include a message about the + * "root cause" exception that interfered with its normal operation, including a description message. + * + * @param message a String containing the text of the exception message + * @param rootCause the Throwable exception that interfered with the JSP Tag's normal operation, making + * this JSP Tag exception necessary + * + * @since JSP 2.0 + */ + public JspTagException(String message, Throwable rootCause) { + super(message, rootCause); + } + + /** + * Constructs a new JSP Tag exception when the JSP Tag needs to throw an exception and include a message about the + * "root cause" exception that interfered with its normal operation. The exception's message is based on the + * localized message of the underlying exception. + *

+ * This method calls the getLocalizedMessage method on the Throwable exception to get a + * localized exception message. When subclassing JspTagException, this method can be overridden to + * create an exception message designed for a specific locale. + * + * @param rootCause the Throwable exception that interfered with the JSP Tag's normal operation, making + * the JSP Tag exception necessary + * + * @since JSP 2.0 + */ + public JspTagException(Throwable rootCause) { + super(rootCause); + } +} diff --git a/java/jakarta/servlet/jsp/JspWriter.java b/java/jakarta/servlet/jsp/JspWriter.java new file mode 100644 index 0000000..227700f --- /dev/null +++ b/java/jakarta/servlet/jsp/JspWriter.java @@ -0,0 +1,392 @@ +/* + * 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 jakarta.servlet.jsp; + +import java.io.IOException; + +/** + *

+ * The actions and template data in a JSP page is written using the JspWriter object that is referenced by the implicit + * variable out which is initialized automatically using methods in the PageContext object. + *

+ * This abstract class emulates some of the functionality found in the java.io.BufferedWriter and java.io.PrintWriter + * classes, however it differs in that it throws IOException from the print methods while PrintWriter does not. + *

+ * Buffering + *

+ * The initial JspWriter object is associated with the PrintWriter object of the ServletResponse in a way that depends + * on whether the page is or is not buffered. If the page is not buffered, output written to this JspWriter object will + * be written through to the PrintWriter directly, which will be created if necessary by invoking the getWriter() method + * on the response object. But if the page is buffered, the PrintWriter object will not be created until the buffer is + * flushed and operations like setContentType() are legal. Since this flexibility simplifies programming substantially, + * buffering is the default for JSP pages. + *

+ * Buffering raises the issue of what to do when the buffer is exceeded. Two approaches can be taken: + *

    + *
  • Exceeding the buffer is not a fatal error; when the buffer is exceeded, just flush the output. + *
  • Exceeding the buffer is a fatal error; when the buffer is exceeded, raise an exception. + *
+ *

+ * Both approaches are valid, and thus both are supported in the JSP technology. The behavior of a page is controlled by + * the autoFlush attribute, which defaults to true. In general, JSP pages that need to be sure that correct and complete + * data has been sent to their client may want to set autoFlush to false, with a typical case being that where the + * client is an application itself. On the other hand, JSP pages that send data that is meaningful even when partially + * constructed may want to set autoFlush to true; such as when the data is sent for immediate display through a browser. + * Each application will need to consider their specific needs. + *

+ * An alternative considered was to make the buffer size unbounded; but, this had the disadvantage that runaway + * computations would consume an unbounded amount of resources. + *

+ * The "out" implicit variable of a JSP implementation class is of this type. If the page directive selects + * autoflush="true" then all the I/O operations on this class shall automatically flush the contents of the buffer if an + * overflow condition would result if the current operation were performed without a flush. If autoflush="false" then + * all the I/O operations on this class shall throw an IOException if performing the current operation would result in a + * buffer overflow condition. + * + * @see java.io.Writer + * @see java.io.BufferedWriter + * @see java.io.PrintWriter + */ +public abstract class JspWriter extends java.io.Writer { + + /** + * Constant indicating that the Writer is not buffering output. + */ + public static final int NO_BUFFER = 0; + + /** + * Constant indicating that the Writer is buffered and is using the implementation default buffer size. + */ + public static final int DEFAULT_BUFFER = -1; + + /** + * Constant indicating that the Writer is buffered and is unbounded; this is used in BodyContent. + */ + public static final int UNBOUNDED_BUFFER = -2; + + /** + * Protected constructor. + * + * @param bufferSize the size of the buffer to be used by the JspWriter + * @param autoFlush whether the JspWriter should be autoflushing + */ + protected JspWriter(int bufferSize, boolean autoFlush) { + this.bufferSize = bufferSize; + this.autoFlush = autoFlush; + } + + /** + * Write a line separator. The line separator string is defined by the system property line.separator, + * and is not necessarily a single newline ('\n') character. + * + * @exception IOException If an I/O error occurs + */ + public abstract void newLine() throws IOException; + + /** + * Print a boolean value. The string produced by {@link + * String#valueOf(boolean)} is written to the JspWriter's buffer or, if no buffer is used, directly to the + * underlying writer. + * + * @param b The boolean to be printed + * + * @throws IOException If an error occurred while writing + */ + public abstract void print(boolean b) throws IOException; + + /** + * Print a character. The character is written to the JspWriter's buffer or, if no buffer is used, directly to the + * underlying writer. + * + * @param c The char to be printed + * + * @throws IOException If an error occurred while writing + */ + public abstract void print(char c) throws IOException; + + /** + * Print an integer. The string produced by {@link + * String#valueOf(int)} is written to the JspWriter's buffer or, if no buffer is used, directly to the + * underlying writer. + * + * @param i The int to be printed + * + * @see java.lang.Integer#toString(int) + * + * @throws IOException If an error occurred while writing + */ + public abstract void print(int i) throws IOException; + + /** + * Print a long integer. The string produced by {@link + * String#valueOf(long)} is written to the JspWriter's buffer or, if no buffer is used, directly to the + * underlying writer. + * + * @param l The long to be printed + * + * @see java.lang.Long#toString(long) + * + * @throws IOException If an error occurred while writing + */ + public abstract void print(long l) throws IOException; + + /** + * Print a floating-point number. The string produced by {@link + * String#valueOf(float)} is written to the JspWriter's buffer or, if no buffer is used, directly to the + * underlying writer. + * + * @param f The float to be printed + * + * @see java.lang.Float#toString(float) + * + * @throws IOException If an error occurred while writing + */ + public abstract void print(float f) throws IOException; + + /** + * Print a double-precision floating-point number. The string produced by + * {@link String#valueOf(double)} is written to the JspWriter's buffer or, if no buffer is used, + * directly to the underlying writer. + * + * @param d The double to be printed + * + * @see java.lang.Double#toString(double) + * + * @throws IOException If an error occurred while writing + */ + public abstract void print(double d) throws IOException; + + /** + * Print an array of characters. The characters are written to the JspWriter's buffer or, if no buffer is used, + * directly to the underlying writer. + * + * @param s The array of chars to be printed + * + * @throws NullPointerException If s is null + * @throws IOException If an error occurred while writing + */ + public abstract void print(char s[]) throws IOException; + + /** + * Print a string. If the argument is null then the string "null" is printed. Otherwise, + * the string's characters are written to the JspWriter's buffer or, if no buffer is used, directly to the + * underlying writer. + * + * @param s The String to be printed + * + * @throws IOException If an error occurred while writing + */ + public abstract void print(String s) throws IOException; + + /** + * Print an object. The string produced by the {@link + * String#valueOf(Object)} method is written to the JspWriter's buffer or, if no buffer is used, directly to + * the underlying writer. + * + * @param obj The Object to be printed + * + * @see java.lang.Object#toString() + * + * @throws IOException If an error occurred while writing + */ + public abstract void print(Object obj) throws IOException; + + /** + * Terminate the current line by writing the line separator string. The line separator string is defined by the + * system property line.separator, and is not necessarily a single newline character + * ('\n'). + * + * @throws IOException If an error occurred while writing + */ + public abstract void println() throws IOException; + + /** + * Print a boolean value and then terminate the line. This method behaves as though it invokes + * {@link #print(boolean)} and then {@link #println()}. + * + * @param x the boolean to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(boolean x) throws IOException; + + /** + * Print a character and then terminate the line. This method behaves as though it invokes + * {@link #print(char)} and then {@link + * #println()} . + * + * @param x the char to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(char x) throws IOException; + + /** + * Print an integer and then terminate the line. This method behaves as though it invokes + * {@link #print(int)} and then {@link + * #println()} . + * + * @param x the int to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(int x) throws IOException; + + /** + * Print a long integer and then terminate the line. This method behaves as though it invokes + * {@link #print(long)} and then {@link #println()}. + * + * @param x the long to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(long x) throws IOException; + + /** + * Print a floating-point number and then terminate the line. This method behaves as though it invokes + * {@link #print(float)} and then {@link #println()}. + * + * @param x the float to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(float x) throws IOException; + + /** + * Print a double-precision floating-point number and then terminate the line. This method behaves as though it + * invokes {@link + * #print(double)} and then {@link #println()}. + * + * @param x the double to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(double x) throws IOException; + + /** + * Print an array of characters and then terminate the line. This method behaves as though it invokes + * print(char[]) and then println(). + * + * @param x the char[] to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(char x[]) throws IOException; + + /** + * Print a String and then terminate the line. This method behaves as though it invokes + * {@link #print(String)} and then {@link #println()}. + * + * @param x the String to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(String x) throws IOException; + + /** + * Print an Object and then terminate the line. This method behaves as though it invokes + * {@link #print(Object)} and then {@link #println()}. + * + * @param x the Object to write + * + * @throws IOException If an error occurred while writing + */ + public abstract void println(Object x) throws IOException; + + /** + * Clear the contents of the buffer. If the buffer has been already been flushed then the clear operation shall + * throw an IOException to signal the fact that some data has already been irrevocably written to the client + * response stream. + * + * @throws IOException If an I/O error occurs + */ + public abstract void clear() throws IOException; + + /** + * Clears the current contents of the buffer. Unlike clear(), this method will not throw an IOException if the + * buffer has already been flushed. It merely clears the current content of the buffer and returns. + * + * @throws IOException If an I/O error occurs + */ + public abstract void clearBuffer() throws IOException; + + /** + * Flush the stream. If the stream has saved any characters from the various write() methods in a buffer, write them + * immediately to their intended destination. Then, if that destination is another character or byte stream, flush + * it. Thus one flush() invocation will flush all the buffers in a chain of Writers and OutputStreams. + *

+ * The method may be invoked indirectly if the buffer size is exceeded. + *

+ * Once a stream has been closed, further write() or flush() invocations will cause an IOException to be thrown. + * + * @exception IOException If an I/O error occurs + */ + @Override + public abstract void flush() throws IOException; + + /** + * Close the stream, flushing it first. + *

+ * This method needs not be invoked explicitly for the initial JspWriter as the code generated by the JSP container + * will automatically include a call to close(). + *

+ * Closing a previously-closed stream, unlike flush(), has no effect. + * + * @exception IOException If an I/O error occurs + */ + @Override + public abstract void close() throws IOException; + + /** + * This method returns the size of the buffer used by the JspWriter. + * + * @return the size of the buffer in bytes, or 0 is unbuffered. + */ + public int getBufferSize() { + return bufferSize; + } + + /** + * This method returns the number of unused bytes in the buffer. + * + * @return the number of bytes unused in the buffer + */ + public abstract int getRemaining(); + + /** + * This method indicates whether the JspWriter is autoFlushing. + * + * @return if this JspWriter is auto flushing or throwing IOExceptions on buffer overflow conditions + */ + public boolean isAutoFlush() { + return autoFlush; + } + + /* + * fields + */ + + /** + * The size of the buffer used by the JspWriter. + */ + protected int bufferSize; + + /** + * Whether the JspWriter is autoflushing. + */ + protected boolean autoFlush; +} diff --git a/java/jakarta/servlet/jsp/LocalStrings.properties b/java/jakarta/servlet/jsp/LocalStrings.properties new file mode 100644 index 0000000..081f2fa --- /dev/null +++ b/java/jakarta/servlet/jsp/LocalStrings.properties @@ -0,0 +1,16 @@ +# 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. + +el.unknown.identifier=Unknown identifier diff --git a/java/jakarta/servlet/jsp/LocalStrings_de.properties b/java/jakarta/servlet/jsp/LocalStrings_de.properties new file mode 100644 index 0000000..6c9be8a --- /dev/null +++ b/java/jakarta/servlet/jsp/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +el.unknown.identifier=Unbekannter Identifier diff --git a/java/jakarta/servlet/jsp/LocalStrings_es.properties b/java/jakarta/servlet/jsp/LocalStrings_es.properties new file mode 100644 index 0000000..4cf7d5b --- /dev/null +++ b/java/jakarta/servlet/jsp/LocalStrings_es.properties @@ -0,0 +1,16 @@ +# 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. + +el.unknown.identifier=Identificador desconocido diff --git a/java/jakarta/servlet/jsp/LocalStrings_fr.properties b/java/jakarta/servlet/jsp/LocalStrings_fr.properties new file mode 100644 index 0000000..c8ea47d --- /dev/null +++ b/java/jakarta/servlet/jsp/LocalStrings_fr.properties @@ -0,0 +1,16 @@ +# 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. + +el.unknown.identifier=Identifiant inconnu diff --git a/java/jakarta/servlet/jsp/LocalStrings_ja.properties b/java/jakarta/servlet/jsp/LocalStrings_ja.properties new file mode 100644 index 0000000..a39b60b --- /dev/null +++ b/java/jakarta/servlet/jsp/LocalStrings_ja.properties @@ -0,0 +1,16 @@ +# 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. + +el.unknown.identifier=ä¸æ˜Žãªè­˜åˆ¥å­ diff --git a/java/jakarta/servlet/jsp/LocalStrings_ko.properties b/java/jakarta/servlet/jsp/LocalStrings_ko.properties new file mode 100644 index 0000000..1361d37 --- /dev/null +++ b/java/jakarta/servlet/jsp/LocalStrings_ko.properties @@ -0,0 +1,16 @@ +# 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. + +el.unknown.identifier=ì•Œ 수 없는 ì‹ë³„ìž diff --git a/java/jakarta/servlet/jsp/LocalStrings_zh_CN.properties b/java/jakarta/servlet/jsp/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..d6e6266 --- /dev/null +++ b/java/jakarta/servlet/jsp/LocalStrings_zh_CN.properties @@ -0,0 +1,16 @@ +# 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. + +el.unknown.identifier=未定义的标识符 diff --git a/java/jakarta/servlet/jsp/PageContext.java b/java/jakarta/servlet/jsp/PageContext.java new file mode 100644 index 0000000..dacf8e1 --- /dev/null +++ b/java/jakarta/servlet/jsp/PageContext.java @@ -0,0 +1,445 @@ +/* + * 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 jakarta.servlet.jsp; + +import java.io.IOException; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.jsp.tagext.BodyContent; + +/** + *

+ * PageContext extends JspContext to provide useful context information for when JSP technology is used in a Servlet + * environment. + *

+ * A PageContext instance provides access to all the namespaces associated with a JSP page, provides access to several + * page attributes, as well as a layer above the implementation details. Implicit objects are added to the pageContext + * automatically. + *

+ * The PageContext class is an abstract class, designed to be extended to provide implementation + * dependent implementations thereof, by conformant JSP engine runtime environments. A PageContext instance is obtained + * by a JSP implementation class by calling the JspFactory.getPageContext() method, and is released by calling + * JspFactory.releasePageContext(). + *

+ * An example of how PageContext, JspFactory, and other classes can be used within a JSP Page Implementation object is + * given elsewhere. + *

+ * The PageContext provides a number of facilities to the page/component author and page implementor, including: + *

    + *
  • a single API to manage the various scoped namespaces + *
  • a number of convenience API's to access various public objects + *
  • a mechanism to obtain the JspWriter for output + *
  • a mechanism to manage session usage by the page + *
  • a mechanism to expose page directive attributes to the scripting environment + *
  • mechanisms to forward or include the current request to other active components in the application + *
  • a mechanism to handle errorpage exception processing + *
+ *

+ * Methods Intended for Container Generated Code + *

+ * Some methods are intended to be used by the code generated by the container, not by code written by JSP page authors, + * or JSP tag library authors. + *

+ * The methods supporting lifecycle are initialize() and release() + *

+ * The following methods enable the management of nested JspWriter streams to implement Tag Extensions: + * pushBody() + *

+ * Methods Intended for JSP authors + *

+ * The following methods provide convenient access to implicit objects: getException(), + * getPage() getRequest(), getResponse(), getSession(), + * getServletConfig() and getServletContext(). + *

+ * The following methods provide support for forwarding, inclusion and error handling: forward(), + * include(), and handlePageException(). + */ +public abstract class PageContext extends JspContext { + + /** + * Sole constructor. (For invocation by subclass constructors, typically implicit.) + */ + public PageContext() { + // NOOP by default + } + + /** + * Page scope: (this is the default) the named reference remains available in this PageContext until the return from + * the current Servlet.service() invocation. + */ + + public static final int PAGE_SCOPE = 1; + + /** + * Request scope: the named reference remains available from the ServletRequest associated with the Servlet until + * the current request is completed. + */ + + public static final int REQUEST_SCOPE = 2; + + /** + * Session scope (only valid if this page participates in a session): the named reference remains available from the + * HttpSession (if any) associated with the Servlet until the HttpSession is invalidated. + */ + + public static final int SESSION_SCOPE = 3; + + /** + * Application scope: named reference remains available in the ServletContext until it is reclaimed. + */ + + public static final int APPLICATION_SCOPE = 4; + + /** + * Name used to store the Servlet in this PageContext's nametables. + */ + + public static final String PAGE = "jakarta.servlet.jsp.jspPage"; + + /** + * Name used to store this PageContext in it's own name table. + */ + + public static final String PAGECONTEXT = "jakarta.servlet.jsp.jspPageContext"; + + /** + * Name used to store ServletRequest in PageContext name table. + */ + + public static final String REQUEST = "jakarta.servlet.jsp.jspRequest"; + + /** + * Name used to store ServletResponse in PageContext name table. + */ + + public static final String RESPONSE = "jakarta.servlet.jsp.jspResponse"; + + /** + * Name used to store ServletConfig in PageContext name table. + */ + + public static final String CONFIG = "jakarta.servlet.jsp.jspConfig"; + + /** + * Name used to store HttpSession in PageContext name table. + */ + + public static final String SESSION = "jakarta.servlet.jsp.jspSession"; + /** + * Name used to store current JspWriter in PageContext name table. + */ + + public static final String OUT = "jakarta.servlet.jsp.jspOut"; + + /** + * Name used to store ServletContext in PageContext name table. + */ + + public static final String APPLICATION = "jakarta.servlet.jsp.jspApplication"; + + /** + * Name used to store uncaught exception in ServletRequest attribute list and PageContext name table. + */ + + public static final String EXCEPTION = "jakarta.servlet.jsp.jspException"; + + /** + *

+ * The initialize method is called to initialize an uninitialized PageContext so that it may be used by a JSP + * Implementation class to service an incoming request and response within it's _jspService() method. + *

+ * This method is typically called from JspFactory.getPageContext() in order to initialize state. + *

+ * This method is required to create an initial JspWriter, and associate the "out" name in page scope with this + * newly created object. + *

+ * This method should not be used by page or tag library authors. + * + * @param servlet The Servlet that is associated with this PageContext + * @param request The currently pending request for this Servlet + * @param response The currently pending response for this Servlet + * @param errorPageURL The value of the errorpage attribute from the page directive or null + * @param needsSession The value of the session attribute from the page directive + * @param bufferSize The value of the buffer attribute from the page directive + * @param autoFlush The value of the autoflush attribute from the page directive + * + * @throws IOException during creation of JspWriter + * @throws IllegalStateException if out not correctly initialized + * @throws IllegalArgumentException If one of the given parameters is invalid + */ + + public abstract void initialize(Servlet servlet, ServletRequest request, ServletResponse response, + String errorPageURL, boolean needsSession, int bufferSize, boolean autoFlush) + throws IOException, IllegalStateException, IllegalArgumentException; + + /** + *

+ * This method shall "reset" the internal state of a PageContext, releasing all internal references, and preparing + * the PageContext for potential reuse by a later invocation of initialize(). This method is typically called from + * JspFactory.releasePageContext(). + *

+ * Subclasses shall envelope this method. + *

+ * This method should not be used by page or tag library authors. + */ + + public abstract void release(); + + /** + * The current value of the session object (an HttpSession). + * + * @return the HttpSession for this PageContext or null + */ + + public abstract HttpSession getSession(); + + /** + * The current value of the page object (In a Servlet environment, this is an instance of jakarta.servlet.Servlet). + * + * @return the Page implementation class instance associated with this PageContext + */ + + public abstract Object getPage(); + + + /** + * The current value of the request object (a ServletRequest). + * + * @return The ServletRequest for this PageContext + */ + + public abstract ServletRequest getRequest(); + + /** + * The current value of the response object (a ServletResponse). + * + * @return the ServletResponse for this PageContext + */ + + public abstract ServletResponse getResponse(); + + /** + * The current value of the exception object (an Exception). + * + * @return any exception passed to this as an errorpage + */ + + public abstract Exception getException(); + + /** + * The ServletConfig instance. + * + * @return the ServletConfig for this PageContext + */ + + public abstract ServletConfig getServletConfig(); + + /** + * The ServletContext instance. + * + * @return the ServletContext for this PageContext + */ + + public abstract ServletContext getServletContext(); + + /** + *

+ * This method is used to re-direct, or "forward" the current ServletRequest and ServletResponse to another active + * component in the application. + *

+ *

+ * If the relativeUrlPath begins with a "/" then the URL specified is calculated relative to the DOCROOT of + * the ServletContext for this JSP. If the path does not begin with a "/" then the URL specified is + * calculated relative to the URL of the request that was mapped to the calling JSP. + *

+ *

+ * It is only valid to call this method from a Thread executing within a + * _jspService(...) method of a JSP. + *

+ *

+ * Once this method has been called successfully, it is illegal for the calling Thread to attempt to + * modify the + * ServletResponse object. Any such attempt to do so, shall result in undefined behavior. Typically, callers + * immediately return from _jspService(...) after calling this method. + *

+ * + * @param relativeUrlPath specifies the relative URL path to the target resource as described above + * + * @throws IllegalStateException if ServletResponse is not in a state where a forward can be + * performed + * @throws ServletException if the page that was forwarded to throws a ServletException + * @throws IOException if an I/O error occurred while forwarding + */ + + public abstract void forward(String relativeUrlPath) throws ServletException, IOException; + + /** + *

+ * Causes the resource specified to be processed as part of the current ServletRequest and ServletResponse being + * processed by the calling Thread. The output of the target resources processing of the request is written directly + * to the ServletResponse output stream. + *

+ *

+ * The current JspWriter "out" for this JSP is flushed as a side-effect of this call, prior to processing the + * include. + *

+ *

+ * If the relativeUrlPath begins with a "/" then the URL specified is calculated relative to the DOCROOT of + * the ServletContext for this JSP. If the path does not begin with a "/" then the URL specified is + * calculated relative to the URL of the request that was mapped to the calling JSP. + *

+ *

+ * It is only valid to call this method from a Thread executing within a + * _jspService(...) method of a JSP. + *

+ * + * @param relativeUrlPath specifies the relative URL path to the target resource to be included + * + * @throws ServletException if the page that was forwarded to throws a ServletException + * @throws IOException if an I/O error occurred while forwarding + */ + public abstract void include(String relativeUrlPath) throws ServletException, IOException; + + /** + *

+ * Causes the resource specified to be processed as part of the current ServletRequest and ServletResponse being + * processed by the calling Thread. The output of the target resources processing of the request is written directly + * to the current JspWriter returned by a call to getOut(). + *

+ *

+ * If flush is true, The current JspWriter "out" for this JSP is flushed as a side-effect of this call, prior to + * processing the include. Otherwise, the JspWriter "out" is not flushed. + *

+ *

+ * If the relativeUrlPath begins with a "/" then the URL specified is calculated relative to the DOCROOT of + * the ServletContext for this JSP. If the path does not begin with a "/" then the URL specified is + * calculated relative to the URL of the request that was mapped to the calling JSP. + *

+ *

+ * It is only valid to call this method from a Thread executing within a + * _jspService(...) method of a JSP. + *

+ * + * @param relativeUrlPath specifies the relative URL path to the target resource to be included + * @param flush True if the JspWriter is to be flushed before the include, or false if not. + * + * @throws ServletException if the page that was forwarded to throws a ServletException + * @throws IOException if an I/O error occurred while forwarding + * + * @since JSP 2.0 + */ + public abstract void include(String relativeUrlPath, boolean flush) throws ServletException, IOException; + + /** + *

+ * This method is intended to process an unhandled 'page' level exception by forwarding the exception to the + * specified error page for this JSP. If forwarding is not possible (for example because the response has already + * been committed), an implementation dependent mechanism should be used to invoke the error page (e.g. "including" + * the error page instead). + *

+ * If no error page is defined in the page, the exception should be rethrown so that the standard servlet error + * handling takes over. + *

+ * A JSP implementation class shall typically clean up any local state prior to invoking this and will return + * immediately thereafter. It is illegal to generate any output to the client, or to modify any ServletResponse + * state after invoking this call. + *

+ * This method is kept for backwards compatibility reasons. Newly generated code should use + * PageContext.handlePageException(Throwable). + * + * @param e the exception to be handled + * + * @throws ServletException if an error occurs while invoking the error page + * @throws IOException if an I/O error occurred while invoking the error page + * @throws NullPointerException if the exception is null + * + * @see #handlePageException(Throwable) + */ + + public abstract void handlePageException(Exception e) throws ServletException, IOException; + + /** + *

+ * This method is intended to process an unhandled 'page' level exception by forwarding the exception to the + * specified error page for this JSP. If forwarding is not possible (for example because the response has already + * been committed), an implementation dependent mechanism should be used to invoke the error page (e.g. "including" + * the error page instead). + *

+ * If no error page is defined in the page, the exception should be rethrown so that the standard servlet error + * handling takes over. + *

+ * This method is intended to process an unhandled "page" level exception by redirecting the exception to either the + * specified error page for this JSP, or if none was specified, to perform some implementation dependent action. + *

+ * A JSP implementation class shall typically clean up any local state prior to invoking this and will return + * immediately thereafter. It is illegal to generate any output to the client, or to modify any ServletResponse + * state after invoking this call. + * + * @param t the throwable to be handled + * + * @throws ServletException if an error occurs while invoking the error page + * @throws IOException if an I/O error occurred while invoking the error page + * @throws NullPointerException if the exception is null + * + * @see #handlePageException(Exception) + */ + + public abstract void handlePageException(Throwable t) throws ServletException, IOException; + + /** + * Return a new BodyContent object, save the current "out" JspWriter, and update the value of the "out" attribute in + * the page scope attribute namespace of the PageContext. + * + * @return the new BodyContent + */ + + public BodyContent pushBody() { + return null; // XXX to implement + } + + + /** + * Provides convenient access to error information. + * + * @return an ErrorData instance containing information about the error, as obtained from the request attributes, as + * per the Servlet specification. If this is not an error page (that is, if the isErrorPage attribute of + * the page directive is not set to "true"), the information is meaningless. + * + * @since JSP 2.0 + */ + public ErrorData getErrorData() { + int status = 0; + + Integer status_code = (Integer) getRequest().getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + // Avoid NPE if attribute is not set + if (status_code != null) { + status = status_code.intValue(); + } + + return new ErrorData((Throwable) getRequest().getAttribute(RequestDispatcher.ERROR_EXCEPTION), status, + (String) getRequest().getAttribute(RequestDispatcher.ERROR_REQUEST_URI), + (String) getRequest().getAttribute(RequestDispatcher.ERROR_SERVLET_NAME)); + } + +} diff --git a/java/jakarta/servlet/jsp/SkipPageException.java b/java/jakarta/servlet/jsp/SkipPageException.java new file mode 100644 index 0000000..f9926c3 --- /dev/null +++ b/java/jakarta/servlet/jsp/SkipPageException.java @@ -0,0 +1,71 @@ +/* + * 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 jakarta.servlet.jsp; + +/** + * Exception to indicate the calling page must cease evaluation. Thrown by a simple tag handler to indicate that the + * remainder of the page must not be evaluated. The result is propagated back to the page in the case where one tag + * invokes another (as can be the case with tag files). The effect is similar to that of a Classic Tag Handler returning + * Tag.SKIP_PAGE from doEndTag(). Jsp Fragments may also throw this exception. This exception should not be thrown + * manually in a JSP page or tag file - the behavior is undefined. The exception is intended to be thrown inside + * SimpleTag handlers and in JSP fragments. + * + * @see jakarta.servlet.jsp.tagext.SimpleTag#doTag + * @see jakarta.servlet.jsp.tagext.JspFragment#invoke + * @see jakarta.servlet.jsp.tagext.Tag#doEndTag + * + * @since JSP 2.0 + */ +public class SkipPageException extends JspException { + + private static final long serialVersionUID = 1L; + + /** + * Creates a SkipPageException with no message. + */ + public SkipPageException() { + super(); + } + + /** + * Creates a SkipPageException with the provided message. + * + * @param message the detail message + */ + public SkipPageException(String message) { + super(message); + } + + /** + * Creates a SkipPageException with the provided message and root cause. + * + * @param message the detail message + * @param rootCause the originating cause of this exception + */ + public SkipPageException(String message, Throwable rootCause) { + super(message, rootCause); + } + + /** + * Creates a SkipPageException with the provided root cause. + * + * @param rootCause the originating cause of this exception + */ + public SkipPageException(Throwable rootCause) { + super(rootCause); + } +} diff --git a/java/jakarta/servlet/jsp/el/ELException.java b/java/jakarta/servlet/jsp/el/ELException.java new file mode 100644 index 0000000..acff138 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/ELException.java @@ -0,0 +1,76 @@ +/* + * 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 jakarta.servlet.jsp.el; + +/** + * Represents any of the exception conditions that arise during the operation evaluation of the evaluator. + * + * @since JSP 2.0 + * + * @deprecated As of JSP 2.1, replaced by jakarta.el.ELException + */ +@Deprecated +public class ELException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Creates an ELException with no detail message. + **/ + public ELException() { + super(); + } + + /** + * Creates an ELException with the provided detail message. + * + * @param pMessage the detail message + **/ + public ELException(String pMessage) { + super(pMessage); + } + + /** + * Creates an ELException with the given root cause. + * + * @param pRootCause the originating cause of this exception + **/ + public ELException(Throwable pRootCause) { + super(pRootCause); + } + + // ------------------------------------- + /** + * Creates an ELException with the given detail message and root cause. + * + * @param pMessage the detail message + * @param pRootCause the originating cause of this exception + **/ + public ELException(String pMessage, Throwable pRootCause) { + super(pMessage, pRootCause); + } + + // ------------------------------------- + /** + * Returns the root cause. + * + * @return the root cause of this exception + */ + public Throwable getRootCause() { + return getCause(); + } +} diff --git a/java/jakarta/servlet/jsp/el/ELParseException.java b/java/jakarta/servlet/jsp/el/ELParseException.java new file mode 100644 index 0000000..ff75753 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/ELParseException.java @@ -0,0 +1,49 @@ +/* + * 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 jakarta.servlet.jsp.el; + + +/** + * Represents a parsing error encountered while parsing an EL expression. + * + * @since JSP 2.0 + * + * @deprecated As of JSP 2.1, replaced by jakarta.el.ELException + */ +@Deprecated +public class ELParseException extends ELException { + + private static final long serialVersionUID = 1L; + + + /** + * Creates an ELParseException with no detail message. + */ + public ELParseException() { + super(); + } + + + /** + * Creates an ELParseException with the provided detail message. + * + * @param pMessage the detail message + */ + public ELParseException(String pMessage) { + super(pMessage); + } +} diff --git a/java/jakarta/servlet/jsp/el/Expression.java b/java/jakarta/servlet/jsp/el/Expression.java new file mode 100644 index 0000000..7ecee68 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/Expression.java @@ -0,0 +1,53 @@ +/* + * 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 jakarta.servlet.jsp.el; + + +/** + *

+ * The abstract class for a prepared expression. + *

+ *

+ * An instance of an Expression can be obtained via from an ExpressionEvaluator instance. + *

+ *

+ * An Expression may or not have done a syntactic parse of the expression. A client invoking the evaluate() method + * should be ready for the case where ELParseException exceptions are raised. + *

+ * + * @since JSP 2.0 + * + * @deprecated As of JSP 2.1, replaced by jakarta.el.ValueExpression + */ +@Deprecated +public abstract class Expression { + + /** + * Evaluates an expression that was previously prepared. In some implementations preparing an expression involves + * full syntactic validation, but others may not do so. Evaluating the expression may raise an ELParseException as + * well as other ELExceptions due to run-time evaluation. + * + * @param vResolver A VariableResolver instance that can be used at runtime to resolve the name of implicit objects + * into Objects. + * + * @return The result of the expression evaluation. + * + * @exception ELException Thrown if the expression evaluation failed. + */ + public abstract Object evaluate(VariableResolver vResolver) throws ELException; +} + diff --git a/java/jakarta/servlet/jsp/el/ExpressionEvaluator.java b/java/jakarta/servlet/jsp/el/ExpressionEvaluator.java new file mode 100644 index 0000000..7f0ec63 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/ExpressionEvaluator.java @@ -0,0 +1,94 @@ +/* + * 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 jakarta.servlet.jsp.el; + +/** + *

+ * The abstract base class for an expression-language evaluator. Classes that implement an expression language expose + * their functionality via this abstract class. + *

+ *

+ * An instance of the ExpressionEvaluator can be obtained via the JspContext / PageContext + *

+ *

+ * The parseExpression() and evaluate() methods must be thread-safe. That is, multiple threads may call these methods on + * the same ExpressionEvaluator object simultaneously. Implementations should synchronize access if they depend on + * transient state. Implementations should not, however, assume that only one object of each ExpressionEvaluator type + * will be instantiated; global caching should therefore be static. + *

+ *

+ * Only a single EL expression, starting with '${' and ending with '}', can be parsed or evaluated at a time. EL + * expressions cannot be mixed with static text. For example, attempting to parse or evaluate " + * abc${1+1}def${1+1}ghi" or even "${1+1}${1+1}" will cause an ELException to be + * thrown. + *

+ *

+ * The following are examples of syntactically legal EL expressions: + *

+ *
    + *
  • ${person.lastName}
  • + *
  • ${8 * 8}
  • + *
  • ${my:reverse('hello')}
  • + *
+ * + * @since JSP 2.0 + * + * @deprecated As of JSP 2.1, replaced by jakarta.el.ExpressionFactory + */ +@Deprecated +public abstract class ExpressionEvaluator { + + /** + * Prepare an expression for later evaluation. This method should perform syntactic validation of the expression; if + * in doing so it detects errors, it should raise an ELParseException. + * + * @param expression The expression to be evaluated. + * @param expectedType The expected type of the result of the evaluation + * @param fMapper A FunctionMapper to resolve functions found in the expression. It can be null, in which case + * no functions are supported for this invocation. The ExpressionEvaluator must not hold on + * to the FunctionMapper reference after returning from parseExpression(). The + * Expression object returned must invoke the same functions regardless of + * whether the mappings in the provided FunctionMapper instance change between + * calling ExpressionEvaluator.parseExpression() and + * Expression.evaluate(). + * + * @return The Expression object encapsulating the arguments. + * + * @exception ELException Thrown if parsing errors were found. + */ + public abstract Expression parseExpression(String expression, Class expectedType, FunctionMapper fMapper) + throws ELException; + + /** + * Evaluates an expression. This method may perform some syntactic validation and, if so, it should raise an + * ELParseException error if it encounters syntactic errors. EL evaluation errors should cause an ELException to be + * raised. + * + * @param expression The expression to be evaluated. + * @param expectedType The expected type of the result of the evaluation + * @param vResolver A VariableResolver instance that can be used at runtime to resolve the name of implicit + * objects into Objects. + * @param fMapper A FunctionMapper to resolve functions found in the expression. It can be null, in which case + * no functions are supported for this invocation. + * + * @return The result of the expression evaluation. + * + * @exception ELException Thrown if the expression evaluation failed. + */ + public abstract Object evaluate(String expression, Class expectedType, VariableResolver vResolver, + FunctionMapper fMapper) throws ELException; +} diff --git a/java/jakarta/servlet/jsp/el/FunctionMapper.java b/java/jakarta/servlet/jsp/el/FunctionMapper.java new file mode 100644 index 0000000..2cc66cb --- /dev/null +++ b/java/jakarta/servlet/jsp/el/FunctionMapper.java @@ -0,0 +1,43 @@ +/* + * 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 jakarta.servlet.jsp.el; + +/** + *

+ * The interface to a map between EL function names and methods. + *

+ *

+ * Classes implementing this interface may, for instance, consult tag library information to resolve the map. + *

+ * + * @since JSP 2.0 + * + * @deprecated As of JSP 2.1, replaced by jakarta.el.FunctionMapper + */ +@Deprecated +public interface FunctionMapper { + /** + * Resolves the specified local name and prefix into a Java.lang.Method. Returns null if the prefix and local name + * are not found. + * + * @param prefix the prefix of the function, or "" if no prefix. + * @param localName the short name of the function + * + * @return the result of the method mapping. Null means no entry found. + */ + java.lang.reflect.Method resolveFunction(String prefix, String localName); +} diff --git a/java/jakarta/servlet/jsp/el/ImplicitObjectELResolver.java b/java/jakarta/servlet/jsp/el/ImplicitObjectELResolver.java new file mode 100644 index 0000000..07cdec4 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/ImplicitObjectELResolver.java @@ -0,0 +1,639 @@ +/* + * 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 jakarta.servlet.jsp.el; + +import java.beans.FeatureDescriptor; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.el.PropertyNotWritableException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.jsp.JspContext; +import jakarta.servlet.jsp.PageContext; + +/** + * Provides resolution in EL for the implicit variables of a JSP page. + * + * @since JSP 2.1 + */ +public class ImplicitObjectELResolver extends ELResolver { + + /** + * Creates an instance of the implicit object resolver for EL. + */ + public ImplicitObjectELResolver() { + super(); + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base == null && property != null) { + Scope scope = Scope.lookupMap.get(property.toString()); + if (scope != null) { + return scope.getScopeValue(context, base, property); + } + } + return null; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base == null && property != null) { + if (Scope.lookupMap.containsKey(property.toString())) { + context.setPropertyResolved(base, property); + } + } + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + + if (base == null && property != null) { + if (Scope.lookupMap.containsKey(property.toString())) { + context.setPropertyResolved(base, property); + throw new PropertyNotWritableException(); + } + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base == null && property != null) { + if (Scope.lookupMap.containsKey(property.toString())) { + context.setPropertyResolved(base, property); + return true; + } + } + return false; + } + + @Deprecated(forRemoval = true, since = "JSP 3.1") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + List feats = new ArrayList<>(Scope.values().length); + FeatureDescriptor feat; + for (Scope scope : Scope.values()) { + feat = new FeatureDescriptor(); + feat.setDisplayName(scope.implicitName); + feat.setExpert(false); + feat.setHidden(false); + feat.setName(scope.implicitName); + feat.setPreferred(true); + feat.setValue(RESOLVABLE_AT_DESIGN_TIME, Boolean.TRUE); + feat.setValue(TYPE, String.class); + feats.add(feat); + } + return feats.iterator(); + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base == null) { + return String.class; + } + return null; + } + + + private static class ScopeManager { + private static final String MNGR_KEY = ScopeManager.class.getName(); + + private final PageContext page; + + private Map applicationScope; + + private Map cookie; + + private Map header; + + private Map headerValues; + + private Map initParam; + + private Map pageScope; + + private Map param; + + private Map paramValues; + + private Map requestScope; + + private Map sessionScope; + + ScopeManager(PageContext page) { + this.page = page; + } + + public static ScopeManager get(PageContext page) { + ScopeManager mngr = (ScopeManager) page.getAttribute(MNGR_KEY); + if (mngr == null) { + mngr = new ScopeManager(page); + page.setAttribute(MNGR_KEY, mngr); + } + return mngr; + } + + public Map getApplicationScope() { + if (this.applicationScope == null) { + this.applicationScope = new ScopeMap<>() { + @Override + protected void setAttribute(String name, Object value) { + page.getServletContext().setAttribute(name, value); + } + + @Override + protected void removeAttribute(String name) { + page.getServletContext().removeAttribute(name); + } + + @Override + protected Enumeration getAttributeNames() { + return page.getServletContext().getAttributeNames(); + } + + @Override + protected Object getAttribute(String name) { + return page.getServletContext().getAttribute(name); + } + }; + } + return this.applicationScope; + } + + public Map getCookie() { + if (this.cookie == null) { + this.cookie = new ScopeMap<>() { + @Override + protected Enumeration getAttributeNames() { + Cookie[] cookies = ((HttpServletRequest) page.getRequest()).getCookies(); + if (cookies != null) { + List list = new ArrayList<>(cookies.length); + for (Cookie cookie : cookies) { + list.add(cookie.getName()); + } + return Collections.enumeration(list); + } + return null; + } + + @Override + protected Cookie getAttribute(String name) { + Cookie[] cookies = ((HttpServletRequest) page.getRequest()).getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (name.equals(cookie.getName())) { + return cookie; + } + } + } + return null; + } + + }; + } + return this.cookie; + } + + public Map getHeader() { + if (this.header == null) { + this.header = new ScopeMap<>() { + @Override + protected Enumeration getAttributeNames() { + return ((HttpServletRequest) page.getRequest()).getHeaderNames(); + } + + @Override + protected String getAttribute(String name) { + return ((HttpServletRequest) page.getRequest()).getHeader(name); + } + }; + } + return this.header; + } + + public Map getHeaderValues() { + if (this.headerValues == null) { + this.headerValues = new ScopeMap<>() { + @Override + protected Enumeration getAttributeNames() { + return ((HttpServletRequest) page.getRequest()).getHeaderNames(); + } + + @Override + protected String[] getAttribute(String name) { + Enumeration e = ((HttpServletRequest) page.getRequest()).getHeaders(name); + if (e != null) { + List list = new ArrayList<>(); + while (e.hasMoreElements()) { + list.add(e.nextElement()); + } + return list.toArray(new String[0]); + } + return null; + } + + }; + } + return this.headerValues; + } + + public Map getInitParam() { + if (this.initParam == null) { + this.initParam = new ScopeMap<>() { + @Override + protected Enumeration getAttributeNames() { + return page.getServletContext().getInitParameterNames(); + } + + @Override + protected String getAttribute(String name) { + return page.getServletContext().getInitParameter(name); + } + }; + } + return this.initParam; + } + + public PageContext getPageContext() { + return this.page; + } + + public Map getPageScope() { + if (this.pageScope == null) { + this.pageScope = new ScopeMap<>() { + @Override + protected void setAttribute(String name, Object value) { + page.setAttribute(name, value); + } + + @Override + protected void removeAttribute(String name) { + page.removeAttribute(name); + } + + @Override + protected Enumeration getAttributeNames() { + return page.getAttributeNamesInScope(PageContext.PAGE_SCOPE); + } + + @Override + protected Object getAttribute(String name) { + return page.getAttribute(name); + } + }; + } + return this.pageScope; + } + + public Map getParam() { + if (this.param == null) { + this.param = new ScopeMap<>() { + @Override + protected Enumeration getAttributeNames() { + return page.getRequest().getParameterNames(); + } + + @Override + protected String getAttribute(String name) { + return page.getRequest().getParameter(name); + } + }; + } + return this.param; + } + + public Map getParamValues() { + if (this.paramValues == null) { + this.paramValues = new ScopeMap<>() { + @Override + protected String[] getAttribute(String name) { + return page.getRequest().getParameterValues(name); + } + + @Override + protected Enumeration getAttributeNames() { + return page.getRequest().getParameterNames(); + } + }; + } + return this.paramValues; + } + + public Map getRequestScope() { + if (this.requestScope == null) { + this.requestScope = new ScopeMap<>() { + @Override + protected void setAttribute(String name, Object value) { + page.getRequest().setAttribute(name, value); + } + + @Override + protected void removeAttribute(String name) { + page.getRequest().removeAttribute(name); + } + + @Override + protected Enumeration getAttributeNames() { + return page.getRequest().getAttributeNames(); + } + + @Override + protected Object getAttribute(String name) { + return page.getRequest().getAttribute(name); + } + }; + } + return this.requestScope; + } + + public Map getSessionScope() { + if (this.sessionScope == null) { + this.sessionScope = new ScopeMap<>() { + @Override + protected void setAttribute(String name, Object value) { + ((HttpServletRequest) page.getRequest()).getSession().setAttribute(name, value); + } + + @Override + protected void removeAttribute(String name) { + HttpSession session = page.getSession(); + if (session != null) { + session.removeAttribute(name); + } + } + + @Override + protected Enumeration getAttributeNames() { + HttpSession session = page.getSession(); + if (session != null) { + return session.getAttributeNames(); + } + return null; + } + + @Override + protected Object getAttribute(String name) { + HttpSession session = page.getSession(); + if (session != null) { + return session.getAttribute(name); + } + return null; + } + }; + } + return this.sessionScope; + } + } + + + private abstract static class ScopeMap extends AbstractMap { + + protected abstract Enumeration getAttributeNames(); + + protected abstract V getAttribute(String name); + + @SuppressWarnings("unused") + protected void removeAttribute(String name) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unused") + protected void setAttribute(String name, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public final Set> entrySet() { + Enumeration e = getAttributeNames(); + Set> set = new HashSet<>(); + if (e != null) { + while (e.hasMoreElements()) { + set.add(new ScopeEntry(e.nextElement())); + } + } + return set; + } + + @Override + public final int size() { + int size = 0; + Enumeration e = getAttributeNames(); + if (e != null) { + while (e.hasMoreElements()) { + e.nextElement(); + size++; + } + } + return size; + } + + @Override + public final boolean containsKey(Object key) { + if (key == null) { + return false; + } + Enumeration e = getAttributeNames(); + if (e != null) { + while (e.hasMoreElements()) { + if (key.equals(e.nextElement())) { + return true; + } + } + } + return false; + } + + private class ScopeEntry implements Map.Entry { + + private final String key; + + ScopeEntry(String key) { + this.key = key; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public V getValue() { + return getAttribute(this.key); + } + + @Override + public V setValue(Object value) { + if (value == null) { + removeAttribute(this.key); + } else { + setAttribute(this.key, value); + } + return null; + } + + @Override + public boolean equals(Object obj) { + return obj != null && this.hashCode() == obj.hashCode(); + } + + @Override + public int hashCode() { + return this.key.hashCode(); + } + + } + + @Override + public final V get(Object key) { + if (key != null) { + return getAttribute((String) key); + } + return null; + } + + @Override + public final V put(String key, V value) { + Objects.requireNonNull(key); + if (value == null) { + this.removeAttribute(key); + } else { + this.setAttribute(key, value); + } + return null; + } + + @Override + public final V remove(Object key) { + Objects.requireNonNull(key); + this.removeAttribute((String) key); + return null; + } + } + + + private enum Scope { + APPLICATION_SCOPE("applicationScope") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getApplicationScope(); + } + }, + COOKIE("cookie") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getCookie(); + } + }, + HEADER("header") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getHeader(); + } + }, + HEADER_VALUES("headerValues") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getHeaderValues(); + } + }, + INIT_PARAM("initParam") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getInitParam(); + } + }, + PAGE_CONTEXT("pageContext") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getPageContext(); + } + }, + PAGE_SCOPE("pageScope") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getPageScope(); + } + }, + PARAM("param") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getParam(); + } + }, + PARAM_VALUES("paramValues") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getParamValues(); + } + }, + REQUEST_SCOPE("requestScope") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getRequestScope(); + } + }, + SESSION_SCOPE("sessionScope") { + @Override + Object getScopeValue(ELContext context, Object base, Object property) { + return getScopeManager(context, base, property).getSessionScope(); + } + }; + + private static final Map lookupMap = new HashMap<>(); + + static { + for (Scope scope : values()) { + lookupMap.put(scope.implicitName, scope); + } + } + + private static ScopeManager getScopeManager(ELContext context, Object base, Object property) { + PageContext page = (PageContext) context.getContext(JspContext.class); + context.setPropertyResolved(base, property); + return ScopeManager.get(page); + } + + private final String implicitName; + + Scope(String implicitName) { + this.implicitName = implicitName; + } + + abstract Object getScopeValue(ELContext context, Object base, Object property); + } +} diff --git a/java/jakarta/servlet/jsp/el/ImportELResolver.java b/java/jakarta/servlet/jsp/el/ImportELResolver.java new file mode 100644 index 0000000..4e58509 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/ImportELResolver.java @@ -0,0 +1,147 @@ +/* + * 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 jakarta.servlet.jsp.el; + +import java.beans.FeatureDescriptor; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; + +import jakarta.el.ELClass; +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.el.ImportHandler; + +/** + * Providers resolution of imports and static imports in the Jakarta Server Pages ELResolver chain. + * + * @since JSP 3.1 + */ +public class ImportELResolver extends ELResolver { + + // Indicates if a performance short-cut is available + private static final Class AST_IDENTIFIER_KEY; + + static { + Class key = null; + try { + key = Class.forName("org.apache.el.parser.AstIdentifier"); + } catch (Exception e) { + // Ignore: Expected if not running on Tomcat. Not a problem since + // this just allows a short-cut. + } + AST_IDENTIFIER_KEY = key; + } + + /** + * Default constructor. + */ + public ImportELResolver() { + super(); + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + Object result = null; + + if (base == null) { + if (property != null) { + boolean resolveClass = true; + // Performance short-cut available when running on Tomcat + if (AST_IDENTIFIER_KEY != null) { + // Tomcat will set this key to Boolean.TRUE if the + // identifier is a stand-alone identifier (i.e. + // identifier) rather than part of an AstValue (i.e. + // identifier.something). Imports do not need to be + // checked if this is a stand-alone identifier + Boolean value = (Boolean) context.getContext(AST_IDENTIFIER_KEY); + if (value != null && value.booleanValue()) { + resolveClass = false; + } + } + + ImportHandler importHandler = context.getImportHandler(); + if (importHandler != null) { + String key = property.toString(); + Class clazz = null; + if (resolveClass) { + clazz = importHandler.resolveClass(key); + if (clazz != null) { + result = new ELClass(clazz); + } + } + if (result == null) { + // This might be the name of an imported static field + clazz = importHandler.resolveStatic(key); + if (clazz != null) { + try { + result = clazz.getField(key).get(null); + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | + SecurityException e) { + // Most (all?) of these should have been + // prevented by the checks when the import + // was defined. + } + } + } + } + } + } + + if (result != null) { + context.setPropertyResolved(base, property); + } + + return result; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + // In normal usage, ScopedAttributeELResolver will have responded. + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + // In normal usage, ScopedAttributeELResolver will have responded. + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + // In normal usage, ScopedAttributeELResolver will have responded. + return false; + } + + @Deprecated(forRemoval = true, since = "JSP 3.1") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return Collections.emptyIterator(); + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + // In normal usage, ScopedAttributeELResolver will have responded. + return null; + } +} diff --git a/java/jakarta/servlet/jsp/el/NotFoundELResolver.java b/java/jakarta/servlet/jsp/el/NotFoundELResolver.java new file mode 100644 index 0000000..cc865e9 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/NotFoundELResolver.java @@ -0,0 +1,129 @@ +/* + * 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 jakarta.servlet.jsp.el; + +import java.beans.FeatureDescriptor; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; +import java.util.ResourceBundle; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.el.PropertyNotFoundException; + +/** + * The final resolver of the Jakarta Server Pages ELResolver chain. It always resolves the requested value, returning + * {@code null} when it does so. + * + * @since JSP 3.1 + */ +public class NotFoundELResolver extends ELResolver { + + private static final String LSTRING_FILE = "jakarta.servlet.jsp.LocalStrings"; + private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + + /** + * Default constructor. + */ + public NotFoundELResolver() { + super(); + } + + /** + * {@inheritDoc} + *

+ * Resolves the property and always returns {@code null} unless the provided context contains a Boolean object with + * value {@code Boolean.TRUE} as the value associated with the key + * {@code jakarta.servlet.jsp.el.NotFoundELResolver.class} in which case an exception is thrown. This is to support + * implementation of the {@code errorOnELNotFound} page/tag directive. + * + * @return Always {@code null} + * + * @throws PropertyNotFoundException if the provided context contains a Boolean object with value + * {@code Boolean.TRUE} as the value associated with the key + * {@code jakarta.servlet.jsp.el.NotFoundELResolver.class} + */ + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + Object obj = context.getContext(this.getClass()); + if (obj instanceof Boolean && ((Boolean) obj).booleanValue()) { + throw new PropertyNotFoundException( + lStrings.getString("el.unknown.identifier") + " [" + property.toString() + "]"); + } + + context.setPropertyResolved(base, property); + return null; + } + + /** + * {@inheritDoc} + *

+ * In normal usage, {@link ScopedAttributeELResolver} will have responded. + * + * @return Always {@code null} + */ + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + return null; + } + + /** + * {@inheritDoc} + *

+ * No-op. In normal usage, {@link ScopedAttributeELResolver} will have responded. + */ + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + } + + + /** + * {@inheritDoc} + *

+ * In normal usage, {@link ScopedAttributeELResolver} will have responded. + * + * @return Always {@code false} + */ + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + return false; + } + + @Deprecated(forRemoval = true, since = "JSP 3.1") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return Collections.emptyIterator(); + } + + /** + * {@inheritDoc} + *

+ * In normal usage, {@link ScopedAttributeELResolver} will have responded. + * + * @return Always {@code null} + */ + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return null; + } +} diff --git a/java/jakarta/servlet/jsp/el/ScopedAttributeELResolver.java b/java/jakarta/servlet/jsp/el/ScopedAttributeELResolver.java new file mode 100644 index 0000000..5044c04 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/ScopedAttributeELResolver.java @@ -0,0 +1,193 @@ +/* + * 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 jakarta.servlet.jsp.el; + +import java.beans.FeatureDescriptor; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.servlet.jsp.JspContext; +import jakarta.servlet.jsp.PageContext; + +/** + * An ELResolver for working with JSP scoped attributes which may have page, request, session or application scope. + * + * @since JSP 2.1 + */ +public class ScopedAttributeELResolver extends ELResolver { + + /** + * Default constructor. + */ + public ScopedAttributeELResolver() { + super(); + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + Object result = null; + + if (base == null) { + if (property != null) { + String key = property.toString(); + PageContext page = (PageContext) context.getContext(JspContext.class); + result = page.findAttribute(key); + + if (result != null) { + context.setPropertyResolved(base, property); + } + } + } + + return result; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base == null) { + context.setPropertyResolved(base, property); + return Object.class; + } + + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + Objects.requireNonNull(context); + + if (base == null) { + context.setPropertyResolved(base, property); + if (property != null) { + String key = property.toString(); + PageContext page = (PageContext) context.getContext(JspContext.class); + int scope = page.getAttributesScope(key); + if (scope != 0) { + page.setAttribute(key, value, scope); + } else { + page.setAttribute(key, value); + } + } + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base == null) { + context.setPropertyResolved(base, property); + } + + return false; + } + + @Deprecated(forRemoval = true, since = "JSP 3.1") + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + + PageContext ctxt = (PageContext) context.getContext(JspContext.class); + List list = new ArrayList<>(); + Enumeration e; + Object value; + String name; + + e = ctxt.getAttributeNamesInScope(PageContext.PAGE_SCOPE); + while (e.hasMoreElements()) { + name = e.nextElement(); + value = ctxt.getAttribute(name, PageContext.PAGE_SCOPE); + FeatureDescriptor descriptor = new FeatureDescriptor(); + descriptor.setName(name); + descriptor.setDisplayName(name); + descriptor.setExpert(false); + descriptor.setHidden(false); + descriptor.setPreferred(true); + descriptor.setShortDescription("page scoped attribute"); + descriptor.setValue("type", value.getClass()); + descriptor.setValue("resolvableAtDesignTime", Boolean.FALSE); + list.add(descriptor); + } + + e = ctxt.getAttributeNamesInScope(PageContext.REQUEST_SCOPE); + while (e.hasMoreElements()) { + name = e.nextElement(); + value = ctxt.getAttribute(name, PageContext.REQUEST_SCOPE); + FeatureDescriptor descriptor = new FeatureDescriptor(); + descriptor.setName(name); + descriptor.setDisplayName(name); + descriptor.setExpert(false); + descriptor.setHidden(false); + descriptor.setPreferred(true); + descriptor.setShortDescription("request scope attribute"); + descriptor.setValue("type", value.getClass()); + descriptor.setValue("resolvableAtDesignTime", Boolean.FALSE); + list.add(descriptor); + } + + if (ctxt.getSession() != null) { + e = ctxt.getAttributeNamesInScope(PageContext.SESSION_SCOPE); + while (e.hasMoreElements()) { + name = e.nextElement(); + value = ctxt.getAttribute(name, PageContext.SESSION_SCOPE); + FeatureDescriptor descriptor = new FeatureDescriptor(); + descriptor.setName(name); + descriptor.setDisplayName(name); + descriptor.setExpert(false); + descriptor.setHidden(false); + descriptor.setPreferred(true); + descriptor.setShortDescription("session scoped attribute"); + descriptor.setValue("type", value.getClass()); + descriptor.setValue("resolvableAtDesignTime", Boolean.FALSE); + list.add(descriptor); + } + } + + e = ctxt.getAttributeNamesInScope(PageContext.APPLICATION_SCOPE); + while (e.hasMoreElements()) { + name = e.nextElement(); + value = ctxt.getAttribute(name, PageContext.APPLICATION_SCOPE); + FeatureDescriptor descriptor = new FeatureDescriptor(); + descriptor.setName(name); + descriptor.setDisplayName(name); + descriptor.setExpert(false); + descriptor.setHidden(false); + descriptor.setPreferred(true); + descriptor.setShortDescription("application scoped attribute"); + descriptor.setValue("type", value.getClass()); + descriptor.setValue("resolvableAtDesignTime", Boolean.FALSE); + list.add(descriptor); + } + return list.iterator(); + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base == null) { + return String.class; + } + return null; + } +} diff --git a/java/jakarta/servlet/jsp/el/VariableResolver.java b/java/jakarta/servlet/jsp/el/VariableResolver.java new file mode 100644 index 0000000..1c5fef3 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/VariableResolver.java @@ -0,0 +1,47 @@ +/* + * 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 jakarta.servlet.jsp.el; + +/** + *

+ * This class is used to customize the way an ExpressionEvaluator resolves variable references at evaluation time. For + * example, instances of this class can implement their own variable lookup mechanisms, or introduce the notion of + * "implicit variables" which override any other variables. An instance of this class should be passed when evaluating + * an expression. + *

+ *

+ * An instance of this class includes the context against which resolution will happen + *

+ * + * @since JSP 2.0 + * + * @deprecated As of JSP 2.1, replaced by jakarta.el.ELResolver + */ +@Deprecated +public interface VariableResolver { + + /** + * Resolves the specified variable. Returns null if the variable is not found. + * + * @param pName the name of the variable to resolve + * + * @return the result of the variable resolution + * + * @throws ELException if a failure occurred while trying to resolve the given variable + */ + Object resolveVariable(String pName) throws ELException; +} diff --git a/java/jakarta/servlet/jsp/el/package.html b/java/jakarta/servlet/jsp/el/package.html new file mode 100644 index 0000000..38618f3 --- /dev/null +++ b/java/jakarta/servlet/jsp/el/package.html @@ -0,0 +1,37 @@ + + + + + + + +Classes and interfaces for the JSP 2.0 Expression Language API. + +

+The JavaServer Pages(tm) (JSP) 2.0 specification provides a portable +API for evaluating "EL Expressions". As of JSP 2.0, EL expressions can +be placed directly in the template text of JSP pages and tag files. +

+This package contains a number of classes and interfaces that describe +and define programmatic access to the Expression Language evaluator. +This API can also be used by an implementation of JSP to evaluate the +expressions, but other implementations, like open-coding into Java +bytecodes, are allowed. This package is intended to have no dependencies +on other portions of the JSP 2.0 specification. + + diff --git a/java/jakarta/servlet/jsp/package.html b/java/jakarta/servlet/jsp/package.html new file mode 100644 index 0000000..104fac6 --- /dev/null +++ b/java/jakarta/servlet/jsp/package.html @@ -0,0 +1,29 @@ + + + + + + +Classes and interfaces for the Core JSP 2.0 API. +

+The jakarta.servlet.jsp package contains a number of classes and +interfaces that describe and define the contracts between a JSP page +implementation class and the runtime environment provided for an +instance of such a class by a conforming JSP container. + + diff --git a/java/jakarta/servlet/jsp/resources/jspxml.dtd b/java/jakarta/servlet/jsp/resources/jspxml.dtd new file mode 100644 index 0000000..a5fcc5f --- /dev/null +++ b/java/jakarta/servlet/jsp/resources/jspxml.dtd @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/jsp/resources/jspxml.xsd b/java/jakarta/servlet/jsp/resources/jspxml.xsd new file mode 100644 index 0000000..4cad6bb --- /dev/null +++ b/java/jakarta/servlet/jsp/resources/jspxml.xsd @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + +]> + + + + + + + + + XML Schema for JSP 2.0. + + This schema is based upon the recent (May 5th, 2001) + W3C recommendation for XML Schema. + + A JSP translator should reject an XML-format file that is + not strictly valid according to this schema or does not observe + the constraints documented here. A translator is not required + to use this schema for validation or to use a validating parser. + + + + + + + + + + Body defines the "top-level" elements in root and beanInfo. + There are probably other elements that should use it. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A request-time expression value + + + + + + + + + + + Bool would be boolean except it does not accept 1 and 0. + + + + + + + + + + + + + + Identifier is an unqualified Java identifier. + + + + + + + + + + + TypeName is one or more Java identifiers separated by dots + with no whitespace. + + + + + + + + + + + ImportList is one or more typeNames separated by commas. + Whitespace is allowed before and after the comma. + + + + + + + + + + + SetProp is an Identifier or *. + + + + + + + + + + + RelativeURL is a uriReference with no colon character + before the first /, ? or #, if any (RFC2396). + + + + + + + + + + + + + + + Length is nn or nn%. + + + + + + + + + + + + Buffer Size with an explicit value + + + + + + + + + + + Buffer Size with a "none" value + + + + + + + + + + + Buffer size is xkb or none. + + + + + + + + + Content type and character encoding for this page. + + + + + + + + + + + Page Encoding for this page. + + + + + + + + + + + valid scope values + + + + + + + + + + + + + + valid values for a plugin type + + + + + + + + + + + + Buffer size is xkb. + + + + + + + + + + + + + + + + + The root element of all JSP documents is named root. + + Authors may, if they wish, include schema location information. + If specified, the information may appear as attributes of + the root element as follows: + + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/JSP/Page xsd-file-location" + + Documents should not specify the system identifier of a DTD + in a DOCTYPE declaration. + + + + + + + + + + + + + + + directive.page is the "page directive". + + + + + + + + + + + + + + + + + + + + + + + + directive.include is the "include directive". + This element does not appear on XML views of JSP pages. + + + + + + + + + + + The representation of a scriplet. + + + + + + + + The representation of a declaration. + + + + + + + + The representation of an expression. + + + + + + + + Verbatim template text. + + + + + + + + useBean instantiates or accesses a bean in the specified scope. + + Constraint: The allowed combinations of attributes are: + + class [type] | type [( class | beanName)] + + + + + + + + + + + + + + + + + + + + setProperty changes the value of an object property. + + Constraint: The object named by the name must have been + "introduced" to the JSP processor using either the + jsp:useBean action or a custom action with an associated + VariableInfo entry for this name. + + Exact valid combinations are not expressable in XML Schema. + They are: + + name="Identifier" property="*" + name="Identifier" property="Identfiier" param="string" + name="Identifier" property="Identifier" value="string" + + + + + + + + + + + + + + + getProperty obtains the value of an object property. + + Constraint: The object named by the name must have been + "introduced" to the JSP processor using either the + jsp:useBean action or a custom action with an associated + VariableInfo entry for this name. + + ???The spec is interpreted as restricting the values of + property to Identifier. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/jsp/tagext/BodyContent.java b/java/jakarta/servlet/jsp/tagext/BodyContent.java new file mode 100644 index 0000000..6858ddd --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/BodyContent.java @@ -0,0 +1,115 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; + +import jakarta.servlet.jsp.JspWriter; + +/** + * An encapsulation of the evaluation of the body of an action so it is available to a tag handler. BodyContent is a + * subclass of JspWriter. + *

+ * Note that the content of BodyContent is the result of evaluation, so it will not contain actions and the like, but + * the result of their invocation. + *

+ * BodyContent has methods to convert its contents into a String, to read its contents, and to clear the contents. + *

+ * The buffer size of a BodyContent object is unbounded. A BodyContent object cannot be in autoFlush mode. It is not + * possible to invoke flush on a BodyContent object, as there is no backing stream. + *

+ * Instances of BodyContent are created by invoking the pushBody and popBody methods of the PageContext class. A + * BodyContent is enclosed within another JspWriter (maybe another BodyContent object) following the structure of their + * associated actions. + *

+ * A BodyContent is made available to a BodyTag through a setBodyContent() call. The tag handler can use the object + * until after the call to doEndTag(). + */ +public abstract class BodyContent extends JspWriter { + + /** + * Protected constructor. Unbounded buffer, no autoflushing. + * + * @param e the enclosing JspWriter + */ + protected BodyContent(JspWriter e) { + super(UNBOUNDED_BUFFER, false); + this.enclosingWriter = e; + } + + /** + * Redefined flush() so it is not legal. + *

+ * It is not valid to flush a BodyContent because there is no backing stream behind it. + * + * @throws IOException always thrown + */ + @Override + public void flush() throws IOException { + throw new IOException("Illegal to flush within a custom tag"); + } + + /** + * Clear the body without throwing any exceptions. + */ + public void clearBody() { + try { + this.clear(); + } catch (IOException ex) { + // TODO -- clean this one up. + throw new Error("internal error!;"); + } + } + + /** + * Return the value of this BodyContent as a Reader. + * + * @return the value of this BodyContent as a Reader + */ + public abstract Reader getReader(); + + /** + * Return the value of the BodyContent as a String. + * + * @return the value of the BodyContent as a String + */ + public abstract String getString(); + + /** + * Write the contents of this BodyContent into a Writer. Subclasses may optimize common invocation patterns. + * + * @param out The writer into which to place the contents of this body evaluation + * + * @throws IOException if an I/O error occurred while writing the contents of this BodyContent to the given Writer + */ + public abstract void writeOut(Writer out) throws IOException; + + /** + * Get the enclosing JspWriter. + * + * @return the enclosing JspWriter passed at construction time + */ + public JspWriter getEnclosingWriter() { + return enclosingWriter; + } + + // private fields + + private final JspWriter enclosingWriter; +} diff --git a/java/jakarta/servlet/jsp/tagext/BodyTag.java b/java/jakarta/servlet/jsp/tagext/BodyTag.java new file mode 100644 index 0000000..962cae1 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/BodyTag.java @@ -0,0 +1,134 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import jakarta.servlet.jsp.JspException; + + +/** + * The BodyTag interface extends IterationTag by defining additional methods that let a tag handler manipulate the + * content of evaluating its body. + *

+ * It is the responsibility of the tag handler to manipulate the body content. For example the tag handler may take the + * body content, convert it into a String using the bodyContent.getString method and then use it. Or the tag handler may + * take the body content and write it out into its enclosing JspWriter using the bodyContent.writeOut method. + *

+ * A tag handler that implements BodyTag is treated as one that implements IterationTag, except that the doStartTag + * method can return SKIP_BODY, EVAL_BODY_INCLUDE or EVAL_BODY_BUFFERED. + *

+ * If EVAL_BODY_INCLUDE is returned, then evaluation happens as in IterationTag. + *

+ * If EVAL_BODY_BUFFERED is returned, then a BodyContent object will be created (by code generated by the JSP compiler) + * to capture the body evaluation. The code generated by the JSP compiler obtains the BodyContent object by calling the + * pushBody method of the current pageContext, which additionally has the effect of saving the previous out value. The + * page compiler returns this object by calling the popBody method of the PageContext class; the call also restores the + * value of out. + *

+ * The interface provides one new property with a setter method and one new action method. + *

+ * Properties + *

+ * There is a new property: bodyContent, to contain the BodyContent object, where the JSP Page implementation object + * will place the evaluation (and reevaluation, if appropriate) of the body. The setter method (setBodyContent) will + * only be invoked if doStartTag() returns EVAL_BODY_BUFFERED and the corresponding action element does not have an + * empty body. + *

+ * Methods + *

+ * In addition to the setter method for the bodyContent property, there is a new action method: doInitBody(), which is + * invoked right after setBodyContent() and before the body evaluation. This method is only invoked if doStartTag() + * returns EVAL_BODY_BUFFERED. + *

+ * Lifecycle + *

+ * Lifecycle details are described by the transition diagram below. Exceptions that are thrown during the computation of + * doStartTag(), setBodyContent(), doInitBody(), BODY, doAfterBody() interrupt the execution sequence and are propagated + * up the stack, unless the tag handler implements the TryCatchFinally interface; see that interface for details. + *

+ * Lifecycle Details Transition Diagram for BodyTag + *

+ * Empty and Non-Empty Action + *

+ * If the TagLibraryDescriptor file indicates that the action must always have an empty element body, by an + * <body-content> entry of "empty", then the doStartTag() method must return SKIP_BODY. Otherwise, the + * doStartTag() method may return SKIP_BODY, EVAL_BODY_INCLUDE, or EVAL_BODY_BUFFERED. + *

+ * Note that which methods are invoked after the doStartTag() depends on both the return value and on if the custom + * action element is empty or not in the JSP page, not how it's declared in the TLD. + *

+ * If SKIP_BODY is returned the body is not evaluated, and doEndTag() is invoked. + *

+ * If EVAL_BODY_INCLUDE is returned, and the custom action element is not empty, setBodyContent() is not invoked, + * doInitBody() is not invoked, the body is evaluated and "passed through" to the current out, doAfterBody() is invoked + * and then, after zero or more iterations, doEndTag() is invoked. If the custom action element is empty, only doStart() + * and doEndTag() are invoked. + *

+ * If EVAL_BODY_BUFFERED is returned, and the custom action element is not empty, setBodyContent() is invoked, + * doInitBody() is invoked, the body is evaluated, doAfterBody() is invoked, and then, after zero or more iterations, + * doEndTag() is invoked. If the custom action element is empty, only doStart() and doEndTag() are invoked. + */ +public interface BodyTag extends IterationTag { + + /** + * Deprecated constant that has the same value as EVAL_BODY_BUFFERED and EVAL_BODY_AGAIN. This name has been marked + * as deprecated to encourage the use of the two different terms, which are much more descriptive. + * + * @deprecated As of Java JSP API 1.2, use BodyTag.EVAL_BODY_BUFFERED or IterationTag.EVAL_BODY_AGAIN. + */ + @Deprecated + int EVAL_BODY_TAG = 2; + + /** + * Request the creation of new buffer, a BodyContent on which to evaluate the body of this tag. Returned from + * doStartTag when it implements BodyTag. This is an illegal return value for doStartTag when the class does not + * implement BodyTag. + */ + int EVAL_BODY_BUFFERED = 2; + + /** + * Set the bodyContent property. This method is invoked by the JSP page implementation object at most once per + * action invocation. This method will be invoked before doInitBody. This method will not be invoked for empty tags + * or for non-empty tags whose doStartTag() method returns SKIP_BODY or EVAL_BODY_INCLUDE. + *

+ * When setBodyContent is invoked, the value of the implicit object out has already been changed in the pageContext + * object. The BodyContent object passed will have not data on it but may have been reused (and cleared) from some + * previous invocation. + *

+ * The BodyContent object is available and with the appropriate content until after the invocation of the doEndTag + * method, at which case it may be reused. + * + * @param b the BodyContent + * + * @see #doInitBody + * @see #doAfterBody + */ + void setBodyContent(BodyContent b); + + /** + * Prepare for evaluation of the body. This method is invoked by the JSP page implementation object after + * setBodyContent and before the first time the body is to be evaluated. This method will not be invoked for empty + * tags or for non-empty tags whose doStartTag() method returns SKIP_BODY or EVAL_BODY_INCLUDE. + *

+ * The JSP container will resynchronize the values of any AT_BEGIN and NESTED variables (defined by the associated + * TagExtraInfo or TLD) after the invocation of doInitBody(). + * + * @throws JspException if an error occurred while processing this tag + * + * @see #doAfterBody + */ + void doInitBody() throws JspException; +} diff --git a/java/jakarta/servlet/jsp/tagext/BodyTagSupport.java b/java/jakarta/servlet/jsp/tagext/BodyTagSupport.java new file mode 100644 index 0000000..ffb5909 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/BodyTagSupport.java @@ -0,0 +1,152 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; + +/** + * A base class for defining tag handlers implementing BodyTag. + *

+ * The BodyTagSupport class implements the BodyTag interface and adds additional convenience methods including getter + * methods for the bodyContent property and methods to get at the previous out JspWriter. + *

+ * Many tag handlers will extend BodyTagSupport and only redefine a few methods. + */ +public class BodyTagSupport extends TagSupport implements BodyTag { + + private static final long serialVersionUID = -7235752615580319833L; + + /** + * Default constructor, all subclasses are required to only define a public constructor with the same signature, and + * to call the superclass constructor. This constructor is called by the code generated by the JSP translator. + */ + public BodyTagSupport() { + super(); + } + + /** + * Default processing of the start tag returning EVAL_BODY_BUFFERED. + * + * @return EVAL_BODY_BUFFERED + * + * @throws JspException if an error occurred while processing this tag + * + * @see BodyTag#doStartTag + */ + @Override + public int doStartTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** + * Default processing of the end tag returning EVAL_PAGE. + * + * @return EVAL_PAGE + * + * @throws JspException if an error occurred while processing this tag + * + * @see Tag#doEndTag + */ + @Override + public int doEndTag() throws JspException { + return super.doEndTag(); + } + + // Actions related to body evaluation + + /** + * Prepare for evaluation of the body: stash the bodyContent away. + * + * @param b the BodyContent + * + * @see #doAfterBody + * @see #doInitBody() + * @see BodyTag#setBodyContent + */ + @Override + public void setBodyContent(BodyContent b) { + this.bodyContent = b; + } + + /** + * Prepare for evaluation of the body just before the first body evaluation: no action. + * + * @throws JspException if an error occurred while processing this tag + * + * @see #setBodyContent + * @see #doAfterBody + * @see BodyTag#doInitBody + */ + @Override + public void doInitBody() throws JspException { + // NOOP by default + } + + /** + * After the body evaluation: do not reevaluate and continue with the page. By default nothing is done with the + * bodyContent data (if any). + * + * @return SKIP_BODY + * + * @throws JspException if an error occurred while processing this tag + * + * @see #doInitBody + * @see BodyTag#doAfterBody + */ + @Override + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Release state. + * + * @see Tag#release + */ + @Override + public void release() { + bodyContent = null; + + super.release(); + } + + /** + * Get current bodyContent. + * + * @return the body content. + */ + public BodyContent getBodyContent() { + return bodyContent; + } + + /** + * Get surrounding out JspWriter. + * + * @return the enclosing JspWriter, from the bodyContent. + */ + public JspWriter getPreviousOut() { + return bodyContent.getEnclosingWriter(); + } + + // protected fields + + /** + * The current BodyContent for this BodyTag. + */ + protected transient BodyContent bodyContent; +} diff --git a/java/jakarta/servlet/jsp/tagext/DynamicAttributes.java b/java/jakarta/servlet/jsp/tagext/DynamicAttributes.java new file mode 100644 index 0000000..8a776a9 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/DynamicAttributes.java @@ -0,0 +1,44 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import jakarta.servlet.jsp.JspException; + +/** + * For a tag to declare that it accepts dynamic attributes, it must implement this interface. The entry for the tag in + * the Tag Library Descriptor must also be configured to indicate dynamic attributes are accepted.
+ * For any attribute that is not declared in the Tag Library Descriptor for this tag, instead of getting an error at + * translation time, the setDynamicAttribute() method is called, with the name and value of the attribute. + * It is the responsibility of the tag to remember the names and values of the dynamic attributes. + * + * @since JSP 2.0 + */ +public interface DynamicAttributes { + + /** + * Called when a tag declared to accept dynamic attributes is passed an attribute that is not declared in the Tag + * Library Descriptor. + * + * @param uri the namespace of the attribute, or null if in the default namespace. + * @param localName the name of the attribute being set. + * @param value the value of the attribute + * + * @throws JspException if the tag handler wishes to signal that it does not accept the given attribute. The + * container must not call doStartTag() or doTag() for this tag. + */ + void setDynamicAttribute(String uri, String localName, Object value) throws JspException; +} diff --git a/java/jakarta/servlet/jsp/tagext/FunctionInfo.java b/java/jakarta/servlet/jsp/tagext/FunctionInfo.java new file mode 100644 index 0000000..70786bb --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/FunctionInfo.java @@ -0,0 +1,77 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Information for a function in a Tag Library. This class is instantiated from the Tag Library Descriptor file (TLD) + * and is available only at translation time. + * + * @since JSP 2.0 + */ +public class FunctionInfo { + + /** + * Constructor for FunctionInfo. + * + * @param name The name of the function + * @param klass The class of the function + * @param signature The signature of the function + */ + + public FunctionInfo(String name, String klass, String signature) { + this.name = name; + this.functionClass = klass; + this.functionSignature = signature; + } + + /** + * The name of the function. + * + * @return The name of the function + */ + + public String getName() { + return name; + } + + /** + * The class of the function. + * + * @return The class of the function + */ + + public String getFunctionClass() { + return functionClass; + } + + /** + * The signature of the function. + * + * @return The signature of the function + */ + + public String getFunctionSignature() { + return functionSignature; + } + + /* + * fields + */ + private final String name; + private final String functionClass; + private final String functionSignature; +} diff --git a/java/jakarta/servlet/jsp/tagext/IterationTag.java b/java/jakarta/servlet/jsp/tagext/IterationTag.java new file mode 100644 index 0000000..3fa65d1 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/IterationTag.java @@ -0,0 +1,88 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import jakarta.servlet.jsp.JspException; + + +/** + * The IterationTag interface extends Tag by defining one additional method that controls the reevaluation of its body. + *

+ * A tag handler that implements IterationTag is treated as one that implements Tag regarding the doStartTag() and + * doEndTag() methods. IterationTag provides a new method: doAfterBody(). + *

+ * The doAfterBody() method is invoked after every body evaluation to control whether the body will be reevaluated or + * not. If doAfterBody() returns IterationTag.EVAL_BODY_AGAIN, then the body will be reevaluated. If doAfterBody() + * returns Tag.SKIP_BODY, then the body will be skipped and doEndTag() will be evaluated instead. + *

+ * Properties There are no new properties in addition to those in Tag. + *

+ * Methods There is one new methods: doAfterBody(). + *

+ * Lifecycle + *

+ * Lifecycle details are described by the transition diagram below. Exceptions that are thrown during the computation of + * doStartTag(), BODY and doAfterBody() interrupt the execution sequence and are propagated up the stack, unless the tag + * handler implements the TryCatchFinally interface; see that interface for details. + *

+ * Lifecycle Details Transition Diagram for IterationTag + *

+ * Empty and Non-Empty Action + *

+ * If the TagLibraryDescriptor file indicates that the action must always have an empty element body, by a + * <body-content> entry of "empty", then the doStartTag() method must return SKIP_BODY. + *

+ * Note that which methods are invoked after the doStartTag() depends on both the return value and on if the custom + * action element is empty or not in the JSP page, not on how it's declared in the TLD. + *

+ * If SKIP_BODY is returned the body is not evaluated, and then doEndTag() is invoked. + *

+ * If EVAL_BODY_INCLUDE is returned, and the custom action element is not empty, the body is evaluated and "passed + * through" to the current out, then doAfterBody() is invoked and, after zero or more iterations, doEndTag() is invoked. + */ + +public interface IterationTag extends Tag { + + /** + * Request the reevaluation of some body. Returned from doAfterBody. For compatibility with JSP 1.1, the value is + * carefully selected to be the same as the, now deprecated, BodyTag.EVAL_BODY_TAG, + */ + int EVAL_BODY_AGAIN = 2; + + /** + * Process body (re)evaluation. This method is invoked by the JSP Page implementation object after every evaluation + * of the body into the BodyEvaluation object. The method is not invoked if there is no body evaluation. + *

+ * If doAfterBody returns EVAL_BODY_AGAIN, a new evaluation of the body will happen (followed by another invocation + * of doAfterBody). If doAfterBody returns SKIP_BODY, no more body evaluations will occur, and the doEndTag method + * will be invoked. + *

+ * If this tag handler implements BodyTag and doAfterBody returns SKIP_BODY, the value of out will be restored using + * the popBody method in pageContext prior to invoking doEndTag. + *

+ * The method re-invocations may be lead to different actions because there might have been some changes to shared + * state, or because of external computation. + *

+ * The JSP container will resynchronize the values of any AT_BEGIN and NESTED variables (defined by the associated + * TagExtraInfo or TLD) after the invocation of doAfterBody(). + * + * @return whether additional evaluations of the body are desired + * + * @throws JspException if an error occurred while processing this tag + */ + int doAfterBody() throws JspException; +} diff --git a/java/jakarta/servlet/jsp/tagext/JspFragment.java b/java/jakarta/servlet/jsp/tagext/JspFragment.java new file mode 100644 index 0000000..c384d71 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/JspFragment.java @@ -0,0 +1,79 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import java.io.IOException; +import java.io.Writer; + +import jakarta.servlet.jsp.JspContext; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.SkipPageException; + +/** + * Encapsulates a portion of JSP code in an object that can be invoked as many times as needed. JSP Fragments are + * defined using JSP syntax as the body of a tag for an invocation to a SimpleTag handler, or as the body of a + * <jsp:attribute> standard action specifying the value of an attribute that is declared as a fragment, or to be + * of type JspFragment in the TLD. + *

+ * The definition of the JSP fragment must only contain template text and JSP action elements. In other words, it must + * not contain scriptlets or scriptlet expressions. At translation time, the container generates an implementation of + * the JspFragment abstract class capable of executing the defined fragment. + *

+ * A tag handler can invoke the fragment zero or more times, or pass it along to other tags, before returning. To + * communicate values to/from a JSP fragment, tag handlers store/retrieve values in the JspContext associated with the + * fragment. + *

+ * Note that tag library developers and page authors should not generate JspFragment implementations manually. + *

+ * Implementation Note: It is not necessary to generate a separate class for each fragment. One possible + * implementation is to generate a single helper class for each page that implements JspFragment. Upon construction, a + * discriminator can be passed to select which fragment that instance will execute. + * + * @since JSP 2.0 + */ +public abstract class JspFragment { + + /** + * Default constructor. + */ + public JspFragment() { + // NO-OP by default + } + + /** + * Executes the fragment and directs all output to the given Writer, or the JspWriter returned by the getOut() + * method of the JspContext associated with the fragment if out is null. + * + * @param out The Writer to output the fragment to, or null if output should be sent to JspContext.getOut(). + * + * @throws JspException Thrown if an error occurred while invoking this fragment. + * @throws SkipPageException Thrown if the page that (either directly or indirectly) invoked the tag handler that + * invoked this fragment is to cease evaluation. The container must throw this + * exception if a Classic Tag Handler returned Tag.SKIP_PAGE or if a Simple Tag + * Handler threw SkipPageException. + * @throws IOException If there was an error writing to the stream. + */ + public abstract void invoke(Writer out) throws JspException, IOException; + + /** + * Returns the JspContext that is bound to this JspFragment. + * + * @return The JspContext used by this fragment at invocation time. + */ + public abstract JspContext getJspContext(); + +} diff --git a/java/jakarta/servlet/jsp/tagext/JspIdConsumer.java b/java/jakarta/servlet/jsp/tagext/JspIdConsumer.java new file mode 100644 index 0000000..f8cfea5 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/JspIdConsumer.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Interface that allows tag handlers to be provided with a unique (within the scope of the web application) ID. + */ +public interface JspIdConsumer { + + /** + * Set the unique ID for the tag handler. + * + * @param jspId The unique Id + */ + void setJspId(String jspId); +} diff --git a/java/jakarta/servlet/jsp/tagext/JspTag.java b/java/jakarta/servlet/jsp/tagext/JspTag.java new file mode 100644 index 0000000..e52984f --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/JspTag.java @@ -0,0 +1,26 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Serves as a base class for Tag and SimpleTag. This is mostly for organizational and type-safety purposes. + * + * @since JSP 2.0 + */ +public interface JspTag { + // No methods even through there are some common methods +} diff --git a/java/jakarta/servlet/jsp/tagext/PageData.java b/java/jakarta/servlet/jsp/tagext/PageData.java new file mode 100644 index 0000000..d0894fb --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/PageData.java @@ -0,0 +1,43 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import java.io.InputStream; + +/** + * Translation-time information on a JSP page. The information corresponds to the XML view of the JSP page. + *

+ * Objects of this type are generated by the JSP translator, e.g. when being passed to a TagLibraryValidator instance. + */ + +public abstract class PageData { + + /** + * Sole constructor. (For invocation by subclass constructors, typically implicit.) + */ + public PageData() { + // NOOP by default + } + + /** + * Returns an input stream on the XML view of a JSP page. The stream is encoded in UTF-8. Recall that the XML view + * of a JSP page has the include directives expanded. + * + * @return An input stream on the document. + */ + public abstract InputStream getInputStream(); +} diff --git a/java/jakarta/servlet/jsp/tagext/SimpleTag.java b/java/jakarta/servlet/jsp/tagext/SimpleTag.java new file mode 100644 index 0000000..e661414 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/SimpleTag.java @@ -0,0 +1,121 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import java.io.IOException; + +import jakarta.servlet.jsp.JspContext; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.SkipPageException; + +/** + * Interface for defining Simple Tag Handlers. + *

+ * Simple Tag Handlers differ from Classic Tag Handlers in that instead of supporting doStartTag() and + * doEndTag(), the SimpleTag interface provides a simple doTag() method, which is + * called once and only once for any given tag invocation. All tag logic, iteration, body evaluations, etc. are to be + * performed in this single method. Thus, simple tag handlers have the equivalent power of BodyTag, but + * with a much simpler lifecycle and interface. + *

+ *

+ * To support body content, the setJspBody() method is provided. The container invokes the + * setJspBody() method with a JspFragment object encapsulating the body of the tag. The tag + * handler implementation can call invoke() on that fragment to evaluate the body as many times as it + * needs. + *

+ *

+ * A SimpleTag handler must have a public no-args constructor. Most SimpleTag handlers should extend SimpleTagSupport. + *

+ *

+ * Lifecycle + *

+ *

+ * The following is a non-normative, brief overview of the SimpleTag lifecycle. Refer to the JSP Specification for + * details. + *

+ *
    + *
  1. A new tag handler instance is created each time by the container by calling the provided zero-args constructor. + * Unlike classic tag handlers, simple tag handlers are never cached and reused by the JSP container.
  2. + *
  3. The setJspContext() and setParent() methods are called by the container. The + * setParent() method is only called if the element is nested within another tag invocation.
  4. + *
  5. The setters for each attribute defined for this tag are called by the container.
  6. + *
  7. If a body exists, the setJspBody() method is called by the container to set the body of this tag, as + * a JspFragment. If the action element is empty in the page, this method is not called at all.
  8. + *
  9. The doTag() method is called by the container. All tag logic, iteration, body evaluations, etc. + * occur in this method.
  10. + *
  11. The doTag() method returns and all variables are synchronized.
  12. + *
+ * + * @see SimpleTagSupport + * + * @since JSP 2.0 + */ +public interface SimpleTag extends JspTag { + + /** + * Called by the container to invoke this tag. The implementation of this method is provided by the tag library + * developer, and handles all tag processing, body iteration, etc. + *

+ * The JSP container will resynchronize any AT_BEGIN and AT_END variables (defined by the associated tag file, + * TagExtraInfo, or TLD) after the invocation of doTag(). + * + * @throws JspException If an error occurred while processing this tag. + * @throws SkipPageException If the page that (either directly or indirectly) invoked this tag is to cease + * evaluation. A Simple Tag Handler generated from a tag file must throw this + * exception if an invoked Classic Tag Handler returned SKIP_PAGE or if an invoked + * Simple Tag Handler threw SkipPageException or if an invoked Jsp Fragment threw a + * SkipPageException. + * @throws IOException If there was an error writing to the output stream. + */ + void doTag() throws JspException, IOException; + + /** + * Sets the parent of this tag, for collaboration purposes. + *

+ * The container invokes this method only if this tag invocation is nested within another tag invocation. + * + * @param parent the tag that encloses this tag + */ + void setParent(JspTag parent); + + /** + * Returns the parent of this tag, for collaboration purposes. + * + * @return the parent of this tag + */ + JspTag getParent(); + + /** + * Called by the container to provide this tag handler with the JspContext for this invocation. An + * implementation should save this value. + * + * @param pc the page context for this invocation + * + * @see Tag#setPageContext + */ + void setJspContext(JspContext pc); + + /** + * Provides the body of this tag as a JspFragment object, able to be invoked zero or more times by the tag handler. + *

+ * This method is invoked by the JSP page implementation object prior to doTag(). If the action element + * is empty in the page, this method is not called at all. + * + * @param jspBody The fragment encapsulating the body of this tag. + */ + void setJspBody(JspFragment jspBody); +} diff --git a/java/jakarta/servlet/jsp/tagext/SimpleTagSupport.java b/java/jakarta/servlet/jsp/tagext/SimpleTagSupport.java new file mode 100644 index 0000000..3398b2d --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/SimpleTagSupport.java @@ -0,0 +1,190 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import java.io.IOException; + +import jakarta.servlet.jsp.JspContext; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.SkipPageException; + +/** + * A base class for defining tag handlers implementing SimpleTag. + *

+ * The SimpleTagSupport class is a utility class intended to be used as the base class for new simple tag handlers. The + * SimpleTagSupport class implements the SimpleTag interface and adds additional convenience methods including getter + * methods for the properties in SimpleTag. + * + * @since JSP 2.0 + */ +public class SimpleTagSupport implements SimpleTag { + /** Reference to the enclosing tag. */ + private JspTag parentTag; + + /** The JSP context for the upcoming tag invocation. */ + private JspContext jspContext; + + /** The body of the tag. */ + private JspFragment jspBody; + + /** + * Sole constructor. (For invocation by subclass constructors, typically implicit.) + */ + public SimpleTagSupport() { + // NOOP by default + } + + /** + * Default processing of the tag does nothing. + * + * @throws JspException Subclasses can throw JspException to indicate an error occurred while processing this + * tag. + * @throws SkipPageException If the page that (either directly or indirectly) invoked this tag is to cease + * evaluation. A Simple Tag Handler generated from a tag file must throw this + * exception if an invoked Classic Tag Handler returned SKIP_PAGE or if an invoked + * Simple Tag Handler threw SkipPageException or if an invoked Jsp Fragment threw a + * SkipPageException. + * @throws IOException Subclasses can throw IOException if there was an error writing to the output stream + * + * @see SimpleTag#doTag() + */ + @Override + public void doTag() throws JspException, IOException { + // NOOP by default + } + + /** + * Sets the parent of this tag, for collaboration purposes. + *

+ * The container invokes this method only if this tag invocation is nested within another tag invocation. + * + * @param parent the tag that encloses this tag + */ + @Override + public void setParent(JspTag parent) { + this.parentTag = parent; + } + + /** + * Returns the parent of this tag, for collaboration purposes. + * + * @return the parent of this tag + */ + @Override + public JspTag getParent() { + return this.parentTag; + } + + /** + * Stores the provided JSP context in the private jspContext field. Subclasses can access the + * JspContext via getJspContext(). + * + * @param pc the page context for this invocation + * + * @see SimpleTag#setJspContext + */ + @Override + public void setJspContext(JspContext pc) { + this.jspContext = pc; + } + + /** + * Returns the page context passed in by the container via setJspContext. + * + * @return the page context for this invocation + */ + protected JspContext getJspContext() { + return this.jspContext; + } + + /** + * Stores the provided JspFragment. + * + * @param jspBody The fragment encapsulating the body of this tag. If the action element is empty in the page, this + * method is not called at all. + * + * @see SimpleTag#setJspBody + */ + @Override + public void setJspBody(JspFragment jspBody) { + this.jspBody = jspBody; + } + + /** + * Returns the body passed in by the container via setJspBody. + * + * @return the fragment encapsulating the body of this tag, or null if the action element is empty in the page. + */ + protected JspFragment getJspBody() { + return this.jspBody; + } + + /** + * Find the instance of a given class type that is closest to a given instance. This method uses the getParent + * method from the Tag and/or SimpleTag interfaces. This method is used for coordination among cooperating tags. + *

+ * For every instance of TagAdapter encountered while traversing the ancestors, the tag handler returned by + * TagAdapter.getAdaptee() - instead of the TagAdapter itself - is compared to klass. If + * the tag handler matches, it - and not its TagAdapter - is returned. + *

+ * The current version of the specification only provides one formal way of indicating the observable type of a tag + * handler: its tag handler implementation class, described in the tag-class subelement of the tag element. This is + * extended in an informal manner by allowing the tag library author to indicate in the description subelement an + * observable type. The type should be a subtype of the tag handler implementation class or void. This additional + * constraint can be exploited by a specialized container that knows about that specific tag library, as in the case + * of the JSP standard tag library. + *

+ * When a tag library author provides information on the observable type of a tag handler, client programmatic code + * should adhere to that constraint. Specifically, the Class passed to findAncestorWithClass should be a subtype of + * the observable type. + * + * @param from The instance from where to start looking. + * @param klass The subclass of JspTag or interface to be matched + * + * @return the nearest ancestor that implements the interface or is an instance of the class specified + */ + public static final JspTag findAncestorWithClass(JspTag from, Class klass) { + boolean isInterface = false; + + if (from == null || klass == null || + !JspTag.class.isAssignableFrom(klass) && !(isInterface = klass.isInterface())) { + return null; + } + + for (;;) { + JspTag parent = null; + if (from instanceof SimpleTag) { + parent = ((SimpleTag) from).getParent(); + } else if (from instanceof Tag) { + parent = ((Tag) from).getParent(); + } + if (parent == null) { + return null; + } + + if (parent instanceof TagAdapter) { + parent = ((TagAdapter) parent).getAdaptee(); + } + + if (isInterface && klass.isInstance(parent) || klass.isAssignableFrom(parent.getClass())) { + return parent; + } + + from = parent; + } + } +} diff --git a/java/jakarta/servlet/jsp/tagext/Tag.java b/java/jakarta/servlet/jsp/tagext/Tag.java new file mode 100644 index 0000000..1631441 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/Tag.java @@ -0,0 +1,225 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; + + +/** + * The interface of a classic tag handler that does not want to manipulate its body. The Tag interface defines the basic + * protocol between a Tag handler and JSP page implementation class. It defines the life cycle and the methods to be + * invoked at start and end tag. + *

+ * Properties + *

+ *

+ * The Tag interface specifies the setter and getter methods for the core pageContext and parent properties. + *

+ *

+ * The JSP page implementation object invokes setPageContext and setParent, in that order, before invoking doStartTag() + * or doEndTag(). + *

+ *

+ * Methods + *

+ *

+ * There are two main actions: doStartTag and doEndTag. Once all appropriate properties have been initialized, the + * doStartTag and doEndTag methods can be invoked on the tag handler. Between these invocations, the tag handler is + * assumed to hold a state that must be preserved. After the doEndTag invocation, the tag handler is available for + * further invocations (and it is expected to have retained its properties). + *

+ *

+ * Lifecycle + *

+ *

+ * Lifecycle details are described by the transition diagram below, with the following comments: + *

+ *
    + *
  • [1] This transition is intended to be for releasing long-term data. no guarantees are assumed on whether any + * properties have been retained or not. + *
  • [2] This transition happens if and only if the tag ends normally without raising an exception + *
  • [3] Some setters may be called again before a tag handler is reused. For instance, setParent() is + * called if it's reused within the same page but at a different level, setPageContext() is called if it's + * used in another page, and attribute setters are called if the values differ or are expressed as request-time + * attribute values. + *
  • Check the TryCatchFinally interface for additional details related to exception handling and resource management. + *
+ * Lifecycle Details Transition Diagram for Tag + *

+ * Once all invocations on the tag handler are completed, the release method is invoked on it. Once a release method is + * invoked all properties, including parent and pageContext, are assumed to have been reset to an unspecified + * value. The page compiler guarantees that release() will be invoked on the Tag handler before the handler is released + * to the GC. + *

+ *

+ * Empty and Non-Empty Action + *

+ *

+ * If the TagLibraryDescriptor file indicates that the action must always have an empty action, by an + * <body-content> entry of "empty", then the doStartTag() method must return SKIP_BODY. + *

+ *

+ * Otherwise, the doStartTag() method may return SKIP_BODY or EVAL_BODY_INCLUDE. + *

+ *

+ * If SKIP_BODY is returned the body, if present, is not evaluated. + *

+ *

+ * If EVAL_BODY_INCLUDE is returned, the body is evaluated and "passed through" to the current out. + *

+ */ + +public interface Tag extends JspTag { + + /** + * Skip body evaluation. Valid return value for doStartTag and doAfterBody. + */ + + int SKIP_BODY = 0; + + /** + * Evaluate body into existing out stream. Valid return value for doStartTag. + */ + + int EVAL_BODY_INCLUDE = 1; + + /** + * Skip the rest of the page. Valid return value for doEndTag. + */ + + int SKIP_PAGE = 5; + + /** + * Continue evaluating the page. Valid return value for doEndTag(). + */ + + int EVAL_PAGE = 6; + + // Setters for Tag handler data + + + /** + * Set the current page context. This method is invoked by the JSP page implementation object prior to doStartTag(). + *

+ * This value is *not* reset by doEndTag() and must be explicitly reset by a page implementation if it changes + * between calls to doStartTag(). + * + * @param pc The page context for this tag handler. + */ + + void setPageContext(PageContext pc); + + + /** + * Set the parent (closest enclosing tag handler) of this tag handler. Invoked by the JSP page implementation object + * prior to doStartTag(). + *

+ * This value is *not* reset by doEndTag() and must be explicitly reset by a page implementation. + * + * @param t The parent tag, or null. + */ + + + void setParent(Tag t); + + + /** + * Get the parent (closest enclosing tag handler) for this tag handler. + *

+ * The getParent() method can be used to navigate the nested tag handler structure at runtime for cooperation among + * custom actions; for example, the findAncestorWithClass() method in TagSupport provides a convenient way of doing + * this. + *

+ * The current version of the specification only provides one formal way of indicating the observable type of a tag + * handler: its tag handler implementation class, described in the tag-class sub-element of the tag element. This is + * extended in an informal manner by allowing the tag library author to indicate in the description sub-element an + * observable type. The type should be a sub-type of the tag handler implementation class or void. This additional + * constraint can be exploited by a specialized container that knows about that specific tag library, as in the case + * of the JSP standard tag library. + * + * @return the current parent, or null if none. + * + * @see TagSupport#findAncestorWithClass + */ + + Tag getParent(); + + + // Actions for basic start/end processing. + + + /** + * Process the start tag for this instance. This method is invoked by the JSP page implementation object. + *

+ * The doStartTag method assumes that the properties pageContext and parent have been set. It also assumes that any + * properties exposed as attributes have been set too. When this method is invoked, the body has not yet been + * evaluated. + *

+ * This method returns Tag.EVAL_BODY_INCLUDE or BodyTag.EVAL_BODY_BUFFERED to indicate that the body of the action + * should be evaluated or SKIP_BODY to indicate otherwise. + *

+ * When a Tag returns EVAL_BODY_INCLUDE the result of evaluating the body (if any) is included into the current + * "out" JspWriter as it happens and then doEndTag() is invoked. + *

+ * BodyTag.EVAL_BODY_BUFFERED is only valid if the tag handler implements BodyTag. + *

+ * The JSP container will resynchronize the values of any AT_BEGIN and NESTED variables (defined by the associated + * TagExtraInfo or TLD) after the invocation of doStartTag(), except for a tag handler implementing BodyTag whose + * doStartTag() method returns BodyTag.EVAL_BODY_BUFFERED. + * + * @return EVAL_BODY_INCLUDE if the tag wants to process body, SKIP_BODY if it does not want to process it. + * + * @throws JspException if an error occurred while processing this tag + * + * @see BodyTag + */ + + int doStartTag() throws JspException; + + + /** + * Process the end tag for this instance. This method is invoked by the JSP page implementation object on all Tag + * handlers. + *

+ * This method will be called after returning from doStartTag. The body of the action may or may not have been + * evaluated, depending on the return value of doStartTag. + *

+ * If this method returns EVAL_PAGE, the rest of the page continues to be evaluated. If this method returns + * SKIP_PAGE, the rest of the page is not evaluated, the request is completed, and the doEndTag() methods of + * enclosing tags are not invoked. If this request was forwarded or included from another page (or Servlet), only + * the current page evaluation is stopped. + *

+ * The JSP container will resynchronize the values of any AT_BEGIN and AT_END variables (defined by the associated + * TagExtraInfo or TLD) after the invocation of doEndTag(). + * + * @return indication of whether to continue evaluating the JSP page. + * + * @throws JspException if an error occurred while processing this tag + */ + + int doEndTag() throws JspException; + + /** + * Called on a Tag handler to release state. The page compiler guarantees that JSP page implementation objects will + * invoke this method on all tag handlers, but there may be multiple invocations on doStartTag and doEndTag in + * between. + */ + + void release(); + +} diff --git a/java/jakarta/servlet/jsp/tagext/TagAdapter.java b/java/jakarta/servlet/jsp/tagext/TagAdapter.java new file mode 100644 index 0000000..30ee660 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagAdapter.java @@ -0,0 +1,150 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; + +/** + * Wraps any SimpleTag and exposes it using a Tag interface. This is used to allow collaboration between classic Tag + * handlers and SimpleTag handlers. + *

+ * Because SimpleTag does not extend Tag, and because Tag.setParent() only accepts a Tag instance, a classic tag handler + * (one that implements Tag) cannot have a SimpleTag as its parent. To remedy this, a TagAdapter is created to wrap the + * SimpleTag parent, and the adapter is passed to setParent() instead. A classic Tag Handler can call getAdaptee() to + * retrieve the encapsulated SimpleTag instance. + * + * @since JSP 2.0 + */ +public class TagAdapter implements Tag { + /** The simple tag that's being adapted. */ + private final SimpleTag simpleTagAdaptee; + + /** The parent, of this tag, converted (if necessary) to be of type Tag. */ + private Tag parent; + + // Flag indicating whether we have already determined the parent + private boolean parentDetermined; + + /** + * Creates a new TagAdapter that wraps the given SimpleTag and returns the parent tag when getParent() is called. + * + * @param adaptee The SimpleTag being adapted as a Tag. + */ + public TagAdapter(SimpleTag adaptee) { + if (adaptee == null) { + // Cannot wrap a null adaptee. + throw new IllegalArgumentException(); + } + this.simpleTagAdaptee = adaptee; + } + + /** + * Must not be called. + * + * @param pc ignored. + * + * @throws UnsupportedOperationException Must not be called + */ + @Override + public void setPageContext(PageContext pc) { + throw new UnsupportedOperationException("Illegal to invoke setPageContext() on TagAdapter wrapper"); + } + + /** + * Must not be called. The parent of this tag is always getAdaptee().getParent(). + * + * @param parentTag ignored. + * + * @throws UnsupportedOperationException Must not be called. + */ + @Override + public void setParent(Tag parentTag) { + throw new UnsupportedOperationException("Illegal to invoke setParent() on TagAdapter wrapper"); + } + + /** + * Returns the parent of this tag, which is always getAdaptee().getParent(). This will either be the enclosing Tag + * (if getAdaptee().getParent() implements Tag), or an adapter to the enclosing Tag (if getAdaptee().getParent() + * does not implement Tag). + * + * @return The parent of the tag being adapted. + */ + @Override + public Tag getParent() { + if (!parentDetermined) { + JspTag adapteeParent = simpleTagAdaptee.getParent(); + if (adapteeParent != null) { + if (adapteeParent instanceof Tag) { + this.parent = (Tag) adapteeParent; + } else { + // Must be SimpleTag - no other types defined. + this.parent = new TagAdapter((SimpleTag) adapteeParent); + } + } + parentDetermined = true; + } + + return this.parent; + } + + /** + * Gets the tag that is being adapted to the Tag interface. This should be an instance of SimpleTag in JSP 2.0, but + * room is left for other kinds of tags in future spec versions. + * + * @return the tag that is being adapted + */ + public JspTag getAdaptee() { + return this.simpleTagAdaptee; + } + + /** + * Must not be called. + * + * @return always throws UnsupportedOperationException + * + * @throws UnsupportedOperationException Must not be called + * @throws JspException never thrown + */ + @Override + public int doStartTag() throws JspException { + throw new UnsupportedOperationException("Illegal to invoke doStartTag() on TagAdapter wrapper"); + } + + /** + * Must not be called. + * + * @return always throws UnsupportedOperationException + * + * @throws UnsupportedOperationException Must not be called + * @throws JspException never thrown + */ + @Override + public int doEndTag() throws JspException { + throw new UnsupportedOperationException("Illegal to invoke doEndTag() on TagAdapter wrapper"); + } + + /** + * Must not be called. + * + * @throws UnsupportedOperationException Must not be called + */ + @Override + public void release() { + throw new UnsupportedOperationException("Illegal to invoke release() on TagAdapter wrapper"); + } +} diff --git a/java/jakarta/servlet/jsp/tagext/TagAttributeInfo.java b/java/jakarta/servlet/jsp/tagext/TagAttributeInfo.java new file mode 100644 index 0000000..92e9df6 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagAttributeInfo.java @@ -0,0 +1,254 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Information on the attributes of a Tag, available at translation time. This class is instantiated from the Tag + * Library Descriptor file (TLD). + *

+ * Only the information needed to generate code is included here. Other information like SCHEMA for validation belongs + * elsewhere. + */ + +public class TagAttributeInfo { + + /** + * "id" is wired in to be ID. There is no real benefit in having it be something else IDREFs are not handled any + * differently. + */ + public static final String ID = "id"; + + /** + * Constructor for TagAttributeInfo. This class is to be instantiated only from the TagLibrary code under request + * from some JSP code that is parsing a TLD (Tag Library Descriptor). + * + * @param name The name of the attribute. + * @param required If this attribute is required in tag instances. + * @param type The name of the type of the attribute. + * @param reqTime Whether this attribute holds a request-time Attribute. + */ + public TagAttributeInfo(String name, boolean required, String type, boolean reqTime) { + this(name, required, type, reqTime, false); + } + + /** + * JSP 2.0 Constructor for TagAttributeInfo. This class is to be instantiated only from the TagLibrary code under + * request from some JSP code that is parsing a TLD (Tag Library Descriptor). + * + * @param name The name of the attribute. + * @param required If this attribute is required in tag instances. + * @param type The name of the type of the attribute. + * @param reqTime Whether this attribute holds a request-time Attribute. + * @param fragment Whether this attribute is of type JspFragment + * + * @since JSP 2.0 + */ + public TagAttributeInfo(String name, boolean required, String type, boolean reqTime, boolean fragment) { + this(name, required, type, reqTime, fragment, null, false, false, null, null); + } + + /** + * JSP 2.1 Constructor for TagAttributeInfo. This class is to be instantiated only from the TagLibrary code under + * request from some JSP code that is parsing a TLD (Tag Library Descriptor). + * + * @param name The name of the attribute. + * @param required If this attribute is required in tag instances. + * @param type The name of the type of the attribute. + * @param reqTime Whether this attribute holds a request-time Attribute. + * @param fragment Whether this attribute is of type JspFragment + * @param description Description of this attribute + * @param deferredValue Does this attribute accept value expressions (written as Strings) as attribute values the + * evaluation of which is deferred until calculated by the tag + * @param deferredMethod Does this attribute accept method expressions (written as Strings) as attribute values + * the evaluation of which is deferred until calculated by the tag + * @param expectedTypeName The expected type when the deferred value is evaluated + * @param methodSignature The expected method signature if a deferred method + * + * @since JSP 2.1 + */ + public TagAttributeInfo(String name, boolean required, String type, boolean reqTime, boolean fragment, + String description, boolean deferredValue, boolean deferredMethod, String expectedTypeName, + String methodSignature) { + this.name = name; + this.required = required; + this.type = type; + this.reqTime = reqTime; + this.fragment = fragment; + this.description = description; + this.deferredValue = deferredValue; + this.deferredMethod = deferredMethod; + this.expectedTypeName = expectedTypeName; + this.methodSignature = methodSignature; + } + + /** + * The name of this attribute. + * + * @return the name of the attribute + */ + public String getName() { + return name; + } + + /** + * The type (as a String) of this attribute. + * + * @return the type of the attribute + */ + public String getTypeName() { + return type; + } + + /** + * Whether this attribute can hold a request-time value. + * + * @return if the attribute can hold a request-time value. + */ + public boolean canBeRequestTime() { + return reqTime; + } + + /** + * Whether this attribute is required. + * + * @return if the attribute is required. + */ + public boolean isRequired() { + return required; + } + + /** + * Convenience static method that goes through an array of TagAttributeInfo objects and looks for "id". + * + * @param tagAttributeInfos An array of TagAttributeInfo + * + * @return The TagAttributeInfo reference with name "id" + */ + public static TagAttributeInfo getIdAttribute(TagAttributeInfo[] tagAttributeInfos) { + for (TagAttributeInfo tagAttributeInfo : tagAttributeInfos) { + if (tagAttributeInfo.getName().equals(ID)) { + return tagAttributeInfo; + } + } + return null; // no such attribute + } + + /** + * Whether this attribute is of type JspFragment. + * + * @return if the attribute is of type JspFragment + * + * @since JSP 2.0 + */ + public boolean isFragment() { + return fragment; + } + + /** + * Returns a String representation of this TagAttributeInfo, suitable for debugging purposes. + * + * @return a String representation of this TagAttributeInfo + */ + @Override + public String toString() { + StringBuilder b = new StringBuilder(64); + b.append("name = " + name + " "); + b.append("type = " + type + " "); + b.append("reqTime = " + reqTime + " "); + b.append("required = " + required + " "); + b.append("fragment = " + fragment + " "); + b.append("deferredValue = " + deferredValue + " "); + b.append("expectedTypeName = " + expectedTypeName + " "); + b.append("deferredMethod = " + deferredMethod + " "); + b.append("methodSignature = " + methodSignature); + return b.toString(); + } + + /* + * private fields + */ + private final String name; + + private final String type; + + private final boolean reqTime; + + private final boolean required; + + /* + * private fields for JSP 2.0 + */ + private final boolean fragment; + + /* + * private fields for JSP 2.1 + */ + private final String description; + + private final boolean deferredValue; + + private final boolean deferredMethod; + + private final String expectedTypeName; + + private final String methodSignature; + + /** + * Does the attribute expect to be passed a deferred method? + * + * @return {@code true} if a deferred method expression is expected, otherwise {@code false} + */ + public boolean isDeferredMethod() { + return deferredMethod; + } + + /** + * Does the attribute expect to be passed a deferred value? + * + * @return {@code true} if a deferred value expression is expected, otherwise {@code false} + */ + public boolean isDeferredValue() { + return deferredValue; + } + + /** + * Obtain the description for the attribute, + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Obtain the type name, as a string, expected by this attribute. + * + * @return the type name, as a string + */ + public String getExpectedTypeName() { + return expectedTypeName; + } + + /** + * If this is a deferred method attribute, obtain the expected method signature. + * + * @return The expected method signature or {@code null} if this attribute is not a deferred method attribute + */ + public String getMethodSignature() { + return methodSignature; + } +} diff --git a/java/jakarta/servlet/jsp/tagext/TagData.java b/java/jakarta/servlet/jsp/tagext/TagData.java new file mode 100644 index 0000000..30de13b --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagData.java @@ -0,0 +1,142 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import java.util.Hashtable; + +/** + * The (translation-time only) attribute/value information for a tag instance. + *

+ * TagData is only used as an argument to the isValid, validate, and getVariableInfo methods of TagExtraInfo, which are + * invoked at translation time. + */ + +public class TagData implements Cloneable { + + /** + * Distinguished value for an attribute to indicate its value is a request-time expression (which is not yet + * available because TagData instances are used at translation-time). + */ + + public static final Object REQUEST_TIME_VALUE = new Object(); + + + /** + * Constructor for TagData. + *

+ * A typical constructor may be + * + *

+     * static final Object[][] att = { { "connection", "conn0" }, { "id", "query0" } };
+     * static final TagData td = new TagData(att);
+     * 
+ * + * All values must be Strings except for those holding the distinguished object REQUEST_TIME_VALUE. + * + * @param atts the static attribute and values. May be null. + */ + public TagData(Object[] atts[]) { + if (atts == null) { + attributes = new Hashtable<>(); + } else { + attributes = new Hashtable<>(atts.length); + } + + if (atts != null) { + for (Object[] att : atts) { + attributes.put((String) att[0], att[1]); + } + } + } + + /** + * Constructor for a TagData. If you already have the attributes in a hashtable, use this constructor. + * + * @param attrs A hashtable to get the values from. + */ + public TagData(Hashtable attrs) { + this.attributes = attrs; + } + + /** + * The value of the tag's id attribute. + * + * @return the value of the tag's id attribute, or null if no such attribute was specified. + */ + + public String getId() { + return getAttributeString(TagAttributeInfo.ID); + } + + /** + * The value of the attribute. If a static value is specified for an attribute that accepts a request-time attribute + * expression then that static value is returned, even if the value is provided in the body of a + * <jsp:attribute> action. The distinguished object REQUEST_TIME_VALUE is only returned if the value is + * specified as a request-time attribute expression or via the <jsp:attribute> action with a body that + * contains dynamic content (scriptlets, scripting expressions, EL expressions, standard actions, or custom + * actions). Returns null if the attribute is not set. + * + * @param attName the name of the attribute + * + * @return the attribute's value + */ + + public Object getAttribute(String attName) { + return attributes.get(attName); + } + + /** + * Set the value of an attribute. + * + * @param attName the name of the attribute + * @param value the value. + */ + public void setAttribute(String attName, Object value) { + attributes.put(attName, value); + } + + /** + * Get the value for a given attribute. + * + * @param attName the name of the attribute + * + * @return the attribute value string + * + * @throws ClassCastException if attribute value is not a String + */ + + public String getAttributeString(String attName) { + Object o = attributes.get(attName); + if (o == null) { + return null; + } + return (String) o; + } + + /** + * Enumerates the attributes. + * + * @return An enumeration of the attributes in a TagData + */ + public java.util.Enumeration getAttributes() { + return attributes.keys(); + } + + // private data + + private final Hashtable attributes; // the tagname/value map +} diff --git a/java/jakarta/servlet/jsp/tagext/TagExtraInfo.java b/java/jakarta/servlet/jsp/tagext/TagExtraInfo.java new file mode 100644 index 0000000..7a6b60a --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagExtraInfo.java @@ -0,0 +1,131 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Optional class provided by the tag library author to describe additional translation-time information not described + * in the TLD. The TagExtraInfo class is mentioned in the Tag Library Descriptor file (TLD). + *

+ * This class can be used: + *

    + *
  • to indicate that the tag defines scripting variables + *
  • to perform translation-time validation of the tag attributes. + *
+ *

+ * It is the responsibility of the JSP translator that the initial value to be returned by calls to getTagInfo() + * corresponds to a TagInfo object for the tag being translated. If an explicit call to setTagInfo() is done, then the + * object passed will be returned in subsequent calls to getTagInfo(). + *

+ * The only way to affect the value returned by getTagInfo() is through a setTagInfo() call, and thus, + * TagExtraInfo.setTagInfo() is to be called by the JSP translator, with a TagInfo object that corresponds to the tag + * being translated. The call should happen before any invocation on validate() and before any invocation on + * getVariableInfo(). + *

+ * NOTE: It is a (translation time) error for a tag definition in a TLD with one or more variable subelements to + * have an associated TagExtraInfo implementation that returns a VariableInfo array with one or more elements from a + * call to getVariableInfo(). + */ + +public abstract class TagExtraInfo { + + /** + * Sole constructor. (For invocation by subclass constructors, typically implicit.) + */ + public TagExtraInfo() { + // NOOP by default + } + + /** + * information on scripting variables defined by the tag associated with this TagExtraInfo instance. Request-time + * attributes are indicated as such in the TagData parameter. + * + * @param data The TagData instance. + * + * @return An array of VariableInfo data, or null or a zero length array if no scripting variables are to be + * defined. + */ + public VariableInfo[] getVariableInfo(TagData data) { + return ZERO_VARIABLE_INFO; + } + + /** + * Translation-time validation of the attributes. Request-time attributes are indicated as such in the TagData + * parameter. Note that the preferred way to do validation is with the validate() method, since it can return more + * detailed information. + * + * @param data The TagData instance. + * + * @return Whether this tag instance is valid. + * + * @see TagExtraInfo#validate + */ + + public boolean isValid(TagData data) { + return true; + } + + /** + * Translation-time validation of the attributes. Request-time attributes are indicated as such in the TagData + * parameter. Because of the higher quality validation messages possible, this is the preferred way to do validation + * (although isValid() still works). + *

+ * JSP 2.0 and higher containers call validate() instead of isValid(). The default implementation of this method is + * to call isValid(). If isValid() returns false, a generic ValidationMessage[] is returned indicating isValid() + * returned false. + *

+ * + * @param data The TagData instance. + * + * @return A null object, or zero length array if no errors, an array of ValidationMessages otherwise. + * + * @since JSP 2.0 + */ + public ValidationMessage[] validate(TagData data) { + ValidationMessage[] result = null; + + if (!isValid(data)) { + result = new ValidationMessage[] { new ValidationMessage(data.getId(), "isValid() == false") }; + } + + return result; + } + + /** + * Set the TagInfo for this class. + * + * @param tagInfo The TagInfo this instance is extending + */ + public final void setTagInfo(TagInfo tagInfo) { + this.tagInfo = tagInfo; + } + + /** + * Get the TagInfo for this class. + * + * @return the taginfo instance this instance is extending + */ + public final TagInfo getTagInfo() { + return tagInfo; + } + + // private data + private TagInfo tagInfo; + + // zero length VariableInfo array + private static final VariableInfo[] ZERO_VARIABLE_INFO = {}; +} + diff --git a/java/jakarta/servlet/jsp/tagext/TagFileInfo.java b/java/jakarta/servlet/jsp/tagext/TagFileInfo.java new file mode 100644 index 0000000..bb08483 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagFileInfo.java @@ -0,0 +1,75 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Tag information for a tag file in a Tag Library; This class is instantiated from the Tag Library Descriptor file + * (TLD) and is available only at translation time. + * + * @since JSP 2.0 + */ +public class TagFileInfo { + + /** + * Constructor for TagFileInfo from data in the JSP 2.0 format for TLD. This class is to be instantiated only from + * the TagLibrary code under request from some JSP code that is parsing a TLD (Tag Library Descriptor). Note that, + * since TagLibraryInfo reflects both TLD information and taglib directive information, a TagFileInfo instance is + * dependent on a taglib directive. This is probably a design error, which may be fixed in the future. + * + * @param name The unique action name of this tag + * @param path Where to find the .tag file implementing this action, relative to the location of the TLD file. + * @param tagInfo The detailed information about this tag, as parsed from the directives in the tag file. + */ + public TagFileInfo(String name, String path, TagInfo tagInfo) { + this.name = name; + this.path = path; + this.tagInfo = tagInfo; + } + + /** + * The unique action name of this tag. + * + * @return The (short) name of the tag. + */ + public String getName() { + return name; + } + + /** + * Where to find the .tag file implementing this action. + * + * @return The path of the tag file, relative to the TLD, or "." if the tag file was defined in an implicit tag + * file. + */ + public String getPath() { + return path; + } + + /** + * Returns information about this tag, parsed from the directives in the tag file. + * + * @return a TagInfo object containing information about this tag + */ + public TagInfo getTagInfo() { + return tagInfo; + } + + // private fields for 2.0 info + private final String name; + private final String path; + private final TagInfo tagInfo; +} diff --git a/java/jakarta/servlet/jsp/tagext/TagInfo.java b/java/jakarta/servlet/jsp/tagext/TagInfo.java new file mode 100644 index 0000000..40e978d --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagInfo.java @@ -0,0 +1,396 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Tag information for a tag in a Tag Library; This class is instantiated from the Tag Library Descriptor file (TLD) and + * is available only at translation time. + */ + +public class TagInfo { + + /** + * Static constant for getBodyContent() when it is JSP. + */ + public static final String BODY_CONTENT_JSP = "JSP"; + + /** + * Static constant for getBodyContent() when it is Tag dependent. + */ + public static final String BODY_CONTENT_TAG_DEPENDENT = "tagdependent"; + + + /** + * Static constant for getBodyContent() when it is empty. + */ + public static final String BODY_CONTENT_EMPTY = "empty"; + + /** + * Static constant for getBodyContent() when it is scriptless. + * + * @since JSP 2.0 + */ + public static final String BODY_CONTENT_SCRIPTLESS = "scriptless"; + + /** + * Constructor for TagInfo from data in the JSP 1.1 format for TLD. This class is to be instantiated only from the + * TagLibrary code under request from some JSP code that is parsing a TLD (Tag Library Descriptor). Note that, since + * TagLibraryInfo reflects both TLD information and taglib directive information, a TagInfo instance is dependent on + * a taglib directive. This is probably a design error, which may be fixed in the future. + * + * @param tagName The name of this tag + * @param tagClassName The name of the tag handler class + * @param bodycontent Information on the body content of these tags + * @param infoString The (optional) string information for this tag + * @param taglib The instance of the tag library that contains us. + * @param tagExtraInfo The instance providing extra Tag info. May be null + * @param attributeInfo An array of AttributeInfo data from descriptor. May be null; + */ + public TagInfo(String tagName, String tagClassName, String bodycontent, String infoString, TagLibraryInfo taglib, + TagExtraInfo tagExtraInfo, TagAttributeInfo[] attributeInfo) { + this.tagName = tagName; + this.tagClassName = tagClassName; + this.bodyContent = bodycontent; + this.infoString = infoString; + this.tagLibrary = taglib; + this.tagExtraInfo = tagExtraInfo; + this.attributeInfo = attributeInfo; + + // Use defaults for unspecified values + this.displayName = null; + this.largeIcon = null; + this.smallIcon = null; + this.tagVariableInfo = null; + this.dynamicAttributes = false; + + if (tagExtraInfo != null) { + tagExtraInfo.setTagInfo(this); + } + } + + /** + * Constructor for TagInfo from data in the JSP 1.2 format for TLD. This class is to be instantiated only from the + * TagLibrary code under request from some JSP code that is parsing a TLD (Tag Library Descriptor). Note that, since + * TagLibraryInfo reflects both TLD information and taglib directive information, a TagInfo instance is dependent on + * a taglib directive. This is probably a design error, which may be fixed in the future. + * + * @param tagName The name of this tag + * @param tagClassName The name of the tag handler class + * @param bodycontent Information on the body content of these tags + * @param infoString The (optional) string information for this tag + * @param taglib The instance of the tag library that contains us. + * @param tagExtraInfo The instance providing extra Tag info. May be null + * @param attributeInfo An array of AttributeInfo data from descriptor. May be null; + * @param displayName A short name to be displayed by tools + * @param smallIcon Path to a small icon to be displayed by tools + * @param largeIcon Path to a large icon to be displayed by tools + * @param tvi An array of a TagVariableInfo (or null) + */ + public TagInfo(String tagName, String tagClassName, String bodycontent, String infoString, TagLibraryInfo taglib, + TagExtraInfo tagExtraInfo, TagAttributeInfo[] attributeInfo, String displayName, String smallIcon, + String largeIcon, TagVariableInfo[] tvi) { + this.tagName = tagName; + this.tagClassName = tagClassName; + this.bodyContent = bodycontent; + this.infoString = infoString; + this.tagLibrary = taglib; + this.tagExtraInfo = tagExtraInfo; + this.attributeInfo = attributeInfo; + this.displayName = displayName; + this.smallIcon = smallIcon; + this.largeIcon = largeIcon; + this.tagVariableInfo = tvi; + + // Use defaults for unspecified values + this.dynamicAttributes = false; + + if (tagExtraInfo != null) { + tagExtraInfo.setTagInfo(this); + } + } + + /** + * Constructor for TagInfo from data in the JSP 2.0 format for TLD. This class is to be instantiated only from the + * TagLibrary code under request from some JSP code that is parsing a TLD (Tag Library Descriptor). Note that, since + * TagLibraryInfo reflects both TLD information and taglib directive information, a TagInfo instance is dependent on + * a taglib directive. This is probably a design error, which may be fixed in the future. + * + * @param tagName The name of this tag + * @param tagClassName The name of the tag handler class + * @param bodycontent Information on the body content of these tags + * @param infoString The (optional) string information for this tag + * @param taglib The instance of the tag library that contains us. + * @param tagExtraInfo The instance providing extra Tag info. May be null + * @param attributeInfo An array of AttributeInfo data from descriptor. May be null; + * @param displayName A short name to be displayed by tools + * @param smallIcon Path to a small icon to be displayed by tools + * @param largeIcon Path to a large icon to be displayed by tools + * @param tvi An array of a TagVariableInfo (or null) + * @param dynamicAttributes True if supports dynamic attributes + * + * @since JSP 2.0 + */ + public TagInfo(String tagName, String tagClassName, String bodycontent, String infoString, TagLibraryInfo taglib, + TagExtraInfo tagExtraInfo, TagAttributeInfo[] attributeInfo, String displayName, String smallIcon, + String largeIcon, TagVariableInfo[] tvi, boolean dynamicAttributes) { + this.tagName = tagName; + this.tagClassName = tagClassName; + this.bodyContent = bodycontent; + this.infoString = infoString; + this.tagLibrary = taglib; + this.tagExtraInfo = tagExtraInfo; + this.attributeInfo = attributeInfo; + this.displayName = displayName; + this.smallIcon = smallIcon; + this.largeIcon = largeIcon; + this.tagVariableInfo = tvi; + this.dynamicAttributes = dynamicAttributes; + + if (tagExtraInfo != null) { + tagExtraInfo.setTagInfo(this); + } + } + + /** + * The name of the Tag. + * + * @return The (short) name of the tag. + */ + public String getTagName() { + return tagName; + } + + /** + * Attribute information (in the TLD) on this tag. The return is an array describing the attributes of this tag, as + * indicated in the TLD. + * + * @return The array of TagAttributeInfo for this tag, or a zero-length array if the tag has no attributes. + */ + public TagAttributeInfo[] getAttributes() { + return attributeInfo; + } + + /** + * Information on the scripting objects created by this tag at runtime. This is a convenience method on the + * associated TagExtraInfo class. + * + * @param data TagData describing this action. + * + * @return if a TagExtraInfo object is associated with this TagInfo, the result of + * getTagExtraInfo().getVariableInfo( data ), otherwise null. + */ + public VariableInfo[] getVariableInfo(TagData data) { + VariableInfo[] result = null; + TagExtraInfo tei = getTagExtraInfo(); + if (tei != null) { + result = tei.getVariableInfo(data); + } + return result; + } + + /** + * Translation-time validation of the attributes. This is a convenience method on the associated TagExtraInfo class. + * + * @param data The translation-time TagData instance. + * + * @return Whether the data is valid. + */ + public boolean isValid(TagData data) { + TagExtraInfo tei = getTagExtraInfo(); + if (tei == null) { + return true; + } + return tei.isValid(data); + } + + /** + * Translation-time validation of the attributes. This is a convenience method on the associated TagExtraInfo class. + * + * @param data The translation-time TagData instance. + * + * @return A null object, or zero length array if no errors, an array of ValidationMessages otherwise. + * + * @since JSP 2.0 + */ + public ValidationMessage[] validate(TagData data) { + TagExtraInfo tei = getTagExtraInfo(); + if (tei == null) { + return null; + } + return tei.validate(data); + } + + /** + * Set the instance for extra tag information. + * + * @param tei the TagExtraInfo instance + */ + public void setTagExtraInfo(TagExtraInfo tei) { + tagExtraInfo = tei; + } + + + /** + * The instance (if any) for extra tag information. + * + * @return The TagExtraInfo instance, if any. + */ + public TagExtraInfo getTagExtraInfo() { + return tagExtraInfo; + } + + + /** + * Name of the class that provides the handler for this tag. + * + * @return The name of the tag handler class. + */ + + public String getTagClassName() { + return tagClassName; + } + + + /** + * The bodycontent information for this tag. If the bodycontent is not defined for this tag, the default of JSP will + * be returned. + * + * @return the body content string. + */ + + public String getBodyContent() { + return bodyContent; + } + + + /** + * The information string for the tag. + * + * @return the info string, or null if not defined + */ + + public String getInfoString() { + return infoString; + } + + + /** + * Set the TagLibraryInfo property. Note that a TagLibraryInfo element is dependent not just on the TLD information + * but also on the specific taglib instance used. This means that a fair amount of work needs to be done to + * construct and initialize TagLib objects. If used carefully, this setter can be used to avoid having to create new + * TagInfo elements for each taglib directive. + * + * @param tl the TagLibraryInfo to assign + */ + + public void setTagLibrary(TagLibraryInfo tl) { + tagLibrary = tl; + } + + /** + * The instance of TabLibraryInfo we belong to. + * + * @return the tag library instance we belong to + */ + + public TagLibraryInfo getTagLibrary() { + return tagLibrary; + } + + + // ============== JSP 2.0 TLD Information ======== + + + /** + * Get the displayName. + * + * @return A short name to be displayed by tools, or null if not defined + */ + + public String getDisplayName() { + return displayName; + } + + /** + * Get the path to the small icon. + * + * @return Path to a small icon to be displayed by tools, or null if not defined + */ + + public String getSmallIcon() { + return smallIcon; + } + + /** + * Get the path to the large icon. + * + * @return Path to a large icon to be displayed by tools, or null if not defined + */ + + public String getLargeIcon() { + return largeIcon; + } + + /** + * Get TagVariableInfo objects associated with this TagInfo. + * + * @return Array of TagVariableInfo objects corresponding to variables declared by this tag, or a zero length array + * if no variables have been declared + */ + + public TagVariableInfo[] getTagVariableInfos() { + return tagVariableInfo; + } + + + // ============== JSP 2.0 TLD Information ======== + + /** + * Get dynamicAttributes associated with this TagInfo. + * + * @return True if tag handler supports dynamic attributes + * + * @since JSP 2.0 + */ + public boolean hasDynamicAttributes() { + return dynamicAttributes; + } + + /* + * private fields for 1.1 info + */ + private final String tagName; // the name of the tag + private final String tagClassName; + private final String bodyContent; + private final String infoString; + private TagLibraryInfo tagLibrary; + private TagExtraInfo tagExtraInfo; // instance of TagExtraInfo + private final TagAttributeInfo[] attributeInfo; + + /* + * private fields for 1.2 info + */ + private final String displayName; + private final String smallIcon; + private final String largeIcon; + private final TagVariableInfo[] tagVariableInfo; + + /* + * Additional private fields for 2.0 info + */ + private final boolean dynamicAttributes; +} diff --git a/java/jakarta/servlet/jsp/tagext/TagLibraryInfo.java b/java/jakarta/servlet/jsp/tagext/TagLibraryInfo.java new file mode 100644 index 0000000..a4f799f --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagLibraryInfo.java @@ -0,0 +1,273 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + + +/** + * Translation-time information associated with a taglib directive, and its underlying TLD file. Most of the information + * is directly from the TLD, except for the prefix and the uri values used in the taglib directive + */ +public abstract class TagLibraryInfo { + + /** + * Constructor. This will invoke the constructors for TagInfo, and TagAttributeInfo after parsing the TLD file. + * + * @param prefix the prefix actually used by the taglib directive + * @param uri the URI actually used by the taglib directive + */ + protected TagLibraryInfo(String prefix, String uri) { + this.prefix = prefix; + this.uri = uri; + } + + // ==== methods accessing taglib information ======= + + /** + * The value of the uri attribute from the taglib directive for this library. + * + * @return the value of the uri attribute + */ + public String getURI() { + return uri; + } + + /** + * The prefix assigned to this taglib from the taglib directive + * + * @return the prefix assigned to this taglib from the taglib directive + */ + public String getPrefixString() { + return prefix; + } + + // ==== methods using the TLD data ======= + + /** + * The preferred short name (prefix) as indicated in the TLD. This may be used by authoring tools as the preferred + * prefix to use when creating an taglib directive for this library. + * + * @return the preferred short name for the library + */ + public String getShortName() { + return shortname; + } + + /** + * The "reliable" URN indicated in the TLD (the uri element). This may be used by authoring tools as a global + * identifier to use when creating a taglib directive for this library. + * + * @return a reliable URN to a TLD like this + */ + public String getReliableURN() { + return urn; + } + + /** + * Information (documentation) for this TLD. + * + * @return the info string for this tag lib + */ + public String getInfoString() { + return info; + } + + /** + * A string describing the required version of the JSP container. + * + * @return the (minimal) required version of the JSP container. + * + * @see jakarta.servlet.jsp.JspEngineInfo + */ + public String getRequiredVersion() { + return jspversion; + } + + /** + * An array describing the tags that are defined in this tag library. + * + * @return the TagInfo objects corresponding to the tags defined by this tag library, or a zero length array if this + * tag library defines no tags + */ + public TagInfo[] getTags() { + return tags; + } + + /** + * An array describing the tag files that are defined in this tag library. + * + * @return the TagFileInfo objects corresponding to the tag files defined by this tag library, or a zero length + * array if this tag library defines no tags files + * + * @since JSP 2.0 + */ + public TagFileInfo[] getTagFiles() { + return tagFiles; + } + + /** + * Get the TagInfo for a given tag name, looking through all the tags in this tag library. + * + * @param shortname The short name (no prefix) of the tag + * + * @return the TagInfo for the tag with the specified short name, or null if no such tag is found + */ + public TagInfo getTag(String shortname) { + TagInfo tags[] = getTags(); + + if (tags == null || tags.length == 0 || shortname == null) { + return null; + } + + for (TagInfo tag : tags) { + if (shortname.equals(tag.getTagName())) { + return tag; + } + } + return null; + } + + /** + * Get the TagFileInfo for a given tag name, looking through all the tag files in this tag library. + * + * @param shortname The short name (no prefix) of the tag + * + * @return the TagFileInfo for the specified Tag file, or null if no Tag file is found + * + * @since JSP 2.0 + */ + public TagFileInfo getTagFile(String shortname) { + TagFileInfo tagFiles[] = getTagFiles(); + + if (tagFiles == null || tagFiles.length == 0) { + return null; + } + + for (TagFileInfo tagFile : tagFiles) { + if (tagFile.getName().equals(shortname)) { + return tagFile; + } + } + return null; + } + + /** + * An array describing the functions that are defined in this tag library. + * + * @return the functions defined in this tag library, or a zero length array if the tag library defines no + * functions. + * + * @since JSP 2.0 + */ + public FunctionInfo[] getFunctions() { + return functions; + } + + /** + * Get the FunctionInfo for a given function name, looking through all the functions in this tag library. + * + * @param name The name (no prefix) of the function + * + * @return the FunctionInfo for the function with the given name, or null if no such function exists + * + * @since JSP 2.0 + */ + public FunctionInfo getFunction(String name) { + + if (functions == null || functions.length == 0) { + return null; + } + + for (FunctionInfo function : functions) { + if (function.getName().equals(name)) { + return function; + } + } + return null; + } + + /** + * Returns an array of TagLibraryInfo objects representing the entire set of tag libraries (including this + * TagLibraryInfo) imported by taglib directives in the translation unit that references this TagLibraryInfo. If a + * tag library is imported more than once and bound to different prefixes, only the TagLibraryInfo bound to the + * first prefix must be included in the returned array. + * + * @return Array of TagLibraryInfo objects representing the entire set of tag libraries (including this + * TagLibraryInfo) imported by taglib directives in the translation unit that references this + * TagLibraryInfo. + * + * @since JSP 2.1 + */ + public abstract TagLibraryInfo[] getTagLibraryInfos(); + + // Protected fields + + /** + * The prefix assigned to this taglib from the taglib directive. + */ + protected String prefix; + + /** + * The value of the uri attribute from the taglib directive for this library. + */ + protected String uri; + + /** + * An array describing the tags that are defined in this tag library. + */ + protected TagInfo[] tags; + + /** + * An array describing the tag files that are defined in this tag library. + * + * @since JSP 2.0 + */ + protected TagFileInfo[] tagFiles; + + /** + * An array describing the functions that are defined in this tag library. + * + * @since JSP 2.0 + */ + protected FunctionInfo[] functions; + + // Tag Library Data + + /** + * The version of the tag library. + */ + protected String tlibversion; // required + + /** + * The version of the JSP specification this tag library is written to. + */ + protected String jspversion; // required + + /** + * The preferred short name (prefix) as indicated in the TLD. + */ + protected String shortname; // required + + /** + * The "reliable" URN indicated in the TLD. + */ + protected String urn; // required + + /** + * Information (documentation) for this TLD. + */ + protected String info; // optional +} diff --git a/java/jakarta/servlet/jsp/tagext/TagLibraryValidator.java b/java/jakarta/servlet/jsp/tagext/TagLibraryValidator.java new file mode 100644 index 0000000..478d9ee --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagLibraryValidator.java @@ -0,0 +1,112 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Translation-time validator class for a JSP page. A validator operates on the XML view associated with the JSP page. + *

+ * The TLD file associates a TagLibraryValidator class and some init arguments with a tag library. + *

+ * The JSP container is responsible for locating an appropriate instance of the appropriate subclass by + *

    + *
  • new a fresh instance, or reuse an available one + *
  • invoke the setInitParams(Map) method on the instance + *
+ * once initialized, the validate(String, String, PageData) method will be invoked, where the first two arguments are + * the prefix and uri for this tag library in the XML View. The prefix is intended to make it easier to produce an error + * message. However, it is not always accurate. In the case where a single URI is mapped to more than one prefix in the + * XML view, the prefix of the first URI is provided. Therefore, to provide high quality error messages in cases where + * the tag elements themselves are checked, the prefix parameter should be ignored and the actual prefix of the element + * should be used instead. TagLibraryValidators should always use the uri to identify elements as belonging to the tag + * library, not the prefix. + *

+ * A TagLibraryValidator instance may create auxiliary objects internally to perform the validation (e.g. an XSchema + * validator) and may reuse it for all the pages in a given translation run. + *

+ * The JSP container is not guaranteed to serialize invocations of validate() method, and TagLibraryValidators should + * perform any synchronization they may require. + *

+ * As of JSP 2.0, a JSP container must provide a jsp:id attribute to provide higher quality validation errors. The + * container will track the JSP pages as passed to the container, and will assign to each element a unique "id", which + * is passed as the value of the jsp:id attribute. Each XML element in the XML view available will be extended with this + * attribute. The TagLibraryValidator can then use the attribute in one or more ValidationMessage objects. The container + * then, in turn, can use these values to provide more precise information on the location of an error. + *

+ * The actual prefix of the id attribute may or may not be jsp but it will always map to the + * namespace http://java.sun.com/JSP/Page. A TagLibraryValidator implementation must rely on the uri, not + * the prefix, of the id attribute. + */ + +public abstract class TagLibraryValidator { + + /** + * Sole constructor. (For invocation by subclass constructors, typically implicit.) + */ + public TagLibraryValidator() { + // NOOP by default + } + + /** + * Set the init data in the TLD for this validator. Parameter names are keys, and parameter values are the values. + * + * @param map A Map describing the init parameters + */ + public void setInitParameters(Map map) { + initParameters = Collections.unmodifiableMap(new HashMap<>(map)); + } + + + /** + * Get the init parameters data as an immutable Map. Parameter names are keys, and parameter values are the values. + * + * @return The init parameters as an immutable map. + */ + public Map getInitParameters() { + return initParameters; + } + + /** + * Validate a JSP page. This will get invoked once per unique tag library URI in the XML view. This method will + * return null if the page is valid; otherwise the method should return an array of ValidationMessage objects. An + * array of length zero is also interpreted as no errors. + * + * @param prefix the first prefix with which the tag library is associated, in the XML view. Note that some tags may + * use a different prefix if the namespace is redefined. + * @param uri the tag library's unique identifier + * @param page the JspData page object + * + * @return A null object, or zero length array if no errors, an array of ValidationMessages otherwise. + */ + public ValidationMessage[] validate(String prefix, String uri, PageData page) { + return null; + } + + /** + * Release any data kept by this instance for validation purposes. + */ + public void release() { + initParameters = null; + } + + // Private data + private Map initParameters; + +} diff --git a/java/jakarta/servlet/jsp/tagext/TagSupport.java b/java/jakarta/servlet/jsp/tagext/TagSupport.java new file mode 100644 index 0000000..7fda1d0 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagSupport.java @@ -0,0 +1,278 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; + +/** + * A base class for defining new tag handlers implementing Tag. + *

+ * The TagSupport class is a utility class intended to be used as the base class for new tag handlers. The TagSupport + * class implements the Tag and IterationTag interfaces and adds additional convenience methods including getter methods + * for the properties in Tag. TagSupport has one static method that is included to facilitate coordination among + * cooperating tags. + *

+ * Many tag handlers will extend TagSupport and only redefine a few methods. + */ +public class TagSupport implements IterationTag, Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Find the instance of a given class type that is closest to a given instance. This method uses the getParent + * method from the Tag interface. This method is used for coordination among cooperating tags. + *

+ * The current version of the specification only provides one formal way of indicating the observable type of a tag + * handler: its tag handler implementation class, described in the tag-class subelement of the tag element. This is + * extended in an informal manner by allowing the tag library author to indicate in the description subelement an + * observable type. The type should be a subtype of the tag handler implementation class or void. This additional + * constraint can be exploited by a specialized container that knows about that specific tag library, as in the case + * of the JSP standard tag library. + *

+ * When a tag library author provides information on the observable type of a tag handler, client programmatic code + * should adhere to that constraint. Specifically, the Class passed to findAncestorWithClass should be a subtype of + * the observable type. + * + * @param from The instance from where to start looking. + * @param klass The subclass of Tag or interface to be matched + * + * @return the nearest ancestor that implements the interface or is an instance of the class specified + */ + public static final Tag findAncestorWithClass(Tag from, Class klass) { + boolean isInterface = false; + + if (from == null || klass == null || + (!Tag.class.isAssignableFrom(klass) && !(isInterface = klass.isInterface()))) { + return null; + } + + for (;;) { + Tag tag = from.getParent(); + + if (tag == null) { + return null; + } + + if ((isInterface && klass.isInstance(tag)) || ((Class) klass).isAssignableFrom(tag.getClass())) { + return tag; + } + from = tag; + } + } + + /** + * Default constructor, all subclasses are required to define only a public constructor with the same signature, and + * to call the superclass constructor. This constructor is called by the code generated by the JSP translator. + */ + public TagSupport() { + // NOOP by default + } + + /** + * Default processing of the start tag, returning SKIP_BODY. + * + * @return SKIP_BODY + * + * @throws JspException if an error occurs while processing this tag + * + * @see Tag#doStartTag() + */ + @Override + public int doStartTag() throws JspException { + return SKIP_BODY; + } + + /** + * Default processing of the end tag returning EVAL_PAGE. + * + * @return EVAL_PAGE + * + * @throws JspException if an error occurs while processing this tag + * + * @see Tag#doEndTag() + */ + @Override + public int doEndTag() throws JspException { + return EVAL_PAGE; + } + + + /** + * Default processing for a body. + * + * @return SKIP_BODY + * + * @throws JspException if an error occurs while processing this tag + * + * @see IterationTag#doAfterBody() + */ + @Override + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + // Actions related to body evaluation + + + /** + * Release state. + * + * @see Tag#release() + */ + @Override + public void release() { + parent = null; + id = null; + if (values != null) { + values.clear(); + } + values = null; + } + + /** + * Set the nesting tag of this tag. + * + * @param t The parent Tag. + * + * @see Tag#setParent(Tag) + */ + @Override + public void setParent(Tag t) { + parent = t; + } + + /** + * The Tag instance most closely enclosing this tag instance. + * + * @see Tag#getParent() + * + * @return the parent tag instance or null + */ + @Override + public Tag getParent() { + return parent; + } + + /** + * Set the id attribute for this tag. + * + * @param id The String for the id. + */ + public void setId(String id) { + this.id = id; + } + + /** + * The value of the id attribute of this tag; or null. + * + * @return the value of the id attribute, or null + */ + public String getId() { + return id; + } + + /** + * Set the page context. + * + * @param pageContext The PageContext. + * + * @see Tag#setPageContext + */ + @Override + public void setPageContext(PageContext pageContext) { + this.pageContext = pageContext; + } + + /** + * Associate a value with a String key. + * + * @param k The key String. + * @param o The value to associate. + */ + public void setValue(String k, Object o) { + if (values == null) { + values = new ConcurrentHashMap<>(); + } + values.put(k, o); + } + + /** + * Get a the value associated with a key. + * + * @param k The string key. + * + * @return The value associated with the key, or null. + */ + public Object getValue(String k) { + if (values == null) { + return null; + } + return values.get(k); + } + + /** + * Remove a value associated with a key. + * + * @param k The string key. + */ + public void removeValue(String k) { + if (values != null) { + values.remove(k); + } + } + + /** + * Enumerate the keys for the values kept by this tag handler. + * + * @return An enumeration of all the keys for the values set, or null or an empty Enumeration if no values have been + * set. + */ + public Enumeration getValues() { + if (values == null) { + return null; + } + return Collections.enumeration(values.keySet()); + } + + /** + * The parent, if any, of this tag. + */ + private Tag parent; + + /** + * Map of object values keyed by Strings for this tag. + */ + private Map values; + + /** + * The value of the id attribute of this tag; or null. + */ + protected String id; + + /** + * The PageContext. + */ + protected transient PageContext pageContext; +} + diff --git a/java/jakarta/servlet/jsp/tagext/TagVariableInfo.java b/java/jakarta/servlet/jsp/tagext/TagVariableInfo.java new file mode 100644 index 0000000..c4929b4 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TagVariableInfo.java @@ -0,0 +1,97 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Variable information for a tag in a Tag Library; This class is instantiated from the Tag Library Descriptor file + * (TLD) and is available only at translation time. This object should be immutable. This information is only available + * in JSP 1.2 format TLDs or above. + */ +public class TagVariableInfo { + + /** + * Constructor for TagVariableInfo. + * + * @param nameGiven value of <name-given> + * @param nameFromAttribute value of <name-from-attribute> + * @param className value of <variable-class> + * @param declare value of <declare> + * @param scope value of <scope> + */ + public TagVariableInfo(String nameGiven, String nameFromAttribute, String className, boolean declare, int scope) { + this.nameGiven = nameGiven; + this.nameFromAttribute = nameFromAttribute; + this.className = className; + this.declare = declare; + this.scope = scope; + } + + /** + * The body of the <name-given> element. + * + * @return The variable name as a constant + */ + public String getNameGiven() { + return nameGiven; + } + + /** + * The body of the <name-from-attribute> element. This is the name of an attribute whose (translation-time) + * value will give the name of the variable. One of <name-given> or <name-from-attribute> is required. + * + * @return The attribute whose value defines the variable name + */ + public String getNameFromAttribute() { + return nameFromAttribute; + } + + /** + * The body of the <variable-class> element. + * + * @return The name of the class of the variable or {@link String} if not defined in the TLD. + */ + public String getClassName() { + return className; + } + + /** + * The body of the <declare> element. + * + * @return Whether the variable is to be declared or not. If not defined in the TLD, 'true' will be returned. + */ + public boolean getDeclare() { + return declare; + } + + /** + * The body of the <scope> element. + * + * @return The scope to give the variable. NESTED scope will be returned if not defined in the TLD. + */ + public int getScope() { + return scope; + } + + /* + * private fields + */ + private final String nameGiven; // + private final String nameFromAttribute; // + private final String className; // + private final boolean declare; // + private final int scope; // +} diff --git a/java/jakarta/servlet/jsp/tagext/TryCatchFinally.java b/java/jakarta/servlet/jsp/tagext/TryCatchFinally.java new file mode 100644 index 0000000..94a1324 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/TryCatchFinally.java @@ -0,0 +1,86 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + + +/** + * The auxiliary interface of a Tag, IterationTag or BodyTag tag handler that wants additional hooks for managing + * resources. + *

+ * This interface provides two new methods: doCatch(Throwable) and doFinally(). The prototypical invocation is as + * follows: + * + *

+ * h = get a Tag();  // get a tag handler, perhaps from pool
+ *
+ * h.setPageContext(pc);  // initialize as desired
+ * h.setParent(null);
+ * h.setFoo("foo");
+ *
+ * // tag invocation protocol; see Tag.java
+ * try {
+ *   doStartTag()...
+ *   ....
+ *   doEndTag()...
+ * } catch (Throwable t) {
+ *   // react to exceptional condition
+ *   h.doCatch(t);
+ * } finally {
+ *   // restore data invariants and release per-invocation resources
+ *   h.doFinally();
+ * }
+ *
+ * ... other invocations perhaps with some new setters
+ * ...
+ * h.release();  // release long-term resources
+ * 
+ */ + +public interface TryCatchFinally { + + /** + * Invoked if a Throwable occurs while evaluating the BODY inside a tag or in any of the following methods: + * Tag.doStartTag(), Tag.doEndTag(), IterationTag.doAfterBody() and BodyTag.doInitBody(). + *

+ * This method is not invoked if the Throwable occurs during one of the setter methods. + *

+ * This method may throw an exception (the same or a new one) that will be propagated further up the nest chain. If + * an exception is thrown, doFinally() will be invoked. + *

+ * This method is intended to be used to respond to an exceptional condition. + * + * @param t The throwable exception navigating through this tag. + * + * @throws Throwable if the exception is to be rethrown further up the nest chain. + */ + + void doCatch(Throwable t) throws Throwable; + + /** + * Invoked in all cases after doEndTag() for any class implementing Tag, IterationTag or BodyTag. This method is + * invoked even if an exception has occurred in the BODY of the tag, or in any of the following methods: + * Tag.doStartTag(), Tag.doEndTag(), IterationTag.doAfterBody() and BodyTag.doInitBody(). + *

+ * This method is not invoked if the Throwable occurs during one of the setter methods. + *

+ * This method should not throw an Exception. + *

+ * This method is intended to maintain per-invocation data integrity and resource management actions. + */ + + void doFinally(); +} diff --git a/java/jakarta/servlet/jsp/tagext/ValidationMessage.java b/java/jakarta/servlet/jsp/tagext/ValidationMessage.java new file mode 100644 index 0000000..c231ec0 --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/ValidationMessage.java @@ -0,0 +1,68 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * A validation message from either TagLibraryValidator or TagExtraInfo. + *

+ * As of JSP 2.0, a JSP container must support a jsp:id attribute to provide higher quality validation errors. The + * container will track the JSP pages as passed to the container, and will assign to each element a unique "id", which + * is passed as the value of the jsp:id attribute. Each XML element in the XML view available will be extended with this + * attribute. The TagLibraryValidator can then use the attribute in one or more ValidationMessage objects. The container + * then, in turn, can use these values to provide more precise information on the location of an error. + *

+ * The actual prefix of the id attribute may or may not be jsp but it will always map to the + * namespace http://java.sun.com/JSP/Page. A TagLibraryValidator implementation must rely on the uri, not + * the prefix, of the id attribute. + */ +public class ValidationMessage { + + /** + * Create a ValidationMessage. The message String should be non-null. The value of id may be null, if the message is + * not specific to any XML element, or if no jsp:id attributes were passed on. If non-null, the value of id must be + * the value of a jsp:id attribute for the PageData passed into the validate() method. + * + * @param id Either null, or the value of a jsp:id attribute. + * @param message A localized validation message. + */ + public ValidationMessage(String id, String message) { + this.id = id; + this.message = message; + } + + /** + * Get the jsp:id. Null means that there is no information available. + * + * @return The jsp:id information. + */ + public String getId() { + return id; + } + + /** + * Get the localized validation message. + * + * @return A validation message + */ + public String getMessage() { + return message; + } + + // Private data + private final String id; + private final String message; +} diff --git a/java/jakarta/servlet/jsp/tagext/VariableInfo.java b/java/jakarta/servlet/jsp/tagext/VariableInfo.java new file mode 100644 index 0000000..e3409ec --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/VariableInfo.java @@ -0,0 +1,230 @@ +/* + * 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 jakarta.servlet.jsp.tagext; + +/** + * Information on the scripting variables that are created/modified by a tag (at run-time). This information is provided + * by TagExtraInfo classes and it is used by the translation phase of JSP. + *

+ * Scripting variables generated by a custom action have an associated scope of either AT_BEGIN, NESTED, or AT_END. + *

+ * The class name (VariableInfo.getClassName) in the returned objects is used to determine the types of the scripting + * variables. Note that because scripting variables are assigned their values from scoped attributes which cannot be of + * primitive types, "boxed" types such as java.lang.Integer must be used instead of primitives. + *

+ * The class name may be a Fully Qualified Class Name, or a short class name. + *

+ * If a Fully Qualified Class Name is provided, it should refer to a class that should be in the CLASSPATH for the Web + * Application (see Servlet 2.4 specification - essentially it is WEB-INF/lib and WEB-INF/classes). Failure to be so + * will lead to a translation-time error. + *

+ * If a short class name is given in the VariableInfo objects, then the class name must be that of a public class in the + * context of the import directives of the page where the custom action appears. The class must also be in the CLASSPATH + * for the Web Application (see Servlet 2.4 specification - essentially it is WEB-INF/lib and WEB-INF/classes). Failure + * to be so will lead to a translation-time error. + *

+ * Usage Comments + *

+ * Frequently a fully qualified class name will refer to a class that is known to the tag library and thus, delivered in + * the same JAR file as the tag handlers. In most other remaining cases it will refer to a class that is in the platform + * on which the JSP processor is built. Using fully qualified class names in this manner makes the usage relatively + * resistant to configuration errors. + *

+ * A short name is usually generated by the tag library based on some attributes passed through from the custom action + * user (the author), and it is thus less robust: for instance a missing import directive in the referring JSP page will + * lead to an invalid short name class and a translation error. + *

+ * Synchronization Protocol + *

+ * The result of the invocation on getVariableInfo is an array of VariableInfo objects. Each such object describes a + * scripting variable by providing its name, its type, whether the variable is new or not, and what its scope is. Scope + * is best described through a picture: + *

+ * NESTED, AT_BEGIN and AT_END Variable Scopes + *

+ * The JSP 2.0 specification defines the interpretation of 3 values: + *

    + *
  • NESTED, if the scripting variable is available between the start tag and the end tag of the action that defines + * it. + *
  • AT_BEGIN, if the scripting variable is available from the start tag of the action that defines it until the end + * of the scope. + *
  • AT_END, if the scripting variable is available after the end tag of the action that defines it until the end of + * the scope. + *
+ * The scope value for a variable implies what methods may affect its value and thus where synchronization is needed as + * illustrated by the table below. Note: the synchronization of the variable(s) will occur after the + * respective method has been called.
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Variable Synchronization Points
Variable Synchronization Points
+ *
 doStartTag()doInitBody()doAfterBody()doEndTag()doTag()
Tag
+ *
AT_BEGIN, NESTED
+ *

+ *

+ *
AT_BEGIN, AT_END
+ *

+ *
IterationTag
+ *
AT_BEGIN, NESTED
+ *

+ *
AT_BEGIN, NESTED
+ *
AT_BEGIN, AT_END
+ *

+ *
BodyTag
+ *
AT_BEGIN, NESTED1
+ *
AT_BEGIN, NESTED1
+ *
AT_BEGIN, NESTED
+ *
AT_BEGIN, AT_END
+ *

+ *
SimpleTag
+ *

+ *

+ *

+ *

+ *
AT_BEGIN, AT_END
+ *
+ * 1 Called after doStartTag() if EVAL_BODY_INCLUDE is returned, or after + * doInitBody() otherwise.
+ *

+ * Variable Information in the TLD + *

+ * Scripting variable information can also be encoded directly for most cases into the Tag Library Descriptor using the + * <variable> subelement of the <tag> element. See the JSP specification. + */ +public class VariableInfo { + + /** + * Scope information that scripting variable is visible only within the start/end tags. + */ + public static final int NESTED = 0; + + /** + * Scope information that scripting variable is visible after start tag. + */ + public static final int AT_BEGIN = 1; + + /** + * Scope information that scripting variable is visible after end tag. + */ + public static final int AT_END = 2; + + /** + * Constructor These objects can be created (at translation time) by the TagExtraInfo instances. + * + * @param varName The name of the scripting variable + * @param className The type of this variable + * @param declare If true, it is a new variable (in some languages this will require a declaration) + * @param scope Indication on the lexical scope of the variable + */ + public VariableInfo(String varName, String className, boolean declare, int scope) { + this.varName = varName; + this.className = className; + this.declare = declare; + this.scope = scope; + } + + // Accessor methods + + /** + * Returns the name of the scripting variable. + * + * @return the name of the scripting variable + */ + public String getVarName() { + return varName; + } + + /** + * Returns the type of this variable. + * + * @return the type of this variable + */ + public String getClassName() { + return className; + } + + /** + * Returns whether this is a new variable. If so, in some languages this will require a declaration. + * + * @return whether this is a new variable. + */ + public boolean getDeclare() { + return declare; + } + + /** + * Returns the lexical scope of the variable. + * + * @return the lexical scope of the variable, either AT_BEGIN, AT_END, or NESTED. + * + * @see #AT_BEGIN + * @see #AT_END + * @see #NESTED + */ + public int getScope() { + return scope; + } + + // == private data + private final String varName; + private final String className; + private final boolean declare; + private final int scope; +} diff --git a/java/jakarta/servlet/jsp/tagext/doc-files/BodyTagProtocol.gif b/java/jakarta/servlet/jsp/tagext/doc-files/BodyTagProtocol.gif new file mode 100644 index 0000000000000000000000000000000000000000..a61e82b9bfc7216be2f79d31bb447dd9c6fe1ee7 GIT binary patch literal 4647 zcmeHG_g4}O!%ee7%ZBFOdv9|#wKOy}_qNQ9sVO1gOmQG8?m$#rC<>M<6+s$~#8tUP zP0Nj0Jyz}~`kv?eAKr7`AKrWJxxe0f?zz_zXkldRE5ZTd*!y$rAN=|AhlBI@e~AAY z{15+Y;J>ngf0i$E!1e#p)c@6gKXDAeK6V@+e!HpS4FbTg>cwcP>`6Z_XH|N;xvDQy zSPwMMXs+(hkv2z(16v3K1&|9>r8MX^2bKm8?+XbH! zboWmii0W-zyA^e_cNnA9-9F5$*ldG==bnEphDIz>ZH{Vx$3h~Px>eqxpNV${e>f4L zr1U(-B#PBjaEj>t`!M{=6pek&>z&uqL)Oe2)0~9MblCdj;l_B%Xe`1dg zj}|zf^G~*~bSymKG&og}&?v$1NoED?NyjzF0DO{u8M{eA1XlZ{6GYDnd8LRwUzGH0>%LF|h6CQ+?C2Yid+JU zTWkiDEd1H-B}K`*z~D-n1S%MT(p2@TMq6>Ss*)^tRtQKJP>30h&1BVPBoBTdR-N_O z#3tZTsyNw#E`$QRzZRSy)U)luSfg7c-thj|K#Z`0SxyrK`U&?#4A$-S{Xy)g*Ri0Q) z`eDG)x0YvR!cyT7p1Ac_6QntE_gZtNH-5H$9Yfh33LC|ETtEySM;3JIoYhZ`EVzoT=L;GQ=Ip*!#LWR)LbSZ7e@%w z3Ooa5kLAZps8PK8xgR4A3__tJ-7OOCSxdGy`wLTiUtd(ZdDaGhGPc^9>Me-~mxq># zZPlg)#c=ICSjqOJMDq{DMSYo1I@`HY=ogRQB=(uB7#yb)L<{%?2F z*jKVVrpy4ne5%&)zf5qaaO&Gb zKRX5`o`+>W_CFF81_EWD+l{pn_N0q}Hiz4>!9V4XGwW{Izr2%+844; zTesk?5$vv|jWwW>Tt>a~ew_D}0oHpNoL(BoN4&n4KM8QfNxAdJJ*^QTY76(bxS6@9 z7N-we%gb_tq)xCDzrzO~3XVell%muTwH7(M;|~2@8}6z>wq@-Kn2h>+vb-v0wIlur zb7r}Hj>m+9!v~bUJ;{*+{Fi7?-24CF{$2nFu-8`1OIWnmd!J7$0rPE&o~DI@L`><9yA_3AWoou*bEHUWZJT z9#woeS&;D@QmWWnXGJ3sYxn}%5F(!Y=&{-EvYkus=_)1KNUw4CpOn(*IG2xW;Wy9I@xj^k8iC&oDleRPwm!>h zh2-mYEw$zP1f}DJ>dO}%F{&0>!(xK zQt|5~_ESAB8u|cJ$v35r1+T4q zQQ?Mr+4vQAy;w3xzxd#{zOM9;s8Rw+tM^&b4>W(4^<_1u{w?(}!D-c(r(>-2yfgF}Fx1-O z&tdVZ#m|oBu+6Qqv+7y$choeKqYi2b$54(<2_#?dq0(sYBM>85a_(PW3wGuU^*g> z{(7j7vyUy&_g4CD0E}dBMpaDgKEeA9iGC>H0!}*;h!mCAQ&jndq3*9#%9n z)%cU6E#7pQv{k+tzorrq4B9yjd(#hIhDXDvPXA3Pnl>h&`a7_r3sG&Zxg&nNwpbyL zC9yg8pCMXt0cRICWnRUzj`MpkZD)5melovg9__bC$xVgXNzR(K?T&wy#BZP|M5^JL zAF+u$(6GJTN%gMH8=5YLLXWF4eTu|aY_GNzHF}*&f>Y_F|B_5{%*=zJOo{?^Zx|!h zKaYVPj<>LPkBqx_?E-Bl&Aze|YY>5xKKI|)8Lmqm?L8cD0olEc1?(Gq+PSZz6E7zR z=M4tGgxa!+W`Y}V?@OUgE|Irnl1Al{xsi8dZpUAQB}-y&Uhha=AO=DFlS$4lfzAln z-gSOu#B3mFmOr3-9W<>N8u$wmXC56g4cDQBhCY7epO(0=i%_pgK@|ma@kJT}5E@t~ zZCyjI9e=SsSIv_U2jw_mn)E$niUcO5ed=0(o1BH4$M`Mv^ws-1L<2ob7cR;pI8p<= zksNN3eCJY{#D4f)1tdMdhuu(v=uSoE)%beshrd=xPs4(Bl_CiS89kP94uKi5$TVyk zw5}SN)9Y+OiFm_>99csa+(MYN$GGXnC{<^MyiEcUv-;}pZQKa5xtQn)3m;yMdtC=VDI78g$mUH! zi4&7oiZb8nALDmlFCJ- z&&k}ao(y?+cG7(Xfgg1)cJ>7Z%!@|F1uvBg)S2m55A=h7_?ZSd-+=RHLL=5)lP$Sj zY!%Tw?g2K%!d}WT>VdhAz#k3B2YJtUV9l&@D`;P^A=@pMRNR37SWV#HE@9)REU zz`GI}w6>nLLCHhAWm|>7V}Z%0BAC>)+*tR*l4nU zg{9rt!@ge25p#ynxWsi%7KEl_?uiuS8si8mxLRi3^PTZw3^#yJUrSUqDXDr->;L^WjxB})Qo&XZp_9&31n(%`HD`(G zqFz@xg@rkYRLaf<<-3O*0V*T>qdRxfI1kH22CDdaAZKwEd)#gZQz^yZDzVL5rC?P)yf39#~s{K9#jaM$uh!a0A5swcRuZKLzHcxf_MVHFpAl@+F%fZ2&WcS zvIu{}!IINO^^CeVdr$X|*XN58Oasa{_sTtaFqW8D6Vdt}krawCF&mfcWa+1yR!Tlm zQ4Uke79iv5h#9C9xqcMDqw@OT=V+NJeAdg=-zuyB5v$NGj8F|I&_x?iGE9%t(FytH@I+@3F)Fk#h zEd7dXV=q??%a}anL5{H`+nA7~>&j)-h`cD$2jeFF7&eYzl8cGKH|-(=!R%-UOO5GS4#OH);e(+$?N)D@8uyVGs|%p5pZwg7i?wRZIu z3ilk!mZXKco3_#1+NKWLj4rj~LGAvYXb?FrVy^v>SXRX#94Jriafj}tNZe7! z6ERA%DJ8{|k`_xrmQYYMO4b}@A|I4w{z5je9&MeHIPHH?sk(@K$-^RRJHYo9qe!c> zQ=MS#GMF=cUHT7mt6*hvl)>BAgRI)X#^t4&xMy2(hUqxn37Y%>S yqsxDwnnCLtp6hyd)b&A(K4wav@T5=i$I@p?=(9BX{2YDZi2jLofrI0i>Hh)G&bdYa literal 0 HcmV?d00001 diff --git a/java/jakarta/servlet/jsp/tagext/doc-files/IterationTagProtocol.gif b/java/jakarta/servlet/jsp/tagext/doc-files/IterationTagProtocol.gif new file mode 100644 index 0000000000000000000000000000000000000000..c262341d953899ca416914e2c6ed247ad248ac38 GIT binary patch literal 3762 zcmeH{_ct4k8phS;7H#bjrD~<6wgg>TLCo63sL_hONn*rm1T`v1>{_u`>{?xBjW1ez z)E2FxTB3;i{oeoKo^yY=&w0=D>+_z^dEZA{7Xku1a$SV#XejPa5qXp#q)RFp#7UV}+76ORKjGz^hjMiri6c3nfSrZG#@xIGX%$TFBjxH^)ZMyrOOuVpV02( z5tKX^_-<(|gvS;{%Rjy%2-EbML*)dDXbOE>ytD>SA#n2jJq* zi*_WheduXkt5Eo{S8IU}s|*G3c;{TzHfj(M_Zoc`FEJm!EDgW@g1rwfjJZbAE{yu> z$GnQ^MQ+>2T~~o$k5|aIS`AW~D4Riu7{)mv<#r_35>*T>*OLZju_9g^N1)dyKP#}EuKhd zy!Z2?Cwg)e<%#Yk($_qlM}eJVkip3EmT6X)?DLN;1OV!bMPhA7NB*sHlB|N1Y)i(V zhfHTzri~RE(&{hzaxInC27f(@V>z_G8!tUvm_ygEJ&iHvIjhm-apfjq zO^M7q@0oyZw|kP4UW$et(eeiP9zi?#KP9%?iuBXRFlOgIIG!Ss)>qGN_pU)Z{Ht>L z&lQJ!WzK;Ud;y)+pBqfh_x|z)1Kg(eS8Y7o0^ITr+ry}5b6=hbJU<$*P7B|fGw~+x zmkQ_LkEsw@7{m6`YJ;`l_;!T8g0l}VkLcHLw0GM^&ZicGA>o$XfVC~~58@c@{Y6ED z;9+$kT0O=LY~JEnd(fLVmRmAtkfT4dR7#f#YArEE-RL0Ish8~~O{$1|PBV=-b--0b z$RBiRYpCz@L&na2y;WedULaSV)(_W@4S(JLWVH1CfCYU5T7gw{HEfl>=v`RA@ZRA+ z@!7@RSy!nT9w(cQP`|yh4}cxXy}w^UkFln45&XlfbHAn9C*swlF6~v`x3SW9Z0Lpu z>60wq{Tck19$ zQ9))l9Je+^MOr+usXH_5NKlW6rdIrx^C~5Gn``O(j=h2YYRM1D-gi&@q)R)TT!`im ziht?6)E+J>-sbLYthj3G5M$iyLMbz79!~;R7X$tk*Q*gcWOCmV_qO}SR zg}>H$Vf@XO?-g)UiWawxD)elh*WmNc#M^_V^`8R$-BS)WF57A9EAcCLvlRV0E6(hj$>}E+^iLQFk5{3Us;Edaf6)06#CJ*Fhg}3s_8OE z3|`)4T+X*9(JDNjiRgKEFMRzo>^rkSw>i)xs~Q$O0r#>Q{T1Ir_UfnxeW3`?6k2)a%?E@cg3C(bU21# z*H?Ib97=*Q%gCU(Vu$=0ZWttIlDDZ}nLX?TG50g}a_3bxH?L)%ta4zmyvdTK zd1JG#ruO4k1&^YhG+EzTtF9{P4N2+7w1GO`XOb3YZwxzzxa)lGey5r5x5{xcZ78ti zdYX4rI~~yUoKW*!ycxwgB+Lr^Sar?hgw;wBdWA&2#tzTpo!!WoYARc#R;R>1FNmXdpC_Bw0sLux1T@_rh}D~k&H*NH)cxi1p1=mo^dULq?rw&R za)z2ixKo|=#jdk4>#YePWuvzA&}w7!`=_!pSE3@Nec4Y=4I(YD#f7j5gF}mlEGhm0 zC%g53d^v%EeE~)DdgL(8x%uRM{rqX61~9L!#*Rd~;}stAzFf}O`g`5iIm2@3=$t~D zP~E_~HS%7Op3w*^Sf^HybVMTw9g#=A?of6cGp-!Prs0G5%-yJ&g>g4zHh_ke%E zw+TKEGq}RNfcehQvUtp93ku~a-E+E;TuB<0?rLNYa@nZ)ffQG8{^@?;F`4(q%*#}z z{;~7+wqG}Y|DE0$Z%N47)yI|@q5v&(uZZ}}mw^vE=;V-<`yn70_=Xi`LR zxv9Iu)uw)xzhmPHn9p9EwO8%5li5S|D^DE}L(l~}V?x)FZ=H}^JqI%1h+J<%ADRSY zFYbC7LpHt|U}oAzhW8T$uZ%LQ9=?tFvx?eNp7t9>cvA-zS8u4Et~aqGUykP%#Vek! z{ZT>mJgQl|`%t~VuUmQOknJ%4P-E$odeXom8~A!}AsRt;P`_i(Ge#dq-e6q&yU-m9 zU|k+yqu^*9+wWjkyJ%Qkpvj!z(Xm>_N{6t@+ zkMbRwkbbzCFMNK;=wG{4y@O`bh>97ZxGOH*&i(PmFJ3mS@xtb(F1uPquYlEvYyQvW zO$}<1?9;K1uF2wK(DMC)kPpQ?hi-K+XTUAN9wzVs=~#BxIbr8BF8_BNL*+NCj*laP z`V{`-hCTF!H|G-&D6MOVBYJ;wI)b^Ny9ld)gFx|yWO14 zdtQb`4n{0H-g}Y-=liqyYG2#x-J&NqO4U(Irrio&txNsJ~@`Y^gfu zt2mFBSr+0WuY&AYnVl^i;pI`L3}Wc!e)rZ8R8ikO4S$#@#g4uW74|W@0S_$ik9e^a zP1mNA>lzxt8PW!cwrv&AHAlp@hTKCqJ%EQrsXEs8qsv3|0dU1WOiak5DB>h~g%q(( zG%Y=jnUt_0C`MuFBiLHp<(H&_!Hmg@YczAvgFZjA zm@F)|k#$%F;W7~6lo3B7wyc|U)p~VDCo6u@yV04%$ zjfk5MdLdqhR-Q?ewvZ&727Ho?V>pf+pj_*u*jn^M8~b9rO)Mc&NfYU@+=y7`jU>~y zSZi}MR0=f%PhK8K;yy$qX+wX^1Zug;9_l<$qdfl2=V%PFIw?;<9Q&Scr%YPix-=Vd zLGYj(lmWIyv9MT(5BMBgr4sy71-M>ab4%elabp8NQrJfDBVR$9Qv&1>B5n{P)mJW0 z(%6**fU{|`Cuws0**FC-PRSalg2aKcaBA(iOJKD6W}-n`xJG<3k)nVTGh|au7l(W8 zPbxL5VlUF%`peUAD47tXyeVcbm$}V!bu*lvD+hwSosQ6EEP8lu*DQv3i!4`D>rAT7 z6A(VSvo$sFZ{+P!jtG1vofc{qA8~?@`Xf?YGU#E{`CSA@3>-Q>k_uYmEi04-~7kGG=m*|%0$Ojo2QdlVX zc%|qV2Zfn=*LVoZC`nq%QMtJ&I*7QK=O!w)df94fdkaw+>PagHyT$w3X*e5eiHn@9 zE6ZC9{5c%`Xx!^Mnv9LDJGX7^o!Sd4y;jTso{e7G{nuD-F4%6^Jg@3Gn~u-Zttrp> z4j&BEzIFD3 zMrgZ7?%Viw@9@n#8}J;%VG|C{4RCBd=a6$cu5Y;vBXOuZJm!21^xx8_PyTME^Y+%o zZQ#=2D2x>+&xBmXw%%k8&5p`3-#;%&oFDB-FY<#$elOt5g3Mkk|{_YgXTGC zpK|!&fMA5oK{z0W7Vc4@d>UrBVFVq*FrtSc=JMfmDJFR010}lf;(jf0-H!<8t2 z`BM^5WMqPvX$A?SR91*crUYu1FejZ(y=g(5DC%h=pEfRK(FT4d$r+n14BAql9UzJc zqf#2mq*01~;AnVTDtXkTlh|1)mYt3%X{4KedP1Eekm_2SBAB`=q>y%L+d@JAMZcJQ9OX3$Qt{N^&~$1a*o@yd2)9P!23 zb}Wy|8si%`NNoQDNOi3`yOe)EkKd z$9l44?IY3-R5$h4T3@X-e`sUfsMWw!jc`922i@+_WJrXW+3%TMou$*Tm-jAV7rb&8 z)R8k^D1j&b%ig2>r*qRZ(*8}d-Qz%xIoAJ?4Ra7w-=uYftWJdK6-<~RAmon7))Bdo@KwW*$`*g+ZyY96bln-VRW;z zAP*lnJ^|)(^2u3LS zuxY|mpWfi+H6x~tZ0+k>U}BiXw7G42|I1s~7@5dXrx5y!XP8Z6|i>isB=M0l*`o?h=NarPsjLNd$fpe5X6* zA`b^kJw^`;_xR%qBdI*n8>Mss@0%-}A)$w~2%(h~9sO{@vO;ZiP_NbU*l^t1IYc9|t_O`hF>}wfF+uo9Pv!)HI zaB2Q}(Xg`hxIHUtae2#JUH(>rNsaCmr@Nq4T9>KN#OP_a3tikj7f#@nu5(wL#_~2- zOUgx=dfj;5-B$Cx;brB_P9fU{saC%DrLSm{%UP0ru(!s{FLC4L-;CC_mE9U}W((|H z{2I6{5N_`z9Ng9l2RKcxE3Agcdnh2v6|WHvFmg zkn+`(Tw*#Yk$6&dS)RtcUlwt1&BKG}jh#S45W5w|RSYgbxEwh;|IS|~k{{msDE^)R zS_I90+c3V;1HJHNSrz@sqv9qS5JX3Fe}Y^zIhjNWCgaY{UY018f!kO1Xmg$~MD#o< z&5vP-dOmDET&bayNY!Z$l-B&*pW}>PF|)3bgp?)qTDQJLGkVv;1m>bGdFevEqL|vf za-qqUO-pUVc5LPcvcqu=Yj@LedsXq;Fap#e_xH=b@{-}sy+2~^(F$Ng_p@%*1Q9#? ze3~NkvvZxF&I-5|p-$8V#VsEvyO7Tc(;%-s4cl-V8YJ^x+jRu~Cxy$osiY7#x-aza zg}2mJpX4!==dI%cK^#V%9=WI_j_;LrRoIgG#UU9e$uRdSJPx_#9+M60{%HkAYU zf|&+5X`AxrOw_r_UqwQnHyqrk#@`*&S(vBm4>moQIe(v-Y4O&^);Okm(yhK4P{v!` zTMoOKd0scAS4Q7qVz$?-4)7F)Tu${qx@Nx~cNyM^!5V)UhpU>8j;Y;RhZZr74G$l_ zPh2ws?r`IMZri-;or!iALgjU(_>{xskM{mM%p>1-kTE?Wm-lVvy?s@BUE!X%+nihd)uvwBG5b zU-KuIM(1W-el#XV)6^Z4Vnv76Cc}4p% zBse@>=Sx6%N!g}`-3BI<(|ySld|;@7X7qGemt5CoI#7s7@Dw-`NP|N3AYRyfaF%XS z@J|%ANGTX=4CQi#SV*^YV(i0W;df^FA&C9Aet~CHo7nz^OQ?T$r-?}DUfgksm?tHz zryHUOh^bhLNS74G)P9!eiO3}su!u7G^@bU@fbs`{JI8aan2DYjee)+&od+dEmv(Uo zbWXK}80b=U=WMx%a$lHRILC|u*nUa0bkc}x)QCiq=tup75qu*{VuvQ36KgAoKOVSq zjt6QN_=#tNO7mDVJ!pq|20fdjNqBfnZ!uKBD1LcHY`;_*RoH?H@ePQhkBO#dw}^_Q zIE$9|djhGCtWao^=2%g6hj^BT$ux`~rG6KN1ujI7=(KGZxsgmageciYzIZ|aX??&H zGvO#vFGh&*<8Y0JONm$!Z{u`2*<2r~TUB<0SN=$hs%BV-a)8}Neh`UtM#+i%W{M8@ zfP^KK*kv59xIPjofUqVrj`fleX;TDOi+?DVeG-gRi4z(bmhc#rKShOf_mCBcjLzql zTc}iE=}WY*E|hc&+z67@=!bDgh7HL=erXGU`Dr`Vm*m)knpTL*c0^NIgYVds>{yu& zLyo1SW#GV&YB^t-nO$c&n4sBs@N{GdNlQG~msJ^(g&{4r_>`#WX3+*6_mfm9NQ){@v-FxYC~6nU(5koIY`lJOY#HrJeXUpG1kC z=Xp2TN1#}Bly~VNwYL@@DV}LYi($c_<>`H?)nb1sp>Rl{YB7urI+R{ip{Bs02NsWF zv7r~Li_SO}CAy!nNMVU-qSN@GBZqSpDMHgTf}igRGGqcv2ZK`Nv}N~DUB zqyLGS9}1$>Qlw4lq)!T^6RCWZft@N*r3@DwSISejp`}H|8(ms4SxTj1Di~lorEOuR zleeH$f~IWB7HO)c8X6#LI;V14r%LLkX91^vXQLLfrxqF^f4Za_BB&J?B88fGhnk`} zf~XkRB8~caGXkj#GKvE#k<~K(msaW(l&CvI7LO_t1st4Q5uQKSw(xuYQZULsn#3lbB?yg zd6qgqc6Vi+f;_V3laN@Gw>pwNYN++-56c>^OEhgD2%GTvAVJthM6U7ONp|1DyvB(ok@CNT39;zu`nJx z82Gw+4?&*yHiz40vn~GXRiR^y_lak3;&L=em*kaTG0UR%V@?H;hfBMj(FJjw%4ZP8 zkplXSO)HPN=|fp-O>5YnEaSAAtD33D)nV28P(>@Z zNQAVQ(zbnDDlGP~%O|+@`FVtUxb#P<^SZd`I&}|$A5vPjkIT5nP)~#NOiydM#)`RT zrL}3>xr}RUTUn0KxeiK8ouiwCkCqBsS&=2khw;g}6ogI9kZf7I!Fan*XovCY zgS6Ybt^2zM3ABSi8#sHXlIv)OXtM13Vs5*-#ml)7r-jywxEz$d+MA&iQg7ZnosBEL zmy34gYrf}-z9-*Xuj@Ok#9DmrE5Gwgzx8Xs_lv*ztH1lpzy0gK{|mqYEWj&J0029< Cyc@Ov literal 0 HcmV?d00001 diff --git a/java/jakarta/servlet/jsp/tagext/doc-files/VariableInfo-1.gif b/java/jakarta/servlet/jsp/tagext/doc-files/VariableInfo-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..32eabeb836735bdd143094fd9494c4d3b7579f6d GIT binary patch literal 2306 zcmaKrdod?^=89zxI0e`h52Hv9`6^yk-AwnN;`&fQSIF zXwf1K4GkkBBU@WrPfyRlz(6LG$zri`b8{;zD@7tvcX#)fFJGj?6aX3k1Aqy@1`q%c z0AlGo(jt?QN<}&ygM*R9LOve}f}#j^D&VsMLbV`y1LPWk&0Au1L{>BM zdyoLBASZx8fJA^ofChkfzwD5ZL_rD-X$)jAk;z8300qRac}1X+fT0e|b-<>8zyt^j z5dX46Mt>?s(D7n0a#>i($7TY1;QtCz#|jIqXCM)e9X#wOa2TY5Xf$9jfXV!2M<4)# z0I^u=g^Wl=q$36+vJm-FJ5paH3TQcjQ6N|*f@>l8Ho)FlV$743;RH$<$jL~}LV6Jf-$j-P`5jV#mD94){A75acs(NG@Mta9DXe-<375$Vn^a~+w$C|OIj{Gucq?6mW+)D znmM+D{MO9PCqBJy5)?Sb$;?tv&`#6Z6=ju5+8dTO_@uyzYnNr`G|*OLBjfSnYWlj4 zV)xr&d3KdIUkNm$eaYRE*F4Gs`)_3Jyj{{0YuO#Q*K@SEwN{Wi6%$tNd(5dC6Sr|YPV3&gJK5JE2pF-C6n-0V6-uJj9J33i z#s;1}GU3f?{znVK_Sr^dcCxHALh+xSRFcWczPPw4&gGEh-$^zQt{^mw%ZE!LX1(d;d9 z{mIcyqOLf@n#){o{V{((2TMsjJ%zqjg`R3{W&7+@Mp@zSzCkk~Zz9eu3GQY($4x7x z`(9?NvKjf)(7J}wCl z9V^k){n9d49X9wh!#HT4_i6u(j|bdus<}*rw~Ws`sM%o79?eXy{U+IQOh4(fz<-;} zw*Y>vtfoD&O@6XPt2(ZcTl!$`)8F(=Q#MbOK5XCgx~*=?-cPYk(aoaFQf2Gngoi3e zpSM~q59QFTRnM5TTCX_w)80n?Vl`o-k>O=;yC!Eb(RA+jBZ(E-U%6w=%GclPH^?oR z5Y@B&lrH7#&FL!NX;2@nXFp2BT+%U|JfYocFcP!Cc#cxy!pEa>ix)a6eAP&MVpx#2 z)|i43N#@&>bT+j=ukkwWTtRXh*k!%)f=jzi`sW4t`fvPrx(;6^(rj1p-z&9l+nJ~O z?1iOEo`K`dIN$Gv^Rk&Zp4OIR=VhtHD8G3%mad?5>VeW54Y4F6-=A{&xuR5!omWi~^z)k27s=TF)wIahI*vpOnNX$_JkBx+|Ne8! zpZzTvn-a=7e=pQ*%d^jeWhRj@QUOnU$bb(#c6&U*o|!zwvQrX40l!kIW03hp${sHj#+f7tQC-VW(KA cCit=JbesGxIhf5knC9$YdocaXY*{e=FN_F&761SM literal 0 HcmV?d00001 diff --git a/java/jakarta/servlet/jsp/tagext/package.html b/java/jakarta/servlet/jsp/tagext/package.html new file mode 100644 index 0000000..51bad0a --- /dev/null +++ b/java/jakarta/servlet/jsp/tagext/package.html @@ -0,0 +1,47 @@ + + + + + + + +Classes and interfaces for the definition of JavaServer Pages Tag Libraries. + +

+The JavaServer Pages(tm) (JSP) 2.0 specification provides a portable +mechanism for the description of tag libraries. +

+A JSP tag library contains +

    +
  • A Tag Library Descriptor
  • +
  • A number of Tag Files or Tag handler classes defining + request-time behavior
  • +
  • Additional classes and resources used at runtime
  • +
  • Possibly some additional classes to provide extra translation + information
  • +
+

+The JSP 2.0 specification and the reference implementation both contain +simple and moderately complex examples of actions defined using this +mechanism. These are available at JSP's web site, at +http://java.sun.com/products/jsp. +Some readers may want to consult those to get a quick feel for how +the mechanisms work together. + + + diff --git a/java/jakarta/servlet/package.html b/java/jakarta/servlet/package.html new file mode 100644 index 0000000..b08e998 --- /dev/null +++ b/java/jakarta/servlet/package.html @@ -0,0 +1,30 @@ + + + + + + + +The jakarta.servlet package contains a number of classes and interfaces that +describe and define the contracts between a servlet class and the +runtime environment provided for an instance of such a class by a +conforming servlet container. + + + + diff --git a/java/jakarta/servlet/resources/XMLSchema.dtd b/java/jakarta/servlet/resources/XMLSchema.dtd new file mode 100644 index 0000000..5d215ee --- /dev/null +++ b/java/jakarta/servlet/resources/XMLSchema.dtd @@ -0,0 +1,418 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +%xs-datatypes; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/datatypes.dtd b/java/jakarta/servlet/resources/datatypes.dtd new file mode 100644 index 0000000..b8b4d51 --- /dev/null +++ b/java/jakarta/servlet/resources/datatypes.dtd @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/j2ee_1_4.xsd b/java/jakarta/servlet/resources/j2ee_1_4.xsd new file mode 100644 index 0000000..ac2e8c7 --- /dev/null +++ b/java/jakarta/servlet/resources/j2ee_1_4.xsd @@ -0,0 +1,1582 @@ + + + + + + +The following definitions that appear in the common +shareable schema(s) of J2EE deployment descriptors should be +interpreted with respect to the context they are included: + +Deployment Component may indicate one of the following: + j2ee application; + application client; + web application; + enterprise bean; + resource adapter; + +Deployment File may indicate one of the following: + ear file; + war file; + jar file; + rar file; + + + + + + + + + + + + + + + + This group keeps the usage of the contained description related + elements consistent across J2EE deployment descriptors. + + All elements may occur multiple times with different languages, + to support localization of the content. + + + + + + + + + + + + + + + + + The description type is used by a description element to + provide text describing the parent element. The elements + that use this type should include any information that the + Deployment Component's Deployment File file producer wants + to provide to the consumer of the Deployment Component's + Deployment File (i.e., to the Deployer). Typically, the + tools used by such a Deployment File consumer will display + the description when processing the parent element that + contains the description. + + The lang attribute defines the language that the + description is provided in. The default value is "en" (English). + + + + + + + + + + + + + + + + + This type defines a dewey decimal which is used + to describe versions of documents. + + + + + + + + + + + + + + + + Employee Self Service + + The value of the xml:lang attribute is "en" (English) by default. + + ]]> + + + + + + + + + + + + + + + EmployeeRecord + + ../products/product.jar#ProductEJB + + ]]> + + + + + + + + + + + + + + The ejb-local-refType is used by ejb-local-ref elements for + the declaration of a reference to an enterprise bean's local + home. The declaration consists of: + + - an optional description + - the EJB reference name used in the code of the Deployment + Component that's referencing the enterprise bean + - the expected type of the referenced enterprise bean + - the expected local home and local interfaces of the + referenced enterprise bean + - optional ejb-link information, used to specify the + referenced enterprise bean + + + + + + + + + + + + + + + + + + + + + ejb/Payroll + + ]]> + + + + + + + + + + + + + + The ejb-ref-typeType contains the expected type of the + referenced enterprise bean. + + The ejb-ref-type designates a value + that must be one of the following: + + Entity + Session + + + + + + + + + + + + + + + + + + The ejb-refType is used by ejb-ref elements for the + declaration of a reference to an enterprise bean's home. The + declaration consists of: + + - an optional description + - the EJB reference name used in the code of + the Deployment Component that's referencing the enterprise + bean + - the expected type of the referenced enterprise bean + - the expected home and remote interfaces of the referenced + enterprise bean + - optional ejb-link information, used to specify the + referenced enterprise bean + + + + + + + + + + + + + + + + + + + + + + + This type is used to designate an empty + element when used. + + + + + + + + + + + + java.lang.Boolean + + ]]> + + + + + + + + + + + + + + + + + + + + + + + + The env-entryType is used to declare an application's + environment entry. The declaration consists of an optional + description, the name of the environment entry, and an + optional value. If a value is not specified, one must be + supplied during deployment. + + It is used by env-entry elements. + + + + + + + + + + minAmount + + ]]> + + + + + + + + + + 100.00 + + ]]> + + + + + + + + + + + + + + + The elements that use this type designate the name of a + Java class or interface. The name is in the form of a + "binary name", as defined in the JLS. This is the form + of name used in Class.forName(). Tools that need the + canonical name (the name used in source code) will need + to convert this binary name to the canonical name. + + + + + + + + + + + + + + + This type defines four different values which can designate + boolean values. This includes values yes and no which are + not designated by xsd:boolean + + + + + + + + + + + + + + + + + + + com.aardvark.payroll.PayrollHome + + ]]> + + + + + + + + + + + + + + The icon type contains small-icon and large-icon elements + that specify the file names for small and large GIF or + JPEG icon images used to represent the parent element in a + GUI tool. + + The xml:lang attribute defines the language that the + icon file names are provided in. Its value is "en" (English) + by default. + + + + + + + + + employee-service-icon16x16.jpg + + ]]> + + + + + + + employee-service-icon32x32.jpg + + ]]> + + + + + + + + + + + + + + + + + + The java-identifierType defines a Java identifier. + The users of this type should further verify that + the content does not contain Java reserved keywords. + + + + + + + + + + + + + + + + + This is a generic type that designates a Java primitive + type or a fully qualified name of a Java interface/type, + or an array of such types. + + + + + + + + + + + + + + + + + The jndi-nameType type designates a JNDI name in the + Deployment Component's environment and is relative to the + java:comp/env context. A JNDI name must be unique within the + Deployment Component. + + + + + + + + + + + + + + + This group keeps the usage of the contained JNDI environment + reference elements consistent across J2EE deployment descriptors. + + + + + + + + + + + + + + + + + + + + + The listenerType indicates the deployment properties for a web + application listener bean. + + + + + + + + + + + The listener-class element declares a class in the + application must be registered as a web + application listener bean. The value is the fully + qualified classname of the listener class. + + + + + + + + + + + + + + + The local-homeType defines the fully-qualified + name of an enterprise bean's local home interface. + + + + + + + + + + + + + + + The localType defines the fully-qualified name of an + enterprise bean's local interface. + + + + + + + + + + + + + + + The message-destination-linkType is used to link a message + destination reference or message-driven bean to a message + destination. + + The Assembler sets the value to reflect the flow of messages + between producers and consumers in the application. + + The value must be the message-destination-name of a message + destination in the same Deployment File or in another + Deployment File in the same J2EE application unit. + + Alternatively, the value may be composed of a path name + specifying a Deployment File containing the referenced + message destination with the message-destination-name of the + destination appended and separated from the path name by + "#". The path name is relative to the Deployment File + containing Deployment Component that is referencing the + message destination. This allows multiple message + destinations with the same name to be uniquely identified. + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + Consumes + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context and must be + unique within an ejb-jar (for enterprise beans) or a + Deployment File (for others). + + + + + + + + + + + + + + + + + + javax.jms.Queue + + + ]]> + + + + + + + + + + + + + + The message-destination-usageType specifies the use of the + message destination indicated by the reference. The value + indicates whether messages are consumed from the message + destination, produced for the destination, or both. The + Assembler makes use of this information in linking producers + of a destination with its consumers. + + The value of the message-destination-usage element must be + one of the following: + Consumes + Produces + ConsumesProduces + + + + + + + + + + + + + + + + + + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-name element specifies a + name for a message destination. This name must be + unique among the names of message destinations + within the Deployment File. + + + + + + + + + + + + + + + This type is a general type that can be used to declare + parameter/value lists. + + + + + + + + + + + The param-name element contains the name of a + parameter. + + + + + + + + + + The param-value element contains the value of a + parameter. + + + + + + + + + + + + + + + The elements that use this type designate either a relative + path or an absolute path starting with a "/". + + In elements that specify a pathname to a file within the + same Deployment File, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the Deployment File's namespace. Absolute filenames (i.e., + those starting with "/") also specify names in the root of + the Deployment File's namespace. In general, relative names + are preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + The res-authType specifies whether the Deployment Component + code signs on programmatically to the resource manager, or + whether the Container will sign on to the resource manager + on behalf of the Deployment Component. In the latter case, + the Container uses information that is supplied by the + Deployer. + + The value must be one of the two following: + + Application + Container + + + + + + + + + + + + + + + + + + The res-sharing-scope type specifies whether connections + obtained through the given resource manager connection + factory reference can be shared. The value, if specified, + must be one of the two following: + + Shareable + Unshareable + + The default value is Shareable. + + + + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + + + ]]> + + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in + the Deployment Component code. The name is a JNDI + name relative to the java:comp/env context and must + be unique within a Deployment Component. + + + + + + + + + + The resource-env-ref-type element specifies the type + of a resource environment reference. It is the + fully qualified name of a Java language class or + interface. + + + + + + + + + + + + + + + + jdbc/EmployeeAppDB + javax.sql.DataSource + Container + Shareable + + + ]]> + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. + The name is a JNDI name relative to the + java:comp/env context. + The name must be unique within a Deployment File. + + + + + + + + + + The res-type element specifies the type of the data + source. The type is specified by the fully qualified + Java language class or interface + expected to be implemented by the data source. + + + + + + + + + + + + + + + + + + + The role-nameType designates the name of a security role. + + The name must conform to the lexical rules for a token. + + + + + + + + + + + + + + + + The run-asType specifies the run-as identity to be + used for the execution of a component. It contains an + optional description, and the name of a security role. + + + + + + + + + + + + + + + + + + The security-role-refType contains the declaration of a + security role reference in a component's or a + Deployment Component's code. The declaration consists of an + optional description, the security role name used in the + code, and an optional link to a security role. If the + security role is not specified, the Deployer must choose an + appropriate security role. + + + + + + + + + + + The value of the role-name element must be the String used + as the parameter to the + EJBContext.isCallerInRole(String roleName) method or the + HttpServletRequest.isUserInRole(String role) method. + + + + + + + + + + The role-link element is a reference to a defined + security role. The role-link element must contain + the name of one of the security roles defined in the + security-role elements. + + + + + + + + + + + + + + + + This role includes all employees who are authorized + to access the employee service application. + + employee + + + ]]> + + + + + + + + + + + + + + + + + This is a special string datatype that is defined by J2EE as + a base type for defining collapsed strings. When schemas + require trailing/leading space elimination as well as + collapsing the existing whitespace, this base type may be + used. + + + + + + + + + + + + + + + + + This simple type designates a boolean with only two + permissible values + + - true + - false + + + + + + + + + + + + + + + + + The url-patternType contains the url pattern of the mapping. + It must follow the rules specified in Section 11.2 of the + Servlet API Specification. This pattern is assumed to be in + URL-decoded form and must not contain CR(#xD) or LF(#xA). + If it contains those characters, the container must inform + the developer with a descriptive error message. + The container must preserve all characters including whitespaces. + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:anyURI. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:boolean. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:integer. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:NMTOKEN. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:nonNegativeInteger. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:positiveInteger. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:QName. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:string. + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/j2ee_web_services_1_1.xsd b/java/jakarta/servlet/resources/j2ee_web_services_1_1.xsd new file mode 100644 index 0000000..d0cdf11 --- /dev/null +++ b/java/jakarta/servlet/resources/j2ee_web_services_1_1.xsd @@ -0,0 +1,465 @@ + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + ... + + + The instance documents may indicate the published version of the + schema using the xsi:schemaLocation attribute for the J2EE + namespace with the following location: + + http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd + + ]]> + + + + + + + The following conventions apply to all J2EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + The webservices element is the root element for the web services + deployment descriptor. It specifies the set of web service + descriptions that are to be deployed into the J2EE Application Server + and the dependencies they have on container resources and services. + + Used in: webservices.xml + + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and JAX-RPC mapping. The + name must be unique within the deployment descriptor. + + + + + + + + + + + + + + + The port-component element associates a WSDL port with a web service + interface and implementation. It defines the name of the port as a + component, optional description, optional display name, optional iconic + representations, WSDL port QName, Service Endpoint Interface, Service + Implementation Bean. + + + + + + + + + + + + EmployeeService + + + ]]> + + + + + + + + Defines the name space and local name part of the WSDL port QName. + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + + + + Declares the handler for a port-component. Handlers can access the + init-param name/value pairs using the HandlerInfo interface. + + Used in: port-component + + + + + + + + + + Defines the name of the handler. The name must be unique within the + module. + + + + + + + + + Defines a fully qualified class name for the handler implementation. + + + + + + + + + + + Defines the QName of a SOAP header that will be processed by the + handler. + + + + + + + + + The soap-role element contains a SOAP actor definition that the + Handler will play as a role. + + + + + + + + + + + + + + + The service-impl-bean element defines the web service implementation. + A service implementation can be an EJB bean class or JAX-RPC web + component. Existing EJB implementations are exposed as a web service + using an ejb-link. + + Used in: port-component + + + + + + + + + + + + + + + + StockQuoteService + + ]]> + + + + + + + + + + + + + + The webservice-description element defines a WSDL document file + and the set of Port components associated with the WSDL ports + defined in the WSDL document. There may be multiple + webservice-descriptions defined within a module. + + All WSDL file ports must have a corresponding port-component element + defined. + + Used in: webservices + + + + + + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and JAX-RPC + mapping. The name must be unique within the deployment descriptor. + + + + + + + + + The wsdl-file element contains the name of a WSDL file in the + module. The file name is a relative path within the module. + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the JAX-RPC mapping between the Java interfaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module. + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + + + + + + + + + + + EmployeeService + + + ]]> + + + + + + + + + + + + + The required value for the version is 1.1. + + + + + + + + + diff --git a/java/jakarta/servlet/resources/j2ee_web_services_client_1_1.xsd b/java/jakarta/servlet/resources/j2ee_web_services_client_1_1.xsd new file mode 100644 index 0000000..85d1e9c --- /dev/null +++ b/java/jakarta/servlet/resources/j2ee_web_services_client_1_1.xsd @@ -0,0 +1,319 @@ + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + + The service-endpoint-interface element defines a fully qualified + Java class that represents the Service Endpoint Interface of a + WSDL port. + + + + + + + + + + The port-component-link element links a port-component-ref + to a specific port-component required to be made available + by a service reference. + + The value of a port-component-link must be the + port-component-name of a port-component in the same module + or another module in the same application unit. The syntax + for specification follows the syntax defined for ejb-link + in the EJB 2.0 specification. + + + + + + + + + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + + + + + + + The service-ref element declares a reference to a Web + service. It contains optional description, display name and + icons, a declaration of the required Service interface, + an optional WSDL document location, an optional set + of JAX-RPC mappings, an optional QName for the service element, + an optional set of Service Endpoint Interfaces to be resolved + by the container to a WSDL port, and an optional set of handlers. + + + + + + + + + + + The service-ref-name element declares logical name that the + components in the module use to look up the Web service. It + is recommended that all service reference names start with + "service/". + + + + + + + + + + The service-interface element declares the fully qualified class + name of the JAX-RPC Service interface the client depends on. + In most cases the value will be javax.xml.rpc.Service. A JAX-RPC + generated Service Interface class may also be specified. + + + + + + + + + + The wsdl-file element contains the URI location of a WSDL + file. The location is relative to the root of the module. + + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the JAX-RPC mapping between the Java interfaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module file. + + + + + + + + + + The service-qname element declares the specific WSDL service + element that is being referred to. It is not specified if no + wsdl-file is declared. + + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + + Declares the handler for a port-component. Handlers can + access the init-param name/value pairs using the + HandlerInfo interface. If port-name is not specified, the + handler is assumed to be associated with all ports of the + service. + + + + + + + + + + + + + + + Declares the handler for a port-component. Handlers can access the + init-param name/value pairs using the HandlerInfo interface. If + port-name is not specified, the handler is assumed to be associated + with all ports of the service. + + Used in: service-ref + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + Defines a fully qualified class name for the handler + implementation. + + + + + + + + + + + Defines the QName of a SOAP header that will be processed + by the handler. + + + + + + + + + + The soap-role element contains a SOAP actor definition that + the Handler will play as a role. + + + + + + + + + + The port-name element defines the WSDL port-name that a + handler should be associated with. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jakartaee_10.xsd b/java/jakarta/servlet/resources/jakartaee_10.xsd new file mode 100644 index 0000000..ca188fd --- /dev/null +++ b/java/jakarta/servlet/resources/jakartaee_10.xsd @@ -0,0 +1,3073 @@ + + + + + + Copyright (c) 2009, 2021 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + The following definitions that appear in the common + shareable schema(s) of Jakarta EE deployment descriptors should be + interpreted with respect to the context they are included: + + Deployment Component may indicate one of the following: + Jakarta EE application; + application client; + web application; + enterprise bean; + resource adapter; + + Deployment File may indicate one of the following: + ear file; + war file; + jar file; + rar file; + + + + + + + + + + + + + This group keeps the usage of the contained description related + elements consistent across Jakarta EE deployment descriptors. + + All elements may occur multiple times with different languages, + to support localization of the content. + + + + + + + + + + + + + + + This group keeps the usage of the contained JNDI environment + reference elements consistent across Jakarta EE deployment descriptors. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This group collects elements that are common to most + JNDI resource elements. + + + + + + + + + + The JNDI name to be looked up to resolve a resource reference. + + + + + + + + + + + + This group collects elements that are common to all the + JNDI resource elements. It does not include the lookup-name + element, that is only applicable to some resource elements. + + + + + + + + + A product specific name that this resource should be + mapped to. The name of this resource, as defined by the + resource's name element or defaulted, is a name that is + local to the application component using the resource. + (It's a name in the JNDI java:comp/env namespace.) Many + application servers provide a way to map these local + names to names of resources known to the application + server. This mapped name is often a global JNDI name, + but may be a name of any form. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + + + + + + + + Configuration of an administered object. + + + + + + + + + Description of this administered object. + + + + + + + + + The name element specifies the JNDI name of the + administered object being defined. + + + + + + + + + The administered object's interface type. + + + + + + + + + The administered object's class name. + + + + + + + + + Resource adapter name. + + + + + + + + + Property of the administered object property. This may be a + vendor-specific property. + + + + + + + + + + + + + + + + Configuration of a Connector Connection Factory resource. + + + + + + + + + Description of this resource. + + + + + + + + + The name element specifies the JNDI name of the + resource being defined. + + + + + + + + + The fully qualified class name of the connection factory + interface. + + + + + + + + + Resource adapter name. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The level of transaction support the connection factory + needs to support. + + + + + + + + + Resource property. This may be a vendor-specific + property. + + + + + + + + + + + + + + + + Configuration of a DataSource. + + + + + + + + + Description of this DataSource. + + + + + + + + + The name element specifies the JNDI name of the + data source being defined. + + + + + + + + + DataSource, XADataSource or ConnectionPoolDataSource + implementation class. + + + + + + + + + Database server name. + + + + + + + + + Port number where a server is listening for requests. + + + + + + + + + Name of a database on a server. + + + + + + + + url property is specified + along with other standard DataSource properties + such as serverName, databaseName + and portNumber, the more specific properties will + take precedence and url will be ignored. + + ]]> + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + JDBC DataSource property. This may be a vendor-specific + property or a less commonly used DataSource property. + + + + + + + + + Sets the maximum time in seconds that this data source + will wait while attempting to connect to a database. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Isolation level for connections. + + + + + + + + + Number of connections that should be created when a + connection pool is initialized. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The number of seconds that a physical connection should + remain unused in the pool before the connection is + closed for a connection pool. + + + + + + + + + The total number of statements that a connection pool + should keep open. + + + + + + + + + + + + + + + + The description type is used by a description element to + provide text describing the parent element. The elements + that use this type should include any information that the + Deployment Component's Deployment File file producer wants + to provide to the consumer of the Deployment Component's + Deployment File (i.e., to the Deployer). Typically, the + tools used by such a Deployment File consumer will display + the description when processing the parent element that + contains the description. + + The lang attribute defines the language that the + description is provided in. The default value is "en" (English). + + + + + + + + + + + + + + + This type defines a dewey decimal that is used + to describe versions of documents. + + + + + + + + + + + + + + + + Employee Self Service + + + The value of the xml:lang attribute is "en" (English) by default. + + ]]> + + + + + + + + + + + + + + + + EmployeeRecord + + ../products/product.jar#ProductEJB + + ]]> + + + + + + + + + + + + + + + The ejb-local-refType is used by ejb-local-ref elements for + the declaration of a reference to an enterprise bean's local + home or to the local business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the enterprise bean's reference name used in the code of the + Deployment Component that's referencing the enterprise bean. + - the optional expected type of the referenced enterprise bean + - the optional expected local interface of the referenced + enterprise bean or the local business interface of the + referenced enterprise bean. + - the optional expected local home interface of the referenced + enterprise bean. Not applicable if this ejb-local-ref refers + to the local business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property. + + + + + + + + + + + + + + + + + + + + + + ejb/Payroll + + ]]> + + + + + + + + + + + + + + + The ejb-refType is used by ejb-ref elements for the + declaration of a reference to an enterprise bean's home or + to the remote business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the enterprise bean's reference name used in the code of + the Deployment Component that's referencing the enterprise + bean. + - the optional expected type of the referenced enterprise bean + - the optional remote interface of the referenced enterprise bean + or the remote business interface of the referenced enterprise + bean + - the optional expected home interface of the referenced + enterprise bean. Not applicable if this ejb-ref + refers to the remote business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property + + + + + + + + + + + + + + + + + + + + + + + The ejb-ref-typeType contains the expected type of the + referenced enterprise bean. + + The ejb-ref-type designates a value + that must be one of the following: + + Entity + Session + + + + + + + + + + + + + + + + + + + This type is used to designate an empty + element when used. + + + + + + + + + + + + + + The env-entryType is used to declare an application's + environment entry. The declaration consists of an optional + description, the name of the environment entry, a type + (optional if the value is injected, otherwise required), and + an optional value. + + It also includes optional elements to define injection of + the named resource into fields or JavaBeans properties. + + If a value is not specified and injection is requested, + no injection will occur and no entry of the specified name + will be created. This allows an initial value to be + specified in the source code without being incorrectly + changed when no override has been specified. + + If a value is not specified and no injection is requested, + a value must be supplied during deployment. + + This type is used by env-entry elements. + + + + + + + + + minAmount + + ]]> + + + + + + + java.lang.Integer + + ]]> + + + + + + + 100.00 + + ]]> + + + + + + + + + + + + + + + java.lang.Boolean + java.lang.Class + com.example.Color + + ]]> + + + + + + + + + + + + + + + The elements that use this type designate the name of a + Java class or interface. The name is in the form of a + "binary name", as defined in the JLS. This is the form + of name used in Class.forName(). Tools that need the + canonical name (the name used in source code) will need + to convert this binary name to the canonical name. + + + + + + + + + + + + + + + + This type defines four different values which can designate + boolean values. This includes values yes and no which are + not designated by xsd:boolean + + + + + + + + + + + + + + + + + + + + + The icon type contains small-icon and large-icon elements + that specify the file names for small and large GIF, JPEG, + or PNG icon images used to represent the parent element in a + GUI tool. + + The xml:lang attribute defines the language that the + icon file names are provided in. Its value is "en" (English) + by default. + + + + + + + + employee-service-icon16x16.jpg + + ]]> + + + + + + + employee-service-icon32x32.jpg + + ]]> + + + + + + + + + + + + + + + + An injection target specifies a class and a name within + that class into which a resource should be injected. + + The injection target class specifies the fully qualified + class name that is the target of the injection. The + Jakarta EE specifications describe which classes can be an + injection target. + + The injection target name specifies the target within + the specified class. The target is first looked for as a + JavaBeans property name. If not found, the target is + looked for as a field name. + + The specified resource will be injected into the target + during initialization of the class by either calling the + set method for the target property or by setting a value + into the named field. + + + + + + + + + + + + + + The following transaction isolation levels are allowed + (see documentation for the java.sql.Connection interface): + TRANSACTION_READ_UNCOMMITTED + TRANSACTION_READ_COMMITTED + TRANSACTION_REPEATABLE_READ + TRANSACTION_SERIALIZABLE + + + + + + + + + + + + + + + + + + + The java-identifierType defines a Java identifier. + The users of this type should further verify that + the content does not contain Java reserved keywords. + + + + + + + + + + + + + + + + + + This is a generic type that designates a Java primitive + type or a fully qualified name of a Java interface/type, + or an array of such types. + + + + + + + + + + + + + + + + + : + + Example: + + jdbc:mysql://localhost:3307/testdb + + ]]> + + + + + + + + + + + + + + + + + Configuration of a Messaging Connection Factory. + + + + + + + + + Description of this Messaging Connection Factory. + + + + + + + + + The name element specifies the JNDI name of the + messaging connection factory being defined. + + + + + + + + + Fully-qualified name of the messaging connection factory + interface. Permitted values are jakarta.jms.ConnectionFactory, + jakarta.jms.QueueConnectionFactory, or + jakarta.jms.TopicConnectionFactory. If not specified, + jakarta.jms.ConnectionFactory will be used. + + + + + + + + + Fully-qualified name of the messaging connection factory + implementation class. Ignored if a resource adapter + is used. + + + + + + + + + Resource adapter name. If not specified, the application + server will define the default behavior, which may or may + not involve the use of a resource adapter. + + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + Client id to use for connection. + + + + + + + + + Messaging Connection Factory property. This may be a vendor-specific + property or a less commonly used ConnectionFactory property. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + + + + + + + + Configuration of a Messaging Destination. + + + + + + + + + Description of this Messaging Destination. + + + + + + + + + The name element specifies the JNDI name of the + messaging destination being defined. + + + + + + + + + Fully-qualified name of the messaging destination interface. + Permitted values are jakarta.jms.Queue and jakarta.jms.Topic + + + + + + + + + Fully-qualified name of the messaging destination implementation + class. Ignored if a resource adapter is used unless the + resource adapter defines more than one destination implementation + class for the specified interface. + + + + + + + + + Resource adapter name. If not specified, the application + server will define the default behavior, which may or may + not involve the use of a resource adapter. + + + + + + + + + Name of the queue or topic. + + + + + + + + + Messaging Destination property. This may be a vendor-specific + property or a less commonly used Destination property. + + + + + + + + + + + + + + + + The jndi-nameType type designates a JNDI name in the + Deployment Component's environment and is relative to the + java:comp/env context. A JNDI name must be unique within the + Deployment Component. + + + + + + + + + + + + + + + com.aardvark.payroll.PayrollHome + + ]]> + + + + + + + + + + + + + + + The lifecycle-callback type specifies a method on a + class to be called when a lifecycle event occurs. + Note that each class may have only one lifecycle callback + method for any given event and that the method may not + be overloaded. + + If the lifefycle-callback-class element is missing then + the class defining the callback is assumed to be the + component class in scope at the place in the descriptor + in which the callback definition appears. + + + + + + + + + + + + + + + + + The listenerType indicates the deployment properties for a web + application listener bean. + + + + + + + + + + The listener-class element declares a class in the + application must be registered as a web + application listener bean. The value is the fully + qualified classname of the listener class. + + + + + + + + + + + + + + + + The localType defines the fully-qualified name of an + enterprise bean's local interface. + + + + + + + + + + + + + + + + The local-homeType defines the fully-qualified + name of an enterprise bean's local home interface. + + + + + + + + + + + + + + + + Configuration of a Mail Session resource. + + + + + + + + + Description of this Mail Session resource. + + + + + + + + + The name element specifies the JNDI name of the + Mail Session resource being defined. + + + + + + + + + Storage protocol. + + + + + + + + + Service provider store protocol implementation class + + + + + + + + + Transport protocol. + + + + + + + + + Service provider transport protocol implementation class + + + + + + + + + Mail server host name. + + + + + + + + + Mail server user name. + + + + + + + + + Password. + + + + + + + + + Email address to indicate the message sender. + + + + + + + + + Mail server property. This may be a vendor-specific + property. + + + + + + + + + + + + + + + + This type is a general type that can be used to declare + parameter/value lists. + + + + + + + + + + The param-name element contains the name of a + parameter. + + + + + + + + + The param-value element contains the value of a + parameter. + + + + + + + + + + + + + + + + The elements that use this type designate either a relative + path or an absolute path starting with a "/". + + In elements that specify a pathname to a file within the + same Deployment File, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the Deployment File's namespace. Absolute filenames (i.e., + those starting with "/") also specify names in the root of + the Deployment File's namespace. In general, relative names + are preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + myPersistenceContext + + + + + myPersistenceContext + + PersistenceUnit1 + + Extended + + + ]]> + + + + + + + + + The persistence-context-ref-name element specifies + the name of a persistence context reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Jakarta EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + Used to specify properties for the container or persistence + provider. Vendor-specific properties may be included in + the set of properties. Properties that are not recognized + by a vendor must be ignored. Entries that make use of the + namespace jakarta.persistence and its subnamespaces must not + be used for vendor-specific properties. The namespace + jakarta.persistence is reserved for use by the specification. + + + + + + + + + + + + + + + + + The persistence-context-synchronizationType specifies + whether a container-managed persistence context is automatically + synchronized with the current transaction. + + The value of the persistence-context-synchronization element + must be one of the following: + Synchronized + Unsynchronized + + + + + + + + + + + + + + + + + + + The persistence-context-typeType specifies the transactional + nature of a persistence context reference. + + The value of the persistence-context-type element must be + one of the following: + Transaction + Extended + + + + + + + + + + + + + + + + + + + Specifies a name/value pair. + + + + + + + + + + + + + + + + + + + + myPersistenceUnit + + + + + myPersistenceUnit + + PersistenceUnit1 + + + + ]]> + + + + + + + + + The persistence-unit-ref-name element specifies + the name of a persistence unit reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Jakarta EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + + jms/StockQueue + + jakarta.jms.Queue + + + + ]]> + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in + the Deployment Component code. The name is a JNDI + name relative to the java:comp/env context and must + be unique within a Deployment Component. + + + + + + + + + The resource-env-ref-type element specifies the type + of a resource environment reference. It is the + fully qualified name of a Java language class or + interface. + + + + + + + + + + + + + + + + + jdbc/EmployeeAppDB + javax.sql.DataSource + Container + Shareable + + + ]]> + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. + The name is a JNDI name relative to the + java:comp/env context. + The name must be unique within a Deployment File. + + + + + + + + + The res-type element specifies the type of the data + source. The type is specified by the fully qualified + Java language class or interface + expected to be implemented by the data source. + + + + + + + + + + + + + + + + + + + The res-authType specifies whether the Deployment Component + code signs on programmatically to the resource manager, or + whether the Container will sign on to the resource manager + on behalf of the Deployment Component. In the latter case, + the Container uses information that is supplied by the + Deployer. + + The value must be one of the two following: + + Application + Container + + + + + + + + + + + + + + + + + + + The res-sharing-scope type specifies whether connections + obtained through the given resource manager connection + factory reference can be shared. The value, if specified, + must be one of the two following: + + Shareable + Unshareable + + The default value is Shareable. + + + + + + + + + + + + + + + + + + + The run-asType specifies the run-as identity to be + used for the execution of a component. It contains an + optional description, and the name of a security role. + + + + + + + + + + + + + + + + + + The role-nameType designates the name of a security role. + + The name must conform to the lexical rules for a token. + + + + + + + + + + + + + + + + + This role includes all employees who are authorized + to access the employee service application. + + employee + + + ]]> + + + + + + + + + + + + + + + + + The security-role-refType contains the declaration of a + security role reference in a component's or a + Deployment Component's code. The declaration consists of an + optional description, the security role name used in the + code, and an optional link to a security role. If the + security role is not specified, the Deployer must choose an + appropriate security role. + + + + + + + + + + The value of the role-name element must be the String used + as the parameter to the + EJBContext.isCallerInRole(String roleName) method or the + HttpServletRequest.isUserInRole(String role) method. + + + + + + + + + The role-link element is a reference to a defined + security role. The role-link element must contain + the name of one of the security roles defined in the + security-role elements. + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:QName. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:boolean. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:NMTOKEN. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:anyURI. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:integer. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:positiveInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:nonNegativeInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:string. + + + + + + + + + + + + + + + + + + This is a special string datatype that is defined by Jakarta EE as + a base type for defining collapsed strings. When schemas + require trailing/leading space elimination as well as + collapsing the existing whitespace, this base type may be + used. + + + + + + + + + + + + + + + + + + This simple type designates a boolean with only two + permissible values + + - true + - false + + + + + + + + + + + + + + + + + + The url-patternType contains the url pattern of the mapping. + It must follow the rules specified in Section 11.2 of the + Servlet API Specification. This pattern is assumed to be in + URL-decoded form and must not contain CR(#xD) or LF(#xA). + If it contains those characters, the container must inform + the developer with a descriptive error message. + The container must preserve all characters including whitespaces. + + + + + + + + + + + + + + + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-name element specifies a + name for a message destination. This name must be + unique among the names of message destinations + within the Deployment File. + + + + + + + + + A product specific name that this message destination + should be mapped to. Each message-destination-ref + element that references this message destination will + define a name in the namespace of the referencing + component or in one of the other predefined namespaces. + Many application servers provide a way to map these + local names to names of resources known to the + application server. This mapped name is often a global + JNDI name, but may be a name of any form. Each of the + local names should be mapped to this same global name. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + The JNDI name to be looked up to resolve the message destination. + + + + + + + + + + + + + + + + jms/StockQueue + + jakarta.jms.Queue + + Consumes + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in + Deployment Component code. + + + + + + + + + + + + + + + + + + + + The message-destination-usageType specifies the use of the + message destination indicated by the reference. The value + indicates whether messages are consumed from the message + destination, produced for the destination, or both. The + Assembler makes use of this information in linking producers + of a destination with its consumers. + + The value of the message-destination-usage element must be + one of the following: + Consumes + Produces + ConsumesProduces + + + + + + + + + + + + + + + + + + + jakarta.jms.Queue + + + ]]> + + + + + + + + + + + + + + + The message-destination-linkType is used to link a message + destination reference or message-driven bean to a message + destination. + + The Assembler sets the value to reflect the flow of messages + between producers and consumers in the application. + + The value must be the message-destination-name of a message + destination in the same Deployment File or in another + Deployment File in the same Jakarta EE application unit. + + Alternatively, the value may be composed of a path name + specifying a Deployment File containing the referenced + message destination with the message-destination-name of the + destination appended and separated from the path name by + "#". The path name is relative to the Deployment File + containing Deployment Component that is referencing the + message destination. This allows multiple message + destinations with the same name to be uniquely identified. + + + + + + + + + + + + + + + + The transaction-supportType specifies the level of + transaction support provided by the resource adapter. It is + used by transaction-support elements. + + The value must be one of the following: + + NoTransaction + LocalTransaction + XATransaction + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jakartaee_9.xsd b/java/jakarta/servlet/resources/jakartaee_9.xsd new file mode 100644 index 0000000..bd55ed7 --- /dev/null +++ b/java/jakarta/servlet/resources/jakartaee_9.xsd @@ -0,0 +1,3073 @@ + + + + + + Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + The following definitions that appear in the common + shareable schema(s) of Jakarta EE deployment descriptors should be + interpreted with respect to the context they are included: + + Deployment Component may indicate one of the following: + Jakarta EE application; + application client; + web application; + enterprise bean; + resource adapter; + + Deployment File may indicate one of the following: + ear file; + war file; + jar file; + rar file; + + + + + + + + + + + + + This group keeps the usage of the contained description related + elements consistent across Jakarta EE deployment descriptors. + + All elements may occur multiple times with different languages, + to support localization of the content. + + + + + + + + + + + + + + + This group keeps the usage of the contained JNDI environment + reference elements consistent across Jakarta EE deployment descriptors. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This group collects elements that are common to most + JNDI resource elements. + + + + + + + + + + The JNDI name to be looked up to resolve a resource reference. + + + + + + + + + + + + This group collects elements that are common to all the + JNDI resource elements. It does not include the lookup-name + element, that is only applicable to some resource elements. + + + + + + + + + A product specific name that this resource should be + mapped to. The name of this resource, as defined by the + resource's name element or defaulted, is a name that is + local to the application component using the resource. + (It's a name in the JNDI java:comp/env namespace.) Many + application servers provide a way to map these local + names to names of resources known to the application + server. This mapped name is often a global JNDI name, + but may be a name of any form. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + + + + + + + + Configuration of an administered object. + + + + + + + + + Description of this administered object. + + + + + + + + + The name element specifies the JNDI name of the + administered object being defined. + + + + + + + + + The administered object's interface type. + + + + + + + + + The administered object's class name. + + + + + + + + + Resource adapter name. + + + + + + + + + Property of the administered object property. This may be a + vendor-specific property. + + + + + + + + + + + + + + + + Configuration of a Connector Connection Factory resource. + + + + + + + + + Description of this resource. + + + + + + + + + The name element specifies the JNDI name of the + resource being defined. + + + + + + + + + The fully qualified class name of the connection factory + interface. + + + + + + + + + Resource adapter name. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The level of transaction support the connection factory + needs to support. + + + + + + + + + Resource property. This may be a vendor-specific + property. + + + + + + + + + + + + + + + + Configuration of a DataSource. + + + + + + + + + Description of this DataSource. + + + + + + + + + The name element specifies the JNDI name of the + data source being defined. + + + + + + + + + DataSource, XADataSource or ConnectionPoolDataSource + implementation class. + + + + + + + + + Database server name. + + + + + + + + + Port number where a server is listening for requests. + + + + + + + + + Name of a database on a server. + + + + + + + + url property is specified + along with other standard DataSource properties + such as serverName, databaseName + and portNumber, the more specific properties will + take precedence and url will be ignored. + + ]]> + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + JDBC DataSource property. This may be a vendor-specific + property or a less commonly used DataSource property. + + + + + + + + + Sets the maximum time in seconds that this data source + will wait while attempting to connect to a database. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Isolation level for connections. + + + + + + + + + Number of connections that should be created when a + connection pool is initialized. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The number of seconds that a physical connection should + remain unused in the pool before the connection is + closed for a connection pool. + + + + + + + + + The total number of statements that a connection pool + should keep open. + + + + + + + + + + + + + + + + The description type is used by a description element to + provide text describing the parent element. The elements + that use this type should include any information that the + Deployment Component's Deployment File file producer wants + to provide to the consumer of the Deployment Component's + Deployment File (i.e., to the Deployer). Typically, the + tools used by such a Deployment File consumer will display + the description when processing the parent element that + contains the description. + + The lang attribute defines the language that the + description is provided in. The default value is "en" (English). + + + + + + + + + + + + + + + This type defines a dewey decimal that is used + to describe versions of documents. + + + + + + + + + + + + + + + + Employee Self Service + + + The value of the xml:lang attribute is "en" (English) by default. + + ]]> + + + + + + + + + + + + + + + + EmployeeRecord + + ../products/product.jar#ProductEJB + + ]]> + + + + + + + + + + + + + + + The ejb-local-refType is used by ejb-local-ref elements for + the declaration of a reference to an enterprise bean's local + home or to the local business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the enterprise bean's reference name used in the code of the + Deployment Component that's referencing the enterprise bean. + - the optional expected type of the referenced enterprise bean + - the optional expected local interface of the referenced + enterprise bean or the local business interface of the + referenced enterprise bean. + - the optional expected local home interface of the referenced + enterprise bean. Not applicable if this ejb-local-ref refers + to the local business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property. + + + + + + + + + + + + + + + + + + + + + + ejb/Payroll + + ]]> + + + + + + + + + + + + + + + The ejb-refType is used by ejb-ref elements for the + declaration of a reference to an enterprise bean's home or + to the remote business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the enterprise bean's reference name used in the code of + the Deployment Component that's referencing the enterprise + bean. + - the optional expected type of the referenced enterprise bean + - the optional remote interface of the referenced enterprise bean + or the remote business interface of the referenced enterprise + bean + - the optional expected home interface of the referenced + enterprise bean. Not applicable if this ejb-ref + refers to the remote business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property + + + + + + + + + + + + + + + + + + + + + + + The ejb-ref-typeType contains the expected type of the + referenced enterprise bean. + + The ejb-ref-type designates a value + that must be one of the following: + + Entity + Session + + + + + + + + + + + + + + + + + + + This type is used to designate an empty + element when used. + + + + + + + + + + + + + + The env-entryType is used to declare an application's + environment entry. The declaration consists of an optional + description, the name of the environment entry, a type + (optional if the value is injected, otherwise required), and + an optional value. + + It also includes optional elements to define injection of + the named resource into fields or JavaBeans properties. + + If a value is not specified and injection is requested, + no injection will occur and no entry of the specified name + will be created. This allows an initial value to be + specified in the source code without being incorrectly + changed when no override has been specified. + + If a value is not specified and no injection is requested, + a value must be supplied during deployment. + + This type is used by env-entry elements. + + + + + + + + + minAmount + + ]]> + + + + + + + java.lang.Integer + + ]]> + + + + + + + 100.00 + + ]]> + + + + + + + + + + + + + + + java.lang.Boolean + java.lang.Class + com.example.Color + + ]]> + + + + + + + + + + + + + + + The elements that use this type designate the name of a + Java class or interface. The name is in the form of a + "binary name", as defined in the JLS. This is the form + of name used in Class.forName(). Tools that need the + canonical name (the name used in source code) will need + to convert this binary name to the canonical name. + + + + + + + + + + + + + + + + This type defines four different values which can designate + boolean values. This includes values yes and no which are + not designated by xsd:boolean + + + + + + + + + + + + + + + + + + + + + The icon type contains small-icon and large-icon elements + that specify the file names for small and large GIF, JPEG, + or PNG icon images used to represent the parent element in a + GUI tool. + + The xml:lang attribute defines the language that the + icon file names are provided in. Its value is "en" (English) + by default. + + + + + + + + employee-service-icon16x16.jpg + + ]]> + + + + + + + employee-service-icon32x32.jpg + + ]]> + + + + + + + + + + + + + + + + An injection target specifies a class and a name within + that class into which a resource should be injected. + + The injection target class specifies the fully qualified + class name that is the target of the injection. The + Jakarta EE specifications describe which classes can be an + injection target. + + The injection target name specifies the target within + the specified class. The target is first looked for as a + JavaBeans property name. If not found, the target is + looked for as a field name. + + The specified resource will be injected into the target + during initialization of the class by either calling the + set method for the target property or by setting a value + into the named field. + + + + + + + + + + + + + + The following transaction isolation levels are allowed + (see documentation for the java.sql.Connection interface): + TRANSACTION_READ_UNCOMMITTED + TRANSACTION_READ_COMMITTED + TRANSACTION_REPEATABLE_READ + TRANSACTION_SERIALIZABLE + + + + + + + + + + + + + + + + + + + The java-identifierType defines a Java identifier. + The users of this type should further verify that + the content does not contain Java reserved keywords. + + + + + + + + + + + + + + + + + + This is a generic type that designates a Java primitive + type or a fully qualified name of a Java interface/type, + or an array of such types. + + + + + + + + + + + + + + + + + : + + Example: + + jdbc:mysql://localhost:3307/testdb + + ]]> + + + + + + + + + + + + + + + + + Configuration of a Messaging Connection Factory. + + + + + + + + + Description of this Messaging Connection Factory. + + + + + + + + + The name element specifies the JNDI name of the + messaging connection factory being defined. + + + + + + + + + Fully-qualified name of the messaging connection factory + interface. Permitted values are jakarta.jms.ConnectionFactory, + jakarta.jms.QueueConnectionFactory, or + jakarta.jms.TopicConnectionFactory. If not specified, + jakarta.jms.ConnectionFactory will be used. + + + + + + + + + Fully-qualified name of the messaging connection factory + implementation class. Ignored if a resource adapter + is used. + + + + + + + + + Resource adapter name. If not specified, the application + server will define the default behavior, which may or may + not involve the use of a resource adapter. + + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + Client id to use for connection. + + + + + + + + + Messaging Connection Factory property. This may be a vendor-specific + property or a less commonly used ConnectionFactory property. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + + + + + + + + Configuration of a Messaging Destination. + + + + + + + + + Description of this Messaging Destination. + + + + + + + + + The name element specifies the JNDI name of the + messaging destination being defined. + + + + + + + + + Fully-qualified name of the messaging destination interface. + Permitted values are jakarta.jms.Queue and jakarta.jms.Topic + + + + + + + + + Fully-qualified name of the messaging destination implementation + class. Ignored if a resource adapter is used unless the + resource adapter defines more than one destination implementation + class for the specified interface. + + + + + + + + + Resource adapter name. If not specified, the application + server will define the default behavior, which may or may + not involve the use of a resource adapter. + + + + + + + + + Name of the queue or topic. + + + + + + + + + Messaging Destination property. This may be a vendor-specific + property or a less commonly used Destination property. + + + + + + + + + + + + + + + + The jndi-nameType type designates a JNDI name in the + Deployment Component's environment and is relative to the + java:comp/env context. A JNDI name must be unique within the + Deployment Component. + + + + + + + + + + + + + + + com.aardvark.payroll.PayrollHome + + ]]> + + + + + + + + + + + + + + + The lifecycle-callback type specifies a method on a + class to be called when a lifecycle event occurs. + Note that each class may have only one lifecycle callback + method for any given event and that the method may not + be overloaded. + + If the lifefycle-callback-class element is missing then + the class defining the callback is assumed to be the + component class in scope at the place in the descriptor + in which the callback definition appears. + + + + + + + + + + + + + + + + + The listenerType indicates the deployment properties for a web + application listener bean. + + + + + + + + + + The listener-class element declares a class in the + application must be registered as a web + application listener bean. The value is the fully + qualified classname of the listener class. + + + + + + + + + + + + + + + + The localType defines the fully-qualified name of an + enterprise bean's local interface. + + + + + + + + + + + + + + + + The local-homeType defines the fully-qualified + name of an enterprise bean's local home interface. + + + + + + + + + + + + + + + + Configuration of a Mail Session resource. + + + + + + + + + Description of this Mail Session resource. + + + + + + + + + The name element specifies the JNDI name of the + Mail Session resource being defined. + + + + + + + + + Storage protocol. + + + + + + + + + Service provider store protocol implementation class + + + + + + + + + Transport protocol. + + + + + + + + + Service provider transport protocol implementation class + + + + + + + + + Mail server host name. + + + + + + + + + Mail server user name. + + + + + + + + + Password. + + + + + + + + + Email address to indicate the message sender. + + + + + + + + + Mail server property. This may be a vendor-specific + property. + + + + + + + + + + + + + + + + This type is a general type that can be used to declare + parameter/value lists. + + + + + + + + + + The param-name element contains the name of a + parameter. + + + + + + + + + The param-value element contains the value of a + parameter. + + + + + + + + + + + + + + + + The elements that use this type designate either a relative + path or an absolute path starting with a "/". + + In elements that specify a pathname to a file within the + same Deployment File, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the Deployment File's namespace. Absolute filenames (i.e., + those starting with "/") also specify names in the root of + the Deployment File's namespace. In general, relative names + are preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + myPersistenceContext + + + + + myPersistenceContext + + PersistenceUnit1 + + Extended + + + ]]> + + + + + + + + + The persistence-context-ref-name element specifies + the name of a persistence context reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Jakarta EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + Used to specify properties for the container or persistence + provider. Vendor-specific properties may be included in + the set of properties. Properties that are not recognized + by a vendor must be ignored. Entries that make use of the + namespace jakarta.persistence and its subnamespaces must not + be used for vendor-specific properties. The namespace + jakarta.persistence is reserved for use by the specification. + + + + + + + + + + + + + + + + + The persistence-context-synchronizationType specifies + whether a container-managed persistence context is automatically + synchronized with the current transaction. + + The value of the persistence-context-synchronization element + must be one of the following: + Synchronized + Unsynchronized + + + + + + + + + + + + + + + + + + + The persistence-context-typeType specifies the transactional + nature of a persistence context reference. + + The value of the persistence-context-type element must be + one of the following: + Transaction + Extended + + + + + + + + + + + + + + + + + + + Specifies a name/value pair. + + + + + + + + + + + + + + + + + + + + myPersistenceUnit + + + + + myPersistenceUnit + + PersistenceUnit1 + + + + ]]> + + + + + + + + + The persistence-unit-ref-name element specifies + the name of a persistence unit reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Jakarta EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + + jms/StockQueue + + jakarta.jms.Queue + + + + ]]> + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in + the Deployment Component code. The name is a JNDI + name relative to the java:comp/env context and must + be unique within a Deployment Component. + + + + + + + + + The resource-env-ref-type element specifies the type + of a resource environment reference. It is the + fully qualified name of a Java language class or + interface. + + + + + + + + + + + + + + + + + jdbc/EmployeeAppDB + javax.sql.DataSource + Container + Shareable + + + ]]> + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. + The name is a JNDI name relative to the + java:comp/env context. + The name must be unique within a Deployment File. + + + + + + + + + The res-type element specifies the type of the data + source. The type is specified by the fully qualified + Java language class or interface + expected to be implemented by the data source. + + + + + + + + + + + + + + + + + + + The res-authType specifies whether the Deployment Component + code signs on programmatically to the resource manager, or + whether the Container will sign on to the resource manager + on behalf of the Deployment Component. In the latter case, + the Container uses information that is supplied by the + Deployer. + + The value must be one of the two following: + + Application + Container + + + + + + + + + + + + + + + + + + + The res-sharing-scope type specifies whether connections + obtained through the given resource manager connection + factory reference can be shared. The value, if specified, + must be one of the two following: + + Shareable + Unshareable + + The default value is Shareable. + + + + + + + + + + + + + + + + + + + The run-asType specifies the run-as identity to be + used for the execution of a component. It contains an + optional description, and the name of a security role. + + + + + + + + + + + + + + + + + + The role-nameType designates the name of a security role. + + The name must conform to the lexical rules for a token. + + + + + + + + + + + + + + + + + This role includes all employees who are authorized + to access the employee service application. + + employee + + + ]]> + + + + + + + + + + + + + + + + + The security-role-refType contains the declaration of a + security role reference in a component's or a + Deployment Component's code. The declaration consists of an + optional description, the security role name used in the + code, and an optional link to a security role. If the + security role is not specified, the Deployer must choose an + appropriate security role. + + + + + + + + + + The value of the role-name element must be the String used + as the parameter to the + EJBContext.isCallerInRole(String roleName) method or the + HttpServletRequest.isUserInRole(String role) method. + + + + + + + + + The role-link element is a reference to a defined + security role. The role-link element must contain + the name of one of the security roles defined in the + security-role elements. + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:QName. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:boolean. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:NMTOKEN. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:anyURI. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:integer. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:positiveInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:nonNegativeInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:string. + + + + + + + + + + + + + + + + + + This is a special string datatype that is defined by Jakarta EE as + a base type for defining collapsed strings. When schemas + require trailing/leading space elimination as well as + collapsing the existing whitespace, this base type may be + used. + + + + + + + + + + + + + + + + + + This simple type designates a boolean with only two + permissible values + + - true + - false + + + + + + + + + + + + + + + + + + The url-patternType contains the url pattern of the mapping. + It must follow the rules specified in Section 11.2 of the + Servlet API Specification. This pattern is assumed to be in + URL-decoded form and must not contain CR(#xD) or LF(#xA). + If it contains those characters, the container must inform + the developer with a descriptive error message. + The container must preserve all characters including whitespaces. + + + + + + + + + + + + + + + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-name element specifies a + name for a message destination. This name must be + unique among the names of message destinations + within the Deployment File. + + + + + + + + + A product specific name that this message destination + should be mapped to. Each message-destination-ref + element that references this message destination will + define a name in the namespace of the referencing + component or in one of the other predefined namespaces. + Many application servers provide a way to map these + local names to names of resources known to the + application server. This mapped name is often a global + JNDI name, but may be a name of any form. Each of the + local names should be mapped to this same global name. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + The JNDI name to be looked up to resolve the message destination. + + + + + + + + + + + + + + + + jms/StockQueue + + jakarta.jms.Queue + + Consumes + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in + Deployment Component code. + + + + + + + + + + + + + + + + + + + + The message-destination-usageType specifies the use of the + message destination indicated by the reference. The value + indicates whether messages are consumed from the message + destination, produced for the destination, or both. The + Assembler makes use of this information in linking producers + of a destination with its consumers. + + The value of the message-destination-usage element must be + one of the following: + Consumes + Produces + ConsumesProduces + + + + + + + + + + + + + + + + + + + jakarta.jms.Queue + + + ]]> + + + + + + + + + + + + + + + The message-destination-linkType is used to link a message + destination reference or message-driven bean to a message + destination. + + The Assembler sets the value to reflect the flow of messages + between producers and consumers in the application. + + The value must be the message-destination-name of a message + destination in the same Deployment File or in another + Deployment File in the same Jakarta EE application unit. + + Alternatively, the value may be composed of a path name + specifying a Deployment File containing the referenced + message destination with the message-destination-name of the + destination appended and separated from the path name by + "#". The path name is relative to the Deployment File + containing Deployment Component that is referencing the + message destination. This allows multiple message + destinations with the same name to be uniquely identified. + + + + + + + + + + + + + + + + The transaction-supportType specifies the level of + transaction support provided by the resource adapter. It is + used by transaction-support elements. + + The value must be one of the following: + + NoTransaction + LocalTransaction + XATransaction + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jakartaee_web_services_2_0.xsd b/java/jakarta/servlet/resources/jakartaee_web_services_2_0.xsd new file mode 100644 index 0000000..9f5716b --- /dev/null +++ b/java/jakarta/servlet/resources/jakartaee_web_services_2_0.xsd @@ -0,0 +1,551 @@ + + + + + + Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + ... + + + The instance documents may indicate the published version of the + schema using the xsi:schemaLocation attribute for the Jakarta EE + namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/jakartaee_web_services_2_0.xsd + + ]]> + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The webservices element is the root element for the web services + deployment descriptor. It specifies the set of web service + descriptions that are to be deployed into the Jakarta EE Application Server + and the dependencies they have on container resources and services. + + Used in: webservices.xml + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and Jakarta XML RPC mapping. + The name must be unique within the deployment descriptor. + + + + + + + + + + + + + + + + The port-component element associates a WSDL port with a web service + interface and implementation. It defines the name of the port as a + component, optional description, optional display name, optional iconic + representations, WSDL port QName, Service Endpoint Interface, Service + Implementation Bean. + + This element also associates a WSDL service with a Jakarta XML Web Services + Provider implementation. + + + + + + + + + + + EmployeeService + + + ]]> + + + + + + + + Defines the name space and local name part of the WSDL + service QName. This is required to be specified for + port components that are Jakarta XML Web Services + Provider implementations. + + + + + + + + + Defines the name space and local name part of the WSDL + port QName. This is not required to be specified for port + components that are Jakarta XML Web Services Provider + implementations + + + + + + + + + Used to enable or disable SOAP MTOM/XOP mechanism for an + endpoint implementation. + + Not to be specified for Jakarta XML RPC runtime + + + + + + + + + When MTOM is enabled, binary data above this size in bytes + will be XOP encoded or sent as attachment. Default value is 0. + + Not to be specified for Jakarta XML RPC runtime + + + + + + + + + This specifies the WS-Addressing requirements for a Jakarta XML + web service. It corresponds to jakarta.xml.ws.soap.Addressing + annotation or its feature jakarta.xml.ws.soap.AddressingFeature. + + See the addressingType for more information. + + Not to be specified for Jakarta XML RPC runtime + + + + + + + + + Corresponds to the jakarta.xml.ws.RespectBinding annotation + or its corresponding jakarta.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a Jakarta XML Web + Services implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + Not to be specified for Jakarta XML RPC runtime + + + + + + + + + Used to specify the protocol binding used by the port-component. + If this element is not specified, then the default binding is + used (SOAP 1.1 over HTTP) + + + + + + + + com.wombat.empl.EmployeeService + + This may not be specified in case there is no Service + Enpoint Interface as is the case with directly using an + implementation class with the @WebService annotation. + + When the port component is a Provider implementation + this is not specified. + + ]]> + + + + + + + + + + To be used with Jakarta XML RPC based runtime only. + + + + + + + + + To be used with Jakarta XML Web Services based runtime only. + + + + + + + + + + + + + + + + + The service-impl-bean element defines the web service implementation. + A service implementation can be an enterprise bean class or Jakarta + XML RPC web component. Existing enterprise bean implementations + are exposed as a web service using an ejb-link. + + Used in: port-component + + + + + + + + + + + + + + + + + StockQuoteService + + ]]> + + + + + + + + + + + + + + + The webservice-description element defines a WSDL document file + and the set of Port components associated with the WSDL ports + defined in the WSDL document. There may be multiple + webservice-descriptions defined within a module. + + All WSDL file ports must have a corresponding port-component element + defined. + + Used in: webservices + + + + + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and Jakarta XML RPC + mapping. The name must be unique within the deployment descriptor. + + + + + + + + + The wsdl-file element contains the name of a WSDL file in the + module. The file name is a relative path within the module. + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the Jakarta XML RPC mapping between the Java interaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module. + + This is not required when JAX-Jakarta Enterprise Web Services based + runtime is used. + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + + + + + + + + + + + + EmployeeService + + + + + ]]> + + + + + + + + + + + + The required value for the version is 2.0. + + + + + + + + diff --git a/java/jakarta/servlet/resources/jakartaee_web_services_client_2_0.xsd b/java/jakarta/servlet/resources/jakartaee_web_services_client_2_0.xsd new file mode 100644 index 0000000..dd01c6f --- /dev/null +++ b/java/jakarta/servlet/resources/jakartaee_web_services_client_2_0.xsd @@ -0,0 +1,714 @@ + + + + + + Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + + + + + The service-ref element declares a reference to a Web + service. It contains optional description, display name and + icons, a declaration of the required Service interface, + an optional WSDL document location, an optional set + of Jakarta XML RPC mappings, an optional QName for the service element, + an optional set of Service Endpoint Interfaces to be resolved + by the container to a WSDL port, and an optional set of handlers. + + + + + + + + + + The service-ref-name element declares logical name that the + components in the module use to look up the Web service. It + is recommended that all service reference names start with + "service/". + + + + + + + + + The service-interface element declares the fully qualified class + name of the Jakarta XML RPC Service interface the client depends on. + In most cases the value will be jakarta.xml.rpc.Service. A Jakarta XML + RPC generated Service Interface class may also be specified. + + + + + + + + + The service-ref-type element declares the type of the service-ref + element that is injected or returned when a JNDI lookup is done. + This must be either a fully qualified name of Service class or + the fully qualified name of service endpoint interface class. + This is only used with Jakarta XML Web Services runtime where + the corresponding @WebServiceRef annotation can be used to denote both + a Service or a Port. + + If this is not specified, then the type of service-ref element + that is injected or returned when a JNDI lookup is done is + always a Service interface/class. + + + + + + + + + The wsdl-file element contains the URI location of a WSDL + file. The location is relative to the root of the module. + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the Jakarta XML RPC mapping between the Java interaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module file. + + This is not required when Jakarta Enterprise Web Services based + runtime is used. + + + + + + + + + The service-qname element declares the specific WSDL service + element that is being refered to. It is not specified if no + wsdl-file is declared. + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + + Declares the handler for a port-component. Handlers can + access the init-param name/value pairs using the + HandlerInfo interface. If port-name is not specified, the + handler is assumed to be associated with all ports of the + service. + + To be used with Jakarta XML RPC based runtime only. + + + + + + + + + To be used with Jakarta XML Web Services based runtime only. + + + + + + + + + + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + The service-endpoint-interface element defines a fully qualified + Java class that represents the Service Endpoint Interface of a + WSDL port. + + + + + + + + + Used to enable or disable SOAP MTOM/XOP mechanism on the client + side for a port-component. + + Not to be specified for Jakarta XML RPC runtime + + + + + + + + + When MTOM is enabled, binary data above this size in bytes + should be XOP encoded or sent as attachment. Default value is 0. + + Not to be specified for Jakarta XML RPC runtime + + + + + + + + + This specifies the WS-Addressing requirements for a Jakarta XML + web service. It corresponds to jakarta.xml.ws.soap.Addressing + annotation or its feature jakarta.xml.ws.soap.AddressingFeature. + + See the addressingType for more information. + + Not to be specified for Jakarta XML RPC runtime + + + + + + + + + Corresponds to the jakarta.xml.ws.RespectBinding annotation + or its corresponding jakarta.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a Jakarta XML Web + Services implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + Not to be specified for Jakarta XML RPC runtime + + + + + + + + + The port-component-link element links a port-component-ref + to a specific port-component required to be made available + by a service reference. + + The value of a port-component-link must be the + port-component-name of a port-component in the same module + or another module in the same application unit. The syntax + for specification follows the syntax defined for ejb-link + in the EJB 2.0 specification. + + + + + + + + + + + + + + + + The handler-chains element defines the handlerchains associated with this + service or service endpoint. + + + + + + + + + + + + + + + + + The handler-chain element defines the handlerchain. + Handlerchain can be defined such that the handlers in the + handlerchain operate,all ports of a service, on a specific + port or on a list of protocol-bindings. The choice of elements + service-name-pattern, port-name-pattern and protocol-bindings + are used to specify whether the handlers in handler-chain are + for a service, port or protocol binding. If none of these + choices are specified with the handler-chain element then the + handlers specified in the handler-chain will be applied on + everything. + + + + + + + + + + + + + + + + + + + Defines the type used for specifying a list of + protocol-bindingType(s). For e.g. + + ##SOAP11_HTTP ##SOAP12_HTTP ##XML_HTTP + + + + + + + + + + + Defines the type used for specifying the URI for the + protocol binding used by the port-component. For + portability one could use one of the following tokens that + alias the standard binding types: + + ##SOAP11_HTTP + ##SOAP11_HTTP_MTOM + ##SOAP12_HTTP + ##SOAP12_HTTP_MTOM + ##XML_HTTP + + Other specifications could define tokens that start with ## + to alias new standard binding URIs that are introduced. + + + + + + + + + + + Defines the type that is used for specifying tokens that + start with ## which are used to alias existing standard + protocol bindings and support aliases for new standard + binding URIs that are introduced in future specifications. + + The following tokens alias the standard protocol binding + URIs: + + ##SOAP11_HTTP = "http://schemas.xmlsoap.org/wsdl/soap/http" + ##SOAP11_HTTP_MTOM = + "http://schemas.xmlsoap.org/wsdl/soap/http?mtom=true" + ##SOAP12_HTTP = "http://www.w3.org/2003/05/soap/bindings/HTTP/" + ##SOAP12_HTTP_MTOM = + "http://www.w3.org/2003/05/soap/bindings/HTTP/?mtom=true" + ##XML_HTTP = "http://www.w3.org/2004/08/wsdl/http" + + + + + + + + + + + + + This is used to specify the QName pattern in the + attribute service-name-pattern and port-name-pattern in + the handler-chain element + + For example, the various forms acceptable here for + service-name-pattern attribute in handler-chain element + are : + + Exact Name: service-name-pattern="ns1:EchoService" + + In this case, handlers specified in this + handler-chain element will apply to all ports with + this exact service name. The namespace prefix must + have been declared in a namespace declaration + attribute in either the start-tag of the element + where the prefix is used or in an an ancestor + element (i.e. an element in whose content the + prefixed markup occurs) + + + Pattern : service-name-pattern="ns1:EchoService*" + + In this case, handlers specified in this + handler-chain element will apply to all ports whose + Service names are like EchoService1, EchoServiceFoo + etc. The namespace prefix must have been declared in + a namespace declaration attribute in either the + start-tag of the element where the prefix is used or + in an an ancestor element (i.e. an element in whose + content the prefixed markup occurs) + + Wild Card : service-name-pattern="*" + + In this case, handlers specified in this handler-chain + element will apply to ports of all service names. + + The same can be applied to port-name attribute in + handler-chain element. + + + + + + + + + + + + + + + + This specifies the WS-Addressing requirements for a Jakarta XML web + service. It corresponds to jakarta.xml.ws.soap.Addressing annotation or its + feature jakarta.xml.ws.soap.AddressingFeature. + + If the "enabled" element is "true", WS-Addressing is enabled. + It means that the endpoint supports WS-Addressing but does not require + its use. The default value for "enabled" is "true". + + If the WS-Addressing is enabled and the "required" element is "true", + it means that the endpoint requires WS-Addressing. The default value + for "required" is "false". + + If WS-Addressing is enabled, the "responses" element determines + if an endpoint requires the use of only anonymous responses, + or only non-anonymous responses, or all. The value of the "responses" + element must be one of the following: + + ANONYMOUS + NON_ANONYMOUS + ALL + + The default value for the "responses" is ALL. + + + + + + + + + + + + + + + + + + If WS-Addressing is enabled, this type determines if an endpoint + requires the use of only anonymous responses, or only non-anonymous + responses, or all. + + + + + + + + + + + + + + + + + + + + Corresponds to the jakarta.xml.ws.RespectBinding annotation + or its corresponding jakarta.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a Jakarta XML + Web Services implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + If the "enabled" element is "true", wsdl:binding in the + associated WSDL, if any, must be respected/honored. + + + + + + + + + + + + + + + + Declares the handler for a port-component, service-ref. Handlers can + access the init-param name/value pairs using the HandlerInfo interface. + + Used in: port-component, service-ref + + + + + + + + + + Defines the name of the handler. The name must be unique within the + module. + + + + + + + + + Defines a fully qualified class name for the handler implementation. + + + + + + + + + Not to be specified for Jakarta XML Web Services runtime + + + + + + + + + Defines the QName of a SOAP header that will be processed by the + handler. + + Not to be specified for Jakarta XML Web Services runtime + + + + + + + + + The soap-role element contains a SOAP actor definition that the + Handler will play as a role. + + + + + + + + + The port-name element defines the WSDL port-name that a + handler should be associated with. If port-name is not + specified, the handler is assumed to be associated with + all ports of the service. + + Not to be specified for Jakarta XML Web Services runtime + + + + + + + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_5.xsd b/java/jakarta/servlet/resources/javaee_5.xsd new file mode 100644 index 0000000..4056261 --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_5.xsd @@ -0,0 +1,2103 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2007 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + +The following definitions that appear in the common +shareable schema(s) of J2EE deployment descriptors should be +interpreted with respect to the context they are included: + +Deployment Component may indicate one of the following: + j2ee application; + application client; + web application; + enterprise bean; + resource adapter; + +Deployment File may indicate one of the following: + ear file; + war file; + jar file; + rar file; + + + + + + + + + + + + + + + + This group keeps the usage of the contained description related + elements consistent across Java EE deployment descriptors. + + All elements may occur multiple times with different languages, + to support localization of the content. + + + + + + + + + + + + + + + + + The description type is used by a description element to + provide text describing the parent element. The elements + that use this type should include any information that the + Deployment Component's Deployment File file producer wants + to provide to the consumer of the Deployment Component's + Deployment File (i.e., to the Deployer). Typically, the + tools used by such a Deployment File consumer will display + the description when processing the parent element that + contains the description. + + The lang attribute defines the language that the + description is provided in. The default value is "en" (English). + + + + + + + + + + + + + + + + + This type defines a dewey decimal that is used + to describe versions of documents. + + + + + + + + + + + + + + + + Employee Self Service + + + The value of the xml:lang attribute is "en" (English) by default. + + ]]> + + + + + + + + + + + + + + + EmployeeRecord + + ../products/product.jar#ProductEJB + + ]]> + + + + + + + + + + + + + + The ejb-local-refType is used by ejb-local-ref elements for + the declaration of a reference to an enterprise bean's local + home or to the local business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of the Deployment + Component that's referencing the enterprise bean. + - the optional expected type of the referenced enterprise bean + - the optional expected local interface of the referenced + enterprise bean or the local business interface of the + referenced enterprise bean. + - the optional expected local home interface of the referenced + enterprise bean. Not applicable if this ejb-local-ref refers + to the local business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property. + + + + + + + + + + + + + + + + + + + + + + ejb/Payroll + + ]]> + + + + + + + + + + + + + + The ejb-ref-typeType contains the expected type of the + referenced enterprise bean. + + The ejb-ref-type designates a value + that must be one of the following: + + Entity + Session + + + + + + + + + + + + + + + + + + The ejb-refType is used by ejb-ref elements for the + declaration of a reference to an enterprise bean's home or + to the remote business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of + the Deployment Component that's referencing the enterprise + bean. + - the optional expected type of the referenced enterprise bean + - the optional remote interface of the referenced enterprise bean + or the remote business interface of the referenced enterprise + bean + - the optional expected home interface of the referenced + enterprise bean. Not applicable if this ejb-ref + refers to the remote business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property + + + + + + + + + + + + + + + + + + + + + + + This type is used to designate an empty + element when used. + + + + + + + + + + + + java.lang.Boolean + + ]]> + + + + + + + + + + + + + + + + + + + + + + + + The env-entryType is used to declare an application's + environment entry. The declaration consists of an optional + description, the name of the environment entry, a type + (optional if the value is injected, otherwise required), and + an optional value. + + It also includes optional elements to define injection of + the named resource into fields or JavaBeans properties. + + If a value is not specified and injection is requested, + no injection will occur and no entry of the specified name + will be created. This allows an initial value to be + specified in the source code without being incorrectly + changed when no override has been specified. + + If a value is not specified and no injection is requested, + a value must be supplied during deployment. + + This type is used by env-entry elements. + + + + + + + + + + minAmount + + ]]> + + + + + + + + java.lang.Integer + + ]]> + + + + + + + + 100.00 + + ]]> + + + + + + + + + + + + + + + + + The elements that use this type designate the name of a + Java class or interface. The name is in the form of a + "binary name", as defined in the JLS. This is the form + of name used in Class.forName(). Tools that need the + canonical name (the name used in source code) will need + to convert this binary name to the canonical name. + + + + + + + + + + + + + + + This type defines four different values which can designate + boolean values. This includes values yes and no which are + not designated by xsd:boolean + + + + + + + + + + + + + + + + + + + com.aardvark.payroll.PayrollHome + + ]]> + + + + + + + + + + + + + + The icon type contains small-icon and large-icon elements + that specify the file names for small and large GIF, JPEG, + or PNG icon images used to represent the parent element in a + GUI tool. + + The xml:lang attribute defines the language that the + icon file names are provided in. Its value is "en" (English) + by default. + + + + + + + + + employee-service-icon16x16.jpg + + ]]> + + + + + + + employee-service-icon32x32.jpg + + ]]> + + + + + + + + + + + + + + + + + + An injection target specifies a class and a name within + that class into which a resource should be injected. + + The injection target class specifies the fully qualified + class name that is the target of the injection. The + Java EE specifications describe which classes can be an + injection target. + + The injection target name specifies the target within + the specified class. The target is first looked for as a + JavaBeans property name. If not found, the target is + looked for as a field name. + + The specified resource will be injected into the target + during initialization of the class by either calling the + set method for the target property or by setting a value + into the named field. + + + + + + + + + + + + + + + + The java-identifierType defines a Java identifier. + The users of this type should further verify that + the content does not contain Java reserved keywords. + + + + + + + + + + + + + + + + + This is a generic type that designates a Java primitive + type or a fully qualified name of a Java interface/type, + or an array of such types. + + + + + + + + + + + + + + + + + The jndi-nameType type designates a JNDI name in the + Deployment Component's environment and is relative to the + java:comp/env context. A JNDI name must be unique within the + Deployment Component. + + + + + + + + + + + + + + + This group keeps the usage of the contained JNDI environment + reference elements consistent across Java EE deployment descriptors. + + + + + + + + + + + + + + + + + + + + + + + + + The lifecycle-callback type specifies a method on a + class to be called when a lifecycle event occurs. + Note that each class may have only one lifecycle callback + method for any given event and that the method may not + be overloaded. + + If the lifecycle-callback-class element is missing then + the class defining the callback is assumed to be the + component class in scope at the place in the descriptor + in which the callback definition appears. + + + + + + + + + + + + + + + + The listenerType indicates the deployment properties for a web + application listener bean. + + + + + + + + + + + The listener-class element declares a class in the + application must be registered as a web + application listener bean. The value is the fully + qualified classname of the listener class. + + + + + + + + + + + + + + + The local-homeType defines the fully-qualified + name of an enterprise bean's local home interface. + + + + + + + + + + + + + + + The localType defines the fully-qualified name of an + enterprise bean's local interface. + + + + + + + + + + + + + + + The message-destination-linkType is used to link a message + destination reference or message-driven bean to a message + destination. + + The Assembler sets the value to reflect the flow of messages + between producers and consumers in the application. + + The value must be the message-destination-name of a message + destination in the same Deployment File or in another + Deployment File in the same Java EE application unit. + + Alternatively, the value may be composed of a path name + specifying a Deployment File containing the referenced + message destination with the message-destination-name of the + destination appended and separated from the path name by + "#". The path name is relative to the Deployment File + containing Deployment Component that is referencing the + message destination. This allows multiple message + destinations with the same name to be uniquely identified. + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + Consumes + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context and must be + unique within an ejb-jar (for enterprise beans) or a + Deployment File (for others). + + + + + + + + + + + + + + + + + + + + + javax.jms.Queue + + + ]]> + + + + + + + + + + + + + + The message-destination-usageType specifies the use of the + message destination indicated by the reference. The value + indicates whether messages are consumed from the message + destination, produced for the destination, or both. The + Assembler makes use of this information in linking producers + of a destination with its consumers. + + The value of the message-destination-usage element must be + one of the following: + Consumes + Produces + ConsumesProduces + + + + + + + + + + + + + + + + + + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-name element specifies a + name for a message destination. This name must be + unique among the names of message destinations + within the Deployment File. + + + + + + + + + + + + + + + + + + + + + + This type is a general type that can be used to declare + parameter/value lists. + + + + + + + + + + + The param-name element contains the name of a + parameter. + + + + + + + + + + The param-value element contains the value of a + parameter. + + + + + + + + + + + + + + + The elements that use this type designate either a relative + path or an absolute path starting with a "/". + + In elements that specify a pathname to a file within the + same Deployment File, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the Deployment File's namespace. Absolute filenames (i.e., + those starting with "/") also specify names in the root of + the Deployment File's namespace. In general, relative names + are preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + myPersistenceContext + + + + + myPersistenceContext + + PersistenceUnit1 + + Extended + + + ]]> + + + + + + + + + The persistence-context-ref-name element specifies + the name of a persistence context reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + + Used to specify properties for the container or persistence + provider. Vendor-specific properties may be included in + the set of properties. Properties that are not recognized + by a vendor must be ignored. Entries that make use of the + namespace javax.persistence and its subnamespaces must not + be used for vendor-specific properties. The namespace + javax.persistence is reserved for use by the specification. + + + + + + + + + + + + + + + + + + + The persistence-context-typeType specifies the transactional + nature of a persistence context reference. + + The value of the persistence-context-type element must be + one of the following: + Transaction + Extended + + + + + + + + + + + + + + + + + + myPersistenceUnit + + + + + myPersistenceUnit + + PersistenceUnit1 + + + + ]]> + + + + + + + + + The persistence-unit-ref-name element specifies + the name of a persistence unit reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + + + + + + + + + Specifies a name/value pair. + + + + + + + + + + + + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + The res-authType specifies whether the Deployment Component + code signs on programmatically to the resource manager, or + whether the Container will sign on to the resource manager + on behalf of the Deployment Component. In the latter case, + the Container uses information that is supplied by the + Deployer. + + The value must be one of the two following: + + Application + Container + + + + + + + + + + + + + + + + + + The res-sharing-scope type specifies whether connections + obtained through the given resource manager connection + factory reference can be shared. The value, if specified, + must be one of the two following: + + Shareable + Unshareable + + The default value is Shareable. + + + + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + + + ]]> + + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in + the Deployment Component code. The name is a JNDI + name relative to the java:comp/env context and must + be unique within a Deployment Component. + + + + + + + + + + The resource-env-ref-type element specifies the type + of a resource environment reference. It is the + fully qualified name of a Java language class or + interface. + + + + + + + + + + + + + + + + + + jdbc/EmployeeAppDB + javax.sql.DataSource + Container + Shareable + + + ]]> + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. + The name is a JNDI name relative to the + java:comp/env context. + The name must be unique within a Deployment File. + + + + + + + + + + The res-type element specifies the type of the data + source. The type is specified by the fully qualified + Java language class or interface + expected to be implemented by the data source. + + + + + + + + + + + + + + + + + + + + + + This group collects elements that are common to all the + JNDI resource elements. + + + + + + + + + + + + + + + + + + + + + + + The role-nameType designates the name of a security role. + + The name must conform to the lexical rules for a token. + + + + + + + + + + + + + + + + The run-asType specifies the run-as identity to be + used for the execution of a component. It contains an + optional description, and the name of a security role. + + + + + + + + + + + + + + + + + + The security-role-refType contains the declaration of a + security role reference in a component's or a + Deployment Component's code. The declaration consists of an + optional description, the security role name used in the + code, and an optional link to a security role. If the + security role is not specified, the Deployer must choose an + appropriate security role. + + + + + + + + + + + The value of the role-name element must be the String used + as the parameter to the + EJBContext.isCallerInRole(String roleName) method or the + HttpServletRequest.isUserInRole(String role) method. + + + + + + + + + + The role-link element is a reference to a defined + security role. The role-link element must contain + the name of one of the security roles defined in the + security-role elements. + + + + + + + + + + + + + + + + This role includes all employees who are authorized + to access the employee service application. + + employee + + + ]]> + + + + + + + + + + + + + + + + + This is a special string datatype that is defined by Java EE as + a base type for defining collapsed strings. When schemas + require trailing/leading space elimination as well as + collapsing the existing whitespace, this base type may be + used. + + + + + + + + + + + + + + + + + This simple type designates a boolean with only two + permissible values + + - true + - false + + + + + + + + + + + + + + + + + The url-patternType contains the url pattern of the mapping. + It must follow the rules specified in Section 11.2 of the + Servlet API Specification. This pattern is assumed to be in + URL-decoded form and must not contain CR(#xD) or LF(#xA). + If it contains those characters, the container must inform + the developer with a descriptive error message. + The container must preserve all characters including whitespaces. + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:anyURI. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:boolean. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:integer. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:NMTOKEN. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:nonNegativeInteger. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:positiveInteger. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:QName. + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:string. + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_6.xsd b/java/jakarta/servlet/resources/javaee_6.xsd new file mode 100644 index 0000000..b516350 --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_6.xsd @@ -0,0 +1,2431 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + The following definitions that appear in the common + shareable schema(s) of Java EE deployment descriptors should be + interpreted with respect to the context they are included: + + Deployment Component may indicate one of the following: + java ee application; + application client; + web application; + enterprise bean; + resource adapter; + + Deployment File may indicate one of the following: + ear file; + war file; + jar file; + rar file; + + + + + + + + + + + + + This group keeps the usage of the contained description related + elements consistent across Java EE deployment descriptors. + + All elements may occur multiple times with different languages, + to support localization of the content. + + + + + + + + + + + + + + + This group keeps the usage of the contained JNDI environment + reference elements consistent across Java EE deployment descriptors. + + + + + + + + + + + + + + + + + + + + + + + + This group collects elements that are common to most + JNDI resource elements. + + + + + + + + + + The JNDI name to be looked up to resolve a resource reference. + + + + + + + + + + + + This group collects elements that are common to all the + JNDI resource elements. It does not include the lookup-name + element, that is only applicable to some resource elements. + + + + + + + + + A product specific name that this resource should be + mapped to. The name of this resource, as defined by the + resource's name element or defaulted, is a name that is + local to the application component using the resource. + (It's a name in the JNDI java:comp/env namespace.) Many + application servers provide a way to map these local + names to names of resources known to the application + server. This mapped name is often a global JNDI name, + but may be a name of any form. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + + + + + + + + Configuration of a DataSource. + + + + + + + + + Description of this DataSource. + + + + + + + + + The name element specifies the JNDI name of the + data source being defined. + + + + + + + + + DataSource, XADataSource or ConnectionPoolDataSource + implementation class. + + + + + + + + + Database server name. + + + + + + + + + Port number where a server is listening for requests. + + + + + + + + + Name of a database on a server. + + + + + + + + url property is specified + along with other standard DataSource properties + such as serverName, databaseName + and portNumber, the more specific properties will + take precedence and url will be ignored. + + ]]> + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + JDBC DataSource property. This may be a vendor-specific + property or a less commonly used DataSource property. + + + + + + + + + Sets the maximum time in seconds that this data source + will wait while attempting to connect to a database. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Isolation level for connections. + + + + + + + + + Number of connections that should be created when a + connection pool is initialized. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The number of seconds that a physical connection should + remain unused in the pool before the connection is + closed for a connection pool. + + + + + + + + + The total number of statements that a connection pool + should keep open. + + + + + + + + + + + + + + + + The description type is used by a description element to + provide text describing the parent element. The elements + that use this type should include any information that the + Deployment Component's Deployment File file producer wants + to provide to the consumer of the Deployment Component's + Deployment File (i.e., to the Deployer). Typically, the + tools used by such a Deployment File consumer will display + the description when processing the parent element that + contains the description. + + The lang attribute defines the language that the + description is provided in. The default value is "en" (English). + + + + + + + + + + + + + + + This type defines a dewey decimal that is used + to describe versions of documents. + + + + + + + + + + + + + + + + Employee Self Service + + + The value of the xml:lang attribute is "en" (English) by default. + + ]]> + + + + + + + + + + + + + + + + EmployeeRecord + + ../products/product.jar#ProductEJB + + ]]> + + + + + + + + + + + + + + + The ejb-local-refType is used by ejb-local-ref elements for + the declaration of a reference to an enterprise bean's local + home or to the local business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of the Deployment + Component that's referencing the enterprise bean. + - the optional expected type of the referenced enterprise bean + - the optional expected local interface of the referenced + enterprise bean or the local business interface of the + referenced enterprise bean. + - the optional expected local home interface of the referenced + enterprise bean. Not applicable if this ejb-local-ref refers + to the local business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property. + + + + + + + + + + + + + + + + + + + + + + ejb/Payroll + + ]]> + + + + + + + + + + + + + + + The ejb-refType is used by ejb-ref elements for the + declaration of a reference to an enterprise bean's home or + to the remote business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of + the Deployment Component that's referencing the enterprise + bean. + - the optional expected type of the referenced enterprise bean + - the optional remote interface of the referenced enterprise bean + or the remote business interface of the referenced enterprise + bean + - the optional expected home interface of the referenced + enterprise bean. Not applicable if this ejb-ref + refers to the remote business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property + + + + + + + + + + + + + + + + + + + + + + + The ejb-ref-typeType contains the expected type of the + referenced enterprise bean. + + The ejb-ref-type designates a value + that must be one of the following: + + Entity + Session + + + + + + + + + + + + + + + + + + + This type is used to designate an empty + element when used. + + + + + + + + + + + + + + The env-entryType is used to declare an application's + environment entry. The declaration consists of an optional + description, the name of the environment entry, a type + (optional if the value is injected, otherwise required), and + an optional value. + + It also includes optional elements to define injection of + the named resource into fields or JavaBeans properties. + + If a value is not specified and injection is requested, + no injection will occur and no entry of the specified name + will be created. This allows an initial value to be + specified in the source code without being incorrectly + changed when no override has been specified. + + If a value is not specified and no injection is requested, + a value must be supplied during deployment. + + This type is used by env-entry elements. + + + + + + + + + minAmount + + ]]> + + + + + + + java.lang.Integer + + ]]> + + + + + + + 100.00 + + ]]> + + + + + + + + + + + + + + + java.lang.Boolean + java.lang.Class + com.example.Color + + ]]> + + + + + + + + + + + + + + + The elements that use this type designate the name of a + Java class or interface. The name is in the form of a + "binary name", as defined in the JLS. This is the form + of name used in Class.forName(). Tools that need the + canonical name (the name used in source code) will need + to convert this binary name to the canonical name. + + + + + + + + + + + + + + + + This type defines four different values which can designate + boolean values. This includes values yes and no which are + not designated by xsd:boolean + + + + + + + + + + + + + + + + + + + + + The icon type contains small-icon and large-icon elements + that specify the file names for small and large GIF, JPEG, + or PNG icon images used to represent the parent element in a + GUI tool. + + The xml:lang attribute defines the language that the + icon file names are provided in. Its value is "en" (English) + by default. + + + + + + + + employee-service-icon16x16.jpg + + ]]> + + + + + + + employee-service-icon32x32.jpg + + ]]> + + + + + + + + + + + + + + + + An injection target specifies a class and a name within + that class into which a resource should be injected. + + The injection target class specifies the fully qualified + class name that is the target of the injection. The + Java EE specifications describe which classes can be an + injection target. + + The injection target name specifies the target within + the specified class. The target is first looked for as a + JavaBeans property name. If not found, the target is + looked for as a field name. + + The specified resource will be injected into the target + during initialization of the class by either calling the + set method for the target property or by setting a value + into the named field. + + + + + + + + + + + + + + The following transaction isolation levels are allowed + (see documentation for the java.sql.Connection interface): + TRANSACTION_READ_UNCOMMITTED + TRANSACTION_READ_COMMITTED + TRANSACTION_REPEATABLE_READ + TRANSACTION_SERIALIZABLE + + + + + + + + + + + + + + + + + + + The java-identifierType defines a Java identifier. + The users of this type should further verify that + the content does not contain Java reserved keywords. + + + + + + + + + + + + + + + + + + This is a generic type that designates a Java primitive + type or a fully qualified name of a Java interface/type, + or an array of such types. + + + + + + + + + + + + + + + + + : + + Example: + + jdbc:mysql://localhost:3307/testdb + + ]]> + + + + + + + + + + + + + + + + + The jndi-nameType type designates a JNDI name in the + Deployment Component's environment and is relative to the + java:comp/env context. A JNDI name must be unique within the + Deployment Component. + + + + + + + + + + + + + + + com.aardvark.payroll.PayrollHome + + ]]> + + + + + + + + + + + + + + + The lifecycle-callback type specifies a method on a + class to be called when a lifecycle event occurs. + Note that each class may have only one lifecycle callback + method for any given event and that the method may not + be overloaded. + + If the lifecycle-callback-class element is missing then + the class defining the callback is assumed to be the + component class in scope at the place in the descriptor + in which the callback definition appears. + + + + + + + + + + + + + + + + + The listenerType indicates the deployment properties for a web + application listener bean. + + + + + + + + + + The listener-class element declares a class in the + application must be registered as a web + application listener bean. The value is the fully + qualified classname of the listener class. + + + + + + + + + + + + + + + + The localType defines the fully-qualified name of an + enterprise bean's local interface. + + + + + + + + + + + + + + + + The local-homeType defines the fully-qualified + name of an enterprise bean's local home interface. + + + + + + + + + + + + + + + + This type is a general type that can be used to declare + parameter/value lists. + + + + + + + + + + The param-name element contains the name of a + parameter. + + + + + + + + + The param-value element contains the value of a + parameter. + + + + + + + + + + + + + + + + The elements that use this type designate either a relative + path or an absolute path starting with a "/". + + In elements that specify a pathname to a file within the + same Deployment File, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the Deployment File's namespace. Absolute filenames (i.e., + those starting with "/") also specify names in the root of + the Deployment File's namespace. In general, relative names + are preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + myPersistenceContext + + + + + myPersistenceContext + + PersistenceUnit1 + + Extended + + + ]]> + + + + + + + + + The persistence-context-ref-name element specifies + the name of a persistence context reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + Used to specify properties for the container or persistence + provider. Vendor-specific properties may be included in + the set of properties. Properties that are not recognized + by a vendor must be ignored. Entries that make use of the + namespace javax.persistence and its subnamespaces must not + be used for vendor-specific properties. The namespace + javax.persistence is reserved for use by the specification. + + + + + + + + + + + + + + + + + The persistence-context-typeType specifies the transactional + nature of a persistence context reference. + + The value of the persistence-context-type element must be + one of the following: + Transaction + Extended + + + + + + + + + + + + + + + + + + + Specifies a name/value pair. + + + + + + + + + + + + + + + + + + + + myPersistenceUnit + + + + + myPersistenceUnit + + PersistenceUnit1 + + + + ]]> + + + + + + + + + The persistence-unit-ref-name element specifies + the name of a persistence unit reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + + + ]]> + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in + the Deployment Component code. The name is a JNDI + name relative to the java:comp/env context and must + be unique within a Deployment Component. + + + + + + + + + The resource-env-ref-type element specifies the type + of a resource environment reference. It is the + fully qualified name of a Java language class or + interface. + + + + + + + + + + + + + + + + + jdbc/EmployeeAppDB + javax.sql.DataSource + Container + Shareable + + + ]]> + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. + The name is a JNDI name relative to the + java:comp/env context. + The name must be unique within a Deployment File. + + + + + + + + + The res-type element specifies the type of the data + source. The type is specified by the fully qualified + Java language class or interface + expected to be implemented by the data source. + + + + + + + + + + + + + + + + + + + The res-authType specifies whether the Deployment Component + code signs on programmatically to the resource manager, or + whether the Container will sign on to the resource manager + on behalf of the Deployment Component. In the latter case, + the Container uses information that is supplied by the + Deployer. + + The value must be one of the two following: + + Application + Container + + + + + + + + + + + + + + + + + + + The res-sharing-scope type specifies whether connections + obtained through the given resource manager connection + factory reference can be shared. The value, if specified, + must be one of the two following: + + Shareable + Unshareable + + The default value is Shareable. + + + + + + + + + + + + + + + + + + + The run-asType specifies the run-as identity to be + used for the execution of a component. It contains an + optional description, and the name of a security role. + + + + + + + + + + + + + + + + + + The role-nameType designates the name of a security role. + + The name must conform to the lexical rules for a token. + + + + + + + + + + + + + + + + + This role includes all employees who are authorized + to access the employee service application. + + employee + + + ]]> + + + + + + + + + + + + + + + + + The security-role-refType contains the declaration of a + security role reference in a component's or a + Deployment Component's code. The declaration consists of an + optional description, the security role name used in the + code, and an optional link to a security role. If the + security role is not specified, the Deployer must choose an + appropriate security role. + + + + + + + + + + The value of the role-name element must be the String used + as the parameter to the + EJBContext.isCallerInRole(String roleName) method or the + HttpServletRequest.isUserInRole(String role) method. + + + + + + + + + The role-link element is a reference to a defined + security role. The role-link element must contain + the name of one of the security roles defined in the + security-role elements. + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:QName. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:boolean. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:NMTOKEN. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:anyURI. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:integer. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:positiveInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:nonNegativeInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:string. + + + + + + + + + + + + + + + + + + This is a special string datatype that is defined by Java EE as + a base type for defining collapsed strings. When schemas + require trailing/leading space elimination as well as + collapsing the existing whitespace, this base type may be + used. + + + + + + + + + + + + + + + + + + This simple type designates a boolean with only two + permissible values + + - true + - false + + + + + + + + + + + + + + + + + + The url-patternType contains the url pattern of the mapping. + It must follow the rules specified in Section 11.2 of the + Servlet API Specification. This pattern is assumed to be in + URL-decoded form and must not contain CR(#xD) or LF(#xA). + If it contains those characters, the container must inform + the developer with a descriptive error message. + The container must preserve all characters including whitespaces. + + + + + + + + + + + + + + + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-name element specifies a + name for a message destination. This name must be + unique among the names of message destinations + within the Deployment File. + + + + + + + + + A product specific name that this message destination + should be mapped to. Each message-destination-ref + element that references this message destination will + define a name in the namespace of the referencing + component or in one of the other predefined namespaces. + Many application servers provide a way to map these + local names to names of resources known to the + application server. This mapped name is often a global + JNDI name, but may be a name of any form. Each of the + local names should be mapped to this same global name. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + The JNDI name to be looked up to resolve the message destination. + + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + Consumes + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in + Deployment Component code. + + + + + + + + + + + + + + + + + + + + The message-destination-usageType specifies the use of the + message destination indicated by the reference. The value + indicates whether messages are consumed from the message + destination, produced for the destination, or both. The + Assembler makes use of this information in linking producers + of a destination with its consumers. + + The value of the message-destination-usage element must be + one of the following: + Consumes + Produces + ConsumesProduces + + + + + + + + + + + + + + + + + + + javax.jms.Queue + + + ]]> + + + + + + + + + + + + + + + The message-destination-linkType is used to link a message + destination reference or message-driven bean to a message + destination. + + The Assembler sets the value to reflect the flow of messages + between producers and consumers in the application. + + The value must be the message-destination-name of a message + destination in the same Deployment File or in another + Deployment File in the same Java EE application unit. + + Alternatively, the value may be composed of a path name + specifying a Deployment File containing the referenced + message destination with the message-destination-name of the + destination appended and separated from the path name by + "#". The path name is relative to the Deployment File + containing Deployment Component that is referencing the + message destination. This allows multiple message + destinations with the same name to be uniquely identified. + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_7.xsd b/java/jakarta/servlet/resources/javaee_7.xsd new file mode 100644 index 0000000..72a15bb --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_7.xsd @@ -0,0 +1,3105 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2013 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + The following definitions that appear in the common + shareable schema(s) of Java EE deployment descriptors should be + interpreted with respect to the context they are included: + + Deployment Component may indicate one of the following: + java ee application; + application client; + web application; + enterprise bean; + resource adapter; + + Deployment File may indicate one of the following: + ear file; + war file; + jar file; + rar file; + + + + + + + + + + + + + This group keeps the usage of the contained description related + elements consistent across Java EE deployment descriptors. + + All elements may occur multiple times with different languages, + to support localization of the content. + + + + + + + + + + + + + + + This group keeps the usage of the contained JNDI environment + reference elements consistent across Java EE deployment descriptors. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This group collects elements that are common to most + JNDI resource elements. + + + + + + + + + + The JNDI name to be looked up to resolve a resource reference. + + + + + + + + + + + + This group collects elements that are common to all the + JNDI resource elements. It does not include the lookup-name + element, that is only applicable to some resource elements. + + + + + + + + + A product specific name that this resource should be + mapped to. The name of this resource, as defined by the + resource's name element or defaulted, is a name that is + local to the application component using the resource. + (It's a name in the JNDI java:comp/env namespace.) Many + application servers provide a way to map these local + names to names of resources known to the application + server. This mapped name is often a global JNDI name, + but may be a name of any form. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + + + + + + + + Configuration of an administered object. + + + + + + + + + Description of this administered object. + + + + + + + + + The name element specifies the JNDI name of the + administered object being defined. + + + + + + + + + The administered object's interface type. + + + + + + + + + The administered object's class name. + + + + + + + + + Resource adapter name. + + + + + + + + + Property of the administered object property. This may be a + vendor-specific property. + + + + + + + + + + + + + + + + Configuration of a Connector Connection Factory resource. + + + + + + + + + Description of this resource. + + + + + + + + + The name element specifies the JNDI name of the + resource being defined. + + + + + + + + + The fully qualified class name of the connection factory + interface. + + + + + + + + + Resource adapter name. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The level of transaction support the connection factory + needs to support. + + + + + + + + + Resource property. This may be a vendor-specific + property. + + + + + + + + + + + + + + + + Configuration of a DataSource. + + + + + + + + + Description of this DataSource. + + + + + + + + + The name element specifies the JNDI name of the + data source being defined. + + + + + + + + + DataSource, XADataSource or ConnectionPoolDataSource + implementation class. + + + + + + + + + Database server name. + + + + + + + + + Port number where a server is listening for requests. + + + + + + + + + Name of a database on a server. + + + + + + + + url property is specified + along with other standard DataSource properties + such as serverName, databaseName + and portNumber, the more specific properties will + take precedence and url will be ignored. + + ]]> + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + JDBC DataSource property. This may be a vendor-specific + property or a less commonly used DataSource property. + + + + + + + + + Sets the maximum time in seconds that this data source + will wait while attempting to connect to a database. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Isolation level for connections. + + + + + + + + + Number of connections that should be created when a + connection pool is initialized. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The number of seconds that a physical connection should + remain unused in the pool before the connection is + closed for a connection pool. + + + + + + + + + The total number of statements that a connection pool + should keep open. + + + + + + + + + + + + + + + + The description type is used by a description element to + provide text describing the parent element. The elements + that use this type should include any information that the + Deployment Component's Deployment File file producer wants + to provide to the consumer of the Deployment Component's + Deployment File (i.e., to the Deployer). Typically, the + tools used by such a Deployment File consumer will display + the description when processing the parent element that + contains the description. + + The lang attribute defines the language that the + description is provided in. The default value is "en" (English). + + + + + + + + + + + + + + + This type defines a dewey decimal that is used + to describe versions of documents. + + + + + + + + + + + + + + + + Employee Self Service + + + The value of the xml:lang attribute is "en" (English) by default. + + ]]> + + + + + + + + + + + + + + + + EmployeeRecord + + ../products/product.jar#ProductEJB + + ]]> + + + + + + + + + + + + + + + The ejb-local-refType is used by ejb-local-ref elements for + the declaration of a reference to an enterprise bean's local + home or to the local business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of the Deployment + Component that's referencing the enterprise bean. + - the optional expected type of the referenced enterprise bean + - the optional expected local interface of the referenced + enterprise bean or the local business interface of the + referenced enterprise bean. + - the optional expected local home interface of the referenced + enterprise bean. Not applicable if this ejb-local-ref refers + to the local business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property. + + + + + + + + + + + + + + + + + + + + + + ejb/Payroll + + ]]> + + + + + + + + + + + + + + + The ejb-refType is used by ejb-ref elements for the + declaration of a reference to an enterprise bean's home or + to the remote business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of + the Deployment Component that's referencing the enterprise + bean. + - the optional expected type of the referenced enterprise bean + - the optional remote interface of the referenced enterprise bean + or the remote business interface of the referenced enterprise + bean + - the optional expected home interface of the referenced + enterprise bean. Not applicable if this ejb-ref + refers to the remote business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property + + + + + + + + + + + + + + + + + + + + + + + The ejb-ref-typeType contains the expected type of the + referenced enterprise bean. + + The ejb-ref-type designates a value + that must be one of the following: + + Entity + Session + + + + + + + + + + + + + + + + + + + This type is used to designate an empty + element when used. + + + + + + + + + + + + + + The env-entryType is used to declare an application's + environment entry. The declaration consists of an optional + description, the name of the environment entry, a type + (optional if the value is injected, otherwise required), and + an optional value. + + It also includes optional elements to define injection of + the named resource into fields or JavaBeans properties. + + If a value is not specified and injection is requested, + no injection will occur and no entry of the specified name + will be created. This allows an initial value to be + specified in the source code without being incorrectly + changed when no override has been specified. + + If a value is not specified and no injection is requested, + a value must be supplied during deployment. + + This type is used by env-entry elements. + + + + + + + + + minAmount + + ]]> + + + + + + + java.lang.Integer + + ]]> + + + + + + + 100.00 + + ]]> + + + + + + + + + + + + + + + java.lang.Boolean + java.lang.Class + com.example.Color + + ]]> + + + + + + + + + + + + + + + The elements that use this type designate the name of a + Java class or interface. The name is in the form of a + "binary name", as defined in the JLS. This is the form + of name used in Class.forName(). Tools that need the + canonical name (the name used in source code) will need + to convert this binary name to the canonical name. + + + + + + + + + + + + + + + + This type defines four different values which can designate + boolean values. This includes values yes and no which are + not designated by xsd:boolean + + + + + + + + + + + + + + + + + + + + + The icon type contains small-icon and large-icon elements + that specify the file names for small and large GIF, JPEG, + or PNG icon images used to represent the parent element in a + GUI tool. + + The xml:lang attribute defines the language that the + icon file names are provided in. Its value is "en" (English) + by default. + + + + + + + + employee-service-icon16x16.jpg + + ]]> + + + + + + + employee-service-icon32x32.jpg + + ]]> + + + + + + + + + + + + + + + + An injection target specifies a class and a name within + that class into which a resource should be injected. + + The injection target class specifies the fully qualified + class name that is the target of the injection. The + Java EE specifications describe which classes can be an + injection target. + + The injection target name specifies the target within + the specified class. The target is first looked for as a + JavaBeans property name. If not found, the target is + looked for as a field name. + + The specified resource will be injected into the target + during initialization of the class by either calling the + set method for the target property or by setting a value + into the named field. + + + + + + + + + + + + + + The following transaction isolation levels are allowed + (see documentation for the java.sql.Connection interface): + TRANSACTION_READ_UNCOMMITTED + TRANSACTION_READ_COMMITTED + TRANSACTION_REPEATABLE_READ + TRANSACTION_SERIALIZABLE + + + + + + + + + + + + + + + + + + + The java-identifierType defines a Java identifier. + The users of this type should further verify that + the content does not contain Java reserved keywords. + + + + + + + + + + + + + + + + + + This is a generic type that designates a Java primitive + type or a fully qualified name of a Java interface/type, + or an array of such types. + + + + + + + + + + + + + + + + + : + + Example: + + jdbc:mysql://localhost:3307/testdb + + ]]> + + + + + + + + + + + + + + + + + Configuration of a JMS Connection Factory. + + + + + + + + + Description of this JMS Connection Factory. + + + + + + + + + The name element specifies the JNDI name of the + JMS connection factory being defined. + + + + + + + + + Fully-qualified name of the JMS connection factory + interface. Permitted values are javax.jms.ConnectionFactory, + javax.jms.QueueConnectionFactory, or + javax.jms.TopicConnectionFactory. If not specified, + javax.jms.ConnectionFactory will be used. + + + + + + + + + Fully-qualified name of the JMS connection factory + implementation class. Ignored if a resource adapter + is used. + + + + + + + + + Resource adapter name. If not specified, the application + server will define the default behavior, which may or may + not involve the use of a resource adapter. + + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + Client id to use for connection. + + + + + + + + + JMS Connection Factory property. This may be a vendor-specific + property or a less commonly used ConnectionFactory property. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + + + + + + + + Configuration of a JMS Destination. + + + + + + + + + Description of this JMS Destination. + + + + + + + + + The name element specifies the JNDI name of the + JMS destination being defined. + + + + + + + + + Fully-qualified name of the JMS destination interface. + Permitted values are javax.jms.Queue and javax.jms.Topic + + + + + + + + + Fully-qualified name of the JMS destination implementation + class. Ignored if a resource adapter is used unless the + resource adapter defines more than one destination implementation + class for the specified interface. + + + + + + + + + Resource adapter name. If not specified, the application + server will define the default behavior, which may or may + not involve the use of a resource adapter. + + + + + + + + + Name of the queue or topic. + + + + + + + + + JMS Destination property. This may be a vendor-specific + property or a less commonly used Destination property. + + + + + + + + + + + + + + + + The jndi-nameType type designates a JNDI name in the + Deployment Component's environment and is relative to the + java:comp/env context. A JNDI name must be unique within the + Deployment Component. + + + + + + + + + + + + + + + com.aardvark.payroll.PayrollHome + + ]]> + + + + + + + + + + + + + + + The lifecycle-callback type specifies a method on a + class to be called when a lifecycle event occurs. + Note that each class may have only one lifecycle callback + method for any given event and that the method may not + be overloaded. + + If the lifecycle-callback-class element is missing then + the class defining the callback is assumed to be the + component class in scope at the place in the descriptor + in which the callback definition appears. + + + + + + + + + + + + + + + + + The listenerType indicates the deployment properties for a web + application listener bean. + + + + + + + + + + The listener-class element declares a class in the + application must be registered as a web + application listener bean. The value is the fully + qualified classname of the listener class. + + + + + + + + + + + + + + + + The localType defines the fully-qualified name of an + enterprise bean's local interface. + + + + + + + + + + + + + + + + The local-homeType defines the fully-qualified + name of an enterprise bean's local home interface. + + + + + + + + + + + + + + + + Configuration of a Mail Session resource. + + + + + + + + + Description of this Mail Session resource. + + + + + + + + + The name element specifies the JNDI name of the + Mail Session resource being defined. + + + + + + + + + Storage protocol. + + + + + + + + + Service provider store protocol implementation class + + + + + + + + + Transport protocol. + + + + + + + + + Service provider transport protocol implementation class + + + + + + + + + Mail server host name. + + + + + + + + + Mail server user name. + + + + + + + + + Password. + + + + + + + + + Email address to indicate the message sender. + + + + + + + + + Mail server property. This may be a vendor-specific + property. + + + + + + + + + + + + + + + + This type is a general type that can be used to declare + parameter/value lists. + + + + + + + + + + The param-name element contains the name of a + parameter. + + + + + + + + + The param-value element contains the value of a + parameter. + + + + + + + + + + + + + + + + The elements that use this type designate either a relative + path or an absolute path starting with a "/". + + In elements that specify a pathname to a file within the + same Deployment File, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the Deployment File's namespace. Absolute filenames (i.e., + those starting with "/") also specify names in the root of + the Deployment File's namespace. In general, relative names + are preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + myPersistenceContext + + + + + myPersistenceContext + + PersistenceUnit1 + + Extended + + + ]]> + + + + + + + + + The persistence-context-ref-name element specifies + the name of a persistence context reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + Used to specify properties for the container or persistence + provider. Vendor-specific properties may be included in + the set of properties. Properties that are not recognized + by a vendor must be ignored. Entries that make use of the + namespace javax.persistence and its subnamespaces must not + be used for vendor-specific properties. The namespace + javax.persistence is reserved for use by the specification. + + + + + + + + + + + + + + + + + The persistence-context-synchronizationType specifies + whether a container-managed persistence context is automatically + synchronized with the current transaction. + + The value of the persistence-context-synchronization element + must be one of the following: + Synchronized + Unsynchronized + + + + + + + + + + + + + + + + + + + The persistence-context-typeType specifies the transactional + nature of a persistence context reference. + + The value of the persistence-context-type element must be + one of the following: + Transaction + Extended + + + + + + + + + + + + + + + + + + + Specifies a name/value pair. + + + + + + + + + + + + + + + + + + + + myPersistenceUnit + + + + + myPersistenceUnit + + PersistenceUnit1 + + + + ]]> + + + + + + + + + The persistence-unit-ref-name element specifies + the name of a persistence unit reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + + + ]]> + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in + the Deployment Component code. The name is a JNDI + name relative to the java:comp/env context and must + be unique within a Deployment Component. + + + + + + + + + The resource-env-ref-type element specifies the type + of a resource environment reference. It is the + fully qualified name of a Java language class or + interface. + + + + + + + + + + + + + + + + + jdbc/EmployeeAppDB + javax.sql.DataSource + Container + Shareable + + + ]]> + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. + The name is a JNDI name relative to the + java:comp/env context. + The name must be unique within a Deployment File. + + + + + + + + + The res-type element specifies the type of the data + source. The type is specified by the fully qualified + Java language class or interface + expected to be implemented by the data source. + + + + + + + + + + + + + + + + + + + The res-authType specifies whether the Deployment Component + code signs on programmatically to the resource manager, or + whether the Container will sign on to the resource manager + on behalf of the Deployment Component. In the latter case, + the Container uses information that is supplied by the + Deployer. + + The value must be one of the two following: + + Application + Container + + + + + + + + + + + + + + + + + + + The res-sharing-scope type specifies whether connections + obtained through the given resource manager connection + factory reference can be shared. The value, if specified, + must be one of the two following: + + Shareable + Unshareable + + The default value is Shareable. + + + + + + + + + + + + + + + + + + + The run-asType specifies the run-as identity to be + used for the execution of a component. It contains an + optional description, and the name of a security role. + + + + + + + + + + + + + + + + + + The role-nameType designates the name of a security role. + + The name must conform to the lexical rules for a token. + + + + + + + + + + + + + + + + + This role includes all employees who are authorized + to access the employee service application. + + employee + + + ]]> + + + + + + + + + + + + + + + + + The security-role-refType contains the declaration of a + security role reference in a component's or a + Deployment Component's code. The declaration consists of an + optional description, the security role name used in the + code, and an optional link to a security role. If the + security role is not specified, the Deployer must choose an + appropriate security role. + + + + + + + + + + The value of the role-name element must be the String used + as the parameter to the + EJBContext.isCallerInRole(String roleName) method or the + HttpServletRequest.isUserInRole(String role) method. + + + + + + + + + The role-link element is a reference to a defined + security role. The role-link element must contain + the name of one of the security roles defined in the + security-role elements. + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:QName. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:boolean. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:NMTOKEN. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:anyURI. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:integer. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:positiveInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:nonNegativeInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:string. + + + + + + + + + + + + + + + + + + This is a special string datatype that is defined by Java EE as + a base type for defining collapsed strings. When schemas + require trailing/leading space elimination as well as + collapsing the existing whitespace, this base type may be + used. + + + + + + + + + + + + + + + + + + This simple type designates a boolean with only two + permissible values + + - true + - false + + + + + + + + + + + + + + + + + + The url-patternType contains the url pattern of the mapping. + It must follow the rules specified in Section 11.2 of the + Servlet API Specification. This pattern is assumed to be in + URL-decoded form and must not contain CR(#xD) or LF(#xA). + If it contains those characters, the container must inform + the developer with a descriptive error message. + The container must preserve all characters including whitespaces. + + + + + + + + + + + + + + + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-name element specifies a + name for a message destination. This name must be + unique among the names of message destinations + within the Deployment File. + + + + + + + + + A product specific name that this message destination + should be mapped to. Each message-destination-ref + element that references this message destination will + define a name in the namespace of the referencing + component or in one of the other predefined namespaces. + Many application servers provide a way to map these + local names to names of resources known to the + application server. This mapped name is often a global + JNDI name, but may be a name of any form. Each of the + local names should be mapped to this same global name. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + The JNDI name to be looked up to resolve the message destination. + + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + Consumes + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in + Deployment Component code. + + + + + + + + + + + + + + + + + + + + The message-destination-usageType specifies the use of the + message destination indicated by the reference. The value + indicates whether messages are consumed from the message + destination, produced for the destination, or both. The + Assembler makes use of this information in linking producers + of a destination with its consumers. + + The value of the message-destination-usage element must be + one of the following: + Consumes + Produces + ConsumesProduces + + + + + + + + + + + + + + + + + + + javax.jms.Queue + + + ]]> + + + + + + + + + + + + + + + The message-destination-linkType is used to link a message + destination reference or message-driven bean to a message + destination. + + The Assembler sets the value to reflect the flow of messages + between producers and consumers in the application. + + The value must be the message-destination-name of a message + destination in the same Deployment File or in another + Deployment File in the same Java EE application unit. + + Alternatively, the value may be composed of a path name + specifying a Deployment File containing the referenced + message destination with the message-destination-name of the + destination appended and separated from the path name by + "#". The path name is relative to the Deployment File + containing Deployment Component that is referencing the + message destination. This allows multiple message + destinations with the same name to be uniquely identified. + + + + + + + + + + + + + + + + The transaction-supportType specifies the level of + transaction support provided by the resource adapter. It is + used by transaction-support elements. + + The value must be one of the following: + + NoTransaction + LocalTransaction + XATransaction + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_8.xsd b/java/jakarta/servlet/resources/javaee_8.xsd new file mode 100644 index 0000000..f071a06 --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_8.xsd @@ -0,0 +1,3105 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2017 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + The following definitions that appear in the common + shareable schema(s) of Java EE deployment descriptors should be + interpreted with respect to the context they are included: + + Deployment Component may indicate one of the following: + java ee application; + application client; + web application; + enterprise bean; + resource adapter; + + Deployment File may indicate one of the following: + ear file; + war file; + jar file; + rar file; + + + + + + + + + + + + + This group keeps the usage of the contained description related + elements consistent across Java EE deployment descriptors. + + All elements may occur multiple times with different languages, + to support localization of the content. + + + + + + + + + + + + + + + This group keeps the usage of the contained JNDI environment + reference elements consistent across Java EE deployment descriptors. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This group collects elements that are common to most + JNDI resource elements. + + + + + + + + + + The JNDI name to be looked up to resolve a resource reference. + + + + + + + + + + + + This group collects elements that are common to all the + JNDI resource elements. It does not include the lookup-name + element, that is only applicable to some resource elements. + + + + + + + + + A product specific name that this resource should be + mapped to. The name of this resource, as defined by the + resource's name element or defaulted, is a name that is + local to the application component using the resource. + (It's a name in the JNDI java:comp/env namespace.) Many + application servers provide a way to map these local + names to names of resources known to the application + server. This mapped name is often a global JNDI name, + but may be a name of any form. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + + + + + + + + Configuration of an administered object. + + + + + + + + + Description of this administered object. + + + + + + + + + The name element specifies the JNDI name of the + administered object being defined. + + + + + + + + + The administered object's interface type. + + + + + + + + + The administered object's class name. + + + + + + + + + Resource adapter name. + + + + + + + + + Property of the administered object property. This may be a + vendor-specific property. + + + + + + + + + + + + + + + + Configuration of a Connector Connection Factory resource. + + + + + + + + + Description of this resource. + + + + + + + + + The name element specifies the JNDI name of the + resource being defined. + + + + + + + + + The fully qualified class name of the connection factory + interface. + + + + + + + + + Resource adapter name. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The level of transaction support the connection factory + needs to support. + + + + + + + + + Resource property. This may be a vendor-specific + property. + + + + + + + + + + + + + + + + Configuration of a DataSource. + + + + + + + + + Description of this DataSource. + + + + + + + + + The name element specifies the JNDI name of the + data source being defined. + + + + + + + + + DataSource, XADataSource or ConnectionPoolDataSource + implementation class. + + + + + + + + + Database server name. + + + + + + + + + Port number where a server is listening for requests. + + + + + + + + + Name of a database on a server. + + + + + + + + url property is specified + along with other standard DataSource properties + such as serverName, databaseName + and portNumber, the more specific properties will + take precedence and url will be ignored. + + ]]> + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + JDBC DataSource property. This may be a vendor-specific + property or a less commonly used DataSource property. + + + + + + + + + Sets the maximum time in seconds that this data source + will wait while attempting to connect to a database. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Isolation level for connections. + + + + + + + + + Number of connections that should be created when a + connection pool is initialized. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The number of seconds that a physical connection should + remain unused in the pool before the connection is + closed for a connection pool. + + + + + + + + + The total number of statements that a connection pool + should keep open. + + + + + + + + + + + + + + + + The description type is used by a description element to + provide text describing the parent element. The elements + that use this type should include any information that the + Deployment Component's Deployment File file producer wants + to provide to the consumer of the Deployment Component's + Deployment File (i.e., to the Deployer). Typically, the + tools used by such a Deployment File consumer will display + the description when processing the parent element that + contains the description. + + The lang attribute defines the language that the + description is provided in. The default value is "en" (English). + + + + + + + + + + + + + + + This type defines a dewey decimal that is used + to describe versions of documents. + + + + + + + + + + + + + + + + Employee Self Service + + + The value of the xml:lang attribute is "en" (English) by default. + + ]]> + + + + + + + + + + + + + + + + EmployeeRecord + + ../products/product.jar#ProductEJB + + ]]> + + + + + + + + + + + + + + + The ejb-local-refType is used by ejb-local-ref elements for + the declaration of a reference to an enterprise bean's local + home or to the local business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of the Deployment + Component that's referencing the enterprise bean. + - the optional expected type of the referenced enterprise bean + - the optional expected local interface of the referenced + enterprise bean or the local business interface of the + referenced enterprise bean. + - the optional expected local home interface of the referenced + enterprise bean. Not applicable if this ejb-local-ref refers + to the local business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property. + + + + + + + + + + + + + + + + + + + + + + ejb/Payroll + + ]]> + + + + + + + + + + + + + + + The ejb-refType is used by ejb-ref elements for the + declaration of a reference to an enterprise bean's home or + to the remote business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of + the Deployment Component that's referencing the enterprise + bean. + - the optional expected type of the referenced enterprise bean + - the optional remote interface of the referenced enterprise bean + or the remote business interface of the referenced enterprise + bean + - the optional expected home interface of the referenced + enterprise bean. Not applicable if this ejb-ref + refers to the remote business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property + + + + + + + + + + + + + + + + + + + + + + + The ejb-ref-typeType contains the expected type of the + referenced enterprise bean. + + The ejb-ref-type designates a value + that must be one of the following: + + Entity + Session + + + + + + + + + + + + + + + + + + + This type is used to designate an empty + element when used. + + + + + + + + + + + + + + The env-entryType is used to declare an application's + environment entry. The declaration consists of an optional + description, the name of the environment entry, a type + (optional if the value is injected, otherwise required), and + an optional value. + + It also includes optional elements to define injection of + the named resource into fields or JavaBeans properties. + + If a value is not specified and injection is requested, + no injection will occur and no entry of the specified name + will be created. This allows an initial value to be + specified in the source code without being incorrectly + changed when no override has been specified. + + If a value is not specified and no injection is requested, + a value must be supplied during deployment. + + This type is used by env-entry elements. + + + + + + + + + minAmount + + ]]> + + + + + + + java.lang.Integer + + ]]> + + + + + + + 100.00 + + ]]> + + + + + + + + + + + + + + + java.lang.Boolean + java.lang.Class + com.example.Color + + ]]> + + + + + + + + + + + + + + + The elements that use this type designate the name of a + Java class or interface. The name is in the form of a + "binary name", as defined in the JLS. This is the form + of name used in Class.forName(). Tools that need the + canonical name (the name used in source code) will need + to convert this binary name to the canonical name. + + + + + + + + + + + + + + + + This type defines four different values which can designate + boolean values. This includes values yes and no which are + not designated by xsd:boolean + + + + + + + + + + + + + + + + + + + + + The icon type contains small-icon and large-icon elements + that specify the file names for small and large GIF, JPEG, + or PNG icon images used to represent the parent element in a + GUI tool. + + The xml:lang attribute defines the language that the + icon file names are provided in. Its value is "en" (English) + by default. + + + + + + + + employee-service-icon16x16.jpg + + ]]> + + + + + + + employee-service-icon32x32.jpg + + ]]> + + + + + + + + + + + + + + + + An injection target specifies a class and a name within + that class into which a resource should be injected. + + The injection target class specifies the fully qualified + class name that is the target of the injection. The + Java EE specifications describe which classes can be an + injection target. + + The injection target name specifies the target within + the specified class. The target is first looked for as a + JavaBeans property name. If not found, the target is + looked for as a field name. + + The specified resource will be injected into the target + during initialization of the class by either calling the + set method for the target property or by setting a value + into the named field. + + + + + + + + + + + + + + The following transaction isolation levels are allowed + (see documentation for the java.sql.Connection interface): + TRANSACTION_READ_UNCOMMITTED + TRANSACTION_READ_COMMITTED + TRANSACTION_REPEATABLE_READ + TRANSACTION_SERIALIZABLE + + + + + + + + + + + + + + + + + + + The java-identifierType defines a Java identifier. + The users of this type should further verify that + the content does not contain Java reserved keywords. + + + + + + + + + + + + + + + + + + This is a generic type that designates a Java primitive + type or a fully qualified name of a Java interface/type, + or an array of such types. + + + + + + + + + + + + + + + + + : + + Example: + + jdbc:mysql://localhost:3307/testdb + + ]]> + + + + + + + + + + + + + + + + + Configuration of a JMS Connection Factory. + + + + + + + + + Description of this JMS Connection Factory. + + + + + + + + + The name element specifies the JNDI name of the + JMS connection factory being defined. + + + + + + + + + Fully-qualified name of the JMS connection factory + interface. Permitted values are javax.jms.ConnectionFactory, + javax.jms.QueueConnectionFactory, or + javax.jms.TopicConnectionFactory. If not specified, + javax.jms.ConnectionFactory will be used. + + + + + + + + + Fully-qualified name of the JMS connection factory + implementation class. Ignored if a resource adapter + is used. + + + + + + + + + Resource adapter name. If not specified, the application + server will define the default behavior, which may or may + not involve the use of a resource adapter. + + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + Client id to use for connection. + + + + + + + + + JMS Connection Factory property. This may be a vendor-specific + property or a less commonly used ConnectionFactory property. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + + + + + + + + Configuration of a JMS Destination. + + + + + + + + + Description of this JMS Destination. + + + + + + + + + The name element specifies the JNDI name of the + JMS destination being defined. + + + + + + + + + Fully-qualified name of the JMS destination interface. + Permitted values are javax.jms.Queue and javax.jms.Topic + + + + + + + + + Fully-qualified name of the JMS destination implementation + class. Ignored if a resource adapter is used unless the + resource adapter defines more than one destination implementation + class for the specified interface. + + + + + + + + + Resource adapter name. If not specified, the application + server will define the default behavior, which may or may + not involve the use of a resource adapter. + + + + + + + + + Name of the queue or topic. + + + + + + + + + JMS Destination property. This may be a vendor-specific + property or a less commonly used Destination property. + + + + + + + + + + + + + + + + The jndi-nameType type designates a JNDI name in the + Deployment Component's environment and is relative to the + java:comp/env context. A JNDI name must be unique within the + Deployment Component. + + + + + + + + + + + + + + + com.aardvark.payroll.PayrollHome + + ]]> + + + + + + + + + + + + + + + The lifecycle-callback type specifies a method on a + class to be called when a lifecycle event occurs. + Note that each class may have only one lifecycle callback + method for any given event and that the method may not + be overloaded. + + If the lifefycle-callback-class element is missing then + the class defining the callback is assumed to be the + component class in scope at the place in the descriptor + in which the callback definition appears. + + + + + + + + + + + + + + + + + The listenerType indicates the deployment properties for a web + application listener bean. + + + + + + + + + + The listener-class element declares a class in the + application must be registered as a web + application listener bean. The value is the fully + qualified classname of the listener class. + + + + + + + + + + + + + + + + The localType defines the fully-qualified name of an + enterprise bean's local interface. + + + + + + + + + + + + + + + + The local-homeType defines the fully-qualified + name of an enterprise bean's local home interface. + + + + + + + + + + + + + + + + Configuration of a Mail Session resource. + + + + + + + + + Description of this Mail Session resource. + + + + + + + + + The name element specifies the JNDI name of the + Mail Session resource being defined. + + + + + + + + + Storage protocol. + + + + + + + + + Service provider store protocol implementation class + + + + + + + + + Transport protocol. + + + + + + + + + Service provider transport protocol implementation class + + + + + + + + + Mail server host name. + + + + + + + + + Mail server user name. + + + + + + + + + Password. + + + + + + + + + Email address to indicate the message sender. + + + + + + + + + Mail server property. This may be a vendor-specific + property. + + + + + + + + + + + + + + + + This type is a general type that can be used to declare + parameter/value lists. + + + + + + + + + + The param-name element contains the name of a + parameter. + + + + + + + + + The param-value element contains the value of a + parameter. + + + + + + + + + + + + + + + + The elements that use this type designate either a relative + path or an absolute path starting with a "/". + + In elements that specify a pathname to a file within the + same Deployment File, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the Deployment File's namespace. Absolute filenames (i.e., + those starting with "/") also specify names in the root of + the Deployment File's namespace. In general, relative names + are preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + myPersistenceContext + + + + + myPersistenceContext + + PersistenceUnit1 + + Extended + + + ]]> + + + + + + + + + The persistence-context-ref-name element specifies + the name of a persistence context reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + Used to specify properties for the container or persistence + provider. Vendor-specific properties may be included in + the set of properties. Properties that are not recognized + by a vendor must be ignored. Entries that make use of the + namespace javax.persistence and its subnamespaces must not + be used for vendor-specific properties. The namespace + javax.persistence is reserved for use by the specification. + + + + + + + + + + + + + + + + + The persistence-context-synchronizationType specifies + whether a container-managed persistence context is automatically + synchronized with the current transaction. + + The value of the persistence-context-synchronization element + must be one of the following: + Synchronized + Unsynchronized + + + + + + + + + + + + + + + + + + + The persistence-context-typeType specifies the transactional + nature of a persistence context reference. + + The value of the persistence-context-type element must be + one of the following: + Transaction + Extended + + + + + + + + + + + + + + + + + + + Specifies a name/value pair. + + + + + + + + + + + + + + + + + + + + myPersistenceUnit + + + + + myPersistenceUnit + + PersistenceUnit1 + + + + ]]> + + + + + + + + + The persistence-unit-ref-name element specifies + the name of a persistence unit reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + + + ]]> + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in + the Deployment Component code. The name is a JNDI + name relative to the java:comp/env context and must + be unique within a Deployment Component. + + + + + + + + + The resource-env-ref-type element specifies the type + of a resource environment reference. It is the + fully qualified name of a Java language class or + interface. + + + + + + + + + + + + + + + + + jdbc/EmployeeAppDB + javax.sql.DataSource + Container + Shareable + + + ]]> + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. + The name is a JNDI name relative to the + java:comp/env context. + The name must be unique within a Deployment File. + + + + + + + + + The res-type element specifies the type of the data + source. The type is specified by the fully qualified + Java language class or interface + expected to be implemented by the data source. + + + + + + + + + + + + + + + + + + + The res-authType specifies whether the Deployment Component + code signs on programmatically to the resource manager, or + whether the Container will sign on to the resource manager + on behalf of the Deployment Component. In the latter case, + the Container uses information that is supplied by the + Deployer. + + The value must be one of the two following: + + Application + Container + + + + + + + + + + + + + + + + + + + The res-sharing-scope type specifies whether connections + obtained through the given resource manager connection + factory reference can be shared. The value, if specified, + must be one of the two following: + + Shareable + Unshareable + + The default value is Shareable. + + + + + + + + + + + + + + + + + + + The run-asType specifies the run-as identity to be + used for the execution of a component. It contains an + optional description, and the name of a security role. + + + + + + + + + + + + + + + + + + The role-nameType designates the name of a security role. + + The name must conform to the lexical rules for a token. + + + + + + + + + + + + + + + + + This role includes all employees who are authorized + to access the employee service application. + + employee + + + ]]> + + + + + + + + + + + + + + + + + The security-role-refType contains the declaration of a + security role reference in a component's or a + Deployment Component's code. The declaration consists of an + optional description, the security role name used in the + code, and an optional link to a security role. If the + security role is not specified, the Deployer must choose an + appropriate security role. + + + + + + + + + + The value of the role-name element must be the String used + as the parameter to the + EJBContext.isCallerInRole(String roleName) method or the + HttpServletRequest.isUserInRole(String role) method. + + + + + + + + + The role-link element is a reference to a defined + security role. The role-link element must contain + the name of one of the security roles defined in the + security-role elements. + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:QName. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:boolean. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:NMTOKEN. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:anyURI. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:integer. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:positiveInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:nonNegativeInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:string. + + + + + + + + + + + + + + + + + + This is a special string datatype that is defined by Java EE as + a base type for defining collapsed strings. When schemas + require trailing/leading space elimination as well as + collapsing the existing whitespace, this base type may be + used. + + + + + + + + + + + + + + + + + + This simple type designates a boolean with only two + permissible values + + - true + - false + + + + + + + + + + + + + + + + + + The url-patternType contains the url pattern of the mapping. + It must follow the rules specified in Section 11.2 of the + Servlet API Specification. This pattern is assumed to be in + URL-decoded form and must not contain CR(#xD) or LF(#xA). + If it contains those characters, the container must inform + the developer with a descriptive error message. + The container must preserve all characters including whitespaces. + + + + + + + + + + + + + + + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-name element specifies a + name for a message destination. This name must be + unique among the names of message destinations + within the Deployment File. + + + + + + + + + A product specific name that this message destination + should be mapped to. Each message-destination-ref + element that references this message destination will + define a name in the namespace of the referencing + component or in one of the other predefined namespaces. + Many application servers provide a way to map these + local names to names of resources known to the + application server. This mapped name is often a global + JNDI name, but may be a name of any form. Each of the + local names should be mapped to this same global name. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + The JNDI name to be looked up to resolve the message destination. + + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + Consumes + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in + Deployment Component code. + + + + + + + + + + + + + + + + + + + + The message-destination-usageType specifies the use of the + message destination indicated by the reference. The value + indicates whether messages are consumed from the message + destination, produced for the destination, or both. The + Assembler makes use of this information in linking producers + of a destination with its consumers. + + The value of the message-destination-usage element must be + one of the following: + Consumes + Produces + ConsumesProduces + + + + + + + + + + + + + + + + + + + javax.jms.Queue + + + ]]> + + + + + + + + + + + + + + + The message-destination-linkType is used to link a message + destination reference or message-driven bean to a message + destination. + + The Assembler sets the value to reflect the flow of messages + between producers and consumers in the application. + + The value must be the message-destination-name of a message + destination in the same Deployment File or in another + Deployment File in the same Java EE application unit. + + Alternatively, the value may be composed of a path name + specifying a Deployment File containing the referenced + message destination with the message-destination-name of the + destination appended and separated from the path name by + "#". The path name is relative to the Deployment File + containing Deployment Component that is referencing the + message destination. This allows multiple message + destinations with the same name to be uniquely identified. + + + + + + + + + + + + + + + + The transaction-supportType specifies the level of + transaction support provided by the resource adapter. It is + used by transaction-support elements. + + The value must be one of the following: + + NoTransaction + LocalTransaction + XATransaction + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_web_services_1_2.xsd b/java/jakarta/servlet/resources/javaee_web_services_1_2.xsd new file mode 100644 index 0000000..4391474 --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_web_services_1_2.xsd @@ -0,0 +1,755 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2007 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + ... + + + The instance documents may indicate the published version of the + schema using the xsi:schemaLocation attribute for the Java EE + namespace with the following location: + + http://java.sun.com/xml/ns/javaee/javaee_web_services_1_2.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The webservices element is the root element for the web services + deployment descriptor. It specifies the set of web service + descriptions that are to be deployed into the Java EE Application Server + and the dependencies they have on container resources and services. + + Used in: webservices.xml + + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and JAX-RPC mapping. The + name must be unique within the deployment descriptor. + + + + + + + + + + + + + + + The handler-chain element defines the handlerchain. + Handlerchain can be defined such that the handlers in the + handlerchain operate,all ports of a service, on a specific + port or on a list of protocol-bindings. The choice of elements + service-name-pattern, port-name-pattern and protocol-bindings + are used to specify whether the handlers in handler-chain are + for a service, port or protocol binding. If none of these + choices are specified with the handler-chain element then the + handlers specified in the handler-chain will be applied on + everything. + + + + + + + + + + + + + + + + + + + + + + + + + The handler-chains element defines the handlerchains associated + with this service or service endpoint. + + + + + + + + + + + + + + + + + + The port-component element associates a WSDL port with a web service + interface and implementation. It defines the name of the port as a + component, optional description, optional display name, optional iconic + representations, WSDL port QName, Service Endpoint Interface, Service + Implementation Bean. + + This element also associates a WSDL service with a JAX-WS Provider + implementation. + + + + + + + + + + + + EmployeeService + + + ]]> + + + + + + + + Defines the name space and local name part of the WSDL + service QName. This is required to be specified for + port components that are JAX-WS Provider implementations. + + + + + + + + + Defines the name space and local name part of the WSDL + port QName. This is not required to be specified for port + components that are JAX-WS Provider implementations + + + + + + + + + Used to enable or disable SOAP MTOM/XOP mechanism for an + endpoint implementation. + + Not to be specified for JAX-RPC runtime + + + + + + + + Used to specify the protocol binding used by the port-component. + If this element is not specified, then the default binding is + used (SOAP 1.1 over HTTP) + + + + + + + + + com.wombat.empl.EmployeeService + + This may not be specified in case there is no Service + Enpoint Interface as is the case with directly using an + implementation class with the @WebService annotation. + + When the port component is a Provider implementation + this is not specified. + + ]]> + + + + + + + + + + To be used with JAX-RPC based runtime only. + + + + + + + To be used with JAX-WS based runtime only. + + + + + + + + + + + + + + + + Declares the handler for a port-component. Handlers can access the + init-param name/value pairs using the HandlerInfo interface. + + Used in: port-component + + + + + + + + + + Defines the name of the handler. The name must be unique within the + module. + + + + + + + + + Defines a fully qualified class name for the handler implementation. + + + + + + + + + + + Defines the QName of a SOAP header that will be processed by the + handler. + + + + + + + + + The soap-role element contains a SOAP actor definition that the + Handler will play as a role. + + + + + + + + + + + + + + Defines the type that is used for specifying tokens that + start with ## which are used to alias existing standard + protocol bindings and support aliases for new standard + binding URIs that are introduced in future specifications. + + The following tokens alias the standard protocol binding + URIs: + + ##SOAP11_HTTP = "http://schemas.xmlsoap.org/wsdl/soap/http" + ##SOAP11_HTTP_MTOM = + "http://schemas.xmlsoap.org/wsdl/soap/http?mtom=true" + ##SOAP12_HTTP = "http://www.w3.org/2003/05/soap/bindings/HTTP/" + ##SOAP12_HTTP_MTOM = + "http://www.w3.org/2003/05/soap/bindings/HTTP/?mtom=true" + ##XML_HTTP = "http://www.w3.org/2004/08/wsdl/http" + + + + + + + + + + + + + + Defines the type used for specifying a list of + protocol-bindingType(s). For e.g. + + ##SOAP11_HTTP ##SOAP12_HTTP ##XML_HTTP + + + + + + + + + + + + Defines the type used for specifying the URI for the + protocol binding used by the port-component. For + portability one could use one of the following tokens that + alias the standard binding types: + + ##SOAP11_HTTP + ##SOAP11_HTTP_MTOM + ##SOAP12_HTTP + ##SOAP12_HTTP_MTOM + ##XML_HTTP + + Other specifications could define tokens that start with ## + to alias new standard binding URIs that are introduced. + + + + + + + + + + + + This is used to specify the QName pattern in the + attribute service-name-pattern and port-name-pattern in + the handler-chain element + + For example, the various forms acceptable here for + service-name-pattern attribute in handler-chain element + are : + + Exact Name: service-name-pattern="ns1:EchoService" + + In this case, handlers specified in this + handler-chain element will apply to all ports with + this exact service name. The namespace prefix must + have been declared in a namespace declaration + attribute in either the start-tag of the element + where the prefix is used or in an an ancestor + element (i.e. an element in whose content the + prefixed markup occurs) + + Pattern : service-name-pattern="ns1:EchoService*" + + In this case, handlers specified in this + handler-chain element will apply to all ports whose + Service names are like EchoService1, EchoServiceFoo + etc. The namespace prefix must have been declared in + a namespace declaration attribute in either the + start-tag of the element where the prefix is used or + in an an ancestor element (i.e. an element in whose + content the prefixed markup occurs) + + Wild Card : service-name-pattern="*" + + In this case, handlers specified in this handler-chain + element will apply to ports of all service names. + + The same can be applied to port-name attribute in + handler-chain element. + + + + + + + + + + + + + + + + + The service-impl-bean element defines the web service implementation. + A service implementation can be an EJB bean class or JAX-RPC web + component. Existing EJB implementations are exposed as a web service + using an ejb-link. + + Used in: port-component + + + + + + + + + + + + + + + + StockQuoteService + + ]]> + + + + + + + + + + + + + + The webservice-description element defines a WSDL document file + and the set of Port components associated with the WSDL ports + defined in the WSDL document. There may be multiple + webservice-descriptions defined within a module. + + All WSDL file ports must have a corresponding port-component element + defined. + + Used in: webservices + + + + + + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and JAX-RPC + mapping. The name must be unique within the deployment descriptor. + + + + + + + + + The wsdl-file element contains the name of a WSDL file in the + module. The file name is a relative path within the module. + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the JAX-RPC mapping between the Java interfaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module. + + This is not required when JAX-WS based runtime is used. + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + + + + + + + + + + + EmployeeService + + + ]]> + + + + + + + + + + + + + The required value for the version is 1.2. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_web_services_1_3.xsd b/java/jakarta/servlet/resources/javaee_web_services_1_3.xsd new file mode 100644 index 0000000..d69d723 --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_web_services_1_3.xsd @@ -0,0 +1,578 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + ... + + + The instance documents may indicate the published version of the + schema using the xsi:schemaLocation attribute for the Java EE + namespace with the following location: + + http://java.sun.com/xml/ns/javaee/javaee_web_services_1_3.xsd + +]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The webservices element is the root element for the web services + deployment descriptor. It specifies the set of web service + descriptions that are to be deployed into the Java EE Application Server + and the dependencies they have on container resources and services. + + Used in: webservices.xml + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and JAX-RPC mapping. The + name must be unique within the deployment descriptor. + + + + + + + + + + + + + + + + The port-component element associates a WSDL port with a web service + interface and implementation. It defines the name of the port as a + component, optional description, optional display name, optional iconic + representations, WSDL port QName, Service Endpoint Interface, Service + Implementation Bean. + + This element also associates a WSDL service with a JAX-WS Provider + implementation. + + + + + + + + + + +EmployeeService + + +]]> + + + + + + + + Defines the name space and local name part of the WSDL + service QName. This is required to be specified for + port components that are JAX-WS Provider implementations. + + + + + + + + + Defines the name space and local name part of the WSDL + port QName. This is not required to be specified for port + components that are JAX-WS Provider implementations + + + + + + + + + Used to enable or disable SOAP MTOM/XOP mechanism for an + endpoint implementation. + + Not to be specified for JAX-RPC runtime + + + + + + + + + When MTOM is enabled, binary data above this size in bytes + will be XOP encoded or sent as attachment. Default value is 0. + + Not to be specified for JAX-RPC runtime + + + + + + + + + This specifies the WS-Addressing requirements for a JAX-WS + web service. It corresponds to javax.xml.ws.soap.Addressing + annotation or its feature javax.xml.ws.soap.AddressingFeature. + + See the addressingType for more information. + + Not to be specified for JAX-RPC runtime + + + + + + + + + Corresponds to the javax.xml.ws.RespectBinding annotation + or its corresponding javax.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a JAX-WS + implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + Not to be specified for JAX-RPC runtime + + + + + + + + + Used to specify the protocol binding used by the port-component. + If this element is not specified, then the default binding is + used (SOAP 1.1 over HTTP) + + + + + + + +com.wombat.empl.EmployeeService + + This may not be specified in case there is no Service + Enpoint Interface as is the case with directly using an + implementation class with the @WebService annotation. + + When the port component is a Provider implementation + this is not specified. + +]]> + + + + + + + + + + To be used with JAX-RPC based runtime only. + + + + + + + + + To be used with JAX-WS based runtime only. + + + + + + + + + + + + + + + + + The service-impl-bean element defines the web service implementation. + A service implementation can be an EJB bean class or JAX-RPC web + component. Existing EJB implementations are exposed as a web service + using an ejb-link. + + Used in: port-component + + + + + + + + + + + + + + + + +StockQuoteService + +]]> + + + + + + + + + + + + + + + The webservice-description element defines a WSDL document file + and the set of Port components associated with the WSDL ports + defined in the WSDL document. There may be multiple + webservice-descriptions defined within a module. + + All WSDL file ports must have a corresponding port-component element + defined. + + Used in: webservices + + + + + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and JAX-RPC + mapping. The name must be unique within the deployment descriptor. + + + + + + + + + The wsdl-file element contains the name of a WSDL file in the + module. The file name is a relative path within the module. + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the JAX-RPC mapping between the Java interfaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module. + + This is not required when JAX-WS based runtime is used. + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + + + + + + + + + + + +EmployeeService + + + +]]> + + + + + + + + + + + + The required value for the version is 1.3. + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_web_services_1_4.xsd b/java/jakarta/servlet/resources/javaee_web_services_1_4.xsd new file mode 100644 index 0000000..e32c5b7 --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_web_services_1_4.xsd @@ -0,0 +1,579 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2013 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + ... + + + The instance documents may indicate the published version of the + schema using the xsi:schemaLocation attribute for the Java EE + namespace with the following location: + + http://xmlns.jcp.org/xml/ns/javaee/javaee_web_services_1_4.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The webservices element is the root element for the web services + deployment descriptor. It specifies the set of web service + descriptions that are to be deployed into the Java EE Application Server + and the dependencies they have on container resources and services. + + Used in: webservices.xml + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and JAX-RPC mapping. The + name must be unique within the deployment descriptor. + + + + + + + + + + + + + + + + The port-component element associates a WSDL port with a web service + interface and implementation. It defines the name of the port as a + component, optional description, optional display name, optional iconic + representations, WSDL port QName, Service Endpoint Interface, Service + Implementation Bean. + + This element also associates a WSDL service with a JAX-WS Provider + implementation. + + + + + + + + + + + EmployeeService + + + ]]> + + + + + + + + Defines the name space and local name part of the WSDL + service QName. This is required to be specified for + port components that are JAX-WS Provider implementations. + + + + + + + + + Defines the name space and local name part of the WSDL + port QName. This is not required to be specified for port + components that are JAX-WS Provider implementations + + + + + + + + + Used to enable or disable SOAP MTOM/XOP mechanism for an + endpoint implementation. + + Not to be specified for JAX-RPC runtime + + + + + + + + + When MTOM is enabled, binary data above this size in bytes + will be XOP encoded or sent as attachment. Default value is 0. + + Not to be specified for JAX-RPC runtime + + + + + + + + + This specifies the WS-Addressing requirements for a JAX-WS + web service. It corresponds to javax.xml.ws.soap.Addressing + annotation or its feature javax.xml.ws.soap.AddressingFeature. + + See the addressingType for more information. + + Not to be specified for JAX-RPC runtime + + + + + + + + + Corresponds to the javax.xml.ws.RespectBinding annotation + or its corresponding javax.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a JAX-WS + implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + Not to be specified for JAX-RPC runtime + + + + + + + + + Used to specify the protocol binding used by the port-component. + If this element is not specified, then the default binding is + used (SOAP 1.1 over HTTP) + + + + + + + + com.wombat.empl.EmployeeService + + This may not be specified in case there is no Service + Enpoint Interface as is the case with directly using an + implementation class with the @WebService annotation. + + When the port component is a Provider implementation + this is not specified. + + ]]> + + + + + + + + + + To be used with JAX-RPC based runtime only. + + + + + + + + + To be used with JAX-WS based runtime only. + + + + + + + + + + + + + + + + + The service-impl-bean element defines the web service implementation. + A service implementation can be an EJB bean class or JAX-RPC web + component. Existing EJB implementations are exposed as a web service + using an ejb-link. + + Used in: port-component + + + + + + + + + + + + + + + + + StockQuoteService + + ]]> + + + + + + + + + + + + + + + The webservice-description element defines a WSDL document file + and the set of Port components associated with the WSDL ports + defined in the WSDL document. There may be multiple + webservice-descriptions defined within a module. + + All WSDL file ports must have a corresponding port-component element + defined. + + Used in: webservices + + + + + + + + + + + + The webservice-description-name identifies the collection of + port-components associated with a WSDL file and JAX-RPC + mapping. The name must be unique within the deployment descriptor. + + + + + + + + + The wsdl-file element contains the name of a WSDL file in the + module. The file name is a relative path within the module. + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the JAX-RPC mapping between the Java interfaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module. + + This is not required when JAX-WS based runtime is used. + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + + + + + + + + + + + + EmployeeService + + + + + ]]> + + + + + + + + + + + + The required value for the version is 1.4. + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_web_services_client_1_2.xsd b/java/jakarta/servlet/resources/javaee_web_services_client_1_2.xsd new file mode 100644 index 0000000..e95308a --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_web_services_client_1_2.xsd @@ -0,0 +1,586 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2007 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + + The service-endpoint-interface element defines a fully qualified + Java class that represents the Service Endpoint Interface of a + WSDL port. + + + + + + + + + + Used to enable or disable SOAP MTOM/XOP mechanism on the client + side for a port-component. + + Not to be specified for JAX-RPC runtime + + + + + + + + + The port-component-link element links a port-component-ref + to a specific port-component required to be made available + by a service reference. + + The value of a port-component-link must be the + port-component-name of a port-component in the same module + or another module in the same application unit. The syntax + for specification follows the syntax defined for ejb-link + in the EJB 2.0 specification. + + + + + + + + + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + + + + + + + The service-ref element declares a reference to a Web + service. It contains optional description, display name and + icons, a declaration of the required Service interface, + an optional WSDL document location, an optional set + of JAX-RPC mappings, an optional QName for the service element, + an optional set of Service Endpoint Interfaces to be resolved + by the container to a WSDL port, and an optional set of handlers. + + + + + + + + + + + The service-ref-name element declares logical name that the + components in the module use to look up the Web service. It + is recommended that all service reference names start with + "service/". + + + + + + + + + + The service-interface element declares the fully qualified class + name of the JAX-RPC Service interface the client depends on. + In most cases the value will be javax.xml.rpc.Service. A JAX-RPC + generated Service Interface class may also be specified. + + + + + + + + + + The service-ref-type element declares the type of the service-ref + element that is injected or returned when a JNDI lookup is done. + This must be either a fully qualified name of Service class or + the fully qualified name of service endpoint interface class. + This is only used with JAX-WS runtime where the corresponding + @WebServiceRef annotation can be used to denote both a Service + or a Port. + + If this is not specified, then the type of service-ref element + that is injected or returned when a JNDI lookup is done is + always a Service interface/class. + + + + + + + + + The wsdl-file element contains the URI location of a WSDL + file. The location is relative to the root of the module. + + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the JAX-RPC mapping between the Java interfaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module file. + + This is not required when JAX-WS based runtime is used. + + + + + + + + + + The service-qname element declares the specific WSDL service + element that is being refered to. It is not specified if no + wsdl-file is declared. + + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + + + Declares the handler for a port-component. Handlers can + access the init-param name/value pairs using the + HandlerInfo interface. If port-name is not specified, the + handler is assumed to be associated with all ports of the + service. + + To be used with JAX-RPC based runtime only. + + + + + + + + To be used with JAX-WS based runtime only. + + + + + + + + + + + + + + + + + + The handler-chain element defines the handlerchain. + Handlerchain can be defined such that the handlers in the + handlerchain operate,all ports of a service, on a specific + port or on a list of protocol-bindings. The choice of elements + service-name-pattern, port-name-pattern and protocol-bindings + are used to specify whether the handlers in handler-chain are + for a service, port or protocol binding. If none of these + choices are specified with the handler-chain element then the + handlers specified in the handler-chain will be applied on + everything. + + + + + + + + + + + + + + + + + + + + + + + + + The handler-chains element defines the handlerchains associated with this + service or service endpoint. + + + + + + + + + + + + + + + + + + Declares the handler for a port-component. Handlers can access the + init-param name/value pairs using the HandlerInfo interface. If + port-name is not specified, the handler is assumed to be associated + with all ports of the service. + + Used in: service-ref + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + Defines a fully qualified class name for the handler + implementation. + + + + + + + + + + + Defines the QName of a SOAP header that will be processed + by the handler. + + + + + + + + + + The soap-role element contains a SOAP actor definition that + the Handler will play as a role. + + + + + + + + + + The port-name element defines the WSDL port-name that a + handler should be associated with. + + + + + + + + + + + + + + Defines the type that is used for specifying tokens that + start with ## which are used to alias existing standard + protocol bindings and support aliases for new standard + binding URIs that are introduced in future specifications. + + The following tokens alias the standard protocol binding + URIs: + + ##SOAP11_HTTP = "http://schemas.xmlsoap.org/wsdl/soap/http" + ##SOAP11_HTTP_MTOM = + "http://schemas.xmlsoap.org/wsdl/soap/http?mtom=true" + ##SOAP12_HTTP = "http://www.w3.org/2003/05/soap/bindings/HTTP/" + ##SOAP12_HTTP_MTOM = + "http://www.w3.org/2003/05/soap/bindings/HTTP/?mtom=true" + ##XML_HTTP = "http://www.w3.org/2004/08/wsdl/http" + + + + + + + + + + + + + + Defines the type used for specifying a list of + protocol-bindingType(s). For e.g. + + ##SOAP11_HTTP ##SOAP12_HTTP ##XML_HTTP + + + + + + + + + + + + Defines the type used for specifying the URI for the + protocol binding used by the port-component. For + portability one could use one of the following tokens that + alias the standard binding types: + + ##SOAP11_HTTP + ##SOAP11_HTTP_MTOM + ##SOAP12_HTTP + ##SOAP12_HTTP_MTOM + ##XML_HTTP + + Other specifications could define tokens that start with ## + to alias new standard binding URIs that are introduced. + + + + + + + + + + + + This is used to specify the QName pattern in the + attribute service-name-pattern and port-name-pattern in + the handler-chain element + + For example, the various forms acceptable here for + service-name-pattern attribute in handler-chain element + are : + + Exact Name: service-name-pattern="ns1:EchoService" + + In this case, handlers specified in this + handler-chain element will apply to all ports with + this exact service name. The namespace prefix must + have been declared in a namespace declaration + attribute in either the start-tag of the element + where the prefix is used or in an an ancestor + element (i.e. an element in whose content the + prefixed markup occurs) + + Pattern : service-name-pattern="ns1:EchoService*" + + In this case, handlers specified in this + handler-chain element will apply to all ports whose + Service names are like EchoService1, EchoServiceFoo + etc. The namespace prefix must have been declared in + a namespace declaration attribute in either the + start-tag of the element where the prefix is used or + in an an ancestor element (i.e. an element in whose + content the prefixed markup occurs) + + Wild Card : service-name-pattern="*" + + In this case, handlers specified in this handler-chain + element will apply to ports of all service names. + + The same can be applied to port-name attribute in + handler-chain element. + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_web_services_client_1_3.xsd b/java/jakarta/servlet/resources/javaee_web_services_client_1_3.xsd new file mode 100644 index 0000000..b0d0985 --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_web_services_client_1_3.xsd @@ -0,0 +1,744 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + + + + + The service-ref element declares a reference to a Web + service. It contains optional description, display name and + icons, a declaration of the required Service interface, + an optional WSDL document location, an optional set + of JAX-RPC mappings, an optional QName for the service element, + an optional set of Service Endpoint Interfaces to be resolved + by the container to a WSDL port, and an optional set of handlers. + + + + + + + + + + The service-ref-name element declares logical name that the + components in the module use to look up the Web service. It + is recommended that all service reference names start with + "service/". + + + + + + + + + The service-interface element declares the fully qualified class + name of the JAX-RPC Service interface the client depends on. + In most cases the value will be javax.xml.rpc.Service. A JAX-RPC + generated Service Interface class may also be specified. + + + + + + + + + The service-ref-type element declares the type of the service-ref + element that is injected or returned when a JNDI lookup is done. + This must be either a fully qualified name of Service class or + the fully qualified name of service endpoint interface class. + This is only used with JAX-WS runtime where the corresponding + @WebServiceRef annotation can be used to denote both a Service + or a Port. + + If this is not specified, then the type of service-ref element + that is injected or returned when a JNDI lookup is done is + always a Service interface/class. + + + + + + + + + The wsdl-file element contains the URI location of a WSDL + file. The location is relative to the root of the module. + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the JAX-RPC mapping between the Java interfaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module file. + + This is not required when JAX-WS based runtime is used. + + + + + + + + + The service-qname element declares the specific WSDL service + element that is being refered to. It is not specified if no + wsdl-file is declared. + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + + Declares the handler for a port-component. Handlers can + access the init-param name/value pairs using the + HandlerInfo interface. If port-name is not specified, the + handler is assumed to be associated with all ports of the + service. + + To be used with JAX-RPC based runtime only. + + + + + + + + + To be used with JAX-WS based runtime only. + + + + + + + + + + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + The service-endpoint-interface element defines a fully qualified + Java class that represents the Service Endpoint Interface of a + WSDL port. + + + + + + + + + Used to enable or disable SOAP MTOM/XOP mechanism on the client + side for a port-component. + + Not to be specified for JAX-RPC runtime + + + + + + + + + When MTOM is enabled, binary data above this size in bytes + should be XOP encoded or sent as attachment. Default value is 0. + + Not to be specified for JAX-RPC runtime + + + + + + + + + This specifies the WS-Addressing requirements for a JAX-WS + web service. It corresponds to javax.xml.ws.soap.Addressing + annotation or its feature javax.xml.ws.soap.AddressingFeature. + + See the addressingType for more information. + + Not to be specified for JAX-RPC runtime + + + + + + + + + Corresponds to the javax.xml.ws.RespectBinding annotation + or its corresponding javax.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a JAX-WS + implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + Not to be specified for JAX-RPC runtime + + + + + + + + + The port-component-link element links a port-component-ref + to a specific port-component required to be made available + by a service reference. + + The value of a port-component-link must be the + port-component-name of a port-component in the same module + or another module in the same application unit. The syntax + for specification follows the syntax defined for ejb-link + in the EJB 2.0 specification. + + + + + + + + + + + + + + + + The handler-chains element defines the handlerchains associated with this + service or service endpoint. + + + + + + + + + + + + + + + + + The handler-chain element defines the handlerchain. + Handlerchain can be defined such that the handlers in the + handlerchain operate,all ports of a service, on a specific + port or on a list of protocol-bindings. The choice of elements + service-name-pattern, port-name-pattern and protocol-bindings + are used to specify whether the handlers in handler-chain are + for a service, port or protocol binding. If none of these + choices are specified with the handler-chain element then the + handlers specified in the handler-chain will be applied on + everything. + + + + + + + + + + + + + + + + + + + Defines the type used for specifying a list of + protocol-bindingType(s). For e.g. + + ##SOAP11_HTTP ##SOAP12_HTTP ##XML_HTTP + + + + + + + + + + + Defines the type used for specifying the URI for the + protocol binding used by the port-component. For + portability one could use one of the following tokens that + alias the standard binding types: + + ##SOAP11_HTTP + ##SOAP11_HTTP_MTOM + ##SOAP12_HTTP + ##SOAP12_HTTP_MTOM + ##XML_HTTP + + Other specifications could define tokens that start with ## + to alias new standard binding URIs that are introduced. + + + + + + + + + + + Defines the type that is used for specifying tokens that + start with ## which are used to alias existing standard + protocol bindings and support aliases for new standard + binding URIs that are introduced in future specifications. + + The following tokens alias the standard protocol binding + URIs: + + ##SOAP11_HTTP = "http://schemas.xmlsoap.org/wsdl/soap/http" + ##SOAP11_HTTP_MTOM = + "http://schemas.xmlsoap.org/wsdl/soap/http?mtom=true" + ##SOAP12_HTTP = "http://www.w3.org/2003/05/soap/bindings/HTTP/" + ##SOAP12_HTTP_MTOM = + "http://www.w3.org/2003/05/soap/bindings/HTTP/?mtom=true" + ##XML_HTTP = "http://www.w3.org/2004/08/wsdl/http" + + + + + + + + + + + + + This is used to specify the QName pattern in the + attribute service-name-pattern and port-name-pattern in + the handler-chain element + + For example, the various forms acceptable here for + service-name-pattern attribute in handler-chain element + are : + + Exact Name: service-name-pattern="ns1:EchoService" + + In this case, handlers specified in this + handler-chain element will apply to all ports with + this exact service name. The namespace prefix must + have been declared in a namespace declaration + attribute in either the start-tag of the element + where the prefix is used or in an an ancestor + element (i.e. an element in whose content the + prefixed markup occurs) + + + Pattern : service-name-pattern="ns1:EchoService*" + + In this case, handlers specified in this + handler-chain element will apply to all ports whose + Service names are like EchoService1, EchoServiceFoo + etc. The namespace prefix must have been declared in + a namespace declaration attribute in either the + start-tag of the element where the prefix is used or + in an an ancestor element (i.e. an element in whose + content the prefixed markup occurs) + + Wild Card : service-name-pattern="*" + + In this case, handlers specified in this handler-chain + element will apply to ports of all service names. + + The same can be applied to port-name attribute in + handler-chain element. + + + + + + + + + + + + + + + + This specifies the WS-Addressing requirements for a JAX-WS web service. + It corresponds to javax.xml.ws.soap.Addressing annotation or its + feature javax.xml.ws.soap.AddressingFeature. + + If the "enabled" element is "true", WS-Addressing is enabled. + It means that the endpoint supports WS-Addressing but does not require + its use. The default value for "enabled" is "true". + + If the WS-Addressing is enabled and the "required" element is "true", + it means that the endpoint requires WS-Addressing. The default value + for "required" is "false". + + If WS-Addressing is enabled, the "responses" element determines + if an endpoint requires the use of only anonymous responses, + or only non-anonymous responses, or all. The value of the "responses" + element must be one of the following: + + ANONYMOUS + NON_ANONYMOUS + ALL + + The default value for the "responses" is ALL. + + + + + + + + + + + + + + + + + + If WS-Addressing is enabled, this type determines if an endpoint + requires the use of only anonymous responses, or only non-anonymous + responses, or all. + + + + + + + + + + + + + + + + + + + + Corresponds to the javax.xml.ws.RespectBinding annotation + or its corresponding javax.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a JAX-WS + implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + If the "enabled" element is "true", wsdl:binding in the + associated WSDL, if any, must be respected/honored. + + + + + + + + + + + + + + + + Declares the handler for a port-component, service-ref. Handlers can + access the init-param name/value pairs using the HandlerInfo interface. + + Used in: port-component, service-ref + + + + + + + + + + Defines the name of the handler. The name must be unique within the + module. + + + + + + + + + Defines a fully qualified class name for the handler implementation. + + + + + + + + + Not to be specified for JAX-WS runtime + + + + + + + + + Defines the QName of a SOAP header that will be processed by the + handler. + + Not to be specified for JAX-WS runtime + + + + + + + + + The soap-role element contains a SOAP actor definition that the + Handler will play as a role. + + + + + + + + + The port-name element defines the WSDL port-name that a + handler should be associated with. If port-name is not + specified, the handler is assumed to be associated with + all ports of the service. + + Not to be specified for JAX-WS runtime + + + + + + + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/javaee_web_services_client_1_4.xsd b/java/jakarta/servlet/resources/javaee_web_services_client_1_4.xsd new file mode 100644 index 0000000..fbd00ce --- /dev/null +++ b/java/jakarta/servlet/resources/javaee_web_services_client_1_4.xsd @@ -0,0 +1,744 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2013 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + (C) Copyright International Business Machines Corporation 2002 + + + + + + + + + + + + The service-ref element declares a reference to a Web + service. It contains optional description, display name and + icons, a declaration of the required Service interface, + an optional WSDL document location, an optional set + of JAX-RPC mappings, an optional QName for the service element, + an optional set of Service Endpoint Interfaces to be resolved + by the container to a WSDL port, and an optional set of handlers. + + + + + + + + + + The service-ref-name element declares logical name that the + components in the module use to look up the Web service. It + is recommended that all service reference names start with + "service/". + + + + + + + + + The service-interface element declares the fully qualified class + name of the JAX-RPC Service interface the client depends on. + In most cases the value will be javax.xml.rpc.Service. A JAX-RPC + generated Service Interface class may also be specified. + + + + + + + + + The service-ref-type element declares the type of the service-ref + element that is injected or returned when a JNDI lookup is done. + This must be either a fully qualified name of Service class or + the fully qualified name of service endpoint interface class. + This is only used with JAX-WS runtime where the corresponding + @WebServiceRef annotation can be used to denote both a Service + or a Port. + + If this is not specified, then the type of service-ref element + that is injected or returned when a JNDI lookup is done is + always a Service interface/class. + + + + + + + + + The wsdl-file element contains the URI location of a WSDL + file. The location is relative to the root of the module. + + + + + + + + + The jaxrpc-mapping-file element contains the name of a file that + describes the JAX-RPC mapping between the Java interfaces used by + the application and the WSDL description in the wsdl-file. The + file name is a relative path within the module file. + + This is not required when JAX-WS based runtime is used. + + + + + + + + + The service-qname element declares the specific WSDL service + element that is being refered to. It is not specified if no + wsdl-file is declared. + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + + Declares the handler for a port-component. Handlers can + access the init-param name/value pairs using the + HandlerInfo interface. If port-name is not specified, the + handler is assumed to be associated with all ports of the + service. + + To be used with JAX-RPC based runtime only. + + + + + + + + + To be used with JAX-WS based runtime only. + + + + + + + + + + + + + + + + + + The port-component-ref element declares a client dependency + on the container for resolving a Service Endpoint Interface + to a WSDL port. It optionally associates the Service Endpoint + Interface with a particular port-component. This is only used + by the container for a Service.getPort(Class) method call. + + + + + + + + + The service-endpoint-interface element defines a fully qualified + Java class that represents the Service Endpoint Interface of a + WSDL port. + + + + + + + + + Used to enable or disable SOAP MTOM/XOP mechanism on the client + side for a port-component. + + Not to be specified for JAX-RPC runtime + + + + + + + + + When MTOM is enabled, binary data above this size in bytes + should be XOP encoded or sent as attachment. Default value is 0. + + Not to be specified for JAX-RPC runtime + + + + + + + + + This specifies the WS-Addressing requirements for a JAX-WS + web service. It corresponds to javax.xml.ws.soap.Addressing + annotation or its feature javax.xml.ws.soap.AddressingFeature. + + See the addressingType for more information. + + Not to be specified for JAX-RPC runtime + + + + + + + + + Corresponds to the javax.xml.ws.RespectBinding annotation + or its corresponding javax.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a JAX-WS + implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + Not to be specified for JAX-RPC runtime + + + + + + + + + The port-component-link element links a port-component-ref + to a specific port-component required to be made available + by a service reference. + + The value of a port-component-link must be the + port-component-name of a port-component in the same module + or another module in the same application unit. The syntax + for specification follows the syntax defined for ejb-link + in the EJB 2.0 specification. + + + + + + + + + + + + + + + + The handler-chains element defines the handlerchains associated with this + service or service endpoint. + + + + + + + + + + + + + + + + + The handler-chain element defines the handlerchain. + Handlerchain can be defined such that the handlers in the + handlerchain operate,all ports of a service, on a specific + port or on a list of protocol-bindings. The choice of elements + service-name-pattern, port-name-pattern and protocol-bindings + are used to specify whether the handlers in handler-chain are + for a service, port or protocol binding. If none of these + choices are specified with the handler-chain element then the + handlers specified in the handler-chain will be applied on + everything. + + + + + + + + + + + + + + + + + + + Defines the type used for specifying a list of + protocol-bindingType(s). For e.g. + + ##SOAP11_HTTP ##SOAP12_HTTP ##XML_HTTP + + + + + + + + + + + Defines the type used for specifying the URI for the + protocol binding used by the port-component. For + portability one could use one of the following tokens that + alias the standard binding types: + + ##SOAP11_HTTP + ##SOAP11_HTTP_MTOM + ##SOAP12_HTTP + ##SOAP12_HTTP_MTOM + ##XML_HTTP + + Other specifications could define tokens that start with ## + to alias new standard binding URIs that are introduced. + + + + + + + + + + + Defines the type that is used for specifying tokens that + start with ## which are used to alias existing standard + protocol bindings and support aliases for new standard + binding URIs that are introduced in future specifications. + + The following tokens alias the standard protocol binding + URIs: + + ##SOAP11_HTTP = "http://schemas.xmlsoap.org/wsdl/soap/http" + ##SOAP11_HTTP_MTOM = + "http://schemas.xmlsoap.org/wsdl/soap/http?mtom=true" + ##SOAP12_HTTP = "http://www.w3.org/2003/05/soap/bindings/HTTP/" + ##SOAP12_HTTP_MTOM = + "http://www.w3.org/2003/05/soap/bindings/HTTP/?mtom=true" + ##XML_HTTP = "http://www.w3.org/2004/08/wsdl/http" + + + + + + + + + + + + + This is used to specify the QName pattern in the + attribute service-name-pattern and port-name-pattern in + the handler-chain element + + For example, the various forms acceptable here for + service-name-pattern attribute in handler-chain element + are : + + Exact Name: service-name-pattern="ns1:EchoService" + + In this case, handlers specified in this + handler-chain element will apply to all ports with + this exact service name. The namespace prefix must + have been declared in a namespace declaration + attribute in either the start-tag of the element + where the prefix is used or in an an ancestor + element (i.e. an element in whose content the + prefixed markup occurs) + + + Pattern : service-name-pattern="ns1:EchoService*" + + In this case, handlers specified in this + handler-chain element will apply to all ports whose + Service names are like EchoService1, EchoServiceFoo + etc. The namespace prefix must have been declared in + a namespace declaration attribute in either the + start-tag of the element where the prefix is used or + in an an ancestor element (i.e. an element in whose + content the prefixed markup occurs) + + Wild Card : service-name-pattern="*" + + In this case, handlers specified in this handler-chain + element will apply to ports of all service names. + + The same can be applied to port-name attribute in + handler-chain element. + + + + + + + + + + + + + + + + This specifies the WS-Addressing requirements for a JAX-WS web service. + It corresponds to javax.xml.ws.soap.Addressing annotation or its + feature javax.xml.ws.soap.AddressingFeature. + + If the "enabled" element is "true", WS-Addressing is enabled. + It means that the endpoint supports WS-Addressing but does not require + its use. The default value for "enabled" is "true". + + If the WS-Addressing is enabled and the "required" element is "true", + it means that the endpoint requires WS-Addressing. The default value + for "required" is "false". + + If WS-Addressing is enabled, the "responses" element determines + if an endpoint requires the use of only anonymous responses, + or only non-anonymous responses, or all. The value of the "responses" + element must be one of the following: + + ANONYMOUS + NON_ANONYMOUS + ALL + + The default value for the "responses" is ALL. + + + + + + + + + + + + + + + + + + If WS-Addressing is enabled, this type determines if an endpoint + requires the use of only anonymous responses, or only non-anonymous + responses, or all. + + + + + + + + + + + + + + + + + + + + Corresponds to the javax.xml.ws.RespectBinding annotation + or its corresponding javax.xml.ws.RespectBindingFeature web + service feature. This is used to control whether a JAX-WS + implementation must respect/honor the contents of the + wsdl:binding in the WSDL that is associated with the service. + + If the "enabled" element is "true", wsdl:binding in the + associated WSDL, if any, must be respected/honored. + + + + + + + + + + + + + + + + Declares the handler for a port-component, service-ref. Handlers can + access the init-param name/value pairs using the HandlerInfo interface. + + Used in: port-component, service-ref + + + + + + + + + + Defines the name of the handler. The name must be unique within the + module. + + + + + + + + + Defines a fully qualified class name for the handler implementation. + + + + + + + + + Not to be specified for JAX-WS runtime + + + + + + + + + Defines the QName of a SOAP header that will be processed by the + handler. + + Not to be specified for JAX-WS runtime + + + + + + + + + The soap-role element contains a SOAP actor definition that the + Handler will play as a role. + + + + + + + + + The port-name element defines the WSDL port-name that a + handler should be associated with. If port-name is not + specified, the handler is assumed to be associated with + all ports of the service. + + Not to be specified for JAX-WS runtime + + + + + + + + + + + + + + + + Defines the name of the handler. The name must be unique + within the module. + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jsp_2_0.xsd b/java/jakarta/servlet/resources/jsp_2_0.xsd new file mode 100644 index 0000000..2f9a269 --- /dev/null +++ b/java/jakarta/servlet/resources/jsp_2_0.xsd @@ -0,0 +1,282 @@ + + + + + + + This is the XML Schema for the JSP 2.0 deployment descriptor + types. The JSP 2.0 schema contains all the special + structures and datatypes that are necessary to use JSP files + from a web application. + + The contents of this schema is used by the web-app_2_4.xsd + file to define JSP specific content. + + + + + + + + The following conventions apply to all J2EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The jsp-configType is used to provide global configuration + information for the JSP files in a web application. It has + two subelements, taglib and jsp-property-group. + + + + + + + + + + + + + + + + + + The jsp-file element contains the full path to a JSP file + within the web application beginning with a `/'. + + + + + + + + + + + + + + + + The jsp-property-groupType is used to group a number of + files so they can be given global property information. + All files so described are deemed to be JSP files. The + following additional properties can be described: + + - Control whether EL is ignored + - Control whether scripting elements are invalid + - Indicate pageEncoding information. + - Indicate that a resource is a JSP document (XML) + - Prelude and Coda automatic includes. + + + + + + + + + + + + Can be used to easily set the isELIgnored + property of a group of JSP pages. By default, the + EL evaluation is enabled for Web Applications using + a Servlet 2.4 or greater web.xml, and disabled + otherwise. + + + + + + + + + The valid values of page-encoding are those of the + pageEncoding page directive. It is a + translation-time error to name different encodings + in the pageEncoding attribute of the page directive + of a JSP page and in a JSP configuration element + matching the page. It is also a translation-time + error to name different encodings in the prolog + or text declaration of a document in XML syntax and + in a JSP configuration element matching the document. + It is legal to name the same encoding through + multiple mechanisms. + + + + + + + + + Can be used to easily disable scripting in a + group of JSP pages. By default, scripting is + enabled. + + + + + + + + + If true, denotes that the group of resources + that match the URL pattern are JSP documents, + and thus must be interpreted as XML documents. + If false, the resources are assumed to not + be JSP documents, unless there is another + property group that indicates otherwise. + + + + + + + + + The include-prelude element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the beginning of each + JSP page in this jsp-property-group. + + + + + + + + + The include-coda element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the end of each + JSP page in this jsp-property-group. + + + + + + + + + + + + + + + The taglibType defines the syntax for declaring in + the deployment descriptor that a tag library is + available to the application. This can be done + to override implicit map entries from TLD files and + from the container. + + + + + + + + + + A taglib-uri element describes a URI identifying a + tag library used in the web application. The body + of the taglib-uri element may be either an + absolute URI specification, or a relative URI. + There should be no entries in web.xml with the + same taglib-uri value. + + + + + + + + + + the taglib-location element contains the location + (as a resource relative to the root of the web + application) where to find the Tag Library + Description file for the tag library. + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jsp_2_1.xsd b/java/jakarta/servlet/resources/jsp_2_1.xsd new file mode 100644 index 0000000..e58edba --- /dev/null +++ b/java/jakarta/servlet/resources/jsp_2_1.xsd @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jsp_2_2.xsd b/java/jakarta/servlet/resources/jsp_2_2.xsd new file mode 100644 index 0000000..bdae5b5 --- /dev/null +++ b/java/jakarta/servlet/resources/jsp_2_2.xsd @@ -0,0 +1,398 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + This is the XML Schema for the JSP 2.2 deployment descriptor + types. The JSP 2.2 schema contains all the special + structures and datatypes that are necessary to use JSP files + from a web application. + + The contents of this schema is used by the web-common_3_0.xsd + file to define JSP specific content. + + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The jsp-configType is used to provide global configuration + information for the JSP files in a web application. It has + two subelements, taglib and jsp-property-group. + + + + + + + + + + + + + + + + + + The jsp-file element contains the full path to a JSP file + within the web application beginning with a `/'. + + + + + + + + + + + + + + + + The jsp-property-groupType is used to group a number of + files so they can be given global property information. + All files so described are deemed to be JSP files. The + following additional properties can be described: + + - Control whether EL is ignored. + - Control whether scripting elements are invalid. + - Indicate pageEncoding information. + - Indicate that a resource is a JSP document (XML). + - Prelude and Coda automatic includes. + - Control whether the character sequence #{ is allowed + when used as a String literal. + - Control whether template text containing only + whitespaces must be removed from the response output. + - Indicate the default contentType information. + - Indicate the default buffering model for JspWriter + - Control whether error should be raised for the use of + undeclared namespaces in a JSP page. + + + + + + + + + + + Can be used to easily set the isELIgnored + property of a group of JSP pages. By default, the + EL evaluation is enabled for Web Applications using + a Servlet 2.4 or greater web.xml, and disabled + otherwise. + + + + + + + + + The valid values of page-encoding are those of the + pageEncoding page directive. It is a + translation-time error to name different encodings + in the pageEncoding attribute of the page directive + of a JSP page and in a JSP configuration element + matching the page. It is also a translation-time + error to name different encodings in the prolog + or text declaration of a document in XML syntax and + in a JSP configuration element matching the document. + It is legal to name the same encoding through + multiple mechanisms. + + + + + + + + + Can be used to easily disable scripting in a + group of JSP pages. By default, scripting is + enabled. + + + + + + + + + If true, denotes that the group of resources + that match the URL pattern are JSP documents, + and thus must be interpreted as XML documents. + If false, the resources are assumed to not + be JSP documents, unless there is another + property group that indicates otherwise. + + + + + + + + + The include-prelude element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the beginning of each + JSP page in this jsp-property-group. + + + + + + + + + The include-coda element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the end of each + JSP page in this jsp-property-group. + + + + + + + + + The character sequence #{ is reserved for EL expressions. + Consequently, a translation error occurs if the #{ + character sequence is used as a String literal, unless + this element is enabled (true). Disabled (false) by + default. + + + + + + + + + Indicates that template text containing only whitespaces + must be removed from the response output. It has no + effect on JSP documents (XML syntax). Disabled (false) + by default. + + + + + + + + + The valid values of default-content-type are those of the + contentType page directive. It specifies the default + response contentType if the page directive does not include + a contentType attribute. + + + + + + + + + The valid values of buffer are those of the + buffer page directive. It specifies if buffering should be + used for the output to response, and if so, the size of the + buffer to use. + + + + + + + + + The default behavior when a tag with unknown namespace is used + in a JSP page (regular syntax) is to silently ignore it. If + set to true, then an error must be raised during the translation + time when an undeclared tag is used in a JSP page. Disabled + (false) by default. + + + + + + + + + + + + + + + + The taglibType defines the syntax for declaring in + the deployment descriptor that a tag library is + available to the application. This can be done + to override implicit map entries from TLD files and + from the container. + + + + + + + + + A taglib-uri element describes a URI identifying a + tag library used in the web application. The body + of the taglib-uri element may be either an + absolute URI specification, or a relative URI. + There should be no entries in web.xml with the + same taglib-uri value. + + + + + + + + + the taglib-location element contains the location + (as a resource relative to the root of the web + application) where to find the Tag Library + Description file for the tag library. + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jsp_2_3.xsd b/java/jakarta/servlet/resources/jsp_2_3.xsd new file mode 100644 index 0000000..5bab43f --- /dev/null +++ b/java/jakarta/servlet/resources/jsp_2_3.xsd @@ -0,0 +1,396 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2013 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + This is the XML Schema for the JSP 2.3 deployment descriptor + types. The JSP 2.3 schema contains all the special + structures and datatypes that are necessary to use JSP files + from a web application. + + The contents of this schema is used by the web-common_3_1.xsd + file to define JSP specific content. + + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The jsp-configType is used to provide global configuration + information for the JSP files in a web application. It has + two subelements, taglib and jsp-property-group. + + + + + + + + + + + + + + + + + + The jsp-file element contains the full path to a JSP file + within the web application beginning with a `/'. + + + + + + + + + + + + + + + + The jsp-property-groupType is used to group a number of + files so they can be given global property information. + All files so described are deemed to be JSP files. The + following additional properties can be described: + + - Control whether EL is ignored. + - Control whether scripting elements are invalid. + - Indicate pageEncoding information. + - Indicate that a resource is a JSP document (XML). + - Prelude and Coda automatic includes. + - Control whether the character sequence #{ is allowed + when used as a String literal. + - Control whether template text containing only + whitespaces must be removed from the response output. + - Indicate the default contentType information. + - Indicate the default buffering model for JspWriter + - Control whether error should be raised for the use of + undeclared namespaces in a JSP page. + + + + + + + + + + + Can be used to easily set the isELIgnored + property of a group of JSP pages. By default, the + EL evaluation is enabled for Web Applications using + a Servlet 2.4 or greater web.xml, and disabled + otherwise. + + + + + + + + + The valid values of page-encoding are those of the + pageEncoding page directive. It is a + translation-time error to name different encodings + in the pageEncoding attribute of the page directive + of a JSP page and in a JSP configuration element + matching the page. It is also a translation-time + error to name different encodings in the prolog + or text declaration of a document in XML syntax and + in a JSP configuration element matching the document. + It is legal to name the same encoding through + multiple mechanisms. + + + + + + + + + Can be used to easily disable scripting in a + group of JSP pages. By default, scripting is + enabled. + + + + + + + + + If true, denotes that the group of resources + that match the URL pattern are JSP documents, + and thus must be interpreted as XML documents. + If false, the resources are assumed to not + be JSP documents, unless there is another + property group that indicates otherwise. + + + + + + + + + The include-prelude element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the beginning of each + JSP page in this jsp-property-group. + + + + + + + + + The include-coda element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the end of each + JSP page in this jsp-property-group. + + + + + + + + + The character sequence #{ is reserved for EL expressions. + Consequently, a translation error occurs if the #{ + character sequence is used as a String literal, unless + this element is enabled (true). Disabled (false) by + default. + + + + + + + + + Indicates that template text containing only whitespaces + must be removed from the response output. It has no + effect on JSP documents (XML syntax). Disabled (false) + by default. + + + + + + + + + The valid values of default-content-type are those of the + contentType page directive. It specifies the default + response contentType if the page directive does not include + a contentType attribute. + + + + + + + + + The valid values of buffer are those of the + buffer page directive. It specifies if buffering should be + used for the output to response, and if so, the size of the + buffer to use. + + + + + + + + + The default behavior when a tag with unknown namespace is used + in a JSP page (regular syntax) is to silently ignore it. If + set to true, then an error must be raised during the translation + time when an undeclared tag is used in a JSP page. Disabled + (false) by default. + + + + + + + + + + + + + + + + The taglibType defines the syntax for declaring in + the deployment descriptor that a tag library is + available to the application. This can be done + to override implicit map entries from TLD files and + from the container. + + + + + + + + + A taglib-uri element describes a URI identifying a + tag library used in the web application. The body + of the taglib-uri element may be either an + absolute URI specification, or a relative URI. + There should be no entries in web.xml with the + same taglib-uri value. + + + + + + + + + the taglib-location element contains the location + (as a resource relative to the root of the web + application) where to find the Tag Library + Description file for the tag library. + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jsp_3_0.xsd b/java/jakarta/servlet/resources/jsp_3_0.xsd new file mode 100644 index 0000000..4bb8639 --- /dev/null +++ b/java/jakarta/servlet/resources/jsp_3_0.xsd @@ -0,0 +1,365 @@ + + + + + + Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + This is the XML Schema for the JSP 3.0 deployment descriptor + types. The JSP 3.0 schema contains all the special + structures and datatypes that are necessary to use JSP files + from a web application. + + The contents of this schema is used by the web-common_3_1.xsd + file to define JSP specific content. + + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The jsp-configType is used to provide global configuration + information for the JSP files in a web application. It has + two subelements, taglib and jsp-property-group. + + + + + + + + + + + + + + + + + + The jsp-file element contains the full path to a JSP file + within the web application beginning with a `/'. + + + + + + + + + + + + + + + + The jsp-property-groupType is used to group a number of + files so they can be given global property information. + All files so described are deemed to be JSP files. The + following additional properties can be described: + + - Control whether EL is ignored. + - Control whether scripting elements are invalid. + - Indicate pageEncoding information. + - Indicate that a resource is a JSP document (XML). + - Prelude and Coda automatic includes. + - Control whether the character sequence #{ is allowed + when used as a String literal. + - Control whether template text containing only + whitespaces must be removed from the response output. + - Indicate the default contentType information. + - Indicate the default buffering model for JspWriter + - Control whether error should be raised for the use of + undeclared namespaces in a JSP page. + + + + + + + + + + + Can be used to easily set the isELIgnored + property of a group of JSP pages. By default, the + EL evaluation is enabled for Web Applications using + a Servlet 2.4 or greater web.xml, and disabled + otherwise. + + + + + + + + + The valid values of page-encoding are those of the + pageEncoding page directive. It is a + translation-time error to name different encodings + in the pageEncoding attribute of the page directive + of a JSP page and in a JSP configuration element + matching the page. It is also a translation-time + error to name different encodings in the prolog + or text declaration of a document in XML syntax and + in a JSP configuration element matching the document. + It is legal to name the same encoding through + mulitple mechanisms. + + + + + + + + + Can be used to easily disable scripting in a + group of JSP pages. By default, scripting is + enabled. + + + + + + + + + If true, denotes that the group of resources + that match the URL pattern are JSP documents, + and thus must be interpreted as XML documents. + If false, the resources are assumed to not + be JSP documents, unless there is another + property group that indicates otherwise. + + + + + + + + + The include-prelude element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the beginning of each + JSP page in this jsp-property-group. + + + + + + + + + The include-coda element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the end of each + JSP page in this jsp-property-group. + + + + + + + + + The character sequence #{ is reserved for EL expressions. + Consequently, a translation error occurs if the #{ + character sequence is used as a String literal, unless + this element is enabled (true). Disabled (false) by + default. + + + + + + + + + Indicates that template text containing only whitespaces + must be removed from the response output. It has no + effect on JSP documents (XML syntax). Disabled (false) + by default. + + + + + + + + + The valid values of default-content-type are those of the + contentType page directive. It specifies the default + response contentType if the page directive does not include + a contentType attribute. + + + + + + + + + The valid values of buffer are those of the + buffer page directive. It specifies if buffering should be + used for the output to response, and if so, the size of the + buffer to use. + + + + + + + + + The default behavior when a tag with unknown namespace is used + in a JSP page (regular syntax) is to silently ignore it. If + set to true, then an error must be raised during the translation + time when an undeclared tag is used in a JSP page. Disabled + (false) by default. + + + + + + + + + + + + + + + + The taglibType defines the syntax for declaring in + the deployment descriptor that a tag library is + available to the application. This can be done + to override implicit map entries from TLD files and + from the container. + + + + + + + + + A taglib-uri element describes a URI identifying a + tag library used in the web application. The body + of the taglib-uri element may be either an + absolute URI specification, or a relative URI. + There should be no entries in web.xml with the + same taglib-uri value. + + + + + + + + + the taglib-location element contains the location + (as a resource relative to the root of the web + application) where to find the Tag Library + Description file for the tag library. + + + + + + + + + diff --git a/java/jakarta/servlet/resources/jsp_3_1.xsd b/java/jakarta/servlet/resources/jsp_3_1.xsd new file mode 100644 index 0000000..4e896ca --- /dev/null +++ b/java/jakarta/servlet/resources/jsp_3_1.xsd @@ -0,0 +1,378 @@ + + + + + + Copyright (c) 2009, 2021 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + This is the XML Schema for the JSP 3.1 deployment descriptor + types. The JSP 3.1 schema contains all the special + structures and datatypes that are necessary to use JSP files + from a web application. + + The contents of this schema is used by the web-common_6_0.xsd + file to define JSP specific content. + + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The jsp-configType is used to provide global configuration + information for the JSP files in a web application. It has + two subelements, taglib and jsp-property-group. + + + + + + + + + + + + + + + + + + The jsp-file element contains the full path to a JSP file + within the web application beginning with a `/'. + + + + + + + + + + + + + + + + The jsp-property-groupType is used to group a number of + files so they can be given global property information. + All files so described are deemed to be JSP files. The + following additional properties can be described: + + - Control whether EL is ignored. + - Control whether scripting elements are invalid. + - Indicate pageEncoding information. + - Indicate that a resource is a JSP document (XML). + - Prelude and Coda automatic includes. + - Control whether the character sequence #{ is allowed + when used as a String literal. + - Control whether template text containing only + whitespaces must be removed from the response output. + - Indicate the default contentType information. + - Indicate the default buffering model for JspWriter + - Control whether error should be raised for the use of + undeclared namespaces in a JSP page. + + + + + + + + + + + Can be used to easily set the isELIgnored + property of a group of JSP pages. By default, the + EL evaluation is enabled for Web Applications using + a Servlet 2.4 or greater web.xml, and disabled + otherwise. + + + + + + + + + Can be used to easily set the errorOnELNotFound + property of a group of JSP pages. By default, this + property is false. + + + + + + + + + The valid values of page-encoding are those of the + pageEncoding page directive. It is a + translation-time error to name different encodings + in the pageEncoding attribute of the page directive + of a JSP page and in a JSP configuration element + matching the page. It is also a translation-time + error to name different encodings in the prolog + or text declaration of a document in XML syntax and + in a JSP configuration element matching the document. + It is legal to name the same encoding through + mulitple mechanisms. + + + + + + + + + Can be used to easily disable scripting in a + group of JSP pages. By default, scripting is + enabled. + + + + + + + + + If true, denotes that the group of resources + that match the URL pattern are JSP documents, + and thus must be interpreted as XML documents. + If false, the resources are assumed to not + be JSP documents, unless there is another + property group that indicates otherwise. + + + + + + + + + The include-prelude element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the beginning of each + JSP page in this jsp-property-group. + + + + + + + + + The include-coda element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the end of each + JSP page in this jsp-property-group. + + + + + + + + + The character sequence #{ is reserved for EL expressions. + Consequently, a translation error occurs if the #{ + character sequence is used as a String literal, unless + this element is enabled (true). Disabled (false) by + default. + + + + + + + + + Indicates that template text containing only whitespaces + must be removed from the response output. It has no + effect on JSP documents (XML syntax). Disabled (false) + by default. + + + + + + + + + The valid values of default-content-type are those of the + contentType page directive. It specifies the default + response contentType if the page directive does not include + a contentType attribute. + + + + + + + + + The valid values of buffer are those of the + buffer page directive. It specifies if buffering should be + used for the output to response, and if so, the size of the + buffer to use. + + + + + + + + + The default behavior when a tag with unknown namespace is used + in a JSP page (regular syntax) is to silently ignore it. If + set to true, then an error must be raised during the translation + time when an undeclared tag is used in a JSP page. Disabled + (false) by default. + + + + + + + + + + + + + + + + The taglibType defines the syntax for declaring in + the deployment descriptor that a tag library is + available to the application. This can be done + to override implicit map entries from TLD files and + from the container. + + + + + + + + + A taglib-uri element describes a URI identifying a + tag library used in the web application. The body + of the taglib-uri element may be either an + absolute URI specification, or a relative URI. + There should be no entries in web.xml with the + same taglib-uri value. + + + + + + + + + the taglib-location element contains the location + (as a resource relative to the root of the web + application) where to find the Tag Library + Description file for the tag library. + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_2_2.dtd b/java/jakarta/servlet/resources/web-app_2_2.dtd new file mode 100644 index 0000000..cc99a90 --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_2_2.dtd @@ -0,0 +1,581 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_2_3.dtd b/java/jakarta/servlet/resources/web-app_2_3.dtd new file mode 100644 index 0000000..06bd9d2 --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_2_3.dtd @@ -0,0 +1,1003 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_2_4.xsd b/java/jakarta/servlet/resources/web-app_2_4.xsd new file mode 100644 index 0000000..cee5b5a --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_2_4.xsd @@ -0,0 +1,1208 @@ + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for J2EE + namespace with the following location: + + http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd + + ]]> + + + + + + + The following conventions apply to all J2EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + The web-app element is the root of the deployment + descriptor for a web application. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurrence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + + The ejb-local-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + The auth-constraintType indicates the user roles that + should be permitted access to this resource + collection. The role-name used here must either correspond + to the role-name of one of the security-role elements + defined for this web application, or be the specially + reserved role-name "*" that is a compact syntax for + indicating all roles in the web application. If both "*" + and rolenames appear, the container interprets this as all + roles. If no roles are defined, no user is allowed access + to the portion of the web application described by the + containing security-constraint. The container matches + role names case sensitively when determining access. + + + + + + + + + + + + + + + + + + The auth-methodType is used to configure the authentication + mechanism for the web application. As a prerequisite to + gaining access to any web resources which are protected by + an authorization constraint, a user must have authenticated + using the configured mechanism. Legal values are "BASIC", + "DIGEST", "FORM", "CLIENT-CERT", or a vendor-specific + authentication scheme. + + Used in: login-config + + + + + + + + + + + + + + + + The dispatcher has four legal values: FORWARD, REQUEST, INCLUDE, + and ERROR. A value of FORWARD means the Filter will be applied + under RequestDispatcher.forward() calls. A value of REQUEST + means the Filter will be applied under ordinary client calls to + the path or servlet. A value of INCLUDE means the Filter will be + applied under RequestDispatcher.include() calls. A value of + ERROR means the Filter will be applied under the error page + mechanism. The absence of any dispatcher elements in a + filter-mapping indicates a default of applying filters only under + ordinary client calls to the path or servlet. + + + + + + + + + + + + + + + + + + + + + The encodingType defines IANA character sets. + + + + + + + + + + + + + + + + The error-code contains an HTTP error code, ex: 404 + + Used in: error-page + + + + + + + + + + + + + + + + + + + The error-pageType contains a mapping between an error code + or exception type to the path of a resource in the web + application. + + Used in: web-app + + + + + + + + + + + + + The exception-type contains a fully qualified class + name of a Java exception type. + + + + + + + + + + + The location element contains the location of the + resource in the web application relative to the root of + the web application. The value of the location must have + a leading `/'. + + + + + + + + + + + + + + + Declaration of the filter mappings in this web + application is done by using filter-mappingType. + The container uses the filter-mapping + declarations to decide which filters to apply to a request, + and in what order. The container matches the request URI to + a Servlet in the normal way. To determine which filters to + apply it matches filter-mapping declarations either on + servlet-name, or on url-pattern for each filter-mapping + element, depending on which style is used. The order in + which filters are invoked is the order in which + filter-mapping declarations that match a request URI for a + servlet appear in the list of filter-mapping elements. The + filter-name value must be the value of the filter-name + sub-elements of one of the filter declarations in the + deployment descriptor. + + + + + + + + + + + + + + + + + + + + + + The logical name of the filter is declare + by using filter-nameType. This name is used to map the + filter. Each filter name is unique within the web + application. + + Used in: filter, filter-mapping + + + + + + + + + + + + + + + + The filterType is used to declare a filter in the web + application. The filter is mapped to either a servlet or a + URL pattern in the filter-mapping element, using the + filter-name value to reference. Filters can access the + initialization parameters declared in the deployment + descriptor at runtime via the FilterConfig interface. + + Used in: web-app + + + + + + + + + + + + The fully qualified classname of the filter. + + + + + + + + + + The init-param element contains a name/value pair as + an initialization param of a servlet filter + + + + + + + + + + + + + + + The form-login-configType specifies the login and error + pages that should be used in form based login. If form based + authentication is not used, these elements are ignored. + + Used in: login-config + + + + + + + + + + + The form-login-page element defines the location in the web + app where the page that can be used for login can be + found. The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + + The form-error-page element defines the location in + the web app where the error page that is displayed + when login is not successful can be found. + The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + + + + + + + + + The http-method contains an HTTP method recognized by the + web-app, for example GET, POST, ... + + + + + + + + + + + + + + + + + + + + + + + + The locale-encoding-mapping-list contains one or more + locale-encoding-mapping(s). + + + + + + + + + + + + + + + + + The locale-encoding-mapping contains locale name and + encoding name. The locale name must be either "Language-code", + such as "ja", defined by ISO-639 or "Language-code_Country-code", + such as "ja_JP". "Country code" is defined by ISO-3166. + + + + + + + + + + + + + + + + + + The localeType defines valid locale defined by ISO-639-1 + and ISO-3166. + + + + + + + + + + + + + + + + The login-configType is used to configure the authentication + method that should be used, the realm name that should be + used for this application, and the attributes that are + needed by the form login mechanism. + + Used in: web-app + + + + + + + + + + + The realm name element specifies the realm name to + use in HTTP Basic authorization. + + + + + + + + + + + + + + + + The mime-mappingType defines a mapping between an extension + and a mime type. + + Used in: web-app + + + + + + + + + The extension element contains a string describing an + extension. example: "txt" + + + + + + + + + + + + + + + + + The mime-typeType is used to indicate a defined mime type. + + Example: + "text/plain" + + Used in: mime-mapping + + + + + + + + + + + + + + + + + This type defines a string which contains at least one + character. + + + + + + + + + + + + + + + + The security-constraintType is used to associate + security constraints with one or more web resource + collections + + Used in: web-app + + + + + + + + + + + + + + + + + + + + The servlet-mappingType defines a mapping between a + servlet and a url pattern. + + Used in: web-app + + + + + + + + + + + + + + + + + + The servlet-name element contains the canonical name of the + servlet. Each servlet name is unique within the web + application. + + + + + + + + + + + + + + + + The servletType is used to declare a servlet. + It contains the declarative data of a + servlet. If a jsp-file is specified and the load-on-startup + element is present, then the JSP should be pre-compiled and + loaded. + + Used in: web-app + + + + + + + + + + + + + The servlet-class element contains the fully + qualified class name of the servlet. + + + + + + + + + + + + + + + The load-on-startup element indicates that this + servlet should be loaded (instantiated and have + its init() called) on the start-up of the web + application. The optional contents of these + element must be an integer indicating the order in + which the servlet should be loaded. If the value + is a negative integer, or the element is not + present, the container is free to load the servlet + whenever it chooses. If the value is a positive + integer or 0, the container must load and + initialize the servlet as the application is + deployed. The container must guarantee that + servlets marked with lower integers are loaded + before servlets marked with higher integers. The + container may choose the order of loading of + servlets with the same load-on-start-up value. + + + + + + + + + + + + + + + + + The session-configType defines the session parameters + for this web application. + + Used in: web-app + + + + + + + + + + The session-timeout element defines the default + session timeout interval for all sessions created + in this web application. The specified timeout + must be expressed in a whole number of minutes. + If the timeout is 0 or less, the container ensures + the default behaviour of sessions is never to time + out. If this element is not specified, the container + must set its default timeout period. + + + + + + + + + + + + + + + The transport-guaranteeType specifies that the communication + between client and server should be NONE, INTEGRAL, or + CONFIDENTIAL. NONE means that the application does not + require any transport guarantees. A value of INTEGRAL means + that the application requires that the data sent between the + client and server be sent in such a way that it can't be + changed in transit. CONFIDENTIAL means that the application + requires that the data be transmitted in a fashion that + prevents other entities from observing the contents of the + transmission. In most cases, the presence of the INTEGRAL or + CONFIDENTIAL flag will indicate that the use of SSL is + required. + + Used in: user-data-constraint + + + + + + + + + + + + + + + + + + + + The user-data-constraintType is used to indicate how + data communicated between the client and container should be + protected. + + Used in: security-constraint + + + + + + + + + + + + + + + + + + The elements that use this type designate a path starting + with a "/" and interpreted relative to the root of a WAR + file. + + + + + + + + + + + + + + + + + This type contains the recognized versions of + web-application supported. It is used to designate the + version of the web application. + + + + + + + + + + + + + + + + + + + + + The context-param element contains the declaration + of a web application's servlet context + initialization parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The web-resource-collectionType is used to identify a subset + of the resources and HTTP methods on those resources within + a web application to which a security constraint applies. If + no HTTP methods are specified, then the security constraint + applies to all HTTP methods. + + Used in: security-constraint + + + + + + + + + + The web-resource-name contains the name of this web + resource collection. + + + + + + + + + + + + + + + + + + The welcome-file-list contains an ordered list of welcome + files elements. + + Used in: web-app + + + + + + + + + + The welcome-file element contains file name to use + as a default welcome file, such as index.html + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_2_5.xsd b/java/jakarta/servlet/resources/web-app_2_5.xsd new file mode 100644 index 0000000..72a0f6a --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_2_5.xsd @@ -0,0 +1,945 @@ + + + + + + + ... + +The instance documents may indicate the published version of +the schema using the xsi:schemaLocation attribute for Java EE +namespace with the following location: +http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd +]]> + + + + + The following conventions apply to all Java EE deployment + descriptor elements unless indicated otherwise. - In + elements that specify a pathname to a file within the same + JAR file, relative filenames (i.e., those not starting with + "/") are considered relative to the root of the JAR file's + namespace. Absolute filenames (i.e., those starting with + "/") also specify names in the root of the JAR file's + namespace. In general, relative names are preferred. The + exception is .war files where absolute names are preferred + for consistency with the Servlet API. + + + + + + + + + The web-app element is the root of the deployment + descriptor for a web application. Note that the + sub-elements of this element can be in the arbitrary + order. Because of that, the multiplicity of the elements + of distributable, session-config, welcome-file-list, + jsp-config, login-config, and + locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor + instance file must not contain multiple elements of + session-config, jsp-config, and login-config. When there + are multiple elements of welcome-file-list or + locale-encoding-mapping-list, the container must + concatenate the element contents. The multiple occurrence + of the element distributable is redundant and the + container treats that case exactly in the same way when + there is only one distributable. + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + The ejb-local-ref-name element contains the name of + an EJB reference. The EJB reference is an entry in + the web application's environment and is relative to + the java:comp/env context. The name must be unique + within the web application. It is recommended that + name is prefixed with "ejb/". + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique + within the web application. It is recommended that + name is prefixed with "ejb/". + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in the web + application code. The name is a JNDI name relative + to the java:comp/env context and must be unique + within a web application. + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in the web + application code. The name is a JNDI name relative + to the java:comp/env context and must be unique + within a web application. + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The + name is a JNDI name relative to the java:comp/env + context. The name must be unique within a web + application. + + + + + + + + + The env-entry-name element contains the name of a + web application's environment entry. The name is a + JNDI name relative to the java:comp/env context. The + name must be unique within a web application. + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + The auth-constraintType indicates the user roles that + should be permitted access to this resource collection. + The role-name used here must either correspond to the + role-name of one of the security-role elements defined + for this web application, or be the specially reserved + role-name "*" that is a compact syntax for indicating + all roles in the web application. If both "*" and + rolenames appear, the container interprets this as all + roles. If no roles are defined, no user is allowed + access to the portion of the web application described + by the containing security-constraint. The container + matches role names case sensitively when determining + access. + + + + + + + + + + + + + The auth-methodType is used to configure the + authentication mechanism for the web application. As a + prerequisite to gaining access to any web resources + which are protected by an authorization constraint, a + user must have authenticated using the configured + mechanism. Legal values are "BASIC", "DIGEST", "FORM", + "CLIENT-CERT", or a vendor-specific authentication + scheme. Used in: login-config + + + + + + + + + + + The dispatcher has four legal values: FORWARD, REQUEST, + INCLUDE, and ERROR. A value of FORWARD means the Filter + will be applied under RequestDispatcher.forward() calls. + A value of REQUEST means the Filter will be applied + under ordinary client calls to the path or servlet. A + value of INCLUDE means the Filter will be applied under + RequestDispatcher.include() calls. A value of ERROR + means the Filter will be applied under the error page + mechanism. The absence of any dispatcher elements in a + filter-mapping indicates a default of applying filters + only under ordinary client calls to the path or servlet. + + + + + + + + + + + + + + + + The encodingType defines IANA character sets. + + + + + + + + + + + The error-code contains an HTTP error code, ex: 404 Used + in: error-page + + + + + + + + + + + + + + The error-pageType contains a mapping between an error + code or exception type to the path of a resource in the + web application. Used in: web-app + + + + + + + + + The exception-type contains a fully + qualified class name of a Java exception + type. + + + + + + + + The location element contains the location of + the resource in the web application relative to + the root of the web application. The value of + the location must have a leading `/'. + + + + + + + + + + + Declaration of the filter mappings in this web + application is done by using filter-mappingType. The + container uses the filter-mapping declarations to decide + which filters to apply to a request, and in what order. + The container matches the request URI to a Servlet in + the normal way. To determine which filters to apply it + matches filter-mapping declarations either on + servlet-name, or on url-pattern for each filter-mapping + element, depending on which style is used. The order in + which filters are invoked is the order in which + filter-mapping declarations that match a request URI for + a servlet appear in the list of filter-mapping + elements. The filter-name value must be the value of the + filter-name sub-elements of one of the filter + declarations in the deployment descriptor. + + + + + + + + + + + + + + + + + The logical name of the filter is declare by using + filter-nameType. This name is used to map the filter. + Each filter name is unique within the web application. + Used in: filter, filter-mapping + + + + + + + + + + + The filterType is used to declare a filter in the web + application. The filter is mapped to either a servlet or + a URL pattern in the filter-mapping element, using the + filter-name value to reference. Filters can access the + initialization parameters declared in the deployment + descriptor at runtime via the FilterConfig interface. + Used in: web-app + + + + + + + + + The fully qualified classname of the filter. + + + + + + + The init-param element contains a name/value + pair as an initialization param of a servlet + filter + + + + + + + + + + + The form-login-configType specifies the login and error + pages that should be used in form based login. If form + based authentication is not used, these elements are + ignored. Used in: login-config + + + + + + + The form-login-page element defines the location + in the web app where the page that can be used + for login can be found. The path begins with a + leading / and is interpreted relative to the + root of the WAR. + + + + + + + The form-error-page element defines the location + in the web app where the error page that is + displayed when login is not successful can be + found. The path begins with a leading / and is + interpreted relative to the root of the WAR. + + + + + + + + + + + An HTTP method type as defined in HTTP 1.1 section 2.2. + + + + + + + + + + + + + + + + The locale-encoding-mapping-list contains one or more + locale-encoding-mapping(s). + + + + + + + + + + + + The locale-encoding-mapping contains locale name and + encoding name. The locale name must be either + "Language-code", such as "ja", defined by ISO-639 or + "Language-code_Country-code", such as "ja_JP". "Country + code" is defined by ISO-3166. + + + + + + + + + + + + + The localeType defines valid locale defined by ISO-639-1 + and ISO-3166. + + + + + + + + + + + The login-configType is used to configure the + authentication method that should be used, the realm + name that should be used for this application, and the + attributes that are needed by the form login mechanism. + Used in: web-app + + + + + + + + The realm name element specifies the realm name + to use in HTTP Basic authorization. + + + + + + + + + + + + The mime-mappingType defines a mapping between an + extension and a mime type. Used in: web-app + + + + + + The extension element contains a string describing + an extension. example: "txt" + + + + + + + + + + + + The mime-typeType is used to indicate a defined mime + type. Example: "text/plain" Used in: mime-mapping + + + + + + + + + + + + + This type defines a string which contains at least one + character. + + + + + + + + + + + + + + + + + + + The security-constraintType is used to associate + security constraints with one or more web resource + collections Used in: web-app + + + + + + + + + + + + + + + The servlet-mappingType defines a mapping between a + servlet and a url pattern. Used in: web-app + + + + + + + + + + + + + The servlet-name element contains the canonical name of + the servlet. Each servlet name is unique within the web + application. + + + + + + + + + + + The servletType is used to declare a servlet. It + contains the declarative data of a servlet. If a + jsp-file is specified and the load-on-startup element is + present, then the JSP should be precompiled and loaded. + Used in: web-app + + + + + + + + + + The servlet-class element contains the fully + qualified class name of the servlet. + + + + + + + + + + The load-on-startup element indicates that this + servlet should be loaded (instantiated and have + its init() called) on the start-up of the web + application. The optional contents of these + element must be an integer indicating the order + in which the servlet should be loaded. If the + value is a negative integer, or the element is + not present, the container is free to load the + servlet whenever it chooses. If the value is a + positive integer or 0, the container must load + and initialize the servlet as the application is + deployed. The container must guarantee that + servlets marked with lower integers are loaded + before servlets marked with higher integers. The + container may choose the order of loading of + servlets with the same load-on-start-up value. + + + + + + + + + + + + + The session-configType defines the session parameters + for this web application. + + Used in: web-app + + + + + + + The session-timeout element defines the default + session timeout interval for all sessions + created in this web application. The specified + timeout must be expressed in a whole number of + minutes. If the timeout is 0 or less, the + container ensures the default behaviour of + sessions is never to time out. If this element + is not specified, the container must set its + default timeout period. + + + + + + + + + + + The transport-guaranteeType specifies that the + communication between client and server should be NONE, + INTEGRAL, or CONFIDENTIAL. NONE means that the + application does not require any transport guarantees. A + value of INTEGRAL means that the application requires + that the data sent between the client and server be sent + in such a way that it can't be changed in transit. + CONFIDENTIAL means that the application requires that + the data be transmitted in a fashion that prevents other + entities from observing the contents of the + transmission. In most cases, the presence of the + INTEGRAL or CONFIDENTIAL flag will indicate that the use + of SSL is required. Used in: user-data-constraint + + + + + + + + + + + + + + + The user-data-constraintType is used to indicate how + data communicated between the client and container + should be protected. Used in: security-constraint + + + + + + + + + + + + + The elements that use this type designate a path + starting with a "/" and interpreted relative to the root + of a WAR file. + + + + + + + + + + + + + This type contains the recognized versions of + web-application supported. It is used to designate the + version of the web application. + + + + + + + + + + + + + + + The context-param element contains the + declaration of a web application's servlet + context initialization parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + The metadata-complete attribute defines whether this + deployment descriptor is complete, or whether the class + files of the jar file should be examined for annotations + that specify deployment information. If + metadata-complete is set to "true", the deployment tool + must ignore any Servlet annotations present in the class + files of the application. If metadata-complete is not + specified or is set to "false", the deployment tool must + examine the class files of the application for + annotations, as specified by the Servlet specifications. + + + + + + + + + The web-resource-collectionType is used to identify a + subset of the resources and HTTP methods on those + resources within a web application to which a security + constraint applies. If no HTTP methods are specified, + then the security constraint applies to all HTTP + methods. Used in: security-constraint + + + + + + + The web-resource-name contains the name of this + web resource collection. + + + + + + + + + + + + + + The welcome-file-list contains an ordered list of + welcome files elements. Used in: web-app + + + + + + + The welcome-file element contains file name to + use as a default welcome file, such as + index.html + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_3_0.xsd b/java/jakarta/servlet/resources/web-app_3_0.xsd new file mode 100644 index 0000000..01526f0 --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_3_0.xsd @@ -0,0 +1,281 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-app element is the root of the deployment + descriptor for a web application. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_3_1.xsd b/java/jakarta/servlet/resources/web-app_3_1.xsd new file mode 100644 index 0000000..84ad8b9 --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_3_1.xsd @@ -0,0 +1,333 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2013 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-app element is the root of the deployment + descriptor for a web application. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + + + + + When specified, this element causes uncovered http methods + to be denied. For every url-pattern that is the target of a + security-constraint, this element causes all HTTP methods that + are NOT covered (by a security constraint) at the url-pattern + to be denied. + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_4_0.xsd b/java/jakarta/servlet/resources/web-app_4_0.xsd new file mode 100644 index 0000000..b5cfa9e --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_4_0.xsd @@ -0,0 +1,372 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2017 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-app element is the root of the deployment + descriptor for a web application. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + + + + + When specified, this element provides a default context path + of the web application. An empty value for this element must cause + the web application to be deployed at the root for the container. + Otherwise, the default context path must start with + a “/“ character but not end with a “/“ character. + Servlet containers may provide vendor specific configuration + options that allows specifying a value that overrides the value + specified here. + + + + + + + + + When specified, this element provides a default request + character encoding of the web application. + + + + + + + + + When specified, this element provides a default response + character encoding of the web application. + + + + + + + + + When specified, this element causes uncovered http methods + to be denied. For every url-pattern that is the target of a + security-constrant, this element causes all HTTP methods that + are NOT covered (by a security constraint) at the url-pattern + to be denied. + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_5_0.xsd b/java/jakarta/servlet/resources/web-app_5_0.xsd new file mode 100644 index 0000000..de09e13 --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_5_0.xsd @@ -0,0 +1,342 @@ + + + + + + Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Jakarta EE + namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd + + ]]> + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-app element is the root of the deployment + descriptor for a web application. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an + enterprise bean reference. The enterprise + bean reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an + enterprise bean reference. The enterprise bean + reference is an entry in the web application's environment + and is relative to the java:comp/env context. + The name must be unique within the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + + + + + When specified, this element provides a default context path + of the web application. An empty value for this element must cause + the web application to be deployed at the root for the container. + Otherwise, the default context path must start with + a "/" character but not end with a "/" character. + Servlet containers may provide vendor specific configuration + options that allows specifying a value that overrides the value + specified here. + + + + + + + + + When specified, this element provides a default request + character encoding of the web application. + + + + + + + + + When specified, this element provides a default response + character encoding of the web application. + + + + + + + + + When specified, this element causes uncovered http methods + to be denied. For every url-pattern that is the target of a + security-constrant, this element causes all HTTP methods that + are NOT covered (by a security constraint) at the url-pattern + to be denied. + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-app_6_0.xsd b/java/jakarta/servlet/resources/web-app_6_0.xsd new file mode 100644 index 0000000..4cb6e8d --- /dev/null +++ b/java/jakarta/servlet/resources/web-app_6_0.xsd @@ -0,0 +1,342 @@ + + + + + + Copyright (c) 2009, 2021 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Jakarta EE + namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd + + ]]> + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-app element is the root of the deployment + descriptor for a web application. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an + enterprise bean reference. The enterprise + bean reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an + enterprise bean reference. The enterprise bean + reference is an entry in the web application's environment + and is relative to the java:comp/env context. + The name must be unique within the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + + + + + When specified, this element provides a default context path + of the web application. An empty value for this element must cause + the web application to be deployed at the root for the container. + Otherwise, the default context path must start with + a "/" character but not end with a "/" character. + Servlet containers may provide vendor specific configuration + options that allows specifying a value that overrides the value + specified here. + + + + + + + + + When specified, this element provides a default request + character encoding of the web application. + + + + + + + + + When specified, this element provides a default response + character encoding of the web application. + + + + + + + + + When specified, this element causes uncovered http methods + to be denied. For every url-pattern that is the target of a + security-constrant, this element causes all HTTP methods that + are NOT covered (by a security constraint) at the url-pattern + to be denied. + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-common_3_0.xsd b/java/jakarta/servlet/resources/web-common_3_0.xsd new file mode 100644 index 0000000..6d5d875 --- /dev/null +++ b/java/jakarta/servlet/resources/web-common_3_0.xsd @@ -0,0 +1,1584 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://java.sun.com/xml/ns/javaee/web-common_3_0.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + + The context-param element contains the declaration + of a web application's servlet context + initialization parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The metadata-complete attribute defines whether this + deployment descriptor and other related deployment + descriptors for this module (e.g., web service + descriptors) are complete, or whether the class + files available to this module and packaged with + this application should be examined for annotations + that specify deployment information. + + If metadata-complete is set to "true", the deployment + tool must ignore any annotations that specify deployment + information, which might be present in the class files + of the application. + + If metadata-complete is not specified or is set to + "false", the deployment tool must examine the class + files of the application for annotations, as + specified by the specifications. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The auth-constraintType indicates the user roles that + should be permitted access to this resource + collection. The role-name used here must either correspond + to the role-name of one of the security-role elements + defined for this web application, or be the specially + reserved role-name "*" that is a compact syntax for + indicating all roles in the web application. If both "*" + and rolenames appear, the container interprets this as all + roles. If no roles are defined, no user is allowed access + to the portion of the web application described by the + containing security-constraint. The container matches + role names case sensitively when determining access. + + + + + + + + + + + + + + + + + + The auth-methodType is used to configure the authentication + mechanism for the web application. As a prerequisite to + gaining access to any web resources which are protected by + an authorization constraint, a user must have authenticated + using the configured mechanism. Legal values are "BASIC", + "DIGEST", "FORM", "CLIENT-CERT", or a vendor-specific + authentication scheme. + + Used in: login-config + + + + + + + + + + + + + + + + The dispatcher has five legal values: FORWARD, REQUEST, + INCLUDE, ASYNC, and ERROR. + + A value of FORWARD means the Filter will be applied under + RequestDispatcher.forward() calls. + A value of REQUEST means the Filter will be applied under + ordinary client calls to the path or servlet. + A value of INCLUDE means the Filter will be applied under + RequestDispatcher.include() calls. + A value of ASYNC means the Filter will be applied under + calls dispatched from an AsyncContext. + A value of ERROR means the Filter will be applied under the + error page mechanism. + + The absence of any dispatcher elements in a filter-mapping + indicates a default of applying filters only under ordinary + client calls to the path or servlet. + + + + + + + + + + + + + + + + + + + + + + The error-code contains an HTTP error code, ex: 404 + + Used in: error-page + + + + + + + + + + + + + + + + + + + The error-pageType contains a mapping between an error code + or exception type to the path of a resource in the web + application. + + Error-page declarations using the exception-type element in + the deployment descriptor must be unique up to the class name of + the exception-type. Similarly, error-page declarations using the + status-code element must be unique in the deployment descriptor + up to the status code. + + Used in: web-app + + + + + + + + + + + The exception-type contains a fully qualified class + name of a Java exception type. + + + + + + + + + + The location element contains the location of the + resource in the web application relative to the root of + the web application. The value of the location must have + a leading `/'. + + + + + + + + + + + + + + + + The filterType is used to declare a filter in the web + application. The filter is mapped to either a servlet or a + URL pattern in the filter-mapping element, using the + filter-name value to reference. Filters can access the + initialization parameters declared in the deployment + descriptor at runtime via the FilterConfig interface. + + Used in: web-app + + + + + + + + + + + The fully qualified classname of the filter. + + + + + + + + + + The init-param element contains a name/value pair as + an initialization param of a servlet filter + + + + + + + + + + + + + + + + Declaration of the filter mappings in this web + application is done by using filter-mappingType. + The container uses the filter-mapping + declarations to decide which filters to apply to a request, + and in what order. The container matches the request URI to + a Servlet in the normal way. To determine which filters to + apply it matches filter-mapping declarations either on + servlet-name, or on url-pattern for each filter-mapping + element, depending on which style is used. The order in + which filters are invoked is the order in which + filter-mapping declarations that match a request URI for a + servlet appear in the list of filter-mapping elements.The + filter-name value must be the value of the filter-name + sub-elements of one of the filter declarations in the + deployment descriptor. + + + + + + + + + + + + + + + + + + + + + + This type defines a string which contains at least one + character. + + + + + + + + + + + + + + + + + + The logical name of the filter is declare + by using filter-nameType. This name is used to map the + filter. Each filter name is unique within the web + application. + + Used in: filter, filter-mapping + + + + + + + + + + + + + + + + The form-login-configType specifies the login and error + pages that should be used in form based login. If form based + authentication is not used, these elements are ignored. + + Used in: login-config + + + + + + + + + The form-login-page element defines the location in the web + app where the page that can be used for login can be + found. The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + The form-error-page element defines the location in + the web app where the error page that is displayed + when login is not successful can be found. + The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + + + + + An HTTP method type as defined in HTTP 1.1 section 2.2. + + + + + + + + + + + + + + + + + + + + + + + + + + The login-configType is used to configure the authentication + method that should be used, the realm name that should be + used for this application, and the attributes that are + needed by the form login mechanism. + + Used in: web-app + + + + + + + + + + The realm name element specifies the realm name to + use in HTTP Basic authorization. + + + + + + + + + + + + + + + + + The mime-mappingType defines a mapping between an extension + and a mime type. + + Used in: web-app + + + + + + + + The extension element contains a string describing an + extension. example: "txt" + + + + + + + + + + + + + + + + + The mime-typeType is used to indicate a defined mime type. + + Example: + "text/plain" + + Used in: mime-mapping + + + + + + + + + + + + + + + + + + The security-constraintType is used to associate + security constraints with one or more web resource + collections + + Used in: web-app + + + + + + + + + + + + + + + + + + + + The servletType is used to declare a servlet. + It contains the declarative data of a + servlet. If a jsp-file is specified and the load-on-startup + element is present, then the JSP should be precompiled and + loaded. + + Used in: web-app + + + + + + + + + + + + The servlet-class element contains the fully + qualified class name of the servlet. + + + + + + + + + + + + The load-on-startup element indicates that this + servlet should be loaded (instantiated and have + its init() called) on the startup of the web + application. The optional contents of these + element must be an integer indicating the order in + which the servlet should be loaded. If the value + is a negative integer, or the element is not + present, the container is free to load the servlet + whenever it chooses. If the value is a positive + integer or 0, the container must load and + initialize the servlet as the application is + deployed. The container must guarantee that + servlets marked with lower integers are loaded + before servlets marked with higher integers. The + container may choose the order of loading of + servlets with the same load-on-start-up value. + + + + + + + + + + + + + + + + + + + + + The servlet-mappingType defines a mapping between a + servlet and a url pattern. + + Used in: web-app + + + + + + + + + + + + + + + + + + The servlet-name element contains the canonical name of the + servlet. Each servlet name is unique within the web + application. + + + + + + + + + + + + + + + + The session-configType defines the session parameters + for this web application. + + Used in: web-app + + + + + + + + + The session-timeout element defines the default + session timeout interval for all sessions created + in this web application. The specified timeout + must be expressed in a whole number of minutes. + If the timeout is 0 or less, the container ensures + the default behaviour of sessions is never to time + out. If this element is not specified, the container + must set its default timeout period. + + + + + + + + + The cookie-config element defines the configuration of the + session tracking cookies created by this web application. + + + + + + + + + The tracking-mode element defines the tracking modes + for sessions created by this web application + + + + + + + + + + + + + + + + The cookie-configType defines the configuration for the + session tracking cookies of this web application. + + Used in: session-config + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as HttpOnly + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as secure + even if the request that initiated the corresponding session + is using plain HTTP instead of HTTPS + + + + + + + + + The lifetime (in seconds) that will be assigned to any + session tracking cookies created by this web application. + Default is -1 + + + + + + + + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + Used in: cookie-config + + + + + + + + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The tracking modes for sessions created by this web + application + + Used in: session-config + + + + + + + + + + + + + + + + + + + + The transport-guaranteeType specifies that the communication + between client and server should be NONE, INTEGRAL, or + CONFIDENTIAL. NONE means that the application does not + require any transport guarantees. A value of INTEGRAL means + that the application requires that the data sent between the + client and server be sent in such a way that it can't be + changed in transit. CONFIDENTIAL means that the application + requires that the data be transmitted in a fashion that + prevents other entities from observing the contents of the + transmission. In most cases, the presence of the INTEGRAL or + CONFIDENTIAL flag will indicate that the use of SSL is + required. + + Used in: user-data-constraint + + + + + + + + + + + + + + + + + + + + The user-data-constraintType is used to indicate how + data communicated between the client and container should be + protected. + + Used in: security-constraint + + + + + + + + + + + + + + + + + + The elements that use this type designate a path starting + with a "/" and interpreted relative to the root of a WAR + file. + + + + + + + + + + + + + + + This type contains the recognized versions of + web-application supported. It is used to designate the + version of the web application. + + + + + + + + + + + + + + + + The web-resource-collectionType is used to identify the + resources and HTTP methods on those resources to which a + security constraint applies. If no HTTP methods are specified, + then the security constraint applies to all HTTP methods. + If HTTP methods are specified by http-method-omission + elements, the security constraint applies to all methods + except those identified in the collection. + http-method-omission and http-method elements are never + mixed in the same collection. + + Used in: security-constraint + + + + + + + + + The web-resource-name contains the name of this web + resource collection. + + + + + + + + + + + + Each http-method names an HTTP method to which the + constraint applies. + + + + + + + + + Each http-method-omission names an HTTP method to + which the constraint does not apply. + + + + + + + + + + + + + + + + + The welcome-file-list contains an ordered list of welcome + files elements. + + Used in: web-app + + + + + + + + + The welcome-file element contains file name to use + as a default welcome file, such as index.html + + + + + + + + + + + + + The localeType defines valid locale defined by ISO-639-1 + and ISO-3166. + + + + + + + + + + + + + The encodingType defines IANA character sets. + + + + + + + + + + + + + + + + The locale-encoding-mapping-list contains one or more + locale-encoding-mapping(s). + + + + + + + + + + + + + + + + + The locale-encoding-mapping contains locale name and + encoding name. The locale name must be either "Language-code", + such as "ja", defined by ISO-639 or "Language-code_Country-code", + such as "ja_JP". "Country code" is defined by ISO-3166. + + + + + + + + + + + + + + + + + + This element indicates that the ordering sub-element in which + it was placed should take special action regarding the ordering + of this application resource relative to other application + configuration resources. + See section 8.2.2 of the specification for details. + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + This element contains a sequence of "name" elements, each of + which + refers to an application configuration resource by the "name" + declared on its web.xml fragment. This element can also contain + a single "others" element which specifies that this document + comes + before or after other documents within the application. + See section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + This element specifies configuration information related to the + handling of multipart/form-data requests. + + + + + + + + + The directory location where uploaded files will be stored + + + + + + + + + The maximum size limit of uploaded files + + + + + + + + + The maximum size limit of multipart/form-data requests + + + + + + + + + The size threshold after which an uploaded file will be + written to disk + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-common_3_1.xsd b/java/jakarta/servlet/resources/web-common_3_1.xsd new file mode 100644 index 0000000..c006c3b --- /dev/null +++ b/java/jakarta/servlet/resources/web-common_3_1.xsd @@ -0,0 +1,1481 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2013 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://xmlns.jcp.org/xml/ns/javaee/web-common_3_1.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + + The context-param element contains the declaration + of a web application's servlet context + initialization parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The metadata-complete attribute defines whether this + deployment descriptor and other related deployment + descriptors for this module (e.g., web service + descriptors) are complete, or whether the class + files available to this module and packaged with + this application should be examined for annotations + that specify deployment information. + + If metadata-complete is set to "true", the deployment + tool must ignore any annotations that specify deployment + information, which might be present in the class files + of the application. + + If metadata-complete is not specified or is set to + "false", the deployment tool must examine the class + files of the application for annotations, as + specified by the specifications. + + + + + + + + + + + + + + The auth-constraintType indicates the user roles that + should be permitted access to this resource + collection. The role-name used here must either correspond + to the role-name of one of the security-role elements + defined for this web application, or be the specially + reserved role-name "*" that is a compact syntax for + indicating all roles in the web application. If both "*" + and rolenames appear, the container interprets this as all + roles. If no roles are defined, no user is allowed access + to the portion of the web application described by the + containing security-constraint. The container matches + role names case sensitively when determining access. + + + + + + + + + + + + + + + + + + The auth-methodType is used to configure the authentication + mechanism for the web application. As a prerequisite to + gaining access to any web resources which are protected by + an authorization constraint, a user must have authenticated + using the configured mechanism. Legal values are "BASIC", + "DIGEST", "FORM", "CLIENT-CERT", or a vendor-specific + authentication scheme. + + Used in: login-config + + + + + + + + + + + + + + + + The dispatcher has five legal values: FORWARD, REQUEST, + INCLUDE, ASYNC, and ERROR. + + A value of FORWARD means the Filter will be applied under + RequestDispatcher.forward() calls. + A value of REQUEST means the Filter will be applied under + ordinary client calls to the path or servlet. + A value of INCLUDE means the Filter will be applied under + RequestDispatcher.include() calls. + A value of ASYNC means the Filter will be applied under + calls dispatched from an AsyncContext. + A value of ERROR means the Filter will be applied under the + error page mechanism. + + The absence of any dispatcher elements in a filter-mapping + indicates a default of applying filters only under ordinary + client calls to the path or servlet. + + + + + + + + + + + + + + + + + + + + + + The error-code contains an HTTP error code, ex: 404 + + Used in: error-page + + + + + + + + + + + + + + + + + + + The error-pageType contains a mapping between an error code + or exception type to the path of a resource in the web + application. + + Error-page declarations using the exception-type element in + the deployment descriptor must be unique up to the class name of + the exception-type. Similarly, error-page declarations using the + error-code element must be unique in the deployment descriptor + up to the status code. + + If an error-page element in the deployment descriptor does not + contain an exception-type or an error-code element, the error + page is a default error page. + + Used in: web-app + + + + + + + + + + + The exception-type contains a fully qualified class + name of a Java exception type. + + + + + + + + + + The location element contains the location of the + resource in the web application relative to the root of + the web application. The value of the location must have + a leading `/'. + + + + + + + + + + + + + + + + The filterType is used to declare a filter in the web + application. The filter is mapped to either a servlet or a + URL pattern in the filter-mapping element, using the + filter-name value to reference. Filters can access the + initialization parameters declared in the deployment + descriptor at runtime via the FilterConfig interface. + + Used in: web-app + + + + + + + + + + + The fully qualified classname of the filter. + + + + + + + + + + The init-param element contains a name/value pair as + an initialization param of a servlet filter + + + + + + + + + + + + + + + + Declaration of the filter mappings in this web + application is done by using filter-mappingType. + The container uses the filter-mapping + declarations to decide which filters to apply to a request, + and in what order. The container matches the request URI to + a Servlet in the normal way. To determine which filters to + apply it matches filter-mapping declarations either on + servlet-name, or on url-pattern for each filter-mapping + element, depending on which style is used. The order in + which filters are invoked is the order in which + filter-mapping declarations that match a request URI for a + servlet appear in the list of filter-mapping elements.The + filter-name value must be the value of the filter-name + sub-elements of one of the filter declarations in the + deployment descriptor. + + + + + + + + + + + + + + + + + + + + + + This type defines a string which contains at least one + character. + + + + + + + + + + + + + + + + + + The logical name of the filter is declare + by using filter-nameType. This name is used to map the + filter. Each filter name is unique within the web + application. + + Used in: filter, filter-mapping + + + + + + + + + + + + + + + + The form-login-configType specifies the login and error + pages that should be used in form based login. If form based + authentication is not used, these elements are ignored. + + Used in: login-config + + + + + + + + + The form-login-page element defines the location in the web + app where the page that can be used for login can be + found. The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + The form-error-page element defines the location in + the web app where the error page that is displayed + when login is not successful can be found. + The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + + + + + An HTTP method type as defined in HTTP 1.1 section 2.2. + + + + + + + + + + + + + + + + + + + + + + + + + + The login-configType is used to configure the authentication + method that should be used, the realm name that should be + used for this application, and the attributes that are + needed by the form login mechanism. + + Used in: web-app + + + + + + + + + + The realm name element specifies the realm name to + use in HTTP Basic authorization. + + + + + + + + + + + + + + + + + The mime-mappingType defines a mapping between an extension + and a mime type. + + Used in: web-app + + + + + + + + The extension element contains a string describing an + extension. example: "txt" + + + + + + + + + + + + + + + + + The mime-typeType is used to indicate a defined mime type. + + Example: + "text/plain" + + Used in: mime-mapping + + + + + + + + + + + + + + + + + + The security-constraintType is used to associate + security constraints with one or more web resource + collections + + Used in: web-app + + + + + + + + + + + + + + + + + + + + The servletType is used to declare a servlet. + It contains the declarative data of a + servlet. If a jsp-file is specified and the load-on-startup + element is present, then the JSP should be precompiled and + loaded. + + Used in: web-app + + + + + + + + + + + + The servlet-class element contains the fully + qualified class name of the servlet. + + + + + + + + + + + + The load-on-startup element indicates that this + servlet should be loaded (instantiated and have + its init() called) on the startup of the web + application. The optional contents of these + element must be an integer indicating the order in + which the servlet should be loaded. If the value + is a negative integer, or the element is not + present, the container is free to load the servlet + whenever it chooses. If the value is a positive + integer or 0, the container must load and + initialize the servlet as the application is + deployed. The container must guarantee that + servlets marked with lower integers are loaded + before servlets marked with higher integers. The + container may choose the order of loading of + servlets with the same load-on-start-up value. + + + + + + + + + + + + + + + + + + + + + The servlet-mappingType defines a mapping between a + servlet and a url pattern. + + Used in: web-app + + + + + + + + + + + + + + + + + + The servlet-name element contains the canonical name of the + servlet. Each servlet name is unique within the web + application. + + + + + + + + + + + + + + + + The session-configType defines the session parameters + for this web application. + + Used in: web-app + + + + + + + + + The session-timeout element defines the default + session timeout interval for all sessions created + in this web application. The specified timeout + must be expressed in a whole number of minutes. + If the timeout is 0 or less, the container ensures + the default behaviour of sessions is never to time + out. If this element is not specified, the container + must set its default timeout period. + + + + + + + + + The cookie-config element defines the configuration of the + session tracking cookies created by this web application. + + + + + + + + + The tracking-mode element defines the tracking modes + for sessions created by this web application + + + + + + + + + + + + + + + + The cookie-configType defines the configuration for the + session tracking cookies of this web application. + + Used in: session-config + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as HttpOnly + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as secure. + When true, all session tracking cookies must be marked + as secure independent of the nature of the request that + initiated the corresponding session. + When false, the session cookie should only be marked secure + if the request that initiated the session was secure. + + + + + + + + + The lifetime (in seconds) that will be assigned to any + session tracking cookies created by this web application. + Default is -1 + + + + + + + + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + Used in: cookie-config + + + + + + + + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The tracking modes for sessions created by this web + application + + Used in: session-config + + + + + + + + + + + + + + + + + + + + The transport-guaranteeType specifies that the communication + between client and server should be NONE, INTEGRAL, or + CONFIDENTIAL. NONE means that the application does not + require any transport guarantees. A value of INTEGRAL means + that the application requires that the data sent between the + client and server be sent in such a way that it can't be + changed in transit. CONFIDENTIAL means that the application + requires that the data be transmitted in a fashion that + prevents other entities from observing the contents of the + transmission. In most cases, the presence of the INTEGRAL or + CONFIDENTIAL flag will indicate that the use of SSL is + required. + + Used in: user-data-constraint + + + + + + + + + + + + + + + + + + + + The user-data-constraintType is used to indicate how + data communicated between the client and container should be + protected. + + Used in: security-constraint + + + + + + + + + + + + + + + + + + The elements that use this type designate a path starting + with a "/" and interpreted relative to the root of a WAR + file. + + + + + + + + + + + + + + + This type contains the recognized versions of + web-application supported. It is used to designate the + version of the web application. + + + + + + + + + + + + + + + + The web-resource-collectionType is used to identify the + resources and HTTP methods on those resources to which a + security constraint applies. If no HTTP methods are specified, + then the security constraint applies to all HTTP methods. + If HTTP methods are specified by http-method-omission + elements, the security constraint applies to all methods + except those identified in the collection. + http-method-omission and http-method elements are never + mixed in the same collection. + + Used in: security-constraint + + + + + + + + + The web-resource-name contains the name of this web + resource collection. + + + + + + + + + + + + Each http-method names an HTTP method to which the + constraint applies. + + + + + + + + + Each http-method-omission names an HTTP method to + which the constraint does not apply. + + + + + + + + + + + + + + + + + The welcome-file-list contains an ordered list of welcome + files elements. + + Used in: web-app + + + + + + + + + The welcome-file element contains file name to use + as a default welcome file, such as index.html + + + + + + + + + + + + + The localeType defines valid locale defined by ISO-639-1 + and ISO-3166. + + + + + + + + + + + + + The encodingType defines IANA character sets. + + + + + + + + + + + + + + + + The locale-encoding-mapping-list contains one or more + locale-encoding-mapping(s). + + + + + + + + + + + + + + + + + The locale-encoding-mapping contains locale name and + encoding name. The locale name must be either "Language-code", + such as "ja", defined by ISO-639 or "Language-code_Country-code", + such as "ja_JP". "Country code" is defined by ISO-3166. + + + + + + + + + + + + + + + + + + This element indicates that the ordering sub-element in which + it was placed should take special action regarding the ordering + of this application resource relative to other application + configuration resources. + See section 8.2.2 of the specification for details. + + + + + + + + + + + + + + This element specifies configuration information related to the + handling of multipart/form-data requests. + + + + + + + + + The directory location where uploaded files will be stored + + + + + + + + + The maximum size limit of uploaded files + + + + + + + + + The maximum size limit of multipart/form-data requests + + + + + + + + + The size threshold after which an uploaded file will be + written to disk + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-common_4_0.xsd b/java/jakarta/servlet/resources/web-common_4_0.xsd new file mode 100644 index 0000000..72dfb41 --- /dev/null +++ b/java/jakarta/servlet/resources/web-common_4_0.xsd @@ -0,0 +1,1481 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2017 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://xmlns.jcp.org/xml/ns/javaee/web-common_4_0.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + + The context-param element contains the declaration + of a web application's servlet context + initialization parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The metadata-complete attribute defines whether this + deployment descriptor and other related deployment + descriptors for this module (e.g., web service + descriptors) are complete, or whether the class + files available to this module and packaged with + this application should be examined for annotations + that specify deployment information. + + If metadata-complete is set to "true", the deployment + tool must ignore any annotations that specify deployment + information, which might be present in the class files + of the application. + + If metadata-complete is not specified or is set to + "false", the deployment tool must examine the class + files of the application for annotations, as + specified by the specifications. + + + + + + + + + + + + + + The auth-constraintType indicates the user roles that + should be permitted access to this resource + collection. The role-name used here must either correspond + to the role-name of one of the security-role elements + defined for this web application, or be the specially + reserved role-name "*" that is a compact syntax for + indicating all roles in the web application. If both "*" + and rolenames appear, the container interprets this as all + roles. If no roles are defined, no user is allowed access + to the portion of the web application described by the + containing security-constraint. The container matches + role names case sensitively when determining access. + + + + + + + + + + + + + + + + + + The auth-methodType is used to configure the authentication + mechanism for the web application. As a prerequisite to + gaining access to any web resources which are protected by + an authorization constraint, a user must have authenticated + using the configured mechanism. Legal values are "BASIC", + "DIGEST", "FORM", "CLIENT-CERT", or a vendor-specific + authentication scheme. + + Used in: login-config + + + + + + + + + + + + + + + + The dispatcher has five legal values: FORWARD, REQUEST, + INCLUDE, ASYNC, and ERROR. + + A value of FORWARD means the Filter will be applied under + RequestDispatcher.forward() calls. + A value of REQUEST means the Filter will be applied under + ordinary client calls to the path or servlet. + A value of INCLUDE means the Filter will be applied under + RequestDispatcher.include() calls. + A value of ASYNC means the Filter will be applied under + calls dispatched from an AsyncContext. + A value of ERROR means the Filter will be applied under the + error page mechanism. + + The absence of any dispatcher elements in a filter-mapping + indicates a default of applying filters only under ordinary + client calls to the path or servlet. + + + + + + + + + + + + + + + + + + + + + + The error-code contains an HTTP error code, ex: 404 + + Used in: error-page + + + + + + + + + + + + + + + + + + + The error-pageType contains a mapping between an error code + or exception type to the path of a resource in the web + application. + + Error-page declarations using the exception-type element in + the deployment descriptor must be unique up to the class name of + the exception-type. Similarly, error-page declarations using the + error-code element must be unique in the deployment descriptor + up to the status code. + + If an error-page element in the deployment descriptor does not + contain an exception-type or an error-code element, the error + page is a default error page. + + Used in: web-app + + + + + + + + + + + The exception-type contains a fully qualified class + name of a Java exception type. + + + + + + + + + + The location element contains the location of the + resource in the web application relative to the root of + the web application. The value of the location must have + a leading `/'. + + + + + + + + + + + + + + + + The filterType is used to declare a filter in the web + application. The filter is mapped to either a servlet or a + URL pattern in the filter-mapping element, using the + filter-name value to reference. Filters can access the + initialization parameters declared in the deployment + descriptor at runtime via the FilterConfig interface. + + Used in: web-app + + + + + + + + + + + The fully qualified classname of the filter. + + + + + + + + + + The init-param element contains a name/value pair as + an initialization param of a servlet filter + + + + + + + + + + + + + + + + Declaration of the filter mappings in this web + application is done by using filter-mappingType. + The container uses the filter-mapping + declarations to decide which filters to apply to a request, + and in what order. The container matches the request URI to + a Servlet in the normal way. To determine which filters to + apply it matches filter-mapping declarations either on + servlet-name, or on url-pattern for each filter-mapping + element, depending on which style is used. The order in + which filters are invoked is the order in which + filter-mapping declarations that match a request URI for a + servlet appear in the list of filter-mapping elements.The + filter-name value must be the value of the filter-name + sub-elements of one of the filter declarations in the + deployment descriptor. + + + + + + + + + + + + + + + + + + + + + + This type defines a string which contains at least one + character. + + + + + + + + + + + + + + + + + + The logical name of the filter is declare + by using filter-nameType. This name is used to map the + filter. Each filter name is unique within the web + application. + + Used in: filter, filter-mapping + + + + + + + + + + + + + + + + The form-login-configType specifies the login and error + pages that should be used in form based login. If form based + authentication is not used, these elements are ignored. + + Used in: login-config + + + + + + + + + The form-login-page element defines the location in the web + app where the page that can be used for login can be + found. The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + The form-error-page element defines the location in + the web app where the error page that is displayed + when login is not successful can be found. + The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + + + + + An HTTP method type as defined in HTTP 1.1 section 2.2. + + + + + + + + + + + + + + + + + + + + + + + + + + The login-configType is used to configure the authentication + method that should be used, the realm name that should be + used for this application, and the attributes that are + needed by the form login mechanism. + + Used in: web-app + + + + + + + + + + The realm name element specifies the realm name to + use in HTTP Basic authorization. + + + + + + + + + + + + + + + + + The mime-mappingType defines a mapping between an extension + and a mime type. + + Used in: web-app + + + + + + + + The extension element contains a string describing an + extension. example: "txt" + + + + + + + + + + + + + + + + + The mime-typeType is used to indicate a defined mime type. + + Example: + "text/plain" + + Used in: mime-mapping + + + + + + + + + + + + + + + + + + The security-constraintType is used to associate + security constraints with one or more web resource + collections + + Used in: web-app + + + + + + + + + + + + + + + + + + + + The servletType is used to declare a servlet. + It contains the declarative data of a + servlet. If a jsp-file is specified and the load-on-startup + element is present, then the JSP should be precompiled and + loaded. + + Used in: web-app + + + + + + + + + + + + The servlet-class element contains the fully + qualified class name of the servlet. + + + + + + + + + + + + The load-on-startup element indicates that this + servlet should be loaded (instantiated and have + its init() called) on the startup of the web + application. The optional contents of these + element must be an integer indicating the order in + which the servlet should be loaded. If the value + is a negative integer, or the element is not + present, the container is free to load the servlet + whenever it chooses. If the value is a positive + integer or 0, the container must load and + initialize the servlet as the application is + deployed. The container must guarantee that + servlets marked with lower integers are loaded + before servlets marked with higher integers. The + container may choose the order of loading of + servlets with the same load-on-start-up value. + + + + + + + + + + + + + + + + + + + + + The servlet-mappingType defines a mapping between a + servlet and a url pattern. + + Used in: web-app + + + + + + + + + + + + + + + + + + The servlet-name element contains the canonical name of the + servlet. Each servlet name is unique within the web + application. + + + + + + + + + + + + + + + + The session-configType defines the session parameters + for this web application. + + Used in: web-app + + + + + + + + + The session-timeout element defines the default + session timeout interval for all sessions created + in this web application. The specified timeout + must be expressed in a whole number of minutes. + If the timeout is 0 or less, the container ensures + the default behaviour of sessions is never to time + out. If this element is not specified, the container + must set its default timeout period. + + + + + + + + + The cookie-config element defines the configuration of the + session tracking cookies created by this web application. + + + + + + + + + The tracking-mode element defines the tracking modes + for sessions created by this web application + + + + + + + + + + + + + + + + The cookie-configType defines the configuration for the + session tracking cookies of this web application. + + Used in: session-config + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as HttpOnly + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as secure. + When true, all session tracking cookies must be marked + as secure independent of the nature of the request that + initiated the corresponding session. + When false, the session cookie should only be marked secure + if the request that initiated the session was secure. + + + + + + + + + The lifetime (in seconds) that will be assigned to any + session tracking cookies created by this web application. + Default is -1 + + + + + + + + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + Used in: cookie-config + + + + + + + + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The tracking modes for sessions created by this web + application + + Used in: session-config + + + + + + + + + + + + + + + + + + + + The transport-guaranteeType specifies that the communication + between client and server should be NONE, INTEGRAL, or + CONFIDENTIAL. NONE means that the application does not + require any transport guarantees. A value of INTEGRAL means + that the application requires that the data sent between the + client and server be sent in such a way that it can't be + changed in transit. CONFIDENTIAL means that the application + requires that the data be transmitted in a fashion that + prevents other entities from observing the contents of the + transmission. In most cases, the presence of the INTEGRAL or + CONFIDENTIAL flag will indicate that the use of SSL is + required. + + Used in: user-data-constraint + + + + + + + + + + + + + + + + + + + + The user-data-constraintType is used to indicate how + data communicated between the client and container should be + protected. + + Used in: security-constraint + + + + + + + + + + + + + + + + + + The elements that use this type designate a path starting + with a "/" and interpreted relative to the root of a WAR + file. + + + + + + + + + + + + + + + This type contains the recognized versions of + web-application supported. It is used to designate the + version of the web application. + + + + + + + + + + + + + + + + The web-resource-collectionType is used to identify the + resources and HTTP methods on those resources to which a + security constraint applies. If no HTTP methods are specified, + then the security constraint applies to all HTTP methods. + If HTTP methods are specified by http-method-omission + elements, the security constraint applies to all methods + except those identified in the collection. + http-method-omission and http-method elements are never + mixed in the same collection. + + Used in: security-constraint + + + + + + + + + The web-resource-name contains the name of this web + resource collection. + + + + + + + + + + + + Each http-method names an HTTP method to which the + constraint applies. + + + + + + + + + Each http-method-omission names an HTTP method to + which the constraint does not apply. + + + + + + + + + + + + + + + + + The welcome-file-list contains an ordered list of welcome + files elements. + + Used in: web-app + + + + + + + + + The welcome-file element contains file name to use + as a default welcome file, such as index.html + + + + + + + + + + + + + The localeType defines valid locale defined by ISO-639-1 + and ISO-3166. + + + + + + + + + + + + + The encodingType defines IANA character sets. + + + + + + + + + + + + + + + + The locale-encoding-mapping-list contains one or more + locale-encoding-mapping(s). + + + + + + + + + + + + + + + + + The locale-encoding-mapping contains locale name and + encoding name. The locale name must be either "Language-code", + such as "ja", defined by ISO-639 or "Language-code_Country-code", + such as "ja_JP". "Country code" is defined by ISO-3166. + + + + + + + + + + + + + + + + + + This element indicates that the ordering sub-element in which + it was placed should take special action regarding the ordering + of this application resource relative to other application + configuration resources. + See section 8.2.2 of the specification for details. + + + + + + + + + + + + + + This element specifies configuration information related to the + handling of multipart/form-data requests. + + + + + + + + + The directory location where uploaded files will be stored + + + + + + + + + The maximum size limit of uploaded files + + + + + + + + + The maximum size limit of multipart/form-data requests + + + + + + + + + The size threshold after which an uploaded file will be + written to disk + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-common_5_0.xsd b/java/jakarta/servlet/resources/web-common_5_0.xsd new file mode 100644 index 0000000..bf8132d --- /dev/null +++ b/java/jakarta/servlet/resources/web-common_5_0.xsd @@ -0,0 +1,1450 @@ + + + + + + Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Jakarta EE + namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/web-common_5_0.xsd + + ]]> + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + + The context-param element contains the declaration + of a web application's servlet context + initialization parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The metadata-complete attribute defines whether this + deployment descriptor and other related deployment + descriptors for this module (e.g., web service + descriptors) are complete, or whether the class + files available to this module and packaged with + this application should be examined for annotations + that specify deployment information. + + If metadata-complete is set to "true", the deployment + tool must ignore any annotations that specify deployment + information, which might be present in the class files + of the application. + + If metadata-complete is not specified or is set to + "false", the deployment tool must examine the class + files of the application for annotations, as + specified by the specifications. + + + + + + + + + + + + + + The auth-constraintType indicates the user roles that + should be permitted access to this resource + collection. The role-name used here must either correspond + to the role-name of one of the security-role elements + defined for this web application, or be the specially + reserved role-name "*" that is a compact syntax for + indicating all roles in the web application. If both "*" + and rolenames appear, the container interprets this as all + roles. If no roles are defined, no user is allowed access + to the portion of the web application described by the + containing security-constraint. The container matches + role names case sensitively when determining access. + + + + + + + + + + + + + + + + + + The auth-methodType is used to configure the authentication + mechanism for the web application. As a prerequisite to + gaining access to any web resources which are protected by + an authorization constraint, a user must have authenticated + using the configured mechanism. Legal values are "BASIC", + "DIGEST", "FORM", "CLIENT-CERT", or a vendor-specific + authentication scheme. + + Used in: login-config + + + + + + + + + + + + + + + + The dispatcher has five legal values: FORWARD, REQUEST, + INCLUDE, ASYNC, and ERROR. + + A value of FORWARD means the Filter will be applied under + RequestDispatcher.forward() calls. + A value of REQUEST means the Filter will be applied under + ordinary client calls to the path or servlet. + A value of INCLUDE means the Filter will be applied under + RequestDispatcher.include() calls. + A value of ASYNC means the Filter will be applied under + calls dispatched from an AsyncContext. + A value of ERROR means the Filter will be applied under the + error page mechanism. + + The absence of any dispatcher elements in a filter-mapping + indicates a default of applying filters only under ordinary + client calls to the path or servlet. + + + + + + + + + + + + + + + + + + + + + + The error-code contains an HTTP error code, ex: 404 + + Used in: error-page + + + + + + + + + + + + + + + + + + + The error-pageType contains a mapping between an error code + or exception type to the path of a resource in the web + application. + + Error-page declarations using the exception-type element in + the deployment descriptor must be unique up to the class name of + the exception-type. Similarly, error-page declarations using the + error-code element must be unique in the deployment descriptor + up to the status code. + + If an error-page element in the deployment descriptor does not + contain an exception-type or an error-code element, the error + page is a default error page. + + Used in: web-app + + + + + + + + + + + The exception-type contains a fully qualified class + name of a Java exception type. + + + + + + + + + + The location element contains the location of the + resource in the web application relative to the root of + the web application. The value of the location must have + a leading `/'. + + + + + + + + + + + + + + + + The filterType is used to declare a filter in the web + application. The filter is mapped to either a servlet or a + URL pattern in the filter-mapping element, using the + filter-name value to reference. Filters can access the + initialization parameters declared in the deployment + descriptor at runtime via the FilterConfig interface. + + Used in: web-app + + + + + + + + + + + The fully qualified classname of the filter. + + + + + + + + + + The init-param element contains a name/value pair as + an initialization param of a servlet filter + + + + + + + + + + + + + + + + Declaration of the filter mappings in this web + application is done by using filter-mappingType. + The container uses the filter-mapping + declarations to decide which filters to apply to a request, + and in what order. The container matches the request URI to + a Servlet in the normal way. To determine which filters to + apply it matches filter-mapping declarations either on + servlet-name, or on url-pattern for each filter-mapping + element, depending on which style is used. The order in + which filters are invoked is the order in which + filter-mapping declarations that match a request URI for a + servlet appear in the list of filter-mapping elements.The + filter-name value must be the value of the filter-name + sub-elements of one of the filter declarations in the + deployment descriptor. + + + + + + + + + + + + + + + + + + + + + + This type defines a string which contains at least one + character. + + + + + + + + + + + + + + + + + + The logical name of the filter is declare + by using filter-nameType. This name is used to map the + filter. Each filter name is unique within the web + application. + + Used in: filter, filter-mapping + + + + + + + + + + + + + + + + The form-login-configType specifies the login and error + pages that should be used in form based login. If form based + authentication is not used, these elements are ignored. + + Used in: login-config + + + + + + + + + The form-login-page element defines the location in the web + app where the page that can be used for login can be + found. The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + The form-error-page element defines the location in + the web app where the error page that is displayed + when login is not successful can be found. + The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + + + + + A HTTP method type as defined in HTTP 1.1 section 2.2. + + + + + + + + + + + + + + + + + + + + + + + + + + The login-configType is used to configure the authentication + method that should be used, the realm name that should be + used for this application, and the attributes that are + needed by the form login mechanism. + + Used in: web-app + + + + + + + + + + The realm name element specifies the realm name to + use in HTTP Basic authorization. + + + + + + + + + + + + + + + + + The mime-mappingType defines a mapping between an extension + and a mime type. + + Used in: web-app + + + + + + + + The extension element contains a string describing an + extension. example: "txt" + + + + + + + + + + + + + + + + + The mime-typeType is used to indicate a defined mime type. + + Example: + "text/plain" + + Used in: mime-mapping + + + + + + + + + + + + + + + + + + The security-constraintType is used to associate + security constraints with one or more web resource + collections + + Used in: web-app + + + + + + + + + + + + + + + + + + + + The servletType is used to declare a servlet. + It contains the declarative data of a + servlet. If a jsp-file is specified and the load-on-startup + element is present, then the JSP should be precompiled and + loaded. + + Used in: web-app + + + + + + + + + + + + The servlet-class element contains the fully + qualified class name of the servlet. + + + + + + + + + + + + The load-on-startup element indicates that this + servlet should be loaded (instantiated and have + its init() called) on the startup of the web + application. The optional contents of these + element must be an integer indicating the order in + which the servlet should be loaded. If the value + is a negative integer, or the element is not + present, the container is free to load the servlet + whenever it chooses. If the value is a positive + integer or 0, the container must load and + initialize the servlet as the application is + deployed. The container must guarantee that + servlets marked with lower integers are loaded + before servlets marked with higher integers. The + container may choose the order of loading of + servlets with the same load-on-start-up value. + + + + + + + + + + + + + + + + + + + + + The servlet-mappingType defines a mapping between a + servlet and a url pattern. + + Used in: web-app + + + + + + + + + + + + + + + + + + The servlet-name element contains the canonical name of the + servlet. Each servlet name is unique within the web + application. + + + + + + + + + + + + + + + + The session-configType defines the session parameters + for this web application. + + Used in: web-app + + + + + + + + + The session-timeout element defines the default + session timeout interval for all sessions created + in this web application. The specified timeout + must be expressed in a whole number of minutes. + If the timeout is 0 or less, the container ensures + the default behaviour of sessions is never to time + out. If this element is not specified, the container + must set its default timeout period. + + + + + + + + + The cookie-config element defines the configuration of the + session tracking cookies created by this web application. + + + + + + + + + The tracking-mode element defines the tracking modes + for sessions created by this web application + + + + + + + + + + + + + + + + The cookie-configType defines the configuration for the + session tracking cookies of this web application. + + Used in: session-config + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as HttpOnly + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as secure. + When true, all session tracking cookies must be marked + as secure independent of the nature of the request that + initiated the corresponding session. + When false, the session cookie should only be marked secure + if the request that initiated the session was secure. + + + + + + + + + The lifetime (in seconds) that will be assigned to any + session tracking cookies created by this web application. + Default is -1 + + + + + + + + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + Used in: cookie-config + + + + + + + + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The tracking modes for sessions created by this web + application + + Used in: session-config + + + + + + + + + + + + + + + + + + + + The transport-guaranteeType specifies that the communication + between client and server should be NONE, INTEGRAL, or + CONFIDENTIAL. NONE means that the application does not + require any transport guarantees. A value of INTEGRAL means + that the application requires that the data sent between the + client and server be sent in such a way that it can't be + changed in transit. CONFIDENTIAL means that the application + requires that the data be transmitted in a fashion that + prevents other entities from observing the contents of the + transmission. In most cases, the presence of the INTEGRAL or + CONFIDENTIAL flag will indicate that the use of SSL is + required. + + Used in: user-data-constraint + + + + + + + + + + + + + + + + + + + + The user-data-constraintType is used to indicate how + data communicated between the client and container should be + protected. + + Used in: security-constraint + + + + + + + + + + + + + + + + + + The elements that use this type designate a path starting + with a "/" and interpreted relative to the root of a WAR + file. + + + + + + + + + + + + + + + This type contains the recognized versions of + web-application supported. It is used to designate the + version of the web application. + + + + + + + + + + + + + + + + The web-resource-collectionType is used to identify the + resources and HTTP methods on those resources to which a + security constraint applies. If no HTTP methods are specified, + then the security constraint applies to all HTTP methods. + If HTTP methods are specified by http-method-omission + elements, the security constraint applies to all methods + except those identified in the collection. + http-method-omission and http-method elements are never + mixed in the same collection. + + Used in: security-constraint + + + + + + + + + The web-resource-name contains the name of this web + resource collection. + + + + + + + + + + + + Each http-method names an HTTP method to which the + constraint applies. + + + + + + + + + Each http-method-omission names an HTTP method to + which the constraint does not apply. + + + + + + + + + + + + + + + + + The welcome-file-list contains an ordered list of welcome + files elements. + + Used in: web-app + + + + + + + + + The welcome-file element contains file name to use + as a default welcome file, such as index.html + + + + + + + + + + + + + The localeType defines valid locale defined by ISO-639-1 + and ISO-3166. + + + + + + + + + + + + + The encodingType defines IANA character sets. + + + + + + + + + + + + + + + + The locale-encoding-mapping-list contains one or more + locale-encoding-mapping(s). + + + + + + + + + + + + + + + + + The locale-encoding-mapping contains locale name and + encoding name. The locale name must be either "Language-code", + such as "ja", defined by ISO-639 or "Language-code_Country-code", + such as "ja_JP". "Country code" is defined by ISO-3166. + + + + + + + + + + + + + + + + + + This element indicates that the ordering sub-element in which + it was placed should take special action regarding the ordering + of this application resource relative to other application + configuration resources. + See section 8.2.2 of the specification for details. + + + + + + + + + + + + + + This element specifies configuration information related to the + handling of multipart/form-data requests. + + + + + + + + + The directory location where uploaded files will be stored + + + + + + + + + The maximum size limit of uploaded files + + + + + + + + + The maximum size limit of multipart/form-data requests + + + + + + + + + The size threshold after which an uploaded file will be + written to disk + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-common_6_0.xsd b/java/jakarta/servlet/resources/web-common_6_0.xsd new file mode 100644 index 0000000..fae4e41 --- /dev/null +++ b/java/jakarta/servlet/resources/web-common_6_0.xsd @@ -0,0 +1,1507 @@ + + + + + + Copyright (c) 2009, 2021 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Jakarta EE + namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/web-common_6_0.xsd + + ]]> + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + + The context-param element contains the declaration + of a web application's servlet context + initialization parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The metadata-complete attribute defines whether this + deployment descriptor and other related deployment + descriptors for this module (e.g., web service + descriptors) are complete, or whether the class + files available to this module and packaged with + this application should be examined for annotations + that specify deployment information. + + If metadata-complete is set to "true", the deployment + tool must ignore any annotations that specify deployment + information, which might be present in the class files + of the application. + + If metadata-complete is not specified or is set to + "false", the deployment tool must examine the class + files of the application for annotations, as + specified by the specifications. + + + + + + + + + + + + + + This type is a general type that can be used to declare + attribute/value lists. + + + + + + + + + + The attribute-name element contains the name of an + attribute. + + + + + + + + + The attribute-value element contains the value of a + attribute. + + + + + + + + + + + + + + + + The auth-constraintType indicates the user roles that + should be permitted access to this resource + collection. The role-name used here must either correspond + to the role-name of one of the security-role elements + defined for this web application, or be the specially + reserved role-name "*" that is a compact syntax for + indicating all roles in the web application. If both "*" + and rolenames appear, the container interprets this as all + roles. If no roles are defined, no user is allowed access + to the portion of the web application described by the + containing security-constraint. The container matches + role names case sensitively when determining access. + + + + + + + + + + + + + + + + + + The auth-methodType is used to configure the authentication + mechanism for the web application. As a prerequisite to + gaining access to any web resources which are protected by + an authorization constraint, a user must have authenticated + using the configured mechanism. Legal values are "BASIC", + "DIGEST", "FORM", "CLIENT-CERT", or a vendor-specific + authentication scheme. + + Used in: login-config + + + + + + + + + + + + + + + + The dispatcher has five legal values: FORWARD, REQUEST, + INCLUDE, ASYNC, and ERROR. + + A value of FORWARD means the Filter will be applied under + RequestDispatcher.forward() calls. + A value of REQUEST means the Filter will be applied under + ordinary client calls to the path or servlet. + A value of INCLUDE means the Filter will be applied under + RequestDispatcher.include() calls. + A value of ASYNC means the Filter will be applied under + calls dispatched from an AsyncContext. + A value of ERROR means the Filter will be applied under the + error page mechanism. + + The absence of any dispatcher elements in a filter-mapping + indicates a default of applying filters only under ordinary + client calls to the path or servlet. + + + + + + + + + + + + + + + + + + + + + + The error-code contains an HTTP error code, ex: 404 + + Used in: error-page + + + + + + + + + + + + + + + + + + + The error-pageType contains a mapping between an error code + or exception type to the path of a resource in the web + application. + + Error-page declarations using the exception-type element in + the deployment descriptor must be unique up to the class name of + the exception-type. Similarly, error-page declarations using the + error-code element must be unique in the deployment descriptor + up to the status code. + + If an error-page element in the deployment descriptor does not + contain an exception-type or an error-code element, the error + page is a default error page. + + Used in: web-app + + + + + + + + + + + The exception-type contains a fully qualified class + name of a Java exception type. + + + + + + + + + + The location element contains the location of the + resource in the web application relative to the root of + the web application. The value of the location must have + a leading `/'. + + + + + + + + + + + + + + + + The filterType is used to declare a filter in the web + application. The filter is mapped to either a servlet or a + URL pattern in the filter-mapping element, using the + filter-name value to reference. Filters can access the + initialization parameters declared in the deployment + descriptor at runtime via the FilterConfig interface. + + Used in: web-app + + + + + + + + + + + The fully qualified classname of the filter. + + + + + + + + + + The init-param element contains a name/value pair as + an initialization param of a servlet filter + + + + + + + + + + + + + + + + Declaration of the filter mappings in this web + application is done by using filter-mappingType. + The container uses the filter-mapping + declarations to decide which filters to apply to a request, + and in what order. The container matches the request URI to + a Servlet in the normal way. To determine which filters to + apply it matches filter-mapping declarations either on + servlet-name, or on url-pattern for each filter-mapping + element, depending on which style is used. The order in + which filters are invoked is the order in which + filter-mapping declarations that match a request URI for a + servlet appear in the list of filter-mapping elements.The + filter-name value must be the value of the filter-name + sub-elements of one of the filter declarations in the + deployment descriptor. + + + + + + + + + + + + + + + + + + + + + + This type defines a string which contains at least one + character. + + + + + + + + + + + + + + + + + + The logical name of the filter is declare + by using filter-nameType. This name is used to map the + filter. Each filter name is unique within the web + application. + + Used in: filter, filter-mapping + + + + + + + + + + + + + + + + The form-login-configType specifies the login and error + pages that should be used in form based login. If form based + authentication is not used, these elements are ignored. + + Used in: login-config + + + + + + + + + The form-login-page element defines the location in the web + app where the page that can be used for login can be + found. The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + The form-error-page element defines the location in + the web app where the error page that is displayed + when login is not successful can be found. + The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + + + + + A HTTP method type as defined in HTTP 1.1 section 2.2. + + + + + + + + + + + + + + + + + + + + + + + + + + The login-configType is used to configure the authentication + method that should be used, the realm name that should be + used for this application, and the attributes that are + needed by the form login mechanism. + + Used in: web-app + + + + + + + + + + The realm name element specifies the realm name to + use in HTTP Basic authorization. + + + + + + + + + + + + + + + + + The mime-mappingType defines a mapping between an extension + and a mime type. + + Used in: web-app + + + + + + + + The extension element contains a string describing an + extension. example: "txt" + + + + + + + + + + + + + + + + + The mime-typeType is used to indicate a defined mime type. + + Example: + "text/plain" + + Used in: mime-mapping + + + + + + + + + + + + + + + + + + The security-constraintType is used to associate + security constraints with one or more web resource + collections + + Used in: web-app + + + + + + + + + + + + + + + + + + + + The servletType is used to declare a servlet. + It contains the declarative data of a + servlet. If a jsp-file is specified and the load-on-startup + element is present, then the JSP should be precompiled and + loaded. + + Used in: web-app + + + + + + + + + + + + The servlet-class element contains the fully + qualified class name of the servlet. + + + + + + + + + + + + The load-on-startup element indicates that this + servlet should be loaded (instantiated and have + its init() called) on the startup of the web + application. The optional contents of these + element must be an integer indicating the order in + which the servlet should be loaded. If the value + is a negative integer, or the element is not + present, the container is free to load the servlet + whenever it chooses. If the value is a positive + integer or 0, the container must load and + initialize the servlet as the application is + deployed. The container must guarantee that + servlets marked with lower integers are loaded + before servlets marked with higher integers. The + container may choose the order of loading of + servlets with the same load-on-start-up value. + + + + + + + + + + + + + + + + + + + + + The servlet-mappingType defines a mapping between a + servlet and a url pattern. + + Used in: web-app + + + + + + + + + + + + + + + + + + The servlet-name element contains the canonical name of the + servlet. Each servlet name is unique within the web + application. + + + + + + + + + + + + + + + + The session-configType defines the session parameters + for this web application. + + Used in: web-app + + + + + + + + + The session-timeout element defines the default + session timeout interval for all sessions created + in this web application. The specified timeout + must be expressed in a whole number of minutes. + If the timeout is 0 or less, the container ensures + the default behaviour of sessions is never to time + out. If this element is not specified, the container + must set its default timeout period. + + + + + + + + + The cookie-config element defines the configuration of the + session tracking cookies created by this web application. + + + + + + + + + The tracking-mode element defines the tracking modes + for sessions created by this web application + + + + + + + + + + + + + + + + The cookie-configType defines the configuration for the + session tracking cookies of this web application. + + Used in: session-config + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as HttpOnly + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as secure. + When true, all session tracking cookies must be marked + as secure independent of the nature of the request that + initiated the corresponding session. + When false, the session cookie should only be marked secure + if the request that initiated the session was secure. + + + + + + + + + The lifetime (in seconds) that will be assigned to any + session tracking cookies created by this web application. + Default is -1 + + + + + + + + + The attribute-param element contains a name/value pair to + be added as an attribute to every session cookie. + + + + + + + + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + Used in: cookie-config + + + + + + + + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The tracking modes for sessions created by this web + application + + Used in: session-config + + + + + + + + + + + + + + + + + + + + The transport-guaranteeType specifies that the communication + between client and server should be NONE, INTEGRAL, or + CONFIDENTIAL. NONE means that the application does not + require any transport guarantees. A value of INTEGRAL means + that the application requires that the data sent between the + client and server be sent in such a way that it can't be + changed in transit. CONFIDENTIAL means that the application + requires that the data be transmitted in a fashion that + prevents other entities from observing the contents of the + transmission. In most cases, the presence of the INTEGRAL or + CONFIDENTIAL flag will indicate that the use of SSL is + required. + + Used in: user-data-constraint + + + + + + + + + + + + + + + + + + + + The user-data-constraintType is used to indicate how + data communicated between the client and container should be + protected. + + Used in: security-constraint + + + + + + + + + + + + + + + + + + The elements that use this type designate a path starting + with a "/" and interpreted relative to the root of a WAR + file. + + + + + + + + + + + + + + + This type contains the recognized versions of + web-application supported. It is used to designate the + version of the web application. + + + + + + + + + + + + + + + + The web-resource-collectionType is used to identify the + resources and HTTP methods on those resources to which a + security constraint applies. If no HTTP methods are specified, + then the security constraint applies to all HTTP methods. + If HTTP methods are specified by http-method-omission + elements, the security constraint applies to all methods + except those identified in the collection. + http-method-omission and http-method elements are never + mixed in the same collection. + + Used in: security-constraint + + + + + + + + + The web-resource-name contains the name of this web + resource collection. + + + + + + + + + + + + Each http-method names an HTTP method to which the + constraint applies. + + + + + + + + + Each http-method-omission names an HTTP method to + which the constraint does not apply. + + + + + + + + + + + + + + + + + The welcome-file-list contains an ordered list of welcome + files elements. + + Used in: web-app + + + + + + + + + The welcome-file element contains file name to use + as a default welcome file, such as index.html + + + + + + + + + + + + + The localeType defines valid locale defined by ISO-639-1 + and ISO-3166. + + + + + + + + + + + + + The encodingType defines IANA character sets. + + + + + + + + + + + + + + + + The locale-encoding-mapping-list contains one or more + locale-encoding-mapping(s). + + + + + + + + + + + + + + + + + The locale-encoding-mapping contains locale name and + encoding name. The locale name must be either "Language-code", + such as "ja", defined by ISO-639 or "Language-code_Country-code", + such as "ja_JP". "Country code" is defined by ISO-3166. + + + + + + + + + + + + + + + + + + This element indicates that the ordering sub-element in which + it was placed should take special action regarding the ordering + of this application resource relative to other application + configuration resources. + See section 8.2.2 of the specification for details. + + + + + + + + + + + + + + This element specifies configuration information related to the + handling of multipart/form-data requests. + + + + + + + + + The directory location where uploaded files will be stored + + + + + + + + + The maximum size limit of uploaded files + + + + + + + + + The maximum size limit of multipart/form-data requests + + + + + + + + + The size threshold after which an uploaded file will be + written to disk + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-fragment_3_0.xsd b/java/jakarta/servlet/resources/web-fragment_3_0.xsd new file mode 100644 index 0000000..bd4e961 --- /dev/null +++ b/java/jakarta/servlet/resources/web-fragment_3_0.xsd @@ -0,0 +1,281 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-fragment element is the root of the deployment + descriptor for a web fragment. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-fragment_3_1.xsd b/java/jakarta/servlet/resources/web-fragment_3_1.xsd new file mode 100644 index 0000000..0373a66 --- /dev/null +++ b/java/jakarta/servlet/resources/web-fragment_3_1.xsd @@ -0,0 +1,347 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2013 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://xmlns.jcp.org/xml/ns/javaee/web-fragment_3_1.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-fragment element is the root of the deployment + descriptor for a web fragment. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + This element contains a sequence of "name" elements, each of + which + refers to an application configuration resource by the "name" + declared on its web.xml fragment. This element can also contain + a single "others" element which specifies that this document + comes + before or after other documents within the application. + See section 8.2.2 of the specification for details. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-fragment_4_0.xsd b/java/jakarta/servlet/resources/web-fragment_4_0.xsd new file mode 100644 index 0000000..ed2d760 --- /dev/null +++ b/java/jakarta/servlet/resources/web-fragment_4_0.xsd @@ -0,0 +1,347 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright (c) 2009-2017 Oracle and/or its affiliates. All rights reserved. + + The contents of this file are subject to the terms of either the GNU + General Public License Version 2 only ("GPL") or the Common Development + and Distribution License("CDDL") (collectively, the "License"). You + may not use this file except in compliance with the License. You can + obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + or packager/legal/LICENSE.txt. See the License for the specific + language governing permissions and limitations under the License. + + When distributing the software, include this License Header Notice in each + file and include the License file at packager/legal/LICENSE.txt. + + GPL Classpath Exception: + Oracle designates this particular file as subject to the "Classpath" + exception as provided by Oracle in the GPL Version 2 section of the License + file that accompanied this code. + + Modifications: + If applicable, add the following below the License Header, with the fields + enclosed by brackets [] replaced by your own identifying information: + "Portions Copyright [year] [name of copyright owner]" + + Contributor(s): + If you wish your version of this file to be governed by only the CDDL or + only the GPL Version 2, indicate your decision by adding "[Contributor] + elects to include this software in this distribution under the [CDDL or GPL + Version 2] license." If you don't indicate a single choice of license, a + recipient has the option to distribute your version of this file under + either the CDDL, the GPL Version 2 or to extend the choice of license to + its licensees as provided above. However, if you add GPL Version 2 code + and therefore, elected the GPL Version 2 license, then the option applies + only if the new code is made subject to such option by the copyright + holder. + + + + + + + The Apache Software Foundation elects to include this software under the + CDDL license. + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://xmlns.jcp.org/xml/ns/javaee/web-fragment_4_0.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-fragment element is the root of the deployment + descriptor for a web fragment. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + This element contains a sequence of "name" elements, each of + which + refers to an application configuration resource by the "name" + declared on its web.xml fragment. This element can also contain + a single "others" element which specifies that this document + comes + before or after other documents within the application. + See section 8.2.2 of the specification for details. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-fragment_5_0.xsd b/java/jakarta/servlet/resources/web-fragment_5_0.xsd new file mode 100644 index 0000000..a5c8450 --- /dev/null +++ b/java/jakarta/servlet/resources/web-fragment_5_0.xsd @@ -0,0 +1,316 @@ + + + + + + Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Jakarta EE + namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/web-fragment_5_0.xsd + + ]]> + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-fragment element is the root of the deployment + descriptor for a web fragment. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an + enterprise bean reference. The enterprise bean reference + is an entry in the web application's environment and is relative + to the java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an + enterprise bean reference. The enterprise bean reference + is an entry in the web application's environment and is relative + to the java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + This element contains a sequence of "name" elements, each of + which + refers to an application configuration resource by the "name" + declared on its web.xml fragment. This element can also contain + a single "others" element which specifies that this document + comes + before or after other documents within the application. + See section 8.2.2 of the specification for details. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-fragment_6_0.xsd b/java/jakarta/servlet/resources/web-fragment_6_0.xsd new file mode 100644 index 0000000..65827ef --- /dev/null +++ b/java/jakarta/servlet/resources/web-fragment_6_0.xsd @@ -0,0 +1,316 @@ + + + + + + Copyright (c) 2009, 2021 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Jakarta EE + namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/web-fragment_6_0.xsd + + ]]> + + + + + + + The following conventions apply to all Jakarta EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-fragment element is the root of the deployment + descriptor for a web fragment. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an + enterprise bean reference. The enterprise bean reference + is an entry in the web application's environment and is relative + to the java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an + enterprise bean reference. The enterprise bean reference + is an entry in the web application's environment and is relative + to the java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + This element contains a sequence of "name" elements, each of + which + refers to an application configuration resource by the "name" + declared on its web.xml fragment. This element can also contain + a single "others" element which specifies that this document + comes + before or after other documents within the application. + See section 8.2.2 of the specification for details. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-jsptaglibrary_1_1.dtd b/java/jakarta/servlet/resources/web-jsptaglibrary_1_1.dtd new file mode 100644 index 0000000..acd148f --- /dev/null +++ b/java/jakarta/servlet/resources/web-jsptaglibrary_1_1.dtd @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-jsptaglibrary_1_2.dtd b/java/jakarta/servlet/resources/web-jsptaglibrary_1_2.dtd new file mode 100644 index 0000000..0f87842 --- /dev/null +++ b/java/jakarta/servlet/resources/web-jsptaglibrary_1_2.dtd @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-jsptaglibrary_2_0.xsd b/java/jakarta/servlet/resources/web-jsptaglibrary_2_0.xsd new file mode 100644 index 0000000..0e2b965 --- /dev/null +++ b/java/jakarta/servlet/resources/web-jsptaglibrary_2_0.xsd @@ -0,0 +1,983 @@ + + + + + + + ... + + + The instance documents may indicate the published + version of the schema using xsi:schemaLocation attribute + for J2EE namespace with the following location: + + http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd + + ]]> + + + + + + + + + + + + + + The taglib tag is the document root. + The definition of taglib is provided + by the tldTaglibType. + + + + + + + + The taglib element contains, among other things, tag and + tag-file elements. + The name subelements of these elements must each be unique. + + + + + + + + + + + + The taglib element contains function elements. + The name subelements of these elements must each be unique. + + + + + + + + + + + + + + + + + Specifies the type of body that is valid for a tag. + This value is used by the JSP container to validate + that a tag invocation has the correct body syntax and + by page composition tools to assist the page author + in providing a valid tag body. + + There are currently four values specified: + + tagdependent The body of the tag is interpreted by the tag + implementation itself, and is most likely + in a different "language", e.g embedded SQL + statements. + + JSP The body of the tag contains nested JSP + syntax. + + empty The body must be empty + + scriptless The body accepts only template text, EL + Expressions, and JSP action elements. No + scripting elements are allowed. + + + + + + + + + + + + + + + + + + + + + The extensibleType is an abstract base type that is used to + define the type of extension-elements. Instance documents + must substitute a known type to define the extension by + using xsi:type attribute to define the actual type of + extension-elements. + + + + + + + + + + + + + The function element is used to provide information on each + function in the tag library that is to be exposed to the EL. + + The function element may have several subelements defining: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name A unique name for this function + + function-class Provides the name of the Java class that + implements the function + + function-signature Provides the signature, as in the Java + Language Specification, of the Java + method that is to be used to implement + the function. + + example Optional informal description of an + example of a use of this function + + function-extension Zero or more extensions that provide extra + information about this function, for tool + consumption + + + + + + + + + + + A unique name for this function. + + + + + + + + + Provides the fully-qualified class name of the Java + class containing the static method that implements + the function. + + + + + + + + + + Provides the signature, of the static Java method that is + to be used to implement the function. The syntax of the + function-signature element is as follows: + + FunctionSignature ::= ReturnType S MethodName S? + '(' S? Parameters? S? ')' + + ReturnType ::= Type + + MethodName ::= Identifier + + Parameters ::= Parameter + | ( Parameter S? ',' S? Parameters ) + + Parameter ::= Type + + Where: + + * Type is a basic type or a fully qualified Java class name + (including package name), as per the 'Type' production + in the Java Language Specification, Second Edition, + Chapter 18. + + * Identifier is a Java identifier, as per the 'Identifier' + production in the Java Language Specification, Second + Edition, Chapter 18. + + Example: + + java.lang.String nickName( java.lang.String, int ) + + + + + + + + + + The example element contains an informal description + of an example of the use of this function. + + + + + + + + + + Function extensions are for tool use only and must not affect + the behavior of a container. + + + + + + + + + + + + + + + Defines an action in this tag library that is implemented + as a .tag file. + + The tag-file element has two required subelements: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name The unique action name + + path Where to find the .tag file implementing this + action, relative to the root of the web + application or the root of the JAR file for a + tag library packaged in a JAR. This must + begin with /WEB-INF/tags if the .tag file + resides in the WAR, or /META-INF/tags if the + .tag file resides in a JAR. + + example Optional informal description of an + example of a use of this tag + + tag-extension Zero or more extensions that provide extra + information about this tag, for tool + consumption + + + + + + + + + + + + + The example element contains an informal description + of an example of the use of a tag. + + + + + + + + + + Tag extensions are for tool use only and must not affect + the behavior of a container. + + + + + + + + + + + + + + + The tag defines a unique tag in this tag library. It has one + attribute, id. + + The tag element may have several subelements defining: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name The unique action name + + tag-class The tag handler class implementing + javax.servlet.jsp.tagext.JspTag + + tei-class An optional subclass of + javax.servlet.jsp.tagext.TagExtraInfo + + body-content The body content type + + variable Optional scripting variable information + + attribute All attributes of this action that are + evaluated prior to invocation. + + dynamic-attributes Whether this tag supports additional + attributes with dynamic names. If + true, the tag-class must implement the + javax.servlet.jsp.tagext.DynamicAttributes + interface. Defaults to false. + + example Optional informal description of an + example of a use of this tag + + tag-extension Zero or more extensions that provide extra + information about this tag, for tool + consumption + + + + + + + + + + + + Defines the subclass of javax.servlet.jsp.tagext.JspTag + that implements the request time semantics for + this tag. (required) + + + + + + + + + + Defines the subclass of javax.servlet.jsp.tagext.TagExtraInfo + for this tag. (optional) + + If this is not given, the class is not consulted at + translation time. + + + + + + + + + Specifies the format for the body of this tag. + The default in JSP 1.2 was "JSP" but because this + is an invalid setting for simple tag handlers, there + is no longer a default in JSP 2.0. A reasonable + default for simple tag handlers is "scriptless" if + the tag can have a body. + + + + + + + + + + + + The example element contains an informal description + of an example of the use of a tag. + + + + + + + + + + Tag extensions are for tool use only and must not affect + the behavior of a container. + + + + + + + + + + + + + + + The attribute element defines an attribute for the nesting + tag. The attribute element may have several subelements + defining: + + description a description of the attribute + + name the name of the attribute + + required whether the attribute is required or + optional + + rtexprvalue whether the attribute is a runtime attribute + + type the type of the attributes + + fragment whether this attribute is a fragment + + + + + + + + + + + Defines if the nesting attribute is required or + optional. + + If not present then the default is "false", i.e + the attribute is optional. + + + + + + + + + + + + Defines if the nesting attribute can have scriptlet + expressions as a value, i.e the value of the + attribute may be dynamically calculated at request + time, as opposed to a static value determined at + translation time. + + If not present then the default is "false", i.e the + attribute has a static value + + + + + + + + + + Defines the Java type of the attributes value. For + static values (those determined at translation time) + the type is always java.lang.String. + + + + + + + + + + "true" if this attribute is of type + javax.jsp.tagext.JspFragment, representing dynamic + content that can be re-evaluated as many times + as needed by the tag handler. If omitted or "false", + the default is still type="java.lang.String" + + + + + + + + + + + + + + + + + Defines the canonical name of a tag or attribute being + defined. + + The name must conform to the lexical rules for an NMTOKEN. + + + + + + + + + + + + + + + + The tld-extensionType is used to indicate + extensions to a specific TLD element. + + It is used by elements to designate an extension block + that is targeted to a specific extension designated by + a set of extension elements that are declared by a + namespace. The namespace identifies the extension to + the tool that processes the extension. + + The type of the extension-element is abstract. Therefore, + a concrete type must be specified by the TLD using + xsi:type attribute for each extension-element. + + + + + + + + + + + + + + + + + + + + The taglib tag is the document root, it defines: + + description a simple string describing the "use" of this taglib, + should be user discernible + + display-name the display-name element contains a + short name that is intended to be displayed + by tools + + icon optional icon that can be used by tools + + tlib-version the version of the tag library implementation + + short-name a simple default short name that could be + used by a JSP authoring tool to create + names with a mnemonic value; for example, + the it may be used as the preferred prefix + value in taglib directives + + uri a uri uniquely identifying this taglib + + validator optional TagLibraryValidator information + + listener optional event listener specification + + tag tags in this tag library + + tag-file tag files in this tag library + + function zero or more EL functions defined in this + tag library + + taglib-extension zero or more extensions that provide extra + information about this taglib, for tool + consumption + + + + + + + + + + Describes this version (number) of the taglibrary. + It is described as a dewey decimal. + + + + + + + + + + + Defines a simple default name that could be used by + a JSP authoring tool to create names with a + mnemonicvalue; for example, it may be used as the + preferred prefix value in taglib directives. Do + not use white space, and do not start with digits + or underscore. + + + + + + + + + + Defines a public URI that uniquely identifies this + version of the taglibrary. Leave it empty if it + does not apply. + + + + + + + + + + + + + + + + + Taglib extensions are for tool use only and must not affect + the behavior of a container. + + + + + + + + + + Describes the JSP version (number) this taglibrary + requires in order to function (dewey decimal) + + + + + + + + + + + + + + + A validator that can be used to validate + the conformance of a JSP page to using this tag library is + defined by a validatorType. + + + + + + + + + + + Defines the TagLibraryValidator class that can be used + to validate the conformance of a JSP page to using this + tag library. + + + + + + + + + The init-param element contains a name/value pair as an + initialization param. + + + + + + + + + + + + + + + + + This type defines scope of the scripting variable. See + TagExtraInfo for details. The allowed values are, + "NESTED", "AT_BEGIN" and "AT_END". + + + + + + + + + + + + + + + + + + + + The variableType provides information on the scripting + variables defined by using this tag. It is a (translation + time) error for a tag that has one or more variable + subelements to have a TagExtraInfo class that returns a + non-null value from a call to getVariableInfo(). + + The subelements of variableType are of the form: + + description Optional description of this + variable + + name-given The variable name as a constant + + name-from-attribute The name of an attribute whose + (translation time) value will + give the name of the + variable. One of name-given or + name-from-attribute is required. + + variable-class Name of the class of the variable. + java.lang.String is default. + + declare Whether the variable is declared + or not. True is the default. + + scope The scope of the scripting variable + defined. NESTED is default. + + + + + + + + + + + + The name for the scripting variable. + + + + + + + + + + The name of an attribute whose + (translation-time) value will give the name of + the variable. + + + + + + + + + + The optional name of the class for the scripting + variable. The default is java.lang.String. + + + + + + + + + + + + Whether the scripting variable is to be defined + or not. See TagExtraInfo for details. This + element is optional and "true" is the default. + + + + + + + + + The element is optional and "NESTED" is the default. + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-jsptaglibrary_2_1.xsd b/java/jakarta/servlet/resources/web-jsptaglibrary_2_1.xsd new file mode 100644 index 0000000..00975cd --- /dev/null +++ b/java/jakarta/servlet/resources/web-jsptaglibrary_2_1.xsd @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/jakarta/servlet/resources/web-jsptaglibrary_3_0.xsd b/java/jakarta/servlet/resources/web-jsptaglibrary_3_0.xsd new file mode 100644 index 0000000..30c3444 --- /dev/null +++ b/java/jakarta/servlet/resources/web-jsptaglibrary_3_0.xsd @@ -0,0 +1,1109 @@ + + + + + + Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + ... + + + The instance documents may indicate the published + version of the schema using xsi:schemaLocation attribute + for Jakarta EE namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_3_0.xsd + + ]]> + + + + + + + + + + + + + The taglib tag is the document root. + The definition of taglib is provided + by the tldTaglibType. + + + + + + + + The taglib element contains, among other things, tag and + tag-file elements. + The name subelements of these elements must each be unique. + + + + + + + + + + + The taglib element contains function elements. + The name subelements of these elements must each be unique. + + + + + + + + + + + + + + + + Specifies the type of body that is valid for a tag. + This value is used by the JSP container to validate + that a tag invocation has the correct body syntax and + by page composition tools to assist the page author + in providing a valid tag body. + + There are currently four values specified: + + tagdependent The body of the tag is interpreted by the tag + implementation itself, and is most likely + in a different "language", e.g embedded SQL + statements. + + JSP The body of the tag contains nested JSP + syntax. + + empty The body must be empty + + scriptless The body accepts only template text, EL + Expressions, and JSP action elements. No + scripting elements are allowed. + + + + + + + + + + + + + + + + + + + + + Defines the canonical name of a tag or attribute being + defined. + + The name must conform to the lexical rules for an NMTOKEN. + + + + + + + + + + + + + + + + A validator that can be used to validate + the conformance of a JSP page to using this tag library is + defined by a validatorType. + + + + + + + + + + Defines the TagLibraryValidator class that can be used + to validate the conformance of a JSP page to using this + tag library. + + + + + + + + + The init-param element contains a name/value pair as an + initialization param. + + + + + + + + + + + + + + + + The tag defines a unique tag in this tag library. It has one + attribute, id. + + The tag element may have several subelements defining: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name The unique action name + + tag-class The tag handler class implementing + jakarta.servlet.jsp.tagext.JspTag + + tei-class An optional subclass of + jakarta.servlet.jsp.tagext.TagExtraInfo + + body-content The body content type + + variable Optional scripting variable information + + attribute All attributes of this action that are + evaluated prior to invocation. + + dynamic-attributes Whether this tag supports additional + attributes with dynamic names. If + true, the tag-class must implement the + jakarta.servlet.jsp.tagext.DynamicAttributes + interface. Defaults to false. + + example Optional informal description of an + example of a use of this tag + + tag-extension Zero or more extensions that provide extra + information about this tag, for tool + consumption + + + + + + + + + + + Defines the subclass of jakarta.serlvet.jsp.tagext.JspTag + that implements the request time semantics for + this tag. (required) + + + + + + + + + Defines the subclass of jakarta.servlet.jsp.tagext.TagExtraInfo + for this tag. (optional) + + If this is not given, the class is not consulted at + translation time. + + + + + + + + + Specifies the format for the body of this tag. + The default in JSP 1.2 was "JSP" but because this + is an invalid setting for simple tag handlers, there + is no longer a default in JSP 2.0. A reasonable + default for simple tag handlers is "scriptless" if + the tag can have a body. + + + + + + + + + + + + The example element contains an informal description + of an example of the use of a tag. + + + + + + + + + Tag extensions are for tool use only and must not affect + the behavior of a container. + + + + + + + + + + + + + + + + Defines an action in this tag library that is implemented + as a .tag file. + + The tag-file element has two required subelements: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name The unique action name + + path Where to find the .tag file implementing this + action, relative to the root of the web + application or the root of the JAR file for a + tag library packaged in a JAR. This must + begin with /WEB-INF/tags if the .tag file + resides in the WAR, or /META-INF/tags if the + .tag file resides in a JAR. + + example Optional informal description of an + example of a use of this tag + + tag-extension Zero or more extensions that provide extra + information about this tag, for tool + consumption + + + + + + + + + + + + The example element contains an informal description + of an example of the use of a tag. + + + + + + + + + Tag extensions are for tool use only and must not affect + the behavior of a container. + + + + + + + + + + + + + + + + The function element is used to provide information on each + function in the tag library that is to be exposed to the EL. + + The function element may have several subelements defining: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name A unique name for this function + + function-class Provides the name of the Java class that + implements the function + + function-signature Provides the signature, as in the Java + Language Specification, of the Java + method that is to be used to implement + the function. + + example Optional informal description of an + example of a use of this function + + function-extension Zero or more extensions that provide extra + information about this function, for tool + consumption + + + + + + + + + + A unique name for this function. + + + + + + + + + Provides the fully-qualified class name of the Java + class containing the static method that implements + the function. + + + + + + + + + Provides the signature, of the static Java method that is + to be used to implement the function. The syntax of the + function-signature element is as follows: + + FunctionSignature ::= ReturnType S MethodName S? + '(' S? Parameters? S? ')' + + ReturnType ::= Type + + MethodName ::= Identifier + + Parameters ::= Parameter + | ( Parameter S? ',' S? Parameters ) + + Parameter ::= Type + + Where: + + * Type is a basic type or a fully qualified + Java class name (including package name), + as per the 'Type' production in the Java + Language Specification, Second Edition, + Chapter 18. + + * Identifier is a Java identifier, as per + the 'Identifier' production in the Java + Language Specification, Second + Edition, Chapter 18. + + Example: + + java.lang.String nickName( java.lang.String, int ) + + + + + + + + + The example element contains an informal description + of an example of the use of this function. + + + + + + + + + Function extensions are for tool use only and must not + affect the behavior of a container. + + + + + + + + + + + + + + + + The taglib tag is the document root, it defines: + + description a simple string describing the "use" of this + taglib, should be user discernable + + display-name the display-name element contains a + short name that is intended to be displayed + by tools + + icon optional icon that can be used by tools + + tlib-version the version of the tag library implementation + + short-name a simple default short name that could be + used by a JSP authoring tool to create + names with a mnemonic value; for example, + the it may be used as the prefered prefix + value in taglib directives + + uri a uri uniquely identifying this taglib + + validator optional TagLibraryValidator information + + listener optional event listener specification + + tag tags in this tag library + + tag-file tag files in this tag library + + function zero or more EL functions defined in this + tag library + + taglib-extension zero or more extensions that provide extra + information about this taglib, for tool + consumption + + + + + + + + + + Describes this version (number) of the taglibrary. + It is described as a dewey decimal. + + + + + + + + + Defines a simple default name that could be used by + a JSP authoring tool to create names with a + mnemonicvalue; for example, it may be used as the + preferred prefix value in taglib directives. Do + not use white space, and do not start with digits + or underscore. + + + + + + + + + Defines a public URI that uniquely identifies this + version of the taglibrary. Leave it empty if it + does not apply. + + + + + + + + + + + + + + + + Taglib extensions are for tool use only and must not + affect the behavior of a container. + + + + + + + + + + Describes the JSP version (number) this taglibrary + requires in order to function (dewey decimal) + + + + + + + + + + + + + + + The variableType provides information on the scripting + variables defined by using this tag. It is a (translation + time) error for a tag that has one or more variable + subelements to have a TagExtraInfo class that returns a + non-null value from a call to getVariableInfo(). + + The subelements of variableType are of the form: + + description Optional description of this + variable + + name-given The variable name as a constant + + name-from-attribute The name of an attribute whose + (translation time) value will + give the name of the + variable. One of name-given or + name-from-attribute is required. + + variable-class Name of the class of the variable. + java.lang.String is default. + + declare Whether the variable is declared + or not. True is the default. + + scope The scope of the scripting varaible + defined. NESTED is default. + + + + + + + + + + + The name for the scripting variable. + + + + + + + + + The name of an attribute whose + (translation-time) value will give the name of + the variable. + + + + + + + + + + The optional name of the class for the scripting + variable. The default is java.lang.String. + + + + + + + + + Whether the scripting variable is to be defined + or not. See TagExtraInfo for details. This + element is optional and "true" is the default. + + + + + + + + + The element is optional and "NESTED" is the default. + + + + + + + + + + + + + + + + This type defines scope of the scripting variable. See + TagExtraInfo for details. The allowed values are, + "NESTED", "AT_BEGIN" and "AT_END". + + + + + + + + + + + + + + + + + + + + The attribute element defines an attribute for the nesting + tag. The attribute element may have several subelements + defining: + + description a description of the attribute + + name the name of the attribute + + required whether the attribute is required or + optional + + rtexprvalue whether the attribute is a runtime attribute + + type the type of the attributes + + fragment whether this attribute is a fragment + + deferred-value present if this attribute is to be parsed as a + jakarta.el.ValueExpression + + deferred-method present if this attribute is to be parsed as a + jakarta.el.MethodExpression + + + + + + + + + + + Defines if the nesting attribute is required or + optional. + + If not present then the default is "false", i.e + the attribute is optional. + + + + + + + + + + + + Defines if the nesting attribute can have scriptlet + expressions as a value, i.e the value of the + attribute may be dynamically calculated at request + time, as opposed to a static value determined at + translation time. + If not present then the default is "false", i.e the + attribute has a static value + + + + + + + + + Defines the Java type of the attributes value. + If this element is omitted, the expected type is + assumed to be "java.lang.Object". + + + + + + + + + + + Present if the value for this attribute is to be + passed to the tag handler as a + jakarta.el.ValueExpression. This allows for deferred + evaluation of EL expressions. An optional subelement + will contain the expected type that the value will + be coerced to after evaluation of the expression. + The type defaults to Object if one is not provided. + + + + + + + + + Present if the value for this attribute is to be + passed to the tag handler as a + jakarta.el.MethodExpression. This allows for deferred + evaluation of an EL expression that identifies a + method to be invoked on an Object. An optional + subelement will contain the expected method + signature. The signature defaults to "void method()" + if one is not provided. + + + + + + + + + + + "true" if this attribute is of type + jakarta.servlet.jsp.tagext.JspFragment, representing dynamic + content that can be re-evaluated as many times + as needed by the tag handler. If omitted or "false", + the default is still type="java.lang.String" + + + + + + + + + + + + + + + + + Defines information about how to provide the value for a + tag handler attribute that accepts a jakarta.el.ValueExpression. + + The deferred-value element has one optional subelement: + + type the expected type of the attribute + + + + + + + + + The fully-qualified name of the Java type that is the + expected type for this deferred expression. If this + element is omitted, the expected type is assumed to be + "java.lang.Object". + + + + + + + + + + + + + + + + Defines information about how to provide the value for a + tag handler attribute that accepts a jakarta.el.MethodExpression. + + The deferred-method element has one optional subelement: + + method-signature Provides the signature, as in the Java + Language Specifies, that is expected for + the method being identified by the + expression. + + + + + + + + + Provides the expected signature of the method identified + by the jakarta.el.MethodExpression. + + This disambiguates overloaded methods and ensures that + the return value is of the expected type. + + The syntax of the method-signature element is identical + to that of the function-signature element. See the + documentation for function-signature for more details. + + The name of the method is for documentation purposes only + and is ignored by the JSP container. + + Example: + + boolean validate(java.lang.String) + + + + + + + + + + + + + + + + The tld-extensionType is used to indicate + extensions to a specific TLD element. + + It is used by elements to designate an extension block + that is targeted to a specific extension designated by + a set of extension elements that are declared by a + namespace. The namespace identifies the extension to + the tool that processes the extension. + + The type of the extension-element is abstract. Therefore, + a concrete type must be specified by the TLD using + xsi:type attribute for each extension-element. + + + + + + + + + + + + + + + + + + The extensibleType is an abstract base type that is used to + define the type of extension-elements. Instance documents + must substitute a known type to define the extension by + using xsi:type attribute to define the actual type of + extension-elements. + + + + + + + diff --git a/java/jakarta/servlet/resources/web-jsptaglibrary_3_1.xsd b/java/jakarta/servlet/resources/web-jsptaglibrary_3_1.xsd new file mode 100644 index 0000000..acdaea8 --- /dev/null +++ b/java/jakarta/servlet/resources/web-jsptaglibrary_3_1.xsd @@ -0,0 +1,1109 @@ + + + + + + Copyright (c) 2009, 2021 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + + + + + + + ... + + + The instance documents may indicate the published + version of the schema using xsi:schemaLocation attribute + for Jakarta EE namespace with the following location: + + https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_3_1.xsd + + ]]> + + + + + + + + + + + + + The taglib tag is the document root. + The definition of taglib is provided + by the tldTaglibType. + + + + + + + + The taglib element contains, among other things, tag and + tag-file elements. + The name subelements of these elements must each be unique. + + + + + + + + + + + The taglib element contains function elements. + The name subelements of these elements must each be unique. + + + + + + + + + + + + + + + + Specifies the type of body that is valid for a tag. + This value is used by the JSP container to validate + that a tag invocation has the correct body syntax and + by page composition tools to assist the page author + in providing a valid tag body. + + There are currently four values specified: + + tagdependent The body of the tag is interpreted by the tag + implementation itself, and is most likely + in a different "language", e.g embedded SQL + statements. + + JSP The body of the tag contains nested JSP + syntax. + + empty The body must be empty + + scriptless The body accepts only template text, EL + Expressions, and JSP action elements. No + scripting elements are allowed. + + + + + + + + + + + + + + + + + + + + + Defines the canonical name of a tag or attribute being + defined. + + The name must conform to the lexical rules for an NMTOKEN. + + + + + + + + + + + + + + + + A validator that can be used to validate + the conformance of a JSP page to using this tag library is + defined by a validatorType. + + + + + + + + + + Defines the TagLibraryValidator class that can be used + to validate the conformance of a JSP page to using this + tag library. + + + + + + + + + The init-param element contains a name/value pair as an + initialization param. + + + + + + + + + + + + + + + + The tag defines a unique tag in this tag library. It has one + attribute, id. + + The tag element may have several subelements defining: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name The unique action name + + tag-class The tag handler class implementing + jakarta.servlet.jsp.tagext.JspTag + + tei-class An optional subclass of + jakarta.servlet.jsp.tagext.TagExtraInfo + + body-content The body content type + + variable Optional scripting variable information + + attribute All attributes of this action that are + evaluated prior to invocation. + + dynamic-attributes Whether this tag supports additional + attributes with dynamic names. If + true, the tag-class must implement the + jakarta.servlet.jsp.tagext.DynamicAttributes + interface. Defaults to false. + + example Optional informal description of an + example of a use of this tag + + tag-extension Zero or more extensions that provide extra + information about this tag, for tool + consumption + + + + + + + + + + + Defines the subclass of jakarta.serlvet.jsp.tagext.JspTag + that implements the request time semantics for + this tag. (required) + + + + + + + + + Defines the subclass of jakarta.servlet.jsp.tagext.TagExtraInfo + for this tag. (optional) + + If this is not given, the class is not consulted at + translation time. + + + + + + + + + Specifies the format for the body of this tag. + The default in JSP 1.2 was "JSP" but because this + is an invalid setting for simple tag handlers, there + is no longer a default in JSP 2.0. A reasonable + default for simple tag handlers is "scriptless" if + the tag can have a body. + + + + + + + + + + + + The example element contains an informal description + of an example of the use of a tag. + + + + + + + + + Tag extensions are for tool use only and must not affect + the behavior of a container. + + + + + + + + + + + + + + + + Defines an action in this tag library that is implemented + as a .tag file. + + The tag-file element has two required subelements: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name The unique action name + + path Where to find the .tag file implementing this + action, relative to the root of the web + application or the root of the JAR file for a + tag library packaged in a JAR. This must + begin with /WEB-INF/tags if the .tag file + resides in the WAR, or /META-INF/tags if the + .tag file resides in a JAR. + + example Optional informal description of an + example of a use of this tag + + tag-extension Zero or more extensions that provide extra + information about this tag, for tool + consumption + + + + + + + + + + + + The example element contains an informal description + of an example of the use of a tag. + + + + + + + + + Tag extensions are for tool use only and must not affect + the behavior of a container. + + + + + + + + + + + + + + + + The function element is used to provide information on each + function in the tag library that is to be exposed to the EL. + + The function element may have several subelements defining: + + description Optional tag-specific information + + display-name A short name that is intended to be + displayed by tools + + icon Optional icon element that can be used + by tools + + name A unique name for this function + + function-class Provides the name of the Java class that + implements the function + + function-signature Provides the signature, as in the Java + Language Specification, of the Java + method that is to be used to implement + the function. + + example Optional informal description of an + example of a use of this function + + function-extension Zero or more extensions that provide extra + information about this function, for tool + consumption + + + + + + + + + + A unique name for this function. + + + + + + + + + Provides the fully-qualified class name of the Java + class containing the static method that implements + the function. + + + + + + + + + Provides the signature, of the static Java method that is + to be used to implement the function. The syntax of the + function-signature element is as follows: + + FunctionSignature ::= ReturnType S MethodName S? + '(' S? Parameters? S? ')' + + ReturnType ::= Type + + MethodName ::= Identifier + + Parameters ::= Parameter + | ( Parameter S? ',' S? Parameters ) + + Parameter ::= Type + + Where: + + * Type is a basic type or a fully qualified + Java class name (including package name), + as per the 'Type' production in the Java + Language Specification, Second Edition, + Chapter 18. + + * Identifier is a Java identifier, as per + the 'Identifier' production in the Java + Language Specification, Second + Edition, Chapter 18. + + Example: + + java.lang.String nickName( java.lang.String, int ) + + + + + + + + + The example element contains an informal description + of an example of the use of this function. + + + + + + + + + Function extensions are for tool use only and must not + affect the behavior of a container. + + + + + + + + + + + + + + + + The taglib tag is the document root, it defines: + + description a simple string describing the "use" of this + taglib, should be user discernable + + display-name the display-name element contains a + short name that is intended to be displayed + by tools + + icon optional icon that can be used by tools + + tlib-version the version of the tag library implementation + + short-name a simple default short name that could be + used by a JSP authoring tool to create + names with a mnemonic value; for example, + the it may be used as the prefered prefix + value in taglib directives + + uri a uri uniquely identifying this taglib + + validator optional TagLibraryValidator information + + listener optional event listener specification + + tag tags in this tag library + + tag-file tag files in this tag library + + function zero or more EL functions defined in this + tag library + + taglib-extension zero or more extensions that provide extra + information about this taglib, for tool + consumption + + + + + + + + + + Describes this version (number) of the taglibrary. + It is described as a dewey decimal. + + + + + + + + + Defines a simple default name that could be used by + a JSP authoring tool to create names with a + mnemonicvalue; for example, it may be used as the + preferred prefix value in taglib directives. Do + not use white space, and do not start with digits + or underscore. + + + + + + + + + Defines a public URI that uniquely identifies this + version of the taglibrary. Leave it empty if it + does not apply. + + + + + + + + + + + + + + + + Taglib extensions are for tool use only and must not + affect the behavior of a container. + + + + + + + + + + Describes the JSP version (number) this taglibrary + requires in order to function (dewey decimal) + + + + + + + + + + + + + + + The variableType provides information on the scripting + variables defined by using this tag. It is a (translation + time) error for a tag that has one or more variable + subelements to have a TagExtraInfo class that returns a + non-null value from a call to getVariableInfo(). + + The subelements of variableType are of the form: + + description Optional description of this + variable + + name-given The variable name as a constant + + name-from-attribute The name of an attribute whose + (translation time) value will + give the name of the + variable. One of name-given or + name-from-attribute is required. + + variable-class Name of the class of the variable. + java.lang.String is default. + + declare Whether the variable is declared + or not. True is the default. + + scope The scope of the scripting varaible + defined. NESTED is default. + + + + + + + + + + + The name for the scripting variable. + + + + + + + + + The name of an attribute whose + (translation-time) value will give the name of + the variable. + + + + + + + + + + The optional name of the class for the scripting + variable. The default is java.lang.String. + + + + + + + + + Whether the scripting variable is to be defined + or not. See TagExtraInfo for details. This + element is optional and "true" is the default. + + + + + + + + + The element is optional and "NESTED" is the default. + + + + + + + + + + + + + + + + This type defines scope of the scripting variable. See + TagExtraInfo for details. The allowed values are, + "NESTED", "AT_BEGIN" and "AT_END". + + + + + + + + + + + + + + + + + + + + The attribute element defines an attribute for the nesting + tag. The attribute element may have several subelements + defining: + + description a description of the attribute + + name the name of the attribute + + required whether the attribute is required or + optional + + rtexprvalue whether the attribute is a runtime attribute + + type the type of the attributes + + fragment whether this attribute is a fragment + + deferred-value present if this attribute is to be parsed as a + jakarta.el.ValueExpression + + deferred-method present if this attribute is to be parsed as a + jakarta.el.MethodExpression + + + + + + + + + + + Defines if the nesting attribute is required or + optional. + + If not present then the default is "false", i.e + the attribute is optional. + + + + + + + + + + + + Defines if the nesting attribute can have scriptlet + expressions as a value, i.e the value of the + attribute may be dynamically calculated at request + time, as opposed to a static value determined at + translation time. + If not present then the default is "false", i.e the + attribute has a static value + + + + + + + + + Defines the Java type of the attributes value. + If this element is omitted, the expected type is + assumed to be "java.lang.Object". + + + + + + + + + + + Present if the value for this attribute is to be + passed to the tag handler as a + jakarta.el.ValueExpression. This allows for deferred + evaluation of EL expressions. An optional subelement + will contain the expected type that the value will + be coerced to after evaluation of the expression. + The type defaults to Object if one is not provided. + + + + + + + + + Present if the value for this attribute is to be + passed to the tag handler as a + jakarta.el.MethodExpression. This allows for deferred + evaluation of an EL expression that identifies a + method to be invoked on an Object. An optional + subelement will contain the expected method + signature. The signature defaults to "void method()" + if one is not provided. + + + + + + + + + + + "true" if this attribute is of type + jakarta.servlet.jsp.tagext.JspFragment, representing dynamic + content that can be re-evaluated as many times + as needed by the tag handler. If omitted or "false", + the default is still type="java.lang.String" + + + + + + + + + + + + + + + + + Defines information about how to provide the value for a + tag handler attribute that accepts a jakarta.el.ValueExpression. + + The deferred-value element has one optional subelement: + + type the expected type of the attribute + + + + + + + + + The fully-qualified name of the Java type that is the + expected type for this deferred expression. If this + element is omitted, the expected type is assumed to be + "java.lang.Object". + + + + + + + + + + + + + + + + Defines information about how to provide the value for a + tag handler attribute that accepts a jakarta.el.MethodExpression. + + The deferred-method element has one optional subelement: + + method-signature Provides the signature, as in the Java + Language Specifies, that is expected for + the method being identified by the + expression. + + + + + + + + + Provides the expected signature of the method identified + by the jakarta.el.MethodExpression. + + This disambiguates overloaded methods and ensures that + the return value is of the expected type. + + The syntax of the method-signature element is identical + to that of the function-signature element. See the + documentation for function-signature for more details. + + The name of the method is for documentation purposes only + and is ignored by the JSP container. + + Example: + + boolean validate(java.lang.String) + + + + + + + + + + + + + + + + The tld-extensionType is used to indicate + extensions to a specific TLD element. + + It is used by elements to designate an extension block + that is targeted to a specific extension designated by + a set of extension elements that are declared by a + namespace. The namespace identifies the extension to + the tool that processes the extension. + + The type of the extension-element is abstract. Therefore, + a concrete type must be specified by the TLD using + xsi:type attribute for each extension-element. + + + + + + + + + + + + + + + + + + The extensibleType is an abstract base type that is used to + define the type of extension-elements. Instance documents + must substitute a known type to define the extension by + using xsi:type attribute to define the actual type of + extension-elements. + + + + + + + diff --git a/java/jakarta/servlet/resources/xml.xsd b/java/jakarta/servlet/resources/xml.xsd new file mode 100644 index 0000000..82bb2cc --- /dev/null +++ b/java/jakarta/servlet/resources/xml.xsd @@ -0,0 +1,97 @@ + + + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/java/jakarta/transaction/HeuristicCommitException.java b/java/jakarta/transaction/HeuristicCommitException.java new file mode 100644 index 0000000..455e989 --- /dev/null +++ b/java/jakarta/transaction/HeuristicCommitException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public class HeuristicCommitException extends Exception { + + private static final long serialVersionUID = -3977609782149921760L; + + public HeuristicCommitException() { + super(); + } + + public HeuristicCommitException(String msg) { + super(msg); + } +} diff --git a/java/jakarta/transaction/HeuristicMixedException.java b/java/jakarta/transaction/HeuristicMixedException.java new file mode 100644 index 0000000..7d05344 --- /dev/null +++ b/java/jakarta/transaction/HeuristicMixedException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public class HeuristicMixedException extends Exception { + + private static final long serialVersionUID = 2345014349685956666L; + + public HeuristicMixedException() { + super(); + } + + public HeuristicMixedException(String msg) { + super(msg); + } +} diff --git a/java/jakarta/transaction/HeuristicRollbackException.java b/java/jakarta/transaction/HeuristicRollbackException.java new file mode 100644 index 0000000..c7312f5 --- /dev/null +++ b/java/jakarta/transaction/HeuristicRollbackException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public class HeuristicRollbackException extends Exception { + + private static final long serialVersionUID = -3483618944556408897L; + + public HeuristicRollbackException() { + super(); + } + + public HeuristicRollbackException(String msg) { + super(msg); + } +} diff --git a/java/jakarta/transaction/InvalidTransactionException.java b/java/jakarta/transaction/InvalidTransactionException.java new file mode 100644 index 0000000..f75b185 --- /dev/null +++ b/java/jakarta/transaction/InvalidTransactionException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public class InvalidTransactionException extends java.rmi.RemoteException { + + private static final long serialVersionUID = 3597320220337691496L; + + public InvalidTransactionException() { + super(); + } + + public InvalidTransactionException(String msg) { + super(msg); + } +} diff --git a/java/jakarta/transaction/NotSupportedException.java b/java/jakarta/transaction/NotSupportedException.java new file mode 100644 index 0000000..89ffc63 --- /dev/null +++ b/java/jakarta/transaction/NotSupportedException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public class NotSupportedException extends Exception { + + private static final long serialVersionUID = 56870312332816390L; + + public NotSupportedException() { + super(); + } + + public NotSupportedException(String msg) { + super(msg); + } +} diff --git a/java/jakarta/transaction/RollbackException.java b/java/jakarta/transaction/RollbackException.java new file mode 100644 index 0000000..d44cfcd --- /dev/null +++ b/java/jakarta/transaction/RollbackException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public class RollbackException extends Exception { + + private static final long serialVersionUID = 4151607774785285395L; + + public RollbackException() { + super(); + } + + public RollbackException(String msg) { + super(msg); + } +} diff --git a/java/jakarta/transaction/Status.java b/java/jakarta/transaction/Status.java new file mode 100644 index 0000000..9b113fc --- /dev/null +++ b/java/jakarta/transaction/Status.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public interface Status { + int STATUS_ACTIVE = 0; + int STATUS_MARKED_ROLLBACK = 1; + int STATUS_PREPARED = 2; + int STATUS_COMMITTED = 3; + int STATUS_ROLLEDBACK = 4; + int STATUS_UNKNOWN = 5; + int STATUS_NO_TRANSACTION = 6; + int STATUS_PREPARING = 7; + int STATUS_COMMITTING = 8; + int STATUS_ROLLING_BACK = 9; +} diff --git a/java/jakarta/transaction/Synchronization.java b/java/jakarta/transaction/Synchronization.java new file mode 100644 index 0000000..b188911 --- /dev/null +++ b/java/jakarta/transaction/Synchronization.java @@ -0,0 +1,23 @@ +/* + * 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 jakarta.transaction; + +public interface Synchronization { + void beforeCompletion(); + + void afterCompletion(int status); +} diff --git a/java/jakarta/transaction/SystemException.java b/java/jakarta/transaction/SystemException.java new file mode 100644 index 0000000..80c251d --- /dev/null +++ b/java/jakarta/transaction/SystemException.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.transaction; + +public class SystemException extends Exception { + + private static final long serialVersionUID = 8615483418828223571L; + + public int errorCode; + + public SystemException() { + super(); + } + + public SystemException(String s) { + super(s); + } + + public SystemException(int errcode) { + super(); + errorCode = errcode; + } + +} diff --git a/java/jakarta/transaction/Transaction.java b/java/jakarta/transaction/Transaction.java new file mode 100644 index 0000000..1f031d2 --- /dev/null +++ b/java/jakarta/transaction/Transaction.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.transaction; + +import javax.transaction.xa.XAResource; + +public interface Transaction { + + void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, + IllegalStateException, SystemException; + + boolean delistResource(XAResource xaRes, int flag) throws IllegalStateException, SystemException; + + boolean enlistResource(XAResource xaRes) throws RollbackException, IllegalStateException, SystemException; + + int getStatus() throws SystemException; + + void registerSynchronization(Synchronization sync) throws RollbackException, IllegalStateException, SystemException; + + void rollback() throws IllegalStateException, SystemException; + + void setRollbackOnly() throws IllegalStateException, SystemException; + +} diff --git a/java/jakarta/transaction/TransactionManager.java b/java/jakarta/transaction/TransactionManager.java new file mode 100644 index 0000000..2bd58e1 --- /dev/null +++ b/java/jakarta/transaction/TransactionManager.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.transaction; + +public interface TransactionManager { + void begin() throws NotSupportedException, SystemException; + + void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, + IllegalStateException, SystemException; + + int getStatus() throws SystemException; + + Transaction getTransaction() throws SystemException; + + void resume(Transaction tobj) throws InvalidTransactionException, IllegalStateException, SystemException; + + void rollback() throws IllegalStateException, SecurityException, SystemException; + + void setRollbackOnly() throws IllegalStateException, SystemException; + + void setTransactionTimeout(int seconds) throws SystemException; + + Transaction suspend() throws SystemException; +} diff --git a/java/jakarta/transaction/TransactionRequiredException.java b/java/jakarta/transaction/TransactionRequiredException.java new file mode 100644 index 0000000..048ca2e --- /dev/null +++ b/java/jakarta/transaction/TransactionRequiredException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public class TransactionRequiredException extends java.rmi.RemoteException { + + private static final long serialVersionUID = -1898806419937446439L; + + public TransactionRequiredException() { + super(); + } + + public TransactionRequiredException(String msg) { + super(msg); + } +} diff --git a/java/jakarta/transaction/TransactionRolledbackException.java b/java/jakarta/transaction/TransactionRolledbackException.java new file mode 100644 index 0000000..f8e01db --- /dev/null +++ b/java/jakarta/transaction/TransactionRolledbackException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.transaction; + +public class TransactionRolledbackException extends java.rmi.RemoteException { + + private static final long serialVersionUID = -3142798139623020577L; + + public TransactionRolledbackException() { + super(); + } + + public TransactionRolledbackException(String msg) { + super(msg); + } +} diff --git a/java/jakarta/transaction/TransactionSynchronizationRegistry.java b/java/jakarta/transaction/TransactionSynchronizationRegistry.java new file mode 100644 index 0000000..b2ac98f --- /dev/null +++ b/java/jakarta/transaction/TransactionSynchronizationRegistry.java @@ -0,0 +1,33 @@ +/* + * 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 jakarta.transaction; + +public interface TransactionSynchronizationRegistry { + Object getTransactionKey(); + + void putResource(Object key, Object value); + + Object getResource(Object key); + + void registerInterposedSynchronization(Synchronization sync); + + int getTransactionStatus(); + + void setRollbackOnly(); + + boolean getRollbackOnly(); +} diff --git a/java/jakarta/transaction/UserTransaction.java b/java/jakarta/transaction/UserTransaction.java new file mode 100644 index 0000000..0506c9c --- /dev/null +++ b/java/jakarta/transaction/UserTransaction.java @@ -0,0 +1,32 @@ +/* + * 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 jakarta.transaction; + +public interface UserTransaction { + void begin() throws NotSupportedException, SystemException; + + void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, + IllegalStateException, SystemException; + + void rollback() throws IllegalStateException, SecurityException, SystemException; + + void setRollbackOnly() throws IllegalStateException, SystemException; + + int getStatus() throws SystemException; + + void setTransactionTimeout(int seconds) throws SystemException; +} diff --git a/java/jakarta/websocket/ClientEndpoint.java b/java/jakarta/websocket/ClientEndpoint.java new file mode 100644 index 0000000..acaa160 --- /dev/null +++ b/java/jakarta/websocket/ClientEndpoint.java @@ -0,0 +1,36 @@ +/* + * 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 jakarta.websocket; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.websocket.ClientEndpointConfig.Configurator; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ClientEndpoint { + String[] subprotocols() default {}; + + Class[] decoders() default {}; + + Class[] encoders() default {}; + + Class configurator() default Configurator.class; +} diff --git a/java/jakarta/websocket/ClientEndpointConfig.java b/java/jakarta/websocket/ClientEndpointConfig.java new file mode 100644 index 0000000..ce3caba --- /dev/null +++ b/java/jakarta/websocket/ClientEndpointConfig.java @@ -0,0 +1,141 @@ +/* + * 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 jakarta.websocket; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLContext; + +public interface ClientEndpointConfig extends EndpointConfig { + + List getPreferredSubprotocols(); + + List getExtensions(); + + SSLContext getSSLContext(); + + Configurator getConfigurator(); + + final class Builder { + + private static final Configurator DEFAULT_CONFIGURATOR = new Configurator() { + }; + + + public static Builder create() { + return new Builder(); + } + + + private Builder() { + // Hide default constructor + } + + private Configurator configurator = DEFAULT_CONFIGURATOR; + private List preferredSubprotocols = Collections.emptyList(); + private List extensions = Collections.emptyList(); + private List> encoders = Collections.emptyList(); + private List> decoders = Collections.emptyList(); + private SSLContext sslContext = null; + + public ClientEndpointConfig build() { + return new DefaultClientEndpointConfig(preferredSubprotocols, extensions, encoders, decoders, sslContext, + configurator); + } + + + public Builder configurator(Configurator configurator) { + if (configurator == null) { + this.configurator = DEFAULT_CONFIGURATOR; + } else { + this.configurator = configurator; + } + return this; + } + + + public Builder preferredSubprotocols(List preferredSubprotocols) { + if (preferredSubprotocols == null || preferredSubprotocols.size() == 0) { + this.preferredSubprotocols = Collections.emptyList(); + } else { + this.preferredSubprotocols = Collections.unmodifiableList(preferredSubprotocols); + } + return this; + } + + + public Builder extensions(List extensions) { + if (extensions == null || extensions.size() == 0) { + this.extensions = Collections.emptyList(); + } else { + this.extensions = Collections.unmodifiableList(extensions); + } + return this; + } + + + public Builder encoders(List> encoders) { + if (encoders == null || encoders.size() == 0) { + this.encoders = Collections.emptyList(); + } else { + this.encoders = Collections.unmodifiableList(encoders); + } + return this; + } + + + public Builder decoders(List> decoders) { + if (decoders == null || decoders.size() == 0) { + this.decoders = Collections.emptyList(); + } else { + this.decoders = Collections.unmodifiableList(decoders); + } + return this; + } + + + public Builder sslContext(SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + } + + + class Configurator { + + /** + * Provides the client with a mechanism to inspect and/or modify the headers that are sent to the server to + * start the WebSocket handshake. + * + * @param headers The HTTP headers + */ + public void beforeRequest(Map> headers) { + // NO-OP + } + + /** + * Provides the client with a mechanism to inspect the handshake response that is returned from the server. + * + * @param handshakeResponse The response + */ + public void afterResponse(HandshakeResponse handshakeResponse) { + // NO-OP + } + } +} diff --git a/java/jakarta/websocket/CloseReason.java b/java/jakarta/websocket/CloseReason.java new file mode 100644 index 0000000..da33c5f --- /dev/null +++ b/java/jakarta/websocket/CloseReason.java @@ -0,0 +1,120 @@ +/* + * 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 jakarta.websocket; + +public class CloseReason { + + private final CloseCode closeCode; + private final String reasonPhrase; + + public CloseReason(CloseReason.CloseCode closeCode, String reasonPhrase) { + this.closeCode = closeCode; + this.reasonPhrase = reasonPhrase; + } + + public CloseCode getCloseCode() { + return closeCode; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + @Override + public String toString() { + return "CloseReason: code [" + closeCode.getCode() + "], reason [" + reasonPhrase + "]"; + } + + public interface CloseCode { + int getCode(); + } + + public enum CloseCodes implements CloseReason.CloseCode { + + NORMAL_CLOSURE(1000), + GOING_AWAY(1001), + PROTOCOL_ERROR(1002), + CANNOT_ACCEPT(1003), + RESERVED(1004), + NO_STATUS_CODE(1005), + CLOSED_ABNORMALLY(1006), + NOT_CONSISTENT(1007), + VIOLATED_POLICY(1008), + TOO_BIG(1009), + NO_EXTENSION(1010), + UNEXPECTED_CONDITION(1011), + SERVICE_RESTART(1012), + TRY_AGAIN_LATER(1013), + TLS_HANDSHAKE_FAILURE(1015); + + private int code; + + CloseCodes(int code) { + this.code = code; + } + + public static CloseCode getCloseCode(final int code) { + if (code > 2999 && code < 5000) { + return new CloseCode() { + @Override + public int getCode() { + return code; + } + }; + } + switch (code) { + case 1000: + return NORMAL_CLOSURE; + case 1001: + return GOING_AWAY; + case 1002: + return PROTOCOL_ERROR; + case 1003: + return CANNOT_ACCEPT; + case 1004: + return RESERVED; + case 1005: + return NO_STATUS_CODE; + case 1006: + return CLOSED_ABNORMALLY; + case 1007: + return NOT_CONSISTENT; + case 1008: + return VIOLATED_POLICY; + case 1009: + return TOO_BIG; + case 1010: + return NO_EXTENSION; + case 1011: + return UNEXPECTED_CONDITION; + case 1012: + return SERVICE_RESTART; + case 1013: + return TRY_AGAIN_LATER; + case 1015: + return TLS_HANDSHAKE_FAILURE; + default: + throw new IllegalArgumentException("Invalid close code: [" + code + "]"); + } + } + + @Override + public int getCode() { + return code; + } + } +} diff --git a/java/jakarta/websocket/ContainerProvider.java b/java/jakarta/websocket/ContainerProvider.java new file mode 100644 index 0000000..8069d89 --- /dev/null +++ b/java/jakarta/websocket/ContainerProvider.java @@ -0,0 +1,58 @@ +/* + * 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 jakarta.websocket; + +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * Use the {@link ServiceLoader} mechanism to provide instances of the WebSocket client container. + */ +public abstract class ContainerProvider { + + private static final String DEFAULT_PROVIDER_CLASS_NAME = "org.apache.tomcat.websocket.WsWebSocketContainer"; + + /** + * Create a new container used to create outgoing WebSocket connections. + * + * @return A newly created container. + */ + public static WebSocketContainer getWebSocketContainer() { + WebSocketContainer result = null; + + ServiceLoader serviceLoader = ServiceLoader.load(ContainerProvider.class); + Iterator iter = serviceLoader.iterator(); + while (result == null && iter.hasNext()) { + result = iter.next().getContainer(); + } + + // Fall-back. Also used by unit tests + if (result == null) { + try { + @SuppressWarnings("unchecked") + Class clazz = + (Class) Class.forName(DEFAULT_PROVIDER_CLASS_NAME); + result = clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) { + // No options left. Just return null. + } + } + return result; + } + + protected abstract WebSocketContainer getContainer(); +} diff --git a/java/jakarta/websocket/DecodeException.java b/java/jakarta/websocket/DecodeException.java new file mode 100644 index 0000000..1fc3159 --- /dev/null +++ b/java/jakarta/websocket/DecodeException.java @@ -0,0 +1,55 @@ +/* + * 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 jakarta.websocket; + +import java.nio.ByteBuffer; + +public class DecodeException extends Exception { + + private static final long serialVersionUID = 1L; + + private ByteBuffer bb; + private String encodedString; + + public DecodeException(ByteBuffer bb, String message, Throwable cause) { + super(message, cause); + this.bb = bb; + } + + public DecodeException(String encodedString, String message, Throwable cause) { + super(message, cause); + this.encodedString = encodedString; + } + + public DecodeException(ByteBuffer bb, String message) { + super(message); + this.bb = bb; + } + + public DecodeException(String encodedString, String message) { + super(message); + this.encodedString = encodedString; + } + + public ByteBuffer getBytes() { + return bb; + } + + public String getText() { + return encodedString; + } +} diff --git a/java/jakarta/websocket/Decoder.java b/java/jakarta/websocket/Decoder.java new file mode 100644 index 0000000..4f0a4e4 --- /dev/null +++ b/java/jakarta/websocket/Decoder.java @@ -0,0 +1,63 @@ +/* + * 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 jakarta.websocket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.nio.ByteBuffer; + +public interface Decoder { + + /** + * Initialise the decoder. The default implementation is a NO-OP. + * + * @param endpointConfig The end-point configuration + */ + default void init(EndpointConfig endpointConfig) { + } + + /** + * Destroy the decoder. The default implementation is a NO-OP. + */ + default void destroy() { + } + + interface Binary extends Decoder { + + T decode(ByteBuffer bytes) throws DecodeException; + + boolean willDecode(ByteBuffer bytes); + } + + interface BinaryStream extends Decoder { + + T decode(InputStream is) throws DecodeException, IOException; + } + + interface Text extends Decoder { + + T decode(String s) throws DecodeException; + + boolean willDecode(String s); + } + + interface TextStream extends Decoder { + + T decode(Reader reader) throws DecodeException, IOException; + } +} diff --git a/java/jakarta/websocket/DefaultClientEndpointConfig.java b/java/jakarta/websocket/DefaultClientEndpointConfig.java new file mode 100644 index 0000000..71a1b9b --- /dev/null +++ b/java/jakarta/websocket/DefaultClientEndpointConfig.java @@ -0,0 +1,88 @@ +/* + * 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 jakarta.websocket; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.net.ssl.SSLContext; + +final class DefaultClientEndpointConfig implements ClientEndpointConfig { + + private final List preferredSubprotocols; + private final List extensions; + private final List> encoders; + private final List> decoders; + private final SSLContext sslContext; + private final Map userProperties = new ConcurrentHashMap<>(); + private final Configurator configurator; + + + DefaultClientEndpointConfig(List preferredSubprotocols, List extensions, + List> encoders, List> decoders, SSLContext sslContext, + Configurator configurator) { + this.preferredSubprotocols = preferredSubprotocols; + this.extensions = extensions; + this.encoders = encoders; + this.decoders = decoders; + this.sslContext = sslContext; + this.configurator = configurator; + } + + + @Override + public List getPreferredSubprotocols() { + return preferredSubprotocols; + } + + + @Override + public List getExtensions() { + return extensions; + } + + + @Override + public List> getEncoders() { + return encoders; + } + + + @Override + public List> getDecoders() { + return decoders; + } + + + @Override + public SSLContext getSSLContext() { + return sslContext; + } + + + @Override + public Map getUserProperties() { + return userProperties; + } + + + @Override + public Configurator getConfigurator() { + return configurator; + } +} diff --git a/java/jakarta/websocket/DeploymentException.java b/java/jakarta/websocket/DeploymentException.java new file mode 100644 index 0000000..75bb027 --- /dev/null +++ b/java/jakarta/websocket/DeploymentException.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.websocket; + +public class DeploymentException extends Exception { + + private static final long serialVersionUID = 1L; + + public DeploymentException(String message) { + super(message); + } + + public DeploymentException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/jakarta/websocket/EncodeException.java b/java/jakarta/websocket/EncodeException.java new file mode 100644 index 0000000..21e8598 --- /dev/null +++ b/java/jakarta/websocket/EncodeException.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.websocket; + +public class EncodeException extends Exception { + + private static final long serialVersionUID = 1L; + + private Object object; + + public EncodeException(Object object, String message) { + super(message); + this.object = object; + } + + public EncodeException(Object object, String message, Throwable cause) { + super(message, cause); + this.object = object; + } + + public Object getObject() { + return this.object; + } +} diff --git a/java/jakarta/websocket/Encoder.java b/java/jakarta/websocket/Encoder.java new file mode 100644 index 0000000..808cf3d --- /dev/null +++ b/java/jakarta/websocket/Encoder.java @@ -0,0 +1,59 @@ +/* + * 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 jakarta.websocket; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; + +public interface Encoder { + + /** + * Initialise the encoder. The default implementation is a NO-OP. + * + * @param endpointConfig The end-point configuration + */ + default void init(EndpointConfig endpointConfig) { + } + + /** + * Destroy the decoder. The default implementation is a NO-OP. + */ + default void destroy() { + } + + interface Text extends Encoder { + + String encode(T object) throws EncodeException; + } + + interface TextStream extends Encoder { + + void encode(T object, Writer writer) throws EncodeException, IOException; + } + + interface Binary extends Encoder { + + ByteBuffer encode(T object) throws EncodeException; + } + + interface BinaryStream extends Encoder { + + void encode(T object, OutputStream os) throws EncodeException, IOException; + } +} diff --git a/java/jakarta/websocket/Endpoint.java b/java/jakarta/websocket/Endpoint.java new file mode 100644 index 0000000..059430d --- /dev/null +++ b/java/jakarta/websocket/Endpoint.java @@ -0,0 +1,48 @@ +/* + * 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 jakarta.websocket; + +public abstract class Endpoint { + + /** + * Event that is triggered when a new session starts. + * + * @param session The new session. + * @param config The configuration with which the Endpoint was configured. + */ + public abstract void onOpen(Session session, EndpointConfig config); + + /** + * Event that is triggered when a session has closed. + * + * @param session The session + * @param closeReason Why the session was closed + */ + public void onClose(Session session, CloseReason closeReason) { + // NO-OP by default + } + + /** + * Event that is triggered when a protocol error occurs. + * + * @param session The session. + * @param throwable The exception. + */ + public void onError(Session session, Throwable throwable) { + // NO-OP by default + } +} diff --git a/java/jakarta/websocket/EndpointConfig.java b/java/jakarta/websocket/EndpointConfig.java new file mode 100644 index 0000000..ed2d423 --- /dev/null +++ b/java/jakarta/websocket/EndpointConfig.java @@ -0,0 +1,29 @@ +/* + * 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 jakarta.websocket; + +import java.util.List; +import java.util.Map; + +public interface EndpointConfig { + + List> getEncoders(); + + List> getDecoders(); + + Map getUserProperties(); +} diff --git a/java/jakarta/websocket/Extension.java b/java/jakarta/websocket/Extension.java new file mode 100644 index 0000000..a4cbca8 --- /dev/null +++ b/java/jakarta/websocket/Extension.java @@ -0,0 +1,31 @@ +/* + * 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 jakarta.websocket; + +import java.util.List; + +public interface Extension { + String getName(); + + List getParameters(); + + interface Parameter { + String getName(); + + String getValue(); + } +} diff --git a/java/jakarta/websocket/HandshakeResponse.java b/java/jakarta/websocket/HandshakeResponse.java new file mode 100644 index 0000000..e5faf9d --- /dev/null +++ b/java/jakarta/websocket/HandshakeResponse.java @@ -0,0 +1,30 @@ +/* + * 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 jakarta.websocket; + +import java.util.List; +import java.util.Map; + +public interface HandshakeResponse { + + /** + * Name of the WebSocket accept HTTP header. + */ + String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; + + Map> getHeaders(); +} diff --git a/java/jakarta/websocket/MessageHandler.java b/java/jakarta/websocket/MessageHandler.java new file mode 100644 index 0000000..748dd76 --- /dev/null +++ b/java/jakarta/websocket/MessageHandler.java @@ -0,0 +1,41 @@ +/* + * 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 jakarta.websocket; + +public interface MessageHandler { + + interface Partial extends MessageHandler { + + /** + * Called when part of a message is available to be processed. + * + * @param messagePart The message part + * @param last true if this is the last part of this message, else false + */ + void onMessage(T messagePart, boolean last); + } + + interface Whole extends MessageHandler { + + /** + * Called when a whole message is available to be processed. + * + * @param message The message + */ + void onMessage(T message); + } +} diff --git a/java/jakarta/websocket/OnClose.java b/java/jakarta/websocket/OnClose.java new file mode 100644 index 0000000..3129116 --- /dev/null +++ b/java/jakarta/websocket/OnClose.java @@ -0,0 +1,27 @@ +/* + * 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 jakarta.websocket; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnClose { +} diff --git a/java/jakarta/websocket/OnError.java b/java/jakarta/websocket/OnError.java new file mode 100644 index 0000000..a09a5bd --- /dev/null +++ b/java/jakarta/websocket/OnError.java @@ -0,0 +1,27 @@ +/* + * 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 jakarta.websocket; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnError { +} diff --git a/java/jakarta/websocket/OnMessage.java b/java/jakarta/websocket/OnMessage.java new file mode 100644 index 0000000..34cfcfa --- /dev/null +++ b/java/jakarta/websocket/OnMessage.java @@ -0,0 +1,28 @@ +/* + * 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 jakarta.websocket; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnMessage { + long maxMessageSize() default -1; +} diff --git a/java/jakarta/websocket/OnOpen.java b/java/jakarta/websocket/OnOpen.java new file mode 100644 index 0000000..26b8e9d --- /dev/null +++ b/java/jakarta/websocket/OnOpen.java @@ -0,0 +1,27 @@ +/* + * 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 jakarta.websocket; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnOpen { +} diff --git a/java/jakarta/websocket/PongMessage.java b/java/jakarta/websocket/PongMessage.java new file mode 100644 index 0000000..ae77048 --- /dev/null +++ b/java/jakarta/websocket/PongMessage.java @@ -0,0 +1,32 @@ +/* + * 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 jakarta.websocket; + +import java.nio.ByteBuffer; + +/** + * Represents a WebSocket Pong message and used by message handlers to enable applications to process the response to + * any Pings they send. + */ +public interface PongMessage { + /** + * Get the payload of the Pong message. + * + * @return The payload of the Pong message. + */ + ByteBuffer getApplicationData(); +} diff --git a/java/jakarta/websocket/RemoteEndpoint.java b/java/jakarta/websocket/RemoteEndpoint.java new file mode 100644 index 0000000..9826d98 --- /dev/null +++ b/java/jakarta/websocket/RemoteEndpoint.java @@ -0,0 +1,223 @@ +/* + * 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 jakarta.websocket; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.util.concurrent.Future; + + +public interface RemoteEndpoint { + + interface Async extends RemoteEndpoint { + + /** + * Obtain the timeout (in milliseconds) for sending a message asynchronously. The default value is determined by + * {@link WebSocketContainer#getDefaultAsyncSendTimeout()}. + * + * @return The current send timeout in milliseconds. A non-positive value means an infinite timeout. + */ + long getSendTimeout(); + + /** + * Set the timeout (in milliseconds) for sending a message asynchronously. The default value is determined by + * {@link WebSocketContainer#getDefaultAsyncSendTimeout()}. + * + * @param timeout The new timeout for sending messages asynchronously in milliseconds. A non-positive value + * means an infinite timeout. + */ + void setSendTimeout(long timeout); + + /** + * Send the message asynchronously, using the SendHandler to signal to the client when the message has been + * sent. + * + * @param text The text message to send + * @param completion Used to signal to the client when the message has been sent + */ + void sendText(String text, SendHandler completion); + + /** + * Send the message asynchronously, using the Future to signal to the client when the message has been sent. + * + * @param text The text message to send + * + * @return A Future that signals when the message has been sent. + */ + Future sendText(String text); + + /** + * Send the message asynchronously, using the Future to signal to the client when the message has been sent. + * + * @param data The text message to send + * + * @return A Future that signals when the message has been sent. + * + * @throws IllegalArgumentException if {@code data} is {@code null}. + */ + Future sendBinary(ByteBuffer data); + + /** + * Send the message asynchronously, using the SendHandler to signal to the client when the message has been + * sent. + * + * @param data The text message to send + * @param completion Used to signal to the client when the message has been sent + * + * @throws IllegalArgumentException if {@code data} or {@code completion} is {@code null}. + */ + void sendBinary(ByteBuffer data, SendHandler completion); + + /** + * Encodes object as a message and sends it asynchronously, using the Future to signal to the client when the + * message has been sent. + * + * @param obj The object to be sent. + * + * @return A Future that signals when the message has been sent. + * + * @throws IllegalArgumentException if {@code obj} is {@code null}. + */ + Future sendObject(Object obj); + + /** + * Encodes object as a message and sends it asynchronously, using the SendHandler to signal to the client when + * the message has been sent. + * + * @param obj The object to be sent. + * @param completion Used to signal to the client when the message has been sent + * + * @throws IllegalArgumentException if {@code obj} or {@code completion} is {@code null}. + */ + void sendObject(Object obj, SendHandler completion); + + } + + interface Basic extends RemoteEndpoint { + + /** + * Send the message, blocking until the message is sent. + * + * @param text The text message to send. + * + * @throws IllegalArgumentException if {@code text} is {@code null}. + * @throws IOException if an I/O error occurs during the sending of the message. + */ + void sendText(String text) throws IOException; + + /** + * Send the message, blocking until the message is sent. + * + * @param data The binary message to send + * + * @throws IllegalArgumentException if {@code data} is {@code null}. + * @throws IOException if an I/O error occurs during the sending of the message. + */ + void sendBinary(ByteBuffer data) throws IOException; + + /** + * Sends part of a text message to the remote endpoint. Once the first part of a message has been sent, no other + * text or binary messages may be sent until all remaining parts of this message have been sent. + * + * @param fragment The partial message to send + * @param isLast true if this is the last part of the message, otherwise false + * + * @throws IllegalArgumentException if {@code fragment} is {@code null}. + * @throws IOException if an I/O error occurs during the sending of the message. + */ + void sendText(String fragment, boolean isLast) throws IOException; + + /** + * Sends part of a binary message to the remote endpoint. Once the first part of a message has been sent, no + * other text or binary messages may be sent until all remaining parts of this message have been sent. + * + * @param partialByte The partial message to send + * @param isLast true if this is the last part of the message, otherwise false + * + * @throws IllegalArgumentException if {@code partialByte} is {@code null}. + * @throws IOException if an I/O error occurs during the sending of the message. + */ + void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException; + + OutputStream getSendStream() throws IOException; + + Writer getSendWriter() throws IOException; + + /** + * Encodes object as a message and sends it to the remote endpoint. + * + * @param data The object to be sent. + * + * @throws EncodeException if there was a problem encoding the {@code data} object as a websocket + * message. + * @throws IllegalArgumentException if {@code data} is {@code null}. + * @throws IOException if an I/O error occurs during the sending of the message. + */ + void sendObject(Object data) throws IOException, EncodeException; + + } + + /** + * Enable or disable the batching of outgoing messages for this endpoint. If batching is disabled when it was + * previously enabled then this method will block until any currently batched messages have been written. + * + * @param batchingAllowed New setting + * + * @throws IOException If changing the value resulted in a call to {@link #flushBatch()} and that call threw an + * {@link IOException}. + */ + void setBatchingAllowed(boolean batchingAllowed) throws IOException; + + /** + * Obtains the current batching status of the endpoint. + * + * @return true if batching is enabled, otherwise false. + */ + boolean getBatchingAllowed(); + + /** + * Flush any currently batched messages to the remote endpoint. This method will block until the flush completes. + * + * @throws IOException If an I/O error occurs while flushing + */ + void flushBatch() throws IOException; + + /** + * Send a ping message blocking until the message has been sent. Note that if a message is in the process of being + * sent asynchronously, this method will block until that message and this ping has been sent. + * + * @param applicationData The payload for the ping message + * + * @throws IOException If an I/O error occurs while sending the ping + * @throws IllegalArgumentException if the applicationData is too large for a control message (max 125 bytes) + */ + void sendPing(ByteBuffer applicationData) throws IOException, IllegalArgumentException; + + /** + * Send a pong message blocking until the message has been sent. Note that if a message is in the process of being + * sent asynchronously, this method will block until that message and this pong has been sent. + * + * @param applicationData The payload for the pong message + * + * @throws IOException If an I/O error occurs while sending the pong + * @throws IllegalArgumentException if the applicationData is too large for a control message (max 125 bytes) + */ + void sendPong(ByteBuffer applicationData) throws IOException, IllegalArgumentException; +} + diff --git a/java/jakarta/websocket/SendHandler.java b/java/jakarta/websocket/SendHandler.java new file mode 100644 index 0000000..9e8317c --- /dev/null +++ b/java/jakarta/websocket/SendHandler.java @@ -0,0 +1,22 @@ +/* + * 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 jakarta.websocket; + +public interface SendHandler { + + void onResult(SendResult result); +} diff --git a/java/jakarta/websocket/SendResult.java b/java/jakarta/websocket/SendResult.java new file mode 100644 index 0000000..47f5021 --- /dev/null +++ b/java/jakarta/websocket/SendResult.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.websocket; + +public final class SendResult { + private final Throwable exception; + private final boolean ok; + + public SendResult(Throwable exception) { + this.exception = exception; + this.ok = (exception == null); + } + + public SendResult() { + this(null); + } + + public Throwable getException() { + return exception; + } + + public boolean isOK() { + return ok; + } +} diff --git a/java/jakarta/websocket/Session.java b/java/jakarta/websocket/Session.java new file mode 100644 index 0000000..06e25a5 --- /dev/null +++ b/java/jakarta/websocket/Session.java @@ -0,0 +1,184 @@ +/* + * 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 jakarta.websocket; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface Session extends Closeable { + + /** + * Get the container that created this session. + * + * @return the container that created this session. + */ + WebSocketContainer getContainer(); + + /** + * Registers a {@link MessageHandler} for incoming messages. Only one {@link MessageHandler} may be registered for + * each message type (text, binary, pong). The message type will be derived at runtime from the provided + * {@link MessageHandler} instance. It is not always possible to do this so it is better to use + * {@link #addMessageHandler(Class, jakarta.websocket.MessageHandler.Partial)} or + * {@link #addMessageHandler(Class, jakarta.websocket.MessageHandler.Whole)}. + * + * @param handler The message handler for a incoming message + * + * @throws IllegalStateException If a message handler has already been registered for the associated message type + */ + void addMessageHandler(MessageHandler handler) throws IllegalStateException; + + Set getMessageHandlers(); + + void removeMessageHandler(MessageHandler listener); + + String getProtocolVersion(); + + String getNegotiatedSubprotocol(); + + List getNegotiatedExtensions(); + + boolean isSecure(); + + boolean isOpen(); + + /** + * Get the idle timeout for this session. + * + * @return The current idle timeout for this session in milliseconds. Zero or negative values indicate an infinite + * timeout. + */ + long getMaxIdleTimeout(); + + /** + * Set the idle timeout for this session. + * + * @param timeout The new idle timeout for this session in milliseconds. Zero or negative values indicate an + * infinite timeout. + */ + void setMaxIdleTimeout(long timeout); + + /** + * Set the current maximum buffer size for binary messages. + * + * @param max The new maximum buffer size in bytes + */ + void setMaxBinaryMessageBufferSize(int max); + + /** + * Get the current maximum buffer size for binary messages. + * + * @return The current maximum buffer size in bytes + */ + int getMaxBinaryMessageBufferSize(); + + /** + * Set the maximum buffer size for text messages. + * + * @param max The new maximum buffer size in characters. + */ + void setMaxTextMessageBufferSize(int max); + + /** + * Get the maximum buffer size for text messages. + * + * @return The maximum buffer size in characters. + */ + int getMaxTextMessageBufferSize(); + + RemoteEndpoint.Async getAsyncRemote(); + + RemoteEndpoint.Basic getBasicRemote(); + + /** + * Provides a unique identifier for the session. This identifier should not be relied upon to be generated from a + * secure random source. + * + * @return A unique identifier for the session. + */ + String getId(); + + /** + * Close the connection to the remote end point using the code + * {@link jakarta.websocket.CloseReason.CloseCodes#NORMAL_CLOSURE} and an empty reason phrase. + * + * @throws IOException if an I/O error occurs while the WebSocket session is being closed. + */ + @Override + void close() throws IOException; + + + /** + * Close the connection to the remote end point using the specified code and reason phrase. + * + * @param closeReason The reason the WebSocket session is being closed. + * + * @throws IOException if an I/O error occurs while the WebSocket session is being closed. + */ + void close(CloseReason closeReason) throws IOException; + + URI getRequestURI(); + + Map> getRequestParameterMap(); + + String getQueryString(); + + Map getPathParameters(); + + Map getUserProperties(); + + Principal getUserPrincipal(); + + /** + * Obtain the set of open sessions associated with the same local endpoint as this session. + * + * @return The set of currently open sessions for the local endpoint that this session is associated with. + */ + Set getOpenSessions(); + + /** + * Registers a {@link MessageHandler} for partial incoming messages. Only one {@link MessageHandler} may be + * registered for each message type (text or binary, pong messages are never presented as partial messages). + * + * @param The type of message that the given handler is intended for + * @param clazz The Class that implements T + * @param handler The message handler for a incoming message + * + * @throws IllegalStateException If a message handler has already been registered for the associated message type + * + * @since WebSocket 1.1 + */ + void addMessageHandler(Class clazz, MessageHandler.Partial handler) throws IllegalStateException; + + /** + * Registers a {@link MessageHandler} for whole incoming messages. Only one {@link MessageHandler} may be registered + * for each message type (text, binary, pong). + * + * @param The type of message that the given handler is intended for + * @param clazz The Class that implements T + * @param handler The message handler for a incoming message + * + * @throws IllegalStateException If a message handler has already been registered for the associated message type + * + * @since WebSocket 1.1 + */ + void addMessageHandler(Class clazz, MessageHandler.Whole handler) throws IllegalStateException; +} diff --git a/java/jakarta/websocket/SessionException.java b/java/jakarta/websocket/SessionException.java new file mode 100644 index 0000000..b3eafed --- /dev/null +++ b/java/jakarta/websocket/SessionException.java @@ -0,0 +1,35 @@ +/* + * 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 jakarta.websocket; + +public class SessionException extends Exception { + + private static final long serialVersionUID = 1L; + + private final Session session; + + + public SessionException(String message, Throwable cause, Session session) { + super(message, cause); + this.session = session; + } + + + public Session getSession() { + return session; + } +} diff --git a/java/jakarta/websocket/WebSocketContainer.java b/java/jakarta/websocket/WebSocketContainer.java new file mode 100644 index 0000000..82ddb7f --- /dev/null +++ b/java/jakarta/websocket/WebSocketContainer.java @@ -0,0 +1,123 @@ +/* + * 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 jakarta.websocket; + +import java.io.IOException; +import java.net.URI; +import java.util.Set; + +public interface WebSocketContainer { + + /** + * Get the default timeout for sending a message asynchronously. + * + * @return The current default timeout in milliseconds. A non-positive value means an infinite timeout. + */ + long getDefaultAsyncSendTimeout(); + + /** + * Set the default timeout for sending a message asynchronously. + * + * @param timeout The new default timeout in milliseconds. A non-positive value means an infinite timeout. + */ + void setAsyncSendTimeout(long timeout); + + Session connectToServer(Object endpoint, URI path) throws DeploymentException, IOException; + + Session connectToServer(Class annotatedEndpointClass, URI path) throws DeploymentException, IOException; + + /** + * Creates a new connection to the WebSocket. + * + * @param endpoint The endpoint instance that will handle responses from the server + * @param clientEndpointConfiguration Used to configure the new connection + * @param path The full URL of the WebSocket endpoint to connect to + * + * @return The WebSocket session for the connection + * + * @throws DeploymentException If the connection cannot be established + * @throws IOException If an I/O occurred while trying to establish the connection + */ + Session connectToServer(Endpoint endpoint, ClientEndpointConfig clientEndpointConfiguration, URI path) + throws DeploymentException, IOException; + + /** + * Creates a new connection to the WebSocket. + * + * @param endpoint An instance of this class will be created to handle responses from the server + * @param clientEndpointConfiguration Used to configure the new connection + * @param path The full URL of the WebSocket endpoint to connect to + * + * @return The WebSocket session for the connection + * + * @throws DeploymentException If the connection cannot be established + * @throws IOException If an I/O occurred while trying to establish the connection + */ + Session connectToServer(Class endpoint, ClientEndpointConfig clientEndpointConfiguration, + URI path) throws DeploymentException, IOException; + + /** + * Get the current default session idle timeout. + * + * @return The current default session idle timeout in milliseconds. Zero or negative values indicate an infinite + * timeout. + */ + long getDefaultMaxSessionIdleTimeout(); + + /** + * Set the default session idle timeout. + * + * @param timeout The new default session idle timeout in milliseconds. Zero or negative values indicate an infinite + * timeout. + */ + void setDefaultMaxSessionIdleTimeout(long timeout); + + /** + * Get the default maximum buffer size for binary messages. + * + * @return The current default maximum buffer size in bytes + */ + int getDefaultMaxBinaryMessageBufferSize(); + + /** + * Set the default maximum buffer size for binary messages. + * + * @param max The new default maximum buffer size in bytes + */ + void setDefaultMaxBinaryMessageBufferSize(int max); + + /** + * Get the default maximum buffer size for text messages. + * + * @return The current default maximum buffer size in characters + */ + int getDefaultMaxTextMessageBufferSize(); + + /** + * Set the default maximum buffer size for text messages. + * + * @param max The new default maximum buffer size in characters + */ + void setDefaultMaxTextMessageBufferSize(int max); + + /** + * Get the installed extensions. + * + * @return The set of extensions that are supported by this WebSocket implementation. + */ + Set getInstalledExtensions(); +} diff --git a/java/jakarta/websocket/server/DefaultServerEndpointConfig.java b/java/jakarta/websocket/server/DefaultServerEndpointConfig.java new file mode 100644 index 0000000..63c7389 --- /dev/null +++ b/java/jakarta/websocket/server/DefaultServerEndpointConfig.java @@ -0,0 +1,92 @@ +/* + * 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 jakarta.websocket.server; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.websocket.Decoder; +import jakarta.websocket.Encoder; +import jakarta.websocket.Extension; + +/** + * Provides the default configuration for WebSocket server endpoints. + */ +final class DefaultServerEndpointConfig implements ServerEndpointConfig { + + private final Class endpointClass; + private final String path; + private final List subprotocols; + private final List extensions; + private final List> encoders; + private final List> decoders; + private final Configurator serverEndpointConfigurator; + private final Map userProperties = new ConcurrentHashMap<>(); + + DefaultServerEndpointConfig(Class endpointClass, String path, List subprotocols, + List extensions, List> encoders, + List> decoders, Configurator serverEndpointConfigurator) { + this.endpointClass = endpointClass; + this.path = path; + this.subprotocols = subprotocols; + this.extensions = extensions; + this.encoders = encoders; + this.decoders = decoders; + this.serverEndpointConfigurator = serverEndpointConfigurator; + } + + @Override + public Class getEndpointClass() { + return endpointClass; + } + + @Override + public List> getEncoders() { + return this.encoders; + } + + @Override + public List> getDecoders() { + return this.decoders; + } + + @Override + public String getPath() { + return path; + } + + @Override + public Configurator getConfigurator() { + return serverEndpointConfigurator; + } + + @Override + public Map getUserProperties() { + return userProperties; + } + + @Override + public List getSubprotocols() { + return subprotocols; + } + + @Override + public List getExtensions() { + return extensions; + } +} diff --git a/java/jakarta/websocket/server/HandshakeRequest.java b/java/jakarta/websocket/server/HandshakeRequest.java new file mode 100644 index 0000000..457942e --- /dev/null +++ b/java/jakarta/websocket/server/HandshakeRequest.java @@ -0,0 +1,53 @@ +/* + * 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 jakarta.websocket.server; + +import java.net.URI; +import java.security.Principal; +import java.util.List; +import java.util.Map; + +/** + * Represents the HTTP request that asked to be upgraded to WebSocket. + */ +public interface HandshakeRequest { + + String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; + String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; + String SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; + + Map> getHeaders(); + + Principal getUserPrincipal(); + + URI getRequestURI(); + + boolean isUserInRole(String role); + + /** + * Get the HTTP Session object associated with this request. Object is used to avoid a direct dependency on the + * Servlet API. + * + * @return The jakarta.servlet.http.HttpSession object associated with this request, if any. + */ + Object getHttpSession(); + + Map> getParameterMap(); + + String getQueryString(); +} diff --git a/java/jakarta/websocket/server/PathParam.java b/java/jakarta/websocket/server/PathParam.java new file mode 100644 index 0000000..e61294b --- /dev/null +++ b/java/jakarta/websocket/server/PathParam.java @@ -0,0 +1,32 @@ +/* + * 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 jakarta.websocket.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to annotate method parameters on POJO endpoints the the {@link ServerEndpoint} has been defined with a + * {@link ServerEndpoint#value()} that uses a URI template. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface PathParam { + String value(); +} diff --git a/java/jakarta/websocket/server/ServerApplicationConfig.java b/java/jakarta/websocket/server/ServerApplicationConfig.java new file mode 100644 index 0000000..ec23a48 --- /dev/null +++ b/java/jakarta/websocket/server/ServerApplicationConfig.java @@ -0,0 +1,46 @@ +/* + * 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 jakarta.websocket.server; + +import java.util.Set; + +import jakarta.websocket.Endpoint; + +/** + * Applications may provide an implementation of this interface to filter the discovered WebSocket endpoints that are + * deployed. Implementations of this class will be discovered via an ServletContainerInitializer scan. + */ +public interface ServerApplicationConfig { + + /** + * Enables applications to filter the discovered implementations of {@link ServerEndpointConfig}. + * + * @param scanned The {@link Endpoint} implementations found in the application + * + * @return The set of configurations for the endpoint the application wishes to deploy + */ + Set getEndpointConfigs(Set> scanned); + + /** + * Enables applications to filter the discovered classes annotated with {@link ServerEndpoint}. + * + * @param scanned The POJOs annotated with {@link ServerEndpoint} found in the application + * + * @return The set of POJOs the application wishes to deploy + */ + Set> getAnnotatedEndpointClasses(Set> scanned); +} diff --git a/java/jakarta/websocket/server/ServerContainer.java b/java/jakarta/websocket/server/ServerContainer.java new file mode 100644 index 0000000..82b92dc --- /dev/null +++ b/java/jakarta/websocket/server/ServerContainer.java @@ -0,0 +1,65 @@ +/* + * 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 jakarta.websocket.server; + +import java.io.IOException; +import java.util.Map; + +import jakarta.websocket.DeploymentException; +import jakarta.websocket.WebSocketContainer; + +/** + * Provides the ability to deploy endpoints programmatically. + */ +public interface ServerContainer extends WebSocketContainer { + void addEndpoint(Class clazz) throws DeploymentException; + + void addEndpoint(ServerEndpointConfig sec) throws DeploymentException; + + /** + * Upgrade the HTTP connection represented by the {@code HttpServletRequest} and {@code HttpServletResponse} to the + * WebSocket protocol and establish a WebSocket connection as per the provided {@link ServerEndpointConfig}. + *

+ * This method is primarily intended to be used by frameworks that implement the front-controller pattern. It does + * not deploy the provided endpoint. + *

+ * If the WebSocket implementation is not deployed as part of a Jakarta Servlet container, this method will throw an + * {@link UnsupportedOperationException}. + * + * @param httpServletRequest The {@code HttpServletRequest} to be processed as a WebSocket handshake as per section + * 4.0 of RFC 6455. + * @param httpServletResponse The {@code HttpServletResponse} to be used when processing the + * {@code httpServletRequest} as a WebSocket handshake as per section 4.0 of RFC + * 6455. + * @param sec The server endpoint configuration to use to configure the WebSocket endpoint + * @param pathParameters Provides a mapping of path parameter names and values, if any, to be used for the + * WebSocket connection established by the call to this method. If no such mapping is + * defined, an empty Map must be passed. + * + * @throws IllegalStateException if the provided request does not meet the requirements of the WebSocket + * handshake + * @throws UnsupportedOperationException if the WebSocket implementation is not deployed as part of a Jakarta + * Servlet container + * @throws IOException if an I/O error occurs during the establishment of a WebSocket connection + * @throws DeploymentException if a configuration error prevents the establishment of a WebSocket + * connection + * + * @since WebSocket 2.0 + */ + void upgradeHttpToWebSocket(Object httpServletRequest, Object httpServletResponse, ServerEndpointConfig sec, + Map pathParameters) throws IOException, DeploymentException; +} diff --git a/java/jakarta/websocket/server/ServerEndpoint.java b/java/jakarta/websocket/server/ServerEndpoint.java new file mode 100644 index 0000000..b29ebec --- /dev/null +++ b/java/jakarta/websocket/server/ServerEndpoint.java @@ -0,0 +1,45 @@ +/* + * 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 jakarta.websocket.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.websocket.Decoder; +import jakarta.websocket.Encoder; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ServerEndpoint { + + /** + * URI or URI-template that the annotated class should be mapped to. + * + * @return The URI or URI-template that the annotated class should be mapped to. + */ + String value(); + + String[] subprotocols() default {}; + + Class[] decoders() default {}; + + Class[] encoders() default {}; + + Class configurator() default ServerEndpointConfig.Configurator.class; +} diff --git a/java/jakarta/websocket/server/ServerEndpointConfig.java b/java/jakarta/websocket/server/ServerEndpointConfig.java new file mode 100644 index 0000000..c09fdca --- /dev/null +++ b/java/jakarta/websocket/server/ServerEndpointConfig.java @@ -0,0 +1,233 @@ +/* + * 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 jakarta.websocket.server; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; + +import jakarta.websocket.Decoder; +import jakarta.websocket.Encoder; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Extension; +import jakarta.websocket.HandshakeResponse; + +/** + * Provides configuration information for WebSocket endpoints published to a server. Applications may provide their own + * implementation or use {@link Builder}. + */ +public interface ServerEndpointConfig extends EndpointConfig { + + Class getEndpointClass(); + + /** + * Returns the path at which this WebSocket server endpoint has been registered. It may be a path or a level 0 URI + * template. + * + * @return The registered path + */ + String getPath(); + + List getSubprotocols(); + + List getExtensions(); + + Configurator getConfigurator(); + + + final class Builder { + + public static Builder create(Class endpointClass, String path) { + return new Builder(endpointClass, path); + } + + + private final Class endpointClass; + private final String path; + private List> encoders = Collections.emptyList(); + private List> decoders = Collections.emptyList(); + private List subprotocols = Collections.emptyList(); + private List extensions = Collections.emptyList(); + private Configurator configurator = Configurator.fetchContainerDefaultConfigurator(); + + + private Builder(Class endpointClass, String path) { + if (endpointClass == null) { + throw new IllegalArgumentException("Endpoint class may not be null"); + } + if (path == null) { + throw new IllegalArgumentException("Path may not be null"); + } + if (path.isEmpty()) { + throw new IllegalArgumentException("Path may not be empty"); + } + if (path.charAt(0) != '/') { + throw new IllegalArgumentException("Path must start with '/'"); + } + this.endpointClass = endpointClass; + this.path = path; + } + + public ServerEndpointConfig build() { + return new DefaultServerEndpointConfig(endpointClass, path, subprotocols, extensions, encoders, decoders, + configurator); + } + + + public Builder encoders(List> encoders) { + if (encoders == null || encoders.size() == 0) { + this.encoders = Collections.emptyList(); + } else { + this.encoders = Collections.unmodifiableList(encoders); + } + return this; + } + + + public Builder decoders(List> decoders) { + if (decoders == null || decoders.size() == 0) { + this.decoders = Collections.emptyList(); + } else { + this.decoders = Collections.unmodifiableList(decoders); + } + return this; + } + + + public Builder subprotocols(List subprotocols) { + if (subprotocols == null || subprotocols.size() == 0) { + this.subprotocols = Collections.emptyList(); + } else { + this.subprotocols = Collections.unmodifiableList(subprotocols); + } + return this; + } + + + public Builder extensions(List extensions) { + if (extensions == null || extensions.size() == 0) { + this.extensions = Collections.emptyList(); + } else { + this.extensions = Collections.unmodifiableList(extensions); + } + return this; + } + + + public Builder configurator(Configurator serverEndpointConfigurator) { + if (serverEndpointConfigurator == null) { + this.configurator = Configurator.fetchContainerDefaultConfigurator(); + } else { + this.configurator = serverEndpointConfigurator; + } + return this; + } + } + + + class Configurator { + + private static volatile Configurator defaultImpl = null; + private static final Object defaultImplLock = new Object(); + + private static final String DEFAULT_IMPL_CLASSNAME = + "org.apache.tomcat.websocket.server.DefaultServerEndpointConfigurator"; + + static Configurator fetchContainerDefaultConfigurator() { + if (defaultImpl == null) { + synchronized (defaultImplLock) { + if (defaultImpl == null) { + if (System.getSecurityManager() == null) { + defaultImpl = loadDefault(); + } else { + defaultImpl = AccessController.doPrivileged(new PrivilegedLoadDefault()); + } + } + } + } + return defaultImpl; + } + + + private static Configurator loadDefault() { + Configurator result = null; + + ServiceLoader serviceLoader = ServiceLoader.load(Configurator.class); + + Iterator iter = serviceLoader.iterator(); + while (result == null && iter.hasNext()) { + result = iter.next(); + } + + // Fall-back. Also used by unit tests + if (result == null) { + try { + @SuppressWarnings("unchecked") + Class clazz = (Class) Class.forName(DEFAULT_IMPL_CLASSNAME); + result = clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) { + // No options left. Just return null. + } + } + return result; + } + + + private static class PrivilegedLoadDefault implements PrivilegedAction { + + @Override + public Configurator run() { + return Configurator.loadDefault(); + } + } + + + /** + * Return the platform default configurator. + * + * @return the platform default configurator + * + * @since WebSocket 2.1 + */ + public ServerEndpointConfig.Configurator getContainerDefaultConfigurator() { + return fetchContainerDefaultConfigurator(); + } + + public String getNegotiatedSubprotocol(List supported, List requested) { + return fetchContainerDefaultConfigurator().getNegotiatedSubprotocol(supported, requested); + } + + public List getNegotiatedExtensions(List installed, List requested) { + return fetchContainerDefaultConfigurator().getNegotiatedExtensions(installed, requested); + } + + public boolean checkOrigin(String originHeaderValue) { + return fetchContainerDefaultConfigurator().checkOrigin(originHeaderValue); + } + + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + fetchContainerDefaultConfigurator().modifyHandshake(sec, request, response); + } + + public T getEndpointInstance(Class clazz) throws InstantiationException { + return fetchContainerDefaultConfigurator().getEndpointInstance(clazz); + } + } +} diff --git a/java/jakarta/xml/ws/WebServiceRef.java b/java/jakarta/xml/ws/WebServiceRef.java new file mode 100644 index 0000000..3c7e7a4 --- /dev/null +++ b/java/jakarta/xml/ws/WebServiceRef.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.xml.ws; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) + +public @interface WebServiceRef { + String name() default ""; + + @SuppressWarnings("rawtypes") // Can't use Class because API needs to match specification + Class type() default Object.class; + + @SuppressWarnings("rawtypes") // Can't use Class because API needs to match specification + Class value() default Object.class; + + String wsdlLocation() default ""; + + String mappedName() default ""; +} diff --git a/java/jakarta/xml/ws/WebServiceRefs.java b/java/jakarta/xml/ws/WebServiceRefs.java new file mode 100644 index 0000000..0b69d9e --- /dev/null +++ b/java/jakarta/xml/ws/WebServiceRefs.java @@ -0,0 +1,29 @@ +/* + * 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 jakarta.xml.ws; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) + +public @interface WebServiceRefs { + WebServiceRef[] value(); +} diff --git a/java/org/apache/catalina/AccessLog.java b/java/org/apache/catalina/AccessLog.java new file mode 100644 index 0000000..046facc --- /dev/null +++ b/java/org/apache/catalina/AccessLog.java @@ -0,0 +1,109 @@ +/* + * 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.catalina; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; + + +/** + * Intended for use by a {@link Valve} to indicate that the {@link Valve} + * provides access logging. It is used by the Tomcat internals to identify a + * Valve that logs access requests so requests that are rejected + * earlier in the processing chain can still be added to the access log. + * Implementations of this interface should be robust against the provided + * {@link Request} and {@link Response} objects being null, having null + * attributes or any other 'oddness' that may result from attempting to log + * a request that was almost certainly rejected because it was mal-formed. + */ +public interface AccessLog { + + /** + * Name of request attribute used to override the remote address recorded by + * the AccessLog. + */ + String REMOTE_ADDR_ATTRIBUTE = + "org.apache.catalina.AccessLog.RemoteAddr"; + + /** + * Name of request attribute used to override remote host name recorded by + * the AccessLog. + */ + String REMOTE_HOST_ATTRIBUTE = + "org.apache.catalina.AccessLog.RemoteHost"; + + /** + * Name of request attribute used to override the protocol recorded by the + * AccessLog. + */ + String PROTOCOL_ATTRIBUTE = + "org.apache.catalina.AccessLog.Protocol"; + + /** + * Name of request attribute used to override the server name recorded by + * the AccessLog. + */ + String SERVER_NAME_ATTRIBUTE = + "org.apache.catalina.AccessLog.ServerName"; + + /** + * Name of request attribute used to override the server port recorded by + * the AccessLog. + */ + String SERVER_PORT_ATTRIBUTE = + "org.apache.catalina.AccessLog.ServerPort"; + + + /** + * Add the request/response to the access log using the specified processing + * time. + * + * @param request Request (associated with the response) to log + * @param response Response (associated with the request) to log + * @param time Time taken to process the request/response in + * nanoseconds (use 0 if not known); in Tomcat + * versions prior to 10, the time unit was + * milliseconds + */ + void log(Request request, Response response, long time); + + /** + * Should this valve use request attributes for IP address, hostname, + * protocol and port used for the request? + * + * The attributes used are: + *

    + *
  • org.apache.catalina.RemoteAddr
  • + *
  • org.apache.catalina.RemoteHost
  • + *
  • org.apache.catalina.Protocol
  • + *
  • org.apache.catalina.ServerName
  • + *
  • org.apache.catalina.ServerPost
  • + *
+ * + * @param requestAttributesEnabled true causes the attributes + * to be used, false causes + * the original values to be used. + */ + void setRequestAttributesEnabled(boolean requestAttributesEnabled); + + /** + * @see #setRequestAttributesEnabled(boolean) + * @return true if the attributes will be logged, otherwise + * false + */ + boolean getRequestAttributesEnabled(); +} diff --git a/java/org/apache/catalina/AsyncDispatcher.java b/java/org/apache/catalina/AsyncDispatcher.java new file mode 100644 index 0000000..84271d8 --- /dev/null +++ b/java/org/apache/catalina/AsyncDispatcher.java @@ -0,0 +1,39 @@ +/* + * 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.catalina; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +public interface AsyncDispatcher { + + /** + * Perform an asynchronous dispatch. The method does not check if the + * request is in an appropriate state for this; it is the caller's + * responsibility to check this. + * @param request The request object to pass to the dispatch target + * @param response The response object to pass to the dispatch target + * @throws ServletException if thrown by the dispatch target + * @throws IOException if an I/O error occurs while processing the + * dispatch + */ + void dispatch(ServletRequest request, ServletResponse response) + throws ServletException, IOException; +} diff --git a/java/org/apache/catalina/Authenticator.java b/java/org/apache/catalina/Authenticator.java new file mode 100644 index 0000000..1a050d4 --- /dev/null +++ b/java/org/apache/catalina/Authenticator.java @@ -0,0 +1,57 @@ +/* + * 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.catalina; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.connector.Request; + + +/** + * An Authenticator is a component (usually a Valve or Container) that + * provides some sort of authentication service. + * + * @author Craig R. McClanahan + */ +public interface Authenticator { + + /** + * Authenticate the user making this request, based on the login + * configuration of the {@link Context} with which this Authenticator is + * associated. + * + * @param request Request we are processing + * @param response Response we are populating + * + * @return true if any specified constraints have been + * satisfied, or false if one more constraints were not + * satisfied (in which case an authentication challenge will have + * been written to the response). + * + * @exception IOException if an input/output error occurs + */ + boolean authenticate(Request request, HttpServletResponse response) + throws IOException; + + void login(String userName, String password, Request request) + throws ServletException; + + void logout(Request request); +} diff --git a/java/org/apache/catalina/Cluster.java b/java/org/apache/catalina/Cluster.java new file mode 100644 index 0000000..6e21680 --- /dev/null +++ b/java/org/apache/catalina/Cluster.java @@ -0,0 +1,85 @@ +/* + * 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.catalina; + +/** + * A Cluster works as a Cluster client/server for the local host + * Different Cluster implementations can be used to support different + * ways to communicate within the Cluster. A Cluster implementation is + * responsible for setting up a way to communicate within the Cluster + * and also supply "ClientApplications" with ClusterSender + * used when sending information in the Cluster and + * ClusterInfo used for receiving information in the Cluster. + * + * @author Bip Thelin + * @author Remy Maucherat + */ +public interface Cluster extends Contained { + + /** + * Return the name of the cluster that this Server is currently + * configured to operate within. + * + * @return The name of the cluster associated with this server + */ + String getClusterName(); + + + /** + * Set the name of the cluster to join, if no cluster with + * this name is present create one. + * + * @param clusterName The clustername to join + */ + void setClusterName(String clusterName); + + + /** + * Create a new manager which will use this cluster to replicate its + * sessions. + * + * @param name Name (key) of the application with which the manager is + * associated + * + * @return The newly created Manager instance + */ + Manager createManager(String name); + + + /** + * Register a manager with the cluster. If the cluster is not responsible + * for creating a manager, then the container will at least notify the + * cluster that this manager is participating in the cluster. + * @param manager Manager + */ + void registerManager(Manager manager); + + + /** + * Removes a manager from the cluster + * @param manager Manager + */ + void removeManager(Manager manager); + + + /** + * Execute a periodic task, such as reloading, etc. This method will be + * invoked inside the classloading context of this container. Unexpected + * throwables will be caught and logged. + */ + void backgroundProcess(); +} diff --git a/java/org/apache/catalina/Contained.java b/java/org/apache/catalina/Contained.java new file mode 100644 index 0000000..7288893 --- /dev/null +++ b/java/org/apache/catalina/Contained.java @@ -0,0 +1,45 @@ +/* + * 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.catalina; + +/** + *

Decoupling interface which specifies that an implementing class is + * associated with at most one Container instance.

+ * + * @author Craig R. McClanahan + * @author Peter Donald + */ +public interface Contained { + + /** + * Get the {@link Container} with which this instance is associated. + * + * @return The Container with which this instance is associated or + * null if not associated with a Container + */ + Container getContainer(); + + + /** + * Set the Container with which this instance is associated. + * + * @param container The Container instance with which this instance is to + * be associated, or null to disassociate this instance + * from any Container + */ + void setContainer(Container container); +} diff --git a/java/org/apache/catalina/Container.java b/java/org/apache/catalina/Container.java new file mode 100644 index 0000000..fd897dc --- /dev/null +++ b/java/org/apache/catalina/Container.java @@ -0,0 +1,523 @@ +/* + * 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.catalina; + +import java.beans.PropertyChangeListener; +import java.io.File; + +import javax.management.ObjectName; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; + + +/** + * A Container is an object that can execute requests received from + * a client, and return responses based on those requests. A Container may + * optionally support a pipeline of Valves that process the request in an + * order configured at runtime, by implementing the Pipeline interface + * as well. + *

+ * Containers will exist at several conceptual levels within Catalina. The + * following examples represent common cases: + *

    + *
  • Engine - Representation of the entire Catalina servlet engine, + * most likely containing one or more subcontainers that are either Host + * or Context implementations, or other custom groups. + *
  • Host - Representation of a virtual host containing a number + * of Contexts. + *
  • Context - Representation of a single ServletContext, which will + * typically contain one or more Wrappers for the supported servlets. + *
  • Wrapper - Representation of an individual servlet definition. + *
+ * A given deployment of Catalina need not include Containers at all of the + * levels described above. For example, an administration application + * embedded within a network device (such as a router) might only contain + * a single Context and a few Wrappers, or even a single Wrapper if the + * application is relatively small. Therefore, Container implementations + * need to be designed so that they will operate correctly in the absence + * of parent Containers in a given deployment. + *

+ * A Container may also be associated with a number of support components + * that provide functionality which might be shared (by attaching it to a + * parent Container) or individually customized. The following support + * components are currently recognized: + *

    + *
  • Loader - Class loader to use for integrating new Java classes + * for this Container into the JVM in which Catalina is running. + *
  • Logger - Implementation of the log() method + * signatures of the ServletContext interface. + *
  • Manager - Manager for the pool of Sessions associated with + * this Container. + *
  • Realm - Read-only interface to a security domain, for + * authenticating user identities and their corresponding roles. + *
  • Resources - JNDI directory context enabling access to static + * resources, enabling custom linkages to existing server components when + * Catalina is embedded in a larger server. + *
+ * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public interface Container extends Lifecycle { + + + // ----------------------------------------------------- Manifest Constants + + + /** + * The ContainerEvent event type sent when a child container is added + * by addChild(). + */ + String ADD_CHILD_EVENT = "addChild"; + + + /** + * The ContainerEvent event type sent when a valve is added + * by addValve(), if this Container supports pipelines. + */ + String ADD_VALVE_EVENT = "addValve"; + + + /** + * The ContainerEvent event type sent when a child container is removed + * by removeChild(). + */ + String REMOVE_CHILD_EVENT = "removeChild"; + + + /** + * The ContainerEvent event type sent when a valve is removed + * by removeValve(), if this Container supports pipelines. + */ + String REMOVE_VALVE_EVENT = "removeValve"; + + + // ------------------------------------------------------------- Properties + + /** + * Obtain the log to which events for this container should be logged. + * + * @return The Logger with which this Container is associated. If there is + * no associated Logger, return the Logger associated with the + * parent Container (if any); otherwise return null. + */ + Log getLogger(); + + + /** + * Return the logger name that the container will use. + * @return the abbreviated name of this container for logging messages + */ + String getLogName(); + + + /** + * Obtain the JMX name for this container. + * + * @return the JMX name associated with this container. + */ + ObjectName getObjectName(); + + + /** + * Obtain the JMX domain under which this container will be / has been + * registered. + * + * @return The JMX domain name + */ + String getDomain(); + + + /** + * Calculate the key properties string to be added to an object's + * {@link ObjectName} to indicate that it is associated with this container. + * + * @return A string suitable for appending to the ObjectName + * + */ + String getMBeanKeyProperties(); + + + /** + * Return the Pipeline object that manages the Valves associated with + * this Container. + * + * @return The Pipeline + */ + Pipeline getPipeline(); + + + /** + * Get the Cluster for this container. + * + * @return The Cluster with which this Container is associated. If there is + * no associated Cluster, return the Cluster associated with our + * parent Container (if any); otherwise return null. + */ + Cluster getCluster(); + + + /** + * Set the Cluster with which this Container is associated. + * + * @param cluster the Cluster with which this Container is associated. + */ + void setCluster(Cluster cluster); + + + /** + * Get the delay between the invocation of the backgroundProcess method on + * this container and its children. Child containers will not be invoked if + * their delay value is positive (which would mean they are using their own + * thread). Setting this to a positive value will cause a thread to be + * spawned. After waiting the specified amount of time, the thread will + * invoke the {@link #backgroundProcess()} method on this container and all + * children with non-positive delay values. + * + * @return The delay between the invocation of the backgroundProcess method + * on this container and its children. A non-positive value + * indicates that background processing will be managed by the + * parent. + */ + int getBackgroundProcessorDelay(); + + + /** + * Set the delay between the invocation of the execute method on this + * container and its children. + * + * @param delay The delay in seconds between the invocation of + * backgroundProcess methods + */ + void setBackgroundProcessorDelay(int delay); + + + /** + * Return a name string (suitable for use by humans) that describes this + * Container. Within the set of child containers belonging to a particular + * parent, Container names must be unique. + * + * @return The human readable name of this container. + */ + String getName(); + + + /** + * Set a name string (suitable for use by humans) that describes this + * Container. Within the set of child containers belonging to a particular + * parent, Container names must be unique. + * + * @param name New name of this container + * + * @exception IllegalStateException if this Container has already been + * added to the children of a parent Container (after which the name + * may not be changed) + */ + void setName(String name); + + + /** + * Get the parent container. + * + * @return Return the Container for which this Container is a child, if + * there is one. If there is no defined parent, return + * null. + */ + Container getParent(); + + + /** + * Set the parent Container to which this Container is being added as a + * child. This Container may refuse to become attached to the specified + * Container by throwing an exception. + * + * @param container Container to which this Container is being added + * as a child + * + * @exception IllegalArgumentException if this Container refuses to become + * attached to the specified Container + */ + void setParent(Container container); + + + /** + * Get the parent class loader. + * + * @return the parent class loader for this component. If not set, return + * {@link #getParent()}.{@link #getParentClassLoader()}. If no + * parent has been set, return the system class loader. + */ + ClassLoader getParentClassLoader(); + + + /** + * Set the parent class loader for this component. For {@link Context}s + * this call is meaningful only before a Loader has + * been configured, and the specified value (if non-null) should be + * passed as an argument to the class loader constructor. + * + * @param parent The new parent class loader + */ + void setParentClassLoader(ClassLoader parent); + + + /** + * Obtain the Realm with which this Container is associated. + * + * @return The associated Realm; if there is no associated Realm, the + * Realm associated with the parent Container (if any); otherwise + * return null. + */ + Realm getRealm(); + + + /** + * Set the Realm with which this Container is associated. + * + * @param realm The newly associated Realm + */ + void setRealm(Realm realm); + + + /** + * Find the configuration path where a configuration resource + * is located. + * @param container The container + * @param resourceName The resource file name + * @return the configuration path + */ + static String getConfigPath(Container container, String resourceName) { + StringBuilder result = new StringBuilder(); + Container host = null; + Container engine = null; + while (container != null) { + if (container instanceof Host) { + host = container; + } else if (container instanceof Engine) { + engine = container; + } + container = container.getParent(); + } + if (host != null && ((Host) host).getXmlBase() != null) { + result.append(((Host) host).getXmlBase()).append('/'); + } else { + result.append("conf/"); + if (engine != null) { + result.append(engine.getName()).append('/'); + } + if (host != null) { + result.append(host.getName()).append('/'); + } + } + result.append(resourceName); + return result.toString(); + } + + + /** + * Return the Service to which this container belongs. + * @param container The container to start from + * @return the Service, or null if not found + */ + static Service getService(Container container) { + while (container != null && !(container instanceof Engine)) { + container = container.getParent(); + } + if (container == null) { + return null; + } + return ((Engine) container).getService(); + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Execute a periodic task, such as reloading, etc. This method will be + * invoked inside the classloading context of this container. Unexpected + * throwables will be caught and logged. + */ + void backgroundProcess(); + + + /** + * Add a new child Container to those associated with this Container, + * if supported. Prior to adding this Container to the set of children, + * the child's setParent() method must be called, with this + * Container as an argument. This method may thrown an + * IllegalArgumentException if this Container chooses not + * to be attached to the specified Container, in which case it is not added + * + * @param child New child Container to be added + * + * @exception IllegalArgumentException if this exception is thrown by + * the setParent() method of the child Container + * @exception IllegalArgumentException if the new child does not have + * a name unique from that of existing children of this Container + * @exception IllegalStateException if this Container does not support + * child Containers + */ + void addChild(Container child); + + + /** + * Add a container event listener to this component. + * + * @param listener The listener to add + */ + void addContainerListener(ContainerListener listener); + + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + void addPropertyChangeListener(PropertyChangeListener listener); + + + /** + * Obtain a child Container by name. + * + * @param name Name of the child Container to be retrieved + * + * @return The child Container with the given name or null if + * no such child exists. + */ + Container findChild(String name); + + + /** + * Obtain the child Containers associated with this Container. + * + * @return An array containing all children of this container. If this + * Container has no children, a zero-length array is returned. + */ + Container[] findChildren(); + + + /** + * Obtain the container listeners associated with this Container. + * + * @return An array containing the container listeners associated with this + * Container. If this Container has no registered container + * listeners, a zero-length array is returned. + */ + ContainerListener[] findContainerListeners(); + + + /** + * Remove an existing child Container from association with this parent + * Container. + * + * @param child Existing child Container to be removed + */ + void removeChild(Container child); + + + /** + * Remove a container event listener from this component. + * + * @param listener The listener to remove + */ + void removeContainerListener(ContainerListener listener); + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + void removePropertyChangeListener(PropertyChangeListener listener); + + + /** + * Notify all container event listeners that a particular event has + * occurred for this Container. The default implementation performs + * this notification synchronously using the calling thread. + * + * @param type Event type + * @param data Event data + */ + void fireContainerEvent(String type, Object data); + + + /** + * Log a request/response that was destined for this container but has been + * handled earlier in the processing chain so that the request/response + * still appears in the correct access logs. + * @param request Request (associated with the response) to log + * @param response Response (associated with the request) to log + * @param time Time taken to process the request/response in + * milliseconds (use 0 if not known) + * @param useDefault Flag that indicates that the request/response should + * be logged in the engine's default access log + */ + void logAccess(Request request, Response response, long time, + boolean useDefault); + + + /** + * Obtain the AccessLog to use to log a request/response that is destined + * for this container. This is typically used when the request/response was + * handled (and rejected) earlier in the processing chain so that the + * request/response still appears in the correct access logs. + * + * @return The AccessLog to use for a request/response destined for this + * container + */ + AccessLog getAccessLog(); + + + /** + * Obtain the number of threads available for starting and stopping any + * children associated with this container. This allows start/stop calls to + * children to be processed in parallel. + * + * @return The currently configured number of threads used to start/stop + * children associated with this container + */ + int getStartStopThreads(); + + + /** + * Sets the number of threads available for starting and stopping any + * children associated with this container. This allows start/stop calls to + * children to be processed in parallel. + * @param startStopThreads The new number of threads to be used + */ + void setStartStopThreads(int startStopThreads); + + + /** + * Obtain the location of CATALINA_BASE. + * + * @return The location of CATALINA_BASE. + */ + File getCatalinaBase(); + + + /** + * Obtain the location of CATALINA_HOME. + * + * @return The location of CATALINA_HOME. + */ + File getCatalinaHome(); +} diff --git a/java/org/apache/catalina/ContainerEvent.java b/java/org/apache/catalina/ContainerEvent.java new file mode 100644 index 0000000..b651356 --- /dev/null +++ b/java/org/apache/catalina/ContainerEvent.java @@ -0,0 +1,96 @@ +/* + * 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.catalina; + +import java.util.EventObject; + +/** + * General event for notifying listeners of significant changes on a Container. + * + * @author Craig R. McClanahan + */ +public final class ContainerEvent extends EventObject { + + private static final long serialVersionUID = 1L; + + /** + * The event data associated with this event. + */ + private final Object data; + + + /** + * The event type this instance represents. + */ + private final String type; + + + /** + * Construct a new ContainerEvent with the specified parameters. + * + * @param container Container on which this event occurred + * @param type Event type + * @param data Event data + */ + public ContainerEvent(Container container, String type, Object data) { + super(container); + this.type = type; + this.data = data; + } + + + /** + * Return the event data of this event. + * + * @return The data, if any, associated with this event. + */ + public Object getData() { + return this.data; + } + + + /** + * Return the Container on which this event occurred. + * + * @return The Container on which this event occurred. + */ + public Container getContainer() { + return (Container) getSource(); + } + + + /** + * Return the event type of this event. + * + * @return The event type of this event. Although this is a String, it is + * safe to rely on the value returned by this method remaining + * consistent between point releases. + */ + public String getType() { + return this.type; + } + + + /** + * Return a string representation of this event. + */ + @Override + public String toString() { + return "ContainerEvent['" + getContainer() + "','" + + getType() + "','" + getData() + "']"; + } +} diff --git a/java/org/apache/catalina/ContainerListener.java b/java/org/apache/catalina/ContainerListener.java new file mode 100644 index 0000000..ae72758 --- /dev/null +++ b/java/org/apache/catalina/ContainerListener.java @@ -0,0 +1,38 @@ +/* + * 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.catalina; + + +/** + * Interface defining a listener for significant Container generated events. + * Note that "container start" and "container stop" events are normally + * LifecycleEvents, not ContainerEvents. + * + * @author Craig R. McClanahan + */ +public interface ContainerListener { + + + /** + * Acknowledge the occurrence of the specified event. + * + * @param event ContainerEvent that has occurred + */ + void containerEvent(ContainerEvent event); + + +} diff --git a/java/org/apache/catalina/ContainerServlet.java b/java/org/apache/catalina/ContainerServlet.java new file mode 100644 index 0000000..3db927d --- /dev/null +++ b/java/org/apache/catalina/ContainerServlet.java @@ -0,0 +1,44 @@ +/* + * 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.catalina; + +/** + * A ContainerServlet is a servlet that has access to Catalina + * internal functionality, and is loaded from the Catalina class loader + * instead of the web application class loader. The property setter + * methods must be called by the container whenever a new instance of + * this servlet is put into service. + * + * @author Craig R. McClanahan + */ +public interface ContainerServlet { + + /** + * Obtain the Wrapper with which this Servlet is associated. + * + * @return The Wrapper with which this Servlet is associated. + */ + Wrapper getWrapper(); + + + /** + * Set the Wrapper with which this Servlet is associated. + * + * @param wrapper The new associated Wrapper + */ + void setWrapper(Wrapper wrapper); +} diff --git a/java/org/apache/catalina/Context.java b/java/org/apache/catalina/Context.java new file mode 100644 index 0000000..d91556c --- /dev/null +++ b/java/org/apache/catalina/Context.java @@ -0,0 +1,2060 @@ +/* + * 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.catalina; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.descriptor.JspConfigDescriptor; + +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.tomcat.ContextBind; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.util.descriptor.web.ApplicationParameter; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource.Resource; +import org.apache.tomcat.util.http.CookieProcessor; + +/** + * A Context is a Container that represents a servlet context, and + * therefore an individual web application, in the Catalina servlet engine. + * It is therefore useful in almost every deployment of Catalina (even if a + * Connector attached to a web server (such as Apache) uses the web server's + * facilities to identify the appropriate Wrapper to handle this request. + * It also provides a convenient mechanism to use Interceptors that see + * every request processed by this particular web application. + *

+ * The parent Container attached to a Context is generally a Host, but may + * be some other implementation, or may be omitted if it is not necessary. + *

+ * The child containers attached to a Context are generally implementations + * of Wrapper (representing individual servlet definitions). + *

+ * + * @author Craig R. McClanahan + */ +public interface Context extends Container, ContextBind { + + + // ----------------------------------------------------- Manifest Constants + + /** + * Container event for adding a welcome file. + */ + String ADD_WELCOME_FILE_EVENT = "addWelcomeFile"; + + /** + * Container event for removing a wrapper. + */ + String REMOVE_WELCOME_FILE_EVENT = "removeWelcomeFile"; + + /** + * Container event for clearing welcome files. + */ + String CLEAR_WELCOME_FILES_EVENT = "clearWelcomeFiles"; + + /** + * Container event for changing the ID of a session. + */ + String CHANGE_SESSION_ID_EVENT = "changeSessionId"; + + + /** + * Prefix for resource lookup. + */ + String WEBAPP_PROTOCOL = "webapp:"; + + + // ------------------------------------------------------------- Properties + + /** + * Returns true if requests mapped to servlets without + * "multipart config" to parse multipart/form-data requests anyway. + * + * @return true if requests mapped to servlets without + * "multipart config" to parse multipart/form-data requests, + * false otherwise. + */ + boolean getAllowCasualMultipartParsing(); + + + /** + * Set to true to allow requests mapped to servlets that + * do not explicitly declare @MultipartConfig or have + * <multipart-config> specified in web.xml to parse + * multipart/form-data requests. + * + * @param allowCasualMultipartParsing true to allow such + * casual parsing, false otherwise. + */ + void setAllowCasualMultipartParsing(boolean allowCasualMultipartParsing); + + + /** + * Obtain the registered application event listeners. + * + * @return An array containing the application event listener instances for + * this web application in the order they were specified in the web + * application deployment descriptor + */ + Object[] getApplicationEventListeners(); + + + /** + * Store the set of initialized application event listener objects, + * in the order they were specified in the web application deployment + * descriptor, for this application. + * + * @param listeners The set of instantiated listener objects. + */ + void setApplicationEventListeners(Object listeners[]); + + + /** + * Obtain the registered application lifecycle listeners. + * + * @return An array containing the application lifecycle listener instances + * for this web application in the order they were specified in the + * web application deployment descriptor + */ + Object[] getApplicationLifecycleListeners(); + + + /** + * Store the set of initialized application lifecycle listener objects, + * in the order they were specified in the web application deployment + * descriptor, for this application. + * + * @param listeners The set of instantiated listener objects. + */ + void setApplicationLifecycleListeners(Object listeners[]); + + + /** + * Obtain the character set name to use with the given Locale. Note that + * different Contexts may have different mappings of Locale to character + * set. + * + * @param locale The locale for which the mapped character set should be + * returned + * + * @return The name of the character set to use with the given Locale + */ + String getCharset(Locale locale); + + + /** + * Return the URL of the XML descriptor for this context. + * + * @return The URL of the XML descriptor for this context + */ + URL getConfigFile(); + + + /** + * Set the URL of the XML descriptor for this context. + * + * @param configFile The URL of the XML descriptor for this context. + */ + void setConfigFile(URL configFile); + + + /** + * Return the "correctly configured" flag for this Context. + * + * @return true if the Context has been correctly configured, + * otherwise false + */ + boolean getConfigured(); + + + /** + * Set the "correctly configured" flag for this Context. This can be + * set to false by startup listeners that detect a fatal configuration + * error to avoid the application from being made available. + * + * @param configured The new correctly configured flag + */ + void setConfigured(boolean configured); + + + /** + * Return the "use cookies for session ids" flag. + * + * @return true if it is permitted to use cookies to track + * session IDs for this web application, otherwise + * false + */ + boolean getCookies(); + + + /** + * Set the "use cookies for session ids" flag. + * + * @param cookies The new flag + */ + void setCookies(boolean cookies); + + + /** + * Gets the name to use for session cookies. Overrides any setting that + * may be specified by the application. + * + * @return The value of the default session cookie name or null if not + * specified + */ + String getSessionCookieName(); + + + /** + * Sets the name to use for session cookies. Overrides any setting that + * may be specified by the application. + * + * @param sessionCookieName The name to use + */ + void setSessionCookieName(String sessionCookieName); + + + /** + * Gets the value of the use HttpOnly cookies for session cookies flag. + * + * @return true if the HttpOnly flag should be set on session + * cookies + */ + boolean getUseHttpOnly(); + + + /** + * Sets the use HttpOnly cookies for session cookies flag. + * + * @param useHttpOnly Set to true to use HttpOnly cookies + * for session cookies + */ + void setUseHttpOnly(boolean useHttpOnly); + + + /** + * Should the {@code Partitioned} attribute be added to session cookies created for this web application. + *

+ * The name of the attribute used to indicate a partitioned cookie as part of + * CHIPS is not defined by an RFC and + * may change in a non-backwards compatible way once equivalent functionality is included in an RFC. + * + * @return {@code true} if the {@code Partitioned} attribute should be added to session cookies created for this web + * application, otherwise {@code false} + */ + boolean getUsePartitioned(); + + + /** + * Configure whether the {@code Partitioned} attribute should be added to session cookies created for this web + * application. + *

+ * The name of the attribute used to indicate a partitioned cookie as part of + * CHIPS is not defined by an RFC and + * may change in a non-backwards compatible way once equivalent functionality is included in an RFC. + * + * @param usePartitioned {@code true} if the {@code Partitioned} attribute should be added to session cookies + * created for this web application, otherwise {@code false} + */ + void setUsePartitioned(boolean usePartitioned); + + + /** + * Gets the domain to use for session cookies. Overrides any setting that + * may be specified by the application. + * + * @return The value of the default session cookie domain or null if not + * specified + */ + String getSessionCookieDomain(); + + + /** + * Sets the domain to use for session cookies. Overrides any setting that + * may be specified by the application. + * + * @param sessionCookieDomain The domain to use + */ + void setSessionCookieDomain(String sessionCookieDomain); + + + /** + * Gets the path to use for session cookies. Overrides any setting that + * may be specified by the application. + * + * @return The value of the default session cookie path or null if not + * specified + */ + String getSessionCookiePath(); + + + /** + * Sets the path to use for session cookies. Overrides any setting that + * may be specified by the application. + * + * @param sessionCookiePath The path to use + */ + void setSessionCookiePath(String sessionCookiePath); + + + /** + * Is a / added to the end of the session cookie path to ensure browsers, + * particularly IE, don't send a session cookie for context /foo with + * requests intended for context /foobar. + * + * @return true if the slash is added, otherwise + * false + */ + boolean getSessionCookiePathUsesTrailingSlash(); + + + /** + * Configures if a / is added to the end of the session cookie path to + * ensure browsers, particularly IE, don't send a session cookie for context + * /foo with requests intended for context /foobar. + * + * @param sessionCookiePathUsesTrailingSlash true if the + * slash is should be added, + * otherwise false + */ + void setSessionCookiePathUsesTrailingSlash( + boolean sessionCookiePathUsesTrailingSlash); + + + /** + * Return the "allow crossing servlet contexts" flag. + * + * @return true if cross-contest requests are allowed from this + * web applications, otherwise false + */ + boolean getCrossContext(); + + + /** + * Return the alternate Deployment Descriptor name. + * + * @return the name + */ + String getAltDDName(); + + + /** + * Set an alternate Deployment Descriptor name. + * + * @param altDDName The new name + */ + void setAltDDName(String altDDName) ; + + + /** + * Set the "allow crossing servlet contexts" flag. + * + * @param crossContext The new cross contexts flag + */ + void setCrossContext(boolean crossContext); + + + /** + * Return the deny-uncovered-http-methods flag for this web application. + * + * @return The current value of the flag + */ + boolean getDenyUncoveredHttpMethods(); + + + /** + * Set the deny-uncovered-http-methods flag for this web application. + * + * @param denyUncoveredHttpMethods The new deny-uncovered-http-methods flag + */ + void setDenyUncoveredHttpMethods(boolean denyUncoveredHttpMethods); + + + /** + * Return the display name of this web application. + * + * @return The display name + */ + String getDisplayName(); + + + /** + * Set the display name of this web application. + * + * @param displayName The new display name + */ + void setDisplayName(String displayName); + + + /** + * Get the distributable flag for this web application. + * + * @return The value of the distributable flag for this web application. + */ + boolean getDistributable(); + + + /** + * Set the distributable flag for this web application. + * + * @param distributable The new distributable flag + */ + void setDistributable(boolean distributable); + + + /** + * Obtain the document root for this Context. + * + * @return An absolute pathname or a relative (to the Host's appBase) + * pathname. + */ + String getDocBase(); + + + /** + * Set the document root for this Context. This can be either an absolute + * pathname or a relative pathname. Relative pathnames are relative to the + * containing Host's appBase. + * + * @param docBase The new document root + */ + void setDocBase(String docBase); + + + /** + * Return the URL encoded context path + * + * @return The URL encoded (with UTF-8) context path + */ + String getEncodedPath(); + + + /** + * Determine if annotations parsing is currently disabled + * + * @return {@code true} if annotation parsing is disabled for this web + * application + */ + boolean getIgnoreAnnotations(); + + + /** + * Set the boolean on the annotations parsing for this web + * application. + * + * @param ignoreAnnotations The boolean on the annotations parsing + */ + void setIgnoreAnnotations(boolean ignoreAnnotations); + + + /** + * @return the login configuration descriptor for this web application. + */ + LoginConfig getLoginConfig(); + + + /** + * Set the login configuration descriptor for this web application. + * + * @param config The new login configuration + */ + void setLoginConfig(LoginConfig config); + + + /** + * @return the naming resources associated with this web application. + */ + NamingResourcesImpl getNamingResources(); + + + /** + * Set the naming resources for this web application. + * + * @param namingResources The new naming resources + */ + void setNamingResources(NamingResourcesImpl namingResources); + + + /** + * @return the context path for this web application. + */ + String getPath(); + + + /** + * Set the context path for this web application. + * + * @param path The new context path + */ + void setPath(String path); + + + /** + * @return the public identifier of the deployment descriptor DTD that is + * currently being parsed. + */ + String getPublicId(); + + + /** + * Set the public identifier of the deployment descriptor DTD that is + * currently being parsed. + * + * @param publicId The public identifier + */ + void setPublicId(String publicId); + + + /** + * @return the reloadable flag for this web application. + */ + boolean getReloadable(); + + + /** + * Set the reloadable flag for this web application. + * + * @param reloadable The new reloadable flag + */ + void setReloadable(boolean reloadable); + + + /** + * @return the override flag for this web application. + */ + boolean getOverride(); + + + /** + * Set the override flag for this web application. + * + * @param override The new override flag + */ + void setOverride(boolean override); + + + /** + * @return the privileged flag for this web application. + */ + boolean getPrivileged(); + + + /** + * Set the privileged flag for this web application. + * + * @param privileged The new privileged flag + */ + void setPrivileged(boolean privileged); + + + /** + * @return the Servlet context for which this Context is a facade. + */ + ServletContext getServletContext(); + + + /** + * @return the default session timeout (in minutes) for this + * web application. + */ + int getSessionTimeout(); + + + /** + * Set the default session timeout (in minutes) for this + * web application. + * + * @param timeout The new default session timeout + */ + void setSessionTimeout(int timeout); + + + /** + * Returns true if remaining request data will be read + * (swallowed) even the request violates a data size constraint. + * + * @return true if data will be swallowed (default), + * false otherwise. + */ + boolean getSwallowAbortedUploads(); + + + /** + * Set to false to disable request data swallowing + * after an upload was aborted due to size constraints. + * + * @param swallowAbortedUploads false to disable + * swallowing, true otherwise (default). + */ + void setSwallowAbortedUploads(boolean swallowAbortedUploads); + + /** + * @return the value of the swallowOutput flag. + */ + boolean getSwallowOutput(); + + + /** + * Set the value of the swallowOutput flag. If set to true, the system.out + * and system.err will be redirected to the logger during a servlet + * execution. + * + * @param swallowOutput The new value + */ + void setSwallowOutput(boolean swallowOutput); + + + /** + * @return the Java class name of the Wrapper implementation used + * for servlets registered in this Context. + */ + String getWrapperClass(); + + + /** + * Set the Java class name of the Wrapper implementation used + * for servlets registered in this Context. + * + * @param wrapperClass The new wrapper class + */ + void setWrapperClass(String wrapperClass); + + + /** + * Will the parsing of web.xml and web-fragment.xml files for this Context + * be performed by a namespace aware parser? + * + * @return true if namespace awareness is enabled. + */ + boolean getXmlNamespaceAware(); + + + /** + * Controls whether the parsing of web.xml and web-fragment.xml files for + * this Context will be performed by a namespace aware parser. + * + * @param xmlNamespaceAware true to enable namespace awareness + */ + void setXmlNamespaceAware(boolean xmlNamespaceAware); + + + /** + * Will the parsing of web.xml and web-fragment.xml files for this Context + * be performed by a validating parser? + * + * @return true if validation is enabled. + */ + boolean getXmlValidation(); + + + /** + * Controls whether the parsing of web.xml and web-fragment.xml files + * for this Context will be performed by a validating parser. + * + * @param xmlValidation true to enable xml validation + */ + void setXmlValidation(boolean xmlValidation); + + + /** + * Will the parsing of web.xml, web-fragment.xml, *.tld, *.jspx, *.tagx and + * tagplugin.xml files for this Context block the use of external entities? + * + * @return true if access to external entities is blocked + */ + boolean getXmlBlockExternal(); + + + /** + * Controls whether the parsing of web.xml, web-fragment.xml, *.tld, *.jspx, + * *.tagx and tagplugin.xml files for this Context will block the use of + * external entities. + * + * @param xmlBlockExternal true to block external entities + */ + void setXmlBlockExternal(boolean xmlBlockExternal); + + + /** + * Will the parsing of *.tld files for this Context be performed by a + * validating parser? + * + * @return true if validation is enabled. + */ + boolean getTldValidation(); + + + /** + * Controls whether the parsing of *.tld files for this Context will be + * performed by a validating parser. + * + * @param tldValidation true to enable xml validation + */ + void setTldValidation(boolean tldValidation); + + + /** + * Get the Jar Scanner to be used to scan for JAR resources for this + * context. + * @return The Jar Scanner configured for this context. + */ + JarScanner getJarScanner(); + + /** + * Set the Jar Scanner to be used to scan for JAR resources for this + * context. + * @param jarScanner The Jar Scanner to be used for this context. + */ + void setJarScanner(JarScanner jarScanner); + + /** + * @return the {@link Authenticator} that is used by this context. This is + * always non-{@code null} for a started Context + */ + Authenticator getAuthenticator(); + + /** + * Set whether or not the effective web.xml for this context should be + * logged on context start. + * + * @param logEffectiveWebXml set to true to log the complete + * web.xml that will be used for the webapp + */ + void setLogEffectiveWebXml(boolean logEffectiveWebXml); + + /** + * Should the effective web.xml for this context be logged on context start? + * + * @return true if the reconstructed web.xml that will be used for the + * webapp should be logged + */ + boolean getLogEffectiveWebXml(); + + /** + * @return the instance manager associated with this context. + */ + InstanceManager getInstanceManager(); + + /** + * Set the instance manager associated with this context. + * + * @param instanceManager the new instance manager instance + */ + void setInstanceManager(InstanceManager instanceManager); + + /** + * Sets the regular expression that specifies which container provided SCIs + * should be filtered out and not used for this context. Matching uses + * {@link java.util.regex.Matcher#find()} so the regular expression only has + * to match a sub-string of the fully qualified class name of the container + * provided SCI for it to be filtered out. + * + * @param containerSciFilter The regular expression against which the fully + * qualified class name of each container provided + * SCI should be checked + */ + void setContainerSciFilter(String containerSciFilter); + + /** + * Obtains the regular expression that specifies which container provided + * SCIs should be filtered out and not used for this context. Matching uses + * {@link java.util.regex.Matcher#find()} so the regular expression only has + * to match a sub-string of the fully qualified class name of the container + * provided SCI for it to be filtered out. + * + * @return The regular expression against which the fully qualified class + * name of each container provided SCI will be checked + */ + String getContainerSciFilter(); + + + /** + * @return the value of the parallel annotation scanning flag. If true, + * it will dispatch scanning to the utility executor. + * @deprecated This method will be removed in Tomcat 11 onwards + */ + @Deprecated + default boolean isParallelAnnotationScanning() { + return getParallelAnnotationScanning(); + } + + /** + * @return the value of the parallel annotation scanning flag. If true, + * it will dispatch scanning to the utility executor. + */ + boolean getParallelAnnotationScanning(); + + /** + * Set the parallel annotation scanning value. + * + * @param parallelAnnotationScanning new parallel annotation scanning flag + */ + void setParallelAnnotationScanning(boolean parallelAnnotationScanning); + + + // --------------------------------------------------------- Public Methods + + /** + * Add a new Listener class name to the set of Listeners + * configured for this application. + * + * @param listener Java class name of a listener class + */ + void addApplicationListener(String listener); + + + /** + * Add a new application parameter for this application. + * + * @param parameter The new application parameter + */ + void addApplicationParameter(ApplicationParameter parameter); + + + /** + * Add a security constraint to the set for this web application. + * + * @param constraint The security constraint that should be added + */ + void addConstraint(SecurityConstraint constraint); + + + /** + * Add an error page for the specified error or Java exception. + * + * @param errorPage The error page definition to be added + */ + void addErrorPage(ErrorPage errorPage); + + + /** + * Add a filter definition to this Context. + * + * @param filterDef The filter definition to be added + */ + void addFilterDef(FilterDef filterDef); + + + /** + * Add a filter mapping to this Context. + * + * @param filterMap The filter mapping to be added + */ + void addFilterMap(FilterMap filterMap); + + /** + * Add a filter mapping to this Context before the mappings defined in the + * deployment descriptor but after any other mappings added via this method. + * + * @param filterMap The filter mapping to be added + * + * @exception IllegalArgumentException if the specified filter name + * does not match an existing filter definition, or the filter mapping + * is malformed + */ + void addFilterMapBefore(FilterMap filterMap); + + + /** + * Add a Locale Encoding Mapping (see Sec 5.4 of Servlet spec 2.4) + * + * @param locale locale to map an encoding for + * @param encoding encoding to be used for a give locale + */ + void addLocaleEncodingMappingParameter(String locale, String encoding); + + + /** + * Add a new MIME mapping, replacing any existing mapping for + * the specified extension. + * + * @param extension Filename extension being mapped + * @param mimeType Corresponding MIME type + */ + void addMimeMapping(String extension, String mimeType); + + + /** + * Add a new context initialization parameter, replacing any existing + * value for the specified name. + * + * @param name Name of the new parameter + * @param value Value of the new parameter + */ + void addParameter(String name, String value); + + + /** + * Add a security role reference for this web application. + * + * @param role Security role used in the application + * @param link Actual security role to check for + */ + void addRoleMapping(String role, String link); + + + /** + * Add a new security role for this web application. + * + * @param role New security role + */ + void addSecurityRole(String role); + + + /** + * Add a new servlet mapping, replacing any existing mapping for + * the specified pattern. + * + * @param pattern URL pattern to be mapped + * @param name Name of the corresponding servlet to execute + */ + default void addServletMappingDecoded(String pattern, String name) { + addServletMappingDecoded(pattern, name, false); + } + + + /** + * Add a new servlet mapping, replacing any existing mapping for + * the specified pattern. + * + * @param pattern URL pattern to be mapped + * @param name Name of the corresponding servlet to execute + * @param jspWildcard true if name identifies the JspServlet + * and pattern contains a wildcard; false otherwise + */ + void addServletMappingDecoded(String pattern, String name, + boolean jspWildcard); + + + /** + * Add a resource which will be watched for reloading by the host auto + * deployer. Note: this will not be used in embedded mode. + * + * @param name Path to the resource, relative to docBase + */ + void addWatchedResource(String name); + + + /** + * Add a new welcome file to the set recognized by this Context. + * + * @param name New welcome file name + */ + void addWelcomeFile(String name); + + + /** + * Add the classname of a LifecycleListener to be added to each + * Wrapper appended to this Context. + * + * @param listener Java class name of a LifecycleListener class + */ + void addWrapperLifecycle(String listener); + + + /** + * Add the classname of a ContainerListener to be added to each + * Wrapper appended to this Context. + * + * @param listener Java class name of a ContainerListener class + */ + void addWrapperListener(String listener); + + + /** + * Factory method to create and return a new InstanceManager + * instance. This can be used for framework integration or easier + * configuration with custom Context implementations. + * @return the instance manager + */ + InstanceManager createInstanceManager(); + + /** + * Factory method to create and return a new Wrapper instance, of + * the Java implementation class appropriate for this Context + * implementation. The constructor of the instantiated Wrapper + * will have been called, but no properties will have been set. + * + * @return a newly created wrapper instance that is used to wrap a Servlet + */ + Wrapper createWrapper(); + + + /** + * @return the set of application listener class names configured + * for this application. + */ + String[] findApplicationListeners(); + + + /** + * @return the set of application parameters for this application. + */ + ApplicationParameter[] findApplicationParameters(); + + + /** + * @return the set of security constraints for this web application. + * If there are none, a zero-length array is returned. + */ + SecurityConstraint[] findConstraints(); + + + /** + * @return the error page entry for the specified HTTP error code, + * if any; otherwise return null. + * + * @param errorCode Error code to look up + */ + ErrorPage findErrorPage(int errorCode); + + + /** + * Find and return the ErrorPage instance for the specified exception's + * class, or an ErrorPage instance for the closest superclass for which + * there is such a definition. If no associated ErrorPage instance is + * found, return null. + * + * @param throwable The exception type for which to find an ErrorPage + * + * @return the error page entry for the specified Java exception type, + * if any; otherwise return {@code null}. + */ + ErrorPage findErrorPage(Throwable throwable); + + + /** + * @return the set of defined error pages for all specified error codes + * and exception types. + */ + ErrorPage[] findErrorPages(); + + + /** + * @return the filter definition for the specified filter name, if any; + * otherwise return null. + * + * @param filterName Filter name to look up + */ + FilterDef findFilterDef(String filterName); + + + /** + * @return the set of defined filters for this Context. + */ + FilterDef[] findFilterDefs(); + + + /** + * @return the set of filter mappings for this Context. + */ + FilterMap[] findFilterMaps(); + + + /** + * @return the MIME type to which the specified extension is mapped, + * if any; otherwise return null. + * + * @param extension Extension to map to a MIME type + */ + String findMimeMapping(String extension); + + + /** + * @return the extensions for which MIME mappings are defined. If there + * are none, a zero-length array is returned. + */ + String[] findMimeMappings(); + + + /** + * @return the value for the specified context initialization + * parameter name, if any; otherwise return null. + * + * @param name Name of the parameter to return + */ + String findParameter(String name); + + + /** + * @return the names of all defined context initialization parameters + * for this Context. If no parameters are defined, a zero-length + * array is returned. + */ + String[] findParameters(); + + + /** + * For the given security role (as used by an application), return the + * corresponding role name (as defined by the underlying Realm) if there + * is one. Otherwise, return the specified role unchanged. + * + * @param role Security role to map + * @return The role name that was mapped to the specified role + */ + String findRoleMapping(String role); + + + /** + * @return true if the specified security role is defined + * for this application; otherwise return false. + * + * @param role Security role to verify + */ + boolean findSecurityRole(String role); + + + /** + * @return the security roles defined for this application. If none + * have been defined, a zero-length array is returned. + */ + String[] findSecurityRoles(); + + + /** + * @return the servlet name mapped by the specified pattern (if any); + * otherwise return null. + * + * @param pattern Pattern for which a mapping is requested + */ + String findServletMapping(String pattern); + + + /** + * @return the patterns of all defined servlet mappings for this + * Context. If no mappings are defined, a zero-length array is returned. + */ + String[] findServletMappings(); + + + /** + * @return the associated ThreadBindingListener. + */ + ThreadBindingListener getThreadBindingListener(); + + + /** + * Get the associated ThreadBindingListener. + * + * @param threadBindingListener Set the listener that will receive + * notifications when entering and exiting the application scope + */ + void setThreadBindingListener(ThreadBindingListener threadBindingListener); + + + /** + * @return the set of watched resources for this Context. If none are + * defined, a zero length array will be returned. + */ + String[] findWatchedResources(); + + + /** + * @return true if the specified welcome file is defined + * for this Context; otherwise return false. + * + * @param name Welcome file to verify + */ + boolean findWelcomeFile(String name); + + + /** + * @return the set of welcome files defined for this Context. If none are + * defined, a zero-length array is returned. + */ + String[] findWelcomeFiles(); + + + /** + * @return the set of LifecycleListener classes that will be added to + * newly created Wrappers automatically. + */ + String[] findWrapperLifecycles(); + + + /** + * @return the set of ContainerListener classes that will be added to + * newly created Wrappers automatically. + */ + String[] findWrapperListeners(); + + + /** + * Notify all {@link jakarta.servlet.ServletRequestListener}s that a request + * has started. + * + * @param request The request object that will be passed to the listener + * @return true if the listeners fire successfully, else + * false + */ + boolean fireRequestInitEvent(ServletRequest request); + + /** + * Notify all {@link jakarta.servlet.ServletRequestListener}s that a request + * has ended. + * + * @param request The request object that will be passed to the listener + * @return true if the listeners fire successfully, else + * false + */ + boolean fireRequestDestroyEvent(ServletRequest request); + + /** + * Reload this web application, if reloading is supported. + * + * @exception IllegalStateException if the reloadable + * property is set to false. + */ + void reload(); + + + /** + * Remove the specified application listener class from the set of + * listeners for this application. + * + * @param listener Java class name of the listener to be removed + */ + void removeApplicationListener(String listener); + + + /** + * Remove the application parameter with the specified name from + * the set for this application. + * + * @param name Name of the application parameter to remove + */ + void removeApplicationParameter(String name); + + + /** + * Remove the specified security constraint from this web application. + * + * @param constraint Constraint to be removed + */ + void removeConstraint(SecurityConstraint constraint); + + + /** + * Remove the error page for the specified error code or + * Java language exception, if it exists; otherwise, no action is taken. + * + * @param errorPage The error page definition to be removed + */ + void removeErrorPage(ErrorPage errorPage); + + + /** + * Remove the specified filter definition from this Context, if it exists; + * otherwise, no action is taken. + * + * @param filterDef Filter definition to be removed + */ + void removeFilterDef(FilterDef filterDef); + + + /** + * Remove a filter mapping from this Context. + * + * @param filterMap The filter mapping to be removed + */ + void removeFilterMap(FilterMap filterMap); + + + /** + * Remove the MIME mapping for the specified extension, if it exists; + * otherwise, no action is taken. + * + * @param extension Extension to remove the mapping for + */ + void removeMimeMapping(String extension); + + + /** + * Remove the context initialization parameter with the specified + * name, if it exists; otherwise, no action is taken. + * + * @param name Name of the parameter to remove + */ + void removeParameter(String name); + + + /** + * Remove any security role reference for the specified name + * + * @param role Security role (as used in the application) to remove + */ + void removeRoleMapping(String role); + + + /** + * Remove any security role with the specified name. + * + * @param role Security role to remove + */ + void removeSecurityRole(String role); + + + /** + * Remove any servlet mapping for the specified pattern, if it exists; + * otherwise, no action is taken. + * + * @param pattern URL pattern of the mapping to remove + */ + void removeServletMapping(String pattern); + + + /** + * Remove the specified watched resource name from the list associated + * with this Context. + * + * @param name Name of the watched resource to be removed + */ + void removeWatchedResource(String name); + + + /** + * Remove the specified welcome file name from the list recognized + * by this Context. + * + * @param name Name of the welcome file to be removed + */ + void removeWelcomeFile(String name); + + + /** + * Remove a class name from the set of LifecycleListener classes that + * will be added to newly created Wrappers. + * + * @param listener Class name of a LifecycleListener class to be removed + */ + void removeWrapperLifecycle(String listener); + + + /** + * Remove a class name from the set of ContainerListener classes that + * will be added to newly created Wrappers. + * + * @param listener Class name of a ContainerListener class to be removed + */ + void removeWrapperListener(String listener); + + + /** + * @return the real path for a given virtual path, if possible; otherwise + * return null. + * + * @param path The path to the desired resource + */ + String getRealPath(String path); + + + /** + * @return the effective major version of the Servlet spec used by this + * context. + */ + int getEffectiveMajorVersion(); + + + /** + * Set the effective major version of the Servlet spec used by this + * context. + * + * @param major Set the version number + */ + void setEffectiveMajorVersion(int major); + + + /** + * @return the effective minor version of the Servlet spec used by this + * context. + */ + int getEffectiveMinorVersion(); + + + /** + * Set the effective minor version of the Servlet spec used by this + * context. + * + * @param minor Set the version number + */ + void setEffectiveMinorVersion(int minor); + + + /** + * @return the JSP configuration for this context. + * Will be null if there is no JSP configuration. + */ + JspConfigDescriptor getJspConfigDescriptor(); + + + /** + * Set the JspConfigDescriptor for this context. + * A null value indicates there is not JSP configuration. + * + * @param descriptor the new JSP configuration + */ + void setJspConfigDescriptor(JspConfigDescriptor descriptor); + + + /** + * Add a ServletContainerInitializer instance to this web application. + * + * @param sci The instance to add + * @param classes The classes in which the initializer expressed an + * interest + */ + void addServletContainerInitializer( + ServletContainerInitializer sci, Set> classes); + + + /** + * Is this Context paused whilst it is reloaded? + * + * @return true if the context has been paused + */ + boolean getPaused(); + + + /** + * Is this context using version 2.2 of the Servlet spec? + * + * @return true for a legacy Servlet 2.2 webapp + */ + boolean isServlet22(); + + + /** + * Notification that Servlet security has been dynamically set in a + * {@link jakarta.servlet.ServletRegistration.Dynamic} + * @param registration Servlet security was modified for + * @param servletSecurityElement new security constraints for this Servlet + * @return urls currently mapped to this registration that are already + * present in web.xml + */ + Set addServletSecurity(ServletRegistration.Dynamic registration, + ServletSecurityElement servletSecurityElement); + + /** + * Sets the (comma separated) list of Servlets that expect a resource to be + * present. Used to ensure that welcome files associated with Servlets that + * expect a resource to be present are not mapped when there is no resource. + * + * @param resourceOnlyServlets The Servlet names comma separated list + */ + void setResourceOnlyServlets(String resourceOnlyServlets); + + /** + * Obtains the list of Servlets that expect a resource to be present. + * + * @return A comma separated list of Servlet names as used in web.xml + */ + String getResourceOnlyServlets(); + + /** + * Checks the named Servlet to see if it expects a resource to be present. + * + * @param servletName Name of the Servlet (as per web.xml) to check + * @return true if the Servlet expects a resource, + * otherwise false + */ + boolean isResourceOnlyServlet(String servletName); + + /** + * @return the base name to use for WARs, directories or context.xml files + * for this context. + */ + String getBaseName(); + + /** + * Set the version of this web application - used to differentiate + * different versions of the same web application when using parallel + * deployment. + * + * @param webappVersion The webapp version associated with the context, + * which should be unique + */ + void setWebappVersion(String webappVersion); + + /** + * @return The version of this web application, used to differentiate + * different versions of the same web application when using parallel + * deployment. If not specified, defaults to the empty string. + */ + String getWebappVersion(); + + /** + * Configure whether or not requests listeners will be fired on forwards for + * this Context. + * + * @param enable true to fire request listeners when forwarding + */ + void setFireRequestListenersOnForwards(boolean enable); + + /** + * @return whether or not requests listeners will be fired on forwards for + * this Context. + */ + boolean getFireRequestListenersOnForwards(); + + /** + * Configures if a user presents authentication credentials, whether the + * context will process them when the request is for a non-protected + * resource. + * + * @param enable true to perform authentication even outside + * security constraints + */ + void setPreemptiveAuthentication(boolean enable); + + /** + * @return if a user presents authentication credentials, will the + * context will process them when the request is for a non-protected + * resource. + */ + boolean getPreemptiveAuthentication(); + + /** + * Configures if a response body is included when a redirect response is + * sent to the client. + * + * @param enable true to send a response body for redirects + */ + void setSendRedirectBody(boolean enable); + + /** + * @return if the context is configured to include a response body as + * part of a redirect response. + */ + boolean getSendRedirectBody(); + + /** + * @return the Loader with which this Context is associated. + */ + Loader getLoader(); + + /** + * Set the Loader with which this Context is associated. + * + * @param loader The newly associated loader + */ + void setLoader(Loader loader); + + /** + * @return the Resources with which this Context is associated. + */ + WebResourceRoot getResources(); + + /** + * Set the Resources object with which this Context is associated. + * + * @param resources The newly associated Resources + */ + void setResources(WebResourceRoot resources); + + /** + * @return the Manager with which this Context is associated. If there is + * no associated Manager, return null. + */ + Manager getManager(); + + + /** + * Set the Manager with which this Context is associated. + * + * @param manager The newly associated Manager + */ + void setManager(Manager manager); + + /** + * Sets the flag that indicates if /WEB-INF/classes should be treated like + * an exploded JAR and JAR resources made available as if they were in a + * JAR. + * + * @param addWebinfClassesResources The new value for the flag + */ + void setAddWebinfClassesResources(boolean addWebinfClassesResources); + + /** + * @return the flag that indicates if /WEB-INF/classes should be treated like + * an exploded JAR and JAR resources made available as if they were in a + * JAR. + */ + boolean getAddWebinfClassesResources(); + + /** + * Add a post construct method definition for the given class, if there is + * an existing definition for the specified class - IllegalArgumentException + * will be thrown. + * + * @param clazz Fully qualified class name + * @param method + * Post construct method name + * @throws IllegalArgumentException + * if the fully qualified class name or method name are + * NULL; if there is already post construct method + * definition for the given class + */ + void addPostConstructMethod(String clazz, String method); + + /** + * Add a pre destroy method definition for the given class, if there is an + * existing definition for the specified class - IllegalArgumentException + * will be thrown. + * + * @param clazz Fully qualified class name + * @param method + * Post construct method name + * @throws IllegalArgumentException + * if the fully qualified class name or method name are + * NULL; if there is already pre destroy method + * definition for the given class + */ + void addPreDestroyMethod(String clazz, String method); + + /** + * Removes the post construct method definition for the given class, if it + * exists; otherwise, no action is taken. + * + * @param clazz + * Fully qualified class name + */ + void removePostConstructMethod(String clazz); + + /** + * Removes the pre destroy method definition for the given class, if it + * exists; otherwise, no action is taken. + * + * @param clazz + * Fully qualified class name + */ + void removePreDestroyMethod(String clazz); + + /** + * Returns the method name that is specified as post construct method for + * the given class, if it exists; otherwise NULL will be + * returned. + * + * @param clazz + * Fully qualified class name + * + * @return the method name that is specified as post construct method for + * the given class, if it exists; otherwise NULL will + * be returned. + */ + String findPostConstructMethod(String clazz); + + /** + * Returns the method name that is specified as pre destroy method for the + * given class, if it exists; otherwise NULL will be returned. + * + * @param clazz + * Fully qualified class name + * + * @return the method name that is specified as pre destroy method for the + * given class, if it exists; otherwise NULL will be + * returned. + */ + String findPreDestroyMethod(String clazz); + + /** + * Returns a map with keys - fully qualified class names of the classes that + * have post construct methods and the values are the corresponding method + * names. If there are no such classes an empty map will be returned. + * + * @return a map with keys - fully qualified class names of the classes that + * have post construct methods and the values are the corresponding + * method names. + */ + Map findPostConstructMethods(); + + /** + * Returns a map with keys - fully qualified class names of the classes that + * have pre destroy methods and the values are the corresponding method + * names. If there are no such classes an empty map will be returned. + * + * @return a map with keys - fully qualified class names of the classes that + * have pre destroy methods and the values are the corresponding + * method names. + */ + Map findPreDestroyMethods(); + + /** + * @return the token necessary for operations on the associated JNDI naming + * context. + */ + Object getNamingToken(); + + /** + * Sets the {@link CookieProcessor} that will be used to process cookies + * for this Context. + * + * @param cookieProcessor The new cookie processor + * + * @throws IllegalArgumentException If a {@code null} CookieProcessor is + * specified + */ + void setCookieProcessor(CookieProcessor cookieProcessor); + + /** + * @return the {@link CookieProcessor} that will be used to process cookies + * for this Context. + */ + CookieProcessor getCookieProcessor(); + + /** + * When a client provides the ID for a new session, should that ID be + * validated? The only use case for using a client provided session ID is to + * have a common session ID across multiple web applications. Therefore, + * any client provided session ID should already exist in another web + * application. If this check is enabled, the client provided session ID + * will only be used if the session ID exists in at least one other web + * application for the current host. Note that the following additional + * tests are always applied, irrespective of this setting: + *

    + *
  • The session ID is provided by a cookie
  • + *
  • The session cookie has a path of {@code /}
  • + *
+ * + * @param validateClientProvidedNewSessionId + * {@code true} if validation should be applied + */ + void setValidateClientProvidedNewSessionId(boolean validateClientProvidedNewSessionId); + + /** + * Will client provided session IDs be validated (see {@link + * #setValidateClientProvidedNewSessionId(boolean)}) before use? + * + * @return {@code true} if validation will be applied. Otherwise, {@code + * false} + */ + boolean getValidateClientProvidedNewSessionId(); + + /** + * If enabled, requests for a web application context root will be + * redirected (adding a trailing slash) by the Mapper. This is more + * efficient but has the side effect of confirming that the context path is + * valid. + * + * @param mapperContextRootRedirectEnabled Should the redirects be enabled? + */ + void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled); + + /** + * Determines if requests for a web application context root will be + * redirected (adding a trailing slash) by the Mapper. This is more + * efficient but has the side effect of confirming that the context path is + * valid. + * + * @return {@code true} if the Mapper level redirect is enabled for this + * Context. + */ + boolean getMapperContextRootRedirectEnabled(); + + /** + * If enabled, requests for a directory will be redirected (adding a + * trailing slash) by the Mapper. This is more efficient but has the + * side effect of confirming that the directory is valid. + * + * @param mapperDirectoryRedirectEnabled Should the redirects be enabled? + */ + void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled); + + /** + * Determines if requests for a directory will be redirected (adding a + * trailing slash) by the Mapper. This is more efficient but has the + * side effect of confirming that the directory is valid. + * + * @return {@code true} if the Mapper level redirect is enabled for this + * Context. + */ + boolean getMapperDirectoryRedirectEnabled(); + + /** + * Controls whether HTTP 1.1 and later location headers generated by a call + * to {@link jakarta.servlet.http.HttpServletResponse#sendRedirect(String)} + * will use relative or absolute redirects. + *

+ * Relative redirects are more efficient but may not work with reverse + * proxies that change the context path. It should be noted that it is not + * recommended to use a reverse proxy to change the context path because of + * the multiple issues it creates. + *

+ * Absolute redirects should work with reverse proxies that change the + * context path but may cause issues with the + * {@link org.apache.catalina.filters.RemoteIpFilter} if the filter is + * changing the scheme and/or port. + * + * @param useRelativeRedirects {@code true} to use relative redirects and + * {@code false} to use absolute redirects + */ + void setUseRelativeRedirects(boolean useRelativeRedirects); + + /** + * Will HTTP 1.1 and later location headers generated by a call to + * {@link jakarta.servlet.http.HttpServletResponse#sendRedirect(String)} use + * relative or absolute redirects. + * + * @return {@code true} if relative redirects will be used {@code false} if + * absolute redirects are used. + * + * @see #setUseRelativeRedirects(boolean) + */ + boolean getUseRelativeRedirects(); + + /** + * Are paths used in calls to obtain a request dispatcher expected to be + * encoded? This affects both how Tomcat handles calls to obtain a request + * dispatcher as well as how Tomcat generates paths used to obtain request + * dispatchers internally. + * + * @param dispatchersUseEncodedPaths {@code true} to use encoded paths, + * otherwise {@code false} + */ + void setDispatchersUseEncodedPaths(boolean dispatchersUseEncodedPaths); + + /** + * Are paths used in calls to obtain a request dispatcher expected to be + * encoded? This applies to both how Tomcat handles calls to obtain a request + * dispatcher as well as how Tomcat generates paths used to obtain request + * dispatchers internally. + * + * @return {@code true} if encoded paths will be used, otherwise + * {@code false} + */ + boolean getDispatchersUseEncodedPaths(); + + /** + * Set the default request body encoding for this web application. + * + * @param encoding The default encoding + */ + void setRequestCharacterEncoding(String encoding); + + /** + * Get the default request body encoding for this web application. + * + * @return The default request body encoding + */ + String getRequestCharacterEncoding(); + + /** + * Set the default response body encoding for this web application. + * + * @param encoding The default encoding + */ + void setResponseCharacterEncoding(String encoding); + + /** + * Get the default response body encoding for this web application. + * + * @return The default response body encoding + */ + String getResponseCharacterEncoding(); + + /** + * Configure if, when returning a context path from {@link + * jakarta.servlet.http.HttpServletRequest#getContextPath()}, the return value + * is allowed to contain multiple leading '/' characters. + * + * @param allowMultipleLeadingForwardSlashInPath The new value for the flag + */ + void setAllowMultipleLeadingForwardSlashInPath( + boolean allowMultipleLeadingForwardSlashInPath); + + /** + * When returning a context path from {@link + * jakarta.servlet.http.HttpServletRequest#getContextPath()}, is it allowed to + * contain multiple leading '/' characters? + * + * @return true if multiple leading '/' characters are allowed, + * otherwise false + */ + boolean getAllowMultipleLeadingForwardSlashInPath(); + + + void incrementInProgressAsyncCount(); + + + void decrementInProgressAsyncCount(); + + + /** + * Configure whether Tomcat will attempt to create an upload target used by + * this web application if it does not exist when the web application + * attempts to use it. + * + * @param createUploadTargets {@code true} if Tomcat should attempt to + * create the upload target, otherwise {@code false} + */ + void setCreateUploadTargets(boolean createUploadTargets); + + + /** + * Will Tomcat attempt to create an upload target used by this web + * application if it does not exist when the web application attempts to use + * it? + * + * @return {@code true} if Tomcat will attempt to create an upload target + * otherwise {@code false} + */ + boolean getCreateUploadTargets(); + + + /** + * If this is true, every request that is associated with a + * session will cause the session's last accessed time to be updated + * regardless of whether or not the request explicitly accesses the session. + * If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + * true, the default of this setting will be true, + * else the default value will be false. + * @return the flag value + */ + boolean getAlwaysAccessSession(); + + + /** + * Set the session access behavior. + * @param alwaysAccessSession the new flag value + */ + void setAlwaysAccessSession(boolean alwaysAccessSession); + + + /** + * If this is true then the path passed to + * ServletContext.getResource() or + * ServletContext.getResourceAsStream() must start with + * "/". If false, code like + * getResource("myfolder/myresource.txt") will work as Tomcat + * will prepend "/" to the provided path. + * If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + * true, the default of this setting will be true, + * else the default value will be false. + * @return the flag value + */ + boolean getContextGetResourceRequiresSlash(); + + + /** + * Allow using ServletContext.getResource() or + * ServletContext.getResourceAsStream() without + * a leading "/". + * @param contextGetResourceRequiresSlash the new flag value + */ + void setContextGetResourceRequiresSlash(boolean contextGetResourceRequiresSlash); + + + /** + * If this is true then any wrapped request or response + * object passed to an application dispatcher will be checked to ensure that + * it has wrapped the original request or response. + * If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + * true, the default of this setting will be true, + * else the default value will be false. + * @return the flag value + */ + boolean getDispatcherWrapsSameObject(); + + + /** + * Allow disabling the object wrap check in the request dispatcher. + * @param dispatcherWrapsSameObject the new flag value + */ + void setDispatcherWrapsSameObject(boolean dispatcherWrapsSameObject); + + + /** + * If this is true, then following a forward the response will + * be unwrapped to suspend the Catalina response instead of simply closing + * the top level response. The default value is false. + * @return the flag value + */ + boolean getSuspendWrappedResponseAfterForward(); + + + /** + * Allows unwrapping the response object to suspend the response following + * a forward. + * @param suspendWrappedResponseAfterForward the new flag value + */ + void setSuspendWrappedResponseAfterForward(boolean suspendWrappedResponseAfterForward); + + + /** + * Find configuration file with the specified path, first looking into the + * webapp resources, then delegating to + * ConfigFileLoader.getSource().getResource. The + * WEBAPP_PROTOCOL constant prefix is used to denote webapp + * resources. + * @param name The resource name + * @return the resource + * @throws IOException if an error occurs or if the resource does not exist + */ + default Resource findConfigFileResource(String name) throws IOException { + if (name.startsWith(WEBAPP_PROTOCOL)) { + String path = name.substring(WEBAPP_PROTOCOL.length()); + WebResource resource = getResources().getResource(path); + if (resource.canRead() && resource.isFile()) { + InputStream stream = resource.getInputStream(); + try { + return new Resource(stream, resource.getURL().toURI()); + } catch (URISyntaxException e) { + stream.close(); + } + } + throw new FileNotFoundException(name); + } + return ConfigFileLoader.getSource().getResource(name); + } + + + /** + * @return true if the resources archive lookup will + * use a bloom filter. + * + * @deprecated This method will be removed in Tomcat 11 onwards. + * Use {@link WebResourceRoot#getArchiveIndexStrategy()} + */ + @Deprecated boolean getUseBloomFilterForArchives(); + + /** + * Set bloom filter flag value. + * + * @param useBloomFilterForArchives The new fast class path scan flag + * + * @deprecated This method will be removed in Tomcat 11 onwards + * Use {@link WebResourceRoot#setArchiveIndexStrategy(String)} + */ + @Deprecated void setUseBloomFilterForArchives(boolean useBloomFilterForArchives); +} diff --git a/java/org/apache/catalina/CredentialHandler.java b/java/org/apache/catalina/CredentialHandler.java new file mode 100644 index 0000000..e3dc221 --- /dev/null +++ b/java/org/apache/catalina/CredentialHandler.java @@ -0,0 +1,46 @@ +/* + * 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.catalina; + +/** + * This interface is used by the {@link Realm} to compare the user provided + * credentials with the credentials stored in the {@link Realm} for that user. + */ +public interface CredentialHandler { + + /** + * Checks to see if the input credentials match the stored credentials + * + * @param inputCredentials User provided credentials + * @param storedCredentials Credentials stored in the {@link Realm} + * + * @return true if the inputCredentials match the + * storedCredentials, otherwise false + */ + boolean matches(String inputCredentials, String storedCredentials); + + /** + * Generates the equivalent stored credentials for the given input + * credentials. + * + * @param inputCredentials User provided credentials + * + * @return The equivalent stored credentials for the given input + * credentials + */ + String mutate(String inputCredentials); +} diff --git a/java/org/apache/catalina/DistributedManager.java b/java/org/apache/catalina/DistributedManager.java new file mode 100644 index 0000000..2ac5fd4 --- /dev/null +++ b/java/org/apache/catalina/DistributedManager.java @@ -0,0 +1,50 @@ +/* + * 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.catalina; + +import java.util.Set; + +/** + * Interface implemented by session managers that do not keep a complete copy + * of all sessions in memory but do know where every session is. The + * BackupManager is an example of such a Manager as are implementations of the + * StoreManager interface. + *

+ * With the BackupManager, sessions can be primary (primary copy on this node), + * backup (backup copy on this node) or proxy (only the session ID on this + * node). The identity of the primary and backup nodes are known for all + * sessions, including proxy sessions. + *

+ * With StoreManager implementations, sessions can be primary (session is in + * memory) or proxy (session is in the Store). + */ +public interface DistributedManager { + + /** + * Returns the total session count for primary, backup and proxy. + * + * @return The total session count across the cluster. + */ + int getActiveSessionsFull(); + + /** + * Returns the list of all sessions IDS (primary, backup and proxy). + * + * @return The complete set of sessions IDs across the cluster. + */ + Set getSessionIdsFull(); +} diff --git a/java/org/apache/catalina/Engine.java b/java/org/apache/catalina/Engine.java new file mode 100644 index 0000000..201e93c --- /dev/null +++ b/java/org/apache/catalina/Engine.java @@ -0,0 +1,86 @@ +/* + * 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.catalina; + +/** + * An Engine is a Container that represents the entire Catalina servlet + * engine. It is useful in the following types of scenarios: + *

    + *
  • You wish to use Interceptors that see every single request processed + * by the entire engine. + *
  • You wish to run Catalina in with a standalone HTTP connector, but still + * want support for multiple virtual hosts. + *
+ * In general, you would not use an Engine when deploying Catalina connected + * to a web server (such as Apache), because the Connector will have + * utilized the web server's facilities to determine which Context (or + * perhaps even which Wrapper) should be utilized to process this request. + *

+ * The child containers attached to an Engine are generally implementations + * of Host (representing a virtual host) or Context (representing individual + * an individual servlet context), depending upon the Engine implementation. + *

+ * If used, an Engine is always the top level Container in a Catalina + * hierarchy. Therefore, the implementation's setParent() method + * should throw IllegalArgumentException. + * + * @author Craig R. McClanahan + */ +public interface Engine extends Container { + + /** + * @return the default host name for this Engine. + */ + String getDefaultHost(); + + + /** + * Set the default hostname for this Engine. + * + * @param defaultHost The new default host + */ + void setDefaultHost(String defaultHost); + + + /** + * @return the JvmRouteId for this engine. + */ + String getJvmRoute(); + + + /** + * Set the JvmRouteId for this engine. + * + * @param jvmRouteId the (new) JVM Route ID. Each Engine within a cluster + * must have a unique JVM Route ID. + */ + void setJvmRoute(String jvmRouteId); + + + /** + * @return the Service with which we are associated (if any). + */ + Service getService(); + + + /** + * Set the Service with which we are associated (if any). + * + * @param service The service that owns this Engine + */ + void setService(Service service); +} diff --git a/java/org/apache/catalina/Executor.java b/java/org/apache/catalina/Executor.java new file mode 100644 index 0000000..bf9b575 --- /dev/null +++ b/java/org/apache/catalina/Executor.java @@ -0,0 +1,22 @@ +/* + * 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.catalina; + +public interface Executor extends java.util.concurrent.Executor, Lifecycle { + + String getName(); +} \ No newline at end of file diff --git a/java/org/apache/catalina/Globals.java b/java/org/apache/catalina/Globals.java new file mode 100644 index 0000000..ba1c4b5 --- /dev/null +++ b/java/org/apache/catalina/Globals.java @@ -0,0 +1,281 @@ +/* + * 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.catalina; + +/** + * Global constants that are applicable to multiple packages within Catalina. + * + * @author Craig R. McClanahan + */ +public final class Globals { + + // ------------------------------------------------- Request attribute names + + public static final String ASYNC_SUPPORTED_ATTR = "org.apache.catalina.ASYNC_SUPPORTED"; + + + public static final String GSS_CREDENTIAL_ATTR = "org.apache.catalina.realm.GSS_CREDENTIAL"; + + + /** + * Request dispatcher state. + */ + public static final String DISPATCHER_TYPE_ATTR = "org.apache.catalina.core.DISPATCHER_TYPE"; + + + /** + * Request dispatcher path. + */ + public static final String DISPATCHER_REQUEST_PATH_ATTR = "org.apache.catalina.core.DISPATCHER_REQUEST_PATH"; + + + /** + * The request attribute under which we store the servlet name on a + * named dispatcher request. + */ + public static final String NAMED_DISPATCHER_ATTR = "org.apache.catalina.NAMED"; + + + /** + * The request attribute that is set to {@code Boolean.TRUE} if some request + * parameters have been ignored during request parameters parsing. It can + * happen, for example, if there is a limit on the total count of parseable + * parameters, or if parameter cannot be decoded, or any other error + * happened during parameter parsing. + */ + public static final String PARAMETER_PARSE_FAILED_ATTR = "org.apache.catalina.parameter_parse_failed"; + + + /** + * The reason that the parameter parsing failed. + */ + public static final String PARAMETER_PARSE_FAILED_REASON_ATTR = "org.apache.catalina.parameter_parse_failed_reason"; + + + /** + * The request attribute set by the RemoteIpFilter, RemoteIpValve (and may + * be set by other similar components) that identifies for the connector the + * remote IP address claimed to be associated with this request when a + * request is received via one or more proxies. It is typically provided via + * the X-Forwarded-For HTTP header. + * + * Duplicated here for neater code in the catalina packages. + */ + public static final String REMOTE_ADDR_ATTRIBUTE = org.apache.coyote.Constants.REMOTE_ADDR_ATTRIBUTE; + + + /** + * The request attribute that is set to the value of {@code Boolean.TRUE} + * by the RemoteIpFilter, RemoteIpValve (and other similar components) that identifies + * a request which been forwarded via one or more proxies. + */ + public static final String REQUEST_FORWARDED_ATTRIBUTE = "org.apache.tomcat.request.forwarded"; + + + /** + * The request attribute that is set to the value of {@code Boolean.TRUE} + * if connector processing this request supports use of sendfile. + * + * Duplicated here for neater code in the catalina packages. + */ + public static final String SENDFILE_SUPPORTED_ATTR = org.apache.coyote.Constants.SENDFILE_SUPPORTED_ATTR; + + + /** + * The request attribute that is set to the value of {@code Boolean.TRUE} + * if {@link org.apache.catalina.filters.RemoteIpFilter} determines + * that this request was submitted via a secure channel. + */ + public static final String REMOTE_IP_FILTER_SECURE = "org.apache.catalina.filters.RemoteIpFilter.secure"; + + /** + * The request attribute that can be used by a servlet to pass + * to the connector the name of the file that is to be served + * by sendfile. The value should be {@code String} + * that is {@code File.getCanonicalPath()} of the file to be served. + * + * Duplicated here for neater code in the catalina packages. + */ + public static final String SENDFILE_FILENAME_ATTR = org.apache.coyote.Constants.SENDFILE_FILENAME_ATTR; + + + /** + * The request attribute that can be used by a servlet to pass + * to the connector the start offset of the part of a file + * that is to be served by sendfile. The value should be + * {@code Long}. To serve complete file + * the value should be {@code Long.valueOf(0)}. + * + * Duplicated here for neater code in the catalina packages. + */ + public static final String SENDFILE_FILE_START_ATTR = org.apache.coyote.Constants.SENDFILE_FILE_START_ATTR; + + + /** + * The request attribute that can be used by a servlet to pass + * to the connector the end offset (not including) of the part + * of a file that is to be served by sendfile. The value should be + * {@code java.lang.Long}. To serve complete file + * the value should be equal to the length of the file. + * + * Duplicated here for neater code in the catalina packages. + */ + public static final String SENDFILE_FILE_END_ATTR = org.apache.coyote.Constants.SENDFILE_FILE_END_ATTR; + + + /** + * The request attribute under which we store the array of X509Certificate + * objects representing the certificate chain presented by our client, + * if any. + */ + public static final String CERTIFICATES_ATTR = "jakarta.servlet.request.X509Certificate"; + + + /** + * The request attribute under which we store the name of the cipher suite + * being used on an SSL connection (as an object of type {@link String}). + */ + public static final String CIPHER_SUITE_ATTR = "jakarta.servlet.request.cipher_suite"; + + + /** + * The request attribute under which we store the key size being used for + * this SSL connection (as an object of type {@link Integer}). + */ + public static final String KEY_SIZE_ATTR = "jakarta.servlet.request.key_size"; + + + /** + * The request attribute under which we store the session id being used + * for this SSL connection (as an object of type {@link String}). + */ + public static final String SSL_SESSION_ID_ATTR = "jakarta.servlet.request.ssl_session_id"; + + + /** + * The request attribute key for the session manager. + * This one is a Tomcat extension to the Servlet spec. + */ + public static final String SSL_SESSION_MGR_ATTR = "jakarta.servlet.request.ssl_session_mgr"; + + + // ------------------------------------------------- Session attribute names + + /** + * The subject under which the AccessControlContext is running. + */ + public static final String SUBJECT_ATTR = "javax.security.auth.subject"; + + + // ------------------------------------------ ServletContext attribute names + + /** + * The servlet context attribute under which we store the alternate + * deployment descriptor for this web application + */ + public static final String ALT_DD_ATTR = "org.apache.catalina.deploy.alt_dd"; + + + /** + * The servlet context attribute under which we store the class path + * for our application class loader (as an object of type String), + * delimited with the appropriate path delimiter for this platform. + */ + public static final String CLASS_PATH_ATTR = "org.apache.catalina.jsp_classpath"; + + + /** + * Name of the ServletContext attribute under which we store the context + * Realm's CredentialHandler (if both the Realm and the CredentialHandler + * exist). + */ + public static final String CREDENTIAL_HANDLER = "org.apache.catalina.CredentialHandler"; + + + /** + * The WebResourceRoot which is associated with the context. This can be + * used to manipulate static files. + */ + public static final String RESOURCES_ATTR = "org.apache.catalina.resources"; + + + /** + * Name of the ServletContext attribute under which we store the web + * application version string (the text that appears after ## when parallel + * deployment is used). + */ + public static final String WEBAPP_VERSION = "org.apache.catalina.webappVersion"; + + + // --------------------------- ServletContext initialisation parameter names + + /** + * Name of the ServletContext init-param that determines if the JSP engine + * should validate *.tld files when parsing them. + *

+ * This must be kept in sync with org.apache.jasper.Constants + */ + public static final String JASPER_XML_VALIDATION_TLD_INIT_PARAM = "org.apache.jasper.XML_VALIDATE_TLD"; + + + /** + * Name of the ServletContext init-param that determines if the JSP engine + * will block external entities from being used in *.tld, *.jspx, *.tagx and + * tagplugin.xml files. + *

+ * This must be kept in sync with org.apache.jasper.Constants + */ + public static final String JASPER_XML_BLOCK_EXTERNAL_INIT_PARAM = "org.apache.jasper.XML_BLOCK_EXTERNAL"; + + + // --------------------------------------------------- System property names + + /** + * Name of the system property containing + * the tomcat product installation path + */ + public static final String CATALINA_HOME_PROP = org.apache.catalina.startup.Constants.CATALINA_HOME_PROP; + + + /** + * Name of the system property containing + * the tomcat instance installation path + */ + public static final String CATALINA_BASE_PROP = org.apache.catalina.startup.Constants.CATALINA_BASE_PROP; + + + // -------------------------------------------------------- Global constants + + /** + * The flag which controls strict servlet specification compliance. Setting + * this flag to {@code true} will change the defaults for other settings. + */ + public static final boolean STRICT_SERVLET_COMPLIANCE = + Boolean.getBoolean("org.apache.catalina.STRICT_SERVLET_COMPLIANCE"); + + + /** + * Has security been turned on? + */ + public static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); + + + /** + * Default domain for MBeans if none can be determined + */ + public static final String DEFAULT_MBEAN_DOMAIN = "Catalina"; +} diff --git a/java/org/apache/catalina/Group.java b/java/org/apache/catalina/Group.java new file mode 100644 index 0000000..9cf329e --- /dev/null +++ b/java/org/apache/catalina/Group.java @@ -0,0 +1,117 @@ +/* + * 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.catalina; + +import java.security.Principal; +import java.util.Iterator; + +/** + *

Abstract representation of a group of {@link User}s in a + * {@link UserDatabase}. Each user that is a member of this group + * inherits the {@link Role}s assigned to the group.

+ * + * @author Craig R. McClanahan + * @since 4.1 + */ +public interface Group extends Principal { + + // ------------------------------------------------------------- Properties + + /** + * @return the description of this group. + */ + String getDescription(); + + + /** + * Set the description of this group. + * + * @param description The new description + */ + void setDescription(String description); + + + /** + * @return the group name of this group, which must be unique + * within the scope of a {@link UserDatabase}. + */ + String getGroupname(); + + + /** + * Set the group name of this group, which must be unique + * within the scope of a {@link UserDatabase}. + * + * @param groupname The new group name + */ + void setGroupname(String groupname); + + + /** + * @return the set of {@link Role}s assigned specifically to this group. + */ + Iterator getRoles(); + + + /** + * @return the {@link UserDatabase} within which this Group is defined. + */ + UserDatabase getUserDatabase(); + + + /** + * @return the set of {@link User}s that are members of this group. + */ + Iterator getUsers(); + + + // --------------------------------------------------------- Public Methods + + /** + * Add a new {@link Role} to those assigned specifically to this group. + * + * @param role The new role + */ + void addRole(Role role); + + + /** + * Is this group specifically assigned the specified {@link Role}? + * + * @param role The role to check + * + * @return true if the group is assigned to the specified role + * otherwise false + */ + boolean isInRole(Role role); + + + /** + * Remove a {@link Role} from those assigned to this group. + * + * @param role The old role + */ + void removeRole(Role role); + + + /** + * Remove all {@link Role}s from those assigned to this group. + */ + void removeRoles(); + + +} diff --git a/java/org/apache/catalina/Host.java b/java/org/apache/catalina/Host.java new file mode 100644 index 0000000..f9af68e --- /dev/null +++ b/java/org/apache/catalina/Host.java @@ -0,0 +1,280 @@ +/* + * 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.catalina; + +import java.io.File; +import java.util.concurrent.ExecutorService; +import java.util.regex.Pattern; + + +/** + * A Host is a Container that represents a virtual host in the + * Catalina servlet engine. It is useful in the following types of scenarios: + *
    + *
  • You wish to use Interceptors that see every single request processed + * by this particular virtual host. + *
  • You wish to run Catalina in with a standalone HTTP connector, but still + * want support for multiple virtual hosts. + *
+ * In general, you would not use a Host when deploying Catalina connected + * to a web server (such as Apache), because the Connector will have + * utilized the web server's facilities to determine which Context (or + * perhaps even which Wrapper) should be utilized to process this request. + *

+ * The parent Container attached to a Host is generally an Engine, but may + * be some other implementation, or may be omitted if it is not necessary. + *

+ * The child containers attached to a Host are generally implementations + * of Context (representing an individual servlet context). + * + * @author Craig R. McClanahan + */ +public interface Host extends Container { + + + // ----------------------------------------------------- Manifest Constants + + + /** + * The ContainerEvent event type sent when a new alias is added + * by addAlias(). + */ + String ADD_ALIAS_EVENT = "addAlias"; + + + /** + * The ContainerEvent event type sent when an old alias is removed + * by removeAlias(). + */ + String REMOVE_ALIAS_EVENT = "removeAlias"; + + + // ------------------------------------------------------------- Properties + + + /** + * @return the XML root for this Host. This can be an absolute + * pathname or a relative pathname. + * If null, the base path defaults to + * ${catalina.base}/conf/<engine name>/<host name> directory + */ + String getXmlBase(); + + /** + * Set the Xml root for this Host. This can be an absolute + * pathname or a relative pathname. + * If null, the base path defaults to + * ${catalina.base}/conf/<engine name>/<host name> directory + * @param xmlBase The new XML root + */ + void setXmlBase(String xmlBase); + + /** + * @return a default configuration path of this Host. The file will be + * canonical if possible. + */ + File getConfigBaseFile(); + + /** + * @return the application root for this Host. This can be an absolute + * pathname, a relative pathname, or a URL. + */ + String getAppBase(); + + + /** + * @return an absolute {@link File} for the appBase of this Host. The file + * will be canonical if possible. There is no guarantee that that the + * appBase exists. + */ + File getAppBaseFile(); + + + /** + * Set the application root for this Host. This can be an absolute + * pathname, a relative pathname, or a URL. + * + * @param appBase The new application root + */ + void setAppBase(String appBase); + + + /** + * @return the legacy (Java EE) application root for this Host. This can be + * an absolute pathname, a relative pathname, or a URL. + */ + String getLegacyAppBase(); + + + /** + * @return an absolute {@link File} for the legacy (Java EE) appBase of this + * Host. The file will be canonical if possible. There is no guarantee that + * that the appBase exists. + */ + File getLegacyAppBaseFile(); + + + /** + * Set the legacy (Java EE) application root for this Host. This can be an + * absolute pathname, a relative pathname, or a URL. + * + * @param legacyAppBase The new legacy application root + */ + void setLegacyAppBase(String legacyAppBase); + + + /** + * @return the value of the auto deploy flag. If true, it indicates that + * this host's child webapps should be discovered and automatically + * deployed dynamically. + */ + boolean getAutoDeploy(); + + + /** + * Set the auto deploy flag value for this host. + * + * @param autoDeploy The new auto deploy flag + */ + void setAutoDeploy(boolean autoDeploy); + + + /** + * @return the Java class name of the context configuration class + * for new web applications. + */ + String getConfigClass(); + + + /** + * Set the Java class name of the context configuration class + * for new web applications. + * + * @param configClass The new context configuration class + */ + void setConfigClass(String configClass); + + + /** + * @return the value of the deploy on startup flag. If true, it indicates + * that this host's child webapps should be discovered and automatically + * deployed. + */ + boolean getDeployOnStartup(); + + + /** + * Set the deploy on startup flag value for this host. + * + * @param deployOnStartup The new deploy on startup flag + */ + void setDeployOnStartup(boolean deployOnStartup); + + + /** + * @return the regular expression that defines the files and directories in + * the host's appBase that will be ignored by the automatic deployment + * process. + */ + String getDeployIgnore(); + + + /** + * @return the compiled regular expression that defines the files and + * directories in the host's appBase that will be ignored by the automatic + * deployment process. + */ + Pattern getDeployIgnorePattern(); + + + /** + * Set the regular expression that defines the files and directories in + * the host's appBase that will be ignored by the automatic deployment + * process. + * + * @param deployIgnore A regular expression matching file names + */ + void setDeployIgnore(String deployIgnore); + + + /** + * @return the executor that is used for starting and stopping contexts. This + * is primarily for use by components deploying contexts that want to do + * this in a multi-threaded manner. + */ + ExecutorService getStartStopExecutor(); + + + /** + * Returns true if the Host will attempt to create directories for appBase and xmlBase + * unless they already exist. + * @return true if the Host will attempt to create directories + */ + boolean getCreateDirs(); + + + /** + * Should the Host attempt to create directories for xmlBase and appBase + * upon startup. + * + * @param createDirs The new value for this flag + */ + void setCreateDirs(boolean createDirs); + + + /** + * @return true of the Host is configured to automatically undeploy old + * versions of applications deployed using parallel deployment. This only + * takes effect is {@link #getAutoDeploy()} also returns true. + */ + boolean getUndeployOldVersions(); + + + /** + * Set to true if the Host should automatically undeploy old versions of + * applications deployed using parallel deployment. This only takes effect + * if {@link #getAutoDeploy()} returns true. + * + * @param undeployOldVersions The new value for this flag + */ + void setUndeployOldVersions(boolean undeployOldVersions); + + + // --------------------------------------------------------- Public Methods + + /** + * Add an alias name that should be mapped to this same Host. + * + * @param alias The alias to be added + */ + void addAlias(String alias); + + + /** + * @return the set of alias names for this Host. If none are defined, + * a zero length array is returned. + */ + String[] findAliases(); + + + /** + * Remove the specified alias name from the aliases for this Host. + * + * @param alias Alias name to be removed + */ + void removeAlias(String alias); +} diff --git a/java/org/apache/catalina/JmxEnabled.java b/java/org/apache/catalina/JmxEnabled.java new file mode 100644 index 0000000..2d514f4 --- /dev/null +++ b/java/org/apache/catalina/JmxEnabled.java @@ -0,0 +1,52 @@ +/* + * 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.catalina; + +import javax.management.MBeanRegistration; +import javax.management.ObjectName; + +/** + * This interface is implemented by components that will be registered with an + * MBean server when they are created and unregistered when they are destroyed. + * It is primarily intended to be implemented by components that implement + * {@link Lifecycle} but is not exclusively for them. + */ +public interface JmxEnabled extends MBeanRegistration { + + /** + * @return the domain under which this component will be / has been + * registered. + */ + String getDomain(); + + + /** + * Specify the domain under which this component should be registered. Used + * with components that cannot (easily) navigate the component hierarchy to + * determine the correct domain to use. + * + * @param domain The name of the domain under which this component should be + * registered + */ + void setDomain(String domain); + + + /** + * @return the name under which this component has been registered with JMX. + */ + ObjectName getObjectName(); +} diff --git a/java/org/apache/catalina/Lifecycle.java b/java/org/apache/catalina/Lifecycle.java new file mode 100644 index 0000000..5095a59 --- /dev/null +++ b/java/org/apache/catalina/Lifecycle.java @@ -0,0 +1,319 @@ +/* + * 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.catalina; + + +/** + * Common interface for component life cycle methods. Catalina components + * may implement this interface (as well as the appropriate interface(s) for + * the functionality they support) in order to provide a consistent mechanism + * to start and stop the component. + *
+ * The valid state transitions for components that support {@link Lifecycle} + * are: + *

+ *            start()
+ *  -----------------------------
+ *  |                           |
+ *  | init()                    |
+ * NEW -»-- INITIALIZING        |
+ * | |           |              |     ------------------«-----------------------
+ * | |           |auto          |     |                                        |
+ * | |          \|/    start() \|/   \|/     auto          auto         stop() |
+ * | |      INITIALIZED --»-- STARTING_PREP --»- STARTING --»- STARTED --»---  |
+ * | |         |                                                            |  |
+ * | |destroy()|                                                            |  |
+ * | --»-----«--    ------------------------«--------------------------------  ^
+ * |     |          |                                                          |
+ * |     |         \|/          auto                 auto              start() |
+ * |     |     STOPPING_PREP ----»---- STOPPING ------»----- STOPPED -----»-----
+ * |    \|/                               ^                     |  ^
+ * |     |               stop()           |                     |  |
+ * |     |       --------------------------                     |  |
+ * |     |       |                                              |  |
+ * |     |       |    destroy()                       destroy() |  |
+ * |     |    FAILED ----»------ DESTROYING ---«-----------------  |
+ * |     |                        ^     |                          |
+ * |     |     destroy()          |     |auto                      |
+ * |     --------»-----------------    \|/                         |
+ * |                                 DESTROYED                     |
+ * |                                                               |
+ * |                            stop()                             |
+ * ----»-----------------------------»------------------------------
+ *
+ * Any state can transition to FAILED.
+ *
+ * Calling start() while a component is in states STARTING_PREP, STARTING or
+ * STARTED has no effect.
+ *
+ * Calling start() while a component is in state NEW will cause init() to be
+ * called immediately after the start() method is entered.
+ *
+ * Calling stop() while a component is in states STOPPING_PREP, STOPPING or
+ * STOPPED has no effect.
+ *
+ * Calling stop() while a component is in state NEW transitions the component
+ * to STOPPED. This is typically encountered when a component fails to start and
+ * does not start all its sub-components. When the component is stopped, it will
+ * try to stop all sub-components - even those it didn't start.
+ *
+ * Attempting any other transition will throw {@link LifecycleException}.
+ *
+ * 
+ * The {@link LifecycleEvent}s fired during state changes are defined in the + * methods that trigger the changed. No {@link LifecycleEvent}s are fired if the + * attempted transition is not valid. + * + * @author Craig R. McClanahan + */ +public interface Lifecycle { + + + // ----------------------------------------------------- Manifest Constants + + + /** + * The LifecycleEvent type for the "component before init" event. + */ + String BEFORE_INIT_EVENT = "before_init"; + + + /** + * The LifecycleEvent type for the "component after init" event. + */ + String AFTER_INIT_EVENT = "after_init"; + + + /** + * The LifecycleEvent type for the "component start" event. + */ + String START_EVENT = "start"; + + + /** + * The LifecycleEvent type for the "component before start" event. + */ + String BEFORE_START_EVENT = "before_start"; + + + /** + * The LifecycleEvent type for the "component after start" event. + */ + String AFTER_START_EVENT = "after_start"; + + + /** + * The LifecycleEvent type for the "component stop" event. + */ + String STOP_EVENT = "stop"; + + + /** + * The LifecycleEvent type for the "component before stop" event. + */ + String BEFORE_STOP_EVENT = "before_stop"; + + + /** + * The LifecycleEvent type for the "component after stop" event. + */ + String AFTER_STOP_EVENT = "after_stop"; + + + /** + * The LifecycleEvent type for the "component after destroy" event. + */ + String AFTER_DESTROY_EVENT = "after_destroy"; + + + /** + * The LifecycleEvent type for the "component before destroy" event. + */ + String BEFORE_DESTROY_EVENT = "before_destroy"; + + + /** + * The LifecycleEvent type for the "periodic" event. + */ + String PERIODIC_EVENT = "periodic"; + + + /** + * The LifecycleEvent type for the "configure_start" event. Used by those + * components that use a separate component to perform configuration and + * need to signal when configuration should be performed - usually after + * {@link #BEFORE_START_EVENT} and before {@link #START_EVENT}. + */ + String CONFIGURE_START_EVENT = "configure_start"; + + + /** + * The LifecycleEvent type for the "configure_stop" event. Used by those + * components that use a separate component to perform configuration and + * need to signal when de-configuration should be performed - usually after + * {@link #STOP_EVENT} and before {@link #AFTER_STOP_EVENT}. + */ + String CONFIGURE_STOP_EVENT = "configure_stop"; + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a LifecycleEvent listener to this component. + * + * @param listener The listener to add + */ + void addLifecycleListener(LifecycleListener listener); + + + /** + * Get the life cycle listeners associated with this life cycle. + * + * @return An array containing the life cycle listeners associated with this + * life cycle. If this component has no listeners registered, a + * zero-length array is returned. + */ + LifecycleListener[] findLifecycleListeners(); + + + /** + * Remove a LifecycleEvent listener from this component. + * + * @param listener The listener to remove + */ + void removeLifecycleListener(LifecycleListener listener); + + + /** + * Prepare the component for starting. This method should perform any + * initialization required post object creation. The following + * {@link LifecycleEvent}s will be fired in the following order: + *
    + *
  1. INIT_EVENT: On the successful completion of component + * initialization.
  2. + *
+ * + * @exception LifecycleException if this component detects a fatal error + * that prevents this component from being used + */ + void init() throws LifecycleException; + + /** + * Prepare for the beginning of active use of the public methods other than + * property getters/setters and life cycle methods of this component. This + * method should be called before any of the public methods other than + * property getters/setters and life cycle methods of this component are + * utilized. The following {@link LifecycleEvent}s will be fired in the + * following order: + *
    + *
  1. BEFORE_START_EVENT: At the beginning of the method. It is as this + * point the state transitions to + * {@link LifecycleState#STARTING_PREP}.
  2. + *
  3. START_EVENT: During the method once it is safe to call start() for + * any child components. It is at this point that the + * state transitions to {@link LifecycleState#STARTING} + * and that the public methods other than property + * getters/setters and life cycle methods may be + * used.
  4. + *
  5. AFTER_START_EVENT: At the end of the method, immediately before it + * returns. It is at this point that the state + * transitions to {@link LifecycleState#STARTED}. + *
  6. + *
+ * + * @exception LifecycleException if this component detects a fatal error + * that prevents this component from being used + */ + void start() throws LifecycleException; + + + /** + * Gracefully terminate the active use of the public methods other than + * property getters/setters and life cycle methods of this component. Once + * the STOP_EVENT is fired, the public methods other than property + * getters/setters and life cycle methods should not be used. The following + * {@link LifecycleEvent}s will be fired in the following order: + *
    + *
  1. BEFORE_STOP_EVENT: At the beginning of the method. It is at this + * point that the state transitions to + * {@link LifecycleState#STOPPING_PREP}.
  2. + *
  3. STOP_EVENT: During the method once it is safe to call stop() for + * any child components. It is at this point that the + * state transitions to {@link LifecycleState#STOPPING} + * and that the public methods other than property + * getters/setters and life cycle methods may no longer be + * used.
  4. + *
  5. AFTER_STOP_EVENT: At the end of the method, immediately before it + * returns. It is at this point that the state + * transitions to {@link LifecycleState#STOPPED}. + *
  6. + *
+ * + * Note that if transitioning from {@link LifecycleState#FAILED} then the + * three events above will be fired but the component will transition + * directly from {@link LifecycleState#FAILED} to + * {@link LifecycleState#STOPPING}, bypassing + * {@link LifecycleState#STOPPING_PREP} + * + * @exception LifecycleException if this component detects a fatal error + * that needs to be reported + */ + void stop() throws LifecycleException; + + /** + * Prepare to discard the object. The following {@link LifecycleEvent}s will + * be fired in the following order: + *
    + *
  1. DESTROY_EVENT: On the successful completion of component + * destruction.
  2. + *
+ * + * @exception LifecycleException if this component detects a fatal error + * that prevents this component from being used + */ + void destroy() throws LifecycleException; + + + /** + * Obtain the current state of the source component. + * + * @return The current state of the source component. + */ + LifecycleState getState(); + + + /** + * Obtain a textual representation of the current component state. Useful + * for JMX. The format of this string may vary between point releases and + * should not be relied upon to determine component state. To determine + * component state, use {@link #getState()}. + * + * @return The name of the current component state. + */ + String getStateName(); + + + /** + * Marker interface used to indicate that the instance should only be used + * once. Calling {@link #stop()} on an instance that supports this interface + * will automatically call {@link #destroy()} after {@link #stop()} + * completes. + */ + interface SingleUse { + } +} diff --git a/java/org/apache/catalina/LifecycleEvent.java b/java/org/apache/catalina/LifecycleEvent.java new file mode 100644 index 0000000..c681586 --- /dev/null +++ b/java/org/apache/catalina/LifecycleEvent.java @@ -0,0 +1,80 @@ +/* + * 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.catalina; + +import java.util.EventObject; + +/** + * General event for notifying listeners of significant changes on a component + * that implements the Lifecycle interface. + * + * @author Craig R. McClanahan + */ +public final class LifecycleEvent extends EventObject { + + private static final long serialVersionUID = 1L; + + + /** + * Construct a new LifecycleEvent with the specified parameters. + * + * @param lifecycle Component on which this event occurred + * @param type Event type (required) + * @param data Event data (if any) + */ + public LifecycleEvent(Lifecycle lifecycle, String type, Object data) { + super(lifecycle); + this.type = type; + this.data = data; + } + + + /** + * The event data associated with this event. + */ + private final Object data; + + + /** + * The event type this instance represents. + */ + private final String type; + + + /** + * @return the event data of this event. + */ + public Object getData() { + return data; + } + + + /** + * @return the Lifecycle on which this event occurred. + */ + public Lifecycle getLifecycle() { + return (Lifecycle) getSource(); + } + + + /** + * @return the event type of this event. + */ + public String getType() { + return this.type; + } +} diff --git a/java/org/apache/catalina/LifecycleException.java b/java/org/apache/catalina/LifecycleException.java new file mode 100644 index 0000000..cfd8e49 --- /dev/null +++ b/java/org/apache/catalina/LifecycleException.java @@ -0,0 +1,72 @@ +/* + * 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.catalina; + + +/** + * General purpose exception that is thrown to indicate a lifecycle related + * problem. Such exceptions should generally be considered fatal to the + * operation of the application containing this component. + * + * @author Craig R. McClanahan + */ +public final class LifecycleException extends Exception { + + private static final long serialVersionUID = 1L; + + //------------------------------------------------------------ Constructors + + + /** + * Construct a new LifecycleException with no other information. + */ + public LifecycleException() { + super(); + } + + + /** + * Construct a new LifecycleException for the specified message. + * + * @param message Message describing this exception + */ + public LifecycleException(String message) { + super(message); + } + + + /** + * Construct a new LifecycleException for the specified throwable. + * + * @param throwable Throwable that caused this exception + */ + public LifecycleException(Throwable throwable) { + super(throwable); + } + + + /** + * Construct a new LifecycleException for the specified message + * and throwable. + * + * @param message Message describing this exception + * @param throwable Throwable that caused this exception + */ + public LifecycleException(String message, Throwable throwable) { + super(message, throwable); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/LifecycleListener.java b/java/org/apache/catalina/LifecycleListener.java new file mode 100644 index 0000000..0efe7ea --- /dev/null +++ b/java/org/apache/catalina/LifecycleListener.java @@ -0,0 +1,39 @@ +/* + * 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.catalina; + + +/** + * Interface defining a listener for significant events (including "component + * start" and "component stop" generated by a component that implements the + * Lifecycle interface. The listener will be fired after the associated state + * change has taken place. + * + * @author Craig R. McClanahan + */ +public interface LifecycleListener { + + + /** + * Acknowledge the occurrence of the specified event. + * + * @param event LifecycleEvent that has occurred + */ + void lifecycleEvent(LifecycleEvent event); + + +} diff --git a/java/org/apache/catalina/LifecycleState.java b/java/org/apache/catalina/LifecycleState.java new file mode 100644 index 0000000..717e587 --- /dev/null +++ b/java/org/apache/catalina/LifecycleState.java @@ -0,0 +1,65 @@ +/* + * 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.catalina; + +/** + * The list of valid states for components that implement {@link Lifecycle}. + * See {@link Lifecycle} for the state transition diagram. + */ +public enum LifecycleState { + NEW(false, null), + INITIALIZING(false, Lifecycle.BEFORE_INIT_EVENT), + INITIALIZED(false, Lifecycle.AFTER_INIT_EVENT), + STARTING_PREP(false, Lifecycle.BEFORE_START_EVENT), + STARTING(true, Lifecycle.START_EVENT), + STARTED(true, Lifecycle.AFTER_START_EVENT), + STOPPING_PREP(true, Lifecycle.BEFORE_STOP_EVENT), + STOPPING(false, Lifecycle.STOP_EVENT), + STOPPED(false, Lifecycle.AFTER_STOP_EVENT), + DESTROYING(false, Lifecycle.BEFORE_DESTROY_EVENT), + DESTROYED(false, Lifecycle.AFTER_DESTROY_EVENT), + FAILED(false, null); + + private final boolean available; + private final String lifecycleEvent; + + LifecycleState(boolean available, String lifecycleEvent) { + this.available = available; + this.lifecycleEvent = lifecycleEvent; + } + + /** + * May the public methods other than property getters/setters and lifecycle + * methods be called for a component in this state? It returns + * true for any component in any of the following states: + *
    + *
  • {@link #STARTING}
  • + *
  • {@link #STARTED}
  • + *
  • {@link #STOPPING_PREP}
  • + *
+ * + * @return true if the component is available for use, + * otherwise false + */ + public boolean isAvailable() { + return available; + } + + public String getLifecycleEvent() { + return lifecycleEvent; + } +} diff --git a/java/org/apache/catalina/Loader.java b/java/org/apache/catalina/Loader.java new file mode 100644 index 0000000..667acf7 --- /dev/null +++ b/java/org/apache/catalina/Loader.java @@ -0,0 +1,121 @@ +/* + * 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.catalina; + +import java.beans.PropertyChangeListener; + +/** + * A Loader represents a Java ClassLoader implementation that can + * be used by a Container to load class files (within a repository associated + * with the Loader) that are designed to be reloaded upon request, as well as + * a mechanism to detect whether changes have occurred in the underlying + * repository. + *

+ * In order for a Loader implementation to successfully operate + * with a Context implementation that implements reloading, it + * must obey the following constraints: + *

    + *
  • Must implement Lifecycle so that the Context can indicate + * that a new class loader is required. + *
  • The start() method must unconditionally create a new + * ClassLoader implementation. + *
  • The stop() method must throw away its reference to the + * ClassLoader previously utilized, so that the class loader, + * all classes loaded by it, and all objects of those classes, can be + * garbage collected. + *
  • Must allow a call to stop() to be followed by a call to + * start() on the same Loader instance. + *
  • Based on a policy chosen by the implementation, must call the + * Context.reload() method on the owning Context + * when a change to one or more of the class files loaded by this class + * loader is detected. + *
+ * + * @author Craig R. McClanahan + */ +public interface Loader { + + + /** + * Execute a periodic task, such as reloading, etc. This method will be + * invoked inside the classloading context of this container. Unexpected + * throwables will be caught and logged. + */ + void backgroundProcess(); + + + /** + * @return the Java class loader to be used by this Container. + */ + ClassLoader getClassLoader(); + + + /** + * @return the Context with which this Loader has been associated. + */ + Context getContext(); + + + /** + * Set the Context with which this Loader has been associated. + * + * @param context The associated Context + */ + void setContext(Context context); + + + /** + * @return the "follow standard delegation model" flag used to configure + * our ClassLoader. + */ + boolean getDelegate(); + + + /** + * Set the "follow standard delegation model" flag used to configure + * our ClassLoader. + * + * @param delegate The new flag + */ + void setDelegate(boolean delegate); + + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + void addPropertyChangeListener(PropertyChangeListener listener); + + + /** + * Has the internal repository associated with this Loader been modified, + * such that the loaded classes should be reloaded? + * + * @return true when the repository has been modified, + * false otherwise + */ + boolean modified(); + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + void removePropertyChangeListener(PropertyChangeListener listener); +} diff --git a/java/org/apache/catalina/Manager.java b/java/org/apache/catalina/Manager.java new file mode 100644 index 0000000..b8438b1 --- /dev/null +++ b/java/org/apache/catalina/Manager.java @@ -0,0 +1,498 @@ +/* + * 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.catalina; + +import java.beans.PropertyChangeListener; +import java.io.IOException; + +/** + * A Manager manages the pool of Sessions that are associated with a + * particular Context. Different Manager implementations may support + * value-added features such as the persistent storage of session data, + * as well as migrating sessions for distributable web applications. + *

+ * In order for a Manager implementation to successfully operate + * with a Context implementation that implements reloading, it + * must obey the following constraints: + *

    + *
  • Must implement Lifecycle so that the Context can indicate + * that a restart is required. + *
  • Must allow a call to stop() to be followed by a call to + * start() on the same Manager instance. + *
+ * + * @author Craig R. McClanahan + */ +public interface Manager { + + // ------------------------------------------------------------- Properties + + /** + * Get the Context with which this Manager is associated. + * + * @return The associated Context + */ + Context getContext(); + + + /** + * Set the Context with which this Manager is associated. The Context must + * be set to a non-null value before the Manager is first used. Multiple + * calls to this method before first use are permitted. Once the Manager has + * been used, this method may not be used to change the Context (including + * setting a {@code null} value) that the Manager is associated with. + * + * @param context The newly associated Context + */ + void setContext(Context context); + + + /** + * @return the session id generator + */ + SessionIdGenerator getSessionIdGenerator(); + + + /** + * Sets the session id generator + * + * @param sessionIdGenerator The session id generator + */ + void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator); + + + /** + * Returns the total number of sessions created by this manager, which is + * approximated as the number of active sessions plus the number of + * expired sessions. + * + * @return Total number of sessions created by this manager. + */ + long getSessionCounter(); + + + /** + * Sets the total number of sessions created by this manager. + * + * @param sessionCounter Total number of sessions created by this manager. + * @deprecated This will be removed in Tomcat 11 + */ + @Deprecated + void setSessionCounter(long sessionCounter); + + + /** + * Gets the maximum number of sessions that have been active at the same + * time. + * + * @return Maximum number of sessions that have been active at the same + * time + */ + int getMaxActive(); + + + /** + * (Re)sets the maximum number of sessions that have been active at the + * same time. + * + * @param maxActive Maximum number of sessions that have been active at + * the same time. + */ + void setMaxActive(int maxActive); + + + /** + * Gets the number of currently active sessions. + * + * @return Number of currently active sessions + */ + int getActiveSessions(); + + + /** + * Gets the number of sessions that have expired. + * + * @return Number of sessions that have expired + */ + long getExpiredSessions(); + + + /** + * Sets the number of sessions that have expired. + * + * @param expiredSessions Number of sessions that have expired + */ + void setExpiredSessions(long expiredSessions); + + + /** + * Gets the number of sessions that were not created because the maximum + * number of active sessions was reached. + * + * @return Number of rejected sessions + */ + int getRejectedSessions(); + + + /** + * Gets the longest time (in seconds) that an expired session had been + * alive. + * + * @return Longest time (in seconds) that an expired session had been + * alive. + */ + int getSessionMaxAliveTime(); + + + /** + * Sets the longest time (in seconds) that an expired session had been + * alive. + * + * @param sessionMaxAliveTime Longest time (in seconds) that an expired + * session had been alive. + */ + void setSessionMaxAliveTime(int sessionMaxAliveTime); + + + /** + * Gets the average time (in seconds) that expired sessions had been + * alive. This may be based on sample data. + * + * @return Average time (in seconds) that expired sessions had been + * alive. + */ + int getSessionAverageAliveTime(); + + + /** + * Gets the current rate of session creation (in session per minute). This + * may be based on sample data. + * + * @return The current rate (in sessions per minute) of session creation + */ + int getSessionCreateRate(); + + + /** + * Gets the current rate of session expiration (in session per minute). This + * may be based on sample data + * + * @return The current rate (in sessions per minute) of session expiration + */ + int getSessionExpireRate(); + + + // --------------------------------------------------------- Public Methods + + /** + * Add this Session to the set of active Sessions for this Manager. + * + * @param session Session to be added + */ + void add(Session session); + + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + void addPropertyChangeListener(PropertyChangeListener listener); + + + /** + * Change the session ID of the current session to a new randomly generated + * session ID. + * + * @param session The session to change the session ID for + * + * @return The new session ID + */ + default String rotateSessionId(Session session) { + String newSessionId = null; + // Assume the new Id is a duplicate until we prove it isn't. The + // chances of a duplicate are extremely low but the current ManagerBase + // code protects against duplicates so this default method does too. + boolean duplicate = true; + do { + newSessionId = getSessionIdGenerator().generateSessionId(); + try { + if (findSession(newSessionId) == null) { + duplicate = false; + } + } catch (IOException ioe) { + // Swallow. An IOE means the ID was known so continue looping + } + } while (duplicate); + changeSessionId(session, newSessionId); + return newSessionId; + } + + + /** + * Change the session ID of the current session to a specified session ID. + * + * @param session The session to change the session ID for + * @param newId new session ID + */ + void changeSessionId(Session session, String newId); + + + /** + * Get a session from the recycled ones or create a new empty one. + * The PersistentManager manager does not need to create session data + * because it reads it from the Store. + * + * @return An empty Session object + */ + Session createEmptySession(); + + + /** + * Construct and return a new session object, based on the default + * settings specified by this Manager's properties. The session + * id specified will be used as the session id. + * If a new session cannot be created for any reason, return + * null. + * + * @param sessionId The session id which should be used to create the + * new session; if null, the session + * id will be assigned by this method, and available via the getId() + * method of the returned session. + * @exception IllegalStateException if a new session cannot be + * instantiated for any reason + * + * @return An empty Session object with the given ID or a newly created + * session ID if none was specified + */ + Session createSession(String sessionId); + + + /** + * Return the active Session, associated with this Manager, with the + * specified session id (if any); otherwise return null. + * + * @param id The session id for the session to be returned + * + * @exception IllegalStateException if a new session cannot be + * instantiated for any reason + * @exception IOException if an input/output error occurs while + * processing this request + * + * @return the request session or {@code null} if a session with the + * requested ID could not be found + */ + Session findSession(String id) throws IOException; + + + /** + * Return the set of active Sessions associated with this Manager. + * If this Manager has no active Sessions, a zero-length array is returned. + * + * @return All the currently active sessions managed by this manager + */ + Session[] findSessions(); + + + /** + * Load any currently active sessions that were previously unloaded + * to the appropriate persistence mechanism, if any. If persistence is not + * supported, this method returns without doing anything. + * + * @exception ClassNotFoundException if a serialized class cannot be + * found during the reload + * @exception IOException if an input/output error occurs + */ + void load() throws ClassNotFoundException, IOException; + + + /** + * Remove this Session from the active Sessions for this Manager. + * + * @param session Session to be removed + */ + void remove(Session session); + + + /** + * Remove this Session from the active Sessions for this Manager. + * + * @param session Session to be removed + * @param update Should the expiration statistics be updated + */ + void remove(Session session, boolean update); + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + void removePropertyChangeListener(PropertyChangeListener listener); + + + /** + * Save any currently active sessions in the appropriate persistence + * mechanism, if any. If persistence is not supported, this method + * returns without doing anything. + * + * @exception IOException if an input/output error occurs + */ + void unload() throws IOException; + + + /** + * This method will be invoked by the context/container on a periodic + * basis and allows the manager to implement + * a method that executes periodic tasks, such as expiring sessions etc. + */ + void backgroundProcess(); + + + /** + * Would the Manager distribute the given session attribute? Manager + * implementations may provide additional configuration options to control + * which attributes are distributable. + * + * @param name The attribute name + * @param value The attribute value + * + * @return {@code true} if the Manager would distribute the given attribute + * otherwise {@code false} + */ + boolean willAttributeDistribute(String name, Object value); + + + /** + * When an attribute that is already present in the session is added again + * under the same name and the attribute implements {@link + * jakarta.servlet.http.HttpSessionBindingListener}, should + * {@link jakarta.servlet.http.HttpSessionBindingListener#valueUnbound(jakarta.servlet.http.HttpSessionBindingEvent)} + * be called followed by + * {@link jakarta.servlet.http.HttpSessionBindingListener#valueBound(jakarta.servlet.http.HttpSessionBindingEvent)}? + *

+ * The default value is {@code false}. + * + * @return {@code true} if the listener will be notified, {@code false} if + * it will not + */ + default boolean getNotifyBindingListenerOnUnchangedValue() { + return false; + } + + + /** + * Configure if + * {@link jakarta.servlet.http.HttpSessionBindingListener#valueUnbound(jakarta.servlet.http.HttpSessionBindingEvent)} + * be called followed by + * {@link jakarta.servlet.http.HttpSessionBindingListener#valueBound(jakarta.servlet.http.HttpSessionBindingEvent)} + * when an attribute that is already present in the session is added again + * under the same name and the attribute implements {@link + * jakarta.servlet.http.HttpSessionBindingListener}. + * + * @param notifyBindingListenerOnUnchangedValue {@code true} the listener + * will be called, {@code + * false} it will not + */ + void setNotifyBindingListenerOnUnchangedValue( + boolean notifyBindingListenerOnUnchangedValue); + + + /** + * When an attribute that is already present in the session is added again + * under the same name and a {@link + * jakarta.servlet.http.HttpSessionAttributeListener} is configured for the + * session should + * {@link jakarta.servlet.http.HttpSessionAttributeListener#attributeReplaced(jakarta.servlet.http.HttpSessionBindingEvent)} + * be called? + *

+ * The default value is {@code true}. + * + * @return {@code true} if the listener will be notified, {@code false} if + * it will not + */ + default boolean getNotifyAttributeListenerOnUnchangedValue() { + return true; + } + + + /** + * Configure if + * {@link jakarta.servlet.http.HttpSessionAttributeListener#attributeReplaced(jakarta.servlet.http.HttpSessionBindingEvent)} + * when an attribute that is already present in the session is added again + * under the same name and a {@link + * jakarta.servlet.http.HttpSessionAttributeListener} is configured for the + * session. + * + * @param notifyAttributeListenerOnUnchangedValue {@code true} the listener + * will be called, {@code + * false} it will not + */ + void setNotifyAttributeListenerOnUnchangedValue( + boolean notifyAttributeListenerOnUnchangedValue); + + + /** + * If this is true, Tomcat will track the number of active + * requests for each session. When determining if a session is valid, any + * session with at least one active request will always be considered valid. + * If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + * true, the default of this setting will be true, + * else the default value will be false. + * @return the flag value + */ + default boolean getSessionActivityCheck() { + return Globals.STRICT_SERVLET_COMPLIANCE; + } + + + /** + * Configure if Tomcat will track the number of active requests for each + * session. When determining if a session is valid, any session with at + * least one active request will always be considered valid. + * @param sessionActivityCheck the new flag value + */ + void setSessionActivityCheck(boolean sessionActivityCheck); + + + /** + * If this is true, the last accessed time for sessions will + * be calculated from the beginning of the previous request. If + * false, the last accessed time for sessions will be calculated + * from the end of the previous request. This also affects how the idle time + * is calculated. + * If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + * true, the default of this setting will be true, + * else the default value will be false. + * @return the flag value + */ + default boolean getSessionLastAccessAtStart() { + return Globals.STRICT_SERVLET_COMPLIANCE; + } + + + /** + * Configure if the last accessed time for sessions will + * be calculated from the beginning of the previous request. If + * false, the last accessed time for sessions will be calculated + * from the end of the previous request. This also affects how the idle time + * is calculated. + * @param sessionLastAccessAtStart the new flag value + */ + void setSessionLastAccessAtStart(boolean sessionLastAccessAtStart); + +} diff --git a/java/org/apache/catalina/Pipeline.java b/java/org/apache/catalina/Pipeline.java new file mode 100644 index 0000000..67ce954 --- /dev/null +++ b/java/org/apache/catalina/Pipeline.java @@ -0,0 +1,135 @@ +/* + * 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.catalina; + +import java.util.Set; + +/** + *

Interface describing a collection of Valves that should be executed + * in sequence when the invoke() method is invoked. It is + * required that a Valve somewhere in the pipeline (usually the last one) + * must process the request and create the corresponding response, rather + * than trying to pass the request on.

+ * + *

There is generally a single Pipeline instance associated with each + * Container. The container's normal request processing functionality is + * generally encapsulated in a container-specific Valve, which should always + * be executed at the end of a pipeline. To facilitate this, the + * setBasic() method is provided to set the Valve instance that + * will always be executed last. Other Valves will be executed in the order + * that they were added, before the basic Valve is executed.

+ * + * @author Craig R. McClanahan + * @author Peter Donald + */ +public interface Pipeline extends Contained { + + /** + * @return the Valve instance that has been distinguished as the basic + * Valve for this Pipeline (if any). + */ + Valve getBasic(); + + + /** + *

Set the Valve instance that has been distinguished as the basic + * Valve for this Pipeline (if any). Prior to setting the basic Valve, + * the Valve's setContainer() will be called, if it + * implements Contained, with the owning Container as an + * argument. The method may throw an IllegalArgumentException + * if this Valve chooses not to be associated with this Container, or + * IllegalStateException if it is already associated with + * a different Container.

+ * + * @param valve Valve to be distinguished as the basic Valve + */ + void setBasic(Valve valve); + + + /** + *

Add a new Valve to the end of the pipeline associated with this + * Container. Prior to adding the Valve, the Valve's + * setContainer() method will be called, if it implements + * Contained, with the owning Container as an argument. + * The method may throw an + * IllegalArgumentException if this Valve chooses not to + * be associated with this Container, or IllegalStateException + * if it is already associated with a different Container.

+ * + *

Implementation note: Implementations are expected to trigger the + * {@link Container#ADD_VALVE_EVENT} for the associated container if this + * call is successful.

+ * + * @param valve Valve to be added + * + * @exception IllegalArgumentException if this Container refused to + * accept the specified Valve + * @exception IllegalArgumentException if the specified Valve refuses to be + * associated with this Container + * @exception IllegalStateException if the specified Valve is already + * associated with a different Container + */ + void addValve(Valve valve); + + + /** + * @return the set of Valves in the pipeline associated with this + * Container, including the basic Valve (if any). If there are no + * such Valves, a zero-length array is returned. + */ + Valve[] getValves(); + + + /** + * Remove the specified Valve from the pipeline associated with this + * Container, if it is found; otherwise, do nothing. If the Valve is + * found and removed, the Valve's setContainer(null) method + * will be called if it implements Contained. + * + *

Implementation note: Implementations are expected to trigger the + * {@link Container#REMOVE_VALVE_EVENT} for the associated container if this + * call is successful.

+ * + * @param valve Valve to be removed + */ + void removeValve(Valve valve); + + + /** + * @return the Valve instance that has been distinguished as the basic + * Valve for this Pipeline (if any). + */ + Valve getFirst(); + + + /** + * Returns true if all the valves in this pipeline support async, false otherwise + * @return true if all the valves in this pipeline support async, false otherwise + */ + boolean isAsyncSupported(); + + + /** + * Identifies the Valves, if any, in this Pipeline that do not support + * async. + * + * @param result The Set to which the fully qualified class names of each + * Valve in this Pipeline that does not support async will be + * added + */ + void findNonAsyncValves(Set result); +} diff --git a/java/org/apache/catalina/Realm.java b/java/org/apache/catalina/Realm.java new file mode 100644 index 0000000..e818029 --- /dev/null +++ b/java/org/apache/catalina/Realm.java @@ -0,0 +1,276 @@ +/* + * 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.catalina; + +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.security.Principal; +import java.security.cert.X509Certificate; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSName; + +/** + * A Realm is a read-only facade for an underlying security realm + * used to authenticate individual users, and identify the security roles + * associated with those users. Realms can be attached at any Container + * level, but will typically only be attached to a Context, or higher level, + * Container. + * + * @author Craig R. McClanahan + */ +public interface Realm extends Contained { + + /** + * @return the CredentialHandler configured for this Realm. + */ + CredentialHandler getCredentialHandler(); + + + /** + * Set the CredentialHandler to be used by this Realm. + * + * @param credentialHandler the {@link CredentialHandler} to use + */ + void setCredentialHandler(CredentialHandler credentialHandler); + + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + void addPropertyChangeListener(PropertyChangeListener listener); + + + /** + * Try to authenticate with the specified username. + * + * @param username Username of the Principal to look up + * + * @return the associated principal, or {@code null} if none is associated. + */ + Principal authenticate(String username); + + + /** + * Try to authenticate using the specified username and + * credentials. + * + * @param username Username of the Principal to look up + * @param credentials Password or other credentials to use in + * authenticating this username + * + * @return the associated principal, or {@code null} if there is none + */ + Principal authenticate(String username, String credentials); + + + /** + * Try to authenticate with the specified username, which + * matches the digest calculated using the given parameters using the + * method described in RFC 2617 (which is a superset of RFC 2069). + * + * @param username Username of the Principal to look up + * @param digest Digest which has been submitted by the client + * @param nonce Unique (or supposedly unique) token which has been used + * for this request + * @param nc the nonce counter + * @param cnonce the client chosen nonce + * @param qop the "quality of protection" ({@code nc} and {@code cnonce} + * will only be used, if {@code qop} is not {@code null}). + * @param realm Realm name + * @param digestA2 Second digest calculated as digest(Method + ":" + uri) + * + * @return the associated principal, or {@code null} if there is none. + * + * @deprecated Unused. Use {@link #authenticate(String, String, String, + * String, String, String, String, String, String)}. Will be removed in + * Tomcat 11. + */ + @Deprecated + Principal authenticate(String username, String digest, + String nonce, String nc, String cnonce, + String qop, String realm, + String digestA2); + + + /** + * Try to authenticate with the specified username, which + * matches the digest calculated using the given parameters using the + * method described in RFC 7616. + *

+ * The default implementation calls {@link #authenticate(String, String, + * String, String, String, String, String, String)} for backwards + * compatibility which effectively forces the use of MD5 regardless of the + * algorithm specified in the call to this method. + *

+ * Implementations are expected to override the default implementation and + * take account of the algorithm parameter. + * + * @param username Username of the Principal to look up + * @param digest Digest which has been submitted by the client + * @param nonce Unique (or supposedly unique) token which has been used + * for this request + * @param nc the nonce counter + * @param cnonce the client chosen nonce + * @param qop the "quality of protection" ({@code nc} and {@code cnonce} + * will only be used, if {@code qop} is not {@code null}). + * @param realm Realm name + * @param digestA2 Second digest calculated as digest(Method + ":" + uri) + * @param algorithm The message digest algorithm to use + * + * @return the associated principal, or {@code null} if there is none. + */ + default Principal authenticate(String username, String digest, + String nonce, String nc, String cnonce, + String qop, String realm, + String digestA2, String algorithm) { + return authenticate(username, digest, nonce, nc, cnonce, qop, realm, digestA2); + } + + + /** + * Try to authenticate using a {@link GSSContext}. + * + * @param gssContext The gssContext processed by the {@link Authenticator}. + * @param storeCreds Should the realm attempt to store the delegated + * credentials in the returned Principal? + * @return the associated principal, or {@code null} if there is none + */ + Principal authenticate(GSSContext gssContext, boolean storeCreds); + + + /** + * Try to authenticate using a {@link GSSName}. + * + * @param gssName The {@link GSSName} of the principal to look up + * @param gssCredential The {@link GSSCredential} of the principal, may be + * {@code null} + * @return the associated principal, or {@code null} if there is none + */ + Principal authenticate(GSSName gssName, GSSCredential gssCredential); + + + /** + * Try to authenticate using a chain of {@link X509Certificate}s. + * + * @param certs Array of client certificates, with the first one in + * the array being the certificate of the client itself. + * + * @return the associated principal, or {@code null} if there is none + */ + Principal authenticate(X509Certificate certs[]); + + + /** + * Execute a periodic task, such as reloading, etc. This method will be + * invoked inside the classloading context of this container. Unexpected + * throwables will be caught and logged. + */ + void backgroundProcess(); + + + /** + * Find the SecurityConstraints configured to guard the request URI for + * this request. + * + * @param request Request we are processing + * @param context Context the Request is mapped to + * + * @return the configured {@link SecurityConstraint}, or {@code null} if + * there is none + */ + SecurityConstraint [] findSecurityConstraints(Request request, + Context context); + + + /** + * Perform access control based on the specified authorization constraint. + * + * @param request Request we are processing + * @param response Response we are creating + * @param constraint Security constraint we are enforcing + * @param context The Context to which client of this class is attached. + * + * @return {@code true} if this constraint is satisfied and processing + * should continue, or {@code false} otherwise + * + * @exception IOException if an input/output error occurs + */ + boolean hasResourcePermission(Request request, + Response response, + SecurityConstraint [] constraint, + Context context) + throws IOException; + + + /** + * Check if the specified Principal has the specified + * security role, within the context of this Realm. + * + * @param wrapper wrapper context for evaluating role + * @param principal Principal for whom the role is to be checked + * @param role Security role to be checked + * + * @return {@code true} if the specified Principal has the specified + * security role, within the context of this Realm; otherwise return + * {@code false}. + */ + boolean hasRole(Wrapper wrapper, Principal principal, String role); + + + /** + * Enforce any user data constraint required by the security constraint + * guarding this request URI. + * + * @param request Request we are processing + * @param response Response we are creating + * @param constraint Security constraint being checked + * + * @return {@code true} if this constraint + * was not violated and processing should continue, or {@code false} + * if we have created a response already. + * + * @exception IOException if an input/output error occurs + */ + boolean hasUserDataPermission(Request request, + Response response, + SecurityConstraint []constraint) + throws IOException; + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + void removePropertyChangeListener(PropertyChangeListener listener); + + + /** + * Return the availability of the realm for authentication. + * @return {@code true} if the realm is able to perform authentication + */ + default boolean isAvailable() { + return true; + } +} diff --git a/java/org/apache/catalina/Role.java b/java/org/apache/catalina/Role.java new file mode 100644 index 0000000..18fea5e --- /dev/null +++ b/java/org/apache/catalina/Role.java @@ -0,0 +1,72 @@ +/* + * 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.catalina; + + +import java.security.Principal; + + +/** + *

Abstract representation of a security role, suitable for use in + * environments like JAAS that want to deal with Principals.

+ * + * @author Craig R. McClanahan + * @since 4.1 + */ +public interface Role extends Principal { + + + // ------------------------------------------------------------- Properties + + + /** + * @return the description of this role. + */ + String getDescription(); + + + /** + * Set the description of this role. + * + * @param description The new description + */ + void setDescription(String description); + + + /** + * @return the role name of this role, which must be unique + * within the scope of a {@link UserDatabase}. + */ + String getRolename(); + + + /** + * Set the role name of this role, which must be unique + * within the scope of a {@link UserDatabase}. + * + * @param rolename The new role name + */ + void setRolename(String rolename); + + + /** + * @return the {@link UserDatabase} within which this Role is defined. + */ + UserDatabase getUserDatabase(); + + +} diff --git a/java/org/apache/catalina/Server.java b/java/org/apache/catalina/Server.java new file mode 100644 index 0000000..eff7279 --- /dev/null +++ b/java/org/apache/catalina/Server.java @@ -0,0 +1,268 @@ +/* + * 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.catalina; + +import java.io.File; +import java.util.concurrent.ScheduledExecutorService; + +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.catalina.startup.Catalina; + +/** + * A Server element represents the entire Catalina + * servlet container. Its attributes represent the characteristics of + * the servlet container as a whole. A Server may contain + * one or more Services, and the top level set of naming + * resources. + *

+ * Normally, an implementation of this interface will also implement + * Lifecycle, such that when the start() and + * stop() methods are called, all of the defined + * Services are also started or stopped. + *

+ * In between, the implementation must open a server socket on the port number + * specified by the port property. When a connection is accepted, + * the first line is read and compared with the specified shutdown command. + * If the command matches, shutdown of the server is initiated. + * + * @author Craig R. McClanahan + */ +public interface Server extends Lifecycle { + + // ------------------------------------------------------------- Properties + + /** + * @return the global naming resources. + */ + NamingResourcesImpl getGlobalNamingResources(); + + + /** + * Set the global naming resources. + * + * @param globalNamingResources The new global naming resources + */ + void setGlobalNamingResources + (NamingResourcesImpl globalNamingResources); + + + /** + * @return the global naming resources context. + */ + javax.naming.Context getGlobalNamingContext(); + + + /** + * @return the port number we listen to for shutdown commands. + * + * @see #getPortOffset() + * @see #getPortWithOffset() + */ + int getPort(); + + + /** + * Set the port number we listen to for shutdown commands. + * + * @param port The new port number + * + * @see #setPortOffset(int) + */ + void setPort(int port); + + /** + * Get the number that offsets the port used for shutdown commands. + * For example, if port is 8005, and portOffset is 1000, + * the server listens at 9005. + * + * @return the port offset + */ + int getPortOffset(); + + /** + * Set the number that offsets the server port used for shutdown commands. + * For example, if port is 8005, and you set portOffset to 1000, + * connector listens at 9005. + * + * @param portOffset sets the port offset + */ + void setPortOffset(int portOffset); + + /** + * Get the actual port on which server is listening for the shutdown commands. + * If you do not set port offset, port is returned. If you set + * port offset, port offset + port is returned. + * + * @return the port with offset + */ + int getPortWithOffset(); + + /** + * @return the address on which we listen to for shutdown commands. + */ + String getAddress(); + + + /** + * Set the address on which we listen to for shutdown commands. + * + * @param address The new address + */ + void setAddress(String address); + + + /** + * @return the shutdown command string we are waiting for. + */ + String getShutdown(); + + + /** + * Set the shutdown command we are waiting for. + * + * @param shutdown The new shutdown command + */ + void setShutdown(String shutdown); + + + /** + * @return the parent class loader for this component. If not set, return + * {@link #getCatalina()} {@link Catalina#getParentClassLoader()}. If + * catalina has not been set, return the system class loader. + */ + ClassLoader getParentClassLoader(); + + + /** + * Set the parent class loader for this server. + * + * @param parent The new parent class loader + */ + void setParentClassLoader(ClassLoader parent); + + + /** + * @return the outer Catalina startup/shutdown component if present. + */ + Catalina getCatalina(); + + /** + * Set the outer Catalina startup/shutdown component if present. + * + * @param catalina the outer Catalina component + */ + void setCatalina(Catalina catalina); + + + /** + * @return the configured base (instance) directory. Note that home and base + * may be the same (and are by default). If this is not set the value + * returned by {@link #getCatalinaHome()} will be used. + */ + File getCatalinaBase(); + + /** + * Set the configured base (instance) directory. Note that home and base + * may be the same (and are by default). + * + * @param catalinaBase the configured base directory + */ + void setCatalinaBase(File catalinaBase); + + + /** + * @return the configured home (binary) directory. Note that home and base + * may be the same (and are by default). + */ + File getCatalinaHome(); + + /** + * Set the configured home (binary) directory. Note that home and base + * may be the same (and are by default). + * + * @param catalinaHome the configured home directory + */ + void setCatalinaHome(File catalinaHome); + + + /** + * Get the utility thread count. + * @return the thread count + */ + int getUtilityThreads(); + + + /** + * Set the utility thread count. + * @param utilityThreads the new thread count + */ + void setUtilityThreads(int utilityThreads); + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new Service to the set of defined Services. + * + * @param service The Service to be added + */ + void addService(Service service); + + + /** + * Wait until a proper shutdown command is received, then return. + */ + void await(); + + + /** + * Find the specified Service + * + * @param name Name of the Service to be returned + * @return the specified Service, or null if none exists. + */ + Service findService(String name); + + + /** + * @return the set of Services defined within this Server. + */ + Service[] findServices(); + + + /** + * Remove the specified Service from the set associated from this + * Server. + * + * @param service The Service to be removed + */ + void removeService(Service service); + + + /** + * @return the token necessary for operations on the associated JNDI naming + * context. + */ + Object getNamingToken(); + + /** + * @return the utility executor managed by the Service. + */ + ScheduledExecutorService getUtilityExecutor(); + +} diff --git a/java/org/apache/catalina/Service.java b/java/org/apache/catalina/Service.java new file mode 100644 index 0000000..0d7b37a --- /dev/null +++ b/java/org/apache/catalina/Service.java @@ -0,0 +1,152 @@ +/* + * 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.catalina; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.mapper.Mapper; + +/** + * A Service is a group of one or more + * Connectors that share a single Container + * to process their incoming requests. This arrangement allows, for example, + * a non-SSL and SSL connector to share the same population of web apps. + *

+ * A given JVM can contain any number of Service instances; however, they are + * completely independent of each other and share only the basic JVM facilities + * and classes on the system class path. + * + * @author Craig R. McClanahan + */ +public interface Service extends Lifecycle { + + // ------------------------------------------------------------- Properties + + /** + * @return the Engine that handles requests for all + * Connectors associated with this Service. + */ + Engine getContainer(); + + /** + * Set the Engine that handles requests for all + * Connectors associated with this Service. + * + * @param engine The new Engine + */ + void setContainer(Engine engine); + + /** + * @return the name of this Service. + */ + String getName(); + + /** + * Set the name of this Service. + * + * @param name The new service name + */ + void setName(String name); + + /** + * @return the Server with which we are associated (if any). + */ + Server getServer(); + + /** + * Set the Server with which we are associated (if any). + * + * @param server The server that owns this Service + */ + void setServer(Server server); + + /** + * @return the parent class loader for this component. If not set, return + * {@link #getServer()} {@link Server#getParentClassLoader()}. If no server + * has been set, return the system class loader. + */ + ClassLoader getParentClassLoader(); + + /** + * Set the parent class loader for this service. + * + * @param parent The new parent class loader + */ + void setParentClassLoader(ClassLoader parent); + + /** + * @return the domain under which this container will be / has been + * registered. + */ + String getDomain(); + + + // --------------------------------------------------------- Public Methods + + /** + * Add a new Connector to the set of defined Connectors, and associate it + * with this Service's Container. + * + * @param connector The Connector to be added + */ + void addConnector(Connector connector); + + /** + * Find and return the set of Connectors associated with this Service. + * + * @return the set of associated Connectors + */ + Connector[] findConnectors(); + + /** + * Remove the specified Connector from the set associated from this + * Service. The removed Connector will also be disassociated from our + * Container. + * + * @param connector The Connector to be removed + */ + void removeConnector(Connector connector); + + /** + * Adds a named executor to the service + * @param ex Executor + */ + void addExecutor(Executor ex); + + /** + * Retrieves all executors + * @return Executor[] + */ + Executor[] findExecutors(); + + /** + * Retrieves executor by name, null if not found + * @param name String + * @return Executor + */ + Executor getExecutor(String name); + + /** + * Removes an executor from the service + * @param ex Executor + */ + void removeExecutor(Executor ex); + + /** + * @return the mapper associated with this Service. + */ + Mapper getMapper(); +} diff --git a/java/org/apache/catalina/Session.java b/java/org/apache/catalina/Session.java new file mode 100644 index 0000000..4ec7aca --- /dev/null +++ b/java/org/apache/catalina/Session.java @@ -0,0 +1,376 @@ +/* + * 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.catalina; + + +import java.security.Principal; +import java.util.Iterator; + +import jakarta.servlet.http.HttpSession; + + +/** + * A Session is the Catalina-internal facade for an + * HttpSession that is used to maintain state information + * between requests for a particular user of a web application. + * + * @author Craig R. McClanahan + */ +public interface Session { + + + // ----------------------------------------------------- Manifest Constants + + + /** + * The SessionEvent event type when a session is created. + */ + String SESSION_CREATED_EVENT = "createSession"; + + + /** + * The SessionEvent event type when a session is destroyed. + */ + String SESSION_DESTROYED_EVENT = "destroySession"; + + + /** + * The SessionEvent event type when a session is activated. + */ + String SESSION_ACTIVATED_EVENT = "activateSession"; + + + /** + * The SessionEvent event type when a session is passivated. + */ + String SESSION_PASSIVATED_EVENT = "passivateSession"; + + + // ------------------------------------------------------------- Properties + + + /** + * @return the authentication type used to authenticate our cached + * Principal, if any. + */ + String getAuthType(); + + + /** + * Set the authentication type used to authenticate our cached + * Principal, if any. + * + * @param authType The new cached authentication type + */ + void setAuthType(String authType); + + + /** + * @return the creation time for this session. + */ + long getCreationTime(); + + + /** + * @return the creation time for this session, bypassing the session validity + * checks. + */ + long getCreationTimeInternal(); + + + /** + * Set the creation time for this session. This method is called by the + * Manager when an existing Session instance is reused. + * + * @param time The new creation time + */ + void setCreationTime(long time); + + + /** + * @return the session identifier for this session. + */ + String getId(); + + + /** + * @return the session identifier for this session. + */ + String getIdInternal(); + + + /** + * Set the session identifier for this session and notifies any associated + * listeners that a new session has been created. + * + * @param id The new session identifier + */ + void setId(String id); + + + /** + * Set the session identifier for this session and optionally notifies any + * associated listeners that a new session has been created. + * + * @param id The new session identifier + * @param notify Should any associated listeners be notified that a new + * session has been created? + */ + void setId(String id, boolean notify); + + + /** + * @return the last time the client sent a request associated with this + * session, as the number of milliseconds since midnight, January 1, 1970 + * GMT. Actions that your application takes, such as getting or setting + * a value associated with the session, do not affect the access time. + * This one gets updated whenever a request starts. + */ + long getThisAccessedTime(); + + /** + * @return the last client access time without invalidation check + * @see #getThisAccessedTime() + */ + long getThisAccessedTimeInternal(); + + /** + * @return the last time the client sent a request associated with this + * session, as the number of milliseconds since midnight, January 1, 1970 + * GMT. Actions that your application takes, such as getting or setting + * a value associated with the session, do not affect the access time. + * This one gets updated whenever a request finishes. + */ + long getLastAccessedTime(); + + /** + * @return the last client access time without invalidation check + * @see #getLastAccessedTime() + */ + long getLastAccessedTimeInternal(); + + /** + * @return the idle time (in milliseconds) from last client access time. + */ + long getIdleTime(); + + /** + * @return the idle time from last client access time without invalidation check + * @see #getIdleTime() + */ + long getIdleTimeInternal(); + + /** + * @return the Manager within which this Session is valid. + */ + Manager getManager(); + + + /** + * Set the Manager within which this Session is valid. + * + * @param manager The new Manager + */ + void setManager(Manager manager); + + + /** + * @return the maximum time interval, in seconds, between client requests + * before the servlet container will invalidate the session. A negative + * time indicates that the session should never time out. + */ + int getMaxInactiveInterval(); + + + /** + * Set the maximum time interval, in seconds, between client requests + * before the servlet container will invalidate the session. A negative + * time indicates that the session should never time out. + * + * @param interval The new maximum interval + */ + void setMaxInactiveInterval(int interval); + + + /** + * Set the isNew flag for this session. + * + * @param isNew The new value for the isNew flag + */ + void setNew(boolean isNew); + + + /** + * @return the authenticated Principal that is associated with this Session. + * This provides an Authenticator with a means to cache a + * previously authenticated Principal, and avoid potentially expensive + * Realm.authenticate() calls on every request. If there + * is no current associated Principal, return null. + */ + Principal getPrincipal(); + + + /** + * Set the authenticated Principal that is associated with this Session. + * This provides an Authenticator with a means to cache a + * previously authenticated Principal, and avoid potentially expensive + * Realm.authenticate() calls on every request. + * + * @param principal The new Principal, or null if none + */ + void setPrincipal(Principal principal); + + + /** + * @return the HttpSession for which this object + * is the facade. + */ + HttpSession getSession(); + + + /** + * Set the isValid flag for this session. + * + * @param isValid The new value for the isValid flag + */ + void setValid(boolean isValid); + + + /** + * @return true if the session is still valid + */ + boolean isValid(); + + + // --------------------------------------------------------- Public Methods + + + /** + * Update the accessed time information for this session. This method + * should be called by the context when a request comes in for a particular + * session, even if the application does not reference it. + */ + void access(); + + + /** + * Add a session event listener to this component. + * + * @param listener the SessionListener instance that should be notified + * for session events + */ + void addSessionListener(SessionListener listener); + + + /** + * End access to the session. + */ + void endAccess(); + + + /** + * Perform the internal processing required to invalidate this session, + * without triggering an exception if the session has already expired. + */ + void expire(); + + + /** + * @return the object bound with the specified name to the internal notes + * for this session, or null if no such binding exists. + * + * @param name Name of the note to be returned + */ + Object getNote(String name); + + + /** + * @return an Iterator containing the String names of all notes bindings + * that exist for this session. + */ + Iterator getNoteNames(); + + + /** + * Release all object references, and initialize instance variables, in + * preparation for reuse of this object. + */ + void recycle(); + + + /** + * Remove any object bound to the specified name in the internal notes + * for this session. + * + * @param name Name of the note to be removed + */ + void removeNote(String name); + + + /** + * Remove a session event listener from this component. + * + * @param listener remove the session listener, which will no longer be + * notified + */ + void removeSessionListener(SessionListener listener); + + + /** + * Bind an object to a specified name in the internal notes associated + * with this session, replacing any existing binding for this name. + * + * @param name Name to which the object should be bound + * @param value Object to be bound to the specified name + */ + void setNote(String name, Object value); + + + /** + * Inform the listeners about the change session ID. + * + * @param newId new session ID + * @param oldId old session ID + * @param notifySessionListeners Should any associated sessionListeners be + * notified that session ID has been changed? + * @param notifyContainerListeners Should any associated ContainerListeners + * be notified that session ID has been changed? + */ + void tellChangedSessionId(String newId, String oldId, + boolean notifySessionListeners, boolean notifyContainerListeners); + + + /** + * Does the session implementation support the distributing of the given + * attribute? If the Manager is marked as distributable, then this method + * must be used to check attributes before adding them to a session and + * an {@link IllegalArgumentException} thrown if the proposed attribute is + * not distributable. + *

+ * Note that the {@link Manager} implementation may further restrict which + * attributes are distributed but a {@link Manager} level restriction should + * not trigger an {@link IllegalArgumentException} in + * {@link HttpSession#setAttribute(String, Object)} + * + * @param name The attribute name + * @param value The attribute value + * + * @return {@code true} if distribution is supported, otherwise {@code + * false} + */ + boolean isAttributeDistributable(String name, Object value); +} diff --git a/java/org/apache/catalina/SessionEvent.java b/java/org/apache/catalina/SessionEvent.java new file mode 100644 index 0000000..e92ff38 --- /dev/null +++ b/java/org/apache/catalina/SessionEvent.java @@ -0,0 +1,98 @@ +/* + * 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.catalina; + + +import java.util.EventObject; + + +/** + * General event for notifying listeners of significant changes on a Session. + * + * @author Craig R. McClanahan + */ +public final class SessionEvent extends EventObject { + + private static final long serialVersionUID = 1L; + + + /** + * The event data associated with this event. + */ + private final Object data; + + + /** + * The Session on which this event occurred. + */ + private final Session session; + + + /** + * The event type this instance represents. + */ + private final String type; + + + /** + * Construct a new SessionEvent with the specified parameters. + * + * @param session Session on which this event occurred + * @param type Event type + * @param data Event data + */ + public SessionEvent(Session session, String type, Object data) { + + super(session); + this.session = session; + this.type = type; + this.data = data; + + } + + + /** + * @return the event data of this event. + */ + public Object getData() { + return this.data; + } + + + /** + * @return the Session on which this event occurred. + */ + public Session getSession() { + return this.session; + } + + + /** + * @return the event type of this event. + */ + public String getType() { + return this.type; + } + + + @Override + public String toString() { + return "SessionEvent['" + getSession() + "','" + getType() + "']"; + } + + +} diff --git a/java/org/apache/catalina/SessionIdGenerator.java b/java/org/apache/catalina/SessionIdGenerator.java new file mode 100644 index 0000000..09a08df --- /dev/null +++ b/java/org/apache/catalina/SessionIdGenerator.java @@ -0,0 +1,61 @@ +/* + * 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.catalina; + +public interface SessionIdGenerator { + + /** + * @return the node identifier associated with this node which will be + * included in the generated session ID. + */ + String getJvmRoute(); + + /** + * Specify the node identifier associated with this node which will be + * included in the generated session ID. + * + * @param jvmRoute The node identifier + */ + void setJvmRoute(String jvmRoute); + + /** + * @return the number of bytes for a session ID + */ + int getSessionIdLength(); + + /** + * Specify the number of bytes for a session ID + * + * @param sessionIdLength Number of bytes + */ + void setSessionIdLength(int sessionIdLength); + + /** + * Generate and return a new session identifier. + * + * @return the newly generated session id + */ + String generateSessionId(); + + /** + * Generate and return a new session identifier. + * + * @param route node identifier to include in generated id + * @return the newly generated session id + */ + String generateSessionId(String route); +} diff --git a/java/org/apache/catalina/SessionListener.java b/java/org/apache/catalina/SessionListener.java new file mode 100644 index 0000000..47516ab --- /dev/null +++ b/java/org/apache/catalina/SessionListener.java @@ -0,0 +1,38 @@ +/* + * 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.catalina; + +import java.util.EventListener; + + +/** + * Interface defining a listener for significant Session generated events. + * + * @author Craig R. McClanahan + */ +public interface SessionListener extends EventListener { + + + /** + * Acknowledge the occurrence of the specified event. + * + * @param event SessionEvent that has occurred + */ + void sessionEvent(SessionEvent event); + + +} diff --git a/java/org/apache/catalina/Store.java b/java/org/apache/catalina/Store.java new file mode 100644 index 0000000..9512121 --- /dev/null +++ b/java/org/apache/catalina/Store.java @@ -0,0 +1,134 @@ +/* + * 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.catalina; + + +import java.beans.PropertyChangeListener; +import java.io.IOException; + + +/** + * A Store is the abstraction of a Catalina component that provides + * persistent storage and loading of Sessions and their associated user data. + * Implementations are free to save and load the Sessions to any media they + * wish, but it is assumed that saved Sessions are persistent across + * server or context restarts. + * + * @author Craig R. McClanahan + */ +public interface Store { + + // ------------------------------------------------------------- Properties + + /** + * @return the Manager instance associated with this Store. + */ + Manager getManager(); + + + /** + * Set the Manager associated with this Store. + * + * @param manager The Manager which will use this Store. + */ + void setManager(Manager manager); + + + /** + * @return the number of Sessions present in this Store. + * + * @exception IOException if an input/output error occurs + */ + int getSize() throws IOException; + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + void addPropertyChangeListener(PropertyChangeListener listener); + + + /** + * @return an array containing the session identifiers of all Sessions + * currently saved in this Store. If there are no such Sessions, a + * zero-length array is returned. + * + * @exception IOException if an input/output error occurred + */ + String[] keys() throws IOException; + + + /** + * Load and return the Session associated with the specified session + * identifier from this Store, without removing it. If there is no + * such stored Session, return null. + * + * @param id Session identifier of the session to load + * + * @exception ClassNotFoundException if a deserialization error occurs + * @exception IOException if an input/output error occurs + * @return the loaded Session instance + */ + Session load(String id) + throws ClassNotFoundException, IOException; + + + /** + * Remove the Session with the specified session identifier from + * this Store, if present. If no such Session is present, this method + * takes no action. + * + * @param id Session identifier of the Session to be removed + * + * @exception IOException if an input/output error occurs + */ + void remove(String id) throws IOException; + + + /** + * Remove all Sessions from this Store. + * + * @exception IOException if an input/output error occurs + */ + void clear() throws IOException; + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + void removePropertyChangeListener(PropertyChangeListener listener); + + + /** + * Save the specified Session into this Store. Any previously saved + * information for the associated session identifier is replaced. + * + * @param session Session to be saved + * + * @exception IOException if an input/output error occurs + */ + void save(Session session) throws IOException; + + +} diff --git a/java/org/apache/catalina/StoreManager.java b/java/org/apache/catalina/StoreManager.java new file mode 100644 index 0000000..fbc05b8 --- /dev/null +++ b/java/org/apache/catalina/StoreManager.java @@ -0,0 +1,38 @@ +/* + * 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.catalina; + +/** + * PersistentManager would have been a better name but that would have clashed + * with the implementation name. + */ +public interface StoreManager extends DistributedManager { + + /** + * @return the Store object which manages persistent Session + * storage for this Manager. + */ + Store getStore(); + + /** + * Remove this Session from the active Sessions for this Manager, + * but not from the Store. (Used by the PersistentValve) + * + * @param session Session to be removed + */ + void removeSuper(Session session); +} diff --git a/java/org/apache/catalina/ThreadBindingListener.java b/java/org/apache/catalina/ThreadBindingListener.java new file mode 100644 index 0000000..f901e53 --- /dev/null +++ b/java/org/apache/catalina/ThreadBindingListener.java @@ -0,0 +1,29 @@ +/* + * 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.catalina; + + +/** + * Callback for establishing naming association when entering the application + * scope. This corresponds to setting the context classloader. + */ +public interface ThreadBindingListener { + + void bind(); + void unbind(); + +} diff --git a/java/org/apache/catalina/TomcatPrincipal.java b/java/org/apache/catalina/TomcatPrincipal.java new file mode 100644 index 0000000..3d69f0b --- /dev/null +++ b/java/org/apache/catalina/TomcatPrincipal.java @@ -0,0 +1,98 @@ +/* + * 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.catalina; + +import java.security.Principal; +import java.util.Enumeration; + +import org.ietf.jgss.GSSCredential; + +/** + * Defines additional methods implemented by {@link Principal}s created by + * Tomcat's standard {@link Realm} implementations. + */ +public interface TomcatPrincipal extends Principal { + + /** + * @return The authenticated Principal to be exposed to applications. + */ + Principal getUserPrincipal(); + + /** + * @return The user's delegated credentials. + */ + GSSCredential getGssCredential(); + + /** + * Calls logout, if necessary, on any associated JAASLoginContext and/or + * GSSContext. May in the future be extended to cover other logout + * requirements. + * + * @throws Exception If something goes wrong with the logout. Uses Exception + * to allow for future expansion of this method to cover + * other logout mechanisms that might throw a different + * exception to LoginContext + */ + void logout() throws Exception; + + /** + * Returns the value of the named attribute as an Object, or + * null if no attribute of the given name exists, or if + * null has been specified as the attribute's name. + *

+ * Only the servlet container may set attributes to make available custom + * information about a Principal or the user it represents. + *

+ * The purpose of the method is to implement read only access to attributes + * which may be stored in the Realm implementation's backend + * due to its inherent design. + *

+ * As using this method from application code will make it non portable to + * other EE compliant containers, it is advised this should never be used + * as an object storage facility tied to the Principal, but + * rather as simple extra additional metadata. It is recommended that a + * container level object is used to further process the attributes that + * may be associated with the Principal. + *

+ * Realm implementations that are provided by Tomcat will + * not provide complex type mapping, but will in most cases always + * return a result as a String object which may need custom + * decoding. + *

+ * Realm implementations that are provided by Tomcat will + * not provide an implementation for this facility unless it is inherent + * to the storage backend of the Realm itself and metadata + * is available without additional user intervention or configuration. + * + * @param name a String specifying the name of the attribute + * @return an Object containing the value of the attribute, or + * null if the attribute does not exist, or if + * null has been specified as the attribute's name + */ + Object getAttribute(String name); + + /** + * Returns an Enumeration containing the names of the + * attributes available to this Principal. This method returns an empty + * Enumeration if the Principal has no attributes available to + * it. + * + * @return an Enumeration of strings containing the names of + * the Principal's attributes + */ + Enumeration getAttributeNames(); +} diff --git a/java/org/apache/catalina/TrackedWebResource.java b/java/org/apache/catalina/TrackedWebResource.java new file mode 100644 index 0000000..b6576e4 --- /dev/null +++ b/java/org/apache/catalina/TrackedWebResource.java @@ -0,0 +1,24 @@ +/* + * 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.catalina; + +import java.io.Closeable; + +public interface TrackedWebResource extends Closeable { + Exception getCreatedBy(); + String getName(); +} diff --git a/java/org/apache/catalina/User.java b/java/org/apache/catalina/User.java new file mode 100644 index 0000000..eb51a1b --- /dev/null +++ b/java/org/apache/catalina/User.java @@ -0,0 +1,172 @@ +/* + * 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.catalina; + + +import java.security.Principal; +import java.util.Iterator; + + +/** + * Abstract representation of a user in a {@link UserDatabase}. Each user is + * optionally associated with a set of {@link Group}s through which they inherit + * additional security roles, and is optionally assigned a set of specific + * {@link Role}s. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public interface User extends Principal { + + + // ------------------------------------------------------------- Properties + + + /** + * @return the full name of this user. + */ + String getFullName(); + + + /** + * Set the full name of this user. + * + * @param fullName The new full name + */ + void setFullName(String fullName); + + + /** + * @return the set of {@link Group}s to which this user belongs. + */ + Iterator getGroups(); + + + /** + * @return the logon password of this user, optionally prefixed with the + * identifier of an encoding scheme surrounded by curly braces, such as + * {md5}xxxxx. + */ + String getPassword(); + + + /** + * Set the logon password of this user, optionally prefixed with the + * identifier of an encoding scheme surrounded by curly braces, such as + * {md5}xxxxx. + * + * @param password The new logon password + */ + void setPassword(String password); + + + /** + * @return the set of {@link Role}s assigned specifically to this user. + */ + Iterator getRoles(); + + + /** + * @return the {@link UserDatabase} within which this User is defined. + */ + UserDatabase getUserDatabase(); + + + /** + * @return the logon username of this user, which must be unique + * within the scope of a {@link UserDatabase}. + */ + String getUsername(); + + + /** + * Set the logon username of this user, which must be unique within + * the scope of a {@link UserDatabase}. + * + * @param username The new logon username + */ + void setUsername(String username); + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new {@link Group} to those this user belongs to. + * + * @param group The new group + */ + void addGroup(Group group); + + + /** + * Add a {@link Role} to those assigned specifically to this user. + * + * @param role The new role + */ + void addRole(Role role); + + + /** + * Is this user in the specified {@link Group}? + * + * @param group The group to check + * @return true if the user is in the specified group + */ + boolean isInGroup(Group group); + + + /** + * Is this user specifically assigned the specified {@link Role}? This + * method does NOT check for roles inherited based on + * {@link Group} membership. + * + * @param role The role to check + * @return true if the user has the specified role + */ + boolean isInRole(Role role); + + + /** + * Remove a {@link Group} from those this user belongs to. + * + * @param group The old group + */ + void removeGroup(Group group); + + + /** + * Remove all {@link Group}s from those this user belongs to. + */ + void removeGroups(); + + + /** + * Remove a {@link Role} from those assigned to this user. + * + * @param role The old role + */ + void removeRole(Role role); + + + /** + * Remove all {@link Role}s from those assigned to this user. + */ + void removeRoles(); + + +} diff --git a/java/org/apache/catalina/UserDatabase.java b/java/org/apache/catalina/UserDatabase.java new file mode 100644 index 0000000..e07b90b --- /dev/null +++ b/java/org/apache/catalina/UserDatabase.java @@ -0,0 +1,223 @@ +/* + * 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.catalina; + +import java.util.Iterator; + +/** + * Abstract representation of a database of {@link User}s and {@link Group}s + * that can be maintained by an application, along with definitions of + * corresponding {@link Role}s, and referenced by a {@link Realm} for + * authentication and access control. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public interface UserDatabase { + + // ------------------------------------------------------------- Properties + + /** + * @return the set of {@link Group}s defined in this user database. + */ + Iterator getGroups(); + + + /** + * @return the unique global identifier of this user database. + */ + String getId(); + + + /** + * @return the set of {@link Role}s defined in this user database. + */ + Iterator getRoles(); + + + /** + * @return the set of {@link User}s defined in this user database. + */ + Iterator getUsers(); + + + // --------------------------------------------------------- Public Methods + + /** + * Finalize access to this user database. + * + * @exception Exception if any exception is thrown during closing + */ + void close() throws Exception; + + + /** + * Create and return a new {@link Group} defined in this user database. + * + * @param groupname The group name of the new group (must be unique) + * @param description The description of this group + * @return The new group + */ + Group createGroup(String groupname, String description); + + + /** + * Create and return a new {@link Role} defined in this user database. + * + * @param rolename The role name of the new role (must be unique) + * @param description The description of this role + * @return The new role + */ + Role createRole(String rolename, String description); + + + /** + * Create and return a new {@link User} defined in this user database. + * + * @param username The logon username of the new user (must be unique) + * @param password The logon password of the new user + * @param fullName The full name of the new user + * @return The new user + */ + User createUser(String username, String password, String fullName); + + + /** + * @return the {@link Group} with the specified group name, if any; + * otherwise return null. + * + * @param groupname Name of the group to return + */ + Group findGroup(String groupname); + + + /** + * @return the {@link Role} with the specified role name, if any; otherwise + * return null. + * + * @param rolename Name of the role to return + */ + Role findRole(String rolename); + + + /** + * @return the {@link User} with the specified user name, if any; otherwise + * return null. + * + * @param username Name of the user to return + */ + User findUser(String username); + + + /** + * Initialize access to this user database. + * + * @exception Exception if any exception is thrown during opening + */ + void open() throws Exception; + + + /** + * Remove the specified {@link Group} from this user database. + * + * @param group The group to be removed + */ + void removeGroup(Group group); + + + /** + * Remove the specified {@link Role} from this user database. + * + * @param role The role to be removed + */ + void removeRole(Role role); + + + /** + * Remove the specified {@link User} from this user database. + * + * @param user The user to be removed + */ + void removeUser(User user); + + + /** + * Signal the specified {@link Group} from this user database has been + * modified. + * + * @param group The group that has been modified + */ + default void modifiedGroup(Group group) {} + + + /** + * Signal the specified {@link Role} from this user database has been + * modified. + * + * @param role The role that has been modified + */ + default void modifiedRole(Role role) {} + + + /** + * Signal the specified {@link User} from this user database has been + * modified. + * + * @param user The user that has been modified + */ + default void modifiedUser(User user) {} + + + /** + * Save any updated information to the persistent storage location for this + * user database. + * + * @exception Exception if any exception is thrown during saving + */ + void save() throws Exception; + + + /** + * Perform any background processing (e.g. checking for changes in persisted + * storage) required for the user database. + */ + default void backgroundProcess() { + // NO-OP by default + } + + + /** + * Is the database available. + * + * @return true + */ + default boolean isAvailable() { + return true; + } + + + /** + * Is the database data loaded on demand. This is used to avoid eager + * loading of the full database data, for example for JMX registration of + * all objects. + * + * @return false + */ + default boolean isSparse() { + return false; + } +} diff --git a/java/org/apache/catalina/Valve.java b/java/org/apache/catalina/Valve.java new file mode 100644 index 0000000..a3704fc --- /dev/null +++ b/java/org/apache/catalina/Valve.java @@ -0,0 +1,126 @@ +/* + * 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.catalina; + +import java.io.IOException; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; + +/** + *

A Valve is a request processing component associated with a + * particular Container. A series of Valves are generally associated with + * each other into a Pipeline. The detailed contract for a Valve is included + * in the description of the invoke() method below.

+ * + * HISTORICAL NOTE: The "Valve" name was assigned to this concept + * because a valve is what you use in a real world pipeline to control and/or + * modify flows through it. + * + * @author Craig R. McClanahan + * @author Gunnar Rjnning + * @author Peter Donald + */ +public interface Valve { + + + //-------------------------------------------------------------- Properties + + /** + * Returns the next Valve in this pipeline, or null if this is + * the last Valve in the pipeline. + * + * @return the next Valve in the pipeline containing this Valve, or + * null if this is the last Valve in the pipeline. + */ + Valve getNext(); + + + /** + * Set the next Valve in the pipeline containing this Valve. + * + * @param valve The new next valve, or null if none + */ + void setNext(Valve valve); + + + //---------------------------------------------------------- Public Methods + + + /** + * Execute a periodic task, such as reloading, etc. This method will be + * invoked inside the classloading context of this container. Unexpected + * throwables will be caught and logged. + */ + void backgroundProcess(); + + + /** + *

Perform request processing as required by this Valve.

+ * + *

An individual Valve MAY perform the following actions, in + * the specified order:

+ *
    + *
  • Examine and/or modify the properties of the specified Request and + * Response. + *
  • Examine the properties of the specified Request, completely generate + * the corresponding Response, and return control to the caller. + *
  • Examine the properties of the specified Request and Response, wrap + * either or both of these objects to supplement their functionality, + * and pass them on. + *
  • If the corresponding Response was not generated (and control was not + * returned, call the next Valve in the pipeline (if there is one) by + * executing getNext().invoke(). + *
  • Examine, but not modify, the properties of the resulting Response + * (which was created by a subsequently invoked Valve or Container). + *
+ * + *

A Valve MUST NOT do any of the following things:

+ *
    + *
  • Change request properties that have already been used to direct + * the flow of processing control for this request (for instance, + * trying to change the virtual host to which a Request should be + * sent from a pipeline attached to a Host or Context in the + * standard implementation). + *
  • Create a completed Response AND pass this + * Request and Response on to the next Valve in the pipeline. + *
  • Consume bytes from the input stream associated with the Request, + * unless it is completely generating the response, or wrapping the + * request before passing it on. + *
  • Modify the HTTP headers included with the Response after the + * getNext().invoke() method has returned. + *
  • Perform any actions on the output stream associated with the + * specified Response after the getNext().invoke() method has + * returned. + *
+ * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * + * @exception IOException if an input/output error occurs, or is thrown + * by a subsequently invoked Valve, Filter, or Servlet + * @exception ServletException if a servlet error occurs, or is thrown + * by a subsequently invoked Valve, Filter, or Servlet + */ + void invoke(Request request, Response response) + throws IOException, ServletException; + + + boolean isAsyncSupported(); +} diff --git a/java/org/apache/catalina/WebResource.java b/java/org/apache/catalina/WebResource.java new file mode 100644 index 0000000..2c8b05d --- /dev/null +++ b/java/org/apache/catalina/WebResource.java @@ -0,0 +1,171 @@ +/* + * 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.catalina; + +import java.io.InputStream; +import java.net.URL; +import java.security.cert.Certificate; +import java.util.jar.Manifest; + +/** + * Represents a file or directory within a web application. It borrows heavily + * from {@link java.io.File}. + */ +public interface WebResource { + /** + * @return {@link java.io.File#lastModified()}. + */ + long getLastModified(); + + /** + * @return the last modified time of this resource in the correct format for + * the HTTP Last-Modified header as specified by RFC 2616. + */ + String getLastModifiedHttp(); + + /** + * @return {@link java.io.File#exists()}. + */ + boolean exists(); + + /** + * Indicates if this resource is required for applications to correctly scan + * the file structure but that does not exist in either the main or any + * additional {@link WebResourceSet}. For example, if an external + * directory is mapped to /WEB-INF/lib in an otherwise empty web + * application, /WEB-INF will be represented as a virtual resource. + * + * @return true for a virtual resource + */ + boolean isVirtual(); + + /** + * @return {@link java.io.File#isDirectory()}. + */ + boolean isDirectory(); + + /** + * @return {@link java.io.File#isFile()}. + */ + boolean isFile(); + + /** + * @return {@link java.io.File#delete()}. + */ + boolean delete(); + + /** + * @return {@link java.io.File#getName()}. + */ + String getName(); + + /** + * @return {@link java.io.File#length()}. + */ + long getContentLength(); + + /** + * @return {@link java.io.File#getCanonicalPath()}. + */ + String getCanonicalPath(); + + /** + * @return {@link java.io.File#canRead()}. + */ + boolean canRead(); + + /** + * @return The path of this resource relative to the web application root. If the + * resource is a directory, the return value will end in '/'. + */ + String getWebappPath(); + + /** + * Return the strong ETag if available (currently not supported) else return + * the weak ETag calculated from the content length and last modified. + * + * @return The ETag for this resource + */ + String getETag(); + + /** + * Set the MIME type for this Resource. + * + * @param mimeType The mime type that will be associated with the resource + */ + void setMimeType(String mimeType); + + /** + * @return the MIME type for this Resource. + */ + String getMimeType(); + + /** + * Obtain an InputStream based on the contents of this resource. + * + * @return An InputStream based on the contents of this resource or + * null if the resource does not exist or does not + * represent a file + */ + InputStream getInputStream(); + + /** + * @return the binary content of this resource or {@code null} if it is not + * available in a byte[] because, for example, it is too big. + */ + byte[] getContent(); + + /** + * @return The time the file was created. If not available, the result of + * {@link #getLastModified()} will be returned. + */ + long getCreation(); + + /** + * @return a URL to access the resource or null if no such URL + * is available or if the resource does not exist. + */ + URL getURL(); + + /** + * @return the code base for this resource that will be used when looking up the + * assigned permissions for the code base in the security policy file when + * running under a security manager. + */ + URL getCodeBase(); + + /** + * @return a reference to the WebResourceRoot of which this WebResource is a + * part. + */ + WebResourceRoot getWebResourceRoot(); + + /** + * @return the certificates that were used to sign this resource to verify + * it or @null if none. + * + * @see java.util.jar.JarEntry#getCertificates() + */ + Certificate[] getCertificates(); + + /** + * @return the manifest associated with this resource or @null if none. + * + * @see java.util.jar.JarFile#getManifest() + */ + Manifest getManifest(); +} diff --git a/java/org/apache/catalina/WebResourceRoot.java b/java/org/apache/catalina/WebResourceRoot.java new file mode 100644 index 0000000..36bad52 --- /dev/null +++ b/java/org/apache/catalina/WebResourceRoot.java @@ -0,0 +1,527 @@ +/* + * 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.catalina; + +import java.io.InputStream; +import java.net.URL; +import java.util.List; +import java.util.Set; + +/** + * Represents the complete set of resources for a web application. The resources + * for a web application comprise of multiple ResourceSets and when looking for + * a Resource, the ResourceSets are processed in the following order: + *
    + *
  1. Pre - Resources defined by the <PreResource> element in the web + * application's context.xml. Resources will be searched in the order + * they were specified.
  2. + *
  3. Main - The main resources for the web application - i.e. the WAR or the + * directory containing the expanded WAR
  4. + *
  5. JARs - Resource JARs as defined by the Servlet specification. JARs will + * be searched in the order they were added to the ResourceRoot.
  6. + *
  7. Post - Resources defined by the <PostResource> element in the web + * application's context.xml. Resources will be searched in the order + * they were specified.
  8. + *
+ * The following conventions should be noted: + *
    + *
  • Write operations (including delete) will only be applied to the main + * ResourceSet. The write operation will fail if the presence of a Resource + * in one of the other ResourceSets effectively makes the operation on the + * main ResourceSet a NO-OP.
  • + *
  • A file in a ResourceSet will hide a directory of the same name (and all + * the contents of that directory) in a ResourceSet that is later in the + * search order.
  • + *
  • Only the main ResourceSet may define a META-INF/context.xml since that + * file defines the Pre- and Post-Resources.
  • + *
  • As per the Servlet specification, any META-INF or WEB-INF directories in + * a resource JAR will be ignored.
  • + *
  • Pre- and Post-Resources may define WEB-INF/lib and WEB-INF/classes in + * order to make additional libraries and/or classes available to the web + * application. + *
+ * This mechanism replaces and extends the following features that were present + * in earlier versions: + *
    + *
  • Aliases - Replaced by Post-Resources with the addition of + * support for single files as well as directories + * and JARs.
  • + *
  • VirtualWebappLoader - Replaced by Pre- and Post-Resources mapped to + * WEB-INF/lib and WEB-INF/classes
  • + *
  • VirtualDirContext - Replaced by Pre- and Post-Resources
  • + *
  • External repositories - Replaced by Pre- and Post-Resources mapped to + * WEB-INF/lib and WEB-INF/classes
  • + *
  • Resource JARs - Same feature but implemented using the same + * mechanism as all the other additional + * resources.
  • + *
+ */ +/* + * A potential future enhancement is to allow writing to any ResourceSet, + * not just the main ResourceSet although that adds all sorts complications + * including: + * - which ResourceSet to write to + * - unexpected behaviour when deleting a resource from one ResourceSet since + * that may unmask a resource in a lower priority ResourceSet so what was a + * delete looks like a replace with the user having no idea where the 'new' + * resource came from + * - how to handle PUT when the target is read-only but it could be written to + * a higher priority ResourceSet that is read-write + */ +public interface WebResourceRoot extends Lifecycle { + /** + * Obtain the object that represents the resource at the given path. Note + * that the resource at that path may not exist. If the resource does not + * exist, the WebResource returned will be associated with the main + * WebResourceSet. + * + * @param path The path for the resource of interest relative to the root + * of the web application. It must start with '/'. + * + * @return The object that represents the resource at the given path + */ + WebResource getResource(String path); + + /** + * Obtain the objects that represent the resource at the given path. Note + * that the resource at that path may not exist. If the resource does not + * exist, the WebResource returned will be associated with the main + * WebResourceSet. This will include all matches even if the resource would + * not normally be accessible (e.g. because it was overridden by another + * resource) + * + * @param path The path for the resource of interest relative to the root + * of the web application. It must start with '/'. + * + * @return The objects that represents the resource at the given path + */ + WebResource[] getResources(String path); + + /** + * Obtain the object that represents the class loader resource at the given + * path. WEB-INF/classes is always searched prior to searching JAR files in + * WEB-INF/lib. The search order for JAR files will be consistent across + * subsequent calls to this method until the web application is reloaded. No + * guarantee is made as to what the search order for JAR files may be. + * + * @param path The path of the class loader resource of interest relative + * to the the root of class loader resources for this web + * application. + * + * @return The object that represents the class loader resource at the + * given path + */ + WebResource getClassLoaderResource(String path); + + /** + * Obtain the objects that represent the class loader resource at the given + * path. Note that the resource at that path may not exist. If the path does + * not exist, the WebResource returned will be associated with the main + * WebResourceSet. This will include all matches even if the resource would + * not normally be accessible (e.g. because it was overridden by another + * resource) + * + * @param path The path for the class loader resource of interest relative + * to the root of the class loader resources for the web + * application. It must start with '/'. + * + * @return The objects that represents the class loader resources at the + * given path. There will always be at least one element although + * that element may represent a resource that is not present. + */ + WebResource[] getClassLoaderResources(String path); + + /** + * Obtain the list of the names of all of the files and directories located + * in the specified directory. + * + * @param path The path for the resource of interest relative to the root + * of the web application. It must start with '/'. + * + * @return The list of resources. If path does not refer to a directory + * then a zero length array will be returned. + */ + String[] list(String path); + + /** + * Obtain the Set of the web applications pathnames of all of the files and + * directories located in the specified directory. Paths representing + * directories will end with a '/' character. + * + * @param path The path for the resource of interest relative to the root + * of the web application. It must start with '/'. + * + * @return The Set of resources. If path does not refer to a directory + * then null will be returned. + */ + Set listWebAppPaths(String path); + + /** + * Obtain the list of all of the WebResources in the specified directory. + * + * @param path The path for the resource of interest relative to the root + * of the web application. It must start with '/'. + * + * @return The list of resources. If path does not refer to a directory + * then a zero length array will be returned. + */ + WebResource[] listResources(String path); + + /** + * Create a new directory at the given path. + * + * @param path The path for the new resource to create relative to the root + * of the web application. It must start with '/'. + * + * @return true if the directory was created, otherwise + * false + */ + boolean mkdir(String path); + + /** + * Create a new resource at the requested path using the provided + * InputStream. + * + * @param path The path to be used for the new Resource. It is relative + * to the root of the web application and must start with + * '/'. + * @param is The InputStream that will provide the content for the + * new Resource. + * @param overwrite If true and the resource already exists it + * will be overwritten. If false and the + * resource already exists the write will fail. + * + * @return true if and only if the new Resource is written + */ + boolean write(String path, InputStream is, boolean overwrite); + + /** + * Creates a new {@link WebResourceSet} for this {@link WebResourceRoot} + * based on the provided parameters. + * + * @param type The type of {@link WebResourceSet} to create + * @param webAppMount The path within the web application that the + * resources should be published at. It must start + * with '/'. + * @param url The URL of the resource (must locate a JAR, file or + * directory) + * @param internalPath The path within the resource where the content is to + * be found. It must start with '/'. + */ + void createWebResourceSet(ResourceSetType type, String webAppMount, URL url, + String internalPath); + + /** + * Creates a new {@link WebResourceSet} for this {@link WebResourceRoot} + * based on the provided parameters. + * + * @param type The type of {@link WebResourceSet} to create + * @param webAppMount The path within the web application that the + * resources should be published at. It must start + * with '/'. + * @param base The location of the resources + * @param archivePath The path within the resource to the archive where + * the content is to be found. If there is no + * archive then this should be null. + * @param internalPath The path within the archive (or the resource if the + * archivePath is null where the + * content is to be found. It must start with '/'. + */ + void createWebResourceSet(ResourceSetType type, String webAppMount, + String base, String archivePath, String internalPath); + + + /** + * Adds the provided WebResourceSet to this web application as a 'Pre' + * resource. + * + * @param webResourceSet the resource set to use + */ + void addPreResources(WebResourceSet webResourceSet); + + /** + * @return the list of WebResourceSet configured to this web application + * as a 'Pre' resource. + */ + WebResourceSet[] getPreResources(); + + /** + * Adds the provided WebResourceSet to this web application as a 'Jar' + * resource. + * + * @param webResourceSet the resource set to use + */ + void addJarResources(WebResourceSet webResourceSet); + + /** + * @return the list of WebResourceSet configured to this web application + * as a 'Jar' resource. + */ + WebResourceSet[] getJarResources(); + + /** + * Adds the provided WebResourceSet to this web application as a 'Post' + * resource. + * + * @param webResourceSet the resource set to use + */ + void addPostResources(WebResourceSet webResourceSet); + + /** + * @return the list of WebResourceSet configured to this web application + * as a 'Post' resource. + */ + WebResourceSet[] getPostResources(); + + /** + * @return the web application this WebResourceRoot is associated with. + */ + Context getContext(); + + /** + * Set the web application this WebResourceRoot is associated with. + * + * @param context the associated context + */ + void setContext(Context context); + + /** + * Configure if this resources allow the use of symbolic links. + * + * @param allowLinking true if symbolic links are allowed. + */ + void setAllowLinking(boolean allowLinking); + + /** + * Determine if this resources allow the use of symbolic links. + * + * @return true if symbolic links are allowed + */ + boolean getAllowLinking(); + + /** + * Set whether or not caching is permitted for this web application. + * + * @param cachingAllowed true to enable caching, else + * false + */ + void setCachingAllowed(boolean cachingAllowed); + + /** + * @return true if caching is permitted for this web application. + */ + boolean isCachingAllowed(); + + /** + * Set the Time-To-Live (TTL) for cache entries. + * + * @param ttl TTL in milliseconds + */ + void setCacheTtl(long ttl); + + /** + * Get the Time-To-Live (TTL) for cache entries. + * + * @return TTL in milliseconds + */ + long getCacheTtl(); + + /** + * Set the maximum permitted size for the cache. + * + * @param cacheMaxSize Maximum cache size in kilobytes + */ + void setCacheMaxSize(long cacheMaxSize); + + /** + * Get the maximum permitted size for the cache. + * + * @return Maximum cache size in kilobytes + */ + long getCacheMaxSize(); + + /** + * Set the maximum permitted size for a single object in the cache. Note + * that the maximum size in bytes may not exceed {@link Integer#MAX_VALUE}. + * + * @param cacheObjectMaxSize Maximum size for a single cached object in + * kilobytes + */ + void setCacheObjectMaxSize(int cacheObjectMaxSize); + + /** + * Get the maximum permitted size for a single object in the cache. Note + * that the maximum size in bytes may not exceed {@link Integer#MAX_VALUE}. + * + * @return Maximum size for a single cached object in kilobytes + */ + int getCacheObjectMaxSize(); + + /** + * Controls whether the track locked files feature is enabled. If enabled, + * all calls to methods that return objects that lock a file and need to be + * closed to release that lock (e.g. {@link WebResource#getInputStream()} + * will perform a number of additional tasks. + *
    + *
  • The stack trace at the point where the method was called will be + * recorded and associated with the returned object.
  • + *
  • The returned object will be wrapped so that the point where close() + * (or equivalent) is called to release the resources can be detected. + * Tracking of the object will cease once the resources have been + * released.
  • + *
  • All remaining locked resources on web application shutdown will be + * logged and then closed.
  • + *
+ * + * @param trackLockedFiles {@code true} to enable it, {@code false} to + * disable it + */ + void setTrackLockedFiles(boolean trackLockedFiles); + + /** + * Has the track locked files feature been enabled? + * + * @return {@code true} if it has been enabled, otherwise {@code false} + */ + boolean getTrackLockedFiles(); + + /** + * Set the strategy to use for the resources archive lookup. + * + * @param archiveIndexStrategy The strategy to use for the resources archive lookup + */ + void setArchiveIndexStrategy(String archiveIndexStrategy); + + /** + * Get the strategy to use for the resources archive lookup. + * + * @return The strategy to use for the resources archive lookup + */ + String getArchiveIndexStrategy(); + + /** + * Get the strategy to use for the resources archive lookup. + * + * @return The strategy to use for the resources archive lookup + */ + ArchiveIndexStrategy getArchiveIndexStrategyEnum(); + + /** + * This method will be invoked by the context on a periodic basis and allows + * the implementation a method that executes periodic tasks, such as purging + * expired cache entries. + */ + void backgroundProcess(); + + /** + * Add a specified resource to track to be able to later release + * resources on stop. + * @param trackedResource the resource that will be tracked + */ + void registerTrackedResource(TrackedWebResource trackedResource); + + /** + * Stop tracking specified resource, once it no longer needs to free resources. + * @param trackedResource the resource that was tracked + */ + void deregisterTrackedResource(TrackedWebResource trackedResource); + + /** + * @return the set of {@link WebResourceSet#getBaseUrl()} for all + * {@link WebResourceSet}s used by this root. + */ + List getBaseUrls(); + + /** + * Implementations may cache some information to improve performance. This + * method triggers the clean-up of those resources. + */ + void gc(); + + /** + * Obtain the current caching strategy. + *

+ * The default implementation returns {@code null}. Sub-classes wishing to + * utilise a {@link CacheStrategy} should provide an appropriate + * implementation. + * + * @return the current caching strategy or {@code null} if no strategy has + * been configured + */ + default CacheStrategy getCacheStrategy() { + return null; + } + + /** + * Set the current caching strategy. + *

+ * The default implementation is a NO-OP. Sub-classes wishing to utilise a + * {@link CacheStrategy} should provide an appropriate implementation. + * + * @param strategy The new strategy to use or {@code null} for no strategy + */ + default void setCacheStrategy(CacheStrategy strategy) { + // NO-OP + } + + enum ResourceSetType { + PRE, + RESOURCE_JAR, + POST, + CLASSES_JAR + } + + enum ArchiveIndexStrategy { + SIMPLE(false, false), + BLOOM(true, true), + PURGED(true, false); + + private final boolean usesBloom; + private final boolean retain; + + ArchiveIndexStrategy(boolean usesBloom, boolean retain) { + this.usesBloom = usesBloom; + this.retain = retain; + } + + public boolean getUsesBloom() { + return usesBloom; + } + + public boolean getRetain() { + return retain; + } + } + + /** + * Provides a mechanism to modify the caching behaviour. + */ + interface CacheStrategy { + + /** + * Should the result of looking up the resource at the given path be + * excluded from caching? + * + * @param path The path to check against the strategy to see if the + * result should be cached + * + * @return {@code true} if the result should not be cached, otherwise + * {@code false} + */ + boolean noCache(String path); + } +} diff --git a/java/org/apache/catalina/WebResourceSet.java b/java/org/apache/catalina/WebResourceSet.java new file mode 100644 index 0000000..9d71cd0 --- /dev/null +++ b/java/org/apache/catalina/WebResourceSet.java @@ -0,0 +1,156 @@ +/* + * 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.catalina; + +import java.io.InputStream; +import java.net.URL; +import java.util.Set; + +/** + * Represents a set of resources that are part of a web application. Examples + * include a directory structure, a resources JAR and a WAR file. + */ +public interface WebResourceSet extends Lifecycle { + /** + * Obtain the object that represents the resource at the given path. Note + * the resource at that path may not exist. + * + * @param path The path for the resource of interest relative to the root + * of the web application. It must start with '/'. + * + * @return The object that represents the resource at the given path + */ + WebResource getResource(String path); + + /** + * Obtain the list of the names of all of the files and directories located + * in the specified directory. + * + * @param path The path for the resource of interest relative to the root + * of the web application. It must start with '/'. + * + * @return The list of resources. If path does not refer to a directory + * then a zero length array will be returned. + */ + String[] list(String path); + + /** + * Obtain the Set of the web applications pathnames of all of the files and + * directories located in the specified directory. Paths representing + * directories will end with a "/" character. + * + * @param path The path for the resource of interest relative to the root + * of the web application. It must start with '/'. + * + * @return The Set of resources. If path does not refer to a directory + * then an empty set will be returned. + */ + Set listWebAppPaths(String path); + + /** + * Create a new directory at the given path. + * + * @param path The path for the new resource to create relative to the root + * of the web application. It must start with '/'. + * + * @return true if the directory was created, otherwise + * false + */ + boolean mkdir(String path); + + /** + * Create a new resource at the requested path using the provided + * InputStream. + * + * @param path The path to be used for the new Resource. It is relative + * to the root of the web application and must start with + * '/'. + * @param is The InputStream that will provide the content for the + * new Resource. + * @param overwrite If true and the resource already exists it + * will be overwritten. If false and the + * resource already exists the write will fail. + * + * @return true if and only if the new Resource is written + */ + boolean write(String path, InputStream is, boolean overwrite); + + void setRoot(WebResourceRoot root); + + /** + * Should resources returned by this resource set only be included in any + * results when the lookup is explicitly looking for class loader resources. + * i.e. should these resources be excluded from look ups that are explicitly + * looking for static (non-class loader) resources. + * + * @return true if these resources should only be used for + * class loader resource lookups, otherwise false + */ + boolean getClassLoaderOnly(); + + void setClassLoaderOnly(boolean classLoaderOnly); + + /** + * Should resources returned by this resource set only be included in any + * results when the lookup is explicitly looking for static (non-class + * loader) resources. i.e. should these resources be excluded from look ups + * that are explicitly looking for class loader resources. + * + * @return true if these resources should only be used for + * static (non-class loader) resource lookups, otherwise + * false + */ + boolean getStaticOnly(); + + void setStaticOnly(boolean staticOnly); + + /** + * Obtain the base URL for this set of resources. One of the uses of this is + * to grant read permissions to the resources when running under a security + * manager. + * + * @return The base URL for this set of resources + */ + URL getBaseUrl(); + + /** + * Configures whether or not this set of resources is read-only. + * + * @param readOnly true if this set of resources should be + * configured to be read-only + * + * @throws IllegalArgumentException if an attempt is made to configure a + * {@link WebResourceSet} that is hard-coded to be read-only as + * writable + */ + void setReadOnly(boolean readOnly); + + /** + * Obtains the current value of the read-only setting for this set of + * resources. + * + * @return true if this set of resources is configured to be + * read-only, otherwise false + */ + boolean isReadOnly(); + + /** + * Implementations may cache some information to improve performance. This + * method triggers the clean-up of those resources. + */ + void gc(); +} diff --git a/java/org/apache/catalina/Wrapper.java b/java/org/apache/catalina/Wrapper.java new file mode 100644 index 0000000..0803161 --- /dev/null +++ b/java/org/apache/catalina/Wrapper.java @@ -0,0 +1,374 @@ +/* + * 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.catalina; + + +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.UnavailableException; + + +/** + * A Wrapper is a Container that represents an individual servlet + * definition from the deployment descriptor of the web application. It + * provides a convenient mechanism to use Interceptors that see every single + * request to the servlet represented by this definition. + *

+ * Implementations of Wrapper are responsible for managing the servlet life + * cycle for their underlying servlet class, including calling init() and + * destroy() at appropriate times. + *

+ * The parent Container attached to a Wrapper will generally be an + * implementation of Context, representing the servlet context (and + * therefore the web application) within which this servlet executes. + *

+ * Child Containers are not allowed on Wrapper implementations, so the + * addChild() method should throw an + * IllegalArgumentException. + * + * @author Craig R. McClanahan + */ +public interface Wrapper extends Container { + + /** + * Container event for adding a wrapper. + */ + String ADD_MAPPING_EVENT = "addMapping"; + + /** + * Container event for removing a wrapper. + */ + String REMOVE_MAPPING_EVENT = "removeMapping"; + + // ------------------------------------------------------------- Properties + + + /** + * @return the available date/time for this servlet, in milliseconds since + * the epoch. If this date/time is in the future, any request for this + * servlet will return an SC_SERVICE_UNAVAILABLE error. If it is zero, + * the servlet is currently available. A value equal to Long.MAX_VALUE + * is considered to mean that unavailability is permanent. + */ + long getAvailable(); + + + /** + * Set the available date/time for this servlet, in milliseconds since the + * epoch. If this date/time is in the future, any request for this servlet + * will return an SC_SERVICE_UNAVAILABLE error. A value equal to + * Long.MAX_VALUE is considered to mean that unavailability is permanent. + * + * @param available The new available date/time + */ + void setAvailable(long available); + + + /** + * @return the load-on-startup order value (negative value means + * load on first call). + */ + int getLoadOnStartup(); + + + /** + * Set the load-on-startup order value (negative value means + * load on first call). + * + * @param value New load-on-startup value + */ + void setLoadOnStartup(int value); + + + /** + * @return the run-as identity for this servlet. + */ + String getRunAs(); + + + /** + * Set the run-as identity for this servlet. + * + * @param runAs New run-as identity value + */ + void setRunAs(String runAs); + + + /** + * @return the fully qualified servlet class name for this servlet. + */ + String getServletClass(); + + + /** + * Set the fully qualified servlet class name for this servlet. + * + * @param servletClass Servlet class name + */ + void setServletClass(String servletClass); + + + /** + * Gets the names of the methods supported by the underlying servlet. + * + * This is the same set of methods included in the Allow response header + * in response to an OPTIONS request method processed by the underlying + * servlet. + * + * @return Array of names of the methods supported by the underlying + * servlet + * + * @throws ServletException If the target servlet cannot be loaded + */ + String[] getServletMethods() throws ServletException; + + + /** + * @return true if this Servlet is currently unavailable. + */ + boolean isUnavailable(); + + + /** + * @return the associated Servlet instance. + */ + Servlet getServlet(); + + + /** + * Set the associated Servlet instance + * + * @param servlet The associated Servlet + */ + void setServlet(Servlet servlet); + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new servlet initialization parameter for this servlet. + * + * @param name Name of this initialization parameter to add + * @param value Value of this initialization parameter to add + */ + void addInitParameter(String name, String value); + + + /** + * Add a mapping associated with the Wrapper. + * + * @param mapping The new wrapper mapping + */ + void addMapping(String mapping); + + + /** + * Add a new security role reference record to the set of records for + * this servlet. + * + * @param name Role name used within this servlet + * @param link Role name used within the web application + */ + void addSecurityReference(String name, String link); + + + /** + * Allocate an initialized instance of this Servlet that is ready to have + * its service() method called. The previously initialized + * instance may be returned immediately. + * + * @exception ServletException if the Servlet init() method threw + * an exception + * @exception ServletException if a loading error occurs + * @return a new Servlet instance + */ + Servlet allocate() throws ServletException; + + + /** + * Decrement the allocation count for the servlet instance. + * + * @param servlet The servlet to be returned + * + * @exception ServletException if a deallocation error occurs + */ + void deallocate(Servlet servlet) throws ServletException; + + + /** + * @return the value for the specified initialization parameter name, + * if any; otherwise return null. + * + * @param name Name of the requested initialization parameter + */ + String findInitParameter(String name); + + + /** + * @return the names of all defined initialization parameters for this + * servlet. + */ + String[] findInitParameters(); + + + /** + * @return the mappings associated with this wrapper. + */ + String[] findMappings(); + + + /** + * @return the security role link for the specified security role + * reference name, if any; otherwise return null. + * + * @param name Security role reference used within this servlet + */ + String findSecurityReference(String name); + + + /** + * @return the set of security role reference names associated with + * this servlet, if any; otherwise return a zero-length array. + */ + String[] findSecurityReferences(); + + + /** + * Increment the error count value used when monitoring. + */ + void incrementErrorCount(); + + + /** + * Load and initialize an instance of this Servlet, if there is not already + * at least one initialized instance. This can be used, for example, to + * load Servlets that are marked in the deployment descriptor to be loaded + * at server startup time. + * + * @exception ServletException if the Servlet init() method threw + * an exception or if some other loading problem occurs + */ + void load() throws ServletException; + + + /** + * Remove the specified initialization parameter from this Servlet. + * + * @param name Name of the initialization parameter to remove + */ + void removeInitParameter(String name); + + + /** + * Remove a mapping associated with the wrapper. + * + * @param mapping The pattern to remove + */ + void removeMapping(String mapping); + + + /** + * Remove any security role reference for the specified role name. + * + * @param name Security role used within this servlet to be removed + */ + void removeSecurityReference(String name); + + + /** + * Process an UnavailableException, marking this Servlet as unavailable + * for the specified amount of time. + * + * @param unavailable The exception that occurred, or null + * to mark this Servlet as permanently unavailable + */ + void unavailable(UnavailableException unavailable); + + + /** + * Unload all initialized instances of this servlet, after calling the + * destroy() method for each instance. This can be used, + * for example, prior to shutting down the entire servlet engine, or + * prior to reloading all of the classes from the Loader associated with + * our Loader's repository. + * + * @exception ServletException if an unload error occurs + */ + void unload() throws ServletException; + + + /** + * @return the multi-part configuration for the associated Servlet. If no + * multi-part configuration has been defined, then null will be + * returned. + */ + MultipartConfigElement getMultipartConfigElement(); + + + /** + * Set the multi-part configuration for the associated Servlet. To clear the + * multi-part configuration specify null as the new value. + * + * @param multipartConfig The configuration associated with the Servlet + */ + void setMultipartConfigElement( + MultipartConfigElement multipartConfig); + + /** + * Does the associated Servlet support async processing? Defaults to + * false. + * + * @return true if the Servlet supports async + */ + boolean isAsyncSupported(); + + /** + * Set the async support for the associated Servlet. + * + * @param asyncSupport the new value + */ + void setAsyncSupported(boolean asyncSupport); + + /** + * Is the associated Servlet enabled? Defaults to true. + * + * @return true if the Servlet is enabled + */ + boolean isEnabled(); + + /** + * Sets the enabled attribute for the associated servlet. + * + * @param enabled the new value + */ + void setEnabled(boolean enabled); + + /** + * Is the Servlet overridable by a ServletContainerInitializer? + * + * @return true if the Servlet can be overridden in a ServletContainerInitializer + */ + boolean isOverridable(); + + /** + * Sets the overridable attribute for this Servlet. + * + * @param overridable the new value + */ + void setOverridable(boolean overridable); +} diff --git a/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java b/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java new file mode 100644 index 0000000..8c4b2af --- /dev/null +++ b/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java @@ -0,0 +1,84 @@ +/* + * 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.catalina.ant; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.apache.tools.ant.BuildException; + +public abstract class AbstractCatalinaCommandTask extends AbstractCatalinaTask { + + /** + * The context path of the web application we are managing. + */ + protected String path = null; + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + /** + * The context version of the web application we are managing. + */ + protected String version = null; + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Create query string for the specified command. + * + * @param command Command to be executed + * + * @return The generated query string + * + * @exception BuildException if an error occurs + */ + public StringBuilder createQueryString(String command) throws BuildException { + StringBuilder buffer = new StringBuilder(); + + try { + buffer.append(command); + if (path == null) { + throw new BuildException("Must specify 'path' attribute"); + } else { + buffer.append("?path="); + buffer.append(URLEncoder.encode(this.path, getCharset())); + if (this.version != null) { + buffer.append("&version="); + buffer.append(URLEncoder.encode(this.version, getCharset())); + } + } + } catch (UnsupportedEncodingException e) { + throw new BuildException("Invalid 'charset' attribute: " + getCharset()); + } + return buffer; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ant/AbstractCatalinaTask.java b/java/org/apache/catalina/ant/AbstractCatalinaTask.java new file mode 100644 index 0000000..61ac76e --- /dev/null +++ b/java/org/apache/catalina/ant/AbstractCatalinaTask.java @@ -0,0 +1,348 @@ +/* + * 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.catalina.ant; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Authenticator; +import java.net.HttpURLConnection; +import java.net.PasswordAuthentication; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLConnection; + +import org.apache.catalina.util.IOTools; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; + +/** + * Abstract base class for Ant tasks that interact with the Manager web + * application for dynamically deploying and undeploying applications. These + * tasks require Ant 1.4 or later. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public abstract class AbstractCatalinaTask extends BaseRedirectorHelperTask { + + // ----------------------------------------------------- Instance Variables + + /** + * manager webapp's encoding. + */ + private static final String CHARSET = "utf-8"; + + + // ------------------------------------------------------------- Properties + + /** + * The charset used during URL encoding. + */ + protected String charset = "ISO-8859-1"; + + public String getCharset() { + return charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + + /** + * The login password for the Manager application. + */ + protected String password = null; + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + + /** + * The URL of the Manager application to be used. + */ + protected String url = "http://localhost:8080/manager/text"; + + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + + /** + * The login username for the Manager application. + */ + protected String username = null; + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + /** + * If set to true - ignore the constraint of the first line of the response + * message that must be "OK -". + *

+ * When this attribute is set to {@code false} (the default), the first line + * of server response is expected to start with "OK -". If it does not then + * the task is considered as failed and the first line is treated as an + * error message. + *

+ * When this attribute is set to {@code true}, the first line of the + * response is treated like any other, regardless of its text. + */ + protected boolean ignoreResponseConstraint = false; + + public boolean isIgnoreResponseConstraint() { + return ignoreResponseConstraint; + } + + public void setIgnoreResponseConstraint(boolean ignoreResponseConstraint) { + this.ignoreResponseConstraint = ignoreResponseConstraint; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Execute the specified command. This logic only performs the common + * attribute validation required by all subclasses; it does not perform any + * functional logic directly. + * + * @exception BuildException if a validation error occurs + */ + @Override + public void execute() throws BuildException { + if ((username == null) || (password == null) || (url == null)) { + throw new BuildException("Must specify all of 'username', 'password', and 'url'"); + } + } + + + /** + * Execute the specified command, based on the configured properties. + * + * @param command Command to be executed + * + * @exception BuildException if an error occurs + */ + public void execute(String command) throws BuildException { + execute(command, null, null, -1); + } + + + /** + * Execute the specified command, based on the configured properties. The + * input stream will be closed upon completion of this task, whether it was + * executed successfully or not. + * + * @param command Command to be executed + * @param istream InputStream to include in an HTTP PUT, if any + * @param contentType Content type to specify for the input, if any + * @param contentLength Content length to specify for the input, if any + * + * @exception BuildException if an error occurs + */ + public void execute(String command, InputStream istream, String contentType, long contentLength) + throws BuildException { + + URLConnection conn = null; + InputStreamReader reader = null; + try { + // Set up authorization with our credentials + Authenticator.setDefault(new TaskAuthenticator(username, password)); + + // Create a connection for this command + URI uri = new URI(url + command); + uri.parseServerAuthority(); + conn = uri.toURL().openConnection(); + HttpURLConnection hconn = (HttpURLConnection) conn; + + // Set up standard connection characteristics + hconn.setAllowUserInteraction(false); + hconn.setDoInput(true); + hconn.setUseCaches(false); + if (istream != null) { + preAuthenticate(); + + hconn.setDoOutput(true); + hconn.setRequestMethod("PUT"); + if (contentType != null) { + hconn.setRequestProperty("Content-Type", contentType); + } + if (contentLength >= 0) { + hconn.setRequestProperty("Content-Length", "" + contentLength); + + hconn.setFixedLengthStreamingMode(contentLength); + } + } else { + hconn.setDoOutput(false); + hconn.setRequestMethod("GET"); + } + hconn.setRequestProperty("User-Agent", "Catalina-Ant-Task/1.0"); + + // Establish the connection with the server + hconn.connect(); + + // Send the request data (if any) + if (istream != null) { + try (OutputStream ostream = hconn.getOutputStream()) { + IOTools.flow(istream, ostream); + } finally { + try { + istream.close(); + } catch (Exception e) { + } + } + } + + // Process the response message + reader = new InputStreamReader(hconn.getInputStream(), CHARSET); + StringBuilder buff = new StringBuilder(); + String error = null; + int msgPriority = Project.MSG_INFO; + boolean first = true; + while (true) { + int ch = reader.read(); + if (ch < 0) { + break; + } else if (ch == '\r' || ch == '\n') { + // in Win \r\n would cause handleOutput() to be called + // twice, the second time with an empty string, + // producing blank lines + if (buff.length() > 0) { + String line = buff.toString(); + buff.setLength(0); + if (!ignoreResponseConstraint && first) { + if (!line.startsWith("OK -")) { + error = line; + msgPriority = Project.MSG_ERR; + } + first = false; + } + handleOutput(line, msgPriority); + } + } else { + buff.append((char) ch); + } + } + if (buff.length() > 0) { + handleOutput(buff.toString(), msgPriority); + } + if (error != null && isFailOnError()) { + // exception should be thrown only if failOnError == true + // or error line will be logged twice + throw new BuildException(error); + } + } catch (Exception e) { + if (isFailOnError()) { + throw new BuildException(e); + } else { + handleErrorOutput(e.getMessage()); + } + } finally { + closeRedirector(); + if (reader != null) { + try { + reader.close(); + } catch (IOException ioe) { + // Ignore + } + reader = null; + } + if (istream != null) { + try { + istream.close(); + } catch (IOException ioe) { + // Ignore + } + } + } + } + + + /* + * This is a hack. + * We need to use streaming to avoid OOME on large uploads. + * We'd like to use Authenticator.setDefault() for authentication as the JRE + * then provides the DIGEST client implementation. + * However, the above two are not compatible. When the request is made, the + * resulting 401 triggers an exception because, when using streams, the + * InputStream is no longer available to send with the repeated request that + * now includes the appropriate Authorization header. + * The hack is to make a simple OPTIONS request- i.e. without a request + * body. + * This triggers authentication and the requirement to authenticate for this + * host is cached and used to provide an appropriate Authorization when the + * next request is made (that includes a request body). + */ + private void preAuthenticate() throws IOException, URISyntaxException { + URLConnection conn = null; + + // Create a connection for this command + URI uri = new URI(url); + uri.parseServerAuthority(); + conn = uri.toURL().openConnection(); + HttpURLConnection hconn = (HttpURLConnection) conn; + + // Set up standard connection characteristics + hconn.setAllowUserInteraction(false); + hconn.setDoInput(true); + hconn.setUseCaches(false); + hconn.setDoOutput(false); + hconn.setRequestMethod("OPTIONS"); + hconn.setRequestProperty("User-Agent", "Catalina-Ant-Task/1.0"); + + // Establish the connection with the server + hconn.connect(); + + // Swallow response message + try (InputStream is = hconn.getInputStream()) { + IOTools.flow(is, null); + } + } + + + private static class TaskAuthenticator extends Authenticator { + + private final String user; + private final String password; + + private TaskAuthenticator(String user, String password) { + this.user = user; + this.password = password; + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(user, password.toCharArray()); + } + } +} diff --git a/java/org/apache/catalina/ant/BaseRedirectorHelperTask.java b/java/org/apache/catalina/ant/BaseRedirectorHelperTask.java new file mode 100644 index 0000000..7e4d7c3 --- /dev/null +++ b/java/org/apache/catalina/ant/BaseRedirectorHelperTask.java @@ -0,0 +1,373 @@ +/* + * 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.catalina.ant; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.taskdefs.Redirector; +import org.apache.tools.ant.types.RedirectorElement; + +/** + * Abstract base class to add output redirection support for Catalina Ant tasks. + * These tasks require Ant 1.5 or later. + *
+ * WARNING: due to depends chain, Ant could call a Task more + * than once and this can affect the output redirection when configured. If you + * are collecting the output in a property, it will collect the output of only + * the first run, since Ant properties are immutable and once created they + * cannot be changed.
+ * If you are collecting output in a file the file will be overwritten with the + * output of the last run, unless you set append="true", in which case each run + * will append it's output to the file. + * + * + * @author Gabriele Garuglieri + * @since 5.5 + */ +public abstract class BaseRedirectorHelperTask extends Task { + + /** Redirector helper */ + protected final Redirector redirector = new Redirector(this); + + /** Redirector element for this task */ + protected RedirectorElement redirectorElement = null; + + /** The stream for info output */ + protected OutputStream redirectOutStream = null; + + /** The stream for error output */ + protected OutputStream redirectErrStream = null; + + /** The print stream for info output */ + PrintStream redirectOutPrintStream = null; + + /** The print stream for error output */ + PrintStream redirectErrPrintStream = null; + + /** + * Whether to fail (with a BuildException) if ManagerServlet returns an + * error. The default behavior is to do so. This flag does not control + * parameters checking. If the task is called with wrong or invalid + * parameters, it will throw BuildException independently from the setting + * of this flag. + */ + protected boolean failOnError = true; + + /** + * true true when output redirection is requested for this task. + * Default is to log on Ant log. + */ + protected boolean redirectOutput = false; + + /** + * will be set to true when the configuration of the Redirector + * is complete. + */ + protected boolean redirectorConfigured = false; + + /** + * Flag which indicates that, if redirected, output should also be always + * sent to the log. Default is that output is sent only to redirected + * streams. + */ + protected boolean alwaysLog = false; + + + /** + * Whether to fail (with a BuildException) if ManagerServlet returns an + * error. The default behavior is to do so. + * + * @param fail The new value of failonerror + */ + public void setFailonerror(boolean fail) { + failOnError = fail; + } + + + /** + * Returns the value of the failOnError property. + * + * @return true if the task should will if an error occurs, + * otherwise false + */ + public boolean isFailOnError() { + return failOnError; + } + + + /** + * File the output of the task is redirected to. + * + * @param out name of the output file + */ + public void setOutput(File out) { + redirector.setOutput(out); + redirectOutput = true; + } + + + /** + * File the error output of the task is redirected to. + * + * @param error name of the error file + * + */ + public void setError(File error) { + redirector.setError(error); + redirectOutput = true; + } + + + /** + * Controls whether error output is logged. This is only useful when output + * is being redirected and error output is desired in the Ant log + * + * @param logError if true the standard error is sent to the Ant log system + * and not sent to output stream. + */ + public void setLogError(boolean logError) { + redirector.setLogError(logError); + redirectOutput = true; + } + + + /** + * Property name whose value should be set to the output of the task. + * + * @param outputProperty property name + * + */ + public void setOutputproperty(String outputProperty) { + redirector.setOutputProperty(outputProperty); + redirectOutput = true; + } + + + /** + * Property name whose value should be set to the error of the task. + * + * @param errorProperty property name + * + */ + public void setErrorProperty(String errorProperty) { + redirector.setErrorProperty(errorProperty); + redirectOutput = true; + } + + + /** + * If true, append output to existing file. + * + * @param append if true, append output to existing file + * + */ + public void setAppend(boolean append) { + redirector.setAppend(append); + redirectOutput = true; + } + + + /** + * If true, (error and non-error) output will be redirected as specified + * while being sent to Ant's logging mechanism as if no redirection had + * taken place. Defaults to false. + *
+ * Actually handled internally, with Ant 1.6.3 it will be handled by the + * Redirector itself. + * + * @param alwaysLog boolean + */ + public void setAlwaysLog(boolean alwaysLog) { + this.alwaysLog = alwaysLog; + redirectOutput = true; + } + + + /** + * Whether output and error files should be created even when empty. + * Defaults to true. + * + * @param createEmptyFiles boolean. + */ + public void setCreateEmptyFiles(boolean createEmptyFiles) { + redirector.setCreateEmptyFiles(createEmptyFiles); + redirectOutput = true; + } + + + /** + * Add a RedirectorElement to this task. + * + * @param redirectorElement RedirectorElement. + */ + public void addConfiguredRedirector(RedirectorElement redirectorElement) { + if (this.redirectorElement != null) { + throw new BuildException("Cannot have > 1 nested s"); + } else { + this.redirectorElement = redirectorElement; + } + } + + + /** + * Set up properties on the Redirector from RedirectorElement if present. + */ + private void configureRedirector() { + if (redirectorElement != null) { + redirectorElement.configure(redirector); + redirectOutput = true; + } + /* + * Due to depends chain, Ant could call the Task more than once, this is + * to prevent that we attempt to configure uselessly more than once the + * Redirector. + */ + redirectorConfigured = true; + } + + + /** + * Set up properties on the Redirector and create output streams. + */ + protected void openRedirector() { + if (!redirectorConfigured) { + configureRedirector(); + } + if (redirectOutput) { + redirector.createStreams(); + redirectOutStream = redirector.getOutputStream(); + redirectOutPrintStream = new PrintStream(redirectOutStream); + redirectErrStream = redirector.getErrorStream(); + redirectErrPrintStream = new PrintStream(redirectErrStream); + } + } + + + /** + * Ask redirector to close all the streams. It is necessary to call this + * method before leaving the Task to have the Streams flush their contents. + * If you are collecting output in a property, it will be created only if + * this method is called, otherwise you'll find it unset. + */ + protected void closeRedirector() { + try { + if (redirectOutput && redirectOutPrintStream != null) { + redirector.complete(); + } + } catch (IOException ioe) { + log("Error closing redirector: " + ioe.getMessage(), Project.MSG_ERR); + } + /* + * Due to depends chain, Ant could call the Task more than once, this is + * to prevent that we attempt to reuse the previously closed Streams. + */ + redirectOutStream = null; + redirectOutPrintStream = null; + redirectErrStream = null; + redirectErrPrintStream = null; + } + + + /** + * Handles output with the INFO priority. + * + * @param output The output to log. Should not be null. + */ + @Override + protected void handleOutput(String output) { + if (redirectOutput) { + if (redirectOutPrintStream == null) { + openRedirector(); + } + redirectOutPrintStream.println(output); + if (alwaysLog) { + log(output, Project.MSG_INFO); + } + } else { + log(output, Project.MSG_INFO); + } + } + + + /** + * Handles output with the INFO priority and flushes the stream. + * + * @param output The output to log. Should not be null. + * + */ + @Override + protected void handleFlush(String output) { + handleOutput(output); + redirectOutPrintStream.flush(); + } + + + /** + * Handles error output with the ERR priority. + * + * @param output The error output to log. Should not be null. + */ + @Override + protected void handleErrorOutput(String output) { + if (redirectOutput) { + if (redirectErrPrintStream == null) { + openRedirector(); + } + redirectErrPrintStream.println(output); + if (alwaysLog) { + log(output, Project.MSG_ERR); + } + } else { + log(output, Project.MSG_ERR); + } + } + + + /** + * Handles error output with the ERR priority and flushes the stream. + * + * @param output The error output to log. Should not be null. + * + */ + @Override + protected void handleErrorFlush(String output) { + handleErrorOutput(output); + redirectErrPrintStream.flush(); + } + + + /** + * Handles output with ERR priority to error stream and all other priorities + * to output stream. + * + * @param output The output to log. Should not be null. + * @param priority The priority level that should be used + */ + protected void handleOutput(String output, int priority) { + if (priority == Project.MSG_ERR) { + handleErrorOutput(output); + } else { + handleOutput(output); + } + } +} diff --git a/java/org/apache/catalina/ant/DeployTask.java b/java/org/apache/catalina/ant/DeployTask.java new file mode 100644 index 0000000..bf4bc67 --- /dev/null +++ b/java/org/apache/catalina/ant/DeployTask.java @@ -0,0 +1,194 @@ +/* + * 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.catalina.ant; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.nio.channels.FileChannel; +import java.util.regex.Pattern; + +import org.apache.tools.ant.BuildException; + +/** + * Ant task that implements the /deploy command, supported by the + * Tomcat manager application. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public class DeployTask extends AbstractCatalinaCommandTask { + + private static final Pattern PROTOCOL_PATTERN = Pattern.compile("\\w{3,5}\\:"); + + /** + * URL of the context configuration file for this application, if any. + */ + protected String config = null; + + public String getConfig() { + return this.config; + } + + public void setConfig(String config) { + this.config = config; + } + + + /** + * URL of the server local web application archive (WAR) file to be + * deployed. + */ + protected String localWar = null; + + public String getLocalWar() { + return this.localWar; + } + + public void setLocalWar(String localWar) { + this.localWar = localWar; + } + + + /** + * Tag to associate with this to be deployed webapp. + */ + protected String tag = null; + + public String getTag() { + return this.tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + + /** + * Update existing webapps. + */ + protected boolean update = false; + + public boolean getUpdate() { + return this.update; + } + + public void setUpdate(boolean update) { + this.update = update; + } + + + /** + * URL of the web application archive (WAR) file to be deployed. + */ + protected String war = null; + + public String getWar() { + return this.war; + } + + public void setWar(String war) { + this.war = war; + } + + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + super.execute(); + if (path == null) { + throw new BuildException("Must specify 'path' attribute"); + } + if ((war == null) && (localWar == null) && (config == null) && (tag == null)) { + throw new BuildException( + "Must specify either 'war', 'localWar', 'config', or 'tag' attribute"); + } + // Building an input stream on the WAR to upload, if any + BufferedInputStream stream = null; + String contentType = null; + long contentLength = -1; + if (war != null) { + if (PROTOCOL_PATTERN.matcher(war).lookingAt()) { + try { + URI uri = new URI(war); + URLConnection conn = uri.toURL().openConnection(); + contentLength = conn.getContentLengthLong(); + stream = new BufferedInputStream(conn.getInputStream(), 1024); + } catch (IOException | URISyntaxException e) { + throw new BuildException(e); + } + } else { + FileInputStream fsInput= null; + try { + fsInput = new FileInputStream(war); + FileChannel fsChannel = fsInput.getChannel(); + contentLength = fsChannel.size(); + stream = new BufferedInputStream(fsInput, 1024); + } catch (IOException e) { + if (fsInput != null) { + try { + fsInput.close(); + } catch (IOException ioe) { + // Ignore + } + } + throw new BuildException(e); + } + } + contentType = "application/octet-stream"; + } + // Building URL + StringBuilder sb = createQueryString("/deploy"); + try { + if ((war == null) && (config != null)) { + sb.append("&config="); + sb.append(URLEncoder.encode(config, getCharset())); + } + if ((war == null) && (localWar != null)) { + sb.append("&war="); + sb.append(URLEncoder.encode(localWar, getCharset())); + } + if (update) { + sb.append("&update=true"); + } + if (tag != null) { + sb.append("&tag="); + sb.append(URLEncoder.encode(tag, getCharset())); + } + execute(sb.toString(), stream, contentType, contentLength); + } catch (UnsupportedEncodingException e) { + throw new BuildException("Invalid 'charset' attribute: " + getCharset()); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException ioe) { + // Ignore + } + } + } + } +} diff --git a/java/org/apache/catalina/ant/FindLeaksTask.java b/java/org/apache/catalina/ant/FindLeaksTask.java new file mode 100644 index 0000000..4be0996 --- /dev/null +++ b/java/org/apache/catalina/ant/FindLeaksTask.java @@ -0,0 +1,61 @@ +/* + * 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.catalina.ant; + +import org.apache.tools.ant.BuildException; + +/** + * Ant task that implements the /findleaks command, supported by + * the Tomcat manager application. + */ +public class FindLeaksTask extends AbstractCatalinaTask { + + private boolean statusLine = true; + + /** + * Sets the statusLine parameter that controls if the response includes a + * status line or not. + * + * @param statusLine true if the status line should be included + */ + public void setStatusLine(boolean statusLine) { + this.statusLine = statusLine; + } + + /** + * Returns the statusLine parameter that controls if the response includes a + * status line or not. + * + * @return true if the status line should be included, + * otherwise false + */ + public boolean getStatusLine() { + return statusLine; + } + + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + super.execute(); + execute("/findleaks?statusLine=" + Boolean.toString(statusLine)); + } +} diff --git a/java/org/apache/catalina/ant/JKStatusUpdateTask.java b/java/org/apache/catalina/ant/JKStatusUpdateTask.java new file mode 100644 index 0000000..4f322b7 --- /dev/null +++ b/java/org/apache/catalina/ant/JKStatusUpdateTask.java @@ -0,0 +1,415 @@ +/* + * 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.catalina.ant; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.apache.tools.ant.BuildException; + +/** + * Ant task that implements the /status command, supported by the + * mod_jk status (1.2.9) application. + * + * @author Peter Rossbach + * @since 5.5.9 + */ +public class JKStatusUpdateTask extends AbstractCatalinaTask { + + private String worker = "lb"; + + private String workerType = "lb"; + + private int internalid = 0; + + private Integer lbRetries; + + private Integer lbRecovertime; + + private Boolean lbStickySession = Boolean.TRUE; + + private Boolean lbForceSession = Boolean.FALSE; + + private Integer workerLoadFactor; + + private String workerRedirect; + + private String workerClusterDomain; + + private Boolean workerDisabled = Boolean.FALSE; + + private Boolean workerStopped = Boolean.FALSE; + + private boolean isLBMode = true; + + private String workerLb; + + /** + * + */ + public JKStatusUpdateTask() { + super(); + setUrl("http://localhost/status"); + } + + /** + * @return Returns the internalid. + */ + public int getInternalid() { + return internalid; + } + + /** + * @param internalid + * The internalid to set. + */ + public void setInternalid(int internalid) { + this.internalid = internalid; + } + + /** + * @return Returns the lbForceSession. + */ + public Boolean getLbForceSession() { + return lbForceSession; + } + + /** + * @param lbForceSession + * The lbForceSession to set. + */ + public void setLbForceSession(Boolean lbForceSession) { + this.lbForceSession = lbForceSession; + } + + /** + * @return Returns the lbRecovertime. + */ + public Integer getLbRecovertime() { + return lbRecovertime; + } + + /** + * @param lbRecovertime + * The lbRecovertime to set. + */ + public void setLbRecovertime(Integer lbRecovertime) { + this.lbRecovertime = lbRecovertime; + } + + /** + * @return Returns the lbRetries. + */ + public Integer getLbRetries() { + return lbRetries; + } + + /** + * @param lbRetries + * The lbRetries to set. + */ + public void setLbRetries(Integer lbRetries) { + this.lbRetries = lbRetries; + } + + /** + * @return Returns the lbStickySession. + */ + public Boolean getLbStickySession() { + return lbStickySession; + } + + /** + * @param lbStickySession + * The lbStickySession to set. + */ + public void setLbStickySession(Boolean lbStickySession) { + this.lbStickySession = lbStickySession; + } + + /** + * @return Returns the worker. + */ + public String getWorker() { + return worker; + } + + /** + * @param worker + * The worker to set. + */ + public void setWorker(String worker) { + this.worker = worker; + } + + /** + * @return Returns the workerType. + */ + public String getWorkerType() { + return workerType; + } + + /** + * @param workerType + * The workerType to set. + */ + public void setWorkerType(String workerType) { + this.workerType = workerType; + } + + /** + * @return Returns the workerLb. + */ + public String getWorkerLb() { + return workerLb; + } + + /** + * @param workerLb + * The workerLb to set. + */ + public void setWorkerLb(String workerLb) { + this.workerLb = workerLb; + } + + /** + * @return Returns the workerClusterDomain. + */ + public String getWorkerClusterDomain() { + return workerClusterDomain; + } + + /** + * @param workerClusterDomain + * The workerClusterDomain to set. + */ + public void setWorkerClusterDomain(String workerClusterDomain) { + this.workerClusterDomain = workerClusterDomain; + } + + /** + * @return Returns the workerDisabled. + */ + public Boolean getWorkerDisabled() { + return workerDisabled; + } + + /** + * @param workerDisabled + * The workerDisabled to set. + */ + public void setWorkerDisabled(Boolean workerDisabled) { + this.workerDisabled = workerDisabled; + } + + /** + * @return Returns the workerStopped. + */ + public Boolean getWorkerStopped() { + return workerStopped; + } + + /** + * @param workerStopped The workerStopped to set. + */ + public void setWorkerStopped(Boolean workerStopped) { + this.workerStopped = workerStopped; + } + + /** + * @return Returns the workerLoadFactor. + */ + public Integer getWorkerLoadFactor() { + return workerLoadFactor; + } + + /** + * @param workerLoadFactor + * The workerLoadFactor to set. + */ + public void setWorkerLoadFactor(Integer workerLoadFactor) { + this.workerLoadFactor = workerLoadFactor; + } + + /** + * @return Returns the workerRedirect. + */ + public String getWorkerRedirect() { + return workerRedirect; + } + + /** + * @param workerRedirect + * The workerRedirect to set. + */ + public void setWorkerRedirect(String workerRedirect) { + this.workerRedirect = workerRedirect; + } + + /** + * Execute the requested operation. + * + * @exception BuildException + * if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + checkParameter(); + StringBuilder sb = createLink(); + execute(sb.toString(), null, null, -1); + + } + + /** + * Create JkStatus link + *

    + *
  • load balance example: + * http://localhost/status?cmd=update&mime=txt&w=lb&lf=false&ls=true
  • + *
  • worker example: + * http://localhost/status?cmd=update&mime=txt&w=node1&l=lb&wf=1&wd=false&ws=false + *
  • + *
+ * + * @return create jkstatus link + */ + private StringBuilder createLink() { + // Building URL + StringBuilder sb = new StringBuilder(); + try { + sb.append("?cmd=update&mime=txt"); + sb.append("&w="); + sb.append(URLEncoder.encode(worker, getCharset())); + + if (isLBMode) { + //http://localhost/status?cmd=update&mime=txt&w=lb&lf=false&ls=true + if ((lbRetries != null)) { // > 0 + sb.append("&lr="); + sb.append(lbRetries); + } + if ((lbRecovertime != null)) { // > 59 + sb.append("<="); + sb.append(lbRecovertime); + } + if ((lbStickySession != null)) { + sb.append("&ls="); + sb.append(lbStickySession); + } + if ((lbForceSession != null)) { + sb.append("&lf="); + sb.append(lbForceSession); + } + } else { + //http://localhost/status?cmd=update&mime=txt&w=node1&l=lb&wf=1&wd=false&ws=false + if ((workerLb != null)) { // must be configured + sb.append("&l="); + sb.append(URLEncoder.encode(workerLb, getCharset())); + } + if ((workerLoadFactor != null)) { // >= 1 + sb.append("&wf="); + sb.append(workerLoadFactor); + } + if ((workerDisabled != null)) { + sb.append("&wd="); + sb.append(workerDisabled); + } + if ((workerStopped != null)) { + sb.append("&ws="); + sb.append(workerStopped); + } + if ((workerRedirect != null)) { // other worker conrecte lb's + sb.append("&wr="); + } + if ((workerClusterDomain != null)) { + sb.append("&wc="); + sb.append(URLEncoder.encode(workerClusterDomain, + getCharset())); + } + } + + } catch (UnsupportedEncodingException e) { + throw new BuildException("Invalid 'charset' attribute: " + + getCharset()); + } + return sb; + } + + /** + * check correct lb and worker parameter + */ + protected void checkParameter() { + if (worker == null) { + throw new BuildException("Must specify 'worker' attribute"); + } + if (workerType == null) { + throw new BuildException("Must specify 'workerType' attribute"); + } + if ("lb".equals(workerType)) { + if (lbRecovertime == null && lbRetries == null) { + throw new BuildException( + "Must specify at a lb worker either 'lbRecovertime' or" + + "'lbRetries' attribute"); + } + if (lbStickySession == null || lbForceSession == null) { + throw new BuildException("Must specify at a lb worker either" + + "'lbStickySession' and 'lbForceSession' attribute"); + } + if (null != lbRecovertime && 60 < lbRecovertime.intValue()) { + throw new BuildException( + "The 'lbRecovertime' must be greater than 59"); + } + if (null != lbRetries && 1 < lbRetries.intValue()) { + throw new BuildException( + "The 'lbRetries' must be greater than 1"); + } + isLBMode = true; + } else if ("worker".equals(workerType)) { + if (workerDisabled == null) { + throw new BuildException( + "Must specify at a node worker 'workerDisabled' attribute"); + } + if (workerStopped == null) { + throw new BuildException( + "Must specify at a node worker 'workerStopped' attribute"); + } + if (workerLoadFactor == null ) { + throw new BuildException( + "Must specify at a node worker 'workerLoadFactor' attribute"); + } + if (workerClusterDomain == null) { + throw new BuildException( + "Must specify at a node worker 'workerClusterDomain' attribute"); + } + if (workerRedirect == null) { + throw new BuildException( + "Must specify at a node worker 'workerRedirect' attribute"); + } + if (workerLb == null) { + throw new BuildException("Must specify 'workerLb' attribute"); + } + if (workerLoadFactor.intValue() < 1) { + throw new BuildException( + "The 'workerLoadFactor' must be greater or equal 1"); + } + isLBMode = false; + } else { + throw new BuildException( + "Only 'lb' and 'worker' supported as workerType attribute"); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ant/JMXGetTask.java b/java/org/apache/catalina/ant/JMXGetTask.java new file mode 100644 index 0000000..fd0ba47 --- /dev/null +++ b/java/org/apache/catalina/ant/JMXGetTask.java @@ -0,0 +1,100 @@ +/* + * 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.catalina.ant; + + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the JMX Get command (/jmxproxy/?get) + * supported by the Tomcat manager application. + * + * @author Peter Rossbach + */ +public class JMXGetTask extends AbstractCatalinaTask { + + // Properties + + /** + * The full bean name + */ + protected String bean = null; + + /** + * The attribute you wish to alter + */ + protected String attribute = null; + + // Public Methods + + /** + * Get method for the bean name + * @return Bean name + */ + public String getBean () { + return this.bean; + } + + /** + * Set method for the bean name + * @param bean Bean name + */ + public void setBean (String bean) { + this.bean = bean; + } + + /** + * Get method for the attribute name + * @return Attribute name + */ + public String getAttribute () { + return this.attribute; + } + + /** + * Set method for the attribute name + * @param attribute Attribute name + */ + public void setAttribute (String attribute) { + this.attribute = attribute; + } + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + super.execute(); + if (bean == null || attribute == null) { + throw new BuildException("Must specify 'bean' and 'attribute' attributes"); + } + log("Getting attribute " + attribute + + " in bean " + bean ); + try { + execute("/jmxproxy/?get=" + URLEncoder.encode(bean, getCharset()) + + "&att=" + URLEncoder.encode(attribute, getCharset())); + } catch (UnsupportedEncodingException e) { + throw new BuildException("Invalid 'charset' attribute: " + getCharset()); + } + } +} diff --git a/java/org/apache/catalina/ant/JMXQueryTask.java b/java/org/apache/catalina/ant/JMXQueryTask.java new file mode 100644 index 0000000..d239efb --- /dev/null +++ b/java/org/apache/catalina/ant/JMXQueryTask.java @@ -0,0 +1,88 @@ +/* + * 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.catalina.ant; + + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the JMX Query command + * (/jmxproxy/?qry) supported by the Tomcat manager application. + * + * @author Vivek Chopra + */ +public class JMXQueryTask extends AbstractCatalinaTask { + + // Properties + + /** + * The JMX query string + * @see #setQuery(String) + */ + protected String query = null; + + // Public Methods + + /** + * Get method for the JMX query string + * @return Query string + */ + public String getQuery () { + return this.query; + } + + /** + * Set method for the JMX query string. + *

Examples of query format:

+ *
    + *
  • *:*
  • + *
  • *:type=RequestProcessor,*
  • + *
  • *:j2eeType=Servlet,*
  • + *
  • Catalina:type=Environment,resourcetype=Global,name=simpleValue
  • + *
+ * @param query JMX Query string + */ + public void setQuery (String query) { + this.query = query; + } + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + super.execute(); + String queryString; + if (query == null) { + queryString = ""; + } else { + try { + queryString = "?qry=" + URLEncoder.encode(query, getCharset()); + } catch (UnsupportedEncodingException e) { + throw new BuildException("Invalid 'charset' attribute: " + getCharset()); + } + } + log("Query string is " + queryString); + execute ("/jmxproxy/" + queryString); + } +} diff --git a/java/org/apache/catalina/ant/JMXSetTask.java b/java/org/apache/catalina/ant/JMXSetTask.java new file mode 100644 index 0000000..547669a --- /dev/null +++ b/java/org/apache/catalina/ant/JMXSetTask.java @@ -0,0 +1,123 @@ +/* + * 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.catalina.ant; + + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the JMX Set command (/jmxproxy/?set) + * supported by the Tomcat manager application. + * + * @author Vivek Chopra + */ +public class JMXSetTask extends AbstractCatalinaTask { + + // Properties + + /** + * The full bean name + */ + protected String bean = null; + + /** + * The attribute you wish to alter + */ + protected String attribute = null; + + /** + * The new value for the attribute + */ + protected String value = null; + + // Public Methods + + /** + * Get method for the bean name + * @return Bean name + */ + public String getBean () { + return this.bean; + } + + /** + * Set method for the bean name + * @param bean Bean name + */ + public void setBean (String bean) { + this.bean = bean; + } + + /** + * Get method for the attribute name + * @return Attribute name + */ + public String getAttribute () { + return this.attribute; + } + + /** + * Set method for the attribute name + * @param attribute Attribute name + */ + public void setAttribute (String attribute) { + this.attribute = attribute; + } + + /** + * Get method for the attribute value + * @return Attribute value + */ + public String getValue () { + return this.value; + } + + /** + * Set method for the attribute value. + * @param value Attribute value + */ + public void setValue (String value) { + this.value = value; + } + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + super.execute(); + if (bean == null || attribute == null || value == null) { + throw new BuildException("Must specify 'bean', 'attribute' and 'value' attributes"); + } + log("Setting attribute " + attribute + + " in bean " + bean + + " to " + value); + try { + execute("/jmxproxy/?set=" + URLEncoder.encode(bean, getCharset()) + + "&att=" + URLEncoder.encode(attribute, getCharset()) + + "&val=" + URLEncoder.encode(value, getCharset())); + } catch (UnsupportedEncodingException e) { + throw new BuildException("Invalid 'charset' attribute: " + getCharset()); + } + } +} diff --git a/java/org/apache/catalina/ant/ListTask.java b/java/org/apache/catalina/ant/ListTask.java new file mode 100644 index 0000000..eeab9cd --- /dev/null +++ b/java/org/apache/catalina/ant/ListTask.java @@ -0,0 +1,53 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /list command, supported by the + * Tomcat manager application. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public class ListTask extends AbstractCatalinaTask { + + + // ------------------------------------------------------------- Properties + + + // --------------------------------------------------------- Public Methods + + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute("/list"); + + } + + +} diff --git a/java/org/apache/catalina/ant/ReloadTask.java b/java/org/apache/catalina/ant/ReloadTask.java new file mode 100644 index 0000000..4497c6a --- /dev/null +++ b/java/org/apache/catalina/ant/ReloadTask.java @@ -0,0 +1,47 @@ +/* + * 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.catalina.ant; + + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /reload command, supported by the + * Tomcat manager application. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public class ReloadTask extends AbstractCatalinaCommandTask { + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute(createQueryString("/reload").toString()); + + } + + +} diff --git a/java/org/apache/catalina/ant/ResourcesTask.java b/java/org/apache/catalina/ant/ResourcesTask.java new file mode 100644 index 0000000..b8b6c46 --- /dev/null +++ b/java/org/apache/catalina/ant/ResourcesTask.java @@ -0,0 +1,80 @@ +/* + * 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.catalina.ant; + + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /resources command, supported by + * the Tomcat manager application. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public class ResourcesTask extends AbstractCatalinaTask { + + + // ------------------------------------------------------------- Properties + + + /** + * The fully qualified class name of the resource type being requested + * (if any). + */ + protected String type = null; + + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + if (type != null) { + try { + execute("/resources?type=" + + URLEncoder.encode(type, getCharset())); + } catch (UnsupportedEncodingException e) { + throw new BuildException("Invalid 'charset' attribute: " + getCharset()); + } + } else { + execute("/resources"); + } + + } + + +} diff --git a/java/org/apache/catalina/ant/ServerinfoTask.java b/java/org/apache/catalina/ant/ServerinfoTask.java new file mode 100644 index 0000000..f8c2ced --- /dev/null +++ b/java/org/apache/catalina/ant/ServerinfoTask.java @@ -0,0 +1,45 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /serverinfo command + * supported by the Tomcat manager application. + * + * @author Vivek Chopra + */ +public class ServerinfoTask extends AbstractCatalinaTask { + + // Public Methods + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute("/serverinfo"); + + } +} diff --git a/java/org/apache/catalina/ant/SessionsTask.java b/java/org/apache/catalina/ant/SessionsTask.java new file mode 100644 index 0000000..3649ceb --- /dev/null +++ b/java/org/apache/catalina/ant/SessionsTask.java @@ -0,0 +1,65 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /sessions command + * supported by the Tomcat manager application. + * + * @author Vivek Chopra + */ +public class SessionsTask extends AbstractCatalinaCommandTask { + + + protected String idle = null; + + public String getIdle() { + return this.idle; + } + + public void setIdle(String idle) { + this.idle = idle; + } + + @Override + public StringBuilder createQueryString(String command) { + StringBuilder buffer = super.createQueryString(command); + if (path != null && idle != null) { + buffer.append("&idle="); + buffer.append(this.idle); + } + return buffer; + } + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute(createQueryString("/sessions").toString()); + + } + +} diff --git a/java/org/apache/catalina/ant/SslConnectorCiphersTask.java b/java/org/apache/catalina/ant/SslConnectorCiphersTask.java new file mode 100644 index 0000000..45826a8 --- /dev/null +++ b/java/org/apache/catalina/ant/SslConnectorCiphersTask.java @@ -0,0 +1,45 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /sslConnectorCiphers command + * supported by the Tomcat manager application. + * + */ +public class SslConnectorCiphersTask extends AbstractCatalinaTask { + + // Public Methods + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute("/sslConnectorCiphers"); + + } + +} diff --git a/java/org/apache/catalina/ant/StartTask.java b/java/org/apache/catalina/ant/StartTask.java new file mode 100644 index 0000000..5eb3c46 --- /dev/null +++ b/java/org/apache/catalina/ant/StartTask.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /start command, supported by the + * Tomcat manager application. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public class StartTask extends AbstractCatalinaCommandTask { + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute(createQueryString("/start").toString()); + + } + + +} diff --git a/java/org/apache/catalina/ant/StopTask.java b/java/org/apache/catalina/ant/StopTask.java new file mode 100644 index 0000000..0ce0633 --- /dev/null +++ b/java/org/apache/catalina/ant/StopTask.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /stop command, supported by the + * Tomcat manager application. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public class StopTask extends AbstractCatalinaCommandTask { + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute(createQueryString("/stop").toString()); + + } + + +} diff --git a/java/org/apache/catalina/ant/ThreaddumpTask.java b/java/org/apache/catalina/ant/ThreaddumpTask.java new file mode 100644 index 0000000..007da33 --- /dev/null +++ b/java/org/apache/catalina/ant/ThreaddumpTask.java @@ -0,0 +1,45 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /threaddump command + * supported by the Tomcat manager application. + * + */ +public class ThreaddumpTask extends AbstractCatalinaTask { + + // Public Methods + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute("/threaddump"); + + } + +} diff --git a/java/org/apache/catalina/ant/UndeployTask.java b/java/org/apache/catalina/ant/UndeployTask.java new file mode 100644 index 0000000..15e8991 --- /dev/null +++ b/java/org/apache/catalina/ant/UndeployTask.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /undeploy command, supported by + * the Tomcat manager application. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public class UndeployTask extends AbstractCatalinaCommandTask { + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute(createQueryString("/undeploy").toString()); + + } + + +} diff --git a/java/org/apache/catalina/ant/ValidatorTask.java b/java/org/apache/catalina/ant/ValidatorTask.java new file mode 100644 index 0000000..81d3997 --- /dev/null +++ b/java/org/apache/catalina/ant/ValidatorTask.java @@ -0,0 +1,111 @@ +/* + * 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.catalina.ant; + + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +import org.apache.catalina.Globals; +import org.apache.tomcat.util.descriptor.DigesterFactory; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tools.ant.BuildException; +import org.xml.sax.InputSource; + + +/** + * Task for validating a web application deployment descriptor, using XML + * schema validation. + * + * @author Remy Maucherat + * @since 5.0 + */ +public class ValidatorTask extends BaseRedirectorHelperTask { + + + // ----------------------------------------------------- Instance Variables + + + // ------------------------------------------------------------- Properties + + + /** + * The path to the webapp directory. + */ + protected String path = null; + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Execute the specified command. This logic only performs the common + * attribute validation required by all subclasses; it does not perform + * any functional logic directly. + * + * @exception BuildException if a validation error occurs + */ + @Override + public void execute() throws BuildException { + + if (path == null) { + throw new BuildException("Must specify 'path'"); + } + + File file = new File(path, "WEB-INF/web.xml"); + if (!file.canRead()) { + throw new BuildException("Cannot find web.xml"); + } + + // Commons-logging likes having the context classloader set + Thread currentThread = Thread.currentThread(); + ClassLoader oldCL = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(ValidatorTask.class.getClassLoader()); + + // Called through trusted manager interface. If running under a + // SecurityManager assume that untrusted applications may be deployed. + Digester digester = DigesterFactory.newDigester( + true, true, null, Globals.IS_SECURITY_ENABLED); + try (InputStream stream = new BufferedInputStream(new FileInputStream(file.getCanonicalFile()))) { + InputSource is = new InputSource(file.toURI().toURL().toExternalForm()); + is.setByteStream(stream); + digester.parse(is); + handleOutput("web.xml validated"); + } catch (Exception e) { + if (isFailOnError()) { + throw new BuildException("Validation failure", e); + } else { + handleErrorOutput("Validation failure: " + e); + } + } finally { + currentThread.setContextClassLoader(oldCL); + closeRedirector(); + } + + } + + +} diff --git a/java/org/apache/catalina/ant/VminfoTask.java b/java/org/apache/catalina/ant/VminfoTask.java new file mode 100644 index 0000000..52394cc --- /dev/null +++ b/java/org/apache/catalina/ant/VminfoTask.java @@ -0,0 +1,45 @@ +/* + * 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.catalina.ant; + + +import org.apache.tools.ant.BuildException; + + +/** + * Ant task that implements the /vminfo command + * supported by the Tomcat manager application. + * + */ +public class VminfoTask extends AbstractCatalinaTask { + + // Public Methods + + /** + * Execute the requested operation. + * + * @exception BuildException if an error occurs + */ + @Override + public void execute() throws BuildException { + + super.execute(); + execute("/vminfo"); + + } + +} diff --git a/java/org/apache/catalina/ant/antlib.xml b/java/org/apache/catalina/ant/antlib.xml new file mode 100644 index 0000000..787c4fa --- /dev/null +++ b/java/org/apache/catalina/ant/antlib.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/org/apache/catalina/ant/catalina.tasks b/java/org/apache/catalina/ant/catalina.tasks new file mode 100644 index 0000000..eb901ed --- /dev/null +++ b/java/org/apache/catalina/ant/catalina.tasks @@ -0,0 +1,40 @@ +# 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. + +# Pure catalina tasks +list=org.apache.catalina.ant.ListTask +deploy=org.apache.catalina.ant.DeployTask +start=org.apache.catalina.ant.StartTask +reload=org.apache.catalina.ant.ReloadTask +stop=org.apache.catalina.ant.StopTask +undeploy=org.apache.catalina.ant.UndeployTask +resources=org.apache.catalina.ant.ResourcesTask +sessions=org.apache.catalina.ant.SessionsTask +validator=org.apache.catalina.ant.ValidatorTask +findleaks=org.apache.catalina.ant.FindLeaksTask +vminfo=org.apache.catalina.ant.VminfoTask +threaddump=org.apache.catalina.ant.ThreaddumpTask +sslConnectorCiphers=org.apache.catalina.ant.SslConnectorCiphersTask + +#Jk Task +jkupdate=org.apache.catalina.ant.JKStatusUpdateTask + +# Manager JMX +jmxManagerSet=org.apache.catalina.ant.JMXSetTask +jmxManagerGet=org.apache.catalina.ant.JMXGetTask +jmxManagerQuery=org.apache.catalina.ant.JMXQueryTask + +# Jasper tasks +jasper=org.apache.jasper.JspC diff --git a/java/org/apache/catalina/ant/jmx/Arg.java b/java/org/apache/catalina/ant/jmx/Arg.java new file mode 100644 index 0000000..dc47302 --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/Arg.java @@ -0,0 +1,39 @@ +/* + * 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.catalina.ant.jmx; + +public class Arg { + + private String type; + private String value; + + public void setType( String type) { + this.type=type; + } + + public void setValue( String value ) { + this.value=value; + } + + public String getValue() { + return value; + } + + public String getType() { + return type; + } +} diff --git a/java/org/apache/catalina/ant/jmx/JMXAccessorCondition.java b/java/org/apache/catalina/ant/jmx/JMXAccessorCondition.java new file mode 100644 index 0000000..37e2e6a --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/JMXAccessorCondition.java @@ -0,0 +1,229 @@ +/* + * 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.catalina.ant.jmx; + +import org.apache.tools.ant.BuildException; + +/** + * Definition: + *
+ *   <path id="catalina_ant">
+ *       <fileset dir="${catalina.home}/server/lib">
+ *           <include name="catalina-ant.jar"/>
+ *       </fileset>
+ *   </path>
+ *
+ *   <typedef
+ *       name="jmxCondition"
+ *       classname="org.apache.catalina.ant.jmx.JMXAccessorCondition"
+ *       classpathref="catalina_ant"/>
+ *   <taskdef
+ *       name="jmxOpen"
+ *       classname="org.apache.catalina.ant.jmx.JMXAccessorTask"
+ *       classpathref="catalina_ant"/>
+ * 
+ * + * Usage: Wait for start backup node + *
+ *     <target name="wait">
+ *       <jmxOpen
+ *               host="${jmx.host}" port="${jmx.port}" username="${jmx.username}" password="${jmx.password}" />
+ *        <waitfor maxwait="${maxwait}" maxwaitunit="second" timeoutproperty="server.timeout" >
+ *           <and>
+ *               <socket server="${server.name}" port="${server.port}"/>
+ *               <http url="${url}"/>
+ *               <jmxCondition
+ *                   name="Catalina:type=IDataSender,host=localhost,senderAddress=192.168.111.1,senderPort=9025"
+ *                   operation="=="
+ *                   attribute="connected" value="true"
+ *               />
+ *               <jmxCondition
+ *                   operation="&lt;"
+ *                   name="Catalina:j2eeType=WebModule,name=//${tomcat.application.host}${tomcat.application.path},J2EEApplication=none,J2EEServer=none"
+ *                   attribute="startupTime" value="250"
+ *               />
+ *           </and>
+ *       </waitfor>
+ *       <fail if="server.timeout" message="Server ${url} don't answer inside ${maxwait} sec" />
+ *       <echo message="Server ${url} alive" />
+ *   </target>
+ *
+ * 
+ * Allowed operation between jmx attribute and reference value: + *
    + *
  • == equals
  • + *
  • != not equals
  • + *
  • > greater than (&gt;)
  • + *
  • >= greater than or equals (&gt;=)
  • + *
  • < lesser than (&lt;)
  • + *
  • <= lesser than or equals (&lt;=)
  • + *
+ * NOTE: For numeric expressions the type must be set and use xml entities as operations.
+ * As type we currently support long and double. + * @author Peter Rossbach + * @since 5.5.10 + */ +public class JMXAccessorCondition extends JMXAccessorConditionBase { + + // ----------------------------------------------------- Instance Variables + + private String operation = "==" ; + private String type = "long" ; + private String unlessCondition; + private String ifCondition; + + + // ----------------------------------------------------- Properties + + /** + * @return Returns the operation. + */ + public String getOperation() { + return operation; + } + /** + * @param operation The operation to set. + */ + public void setOperation(String operation) { + this.operation = operation; + } + + /** + * @return Returns the type. + */ + public String getType() { + return type; + } + /** + * @param type The type to set. + */ + public void setType(String type) { + this.type = type; + } + + /** + * @return Returns the ifCondition. + */ + public String getIf() { + return ifCondition; + } + /** + * Only execute if a property of the given name exists in the current project. + * @param c property name + */ + public void setIf(String c) { + ifCondition = c; + } + + /** + * @return Returns the unlessCondition. + */ + public String getUnless() { + return unlessCondition; + } + /** + * Only execute if a property of the given name does not + * exist in the current project. + * @param c property name + */ + public void setUnless(String c) { + unlessCondition = c; + } + + /** + * test the if condition + * @return true if there is no if condition, or the named property exists + */ + protected boolean testIfCondition() { + if (ifCondition == null || ifCondition.isEmpty()) { + return true; + } + return getProject().getProperty(ifCondition) != null; + } + + /** + * test the unless condition + * @return true if there is no unless condition, + * or there is a named property but it doesn't exist + */ + protected boolean testUnlessCondition() { + if (unlessCondition == null || "".equals(unlessCondition)) { + return true; + } + return getProject().getProperty(unlessCondition) == null; + } + + /** + * This method evaluates the condition + * It support for operation ">,>=,<,<=" the types long and double. + * @return expression jmxValue operation value + */ + @Override + public boolean eval() { + String value = getValue(); + if (operation == null) { + throw new BuildException("operation attribute is not set"); + } + if (value == null) { + throw new BuildException("value attribute is not set"); + } + if ((getName() == null || getAttribute() == null)) { + throw new BuildException( + "Must specify an MBean name and attribute for condition"); + } + if (testIfCondition() && testUnlessCondition()) { + String jmxValue = accessJMXValue(); + if (jmxValue != null) { + String op = getOperation(); + if ("==".equals(op)) { + return jmxValue.equals(value); + } else if ("!=".equals(op)) { + return !jmxValue.equals(value); + } else { + if ("long".equals(type)) { + long jvalue = Long.parseLong(jmxValue); + long lvalue = Long.parseLong(value); + if (">".equals(op)) { + return jvalue > lvalue; + } else if (">=".equals(op)) { + return jvalue >= lvalue; + } else if ("<".equals(op)) { + return jvalue < lvalue; + } else if ("<=".equals(op)) { + return jvalue <= lvalue; + } + } else if ("double".equals(type)) { + double jvalue = Double.parseDouble(jmxValue); + double dvalue = Double.parseDouble(value); + if (">".equals(op)) { + return jvalue > dvalue; + } else if (">=".equals(op)) { + return jvalue >= dvalue; + } else if ("<".equals(op)) { + return jvalue < dvalue; + } else if ("<=".equals(op)) { + return jvalue <= dvalue; + } + } + } + } + return false; + } + return true; + } +} + diff --git a/java/org/apache/catalina/ant/jmx/JMXAccessorConditionBase.java b/java/org/apache/catalina/ant/jmx/JMXAccessorConditionBase.java new file mode 100644 index 0000000..27e30e6 --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/JMXAccessorConditionBase.java @@ -0,0 +1,182 @@ +/* + * 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.catalina.ant.jmx; + +import java.io.IOException; +import java.net.MalformedURLException; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.apache.tools.ant.ProjectComponent; +import org.apache.tools.ant.taskdefs.condition.Condition; + +public abstract class JMXAccessorConditionBase extends ProjectComponent implements Condition { + + private String url = null; + private String host = "localhost"; + private String port = "8050"; + private String password = null; + private String username = null; + private String name = null; + private String attribute; + private String value; + private String ref = "jmx.server" ; + + /** + * @return Returns the attribute. + */ + public String getAttribute() { + return attribute; + } + /** + * @param attribute The attribute to set. + */ + public void setAttribute(String attribute) { + this.attribute = attribute; + } + /** + * @return Returns the host. + */ + public String getHost() { + return host; + } + /** + * @param host The host to set. + */ + public void setHost(String host) { + this.host = host; + } + /** + * @return Returns the name. + */ + public String getName() { + return name; + } + /** + * @param objectName The name to set. + */ + public void setName(String objectName) { + this.name = objectName; + } + /** + * @return Returns the password. + */ + public String getPassword() { + return password; + } + /** + * @param password The password to set. + */ + public void setPassword(String password) { + this.password = password; + } + /** + * @return Returns the port. + */ + public String getPort() { + return port; + } + /** + * @param port The port to set. + */ + public void setPort(String port) { + this.port = port; + } + /** + * @return Returns the url. + */ + public String getUrl() { + return url; + } + /** + * @param url The url to set. + */ + public void setUrl(String url) { + this.url = url; + } + /** + * @return Returns the username. + */ + public String getUsername() { + return username; + } + /** + * @param username The username to set. + */ + public void setUsername(String username) { + this.username = username; + } + /** + * @return Returns the value. + */ + public String getValue() { + return value; + } + // The setter for the "value" attribute + public void setValue(String value) { + this.value = value; + } + + /** + * @return Returns the ref. + */ + public String getRef() { + return ref; + } + /** + * @param refId The ref to set. + */ + public void setRef(String refId) { + this.ref = refId; + } + + /** + * Get JMXConnection (default look at jmx.server project reference + * from jmxOpen Task). + * + * @return active JMXConnection + * @throws MalformedURLException Invalid URL for JMX server + * @throws IOException Connection error + */ + protected MBeanServerConnection getJMXConnection() + throws MalformedURLException, IOException { + return JMXAccessorTask.accessJMXConnection( + getProject(), + getUrl(), getHost(), + getPort(), getUsername(), getPassword(), ref); + } + + /** + * Get value from MBeans attribute. + * + * @return The value + */ + protected String accessJMXValue() { + try { + Object result = getJMXConnection().getAttribute( + new ObjectName(name), attribute); + if (result != null) { + return result.toString(); + } + } catch (Exception e) { + // ignore access or connection open errors + } + return null; + } +} + diff --git a/java/org/apache/catalina/ant/jmx/JMXAccessorCreateTask.java b/java/org/apache/catalina/ant/jmx/JMXAccessorCreateTask.java new file mode 100644 index 0000000..9c5fc96 --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/JMXAccessorCreateTask.java @@ -0,0 +1,179 @@ +/* + * 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.catalina.ant.jmx; + +import java.util.ArrayList; +import java.util.List; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.apache.tools.ant.BuildException; + +/** + * Create new MBean at JMX JSR 160 MBeans Server. + *
    + *
  • Create Mbeans
  • + *
  • Create Mbeans with parameter
  • + *
  • Create remote Mbeans with different classloader
  • + *
+ *

+ * Examples: + *
+ * create a new Mbean at jmx.server connection + *

+ *
+ *   <jmx:create
+ *           ref="jmx.server"
+ *           name="Catalina:type=MBeanFactory"
+ *           className="org.apache.catalina.mbeans.MBeanFactory"
+ *           classLoader="Catalina:type=ServerClassLoader,name=server">
+ *            <Arg value="org.apache.catalina.mbeans.MBeanFactory" />
+ *   </jmxCreate/>
+ * 
+ *

+ * WARNINGNot all Tomcat MBeans can create remotely and autoregister by its parents! + * Please, use the MBeanFactory operation to generate valves and realms. + *

+ *

+ * First call to a remote MBeanserver save the JMXConnection a reference jmx.server + *

+ * These tasks require Ant 1.6 or later interface. + * + * @author Peter Rossbach + * @since 5.5.12 + */ +public class JMXAccessorCreateTask extends JMXAccessorTask { + // ----------------------------------------------------- Instance Variables + + private String className; + private String classLoader; + private List args=new ArrayList<>(); + + // ------------------------------------------------------------- Properties + + /** + * @return Returns the classLoader. + */ + public String getClassLoader() { + return classLoader; + } + + /** + * @param classLoaderName The classLoader to set. + */ + public void setClassLoader(String classLoaderName) { + this.classLoader = classLoaderName; + } + + /** + * @return Returns the className. + */ + public String getClassName() { + return className; + } + + /** + * @param className The className to set. + */ + public void setClassName(String className) { + this.className = className; + } + + public void addArg(Arg arg ) { + args.add(arg); + } + + /** + * @return Returns the args. + */ + public List getArgs() { + return args; + } + /** + * @param args The args to set. + */ + public void setArgs(List args) { + this.args = args; + } + + // ------------------------------------------------------ protected Methods + + @Override + public String jmxExecute(MBeanServerConnection jmxServerConnection) + throws Exception { + + if (getName() == null) { + throw new BuildException("Must specify a 'name'"); + } + if ((className == null)) { + throw new BuildException( + "Must specify a 'className' for get"); + } + jmxCreate(jmxServerConnection, getName()); + return null; + } + + /** + * Create new MBean from ClassLoader identified by an ObjectName. + * + * @param jmxServerConnection Connection to the JMX server + * @param name MBean name + * @throws Exception Error creating MBean + */ + protected void jmxCreate(MBeanServerConnection jmxServerConnection, + String name) throws Exception { + Object argsA[] = null; + String sigA[] = null; + if (args != null) { + argsA = new Object[ args.size()]; + sigA = new String[args.size()]; + for( int i=0; i + * <path id="catalina_ant"> + * <fileset dir="${catalina.home}/server/lib"> + * <include name="catalina-ant.jar"/> + * </fileset> + * </path> + * + * <typedef + * name="jmxEquals" + * classname="org.apache.catalina.ant.jmx.JMXAccessorEqualsCondition" + * classpathref="catalina_ant"/> + * + * + * usage: Wait for start backup node + *
+ *     <target name="wait">
+ *        <waitfor maxwait="${maxwait}" maxwaitunit="second" timeoutproperty="server.timeout" >
+ *           <and>
+ *               <socket server="${server.name}" port="${server.port}"/>
+ *               <http url="${url}"/>
+ *               <jmxEquals
+ *                   host="localhost" port="9014" username="controlRole" password="tomcat"
+ *                   name="Catalina:type=IDataSender,host=localhost,senderAddress=192.168.111.1,senderPort=9025"
+ *                   attribute="connected" value="true"
+ *               />
+ *           </and>
+ *       </waitfor>
+ *       <fail if="server.timeout" message="Server ${url} don't answer inside ${maxwait} sec" />
+ *       <echo message="Server ${url} alive" />
+ *   </target>
+ *
+ * 
+ * + * @author Peter Rossbach + * @since 5.5.10 + */ +public class JMXAccessorEqualsCondition extends JMXAccessorConditionBase { + + @Override + public boolean eval() { + String value = getValue(); + + if (value == null) { + throw new BuildException("value attribute is not set"); + } + if (getName() == null || getAttribute() == null) { + throw new BuildException( + "Must specify an MBean name and attribute for equals condition"); + } + //FIXME check url or host/parameter + String jmxValue = accessJMXValue(); + if (jmxValue != null) { + return jmxValue.equals(value); + } + return false; + } +} + diff --git a/java/org/apache/catalina/ant/jmx/JMXAccessorGetTask.java b/java/org/apache/catalina/ant/jmx/JMXAccessorGetTask.java new file mode 100644 index 0000000..541abf2 --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/JMXAccessorGetTask.java @@ -0,0 +1,119 @@ +/* + * 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.catalina.ant.jmx; + + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.apache.tools.ant.BuildException; + + +/** + * Access JMX JSR 160 MBeans Server. + *
    + *
  • Get Mbeans attributes
  • + *
  • Show Get result as Ant console log
  • + *
  • Bind Get result as Ant properties
  • + *
+ *

+ * Examples: + *
+ * Get an Mbean IDataSender attribute nrOfRequests and create a new ant property IDataSender.9025.nrOfRequests + *

+ *
+ *   <jmx:get
+ *           ref="jmx.server"
+ *           name="Catalina:type=IDataSender,host=localhost,senderAddress=192.168.1.2,senderPort=9025"
+ *           attribute="nrOfRequests"
+ *           resultproperty="IDataSender.9025.nrOfRequests"
+ *           echo="false">
+ *       />
+ * 
+ *

+ * First call to a remote MBeanserver save the JMXConnection a referenz jmx.server + *

+ * These tasks require Ant 1.6 or later interface. + * + * @author Peter Rossbach + * @since 5.5.10 + */ +public class JMXAccessorGetTask extends JMXAccessorTask { + + + // ----------------------------------------------------- Instance Variables + + private String attribute; + + // ------------------------------------------------------------- Properties + + /** + * @return Returns the attribute. + */ + public String getAttribute() { + return attribute; + } + + /** + * @param attribute The attribute to set. + */ + public void setAttribute(String attribute) { + this.attribute = attribute; + } + + + // ------------------------------------------------------ protected Methods + + @Override + public String jmxExecute(MBeanServerConnection jmxServerConnection) + throws Exception { + + if (getName() == null) { + throw new BuildException("Must specify a 'name'"); + } + if ((attribute == null)) { + throw new BuildException( + "Must specify a 'attribute' for get"); + } + return jmxGet(jmxServerConnection, getName()); + } + + + /** + * Get property value. + * + * @param jmxServerConnection Connection to the JMX server + * @param name The MBean name + * @return The error message if any + * @throws Exception An error occurred + */ + protected String jmxGet(MBeanServerConnection jmxServerConnection, String name) throws Exception { + String error = null; + if(isEcho()) { + handleOutput("MBean " + name + " get attribute " + attribute ); + } + Object result = jmxServerConnection.getAttribute( + new ObjectName(name), attribute); + if (result != null) { + echoResult(attribute,result); + createProperty(result); + } else { + error = "Attribute " + attribute + " is empty"; + } + return error; + } +} diff --git a/java/org/apache/catalina/ant/jmx/JMXAccessorInvokeTask.java b/java/org/apache/catalina/ant/jmx/JMXAccessorInvokeTask.java new file mode 100644 index 0000000..daf5013 --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/JMXAccessorInvokeTask.java @@ -0,0 +1,181 @@ +/* + * 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.catalina.ant.jmx; + + +import java.util.ArrayList; +import java.util.List; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.apache.tools.ant.BuildException; + + +/** + * Access JMX JSR 160 MBeans Server. + *
    + *
  • open more then one JSR 160 rmi connection
  • + *
  • Get/Set Mbeans attributes
  • + *
  • Call Mbean Operation with arguments
  • + *
  • Argument values can be converted from string to int,long,float,double,boolean,ObjectName or InetAddress
  • + *
  • Query Mbeans
  • + *
  • Show Get, Call, Query result at Ant console log
  • + *
  • Bind Get, Call, Query result at Ant properties
  • + *
+ * + * Examples: + *
    + *
  • + * Get a session attribute hello from session with ref ${sessionid.0} form + * app Catalina:type=Manager,context=/ClusterTest,host=localhost + *
    + *   <jmx:invoke
    + *           name="Catalina:type=Manager,context=/ClusterTest,host=localhost"
    + *           operation="getSessionAttribute"
    + *           resultproperty="hello">
    + *         <arg value="${sessionid.0}"/>
    + *         <arg value="Hello"/>
    + *   </jmx:invoke>
    + * 
    + *
  • + *
  • + * Create new AccessLogger at localhost + * + * <jmx:invoke + * name="Catalina:type=MBeanFactory" + * operation="createAccessLoggerValve" + * resultproperty="accessLoggerObjectName" + * > + * <arg value="Catalina:type=Host,host=localhost"/> + * </jmx:invoke> + * + * + *
  • + *
  • + * Remove existing AccessLogger at localhost + * + * <jmx:invoke + * name="Catalina:type=MBeanFactory" + * operation="removeValve" + * > + * <arg value="Catalina:type=Valve,name=AccessLogValve,host=localhost"/> + * </jmx:invoke> + * + * + *
  • + *
+ *

+ * First call to a remote MBeanserver save the JMXConnection a referenz jmx.server + *

+ * These tasks require Ant 1.6 or later interface. + * + * @author Peter Rossbach + * @since 5.5.10 + */ +public class JMXAccessorInvokeTask extends JMXAccessorTask { + + + // ----------------------------------------------------- Instance Variables + + private String operation ; + private List args=new ArrayList<>(); + + // ------------------------------------------------------------- Properties + + /** + * @return Returns the operation. + */ + public String getOperation() { + return operation; + } + /** + * @param operation The operation to set. + */ + public void setOperation(String operation) { + this.operation = operation; + } + + public void addArg(Arg arg ) { + args.add(arg); + } + + /** + * @return Returns the args. + */ + public List getArgs() { + return args; + } + /** + * @param args The args to set. + */ + public void setArgs(List args) { + this.args = args; + } + + // ------------------------------------------------------ protected Methods + + @Override + public String jmxExecute(MBeanServerConnection jmxServerConnection) + throws Exception { + + if (getName() == null) { + throw new BuildException("Must specify a 'name'"); + } + if ((operation == null)) { + throw new BuildException( + "Must specify a 'operation' for call"); + } + return jmxInvoke(jmxServerConnection, getName()); + } + + /** + * Invoke specified operation. + * + * @param jmxServerConnection Connection to the JMX server + * @param name The MBean name + * @return null (no error message to report other than exception) + * @throws Exception An error occurred + */ + protected String jmxInvoke(MBeanServerConnection jmxServerConnection, String name) throws Exception { + Object result ; + if (args == null) { + result = jmxServerConnection.invoke(new ObjectName(name), operation, null, null); + } else { + Object argsA[]=new Object[ args.size()]; + String sigA[]=new String[args.size()]; + for( int i=0; i + *
  • open no existing JSR 160 rmi jmx connection
  • + *
  • Get all Mbeans attributes
  • + *
  • Get only the Query Mbeans ObjectNames
  • + *
  • Show query result as Ant console log
  • + *
  • Bind query result as Ant properties
  • + * + *
    + * Query a list of Mbeans. + *
    + *   <jmxQuery
    + *           host="127.0.0.1"
    + *           port="9014"
    + *           name="Catalina:type=Manager,*
    + *           resultproperty="manager" />
    + * 
    + * with attribute attributebinding="true" you can get + * all attributes also from result objects.
    + * The property manager.length show the size of the result + * and with manager.[0..length].name the + * resulted ObjectNames are saved. + * These tasks require Ant 1.6 or later interface. + * + * @author Peter Rossbach + * @since 5.5.10 + */ +public class JMXAccessorQueryTask extends JMXAccessorTask { + + // ----------------------------------------------------- Instance Variables + + private boolean attributebinding = false; + + // ------------------------------------------------------------- Properties + + /** + * @return Returns the attributebinding. + */ + public boolean isAttributebinding() { + return attributebinding; + } + /** + * @param attributeBinding The attributebinding to set. + */ + public void setAttributebinding(boolean attributeBinding) { + this.attributebinding = attributeBinding; + } + + // ------------------------------------------------------ protected Methods + + + @Override + public String jmxExecute(MBeanServerConnection jmxServerConnection) + throws Exception { + + if (getName() == null) { + throw new BuildException("Must specify a 'name'"); + } + return jmxQuery(jmxServerConnection, getName()); + + } + + + /** + * Call Mbean server for some mbeans with same domain, attributes. + * with attributebinding=true you can save all attributes from all found objects + * + * @param jmxServerConnection Connection to the JMX server + * @param qry The query + * @return null (no error message to report other than exception) + */ + protected String jmxQuery(MBeanServerConnection jmxServerConnection, String qry) { + String isError = null; + Set names = null; + String resultproperty = getResultproperty(); + try { + names = jmxServerConnection.queryNames(new ObjectName(qry), null); + if (resultproperty != null) { + setProperty(resultproperty + ".Length",Integer.toString(names.size())); + } + } catch (Exception e) { + if (isEcho()) { + handleErrorOutput(e.getMessage()); + } + return "Can't query mbeans " + qry; + } + + if (resultproperty != null) { + int oindex = 0; + String pname = null; + for (ObjectName oname : names) { + pname = resultproperty + "." + Integer.toString(oindex) + "."; + oindex++; + setProperty(pname + "Name", oname.toString()); + if (isAttributebinding()) { + bindAttributes(jmxServerConnection, pname, oname); + } + } + } + return isError; + } + + protected void bindAttributes(MBeanServerConnection jmxServerConnection, String pname, ObjectName oname) { + try { + MBeanInfo minfo = jmxServerConnection.getMBeanInfo(oname); + MBeanAttributeInfo attrs[] = minfo.getAttributes(); + Object value = null; + + for (MBeanAttributeInfo attr : attrs) { + if (!attr.isReadable()) { + continue; + } + String attName = attr.getName(); + if (attName.indexOf('=') >= 0 || attName.indexOf(':') >= 0 || attName.indexOf(' ') >= 0) { + continue; + } + + try { + value = jmxServerConnection.getAttribute(oname, attName); + } catch (Exception e) { + if (isEcho()) { + handleErrorOutput( + "Error getting attribute " + oname + " " + pname + attName + " " + e.toString()); + } + continue; + } + if ((value == null) || "modelerType".equals(attName)) { + continue; + } + createProperty(pname + attName, value); + } + } catch (Exception e) { + // Ignore + } + } +} diff --git a/java/org/apache/catalina/ant/jmx/JMXAccessorSetTask.java b/java/org/apache/catalina/ant/jmx/JMXAccessorSetTask.java new file mode 100644 index 0000000..c870f92 --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/JMXAccessorSetTask.java @@ -0,0 +1,195 @@ +/* + * 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.catalina.ant.jmx; + + +import javax.management.Attribute; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.apache.tools.ant.BuildException; + + +/** + * Access JMX JSR 160 MBeans Server. + *
      + *
    • Get Mbeans attributes
    • + *
    • Show Get result as Ant console log
    • + *
    • Bind Get result as Ant properties
    • + *
    + *

    + * Examples: + * Set an Mbean Manager attribute maxActiveSessions. + * Set this attribute with fresh jmx connection without save reference + *

    + *
    + *   <jmx:set
    + *           host="127.0.0.1"
    + *           port="9014"
    + *           ref=""
    + *           name="Catalina:type=Manager,context="/ClusterTest",host=localhost"
    + *           attribute="maxActiveSessions"
    + *           value="100"
    + *           type="int"
    + *           echo="false">
    + *       />
    + * 
    + *

    + * First call to a remote MBeanserver save the JMXConnection a referenz jmx.server + *

    + * These tasks require Ant 1.6 or later interface. + * + * @author Peter Rossbach + * @since 5.5.10 + */ +public class JMXAccessorSetTask extends JMXAccessorTask { + + // ----------------------------------------------------- Instance Variables + + private String attribute; + private String value; + private String type; + private boolean convert = false ; + + // ------------------------------------------------------------- Properties + + /** + * @return Returns the attribute. + */ + public String getAttribute() { + return attribute; + } + + /** + * @param attribute The attribute to set. + */ + public void setAttribute(String attribute) { + this.attribute = attribute; + } + + /** + * @return Returns the value. + */ + public String getValue() { + return value; + } + /** + * @param value The value to set. + */ + public void setValue(String value) { + this.value = value; + } + + + /** + * @return Returns the type. + */ + public String getType() { + return type; + } + + /** + * @param valueType The type to set. + */ + public void setType(String valueType) { + this.type = valueType; + } + + + /** + * @return Returns the convert. + */ + public boolean isConvert() { + return convert; + } + /** + * @param convert The convert to set. + */ + public void setConvert(boolean convert) { + this.convert = convert; + } + // ------------------------------------------------------ protected Methods + + @Override + public String jmxExecute(MBeanServerConnection jmxServerConnection) + throws Exception { + + if (getName() == null) { + throw new BuildException("Must specify a 'name'"); + } + if ((attribute == null || value == null)) { + throw new BuildException( + "Must specify a 'attribute' and 'value' for set"); + } + return jmxSet(jmxServerConnection, getName()); + } + + /** + * Set property value. + * + * @param jmxServerConnection Connection to the JMX server + * @param name The MBean name + * @return null (no error message to report other than exception) + * @throws Exception An error occurred + */ + protected String jmxSet(MBeanServerConnection jmxServerConnection, + String name) throws Exception { + Object realValue; + if (type != null) { + realValue = convertStringToType(value, type); + } else { + if (isConvert()) { + String mType = getMBeanAttributeType(jmxServerConnection, name, + attribute); + realValue = convertStringToType(value, mType); + } else { + realValue = value; + } + } + jmxServerConnection.setAttribute(new ObjectName(name), new Attribute( + attribute, realValue)); + return null; + } + + + /** + * Get MBean Attribute from Mbean Server + * + * @param jmxServerConnection The JMX connection name + * @param name The MBean name + * @param attribute The attribute name + * @return The type of the attribute + * @throws Exception An error occurred + */ + protected String getMBeanAttributeType( + MBeanServerConnection jmxServerConnection, + String name, + String attribute) throws Exception { + ObjectName oname = new ObjectName(name); + String mattrType = null; + MBeanInfo minfo = jmxServerConnection.getMBeanInfo(oname); + MBeanAttributeInfo attrs[] = minfo.getAttributes(); + for (int i = 0; mattrType == null && i < attrs.length; i++) { + if (attribute.equals(attrs[i].getName())) { + mattrType = attrs[i].getType(); + } + } + return mattrType; + } +} diff --git a/java/org/apache/catalina/ant/jmx/JMXAccessorTask.java b/java/org/apache/catalina/ant/jmx/JMXAccessorTask.java new file mode 100644 index 0000000..c03d543 --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/JMXAccessorTask.java @@ -0,0 +1,722 @@ +/* + * 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.catalina.ant.jmx; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; + +import javax.management.MBeanServerConnection; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; +import javax.management.openmbean.TabularDataSupport; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import org.apache.catalina.ant.BaseRedirectorHelperTask; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; + +/** + * Access JMX JSR 160 MBeans Server. + *
      + *
    • open more then one JSR 160 rmi connection
    • + *
    • Get/Set Mbeans attributes
    • + *
    • Call Mbean Operation with arguments
    • + *
    • Argument values can be converted from string to + * int,long,float,double,boolean,ObjectName or InetAddress
    • + *
    • Query Mbeans
    • + *
    • Show Get, Call, Query result at Ant console log
    • + *
    • Bind Get, Call, Query result at Ant properties
    • + *
    + * + * Examples: open server with reference and authorisation + * + *
    + *
    + *    <jmxOpen
    + *            host="127.0.0.1"
    + *            port="9014"
    + *            username="monitorRole"
    + *            password="mysecret"
    + *            ref="jmx.myserver"
    + *        />
    + *
    + * 
    + * + * All calls after opening with same refid reuse the connection. + *

    + * First call to a remote MBeanserver save the JMXConnection a referenz + * jmx.server + *

    + * All JMXAccessorXXXTask support the attribute if and + * unless. With if the task is only execute when property + * exist and with unless when property not exists.
    NOTE + * : These tasks require Ant 1.6 or later interface. + * + * @author Peter Rossbach + * @since 5.5.10 + */ +public class JMXAccessorTask extends BaseRedirectorHelperTask { + + public static final String JMX_SERVICE_PREFIX = "service:jmx:rmi:///jndi/rmi://"; + + public static final String JMX_SERVICE_SUFFIX = "/jmxrmi"; + + // ----------------------------------------------------- Instance Variables + + private String name = null; + + private String resultproperty; + + private String url = null; + + private String host = "localhost"; + + private String port = "8050"; + + private String password = null; + + private String username = null; + + private String ref = "jmx.server"; + + private boolean echo = false; + + private boolean separatearrayresults = true; + + private String delimiter; + + private String unlessCondition; + + private String ifCondition; + + private final Properties properties = new Properties(); + + // ------------------------------------------------------------- Properties + + /** + * Get the name used at remote MbeanServer. + * + * @return the name used at remote MbeanServer + */ + public String getName() { + return this.name; + } + + public void setName(String objectName) { + this.name = objectName; + } + + /** + * @return Returns the resultproperty. + */ + public String getResultproperty() { + return resultproperty; + } + + /** + * @param propertyName The resultproperty to set. + */ + public void setResultproperty(String propertyName) { + this.resultproperty = propertyName; + } + + /** + * @return Returns the delimiter. + */ + public String getDelimiter() { + return delimiter; + } + + /** + * @param separator The delimiter to set. + */ + public void setDelimiter(String separator) { + this.delimiter = separator; + } + + /** + * @return Returns the echo. + */ + public boolean isEcho() { + return echo; + } + + /** + * @param echo + * The echo to set. + */ + public void setEcho(boolean echo) { + this.echo = echo; + } + + /** + * @return Returns the separatearrayresults. + */ + public boolean isSeparatearrayresults() { + return separatearrayresults; + } + + /** + * @param separateArrayResults + * The separatearrayresults to set. + */ + public void setSeparatearrayresults(boolean separateArrayResults) { + this.separatearrayresults = separateArrayResults; + } + + /** + * @return The login password for the Manager application. + */ + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + /** + * @return The login username for the JMX MBeanServer. + */ + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + /** + * @return The URL of the JMX JSR 160 MBeanServer to be used. + */ + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + /** + * @return The Host of the JMX JSR 160 MBeanServer to be used. + */ + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + /** + * @return The Port of the JMX JSR 160 MBeanServer to be used. + */ + public String getPort() { + return this.port; + } + + public void setPort(String port) { + this.port = port; + } + + /** + * @return Returns the useRef. + */ + public boolean isUseRef() { + return ref != null && !ref.isEmpty(); + } + + /** + * @return Returns the ref. + */ + public String getRef() { + return ref; + } + + /** + * @param refId The ref to set. + */ + public void setRef(String refId) { + this.ref = refId; + } + + /** + * @return Returns the ifCondition. + */ + public String getIf() { + return ifCondition; + } + + /** + * Only execute if a property of the given name exists in the current + * project. + * + * @param c property name + */ + public void setIf(String c) { + ifCondition = c; + } + + /** + * @return Returns the unlessCondition. + */ + public String getUnless() { + return unlessCondition; + } + + /** + * Only execute if a property of the given name does not exist in the + * current project. + * + * @param c property name + */ + public void setUnless(String c) { + unlessCondition = c; + } + + // --------------------------------------------------------- Public Methods + + /** + * Execute the specified command. This logic only performs the common + * attribute validation required by all subclasses; it does not perform any + * functional logic directly. + * + * @exception BuildException + * if a validation error occurs + */ + @Override + public void execute() throws BuildException { + if (testIfCondition() && testUnlessCondition()) { + try { + String error = null; + + MBeanServerConnection jmxServerConnection = getJMXConnection(); + error = jmxExecute(jmxServerConnection); + if (error != null && isFailOnError()) { + // exception should be thrown only if failOnError == true + // or error line will be logged twice + throw new BuildException(error); + } + } catch (Exception e) { + if (isFailOnError()) { + throw new BuildException(e); + } else { + handleErrorOutput(e.getMessage()); + } + } finally { + closeRedirector(); + } + } + } + + /** + * Create a new JMX Connection with auth when username and password is set. + * + * @param url URL to be used for the JMX connection + * (if specified, it is a complete URL so host and port will not + * be used) + * @param host Host name of the JMX server + * @param port Port number for the JMX server + * @param username User name for the connection + * @param password Credentials corresponding to the specified user + * @throws MalformedURLException Invalid URL specified + * @throws IOException Other connection error + * @return the JMX connection + */ + public static MBeanServerConnection createJMXConnection(String url, + String host, String port, String username, String password) + throws MalformedURLException, IOException { + String urlForJMX; + if (url != null) { + urlForJMX = url; + } else { + urlForJMX = JMX_SERVICE_PREFIX + host + ":" + port + JMX_SERVICE_SUFFIX; + } + Map environment = null; + if (username != null && password != null) { + String[] credentials = new String[2]; + credentials[0] = username; + credentials[1] = password; + environment = new HashMap<>(); + environment.put(JMXConnector.CREDENTIALS, credentials); + } + return JMXConnectorFactory.connect(new JMXServiceURL(urlForJMX), + environment).getMBeanServerConnection(); + + } + + /** + * test the if condition + * + * @return true if there is no if condition, or the named property exists + */ + protected boolean testIfCondition() { + if (ifCondition == null || "".equals(ifCondition)) { + return true; + } + return getProperty(ifCondition) != null; + } + + /** + * test the unless condition + * + * @return true if there is no unless condition, or there is a named + * property but it doesn't exist + */ + protected boolean testUnlessCondition() { + if (unlessCondition == null || "".equals(unlessCondition)) { + return true; + } + return getProperty(unlessCondition) == null; + } + + /** + * Get Current Connection from ref parameter or create a new one! + * + * @param project The Ant project + * @param url URL to be used for the JMX connection + * (if specified, it is a complete URL so host and port will not + * be used) + * @param host Host name of the JMX server + * @param port Port number for the JMX server + * @param username User name for the connection + * @param password Credentials corresponding to the specified user + * @param refId The Id of the reference to retrieve in the project + * @throws MalformedURLException Invalid URL specified + * @throws IOException Other connection error + * @return the JMX connection + */ + @SuppressWarnings("null") + public static MBeanServerConnection accessJMXConnection(Project project, + String url, String host, String port, String username, + String password, String refId) throws MalformedURLException, + IOException { + MBeanServerConnection jmxServerConnection = null; + boolean isRef = project != null && refId != null && refId.length() > 0; + if (isRef) { + Object pref = project.getReference(refId); + try { + jmxServerConnection = (MBeanServerConnection) pref; + } catch (ClassCastException cce) { + project.log("wrong object reference " + refId + " - " + + pref.getClass()); + return null; + } + } + if (jmxServerConnection == null) { + jmxServerConnection = createJMXConnection(url, host, port, + username, password); + } + if (isRef && jmxServerConnection != null) { + project.addReference(refId, jmxServerConnection); + } + return jmxServerConnection; + } + + // ------------------------------------------------------ protected Methods + + /** + * get JMXConnection + * + * @throws MalformedURLException Invalid URL specified + * @throws IOException Other connection error + * @return the JMX connection + */ + protected MBeanServerConnection getJMXConnection() + throws MalformedURLException, IOException { + + MBeanServerConnection jmxServerConnection = null; + if (isUseRef()) { + Object pref = null ; + if(getProject() != null) { + pref = getProject().getReference(getRef()); + if (pref != null) { + try { + jmxServerConnection = (MBeanServerConnection) pref; + } catch (ClassCastException cce) { + getProject().log( + "Wrong object reference " + getRef() + " - " + + pref.getClass()); + return null; + } + } + } + if (jmxServerConnection == null) { + jmxServerConnection = accessJMXConnection(getProject(), + getUrl(), getHost(), getPort(), getUsername(), + getPassword(), getRef()); + } + } else { + jmxServerConnection = accessJMXConnection(getProject(), getUrl(), + getHost(), getPort(), getUsername(), getPassword(), null); + } + return jmxServerConnection; + } + + /** + * Execute the specified command, based on the configured properties. The + * input stream will be closed upon completion of this task, whether it was + * executed successfully or not. + * + * @param jmxServerConnection The JMX connection that should be used + * @return An error message string in some situations + * @exception Exception if an error occurs + */ + public String jmxExecute(MBeanServerConnection jmxServerConnection) + throws Exception { + + if ((jmxServerConnection == null)) { + throw new BuildException("Must open a connection!"); + } else if (isEcho()) { + handleOutput("JMX Connection ref=" + ref + " is open!"); + } + return null; + } + + /** + * Convert string to datatype FIXME How we can transfer values from ant + * project reference store (ref)? + * + * @param value The value + * @param valueType The type + * @return The converted object + */ + protected Object convertStringToType(String value, String valueType) { + if ("java.lang.String".equals(valueType)) { + return value; + } + + Object convertValue = value; + if ("java.lang.Integer".equals(valueType) || "int".equals(valueType)) { + try { + convertValue = Integer.valueOf(value); + } catch (NumberFormatException ex) { + if (isEcho()) { + handleErrorOutput("Unable to convert to integer:" + value); + } + } + } else if ("java.lang.Long".equals(valueType) + || "long".equals(valueType)) { + try { + convertValue = Long.valueOf(value); + } catch (NumberFormatException ex) { + if (isEcho()) { + handleErrorOutput("Unable to convert to long:" + value); + } + } + } else if ("java.lang.Boolean".equals(valueType) + || "boolean".equals(valueType)) { + convertValue = Boolean.valueOf(value); + } else if ("java.lang.Float".equals(valueType) + || "float".equals(valueType)) { + try { + convertValue = Float.valueOf(value); + } catch (NumberFormatException ex) { + if (isEcho()) { + handleErrorOutput("Unable to convert to float:" + value); + } + } + } else if ("java.lang.Double".equals(valueType) + || "double".equals(valueType)) { + try { + convertValue = Double.valueOf(value); + } catch (NumberFormatException ex) { + if (isEcho()) { + handleErrorOutput("Unable to convert to double:" + value); + } + } + } else if ("javax.management.ObjectName".equals(valueType) + || "name".equals(valueType)) { + try { + convertValue = new ObjectName(value); + } catch (MalformedObjectNameException e) { + if (isEcho()) { + handleErrorOutput("Unable to convert to ObjectName:" + value); + } + } + } else if ("java.net.InetAddress".equals(valueType)) { + try { + convertValue = InetAddress.getByName(value); + } catch (UnknownHostException exc) { + if (isEcho()) { + handleErrorOutput("Unable to resolve host name:" + value); + } + } + } + return convertValue; + } + + /** + * @param name context of result + * @param result The result + */ + protected void echoResult(String name, Object result) { + if (isEcho()) { + if (result.getClass().isArray()) { + for (int i = 0; i < Array.getLength(result); i++) { + handleOutput(name + "." + i + "=" + Array.get(result, i)); + } + } else { + handleOutput(name + "=" + result); + } + } + } + + /** + * create result as property with name from attribute resultproperty + * + * @param result The result + * @see #createProperty(String, Object) + */ + protected void createProperty(Object result) { + if (resultproperty != null) { + createProperty(resultproperty, result); + } + } + + /** + * create result as property with name from property prefix When result is + * an array and isSeparateArrayResults is true, resultproperty used as + * prefix (resultproperty.0-array.length and store the + * result array length at resultproperty.length. Other + * option is that you delimit your result with a delimiter + * (java.util.StringTokenizer is used). + * + * @param propertyPrefix Prefix for the property + * @param result The result + */ + protected void createProperty(String propertyPrefix, Object result) { + if (propertyPrefix == null) { + propertyPrefix = ""; + } + if (result instanceof CompositeDataSupport) { + CompositeDataSupport data = (CompositeDataSupport) result; + CompositeType compositeType = data.getCompositeType(); + Set keys = compositeType.keySet(); + for (String key : keys) { + Object value = data.get(key); + OpenType type = compositeType.getType(key); + if (type instanceof SimpleType) { + setProperty(propertyPrefix + "." + key, value); + } else { + createProperty(propertyPrefix + "." + key, value); + } + } + } else if (result instanceof TabularDataSupport) { + TabularDataSupport data = (TabularDataSupport) result; + for (Object key : data.keySet()) { + for (Object key1 : ((List) key)) { + CompositeData valuedata = data.get(new Object[] { key1 }); + Object value = valuedata.get("value"); + OpenType type = valuedata.getCompositeType().getType( + "value"); + if (type instanceof SimpleType) { + setProperty(propertyPrefix + "." + key1, value); + } else { + createProperty(propertyPrefix + "." + key1, value); + } + } + } + } else if (result.getClass().isArray()) { + if (isSeparatearrayresults()) { + int size = 0; + for (int i = 0; i < Array.getLength(result); i++) { + if (setProperty(propertyPrefix + "." + size, Array.get( + result, i))) { + size++; + } + } + if (size > 0) { + setProperty(propertyPrefix + ".Length", Integer + .toString(size)); + } + } + } else { + String delim = getDelimiter(); + if (delim != null) { + StringTokenizer tokenizer = new StringTokenizer(result + .toString(), delim); + int size = 0; + for (; tokenizer.hasMoreTokens();) { + String token = tokenizer.nextToken(); + if (setProperty(propertyPrefix + "." + size, token)) { + size++; + } + } + if (size > 0) { + setProperty(propertyPrefix + ".Length", Integer.toString(size)); + } + } else { + setProperty(propertyPrefix, result.toString()); + } + } + } + + /** + * Get Property + * @param property name + * @return The property value + */ + public String getProperty(String property) { + Project currentProject = getProject(); + if (currentProject != null) { + return currentProject.getProperty(property); + } else { + return properties.getProperty(property); + } + } + + /** + * @param property The property + * @param value The value + * @return True if successful + */ + public boolean setProperty(String property, Object value) { + if (property != null) { + if (value == null) { + value = ""; + } + if (isEcho()) { + handleOutput(property + "=" + value.toString()); + } + Project currentProject = getProject(); + if (currentProject != null) { + currentProject.setNewProperty(property, value.toString()); + } else { + properties.setProperty(property, value.toString()); + } + return true; + } + return false; + } +} diff --git a/java/org/apache/catalina/ant/jmx/JMXAccessorUnregisterTask.java b/java/org/apache/catalina/ant/jmx/JMXAccessorUnregisterTask.java new file mode 100644 index 0000000..1226d5e --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/JMXAccessorUnregisterTask.java @@ -0,0 +1,85 @@ +/* + * 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.catalina.ant.jmx; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.apache.tools.ant.BuildException; + +/** + * unregister an MBean at JMX JSR 160 MBeans Server. + *
      + *
    • unregister Mbeans
    • + *
    + *

    + * Examples: + *
    + * unregister an existing Mbean at jmx.server connection + *

    + *
    + *   <jmx:unregister
    + *           ref="jmx.server"
    + *           name="Catalina:type=MBeanFactory" />
    + * 
    + *

    + * WARNINGNot all Tomcat MBeans can successfully unregister remotely. The mbean + * unregistration don't remove valves, realm, .. from parent class. + * Please, use the MBeanFactory operation to remove valves and realms. + *

    + *

    + * First call to a remote MBeanserver save the JMXConnection a reference jmx.server + *

    + * These tasks require Ant 1.6 or later interface. + * + * @author Peter Rossbach + * @since 5.5.12 + */ +public class JMXAccessorUnregisterTask extends JMXAccessorTask { + + // ------------------------------------------------------ protected Methods + + @Override + public String jmxExecute(MBeanServerConnection jmxServerConnection) + throws Exception { + + if (getName() == null) { + throw new BuildException("Must specify a 'name'"); + } + return jmxUuregister(jmxServerConnection, getName()); + } + + + /** + * Unregister MBean. + * + * @param jmxServerConnection Connection to the JMX server + * @param name The MBean name + * @return null (no error message to report other than exception) + * @throws Exception An error occurred + */ + protected String jmxUuregister(MBeanServerConnection jmxServerConnection,String name) throws Exception { + String error = null; + if(isEcho()) { + handleOutput("Unregister MBean " + name ); + } + jmxServerConnection.unregisterMBean( + new ObjectName(name)); + return error; + } + +} diff --git a/java/org/apache/catalina/ant/jmx/antlib.xml b/java/org/apache/catalina/ant/jmx/antlib.xml new file mode 100644 index 0000000..3ba8d2e --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/antlib.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/org/apache/catalina/ant/jmx/jmxaccessor.tasks b/java/org/apache/catalina/ant/jmx/jmxaccessor.tasks new file mode 100644 index 0000000..05a2a8f --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/jmxaccessor.tasks @@ -0,0 +1,25 @@ +# 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. + +# JMX +jmxOpen=org.apache.catalina.ant.jmx.JMXAccessorTask +jmxSet=org.apache.catalina.ant.jmx.JMXAccessorSetTask +jmxGet=org.apache.catalina.ant.jmx.JMXAccessorGetTask +jmxInvoke=org.apache.catalina.ant.jmx.JMXAccessorInvokeTask +jmxQuery=org.apache.catalina.ant.jmx.JMXAccessorQueryTask +jmxCreate=org.apache.catalina.ant.jmx.JMXAccessorCreateTask +jmxUnregister=org.apache.catalina.ant.jmx.JMXAccessorUnregisterTask +jmxEquals=org.apache.catalina.ant.jmx.JMXAccessorEqualsCondition +jmxCondition=org.apache.catalina.ant.jmx.JMXAccessorCondition diff --git a/java/org/apache/catalina/ant/jmx/package.html b/java/org/apache/catalina/ant/jmx/package.html new file mode 100644 index 0000000..e05ece3 --- /dev/null +++ b/java/org/apache/catalina/ant/jmx/package.html @@ -0,0 +1,77 @@ + + + +

    This package contains a set of JMX Task implementations for +Ant (version 1.6 or later) that can be used to interact with the +Remote JMX JSR 160 RMI Adaptor to get/set attributes, invoke MBean operations +and query for Mbeans inside a running instance of Tomcat. For more information, see + +https://tomcat.apache.org/tomcat-10.1-doc/monitoring.html.

    + +

    Each task element can open a new jmx connection or reference an +existing one. The following attribute are exists in every tasks:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Common task attributes
    AttributeDescription
    url + The JMX Connection URL of the remote Tomcat MBeansServer. +
    username + The username of an MBeanServer auth, when configured. +
    password + The password of an MBeanServer auth, when configured. +
    host + The JMX Connection host. +
    port + The JMX Connection port. +
    ref + The name of the ant internal reference for a jmx connection. +
    + +

    NOTE - This Tasks only work, +when JSR 160 MBean Adaptor as remote jvm is configured.

    + + diff --git a/java/org/apache/catalina/ant/package.html b/java/org/apache/catalina/ant/package.html new file mode 100644 index 0000000..830afc8 --- /dev/null +++ b/java/org/apache/catalina/ant/package.html @@ -0,0 +1,103 @@ + + + +

    This package contains a set of Task implementations for +Ant (version 1.6.x or later) that can be used to interact with the +Manager application to deploy, undeploy, list, reload, start and stop web applications +from a running instance of Tomcat. For more information, see + +https://tomcat.apache.org/tomcat-10.1-doc/manager-howto.html.

    + +

    The attributes of each task element correspond +exactly to the request parameters that are included with an HTTP request +sent directly to the Manager application. They are summarized as follows: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Task attributes
    AttributeDescription
    url + The URL of the Manager web application you will use to + perform the requested operations. If not specified, defaults to + http://localhost:8080/manager/text (which corresponds + to a standard installation of Tomcat 7). +
    username + The username of a Tomcat user that has been configured with the + manager-script role, as required to execute Manager + application commands. This attribute is required. +
    password + The password of a Tomcat user that has been configured with the + manager-script role, as required to execute Manager + application commands. This attribute is required. +
    config + A URL pointing at the context configuration file (i.e. a file + containing only the <Context> element, and + its nested elements, from server.xml for a particular + web application). This attribute is supported only on the + install target, and is required only if you wish to + install an application with non-default configuration characteristics. +
    path + The context path (including the leading slash) of the web application + this command is intended to manage, or a zero-length string for the + ROOT web application. This attribute is valid for the + install, reload, remove, + start, and stop tasks only, and is + required in all of those cases. +
    war + A jar: URL that points at a web application archive (WAR) + file, or a file: URL that points at an unpacked directory + containing the web application. This attribute is supported only on + the install target. You must specify at least one of the + config and war attributes; if you specify + both, the war attribute overrides the docBase + attribute in the context configuration file. +
    + +

    NOTE - Commands executed through the Manager +application are NOT reflected in updates to the Tomcat +server.xml configuration file, so they do not persist past the +next time you restart the entire Tomcat container.

    + + diff --git a/java/org/apache/catalina/authenticator/AuthenticatorBase.java b/java/org/apache/catalina/authenticator/AuthenticatorBase.java new file mode 100644 index 0000000..e52fa2f --- /dev/null +++ b/java/org/apache/catalina/authenticator/AuthenticatorBase.java @@ -0,0 +1,1296 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.security.Principal; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.AuthStatus; +import jakarta.security.auth.message.MessageInfo; +import jakarta.security.auth.message.config.AuthConfigFactory; +import jakarta.security.auth.message.config.AuthConfigProvider; +import jakarta.security.auth.message.config.RegistrationListener; +import jakarta.security.auth.message.config.ServerAuthConfig; +import jakarta.security.auth.message.config.ServerAuthContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Authenticator; +import org.apache.catalina.Contained; +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Realm; +import org.apache.catalina.Session; +import org.apache.catalina.TomcatPrincipal; +import org.apache.catalina.Valve; +import org.apache.catalina.authenticator.jaspic.MessageInfoImpl; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.filters.CorsFilter; +import org.apache.catalina.filters.RemoteIpFilter; +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.catalina.util.SessionIdGeneratorBase; +import org.apache.catalina.util.StandardSessionIdGenerator; +import org.apache.catalina.valves.RemoteIpValve; +import org.apache.catalina.valves.ValveBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.RequestUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + * Basic implementation of the Valve interface that enforces the <security-constraint> + * elements in the web application deployment descriptor. This functionality is implemented as a Valve so that it can be + * omitted in environments that do not require these features. Individual implementations of each supported + * authentication method can subclass this base class as required. + *

    + * USAGE CONSTRAINT: When this class is utilized, the Context to which it is attached (or a parent Container in a + * hierarchy) must have an associated Realm that can be used for authenticating users and enumerating the roles to which + * they have been assigned. + *

    + * USAGE CONSTRAINT: This Valve is only useful when processing HTTP requests. Requests of any other type will + * simply be passed through. + * + * @author Craig R. McClanahan + */ +public abstract class AuthenticatorBase extends ValveBase implements Authenticator, RegistrationListener { + + private final Log log = LogFactory.getLog(AuthenticatorBase.class); // must not be static + + /** + * "Expires" header always set to Date(1), so generate once only + */ + private static final String DATE_ONE = FastHttpDateFormat.formatDate(1); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(AuthenticatorBase.class); + + /** + * Authentication header + */ + protected static final String AUTH_HEADER_NAME = "WWW-Authenticate"; + + /** + * Default authentication realm name. + */ + protected static final String REALM_NAME = "Authentication required"; + + protected static String getRealmName(Context context) { + if (context == null) { + // Very unlikely + return REALM_NAME; + } + + LoginConfig config = context.getLoginConfig(); + if (config == null) { + return REALM_NAME; + } + + String result = config.getRealmName(); + if (result == null) { + return REALM_NAME; + } + + return result; + } + + // ------------------------------------------------------ Constructor + + public AuthenticatorBase() { + super(true); + } + + // ----------------------------------------------------- Instance Variables + + /** + * Should a session always be used once a user is authenticated? This may offer some performance benefits since the + * session can then be used to cache the authenticated Principal, hence removing the need to authenticate the user + * via the Realm on every request. This may be of help for combinations such as BASIC authentication used with the + * JNDIRealm or DataSourceRealms. However there will also be the performance cost of creating and GC'ing the + * session. By default, a session will not be created. + */ + protected boolean alwaysUseSession = false; + + /** + * Should we cache authenticated Principals if the request is part of an HTTP session? + */ + protected boolean cache = true; + + /** + * Should the session ID, if any, be changed upon a successful authentication to prevent a session fixation attack? + */ + protected boolean changeSessionIdOnAuthentication = true; + + /** + * The Context to which this Valve is attached. + */ + protected Context context = null; + + /** + * Flag to determine if we disable proxy caching, or leave the issue up to the webapp developer. + */ + protected boolean disableProxyCaching = true; + + /** + * Flag to determine if we disable proxy caching with headers incompatible with IE. + */ + protected boolean securePagesWithPragma = false; + + /** + * The Java class name of the secure random number generator class to be used when generating SSO session + * identifiers. The random number generator class must be self-seeding and have a zero-argument constructor. If not + * specified, an instance of {@link java.security.SecureRandom} will be generated. + */ + protected String secureRandomClass = null; + + /** + * The name of the algorithm to use to create instances of {@link java.security.SecureRandom} which are used to + * generate SSO session IDs. If no algorithm is specified, SHA1PRNG is used. If SHA1PRNG is not available, the + * platform default will be used. To use the platform default (which may be SHA1PRNG), specify the empty string. If + * an invalid algorithm and/or provider is specified the SecureRandom instances will be created using the defaults. + * If that fails, the SecureRandom instances will be created using platform defaults. + */ + protected String secureRandomAlgorithm = SessionIdGeneratorBase.DEFAULT_SECURE_RANDOM_ALGORITHM; + + /** + * The name of the provider to use to create instances of {@link java.security.SecureRandom} which are used to + * generate session SSO IDs. If no provider is specified the platform default is used. If an invalid algorithm + * and/or provider is specified the SecureRandom instances will be created using the defaults. If that fails, the + * SecureRandom instances will be created using platform defaults. + */ + protected String secureRandomProvider = null; + + /** + * The name of the JASPIC callback handler class. If none is specified the default + * {@link org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl} will be used. + */ + protected String jaspicCallbackHandlerClass = "org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl"; + + /** + * Should the auth information (remote user and auth type) be returned as response headers for a forwarded/proxied + * request? When the {@link RemoteIpValve} or {@link RemoteIpFilter} mark a forwarded request with the + * {@link Globals#REQUEST_FORWARDED_ATTRIBUTE} this authenticator can return the values of + * {@link HttpServletRequest#getRemoteUser()} and {@link HttpServletRequest#getAuthType()} as response headers + * {@code remote-user} and {@code auth-type} to a reverse proxy. This is useful, e.g., for access log consistency or + * other decisions to make. + */ + + protected boolean sendAuthInfoResponseHeaders = false; + + protected SessionIdGeneratorBase sessionIdGenerator = null; + + /** + * The SingleSignOn implementation in our request processing chain, if there is one. + */ + protected SingleSignOn sso = null; + + private AllowCorsPreflight allowCorsPreflight = AllowCorsPreflight.NEVER; + + private volatile String jaspicAppContextID = null; + private volatile Optional jaspicProvider = null; + private volatile CallbackHandler jaspicCallbackHandler = null; + + + // ------------------------------------------------------------- Properties + + public String getAllowCorsPreflight() { + return allowCorsPreflight.name().toLowerCase(Locale.ENGLISH); + } + + public void setAllowCorsPreflight(String allowCorsPreflight) { + this.allowCorsPreflight = AllowCorsPreflight.valueOf(allowCorsPreflight.trim().toUpperCase(Locale.ENGLISH)); + } + + public boolean getAlwaysUseSession() { + return alwaysUseSession; + } + + public void setAlwaysUseSession(boolean alwaysUseSession) { + this.alwaysUseSession = alwaysUseSession; + } + + /** + * Return the cache authenticated Principals flag. + * + * @return true if authenticated Principals will be cached, otherwise false + */ + public boolean getCache() { + return this.cache; + } + + /** + * Set the cache authenticated Principals flag. + * + * @param cache The new cache flag + */ + public void setCache(boolean cache) { + this.cache = cache; + } + + @Override + public Container getContainer() { + return this.context; + } + + @Override + public void setContainer(Container container) { + if (container != null && !(container instanceof Context)) { + throw new IllegalArgumentException(sm.getString("authenticator.notContext")); + } + super.setContainer(container); + this.context = (Context) container; + } + + /** + * Return the flag that states if we add headers to disable caching by proxies. + * + * @return true if the headers will be added, otherwise false + */ + public boolean getDisableProxyCaching() { + return disableProxyCaching; + } + + /** + * Set the value of the flag that states if we add headers to disable caching by proxies. + * + * @param nocache true if we add headers to disable proxy caching, false if we leave the + * headers alone. + */ + public void setDisableProxyCaching(boolean nocache) { + disableProxyCaching = nocache; + } + + /** + * Return the flag that states, if proxy caching is disabled, what headers we add to disable the caching. + * + * @return true if a Pragma header should be used, otherwise false + */ + public boolean getSecurePagesWithPragma() { + return securePagesWithPragma; + } + + /** + * Set the value of the flag that states what headers we add to disable proxy caching. + * + * @param securePagesWithPragma true if we add headers which are incompatible with downloading office + * documents in IE under SSL but which fix a caching problem in Mozilla. + */ + public void setSecurePagesWithPragma(boolean securePagesWithPragma) { + this.securePagesWithPragma = securePagesWithPragma; + } + + /** + * Return the flag that states if we should change the session ID of an existing session upon successful + * authentication. + * + * @return true to change session ID upon successful authentication, false to do not + * perform the change. + */ + public boolean getChangeSessionIdOnAuthentication() { + return changeSessionIdOnAuthentication; + } + + /** + * Set the value of the flag that states if we should change the session ID of an existing session upon successful + * authentication. + * + * @param changeSessionIdOnAuthentication true to change session ID upon successful authentication, + * false to do not perform the change. + */ + public void setChangeSessionIdOnAuthentication(boolean changeSessionIdOnAuthentication) { + this.changeSessionIdOnAuthentication = changeSessionIdOnAuthentication; + } + + /** + * Return the secure random number generator class name. + * + * @return The fully qualified name of the SecureRandom implementation to use + */ + public String getSecureRandomClass() { + return this.secureRandomClass; + } + + /** + * Set the secure random number generator class name. + * + * @param secureRandomClass The new secure random number generator class name + */ + public void setSecureRandomClass(String secureRandomClass) { + this.secureRandomClass = secureRandomClass; + } + + /** + * Return the secure random number generator algorithm name. + * + * @return The name of the SecureRandom algorithm used + */ + public String getSecureRandomAlgorithm() { + return secureRandomAlgorithm; + } + + /** + * Set the secure random number generator algorithm name. + * + * @param secureRandomAlgorithm The new secure random number generator algorithm name + */ + public void setSecureRandomAlgorithm(String secureRandomAlgorithm) { + this.secureRandomAlgorithm = secureRandomAlgorithm; + } + + /** + * Return the secure random number generator provider name. + * + * @return The name of the SecureRandom provider + */ + public String getSecureRandomProvider() { + return secureRandomProvider; + } + + /** + * Set the secure random number generator provider name. + * + * @param secureRandomProvider The new secure random number generator provider name + */ + public void setSecureRandomProvider(String secureRandomProvider) { + this.secureRandomProvider = secureRandomProvider; + } + + /** + * Return the JASPIC callback handler class name + * + * @return The name of the JASPIC callback handler + */ + public String getJaspicCallbackHandlerClass() { + return jaspicCallbackHandlerClass; + } + + /** + * Set the JASPIC callback handler class name + * + * @param jaspicCallbackHandlerClass The new JASPIC callback handler class name + */ + public void setJaspicCallbackHandlerClass(String jaspicCallbackHandlerClass) { + this.jaspicCallbackHandlerClass = jaspicCallbackHandlerClass; + } + + /** + * Returns the flag whether authentication information will be sent to a reverse proxy on a forwarded request. + * + * @return {@code true} if response headers shall be sent, {@code false} otherwise + */ + public boolean isSendAuthInfoResponseHeaders() { + return sendAuthInfoResponseHeaders; + } + + /** + * Sets the flag whether authentication information will be send to a reverse proxy on a forwarded request. + * + * @param sendAuthInfoResponseHeaders {@code true} if response headers shall be sent, {@code false} otherwise + */ + public void setSendAuthInfoResponseHeaders(boolean sendAuthInfoResponseHeaders) { + this.sendAuthInfoResponseHeaders = sendAuthInfoResponseHeaders; + } + + // --------------------------------------------------------- Public Methods + + /** + * Enforce the security restrictions in the web application deployment descriptor of our associated Context. + * + * @param request Request to be processed + * @param response Response to be processed + * + * @exception IOException if an input/output error occurs + * @exception ServletException if thrown by a processing element + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + if (log.isTraceEnabled()) { + log.trace("Security checking request " + request.getMethod() + " " + request.getRequestURI()); + } + + // Have we got a cached authenticated Principal to record? + if (cache) { + Principal principal = request.getUserPrincipal(); + if (principal == null) { + Session session = request.getSessionInternal(false); + if (session != null) { + principal = session.getPrincipal(); + if (principal != null) { + if (log.isTraceEnabled()) { + log.trace("We have cached auth type " + session.getAuthType() + " for principal " + + principal); + } + request.setAuthType(session.getAuthType()); + request.setUserPrincipal(principal); + } + } + } + } + + boolean authRequired = isContinuationRequired(request); + + Realm realm = this.context.getRealm(); + // Is this request URI subject to a security constraint? + SecurityConstraint[] constraints = realm.findSecurityConstraints(request, this.context); + + AuthConfigProvider jaspicProvider = getJaspicProvider(); + if (jaspicProvider != null) { + authRequired = true; + } + + if (constraints == null && !context.getPreemptiveAuthentication() && !authRequired) { + if (log.isTraceEnabled()) { + log.trace("Not subject to any constraint"); + } + getNext().invoke(request, response); + return; + } + + // Make sure that constrained resources are not cached by web proxies + // or browsers as caching can provide a security hole + if (constraints != null && disableProxyCaching && !"POST".equalsIgnoreCase(request.getMethod())) { + if (securePagesWithPragma) { + // Note: These can cause problems with downloading files with IE + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", DATE_ONE); + } else { + response.setHeader("Cache-Control", "private"); + } + } + + if (constraints != null) { + // Enforce any user data constraint for this security constraint + if (log.isTraceEnabled()) { + log.trace("Calling hasUserDataPermission()"); + } + if (!realm.hasUserDataPermission(request, response, constraints)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.userDataPermissionFail")); + } + /* + * ASSERT: Authenticator already set the appropriate HTTP status code, so we do not have to do anything + * special + */ + return; + } + } + + // Since authenticate modifies the response on failure, + // we have to check for allow-from-all first. + boolean hasAuthConstraint = false; + if (constraints != null) { + hasAuthConstraint = true; + for (int i = 0; i < constraints.length && hasAuthConstraint; i++) { + if (!constraints[i].getAuthConstraint()) { + hasAuthConstraint = false; + } else if (!constraints[i].getAllRoles() && !constraints[i].getAuthenticatedUsers()) { + String[] roles = constraints[i].findAuthRoles(); + if (roles == null || roles.length == 0) { + hasAuthConstraint = false; + } + } + } + } + + if (!authRequired && hasAuthConstraint) { + authRequired = true; + } + + if (!authRequired && context.getPreemptiveAuthentication() && isPreemptiveAuthPossible(request)) { + authRequired = true; + } + + JaspicState jaspicState = null; + + if ((authRequired || constraints != null) && allowCorsPreflightBypass(request)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.corsBypass")); + } + getNext().invoke(request, response); + return; + } + + if (authRequired) { + if (log.isTraceEnabled()) { + log.trace("Calling authenticate()"); + } + + if (jaspicProvider != null) { + jaspicState = getJaspicState(jaspicProvider, request, response, hasAuthConstraint); + if (jaspicState == null) { + return; + } + } + + if (jaspicProvider == null && !doAuthenticate(request, response) || + jaspicProvider != null && !authenticateJaspic(request, response, jaspicState, false)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.authenticationFail")); + } + /* + * ASSERT: Authenticator already set the appropriate HTTP status code, so we do not have to do anything + * special + */ + return; + } + + } + + if (constraints != null) { + if (log.isTraceEnabled()) { + log.trace("Calling accessControl()"); + } + if (!realm.hasResourcePermission(request, response, constraints, this.context)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.userPermissionFail", request.getUserPrincipal().getName())); + } + /* + * ASSERT: AccessControl method has already set the appropriate HTTP status code, so we do not have to + * do anything special + */ + return; + } + } + + // Any and all specified constraints have been satisfied + if (log.isTraceEnabled()) { + log.trace("Successfully passed all security constraints"); + } + getNext().invoke(request, response); + + if (jaspicProvider != null) { + secureResponseJspic(request, response, jaspicState); + } + } + + + protected boolean allowCorsPreflightBypass(Request request) { + boolean allowBypass = false; + + if (allowCorsPreflight != AllowCorsPreflight.NEVER) { + // First check to see if this is a CORS Preflight request + // This is a subset of the tests in CorsFilter.checkRequestType + if ("OPTIONS".equals(request.getMethod())) { + String originHeader = request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN); + if (originHeader != null && !originHeader.isEmpty() && RequestUtil.isValidOrigin(originHeader) && + !RequestUtil.isSameOrigin(request, originHeader)) { + String accessControlRequestMethodHeader = + request.getHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethodHeader != null && !accessControlRequestMethodHeader.isEmpty()) { + // This appears to be a CORS Preflight request + if (allowCorsPreflight == AllowCorsPreflight.ALWAYS) { + allowBypass = true; + } else if (allowCorsPreflight == AllowCorsPreflight.FILTER) { + if (DispatcherType.REQUEST == request.getDispatcherType()) { + // Look at Filter configuration for the Context + // Can't cache this unless we add a listener to + // the Context to clear the cache on reload + for (FilterDef filterDef : request.getContext().findFilterDefs()) { + if (CorsFilter.class.getName().equals(filterDef.getFilterClass())) { + for (FilterMap filterMap : context.findFilterMaps()) { + if (filterMap.getFilterName().equals(filterDef.getFilterName())) { + if ((filterMap.getDispatcherMapping() & FilterMap.REQUEST) > 0) { + for (String urlPattern : filterMap.getURLPatterns()) { + if ("/*".equals(urlPattern)) { + allowBypass = true; + // No need to check other patterns + break; + } + } + } + // Found mappings for CORS filter. + // No need to look further + break; + } + } + // Found the CORS filter. No need to look further. + break; + } + } + } + } else { + // Unexpected enum type + } + } + } + } + } + return allowBypass; + } + + + @Override + public boolean authenticate(Request request, HttpServletResponse httpResponse) throws IOException { + + AuthConfigProvider jaspicProvider = getJaspicProvider(); + + if (jaspicProvider == null) { + return doAuthenticate(request, httpResponse); + } else { + Response response = request.getResponse(); + JaspicState jaspicState = getJaspicState(jaspicProvider, request, response, true); + if (jaspicState == null) { + return false; + } + + boolean result = authenticateJaspic(request, response, jaspicState, true); + + secureResponseJspic(request, response, jaspicState); + + return result; + } + } + + + private void secureResponseJspic(Request request, Response response, JaspicState state) { + try { + state.serverAuthContext.secureResponse(state.messageInfo, null); + request.setRequest((HttpServletRequest) state.messageInfo.getRequestMessage()); + response.setResponse((HttpServletResponse) state.messageInfo.getResponseMessage()); + } catch (AuthException e) { + log.warn(sm.getString("authenticator.jaspicSecureResponseFail"), e); + } + } + + + private JaspicState getJaspicState(AuthConfigProvider jaspicProvider, Request request, Response response, + boolean authMandatory) throws IOException { + JaspicState jaspicState = new JaspicState(); + + jaspicState.messageInfo = new MessageInfoImpl(request.getRequest(), response.getResponse(), authMandatory); + + try { + CallbackHandler callbackHandler = getCallbackHandler(); + ServerAuthConfig serverAuthConfig = + jaspicProvider.getServerAuthConfig("HttpServlet", jaspicAppContextID, callbackHandler); + String authContextID = serverAuthConfig.getAuthContextID(jaspicState.messageInfo); + jaspicState.serverAuthContext = serverAuthConfig.getAuthContext(authContextID, null, null); + } catch (AuthException e) { + log.warn(sm.getString("authenticator.jaspicServerAuthContextFail"), e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return null; + } + + return jaspicState; + } + + + private CallbackHandler getCallbackHandler() { + CallbackHandler handler = jaspicCallbackHandler; + if (handler == null) { + handler = createCallbackHandler(); + } + return handler; + } + + + private CallbackHandler createCallbackHandler() { + CallbackHandler callbackHandler = null; + + Class clazz = null; + try { + clazz = Class.forName(jaspicCallbackHandlerClass, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + // Proceed with the retry below + } + + try { + if (clazz == null) { + clazz = Class.forName(jaspicCallbackHandlerClass); + } + callbackHandler = (CallbackHandler) clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new SecurityException(e); + } + + if (callbackHandler instanceof Contained) { + ((Contained) callbackHandler).setContainer(getContainer()); + } + + jaspicCallbackHandler = callbackHandler; + return callbackHandler; + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Provided for sub-classes to implement their specific authentication mechanism. + * + * @param request The request that triggered the authentication + * @param response The response associated with the request + * + * @return {@code true} if the the user was authenticated, otherwise {@code + * false}, in which case an authentication challenge will have been written to the response + * + * @throws IOException If an I/O problem occurred during the authentication process + */ + protected abstract boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException; + + + /** + * Does this authenticator require that {@link #authenticate(Request, HttpServletResponse)} is called to continue an + * authentication process that started in a previous request? + * + * @param request The request currently being processed + * + * @return {@code true} if authenticate() must be called, otherwise {@code false} + */ + protected boolean isContinuationRequired(Request request) { + return false; + } + + + /** + * Associate the specified single sign on identifier with the specified Session. + * + * @param ssoId Single sign on identifier + * @param session Session to be associated + */ + protected void associate(String ssoId, Session session) { + + if (sso == null) { + return; + } + sso.associate(ssoId, session); + + } + + + private boolean authenticateJaspic(Request request, Response response, JaspicState state, + boolean requirePrincipal) { + + boolean cachedAuth = checkForCachedAuthentication(request, response, false); + Subject client = new Subject(); + AuthStatus authStatus; + try { + authStatus = state.serverAuthContext.validateRequest(state.messageInfo, client, null); + } catch (AuthException e) { + log.debug(sm.getString("authenticator.loginFail"), e); + return false; + } + + request.setRequest((HttpServletRequest) state.messageInfo.getRequestMessage()); + response.setResponse((HttpServletResponse) state.messageInfo.getResponseMessage()); + + if (authStatus == AuthStatus.SUCCESS) { + GenericPrincipal principal = getPrincipal(client); + if (log.isTraceEnabled()) { + log.trace("Authenticated user: " + principal); + } + if (principal == null) { + request.setUserPrincipal(null); + request.setAuthType(null); + if (requirePrincipal) { + return false; + } + } else if (cachedAuth == false || !principal.getUserPrincipal().equals(request.getUserPrincipal())) { + // Skip registration if authentication credentials were + // cached and the Principal did not change. + + // Check to see if any of the JASPIC properties were set + Boolean register = null; + String authType = "JASPIC"; + @SuppressWarnings("rawtypes") // JASPIC API uses raw types + Map map = state.messageInfo.getMap(); + + String registerValue = (String) map.get("jakarta.servlet.http.registerSession"); + if (registerValue != null) { + register = Boolean.valueOf(registerValue); + } + String authTypeValue = (String) map.get("jakarta.servlet.http.authType"); + if (authTypeValue != null) { + authType = authTypeValue; + } + + /* + * Need to handle three cases. See https://bz.apache.org/bugzilla/show_bug.cgi?id=64713 1. + * registerSession TRUE always use session, always cache 2. registerSession NOT SET config for session, + * config for cache 3. registerSession FALSE config for session, never cache + */ + if (register != null) { + register(request, response, principal, authType, null, null, + alwaysUseSession || register.booleanValue(), register.booleanValue()); + } else { + register(request, response, principal, authType, null, null); + } + } + request.setNote(Constants.REQ_JASPIC_SUBJECT_NOTE, client); + return true; + } + return false; + } + + + private GenericPrincipal getPrincipal(Subject subject) { + if (subject == null) { + return null; + } + + Set principals = subject.getPrivateCredentials(GenericPrincipal.class); + if (principals.isEmpty()) { + return null; + } + + return principals.iterator().next(); + } + + + /** + * Check to see if the user has already been authenticated earlier in the processing chain or if there is enough + * information available to authenticate the user without requiring further user interaction. + * + * @param request The current request + * @param response The current response + * @param useSSO Should information available from SSO be used to attempt to authenticate the current user? + * + * @return true if the user was authenticated via the cache, otherwise false + */ + protected boolean checkForCachedAuthentication(Request request, HttpServletResponse response, boolean useSSO) { + + // Has the user already been authenticated? + Principal principal = request.getUserPrincipal(); + String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE); + if (principal != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.check.found", principal.getName())); + } + // Associate the session with any existing SSO session. Even if + // useSSO is false, this will ensure coordinated session + // invalidation at log out. + if (ssoId != null) { + associate(ssoId, request.getSessionInternal(true)); + } + return true; + } + + // Is there an SSO session against which we can try to reauthenticate? + if (useSSO && ssoId != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.check.sso", ssoId)); + } + /* + * Try to reauthenticate using data cached by SSO. If this fails, either the original SSO logon was of + * DIGEST or SSL (which we can't reauthenticate ourselves because there is no cached username and password), + * or the realm denied the user's reauthentication for some reason. In either case we have to prompt the + * user for a logon + */ + if (reauthenticateFromSSO(ssoId, request)) { + return true; + } + } + + // Has the Connector provided a pre-authenticated Principal that now + // needs to be authorized? + if (request.getCoyoteRequest().getRemoteUserNeedsAuthorization()) { + String username = request.getCoyoteRequest().getRemoteUser().toString(); + if (username != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.check.authorize", username)); + } + Principal authorized = context.getRealm().authenticate(username); + if (authorized == null) { + // Realm doesn't recognise user. Create a user with no roles + // from the authenticated user name + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.check.authorizeFail", username)); + } + authorized = new GenericPrincipal(username); + } + String authType = request.getAuthType(); + if (authType == null || authType.length() == 0) { + authType = getAuthMethod(); + } + register(request, response, authorized, authType, username, null); + return true; + } + } + return false; + } + + /** + * Attempts reauthentication to the Realm using the credentials included in argument + * entry. + * + * @param ssoId identifier of SingleSignOn session with which the caller is associated + * @param request the request that needs to be authenticated + * + * @return true if the reauthentication from SSL occurred + */ + protected boolean reauthenticateFromSSO(String ssoId, Request request) { + + if (sso == null || ssoId == null) { + return false; + } + + boolean reauthenticated = false; + + Container parent = getContainer(); + if (parent != null) { + Realm realm = parent.getRealm(); + if (realm != null) { + reauthenticated = sso.reauthenticate(ssoId, realm, request); + } + } + + if (reauthenticated) { + associate(ssoId, request.getSessionInternal(true)); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.reauthentication", request.getUserPrincipal().getName(), request.getAuthType())); + } + } + + return reauthenticated; + } + + /** + * Register an authenticated Principal and authentication type in our request, in the current session (if there is + * one), and with our SingleSignOn valve, if there is one. Set the appropriate cookie to be returned. + * + * @param request The servlet request we are processing + * @param response The servlet response we are generating + * @param principal The authenticated Principal to be registered + * @param authType The authentication type to be registered + * @param username Username used to authenticate (if any) + * @param password Password used to authenticate (if any) + */ + public void register(Request request, HttpServletResponse response, Principal principal, String authType, + String username, String password) { + register(request, response, principal, authType, username, password, alwaysUseSession, cache); + } + + + /** + * Register an authenticated Principal and authentication type in our request, in the current session (if there is + * one), and with our SingleSignOn valve, if there is one. Set the appropriate cookie to be returned. + * + * @param request The servlet request we are processing + * @param response The servlet response we are generating + * @param principal The authenticated Principal to be registered + * @param authType The authentication type to be registered + * @param username Username used to authenticate (if any) + * @param password Password used to authenticate (if any) + * @param alwaysUseSession Should a session always be used once a user is authenticated? + * @param cache Should we cache authenticated Principals if the request is part of an HTTP session? + */ + protected void register(Request request, HttpServletResponse response, Principal principal, String authType, + String username, String password, boolean alwaysUseSession, boolean cache) { + + if (log.isDebugEnabled()) { + String name = (principal == null) ? "none" : principal.getName(); + log.debug(sm.getString("authenticator.authentication", name, authType)); + } + + // Cache the authentication information in our request + request.setAuthType(authType); + request.setUserPrincipal(principal); + + if (sendAuthInfoResponseHeaders && + Boolean.TRUE.equals(request.getAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE))) { + response.setHeader("remote-user", request.getRemoteUser()); + response.setHeader("auth-type", request.getAuthType()); + } + + Session session = request.getSessionInternal(false); + + if (session != null) { + // If the principal is null then this is a logout. No need to change + // the session ID. See BZ 59043. + if (getChangeSessionIdOnAuthentication() && principal != null) { + String newSessionId = changeSessionID(request, session); + // If the current session ID is being tracked, update it. + if (session.getNote(Constants.SESSION_ID_NOTE) != null) { + session.setNote(Constants.SESSION_ID_NOTE, newSessionId); + } + } + } else if (alwaysUseSession) { + session = request.getSessionInternal(true); + } + + // Cache the authentication information in our session, if any + if (session != null && cache) { + session.setAuthType(authType); + session.setPrincipal(principal); + } + + // Construct a cookie to be returned to the client + if (sso == null) { + return; + } + + // Only create a new SSO entry if the SSO did not already set a note + // for an existing entry (as it would do with subsequent requests + // for DIGEST and SSL authenticated contexts) + String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE); + if (ssoId == null) { + // Construct a cookie to be returned to the client + ssoId = sessionIdGenerator.generateSessionId(); + Cookie cookie = new Cookie(sso.getCookieName(), ssoId); + cookie.setMaxAge(-1); + cookie.setPath("/"); + + // Bugzilla 41217 + cookie.setSecure(request.isSecure()); + + // Bugzilla 34724 + String ssoDomain = sso.getCookieDomain(); + if (ssoDomain != null) { + cookie.setDomain(ssoDomain); + } + + // Configure httpOnly on SSO cookie using same rules as session cookies + if (request.getServletContext().getSessionCookieConfig().isHttpOnly() || + request.getContext().getUseHttpOnly()) { + cookie.setHttpOnly(true); + } + + // Configure Partitioned on SSO cookie using same rules as session cookies + cookie.setAttribute(Constants.COOKIE_PARTITIONED_ATTR, + Boolean.toString(request.getContext().getUsePartitioned())); + + response.addCookie(cookie); + + // Register this principal with our SSO valve + sso.register(ssoId, principal, authType, username, password); + request.setNote(Constants.REQ_SSOID_NOTE, ssoId); + + } else { + if (principal == null) { + // Registering a programmatic logout + sso.deregister(ssoId); + request.removeNote(Constants.REQ_SSOID_NOTE); + return; + } else { + // Update the SSO session with the latest authentication data + sso.update(ssoId, principal, authType, username, password); + } + } + + // Fix for Bug 10040 + // Always associate a session with a new SSO registration. + // SSO entries are only removed from the SSO registry map when + // associated sessions are destroyed; if a new SSO entry is created + // above for this request and the user never revisits the context, the + // SSO entry will never be cleared if we don't associate the session + if (session == null) { + session = request.getSessionInternal(true); + } + sso.associate(ssoId, session); + + } + + + protected String changeSessionID(Request request, Session session) { + String oldId = null; + if (log.isDebugEnabled()) { + oldId = session.getId(); + } + String newId = request.changeSessionId(); + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.changeSessionId", oldId, newId)); + } + return newId; + } + + + @Override + public void login(String username, String password, Request request) throws ServletException { + Principal principal = doLogin(request, username, password); + register(request, request.getResponse(), principal, getAuthMethod(), username, password); + } + + /** + * Return the authentication method, which is vendor-specific and not defined by HttpServletRequest. + * + * @return the authentication method, which is vendor-specific and not defined by HttpServletRequest. + */ + protected abstract String getAuthMethod(); + + /** + * Process the login request. + * + * @param request Associated request + * @param username The user + * @param password The password + * + * @return The authenticated Principal + * + * @throws ServletException No principal was authenticated with the specified credentials + */ + protected Principal doLogin(Request request, String username, String password) throws ServletException { + Principal p = context.getRealm().authenticate(username, password); + if (p == null) { + throw new ServletException(sm.getString("authenticator.loginFail")); + } + return p; + } + + @Override + public void logout(Request request) { + AuthConfigProvider provider = getJaspicProvider(); + if (provider != null) { + MessageInfo messageInfo = new MessageInfoImpl(request, request.getResponse(), true); + Subject client = (Subject) request.getNote(Constants.REQ_JASPIC_SUBJECT_NOTE); + if (client != null) { + ServerAuthContext serverAuthContext; + try { + ServerAuthConfig serverAuthConfig = + provider.getServerAuthConfig("HttpServlet", jaspicAppContextID, getCallbackHandler()); + String authContextID = serverAuthConfig.getAuthContextID(messageInfo); + serverAuthContext = serverAuthConfig.getAuthContext(authContextID, null, null); + serverAuthContext.cleanSubject(messageInfo, client); + } catch (AuthException e) { + log.debug(sm.getString("authenticator.jaspicCleanSubjectFail"), e); + } + } + } + + Principal p = request.getPrincipal(); + if (p instanceof TomcatPrincipal) { + try { + ((TomcatPrincipal) p).logout(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.debug(sm.getString("authenticator.tomcatPrincipalLogoutFail"), t); + } + } + + register(request, request.getResponse(), null, null, null, null); + } + + + @Override + protected void startInternal() throws LifecycleException { + ServletContext servletContext = context.getServletContext(); + jaspicAppContextID = servletContext.getVirtualServerName() + " " + servletContext.getContextPath(); + + // Look up the SingleSignOn implementation in our request processing + // path, if there is one + Container parent = context.getParent(); + while ((sso == null) && (parent != null)) { + Valve valves[] = parent.getPipeline().getValves(); + for (Valve valve : valves) { + if (valve instanceof SingleSignOn) { + sso = (SingleSignOn) valve; + break; + } + } + if (sso == null) { + parent = parent.getParent(); + } + } + if (log.isDebugEnabled()) { + if (sso != null) { + log.debug(sm.getString("authenticator.sso", sso)); + } else { + log.trace("No SingleSignOn Valve is present"); + } + } + + sessionIdGenerator = new StandardSessionIdGenerator(); + sessionIdGenerator.setSecureRandomAlgorithm(getSecureRandomAlgorithm()); + sessionIdGenerator.setSecureRandomClass(getSecureRandomClass()); + sessionIdGenerator.setSecureRandomProvider(getSecureRandomProvider()); + + super.startInternal(); + } + + + @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + sso = null; + } + + + /** + * Can the authenticator perform preemptive authentication for the given request? + * + * @param request The request to check for credentials + * + * @return {@code true} if preemptive authentication is possible, otherwise {@code false} + */ + protected boolean isPreemptiveAuthPossible(Request request) { + return false; + } + + + private AuthConfigProvider getJaspicProvider() { + Optional provider = jaspicProvider; + if (provider == null) { + provider = findJaspicProvider(); + } + return provider.orElse(null); + } + + + private Optional findJaspicProvider() { + AuthConfigFactory factory = AuthConfigFactory.getFactory(); + Optional provider; + if (factory == null) { + provider = Optional.empty(); + } else { + provider = Optional.ofNullable(factory.getConfigProvider("HttpServlet", jaspicAppContextID, this)); + } + jaspicProvider = provider; + return provider; + } + + + @Override + public void notify(String layer, String appContext) { + findJaspicProvider(); + } + + + private static class JaspicState { + public MessageInfo messageInfo = null; + public ServerAuthContext serverAuthContext = null; + } + + + protected enum AllowCorsPreflight { + NEVER, + FILTER, + ALWAYS + } +} diff --git a/java/org/apache/catalina/authenticator/BasicAuthenticator.java b/java/org/apache/catalina/authenticator/BasicAuthenticator.java new file mode 100644 index 0000000..7060cca --- /dev/null +++ b/java/org/apache/catalina/authenticator/BasicAuthenticator.java @@ -0,0 +1,291 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.Base64; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.connector.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; + +/** + * An Authenticator and Valve implementation of HTTP BASIC Authentication, as outlined in RFC 7617: "The + * 'Basic' HTTP Authentication Scheme" + * + * @author Craig R. McClanahan + */ +public class BasicAuthenticator extends AuthenticatorBase { + + private final Log log = LogFactory.getLog(BasicAuthenticator.class); // must not be static + + private Charset charset = StandardCharsets.ISO_8859_1; + private String charsetString = null; + private boolean trimCredentials = false; + + + public String getCharset() { + return charsetString; + } + + + public void setCharset(String charsetString) { + // Only acceptable options are null, "" or "UTF-8" (case insensitive) + if (charsetString == null || charsetString.isEmpty()) { + charset = StandardCharsets.ISO_8859_1; + } else if ("UTF-8".equalsIgnoreCase(charsetString)) { + charset = StandardCharsets.UTF_8; + } else { + throw new IllegalArgumentException(sm.getString("basicAuthenticator.invalidCharset")); + } + this.charsetString = charsetString; + } + + + /** + * Obtain the current setting for the removal of whitespace around the decoded user name and password. + * + * @return {@code true} if white space will be removed around the decoded user name and password + * + * @deprecated Will be removed in Tomcat 11 onwards. + */ + @Deprecated + public boolean getTrimCredentials() { + return trimCredentials; + } + + + /** + * Configures trimming of whitespace around the decoded user name and password. + * + * @param trimCredentials {@code true} to remove white space around the decoded user name and password + * + * @deprecated Will be removed in Tomcat 11 onwards. + */ + @Deprecated + public void setTrimCredentials(boolean trimCredentials) { + this.trimCredentials = trimCredentials; + } + + + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { + + if (checkForCachedAuthentication(request, response, true)) { + return true; + } + + // Validate any credentials already included with this request + MessageBytes authorization = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); + + if (authorization != null) { + authorization.toBytes(); + ByteChunk authorizationBC = authorization.getByteChunk(); + BasicCredentials credentials = null; + try { + credentials = new BasicCredentials(authorizationBC, charset, getTrimCredentials()); + String username = credentials.getUsername(); + String password = credentials.getPassword(); + + Principal principal = context.getRealm().authenticate(username, password); + if (principal != null) { + register(request, response, principal, HttpServletRequest.BASIC_AUTH, username, password); + return true; + } + } catch (IllegalArgumentException iae) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("basicAuthenticator.invalidAuthorization", iae.getMessage())); + } + } + } + + // the request could not be authenticated, so reissue the challenge + StringBuilder value = new StringBuilder(16); + value.append("Basic realm=\""); + value.append(getRealmName(context)); + value.append('\"'); + if (charsetString != null && !charsetString.isEmpty()) { + value.append(", charset="); + value.append(charsetString); + } + response.setHeader(AUTH_HEADER_NAME, value.toString()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + + } + + + @Override + protected String getAuthMethod() { + return HttpServletRequest.BASIC_AUTH; + } + + + @Override + protected boolean isPreemptiveAuthPossible(Request request) { + MessageBytes authorizationHeader = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); + return authorizationHeader != null && authorizationHeader.startsWithIgnoreCase("basic ", 0); + } + + + /** + * Parser for an HTTP Authorization header for BASIC authentication as per RFC 2617 section 2, and the Base64 + * encoded credentials as per RFC 2045 section 6.8. + */ + public static class BasicCredentials { + + // the only authentication method supported by this parser + // note: we include single white space as its delimiter + private static final String METHOD = "basic "; + + private final Charset charset; + private final boolean trimCredentials; + private final ByteChunk authorization; + private final int initialOffset; + private int base64blobOffset; + private int base64blobLength; + + private String username = null; + private String password = null; + + /** + * Parse the HTTP Authorization header for BASIC authentication as per RFC 7617. + * + * @param input The header value to parse in-place + * @param charset The character set to use to convert the bytes to a string + * + * @throws IllegalArgumentException If the header does not conform to RFC 7617 + */ + public BasicCredentials(ByteChunk input, Charset charset) throws IllegalArgumentException { + this(input, charset, false); + } + + /** + * Parse the HTTP Authorization header for BASIC authentication as per RFC 7617. + * + * @param input The header value to parse in-place + * @param charset The character set to use to convert the bytes to a string + * @param trimCredentials Should leading and trailing whitespace be removed from the parsed credentials + * + * @throws IllegalArgumentException If the header does not conform to RFC 7617 + * + * @deprecated Will be removed in Tomcat 11 onwards + */ + @Deprecated + public BasicCredentials(ByteChunk input, Charset charset, boolean trimCredentials) + throws IllegalArgumentException { + authorization = input; + initialOffset = input.getOffset(); + this.charset = charset; + this.trimCredentials = trimCredentials; + + parseMethod(); + byte[] decoded = parseBase64(); + parseCredentials(decoded); + } + + /** + * Trivial accessor. + * + * @return the decoded username token as a String, which is never be null, but can be empty. + */ + public String getUsername() { + return username; + } + + /** + * Trivial accessor. + * + * @return the decoded password token as a String, or null if no password was found in the + * credentials. + */ + public String getPassword() { + return password; + } + + /* + * The authorization method string is case-insensitive and must have at exactly one space character as a + * delimiter. + */ + private void parseMethod() throws IllegalArgumentException { + if (authorization.startsWithIgnoreCase(METHOD, 0)) { + // step past the auth method name + base64blobOffset = initialOffset + METHOD.length(); + base64blobLength = authorization.getLength() - METHOD.length(); + } else { + // is this possible, or permitted? + throw new IllegalArgumentException(sm.getString("basicAuthenticator.notBasic")); + } + } + + /* + * Decode the base64-user-pass token, which RFC 2617 states can be longer than the 76 characters per line limit + * defined in RFC 2045. The base64 decoder will ignore embedded line break characters as well as surplus + * surrounding white space. + */ + private byte[] parseBase64() throws IllegalArgumentException { + byte[] encoded = new byte[base64blobLength]; + System.arraycopy(authorization.getBuffer(), base64blobOffset, encoded, 0, base64blobLength); + byte[] decoded = Base64.getDecoder().decode(encoded); + // restore original offset + authorization.setOffset(initialOffset); + if (decoded == null) { + throw new IllegalArgumentException(sm.getString("basicAuthenticator.notBase64")); + } + return decoded; + } + + /* + * Extract the mandatory username token and separate it from the optional password token. Tolerate surplus + * surrounding white space. + */ + private void parseCredentials(byte[] decoded) throws IllegalArgumentException { + + int colon = -1; + for (int i = 0; i < decoded.length; i++) { + if (decoded[i] == ':') { + colon = i; + break; + } + } + + if (colon < 0) { + username = new String(decoded, charset); + // password will remain null! + } else { + username = new String(decoded, 0, colon, charset); + password = new String(decoded, colon + 1, decoded.length - colon - 1, charset); + // tolerate surplus white space around credentials + if (password.length() > 1 && trimCredentials) { + password = password.trim(); + } + } + // tolerate surplus white space around credentials + if (username.length() > 1 && trimCredentials) { + username = username.trim(); + } + } + } +} diff --git a/java/org/apache/catalina/authenticator/Constants.java b/java/org/apache/catalina/authenticator/Constants.java new file mode 100644 index 0000000..5485782 --- /dev/null +++ b/java/org/apache/catalina/authenticator/Constants.java @@ -0,0 +1,88 @@ +/* + * 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.catalina.authenticator; + +public class Constants { + // Authentication methods for login configuration + // Servlet spec schemes are defined in HttpServletRequest + // Vendor specific schemes + public static final String SPNEGO_METHOD = "SPNEGO"; + + // Form based authentication constants + public static final String FORM_ACTION = "/j_security_check"; + public static final String FORM_PASSWORD = "j_password"; + public static final String FORM_USERNAME = "j_username"; + + // SPNEGO authentication constants + public static final String KRB5_CONF_PROPERTY = "java.security.krb5.conf"; + public static final String DEFAULT_KRB5_CONF = "conf/krb5.ini"; + public static final String JAAS_CONF_PROPERTY = "java.security.auth.login.config"; + public static final String DEFAULT_JAAS_CONF = "conf/jaas.conf"; + public static final String DEFAULT_LOGIN_MODULE_NAME = "com.sun.security.jgss.krb5.accept"; + + // Cookie name for single sign on support + public static final String SINGLE_SIGN_ON_COOKIE = "JSESSIONIDSSO"; + + /** + * The name of the attribute used to indicate a partitioned cookie as part of + * CHIPS. This cookie attribute is not + * defined by an RFC and may change in a non-backwards compatible way once equivalent functionality is included in + * an RFC. + */ + public static final String COOKIE_PARTITIONED_ATTR = + org.apache.tomcat.util.descriptor.web.Constants.COOKIE_PARTITIONED_ATTR; + + + // --------------------------------------------------------- Request Notes + + /** + * The notes key to track the single-sign-on identity with which this request is associated. + */ + public static final String REQ_SSOID_NOTE = "org.apache.catalina.request.SSOID"; + + public static final String REQ_JASPIC_SUBJECT_NOTE = "org.apache.catalina.authenticator.jaspic.SUBJECT"; + + + // ---------------------------------------------------------- Session Notes + + /** + * The session id used as a CSRF marker when redirecting a user's request. + */ + public static final String SESSION_ID_NOTE = "org.apache.catalina.authenticator.SESSION_ID"; + + + /** + * If the cache property of the authenticator is set, and the current request is part of a session, the + * password used to authenticate this user will be cached under this key to avoid the need for repeated calls to + * Realm.authenticate(). + */ + public static final String SESS_PASSWORD_NOTE = "org.apache.catalina.session.PASSWORD"; + + /** + * If the cache property of the authenticator is set, and the current request is part of a session, the + * user name used to authenticate this user will be cached under this key to avoid the need for repeated calls to + * Realm.authenticate(). + */ + public static final String SESS_USERNAME_NOTE = "org.apache.catalina.session.USERNAME"; + + + /** + * The original request information, to which the user will be redirected if authentication succeeds, is cached in + * the notes under this key during the authentication process. + */ + public static final String FORM_REQUEST_NOTE = "org.apache.catalina.authenticator.REQUEST"; +} diff --git a/java/org/apache/catalina/authenticator/DigestAuthenticator.java b/java/org/apache/catalina/authenticator/DigestAuthenticator.java new file mode 100644 index 0000000..6014317 --- /dev/null +++ b/java/org/apache/catalina/authenticator/DigestAuthenticator.java @@ -0,0 +1,766 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Realm; +import org.apache.catalina.connector.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.http.parser.Authorization; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; + + +/** + * An Authenticator and Valve implementation of HTTP DIGEST Authentication, as outlined in RFC 7616: "HTTP + * Digest Authentication" + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class DigestAuthenticator extends AuthenticatorBase { + + private final Log log = LogFactory.getLog(DigestAuthenticator.class); // must not be static + + + // -------------------------------------------------------------- Constants + + /** + * Tomcat's DIGEST implementation only supports auth quality of protection. + */ + protected static final String QOP = "auth"; + + private static final AuthDigest FALLBACK_DIGEST = AuthDigest.MD5; + + private static final String NONCE_DIGEST = "SHA-256"; + + // List permitted algorithms and maps them to Java standard names + private static final Map PERMITTED_ALGORITHMS = new HashMap<>(); + static { + // Allows the digester to be configured with either the Standard Java name or the name used the RFC. + for (AuthDigest authDigest : AuthDigest.values()) { + PERMITTED_ALGORITHMS.put(authDigest.getJavaName(), authDigest); + PERMITTED_ALGORITHMS.put(authDigest.getRfcName(), authDigest); + } + } + + + // ----------------------------------------------------------- Constructors + + public DigestAuthenticator() { + super(); + setCache(false); + } + + + // ----------------------------------------------------- Instance Variables + + /** + * List of server nonce values currently being tracked + */ + protected Map nonces; + + + /** + * The last timestamp used to generate a nonce. Each nonce should get a unique timestamp. + */ + protected long lastTimestamp = 0; + protected final Object lastTimestampLock = new Object(); + + + /** + * Maximum number of server nonces to keep in the cache. If not specified, the default value of 1000 is used. + */ + protected int nonceCacheSize = 1000; + + + /** + * The window size to use to track seen nonce count values for a given nonce. If not specified, the default of 100 + * is used. + */ + protected int nonceCountWindowSize = 100; + + /** + * Private key. + */ + protected String key = null; + + + /** + * How long server nonces are valid for in milliseconds. Defaults to 5 minutes. + */ + protected long nonceValidity = 5 * 60 * 1000; + + + /** + * Opaque string. + */ + protected String opaque; + + + /** + * Should the URI be validated as required by RFC2617? Can be disabled in reverse proxies where the proxy has + * modified the URI. + */ + protected boolean validateUri = true; + + + /** + * Algorithms to use for WWW-Authenticate challenges. + */ + private List algorithms = Arrays.asList(AuthDigest.SHA_256, AuthDigest.MD5); + + + // ------------------------------------------------------------- Properties + + public int getNonceCountWindowSize() { + return nonceCountWindowSize; + } + + + public void setNonceCountWindowSize(int nonceCountWindowSize) { + this.nonceCountWindowSize = nonceCountWindowSize; + } + + + public int getNonceCacheSize() { + return nonceCacheSize; + } + + + public void setNonceCacheSize(int nonceCacheSize) { + this.nonceCacheSize = nonceCacheSize; + } + + + public String getKey() { + return key; + } + + + public void setKey(String key) { + this.key = key; + } + + + public long getNonceValidity() { + return nonceValidity; + } + + + public void setNonceValidity(long nonceValidity) { + this.nonceValidity = nonceValidity; + } + + + public String getOpaque() { + return opaque; + } + + + public void setOpaque(String opaque) { + this.opaque = opaque; + } + + + public boolean isValidateUri() { + return validateUri; + } + + + public void setValidateUri(boolean validateUri) { + this.validateUri = validateUri; + } + + + public String getAlgorithms() { + StringBuilder result = new StringBuilder(); + StringUtils.join(algorithms, ',', (x) -> x.getRfcName(), result); + return result.toString(); + } + + + public void setAlgorithms(String algorithmsString) { + String[] algorithmsArray = algorithmsString.split(","); + List algorithms = new ArrayList<>(); + + // Ignore the new setting if any of the algorithms are invalid + for (String algorithm : algorithmsArray) { + AuthDigest authDigest = PERMITTED_ALGORITHMS.get(algorithm); + if (authDigest == null) { + log.warn(sm.getString("digestAuthenticator.invalidAlgorithm", algorithmsString, algorithm)); + return; + } + algorithms.add(authDigest); + } + + initAlgorithms(algorithms); + this.algorithms = algorithms; + } + + + /* + * Initialise algorithms, removing ones that the JRE does not support + */ + private void initAlgorithms(List algorithms) { + Iterator algorithmIterator = algorithms.iterator(); + while (algorithmIterator.hasNext()) { + AuthDigest algorithm = algorithmIterator.next(); + try { + ConcurrentMessageDigest.init(algorithm.getJavaName()); + } catch (NoSuchAlgorithmException e) { + // In theory, a JRE can choose not to implement SHA-512/256 + log.warn(sm.getString("digestAuthenticator.unsupportedAlgorithm", algorithms, algorithm.getJavaName()), + e); + algorithmIterator.remove(); + } + } + } + + + // --------------------------------------------------------- Public Methods + + /** + * Authenticate the user making this request, based on the specified login configuration. Return true + * if any specified constraint has been satisfied, or false if we have created a response challenge + * already. + * + * @param request Request we are processing + * @param response Response we are creating + * + * @exception IOException if an input/output error occurs + */ + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { + + // NOTE: We don't try to reauthenticate using any existing SSO session, + // because that will only work if the original authentication was + // BASIC or FORM, which are less secure than the DIGEST auth-type + // specified for this webapp + // + // Change to true below to allow previous FORM or BASIC authentications + // to authenticate users for this webapp + // TODO make this a configurable attribute (in SingleSignOn??) + if (checkForCachedAuthentication(request, response, false)) { + return true; + } + + // Validate any credentials already included with this request + Principal principal = null; + String authorization = request.getHeader("authorization"); + DigestInfo digestInfo = new DigestInfo(getOpaque(), getNonceValidity(), getKey(), nonces, isValidateUri()); + if (authorization != null) { + if (digestInfo.parse(request, authorization)) { + if (digestInfo.validate(request, algorithms)) { + principal = digestInfo.authenticate(context.getRealm()); + } + + if (principal != null && !digestInfo.isNonceStale()) { + register(request, response, principal, HttpServletRequest.DIGEST_AUTH, digestInfo.getUsername(), + null); + return true; + } + } + } + + // Send an "unauthorized" response and an appropriate challenge + + // Next, generate a nonce token (that is a token which is supposed + // to be unique). + String nonce = generateNonce(request); + + setAuthenticateHeader(request, response, nonce, principal != null && digestInfo.isNonceStale()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + + @Override + protected String getAuthMethod() { + return HttpServletRequest.DIGEST_AUTH; + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Removes the quotes on a string. RFC2617 states quotes are optional for all parameters except realm. + * + * @param quotedString The quoted string + * @param quotesRequired true if quotes were required + * + * @return The unquoted string + */ + protected static String removeQuotes(String quotedString, boolean quotesRequired) { + // support both quoted and non-quoted + if (quotedString.length() > 0 && quotedString.charAt(0) != '"' && !quotesRequired) { + return quotedString; + } else if (quotedString.length() > 2) { + return quotedString.substring(1, quotedString.length() - 1); + } else { + return ""; + } + } + + /** + * Removes the quotes on a string. + * + * @param quotedString The quoted string + * + * @return The unquoted string + */ + protected static String removeQuotes(String quotedString) { + return removeQuotes(quotedString, false); + } + + /** + * Generate a unique token. The token is generated according to the following pattern. NOnceToken = Base64 ( + * NONCE_DIGEST ( client-IP ":" time-stamp ":" private-key ) ). + * + * @param request HTTP Servlet request + * + * @return The generated nonce + */ + protected String generateNonce(Request request) { + + long currentTime = System.currentTimeMillis(); + + synchronized (lastTimestampLock) { + if (currentTime > lastTimestamp) { + lastTimestamp = currentTime; + } else { + currentTime = ++lastTimestamp; + } + } + + String ipTimeKey = request.getRemoteAddr() + ":" + currentTime + ":" + getKey(); + + // Note: The digest used to generate the nonce is independent of the the digest used for authentication. + byte[] buffer = ConcurrentMessageDigest.digest(NONCE_DIGEST, ipTimeKey.getBytes(StandardCharsets.ISO_8859_1)); + String nonce = currentTime + ":" + HexUtils.toHexString(buffer); + + NonceInfo info = new NonceInfo(currentTime, getNonceCountWindowSize()); + synchronized (nonces) { + nonces.put(nonce, info); + } + + return nonce; + } + + + /** + * Generates the WWW-Authenticate header(s) as per RFC 7616. + * + * @param request HTTP Servlet request + * @param response HTTP Servlet response + * @param nonce nonce token + * @param isNonceStale true to add a stale parameter + */ + protected void setAuthenticateHeader(HttpServletRequest request, HttpServletResponse response, String nonce, + boolean isNonceStale) { + + String realmName = getRealmName(context); + + boolean first = true; + for (AuthDigest algorithm : algorithms) { + StringBuilder authenticateHeader = new StringBuilder(200); + authenticateHeader.append("Digest realm=\""); + authenticateHeader.append(realmName); + authenticateHeader.append("\", qop=\""); + authenticateHeader.append(QOP); + authenticateHeader.append("\", nonce=\""); + authenticateHeader.append(nonce); + authenticateHeader.append("\", opaque=\""); + authenticateHeader.append(getOpaque()); + authenticateHeader.append("\""); + if (isNonceStale) { + authenticateHeader.append(", stale=true"); + } + authenticateHeader.append(", algorithm="); + authenticateHeader.append(algorithm.getRfcName()); + + if (first) { + response.setHeader(AUTH_HEADER_NAME, authenticateHeader.toString()); + first = false; + } else { + response.addHeader(AUTH_HEADER_NAME, authenticateHeader.toString()); + } + /* + * Note: userhash is not supported by this implementation so don't include it. The clients will use the + * default of false. + */ + } + } + + + @Override + protected boolean isPreemptiveAuthPossible(Request request) { + MessageBytes authorizationHeader = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); + return authorizationHeader != null && authorizationHeader.startsWithIgnoreCase("digest ", 0); + } + + + // ------------------------------------------------------- Lifecycle Methods + + @Override + protected void startInternal() throws LifecycleException { + super.startInternal(); + + // Generate a random secret key + if (getKey() == null) { + setKey(sessionIdGenerator.generateSessionId()); + } + + // Generate the opaque string the same way + if (getOpaque() == null) { + setOpaque(sessionIdGenerator.generateSessionId()); + } + + /* + * This is a FIFO cache as using an older nonce should not delay its removal from the cache in favour of more + * recent values. + */ + nonces = new LinkedHashMap<>() { + + private static final long serialVersionUID = 1L; + private static final long LOG_SUPPRESS_TIME = 5 * 60 * 1000; + + private long lastLog = 0; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + // This is called from a sync so keep it simple + long currentTime = System.currentTimeMillis(); + if (size() > getNonceCacheSize()) { + if (lastLog < currentTime && currentTime - eldest.getValue().getTimestamp() < getNonceValidity()) { + // Replay attack is possible + log.warn(sm.getString("digestAuthenticator.cacheRemove")); + lastLog = currentTime + LOG_SUPPRESS_TIME; + } + return true; + } + return false; + } + }; + + initAlgorithms(algorithms); + try { + ConcurrentMessageDigest.init(NONCE_DIGEST); + } catch (NoSuchAlgorithmException e) { + // Not possible. NONCE_DIGEST uses an algorithm that JREs must support. + } + } + + + public static class DigestInfo { + + private final String opaque; + private final long nonceValidity; + private final String key; + private final Map nonces; + private boolean validateUri = true; + + private String userName = null; + private String method = null; + private String uri = null; + private String response = null; + private String nonce = null; + private String nc = null; + private String cnonce = null; + private String realmName = null; + private String qop = null; + private String opaqueReceived = null; + + private boolean nonceStale = false; + private AuthDigest algorithm = null; + + + public DigestInfo(String opaque, long nonceValidity, String key, Map nonces, + boolean validateUri) { + this.opaque = opaque; + this.nonceValidity = nonceValidity; + this.key = key; + this.nonces = nonces; + this.validateUri = validateUri; + } + + + public String getUsername() { + return userName; + } + + + public boolean parse(Request request, String authorization) { + // Validate the authorization credentials format + if (authorization == null) { + return false; + } + + Map directives; + try { + directives = Authorization.parseAuthorizationDigest(new StringReader(authorization)); + } catch (IOException e) { + return false; + } + + if (directives == null) { + return false; + } + + method = request.getMethod(); + userName = directives.get("username"); + realmName = directives.get("realm"); + nonce = directives.get("nonce"); + nc = directives.get("nc"); + cnonce = directives.get("cnonce"); + qop = directives.get("qop"); + uri = directives.get("uri"); + response = directives.get("response"); + opaqueReceived = directives.get("opaque"); + algorithm = PERMITTED_ALGORITHMS.get(directives.get("algorithm")); + if (algorithm == null) { + algorithm = FALLBACK_DIGEST; + } + + return true; + } + + @Deprecated + public boolean validate(Request request) { + List fallbackList = Arrays.asList(FALLBACK_DIGEST); + return validate(request, fallbackList); + } + + public boolean validate(Request request, List algorithms) { + if ((userName == null) || (realmName == null) || (nonce == null) || (uri == null) || (response == null)) { + return false; + } + + // Validate the URI - should match the request line sent by client + if (validateUri) { + String uriQuery; + String query = request.getQueryString(); + if (query == null) { + uriQuery = request.getRequestURI(); + } else { + uriQuery = request.getRequestURI() + "?" + query; + } + if (!uri.equals(uriQuery)) { + // Some clients (older Android) use an absolute URI for + // DIGEST but a relative URI in the request line. + // request. 2.3.5 < fixed Android version <= 4.0.3 + String host = request.getHeader("host"); + String scheme = request.getScheme(); + if (host != null && !uriQuery.startsWith(scheme)) { + StringBuilder absolute = new StringBuilder(); + absolute.append(scheme); + absolute.append("://"); + absolute.append(host); + absolute.append(uriQuery); + if (!uri.equals(absolute.toString())) { + return false; + } + } else { + return false; + } + } + } + + // Validate the Realm name + String lcRealm = getRealmName(request.getContext()); + if (!lcRealm.equals(realmName)) { + return false; + } + + // Validate the opaque string + if (!opaque.equals(opaqueReceived)) { + return false; + } + + // Validate nonce + int i = nonce.indexOf(':'); + if (i < 0 || (i + 1) == nonce.length()) { + return false; + } + long nonceTime; + try { + nonceTime = Long.parseLong(nonce.substring(0, i)); + } catch (NumberFormatException nfe) { + return false; + } + String digestclientIpTimeKey = nonce.substring(i + 1); + long currentTime = System.currentTimeMillis(); + if ((currentTime - nonceTime) > nonceValidity) { + nonceStale = true; + synchronized (nonces) { + nonces.remove(nonce); + } + } + String serverIpTimeKey = request.getRemoteAddr() + ":" + nonceTime + ":" + key; + // Note: The digest used to generate the nonce is independent of the the digest used for authentication/ + byte[] buffer = + ConcurrentMessageDigest.digest(NONCE_DIGEST, serverIpTimeKey.getBytes(StandardCharsets.ISO_8859_1)); + String digestServerIpTimeKey = HexUtils.toHexString(buffer); + if (!digestServerIpTimeKey.equals(digestclientIpTimeKey)) { + return false; + } + + // Validate qop + if (qop != null && !QOP.equals(qop)) { + return false; + } + + // Validate cnonce and nc + // Check if presence of nc and Cnonce is consistent with presence of qop + if (qop == null) { + if (cnonce != null || nc != null) { + return false; + } + } else { + if (cnonce == null || nc == null) { + return false; + } + // RFC 2617 says nc must be 8 digits long. Older Android clients + // use 6. 2.3.5 < fixed Android version <= 4.0.3 + if (nc.length() < 6 || nc.length() > 8) { + return false; + } + long count; + try { + count = Long.parseLong(nc, 16); + } catch (NumberFormatException nfe) { + return false; + } + NonceInfo info; + synchronized (nonces) { + info = nonces.get(nonce); + } + if (info == null) { + // Nonce is valid but not in cache. It must have dropped out + // of the cache - force a re-authentication + nonceStale = true; + } else { + if (!info.nonceCountValid(count)) { + return false; + } + } + } + + // Validate algorithm is one of the algorithms configured for the authenticator + if (!algorithms.contains(algorithm)) { + return false; + } + + return true; + } + + public boolean isNonceStale() { + return nonceStale; + } + + public Principal authenticate(Realm realm) { + String a2 = method + ":" + uri; + + byte[] buffer = + ConcurrentMessageDigest.digest(algorithm.getJavaName(), a2.getBytes(StandardCharsets.ISO_8859_1)); + String digestA2 = HexUtils.toHexString(buffer); + + return realm.authenticate(userName, response, nonce, nc, cnonce, qop, realmName, digestA2, + algorithm.getJavaName()); + } + + } + + public static class NonceInfo { + private final long timestamp; + private final boolean seen[]; + private final int offset; + private int count = 0; + + public NonceInfo(long currentTime, int seenWindowSize) { + this.timestamp = currentTime; + seen = new boolean[seenWindowSize]; + offset = seenWindowSize / 2; + } + + public synchronized boolean nonceCountValid(long nonceCount) { + if ((count - offset) >= nonceCount || (nonceCount > count - offset + seen.length)) { + return false; + } + int checkIndex = (int) ((nonceCount + offset) % seen.length); + if (seen[checkIndex]) { + return false; + } else { + seen[checkIndex] = true; + seen[count % seen.length] = false; + count++; + return true; + } + } + + public long getTimestamp() { + return timestamp; + } + } + + + /** + * This enum exists because RFC 7616 and Java use different names for some digests. + */ + public enum AuthDigest { + + MD5("MD5", "MD5"), + SHA_256("SHA-256", "SHA-256"), + SHA_512_256("SHA-512/256", "SHA-512-256"); + + private final String javaName; + private final String rfcName; + + AuthDigest(String javaName, String rfcName) { + this.javaName = javaName; + this.rfcName = rfcName; + } + + public String getJavaName() { + return javaName; + } + + public String getRfcName() { + return rfcName; + } + } +} diff --git a/java/org/apache/catalina/authenticator/FormAuthenticator.java b/java/org/apache/catalina/authenticator/FormAuthenticator.java new file mode 100644 index 0000000..f5efd87 --- /dev/null +++ b/java/org/apache/catalina/authenticator/FormAuthenticator.java @@ -0,0 +1,768 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.io.InputStream; +import java.security.Principal; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Locale; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.Realm; +import org.apache.catalina.Session; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.coyote.ActionCode; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.http.MimeHeaders; + +/** + * An Authenticator and Valve implementation of FORM BASED Authentication, as described in the Servlet API + * Specification. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class FormAuthenticator extends AuthenticatorBase { + + private final Log log = LogFactory.getLog(FormAuthenticator.class); // must not be static + + + // ----------------------------------------------------- Instance Variables + + /** + * Character encoding to use to read the username and password parameters from the request. If not set, the encoding + * of the request body will be used. + */ + protected String characterEncoding = null; + + /** + * Landing page to use if a user tries to access the login page directly or if the session times out during login. + * If not set, error responses will be sent instead. + */ + protected String landingPage = null; + + /** + * If the authentication process creates a session, this is the maximum session timeout (in seconds) during the + * authentication process. Once authentication is complete, the default session timeout will apply. Sessions that + * exist before the authentication process starts will retain their original session timeout throughout. + */ + protected int authenticationSessionTimeout = 120; + + + // ------------------------------------------------------------- Properties + + /** + * Return the character encoding to use to read the user name and password. + * + * @return The name of the character encoding + */ + public String getCharacterEncoding() { + return characterEncoding; + } + + + /** + * Set the character encoding to be used to read the user name and password. + * + * @param encoding The name of the encoding to use + */ + public void setCharacterEncoding(String encoding) { + characterEncoding = encoding; + } + + + /** + * Return the landing page to use when FORM auth is mis-used. + * + * @return The path to the landing page relative to the web application root + */ + public String getLandingPage() { + return landingPage; + } + + + /** + * Set the landing page to use when the FORM auth is mis-used. + * + * @param landingPage The path to the landing page relative to the web application root + */ + public void setLandingPage(String landingPage) { + this.landingPage = landingPage; + } + + + /** + * Returns the maximum session timeout to be used during authentication if the authentication process creates a + * session. + * + * @return the maximum session timeout to be used during authentication if the authentication process creates a + * session + */ + public int getAuthenticationSessionTimeout() { + return authenticationSessionTimeout; + } + + + /** + * Configures the maximum session timeout to be used during authentication if the authentication process creates a + * session. + * + * @param authenticationSessionTimeout The maximum session timeout to use duriing authentication if the + * authentication process creates a session + */ + public void setAuthenticationSessionTimeout(int authenticationSessionTimeout) { + this.authenticationSessionTimeout = authenticationSessionTimeout; + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Authenticate the user making this request, based on the specified login configuration. Return true + * if any specified constraint has been satisfied, or false if we have created a response challenge + * already. + * + * @param request Request we are processing + * @param response Response we are creating + * + * @exception IOException if an input/output error occurs + */ + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { + + // References to objects we will need later + Session session = null; + Principal principal = null; + + // Have we authenticated this user before but have caching disabled? + if (!cache) { + session = request.getSessionInternal(true); + if (log.isTraceEnabled()) { + log.trace("Checking for reauthenticate in session " + session); + } + String username = (String) session.getNote(Constants.SESS_USERNAME_NOTE); + String password = (String) session.getNote(Constants.SESS_PASSWORD_NOTE); + if (username != null && password != null) { + if (log.isTraceEnabled()) { + log.trace("Reauthenticating username '" + username + "'"); + } + principal = context.getRealm().authenticate(username, password); + if (principal != null) { + register(request, response, principal, HttpServletRequest.FORM_AUTH, username, password); + if (!matchRequest(request)) { + return true; + } + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("formAuthenticator.reauthFailed")); + } + } + } + + // Is this the re-submit of the original request URI after successful + // authentication? If so, forward the *original* request instead. + if (matchRequest(request)) { + session = request.getSessionInternal(true); + if (log.isTraceEnabled()) { + log.trace("Restore request from session '" + session.getIdInternal() + "'"); + } + if (restoreRequest(request, session)) { + if (log.isTraceEnabled()) { + log.trace("Proceed to restored request"); + } + return true; + } else { + if (log.isDebugEnabled()) { + log.debug(sm.getString("formAuthenticator.restoreFailed")); + } + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return false; + } + } + + // This check has to be after the previous check for a matching request + // because that matching request may also include a cached Principal. + if (checkForCachedAuthentication(request, response, true)) { + return true; + } + + // Acquire references to objects we will need to evaluate + String contextPath = request.getContextPath(); + String requestURI = request.getDecodedRequestURI(); + + // Is this the action request from the login page? + boolean loginAction = requestURI.startsWith(contextPath) && requestURI.endsWith(Constants.FORM_ACTION); + + LoginConfig config = context.getLoginConfig(); + + // No -- Save this request and redirect to the form login page + if (!loginAction) { + // If this request was to the root of the context without a trailing + // '/', need to redirect to add it else the submit of the login form + // may not go to the correct web application + if (request.getServletPath().length() == 0 && request.getPathInfo() == null) { + StringBuilder location = new StringBuilder(requestURI); + location.append('/'); + if (request.getQueryString() != null) { + location.append('?'); + location.append(request.getQueryString()); + } + response.sendRedirect(response.encodeRedirectURL(location.toString())); + return false; + } + + session = request.getSessionInternal(true); + if (log.isTraceEnabled()) { + log.trace("Save request in session '" + session.getIdInternal() + "'"); + } + try { + saveRequest(request, session); + } catch (IOException ioe) { + log.debug(sm.getString("authenticator.requestBodyTooBig")); + response.sendError(HttpServletResponse.SC_FORBIDDEN, sm.getString("authenticator.requestBodyTooBig")); + return false; + } + forwardToLoginPage(request, response, config); + return false; + } + + // Yes -- Acknowledge the request, validate the specified credentials + // and redirect to the error page if they are not correct + request.getResponse().sendAcknowledgement(ContinueResponseTiming.ALWAYS); + Realm realm = context.getRealm(); + if (characterEncoding != null) { + request.setCharacterEncoding(characterEncoding); + } + String username = request.getParameter(Constants.FORM_USERNAME); + String password = request.getParameter(Constants.FORM_PASSWORD); + if (log.isTraceEnabled()) { + log.trace("Authenticating username '" + username + "'"); + } + principal = realm.authenticate(username, password); + if (principal == null) { + forwardToErrorPage(request, response, config); + return false; + } + + if (log.isTraceEnabled()) { + log.trace("Authentication of '" + username + "' was successful"); + } + + if (session == null) { + session = request.getSessionInternal(false); + } + if (session != null && getChangeSessionIdOnAuthentication()) { + // Does session id match? + String expectedSessionId = (String) session.getNote(Constants.SESSION_ID_NOTE); + if (expectedSessionId == null || !expectedSessionId.equals(request.getRequestedSessionId())) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("formAuthenticator.sessionIdMismatch", session.getId(), expectedSessionId)); + } + session.expire(); + session = null; + } + } + if (session == null) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("formAuthenticator.sessionExpired")); + } + if (landingPage == null) { + response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, + sm.getString("authenticator.sessionExpired")); + } else { + // Make the authenticator think the user originally requested + // the landing page + String uri = request.getContextPath() + landingPage; + SavedRequest saved = new SavedRequest(); + saved.setMethod("GET"); + saved.setRequestURI(uri); + saved.setDecodedRequestURI(uri); + request.getSessionInternal(true).setNote(Constants.FORM_REQUEST_NOTE, saved); + response.sendRedirect(response.encodeRedirectURL(uri)); + } + return false; + } + + register(request, response, principal, HttpServletRequest.FORM_AUTH, username, password); + + // Redirect the user to the original request URI (which will cause + // the original request to be restored) + requestURI = savedRequestURL(session); + if (log.isTraceEnabled()) { + log.trace("Redirecting to original '" + requestURI + "'"); + } + if (requestURI == null) { + if (landingPage == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, sm.getString("authenticator.formlogin")); + } else { + // Make the authenticator think the user originally requested + // the landing page + String uri = request.getContextPath() + landingPage; + SavedRequest saved = new SavedRequest(); + saved.setMethod("GET"); + saved.setRequestURI(uri); + saved.setDecodedRequestURI(uri); + session.setNote(Constants.FORM_REQUEST_NOTE, saved); + response.sendRedirect(response.encodeRedirectURL(uri)); + } + } else { + // Until the Servlet API allows specifying the type of redirect to + // use. + Response internalResponse = request.getResponse(); + String location = response.encodeRedirectURL(requestURI); + if ("HTTP/1.1".equals(request.getProtocol())) { + internalResponse.sendRedirect(location, HttpServletResponse.SC_SEE_OTHER); + } else { + internalResponse.sendRedirect(location, HttpServletResponse.SC_FOUND); + } + } + return false; + } + + + @Override + protected boolean isContinuationRequired(Request request) { + // Special handling for form-based logins to deal with the case + // where the login form (and therefore the "j_security_check" URI + // to which it submits) might be outside the secured area + String contextPath = this.context.getPath(); + String decodedRequestURI = request.getDecodedRequestURI(); + if (decodedRequestURI.startsWith(contextPath) && decodedRequestURI.endsWith(Constants.FORM_ACTION)) { + return true; + } + + // Special handling for form-based logins to deal with the case where + // a resource is protected for some HTTP methods but not protected for + // GET which is used after authentication when redirecting to the + // protected resource. + // TODO: This is similar to the FormAuthenticator.matchRequest() logic + // Is there a way to remove the duplication? + Session session = request.getSessionInternal(false); + if (session != null) { + SavedRequest savedRequest = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE); + if (savedRequest != null && decodedRequestURI.equals(savedRequest.getDecodedRequestURI())) { + return true; + } + } + + return false; + } + + + @Override + protected String getAuthMethod() { + return HttpServletRequest.FORM_AUTH; + } + + + @Override + protected void register(Request request, HttpServletResponse response, Principal principal, String authType, + String username, String password, boolean alwaysUseSession, boolean cache) { + + super.register(request, response, principal, authType, username, password, alwaysUseSession, cache); + + // If caching an authenticated Principal is turned off, + // store username and password as session notes to use them for re-authentication. + if (!cache) { + Session session = request.getSessionInternal(false); + if (session != null) { + if (username != null) { + session.setNote(Constants.SESS_USERNAME_NOTE, username); + } else { + session.removeNote(Constants.SESS_USERNAME_NOTE); + } + if (password != null) { + session.setNote(Constants.SESS_PASSWORD_NOTE, password); + } else { + session.removeNote(Constants.SESS_PASSWORD_NOTE); + } + } + } + } + + + /** + * Called to forward to the login page + * + * @param request Request we are processing + * @param response Response we are populating + * @param config Login configuration describing how authentication should be performed + * + * @throws IOException If the forward to the login page fails and the call to + * {@link HttpServletResponse#sendError(int, String)} throws an {@link IOException} + */ + protected void forwardToLoginPage(Request request, HttpServletResponse response, LoginConfig config) + throws IOException { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("formAuthenticator.forwardLogin", request.getRequestURI(), request.getMethod(), + config.getLoginPage(), context.getName())); + } + + String loginPage = config.getLoginPage(); + if (loginPage == null || loginPage.length() == 0) { + String msg = sm.getString("formAuthenticator.noLoginPage", context.getName()); + log.warn(msg); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg); + return; + } + + if (getChangeSessionIdOnAuthentication()) { + Session session = request.getSessionInternal(false); + if (session != null) { + String oldSessionId = session.getId(); + String newSessionId = changeSessionID(request, session); + session.setNote(Constants.SESSION_ID_NOTE, newSessionId); + if (log.isDebugEnabled()) { + log.debug(sm.getString("formAuthenticator.changeSessionIdLogin", oldSessionId, newSessionId)); + } + } + } + + // Always use GET for the login page, regardless of the method used + String oldMethod = request.getMethod(); + request.getCoyoteRequest().method().setString("GET"); + + RequestDispatcher disp = context.getServletContext().getRequestDispatcher(loginPage); + try { + if (context.fireRequestInitEvent(request.getRequest())) { + disp.forward(request.getRequest(), response); + context.fireRequestDestroyEvent(request.getRequest()); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + String msg = sm.getString("formAuthenticator.forwardLoginFail"); + log.warn(msg, t); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg); + } finally { + // Restore original method so that it is written into access log + request.getCoyoteRequest().method().setString(oldMethod); + } + } + + + /** + * Called to forward to the error page + * + * @param request Request we are processing + * @param response Response we are populating + * @param config Login configuration describing how authentication should be performed + * + * @throws IOException If the forward to the error page fails and the call to + * {@link HttpServletResponse#sendError(int, String)} throws an {@link IOException} + */ + protected void forwardToErrorPage(Request request, HttpServletResponse response, LoginConfig config) + throws IOException { + + String errorPage = config.getErrorPage(); + if (errorPage == null || errorPage.length() == 0) { + String msg = sm.getString("formAuthenticator.noErrorPage", context.getName()); + log.warn(msg); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg); + return; + } + + RequestDispatcher disp = context.getServletContext().getRequestDispatcher(config.getErrorPage()); + try { + if (context.fireRequestInitEvent(request.getRequest())) { + disp.forward(request.getRequest(), response); + context.fireRequestDestroyEvent(request.getRequest()); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + String msg = sm.getString("formAuthenticator.forwardErrorFail"); + log.warn(msg, t); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg); + } + } + + + /** + * Does this request match the saved one (so that it must be the redirect we signaled after successful + * authentication? + * + * @param request The request to be verified + * + * @return true if the requests matched the saved one + */ + protected boolean matchRequest(Request request) { + // Has a session been created? + Session session = request.getSessionInternal(false); + if (session == null) { + return false; + } + + // Is there a saved request? + SavedRequest sreq = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE); + if (sreq == null) { + return false; + } + + // Is there a saved principal? + if (cache && session.getPrincipal() == null || !cache && request.getPrincipal() == null) { + return false; + } + + // Does session id match? + if (getChangeSessionIdOnAuthentication()) { + String expectedSessionId = (String) session.getNote(Constants.SESSION_ID_NOTE); + if (expectedSessionId == null || !expectedSessionId.equals(request.getRequestedSessionId())) { + return false; + } + } + + // Does the request URI match? + String decodedRequestURI = request.getDecodedRequestURI(); + if (decodedRequestURI == null) { + return false; + } + return decodedRequestURI.equals(sreq.getDecodedRequestURI()); + } + + + /** + * Restore the original request from information stored in our session. If the original request is no longer present + * (because the session timed out), return false; otherwise, return true. + * + * @param request The request to be restored + * @param session The session containing the saved information + * + * @return true if the request was successfully restored + * + * @throws IOException if an IO error occurred during the process + */ + protected boolean restoreRequest(Request request, Session session) throws IOException { + + // Retrieve and remove the SavedRequest object from our session + SavedRequest saved = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE); + session.removeNote(Constants.FORM_REQUEST_NOTE); + session.removeNote(Constants.SESSION_ID_NOTE); + if (saved == null) { + return false; + } + + // Swallow any request body since we will be replacing it + // Need to do this before headers are restored as AJP connector uses + // content length header to determine how much data needs to be read for + // request body + byte[] buffer = new byte[4096]; + InputStream is = request.createInputStream(); + while (is.read(buffer) >= 0) { + // Ignore request body + } + + // Modify our current request to reflect the original one + request.clearCookies(); + Iterator cookies = saved.getCookies(); + while (cookies.hasNext()) { + request.addCookie(cookies.next()); + } + + String method = saved.getMethod(); + MimeHeaders rmh = request.getCoyoteRequest().getMimeHeaders(); + rmh.recycle(); + boolean cacheable = "GET".equalsIgnoreCase(method) || "HEAD".equalsIgnoreCase(method); + Iterator names = saved.getHeaderNames(); + while (names.hasNext()) { + String name = names.next(); + // The browser isn't expecting this conditional response now. + // Assuming that it can quietly recover from an unexpected 412. + // BZ 43687 + if (!("If-Modified-Since".equalsIgnoreCase(name) || + (cacheable && "If-None-Match".equalsIgnoreCase(name)))) { + Iterator values = saved.getHeaderValues(name); + while (values.hasNext()) { + rmh.addValue(name).setString(values.next()); + } + } + } + + request.clearLocales(); + Iterator locales = saved.getLocales(); + while (locales.hasNext()) { + request.addLocale(locales.next()); + } + + request.getCoyoteRequest().getParameters().recycle(); + + ByteChunk body = saved.getBody(); + + if (body != null) { + request.getCoyoteRequest().action(ActionCode.REQ_SET_BODY_REPLAY, body); + + // Set content type + MessageBytes contentType = MessageBytes.newInstance(); + + // If no content type specified, use default for POST + String savedContentType = saved.getContentType(); + if (savedContentType == null && "POST".equalsIgnoreCase(method)) { + savedContentType = "application/x-www-form-urlencoded"; + } + + contentType.setString(savedContentType); + request.getCoyoteRequest().setContentType(contentType); + } + + request.getCoyoteRequest().method().setString(method); + // The method, URI, queryString and protocol are normally stored as + // bytes in the HttpInputBuffer and converted lazily to String. At this + // point, the method has already been set as String in the line above + // but the URI, queryString and protocol are still in byte form in the + // HttpInputBuffer. Processing the saved request body will overwrite + // these bytes. Configuring the HttpInputBuffer to retain these bytes as + // it would in a normal request would require some invasive API changes. + // Therefore force the conversion to String now so the correct values + // are presented if the application requests them. + request.getCoyoteRequest().requestURI().toStringType(); + request.getCoyoteRequest().queryString().toStringType(); + request.getCoyoteRequest().protocol().toStringType(); + + if (saved.getOriginalMaxInactiveInterval() > 0) { + session.setMaxInactiveInterval(saved.getOriginalMaxInactiveInterval()); + } + + return true; + } + + + /** + * Save the original request information into our session. + * + * @param request The request to be saved + * @param session The session to contain the saved information + * + * @throws IOException if an IO error occurred during the process + */ + protected void saveRequest(Request request, Session session) throws IOException { + + // Create and populate a SavedRequest object for this request + SavedRequest saved = new SavedRequest(); + Cookie cookies[] = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + saved.addCookie(cookie); + } + } + Enumeration names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + Enumeration values = request.getHeaders(name); + while (values.hasMoreElements()) { + String value = values.nextElement(); + saved.addHeader(name, value); + } + } + Enumeration locales = request.getLocales(); + while (locales.hasMoreElements()) { + Locale locale = locales.nextElement(); + saved.addLocale(locale); + } + + // May need to acknowledge a 100-continue expectation + request.getResponse().sendAcknowledgement(ContinueResponseTiming.ALWAYS); + + int maxSavePostSize = request.getConnector().getMaxSavePostSize(); + if (maxSavePostSize != 0) { + ByteChunk body = new ByteChunk(); + body.setLimit(maxSavePostSize); + + byte[] buffer = new byte[4096]; + int bytesRead; + InputStream is = request.getInputStream(); + + while ((bytesRead = is.read(buffer)) >= 0) { + body.append(buffer, 0, bytesRead); + } + + // Only save the request body if there is something to save + if (body.getLength() > 0) { + saved.setContentType(request.getContentType()); + saved.setBody(body); + } + } + + saved.setMethod(request.getMethod()); + saved.setQueryString(request.getQueryString()); + saved.setRequestURI(request.getRequestURI()); + saved.setDecodedRequestURI(request.getDecodedRequestURI()); + + SavedRequest previousSavedRequest = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE); + if (session instanceof HttpSession) { + if (((HttpSession) session).isNew()) { + int originalMaxInactiveInterval = session.getMaxInactiveInterval(); + if (originalMaxInactiveInterval > getAuthenticationSessionTimeout()) { + saved.setOriginalMaxInactiveInterval(originalMaxInactiveInterval); + session.setMaxInactiveInterval(getAuthenticationSessionTimeout()); + } + } else if (previousSavedRequest != null && previousSavedRequest.getOriginalMaxInactiveInterval() > 0) { + /* + * The user may have refreshed the browser page during authentication. Transfer the original max inactive + * interval from previous saved request to current one else, once authentication is completed, the session + * will retain the the shorter authentication session timeout + */ + saved.setOriginalMaxInactiveInterval(previousSavedRequest.getOriginalMaxInactiveInterval()); + } + } + + // Stash the SavedRequest in our session for later use + session.setNote(Constants.FORM_REQUEST_NOTE, saved); + } + + + /** + * Return the request URI (with the corresponding query string, if any) from the saved request so that we can + * redirect to it. + * + * @param session Our current session + * + * @return the original request URL + */ + protected String savedRequestURL(Session session) { + SavedRequest saved = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE); + if (saved == null) { + return null; + } + StringBuilder sb = new StringBuilder(saved.getRequestURI()); + if (saved.getQueryString() != null) { + sb.append('?'); + sb.append(saved.getQueryString()); + } + + // Avoid protocol relative redirects + while (sb.length() > 1 && sb.charAt(1) == '/') { + sb.deleteCharAt(0); + } + + return sb.toString(); + } +} diff --git a/java/org/apache/catalina/authenticator/LocalStrings.properties b/java/org/apache/catalina/authenticator/LocalStrings.properties new file mode 100644 index 0000000..62825a2 --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings.properties @@ -0,0 +1,94 @@ +# 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. + +authenticator.authentication=Authenticated principal [{0}] with authentication type [{1}] +authenticator.authenticationFail=User authentication failure +authenticator.certificates=No client certificate chain in this request +authenticator.changeSessionId=Session ID changed on authentication from [{0}] to [{1}] +authenticator.check.authorize=User name [{0}] obtained from the Connector and trusted to be valid. Obtaining roles for this user from the Tomcat Realm. +authenticator.check.authorizeFail=Realm did not recognise user [{0}]. Creating a Principal with that name and no roles. +authenticator.check.found=Already authenticated [{0}] +authenticator.check.sso=Not authenticated but SSO session ID [{0}] found. Attempting re-authentication. +authenticator.corsBypass=CORS Preflight request bypassing authentication +authenticator.formlogin=Invalid direct reference to form login page +authenticator.jaspicCleanSubjectFail=Failed to clean JASPIC subject +authenticator.jaspicSecureResponseFail=Failed to secure response during JASPIC processing +authenticator.jaspicServerAuthContextFail=Failed to obtain a JASPIC ServerAuthContext instance +authenticator.loginFail=Login failed +authenticator.manager=Exception initializing trust managers +authenticator.noAuthHeader=No authorization header sent by client +authenticator.notContext=Configuration error: Must be attached to a Context +authenticator.reauthentication=Reauthenticated cached principal [{0}] with authentication type [{1}] +authenticator.requestBodyTooBig=The request body was too large to be cached during the authentication process +authenticator.sessionExpired=The time allowed for the login process has been exceeded. If you wish to continue you must either click back twice and re-click the link you requested or close and re-open your browser +authenticator.sso=Found SSO [{0}] +authenticator.tomcatPrincipalLogoutFail=Logout with TomcatPrincipal instance has failed +authenticator.unauthorized=Cannot authenticate with the provided credentials +authenticator.userDataPermissionFail=User data does not comply with the constraints of the resource +authenticator.userPermissionFail=User [{0}] does not have authorization to access the resource + +basicAuthenticator.invalidAuthorization=Invalid Authorization: [{0}] +basicAuthenticator.invalidCharset=The only permitted values are null, the empty string or UTF-8 +basicAuthenticator.notBase64=Basic Authorization credentials are not Base64 +basicAuthenticator.notBasic=Authorization header method is not ''Basic'' + +digestAuthenticator.cacheRemove=A valid entry has been removed from client nonce cache to make room for new entries. A replay attack is now possible. To prevent the possibility of replay attacks, reduce nonceValidity or increase nonceCacheSize. Further warnings of this type will be suppressed for 5 minutes. +digestAuthenticator.invalidAlgorithm=Unable to configure DIGEST authentication to use the algorithm [{0}] as it is not permitted by RFC 7616. +digestAuthenticator.unsupportedAlgorithm=Unable to configure DIGEST authentication to use the algorithms [{0}] as [{1}] is not supported by the JRE. + +formAuthenticator.changeSessionIdLogin=Session ID changed before forwarding to login page during FORM authentication from [{0}] to [{1}] +formAuthenticator.forwardErrorFail=Unexpected error forwarding to error page +formAuthenticator.forwardLogin=Forwarding request for [{0}] made with method [{1}] to login page [{2}] of context [{3}] using request method GET +formAuthenticator.forwardLoginFail=Unexpected error forwarding to login page +formAuthenticator.noErrorPage=No error page was defined for FORM authentication in context [{0}] +formAuthenticator.noLoginPage=No login page was defined for FORM authentication in context [{0}] +formAuthenticator.reauthFailed=Reauthentication failed, proceeding with regular authentication +formAuthenticator.restoreFailed=Restoring the original request failed +formAuthenticator.sessionExpired=Client inactivity caused the session to expire during authentication +formAuthenticator.sessionIdMismatch=Current session ID is [{0}] but FORM authenticator was expecting [{1}] + +singleSignOn.debug.associate=SSO associating application session [{1}] with SSO session [{0}] +singleSignOn.debug.associateFail=SSO failed to associate application session [{0}] since SSO session [{1}] does not exist +singleSignOn.debug.cookieCheck=SSO checking for SSO cookie +singleSignOn.debug.cookieNotFound=SSO did not find an SSO cookie +singleSignOn.debug.deregister=SSO expiring application session [{0}] associated with SSO session [{1}] +singleSignOn.debug.deregisterFail=SSO failed to deregister the SSO session [{0}] because it was not in the cache +singleSignOn.debug.deregisterNone=SSO deregistered the SSO session [{0}] but found no associated application sessions +singleSignOn.debug.hasPrincipal=SSO found previously authenticated Principal [{0}] +singleSignOn.debug.invoke=SSO processing request for [{0}] +singleSignOn.debug.principalCheck=SSO looking for a cached Principal for SSO session [{0}] +singleSignOn.debug.principalFound=SSO found cached Principal [{0}] with authentication type [{1}] +singleSignOn.debug.principalNotFound=SSO did not find a cached Principal. Erasing SSO cookie for session [{0}] +singleSignOn.debug.register=SSO registering SSO session [{0}] for user [{1}] with authentication type [{2}] +singleSignOn.debug.removeSession=SSO removing application session [{0}] from SSO session [{1}] +singleSignOn.debug.sessionLogout=SSO processing a log out for SSO session [{0}] and application session [{1}] +singleSignOn.debug.sessionTimeout=SSO processing a time out for SSO session [{0}] and application session [{1}] +singleSignOn.debug.update=SSO updating SSO session [{0}] to authentication type [{1}] +singleSignOn.sessionExpire.contextNotFound=SSO unable to expire session [{0}] because the Context could not be found +singleSignOn.sessionExpire.engineNull=SSO unable to expire session [{0}] because the Engine was null +singleSignOn.sessionExpire.hostNotFound=SSO unable to expire session [{0}] because the Host could not be found +singleSignOn.sessionExpire.managerError=SSO unable to expire session [{0}] because the Manager threw an Exception when searching for the session +singleSignOn.sessionExpire.managerNotFound=SSO unable to expire session [{0}] because the Manager could not be found +singleSignOn.sessionExpire.sessionNotFound=SSO unable to expire session [{0}] because the Session could not be found + +spnegoAuthenticator.authHeaderNoToken=The Negotiate authorization header sent by the client did not include a token +spnegoAuthenticator.authHeaderNotNego=The authorization header sent by the client did not start with Negotiate +spnegoAuthenticator.serviceLoginFail=Unable to login as the service principal +spnegoAuthenticator.ticketValidateFail=Failed to validate client supplied ticket + +sslAuthenticatorValve.authFailed=Authentication with the provided certificates failed +sslAuthenticatorValve.http2=The context [{0}] in virtual host [{1}] is configured to use CLIENT-CERT authentication and [{2}] is configured to support HTTP/2. Use of CLIENT-CERT authentication is not compatible with the use of HTTP/2. +sslAuthenticatorValve.noCertificates=No certificates are included with this request +sslAuthenticatorValve.tls13=The context [{0}] in virtual host [{1}] is configured to use CLIENT-CERT authentication and [{2}] is configured to support TLS 1.3 using JSSE. Use of CLIENT-CERT authentication is not compatible with the use of TLS 1.3 and JSSE. diff --git a/java/org/apache/catalina/authenticator/LocalStrings_cs.properties b/java/org/apache/catalina/authenticator/LocalStrings_cs.properties new file mode 100644 index 0000000..80cd0b7 --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_cs.properties @@ -0,0 +1,26 @@ +# 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. + +authenticator.formlogin=Neplatná přímá reference na formulář pÅ™ihlaÅ¡ovací stránky +authenticator.jaspicCleanSubjectFail=Chyba ÄiÅ¡tÄ›ní JASPIC pÅ™edmÄ›tu +authenticator.jaspicServerAuthContextFail=Získání instance JASPIC ServerAuthContext selhalo +authenticator.sessionExpired=ÄŒasový rámec pro pÅ™ihlášení byl pÅ™ekroÄen. Pokud chcete pokraÅ™ovat, tak musíte kliknout dvakrát zpÄ›t a znovu kliknout na požadovaný link, nebo zavřít a znovu otevřít Váš prohlížeÄ + +singleSignOn.debug.principalFound=SSO naÅ¡lo cachovaný objekt Principal [{0}] s autentizaÄním typem [{1}] +singleSignOn.debug.removeSession=SSO odstraňuje aplikaÄní session [{0}] z SSO session [{1}] +singleSignOn.sessionExpire.hostNotFound=SSO nemůže ukonÄit session [{0}], protože host nebyl nalezen +singleSignOn.sessionExpire.managerError=SSO nemůže expirovat relaci [{0}], protože Manager vyhodil Exception pÅ™i vyhledávání relace + +spnegoAuthenticator.authHeaderNoToken=Domlouvací autorizaÄní hlaviÄka zaslaná klientem neobsahuje token diff --git a/java/org/apache/catalina/authenticator/LocalStrings_de.properties b/java/org/apache/catalina/authenticator/LocalStrings_de.properties new file mode 100644 index 0000000..d6eb4c9 --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_de.properties @@ -0,0 +1,28 @@ +# 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. + +authenticator.certificates=Keine Client Zertifikatskette im Request +authenticator.check.found=Bereits authentifiziert [{0}] +authenticator.jaspicCleanSubjectFail=Konnte JASPIC Subject nicht leeren. +authenticator.jaspicServerAuthContextFail=Kontte keine JASPIC ServerAuthContext Instanz erhalten +authenticator.sessionExpired=Die erlaubte Zeit für den Login-Prozess wurde überschritten. Wenn sie weiter machen wollen müssen sie entweder zwei mal zurück klicken und den angeforderten Link erneut klicken oder den Browser schließen und wieder öffnen + +singleSignOn.debug.cookieCheck=SSO prüfe nach SSO Cookie +singleSignOn.debug.principalFound=SSO fand Principal [{0}] mut Authentication Typ [{1}] im Cache +singleSignOn.debug.removeSession=SSO entfernt Applications-Session [{0}] von SSO Session [{1}] +singleSignOn.sessionExpire.hostNotFound=SSO kann Session [{0}] nicht ablaufen lassen, da der Host nicht gefunden werden konnte +singleSignOn.sessionExpire.managerError=SSO kann die Session [{0}] nicht invalidieren weil der Manager einen Fehler , während der Suche nach der Session geworfen hat + +spnegoAuthenticator.authHeaderNoToken=Der vom Client gesendete Negotiate authorization header enthält kein Token diff --git a/java/org/apache/catalina/authenticator/LocalStrings_es.properties b/java/org/apache/catalina/authenticator/LocalStrings_es.properties new file mode 100644 index 0000000..6cc945d --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_es.properties @@ -0,0 +1,44 @@ +# 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. + +authenticator.certificates=No hay cadena de certificados del cliente en esta petición +authenticator.formlogin=Referencia directa al formulario de conexión (página de formulario de login) inválida +authenticator.jaspicCleanSubjectFail=Fallo al limpiar el elemento JASPIC \n +authenticator.jaspicServerAuthContextFail=Fallo al intentar obtener una instancia JASPIC ServerAuthContext +authenticator.loginFail=No pude ingresar +authenticator.manager=Excepción inicializando administradores de confianza +authenticator.noAuthHeader=El cliente no ha enviado autorización de cabecera +authenticator.notContext=Error de Configuración: Debe de estar unido a un Contexto +authenticator.requestBodyTooBig=El cuerpo del requerimiento era demasiado grande para realizar caché durante el proceso de autenticación +authenticator.sessionExpired=El tiempo permitido para realizar login ha sido excedido. Si deseas continuar, debes hacer clik dos veces y volver a hacer clik otra vez o cerrar y reabrir tu navegador +authenticator.unauthorized=Imposible autenticar mediante las credenciales suministradas + +digestAuthenticator.cacheRemove=Se ha quitado una entrada válida de la caché "nonce" del cliente para hacer espacio a nuevas entradas.. Ahora es posible un ataque de reinyección. Para prevenirlos, reduce "nonceValidity" o incrementa "nonceCacheSize". El resto de mensajes de este tipo serán suspendidos durante 5 minutos. + +formAuthenticator.forwardErrorFail=Error inesperado de reenvío a página de error +formAuthenticator.forwardLoginFail=Error inesperado de reenvío a pagina de ingreso +formAuthenticator.noErrorPage=No se ha definido página de error para la autenticación FORM en el contexto [{0}] +formAuthenticator.noLoginPage=No se ha definido página de ingreso para la autenticación FORM en el contexto [{0}] + +singleSignOn.debug.principalCheck=SSO esta buscando un Principal cacheado para las sesión SSO [{0}]\n +singleSignOn.debug.principalFound=SSO encontró el Principal cacheado [{0}] con autenticación tipo [{1}]\n +singleSignOn.debug.removeSession=SSO removiendo la sesión de la aplicación [{0}] SSO con sesión [{1}]\n +singleSignOn.sessionExpire.hostNotFound=SSO es incapaz de expirar la session [{0}] porque el Host no puede ser encontrado +singleSignOn.sessionExpire.managerError=SSO incapaz de expirar sesión [{0}] porque el Gerenciador lanzó una excepción mientras buscaba la sesión + +spnegoAuthenticator.authHeaderNoToken=La cabecera de Negociación de autorización enviada por el cliente no incluía una ficha +spnegoAuthenticator.authHeaderNotNego=La cabecera de autorización enviada por el cliente no comenzaba con Negotiate +spnegoAuthenticator.serviceLoginFail=No puedo ingresar como director del servicio +spnegoAuthenticator.ticketValidateFail=No pude validar el billete suministrado por el cliente diff --git a/java/org/apache/catalina/authenticator/LocalStrings_fr.properties b/java/org/apache/catalina/authenticator/LocalStrings_fr.properties new file mode 100644 index 0000000..ec3dfe1 --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_fr.properties @@ -0,0 +1,94 @@ +# 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. + +authenticator.authentication=Le principal [{0}] a été authentifié avec le type d''authentification [{1}] +authenticator.authenticationFail=Erreur d'authentification de l'utilisateur +authenticator.certificates=Aucune chaîne de certificat client (client certificate chain) dans cette requête +authenticator.changeSessionId=L''id de session a changé suite à l''authntification de [{0}] en [{1}] +authenticator.check.authorize=Le nom d''utilisateur [{0}] obtenu à partir du connecteur est considéré comme de valide et de confiance, les rôles sont obtenus à partir du royaume +authenticator.check.authorizeFail=Le royaume ne reconnait pas l''utilisateur [{0}], un principal a été crée avec ce nom mais sans rôles +authenticator.check.found=Déjà authentifié [{0}] +authenticator.check.sso=Pas d''authentification mais un session ID SSO [{0}] a été trouvé, nouvelle tentative d''authentification +authenticator.corsBypass=La requête CORS Preflight évite l'authentification +authenticator.formlogin=Référence directe au formulaire de connexion (form login page) invalide +authenticator.jaspicCleanSubjectFail=Échec du nettoyage du sujet de JASPIC +authenticator.jaspicSecureResponseFail=Echec de la sécurisation de la réponse lors du traitement de JASPIC +authenticator.jaspicServerAuthContextFail=Échec d'obtention d'une instance JASPIC ServerAuthContext +authenticator.loginFail=Échec de connexion ("Login failed") +authenticator.manager=Exception lors de l'initialisation des gestionnaires d'authentification (trust managers) +authenticator.noAuthHeader=Aucun en-tête d'autorisation envoyé par le client +authenticator.notContext=Erreur de configuration : Doit être attaché à un contexte +authenticator.reauthentication=Le principal en cache [{0}] est réauthentifié avec le type d''authentification [{1}] +authenticator.requestBodyTooBig=Le corps de la requête était trop grand pour être mis en cache pendant le processus d'authentification +authenticator.sessionExpired=Le temps alloué au processus de login est échu. Si vous désirez continuer, veuillez soit retourner en arrière 2 fois et recliquer le lien demandé, soit fermer et ré-ouvrir votre navigateur +authenticator.sso=Le SSO [{0}] a été trouvé +authenticator.tomcatPrincipalLogoutFail=La déconnection avec l'instance de TomcatPrincipal a échoué +authenticator.unauthorized=Impossible d'authentifier avec les crédits fournis (provided credentials) +authenticator.userDataPermissionFail=Les données envoyées par l'utilisateur ne répondent pas aux contraintes définies pour la ressource +authenticator.userPermissionFail=L''utilisateur [{0}] n''a pas l''autorisation d''accéder à la ressource + +basicAuthenticator.invalidAuthorization=L''autorisation est invalide: [{0}] +basicAuthenticator.invalidCharset=Les seules valeurs permises sont null, la chaîne vide, ou des caractères UTF-8 +basicAuthenticator.notBase64=Les informations d'identification Basic ne sont pas encodées en Base64 +basicAuthenticator.notBasic=La méthode d'authentification n'est pas ''Basic'' + +digestAuthenticator.cacheRemove=Une entrée valide du cache de nonce des clients a été enlevée pour faire de la place pour de nouvelles entrées, ce qui rend possible une attaque par répétition ; pour éviter cela, il est possible de reduire nonceValidity ou d'augmenter nonceCacheSize ; les avertissements de ce type ne se reproduiront pas avant 5 minutes +digestAuthenticator.invalidAlgorithm=Impossible de configurer l''authentification DIGEST avec l''algorithme [{0}] car il n''est pas autorisé par la RFC 7616 +digestAuthenticator.unsupportedAlgorithm=Impossible de configurer l''authentification DIGEST avec l''algorithme [{0}] en tant que [{1}] car il n''est pas supporté par le JRE + +formAuthenticator.changeSessionIdLogin=L''id de session a été changé avant de forwarder vers la page de login lors de l''authentification FORM de [{0}] vers [{1}] +formAuthenticator.forwardErrorFail=Erreur inattendue lors de la transmission à la page d'erreur +formAuthenticator.forwardLogin=Transmission de la requête pour [{0}] faite avec la méthode [{1}] à la page de connection [{2}] du contexte [{3}] en utilisant la méthode GET +formAuthenticator.forwardLoginFail=Erreur inattendue lors de la transmission à la page de connection +formAuthenticator.noErrorPage=Aucune page d''erreur n''a été définie pour la méthode d''authentification FORM dans le contexte [{0}] +formAuthenticator.noLoginPage=Aucune page de connection n''a été définie pour la méthode d''authentification FORM dans le contexte [{0}] +formAuthenticator.reauthFailed=La réauthentification a échouée, l'authentification classique va être utilisée +formAuthenticator.restoreFailed=Echec de la restauration de la requête d'origine +formAuthenticator.sessionExpired=L'inactivité du client a causé l'expiration de la session pendant l'authentification +formAuthenticator.sessionIdMismatch=L''id de session actuel est [{0}] mais l''authentificateur FORM attendait [{1}] + +singleSignOn.debug.associate=Association de la session [{1}] de l''application avec la session SSO [{0}] +singleSignOn.debug.associateFail=Le SSO n''a pu associer la session [{0}] de l''application car la session SSO [{1}] n''existe pas +singleSignOn.debug.cookieCheck=Le SSO recherche un cookie SSO. +singleSignOn.debug.cookieNotFound=Le SSO n'a pas trouvé de cookie SSO +singleSignOn.debug.deregister=Le SSO expire la session [{0}] de l''application associée à la session SSO [{1}] +singleSignOn.debug.deregisterFail=Le SSO n''a pu déenregistrer la session SSO [{0}] parce qu''elle n''est pas dans le cache +singleSignOn.debug.deregisterNone=Le SSO a désenregistré la session SSO [{0}] mais n''a trouvé aucune session d''application associée +singleSignOn.debug.hasPrincipal=Le SSO a trouvé un principal [{0}] précédemment authentifié +singleSignOn.debug.invoke=Le SSO traite la requête pour [{0}] +singleSignOn.debug.principalCheck=Le SSO recherche le Principal en cache pour la session SSO [{0}] +singleSignOn.debug.principalFound=Le SSO a trouvé en cache le Principal [{0}] avec le type d''authentification [{1}] +singleSignOn.debug.principalNotFound=Le SSO n''a pas trouvé de principal en cache, le cookie SSO de la session [{0}] est effacé +singleSignOn.debug.register=Enregistrement de la session SSO [{0}] pour l''utilisateur [{1}] avec le type d''authentification [{2}] +singleSignOn.debug.removeSession=Le SSO retire la session applicative [{0}] de la session SSO [{1}] +singleSignOn.debug.sessionLogout=Le SSO effectue une déconnection pour la session SSO [{0}] et la session [{1}] de l''application +singleSignOn.debug.sessionTimeout=Le SSO traite un timeout pour la session SSO [{0}] et la session [{1}] de l''application +singleSignOn.debug.update=Le SSO met à jour la session SSO [{0}] avec le type d''authentification [{1}] +singleSignOn.sessionExpire.contextNotFound=Le SSO n''a pu faire expirer la session [{0}] parce que le contexte n''a pas été trouvé +singleSignOn.sessionExpire.engineNull=Le SSO n''a pu faire expirer la session [{0}] parce que le moteur est null +singleSignOn.sessionExpire.hostNotFound=SSO ne peut pas faire expirer le session [{0}] parce que l''hôte ("Host") n''a pas été trouvé +singleSignOn.sessionExpire.managerError=Impossible d''expirer la session [{0}] parce que le Manager a lancé une exception lors de la recherche de la session +singleSignOn.sessionExpire.managerNotFound=Le SSO n''a pu faire expirer la session [{0}] parce que le gestionnaire de sessions n''a pas été trouvé +singleSignOn.sessionExpire.sessionNotFound=Impossible d''expirer la session [{0}] parce que la session n''a pas été trouvée + +spnegoAuthenticator.authHeaderNoToken=L'en-tête de négociation d’autorisation ("Negotiate authorization header") envoyé par le client n'incluait pas de jeton ("token") +spnegoAuthenticator.authHeaderNotNego=L'en-tête d'autorisation envoyé par le client ne commence pas par Negotiate +spnegoAuthenticator.serviceLoginFail=Impossible de se connecteur en tant que principal de service +spnegoAuthenticator.ticketValidateFail=Impossible de valider le ticket fourni par le client + +sslAuthenticatorValve.authFailed=L'authentification avec les certificats fournis a échouée +sslAuthenticatorValve.http2=Le contexte [{0}] dans l''hôte vituel [{1}] est configuré pour utiliser l''autentification CLIENT-CERT et [{2}] est configuré avec le support de HTTP/2. L''utilisation de l''authentification CLIENT-CERT est incompatible avec l''utilisation de HTTP/2. +sslAuthenticatorValve.noCertificates=Aucun certificat n'est fourni avec cette requête +sslAuthenticatorValve.tls13=Le contexte [{0}] dans l''hôte vituel [{1}] est configuré pour utiliser l''autentification CLIENT-CERT et [{2}] est configuré pour supporter TLS 1.3 avec JSSE. L''utilisation de l''authentification CLIENT-CERT est incompatible avec l''utilisation de TLS/1.3 avec JSSE. diff --git a/java/org/apache/catalina/authenticator/LocalStrings_ja.properties b/java/org/apache/catalina/authenticator/LocalStrings_ja.properties new file mode 100644 index 0000000..4403c97 --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_ja.properties @@ -0,0 +1,94 @@ +# 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. + +authenticator.authentication=èªè¨¼ã‚¿ã‚¤ãƒ— [{1}] ã§èªè¨¼ã•ã‚ŒãŸãƒ—リンシパル [{0}] +authenticator.authenticationFail=User èªè¨¼å¤±æ•— +authenticator.certificates=ã“ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ã¯ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆèªè¨¼ãƒã‚§ãƒ¼ãƒ³ãŒã‚ã‚Šã¾ã›ã‚“ +authenticator.changeSessionId=èªè¨¼æ™‚ã« [{0}] ã‹ã‚‰ [{1}] ã«ã‚»ãƒƒã‚·ãƒ§ãƒ³IDãŒå¤‰æ›´ã•ã‚Œã¾ã—㟠+authenticator.check.authorize=Connector ã‹ã‚‰å–å¾—ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼å [{0}] を正当ãªã‚‚ã®ã¨ã—ã¦ä¿¡é ¼ã—ã¾ã™ã€‚ユーザーã®ãƒ­ãƒ¼ãƒ«ã¯ Tomcat Realmã‹ã‚‰å–å¾—ã—ã¾ã™ã€‚ +authenticator.check.authorizeFail=Realm ãŒãƒ¦ãƒ¼ã‚¶ãƒ¼[{0}]ã‚’èªè­˜ã—ã¾ã›ã‚“ã§ã—ãŸã€‚ ãã®åå‰ã¨ãƒ­ãƒ¼ãƒ«ã®ãªã„プリンシパルを作æˆã—ã¾ã™ã€‚ +authenticator.check.found=æ—¢ã«èªè¨¼ã•ã‚ŒãŸ [{0}] +authenticator.check.sso=èªè¨¼ã•ã‚Œã¦ã„ã¾ã›ã‚“ãŒã€SSOセッションID [{0}]ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚ å†èªè¨¼ã‚’試ã¿ã¾ã™ã€‚ +authenticator.corsBypass=CORS プリフライト リクエストã«ã‚ˆã‚‹èªè¨¼ã®ãƒã‚¤ãƒ‘ス +authenticator.formlogin=フォームログインページã¸ã®ç„¡åŠ¹ãªç›´æŽ¥å‚ç…§ã§ã™ +authenticator.jaspicCleanSubjectFail=JASPIC Subject ã®ã‚¯ãƒªãƒ¼ãƒ³ã‚¢ãƒƒãƒ—ã«å¤±æ•—ã—ã¾ã—㟠+authenticator.jaspicSecureResponseFail=JASPIC処ç†ä¸­ã®secure レスãƒãƒ³ã‚¹ã«å¤±æ•—ã—ã¾ã—㟠+authenticator.jaspicServerAuthContextFail=JASPIC ServerAuthContext インスタンスã®å–å¾—ã«å¤±æ•—ã—ã¾ã—㟠+authenticator.loginFail=ログイン失敗 +authenticator.manager=トラストマãƒãƒ¼ã‚¸ãƒ£ã‚’åˆæœŸåŒ–中ã®ä¾‹å¤–ã§ã™ +authenticator.noAuthHeader=クライアントã¯èªè¨¼ãƒ˜ãƒƒãƒ€ãƒ¼ã‚’é€ä¿¡ã—ã¾ã›ã‚“ã§ã—ãŸã€‚ +authenticator.notContext=設定エラー: コンテキストã«æŒ‡å®šã—ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +authenticator.reauthentication=èªè¨¼ã‚¿ã‚¤ãƒ— [{1}] ã®ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã•ã‚ŒãŸãƒ—リンシパル [{0}] ãŒå†èªè¨¼ã•ã‚Œã¾ã—㟠+authenticator.requestBodyTooBig=èªè¨¼å‡¦ç†ä¸­ã«ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒœãƒ‡ã‚£ãŒå¤§ãã™ãŽã¦ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã•ã‚Œã¾ã›ã‚“ã§ã—ãŸã€‚ +authenticator.sessionExpired=ログインプロセスã«èªã‚られã¦ã„ãŸæ™‚é–“ãŒéŽãŽã¾ã—ãŸã€‚継続ã—ãŸã„ãªã‚‰ã°ï¼Œãƒãƒƒã‚¯ãƒœã‚¿ãƒ³ã‚’2度押ã—ã¦ã‹ã‚‰å†åº¦ãƒªãƒ³ã‚¯ã‚’押ã™ã‹ï¼Œãƒ–ラウザを立ã¡ä¸Šã’ç›´ã—ã¦ãã ã•ã„ +authenticator.sso=SSO [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã—㟠+authenticator.tomcatPrincipalLogoutFail=TomcatPrincipal インスタンスã«ã‚ˆã‚‹ãƒ­ã‚°ã‚¢ã‚¦ãƒˆãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ +authenticator.unauthorized=æä¾›ã•ã‚ŒãŸè¨¼æ˜Žæ›¸ã§èªè¨¼ã§ãã¾ã›ã‚“ +authenticator.userDataPermissionFail=ユーザデータãŒãƒªã‚½ãƒ¼ã‚¹ã®åˆ¶ç´„ã«å¾“ã£ã¦ã„ã¾ã›ã‚“ +authenticator.userPermissionFail=ユーザ [{0}] ã«ã¯ãƒªã‚½ãƒ¼ã‚¹ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“ + +basicAuthenticator.invalidAuthorization=無効ãªèªè¨¼: [{0}] +basicAuthenticator.invalidCharset=指定ã§ãる値ã¯ã€nullã€ç©ºã®æ–‡å­—列ã¾ãŸã¯UTF-8ã§ã™ã€‚ +basicAuthenticator.notBase64=Basicèªè¨¼ã®è³‡æ ¼æƒ…å ±ãŒBase64ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +basicAuthenticator.notBasic=èªè¨¼ãƒ˜ãƒƒãƒ€ãƒ¡ã‚½ãƒƒãƒ‰ãŒ ''Basic'' ã§ã¯ã‚ã‚Šã¾ã›ã‚“ + +digestAuthenticator.cacheRemove=有効ãªã‚¨ãƒ³ãƒˆãƒªãŒã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã®nonceキャッシュã‹ã‚‰å‰Šé™¤ã•ã‚Œã€æ–°ã—ã„エントリã®ãŸã‚ã®ã‚¹ãƒšãƒ¼ã‚¹ãŒç¢ºä¿ã•ã‚Œã¾ã—ãŸã€‚ リプレイ攻撃ãŒå¯èƒ½ã«ãªã‚Šã¾ã—ãŸã€‚ リプレイ攻撃ã®å¯èƒ½æ€§ã‚’防ãã«ã¯ã€nonceValidityを減らã™ã‹ã€nonceCacheSizeを増やã—ã¦ãã ã•ã„。 ã“ã®ã‚¿ã‚¤ãƒ—ã®è­¦å‘Šã¯5分間表示ã•ã‚Œãªããªã‚Šã¾ã™ã€‚ +digestAuthenticator.invalidAlgorithm=RFC 7616 ã§è¨±å¯ã•ã‚Œã¦ã„ãªã„ãŸã‚ã€ã‚¢ãƒ«ã‚´ãƒªã‚ºãƒ  [{0}] を使用ã™ã‚‹ã‚ˆã†ã« DIGEST èªè¨¼ã‚’構æˆã§ãã¾ã›ã‚“。 +digestAuthenticator.unsupportedAlgorithm=[{1}] 㯠JRE ã§ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„ãŸã‚ã€ã‚¢ãƒ«ã‚´ãƒªã‚ºãƒ  [{0}] を使用ã™ã‚‹ã‚ˆã†ã« DIGEST èªè¨¼ã‚’構æˆã§ãã¾ã›ã‚“。 + +formAuthenticator.changeSessionIdLogin=FORM èªè¨¼ã§ãƒ­ã‚°ã‚¤ãƒ³ ページã«è»¢é€ã™ã‚‹å‰ã«ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ ID ㌠[{0}] ã‹ã‚‰ [{1}] ã«å¤‰æ›´ã•ã‚Œã¾ã—㟠+formAuthenticator.forwardErrorFail=エラーページã¸è»¢é€ä¸­ã®äºˆæœŸã›ã¬ã‚¨ãƒ©ãƒ¼ +formAuthenticator.forwardLogin=リクエストメソッドGETを使用ã—ã¦ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{3}] ã®ãƒšãƒ¼ã‚¸ [{2}] ã«ãƒ¡ã‚½ãƒƒãƒ‰ [{1}] ã§è¡Œã‚れ㟠[{0}] ã®è¦æ±‚をフォワードã—ã¾ã™ +formAuthenticator.forwardLoginFail=ログインページã¸ã®è»¢é€ã§ã®äºˆæœŸã—ãªã„エラー +formAuthenticator.noErrorPage=コンテキスト [{0}] ã®FORMèªè¨¼ã«ã‚¨ãƒ©ãƒ¼ãƒšãƒ¼ã‚¸ãŒå®šç¾©ã•ã‚Œã¦ã„ã¾ã›ã‚“ +formAuthenticator.noLoginPage=コンテキスト [{0}] ã®FORMèªè¨¼ã«ãƒ­ã‚°ã‚¤ãƒ³ãƒšãƒ¼ã‚¸ãŒå®šç¾©ã•ã‚Œã¦ã„ã¾ã›ã‚“ +formAuthenticator.reauthFailed=å†èªè¨¼ã«å¤±æ•—ã—ã¾ã—ãŸã€‚通常ã®èªè¨¼ã‚’続行ã—ã¾ã™ +formAuthenticator.restoreFailed=å…ƒã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å¾©å…ƒã«å¤±æ•—ã—ã¾ã—㟠+formAuthenticator.sessionExpired=クライアントãŒéžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã§ã‚ã‚‹ãŸã‚ã€èªè¨¼ä¸­ã«ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒæœŸé™åˆ‡ã‚Œã«ãªã‚Šã¾ã—㟠+formAuthenticator.sessionIdMismatch=ç¾åœ¨ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ID㯠[{0}] ã§ã™ãŒã€FORMèªè¨¼ 㯠[{1}] を期待ã—ã¦ã„ã¾ã—㟠+ +singleSignOn.debug.associate=SSO ã¯ã‚¢ãƒ—リケーションセッション [{1}] ã‚’ SSO セッション [{0}] ã«é–¢é€£ä»˜ã‘ã¾ã™ +singleSignOn.debug.associateFail=SSOセッション[{1}]ãŒå­˜åœ¨ã—ãªã„ãŸã‚ã€SSOã¯ã‚¢ãƒ—リケーションセッション[{0}]を関連付ã‘られã¾ã›ã‚“ã§ã—ãŸã€‚ +singleSignOn.debug.cookieCheck=SSOã¯SSO Cookieã‚’ãƒã‚§ãƒƒã‚¯ã—ã¦ã„ã¾ã™ +singleSignOn.debug.cookieNotFound=SSOã¯SSO Cookieを検出ã—ã¾ã›ã‚“ã§ã—ãŸã€‚ +singleSignOn.debug.deregister=SSOセッション[{1}]ã«é–¢é€£ä»˜ã‘られãŸã‚¢ãƒ—リケーションセッション[{0}]を破棄ã—ã¾ã™ã€‚ +singleSignOn.debug.deregisterFail=キャッシュã«ãªã„ãŸã‚ã€SSO セッション [{0}] ã®ç™»éŒ²ã‚’解除ã§ãã¾ã›ã‚“ã§ã—㟠+singleSignOn.debug.deregisterNone=SSOセッション[{0}]ã®ç™»éŒ²ã‚’解除ã—ã¾ã—ãŸãŒã€é–¢é€£ä»˜ã‘られãŸã‚¢ãƒ—リケーションセッションã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ +singleSignOn.debug.hasPrincipal=SSOãŒä»¥å‰ã«èªè¨¼ã•ã‚ŒãŸãƒ—リンシパル [{0}] を検出ã—ã¾ã—㟠+singleSignOn.debug.invoke=SSO 㯠[{0}] ã«å¯¾ã™ã‚‹ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã—ã¦ã„ã¾ã™ +singleSignOn.debug.principalCheck=SSO 㯠SSO セッション [{0}] ã®ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã•ã‚ŒãŸãƒ—リンシパルを探索ã—ã¦ã„ã¾ã™ +singleSignOn.debug.principalFound=SSO ã®ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã•ã‚ŒãŸãƒ—リンシパル [{0}] ã‚’å–å¾—ã—ã¾ã—ãŸã€‚èªè¨¼ã‚¿ã‚¤ãƒ—㯠[{1}] ã§ã™ +singleSignOn.debug.principalNotFound=SSO ã¯ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã•ã‚ŒãŸãƒ—リンシパルを検出ã—ã¾ã›ã‚“ã§ã—ãŸã€‚セッション [{0}] ã® SSO Cookie を消去ã—ã¦ã„ã¾ã™ +singleSignOn.debug.register=SSO ã¯èªè¨¼ã‚¿ã‚¤ãƒ— [{2}] ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ [{1}] ã® SSO セッション [{0}] を登録ã—ã¦ã„ã¾ã™ +singleSignOn.debug.removeSession=SSOã¯SSOセッション [{1}] ã‹ã‚‰ã‚¢ãƒ—リケーションセッション [{0}] を削除ã—ã¦ã„ã¾ã™ +singleSignOn.debug.sessionLogout=SSOã¯SSOセッション[{0}]ã¨ã‚¢ãƒ—リケーションセッション[{1}]をログアウト処ç†ã—ã¦ã„ã¾ã™ +singleSignOn.debug.sessionTimeout=SSOã¯SSOセッション[{0}]ã¨ã‚¢ãƒ—リケーションセッション[{1}]ã®ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã‚’処ç†ã—ã¦ã„ã¾ã™ +singleSignOn.debug.update=SSOã¯SSOセッション [{0}] ã‚’èªè¨¼ã‚¿ã‚¤ãƒ— [{1}] ã«æ›´æ–°ã—ã¾ã™ +singleSignOn.sessionExpire.contextNotFound=Context ãŒè¦‹ã¤ã‹ã‚‰ãªã„ãŸã‚ã€SSO ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] を破棄ã§ãã¾ã›ã‚“ +singleSignOn.sessionExpire.engineNull=Engine ㌠null ã ã£ãŸãŸã‚ã€SSO ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] を破棄ã§ãã¾ã›ã‚“ +singleSignOn.sessionExpire.hostNotFound=ホストãŒè¦‹ã¤ã‹ã‚‰ãªã„ãŸã‚ SSO セッション [{0}] を破棄ã§ãã¾ã›ã‚“ +singleSignOn.sessionExpire.managerError=セッションを検索ã™ã‚‹ã¨ãã«ãƒžãƒãƒ¼ã‚¸ãƒ£ãŒä¾‹å¤–をスローã—ãŸãŸã‚ã€SSOã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] を破棄ã§ãã¾ã›ã‚“ +singleSignOn.sessionExpire.managerNotFound=マãƒãƒ¼ã‚¸ãƒ£ãŒè¦‹ã¤ã‹ã‚‰ãªã‹ã£ãŸãŸã‚ã€SSO ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] を破棄ã§ãã¾ã›ã‚“ +singleSignOn.sessionExpire.sessionNotFound=セッションãŒè¦‹ã¤ã‹ã‚‰ãªã„ãŸã‚ã€SSO ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] を破棄ã§ãã¾ã›ã‚“ + +spnegoAuthenticator.authHeaderNoToken=クライアントã‹ã‚‰å—ä¿¡ã—㟠Negoiate èªè¨¼ãƒ˜ãƒƒãƒ€ã«ã¯ãƒˆãƒ¼ã‚¯ãƒ³ãŒã‚ã‚Šã¾ã›ã‚“。 +spnegoAuthenticator.authHeaderNotNego=クライアントã‹ã‚‰å—ä¿¡ã—ãŸèªè¨¼ãƒ˜ãƒƒãƒ€ãƒ¼ã¯ Negotiate ã‹ã‚‰å§‹ã¾ã£ã¦ã„ã¾ã›ã‚“。 +spnegoAuthenticator.serviceLoginFail=サービスプリンシパルã¨ã—ã¦ãƒ­ã‚°ã‚¤ãƒ³ã§ãã¾ã›ã‚“ +spnegoAuthenticator.ticketValidateFail=クライアントæä¾›ã®ãƒã‚±ãƒƒãƒˆã®æ¤œè¨¼ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ + +sslAuthenticatorValve.authFailed=æä¾›ã•ã‚ŒãŸè¨¼æ˜Žæ›¸ã«ã‚ˆã‚‹èªè¨¼ã«å¤±æ•—ã—ã¾ã—㟠+sslAuthenticatorValve.http2=仮想ホスト[{1}]ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ[{0}]ã¯CLIENT-CERTèªè¨¼ã‚’使用ã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã€[{2}]ã¯HTTP/2をサãƒãƒ¼ãƒˆã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ã€‚ CLIENT-CERTèªè¨¼ã®åˆ©ç”¨ã¯ã€HTTP/2ã®ä½¿ç”¨ã¨äº’æ›æ€§ãŒã‚ã‚Šã¾ã›ã‚“。 +sslAuthenticatorValve.noCertificates=ã“ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ã¯è¨¼æ˜Žæ›¸ãŒå«ã¾ã‚Œã¦ã„ã¾ã›ã‚“ +sslAuthenticatorValve.tls13=仮想ホスト[{1}]ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ[{0}]ã¯ã€CLIENT-CERTèªè¨¼ã‚’使用ã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã€[{2}]ã¯ã€JSSEを使用ã—ã¦TLS1.3をサãƒãƒ¼ãƒˆã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ã€‚ CLIENT-CERTèªè¨¼ã®åˆ©ç”¨ã¯ã€TLS1.3ãŠã‚ˆã³JSSEã®ä½¿ç”¨ã¨äº’æ›æ€§ãŒã‚ã‚Šã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/authenticator/LocalStrings_ko.properties b/java/org/apache/catalina/authenticator/LocalStrings_ko.properties new file mode 100644 index 0000000..9f13e97 --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_ko.properties @@ -0,0 +1,77 @@ +# 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. + +authenticator.certificates=ì´ ìš”ì²­ì— í´ë¼ì¸íŠ¸ ì¸ì¦ì„œ ì²´ì¸ì´ 없습니다. +authenticator.changeSessionId=ì¸ì¦ 처리 ì‹œ, 세션 ID를 [{0}]ì—ì„œ [{1}](으)ë¡œ 변경했습니다. +authenticator.check.authorize=ì‚¬ìš©ìž ì´ë¦„ [{0}]ì„(를) Connector로부터 얻었으며, ì´ëŠ” 유효한 것으로 신뢰ë˜ì—ˆìŠµë‹ˆë‹¤. Tomcat Realm으로부터, ì´ ì‚¬ìš©ìžë¥¼ 위한 ì—­í• ë“¤ì„ êµ¬í•©ë‹ˆë‹¤. +authenticator.check.authorizeFail=Realmì´ ì‚¬ìš©ìž [{0}]ì„(를) ì¸ì‹í•˜ì§€ 못했습니다. 해당 사용ìžëª…ì— ëŒ€í•´ 아무런 ì—­í•  ì—†ì´ Principalì„ ìƒì„±í•©ë‹ˆë‹¤. +authenticator.check.found=[{0}]ì€(는) ì´ë¯¸ ì¸ì¦ë˜ì—ˆìŠµë‹ˆë‹¤. +authenticator.check.sso=ì¸ì¦ë˜ì§€ 않았는ë°, SSO 세션 ID [{0}]ì´(ê°€) 발견ë˜ì—ˆìŠµë‹ˆë‹¤. 다시 ì¸ì¦ì„ ì‹œë„합니다. +authenticator.formlogin=í¼ ë¡œê·¸ì¸ íŽ˜ì´ì§€ì— 대한 유효하지 ì•Šì€ ì§ì ‘ 참조 +authenticator.jaspicCleanSubjectFail=JASPIC subject를 제거하지 못했습니다. +authenticator.jaspicSecureResponseFail=JASPIC 처리 중 ì‘ë‹µì„ ë³´ì•ˆì²˜ë¦¬ 하지 못했습니다. +authenticator.jaspicServerAuthContextFail=JASPIC ServerAuthContext ì¸ìŠ¤í„´ìŠ¤ë¥¼ íšë“하지 못했습니다. +authenticator.loginFail=ë¡œê·¸ì¸ ì‹¤íŒ¨ +authenticator.manager=Trust ë§¤ë‹ˆì €ë“¤ì„ ì´ˆê¸°í™”í•˜ëŠ” 중 예외 ë°œìƒ +authenticator.noAuthHeader=í´ë¼ì´ì–¸íŠ¸ê°€ authorization í—¤ë”를 보내지 않았습니다. +authenticator.notContext=설정 오류: 컨í…ìŠ¤íŠ¸ì— ì„¤ì •ë˜ì–´ì•¼ë§Œ 합니다. +authenticator.requestBodyTooBig=ìš”ì²­ì˜ bodyê°€ 너무 커서, ì¸ì¦ 처리 과정ì—ì„œ ìºì‹œì— ì €ìž¥ë  ìˆ˜ 없습니다. +authenticator.sessionExpired=ë¡œê·¸ì¸ ì²˜ë¦¬ 허용 ì‹œê°„ì´ ì´ˆê³¼ë˜ì—ˆìŠµë‹ˆë‹¤. 계ì†í•˜ì‹œë ¤ë©´ 뒤로 가기를 ë‘번 í´ë¦­í•œ 후, ìš”ì²­í–ˆë˜ ë§í¬ë¥¼ 다시 í´ë¦­í•˜ê±°ë‚˜, 브ë¼ìš°ì €ë¥¼ 닫았다가 다시 시작해야 합니다. +authenticator.tomcatPrincipalLogoutFail=TomcatPrincipal ì¸ìŠ¤í„´ìŠ¤ë¥¼ 사용한 로그아웃 ì‹œë„ê°€ 실패했습니다. +authenticator.unauthorized=ì œê³µëœ credentials를 사용하여 ì¸ì¦í•  수 없습니다. + +basicAuthenticator.invalidCharset=í—ˆìš©ëœ ê°’ë“¤ì€ ì˜¤ì§ ë„, 빈 문ìžì—´, ë˜ëŠ” UTF-8 문ìžì—´ìž…니다. + +digestAuthenticator.cacheRemove=새로운 ì—”íŠ¸ë¦¬ë“¤ì„ ìœ„í•œ ê³µê°„ì„ ë§Œë“¤ê¸° 위해, client nonce cache로부터 유효한 엔트리를 제거했습니다. ë¦¬í”Œë ˆì´ ê³µê²©ì´ ê°€ëŠ¥í•´ì§„ ìƒíƒœìž…니다. 가능성 있는 ë¦¬í”Œë ˆì´ ê³µê²©ë“¤ì„ ë°©ì§€í•˜ë ¤ë©´, nonceValidity를 ê°ì†Œ 시키거나, nonceCacheSize를 ì¦ê°€ 시키십시오. ë” ì´ìƒ ì´ëŸ¬í•œ ì¢…ë¥˜ì˜ ê²½ê³  ë©”ì‹œì§€ë“¤ì€ í–¥í›„ 5분 ë™ì•ˆ 나오지 ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. + +formAuthenticator.changeSessionIdLogin=ë¡œê·¸ì¸ íŽ˜ì´ì§€ë¡œ forward하기 ì§ì „ì— ì„¸ì…˜ IDê°€ [{0}]ì—ì„œ [{1}](으)ë¡œ 변경ë˜ì—ˆìŠµë‹ˆë‹¤. +formAuthenticator.forwardErrorFail=오류 페ì´ì§€ë¡œ forward하는 중 예기치 ì•Šì€ ì˜¤ë¥˜ ë°œìƒ +formAuthenticator.forwardLogin=메소드 [{1}]ì„(를) 사용한 [{0}]ì— ëŒ€í•œ 요청ì„, 컨í…스트 [{3}]ì˜ ë¡œê·¸ì¸ íŽ˜ì´ì§€ [{2}](으)ë¡œ, GET 요청 메소드를 사용하여 forward 합니다. +formAuthenticator.forwardLoginFail=ë¡œê·¸ì¸ íŽ˜ì´ì§€ë¡œ forward하는 중 예기치 ì•Šì€ ì˜¤ë¥˜ ë°œìƒ +formAuthenticator.noErrorPage=컨í…스트 [{0}]ì—ì„œ í¼ ê¸°ë°˜ ì¸ì¦ì„ 위한 오류 페ì´ì§€ê°€ ì •ì˜ë˜ì§€ 않았습니다. +formAuthenticator.noLoginPage=컨í…스트 [{0}]ì—ì„œ, í¼ ê¸°ë°˜ ì¸ì¦ì„ 위한 ë¡œê·¸ì¸ íŽ˜ì´ì§€ê°€ ì •ì˜ë˜ì§€ 않았습니다. +formAuthenticator.sessionIdMismatch=현 세션 ID는 [{0}]입니다만 FORM authenticatorê°€ ê¸°ëŒ€í–ˆë˜ ì„¸ì…˜ ID는 [{1}]ì´ì—ˆìŠµë‹ˆë‹¤. + +singleSignOn.debug.associate=SSOê°€, 애플리케ì´ì…˜ 세션 [{1}]ì„(를) SSO 세션 [{0}]와(ê³¼) 연관시킵니다. +singleSignOn.debug.associateFail=SSO 세션 [{1}]ì´(ê°€) 존재하지 않기 때문ì—, SSOê°€ 애플리케ì´ì…˜ 세션 [{0}]ì„(를) 연관시키지 못했습니다. +singleSignOn.debug.cookieCheck=SSOê°€, SSO 쿠키가 존재하는지 ì ê²€í•©ë‹ˆë‹¤. +singleSignOn.debug.cookieNotFound=SSOê°€, SSO 쿠키를 찾지 못했습니다. +singleSignOn.debug.deregister=SSOê°€, SSO 세션 [{1}]와(ê³¼) ì—°ê´€ëœ ì• í”Œë¦¬ì¼€ì´ì…˜ 세션 [{0}]ì„(를) 만료시킵니다. +singleSignOn.debug.deregisterFail=SSO 세션 [{0}]ì´(ê°€) ìºì‹œì— 존재하지 않기 때문ì—, SSOê°€ 해당 SSO ì„¸ì…˜ì— ëŒ€í•œ 등ë¡ì„ 제거하지 못했습니다. +singleSignOn.debug.deregisterNone=SSOê°€ SSO 세션 [{0}]ì˜ ë“±ë¡ì„ 제거했으나, ì—°ê´€ëœ ì• í”Œë¦¬ì¼€ì´ì…˜ ì„¸ì…˜ë“¤ì„ ì°¾ì§€ 못했습니다. +singleSignOn.debug.hasPrincipal=SSOê°€ ì´ì „ì— ì¸ì¦ëœ Principal [{0}]ì„(를) 발견했습니다. +singleSignOn.debug.invoke=SSOê°€ [{0}]ì„(를) 위해 ìš”ì²­ì„ ì²˜ë¦¬í•©ë‹ˆë‹¤. +singleSignOn.debug.principalCheck=SSO 세션 [{0}]ì„(를) 위하여, SSOê°€ ìºì‹œëœ Principalì„ ì°¾ìŠµë‹ˆë‹¤. +singleSignOn.debug.principalFound=ì¸ì¦ íƒ€ìž…ì´ [{1}]ì¸, ìºì‹œëœ Principal [{0}]ì„(를), SSOê°€ 발견했습니다. +singleSignOn.debug.principalNotFound=SSOê°€ ìºì‹œëœ Principalì„ ì°¾ì§€ 못했습니다. 세션 [{0}]ì„(를) 위한 SSO 쿠키를 지ì›ë‹ˆë‹¤. +singleSignOn.debug.register=ì‚¬ìš©ìž [{1}]ì„(를) 위해, ì¸ì¦ 타입 [{2}]ì„ ì‚¬ìš©í•˜ì—¬, SSOê°€ SSO 세션 [{0}]ì„(를) 등ë¡í•©ë‹ˆë‹¤. +singleSignOn.debug.removeSession=SSOê°€ SSO 세션 [{1}](으)로부터 세션 [{0}]ì„(를) 제거합니다. +singleSignOn.debug.sessionLogout=SSO 세션 [{0}]와(ê³¼) 애플리케ì´ì…˜ 세션 [{1}]ì„(를) 위해, SSOê°€ ë¡œê·¸ì•„ì›ƒì„ ì²˜ë¦¬ 중 +singleSignOn.debug.sessionTimeout=SSO 세션 [{0}]와(ê³¼) 애플리케ì´ì…˜ 세션 [{1}]ì„(를) 위한, SSO 처리가 제한 시간 초과ë˜ì—ˆìŠµë‹ˆë‹¤. +singleSignOn.debug.update=SSOê°€, SSO 세션 [{0}]ì˜ ì¸ì¦ íƒ€ìž…ì„ [{1}](으)ë¡œ 변경합니다. +singleSignOn.sessionExpire.contextNotFound=컨í…스트를 ì°¾ì„ ìˆ˜ 없기 때문ì—, SSOê°€ 세션 [{0}]ì„(를) 만료시킬 수 없습니다. +singleSignOn.sessionExpire.engineNull=ì—”ì§„ì´ ë„ì´ê¸° ë•Œë¬¸ì— SSOê°€ 세션 [{0}]ì„(를) 만료시킬 수 없습니다. +singleSignOn.sessionExpire.hostNotFound=호스트를 ì°¾ì„ ìˆ˜ 없어서, SSOê°€ 세션 [{0}]ì„(를) 만료시킬 수 없습니다. +singleSignOn.sessionExpire.managerError=ì„¸ì…˜ì„ ì°¾ëŠ” ë™ì•ˆ 매니저가 예외를 ë°œìƒì‹œì¼œ, SSOê°€ 세션 [{0}]ì„(를) 만료시킬 수 없습니다. +singleSignOn.sessionExpire.managerNotFound=매니저를 ì°¾ì„ ìˆ˜ 없기 때문ì—, SSOê°€ 세션 [{0}]ì„(를) 만료시킬 수 없습니다. +singleSignOn.sessionExpire.sessionNotFound=ì„¸ì…˜ì„ ì°¾ì„ ìˆ˜ 없기 때문ì—, SSOê°€ 세션 [{0}]ì„(를) 만료시킬 수 없습니다. + +spnegoAuthenticator.authHeaderNoToken=í´ë¼ì´ì–¸íŠ¸ì— ì˜í•´ ì „ì†¡ëœ Negotiate authorization í—¤ë”ê°€ 토í°ì„ í¬í•¨í•˜ì§€ 않았습니다. +spnegoAuthenticator.authHeaderNotNego=í´ë¼ì´ì–¸íŠ¸ê°€ 보낸 Authorization í—¤ë”ê°€ Negotiateë¡œ 시작하지 않았습니다. +spnegoAuthenticator.serviceLoginFail=서비스 Principal로서 ë¡œê·¸ì¸ í•  수 없습니다. +spnegoAuthenticator.ticketValidateFail=í´ë¼ì´ì–¸íŠ¸ì— ì˜í•´ ì œê³µëœ í‹°ì¼“ì´ ìœ íš¨í•œì§€ë¥¼ 확ì¸í•˜ì§€ 못했습니다. + +sslAuthenticatorValve.http2=ê°€ìƒ í˜¸ìŠ¤íŠ¸ [{1}]ì˜ ì»¨í…스트 [{0}](ì€)는 CLIENT-CERT ì¸ì¦ì„ 사용하ë„ë¡ ì„¤ì •ë˜ì—ˆê³ , [{2}](ì€)는 HTTP/2를 지ì›í•˜ë„ë¡ ì„¤ì •ë˜ì—ˆìŠµë‹ˆë‹¤. CLIENT-CERT ì¸ì¦ ì‚¬ìš©ì€ HTTP/2ì˜ ì‚¬ìš©ê³¼ 호환ë˜ì§€ 않습니다. +sslAuthenticatorValve.tls13=ê°€ìƒ í˜¸ìŠ¤íŠ¸ [{1}]ì˜ ì»¨í…스트 [{0}]ì€ CLIENT-CERT ì¸ì¦ì„ 사용하ë„ë¡ ì„¤ì •ë˜ì—ˆê³ , [{2}](ì€)는 JSSE를 사용하여 TLS 1.3ì„ ì§€ì›í•˜ë„ë¡ ì„¤ì •ë˜ì—ˆìŠµë‹ˆë‹¤. CLIENT-CERT ì¸ì¦ ì‚¬ìš©ì€ TLS 1.3ê³¼ JSSEì˜ ì‚¬ìš©ê³¼ 호환ë˜ì§€ 않습니다. diff --git a/java/org/apache/catalina/authenticator/LocalStrings_pt_BR.properties b/java/org/apache/catalina/authenticator/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..763c337 --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_pt_BR.properties @@ -0,0 +1,20 @@ +# 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. + +authenticator.jaspicServerAuthContextFail=Falha ao obter uma instância do JASPIC ServerAuthContext + +singleSignOn.debug.principalFound=SSO encontrou Principal cacheado [{0}] com tipo de autenticação [{1}] +singleSignOn.sessionExpire.hostNotFound=Impossível experar sessão SSO [{0}] porque o host não foi encontrado +singleSignOn.sessionExpire.managerError=SSO impossível de esperar sessão [{0}] porque o Manager lançou uma exception quando realizava busca pela sessão diff --git a/java/org/apache/catalina/authenticator/LocalStrings_ru.properties b/java/org/apache/catalina/authenticator/LocalStrings_ru.properties new file mode 100644 index 0000000..40eec8d --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +authenticator.noAuthHeader=Заголовок авторизации не был отправлен клиентом diff --git a/java/org/apache/catalina/authenticator/LocalStrings_zh_CN.properties b/java/org/apache/catalina/authenticator/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..1cef284 --- /dev/null +++ b/java/org/apache/catalina/authenticator/LocalStrings_zh_CN.properties @@ -0,0 +1,76 @@ +# 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. + +authenticator.certificates=此请求中没有客户端è¯ä¹¦é“¾ +authenticator.changeSessionId=在身份验è¯æ—¶, ä¼šè¯ ID 从 [{0}] 更改为 [{1}] +authenticator.check.authorize=用户å[{0}]从连接器获得,并被信任为有效。从Tomcat领域获å–此用户的角色。 +authenticator.check.authorizeFail=领域无法识别用户[{0}]。创建具有该å称且没有角色的主体。 +authenticator.check.found=å·²é€šè¿‡èº«ä»½éªŒè¯ [{0}] +authenticator.check.sso=未ç»è¿‡èº«ä»½éªŒè¯ä½†æ‰¾åˆ°äº†SSO会è¯ID [{0}]。å°è¯•é‡æ–°éªŒè¯ã€‚ +authenticator.formlogin=对表å•ç™»å½•é¡µçš„直接引用无效 +authenticator.jaspicCleanSubjectFail=清除 JASPIC 主题失败 +authenticator.jaspicSecureResponseFail=在JASPIC处ç†æœŸé—´æ— æ³•ä¿è¯å“应 +authenticator.jaspicServerAuthContextFail=失败的获å–一个JASPIC ServerAuthContext 实例 +authenticator.loginFail=登录失败 +authenticator.manager=åˆå§‹åŒ–信任管ç†å™¨å¼‚常 +authenticator.noAuthHeader=客户端未å‘é€æŽˆæƒè¯·æ±‚头 +authenticator.notContext=é…置错误:必须被附属于一个上下文 +authenticator.requestBodyTooBig=请求正文太大,无法在身份验è¯è¿‡ç¨‹ä¸­è¿›è¡Œç¼“å­˜ +authenticator.sessionExpired=已超出登录过程所å…许的时间。 如果您希望继续,则必须å•å‡»ä¸¤æ¬¡åŽé€€å¹¶é‡æ–°å•å‡»æ‚¨è¯·æ±‚的链接或先关闭然åŽé‡æ–°æ‰“å¼€æµè§ˆå™¨ +authenticator.tomcatPrincipalLogoutFail=使用TomcatPrincipal实例注销失败 +authenticator.unauthorized=无法使用æ供的凭æ®è¿›è¡Œèº«ä»½éªŒè¯ + +basicAuthenticator.invalidCharset=åªå…许值为nullã€ç©ºå­—符串或UTF-8 + +digestAuthenticator.cacheRemove=已从客户端 nonce 缓存中删除有效æ¡ç›®ï¼Œä»¥ä¾¿ä¸ºæ–°æ¡ç›®è…¾å‡ºç©ºé—´ã€‚é‡æ’­æ”»å‡»çŽ°åœ¨æ˜¯å¯èƒ½çš„。为防止é‡æ’­æ”»å‡»çš„å¯èƒ½æ€§ï¼Œè¯·é™ä½ŽnonceValidity或增加nonceCacheSize。此类型的进一步警告将被抑制5分钟。 + +formAuthenticator.forwardErrorFail=转å‘到错误页时出现æ„外错误。 +formAuthenticator.forwardLogin=使用请求方法GET将使用方法[{1}]å‘出的对[{0}]的请求转å‘到上下文[{3}]的登录页[{2}] +formAuthenticator.forwardLoginFail=转å‘到登录页时出现æ„外错误。 +formAuthenticator.noErrorPage=没有为上下文[{0}]中的表å•èº«ä»½éªŒè¯å®šä¹‰é”™è¯¯é¡µ +formAuthenticator.noLoginPage=在环境[{0}]中,未为FORM认è¯å®šä¹‰ç™»å½•é¡µé¢ +formAuthenticator.sessionIdMismatch=当å‰ä¼šè¯ID是[{0}],但是FORM认è¯å™¨æœŸæœ›çš„是[{1}]。 + +singleSignOn.debug.associate=SSO将应用程åºä¼šè¯[{1}]与SSO会è¯[{0}]å…³è” +singleSignOn.debug.associateFail=SSO无法关è”应用程åºä¼šè¯[{0}],因为SSO会è¯[{1}]ä¸å­˜åœ¨ã€‚ +singleSignOn.debug.cookieCheck=SSO检查SSO cookie +singleSignOn.debug.cookieNotFound=SSO没有找到SSO cookie +singleSignOn.debug.deregister=与SSO会è¯[{1}]å…³è”çš„SSO过期应用程åºä¼šè¯[{0}] +singleSignOn.debug.deregisterFail=SSO撤销登记SSO会è¯[{0}]失败,因为缓存中ä¸åŒ…å«è¿™ä¸ªSSOä¼šè¯ +singleSignOn.debug.deregisterNone=SSO注销了SSO会è¯[{0}],但未找到关è”的应用程åºä¼šè¯ +singleSignOn.debug.hasPrincipal=找到以å‰ç»è¿‡èº«ä»½éªŒè¯çš„主体[{0}] +singleSignOn.debug.invoke=SSO为[{0}]处ç†è¯·æ±‚ +singleSignOn.debug.principalCheck=SSO为SSO会è¯[{0}]寻找缓存的Principal +singleSignOn.debug.principalFound=SSO 找到了带ç€è®¤è¯ç±»åž‹[{1}]的缓存Principal [{0}] +singleSignOn.debug.principalNotFound=SSO未找到缓存的Principal,为会è¯[{0}]擦除SSO cookie +singleSignOn.debug.register=使用身份验è¯ç±»åž‹[{2}]的用户[{1}]çš„SSO注册SSO会è¯[{0}]。 +singleSignOn.debug.removeSession=SSO 从 SSO session [{1}] 中删除应用程åºä¼šè¯ [{0}] +singleSignOn.debug.sessionLogout=SSO正在处ç†SSO会è¯[{0}]和应用程åºä¼šè¯[{1}]的注销 +singleSignOn.debug.sessionTimeout=SSO正在处ç†SSO会è¯[{0}]和应用程åºä¼šè¯[{1}]的超时 +singleSignOn.debug.update=SSO æ›´æ–°SSO 会è¯[{0}] å¯¹è®¤è¯ ç±»åž‹[{1}] +singleSignOn.sessionExpire.contextNotFound=SSO无法中止[{0}],因为Context未找到 +singleSignOn.sessionExpire.engineNull=SSO无法使会è¯[{0}]过期,因为引擎为空。 +singleSignOn.sessionExpire.hostNotFound=由于无法找到主机,å•ç‚¹ç™»å½•æ— æ³•ä½¿ä¼šè¯[{0}]过期 +singleSignOn.sessionExpire.managerError=由于会è¯ç®¡ç†å™¨åœ¨æ£€ç´¢ä¼šè¯æ—¶æŠ›å‡ºå¼‚常,导致å•ç‚¹ç™»å½•æ— æ³•ä½¿ä¼šè¯[{0}]失效 +singleSignOn.sessionExpire.managerNotFound=SSO无法使会è¯[{0}]过期,因为找ä¸åˆ°ç®¡ç†å™¨ +singleSignOn.sessionExpire.sessionNotFound=SSO无法使会è¯[{0}]过期,因为找ä¸åˆ°è¯¥ä¼šè¯ + +spnegoAuthenticator.authHeaderNoToken=客户端å‘é€çš„åå•†æŽˆæƒ header æœªåŒ…å« token +spnegoAuthenticator.authHeaderNotNego=客户端å‘é€çš„授æƒå¤´ä¸æ˜¯ä»¥å商开始的。 +spnegoAuthenticator.serviceLoginFail=无法作为æœåŠ¡ä¸»ä½“登录 +spnegoAuthenticator.ticketValidateFail=无法验è¯å®¢æˆ·ç«¯æä¾›çš„ç¥¨è¯ + +sslAuthenticatorValve.http2=虚拟主机 [{1}] 中的上下文 [{0}] é…置为使用 CLIENT-CERT 身份验è¯ï¼Œå¹¶ä¸” [{2}] é…ç½®ä¸ºæ”¯æŒ HTTP/2。使用 CLIENT-CERT 身份验è¯ä¸Žä½¿ç”¨ HTTP/2 ä¸å…¼å®¹ã€‚ +sslAuthenticatorValve.tls13=虚拟主机 [{1}] 中的上下文 [{0}] é…置为使用 CLIENT-CERT 身份验è¯ï¼Œå¹¶ä¸” [{2}] é…置为使用 JSSE æ”¯æŒ TLS 1.3。使用 CLIENT-CERT 身份验è¯ä¸Žä½¿ç”¨ TLS 1.3 å’Œ JSSE ä¸å…¼å®¹ã€‚ diff --git a/java/org/apache/catalina/authenticator/NonLoginAuthenticator.java b/java/org/apache/catalina/authenticator/NonLoginAuthenticator.java new file mode 100644 index 0000000..2cf878f --- /dev/null +++ b/java/org/apache/catalina/authenticator/NonLoginAuthenticator.java @@ -0,0 +1,104 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.connector.Request; + +/** + * An Authenticator and Valve implementation that checks only security constraints not involving user + * authentication. + * + * @author Craig R. McClanahan + */ +public final class NonLoginAuthenticator extends AuthenticatorBase { + + + // --------------------------------------------------------- Public Methods + + + /** + *

    + * Authenticate the user making this request, based on the fact that no login-config has been defined + * for the container. + *

    + *

    + * This implementation means "login the user even though there is no self-contained way to establish a security + * Principal for that user". + *

    + *

    + * This method is called by the AuthenticatorBase super class to establish a Principal for the user BEFORE the + * container security constraints are examined, i.e. it is not yet known whether the user will eventually be + * permitted to access the requested resource. Therefore, it is necessary to always return true to + * indicate the user has not failed authentication. + *

    + *

    + * There are two cases: + *

    + *
      + *
    • without SingleSignon: a Session instance does not yet exist and there is no auth-method to + * authenticate the user, so leave Request's Principal as null. Note: AuthenticatorBase will later examine the + * security constraints to determine whether the resource is accessible by a user without a security Principal and + * Role (i.e. unauthenticated).
    • + *
    • with SingleSignon: if the user has already authenticated via another container (using its own login + * configuration), then associate this Session with the SSOEntry so it inherits the already-established security + * Principal and associated Roles. Note: This particular session will become a full member of the SingleSignOnEntry + * Session collection and so will potentially keep the SSOE "alive", even if all the other properly authenticated + * Sessions expire first... until it expires too.
    • + *
    + * + * @param request Request we are processing + * @param response Response we are creating + * + * @return boolean to indicate whether the user is authenticated + * + * @exception IOException if an input/output error occurs + */ + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { + + // Don't try and use SSO to authenticate since there is no auth + // configured for this web application + if (checkForCachedAuthentication(request, response, true)) { + // Save the inherited Principal in this session so it can remain + // authenticated until it expires. + if (cache) { + request.getSessionInternal(true).setPrincipal(request.getPrincipal()); + } + return true; + } + + // No Principal means the user is not already authenticated + // and so will not be assigned any roles. It is safe to + // to say the user is now authenticated because access to + // protected resources will only be allowed with a matching role. + // i.e. SC_FORBIDDEN (403 status) will be generated later. + if (containerLog.isTraceEnabled()) { + containerLog.trace("User authenticated without any roles"); + } + return true; + } + + + @Override + protected String getAuthMethod() { + return "NONE"; + } +} diff --git a/java/org/apache/catalina/authenticator/SSLAuthenticator.java b/java/org/apache/catalina/authenticator/SSLAuthenticator.java new file mode 100644 index 0000000..b3df81c --- /dev/null +++ b/java/org/apache/catalina/authenticator/SSLAuthenticator.java @@ -0,0 +1,206 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.security.Principal; +import java.security.cert.X509Certificate; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.coyote.ActionCode; +import org.apache.coyote.UpgradeProtocol; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.Constants; +import org.apache.tomcat.util.net.SSLHostConfig; + +/** + * An Authenticator and Valve implementation of authentication that utilizes SSL certificates to identify + * client users. + * + * @author Craig R. McClanahan + */ +public class SSLAuthenticator extends AuthenticatorBase { + + private final Log log = LogFactory.getLog(SSLAuthenticator.class); // must not be static + + /** + * Authenticate the user by checking for the existence of a certificate chain, validating it against the trust + * manager for the connector and then validating the user's identity against the configured Realm. + * + * @param request Request we are processing + * @param response Response we are creating + * + * @exception IOException if an input/output error occurs + */ + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { + + // NOTE: We don't try to reauthenticate using any existing SSO session, + // because that will only work if the original authentication was + // BASIC or FORM, which are less secure than the CLIENT-CERT auth-type + // specified for this webapp + // + // Change to true below to allow previous FORM or BASIC authentications + // to authenticate users for this webapp + // TODO make this a configurable attribute (in SingleSignOn??) + if (checkForCachedAuthentication(request, response, false)) { + return true; + } + + // Retrieve the certificate chain for this client + if (containerLog.isTraceEnabled()) { + containerLog.trace(" Looking up certificates"); + } + + X509Certificate certs[] = getRequestCertificates(request); + + if ((certs == null) || (certs.length < 1)) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("sslAuthenticatorValve.noCertificates")); + } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, sm.getString("authenticator.certificates")); + return false; + } + + // Authenticate the specified certificate chain + Principal principal = context.getRealm().authenticate(certs); + if (principal == null) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("sslAuthenticatorValve.authFailed")); + } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, sm.getString("authenticator.unauthorized")); + return false; + } + + // Cache the principal (if requested) and record this authentication + register(request, response, principal, HttpServletRequest.CLIENT_CERT_AUTH, null, null); + return true; + + } + + + @Override + protected String getAuthMethod() { + return HttpServletRequest.CLIENT_CERT_AUTH; + } + + + @Override + protected boolean isPreemptiveAuthPossible(Request request) { + X509Certificate[] certs = getRequestCertificates(request); + return certs != null && certs.length > 0; + } + + + /** + * Look for the X509 certificate chain in the Request under the key + * jakarta.servlet.request.X509Certificate. If not found, trigger extracting the certificate chain from + * the Coyote request. + * + * @param request Request to be processed + * + * @return The X509 certificate chain if found, null otherwise. + */ + protected X509Certificate[] getRequestCertificates(final Request request) throws IllegalStateException { + + X509Certificate certs[] = (X509Certificate[]) request.getAttribute(Globals.CERTIFICATES_ATTR); + + if ((certs == null) || (certs.length < 1)) { + try { + request.getCoyoteRequest().action(ActionCode.REQ_SSL_CERTIFICATE, null); + certs = (X509Certificate[]) request.getAttribute(Globals.CERTIFICATES_ATTR); + } catch (IllegalStateException ise) { + // Request body was too large for save buffer + // Return null which will trigger an auth failure + } + } + + return certs; + } + + + @Override + protected void startInternal() throws LifecycleException { + + super.startInternal(); + + /* + * This Valve should only ever be added to a Context and if the Context is started there should always be a Host + * and an Engine but test at each stage to be safe. + */ + Container container = getContainer(); + if (!(container instanceof Context)) { + return; + } + Context context = (Context) container; + + container = context.getParent(); + if (!(container instanceof Host)) { + return; + } + Host host = (Host) container; + + container = host.getParent(); + if (!(container instanceof Engine)) { + return; + } + Engine engine = (Engine) container; + + + Connector[] connectors = engine.getService().findConnectors(); + + for (Connector connector : connectors) { + // First check for upgrade + UpgradeProtocol[] upgradeProtocols = connector.findUpgradeProtocols(); + for (UpgradeProtocol upgradeProtocol : upgradeProtocols) { + if ("h2".equals(upgradeProtocol.getAlpnName())) { + log.warn(sm.getString("sslAuthenticatorValve.http2", context.getName(), host.getName(), connector)); + break; + } + } + + // Then check for TLS 1.3 + SSLHostConfig[] sslHostConfigs = connector.findSslHostConfigs(); + for (SSLHostConfig sslHostConfig : sslHostConfigs) { + if (!sslHostConfig.isTls13RenegotiationAvailable()) { + String[] enabledProtocols = sslHostConfig.getEnabledProtocols(); + if (enabledProtocols == null) { + // Possibly boundOnInit is used, so use the less accurate protocols + enabledProtocols = sslHostConfig.getProtocols().toArray(new String[0]); + } + for (String enbabledProtocol : enabledProtocols) { + if (Constants.SSL_PROTO_TLSv1_3.equals(enbabledProtocol)) { + log.warn(sm.getString("sslAuthenticatorValve.tls13", context.getName(), host.getName(), + connector)); + } + } + } + } + } + } +} diff --git a/java/org/apache/catalina/authenticator/SavedRequest.java b/java/org/apache/catalina/authenticator/SavedRequest.java new file mode 100644 index 0000000..a1ce589 --- /dev/null +++ b/java/org/apache/catalina/authenticator/SavedRequest.java @@ -0,0 +1,193 @@ +/* + * 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.catalina.authenticator; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import jakarta.servlet.http.Cookie; + +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Object that saves the critical information from a request so that form-based authentication can reproduce it once the + * user has been authenticated. + *

    + * IMPLEMENTATION NOTE - It is assumed that this object is accessed only from the context of a single thread, so + * no synchronization around internal collection classes is performed. + * + * @author Craig R. McClanahan + */ +public final class SavedRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * The set of Cookies associated with this Request. + */ + private final List cookies = new ArrayList<>(); + + public void addCookie(Cookie cookie) { + cookies.add(cookie); + } + + public Iterator getCookies() { + return cookies.iterator(); + } + + + /** + * The set of Headers associated with this Request. Each key is a header name, while the value is a List containing + * one or more actual values for this header. The values are returned as an Iterator when you ask for them. + */ + private final Map> headers = new HashMap<>(); + + public void addHeader(String name, String value) { + headers.computeIfAbsent(name, k -> new ArrayList<>()).add(value); + } + + public Iterator getHeaderNames() { + return headers.keySet().iterator(); + } + + public Iterator getHeaderValues(String name) { + List values = headers.get(name); + if (values == null) { + return Collections.emptyIterator(); + } else { + return values.iterator(); + } + } + + + /** + * The set of Locales associated with this Request. + */ + private final List locales = new ArrayList<>(); + + public void addLocale(Locale locale) { + locales.add(locale); + } + + public Iterator getLocales() { + return locales.iterator(); + } + + + /** + * The request method used on this Request. + */ + private String method = null; + + public String getMethod() { + return this.method; + } + + public void setMethod(String method) { + this.method = method; + } + + + /** + * The query string associated with this Request. + */ + private String queryString = null; + + public String getQueryString() { + return this.queryString; + } + + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + + /** + * The request URI associated with this Request. + */ + private String requestURI = null; + + public String getRequestURI() { + return this.requestURI; + } + + public void setRequestURI(String requestURI) { + this.requestURI = requestURI; + } + + + /** + * The decode request URI associated with this Request. Path parameters are also excluded + */ + private String decodedRequestURI = null; + + public String getDecodedRequestURI() { + return this.decodedRequestURI; + } + + public void setDecodedRequestURI(String decodedRequestURI) { + this.decodedRequestURI = decodedRequestURI; + } + + + /** + * The body of this request. + */ + private ByteChunk body = null; + + public ByteChunk getBody() { + return this.body; + } + + public void setBody(ByteChunk body) { + this.body = body; + } + + + /** + * The content type of the request, used if this is a POST. + */ + private String contentType = null; + + public String getContentType() { + return this.contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + + /** + * The original maxInactiveInterval for the session. + */ + private int originalMaxInactiveInterval = -1; + + public int getOriginalMaxInactiveInterval() { + return originalMaxInactiveInterval; + } + + public void setOriginalMaxInactiveInterval(int originalMaxInactiveInterval) { + this.originalMaxInactiveInterval = originalMaxInactiveInterval; + } +} diff --git a/java/org/apache/catalina/authenticator/SingleSignOn.java b/java/org/apache/catalina/authenticator/SingleSignOn.java new file mode 100644 index 0000000..813b37e --- /dev/null +++ b/java/org/apache/catalina/authenticator/SingleSignOn.java @@ -0,0 +1,590 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.security.Principal; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Manager; +import org.apache.catalina.Realm; +import org.apache.catalina.Session; +import org.apache.catalina.SessionListener; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; +import org.apache.tomcat.util.res.StringManager; + +/** + * A Valve that supports a "single sign on" user experience, where the security identity of a user who + * successfully authenticates to one web application is propagated to other web applications in the same security + * domain. For successful use, the following requirements must be met: + *

      + *
    • This Valve must be configured on the Container that represents a virtual host (typically an implementation of + * Host).
    • + *
    • The Realm that contains the shared user and role information must be configured on the same + * Container (or a higher one), and not overridden at the web application level.
    • + *
    • The web applications themselves must use one of the standard Authenticators found in the + * org.apache.catalina.authenticator package.
    • + *
    + * + * @author Craig R. McClanahan + */ +public class SingleSignOn extends ValveBase { + + private static final StringManager sm = StringManager.getManager(SingleSignOn.class); + + /* + * The engine at the top of the container hierarchy in which this SSO Valve has been placed. It is used to get back + * to a session object from a SingleSignOnSessionKey and is updated when the Valve starts and stops. + */ + private Engine engine; + + // ------------------------------------------------------ Constructor + + public SingleSignOn() { + super(true); + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The cache of SingleSignOnEntry instances for authenticated Principals, keyed by the cookie value that is used to + * select them. + */ + protected Map cache = new ConcurrentHashMap<>(); + + /** + * Indicates whether this valve should require a downstream Authenticator to reauthenticate each request, or if it + * itself can bind a UserPrincipal and AuthType object to the request. + */ + private boolean requireReauthentication = false; + + /** + * Optional SSO cookie domain. + */ + private String cookieDomain; + + /** + * SSO cookie name, the default value is JSESSIONIDSSO. + */ + private String cookieName = Constants.SINGLE_SIGN_ON_COOKIE; + + // ------------------------------------------------------------- Properties + + /** + * Returns the optional cookie domain. May return null. + * + * @return The cookie domain + */ + public String getCookieDomain() { + return cookieDomain; + } + + + /** + * Sets the domain to be used for sso cookies. + * + * @param cookieDomain cookie domain name + */ + public void setCookieDomain(String cookieDomain) { + if (cookieDomain != null && cookieDomain.trim().length() == 0) { + this.cookieDomain = null; + } else { + this.cookieDomain = cookieDomain; + } + } + + + /** + * @return the cookie name + */ + public String getCookieName() { + return cookieName; + } + + + /** + * Set the cookie name that will be used for the SSO cookie. + * + * @param cookieName the cookieName to set + */ + public void setCookieName(String cookieName) { + this.cookieName = cookieName; + } + + + /** + * Gets whether each request needs to be reauthenticated (by an Authenticator downstream in the pipeline) to the + * security Realm, or if this Valve can itself bind security info to the request based on the presence + * of a valid SSO entry without rechecking with the Realm. + * + * @return true if it is required that a downstream Authenticator reauthenticate each request before + * calls to HttpServletRequest.setUserPrincipal() and + * HttpServletRequest.setAuthType() are made; false if the Valve + * can itself make those calls relying on the presence of a valid SingleSignOn entry associated with the + * request. + * + * @see #setRequireReauthentication + */ + public boolean getRequireReauthentication() { + return requireReauthentication; + } + + + /** + * Sets whether each request needs to be reauthenticated (by an Authenticator downstream in the pipeline) to the + * security Realm, or if this Valve can itself bind security info to the request, based on the presence + * of a valid SSO entry, without rechecking with the Realm. + *

    + * If this property is false (the default), this Valve will bind a UserPrincipal and + * AuthType to the request if a valid SSO entry is associated with the request. It will not notify the security + * Realm of the incoming request. + *

    + * This property should be set to true if the overall server configuration requires that the + * Realm reauthenticate each request thread. An example of such a configuration would be one where the + * Realm implementation provides security for both a web tier and an associated EJB tier, and needs to + * set security credentials on each request thread in order to support EJB access. + *

    + * If this property is set to true, this Valve will set flags on the request notifying the downstream + * Authenticator that the request is associated with an SSO session. The Authenticator will then call its + * {@link AuthenticatorBase#reauthenticateFromSSO reauthenticateFromSSO} method to attempt to reauthenticate the + * request to the Realm, using any credentials that were cached with this Valve. + *

    + * The default value of this property is false, in order to maintain backward compatibility with + * previous versions of Tomcat. + * + * @param required true if it is required that a downstream Authenticator reauthenticate each request + * before calls to HttpServletRequest.setUserPrincipal() and + * HttpServletRequest.setAuthType() are made; false if the + * Valve can itself make those calls relying on the presence of a valid + * SingleSignOn entry associated with the request. + * + * @see AuthenticatorBase#reauthenticateFromSSO + */ + public void setRequireReauthentication(boolean required) { + this.requireReauthentication = required; + } + + + // ---------------------------------------------------------- Valve Methods + + /** + * Perform single-sign-on support processing for this request. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + request.removeNote(Constants.REQ_SSOID_NOTE); + + // Has a valid user already been authenticated? + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("singleSignOn.debug.invoke", request.getRequestURI())); + } + if (request.getUserPrincipal() != null) { + if (containerLog.isDebugEnabled()) { + containerLog + .debug(sm.getString("singleSignOn.debug.hasPrincipal", request.getUserPrincipal().getName())); + } + getNext().invoke(request, response); + return; + } + + // Check for the single sign on cookie + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("singleSignOn.debug.cookieCheck")); + } + Cookie cookie = null; + Cookie cookies[] = request.getCookies(); + if (cookies != null) { + for (Cookie value : cookies) { + if (cookieName.equals(value.getName())) { + cookie = value; + break; + } + } + } + if (cookie == null) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.cookieNotFound")); + } + getNext().invoke(request, response); + return; + } + + // Look up the cached Principal associated with this cookie value + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("singleSignOn.debug.principalCheck", cookie.getValue())); + } + SingleSignOnEntry entry = cache.get(cookie.getValue()); + if (entry != null) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.principalFound", + entry.getPrincipal() != null ? entry.getPrincipal().getName() : "", entry.getAuthType())); + } + request.setNote(Constants.REQ_SSOID_NOTE, cookie.getValue()); + // Only set security elements if reauthentication is not required + if (!getRequireReauthentication()) { + request.setAuthType(entry.getAuthType()); + request.setUserPrincipal(entry.getPrincipal()); + } + } else { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.principalNotFound", cookie.getValue())); + } + // No need to return a valid SSO session ID + cookie.setValue("REMOVE"); + // Age of zero will trigger removal + cookie.setMaxAge(0); + // Domain and path have to match the original cookie to 'replace' + // the original cookie + cookie.setPath("/"); + String domain = getCookieDomain(); + if (domain != null) { + cookie.setDomain(domain); + } + /* + * This is going to trigger a Set-Cookie header. While the value is not security sensitive, ensure that + * expectations for secure, httpOnly and Partitioned are met. + */ + cookie.setSecure(request.isSecure()); + if (request.getServletContext().getSessionCookieConfig().isHttpOnly() || + request.getContext().getUseHttpOnly()) { + cookie.setHttpOnly(true); + } + cookie.setAttribute(Constants.COOKIE_PARTITIONED_ATTR, + Boolean.toString(request.getContext().getUsePartitioned())); + + response.addCookie(cookie); + } + + // Invoke the next Valve in our pipeline + getNext().invoke(request, response); + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Process a session destroyed event by removing references to that session from the caches and - if the session + * destruction is the result of a logout - destroy the associated SSO session. + * + * @param ssoId The ID of the SSO session which which the destroyed session was associated + * @param session The session that has been destroyed + */ + public void sessionDestroyed(String ssoId, Session session) { + + if (!getState().isAvailable()) { + return; + } + + // Was the session destroyed as the result of a timeout or context stop? + // If so, we'll just remove the expired session from the SSO. If the + // session was logged out, we'll log out of all session associated with + // the SSO. + if (((session.getMaxInactiveInterval() > 0) && + (session.getIdleTimeInternal() >= session.getMaxInactiveInterval() * 1000)) || + (!session.getManager().getContext().getState().isAvailable())) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.sessionTimeout", ssoId, session)); + } + removeSession(ssoId, session); + } else { + // The session was logged out. + // Deregister this single session id, invalidating + // associated sessions + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.sessionLogout", ssoId, session)); + } + // First remove the session that we know has expired / been logged + // out since it has already been removed from its Manager and, if + // we don't remove it first, deregister() will log a warning that it + // can't be found + removeSession(ssoId, session); + // If the SSO session was only associated with one web app the call + // above will have removed the SSO session from the cache + if (cache.containsKey(ssoId)) { + deregister(ssoId); + } + } + } + + + /** + * Associate the specified single sign on identifier with the specified Session. + * + * @param ssoId Single sign on identifier + * @param session Session to be associated + * + * @return true if the session was associated to the given SSO session, otherwise false + */ + protected boolean associate(String ssoId, Session session) { + SingleSignOnEntry sso = cache.get(ssoId); + if (sso == null) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.associateFail", ssoId, session)); + } + return false; + } else { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.associate", ssoId, session)); + } + sso.addSession(this, ssoId, session); + return true; + } + } + + + /** + * Deregister the specified single sign on identifier, and invalidate any associated sessions. + * + * @param ssoId Single sign on identifier to deregister + */ + protected void deregister(String ssoId) { + + // Look up and remove the corresponding SingleSignOnEntry + SingleSignOnEntry sso = cache.remove(ssoId); + + if (sso == null) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.deregisterFail", ssoId)); + } + return; + } + + // Expire any associated sessions + Set ssoKeys = sso.findSessions(); + if (ssoKeys.size() == 0) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.deregisterNone", ssoId)); + } + } + for (SingleSignOnSessionKey ssoKey : ssoKeys) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.deregister", ssoKey, ssoId)); + } + // Invalidate this session + expire(ssoKey); + } + + // NOTE: Clients may still possess the old single sign on cookie, + // but it will be removed on the next request since it is no longer + // in the cache + } + + + private void expire(SingleSignOnSessionKey key) { + if (engine == null) { + containerLog.warn(sm.getString("singleSignOn.sessionExpire.engineNull", key)); + return; + } + Container host = engine.findChild(key.getHostName()); + if (host == null) { + containerLog.warn(sm.getString("singleSignOn.sessionExpire.hostNotFound", key)); + return; + } + Context context = (Context) host.findChild(key.getContextName()); + if (context == null) { + containerLog.warn(sm.getString("singleSignOn.sessionExpire.contextNotFound", key)); + return; + } + Manager manager = context.getManager(); + if (manager == null) { + containerLog.warn(sm.getString("singleSignOn.sessionExpire.managerNotFound", key)); + return; + } + Session session = null; + try { + session = manager.findSession(key.getSessionId()); + } catch (IOException e) { + containerLog.warn(sm.getString("singleSignOn.sessionExpire.managerError", key), e); + return; + } + if (session == null) { + containerLog.warn(sm.getString("singleSignOn.sessionExpire.sessionNotFound", key)); + return; + } + session.expire(); + } + + + /** + * Attempts reauthentication to the given Realm using the credentials associated with the single + * sign-on session identified by argument ssoId. + *

    + * If reauthentication is successful, the Principal and authorization type associated with the SSO + * session will be bound to the given Request object via calls to {@link Request#setAuthType + * Request.setAuthType()} and {@link Request#setUserPrincipal Request.setUserPrincipal()} + *

    + * + * @param ssoId identifier of SingleSignOn session with which the caller is associated + * @param realm Realm implementation against which the caller is to be authenticated + * @param request the request that needs to be authenticated + * + * @return true if reauthentication was successful, false otherwise. + */ + protected boolean reauthenticate(String ssoId, Realm realm, Request request) { + + if (ssoId == null || realm == null) { + return false; + } + + boolean reauthenticated = false; + + SingleSignOnEntry entry = cache.get(ssoId); + if (entry != null && entry.getCanReauthenticate()) { + + String username = entry.getUsername(); + if (username != null) { + Principal reauthPrincipal = realm.authenticate(username, entry.getPassword()); + if (reauthPrincipal != null) { + reauthenticated = true; + // Bind the authorization credentials to the request + request.setAuthType(entry.getAuthType()); + request.setUserPrincipal(reauthPrincipal); + } + } + } + + return reauthenticated; + } + + + /** + * Register the specified Principal as being associated with the specified value for the single sign on identifier. + * + * @param ssoId Single sign on identifier to register + * @param principal Associated user principal that is identified + * @param authType Authentication type used to authenticate this user principal + * @param username Username used to authenticate this user + * @param password Password used to authenticate this user + */ + protected void register(String ssoId, Principal principal, String authType, String username, String password) { + + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.register", ssoId, + principal != null ? principal.getName() : "", authType)); + } + + cache.put(ssoId, new SingleSignOnEntry(principal, authType, username, password)); + } + + + /** + * Updates any SingleSignOnEntry found under key ssoId with the given authentication data. + *

    + * The purpose of this method is to allow an SSO entry that was established without a username/password combination + * (i.e. established following DIGEST or CLIENT_CERT authentication) to be updated with a username and password if + * one becomes available through a subsequent BASIC or FORM authentication. The SSO entry will then be usable for + * reauthentication. + *

    + * NOTE: Only updates the SSO entry if a call to SingleSignOnEntry.getCanReauthenticate() + * returns false; otherwise, it is assumed that the SSO entry already has sufficient information to + * allow reauthentication and that no update is needed. + * + * @param ssoId identifier of Single sign to be updated + * @param principal the Principal returned by the latest call to Realm.authenticate. + * @param authType the type of authenticator used (BASIC, CLIENT_CERT, DIGEST or FORM) + * @param username the username (if any) used for the authentication + * @param password the password (if any) used for the authentication + * + * @return true if the credentials were updated, otherwise false + */ + protected boolean update(String ssoId, Principal principal, String authType, String username, String password) { + + SingleSignOnEntry sso = cache.get(ssoId); + if (sso != null && !sso.getCanReauthenticate()) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.update", ssoId, authType)); + } + + sso.updateCredentials(principal, authType, username, password); + return true; + } + return false; + } + + + /** + * Remove a single Session from a SingleSignOn. Called when a session is timed out and no longer active. + * + * @param ssoId Single sign on identifier from which to remove the session. + * @param session the session to be removed. + */ + protected void removeSession(String ssoId, Session session) { + + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("singleSignOn.debug.removeSession", session, ssoId)); + } + + // Get a reference to the SingleSignOn + SingleSignOnEntry entry = cache.get(ssoId); + if (entry == null) { + return; + } + + // Remove the inactive session from SingleSignOnEntry + entry.removeSession(session); + + // If there are not sessions left in the SingleSignOnEntry, + // deregister the entry. + if (entry.findSessions().size() == 0) { + deregister(ssoId); + } + } + + + protected SessionListener getSessionListener(String ssoId) { + return new SingleSignOnListener(ssoId); + } + + + @Override + protected void startInternal() throws LifecycleException { + Container c = getContainer(); + while (c != null && !(c instanceof Engine)) { + c = c.getParent(); + } + if (c != null) { + engine = (Engine) c; + } + super.startInternal(); + } + + + @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + engine = null; + } +} diff --git a/java/org/apache/catalina/authenticator/SingleSignOnEntry.java b/java/org/apache/catalina/authenticator/SingleSignOnEntry.java new file mode 100644 index 0000000..1465bb7 --- /dev/null +++ b/java/org/apache/catalina/authenticator/SingleSignOnEntry.java @@ -0,0 +1,196 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.security.Principal; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.catalina.Session; + +/** + * A class that represents entries in the cache of authenticated users. This is necessary to make it available to + * AuthenticatorBase subclasses that need it in order to perform reauthentications when SingleSignOn is in + * use. + * + * @author B Stansberry, based on work by Craig R. McClanahan + * + * @see SingleSignOn + * @see AuthenticatorBase#reauthenticateFromSSO + */ +public class SingleSignOnEntry implements Serializable { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------ Instance Fields + + private String authType = null; + + private String password = null; + + // Marked as transient so special handling can be applied to serialization + private transient Principal principal = null; + + private final Map sessionKeys = new ConcurrentHashMap<>(); + + private String username = null; + + private boolean canReauthenticate = false; + + // --------------------------------------------------------- Constructors + + /** + * Creates a new SingleSignOnEntry + * + * @param principal the Principal returned by the latest call to Realm.authenticate. + * @param authType the type of authenticator used (BASIC, CLIENT_CERT, DIGEST or FORM) + * @param username the username (if any) used for the authentication + * @param password the password (if any) used for the authentication + */ + public SingleSignOnEntry(Principal principal, String authType, String username, String password) { + + updateCredentials(principal, authType, username, password); + } + + // ------------------------------------------------------- Package Methods + + /** + * Adds a Session to the list of those associated with this SSO. + * + * @param sso The SingleSignOn valve that is managing the SSO session. + * @param ssoId The ID of the SSO session. + * @param session The Session being associated with the SSO. + */ + public void addSession(SingleSignOn sso, String ssoId, Session session) { + SingleSignOnSessionKey key = new SingleSignOnSessionKey(session); + SingleSignOnSessionKey currentKey = sessionKeys.putIfAbsent(key, key); + if (currentKey == null) { + // Session not previously added + session.addSessionListener(sso.getSessionListener(ssoId)); + } + } + + /** + * Removes the given Session from the list of those associated with this SSO. + * + * @param session the Session to remove. + */ + public void removeSession(Session session) { + SingleSignOnSessionKey key = new SingleSignOnSessionKey(session); + sessionKeys.remove(key); + } + + /** + * Returns the HTTP Session identifiers associated with this SSO. + * + * @return The identifiers for the HTTP sessions that are current associated with this SSo entry + */ + public Set findSessions() { + return sessionKeys.keySet(); + } + + /** + * Gets the name of the authentication type originally used to authenticate the user associated with the SSO. + * + * @return "BASIC", "CLIENT_CERT", "DIGEST", "FORM" or "NONE" + */ + public String getAuthType() { + return this.authType; + } + + /** + * Gets whether the authentication type associated with the original authentication supports reauthentication. + * + * @return true if getAuthType returns "BASIC" or "FORM", false otherwise. + */ + public boolean getCanReauthenticate() { + return this.canReauthenticate; + } + + /** + * Gets the password credential (if any) associated with the SSO. + * + * @return the password credential associated with the SSO, or null if the original authentication type + * does not involve a password. + */ + public String getPassword() { + return this.password; + } + + /** + * Gets the Principal that has been authenticated by the SSO. + * + * @return The Principal that was created by the authentication that triggered the creation of the SSO entry + */ + public Principal getPrincipal() { + return this.principal; + } + + /** + * Gets the user name provided by the user as part of the authentication process. + * + * @return The user name that was authenticated as part of the authentication that triggered the creation of the SSO + * entry + */ + public String getUsername() { + return this.username; + } + + + /** + * Updates the SingleSignOnEntry to reflect the latest security information associated with the caller. + * + * @param principal the Principal returned by the latest call to Realm.authenticate. + * @param authType the type of authenticator used (BASIC, CLIENT_CERT, DIGEST or FORM) + * @param username the username (if any) used for the authentication + * @param password the password (if any) used for the authentication + */ + public synchronized void updateCredentials(Principal principal, String authType, String username, String password) { + this.principal = principal; + this.authType = authType; + this.username = username; + this.password = password; + this.canReauthenticate = + (HttpServletRequest.BASIC_AUTH.equals(authType) || HttpServletRequest.FORM_AUTH.equals(authType)); + } + + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + if (principal instanceof Serializable) { + out.writeBoolean(true); + out.writeObject(principal); + } else { + out.writeBoolean(false); + } + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + boolean hasPrincipal = in.readBoolean(); + if (hasPrincipal) { + principal = (Principal) in.readObject(); + } + } +} diff --git a/java/org/apache/catalina/authenticator/SingleSignOnListener.java b/java/org/apache/catalina/authenticator/SingleSignOnListener.java new file mode 100644 index 0000000..15e88e0 --- /dev/null +++ b/java/org/apache/catalina/authenticator/SingleSignOnListener.java @@ -0,0 +1,61 @@ +/* + * 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.catalina.authenticator; + +import java.io.Serializable; + +import org.apache.catalina.Authenticator; +import org.apache.catalina.Context; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.SessionEvent; +import org.apache.catalina.SessionListener; + +public class SingleSignOnListener implements SessionListener, Serializable { + + private static final long serialVersionUID = 1L; + + private final String ssoId; + + public SingleSignOnListener(String ssoId) { + this.ssoId = ssoId; + } + + + @Override + public void sessionEvent(SessionEvent event) { + if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())) { + return; + } + + Session session = event.getSession(); + Manager manager = session.getManager(); + if (manager == null) { + return; + } + Context context = manager.getContext(); + Authenticator authenticator = context.getAuthenticator(); + if (!(authenticator instanceof AuthenticatorBase)) { + return; + } + SingleSignOn sso = ((AuthenticatorBase) authenticator).sso; + if (sso == null) { + return; + } + sso.sessionDestroyed(ssoId, session); + } +} diff --git a/java/org/apache/catalina/authenticator/SingleSignOnSessionKey.java b/java/org/apache/catalina/authenticator/SingleSignOnSessionKey.java new file mode 100644 index 0000000..521f404 --- /dev/null +++ b/java/org/apache/catalina/authenticator/SingleSignOnSessionKey.java @@ -0,0 +1,117 @@ +/* + * 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.catalina.authenticator; + +import java.io.Serializable; + +import org.apache.catalina.Context; +import org.apache.catalina.Session; + +/** + * Key used by SSO to identify a session. This key is used rather than the actual session to facilitate the replication + * of the SSO information across a cluster where replicating the entire session would generate significant, unnecessary + * overhead. + */ +public class SingleSignOnSessionKey implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String sessionId; + private final String contextName; + private final String hostName; + + public SingleSignOnSessionKey(Session session) { + this.sessionId = session.getId(); + Context context = session.getManager().getContext(); + this.contextName = context.getName(); + this.hostName = context.getParent().getName(); + } + + public String getSessionId() { + return sessionId; + } + + public String getContextName() { + return contextName; + } + + public String getHostName() { + return hostName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((sessionId == null) ? 0 : sessionId.hashCode()); + result = prime * result + ((contextName == null) ? 0 : contextName.hashCode()); + result = prime * result + ((hostName == null) ? 0 : hostName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SingleSignOnSessionKey other = (SingleSignOnSessionKey) obj; + if (sessionId == null) { + if (other.sessionId != null) { + return false; + } + } else if (!sessionId.equals(other.sessionId)) { + return false; + } + if (contextName == null) { + if (other.contextName != null) { + return false; + } + } else if (!contextName.equals(other.contextName)) { + return false; + } + if (hostName == null) { + if (other.hostName != null) { + return false; + } + } else if (!hostName.equals(other.hostName)) { + return false; + } + return true; + } + + @Override + public String toString() { + // Session ID is 32. Standard text is 36. Host could easily be 20+. + // Context could be anything from 0 upwards. 128 seems like a reasonable + // size to accommodate most cases without being too big. + StringBuilder sb = new StringBuilder(128); + sb.append("Host: ["); + sb.append(hostName); + sb.append("], Context: ["); + sb.append(contextName); + sb.append("], SessionID: ["); + sb.append(sessionId); + sb.append(']'); + return sb.toString(); + } +} diff --git a/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java b/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java new file mode 100644 index 0000000..6377416 --- /dev/null +++ b/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java @@ -0,0 +1,496 @@ +/* + * 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.catalina.authenticator; + +import java.io.File; +import java.io.IOException; +import java.security.Principal; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.LinkedHashMap; +import java.util.regex.Pattern; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Realm; +import org.apache.catalina.connector.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.compat.JreVendor; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.Oid; + +/** + * A SPNEGO authenticator that uses the SPNEGO/Kerberos support built in to Java 6. Successful Kerberos authentication + * depends on the correct configuration of multiple components. If the configuration is invalid, the error messages are + * often cryptic although a Google search will usually point you in the right direction. + */ +public class SpnegoAuthenticator extends AuthenticatorBase { + + private final Log log = LogFactory.getLog(SpnegoAuthenticator.class); // must not be static + private static final String AUTH_HEADER_VALUE_NEGOTIATE = "Negotiate"; + + private String loginConfigName = Constants.DEFAULT_LOGIN_MODULE_NAME; + + public String getLoginConfigName() { + return loginConfigName; + } + + public void setLoginConfigName(String loginConfigName) { + this.loginConfigName = loginConfigName; + } + + private boolean storeDelegatedCredential = true; + + public boolean isStoreDelegatedCredential() { + return storeDelegatedCredential; + } + + public void setStoreDelegatedCredential(boolean storeDelegatedCredential) { + this.storeDelegatedCredential = storeDelegatedCredential; + } + + private Pattern noKeepAliveUserAgents = null; + + public String getNoKeepAliveUserAgents() { + Pattern p = noKeepAliveUserAgents; + if (p == null) { + return null; + } else { + return p.pattern(); + } + } + + public void setNoKeepAliveUserAgents(String noKeepAliveUserAgents) { + if (noKeepAliveUserAgents == null || noKeepAliveUserAgents.length() == 0) { + this.noKeepAliveUserAgents = null; + } else { + this.noKeepAliveUserAgents = Pattern.compile(noKeepAliveUserAgents); + } + } + + private boolean applyJava8u40Fix = true; + + public boolean getApplyJava8u40Fix() { + return applyJava8u40Fix; + } + + public void setApplyJava8u40Fix(boolean applyJava8u40Fix) { + this.applyJava8u40Fix = applyJava8u40Fix; + } + + + @Override + protected String getAuthMethod() { + return Constants.SPNEGO_METHOD; + } + + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + + // Kerberos configuration file location + String krb5Conf = System.getProperty(Constants.KRB5_CONF_PROPERTY); + if (krb5Conf == null) { + // System property not set, use the Tomcat default + File krb5ConfFile = new File(container.getCatalinaBase(), Constants.DEFAULT_KRB5_CONF); + System.setProperty(Constants.KRB5_CONF_PROPERTY, krb5ConfFile.getAbsolutePath()); + } + + // JAAS configuration file location + String jaasConf = System.getProperty(Constants.JAAS_CONF_PROPERTY); + if (jaasConf == null) { + // System property not set, use the Tomcat default + File jaasConfFile = new File(container.getCatalinaBase(), Constants.DEFAULT_JAAS_CONF); + System.setProperty(Constants.JAAS_CONF_PROPERTY, jaasConfFile.getAbsolutePath()); + } + } + + + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException { + + if (checkForCachedAuthentication(request, response, true)) { + return true; + } + + MessageBytes authorization = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); + + if (authorization == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.noAuthHeader")); + } + response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + authorization.toBytes(); + ByteChunk authorizationBC = authorization.getByteChunk(); + + if (!authorizationBC.startsWithIgnoreCase("negotiate ", 0)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("spnegoAuthenticator.authHeaderNotNego")); + } + response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + authorizationBC.setOffset(authorizationBC.getOffset() + 10); + + byte[] decoded = Base64.decodeBase64(authorizationBC.getBuffer(), authorizationBC.getOffset(), + authorizationBC.getLength()); + + if (getApplyJava8u40Fix()) { + SpnegoTokenFixer.fix(decoded); + } + + if (decoded.length == 0) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("spnegoAuthenticator.authHeaderNoToken")); + } + response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + LoginContext lc = null; + GSSContext gssContext = null; + byte[] outToken = null; + Principal principal = null; + try { + try { + lc = new LoginContext(getLoginConfigName()); + lc.login(); + } catch (LoginException e) { + log.error(sm.getString("spnegoAuthenticator.serviceLoginFail"), e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return false; + } + + Subject subject = lc.getSubject(); + + // Assume the GSSContext is stateless + // TODO: Confirm this assumption + final GSSManager manager = GSSManager.getInstance(); + // IBM JDK only understands indefinite lifetime + final int credentialLifetime; + if (JreVendor.IS_IBM_JVM) { + credentialLifetime = GSSCredential.INDEFINITE_LIFETIME; + } else { + credentialLifetime = GSSCredential.DEFAULT_LIFETIME; + } + final PrivilegedExceptionAction action = () -> manager.createCredential(null, + credentialLifetime, new Oid("1.3.6.1.5.5.2"), GSSCredential.ACCEPT_ONLY); + gssContext = manager.createContext(Subject.doAs(subject, action)); + + outToken = Subject.doAs(lc.getSubject(), new AcceptAction(gssContext, decoded)); + + if (outToken == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("spnegoAuthenticator.ticketValidateFail")); + } + // Start again + response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + principal = Subject.doAs(subject, + new AuthenticateAction(context.getRealm(), gssContext, storeDelegatedCredential)); + + } catch (GSSException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("spnegoAuthenticator.ticketValidateFail"), e); + } + response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } catch (PrivilegedActionException e) { + Throwable cause = e.getCause(); + if (cause instanceof GSSException) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("spnegoAuthenticator.serviceLoginFail"), e); + } + } else { + log.error(sm.getString("spnegoAuthenticator.serviceLoginFail"), e); + } + response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } finally { + if (gssContext != null) { + try { + gssContext.dispose(); + } catch (GSSException e) { + // Ignore + } + } + if (lc != null) { + try { + lc.logout(); + } catch (LoginException e) { + // Ignore + } + } + } + + // Send response token on success and failure + response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE + " " + Base64.encodeBase64String(outToken)); + + if (principal != null) { + register(request, response, principal, Constants.SPNEGO_METHOD, principal.getName(), null); + + Pattern p = noKeepAliveUserAgents; + if (p != null) { + MessageBytes ua = request.getCoyoteRequest().getMimeHeaders().getValue("user-agent"); + if (ua != null && p.matcher(ua.toString()).matches()) { + response.setHeader("Connection", "close"); + } + } + return true; + } + + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + + @Override + protected boolean isPreemptiveAuthPossible(Request request) { + MessageBytes authorizationHeader = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); + return authorizationHeader != null && authorizationHeader.startsWithIgnoreCase("negotiate ", 0); + } + + + /** + * This class gets a gss credential via a privileged action. + */ + public static class AcceptAction implements PrivilegedExceptionAction { + + GSSContext gssContext; + + byte[] decoded; + + public AcceptAction(GSSContext context, byte[] decodedToken) { + this.gssContext = context; + this.decoded = decodedToken; + } + + @Override + public byte[] run() throws GSSException { + return gssContext.acceptSecContext(decoded, 0, decoded.length); + } + } + + + public static class AuthenticateAction implements PrivilegedAction { + + private final Realm realm; + private final GSSContext gssContext; + private final boolean storeDelegatedCredential; + + public AuthenticateAction(Realm realm, GSSContext gssContext, boolean storeDelegatedCredential) { + this.realm = realm; + this.gssContext = gssContext; + this.storeDelegatedCredential = storeDelegatedCredential; + } + + @Override + public Principal run() { + return realm.authenticate(gssContext, storeDelegatedCredential); + } + } + + + /** + * This class implements a hack around an incompatibility between the SPNEGO implementation in Windows and the + * SPNEGO implementation in Java 8 update 40 onwards. It was introduced by the change to fix this bug: + * https://bugs.openjdk.java.net/browse/JDK-8048194 (note: the change applied is not the one suggested in the bug + * report) + *

    + * It is not clear to me if Windows, Java or Tomcat is at fault here. I think it is Java but I could be wrong. + *

    + * This hack works by re-ordering the list of mechTypes in the NegTokenInit token. + */ + public static class SpnegoTokenFixer { + + public static void fix(byte[] token) { + SpnegoTokenFixer fixer = new SpnegoTokenFixer(token); + fixer.fix(); + } + + + private final byte[] token; + private int pos = 0; + + + private SpnegoTokenFixer(byte[] token) { + this.token = token; + } + + + // Fixes the token in-place + private void fix() { + /* + * Useful references: http://tools.ietf.org/html/rfc4121#page-5 http://tools.ietf.org/html/rfc2743#page-81 + * https://msdn.microsoft.com/en-us/library/ms995330.aspx + */ + + // Scan until we find the mech types list. If we find anything + // unexpected, abort the fix process. + if (!tag(0x60)) { + return; + } + if (!length()) { + return; + } + if (!oid("1.3.6.1.5.5.2")) { + return; + } + if (!tag(0xa0)) { + return; + } + if (!length()) { + return; + } + if (!tag(0x30)) { + return; + } + if (!length()) { + return; + } + if (!tag(0xa0)) { + return; + } + lengthAsInt(); + if (!tag(0x30)) { + return; + } + // Now at the start of the mechType list. + // Read the mechTypes into an ordered set + int mechTypesLen = lengthAsInt(); + int mechTypesStart = pos; + LinkedHashMap mechTypeEntries = new LinkedHashMap<>(); + while (pos < mechTypesStart + mechTypesLen) { + int[] value = new int[2]; + value[0] = pos; + String key = oidAsString(); + value[1] = pos - value[0]; + mechTypeEntries.put(key, value); + } + // Now construct the re-ordered mechType list + byte[] replacement = new byte[mechTypesLen]; + int replacementPos = 0; + + int[] first = mechTypeEntries.remove("1.2.840.113554.1.2.2"); + if (first != null) { + System.arraycopy(token, first[0], replacement, replacementPos, first[1]); + replacementPos += first[1]; + } + for (int[] markers : mechTypeEntries.values()) { + System.arraycopy(token, markers[0], replacement, replacementPos, markers[1]); + replacementPos += markers[1]; + } + + // Finally, replace the original mechType list with the re-ordered + // one. + System.arraycopy(replacement, 0, token, mechTypesStart, mechTypesLen); + } + + + private boolean tag(int expected) { + return (token[pos++] & 0xFF) == expected; + } + + + private boolean length() { + // No need to retain the length - just need to consume it and make + // sure it is valid. + int len = lengthAsInt(); + return pos + len == token.length; + } + + + private int lengthAsInt() { + int len = token[pos++] & 0xFF; + if (len > 127) { + int bytes = len - 128; + len = 0; + for (int i = 0; i < bytes; i++) { + len = len << 8; + len = len + (token[pos++] & 0xff); + } + } + return len; + } + + + private boolean oid(String expected) { + return expected.equals(oidAsString()); + } + + + private String oidAsString() { + if (!tag(0x06)) { + return null; + } + StringBuilder result = new StringBuilder(); + int len = lengthAsInt(); + // First byte is special case + int v = token[pos++] & 0xFF; + int c2 = v % 40; + int c1 = (v - c2) / 40; + result.append(c1); + result.append('.'); + result.append(c2); + int c = 0; + boolean write = false; + for (int i = 1; i < len; i++) { + int b = token[pos++] & 0xFF; + if (b > 127) { + b -= 128; + } else { + write = true; + } + c = c << 7; + c += b; + if (write) { + result.append('.'); + result.append(c); + c = 0; + write = false; + } + } + return result.toString(); + } + } +} diff --git a/java/org/apache/catalina/authenticator/jaspic/AuthConfigFactoryImpl.java b/java/org/apache/catalina/authenticator/jaspic/AuthConfigFactoryImpl.java new file mode 100644 index 0000000..9da0d05 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/AuthConfigFactoryImpl.java @@ -0,0 +1,646 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.AuthStatus; +import jakarta.security.auth.message.MessageInfo; +import jakarta.security.auth.message.config.AuthConfigFactory; +import jakarta.security.auth.message.config.AuthConfigProvider; +import jakarta.security.auth.message.config.ClientAuthConfig; +import jakarta.security.auth.message.config.RegistrationListener; +import jakarta.security.auth.message.config.ServerAuthConfig; +import jakarta.security.auth.message.config.ServerAuthContext; +import jakarta.security.auth.message.module.ServerAuthModule; +import jakarta.servlet.ServletContext; + +import org.apache.catalina.Globals; +import org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations.Provider; +import org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations.Providers; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class AuthConfigFactoryImpl extends AuthConfigFactory { + + private final Log log = LogFactory.getLog(AuthConfigFactoryImpl.class); // must not be static + private static final StringManager sm = StringManager.getManager(AuthConfigFactoryImpl.class); + + private static final String CONFIG_PATH = "conf/jaspic-providers.xml"; + private static final File CONFIG_FILE = new File(System.getProperty(Globals.CATALINA_BASE_PROP), CONFIG_PATH); + private static final Object CONFIG_FILE_LOCK = new Object(); + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static final String SERVLET_LAYER_ID = "HttpServlet"; + + private static String DEFAULT_REGISTRATION_ID = getRegistrationID(null, null); + + private final Map layerAppContextRegistrations = new ConcurrentHashMap<>(); + private final Map appContextRegistrations = new ConcurrentHashMap<>(); + private final Map layerRegistrations = new ConcurrentHashMap<>(); + // Note: Although there will only ever be a maximum of one entry in this + // Map, use a ConcurrentHashMap for consistency + private final Map defaultRegistration = new ConcurrentHashMap<>(1); + + + public AuthConfigFactoryImpl() { + loadPersistentRegistrations(); + } + + + @Override + public AuthConfigProvider getConfigProvider(String layer, String appContext, RegistrationListener listener) { + RegistrationContextImpl registrationContext = findRegistrationContextImpl(layer, appContext); + if (registrationContext != null) { + if (listener != null) { + RegistrationListenerWrapper wrapper = new RegistrationListenerWrapper(layer, appContext, listener); + registrationContext.addListener(wrapper); + } + return registrationContext.getProvider(); + } + return null; + } + + + @Override + public String registerConfigProvider(String className, Map properties, String layer, + String appContext, String description) { + String registrationID = doRegisterConfigProvider(className, properties, layer, appContext, description); + savePersistentRegistrations(); + return registrationID; + } + + + private String doRegisterConfigProvider(String className, Map properties, String layer, + String appContext, String description) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authConfigFactoryImpl.registerClass", className, layer, appContext)); + } + + AuthConfigProvider provider = null; + if (className != null) { + provider = createAuthConfigProvider(className, properties); + } + + String registrationID = getRegistrationID(layer, appContext); + RegistrationContextImpl registrationContextImpl = + new RegistrationContextImpl(layer, appContext, description, true, provider, properties); + addRegistrationContextImpl(layer, appContext, registrationID, registrationContextImpl); + return registrationID; + } + + + private AuthConfigProvider createAuthConfigProvider(String className, Map properties) + throws SecurityException { + Class clazz = null; + AuthConfigProvider provider = null; + try { + clazz = Class.forName(className, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + // Ignore so the re-try below can proceed + } + try { + if (clazz == null) { + clazz = Class.forName(className); + } + Constructor constructor = clazz.getConstructor(Map.class, AuthConfigFactory.class); + provider = (AuthConfigProvider) constructor.newInstance(properties, null); + } catch (ReflectiveOperationException | IllegalArgumentException e) { + throw new SecurityException(e); + } + return provider; + } + + + @Override + public String registerConfigProvider(AuthConfigProvider provider, String layer, String appContext, + String description) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authConfigFactoryImpl.registerInstance", provider.getClass().getName(), layer, + appContext)); + } + String registrationID = getRegistrationID(layer, appContext); + RegistrationContextImpl registrationContextImpl = + new RegistrationContextImpl(layer, appContext, description, false, provider, null); + addRegistrationContextImpl(layer, appContext, registrationID, registrationContextImpl); + return registrationID; + } + + + private void addRegistrationContextImpl(String layer, String appContext, String registrationID, + RegistrationContextImpl registrationContextImpl) { + RegistrationContextImpl previous = null; + + // Add the registration, noting any registration it replaces + if (layer != null && appContext != null) { + previous = layerAppContextRegistrations.put(registrationID, registrationContextImpl); + } else if (layer == null && appContext != null) { + previous = appContextRegistrations.put(registrationID, registrationContextImpl); + } else if (layer != null && appContext == null) { + previous = layerRegistrations.put(registrationID, registrationContextImpl); + } else { + previous = defaultRegistration.put(registrationID, registrationContextImpl); + } + + if (previous == null) { + // No match with previous registration so need to check listeners + // for all less specific registrations to see if they need to be + // notified of this new registration. That there is no exact match + // with a previous registration allows a few short-cuts to be taken + if (layer != null && appContext != null) { + // Need to check existing appContext registrations + // (and layer and default) + // appContext must match + RegistrationContextImpl registration = appContextRegistrations.get(getRegistrationID(null, appContext)); + if (registration != null) { + for (RegistrationListenerWrapper wrapper : registration.listeners) { + if (layer.equals(wrapper.getMessageLayer()) && appContext.equals(wrapper.getAppContext())) { + registration.listeners.remove(wrapper); + wrapper.listener.notify(wrapper.messageLayer, wrapper.appContext); + } + } + } + } + if (appContext != null) { + // Need to check existing layer registrations + // (and default) + // Need to check registrations for all layers + for (RegistrationContextImpl registration : layerRegistrations.values()) { + for (RegistrationListenerWrapper wrapper : registration.listeners) { + if (appContext.equals(wrapper.getAppContext())) { + registration.listeners.remove(wrapper); + wrapper.listener.notify(wrapper.messageLayer, wrapper.appContext); + } + } + } + } + if (layer != null || appContext != null) { + // Need to check default + for (RegistrationContextImpl registration : defaultRegistration.values()) { + for (RegistrationListenerWrapper wrapper : registration.listeners) { + if (appContext != null && appContext.equals(wrapper.getAppContext()) || + layer != null && layer.equals(wrapper.getMessageLayer())) { + registration.listeners.remove(wrapper); + wrapper.listener.notify(wrapper.messageLayer, wrapper.appContext); + } + } + } + } + } else { + // Replaced an existing registration so need to notify those listeners + for (RegistrationListenerWrapper wrapper : previous.listeners) { + previous.listeners.remove(wrapper); + wrapper.listener.notify(wrapper.messageLayer, wrapper.appContext); + } + } + } + + + @Override + public boolean removeRegistration(String registrationID) { + RegistrationContextImpl registration = null; + if (DEFAULT_REGISTRATION_ID.equals(registrationID)) { + registration = defaultRegistration.remove(registrationID); + } + if (registration == null) { + registration = layerAppContextRegistrations.remove(registrationID); + } + if (registration == null) { + registration = appContextRegistrations.remove(registrationID); + } + if (registration == null) { + registration = layerRegistrations.remove(registrationID); + } + + if (registration == null) { + return false; + } else { + for (RegistrationListenerWrapper wrapper : registration.listeners) { + wrapper.getListener().notify(wrapper.getMessageLayer(), wrapper.getAppContext()); + } + if (registration.isPersistent()) { + savePersistentRegistrations(); + } + return true; + } + } + + + @Override + public String[] detachListener(RegistrationListener listener, String layer, String appContext) { + String registrationID = getRegistrationID(layer, appContext); + RegistrationContextImpl registrationContext = findRegistrationContextImpl(layer, appContext); + if (registrationContext != null && registrationContext.removeListener(listener)) { + return new String[] { registrationID }; + } + return EMPTY_STRING_ARRAY; + } + + + @Override + public String[] getRegistrationIDs(AuthConfigProvider provider) { + List result = new ArrayList<>(); + if (provider == null) { + result.addAll(layerAppContextRegistrations.keySet()); + result.addAll(appContextRegistrations.keySet()); + result.addAll(layerRegistrations.keySet()); + if (!defaultRegistration.isEmpty()) { + result.add(DEFAULT_REGISTRATION_ID); + } + } else { + findProvider(provider, layerAppContextRegistrations, result); + findProvider(provider, appContextRegistrations, result); + findProvider(provider, layerRegistrations, result); + findProvider(provider, defaultRegistration, result); + } + return result.toArray(EMPTY_STRING_ARRAY); + } + + + private void findProvider(AuthConfigProvider provider, Map registrations, + List result) { + for (Entry entry : registrations.entrySet()) { + if (provider.equals(entry.getValue().getProvider())) { + result.add(entry.getKey()); + } + } + } + + + @Override + public RegistrationContext getRegistrationContext(String registrationID) { + RegistrationContext result = defaultRegistration.get(registrationID); + if (result == null) { + result = layerAppContextRegistrations.get(registrationID); + } + if (result == null) { + result = appContextRegistrations.get(registrationID); + } + if (result == null) { + result = layerRegistrations.get(registrationID); + } + return result; + } + + + @Override + public void refresh() { + loadPersistentRegistrations(); + } + + + @Override + public String registerServerAuthModule(ServerAuthModule serverAuthModule, Object context) { + if (context == null) { + throw new IllegalArgumentException(sm.getString("authConfigFactoryImpl.nullContext")); + } + + if (context instanceof ServletContext) { + ServletContext servletContext = (ServletContext) context; + String appContext = servletContext.getVirtualServerName() + " " + servletContext.getContextPath(); + + ServerAuthContext serverAuthContext = new SingleModuleServerAuthContext(serverAuthModule); + ServerAuthConfig serverAuthConfig = new SingleContextServerAuthConfig(serverAuthContext, appContext); + AuthConfigProvider authConfigProvider = new SingleConfigAuthConfigProvider(serverAuthConfig); + + return registerConfigProvider(authConfigProvider, SERVLET_LAYER_ID, appContext, ""); + } + + // Unsupported context type + throw new IllegalArgumentException( + sm.getString("authConfigFactoryImpl.unsupportedContextType", context.getClass().getName())); + } + + + @Override + public void removeServerAuthModule(Object context) { + if (context == null) { + throw new IllegalArgumentException(sm.getString("authConfigFactoryImpl.nullContext")); + } + + if (context instanceof ServletContext) { + ServletContext servletContext = (ServletContext) context; + String layer = "HttpServlet"; + String appContextID = servletContext.getVirtualServerName() + " " + servletContext.getContextPath(); + + removeRegistration(getRegistrationID(layer, appContextID)); + } + + // Unsupported context type + throw new IllegalArgumentException( + sm.getString("authConfigFactoryImpl.unsupportedContextType", context.getClass().getName())); + } + + + private static String getRegistrationID(String layer, String appContext) { + if (layer != null && layer.length() == 0) { + throw new IllegalArgumentException(sm.getString("authConfigFactoryImpl.zeroLengthMessageLayer")); + } + if (appContext != null && appContext.length() == 0) { + throw new IllegalArgumentException(sm.getString("authConfigFactoryImpl.zeroLengthAppContext")); + } + return (layer == null ? "" : layer) + ":" + (appContext == null ? "" : appContext); + } + + + private void loadPersistentRegistrations() { + synchronized (CONFIG_FILE_LOCK) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authConfigFactoryImpl.load", CONFIG_FILE.getAbsolutePath())); + } + if (!CONFIG_FILE.isFile()) { + return; + } + Providers providers = PersistentProviderRegistrations.loadProviders(CONFIG_FILE); + for (Provider provider : providers.getProviders()) { + doRegisterConfigProvider(provider.getClassName(), provider.getProperties(), provider.getLayer(), + provider.getAppContext(), provider.getDescription()); + } + } + } + + + private void savePersistentRegistrations() { + synchronized (CONFIG_FILE_LOCK) { + Providers providers = new Providers(); + savePersistentProviders(providers, layerAppContextRegistrations); + savePersistentProviders(providers, appContextRegistrations); + savePersistentProviders(providers, layerRegistrations); + savePersistentProviders(providers, defaultRegistration); + PersistentProviderRegistrations.writeProviders(providers, CONFIG_FILE); + } + } + + + private void savePersistentProviders(Providers providers, Map registrations) { + for (Entry entry : registrations.entrySet()) { + savePersistentProvider(providers, entry.getValue()); + } + } + + + private void savePersistentProvider(Providers providers, RegistrationContextImpl registrationContextImpl) { + if (registrationContextImpl != null && registrationContextImpl.isPersistent()) { + Provider provider = new Provider(); + provider.setAppContext(registrationContextImpl.getAppContext()); + if (registrationContextImpl.getProvider() != null) { + provider.setClassName(registrationContextImpl.getProvider().getClass().getName()); + } + provider.setDescription(registrationContextImpl.getDescription()); + provider.setLayer(registrationContextImpl.getMessageLayer()); + for (Entry property : registrationContextImpl.getProperties().entrySet()) { + provider.addProperty(property.getKey(), property.getValue()); + } + providers.addProvider(provider); + } + } + + + private RegistrationContextImpl findRegistrationContextImpl(String layer, String appContext) { + RegistrationContextImpl result; + result = layerAppContextRegistrations.get(getRegistrationID(layer, appContext)); + if (result == null) { + result = appContextRegistrations.get(getRegistrationID(null, appContext)); + } + if (result == null) { + result = layerRegistrations.get(getRegistrationID(layer, null)); + } + if (result == null) { + result = defaultRegistration.get(DEFAULT_REGISTRATION_ID); + } + return result; + } + + + private static class RegistrationContextImpl implements RegistrationContext { + + private RegistrationContextImpl(String messageLayer, String appContext, String description, boolean persistent, + AuthConfigProvider provider, Map properties) { + this.messageLayer = messageLayer; + this.appContext = appContext; + this.description = description; + this.persistent = persistent; + this.provider = provider; + Map propertiesCopy = new HashMap<>(); + if (properties != null) { + propertiesCopy.putAll(properties); + } + this.properties = Collections.unmodifiableMap(propertiesCopy); + } + + private final String messageLayer; + private final String appContext; + private final String description; + private final boolean persistent; + private final AuthConfigProvider provider; + private final Map properties; + private final List listeners = new CopyOnWriteArrayList<>(); + + @Override + public String getMessageLayer() { + return messageLayer; + } + + + @Override + public String getAppContext() { + return appContext; + } + + @Override + public String getDescription() { + return description; + } + + + @Override + public boolean isPersistent() { + return persistent; + } + + + private AuthConfigProvider getProvider() { + return provider; + } + + + private void addListener(RegistrationListenerWrapper listener) { + if (listener != null) { + listeners.add(listener); + } + } + + + private Map getProperties() { + return properties; + } + + + private boolean removeListener(RegistrationListener listener) { + boolean result = false; + for (RegistrationListenerWrapper wrapper : listeners) { + if (wrapper.getListener().equals(listener)) { + listeners.remove(wrapper); + result = true; + } + } + return result; + } + } + + + private static class RegistrationListenerWrapper { + + private final String messageLayer; + private final String appContext; + private final RegistrationListener listener; + + + RegistrationListenerWrapper(String messageLayer, String appContext, RegistrationListener listener) { + this.messageLayer = messageLayer; + this.appContext = appContext; + this.listener = listener; + } + + + public String getMessageLayer() { + return messageLayer; + } + + + public String getAppContext() { + return appContext; + } + + + public RegistrationListener getListener() { + return listener; + } + } + + + private static class SingleModuleServerAuthContext implements ServerAuthContext { + + private final ServerAuthModule module; + + SingleModuleServerAuthContext(ServerAuthModule module) { + this.module = module; + } + + @Override + public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) + throws AuthException { + return module.validateRequest(messageInfo, clientSubject, serviceSubject); + } + + @Override + public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException { + return module.secureResponse(messageInfo, serviceSubject); + } + + @Override + public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException { + module.cleanSubject(messageInfo, subject); + } + } + + + private static class SingleContextServerAuthConfig implements ServerAuthConfig { + + private final ServerAuthContext context; + private final String appContext; + + SingleContextServerAuthConfig(ServerAuthContext context, String appContext) { + this.context = context; + this.appContext = appContext; + } + + @Override + public String getMessageLayer() { + return SERVLET_LAYER_ID; + } + + @Override + public String getAppContext() { + return appContext; + } + + @Override + public String getAuthContextID(MessageInfo messageInfo) { + return messageInfo.toString(); + } + + @Override + public void refresh() { + // NO-OP + } + + @Override + public boolean isProtected() { + return false; + } + + @Override + public ServerAuthContext getAuthContext(String authContextID, Subject serviceSubject, + Map properties) throws AuthException { + return context; + } + } + + + private static class SingleConfigAuthConfigProvider implements AuthConfigProvider { + + private final ServerAuthConfig serverAuthConfig; + + SingleConfigAuthConfigProvider(ServerAuthConfig serverAuthConfig) { + this.serverAuthConfig = serverAuthConfig; + } + + @Override + public ClientAuthConfig getClientAuthConfig(String layer, String appContext, CallbackHandler handler) + throws AuthException { + // Should never be called + throw new UnsupportedOperationException(); + } + + @Override + public ServerAuthConfig getServerAuthConfig(String layer, String appContext, CallbackHandler handler) + throws AuthException { + return serverAuthConfig; + } + + @Override + public void refresh() { + // NO-OP + } + } +} diff --git a/java/org/apache/catalina/authenticator/jaspic/CallbackHandlerImpl.java b/java/org/apache/catalina/authenticator/jaspic/CallbackHandlerImpl.java new file mode 100644 index 0000000..17d97c6 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/CallbackHandlerImpl.java @@ -0,0 +1,132 @@ +/* + * 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.catalina.authenticator.jaspic; + +import java.io.IOException; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; + +import jakarta.security.auth.message.callback.CallerPrincipalCallback; +import jakarta.security.auth.message.callback.GroupPrincipalCallback; +import jakarta.security.auth.message.callback.PasswordValidationCallback; + +import org.apache.catalina.Contained; +import org.apache.catalina.Container; +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Default implementation of a JASPIC CallbackHandler. + */ +public class CallbackHandlerImpl implements CallbackHandler, Contained { + + private static final StringManager sm = StringManager.getManager(CallbackHandlerImpl.class); + private final Log log = LogFactory.getLog(CallbackHandlerImpl.class); // must not be static + + private Container container; + + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + + String name = null; + Principal principal = null; + Subject subject = null; + String[] groups = null; + + if (callbacks != null) { + // Need to combine data from multiple callbacks so use this to hold + // the data + // Process the callbacks + for (Callback callback : callbacks) { + if (callback instanceof CallerPrincipalCallback) { + CallerPrincipalCallback cpc = (CallerPrincipalCallback) callback; + name = cpc.getName(); + principal = cpc.getPrincipal(); + subject = cpc.getSubject(); + } else if (callback instanceof GroupPrincipalCallback) { + GroupPrincipalCallback gpc = (GroupPrincipalCallback) callback; + groups = gpc.getGroups(); + } else if (callback instanceof PasswordValidationCallback) { + if (container == null) { + log.warn(sm.getString("callbackHandlerImpl.containerMissing", callback.getClass().getName())); + } else if (container.getRealm() == null) { + log.warn(sm.getString("callbackHandlerImpl.realmMissing", callback.getClass().getName(), + container.getName())); + } else { + PasswordValidationCallback pvc = (PasswordValidationCallback) callback; + principal = + container.getRealm().authenticate(pvc.getUsername(), String.valueOf(pvc.getPassword())); + pvc.setResult(principal != null); + subject = pvc.getSubject(); + } + } else { + log.error(sm.getString("callbackHandlerImpl.jaspicCallbackMissing", callback.getClass().getName())); + } + } + + // Create the GenericPrincipal + Principal gp = getPrincipal(principal, name, groups); + if (subject != null && gp != null) { + subject.getPrivateCredentials().add(gp); + } + } + } + + + private Principal getPrincipal(Principal principal, String name, String[] groups) { + // If the Principal is cached in the session JASPIC may simply return it + if (principal instanceof GenericPrincipal) { + return principal; + } + if (name == null && principal != null) { + name = principal.getName(); + } + if (name == null) { + return null; + } + List roles; + if (groups == null || groups.length == 0) { + roles = Collections.emptyList(); + } else { + roles = Arrays.asList(groups); + } + + return new GenericPrincipal(name, roles, principal); + } + + // Contained interface methods + @Override + public Container getContainer() { + return this.container; + } + + + @Override + public void setContainer(Container container) { + this.container = container; + } +} diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings.properties new file mode 100644 index 0000000..1930492 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings.properties @@ -0,0 +1,35 @@ +# 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. + +authConfigFactoryImpl.load=Loading persistent provider registrations from [{0}] +authConfigFactoryImpl.nullContext=The provided context object must not be null +authConfigFactoryImpl.registerClass=Registering class [{0}] for layer [{1}] and application context [{2}] +authConfigFactoryImpl.registerInstance=Registering instance of type[{0}] for layer [{1}] and application context [{2}] +authConfigFactoryImpl.unsupportedContextType=This implementation only supports contexts of type jakarta.servlet.ServletContext but the provided context was of type [{0}] +authConfigFactoryImpl.zeroLengthAppContext=A zero length application context name is not valid +authConfigFactoryImpl.zeroLengthMessageLayer=A zero length message layer name is not valid + +callbackHandlerImpl.containerMissing=Missing container for JASPIC callback of type [{0}] which was ignored +callbackHandlerImpl.jaspicCallbackMissing=Unsupported JASPIC callback of type [{0}] received which was ignored +callbackHandlerImpl.realmMissing=Missing realm for JASPIC callback of type [{0}] in container [{1}] which was ignored + +jaspicAuthenticator.authenticate=Authenticating request for [{0}] via JASPIC + +persistentProviderRegistrations.deleteFail=The temporary file [{0}] cannot be deleted +persistentProviderRegistrations.existsDeleteFail=The temporary file [{0}] already exists and cannot be deleted +persistentProviderRegistrations.moveFail=Failed to move [{0}] to [{1}] +persistentProviderRegistrations.xmlFeatureEncoding=Exception configuring JASPIC to permit java encoding names in XML configuration files. Only IANA encoding names will be supported. + +simpleServerAuthConfig.noModules="No ServerAuthModules configured" diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_cs.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_cs.properties new file mode 100644 index 0000000..8ee724d --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_cs.properties @@ -0,0 +1,18 @@ +# 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. + +authConfigFactoryImpl.zeroLengthAppContext=Prázdné jméno (nulová délka) aplikaÄního kontextu je neplatné + +persistentProviderRegistrations.existsDeleteFail=DoÄasný soubor [{0}] již existuje a nemůže být smazán diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_de.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_de.properties new file mode 100644 index 0000000..5f4657e --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_de.properties @@ -0,0 +1,18 @@ +# 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. + +authConfigFactoryImpl.zeroLengthAppContext=Ein leerer Name für einen Applikationskontext ist nicht erlaubt. + +persistentProviderRegistrations.existsDeleteFail=Die temporäre Datei [{0}] existiert bereits und kann nicht gelöscht werden diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_es.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_es.properties new file mode 100644 index 0000000..b2d7da9 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_es.properties @@ -0,0 +1,20 @@ +# 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. + +authConfigFactoryImpl.zeroLengthAppContext=Un nombre de aplicación con nombre de contexto de longitud cero no es válido + +persistentProviderRegistrations.deleteFail=El archivo temporal [{0}] no puede ser eliminado +persistentProviderRegistrations.existsDeleteFail=El archivo temporal [{0}] ya existe y no puede ser borrado +persistentProviderRegistrations.moveFail=Fallo al mover [{0}] a [{1}] diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_fr.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_fr.properties new file mode 100644 index 0000000..be339fe --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_fr.properties @@ -0,0 +1,35 @@ +# 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. + +authConfigFactoryImpl.load=Chargement des enregistrements pour le fournisseurs persistants à partir de [{0}] +authConfigFactoryImpl.nullContext=L'objet context fourni ne doit pas être null +authConfigFactoryImpl.registerClass=Enregistrement de la classe [{0}] pour la couche [{1}] et le contexte d''application [{2}] +authConfigFactoryImpl.registerInstance=Enregistrement de l''instance de type [{0}] pour la couche [{1}] et le contexte d''application [{2}] +authConfigFactoryImpl.unsupportedContextType=L''implémentation ne supporte que des contextes de type jakarta.servlet.ServletContext mais le contexte fourni était de type [{0}] +authConfigFactoryImpl.zeroLengthAppContext=Un nom de contexte vide n'est pas valide +authConfigFactoryImpl.zeroLengthMessageLayer=Un message vide de nom de couche est invalide + +callbackHandlerImpl.containerMissing=Le rappel (callback) JASPIC de type [{0}] a un conteneur manquant et a été ignoré +callbackHandlerImpl.jaspicCallbackMissing=Le rappel (callback) JASPIC de type [{0}] reçu n''est pas supporté et a été ignoré +callbackHandlerImpl.realmMissing=Le rappel (callback) JASPIC de type [{0}] reçu n''a pas de royaume (realm) et a été ignoré + +jaspicAuthenticator.authenticate=Authentification de la requête pour [{0}] avec JASPIC + +persistentProviderRegistrations.deleteFail=Le fichier temporaire [{0}] n''a pas pu être effacé +persistentProviderRegistrations.existsDeleteFail=Le fichier temporaire [{0}] existe déjà et ne peut être effacé +persistentProviderRegistrations.moveFail=Echec de déplacement de [{0}] vers [{1}] +persistentProviderRegistrations.xmlFeatureEncoding=Exception lors de la configuration de JASPIC pour permettre des nom d'encodage Java dans les fichiers de configuration XML, seuls les noms d'encodage IANA seront supportés + +simpleServerAuthConfig.noModules=Aucun ServerAuthModules n'est configuré diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ja.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ja.properties new file mode 100644 index 0000000..2ae4573 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ja.properties @@ -0,0 +1,35 @@ +# 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. + +authConfigFactoryImpl.load=[{0}]ã‹ã‚‰æ°¸ç¶šåŒ–プロãƒã‚¤ãƒ€ã®ç™»éŒ²ã‚’読ã¿è¾¼ã¿ã¾ã™ã€‚ +authConfigFactoryImpl.nullContext=æä¾›ã•ã‚ŒãŸã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚ªãƒ–ジェクトã¯nullã§ã‚ã£ã¦ã¯ãªã‚Šã¾ã›ã‚“ +authConfigFactoryImpl.registerClass=アプリケーションコンテキスト [{2}] ã®ãƒ¬ã‚¤ãƒ¤ãƒ¼ [{1}] ã«ã‚¯ãƒ©ã‚¹ [{0}] を登録ã—ã¾ã™ã€‚ +authConfigFactoryImpl.registerInstance=èªè¨¼æ§‹æˆãƒ—ロãƒã‚¤ãƒ€ [{0}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’ã€ãƒ¬ã‚¤ãƒ¤ [{1}] ã¨ã‚¢ãƒ—リケーションコンテキスト [{2}] ã«ç™»éŒ²ã—ã¦ã„ã¾ã™ +authConfigFactoryImpl.unsupportedContextType=ã“ã®å®Ÿè£…ã¯ã€jakarta.servlet.ServletContextåž‹ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã®ã¿ã‚’サãƒãƒ¼ãƒˆã—ã¾ã™ãŒã€æä¾›ã•ã‚ŒãŸã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã¯[{0}]åž‹ã§ã—㟠+authConfigFactoryImpl.zeroLengthAppContext=é•·ã• 0 ã®ã‚¢ãƒ—リケーションコンテキストåã¯ç„¡åŠ¹ã§ã™ +authConfigFactoryImpl.zeroLengthMessageLayer=é•·ã• 0 ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ¬ã‚¤ãƒ¤åã¯ç„¡åŠ¹ã§ã™ + +callbackHandlerImpl.containerMissing=無視ã•ã‚ŒãŸã‚¿ã‚¤ãƒ— [{0}] ã®JASPICコールãƒãƒƒã‚¯ã®ã‚³ãƒ³ãƒ†ãƒŠãƒ¼ãŒã‚ã‚Šã¾ã›ã‚“ +callbackHandlerImpl.jaspicCallbackMissing=å—ä¿¡ã—ãŸã‚¿ã‚¤ãƒ— [{0}] ã®ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„JASPICコールãƒãƒƒã‚¯ãŒç„¡è¦–ã•ã‚Œã¾ã—㟠+callbackHandlerImpl.realmMissing=無視ã•ã‚ŒãŸã‚³ãƒ³ãƒ†ãƒŠ [{1}] ã®ã‚¿ã‚¤ãƒ— [{0}] ã®JASPICコールãƒãƒƒã‚¯ã®ãƒ¬ãƒ«ãƒ ãŒã‚ã‚Šã¾ã›ã‚“ + +jaspicAuthenticator.authenticate=JASPIC経由㧠[{0}] ã¸ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’èªè¨¼ã—ã¦ã„ã¾ã™ + +persistentProviderRegistrations.deleteFail=一時ファイル [{0}] を削除ã§ãã¾ã›ã‚“。 +persistentProviderRegistrations.existsDeleteFail=åŒåã®ä¸€æ™‚ファイル [{0}] ãŒå­˜åœ¨ã—ã€å‰Šé™¤ã‚‚ã§ãã¾ã›ã‚“ +persistentProviderRegistrations.moveFail=[{0}]ã‚’[{1}]ã«ç§»å‹•ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +persistentProviderRegistrations.xmlFeatureEncoding=XML構æˆãƒ•ã‚¡ã‚¤ãƒ«ã§Javaエンコーディングåを許å¯ã™ã‚‹ã‚ˆã†ã«JASPICを構æˆã™ã‚‹éš›ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ IANAエンコーディングåã®ã¿ãŒã‚µãƒãƒ¼ãƒˆã•ã‚Œã¾ã™ã€‚ + +simpleServerAuthConfig.noModules="ServerAuthModulesãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã›ã‚“" diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ko.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ko.properties new file mode 100644 index 0000000..8be59f3 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ko.properties @@ -0,0 +1,35 @@ +# 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. + +authConfigFactoryImpl.load=[{0}](으)로부터 persistent provider ë“±ë¡ ì‚¬í•­ë“¤ì„ ë¡œë“œí•©ë‹ˆë‹¤. +authConfigFactoryImpl.nullContext=ì œê³µëœ ì»¨í…스트가 ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +authConfigFactoryImpl.registerClass=ë ˆì´ì–´ [{1}]와(ê³¼) 애플리케ì´ì…˜ 컨í…스트 [{2}]ì„(를) 위한 í´ëž˜ìŠ¤ [{0}]ì„(를) 등ë¡í•©ë‹ˆë‹¤. +authConfigFactoryImpl.registerInstance=ë ˆì´ì–´ [{1}]와(ê³¼) 애플리케ì´ì…˜ 컨í…스트 [{2}]ì„(를) 위한 타입 [{0}]ì˜ ì¸ìŠ¤í„´ìŠ¤ë¥¼ 등ë¡í•©ë‹ˆë‹¤. +authConfigFactoryImpl.unsupportedContextType=ì´ êµ¬í˜„ì€ íƒ€ìž…ì´ jakarta.servlet.ServletContextì¸ ì»¨í…스트 ê°ì²´ë“¤ë§Œì„ 지ì›í•˜ëŠ”ë°, íƒ€ìž…ì´ [{0}]ì¸ ì»¨í…스트 ê°ì²´ê°€ 제공ë˜ì—ˆìŠµë‹ˆë‹¤. +authConfigFactoryImpl.zeroLengthAppContext=애플리케ì´ì…˜ 컨í…스트 ì´ë¦„ì˜ ê¸¸ì´ê°€ 0으로, ì´ëŠ” 유효하지 않습니다. +authConfigFactoryImpl.zeroLengthMessageLayer=길ì´ê°€ 0ì¸ ë©”ì‹œì§€ ë ˆì´ì–´ ì´ë¦„ì€ ìœ íš¨í•˜ì§€ 않습니다. + +callbackHandlerImpl.containerMissing=íƒ€ìž…ì´ [{0}]ì¸ JASPIC ì½œë°±ì„ ìœ„í•œ 컨테ì´ë„ˆê°€ 존재하지 ì•Šì•„ 무시ë©ë‹ˆë‹¤. +callbackHandlerImpl.jaspicCallbackMissing=íƒ€ìž…ì´ [{0}]ì¸ ì§€ì›ë˜ì§€ 않는 JASPIC ì½œë°±ì„ ë°›ì•˜ëŠ”ë°, ì´ëŠ” 무시ë©ë‹ˆë‹¤. +callbackHandlerImpl.realmMissing=컨테ì´ë„ˆ [{1}]ì— íƒ€ìž…ì´ [{0}]ì¸ JASPIC ì½œë°±ì„ ìœ„í•œ Realmì´ ì¡´ìž¬í•˜ì§€ ì•Šì•„ 무시ë©ë‹ˆë‹¤. + +jaspicAuthenticator.authenticate=[{0}]ì„(를) 위한 ìš”ì²­ì„ JASPIC를 통하여 ì¸ì¦í•©ë‹ˆë‹¤. + +persistentProviderRegistrations.deleteFail=ìž„ì‹œ íŒŒì¼ [{0}]ì„(를) 삭제할 수 없습니다. +persistentProviderRegistrations.existsDeleteFail=ìž„ì‹œ íŒŒì¼ [{0}]ì´(ê°€) ì´ë¯¸ 존재하며 ì‚­ì œë  ìˆ˜ 없습니다. +persistentProviderRegistrations.moveFail=[{0}]ì„(를) [{1}](으)ë¡œ ì´ë™ì‹œí‚¤ì§€ 못했습니다. +persistentProviderRegistrations.xmlFeatureEncoding=JASPICê°€ XML 설정 파ì¼ë“¤ì—ì„œ ìžë°” ì¸ì½”딩 ëª…ì¹­ë“¤ì„ í—ˆìš©í•˜ë„ë¡ ì„¤ì •í•˜ëŠ” 중 예외 ë°œìƒ. IANA ì¸ì½”딩 명칭들만 지ì›ë  것입니다. + +simpleServerAuthConfig.noModules="ServerAuthModuleì´ ì„¤ì •ë˜ì§€ ì•ŠìŒ" diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_pt_BR.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..0bf0679 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +persistentProviderRegistrations.existsDeleteFail=O arquivo temporário [{0}] já existe e não pode ser deletado diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ru.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ru.properties new file mode 100644 index 0000000..af9a981 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_ru.properties @@ -0,0 +1,18 @@ +# 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. + +authConfigFactoryImpl.zeroLengthAppContext=Ðазвание контекÑта Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½ÑƒÐ»ÐµÐ²Ð¾Ð¹ длины ÑвлÑетÑÑ Ð½ÐµÐ´ÐµÐ¹Ñтвительным + +persistentProviderRegistrations.existsDeleteFail=Временный файл [{0}] уже ÑущеÑтвует и не может быть удалён diff --git a/java/org/apache/catalina/authenticator/jaspic/LocalStrings_zh_CN.properties b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..3fe84f8 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/LocalStrings_zh_CN.properties @@ -0,0 +1,35 @@ +# 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. + +authConfigFactoryImpl.load=从[{0}]加载æŒä¹…化æä¾›è€…æ³¨å†Œä¿¡æ¯ +authConfigFactoryImpl.nullContext=æ供的上下文对象ä¸å¯ä¸ºnull +authConfigFactoryImpl.registerClass=正在为层[{1}]和应用程åºä¸Šä¸‹æ–‡[{2}]注册类[{0}] +authConfigFactoryImpl.registerInstance=正在为层[{1}]和应用程åºä¸Šä¸‹æ–‡[{2}]注册类型为[{0}]的实例 +authConfigFactoryImpl.unsupportedContextType=该实现åªæ”¯æŒjakarta.servlet.ServletContext类型的上下文,但是æ供的上下文类型为[{0}] +authConfigFactoryImpl.zeroLengthAppContext=应用上下文å称的长度为0是无效的 +authConfigFactoryImpl.zeroLengthMessageLayer=零长度的消æ¯å±‚å称是无效的 + +callbackHandlerImpl.containerMissing=类型[{0}]çš„JASPIC回调缺少容器,已被忽略 +callbackHandlerImpl.jaspicCallbackMissing=接收到ä¸æ”¯æŒçš„类型为[{0}]çš„JASPIC回调,该回调被忽略 +callbackHandlerImpl.realmMissing=容器[{1}]中类型为[{0}]çš„JASPIC回调缺少realm,该realm已被忽略 + +jaspicAuthenticator.authenticate=通过JASPIC验è¯[{0}]的请求 + +persistentProviderRegistrations.deleteFail=无法删除临时文件[{0}] +persistentProviderRegistrations.existsDeleteFail=临时文件[{0}]已存在且无法删除 +persistentProviderRegistrations.moveFail=无法将[{0}]移至[{1}] +persistentProviderRegistrations.xmlFeatureEncoding=å°†JASPICé…置为å…许XMLé…置文件中的javaç¼–ç å称时出现异常。仅支æŒIANAç¼–ç å称。 + +simpleServerAuthConfig.noModules=“没有é…ç½®ServerAuthModules†diff --git a/java/org/apache/catalina/authenticator/jaspic/MessageInfoImpl.java b/java/org/apache/catalina/authenticator/jaspic/MessageInfoImpl.java new file mode 100644 index 0000000..6b1c8a4 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/MessageInfoImpl.java @@ -0,0 +1,78 @@ +/* + * 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.catalina.authenticator.jaspic; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.security.auth.message.MessageInfo; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.tomcat.util.res.StringManager; + +public class MessageInfoImpl implements MessageInfo { + protected static final StringManager sm = StringManager.getManager(MessageInfoImpl.class); + + public static final String IS_MANDATORY = "jakarta.security.auth.message.MessagePolicy.isMandatory"; + + private final Map map = new HashMap<>(); + private HttpServletRequest request; + private HttpServletResponse response; + + public MessageInfoImpl() { + } + + public MessageInfoImpl(HttpServletRequest request, HttpServletResponse response, boolean authMandatory) { + this.request = request; + this.response = response; + map.put(IS_MANDATORY, Boolean.toString(authMandatory)); + } + + @Override + public Map getMap() { + return map; + } + + @Override + public Object getRequestMessage() { + return request; + } + + @Override + public Object getResponseMessage() { + return response; + } + + @Override + public void setRequestMessage(Object request) { + if (!(request instanceof HttpServletRequest)) { + throw new IllegalArgumentException( + sm.getString("authenticator.jaspic.badRequestType", request.getClass().getName())); + } + this.request = (HttpServletRequest) request; + } + + @Override + public void setResponseMessage(Object response) { + if (!(response instanceof HttpServletResponse)) { + throw new IllegalArgumentException( + sm.getString("authenticator.jaspic.badResponseType", response.getClass().getName())); + } + this.response = (HttpServletResponse) response; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/authenticator/jaspic/PersistentProviderRegistrations.java b/java/org/apache/catalina/authenticator/jaspic/PersistentProviderRegistrations.java new file mode 100644 index 0000000..4084578 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/PersistentProviderRegistrations.java @@ -0,0 +1,281 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.SAXException; + +/** + * Utility class for the loading and saving of JASPIC persistent provider registrations. + */ +public final class PersistentProviderRegistrations { + + private static final Log log = LogFactory.getLog(PersistentProviderRegistrations.class); + private static final StringManager sm = StringManager.getManager(PersistentProviderRegistrations.class); + + + private PersistentProviderRegistrations() { + // Utility class. Hide default constructor + } + + + static Providers loadProviders(File configFile) { + try (InputStream is = new FileInputStream(configFile)) { + // Construct a digester to read the XML input file + Digester digester = new Digester(); + + try { + digester.setFeature("http://apache.org/xml/features/allow-java-encodings", true); + } catch (SAXException se) { + log.warn(sm.getString("persistentProviderRegistrations.xmlFeatureEncoding"), se); + } + + digester.setValidating(true); + digester.setNamespaceAware(true); + + // Create an object to hold the parse results and put it on the top + // of the digester's stack + Providers result = new Providers(); + digester.push(result); + + // Configure the digester + digester.addObjectCreate("jaspic-providers/provider", Provider.class.getName()); + digester.addSetProperties("jaspic-providers/provider"); + digester.addSetNext("jaspic-providers/provider", "addProvider", Provider.class.getName()); + + digester.addObjectCreate("jaspic-providers/provider/property", Property.class.getName()); + digester.addSetProperties("jaspic-providers/provider/property"); + digester.addSetNext("jaspic-providers/provider/property", "addProperty", Property.class.getName()); + + // Parse the input + digester.parse(is); + + return result; + } catch (IOException | ParserConfigurationException | SAXException e) { + throw new SecurityException(e); + } + } + + + static void writeProviders(Providers providers, File configFile) { + File configFileOld = new File(configFile.getAbsolutePath() + ".old"); + File configFileNew = new File(configFile.getAbsolutePath() + ".new"); + + // Remove left over temporary files if present + if (configFileOld.exists()) { + if (configFileOld.delete()) { + throw new SecurityException(sm.getString("persistentProviderRegistrations.existsDeleteFail", + configFileOld.getAbsolutePath())); + } + } + if (configFileNew.exists()) { + if (configFileNew.delete()) { + throw new SecurityException(sm.getString("persistentProviderRegistrations.existsDeleteFail", + configFileNew.getAbsolutePath())); + } + } + + // Write out the providers to the temporary new file + try (OutputStream fos = new FileOutputStream(configFileNew); + Writer writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { + writer.write("\n" + "\n"); + for (Provider provider : providers.providers) { + writer.write(" \n"); + for (Entry entry : provider.getProperties().entrySet()) { + writer.write(" \n"); + } + writer.write(" \n"); + } + writer.write("\n"); + } catch (IOException e) { + if (!configFileNew.delete()) { + Log log = LogFactory.getLog(PersistentProviderRegistrations.class); + log.warn(sm.getString("persistentProviderRegistrations.deleteFail", configFileNew.getAbsolutePath())); + } + throw new SecurityException(e); + } + + // Move the current file out of the way + if (configFile.isFile()) { + if (!configFile.renameTo(configFileOld)) { + throw new SecurityException(sm.getString("persistentProviderRegistrations.moveFail", + configFile.getAbsolutePath(), configFileOld.getAbsolutePath())); + } + } + + // Move the new file into place + if (!configFileNew.renameTo(configFile)) { + throw new SecurityException(sm.getString("persistentProviderRegistrations.moveFail", + configFileNew.getAbsolutePath(), configFile.getAbsolutePath())); + } + + // Remove the old file + if (configFileOld.exists() && !configFileOld.delete()) { + Log log = LogFactory.getLog(PersistentProviderRegistrations.class); + log.warn(sm.getString("persistentProviderRegistrations.deleteFail", configFileOld.getAbsolutePath())); + } + } + + + private static void writeOptional(String name, String value, Writer writer) throws IOException { + if (value != null) { + writer.write(" " + name + "=\""); + writer.write(value); + writer.write("\""); + } + } + + + public static class Providers { + private final List providers = new ArrayList<>(); + + public void addProvider(Provider provider) { + providers.add(provider); + } + + public List getProviders() { + return providers; + } + } + + + public static class Provider { + private String className; + private String layer; + private String appContext; + private String description; + private final Map properties = new HashMap<>(); + + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + + public String getLayer() { + return layer; + } + + public void setLayer(String layer) { + this.layer = layer; + } + + + public String getAppContext() { + return appContext; + } + + public void setAppContext(String appContext) { + this.appContext = appContext; + } + + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + + public void addProperty(Property property) { + properties.put(property.getName(), property.getValue()); + } + + /** + * Used by IntrospectionUtils via reflection. + * + * @param name - the name of of the property to set on this object + * @param value - the value to set + * + * @see #addProperty(String, String) + */ + public void setProperty(String name, String value) { + addProperty(name, value); + } + + void addProperty(String name, String value) { + properties.put(name, value); + } + + public Map getProperties() { + return properties; + } + } + + + public static class Property { + private String name; + private String value; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/java/org/apache/catalina/authenticator/jaspic/SimpleAuthConfigProvider.java b/java/org/apache/catalina/authenticator/jaspic/SimpleAuthConfigProvider.java new file mode 100644 index 0000000..bbc64a3 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/SimpleAuthConfigProvider.java @@ -0,0 +1,88 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.config.AuthConfigFactory; +import jakarta.security.auth.message.config.AuthConfigProvider; +import jakarta.security.auth.message.config.ClientAuthConfig; +import jakarta.security.auth.message.config.ServerAuthConfig; + +/** + * Basic implementation primarily intended for use when using third-party + * {@link jakarta.security.auth.message.module.ServerAuthModule} implementations that only provide the module. + */ +public class SimpleAuthConfigProvider implements AuthConfigProvider { + + private final Map properties; + + private volatile ServerAuthConfig serverAuthConfig; + + public SimpleAuthConfigProvider(Map properties, AuthConfigFactory factory) { + this.properties = properties; + if (factory != null) { + factory.registerConfigProvider(this, null, null, "Automatic registration"); + } + } + + + /** + * {@inheritDoc} + *

    + * This implementation does not support client-side authentication and therefore always returns {@code null}. + */ + @Override + public ClientAuthConfig getClientAuthConfig(String layer, String appContext, CallbackHandler handler) + throws AuthException { + return null; + } + + + @Override + public ServerAuthConfig getServerAuthConfig(String layer, String appContext, CallbackHandler handler) + throws AuthException { + ServerAuthConfig serverAuthConfig = this.serverAuthConfig; + if (serverAuthConfig == null) { + synchronized (this) { + if (this.serverAuthConfig == null) { + this.serverAuthConfig = createServerAuthConfig(layer, appContext, handler, properties); + } + serverAuthConfig = this.serverAuthConfig; + } + } + return serverAuthConfig; + } + + + protected ServerAuthConfig createServerAuthConfig(String layer, String appContext, CallbackHandler handler, + Map properties) { + return new SimpleServerAuthConfig(layer, appContext, handler, properties); + } + + + @Override + public void refresh() { + ServerAuthConfig serverAuthConfig = this.serverAuthConfig; + if (serverAuthConfig != null) { + serverAuthConfig.refresh(); + } + } +} diff --git a/java/org/apache/catalina/authenticator/jaspic/SimpleServerAuthConfig.java b/java/org/apache/catalina/authenticator/jaspic/SimpleServerAuthConfig.java new file mode 100644 index 0000000..2c6cd51 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/SimpleServerAuthConfig.java @@ -0,0 +1,147 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.MessageInfo; +import jakarta.security.auth.message.config.ServerAuthConfig; +import jakarta.security.auth.message.config.ServerAuthContext; +import jakarta.security.auth.message.module.ServerAuthModule; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Basic implementation primarily intended for use when using third-party {@link ServerAuthModule} implementations that + * only provide the module. This implementation supports configuring the {@link ServerAuthContext} with multiple + * modules. + */ +public class SimpleServerAuthConfig implements ServerAuthConfig { + + private static StringManager sm = StringManager.getManager(SimpleServerAuthConfig.class); + + private static final String SERVER_AUTH_MODULE_KEY_PREFIX = + "org.apache.catalina.authenticator.jaspic.ServerAuthModule."; + + private final String layer; + private final String appContext; + private final CallbackHandler handler; + private final Map properties; + + private volatile ServerAuthContext serverAuthContext; + + public SimpleServerAuthConfig(String layer, String appContext, CallbackHandler handler, + Map properties) { + this.layer = layer; + this.appContext = appContext; + this.handler = handler; + this.properties = properties; + } + + + @Override + public String getMessageLayer() { + return layer; + } + + + @Override + public String getAppContext() { + return appContext; + } + + + @Override + public String getAuthContextID(MessageInfo messageInfo) { + return messageInfo.toString(); + } + + + @Override + public void refresh() { + serverAuthContext = null; + } + + + @Override + public boolean isProtected() { + return false; + } + + + @Override + public ServerAuthContext getAuthContext(String authContextID, Subject serviceSubject, Map properties) + throws AuthException { + ServerAuthContext serverAuthContext = this.serverAuthContext; + if (serverAuthContext == null) { + synchronized (this) { + if (this.serverAuthContext == null) { + Map mergedProperties = new HashMap<>(); + if (this.properties != null) { + mergedProperties.putAll(this.properties); + } + if (properties != null) { + mergedProperties.putAll(properties); + } + + List modules = new ArrayList<>(); + int moduleIndex = 1; + String key = SERVER_AUTH_MODULE_KEY_PREFIX + moduleIndex; + Object moduleClassName = mergedProperties.get(key); + while (moduleClassName instanceof String) { + try { + Class clazz = Class.forName((String) moduleClassName); + ServerAuthModule module = (ServerAuthModule) clazz.getConstructor().newInstance(); + module.initialize(null, null, handler, mergedProperties); + modules.add(module); + } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) { + AuthException ae = new AuthException(); + ae.initCause(e); + throw ae; + } + + // Look for the next module + moduleIndex++; + key = SERVER_AUTH_MODULE_KEY_PREFIX + moduleIndex; + moduleClassName = mergedProperties.get(key); + } + + if (modules.size() == 0) { + throw new AuthException(sm.getString("simpleServerAuthConfig.noModules")); + } + + this.serverAuthContext = createServerAuthContext(modules); + } + serverAuthContext = this.serverAuthContext; + } + } + + return serverAuthContext; + } + + + protected ServerAuthContext createServerAuthContext(List modules) { + return new SimpleServerAuthContext(modules); + } +} diff --git a/java/org/apache/catalina/authenticator/jaspic/SimpleServerAuthContext.java b/java/org/apache/catalina/authenticator/jaspic/SimpleServerAuthContext.java new file mode 100644 index 0000000..dca6bd0 --- /dev/null +++ b/java/org/apache/catalina/authenticator/jaspic/SimpleServerAuthContext.java @@ -0,0 +1,72 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.util.List; + +import javax.security.auth.Subject; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.AuthStatus; +import jakarta.security.auth.message.MessageInfo; +import jakarta.security.auth.message.config.ServerAuthContext; +import jakarta.security.auth.message.module.ServerAuthModule; + +/** + * Basic implementation primarily intended for use when using third-party {@link ServerAuthModule} implementations that + * only provide the module. This implementation supports multiple modules and will treat the user as authenticated if + * any one module is able to authenticate the user. + */ +public class SimpleServerAuthContext implements ServerAuthContext { + + private final List modules; + + + public SimpleServerAuthContext(List modules) { + this.modules = modules; + } + + + @Override + public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) + throws AuthException { + for (int moduleIndex = 0; moduleIndex < modules.size(); moduleIndex++) { + ServerAuthModule module = modules.get(moduleIndex); + AuthStatus result = module.validateRequest(messageInfo, clientSubject, serviceSubject); + if (result != AuthStatus.SEND_FAILURE) { + messageInfo.getMap().put("moduleIndex", Integer.valueOf(moduleIndex)); + return result; + } + } + return AuthStatus.SEND_FAILURE; + } + + + @Override + public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException { + ServerAuthModule module = modules.get(((Integer) messageInfo.getMap().get("moduleIndex")).intValue()); + return module.secureResponse(messageInfo, serviceSubject); + } + + + @Override + public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException { + for (ServerAuthModule module : modules) { + module.cleanSubject(messageInfo, subject); + } + } +} diff --git a/java/org/apache/catalina/authenticator/mbeans-descriptors.xml b/java/org/apache/catalina/authenticator/mbeans-descriptors.xml new file mode 100644 index 0000000..bcb6601 --- /dev/null +++ b/java/org/apache/catalina/authenticator/mbeans-descriptors.xml @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/authenticator/package.html b/java/org/apache/catalina/authenticator/package.html new file mode 100644 index 0000000..15b04ff --- /dev/null +++ b/java/org/apache/catalina/authenticator/package.html @@ -0,0 +1,54 @@ + + + +

    This package contains Authenticator implementations for the +various supported authentication methods (BASIC, DIGEST, and FORM). In +addition, there is a convenience base class, +AuthenticatorBase, for customized Authenticator +implementations.

    + +

    If you are using the standard context configuration class +(org.apache.catalina.startup.ContextConfig) to configure the +Authenticator associated with a particular context, you can register the Java +class to be used for each possible authentication method by modifying the +following Properties file:

    +
    +    src/share/org/apache/catalina/startup/Authenticators.properties
    +
    + +

    Each of the standard implementations extends a common base class +(AuthenticatorBase), which is configured by setting the +following JavaBeans properties (with default values in square brackets):

    +
      +
    • cache - Should we cache authenticated Principals (thus avoiding + per-request lookups in our underlying Realm) if this request + is part of an HTTP session? [true]
    • +
    • debug - Debugging detail level for this component. [0]
    • +
    + +

    The standard authentication methods that are currently provided include:

    +
      +
    • BasicAuthenticator - Implements HTTP BASIC authentication, as + described in RFC 2617.
    • +
    • DigestAuthenticator - Implements HTTP DIGEST authentication, as + described in RFC 2617.
    • +
    • FormAuthenticator - Implements FORM-BASED authentication, as + described in the Servlet API Specification.
    • +
    + + diff --git a/java/org/apache/catalina/connector/ClientAbortException.java b/java/org/apache/catalina/connector/ClientAbortException.java new file mode 100644 index 0000000..0ee9edc --- /dev/null +++ b/java/org/apache/catalina/connector/ClientAbortException.java @@ -0,0 +1,70 @@ +/* + * 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.catalina.connector; + +import org.apache.coyote.BadRequestException; + +/** + * Extend IOException to identify it as being caused by an abort of a request by a remote client. + * + * @author Glenn L. Nielsen + */ +public final class ClientAbortException extends BadRequestException { + + private static final long serialVersionUID = 1L; + + + // ------------------------------------------------------------ Constructors + + /** + * Construct a new ClientAbortException with no other information. + */ + public ClientAbortException() { + super(); + } + + + /** + * Construct a new ClientAbortException for the specified message. + * + * @param message Message describing this exception + */ + public ClientAbortException(String message) { + super(message); + } + + + /** + * Construct a new ClientAbortException for the specified throwable. + * + * @param throwable Throwable that caused this exception + */ + public ClientAbortException(Throwable throwable) { + super(throwable); + } + + + /** + * Construct a new ClientAbortException for the specified message and throwable. + * + * @param message Message describing this exception + * @param throwable Throwable that caused this exception + */ + public ClientAbortException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/java/org/apache/catalina/connector/Connector.java b/java/org/apache/catalina/connector/Connector.java new file mode 100644 index 0000000..fae325a --- /dev/null +++ b/java/org/apache/catalina/connector/Connector.java @@ -0,0 +1,1144 @@ +/* + * 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.catalina.connector; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; + +import javax.management.ObjectName; + +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Service; +import org.apache.catalina.core.AprStatus; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.coyote.AbstractProtocol; +import org.apache.coyote.Adapter; +import org.apache.coyote.ProtocolHandler; +import org.apache.coyote.UpgradeProtocol; +import org.apache.coyote.http11.AbstractHttp11JsseProtocol; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.CharsetUtil; +import org.apache.tomcat.util.buf.EncodedSolidusHandling; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.openssl.OpenSSLImplementation; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Implementation of a Coyote connector. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class Connector extends LifecycleMBeanBase { + + private static final Log log = LogFactory.getLog(Connector.class); + + + public static final String INTERNAL_EXECUTOR_NAME = "Internal"; + + + // ------------------------------------------------------------ Constructor + + /** + * Defaults to using HTTP/1.1 NIO implementation. + */ + public Connector() { + this("HTTP/1.1"); + } + + + public Connector(String protocol) { + configuredProtocol = protocol; + ProtocolHandler p = null; + try { + p = ProtocolHandler.create(protocol); + } catch (Exception e) { + log.error(sm.getString("coyoteConnector.protocolHandlerInstantiationFailed"), e); + } + if (p != null) { + protocolHandler = p; + protocolHandlerClassName = protocolHandler.getClass().getName(); + } else { + protocolHandler = null; + protocolHandlerClassName = protocol; + } + // Default for Connector depends on this system property + setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")); + } + + + public Connector(ProtocolHandler protocolHandler) { + protocolHandlerClassName = protocolHandler.getClass().getName(); + configuredProtocol = protocolHandlerClassName; + this.protocolHandler = protocolHandler; + // Default for Connector depends on this system property + setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")); + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The Service we are associated with (if any). + */ + protected Service service = null; + + + /** + * If this is true the '\' character will be permitted as a path delimiter. If not specified, the + * default value of false will be used. + */ + protected boolean allowBackslash = false; + + + /** + * Do we allow TRACE ? + */ + protected boolean allowTrace = false; + + + /** + * Default timeout for asynchronous requests (ms). + */ + protected long asyncTimeout = 30000; + + + /** + * The "enable DNS lookups" flag for this Connector. + */ + protected boolean enableLookups = false; + + + /** + * If this is true then a call to Response.getWriter() if no character encoding has been + * specified will result in subsequent calls to Response.getCharacterEncoding() returning + * ISO-8859-1 and the Content-Type response header will include a + * charset=ISO-8859-1 component. (SRV.15.2.22.1) If not specified, the default specification compliant + * value of true will be used. + */ + protected boolean enforceEncodingInGetWriter = true; + + + /** + * Is generation of X-Powered-By response header enabled/disabled? + */ + protected boolean xpoweredBy = false; + + + /** + * The server name to which we should pretend requests to this Connector were directed. This is useful when + * operating Tomcat behind a proxy server, so that redirects get constructed accurately. If not specified, the + * server name included in the Host header is used. + */ + protected String proxyName = null; + + + /** + * The server port to which we should pretend requests to this Connector were directed. This is useful when + * operating Tomcat behind a proxy server, so that redirects get constructed accurately. If not specified, the port + * number specified by the port property is used. + */ + protected int proxyPort = 0; + + + /** + * The flag that controls recycling of the facades of the request processing objects. If set to true + * the object facades will be discarded when the request is recycled. If the security manager is enabled, this + * setting is ignored and object facades are always discarded. + */ + protected boolean discardFacades = true; + + + /** + * The redirect port for non-SSL to SSL redirects. + */ + protected int redirectPort = 443; + + + /** + * The request scheme that will be set on all requests received through this connector. + */ + protected String scheme = "http"; + + + /** + * The secure connection flag that will be set on all requests received through this connector. + */ + protected boolean secure = false; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Connector.class); + + + /** + * The maximum number of cookies permitted for a request. Use a value less than zero for no limit. Defaults to 200. + */ + private int maxCookieCount = 200; + + /** + * The maximum number of parameters (GET plus POST) which will be automatically parsed by the container. 10000 by + * default. The default Tomcat server.xml configures a lower default of 1000. A value of less than 0 means no limit. + */ + protected int maxParameterCount = 10000; + + /** + * Maximum size of a POST which will be automatically parsed by the container. 2 MiB by default. + */ + protected int maxPostSize = 2 * 1024 * 1024; + + + /** + * Maximum size of a POST which will be saved by the container during authentication. 4 KiB by default + */ + protected int maxSavePostSize = 4 * 1024; + + /** + * Comma-separated list of HTTP methods that will be parsed according to POST-style rules for + * application/x-www-form-urlencoded request bodies. + */ + protected String parseBodyMethods = "POST"; + + /** + * A Set of methods determined by {@link #parseBodyMethods}. + */ + protected HashSet parseBodyMethodsSet; + + + /** + * Flag to use IP-based virtual hosting. + */ + protected boolean useIPVHosts = false; + + + /** + * Coyote Protocol handler class name. See {@link #Connector()} for current default. + */ + protected final String protocolHandlerClassName; + + + /** + * Name of the protocol that was configured. + */ + protected final String configuredProtocol; + + + /** + * Coyote protocol handler. + */ + protected final ProtocolHandler protocolHandler; + + + /** + * Coyote adapter. + */ + protected Adapter adapter = null; + + + /** + * The URI encoding in use. + */ + private Charset uriCharset = StandardCharsets.UTF_8; + + + /** + * The behavior when an encoded solidus (slash) is submitted. + */ + private EncodedSolidusHandling encodedSolidusHandling = EncodedSolidusHandling.REJECT; + + + /** + * URI encoding as body. + */ + protected boolean useBodyEncodingForURI = false; + + + private boolean rejectSuspiciousURIs; + + + // ------------------------------------------------------------- Properties + + /** + * Return a property from the protocol handler. + * + * @param name the property name + * + * @return the property value + */ + public Object getProperty(String name) { + if (protocolHandler == null) { + return null; + } + return IntrospectionUtils.getProperty(protocolHandler, name); + } + + + /** + * Set a property on the protocol handler. + * + * @param name the property name + * @param value the property value + * + * @return true if the property was successfully set + */ + public boolean setProperty(String name, String value) { + if (protocolHandler == null) { + return false; + } + return IntrospectionUtils.setProperty(protocolHandler, name, value); + } + + + /** + * @return the Service with which we are associated (if any). + */ + public Service getService() { + return this.service; + } + + + /** + * Set the Service with which we are associated (if any). + * + * @param service The service that owns this Engine + */ + public void setService(Service service) { + this.service = service; + } + + + /** + * @return true if backslash characters are allowed in URLs. Default value is false. + */ + public boolean getAllowBackslash() { + return allowBackslash; + } + + + /** + * Set the allowBackslash flag. + * + * @param allowBackslash the new flag value + */ + public void setAllowBackslash(boolean allowBackslash) { + this.allowBackslash = allowBackslash; + } + + + /** + * @return true if the TRACE method is allowed. Default value is false. + */ + public boolean getAllowTrace() { + return this.allowTrace; + } + + + /** + * Set the allowTrace flag, to disable or enable the TRACE HTTP method. + * + * @param allowTrace The new allowTrace flag + */ + public void setAllowTrace(boolean allowTrace) { + this.allowTrace = allowTrace; + } + + + /** + * @return the default timeout for async requests in ms. + */ + public long getAsyncTimeout() { + return asyncTimeout; + } + + + /** + * Set the default timeout for async requests. + * + * @param asyncTimeout The new timeout in ms. + */ + public void setAsyncTimeout(long asyncTimeout) { + this.asyncTimeout = asyncTimeout; + } + + + /** + * @return true if the object facades are discarded, either when the discardFacades value is + * true or when the security manager is enabled. + */ + public boolean getDiscardFacades() { + return discardFacades || Globals.IS_SECURITY_ENABLED; + } + + + /** + * Set the recycling strategy for the object facades. + * + * @param discardFacades the new value of the flag + */ + public void setDiscardFacades(boolean discardFacades) { + this.discardFacades = discardFacades; + } + + + /** + * @return the "enable DNS lookups" flag. + */ + public boolean getEnableLookups() { + return this.enableLookups; + } + + + /** + * Set the "enable DNS lookups" flag. + * + * @param enableLookups The new "enable DNS lookups" flag value + */ + public void setEnableLookups(boolean enableLookups) { + this.enableLookups = enableLookups; + } + + + /** + * @return true if a default character encoding will be set when calling Response.getWriter() + */ + public boolean getEnforceEncodingInGetWriter() { + return enforceEncodingInGetWriter; + } + + + /** + * Set the enforceEncodingInGetWriter flag. + * + * @param enforceEncodingInGetWriter the new flag value + */ + public void setEnforceEncodingInGetWriter(boolean enforceEncodingInGetWriter) { + this.enforceEncodingInGetWriter = enforceEncodingInGetWriter; + } + + + public int getMaxCookieCount() { + return maxCookieCount; + } + + + public void setMaxCookieCount(int maxCookieCount) { + this.maxCookieCount = maxCookieCount; + } + + + /** + * @return the maximum number of parameters (GET plus POST) that will be automatically parsed by the container. A + * value of less than 0 means no limit. + */ + public int getMaxParameterCount() { + return maxParameterCount; + } + + + /** + * Set the maximum number of parameters (GET plus POST) that will be automatically parsed by the container. A value + * of less than 0 means no limit. + * + * @param maxParameterCount The new setting + */ + public void setMaxParameterCount(int maxParameterCount) { + this.maxParameterCount = maxParameterCount; + } + + + /** + * @return the maximum size of a POST which will be automatically parsed by the container. + */ + public int getMaxPostSize() { + return maxPostSize; + } + + + /** + * Set the maximum size of a POST which will be automatically parsed by the container. + * + * @param maxPostSize The new maximum size in bytes of a POST which will be automatically parsed by the container + */ + public void setMaxPostSize(int maxPostSize) { + this.maxPostSize = maxPostSize; + } + + + /** + * @return the maximum size of a POST which will be saved by the container during authentication. + */ + public int getMaxSavePostSize() { + return maxSavePostSize; + } + + + /** + * Set the maximum size of a POST which will be saved by the container during authentication. + * + * @param maxSavePostSize The new maximum size in bytes of a POST which will be saved by the container during + * authentication. + */ + public void setMaxSavePostSize(int maxSavePostSize) { + this.maxSavePostSize = maxSavePostSize; + setProperty("maxSavePostSize", String.valueOf(maxSavePostSize)); + } + + + /** + * @return the HTTP methods which will support body parameters parsing + */ + public String getParseBodyMethods() { + return this.parseBodyMethods; + } + + + /** + * Set list of HTTP methods which should allow body parameter parsing. This defaults to POST. + * + * @param methods Comma separated list of HTTP method names + */ + public void setParseBodyMethods(String methods) { + + HashSet methodSet = new HashSet<>(); + + if (null != methods) { + methodSet.addAll(Arrays.asList(StringUtils.splitCommaSeparated(methods))); + } + + if (methodSet.contains("TRACE")) { + throw new IllegalArgumentException(sm.getString("coyoteConnector.parseBodyMethodNoTrace")); + } + + this.parseBodyMethods = methods; + this.parseBodyMethodsSet = methodSet; + } + + + protected boolean isParseBodyMethod(String method) { + return parseBodyMethodsSet.contains(method); + } + + + /** + * @return the port number on which this connector is configured to listen for requests. The special value of 0 + * means select a random free port when the socket is bound. + */ + public int getPort() { + // Try shortcut that should work for nearly all uses first as it does + // not use reflection and is therefore faster. + if (protocolHandler instanceof AbstractProtocol) { + return ((AbstractProtocol) protocolHandler).getPort(); + } + // Fall back for custom protocol handlers not based on AbstractProtocol + Object port = getProperty("port"); + if (port instanceof Integer) { + return ((Integer) port).intValue(); + } + // Usually means an invalid protocol has been configured + return -1; + } + + + /** + * Set the port number on which we listen for requests. + * + * @param port The new port number + */ + public void setPort(int port) { + setProperty("port", String.valueOf(port)); + } + + + public int getPortOffset() { + // Try shortcut that should work for nearly all uses first as it does + // not use reflection and is therefore faster. + if (protocolHandler instanceof AbstractProtocol) { + return ((AbstractProtocol) protocolHandler).getPortOffset(); + } + // Fall back for custom protocol handlers not based on AbstractProtocol + Object port = getProperty("portOffset"); + if (port instanceof Integer) { + return ((Integer) port).intValue(); + } + // Usually means an invalid protocol has been configured. + return 0; + } + + + public void setPortOffset(int portOffset) { + setProperty("portOffset", String.valueOf(portOffset)); + } + + + public int getPortWithOffset() { + int port = getPort(); + // Zero is a special case and negative values are invalid + if (port > 0) { + return port + getPortOffset(); + } + return port; + } + + + /** + * @return the port number on which this connector is listening to requests. If the special value for + * {@link #getPort} of zero is used then this method will report the actual port bound. + */ + public int getLocalPort() { + return ((Integer) getProperty("localPort")).intValue(); + } + + + /** + * @return the Coyote protocol handler in use. + */ + public String getProtocol() { + return configuredProtocol; + } + + + /** + * @return the class name of the Coyote protocol handler in use. + */ + public String getProtocolHandlerClassName() { + return this.protocolHandlerClassName; + } + + + /** + * @return the protocol handler associated with the connector. + */ + public ProtocolHandler getProtocolHandler() { + return this.protocolHandler; + } + + + /** + * @return the proxy server name for this Connector. + */ + public String getProxyName() { + return this.proxyName; + } + + + /** + * Set the proxy server name for this Connector. + * + * @param proxyName The new proxy server name + */ + public void setProxyName(String proxyName) { + + if (proxyName != null && proxyName.length() > 0) { + this.proxyName = proxyName; + } else { + this.proxyName = null; + } + } + + + /** + * @return the proxy server port for this Connector. + */ + public int getProxyPort() { + return this.proxyPort; + } + + + /** + * Set the proxy server port for this Connector. + * + * @param proxyPort The new proxy server port + */ + public void setProxyPort(int proxyPort) { + this.proxyPort = proxyPort; + } + + + /** + * @return the port number to which a request should be redirected if it comes in on a non-SSL port and is subject + * to a security constraint with a transport guarantee that requires SSL. + */ + public int getRedirectPort() { + return this.redirectPort; + } + + + /** + * Set the redirect port number. + * + * @param redirectPort The redirect port number (non-SSL to SSL) + */ + public void setRedirectPort(int redirectPort) { + this.redirectPort = redirectPort; + } + + + public int getRedirectPortWithOffset() { + return getRedirectPort() + getPortOffset(); + } + + + /** + * @return the scheme that will be assigned to requests received through this connector. Default value is "http". + */ + public String getScheme() { + return this.scheme; + } + + + /** + * Set the scheme that will be assigned to requests received through this connector. + * + * @param scheme The new scheme + */ + public void setScheme(String scheme) { + this.scheme = scheme; + } + + + /** + * @return the secure connection flag that will be assigned to requests received through this connector. Default + * value is "false". + */ + public boolean getSecure() { + return this.secure; + } + + + /** + * Set the secure connection flag that will be assigned to requests received through this connector. + * + * @param secure The new secure connection flag + */ + public void setSecure(boolean secure) { + this.secure = secure; + setProperty("secure", Boolean.toString(secure)); + } + + + /** + * @return the name of character encoding to be used for the URI using the original case. + */ + public String getURIEncoding() { + return uriCharset.name(); + } + + + /** + * @return The Charset to use to convert raw URI bytes (after %nn decoding) to characters. This will never be null + */ + public Charset getURICharset() { + return uriCharset; + } + + /** + * Set the URI encoding to be used for the URI. + * + * @param URIEncoding The new URI character encoding. + */ + public void setURIEncoding(String URIEncoding) { + try { + Charset charset = B2CConverter.getCharset(URIEncoding); + if (!CharsetUtil.isAsciiSuperset(charset)) { + log.error(sm.getString("coyoteConnector.notAsciiSuperset", URIEncoding, uriCharset.name())); + return; + } + uriCharset = charset; + } catch (UnsupportedEncodingException e) { + log.error(sm.getString("coyoteConnector.invalidEncoding", URIEncoding, uriCharset.name()), e); + } + } + + + /** + * @return the true if the entity body encoding should be used for the URI. + */ + public boolean getUseBodyEncodingForURI() { + return this.useBodyEncodingForURI; + } + + + /** + * Set if the entity body encoding should be used for the URI. + * + * @param useBodyEncodingForURI The new value for the flag. + */ + public void setUseBodyEncodingForURI(boolean useBodyEncodingForURI) { + this.useBodyEncodingForURI = useBodyEncodingForURI; + } + + /** + * Indicates whether the generation of an X-Powered-By response header for Servlet-generated responses is enabled or + * disabled for this Connector. + * + * @return true if generation of X-Powered-By response header is enabled, false otherwise + */ + public boolean getXpoweredBy() { + return xpoweredBy; + } + + + /** + * Enables or disables the generation of an X-Powered-By header (with value Servlet/2.5) for all servlet-generated + * responses returned by this Connector. + * + * @param xpoweredBy true if generation of X-Powered-By response header is to be enabled, false otherwise + */ + public void setXpoweredBy(boolean xpoweredBy) { + this.xpoweredBy = xpoweredBy; + } + + + /** + * Enable the use of IP-based virtual hosting. + * + * @param useIPVHosts true if Hosts are identified by IP, false if Hosts are identified by + * name. + */ + public void setUseIPVHosts(boolean useIPVHosts) { + this.useIPVHosts = useIPVHosts; + } + + + /** + * Test if IP-based virtual hosting is enabled. + * + * @return true if IP vhosts are enabled + */ + public boolean getUseIPVHosts() { + return useIPVHosts; + } + + + public String getExecutorName() { + Object obj = protocolHandler.getExecutor(); + if (obj instanceof org.apache.catalina.Executor) { + return ((org.apache.catalina.Executor) obj).getName(); + } + return INTERNAL_EXECUTOR_NAME; + } + + + public void addSslHostConfig(SSLHostConfig sslHostConfig) { + protocolHandler.addSslHostConfig(sslHostConfig); + } + + + public SSLHostConfig[] findSslHostConfigs() { + return protocolHandler.findSslHostConfigs(); + } + + + public void addUpgradeProtocol(UpgradeProtocol upgradeProtocol) { + protocolHandler.addUpgradeProtocol(upgradeProtocol); + } + + + public UpgradeProtocol[] findUpgradeProtocols() { + return protocolHandler.findUpgradeProtocols(); + } + + + public String getEncodedSolidusHandling() { + return encodedSolidusHandling.getValue(); + } + + + public void setEncodedSolidusHandling(String encodedSolidusHandling) { + this.encodedSolidusHandling = EncodedSolidusHandling.fromString(encodedSolidusHandling); + } + + + public EncodedSolidusHandling getEncodedSolidusHandlingInternal() { + return encodedSolidusHandling; + } + + + public boolean getRejectSuspiciousURIs() { + return rejectSuspiciousURIs; + } + + + public void setRejectSuspiciousURIs(boolean rejectSuspiciousURIs) { + this.rejectSuspiciousURIs = rejectSuspiciousURIs; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Create (or allocate) and return a Request object suitable for specifying the contents of a Request to the + * responsible Container. + * + * @return a new Servlet request object + */ + public Request createRequest() { + return new Request(this); + } + + + /** + * Create (or allocate) and return a Response object suitable for receiving the contents of a Response from the + * responsible Container. + * + * @return a new Servlet response object + */ + public Response createResponse() { + int size = protocolHandler.getDesiredBufferSize(); + if (size > 0) { + return new Response(size); + } else { + return new Response(); + } + } + + + protected String createObjectNameKeyProperties(String type) { + + Object addressObj = getProperty("address"); + + StringBuilder sb = new StringBuilder("type="); + sb.append(type); + String id = (protocolHandler != null) ? protocolHandler.getId() : null; + if (id != null) { + // Maintain MBean name compatibility, even if not accurate + sb.append(",port=0,address="); + sb.append(ObjectName.quote(id)); + } else { + sb.append(",port="); + int port = getPortWithOffset(); + if (port > 0) { + sb.append(port); + } else { + sb.append("auto-"); + sb.append(getProperty("nameIndex")); + } + String address = ""; + if (addressObj instanceof InetAddress) { + address = ((InetAddress) addressObj).getHostAddress(); + } else if (addressObj != null) { + address = addressObj.toString(); + } + if (address.length() > 0) { + sb.append(",address="); + sb.append(ObjectName.quote(address)); + } + } + return sb.toString(); + } + + + /** + * Pause the connector. + */ + public void pause() { + try { + if (protocolHandler != null) { + protocolHandler.pause(); + } + } catch (Exception e) { + log.error(sm.getString("coyoteConnector.protocolHandlerPauseFailed"), e); + } + } + + + /** + * Resume the connector. + */ + public void resume() { + try { + if (protocolHandler != null) { + protocolHandler.resume(); + } + } catch (Exception e) { + log.error(sm.getString("coyoteConnector.protocolHandlerResumeFailed"), e); + } + } + + + @Override + protected void initInternal() throws LifecycleException { + + super.initInternal(); + + if (protocolHandler == null) { + throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerInstantiationFailed")); + } + + // Initialize adapter + adapter = new CoyoteAdapter(this); + protocolHandler.setAdapter(adapter); + + // Make sure parseBodyMethodsSet has a default + if (null == parseBodyMethodsSet) { + setParseBodyMethods(getParseBodyMethods()); + } + + if (AprStatus.isAprAvailable() && AprStatus.getUseOpenSSL() && + protocolHandler instanceof AbstractHttp11JsseProtocol) { + AbstractHttp11JsseProtocol jsseProtocolHandler = (AbstractHttp11JsseProtocol) protocolHandler; + if (jsseProtocolHandler.isSSLEnabled() && jsseProtocolHandler.getSslImplementationName() == null) { + // OpenSSL is compatible with the JSSE configuration, so use it if APR is available + jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName()); + } + } + + try { + protocolHandler.init(); + } catch (Exception e) { + throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e); + } + } + + + /** + * Begin processing requests via this Connector. + * + * @exception LifecycleException if a fatal startup error occurs + */ + @Override + protected void startInternal() throws LifecycleException { + + // Validate settings before starting + String id = (protocolHandler != null) ? protocolHandler.getId() : null; + if (id == null && getPortWithOffset() < 0) { + throw new LifecycleException( + sm.getString("coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset()))); + } + + setState(LifecycleState.STARTING); + + // Configure the utility executor before starting the protocol handler + if (protocolHandler != null && service != null) { + protocolHandler.setUtilityExecutor(service.getServer().getUtilityExecutor()); + } + + try { + protocolHandler.start(); + } catch (Exception e) { + // Includes NPE - protocolHandler will be null for invalid protocol if throwOnFailure is false + throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerStartFailed"), e); + } + } + + + /** + * Terminate processing requests via this Connector. + * + * @exception LifecycleException if a fatal shutdown error occurs + */ + @Override + protected void stopInternal() throws LifecycleException { + + setState(LifecycleState.STOPPING); + + try { + if (protocolHandler != null) { + protocolHandler.stop(); + } + } catch (Exception e) { + throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerStopFailed"), e); + } + + // Remove the utility executor once the protocol handler has been stopped + if (protocolHandler != null) { + protocolHandler.setUtilityExecutor(null); + } + } + + + @Override + protected void destroyInternal() throws LifecycleException { + try { + if (protocolHandler != null) { + protocolHandler.destroy(); + } + } catch (Exception e) { + throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerDestroyFailed"), e); + } + + if (getService() != null) { + getService().removeConnector(this); + } + + super.destroyInternal(); + } + + + /** + * Provide a useful toString() implementation as it may be used when logging Lifecycle errors to identify the + * component. + */ + @Override + public String toString() { + // Not worth caching this right now + StringBuilder sb = new StringBuilder("Connector["); + String name = (String) getProperty("name"); + if (name == null) { + sb.append(getProtocol()); + sb.append('-'); + String id = (protocolHandler != null) ? protocolHandler.getId() : null; + if (id != null) { + sb.append(id); + } else { + int port = getPortWithOffset(); + if (port > 0) { + sb.append(port); + } else { + sb.append("auto-"); + sb.append(getProperty("nameIndex")); + } + } + } else { + sb.append(name); + } + sb.append(']'); + return sb.toString(); + } + + + // -------------------- JMX registration -------------------- + + @Override + protected String getDomainInternal() { + Service s = getService(); + if (s == null) { + return null; + } else { + return service.getDomain(); + } + } + + @Override + protected String getObjectNameKeyProperties() { + return createObjectNameKeyProperties("Connector"); + } + +} diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java b/java/org/apache/catalina/connector/CoyoteAdapter.java new file mode 100644 index 0000000..e0ed1b9 --- /dev/null +++ b/java/org/apache/catalina/connector/CoyoteAdapter.java @@ -0,0 +1,1315 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Authenticator; +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Wrapper; +import org.apache.catalina.authenticator.AuthenticatorBase; +import org.apache.catalina.core.AsyncContextImpl; +import org.apache.catalina.util.ServerInfo; +import org.apache.catalina.util.SessionConfig; +import org.apache.catalina.util.URLEncoder; +import org.apache.coyote.ActionCode; +import org.apache.coyote.Adapter; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.ServerCookie; +import org.apache.tomcat.util.http.ServerCookies; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Implementation of a request processor which delegates the processing to a Coyote processor. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class CoyoteAdapter implements Adapter { + + private static final Log log = LogFactory.getLog(CoyoteAdapter.class); + + // -------------------------------------------------------------- Constants + + private static final String POWERED_BY = "Servlet/6.0 JSP/3.1 " + "(" + ServerInfo.getServerInfo() + " Java/" + + System.getProperty("java.vm.vendor") + "/" + System.getProperty("java.runtime.version") + ")"; + + private static final EnumSet SSL_ONLY = EnumSet.of(SessionTrackingMode.SSL); + + public static final int ADAPTER_NOTES = 1; + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new CoyoteProcessor associated with the specified connector. + * + * @param connector CoyoteConnector that owns this processor + */ + public CoyoteAdapter(Connector connector) { + + super(); + this.connector = connector; + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The CoyoteConnector with which this processor is associated. + */ + private final Connector connector; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(CoyoteAdapter.class); + + + // -------------------------------------------------------- Adapter Methods + + @Override + public boolean asyncDispatch(org.apache.coyote.Request req, org.apache.coyote.Response res, SocketEvent status) + throws Exception { + + Request request = (Request) req.getNote(ADAPTER_NOTES); + Response response = (Response) res.getNote(ADAPTER_NOTES); + + if (request == null) { + throw new IllegalStateException(sm.getString("coyoteAdapter.nullRequest")); + } + + boolean success = true; + AsyncContextImpl asyncConImpl = request.getAsyncContextInternal(); + + req.setRequestThread(); + + try { + if (!request.isAsync()) { + // Error or timeout + // Lift any suspension (e.g. if sendError() was used by an async + // request) to allow the response to be written to the client + response.setSuspended(false); + } + + if (status == SocketEvent.TIMEOUT) { + if (!asyncConImpl.timeout()) { + asyncConImpl.setErrorState(null, false); + } + } else if (status == SocketEvent.ERROR) { + // An I/O error occurred on a non-container thread which means + // that the socket needs to be closed so set success to false to + // trigger a close + success = false; + Throwable t = (Throwable) req.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + Context context = request.getContext(); + ClassLoader oldCL = null; + try { + oldCL = context.bind(false, null); + if (req.getReadListener() != null) { + req.getReadListener().onError(t); + } + if (res.getWriteListener() != null) { + res.getWriteListener().onError(t); + } + res.action(ActionCode.CLOSE_NOW, t); + asyncConImpl.setErrorState(t, true); + } finally { + context.unbind(false, oldCL); + } + } + + // Check to see if non-blocking writes or reads are being used + if (!request.isAsyncDispatching() && request.isAsync()) { + WriteListener writeListener = res.getWriteListener(); + ReadListener readListener = req.getReadListener(); + if (writeListener != null && status == SocketEvent.OPEN_WRITE) { + Context context = request.getContext(); + ClassLoader oldCL = null; + try { + oldCL = context.bind(false, null); + res.onWritePossible(); + if (request.isFinished() && req.sendAllDataReadEvent() && readListener != null) { + readListener.onAllDataRead(); + } + // User code may have swallowed an IOException + if (response.getCoyoteResponse().isExceptionPresent()) { + throw response.getCoyoteResponse().getErrorException(); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // Allow the error handling to write to the response + response.setSuspended(false); + // Need to trigger the call to AbstractProcessor.setErrorState() + // before the listener is called so the listener can call complete + // Therefore no need to set success=false as that would trigger a + // second call to AbstractProcessor.setErrorState() + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65001 + writeListener.onError(t); + res.action(ActionCode.CLOSE_NOW, t); + asyncConImpl.setErrorState(t, true); + } finally { + context.unbind(false, oldCL); + } + } else if (readListener != null && status == SocketEvent.OPEN_READ) { + Context context = request.getContext(); + ClassLoader oldCL = null; + try { + oldCL = context.bind(false, null); + // If data is being read on a non-container thread a + // dispatch with status OPEN_READ will be used to get + // execution back on a container thread for the + // onAllDataRead() event. Therefore, make sure + // onDataAvailable() is not called in this case. + if (!request.isFinished()) { + req.onDataAvailable(); + } + if (request.isFinished() && req.sendAllDataReadEvent()) { + readListener.onAllDataRead(); + } + // User code may have swallowed an IOException + if (request.getCoyoteRequest().isExceptionPresent()) { + throw request.getCoyoteRequest().getErrorException(); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // Allow the error handling to write to the response + response.setSuspended(false); + // Need to trigger the call to AbstractProcessor.setErrorState() + // before the listener is called so the listener can call complete + // Therefore no need to set success=false as that would trigger a + // second call to AbstractProcessor.setErrorState() + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65001 + readListener.onError(t); + res.action(ActionCode.CLOSE_NOW, t); + asyncConImpl.setErrorState(t, true); + } finally { + context.unbind(false, oldCL); + } + } + } + + // Has an error occurred during async processing that needs to be + // processed by the application's error page mechanism (or Tomcat's + // if the application doesn't define one)? + if (!request.isAsyncDispatching() && request.isAsync() && response.isErrorReportRequired()) { + connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); + } + + if (request.isAsyncDispatching()) { + connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); + if (response.isError()) { + Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + asyncConImpl.setErrorState(t, true); + } + } + + if (!request.isAsync()) { + request.finishRequest(); + response.finishResponse(); + } + + // Check to see if the processor is in an error state. If it is, + // bail out now. + AtomicBoolean error = new AtomicBoolean(false); + res.action(ActionCode.IS_ERROR, error); + if (error.get()) { + if (request.isAsyncCompleting() || request.isAsyncDispatching()) { + // Connection will be forcibly closed which will prevent completion/dispatch happening at the usual + // point. Trigger post processing here. + res.action(ActionCode.ASYNC_POST_PROCESS, null); + } + success = false; + } + } catch (IOException e) { + success = false; + // Ignore + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + success = false; + log.error(sm.getString("coyoteAdapter.asyncDispatch"), t); + } finally { + if (!success) { + res.setStatus(500); + } + + // Access logging + if (!success || !request.isAsync()) { + long time = 0; + if (req.getStartTimeNanos() != -1) { + time = System.nanoTime() - req.getStartTimeNanos(); + } + Context context = request.getContext(); + if (context != null) { + context.logAccess(request, response, time, false); + } else { + log(req, res, time); + } + } + + req.getRequestProcessor().setWorkerThreadName(null); + req.clearRequestThread(); + // Recycle the wrapper request and response + if (!success || !request.isAsync()) { + updateWrapperErrorCount(request, response); + request.recycle(); + response.recycle(); + } + } + return success; + } + + + @Override + public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { + + Request request = (Request) req.getNote(ADAPTER_NOTES); + Response response = (Response) res.getNote(ADAPTER_NOTES); + + if (request == null) { + // Create objects + request = connector.createRequest(); + request.setCoyoteRequest(req); + response = connector.createResponse(); + response.setCoyoteResponse(res); + + // Link objects + request.setResponse(response); + response.setRequest(request); + + // Set as notes + req.setNote(ADAPTER_NOTES, request); + res.setNote(ADAPTER_NOTES, response); + + // Set query string encoding + req.getParameters().setQueryStringCharset(connector.getURICharset()); + } + + if (connector.getXpoweredBy()) { + response.addHeader("X-Powered-By", POWERED_BY); + } + + boolean async = false; + boolean postParseSuccess = false; + + req.setRequestThread(); + + try { + // Parse and set Catalina and configuration specific + // request parameters + postParseSuccess = postParseRequest(req, request, res, response); + if (postParseSuccess) { + // check valves if we support async + request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported()); + // Calling the container + connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); + } + if (request.isAsync()) { + async = true; + ReadListener readListener = req.getReadListener(); + if (readListener != null && request.isFinished()) { + // Possible the all data may have been read during service() + // method so this needs to be checked here + ClassLoader oldCL = null; + try { + oldCL = request.getContext().bind(false, null); + if (req.sendAllDataReadEvent()) { + req.getReadListener().onAllDataRead(); + } + } finally { + request.getContext().unbind(false, oldCL); + } + } + + Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + + // If an async request was started, is not going to end once + // this container thread finishes and an error occurred, trigger + // the async error process + if (!request.isAsyncCompleting() && throwable != null) { + request.getAsyncContextInternal().setErrorState(throwable, true); + } + } else { + request.finishRequest(); + response.finishResponse(); + } + + } catch (IOException e) { + // Ignore + } finally { + AtomicBoolean error = new AtomicBoolean(false); + res.action(ActionCode.IS_ERROR, error); + + if (request.isAsyncCompleting() && error.get()) { + // Connection will be forcibly closed which will prevent + // completion happening at the usual point. Need to trigger + // call to onComplete() here. + res.action(ActionCode.ASYNC_POST_PROCESS, null); + async = false; + } + + // Access log + if (!async && postParseSuccess) { + // Log only if processing was invoked. + // If postParseRequest() failed, it has already logged it. + Context context = request.getContext(); + Host host = request.getHost(); + // If the context is null, it is likely that the endpoint was + // shutdown, this connection closed and the request recycled in + // a different thread. That thread will have updated the access + // log so it is OK not to update the access log here in that + // case. + // The other possibility is that an error occurred early in + // processing and the request could not be mapped to a Context. + // Log via the host or engine in that case. + long time = System.nanoTime() - req.getStartTimeNanos(); + if (context != null) { + context.logAccess(request, response, time, false); + } else if (response.isError()) { + if (host != null) { + host.logAccess(request, response, time, false); + } else { + connector.getService().getContainer().logAccess(request, response, time, false); + } + } + } + + req.getRequestProcessor().setWorkerThreadName(null); + req.clearRequestThread(); + + // Recycle the wrapper request and response + if (!async) { + updateWrapperErrorCount(request, response); + request.recycle(); + response.recycle(); + } + } + } + + + private void updateWrapperErrorCount(Request request, Response response) { + if (response.isError()) { + Wrapper wrapper = request.getWrapper(); + if (wrapper != null) { + wrapper.incrementErrorCount(); + } + } + } + + + @Override + public boolean prepare(org.apache.coyote.Request req, org.apache.coyote.Response res) + throws IOException, ServletException { + Request request = (Request) req.getNote(ADAPTER_NOTES); + Response response = (Response) res.getNote(ADAPTER_NOTES); + + return postParseRequest(req, request, res, response); + } + + + @Override + public void log(org.apache.coyote.Request req, org.apache.coyote.Response res, long time) { + + Request request = (Request) req.getNote(ADAPTER_NOTES); + Response response = (Response) res.getNote(ADAPTER_NOTES); + + if (request == null) { + // Create objects + request = connector.createRequest(); + request.setCoyoteRequest(req); + response = connector.createResponse(); + response.setCoyoteResponse(res); + + // Link objects + request.setResponse(response); + response.setRequest(request); + + // Set as notes + req.setNote(ADAPTER_NOTES, request); + res.setNote(ADAPTER_NOTES, response); + + // Set query string encoding + req.getParameters().setQueryStringCharset(connector.getURICharset()); + } + + try { + // Log at the lowest level available. logAccess() will be + // automatically called on parent containers. + boolean logged = false; + Context context = request.mappingData.context; + Host host = request.mappingData.host; + if (context != null) { + logged = true; + context.logAccess(request, response, time, true); + } else if (host != null) { + logged = true; + host.logAccess(request, response, time, true); + } + if (!logged) { + connector.getService().getContainer().logAccess(request, response, time, true); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("coyoteAdapter.accesslogFail"), t); + } finally { + updateWrapperErrorCount(request, response); + request.recycle(); + response.recycle(); + } + } + + + private static class RecycleRequiredException extends Exception { + private static final long serialVersionUID = 1L; + } + + @Override + public void checkRecycled(org.apache.coyote.Request req, org.apache.coyote.Response res) { + Request request = (Request) req.getNote(ADAPTER_NOTES); + Response response = (Response) res.getNote(ADAPTER_NOTES); + String messageKey = null; + if (request != null && request.getHost() != null) { + messageKey = "coyoteAdapter.checkRecycled.request"; + } else if (response != null && response.getContentWritten() != 0) { + messageKey = "coyoteAdapter.checkRecycled.response"; + } + if (messageKey != null) { + // Log this request, as it has probably skipped the access log. + // The log() method will take care of recycling. + log(req, res, 0L); + + if (connector.getState().isAvailable()) { + if (log.isInfoEnabled()) { + log.info(sm.getString(messageKey), new RecycleRequiredException()); + } + } else { + // There may be some aborted requests. + // When connector shuts down, the request and response will not + // be reused, so there is no issue to warn about here. + if (log.isDebugEnabled()) { + log.debug(sm.getString(messageKey), new RecycleRequiredException()); + } + } + } + } + + + @Override + public String getDomain() { + return connector.getDomain(); + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Perform the necessary processing after the HTTP headers have been parsed to enable the request/response pair to + * be passed to the start of the container pipeline for processing. + * + * @param req The coyote request object + * @param request The catalina request object + * @param res The coyote response object + * @param response The catalina response object + * + * @return true if the request should be passed on to the start of the container pipeline, otherwise + * false + * + * @throws IOException If there is insufficient space in a buffer while processing headers + * @throws ServletException If the supported methods of the target servlet cannot be determined + */ + @SuppressWarnings("deprecation") + protected boolean postParseRequest(org.apache.coyote.Request req, Request request, org.apache.coyote.Response res, + Response response) throws IOException, ServletException { + + // If the processor has set the scheme (AJP does this, HTTP does this if + // SSL is enabled) use this to set the secure flag as well. If the + // processor hasn't set it, use the settings from the connector + if (req.scheme().isNull()) { + // Use connector scheme and secure configuration, (defaults to + // "http" and false respectively) + req.scheme().setString(connector.getScheme()); + request.setSecure(connector.getSecure()); + } else { + // Use processor specified scheme to determine secure state + request.setSecure(req.scheme().equals("https")); + } + + // At this point the Host header has been processed. + // Override if the proxyPort/proxyHost are set + String proxyName = connector.getProxyName(); + int proxyPort = connector.getProxyPort(); + if (proxyPort != 0) { + req.setServerPort(proxyPort); + } else if (req.getServerPort() == -1) { + // Not explicitly set. Use default ports based on the scheme + if (req.scheme().equals("https")) { + req.setServerPort(443); + } else { + req.setServerPort(80); + } + } + if (proxyName != null) { + req.serverName().setString(proxyName); + } + + MessageBytes undecodedURI = req.requestURI(); + + // Check for ping OPTIONS * request + if (undecodedURI.equals("*")) { + if (req.method().equals("OPTIONS")) { + StringBuilder allow = new StringBuilder(); + allow.append("GET, HEAD, POST, PUT, DELETE, OPTIONS"); + // Trace if allowed + if (connector.getAllowTrace()) { + allow.append(", TRACE"); + } + res.setHeader("Allow", allow.toString()); + // Access log entry as processing won't reach AccessLogValve + connector.getService().getContainer().logAccess(request, response, 0, true); + return false; + } else { + response.sendError(400, sm.getString("coyoteAdapter.invalidURI")); + } + } + + MessageBytes decodedURI = req.decodedURI(); + + // Filter CONNECT method + if (req.method().equals("CONNECT")) { + response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, sm.getString("coyoteAdapter.connect")); + } else { + // No URI for CONNECT requests + if (undecodedURI.getType() == MessageBytes.T_BYTES) { + if (connector.getRejectSuspiciousURIs()) { + if (checkSuspiciousURIs(undecodedURI.getByteChunk())) { + response.sendError(400, sm.getString("coyoteAdapter.invalidURI")); + } + } + + // Copy the raw URI to the decodedURI + decodedURI.duplicate(undecodedURI); + + // Parse (and strip out) the path parameters + parsePathParameters(req, request); + + // URI decoding + // %xx decoding of the URL + try { + req.getURLDecoder().convert(decodedURI.getByteChunk(), + connector.getEncodedSolidusHandlingInternal()); + } catch (IOException ioe) { + response.sendError(400, sm.getString("coyoteAdapter.invalidURIWithMessage", ioe.getMessage())); + } + // Normalization + if (normalize(req.decodedURI(), connector.getAllowBackslash())) { + // Character decoding + convertURI(decodedURI, request); + // URIEncoding values are limited to US-ASCII supersets. + // Therefore it is not necessary to check that the URI remains + // normalized after character decoding + } else { + response.sendError(400, sm.getString("coyoteAdapter.invalidURI")); + } + } else { + /* + * The URI is chars or String, and has been sent using an in-memory protocol handler. The following + * assumptions are made: - req.requestURI() has been set to the 'original' non-decoded, non-normalized + * URI - req.decodedURI() has been set to the decoded, normalized form of req.requestURI() - + * 'suspicious' URI filtering - if required - has already been performed + */ + decodedURI.toChars(); + // Remove all path parameters; any needed path parameter should be set + // using the request object rather than passing it in the URL + CharChunk uriCC = decodedURI.getCharChunk(); + int semicolon = uriCC.indexOf(';'); + if (semicolon > 0) { + decodedURI.setChars(uriCC.getBuffer(), uriCC.getStart(), semicolon); + } + } + } + + // Request mapping. + MessageBytes serverName; + if (connector.getUseIPVHosts()) { + serverName = req.localName(); + if (serverName.isNull()) { + // well, they did ask for it + res.action(ActionCode.REQ_LOCAL_NAME_ATTRIBUTE, null); + } + } else { + serverName = req.serverName(); + } + + // Version for the second mapping loop and + // Context that we expect to get for that version + String version = null; + Context versionContext = null; + boolean mapRequired = true; + + if (response.isError()) { + // An error this early means the URI is invalid. Ensure invalid data + // is not passed to the mapper. Note we still want the mapper to + // find the correct host. + decodedURI.recycle(); + } + + while (mapRequired) { + // This will map the the latest version by default + connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData()); + + // If there is no context at this point, either this is a 404 + // because no ROOT context has been deployed or the URI was invalid + // so no context could be mapped. + if (request.getContext() == null) { + // Allow processing to continue. + // If present, the rewrite Valve may rewrite this to a valid + // request. + // The StandardEngineValve will handle the case of a missing + // Host and the StandardHostValve the case of a missing Context. + // If present, the error reporting valve will provide a response + // body. + return true; + } + + // Now we have the context, we can parse the session ID from the URL + // (if any). Need to do this before we redirect in case we need to + // include the session id in the redirect + String sessionID; + if (request.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)) { + + // Get the session ID if there was one + sessionID = request.getPathParameter(SessionConfig.getSessionUriParamName(request.getContext())); + if (sessionID != null) { + request.setRequestedSessionId(sessionID); + request.setRequestedSessionURL(true); + } + } + + // Look for session ID in cookies and SSL session + try { + parseSessionCookiesId(request); + } catch (IllegalArgumentException e) { + // Too many cookies + if (!response.isError()) { + response.setError(); + response.sendError(400, e.getMessage()); + } + return true; + } + parseSessionSslId(request); + + sessionID = request.getRequestedSessionId(); + + mapRequired = false; + if (version != null && request.getContext() == versionContext) { + // We got the version that we asked for. That is it. + } else { + version = null; + versionContext = null; + + Context[] contexts = request.getMappingData().contexts; + // Single contextVersion means no need to remap + // No session ID means no possibility of remap + if (contexts != null && sessionID != null) { + // Find the context associated with the session + for (int i = contexts.length; i > 0; i--) { + Context ctxt = contexts[i - 1]; + if (ctxt.getManager().findSession(sessionID) != null) { + // We found a context. Is it the one that has + // already been mapped? + if (!ctxt.equals(request.getMappingData().context)) { + // Set version so second time through mapping + // the correct context is found + version = ctxt.getWebappVersion(); + versionContext = ctxt; + // Reset mapping + request.getMappingData().recycle(); + mapRequired = true; + // Recycle cookies and session info in case the + // correct context is configured with different + // settings + request.recycleSessionInfo(); + request.recycleCookieInfo(true); + } + break; + } + } + } + } + + if (!mapRequired && request.getContext().getPaused()) { + // Found a matching context but it is paused. Mapping data will + // be wrong since some Wrappers may not be registered at this + // point. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Should never happen + } + // Reset mapping + request.getMappingData().recycle(); + mapRequired = true; + } + } + + // Possible redirect + MessageBytes redirectPathMB = request.getMappingData().redirectPath; + if (!redirectPathMB.isNull()) { + String redirectPath = URLEncoder.DEFAULT.encode(redirectPathMB.toString(), StandardCharsets.UTF_8); + String query = request.getQueryString(); + if (request.isRequestedSessionIdFromURL()) { + // This is not optimal, but as this is not very common, it + // shouldn't matter + redirectPath = redirectPath + ";" + SessionConfig.getSessionUriParamName(request.getContext()) + "=" + + request.getRequestedSessionId(); + } + if (query != null) { + // This is not optimal, but as this is not very common, it + // shouldn't matter + redirectPath = redirectPath + "?" + query; + } + response.sendRedirect(redirectPath); + request.getContext().logAccess(request, response, 0, true); + return false; + } + + // Filter TRACE method + if (!connector.getAllowTrace() && req.method().equals("TRACE")) { + Wrapper wrapper = request.getWrapper(); + String header = null; + if (wrapper != null) { + String[] methods = wrapper.getServletMethods(); + if (methods != null) { + for (String method : methods) { + if ("TRACE".equals(method)) { + continue; + } + if (header == null) { + header = method; + } else { + header += ", " + method; + } + } + } + } + if (header != null) { + res.addHeader("Allow", header); + } + response.sendError(405, sm.getString("coyoteAdapter.trace")); + // Safe to skip the remainder of this method. + return true; + } + + doConnectorAuthenticationAuthorization(req, request); + + return true; + } + + + private void doConnectorAuthenticationAuthorization(org.apache.coyote.Request req, Request request) { + // Set the remote principal + String username = req.getRemoteUser().toString(); + if (username != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("coyoteAdapter.authenticate", username)); + } + if (req.getRemoteUserNeedsAuthorization()) { + Authenticator authenticator = request.getContext().getAuthenticator(); + if (!(authenticator instanceof AuthenticatorBase)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("coyoteAdapter.authorize", username)); + } + // Custom authenticator that may not trigger authorization. + // Do the authorization here to make sure it is done. + request.setUserPrincipal(request.getContext().getRealm().authenticate(username)); + } + // If the Authenticator is an instance of AuthenticatorBase then + // it will check req.getRemoteUserNeedsAuthorization() and + // trigger authorization as necessary. It will also cache the + // result preventing excessive calls to the Realm. + } else { + // The connector isn't configured for authorization. Create a + // user without any roles using the supplied user name. + request.setUserPrincipal(new CoyotePrincipal(username)); + } + } + + // Set the authorization type + String authType = req.getAuthType().toString(); + if (authType != null) { + request.setAuthType(authType); + } + } + + + /** + * Extract the path parameters from the request. This assumes parameters are of the form + * /path;name=value;name2=value2/ etc. Currently only really interested in the session ID that will be in this form. + * Other parameters can safely be ignored. + * + * @param req The Coyote request object + * @param request The Servlet request object + */ + protected void parsePathParameters(org.apache.coyote.Request req, Request request) { + + // Process in bytes (this is default format so this is normally a NO-OP + req.decodedURI().toBytes(); + + ByteChunk uriBC = req.decodedURI().getByteChunk(); + // The first character must always be '/' so start search at position 1. + // If the first character is ';' the URI will be rejected at the + // normalization stage + int semicolon = uriBC.indexOf(';', 1); + // Performance optimisation. Return as soon as it is known there are no + // path parameters; + if (semicolon == -1) { + return; + } + + // What encoding to use? Some platforms, eg z/os, use a default + // encoding that doesn't give the expected result so be explicit + Charset charset = connector.getURICharset(); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("coyoteAdapter.debug", "uriBC", uriBC.toString())); + log.trace(sm.getString("coyoteAdapter.debug", "semicolon", String.valueOf(semicolon))); + log.trace(sm.getString("coyoteAdapter.debug", "enc", charset.name())); + } + + while (semicolon > -1) { + // Parse path param, and extract it from the decoded request URI + int start = uriBC.getStart(); + int end = uriBC.getEnd(); + + int pathParamStart = semicolon + 1; + int pathParamEnd = + ByteChunk.findBytes(uriBC.getBuffer(), start + pathParamStart, end, new byte[] { ';', '/' }); + + String pv = null; + + if (pathParamEnd >= 0) { + if (charset != null) { + pv = new String(uriBC.getBuffer(), start + pathParamStart, pathParamEnd - pathParamStart, charset); + } + // Extract path param from decoded request URI + byte[] buf = uriBC.getBuffer(); + for (int i = 0; i < end - start - pathParamEnd; i++) { + buf[start + semicolon + i] = buf[start + i + pathParamEnd]; + } + uriBC.setBytes(buf, start, end - start - pathParamEnd + semicolon); + } else { + if (charset != null) { + pv = new String(uriBC.getBuffer(), start + pathParamStart, (end - start) - pathParamStart, charset); + } + uriBC.setEnd(start + semicolon); + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("coyoteAdapter.debug", "pathParamStart", String.valueOf(pathParamStart))); + log.trace(sm.getString("coyoteAdapter.debug", "pathParamEnd", String.valueOf(pathParamEnd))); + log.trace(sm.getString("coyoteAdapter.debug", "pv", pv)); + } + + if (pv != null) { + int equals = pv.indexOf('='); + if (equals > -1) { + String name = pv.substring(0, equals); + String value = pv.substring(equals + 1); + request.addPathParameter(name, value); + if (log.isTraceEnabled()) { + log.trace(sm.getString("coyoteAdapter.debug", "equals", String.valueOf(equals))); + log.trace(sm.getString("coyoteAdapter.debug", "name", name)); + log.trace(sm.getString("coyoteAdapter.debug", "value", value)); + } + } + } + + semicolon = uriBC.indexOf(';', semicolon); + } + } + + + /** + * Look for SSL session ID if required. Only look for SSL Session ID if it is the only tracking method enabled. + * + * @param request The Servlet request object + */ + protected void parseSessionSslId(Request request) { + if (request.getRequestedSessionId() == null && + SSL_ONLY.equals(request.getServletContext().getEffectiveSessionTrackingModes()) && + request.connector.secure) { + String sessionId = (String) request.getAttribute(SSLSupport.SESSION_ID_KEY); + if (sessionId != null) { + request.setRequestedSessionId(sessionId); + request.setRequestedSessionSSL(true); + } + } + } + + + /** + * Parse session id in Cookie. + * + * @param request The Servlet request object + */ + protected void parseSessionCookiesId(Request request) { + + // If session tracking via cookies has been disabled for the current + // context, don't go looking for a session ID in a cookie as a cookie + // from a parent context with a session ID may be present which would + // overwrite the valid session ID encoded in the URL + Context context = request.getMappingData().context; + if (context != null && + !context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE)) { + return; + } + + // Parse session id from cookies + ServerCookies serverCookies = request.getServerCookies(); + int count = serverCookies.getCookieCount(); + if (count <= 0) { + return; + } + + String sessionCookieName = SessionConfig.getSessionCookieName(context); + + for (int i = 0; i < count; i++) { + ServerCookie scookie = serverCookies.getCookie(i); + if (scookie.getName().equals(sessionCookieName)) { + // Override anything requested in the URL + if (!request.isRequestedSessionIdFromCookie()) { + // Accept only the first session id cookie + convertMB(scookie.getValue()); + request.setRequestedSessionId(scookie.getValue().toString()); + request.setRequestedSessionCookie(true); + request.setRequestedSessionURL(false); + if (log.isTraceEnabled()) { + log.trace(" Requested cookie session id is " + request.getRequestedSessionId()); + } + } else { + if (!request.isRequestedSessionIdValid()) { + // Replace the session id until one is valid + convertMB(scookie.getValue()); + request.setRequestedSessionId(scookie.getValue().toString()); + } + } + } + } + + } + + + /** + * Character conversion of the URI. + * + * @param uri MessageBytes object containing the URI + * @param request The Servlet request object + * + * @throws IOException if a IO exception occurs sending an error to the client + */ + protected void convertURI(MessageBytes uri, Request request) throws IOException { + + ByteChunk bc = uri.getByteChunk(); + int length = bc.getLength(); + CharChunk cc = uri.getCharChunk(); + cc.allocate(length, -1); + + Charset charset = connector.getURICharset(); + + B2CConverter conv = request.getURIConverter(); + if (conv == null) { + conv = new B2CConverter(charset, false); + request.setURIConverter(conv); + } else { + conv.recycle(); + } + + try { + conv.convert(bc, cc, true); + uri.setChars(cc.getBuffer(), cc.getStart(), cc.getLength()); + } catch (IOException ioe) { + // Should never happen as B2CConverter should replace + // problematic characters + request.getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST); + } + } + + + /** + * Character conversion of the a US-ASCII MessageBytes. + * + * @param mb The MessageBytes instance containing the bytes that should be converted to chars + */ + protected void convertMB(MessageBytes mb) { + + // This is of course only meaningful for bytes + if (mb.getType() != MessageBytes.T_BYTES) { + return; + } + + ByteChunk bc = mb.getByteChunk(); + CharChunk cc = mb.getCharChunk(); + int length = bc.getLength(); + cc.allocate(length, -1); + + // Default encoding: fast conversion + byte[] bbuf = bc.getBuffer(); + char[] cbuf = cc.getBuffer(); + int start = bc.getStart(); + for (int i = 0; i < length; i++) { + cbuf[i] = (char) (bbuf[i + start] & 0xff); + } + mb.setChars(cbuf, 0, length); + + } + + + /** + * This method normalizes "\", "//", "/./" and "/../". + * + * @param uriMB URI to be normalized + * @param allowBackslash true if backslash characters are allowed in URLs + * + * @return false if normalizing this URI would require going above the root, or if the URI contains a + * null byte, otherwise true + */ + public static boolean normalize(MessageBytes uriMB, boolean allowBackslash) { + + ByteChunk uriBC = uriMB.getByteChunk(); + final byte[] b = uriBC.getBytes(); + final int start = uriBC.getStart(); + int end = uriBC.getEnd(); + boolean appendedSlash = false; + + // An empty URL is not acceptable + if (start == end) { + return false; + } + + int pos = 0; + int index = 0; + + + // The URL must start with '/' (or '\' that will be replaced soon) + if (b[start] != (byte) '/' && b[start] != (byte) '\\') { + return false; + } + + // Replace '\' with '/' + // Check for null byte + for (pos = start; pos < end; pos++) { + if (b[pos] == (byte) '\\') { + if (allowBackslash) { + b[pos] = (byte) '/'; + } else { + return false; + } + } else if (b[pos] == (byte) 0) { + return false; + } + } + + // Replace "//" with "/" + for (pos = start; pos < (end - 1); pos++) { + if (b[pos] == (byte) '/') { + while ((pos + 1 < end) && (b[pos + 1] == (byte) '/')) { + copyBytes(b, pos, pos + 1, end - pos - 1); + end--; + } + } + } + + // If the URI ends with "/." or "/..", then we append an extra "/" + // Note: It is possible to extend the URI by 1 without any side effect + // as the next character is a non-significant WS. + if (((end - start) >= 2) && (b[end - 1] == (byte) '.')) { + if ((b[end - 2] == (byte) '/') || ((b[end - 2] == (byte) '.') && (b[end - 3] == (byte) '/'))) { + b[end] = (byte) '/'; + end++; + appendedSlash = true; + } + } + + uriBC.setEnd(end); + + index = 0; + + // Resolve occurrences of "/./" in the normalized path + while (true) { + index = uriBC.indexOf("/./", 0, 3, index); + if (index < 0) { + break; + } + copyBytes(b, start + index, start + index + 2, end - start - index - 2); + end = end - 2; + uriBC.setEnd(end); + } + + index = 0; + + // Resolve occurrences of "/../" in the normalized path + while (true) { + index = uriBC.indexOf("/../", 0, 4, index); + if (index < 0) { + break; + } + // Prevent from going outside our context + if (index == 0) { + return false; + } + int index2 = -1; + for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos--) { + if (b[pos] == (byte) '/') { + index2 = pos; + } + } + copyBytes(b, start + index2, start + index + 3, end - start - index - 3); + end = end + index2 - index - 3; + uriBC.setEnd(end); + index = index2; + } + + // If a slash was appended to help normalize "/." or "/.." then remove + // any trailing "/" from the result unless the result is "/". + if (appendedSlash && end > 1 && b[end - 1] == '/') { + uriBC.setEnd(end - 1); + } + + return true; + } + + + /** + * Copy an array of bytes to a different position. Used during normalization. + * + * @param b The bytes that should be copied + * @param dest Destination offset + * @param src Source offset + * @param len Length + */ + protected static void copyBytes(byte[] b, int dest, int src, int len) { + System.arraycopy(b, src, b, dest, len); + } + + + /* + * Examine URI segment by segment for 'suspicious' URIs. + */ + private static boolean checkSuspiciousURIs(ByteChunk undecodedURI) { + byte[] bytes = undecodedURI.getBytes(); + int start = undecodedURI.getStart(); + int end = undecodedURI.getEnd(); + int segmentStart = -1; + int segmentEnd = -1; + + // Find first segment + segmentStart = undecodedURI.indexOf('/', 0); + if (segmentStart > -1) { + segmentEnd = undecodedURI.indexOf('/', segmentStart + 1); + } + + while (segmentStart > -1) { + int pos = start + segmentStart + 1; + + // Empty segment other than final segment with path parameters + if (segmentEnd > 0 && bytes[pos] == ';') { + return true; + } + + // encoded dot-segments and/or dot-segments with path parameters + int dotCount = 0; + boolean encodedDot = false; + while (pos < end) { + if (bytes[pos] == '.') { + dotCount++; + pos++; + } else if (pos + 2 < end && bytes[pos] == '%' && bytes[pos + 1] == '2' && + (bytes[pos + 2] == 'e' || bytes[pos + 2] == 'E')) { + encodedDot = true; + dotCount++; + pos += 3; + } else if (bytes[pos] == ';') { + if (dotCount > 0) { + return true; + } + break; + } else if (bytes[pos] == '/') { + break; + } else { + dotCount = 0; + break; + } + } + if (dotCount > 0 && encodedDot) { + return true; + } + + // %nn encoded controls or '/' + pos = start + segmentStart + 1; + while (pos < end) { + if (pos + 2 < end && bytes[pos] == '%') { + byte b1 = bytes[pos + 1]; + byte b2 = bytes[pos + 2]; + pos += 3; + int decoded = (HexUtils.getDec(b1) << 4) + HexUtils.getDec(b2); + if (decoded < 20 || decoded == 0x7F || decoded == 0x2F) { + return true; + } + } else { + pos++; + } + } + + // Move to next segment + if (segmentEnd == -1) { + segmentStart = -1; + } else { + segmentStart = segmentEnd; + if (segmentStart > -1) { + segmentEnd = undecodedURI.indexOf('/', segmentStart + 1); + } + } + } + + return false; + } +} diff --git a/java/org/apache/catalina/connector/CoyoteInputStream.java b/java/org/apache/catalina/connector/CoyoteInputStream.java new file mode 100644 index 0000000..ffac217 --- /dev/null +++ b/java/org/apache/catalina/connector/CoyoteInputStream.java @@ -0,0 +1,305 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Objects; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; + +import org.apache.catalina.security.SecurityUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + * This class handles reading bytes. + * + * @author Remy Maucherat + */ +public class CoyoteInputStream extends ServletInputStream { + + protected static final StringManager sm = StringManager.getManager(CoyoteInputStream.class); + + + protected InputBuffer ib; + + + protected CoyoteInputStream(InputBuffer ib) { + this.ib = ib; + } + + + /** + * Clear facade. + */ + void clear() { + ib = null; + } + + + /** + * Prevent cloning the facade. + */ + @Override + protected Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + + @Override + public int read() throws IOException { + checkNonBlockingRead(); + + if (SecurityUtil.isPackageProtectionEnabled()) { + + try { + Integer result = AccessController.doPrivileged(new PrivilegedRead(ib)); + return result.intValue(); + } catch (PrivilegedActionException pae) { + Exception e = pae.getException(); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new RuntimeException(e.getMessage(), e); + } + } + } else { + return ib.readByte(); + } + } + + @Override + public int available() throws IOException { + + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + Integer result = AccessController.doPrivileged(new PrivilegedAvailable(ib)); + return result.intValue(); + } catch (PrivilegedActionException pae) { + Exception e = pae.getException(); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new RuntimeException(e.getMessage(), e); + } + } + } else { + return ib.available(); + } + } + + @Override + public int read(final byte[] b) throws IOException { + return read(b, 0, b.length); + } + + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + checkNonBlockingRead(); + + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + Integer result = AccessController.doPrivileged(new PrivilegedReadArray(ib, b, off, len)); + return result.intValue(); + } catch (PrivilegedActionException pae) { + Exception e = pae.getException(); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new RuntimeException(e.getMessage(), e); + } + } + } else { + return ib.read(b, off, len); + } + } + + + /** + * Transfers bytes from the buffer to the specified ByteBuffer. After the operation the position of the ByteBuffer + * will be returned to the one before the operation, the limit will be the position incremented by the number of the + * transferred bytes. + * + * @param b the ByteBuffer into which bytes are to be written. + * + * @return an integer specifying the actual number of bytes read, or -1 if the end of the stream is reached + * + * @throws IOException if an input or output exception has occurred + */ + public int read(final ByteBuffer b) throws IOException { + Objects.requireNonNull(b); + checkNonBlockingRead(); + + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + Integer result = AccessController.doPrivileged(new PrivilegedReadBuffer(ib, b)); + return result.intValue(); + } catch (PrivilegedActionException pae) { + Exception e = pae.getException(); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new RuntimeException(e.getMessage(), e); + } + } + } else { + return ib.read(b); + } + } + + + /** + * Close the stream Since we re-cycle, we can't allow the call to super.close() which would permanently disable us. + */ + @Override + public void close() throws IOException { + + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + AccessController.doPrivileged(new PrivilegedClose(ib)); + } catch (PrivilegedActionException pae) { + Exception e = pae.getException(); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new RuntimeException(e.getMessage(), e); + } + } + } else { + ib.close(); + } + } + + @Override + public boolean isFinished() { + return ib.isFinished(); + } + + + @Override + public boolean isReady() { + if (ib == null) { + throw new IllegalStateException(sm.getString("coyoteInputStream.null")); + } + return ib.isReady(); + } + + + @Override + public void setReadListener(ReadListener listener) { + ib.setReadListener(listener); + } + + + private void checkNonBlockingRead() { + if (!ib.isBlocking() && !ib.isReady()) { + throw new IllegalStateException(sm.getString("coyoteInputStream.nbNotready")); + } + } + + + private static class PrivilegedAvailable implements PrivilegedExceptionAction { + + private final InputBuffer inputBuffer; + + PrivilegedAvailable(InputBuffer inputBuffer) { + this.inputBuffer = inputBuffer; + } + + @Override + public Integer run() throws IOException { + return Integer.valueOf(inputBuffer.available()); + } + } + + + private static class PrivilegedClose implements PrivilegedExceptionAction { + + private final InputBuffer inputBuffer; + + PrivilegedClose(InputBuffer inputBuffer) { + this.inputBuffer = inputBuffer; + } + + @Override + public Void run() throws IOException { + inputBuffer.close(); + return null; + } + } + + + private static class PrivilegedRead implements PrivilegedExceptionAction { + + private final InputBuffer inputBuffer; + + PrivilegedRead(InputBuffer inputBuffer) { + this.inputBuffer = inputBuffer; + } + + @Override + public Integer run() throws IOException { + Integer integer = Integer.valueOf(inputBuffer.readByte()); + return integer; + } + } + + + private static class PrivilegedReadArray implements PrivilegedExceptionAction { + + private final InputBuffer inputBuffer; + private final byte[] buf; + private final int off; + private final int len; + + PrivilegedReadArray(InputBuffer inputBuffer, byte[] buf, int off, int len) { + this.inputBuffer = inputBuffer; + this.buf = buf; + this.off = off; + this.len = len; + } + + @Override + public Integer run() throws IOException { + Integer integer = Integer.valueOf(inputBuffer.read(buf, off, len)); + return integer; + } + } + + + private static class PrivilegedReadBuffer implements PrivilegedExceptionAction { + + private final InputBuffer inputBuffer; + private final ByteBuffer bb; + + PrivilegedReadBuffer(InputBuffer inputBuffer, ByteBuffer bb) { + this.inputBuffer = inputBuffer; + this.bb = bb; + } + + @Override + public Integer run() throws IOException { + Integer integer = Integer.valueOf(inputBuffer.read(bb)); + return integer; + } + } +} diff --git a/java/org/apache/catalina/connector/CoyoteOutputStream.java b/java/org/apache/catalina/connector/CoyoteOutputStream.java new file mode 100644 index 0000000..139b1e6 --- /dev/null +++ b/java/org/apache/catalina/connector/CoyoteOutputStream.java @@ -0,0 +1,197 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Coyote implementation of the servlet output stream. + * + * @author Costin Manolache + * @author Remy Maucherat + */ +public class CoyoteOutputStream extends ServletOutputStream { + + protected static final StringManager sm = StringManager.getManager(CoyoteOutputStream.class); + + + // ----------------------------------------------------- Instance Variables + + protected OutputBuffer ob; + + + // ----------------------------------------------------------- Constructors + + + protected CoyoteOutputStream(OutputBuffer ob) { + this.ob = ob; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Prevent cloning the facade. + */ + @Override + protected Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + + // -------------------------------------------------------- Package Methods + + + /** + * Clear facade. + */ + void clear() { + ob = null; + } + + + // --------------------------------------------------- OutputStream Methods + + + @Override + public void write(int i) throws IOException { + boolean nonBlocking = checkNonBlockingWrite(); + try { + ob.writeByte(i); + } catch (IOException ioe) { + ob.setErrorException(ioe); + throw ioe; + } + if (nonBlocking) { + checkRegisterForWrite(); + } + } + + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + + @Override + public void write(byte[] b, int off, int len) throws IOException { + boolean nonBlocking = checkNonBlockingWrite(); + try { + ob.write(b, off, len); + } catch (IOException ioe) { + ob.setErrorException(ioe); + throw ioe; + } + if (nonBlocking) { + checkRegisterForWrite(); + } + } + + + public void write(ByteBuffer from) throws IOException { + Objects.requireNonNull(from); + boolean nonBlocking = checkNonBlockingWrite(); + try { + ob.write(from); + } catch (IOException ioe) { + ob.setErrorException(ioe); + throw ioe; + } + if (nonBlocking) { + checkRegisterForWrite(); + } + } + + + /** + * Will send the buffer to the client. + */ + @Override + public void flush() throws IOException { + boolean nonBlocking = checkNonBlockingWrite(); + try { + ob.flush(); + } catch (IOException ioe) { + ob.setErrorException(ioe); + throw ioe; + } + if (nonBlocking) { + checkRegisterForWrite(); + } + } + + + /** + * Checks for concurrent writes which are not permitted. This object has no state information so the call chain is + * CoyoteOutputStream->OutputBuffer->CoyoteResponse. + * + * @return true if this OutputStream is currently in non-blocking mode. + */ + private boolean checkNonBlockingWrite() { + boolean nonBlocking = !ob.isBlocking(); + if (nonBlocking && !ob.isReady()) { + throw new IllegalStateException(sm.getString("coyoteOutputStream.nbNotready")); + } + return nonBlocking; + } + + + /** + * Checks to see if there is data left in the Coyote output buffers (NOT the servlet output buffer) and if so + * registers the associated socket for write so the buffers will be emptied. The container will take care of this. + * As far as the app is concerned, there is a non-blocking write in progress. It doesn't have visibility of whether + * the data is buffered in the socket buffer or the Coyote buffers. + */ + private void checkRegisterForWrite() { + ob.checkRegisterForWrite(); + } + + + @Override + public void close() throws IOException { + try { + ob.close(); + } catch (IOException ioe) { + ob.setErrorException(ioe); + throw ioe; + } + } + + @Override + public boolean isReady() { + if (ob == null) { + throw new IllegalStateException(sm.getString("coyoteOutputStream.null")); + } + return ob.isReady(); + } + + + @Override + public void setWriteListener(WriteListener listener) { + ob.setWriteListener(listener); + } +} + diff --git a/java/org/apache/catalina/connector/CoyotePrincipal.java b/java/org/apache/catalina/connector/CoyotePrincipal.java new file mode 100644 index 0000000..30b6925 --- /dev/null +++ b/java/org/apache/catalina/connector/CoyotePrincipal.java @@ -0,0 +1,68 @@ +/* + * 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.catalina.connector; + +import java.io.Serializable; +import java.security.Principal; + +/** + * Generic implementation of java.security.Principal that is used to represent principals authenticated + * at the protocol handler level. + * + * @author Remy Maucherat + */ +public class CoyotePrincipal implements Principal, Serializable { + + private static final long serialVersionUID = 1L; + + + // ----------------------------------------------------------- Constructors + + public CoyotePrincipal(String name) { + + this.name = name; + + } + + + // ------------------------------------------------------------- Properties + + + /** + * The username of the user represented by this Principal. + */ + protected final String name; + + @Override + public String getName() { + return this.name; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object, which exposes only information that should be public. + */ + @Override + public String toString() { + return "CoyotePrincipal[" + this.name + "]"; + } + + +} diff --git a/java/org/apache/catalina/connector/CoyoteReader.java b/java/org/apache/catalina/connector/CoyoteReader.java new file mode 100644 index 0000000..da6bc3a --- /dev/null +++ b/java/org/apache/catalina/connector/CoyoteReader.java @@ -0,0 +1,207 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedReader; +import java.io.IOException; + + +/** + * Coyote implementation of the buffered reader. + * + * @author Remy Maucherat + */ +public class CoyoteReader extends BufferedReader { + + + // -------------------------------------------------------------- Constants + + + private static final char[] LINE_SEP = { '\r', '\n' }; + private static final int MAX_LINE_LENGTH = 4096; + + + // ----------------------------------------------------- Instance Variables + + + protected InputBuffer ib; + + + protected char[] lineBuffer = null; + + + // ----------------------------------------------------------- Constructors + + + public CoyoteReader(InputBuffer ib) { + super(ib, 1); + this.ib = ib; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Prevent cloning the facade. + */ + @Override + protected Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + + // -------------------------------------------------------- Package Methods + + + /** + * Clear facade. + */ + void clear() { + ib = null; + } + + + // --------------------------------------------------------- Reader Methods + + + @Override + public void close() throws IOException { + ib.close(); + } + + + @Override + public int read() throws IOException { + return ib.read(); + } + + + @Override + public int read(char[] cbuf) throws IOException { + return ib.read(cbuf, 0, cbuf.length); + } + + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + return ib.read(cbuf, off, len); + } + + + @Override + public long skip(long n) throws IOException { + return ib.skip(n); + } + + + @Override + public boolean ready() throws IOException { + return ib.ready(); + } + + + @Override + public boolean markSupported() { + return true; + } + + + @Override + public void mark(int readAheadLimit) throws IOException { + ib.mark(readAheadLimit); + } + + + @Override + public void reset() throws IOException { + ib.reset(); + } + + + @Override + public String readLine() throws IOException { + + if (lineBuffer == null) { + lineBuffer = new char[MAX_LINE_LENGTH]; + } + + String result = null; + + int pos = 0; + int end = -1; + int skip = -1; + StringBuilder aggregator = null; + while (end < 0) { + mark(MAX_LINE_LENGTH); + while ((pos < MAX_LINE_LENGTH) && (end < 0)) { + int nRead = read(lineBuffer, pos, MAX_LINE_LENGTH - pos); + if (nRead < 0) { + if (pos == 0 && aggregator == null) { + return null; + } + end = pos; + skip = pos; + } + for (int i = pos; (i < (pos + nRead)) && (end < 0); i++) { + if (lineBuffer[i] == LINE_SEP[0]) { + end = i; + skip = i + 1; + char nextchar; + if (i == (pos + nRead - 1)) { + nextchar = (char) read(); + } else { + nextchar = lineBuffer[i + 1]; + } + if (nextchar == LINE_SEP[1]) { + skip++; + } + } else if (lineBuffer[i] == LINE_SEP[1]) { + end = i; + skip = i + 1; + } + } + if (nRead > 0) { + pos += nRead; + } + } + if (end < 0) { + if (aggregator == null) { + aggregator = new StringBuilder(); + } + aggregator.append(lineBuffer); + pos = 0; + } else { + reset(); + // No need to check return value. We know there are at least skip characters available. + skip(skip); + } + } + + if (aggregator == null) { + result = new String(lineBuffer, 0, end); + } else { + aggregator.append(lineBuffer, 0, end); + result = aggregator.toString(); + } + + return result; + + } + + +} diff --git a/java/org/apache/catalina/connector/CoyoteWriter.java b/java/org/apache/catalina/connector/CoyoteWriter.java new file mode 100644 index 0000000..6868156 --- /dev/null +++ b/java/org/apache/catalina/connector/CoyoteWriter.java @@ -0,0 +1,320 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Coyote implementation of the servlet writer. + * + * @author Remy Maucherat + */ +public class CoyoteWriter extends PrintWriter { + + + // -------------------------------------------------------------- Constants + + private static final char[] LINE_SEP = System.lineSeparator().toCharArray(); + + + // ----------------------------------------------------- Instance Variables + + + protected OutputBuffer ob; + protected boolean error = false; + + + // ----------------------------------------------------------- Constructors + + + public CoyoteWriter(OutputBuffer ob) { + super(ob); + this.ob = ob; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Prevent cloning the facade. + */ + @Override + protected Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + + // -------------------------------------------------------- Package Methods + + + /** + * Clear facade. + */ + void clear() { + ob = null; + } + + + /** + * Recycle. + */ + void recycle() { + error = false; + } + + + // --------------------------------------------------------- Writer Methods + + + @Override + public void flush() { + + if (error) { + return; + } + + try { + ob.flush(); + } catch (IOException e) { + setErrorException(e); + } + + } + + + @Override + public void close() { + + // We don't close the PrintWriter - super() is not called, + // so the stream can be reused. We close ob. + try { + ob.close(); + } catch (IOException ex) { + // Ignore + } + error = false; + + } + + + @Override + public boolean checkError() { + flush(); + return error; + } + + + @Override + public void write(int c) { + + if (error) { + return; + } + + try { + ob.write(c); + } catch (IOException e) { + setErrorException(e); + } + + } + + + @Override + public void write(char buf[], int off, int len) { + + if (error) { + return; + } + + try { + ob.write(buf, off, len); + } catch (IOException e) { + setErrorException(e); + } + + } + + + @Override + public void write(char buf[]) { + write(buf, 0, buf.length); + } + + + @Override + public void write(String s, int off, int len) { + + if (error) { + return; + } + + try { + ob.write(s, off, len); + } catch (IOException e) { + setErrorException(e); + } + + } + + + @Override + public void write(String s) { + write(s, 0, s.length()); + } + + + // ---------------------------------------------------- PrintWriter Methods + + + @Override + public void print(boolean b) { + if (b) { + write("true"); + } else { + write("false"); + } + } + + + @Override + public void print(char c) { + write(c); + } + + + @Override + public void print(int i) { + write(String.valueOf(i)); + } + + + @Override + public void print(long l) { + write(String.valueOf(l)); + } + + + @Override + public void print(float f) { + write(String.valueOf(f)); + } + + + @Override + public void print(double d) { + write(String.valueOf(d)); + } + + + @Override + public void print(char s[]) { + write(s); + } + + + @Override + public void print(String s) { + if (s == null) { + s = "null"; + } + write(s); + } + + + @Override + public void print(Object obj) { + write(String.valueOf(obj)); + } + + + @Override + public void println() { + write(LINE_SEP); + } + + + @Override + public void println(boolean b) { + print(b); + println(); + } + + + @Override + public void println(char c) { + print(c); + println(); + } + + + @Override + public void println(int i) { + print(i); + println(); + } + + + @Override + public void println(long l) { + print(l); + println(); + } + + + @Override + public void println(float f) { + print(f); + println(); + } + + + @Override + public void println(double d) { + print(d); + println(); + } + + + @Override + public void println(char c[]) { + print(c); + println(); + } + + + @Override + public void println(String s) { + print(s); + println(); + } + + + @Override + public void println(Object o) { + print(o); + println(); + } + + + private void setErrorException(Exception e) { + error = true; + ob.setErrorException(e); + } +} diff --git a/java/org/apache/catalina/connector/InputBuffer.java b/java/org/apache/catalina/connector/InputBuffer.java new file mode 100644 index 0000000..e22ef96 --- /dev/null +++ b/java/org/apache/catalina/connector/InputBuffer.java @@ -0,0 +1,689 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.io.Reader; +import java.net.SocketTimeoutException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.security.SecurityUtil; +import org.apache.coyote.ActionCode; +import org.apache.coyote.BadRequestException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.res.StringManager; + +/** + * The buffer used by Tomcat request. This is a derivative of the Tomcat 3.3 OutputBuffer, adapted to handle input + * instead of output. This allows complete recycling of the facade objects (the ServletInputStream and the + * BufferedReader). + * + * @author Remy Maucherat + */ +public class InputBuffer extends Reader implements ByteChunk.ByteInputChannel, ApplicationBufferHandler { + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(InputBuffer.class); + + private static final Log log = LogFactory.getLog(InputBuffer.class); + + public static final int DEFAULT_BUFFER_SIZE = 8 * 1024; + + // The buffer can be used for byte[] and char[] reading + // ( this is needed to support ServletInputStream and BufferedReader ) + public final int INITIAL_STATE = 0; + public final int CHAR_STATE = 1; + public final int BYTE_STATE = 2; + + + /** + * Encoder cache. + */ + private static final Map> encoders = new ConcurrentHashMap<>(); + + // ----------------------------------------------------- Instance Variables + + /** + * The byte buffer. + */ + private ByteBuffer bb; + + + /** + * The char buffer. + */ + private CharBuffer cb; + + + /** + * State of the output buffer. + */ + private int state = 0; + + + /** + * Flag which indicates if the input buffer is closed. + */ + private boolean closed = false; + + + /** + * Current byte to char converter. + */ + protected B2CConverter conv; + + + /** + * Associated Coyote request. + */ + private org.apache.coyote.Request coyoteRequest; + + + /** + * Buffer position. + */ + private int markPos = -1; + + + /** + * Char buffer limit. + */ + private int readLimit; + + + /** + * Buffer size. + */ + private final int size; + + + // ----------------------------------------------------------- Constructors + + + /** + * Default constructor. Allocate the buffer with the default buffer size. + */ + public InputBuffer() { + + this(DEFAULT_BUFFER_SIZE); + + } + + + /** + * Alternate constructor which allows specifying the initial buffer size. + * + * @param size Buffer size to use + */ + public InputBuffer(int size) { + + this.size = size; + bb = ByteBuffer.allocate(size); + clear(bb); + cb = CharBuffer.allocate(size); + clear(cb); + readLimit = size; + + } + + + // ------------------------------------------------------------- Properties + + + /** + * Associated Coyote request. + * + * @param coyoteRequest Associated Coyote request + */ + public void setRequest(org.apache.coyote.Request coyoteRequest) { + this.coyoteRequest = coyoteRequest; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Recycle the output buffer. + */ + public void recycle() { + + state = INITIAL_STATE; + + // If usage of mark made the buffer too big, reallocate it + if (cb.capacity() > size) { + cb = CharBuffer.allocate(size); + clear(cb); + } else { + clear(cb); + } + readLimit = size; + markPos = -1; + clear(bb); + closed = false; + + if (conv != null) { + conv.recycle(); + encoders.get(conv.getCharset()).push(conv); + conv = null; + } + } + + + /** + * Close the input buffer. + * + * @throws IOException An underlying IOException occurred + */ + @Override + public void close() throws IOException { + closed = true; + } + + + public int available() { + int available = availableInThisBuffer(); + if (available == 0) { + coyoteRequest.action(ActionCode.AVAILABLE, Boolean.valueOf(coyoteRequest.getReadListener() != null)); + available = (coyoteRequest.getAvailable() > 0) ? 1 : 0; + } + return available; + } + + + private int availableInThisBuffer() { + int available = 0; + if (state == BYTE_STATE) { + available = bb.remaining(); + } else if (state == CHAR_STATE) { + available = cb.remaining(); + } + return available; + } + + + public void setReadListener(ReadListener listener) { + coyoteRequest.setReadListener(listener); + } + + + public boolean isFinished() { + int available = 0; + if (state == BYTE_STATE) { + available = bb.remaining(); + } else if (state == CHAR_STATE) { + available = cb.remaining(); + } + if (available > 0) { + return false; + } else { + return coyoteRequest.isFinished(); + } + } + + + public boolean isReady() { + if (coyoteRequest.getReadListener() == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("inputBuffer.requiresNonBlocking")); + } + return false; + } + if (isFinished()) { + // If this is a non-container thread, need to trigger a read + // which will eventually lead to a call to onAllDataRead() via a + // container thread. + if (!coyoteRequest.isRequestThread()) { + coyoteRequest.action(ActionCode.DISPATCH_READ, null); + coyoteRequest.action(ActionCode.DISPATCH_EXECUTE, null); + } + return false; + } + // Checking for available data at the network level and registering for + // read can be done sequentially for HTTP/1.x and AJP as there is only + // ever a single thread processing the socket at any one time. However, + // for HTTP/2 there is one thread processing the connection and separate + // threads for each stream. For HTTP/2 the two operations have to be + // performed atomically else it is possible for the connection thread to + // read more data in to the buffer after the stream thread checks for + // available network data but before it registers for read. + if (availableInThisBuffer() > 0) { + return true; + } + + return coyoteRequest.isReady(); + } + + + boolean isBlocking() { + return coyoteRequest.getReadListener() == null; + } + + + // ------------------------------------------------- Bytes Handling Methods + + /** + * Reads new bytes in the byte chunk. + * + * @throws IOException An underlying IOException occurred + */ + @Override + public int realReadBytes() throws IOException { + if (closed) { + return -1; + } + + if (state == INITIAL_STATE) { + state = BYTE_STATE; + } + + try { + return coyoteRequest.doRead(this); + } catch (BadRequestException bre) { + // Make the exception visible to the application + handleReadException(bre); + throw bre; + } catch (IOException ioe) { + handleReadException(ioe); + // Any other IOException on a read is almost always due to the remote client aborting the request. + // Make the exception visible to the application + throw new ClientAbortException(ioe); + } + } + + + private void handleReadException(Exception e) throws IOException { + // Set flag used by asynchronous processing to detect errors on non-container threads + coyoteRequest.setErrorException(e); + // In synchronous processing, this exception may be swallowed by the application so set error flags here. + Request request = (Request) coyoteRequest.getNote(CoyoteAdapter.ADAPTER_NOTES); + Response response = request.getResponse(); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, e); + if (e instanceof SocketTimeoutException) { + try { + response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT); + } catch(IllegalStateException ex) { + // Response already committed + response.setStatus(HttpServletResponse.SC_REQUEST_TIMEOUT); + response.setError(); + } + } else { + try { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + } catch(IllegalStateException ex) { + // Response already committed + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setError(); + } + } + } + + + public int readByte() throws IOException { + throwIfClosed(); + + if (checkByteBufferEof()) { + return -1; + } + return bb.get() & 0xFF; + } + + + public int read(byte[] b, int off, int len) throws IOException { + throwIfClosed(); + + if (checkByteBufferEof()) { + return -1; + } + int n = Math.min(len, bb.remaining()); + bb.get(b, off, n); + return n; + } + + + /** + * Transfers bytes from the buffer to the specified ByteBuffer. After the operation the position of the ByteBuffer + * will be returned to the one before the operation, the limit will be the position incremented by the number of the + * transferred bytes. + * + * @param to the ByteBuffer into which bytes are to be written. + * + * @return an integer specifying the actual number of bytes read, or -1 if the end of the stream is reached + * + * @throws IOException if an input or output exception has occurred + */ + public int read(ByteBuffer to) throws IOException { + throwIfClosed(); + + if (checkByteBufferEof()) { + return -1; + } + int n = Math.min(to.remaining(), bb.remaining()); + int orgLimit = bb.limit(); + bb.limit(bb.position() + n); + to.put(bb); + bb.limit(orgLimit); + to.limit(to.position()).position(to.position() - n); + return n; + } + + + // ------------------------------------------------- Chars Handling Methods + + public int realReadChars() throws IOException { + checkConverter(); + + boolean eof = false; + + if (bb.remaining() <= 0) { + int nRead = realReadBytes(); + if (nRead < 0) { + eof = true; + } + } + + if (markPos == -1) { + clear(cb); + } else { + // Make sure there's enough space in the worst case + makeSpace(bb.remaining()); + if ((cb.capacity() - cb.limit()) == 0 && bb.remaining() != 0) { + // We went over the limit + clear(cb); + markPos = -1; + } + } + + state = CHAR_STATE; + conv.convert(bb, cb, this, eof); + + if (cb.remaining() == 0 && eof) { + return -1; + } else { + return cb.remaining(); + } + } + + + @Override + public int read() throws IOException { + throwIfClosed(); + + if (checkCharBufferEof()) { + return -1; + } + return cb.get(); + } + + + @Override + public int read(char[] cbuf) throws IOException { + throwIfClosed(); + return read(cbuf, 0, cbuf.length); + } + + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + throwIfClosed(); + + if (checkCharBufferEof()) { + return -1; + } + int n = Math.min(len, cb.remaining()); + cb.get(cbuf, off, n); + return n; + } + + + @Override + public long skip(long n) throws IOException { + throwIfClosed(); + + if (n < 0) { + throw new IllegalArgumentException(); + } + + long nRead = 0; + while (nRead < n) { + if (cb.remaining() >= n) { + cb.position(cb.position() + (int) n); + nRead = n; + } else { + nRead += cb.remaining(); + cb.position(cb.limit()); + int nb = realReadChars(); + if (nb < 0) { + break; + } + } + } + return nRead; + } + + + @Override + public boolean ready() throws IOException { + throwIfClosed(); + if (state == INITIAL_STATE) { + state = CHAR_STATE; + } + return (available() > 0); + } + + + @Override + public boolean markSupported() { + return true; + } + + + @Override + public void mark(int readAheadLimit) throws IOException { + + throwIfClosed(); + + if (cb.remaining() <= 0) { + clear(cb); + } else { + if ((cb.capacity() > (2 * size)) && (cb.remaining()) < (cb.position())) { + cb.compact(); + cb.flip(); + } + } + readLimit = cb.position() + readAheadLimit + size; + markPos = cb.position(); + } + + + @Override + public void reset() throws IOException { + + throwIfClosed(); + + if (state == CHAR_STATE) { + if (markPos < 0) { + clear(cb); + markPos = -1; + IOException ioe = new IOException(); + coyoteRequest.setErrorException(ioe); + throw ioe; + } else { + cb.position(markPos); + } + } else { + clear(bb); + } + } + + + private void throwIfClosed() throws IOException { + if (closed) { + IOException ioe = new IOException(sm.getString("inputBuffer.streamClosed")); + coyoteRequest.setErrorException(ioe); + throw ioe; + } + } + + public void checkConverter() throws IOException { + if (conv != null) { + return; + } + + Charset charset = coyoteRequest.getCharset(); + + if (charset == null) { + charset = org.apache.coyote.Constants.DEFAULT_BODY_CHARSET; + } + + SynchronizedStack stack = encoders.get(charset); + if (stack == null) { + stack = new SynchronizedStack<>(); + encoders.putIfAbsent(charset, stack); + stack = encoders.get(charset); + } + conv = stack.pop(); + + if (conv == null) { + conv = createConverter(charset); + } + } + + + private static B2CConverter createConverter(Charset charset) throws IOException { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + return AccessController.doPrivileged(new PrivilegedCreateConverter(charset)); + } catch (PrivilegedActionException ex) { + Exception e = ex.getException(); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new IOException(e); + } + } + } else { + return new B2CConverter(charset); + } + + } + + + @Override + public void setByteBuffer(ByteBuffer buffer) { + bb = buffer; + } + + + @Override + public ByteBuffer getByteBuffer() { + return bb; + } + + + @Override + public void expand(int size) { + // no-op + } + + + private boolean checkByteBufferEof() throws IOException { + if (bb.remaining() == 0) { + int n = realReadBytes(); + if (n < 0) { + return true; + } + } + return false; + } + + private boolean checkCharBufferEof() throws IOException { + if (cb.remaining() == 0) { + int n = realReadChars(); + if (n < 0) { + return true; + } + } + return false; + } + + private void clear(Buffer buffer) { + buffer.rewind().limit(0); + } + + private void makeSpace(int count) { + int desiredSize = cb.limit() + count; + if (desiredSize > readLimit) { + desiredSize = readLimit; + } + + if (desiredSize <= cb.capacity()) { + return; + } + + int newSize = 2 * cb.capacity(); + if (desiredSize >= newSize) { + newSize = 2 * cb.capacity() + count; + } + + if (newSize > readLimit) { + newSize = readLimit; + } + + CharBuffer tmp = CharBuffer.allocate(newSize); + int oldPosition = cb.position(); + cb.position(0); + tmp.put(cb); + tmp.flip(); + tmp.position(oldPosition); + cb = tmp; + tmp = null; + } + + + private static class PrivilegedCreateConverter implements PrivilegedExceptionAction { + + private final Charset charset; + + PrivilegedCreateConverter(Charset charset) { + this.charset = charset; + } + + @Override + public B2CConverter run() throws IOException { + return new B2CConverter(charset); + } + } +} diff --git a/java/org/apache/catalina/connector/LocalStrings.properties b/java/org/apache/catalina/connector/LocalStrings.properties new file mode 100644 index 0000000..1efd59b --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings.properties @@ -0,0 +1,98 @@ +# 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. + +coyoteAdapter.accesslogFail=Exception while attempting to add an entry to the access log +coyoteAdapter.asyncDispatch=Exception while processing an asynchronous request +coyoteAdapter.authenticate=Authenticated user [{0}] provided by connector +coyoteAdapter.authorize=Authorizing user [{0}] using Tomcat''s Realm +coyoteAdapter.checkRecycled.request=Encountered a non-recycled request and recycled it forcedly. +coyoteAdapter.checkRecycled.response=Encountered a non-recycled response and recycled it forcedly. +coyoteAdapter.connect=HTTP requests using the CONNECT method are not supported +coyoteAdapter.debug=The variable [{0}] has value [{1}] +coyoteAdapter.invalidURI=Invalid URI +coyoteAdapter.invalidURIWithMessage=Invalid URI: [{0}] +coyoteAdapter.nullRequest=An asynchronous dispatch may only happen on an existing request +coyoteAdapter.trace=TRACE method is not allowed + +coyoteConnector.invalidEncoding=The encoding [{0}] is not recognised by the JRE. The Connector will continue to use [{1}] +coyoteConnector.invalidPort=The connector cannot start since the specified port value of [{0}] is invalid +coyoteConnector.notAsciiSuperset=The encoding [{0}] is not a superset of ASCII as required by RFC 7230. The Connector will continue to use [{1}] +coyoteConnector.parseBodyMethodNoTrace=TRACE method MUST NOT include an entity (see RFC 2616 Section 9.6) +coyoteConnector.protocolHandlerDestroyFailed=Protocol handler destroy failed +coyoteConnector.protocolHandlerInitializationFailed=Protocol handler initialization failed +coyoteConnector.protocolHandlerInstantiationFailed=Protocol handler instantiation failed +coyoteConnector.protocolHandlerPauseFailed=Protocol handler pause failed +coyoteConnector.protocolHandlerResumeFailed=Protocol handler resume failed +coyoteConnector.protocolHandlerStartFailed=Protocol handler start failed +coyoteConnector.protocolHandlerStopFailed=Protocol handler stop failed + +coyoteInputStream.nbNotready=In non-blocking mode you may not read from the ServletInputStream until the previous read has completed and isReady() returns true +coyoteInputStream.null=The input buffer object has been recycled and is no longer associated with this facade + +coyoteOutputStream.nbNotready=In non-blocking mode you may not write to the ServletOutputStream until the previous write has completed and isReady() returns true +coyoteOutputStream.null=The output buffer object has been recycled and is no longer associated with this facade + +coyoteRequest.alreadyAuthenticated=This request has already been authenticated +coyoteRequest.attributeEvent=Exception thrown by attributes event listener +coyoteRequest.authenticate.ise=Cannot call authenticate() after the response has been committed +coyoteRequest.changeSessionId=Cannot change session ID. There is no session associated with this request. +coyoteRequest.chunkedPostTooLarge=Parameters were not parsed because the size of the posted data was too big. Because this request was a chunked request, it could not be processed further. Use the maxPostSize attribute of the connector to resolve this if the application should accept large POSTs. +coyoteRequest.deletePartFailed=Failed to deleted temporary file used for part [{0}] +coyoteRequest.filterAsyncSupportUnknown=Unable to determine if any filters do not support async processing +coyoteRequest.getContextPath.ise=Unable to find match between the canonical context path [{0}] and the URI presented by the user agent [{1}] +coyoteRequest.getInputStream.ise=getReader() has already been called for this request +coyoteRequest.getReader.ise=getInputStream() has already been called for this request +coyoteRequest.gssLifetimeFail=Failed to obtain remaining lifetime for user principal [{0}] +coyoteRequest.maxPostSizeExceeded=The multi-part request contained parameter data (excluding uploaded files) that exceeded the limit for maxPostSize set on the associated connector +coyoteRequest.noAsync=Unable to start async because the following classes in the processing chain do not support async [{0}] +coyoteRequest.noMultipartConfig=Unable to process parts as no multi-part configuration has been provided +coyoteRequest.parseParameters=Exception thrown whilst processing POSTed parameters +coyoteRequest.postTooLarge=Parameters were not parsed because the size of the posted data was too big. Use the maxPostSize attribute of the connector to resolve this if the application should accept large POSTs. +coyoteRequest.sendfileNotCanonical=Unable to determine canonical name of file [{0}] specified for use with sendfile +coyoteRequest.sessionCreateCommitted=Cannot create a session after the response has been committed +coyoteRequest.sessionEndAccessFail=Exception triggered ending access to session while recycling request +coyoteRequest.setAttribute.namenull=Cannot call setAttribute with a null name +coyoteRequest.trailersNotReady=It is illegal to call getTrailerFields() before isTrailerFieldsReady() has returned true +coyoteRequest.uploadCreate=Creating the temporary upload location [{0}] as it is required by the servlet [{1}] +coyoteRequest.uploadCreateFail=Failed to create the upload location [{0}] +coyoteRequest.uploadLocationInvalid=The temporary upload location [{0}] is not valid + +coyoteResponse.encoding.invalid=The encoding [{0}] is not recognised by the JRE +coyoteResponse.getOutputStream.ise=getWriter() has already been called for this response +coyoteResponse.getWriter.ise=getOutputStream() has already been called for this response +coyoteResponse.reset.ise=Cannot call reset() after response has been committed +coyoteResponse.resetBuffer.ise=Cannot reset buffer after response has been committed +coyoteResponse.sendError.ise=Cannot call sendError() after the response has been committed +coyoteResponse.sendRedirect.ise=Cannot call sendRedirect() after the response has been committed +coyoteResponse.sendRedirect.note=

    Redirecting to {0}

    +coyoteResponse.setBufferSize.ise=Cannot change buffer size after data has been written + +inputBuffer.requiresNonBlocking=Not available in non blocking mode +inputBuffer.streamClosed=Stream closed + +outputBuffer.writeNull=The String argument to write(String,int,int) may not be null + +request.asyncNotSupported=A filter or servlet of the current chain does not support asynchronous operations. +request.fragmentInDispatchPath=The fragment in dispatch path [{0}] has been removed +request.illegalWrap=The request wrapper must wrap the request obtained from getRequest() +request.notAsync=It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false) +request.session.failed=Failed to load session [{0}] due to [{1}] + +requestFacade.nullRequest=The request object has been recycled and is no longer associated with this facade + +response.illegalWrap=The response wrapper must wrap the response obtained from getResponse() +response.sendRedirectFail=Failed to redirect to [{0}] + +responseFacade.nullResponse=The response object has been recycled and is no longer associated with this facade diff --git a/java/org/apache/catalina/connector/LocalStrings_cs.properties b/java/org/apache/catalina/connector/LocalStrings_cs.properties new file mode 100644 index 0000000..8914ecc --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_cs.properties @@ -0,0 +1,30 @@ +# 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. + +coyoteAdapter.checkRecycled.response=Nerecyklovatelná odpovÄ›Ä byla nalezena a vynucenÄ› recyklována. +coyoteAdapter.debug=PromÄ›nná [{0}] má hodnotu [{1}] + +coyoteConnector.invalidEncoding=Kódování [{0}] není rozeznáno JRE. Kontektor použije [{1}] +coyoteConnector.invalidPort=Konektor nemůže nastartova, neboÅ¥ uvedená hodnota portu [{0}] není platná +coyoteConnector.parseBodyMethodNoTrace=TRACE metoda nesmí obsahovat entitu (viz sekce 9.6 v RFC 2616) + +coyoteInputStream.nbNotready=V neblokujícím módu nelze Äíst ServletInputStream aniž pÅ™edchozí Ätení bylo dokonÄeno a metoda isReady() vrátila hodnotu true + +coyoteRequest.filterAsyncSupportUnknown=Nelze urÄit, zda nÄ›který filtr nepodporuje asynchronní zpracování +coyoteRequest.gssLifetimeFail=Selhalo získání zbývající doby trvání pro uživatele [{0}] +coyoteRequest.noMultipartConfig=Nelze zpracovat Äásti, protože nebyla poskytnuta žádná multi-part konfigurace +coyoteRequest.sessionEndAccessFail=Výjimka vyvolala ukonÄení přístupu k session bÄ›hem recykllování dotazu + +responseFacade.nullResponse=Objekt odpovÄ›di byl recyklován a již není asociován s touto fasádou diff --git a/java/org/apache/catalina/connector/LocalStrings_de.properties b/java/org/apache/catalina/connector/LocalStrings_de.properties new file mode 100644 index 0000000..63b46c4 --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_de.properties @@ -0,0 +1,27 @@ +# 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. + +coyoteAdapter.checkRecycled.response=Eine nicht recycelte Antwort wurde erkannt und recylet +coyoteAdapter.debug=Die Variable [{0}] hat den Wert [{1}] + +coyoteConnector.invalidEncoding=Das Encoding [{0}] wird von der JRE nicht erkannt. Der Konnektor wird weiterhin [{1}] nutzen +coyoteConnector.invalidPort=Der Konnektor kann nicht starten, da der als Port angegebene Wert [{0}] nicht gültig ist. +coyoteConnector.parseBodyMethodNoTrace=Trace-Methode darf keine Entität enthalten (siehe RFC 2616, Sektion 9.6) +coyoteConnector.protocolHandlerStartFailed=Der Start des Protokoll-Handlers ist fehlgeschlagen + +coyoteRequest.filterAsyncSupportUnknown=Es konnte nicht ermittelt werden ob einer der Filter asyncrone Bearbeitung nicht unterstützt +coyoteRequest.gssLifetimeFail=Die verbleibende Lebenszeit für den Principal [{0}] konnte nicht ermittelt werden. + +responseFacade.nullResponse=Das Response Objekt ist wiederverwendet worden und nicht mehr mit der Facade verknüpft. diff --git a/java/org/apache/catalina/connector/LocalStrings_es.properties b/java/org/apache/catalina/connector/LocalStrings_es.properties new file mode 100644 index 0000000..341b791 --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_es.properties @@ -0,0 +1,64 @@ +# 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. + +coyoteAdapter.accesslogFail=Excepción al intentar añadir una entrada al historial de acceso +coyoteAdapter.checkRecycled.response=Se encontró una respuesta no reciclable y se crecicló a la fuerza +coyoteAdapter.debug=La variable [{0}] tiene el valor [{1}] + +coyoteConnector.invalidEncoding=La codificación [{0}] no esta reconocida por JRE. El conector continuará usando [{1}]\n +coyoteConnector.invalidPort=El conector no puede inciar debido a que el valor del puerto especificado [{0}] no es válido +coyoteConnector.parseBodyMethodNoTrace=El método TRACE NO DEBE DE incluir una entidad (vea RFC 2616 Sección 9.6) +coyoteConnector.protocolHandlerDestroyFailed=Falló la destrucción del manejador de protocolo +coyoteConnector.protocolHandlerInitializationFailed=Falló la inicialización del manejador de protocolo +coyoteConnector.protocolHandlerInstantiationFailed=Falló la instanciación del manejador de protocolo +coyoteConnector.protocolHandlerPauseFailed=Ha fallado la pausa del manejador de protocolo +coyoteConnector.protocolHandlerResumeFailed=Ha fallado el rearranque del manejador de protocolo +coyoteConnector.protocolHandlerStartFailed=Falló el arranque del manejador de protocolo +coyoteConnector.protocolHandlerStopFailed=Ocurrió un fallo al detener el manejador del protocolo + +coyoteInputStream.nbNotready=En modo non-blocking usted no puede leer desde ServletInputStream hasta que la lectura previa haya sido completada y isReady() devuelva verdadero + +coyoteRequest.alreadyAuthenticated=Este requerimiento ya ha sido autenticado +coyoteRequest.attributeEvent=Excepción lanzada mediante el escuchador de eventos de atributos +coyoteRequest.authenticate.ise=No puedo llamar a authenticate() tras haberse acometido la respuesta +coyoteRequest.changeSessionId=No se puede cambiar el ID de sesión. No hay sesión asociada con esta solicitud +coyoteRequest.chunkedPostTooLarge=No se han analizado los parámetros porque la medida de los datos enviados meiante "post" era demasiado grande. Debido a que este requerimiento es una parte del original, no puede ser procesado. Utiliza el atributo "maxPostSize" del conector para resolver esta situación, en caso de que la aplicación deba de aceptar POSTs mayores. +coyoteRequest.filterAsyncSupportUnknown=Imposible determinar si algún filtro no soporta procesamiento asincrónico +coyoteRequest.getInputStream.ise=getReader() ya ha sido llamado para este requerimiento +coyoteRequest.getReader.ise=getInputStream() ya ha sido llamado para este requerimiento +coyoteRequest.gssLifetimeFail=Fallo al obtener el tiempo de vida restante para el usuario principal [{0}]\n +coyoteRequest.noMultipartConfig=Imposible procesar partes debido a que se ha proveído una configuración no multipartes +coyoteRequest.parseParameters=Excepción lanzada al procesar parámetros POST +coyoteRequest.postTooLarge=No se analizaron los parámetros porque la medida de los datos enviados era demasiado grande. Usa el atributo maxPostSize del conector para resolver esto en caso de que la aplicación debiera de aceptar POSTs más grandes. +coyoteRequest.sendfileNotCanonical=Incapaz de determinar el nombre canónico del archivo [{0}] especificado para ser usado con sendfile +coyoteRequest.sessionCreateCommitted=No puedo crear una sesión después de llevar a cabo la respueta +coyoteRequest.sessionEndAccessFail=Excepción disparada acabando acceso a sesión mientras se reciclaba el requerimiento +coyoteRequest.setAttribute.namenull=No pudeo llamar a setAttribute con un nombre nulo +coyoteRequest.uploadLocationInvalid=No es válida la localización [{0}] de carga temporal + +coyoteResponse.getOutputStream.ise=getWriter() ya ha sido llamado para esta respuesta +coyoteResponse.getWriter.ise=getOutputStream() ya ha sido llamado para esta respuesta +coyoteResponse.resetBuffer.ise=No puedo limpiar el búfer después de que la respuesta ha sido llevada a cabo +coyoteResponse.sendError.ise=No puedo llamar a sendError() tras llevar a cabo la respuesta +coyoteResponse.sendRedirect.ise=No puedo llamar a sendRedirect() tras llevar a cabo la respuesta +coyoteResponse.setBufferSize.ise=No puedo cambiar la medida del búfer tras escribir los datos + +inputBuffer.streamClosed=Flujo cerrado + +requestFacade.nullRequest=El objeto de requerimiento ha sido reciclado y ya no está asociado con esta fachada + +response.sendRedirectFail=Fallo al redirigir a [{0}] + +responseFacade.nullResponse=El objeto de respuesta ha sido reciclado y ya no está asociado con esta fachada diff --git a/java/org/apache/catalina/connector/LocalStrings_fr.properties b/java/org/apache/catalina/connector/LocalStrings_fr.properties new file mode 100644 index 0000000..88270a8 --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_fr.properties @@ -0,0 +1,98 @@ +# 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. + +coyoteAdapter.accesslogFail=Exception lors d'une tentative d'ajout d'une entrée au journal d'accès (access log) +coyoteAdapter.asyncDispatch=Exception lors du traitement d'une requête asynchrone +coyoteAdapter.authenticate=L''utilisateur authentifié [{0}] a été fourni par le connecteur +coyoteAdapter.authorize=Autorisation de l''utilisateur [{0}] en utilisant le Realm de Tomcat +coyoteAdapter.checkRecycled.request=Trouvé une requête non recyclée dont le recyclage a été forcé +coyoteAdapter.checkRecycled.response=Trouvé une réponse non recyclée, et forcé son recyclage +coyoteAdapter.connect=Les requêtes HTTP utilisant la méthode CONNECT ne sont pas supportées +coyoteAdapter.debug=La variable [{0}] a la valeur [{1}] +coyoteAdapter.invalidURI=URI invalide +coyoteAdapter.invalidURIWithMessage=URI invalide: [{0}] +coyoteAdapter.nullRequest=Un dispatch asynchrone peut seulement se produire sur une requête existante +coyoteAdapter.trace=La méthode TRACE n'est pas autorisée + +coyoteConnector.invalidEncoding=L''encodage [{0}] n''est pas reconnu par la JRE. Le connecteur (Connector) continuera à utiliser [{1}] +coyoteConnector.invalidPort=Le connecteur ne peut pas démarrer, parce que la valeur spécifiée du port [{0}] n''est pas valide +coyoteConnector.notAsciiSuperset=L''encodage [{0}] n''inclut pas l''ASCII comme requis par la RFC 7230, le connecteur va continuer à utiliser [{1}] +coyoteConnector.parseBodyMethodNoTrace=La méthode "TRACE" NE PEUT PAS contenir une entité (voir RFC 2616 Section 9.6) +coyoteConnector.protocolHandlerDestroyFailed=La destruction du gestionnaire de protocole a échoué +coyoteConnector.protocolHandlerInitializationFailed=L'initialisation du gestionnaire de protocole a échoué +coyoteConnector.protocolHandlerInstantiationFailed=L'instantiation du gestionnaire de protocole a échoué +coyoteConnector.protocolHandlerPauseFailed=La suspension du gestionnaire de protocole a échouée +coyoteConnector.protocolHandlerResumeFailed=Le redémarrage du gestionnaire de protocole a échoué +coyoteConnector.protocolHandlerStartFailed=Le démarrage du gestionnaire de protocole a échoué +coyoteConnector.protocolHandlerStopFailed=L'arrêt du gestionnaire de protocole a échoué + +coyoteInputStream.nbNotready=En mode non-bloquant, vous ne pouvez pas lire du ServletInputStream tant que la lecture précédente n'est pas terminée et isReady() renvoie "true" +coyoteInputStream.null=Le tampon d'entrée a été recyclé et n'est plus associé à cette facade + +coyoteOutputStream.nbNotready=En mode non bloquant, vous ne devez pas écrire sur la ServletOutputStream avant que l'écriture précédente ne soit terminée et que isReady() ne renvoie true +coyoteOutputStream.null=Le tampon de sortie a été recyclé et n'est plus associé à cette facade + +coyoteRequest.alreadyAuthenticated=Cette requête a déjà été authentifiée +coyoteRequest.attributeEvent=Une exception a été lancée par l'instance d'écoute pour l'évènement attributs (attributes) +coyoteRequest.authenticate.ise=Impossible d'appeler authenticate() après le début de l'envoi de la réponse +coyoteRequest.changeSessionId=Impossible de changer l'id de la session, il n'y a pas de session associée à cette requête +coyoteRequest.chunkedPostTooLarge=Les paramètres n'ont pas été traités parce que la taille des données du POST étaient trop grandes ; comme cette requête utilisait le découpage par morceaux (chunking), le traitement est arrêté ; utiliser l'attribut maxPostSize du connecteur pour résoudre ce problème si l'application devrait accepter des tailles de POST plus importantes +coyoteRequest.deletePartFailed=Impossible d''effacer le fichier temporaire utilisé pour la partie [{0}] +coyoteRequest.filterAsyncSupportUnknown=Incapacité de déterminer si un des filtres ne supporte pas le mode asynchrone +coyoteRequest.getContextPath.ise=Impossible de trouver une correspondance entre le chemin canonique du contexte [{0}] et l''URI envoyée par l''agent de l''utilisateur [{1}] +coyoteRequest.getInputStream.ise="getReader()" a déjà été appelé pour cette requête +coyoteRequest.getReader.ise="getInputStream()" a déjà été appelé pour cette requête +coyoteRequest.gssLifetimeFail=Echec d''obtention de la durée de vie restante pour le "user principal" [{0}] +coyoteRequest.maxPostSizeExceeded=La requête multi part contenait des données de paramètres (en excluant les fichiers envoyés) dont la taille a excédé la limite maxPostSize fixée sur le connecteur associé +coyoteRequest.noAsync=Impossible de démarrer le mode asynchrone car les classes [{0}] de la chaîne de traitement ne le supportent pas +coyoteRequest.noMultipartConfig=Impossible de traiter des parties, parce qu'aucune configuration multi-parties n'a été fournie +coyoteRequest.parseParameters=Exception lors du traitement des paramètres envoyés par POST +coyoteRequest.postTooLarge=Les paramètres n'ont pas été évalués car la taille des données postées est trop important. Utilisez l'attribut maxPostSize du connecteur pour corriger ce problème si votre application doit accepter des POSTs importants. +coyoteRequest.sendfileNotCanonical=Impossible d''obtenir le nom canonique du fichier [{0}] qui a été donné pour le sendfile +coyoteRequest.sessionCreateCommitted=Impossible de créer une session après que la réponse ait été envoyée +coyoteRequest.sessionEndAccessFail=Exception lancée durant l'arrêt de l'accès à la session durant le recyclage de la requête +coyoteRequest.setAttribute.namenull=Impossible d'appeler "setAttribute" avec un nom nul +coyoteRequest.trailersNotReady=Impossible d'appeler getTrailerFields() avant que isTrailerFieldsReady() n'ait retourné true +coyoteRequest.uploadCreate=Un répertoire temporaire [{0}] pour les fichiers envoyés sera crée car il est requis par le Servlet [{1}] +coyoteRequest.uploadCreateFail=Echec de création du répertoire [{0}] pour les fichiers envoyés +coyoteRequest.uploadLocationInvalid=Le répertoire temporaire [{0}] pour les envois de fichier est invalide + +coyoteResponse.encoding.invalid=L''encodage [{0}] n''est pas reconnu par le JRE +coyoteResponse.getOutputStream.ise="getWriter()" a déjà été appelé pour cette réponse +coyoteResponse.getWriter.ise="getOutputStream()" a déjà été appelé pour cette réponse +coyoteResponse.reset.ise=Impossible d'appeler reset() après le début de l'envoi de la réponse +coyoteResponse.resetBuffer.ise=Impossible de remettre à zéro le tampon après que la réponse ait été envoyée +coyoteResponse.sendError.ise=Impossible d'appeler "sendError()" après que la réponse ait été envoyée +coyoteResponse.sendRedirect.ise=Impossible d'appeler "sendRedirect()" après que la réponse ait été envoyée +coyoteResponse.sendRedirect.note=

    Redirection vers {0}

    +coyoteResponse.setBufferSize.ise=Impossible de changer la taille du tampon après que les données aient été écrites + +inputBuffer.requiresNonBlocking=Pas disponible en mode non bloquant +inputBuffer.streamClosed=Le flux a été fermé + +outputBuffer.writeNull=L'argument String dans write(String, int, int) ne doit pas être null + +request.asyncNotSupported=Un filtre ou un Servlet de la chaîne actuelle ne supporte pas le mode asynchrone +request.fragmentInDispatchPath=Le fragment dans le chemin de dispatch [{0}] a été enlevé +request.illegalWrap=L'enrobeur de la réponse doit enrober la requête obtenue à partir de getRequest() +request.notAsync=Il est interdit d'appeler cette méthode si la requête actuelle n'est pas en mode asynchrone (isAsyncStarted() a renvoyé false) +request.session.failed=Erreur de chargement de la session [{0}] à cause de [{1}] + +requestFacade.nullRequest=L'objet requête a été recyclé et n'est plus associé à cette façade + +response.illegalWrap=L'enrobeur de la réponse doit enrober la réponse obtenue à partir de getResponse() +response.sendRedirectFail=Impossible d''envoyer une redirection vers [{0}] + +responseFacade.nullResponse=L'objet réponse a été recyclé et n'est plus associé à cette façade diff --git a/java/org/apache/catalina/connector/LocalStrings_ja.properties b/java/org/apache/catalina/connector/LocalStrings_ja.properties new file mode 100644 index 0000000..0ce9f27 --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_ja.properties @@ -0,0 +1,98 @@ +# 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. + +coyoteAdapter.accesslogFail=アクセスログã«ã‚¨ãƒ³ãƒˆãƒªã‚’追加ã™ã‚‹éš›ã®ä¾‹å¤– +coyoteAdapter.asyncDispatch=éžåŒæœŸãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å‡¦ç†ä¸­ã®ä¾‹å¤– +coyoteAdapter.authenticate=コãƒã‚¯ã‚¿ãƒ¼ã‹ã‚‰èªè¨¼æ¸ˆã¿ãƒ¦ãƒ¼ã‚¶ãƒ¼ [{0}] ã‚’å–å¾—ã—ã¾ã—ãŸã€‚ +coyoteAdapter.authorize=Tomcat ã®Realmã§ãƒ¦ãƒ¼ã‚¶ãƒ¼ [{0}] ã‚’èªè¨¼ã—ã¾ã™ã€‚ +coyoteAdapter.checkRecycled.request=リサイクルã•ã‚Œã¦ã„ãªã„リクエストã«é­é‡ã—ã¾ã—ãŸã€‚強制的ã«ãƒªã‚µã‚¤ã‚¯ãƒ«ã—ã¾ã—ãŸã€‚ +coyoteAdapter.checkRecycled.response=リサイクルã•ã‚Œã¦ã„ãªã„レスãƒãƒ³ã‚¹ãŒç™ºç”Ÿã€å¼·åˆ¶çš„ã«ãƒªã‚µã‚¤ã‚¯ãƒ«ã•ã‚Œã¾ã—ãŸã€‚ +coyoteAdapter.connect=CONNECT メソッドを使用ã—㟠HTTP リクエストã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“ +coyoteAdapter.debug=変数[{0}]ã«å€¤[{1}]ãŒã‚ã‚Šã¾ã™ +coyoteAdapter.invalidURI=無効㪠URI +coyoteAdapter.invalidURIWithMessage=無効㪠URI: [{0}] +coyoteAdapter.nullRequest=éžåŒæœŸãƒ‡ã‚£ã‚¹ãƒ‘ッãƒã¯ã€æ—¢å­˜ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã§ã®ã¿ç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ +coyoteAdapter.trace=TRACE メソッドã¯ä½¿ç”¨ã§ãã¾ã›ã‚“ + +coyoteConnector.invalidEncoding=[{0}] 㯠JRE ã®ç†è§£ã§ããªã„符å·åŒ–æ–¹å¼ã§ã™ã€‚Connector 㯠[{1}] ã§å‡¦ç†ã‚’続行ã—ã¾ã™ã€‚ +coyoteConnector.invalidPort=[{0}]ã®æŒ‡å®šã•ã‚ŒãŸãƒãƒ¼ãƒˆå€¤ãŒç„¡åŠ¹ã§ã‚ã‚‹ãŸã‚ã€ã‚³ãƒã‚¯ã‚¿ã‚’開始ã§ãã¾ã›ã‚“ +coyoteConnector.notAsciiSuperset=ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‡ã‚£ãƒ³ã‚°æ–¹å¼ [{0}] 㯠RFC 7230 ã§è¦æ±‚ã•ã‚Œã‚‹ ASCII ã¨ã®ä¸Šä½äº’æ›æ€§ãŒã‚ã‚Šã¾ã›ã‚“。コãƒã‚¯ã‚¿ã¯å¼•ã続ã [{1}] を使用ã—ã¾ã™ +coyoteConnector.parseBodyMethodNoTrace=TRACE メソッドã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯ã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ã‚’å«ã‚ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ (RFC 2616 ã® 9.6 節をå‚ç…§)。 +coyoteConnector.protocolHandlerDestroyFailed=プロトコルãƒãƒ³ãƒ‰ãƒ©ã®å»ƒæ£„ã«å¤±æ•—ã—ã¾ã—㟠+coyoteConnector.protocolHandlerInitializationFailed=プロトコルãƒãƒ³ãƒ‰ãƒ©ã®åˆæœŸåŒ–ã«å¤±æ•—ã—ã¾ã—㟠+coyoteConnector.protocolHandlerInstantiationFailed=プロトコルãƒãƒ³ãƒ‰ãƒ©ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹åŒ–ã«å¤±æ•—ã—ã¾ã—㟠+coyoteConnector.protocolHandlerPauseFailed=プロトコルãƒãƒ³ãƒ‰ãƒ©ã®ä¸€æ™‚åœæ­¢ã«å¤±æ•—ã—ã¾ã—㟠+coyoteConnector.protocolHandlerResumeFailed=プロトコルãƒãƒ³ãƒ‰ãƒ©ã®å†é–‹ã«å¤±æ•—ã—ã¾ã—㟠+coyoteConnector.protocolHandlerStartFailed=プロトコルãƒãƒ³ãƒ‰ãƒ©ã®èµ·å‹•ã«å¤±æ•—ã—ã¾ã—㟠+coyoteConnector.protocolHandlerStopFailed=プロトコルãƒãƒ³ãƒ‰ãƒ©ã®åœæ­¢ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ + +coyoteInputStream.nbNotready=ノンブロッキングモードã§ã¯ã€ä»¥å‰ã®èª­ã¿å–ã‚ŠãŒå®Œäº†ã—㦠isReady() ㌠true ã‚’è¿”ã™ã¾ã§ã€ServletInputStream ã‹ã‚‰èª­ã¿å–ã‚Šã§ãã¾ã›ã‚“。 +coyoteInputStream.null=入力ãƒãƒƒãƒ•ã‚¡ã‚ªãƒ–ジェクトã¯ãƒªã‚µã‚¤ã‚¯ãƒ«ã•ã‚Œã€ã“ã®ãƒ•ã‚¡ã‚µãƒ¼ãƒ‰ã«é–¢é€£ä»˜ã‘られãªããªã‚Šã¾ã—㟠+ +coyoteOutputStream.nbNotready=ノンブロッキングモードã§ã¯ç›´å‰ã®æ›¸ãè¾¼ã¿ãŒå®Œäº†ã— isReady() ㌠true ã‚’è¿”ã™ã¾ã§ ServletOutputStream ã¸ã®æ›¸ãè¾¼ã¿ã¯ã§ãã¾ã›ã‚“。 +coyoteOutputStream.null=出力ãƒãƒƒãƒ•ã‚¡ã‚ªãƒ–ジェクトã¯ãƒªã‚µã‚¤ã‚¯ãƒ«ã•ã‚Œã€ã“ã®ãƒ•ã‚¡ã‚µãƒ¼ãƒ‰ã«é–¢é€£ä»˜ã‘られãªããªã‚Šã¾ã—㟠+ +coyoteRequest.alreadyAuthenticated=èªè¨¼æ¸ˆã¿ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã§ã™ã€‚ +coyoteRequest.attributeEvent=属性イベントリスナã«ã‚ˆã£ã¦ä¾‹å¤–ãŒæŠ•ã’られã¾ã—㟠+coyoteRequest.authenticate.ise=レスãƒãƒ³ã‚¹ã‚’コミットã—ãŸå¾Œã¯ authenticate() を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +coyoteRequest.changeSessionId=セッション ID ã¯å¤‰æ›´ã§ãã¾ã›ã‚“。リクエストã«é–¢é€£ä»˜ã‘られãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒã‚ã‚Šã¾ã›ã‚“。 +coyoteRequest.chunkedPostTooLarge=POST データãŒå¤§ãã™ãŽã‚‹ãŸã‚パラメーターを解æžã—ã¾ã›ã‚“ã§ã—ãŸã€‚リクエストã¯ãƒãƒ£ãƒ³ã‚¯åŒ–ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€å‡¦ç†ã‚’継続ã§ãã¾ã›ã‚“。アプリケーションãŒå¤§ã㪠POST データをå—ä¿¡ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ãªã‚‰ã€Connector è¦ç´ ã® maxPostSize 属性を変更ã—ã¦ãã ã•ã„。 +coyoteRequest.deletePartFailed=part [{0}] ã«ä½¿ç”¨ã•ã‚Œã‚‹ä¸€æ™‚ファイルã®å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—㟠+coyoteRequest.filterAsyncSupportUnknown=éžåŒæœŸå‡¦ç†ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ãªã„FilterãŒã‚ã‚‹ã‹ã©ã†ã‹ã‚’判断ã§ãã¾ã›ã‚“ +coyoteRequest.getContextPath.ise=標準的ãªã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãƒ‘ス [{0}] ã¨ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚¨ãƒ¼ã‚¸ã‚§ãƒ³ãƒˆ [{1}] ã«ã‚ˆã£ã¦æ示ã•ã‚ŒãŸURIã¨ã®ä¸€è‡´ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +coyoteRequest.getInputStream.ise=getReader()ã¯ã“ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾ã—ã¦æ—¢ã«å‘¼ã³å‡ºã•ã‚Œã¦ã„ã¾ã™ +coyoteRequest.getReader.ise=getInputStream()ã¯ã“ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾ã—ã¦æ—¢ã«å‘¼ã³å‡ºã•ã‚Œã¦ã„ã¾ã™ +coyoteRequest.gssLifetimeFail=ユーザープリンシパル [{0}] ã®æ®‹ã‚Šã®å¯¿å‘½(秒å˜ä½) ã‚’å–å¾—ã§ãã¾ã›ã‚“ +coyoteRequest.maxPostSizeExceeded=マルãƒãƒ‘ートリクエストã«ã¯ã€é–¢é€£ä»˜ã‘られãŸã‚³ãƒã‚¯ã‚¿ã§è¨­å®šã•ã‚ŒãŸmaxPostSizeã®åˆ¶é™ã‚’超ãˆãŸãƒ‘ラメータデータ(アップロードã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«ã‚’除ã)ãŒå«ã¾ã‚Œã¦ã„ã¾ã—ãŸã€‚ +coyoteRequest.noAsync=処ç†ãƒã‚§ãƒ¼ãƒ³å†…ã®æ¬¡ã®ã‚¯ãƒ©ã‚¹ãŒéžåŒæœŸã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ãªã„ãŸã‚ã€éžåŒæœŸã‚’開始ã§ãã¾ã›ã‚“ [{0}] +coyoteRequest.noMultipartConfig=multi-part 構æˆãŒæä¾›ã•ã‚Œã¦ã„ãªã„ãŸã‚ã€partを処ç†ã§ãã¾ã›ã‚“ +coyoteRequest.parseParameters=POST パラメーターã®å‡¦ç†ä¸­ã«ä¾‹å¤–を投ã’ã¾ã—ãŸã€‚ +coyoteRequest.postTooLarge=POSTã•ã‚ŒãŸãƒ‡ãƒ¼ã‚¿ãŒå¤§ãã™ãŽãŸã®ã§ã€ãƒ‘ラメータãŒæ§‹æ–‡è§£æžã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ãã®ã‚¢ãƒ—リケーションãŒå·¨å¤§ãªPOSTã‚’å—ã‘付ã‘ã­ã°ãªã‚‰ãªã„å ´åˆã«ã¯ã€ã“れを解決ã™ã‚‹ãŸã‚ã«ã‚³ãƒã‚¯ã‚¿ã®maxPostSize属性を使用ã—ã¦ãã ã•ã„。 +coyoteRequest.sendfileNotCanonical=sendfile ã«æŒ‡å®šã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ« [{0}] ã®æ­£å¼åã‚’å–å¾—ã§ãã¾ã›ã‚“ +coyoteRequest.sessionCreateCommitted=レスãƒãƒ³ã‚¹ã‚’コミットã—ãŸå¾Œã§ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’作æˆã§ãã¾ã›ã‚“ +coyoteRequest.sessionEndAccessFail=リクエストã®å†åˆ©ç”¨ä¸­ã«è¡Œã£ãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹çµ‚了処ç†ã§ä¾‹å¤–ãŒé€å‡ºã•ã‚Œã¾ã—ãŸã€‚ +coyoteRequest.setAttribute.namenull=setAttributeã‚’åå‰ã‚’指定ã›ãšã«å‘¼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“ +coyoteRequest.trailersNotReady=isTrailerFieldsReady()ãŒtrueã‚’è¿”ã™å‰ã«getTrailerFields()を呼ã³å‡ºã™ã“ã¨ã¯ä¸æ­£ã§ã™ +coyoteRequest.uploadCreate=サーブレット [{1}] ã«å¿…è¦ãªä¸€æ™‚アップロード場所[{0}]を作æˆã—ã¾ã™ã€‚ +coyoteRequest.uploadCreateFail=アップロード場所[{0}]ã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +coyoteRequest.uploadLocationInvalid=一時的ãªã‚¢ãƒƒãƒ—ロード場所[{0}]ã¯ç„¡åŠ¹ã§ã™ + +coyoteResponse.encoding.invalid=JRE ã¯æ–‡å­—エンコーディング [{0}] ã‚’ç†è§£ã—ã¾ã›ã‚“。 +coyoteResponse.getOutputStream.ise=getWriter()ã¯ã“ã®ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã«å¯¾ã—ã¦æ—¢ã«å‘¼ã³å‡ºã•ã‚Œã¦ã„ã¾ã™ +coyoteResponse.getWriter.ise=getOutputStream()ã¯ã“ã®ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã«å¯¾ã—ã¦æ—¢ã«å‘¼ã³å‡ºã•ã‚Œã¦ã„ã¾ã™ +coyoteResponse.reset.ise=レスãƒãƒ³ã‚¹ãŒã‚³ãƒŸãƒƒãƒˆã•ã‚ŒãŸå¾Œã§reset()を呼ã³å‡ºã™ã“ã¨ãŒã§ãã¾ã›ã‚“。 +coyoteResponse.resetBuffer.ise=レスãƒãƒ³ã‚¹ãŒã‚³ãƒŸãƒƒãƒˆã•ã‚ŒãŸå¾Œã§ãƒãƒƒãƒ•ã‚¡ã‚’リセットã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +coyoteResponse.sendError.ise=レスãƒãƒ³ã‚¹ãŒã‚³ãƒŸãƒƒãƒˆã•ã‚ŒãŸå¾Œã§sendError()を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“ +coyoteResponse.sendRedirect.ise=レスãƒãƒ³ã‚¹ãŒã‚³ãƒŸãƒƒãƒˆã•ã‚ŒãŸå¾Œã§sendRedirect()を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“ +coyoteResponse.sendRedirect.note=

    {0} ã¸ã®ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆ

    +coyoteResponse.setBufferSize.ise=データãŒæ—¢ã«æ›¸ãè¾¼ã¾ã‚ŒãŸå¾Œã§ãƒãƒƒãƒ•ã‚¡ã‚µã‚¤ã‚ºã‚’変更ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ + +inputBuffer.requiresNonBlocking=ノンブロッキングモードã§ã¯åˆ©ç”¨ã§ãã¾ã›ã‚“。 +inputBuffer.streamClosed=ストリームã¯ã‚¯ãƒ­ãƒ¼ã‚ºã—ã¦ã„ã¾ã™ + +outputBuffer.writeNull=write(String, int, int) メソッド㮠String åž‹ã®å¼•æ•°ã« null を指定ã§ãã¾ã›ã‚“。 + +request.asyncNotSupported=ç¾åœ¨ã®ãƒã‚§ãƒ¼ãƒ³ã®ãƒ•ã‚£ãƒ«ã‚¿ã¾ãŸã¯ã‚µãƒ¼ãƒ–レットã¯éžåŒæœŸæ“作をサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 +request.fragmentInDispatchPath=ディスパッãƒãƒ‘ス [{0}] 中ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¯é™¤åŽ»ã•ã‚Œã¾ã—㟠+request.illegalWrap=リクエストラッパー㯠getRequest() ã§å–å¾—ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ラップã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +request.notAsync=éžåŒæœŸãƒ¢ãƒ¼ãƒ‰ã§ã¯ãªã„リクエストã§ã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ã‚’呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。(例ãˆã° isAsyncStarted() ㌠false ã‚’è¿”ã™å ´åˆ) +request.session.failed=[{1}]ãŒåŽŸå› ã§ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³[{0}]ã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—㟠+ +requestFacade.nullRequest=リクエストオブジェクトã¯å›žåŽã•ã‚Œã“ã®ãƒ•ã‚¡ã‚µãƒ¼ãƒ‰ã«é–¢é€£ä»˜ã‘られãªããªã‚Šã¾ã—ãŸã€‚ + +response.illegalWrap=レスãƒãƒ³ã‚¹ãƒ©ãƒƒãƒ‘ーã¯ã€getResponse()ã‹ã‚‰å–å¾—ã—ãŸãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚’ラップã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +response.sendRedirectFail=[{0}] ã¸ã®ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ + +responseFacade.nullResponse=レスãƒãƒ³ã‚¹ã‚ªãƒ–ジェクトã¯ã™ã§ã«å›žåŽã•ã‚ŒãŸãŸã‚ã“ã®ãƒ•ã‚¡ã‚µãƒ¼ãƒ‰ã¨ã¯é–¢é€£ä»˜ã‘ãŒã‚ã‚Šã¾ã›ã‚“ diff --git a/java/org/apache/catalina/connector/LocalStrings_ko.properties b/java/org/apache/catalina/connector/LocalStrings_ko.properties new file mode 100644 index 0000000..38b1938 --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_ko.properties @@ -0,0 +1,93 @@ +# 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. + +coyoteAdapter.accesslogFail=ì ‘ê·¼ ë¡œê·¸ì— ì—”íŠ¸ë¦¬ë¥¼ 추가하기 위한 ì‹œë„ ì¤‘ 예외 ë°œìƒ +coyoteAdapter.asyncDispatch=비ë™ê¸° ìš”ì²­ì„ ì²˜ë¦¬í•˜ëŠ” 중 예외 ë°œìƒ +coyoteAdapter.authenticate=Connectorì— ì˜í•´ ì œê³µëœ ì‚¬ìš©ìž [{0}]ì„(를) ì¸ì¦í–ˆìŠµë‹ˆë‹¤. +coyoteAdapter.authorize=Tomcatì˜ Realmì„ ì‚¬ìš©í•˜ì—¬ ì‚¬ìš©ìž [{0}]ì„(를) ìŠ¹ì¸ ì¤‘ +coyoteAdapter.checkRecycled.request=참조 í•´ì œ ë˜ì§€ì•Šì€ ìš”ì²­ì„ ë°œê²¬í•˜ì—¬ 강제로 참조 해제했습니다. +coyoteAdapter.checkRecycled.response=참조 í•´ì œë˜ì§€ ì•Šì€ ì‘ë‹µì´ ë°œê²¬ë˜ì–´ 강제로 참조 해제합니다. +coyoteAdapter.debug=변수 [{0}]ì´(ê°€) ê°’ [{1}]ì„(를) 가지고 있습니다. +coyoteAdapter.nullRequest=비ë™ê¸° 디스패치는, 기존 ìš”ì²­ì— ëŒ€í•´ ì˜¤ì§ í•œë²ˆë§Œ ì¼ì–´ë‚˜ì•¼ 합니다. + +coyoteConnector.invalidEncoding=ì¸ì½”딩 [{0}]ì€(는) JREì— ì˜í•´ ì¸ì‹ë˜ì§€ 않습니다. Connector는 [{1}]ì„(를) ê³„ì† ì‚¬ìš©í•  것입니다. +coyoteConnector.invalidPort=ì§€ì •ëœ í¬íŠ¸ 번호, [{0}]ì´(ê°€) 유효하지 않기 때문ì—, Connectorê°€ ì‹œìž‘ë  ìˆ˜ 없습니다. +coyoteConnector.notAsciiSuperset=ì¸ì½”딩 [{0}]ì€(는), RFC 7230ì´ ìš”êµ¬í•˜ëŠ” ASCIIì˜ ìƒìœ„ì§‘í•©ì´ ì•„ë‹™ë‹ˆë‹¤. 해당 Connector는 [{1}]ì„(를) ê³„ì† ì‚¬ìš©í•  것입니다, +coyoteConnector.parseBodyMethodNoTrace=TRACE 메소드는 엔티티를 í¬í•¨í•´ì„œëŠ” 안ë©ë‹ˆë‹¤. (RFC 2616 Section 9.6 참조) +coyoteConnector.protocolHandlerDestroyFailed=프로토콜 핸들러 소멸 중 실패 +coyoteConnector.protocolHandlerInitializationFailed=프로토콜 핸들러 초기화가 실패했습니다. +coyoteConnector.protocolHandlerInstantiationFailed=프로토콜 핸들러 ì¸ìŠ¤í„´ìŠ¤ ìƒì„±ì— 실패했습니다. +coyoteConnector.protocolHandlerPauseFailed=프로토콜 í•¸ë“¤ëŸ¬ì— ëŒ€í•œ ì¼ì‹œ 정지가 실패했습니다. +coyoteConnector.protocolHandlerResumeFailed=프로토콜 핸들러를 재개하지 못했습니다. +coyoteConnector.protocolHandlerStartFailed=프로토콜 핸들러 시작 실패 +coyoteConnector.protocolHandlerStopFailed=프로토콜 핸들러를 중지시키지 못했습니다. + +coyoteInputStream.nbNotready=Non-blocking 모드ì—서는, ì´ì „ì˜ ë°ì´í„° ì½ê¸°ê°€ 완료ë˜ê³  isReady()ê°€ true를 반환하기 전까지는, ServletInputStream으로부터 ë°ì´í„°ë¥¼ ì½ì„ 수 없습니다. +coyoteInputStream.null=ìž…ë ¥ ë²„í¼ ê°ì²´ëŠ” ì´ë¯¸ 제거ë˜ì—ˆê³  ì´ facadeì— ë” ì´ìƒ ì—°ê²°ë˜ì–´ 있지 않습니다. + +coyoteOutputStream.nbNotready=Non-blocking 모드ì—서는, ì´ì „ì˜ ì“°ê¸°ê°€ 완료ë˜ê³  isReady()ê°€ true를 반환할 때까지는, ServletOutputStreamì— ì“¸ 수 없습니다. +coyoteOutputStream.null=출력 ë²„í¼ ê°ì²´ëŠ” ì´ë¯¸ 제거ë˜ì—ˆê³  ì´ facadeì— ë” ì´ìƒ ì—°ê²°ë˜ì–´ 있지 않습니다. + +coyoteRequest.alreadyAuthenticated=해당 ìš”ì²­ì€ ì´ë¯¸ ì¸ì¦ë˜ì—ˆìŠµë‹ˆë‹¤. +coyoteRequest.attributeEvent=ì†ì„± ì´ë²¤íŠ¸ ë¦¬ìŠ¤ë„ˆì— ì˜í•´ 예외 ë°œìƒ +coyoteRequest.authenticate.ise=ì‘ë‹µì´ ì»¤ë°‹ëœ í›„ì—는 authenticate()를 호출할 수 없습니다. +coyoteRequest.changeSessionId=세션 ID를 변경할 수 없습니다. ì´ ìš”ì²­ê³¼ ì—°ê´€ëœ ì„¸ì…˜ì´ ì—†ìŠµë‹ˆë‹¤. +coyoteRequest.chunkedPostTooLarge=í¬ìŠ¤íŠ¸ëœ ë°ì´í„°ì˜ í¬ê¸°ê°€ 너무 커서 파ë¼ë¯¸í„°ë“¤ì´ 파싱ë˜ì§€ 않았습니다. ì´ ìš”ì²­ì€ chunked request였기 ë•Œë¬¸ì— ë” ì´ìƒ ì²˜ë¦¬ë  ìˆ˜ 없었습니다. ë§Œì¼ ì• í”Œë¦¬ì¼€ì´ì…˜ì´ 매우 í° í¬ìŠ¤íŠ¸ ë°ì´í„°ë¥¼ 받아들여야 한다면, Connectorì˜ maxPostSize ì†ì„±ì„ 사용하여 ì´ ë¬¸ì œë¥¼ 해결하십시오. +coyoteRequest.filterAsyncSupportUnknown=ì–´ë–¤ í•„í„°ë“¤ì´ ë¹„ë™ê¸° 처리를 지ì›í•˜ì§€ 않는지 여부를 ê²°ì •í•  수 없습니다. +coyoteRequest.getContextPath.ise=Canonical 컨í…스트 경로 [{0}]ì´(ê°€), User Agent [{1}]ì— ì˜í•´ í‘œì‹œëœ URI와 부합ë˜ì§€ 않습니다. +coyoteRequest.getInputStream.ise=ì´ ìš”ì²­ì— ëŒ€í•´ getReader()ê°€ ì´ë¯¸ 호출ë˜ì—ˆìŠµë‹ˆë‹¤. +coyoteRequest.getReader.ise=getInputStream()ì´ ì´ë¯¸ ì´ ìš”ì²­ì„ ìœ„í•´ 호출ë˜ì—ˆìŠµë‹ˆë‹¤. +coyoteRequest.gssLifetimeFail=ì‚¬ìš©ìž principal [{0}]ì˜ ë‚¨ì•„ìžˆëŠ” lifetimeì„ êµ¬í•˜ì§€ 못했습니다. +coyoteRequest.maxPostSizeExceeded=Multi-part 요청ì´, ì—°ê´€ëœ connectorì— ì„¤ì •ëœ maxPostSizeì˜ í•œê³„ì¹˜ë¥¼ 초과하는 파ë¼ë¯¸í„° ë°ì´í„°(ì—…ë¡œë“œëœ íŒŒì¼ë“¤ì€ 제외)를 í¬í•¨í–ˆìŠµë‹ˆë‹¤. +coyoteRequest.noAsync=요청 처리 ì²´ì¸ ë‚´ì˜ ë‹¤ìŒ í´ëž˜ìŠ¤ë“¤ì´ 비ë™ê¸° 모드를 지ì›í•˜ì§€ 않기 때문ì—, 비ë™ê¸° 모드를 시작할 수 없습니다: [{0}] +coyoteRequest.noMultipartConfig=ì–´ë–¤ multi-part ì„¤ì •ë„ ì œê³µë˜ì§€ 않았기 때문ì—, partë“¤ì„ ì²˜ë¦¬í•  수 없습니다. +coyoteRequest.parseParameters=í¬ìŠ¤íŠ¸ëœ 파ë¼ë¯¸í„°ë“¤ì„ 처리하는 중 예외 ë°œìƒ +coyoteRequest.postTooLarge=í¬ìŠ¤íŠ¸ëœ ë°ì´í„°ì˜ í¬ê¸°ê°€ 너무 커서, 파ë¼ë¯¸í„°ë“¤ì´ 파싱ë˜ì§€ 않았습니다. ë§Œì¼ ì• í”Œë¦¬ì¼€ì´ì…˜ì´ ëŒ€ëŸ‰ì˜ í¬ìŠ¤íŠ¸ ë°ì´í„°ë¥¼ 받아들여야 하는 경우, Connectorì˜ maxPostSize ì†ì„±ì„ 설정하여 문제를 해결하십시오. +coyoteRequest.sendfileNotCanonical=sendfileê³¼ 사용ë˜ë„ë¡ ì§€ì •ëœ íŒŒì¼ [{0}]ì˜ canonical ì´ë¦„ì„ ê²°ì •í•  수 없습니다. +coyoteRequest.sessionCreateCommitted=ì‘ë‹µì´ ì´ë¯¸ ì»¤ë°‹ëœ í›„ì—는, ì„¸ì…˜ì„ ìƒì„±í•  수 없습니다. +coyoteRequest.sessionEndAccessFail=ìš”ì²­ì„ ì°¸ì¡° 해제하는 과정ì—ì„œ, ì„¸ì…˜ì— ëŒ€í•œ ì ‘ê·¼ì„ ì¢…ë£Œì‹œí‚¤ë ¤ 개시하는 중 예외 ë°œìƒ +coyoteRequest.setAttribute.namenull=ë„ì¸ ì´ë¦„ì„ ì‚¬ìš©í•˜ì—¬ setAttribute를 호출할 수 없습니다. +coyoteRequest.trailersNotReady=isTrailerFieldsReady()ê°€ true를 반환하기 전까지는, getTrailerFields()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +coyoteRequest.uploadCreate=서블릿 [{1}]ì— ì˜í•´ 요구ë˜ëŠ”, ìž„ì‹œ 업로드 í´ë”를 [{0}] ìœ„ì¹˜ì— ìƒì„±í•©ë‹ˆë‹¤. +coyoteRequest.uploadCreateFail=[{0}]ì— ì—…ë¡œë“œ í´ë”를 ìƒì„±í•˜ì§€ 못했습니다. +coyoteRequest.uploadLocationInvalid=ìž„ì‹œ íŒŒì¼ ì—…ë¡œë“œ 위치 [{0}]ì€(는) 유효하지 않습니다. + +coyoteResponse.encoding.invalid=ì¸ì½”딩 [{0}]ì€(는) JREì— ì˜í•´ ì¸ì‹ë˜ì§€ 않는 것입니다. +coyoteResponse.getOutputStream.ise=ì´ ì‘ë‹µì— ëŒ€í•´ getWriter()ê°€ ì´ë¯¸ 호출ë˜ì—ˆìŠµë‹ˆë‹¤. +coyoteResponse.getWriter.ise=ì´ ì‘ë‹µì„ ìœ„í•´ getOutputStream()ì´ ì´ë¯¸ 호출ë˜ì—ˆìŠµë‹ˆë‹¤. +coyoteResponse.reset.ise=ì‘ë‹µì´ ì´ë¯¸ ì»¤ë°‹ëœ í›„ì—는, reset()ì„ í˜¸ì¶œí•  수 없습니다. +coyoteResponse.resetBuffer.ise=ì‘ë‹µì´ ì´ë¯¸ ì»¤ë°‹ëœ í›„ì—는, 버í¼ë¥¼ 재설정(reset)í•  수 없습니다. +coyoteResponse.sendError.ise=ì‘ë‹µì´ ì´ë¯¸ ì»¤ë°‹ëœ í›„ì—는 sendError()를 호출할 수 없습니다. +coyoteResponse.sendRedirect.ise=ì‘ë‹µì´ ì´ë¯¸ ì»¤ë°‹ëœ í›„ì—는, sendRedirect()를 호출할 수 없습니다. +coyoteResponse.sendRedirect.note=

    Redirecting to {0}

    +coyoteResponse.setBufferSize.ise=ë°ì´í„°ê°€ ì´ë¯¸ 쓰여진 후ì—는, ë²„í¼ í¬ê¸°ë¥¼ 변경할 수 없습니다. + +inputBuffer.requiresNonBlocking=Non blocking 모드ì—서는 가용하지 않습니다. +inputBuffer.streamClosed=ìŠ¤íŠ¸ë¦¼ì´ ë‹«í˜”ìŠµë‹ˆë‹¤. + +outputBuffer.writeNull=write(String,int,int) ë©”ì†Œë“œì— ì „ë‹¬ë˜ëŠ” String 아규먼트는 ë„ì¼ ìˆ˜ 없습니다. + +request.asyncNotSupported=현재 ì²´ì¸ì˜ í•„í„° ë˜ëŠ” 서블릿ì´, 비ë™ê¸° 오í¼ë ˆì´ì…˜ë“¤ì„ 지ì›í•˜ì§€ 않습니다. +request.fragmentInDispatchPath=디스패치 경로 [{0}](으)로부터 URI fragment를 제거했습니다. +request.illegalWrap=요청 wrapper는 반드시 getRequest()로부터 얻어진 ìš”ì²­ì„ wrap해야 합니다. +request.notAsync=ë§Œì¼ í˜„ìž¬ì˜ ì“°ë ˆë“œê°€ 비ë™ê¸° ëª¨ë“œì— ìžˆì§€ 않다면, ì´ ë©”ì†Œë“œë¥¼ 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (즉, isAsyncStarted()ê°€ false를 반환하는 경우) +request.session.failed=ë‹¤ìŒ ì˜¤ë¥˜ë¡œ ì¸í•´ 세션 [{0}]ì„(를) 로드하지 못했습니다: [{1}] + +requestFacade.nullRequest=요청 ê°ì²´ê°€ ì´ë¯¸ 참조 í•´ì œ ë˜ì—ˆê³ , ë” ì´ìƒ ì´ facade와 ì—°ê´€ë˜ì§€ 않습니다. + +response.illegalWrap=ì‘답 wrapper는, 반드시 getResponse()로부터 얻어진 ì‘답 ê°ì²´ë¥¼ wrapí•œ 것ì´ì–´ì•¼ 합니다. +response.sendRedirectFail=[{0}](으)ë¡œ redirect하지 못했습니다. + +responseFacade.nullResponse=해당 ì‘답 ê°ì²´ê°€ ì´ë¯¸ 참조 í•´ì œë˜ì—ˆìœ¼ë©°, ë” ì´ìƒ ì´ ResponseFacade ê°ì²´ì™€ ì—°ê´€ì´ ì—†ìŠµë‹ˆë‹¤. diff --git a/java/org/apache/catalina/connector/LocalStrings_pt_BR.properties b/java/org/apache/catalina/connector/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..ce9eded --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +coyoteRequest.noMultipartConfig=Impossível processar partes já que não há configuração de multi-part diff --git a/java/org/apache/catalina/connector/LocalStrings_ru.properties b/java/org/apache/catalina/connector/LocalStrings_ru.properties new file mode 100644 index 0000000..5de1419 --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_ru.properties @@ -0,0 +1,26 @@ +# 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. + +coyoteAdapter.debug=ÐŸÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ [{0}] имеет значение [{1}] + +coyoteConnector.invalidEncoding=\n\ +Кодировка [{0}] не раÑпознана JRE. Коннектор продолжит иÑпользовать [{1}] +coyoteConnector.invalidPort=Коннектор не может быть запущен так как указанное значение порта [{0}] не корректно +coyoteConnector.parseBodyMethodNoTrace=Метод TRACE ÐЕ ДОЛЖЕРвключать в ÑÐµÐ±Ñ ÑущноÑÑ‚ÑŒ (Ñмотри RFC 2616 CÐµÐºÑ†Ð¸Ñ 9.6) + +coyoteInputStream.nbNotready=Ð’ неблокирующем режиме невозможно читать из ServletInputStream до тех пор пока не завершитÑÑ Ð¿Ñ€ÐµÐ´Ñ‹Ð´ÑƒÑ‰ÐµÐµ чтение и IsReady() не вернёт true + +coyoteRequest.sendfileNotCanonical=Ðевозможно определить каноничеÑкое Ð¸Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð° [{0}] указанное Ð´Ð»Ñ Ð¸ÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ sendfile +coyoteRequest.sessionEndAccessFail=ИÑключение вызвало прекращение доÑтупа к ÑеÑÑии при очиÑтке запроÑа diff --git a/java/org/apache/catalina/connector/LocalStrings_zh_CN.properties b/java/org/apache/catalina/connector/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..2da544f --- /dev/null +++ b/java/org/apache/catalina/connector/LocalStrings_zh_CN.properties @@ -0,0 +1,97 @@ +# 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. + +coyoteAdapter.accesslogFail=å°è¯•å‘访问日志添加æ¡ç›®æ—¶å‘生异常。 +coyoteAdapter.asyncDispatch=处ç†å¼‚步请求时å‘生异常 +coyoteAdapter.authenticate=连接器æ供的ç»è¿‡èº«ä»½éªŒè¯çš„用户[{0}]。 +coyoteAdapter.authorize=(:使用Tomcat的领域授æƒç”¨æˆ·[{0}] +coyoteAdapter.checkRecycled.request=é‡åˆ°æœªå›žæ”¶çš„请求并强制回收。 +coyoteAdapter.checkRecycled.response=é‡åˆ°éžå›žæ”¶çš„相应并强行回收。 +coyoteAdapter.connect=ä¸æ”¯æŒCONNECT方法的HTTP请求 +coyoteAdapter.debug=å˜é‡[{0}]的值为[{1}]。 +coyoteAdapter.invalidURI=无效URI +coyoteAdapter.invalidURIWithMessage=无效URI:[{0}] +coyoteAdapter.nullRequest=异步分派åªèƒ½åœ¨çŽ°æœ‰è¯·æ±‚上å‘生 +coyoteAdapter.trace=ä¸å…许使用TRACE方法 + +coyoteConnector.invalidEncoding=ç¼–ç  [{0}] ä¸èƒ½è¢« JRE 识别,Connector 将继续使用 [{1}] +coyoteConnector.invalidPort=连接器ä¸èƒ½å¯åŠ¨ï¼Œå› ä¸ºæŒ‡å®šçš„ç«¯å£ [{0}]无效 +coyoteConnector.notAsciiSuperset=ç¼–ç [{0}]ä¸æ˜¯RFC 7230è¦æ±‚çš„ASCII超集。连接器将继续使用[{1}] +coyoteConnector.parseBodyMethodNoTrace=方法TRACEç¦æ­¢åŒ…å«å®žä½“(详情查看RFC 2616 章节 9.6) +coyoteConnector.protocolHandlerDestroyFailed=å议处ç†ç¨‹åºé”€æ¯å¤±è´¥ +coyoteConnector.protocolHandlerInitializationFailed=å议处ç†ç¨‹åºåˆå§‹åŒ–失败 +coyoteConnector.protocolHandlerInstantiationFailed=å议处ç†ç¨‹åºå®žä¾‹åŒ–失败 +coyoteConnector.protocolHandlerPauseFailed=å议处ç†ç¨‹åºæš‚åœå¤±è´¥ +coyoteConnector.protocolHandlerResumeFailed=å议处ç†ç¨‹åºæ¢å¤å¤±è´¥ +coyoteConnector.protocolHandlerStartFailed=å议处ç†å™¨å¯åŠ¨å¤±è´¥ +coyoteConnector.protocolHandlerStopFailed=å议处ç†ç¨‹åº.åœæ­¢å¤±è´¥ + +coyoteInputStream.nbNotready=在éžé˜»å¡žæ¨¡å¼ä¸‹ï¼Œåªæœ‰ä¹‹å‰çš„读数æ®å®Œæˆï¼Œå¹¶ä¸”isReady()方法返回true,你æ‰å¯ä»¥ä½¿ç”¨ ServletInputStream 读å–æ•°æ® +coyoteInputStream.null=输入缓冲对象已被回收,ä¸å†ä¸Žæ­¤å¤–观关è”\n + +coyoteOutputStream.nbNotready=在éžé˜»å¡žæ¨¡å¼ä¸‹ï¼Œåœ¨ä¸Šä¸€æ¬¡å†™å…¥å®Œæˆä¸”isReady()返回true之å‰ï¼Œæ‚¨ä¸èƒ½å†™å…¥ServletOutputStream +coyoteOutputStream.null=输出缓冲对象已被回收,ä¸å†ä¸Žæ­¤ '外观' å…³è” + +coyoteRequest.alreadyAuthenticated=æ­¤è¯·æ±‚å·²é€šè¿‡èº«ä»½éªŒè¯ +coyoteRequest.attributeEvent=属性事件侦å¬å™¨å¼•å‘的异常 +coyoteRequest.authenticate.ise=):æ交å“应åŽæ— æ³•è°ƒç”¨authenticate() +coyoteRequest.changeSessionId=无法更改 session ID。 没有与此请求关è”çš„ session。 +coyoteRequest.chunkedPostTooLarge=由于请求å‚æ•°æ•°æ®å¤ªå¤§ï¼Œå¯¼è‡´å‚æ•°ä¸èƒ½è§£æžã€‚因为当å‰è¯·æ±‚是å—状请求,åŽç»­ä¹Ÿä¸ä¼šå¤„ç†ã€‚如果应用程åºéœ€è¦æŽ¥æ”¶å¤§çš„POST请求,å¯ä»¥ä½¿ç”¨è¿žæŽ¥å™¨çš„maxPostSize解决它。 +coyoteRequest.filterAsyncSupportUnknown=无法确定是å¦æœ‰ä»»ä½•è¿‡æ»¤å™¨ä¸æ”¯æŒå¼‚æ­¥å¤„ç† +coyoteRequest.getContextPath.ise=找ä¸åˆ°è§„范上下文路径[{0}]与用户代ç†[{1}]æ供的URI之间的匹é…项。 +coyoteRequest.getInputStream.ise=已为此请求调用getReader() +coyoteRequest.getReader.ise=当å‰è¯·æ±‚å·²ç»è°ƒç”¨è¿‡æ–¹æ³•getInputStream() +coyoteRequest.gssLifetimeFail=为用户主体 [{0}] 获å–剩余生命期失败 +coyoteRequest.maxPostSizeExceeded=):大多部分请求包å«çš„å‚æ•°æ•°æ®ï¼ˆä¸åŒ…括上载的文件)超过了关è”连接器上设置的maxPostSize çš„é™åˆ¶ +coyoteRequest.noAsync=无法å¯åŠ¨async,因为处ç†é“¾ä¸­çš„下列类ä¸æ”¯æŒasync[{0}] +coyoteRequest.noMultipartConfig=由于没有æä¾›multi-parté…置,无法处ç†parts +coyoteRequest.parseParameters=处ç†å‘布的å‚数时引å‘异常 +coyoteRequest.postTooLarge=未分æžå‚数,因为å‘布的数æ®å¤ªå¤§ã€‚如果应用程åºåº”接å—大型post,请使用连接器的maxPostSize属性æ¥è§£å†³æ­¤é—®é¢˜ã€‚ +coyoteRequest.sendfileNotCanonical=无法确定指定用于sendfile的文件[{0}]的规范å称 +coyoteRequest.sessionCreateCommitted=æ交å“应åŽæ— æ³•åˆ›å»ºä¼šè¯ +coyoteRequest.sessionEndAccessFail=在回收请求时,异常触å‘了对会è¯çš„结æŸè®¿é—®ã€‚ +coyoteRequest.setAttribute.namenull=ä¸èƒ½åœ¨ä¸€ä¸ªç©ºçš„å字上调用setAttribute +coyoteRequest.trailersNotReady=在isTrailerFieldsReady()返回true之å‰è°ƒç”¨getTrailerFields()是éžæ³•çš„ +coyoteRequest.uploadCreate=æ ¹æ®servlet[{1}]çš„è¦æ±‚创建临时上载ä½ç½®[{0}] +coyoteRequest.uploadCreateFail=无法创建上载ä½ç½®[{0}] +coyoteRequest.uploadLocationInvalid=临时上传路径[{0}]无效 + +coyoteResponse.encoding.invalid=JRE无法识别编ç [{0}] +coyoteResponse.getOutputStream.ise=已为此å“应调用getWriter() +coyoteResponse.getWriter.ise=当å‰å“应已ç»è°ƒç”¨äº†æ–¹æ³•getOutputStream() +coyoteResponse.reset.ise=å·²ç»æ交å“应åŽæ— æ³•è°ƒç”¨reset() +coyoteResponse.resetBuffer.ise=æ交å“应åŽæ— æ³•é‡ç½®ç¼“冲区 +coyoteResponse.sendError.ise=å“应æ交åŽæ— æ³•è°ƒç”¨sendError() +coyoteResponse.sendRedirect.ise=æ交å“应åŽæ— æ³•è°ƒç”¨sendRedirect()。 +coyoteResponse.sendRedirect.note=

    é‡å®šå‘到{0}

    +coyoteResponse.setBufferSize.ise=写入数æ®åŽæ— æ³•æ›´æ”¹ç¼“å†²åŒºå¤§å° + +inputBuffer.requiresNonBlocking=éžé˜»å¡žæ¨¡å¼ä¸‹ä¸å¯ç”¨ +inputBuffer.streamClosed=关闭的æµã€‚ + +outputBuffer.writeNull=è¦å†™å…¥çš„字符串å‚数(String,int,int)ä¸èƒ½ä¸ºç©º + +request.asyncNotSupported=当å‰é“¾çš„筛选器或servletä¸æ”¯æŒå¼‚æ­¥æ“作。 +request.fragmentInDispatchPath=调度路径[{0}]中的片段已被删除 +request.illegalWrap=请求包装器必须包装从getRequest()获得的请求 +request.notAsync=如果当å‰è¯·æ±‚ä¸åœ¨å¼‚步模å¼ä¸‹ï¼Œåˆ™è°ƒç”¨æ­¤æ–¹æ³•æ˜¯éžæ³•çš„(å³isAsyncStarted()返回false) +request.session.failed=由于[{1}],加载会è¯[{0}]失败 + +requestFacade.nullRequest=请求对象已被回收,ä¸å†ä¸Žæ­¤facadeå…³è” + +response.illegalWrap=å“应包装器必须包装从getResponse()获得的å“应 +response.sendRedirectFail=é‡å®šå‘到[{0}]失败 + +responseFacade.nullResponse=å“应对象已被回收,ä¸å†ä¸Žæ­¤å¤–è§‚å…³è” diff --git a/java/org/apache/catalina/connector/OutputBuffer.java b/java/org/apache/catalina/connector/OutputBuffer.java new file mode 100644 index 0000000..de6dbd3 --- /dev/null +++ b/java/org/apache/catalina/connector/OutputBuffer.java @@ -0,0 +1,869 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.io.Writer; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Globals; +import org.apache.coyote.ActionCode; +import org.apache.coyote.CloseNowException; +import org.apache.coyote.Response; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.C2BConverter; +import org.apache.tomcat.util.res.StringManager; + +/** + * The buffer used by Tomcat response. This is a derivative of the Tomcat 3.3 OutputBuffer, with the removal of some of + * the state handling (which in Coyote is mostly the Processor's responsibility). + * + * @author Costin Manolache + * @author Remy Maucherat + */ +public class OutputBuffer extends Writer { + + private static final StringManager sm = StringManager.getManager(OutputBuffer.class); + + public static final int DEFAULT_BUFFER_SIZE = 8 * 1024; + + /** + * Encoder cache. + */ + private final Map encoders = new HashMap<>(); + + + /** + * Default buffer size. + */ + private final int defaultBufferSize; + + // ----------------------------------------------------- Instance Variables + + /** + * The byte buffer. + */ + private ByteBuffer bb; + + + /** + * The char buffer. + */ + private final CharBuffer cb; + + + /** + * State of the output buffer. + */ + private boolean initial = true; + + + /** + * Number of bytes written. + */ + private long bytesWritten = 0; + + + /** + * Number of chars written. + */ + private long charsWritten = 0; + + + /** + * Flag which indicates if the output buffer is closed. + */ + private volatile boolean closed = false; + + + /** + * Do a flush on the next operation. + */ + private boolean doFlush = false; + + + /** + * Current char to byte converter. + */ + protected C2BConverter conv; + + + /** + * Associated Coyote response. + */ + private Response coyoteResponse; + + + /** + * Suspended flag. All output bytes will be swallowed if this is true. + */ + private volatile boolean suspended = false; + + + // ----------------------------------------------------------- Constructors + + /** + * Create the buffer with the specified initial size. + * + * @param size Buffer size to use + */ + public OutputBuffer(int size) { + defaultBufferSize = size; + bb = ByteBuffer.allocate(size); + clear(bb); + cb = CharBuffer.allocate(size); + clear(cb); + } + + + // ------------------------------------------------------------- Properties + + /** + * Associated Coyote response. + * + * @param coyoteResponse Associated Coyote response + */ + public void setResponse(Response coyoteResponse) { + this.coyoteResponse = coyoteResponse; + } + + + /** + * Is the response output suspended ? + * + * @return suspended flag value + */ + public boolean isSuspended() { + return this.suspended; + } + + + /** + * Set the suspended flag. + * + * @param suspended New suspended flag value + */ + public void setSuspended(boolean suspended) { + this.suspended = suspended; + } + + + /** + * Is the response output closed ? + * + * @return closed flag value + */ + public boolean isClosed() { + return this.closed; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Recycle the output buffer. + */ + public void recycle() { + + initial = true; + bytesWritten = 0; + charsWritten = 0; + + if (bb.capacity() > 16 * defaultBufferSize) { + // Discard buffers which are too large + bb = ByteBuffer.allocate(defaultBufferSize); + } + clear(bb); + clear(cb); + closed = false; + suspended = false; + doFlush = false; + + if (conv != null) { + conv.recycle(); + conv = null; + } + } + + + /** + * Close the output buffer. This tries to calculate the response size if the response has not been committed yet. + * + * @throws IOException An underlying IOException occurred + */ + @Override + public void close() throws IOException { + + if (closed) { + return; + } + if (suspended) { + return; + } + + // If there are chars, flush all of them to the byte buffer now as bytes are used to + // calculate the content-length (if everything fits into the byte buffer, of course). + if (cb.remaining() > 0) { + flushCharBuffer(); + } + + if ((!coyoteResponse.isCommitted()) && (coyoteResponse.getContentLengthLong() == -1)) { + // If this didn't cause a commit of the response, the final content + // length can be calculated. + coyoteResponse.setContentLength(bb.remaining()); + } + + if (coyoteResponse.getStatus() == HttpServletResponse.SC_SWITCHING_PROTOCOLS) { + doFlush(true); + } else { + doFlush(false); + } + closed = true; + + // The request should have been completely read by the time the response + // is closed. Further reads of the input a) are pointless and b) really + // confuse AJP (bug 50189) so close the input buffer to prevent them. + Request req = (Request) coyoteResponse.getRequest().getNote(CoyoteAdapter.ADAPTER_NOTES); + req.inputBuffer.close(); + + coyoteResponse.action(ActionCode.CLOSE, null); + } + + + /** + * Flush bytes or chars contained in the buffer. + * + * @throws IOException An underlying IOException occurred + */ + @Override + public void flush() throws IOException { + doFlush(true); + } + + + /** + * Flush bytes or chars contained in the buffer. + * + * @param realFlush true if this should also cause a real network flush + * + * @throws IOException An underlying IOException occurred + */ + protected void doFlush(boolean realFlush) throws IOException { + + if (suspended) { + return; + } + + try { + doFlush = true; + if (initial) { + coyoteResponse.sendHeaders(); + initial = false; + } + if (cb.remaining() > 0) { + flushCharBuffer(); + } + if (bb.remaining() > 0) { + flushByteBuffer(); + } + } finally { + doFlush = false; + } + + if (realFlush) { + coyoteResponse.action(ActionCode.CLIENT_FLUSH, null); + // If some exception occurred earlier, or if some IOE occurred + // here, notify the servlet with an IOE + if (coyoteResponse.isExceptionPresent()) { + throw new ClientAbortException(coyoteResponse.getErrorException()); + } + } + + } + + + // ------------------------------------------------- Bytes Handling Methods + + /** + * Sends the buffer data to the client output, checking the state of Response and calling the right interceptors. + * + * @param buf the ByteBuffer to be written to the response + * + * @throws IOException An underlying IOException occurred + */ + public void realWriteBytes(ByteBuffer buf) throws IOException { + + if (closed) { + return; + } + + // If we really have something to write + if (buf.remaining() > 0) { + // real write to the adapter + try { + coyoteResponse.doWrite(buf); + } catch (CloseNowException e) { + // Catch this sub-class as it requires specific handling. + // Examples where this exception is thrown: + // - HTTP/2 stream timeout + // Prevent further output for this response + closed = true; + throw e; + } catch (IOException e) { + // An IOException on a write is almost always due to + // the remote client aborting the request. Wrap this + // so that it can be handled better by the error dispatcher. + throw new ClientAbortException(e); + } + } + + } + + + public void write(byte b[], int off, int len) throws IOException { + + if (suspended) { + return; + } + + writeBytes(b, off, len); + + } + + + public void write(ByteBuffer from) throws IOException { + + if (suspended) { + return; + } + + writeBytes(from); + + } + + + private void writeBytes(byte b[], int off, int len) throws IOException { + + if (closed) { + return; + } + + append(b, off, len); + bytesWritten += len; + + // if called from within flush(), then immediately flush + // remaining bytes + if (doFlush) { + flushByteBuffer(); + } + + } + + + private void writeBytes(ByteBuffer from) throws IOException { + + if (closed) { + return; + } + + int remaining = from.remaining(); + append(from); + bytesWritten += remaining; + + // if called from within flush(), then immediately flush + // remaining bytes + if (doFlush) { + flushByteBuffer(); + } + + } + + + public void writeByte(int b) throws IOException { + + if (suspended) { + return; + } + + if (isFull(bb)) { + flushByteBuffer(); + } + + transfer((byte) b, bb); + bytesWritten++; + + } + + + // ------------------------------------------------- Chars Handling Methods + + + /** + * Convert the chars to bytes, then send the data to the client. + * + * @param from Char buffer to be written to the response + * + * @throws IOException An underlying IOException occurred + */ + public void realWriteChars(CharBuffer from) throws IOException { + + while (from.remaining() > 0) { + conv.convert(from, bb); + if (bb.remaining() == 0) { + // Break out of the loop if more chars are needed to produce any output + break; + } + if (from.remaining() > 0) { + flushByteBuffer(); + } else if (conv.isUndeflow() && bb.limit() > bb.capacity() - 4) { + // Handle an edge case. There are no more chars to write at the + // moment but there is a leftover character in the converter + // which must be part of a surrogate pair. The byte buffer does + // not have enough space left to output the bytes for this pair + // once it is complete )it will require 4 bytes) so flush now to + // prevent the bytes for the leftover char and the rest of the + // surrogate pair yet to be written from being lost. + // See TestOutputBuffer#testUtf8SurrogateBody() + flushByteBuffer(); + } + } + + } + + @Override + public void write(int c) throws IOException { + + if (suspended) { + return; + } + + if (isFull(cb)) { + flushCharBuffer(); + } + + transfer((char) c, cb); + charsWritten++; + + } + + + @Override + public void write(char c[]) throws IOException { + + if (suspended) { + return; + } + + write(c, 0, c.length); + + } + + + @Override + public void write(char c[], int off, int len) throws IOException { + + if (suspended) { + return; + } + + append(c, off, len); + charsWritten += len; + + } + + + /** + * Append a string to the buffer + */ + @Override + public void write(String s, int off, int len) throws IOException { + + if (suspended) { + return; + } + + if (s == null) { + throw new NullPointerException(sm.getString("outputBuffer.writeNull")); + } + + int sOff = off; + int sEnd = off + len; + while (sOff < sEnd) { + int n = transfer(s, sOff, sEnd - sOff, cb); + sOff += n; + if (sOff < sEnd && isFull(cb)) { + flushCharBuffer(); + } + } + + charsWritten += len; + } + + + @Override + public void write(String s) throws IOException { + + if (suspended) { + return; + } + + if (s == null) { + s = "null"; + } + write(s, 0, s.length()); + } + + + public void checkConverter() throws IOException { + if (conv != null) { + return; + } + + Charset charset = coyoteResponse.getCharset(); + + if (charset == null) { + if (coyoteResponse.getCharacterEncoding() != null) { + // setCharacterEncoding() was called with an invalid character set + // Trigger an UnsupportedEncodingException + charset = B2CConverter.getCharset(coyoteResponse.getCharacterEncoding()); + } + charset = org.apache.coyote.Constants.DEFAULT_BODY_CHARSET; + } + + conv = encoders.get(charset); + + if (conv == null) { + conv = createConverter(charset); + encoders.put(charset, conv); + } + } + + + private static C2BConverter createConverter(final Charset charset) throws IOException { + if (Globals.IS_SECURITY_ENABLED) { + try { + return AccessController.doPrivileged(new PrivilegedCreateConverter(charset)); + } catch (PrivilegedActionException ex) { + Exception e = ex.getException(); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new IOException(ex); + } + } + } else { + return new C2BConverter(charset); + } + } + + + // -------------------- BufferedOutputStream compatibility + + public long getContentWritten() { + return bytesWritten + charsWritten; + } + + /** + * Has this buffer been used at all? + * + * @return true if no chars or bytes have been added to the buffer since the last call to {@link #recycle()} + */ + public boolean isNew() { + return (bytesWritten == 0) && (charsWritten == 0); + } + + + public void setBufferSize(int size) { + if (size > bb.capacity()) { + bb = ByteBuffer.allocate(size); + clear(bb); + } + } + + + public void reset() { + reset(false); + } + + public void reset(boolean resetWriterStreamFlags) { + clear(bb); + clear(cb); + bytesWritten = 0; + charsWritten = 0; + if (resetWriterStreamFlags) { + if (conv != null) { + conv.recycle(); + } + conv = null; + } + initial = true; + } + + + public int getBufferSize() { + return bb.capacity(); + } + + + /* + * All the non-blocking write state information is held in the Response so it is visible / accessible to all the + * code that needs it. + */ + + public boolean isReady() { + return coyoteResponse.isReady(); + } + + + public void setWriteListener(WriteListener listener) { + coyoteResponse.setWriteListener(listener); + } + + + public boolean isBlocking() { + return coyoteResponse.getWriteListener() == null; + } + + public void checkRegisterForWrite() { + coyoteResponse.checkRegisterForWrite(); + } + + /** + * Add data to the buffer. + * + * @param src Bytes array + * @param off Offset + * @param len Length + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void append(byte src[], int off, int len) throws IOException { + if (bb.remaining() == 0) { + appendByteArray(src, off, len); + } else { + int n = transfer(src, off, len, bb); + len = len - n; + off = off + n; + if (len > 0 && isFull(bb)) { + flushByteBuffer(); + appendByteArray(src, off, len); + } + } + } + + /** + * Add data to the buffer. + * + * @param src Char array + * @param off Offset + * @param len Length + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void append(char src[], int off, int len) throws IOException { + // if we have limit and we're below + if (len <= cb.capacity() - cb.limit()) { + transfer(src, off, len, cb); + return; + } + + // Optimization: + // If len-avail < length ( i.e. after we fill the buffer with + // what we can, the remaining will fit in the buffer ) we'll just + // copy the first part, flush, then copy the second part - 1 write + // and still have some space for more. We'll still have 2 writes, but + // we write more on the first. + if (len + cb.limit() < 2 * cb.capacity()) { + /* + * If the request length exceeds the size of the output buffer, flush the output buffer and then write the + * data directly. We can't avoid 2 writes, but we can write more on the second + */ + int n = transfer(src, off, len, cb); + + flushCharBuffer(); + + transfer(src, off + n, len - n, cb); + } else { + // long write - flush the buffer and write the rest + // directly from source + flushCharBuffer(); + + realWriteChars(CharBuffer.wrap(src, off, len)); + } + } + + + public void append(ByteBuffer from) throws IOException { + if (bb.remaining() == 0) { + appendByteBuffer(from); + } else { + transfer(from, bb); + if (from.hasRemaining() && isFull(bb)) { + flushByteBuffer(); + appendByteBuffer(from); + } + } + } + + + public void setErrorException(Exception e) { + coyoteResponse.setErrorException(e); + } + + + private void appendByteArray(byte src[], int off, int len) throws IOException { + if (len == 0) { + return; + } + + int limit = bb.capacity(); + while (len > limit) { + realWriteBytes(ByteBuffer.wrap(src, off, limit)); + len = len - limit; + off = off + limit; + } + + if (len > 0) { + transfer(src, off, len, bb); + } + } + + private void appendByteBuffer(ByteBuffer from) throws IOException { + if (from.remaining() == 0) { + return; + } + + int limit = bb.capacity(); + int fromLimit = from.limit(); + while (from.remaining() > limit) { + from.limit(from.position() + limit); + realWriteBytes(from.slice()); + from.position(from.limit()); + from.limit(fromLimit); + } + + if (from.remaining() > 0) { + transfer(from, bb); + } + } + + private void flushByteBuffer() throws IOException { + realWriteBytes(bb.slice()); + clear(bb); + } + + private void flushCharBuffer() throws IOException { + realWriteChars(cb.slice()); + clear(cb); + } + + private void transfer(byte b, ByteBuffer to) { + toWriteMode(to); + to.put(b); + toReadMode(to); + } + + private void transfer(char b, CharBuffer to) { + toWriteMode(to); + to.put(b); + toReadMode(to); + } + + private int transfer(byte[] buf, int off, int len, ByteBuffer to) { + toWriteMode(to); + int max = Math.min(len, to.remaining()); + if (max > 0) { + to.put(buf, off, max); + } + toReadMode(to); + return max; + } + + private int transfer(char[] buf, int off, int len, CharBuffer to) { + toWriteMode(to); + int max = Math.min(len, to.remaining()); + if (max > 0) { + to.put(buf, off, max); + } + toReadMode(to); + return max; + } + + private int transfer(String s, int off, int len, CharBuffer to) { + toWriteMode(to); + int max = Math.min(len, to.remaining()); + if (max > 0) { + to.put(s, off, off + max); + } + toReadMode(to); + return max; + } + + private void transfer(ByteBuffer from, ByteBuffer to) { + toWriteMode(to); + int max = Math.min(from.remaining(), to.remaining()); + if (max > 0) { + int fromLimit = from.limit(); + from.limit(from.position() + max); + to.put(from); + from.limit(fromLimit); + } + toReadMode(to); + } + + private void clear(Buffer buffer) { + buffer.rewind().limit(0); + } + + private boolean isFull(Buffer buffer) { + return buffer.limit() == buffer.capacity(); + } + + private void toReadMode(Buffer buffer) { + buffer.limit(buffer.position()).reset(); + } + + private void toWriteMode(Buffer buffer) { + buffer.mark().position(buffer.limit()).limit(buffer.capacity()); + } + + + private static class PrivilegedCreateConverter implements PrivilegedExceptionAction { + + private final Charset charset; + + PrivilegedCreateConverter(Charset charset) { + this.charset = charset; + } + + @Override + public C2BConverter run() throws IOException { + return new C2BConverter(charset); + } + } +} diff --git a/java/org/apache/catalina/connector/Request.java b/java/org/apache/catalina/connector/Request.java new file mode 100644 index 0000000..234275e --- /dev/null +++ b/java/org/apache/catalina/connector/Request.java @@ -0,0 +1,3251 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.naming.NamingException; +import javax.security.auth.Subject; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterChain; +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConnection; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestAttributeEvent; +import jakarta.servlet.ServletRequestAttributeListener; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.Part; +import jakarta.servlet.http.PushBuilder; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.Manager; +import org.apache.catalina.Realm; +import org.apache.catalina.Session; +import org.apache.catalina.TomcatPrincipal; +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.ApplicationFilterChain; +import org.apache.catalina.core.ApplicationMapping; +import org.apache.catalina.core.ApplicationPart; +import org.apache.catalina.core.ApplicationPushBuilder; +import org.apache.catalina.core.ApplicationSessionCookieConfig; +import org.apache.catalina.core.AsyncContextImpl; +import org.apache.catalina.mapper.MappingData; +import org.apache.catalina.util.ParameterMap; +import org.apache.catalina.util.RequestUtil; +import org.apache.catalina.util.TLSUtil; +import org.apache.catalina.util.URLEncoder; +import org.apache.coyote.ActionCode; +import org.apache.coyote.UpgradeToken; +import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.EncodedSolidusHandling; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.http.CookieProcessor; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.Parameters; +import org.apache.tomcat.util.http.Parameters.FailReason; +import org.apache.tomcat.util.http.Rfc6265CookieProcessor; +import org.apache.tomcat.util.http.ServerCookie; +import org.apache.tomcat.util.http.ServerCookies; +import org.apache.tomcat.util.http.fileupload.FileItem; +import org.apache.tomcat.util.http.fileupload.FileUpload; +import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory; +import org.apache.tomcat.util.http.fileupload.impl.InvalidContentTypeException; +import org.apache.tomcat.util.http.fileupload.impl.SizeException; +import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext; +import org.apache.tomcat.util.http.parser.AcceptLanguage; +import org.apache.tomcat.util.http.parser.Upgrade; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.res.StringManager; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; + +/** + * Wrapper object for the Coyote request. + * + * @author Remy Maucherat + * @author Craig R. McClanahan + */ +public class Request implements HttpServletRequest { + + private static final String HTTP_UPGRADE_HEADER_NAME = "upgrade"; + + private static final Log log = LogFactory.getLog(Request.class); + + /** + * Create a new Request object associated with the given Connector. + * + * @param connector The Connector with which this Request object will always be associated. In normal usage this + * must be non-null. In some test scenarios, it may be possible to use a null Connector without + * triggering an NPE. + */ + public Request(Connector connector) { + this.connector = connector; + } + + + // ------------------------------------------------------------- Properties + + + /** + * Coyote request. + */ + protected org.apache.coyote.Request coyoteRequest; + + /** + * Set the Coyote request. + * + * @param coyoteRequest The Coyote request + */ + public void setCoyoteRequest(org.apache.coyote.Request coyoteRequest) { + this.coyoteRequest = coyoteRequest; + inputBuffer.setRequest(coyoteRequest); + } + + /** + * Get the Coyote request. + * + * @return the Coyote request object + */ + public org.apache.coyote.Request getCoyoteRequest() { + return this.coyoteRequest; + } + + + // ----------------------------------------------------- Variables + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Request.class); + + + /** + * The set of cookies associated with this Request. + */ + protected Cookie[] cookies = null; + + + /** + * The default Locale if none are specified. + */ + protected static final Locale defaultLocale = Locale.getDefault(); + + + /** + * The attributes associated with this Request, keyed by attribute name. + */ + private final Map attributes = new ConcurrentHashMap<>(); + + + /** + * Flag that indicates if SSL attributes have been parsed to improve performance for applications (usually + * frameworks) that make multiple calls to {@link Request#getAttributeNames()}. + */ + protected boolean sslAttributesParsed = false; + + + /** + * The preferred Locales associated with this Request. + */ + protected final ArrayList locales = new ArrayList<>(); + + + /** + * Internal notes associated with this request by Catalina components and event listeners. + */ + private final transient HashMap notes = new HashMap<>(); + + + /** + * Authentication type. + */ + protected String authType = null; + + + /** + * The current dispatcher type. + */ + protected DispatcherType internalDispatcherType = null; + + + /** + * The associated input buffer. + */ + protected final InputBuffer inputBuffer = new InputBuffer(); + + + /** + * ServletInputStream. + */ + protected CoyoteInputStream inputStream = new CoyoteInputStream(inputBuffer); + + + /** + * Reader. + */ + protected CoyoteReader reader = new CoyoteReader(inputBuffer); + + + /** + * Using stream flag. + */ + protected boolean usingInputStream = false; + + + /** + * Using reader flag. + */ + protected boolean usingReader = false; + + + /** + * User principal. + */ + protected Principal userPrincipal = null; + + + /** + * Request parameters parsed flag. + */ + protected boolean parametersParsed = false; + + + /** + * Cookie headers parsed flag. Indicates that the cookie headers have been parsed into ServerCookies. + */ + protected boolean cookiesParsed = false; + + + /** + * Cookie parsed flag. Indicates that the ServerCookies have been converted into user facing Cookie objects. + */ + protected boolean cookiesConverted = false; + + + /** + * Secure flag. + */ + protected boolean secure = false; + + + /** + * The Subject associated with the current AccessControlContext + */ + protected transient Subject subject = null; + + + /** + * Post data buffer. + */ + protected static final int CACHED_POST_LEN = 8192; + protected byte[] postData = null; + + + /** + * Hash map used in the getParametersMap method. + */ + protected ParameterMap parameterMap = new ParameterMap<>(); + + + /** + * The parts, if any, uploaded with this request. + */ + protected Collection parts = null; + + + /** + * The exception thrown, if any when parsing the parts. + */ + protected Exception partsParseException = null; + + + /** + * The currently active session for this request. + */ + protected Session session = null; + + + /** + * The current request dispatcher path. + */ + protected Object requestDispatcherPath = null; + + + /** + * Was the requested session ID received in a cookie? + */ + protected boolean requestedSessionCookie = false; + + + /** + * The requested session ID (if any) for this request. + */ + protected String requestedSessionId = null; + + + /** + * Was the requested session ID received in a URL? + */ + protected boolean requestedSessionURL = false; + + + /** + * Was the requested session ID obtained from the SSL session? + */ + protected boolean requestedSessionSSL = false; + + + /** + * Parse locales. + */ + protected boolean localesParsed = false; + + + /** + * Local port + */ + protected int localPort = -1; + + /** + * Remote address. + */ + protected String remoteAddr = null; + + + /** + * Connection peer address. + */ + protected String peerAddr = null; + + + /** + * Remote host. + */ + protected String remoteHost = null; + + + /** + * Remote port + */ + protected int remotePort = -1; + + /** + * Local address + */ + protected String localAddr = null; + + + /** + * Local address + */ + protected String localName = null; + + /** + * AsyncContext + */ + private volatile AsyncContextImpl asyncContext = null; + + protected Boolean asyncSupported = null; + + private HttpServletRequest applicationRequest = null; + + + // --------------------------------------------------------- Public Methods + + protected void addPathParameter(String name, String value) { + coyoteRequest.addPathParameter(name, value); + } + + protected String getPathParameter(String name) { + return coyoteRequest.getPathParameter(name); + } + + public void setAsyncSupported(boolean asyncSupported) { + this.asyncSupported = Boolean.valueOf(asyncSupported); + } + + /** + * Release all object references, and initialize instance variables, in preparation for reuse of this object. + */ + public void recycle() { + + internalDispatcherType = null; + requestDispatcherPath = null; + + authType = null; + inputBuffer.recycle(); + usingInputStream = false; + usingReader = false; + userPrincipal = null; + subject = null; + parametersParsed = false; + if (parts != null) { + for (Part part : parts) { + try { + part.delete(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("coyoteRequest.deletePartFailed", part.getName()), t); + } + } + parts = null; + } + partsParseException = null; + locales.clear(); + localesParsed = false; + secure = false; + remoteAddr = null; + peerAddr = null; + remoteHost = null; + remotePort = -1; + localPort = -1; + localAddr = null; + localName = null; + + attributes.clear(); + sslAttributesParsed = false; + notes.clear(); + + recycleSessionInfo(); + recycleCookieInfo(false); + + if (getDiscardFacades()) { + parameterMap = new ParameterMap<>(); + } else { + parameterMap.setLocked(false); + parameterMap.clear(); + } + + mappingData.recycle(); + applicationMapping.recycle(); + + applicationRequest = null; + if (getDiscardFacades()) { + if (facade != null) { + facade.clear(); + facade = null; + } + if (inputStream != null) { + inputStream.clear(); + inputStream = null; + } + if (reader != null) { + reader.clear(); + reader = null; + } + } + + asyncSupported = null; + if (asyncContext != null) { + asyncContext.recycle(); + asyncContext = null; + } + } + + + protected void recycleSessionInfo() { + if (session != null) { + try { + session.endAccess(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("coyoteRequest.sessionEndAccessFail"), t); + } + } + session = null; + requestedSessionCookie = false; + requestedSessionId = null; + requestedSessionURL = false; + requestedSessionSSL = false; + } + + + protected void recycleCookieInfo(boolean recycleCoyote) { + cookiesParsed = false; + cookiesConverted = false; + cookies = null; + if (recycleCoyote) { + getCoyoteRequest().getCookies().recycle(); + } + } + + + // -------------------------------------------------------- Request Methods + + /** + * Associated Catalina connector. + */ + protected final Connector connector; + + /** + * @return the Connector through which this Request was received. + */ + public Connector getConnector() { + return this.connector; + } + + + /** + * Return the Context within which this Request is being processed. + *

    + * This is available as soon as the appropriate Context is identified. Note that availability of a Context allows + * getContextPath() to return a value, and thus enables parsing of the request URI. + * + * @return the Context mapped with the request + */ + public Context getContext() { + return mappingData.context; + } + + + /** + * Get the recycling strategy of the facade objects. + * + * @return the value of the flag as set on the connector, or true if no connector is associated with + * this request + */ + public boolean getDiscardFacades() { + return (connector == null) ? true : connector.getDiscardFacades(); + } + + + /** + * Filter chain associated with the request. + */ + protected FilterChain filterChain = null; + + /** + * Get filter chain associated with the request. + * + * @return the associated filter chain + */ + public FilterChain getFilterChain() { + return this.filterChain; + } + + /** + * Set filter chain associated with the request. + * + * @param filterChain new filter chain + */ + public void setFilterChain(FilterChain filterChain) { + this.filterChain = filterChain; + } + + + /** + * @return the Host within which this Request is being processed. + */ + public Host getHost() { + return mappingData.host; + } + + + /** + * Mapping data. + */ + protected final MappingData mappingData = new MappingData(); + private final ApplicationMapping applicationMapping = new ApplicationMapping(mappingData); + + /** + * @return mapping data. + */ + public MappingData getMappingData() { + return mappingData; + } + + + /** + * The facade associated with this request. + */ + protected RequestFacade facade = null; + + + /** + * @return the ServletRequest for which this object is the facade. This method must be implemented by a + * subclass. + */ + public HttpServletRequest getRequest() { + if (facade == null) { + facade = new RequestFacade(this); + } + if (applicationRequest == null) { + applicationRequest = facade; + } + return applicationRequest; + } + + + /** + * Set a wrapped HttpServletRequest to pass to the application. Components wishing to wrap the request should obtain + * the request via {@link #getRequest()}, wrap it and then call this method with the wrapped request. + * + * @param applicationRequest The wrapped request to pass to the application + */ + public void setRequest(HttpServletRequest applicationRequest) { + // Check the wrapper wraps this request + ServletRequest r = applicationRequest; + while (r instanceof HttpServletRequestWrapper) { + r = ((HttpServletRequestWrapper) r).getRequest(); + } + if (r != facade) { + throw new IllegalArgumentException(sm.getString("request.illegalWrap")); + } + this.applicationRequest = applicationRequest; + } + + + /** + * The response with which this request is associated. + */ + protected Response response = null; + + /** + * @return the Response with which this Request is associated. + */ + public Response getResponse() { + return this.response; + } + + /** + * Set the Response with which this Request is associated. + * + * @param response The new associated response + */ + public void setResponse(Response response) { + this.response = response; + } + + /** + * @return the input stream associated with this Request. + */ + public InputStream getStream() { + if (inputStream == null) { + inputStream = new CoyoteInputStream(inputBuffer); + } + return inputStream; + } + + /** + * URI byte to char converter. + */ + protected B2CConverter URIConverter = null; + + /** + * @return the URI converter. + */ + protected B2CConverter getURIConverter() { + return URIConverter; + } + + /** + * Set the URI converter. + * + * @param URIConverter the new URI converter + */ + protected void setURIConverter(B2CConverter URIConverter) { + this.URIConverter = URIConverter; + } + + + /** + * @return the Wrapper within which this Request is being processed. + */ + public Wrapper getWrapper() { + return mappingData.wrapper; + } + + + // ------------------------------------------------- Request Public Methods + + /** + * Create and return a ServletInputStream to read the content associated with this Request. + * + * @return the created input stream + * + * @exception IOException if an input/output error occurs + */ + public ServletInputStream createInputStream() throws IOException { + if (inputStream == null) { + inputStream = new CoyoteInputStream(inputBuffer); + } + return inputStream; + } + + + /** + * Perform whatever actions are required to flush and close the input stream or reader, in a single operation. + * + * @exception IOException if an input/output error occurs + */ + public void finishRequest() throws IOException { + if (response.getStatus() == HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE) { + checkSwallowInput(); + } + } + + + /** + * @return the object bound with the specified name to the internal notes for this request, or null if + * no such binding exists. + * + * @param name Name of the note to be returned + */ + public Object getNote(String name) { + return notes.get(name); + } + + + /** + * Remove any object bound to the specified name in the internal notes for this request. + * + * @param name Name of the note to be removed + */ + public void removeNote(String name) { + notes.remove(name); + } + + + /** + * Set the port number of the server to process this request. + * + * @param port The server port + */ + public void setLocalPort(int port) { + localPort = port; + } + + /** + * Bind an object to a specified name in the internal notes associated with this request, replacing any existing + * binding for this name. + * + * @param name Name to which the object should be bound + * @param value Object to be bound to the specified name + */ + public void setNote(String name, Object value) { + notes.put(name, value); + } + + + /** + * Set the IP address of the remote client associated with this Request. + * + * @param remoteAddr The remote IP address + */ + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + + /** + * Set the fully qualified name of the remote client associated with this Request. + * + * @param remoteHost The remote host name + */ + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + + /** + * Set the value to be returned by isSecure() for this Request. + * + * @param secure The new isSecure value + */ + public void setSecure(boolean secure) { + this.secure = secure; + } + + + /** + * Set the port number of the server to process this request. + * + * @param port The server port + */ + public void setServerPort(int port) { + coyoteRequest.setServerPort(port); + } + + + // ------------------------------------------------- ServletRequest Methods + + @Override + public Object getAttribute(String name) { + // Special attributes + SpecialAttributeAdapter adapter = specialAttributes.get(name); + if (adapter != null) { + return adapter.get(this, name); + } + + Object attr = attributes.get(name); + + if (attr != null) { + return attr; + } + + attr = coyoteRequest.getAttribute(name); + if (attr != null) { + return attr; + } + if (!sslAttributesParsed && TLSUtil.isTLSRequestAttribute(name)) { + coyoteRequest.action(ActionCode.REQ_SSL_ATTRIBUTE, coyoteRequest); + attr = coyoteRequest.getAttribute(Globals.CERTIFICATES_ATTR); + if (attr != null) { + attributes.put(Globals.CERTIFICATES_ATTR, attr); + } + attr = coyoteRequest.getAttribute(Globals.CIPHER_SUITE_ATTR); + if (attr != null) { + attributes.put(Globals.CIPHER_SUITE_ATTR, attr); + } + attr = coyoteRequest.getAttribute(Globals.KEY_SIZE_ATTR); + if (attr != null) { + attributes.put(Globals.KEY_SIZE_ATTR, attr); + } + attr = coyoteRequest.getAttribute(Globals.SSL_SESSION_ID_ATTR); + if (attr != null) { + attributes.put(Globals.SSL_SESSION_ID_ATTR, attr); + } + attr = coyoteRequest.getAttribute(Globals.SSL_SESSION_MGR_ATTR); + if (attr != null) { + attributes.put(Globals.SSL_SESSION_MGR_ATTR, attr); + } + attr = coyoteRequest.getAttribute(SSLSupport.PROTOCOL_VERSION_KEY); + if (attr != null) { + attributes.put(SSLSupport.PROTOCOL_VERSION_KEY, attr); + } + attr = coyoteRequest.getAttribute(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY); + if (attr != null) { + attributes.put(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY, attr); + } + attr = coyoteRequest.getAttribute(SSLSupport.REQUESTED_CIPHERS_KEY); + if (attr != null) { + attributes.put(SSLSupport.REQUESTED_CIPHERS_KEY, attr); + } + attr = attributes.get(name); + sslAttributesParsed = true; + } + return attr; + } + + + @Override + public long getContentLengthLong() { + return coyoteRequest.getContentLengthLong(); + } + + + /** + * {@inheritDoc} + *

    + * The attribute names returned will only be those for the attributes set via + * {@link #setAttribute(String, Object)}. Tomcat internal attributes will not be included although they are + * accessible via {@link #getAttribute(String)}. The Tomcat internal attributes include: + *

      + *
    • {@link Globals#DISPATCHER_TYPE_ATTR}
    • + *
    • {@link Globals#DISPATCHER_REQUEST_PATH_ATTR}
    • + *
    • {@link Globals#ASYNC_SUPPORTED_ATTR}
    • + *
    • {@link Globals#CERTIFICATES_ATTR} (SSL connections only)
    • + *
    • {@link Globals#CIPHER_SUITE_ATTR} (SSL connections only)
    • + *
    • {@link Globals#KEY_SIZE_ATTR} (SSL connections only)
    • + *
    • {@link Globals#SSL_SESSION_ID_ATTR} (SSL connections only)
    • + *
    • {@link Globals#SSL_SESSION_MGR_ATTR} (SSL connections only)
    • + *
    • {@link Globals#PARAMETER_PARSE_FAILED_ATTR}
    • + *
    + * The underlying connector may also expose request attributes. These all have names starting with + * "org.apache.tomcat" and include: + *
      + *
    • {@link Globals#SENDFILE_SUPPORTED_ATTR}
    • + *
    + * Connector implementations may return some, all or none of these attributes and may also support additional + * attributes. + */ + @Override + public Enumeration getAttributeNames() { + if (isSecure() && !sslAttributesParsed) { + getAttribute(Globals.CERTIFICATES_ATTR); + } + // Take a copy to prevent ConcurrentModificationExceptions if used to + // remove attributes + Set names = new HashSet<>(attributes.keySet()); + return Collections.enumeration(names); + } + + + @Override + public String getCharacterEncoding() { + String characterEncoding = coyoteRequest.getCharacterEncoding(); + if (characterEncoding != null) { + return characterEncoding; + } + + Context context = getContext(); + if (context != null) { + return context.getRequestCharacterEncoding(); + } + + return null; + } + + + private Charset getCharset() { + Charset charset = null; + try { + charset = coyoteRequest.getCharset(); + } catch (UnsupportedEncodingException e) { + // Ignore + } + if (charset != null) { + return charset; + } + + Context context = getContext(); + if (context != null) { + String encoding = context.getRequestCharacterEncoding(); + if (encoding != null) { + try { + return B2CConverter.getCharset(encoding); + } catch (UnsupportedEncodingException e) { + // Ignore + } + } + } + + return org.apache.coyote.Constants.DEFAULT_BODY_CHARSET; + } + + + @Override + public int getContentLength() { + return coyoteRequest.getContentLength(); + } + + + @Override + public String getContentType() { + return coyoteRequest.getContentType(); + } + + + /** + * Set the content type for this Request. + * + * @param contentType The content type + */ + public void setContentType(String contentType) { + coyoteRequest.setContentType(contentType); + } + + + @Override + public ServletInputStream getInputStream() throws IOException { + + if (usingReader) { + throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise")); + } + + usingInputStream = true; + if (inputStream == null) { + inputStream = new CoyoteInputStream(inputBuffer); + } + return inputStream; + + } + + + @Override + public Locale getLocale() { + + if (!localesParsed) { + parseLocales(); + } + + if (locales.size() > 0) { + return locales.get(0); + } + + return defaultLocale; + } + + + @Override + public Enumeration getLocales() { + + if (!localesParsed) { + parseLocales(); + } + + if (locales.size() > 0) { + return Collections.enumeration(locales); + } + ArrayList results = new ArrayList<>(); + results.add(defaultLocale); + return Collections.enumeration(results); + + } + + + /** + * @return the value of the specified request parameter, if any; otherwise, return null. If there is + * more than one value defined, return only the first one. + * + * @param name Name of the desired request parameter + */ + @Override + public String getParameter(String name) { + + if (!parametersParsed) { + parseParameters(); + } + + return coyoteRequest.getParameters().getParameter(name); + + } + + + /** + * Returns a Map of the parameters of this request. Request parameters are extra information sent with + * the request. For HTTP servlets, parameters are contained in the query string or posted form data. + * + * @return A Map containing parameter names as keys and parameter values as map values. + */ + @Override + public Map getParameterMap() { + + if (parameterMap.isLocked()) { + return parameterMap; + } + + Enumeration enumeration = getParameterNames(); + while (enumeration.hasMoreElements()) { + String name = enumeration.nextElement(); + String[] values = getParameterValues(name); + parameterMap.put(name, values); + } + + parameterMap.setLocked(true); + + return parameterMap; + + } + + + /** + * @return the names of all defined request parameters for this request. + */ + @Override + public Enumeration getParameterNames() { + + if (!parametersParsed) { + parseParameters(); + } + + return coyoteRequest.getParameters().getParameterNames(); + + } + + + /** + * @return the defined values for the specified request parameter, if any; otherwise, return null. + * + * @param name Name of the desired request parameter + */ + @Override + public String[] getParameterValues(String name) { + + if (!parametersParsed) { + parseParameters(); + } + + return coyoteRequest.getParameters().getParameterValues(name); + + } + + + @Override + public String getProtocol() { + return coyoteRequest.protocol().toStringType(); + } + + + @Override + public BufferedReader getReader() throws IOException { + + if (usingInputStream) { + throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise")); + } + + // InputBuffer has no easily accessible reference chain to the Context + // to check for a default request character encoding at the Context. + // Therefore, if a Context default should be used, it is set explicitly + // here. Need to do this before setting usingReader. + if (coyoteRequest.getCharacterEncoding() == null) { + // Nothing currently set explicitly. + // Check the context + Context context = getContext(); + if (context != null) { + String enc = context.getRequestCharacterEncoding(); + if (enc != null) { + // Explicitly set the context default so it is visible to + // InputBuffer when creating the Reader. + setCharacterEncoding(enc); + } + } + } + + usingReader = true; + + inputBuffer.checkConverter(); + if (reader == null) { + reader = new CoyoteReader(inputBuffer); + } + return reader; + } + + + @Override + public String getRemoteAddr() { + if (remoteAddr == null) { + coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, coyoteRequest); + remoteAddr = coyoteRequest.remoteAddr().toString(); + } + return remoteAddr; + } + + + /** + * @return the connection peer IP address making this Request. + */ + public String getPeerAddr() { + if (peerAddr == null) { + coyoteRequest.action(ActionCode.REQ_PEER_ADDR_ATTRIBUTE, coyoteRequest); + peerAddr = coyoteRequest.peerAddr().toString(); + } + return peerAddr; + } + + + @Override + public String getRemoteHost() { + if (remoteHost == null) { + if (!connector.getEnableLookups()) { + remoteHost = getRemoteAddr(); + } else { + coyoteRequest.action(ActionCode.REQ_HOST_ATTRIBUTE, coyoteRequest); + remoteHost = coyoteRequest.remoteHost().toString(); + } + } + return remoteHost; + } + + @Override + public int getRemotePort() { + if (remotePort == -1) { + coyoteRequest.action(ActionCode.REQ_REMOTEPORT_ATTRIBUTE, coyoteRequest); + remotePort = coyoteRequest.getRemotePort(); + } + return remotePort; + } + + @Override + public String getLocalName() { + if (localName == null) { + coyoteRequest.action(ActionCode.REQ_LOCAL_NAME_ATTRIBUTE, coyoteRequest); + localName = coyoteRequest.localName().toString(); + } + return localName; + } + + @Override + public String getLocalAddr() { + if (localAddr == null) { + coyoteRequest.action(ActionCode.REQ_LOCAL_ADDR_ATTRIBUTE, coyoteRequest); + localAddr = coyoteRequest.localAddr().toString(); + } + return localAddr; + } + + + @Override + public int getLocalPort() { + if (localPort == -1) { + coyoteRequest.action(ActionCode.REQ_LOCALPORT_ATTRIBUTE, coyoteRequest); + localPort = coyoteRequest.getLocalPort(); + } + return localPort; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + + Context context = getContext(); + if (context == null) { + return null; + } + + if (path == null) { + return null; + } + + int fragmentPos = path.indexOf('#'); + if (fragmentPos > -1) { + log.warn(sm.getString("request.fragmentInDispatchPath", path)); + path = path.substring(0, fragmentPos); + } + + // If the path is already context-relative, just pass it through + if (path.startsWith("/")) { + return context.getServletContext().getRequestDispatcher(path); + } + + /* + * Relative to what, exactly? + * + * From the Servlet 4.0 Javadoc: - The pathname specified may be relative, although it cannot extend outside the + * current servlet context. - If it is relative, it must be relative against the current servlet + * + * From Section 9.1 of the spec: - The servlet container uses information in the request object to transform the + * given relative path against the current servlet to a complete path. + * + * It is undefined whether the requestURI is used or whether servletPath and pathInfo are used. Given that the + * RequestURI includes the contextPath (and extracting that is messy) , using the servletPath and pathInfo looks + * to be the more reasonable choice. + */ + + // Convert a request-relative path to a context-relative one + String servletPath = (String) getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + if (servletPath == null) { + servletPath = getServletPath(); + } + + // Add the path info, if there is any + String pathInfo = getPathInfo(); + String requestPath = null; + + if (pathInfo == null) { + requestPath = servletPath; + } else { + requestPath = servletPath + pathInfo; + } + + int pos = requestPath.lastIndexOf('/'); + String relative = null; + if (context.getDispatchersUseEncodedPaths()) { + if (pos >= 0) { + relative = URLEncoder.DEFAULT.encode(requestPath.substring(0, pos + 1), StandardCharsets.UTF_8) + path; + } else { + relative = URLEncoder.DEFAULT.encode(requestPath, StandardCharsets.UTF_8) + path; + } + } else { + if (pos >= 0) { + relative = requestPath.substring(0, pos + 1) + path; + } else { + relative = requestPath + path; + } + } + + return context.getServletContext().getRequestDispatcher(relative); + } + + + @Override + public String getScheme() { + return coyoteRequest.scheme().toStringType(); + } + + + @Override + public String getServerName() { + return coyoteRequest.serverName().toString(); + } + + + @Override + public int getServerPort() { + return coyoteRequest.getServerPort(); + } + + + @Override + public boolean isSecure() { + return secure; + } + + + @Override + public void removeAttribute(String name) { + // Remove the specified attribute + // Pass special attributes to the native layer + if (name.startsWith("org.apache.tomcat.")) { + coyoteRequest.getAttributes().remove(name); + } + + boolean found = attributes.containsKey(name); + if (found) { + Object value = attributes.get(name); + attributes.remove(name); + + // Notify interested application event listeners + notifyAttributeRemoved(name, value); + } + } + + + @Override + public void setAttribute(String name, Object value) { + + // Name cannot be null + if (name == null) { + throw new IllegalArgumentException(sm.getString("coyoteRequest.setAttribute.namenull")); + } + + // Null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + // Special attributes + SpecialAttributeAdapter adapter = specialAttributes.get(name); + if (adapter != null) { + adapter.set(this, name, value); + return; + } + + // Add or replace the specified attribute + // Do the security check before any updates are made + if (Globals.IS_SECURITY_ENABLED && name.equals(Globals.SENDFILE_FILENAME_ATTR)) { + // Use the canonical file name to avoid any possible symlink and + // relative path issues + String canonicalPath; + try { + canonicalPath = new File(value.toString()).getCanonicalPath(); + } catch (IOException e) { + throw new SecurityException(sm.getString("coyoteRequest.sendfileNotCanonical", value), e); + } + // Sendfile is performed in Tomcat's security context so need to + // check if the web app is permitted to access the file while still + // in the web app's security context + System.getSecurityManager().checkRead(canonicalPath); + // Update the value so the canonical path is used + value = canonicalPath; + } + + Object oldValue = attributes.put(name, value); + + // Pass special attributes to the native layer + if (name.startsWith("org.apache.tomcat.")) { + coyoteRequest.setAttribute(name, value); + } + + // Notify interested application event listeners + notifyAttributeAssigned(name, value, oldValue); + } + + + /** + * Notify interested listeners that attribute has been assigned a value. + * + * @param name Attribute name + * @param value New attribute value + * @param oldValue Old attribute value + */ + private void notifyAttributeAssigned(String name, Object value, Object oldValue) { + Context context = getContext(); + if (context == null) { + return; + } + Object listeners[] = context.getApplicationEventListeners(); + if (listeners == null || listeners.length == 0) { + return; + } + boolean replaced = (oldValue != null); + ServletRequestAttributeEvent event = null; + if (replaced) { + event = new ServletRequestAttributeEvent(context.getServletContext(), getRequest(), name, oldValue); + } else { + event = new ServletRequestAttributeEvent(context.getServletContext(), getRequest(), name, value); + } + + for (Object o : listeners) { + if (!(o instanceof ServletRequestAttributeListener)) { + continue; + } + ServletRequestAttributeListener listener = (ServletRequestAttributeListener) o; + try { + if (replaced) { + listener.attributeReplaced(event); + } else { + listener.attributeAdded(event); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // Error valve will pick this exception up and display it to user + attributes.put(RequestDispatcher.ERROR_EXCEPTION, t); + context.getLogger().error(sm.getString("coyoteRequest.attributeEvent"), t); + } + } + } + + + /** + * Notify interested listeners that attribute has been removed. + * + * @param name Attribute name + * @param value Attribute value + */ + private void notifyAttributeRemoved(String name, Object value) { + Context context = getContext(); + Object listeners[] = context.getApplicationEventListeners(); + if (listeners == null || listeners.length == 0) { + return; + } + ServletRequestAttributeEvent event = + new ServletRequestAttributeEvent(context.getServletContext(), getRequest(), name, value); + for (Object o : listeners) { + if (!(o instanceof ServletRequestAttributeListener)) { + continue; + } + ServletRequestAttributeListener listener = (ServletRequestAttributeListener) o; + try { + listener.attributeRemoved(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // Error valve will pick this exception up and display it to user + attributes.put(RequestDispatcher.ERROR_EXCEPTION, t); + context.getLogger().error(sm.getString("coyoteRequest.attributeEvent"), t); + } + } + } + + + @Override + public void setCharacterEncoding(String enc) throws UnsupportedEncodingException { + + if (usingReader) { + return; + } + + // Confirm that the encoding name is valid + Charset charset = B2CConverter.getCharset(enc); + + // Save the validated encoding + coyoteRequest.setCharset(charset); + } + + + @Override + public ServletContext getServletContext() { + return getContext().getServletContext(); + } + + @Override + public AsyncContext startAsync() { + return startAsync(getRequest(), response.getResponse()); + } + + @Override + public AsyncContext startAsync(ServletRequest request, ServletResponse response) { + if (!isAsyncSupported()) { + IllegalStateException ise = new IllegalStateException(sm.getString("request.asyncNotSupported")); + log.warn(sm.getString("coyoteRequest.noAsync", StringUtils.join(getNonAsyncClassNames())), ise); + throw ise; + } + + if (asyncContext == null) { + asyncContext = new AsyncContextImpl(this); + } + + asyncContext.setStarted(getContext(), request, response, + request == getRequest() && response == getResponse().getResponse()); + asyncContext.setTimeout(getConnector().getAsyncTimeout()); + + return asyncContext; + } + + + private Set getNonAsyncClassNames() { + Set result = new HashSet<>(); + + Wrapper wrapper = getWrapper(); + if (!wrapper.isAsyncSupported()) { + result.add(wrapper.getServletClass()); + } + + FilterChain filterChain = getFilterChain(); + if (filterChain instanceof ApplicationFilterChain) { + ((ApplicationFilterChain) filterChain).findNonAsyncFilters(result); + } else { + result.add(sm.getString("coyoteRequest.filterAsyncSupportUnknown")); + } + + Container c = wrapper; + while (c != null) { + c.getPipeline().findNonAsyncValves(result); + c = c.getParent(); + } + + return result; + } + + @Override + public boolean isAsyncStarted() { + if (asyncContext == null) { + return false; + } + + return asyncContext.isStarted(); + } + + public boolean isAsyncDispatching() { + if (asyncContext == null) { + return false; + } + + AtomicBoolean result = new AtomicBoolean(false); + coyoteRequest.action(ActionCode.ASYNC_IS_DISPATCHING, result); + return result.get(); + } + + public boolean isAsyncCompleting() { + if (asyncContext == null) { + return false; + } + + AtomicBoolean result = new AtomicBoolean(false); + coyoteRequest.action(ActionCode.ASYNC_IS_COMPLETING, result); + return result.get(); + } + + public boolean isAsync() { + if (asyncContext == null) { + return false; + } + + AtomicBoolean result = new AtomicBoolean(false); + coyoteRequest.action(ActionCode.ASYNC_IS_ASYNC, result); + return result.get(); + } + + @Override + public boolean isAsyncSupported() { + if (this.asyncSupported == null) { + return true; + } + + return asyncSupported.booleanValue(); + } + + @Override + public AsyncContext getAsyncContext() { + if (!isAsyncStarted()) { + throw new IllegalStateException(sm.getString("request.notAsync")); + } + return asyncContext; + } + + public AsyncContextImpl getAsyncContextInternal() { + return asyncContext; + } + + @Override + public DispatcherType getDispatcherType() { + if (internalDispatcherType == null) { + return DispatcherType.REQUEST; + } + + return this.internalDispatcherType; + } + + + @Override + public String getRequestId() { + return coyoteRequest.getRequestId(); + } + + + @Override + public String getProtocolRequestId() { + return coyoteRequest.getProtocolRequestId(); + } + + + @Override + public ServletConnection getServletConnection() { + return coyoteRequest.getServletConnection(); + } + + + // ---------------------------------------------------- HttpRequest Methods + + /** + * Add a Cookie to the set of Cookies associated with this Request. + * + * @param cookie The new cookie + */ + public void addCookie(Cookie cookie) { + + if (!cookiesConverted) { + convertCookies(); + } + + int size = 0; + if (cookies != null) { + size = cookies.length; + } + + Cookie[] newCookies = new Cookie[size + 1]; + if (cookies != null) { + System.arraycopy(cookies, 0, newCookies, 0, size); + } + newCookies[size] = cookie; + + cookies = newCookies; + + } + + + /** + * Add a Locale to the set of preferred Locales for this Request. The first added Locale will be the first one + * returned by getLocales(). + * + * @param locale The new preferred Locale + */ + public void addLocale(Locale locale) { + locales.add(locale); + } + + + /** + * Clear the collection of Cookies associated with this Request. + */ + public void clearCookies() { + cookiesParsed = true; + cookiesConverted = true; + cookies = null; + } + + + /** + * Clear the collection of Locales associated with this Request. + */ + public void clearLocales() { + locales.clear(); + } + + + /** + * Set the authentication type used for this request, if any; otherwise set the type to null. Typical + * values are "BASIC", "DIGEST", or "SSL". + * + * @param type The authentication type used + */ + public void setAuthType(String type) { + this.authType = type; + } + + + /** + * Set the path information for this Request. This will normally be called when the associated Context is mapping + * the Request to a particular Wrapper. + * + * @param path The path information + */ + public void setPathInfo(String path) { + mappingData.pathInfo.setString(path); + } + + + /** + * Set a flag indicating whether or not the requested session ID for this request came in through a cookie. This is + * normally called by the HTTP Connector, when it parses the request headers. + * + * @param flag The new flag + */ + public void setRequestedSessionCookie(boolean flag) { + + this.requestedSessionCookie = flag; + + } + + + /** + * Set the requested session ID for this request. This is normally called by the HTTP Connector, when it parses the + * request headers. + * + * @param id The new session id + */ + public void setRequestedSessionId(String id) { + + this.requestedSessionId = id; + + } + + + /** + * Set a flag indicating whether or not the requested session ID for this request came in through a URL. This is + * normally called by the HTTP Connector, when it parses the request headers. + * + * @param flag The new flag + */ + public void setRequestedSessionURL(boolean flag) { + + this.requestedSessionURL = flag; + + } + + + /** + * Set a flag indicating whether or not the requested session ID for this request came in through SSL. This is + * normally called by the HTTP Connector, when it parses the request headers. + * + * @param flag The new flag + */ + public void setRequestedSessionSSL(boolean flag) { + + this.requestedSessionSSL = flag; + + } + + + /** + * Get the decoded request URI. + * + * @return the URL decoded request URI + */ + public String getDecodedRequestURI() { + return coyoteRequest.decodedURI().toString(); + } + + + /** + * Get the decoded request URI. + * + * @return the URL decoded request URI + */ + public MessageBytes getDecodedRequestURIMB() { + return coyoteRequest.decodedURI(); + } + + + /** + * Set the Principal who has been authenticated for this Request. This value is also used to calculate the value to + * be returned by the getRemoteUser() method. + * + * @param principal The user Principal + */ + public void setUserPrincipal(final Principal principal) { + if (Globals.IS_SECURITY_ENABLED && principal != null) { + if (subject == null) { + final HttpSession session = getSession(false); + if (session == null) { + // Cache the subject in the request + subject = newSubject(principal); + } else { + // Cache the subject in the request and the session + subject = (Subject) session.getAttribute(Globals.SUBJECT_ATTR); + if (subject == null) { + subject = newSubject(principal); + session.setAttribute(Globals.SUBJECT_ATTR, subject); + } else { + subject.getPrincipals().add(principal); + } + } + } else { + subject.getPrincipals().add(principal); + } + } + userPrincipal = principal; + } + + + private Subject newSubject(final Principal principal) { + final Subject result = new Subject(); + result.getPrincipals().add(principal); + return result; + } + + + // --------------------------------------------- HttpServletRequest Methods + + @Override + public boolean isTrailerFieldsReady() { + return coyoteRequest.isTrailerFieldsReady(); + } + + + @Override + public Map getTrailerFields() { + if (!isTrailerFieldsReady()) { + throw new IllegalStateException(sm.getString("coyoteRequest.trailersNotReady")); + } + Map result = new HashMap<>(coyoteRequest.getTrailerFields()); + return result; + } + + + @Override + public PushBuilder newPushBuilder() { + return newPushBuilder(this); + } + + + public PushBuilder newPushBuilder(HttpServletRequest request) { + AtomicBoolean result = new AtomicBoolean(); + coyoteRequest.action(ActionCode.IS_PUSH_SUPPORTED, result); + if (result.get()) { + return new ApplicationPushBuilder(this, request); + } else { + return null; + } + } + + + @SuppressWarnings("unchecked") + @Override + public T upgrade(Class httpUpgradeHandlerClass) + throws IOException, ServletException { + T handler; + InstanceManager instanceManager = null; + try { + // Do not go through the instance manager for internal Tomcat classes since they don't + // need injection + if (InternalHttpUpgradeHandler.class.isAssignableFrom(httpUpgradeHandlerClass)) { + handler = httpUpgradeHandlerClass.getConstructor().newInstance(); + } else { + instanceManager = getContext().getInstanceManager(); + handler = (T) instanceManager.newInstance(httpUpgradeHandlerClass); + } + } catch (ReflectiveOperationException | NamingException | IllegalArgumentException | SecurityException e) { + throw new ServletException(e); + } + UpgradeToken upgradeToken = new UpgradeToken(handler, getContext(), instanceManager, + getUpgradeProtocolName(httpUpgradeHandlerClass)); + + coyoteRequest.action(ActionCode.UPGRADE, upgradeToken); + + // Output required by RFC2616. Protocol specific headers should have + // already been set. + response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + + return handler; + } + + + private String getUpgradeProtocolName(Class httpUpgradeHandlerClass) { + // Ideal - the caller has already explicitly set the selected protocol + // on the response + String result = response.getHeader(HTTP_UPGRADE_HEADER_NAME); + + if (result == null) { + // If the request's upgrade header contains a single protocol that + // is the protocol that must have been selected + List upgradeProtocols = Upgrade.parse(getHeaders(HTTP_UPGRADE_HEADER_NAME)); + if (upgradeProtocols != null && upgradeProtocols.size() == 1) { + result = upgradeProtocols.get(0).toString(); + } + } + + if (result == null) { + // Ugly but use the class name - it is better than nothing + result = httpUpgradeHandlerClass.getName(); + } + return result; + } + + + @Override + public String getAuthType() { + return authType; + } + + + @Override + public String getContextPath() { + int lastSlash = mappingData.contextSlashCount; + // Special case handling for the root context + if (lastSlash == 0) { + return ""; + } + + String canonicalContextPath = getServletContext().getContextPath(); + + String uri = getRequestURI(); + int pos = 0; + if (!getContext().getAllowMultipleLeadingForwardSlashInPath()) { + // Ensure that the returned value only starts with a single '/'. + // This prevents the value being misinterpreted as a protocol- + // relative URI if used with sendRedirect(). + do { + pos++; + } while (pos < uri.length() && uri.charAt(pos) == '/'); + pos--; + uri = uri.substring(pos); + } + + char[] uriChars = uri.toCharArray(); + // Need at least the number of slashes in the context path + while (lastSlash > 0) { + pos = nextSlash(uriChars, pos + 1); + if (pos == -1) { + break; + } + lastSlash--; + } + // Now allow for path parameters, normalization and/or encoding. + // Essentially, keep extending the candidate path up to the next slash + // until the decoded and normalized candidate path (with the path + // parameters removed) is the same as the canonical path. + String candidate; + if (pos == -1) { + candidate = uri; + } else { + candidate = uri.substring(0, pos); + } + candidate = removePathParameters(candidate); + candidate = UDecoder.URLDecode(candidate, connector.getURICharset()); + candidate = org.apache.tomcat.util.http.RequestUtil.normalize(candidate); + boolean match = canonicalContextPath.equals(candidate); + while (!match && pos != -1) { + pos = nextSlash(uriChars, pos + 1); + if (pos == -1) { + candidate = uri; + } else { + candidate = uri.substring(0, pos); + } + candidate = removePathParameters(candidate); + candidate = UDecoder.URLDecode(candidate, connector.getURICharset()); + candidate = org.apache.tomcat.util.http.RequestUtil.normalize(candidate); + match = canonicalContextPath.equals(candidate); + } + if (match) { + if (pos == -1) { + return uri; + } else { + return uri.substring(0, pos); + } + } else { + // Should never happen + throw new IllegalStateException( + sm.getString("coyoteRequest.getContextPath.ise", canonicalContextPath, uri)); + } + } + + + private String removePathParameters(String input) { + int nextSemiColon = input.indexOf(';'); + // Shortcut + if (nextSemiColon == -1) { + return input; + } + StringBuilder result = new StringBuilder(input.length()); + result.append(input.substring(0, nextSemiColon)); + while (true) { + int nextSlash = input.indexOf('/', nextSemiColon); + if (nextSlash == -1) { + break; + } + nextSemiColon = input.indexOf(';', nextSlash); + if (nextSemiColon == -1) { + result.append(input.substring(nextSlash)); + break; + } else { + result.append(input.substring(nextSlash, nextSemiColon)); + } + } + + return result.toString(); + } + + + private int nextSlash(char[] uri, int startPos) { + int len = uri.length; + int pos = startPos; + while (pos < len) { + if (uri[pos] == '/') { + return pos; + } else if (connector.getEncodedSolidusHandlingInternal() == EncodedSolidusHandling.DECODE && + uri[pos] == '%' && pos + 2 < len && uri[pos + 1] == '2' && + (uri[pos + 2] == 'f' || uri[pos + 2] == 'F')) { + return pos; + } + pos++; + } + return -1; + } + + + @Override + public Cookie[] getCookies() { + if (!cookiesConverted) { + convertCookies(); + } + return cookies; + } + + + /** + * Return the server representation of the cookies associated with this request. Triggers parsing of the Cookie HTTP + * headers (but not conversion to Cookie objects) if the headers have not yet been parsed. + * + * @return the server cookies + */ + public ServerCookies getServerCookies() { + parseCookies(); + return coyoteRequest.getCookies(); + } + + + @Override + public long getDateHeader(String name) { + + String value = getHeader(name); + if (value == null) { + return -1L; + } + + // Attempt to convert the date header in a variety of formats + long result = FastHttpDateFormat.parseDate(value); + if (result != (-1L)) { + return result; + } + throw new IllegalArgumentException(value); + + } + + + @Override + public String getHeader(String name) { + return coyoteRequest.getHeader(name); + } + + + @Override + public Enumeration getHeaders(String name) { + return coyoteRequest.getMimeHeaders().values(name); + } + + + @Override + public Enumeration getHeaderNames() { + return coyoteRequest.getMimeHeaders().names(); + } + + + @Override + public int getIntHeader(String name) { + + String value = getHeader(name); + if (value == null) { + return -1; + } + + return Integer.parseInt(value); + } + + + @Override + public HttpServletMapping getHttpServletMapping() { + return applicationMapping.getHttpServletMapping(); + } + + + @Override + public String getMethod() { + return coyoteRequest.method().toStringType(); + } + + + @Override + public String getPathInfo() { + return mappingData.pathInfo.toStringType(); + } + + + @Override + public String getPathTranslated() { + + Context context = getContext(); + if (context == null) { + return null; + } + + if (getPathInfo() == null) { + return null; + } + + return context.getServletContext().getRealPath(getPathInfo()); + } + + + @Override + public String getQueryString() { + return coyoteRequest.queryString().toString(); + } + + + @Override + public String getRemoteUser() { + + if (userPrincipal == null) { + return null; + } + + return userPrincipal.getName(); + } + + + /** + * Get the request path. + * + * @return the request path + */ + public MessageBytes getRequestPathMB() { + return mappingData.requestPath; + } + + + @Override + public String getRequestedSessionId() { + return requestedSessionId; + } + + + @Override + public String getRequestURI() { + return coyoteRequest.requestURI().toStringType(); + } + + + @Override + public StringBuffer getRequestURL() { + return RequestUtil.getRequestURL(this); + } + + + @Override + public String getServletPath() { + return mappingData.wrapperPath.toStringType(); + } + + + @Override + public HttpSession getSession() { + return getSession(true); + } + + + @Override + public HttpSession getSession(boolean create) { + Session session = doGetSession(create); + if (session == null) { + return null; + } + + return session.getSession(); + } + + + @Override + public boolean isRequestedSessionIdFromCookie() { + + if (requestedSessionId == null) { + return false; + } + + return requestedSessionCookie; + } + + + @Override + public boolean isRequestedSessionIdFromURL() { + + if (requestedSessionId == null) { + return false; + } + + return requestedSessionURL; + } + + + @Override + public boolean isRequestedSessionIdValid() { + + if (requestedSessionId == null) { + return false; + } + + Context context = getContext(); + if (context == null) { + return false; + } + + /* + * As per PR #594, the manager could be provided by the web application and calls to findSession() could trigger + * class loading so set the thread context class loader appropriately to avoid ClassNotFoundException. + */ + ClassLoader originalClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null); + try { + Manager manager = context.getManager(); + if (manager == null) { + return false; + } + + Session session = null; + try { + session = manager.findSession(requestedSessionId); + } catch (IOException e) { + // Can't find the session + } + + if ((session == null) || !session.isValid()) { + // Check for parallel deployment contexts + if (getMappingData().contexts == null) { + return false; + } else { + for (int i = (getMappingData().contexts.length); i > 0; i--) { + Context ctxt = getMappingData().contexts[i - 1]; + try { + if (ctxt.getManager().findSession(requestedSessionId) != null) { + return true; + } + } catch (IOException e) { + // Ignore + } + } + return false; + } + } + + return true; + } finally { + context.unbind(Globals.IS_SECURITY_ENABLED, originalClassLoader); + } + } + + + @Override + public boolean isUserInRole(String role) { + + // Have we got an authenticated principal at all? + if (userPrincipal == null) { + return false; + } + + // Identify the Realm we will use for checking role assignments + Context context = getContext(); + if (context == null) { + return false; + } + + // If the role is "*" then the return value must be false + // Servlet 31, section 13.3 + if ("*".equals(role)) { + return false; + } + + // If the role is "**" then, unless the application defines a role with + // that name, only check if the user is authenticated + if ("**".equals(role) && !context.findSecurityRole("**")) { + return userPrincipal != null; + } + + Realm realm = context.getRealm(); + if (realm == null) { + return false; + } + + // Check for a role defined directly as a + return realm.hasRole(getWrapper(), userPrincipal, role); + } + + + /** + * @return the principal that has been authenticated for this Request. + */ + public Principal getPrincipal() { + return userPrincipal; + } + + + @Override + public Principal getUserPrincipal() { + if (userPrincipal instanceof TomcatPrincipal) { + GSSCredential gssCredential = ((TomcatPrincipal) userPrincipal).getGssCredential(); + if (gssCredential != null) { + int left = -1; + try { + // Concurrent calls to this method from an expired session + // can trigger an ISE. If one thread calls logout() below + // before another thread calls getRemainingLifetime() then + // then since logout() eventually calls + // GSSCredential.dispose(), the subsequent call to + // GSSCredential.getRemainingLifetime() will throw an ISE. + // Avoiding the ISE would require locking in this method to + // protect against concurrent access to the GSSCredential. + // That would have a small performance impact. The ISE is + // rare so it is caught and handled rather than avoided. + left = gssCredential.getRemainingLifetime(); + } catch (GSSException | IllegalStateException e) { + log.warn(sm.getString("coyoteRequest.gssLifetimeFail", userPrincipal.getName()), e); + } + // zero is expired. Exception above will mean left == -1 + // Treat both as expired. + if (left <= 0) { + // GSS credential has expired. Need to re-authenticate. + try { + logout(); + } catch (ServletException e) { + // Should never happen (no code called by logout() + // throws a ServletException + } + return null; + } + } + return ((TomcatPrincipal) userPrincipal).getUserPrincipal(); + } + + return userPrincipal; + } + + + /** + * @return the session associated with this Request, creating one if necessary. + */ + public Session getSessionInternal() { + return doGetSession(true); + } + + + /** + * Change the ID of the session that this request is associated with. There are several things that may trigger an + * ID change. These include moving between nodes in a cluster and session fixation prevention during the + * authentication process. + * + * @param newSessionId The session to change the session ID for + */ + public void changeSessionId(String newSessionId) { + // This should only ever be called if there was an old session ID but + // double check to be sure + if (requestedSessionId != null && requestedSessionId.length() > 0) { + requestedSessionId = newSessionId; + } + + Context context = getContext(); + if (context != null && + !context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE)) { + return; + } + + if (response != null) { + Cookie newCookie = ApplicationSessionCookieConfig.createSessionCookie(context, newSessionId, isSecure()); + response.addSessionCookieInternal(newCookie); + } + } + + + @Override + public String changeSessionId() { + + Session session = this.getSessionInternal(false); + if (session == null) { + throw new IllegalStateException(sm.getString("coyoteRequest.changeSessionId")); + } + + Manager manager = this.getContext().getManager(); + + String newSessionId = manager.rotateSessionId(session); + this.changeSessionId(newSessionId); + + return newSessionId; + } + + /** + * @return the session associated with this Request, creating one if necessary and requested. + * + * @param create Create a new session if one does not exist + */ + public Session getSessionInternal(boolean create) { + return doGetSession(create); + } + + + /** + * @return true if we have parsed parameters + */ + public boolean isParametersParsed() { + return parametersParsed; + } + + + /** + * @return true if an attempt has been made to read the request body and all of the request body has + * been read. + */ + public boolean isFinished() { + return coyoteRequest.isFinished(); + } + + + /** + * Check the configuration for aborted uploads and if configured to do so, disable the swallowing of any remaining + * input and close the connection once the response has been written. + */ + protected void checkSwallowInput() { + Context context = getContext(); + if (context != null && !context.getSwallowAbortedUploads()) { + coyoteRequest.action(ActionCode.DISABLE_SWALLOW_INPUT, null); + } + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + if (response.isCommitted()) { + throw new IllegalStateException(sm.getString("coyoteRequest.authenticate.ise")); + } + + return getContext().getAuthenticator().authenticate(this, response); + } + + @Override + public void login(String username, String password) throws ServletException { + if (getAuthType() != null || getRemoteUser() != null || getUserPrincipal() != null) { + throw new ServletException(sm.getString("coyoteRequest.alreadyAuthenticated")); + } + + getContext().getAuthenticator().login(username, password, this); + } + + @Override + public void logout() throws ServletException { + getContext().getAuthenticator().logout(this); + } + + @Override + public Collection getParts() throws IOException, IllegalStateException, ServletException { + + parseParts(true); + + if (partsParseException != null) { + if (partsParseException instanceof IOException) { + throw (IOException) partsParseException; + } else if (partsParseException instanceof IllegalStateException) { + throw (IllegalStateException) partsParseException; + } else if (partsParseException instanceof ServletException) { + throw (ServletException) partsParseException; + } + } + + return parts; + } + + private void parseParts(boolean explicit) { + + // Return immediately if the parts have already been parsed + if (parts != null || partsParseException != null) { + return; + } + + Context context = getContext(); + MultipartConfigElement mce = getWrapper().getMultipartConfigElement(); + + if (mce == null) { + if (context.getAllowCasualMultipartParsing()) { + mce = new MultipartConfigElement(null, connector.getMaxPostSize(), connector.getMaxPostSize(), + connector.getMaxPostSize()); + } else { + if (explicit) { + partsParseException = new IllegalStateException(sm.getString("coyoteRequest.noMultipartConfig")); + return; + } else { + parts = Collections.emptyList(); + return; + } + } + } + + int maxParameterCount = getConnector().getMaxParameterCount(); + Parameters parameters = coyoteRequest.getParameters(); + parameters.setLimit(maxParameterCount); + + boolean success = false; + try { + File location; + String locationStr = mce.getLocation(); + if (locationStr == null || locationStr.length() == 0) { + location = ((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR)); + } else { + // If relative, it is relative to TEMPDIR + location = new File(locationStr); + if (!location.isAbsolute()) { + location = new File((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR), + locationStr).getAbsoluteFile(); + } + } + + if (!location.exists() && context.getCreateUploadTargets()) { + log.warn(sm.getString("coyoteRequest.uploadCreate", location.getAbsolutePath(), + getMappingData().wrapper.getName())); + if (!location.mkdirs()) { + log.warn(sm.getString("coyoteRequest.uploadCreateFail", location.getAbsolutePath())); + } + } + + if (!location.isDirectory()) { + parameters.setParseFailedReason(FailReason.MULTIPART_CONFIG_INVALID); + partsParseException = new IOException(sm.getString("coyoteRequest.uploadLocationInvalid", location)); + return; + } + + + // Create a new file upload handler + DiskFileItemFactory factory = new DiskFileItemFactory(); + try { + factory.setRepository(location.getCanonicalFile()); + } catch (IOException ioe) { + parameters.setParseFailedReason(FailReason.IO_ERROR); + partsParseException = ioe; + return; + } + factory.setSizeThreshold(mce.getFileSizeThreshold()); + + FileUpload upload = new FileUpload(); + upload.setFileItemFactory(factory); + upload.setFileSizeMax(mce.getMaxFileSize()); + upload.setSizeMax(mce.getMaxRequestSize()); + if (maxParameterCount > -1) { + // There is a limit. The limit for parts needs to be reduced by + // the number of parameters we have already parsed. + // Must be under the limit else parsing parameters would have + // triggered an exception. + upload.setFileCountMax(maxParameterCount - parameters.size()); + } + + parts = new ArrayList<>(); + try { + List items = upload.parseRequest(new ServletRequestContext(this)); + int maxPostSize = getConnector().getMaxPostSize(); + int postSize = 0; + Charset charset = getCharset(); + for (FileItem item : items) { + ApplicationPart part = new ApplicationPart(item, location); + parts.add(part); + if (part.getSubmittedFileName() == null) { + String name = part.getName(); + if (maxPostSize >= 0) { + // Have to calculate equivalent size. Not completely + // accurate but close enough. + postSize += name.getBytes(charset).length; + // Equals sign + postSize++; + // Value length + postSize += part.getSize(); + // Value separator + postSize++; + if (postSize > maxPostSize) { + parameters.setParseFailedReason(FailReason.POST_TOO_LARGE); + throw new IllegalStateException(sm.getString("coyoteRequest.maxPostSizeExceeded")); + } + } + String value = null; + try { + value = part.getString(charset.name()); + } catch (UnsupportedEncodingException uee) { + // Not possible + } + parameters.addParameter(name, value); + } + } + + success = true; + } catch (InvalidContentTypeException e) { + parameters.setParseFailedReason(FailReason.INVALID_CONTENT_TYPE); + partsParseException = new ServletException(e); + } catch (SizeException e) { + parameters.setParseFailedReason(FailReason.POST_TOO_LARGE); + checkSwallowInput(); + partsParseException = new IllegalStateException(e); + } catch (IOException e) { + parameters.setParseFailedReason(FailReason.IO_ERROR); + partsParseException = e; + } catch (IllegalStateException e) { + // addParameters() will set parseFailedReason + checkSwallowInput(); + partsParseException = e; + } + } finally { + // This might look odd but is correct. setParseFailedReason() only + // sets the failure reason if none is currently set. This code could + // be more efficient but it is written this way to be robust with + // respect to changes in the remainder of the method. + if (partsParseException != null || !success) { + parameters.setParseFailedReason(FailReason.UNKNOWN); + } + } + } + + + @Override + public Part getPart(String name) throws IOException, IllegalStateException, ServletException { + for (Part part : getParts()) { + if (name.equals(part.getName())) { + return part; + } + } + return null; + } + + + // ------------------------------------------------------ Protected Methods + + protected Session doGetSession(boolean create) { + + // There cannot be a session if no context has been assigned yet + Context context = getContext(); + if (context == null) { + return null; + } + + // Return the current session if it exists and is valid + if ((session != null) && !session.isValid()) { + session = null; + } + if (session != null) { + return session; + } + + // Return the requested session if it exists and is valid + Manager manager = context.getManager(); + if (manager == null) { + return null; // Sessions are not supported + } + if (requestedSessionId != null) { + try { + session = manager.findSession(requestedSessionId); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e); + } else { + log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage())); + } + session = null; + } + if ((session != null) && !session.isValid()) { + session = null; + } + if (session != null) { + session.access(); + return session; + } + } + + // Create a new session if requested and the response is not committed + if (!create) { + return null; + } + boolean trackModesIncludesCookie = + context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE); + if (trackModesIncludesCookie && response.getResponse().isCommitted()) { + throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted")); + } + + // Re-use session IDs provided by the client in very limited + // circumstances. + String sessionId = getRequestedSessionId(); + if (requestedSessionSSL) { + // If the session ID has been obtained from the SSL handshake then + // use it. + } else if (("/".equals(context.getSessionCookiePath()) && isRequestedSessionIdFromCookie())) { + /* + * This is the common(ish) use case: using the same session ID with multiple web applications on the same + * host. Typically this is used by Portlet implementations. It only works if sessions are tracked via + * cookies. The cookie must have a path of "/" else it won't be provided for requests to all web + * applications. + * + * Any session ID provided by the client should be for a session that already exists somewhere on the host. + * Check if the context is configured for this to be confirmed. + */ + if (context.getValidateClientProvidedNewSessionId()) { + boolean found = false; + for (Container container : getHost().findChildren()) { + Manager m = ((Context) container).getManager(); + if (m != null) { + try { + if (m.findSession(sessionId) != null) { + found = true; + break; + } + } catch (IOException e) { + // Ignore. Problems with this manager will be + // handled elsewhere. + } + } + } + if (!found) { + sessionId = null; + } + } + } else { + sessionId = null; + } + session = manager.createSession(sessionId); + + // Creating a new session cookie based on that session + if (session != null && trackModesIncludesCookie) { + Cookie cookie = + ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure()); + + response.addSessionCookieInternal(cookie); + } + + if (session == null) { + return null; + } + + session.access(); + return session; + } + + protected String unescape(String s) { + if (s == null) { + return null; + } + if (s.indexOf('\\') == -1) { + return s; + } + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c != '\\') { + buf.append(c); + } else { + if (++i >= s.length()) { + throw new IllegalArgumentException();// invalid escape, hence invalid cookie + } + c = s.charAt(i); + buf.append(c); + } + } + return buf.toString(); + } + + private CookieProcessor getCookieProcessor() { + Context context = getContext(); + if (context == null) { + // No context. Possible call from Valve before a Host level + // context rewrite when no ROOT content is configured. Use the + // default CookieProcessor. + return new Rfc6265CookieProcessor(); + } else { + return context.getCookieProcessor(); + } + } + + /** + * Parse cookies. This only parses the cookies into the memory efficient ServerCookies structure. It does not + * populate the Cookie objects. + */ + protected void parseCookies() { + if (cookiesParsed) { + return; + } + + cookiesParsed = true; + + ServerCookies serverCookies = coyoteRequest.getCookies(); + serverCookies.setLimit(connector.getMaxCookieCount()); + getCookieProcessor().parseCookieHeader(coyoteRequest.getMimeHeaders(), serverCookies); + } + + /** + * Converts the parsed cookies (parsing the Cookie headers first if they have not been parsed) into Cookie objects. + */ + protected void convertCookies() { + if (cookiesConverted) { + return; + } + + cookiesConverted = true; + + parseCookies(); + + ServerCookies serverCookies = coyoteRequest.getCookies(); + + int count = serverCookies.getCookieCount(); + if (count <= 0) { + return; + } + + cookies = new Cookie[count]; + + int idx = 0; + for (int i = 0; i < count; i++) { + ServerCookie scookie = serverCookies.getCookie(i); + try { + // We must unescape the '\\' escape character + Cookie cookie = new Cookie(scookie.getName().toString(), null); + scookie.getValue().getByteChunk().setCharset(getCookieProcessor().getCharset()); + cookie.setValue(unescape(scookie.getValue().toString())); + cookies[idx++] = cookie; + } catch (IllegalArgumentException e) { + // Ignore bad cookie + } + } + if (idx < count) { + Cookie[] ncookies = new Cookie[idx]; + System.arraycopy(cookies, 0, ncookies, 0, idx); + cookies = ncookies; + } + } + + + /** + * Parse request parameters. + */ + protected void parseParameters() { + + parametersParsed = true; + + Parameters parameters = coyoteRequest.getParameters(); + boolean success = false; + try { + // Set this every time in case limit has been changed via JMX + int maxParameterCount = getConnector().getMaxParameterCount(); + if (parts != null && maxParameterCount > 0) { + maxParameterCount -= parts.size(); + } + parameters.setLimit(maxParameterCount); + + // getCharacterEncoding() may have been overridden to search for + // hidden form field containing request encoding + Charset charset = getCharset(); + + boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI(); + parameters.setCharset(charset); + if (useBodyEncodingForURI) { + parameters.setQueryStringCharset(charset); + } + // Note: If !useBodyEncodingForURI, the query string encoding is + // that set towards the start of CoyoteAdapter.service() + + parameters.handleQueryParameters(); + + if (usingInputStream || usingReader) { + success = true; + return; + } + + String contentType = getContentType(); + if (contentType == null) { + contentType = ""; + } + int semicolon = contentType.indexOf(';'); + if (semicolon >= 0) { + contentType = contentType.substring(0, semicolon).trim(); + } else { + contentType = contentType.trim(); + } + + if ("multipart/form-data".equals(contentType)) { + parseParts(false); + success = true; + return; + } + + if (!getConnector().isParseBodyMethod(getMethod())) { + success = true; + return; + } + + if (!("application/x-www-form-urlencoded".equals(contentType))) { + success = true; + return; + } + + int len = getContentLength(); + + if (len > 0) { + int maxPostSize = connector.getMaxPostSize(); + if ((maxPostSize >= 0) && (len > maxPostSize)) { + Context context = getContext(); + if (context != null && context.getLogger().isDebugEnabled()) { + context.getLogger().debug(sm.getString("coyoteRequest.postTooLarge")); + } + checkSwallowInput(); + parameters.setParseFailedReason(FailReason.POST_TOO_LARGE); + return; + } + byte[] formData = null; + if (len < CACHED_POST_LEN) { + if (postData == null) { + postData = new byte[CACHED_POST_LEN]; + } + formData = postData; + } else { + formData = new byte[len]; + } + try { + readPostBodyFully(formData, len); + } catch (IOException e) { + // Client disconnect + Context context = getContext(); + if (context != null && context.getLogger().isDebugEnabled()) { + context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), e); + } + parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT); + return; + } + parameters.processParameters(formData, 0, len); + } else if ("chunked".equalsIgnoreCase(coyoteRequest.getHeader("transfer-encoding"))) { + byte[] formData = null; + try { + formData = readChunkedPostBody(); + } catch (IllegalStateException ise) { + // chunkedPostTooLarge error + parameters.setParseFailedReason(FailReason.POST_TOO_LARGE); + Context context = getContext(); + if (context != null && context.getLogger().isDebugEnabled()) { + context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), ise); + } + return; + } catch (IOException e) { + // Client disconnect + parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT); + Context context = getContext(); + if (context != null && context.getLogger().isDebugEnabled()) { + context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), e); + } + return; + } + if (formData != null) { + parameters.processParameters(formData, 0, formData.length); + } + } + success = true; + } finally { + if (!success) { + parameters.setParseFailedReason(FailReason.UNKNOWN); + } + } + + } + + + /** + * Read post body into an array. + * + * @param body The bytes array in which the body will be read + * @param len The body length + * + * @return the bytes count that has been read + * + * @throws IOException if an IO exception occurred + * + * @deprecated Unused. Will be removed in Tomcat 11.0.x onwards. Use {@link #readPostBodyFully(byte[], int)} + */ + @Deprecated + protected int readPostBody(byte[] body, int len) throws IOException { + + int offset = 0; + do { + int inputLen = getStream().read(body, offset, len - offset); + if (inputLen <= 0) { + return offset; + } + offset += inputLen; + } while ((len - offset) > 0); + return len; + + } + + + /** + * Read post body into an array. + * + * @param body The bytes array in which the body will be read + * @param len The body length + * + * @throws IOException if an IO exception occurred or EOF is reached before the body has been fully read + */ + protected void readPostBodyFully(byte[] body, int len) throws IOException { + int offset = 0; + do { + int inputLen = getStream().read(body, offset, len - offset); + if (inputLen <= 0) { + throw new EOFException(); + } + offset += inputLen; + } while ((len - offset) > 0); + } + + + /** + * Read chunked post body. + * + * @return the post body as a bytes array + * + * @throws IOException if an IO exception occurred + */ + protected byte[] readChunkedPostBody() throws IOException { + ByteChunk body = new ByteChunk(); + + byte[] buffer = new byte[CACHED_POST_LEN]; + + int len = 0; + while (len > -1) { + len = getStream().read(buffer, 0, CACHED_POST_LEN); + if (connector.getMaxPostSize() >= 0 && (body.getLength() + len) > connector.getMaxPostSize()) { + // Too much data + checkSwallowInput(); + throw new IllegalStateException(sm.getString("coyoteRequest.chunkedPostTooLarge")); + } + if (len > 0) { + body.append(buffer, 0, len); + } + } + if (body.getLength() == 0) { + return null; + } + if (body.getLength() < body.getBuffer().length) { + int length = body.getLength(); + byte[] result = new byte[length]; + System.arraycopy(body.getBuffer(), 0, result, 0, length); + return result; + } + + return body.getBuffer(); + } + + + /** + * Parse request locales. + */ + protected void parseLocales() { + + localesParsed = true; + + // Store the accumulated languages that have been requested in + // a local collection, sorted by the quality value (so we can + // add Locales in descending order). The values will be ArrayLists + // containing the corresponding Locales to be added + TreeMap> locales = new TreeMap<>(); + + Enumeration values = getHeaders("accept-language"); + + while (values.hasMoreElements()) { + String value = values.nextElement(); + parseLocalesHeader(value, locales); + } + + // Process the quality values in highest->lowest order (due to + // negating the Double value when creating the key) + for (ArrayList list : locales.values()) { + for (Locale locale : list) { + addLocale(locale); + } + } + } + + + /** + * Parse accept-language header value. + * + * @param value the header value + * @param locales the map that will hold the result + */ + protected void parseLocalesHeader(String value, TreeMap> locales) { + + List acceptLanguages; + try { + acceptLanguages = AcceptLanguage.parse(new StringReader(value)); + } catch (IOException e) { + // Mal-formed headers are ignore. Do the same in the unlikely event + // of an IOException. + return; + } + + for (AcceptLanguage acceptLanguage : acceptLanguages) { + // Add a new Locale to the list of Locales for this quality level + Double key = Double.valueOf(-acceptLanguage.getQuality()); // Reverse the order + locales.computeIfAbsent(key, k -> new ArrayList<>()).add(acceptLanguage.getLocale()); + } + } + + + // ----------------------------------------------------- Special attributes handling + + private interface SpecialAttributeAdapter { + Object get(Request request, String name); + + void set(Request request, String name, Object value); + + // None of special attributes support removal + // void remove(Request request, String name); + } + + private static final Map specialAttributes = new HashMap<>(); + + static { + specialAttributes.put(Globals.DISPATCHER_TYPE_ATTR, new SpecialAttributeAdapter() { + @Override + public Object get(Request request, String name) { + return (request.internalDispatcherType == null) ? DispatcherType.REQUEST : + request.internalDispatcherType; + } + + @Override + public void set(Request request, String name, Object value) { + request.internalDispatcherType = (DispatcherType) value; + } + }); + specialAttributes.put(Globals.DISPATCHER_REQUEST_PATH_ATTR, new SpecialAttributeAdapter() { + @Override + public Object get(Request request, String name) { + return (request.requestDispatcherPath == null) ? request.getRequestPathMB().toString() : + request.requestDispatcherPath.toString(); + } + + @Override + public void set(Request request, String name, Object value) { + request.requestDispatcherPath = value; + } + }); + specialAttributes.put(Globals.ASYNC_SUPPORTED_ATTR, new SpecialAttributeAdapter() { + @Override + public Object get(Request request, String name) { + return request.asyncSupported; + } + + @Override + public void set(Request request, String name, Object value) { + Boolean oldValue = request.asyncSupported; + request.asyncSupported = (Boolean) value; + request.notifyAttributeAssigned(name, value, oldValue); + } + }); + specialAttributes.put(Globals.GSS_CREDENTIAL_ATTR, new SpecialAttributeAdapter() { + @Override + public Object get(Request request, String name) { + if (request.userPrincipal instanceof TomcatPrincipal) { + return ((TomcatPrincipal) request.userPrincipal).getGssCredential(); + } + return null; + } + + @Override + public void set(Request request, String name, Object value) { + // NO-OP + } + }); + specialAttributes.put(Globals.PARAMETER_PARSE_FAILED_ATTR, new SpecialAttributeAdapter() { + @Override + public Object get(Request request, String name) { + if (request.getCoyoteRequest().getParameters().isParseFailed()) { + return Boolean.TRUE; + } + return null; + } + + @Override + public void set(Request request, String name, Object value) { + // NO-OP + } + }); + specialAttributes.put(Globals.PARAMETER_PARSE_FAILED_REASON_ATTR, new SpecialAttributeAdapter() { + @Override + public Object get(Request request, String name) { + return request.getCoyoteRequest().getParameters().getParseFailedReason(); + } + + @Override + public void set(Request request, String name, Object value) { + // NO-OP + } + }); + specialAttributes.put(Globals.SENDFILE_SUPPORTED_ATTR, new SpecialAttributeAdapter() { + @Override + public Object get(Request request, String name) { + return Boolean.valueOf(request.getConnector().getProtocolHandler().isSendfileSupported() && + request.getCoyoteRequest().getSendfile()); + } + + @Override + public void set(Request request, String name, Object value) { + // NO-OP + } + }); + specialAttributes.put(Globals.REMOTE_IP_FILTER_SECURE, new SpecialAttributeAdapter() { + @Override + public Object get(Request request, String name) { + return Boolean.valueOf(request.isSecure()); + } + + @Override + public void set(Request request, String name, Object value) { + if (value instanceof Boolean) { + request.setSecure(((Boolean) value).booleanValue()); + } + } + }); + } +} diff --git a/java/org/apache/catalina/connector/RequestFacade.java b/java/org/apache/catalina/connector/RequestFacade.java new file mode 100644 index 0000000..f6a7d29 --- /dev/null +++ b/java/org/apache/catalina/connector/RequestFacade.java @@ -0,0 +1,858 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedReader; +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConnection; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.Part; +import jakarta.servlet.http.PushBuilder; + +import org.apache.catalina.Globals; +import org.apache.catalina.security.SecurityUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + * Facade class that wraps a Coyote request object. All methods are delegated to the wrapped request. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class RequestFacade implements HttpServletRequest { + + + private final class GetAttributePrivilegedAction implements PrivilegedAction> { + + @Override + public Enumeration run() { + return request.getAttributeNames(); + } + } + + + private final class GetParameterMapPrivilegedAction implements PrivilegedAction> { + + @Override + public Map run() { + return request.getParameterMap(); + } + } + + + private final class GetRequestDispatcherPrivilegedAction implements PrivilegedAction { + + private final String path; + + GetRequestDispatcherPrivilegedAction(String path) { + this.path = path; + } + + @Override + public RequestDispatcher run() { + return request.getRequestDispatcher(path); + } + } + + + private final class GetParameterPrivilegedAction implements PrivilegedAction { + + public String name; + + GetParameterPrivilegedAction(String name) { + this.name = name; + } + + @Override + public String run() { + return request.getParameter(name); + } + } + + + private final class GetParameterNamesPrivilegedAction implements PrivilegedAction> { + + @Override + public Enumeration run() { + return request.getParameterNames(); + } + } + + + private final class GetParameterValuePrivilegedAction implements PrivilegedAction { + + public String name; + + GetParameterValuePrivilegedAction(String name) { + this.name = name; + } + + @Override + public String[] run() { + return request.getParameterValues(name); + } + } + + + private final class GetCookiesPrivilegedAction implements PrivilegedAction { + + @Override + public Cookie[] run() { + return request.getCookies(); + } + } + + + private final class GetCharacterEncodingPrivilegedAction implements PrivilegedAction { + + @Override + public String run() { + return request.getCharacterEncoding(); + } + } + + + private final class GetHeadersPrivilegedAction implements PrivilegedAction> { + + private final String name; + + GetHeadersPrivilegedAction(String name) { + this.name = name; + } + + @Override + public Enumeration run() { + return request.getHeaders(name); + } + } + + + private final class GetHeaderNamesPrivilegedAction implements PrivilegedAction> { + + @Override + public Enumeration run() { + return request.getHeaderNames(); + } + } + + + private final class GetLocalePrivilegedAction implements PrivilegedAction { + + @Override + public Locale run() { + return request.getLocale(); + } + } + + + private final class GetLocalesPrivilegedAction implements PrivilegedAction> { + + @Override + public Enumeration run() { + return request.getLocales(); + } + } + + private final class GetSessionPrivilegedAction implements PrivilegedAction { + + private final boolean create; + + GetSessionPrivilegedAction(boolean create) { + this.create = create; + } + + @Override + public HttpSession run() { + return request.getSession(create); + } + } + + + private static final StringManager sm = StringManager.getManager(RequestFacade.class); + + + /** + * The wrapped request. + */ + protected Request request = null; + + + /** + * Construct a wrapper for the specified request. + * + * @param request The request to be wrapped + */ + public RequestFacade(Request request) { + this.request = request; + } + + + /** + * Clear facade. + */ + public void clear() { + request = null; + } + + + /** + * Prevent cloning the facade. + */ + @Override + protected Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + + // ------------------------------------------------- ServletRequest Methods + + @Override + public Object getAttribute(String name) { + checkFacade(); + return request.getAttribute(name); + } + + + @Override + public Enumeration getAttributeNames() { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetAttributePrivilegedAction()); + } else { + return request.getAttributeNames(); + } + } + + + @Override + public String getCharacterEncoding() { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetCharacterEncodingPrivilegedAction()); + } else { + return request.getCharacterEncoding(); + } + } + + + @Override + public void setCharacterEncoding(String env) throws java.io.UnsupportedEncodingException { + checkFacade(); + request.setCharacterEncoding(env); + } + + + @Override + public int getContentLength() { + checkFacade(); + return request.getContentLength(); + } + + + @Override + public String getContentType() { + checkFacade(); + return request.getContentType(); + } + + + @Override + public ServletInputStream getInputStream() throws IOException { + checkFacade(); + return request.getInputStream(); + } + + + @Override + public String getParameter(String name) { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetParameterPrivilegedAction(name)); + } else { + return request.getParameter(name); + } + } + + + @Override + public Enumeration getParameterNames() { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetParameterNamesPrivilegedAction()); + } else { + return request.getParameterNames(); + } + } + + + @Override + public String[] getParameterValues(String name) { + checkFacade(); + + String[] ret = null; + + /* + * Clone the returned array only if there is a security manager in place, so that performance won't suffer in + * the non-secure case + */ + if (SecurityUtil.isPackageProtectionEnabled()) { + ret = AccessController.doPrivileged(new GetParameterValuePrivilegedAction(name)); + if (ret != null) { + ret = ret.clone(); + } + } else { + ret = request.getParameterValues(name); + } + + return ret; + } + + + @Override + public Map getParameterMap() { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetParameterMapPrivilegedAction()); + } else { + return request.getParameterMap(); + } + } + + + @Override + public String getProtocol() { + checkFacade(); + return request.getProtocol(); + } + + + @Override + public String getScheme() { + checkFacade(); + return request.getScheme(); + } + + + @Override + public String getServerName() { + checkFacade(); + return request.getServerName(); + } + + + @Override + public int getServerPort() { + checkFacade(); + return request.getServerPort(); + } + + + @Override + public BufferedReader getReader() throws IOException { + checkFacade(); + return request.getReader(); + } + + + @Override + public String getRemoteAddr() { + checkFacade(); + return request.getRemoteAddr(); + } + + + @Override + public String getRemoteHost() { + checkFacade(); + return request.getRemoteHost(); + } + + + @Override + public void setAttribute(String name, Object o) { + checkFacade(); + request.setAttribute(name, o); + } + + + @Override + public void removeAttribute(String name) { + checkFacade(); + request.removeAttribute(name); + } + + + @Override + public Locale getLocale() { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetLocalePrivilegedAction()); + } else { + return request.getLocale(); + } + } + + + @Override + public Enumeration getLocales() { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetLocalesPrivilegedAction()); + } else { + return request.getLocales(); + } + } + + + @Override + public boolean isSecure() { + checkFacade(); + return request.isSecure(); + } + + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetRequestDispatcherPrivilegedAction(path)); + } else { + return request.getRequestDispatcher(path); + } + } + + @Override + public String getAuthType() { + checkFacade(); + return request.getAuthType(); + } + + + @Override + public Cookie[] getCookies() { + checkFacade(); + + Cookie[] ret = null; + + /* + * Clone the returned array only if there is a security manager in place, so that performance won't suffer in + * the non-secure case + */ + if (SecurityUtil.isPackageProtectionEnabled()) { + ret = AccessController.doPrivileged(new GetCookiesPrivilegedAction()); + if (ret != null) { + ret = ret.clone(); + } + } else { + ret = request.getCookies(); + } + + return ret; + } + + + @Override + public long getDateHeader(String name) { + checkFacade(); + return request.getDateHeader(name); + } + + + @Override + public String getHeader(String name) { + checkFacade(); + return request.getHeader(name); + } + + + @Override + public Enumeration getHeaders(String name) { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetHeadersPrivilegedAction(name)); + } else { + return request.getHeaders(name); + } + } + + + @Override + public Enumeration getHeaderNames() { + checkFacade(); + + if (Globals.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged(new GetHeaderNamesPrivilegedAction()); + } else { + return request.getHeaderNames(); + } + } + + + @Override + public int getIntHeader(String name) { + checkFacade(); + return request.getIntHeader(name); + } + + + @Override + public HttpServletMapping getHttpServletMapping() { + checkFacade(); + return request.getHttpServletMapping(); + } + + + @Override + public String getMethod() { + checkFacade(); + return request.getMethod(); + } + + + @Override + public String getPathInfo() { + checkFacade(); + return request.getPathInfo(); + } + + + @Override + public String getPathTranslated() { + checkFacade(); + return request.getPathTranslated(); + } + + + @Override + public String getContextPath() { + checkFacade(); + return request.getContextPath(); + } + + + @Override + public String getQueryString() { + checkFacade(); + return request.getQueryString(); + } + + + @Override + public String getRemoteUser() { + checkFacade(); + return request.getRemoteUser(); + } + + + @Override + public boolean isUserInRole(String role) { + checkFacade(); + return request.isUserInRole(role); + } + + + @Override + public java.security.Principal getUserPrincipal() { + checkFacade(); + return request.getUserPrincipal(); + } + + + @Override + public String getRequestedSessionId() { + checkFacade(); + return request.getRequestedSessionId(); + } + + + @Override + public String getRequestURI() { + checkFacade(); + return request.getRequestURI(); + } + + + @Override + public StringBuffer getRequestURL() { + checkFacade(); + return request.getRequestURL(); + } + + + @Override + public String getServletPath() { + checkFacade(); + return request.getServletPath(); + } + + + @Override + public HttpSession getSession(boolean create) { + checkFacade(); + + if (SecurityUtil.isPackageProtectionEnabled()) { + return AccessController.doPrivileged(new GetSessionPrivilegedAction(create)); + } else { + return request.getSession(create); + } + } + + @Override + public HttpSession getSession() { + return getSession(true); + } + + @Override + public String changeSessionId() { + checkFacade(); + return request.changeSessionId(); + } + + @Override + public boolean isRequestedSessionIdValid() { + checkFacade(); + return request.isRequestedSessionIdValid(); + } + + + @Override + public boolean isRequestedSessionIdFromCookie() { + checkFacade(); + return request.isRequestedSessionIdFromCookie(); + } + + + @Override + public boolean isRequestedSessionIdFromURL() { + checkFacade(); + return request.isRequestedSessionIdFromURL(); + } + + + @Override + public String getLocalAddr() { + checkFacade(); + return request.getLocalAddr(); + } + + + @Override + public String getLocalName() { + checkFacade(); + return request.getLocalName(); + } + + + @Override + public int getLocalPort() { + checkFacade(); + return request.getLocalPort(); + } + + + @Override + public int getRemotePort() { + checkFacade(); + return request.getRemotePort(); + } + + + @Override + public ServletContext getServletContext() { + checkFacade(); + return request.getServletContext(); + } + + + @Override + public AsyncContext startAsync() throws IllegalStateException { + checkFacade(); + return request.startAsync(); + } + + + @Override + public AsyncContext startAsync(ServletRequest request, ServletResponse response) throws IllegalStateException { + checkFacade(); + return this.request.startAsync(request, response); + } + + + @Override + public boolean isAsyncStarted() { + checkFacade(); + return request.isAsyncStarted(); + } + + + @Override + public boolean isAsyncSupported() { + checkFacade(); + return request.isAsyncSupported(); + } + + + @Override + public AsyncContext getAsyncContext() { + checkFacade(); + return request.getAsyncContext(); + } + + + @Override + public DispatcherType getDispatcherType() { + checkFacade(); + return request.getDispatcherType(); + } + + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + checkFacade(); + return request.authenticate(response); + } + + @Override + public void login(String username, String password) throws ServletException { + checkFacade(); + request.login(username, password); + } + + @Override + public void logout() throws ServletException { + checkFacade(); + request.logout(); + } + + @Override + public Collection getParts() throws IllegalStateException, IOException, ServletException { + checkFacade(); + return request.getParts(); + } + + + @Override + public Part getPart(String name) throws IllegalStateException, IOException, ServletException { + checkFacade(); + return request.getPart(name); + } + + + public boolean getAllowTrace() { + checkFacade(); + return request.getConnector().getAllowTrace(); + } + + + @Override + public long getContentLengthLong() { + checkFacade(); + return request.getContentLengthLong(); + } + + + @Override + public T upgrade(Class httpUpgradeHandlerClass) + throws IOException, ServletException { + checkFacade(); + return request.upgrade(httpUpgradeHandlerClass); + } + + + @Override + public PushBuilder newPushBuilder() { + checkFacade(); + return request.newPushBuilder(); + } + + + public PushBuilder newPushBuilder(HttpServletRequest request) { + checkFacade(); + return this.request.newPushBuilder(request); + } + + + @Override + public boolean isTrailerFieldsReady() { + checkFacade(); + return request.isTrailerFieldsReady(); + } + + + @Override + public Map getTrailerFields() { + checkFacade(); + return request.getTrailerFields(); + } + + + @Override + public String getRequestId() { + checkFacade(); + return request.getRequestId(); + } + + + @Override + public String getProtocolRequestId() { + checkFacade(); + return request.getProtocolRequestId(); + } + + + @Override + public ServletConnection getServletConnection() { + checkFacade(); + return request.getServletConnection(); + } + + + private void checkFacade() { + if (request == null) { + throw new IllegalStateException(sm.getString("requestFacade.nullRequest")); + } + } +} diff --git a/java/org/apache/catalina/connector/Response.java b/java/org/apache/catalina/connector/Response.java new file mode 100644 index 0000000..f1e496e --- /dev/null +++ b/java/org/apache/catalina/connector/Response.java @@ -0,0 +1,1629 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.Charset; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import org.apache.catalina.Context; +import org.apache.catalina.Session; +import org.apache.catalina.security.SecurityUtil; +import org.apache.catalina.util.SessionConfig; +import org.apache.coyote.ActionCode; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.UEncoder; +import org.apache.tomcat.util.buf.UEncoder.SafeCharsSet; +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.MediaTypeCache; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; + +/** + * Wrapper object for the Coyote response. + * + * @author Remy Maucherat + * @author Craig R. McClanahan + */ +public class Response implements HttpServletResponse { + + private static final Log log = LogFactory.getLog(Response.class); + protected static final StringManager sm = StringManager.getManager(Response.class); + + private static final MediaTypeCache MEDIA_TYPE_CACHE = new MediaTypeCache(100); + + // ----------------------------------------------------- Instance Variables + + public Response() { + this(OutputBuffer.DEFAULT_BUFFER_SIZE); + } + + + public Response(int outputBufferSize) { + outputBuffer = new OutputBuffer(outputBufferSize); + } + + + // ------------------------------------------------------------- Properties + + /** + * Coyote response. + */ + protected org.apache.coyote.Response coyoteResponse; + + /** + * Set the Coyote response. + * + * @param coyoteResponse The Coyote response + */ + public void setCoyoteResponse(org.apache.coyote.Response coyoteResponse) { + this.coyoteResponse = coyoteResponse; + outputBuffer.setResponse(coyoteResponse); + } + + /** + * @return the Coyote response. + */ + public org.apache.coyote.Response getCoyoteResponse() { + return this.coyoteResponse; + } + + + /** + * @return the Context within which this Request is being processed. + */ + public Context getContext() { + return request.getContext(); + } + + + /** + * The associated output buffer. + */ + protected final OutputBuffer outputBuffer; + + + /** + * The associated output stream. + */ + protected CoyoteOutputStream outputStream; + + + /** + * The associated writer. + */ + protected CoyoteWriter writer; + + + /** + * The application commit flag. + */ + protected boolean appCommitted = false; + + + /** + * The included flag. + */ + protected boolean included = false; + + + /** + * The characterEncoding flag + */ + private boolean isCharacterEncodingSet = false; + + + /** + * Using output stream flag. + */ + protected boolean usingOutputStream = false; + + + /** + * Using writer flag. + */ + protected boolean usingWriter = false; + + + /** + * URL encoder. + */ + protected final UEncoder urlEncoder = new UEncoder(SafeCharsSet.WITH_SLASH); + + + /** + * Recyclable buffer to hold the redirect URL. + */ + protected final CharChunk redirectURLCC = new CharChunk(); + + + /* + * Not strictly required but it makes generating HTTP/2 push requests a lot easier if these are retained until the + * response is recycled. + */ + private final List cookies = new ArrayList<>(); + + private HttpServletResponse applicationResponse = null; + + + // --------------------------------------------------------- Public Methods + + /** + * Release all object references, and initialize instance variables, in preparation for reuse of this object. + */ + public void recycle() { + + cookies.clear(); + outputBuffer.recycle(); + usingOutputStream = false; + usingWriter = false; + appCommitted = false; + included = false; + isCharacterEncodingSet = false; + + applicationResponse = null; + if (getRequest().getDiscardFacades()) { + if (facade != null) { + facade.clear(); + facade = null; + } + if (outputStream != null) { + outputStream.clear(); + outputStream = null; + } + if (writer != null) { + writer.clear(); + writer = null; + } + } else if (writer != null) { + writer.recycle(); + } + + } + + + public List getCookies() { + return cookies; + } + + + // ------------------------------------------------------- Response Methods + + /** + * @return the number of bytes the application has actually written to the output stream. This excludes chunking, + * compression, etc. as well as headers. + */ + public long getContentWritten() { + return outputBuffer.getContentWritten(); + } + + + /** + * @return the number of bytes the actually written to the socket. This includes chunking, compression, etc. but + * excludes headers. + * + * @param flush if true will perform a buffer flush first + */ + public long getBytesWritten(boolean flush) { + if (flush) { + try { + outputBuffer.flush(); + } catch (IOException ioe) { + // Ignore - the client has probably closed the connection + } + } + return getCoyoteResponse().getBytesWritten(flush); + } + + /** + * Set the application commit flag. + * + * @param appCommitted The new application committed flag value + */ + public void setAppCommitted(boolean appCommitted) { + this.appCommitted = appCommitted; + } + + + /** + * Application commit flag accessor. + * + * @return true if the application has committed the response + */ + public boolean isAppCommitted() { + return this.appCommitted || isCommitted() || isSuspended() || + ((getContentLength() > 0) && (getContentWritten() >= getContentLength())); + } + + + /** + * The request with which this response is associated. + */ + protected Request request = null; + + /** + * @return the Request with which this Response is associated. + */ + public Request getRequest() { + return this.request; + } + + /** + * Set the Request with which this Response is associated. + * + * @param request The new associated request + */ + public void setRequest(Request request) { + this.request = request; + } + + + /** + * The facade associated with this response. + */ + protected ResponseFacade facade = null; + + + /** + * @return the ServletResponse for which this object is the facade. + */ + public HttpServletResponse getResponse() { + if (facade == null) { + facade = new ResponseFacade(this); + } + if (applicationResponse == null) { + applicationResponse = facade; + } + return applicationResponse; + } + + + /** + * Set a wrapped HttpServletResponse to pass to the application. Components wishing to wrap the response should + * obtain the response via {@link #getResponse()}, wrap it and then call this method with the wrapped response. + * + * @param applicationResponse The wrapped response to pass to the application + */ + public void setResponse(HttpServletResponse applicationResponse) { + // Check the wrapper wraps this request + ServletResponse r = applicationResponse; + while (r instanceof HttpServletResponseWrapper) { + r = ((HttpServletResponseWrapper) r).getResponse(); + } + if (r != facade) { + throw new IllegalArgumentException(sm.getString("response.illegalWrap")); + } + this.applicationResponse = applicationResponse; + } + + + /** + * Set the suspended flag. + * + * @param suspended The new suspended flag value + */ + public void setSuspended(boolean suspended) { + outputBuffer.setSuspended(suspended); + } + + + /** + * Suspended flag accessor. + * + * @return true if the response is suspended + */ + public boolean isSuspended() { + return outputBuffer.isSuspended(); + } + + + /** + * Closed flag accessor. + * + * @return true if the response has been closed + */ + public boolean isClosed() { + return outputBuffer.isClosed(); + } + + + /** + * Set the error flag. + * + * @return false if the error flag was already set + * + * @deprecated This method will be changed to return void in Tomcat 11 onwards + */ + @Deprecated + public boolean setError() { + return getCoyoteResponse().setError(); + } + + + /** + * Error flag accessor. + * + * @return true if the response has encountered an error + */ + public boolean isError() { + return getCoyoteResponse().isError(); + } + + + public boolean isErrorReportRequired() { + return getCoyoteResponse().isErrorReportRequired(); + } + + + public boolean setErrorReported() { + return getCoyoteResponse().setErrorReported(); + } + + + /** + * Perform whatever actions are required to flush and close the output stream or writer, in a single operation. + * + * @exception IOException if an input/output error occurs + */ + public void finishResponse() throws IOException { + // Writing leftover bytes + outputBuffer.close(); + } + + + /** + * @return the content length that was set or calculated for this Response. + */ + public int getContentLength() { + return getCoyoteResponse().getContentLength(); + } + + + @Override + public String getContentType() { + return getCoyoteResponse().getContentType(); + } + + + /** + * Return a PrintWriter that can be used to render error messages, regardless of whether a stream or writer has + * already been acquired. + * + * @return Writer which can be used for error reports. If the response is not an error report returned using + * sendError or triggered by an unexpected exception thrown during the servlet processing (and only in + * that case), null will be returned if the response stream has already been used. + * + * @exception IOException if an input/output error occurs + */ + public PrintWriter getReporter() throws IOException { + if (outputBuffer.isNew()) { + outputBuffer.checkConverter(); + if (writer == null) { + writer = new CoyoteWriter(outputBuffer); + } + return writer; + } else { + return null; + } + } + + + // ------------------------------------------------ ServletResponse Methods + + + @Override + public void flushBuffer() throws IOException { + outputBuffer.flush(); + } + + + @Override + public int getBufferSize() { + return outputBuffer.getBufferSize(); + } + + + @Override + public String getCharacterEncoding() { + String charset = getCoyoteResponse().getCharacterEncoding(); + if (charset != null) { + return charset; + } + + Context context = getContext(); + String result = null; + if (context != null) { + result = context.getResponseCharacterEncoding(); + } + + if (result == null) { + result = org.apache.coyote.Constants.DEFAULT_BODY_CHARSET.name(); + } + + return result; + } + + + @Override + public ServletOutputStream getOutputStream() throws IOException { + + if (usingWriter) { + throw new IllegalStateException(sm.getString("coyoteResponse.getOutputStream.ise")); + } + + usingOutputStream = true; + if (outputStream == null) { + outputStream = new CoyoteOutputStream(outputBuffer); + } + return outputStream; + + } + + + @Override + public Locale getLocale() { + return getCoyoteResponse().getLocale(); + } + + + @Override + public PrintWriter getWriter() throws IOException { + + if (usingOutputStream) { + throw new IllegalStateException(sm.getString("coyoteResponse.getWriter.ise")); + } + + if (request.getConnector().getEnforceEncodingInGetWriter()) { + /* + * If the response's character encoding has not been specified as described in + * getCharacterEncoding (i.e., the method just returns the default value + * ISO-8859-1), getWriter updates it to ISO-8859-1 (with the effect + * that a subsequent call to getContentType() will include a charset=ISO-8859-1 component which will also be + * reflected in the Content-Type response header, thereby satisfying the Servlet spec requirement that + * containers must communicate the character encoding used for the servlet response's writer to the client). + */ + setCharacterEncoding(getCharacterEncoding()); + } + + usingWriter = true; + outputBuffer.checkConverter(); + if (writer == null) { + writer = new CoyoteWriter(outputBuffer); + } + return writer; + } + + + @Override + public boolean isCommitted() { + return getCoyoteResponse().isCommitted(); + } + + + @Override + public void reset() { + // Ignore any call from an included servlet + if (included) { + return; + } + + getCoyoteResponse().reset(); + outputBuffer.reset(); + usingOutputStream = false; + usingWriter = false; + isCharacterEncodingSet = false; + } + + + @Override + public void resetBuffer() { + resetBuffer(false); + } + + + /** + * Reset the data buffer and the using Writer/Stream flags but not any status or header information. + * + * @param resetWriterStreamFlags true if the internal usingWriter, + * usingOutputStream, isCharacterEncodingSet flags + * should also be reset + * + * @exception IllegalStateException if the response has already been committed + */ + public void resetBuffer(boolean resetWriterStreamFlags) { + + if (isCommitted()) { + throw new IllegalStateException(sm.getString("coyoteResponse.resetBuffer.ise")); + } + + outputBuffer.reset(resetWriterStreamFlags); + + if (resetWriterStreamFlags) { + usingOutputStream = false; + usingWriter = false; + isCharacterEncodingSet = false; + } + + } + + + @Override + public void setBufferSize(int size) { + + if (isCommitted() || !outputBuffer.isNew()) { + throw new IllegalStateException(sm.getString("coyoteResponse.setBufferSize.ise")); + } + + outputBuffer.setBufferSize(size); + + } + + + @Override + public void setContentLength(int length) { + + setContentLengthLong(length); + } + + + @Override + public void setContentLengthLong(long length) { + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + getCoyoteResponse().setContentLength(length); + } + + + @Override + public void setContentType(String type) { + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + if (type == null) { + getCoyoteResponse().setContentType(null); + try { + getCoyoteResponse().setCharacterEncoding(null); + } catch (UnsupportedEncodingException e) { + // Can never happen when calling with null + } + isCharacterEncodingSet = false; + return; + } + + String[] m = MEDIA_TYPE_CACHE.parse(type); + if (m == null) { + // Invalid - Assume no charset and just pass through whatever + // the user provided. + getCoyoteResponse().setContentTypeNoCharset(type); + return; + } + + + if (m[1] == null) { + // No charset and we know value is valid as cache lookup was + // successful + // Pass-through user provided value in case user-agent is buggy and + // requires specific format + getCoyoteResponse().setContentTypeNoCharset(type); + } else { + // There is a charset so have to rebuild content-type without it + getCoyoteResponse().setContentTypeNoCharset(m[0]); + + // Ignore charset if getWriter() has already been called + if (!usingWriter) { + try { + getCoyoteResponse().setCharacterEncoding(m[1]); + } catch (UnsupportedEncodingException e) { + log.warn(sm.getString("coyoteResponse.encoding.invalid", m[1]), e); + } + + isCharacterEncodingSet = true; + } + } + } + + + @Override + public void setCharacterEncoding(String encoding) { + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + // Ignore any call made after the getWriter has been invoked + // The default should be used + if (usingWriter) { + return; + } + + try { + getCoyoteResponse().setCharacterEncoding(encoding); + } catch (UnsupportedEncodingException e) { + log.warn(sm.getString("coyoteResponse.encoding.invalid", encoding), e); + return; + } + if (encoding == null) { + isCharacterEncodingSet = false; + } else { + isCharacterEncodingSet = true; + } + } + + + @Override + public void setLocale(Locale locale) { + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + getCoyoteResponse().setLocale(locale); + + // Ignore any call made after the getWriter has been invoked. + // The default should be used + if (usingWriter) { + return; + } + + if (isCharacterEncodingSet) { + return; + } + + if (locale == null) { + try { + getCoyoteResponse().setCharacterEncoding(null); + } catch (UnsupportedEncodingException e) { + // Impossible when calling with null + } + } else { + // In some error handling scenarios, the context is unknown + // (e.g. a 404 when a ROOT context is not present) + Context context = getContext(); + if (context != null) { + String charset = context.getCharset(locale); + if (charset != null) { + try { + getCoyoteResponse().setCharacterEncoding(charset); + } catch (UnsupportedEncodingException e) { + log.warn(sm.getString("coyoteResponse.encoding.invalid", charset), e); + } + } + } + } + } + + + // --------------------------------------------------- HttpResponse Methods + + + @Override + public String getHeader(String name) { + return getCoyoteResponse().getMimeHeaders().getHeader(name); + } + + + @Override + public Collection getHeaderNames() { + MimeHeaders headers = getCoyoteResponse().getMimeHeaders(); + int n = headers.size(); + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + result.add(headers.getName(i).toString()); + } + return result; + + } + + + @Override + public Collection getHeaders(String name) { + Enumeration enumeration = getCoyoteResponse().getMimeHeaders().values(name); + Set result = new LinkedHashSet<>(); + while (enumeration.hasMoreElements()) { + result.add(enumeration.nextElement()); + } + return result; + } + + + /** + * @return the error message that was set with sendError() for this Response. + */ + public String getMessage() { + return getCoyoteResponse().getMessage(); + } + + + @Override + public int getStatus() { + return getCoyoteResponse().getStatus(); + } + + + // -------------------------------------------- HttpServletResponse Methods + + /** + * Add the specified Cookie to those that will be included with this Response. + * + * @param cookie Cookie to be added + */ + @Override + public void addCookie(final Cookie cookie) { + + // Ignore any call from an included servlet + if (included || isCommitted()) { + return; + } + + cookies.add(cookie); + + String header = generateCookieString(cookie); + // if we reached here, no exception, cookie is valid + addHeader("Set-Cookie", header, getContext().getCookieProcessor().getCharset()); + } + + /** + * Special method for adding a session cookie as we should be overriding any previous. + * + * @param cookie The new session cookie to add the response + */ + public void addSessionCookieInternal(final Cookie cookie) { + if (isCommitted()) { + return; + } + + String name = cookie.getName(); + final String headername = "Set-Cookie"; + final String startsWith = name + "="; + String header = generateCookieString(cookie); + boolean set = false; + MimeHeaders headers = getCoyoteResponse().getMimeHeaders(); + int n = headers.size(); + for (int i = 0; i < n; i++) { + if (headers.getName(i).toString().equals(headername)) { + if (headers.getValue(i).toString().startsWith(startsWith)) { + headers.getValue(i).setString(header); + set = true; + } + } + } + if (!set) { + addHeader(headername, header); + } + + + } + + public String generateCookieString(final Cookie cookie) { + // Web application code can receive a IllegalArgumentException + // from the generateHeader() invocation + if (SecurityUtil.isPackageProtectionEnabled()) { + return AccessController + .doPrivileged(new PrivilegedGenerateCookieString(getContext(), cookie, request.getRequest())); + } else { + return getContext().getCookieProcessor().generateHeader(cookie, request.getRequest()); + } + } + + + @Override + public void addDateHeader(String name, long value) { + + if (name == null || name.length() == 0) { + return; + } + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + addHeader(name, FastHttpDateFormat.formatDate(value)); + } + + + @Override + public void addHeader(String name, String value) { + addHeader(name, value, null); + } + + + private void addHeader(String name, String value, Charset charset) { + + if (name == null || name.length() == 0 || value == null) { + return; + } + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + char cc = name.charAt(0); + if (cc == 'C' || cc == 'c') { + if (checkSpecialHeader(name, value)) { + return; + } + } + + getCoyoteResponse().addHeader(name, value, charset); + } + + + /** + * An extended version of this exists in {@link org.apache.coyote.Response}. This check is required here to ensure + * that the usingWriter check in {@link #setContentType(String)} is applied since usingWriter is not visible to + * {@link org.apache.coyote.Response} Called from set/addHeader. + * + * @return true if the header is special, no need to set the header. + */ + private boolean checkSpecialHeader(String name, String value) { + if (name.equalsIgnoreCase("Content-Type")) { + setContentType(value); + return true; + } + return false; + } + + + @Override + public void addIntHeader(String name, int value) { + + if (name == null || name.length() == 0) { + return; + } + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + addHeader(name, "" + value); + + } + + + @Override + public boolean containsHeader(String name) { + // Need special handling for Content-Type and Content-Length due to + // special handling of these in coyoteResponse + char cc = name.charAt(0); + if (cc == 'C' || cc == 'c') { + if (name.equalsIgnoreCase("Content-Type")) { + // Will return null if this has not been set + return (getCoyoteResponse().getContentType() != null); + } + if (name.equalsIgnoreCase("Content-Length")) { + // -1 means not known and is not sent to client + return (getCoyoteResponse().getContentLengthLong() != -1); + } + } + + return getCoyoteResponse().containsHeader(name); + } + + + @Override + public void setTrailerFields(Supplier> supplier) { + getCoyoteResponse().setTrailerFields(supplier); + } + + + @Override + public Supplier> getTrailerFields() { + return getCoyoteResponse().getTrailerFields(); + } + + + @Override + public String encodeRedirectURL(String url) { + if (isEncodeable(toAbsolute(url))) { + return toEncoded(url, request.getSessionInternal().getIdInternal()); + } else { + return url; + } + } + + + @Override + public String encodeURL(String url) { + + String absolute; + try { + absolute = toAbsolute(url); + } catch (IllegalArgumentException iae) { + // Relative URL + return url; + } + + if (isEncodeable(absolute)) { + // W3c spec clearly said + if (url.equalsIgnoreCase("")) { + url = absolute; + } else if (url.equals(absolute) && !hasPath(url)) { + url += '/'; + } + return toEncoded(url, request.getSessionInternal().getIdInternal()); + } else { + return url; + } + + } + + + /** + * Send an acknowledgement of a request. + * + * @param continueResponseTiming Indicates when the request for the ACK originated so it can be compared with the + * configured timing for ACK responses. + * + * @exception IOException if an input/output error occurs + */ + public void sendAcknowledgement(ContinueResponseTiming continueResponseTiming) throws IOException { + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + getCoyoteResponse().action(ActionCode.ACK, continueResponseTiming); + } + + + @Override + public void sendError(int status) throws IOException { + sendError(status, null); + } + + + @Override + public void sendError(int status, String message) throws IOException { + + if (isCommitted()) { + throw new IllegalStateException(sm.getString("coyoteResponse.sendError.ise")); + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + setError(); + + getCoyoteResponse().setStatus(status); + getCoyoteResponse().setMessage(message); + + // Clear any data content that has been buffered + resetBuffer(); + + // Cause the response to be finished (from the application perspective) + setSuspended(true); + } + + + @Override + public void sendRedirect(String location) throws IOException { + sendRedirect(location, SC_FOUND); + } + + + /** + * Internal method that allows a redirect to be sent with a status other than {@link HttpServletResponse#SC_FOUND} + * (302). No attempt is made to validate the status code. + * + * @param location Location URL to redirect to + * @param status HTTP status code that will be sent + * + * @throws IOException an IO exception occurred + */ + public void sendRedirect(String location, int status) throws IOException { + if (isCommitted()) { + throw new IllegalStateException(sm.getString("coyoteResponse.sendRedirect.ise")); + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + // Clear any data content that has been buffered + resetBuffer(true); + + // Generate a temporary redirect to the specified location + try { + Context context = getContext(); + // If no ROOT context is defined, the context can be null. + // In this case, the default Tomcat values are assumed, but without + // reference to org.apache.catalina.STRICT_SERVLET_COMPLIANCE. + String locationUri; + // Relative redirects require HTTP/1.1 or later + if (getRequest().getCoyoteRequest().getSupportsRelativeRedirects() && + (context == null || context.getUseRelativeRedirects())) { + locationUri = location; + } else { + locationUri = toAbsolute(location); + } + setStatus(status); + setHeader("Location", locationUri); + if (context != null && context.getSendRedirectBody()) { + PrintWriter writer = getWriter(); + writer.print(sm.getString("coyoteResponse.sendRedirect.note", Escape.htmlElementContent(locationUri))); + flushBuffer(); + } + } catch (IllegalArgumentException e) { + log.warn(sm.getString("response.sendRedirectFail", location), e); + setStatus(SC_NOT_FOUND); + } + + // Cause the response to be finished (from the application perspective) + setSuspended(true); + } + + + @Override + public void setDateHeader(String name, long value) { + + if (name == null || name.length() == 0) { + return; + } + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + setHeader(name, FastHttpDateFormat.formatDate(value)); + } + + + @Override + public void setHeader(String name, String value) { + + if (name == null || name.length() == 0 || value == null) { + return; + } + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + char cc = name.charAt(0); + if (cc == 'C' || cc == 'c') { + if (checkSpecialHeader(name, value)) { + return; + } + } + + getCoyoteResponse().setHeader(name, value); + } + + + @Override + public void setIntHeader(String name, int value) { + + if (name == null || name.length() == 0) { + return; + } + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + setHeader(name, "" + value); + + } + + + @Override + public void setStatus(int status) { + + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + getCoyoteResponse().setStatus(status); + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Return true if the specified URL should be encoded with a session identifier. This will be true if + * all of the following conditions are met: + *
      + *
    • The request we are responding to asked for a valid session + *
    • The requested session ID was not received via a cookie + *
    • The specified URL points back to somewhere within the web application that is responding to this request + *
    + * + * @param location Absolute URL to be validated + * + * @return true if the URL should be encoded + */ + protected boolean isEncodeable(final String location) { + + if (location == null) { + return false; + } + + // Is this an intra-document reference? + if (location.startsWith("#")) { + return false; + } + + // Are we in a valid session that is not using cookies? + final Request hreq = request; + final Session session = hreq.getSessionInternal(false); + if (session == null) { + return false; + } + if (hreq.isRequestedSessionIdFromCookie()) { + return false; + } + + // Is URL encoding permitted + if (!hreq.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)) { + return false; + } + + if (SecurityUtil.isPackageProtectionEnabled()) { + Boolean result = + AccessController.doPrivileged(new PrivilegedDoIsEncodable(getContext(), hreq, session, location)); + return result.booleanValue(); + } else { + return doIsEncodeable(getContext(), hreq, session, location); + } + } + + + private static boolean doIsEncodeable(Context context, Request hreq, Session session, String location) { + // Is this a valid absolute URL? + URL url = null; + try { + URI uri = new URI(location); + url = uri.toURL(); + } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { + return false; + } + + // Does this URL match down to (and including) the context path? + if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol())) { + return false; + } + if (!hreq.getServerName().equalsIgnoreCase(url.getHost())) { + return false; + } + int serverPort = hreq.getServerPort(); + if (serverPort == -1) { + if ("https".equals(hreq.getScheme())) { + serverPort = 443; + } else { + serverPort = 80; + } + } + int urlPort = url.getPort(); + if (urlPort == -1) { + if ("https".equals(url.getProtocol())) { + urlPort = 443; + } else { + urlPort = 80; + } + } + if (serverPort != urlPort) { + return false; + } + + String contextPath = context.getPath(); + if (contextPath != null) { + String file = url.getFile(); + if (!file.startsWith(contextPath)) { + return false; + } + String tok = ";" + SessionConfig.getSessionUriParamName(context) + "=" + session.getIdInternal(); + if (file.indexOf(tok, contextPath.length()) >= 0) { + return false; + } + } + + // This URL belongs to our web application, so it is encodeable + return true; + + } + + + /** + * Convert (if necessary) and return the absolute URL that represents the resource referenced by this possibly + * relative URL. If this URL is already absolute, return it unchanged. + * + * @param location URL to be (possibly) converted and then returned + * + * @return the encoded URL + * + * @exception IllegalArgumentException if a MalformedURLException is thrown when converting the relative URL to an + * absolute one + */ + protected String toAbsolute(String location) { + + if (location == null) { + return location; + } + + boolean leadingSlash = location.startsWith("/"); + + if (location.startsWith("//")) { + // Scheme relative + redirectURLCC.recycle(); + // Add the scheme + String scheme = request.getScheme(); + try { + redirectURLCC.append(scheme, 0, scheme.length()); + redirectURLCC.append(':'); + redirectURLCC.append(location, 0, location.length()); + return redirectURLCC.toString(); + } catch (IOException e) { + throw new IllegalArgumentException(location, e); + } + + } else if (leadingSlash || !UriUtil.hasScheme(location)) { + + redirectURLCC.recycle(); + + String scheme = request.getScheme(); + String name = request.getServerName(); + int port = request.getServerPort(); + + try { + redirectURLCC.append(scheme, 0, scheme.length()); + redirectURLCC.append("://", 0, 3); + redirectURLCC.append(name, 0, name.length()); + if ((scheme.equals("http") && port != 80) || (scheme.equals("https") && port != 443)) { + redirectURLCC.append(':'); + String portS = port + ""; + redirectURLCC.append(portS, 0, portS.length()); + } + if (!leadingSlash) { + String relativePath = request.getDecodedRequestURI(); + int pos = relativePath.lastIndexOf('/'); + CharChunk encodedURI = null; + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + encodedURI = AccessController + .doPrivileged(new PrivilegedEncodeUrl(urlEncoder, relativePath, pos)); + } catch (PrivilegedActionException pae) { + throw new IllegalArgumentException(location, pae.getException()); + } + } else { + encodedURI = urlEncoder.encodeURL(relativePath, 0, pos); + } + redirectURLCC.append(encodedURI); + encodedURI.recycle(); + redirectURLCC.append('/'); + } + redirectURLCC.append(location, 0, location.length()); + + normalize(redirectURLCC); + } catch (IOException e) { + throw new IllegalArgumentException(location, e); + } + + return redirectURLCC.toString(); + + } else { + + return location; + + } + + } + + /** + * Removes /./ and /../ sequences from absolute URLs. Code borrowed heavily from CoyoteAdapter.normalize() + * + * @param cc the char chunk containing the chars to normalize + */ + private void normalize(CharChunk cc) { + // Strip query string and/or fragment first as doing it this way makes + // the normalization logic a lot simpler + int truncate = cc.indexOf('?'); + if (truncate == -1) { + truncate = cc.indexOf('#'); + } + char[] truncateCC = null; + if (truncate > -1) { + truncateCC = Arrays.copyOfRange(cc.getBuffer(), cc.getStart() + truncate, cc.getEnd()); + cc.setEnd(cc.getStart() + truncate); + } + + if (cc.endsWith("/.") || cc.endsWith("/..")) { + try { + cc.append('/'); + } catch (IOException e) { + throw new IllegalArgumentException(cc.toString(), e); + } + } + + char[] c = cc.getChars(); + int start = cc.getStart(); + int end = cc.getEnd(); + int index = 0; + int startIndex = 0; + + // Advance past the first three / characters (should place index just + // scheme://host[:port] + + for (int i = 0; i < 3; i++) { + startIndex = cc.indexOf('/', startIndex + 1); + } + + // Remove /./ + index = startIndex; + while (true) { + index = cc.indexOf("/./", 0, 3, index); + if (index < 0) { + break; + } + copyChars(c, start + index, start + index + 2, end - start - index - 2); + end = end - 2; + cc.setEnd(end); + } + + // Remove /../ + index = startIndex; + int pos; + while (true) { + index = cc.indexOf("/../", 0, 4, index); + if (index < 0) { + break; + } + // Can't go above the server root + if (index == startIndex) { + throw new IllegalArgumentException(); + } + int index2 = -1; + for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos--) { + if (c[pos] == (byte) '/') { + index2 = pos; + } + } + copyChars(c, start + index2, start + index + 3, end - start - index - 3); + end = end + index2 - index - 3; + cc.setEnd(end); + index = index2; + } + + // Add the query string and/or fragment (if present) back in + if (truncateCC != null) { + try { + cc.append(truncateCC, 0, truncateCC.length); + } catch (IOException ioe) { + throw new IllegalArgumentException(ioe); + } + } + } + + private void copyChars(char[] c, int dest, int src, int len) { + System.arraycopy(c, src, c, dest, len); + } + + + /** + * Determine if an absolute URL has a path component. + * + * @param uri the URL that will be checked + * + * @return true if the URL has a path + */ + private boolean hasPath(String uri) { + int pos = uri.indexOf("://"); + if (pos < 0) { + return false; + } + pos = uri.indexOf('/', pos + 3); + if (pos < 0) { + return false; + } + return true; + } + + /** + * Return the specified URL with the specified session identifier suitably encoded. + * + * @param url URL to be encoded with the session id + * @param sessionId Session id to be included in the encoded URL + * + * @return the encoded URL + */ + protected String toEncoded(String url, String sessionId) { + if (url == null || sessionId == null) { + return url; + } + + String path = url; + String query = ""; + String anchor = ""; + int question = url.indexOf('?'); + if (question >= 0) { + path = url.substring(0, question); + query = url.substring(question); + } + int pound = path.indexOf('#'); + if (pound >= 0) { + anchor = path.substring(pound); + path = path.substring(0, pound); + } + StringBuilder sb = new StringBuilder(path); + if (sb.length() > 0) { // jsessionid can't be first. + sb.append(';'); + sb.append(SessionConfig.getSessionUriParamName(request.getContext())); + sb.append('='); + sb.append(sessionId); + } + sb.append(anchor); + sb.append(query); + return sb.toString(); + } + + + private static class PrivilegedGenerateCookieString implements PrivilegedAction { + + private final Context context; + private final Cookie cookie; + private final HttpServletRequest request; + + PrivilegedGenerateCookieString(Context context, Cookie cookie, HttpServletRequest request) { + this.context = context; + this.cookie = cookie; + this.request = request; + } + + @Override + public String run() { + return context.getCookieProcessor().generateHeader(cookie, request); + } + } + + + private static class PrivilegedDoIsEncodable implements PrivilegedAction { + + private final Context context; + private final Request hreq; + private final Session session; + private final String location; + + PrivilegedDoIsEncodable(Context context, Request hreq, Session session, String location) { + this.context = context; + this.hreq = hreq; + this.session = session; + this.location = location; + } + + @Override + public Boolean run() { + return Boolean.valueOf(doIsEncodeable(context, hreq, session, location)); + } + } + + + private static class PrivilegedEncodeUrl implements PrivilegedExceptionAction { + + private final UEncoder urlEncoder; + private final String relativePath; + private final int end; + + PrivilegedEncodeUrl(UEncoder urlEncoder, String relativePath, int end) { + this.urlEncoder = urlEncoder; + this.relativePath = relativePath; + this.end = end; + } + + @Override + public CharChunk run() throws IOException { + return urlEncoder.encodeURL(relativePath, 0, end); + } + } +} diff --git a/java/org/apache/catalina/connector/ResponseFacade.java b/java/org/apache/catalina/connector/ResponseFacade.java new file mode 100644 index 0000000..5ad4c07 --- /dev/null +++ b/java/org/apache/catalina/connector/ResponseFacade.java @@ -0,0 +1,488 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.io.PrintWriter; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Globals; +import org.apache.catalina.security.SecurityUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + * Facade class that wraps a Coyote response object. All methods are delegated to the wrapped response. + * + * @author Remy Maucherat + */ +public class ResponseFacade implements HttpServletResponse { + + // ----------------------------------------------------------- DoPrivileged + + private final class SetContentTypePrivilegedAction implements PrivilegedAction { + + private final String contentType; + + SetContentTypePrivilegedAction(String contentType) { + this.contentType = contentType; + } + + @Override + public Void run() { + response.setContentType(contentType); + return null; + } + } + + private final class DateHeaderPrivilegedAction implements PrivilegedAction { + + private final String name; + private final long value; + private final boolean add; + + DateHeaderPrivilegedAction(String name, long value, boolean add) { + this.name = name; + this.value = value; + this.add = add; + } + + @Override + public Void run() { + if (add) { + response.addDateHeader(name, value); + } else { + response.setDateHeader(name, value); + } + return null; + } + } + + private static class FlushBufferPrivilegedAction implements PrivilegedExceptionAction { + + private final Response response; + + FlushBufferPrivilegedAction(Response response) { + this.response = response; + } + + @Override + public Void run() throws IOException { + response.setAppCommitted(true); + response.flushBuffer(); + return null; + } + } + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a wrapper for the specified response. + * + * @param response The response to be wrapped + */ + public ResponseFacade(Response response) { + this.response = response; + } + + + // ----------------------------------------------- Class/Instance Variables + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(ResponseFacade.class); + + + /** + * The wrapped response. + */ + protected Response response = null; + + + // --------------------------------------------------------- Public Methods + + /** + * Clear facade. + */ + public void clear() { + response = null; + } + + + /** + * Prevent cloning the facade. + */ + @Override + protected Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + + + public void finish() { + checkFacade(); + response.setSuspended(true); + } + + + public boolean isFinished() { + checkFacade(); + return response.isSuspended(); + } + + + public long getContentWritten() { + checkFacade(); + return response.getContentWritten(); + } + + + // ------------------------------------------------ ServletResponse Methods + + @Override + public String getCharacterEncoding() { + checkFacade(); + return response.getCharacterEncoding(); + } + + + @Override + public ServletOutputStream getOutputStream() throws IOException { + if (isFinished()) { + response.setSuspended(true); + } + return response.getOutputStream(); + } + + + @Override + public PrintWriter getWriter() throws IOException { + if (isFinished()) { + response.setSuspended(true); + } + return response.getWriter(); + } + + + @Override + public void setContentLength(int len) { + if (isCommitted()) { + return; + } + response.setContentLength(len); + } + + + @Override + public void setContentLengthLong(long length) { + if (isCommitted()) { + return; + } + response.setContentLengthLong(length); + } + + + @Override + public void setContentType(String type) { + if (isCommitted()) { + return; + } + + if (SecurityUtil.isPackageProtectionEnabled()) { + AccessController.doPrivileged(new SetContentTypePrivilegedAction(type)); + } else { + response.setContentType(type); + } + } + + + @Override + public void setBufferSize(int size) { + checkCommitted("coyoteResponse.setBufferSize.ise"); + response.setBufferSize(size); + } + + + @Override + public int getBufferSize() { + checkFacade(); + return response.getBufferSize(); + } + + + @Override + public void flushBuffer() throws IOException { + if (isFinished()) { + return; + } + + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + AccessController.doPrivileged(new FlushBufferPrivilegedAction(response)); + } catch (PrivilegedActionException e) { + Exception ex = e.getException(); + if (ex instanceof IOException) { + throw (IOException) ex; + } + } + } else { + response.setAppCommitted(true); + response.flushBuffer(); + } + } + + + @Override + public void resetBuffer() { + checkCommitted("coyoteResponse.resetBuffer.ise"); + response.resetBuffer(); + } + + + @Override + public boolean isCommitted() { + checkFacade(); + return response.isAppCommitted(); + } + + + @Override + public void reset() { + checkCommitted("coyoteResponse.reset.ise"); + response.reset(); + } + + + @Override + public void setLocale(Locale loc) { + if (isCommitted()) { + return; + } + response.setLocale(loc); + } + + + @Override + public Locale getLocale() { + checkFacade(); + return response.getLocale(); + } + + + @Override + public void addCookie(Cookie cookie) { + if (isCommitted()) { + return; + } + response.addCookie(cookie); + } + + + @Override + public boolean containsHeader(String name) { + checkFacade(); + return response.containsHeader(name); + } + + + @Override + public String encodeURL(String url) { + checkFacade(); + return response.encodeURL(url); + } + + + @Override + public String encodeRedirectURL(String url) { + checkFacade(); + return response.encodeRedirectURL(url); + } + + + @Override + public void sendError(int sc, String msg) throws IOException { + checkCommitted("coyoteResponse.sendError.ise"); + response.setAppCommitted(true); + response.sendError(sc, msg); + } + + + @Override + public void sendError(int sc) throws IOException { + checkCommitted("coyoteResponse.sendError.ise"); + response.setAppCommitted(true); + response.sendError(sc); + } + + + @Override + public void sendRedirect(String location) throws IOException { + checkCommitted("coyoteResponse.sendRedirect.ise"); + response.setAppCommitted(true); + response.sendRedirect(location); + } + + + @Override + public void setDateHeader(String name, long date) { + if (isCommitted()) { + return; + } + + if (Globals.IS_SECURITY_ENABLED) { + AccessController.doPrivileged(new DateHeaderPrivilegedAction(name, date, false)); + } else { + response.setDateHeader(name, date); + } + } + + + @Override + public void addDateHeader(String name, long date) { + if (isCommitted()) { + return; + } + + if (Globals.IS_SECURITY_ENABLED) { + AccessController.doPrivileged(new DateHeaderPrivilegedAction(name, date, true)); + } else { + response.addDateHeader(name, date); + } + } + + + @Override + public void setHeader(String name, String value) { + if (isCommitted()) { + return; + } + response.setHeader(name, value); + } + + + @Override + public void addHeader(String name, String value) { + if (isCommitted()) { + return; + } + response.addHeader(name, value); + } + + + @Override + public void setIntHeader(String name, int value) { + if (isCommitted()) { + return; + } + response.setIntHeader(name, value); + } + + + @Override + public void addIntHeader(String name, int value) { + if (isCommitted()) { + return; + } + response.addIntHeader(name, value); + } + + + @Override + public void setStatus(int sc) { + if (isCommitted()) { + return; + } + response.setStatus(sc); + } + + + @Override + public String getContentType() { + checkFacade(); + return response.getContentType(); + } + + + @Override + public void setCharacterEncoding(String encoding) { + checkFacade(); + response.setCharacterEncoding(encoding); + } + + @Override + public int getStatus() { + checkFacade(); + return response.getStatus(); + } + + @Override + public String getHeader(String name) { + checkFacade(); + return response.getHeader(name); + } + + @Override + public Collection getHeaderNames() { + checkFacade(); + return response.getHeaderNames(); + } + + @Override + public Collection getHeaders(String name) { + checkFacade(); + return response.getHeaders(name); + } + + + @Override + public void setTrailerFields(Supplier> supplier) { + checkFacade(); + response.setTrailerFields(supplier); + } + + + @Override + public Supplier> getTrailerFields() { + checkFacade(); + return response.getTrailerFields(); + } + + + private void checkFacade() { + if (response == null) { + throw new IllegalStateException(sm.getString("responseFacade.nullResponse")); + } + } + + + private void checkCommitted(String messageKey) { + if (isCommitted()) { + throw new IllegalStateException(sm.getString(messageKey)); + } + } +} diff --git a/java/org/apache/catalina/connector/mbeans-descriptors.xml b/java/org/apache/catalina/connector/mbeans-descriptors.xml new file mode 100644 index 0000000..fb55170 --- /dev/null +++ b/java/org/apache/catalina/connector/mbeans-descriptors.xml @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/core/AccessLogAdapter.java b/java/org/apache/catalina/core/AccessLogAdapter.java new file mode 100644 index 0000000..8a107a4 --- /dev/null +++ b/java/org/apache/catalina/core/AccessLogAdapter.java @@ -0,0 +1,63 @@ +/* + * 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.catalina.core; + +import java.util.Arrays; +import java.util.Objects; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; + +/** + * A helper class that wraps several AccessLog instances. + */ +public class AccessLogAdapter implements AccessLog { + + private AccessLog[] logs; + + public AccessLogAdapter(AccessLog log) { + Objects.requireNonNull(log); + logs = new AccessLog[] { log }; + } + + public void add(AccessLog log) { + Objects.requireNonNull(log); + AccessLog newArray[] = Arrays.copyOf(logs, logs.length + 1); + newArray[newArray.length - 1] = log; + logs = newArray; + } + + @Override + public void log(Request request, Response response, long time) { + for (AccessLog log : logs) { + log.log(request, response, time); + } + } + + @Override + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + // NOOP + } + + @Override + public boolean getRequestAttributesEnabled() { + // NOOP. Could return logs[0].getRequestAttributesEnabled(), but I do + // not see a use case for that. + return false; + } +} diff --git a/java/org/apache/catalina/core/ApplicationContext.java b/java/org/apache/catalina/core/ApplicationContext.java new file mode 100644 index 0000000..522d564 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationContext.java @@ -0,0 +1,1274 @@ +/* + * 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.catalina.core; + +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +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.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.naming.NamingException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextAttributeEvent; +import jakarta.servlet.ServletContextAttributeListener; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.ServletRegistration.Dynamic; +import jakarta.servlet.ServletRequestAttributeListener; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpSessionAttributeListener; +import jakarta.servlet.http.HttpSessionIdListener; +import jakarta.servlet.http.HttpSessionListener; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Service; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.mapper.MappingData; +import org.apache.catalina.util.Introspection; +import org.apache.catalina.util.ServerInfo; +import org.apache.catalina.util.URLEncoder; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.http.RequestUtil; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Standard implementation of ServletContext that represents a web application's execution environment. An + * instance of this class is associated with each instance of StandardContext. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class ApplicationContext implements ServletContext { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new instance of this class, associated with the specified Context instance. + * + * @param context The associated Context instance + */ + public ApplicationContext(StandardContext context) { + super(); + this.context = context; + this.service = ((Engine) context.getParent().getParent()).getService(); + this.sessionCookieConfig = new ApplicationSessionCookieConfig(context); + + // Populate session tracking modes + populateSessionTrackingModes(); + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The context attributes for this context. + */ + protected Map attributes = new ConcurrentHashMap<>(); + + + /** + * List of read only attributes for this context. + */ + private final Map readOnlyAttributes = new ConcurrentHashMap<>(); + + + /** + * The Context instance with which we are associated. + */ + private final StandardContext context; + + + /** + * The Service instance with which we are associated. + */ + private final Service service; + + + /** + * The facade around this object. + */ + private final ServletContext facade = new ApplicationContextFacade(this); + + + /** + * The merged context initialization parameters for this Context. + */ + private final Map parameters = new ConcurrentHashMap<>(); + + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(ApplicationContext.class); + + + /** + * Thread local data used during request dispatch. + */ + private final ThreadLocal dispatchData = new ThreadLocal<>(); + + + /** + * Session Cookie config + */ + private SessionCookieConfig sessionCookieConfig; + + /** + * Session tracking modes + */ + private Set sessionTrackingModes = null; + private Set defaultSessionTrackingModes = null; + private Set supportedSessionTrackingModes = null; + + /** + * Flag that indicates if a new {@link ServletContextListener} may be added to the application. Once the first + * {@link ServletContextListener} is called, no more may be added. + */ + private boolean newServletContextListenerAllowed = true; + + + // ------------------------------------------------- ServletContext Methods + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + + @Override + public Enumeration getAttributeNames() { + Set names = new HashSet<>(attributes.keySet()); + return Collections.enumeration(names); + } + + + @Override + public ServletContext getContext(String uri) { + + // Validate the format of the specified argument + if (uri == null || !uri.startsWith("/")) { + return null; + } + + Context child = null; + try { + // Look for an exact match + Container host = context.getParent(); + child = (Context) host.findChild(uri); + + // Non-running contexts should be ignored. + if (child != null && !child.getState().isAvailable()) { + child = null; + } + + // Remove any version information and use the mapper + if (child == null) { + int i = uri.indexOf("##"); + if (i > -1) { + uri = uri.substring(0, i); + } + // Note: This could be more efficient with a dedicated Mapper + // method but such an implementation would require some + // refactoring of the Mapper to avoid copy/paste of + // existing code. + MessageBytes hostMB = MessageBytes.newInstance(); + hostMB.setString(host.getName()); + + MessageBytes pathMB = MessageBytes.newInstance(); + pathMB.setString(uri); + + MappingData mappingData = new MappingData(); + service.getMapper().map(hostMB, pathMB, null, mappingData); + child = mappingData.context; + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + return null; + } + + if (child == null) { + return null; + } + + if (context.getCrossContext()) { + // If crossContext is enabled, can always return the context + return child.getServletContext(); + } else if (child == context) { + // Can still return the current context + return context.getServletContext(); + } else { + // Nothing to return + return null; + } + } + + + @Override + public String getContextPath() { + return context.getPath(); + } + + + @Override + public String getInitParameter(final String name) { + // Special handling for XML settings as the context setting must + // always override anything that might have been set by an application. + if (Globals.JASPER_XML_VALIDATION_TLD_INIT_PARAM.equals(name) && context.getTldValidation()) { + return "true"; + } + if (Globals.JASPER_XML_BLOCK_EXTERNAL_INIT_PARAM.equals(name)) { + if (!context.getXmlBlockExternal()) { + // System admin has explicitly changed the default + return "false"; + } + } + return parameters.get(name); + } + + + @Override + public Enumeration getInitParameterNames() { + Set names = new HashSet<>(parameters.keySet()); + // Special handling for XML settings as these attributes will always be + // available if they have been set on the context + if (context.getTldValidation()) { + names.add(Globals.JASPER_XML_VALIDATION_TLD_INIT_PARAM); + } + if (!context.getXmlBlockExternal()) { + names.add(Globals.JASPER_XML_BLOCK_EXTERNAL_INIT_PARAM); + } + return Collections.enumeration(names); + } + + + @Override + public int getMajorVersion() { + return Constants.MAJOR_VERSION; + } + + + @Override + public int getMinorVersion() { + return Constants.MINOR_VERSION; + } + + + @Override + public String getMimeType(String file) { + + if (file == null) { + return null; + } + int period = file.lastIndexOf('.'); + if (period < 0) { + return null; + } + String extension = file.substring(period + 1); + if (extension.length() < 1) { + return null; + } + return context.findMimeMapping(extension); + + } + + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + + // Validate the name argument + if (name == null) { + return null; + } + + // Create and return a corresponding request dispatcher + Wrapper wrapper = (Wrapper) context.findChild(name); + if (wrapper == null) { + return null; + } + + return new ApplicationDispatcher(wrapper, null, null, null, null, null, name); + + } + + + @Override + public String getRealPath(String path) { + String validatedPath = validateResourcePath(path, true); + return context.getRealPath(validatedPath); + } + + + @Override + public RequestDispatcher getRequestDispatcher(final String path) { + + // Validate the path argument + if (path == null) { + return null; + } + if (!path.startsWith("/")) { + throw new IllegalArgumentException(sm.getString("applicationContext.requestDispatcher.iae", path)); + } + + // Same processing order as InputBuffer / CoyoteAdapter + // First remove query string + String uri; + String queryString; + int pos = path.indexOf('?'); + if (pos >= 0) { + uri = path.substring(0, pos); + queryString = path.substring(pos + 1); + } else { + uri = path; + queryString = null; + } + + // Remove path parameters + String uriNoParams = stripPathParams(uri); + + // Then normalize + String normalizedUri = RequestUtil.normalize(uriNoParams); + if (normalizedUri == null) { + return null; + } + + // Mapping is against the normalized uri + + if (getContext().getDispatchersUseEncodedPaths()) { + // Decode + String decodedUri = UDecoder.URLDecode(normalizedUri, StandardCharsets.UTF_8); + + // Security check to catch attempts to encode /../ sequences + normalizedUri = RequestUtil.normalize(decodedUri); + if (!decodedUri.equals(normalizedUri)) { + getContext().getLogger().warn(sm.getString("applicationContext.illegalDispatchPath", path), + new IllegalArgumentException()); + return null; + } + + // URI needs to include the context path + uri = URLEncoder.DEFAULT.encode(getContextPath(), StandardCharsets.UTF_8) + uri; + } else { + // uri is passed to the constructor for ApplicationDispatcher and is + // ultimately used as the value for getRequestURI() which returns + // encoded values. Therefore, since the value passed in for path + // was decoded, encode uri here. + uri = URLEncoder.DEFAULT.encode(getContextPath() + uri, StandardCharsets.UTF_8); + } + + // Use the thread local URI and mapping data + DispatchData dd = dispatchData.get(); + if (dd == null) { + dd = new DispatchData(); + dispatchData.set(dd); + } + + // Use the thread local mapping data + MessageBytes uriMB = dd.uriMB; + MappingData mappingData = dd.mappingData; + + try { + // Map the URI + uriMB.setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + CharChunk uriCC = uriMB.getCharChunk(); + try { + uriCC.append(context.getPath()); + uriCC.append(normalizedUri); + service.getMapper().map(context, uriMB, mappingData); + if (mappingData.wrapper == null) { + return null; + } + } catch (Exception e) { + // Should never happen + log(sm.getString("applicationContext.mapping.error"), e); + return null; + } + + Wrapper wrapper = mappingData.wrapper; + String wrapperPath = mappingData.wrapperPath.toString(); + String pathInfo = mappingData.pathInfo.toString(); + HttpServletMapping mapping = new ApplicationMapping(mappingData).getHttpServletMapping(); + + // Construct a RequestDispatcher to process this request + return new ApplicationDispatcher(wrapper, uri, wrapperPath, pathInfo, queryString, mapping, null); + } finally { + // Recycle thread local data at the end of the request so references + // are not held to a completed request as there is potential for + // that to trigger a memory leak if a context is unloaded. Not + // strictly necessary here for uriMB but it needs to be recycled at + // some point so do it here for consistency with mappingData which + // must be recycled here. + uriMB.recycle(); + mappingData.recycle(); + } + } + + + // Package private to facilitate testing + static String stripPathParams(String input) { + // Shortcut + if (input.indexOf(';') < 0) { + return input; + } + + StringBuilder sb = new StringBuilder(input.length()); + int pos = 0; + int limit = input.length(); + while (pos < limit) { + int nextSemiColon = input.indexOf(';', pos); + if (nextSemiColon < 0) { + nextSemiColon = limit; + } + sb.append(input.substring(pos, nextSemiColon)); + int followingSlash = input.indexOf('/', nextSemiColon); + if (followingSlash < 0) { + pos = limit; + } else { + pos = followingSlash; + } + } + + return sb.toString(); + } + + + @Override + public URL getResource(String path) throws MalformedURLException { + + String validatedPath = validateResourcePath(path, !context.getContextGetResourceRequiresSlash()); + + if (validatedPath == null) { + throw new MalformedURLException(sm.getString("applicationContext.requestDispatcher.iae", path)); + } + + WebResourceRoot resources = context.getResources(); + if (resources != null) { + return resources.getResource(validatedPath).getURL(); + } + + return null; + } + + + @Override + public InputStream getResourceAsStream(String path) { + + String validatedPath = validateResourcePath(path, !context.getContextGetResourceRequiresSlash()); + + if (validatedPath == null) { + return null; + } + + WebResourceRoot resources = context.getResources(); + if (resources != null) { + return resources.getResource(validatedPath).getInputStream(); + } + + return null; + } + + + /* + * Returns null if the input path is not valid or a path that will be acceptable to resources.getResource(). + */ + private String validateResourcePath(String path, boolean addMissingInitialSlash) { + if (path == null) { + return null; + } + + if (!path.startsWith("/")) { + if (addMissingInitialSlash) { + return "/" + path; + } else { + return null; + } + } + + return path; + } + + + @Override + public Set getResourcePaths(String path) { + + // Validate the path argument + if (path == null) { + return null; + } + if (!path.startsWith("/")) { + throw new IllegalArgumentException(sm.getString("applicationContext.resourcePaths.iae", path)); + } + + WebResourceRoot resources = context.getResources(); + if (resources != null) { + return resources.listWebAppPaths(path); + } + + return null; + } + + + @Override + public String getServerInfo() { + return ServerInfo.getServerInfo(); + } + + + @Override + public String getServletContextName() { + return context.getDisplayName(); + } + + + @Override + public void log(String message) { + context.getLogger().info(message); + } + + + @Override + public void log(String message, Throwable throwable) { + context.getLogger().error(message, throwable); + } + + + @Override + public void removeAttribute(String name) { + + Object value = null; + + // Remove the specified attribute + // Check for read only attribute + if (readOnlyAttributes.containsKey(name)) { + return; + } + value = attributes.remove(name); + if (value == null) { + return; + } + + // Notify interested application event listeners + Object listeners[] = context.getApplicationEventListeners(); + if (listeners == null || listeners.length == 0) { + return; + } + ServletContextAttributeEvent event = new ServletContextAttributeEvent(context.getServletContext(), name, value); + for (Object obj : listeners) { + if (!(obj instanceof ServletContextAttributeListener)) { + continue; + } + ServletContextAttributeListener listener = (ServletContextAttributeListener) obj; + try { + context.fireContainerEvent("beforeContextAttributeRemoved", listener); + listener.attributeRemoved(event); + context.fireContainerEvent("afterContextAttributeRemoved", listener); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + context.fireContainerEvent("afterContextAttributeRemoved", listener); + log(sm.getString("applicationContext.attributeEvent"), t); + } + } + } + + + @Override + public void setAttribute(String name, Object value) { + // Name cannot be null + if (name == null) { + throw new NullPointerException(sm.getString("applicationContext.setAttribute.namenull")); + } + + // Null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + // Add or replace the specified attribute + // Check for read only attribute + if (readOnlyAttributes.containsKey(name)) { + return; + } + + Object oldValue = attributes.put(name, value); + boolean replaced = oldValue != null; + + // Notify interested application event listeners + Object listeners[] = context.getApplicationEventListeners(); + if (listeners == null || listeners.length == 0) { + return; + } + ServletContextAttributeEvent event = null; + if (replaced) { + event = new ServletContextAttributeEvent(context.getServletContext(), name, oldValue); + } else { + event = new ServletContextAttributeEvent(context.getServletContext(), name, value); + } + + for (Object obj : listeners) { + if (!(obj instanceof ServletContextAttributeListener)) { + continue; + } + ServletContextAttributeListener listener = (ServletContextAttributeListener) obj; + try { + if (replaced) { + context.fireContainerEvent("beforeContextAttributeReplaced", listener); + listener.attributeReplaced(event); + context.fireContainerEvent("afterContextAttributeReplaced", listener); + } else { + context.fireContainerEvent("beforeContextAttributeAdded", listener); + listener.attributeAdded(event); + context.fireContainerEvent("afterContextAttributeAdded", listener); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (replaced) { + context.fireContainerEvent("afterContextAttributeReplaced", listener); + } else { + context.fireContainerEvent("afterContextAttributeAdded", listener); + } + log(sm.getString("applicationContext.attributeEvent"), t); + } + } + } + + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) { + return addFilter(filterName, className, null); + } + + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + return addFilter(filterName, null, filter); + } + + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Class filterClass) { + return addFilter(filterName, filterClass.getName(), null); + } + + + private FilterRegistration.Dynamic addFilter(String filterName, String filterClass, Filter filter) + throws IllegalStateException { + + if (filterName == null || filterName.equals("")) { + throw new IllegalArgumentException(sm.getString("applicationContext.invalidFilterName", filterName)); + } + + // TODO Spec breaking enhancement to ignore this restriction + checkState("applicationContext.addFilter.ise"); + + FilterDef filterDef = context.findFilterDef(filterName); + + // Assume a 'complete' FilterRegistration is one that has a class and + // a name + if (filterDef == null) { + filterDef = new FilterDef(); + filterDef.setFilterName(filterName); + context.addFilterDef(filterDef); + } else { + if (filterDef.getFilterName() != null && filterDef.getFilterClass() != null) { + return null; + } + } + + if (filter == null) { + filterDef.setFilterClass(filterClass); + } else { + filterDef.setFilterClass(filter.getClass().getName()); + filterDef.setFilter(filter); + } + + return new ApplicationFilterRegistration(filterDef, context); + } + + + @Override + public T createFilter(Class c) throws ServletException { + try { + @SuppressWarnings("unchecked") + T filter = (T) context.getInstanceManager().newInstance(c.getName()); + return filter; + } catch (InvocationTargetException e) { + ExceptionUtils.handleThrowable(e.getCause()); + throw new ServletException(e); + } catch (ReflectiveOperationException | NamingException e) { + throw new ServletException(e); + } + } + + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + FilterDef filterDef = context.findFilterDef(filterName); + if (filterDef == null) { + return null; + } + return new ApplicationFilterRegistration(filterDef, context); + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, String className) { + return addServlet(servletName, className, null, null); + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) { + return addServlet(servletName, null, servlet, null); + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Class servletClass) { + return addServlet(servletName, servletClass.getName(), null, null); + } + + + @Override + public Dynamic addJspFile(String jspName, String jspFile) { + + // jspName is validated in addServlet() + if (jspFile == null || !jspFile.startsWith("/")) { + throw new IllegalArgumentException(sm.getString("applicationContext.addJspFile.iae", jspFile)); + } + + String jspServletClassName = null; + Map jspFileInitParams = new HashMap<>(); + + Wrapper jspServlet = (Wrapper) context.findChild("jsp"); + + if (jspServlet == null) { + // No JSP servlet currently defined. + // Use default JSP Servlet class name + jspServletClassName = Constants.JSP_SERVLET_CLASS; + } else { + // JSP Servlet defined. + // Use same JSP Servlet class name + jspServletClassName = jspServlet.getServletClass(); + // Use same init parameters + String[] params = jspServlet.findInitParameters(); + for (String param : params) { + jspFileInitParams.put(param, jspServlet.findInitParameter(param)); + } + } + + // Add init parameter to specify JSP file + jspFileInitParams.put("jspFile", jspFile); + + return addServlet(jspName, jspServletClassName, null, jspFileInitParams); + } + + + private ServletRegistration.Dynamic addServlet(String servletName, String servletClass, Servlet servlet, + Map initParams) throws IllegalStateException { + + if (servletName == null || servletName.equals("")) { + throw new IllegalArgumentException(sm.getString("applicationContext.invalidServletName", servletName)); + } + + // TODO Spec breaking enhancement to ignore this restriction + checkState("applicationContext.addServlet.ise"); + + Wrapper wrapper = (Wrapper) context.findChild(servletName); + + // Assume a 'complete' ServletRegistration is one that has a class and + // a name + if (wrapper == null) { + wrapper = context.createWrapper(); + wrapper.setName(servletName); + context.addChild(wrapper); + } else { + if (wrapper.getName() != null && wrapper.getServletClass() != null) { + if (wrapper.isOverridable()) { + wrapper.setOverridable(false); + } else { + return null; + } + } + } + + ServletSecurity annotation = null; + if (servlet == null) { + wrapper.setServletClass(servletClass); + Class clazz = Introspection.loadClass(context, servletClass); + if (clazz != null) { + annotation = clazz.getAnnotation(ServletSecurity.class); + } + } else { + wrapper.setServletClass(servlet.getClass().getName()); + wrapper.setServlet(servlet); + if (context.wasCreatedDynamicServlet(servlet)) { + annotation = servlet.getClass().getAnnotation(ServletSecurity.class); + } + } + + if (initParams != null) { + for (Map.Entry initParam : initParams.entrySet()) { + wrapper.addInitParameter(initParam.getKey(), initParam.getValue()); + } + } + + ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, context); + if (annotation != null) { + registration.setServletSecurity(new ServletSecurityElement(annotation)); + } + return registration; + } + + + @Override + public T createServlet(Class c) throws ServletException { + try { + @SuppressWarnings("unchecked") + T servlet = (T) context.getInstanceManager().newInstance(c.getName()); + context.dynamicServletCreated(servlet); + return servlet; + } catch (InvocationTargetException e) { + ExceptionUtils.handleThrowable(e.getCause()); + throw new ServletException(e); + } catch (ReflectiveOperationException | NamingException e) { + throw new ServletException(e); + } + } + + + @Override + public ServletRegistration getServletRegistration(String servletName) { + Wrapper wrapper = (Wrapper) context.findChild(servletName); + if (wrapper == null) { + return null; + } + + return new ApplicationServletRegistration(wrapper, context); + } + + + @Override + public Set getDefaultSessionTrackingModes() { + return defaultSessionTrackingModes; + } + + + private void populateSessionTrackingModes() { + // URL re-writing is always enabled by default + defaultSessionTrackingModes = EnumSet.of(SessionTrackingMode.URL); + supportedSessionTrackingModes = EnumSet.of(SessionTrackingMode.URL); + + if (context.getCookies()) { + defaultSessionTrackingModes.add(SessionTrackingMode.COOKIE); + supportedSessionTrackingModes.add(SessionTrackingMode.COOKIE); + } + + // SSL not enabled by default as it can only used on its own + // Context > Host > Engine > Service + Connector[] connectors = service.findConnectors(); + // Need at least one SSL enabled connector to use the SSL session ID. + for (Connector connector : connectors) { + if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) { + supportedSessionTrackingModes.add(SessionTrackingMode.SSL); + break; + } + } + } + + + @Override + public Set getEffectiveSessionTrackingModes() { + if (sessionTrackingModes != null) { + return sessionTrackingModes; + } + return defaultSessionTrackingModes; + } + + + @Override + public SessionCookieConfig getSessionCookieConfig() { + return sessionCookieConfig; + } + + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) { + + checkState("applicationContext.setSessionTracking.ise"); + + // Check that only supported tracking modes have been requested + for (SessionTrackingMode sessionTrackingMode : sessionTrackingModes) { + if (!supportedSessionTrackingModes.contains(sessionTrackingMode)) { + throw new IllegalArgumentException(sm.getString("applicationContext.setSessionTracking.iae.invalid", + sessionTrackingMode.toString(), getContextPath())); + } + } + + // Check SSL has not be configured with anything else + if (sessionTrackingModes.contains(SessionTrackingMode.SSL)) { + if (sessionTrackingModes.size() > 1) { + throw new IllegalArgumentException( + sm.getString("applicationContext.setSessionTracking.iae.ssl", getContextPath())); + } + } + + this.sessionTrackingModes = sessionTrackingModes; + } + + + @Override + public boolean setInitParameter(String name, String value) { + // Name cannot be null + if (name == null) { + throw new NullPointerException(sm.getString("applicationContext.setAttribute.namenull")); + } + checkState("applicationContext.setInitParam.ise"); + return parameters.putIfAbsent(name, value) == null; + } + + + @Override + public void addListener(Class listenerClass) { + EventListener listener; + try { + listener = createListener(listenerClass); + } catch (ServletException e) { + throw new IllegalArgumentException( + sm.getString("applicationContext.addListener.iae.init", listenerClass.getName()), e); + } + addListener(listener); + } + + + @Override + public void addListener(String className) { + + try { + if (context.getInstanceManager() != null) { + Object obj = context.getInstanceManager().newInstance(className); + + if (!(obj instanceof EventListener)) { + throw new IllegalArgumentException( + sm.getString("applicationContext.addListener.iae.wrongType", className)); + } + + EventListener listener = (EventListener) obj; + addListener(listener); + } + } catch (InvocationTargetException e) { + ExceptionUtils.handleThrowable(e.getCause()); + throw new IllegalArgumentException(sm.getString("applicationContext.addListener.iae.cnfe", className), e); + } catch (ReflectiveOperationException | NamingException e) { + throw new IllegalArgumentException(sm.getString("applicationContext.addListener.iae.cnfe", className), e); + } + + } + + + @Override + public void addListener(T t) { + checkState("applicationContext.addListener.ise"); + + boolean match = false; + if (t instanceof ServletContextAttributeListener || t instanceof ServletRequestListener || + t instanceof ServletRequestAttributeListener || t instanceof HttpSessionIdListener || + t instanceof HttpSessionAttributeListener) { + context.addApplicationEventListener(t); + match = true; + } + + if (t instanceof HttpSessionListener || + (t instanceof ServletContextListener && newServletContextListenerAllowed)) { + // Add listener directly to the list of instances rather than to + // the list of class names. + context.addApplicationLifecycleListener(t); + match = true; + } + + if (match) { + return; + } + + if (t instanceof ServletContextListener) { + throw new IllegalArgumentException( + sm.getString("applicationContext.addListener.iae.sclNotAllowed", t.getClass().getName())); + } else { + throw new IllegalArgumentException( + sm.getString("applicationContext.addListener.iae.wrongType", t.getClass().getName())); + } + } + + + @Override + public T createListener(Class c) throws ServletException { + try { + @SuppressWarnings("unchecked") + T listener = (T) context.getInstanceManager().newInstance(c); + if (listener instanceof ServletContextListener || listener instanceof ServletContextAttributeListener || + listener instanceof ServletRequestListener || listener instanceof ServletRequestAttributeListener || + listener instanceof HttpSessionListener || listener instanceof HttpSessionIdListener || + listener instanceof HttpSessionAttributeListener) { + return listener; + } + throw new IllegalArgumentException( + sm.getString("applicationContext.addListener.iae.wrongType", listener.getClass().getName())); + } catch (InvocationTargetException e) { + ExceptionUtils.handleThrowable(e.getCause()); + throw new ServletException(e); + } catch (ReflectiveOperationException | NamingException e) { + throw new ServletException(e); + } + } + + + @Override + public void declareRoles(String... roleNames) { + + // TODO Spec breaking enhancement to ignore this restriction + checkState("applicationContext.addRole.ise"); + + if (roleNames == null) { + throw new IllegalArgumentException(sm.getString("applicationContext.roles.iae", getContextPath())); + } + + for (String role : roleNames) { + if (role == null || role.isEmpty()) { + throw new IllegalArgumentException(sm.getString("applicationContext.role.iae", getContextPath())); + } + context.addSecurityRole(role); + } + } + + + @Override + public ClassLoader getClassLoader() { + ClassLoader result = context.getLoader().getClassLoader(); + if (Globals.IS_SECURITY_ENABLED) { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + ClassLoader parent = result; + while (parent != null) { + if (parent == tccl) { + break; + } + parent = parent.getParent(); + } + if (parent == null) { + System.getSecurityManager().checkPermission(new RuntimePermission("getClassLoader")); + } + } + + return result; + } + + + @Override + public int getEffectiveMajorVersion() { + return context.getEffectiveMajorVersion(); + } + + + @Override + public int getEffectiveMinorVersion() { + return context.getEffectiveMinorVersion(); + } + + + @Override + public Map getFilterRegistrations() { + Map result = new HashMap<>(); + + FilterDef[] filterDefs = context.findFilterDefs(); + for (FilterDef filterDef : filterDefs) { + result.put(filterDef.getFilterName(), new ApplicationFilterRegistration(filterDef, context)); + } + + return result; + } + + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return context.getJspConfigDescriptor(); + } + + + @Override + public Map getServletRegistrations() { + Map result = new HashMap<>(); + + Container[] wrappers = context.findChildren(); + for (Container wrapper : wrappers) { + result.put(wrapper.getName(), new ApplicationServletRegistration((Wrapper) wrapper, context)); + } + + return result; + } + + + @Override + public String getVirtualServerName() { + // Constructor will fail if context or its parent is null + Container host = context.getParent(); + Container engine = host.getParent(); + return engine.getName() + "/" + host.getName(); + } + + + @Override + public int getSessionTimeout() { + return context.getSessionTimeout(); + } + + + @Override + public void setSessionTimeout(int sessionTimeout) { + checkState("applicationContext.setSessionTimeout.ise"); + context.setSessionTimeout(sessionTimeout); + } + + + @Override + public String getRequestCharacterEncoding() { + return context.getRequestCharacterEncoding(); + } + + + @Override + public void setRequestCharacterEncoding(String encoding) { + checkState("applicationContext.setRequestEncoding.ise"); + context.setRequestCharacterEncoding(encoding); + } + + + @Override + public String getResponseCharacterEncoding() { + return context.getResponseCharacterEncoding(); + } + + + @Override + public void setResponseCharacterEncoding(String encoding) { + checkState("applicationContext.setResponseEncoding.ise"); + context.setResponseCharacterEncoding(encoding); + } + + + private void checkState(String messageKey) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException(sm.getString(messageKey, getContextPath())); + } + } + + + // -------------------------------------------------------- Package Methods + protected StandardContext getContext() { + return this.context; + } + + /** + * Clear all application-created attributes. + */ + protected void clearAttributes() { + + // Create list of attributes to be removed + List list = new ArrayList<>(attributes.keySet()); + + // Remove application originated attributes + // (read only attributes will be left in place) + for (String key : list) { + removeAttribute(key); + } + + } + + + /** + * @return the facade associated with this ApplicationContext. + */ + protected ServletContext getFacade() { + return this.facade; + } + + + /** + * Set an attribute as read only. + */ + void setAttributeReadOnly(String name) { + + if (attributes.containsKey(name)) { + readOnlyAttributes.put(name, name); + } + + } + + + protected void setNewServletContextListenerAllowed(boolean allowed) { + this.newServletContextListenerAllowed = allowed; + } + + /** + * Internal class used as thread-local storage when doing path mapping during dispatch. + */ + private static final class DispatchData { + + public MessageBytes uriMB; + public MappingData mappingData; + + DispatchData() { + uriMB = MessageBytes.newInstance(); + CharChunk uriCC = uriMB.getCharChunk(); + uriCC.setLimit(-1); + mappingData = new MappingData(); + } + } +} diff --git a/java/org/apache/catalina/core/ApplicationContextFacade.java b/java/org/apache/catalina/core/ApplicationContextFacade.java new file mode 100644 index 0000000..647781a --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationContextFacade.java @@ -0,0 +1,854 @@ +/* + * 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.catalina.core; + + +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +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.SessionCookieConfig; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.descriptor.JspConfigDescriptor; + +import org.apache.catalina.Globals; +import org.apache.catalina.security.SecurityUtil; +import org.apache.tomcat.util.ExceptionUtils; + + +/** + * Facade object which masks the internal ApplicationContext object from the web application. + * + * @author Remy Maucherat + */ +public class ApplicationContextFacade implements ServletContext { + + // ---------------------------------------------------------- Attributes + /** + * Cache Class object used for reflection. + */ + private final Map[]> classCache; + + + /** + * Cache method object. + */ + private final Map objectCache; + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new instance of this class, associated with the specified Context instance. + * + * @param context The associated Context instance + */ + public ApplicationContextFacade(ApplicationContext context) { + super(); + this.context = context; + + classCache = new HashMap<>(); + objectCache = new ConcurrentHashMap<>(); + initClassCache(); + } + + + private void initClassCache() { + Class[] clazz = new Class[] { String.class }; + classCache.put("getContext", clazz); + classCache.put("getMimeType", clazz); + classCache.put("getResourcePaths", clazz); + classCache.put("getResource", clazz); + classCache.put("getResourceAsStream", clazz); + classCache.put("getRequestDispatcher", clazz); + classCache.put("getNamedDispatcher", clazz); + classCache.put("getServlet", clazz); + classCache.put("setInitParameter", new Class[] { String.class, String.class }); + classCache.put("createServlet", new Class[] { Class.class }); + classCache.put("addServlet", new Class[] { String.class, String.class }); + classCache.put("createFilter", new Class[] { Class.class }); + classCache.put("addFilter", new Class[] { String.class, String.class }); + classCache.put("createListener", new Class[] { Class.class }); + classCache.put("addListener", clazz); + classCache.put("getFilterRegistration", clazz); + classCache.put("getServletRegistration", clazz); + classCache.put("getInitParameter", clazz); + classCache.put("setAttribute", new Class[] { String.class, Object.class }); + classCache.put("removeAttribute", clazz); + classCache.put("getRealPath", clazz); + classCache.put("getAttribute", clazz); + classCache.put("log", clazz); + classCache.put("setSessionTrackingModes", new Class[] { Set.class }); + classCache.put("addJspFile", new Class[] { String.class, String.class }); + classCache.put("declareRoles", new Class[] { String[].class }); + classCache.put("setSessionTimeout", new Class[] { int.class }); + classCache.put("setRequestCharacterEncoding", new Class[] { String.class }); + classCache.put("setResponseCharacterEncoding", new Class[] { String.class }); + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Wrapped application context. + */ + private final ApplicationContext context; + + + // ------------------------------------------------- ServletContext Methods + + + @Override + public ServletContext getContext(String uripath) { + ServletContext theContext = null; + if (SecurityUtil.isPackageProtectionEnabled()) { + theContext = (ServletContext) doPrivileged("getContext", new Object[] { uripath }); + } else { + theContext = context.getContext(uripath); + } + if ((theContext != null) && (theContext instanceof ApplicationContext)) { + theContext = ((ApplicationContext) theContext).getFacade(); + } + return theContext; + } + + + @Override + public int getMajorVersion() { + return context.getMajorVersion(); + } + + + @Override + public int getMinorVersion() { + return context.getMinorVersion(); + } + + + @Override + public String getMimeType(String file) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getMimeType", new Object[] { file }); + } else { + return context.getMimeType(file); + } + } + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public Set getResourcePaths(String path) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (Set) doPrivileged("getResourcePaths", new Object[] { path }); + } else { + return context.getResourcePaths(path); + } + } + + + @Override + public URL getResource(String path) throws MalformedURLException { + if (Globals.IS_SECURITY_ENABLED) { + try { + return (URL) invokeMethod(context, "getResource", new Object[] { path }); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (t instanceof MalformedURLException) { + throw (MalformedURLException) t; + } + return null; + } + } else { + return context.getResource(path); + } + } + + + @Override + public InputStream getResourceAsStream(String path) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (InputStream) doPrivileged("getResourceAsStream", new Object[] { path }); + } else { + return context.getResourceAsStream(path); + } + } + + + @Override + public RequestDispatcher getRequestDispatcher(final String path) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (RequestDispatcher) doPrivileged("getRequestDispatcher", new Object[] { path }); + } else { + return context.getRequestDispatcher(path); + } + } + + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (RequestDispatcher) doPrivileged("getNamedDispatcher", new Object[] { name }); + } else { + return context.getNamedDispatcher(name); + } + } + + + @Override + public void log(String msg) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("log", new Object[] { msg }); + } else { + context.log(msg); + } + } + + + @Override + public void log(String message, Throwable throwable) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("log", new Class[] { String.class, Throwable.class }, new Object[] { message, throwable }); + } else { + context.log(message, throwable); + } + } + + + @Override + public String getRealPath(String path) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getRealPath", new Object[] { path }); + } else { + return context.getRealPath(path); + } + } + + + @Override + public String getServerInfo() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getServerInfo", null); + } else { + return context.getServerInfo(); + } + } + + + @Override + public String getInitParameter(String name) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getInitParameter", new Object[] { name }); + } else { + return context.getInitParameter(name); + } + } + + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public Enumeration getInitParameterNames() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (Enumeration) doPrivileged("getInitParameterNames", null); + } else { + return context.getInitParameterNames(); + } + } + + + @Override + public Object getAttribute(String name) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return doPrivileged("getAttribute", new Object[] { name }); + } else { + return context.getAttribute(name); + } + } + + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public Enumeration getAttributeNames() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (Enumeration) doPrivileged("getAttributeNames", null); + } else { + return context.getAttributeNames(); + } + } + + + @Override + public void setAttribute(String name, Object object) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("setAttribute", new Object[] { name, object }); + } else { + context.setAttribute(name, object); + } + } + + + @Override + public void removeAttribute(String name) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("removeAttribute", new Object[] { name }); + } else { + context.removeAttribute(name); + } + } + + + @Override + public String getServletContextName() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getServletContextName", null); + } else { + return context.getServletContextName(); + } + } + + + @Override + public String getContextPath() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getContextPath", null); + } else { + return context.getContextPath(); + } + } + + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (FilterRegistration.Dynamic) doPrivileged("addFilter", new Object[] { filterName, className }); + } else { + return context.addFilter(filterName, className); + } + } + + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (FilterRegistration.Dynamic) doPrivileged("addFilter", new Class[] { String.class, Filter.class }, + new Object[] { filterName, filter }); + } else { + return context.addFilter(filterName, filter); + } + } + + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Class filterClass) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (FilterRegistration.Dynamic) doPrivileged("addFilter", new Class[] { String.class, Class.class }, + new Object[] { filterName, filterClass }); + } else { + return context.addFilter(filterName, filterClass); + } + } + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public T createFilter(Class c) throws ServletException { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + return (T) invokeMethod(context, "createFilter", new Object[] { c }); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (t instanceof ServletException) { + throw (ServletException) t; + } + return null; + } + } else { + return context.createFilter(c); + } + } + + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (FilterRegistration) doPrivileged("getFilterRegistration", new Object[] { filterName }); + } else { + return context.getFilterRegistration(filterName); + } + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, String className) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (ServletRegistration.Dynamic) doPrivileged("addServlet", new Object[] { servletName, className }); + } else { + return context.addServlet(servletName, className); + } + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (ServletRegistration.Dynamic) doPrivileged("addServlet", new Class[] { String.class, Servlet.class }, + new Object[] { servletName, servlet }); + } else { + return context.addServlet(servletName, servlet); + } + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Class servletClass) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (ServletRegistration.Dynamic) doPrivileged("addServlet", new Class[] { String.class, Class.class }, + new Object[] { servletName, servletClass }); + } else { + return context.addServlet(servletName, servletClass); + } + } + + + @Override + public Dynamic addJspFile(String jspName, String jspFile) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (ServletRegistration.Dynamic) doPrivileged("addJspFile", new Object[] { jspName, jspFile }); + } else { + return context.addJspFile(jspName, jspFile); + } + } + + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public T createServlet(Class c) throws ServletException { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + return (T) invokeMethod(context, "createServlet", new Object[] { c }); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (t instanceof ServletException) { + throw (ServletException) t; + } + return null; + } + } else { + return context.createServlet(c); + } + } + + + @Override + public ServletRegistration getServletRegistration(String servletName) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (ServletRegistration) doPrivileged("getServletRegistration", new Object[] { servletName }); + } else { + return context.getServletRegistration(servletName); + } + } + + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public Set getDefaultSessionTrackingModes() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (Set) doPrivileged("getDefaultSessionTrackingModes", null); + } else { + return context.getDefaultSessionTrackingModes(); + } + } + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public Set getEffectiveSessionTrackingModes() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (Set) doPrivileged("getEffectiveSessionTrackingModes", null); + } else { + return context.getEffectiveSessionTrackingModes(); + } + } + + + @Override + public SessionCookieConfig getSessionCookieConfig() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (SessionCookieConfig) doPrivileged("getSessionCookieConfig", null); + } else { + return context.getSessionCookieConfig(); + } + } + + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("setSessionTrackingModes", new Object[] { sessionTrackingModes }); + } else { + context.setSessionTrackingModes(sessionTrackingModes); + } + } + + + @Override + public boolean setInitParameter(String name, String value) { + if (SecurityUtil.isPackageProtectionEnabled()) { + return ((Boolean) doPrivileged("setInitParameter", new Object[] { name, value })).booleanValue(); + } else { + return context.setInitParameter(name, value); + } + } + + + @Override + public void addListener(Class listenerClass) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("addListener", new Class[] { Class.class }, new Object[] { listenerClass }); + } else { + context.addListener(listenerClass); + } + } + + + @Override + public void addListener(String className) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("addListener", new Object[] { className }); + } else { + context.addListener(className); + } + } + + + @Override + public void addListener(T t) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("addListener", new Class[] { EventListener.class }, new Object[] { t }); + } else { + context.addListener(t); + } + } + + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public T createListener(Class c) throws ServletException { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + return (T) invokeMethod(context, "createListener", new Object[] { c }); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (t instanceof ServletException) { + throw (ServletException) t; + } + return null; + } + } else { + return context.createListener(c); + } + } + + + @Override + public void declareRoles(String... roleNames) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("declareRoles", new Object[] { roleNames }); + } else { + context.declareRoles(roleNames); + } + } + + + @Override + public ClassLoader getClassLoader() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (ClassLoader) doPrivileged("getClassLoader", null); + } else { + return context.getClassLoader(); + } + } + + + @Override + public int getEffectiveMajorVersion() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return ((Integer) doPrivileged("getEffectiveMajorVersion", null)).intValue(); + } else { + return context.getEffectiveMajorVersion(); + } + } + + + @Override + public int getEffectiveMinorVersion() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return ((Integer) doPrivileged("getEffectiveMinorVersion", null)).intValue(); + } else { + return context.getEffectiveMinorVersion(); + } + } + + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public Map getFilterRegistrations() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (Map) doPrivileged("getFilterRegistrations", null); + } else { + return context.getFilterRegistrations(); + } + } + + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (JspConfigDescriptor) doPrivileged("getJspConfigDescriptor", null); + } else { + return context.getJspConfigDescriptor(); + } + } + + + @Override + @SuppressWarnings("unchecked") // doPrivileged() returns the correct type + public Map getServletRegistrations() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (Map) doPrivileged("getServletRegistrations", null); + } else { + return context.getServletRegistrations(); + } + } + + + @Override + public String getVirtualServerName() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getVirtualServerName", null); + } else { + return context.getVirtualServerName(); + } + } + + + @Override + public int getSessionTimeout() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return ((Integer) doPrivileged("getSessionTimeout", null)).intValue(); + } else { + return context.getSessionTimeout(); + } + } + + + @Override + public void setSessionTimeout(int sessionTimeout) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("setSessionTimeout", new Object[] { Integer.valueOf(sessionTimeout) }); + } else { + context.setSessionTimeout(sessionTimeout); + } + } + + + @Override + public String getRequestCharacterEncoding() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getRequestCharacterEncoding", null); + } else { + return context.getRequestCharacterEncoding(); + } + } + + + @Override + public void setRequestCharacterEncoding(String encoding) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("setRequestCharacterEncoding", new Object[] { encoding }); + } else { + context.setRequestCharacterEncoding(encoding); + } + } + + + @Override + public String getResponseCharacterEncoding() { + if (SecurityUtil.isPackageProtectionEnabled()) { + return (String) doPrivileged("getResponseCharacterEncoding", null); + } else { + return context.getResponseCharacterEncoding(); + } + } + + + @Override + public void setResponseCharacterEncoding(String encoding) { + if (SecurityUtil.isPackageProtectionEnabled()) { + doPrivileged("setResponseCharacterEncoding", new Object[] { encoding }); + } else { + context.setResponseCharacterEncoding(encoding); + } + } + + + /** + * Use reflection to invoke the requested method. Cache the method object to speed up the process + * + * @param methodName The method to call. + * @param params The arguments passed to the called method. + */ + private Object doPrivileged(final String methodName, final Object[] params) { + try { + return invokeMethod(context, methodName, params); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + throw new RuntimeException(t.getMessage(), t); + } + } + + + /** + * Use reflection to invoke the requested method. Cache the method object to speed up the process + * + * @param appContext The ApplicationContext object on which the method will be invoked + * @param methodName The method to call. + * @param params The arguments passed to the called method. + */ + private Object invokeMethod(ApplicationContext appContext, final String methodName, Object[] params) + throws Throwable { + + try { + Method method = objectCache.get(methodName); + if (method == null) { + method = appContext.getClass().getMethod(methodName, classCache.get(methodName)); + objectCache.put(methodName, method); + } + + return executeMethod(method, appContext, params); + } catch (Exception ex) { + handleException(ex); + return null; + } finally { + params = null; + } + } + + /** + * Use reflection to invoke the requested method. Cache the method object to speed up the process + * + * @param methodName The method to invoke. + * @param clazz The class where the method is. + * @param params The arguments passed to the called method. + */ + private Object doPrivileged(final String methodName, final Class[] clazz, Object[] params) { + + try { + Method method = context.getClass().getMethod(methodName, clazz); + return executeMethod(method, context, params); + } catch (Exception ex) { + try { + handleException(ex); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + throw new RuntimeException(t.getMessage()); + } + return null; + } finally { + params = null; + } + } + + + /** + * Executes the method of the specified ApplicationContext + * + * @param method The method object to be invoked. + * @param context The ApplicationContext object on which the method will be invoked + * @param params The arguments passed to the called method. + */ + private Object executeMethod(final Method method, final ApplicationContext context, final Object[] params) + throws PrivilegedActionException, IllegalAccessException, InvocationTargetException { + + if (SecurityUtil.isPackageProtectionEnabled()) { + return AccessController.doPrivileged(new PrivilegedExecuteMethod(method, context, params)); + } else { + return method.invoke(context, params); + } + } + + + /** + * Throw the real exception. + * + * @param ex The current exception + */ + private void handleException(Exception ex) throws Throwable { + + Throwable realException; + + if (ex instanceof PrivilegedActionException) { + ex = ((PrivilegedActionException) ex).getException(); + } + + if (ex instanceof InvocationTargetException) { + realException = ex.getCause(); + if (realException == null) { + realException = ex; + } + } else { + realException = ex; + } + + throw realException; + } + + + private static class PrivilegedExecuteMethod implements PrivilegedExceptionAction { + + private final Method method; + private final ApplicationContext context; + private final Object[] params; + + PrivilegedExecuteMethod(Method method, ApplicationContext context, Object[] params) { + this.method = method; + this.context = context; + this.params = params; + } + + @Override + public Object run() throws Exception { + return method.invoke(context, params); + } + } +} diff --git a/java/org/apache/catalina/core/ApplicationDispatcher.java b/java/org/apache/catalina/core/ApplicationDispatcher.java new file mode 100644 index 0000000..e2f1ad3 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationDispatcher.java @@ -0,0 +1,953 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.PrintWriter; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestWrapper; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.ServletResponseWrapper; +import jakarta.servlet.UnavailableException; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.AsyncDispatcher; +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.RequestFacade; +import org.apache.catalina.connector.Response; +import org.apache.catalina.connector.ResponseFacade; +import org.apache.coyote.BadRequestException; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Standard implementation of RequestDispatcher that allows a request to be forwarded to a different + * resource to create the ultimate response, or to include the output of another resource in the response from this + * resource. This implementation allows application level servlets to wrap the request and/or response objects that are + * passed on to the called resource, as long as the wrapping classes extend + * jakarta.servlet.ServletRequestWrapper and jakarta.servlet.ServletResponseWrapper. + * + * @author Craig R. McClanahan + */ +final class ApplicationDispatcher implements AsyncDispatcher, RequestDispatcher { + + protected class PrivilegedForward implements PrivilegedExceptionAction { + private final ServletRequest request; + private final ServletResponse response; + + PrivilegedForward(ServletRequest request, ServletResponse response) { + this.request = request; + this.response = response; + } + + @Override + public Void run() throws java.lang.Exception { + doForward(request, response); + return null; + } + } + + protected class PrivilegedInclude implements PrivilegedExceptionAction { + private final ServletRequest request; + private final ServletResponse response; + + PrivilegedInclude(ServletRequest request, ServletResponse response) { + this.request = request; + this.response = response; + } + + @Override + public Void run() throws ServletException, IOException { + doInclude(request, response); + return null; + } + } + + protected class PrivilegedDispatch implements PrivilegedExceptionAction { + private final ServletRequest request; + private final ServletResponse response; + + PrivilegedDispatch(ServletRequest request, ServletResponse response) { + this.request = request; + this.response = response; + } + + @Override + public Void run() throws ServletException, IOException { + doDispatch(request, response); + return null; + } + } + + + /** + * Used to pass state when the request dispatcher is used. Using instance variables causes threading issues and + * state is too complex to pass and return single ServletRequest or ServletResponse objects. + */ + private static class State { + State(ServletRequest request, ServletResponse response, boolean including) { + this.outerRequest = request; + this.outerResponse = response; + this.including = including; + } + + /** + * The outermost request that will be passed on to the invoked servlet. + */ + ServletRequest outerRequest = null; + + + /** + * The outermost response that will be passed on to the invoked servlet. + */ + ServletResponse outerResponse = null; + + /** + * The request wrapper we have created and installed (if any). + */ + ServletRequest wrapRequest = null; + + + /** + * The response wrapper we have created and installed (if any). + */ + ServletResponse wrapResponse = null; + + /** + * Are we performing an include() instead of a forward()? + */ + boolean including = false; + + /** + * Outermost HttpServletRequest in the chain + */ + HttpServletRequest hrequest = null; + + /** + * Outermost HttpServletResponse in the chain + */ + HttpServletResponse hresponse = null; + } + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new instance of this class, configured according to the specified parameters. If both servletPath and + * pathInfo are null, it will be assumed that this RequestDispatcher was acquired by name, rather than + * by path. + * + * @param wrapper The Wrapper associated with the resource that will be forwarded to or included (required) + * @param requestURI The request URI to this resource (if any) + * @param servletPath The revised servlet path to this resource (if any) + * @param pathInfo The revised extra path information to this resource (if any) + * @param queryString Query string parameters included with this request (if any) + * @param mapping The mapping for this resource (if any) + * @param name Servlet name (if a named dispatcher was created) else null + */ + ApplicationDispatcher(Wrapper wrapper, String requestURI, String servletPath, String pathInfo, String queryString, + HttpServletMapping mapping, String name) { + + super(); + + // Save all of our configuration parameters + this.wrapper = wrapper; + this.context = (Context) wrapper.getParent(); + this.requestURI = requestURI; + this.servletPath = servletPath; + this.pathInfo = pathInfo; + this.queryString = queryString; + this.mapping = mapping; + this.name = name; + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The Context this RequestDispatcher is associated with. + */ + private final Context context; + + + /** + * The servlet name for a named dispatcher. + */ + private final String name; + + + /** + * The extra path information for this RequestDispatcher. + */ + private final String pathInfo; + + + /** + * The query string parameters for this RequestDispatcher. + */ + private final String queryString; + + + /** + * The request URI for this RequestDispatcher. + */ + private final String requestURI; + + + /** + * The servlet path for this RequestDispatcher. + */ + private final String servletPath; + + + /** + * The mapping for this RequestDispatcher. + */ + private final HttpServletMapping mapping; + + + /** + * The StringManager for this package. + */ + private static final StringManager sm = StringManager.getManager(ApplicationDispatcher.class); + + + /** + * The Wrapper associated with the resource that will be forwarded to or included. + */ + private final Wrapper wrapper; + + + // --------------------------------------------------------- Public Methods + + + @Override + public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException { + if (Globals.IS_SECURITY_ENABLED) { + try { + PrivilegedForward dp = new PrivilegedForward(request, response); + AccessController.doPrivileged(dp); + } catch (PrivilegedActionException pe) { + Exception e = pe.getException(); + if (e instanceof ServletException) { + throw (ServletException) e; + } + throw (IOException) e; + } + } else { + doForward(request, response); + } + } + + private void doForward(ServletRequest request, ServletResponse response) throws ServletException, IOException { + + // Reset any output that has been buffered, but keep headers/cookies + if (response.isCommitted()) { + throw new IllegalStateException(sm.getString("applicationDispatcher.forward.ise")); + } + try { + response.resetBuffer(); + } catch (IllegalStateException e) { + throw e; + } + + // Set up to handle the specified request and response + State state = new State(request, response, false); + + if (context.getDispatcherWrapsSameObject()) { + // Check SRV.9.2 / RequestDispatcher Javadoc + checkSameObjects(request, response); + } + + wrapResponse(state); + // Handle an HTTP named dispatcher forward + if (servletPath == null && pathInfo == null) { + + ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state); + HttpServletRequest hrequest = state.hrequest; + wrequest.setRequestURI(hrequest.getRequestURI()); + wrequest.setContextPath(hrequest.getContextPath()); + wrequest.setServletPath(hrequest.getServletPath()); + wrequest.setPathInfo(hrequest.getPathInfo()); + wrequest.setQueryString(hrequest.getQueryString()); + + processRequest(request, response, state); + } + + // Handle an HTTP path-based forward + else { + + ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state); + HttpServletRequest hrequest = state.hrequest; + if (hrequest.getAttribute(FORWARD_REQUEST_URI) == null) { + wrequest.setAttribute(FORWARD_REQUEST_URI, hrequest.getRequestURI()); + wrequest.setAttribute(FORWARD_CONTEXT_PATH, hrequest.getContextPath()); + wrequest.setAttribute(FORWARD_SERVLET_PATH, hrequest.getServletPath()); + wrequest.setAttribute(FORWARD_PATH_INFO, hrequest.getPathInfo()); + wrequest.setAttribute(FORWARD_QUERY_STRING, hrequest.getQueryString()); + wrequest.setAttribute(FORWARD_MAPPING, hrequest.getHttpServletMapping()); + } + + wrequest.setContextPath(context.getEncodedPath()); + wrequest.setRequestURI(requestURI); + wrequest.setServletPath(servletPath); + wrequest.setPathInfo(pathInfo); + if (queryString != null) { + wrequest.setQueryString(queryString); + wrequest.setQueryParams(queryString); + } + wrequest.setMapping(mapping); + + processRequest(request, response, state); + } + + if (request.isAsyncStarted()) { + // An async request was started during the forward, don't close the + // response as it may be written to during the async handling + return; + } + + // This is not a real close in order to support error processing + if (wrapper.getLogger().isTraceEnabled()) { + wrapper.getLogger().trace(" Disabling the response for further output"); + } + + boolean finished = false; + if (response instanceof ResponseFacade) { + finished = true; + ((ResponseFacade) response).finish(); + } else if (context.getSuspendWrappedResponseAfterForward() + && response instanceof ServletResponseWrapper) { + ServletResponse baseResponse = response; + do { + baseResponse = ((ServletResponseWrapper) baseResponse).getResponse(); + } while (baseResponse instanceof ServletResponseWrapper); + if (baseResponse instanceof ResponseFacade) { + finished = true; + ((ResponseFacade) baseResponse).finish(); + } + } + if (!finished) { + // Servlet SRV.6.2.2. The Request/Response may have been wrapped + // and may no longer be instance of RequestFacade + if (wrapper.getLogger().isDebugEnabled()) { + wrapper.getLogger().debug(sm.getString("applicationDispatcher.customResponse", response.getClass())); + } + // Close anyway + try { + PrintWriter writer = response.getWriter(); + writer.close(); + } catch (IllegalStateException e) { + try { + ServletOutputStream stream = response.getOutputStream(); + stream.close(); + } catch (IllegalStateException | IOException f) { + // Ignore + } + } catch (IOException e) { + // Ignore + } + } + + } + + + /** + * Prepare the request based on the filter configuration. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param state The RD state + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + private void processRequest(ServletRequest request, ServletResponse response, State state) + throws IOException, ServletException { + + DispatcherType disInt = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR); + if (disInt != null) { + boolean doInvoke = true; + + if (context.getFireRequestListenersOnForwards() && !context.fireRequestInitEvent(request)) { + doInvoke = false; + } + + if (doInvoke) { + if (disInt != DispatcherType.ERROR) { + state.outerRequest.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, getCombinedPath()); + state.outerRequest.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.FORWARD); + invoke(state.outerRequest, response, state); + } else { + invoke(state.outerRequest, response, state); + } + + if (context.getFireRequestListenersOnForwards()) { + context.fireRequestDestroyEvent(request); + } + } + } + } + + + /** + * Combine the servletPath and the pathInfo. If pathInfo is null it is ignored. If servletPath is + * null then null is returned. + * + * @return The combined path with pathInfo appended to servletInfo + */ + private String getCombinedPath() { + if (servletPath == null) { + return null; + } + if (pathInfo == null) { + return servletPath; + } + return servletPath + pathInfo; + } + + + @Override + public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException { + if (Globals.IS_SECURITY_ENABLED) { + try { + PrivilegedInclude dp = new PrivilegedInclude(request, response); + AccessController.doPrivileged(dp); + } catch (PrivilegedActionException pe) { + Exception e = pe.getException(); + + if (e instanceof ServletException) { + throw (ServletException) e; + } + throw (IOException) e; + } + } else { + doInclude(request, response); + } + } + + private void doInclude(ServletRequest request, ServletResponse response) throws ServletException, IOException { + + // Set up to handle the specified request and response + State state = new State(request, response, true); + + if (context.getDispatcherWrapsSameObject()) { + // Check SRV.8.2 / SRV.14.2.5.1 compliance + checkSameObjects(request, response); + } + + // Create a wrapped response to use for this request + wrapResponse(state); + + // Handle an HTTP named dispatcher include + if (name != null) { + + ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state); + wrequest.setAttribute(Globals.NAMED_DISPATCHER_ATTR, name); + if (servletPath != null) { + wrequest.setServletPath(servletPath); + } + wrequest.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.INCLUDE); + wrequest.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, getCombinedPath()); + invoke(state.outerRequest, state.outerResponse, state); + } + + // Handle an HTTP path based include + else { + + ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state); + String contextPath = context.getPath(); + if (requestURI != null) { + wrequest.setAttribute(INCLUDE_REQUEST_URI, requestURI); + } + if (contextPath != null) { + wrequest.setAttribute(INCLUDE_CONTEXT_PATH, contextPath); + } + if (servletPath != null) { + wrequest.setAttribute(INCLUDE_SERVLET_PATH, servletPath); + } + if (pathInfo != null) { + wrequest.setAttribute(INCLUDE_PATH_INFO, pathInfo); + } + if (queryString != null) { + wrequest.setAttribute(INCLUDE_QUERY_STRING, queryString); + wrequest.setQueryParams(queryString); + } + if (mapping != null) { + wrequest.setAttribute(INCLUDE_MAPPING, mapping); + } + + wrequest.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.INCLUDE); + wrequest.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, getCombinedPath()); + invoke(state.outerRequest, state.outerResponse, state); + } + + } + + + @Override + public void dispatch(ServletRequest request, ServletResponse response) throws ServletException, IOException { + if (Globals.IS_SECURITY_ENABLED) { + try { + PrivilegedDispatch dp = new PrivilegedDispatch(request, response); + AccessController.doPrivileged(dp); + } catch (PrivilegedActionException pe) { + Exception e = pe.getException(); + + if (e instanceof ServletException) { + throw (ServletException) e; + } + throw (IOException) e; + } + } else { + doDispatch(request, response); + } + } + + private void doDispatch(ServletRequest request, ServletResponse response) throws ServletException, IOException { + + // Set up to handle the specified request and response + State state = new State(request, response, false); + + // Create a wrapped response to use for this request + wrapResponse(state); + + ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state); + HttpServletRequest hrequest = state.hrequest; + + wrequest.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.ASYNC); + wrequest.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, getCombinedPath()); + wrequest.setAttribute(AsyncContext.ASYNC_MAPPING, hrequest.getHttpServletMapping()); + + wrequest.setContextPath(context.getEncodedPath()); + wrequest.setRequestURI(requestURI); + wrequest.setServletPath(servletPath); + wrequest.setPathInfo(pathInfo); + if (queryString != null) { + wrequest.setQueryString(queryString); + wrequest.setQueryParams(queryString); + } + wrequest.setMapping(mapping); + + invoke(state.outerRequest, state.outerResponse, state); + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Ask the resource represented by this RequestDispatcher to process the associated request, and create (or append + * to) the associated response. + *

    + * IMPLEMENTATION NOTE: This implementation assumes that no filters are applied to a forwarded or + * included resource, because they were already done for the original request. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + private void invoke(ServletRequest request, ServletResponse response, State state) + throws IOException, ServletException { + + // Checking to see if the context classloader is the current context + // classloader. If it's not, we're saving it, and setting the context + // classloader to the Context classloader + ClassLoader oldCCL = context.bind(false, null); + + // Initialize local variables we may need + HttpServletResponse hresponse = state.hresponse; + Servlet servlet = null; + IOException ioException = null; + ServletException servletException = null; + RuntimeException runtimeException = null; + boolean unavailable = false; + + // Check for the servlet being marked unavailable + if (wrapper.isUnavailable()) { + wrapper.getLogger().warn(sm.getString("applicationDispatcher.isUnavailable", wrapper.getName())); + long available = wrapper.getAvailable(); + if (available > 0L && available < Long.MAX_VALUE) { + hresponse.setDateHeader("Retry-After", available); + } + hresponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + sm.getString("applicationDispatcher.isUnavailable", wrapper.getName())); + unavailable = true; + } + + // Allocate a servlet instance to process this request + try { + if (!unavailable) { + servlet = wrapper.allocate(); + } + } catch (ServletException e) { + wrapper.getLogger().error(sm.getString("applicationDispatcher.allocateException", wrapper.getName()), + StandardWrapper.getRootCause(e)); + servletException = e; + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + wrapper.getLogger().error(sm.getString("applicationDispatcher.allocateException", wrapper.getName()), e); + servletException = + new ServletException(sm.getString("applicationDispatcher.allocateException", wrapper.getName()), e); + servlet = null; + } + + // Get the FilterChain Here + ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); + + // Call the service() method for the allocated servlet instance + try { + // for includes/forwards + if (servlet != null && filterChain != null) { + filterChain.doFilter(request, response); + } + // Servlet Service Method is called by the FilterChain + } catch (BadRequestException e) { + ioException = e; + } catch (IOException e) { + wrapper.getLogger().error(sm.getString("applicationDispatcher.serviceException", wrapper.getName()), e); + ioException = e; + } catch (UnavailableException e) { + wrapper.getLogger().error(sm.getString("applicationDispatcher.serviceException", wrapper.getName()), e); + servletException = e; + wrapper.unavailable(e); + } catch (ServletException e) { + Throwable rootCause = StandardWrapper.getRootCause(e); + if (!(rootCause instanceof BadRequestException)) { + wrapper.getLogger().error(sm.getString("applicationDispatcher.serviceException", wrapper.getName()), + rootCause); + } + servletException = e; + } catch (RuntimeException e) { + wrapper.getLogger().error(sm.getString("applicationDispatcher.serviceException", wrapper.getName()), e); + runtimeException = e; + } + + // Release the filter chain (if any) for this request + if (filterChain != null) { + filterChain.release(); + } + + // Deallocate the allocated servlet instance + try { + if (servlet != null) { + wrapper.deallocate(servlet); + } + } catch (ServletException e) { + wrapper.getLogger().error(sm.getString("applicationDispatcher.deallocateException", wrapper.getName()), e); + servletException = e; + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + wrapper.getLogger().error(sm.getString("applicationDispatcher.deallocateException", wrapper.getName()), e); + servletException = new ServletException( + sm.getString("applicationDispatcher.deallocateException", wrapper.getName()), e); + } + + // Reset the old context class loader + context.unbind(false, oldCCL); + + // Unwrap request/response if needed + // See Bugzilla 30949 + unwrapRequest(state); + unwrapResponse(state); + // Recycle request if necessary (also BZ 30949) + recycleRequestWrapper(state); + + // Rethrow an exception if one was thrown by the invoked servlet + if (ioException != null) { + throw ioException; + } + if (servletException != null) { + throw servletException; + } + if (runtimeException != null) { + throw runtimeException; + } + } + + + /** + * Unwrap the request if we have wrapped it. + */ + private void unwrapRequest(State state) { + + if (state.wrapRequest == null) { + return; + } + + if (state.outerRequest.isAsyncStarted()) { + if (!state.outerRequest.getAsyncContext().hasOriginalRequestAndResponse()) { + return; + } + } + + ServletRequest previous = null; + ServletRequest current = state.outerRequest; + while (current != null) { + + // If we run into the container request we are done + if (current instanceof Request || current instanceof RequestFacade) { + break; + } + + // Remove the current request if it is our wrapper + if (current == state.wrapRequest) { + ServletRequest next = ((ServletRequestWrapper) current).getRequest(); + if (previous == null) { + state.outerRequest = next; + } else { + ((ServletRequestWrapper) previous).setRequest(next); + } + break; + } + + // Advance to the next request in the chain + previous = current; + current = ((ServletRequestWrapper) current).getRequest(); + + } + + } + + /** + * Unwrap the response if we have wrapped it. + */ + private void unwrapResponse(State state) { + + if (state.wrapResponse == null) { + return; + } + + if (state.outerRequest.isAsyncStarted()) { + if (!state.outerRequest.getAsyncContext().hasOriginalRequestAndResponse()) { + return; + } + } + + ServletResponse previous = null; + ServletResponse current = state.outerResponse; + while (current != null) { + + // If we run into the container response we are done + if (current instanceof Response || current instanceof ResponseFacade) { + break; + } + + // Remove the current response if it is our wrapper + if (current == state.wrapResponse) { + ServletResponse next = ((ServletResponseWrapper) current).getResponse(); + if (previous == null) { + state.outerResponse = next; + } else { + ((ServletResponseWrapper) previous).setResponse(next); + } + break; + } + + // Advance to the next response in the chain + previous = current; + current = ((ServletResponseWrapper) current).getResponse(); + + } + + } + + + /** + * Create and return a request wrapper that has been inserted in the appropriate spot in the request chain. + */ + private ServletRequest wrapRequest(State state) { + + // Locate the request we should insert in front of + ServletRequest previous = null; + ServletRequest current = state.outerRequest; + while (current != null) { + if (state.hrequest == null && current instanceof HttpServletRequest) { + state.hrequest = (HttpServletRequest) current; + } + if (!(current instanceof ServletRequestWrapper)) { + break; + } + if (current instanceof ApplicationHttpRequest) { + break; + } + if (current instanceof ApplicationRequest) { + break; + } + previous = current; + current = ((ServletRequestWrapper) current).getRequest(); + } + + // Instantiate a new wrapper at this point and insert it in the chain + ServletRequest wrapper = null; + if (current instanceof ApplicationHttpRequest || current instanceof Request || + current instanceof HttpServletRequest) { + // Compute a crossContext flag + HttpServletRequest hcurrent = (HttpServletRequest) current; + boolean crossContext = false; + if (state.outerRequest instanceof ApplicationHttpRequest || state.outerRequest instanceof Request || + state.outerRequest instanceof HttpServletRequest) { + HttpServletRequest houterRequest = (HttpServletRequest) state.outerRequest; + Object contextPath = houterRequest.getAttribute(INCLUDE_CONTEXT_PATH); + if (contextPath == null) { + // Forward + contextPath = houterRequest.getContextPath(); + } + crossContext = !context.getPath().equals(contextPath); + } + wrapper = new ApplicationHttpRequest(hcurrent, context, crossContext); + } else { + wrapper = new ApplicationRequest(current); + } + if (previous == null) { + state.outerRequest = wrapper; + } else { + ((ServletRequestWrapper) previous).setRequest(wrapper); + } + state.wrapRequest = wrapper; + return wrapper; + + } + + + /** + * Create and return a response wrapper that has been inserted in the appropriate spot in the response chain. + */ + private ServletResponse wrapResponse(State state) { + + // Locate the response we should insert in front of + ServletResponse previous = null; + ServletResponse current = state.outerResponse; + while (current != null) { + if (state.hresponse == null && current instanceof HttpServletResponse) { + state.hresponse = (HttpServletResponse) current; + if (!state.including) { // Forward only needs hresponse + return null; + } + } + if (!(current instanceof ServletResponseWrapper)) { + break; + } + if (current instanceof ApplicationHttpResponse) { + break; + } + if (current instanceof ApplicationResponse) { + break; + } + previous = current; + current = ((ServletResponseWrapper) current).getResponse(); + } + + // Instantiate a new wrapper at this point and insert it in the chain + ServletResponse wrapper = null; + if (current instanceof ApplicationHttpResponse || current instanceof Response || + current instanceof HttpServletResponse) { + wrapper = new ApplicationHttpResponse((HttpServletResponse) current, state.including); + } else { + wrapper = new ApplicationResponse(current, state.including); + } + if (previous == null) { + state.outerResponse = wrapper; + } else { + ((ServletResponseWrapper) previous).setResponse(wrapper); + } + state.wrapResponse = wrapper; + return wrapper; + + } + + private void checkSameObjects(ServletRequest appRequest, ServletResponse appResponse) throws ServletException { + ServletRequest originalRequest = ApplicationFilterChain.getLastServicedRequest(); + ServletResponse originalResponse = ApplicationFilterChain.getLastServicedResponse(); + + // Some forwards, eg from valves will not set original values + if (originalRequest == null || originalResponse == null) { + return; + } + + boolean same = false; + ServletRequest dispatchedRequest = appRequest; + + // find the request that was passed into the service method + while (originalRequest instanceof ServletRequestWrapper && + ((ServletRequestWrapper) originalRequest).getRequest() != null) { + originalRequest = ((ServletRequestWrapper) originalRequest).getRequest(); + } + // compare with the dispatched request + while (!same) { + if (originalRequest.equals(dispatchedRequest)) { + same = true; + } + if (!same && dispatchedRequest instanceof ServletRequestWrapper) { + dispatchedRequest = ((ServletRequestWrapper) dispatchedRequest).getRequest(); + } else { + break; + } + } + if (!same) { + throw new ServletException(sm.getString("applicationDispatcher.specViolation.request")); + } + + same = false; + ServletResponse dispatchedResponse = appResponse; + + // find the response that was passed into the service method + while (originalResponse instanceof ServletResponseWrapper && + ((ServletResponseWrapper) originalResponse).getResponse() != null) { + originalResponse = ((ServletResponseWrapper) originalResponse).getResponse(); + } + // compare with the dispatched response + while (!same) { + if (originalResponse.equals(dispatchedResponse)) { + same = true; + } + + if (!same && dispatchedResponse instanceof ServletResponseWrapper) { + dispatchedResponse = ((ServletResponseWrapper) dispatchedResponse).getResponse(); + } else { + break; + } + } + + if (!same) { + throw new ServletException(sm.getString("applicationDispatcher.specViolation.response")); + } + } + + private void recycleRequestWrapper(State state) { + if (state.wrapRequest instanceof ApplicationHttpRequest) { + ((ApplicationHttpRequest) state.wrapRequest).recycle(); + } + } +} diff --git a/java/org/apache/catalina/core/ApplicationFilterChain.java b/java/org/apache/catalina/core/ApplicationFilterChain.java new file mode 100644 index 0000000..2dc3147 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationFilterChain.java @@ -0,0 +1,314 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.util.Arrays; +import java.util.Set; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Globals; +import org.apache.catalina.security.SecurityUtil; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implementation of jakarta.servlet.FilterChain used to manage the execution of a set of filters for a + * particular request. When the set of defined filters has all been executed, the next call to doFilter() + * will execute the servlet's service() method itself. + * + * @author Craig R. McClanahan + */ +public final class ApplicationFilterChain implements FilterChain { + + // Used to enforce requirements of SRV.8.2 / SRV.14.2.5.1 + private static final ThreadLocal lastServicedRequest = new ThreadLocal<>(); + private static final ThreadLocal lastServicedResponse = new ThreadLocal<>(); + + + // -------------------------------------------------------------- Constants + + + public static final int INCREMENT = 10; + + + // ----------------------------------------------------- Instance Variables + + /** + * Filters. + */ + private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; + + + /** + * The int which is used to maintain the current position in the filter chain. + */ + private int pos = 0; + + + /** + * The int which gives the current number of filters in the chain. + */ + private int n = 0; + + + /** + * The servlet instance to be executed by this chain. + */ + private Servlet servlet = null; + + + /** + * Does the associated servlet instance support async processing? + */ + private boolean servletSupportsAsync = false; + + /** + * Check the proper Servlet objects have been used. + */ + private boolean dispatcherWrapsSameObject = false; + + /** + * The string manager for our package. + */ + private static final StringManager sm = StringManager.getManager(ApplicationFilterChain.class); + + + /** + * Static class array used when the SecurityManager is turned on and doFilter is invoked. + */ + private static final Class[] classType = + new Class[] { ServletRequest.class, ServletResponse.class, FilterChain.class }; + + /** + * Static class array used when the SecurityManager is turned on and service is invoked. + */ + private static final Class[] classTypeUsedInService = + new Class[] { ServletRequest.class, ServletResponse.class }; + + + // ---------------------------------------------------- FilterChain Methods + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + + if (Globals.IS_SECURITY_ENABLED) { + final ServletRequest req = request; + final ServletResponse res = response; + try { + java.security.AccessController.doPrivileged((java.security.PrivilegedExceptionAction) () -> { + internalDoFilter(req, res); + return null; + }); + } catch (PrivilegedActionException pe) { + Exception e = pe.getException(); + if (e instanceof ServletException) { + throw (ServletException) e; + } else if (e instanceof IOException) { + throw (IOException) e; + } else if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new ServletException(e.getMessage(), e); + } + } + } else { + internalDoFilter(request, response); + } + } + + private void internalDoFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + + // Call the next filter if there is one + if (pos < n) { + ApplicationFilterConfig filterConfig = filters[pos++]; + try { + Filter filter = filterConfig.getFilter(); + + if (request.isAsyncSupported() && + !(filterConfig.getFilterDef().getAsyncSupportedBoolean())) { + request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); + } + if (Globals.IS_SECURITY_ENABLED) { + final ServletRequest req = request; + final ServletResponse res = response; + Principal principal = ((HttpServletRequest) req).getUserPrincipal(); + + Object[] args = new Object[] { req, res, this }; + SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal); + } else { + filter.doFilter(request, response, this); + } + } catch (IOException | ServletException | RuntimeException e) { + throw e; + } catch (Throwable e) { + e = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(e); + throw new ServletException(sm.getString("filterChain.filter"), e); + } + return; + } + + // We fell off the end of the chain -- call the servlet instance + try { + if (dispatcherWrapsSameObject) { + lastServicedRequest.set(request); + lastServicedResponse.set(response); + } + + if (request.isAsyncSupported() && !servletSupportsAsync) { + request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); + } + // Use potentially wrapped request from this point + if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && + Globals.IS_SECURITY_ENABLED) { + final ServletRequest req = request; + final ServletResponse res = response; + Principal principal = ((HttpServletRequest) req).getUserPrincipal(); + Object[] args = new Object[] { req, res }; + SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal); + } else { + servlet.service(request, response); + } + } catch (IOException | ServletException | RuntimeException e) { + throw e; + } catch (Throwable e) { + e = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(e); + throw new ServletException(sm.getString("filterChain.servlet"), e); + } finally { + if (dispatcherWrapsSameObject) { + lastServicedRequest.set(null); + lastServicedResponse.set(null); + } + } + } + + + /** + * The last request passed to a servlet for servicing from the current thread. + * + * @return The last request to be serviced. + */ + public static ServletRequest getLastServicedRequest() { + return lastServicedRequest.get(); + } + + + /** + * The last response passed to a servlet for servicing from the current thread. + * + * @return The last response to be serviced. + */ + public static ServletResponse getLastServicedResponse() { + return lastServicedResponse.get(); + } + + + // -------------------------------------------------------- Package Methods + + /** + * Add a filter to the set of filters that will be executed in this chain. + * + * @param filterConfig The FilterConfig for the servlet to be executed + */ + void addFilter(ApplicationFilterConfig filterConfig) { + + // Prevent the same filter being added multiple times + for (int i = 0; i < n; i++) { + if (filters[i] == filterConfig) { + return; + } + } + + if (n == filters.length) { + filters = Arrays.copyOf(filters, n + INCREMENT); + } + filters[n++] = filterConfig; + + } + + + /** + * Release references to the filters and wrapper executed by this chain. + */ + void release() { + for (int i = 0; i < n; i++) { + filters[i] = null; + } + n = 0; + pos = 0; + servlet = null; + servletSupportsAsync = false; + dispatcherWrapsSameObject = false; + } + + + /** + * Prepare for reuse of the filters and wrapper executed by this chain. + */ + void reuse() { + pos = 0; + } + + + /** + * Set the servlet that will be executed at the end of this chain. + * + * @param servlet The Wrapper for the servlet to be executed + */ + void setServlet(Servlet servlet) { + this.servlet = servlet; + } + + + void setServletSupportsAsync(boolean servletSupportsAsync) { + this.servletSupportsAsync = servletSupportsAsync; + } + + + void setDispatcherWrapsSameObject(boolean dispatcherWrapsSameObject) { + this.dispatcherWrapsSameObject = dispatcherWrapsSameObject; + } + + + /** + * Identifies the Filters, if any, in this FilterChain that do not support async. + * + * @param result The Set to which the fully qualified class names of each Filter in this FilterChain that does not + * support async will be added + */ + public void findNonAsyncFilters(Set result) { + for (int i = 0; i < n; i++) { + ApplicationFilterConfig filter = filters[i]; + if (!(filter.getFilterDef().getAsyncSupportedBoolean())) { + result.add(filter.getFilterClass()); + } + } + } +} diff --git a/java/org/apache/catalina/core/ApplicationFilterConfig.java b/java/org/apache/catalina/core/ApplicationFilterConfig.java new file mode 100644 index 0000000..41cce09 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationFilterConfig.java @@ -0,0 +1,358 @@ +/* + * 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.catalina.core; + + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import javax.management.ObjectName; +import javax.naming.NamingException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.security.SecurityUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.log.SystemLogHandler; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.modeler.Util; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Implementation of a jakarta.servlet.FilterConfig useful in managing the filter instances instantiated + * when a web application is first started. + * + * @author Craig R. McClanahan + */ +public final class ApplicationFilterConfig implements FilterConfig, Serializable { + + private static final long serialVersionUID = 1L; + + static final StringManager sm = StringManager.getManager(ApplicationFilterConfig.class); + + private transient Log log = LogFactory.getLog(ApplicationFilterConfig.class); // must not be static + + /** + * Empty String collection to serve as the basis for empty enumerations. + */ + private static final List emptyString = Collections.emptyList(); + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new ApplicationFilterConfig for the specified filter definition. + * + * @param context The context with which we are associated + * @param filterDef Filter definition for which a FilterConfig is to be constructed + * + * @exception ClassCastException if the specified class does not implement the + * jakarta.servlet.Filter interface + * @exception ClassNotFoundException if the filter class cannot be found + * @exception IllegalAccessException if the filter class cannot be publicly instantiated + * @exception InstantiationException if an exception occurs while instantiating the filter object + * @exception ServletException if thrown by the filter's init() method + * + * @throws NamingException If a JNDI lookup fails + * @throws SecurityException If a security manager prevents the creation + * @throws IllegalArgumentException If the provided configuration is not valid + */ + ApplicationFilterConfig(Context context, FilterDef filterDef) + throws ClassCastException, ReflectiveOperationException, ServletException, NamingException, + IllegalArgumentException, SecurityException { + + super(); + + this.context = context; + this.filterDef = filterDef; + // Allocate a new filter instance if necessary + if (filterDef.getFilter() == null) { + getFilter(); + } else { + this.filter = filterDef.getFilter(); + context.getInstanceManager().newInstance(filter); + initFilter(); + } + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The Context with which we are associated. + */ + private final transient Context context; + + + /** + * The application Filter we are configured for. + */ + private transient Filter filter = null; + + + /** + * The FilterDef that defines our associated Filter. + */ + private final FilterDef filterDef; + + /** + * JMX registration name + */ + private ObjectName oname; + + // --------------------------------------------------- FilterConfig Methods + + + @Override + public String getFilterName() { + return filterDef.getFilterName(); + } + + /** + * @return The class of the filter we are configuring. + */ + public String getFilterClass() { + return filterDef.getFilterClass(); + } + + @Override + public String getInitParameter(String name) { + + Map map = filterDef.getParameterMap(); + if (map == null) { + return null; + } + + return map.get(name); + + } + + + @Override + public Enumeration getInitParameterNames() { + Map map = filterDef.getParameterMap(); + + if (map == null) { + return Collections.enumeration(emptyString); + } + + return Collections.enumeration(map.keySet()); + } + + + @Override + public ServletContext getServletContext() { + + return this.context.getServletContext(); + + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ApplicationFilterConfig["); + sb.append("name="); + sb.append(filterDef.getFilterName()); + sb.append(", filterClass="); + sb.append(filterDef.getFilterClass()); + sb.append(']'); + return sb.toString(); + } + + // --------------------------------------------------------- Public Methods + + public Map getFilterInitParameterMap() { + return Collections.unmodifiableMap(filterDef.getParameterMap()); + } + + // -------------------------------------------------------- Package Methods + + + /** + * Return the application Filter we are configured for. + * + * @exception ClassCastException if the specified class does not implement the + * jakarta.servlet.Filter interface + * @exception ClassNotFoundException if the filter class cannot be found + * @exception IllegalAccessException if the filter class cannot be publicly instantiated + * @exception InstantiationException if an exception occurs while instantiating the filter object + * @exception ServletException if thrown by the filter's init() method + * + * @throws NamingException If a JNDI lookup fails + * @throws ReflectiveOperationException If the creation of the filter fails + * @throws SecurityException If a security manager prevents the creation + * @throws IllegalArgumentException If the provided configuration is not valid + */ + Filter getFilter() throws ClassCastException, ReflectiveOperationException, ServletException, NamingException, + IllegalArgumentException, SecurityException { + + // Return the existing filter instance, if any + if (this.filter != null) { + return this.filter; + } + + // Identify the class loader we will be using + String filterClass = filterDef.getFilterClass(); + this.filter = (Filter) context.getInstanceManager().newInstance(filterClass); + + initFilter(); + + return this.filter; + + } + + private void initFilter() throws ServletException { + if (context instanceof StandardContext && context.getSwallowOutput()) { + try { + SystemLogHandler.startCapture(); + filter.init(this); + } finally { + String capturedlog = SystemLogHandler.stopCapture(); + if (capturedlog != null && capturedlog.length() > 0) { + getServletContext().log(capturedlog); + } + } + } else { + filter.init(this); + } + + // Expose filter via JMX + registerJMX(); + } + + /** + * Return the filter definition we are configured for. + */ + FilterDef getFilterDef() { + return this.filterDef; + } + + /** + * Release the Filter instance associated with this FilterConfig, if there is one. + */ + void release() { + + unregisterJMX(); + + if (this.filter != null) { + try { + if (Globals.IS_SECURITY_ENABLED) { + try { + SecurityUtil.doAsPrivilege("destroy", filter); + } finally { + SecurityUtil.remove(filter); + } + } else { + filter.destroy(); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + context.getLogger().error(sm.getString("applicationFilterConfig.release", filterDef.getFilterName(), + filterDef.getFilterClass()), t); + } + if (!context.getIgnoreAnnotations()) { + try { + context.getInstanceManager().destroyInstance(this.filter); + } catch (Exception e) { + Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + context.getLogger().error(sm.getString("applicationFilterConfig.preDestroy", + filterDef.getFilterName(), filterDef.getFilterClass()), t); + } + } + } + this.filter = null; + + } + + + // -------------------------------------------------------- Private Methods + + private void registerJMX() { + String parentName = context.getName(); + if (!parentName.startsWith("/")) { + parentName = "/" + parentName; + } + + String hostName = context.getParent().getName(); + hostName = (hostName == null) ? "DEFAULT" : hostName; + + // domain == engine name + String domain = context.getParent().getParent().getName(); + + String webMod = "//" + hostName + parentName; + String onameStr = null; + String filterName = filterDef.getFilterName(); + if (Util.objectNameValueNeedsQuote(filterName)) { + filterName = ObjectName.quote(filterName); + } + if (context instanceof StandardContext) { + StandardContext standardContext = (StandardContext) context; + onameStr = domain + ":j2eeType=Filter,WebModule=" + webMod + ",name=" + filterName + ",J2EEApplication=" + + standardContext.getJ2EEApplication() + ",J2EEServer=" + standardContext.getJ2EEServer(); + } else { + onameStr = domain + ":j2eeType=Filter,name=" + filterName + ",WebModule=" + webMod; + } + try { + oname = new ObjectName(onameStr); + Registry.getRegistry(null, null).registerComponent(this, oname, null); + } catch (Exception ex) { + log.warn(sm.getString("applicationFilterConfig.jmxRegisterFail", getFilterClass(), getFilterName()), ex); + } + } + + + private void unregisterJMX() { + // unregister this component + if (oname != null) { + try { + Registry.getRegistry(null, null).unregisterComponent(oname); + if (log.isDebugEnabled()) { + log.debug(sm.getString("applicationFilterConfig.jmxUnregister", getFilterClass(), getFilterName())); + } + } catch (Exception ex) { + log.warn(sm.getString("applicationFilterConfig.jmxUnregisterFail", getFilterClass(), getFilterName()), + ex); + } + } + } + + + /* + * Log objects are not Serializable. + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + log = LogFactory.getLog(ApplicationFilterConfig.class); + } + +} diff --git a/java/org/apache/catalina/core/ApplicationFilterFactory.java b/java/org/apache/catalina/core/ApplicationFilterFactory.java new file mode 100644 index 0000000..d71220d --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationFilterFactory.java @@ -0,0 +1,293 @@ +/* + * 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.catalina.core; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletRequest; + +import org.apache.catalina.Globals; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.res.StringManager; + +/** + * Factory for the creation and caching of Filters and creation of Filter Chains. + * + * @author Greg Murray + * @author Remy Maucherat + */ +public final class ApplicationFilterFactory { + + private static final Log log = LogFactory.getLog(ApplicationFilterFactory.class); + private static final StringManager sm = StringManager.getManager(ApplicationFilterFactory.class); + + private ApplicationFilterFactory() { + // Prevent instance creation. This is a utility class. + } + + + /** + * Construct a FilterChain implementation that will wrap the execution of the specified servlet instance. + * + * @param request The servlet request we are processing + * @param wrapper The wrapper managing the servlet instance + * @param servlet The servlet instance to be wrapped + * + * @return The configured FilterChain instance or null if none is to be executed. + */ + public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) { + + // If there is no servlet to execute, return null + if (servlet == null) { + return null; + } + + // Create and initialize a filter chain object + ApplicationFilterChain filterChain = null; + if (request instanceof Request) { + Request req = (Request) request; + if (Globals.IS_SECURITY_ENABLED) { + // Security: Do not recycle + filterChain = new ApplicationFilterChain(); + } else { + filterChain = (ApplicationFilterChain) req.getFilterChain(); + if (filterChain == null) { + filterChain = new ApplicationFilterChain(); + req.setFilterChain(filterChain); + } + } + } else { + // Request dispatcher in use + filterChain = new ApplicationFilterChain(); + } + + filterChain.setServlet(servlet); + filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); + + // Acquire the filter mappings for this Context + StandardContext context = (StandardContext) wrapper.getParent(); + filterChain.setDispatcherWrapsSameObject(context.getDispatcherWrapsSameObject()); + FilterMap filterMaps[] = context.findFilterMaps(); + + // If there are no filter mappings, we are done + if (filterMaps == null || filterMaps.length == 0) { + return filterChain; + } + + // Acquire the information we will need to match filter mappings + DispatcherType dispatcher = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR); + + String requestPath = null; + Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR); + if (attribute != null) { + requestPath = attribute.toString(); + } + + String servletName = wrapper.getName(); + + // Add the relevant path-mapped filters to this filter chain + for (FilterMap filterMap : filterMaps) { + if (!matchDispatcher(filterMap, dispatcher)) { + continue; + } + if (!matchFiltersURL(filterMap, requestPath)) { + continue; + } + ApplicationFilterConfig filterConfig = + (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); + if (filterConfig == null) { + log.warn(sm.getString("applicationFilterFactory.noFilterConfig", filterMap.getFilterName())); + continue; + } + filterChain.addFilter(filterConfig); + } + + // Add filters that match on servlet name second + for (FilterMap filterMap : filterMaps) { + if (!matchDispatcher(filterMap, dispatcher)) { + continue; + } + if (!matchFiltersServlet(filterMap, servletName)) { + continue; + } + ApplicationFilterConfig filterConfig = + (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); + if (filterConfig == null) { + log.warn(sm.getString("applicationFilterFactory.noFilterConfig", filterMap.getFilterName())); + continue; + } + filterChain.addFilter(filterConfig); + } + + // Return the completed filter chain + return filterChain; + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Return true if the context-relative request path matches the requirements of the specified filter + * mapping; otherwise, return false. + * + * @param filterMap Filter mapping being checked + * @param requestPath Context-relative request path of this request + */ + private static boolean matchFiltersURL(FilterMap filterMap, String requestPath) { + + // Check the specific "*" special URL pattern, which also matches + // named dispatches + if (filterMap.getMatchAllUrlPatterns()) { + return true; + } + + if (requestPath == null) { + return false; + } + + // Match on context relative request path + String[] testPaths = filterMap.getURLPatterns(); + + for (String testPath : testPaths) { + if (matchFiltersURL(testPath, requestPath)) { + return true; + } + } + + // No match + return false; + + } + + + /** + * Return true if the context-relative request path matches the requirements of the specified filter + * mapping; otherwise, return false. + * + * @param testPath URL mapping being checked + * @param requestPath Context-relative request path of this request + */ + private static boolean matchFiltersURL(String testPath, String requestPath) { + + if (testPath == null) { + return false; + } + + // Case 1 - Exact Match + if (testPath.equals(requestPath)) { + return true; + } + + // Case 2 - Path Match ("/.../*") + if (testPath.equals("/*")) { + return true; + } + if (testPath.endsWith("/*")) { + if (testPath.regionMatches(0, requestPath, 0, testPath.length() - 2)) { + if (requestPath.length() == (testPath.length() - 2)) { + return true; + } else if ('/' == requestPath.charAt(testPath.length() - 2)) { + return true; + } + } + return false; + } + + // Case 3 - Extension Match + if (testPath.startsWith("*.")) { + int slash = requestPath.lastIndexOf('/'); + int period = requestPath.lastIndexOf('.'); + if ((slash >= 0) && (period > slash) && (period != requestPath.length() - 1) && + ((requestPath.length() - period) == (testPath.length() - 1))) { + return testPath.regionMatches(2, requestPath, period + 1, testPath.length() - 2); + } + } + + // Case 4 - "Default" Match + return false; // NOTE - Not relevant for selecting filters + + } + + + /** + * Return true if the specified servlet name matches the requirements of the specified filter mapping; + * otherwise return false. + * + * @param filterMap Filter mapping being checked + * @param servletName Servlet name being checked + */ + private static boolean matchFiltersServlet(FilterMap filterMap, String servletName) { + + if (servletName == null) { + return false; + } + // Check the specific "*" special servlet name + else if (filterMap.getMatchAllServletNames()) { + return true; + } else { + String[] servletNames = filterMap.getServletNames(); + for (String name : servletNames) { + if (servletName.equals(name)) { + return true; + } + } + return false; + } + + } + + + /** + * Convenience method which returns true if the dispatcher type matches the dispatcher types specified in the + * FilterMap + */ + private static boolean matchDispatcher(FilterMap filterMap, DispatcherType type) { + switch (type) { + case FORWARD: + if ((filterMap.getDispatcherMapping() & FilterMap.FORWARD) != 0) { + return true; + } + break; + case INCLUDE: + if ((filterMap.getDispatcherMapping() & FilterMap.INCLUDE) != 0) { + return true; + } + break; + case REQUEST: + if ((filterMap.getDispatcherMapping() & FilterMap.REQUEST) != 0) { + return true; + } + break; + case ERROR: + if ((filterMap.getDispatcherMapping() & FilterMap.ERROR) != 0) { + return true; + } + break; + case ASYNC: + if ((filterMap.getDispatcherMapping() & FilterMap.ASYNC) != 0) { + return true; + } + break; + } + return false; + } +} diff --git a/java/org/apache/catalina/core/ApplicationFilterRegistration.java b/java/org/apache/catalina/core/ApplicationFilterRegistration.java new file mode 100644 index 0000000..c14ce03 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationFilterRegistration.java @@ -0,0 +1,204 @@ +/* + * 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.catalina.core; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; + +import org.apache.catalina.Context; +import org.apache.catalina.util.ParameterMap; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.res.StringManager; + +public class ApplicationFilterRegistration implements FilterRegistration.Dynamic { + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(ApplicationFilterRegistration.class); + + private final FilterDef filterDef; + private final Context context; + + public ApplicationFilterRegistration(FilterDef filterDef, Context context) { + this.filterDef = filterDef; + this.context = context; + + } + + @Override + public void addMappingForServletNames(EnumSet dispatcherTypes, boolean isMatchAfter, + String... servletNames) { + + FilterMap filterMap = new FilterMap(); + + filterMap.setFilterName(filterDef.getFilterName()); + + if (dispatcherTypes != null) { + for (DispatcherType dispatcherType : dispatcherTypes) { + filterMap.setDispatcher(dispatcherType.name()); + } + } + + if (servletNames != null) { + for (String servletName : servletNames) { + filterMap.addServletName(servletName); + } + + if (isMatchAfter) { + context.addFilterMap(filterMap); + } else { + context.addFilterMapBefore(filterMap); + } + } + // else error? + } + + @Override + public void addMappingForUrlPatterns(EnumSet dispatcherTypes, boolean isMatchAfter, + String... urlPatterns) { + + FilterMap filterMap = new FilterMap(); + + filterMap.setFilterName(filterDef.getFilterName()); + + if (dispatcherTypes != null) { + for (DispatcherType dispatcherType : dispatcherTypes) { + filterMap.setDispatcher(dispatcherType.name()); + } + } + + if (urlPatterns != null) { + // % decoded (if necessary) using UTF-8 + for (String urlPattern : urlPatterns) { + filterMap.addURLPattern(urlPattern); + } + + if (isMatchAfter) { + context.addFilterMap(filterMap); + } else { + context.addFilterMapBefore(filterMap); + } + } + // else error? + + } + + @Override + public Collection getServletNameMappings() { + Collection result = new HashSet<>(); + + FilterMap[] filterMaps = context.findFilterMaps(); + + for (FilterMap filterMap : filterMaps) { + if (filterMap.getFilterName().equals(filterDef.getFilterName())) { + result.addAll(Arrays.asList(filterMap.getServletNames())); + } + } + return result; + } + + @Override + public Collection getUrlPatternMappings() { + Collection result = new HashSet<>(); + + FilterMap[] filterMaps = context.findFilterMaps(); + + for (FilterMap filterMap : filterMaps) { + if (filterMap.getFilterName().equals(filterDef.getFilterName())) { + result.addAll(Arrays.asList(filterMap.getURLPatterns())); + } + } + return result; + } + + @Override + public String getClassName() { + return filterDef.getFilterClass(); + } + + @Override + public String getInitParameter(String name) { + return filterDef.getParameterMap().get(name); + } + + @Override + public Map getInitParameters() { + ParameterMap result = new ParameterMap<>(); + result.putAll(filterDef.getParameterMap()); + result.setLocked(true); + return result; + } + + @Override + public String getName() { + return filterDef.getFilterName(); + } + + @Override + public boolean setInitParameter(String name, String value) { + if (name == null || value == null) { + throw new IllegalArgumentException( + sm.getString("applicationFilterRegistration.nullInitParam", name, value)); + } + if (getInitParameter(name) != null) { + return false; + } + + filterDef.addInitParameter(name, value); + + return true; + } + + @Override + public Set setInitParameters(Map initParameters) { + + Set conflicts = new HashSet<>(); + + for (Map.Entry entry : initParameters.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + throw new IllegalArgumentException( + sm.getString("applicationFilterRegistration.nullInitParams", entry.getKey(), entry.getValue())); + } + if (getInitParameter(entry.getKey()) != null) { + conflicts.add(entry.getKey()); + } + } + + // Have to add in a separate loop since spec requires no updates at all + // if there is an issue + for (Map.Entry entry : initParameters.entrySet()) { + setInitParameter(entry.getKey(), entry.getValue()); + } + + return conflicts; + } + + @Override + public void setAsyncSupported(boolean asyncSupported) { + filterDef.setAsyncSupported(Boolean.valueOf(asyncSupported).toString()); + } + +} diff --git a/java/org/apache/catalina/core/ApplicationHttpRequest.java b/java/org/apache/catalina/core/ApplicationHttpRequest.java new file mode 100644 index 0000000..12534ba --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationHttpRequest.java @@ -0,0 +1,939 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestWrapper; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.PushBuilder; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.connector.RequestFacade; +import org.apache.catalina.util.ParameterMap; +import org.apache.catalina.util.RequestUtil; +import org.apache.catalina.util.URLEncoder; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.Parameters; +import org.apache.tomcat.util.res.StringManager; + +/** + * Wrapper around a jakarta.servlet.http.HttpServletRequest that transforms an application request object + * (which might be the original one passed to a servlet, or might be based on the 2.3 + * jakarta.servlet.http.HttpServletRequestWrapper class) back into an internal + * org.apache.catalina.HttpRequest. + *

    + * WARNING: Due to Java's lack of support for multiple inheritance, all of the logic in + * ApplicationRequest is duplicated in ApplicationHttpRequest. Make sure that you keep these + * two classes in synchronization when making changes! + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +class ApplicationHttpRequest extends HttpServletRequestWrapper { + + private static final StringManager sm = StringManager.getManager(ApplicationHttpRequest.class); + + /** + * The set of attribute names that are special for request dispatchers. + */ + protected static final String specials[] = + { RequestDispatcher.INCLUDE_REQUEST_URI, RequestDispatcher.INCLUDE_CONTEXT_PATH, + RequestDispatcher.INCLUDE_SERVLET_PATH, RequestDispatcher.INCLUDE_PATH_INFO, + RequestDispatcher.INCLUDE_QUERY_STRING, RequestDispatcher.INCLUDE_MAPPING, + RequestDispatcher.FORWARD_REQUEST_URI, RequestDispatcher.FORWARD_CONTEXT_PATH, + RequestDispatcher.FORWARD_SERVLET_PATH, RequestDispatcher.FORWARD_PATH_INFO, + RequestDispatcher.FORWARD_QUERY_STRING, RequestDispatcher.FORWARD_MAPPING }; + /* + * This duplicates specials to some extent but has been added to improve the performance of [get|set|is]Special(). + * It may be possible to remove specials but that will require changes to AttributeNamesEnumerator. + */ + private static final Map specialsMap = new HashMap<>(); + static { + for (int i = 0; i < specials.length; i++) { + specialsMap.put(specials[i], Integer.valueOf(i)); + } + } + + private static final int shortestSpecialNameLength = + specialsMap.keySet().stream().mapToInt(s -> s.length()).min().getAsInt(); + + + private static final int SPECIALS_FIRST_FORWARD_INDEX = 6; + + + /** + * The context for this request. + */ + protected final Context context; + + + /** + * The context path for this request. + */ + protected String contextPath = null; + + + /** + * If this request is cross context, since this changes session access behavior. + */ + protected final boolean crossContext; + + + /** + * The current dispatcher type. + */ + protected DispatcherType dispatcherType = null; + + + /** + * The request parameters for this request. This is initialized from the wrapped request. + */ + protected Map parameters = null; + + + /** + * Have the parameters for this request already been parsed? + */ + private boolean parsedParams = false; + + + /** + * The path information for this request. + */ + protected String pathInfo = null; + + + /** + * The query parameters for the current request. + */ + private String queryParamString = null; + + + /** + * The query string for this request. + */ + protected String queryString = null; + + + /** + * The current request dispatcher path. + */ + protected Object requestDispatcherPath = null; + + + /** + * The request URI for this request. + */ + protected String requestURI = null; + + + /** + * The servlet path for this request. + */ + protected String servletPath = null; + + + /** + * The mapping for this request. + */ + private HttpServletMapping mapping = null; + + + /** + * The currently active session for this request. + */ + protected Session session = null; + + + /** + * Special attributes. + */ + protected final Object[] specialAttributes = new Object[specials.length]; + + + /** + * Construct a new wrapped request around the specified servlet request. + * + * @param request The servlet request being wrapped + * @param context The target context for the wrapped request + * @param crossContext {@code true} if the wrapped request will be a cross-context request, otherwise {@code false} + */ + ApplicationHttpRequest(HttpServletRequest request, Context context, boolean crossContext) { + super(request); + this.context = context; + this.crossContext = crossContext; + setRequest(request); + } + + + // ------------------------------------------------- ServletRequest Methods + + @Override + public ServletContext getServletContext() { + if (context == null) { + return null; + } + return context.getServletContext(); + } + + + /** + * Override the getAttribute() method of the wrapped request. + * + * @param name Name of the attribute to retrieve + */ + @Override + public Object getAttribute(String name) { + + if (name.equals(Globals.DISPATCHER_TYPE_ATTR)) { + return dispatcherType; + } else if (name.equals(Globals.DISPATCHER_REQUEST_PATH_ATTR)) { + if (requestDispatcherPath != null) { + return requestDispatcherPath.toString(); + } else { + return null; + } + } + + int pos = getSpecial(name); + if (pos == -1) { + return getRequest().getAttribute(name); + } else { + if ((specialAttributes[pos] == null) && (specialAttributes[SPECIALS_FIRST_FORWARD_INDEX] == null) && + (pos >= SPECIALS_FIRST_FORWARD_INDEX)) { + // If it's a forward special attribute, and null, it means this + // is an include, so we check the wrapped request since + // the request could have been forwarded before the include + return getRequest().getAttribute(name); + } else { + return specialAttributes[pos]; + } + } + } + + + /** + * Override the getAttributeNames() method of the wrapped request. + */ + @Override + public Enumeration getAttributeNames() { + return new AttributeNamesEnumerator(); + } + + + /** + * Override the removeAttribute() method of the wrapped request. + * + * @param name Name of the attribute to remove + */ + @Override + public void removeAttribute(String name) { + if (!removeSpecial(name)) { + getRequest().removeAttribute(name); + } + } + + + /** + * Override the setAttribute() method of the wrapped request. + * + * @param name Name of the attribute to set + * @param value Value of the attribute to set + */ + @Override + public void setAttribute(String name, Object value) { + + if (name.equals(Globals.DISPATCHER_TYPE_ATTR)) { + dispatcherType = (DispatcherType) value; + return; + } else if (name.equals(Globals.DISPATCHER_REQUEST_PATH_ATTR)) { + requestDispatcherPath = value; + return; + } + + if (!setSpecial(name, value)) { + getRequest().setAttribute(name, value); + } + } + + + /** + * Return a RequestDispatcher that wraps the resource at the specified path, which may be interpreted as relative to + * the current request path. + * + * @param path Path of the resource to be wrapped + */ + @Override + public RequestDispatcher getRequestDispatcher(String path) { + + if (context == null) { + return null; + } + + if (path == null) { + return null; + } + + int fragmentPos = path.indexOf('#'); + if (fragmentPos > -1) { + context.getLogger().warn(sm.getString("applicationHttpRequest.fragmentInDispatchPath", path)); + path = path.substring(0, fragmentPos); + } + + // If the path is already context-relative, just pass it through + if (path.startsWith("/")) { + return context.getServletContext().getRequestDispatcher(path); + } + + // Convert a request-relative path to a context-relative one + String servletPath = (String) getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + if (servletPath == null) { + servletPath = getServletPath(); + } + + // Add the path info, if there is any + String pathInfo = getPathInfo(); + String requestPath = null; + + if (pathInfo == null) { + requestPath = servletPath; + } else { + requestPath = servletPath + pathInfo; + } + + int pos = requestPath.lastIndexOf('/'); + String relative = null; + if (context.getDispatchersUseEncodedPaths()) { + if (pos >= 0) { + relative = URLEncoder.DEFAULT.encode(requestPath.substring(0, pos + 1), StandardCharsets.UTF_8) + path; + } else { + relative = URLEncoder.DEFAULT.encode(requestPath, StandardCharsets.UTF_8) + path; + } + } else { + if (pos >= 0) { + relative = requestPath.substring(0, pos + 1) + path; + } else { + relative = requestPath + path; + } + } + + return context.getServletContext().getRequestDispatcher(relative); + } + + + /** + * Override the getDispatcherType() method of the wrapped request. + */ + @Override + public DispatcherType getDispatcherType() { + return dispatcherType; + } + + + // --------------------------------------------- HttpServletRequest Methods + + /** + * Override the getContextPath() method of the wrapped request. + */ + @Override + public String getContextPath() { + return this.contextPath; + } + + + /** + * Override the getParameter() method of the wrapped request. + * + * @param name Name of the requested parameter + */ + @Override + public String getParameter(String name) { + parseParameters(); + + String[] value = parameters.get(name); + if (value == null) { + return null; + } + return value[0]; + } + + + /** + * Override the getParameterMap() method of the wrapped request. + */ + @Override + public Map getParameterMap() { + parseParameters(); + return parameters; + } + + + /** + * Override the getParameterNames() method of the wrapped request. + */ + @Override + public Enumeration getParameterNames() { + parseParameters(); + return Collections.enumeration(parameters.keySet()); + } + + + /** + * Override the getParameterValues() method of the wrapped request. + * + * @param name Name of the requested parameter + */ + @Override + public String[] getParameterValues(String name) { + parseParameters(); + return parameters.get(name); + } + + + /** + * Override the getPathInfo() method of the wrapped request. + */ + @Override + public String getPathInfo() { + return this.pathInfo; + } + + + /** + * Override the getPathTranslated() method of the wrapped request. + */ + @Override + public String getPathTranslated() { + if (getPathInfo() == null || getServletContext() == null) { + return null; + } + + return getServletContext().getRealPath(getPathInfo()); + } + + + /** + * Override the getQueryString() method of the wrapped request. + */ + @Override + public String getQueryString() { + return this.queryString; + } + + + /** + * Override the getRequestURI() method of the wrapped request. + */ + @Override + public String getRequestURI() { + return this.requestURI; + } + + + /** + * Override the getRequestURL() method of the wrapped request. + */ + @Override + public StringBuffer getRequestURL() { + return RequestUtil.getRequestURL(this); + } + + + /** + * Override the getServletPath() method of the wrapped request. + */ + @Override + public String getServletPath() { + return this.servletPath; + } + + + @Override + public HttpServletMapping getHttpServletMapping() { + return mapping; + } + + + /** + * Return the session associated with this Request, creating one if necessary. + */ + @Override + public HttpSession getSession() { + return getSession(true); + } + + + /** + * Return the session associated with this Request, creating one if necessary and requested. + * + * @param create Create a new session if one does not exist + */ + @Override + public HttpSession getSession(boolean create) { + + if (crossContext) { + + // There cannot be a session if no context has been assigned yet + if (context == null) { + return null; + } + + // Return the current session if it exists and is valid + if (session != null && session.isValid()) { + return session.getSession(); + } + + HttpSession other = super.getSession(false); + if (create && (other == null)) { + // First create a session in the first context: the problem is + // that the top level request is the only one which can + // create the cookie safely + other = super.getSession(true); + } + if (other != null) { + Session localSession = null; + try { + localSession = context.getManager().findSession(other.getId()); + if (localSession != null && !localSession.isValid()) { + localSession = null; + } + } catch (IOException e) { + // Ignore + } + if (localSession == null && create) { + localSession = context.getManager().createSession(other.getId()); + } + if (localSession != null) { + localSession.access(); + session = localSession; + return session.getSession(); + } + } + return null; + + } else { + return super.getSession(create); + } + } + + + /** + * Returns true if the request specifies a JSESSIONID that is valid within the context of this + * ApplicationHttpRequest, false otherwise. + * + * @return true if the request specifies a JSESSIONID that is valid within the context of this + * ApplicationHttpRequest, false otherwise. + */ + @Override + public boolean isRequestedSessionIdValid() { + + if (crossContext) { + + String requestedSessionId = getRequestedSessionId(); + if (requestedSessionId == null) { + return false; + } + if (context == null) { + return false; + } + Manager manager = context.getManager(); + if (manager == null) { + return false; + } + Session session = null; + try { + session = manager.findSession(requestedSessionId); + } catch (IOException e) { + // Ignore + } + if ((session != null) && session.isValid()) { + return true; + } else { + return false; + } + + } else { + return super.isRequestedSessionIdValid(); + } + } + + + @Override + public PushBuilder newPushBuilder() { + ServletRequest current = getRequest(); + while (current instanceof ServletRequestWrapper) { + current = ((ServletRequestWrapper) current).getRequest(); + } + if (current instanceof RequestFacade) { + return ((RequestFacade) current).newPushBuilder(this); + } else { + return null; + } + } + + + // -------------------------------------------------------- Package Methods + + /** + * Recycle this request + */ + public void recycle() { + if (session != null) { + try { + session.endAccess(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + context.getLogger().warn(sm.getString("applicationHttpRequest.sessionEndAccessFail"), t); + } + } + } + + + /** + * Set the context path for this request. + * + * @param contextPath The new context path + */ + void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + + /** + * Set the path information for this request. + * + * @param pathInfo The new path info + */ + void setPathInfo(String pathInfo) { + this.pathInfo = pathInfo; + } + + + /** + * Set the query string for this request. + * + * @param queryString The new query string + */ + void setQueryString(String queryString) { + this.queryString = queryString; + } + + + /** + * Set the request that we are wrapping. + * + * @param request The new wrapped request + */ + void setRequest(HttpServletRequest request) { + + super.setRequest(request); + + // Initialize the attributes for this request + dispatcherType = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR); + requestDispatcherPath = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR); + + // Initialize the path elements for this request + contextPath = request.getContextPath(); + pathInfo = request.getPathInfo(); + queryString = request.getQueryString(); + requestURI = request.getRequestURI(); + servletPath = request.getServletPath(); + mapping = request.getHttpServletMapping(); + } + + + /** + * Set the request URI for this request. + * + * @param requestURI The new request URI + */ + void setRequestURI(String requestURI) { + this.requestURI = requestURI; + } + + + /** + * Set the servlet path for this request. + * + * @param servletPath The new servlet path + */ + void setServletPath(String servletPath) { + this.servletPath = servletPath; + } + + + /** + * Parses the parameters of this request. If parameters are present in both the query string and the request + * content, they are merged. + */ + void parseParameters() { + + if (parsedParams) { + return; + } + + parameters = new ParameterMap<>(); + parameters.putAll(getRequest().getParameterMap()); + mergeParameters(); + ((ParameterMap) parameters).setLocked(true); + parsedParams = true; + } + + + /** + * Save query parameters for this request. + * + * @param queryString The query string containing parameters for this request + */ + void setQueryParams(String queryString) { + this.queryParamString = queryString; + } + + + void setMapping(HttpServletMapping mapping) { + this.mapping = mapping; + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Is this attribute name one of the special ones that is added only for included servlets? + * + * @param name Attribute name to be tested + */ + protected boolean isSpecial(String name) { + // Performance - see BZ 68089 + if (name.length() < shortestSpecialNameLength) { + return false; + } + return specialsMap.containsKey(name); + } + + + /** + * Get a special attribute. + * + * @return the special attribute pos, or -1 if it is not a special attribute + */ + protected int getSpecial(String name) { + // Performance - see BZ 68089 + if (name.length() < shortestSpecialNameLength) { + return -1; + } + Integer index = specialsMap.get(name); + if (index == null) { + return -1; + } + return index.intValue(); + } + + + /** + * Set a special attribute. + * + * @return true if the attribute was a special attribute, false otherwise + */ + protected boolean setSpecial(String name, Object value) { + // Performance - see BZ 68089 + if (name.length() < shortestSpecialNameLength) { + return false; + } + Integer index = specialsMap.get(name); + if (index == null) { + return false; + } + specialAttributes[index.intValue()] = value; + return true; + } + + + /** + * Remove a special attribute. + * + * @return true if the attribute was a special attribute, false otherwise + */ + protected boolean removeSpecial(String name) { + return setSpecial(name, null); + } + + + /** + * Merge the two sets of parameter values into a single String array. + * + * @param values1 First set of values + * @param values2 Second set of values + */ + private String[] mergeValues(String[] values1, String[] values2) { + + List results = new ArrayList<>(); + + if (values1 == null) { + // Skip - nothing to merge + } else { + results.addAll(Arrays.asList(values1)); + } + + if (values2 == null) { + // Skip - nothing to merge + } else { + results.addAll(Arrays.asList(values2)); + } + + return results.toArray(new String[0]); + } + + + // ------------------------------------------------------ Private Methods + + /** + * Merge the parameters from the saved query parameter string (if any), and the parameters already present on this + * request (if any), such that the parameter values from the query string show up first if there are duplicate + * parameter names. + */ + private void mergeParameters() { + + if ((queryParamString == null) || (queryParamString.length() < 1)) { + return; + } + + // Parse the query string from the dispatch target + Parameters paramParser = new Parameters(); + MessageBytes queryMB = MessageBytes.newInstance(); + queryMB.setString(queryParamString); + + // TODO + // - Should only use body encoding if useBodyEncodingForURI is true + // - Otherwise, should use URIEncoding + // - The problem is that the connector is not available... + // - To add to the fun, the URI default changed in Servlet 4.0 to UTF-8 + + String encoding = getCharacterEncoding(); + Charset charset = null; + if (encoding != null) { + try { + charset = B2CConverter.getCharset(encoding); + queryMB.setCharset(charset); + } catch (UnsupportedEncodingException e) { + // Fall-back to default (ISO-8859-1) + charset = StandardCharsets.ISO_8859_1; + } + } + + paramParser.setQuery(queryMB); + paramParser.setQueryStringCharset(charset); + paramParser.handleQueryParameters(); + + // Insert the additional parameters from the dispatch target + Enumeration dispParamNames = paramParser.getParameterNames(); + while (dispParamNames.hasMoreElements()) { + String dispParamName = dispParamNames.nextElement(); + String[] dispParamValues = paramParser.getParameterValues(dispParamName); + String[] originalValues = parameters.get(dispParamName); + if (originalValues == null) { + parameters.put(dispParamName, dispParamValues); + continue; + } + parameters.put(dispParamName, mergeValues(dispParamValues, originalValues)); + } + } + + + // ----------------------------------- AttributeNamesEnumerator Inner Class + + /** + * Utility class used to expose the special attributes as being available as request attributes. + */ + protected class AttributeNamesEnumerator implements Enumeration { + + protected int pos = -1; + protected final int last; + protected final Enumeration parentEnumeration; + protected String next = null; + + public AttributeNamesEnumerator() { + int last = -1; + parentEnumeration = getRequest().getAttributeNames(); + for (int i = specialAttributes.length - 1; i >= 0; i--) { + if (getAttribute(specials[i]) != null) { + last = i; + break; + } + } + this.last = last; + } + + @Override + public boolean hasMoreElements() { + return ((pos != last) || (next != null) || ((next = findNext()) != null)); + } + + @Override + public String nextElement() { + if (pos != last) { + for (int i = pos + 1; i <= last; i++) { + if (getAttribute(specials[i]) != null) { + pos = i; + return specials[i]; + } + } + } + String result = next; + if (next != null) { + next = findNext(); + } else { + throw new NoSuchElementException(); + } + return result; + } + + protected String findNext() { + String result = null; + while ((result == null) && (parentEnumeration.hasMoreElements())) { + String current = parentEnumeration.nextElement(); + if (!isSpecial(current)) { + result = current; + } + } + return result; + } + } +} diff --git a/java/org/apache/catalina/core/ApplicationHttpResponse.java b/java/org/apache/catalina/core/ApplicationHttpResponse.java new file mode 100644 index 0000000..73ae1ed --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationHttpResponse.java @@ -0,0 +1,363 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.util.Locale; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + + +/** + * Wrapper around a jakarta.servlet.http.HttpServletResponse that transforms an application response object + * (which might be the original one passed to a servlet, or might be based on the 2.3 + * jakarta.servlet.http.HttpServletResponseWrapper class) back into an internal + * org.apache.catalina.HttpResponse. + *

    + * WARNING: Due to Java's lack of support for multiple inheritance, all of the logic in + * ApplicationResponse is duplicated in ApplicationHttpResponse. Make sure that you keep these + * two classes in synchronization when making changes! + * + * @author Craig R. McClanahan + */ +class ApplicationHttpResponse extends HttpServletResponseWrapper { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new wrapped response around the specified servlet response. + * + * @param response The servlet response being wrapped + * @param included true if this response is being processed by a + * RequestDispatcher.include() call + */ + ApplicationHttpResponse(HttpServletResponse response, boolean included) { + + super(response); + setIncluded(included); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Is this wrapped response the subject of an include() call? + */ + protected boolean included = false; + + + // ------------------------------------------------ ServletResponse Methods + + /** + * Disallow reset() calls on a included response. + * + * @exception IllegalStateException if the response has already been committed + */ + @Override + public void reset() { + + // If already committed, the wrapped response will throw ISE + if (!included || getResponse().isCommitted()) { + getResponse().reset(); + } + + } + + + /** + * Disallow setContentLength(int) calls on an included response. + * + * @param len The new content length + */ + @Override + public void setContentLength(int len) { + + if (!included) { + getResponse().setContentLength(len); + } + + } + + + /** + * Disallow setContentLengthLong(long) calls on an included response. + * + * @param len The new content length + */ + @Override + public void setContentLengthLong(long len) { + + if (!included) { + getResponse().setContentLengthLong(len); + } + + } + + + /** + * Disallow setContentType() calls on an included response. + * + * @param type The new content type + */ + @Override + public void setContentType(String type) { + + if (!included) { + getResponse().setContentType(type); + } + + } + + + /** + * Disallow setLocale() calls on an included response. + * + * @param loc The new locale + */ + @Override + public void setLocale(Locale loc) { + + if (!included) { + getResponse().setLocale(loc); + } + + } + + + /** + * Ignore setBufferSize() calls on an included response. + * + * @param size The buffer size + */ + @Override + public void setBufferSize(int size) { + if (!included) { + getResponse().setBufferSize(size); + } + } + + + // -------------------------------------------- HttpServletResponse Methods + + + /** + * Disallow addCookie() calls on an included response. + * + * @param cookie The new cookie + */ + @Override + public void addCookie(Cookie cookie) { + + if (!included) { + ((HttpServletResponse) getResponse()).addCookie(cookie); + } + + } + + + /** + * Disallow addDateHeader() calls on an included response. + * + * @param name The new header name + * @param value The new header value + */ + @Override + public void addDateHeader(String name, long value) { + + if (!included) { + ((HttpServletResponse) getResponse()).addDateHeader(name, value); + } + + } + + + /** + * Disallow addHeader() calls on an included response. + * + * @param name The new header name + * @param value The new header value + */ + @Override + public void addHeader(String name, String value) { + + if (!included) { + ((HttpServletResponse) getResponse()).addHeader(name, value); + } + + } + + + /** + * Disallow addIntHeader() calls on an included response. + * + * @param name The new header name + * @param value The new header value + */ + @Override + public void addIntHeader(String name, int value) { + + if (!included) { + ((HttpServletResponse) getResponse()).addIntHeader(name, value); + } + + } + + + /** + * Disallow sendError() calls on an included response. + * + * @param sc The new status code + * + * @exception IOException if an input/output error occurs + */ + @Override + public void sendError(int sc) throws IOException { + + if (!included) { + ((HttpServletResponse) getResponse()).sendError(sc); + } + + } + + + /** + * Disallow sendError() calls on an included response. + * + * @param sc The new status code + * @param msg The new message + * + * @exception IOException if an input/output error occurs + */ + @Override + public void sendError(int sc, String msg) throws IOException { + + if (!included) { + ((HttpServletResponse) getResponse()).sendError(sc, msg); + } + + } + + + /** + * Disallow sendRedirect() calls on an included response. + * + * @param location The new location + * + * @exception IOException if an input/output error occurs + */ + @Override + public void sendRedirect(String location) throws IOException { + + if (!included) { + ((HttpServletResponse) getResponse()).sendRedirect(location); + } + + } + + + /** + * Disallow setDateHeader() calls on an included response. + * + * @param name The new header name + * @param value The new header value + */ + @Override + public void setDateHeader(String name, long value) { + + if (!included) { + ((HttpServletResponse) getResponse()).setDateHeader(name, value); + } + + } + + + /** + * Disallow setHeader() calls on an included response. + * + * @param name The new header name + * @param value The new header value + */ + @Override + public void setHeader(String name, String value) { + + if (!included) { + ((HttpServletResponse) getResponse()).setHeader(name, value); + } + + } + + + /** + * Disallow setIntHeader() calls on an included response. + * + * @param name The new header name + * @param value The new header value + */ + @Override + public void setIntHeader(String name, int value) { + + if (!included) { + ((HttpServletResponse) getResponse()).setIntHeader(name, value); + } + + } + + + /** + * Disallow setStatus() calls on an included response. + * + * @param sc The new status code + */ + @Override + public void setStatus(int sc) { + + if (!included) { + ((HttpServletResponse) getResponse()).setStatus(sc); + } + + } + + + // -------------------------------------------------------- Package Methods + + /** + * Set the included flag for this response. + * + * @param included The new included flag + */ + void setIncluded(boolean included) { + + this.included = included; + + } + + + /** + * Set the response that we are wrapping. + * + * @param response The new wrapped response + */ + void setResponse(HttpServletResponse response) { + + super.setResponse(response); + + } +} diff --git a/java/org/apache/catalina/core/ApplicationMapping.java b/java/org/apache/catalina/core/ApplicationMapping.java new file mode 100644 index 0000000..fd3cd06 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationMapping.java @@ -0,0 +1,117 @@ +/* + * 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.catalina.core; + +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.MappingMatch; + +import org.apache.catalina.mapper.MappingData; + +public class ApplicationMapping { + + private final MappingData mappingData; + + private volatile HttpServletMapping mapping = null; + + public ApplicationMapping(MappingData mappingData) { + this.mappingData = mappingData; + } + + public HttpServletMapping getHttpServletMapping() { + if (mapping == null) { + String servletName; + if (mappingData.wrapper == null) { + servletName = ""; + } else { + servletName = mappingData.wrapper.getName(); + } + if (mappingData.matchType == null) { + mapping = new MappingImpl("", "", null, servletName); + } else { + switch (mappingData.matchType) { + case CONTEXT_ROOT: + mapping = new MappingImpl("", "", mappingData.matchType, servletName); + break; + case DEFAULT: + mapping = new MappingImpl("", "/", mappingData.matchType, servletName); + break; + case EXACT: + mapping = new MappingImpl(mappingData.wrapperPath.toString().substring(1), + mappingData.wrapperPath.toString(), mappingData.matchType, servletName); + break; + case EXTENSION: + String path = mappingData.wrapperPath.toString(); + int extIndex = path.lastIndexOf('.'); + mapping = new MappingImpl(path.substring(1, extIndex), "*" + path.substring(extIndex), + mappingData.matchType, servletName); + break; + case PATH: + String matchValue; + if (mappingData.pathInfo.isNull()) { + matchValue = null; + } else { + matchValue = mappingData.pathInfo.toString().substring(1); + } + mapping = new MappingImpl(matchValue, mappingData.wrapperPath.toString() + "/*", + mappingData.matchType, servletName); + break; + } + } + } + + return mapping; + } + + public void recycle() { + mapping = null; + } + + private static class MappingImpl implements HttpServletMapping { + + private final String matchValue; + private final String pattern; + private final MappingMatch mappingType; + private final String servletName; + + MappingImpl(String matchValue, String pattern, MappingMatch mappingType, String servletName) { + this.matchValue = matchValue; + this.pattern = pattern; + this.mappingType = mappingType; + this.servletName = servletName; + } + + @Override + public String getMatchValue() { + return matchValue; + } + + @Override + public String getPattern() { + return pattern; + } + + @Override + public MappingMatch getMappingMatch() { + return mappingType; + } + + @Override + public String getServletName() { + return servletName; + } + } +} diff --git a/java/org/apache/catalina/core/ApplicationPart.java b/java/org/apache/catalina/core/ApplicationPart.java new file mode 100644 index 0000000..6272037 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationPart.java @@ -0,0 +1,165 @@ +/* + * 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.catalina.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; + +import jakarta.servlet.http.Part; + +import org.apache.tomcat.util.http.fileupload.FileItem; +import org.apache.tomcat.util.http.fileupload.ParameterParser; +import org.apache.tomcat.util.http.fileupload.disk.DiskFileItem; +import org.apache.tomcat.util.http.parser.HttpParser; + +/** + * Adaptor to allow {@link FileItem} objects generated by the package renamed commons-upload to be used by the Servlet + * 3.0 upload API that expects {@link Part}s. + */ +public class ApplicationPart implements Part { + + private final FileItem fileItem; + private final File location; + + public ApplicationPart(FileItem fileItem, File location) { + this.fileItem = fileItem; + this.location = location; + } + + @Override + public void delete() throws IOException { + fileItem.delete(); + } + + @Override + public String getContentType() { + return fileItem.getContentType(); + } + + @Override + public String getHeader(String name) { + if (fileItem instanceof DiskFileItem) { + return fileItem.getHeaders().getHeader(name); + } + return null; + } + + @Override + public Collection getHeaderNames() { + if (fileItem instanceof DiskFileItem) { + LinkedHashSet headerNames = new LinkedHashSet<>(); + Iterator iter = fileItem.getHeaders().getHeaderNames(); + while (iter.hasNext()) { + headerNames.add(iter.next()); + } + return headerNames; + } + return Collections.emptyList(); + } + + @Override + public Collection getHeaders(String name) { + if (fileItem instanceof DiskFileItem) { + LinkedHashSet headers = new LinkedHashSet<>(); + Iterator iter = fileItem.getHeaders().getHeaders(name); + while (iter.hasNext()) { + headers.add(iter.next()); + } + return headers; + } + return Collections.emptyList(); + } + + @Override + public InputStream getInputStream() throws IOException { + return fileItem.getInputStream(); + } + + @Override + public String getName() { + return fileItem.getFieldName(); + } + + @Override + public long getSize() { + return fileItem.getSize(); + } + + @Override + public void write(String fileName) throws IOException { + File file = new File(fileName); + if (!file.isAbsolute()) { + file = new File(location, fileName); + } + try { + fileItem.write(file); + } catch (Exception e) { + throw new IOException(e); + } + } + + public String getString(String encoding) throws UnsupportedEncodingException, IOException { + return fileItem.getString(encoding); + } + + /* + * Adapted from FileUploadBase.getFileName() + */ + @Override + public String getSubmittedFileName() { + String fileName = null; + String cd = getHeader("Content-Disposition"); + if (cd != null) { + String cdl = cd.toLowerCase(Locale.ENGLISH); + if (cdl.startsWith("form-data") || cdl.startsWith("attachment")) { + ParameterParser paramParser = new ParameterParser(); + paramParser.setLowerCaseNames(true); + // Parameter parser can handle null input + Map params = paramParser.parse(cd, ';'); + if (params.containsKey("filename")) { + fileName = params.get("filename"); + // The parser will remove surrounding '"' but will not + // unquote any \x sequences. + if (fileName != null) { + // RFC 6266. This is either a token or a quoted-string + if (fileName.indexOf('\\') > -1) { + // This is a quoted-string + fileName = HttpParser.unquote(fileName.trim()); + } else { + // This is a token + fileName = fileName.trim(); + } + } else { + // Even if there is no value, the parameter is present, + // so we return an empty file name rather than no file + // name. + fileName = ""; + } + } + } + } + return fileName; + } +} diff --git a/java/org/apache/catalina/core/ApplicationPushBuilder.java b/java/org/apache/catalina/core/ApplicationPushBuilder.java new file mode 100644 index 0000000..9025032 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationPushBuilder.java @@ -0,0 +1,427 @@ +/* + * 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.catalina.core; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.PushBuilder; + +import org.apache.catalina.Context; +import org.apache.catalina.authenticator.AuthenticatorBase; +import org.apache.catalina.connector.Request; +import org.apache.catalina.util.SessionConfig; +import org.apache.coyote.ActionCode; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; +import org.apache.tomcat.util.http.CookieProcessor; +import org.apache.tomcat.util.http.parser.HttpParser; +import org.apache.tomcat.util.res.StringManager; + +public class ApplicationPushBuilder implements PushBuilder { + + private static final StringManager sm = StringManager.getManager(ApplicationPushBuilder.class); + private static final Set DISALLOWED_METHODS = new HashSet<>(); + + static { + DISALLOWED_METHODS.add("POST"); + DISALLOWED_METHODS.add("PUT"); + DISALLOWED_METHODS.add("DELETE"); + DISALLOWED_METHODS.add("CONNECT"); + DISALLOWED_METHODS.add("OPTIONS"); + DISALLOWED_METHODS.add("TRACE"); + } + + private final HttpServletRequest baseRequest; + private final Request catalinaRequest; + private final org.apache.coyote.Request coyoteRequest; + private final String sessionCookieName; + private final String sessionPathParameterName; + private final boolean addSessionCookie; + private final boolean addSessionPathParameter; + + private final Map> headers = new CaseInsensitiveKeyMap<>(); + private final List cookies = new ArrayList<>(); + private String method = "GET"; + private String path; + private String queryString; + private String sessionId; + private String userName; + + + public ApplicationPushBuilder(Request catalinaRequest, HttpServletRequest request) { + + baseRequest = request; + this.catalinaRequest = catalinaRequest; + coyoteRequest = catalinaRequest.getCoyoteRequest(); + + // Populate the initial list of HTTP headers + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + List values = new ArrayList<>(); + headers.put(headerName, values); + Enumeration headerValues = request.getHeaders(headerName); + while (headerValues.hasMoreElements()) { + values.add(headerValues.nextElement()); + } + } + + // Remove the headers + headers.remove("if-match"); + headers.remove("if-none-match"); + headers.remove("if-modified-since"); + headers.remove("if-unmodified-since"); + headers.remove("if-range"); + headers.remove("range"); + headers.remove("expect"); + headers.remove("authorization"); + headers.remove("referer"); + // Also remove the cookie header since it will be regenerated + headers.remove("cookie"); + + // set the referer header + StringBuffer referer = request.getRequestURL(); + if (request.getQueryString() != null) { + referer.append('?'); + referer.append(request.getQueryString()); + } + addHeader("referer", referer.toString()); + + // Session + Context context = catalinaRequest.getContext(); + sessionCookieName = SessionConfig.getSessionCookieName(context); + sessionPathParameterName = SessionConfig.getSessionUriParamName(context); + + HttpSession session = request.getSession(false); + if (session != null) { + sessionId = session.getId(); + } + if (sessionId == null) { + sessionId = request.getRequestedSessionId(); + } + if (!request.isRequestedSessionIdFromCookie() && !request.isRequestedSessionIdFromURL() && sessionId != null) { + Set sessionTrackingModes = + request.getServletContext().getEffectiveSessionTrackingModes(); + addSessionCookie = sessionTrackingModes.contains(SessionTrackingMode.COOKIE); + addSessionPathParameter = sessionTrackingModes.contains(SessionTrackingMode.URL); + } else { + addSessionCookie = request.isRequestedSessionIdFromCookie(); + addSessionPathParameter = request.isRequestedSessionIdFromURL(); + } + + // Cookies + if (request.getCookies() != null) { + cookies.addAll(Arrays.asList(request.getCookies())); + } + for (Cookie responseCookie : catalinaRequest.getResponse().getCookies()) { + if (responseCookie.getMaxAge() < 0) { + // Path information not available so can only remove based on + // name. + cookies.removeIf(cookie -> cookie.getName().equals(responseCookie.getName())); + } else { + cookies.add(new Cookie(responseCookie.getName(), responseCookie.getValue())); + } + } + if (cookies.size() > 0) { + List cookieValues = new ArrayList<>(1); + cookieValues.add(generateCookieHeader(cookies, catalinaRequest.getContext().getCookieProcessor())); + headers.put("cookie", cookieValues); + } + + // Authentication + if (catalinaRequest.getPrincipal() != null) { + if ((session == null) || catalinaRequest.getSessionInternal(false).getPrincipal() == null || + !(context.getAuthenticator() instanceof AuthenticatorBase) || + !((AuthenticatorBase) context.getAuthenticator()).getCache()) { + // Set a username only if there is no session cache for the principal + userName = catalinaRequest.getPrincipal().getName(); + } + setHeader("authorization", "x-push"); + } + } + + @Override + public PushBuilder path(String path) { + if (path.startsWith("/")) { + this.path = path; + } else { + String contextPath = baseRequest.getContextPath(); + int len = contextPath.length() + path.length() + 1; + StringBuilder sb = new StringBuilder(len); + sb.append(contextPath); + sb.append('/'); + sb.append(path); + this.path = sb.toString(); + } + return this; + } + + + @Override + public String getPath() { + return path; + } + + + @Override + public PushBuilder method(String method) { + String upperMethod = method.trim().toUpperCase(Locale.ENGLISH); + if (DISALLOWED_METHODS.contains(upperMethod) || upperMethod.length() == 0) { + throw new IllegalArgumentException(sm.getString("applicationPushBuilder.methodInvalid", upperMethod)); + } + // Check a token was supplied + if (!HttpParser.isToken(upperMethod)) { + throw new IllegalArgumentException(sm.getString("applicationPushBuilder.methodNotToken", upperMethod)); + } + this.method = method; + return this; + } + + + @Override + public String getMethod() { + return method; + } + + + @Override + public PushBuilder queryString(String queryString) { + this.queryString = queryString; + return this; + } + + + @Override + public String getQueryString() { + return queryString; + } + + + @Override + public PushBuilder sessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + + @Override + public String getSessionId() { + return sessionId; + } + + + @Override + public PushBuilder addHeader(String name, String value) { + headers.computeIfAbsent(name, k -> new ArrayList<>()).add(value); + return this; + } + + + @Override + public PushBuilder setHeader(String name, String value) { + List values = headers.get(name); + if (values == null) { + values = new ArrayList<>(); + headers.put(name, values); + } else { + values.clear(); + } + values.add(value); + + return this; + } + + + @Override + public PushBuilder removeHeader(String name) { + headers.remove(name); + + return this; + } + + + @Override + public Set getHeaderNames() { + return Collections.unmodifiableSet(headers.keySet()); + } + + + @Override + public String getHeader(String name) { + List values = headers.get(name); + if (values == null) { + return null; + } else { + return values.get(0); + } + } + + + @Override + public void push() { + if (path == null) { + throw new IllegalStateException(sm.getString("pushBuilder.noPath")); + } + + org.apache.coyote.Request pushTarget = new org.apache.coyote.Request(); + + pushTarget.method().setString(method); + // The next three are implied by the Javadoc getPath() + pushTarget.serverName().setString(baseRequest.getServerName()); + pushTarget.setServerPort(baseRequest.getServerPort()); + pushTarget.scheme().setString(baseRequest.getScheme()); + + // Copy headers + for (Map.Entry> header : headers.entrySet()) { + for (String value : header.getValue()) { + pushTarget.getMimeHeaders().addValue(header.getKey()).setString(value); + } + } + + // Path and query string + int queryIndex = path.indexOf('?'); + String pushPath; + String pushQueryString = null; + if (queryIndex > -1) { + pushPath = path.substring(0, queryIndex); + if (queryIndex + 1 < path.length()) { + pushQueryString = path.substring(queryIndex + 1); + } + } else { + pushPath = path; + } + + // Session ID (do this before setting the path since it may change it) + if (sessionId != null) { + if (addSessionPathParameter) { + pushPath = pushPath + ";" + sessionPathParameterName + "=" + sessionId; + pushTarget.addPathParameter(sessionPathParameterName, sessionId); + } + if (addSessionCookie) { + String sessionCookieHeader = sessionCookieName + "=" + sessionId; + MessageBytes mb = pushTarget.getMimeHeaders().getValue("cookie"); + if (mb == null) { + mb = pushTarget.getMimeHeaders().addValue("cookie"); + mb.setString(sessionCookieHeader); + } else { + mb.setString(mb.getString() + ";" + sessionCookieHeader); + } + } + } + + // Undecoded path - just %nn encoded + pushTarget.requestURI().setString(pushPath); + pushTarget.decodedURI().setString(decode(pushPath, catalinaRequest.getConnector().getURICharset())); + + // Query string + if (pushQueryString == null && queryString != null) { + pushTarget.queryString().setString(queryString); + } else if (pushQueryString != null && queryString == null) { + pushTarget.queryString().setString(pushQueryString); + } else if (pushQueryString != null && queryString != null) { + pushTarget.queryString().setString(pushQueryString + "&" + queryString); + } + + // Authorization + if (userName != null) { + pushTarget.getRemoteUser().setString(userName); + pushTarget.setRemoteUserNeedsAuthorization(true); + } + + coyoteRequest.action(ActionCode.PUSH_REQUEST, pushTarget); + + // Reset for next call to this method + path = null; + headers.remove("if-none-match"); + headers.remove("if-modified-since"); + } + + + // Package private so it can be tested. charsetName must be in lower case. + static String decode(String input, Charset charset) { + int start = input.indexOf('%'); + int end = 0; + + // Shortcut + if (start == -1) { + return input; + } + + StringBuilder result = new StringBuilder(input.length()); + while (start != -1) { + // Found the start of a %nn sequence. Copy everything from the last + // end to this start to the output. + result.append(input.substring(end, start)); + // Advance the end 3 characters: %nn + end = start + 3; + while (end < input.length() && input.charAt(end) == '%') { + end += 3; + } + result.append(decodePercentSequence(input.substring(start, end), charset)); + start = input.indexOf('%', end); + } + // Append the remaining text + result.append(input.substring(end)); + + return result.toString(); + } + + + private static String decodePercentSequence(String sequence, Charset charset) { + byte[] bytes = new byte[sequence.length() / 3]; + for (int i = 0; i < bytes.length; i += 3) { + bytes[i] = (byte) ((HexUtils.getDec(sequence.charAt(1 + 3 * i)) << 4) + + HexUtils.getDec(sequence.charAt(2 + 3 * i))); + } + + return new String(bytes, charset); + } + + + private static String generateCookieHeader(List cookies, CookieProcessor cookieProcessor) { + StringBuilder result = new StringBuilder(); + boolean first = true; + for (Cookie cookie : cookies) { + if (first) { + first = false; + } else { + result.append(';'); + } + // The cookie header value generated by the CookieProcessor was + // originally intended for the Set-Cookie header on the response. + // However, if passed a Cookie with just a name and value set it + // will generate an appropriate header for the Cookie header on the + // pushed request. + result.append(cookieProcessor.generateHeader(cookie, null)); + } + return result.toString(); + } +} diff --git a/java/org/apache/catalina/core/ApplicationRequest.java b/java/org/apache/catalina/core/ApplicationRequest.java new file mode 100644 index 0000000..cdbb845 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationRequest.java @@ -0,0 +1,180 @@ +/* + * 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.catalina.core; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestWrapper; + +/** + * Wrapper around a jakarta.servlet.ServletRequest that transforms an application request object (which + * might be the original one passed to a servlet, or might be based on the 2.3 + * jakarta.servlet.ServletRequestWrapper class) back into an internal + * org.apache.catalina.Request. + *

    + * WARNING: Due to Java's lack of support for multiple inheritance, all of the logic in + * ApplicationRequest is duplicated in ApplicationHttpRequest. Make sure that you keep these + * two classes in synchronization when making changes! + * + * @author Craig R. McClanahan + */ +class ApplicationRequest extends ServletRequestWrapper { + + /** + * The set of attribute names that are special for request dispatchers. + * + * @deprecated Will be removed without replacement in Tomcat 11 onwards. + */ + @Deprecated + protected static final String specials[] = + { RequestDispatcher.INCLUDE_REQUEST_URI, RequestDispatcher.INCLUDE_CONTEXT_PATH, + RequestDispatcher.INCLUDE_SERVLET_PATH, RequestDispatcher.INCLUDE_PATH_INFO, + RequestDispatcher.INCLUDE_QUERY_STRING, RequestDispatcher.INCLUDE_MAPPING, + RequestDispatcher.FORWARD_REQUEST_URI, RequestDispatcher.FORWARD_CONTEXT_PATH, + RequestDispatcher.FORWARD_SERVLET_PATH, RequestDispatcher.FORWARD_PATH_INFO, + RequestDispatcher.FORWARD_QUERY_STRING, RequestDispatcher.FORWARD_MAPPING }; + /* + * This duplicates specials but has been added to improve the performance of isSpecial(). + */ + private static final Set specialsSet = new HashSet<>(Arrays.asList(specials)); + + private static final int shortestSpecialNameLength = + specialsSet.stream().mapToInt(s -> s.length()).min().getAsInt(); + + + /** + * The request attributes for this request. This is initialized from the wrapped request, but updates are allowed. + */ + protected final HashMap attributes = new HashMap<>(); + + /** + * Construct a new wrapped request around the specified servlet request. + * + * @param request The servlet request being wrapped + */ + ApplicationRequest(ServletRequest request) { + super(request); + setRequest(request); + } + + + // ------------------------------------------------- ServletRequest Methods + + /** + * Override the getAttribute() method of the wrapped request. + * + * @param name Name of the attribute to retrieve + */ + @Override + public Object getAttribute(String name) { + synchronized (attributes) { + return attributes.get(name); + } + } + + + /** + * Override the getAttributeNames() method of the wrapped request. + */ + @Override + public Enumeration getAttributeNames() { + synchronized (attributes) { + return Collections.enumeration(attributes.keySet()); + } + } + + + /** + * Override the removeAttribute() method of the wrapped request. + * + * @param name Name of the attribute to remove + */ + @Override + public void removeAttribute(String name) { + synchronized (attributes) { + attributes.remove(name); + if (!isSpecial(name)) { + getRequest().removeAttribute(name); + } + } + } + + + /** + * Override the setAttribute() method of the wrapped request. + * + * @param name Name of the attribute to set + * @param value Value of the attribute to set + */ + @Override + public void setAttribute(String name, Object value) { + synchronized (attributes) { + attributes.put(name, value); + if (!isSpecial(name)) { + getRequest().setAttribute(name, value); + } + } + } + + + /** + * Is this attribute name one of the special ones that is added only for included servlets? + * + * @param name Attribute name to be tested + * + * @deprecated Will be made private in Tomcat 11 onwards. + */ + @Deprecated + protected boolean isSpecial(String name) { + // Performance - see BZ 68089 + if (name.length() < shortestSpecialNameLength) { + return false; + } + return specialsSet.contains(name); + } + + + // ------------------------------------------ ServletRequestWrapper Methods + + /** + * Set the request that we are wrapping. + * + * @param request The new wrapped request + */ + @Override + public void setRequest(ServletRequest request) { + super.setRequest(request); + + // Initialize the attributes for this request + synchronized (attributes) { + attributes.clear(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + Object value = request.getAttribute(name); + attributes.put(name, value); + } + } + } +} diff --git a/java/org/apache/catalina/core/ApplicationResponse.java b/java/org/apache/catalina/core/ApplicationResponse.java new file mode 100644 index 0000000..2173f0a --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationResponse.java @@ -0,0 +1,183 @@ +/* + * 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.catalina.core; + +import java.util.Locale; + +import jakarta.servlet.ServletResponse; +import jakarta.servlet.ServletResponseWrapper; + + +/** + * Wrapper around a jakarta.servlet.ServletResponse that transforms an application response object (which + * might be the original one passed to a servlet, or might be based on the 2.3 + * jakarta.servlet.ServletResponseWrapper class) back into an internal + * org.apache.catalina.Response. + *

    + * WARNING: Due to Java's lack of support for multiple inheritance, all of the logic in + * ApplicationResponse is duplicated in ApplicationHttpResponse. Make sure that you keep these + * two classes in synchronization when making changes! + * + * @author Craig R. McClanahan + */ +class ApplicationResponse extends ServletResponseWrapper { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new wrapped response around the specified servlet response. + * + * @param response The servlet response being wrapped + * @param included true if this response is being processed by a + * RequestDispatcher.include() call + */ + ApplicationResponse(ServletResponse response, boolean included) { + + super(response); + setIncluded(included); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Is this wrapped response the subject of an include() call? + */ + protected boolean included = false; + + + // ------------------------------------------------ ServletResponse Methods + + + /** + * Disallow reset() calls on a included response. + * + * @exception IllegalStateException if the response has already been committed + */ + @Override + public void reset() { + + // If already committed, the wrapped response will throw ISE + if (!included || getResponse().isCommitted()) { + getResponse().reset(); + } + + } + + + /** + * Disallow setContentLength(int) calls on an included response. + * + * @param len The new content length + */ + @Override + public void setContentLength(int len) { + + if (!included) { + getResponse().setContentLength(len); + } + + } + + + /** + * Disallow setContentLengthLong(long) calls on an included response. + * + * @param len The new content length + */ + @Override + public void setContentLengthLong(long len) { + + if (!included) { + getResponse().setContentLengthLong(len); + } + + } + + + /** + * Disallow setContentType() calls on an included response. + * + * @param type The new content type + */ + @Override + public void setContentType(String type) { + + if (!included) { + getResponse().setContentType(type); + } + + } + + + /** + * Ignore setLocale() calls on an included response. + * + * @param loc The new locale + */ + @Override + public void setLocale(Locale loc) { + if (!included) { + getResponse().setLocale(loc); + } + } + + + /** + * Ignore setBufferSize() calls on an included response. + * + * @param size The buffer size + */ + @Override + public void setBufferSize(int size) { + if (!included) { + getResponse().setBufferSize(size); + } + } + + + // ----------------------------------------- ServletResponseWrapper Methods + + + /** + * Set the response that we are wrapping. + * + * @param response The new wrapped response + */ + @Override + public void setResponse(ServletResponse response) { + + super.setResponse(response); + + } + + + // -------------------------------------------------------- Package Methods + + /** + * Set the included flag for this response. + * + * @param included The new included flag + */ + void setIncluded(boolean included) { + + this.included = included; + + } +} diff --git a/java/org/apache/catalina/core/ApplicationServletRegistration.java b/java/org/apache/catalina/core/ApplicationServletRegistration.java new file mode 100644 index 0000000..71adec8 --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationServletRegistration.java @@ -0,0 +1,219 @@ +/* + * 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.catalina.core; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.ServletSecurityElement; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Wrapper; +import org.apache.catalina.util.ParameterMap; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.res.StringManager; + +public class ApplicationServletRegistration implements ServletRegistration.Dynamic { + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(ApplicationServletRegistration.class); + + private final Wrapper wrapper; + private final Context context; + private ServletSecurityElement constraint; + + public ApplicationServletRegistration(Wrapper wrapper, Context context) { + this.wrapper = wrapper; + this.context = context; + + } + + @Override + public String getClassName() { + return wrapper.getServletClass(); + } + + @Override + public String getInitParameter(String name) { + return wrapper.findInitParameter(name); + } + + @Override + public Map getInitParameters() { + ParameterMap result = new ParameterMap<>(); + + String[] parameterNames = wrapper.findInitParameters(); + + for (String parameterName : parameterNames) { + result.put(parameterName, wrapper.findInitParameter(parameterName)); + } + + result.setLocked(true); + return result; + } + + @Override + public String getName() { + return wrapper.getName(); + } + + @Override + public boolean setInitParameter(String name, String value) { + if (name == null || value == null) { + throw new IllegalArgumentException( + sm.getString("applicationFilterRegistration.nullInitParam", name, value)); + } + if (getInitParameter(name) != null) { + return false; + } + + wrapper.addInitParameter(name, value); + + return true; + } + + @Override + public Set setInitParameters(Map initParameters) { + + Set conflicts = new HashSet<>(); + + for (Map.Entry entry : initParameters.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + throw new IllegalArgumentException( + sm.getString("applicationFilterRegistration.nullInitParams", entry.getKey(), entry.getValue())); + } + if (getInitParameter(entry.getKey()) != null) { + conflicts.add(entry.getKey()); + } + } + + // Have to add in a separate loop since spec requires no updates at all + // if there is an issue + if (conflicts.isEmpty()) { + for (Map.Entry entry : initParameters.entrySet()) { + setInitParameter(entry.getKey(), entry.getValue()); + } + } + + return conflicts; + } + + @Override + public void setAsyncSupported(boolean asyncSupported) { + wrapper.setAsyncSupported(asyncSupported); + } + + @Override + public void setLoadOnStartup(int loadOnStartup) { + wrapper.setLoadOnStartup(loadOnStartup); + } + + @Override + public void setMultipartConfig(MultipartConfigElement multipartConfig) { + wrapper.setMultipartConfigElement(multipartConfig); + } + + @Override + public void setRunAsRole(String roleName) { + wrapper.setRunAs(roleName); + } + + @Override + public Set setServletSecurity(ServletSecurityElement constraint) { + if (constraint == null) { + throw new IllegalArgumentException(sm.getString("applicationServletRegistration.setServletSecurity.iae", + getName(), context.getName())); + } + + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException(sm.getString("applicationServletRegistration.setServletSecurity.ise", + getName(), context.getName())); + } + + this.constraint = constraint; + return context.addServletSecurity(this, constraint); + } + + + @Override + public Set addMapping(String... urlPatterns) { + if (urlPatterns == null) { + return Collections.emptySet(); + } + + Set conflicts = new HashSet<>(); + + for (String urlPattern : urlPatterns) { + String wrapperName = context.findServletMapping(urlPattern); + if (wrapperName != null) { + Wrapper wrapper = (Wrapper) context.findChild(wrapperName); + if (wrapper.isOverridable()) { + // Some Wrappers (from global and host web.xml) may be + // overridden rather than generating a conflict + context.removeServletMapping(urlPattern); + } else { + conflicts.add(urlPattern); + } + } + } + + if (!conflicts.isEmpty()) { + return conflicts; + } + + for (String urlPattern : urlPatterns) { + context.addServletMappingDecoded(UDecoder.URLDecode(urlPattern, StandardCharsets.UTF_8), wrapper.getName()); + } + + if (constraint != null) { + context.addServletSecurity(this, constraint); + } + + return Collections.emptySet(); + } + + @Override + public Collection getMappings() { + + Set result = new HashSet<>(); + String servletName = wrapper.getName(); + + String[] urlPatterns = context.findServletMappings(); + for (String urlPattern : urlPatterns) { + String name = context.findServletMapping(urlPattern); + if (name.equals(servletName)) { + result.add(urlPattern); + } + } + return result; + } + + @Override + public String getRunAsRole() { + return wrapper.getRunAs(); + } + +} diff --git a/java/org/apache/catalina/core/ApplicationSessionCookieConfig.java b/java/org/apache/catalina/core/ApplicationSessionCookieConfig.java new file mode 100644 index 0000000..97a001b --- /dev/null +++ b/java/org/apache/catalina/core/ApplicationSessionCookieConfig.java @@ -0,0 +1,248 @@ +/* + * 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.catalina.core; + +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.http.Cookie; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.util.SessionConfig; +import org.apache.tomcat.util.descriptor.web.Constants; +import org.apache.tomcat.util.res.StringManager; + +public class ApplicationSessionCookieConfig implements SessionCookieConfig { + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(ApplicationSessionCookieConfig.class); + + private static final int DEFAULT_MAX_AGE = -1; + private static final boolean DEFAULT_HTTP_ONLY = false; + private static final boolean DEFAULT_SECURE = false; + + private final Map attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + private String name; + private StandardContext context; + + public ApplicationSessionCookieConfig(StandardContext context) { + this.context = context; + } + + @Override + public String getComment() { + return null; + } + + @Override + public String getDomain() { + return getAttribute(Constants.COOKIE_DOMAIN_ATTR); + } + + @Override + public int getMaxAge() { + String maxAge = getAttribute(Constants.COOKIE_MAX_AGE_ATTR); + if (maxAge == null) { + return DEFAULT_MAX_AGE; + } + return Integer.parseInt(maxAge); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getPath() { + return getAttribute(Constants.COOKIE_PATH_ATTR); + } + + @Override + public boolean isHttpOnly() { + String httpOnly = getAttribute(Constants.COOKIE_HTTP_ONLY_ATTR); + if (httpOnly == null) { + return DEFAULT_HTTP_ONLY; + } + return Boolean.parseBoolean(httpOnly); + } + + @Override + public boolean isSecure() { + String secure = getAttribute(Constants.COOKIE_SECURE_ATTR); + if (secure == null) { + return DEFAULT_SECURE; + } + return Boolean.parseBoolean(secure); + } + + @Override + public void setComment(String comment) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException( + sm.getString("applicationSessionCookieConfig.ise", "comment", context.getPath())); + } + } + + @Override + public void setDomain(String domain) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException( + sm.getString("applicationSessionCookieConfig.ise", "domain name", context.getPath())); + } + setAttribute(Constants.COOKIE_DOMAIN_ATTR, domain); + } + + @Override + public void setHttpOnly(boolean httpOnly) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException( + sm.getString("applicationSessionCookieConfig.ise", "HttpOnly", context.getPath())); + } + setAttribute(Constants.COOKIE_HTTP_ONLY_ATTR, Boolean.toString(httpOnly)); + } + + @Override + public void setMaxAge(int maxAge) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException( + sm.getString("applicationSessionCookieConfig.ise", "max age", context.getPath())); + } + setAttribute(Constants.COOKIE_MAX_AGE_ATTR, Integer.toString(maxAge)); + } + + @Override + public void setName(String name) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException( + sm.getString("applicationSessionCookieConfig.ise", "name", context.getPath())); + } + this.name = name; + } + + @Override + public void setPath(String path) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException( + sm.getString("applicationSessionCookieConfig.ise", "path", context.getPath())); + } + setAttribute(Constants.COOKIE_PATH_ATTR, path); + } + + @Override + public void setSecure(boolean secure) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException( + sm.getString("applicationSessionCookieConfig.ise", "secure", context.getPath())); + } + setAttribute(Constants.COOKIE_SECURE_ATTR, Boolean.toString(secure)); + } + + + @Override + public void setAttribute(String name, String value) { + if (!context.getState().equals(LifecycleState.STARTING_PREP)) { + throw new IllegalStateException( + sm.getString("applicationSessionCookieConfig.ise", name, context.getPath())); + } + attributes.put(name, value); + } + + @Override + public String getAttribute(String name) { + return attributes.get(name); + } + + + @Override + public Map getAttributes() { + return Collections.unmodifiableMap(attributes); + } + + /** + * Creates a new session cookie for the given session ID + * + * @param context The Context for the web application + * @param sessionId The ID of the session for which the cookie will be created + * @param secure Should session cookie be configured as secure + * + * @return the cookie for the session + */ + public static Cookie createSessionCookie(Context context, String sessionId, boolean secure) { + + SessionCookieConfig scc = context.getServletContext().getSessionCookieConfig(); + + // NOTE: The priority order for session cookie configuration is: + // 1. Context level configuration + // 2. Values from SessionCookieConfig + // 3. Defaults + + Cookie cookie = new Cookie(SessionConfig.getSessionCookieName(context), sessionId); + + // Just apply the defaults. + cookie.setMaxAge(scc.getMaxAge()); + + if (context.getSessionCookieDomain() == null) { + // Avoid possible NPE + if (scc.getDomain() != null) { + cookie.setDomain(scc.getDomain()); + } + } else { + cookie.setDomain(context.getSessionCookieDomain()); + } + + // Always set secure if the request is secure + if (scc.isSecure() || secure) { + cookie.setSecure(true); + } + + // Always set httpOnly if the context is configured for that + if (scc.isHttpOnly() || context.getUseHttpOnly()) { + cookie.setHttpOnly(true); + } + + cookie.setAttribute(Constants.COOKIE_PARTITIONED_ATTR, + Boolean.toString(context.getUsePartitioned())); + + cookie.setPath(SessionConfig.getSessionCookiePath(context)); + + // Other attributes + for (Map.Entry attribute : scc.getAttributes().entrySet()) { + switch (attribute.getKey()) { + case Constants.COOKIE_COMMENT_ATTR: + case Constants.COOKIE_DOMAIN_ATTR: + case Constants.COOKIE_MAX_AGE_ATTR: + case Constants.COOKIE_PATH_ATTR: + case Constants.COOKIE_SECURE_ATTR: + case Constants.COOKIE_HTTP_ONLY_ATTR: + // Handled above so NO-OP + break; + default: { + cookie.setAttribute(attribute.getKey(), attribute.getValue()); + } + } + } + + return cookie; + } +} diff --git a/java/org/apache/catalina/core/AprLifecycleListener.java b/java/org/apache/catalina/core/AprLifecycleListener.java new file mode 100644 index 0000000..f1621ff --- /dev/null +++ b/java/org/apache/catalina/core/AprLifecycleListener.java @@ -0,0 +1,423 @@ +/* + * 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.catalina.core; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jni.Library; +import org.apache.tomcat.jni.LibraryNotFoundError; +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implementation of LifecycleListener that will init and and destroy APR. + *

    + * This listener must only be nested within {@link Server} elements. + *

    + * Note: If you are running Tomcat in an embedded fashion and have more than one Server instance per + * JVM, this listener must not be added to the {@code Server} instances, but handled outside by the calling + * code which is bootstrapping the embedded Tomcat instances. Not doing so will lead to JVM crashes. + * + * @since 4.1 + */ +public class AprLifecycleListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(AprLifecycleListener.class); + + /** + * Info messages during init() are cached until Lifecycle.BEFORE_INIT_EVENT so that, in normal (non-error) cases, + * init() related log messages appear at the expected point in the lifecycle. + */ + private static final List initInfoLogMessages = new ArrayList<>(3); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(AprLifecycleListener.class); + + + // ---------------------------------------------- Constants + + protected static final int TCN_REQUIRED_MAJOR = 1; + protected static final int TCN_REQUIRED_MINOR = 2; + protected static final int TCN_REQUIRED_PATCH = 34; + protected static final int TCN_RECOMMENDED_MAJOR = 2; + protected static final int TCN_RECOMMENDED_MINOR = 0; + protected static final int TCN_RECOMMENDED_PV = 5; + + + // ---------------------------------------------- Properties + + private static int tcnMajor = 0; + private static int tcnMinor = 0; + private static int tcnPatch = 0; + private static int tcnVersion = 0; + + protected static String SSLEngine = "on"; // default on + protected static String FIPSMode = "off"; // default off, valid only when SSLEngine="on" + protected static String SSLRandomSeed = "builtin"; + protected static boolean sslInitialized = false; + protected static boolean fipsModeActive = false; + + /** + * The "FIPS mode" level that we use as the argument to OpenSSL method FIPS_mode_set() to enable FIPS + * mode and that we expect as the return value of FIPS_mode() when FIPS mode is enabled. + *

    + * In the future the OpenSSL library might grow support for different non-zero "FIPS" modes that specify different + * allowed subsets of ciphers or whatever, but nowadays only "1" is the supported value. + *

    + * + * @see OpenSSL method FIPS_mode_set() + * @see OpenSSL method FIPS_mode() + */ + private static final int FIPS_ON = 1; + + private static final int FIPS_OFF = 0; + + protected static final Object lock = new Object(); + + public static boolean isAprAvailable() { + // https://bz.apache.org/bugzilla/show_bug.cgi?id=48613 + if (AprStatus.isInstanceCreated()) { + synchronized (lock) { + init(); + } + } + return AprStatus.isAprAvailable(); + } + + public AprLifecycleListener() { + AprStatus.setInstanceCreated(true); + } + + // ---------------------------------------------- LifecycleListener Methods + + /** + * Primary entry point for startup and shutdown events. + * + * @param event The event that has occurred + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + + if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) { + synchronized (lock) { + if (!(event.getLifecycle() instanceof Server)) { + log.warn(sm.getString("listener.notServer", event.getLifecycle().getClass().getSimpleName())); + } + init(); + for (String msg : initInfoLogMessages) { + log.info(msg); + } + initInfoLogMessages.clear(); + if (AprStatus.isAprAvailable()) { + try { + initializeSSL(); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("aprListener.sslInit"), t); + } + } + // Failure to initialize FIPS mode is fatal + if (!(null == FIPSMode || "off".equalsIgnoreCase(FIPSMode)) && !isFIPSModeActive()) { + String errorMessage = sm.getString("aprListener.initializeFIPSFailed"); + Error e = new Error(errorMessage); + // Log here, because thrown error might be not logged + log.fatal(errorMessage, e); + throw e; + } + } + } else if (Lifecycle.AFTER_DESTROY_EVENT.equals(event.getType())) { + synchronized (lock) { + if (!AprStatus.isAprAvailable()) { + return; + } + try { + terminateAPR(); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + log.info(sm.getString("aprListener.aprDestroy")); + } + } + } + + } + + private static void terminateAPR() { + AprStatus.setAprInitialized(false); + AprStatus.setAprAvailable(false); + fipsModeActive = false; + sslInitialized = false; // terminate() will clean the pool + Library.terminate(); + } + + private static void init() { + int rqver = TCN_REQUIRED_MAJOR * 1000 + TCN_REQUIRED_MINOR * 100 + TCN_REQUIRED_PATCH; + int rcver = TCN_RECOMMENDED_MAJOR * 1000 + TCN_RECOMMENDED_MINOR * 100 + TCN_RECOMMENDED_PV; + + if (AprStatus.isAprInitialized()) { + return; + } + AprStatus.setAprInitialized(true); + + try { + Library.initialize(null); + tcnMajor = Library.TCN_MAJOR_VERSION; + tcnMinor = Library.TCN_MINOR_VERSION; + tcnPatch = Library.TCN_PATCH_VERSION; + tcnVersion = tcnMajor * 1000 + tcnMinor * 100 + tcnPatch; + } catch (LibraryNotFoundError lnfe) { + // Library not on path + if (log.isDebugEnabled()) { + log.debug(sm.getString("aprListener.aprInitDebug", lnfe.getLibraryNames(), + System.getProperty("java.library.path"), lnfe.getMessage()), lnfe); + } + initInfoLogMessages.add(sm.getString("aprListener.aprInit", System.getProperty("java.library.path"))); + return; + } catch (Throwable t) { + // Library present but failed to load + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("aprListener.aprInitError", t.getMessage()), t); + return; + } + if (tcnMajor > 1 && "off".equalsIgnoreCase(SSLEngine)) { + log.error(sm.getString("aprListener.sslRequired", SSLEngine, Library.versionString())); + try { + // Tomcat Native 2.x onwards requires SSL + terminateAPR(); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + return; + } + if (tcnVersion < rqver) { + log.error(sm.getString("aprListener.tcnInvalid", Library.versionString(), + TCN_REQUIRED_MAJOR + "." + TCN_REQUIRED_MINOR + "." + TCN_REQUIRED_PATCH)); + try { + // Terminate the APR in case the version + // is below required. + terminateAPR(); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + return; + } + if (tcnVersion < rcver) { + initInfoLogMessages.add(sm.getString("aprListener.tcnVersion", Library.versionString(), + TCN_RECOMMENDED_MAJOR + "." + TCN_RECOMMENDED_MINOR + "." + TCN_RECOMMENDED_PV)); + } + + initInfoLogMessages + .add(sm.getString("aprListener.tcnValid", Library.versionString(), Library.aprVersionString())); + + AprStatus.setAprAvailable(true); + } + + private static void initializeSSL() throws Exception { + + if ("off".equalsIgnoreCase(SSLEngine)) { + return; + } + if (sslInitialized) { + // Only once per VM + return; + } + + sslInitialized = true; + + String methodName = "randSet"; + Class paramTypes[] = new Class[1]; + paramTypes[0] = String.class; + Object paramValues[] = new Object[1]; + paramValues[0] = SSLRandomSeed; + Class clazz = Class.forName("org.apache.tomcat.jni.SSL"); + Method method = clazz.getMethod(methodName, paramTypes); + method.invoke(null, paramValues); + + + methodName = "initialize"; + paramValues[0] = "on".equalsIgnoreCase(SSLEngine) ? null : SSLEngine; + method = clazz.getMethod(methodName, paramTypes); + method.invoke(null, paramValues); + + // OpenSSL 3 onwards uses providers + boolean usingProviders = tcnMajor > 1 || (tcnVersion > 1233 && (SSL.version() & 0xF0000000L) > 0x20000000); + + // Tomcat Native 1.x built with OpenSSL 1.x without explicitly enabling + // FIPS and Tomcat Native < 1.2.34 built with OpenSSL 3.x will fail if + // any calls are made to SSL.fipsModeGet or SSL.fipsModeSet + if (usingProviders || !(null == FIPSMode || "off".equalsIgnoreCase(FIPSMode))) { + fipsModeActive = false; + final boolean enterFipsMode; + int fipsModeState = SSL.fipsModeGet(); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("aprListener.currentFIPSMode", Integer.valueOf(fipsModeState))); + } + + if (null == FIPSMode || "off".equalsIgnoreCase(FIPSMode)) { + if (fipsModeState == FIPS_ON) { + fipsModeActive = true; + } + enterFipsMode = false; + } else if ("on".equalsIgnoreCase(FIPSMode)) { + if (fipsModeState == FIPS_ON) { + if (!usingProviders) { + log.info(sm.getString("aprListener.skipFIPSInitialization")); + } + fipsModeActive = true; + enterFipsMode = false; + } else { + if (usingProviders) { + throw new IllegalStateException(sm.getString("aprListener.FIPSProviderNotDefault", FIPSMode)); + } else { + enterFipsMode = true; + } + } + } else if ("require".equalsIgnoreCase(FIPSMode)) { + if (fipsModeState == FIPS_ON) { + fipsModeActive = true; + enterFipsMode = false; + } else { + if (usingProviders) { + throw new IllegalStateException(sm.getString("aprListener.FIPSProviderNotDefault", FIPSMode)); + } else { + throw new IllegalStateException(sm.getString("aprListener.requireNotInFIPSMode")); + } + } + } else if ("enter".equalsIgnoreCase(FIPSMode)) { + if (fipsModeState == FIPS_OFF) { + if (usingProviders) { + throw new IllegalStateException(sm.getString("aprListener.FIPSProviderNotDefault", FIPSMode)); + } else { + enterFipsMode = true; + } + } else { + if (usingProviders) { + fipsModeActive = true; + enterFipsMode = false; + } else { + throw new IllegalStateException( + sm.getString("aprListener.enterAlreadyInFIPSMode", Integer.valueOf(fipsModeState))); + } + } + } else { + throw new IllegalArgumentException(sm.getString("aprListener.wrongFIPSMode", FIPSMode)); + } + + if (enterFipsMode) { + log.info(sm.getString("aprListener.initializingFIPS")); + + fipsModeState = SSL.fipsModeSet(FIPS_ON); + if (fipsModeState != FIPS_ON) { + // This case should be handled by the native method, + // but we'll make absolutely sure, here. + String message = sm.getString("aprListener.initializeFIPSFailed"); + log.error(message); + throw new IllegalStateException(message); + } + + fipsModeActive = true; + log.info(sm.getString("aprListener.initializeFIPSSuccess")); + } + + if (usingProviders && fipsModeActive) { + log.info(sm.getString("aprListener.usingFIPSProvider")); + } + } + + log.info(sm.getString("aprListener.initializedOpenSSL", SSL.versionString())); + } + + public String getSSLEngine() { + return SSLEngine; + } + + public void setSSLEngine(String SSLEngine) { + if (!SSLEngine.equals(AprLifecycleListener.SSLEngine)) { + // Ensure that the SSLEngine is consistent with that used for SSL init + if (sslInitialized) { + throw new IllegalStateException(sm.getString("aprListener.tooLateForSSLEngine")); + } + + AprLifecycleListener.SSLEngine = SSLEngine; + } + } + + public String getSSLRandomSeed() { + return SSLRandomSeed; + } + + public void setSSLRandomSeed(String SSLRandomSeed) { + if (!SSLRandomSeed.equals(AprLifecycleListener.SSLRandomSeed)) { + // Ensure that the random seed is consistent with that used for SSL init + if (sslInitialized) { + throw new IllegalStateException(sm.getString("aprListener.tooLateForSSLRandomSeed")); + } + + AprLifecycleListener.SSLRandomSeed = SSLRandomSeed; + } + } + + public String getFIPSMode() { + return FIPSMode; + } + + public void setFIPSMode(String FIPSMode) { + if (!FIPSMode.equals(AprLifecycleListener.FIPSMode)) { + // Ensure that the FIPS mode is consistent with that used for SSL init + if (sslInitialized) { + throw new IllegalStateException(sm.getString("aprListener.tooLateForFIPSMode")); + } + + AprLifecycleListener.FIPSMode = FIPSMode; + } + } + + public boolean isFIPSModeActive() { + return fipsModeActive; + } + + public void setUseOpenSSL(boolean useOpenSSL) { + if (useOpenSSL != AprStatus.getUseOpenSSL()) { + AprStatus.setUseOpenSSL(useOpenSSL); + } + } + + public static boolean getUseOpenSSL() { + return AprStatus.getUseOpenSSL(); + } + + public static boolean isInstanceCreated() { + return AprStatus.isInstanceCreated(); + } + +} diff --git a/java/org/apache/catalina/core/AprStatus.java b/java/org/apache/catalina/core/AprStatus.java new file mode 100644 index 0000000..5374d07 --- /dev/null +++ b/java/org/apache/catalina/core/AprStatus.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.core; + +/** + * Holds APR status without the need to load other classes. + */ +public class AprStatus { + private static volatile boolean aprInitialized = false; + private static volatile boolean aprAvailable = false; + private static volatile boolean useOpenSSL = true; + private static volatile boolean instanceCreated = false; + + + public static boolean isAprInitialized() { + return aprInitialized; + } + + public static boolean isAprAvailable() { + return aprAvailable; + } + + public static boolean getUseOpenSSL() { + return useOpenSSL; + } + + public static boolean isInstanceCreated() { + return instanceCreated; + } + + public static void setAprInitialized(boolean aprInitialized) { + AprStatus.aprInitialized = aprInitialized; + } + + public static void setAprAvailable(boolean aprAvailable) { + AprStatus.aprAvailable = aprAvailable; + } + + public static void setUseOpenSSL(boolean useOpenSSL) { + AprStatus.useOpenSSL = useOpenSSL; + } + + public static void setInstanceCreated(boolean instanceCreated) { + AprStatus.instanceCreated = instanceCreated; + } +} diff --git a/java/org/apache/catalina/core/AsyncContextImpl.java b/java/org/apache/catalina/core/AsyncContextImpl.java new file mode 100644 index 0000000..210d29a --- /dev/null +++ b/java/org/apache/catalina/core/AsyncContextImpl.java @@ -0,0 +1,592 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.naming.NamingException; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.AsyncDispatcher; +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.coyote.ActionCode; +import org.apache.coyote.AsyncContextCallback; +import org.apache.coyote.RequestInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.res.StringManager; + +public class AsyncContextImpl implements AsyncContext, AsyncContextCallback { + + private static final Log log = LogFactory.getLog(AsyncContextImpl.class); + + protected static final StringManager sm = StringManager.getManager(AsyncContextImpl.class); + + /* + * When a request uses a sequence of multiple start(); dispatch() with non-container threads it is possible for a + * previous dispatch() to interfere with a following start(). This lock prevents that from happening. It is a + * dedicated object as user code may lock on the AsyncContext so if container code also locks on that object + * deadlocks may occur. + */ + private final Object asyncContextLock = new Object(); + + private volatile ServletRequest servletRequest = null; + private volatile ServletResponse servletResponse = null; + private final List listeners = new ArrayList<>(); + private boolean hasOriginalRequestAndResponse = true; + private volatile Runnable dispatch = null; + private Context context = null; + // Default of 30000 (30s) is set by the connector + private long timeout = -1; + private AsyncEvent event = null; + private volatile Request request; + private final AtomicBoolean hasErrorProcessingStarted = new AtomicBoolean(false); + private final AtomicBoolean hasOnErrorReturned = new AtomicBoolean(false); + + public AsyncContextImpl(Request request) { + if (log.isTraceEnabled()) { + logDebug("Constructor"); + } + this.request = request; + } + + @Override + public void complete() { + if (log.isTraceEnabled()) { + logDebug("complete "); + } + check(); + request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null); + } + + @Override + public void fireOnComplete() { + if (log.isTraceEnabled()) { + log.trace(sm.getString("asyncContextImpl.fireOnComplete")); + } + List listenersCopy = new ArrayList<>(listeners); + + ClassLoader oldCL = context.bind(Globals.IS_SECURITY_ENABLED, null); + try { + for (AsyncListenerWrapper listener : listenersCopy) { + try { + listener.fireOnComplete(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("asyncContextImpl.onCompleteError", listener.getClass().getName()), t); + } + } + } finally { + context.fireRequestDestroyEvent(request.getRequest()); + clearServletRequestResponse(); + context.unbind(Globals.IS_SECURITY_ENABLED, oldCL); + } + } + + + public boolean timeout() { + AtomicBoolean result = new AtomicBoolean(); + request.getCoyoteRequest().action(ActionCode.ASYNC_TIMEOUT, result); + // Avoids NPEs during shutdown. A call to recycle will null this field. + Context context = this.context; + + if (result.get()) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("asyncContextImpl.fireOnTimeout")); + } + ClassLoader oldCL = context.bind(false, null); + try { + List listenersCopy = new ArrayList<>(listeners); + for (AsyncListenerWrapper listener : listenersCopy) { + try { + listener.fireOnTimeout(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("asyncContextImpl.onTimeoutError", listener.getClass().getName()), t); + } + } + request.getCoyoteRequest().action(ActionCode.ASYNC_IS_TIMINGOUT, result); + } finally { + context.unbind(false, oldCL); + } + } + return !result.get(); + } + + @Override + public void dispatch() { + check(); + String path; + String cpath; + ServletRequest servletRequest = getRequest(); + if (servletRequest instanceof HttpServletRequest) { + HttpServletRequest sr = (HttpServletRequest) servletRequest; + path = sr.getRequestURI(); + cpath = sr.getContextPath(); + } else { + path = request.getRequestURI(); + cpath = request.getContextPath(); + } + if (cpath.length() > 1) { + path = path.substring(cpath.length()); + } + if (!context.getDispatchersUseEncodedPaths()) { + path = UDecoder.URLDecode(path, StandardCharsets.UTF_8); + } + dispatch(path); + } + + @Override + public void dispatch(String path) { + check(); + dispatch(getRequest().getServletContext(), path); + } + + @Override + public void dispatch(ServletContext servletContext, String path) { + synchronized (asyncContextLock) { + if (log.isTraceEnabled()) { + logDebug("dispatch "); + } + check(); + if (dispatch != null) { + throw new IllegalStateException(sm.getString("asyncContextImpl.dispatchingStarted")); + } + if (request.getAttribute(ASYNC_REQUEST_URI) == null) { + request.setAttribute(ASYNC_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ASYNC_CONTEXT_PATH, request.getContextPath()); + request.setAttribute(ASYNC_SERVLET_PATH, request.getServletPath()); + request.setAttribute(ASYNC_PATH_INFO, request.getPathInfo()); + request.setAttribute(ASYNC_QUERY_STRING, request.getQueryString()); + } + final RequestDispatcher requestDispatcher = servletContext.getRequestDispatcher(path); + if (!(requestDispatcher instanceof AsyncDispatcher)) { + throw new UnsupportedOperationException(sm.getString("asyncContextImpl.noAsyncDispatcher")); + } + final AsyncDispatcher applicationDispatcher = (AsyncDispatcher) requestDispatcher; + final ServletRequest servletRequest = getRequest(); + final ServletResponse servletResponse = getResponse(); + this.dispatch = new AsyncRunnable(request, applicationDispatcher, servletRequest, servletResponse); + this.request.getCoyoteRequest().action(ActionCode.ASYNC_DISPATCH, null); + clearServletRequestResponse(); + } + } + + @Override + public ServletRequest getRequest() { + check(); + if (servletRequest == null) { + throw new IllegalStateException(sm.getString("asyncContextImpl.request.ise")); + } + return servletRequest; + } + + @Override + public ServletResponse getResponse() { + check(); + if (servletResponse == null) { + throw new IllegalStateException(sm.getString("asyncContextImpl.response.ise")); + } + return servletResponse; + } + + @Override + public void start(final Runnable run) { + if (log.isTraceEnabled()) { + logDebug("start "); + } + check(); + Runnable wrapper = new RunnableWrapper(run, context, this.request.getCoyoteRequest()); + this.request.getCoyoteRequest().action(ActionCode.ASYNC_RUN, wrapper); + } + + @Override + public void addListener(AsyncListener listener) { + check(); + AsyncListenerWrapper wrapper = new AsyncListenerWrapper(); + wrapper.setListener(listener); + listeners.add(wrapper); + } + + @Override + public void addListener(AsyncListener listener, ServletRequest servletRequest, ServletResponse servletResponse) { + check(); + AsyncListenerWrapper wrapper = new AsyncListenerWrapper(); + wrapper.setListener(listener); + wrapper.setServletRequest(servletRequest); + wrapper.setServletResponse(servletResponse); + listeners.add(wrapper); + } + + @SuppressWarnings("unchecked") + @Override + public T createListener(Class clazz) throws ServletException { + check(); + T listener = null; + try { + listener = (T) context.getInstanceManager().newInstance(clazz.getName(), clazz.getClassLoader()); + } catch (ReflectiveOperationException | NamingException e) { + ServletException se = new ServletException(e); + throw se; + } catch (Exception e) { + ExceptionUtils.handleThrowable(e.getCause()); + ServletException se = new ServletException(e); + throw se; + } + return listener; + } + + public void recycle() { + if (log.isTraceEnabled()) { + logDebug("recycle "); + } + context = null; + dispatch = null; + event = null; + hasOriginalRequestAndResponse = true; + listeners.clear(); + request = null; + clearServletRequestResponse(); + timeout = -1; + hasErrorProcessingStarted.set(false); + hasOnErrorReturned.set(false); + } + + private void clearServletRequestResponse() { + servletRequest = null; + servletResponse = null; + } + + public boolean isStarted() { + AtomicBoolean result = new AtomicBoolean(false); + Request request = this.request; + check(); + request.getCoyoteRequest().action(ActionCode.ASYNC_IS_STARTED, result); + return result.get(); + } + + public void setStarted(Context context, ServletRequest request, ServletResponse response, + boolean originalRequestResponse) { + + synchronized (asyncContextLock) { + this.request.getCoyoteRequest().action(ActionCode.ASYNC_START, this); + + this.context = context; + context.incrementInProgressAsyncCount(); + this.servletRequest = request; + this.servletResponse = response; + this.hasOriginalRequestAndResponse = originalRequestResponse; + this.event = new AsyncEvent(this, request, response); + + List listenersCopy = new ArrayList<>(listeners); + listeners.clear(); + if (log.isTraceEnabled()) { + log.trace(sm.getString("asyncContextImpl.fireOnStartAsync")); + } + for (AsyncListenerWrapper listener : listenersCopy) { + try { + listener.fireOnStartAsync(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("asyncContextImpl.onStartAsyncError", listener.getClass().getName()), t); + } + } + } + } + + @Override + public boolean hasOriginalRequestAndResponse() { + check(); + return hasOriginalRequestAndResponse; + } + + protected void doInternalDispatch() throws ServletException, IOException { + if (log.isTraceEnabled()) { + logDebug("intDispatch"); + } + try { + Runnable runnable = dispatch; + dispatch = null; + runnable.run(); + if (!request.isAsync()) { + fireOnComplete(); + } + } catch (RuntimeException x) { + // doInternalComplete(true); + if (x.getCause() instanceof ServletException) { + throw (ServletException) x.getCause(); + } + if (x.getCause() instanceof IOException) { + throw (IOException) x.getCause(); + } + throw new ServletException(x); + } + } + + + @Override + public long getTimeout() { + check(); + return timeout; + } + + + @Override + public void setTimeout(long timeout) { + check(); + this.timeout = timeout; + request.getCoyoteRequest().action(ActionCode.ASYNC_SETTIMEOUT, Long.valueOf(timeout)); + } + + + @Override + public boolean isAvailable() { + Context context = this.context; + if (context == null) { + return false; + } + return context.getState().isAvailable(); + } + + + public void setErrorState(Throwable t, boolean fireOnError) { + if (!hasErrorProcessingStarted.compareAndSet(false, true)) { + // Skip duplicate error processing + return; + } + if (t != null) { + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + } + request.getCoyoteRequest().action(ActionCode.ASYNC_ERROR, null); + + if (fireOnError) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("asyncContextImpl.fireOnError")); + } + AsyncEvent errorEvent = + new AsyncEvent(event.getAsyncContext(), event.getSuppliedRequest(), event.getSuppliedResponse(), t); + List listenersCopy = new ArrayList<>(listeners); + for (AsyncListenerWrapper listener : listenersCopy) { + try { + listener.fireOnError(errorEvent); + } catch (Throwable t2) { + ExceptionUtils.handleThrowable(t2); + log.warn(sm.getString("asyncContextImpl.onErrorError", listener.getClass().getName()), t2); + } finally { + hasOnErrorReturned.set(true); + } + } + } + + + AtomicBoolean result = new AtomicBoolean(); + request.getCoyoteRequest().action(ActionCode.ASYNC_IS_ERROR, result); + if (result.get()) { + // No listener called dispatch() or complete(). This is an error. + // SRV.2.3.3.3 (search for "error dispatch") + // Take a local copy to avoid threading issues if another thread + // clears this (can happen during error handling with non-container + // threads) + ServletResponse servletResponse = this.servletResponse; + if (servletResponse instanceof HttpServletResponse) { + ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + Host host = (Host) context.getParent(); + Valve stdHostValve = host.getPipeline().getBasic(); + if (stdHostValve instanceof StandardHostValve) { + ((StandardHostValve) stdHostValve).throwable(request, request.getResponse(), t); + } + + request.getCoyoteRequest().action(ActionCode.ASYNC_IS_ERROR, result); + if (result.get()) { + // Still in the error state. The error page did not call + // complete() or dispatch(). Complete the async processing. + complete(); + } + } + } + + + @Override + public void incrementInProgressAsyncCount() { + context.incrementInProgressAsyncCount(); + } + + + @Override + public void decrementInProgressAsyncCount() { + context.decrementInProgressAsyncCount(); + } + + + private void logDebug(String method) { + String rHashCode; + String crHashCode; + String rpHashCode; + String stage; + StringBuilder uri = new StringBuilder(); + if (request == null) { + rHashCode = "null"; + crHashCode = "null"; + rpHashCode = "null"; + stage = "-"; + uri.append("N/A"); + } else { + rHashCode = Integer.toHexString(request.hashCode()); + org.apache.coyote.Request coyoteRequest = request.getCoyoteRequest(); + if (coyoteRequest == null) { + crHashCode = "null"; + rpHashCode = "null"; + stage = "-"; + } else { + crHashCode = Integer.toHexString(coyoteRequest.hashCode()); + RequestInfo rp = coyoteRequest.getRequestProcessor(); + if (rp == null) { + rpHashCode = "null"; + stage = "-"; + } else { + rpHashCode = Integer.toHexString(rp.hashCode()); + stage = Integer.toString(rp.getStage()); + } + } + uri.append(request.getRequestURI()); + if (request.getQueryString() != null) { + uri.append('?'); + uri.append(request.getQueryString()); + } + } + String threadName = Thread.currentThread().getName(); + int len = threadName.length(); + if (len > 20) { + threadName = threadName.substring(len - 20, len); + } + String msg = String.format( + "Req: %1$8s CReq: %2$8s RP: %3$8s Stage: %4$s " + + "Thread: %5$20s State: %6$20s Method: %7$11s URI: %8$s", + rHashCode, crHashCode, rpHashCode, stage, threadName, "N/A", method, uri); + if (log.isTraceEnabled()) { + log.trace(msg, new DebugException()); + } else { + log.debug(msg); + } + } + + private void check() { + Request request = this.request; + if (request == null) { + // AsyncContext has been recycled and should not be being used + throw new IllegalStateException(sm.getString("asyncContextImpl.requestEnded")); + } + if (hasOnErrorReturned.get() && !request.getCoyoteRequest().isRequestThread()) { + /* + * Non-container thread is trying to use the AsyncContext after an error has occurred and the call to + * AsyncListener.onError() has returned. At this point, the non-container thread should not be trying to use + * the AsyncContext due to possible race conditions. + */ + throw new IllegalStateException(sm.getString("asyncContextImpl.afterOnError")); + } + } + + private static class DebugException extends Exception { + private static final long serialVersionUID = 1L; + } + + private static class RunnableWrapper implements Runnable { + + private final Runnable wrapped; + private final Context context; + private final org.apache.coyote.Request coyoteRequest; + + RunnableWrapper(Runnable wrapped, Context ctxt, org.apache.coyote.Request coyoteRequest) { + this.wrapped = wrapped; + this.context = ctxt; + this.coyoteRequest = coyoteRequest; + } + + @SuppressWarnings("deprecation") + @Override + public void run() { + ClassLoader oldCL = context.bind(Globals.IS_SECURITY_ENABLED, null); + try { + wrapped.run(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + context.getLogger().error(sm.getString("asyncContextImpl.asyncRunnableError"), t); + coyoteRequest.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + org.apache.coyote.Response coyoteResponse = coyoteRequest.getResponse(); + coyoteResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + coyoteResponse.setError(); + } finally { + context.unbind(Globals.IS_SECURITY_ENABLED, oldCL); + } + + // Since this runnable is not executing as a result of a socket + // event, we need to ensure that any registered dispatches are + // executed. + coyoteRequest.action(ActionCode.DISPATCH_EXECUTE, null); + } + } + + + private static class AsyncRunnable implements Runnable { + + private final AsyncDispatcher applicationDispatcher; + private final Request request; + private final ServletRequest servletRequest; + private final ServletResponse servletResponse; + + AsyncRunnable(Request request, AsyncDispatcher applicationDispatcher, ServletRequest servletRequest, + ServletResponse servletResponse) { + this.request = request; + this.applicationDispatcher = applicationDispatcher; + this.servletRequest = servletRequest; + this.servletResponse = servletResponse; + } + + @Override + public void run() { + request.getCoyoteRequest().action(ActionCode.ASYNC_DISPATCHED, null); + try { + applicationDispatcher.dispatch(servletRequest, servletResponse); + } catch (Exception e) { + throw new RuntimeException(sm.getString("asyncContextImpl.asyncDispatchError"), e); + } + } + + } +} diff --git a/java/org/apache/catalina/core/AsyncListenerWrapper.java b/java/org/apache/catalina/core/AsyncListenerWrapper.java new file mode 100644 index 0000000..41fbf57 --- /dev/null +++ b/java/org/apache/catalina/core/AsyncListenerWrapper.java @@ -0,0 +1,80 @@ +/* + * 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.catalina.core; + +import java.io.IOException; + +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +public class AsyncListenerWrapper { + + private AsyncListener listener = null; + private ServletRequest servletRequest = null; + private ServletResponse servletResponse = null; + + + public void fireOnStartAsync(AsyncEvent event) throws IOException { + listener.onStartAsync(customizeEvent(event)); + } + + + public void fireOnComplete(AsyncEvent event) throws IOException { + listener.onComplete(customizeEvent(event)); + } + + + public void fireOnTimeout(AsyncEvent event) throws IOException { + listener.onTimeout(customizeEvent(event)); + } + + + public void fireOnError(AsyncEvent event) throws IOException { + listener.onError(customizeEvent(event)); + } + + + public AsyncListener getListener() { + return listener; + } + + + public void setListener(AsyncListener listener) { + this.listener = listener; + } + + + public void setServletRequest(ServletRequest servletRequest) { + this.servletRequest = servletRequest; + } + + + public void setServletResponse(ServletResponse servletResponse) { + this.servletResponse = servletResponse; + } + + + private AsyncEvent customizeEvent(AsyncEvent event) { + if (servletRequest != null && servletResponse != null) { + return new AsyncEvent(event.getAsyncContext(), servletRequest, servletResponse, event.getThrowable()); + } else { + return event; + } + } +} diff --git a/java/org/apache/catalina/core/Constants.java b/java/org/apache/catalina/core/Constants.java new file mode 100644 index 0000000..3a2bd5b --- /dev/null +++ b/java/org/apache/catalina/core/Constants.java @@ -0,0 +1,25 @@ +/* + * 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.catalina.core; + +public class Constants { + + public static final int MAJOR_VERSION = 6; + public static final int MINOR_VERSION = 0; + + public static final String JSP_SERVLET_CLASS = "org.apache.jasper.servlet.JspServlet"; +} diff --git a/java/org/apache/catalina/core/ContainerBase.java b/java/org/apache/catalina/core/ContainerBase.java new file mode 100644 index 0000000..43201c1 --- /dev/null +++ b/java/org/apache/catalina/core/ContainerBase.java @@ -0,0 +1,1367 @@ +/* + * 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.catalina.core; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.ObjectName; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Cluster; +import org.apache.catalina.Container; +import org.apache.catalina.ContainerEvent; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Loader; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Realm; +import org.apache.catalina.Server; +import org.apache.catalina.Valve; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.MultiThrowable; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.threads.InlineExecutorService; + + +/** + * Abstract implementation of the Container interface, providing common functionality required by nearly every + * implementation. Classes extending this base class must may implement a replacement for invoke(). + *

    + * All subclasses of this abstract base class will include support for a Pipeline object that defines the processing to + * be performed for each request received by the invoke() method of this class, utilizing the "Chain of + * Responsibility" design pattern. A subclass should encapsulate its own processing functionality as a + * Valve, and configure this Valve into the pipeline by calling setBasic(). + *

    + * This implementation fires property change events, per the JavaBeans design pattern, for changes in singleton + * properties. In addition, it fires the following ContainerEvent events to listeners who register + * themselves with addContainerListener(): + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    ContainerEvents fired by this implementation
    TypeDataDescription
    addChildContainerChild container added to this Container.
    {@link #getPipeline() pipeline}.addValveValveValve added to this Container.
    removeChildContainerChild container removed from this Container.
    {@link #getPipeline() pipeline}.removeValveValveValve removed from this Container.
    startnullContainer was started.
    stopnullContainer was stopped.
    + * Subclasses that fire additional events should document them in the class comments of the implementation class. + * + * @author Craig R. McClanahan + */ +public abstract class ContainerBase extends LifecycleMBeanBase implements Container { + + private static final Log log = LogFactory.getLog(ContainerBase.class); + + /** + * Perform addChild with the permissions of this class. addChild can be called with the XML parser on the stack, + * this allows the XML parser to have fewer privileges than Tomcat. + */ + protected class PrivilegedAddChild implements PrivilegedAction { + + private final Container child; + + PrivilegedAddChild(Container child) { + this.child = child; + } + + @Override + public Void run() { + addChildInternal(child); + return null; + } + + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The child Containers belonging to this Container, keyed by name. + */ + protected final HashMap children = new HashMap<>(); + private final ReadWriteLock childrenLock = new ReentrantReadWriteLock(); + + + /** + * The processor delay for this component. + */ + protected int backgroundProcessorDelay = -1; + + + /** + * The future allowing control of the background processor. + */ + protected ScheduledFuture backgroundProcessorFuture; + protected ScheduledFuture monitorFuture; + + /** + * The container event listeners for this Container. Implemented as a CopyOnWriteArrayList since listeners may + * invoke methods to add/remove themselves or other listeners and with a ReadWriteLock that would trigger a + * deadlock. + */ + protected final List listeners = new CopyOnWriteArrayList<>(); + + /** + * The Logger implementation with which this Container is associated. + */ + protected Log logger = null; + + + /** + * Associated logger name. + */ + protected String logName = null; + + + /** + * The cluster with which this Container is associated. + */ + protected Cluster cluster = null; + private final ReadWriteLock clusterLock = new ReentrantReadWriteLock(); + + + /** + * The human-readable name of this Container. + */ + protected String name = null; + + + /** + * The parent Container to which this Container is a child. + */ + protected Container parent = null; + + + /** + * The parent class loader to be configured when we install a Loader. + */ + protected ClassLoader parentClassLoader = null; + + + /** + * The Pipeline object with which this Container is associated. + */ + protected final Pipeline pipeline = new StandardPipeline(this); + + + /** + * The Realm with which this Container is associated. + */ + private volatile Realm realm = null; + + + /** + * Lock used to control access to the Realm. + */ + private final ReadWriteLock realmLock = new ReentrantReadWriteLock(); + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(ContainerBase.class); + + + /** + * Will children be started automatically when they are added. + */ + protected boolean startChildren = true; + + /** + * The property change support for this component. + */ + protected final PropertyChangeSupport support = new PropertyChangeSupport(this); + + + /** + * The access log to use for requests normally handled by this container that have been handled earlier in the + * processing chain. + */ + protected volatile AccessLog accessLog = null; + private volatile boolean accessLogScanComplete = false; + + + /** + * The number of threads available to process start and stop events for any children associated with this container. + */ + private int startStopThreads = 1; + protected ExecutorService startStopExecutor; + + + // ------------------------------------------------------------- Properties + + @Override + public int getStartStopThreads() { + return startStopThreads; + } + + @Override + public void setStartStopThreads(int startStopThreads) { + int oldStartStopThreads = this.startStopThreads; + this.startStopThreads = startStopThreads; + + // Use local copies to ensure thread safety + if (oldStartStopThreads != startStopThreads && startStopExecutor != null) { + reconfigureStartStopExecutor(getStartStopThreads()); + } + } + + + /** + * Get the delay between the invocation of the backgroundProcess method on this container and its children. Child + * containers will not be invoked if their delay value is not negative (which would mean they are using their own + * thread). Setting this to a positive value will cause a thread to be spawn. After waiting the specified amount of + * time, the thread will invoke the executePeriodic method on this container and all its children. + */ + @Override + public int getBackgroundProcessorDelay() { + return backgroundProcessorDelay; + } + + + /** + * Set the delay between the invocation of the execute method on this container and its children. + * + * @param delay The delay in seconds between the invocation of backgroundProcess methods + */ + @Override + public void setBackgroundProcessorDelay(int delay) { + backgroundProcessorDelay = delay; + } + + + /** + * Return the Logger for this Container. + */ + @Override + public Log getLogger() { + if (logger != null) { + return logger; + } + logger = LogFactory.getLog(getLogName()); + return logger; + } + + + /** + * @return the abbreviated name of this container for logging messages + */ + @Override + public String getLogName() { + + if (logName != null) { + return logName; + } + String loggerName = null; + Container current = this; + while (current != null) { + String name = current.getName(); + if ((name == null) || (name.equals(""))) { + name = "/"; + } else if (name.startsWith("##")) { + name = "/" + name; + } + loggerName = "[" + name + "]" + ((loggerName != null) ? ("." + loggerName) : ""); + current = current.getParent(); + } + logName = ContainerBase.class.getName() + "." + loggerName; + return logName; + + } + + + /** + * Return the Cluster with which this Container is associated. If there is no associated Cluster, return the Cluster + * associated with our parent Container (if any); otherwise return null. + */ + @Override + public Cluster getCluster() { + Lock readLock = clusterLock.readLock(); + readLock.lock(); + try { + if (cluster != null) { + return cluster; + } + + if (parent != null) { + return parent.getCluster(); + } + + return null; + } finally { + readLock.unlock(); + } + } + + + /* + * Provide access to just the cluster component attached to this container. + */ + protected Cluster getClusterInternal() { + Lock readLock = clusterLock.readLock(); + readLock.lock(); + try { + return cluster; + } finally { + readLock.unlock(); + } + } + + + /** + * Set the Cluster with which this Container is associated. + * + * @param cluster The newly associated Cluster + */ + @Override + public void setCluster(Cluster cluster) { + + Cluster oldCluster = null; + Lock writeLock = clusterLock.writeLock(); + writeLock.lock(); + try { + // Change components if necessary + oldCluster = this.cluster; + if (oldCluster == cluster) { + return; + } + this.cluster = cluster; + // Start the new component if necessary + if (cluster != null) { + cluster.setContainer(this); + } + } finally { + writeLock.unlock(); + } + + // Stop the old component if necessary + if (getState().isAvailable() && (oldCluster instanceof Lifecycle)) { + try { + ((Lifecycle) oldCluster).stop(); + } catch (LifecycleException e) { + log.error(sm.getString("containerBase.cluster.stop"), e); + } + } + + if (getState().isAvailable() && (cluster instanceof Lifecycle)) { + try { + ((Lifecycle) cluster).start(); + } catch (LifecycleException e) { + log.error(sm.getString("containerBase.cluster.start"), e); + } + } + + // Report this property change to interested listeners + support.firePropertyChange("cluster", oldCluster, cluster); + } + + + /** + * Return a name string (suitable for use by humans) that describes this Container. Within the set of child + * containers belonging to a particular parent, Container names must be unique. + */ + @Override + public String getName() { + return name; + } + + + /** + * Set a name string (suitable for use by humans) that describes this Container. Within the set of child containers + * belonging to a particular parent, Container names must be unique. + * + * @param name New name of this container + * + * @exception IllegalStateException if this Container has already been added to the children of a parent Container + * (after which the name may not be changed) + */ + @Override + public void setName(String name) { + if (name == null) { + throw new IllegalArgumentException(sm.getString("containerBase.nullName")); + } + String oldName = this.name; + this.name = name; + support.firePropertyChange("name", oldName, this.name); + } + + + /** + * Return if children of this container will be started automatically when they are added to this container. + * + * @return true if the children will be started + */ + public boolean getStartChildren() { + return startChildren; + } + + + /** + * Set if children of this container will be started automatically when they are added to this container. + * + * @param startChildren New value of the startChildren flag + */ + public void setStartChildren(boolean startChildren) { + + boolean oldStartChildren = this.startChildren; + this.startChildren = startChildren; + support.firePropertyChange("startChildren", oldStartChildren, this.startChildren); + } + + + /** + * Return the Container for which this Container is a child, if there is one. If there is no defined parent, return + * null. + */ + @Override + public Container getParent() { + return parent; + } + + + /** + * Set the parent Container to which this Container is being added as a child. This Container may refuse to become + * attached to the specified Container by throwing an exception. + * + * @param container Container to which this Container is being added as a child + * + * @exception IllegalArgumentException if this Container refuses to become attached to the specified Container + */ + @Override + public void setParent(Container container) { + + Container oldParent = this.parent; + this.parent = container; + support.firePropertyChange("parent", oldParent, this.parent); + + } + + + /** + * Return the parent class loader (if any) for this web application. This call is meaningful only + * after a Loader has been configured. + */ + @Override + public ClassLoader getParentClassLoader() { + if (parentClassLoader != null) { + return parentClassLoader; + } + if (parent != null) { + return parent.getParentClassLoader(); + } + return ClassLoader.getSystemClassLoader(); + } + + + /** + * Set the parent class loader (if any) for this web application. This call is meaningful only + * before a Loader has been configured, and the specified value (if non-null) should be passed as + * an argument to the class loader constructor. + * + * @param parent The new parent class loader + */ + @Override + public void setParentClassLoader(ClassLoader parent) { + ClassLoader oldParentClassLoader = this.parentClassLoader; + this.parentClassLoader = parent; + support.firePropertyChange("parentClassLoader", oldParentClassLoader, this.parentClassLoader); + + } + + + /** + * Return the Pipeline object that manages the Valves associated with this Container. + */ + @Override + public Pipeline getPipeline() { + return this.pipeline; + } + + + /** + * Return the Realm with which this Container is associated. If there is no associated Realm, return the Realm + * associated with our parent Container (if any); otherwise return null. + */ + @Override + public Realm getRealm() { + + Lock l = realmLock.readLock(); + l.lock(); + try { + if (realm != null) { + return realm; + } + if (parent != null) { + return parent.getRealm(); + } + return null; + } finally { + l.unlock(); + } + } + + + protected Realm getRealmInternal() { + Lock l = realmLock.readLock(); + l.lock(); + try { + return realm; + } finally { + l.unlock(); + } + } + + /** + * Set the Realm with which this Container is associated. + * + * @param realm The newly associated Realm + */ + @Override + public void setRealm(Realm realm) { + + Realm oldRealm = null; + Lock l = realmLock.writeLock(); + l.lock(); + try { + // Change components if necessary + oldRealm = this.realm; + if (oldRealm == realm) { + return; + } + this.realm = realm; + + // Start the new component if necessary + if (realm != null) { + realm.setContainer(this); + } + } finally { + l.unlock(); + } + + // Stop the old component if necessary + if (getState().isAvailable() && oldRealm instanceof Lifecycle) { + try { + ((Lifecycle) oldRealm).stop(); + } catch (LifecycleException e) { + log.error(sm.getString("containerBase.realm.stop"), e); + } + } + + if (getState().isAvailable() && realm instanceof Lifecycle) { + try { + ((Lifecycle) realm).start(); + } catch (LifecycleException e) { + log.error(sm.getString("containerBase.realm.start"), e); + } + } + + // Report this property change to interested listeners + support.firePropertyChange("realm", oldRealm, this.realm); + } + + + // ------------------------------------------------------ Container Methods + + + /** + * Add a new child Container to those associated with this Container, if supported. Prior to adding this Container + * to the set of children, the child's setParent() method must be called, with this Container as an + * argument. This method may thrown an IllegalArgumentException if this Container chooses not to be + * attached to the specified Container, in which case it is not added + * + * @param child New child Container to be added + * + * @exception IllegalArgumentException if this exception is thrown by the setParent() method of the + * child Container + * @exception IllegalArgumentException if the new child does not have a name unique from that of existing children + * of this Container + * @exception IllegalStateException if this Container does not support child Containers + */ + @Override + public void addChild(Container child) { + if (Globals.IS_SECURITY_ENABLED) { + PrivilegedAction dp = new PrivilegedAddChild(child); + AccessController.doPrivileged(dp); + } else { + addChildInternal(child); + } + } + + private void addChildInternal(Container child) { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("containerBase.child.add", child, this)); + } + + childrenLock.writeLock().lock(); + try { + if (children.get(child.getName()) != null) { + throw new IllegalArgumentException(sm.getString("containerBase.child.notUnique", child.getName())); + } + child.setParent(this); // May throw IAE + children.put(child.getName(), child); + } finally { + childrenLock.writeLock().unlock(); + } + + fireContainerEvent(ADD_CHILD_EVENT, child); + + // Start child + // Don't do this inside sync block - start can be a slow process and + // locking the children object can cause problems elsewhere + try { + if ((getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState())) && startChildren) { + child.start(); + } + } catch (LifecycleException e) { + throw new IllegalStateException(sm.getString("containerBase.child.start"), e); + } + } + + + /** + * Add a container event listener to this component. + * + * @param listener The listener to add + */ + @Override + public void addContainerListener(ContainerListener listener) { + listeners.add(listener); + } + + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + + /** + * Return the child Container, associated with this Container, with the specified name (if any); otherwise, return + * null + * + * @param name Name of the child Container to be retrieved + */ + @Override + public Container findChild(String name) { + if (name == null) { + return null; + } + childrenLock.readLock().lock(); + try { + return children.get(name); + } finally { + childrenLock.readLock().unlock(); + } + } + + + /** + * Return the set of children Containers associated with this Container. If this Container has no children, a + * zero-length array is returned. + */ + @Override + public Container[] findChildren() { + childrenLock.readLock().lock(); + try { + return children.values().toArray(new Container[0]); + } finally { + childrenLock.readLock().unlock(); + } + } + + + /** + * Return the set of container listeners associated with this Container. If this Container has no registered + * container listeners, a zero-length array is returned. + */ + @Override + public ContainerListener[] findContainerListeners() { + return listeners.toArray(new ContainerListener[0]); + } + + + /** + * Remove an existing child Container from association with this parent Container. + * + * @param child Existing child Container to be removed + */ + @Override + public void removeChild(Container child) { + + if (child == null) { + return; + } + + try { + if (child.getState().isAvailable()) { + child.stop(); + } + } catch (LifecycleException e) { + log.error(sm.getString("containerBase.child.stop"), e); + } + + boolean destroy = false; + try { + // child.destroy() may have already been called which would have + // triggered this call. If that is the case, no need to destroy the + // child again. + if (!LifecycleState.DESTROYING.equals(child.getState())) { + child.destroy(); + destroy = true; + } + } catch (LifecycleException e) { + log.error(sm.getString("containerBase.child.destroy"), e); + } + + if (!destroy) { + fireContainerEvent(REMOVE_CHILD_EVENT, child); + } + + childrenLock.writeLock().lock(); + try { + children.remove(child.getName()); + } finally { + childrenLock.writeLock().unlock(); + } + + } + + + /** + * Remove a container event listener from this component. + * + * @param listener The listener to remove + */ + @Override + public void removeContainerListener(ContainerListener listener) { + listeners.remove(listener); + } + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + + support.removePropertyChangeListener(listener); + + } + + + private void reconfigureStartStopExecutor(int threads) { + if (threads == 1) { + // Use a fake executor + if (!(startStopExecutor instanceof InlineExecutorService)) { + startStopExecutor = new InlineExecutorService(); + } + } else { + // Delegate utility execution to the Service + Server server = Container.getService(this).getServer(); + server.setUtilityThreads(threads); + startStopExecutor = server.getUtilityExecutor(); + } + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + reconfigureStartStopExecutor(getStartStopThreads()); + + // Start our subordinate components, if any + logger = null; + getLogger(); + Cluster cluster = getClusterInternal(); + if (cluster instanceof Lifecycle) { + ((Lifecycle) cluster).start(); + } + Realm realm = getRealmInternal(); + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).start(); + } + + // Start our child containers, if any + Container[] children = findChildren(); + List> results = new ArrayList<>(children.length); + for (Container child : children) { + results.add(startStopExecutor.submit(new StartChild(child))); + } + + MultiThrowable multiThrowable = null; + + for (Future result : results) { + try { + result.get(); + } catch (Throwable e) { + log.error(sm.getString("containerBase.threadedStartFailed"), e); + if (multiThrowable == null) { + multiThrowable = new MultiThrowable(); + } + multiThrowable.add(e); + } + + } + if (multiThrowable != null) { + throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"), + multiThrowable.getThrowable()); + } + + // Start the Valves in our pipeline (including the basic), if any + if (pipeline instanceof Lifecycle) { + ((Lifecycle) pipeline).start(); + } + + setState(LifecycleState.STARTING); + + // Start our thread + if (backgroundProcessorDelay > 0) { + monitorFuture = Container.getService(ContainerBase.this).getServer().getUtilityExecutor() + .scheduleWithFixedDelay(new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS); + } + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + // Stop our thread + if (monitorFuture != null) { + monitorFuture.cancel(true); + monitorFuture = null; + } + threadStop(); + + setState(LifecycleState.STOPPING); + + // Stop the Valves in our pipeline (including the basic), if any + if (pipeline instanceof Lifecycle && ((Lifecycle) pipeline).getState().isAvailable()) { + ((Lifecycle) pipeline).stop(); + } + + // Stop our child containers, if any + Container[] children = findChildren(); + List> results = new ArrayList<>(children.length); + for (Container child : children) { + results.add(startStopExecutor.submit(new StopChild(child))); + } + + boolean fail = false; + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString("containerBase.threadedStopFailed"), e); + fail = true; + } + } + if (fail) { + throw new LifecycleException(sm.getString("containerBase.threadedStopFailed")); + } + + // Stop our subordinate components, if any + Realm realm = getRealmInternal(); + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).stop(); + } + Cluster cluster = getClusterInternal(); + if (cluster instanceof Lifecycle) { + ((Lifecycle) cluster).stop(); + } + + // If init fails, this may be null + if (startStopExecutor != null) { + startStopExecutor.shutdownNow(); + startStopExecutor = null; + } + } + + @Override + protected void destroyInternal() throws LifecycleException { + + Realm realm = getRealmInternal(); + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).destroy(); + } + Cluster cluster = getClusterInternal(); + if (cluster instanceof Lifecycle) { + ((Lifecycle) cluster).destroy(); + } + + // Stop the Valves in our pipeline (including the basic), if any + if (pipeline instanceof Lifecycle) { + ((Lifecycle) pipeline).destroy(); + } + + // Remove children now this container is being destroyed + for (Container child : findChildren()) { + removeChild(child); + } + + // Required if the child is destroyed directly. + if (parent != null) { + parent.removeChild(this); + } + + super.destroyInternal(); + } + + + /** + * Check this container for an access log and if none is found, look to the parent. If there is no parent and still + * none is found, use the NoOp access log. + */ + @Override + public void logAccess(Request request, Response response, long time, boolean useDefault) { + + boolean logged = false; + + if (getAccessLog() != null) { + getAccessLog().log(request, response, time); + logged = true; + } + + if (getParent() != null) { + // No need to use default logger once request/response has been logged + // once + getParent().logAccess(request, response, time, (useDefault && !logged)); + } + } + + @Override + public AccessLog getAccessLog() { + + if (accessLogScanComplete) { + return accessLog; + } + + AccessLogAdapter adapter = null; + Valve[] valves = getPipeline().getValves(); + for (Valve valve : valves) { + if (valve instanceof AccessLog) { + if (adapter == null) { + adapter = new AccessLogAdapter((AccessLog) valve); + } else { + adapter.add((AccessLog) valve); + } + } + } + if (adapter != null) { + accessLog = adapter; + } + accessLogScanComplete = true; + return accessLog; + } + + // ------------------------------------------------------- Pipeline Methods + + + /** + * Convenience method, intended for use by the digester to simplify the process of adding Valves to containers. See + * {@link Pipeline#addValve(Valve)} for full details. Components other than the digester should use + * {@link #getPipeline()}.{@link #addValve(Valve)} in case a future implementation provides an alternative method + * for the digester to use. + * + * @param valve Valve to be added + * + * @exception IllegalArgumentException if this Container refused to accept the specified Valve + * @exception IllegalArgumentException if the specified Valve refuses to be associated with this Container + * @exception IllegalStateException if the specified Valve is already associated with a different Container + */ + public synchronized void addValve(Valve valve) { + + pipeline.addValve(valve); + } + + + /** + * Execute a periodic task, such as reloading, etc. This method will be invoked inside the classloading context of + * this container. Unexpected throwables will be caught and logged. + */ + @Override + public synchronized void backgroundProcess() { + + if (!getState().isAvailable()) { + return; + } + + Cluster cluster = getClusterInternal(); + if (cluster != null) { + try { + cluster.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e); + } + } + Realm realm = getRealmInternal(); + if (realm != null) { + try { + realm.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e); + } + } + Valve current = pipeline.getFirst(); + while (current != null) { + try { + current.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e); + } + current = current.getNext(); + } + fireLifecycleEvent(PERIODIC_EVENT, null); + } + + + @Override + public File getCatalinaBase() { + + if (parent == null) { + return null; + } + + return parent.getCatalinaBase(); + } + + + @Override + public File getCatalinaHome() { + + if (parent == null) { + return null; + } + + return parent.getCatalinaHome(); + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Notify all container event listeners that a particular event has occurred for this Container. The default + * implementation performs this notification synchronously using the calling thread. + * + * @param type Event type + * @param data Event data + */ + @Override + public void fireContainerEvent(String type, Object data) { + + if (listeners.size() < 1) { + return; + } + + ContainerEvent event = new ContainerEvent(this, type, data); + // Note for each uses an iterator internally so this is safe + for (ContainerListener listener : listeners) { + listener.containerEvent(event); + } + } + + + // -------------------- JMX and Registration -------------------- + + @Override + protected String getDomainInternal() { + + Container p = this.getParent(); + if (p == null) { + return null; + } else { + return p.getDomain(); + } + } + + + @Override + public String getMBeanKeyProperties() { + Container c = this; + StringBuilder keyProperties = new StringBuilder(); + int containerCount = 0; + + // Work up container hierarchy, add a component to the name for + // each container + while (!(c instanceof Engine)) { + if (c instanceof Wrapper) { + keyProperties.insert(0, ",servlet="); + keyProperties.insert(9, c.getName()); + } else if (c instanceof Context) { + keyProperties.insert(0, ",context="); + ContextName cn = new ContextName(c.getName(), false); + keyProperties.insert(9, cn.getDisplayName()); + } else if (c instanceof Host) { + keyProperties.insert(0, ",host="); + keyProperties.insert(6, c.getName()); + } else if (c == null) { + // May happen in unit testing and/or some embedding scenarios + keyProperties.append(",container"); + keyProperties.append(containerCount++); + keyProperties.append("=null"); + break; + } else { + // Should never happen... + keyProperties.append(",container"); + keyProperties.append(containerCount++); + keyProperties.append('='); + keyProperties.append(c.getName()); + } + c = c.getParent(); + } + return keyProperties.toString(); + } + + + public ObjectName[] getChildren() { + List names; + childrenLock.readLock().lock(); + try { + names = new ArrayList<>(children.size()); + for (Container next : children.values()) { + if (next instanceof ContainerBase) { + names.add(next.getObjectName()); + } + } + } finally { + childrenLock.readLock().unlock(); + } + return names.toArray(new ObjectName[0]); + } + + + // -------------------- Background Thread -------------------- + + /** + * Start the background thread that will periodically check for session timeouts. + */ + protected void threadStart() { + if (backgroundProcessorDelay > 0 && + (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState())) && + (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) { + if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) { + // There was an error executing the scheduled task, get it and log it + try { + backgroundProcessorFuture.get(); + } catch (InterruptedException | ExecutionException e) { + log.error(sm.getString("containerBase.backgroundProcess.error"), e); + } + } + backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor() + .scheduleWithFixedDelay(new ContainerBackgroundProcessor(), backgroundProcessorDelay, + backgroundProcessorDelay, TimeUnit.SECONDS); + } + } + + + /** + * Stop the background thread that is periodically checking for session timeouts. + */ + protected void threadStop() { + if (backgroundProcessorFuture != null) { + backgroundProcessorFuture.cancel(true); + backgroundProcessorFuture = null; + } + } + + + @Override + public final String toString() { + StringBuilder sb = new StringBuilder(); + Container parent = getParent(); + if (parent != null) { + sb.append(parent.toString()); + sb.append('.'); + } + sb.append(this.getClass().getSimpleName()); + sb.append('['); + sb.append(getName()); + sb.append(']'); + return sb.toString(); + } + + // ------------------------------- ContainerBackgroundProcessor Inner Class + + protected class ContainerBackgroundProcessorMonitor implements Runnable { + @Override + public void run() { + if (getState().isAvailable()) { + threadStart(); + } + } + } + + /** + * Private runnable class to invoke the backgroundProcess method of this container and its children after a fixed + * delay. + */ + protected class ContainerBackgroundProcessor implements Runnable { + + @Override + public void run() { + processChildren(ContainerBase.this); + } + + protected void processChildren(Container container) { + ClassLoader originalClassLoader = null; + + try { + if (container instanceof Context) { + Loader loader = ((Context) container).getLoader(); + // Loader will be null for FailedContext instances + if (loader == null) { + return; + } + + // Ensure background processing for Contexts and Wrappers + // is performed under the web app's class loader + originalClassLoader = ((Context) container).bind(false, null); + } + container.backgroundProcess(); + Container[] children = container.findChildren(); + for (Container child : children) { + if (child.getBackgroundProcessorDelay() <= 0) { + processChildren(child); + } + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("containerBase.backgroundProcess.error"), t); + } finally { + if (container instanceof Context) { + ((Context) container).unbind(false, originalClassLoader); + } + } + } + } + + + // ---------------------------- Inner classes used with start/stop Executor + + private static class StartChild implements Callable { + + private Container child; + + StartChild(Container child) { + this.child = child; + } + + @Override + public Void call() throws LifecycleException { + child.start(); + return null; + } + } + + private static class StopChild implements Callable { + + private Container child; + + StopChild(Container child) { + this.child = child; + } + + @Override + public Void call() throws LifecycleException { + if (child.getState().isAvailable()) { + child.stop(); + } + return null; + } + } + +} diff --git a/java/org/apache/catalina/core/ContextNamingInfoListener.java b/java/org/apache/catalina/core/ContextNamingInfoListener.java new file mode 100644 index 0000000..5b1c760 --- /dev/null +++ b/java/org/apache/catalina/core/ContextNamingInfoListener.java @@ -0,0 +1,120 @@ +/* + * 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.catalina.core; + +import org.apache.catalina.Context; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implementation of {@code LifecycleListener} that will create context naming information environment entries. + *

    + * This listener must only be nested within {@link Context} elements. + *

    + * The following entries will be added to the initial context ({@code java:comp/env} implied): + *

      + *
    • Path: {@code context/path} from {@link Context#getPath()}
    • + *
    • Encoded Path: {@code context/encodedPath} from {@link Context#getEncodedPath()}
    • + *
    • Webapp Version: {@code context/webappVersion} from {@link Context#getWebappVersion()}
    • + *
    • Name: {@code context/name} from {@link Context#getName()}
    • + *
    • Base Name: {@code context/baseName} from {@link Context#getBaseName()}
    • + *
    • Display Name: {@code context/displayName} from {@link Context#getDisplayName()}
    • + *
    + *

    + * See the Tomcat documentation for + * more details on the values. + */ +public class ContextNamingInfoListener implements LifecycleListener { + + private static final String PATH_ENTRY_NAME = "context/path"; + private static final String ENCODED_PATH_ENTRY_NAME = "context/encodedPath"; + private static final String WEBAPP_VERSION_ENTRY_NAME = "context/webappVersion"; + private static final String NAME_ENTRY_NAME = "context/name"; + private static final String BASE_NAME_ENTRY_NAME = "context/baseName"; + private static final String DISPLAY_NAME_ENTRY_NAME = "context/displayName"; + + private static final Log log = LogFactory.getLog(ContextNamingInfoListener.class); + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(ContextNamingInfoListener.class); + + private boolean emptyOnRoot = true; + + /** + * Sets whether for the root context {@code context/path} and {@code context/encodedPath} will contain {@code "/"} + * and {@code context/name} will contain {@code "ROOT"} with a version, if any. + * + * @param emptyOnRoot whether paths and name for root context shall be empty + */ + public void setEmptyOnRoot(boolean emptyOnRoot) { + this.emptyOnRoot = emptyOnRoot; + } + + /** + * Gets whether paths and name for the root context will be empty. + * + * @return indicator whether paths and name for the root context will be empty + */ + public boolean isEmptyOnRoot() { + return emptyOnRoot; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { + if (!(event.getLifecycle() instanceof Context)) { + log.warn(sm.getString("listener.notContext", event.getLifecycle().getClass().getSimpleName())); + return; + } + Context context = (Context) event.getLifecycle(); + String path = context.getPath(); + String encodedPath = context.getEncodedPath(); + String name = context.getName(); + + if (!emptyOnRoot && path.isEmpty()) { + path = encodedPath = "/"; + name = "ROOT" + name; + } + + addEnvEntry(context, PATH_ENTRY_NAME, path); + addEnvEntry(context, ENCODED_PATH_ENTRY_NAME, encodedPath); + addEnvEntry(context, WEBAPP_VERSION_ENTRY_NAME, context.getWebappVersion()); + addEnvEntry(context, NAME_ENTRY_NAME, name); + addEnvEntry(context, BASE_NAME_ENTRY_NAME, context.getBaseName()); + addEnvEntry(context, DISPLAY_NAME_ENTRY_NAME, context.getDisplayName()); + } + } + + private void addEnvEntry(Context context, String name, String value) { + ContextEnvironment ce = new ContextEnvironment(); + ce.setName(name); + ce.setOverride(true); + ce.setType("java.lang.String"); + ce.setValue(value); + if (log.isDebugEnabled()) { + log.info(sm.getString("contextNamingInfoListener.envEntry", name, value)); + } + context.getNamingResources().addEnvironment(ce); + } + +} diff --git a/java/org/apache/catalina/core/DefaultInstanceManager.java b/java/org/apache/catalina/core/DefaultInstanceManager.java new file mode 100644 index 0000000..d0ffa4c --- /dev/null +++ b/java/org/apache/catalina/core/DefaultInstanceManager.java @@ -0,0 +1,771 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.naming.Context; +import javax.naming.NamingException; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Resource; +import jakarta.ejb.EJB; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceUnit; +import jakarta.xml.ws.WebServiceRef; + +import org.apache.catalina.ContainerServlet; +import org.apache.catalina.Globals; +import org.apache.catalina.security.SecurityUtil; +import org.apache.catalina.util.Introspection; +import org.apache.juli.logging.Log; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.collections.ManagedConcurrentWeakHashMap; +import org.apache.tomcat.util.res.StringManager; + +public class DefaultInstanceManager implements InstanceManager { + + // Used when there are no annotations in a class + private static final AnnotationCacheEntry[] ANNOTATIONS_EMPTY = new AnnotationCacheEntry[0]; + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(DefaultInstanceManager.class); + + private static final boolean EJB_PRESENT; + private static final boolean JPA_PRESENT; + private static final boolean WS_PRESENT; + + static { + Class clazz = null; + try { + clazz = Class.forName("jakarta.ejb.EJB"); + } catch (ClassNotFoundException cnfe) { + // Expected + } + EJB_PRESENT = (clazz != null); + + clazz = null; + try { + clazz = Class.forName("jakarta.persistence.PersistenceContext"); + } catch (ClassNotFoundException cnfe) { + // Expected + } + JPA_PRESENT = (clazz != null); + + clazz = null; + try { + clazz = Class.forName("jakarta.xml.ws.WebServiceRef"); + } catch (ClassNotFoundException cnfe) { + // Expected + } + WS_PRESENT = (clazz != null); + } + + + private final Context context; + private final Map> injectionMap; + protected final ClassLoader classLoader; + protected final ClassLoader containerClassLoader; + protected final boolean privileged; + protected final boolean ignoreAnnotations; + private final Set restrictedClasses; + private final ManagedConcurrentWeakHashMap,AnnotationCacheEntry[]> annotationCache = + new ManagedConcurrentWeakHashMap<>(); + private final Map postConstructMethods; + private final Map preDestroyMethods; + + public DefaultInstanceManager(Context context, Map> injectionMap, + org.apache.catalina.Context catalinaContext, ClassLoader containerClassLoader) { + classLoader = catalinaContext.getLoader().getClassLoader(); + privileged = catalinaContext.getPrivileged(); + this.containerClassLoader = containerClassLoader; + ignoreAnnotations = catalinaContext.getIgnoreAnnotations(); + Log log = catalinaContext.getLogger(); + Set classNames = new HashSet<>(); + loadProperties(classNames, "org/apache/catalina/core/RestrictedServlets.properties", + "defaultInstanceManager.restrictedServletsResource", log); + loadProperties(classNames, "org/apache/catalina/core/RestrictedListeners.properties", + "defaultInstanceManager.restrictedListenersResource", log); + loadProperties(classNames, "org/apache/catalina/core/RestrictedFilters.properties", + "defaultInstanceManager.restrictedFiltersResource", log); + restrictedClasses = Collections.unmodifiableSet(classNames); + this.context = context; + this.injectionMap = injectionMap; + this.postConstructMethods = catalinaContext.findPostConstructMethods(); + this.preDestroyMethods = catalinaContext.findPreDestroyMethods(); + } + + @Override + public Object newInstance(Class clazz) throws IllegalAccessException, InvocationTargetException, NamingException, + InstantiationException, IllegalArgumentException, NoSuchMethodException, SecurityException { + return newInstance(clazz.getConstructor().newInstance(), clazz); + } + + @Override + public Object newInstance(String className) + throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, + ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException { + Class clazz = loadClassMaybePrivileged(className, classLoader); + return newInstance(clazz.getConstructor().newInstance(), clazz); + } + + @Override + public Object newInstance(final String className, final ClassLoader classLoader) + throws IllegalAccessException, NamingException, InvocationTargetException, InstantiationException, + ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException { + Class clazz = classLoader.loadClass(className); + return newInstance(clazz.getConstructor().newInstance(), clazz); + } + + @Override + public void newInstance(Object o) throws IllegalAccessException, InvocationTargetException, NamingException { + newInstance(o, o.getClass()); + } + + private Object newInstance(Object instance, Class clazz) + throws IllegalAccessException, InvocationTargetException, NamingException { + if (!ignoreAnnotations) { + Map injections = assembleInjectionsFromClassHierarchy(clazz); + populateAnnotationsCache(clazz, injections); + processAnnotations(instance, injections); + postConstruct(instance, clazz); + } + return instance; + } + + private Map assembleInjectionsFromClassHierarchy(Class clazz) { + Map injections = new HashMap<>(); + Map currentInjections = null; + while (clazz != null) { + currentInjections = this.injectionMap.get(clazz.getName()); + if (currentInjections != null) { + injections.putAll(currentInjections); + } + clazz = clazz.getSuperclass(); + } + return injections; + } + + @Override + public void destroyInstance(Object instance) throws IllegalAccessException, InvocationTargetException { + if (!ignoreAnnotations) { + preDestroy(instance, instance.getClass()); + } + } + + /** + * Call postConstruct method on the specified instance recursively from deepest superclass to actual class. + * + * @param instance object to call postconstruct methods on + * @param clazz (super) class to examine for postConstruct annotation. + * + * @throws IllegalAccessException if postConstruct method is inaccessible. + * @throws java.lang.reflect.InvocationTargetException if call fails + */ + protected void postConstruct(Object instance, final Class clazz) + throws IllegalAccessException, InvocationTargetException { + if (context == null) { + // No resource injection + return; + } + + Class superClass = clazz.getSuperclass(); + if (superClass != Object.class) { + postConstruct(instance, superClass); + } + + // At the end the postconstruct annotated + // method is invoked + AnnotationCacheEntry[] annotations = annotationCache.get(clazz); + for (AnnotationCacheEntry entry : annotations) { + if (entry.getType() == AnnotationCacheEntryType.POST_CONSTRUCT) { + // This will always return a new Method instance + // Making this instance accessible does not affect other instances + Method postConstruct = getMethod(clazz, entry); + // If this doesn't work, just let invoke() fail + postConstruct.trySetAccessible(); + postConstruct.invoke(instance); + } + } + } + + + /** + * Call preDestroy method on the specified instance recursively from deepest superclass to actual class. + * + * @param instance object to call preDestroy methods on + * @param clazz (super) class to examine for preDestroy annotation. + * + * @throws IllegalAccessException if preDestroy method is inaccessible. + * @throws java.lang.reflect.InvocationTargetException if call fails + */ + protected void preDestroy(Object instance, final Class clazz) + throws IllegalAccessException, InvocationTargetException { + Class superClass = clazz.getSuperclass(); + if (superClass != Object.class) { + preDestroy(instance, superClass); + } + + // At the end the postconstruct annotated + // method is invoked + AnnotationCacheEntry[] annotations = annotationCache.get(clazz); + if (annotations == null) { + // instance not created through the instance manager + return; + } + for (AnnotationCacheEntry entry : annotations) { + if (entry.getType() == AnnotationCacheEntryType.PRE_DESTROY) { + // This will always return a new Method instance + // Making this instance accessible does not affect other instances + Method preDestroy = getMethod(clazz, entry); + // If this doesn't work, just let invoke() fail + preDestroy.trySetAccessible(); + preDestroy.invoke(instance); + } + } + } + + + @Override + public void backgroundProcess() { + annotationCache.maintain(); + } + + + /** + * Make sure that the annotations cache has been populated for the provided class. + * + * @param clazz clazz to populate annotations for + * @param injections map of injections for this class from xml deployment descriptor + * + * @throws IllegalAccessException if injection target is inaccessible + * @throws javax.naming.NamingException if value cannot be looked up in jndi + * @throws java.lang.reflect.InvocationTargetException if injection fails + */ + protected void populateAnnotationsCache(Class clazz, Map injections) + throws IllegalAccessException, InvocationTargetException, NamingException { + + List annotations = null; + Set injectionsMatchedToSetter = new HashSet<>(); + + while (clazz != null) { + AnnotationCacheEntry[] annotationsArray = annotationCache.get(clazz); + if (annotationsArray == null) { + if (annotations == null) { + annotations = new ArrayList<>(); + } else { + annotations.clear(); + } + + // Initialize methods annotations + Method[] methods = Introspection.getDeclaredMethods(clazz); + Method postConstruct = null; + String postConstructFromXml = postConstructMethods.get(clazz.getName()); + Method preDestroy = null; + String preDestroyFromXml = preDestroyMethods.get(clazz.getName()); + for (Method method : methods) { + if (context != null) { + // Resource injection only if JNDI is enabled + if (injections != null && Introspection.isValidSetter(method)) { + String fieldName = Introspection.getPropertyName(method); + injectionsMatchedToSetter.add(fieldName); + if (injections.containsKey(fieldName)) { + annotations.add(new AnnotationCacheEntry(method.getName(), method.getParameterTypes(), + injections.get(fieldName), AnnotationCacheEntryType.SETTER)); + continue; + } + } + Resource resourceAnnotation; + Annotation ejbAnnotation; + Annotation webServiceRefAnnotation; + Annotation persistenceContextAnnotation; + Annotation persistenceUnitAnnotation; + if ((resourceAnnotation = method.getAnnotation(Resource.class)) != null) { + annotations.add(new AnnotationCacheEntry(method.getName(), method.getParameterTypes(), + resourceAnnotation.name(), AnnotationCacheEntryType.SETTER)); + } else if (EJB_PRESENT && (ejbAnnotation = method.getAnnotation(EJB.class)) != null) { + annotations.add(new AnnotationCacheEntry(method.getName(), method.getParameterTypes(), + ((EJB) ejbAnnotation).name(), AnnotationCacheEntryType.SETTER)); + } else if (WS_PRESENT && + (webServiceRefAnnotation = method.getAnnotation(WebServiceRef.class)) != null) { + annotations.add(new AnnotationCacheEntry(method.getName(), method.getParameterTypes(), + ((WebServiceRef) webServiceRefAnnotation).name(), AnnotationCacheEntryType.SETTER)); + } else if (JPA_PRESENT && (persistenceContextAnnotation = + method.getAnnotation(PersistenceContext.class)) != null) { + annotations.add(new AnnotationCacheEntry(method.getName(), method.getParameterTypes(), + ((PersistenceContext) persistenceContextAnnotation).name(), + AnnotationCacheEntryType.SETTER)); + } else if (JPA_PRESENT && + (persistenceUnitAnnotation = method.getAnnotation(PersistenceUnit.class)) != null) { + annotations.add(new AnnotationCacheEntry(method.getName(), method.getParameterTypes(), + ((PersistenceUnit) persistenceUnitAnnotation).name(), + AnnotationCacheEntryType.SETTER)); + } + } + + postConstruct = findPostConstruct(postConstruct, postConstructFromXml, method); + + preDestroy = findPreDestroy(preDestroy, preDestroyFromXml, method); + } + + if (postConstruct != null) { + annotations.add(new AnnotationCacheEntry(postConstruct.getName(), postConstruct.getParameterTypes(), + null, AnnotationCacheEntryType.POST_CONSTRUCT)); + } else if (postConstructFromXml != null) { + throw new IllegalArgumentException(sm.getString("defaultInstanceManager.postConstructNotFound", + postConstructFromXml, clazz.getName())); + } + if (preDestroy != null) { + annotations.add(new AnnotationCacheEntry(preDestroy.getName(), preDestroy.getParameterTypes(), null, + AnnotationCacheEntryType.PRE_DESTROY)); + } else if (preDestroyFromXml != null) { + throw new IllegalArgumentException(sm.getString("defaultInstanceManager.preDestroyNotFound", + preDestroyFromXml, clazz.getName())); + } + + if (context != null) { + // Initialize fields annotations for resource injection if + // JNDI is enabled + Field[] fields = Introspection.getDeclaredFields(clazz); + for (Field field : fields) { + Resource resourceAnnotation; + Annotation ejbAnnotation; + Annotation webServiceRefAnnotation; + Annotation persistenceContextAnnotation; + Annotation persistenceUnitAnnotation; + String fieldName = field.getName(); + if (injections != null && injections.containsKey(fieldName) && + !injectionsMatchedToSetter.contains(fieldName)) { + annotations.add(new AnnotationCacheEntry(fieldName, null, injections.get(fieldName), + AnnotationCacheEntryType.FIELD)); + } else if ((resourceAnnotation = field.getAnnotation(Resource.class)) != null) { + annotations.add(new AnnotationCacheEntry(fieldName, null, resourceAnnotation.name(), + AnnotationCacheEntryType.FIELD)); + } else if (EJB_PRESENT && (ejbAnnotation = field.getAnnotation(EJB.class)) != null) { + annotations.add(new AnnotationCacheEntry(fieldName, null, ((EJB) ejbAnnotation).name(), + AnnotationCacheEntryType.FIELD)); + } else if (WS_PRESENT && + (webServiceRefAnnotation = field.getAnnotation(WebServiceRef.class)) != null) { + annotations.add(new AnnotationCacheEntry(fieldName, null, + ((WebServiceRef) webServiceRefAnnotation).name(), AnnotationCacheEntryType.FIELD)); + } else if (JPA_PRESENT && (persistenceContextAnnotation = + field.getAnnotation(PersistenceContext.class)) != null) { + annotations.add(new AnnotationCacheEntry(fieldName, null, + ((PersistenceContext) persistenceContextAnnotation).name(), + AnnotationCacheEntryType.FIELD)); + } else if (JPA_PRESENT && + (persistenceUnitAnnotation = field.getAnnotation(PersistenceUnit.class)) != null) { + annotations.add(new AnnotationCacheEntry(fieldName, null, + ((PersistenceUnit) persistenceUnitAnnotation).name(), + AnnotationCacheEntryType.FIELD)); + } + } + } + + if (annotations.isEmpty()) { + // Use common object to save memory + annotationsArray = ANNOTATIONS_EMPTY; + } else { + annotationsArray = annotations.toArray(new AnnotationCacheEntry[0]); + } + synchronized (annotationCache) { + annotationCache.put(clazz, annotationsArray); + } + } + clazz = clazz.getSuperclass(); + } + } + + + /** + * Inject resources in specified instance. + * + * @param instance instance to inject into + * @param injections map of injections for this class from xml deployment descriptor + * + * @throws IllegalAccessException if injection target is inaccessible + * @throws javax.naming.NamingException if value cannot be looked up in jndi + * @throws java.lang.reflect.InvocationTargetException if injection fails + */ + protected void processAnnotations(Object instance, Map injections) + throws IllegalAccessException, InvocationTargetException, NamingException { + + if (context == null) { + // No resource injection + return; + } + + Class clazz = instance.getClass(); + + while (clazz != null) { + AnnotationCacheEntry[] annotations = annotationCache.get(clazz); + for (AnnotationCacheEntry entry : annotations) { + if (entry.getType() == AnnotationCacheEntryType.SETTER) { + lookupMethodResource(context, instance, getMethod(clazz, entry), entry.getName(), clazz); + } else if (entry.getType() == AnnotationCacheEntryType.FIELD) { + lookupFieldResource(context, instance, getField(clazz, entry), entry.getName(), clazz); + } + } + clazz = clazz.getSuperclass(); + } + } + + + /** + * Makes cache size available to unit tests. + * + * @return the cache size + */ + protected int getAnnotationCacheSize() { + return annotationCache.size(); + } + + + protected Class loadClassMaybePrivileged(final String className, final ClassLoader classLoader) + throws ClassNotFoundException { + Class clazz; + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + clazz = AccessController.doPrivileged(new PrivilegedLoadClass(className, classLoader)); + } catch (PrivilegedActionException e) { + Throwable t = e.getCause(); + if (t instanceof ClassNotFoundException) { + throw (ClassNotFoundException) t; + } + throw new RuntimeException(t); + } + } else { + clazz = loadClass(className, classLoader); + } + checkAccess(clazz); + return clazz; + } + + protected Class loadClass(String className, ClassLoader classLoader) throws ClassNotFoundException { + if (className.startsWith("org.apache.catalina")) { + return containerClassLoader.loadClass(className); + } + try { + Class clazz = containerClassLoader.loadClass(className); + if (ContainerServlet.class.isAssignableFrom(clazz)) { + return clazz; + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + return classLoader.loadClass(className); + } + + private void checkAccess(Class clazz) { + if (privileged) { + return; + } + if (ContainerServlet.class.isAssignableFrom(clazz)) { + throw new SecurityException(sm.getString("defaultInstanceManager.restrictedContainerServlet", clazz)); + } + while (clazz != null) { + if (restrictedClasses.contains(clazz.getName())) { + throw new SecurityException(sm.getString("defaultInstanceManager.restrictedClass", clazz)); + } + clazz = clazz.getSuperclass(); + } + } + + /** + * Inject resources in specified field. + * + * @param context jndi context to extract value from + * @param instance object to inject into + * @param field field target for injection + * @param name jndi name value is bound under + * @param clazz class annotation is defined in + * + * @throws IllegalAccessException if field is inaccessible + * @throws javax.naming.NamingException if value is not accessible in naming context + */ + protected static void lookupFieldResource(Context context, Object instance, Field field, String name, + Class clazz) throws NamingException, IllegalAccessException { + + Object lookedupResource; + + String normalizedName = normalize(name); + + if ((normalizedName != null) && (normalizedName.length() > 0)) { + lookedupResource = context.lookup(normalizedName); + } else { + lookedupResource = context.lookup(clazz.getName() + "/" + field.getName()); + } + + // This will always be a new Field instance + // Making this instance accessible does not affect other instances + // If this doesn't work, just let set() fail + field.trySetAccessible(); + field.set(instance, lookedupResource); + } + + /** + * Inject resources in specified method. + * + * @param context jndi context to extract value from + * @param instance object to inject into + * @param method field target for injection + * @param name jndi name value is bound under + * @param clazz class annotation is defined in + * + * @throws IllegalAccessException if method is inaccessible + * @throws javax.naming.NamingException if value is not accessible in naming context + * @throws java.lang.reflect.InvocationTargetException if setter call fails + */ + protected static void lookupMethodResource(Context context, Object instance, Method method, String name, + Class clazz) throws NamingException, IllegalAccessException, InvocationTargetException { + + if (!Introspection.isValidSetter(method)) { + throw new IllegalArgumentException(sm.getString("defaultInstanceManager.invalidInjection")); + } + + Object lookedupResource; + + String normalizedName = normalize(name); + + if ((normalizedName != null) && (normalizedName.length() > 0)) { + lookedupResource = context.lookup(normalizedName); + } else { + lookedupResource = context.lookup(clazz.getName() + "/" + Introspection.getPropertyName(method)); + } + + // This will always be a new Method instance + // Making this instance accessible does not affect other instances + // If this doesn't work, just let invoke() fail + method.trySetAccessible(); + method.invoke(instance, lookedupResource); + } + + private static void loadProperties(Set classNames, String resourceName, String messageKey, Log log) { + Properties properties = new Properties(); + ClassLoader cl = DefaultInstanceManager.class.getClassLoader(); + try (InputStream is = cl.getResourceAsStream(resourceName)) { + if (is == null) { + log.error(sm.getString(messageKey, resourceName)); + } else { + properties.load(is); + } + } catch (IOException ioe) { + log.error(sm.getString(messageKey, resourceName), ioe); + } + if (properties.isEmpty()) { + return; + } + for (Map.Entry e : properties.entrySet()) { + if ("restricted".equals(e.getValue())) { + classNames.add(e.getKey().toString()); + } else { + log.warn(sm.getString("defaultInstanceManager.restrictedWrongValue", resourceName, e.getKey(), + e.getValue())); + } + } + } + + private static String normalize(String jndiName) { + if (jndiName != null && jndiName.startsWith("java:comp/env/")) { + return jndiName.substring(14); + } + return jndiName; + } + + private static Method getMethod(final Class clazz, final AnnotationCacheEntry entry) { + Method result = null; + if (Globals.IS_SECURITY_ENABLED) { + result = AccessController.doPrivileged(new PrivilegedGetMethod(clazz, entry)); + } else { + try { + result = clazz.getDeclaredMethod(entry.getAccessibleObjectName(), entry.getParamTypes()); + } catch (NoSuchMethodException e) { + // Should never happen. On that basis don't log it. + } + } + return result; + } + + private static Field getField(final Class clazz, final AnnotationCacheEntry entry) { + Field result = null; + if (Globals.IS_SECURITY_ENABLED) { + result = AccessController.doPrivileged(new PrivilegedGetField(clazz, entry)); + } else { + try { + result = clazz.getDeclaredField(entry.getAccessibleObjectName()); + } catch (NoSuchFieldException e) { + // Should never happen. On that basis don't log it. + } + } + return result; + } + + + private static Method findPostConstruct(Method currentPostConstruct, String postConstructFromXml, Method method) { + return findLifecycleCallback(currentPostConstruct, postConstructFromXml, method, PostConstruct.class); + } + + private static Method findPreDestroy(Method currentPreDestroy, String preDestroyFromXml, Method method) { + return findLifecycleCallback(currentPreDestroy, preDestroyFromXml, method, PreDestroy.class); + } + + private static Method findLifecycleCallback(Method currentMethod, String methodNameFromXml, Method method, + Class annotation) { + Method result = currentMethod; + if (methodNameFromXml != null) { + if (method.getName().equals(methodNameFromXml)) { + if (!Introspection.isValidLifecycleCallback(method)) { + throw new IllegalArgumentException( + sm.getString("defaultInstanceManager.invalidAnnotation", annotation.getName())); + } + result = method; + } + } else { + if (method.isAnnotationPresent(annotation)) { + if (currentMethod != null || !Introspection.isValidLifecycleCallback(method)) { + throw new IllegalArgumentException( + sm.getString("defaultInstanceManager.invalidAnnotation", annotation.getName())); + } + result = method; + } + } + return result; + } + + private static final class AnnotationCacheEntry { + private final String accessibleObjectName; + private final Class[] paramTypes; + private final String name; + private final AnnotationCacheEntryType type; + + AnnotationCacheEntry(String accessibleObjectName, Class[] paramTypes, String name, + AnnotationCacheEntryType type) { + this.accessibleObjectName = accessibleObjectName; + this.paramTypes = paramTypes; + this.name = name; + this.type = type; + } + + public String getAccessibleObjectName() { + return accessibleObjectName; + } + + public Class[] getParamTypes() { + return paramTypes; + } + + public String getName() { + return name; + } + + public AnnotationCacheEntryType getType() { + return type; + } + } + + + private enum AnnotationCacheEntryType { + FIELD, + SETTER, + POST_CONSTRUCT, + PRE_DESTROY + } + + + private static class PrivilegedGetField implements PrivilegedAction { + + private final Class clazz; + private final AnnotationCacheEntry entry; + + PrivilegedGetField(Class clazz, AnnotationCacheEntry entry) { + this.clazz = clazz; + this.entry = entry; + } + + @Override + public Field run() { + Field result = null; + try { + result = clazz.getDeclaredField(entry.getAccessibleObjectName()); + } catch (NoSuchFieldException e) { + // Should never happen. On that basis don't log it. + } + return result; + } + } + + + private static class PrivilegedGetMethod implements PrivilegedAction { + + private final Class clazz; + private final AnnotationCacheEntry entry; + + PrivilegedGetMethod(Class clazz, AnnotationCacheEntry entry) { + this.clazz = clazz; + this.entry = entry; + } + + @Override + public Method run() { + Method result = null; + try { + result = clazz.getDeclaredMethod(entry.getAccessibleObjectName(), entry.getParamTypes()); + } catch (NoSuchMethodException e) { + // Should never happen. On that basis don't log it. + } + return result; + } + } + + + private class PrivilegedLoadClass implements PrivilegedExceptionAction> { + + private final String className; + private final ClassLoader classLoader; + + PrivilegedLoadClass(String className, ClassLoader classLoader) { + this.className = className; + this.classLoader = classLoader; + } + + @Override + public Class run() throws Exception { + return loadClass(className, classLoader); + } + } +} diff --git a/java/org/apache/catalina/core/FrameworkListener.java b/java/org/apache/catalina/core/FrameworkListener.java new file mode 100644 index 0000000..e454d08 --- /dev/null +++ b/java/org/apache/catalina/core/FrameworkListener.java @@ -0,0 +1,121 @@ +/* + * 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.catalina.core; + +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.catalina.Container; +import org.apache.catalina.ContainerEvent; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.catalina.Service; + +/** + * This listener must be declared in server.xml as a Server listener, possibly optional. It will register a lifecycle + * listener on all contexts. This is an alternative to adding a Listener in context.xml with more flexibility. + */ +public abstract class FrameworkListener implements LifecycleListener, ContainerListener { + + protected final ConcurrentHashMap contextListeners = new ConcurrentHashMap<>(); + + /** + * Create a lifecycle listener which will then be added to the specified context. + * + * @param context the associated Context + * + * @return the lifecycle listener + */ + protected abstract LifecycleListener createLifecycleListener(Context context); + + @Override + public void lifecycleEvent(LifecycleEvent event) { + Lifecycle lifecycle = event.getLifecycle(); + if (Lifecycle.BEFORE_START_EVENT.equals(event.getType()) && lifecycle instanceof Server) { + Server server = (Server) lifecycle; + registerListenersForServer(server); + } + } + + @Override + public void containerEvent(ContainerEvent event) { + String type = event.getType(); + if (Container.ADD_CHILD_EVENT.equals(type)) { + processContainerAddChild((Container) event.getData()); + } else if (Container.REMOVE_CHILD_EVENT.equals(type)) { + processContainerRemoveChild((Container) event.getData()); + } + } + + protected void registerListenersForServer(Server server) { + for (Service service : server.findServices()) { + Engine engine = service.getContainer(); + if (engine != null) { + engine.addContainerListener(this); + registerListenersForEngine(engine); + } + } + } + + protected void registerListenersForEngine(Engine engine) { + for (Container hostContainer : engine.findChildren()) { + Host host = (Host) hostContainer; + host.addContainerListener(this); + registerListenersForHost(host); + } + } + + protected void registerListenersForHost(Host host) { + for (Container contextContainer : host.findChildren()) { + Context context = (Context) contextContainer; + registerContextListener(context); + } + } + + protected void registerContextListener(Context context) { + LifecycleListener listener = createLifecycleListener(context); + contextListeners.put(context, listener); + context.addLifecycleListener(listener); + } + + protected void processContainerAddChild(Container child) { + if (child instanceof Context) { + registerContextListener((Context) child); + } else if (child instanceof Engine) { + registerListenersForEngine((Engine) child); + } else if (child instanceof Host) { + registerListenersForHost((Host) child); + } + } + + protected void processContainerRemoveChild(Container child) { + if (child instanceof Context) { + LifecycleListener listener = contextListeners.remove(child); + if (listener != null) { + child.removeLifecycleListener(listener); + } + } else if (child instanceof Host || child instanceof Engine) { + child.removeContainerListener(this); + } + } + +} diff --git a/java/org/apache/catalina/core/JniLifecycleListener.java b/java/org/apache/catalina/core/JniLifecycleListener.java new file mode 100644 index 0000000..d1f3e79 --- /dev/null +++ b/java/org/apache/catalina/core/JniLifecycleListener.java @@ -0,0 +1,87 @@ +/* + * 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.catalina.core; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * An implementation of LifeCycleListener that loads a native library into the JVM. + *

    + * Native libraries are associated with the class loader of the class that loaded them, and the same library may not be + * loaded by more than one class loader. Due to that restriction, loading a native library from a Webapp's class loader + * makes it impossible for other Webapps to load the native library. + *

    + * Loading the native library using this listener solves the issue as it is loaded by a shared class loader (typically + * the Common class loader, but may vary in some configurations). + */ +public class JniLifecycleListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(JniLifecycleListener.class); + protected static final StringManager sm = StringManager.getManager(JniLifecycleListener.class); + + private String libraryName = ""; + private String libraryPath = ""; + + @Override + public void lifecycleEvent(LifecycleEvent event) { + + if (Lifecycle.BEFORE_START_EVENT.equals(event.getType())) { + + if (!libraryName.isEmpty()) { + System.loadLibrary(libraryName); + log.info(sm.getString("jniLifecycleListener.load.name", libraryName)); + } else if (!libraryPath.isEmpty()) { + System.load(libraryPath); + log.info(sm.getString("jniLifecycleListener.load.path", libraryPath)); + } else { + throw new IllegalArgumentException(sm.getString("jniLifecycleListener.missingPathOrName")); + } + } + } + + public void setLibraryName(String libraryName) { + + if (!this.libraryPath.isEmpty()) { + throw new IllegalArgumentException(sm.getString("jniLifecycleListener.bothPathAndName")); + } + + this.libraryName = libraryName; + } + + public String getLibraryName() { + return libraryName; + } + + public void setLibraryPath(String libraryPath) { + + if (!this.libraryName.isEmpty()) { + throw new IllegalArgumentException(sm.getString("jniLifecycleListener.bothPathAndName")); + } + + this.libraryPath = libraryPath; + } + + public String getLibraryPath() { + return libraryPath; + } + +} diff --git a/java/org/apache/catalina/core/JreMemoryLeakPreventionListener.java b/java/org/apache/catalina/core/JreMemoryLeakPreventionListener.java new file mode 100644 index 0000000..ac1c857 --- /dev/null +++ b/java/org/apache/catalina/core/JreMemoryLeakPreventionListener.java @@ -0,0 +1,214 @@ +/* + * 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.catalina.core; + +import java.net.URLConnection; +import java.security.SecureRandom; +import java.sql.DriverManager; +import java.util.StringTokenizer; + +import javax.imageio.ImageIO; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Provide a workaround for known places where the Java Runtime environment can cause a memory leak or lock files. + *

    + * Memory leaks occur when JRE code uses the context class loader to load a singleton as this will cause a memory leak + * if a web application class loader happens to be the context class loader at the time. The work-around is to + * initialise these singletons when Tomcat's common class loader is the context class loader. + *

    + * Locked files usually occur when a resource inside a JAR is accessed without first disabling Jar URL connection + * caching. The workaround is to disable this caching by default. + *

    + * This listener must only be nested within {@link Server} elements. + */ +public class JreMemoryLeakPreventionListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(JreMemoryLeakPreventionListener.class); + private static final StringManager sm = StringManager.getManager(JreMemoryLeakPreventionListener.class); + + /** + * Protect against the memory leak caused when the first call to sun.awt.AppContext.getAppContext() is + * triggered by a web application. Defaults to false since Tomcat code no longer triggers this although + * application code may. + */ + private boolean appContextProtection = false; + + public boolean isAppContextProtection() { + return appContextProtection; + } + + public void setAppContextProtection(boolean appContextProtection) { + this.appContextProtection = appContextProtection; + } + + /** + * Protect against resources being read for JAR files and, as a side-effect, the JAR file becoming locked. Note this + * disables caching for all {@link URLConnection}s, regardless of type. Defaults to true. + */ + private boolean urlCacheProtection = true; + + public boolean isUrlCacheProtection() { + return urlCacheProtection; + } + + public void setUrlCacheProtection(boolean urlCacheProtection) { + this.urlCacheProtection = urlCacheProtection; + } + + /** + * The first access to {@link DriverManager} will trigger the loading of all {@link java.sql.Driver}s in the the + * current class loader. The web application level memory leak protection can take care of this in most cases but + * triggering the loading here has fewer side-effects. + */ + private boolean driverManagerProtection = true; + + public boolean isDriverManagerProtection() { + return driverManagerProtection; + } + + public void setDriverManagerProtection(boolean driverManagerProtection) { + this.driverManagerProtection = driverManagerProtection; + } + + /** + * List of comma-separated fully qualified class names to load and initialize during the startup of this Listener. + * This allows to pre-load classes that are known to provoke classloader leaks if they are loaded during a request + * processing. + */ + private String classesToInitialize = null; + + public String getClassesToInitialize() { + return classesToInitialize; + } + + public void setClassesToInitialize(String classesToInitialize) { + this.classesToInitialize = classesToInitialize; + } + + /** + * Initialize JVM seed generator. On some platforms, the JVM will create a thread for this task, which can get + * associated with a web application depending on the timing. + */ + private boolean initSeedGenerator = false; + + public boolean getInitSeedGenerator() { + return this.initSeedGenerator; + } + + public void setInitSeedGenerator(boolean initSeedGenerator) { + this.initSeedGenerator = initSeedGenerator; + } + + + @Override + public void lifecycleEvent(LifecycleEvent event) { + // Initialise these classes when Tomcat starts + if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) { + if (!(event.getLifecycle() instanceof Server)) { + log.warn(sm.getString("listener.notServer", event.getLifecycle().getClass().getSimpleName())); + } + + /* + * First call to this loads all drivers visible to the current class loader and its parents. + * + * Note: This is called before the context class loader is changed because we want any drivers located in + * CATALINA_HOME/lib and/or CATALINA_HOME/lib to be visible to DriverManager. Users wishing to avoid having + * JDBC drivers loaded by this class loader should add the JDBC driver(s) to the class path so they are + * loaded by the system class loader. + */ + if (driverManagerProtection) { + DriverManager.getDrivers(); + } + + Thread currentThread = Thread.currentThread(); + ClassLoader loader = currentThread.getContextClassLoader(); + + try { + // Use the system classloader as the victim for all this + // ClassLoader pinning we're about to do. + currentThread.setContextClassLoader(ClassLoader.getSystemClassLoader()); + + /* + * Several components end up calling: sun.awt.AppContext.getAppContext() + * + * Those libraries / components known to trigger memory leaks due to eventual calls to getAppContext() + * are: - Google Web Toolkit via its use of javax.imageio - Batik - others TBD + * + * Note that a call to sun.awt.AppContext.getAppContext() results in a thread being started named + * AWT-AppKit that requires a graphical environment to be available. + */ + + // Trigger a call to sun.awt.AppContext.getAppContext(). This + // will pin the system class loader in memory but that shouldn't + // be an issue. + if (appContextProtection) { + ImageIO.getCacheDirectory(); + } + + /* + * Several components end up opening JarURLConnections without first disabling caching. This effectively + * locks the file. Whilst more noticeable and harder to ignore on Windows, it affects all operating + * systems. + * + * Those libraries/components known to trigger this issue include: - log4j versions 1.2.15 and earlier - + * javax.xml.bind.JAXBContext.newInstance() + * + * https://bugs.openjdk.java.net/browse/JDK-8163449 + * + * Disable caching for JAR URLConnections + */ + + // Set the default URL caching policy to not to cache + if (urlCacheProtection) { + URLConnection.setDefaultUseCaches("JAR", false); + } + + /* + * Initialize the SeedGenerator of the JVM, as some platforms use a thread which could end up being + * associated with a webapp rather than the container. + */ + if (initSeedGenerator) { + SecureRandom.getSeed(1); + } + + if (classesToInitialize != null) { + StringTokenizer strTok = new StringTokenizer(classesToInitialize, ", \r\n\t"); + while (strTok.hasMoreTokens()) { + String classNameToLoad = strTok.nextToken(); + try { + Class.forName(classNameToLoad); + } catch (ClassNotFoundException e) { + log.error(sm.getString("jreLeakListener.classToInitializeFail", classNameToLoad), e); + // continue with next class to load + } + } + } + + } finally { + currentThread.setContextClassLoader(loader); + } + } + } +} diff --git a/java/org/apache/catalina/core/LocalStrings.properties b/java/org/apache/catalina/core/LocalStrings.properties new file mode 100644 index 0000000..0524ad8 --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings.properties @@ -0,0 +1,336 @@ +# 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. + +applicationContext.addFilter.ise=Filters cannot be added to context [{0}] as the context has been initialised +applicationContext.addJspFile.iae=The JSP file [{0}] is not valid +applicationContext.addListener.iae.cnfe=Unable to create an instance of type [{0}] +applicationContext.addListener.iae.init=Unable to add an instance of type [{0}] as a listener +applicationContext.addListener.iae.sclNotAllowed=Once the first ServletContextListener has been called, no more ServletContextListeners may be added. +applicationContext.addListener.iae.wrongType=The type specified [{0}] is not one of the expected listener types +applicationContext.addListener.ise=Listeners cannot be added to context [{0}] as the context has been initialised +applicationContext.addRole.ise=Roles cannot be added to context [{0}] as the context has been initialised +applicationContext.addServlet.ise=Servlets cannot be added to context [{0}] as the context has been initialised +applicationContext.attributeEvent=Exception thrown by attributes event listener +applicationContext.illegalDispatchPath=An application attempted to obtain a request dispatcher with an illegal path [{0}] that was rejected because it contained an encoded directory traversal attempt +applicationContext.invalidFilterName=Unable to add filter definition due to invalid filter name [{0}]. +applicationContext.invalidServletName=Unable to add servlet definition due to invalid servlet name [{0}]. +applicationContext.lookup.error=Failed to locate resource [{0}] in context [{1}] +applicationContext.mapping.error=Error during mapping +applicationContext.requestDispatcher.iae=Path [{0}] does not start with a "/" character +applicationContext.resourcePaths.iae=Path [{0}] does not start with a "/" character +applicationContext.role.iae=An individual role to declare for context [{0}] may not be null nor the empty string +applicationContext.roles.iae=Array of roles to declare for context [{0}] cannot be null +applicationContext.setAttribute.namenull=Name cannot be null +applicationContext.setInitParam.ise=Initialization parameters cannot be set after the context has been initialized +applicationContext.setRequestEncoding.ise=The request encoding cannot be set for context [{0}] as the context has been initialised +applicationContext.setResponseEncoding.ise=The response encoding cannot be set for context [{0}] as the context has been initialised +applicationContext.setSessionTimeout.ise=The session timeout cannot be set for context [{0}] as the context has been initialised +applicationContext.setSessionTracking.iae.invalid=The session tracking mode [{0}] requested for context [{1}] is not supported by that context +applicationContext.setSessionTracking.iae.ssl=The session tracking modes requested for context [{0}] included SSL and at least one other mode. SSL may not be configured with other modes. +applicationContext.setSessionTracking.ise=The session tracking modes for context [{0}] cannot be set whilst the context is running + +applicationDispatcher.allocateException=Allocate exception for servlet [{0}] +applicationDispatcher.customResponse=The response class [{0}] could not be unwraped to the Catalina response class and will be closed immediately after the forward +applicationDispatcher.deallocateException=Deallocate exception for servlet [{0}] +applicationDispatcher.forward.ise=Cannot forward after response has been committed +applicationDispatcher.isUnavailable=Servlet [{0}] is currently unavailable +applicationDispatcher.serviceException=Servlet.service() for servlet [{0}] threw exception +applicationDispatcher.specViolation.request=Original ServletRequest or wrapped original ServletRequest not passed to RequestDispatcher in violation of SRV.8.2 and SRV.14.2.5.1 +applicationDispatcher.specViolation.response=Original ServletResponse or wrapped original ServletResponse not passed to RequestDispatcher in violation of SRV.8.2 and SRV.14.2.5.1 + +applicationFilterConfig.jmxRegisterFail=JMX registration failed for filter of type [{0}] and name [{1}] +applicationFilterConfig.jmxUnregister=JMX de-registration complete for filter of type [{0}] and name [{1}] +applicationFilterConfig.jmxUnregisterFail=JMX de-registration failed for filter of type [{0}] and name [{1}] +applicationFilterConfig.preDestroy=Failed the call to preDestroy for the filter named [{0}] of type [{1}] +applicationFilterConfig.release=Failed to destroy the filter named [{0}] of type [{1}] + +applicationFilterFactory.noFilterConfig=No filter configuration found for [{0}] + +applicationFilterRegistration.nullInitParam=Unable to set initialisation parameter for filter due to null name and/or value. Name [{0}], Value [{1}] +applicationFilterRegistration.nullInitParams=Unable to set initialisation parameters for filter due to null name and/or value. Name [{0}], Value [{1}] + +applicationHttpRequest.fragmentInDispatchPath=The fragment in dispatch path [{0}] has been removed +applicationHttpRequest.sessionEndAccessFail=Exception triggered ending access to session while recycling request + +applicationPushBuilder.methodInvalid=The HTTP method for a push request must be both cacheable and safe but [{0}] is not +applicationPushBuilder.methodNotToken=HTTP methods must be tokens but [{0}] contains a non-token character + +applicationServletRegistration.setServletSecurity.iae=Null constraint specified for servlet [{0}] deployed to context with name [{1}] +applicationServletRegistration.setServletSecurity.ise=Security constraints can''t be added to servlet [{0}] deployed to context with name [{1}] as the context has already been initialised + +applicationSessionCookieConfig.ise=Property [{0}] cannot be added to SessionCookieConfig for context [{1}] as the context has been initialised + +aprListener.FIPSProviderNotDefault=The FIPS provider must be configured as the default provider when the AprLifecycleListener is configured with FIPS mode [{0}] +aprListener.aprDestroy=Failed shutdown of the Apache Tomcat Native library +aprListener.aprInit=The Apache Tomcat Native library which allows using OpenSSL was not found on the java.library.path: [{0}] +aprListener.aprInitDebug=The Apache Tomcat Native library could not be found using names [{0}] on the java.library.path [{1}]. The errors reported were [{2}] +aprListener.aprInitError=The Apache Tomcat Native library failed to load. The error reported was [{0}] +aprListener.currentFIPSMode=Current FIPS mode: [{0}] +aprListener.enterAlreadyInFIPSMode=AprLifecycleListener is configured to force entering FIPS mode, but library is already in FIPS mode [{0}] +aprListener.initializeFIPSFailed=Failed to enter FIPS mode +aprListener.initializeFIPSSuccess=Successfully entered FIPS mode +aprListener.initializedOpenSSL=OpenSSL successfully initialized [{0}] +aprListener.initializingFIPS=Initializing FIPS mode... +aprListener.requireNotInFIPSMode=AprLifecycleListener is configured to require the library to already be in FIPS mode, but it was not in FIPS mode +aprListener.skipFIPSInitialization=Already in FIPS mode; skipping FIPS initialization. +aprListener.sslInit=Failed to initialize the SSLEngine. +aprListener.sslRequired=[{0}] is not a valid value for SSLEngine when using version [{1}] of the Tomcat Native library since SSL is required for version 2.x onwards. +aprListener.tcnInvalid=An incompatible version [{0}] of the Apache Tomcat Native library is installed, while Tomcat requires version [{1}] +aprListener.tcnValid=Loaded Apache Tomcat Native library [{0}] using APR version [{1}]. +aprListener.tcnVersion=An older version [{0}] of the Apache Tomcat Native library is installed, while Tomcat recommends a minimum version of [{1}] +aprListener.tooLateForFIPSMode=Cannot setFIPSMode: SSL has already been initialized +aprListener.tooLateForSSLEngine=Cannot setSSLEngine: SSL has already been initialized +aprListener.tooLateForSSLRandomSeed=Cannot setSSLRandomSeed: SSL has already been initialized +aprListener.usingFIPSProvider=Using OpenSSL with the FIPS provider as the default provider +aprListener.wrongFIPSMode=Unexpected value of FIPSMode option of AprLifecycleListener: [{0}] + +asyncContextImpl.afterOnError=A non-container (application) thread attempted to use the AsyncContext after an error had occurred and the call to AsyncListener.onError() had returned. This is not allowed to avoid race conditions. +asyncContextImpl.asyncDispatchError=Error during asynchronous dispatch +asyncContextImpl.asyncRunnableError=Error during processing of asynchronous Runnable via AsyncContext.start() +asyncContextImpl.dispatchingStarted=Asynchronous dispatch operation has already been called. Additional asynchronous dispatch operation within the same asynchronous cycle is not allowed. +asyncContextImpl.fireOnComplete=Firing onComplete() event for any AsyncListeners +asyncContextImpl.fireOnError=Firing onError() event for any AsyncListeners +asyncContextImpl.fireOnStartAsync=Firing onStartAsync() event for any AsyncListeners +asyncContextImpl.fireOnTimeout=Firing onTimeout() event for any AsyncListeners +asyncContextImpl.noAsyncDispatcher=The dispatcher returned from the ServletContext does not support asynchronous dispatching +asyncContextImpl.onCompleteError=onComplete() call failed for listener of type [{0}] +asyncContextImpl.onErrorError=onError() call failed for listener of type [{0}] +asyncContextImpl.onStartAsyncError=onStartAsync() call failed for listener of type [{0}] +asyncContextImpl.onTimeoutError=onTimeout() call failed for listener of type [{0}] +asyncContextImpl.request.ise=It is illegal to call getRequest() after complete() or any of the dispatch() methods has been called +asyncContextImpl.requestEnded=The request associated with the AsyncContext has already completed processing. +asyncContextImpl.response.ise=It is illegal to call getResponse() after complete() or any of the dispatch() methods has been called + +containerBase.backgroundProcess.cluster=Exception processing cluster [{0}] background process +containerBase.backgroundProcess.error=Exception processing background thread +containerBase.backgroundProcess.realm=Exception processing realm [{0}] background process +containerBase.backgroundProcess.valve=Exception processing valve [{0}] background process +containerBase.child.add=Add container child [{0}] to container [{1}] +containerBase.child.destroy=Error destroying child +containerBase.child.notUnique=Child name [{0}] is not unique +containerBase.child.start=Error starting child +containerBase.child.stop=Error stopping child +containerBase.cluster.start=Error starting new cluster +containerBase.cluster.stop=Error stopping old cluster +containerBase.nullName=Container name cannot be null +containerBase.realm.start=Error starting new realm +containerBase.realm.stop=Error stopping old realm +containerBase.threadedStartFailed=A child container failed during start +containerBase.threadedStopFailed=A child container failed during stop + +contextNamingInfoListener.envEntry=Adding context env entry [{0}] with value [{1}] + +defaultInstanceManager.invalidAnnotation=Invalid [{0}] annotation +defaultInstanceManager.invalidInjection=Invalid method resource injection annotation +defaultInstanceManager.postConstructNotFound=Post construct method [{0}] for class [{1}] is declared in deployment descriptor but cannot be found +defaultInstanceManager.preDestroyNotFound=Pre destroy method [{0}] for class [{1}] is declared in deployment descriptor but cannot be found +defaultInstanceManager.restrictedClass=Access to class [{0}] is forbidden. It is a restricted class. A web application must be configured as privileged to be able to load it +defaultInstanceManager.restrictedContainerServlet=Access to class [{0}] is forbidden. It is a restricted class (implements ContainerServlet interface). A web application must be configured as privileged to be able to load it +defaultInstanceManager.restrictedFiltersResource=Restricted filters property file not found [{0}] +defaultInstanceManager.restrictedListenersResource=Restricted listeners property file not found [{0}] +defaultInstanceManager.restrictedServletsResource=Restricted servlets property file not found [{0}] +defaultInstanceManager.restrictedWrongValue=Wrong value in restricted classes property file [{0}] for class name [{1}]. Expected value: [restricted], actual value: [{2}] + +filterChain.filter=Filter execution threw an exception +filterChain.servlet=Servlet execution threw an exception + +jniLifecycleListener.bothPathAndName=Either libraryName or libraryPath may be set, not both +jniLifecycleListener.load.name=Loaded native library [{0}] +jniLifecycleListener.load.path=Loaded native library from [{0}] +jniLifecycleListener.missingPathOrName=One of libraryName or libraryPath must be set + +jreLeakListener.classToInitializeFail=Failed to load class [{0}] during Tomcat start to prevent possible memory leaks. + +listener.notContext=This listener must only be nested within Context elements, but is in [{0}]. +listener.notServer=This listener must only be nested within Server elements, but is in [{0}]. + +naming.addEnvEntry=Adding environment entry [{0}] +naming.addResourceEnvRef=Adding resource env ref [{0}] +naming.addResourceLink=Adding resource link [{0}] +naming.addResourceRef=Adding resource reference [{0}] at [{1}] +naming.addService=Adding service reference [{0}] at [{1}] +naming.addSlash=Changing service URL to [/{0}] +naming.bind=Bind naming context to container [{0}] +naming.bindFailed=Failed to bind object: [{0}] +naming.invalidEnvEntryType=Environment entry [{0}] has an invalid type +naming.invalidEnvEntryValue=Environment entry [{0}] has an invalid value +naming.jmxRegistrationFailed=Failed to register in JMX: [{0}] +naming.namingContextCreationFailed=Creation of the naming context failed: [{0}] +naming.unbindFailed=Failed to unbind object: [{0}] +naming.wsdlFailed=Failed to find wsdl file: [{0}] + +noPluggabilityServletContext.notAllowed=Section 4.4 of the Servlet 3.0 specification does not permit this method to be called from a ServletContextListener that was not defined in web.xml, a web-fragment.xml file nor annotated with @WebListener + +openssllistener.destroy=OpenSSL shutdown failed +openssllistener.initializeFIPSFailed=Failed entering FIPS mode +openssllistener.java22=The FFM API from Java 22 is not available, using OpenSSL requires Apache Tomcat Native +openssllistener.sslInit=OpenSSL initialization failed + +propertiesRoleMappingListener.linkedRole=Successfully linked application role [{0}] to technical role [{1}] +propertiesRoleMappingListener.linkedRoleCount=Linked [{0}] application roles to technical roles +propertiesRoleMappingListener.roleMappingFileEmpty=Role mapping file cannot be empty +propertiesRoleMappingListener.roleMappingFileFail=Failed to load role mapping file [{0}] +propertiesRoleMappingListener.roleMappingFileNull=Role mapping file cannot be null + +pushBuilder.noPath=It is illegal to call push() before setting a path + +standardContext.applicationListener=Error configuring application listener of class [{0}] +standardContext.applicationSkipped=Skipped installing application listeners due to previous error(s) +standardContext.backgroundProcess.instanceManager=Exception processing instance manager [{0}] background process +standardContext.backgroundProcess.loader=Exception processing loader [{0}] background process +standardContext.backgroundProcess.manager=Exception processing manager [{0}] background process +standardContext.backgroundProcess.resources=Exception processing resources [{0}] background process +standardContext.cluster.managerError=Error creating new cluster session manager +standardContext.cluster.noManager=No manager found. Checking if cluster manager should be used. Cluster configured: [{0}], Application distributable: [{1}] +standardContext.configurationFail=One or more components marked the context as not correctly configured +standardContext.cookieProcessor.null=It is not permitted to set the CookieProcessor for a Context to null +standardContext.createWrapper.containerListenerError=Error creating container listener for the wrapper +standardContext.createWrapper.error=Error creating new wrapper +standardContext.createWrapper.listenerError=Error creating lifecycle listener for the wrapper +standardContext.duplicateListener=The listener [{0}] is already configured for this context. The duplicate definition has been ignored. +standardContext.errorPage.error=Error page location [{0}] must start with a ''/'' +standardContext.errorPage.required=ErrorPage cannot be null +standardContext.errorPage.warning=WARNING: Error page location [{0}] must start with a ''/'' in Servlet 2.4 +standardContext.filterFail=One or more Filters failed to start. Full details will be found in the appropriate container log file +standardContext.filterMap.either=Filter mapping must specify either a or a +standardContext.filterMap.name=Filter mapping specifies an unknown filter name [{0}] +standardContext.filterMap.pattern=Invalid [{0}] in filter mapping +standardContext.filterStart=Exception starting filter [{0}] +standardContext.invalidWrapperClass=[{0}] is not a subclass of StandardWrapper +standardContext.isUnavailable=This application is not currently available +standardContext.listenerFail=One or more listeners failed to start. Full details will be found in the appropriate container log file +standardContext.listenerStart=Exception sending context initialized event to listener instance of class [{0}] +standardContext.listenerStop=Exception sending context destroyed event to listener instance of class [{0}] +standardContext.loadOnStartup.loadException=Servlet [{1}] in web application [{0}] threw load() exception +standardContext.loginConfig.errorPage=Form error page [{0}] must start with a ''/'' +standardContext.loginConfig.errorWarning=WARNING: Form error page [{0}] must start with a ''/'' in Servlet 2.4 +standardContext.loginConfig.loginPage=Form login page [{0}] must start with a ''/'' +standardContext.loginConfig.loginWarning=WARNING: Form login page [{0}] must start with a ''/'' in Servlet 2.4 +standardContext.loginConfig.required=LoginConfig cannot be null +standardContext.manager=Configured a manager of class [{0}] +standardContext.managerFail=The session manager failed to start +standardContext.namingResource.destroy.fail=Failed to destroy old naming resources +standardContext.namingResource.init.fail=Failed to init new naming resources +standardContext.notStarted=Context with name [{0}] has not yet been started +standardContext.notWrapper=Child of a Context must be a Wrapper +standardContext.parameter.duplicate=Duplicate context initialization parameter [{0}] +standardContext.parameter.required=Both parameter name and parameter value are required +standardContext.pathInvalid=A context path must either be an empty string or start with a ''/'' and do not end with a ''/''. The path [{0}] does not meet these criteria and has been changed to [{1}] +standardContext.postconstruct.duplicate=Duplicate post construct method definition for class [{0}] +standardContext.postconstruct.required=Both fully qualified class name and method name are required +standardContext.predestroy.duplicate=Duplicate @PreDestroy method definition for class [{0}] +standardContext.predestroy.required=Both fully qualified class name and method name are required +standardContext.reloadingCompleted=Reloading Context with name [{0}] is completed +standardContext.reloadingStarted=Reloading Context with name [{0}] has started +standardContext.requestListener.requestDestroyed=Exception sending request destroyed lifecycle event to listener instance of class [{0}] +standardContext.requestListener.requestInit=Exception sending request initialized lifecycle event to listener instance of class [{0}] +standardContext.resetContextFail=Error resetting Context with name [{0}] +standardContext.resourcesInit=Error initializing static Resources +standardContext.resourcesStart=Error starting static Resources +standardContext.resourcesStop=Error stopping static Resources +standardContext.sciFail=Error during ServletContainerInitializer processing +standardContext.securityConstraint.mixHttpMethod=It is not permitted to mix and in the same web resource collection +standardContext.securityConstraint.pattern=Invalid [{0}] in security constraint +standardContext.servletFail=One or more Servlets failed to load on startup. Full details will be found in the appropriate container log file +standardContext.servletMap.name=Servlet mapping specifies an unknown servlet name [{0}] +standardContext.servletMap.pattern=Invalid [{0}] in servlet mapping +standardContext.setLoader.start=Error starting new loader +standardContext.setLoader.stop=Error stopping old loader +standardContext.setManager.start=Error starting new manager +standardContext.setManager.stop=Error stopping old manager +standardContext.startFailed=Context [{0}] startup failed due to previous errors +standardContext.startingContext=Exception starting Context with name [{0}] +standardContext.stop.asyncWaitInterrupted=Interrupt received while waiting unloadDelay milliseconds for in-flight asynchronous requests to complete. Context stop will continue without further delay. +standardContext.stoppingContext=Exception stopping Context with name [{0}] +standardContext.suspiciousUrl=Suspicious URL pattern: [{0}] in context [{1}], see sections 12.1 and 12.2 of the Servlet specification +standardContext.threadBindingListenerError=An error occurred in the thread binding listener configured for Context [{0}] +standardContext.urlPattern.patternWarning=WARNING: URL pattern [{0}] must start with a ''/'' in Servlet 2.4 +standardContext.workCreateException=Failed to determine absolute work directory from directory [{0}] and CATALINA_HOME [{1}] for context [{2}] +standardContext.workCreateFail=Failed to create work directory [{0}] for context [{1}] +standardContext.workPath=Exception obtaining work path for context [{0}] + +standardContextValve.acknowledgeException=Failed to acknowledge request with a 100 (Continue) response + +standardEngine.notHost=Child of an Engine must be a Host +standardEngine.notParent=Engine cannot have a parent Container +standardEngine.start=Starting Servlet engine: [{0}] + +standardHost.clientAbort=Remote Client Aborted Request, IOException: [{0}] +standardHost.invalidErrorReportValveClass=Couldn''t load specified error report valve class: [{0}] +standardHost.noContext=No Context configured to process this request +standardHost.notContext=Child of a Host must be a Context +standardHost.nullName=Host name is required +standardHost.problematicAppBase=Using an empty string for appBase on host [{0}] will set it to CATALINA_BASE, which is a bad idea +standardHost.problematicLegacyAppBase=Using an empty string for legacyAppBase on host [{0}] will set it to CATALINA_BASE, which is a bad idea + +standardHostValve.customStatusFailed=Custom error page [{0}] could not be dispatched correctly +standardHostValve.exception=Exception Processing [{0}] + +standardPipeline.basic.start=Error starting new basic Valve +standardPipeline.basic.stop=Error stopping old basic Valve +standardPipeline.valve.destroy=Error destroying Valve +standardPipeline.valve.start=Error starting Valve +standardPipeline.valve.stop=Error stopping Valve + +standardServer.accept.error=An IO exception occurred trying to accept on the socket listening for the shutdown command +standardServer.accept.readError=An IO exception occurred trying to read the shutdown command +standardServer.accept.security=A security error occurred trying to accept on the socket listening for the shutdown command +standardServer.accept.timeout=The socket listening for the shutdown command experienced an unexpected timeout [{0}] milliseconds after the call to accept(). Is this an instance of bug 56684? +standardServer.awaitSocket.fail=Failed to create server shutdown socket on address [{0}] and port [{1}] (base port [{2}] and offset [{3}]) +standardServer.invalidShutdownCommand=Invalid shutdown command [{0}] received +standardServer.periodicEventError=Error sending periodic event +standardServer.portOffset.invalid=The value [{0}] for portOffset is not valid as portOffset may not be negative +standardServer.shutdownViaPort=A valid shutdown command was received via the shutdown port. Stopping the Server instance. +standardServer.storeConfig.contextError=Error storing context [{0}] configuration +standardServer.storeConfig.error=Error storing server configuration +standardServer.storeConfig.notAvailable=No StoreConfig implementation was registered as an MBean named [{0}] so no configuration could be saved. A suitable MBean is normally registered via the StoreConfigLifecycleListener. + +standardService.engine.startFailed=Failed to start associated Engine +standardService.engine.stopFailed=Failed to stop associated Engine +standardService.executor.start=Error starting new executor +standardService.executor.stop=Error stopping old executor +standardService.mapperListener.startFailed=Failed to start associated MapperListener +standardService.mapperListener.stopFailed=Failed to stop associated MapperListener +standardService.start.name=Starting service [{0}] +standardService.stop.name=Stopping service [{0}] + +standardThreadExecutor.notStarted=The executor has not been started + +standardVirtualThreadExecutor.noVirtualThreads=Virtual threads require a minimum Java version of Java 21 +standardVirtualThreadExecutor.notStarted=The executor has not been started + +standardWrapper.allocate=Error allocating a servlet instance +standardWrapper.allocateException=Allocate exception for servlet [{0}] +standardWrapper.deallocateException=Deallocate exception for servlet [{0}] +standardWrapper.destroyException=Servlet.destroy() for servlet [{0}] threw exception +standardWrapper.destroyInstance=InstanceManager.destroy() for servlet [{0}] threw exception +standardWrapper.initException=Servlet.init() for servlet [{0}] threw exception +standardWrapper.instantiate=Error instantiating servlet class [{0}] +standardWrapper.isUnavailable=Servlet [{0}] is currently unavailable +standardWrapper.jspMonitorError=Error registering JSP monitor Mbean [{0}] +standardWrapper.notChild=Wrapper container may not have child containers +standardWrapper.notClass=No servlet class has been specified for servlet [{0}] +standardWrapper.notContext=Parent container of a Wrapper must be a Context +standardWrapper.notFound=Servlet [{0}] is not available +standardWrapper.notServlet=Class [{0}] is not a Servlet +standardWrapper.serviceException=Servlet.service() for servlet [{0}] in context with path [{1}] threw exception +standardWrapper.serviceExceptionRoot=Servlet.service() for servlet [{0}] in context with path [{1}] threw exception [{2}] with root cause +standardWrapper.unavailable=Marking servlet [{0}] as unavailable +standardWrapper.unloadException=Servlet [{0}] threw unload() exception +standardWrapper.unloading=Cannot allocate servlet [{0}] because it is being unloaded +standardWrapper.waiting=Waiting for [{0}] instance(s) to be deallocated for Servlet [{1}] + +threadLocalLeakPreventionListener.containerEvent.error=Exception processing container event [{0}] +threadLocalLeakPreventionListener.lifecycleEvent.error=Exception processing lifecycle event [{0}] diff --git a/java/org/apache/catalina/core/LocalStrings_cs.properties b/java/org/apache/catalina/core/LocalStrings_cs.properties new file mode 100644 index 0000000..81c77bb --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_cs.properties @@ -0,0 +1,57 @@ +# 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. + +applicationContext.addJspFile.iae=JSP soubor [{0}] je neplatný +applicationContext.addListener.iae.cnfe=Nelze vytvoÅ™it instanci typu [{0}] +applicationContext.addListener.iae.wrongType=Specifikovaný typ [{0}] není mezi oÄekávanými typy listenerem +applicationContext.addRole.ise=Role nelze pÅ™idat do contextu [{0}], neboÅ¥ byl již inicializován +applicationContext.setAttribute.namenull=Jméno nemůže být null +applicationContext.setSessionTracking.ise=Sledovací mód session pro kontext [{0}] nelze nastavit, neboÅ¥ kontext právÄ› běží + +applicationDispatcher.specViolation.response=Původní ServletResponse nebo zapouzdÅ™ený původní ServletResponse nebyl pÅ™edán do RequestDispatcher, což je poruÅ¡ení SRV.8.2 a SRV.14.2.5.1 + +applicationFilterRegistration.nullInitParams=Není možné nastavit inicializaÄní parametry pro filtr kvůli hodnotÄ› null ve jménu Äi hodnotÄ›. Jméno [{0}], Hodnota [{1}] + +applicationHttpRequest.sessionEndAccessFail=Výjimka vyvolala ukonÄení přístupu k session bÄ›hem recykllování dotazu + +aprListener.initializingFIPS=Inicializace FIPS módu... + +containerBase.backgroundProcess.cluster=Výjimka pÅ™i zpracování procesu na pozadí v clusteru [{0}] + +defaultInstanceManager.invalidInjection=Neplatá anotace pro vložení zdroje (resource) na metodÄ› +defaultInstanceManager.restrictedContainerServlet=Přístup ke třídÄ› [{0}] je zakázán. Jedná se o exkluzivní třítu (implementující interface ContainerServlet). Webová aplikace musí mít nakonfigurované potÅ™ebné právo (pro nahrání této třídy). +defaultInstanceManager.restrictedFiltersResource=Property soubor pro omezující filtry nenalezen [{0}] +defaultInstanceManager.restrictedListenersResource=Property soubor Restricted listeners nebyl nalezen [{0}] + +filterChain.filter=Zpracování filtru vyhodilo výjimku + +naming.addEnvEntry=PÅ™idání promÄ›nné prostÅ™edí [{0}] +naming.namingContextCreationFailed=Selhalo vytvoÅ™ení jmenného kontextu: [{0}] +naming.wsdlFailed=Selhalo nalezení WSDL souboru: [{0}] + +standardContext.filterStart=Zahajuji filtr výjimek [{0}] +standardContext.invalidWrapperClass=[{0}] není odvozená třída od StandardWrapper +standardContext.listenerStart=Výjimka pÅ™i odesílání události inicializace kontextu do instance listeneru třídy [{0}] +standardContext.loginConfig.errorPage=Chyba formuláře stránky [{0}] - musí zaÄínat znakem ''/'' +standardContext.loginConfig.errorWarning=WARNING: Chybová stránka formuláře [{0}] musí zaÄínat znakem ''/'' v rámci Servlet 2.4 +standardContext.parameter.duplicate=Duplikovaný inicializaÄní parametr kontextu [{0}] +standardContext.securityConstraint.mixHttpMethod=Není povoleno míchat a v rámci jedné kolekce webových zdrojů +standardContext.securityConstraint.pattern=Neplatný [{0}] v bezpeÄnostním omezení + +standardServer.storeConfig.notAvailable=Není registrována žádná implementace StoreConfig jako MBean pod jménem [{0}], takže konfiguraci nelze uložit. Vhodná MBean je normálnÄ› registrována pomocí the StoreConfigLifecycleListener. + +standardWrapper.isUnavailable=Servlet [{0}] není aktuálnÄ› dostupný +standardWrapper.notChild=ZapouzdÅ™ující kontejner nemůže obsahovat podřízené kontejnery +standardWrapper.unloading=Nelze alokovat servlet [{0}], protože je uvolňován diff --git a/java/org/apache/catalina/core/LocalStrings_de.properties b/java/org/apache/catalina/core/LocalStrings_de.properties new file mode 100644 index 0000000..5817fc5 --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_de.properties @@ -0,0 +1,60 @@ +# 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. + +applicationContext.addJspFile.iae=Die JSP Datei [{0}] ist nicht gültig. +applicationContext.addListener.iae.cnfe=Eine Instanz vom Typ [{0}] konnte nicht erzeugt werden +applicationContext.addListener.iae.wrongType=Der spezifierte Typ [{0}] ist keiner der erwarteten Typen für einen Listener +applicationContext.addRole.ise=Es können keine Rollen zu dem Context [{0}] hinzugefügt werden, da er nicht initialisiert wurde +applicationContext.invalidServletName=Kann Servlet-Definition nicht hinzunehmen, da der Servlet Name [{0}] ungültig ist. +applicationContext.mapping.error=Fehler beim Mappen +applicationContext.setAttribute.namenull=Der Name darf nicht 'null' sein. + +applicationFilterRegistration.nullInitParams=Kann Initialisierungs Parameter für Filter nicht setzen, da Name oder Wert null sind. Name [{0}], Wert [{0}] + +aprListener.initializingFIPS=FIPS-Modus wird initialisiert... + +containerBase.backgroundProcess.cluster=Ausnahme beim Abarbeiten des Cluster [{0}] Hintergrundprozesses +containerBase.backgroundProcess.error=Während der Hintergrund Thread abgearbeitet wurde trat eine Exception auf + +defaultInstanceManager.invalidInjection=Ungültige ressource injection Annotation + +filterChain.filter=Während der Filter Bearbeitung trat eine Exception auf + +naming.addEnvEntry=Füge Umgebungseintrag [{0}] hinzu +naming.namingContextCreationFailed=Erzeugung des Naming-Contexts ist fehlgeschlagen: [{0}] +naming.wsdlFailed=Die WSDL Datei [{0}] wurde nicht gefunden. + +standardContext.errorPage.required=ErrorPage darf nicht Null sein +standardContext.filterStart=Ausnahme beim Starten des Filters [{0}] +standardContext.invalidWrapperClass=[{0}] ist keine Unterklasse von StandardWrapper +standardContext.isUnavailable=Die Anwendung ist derzeit nicht verfügbar +standardContext.listenerStart=Fehler beim Senden der ''Context Initialized'' Benachrichtigung an den Listener aus der Klasse [{0}] +standardContext.loginConfig.errorPage=Formularfehlerseite [{0}] muss mit einem ''/'' beginnen +standardContext.loginConfig.errorWarning=WARNING: Form Fehler Seite [{0}] muss bei Servlet 2.4 mit einem ''/'' starten +standardContext.manager=Habe einen Manager der Klasse [{0}] konfiguriert +standardContext.managerFail=Der Start des Session-Managers schlug fehl +standardContext.notStarted=Context mit Name [{0}] wurde noch nicht gestartet +standardContext.parameter.duplicate=Doppelter Parameter [{0}] zur Kontext Initialisierung +standardContext.securityConstraint.mixHttpMethod= und dürfen nicht in derselben Web-Ressource-Kollektion verwendet werden +standardContext.securityConstraint.pattern=Ungültiges [{0}] im Security Constraint +standardContext.startingContext=Ausnahme beim Starten des Kontextes [{0}] + +standardEngine.notParent=Engine kann keinen Eltern-Container haben + +standardHost.nullName=Hostname wird benötigt + +standardWrapper.isUnavailable=Das Servlet [{0}] ist zur Zeit nicht verfügbar +standardWrapper.notFound=Servlet [{0}] ist nicht verfügbar +standardWrapper.unloading=Das Servlet [{0}] kann nicht allokiert werden, weil es entladen wurde diff --git a/java/org/apache/catalina/core/LocalStrings_es.properties b/java/org/apache/catalina/core/LocalStrings_es.properties new file mode 100644 index 0000000..2ba6d5b --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_es.properties @@ -0,0 +1,179 @@ +# 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. + +applicationContext.addFilter.ise=No se pueden añadir filtros al contexto [{0}] ya que éste ha sido inicializado +applicationContext.addJspFile.iae=El archivo JSP [{0}] no es válido +applicationContext.addListener.iae.cnfe=No puedo crear una instancia del tipo [{0}] +applicationContext.addListener.iae.sclNotAllowed=Una vez que el primer ServletContextListener ha sido llamado, no se pueden añadir más ServletContextListeners. +applicationContext.addListener.iae.wrongType=El tipo especificado [{0}] no es uno de los tipos de escuchador esperados +applicationContext.addListener.ise=No se pueden añadir escuchadores al contexto [{0}], una vez que ha sido inicializado. +applicationContext.addRole.ise=No se pueden añadir roles al contexto [{0}], una vez que ha sido inicializado. +applicationContext.addServlet.ise=No se pueden añadir servlets al contexto [{0}], una vez que ha sido inicializado. +applicationContext.attributeEvent=Excepción lanzada por escuchador de eventos de atributos +applicationContext.invalidServletName=Incapaz de añadir la definición servlet devido a que el nombre servlet no es válido [{0}]. +applicationContext.lookup.error=No pude localizar el recurso [{0}] en el contexto [{1}] +applicationContext.mapping.error=Error durante mapeo +applicationContext.requestDispatcher.iae=La Trayectoria [{0}] no comienza con carácter "/" +applicationContext.resourcePaths.iae=La Trayectoria [{0}] no comienza con carácter "/" +applicationContext.role.iae=Un rol individual que se ha de declarar para el contexto [{0}] no puede ser nulo o cadena vacía +applicationContext.roles.iae=Un arreglo de roles que se ha de declarar para el contexto [{0}] no puede ser nulo o cadena vacía +applicationContext.setAttribute.namenull=El nombre no puede ser nulo +applicationContext.setInitParam.ise=No es posible establecer parámetros de inicialización luego de la inicialización del contexto +applicationContext.setResponseEncoding.ise=Imposible establecer codificación de respuesta para contexto [{0}], el contexto ya ha sido inicializado +applicationContext.setSessionTimeout.ise=El timeout de la sesión no pudo ser fijado para el contexto [{0}] debido a que el contexto ha sido inicializado +applicationContext.setSessionTracking.iae.invalid=El modo de seguimiento de sesión [{0}] requerido para el contexto [{1}] no está soportado por este contexto +applicationContext.setSessionTracking.iae.ssl=Los modos de seguimiento de sesión requeridos para el contexto [{0}], incluyó SSL y al menos otro modo. SSL no se puede configurar con otros modos. +applicationContext.setSessionTracking.ise=No se pueden poner los modos de seguimiento de sesión para el contexto [{0}] mientras el contexto se está ejecutando. + +applicationDispatcher.allocateException=Excepción de reserva de espacio para servlet [{0}] +applicationDispatcher.deallocateException=Excepción de recuperación de espacio para servlet [{0}] +applicationDispatcher.forward.ise=No puedo reenviar después de que la respuesta se haya llevado a cabo. +applicationDispatcher.isUnavailable=El Servlet [{0}] no está disponible en este momento +applicationDispatcher.serviceException=El Servlet.service() para servlet [{0}] lanzó una excepción +applicationDispatcher.specViolation.request=SevletRequest original o ServletRequest original arropado no pasó a RequestDispatcher en violación de SRV.8.2 y SRV.14.2.5.1 +applicationDispatcher.specViolation.response=SevletResponse original o ServletResponse original arropado no pasó a RequestDispatcher en violación de SRV.8.2 y SRV.14.2.5.1 + +applicationFilterConfig.jmxRegisterFail=Ha fallado el registro JMX para el filtro del tipo [{0}] y nombre [{1}] +applicationFilterConfig.jmxUnregister=Se ha completado el desregistro JMX para el filtro del tipo [{0}] y nombre [{1}] +applicationFilterConfig.jmxUnregisterFail=Ha fallado el desregistro JMX para el filtro del tipo [{0}] y nombre [{1}] + +applicationFilterRegistration.nullInitParam=No puedo poner el parámetro de inicialización para el filtro debido a un nombre nulo y/o valor. Nombre [{0}], Valor [{1}] +applicationFilterRegistration.nullInitParams=No puedo poner los parámetros de inicialización para el filtro debido a un nombre nulo y/o valor. Nombre [{0}], Valor [{1}] + +applicationHttpRequest.sessionEndAccessFail=Excepción disparada acabando acceso a sesión mientras se reciclaba el requerimiento + +applicationServletRegistration.setServletSecurity.iae=Se ha especificado restricción Null para el servlet [{0}] desplegado en el contexto con el nombre [{1}] +applicationServletRegistration.setServletSecurity.ise=No se pueden añadir restricciones de seguridad al servlet [{0}] desplegado en el contexto con el nombre [{1}] ya que el contexto ya ha sido inicializado. + +aprListener.aprDestroy=No pude apagar la biblioteca nativa de Apache Tomcat +aprListener.aprInit=La biblioteca nativa de Apache Tomcat basada en ARP que permite un rendimiento óptimo en entornos de desarrollo no ha sido hallada en java.library.path: [{0}] +aprListener.initializedOpenSSL=OpenSSL inicializado correctamente [{0}] +aprListener.initializingFIPS=Inicializando modo FIPS... +aprListener.sslInit=No pude inicializar el SSLEngine (Motor SSL) +aprListener.tcnInvalid=Se encuentra instalada una versión incompatible [{0}] de la biblioteca nativa APR de Apache Tomcat, mientras que Tomcat necesita la versión [{1}] +aprListener.tcnValid=Cargada la biblioteca nativa APR de Apache Tomcat [{0}] con la versión APR [{1}]. +aprListener.tcnVersion=Se encuentra instalada una versión muy vieja [{0}] de la biblioteca nativa APR de Apache Tomcat, mientras que Tomcat recomienda una versión mayor de [{1}] +aprListener.tooLateForFIPSMode=No se pudo fijar setFIPSMode: SSL ya ha sido inicializado + +asyncContextImpl.requestEnded=El requerimiento asociado con AsyncContext ya ha completado su procesamiento. + +containerBase.backgroundProcess.cluster=Excepción al procesar clúster [{0}] de proceso en segundo plano +containerBase.backgroundProcess.realm=Excepción al procesar reino [{0}] de proceso en segundo plano +containerBase.backgroundProcess.valve=Excepción al procesar válvula [{0}] de proceso en segundo plano + +defaultInstanceManager.invalidInjection=Método inválido para el recurso de inserción de anotación +defaultInstanceManager.restrictedContainerServlet=El acceso a la clase [{0}] esta prohibido. Esta es una clase restringida ( implementa la interface ContainerServlet ). Una applicación web debe ser configurada como privilegiada para ser capaz de cargarla +defaultInstanceManager.restrictedFiltersResource=No se ha hallado el fichero de propiedades restringidas de filtros +defaultInstanceManager.restrictedListenersResource=No se ha hallado el fichero de propiedades restringidas de escuchadores +defaultInstanceManager.restrictedServletsResource=No se ha hallado el fichero de propiedades restringidas de servlets + +filterChain.filter=La ejecución del Filtro lanzó una excepción +filterChain.servlet=La ejecución del Servlet lanzó una excepción + +naming.addEnvEntry=Adicionando entrada de ambiente [{0}]\n +naming.bindFailed=No pude cambiar (bind) objeto: [{0}] +naming.invalidEnvEntryType=La entrada de Entorno [{0}] tiene un tipo inválido +naming.invalidEnvEntryValue=La entrada de Entorno [{0}] tiene un valor inválido +naming.jmxRegistrationFailed=No pude registrar en JMX: [{0}] +naming.namingContextCreationFailed=Falló la creación del contexto de nombres (naming): [{0}] +naming.unbindFailed=No pude descambiar (unbind) objecto: [{0}] +naming.wsdlFailed=No pude hallar fichero wsdl: [{0}] + +standardContext.applicationListener=Error configurando escuchador de aplicación de clase [{0}] +standardContext.applicationSkipped=Se ha saltado la instalación de escuchadores de aplicación debido a error(es) previo(s) +standardContext.backgroundProcess.loader=Excepción al procesar cargador [{0}] de proceso en segundo plano +standardContext.backgroundProcess.manager=Excepción al procesar gestor [{0}] de proceso en segundo plano +standardContext.cluster.noManager=No se ha hallado el gestor. Revisando si hay que usar el gestor de clúster. Clúster configurado: [{0}], Aplicación distribuíble: [{1}] +standardContext.duplicateListener=El escuchador [{0}] ya está configurado para este contexto. La definición duplicada ha sido ignorada. +standardContext.errorPage.error=La localización de la página de error [{0}] debe de comenzar con ''/'' +standardContext.errorPage.required=ErrorPage no puede ser nulo +standardContext.errorPage.warning=AVISO: La localización de la página de error [{0}] debe de comenzar con ''/'' en Servlet 2.4 +standardContext.filterMap.either=El mapeo de filtro debe de especificar o un o un +standardContext.filterMap.name=El mapeo de filtro especifica un nombre desconocido de filtro [{0}] +standardContext.filterMap.pattern= [{0}] inválido en mapeo de filtro +standardContext.filterStart=Excepción arrancando filtro [{0}] +standardContext.invalidWrapperClass=[{0}] no es una subclase de StandardWrapper +standardContext.isUnavailable=Esta aplicación no está disponible en este momento +standardContext.listenerStart=Excepción al enviar evento inicializado de contexto a instancia de escuchador de clase [{0}] +standardContext.listenerStop=Excepción enviando evento de contexto destruído a instancia de escuchador de clase [{0}] +standardContext.loginConfig.errorPage=La Página de error de Formulario [{0}] debe de comenzar con ''/'' +standardContext.loginConfig.errorWarning=AVISO: La página de error de Formulario [{0}] debe de comenzar con ''/'' en Servlet 2.4 +standardContext.loginConfig.loginPage=La página de login de Formulario [{0}] debe de comenzar con ''/'' +standardContext.loginConfig.loginWarning=AVISO: La página de login de Formulario [{0}] debe de comenzar con ''/'' en Servlet 2.4 +standardContext.loginConfig.required=LoginConfig no puede ser nula +standardContext.manager=Configurado un gestor de la clase [{0}] +standardContext.managerFail=El manejador de sesiones falló al iniciar +standardContext.namingResource.destroy.fail=No pude destruir recursos de viejo nombre +standardContext.namingResource.init.fail=No pude inicializar recursos de nuevo nombre +standardContext.notStarted=Aún no se ha arrancado el Contexto [{0}] +standardContext.notWrapper=El Hijo de un Contexto debe de ser un Arropador (Wrapper) +standardContext.parameter.duplicate=Duplicado parámetro de inicialización de contexto [{0}] +standardContext.parameter.required=Es necesario poner nombre de parámetro y valor de parámetro +standardContext.pathInvalid=Una ruta de contexto debe de ser o una cadena vacía o comenzar con "/". La ruta [{0}] no cumple con estos criterios y ha sido cambiada por [{1}] +standardContext.predestroy.duplicate=Definición duplicada de método @PreDestroy para clase [{0}] +standardContext.reloadingCompleted=Se ha completado la recarga de este Contexto +standardContext.reloadingStarted=Ha comenzado la recarga de Contexto [{0}] +standardContext.requestListener.requestInit=Una excepción durante el envío de requerimiento ha iniciado un evento de ciclo de vida (lifecycle event) para la instancia de clase a la escucha (listener) [{0}] +standardContext.resourcesStart=Error arrancando Recursos estáticos +standardContext.securityConstraint.mixHttpMethod=No está permitido mezclar y en la misma colección de recursos web +standardContext.securityConstraint.pattern= [{0}] inválida en restricción de seguridad +standardContext.servletMap.name=El mapeo de Servlet especifica un nombre de servlet desconocido [{0}] +standardContext.servletMap.pattern= [{0}] inválida en mapeo de servlet +standardContext.startFailed=Falló en arranque del Contexto [{0}] debido a errores previos +standardContext.startingContext=Excepción arrancando Contexto con nombre [{0}] +standardContext.stoppingContext=Excepción parando Context [{0}] +standardContext.urlPattern.patternWarning=AVISO: el patrón URL [{0}] debe de comenzar con ''/'' en Servlet 2.4 +standardContext.workCreateException=No pude determinar directorio absoluto de trabajo a partir del directorio [{0}] y CATALINA_HOME [{1}] para el contexto [{2}] +standardContext.workCreateFail=No pude crear el directorio de trabajo [{0}] para el contexto [{1}] +standardContext.workPath=Excepción obteniendo ruta de trabajo para el contexto [{0}] + +standardContextValve.acknowledgeException=No pude reconocer el requerimiento con una respuesta 100 (Continuar) + +standardEngine.notHost=El Hijo de un Motor debe de ser un Máquina +standardEngine.notParent=El Motor no puede tener un Contenedor padre + +standardHost.clientAbort=El Cliente Remoto Abortó el Requerimiento, IOException: [{0}] +standardHost.invalidErrorReportValveClass=No pude cargar clase especifiada de válvula de informe de error: [{0}] +standardHost.noContext=No se ha configurado Contexto para procesar este requerimiento +standardHost.notContext=El Hijo de una Máquina debe de ser un Contexto +standardHost.nullName=Es necesario poner el nombre de Máquina + +standardServer.shutdownViaPort=Se ha recibido un comando de apagado a través del puerto de apagado. Parando la instancia del Servidor. +standardServer.storeConfig.notAvailable=No se registró ninguna implementación de StoreConfig registrada con nombre n MBean [{0}] por eso no se pudo salvar la configuración. Un MBean adecuado es normalmente registrado via el StoreConfigLifecycleListener. + +standardService.start.name=Arrancando servicio [{0}] +standardService.stop.name=Parando servicio [{0}] + +standardWrapper.allocate=Error reservando espacio para una instancia de servlet +standardWrapper.allocateException=Excepción de reserva de espacio para servlet [{0}] +standardWrapper.deallocateException=Excepción de recuperación de espacio para servlet [{0}] +standardWrapper.destroyException=Servlet.destroy() para servlet [{0}] lanzó excepción +standardWrapper.initException=Servlet.init() para servlet [{0}] lanzó excepción +standardWrapper.instantiate=Error instanciando clase de servlet [{0}] +standardWrapper.isUnavailable=El Servlet [{0}] no está disponible en este momento +standardWrapper.notChild=El contenedor de Arropador (Wrapper) no puede tener contenedores hijo +standardWrapper.notClass=No se ha especificado clase de servlet para servlet [{0}] +standardWrapper.notContext=El contenedor padre para un Arropador (Wrapper) debe de ser un Contexto +standardWrapper.notFound=No está disponible el Servlet [{0}] +standardWrapper.notServlet=La Clase [{0}] no es un Servlet +standardWrapper.serviceException=Servlet.service() para servlet [{0}] lanzó excepción +standardWrapper.serviceExceptionRoot=El Servlet.service() para el servlet [{0}] en el contexto con ruta [{1}] lanzó la excepción [{2}] con causa raíz +standardWrapper.unavailable=Marcando el servlet [{0}] como no disponible +standardWrapper.unloadException=El Servlet [{0}] lanzó excepción unload() +standardWrapper.unloading=No se puede reservar espacio para servlet [{0}] porque está siendo descargado +standardWrapper.waiting=Esperando por [{0}] instancia(s) para recuperar su espacio reservado + +threadLocalLeakPreventionListener.containerEvent.error=Excepción procesando evento de contenedor [{0}] +threadLocalLeakPreventionListener.lifecycleEvent.error=Excepción procesando evento de ciclo de vida [{0}] diff --git a/java/org/apache/catalina/core/LocalStrings_fr.properties b/java/org/apache/catalina/core/LocalStrings_fr.properties new file mode 100644 index 0000000..3a88c9a --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_fr.properties @@ -0,0 +1,330 @@ +# 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. + +applicationContext.addFilter.ise=Des filtres ne peuvent plus être ajoutés au contexte [{0}] car il a déjà été initialisé +applicationContext.addJspFile.iae=Le fichier JSP [{0}] est invalide +applicationContext.addListener.iae.cnfe=Incapable de créer une instance de type [{0}] +applicationContext.addListener.iae.init=Impossible d''ajouter une instance du type [{0}] comme écouteur +applicationContext.addListener.iae.sclNotAllowed=Il est impossible d'ajouter un ServletContextListener après qu'un ait été appelé +applicationContext.addListener.iae.wrongType=Le type spécifié [{0}] n''est pas un type attendu de "listener" +applicationContext.addListener.ise=Des Listener ne peuvent plus être ajoutés au contexte [{0}] car il a déjà été initialisé +applicationContext.addRole.ise=Vous ne pouvez pas ajouter de rôles au contexte [{0}], parce qu''il a déjà été initialisé +applicationContext.addServlet.ise=Des Servlets ne peuvent plus être ajoutés au contexte [{0}] car il a déjà été initialisé +applicationContext.attributeEvent=Exception lancée par l'écouteur (listener) d'évènement attributs +applicationContext.illegalDispatchPath=La tentative d''obtenir un dispatcher de requêtes avec le chemin illégal [{0}] a été refusée car il contenait une tentative de traversée de répertoire encodée +applicationContext.invalidFilterName=Impossible d''ajouter la définition du filtre à cause d''un nom de filtre invalide [{0}] +applicationContext.invalidServletName=Impossible d''ajouter la définition de la servlet à cause de son nom invalide [{0}] +applicationContext.lookup.error=Impossible de trouver la ressource [{0}] dans le contexte [{1}] +applicationContext.mapping.error=Erreur lors du mapping +applicationContext.requestDispatcher.iae=Le chemin [{0}] ne commence pas par le caractère "/" +applicationContext.resourcePaths.iae=Le chemin [{0}] ne démarre pas avec un caractère "/" +applicationContext.role.iae=Un rôle individuel à déclarer pour le contexte [{0}] ne peut être null ou une chaîne vide +applicationContext.roles.iae=Le tableaux de rôles à déclarer pour le contexte [{0}] ne peut pas être null +applicationContext.setAttribute.namenull=le nom ne peut être nul +applicationContext.setInitParam.ise=les paramètres d''initialisation ne peuvent pas être configurés sur le contexte [{0}] car il a déjà été initialisé +applicationContext.setRequestEncoding.ise=L''encodage de la requête ne peut pas être configuré sur le contexte [{0}] car il a déjà été initialisé +applicationContext.setResponseEncoding.ise=L''encodage de la réponse ne peut pas être configuré sur le contexte [{0}] car il a déjà été initialisé +applicationContext.setSessionTimeout.ise=Le timeout de session ne peut pas être changé pour le contexte [{0}] car il a déjà été initialisé +applicationContext.setSessionTracking.iae.invalid=Le mode de suivi de la session [{0}] demandé par le contexte [{1}] n''est pas supporté par ce contexte +applicationContext.setSessionTracking.iae.ssl=Les modes de suivi de session pour le contexte [{0}] incluent SSL et au moins un autre mode. SSL ne peut pas être configuré avec d''autres modes. +applicationContext.setSessionTracking.ise=Les modes de suivi de session ("session tracking") du contexte [{0}] ne peuvent être définis pendant que le contexte est en cours d''exécution + +applicationDispatcher.allocateException=Exception d''allocation pour la servlet [{0}] +applicationDispatcher.customResponse=La réponse de classe [{0}] n''a pas permis d''obtenir la réponse de Catalina et sera fermée immédiatement après le forward +applicationDispatcher.deallocateException=Exception de désallocation pour la servlet [{0}] +applicationDispatcher.forward.ise=Impossible d'utiliser faire-suivre (forward) après que la réponse ait été envoyée +applicationDispatcher.isUnavailable=La servlet [{0}] est actuellement indisponible +applicationDispatcher.serviceException="Servlet.service()" pour la servlet [{0}] a lancé une exception +applicationDispatcher.specViolation.request=La ServletRequest d'origine ou la ServletRequest d'origine enrobée n'a pas été passée au RequestDispatcher en violation de SRV.8.2 et SRV.14.2.5.1 +applicationDispatcher.specViolation.response=La ServletResponse originale ou la ServletResponse enveloppée (wrapped) n'a pas été transmise au RequestDispatcher, en violation de SRV.8.2 et SRV.14.2.5.1 + +applicationFilterConfig.jmxRegisterFail=L''enregistrement JMX a échoué pour le filtre de type [{0}] et nommé [{0}] +applicationFilterConfig.jmxUnregister=Le désenregistrement JMX est terminé pour le filtre de type [{0}] nommé [{1}] +applicationFilterConfig.jmxUnregisterFail=Le désenregistrement JMX du filtre de type [{0}] nommé [{1}] a échoué +applicationFilterConfig.preDestroy=Echec lors de l''appel de preDestroy pour le filtre nommé [{0}] de type [{1}] +applicationFilterConfig.release=Impossible de détruite le filtre nommé [{0}] de type [{1}] + +applicationFilterFactory.noFilterConfig=Aucune configuration de filtre trouvée pour [{0}] + +applicationFilterRegistration.nullInitParam=Impossible de fixer le paramètre d''initialisation du filtre, à cause d''un nom ou d''une valeur nulle, nom [{0}], valeur [{1}] +applicationFilterRegistration.nullInitParams=Impossible de fixer les paramètres d''initialisation du filtre, à cause d''un nom ou d''une valeur nulle, nom [{0}], valeur [{1}] + +applicationHttpRequest.fragmentInDispatchPath=Le fragment dans le chemin de dispatch [{0}] a été enlevé +applicationHttpRequest.sessionEndAccessFail=Exception lancée durant l'arrêt de l'accès à la session durant le recyclage de la requête + +applicationPushBuilder.methodInvalid=La méthode HTTP pour une requête push doit être à la fois être sans danger et pouvoir être mise en cache, mais [{0}] ne correspond pas +applicationPushBuilder.methodNotToken=Les méthodes HTTP doivent être des "token", mais [{0}] contient un caractère invalide dans un token. + +applicationServletRegistration.setServletSecurity.iae=Contrainte nulle spécifiée pour le Servlet [{0}] déployé dans le contexte avec le nom [{1}] +applicationServletRegistration.setServletSecurity.ise=Les contraintes de sécurité ne peuvent pas être ajoutées au Servlet [{0}] déployé dans le contexte [{1}] car le contexte a déjà été initialisé + +applicationSessionCookieConfig.ise=La propriété [{0}] ne peut pas être ajoutée au SessionCookieConfig pour le contexte [{1}] car le contexte a déjà été initialisé + +aprListener.FIPSProviderNotDefault=Le provider FIPS doit être configuré comme provider par défaut quand AprLifecycleListener est configuré en mode FIPS [{0}] +aprListener.aprDestroy=Echec de l'arrêt de la librairie Apache Tomcat Native basée sur APR +aprListener.aprInit=La librairie Apache Tomcat Native basée sur APR qui permet des performances optimales dans les environnements de production n''a pas été trouvée sur le java.library.path : [{0}] +aprListener.aprInitDebug=La librairie Apache Tomcat Native basée sur APR n''a pas été trouvée en utilisant les noms [{0}] dans le java.library.path [{1}], les erreurs retournées sont [{2}] +aprListener.aprInitError=La librairie Apache Tomcat Native basée sur APR n''a pas pu être chargée, l''erreur retournée est [{0}] +aprListener.currentFIPSMode=Mode FIPS actuel : [{0}] +aprListener.enterAlreadyInFIPSMode=AprLifecycleListener est configuré pour forcer le mode FIPS mais la librairie est déjà en mode FIPS [{0}] +aprListener.initializeFIPSFailed=Echec d'entrée en mode FIPS +aprListener.initializeFIPSSuccess=Entrée avec succès en mode FIPS +aprListener.initializedOpenSSL=OpenSSL a été initialisé avec succès [{0}] +aprListener.initializingFIPS=Initialisation du mode FIPS... +aprListener.requireNotInFIPSMode=AprLifecycleListener est configuré pour demander que la librarie soit déjà en mode FIPS et elle ne l'était pas +aprListener.skipFIPSInitialization=Déjà en mode FIPS, l'initialisation de FIPS n'est pas effectuée +aprListener.sslInit=Impossible d'initialiser le SSLEngine +aprListener.sslRequired=[{0}] n''est pas une valeur valide pour SSLEngine quand la version [{1}] de la librairie Tomcat Native est utilisée car SSL est nécessaire à partir de la version 2.x +aprListener.tcnInvalid=Une version incompatible [{0}] de la librairie Apache Tomcat Native basée sur APR est installée, alors que Tomcat nécessite la version [{1}] +aprListener.tcnValid=Chargement de la librairie Apache Tomcat Native [{0}] en utilisant APR version [{1}] +aprListener.tcnVersion=Un version ancienne [{0}] de la bibliothèque Apache Tomcat Native basée sur APR est installée, alors que Tomcat recommande au minimum la version [{1}] +aprListener.tooLateForFIPSMode=Ne peut pas passer en mode FIPS, SSL a déjà été initialisé +aprListener.tooLateForSSLEngine=Impossible d'appeler setSSLEngine, SSL a déjà été initialisé +aprListener.tooLateForSSLRandomSeed=setSSLRandomSeed impossible : SSL a déjà été initialisé +aprListener.usingFIPSProvider=Utilisation d'OpenSSL avec le provider FIPS comme provider par défaut +aprListener.wrongFIPSMode=Valuer inattendue de l''option FIPSMode de AprLifecycleListener : [{0}] + +asyncContextImpl.afterOnError=Un fil d'exécution qui n'appartient pas au conteneur (applicatif) a essayé d'utiliser l'AsyncContext après qu'une erreur se soit produite et que l'appel à AsyncListener.onError() ait retourné. Ceci n'est pas autorisé pour éviter une situation de concurrence critique. +asyncContextImpl.asyncDispatchError=Erreur lors d'un dispatch asynchrone +asyncContextImpl.asyncRunnableError=Erreur lors du traitement asynchrone du Runnable via AsyncContext.start() +asyncContextImpl.dispatchingStarted=Une opération de dispatch asynchrone a déjà été appelée, plusieurs dispatch au cours d'un même cycle asynchrone n'est pas autorisé +asyncContextImpl.fireOnComplete=Déclenchement de l'évènement onComplete() sur tous les AsyncListeners +asyncContextImpl.fireOnError=Déclenchement de l'évènement onError() sur tous les AsyncListeners +asyncContextImpl.fireOnStartAsync=Déclenchement de l'évènement onStartAsync() sur tous les AsyncListeners +asyncContextImpl.fireOnTimeout=Déclenchement de l'évènement onTimeout() sur tous les AsyncListeners +asyncContextImpl.noAsyncDispatcher=Le Servlet dispatcher retourné par le ServletContext ne supporte pas de dispatch asynchrone +asyncContextImpl.onCompleteError=L''appel à onComplete() a échoué pour l''écouteur de type [{0}] +asyncContextImpl.onErrorError=L''appel à onError() a échoué pour l''écouteur de type [{0}] +asyncContextImpl.onStartAsyncError=L''appel à onStartAsync() a échoué pour l''écouteur de type [{0}] +asyncContextImpl.onTimeoutError=L''appel à onTimeout() a échoué pour l''écouteur de type [{0}] +asyncContextImpl.request.ise=Il est illégal d'appeler getRequest() après que complete() ou une autre des méthodes dispatch() ait été appelé +asyncContextImpl.requestEnded=La requête associée avec l'AsyncContext est déjà terminée +asyncContextImpl.response.ise=Il est illégal d'appeler getResponse() après que complete() ou n'importe laquelle des méthodes de dispatch a été appelée + +containerBase.backgroundProcess.cluster=Une exception s''est produite lors du traitement d''arrière plan du cluster [{0}] +containerBase.backgroundProcess.error=Exception durant le traitement du fil d'arrière-plan +containerBase.backgroundProcess.realm=Exception lors du traitement d''arrière plan du realm [{0}] +containerBase.backgroundProcess.valve=Exception lors du traitement d''arrière plan de la valve [{0}] +containerBase.child.add=Ajout du conteneur fils [{0}] au conteneur [{1}] +containerBase.child.destroy=Erreur lors de la destruction du conteneur fils +containerBase.child.notUnique=Le nom de conteneur fils [{0}] n''est pas unique +containerBase.child.start=Erreur lors du démarrage du conteneur fils +containerBase.child.stop=Erreur lors de l'arrêt du conteneur fils +containerBase.cluster.start=Erreur lors du démarrage du nouveau cluster +containerBase.cluster.stop=Erreur lors de l'arrêt de l'ancien cluster +containerBase.nullName=Le nom d'un conteneur ne peut être null +containerBase.realm.start=Erreur lors du démarrage du nouveau royaume +containerBase.realm.stop=Erreur lors de l'arrêt de l'ancien royaume +containerBase.threadedStartFailed=Un conteneur fils a échoué pendant son démarrage +containerBase.threadedStopFailed=Erreur lors de l'arrêt d'un conteneur fils + +contextNamingInfoListener.envEntry=Ajout de l''entrée d''environnement de contexte [{0}] avec la valeur [{1}] + +defaultInstanceManager.invalidAnnotation=L''annotation [{0}] est invalide +defaultInstanceManager.invalidInjection=Annotation invalide pour l'injection d'une ressource méthode +defaultInstanceManager.postConstructNotFound=La méthode post construct [{0}] de la classe [{1}] est déclarée dans le descripteur de déploiement mais n''a pas été trouvée +defaultInstanceManager.preDestroyNotFound=La méthode pre destroy [{0}] de la classe [{1}] est déclarée dans le descripteur de déploiement mais n''a pas été trouvée +defaultInstanceManager.restrictedClass=L''accès à la classe à accès restreint [{0}] est interdit, une application web doit être marquée comme étant privilégiée pour pouvoir la charger +defaultInstanceManager.restrictedContainerServlet=L''accès à la classe [{0}] est interdit. C''est une classe restreinte qui (implémente l''interface ContainerServlet). Une application web doit être privilégiée pour pouvoir la charger. +defaultInstanceManager.restrictedFiltersResource=Le fichier de propriétés contenant la liste des filtres restreints n''a pas été trouvée [{0}] +defaultInstanceManager.restrictedListenersResource=Le fichier de propriétés concernant les écouteurs à accès restreint n''a pas été trouvé [{0}] +defaultInstanceManager.restrictedServletsResource=Le fichier de propriétés contenant la liste des Servlets restreints n''a pas été trouvée [{0}] +defaultInstanceManager.restrictedWrongValue=Mauvaise valeur dans le fichier de propriété [{0}] contenant la liste des classes à accès restreint pour le nom de classe [{1}], valeur attendue : [restricted], valeur donnée : [{2}] + +filterChain.filter=L'exécution du filtre (Filter) a lancé une exception +filterChain.servlet=L'exécution de la servlet a lancé une exception + +jniLifecycleListener.bothPathAndName=Soit libraryName ou libraryPath peuvent être définis, pas les deux +jniLifecycleListener.load.name=La librairie native [{0}] a été chargée +jniLifecycleListener.load.path=La librairie native a été chargée à partir de [{0}] +jniLifecycleListener.missingPathOrName=Soit libraryName soit libraryPath doivent être définis + +jreLeakListener.classToInitializeFail=Echec du chargement de la classe [{0}] pendant le démarrage de Tomcat, effectué pour empêcher de possibles fuites de mémoire + +listener.notContext=Cet écouteur doit être uniquement inclus à l''intérieur d''éléments Context mais est dans [{0}] +listener.notServer=Ce listener ne peut être ajouté qu''à des éléments Server, mais est dans [{0}] + +naming.addEnvEntry=Ajout de l''entrée d''environnement [{0}] +naming.addResourceEnvRef=Ajout de la référence de ressource d''environnement [{0}] +naming.addResourceLink=Ajout du lien de ressource [{0}] +naming.addResourceRef=Ajout de la référence de ressource [{0}] à [{1}] +naming.addService=Ajout de la référence de service [{0}] à [{1}] +naming.addSlash=Modification de l''URL du service en [/{0}] +naming.bind=Association de l''environnement de noms au conteneur [{0}] +naming.bindFailed=Echec lors du liage à l''objet : [{0}] +naming.invalidEnvEntryType=L''entrée environnement [{0}] a un type invalide +naming.invalidEnvEntryValue=L''entrée environnement [{0}] a une valeur invalide +naming.jmxRegistrationFailed=Echec d''enregistrement dans JMX : [{0}] +naming.namingContextCreationFailed=La création du contexte de nommage (naming context) a échoué : [{0}] +naming.unbindFailed=Echec lors du déliage à l''objet : [{0}] +naming.wsdlFailed=fichier wsdl [{0}] non trouvé + +noPluggabilityServletContext.notAllowed=La section 4.4 de la spécification Servlet 3.0 ne permet pas à cette méthode d'être appelée à partir d'un ServletContextListener qui n'a pas été déclaré dans web.xml, un web-fragment.xml, ou annoté avec @WebListener + +propertiesRoleMappingListener.linkedRole=Le rôle de l''application [{0}] a été associé avec succès au rôle [{1}] +propertiesRoleMappingListener.linkedRoleCount=[{0}] rôles de l''application ont été associés à des rôles +propertiesRoleMappingListener.roleMappingFileEmpty=Le fichier d'association de rôles ne peut être vide +propertiesRoleMappingListener.roleMappingFileFail=Erreur de chargement du fichier d''association de rôles [{0}] +propertiesRoleMappingListener.roleMappingFileNull=Le fichier d'association de rôles ne peut être null + +pushBuilder.noPath=Il est interdit d'appeler push() avant de fixer un chemin + +standardContext.applicationListener=Erreur lors de la configuration de la classe d''écoute de l''application (application listener) [{0}] +standardContext.applicationSkipped=L'installation des écouteurs (listeners) de l'application a été sautée suite aux erreurs précédentes +standardContext.backgroundProcess.instanceManager=Exception lors du traitement d''arrière plan du gestionnaire d''instances [{0}] +standardContext.backgroundProcess.loader=Exception lors du traitement d''arrière plan du loader [{0}] +standardContext.backgroundProcess.manager=Exception lors du traitement d''arrière plan du gestionnaire de sessions [{0}] +standardContext.backgroundProcess.resources=Exception lors du traitement d''arrière plan des ressources [{0}] +standardContext.cluster.managerError=Erreur lors de la création d'un nouveau gestionnaire de session du cluster +standardContext.cluster.noManager=Aucun gestionnaire de session trouvé, vérification de l''utilisation éventuelle du gestionnaire de session fourni par le cluster ; cluster configuré : [{0}], application distribuable : [{1}] +standardContext.configurationFail=Un ou plusieurs composants ont marqué ce contexte comme n'étant pas correctement configuré +standardContext.cookieProcessor.null=Il est interdit de mettre un CookieProcessor null pour un contexte +standardContext.createWrapper.containerListenerError=Erreur lors de la création de l'écouteur de container de l'enrobeur +standardContext.createWrapper.error=Erreur de la création d'un nouvel enrobeur +standardContext.createWrapper.listenerError=Erreur lors de la création d'un écouteur de cycle de vie de l'enrobeur +standardContext.duplicateListener=L''écouteur [{0}] est déjà configuré pour le contexte, la double déclaration a été ignorée +standardContext.errorPage.error=La position de la page d''erreur (ErrorPage) [{0}] doit commencer par un ''/'' +standardContext.errorPage.required=La page d'erreur (ErrorPage) ne peut être nulle +standardContext.errorPage.warning=WARNING : La position de la page d''erreur (ErrorPage) [{0}] doit commencer par un ''/'' dans l''API Servlet 2.4 +standardContext.filterFail=Un ou plusieurs filtres n'ont pas pu démarrer, les détails sont dans le fichier log du conteneur +standardContext.filterMap.either=L'association de filtre (filter mapping) doit indiquer soit une soit une +standardContext.filterMap.name=L''association de filtre (filter mapping) indique un nom de filtre inconnu [{0}] +standardContext.filterMap.pattern= [{0}] invalide dans l''association de filtre (filter mapping) +standardContext.filterStart=Exception au démarrage du filtre [{0}] +standardContext.invalidWrapperClass=[{0}] n''est pas une sous-classe de StandardWrapper +standardContext.isUnavailable=Cette application n'est pas disponible actuellement +standardContext.listenerFail=Un ou plusieurs écouteurs n'ont pas pu démarrer, les détails sont dans le fichier de log du conteneur +standardContext.listenerStart=Exception lors de l''envoi de l''évènement contexte initialisé (context initialized) à l''instance de classe d''écoute (listener) [{0}] +standardContext.listenerStop=Exception lors de l''envoi de l''évènement contexte détruit (context destroyed) à l''instance de classe d''écoute [{0}] +standardContext.loadOnStartup.loadException=Le Servlet [{1}] dans l''application web [{0}] a retourné une exception lors de son chargement +standardContext.loginConfig.errorPage=La page d''erreur de Form [{0}] doit commencer par un ''/'' +standardContext.loginConfig.errorWarning=La page d''erreur de Form [{0}] doit commencer par un ''/'' dans l''API Servlet 2.4 +standardContext.loginConfig.loginPage=La page de connexion du formulaire [{0}] doit commencer par un ''/'' +standardContext.loginConfig.loginWarning=WARNING : La page de connexion du formulaire [{0}] doit commencer par un ''/'' dans l''API Servlet 2.4 +standardContext.loginConfig.required="LoginConfig" ne peut être nul +standardContext.manager=Configuré un gestionnaire de la classe [{0}] +standardContext.managerFail=Echec lors du démarrage du gestionnaire de sessions +standardContext.namingResource.destroy.fail=Echec de destruction des anciennes ressources JNDI +standardContext.namingResource.init.fail=Echec d'initialisation des nouvelles ressources JNDI +standardContext.notStarted=Le contexte [{0}] n''a pas encore été démarré +standardContext.notWrapper=Le fils du contexte (child of context) doit être un enrobeur (wrapper) +standardContext.parameter.duplicate=Paramètre d''initialisation de contexte dupliqué [{0}] +standardContext.parameter.required=Le nom de paramètre ainsi que la valeur du paramètre sont requis +standardContext.pathInvalid=Un chemin de contexte doit être soit une chaîne vide soit commencer par un ''/'' et ne pas finir par un ''/'', le chemin [{0}] ne répond pas à ces conditions et a été changé en [{1}] +standardContext.postconstruct.duplicate=La méthode post construct est définie en double dans la classe [{0}] +standardContext.postconstruct.required=A la fois le nom complet qualifié et le nom de la méthode sont requis +standardContext.predestroy.duplicate=Définition dupliquée de la méthode de destruction "pre" pour la classe [{0}] +standardContext.predestroy.required=Les noms qualifiés de la classe et de la méthode sont tous deux requis +standardContext.reloadingCompleted=Le rechargement de ce contexte est terminé +standardContext.reloadingStarted=Le rechargement du contexte [{0}] a démarré +standardContext.requestListener.requestDestroyed=Une exception est survenue lors de l''envoi de l''événement de destruction de la requête pour l''instance de listener de cycle de vie [{0}] +standardContext.requestListener.requestInit=Une exception est survenue lors de l''envoi de l''événement d''initialisation de la requête pour l''instance de listener de cycle de vie [{0}] +standardContext.resetContextFail=Erreur de réinitialisation du contexte avec le nom [{0}] +standardContext.resourcesInit=Erreur d'initialisation des ressources statiques +standardContext.resourcesStart=Erreur lors du démarrage des ressources statiques +standardContext.resourcesStop=Erreur lors de l'arrêt des ressources statiques +standardContext.sciFail=Erreur lors du traitement de ServletContainerInitializer +standardContext.securityConstraint.mixHttpMethod=Il n'est pas permis de combiner et dans la même collection de ressources web +standardContext.securityConstraint.pattern= [{0}] invalide d''après les contraintes de sécurité (security constraint) +standardContext.servletFail=Un ou plusieurs Servlets n'ont pas pu démarrer, les détails sont dans le fichier log du conteneur +standardContext.servletMap.name=L''association de servlet (servlet mapping) indique un nom de servlet inconnu [{0}] +standardContext.servletMap.pattern= [{0}] invalide dans l''association de servlet (servlet mapping) +standardContext.setLoader.start=Erreur lors du démarrage du nouveau chargeur +standardContext.setLoader.stop=Erreur lors de l'arrêt de l'ancien chargeur +standardContext.setManager.start=Erreur lors du démarrage du nouveau gestionnaire de sessions +standardContext.setManager.stop=Erreur lors de l'arrêt de l'ancien gestionnaire de sessions +standardContext.startFailed=Erreur de démarrage du contexte [{0}] suite aux erreurs précédentes +standardContext.startingContext=Exception lors du démarrage du contexte [{0}] +standardContext.stop.asyncWaitInterrupted=Une interruption a été reçue en attendant unloadDelay millisecondes pour permettre aux requêtes asynchrones en cours de se terminer, l'arrêt du contexte va se poursuivre sans délai supplémentaire +standardContext.stoppingContext=Exception à l''arrêt du Context [{0}] +standardContext.suspiciousUrl=Modèle d''URL suspect : [{0}] dans le contexte [{1}], voir les sections 12.1 et 12.2 de la spécification Servlets +standardContext.threadBindingListenerError=Une erreur s''est produite dans l''écouteur de l’''association de thread configuré pour le contexte [{0}] +standardContext.urlPattern.patternWarning=WARNING : Le modèle (pattern) d''URL [{0}] doit commencer par un ''/'' dans l''API Servlet 2.4 +standardContext.workCreateException=Impossible de déterminer le chemin absolu pour le répertoire de travail à partir du répertoire [{0}] et de CATALINA_HOME [{1}] pour le contexte [{2}] +standardContext.workCreateFail=Impossible de créer le répertoire de travail [{0}] pour le contexte [{1}] +standardContext.workPath=Impossible d''obtenir le chemin de travail pour le contexte [{0}] + +standardContextValve.acknowledgeException=Impossible de confirmer la requête avec une réponse 100 (continuer) + +standardEngine.notHost=Le fils d'un moteur (child of an Engine) doit être un hôte +standardEngine.notParent=Un moteur (engine) ne peut avoir de conteneur parent (container) +standardEngine.start=Démarrage du moteur de Servlets : [{0}] + +standardHost.clientAbort=Le client distant a abandonné la requête, IOException : [{0}] +standardHost.invalidErrorReportValveClass=Impossible de charger la classe valve de rapport d''erreur : [{0}] +standardHost.noContext=Aucun contexte n'est configuré pour traiter cette requête +standardHost.notContext=Le fils d'un hôte (child of a Host) doit être un contexte +standardHost.nullName=Le nom d'hôte est requis +standardHost.problematicAppBase=Utiliser une chaîne vide pour l''appBase de l''hôte [{0}] la fera correspondre à CATALINA_BASE, ce qui causera des problèmes +standardHost.problematicLegacyAppBase=L''utilisation d''une chaîne vide pour legacyAppBase de l''hôte [{0}] le fixera à CATALINA_BASE, ce qui n''est pas judicieux + +standardHostValve.customStatusFailed=La page d''erreur personnalisée [{0}] n''a pu être redirigée correctement +standardHostValve.exception=Exception lors du traitement de [{0}] + +standardPipeline.basic.start=Erreur lors du démarrage de la nouvelle valve de base +standardPipeline.basic.stop=Erreur lors de l'arrêt de l'ancienne valve de base +standardPipeline.valve.destroy=Erreur lors de la destruction de la valve +standardPipeline.valve.start=Erreur lors du démarrage de la valve +standardPipeline.valve.stop=Erreur lors de l'arrêt de la valve + +standardServer.accept.error=Une erreur d'IO s'est produite en essayant d'accepter sur le socket qui attend la commande d'arrêt +standardServer.accept.readError=Une erreur d'IO s'est produite lors de la lecture de la commande d'arrêt +standardServer.accept.security=Une erreur de sécurité s'est produite en essayant d'accepter sur le socket qui attend la commande d'arrêt +standardServer.accept.timeout=Le socket qui écoute en attendant la commande d''arrêt a rencontré un délai d''attente dépassé inattendu [{0}] millisecondes après l''appel à accept() +standardServer.awaitSocket.fail=Impossible de créer le sokcet d''arrêt du serveur à l''adresse [{0}] et au port [{1}] (port de base [{2}] et offset [{3}]) +standardServer.invalidShutdownCommand=Une commande d''arrêt invalide [{0}] a été reçue +standardServer.periodicEventError=Erreur lors de l'envoi de l'évènement périodique +standardServer.portOffset.invalid=La valeur [{0}] pour portOffset est invalide car elle ne peut pas être négative +standardServer.shutdownViaPort=Une commande d'arrêt valide a été reçue sur le port d'arrêt, arrêt de l'instance du serveur +standardServer.storeConfig.contextError=Erreur lors de l''enregistrement de la configuration du contexte [{0}] +standardServer.storeConfig.error=Erreur lors de l'enregistrement de la configuration du serveur +standardServer.storeConfig.notAvailable=Aucune implémentation de StoreConfig n''a été enregistrée comme un MBean nommé [{0}], et aucune configuration n''a donc été enregistrée. Un MBean adéquat est normalement référencé via le StoreConfigLifecycleListener + +standardService.engine.startFailed=Impossible de démarrer l'Engine associé +standardService.engine.stopFailed=Echec de l'arrêt du moteur associé +standardService.executor.start=Erreur lors du démarrage du nouvel exécuteur +standardService.executor.stop=Erreur lors de l'arrêt de l'ancien exécuteur +standardService.mapperListener.startFailed=Impossible de démarrer le MapperListener associé +standardService.mapperListener.stopFailed=Impossible d'arrêter le MapperListener associé +standardService.start.name=Démarrage du service [{0}] +standardService.stop.name=Arrêt du service [{0}] + +standardThreadExecutor.notStarted=L'exécuteur n'a pas encore été démarré + +standardVirtualThreadExecutor.notStarted=L'executeur n'a pas été démarré + +standardWrapper.allocate=Erreur d'allocation à une instance de servlet +standardWrapper.allocateException=Exception lors de l''allocation pour la servlet [{0}] +standardWrapper.deallocateException=Exception à la désallocation pour la servlet [{0}] +standardWrapper.destroyException="Servlet.destroy()" de la servlet [{0}] a généré une exception +standardWrapper.destroyInstance=InstanceManager.destroy() pour le Servlet [{0}] a renvoyé une exception +standardWrapper.initException="Servlet.init()" pour la servlet [{0}] a généré une exception +standardWrapper.instantiate=Erreur à l''instantiation de la classe servlet [{0}] +standardWrapper.isUnavailable=La servlet [{0}] est actuellement indisponible +standardWrapper.jspMonitorError=Erreur de l'enregistrement du Mbean du moniteur de JSP +standardWrapper.notChild=L'enrobeur de conteneur (wrapper container) ne peut pas avoir de conteneurs fils +standardWrapper.notClass=Aucune classe servlet n''a été spécifiée pour la servlet [{0}] +standardWrapper.notContext=Le conteneur parent d'un enrobeur (wrapper) doit être un contexte +standardWrapper.notFound=Servlet [{0}] n''est pas disponible. +standardWrapper.notServlet=La classe [{0}] n''est pas une servlet +standardWrapper.serviceException="Servlet.service()" pour la servlet [{0}] a généré une exception +standardWrapper.serviceExceptionRoot=Servlet.service() du Servlet [{0}] dans le contexte au chemin [{1}] a retourné une exception [{2}] avec la cause +standardWrapper.unavailable=La servlet [{0}] est marqué comme indisponible +standardWrapper.unloadException=La servlet [{0}] a généré une exception "unload()" +standardWrapper.unloading=Impossible d''allouer la servlet [{0}] car elle a été déchargée +standardWrapper.waiting=Attente de la désallocation de [{0}] instance(s) du Servlet [{1}] + +threadLocalLeakPreventionListener.containerEvent.error=Exception lors du traitement de l''évènement du conteneur [{0}] +threadLocalLeakPreventionListener.lifecycleEvent.error=Exception lors du traitement de l''évènement [{0}] du cycle de vie du composant diff --git a/java/org/apache/catalina/core/LocalStrings_ja.properties b/java/org/apache/catalina/core/LocalStrings_ja.properties new file mode 100644 index 0000000..714d5f4 --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_ja.properties @@ -0,0 +1,330 @@ +# 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. + +applicationContext.addFilter.ise=コンテキストãŒåˆæœŸåŒ–ã•ã‚ŒãŸãŸã‚ã€ãƒ•ã‚£ãƒ«ã‚¿ã‚’コンテキスト [{0}] ã«è¿½åŠ ã§ãã¾ã›ã‚“ +applicationContext.addJspFile.iae=無効㪠JSP ファイル [{0}] ã§ã™ +applicationContext.addListener.iae.cnfe=クラス [{0}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’作æˆã§ãã¾ã›ã‚“ +applicationContext.addListener.iae.init=[{0}] åž‹ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’リスナーã¨ã—ã¦è¿½åŠ ã§ãã¾ã›ã‚“ +applicationContext.addListener.iae.sclNotAllowed=最åˆã®ServletContextListenerãŒå‘¼ã³å‡ºã•ã‚Œã‚‹ã¨ã€ãれ以上ServletContextListenerを追加ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +applicationContext.addListener.iae.wrongType=指定ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{0}] ã¯ãƒªã‚¹ãƒŠãƒ¼ã‚¯ãƒ©ã‚¹ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +applicationContext.addListener.ise=コンテキストãŒåˆæœŸåŒ–ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€ãƒªã‚¹ãƒŠãƒ¼ã‚’コンテキスト [{0}] ã«è¿½åŠ ã§ãã¾ã›ã‚“ +applicationContext.addRole.ise=コンテキスト [{0}] ã¯åˆæœŸåŒ–済ã¿ã®ãŸã‚ロールを追加ã§ãã¾ã›ã‚“。 +applicationContext.addServlet.ise=コンテキストãŒåˆæœŸåŒ–済ã¿ãªãŸã‚ã€Servletをコンテキスト [{0}] ã«è¿½åŠ ã§ãã¾ã›ã‚“ +applicationContext.attributeEvent=属性イベントリスナã«ã‚ˆã£ã¦ä¾‹å¤–ãŒæŠ•ã’られã¾ã—㟠+applicationContext.illegalDispatchPath=アプリケーションãŒã€ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã•ã‚ŒãŸãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªãƒˆãƒ©ãƒãƒ¼ã‚µãƒ«è©¦è¡Œã‚’å«ã‚€ãŸã‚ã«æ‹’å¦ã•ã‚ŒãŸä¸æ­£ãªãƒ‘ス [{0}] ã‚’æŒã¤è¦æ±‚ディスパッãƒãƒ£ã‚’å–å¾—ã—よã†ã¨ã—ã¾ã—㟠+applicationContext.invalidFilterName=無効ãªãƒ•ã‚£ãƒ«ã‚¿å[{0}]ã®ãŸã‚ã«ãƒ•ã‚£ãƒ«ã‚¿å®šç¾©ã‚’追加ã§ãã¾ã›ã‚“。 +applicationContext.invalidServletName=無効ãªã‚µãƒ¼ãƒ–レットå [{0}] ã®ãŸã‚ã€ã‚µãƒ¼ãƒ–レット定義を追加ã§ãã¾ã›ã‚“。 +applicationContext.lookup.error=コンテキスト [{1}] ã§ãƒªã‚½ãƒ¼ã‚¹ [{0}] ã®å ´æ‰€ã‚’特定ã§ãã¾ã›ã‚“ã§ã—㟠+applicationContext.mapping.error=マッピング中ã®ã‚¨ãƒ©ãƒ¼ +applicationContext.requestDispatcher.iae=パス [{0}] ãŒ"/"文字ã§å§‹ã¾ã‚Šã¾ã›ã‚“ +applicationContext.resourcePaths.iae=パス [{0}] ã¯ã€Œ/ã€æ–‡å­—ã§å§‹ã¾ã£ã¦ã„ã¾ã›ã‚“ +applicationContext.role.iae=コンテキスト [{0}] を宣言ã™ã‚‹å€‹ã€…ã®ãƒ­ãƒ¼ãƒ«ã¯ã€nullã§ã‚‚空ã®æ–‡å­—列ã§ã‚‚ã‚ã‚Šã¾ã›ã‚“ +applicationContext.roles.iae=コンテキスト [{0}] ã«å¯¾ã—ã¦å®£è¨€ã™ã‚‹ãƒ­ãƒ¼ãƒ«ã®é…列ã¯nullã«ã§ãã¾ã›ã‚“ +applicationContext.setAttribute.namenull=name ㌠null ã§ã¯ã„ã‘ã¾ã›ã‚“ +applicationContext.setInitParam.ise=åˆæœŸåŒ–パラメータã¯ã€ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãŒåˆæœŸåŒ–ã•ã‚ŒãŸå¾Œã«è¨­å®šã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +applicationContext.setRequestEncoding.ise=コンテキスト [{0}] ã¯åˆæœŸåŒ–済ã¿ã®ãŸã‚リクエストエンコーディングを構æˆã§ãã¾ã›ã‚“。 +applicationContext.setResponseEncoding.ise=コンテキスト [{0}] ã¯åˆæœŸåŒ–済ã¿ã®ãŸã‚ã€ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‡ã‚£ãƒ³ã‚°ã‚’構æˆã§ãã¾ã›ã‚“。 +applicationContext.setSessionTimeout.ise=コンテキストãŒåˆæœŸåŒ–ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] ã«ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã‚’設定ã§ãã¾ã›ã‚“ +applicationContext.setSessionTracking.iae.invalid=コンテキスト [{1}] ã«å¯¾ã—ã¦è¦æ±‚ã•ã‚ŒãŸã‚»ãƒƒã‚·ãƒ§ãƒ³è¿½è·¡ãƒ¢ãƒ¼ãƒ‰ [{0}] ã¯ã€ãã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“ +applicationContext.setSessionTracking.iae.ssl=コンテキスト [{0}] ã«å¯¾ã—ã¦è¦æ±‚ã•ã‚ŒãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ãƒˆãƒ©ãƒƒã‚­ãƒ³ã‚°ãƒ¢ãƒ¼ãƒ‰ã«ã¯ã€SSLã¨å°‘ãªãã¨ã‚‚1ã¤ã®ä»–ã®ãƒ¢ãƒ¼ãƒ‰ãŒå«ã¾ã‚Œã¦ã„ã¾ã—ãŸã€‚ SSLã¯ä»–ã®ãƒ¢ãƒ¼ãƒ‰ã§ã¯è¨­å®šã§ãã¾ã›ã‚“。 +applicationContext.setSessionTracking.ise=コンテキスト [{0}] ã¯å®Ÿè¡Œä¸­ã®ãŸã‚セッション追跡モードを構æˆã§ãã¾ã›ã‚“。 + +applicationDispatcher.allocateException=サーブレット [{0}] ã«ä¾‹å¤–を割り当ã¦ã¾ã™ +applicationDispatcher.customResponse=レスãƒãƒ³ã‚¹ã‚¯ãƒ©ã‚¹ [{0}] 㯠Catalina レスãƒãƒ³ã‚¹ã‚¯ãƒ©ã‚¹ã«ã‚¢ãƒ³ãƒ©ãƒƒãƒ—ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚転é€å¾Œã™ãã«é–‰ã˜ã‚‰ã‚Œã¾ã™ +applicationDispatcher.deallocateException=サーブレット [{0}] ã®ä¾‹å¤–を解除ã—ã¾ã™ +applicationDispatcher.forward.ise=レスãƒãƒ³ã‚¹ã‚’コミットã—ãŸå¾Œã§ãƒ•ã‚©ãƒ¯ãƒ¼ãƒ‰ã§ãã¾ã›ã‚“ +applicationDispatcher.isUnavailable=サーブレット [{0}] ã¯ç¾åœ¨åˆ©ç”¨ã§ãã¾ã›ã‚“ +applicationDispatcher.serviceException=サーブレット [{0}] ã®Servlet.service()ãŒä¾‹å¤–を投ã’ã¾ã—㟠+applicationDispatcher.specViolation.request=å…ƒã®ServletRequestã¾ãŸã¯ãƒ©ãƒƒãƒ—ã•ã‚ŒãŸå…ƒã®ServletRequestã¯SRV.8.2ãŠã‚ˆã³SRV.14.2.5.1ã«é•åã—ã™ã‚‹ã®ã§RequestDispatcherã«æ¸¡ã•ã‚Œã¾ã›ã‚“。 +applicationDispatcher.specViolation.response=SRV.8.2 ãŠã‚ˆã³ SRV.14.2.5.1 ã«é•åã—ã¦ã„ã‚‹ãŸã‚ã€ã‚ªãƒªã‚¸ãƒŠãƒ«ã‚ã‚‹ã„ã¯ãƒ©ãƒƒãƒ—ã•ã‚ŒãŸ ServletResponse 㯠RequestDispatcher を通éŽã—ã¾ã›ã‚“ã§ã—㟠+ +applicationFilterConfig.jmxRegisterFail=クラス [{0}] åå‰ [{1}] ã®ãƒ•ã‚£ãƒ«ã‚¿ãƒ¼ã‚’ JMX ã«ç™»éŒ²ã§ãã¾ã›ã‚“。 +applicationFilterConfig.jmxUnregister=タイプ [{0}] ãŠã‚ˆã³åå‰ [{1}] ã®ãƒ•ã‚£ãƒ«ã‚¿ã®JMX登録解除ãŒå®Œäº†ã—ã¾ã—㟠+applicationFilterConfig.jmxUnregisterFail=クラス [{0}] åå‰ [{1}] ã® JMX フィルターを登録解除ã§ãã¾ã›ã‚“。 +applicationFilterConfig.preDestroy=[{1}]åž‹ã®å[{0}]ã¨ã„ã†ãƒ•ã‚£ãƒ«ã‚¿ã®preDestroyã¸ã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—㟠+applicationFilterConfig.release=タイプ [{1}] ã®åå‰ [{0}] ã®ãƒ•ã‚£ãƒ«ã‚¿ã‚’破棄ã§ãã¾ã›ã‚“ã§ã—㟠+ +applicationFilterFactory.noFilterConfig=[{0}] ã®ãƒ•ã‚£ãƒ«ã‚¿ãƒ¼æ§‹æˆãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ + +applicationFilterRegistration.nullInitParam=キー [{0}] ã¾ãŸã¯å€¤ [{1}] ã®ã„ãšã‚Œã‹ãŒ null ã®ãŸã‚フィルターã®åˆæœŸåŒ–パラメータを設定ã§ãã¾ã›ã‚“。 +applicationFilterRegistration.nullInitParams=キー [{0}] ã¾ãŸã¯å€¤ [{1}] ã®ã„ãšã‚Œã‹ãŒ null ã®ãŸã‚フィルターã®åˆæœŸåŒ–パラメータを設定ã§ãã¾ã›ã‚“。 + +applicationHttpRequest.fragmentInDispatchPath=ディスパッãƒãƒ‘ス [{0}] 中ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¯é™¤åŽ»ã•ã‚Œã¾ã—㟠+applicationHttpRequest.sessionEndAccessFail=リクエストã®å†åˆ©ç”¨ä¸­ã«è¡Œã£ãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹çµ‚了処ç†ã§ä¾‹å¤–ãŒé€å‡ºã•ã‚Œã¾ã—ãŸã€‚ + +applicationPushBuilder.methodInvalid=プッシュリクエスト㮠HTTP メソッドã¯ã‚­ãƒ£ãƒƒã‚·ãƒ¥å¯èƒ½ã€ã‹ã¤ã€å®‰å…¨ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。[{0}] ã¯æŒ‡å®šã§ãã¾ã›ã‚“。 +applicationPushBuilder.methodNotToken=HTTP メソッド [{0}] ã«ãƒˆãƒ¼ã‚¯ãƒ³ã¨ã—ã¦åˆ©ç”¨ã§ããªã„文字ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ + +applicationServletRegistration.setServletSecurity.iae=サーブレット [{0}] ã«æŒ‡å®šã•ã‚ŒãŸNULL制約ãŒã€åå‰ [{1}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã«é…å‚™ã•ã‚Œã¾ã—㟠+applicationServletRegistration.setServletSecurity.ise=コンテキストãŒæ—¢ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€åå‰ [{1}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã«é…å‚™ã•ã‚ŒãŸã‚µãƒ¼ãƒ–レット [{0}] ã«ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£åˆ¶ç´„を追加ã§ãã¾ã›ã‚“ + +applicationSessionCookieConfig.ise=コンテキスト [{1}] ãŒåˆæœŸåŒ–済ã¿ãªã®ã§ã€ãƒ—ロパティ [{0}] ã‚’SessionCookieConfigã«è¿½åŠ ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 + +aprListener.FIPSProviderNotDefault=AprLifecycleListener ㌠FIPS モード [{0}] ã§æ§‹æˆã•ã‚Œã¦ã„ã‚‹å ´åˆã¯ã€FIPS プロãƒã‚¤ãƒ€ã‚’既定ã®ãƒ—ロãƒã‚¤ãƒ€ã¨ã—ã¦æ§‹æˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ +aprListener.aprDestroy=APRベースã®Apache Tomcatãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリã®ã‚·ãƒ£ãƒƒãƒˆãƒ€ã‚¦ãƒ³ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +aprListener.aprInit=商用環境ã«æœ€é©ãªæ€§èƒ½ã‚’発æ®ã™ã‚‹ APR ベース㮠Tomcat ãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリ㌠java.library.path [{0}] ã«å­˜åœ¨ã—ã¾ã›ã‚“。 +aprListener.aprInitDebug=APRベースã®Apache Tomcatãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリã¯ã€java.library.path [{1}] 上ã®åå‰ [{0}] を使用ã—ã¦è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ 報告ã•ã‚ŒãŸã‚¨ãƒ©ãƒ¼ã¯ [{2}] +aprListener.aprInitError=APRベースã®Apache Tomcatãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリをロードã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ 報告ã•ã‚ŒãŸã‚¨ãƒ©ãƒ¼ã¯ [{0}] ã§ã—㟠+aprListener.currentFIPSMode=ç¾åœ¨ã®FIPSモード:[{0}] +aprListener.enterAlreadyInFIPSMode=AprLifecycleListenerã¯å¼·åˆ¶çš„ã«FIPSモードã«å…¥ã‚‹ã‚ˆã†ã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ãŒã€ãƒ©ã‚¤ãƒ–ラリã¯ã™ã§ã«FIPSモードã«ãªã£ã¦ã„ã¾ã™ [{0}] +aprListener.initializeFIPSFailed=FIPS モードã«å¤‰æ›´ã§ãã¾ã›ã‚“。 +aprListener.initializeFIPSSuccess=FIPS モードã«å…¥ã‚Šã¾ã—ãŸã€‚ +aprListener.initializedOpenSSL=OpenSSL㯠[{0}] を正常ã«åˆæœŸåŒ–ã—ã¾ã—㟠+aprListener.initializingFIPS=FIPSモードをåˆæœŸåŒ–ã—ã¦ã„ã¾ã™... +aprListener.requireNotInFIPSMode=AprLifecycleListenerã¯ãƒ©ã‚¤ãƒ–ラリãŒæ—¢ã«FIPSモードã«ãªã£ã¦ã„ã‚‹å¿…è¦ãŒã‚るよã†ã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ãŒã€FIPSモードã§ã¯ã‚ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ +aprListener.skipFIPSInitialization=ã™ã§ã«FIPSモードã«ãªã£ã¦ã„ã¾ã™ã€‚FIPSåˆæœŸåŒ–をスキップã—ã¾ã™ã€‚ +aprListener.sslInit=SSLEngineã®åˆæœŸåŒ–ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +aprListener.sslRequired=ãƒãƒ¼ã‚¸ãƒ§ãƒ³ 2.x以é™ã§ã¯SSLãŒå¿…è¦ãªãŸã‚ã€ãƒãƒ¼ã‚¸ãƒ§ãƒ³ [{1}] ã®Tomcat Native ライブラリを使用ã™ã‚‹å ´åˆã€ [{0}] ã¯SSLEngineã®æœ‰åŠ¹ãªå€¤ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +aprListener.tcnInvalid=APRベースã®Apache Tomcatãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリã®äº’æ›æ€§ã®ãªã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³[{0}]ãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã¾ã™ãŒã€Tomcatã«ã¯ãƒãƒ¼ã‚¸ãƒ§ãƒ³[{1}]ãŒå¿…è¦ã§ã™ã€‚ +aprListener.tcnValid=APRãƒãƒ¼ã‚¸ãƒ§ãƒ³[{1}]を使用ã—ã¦APRベースã®Apache Tomcatãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリ[{0}]をロードã—ã¾ã—ãŸã€‚ +aprListener.tcnVersion=インストールã•ã‚ŒãŸ Apache Tomcat ãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリ㮠APR ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã¯ [{0}] ã§ã™ãŒã€æŽ¨å¥¨ã™ã‚‹æœ€å°ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã¯ [{1}] ã§ã™ã€‚ +aprListener.tooLateForFIPSMode=FIPSModeを設定ã§ãã¾ã›ã‚“:SSLã¯æ—¢ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã™ã€‚ +aprListener.tooLateForSSLEngine=setSSLEngineã§ãã¾ã›ã‚“: SSLã¯ã™ã§ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã™ +aprListener.tooLateForSSLRandomSeed=setSSLRandomSeedã§ãã¾ã›ã‚“:SSLã¯æ—¢ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã™ã€‚ +aprListener.usingFIPSProvider=FIPS プロãƒã‚¤ãƒ€ã‚’既定ã®ãƒ—ロãƒã‚¤ãƒ€ã¨ã—㦠OpenSSL を使用ã—ã¾ã™ +aprListener.wrongFIPSMode=AprLifecycleListener ã®è¨­å®š FIPSMode ã«äºˆæœŸã›ã¬å€¤ [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ + +asyncContextImpl.afterOnError=エラーãŒç™ºç”Ÿã—ã€AsyncListener.onError() ã¸ã®å‘¼ã³å‡ºã—ãŒè¿”ã•ã‚ŒãŸå¾Œã€éžã‚³ãƒ³ãƒ†ãƒŠãƒ¼ (アプリケーション) スレッド㌠AsyncContext を使用ã—よã†ã¨ã—ã¾ã—ãŸã€‚ ã“ã‚Œã¯ç«¶åˆçŠ¶æ…‹ã‚’é¿ã‘ã‚‹ãŸã‚ã«è¨±å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +asyncContextImpl.asyncDispatchError=éžåŒæœŸãƒ‡ã‚£ã‚¹ãƒ‘ッãƒã®å‡¦ç†ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +asyncContextImpl.asyncRunnableError=AsyncContext.start() ã«ã‚ˆã‚‹éžåŒæœŸ Runnable 処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +asyncContextImpl.dispatchingStarted=éžåŒæœŸãƒ‡ã‚£ã‚¹ãƒ‘ッãƒæ“作ã¯æ—¢ã«å‘¼ã³å‡ºã•ã‚Œã¦ã„ã¾ã™ã€‚åŒã˜éžåŒæœŸã‚µã‚¤ã‚¯ãƒ«å†…ã®è¿½åŠ ã®éžåŒæœŸãƒ‡ã‚£ã‚¹ãƒ‘ッãƒæ“作ã¯è¨±å¯ã•ã‚Œã¾ã›ã‚“。 +asyncContextImpl.fireOnComplete=éžåŒæœŸãƒªã‚¹ãƒŠã« onComploete() イベントを発ç«ã—ã¾ã—㟠+asyncContextImpl.fireOnError=éžåŒæœŸãƒªã‚¹ãƒŠã« onError() イベントを発ç«ã—ã¾ã—㟠+asyncContextImpl.fireOnStartAsync=éžåŒæœŸãƒªã‚¹ãƒŠã« onStartAsync() イベントを発ç«ã—ã¾ã—㟠+asyncContextImpl.fireOnTimeout=éžåŒæœŸãƒªã‚¹ãƒŠã« onTimeout() イベントを発ç«ã—ã¾ã—㟠+asyncContextImpl.noAsyncDispatcher=ServletContextã‹ã‚‰è¿”ã•ã‚ŒãŸãƒ‡ã‚£ã‚¹ãƒ‘ッãƒãƒ£ã¯éžåŒæœŸãƒ‡ã‚£ã‚¹ãƒ‘ッãƒã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 +asyncContextImpl.onCompleteError=クラス [{0}] ã®ãƒªã‚¹ãƒŠãƒ¼ã‚ªãƒ–ジェクト㧠onComplete() ã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +asyncContextImpl.onErrorError=クラス [{0}] ã®ãƒªã‚¹ãƒŠãƒ¼ã‚ªãƒ–ジェクト㧠onError() ã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +asyncContextImpl.onStartAsyncError=クラス [{0}] ã®ãƒªã‚¹ãƒŠãƒ¼ã‚ªãƒ–ジェクト㧠onStartAsync() ã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +asyncContextImpl.onTimeoutError=クラス [{0}] ã®ãƒªã‚¹ãƒŠãƒ¼ã‚ªãƒ–ジェクト㧠onTimeout() ã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +asyncContextImpl.request.ise=complete()ã®å¾Œã€ã¾ãŸã¯dispatch()メソッドã®ã©ã‚Œã‹ãŒå‘¼ã³å‡ºã•ã‚ŒãŸä¸æ­£ãªgetRequest()ã®å‘¼ã³å‡ºã— +asyncContextImpl.requestEnded=AsyncContextã«é–¢é€£ä»˜ã‘られãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯ã€ã™ã§ã«å‡¦ç†ã‚’完了ã—ã¦ã„ã¾ã™ã€‚ +asyncContextImpl.response.ise=complete()ã€ã¾ãŸã¯dispatch()メソッドã®ã„ãšã‚Œã‹ãŒå‘¼ã³å‡ºã•ã‚ŒãŸå¾Œã§ã®ã€ä¸æ­£ãªgetResponse()呼ã³å‡ºã—。 + +containerBase.backgroundProcess.cluster=クラスター [{0}] ã§ãƒãƒƒã‚¯ã‚°ãƒ©ãƒ³ãƒ‰å‡¦ç†ã‚’実行中ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +containerBase.backgroundProcess.error=ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã‚¹ãƒ¬ãƒƒãƒ‰å‡¦ç†ä¸­ã®ä¾‹å¤– +containerBase.backgroundProcess.realm=Realm [{0}] ã®ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰å‡¦ç†ä¸­ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +containerBase.backgroundProcess.valve=Valve [{0}] ã®ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ãƒ—ロセス処ç†ä¸­ã®ä¾‹å¤– +containerBase.child.add=å­ã‚³ãƒ³ãƒ†ãƒŠ [{0}] をコンテナ [{1}] ã«è¿½åŠ ã—ã¾ã™ +containerBase.child.destroy=å­ã‚³ãƒ³ãƒ†ãƒŠç ´æ£„中ã®ã‚¨ãƒ©ãƒ¼ +containerBase.child.notUnique=å­è¦ç´ ã®åå‰ [{0}] ãŒä¸€æ„ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +containerBase.child.start=å­è¦ç´ ã‚’開始ã§ãã¾ã›ã‚“。 +containerBase.child.stop=å­ã‚³ãƒ³ãƒ†ãƒŠåœæ­¢æ™‚ã®ã‚¨ãƒ©ãƒ¼ +containerBase.cluster.start=æ–°ã—ã„クラスターを開始ã§ãã¾ã›ã‚“。 +containerBase.cluster.stop=å¤ã„クラスターをåœæ­¢ã§ãã¾ã›ã‚“。 +containerBase.nullName=コンテナーå㯠null ã«ã§ãã¾ã›ã‚“。 +containerBase.realm.start=æ–°ã—ã„Realmを開始ã§ãã¾ã›ã‚“。 +containerBase.realm.stop=å¤ã„Realmã‚’åœæ­¢ã§ãã¾ã›ã‚“。 +containerBase.threadedStartFailed=å­ã‚³ãƒ³ãƒ†ãƒŠãƒ¼ã‚’開始ã§ãã¾ã›ã‚“。 +containerBase.threadedStopFailed=åœæ­¢ä¸­ã«å­ã‚³ãƒ³ãƒ†ãƒŠãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ + +contextNamingInfoListener.envEntry=コンテキスト環境エントリ [{0}] を値 [{1}] ã§è¿½åŠ ã—ã¦ã„ã¾ã™ + +defaultInstanceManager.invalidAnnotation=無効ãªã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ [{0}] +defaultInstanceManager.invalidInjection=無効ãªãƒ¡ã‚½ãƒƒãƒ‰ãƒªã‚½ãƒ¼ã‚¹ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã§ã™ +defaultInstanceManager.postConstructNotFound=é…備記述å­ã«å®£è¨€ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã® post-construct メソッド [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +defaultInstanceManager.preDestroyNotFound=é…備記述å­ã«å®£è¨€ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã® pre-destroy メソッド [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +defaultInstanceManager.restrictedClass=制é™ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{0}] ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã¯æ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚制é™ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ã‚’読ã¿è¾¼ã‚€ã«ã¯ Web アプリケーションã«ç‰¹æ¨©ã‚’構æˆã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +defaultInstanceManager.restrictedContainerServlet=クラス [{0}] ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã¯ç¦æ­¢ã•ã‚Œã¦ã„ã¾ã™ã€‚制é™ã‚¯ãƒ©ã‚¹ã§ã™ (ContainerServlet インターフェイスを実装ã—ã¦ã„ã¾ã™)。アクセスå¯èƒ½ã«ã™ã‚‹ã«ã¯Webアプリケーションã«ç‰¹æ¨©ã‚’構æˆã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +defaultInstanceManager.restrictedFiltersResource=制é™ãƒ•ã‚£ãƒ«ã‚¿ãƒ¼ã®ãƒ—ロパティファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ [{0}] +defaultInstanceManager.restrictedListenersResource=制é™ä»˜ãリスナープロパティファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠[{0}] +defaultInstanceManager.restrictedServletsResource=Restricted サーブレットプロパティファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。[{0}] +defaultInstanceManager.restrictedWrongValue=クラスå[{1}]ã®åˆ¶é™ä»˜ãクラスã®ãƒ—ロパティファイル[{0}]ã®å€¤ãŒé–“é•ã£ã¦ã„ã¾ã™ã€‚ 期待値:[制é™]ã€å®Ÿéš›å€¤ï¼š[{2}] + +filterChain.filter=フィルタã®å®Ÿè¡Œã«ã‚ˆã‚Šä¾‹å¤–を投ã’ã¾ã—㟠+filterChain.servlet=サーブレットã®å®Ÿè¡Œã«ã‚ˆã‚Šä¾‹å¤–を投ã’ã¾ã—㟠+ +jniLifecycleListener.bothPathAndName=libraryName ã‚ã‚‹ã„㯠libraryPath ã®ã©ã¡ã‚‰ã‹ä¸€æ–¹ã‚’設定ã™ã‚‹ã¯ãšã§ã™ã€‚ +jniLifecycleListener.load.name=ãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリ [{0}] を読ã¿è¾¼ã¿ã¾ã—ãŸã€‚ +jniLifecycleListener.load.path=[{0}] ã‹ã‚‰ãƒã‚¤ãƒ†ã‚£ãƒ–ライブラリを読ã¿è¾¼ã¿ã¾ã—ãŸã€‚ +jniLifecycleListener.missingPathOrName=libraryName ã‚ã‚‹ã„㯠libraryPath を設定ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 + +jreLeakListener.classToInitializeFail=Tomcat起動中ã«å¯èƒ½ãªãƒ¡ãƒ¢ãƒªãƒ¼ãƒªãƒ¼ã‚¯ã‚’防止ã™ã‚‹ãŸã‚ã®ã‚¯ãƒ©ã‚¹ [{0}] をロードã™ã‚‹ã“ã¨ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ + +listener.notContext=ã“ã®ãƒªã‚¹ãƒŠãƒ¼ã¯ Context è¦ç´ å†…ã§ã®ã¿ãƒã‚¹ãƒˆã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“ãŒã€[{0}] 内ã«ã‚ã‚Šã¾ã™ã€‚ +listener.notServer=ã“ã®listenerã¯Serverè¦ç´ å†…ã«ã®ã¿ãƒã‚¹ãƒˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ãŒã€[{0}] ã«ã‚ã‚Šã¾ã™ã€‚ + +naming.addEnvEntry=環境エントリ [{0}] を追加ã—ã¾ã™ +naming.addResourceEnvRef=リソースenv ref [{0}]を追加ã—ã¦ã„ã¾ã™ã€‚ +naming.addResourceLink=リソースリンク [{0}] を追加ã—ã¾ã™ +naming.addResourceRef=リソースå‚ç…§ [{0}] ã‚’ [{1}] ã«è¿½åŠ ã—ã¾ã™ +naming.addService=サービスå‚ç…§ [{0}] ã‚’ [{1}] ã«è¿½åŠ ã—ã¾ã™ +naming.addSlash=サービスURLã‚’ [/{0}] ã«å¤‰æ›´ã—ã¾ã™ +naming.bind=ãƒãƒ¼ãƒŸãƒ³ã‚°ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’コンテナ [{0}] ã«ãƒã‚¤ãƒ³ãƒ‰ã—ã¾ã™ +naming.bindFailed=オブジェクトã®ãƒã‚¤ãƒ³ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸ: [{0}] +naming.invalidEnvEntryType=環境エントリ [{0}] ã¯ç„¡åŠ¹ãªåž‹ã‚’æŒã£ã¦ã„ã¾ã™ +naming.invalidEnvEntryValue=環境エントリ [{0}] ã¯ç„¡åŠ¹ãªå€¤ã‚’æŒã£ã¦ã„ã¾ã™ +naming.jmxRegistrationFailed=JMX ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸ: [{0}] +naming.namingContextCreationFailed=åå‰ä»˜ãコンテキストã®ç”Ÿæˆã«å¤±æ•—ã—ã¾ã—ãŸ: [{0}] +naming.unbindFailed=オブジェクトã®ã‚¢ãƒ³ãƒã‚¤ãƒ³ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸ: [{0}] +naming.wsdlFailed=wsdl ファイル [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ + +noPluggabilityServletContext.notAllowed=Servlet 3.0仕様ã®4.4節ã§ã¯ã€web.xmlã«å®šç¾©ã•ã‚Œã¦ã„ãªã„ServletContextListenerã€web-fragment.xmlファイルã€@WebListenerアノテーションã‹ã‚‰ã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ã‚’呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 + +propertiesRoleMappingListener.linkedRole=アプリケーション ロール [{0}] を技術ロール [{1}] ã«ãƒªãƒ³ã‚¯ã—ã¾ã—㟠+propertiesRoleMappingListener.linkedRoleCount=アプリケーション ロール [{0}] を技術ロールã«ãƒªãƒ³ã‚¯ã—ã¾ã—㟠+propertiesRoleMappingListener.roleMappingFileEmpty=ロール マッピング ファイルを空ã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +propertiesRoleMappingListener.roleMappingFileFail=ロール マッピング ファイル [{0}] ã®ãƒ­ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—㟠+propertiesRoleMappingListener.roleMappingFileNull=ロール マッピング ファイルを null ã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ + +pushBuilder.noPath=path を設定ã™ã‚‹å‰ã« push() を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 + +standardContext.applicationListener=クラス [{0}] ã®ã‚¢ãƒ—リケーションリスナã®è¨­å®šä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+standardContext.applicationSkipped=å‰ã®ã‚¨ãƒ©ãƒ¼ã®ãŸã‚ã«ã‚¢ãƒ—リケーションリスナã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã‚’スキップã—ã¾ã™ +standardContext.backgroundProcess.instanceManager=インスタンスマãƒãƒ¼ã‚¸ãƒ£ [{0}] ã®ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ãƒ—ロセス処ç†ä¸­ã®ä¾‹å¤– +standardContext.backgroundProcess.loader=Loader[{0}]ã®ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ãƒ—ロセス処ç†ä¸­ã«ä¾‹å¤–ãŒç™ºç”Ÿ +standardContext.backgroundProcess.manager=マãƒãƒ¼ã‚¸ãƒ£ [{0}] ã®ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ãƒ—ロセス処ç†ä¸­ã®ä¾‹å¤– +standardContext.backgroundProcess.resources=リソース [{0}] ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ãƒ—ロセス処ç†ä¸­ã®ä¾‹å¤– +standardContext.cluster.managerError=æ–°è¦ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ã‚»ãƒƒã‚·ãƒ§ãƒ³ãƒžãƒãƒ¼ã‚¸ãƒ£ç”Ÿæˆä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardContext.cluster.noManager=ManagerãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ ClusterManagerを使用ã™ã‚‹ã‹ã©ã†ã‹ã®ç¢ºèªã€‚ クラスタ構æˆï¼š[{0}]ã€ã‚¢ãƒ—リケーションé…布å¯èƒ½ï¼š[{1}] +standardContext.configurationFail=コンテキストãŒæ­£ã—ã設定ã•ã‚Œã¦ã„ãªã„ã¨ãƒžãƒ¼ã‚¯ã•ã‚ŒãŸ1ã¤ä»¥ä¸Šã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ +standardContext.cookieProcessor.null=コンテキスト㮠CookieProcessor ã«ã¯ null を指定ã§ãã¾ã›ã‚“。 +standardContext.createWrapper.containerListenerError=Wrapperã®ã‚³ãƒ³ãƒ†ãƒŠãƒªã‚¹ãƒŠãƒ¼ç”Ÿæˆä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardContext.createWrapper.error=æ–°è¦Wrapper作æˆä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardContext.createWrapper.listenerError=Wrapperã®ãƒ©ã‚¤ãƒ•ã‚µã‚¤ã‚¯ãƒ«ãƒªã‚¹ãƒŠãƒ¼ç”Ÿæˆä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardContext.duplicateListener=リスナー[{0}]ã¯ã€ã™ã§ã«ã“ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ã€‚é‡è¤‡å®šç¾©ã¯ç„¡è¦–ã•ã‚Œã¾ã—ãŸã€‚ +standardContext.errorPage.error=エラーページã®ä½ç½® [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.errorPage.required=ErrorPage 㯠null ã«ã§ãã¾ã›ã‚“ +standardContext.errorPage.warning=警告: Servlet 2.4ã§ã¯ã‚¨ãƒ©ãƒ¼ãƒšãƒ¼ã‚¸ã®å ´æ‰€ [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.filterFail=1ã¤ã¾ãŸã¯è¤‡æ•°ã®ãƒ•ã‚£ãƒ«ã‚¿ã‚’開始ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚完全ãªè©³ç´°ã¯é©åˆ‡ãªã‚³ãƒ³ãƒ†ãƒŠãƒ­ã‚°ãƒ•ã‚¡ã‚¤ãƒ«ã«ã‚ã‚Šã¾ã™ã€‚ +standardContext.filterMap.either=フィルタマッピングã¯ã¾ãŸã¯ã®ã©ã¡ã‚‰ã‹ã‚’指定ã—ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.filterMap.name=フィルタマッピングã¯æœªçŸ¥ã®ãƒ•ã‚£ãƒ«ã‚¿å [{0}] を指定ã—ã¾ã—㟠+standardContext.filterMap.pattern=フィルタマッピング中ã«ç„¡åŠ¹ãª [{0}] ãŒã‚ã‚Šã¾ã™ +standardContext.filterStart=フィルタ [{0}] ã®èµ·å‹•ä¸­ã®ä¾‹å¤–ã§ã™ +standardContext.invalidWrapperClass=クラス [{0}] 㯠StandardWrapper ã®æ´¾ç”Ÿã‚¯ãƒ©ã‚¹ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +standardContext.isUnavailable=ã“ã®ã‚¢ãƒ—リケーションã¯ç¾åœ¨åˆ©ç”¨ã§ãã¾ã›ã‚“ +standardContext.listenerFail=1ã¤ã¾ãŸã¯è¤‡æ•°ã®ãƒªã‚¹ãƒŠãƒ¼ãŒé–‹å§‹ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ 完全ãªè©³ç´°ã¯é©åˆ‡ãªã‚³ãƒ³ãƒ†ãƒŠãƒ­ã‚°ãƒ•ã‚¡ã‚¤ãƒ«ã«ã‚ã‚Šã¾ã™ã€‚ +standardContext.listenerStart=クラス [{0}] ã®ãƒªã‚¹ãƒŠã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆåˆæœŸåŒ–イベントをé€ä¿¡ä¸­ã®ä¾‹å¤–ã§ã™ +standardContext.listenerStop=クラス [{0}] ã®ãƒªã‚¹ãƒŠã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆç ´æ£„イベントをé€ä¿¡ä¸­ã®ä¾‹å¤–ã§ã™ +standardContext.loadOnStartup.loadException=Web アプリケーション [{0}] ã®ã‚µãƒ¼ãƒ–レット [{1}] ã® load() メソッドã¯ä¾‹å¤–を投ã’ã¾ã—ãŸã€‚ +standardContext.loginConfig.errorPage=フォームã®ã‚¨ãƒ©ãƒ¼ãƒšãƒ¼ã‚¸ [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.loginConfig.errorWarning=警告: Servlet 2.4ã§ã¯ãƒ•ã‚©ãƒ¼ãƒ ã®ã‚¨ãƒ©ãƒ¼ãƒšãƒ¼ã‚¸ [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.loginConfig.loginPage=フォームã®ãƒ­ã‚°ã‚¤ãƒ³ãƒšãƒ¼ã‚¸ [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.loginConfig.loginWarning=警告: Servlet 2.4ã§ã¯ãƒ•ã‚©ãƒ¼ãƒ ã®ãƒ­ã‚°ã‚¤ãƒ³ãƒšãƒ¼ã‚¸ [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.loginConfig.required=LoginConfigã¯nullã§ã¯ã„ã‘ã¾ã›ã‚“ +standardContext.manager=クラス [{0}] ã‚’Managerã¨ã—ã¦æ§‹æˆã—ã¾ã—ãŸã€‚ +standardContext.managerFail=セッションマãƒãƒ¼ã‚¸ãƒ£ãƒ¼ã‚’開始ã§ãã¾ã›ã‚“。 +standardContext.namingResource.destroy.fail=以å‰ã®åå‰ä»˜ãリソースを破棄ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +standardContext.namingResource.init.fail=æ–°ã—ã„ãƒãƒ¼ãƒŸãƒ³ã‚°ãƒªã‚½ãƒ¼ã‚¹ã®åˆæœŸåŒ–ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +standardContext.notStarted=åå‰ [{0}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã¯ã¾ã èµ·å‹•ã•ã‚Œã¦ã„ã¾ã›ã‚“ +standardContext.notWrapper=Context ã®å­ä¾›ã¯ Wrapper ã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.parameter.duplicate=コンテキストåˆæœŸåŒ–パラメタ [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +standardContext.parameter.required=パラメタåã¨ãƒ‘ラメタ値ã®ä¸¡æ–¹ãŒå¿…è¦ã§ã™ +standardContext.pathInvalid=コンテキストパスã¯ç©ºæ–‡å­—列ã‚ã‚‹ã„㯠"/" ã§é–‹å§‹ã— "/" ã§çµ‚了ã—ãªã„文字列ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。パス [{0}] ã¯ã“ã®æ¡ä»¶ã‚’満ãŸã•ãªã„ãŸã‚ [{1}] ã¸å¤‰æ›´ã—ã¾ã—ãŸã€‚ +standardContext.postconstruct.duplicate=クラス [{0}] ã®post constructメソッド定義ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +standardContext.postconstruct.required=完全é™å®šã‚¯ãƒ©ã‚¹åã¨ãƒ¡ã‚½ãƒƒãƒ‰åã®ä¸¡æ–¹ãŒå¿…è¦ã§ã™ã€‚ +standardContext.predestroy.duplicate=クラス [{0}] ã®ãƒ¡ã‚½ãƒƒãƒ‰å®šç¾©ã« @PreDestroy ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ã€‚ +standardContext.predestroy.required=完全修飾クラスåã¨ãƒ¡ã‚½ãƒƒãƒ‰åã®ä¸¡æ–¹ãŒå¿…è¦ã§ã™ã€‚ +standardContext.reloadingCompleted=åå‰ [{0}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã®ãƒªãƒ­ãƒ¼ãƒ‰ãŒå®Œäº†ã—ã¾ã—㟠+standardContext.reloadingStarted=åå‰ [{0}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã®ãƒªãƒ­ãƒ¼ãƒ‰ã‚’開始ã—ã¾ã—㟠+standardContext.requestListener.requestDestroyed=Listener クラス [{0}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆç ´æ£„ライフサイクルイベントをé€ä¿¡ã™ã‚‹ã¨ãã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—㟠+standardContext.requestListener.requestInit=クラス [{0}] ã®ãƒªã‚¹ãƒŠã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«åˆæœŸåŒ–ã™ã‚‹ãƒ©ã‚¤ãƒ•ã‚µã‚¤ã‚¯ãƒ«ã‚¤ãƒ™ãƒ³ãƒˆã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ä¿¡ä¸­ã®ä¾‹å¤–ã§ã™ +standardContext.resetContextFail=[{0}] ã¨ã„ã†åå‰ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’リセット中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+standardContext.resourcesInit=é™çš„リソースã®åˆæœŸåŒ–エラー +standardContext.resourcesStart=é™çš„リソース開始中ã®ã‚¨ãƒ©ãƒ¼ +standardContext.resourcesStop=é™çš„リソースã®åœæ­¢ä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardContext.sciFail=ServletContainerInitializer ã®å‡¦ç†ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +standardContext.securityConstraint.mixHttpMethod=1ã¤ã® Web リソースコレクション㫠㨠を両方指定ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +standardContext.securityConstraint.pattern=セキュリティã®åˆ¶ç´„ã®ä¸­ã«ç„¡åŠ¹ãª [{0}] ãŒã‚ã‚Šã¾ã™ +standardContext.servletFail=起動時ã«ä¸€ã¤ä»¥ä¸Šã®ã‚µãƒ¼ãƒ–レットã®èª­ã¿è¾¼ã¿ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚詳細ã¯é©åˆ‡ãªã‚³ãƒ³ãƒ†ãƒŠãƒ¼ã®ãƒ­ã‚°ãƒ•ã‚¡ã‚¤ãƒ«ã‚’確èªã—ã¦ãã ã•ã„。 +standardContext.servletMap.name=サーブレットマッピングã¯æœªçŸ¥ã®ã‚µãƒ¼ãƒ–レットå [{0}] を指定ã—ã¦ã„ã¾ã™ +standardContext.servletMap.pattern=サーブレットマッピング中ã«ç„¡åŠ¹ãª [{0}] ãŒã‚ã‚Šã¾ã™ +standardContext.setLoader.start=æ–°ã—ã„クラスローダーを開始ã§ãã¾ã›ã‚“。 +standardContext.setLoader.stop=å¤ã„クラスローダーã®åœæ­¢ä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardContext.setManager.start=æ–°è¦ãƒžãƒãƒ¼ã‚¸ãƒ£ãƒ¼é–‹å§‹ä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardContext.setManager.stop=å¤ã„マãƒãƒ¼ã‚¸ãƒ£ãƒ¼ã‚’åœæ­¢ã§ãã¾ã›ã‚“。 +standardContext.startFailed=以å‰ã®ã‚¨ãƒ©ãƒ¼ã®ãŸã‚ã«ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã®èµ·å‹•ãŒå¤±æ•—ã—ã¾ã—㟠[{0}] +standardContext.startingContext=åå‰[{0}]ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’開始ã™ã‚‹éš›ã®ä¾‹å¤– +standardContext.stop.asyncWaitInterrupted=実行中ã®éžåŒæœŸè¦æ±‚ãŒå®Œäº†ã™ã‚‹ã®ã‚’unloadDelayミリ秒待ã£ã¦ã„ã‚‹é–“ã«å‰²ã‚Šè¾¼ã¿ã‚’å—ä¿¡ã—ã¾ã—ãŸã€‚ コンテキストåœæ­¢ã¯ã•ã‚‰ã«é…れるã“ã¨ãªã続行ã•ã‚Œã¾ã™ã€‚ +standardContext.stoppingContext=åå‰[{0}]ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’åœæ­¢ã™ã‚‹éš›ã®ä¾‹å¤– +standardContext.suspiciousUrl=コンテキスト [{1}] ã«ä¸å¯©ãª URL パターン [{0}] ãŒã‚ã‚Šã¾ã™ã€‚サーブレット仕様㮠12.1 ãŠã‚ˆã³ 12.2 ã‚’å‚ç…§ã—ã¦ãã ã•ã„。 +standardContext.threadBindingListenerError=コンテキスト [{0}] ã«æ§‹æˆã•ã‚ŒãŸãƒªã‚¹ãƒŠãƒ¼ã‚’æŸç¸›ã™ã‚‹ã‚¹ãƒ¬ãƒƒãƒ‰ã§ç•°å¸¸ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +standardContext.urlPattern.patternWarning=警告: Servlet 2.4ã§ã¯URLパターン [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardContext.workCreateException=コンテキスト [{2}] ã®ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª [{0}] ã¨CATALINA_HOME [{1}] ã‹ã‚‰ã®çµ¶å¯¾workディレクトリを特定ã§ãã¾ã›ã‚“ã§ã—㟠+standardContext.workCreateFail=コンテキスト [{1}] ã®ä½œæ¥­ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª [{0}] を作æˆã§ãã¾ã›ã‚“。 +standardContext.workPath=コンテキスト [{0}] ã®workパスをå–得中ã®ä¾‹å¤– + +standardContextValve.acknowledgeException=100 (Continue) レスãƒãƒ³ã‚¹ã§ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’確èªã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ + +standardEngine.notHost=Engineã®å­ä¾›ã¯Hostã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardEngine.notParent=エンジンã¯è¦ªã®ã‚³ãƒ³ãƒ†ãƒŠã‚’æŒã¤ã“ã¨ã¯ã§ãã¾ã›ã‚“ +standardEngine.start=サーブレットエンジンã®èµ·å‹•ï¼š[{0}] + +standardHost.clientAbort=リモートクライアントãŒãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’中止ã—ã¾ã—ãŸ, IOException: [{0}] +standardHost.invalidErrorReportValveClass=指定ã•ã‚ŒãŸErrorReportValveクラスをロードã§ãã¾ã›ã‚“: [{0}] +standardHost.noContext=ã“ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã™ã‚‹ãŸã‚ã«è¨­å®šã•ã‚ŒãŸã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãŒã‚ã‚Šã¾ã›ã‚“ +standardHost.notContext=Host ã®å­ä¾›ã¯Contextã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardHost.nullName=ホストåãŒå¿…è¦ã§ã™ +standardHost.problematicAppBase=ホスト [{0}] ã®appBaseã«ç©ºã®æ–‡å­—列を使用ã™ã‚‹ã¨ã€CATALINA_BASEã«è¨­å®šã•ã‚Œã¾ã™ãŒã€ã“ã‚Œã¯æ‚ªã„考ãˆã§ã™ +standardHost.problematicLegacyAppBase=ホスト [{0}] ã®legacyAppBaseã«ç©ºã®æ–‡å­—列を使用ã™ã‚‹ã¨ã€CATALINA_BASEã«è¨­å®šã•ã‚Œã¾ã™ã€‚ã“ã‚Œã¯æ‚ªã„考ãˆã§ã™ + +standardHostValve.customStatusFailed=カスタムエラーページ [{0}] ã‚’æ­£ã—ãディスパッãƒã§ãã¾ã›ã‚“ã§ã—㟠+standardHostValve.exception=ä¾‹å¤–å‡¦ç† [{0}] + +standardPipeline.basic.start=æ–°ã—ã„基本 Valve を開始ã§ãã¾ã›ã‚“。 +standardPipeline.basic.stop=å¤ã„基本 Valve ã‚’åœæ­¢ã§ãã¾ã›ã‚“。 +standardPipeline.valve.destroy=Valve を破棄ã§ãã¾ã›ã‚“。 +standardPipeline.valve.start=Valve を開始ã§ãã¾ã›ã‚“。 +standardPipeline.valve.stop=Valve ã‚’åœæ­¢ã§ãã¾ã›ã‚“。 + +standardServer.accept.error=シャットダウンコマンドをå—ä¿¡ã™ã‚‹ã‚½ã‚±ãƒƒãƒˆã® accept ã§å…¥å‡ºåŠ›ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +standardServer.accept.readError=シャットダウンコマンドã®èª­ã¿å–り時ã«å…¥å‡ºåŠ›ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +standardServer.accept.security=シャットダウンコマンドをå—ä¿¡ã™ã‚‹ã‚½ã‚±ãƒƒãƒˆã® accept ã§ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã‚¨ãƒ©ãƒ¼ã‚’発生ã—ã¾ã—ãŸã€‚ +standardServer.accept.timeout=シャットダウンコマンドをリスンã™ã‚‹ã‚½ã‚±ãƒƒãƒˆã¯ã€accept()ã®å‘¼ã³å‡ºã—後ã«äºˆæœŸã—ãªã„タイムアウト [{0}] ミリ秒を経験ã—ã¾ã—ãŸã€‚ ã“ã‚Œã¯ãƒã‚°56684ã®ä¸€ä¾‹ã§ã™ã‹ï¼Ÿ +standardServer.awaitSocket.fail=アドレス [{0}] ã®ãƒãƒ¼ãƒˆç•ªå· [{1}] ã«ã‚µãƒ¼ãƒãƒ¼åœæ­¢ã‚½ã‚±ãƒƒãƒˆã‚’作æˆã§ãã¾ã›ã‚“ã§ã—㟠(基本ãƒãƒ¼ãƒˆç•ªå·ã¯ [{2}]ã€ã‚ªãƒ•ã‚»ãƒƒãƒˆã¯ [{3}] ã§ã™) +standardServer.invalidShutdownCommand=ä¸æ­£ãªã‚·ãƒ£ãƒƒãƒˆãƒ€ã‚¦ãƒ³ã‚³ãƒžãƒ³ãƒ‰ [{0}] ã‚’å—ä¿¡ã—ã¾ã—ãŸã€‚ +standardServer.periodicEventError=定期イベントé€ä¿¡ä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardServer.portOffset.invalid=portOffsetãŒè² ã§ãªã„å¯èƒ½æ€§ãŒã‚ã‚‹ãŸã‚ã€portOffsetã®å€¤[{0}]ã¯ç„¡åŠ¹ã§ã™ +standardServer.shutdownViaPort=有効ãªã‚·ãƒ£ãƒƒãƒˆãƒ€ã‚¦ãƒ³ã‚³ãƒžãƒ³ãƒ‰ãŒã‚·ãƒ£ãƒƒãƒˆãƒ€ã‚¦ãƒ³ãƒãƒ¼ãƒˆçµŒç”±ã§å—ä¿¡ã•ã‚Œã¾ã—ãŸã€‚ サーãƒãƒ¼ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’åœæ­¢ã—ã¾ã™ã€‚ +standardServer.storeConfig.contextError=コンテキスト [{0}] ã®æ§‹æˆæ ¼ç´ä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardServer.storeConfig.error=サーãƒãƒ¼æ§‹æˆæ ¼ç´ä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardServer.storeConfig.notAvailable=StoreConfig実装㯠[{0}] ã¨ã„ã†åå‰ã®MBeanã¨ã—ã¦ç™»éŒ²ã•ã‚Œã¦ã„ãªã„ãŸã‚ã€è¨­å®šã‚’ä¿å­˜ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。é©åˆ‡ãªMBeanã¯é€šå¸¸StoreConfigLifecycleListenerを介ã—ã¦ç™»éŒ²ã•ã‚Œã¾ã™ã€‚ + +standardService.engine.startFailed=関連付ã‘られãŸEngineã®èµ·å‹•ã«å¤±æ•—ã—ã¾ã—㟠+standardService.engine.stopFailed=関連付ã‘られãŸEngineã®åœæ­¢ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +standardService.executor.start=æ–°ã—ã„エグゼキュータを開始中ã®ã‚¨ãƒ©ãƒ¼ +standardService.executor.stop=å¤ã„エグゼキュータã®åœæ­¢ä¸­ã®ã‚¨ãƒ©ãƒ¼ +standardService.mapperListener.startFailed=関連付ã‘られãŸMapperListenerã®èµ·å‹•ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +standardService.mapperListener.stopFailed=関連付ã‘られãŸMapperListenerã®åœæ­¢ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +standardService.start.name=サービス [{0}] ã‚’èµ·å‹•ã—ã¾ã™ +standardService.stop.name=サービス [{0}] ã‚’åœæ­¢ã—ã¾ã™ + +standardThreadExecutor.notStarted=エグゼキュータã¯é–‹å§‹ã—ã¦ã„ã¾ã›ã‚“ + +standardVirtualThreadExecutor.notStarted=エグゼキュータãŒèµ·å‹•ã•ã‚Œã¦ã„ã¾ã›ã‚“ + +standardWrapper.allocate=サーブレットインスタンスを割り当ã¦ä¸­ã®ã‚¨ãƒ©ãƒ¼ã§ã™ +standardWrapper.allocateException=サーブレット [{0}] ã«ä¾‹å¤–を割り当ã¦ã¾ã™ +standardWrapper.deallocateException=サーブレット [{0}] ã«å¯¾ã™ã‚‹ä¾‹å¤–ã®å‰²ã‚Šå½“ã¦ã‚’解除ã—ã¾ã™ +standardWrapper.destroyException=サーブレット [{0}] ã®Servlet.destroy()ãŒä¾‹å¤–を投ã’ã¾ã—㟠+standardWrapper.destroyInstance=サーブレット [{0}] ã® InstanceManager.destroy() ãŒä¾‹å¤–ã‚’é€å‡ºã—ã¾ã—ãŸã€‚ +standardWrapper.initException=サーブレット [{0}] ã®Servlet.init()ãŒä¾‹å¤–を投ã’ã¾ã—㟠+standardWrapper.instantiate=サーブレットクラス [{0}] ã‚’åˆæœŸåŒ–中ã®ã‚¨ãƒ©ãƒ¼ +standardWrapper.isUnavailable=サーブレット [{0}] ã¯ç¾åœ¨åˆ©ç”¨ã§ãã¾ã›ã‚“ +standardWrapper.jspMonitorError=JSP 監視 MBean [{0}] 登録中ã®ã‚¨ãƒ©ãƒ¼ +standardWrapper.notChild=Wrapper コンテナã¯å­ä¾›ã®ã‚³ãƒ³ãƒ†ãƒŠã‚’æŒã¤ã“ã¨ã¯ã§ãã¾ã›ã‚“ +standardWrapper.notClass=サーブレット [{0}] ã«æŒ‡å®šã•ã‚ŒãŸã‚µãƒ¼ãƒ–レットクラスãŒã‚ã‚Šã¾ã›ã‚“ +standardWrapper.notContext=Wrapper ã®è¦ªã®ã‚³ãƒ³ãƒ†ãƒŠã¯Contextã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +standardWrapper.notFound=サーブレット [{0}] ãŒåˆ©ç”¨ã§ãã¾ã›ã‚“ +standardWrapper.notServlet=クラス [{0}] ã¯Servletã§ã¯ã‚ã‚Šã¾ã›ã‚“ +standardWrapper.serviceException=サーブレット [{0}] ã®Servlet.service()ãŒä¾‹å¤–を投ã’ã¾ã—㟠+standardWrapper.serviceExceptionRoot=パス [{1}] ã‚’æŒã¤ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆå†…ã®ã‚µãƒ¼ãƒ–レット [{0}] ã®Servlet.service() ãŒä¾‹å¤– [{2}] ãŒæ ¹æœ¬çš„è¦å› ã¨å…±ã«æŠ•ã’られã¾ã—㟠+standardWrapper.unavailable=サーブレット [{0}] を利用ä¸å¯èƒ½ã«ãƒžãƒ¼ã‚¯ã—ã¾ã™ +standardWrapper.unloadException=サーブレット [{0}] ãŒunload()例外を投ã’ã¾ã—㟠+standardWrapper.unloading=サーブレット [{0}] ãŒãƒ­ãƒ¼ãƒ‰ã•ã‚Œã¦ã„ãªã„ã®ã§ã€å‰²ã‚Šå½“ã¦ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +standardWrapper.waiting=サーブレット [{1}] ã® [{0}] インスタンスãŒå‰²ã‚Šå½“ã¦è§£é™¤ã•ã‚Œã‚‹ã®ã‚’å¾…æ©Ÿã—ã¦ã„ã¾ã™ + +threadLocalLeakPreventionListener.containerEvent.error=コンテナーイベント [{0}] ã®å‡¦ç†ä¸­ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +threadLocalLeakPreventionListener.lifecycleEvent.error=ライフサイクルイベント [{0}] を処ç†ä¸­ã®ä¾‹å¤– diff --git a/java/org/apache/catalina/core/LocalStrings_ko.properties b/java/org/apache/catalina/core/LocalStrings_ko.properties new file mode 100644 index 0000000..3a8d4d8 --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_ko.properties @@ -0,0 +1,306 @@ +# 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. + +applicationContext.addFilter.ise=컨í…스트가 ì´ë¯¸ 초기화ë˜ì—ˆê¸° 때문ì—, í•„í„°ë“¤ì€ ì»¨í…스트 [{0}]ì— ì¶”ê°€ë  ìˆ˜ 없습니다. +applicationContext.addJspFile.iae=JSP íŒŒì¼ [{0}]ì€(는) 유효하지 않습니다. +applicationContext.addListener.iae.cnfe=타입 [{0}]ì˜ ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•  수 없습니다. +applicationContext.addListener.iae.init=타입 [{0}]ì˜ ì¸ìŠ¤í„´ìŠ¤ë¥¼ 리스너로서 추가할 수 없습니다. +applicationContext.addListener.iae.sclNotAllowed=첫번째 ServletContextListenerê°€ 호출ë˜ê³  나면, ë” ì´ìƒ ServletContextListenerë“¤ì„ ì¶”ê°€í•  수 없습니다. +applicationContext.addListener.iae.wrongType=ì§€ì •ëœ íƒ€ìž… [{0}]ì´(ê°€) 요구ë˜ëŠ” 리스너 타입들 ì¤‘ì˜ í•˜ë‚˜ê°€ 아닙니다. +applicationContext.addListener.ise=컨í…스트가 ì´ë¯¸ 초기화 ë˜ì—ˆê¸°ì—, 컨í…스트 [{0}]ì— ë¦¬ìŠ¤ë„ˆë¥¼ 추가할 수 없습니다. +applicationContext.addRole.ise=해당 컨í…스트가 ì´ë¯¸ 초기화ë˜ì–´ 있기ì—, ì—­í• ë“¤ì´ ì»¨í…스트 [{0}]ì— ì¶”ê°€ë  ìˆ˜ 없습니다. +applicationContext.addServlet.ise=컨í…스트 [{0}]ì´(ê°€) ì´ë¯¸ 초기화ë˜ì—ˆê¸°ì—, ì„œë¸”ë¦¿ë“¤ì´ ì¶”ê°€ë  ìˆ˜ 없습니다. +applicationContext.attributeEvent=ì†ì„± ì´ë²¤íŠ¸ 리스너가 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +applicationContext.illegalDispatchPath=애플리케ì´ì…˜ì´ ë¶ˆí—ˆëœ ê²½ë¡œ [{0}](으)ë¡œ RequestDispatcher를 얻으려 ì‹œë„했지만, 해당 경로가 ì¸ì½”ë”©ëœ ë””ë ‰í† ë¦¬ 경로 ì´ë™ ì‹œë„를 í¬í•¨í•˜ê³  있기 때문ì—, 거부ë˜ì—ˆìŠµë‹ˆë‹¤. +applicationContext.invalidFilterName=유효하지 ì•Šì€ í•„í„° ì´ë¦„ [{0}]으로 ì¸í•˜ì—¬, í•„í„° ì •ì˜ë¥¼ 추가할 수 없습니다. +applicationContext.invalidServletName=유효하지 ì•Šì€ ì„œë¸”ë¦¿ ì´ë¦„ ([{0}]) 때문ì—, 서블릿 ì •ì˜ë¥¼ 추가할 수 없습니다. +applicationContext.lookup.error=컨í…스트 [{1}] ë‚´ì˜ ë¦¬ì†ŒìŠ¤ [{0}]ì˜ ìœ„ì¹˜ë¥¼ 결정하지 못했습니다. +applicationContext.mapping.error=매핑 중 오류 ë°œìƒ +applicationContext.requestDispatcher.iae=경로 [{0}]ì´(ê°€) "/" 문ìžë¡œ 시작하지 않습니다. +applicationContext.resourcePaths.iae=경로 [{0}]ì´(ê°€) "/" 문ìžë¡œ 시작하지 않습니다. +applicationContext.role.iae=컨í…스트 [{0}]ì„(를) 위해 ì„ ì–¸ë˜ëŠ” 개별 ì—­í• ì´, ë„ì´ê±°ë‚˜ 빈 문ìžì—´ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +applicationContext.roles.iae=컨í…스트 [{0}]ì„(를) 위해 ì„ ì–¸ëœ ì—­í• ë“¤ì˜ ë°°ì—´ì´ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +applicationContext.setAttribute.namenull=ì†ì„± ì´ë¦„ì´ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +applicationContext.setInitParam.ise=컨í…스트가 ì´ë¯¸ ì´ˆê¸°í™”ëœ í›„ì—, 초기화 파ë¼ë¯¸í„°ë“¤ì´ ì„¤ì •ë  ìˆ˜ 없습니다. +applicationContext.setRequestEncoding.ise=컨í…스트가 ì´ë¯¸ 초기화ë˜ì—ˆê¸°ì—, ìš”ì²­ëœ ì¸ì½”ë”©ì€ ì»¨í…스트 [{0}]ì„(를) 위해 ì„¤ì •ë  ìˆ˜ 없습니다. +applicationContext.setResponseEncoding.ise=컨í…스트가 ì´ë¯¸ 초기화ë˜ì—ˆìœ¼ë¯€ë¡œ, 컨í…스트 [{0}]ì„(를) 위한 ì‘답 ì¸ì½”ë”©ì„ ì„¤ì •í•  수 없습니다. +applicationContext.setSessionTimeout.ise=컨í…스트 [{0}]ì´(ê°€) ì´ë¯¸ 초기화ë˜ì—ˆê¸°ì—, 세션 ì œí•œì‹œê°„ì´ ì„¤ì •í•  수 없습니다. +applicationContext.setSessionTracking.iae.invalid=컨í…스트 [{1}]를 위해 ìš”ì²­ëœ ì„¸ì…˜ 트랙킹 모드 [{0}]ì€(는), ì´ ì»¨í…스트ì—ì„œ 지ì›ë˜ì§€ 않습니다. +applicationContext.setSessionTracking.iae.ssl=컨í…스트 [{0}]ì„(를) 위해 ìš”ì²­ëœ ì„¸ì…˜ 트랙킹 모드들ì´, SSLê³¼ ì ì–´ë„ 다른 하나 ì´ìƒì˜ ëª¨ë“œë“¤ì„ í¬í•¨í–ˆìŠµë‹ˆë‹¤. SSLì€ ë‹¤ë¥¸ 모드들과 함께 ì„¤ì •ë  ìˆ˜ 없습니다. +applicationContext.setSessionTracking.ise=컨í…스트 [{0}]ì„(를) 위한 세션 트랙킹 모드들ì€, 컨í…스트가 실행ë˜ê³  있는 ì¤‘ì— ì„¤ì •ë  ìˆ˜ 없습니다. + +applicationDispatcher.allocateException=서블릿 [{0}]ì„(를) 위한 할당 중 예외 ë°œìƒ +applicationDispatcher.deallocateException=서블릿 [{0}]ì„(를) 위한 할당 í•´ì œ ìž‘ì—… 중 예외 ë°œìƒ +applicationDispatcher.forward.ise=ì‘ë‹µì´ ì´ë¯¸ ì»¤ë°‹ëœ í›„ì—는 forwardí•  수 없습니다. +applicationDispatcher.isUnavailable=서블릿 [{0}]ì€(는) 현재 가용하지 않습니다. +applicationDispatcher.serviceException=서블릿 [{0}]ì„(를) 위한 Servlet.service() í˜¸ì¶œì´ ì˜ˆì™¸ë¥¼ ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +applicationDispatcher.specViolation.request=ì›ë³¸ ServletRequest ë˜ëŠ” wrapëœ ì›ë³¸ ServletRequestê°€ RequestDispatcherë¡œ 전달ë˜ì§€ 않았으며, ì´ëŠ” SRV.8.2와 SRV.14.2.5.1ì— ëŒ€í•œ 위반입니다. +applicationDispatcher.specViolation.response=ì›ëž˜ì˜ ServletResponse ë˜ëŠ” wrappingëœ ì›ëž˜ì˜ ServletResponseê°€ RequestDispatcherì— ì „ë‹¬ë˜ì§€ 않았으며, ì´ëŠ” SRV.8.2와 SRV.14.2.5.1ì„ ìœ„ë°˜í•©ë‹ˆë‹¤. + +applicationFilterConfig.jmxRegisterFail=íƒ€ìž…ì´ [{0}](ì´)ê³  ì´ë¦„ì´ [{1}]ì¸ í•„í„°ë¥¼ 위한 JMX 등ë¡ì´ 실패했습니다. +applicationFilterConfig.jmxUnregister=íƒ€ìž…ì´ [{0}](ì´)ê³  ì´ë¦„ì´ [{1}]ì¸ í•„í„°ë¥¼ 위해, JMX ë“±ë¡ ì œê±°ê°€ 완료ë˜ì—ˆìŠµë‹ˆë‹¤. +applicationFilterConfig.jmxUnregisterFail=íƒ€ìž…ì´ [{0}](ì´)ê³  ì´ë¦„ì´ [{1}]ì¸ í•„í„°ë¥¼ 위한, JMX 등ë¡ì„ 제거하지 못했습니다. +applicationFilterConfig.preDestroy=íƒ€ìž…ì´ [{1}]ì´ê³  ì´ë¦„ì´ [{0}]ì¸ í•„í„°ì— ëŒ€í•´ preDestroy를 호출하지 못했습니다. +applicationFilterConfig.release=íƒ€ìž…ì´ [{1}](ì´)ê³  ì´ë¦„ì´ [{0}]ì¸ í•„í„°ë¥¼ 소멸시키지 못했습니다. + +applicationFilterRegistration.nullInitParam=ì´ë¦„ ë˜ëŠ” ê°’ ë˜ëŠ” 둘 다 ë„ì´ì–´ì„œ, 필터를 위한 초기화 파ë¼ë¯¸í„°ë¥¼ 설정할 수 없습니다. ì´ë¦„: [{0}], ê°’: [{1}] +applicationFilterRegistration.nullInitParams=ë„ì¸ ì´ë¦„ ë˜ëŠ” ê°’ 때문ì—, í•„í„°ì˜ ì´ˆê¸°í™” 파ë¼ë¯¸í„°ë¥¼ 설정할 수 없습니다. ì´ë¦„: [{0}], ê°’: [{1}] + +applicationHttpRequest.fragmentInDispatchPath=디스패치 경로 [{0}](으)로부터 URI fragment를 제거했습니다. +applicationHttpRequest.sessionEndAccessFail=ìš”ì²­ì„ ì°¸ì¡° 해제하는 과정ì—ì„œ, ì„¸ì…˜ì— ëŒ€í•œ ì ‘ê·¼ì„ ì¢…ë£Œì‹œí‚¤ë ¤ 개시하는 중 예외 ë°œìƒ + +applicationPushBuilder.methodInvalid=PUSH ìš”ì²­ì„ ìœ„í•œ HTTP 메소드는 반드시 ìºì‹œ 가능하고 안전해야 하는ë°, [{0}]ì€(는) 그렇지 않습니다. +applicationPushBuilder.methodNotToken=HTTP ë©”ì†Œë“œë“¤ì€ í† í°ë“¤ì´ì–´ì•¼ 하지만, [{0}]ì€(는) 토í°ì´ ì•„ë‹Œ 문ìžë¥¼ í¬í•¨í•˜ê³  있습니다. + +applicationServletRegistration.setServletSecurity.iae=[{1}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì»¨í…ìŠ¤íŠ¸ì— ë°°ì¹˜ëœ ì„œë¸”ë¦¿ [{0}]ì„(를) 위해, ë„ constraintê°€ 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +applicationServletRegistration.setServletSecurity.ise=컨í…스트가 ì´ë¯¸ 초기화ë˜ì—ˆê¸°ì—, [{1}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì»¨í…ìŠ¤íŠ¸ì— ë°°ì¹˜ëœ ì„œë¸”ë¦¿ [{0}]ì— security constraintë“¤ì´ ì¶”ê°€ë  ìˆ˜ 없습니다. + +applicationSessionCookieConfig.ise=컨í…스트가 ì´ë¯¸ 초기화ë˜ì—ˆê¸°ì—, 컨í…스트 [{1}]ì„(를) 위한 SessionCookieConfigì— í”„ë¡œí¼í‹° [{0}]ì´(ê°€) ì¶”ê°€ë  ìˆ˜ 없습니다. + +aprListener.FIPSProviderNotDefault=AprLifecycleListenerê°€ FIPS 모드 [{0}](으)ë¡œ ì„¤ì •ë  ë•Œì—는 반드시 FIPS 제공ìžê°€ 기본 제공ìžë¡œ 설정ë˜ì–´ì•¼ 합니다. +aprListener.aprDestroy=APR 기반 Apache Tomcat Native ë¼ì´ë¸ŒëŸ¬ë¦¬ë¥¼ 셧다운하지 못했습니다. +aprListener.aprInit=프로ë•ì…˜ 환경들ì—ì„œ 최ì ì˜ ì„±ëŠ¥ì„ ì œê³µí•˜ëŠ”, APR 기반 Apache Tomcat Native ë¼ì´ë¸ŒëŸ¬ë¦¬ê°€, ë‹¤ìŒ java.library.pathì—ì„œ 발견ë˜ì§€ 않습니다: [{0}] +aprListener.aprInitDebug=[{0}](ì´)ë¼ëŠ” ì´ë¦„ë“¤ì„ ì‚¬ìš©í•˜ì—¬, java.library.path [{1}]ì—ì„œ, APR 기반 Apache Tomcat Native ë¼ì´ë¸ŒëŸ¬ë¦¬ë¥¼ ì°¾ì„ ìˆ˜ 없습니다. ë³´ê³ ëœ ì˜¤ë¥˜ë“¤: [{2}] +aprListener.aprInitError=APR 기반 Apache Tomcat Native ë¼ì´ë¸ŒëŸ¬ë¦¬ë¥¼ 로드하지 못했습니다. ë³´ê³ ëœ ì˜¤ë¥˜ëŠ” [{0}]입니다. +aprListener.currentFIPSMode=í˜„ìž¬ì˜ FIPS 모드: [{0}] +aprListener.enterAlreadyInFIPSMode=AprLifecycleListenerê°€ 강제로 FIPS 모드로 들어가ë„ë¡ ì„¤ì •ë˜ì—ˆìœ¼ë‚˜, ë¼ì´ë¸ŒëŸ¬ë¦¬ê°€ ì´ë¯¸ FIPS 모드 [{0}]ì— ìžˆìŠµë‹ˆë‹¤. +aprListener.initializeFIPSFailed=FIPS 모드로 진입하지 못했습니다. +aprListener.initializeFIPSSuccess=FIPS 모드로 성공ì ìœ¼ë¡œ 진입했습니다. +aprListener.initializedOpenSSL=OpenSSLì´ ì„±ê³µì ìœ¼ë¡œ 초기화ë˜ì—ˆìŠµë‹ˆë‹¤: [{0}] +aprListener.initializingFIPS=FIPS 모드 초기화... +aprListener.requireNotInFIPSMode=AprLifecycleListener는 ë¼ì´ë¸ŒëŸ¬ë¦¬ê°€ ì´ë¯¸ FIPS ëª¨ë“œì— ìžˆì–´ì•¼ë§Œ 하ë„ë¡ ì„¤ì •ë˜ì–´ 있는ë°, FIPS ëª¨ë“œì— ìžˆì§€ 않았습니다. +aprListener.skipFIPSInitialization=ì´ë¯¸ FIPS ëª¨ë“œì— ìžˆìŠµë‹ˆë‹¤. FIPS 초기화는 건너ëœë‹ˆë‹¤. +aprListener.sslInit=SSLEngineì„ ì´ˆê¸°í™”í•˜ì§€ 못했습니다. +aprListener.sslRequired=버전 2.x ì´í›„ë¡œ SSLì€ í•„ìˆ˜ì´ë¯€ë¡œ, [{0}]ì€(는), 톰캣 Native ë¼ì´ë¸ŒëŸ¬ë¦¬ 버전 [{1}] 사용 ì‹œ SSLEngineì„ ìœ„í•´ 유효한 ê°’ì´ ì•„ë‹™ë‹ˆë‹¤. +aprListener.tcnInvalid=APR ê¸°ë°˜ì˜ Apache Tomcat Native ë¼ì´ë¸ŒëŸ¬ë¦¬ê°€ 호환ë˜ì§€ 않는 버전 [{0}]ì´(ê°€) 설치ë˜ì–´ 있습니다. Tomcatì€ ë²„ì „ [{1}]ì„(를) 요구합니다. +aprListener.tcnValid=APR 버전 [{1}]ì„(를) 사용한, APR 기반 Apache Tomcat Native ë¼ì´ë¸ŒëŸ¬ë¦¬ [{0}]ì„(를) 로드했습니다. +aprListener.tcnVersion=APR ê¸°ë°˜ì˜ Apache Tomcat Native ë¼ì´ë¸ŒëŸ¬ë¦¬ì˜ 예전 버전 [{0}](ì´)ê°€ 설치ë˜ì–´ 있습니다. Tomcatì€ ìµœì†Œ 버전으로서 [{1}]ì„(를) 추천합니다. +aprListener.tooLateForFIPSMode=setFIPSMode를 수행할 수 없습니다: SSLì´ ì´ë¯¸ 초기화ë˜ì—ˆìŠµë‹ˆë‹¤. +aprListener.tooLateForSSLEngine=setSSLEngineì„ í˜¸ì¶œí•  수 없습니다: SSLì´ ì´ë¯¸ 초기화 ë˜ì—ˆìŠµë‹ˆë‹¤. +aprListener.tooLateForSSLRandomSeed=setSSLRandomSeed를 호출할 수 없습니다: SSLì´ ì´ë¯¸ 초기화ë˜ì–´ 있습니다. +aprListener.usingFIPSProvider=FIPS 제공ìžë¥¼ 기본 제공ìžë¡œ 하는 OpenSSLì„ ì‚¬ìš© 중 +aprListener.wrongFIPSMode=예기치 ì•Šì€ AprLifecycleListenerì˜ FIPSMode 옵션 ê°’: [{0}] + +asyncContextImpl.asyncDispatchError=비ë™ê¸° 디스패치 ë„중 오류 ë°œìƒ +asyncContextImpl.asyncRunnableError=AsyncContext.start()를 통해, 비ë™ê¸°ë¡œ Runnableì„ ì²˜ë¦¬í•˜ëŠ” ë„중 오류 ë°œìƒ +asyncContextImpl.dispatchingStarted=비ë™ê¸° 디스패치 오í¼ë ˆì´ì…˜ì´ ì´ë¯¸ 호출ë˜ì—ˆìŠµë‹ˆë‹¤. ë™ì¼í•œ 비ë™ê¸° 사ì´í´ ë‚´ì—ì„œ, 추가ì ì¸ 비ë™ê¸° 디스패치 오í¼ë ˆì´ì…˜ì€ 허용ë˜ì§€ 않습니다. +asyncContextImpl.fireOnComplete=등ë¡ëœ AsyncListenerë“¤ì— onComplete() ì´ë²¤íŠ¸ë¥¼ 호출합니다. +asyncContextImpl.fireOnError=등ë¡ëœ AsyncListenerë“¤ì— onError() ì´ë²¤íŠ¸ë¥¼ 호출합니다. +asyncContextImpl.fireOnStartAsync=등ë¡ëœ AsyncListenerë“¤ì— onStartAsync() ì´ë²¤íŠ¸ë¥¼ 호출합니다. +asyncContextImpl.fireOnTimeout=등ë¡ëœ AsyncListenerë“¤ì— onTimeout() ì´ë²¤íŠ¸ë¥¼ 호출합니다. +asyncContextImpl.noAsyncDispatcher=ServletContext로부터 ë°˜í™˜ëœ ë””ìŠ¤íŒ¨ì²˜ëŠ” 비ë™ê¸° 디스패치를 지ì›í•˜ì§€ 않습니다. +asyncContextImpl.onCompleteError=타입 [{0}]ì˜ ë¦¬ìŠ¤ë„ˆë¥¼ 위한 onComplete() í˜¸ì¶œì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. +asyncContextImpl.onErrorError=타입 [{0}]ì˜ ë¦¬ìŠ¤ë„ˆë¥¼ 위한 onError() í˜¸ì¶œì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. +asyncContextImpl.onStartAsyncError=타입 [{0}]ì˜ ë¦¬ìŠ¤ë„ˆë¥¼ 위한 onStartAsync() í˜¸ì¶œì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. +asyncContextImpl.onTimeoutError=íƒ€ìž…ì´ [{0}]ì¸ ë¦¬ìŠ¤ë„ˆë¥¼ 위한, onTimeout() í˜¸ì¶œì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. +asyncContextImpl.request.ise=complete() ë˜ëŠ” ì–´ë– í•œ ì¢…ë¥˜ì˜ dispatch() ë©”ì†Œë“œë“¤ì´ í˜¸ì¶œëœ í›„ì—, getRequest()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +asyncContextImpl.requestEnded=AsyncContext와 ì—°ê´€ëœ ìš”ì²­ì€ ì´ë¯¸ 처리를 완료했습니다. +asyncContextImpl.response.ise=complete() ë˜ëŠ” ì–´ë–¤ ì¢…ë¥˜ì˜ dispatch() 메소드ë¼ë„ í˜¸ì¶œëœ ì´í›„ì—는, getResponse()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. + +containerBase.backgroundProcess.cluster=í´ëŸ¬ìŠ¤í„° [{0}]ì„(를) 백그ë¼ìš´ë“œ 프로세스ì—ì„œ 처리하는 중 오류 ë°œìƒ +containerBase.backgroundProcess.error=백그ë¼ìš´ë“œ 쓰레드를 처리하는 중 예외 ë°œìƒ +containerBase.backgroundProcess.realm=Realm [{0}]ì„(를) 백그ë¼ìš´ë“œ 프로세스ì—ì„œ 처리 중 예외 ë°œìƒ +containerBase.backgroundProcess.valve=Valve [{0}]ì˜ ë°±ê·¸ë¼ìš´ë“œ 프로세스를 처리하는 중 예외 ë°œìƒ +containerBase.child.destroy=ìžì‹ 컨테ì´ë„ˆë¥¼ 소멸시키는 중 오류 ë°œìƒ +containerBase.child.notUnique=ìžì‹ 컨테ì´ë„ˆ ì´ë¦„ [{0}]ì´(ê°€) 유ì¼í•˜ì§€ 않습니다. +containerBase.child.start=ìžì‹ 컨테ì´ë„ˆë¥¼ 시작하는 중 오류 ë°œìƒ +containerBase.child.stop=ìžì‹ 컨테ì´ë„ˆë¥¼ 중지시키는 중 오류 ë°œìƒ +containerBase.cluster.start=새로운 í´ëŸ¬ìŠ¤í„°ë¥¼ 시작하는 중 오류 ë°œìƒ +containerBase.cluster.stop=ì´ì „ í´ëŸ¬ìŠ¤í„°ë¥¼ 중지시키는 중 오류 ë°œìƒ +containerBase.nullName=컨테ì´ë„ˆ ì´ë¦„ì€ ë„ì¼ ìˆ˜ 없습니다. +containerBase.realm.start=새로운 realmì„ ì‹œìž‘í•˜ëŠ” 중 오류 ë°œìƒ +containerBase.realm.stop=ì´ì „ realmì„ ì¤‘ì§€ì‹œí‚¤ëŠ” 중 오류 ë°œìƒ +containerBase.threadedStartFailed=ìžì‹ 컨테ì´ë„ˆë¥¼ 시작 중 실패했습니다. +containerBase.threadedStopFailed=ìžì‹ 컨테ì´ë„ˆê°€ 중지ë˜ëŠ” 중 실패했습니다. + +defaultInstanceManager.invalidAnnotation=유효하지 ì•Šì€ [{0}] annotation입니다. +defaultInstanceManager.invalidInjection=유효하지 ì•Šì€ ë©”ì†Œë“œ 리소스 injection annotation +defaultInstanceManager.postConstructNotFound=í´ëž˜ìŠ¤ [{1}]ì„(를) 위한 PostConstruct 메소드 [{0}]ì´(ê°€), 배치 descriptorì— ì„ ì–¸ë˜ì–´ 있지만, 해당 메소드를 ì°¾ì„ ìˆ˜ 없습니다. +defaultInstanceManager.preDestroyNotFound=í´ëž˜ìŠ¤ [{1}]ì„(를) 위한 PreDestroy 메소드 [{0}]ì´(ê°€) 배치 descriptorì— ì„ ì–¸ë˜ì–´ 있으나, 해당 메소드를 ì°¾ì„ ìˆ˜ 없습니다. +defaultInstanceManager.restrictedClass=í´ëž˜ìŠ¤ [{0}]ì— ëŒ€í•œ ì ‘ê·¼ì´ ê¸ˆì§€ë˜ì—ˆìŠµë‹ˆë‹¤. ì ‘ê·¼ ì œí•œëœ í´ëž˜ìŠ¤ìž…니다. ê·¸ê²ƒì„ ë¡œë“œí•  수 있으려면, 웹 애플리케ì´ì…˜ì´ 반드시 privileged ëœ ê²ƒìœ¼ë¡œ 설정ë˜ì–´ì•¼ 합니다. +defaultInstanceManager.restrictedContainerServlet=í´ëž˜ìŠ¤ [{0}]ì„(를) 접근하는 ê²ƒì€ ê¸ˆì§€ë˜ì–´ 있습니다. 해당 í´ëž˜ìŠ¤ëŠ” (ContainerServlet ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 구현하고 있는) ì ‘ê·¼ 제한 í´ëž˜ìŠ¤ìž…니다. 웹 애플리케ì´ì…˜ì´ 만약 ì ‘ê·¼í•˜ê³ ìž í•œë‹¤ë©´ 로드할 수 있는 ê¶Œí•œì´ ì„¤ì •ë˜ì–´ì•¼ 합니다. +defaultInstanceManager.restrictedFiltersResource=제한 í•„í„°ë“¤ì— ëŒ€í•œ 설정 파ì¼ì„ ì°¾ì„ ìˆ˜ 없습니다: [{0}] +defaultInstanceManager.restrictedListenersResource=RestrictedListeners.properties 파ì¼ì„ ì°¾ì„ ìˆ˜ 없습니다: [{0}] +defaultInstanceManager.restrictedServletsResource=제한 ì„œë¸”ë¦¿ë“¤ì— ëŒ€í•œ 프로í¼í‹° 파ì¼ì„ ì°¾ì„ ìˆ˜ 없습니다: [{0}] +defaultInstanceManager.restrictedWrongValue=제한 í´ëž˜ìŠ¤ë“¤ 프로í¼í‹° íŒŒì¼ [{0}]ì— í´ëž˜ìŠ¤ ì´ë¦„ [{1}]ì„(를) 위한 ìž˜ëª»ëœ ê°’ì´ ì„¤ì •ë˜ì—ˆìŠµë‹ˆë‹¤. 요구ë˜ëŠ” ê°’: [restricted], 실제 ê°’: [{2}] + +filterChain.filter=í•„í„° 실행ì—ì„œ 예외가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. +filterChain.servlet=서블릿 ì‹¤í–‰ì´ ì˜ˆì™¸ë¥¼ ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. + +jniLifecycleListener.bothPathAndName=libraryName ë˜ëŠ” libraryPath, 둘 중 하나만 ì„¤ì •ë  ìˆ˜ 있습니다 (둘 다 설정하면 안ë¨). +jniLifecycleListener.load.name=Native ë¼ì´ë¸ŒëŸ¬ë¦¬ [{0}]ì„(를) 로드했습니다. +jniLifecycleListener.load.path=[{0}](으)로부터 native ë¼ì´ë¸ŒëŸ¬ë¦¬ë¥¼ 로드했습니다. +jniLifecycleListener.missingPathOrName=libraryName ë˜ëŠ” libraryPath, 둘 중 하나는 반드시 설정ë˜ì–´ì•¼ 합니다. + +jreLeakListener.classToInitializeFail=Tomcatì„ ì‹œìž‘í•˜ë˜ ì¤‘, ë°œìƒ ê°€ëŠ¥ì„± 있는 메모리 누수를 방지하기 위한 í´ëž˜ìŠ¤ [{0}]ì„(를) 로드하지 못했습니다. + +listener.notServer=리스너 엘리먼트는 서버 엘리먼트 ë‚´ì— ìœ„ì¹˜í•´ì•¼ 합니다만, 현재 [{0}] ë‚´ì— ìžˆìŠµë‹ˆë‹¤. + +naming.addEnvEntry=Environment 엔트리 [{0}]ì„(를) 추가합니다. +naming.addResourceEnvRef=리소스 env ref [{0}]ì„(를) 추가합니다. +naming.bindFailed=ê°ì²´ë¥¼ ë°”ì¸ë”©í•˜ì§€ 못했습니다: [{0}] +naming.invalidEnvEntryType=Environment 엔트리 [{0}]ì´(ê°€) 유효하지 ì•Šì€ íƒ€ìž…ì„ ê°€ì§€ê³  있습니다. +naming.invalidEnvEntryValue=Environment 엔트리 [{0}]ì´(ê°€) 유효하지 ì•Šì€ ê°’ì„ ê°€ì§€ê³  있습니다. +naming.jmxRegistrationFailed=JMX ë‚´ì—ì„œ ë“±ë¡ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤: [{0}] +naming.namingContextCreationFailed=Naming Context ìƒì„± 실패: [{0}] +naming.unbindFailed=ê°ì²´ [{0}]ì„(를) ë°”ì¸ë”© 해제하지 못했습니다. +naming.wsdlFailed=wsdl 파ì¼ì„ 찾지 못했습니다: [{0}] + +noPluggabilityServletContext.notAllowed=Servlet 3.0 ìŠ¤íŽ™ì˜ 4.4 ìž¥ì— ë”°ë¥´ë©´, web.xml ë˜ëŠ” web-fragment.xml 파ì¼ì— ì •ì˜ë˜ì§€ 않거나 @WebListenerë¡œ annotateë˜ì§€ ì•Šì€, ServletContextListenerì—ì„œ ì´ ë©”ì†Œë“œë¥¼ 호출하는 ê²ƒì€ í—ˆìš©ë˜ì§€ 않습니다. + +pushBuilder.noPath=경로를 설정하기 ì „ì— push()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. + +standardContext.applicationListener=í´ëž˜ìŠ¤ [{0}]ì˜ ì• í”Œë¦¬ì¼€ì´ì…˜ 리스너를 설정하는 중 오류 ë°œìƒ +standardContext.applicationSkipped=ì´ì „ 오류(들)ë¡œ ì¸í•˜ì—¬, 애플리케ì´ì…˜ ë¦¬ìŠ¤ë„ˆë“¤ì„ ì„¤ì¹˜í•˜ëŠ” ê²ƒì„ ê±´ë„ˆë›°ì—ˆìŠµë‹ˆë‹¤. +standardContext.backgroundProcess.instanceManager=ì¸ìŠ¤í„´ìŠ¤ 매니저 [{0}]ì„(를) 백그ë¼ìš´ë“œ 프로세스ì—ì„œ 처리 중 예외 ë°œìƒ +standardContext.backgroundProcess.loader=ë¡œë” [{0}]ì„(를) 백그ë¼ìš´ë“œ 프로세스로 처리 중 예외 ë°œìƒ +standardContext.backgroundProcess.manager=매니저 [{0}]ì„(를) 백그ë¼ìš´ë“œ 프로세스로 처리하는 중 예외 ë°œìƒ +standardContext.backgroundProcess.resources=리소스 [{0}]ì„(를) 백그ë¼ìš´ë“œ 프로세스로 처리하는 중 예외 ë°œìƒ +standardContext.cluster.managerError=새로운 í´ëŸ¬ìŠ¤í„° 세션 매니저를 ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ +standardContext.cluster.noManager=매니저가 발견ë˜ì§€ 않습니다. í´ëŸ¬ìŠ¤í„° 매니저가 사용ë˜ì–´ì•¼ 하는지 ì ê²€í•©ë‹ˆë‹¤. ì„¤ì •ëœ í´ëŸ¬ìŠ¤í„°: [{0}], ë°°í¬í•  수 있는 애플리케ì´ì…˜: [{1}] +standardContext.configurationFail=하나 ì´ìƒì˜ 구성요소(들)ì´, 해당 컨í…스트가 올바로 설정ë˜ì§€ 않았다고 표시했습니다. +standardContext.cookieProcessor.null=컨í…스트를 위한 CookieProcessor를 ë„ë¡œ 설정하는 ê²ƒì€ í—ˆìš©ë˜ì§€ 않습니다. +standardContext.createWrapper.containerListenerError=Wrapper를 위한 컨테ì´ë„ˆ 리스너를 ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ +standardContext.createWrapper.error=새로운 wrapper를 ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ +standardContext.createWrapper.listenerError=Wrapper를 위해 Lifecycle 리스너를 ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ +standardContext.duplicateListener=리스너 [{0}]ì´(ê°€) ì´ë¯¸ ì´ ì»¨í…스트를 위해 설정ë˜ì–´ 있습니다. ì¤‘ë³µëœ ì •ì˜ëŠ” 무시ë˜ì—ˆìŠµë‹ˆë‹¤. +standardContext.errorPage.error=오류 페ì´ì§€ 위치 [{0}]ì€(는) 반드시 ''/''ë¡œ 시작해야 합니다. +standardContext.errorPage.required=ErrorPage는 ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +standardContext.errorPage.warning=경고: Servlet 2.4ì—ì„œ 오류 페ì´ì§€ 위치 [{0}]ì€(는) 반드시 ''/''ë¡œ 시작해야 합니다. +standardContext.filterFail=하나 ì´ìƒì˜ í•„í„°ë“¤ì´ ì‹œìž‘í•˜ì§€ 못했습니다. 모든 ìƒì„¸ ì‚¬í•­ì€ ì ì ˆí•œ 컨테ì´ë„ˆÂ ë¡œê·¸ 파ì¼ì—ì„œ ì°¾ì„ ìˆ˜ 있습니다. +standardContext.filterMap.either=í•„í„° ë§¤í•‘ì€ ë°˜ë“œì‹œ ë˜ëŠ” , 둘 중 하나를 지정해야 합니다. +standardContext.filterMap.name=í•„í„° ë§¤í•‘ì´ ì•Œ 수 없는 í•„í„° ì´ë¦„ [{0}]ì„(를) 지정하고 있습니다. +standardContext.filterMap.pattern=í•„í„° 매핑ì—ì„œ 유효하지 ì•Šì€ : [{0}] +standardContext.filterStart=í•„í„° [{0}]ì„(를) 시작하는 중 오류 ë°œìƒ +standardContext.invalidWrapperClass=[{0}]ì€(는) StandardWrapperì˜ í•˜ìœ„ í´ëž˜ìŠ¤ê°€ 아닙니다. +standardContext.isUnavailable=ì´ ì• í”Œë¦¬ì¼€ì´ì…˜ì€ 현재 가용 ìƒíƒœê°€ 아닙니다. +standardContext.listenerFail=하나 ì´ìƒì˜ ë¦¬ìŠ¤ë„ˆë“¤ì´ ì‹œìž‘í•˜ì§€ 못했습니다. ìƒì„¸ ë‚´ì—­ì€ ì ì ˆí•œ 컨테ì´ë„ˆ 로그 파ì¼ì—ì„œ ì°¾ì„ ìˆ˜ 있습니다. +standardContext.listenerStart=Context initialized ì´ë²¤íŠ¸ë¥¼ [{0}] í´ëž˜ìŠ¤ì˜ ì¸ìŠ¤í„´ìŠ¤ì¸ ë¦¬ìŠ¤ë„ˆì— ì „ì†¡í•˜ëŠ” ë™ì•ˆ 예외 ë°œìƒ +standardContext.listenerStop=í´ëž˜ìŠ¤ [{0}]ì˜ ì¸ìŠ¤í„´ìŠ¤ì¸ 리스너ì—게 contextDestroyed ì´ë²¤íŠ¸ë¥¼ 전송하는 중 예외 ë°œìƒ +standardContext.loadOnStartup.loadException=웹 애플리케ì´ì…˜ [{0}] ë‚´ì˜ ì„œë¸”ë¦¿ [{1}]ì´(ê°€) load() 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardContext.loginConfig.errorPage=í¼ ì˜¤ë¥˜ 페ì´ì§€ [{0}]ì€(는) 반드시 "/"ë¡œ 시작해야 합니다. +standardContext.loginConfig.errorWarning=주ì˜: Servlet 2.4ì—ì„œ í¼ ì˜¤ë¥˜ 페ì´ì§€ [{0}]ì€(는) 반드시 "/" ë¡œ 시작해야 합니다. +standardContext.loginConfig.loginPage=í¼ ë¡œê·¸ì¸ íŽ˜ì´ì§€ [{0}]ì€(는) 반드시 ''/''ë¡œ 시작해야 합니다. +standardContext.loginConfig.loginWarning=경고: Servlet 2.4ì—ì„œ í¼ ë¡œê·¸ì¸ íŽ˜ì´ì§€ [{0}]ì€(는) 반드시 ''/''ë¡œ 시작해야 합니다. +standardContext.loginConfig.required=LoginConfigì€(는) ë„ì¼ ìˆ˜ 없습니다. +standardContext.manager=í´ëž˜ìŠ¤ [{0}]ì˜ ë§¤ë‹ˆì € ê°ì²´ë¥¼ 설정했습니다. +standardContext.managerFail=세션 매니저가 시작하지 못했습니다. +standardContext.namingResource.destroy.fail=ì´ì „ Naming 리소스를 소멸시키지 못했습니다. +standardContext.namingResource.init.fail=새로운 Naming ë¦¬ì†ŒìŠ¤ë“¤ì„ ì´ˆê¸°í™”í•˜ì§€ 못했습니다. +standardContext.notStarted=[{0}](ì´)ë¼ëŠ” ì´ë¦„ì„ ê°€ì§„ 컨í…스트는 ì•„ì§ ì‹œìž‘ë˜ì§€ 않았습니다. +standardContext.notWrapper=컨í…ìŠ¤íŠ¸ì˜ ìžì‹ì€ 반드시 Wrapper여야 합니다. +standardContext.parameter.duplicate=ì¤‘ë³µëœ ì»¨í…스트 초기화 파ë¼ë¯¸í„°: [{0}] +standardContext.parameter.required=파ë¼ë¯¸í„° ì´ë¦„ê³¼ 파ë¼ë¯¸í„° ê°’, 둘 다 필수ì ìž…니다. +standardContext.pathInvalid=컨í…스트 경로는 반드시 빈 문ìžì—´ì´ê±°ë‚˜, ë˜ëŠ” ''/''ë¡œ 시작하고 ''/''ë¡œ ë나지 않는 문ìžì—´ì´ì–´ì•¼ 합니다. 해당 경로 [{0}]ì€(는) ì´ ì¡°ê±´ì„ ì¶©ì¡±ì‹œí‚¤ì§€ ì•Šì•„ [{1}](으)ë¡œ 변경ë˜ì—ˆìŠµë‹ˆë‹¤. +standardContext.postconstruct.duplicate=í´ëž˜ìŠ¤ [{0}]ì—ì„œ ì¤‘ë³µëœ PostConstruct 메소드 ì •ì˜ê°€ ë°œê²¬ë¨ +standardContext.postconstruct.required=Fully qualified í´ëž˜ìŠ¤ ì´ë¦„ê³¼ 메소드 ì´ë¦„, 둘 다 필수ì ìž…니다. +standardContext.predestroy.duplicate=í´ëž˜ìŠ¤ [{0}]ì„(를) 위해, ì¤‘ë³µëœ @PreDestroy 메소드 ì •ì˜ìž…니다. +standardContext.predestroy.required=Fully qualified í´ëž˜ìŠ¤ ì´ë¦„ê³¼ 메소드 ì´ë¦„, 둘 다 필수ì ìœ¼ë¡œ 요구ë©ë‹ˆë‹¤. +standardContext.reloadingCompleted=ì´ë¦„ì´ [{0}]ì¸ ì»¨í…스트를 다시 로드하는 ê²ƒì„ ì™„ë£Œí–ˆìŠµë‹ˆë‹¤. +standardContext.reloadingStarted=ì´ë¦„ì´ [{0}]ì¸ ì»¨í…스트를 다시 로드하는 ìž‘ì—…ì´ ì‹œìž‘ë˜ì—ˆìŠµë‹ˆë‹¤. +standardContext.requestListener.requestInit=í´ëž˜ìŠ¤ [{0}]ì˜ ë¦¬ìŠ¤ë„ˆ ì¸ìŠ¤í„´ìŠ¤ì—게, request initialized Lifecycle ì´ë²¤íŠ¸ë¥¼ 보내는 중 예외 ë°œìƒ +standardContext.resourcesInit=ì •ì  ë¦¬ì†ŒìŠ¤ë“¤ì„ ì´ˆê¸°í™”í•˜ëŠ” 중 오류 ë°œìƒ +standardContext.resourcesStart=ì •ì  ë¦¬ì†ŒìŠ¤ë“¤ì„ ì‹œìž‘í•˜ëŠ” 중 오류 ë°œìƒ +standardContext.resourcesStop=ì •ì  ë¦¬ì†ŒìŠ¤ë“¤ì„ ì¤‘ì§€ì‹œí‚¤ëŠ” 중 오류 ë°œìƒ +standardContext.sciFail=ServletContainerInitializer 처리 중 오류 ë°œìƒ +standardContext.securityConstraint.mixHttpMethod=와 ì„ ë™ì¼í•œ web resource collectionì—ì„œ ì„žì–´ì„œ 사용하는 ê²ƒì€ í—ˆìš©ë˜ì§€ 않습니다. +standardContext.securityConstraint.pattern=Security constraint 엘리먼트ì—ì„œ 유효하지 ì•Šì€ [{0}]입니다. +standardContext.servletFail=하나 ì´ìƒì˜ ì„œë¸”ë¦¿ë“¤ì´ ì‹œìž‘ ì‹œì— ì œëŒ€ë¡œ 로드ë˜ì§€ 않았습니다. ìƒì„¸ 정보는 ì ì ˆí•œ 컨테ì´ë„ˆ 로그 파ì¼ì—ì„œ ì°¾ì„ ìˆ˜ 있습니다. +standardContext.servletMap.name=서블릿 ë§¤í•‘ì´ ì•Œ 수 없는 서블릿 ì´ë¦„ [{0}]ì„(를) 지정하고 있습니다. +standardContext.servletMap.pattern=서블릿 매핑ì—ì„œ 유효하지 ì•Šì€ [{0}] +standardContext.setLoader.start=새로운 ë¡œë”를 시작하는 중 오류 ë°œìƒ +standardContext.setLoader.stop=ì´ì „ ë¡œë”를 중지시키는 중 오류 ë°œìƒ +standardContext.setManager.start=새로운 매니저를 시작하는 중 오류 ë°œìƒ +standardContext.setManager.stop=ì´ì „ 매니저를 중지시키는 중 오류 ë°œìƒ +standardContext.startFailed=ì´ì „ 오류들로 ì¸í•´ 컨í…스트 [{0}]ì˜ ì‹œìž‘ì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. +standardContext.startingContext=ì´ë¦„ì´ [{0}]ì¸ ì»¨í…스트를 시작하는 중 예외 ë°œìƒ +standardContext.stop.asyncWaitInterrupted=처리 ì¤‘ì¸ ë¹„ë™ê¸° ìš”ì²­ì´ ì™„ë£Œë˜ê¸°ë¥¼ 기다리기 위해 unloadDelay 밀리초를 대기하는 ë™ì•ˆ ì¸í„°ëŸ½íŠ¸ë¥¼ 받았습니다. ë” ì´ìƒì˜ 지체 ì—†ì´ ì»¨í…스트 중지 ìž‘ì—…ì„ ê³„ì†í•  것입니다. +standardContext.stoppingContext=ì´ë¦„ì´ [{0}]ì¸ ì»¨í…스트를 중지시키는 중 예외 ë°œìƒ +standardContext.suspiciousUrl=ì˜ì‹¬ìŠ¤ëŸ¬ìš´ URL 패턴: 컨í…스트 [{1}] ë‚´ì˜ [{0}]. 서블릿 ìŠ¤íŽ™ì˜ 12.1장과 12.2ìž¥ì„ ì°¸ì¡°í•˜ì‹­ì‹œì˜¤. +standardContext.threadBindingListenerError=컨í…스트 [{0}]ì„(를) 위해 ì„¤ì •ëœ ì“°ë ˆë“œ ë°”ì¸ë”© 리스너ì—ì„œ 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. +standardContext.urlPattern.patternWarning=경고: Servlet 2.4ì—ì„œ URL 패턴 [{0}]ì€(는) 반드시 ''/''ë¡œ 시작해야 합니다. +standardContext.workCreateException=디렉토리 [{0}]와(ê³¼) CATALINA_HOME [{1}](으)로부터, 컨í…스트 [{2}]ì„(를) 위한 ìž‘ì—… ë””ë ‰í† ë¦¬ì˜ ì ˆëŒ€ 경로를 결정하지 못했습니다. +standardContext.workCreateFail=컨í…스트 [{1}]ì„(를) 위한 ìž‘ì—… 디렉토리 [{0}]ì„(를) ìƒì„±í•˜ì§€ 못했습니다. +standardContext.workPath=컨í…스트 [{0}]ì„(를) 위한 ìž‘ì—… 경로를 구하는 중 예외 ë°œìƒ + +standardContextValve.acknowledgeException=ìš”ì²­ì— ëŒ€í•´, 100 (Continue) ì‘답과 함께, ACKì„ ë³´ë‚´ì§€ 못했습니다. + +standardEngine.notHost=ì—”ì§„ì˜ ìžì‹ì€ 반드시 호스트여야 합니다. +standardEngine.notParent=ì—”ì§„ì€ ë¶€ëª¨ 컨테ì´ë„ˆë¥¼ 가질 수 없습니다. +standardEngine.start=서버 ì—”ì§„ì„ ì‹œìž‘í•©ë‹ˆë‹¤: [{0}] + +standardHost.clientAbort=ì›ê²© í´ë¼ì´ì–¸íŠ¸ê°€ ìš”ì²­ì„ ì¤‘ë‹¨ì‹œì¼°ìŠµë‹ˆë‹¤. IOException: [{0}] +standardHost.invalidErrorReportValveClass=ì§€ì •ëœ ì˜¤ë¥˜ ë³´ê³  Valve í´ëž˜ìŠ¤ [{0}]ì„(를) 로드할 수 없었습니다. +standardHost.noContext=ì´ ìš”ì²­ì„ ì²˜ë¦¬í•˜ê¸° 위한 컨í…스트가 설정ë˜ì§€ 않았습니다. +standardHost.notContext=í˜¸ìŠ¤íŠ¸ì˜ ìžì‹ì€ 반드시 컨í…스트ì´ì–´ì•¼ 합니다. +standardHost.nullName=호스트 ì´ë¦„ì´ í•„ìˆ˜ì ìž…니다. +standardHost.problematicAppBase=호스트 [{0}]ì—ì„œ appBase를 위해 빈 문ìžì—´ì„ 사용하는 것ì€, ê²°êµ­ appBase를 CATALINA_BASEë¡œ 설정하게 ë˜ëŠ”ë°, ì´ëŠ” ì¢‹ì€ ìƒê°ì´ 아닙니다. +standardHost.problematicLegacyAppBase=호스트 [{0}]ì— legacyAppBaseë¡œ 빈 문ìžì—´ì´ 설정ë˜ì–´ 있는ë°, ì´ëŠ” CATALINA_BASEë¡œ 재설정 í•  것입니다만, ì´ëŠ” 좋지 ì•Šì€ ì„¤ì • 방법입니다. + +standardHostValve.customStatusFailed=커스텀 오류 페ì´ì§€ [{0}]ì€(는) 올바르게 ë””ìŠ¤íŒ¨ì¹˜ë  ìˆ˜ 없었습니다. + +standardPipeline.basic.start=새로운 기본 Valve를 시작하는 중 오류 ë°œìƒ +standardPipeline.basic.stop=ì´ì „ 기본 Valve를 중지시키는 중 오류 ë°œìƒ +standardPipeline.valve.destroy=Valve를 소멸시키는 중 오류 ë°œìƒ +standardPipeline.valve.start=Valve를 시작시키는 중 오류 ë°œìƒ +standardPipeline.valve.stop=Valve를 중지시키는 중 오류 ë°œìƒ + +standardServer.accept.error=셧다운 ëª…ë ¹ì„ ìœ„í•´ listen하고 있는 소켓ì—ì„œ, accept를 ì‹œë„하는 중, IOExceptionì´ ë°œìƒí–ˆìŠµë‹ˆë‹¤. +standardServer.accept.readError=셧다운 ëª…ë ¹ì„ ì½ìœ¼ë ¤ ì‹œë„하는 중 IOExceptionì´ ë°œìƒí–ˆìŠµë‹ˆë‹¤. +standardServer.accept.security=셧다운 ëª…ë ¹ì„ ìœ„í•´ listen하고 있는 소켓ì—ì„œ, accept를 ì‹œë„하는 중, 보안 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. +standardServer.accept.timeout=셧다운 ëª…ë ¹ì„ ìœ„í•´ listen하고 있는 소켓ì´, accept()를 호출 í•œ 후, 예기치 ì•Šì€ ì œí•œ 시간 초과([{0}] 밀리초)를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. 버그 56684ê°€ ë°œìƒí•œ 경우ì¼ê¹Œìš”? +standardServer.awaitSocket.fail=주소 [{0}]와(ê³¼) í¬íŠ¸ [{1}]ì—, 서버 셧다운 ì†Œì¼“ì„ ìƒì„±í•˜ì§€ 못했습니다. (base í¬íŠ¸ [{2}], offset [{3}]) +standardServer.invalidShutdownCommand=유효하지 ì•Šì€ ì…§ë‹¤ìš´ 명령 [{0}]ì„(를) 받았습니다. +standardServer.periodicEventError=ì£¼ê¸°ì  ì´ë²¤íŠ¸ë¥¼ 전송하는 중 오류 ë°œìƒ +standardServer.portOffset.invalid=portOffsetì´ ìŒìˆ˜ì—¬ì„œëŠ” 안ë˜ê¸°ì—, portOffsetì˜ ê°’ [{0}]ì€(는) 유효하지 않습니다. +standardServer.shutdownViaPort=셧다운 í¬íŠ¸ë¥¼ 통해 유효한 셧다운 ëª…ë ¹ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. 서버 ì¸ìŠ¤í„´ìŠ¤ë¥¼ 중지시킵니다. +standardServer.storeConfig.contextError=컨í…스트 [{0}]ì˜ ì„¤ì •ì„ ì €ìž¥í•˜ëŠ” 중 오류 ë°œìƒ +standardServer.storeConfig.error=서버 ì„¤ì •ì„ ì €ìž¥í•˜ëŠ” 중 오류 ë°œìƒ +standardServer.storeConfig.notAvailable=[{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ MBean으로서 StoreConfig 구현 ê°ì²´ê°€ 등ë¡ë˜ì§€ 않았으므로, ì–´ë–¤ ì„¤ì •ë„ ì €ìž¥ë  ìˆ˜ 없었습니다. 보통 StoreConfigLifecycleListenerì„ í†µí•˜ì—¬ ì ì ˆí•œ MBeanì´ ë“±ë¡ë©ë‹ˆë‹¤. + +standardService.engine.startFailed=ì—°ê´€ëœ ì—”ì§„ì„ ì‹œìž‘í•˜ì§€ 못했습니다. +standardService.engine.stopFailed=ì—°ê´€ëœ ì—”ì§„ì„ ì¤‘ì§€ì‹œí‚¤ì§€ 못했습니다. +standardService.executor.start=새로운 Executor를 시작하는 중 오류 ë°œìƒ +standardService.executor.stop=ì´ì „ Executor를 중지시키는 중 오류 ë°œìƒ +standardService.mapperListener.startFailed=ì—°ê´€ëœ MapperListener를 시작하지 못했습니다. +standardService.mapperListener.stopFailed=ì—°ê´€ëœ MapperListener를 중지시키지 못했습니다. +standardService.start.name=서비스 [{0}]ì„(를) 시작합니다. +standardService.stop.name=서비스 [{0}]ì„(를) 중지시킵니다. + +standardThreadExecutor.notStarted=Executorê°€ ì•„ì§ ì‹œìž‘ë˜ì§€ 않았습니다. + +standardWrapper.allocate=서블릿 ì¸ìŠ¤í„´ìŠ¤ë¥¼ 할당하는 중 오류 ë°œìƒ +standardWrapper.allocateException=서블릿 [{0}]ì„(를) 위해 í• ë‹¹í•˜ë˜ ì¤‘ 예외 ë°œìƒ +standardWrapper.deallocateException=서블릿 [{0}]ì„(를) 위한 할당 í•´ì œ 처리 중 예외 ë°œìƒ +standardWrapper.destroyException=서블릿 [{0}]ì„(를) 위한 Servlet.destroy() 호출 중 ìµì…‰ì…˜ì´ ë°œìƒí–ˆìŠµë‹ˆë‹¤. +standardWrapper.destroyInstance=서블릿 [{0}]ì„(를) 위한 InstanceManager.destroy() í˜¸ì¶œì´ ì˜ˆì™¸ë¥¼ ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardWrapper.initException=서블릿 [{0}]ì„(를) 위한 Servlet.init() í˜¸ì¶œì´ ì˜ˆì™¸ë¥¼ ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardWrapper.instantiate=서블릿 í´ëž˜ìŠ¤ [{0}](으)로부터 ì¸ìŠ¤í„´ìŠ¤ ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ +standardWrapper.isUnavailable=서블릿 [{0}]ì€(는) 현재 가용하지 않습니다. +standardWrapper.jspMonitorError=JSP 모니터 MBean [{0}]ì„(를) 등ë¡í•˜ëŠ” 중 오류 ë°œìƒ +standardWrapper.notChild=Wrapper 컨테ì´ë„ˆëŠ” ìžì‹ 컨테ì´ë„ˆë“¤ì„ 가질 수 없습니다. +standardWrapper.notClass=서블릿 [{0}]ì„(를) 위한 서블릿 í´ëž˜ìŠ¤ê°€ 지정ë˜ì§€ 않았습니다. +standardWrapper.notContext=Wrapperì˜ ë¶€ëª¨ 컨테ì´ë„ˆëŠ” 반드시 컨í…스트여야 합니다. +standardWrapper.notFound=서블릿 [{0}]ì€(는) 가용하지 않습니다. +standardWrapper.notServlet=í´ëž˜ìŠ¤ [{0}]ì€(는) ì„œë¸”ë¦¿ì´ ì•„ë‹™ë‹ˆë‹¤, +standardWrapper.serviceException=경로가 [{1}]ì¸ ì»¨í…ìŠ¤íŠ¸ì˜ ì„œë¸”ë¦¿ [{0}]ì„(를) 위한 Servlet.service() í˜¸ì¶œì´ ì˜ˆì™¸ë¥¼ ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardWrapper.serviceExceptionRoot=경로 [{1}]ì˜ ì»¨í…스트 ë‚´ì˜ ì„œë¸”ë¦¿ [{0}]ì„(를) 위한 Servlet.service() 호출ì´, 근본 ì›ì¸(root cause)ê³¼ 함께, 예외 [{2}]ì„(를) ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardWrapper.unavailable=서블릿 [{0}]ì„(를) 가용하지 ì•Šì€ ìƒíƒœë¡œ 표시합니다. +standardWrapper.unloadException=서블릿 [{0}]ì„(를) 위한 unload() 호출 ì‹œ, 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardWrapper.unloading=ì„œë¸”ë¦¿ì´ ì–¸ë¡œë“œë˜ì—ˆê¸° 때문ì—, 서블릿 [{0}]ì„(를) 할당할 수 없습니다. +standardWrapper.waiting=서블릿 [{1}]ì„(를) 위해, [{0}]ê°œì˜ ì¸ìŠ¤í„´ìŠ¤(들)ì´ í• ë‹¹ í•´ì œë˜ê¸°ë¥¼ 기다립니다. + +threadLocalLeakPreventionListener.containerEvent.error=컨테ì´ë„ˆ ì´ë²¤íŠ¸ [{0}]ì„(를) 처리하는 중 예외 ë°œìƒ +threadLocalLeakPreventionListener.lifecycleEvent.error=Lifecycle ì´ë²¤íŠ¸ [{0}]ì„(를) 처리하는 중 예외 ë°œìƒ diff --git a/java/org/apache/catalina/core/LocalStrings_pt_BR.properties b/java/org/apache/catalina/core/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..0aecd23 --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_pt_BR.properties @@ -0,0 +1,33 @@ +# 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. + +applicationContext.addJspFile.iae=O arquivo JSP [{0}] não é válido +applicationContext.setAttribute.namenull=Nome não pode ser nulo + +applicationDispatcher.specViolation.response=O ServletResponse original ou ServletResponse encapsulado não passou pelo RequestDispatcher em violação ao SRV.8.2 e SRV.14.2.5.1 + +aprListener.initializingFIPS=Iniciando modo FIPS... + +defaultInstanceManager.restrictedListenersResource=Arquivo de propriedades de listeners restritos não encontrado [{0}] + +filterChain.filter=Filtro de execução lançou uma exceção + +naming.wsdlFailed=Falha ao buscar arquivo wsdl [{0}]. + +standardContext.listenerStart=Exceção ao enviar evento de contexto iniciado para instância listener da classe [{0}] +standardContext.loginConfig.errorWarning=AVISO: Página de erro de formulário [{0}] precisa iniciar com "/" no Servlet 2.4 +standardContext.securityConstraint.mixHttpMethod=Não é permitido misturar e na mesma coleção de recursos web + +standardWrapper.unloading=Impossível alocar servlet [{0}] pois está sendo descarregado. diff --git a/java/org/apache/catalina/core/LocalStrings_ru.properties b/java/org/apache/catalina/core/LocalStrings_ru.properties new file mode 100644 index 0000000..3e04a3d --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_ru.properties @@ -0,0 +1,60 @@ +# 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. + +applicationContext.addJspFile.iae=Файл JSP [{0}] Ñодержит ошибки +applicationContext.addListener.iae.cnfe=Ðевозможно Ñоздать ÑкземплÑÑ€ типа [{0}] +applicationContext.addListener.iae.wrongType=Указанный тип[{0}] не ÑвлÑетÑÑ Ð¾Ð´Ð½Ð¸Ð¼ из ожидаемых типов Ñлушателей +applicationContext.addRole.ise=Роли не могут быть добавлены Ð´Ð»Ñ ÐºÐ¾Ð½Ñ‚ÐµÐºÑта [{0}] так как контекÑÑ‚ проинициализирован +applicationContext.invalidServletName=Ðевозможно добавить определение Ñервлета из-за некорректного имени Ñервлета [{0}] +applicationContext.setAttribute.namenull=Ð˜Ð¼Ñ Ð½Ðµ может быть пуÑтым + +applicationDispatcher.specViolation.response=Оригинальный ServletResponse или обернутый оригинальный ServletResponse не передан в Request Dispatcher в нарушение SRV.8.2 и SRV.14.2.5.1 + +applicationHttpRequest.sessionEndAccessFail=ИÑключение вызвало прекращение доÑтупа к ÑеÑÑии при очиÑтке запроÑа + +aprListener.initializingFIPS=ИнициализируетÑÑ Ñ€ÐµÐ¶Ð¸Ð¼ FIPS... + +filterChain.filter=При выполнении фильтра выброшено иÑключение + +naming.unbindFailed=Ошибка при отвÑзывании объекта: [{0}] +naming.wsdlFailed=wsdl файл не найден: [{0}] + +standardContext.filterStart=Ошибка при Ñтарте фильтра [{0}] +standardContext.invalidWrapperClass=[{0}] не ÑвлÑетÑÑ Ð¿Ð¾Ð´ÐºÐ»Ð°ÑÑом StandardWrapper +standardContext.parameter.duplicate=Дублированный параметр инициализации контекÑта [{0}] +standardContext.predestroy.duplicate=Дублированное определение метода @PreDestroy Ð´Ð»Ñ ÐºÐ»Ð°ÑÑа [{0}] +standardContext.securityConstraint.mixHttpMethod=Запрещено Ñмешивать и в одной и той же коллекции веб-реÑурÑов +standardContext.startingContext=Ошибка запуÑка контекÑта Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ [{0}] + +standardWrapper.allocate=Ошибка при выделении ÑкземплÑра Ñервлета +standardWrapper.allocateException=Ошибка Ð²Ñ‹Ð´ÐµÐ»ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñервлета [{0}] +standardWrapper.deallocateException=Ошибка оÑÐ²Ð¾Ð±Ð¾Ð¶Ð´ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñервлета [{0}] +standardWrapper.destroyException=Servlet.destroy() Ð´Ð»Ñ Ñервлета [{0}] выброÑил иÑключение +standardWrapper.destroyInstance=InstanceManager.destroy() Ð´Ð»Ñ Ñервлета [{0}] выброÑил иÑключение +standardWrapper.initException=Servlet.init() Ð´Ð»Ñ Ñервлета [{0}] выброÑил иÑключение +standardWrapper.instantiate=Ошибка ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÑкземплÑра клаÑÑа Ñервлета [{0}] +standardWrapper.isUnavailable=Сервлет [{0}] временно недоÑтупен +standardWrapper.jspMonitorError=Ошибка региÑтрации JSP-монитора Mbean [{0}] +standardWrapper.notChild=Контейнер может не иметь дочерних контейнеров +standardWrapper.notClass=Ð”Ð»Ñ Ñервлета [{0}] не указан Ñпециальный клаÑÑ +standardWrapper.notContext=РодительÑкий контейнер обертки должен быть контекÑтом +standardWrapper.notFound=Сервлет [{0}] недоÑтупен +standardWrapper.notServlet=КлаÑÑ [{0}] не ÑвлÑетÑÑ Ñервлетом +standardWrapper.serviceException=Servlet.service() Ð´Ð»Ñ Ñервлета [{0}] в контекÑте Ñ Ð¿ÑƒÑ‚ÐµÐ¼ [{1}] выброÑил иÑключение +standardWrapper.serviceExceptionRoot=Servlet.service() Ð´Ð»Ñ Ñервлета [{0}] в контекÑте Ñ Ð¿ÑƒÑ‚ÐµÐ¼ [{1}] выброÑил иÑключение [{2}] Ñ Ð¿ÐµÑ€Ð²Ð¾Ð¿Ñ€Ð¸Ñ‡Ð¸Ð½Ð¾Ð¹ +standardWrapper.unavailable=Выделенный Ñервлет [{0}] недоÑтупен +standardWrapper.unloadException=Сервлет [{0}] выброÑил иÑключение unload() +standardWrapper.unloading=Ðевозможно выделить Ñервлет [{0}] потому что он еще загружаетÑÑ +standardWrapper.waiting=Ожидание Ð´Ð»Ñ [{0}] ÑкземплÑра(ов) которые нужно оÑвободить Ð´Ð»Ñ Ñервлета [{1}] diff --git a/java/org/apache/catalina/core/LocalStrings_zh_CN.properties b/java/org/apache/catalina/core/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..a927557 --- /dev/null +++ b/java/org/apache/catalina/core/LocalStrings_zh_CN.properties @@ -0,0 +1,313 @@ +# 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. + +applicationContext.addFilter.ise=无法将筛选器添加到上下文[{0}],因为该上下文已åˆå§‹åŒ– +applicationContext.addJspFile.iae=JSP 文件 [{0}] ä¸å¯ç”¨ +applicationContext.addListener.iae.cnfe=无法创建类型为 [{0}] 的实例 +applicationContext.addListener.iae.init=无法将类型为[{0}]的实例添加为侦å¬å™¨ +applicationContext.addListener.iae.sclNotAllowed=一旦调用了第一个ServletContextListener,就ä¸èƒ½å†æ·»åŠ ServletContextListener。 +applicationContext.addListener.iae.wrongType=指定的类型[{0}]ä¸æ˜¯é¢„期的侦å¬å™¨ç±»åž‹ä¹‹ä¸€ +applicationContext.addListener.ise=无法将侦å¬å™¨æ·»åŠ åˆ°ä¸Šä¸‹æ–‡[{0}]中,因为该上下文已åˆå§‹åŒ– +applicationContext.addRole.ise=上下文被åˆå§‹åŒ–åŽï¼Œè§’色ä¸èƒ½å†è¢«æ·»åŠ åˆ°[{0}]中 +applicationContext.addServlet.ise=无法将servlet添加到上下文[{0}]中,因为该上下文已åˆå§‹åŒ– +applicationContext.attributeEvent=属性事件监å¬å™¨å¼•å‘的异常 +applicationContext.illegalDispatchPath=应用程åºè¯•å›¾èŽ·å–具有éžæ³•è·¯å¾„[{0}]的请求分派器,但该路径被拒ç»ï¼Œå› ä¸ºå®ƒåŒ…å«ä¸€ä¸ªç¼–ç çš„目录é历å°è¯• +applicationContext.invalidFilterName=由于筛选器å称[{0}]无效,无法添加筛选器定义。 +applicationContext.invalidServletName=由于servletå称[{0}]无效,无法添加对应servlet的定义。 +applicationContext.lookup.error=在上下文[{1}]中找ä¸åˆ°èµ„æº[{0}] +applicationContext.mapping.error=映射中.的错误 +applicationContext.requestDispatcher.iae=路径[{0}]ä¸ä»¥â€œ/â€å­—符开头 +applicationContext.resourcePaths.iae=路径[{0}]ä¸ä»¥â€œ/â€å­—符开头 +applicationContext.role.iae=è¦ä¸ºä¸Šä¸‹æ–‡[{0}]声明的å•ä¸ªè§’色ä¸èƒ½ä¸ºç©ºï¼Œä¹Ÿä¸èƒ½ä¸ºç©ºå­—符串 +applicationContext.roles.iae=è¦ä¸ºä¸Šä¸‹æ–‡[{0}]声明的角色数组ä¸èƒ½ä¸ºç©º +applicationContext.setAttribute.namenull=Name ä¸èƒ½ä¸º null +applicationContext.setInitParam.ise=åˆå§‹åŒ–上下文åŽæ— æ³•è®¾ç½®åˆå§‹åŒ–å‚æ•° +applicationContext.setRequestEncoding.ise=无法为上下文[{0}]设置请求编ç ï¼Œå› ä¸ºè¯¥ä¸Šä¸‹æ–‡å·²åˆå§‹åŒ– +applicationContext.setResponseEncoding.ise=无法为上下文[{0}]设置å“应编ç ï¼Œå› ä¸ºè¯¥ä¸Šä¸‹æ–‡å·²åˆå§‹åŒ– +applicationContext.setSessionTimeout.ise=(:无法为上下文[{0}]设置会è¯è¶…时,因为该上下文已åˆå§‹åŒ– +applicationContext.setSessionTracking.iae.invalid=该上下文ä¸æ”¯æŒä¸ºä¸Šä¸‹æ–‡[{1}]请求的会è¯è·Ÿè¸ªæ¨¡å¼[{0}] +applicationContext.setSessionTracking.iae.ssl=为上下文 [{0}] 请求的 session 跟踪模å¼åŒ…括 SSL 和至少一ç§å…¶ä»–模å¼ã€‚ SSLå¯èƒ½æœªé…置其他模å¼ã€‚ +applicationContext.setSessionTracking.ise=当上下文正在è¿è¡Œï¼Œæ— æ³•è®¾ç½®ä¸Šä¸‹æ–‡[{0}]的会è¯è·Ÿè¸ªæ¨¡å¼ + +applicationDispatcher.allocateException=为servlet[{0}]分é…异常 +applicationDispatcher.deallocateException=servlet[{0}]的解除分é…异常 +applicationDispatcher.forward.ise=æ交å“应åŽæ— æ³•è½¬å‘ +applicationDispatcher.isUnavailable=Servlet[{0}]当å‰ä¸å¯ç”¨ +applicationDispatcher.serviceException=Servlet[{0}]çš„Servlet.service()抛出异常 +applicationDispatcher.specViolation.request=原始ServletRequest或包装的原始ServletRequest未传递给RequestDispatcher,这è¿å了SRV.8.2å’ŒSRV.14.2.5.1 +applicationDispatcher.specViolation.response=原始的ServletResponse或包装åŽçš„ServletResponse未传递给RequestDispatcher,原因:è¿å了SRV.8.2å’ŒSRV.14.2.5.1\n\ +\n + +applicationFilterConfig.jmxRegisterFail=类型为[{0}]且å称为[{1}]的筛选器的JMX注册失败 +applicationFilterConfig.jmxUnregister=已完æˆå¯¹ç±»åž‹ä¸º[{0}]且å称为[{1}]的筛选器的JMXå–消注册。 +applicationFilterConfig.jmxUnregisterFail=类型为[{0}]且å称为[{1}]的筛选器的JMXå–消注册失败 +applicationFilterConfig.preDestroy=):为类型为[{1}]çš„å为[{0}]的筛选器调用preDestroy 失败 +applicationFilterConfig.release=失败的销æ¯è¿‡æ»¤å™¨ç±»åž‹ä¸º[{1}]å称为[{0}] + +applicationFilterRegistration.nullInitParam=由于å称和/或值为空,无法为筛选器设置åˆå§‹åŒ–å‚数。å称[{0}],值[{1}] +applicationFilterRegistration.nullInitParams=由于nameå’Œ(或)value为null,无法为过滤器设置åˆå§‹åŒ–å‚数。name为 [{0}],value为 [{1}] + +applicationHttpRequest.fragmentInDispatchPath=调度路径[{0}]中的片段已被删除 +applicationHttpRequest.sessionEndAccessFail=在回收请求时,异常触å‘了对会è¯çš„结æŸè®¿é—®ã€‚ + +applicationPushBuilder.methodInvalid=推é€è¯·æ±‚çš„HTTP方法必须既å¯ç¼“å­˜åˆå®‰å…¨ï¼Œä½†æ˜¯[{0}]ä¸æ˜¯ +applicationPushBuilder.methodNotToken=HTTP方法必须是令牌(token),但 [{0}] 包å«éžä»¤ç‰Œå­—符 + +applicationServletRegistration.setServletSecurity.iae=为部署到å为[{1}]的上下文的Servlet[{0}]æŒ‡å®šçš„ç©ºçº¦æŸ +applicationServletRegistration.setServletSecurity.ise=无法将安全性约æŸæ·»åŠ åˆ°å·²éƒ¨ç½²åˆ°å称为[{1}]的上下文的servlet [{0}]中,因为上下文已被åˆå§‹åŒ– + +applicationSessionCookieConfig.ise=无法将属性[{0}]添加到上下文[{1}]çš„sessioncokieconfig中,因为该上下文已åˆå§‹åŒ– + +aprListener.FIPSProviderNotDefault=当 AprLifecycleListener é…置为 FIPS æ¨¡å¼ [{0}] 时,必须将 FIPS æ供程åºé…置为默认æä¾›ç¨‹åº +aprListener.aprDestroy=无法关闭基于APRçš„Apache Tomcat本机库 +aprListener.aprInit=在java.library.path:[{0}]上找ä¸åˆ°åŸºäºŽAPRçš„Apache Tomcat本机库,该库å…许在生产环境中获得最佳性能 +aprListener.aprInitDebug=在java.library.path[{1}]上使用å称[{0}]找ä¸åˆ°åŸºäºŽAPRçš„ApacheTomcat本机库。报告的错误是[{2}] +aprListener.aprInitError=基于APR的本地库加载失败.错误报告为[{0}] +aprListener.currentFIPSMode=当å‰FIPS模å¼ï¼š[{0}]。 +aprListener.enterAlreadyInFIPSMode=AprLifecycleListener é…置为强制进入FIPS模å¼ï¼Œä½†åº“已处于FIPS模å¼[{0}] +aprListener.initializeFIPSFailed=进入FIPS模å¼å¤±è´¥ +aprListener.initializeFIPSSuccess=æˆåŠŸçš„进入FIPS æ¨¡å¼ +aprListener.initializedOpenSSL=OpenSSLæˆåŠŸåˆå§‹åŒ– [{0}] +aprListener.initializingFIPS=åˆå§‹åŒ–FIPS模å¼... +aprListener.requireNotInFIPSMode=AprLifecycleListeneré…置为è¦æ±‚库已处于FIPS模å¼ï¼Œä½†å®ƒæœªå¤„于FIPSæ¨¡å¼ +aprListener.skipFIPSInitialization=å·²ç»å¤„于FIPS模å¼ï¼Œè·³è¿‡FIPSåˆå§‹åŒ– +aprListener.sslInit=无法åˆå§‹åŒ–SSLEngine +aprListener.sslRequired=当使用版本 [{1}] çš„ Tomcat 本机库时,[{0}] ä¸æ˜¯ SSLEngine 的有效值,因为 2.x åŠæ›´é«˜ç‰ˆæœ¬éœ€è¦ SSL。 +aprListener.tcnInvalid=安装了ä¸å…¼å®¹çš„APR(基于Apache Tomcat原生库)版本[{0}],而Tomcatè¦æ±‚版本[{1}] +aprListener.tcnValid=使用APR版本[{1}]加载了基于APRçš„Apache Tomcat本机库[{0}]。 +aprListener.tcnVersion=安装了基于APRçš„Apache Tomcat Native库的旧版本[{0}],而Tomcat建议使用[{1}]的最低版本 +aprListener.tooLateForFIPSMode=无法设置FIPSMode:SSLå·²åˆå§‹åŒ– +aprListener.tooLateForSSLEngine=无法设置引擎:SSLå·²åˆå§‹åŒ– +aprListener.tooLateForSSLRandomSeed=无法设置 SSLRandomSeed:SSLå·²ç»åˆå§‹åŒ– +aprListener.usingFIPSProvider=å°† OpenSSL 与 FIPS æ供程åºä¸€èµ·ç”¨ä½œé»˜è®¤æä¾›ç¨‹åº +aprListener.wrongFIPSMode=AprLifecycleListenerçš„FIPSMode选项的æ„外值:[{0}] + +asyncContextImpl.asyncDispatchError=异步调度时出错 +asyncContextImpl.asyncRunnableError=通过AsyncContext.start()处ç†å¼‚æ­¥è¿è¡Œæ—¶å‡ºé”™ +asyncContextImpl.dispatchingStarted=异步调度æ“作已ç»è¢«è°ƒç”¨ã€‚ä¸å…许在åŒä¸€å¼‚步周期内进行其他异步调度æ“作。 +asyncContextImpl.fireOnComplete=为任何异步侦å¬å™¨è§¦å‘onComplete()事件 +asyncContextImpl.fireOnError=为任何异步侦å¬å™¨è§¦å‘onError()事件 +asyncContextImpl.fireOnStartAsync=为任何异步侦å¬å™¨å¯åŠ¨onStartAsync()事件 +asyncContextImpl.fireOnTimeout=为任何异步侦å¬å™¨è§¦å‘onTimeout()事件 +asyncContextImpl.noAsyncDispatcher=从ServletContext 返回的调度程åºä¸æ”¯æŒå¼‚步调度 +asyncContextImpl.onCompleteError=对类型为[{0}]的侦å¬å™¨çš„onComplete()调用失败 +asyncContextImpl.onErrorError=对类型为[{0}]的侦å¬å™¨çš„onError()调用失败 +asyncContextImpl.onStartAsyncError=对类型为[{0}]的侦å¬å™¨çš„onStartAsync()调用失败 +asyncContextImpl.onTimeoutError=对类型为[{0}]的侦å¬å™¨çš„onTimeout()调用失败 +asyncContextImpl.request.ise=调用方法complete()åŽæˆ–者任æ„一个dispatch()方法调用åŽï¼Œè°ƒç”¨getRequest()方法是éžæ³•çš„ +asyncContextImpl.requestEnded=AsyncContextå…³è”的请求已ç»å®Œæˆå¤„ç†ã€‚ +asyncContextImpl.response.ise=调用complete()或者任何的dispatch()方法åŽï¼Œè°ƒç”¨getResponse()方法是éžæ³•çš„。 + +containerBase.backgroundProcess.cluster=异常处ç†é›†ç¾¤[{0}]åŽå°è¿›ç¨‹ +containerBase.backgroundProcess.error=处ç†åŽå°çº¿ç¨‹å¼‚常 +containerBase.backgroundProcess.realm=异常处ç†é¢†åŸŸ[{0}]åŽå°è¿›ç¨‹ +containerBase.backgroundProcess.valve=处ç†é˜€é—¨[{0}]åŽå°è¿›ç¨‹å¼‚常 +containerBase.child.destroy=销æ¯å­çº§æ—¶å‡ºé”™ +containerBase.child.notUnique=å­å称[{0}]ä¸å”¯ä¸€ +containerBase.child.start=å¯åŠ¨å­çº§æ—¶å‡ºé”™ +containerBase.child.stop=åœæ­¢å­çº§æ—¶å‡ºé”™ +containerBase.cluster.start=å¯åŠ¨æ–°é›†ç¾¤æ—¶å‡ºé”™ +containerBase.cluster.stop=åœæ­¢æ—§é›†ç¾¤æ—¶å‡ºé”™ +containerBase.nullName=容器å称ä¸èƒ½ä¸ºnull +containerBase.realm.start=å¯åŠ¨æ–°é¢†åŸŸæ—¶å‡ºé”™ +containerBase.realm.stop=åœæ­¢æ—§åŸŸæ—¶å‡ºé”™ +containerBase.threadedStartFailed=å­å®¹å™¨å¯åŠ¨å¤±è´¥ +containerBase.threadedStopFailed=åœæ­¢æœŸé—´å­å®¹å™¨å¤±è´¥ + +defaultInstanceManager.invalidAnnotation=无效注解[{0}] +defaultInstanceManager.invalidInjection=方法资æºæ³¨å…¥æ³¨è§£æ— æ•ˆ +defaultInstanceManager.postConstructNotFound=ç±»[{1}]çš„åŽæž„造方法[{0}]已在部署æ述符中声明,但找ä¸åˆ° +defaultInstanceManager.preDestroyNotFound=ç±»[{1}]的预销æ¯æ–¹æ³•[{0}]已在部署æ述符中声明,但找ä¸åˆ° +defaultInstanceManager.restrictedClass=ç¦æ­¢è®¿é—®ç±»[{0}]。这是一个é™åˆ¶ç±»ã€‚必须将web应用程åºé…置为具有特æƒæ‰èƒ½åŠ è½½å®ƒ +defaultInstanceManager.restrictedContainerServlet=ç¦æ­¢è®¿é—®ç±» [{0}]。 它是一个å—é™åˆ¶çš„类(实现了 ContainerServlet 接å£ï¼‰ã€‚ 必须将 Web 应用程åºé…置为特æƒæ‰èƒ½åŠ è½½å®ƒ +defaultInstanceManager.restrictedFiltersResource=找ä¸åˆ°å—é™åˆ¶çš„过滤器属性文件[{0}] +defaultInstanceManager.restrictedListenersResource=无法找到RestrictedListenerçš„é…置文件[{0}] +defaultInstanceManager.restrictedServletsResource=找ä¸åˆ°å—é™åˆ¶çš„servlets属性文件[{0}] +defaultInstanceManager.restrictedWrongValue=ç±»å为[{1}]çš„å—é™ç±»å±žæ€§æ–‡ä»¶[{0}]中的值错误。期望值:[restricted],实际值:[{2}] + +filterChain.filter=Filter 执行抛出一个异常 +filterChain.servlet=Servlet执行抛出一个异常 + +jniLifecycleListener.bothPathAndName=å¯ä»¥è®¾ç½®libraryName或libraryPath,但ä¸èƒ½åŒæ—¶è®¾ç½®ä¸¤è€… +jniLifecycleListener.load.name=已加载本机库[{0}] +jniLifecycleListener.load.path=已从[{0}]加载本机库 +jniLifecycleListener.missingPathOrName=必须设置libraryName或libraryPath之一 + +jreLeakListener.classToInitializeFail=在tomcatå¯åŠ¨æœŸé—´æœªèƒ½åŠ è½½ç±»[{0}],以防止å¯èƒ½çš„内存泄æ¼ã€‚ + +listener.notServer=监å¬å™¨å¿…须嵌套在Server元素内,但是它在[{0}] + +naming.addEnvEntry=添加环境æ¡ç›® [{0}] +naming.addResourceEnvRef=添加资æºçŽ¯å¢ƒå¼•ç”¨ [{0}] +naming.bindFailed=无法绑定对象:[{0}] +naming.invalidEnvEntryType=环境æ¡ç›®[{0}]没有一个有效哦的类型 +naming.invalidEnvEntryValue=环境项[{0}]的值无效。 +naming.jmxRegistrationFailed=注册到JMX失败:[{0}] +naming.namingContextCreationFailed=创建上下文å称失败 +naming.unbindFailed=解绑对象[{0}]失败 +naming.wsdlFailed=未找到 wsdl 文件:[{0}] + +noPluggabilityServletContext.notAllowed=Servlet 3.0规范的第4.4节ä¸å…许从未在web.xml,web-fragment.xml文件中定义或未用@WebListener注释的ServletContextListener调用此方法。 + +propertiesRoleMappingListener.roleMappingFileEmpty=角色映射文件ä¸èƒ½ä¸ºç©º +propertiesRoleMappingListener.roleMappingFileFail=加载角色映射文件[{0}]失败 +propertiesRoleMappingListener.roleMappingFileNull=角色映射文件ä¸èƒ½ä¸ºNULL + +pushBuilder.noPath=在设置路径之å‰è°ƒç”¨push()是éžæ³•çš„ + +standardContext.applicationListener=é…置应用程åºç›‘å¬å™¨[{0}]错误 +standardContext.applicationSkipped=由于以å‰çš„错误,已跳过安装应用程åºä¾¦å¬å™¨ +standardContext.backgroundProcess.instanceManager=异常处ç†å®žä¾‹ç®¡ç†å™¨[{0}]åŽå°è¿›ç¨‹ +standardContext.backgroundProcess.loader=异常处ç†åŠ è½½ç¨‹åº[{0}]åŽå°è¿›ç¨‹ +standardContext.backgroundProcess.manager=异常处ç†ç®¡ç†å™¨[{0}]åŽå°è¿›ç¨‹ã€‚ +standardContext.backgroundProcess.resources=异常处ç†èµ„æº[{0}] åŽå°è¿›ç¨‹ +standardContext.cluster.managerError=创建新集群会è¯ç®¡ç†å™¨æ—¶å‡ºé”™ +standardContext.cluster.noManager=未å‘现管ç†å™¨ã€‚检查是å¦éœ€è¦é›†ç¾¤ç®¡ç†å™¨ã€‚集群é…置:[{0}],应用程åºåˆ†é…:[{1}] +standardContext.configurationFail=一个或多个组件将上下文标记为未正确é…ç½® +standardContext.cookieProcessor.null=ä¸å…许将上下文的CookieProcessor 设置为null +standardContext.createWrapper.containerListenerError=为包装创建容器侦å¬å™¨æ—¶å‡ºé”™ +standardContext.createWrapper.error=创建新包装时出错 +standardContext.createWrapper.listenerError=为包装创建生命周期侦å¬å™¨æ—¶å‡ºé”™ +standardContext.duplicateListener=当å‰ä¸Šä¸‹æ–‡å·²ç»é…置了监å¬å™¨[{0}],é‡å¤çš„定义将被忽略。 +standardContext.errorPage.error=错误页é¢ä½ç½®[{0}]必须以“ /â€å¼€å¤´ +standardContext.errorPage.required=ErrorPageä¸èƒ½ä¸ºnull +standardContext.errorPage.warning=警告:在Servlet 2.4中,错误页ä½ç½® [{0}] 必须以"/"开头 +standardContext.filterFail=一个或多个筛选器å¯åŠ¨å¤±è´¥ã€‚完整的详细信æ¯å°†åœ¨ç›¸åº”的容器日志文件中找到 +standardContext.filterMap.either=过滤器映射必须指定 或 +standardContext.filterMap.name=Filter mapping 指定了一个未知的 filterå称 [{0}] +standardContext.filterMap.pattern=过滤器映射中的 [{0}] 无效 +standardContext.filterStart=å¯åŠ¨è¿‡æ»¤å™¨å¼‚常[{0}] +standardContext.invalidWrapperClass=[{0}] ä¸æ˜¯StandardWrapperçš„å­ç±» +standardContext.isUnavailable=此应用程åºç›®å‰ä¸å¯ç”¨ +standardContext.listenerFail=一个或多个listenerså¯åŠ¨å¤±è´¥ï¼Œæ›´å¤šè¯¦ç»†ä¿¡æ¯æŸ¥çœ‹å¯¹åº”的容器日志文件 +standardContext.listenerStart=异常将上下文åˆå§‹åŒ–事件å‘é€åˆ°ç±»çš„侦å¬å™¨å®žä¾‹.[{0}] +standardContext.listenerStop=例外情况å‘é€ä¸Šä¸‹æ–‡åˆ é™¤äº‹ä»¶[{0}],以便列表实例 +standardContext.loadOnStartup.loadException=web应用程åº[{0}]中的Servlet[{1}]引å‘了load()异常 +standardContext.loginConfig.errorPage=表å•é”™è¯¯é¡µ[{0}]必须以"/"开始 +standardContext.loginConfig.errorWarning=警告:Servlet 2.4中,表å•é”™è¯¯é¡µ[{0}]必须以"/"开始 +standardContext.loginConfig.loginPage=表å•ç™»å½•é¡µé¢ [{0}] 必须以''/''开头 +standardContext.loginConfig.loginWarning=警告:在Servlet 2.4 中 Form 登录页[{0}] 必须以 "/" 开头 +standardContext.loginConfig.required=LoginConfigä¸èƒ½ä¸ºç©º +standardContext.manager=é…置类为[{0}]的管ç†å™¨ +standardContext.managerFail=会è¯ç®¡ç†å™¨æ— æ³•å¯åŠ¨ +standardContext.namingResource.destroy.fail=无法销æ¯æ—§çš„命åèµ„æº +standardContext.namingResource.init.fail=未能åˆå§‹åŒ–新的命åèµ„æº +standardContext.notStarted=å称为[{0}]的上下文还没有å¯åŠ¨ +standardContext.notWrapper=上下文的å­çº§å¿…须是包装器 +standardContext.parameter.duplicate=é‡å¤çš„上下文åˆå§‹åŒ–å‚æ•°[{0}] +standardContext.parameter.required=å‚æ•°åå’Œå‚数值都是必需的 +standardContext.pathInvalid=上下文路径必须是空字符串或以''/''开头,而ä¸èƒ½ä»¥''/''结尾。路径[{0}]ä¸ç¬¦åˆè¿™äº›æ¡ä»¶ï¼Œå·²æ›´æ”¹ä¸º[{1}] +standardContext.postconstruct.duplicate=ç±»[{0}]的构造åŽæ–¹æ³•å®šä¹‰é‡å¤ +standardContext.postconstruct.required=完全é™å®šçš„ç±»å和方法å都是必需的 +standardContext.predestroy.duplicate=ç±» [{0}] çš„ @PreDestroy 方法定义é‡å¤ +standardContext.predestroy.required=完全åˆæ ¼çš„ç±»å和方法å都是必需的 +standardContext.reloadingCompleted=已完æˆé‡æ–°åŠ è½½å为[{0}]的上下文 +standardContext.reloadingStarted=已开始é‡æ–°åŠ è½½å为[{0}]的上下文 +standardContext.requestListener.requestInit=å‘ç±»[{0}]的侦å¬å™¨å®žä¾‹å‘é€è¯·æ±‚åˆå§‹åŒ–的生命周期事件的异常 +standardContext.resourcesInit=åˆå§‹åŒ–é™æ€å˜é‡é”™è¯¯ +standardContext.resourcesStart=å¯åŠ¨é™æ€èµ„æºå‡ºé”™ +standardContext.resourcesStop=åœæ­¢é™æ€èµ„æºæ—¶å‡ºé”™ +standardContext.sciFail=ServletContainerInitializer处ç†æœŸé—´å‡ºé”™ +standardContext.securityConstraint.mixHttpMethod=在相åŒçš„web资æºé›†åˆä¸­ä¸å…许混用: å’Œ +standardContext.securityConstraint.pattern=安全约æŸä¸­çš„ [{0}] 无效 +standardContext.servletFail=å¯åŠ¨æ—¶æ— æ³•åŠ è½½ä¸€ä¸ªæˆ–多个Servlet。 全部的详细信æ¯å¯åœ¨ç›¸åº”的容器日志文件中找到 +standardContext.servletMap.name=Servlet映射指定未知的Servletå称[{0}] +standardContext.servletMap.pattern=servlet映射中的[{0}]无效 +standardContext.setLoader.start=å¯åŠ¨æ–°åŠ è½½ç¨‹åºæ—¶å‡ºé”™ +standardContext.setLoader.stop=åœæ­¢æ—§åŠ è½½ç¨‹åºæ—¶å‡ºé”™ +standardContext.setManager.start=å¯åŠ¨æ–°ç®¡ç†å™¨æ—¶å‡ºé”™ +standardContext.setManager.stop=åœæ­¢æ—§ç®¡ç†å™¨æ—¶å‡ºé”™ +standardContext.startFailed=由于之å‰çš„错误,Context[{0}]å¯åŠ¨å¤±è´¥ +standardContext.startingContext=å¯åŠ¨Context[{0}]出现异常 +standardContext.stop.asyncWaitInterrupted=等待å¸è½½å»¶è¿Ÿæ¯«ç§’以完æˆé£žè¡Œä¸­çš„异步请求时收到中断。上下文åœæ­¢å°†ç»§ç»­ï¼Œä¸ä¼šæœ‰è¿›ä¸€æ­¥çš„延迟。 +standardContext.stoppingContext=异常åœæ­¢çš„上下文使用å为[{0}] +standardContext.suspiciousUrl=å¯ç–‘çš„URL模å¼ï¼š[{0}]在上下文[{1}]中,请å‚阅Servlet规范的第12.1节和第12.2节 +standardContext.threadBindingListenerError=上下文[{0}]é…置的线程绑定监å¬å™¨å‘生错误 +standardContext.urlPattern.patternWarning=警告:在Servlet 2.4中,URL模å¼[{0}]必须以“/â€å¼€å¤´ +standardContext.workCreateException=无法从目录[{0}]å’Œcatalina_home[{1}]中为上下文[{2}]确定ç»å¯¹å·¥ä½œç›®å½• +standardContext.workCreateFail=无法为上下文[{1}]创建工作目录[{0}] +standardContext.workPath=获å–上下文[{0}]的工作路径时å‘生异常 + +standardContextValve.acknowledgeException=以100(继续)å“应确认请求失败 + +standardEngine.notHost=Engineçš„å­èŠ‚点必须是一个Host +standardEngine.notParent=引擎ä¸èƒ½æœ‰çˆ¶å®¹å™¨ +standardEngine.start=正在å¯åŠ¨ Servlet 引擎:[{0}] + +standardHost.clientAbort=远程客户端中止请求,IOException:[{0}]。 +standardHost.invalidErrorReportValveClass=无法加载指定的错误报告阀类:[{0}] +standardHost.noContext=没有é…置上下文æ¥å¤„ç†æ­¤è¯·æ±‚ +standardHost.notContext=主机的å­èŠ‚点必须有上下文 +standardHost.nullName=主机å是必需的 +standardHost.problematicAppBase=在主机[{0}]上为appBase使用空字符串会将其设置为CATALINA_BASE,这是一个åä¸»æ„ +standardHost.problematicLegacyAppBase=在主机[{0}]上为legacyAppBase使用空字符串会将其设置为CATALINA_BASE,这是个åä¸»æ„ + +standardHostValve.customStatusFailed=无法正确调度自定义错误页[{0}] + +standardPipeline.basic.start=å¯åŠ¨æ–°åŸºæœ¬é˜€æ—¶å‡ºé”™ +standardPipeline.basic.stop=åœæ­¢æ—§åŸºæœ¬é˜€æ—¶å‡ºé”™ +standardPipeline.valve.destroy=ç ´å阀门错误 +standardPipeline.valve.start=错误å¯åŠ¨é˜€ +standardPipeline.valve.stop=错误截止阀 + +standardServer.accept.error=å°è¯•åœ¨ä¾¦å¬shutdown命令的套接字上接å—IO异常 +standardServer.accept.readError=å°è¯•è¯»å–关机命令时å‘生IO异常 +standardServer.accept.security=试图在侦å¬shutdown命令的套接字上接å—æ—¶å‘生安全错误 +standardServer.accept.timeout=在调用accept()方法之åŽï¼Œä¾¦å¬shutdown命令的套接字ç»åŽ†äº†æ„外的超时[{0}]毫秒。 这是bug 56684的一个例å­ï¼Ÿ +standardServer.awaitSocket.fail=无法在地å€[{0}]和端å£[{1}]上创建æœåŠ¡å™¨å…³é—­å¥—接字(基本端å£[{2}]å’Œå移é‡[{3}]) +standardServer.invalidShutdownCommand=收到无效的关闭命令[{0}] +standardServer.periodicEventError=å‘é€å‘¨æœŸæ€§äº‹ä»¶æ—¶å‡ºé”™ +standardServer.portOffset.invalid=portOffset [{0}] 值是无效的,因为portOffsetä¸èƒ½æ˜¯è´Ÿæ•° +standardServer.shutdownViaPort=通过关闭端å£æŽ¥æ”¶åˆ°æœ‰æ•ˆçš„关闭命令。正在åœæ­¢æœåŠ¡å™¨å®žä¾‹ã€‚ +standardServer.storeConfig.contextError=存储上下文[{0}]é…置时出错 +standardServer.storeConfig.error=存储æœåŠ¡å™¨é…置时出错 +standardServer.storeConfig.notAvailable=没有将StoreConfig实现注册为å为[{0}]çš„MBean,因此无法ä¿å­˜é…置。åˆé€‚çš„MBean通常通过StoreConfigLifecycleListener注册。 + +standardService.engine.startFailed=å¯åŠ¨å…³è”çš„Engine失败 +standardService.engine.stopFailed=失败åœæ­¢å…³è”的引擎 +standardService.executor.start=å¯åŠ¨æ–°æ‰§è¡Œå™¨æ—¶å‡ºé”™ +standardService.executor.stop=åœæ­¢æ—§æ‰§è¡Œå™¨æ—¶å‡ºé”™ +standardService.mapperListener.startFailed=无法å¯åŠ¨å…³è”çš„MapperListener +standardService.mapperListener.stopFailed=无法åœæ­¢å…³è”çš„MapperListener +standardService.start.name=正在å¯åŠ¨æœåŠ¡[{0}] +standardService.stop.name=正在åœæ­¢æœåŠ¡[{0}] + +standardThreadExecutor.notStarted=执行器尚未å¯åŠ¨ + +standardVirtualThreadExecutor.notStarted=执行器未å¯åŠ¨ + +standardWrapper.allocate=分é…一个servlet实例错误 +standardWrapper.allocateException=分é…异常的servlet [{0}] +standardWrapper.deallocateException=servlet[{0}]的解除分é…异常 +standardWrapper.destroyException=Servlet[{0}]çš„Servlet.destroy()引å‘异常 +standardWrapper.destroyInstance=servlet[{0}]实例管ç†é”€æ¯(destroy) 抛出异常 +standardWrapper.initException=Servlet[{0}]çš„Servlet.init()引å‘异常 +standardWrapper.instantiate=实例化Servletç±»[{0}]异常 +standardWrapper.isUnavailable=Servlet [{0}]当å‰ä¸å¯ç”¨ã€‚ +standardWrapper.jspMonitorError=注册JSP监视器Mbean[{0}]时出错 +standardWrapper.notChild=Wrapper容器内部ä¸å…许有å­å®¹å™¨ã€‚ +standardWrapper.notClass=未为servlet[{0}]指定servletç±» +standardWrapper.notContext=包装的父容器必须是上下文 +standardWrapper.notFound=Servlet [{0}] ä¸å¯ç”¨ +standardWrapper.notServlet=ç±»[{0}]ä¸æ˜¯Servlet +standardWrapper.serviceException=在路径为[{1}]的上下文中,servlet[{0}]çš„Servlet.service()引å‘异常 +standardWrapper.serviceExceptionRoot=在路径为[{1}]的上下文中,Servlet[{0}]çš„Servlet.service()引å‘了具有根本原因的异常[{2}] +standardWrapper.unavailable=å°†servlet[{0}]标记为ä¸å¯ç”¨ +standardWrapper.unloadException=Servlet[{0}]引å‘unload()异常 +standardWrapper.unloading=无法分é…servlet [{0}],因为它没有被加载 +standardWrapper.waiting=正在等待为Servlet[{1}]释放[{0}]实例 + +threadLocalLeakPreventionListener.containerEvent.error=异常处ç†å®¹å™¨äº‹ä»¶[{0}] +threadLocalLeakPreventionListener.lifecycleEvent.error=处ç†ç”Ÿå‘½å‘¨æœŸäº‹ä»¶[{0}]æ—¶å‘生异常 diff --git a/java/org/apache/catalina/core/NamingContextListener.java b/java/org/apache/catalina/core/NamingContextListener.java new file mode 100644 index 0000000..95c69ea --- /dev/null +++ b/java/org/apache/catalina/core/NamingContextListener.java @@ -0,0 +1,1237 @@ +/* + * 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.catalina.core; + + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.reflect.Constructor; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.StringTokenizer; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.naming.NameAlreadyBoundException; +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.StringRefAddr; + +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.naming.ContextAccessController; +import org.apache.naming.ContextBindings; +import org.apache.naming.EjbRef; +import org.apache.naming.HandlerRef; +import org.apache.naming.LookupRef; +import org.apache.naming.NamingContext; +import org.apache.naming.ResourceEnvRef; +import org.apache.naming.ResourceLinkRef; +import org.apache.naming.ResourceRef; +import org.apache.naming.ServiceRef; +import org.apache.naming.TransactionRef; +import org.apache.naming.factory.ResourceLinkFactory; +import org.apache.tomcat.util.descriptor.web.ContextEjb; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextHandler; +import org.apache.tomcat.util.descriptor.web.ContextLocalEjb; +import org.apache.tomcat.util.descriptor.web.ContextResource; +import org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef; +import org.apache.tomcat.util.descriptor.web.ContextResourceLink; +import org.apache.tomcat.util.descriptor.web.ContextService; +import org.apache.tomcat.util.descriptor.web.ContextTransaction; +import org.apache.tomcat.util.descriptor.web.MessageDestinationRef; +import org.apache.tomcat.util.descriptor.web.ResourceBase; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Helper class used to initialize and populate the JNDI context associated with each context and server. + * + * @author Remy Maucherat + */ +public class NamingContextListener implements LifecycleListener, PropertyChangeListener { + + private static final Log log = LogFactory.getLog(NamingContextListener.class); + protected static final StringManager sm = StringManager.getManager(NamingContextListener.class); + + + // ----------------------------------------------------- Instance Variables + + /** + * Name of the associated naming context. + */ + protected String name = "/"; + + + /** + * Associated container. + */ + protected Object container = null; + + /** + * Token for configuring associated JNDI context. + */ + private Object token = null; + + /** + * Initialized flag. + */ + protected boolean initialized = false; + + + /** + * Associated naming resources. + */ + protected NamingResourcesImpl namingResources = null; + + + /** + * Associated JNDI context. + */ + protected NamingContext namingContext = null; + + + /** + * Comp context. + */ + protected javax.naming.Context compCtx = null; + + + /** + * Env context. + */ + protected javax.naming.Context envCtx = null; + + + /** + * Objectnames Map. + */ + protected HashMap objectNames = new HashMap<>(); + + + /** + * Determines if an attempt to write to a read-only context results in an exception or if the request is ignored. + */ + private boolean exceptionOnFailedWrite = true; + + + // ------------------------------------------------------------- Properties + + /** + * @return whether or not an attempt to modify the JNDI context will trigger an exception or if the request will be + * ignored. + */ + public boolean getExceptionOnFailedWrite() { + return exceptionOnFailedWrite; + } + + + /** + * Controls whether or not an attempt to modify the JNDI context will trigger an exception or if the request will be + * ignored. + * + * @param exceptionOnFailedWrite The new value + */ + public void setExceptionOnFailedWrite(boolean exceptionOnFailedWrite) { + this.exceptionOnFailedWrite = exceptionOnFailedWrite; + } + + + /** + * @return the "name" property. + */ + public String getName() { + return this.name; + } + + + /** + * Set the "name" property. + * + * @param name The new name + */ + public void setName(String name) { + this.name = name; + } + + + /** + * @return the naming environment context. + */ + public javax.naming.Context getEnvContext() { + return this.envCtx; + } + + + // ---------------------------------------------- LifecycleListener Methods + + /** + * Acknowledge the occurrence of the specified event. + * + * @param event LifecycleEvent that has occurred + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + + container = event.getLifecycle(); + + if (container instanceof Context) { + namingResources = ((Context) container).getNamingResources(); + token = ((Context) container).getNamingToken(); + } else if (container instanceof Server) { + namingResources = ((Server) container).getGlobalNamingResources(); + token = ((Server) container).getNamingToken(); + } else { + return; + } + + if (Lifecycle.CONFIGURE_START_EVENT.equals(event.getType())) { + + if (initialized) { + return; + } + + try { + Hashtable contextEnv = new Hashtable<>(); + namingContext = new NamingContext(contextEnv, getName()); + ContextAccessController.setSecurityToken(getName(), token); + ContextAccessController.setSecurityToken(container, token); + ContextBindings.bindContext(container, namingContext, token); + if (log.isDebugEnabled()) { + log.debug(sm.getString("naming.bind", container)); + } + + // Configure write when read-only behaviour + namingContext.setExceptionOnFailedWrite(getExceptionOnFailedWrite()); + + // Setting the context in read/write mode + ContextAccessController.setWritable(getName(), token); + + try { + createNamingContext(); + } catch (NamingException e) { + log.error(sm.getString("naming.namingContextCreationFailed", e)); + } + + namingResources.addPropertyChangeListener(this); + + // Binding the naming context to the class loader + if (container instanceof Context) { + // Setting the context in read only mode + ContextAccessController.setReadOnly(getName()); + try { + ContextBindings.bindClassLoader(container, token, + ((Context) container).getLoader().getClassLoader()); + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + } + + if (container instanceof Server) { + ResourceLinkFactory.setGlobalContext(namingContext); + try { + ContextBindings.bindClassLoader(container, token, this.getClass().getClassLoader()); + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + if (container instanceof StandardServer) { + ((StandardServer) container).setGlobalNamingContext(namingContext); + } + } + + } finally { + // Regardless of success, so that we can do cleanup on configure_stop + initialized = true; + } + + } else if (Lifecycle.CONFIGURE_STOP_EVENT.equals(event.getType())) { + + if (!initialized) { + return; + } + + try { + // Setting the context in read/write mode + ContextAccessController.setWritable(getName(), token); + ContextBindings.unbindContext(container, token); + + if (container instanceof Context) { + ContextBindings.unbindClassLoader(container, token, + ((Context) container).getLoader().getClassLoader()); + } + + if (container instanceof Server) { + ContextBindings.unbindClassLoader(container, token, this.getClass().getClassLoader()); + } + + namingResources.removePropertyChangeListener(this); + + ContextAccessController.unsetSecurityToken(getName(), token); + ContextAccessController.unsetSecurityToken(container, token); + + // unregister mbeans. + if (!objectNames.isEmpty()) { + Collection names = objectNames.values(); + Registry registry = Registry.getRegistry(null, null); + for (ObjectName objectName : names) { + registry.unregisterComponent(objectName); + } + } + + javax.naming.Context global = getGlobalNamingContext(); + if (global != null) { + ResourceLinkFactory.deregisterGlobalResourceAccess(global); + } + } finally { + objectNames.clear(); + + namingContext = null; + envCtx = null; + compCtx = null; + initialized = false; + } + + } + + } + + + // ----------------------------------------- PropertyChangeListener Methods + + + /** + * Process property change events. + * + * @param event The property change event that has occurred + */ + @Override + public void propertyChange(PropertyChangeEvent event) { + + if (!initialized) { + return; + } + + Object source = event.getSource(); + if (source == namingResources) { + + // Setting the context in read/write mode + ContextAccessController.setWritable(getName(), token); + + processGlobalResourcesChange(event.getPropertyName(), event.getOldValue(), event.getNewValue()); + + // Setting the context in read only mode + ContextAccessController.setReadOnly(getName()); + + } + + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Process a property change on the naming resources, by making the corresponding addition or removal to the + * associated JNDI context. + * + * @param name Property name of the change to be processed + * @param oldValue The old value (or null if adding) + * @param newValue The new value (or null if removing) + */ + private void processGlobalResourcesChange(String name, Object oldValue, Object newValue) { + + if (name.equals("ejb")) { + if (oldValue != null) { + ContextEjb ejb = (ContextEjb) oldValue; + if (ejb.getName() != null) { + removeEjb(ejb.getName()); + } + } + if (newValue != null) { + ContextEjb ejb = (ContextEjb) newValue; + if (ejb.getName() != null) { + addEjb(ejb); + } + } + } else if (name.equals("environment")) { + if (oldValue != null) { + ContextEnvironment env = (ContextEnvironment) oldValue; + if (env.getName() != null) { + removeEnvironment(env.getName()); + } + } + if (newValue != null) { + ContextEnvironment env = (ContextEnvironment) newValue; + if (env.getName() != null) { + addEnvironment(env); + } + } + } else if (name.equals("localEjb")) { + if (oldValue != null) { + ContextLocalEjb ejb = (ContextLocalEjb) oldValue; + if (ejb.getName() != null) { + removeLocalEjb(ejb.getName()); + } + } + if (newValue != null) { + ContextLocalEjb ejb = (ContextLocalEjb) newValue; + if (ejb.getName() != null) { + addLocalEjb(ejb); + } + } + } else if (name.equals("messageDestinationRef")) { + if (oldValue != null) { + MessageDestinationRef mdr = (MessageDestinationRef) oldValue; + if (mdr.getName() != null) { + removeMessageDestinationRef(mdr.getName()); + } + } + if (newValue != null) { + MessageDestinationRef mdr = (MessageDestinationRef) newValue; + if (mdr.getName() != null) { + addMessageDestinationRef(mdr); + } + } + } else if (name.equals("resource")) { + if (oldValue != null) { + ContextResource resource = (ContextResource) oldValue; + if (resource.getName() != null) { + removeResource(resource.getName()); + } + } + if (newValue != null) { + ContextResource resource = (ContextResource) newValue; + if (resource.getName() != null) { + addResource(resource); + } + } + } else if (name.equals("resourceEnvRef")) { + if (oldValue != null) { + ContextResourceEnvRef resourceEnvRef = (ContextResourceEnvRef) oldValue; + if (resourceEnvRef.getName() != null) { + removeResourceEnvRef(resourceEnvRef.getName()); + } + } + if (newValue != null) { + ContextResourceEnvRef resourceEnvRef = (ContextResourceEnvRef) newValue; + if (resourceEnvRef.getName() != null) { + addResourceEnvRef(resourceEnvRef); + } + } + } else if (name.equals("resourceLink")) { + if (oldValue != null) { + ContextResourceLink rl = (ContextResourceLink) oldValue; + if (rl.getName() != null) { + removeResourceLink(rl.getName()); + } + } + if (newValue != null) { + ContextResourceLink rl = (ContextResourceLink) newValue; + if (rl.getName() != null) { + addResourceLink(rl); + } + } + } else if (name.equals("service")) { + if (oldValue != null) { + ContextService service = (ContextService) oldValue; + if (service.getName() != null) { + removeService(service.getName()); + } + } + if (newValue != null) { + ContextService service = (ContextService) newValue; + if (service.getName() != null) { + addService(service); + } + } + } + + + } + + + /** + * Create and initialize the JNDI naming context. + */ + private void createNamingContext() throws NamingException { + + // Creating the comp subcontext + if (container instanceof Server) { + compCtx = namingContext; + envCtx = namingContext; + } else { + compCtx = namingContext.createSubcontext("comp"); + envCtx = compCtx.createSubcontext("env"); + } + + int i; + + if (log.isTraceEnabled()) { + log.trace("Creating JNDI naming context"); + } + + if (namingResources == null) { + namingResources = new NamingResourcesImpl(); + namingResources.setContainer(container); + } + + // Resource links + ContextResourceLink[] resourceLinks = namingResources.findResourceLinks(); + for (i = 0; i < resourceLinks.length; i++) { + addResourceLink(resourceLinks[i]); + } + + // Resources + ContextResource[] resources = namingResources.findResources(); + for (i = 0; i < resources.length; i++) { + addResource(resources[i]); + } + + // Resources Env + ContextResourceEnvRef[] resourceEnvRefs = namingResources.findResourceEnvRefs(); + for (i = 0; i < resourceEnvRefs.length; i++) { + addResourceEnvRef(resourceEnvRefs[i]); + } + + // Environment entries + ContextEnvironment[] contextEnvironments = namingResources.findEnvironments(); + for (i = 0; i < contextEnvironments.length; i++) { + addEnvironment(contextEnvironments[i]); + } + + // EJB references + ContextEjb[] ejbs = namingResources.findEjbs(); + for (i = 0; i < ejbs.length; i++) { + addEjb(ejbs[i]); + } + + // Message Destination References + MessageDestinationRef[] mdrs = namingResources.findMessageDestinationRefs(); + for (i = 0; i < mdrs.length; i++) { + addMessageDestinationRef(mdrs[i]); + } + + // WebServices references + ContextService[] services = namingResources.findServices(); + for (i = 0; i < services.length; i++) { + addService(services[i]); + } + + // Binding a User Transaction reference + if (container instanceof Context) { + try { + Reference ref = new TransactionRef(); + compCtx.bind("UserTransaction", ref); + ContextTransaction transaction = namingResources.getTransaction(); + if (transaction != null) { + Iterator params = transaction.listProperties(); + while (params.hasNext()) { + String paramName = params.next(); + String paramValue = (String) transaction.getProperty(paramName); + StringRefAddr refAddr = new StringRefAddr(paramName, paramValue); + ref.add(refAddr); + } + } + } catch (NameAlreadyBoundException e) { + // Ignore because UserTransaction was obviously + // added via ResourceLink + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + } + + // Binding the resources directory context + if (container instanceof Context) { + try { + compCtx.bind("Resources", ((Context) container).getResources()); + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + } + + } + + + /** + * Create an ObjectName for this ContextResource object. + * + * @param resource The resource + * + * @return ObjectName The object name + * + * @exception MalformedObjectNameException if a name cannot be created + */ + protected ObjectName createObjectName(ContextResource resource) throws MalformedObjectNameException { + + String domain = null; + if (container instanceof StandardServer) { + domain = ((StandardServer) container).getDomain(); + } else if (container instanceof ContainerBase) { + domain = ((ContainerBase) container).getDomain(); + } + if (domain == null) { + domain = "Catalina"; + } + + ObjectName name = null; + String quotedResourceName = ObjectName.quote(resource.getName()); + if (container instanceof Server) { + name = new ObjectName( + domain + ":type=DataSource" + ",class=" + resource.getType() + ",name=" + quotedResourceName); + } else if (container instanceof Context) { + String contextName = ((Context) container).getName(); + if (!contextName.startsWith("/")) { + contextName = "/" + contextName; + } + Host host = (Host) ((Context) container).getParent(); + name = new ObjectName(domain + ":type=DataSource" + ",host=" + host.getName() + ",context=" + contextName + + ",class=" + resource.getType() + ",name=" + quotedResourceName); + } + + return name; + + } + + + /** + * Set the specified EJBs in the naming context. + * + * @param ejb the EJB descriptor + */ + public void addEjb(ContextEjb ejb) { + + Reference ref = lookForLookupRef(ejb); + + if (ref == null) { + // Create a reference to the EJB. + ref = new EjbRef(ejb.getType(), ejb.getHome(), ejb.getRemote(), ejb.getLink()); + // Adding the additional parameters, if any + Iterator params = ejb.listProperties(); + while (params.hasNext()) { + String paramName = params.next(); + String paramValue = (String) ejb.getProperty(paramName); + StringRefAddr refAddr = new StringRefAddr(paramName, paramValue); + ref.add(refAddr); + } + } + + try { + createSubcontexts(envCtx, ejb.getName()); + envCtx.bind(ejb.getName(), ref); + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + } + + + /** + * Set the specified environment entries in the naming context. + * + * @param env the environment entry + */ + public void addEnvironment(ContextEnvironment env) { + + Object value = lookForLookupRef(env); + + if (value == null) { + // Instantiating a new instance of the correct object type, and + // initializing it. + String type = env.getType(); + try { + if (type.equals("java.lang.String")) { + value = env.getValue(); + } else if (type.equals("java.lang.Byte")) { + if (env.getValue() == null) { + value = Byte.valueOf((byte) 0); + } else { + value = Byte.decode(env.getValue()); + } + } else if (type.equals("java.lang.Short")) { + if (env.getValue() == null) { + value = Short.valueOf((short) 0); + } else { + value = Short.decode(env.getValue()); + } + } else if (type.equals("java.lang.Integer")) { + if (env.getValue() == null) { + value = Integer.valueOf(0); + } else { + value = Integer.decode(env.getValue()); + } + } else if (type.equals("java.lang.Long")) { + if (env.getValue() == null) { + value = Long.valueOf(0); + } else { + value = Long.decode(env.getValue()); + } + } else if (type.equals("java.lang.Boolean")) { + value = Boolean.valueOf(env.getValue()); + } else if (type.equals("java.lang.Double")) { + if (env.getValue() == null) { + value = Double.valueOf(0); + } else { + value = Double.valueOf(env.getValue()); + } + } else if (type.equals("java.lang.Float")) { + if (env.getValue() == null) { + value = Float.valueOf(0); + } else { + value = Float.valueOf(env.getValue()); + } + } else if (type.equals("java.lang.Character")) { + if (env.getValue() == null) { + value = Character.valueOf((char) 0); + } else { + if (env.getValue().length() == 1) { + value = Character.valueOf(env.getValue().charAt(0)); + } else { + throw new IllegalArgumentException(); + } + } + } else { + value = constructEnvEntry(env.getType(), env.getValue()); + if (value == null) { + log.error(sm.getString("naming.invalidEnvEntryType", env.getName())); + } + } + } catch (IllegalArgumentException e) { + log.error(sm.getString("naming.invalidEnvEntryValue", env.getName())); + } + } + + // Binding the object to the appropriate name + if (value != null) { + try { + if (log.isDebugEnabled()) { + log.debug(sm.getString("naming.addEnvEntry", env.getName())); + } + createSubcontexts(envCtx, env.getName()); + envCtx.bind(env.getName(), value); + } catch (NamingException e) { + log.error(sm.getString("naming.invalidEnvEntryValue", e)); + } + } + } + + + private Object constructEnvEntry(String type, String value) { + try { + Class clazz = Class.forName(type); + Constructor c = null; + try { + c = clazz.getConstructor(String.class); + return c.newInstance(value); + } catch (NoSuchMethodException e) { + // Ignore + } + + if (value.length() != 1) { + return null; + } + + try { + c = clazz.getConstructor(char.class); + return c.newInstance(Character.valueOf(value.charAt(0))); + } catch (NoSuchMethodException e) { + // Ignore + } + } catch (Exception e) { + // Ignore + } + return null; + } + + /** + * Set the specified local EJBs in the naming context. + * + * @param localEjb the local EJB descriptor (unused) + */ + public void addLocalEjb(ContextLocalEjb localEjb) { + // NO-OP + // No factory in org.apache.naming.factory + // No reference in org.apache.naming + } + + + /** + * Set the specified message destination refs in the naming context. + * + * @param mdr the message destination ref descriptor (unused) + */ + public void addMessageDestinationRef(MessageDestinationRef mdr) { + // NO-OP + // No factory in org.apache.naming.factory + // No reference in org.apache.naming + } + + + /** + * Set the specified web service in the naming context. + * + * @param service the web service descriptor + */ + public void addService(ContextService service) { + + Reference ref = lookForLookupRef(service); + + if (ref == null) { + + if (service.getWsdlfile() != null) { + URL wsdlURL = null; + + try { + URI wsdlURI = new URI(service.getWsdlfile()); + wsdlURL = wsdlURI.toURL(); + } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { + // Ignore and carry on + } + if (wsdlURL == null) { + try { + wsdlURL = ((Context) container).getServletContext().getResource(service.getWsdlfile()); + } catch (MalformedURLException e) { + // Ignore and carry on + } + } + if (wsdlURL == null) { + try { + wsdlURL = ((Context) container).getServletContext().getResource("/" + service.getWsdlfile()); + if (log.isDebugEnabled()) { + log.debug(sm.getString("naming.addSlash", service.getWsdlfile())); + } + } catch (MalformedURLException e) { + log.error(sm.getString("naming.wsdlFailed", e)); + } + } + if (wsdlURL == null) { + service.setWsdlfile(null); + } else { + service.setWsdlfile(wsdlURL.toString()); + } + } + + if (service.getJaxrpcmappingfile() != null) { + URL jaxrpcURL = null; + + try { + URI jaxrpcURI = new URI(service.getJaxrpcmappingfile()); + jaxrpcURL = jaxrpcURI.toURL(); + } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { + // Ignore and carry on + } + if (jaxrpcURL == null) { + try { + jaxrpcURL = + ((Context) container).getServletContext().getResource(service.getJaxrpcmappingfile()); + } catch (MalformedURLException e) { + // Ignore and carry on + } + } + if (jaxrpcURL == null) { + try { + jaxrpcURL = ((Context) container).getServletContext() + .getResource("/" + service.getJaxrpcmappingfile()); + if (log.isDebugEnabled()) { + log.debug(sm.getString("naming.addSlash", service.getJaxrpcmappingfile())); + } + } catch (MalformedURLException e) { + log.error(sm.getString("naming.wsdlFailed", e)); + } + } + if (jaxrpcURL == null) { + service.setJaxrpcmappingfile(null); + } else { + service.setJaxrpcmappingfile(jaxrpcURL.toString()); + } + } + + // Create a reference to the resource. + ref = new ServiceRef(service.getName(), service.getInterface(), service.getServiceqname(), + service.getWsdlfile(), service.getJaxrpcmappingfile()); + + // Adding the additional port-component-ref, if any + Iterator portcomponent = service.getServiceendpoints(); + while (portcomponent.hasNext()) { + String serviceendpoint = portcomponent.next(); + StringRefAddr refAddr = new StringRefAddr(ServiceRef.SERVICEENDPOINTINTERFACE, serviceendpoint); + ref.add(refAddr); + String portlink = service.getPortlink(serviceendpoint); + refAddr = new StringRefAddr(ServiceRef.PORTCOMPONENTLINK, portlink); + ref.add(refAddr); + } + // Adding the additional parameters, if any + Iterator handlers = service.getHandlers(); + while (handlers.hasNext()) { + String handlername = handlers.next(); + ContextHandler handler = service.getHandler(handlername); + HandlerRef handlerRef = new HandlerRef(handlername, handler.getHandlerclass()); + Iterator localParts = handler.getLocalparts(); + while (localParts.hasNext()) { + String localPart = localParts.next(); + String namespaceURI = handler.getNamespaceuri(localPart); + handlerRef.add(new StringRefAddr(HandlerRef.HANDLER_LOCALPART, localPart)); + handlerRef.add(new StringRefAddr(HandlerRef.HANDLER_NAMESPACE, namespaceURI)); + } + Iterator params = handler.listProperties(); + while (params.hasNext()) { + String paramName = params.next(); + String paramValue = (String) handler.getProperty(paramName); + handlerRef.add(new StringRefAddr(HandlerRef.HANDLER_PARAMNAME, paramName)); + handlerRef.add(new StringRefAddr(HandlerRef.HANDLER_PARAMVALUE, paramValue)); + } + for (int i = 0; i < handler.getSoapRolesSize(); i++) { + handlerRef.add(new StringRefAddr(HandlerRef.HANDLER_SOAPROLE, handler.getSoapRole(i))); + } + for (int i = 0; i < handler.getPortNamesSize(); i++) { + handlerRef.add(new StringRefAddr(HandlerRef.HANDLER_PORTNAME, handler.getPortName(i))); + } + ((ServiceRef) ref).addHandler(handlerRef); + } + } + + try { + if (log.isDebugEnabled()) { + log.debug(sm.getString("naming.addService", ref, service.getName())); + } + createSubcontexts(envCtx, service.getName()); + envCtx.bind(service.getName(), ref); + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + } + + + /** + * Set the specified resources in the naming context. + * + * @param resource the resource descriptor + */ + public void addResource(ContextResource resource) { + + Reference ref = lookForLookupRef(resource); + + if (ref == null) { + // Create a reference to the resource. + ref = new ResourceRef(resource.getType(), resource.getDescription(), resource.getScope(), + resource.getAuth(), resource.getSingleton()); + // Adding the additional parameters, if any + Iterator params = resource.listProperties(); + while (params.hasNext()) { + String paramName = params.next(); + String paramValue = (String) resource.getProperty(paramName); + StringRefAddr refAddr = new StringRefAddr(paramName, paramValue); + ref.add(refAddr); + } + } + + try { + if (log.isDebugEnabled()) { + log.debug(sm.getString("naming.addResourceRef", ref, resource.getName())); + } + createSubcontexts(envCtx, resource.getName()); + envCtx.bind(resource.getName(), ref); + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + + if (("javax.sql.DataSource".equals(ref.getClassName()) || + "javax.sql.XADataSource".equals(ref.getClassName())) && resource.getSingleton()) { + Object actualResource = null; + try { + ObjectName on = createObjectName(resource); + actualResource = envCtx.lookup(resource.getName()); + Registry.getRegistry(null, null).registerComponent(actualResource, on, null); + objectNames.put(resource.getName(), on); + } catch (Exception e) { + log.warn(sm.getString("naming.jmxRegistrationFailed", e)); + } + // Bug 63210. DBCP2 DataSources require an explicit close. This goes + // further and cleans up and AutoCloseable DataSource by default. + if (actualResource instanceof AutoCloseable && !resource.getCloseMethodConfigured()) { + resource.setCloseMethod("close"); + } + } + } + + + /** + * Set the specified resources in the naming context. + * + * @param resourceEnvRef the resource reference + */ + public void addResourceEnvRef(ContextResourceEnvRef resourceEnvRef) { + + Reference ref = lookForLookupRef(resourceEnvRef); + + if (ref == null) { + // Create a reference to the resource env. + ref = new ResourceEnvRef(resourceEnvRef.getType()); + // Adding the additional parameters, if any + Iterator params = resourceEnvRef.listProperties(); + while (params.hasNext()) { + String paramName = params.next(); + String paramValue = (String) resourceEnvRef.getProperty(paramName); + StringRefAddr refAddr = new StringRefAddr(paramName, paramValue); + ref.add(refAddr); + } + } + + try { + if (log.isDebugEnabled()) { + log.debug(sm.getString("naming.addResourceEnvRef", resourceEnvRef.getName())); + } + createSubcontexts(envCtx, resourceEnvRef.getName()); + envCtx.bind(resourceEnvRef.getName(), ref); + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + } + + + /** + * Set the specified resource link in the naming context. + * + * @param resourceLink the resource link + */ + public void addResourceLink(ContextResourceLink resourceLink) { + + // Create a reference to the resource. + Reference ref = + new ResourceLinkRef(resourceLink.getType(), resourceLink.getGlobal(), resourceLink.getFactory(), null); + Iterator i = resourceLink.listProperties(); + while (i.hasNext()) { + String key = i.next(); + Object val = resourceLink.getProperty(key); + if (val != null) { + StringRefAddr refAddr = new StringRefAddr(key, val.toString()); + ref.add(refAddr); + } + } + javax.naming.Context ctx = "UserTransaction".equals(resourceLink.getName()) ? compCtx : envCtx; + try { + if (log.isDebugEnabled()) { + log.debug(sm.getString("naming.addResourceLink", resourceLink.getName())); + } + createSubcontexts(envCtx, resourceLink.getName()); + ctx.bind(resourceLink.getName(), ref); + } catch (NamingException e) { + log.error(sm.getString("naming.bindFailed", e)); + } + + ResourceLinkFactory.registerGlobalResourceAccess(getGlobalNamingContext(), resourceLink.getName(), + resourceLink.getGlobal()); + } + + + private javax.naming.Context getGlobalNamingContext() { + if (container instanceof Context) { + Engine e = (Engine) ((Context) container).getParent().getParent(); + Server s = e.getService().getServer(); + // When the Service is an embedded Service, there is no Server + if (s != null) { + return s.getGlobalNamingContext(); + } + } + return null; + } + + + /** + * Remove the specified EJB from the naming context. + * + * @param name the name of the EJB which should be removed + */ + public void removeEjb(String name) { + + try { + envCtx.unbind(name); + } catch (NamingException e) { + log.error(sm.getString("naming.unbindFailed", name), e); + } + + } + + + /** + * Remove the specified environment entry from the naming context. + * + * @param name the name of the environment entry which should be removed + */ + public void removeEnvironment(String name) { + + try { + envCtx.unbind(name); + } catch (NamingException e) { + log.error(sm.getString("naming.unbindFailed", name), e); + } + + } + + + /** + * Remove the specified local EJB from the naming context. + * + * @param name the name of the EJB which should be removed + */ + public void removeLocalEjb(String name) { + + try { + envCtx.unbind(name); + } catch (NamingException e) { + log.error(sm.getString("naming.unbindFailed", name), e); + } + + } + + + /** + * Remove the specified message destination ref from the naming context. + * + * @param name the name of the message destination ref which should be removed + */ + public void removeMessageDestinationRef(String name) { + + try { + envCtx.unbind(name); + } catch (NamingException e) { + log.error(sm.getString("naming.unbindFailed", name), e); + } + + } + + + /** + * Remove the specified web service from the naming context. + * + * @param name the name of the web service which should be removed + */ + public void removeService(String name) { + + try { + envCtx.unbind(name); + } catch (NamingException e) { + log.error(sm.getString("naming.unbindFailed", name), e); + } + + } + + + /** + * Remove the specified resource from the naming context. + * + * @param name the name of the resource which should be removed + */ + public void removeResource(String name) { + + try { + envCtx.unbind(name); + } catch (NamingException e) { + log.error(sm.getString("naming.unbindFailed", name), e); + } + + ObjectName on = objectNames.get(name); + if (on != null) { + Registry.getRegistry(null, null).unregisterComponent(on); + } + + } + + + /** + * Remove the specified resource environment reference from the naming context. + * + * @param name the name of the resource environment reference which should be removed + */ + public void removeResourceEnvRef(String name) { + + try { + envCtx.unbind(name); + } catch (NamingException e) { + log.error(sm.getString("naming.unbindFailed", name), e); + } + + } + + + /** + * Remove the specified resource link from the naming context. + * + * @param name the name of the resource link which should be removed + */ + public void removeResourceLink(String name) { + + try { + envCtx.unbind(name); + } catch (NamingException e) { + log.error(sm.getString("naming.unbindFailed", name), e); + } + + ResourceLinkFactory.deregisterGlobalResourceAccess(getGlobalNamingContext(), name); + } + + + /** + * Create all intermediate subcontexts. + */ + private void createSubcontexts(javax.naming.Context ctx, String name) throws NamingException { + javax.naming.Context currentContext = ctx; + StringTokenizer tokenizer = new StringTokenizer(name, "/"); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if ((!token.equals("")) && (tokenizer.hasMoreTokens())) { + try { + currentContext = currentContext.createSubcontext(token); + } catch (NamingException e) { + // Silent catch. Probably an object is already bound in + // the context. + currentContext = (javax.naming.Context) currentContext.lookup(token); + } + } + } + } + + + /** + * Gets look up reference from resource if exist. + * + * @param resourceBase resource base object + * + * @return lookup ref + */ + private LookupRef lookForLookupRef(ResourceBase resourceBase) { + String lookupName = resourceBase.getLookupName(); + if ((lookupName != null && !lookupName.equals(""))) { + return new LookupRef(resourceBase.getType(), lookupName); + } + return null; + } +} diff --git a/java/org/apache/catalina/core/OpenSSLLifecycleListener.java b/java/org/apache/catalina/core/OpenSSLLifecycleListener.java new file mode 100644 index 0000000..477ca41 --- /dev/null +++ b/java/org/apache/catalina/core/OpenSSLLifecycleListener.java @@ -0,0 +1,233 @@ +/* + * 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.catalina.core; + + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.net.openssl.OpenSSLStatus; +import org.apache.tomcat.util.res.StringManager; + + + +/** + * Implementation of LifecycleListener that will do the global + * initialization of OpenSSL according to specified configuration parameters. + * Using the listener is completely optional, but is needed for configuration + * and full cleanup of a few native memory allocations. + */ +public class OpenSSLLifecycleListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(OpenSSLLifecycleListener.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(OpenSSLLifecycleListener.class); + + protected static final Object lock = new Object(); + + public static boolean isAvailable() { + // https://bz.apache.org/bugzilla/show_bug.cgi?id=48613 + if (OpenSSLStatus.isInstanceCreated()) { + synchronized (lock) { + if (!JreCompat.isJre22Available()) { + OpenSSLStatus.setInitialized(true); + } else { + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + openSSLLibraryClass.getMethod("init").invoke(null); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("openssllistener.sslInit"), t); + } + } + } + } + return OpenSSLStatus.isAvailable(); + } + + public OpenSSLLifecycleListener() { + OpenSSLStatus.setInstanceCreated(true); + } + + // ---------------------------------------------- LifecycleListener Methods + + /** + * Primary entry point for startup and shutdown events. + * + * @param event The event that has occurred + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + + boolean initError = false; + if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) { + if (!(event.getLifecycle() instanceof Server)) { + log.warn(sm.getString("listener.notServer", + event.getLifecycle().getClass().getSimpleName())); + } + synchronized (lock) { + if (!JreCompat.isJre22Available()) { + log.info(sm.getString("openssllistener.java22")); + OpenSSLStatus.setInitialized(true); + return; + } + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + openSSLLibraryClass.getMethod("init").invoke(null); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("openssllistener.sslInit"), t); + initError = true; + } + // Failure to initialize FIPS mode is fatal + if (!(null == getFIPSMode() || "off".equalsIgnoreCase(getFIPSMode())) && !isFIPSModeActive()) { + String errorMessage = sm.getString("openssllistener.initializeFIPSFailed"); + Error e = new Error(errorMessage); + // Log here, because thrown error might be not logged + log.fatal(errorMessage, e); + initError = true; + } + } + } + if (initError || Lifecycle.AFTER_DESTROY_EVENT.equals(event.getType())) { + synchronized (lock) { + if (!JreCompat.isJre22Available()) { + return; + } + // Note: Without the listener, destroy will never be called (which is not a significant problem) + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + openSSLLibraryClass.getMethod("destroy").invoke(null); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + log.info(sm.getString("openssllistener.destroy")); + } + } + } + + } + + public String getSSLEngine() { + if (JreCompat.isJre22Available()) { + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + return (String) openSSLLibraryClass.getMethod("getSSLEngine").invoke(null); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + } + return null; + } + + public void setSSLEngine(String SSLEngine) { + if (JreCompat.isJre22Available()) { + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + openSSLLibraryClass.getMethod("setSSLEngine").invoke(null, SSLEngine); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + } + } + + public String getSSLRandomSeed() { + if (JreCompat.isJre22Available()) { + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + return (String) openSSLLibraryClass.getMethod("getSSLRandomSeed").invoke(null); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + } + return null; + } + + public void setSSLRandomSeed(String SSLRandomSeed) { + if (JreCompat.isJre22Available()) { + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + openSSLLibraryClass.getMethod("setSSLRandomSeed").invoke(null, SSLRandomSeed); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + } + } + + public String getFIPSMode() { + if (JreCompat.isJre22Available()) { + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + return (String) openSSLLibraryClass.getMethod("getFIPSMode").invoke(null); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + } + return null; + } + + public void setFIPSMode(String FIPSMode) { + if (JreCompat.isJre22Available()) { + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + openSSLLibraryClass.getMethod("setFIPSMode").invoke(null, FIPSMode); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + } + } + + public boolean isFIPSModeActive() { + if (JreCompat.isJre22Available()) { + try { + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + return ((Boolean) openSSLLibraryClass.getMethod("isFIPSModeActive").invoke(null)).booleanValue(); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + } + } + return false; + } + + public void setUseOpenSSL(boolean useOpenSSL) { + if (useOpenSSL != OpenSSLStatus.getUseOpenSSL()) { + OpenSSLStatus.setUseOpenSSL(useOpenSSL); + } + } + + public static boolean getUseOpenSSL() { + return OpenSSLStatus.getUseOpenSSL(); + } + +} diff --git a/java/org/apache/catalina/core/PropertiesRoleMappingListener.java b/java/org/apache/catalina/core/PropertiesRoleMappingListener.java new file mode 100644 index 0000000..00ddc5b --- /dev/null +++ b/java/org/apache/catalina/core/PropertiesRoleMappingListener.java @@ -0,0 +1,145 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Properties; + +import org.apache.catalina.Context; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource.Resource; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implementation of {@code LifecycleListener} that will populate the context's role mapping from a properties file. + *

    + * This listener must only be nested within {@link Context} elements. + *

    + * The keys represent application roles (e.g., admin, user, uservisor, etc.) while the values represent technical roles + * (e.g., DNs, SIDs, UUIDs, etc.). A key can also be prefixed if, e.g., the properties file contains generic + * application configuration as well: {@code app-roles.}. + *

    + * Note: The default value for the {@code roleMappingFile} is {@code webapp:/WEB-INF/role-mapping.properties}. + */ +public class PropertiesRoleMappingListener implements LifecycleListener { + + private static final String WEBAPP_PROTOCOL = "webapp:"; + + private static final Log log = LogFactory.getLog(PropertiesRoleMappingListener.class); + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(ContextNamingInfoListener.class); + + private String roleMappingFile = WEBAPP_PROTOCOL + "/WEB-INF/role-mapping.properties"; + private String keyPrefix; + + /** + * Sets the path to the role mapping properties file. You can use protocol {@code webapp:} and whatever + * {@link ConfigFileLoader} supports. + * + * @param roleMappingFile the role mapping properties file to load from + * @throws NullPointerException if roleMappingFile is null + * @throws IllegalArgumentException if roleMappingFile is empty + */ + public void setRoleMappingFile(String roleMappingFile) { + Objects.requireNonNull(roleMappingFile, sm.getString("propertiesRoleMappingListener.roleMappingFileNull")); + if (roleMappingFile.isEmpty()) { + throw new IllegalArgumentException(sm.getString("propertiesRoleMappingListener.roleMappingFileEmpty")); + } + + this.roleMappingFile = roleMappingFile; + } + + /** + * Gets the path to the role mapping properties file. + * + * @return the path to the role mapping properties file + */ + public String getRoleMappingFile() { + return roleMappingFile; + } + + /** + * Sets the prefix to filter from property keys. All other keys will be ignored which do not have the prefix. + * + * @param keyPrefix the properties key prefix + */ + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + } + + /** + * Gets the prefix to filter from property keys. + * + * @return the properties key prefix + */ + public String getKeyPrefix() { + return keyPrefix; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { + if (!(event.getLifecycle() instanceof Context)) { + log.warn(sm.getString("listener.notContext", event.getLifecycle().getClass().getSimpleName())); + return; + } + Properties props = new Properties(); + Context context = (Context) event.getLifecycle(); + try (Resource resource = context.findConfigFileResource(roleMappingFile)) { + props.load(resource.getInputStream()); + } catch (IOException e) { + throw new IllegalStateException( + sm.getString("propertiesRoleMappingListener.roleMappingFileFail", roleMappingFile), e); + } + + int linkCount = 0; + for (Entry prop : props.entrySet()) { + String role = (String) prop.getKey(); + + if (keyPrefix != null) { + if (role.startsWith(keyPrefix)) { + role = role.substring(keyPrefix.length()); + } else { + continue; + } + } + + String link = (String) prop.getValue(); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("propertiesRoleMappingListener.linkedRole", role, link)); + } + context.addRoleMapping(role, link); + linkCount++; + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("propertiesRoleMappingListener.linkedRoleCount", Integer.valueOf(linkCount))); + } + } + } + +} diff --git a/java/org/apache/catalina/core/RestrictedFilters.properties b/java/org/apache/catalina/core/RestrictedFilters.properties new file mode 100644 index 0000000..c916920 --- /dev/null +++ b/java/org/apache/catalina/core/RestrictedFilters.properties @@ -0,0 +1,16 @@ +# 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. + +org.apache.catalina.ssi.SSIFilter=restricted diff --git a/java/org/apache/catalina/core/RestrictedListeners.properties b/java/org/apache/catalina/core/RestrictedListeners.properties new file mode 100644 index 0000000..09697dc --- /dev/null +++ b/java/org/apache/catalina/core/RestrictedListeners.properties @@ -0,0 +1,15 @@ +# 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. + diff --git a/java/org/apache/catalina/core/RestrictedServlets.properties b/java/org/apache/catalina/core/RestrictedServlets.properties new file mode 100644 index 0000000..cefa249 --- /dev/null +++ b/java/org/apache/catalina/core/RestrictedServlets.properties @@ -0,0 +1,19 @@ +# 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. + +org.apache.catalina.ssi.SSIServlet=restricted +org.apache.catalina.servlets.CGIServlet=restricted +org.apache.catalina.manager.JMXProxyServlet=restricted +org.apache.catalina.manager.StatusManagerServlet=restricted diff --git a/java/org/apache/catalina/core/StandardContext.java b/java/org/apache/catalina/core/StandardContext.java new file mode 100644 index 0000000..3b88a43 --- /dev/null +++ b/java/org/apache/catalina/core/StandardContext.java @@ -0,0 +1,6347 @@ +/* + * 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.catalina.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Deque; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.ListenerNotFoundException; +import javax.management.MBeanNotificationInfo; +import javax.management.Notification; +import javax.management.NotificationBroadcasterSupport; +import javax.management.NotificationEmitter; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.naming.NamingException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextAttributeListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.ServletRegistration.Dynamic; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestAttributeListener; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.http.HttpSessionAttributeListener; +import jakarta.servlet.http.HttpSessionIdListener; +import jakarta.servlet.http.HttpSessionListener; + +import org.apache.catalina.Authenticator; +import org.apache.catalina.Container; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.CredentialHandler; +import org.apache.catalina.Globals; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Loader; +import org.apache.catalina.Manager; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Realm; +import org.apache.catalina.ThreadBindingListener; +import org.apache.catalina.Valve; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.Wrapper; +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.catalina.loader.WebappClassLoaderBase; +import org.apache.catalina.loader.WebappLoader; +import org.apache.catalina.session.StandardManager; +import org.apache.catalina.util.CharsetMapper; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.util.ErrorPageSupport; +import org.apache.catalina.util.URLEncoder; +import org.apache.catalina.webresources.StandardRoot; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.naming.ContextBindings; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.InstanceManagerBindings; +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.descriptor.XmlIdentifiers; +import org.apache.tomcat.util.descriptor.web.ApplicationParameter; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.Injectable; +import org.apache.tomcat.util.descriptor.web.InjectionTarget; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.MessageDestination; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.http.CookieProcessor; +import org.apache.tomcat.util.http.Rfc6265CookieProcessor; +import org.apache.tomcat.util.scan.StandardJarScanner; +import org.apache.tomcat.util.security.PrivilegedGetTccl; +import org.apache.tomcat.util.security.PrivilegedSetTccl; +import org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor; + +/** + * Standard implementation of the Context interface. Each child container must be a Wrapper implementation to + * process the requests directed to a particular servlet. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class StandardContext extends ContainerBase implements Context, NotificationEmitter { + + private static final Log log = LogFactory.getLog(StandardContext.class); + + + // ----------------------------------------------------------- Constructors + + + /** + * Create a new StandardContext component with the default basic Valve. + */ + public StandardContext() { + + super(); + pipeline.setBasic(new StandardContextValve()); + broadcaster = new NotificationBroadcasterSupport(); + // Set defaults + if (!Globals.STRICT_SERVLET_COMPLIANCE) { + // Strict servlet compliance requires all extension mapped servlets + // to be checked against welcome files + resourceOnlyServlets.add("jsp"); + } + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Allow multipart/form-data requests to be parsed even when the target servlet doesn't specify @MultipartConfig or + * have a <multipart-config> element. + */ + protected boolean allowCasualMultipartParsing = false; + + /** + * Control whether remaining request data will be read (swallowed) even if the request violates a data size + * constraint. + */ + private boolean swallowAbortedUploads = true; + + /** + * The alternate deployment descriptor name. + */ + private String altDDName = null; + + + /** + * Lifecycle provider. + */ + private InstanceManager instanceManager = null; + + + /** + * The antiResourceLocking flag for this Context. + */ + private boolean antiResourceLocking = false; + + + /** + * The list of unique application listener class names configured for this application, in the order they were + * encountered in the resulting merged web.xml file. + */ + private CopyOnWriteArrayList applicationListeners = new CopyOnWriteArrayList<>(); + + /** + * The set of application listeners that are required to have limited access to ServletContext methods. See Servlet + * 3.1 section 4.4. + */ + private final Set noPluggabilityListeners = new HashSet<>(); + + /** + * The list of instantiated application event listener objects. Note that SCIs and other code may use the + * pluggability APIs to add listener instances directly to this list before the application starts. + */ + private List applicationEventListenersList = new CopyOnWriteArrayList<>(); + + + /** + * The set of instantiated application lifecycle listener objects. Note that SCIs and other code may use the + * pluggability APIs to add listener instances directly to this list before the application starts. + */ + private Object applicationLifecycleListenersObjects[] = new Object[0]; + + + /** + * The ordered set of ServletContainerInitializers for this web application. + */ + private Map>> initializers = new LinkedHashMap<>(); + + + /** + * The set of application parameters defined for this application. + */ + private ApplicationParameter applicationParameters[] = new ApplicationParameter[0]; + + private final Object applicationParametersLock = new Object(); + + + /** + * The broadcaster that sends j2ee notifications. + */ + private NotificationBroadcasterSupport broadcaster = null; + + /** + * The Locale to character set mapper for this application. + */ + private CharsetMapper charsetMapper = null; + + + /** + * The Java class name of the CharsetMapper class to be created. + */ + private String charsetMapperClass = "org.apache.catalina.util.CharsetMapper"; + + + /** + * The URL of the XML descriptor for this context. + */ + private URL configFile = null; + + + /** + * The "correctly configured" flag for this Context. + */ + private boolean configured = false; + + + /** + * The security constraints for this web application. + */ + private volatile SecurityConstraint constraints[] = new SecurityConstraint[0]; + + private final Object constraintsLock = new Object(); + + + /** + * The ServletContext implementation associated with this Context. + */ + protected ApplicationContext context = null; + + /** + * The wrapped version of the associated ServletContext that is presented to listeners that are required to have + * limited access to ServletContext methods. See Servlet 3.1 section 4.4. + */ + private NoPluggabilityServletContext noPluggabilityServletContext = null; + + + /** + * Should we attempt to use cookies for session id communication? + */ + private boolean cookies = true; + + + /** + * Should we allow the ServletContext.getContext() method to access the context of other web + * applications in this server? + */ + private boolean crossContext = false; + + + /** + * Encoded path. + */ + private String encodedPath = null; + + + /** + * Unencoded path for this web application. + */ + private String path = null; + + + /** + * The "follow standard delegation model" flag that will be used to configure our ClassLoader. Graal cannot actually + * load a class from the webapp classloader, so delegate by default. + */ + private boolean delegate = JreCompat.isGraalAvailable(); + + + private boolean denyUncoveredHttpMethods; + + + /** + * The display name of this web application. + */ + private String displayName = null; + + + /** + * Override the default context xml location. + */ + private String defaultContextXml; + + + /** + * Override the default web xml location. + */ + private String defaultWebXml; + + + /** + * The distributable flag for this web application. + */ + private boolean distributable = false; + + + /** + * The document root for this web application. + */ + private String docBase = null; + + + private final ErrorPageSupport errorPageSupport = new ErrorPageSupport(); + + /** + * The set of filter configurations (and associated filter instances) we have initialized, keyed by filter name. + */ + private Map filterConfigs = new HashMap<>(); // Guarded by filterDefs + + + /** + * The set of filter definitions for this application, keyed by filter name. + */ + private Map filterDefs = new HashMap<>(); + + + /** + * The set of filter mappings for this application, in the order they were defined in the deployment descriptor with + * additional mappings added via the {@link ServletContext} possibly both before and after those defined in the + * deployment descriptor. + */ + private final ContextFilterMaps filterMaps = new ContextFilterMaps(); + + /** + * Ignore annotations. + */ + private boolean ignoreAnnotations = false; + + + /** + * The Loader implementation with which this Container is associated. + */ + private Loader loader = null; + private final ReadWriteLock loaderLock = new ReentrantReadWriteLock(); + + + /** + * The login configuration descriptor for this web application. + */ + private LoginConfig loginConfig = null; + + + /** + * The Manager implementation with which this Container is associated. + */ + protected Manager manager = null; + private final ReadWriteLock managerLock = new ReentrantReadWriteLock(); + + + /** + * The naming context listener for this web application. + */ + private NamingContextListener namingContextListener = null; + + + /** + * The naming resources for this web application. + */ + private NamingResourcesImpl namingResources = null; + + /** + * The message destinations for this web application. + */ + private HashMap messageDestinations = new HashMap<>(); + + + /** + * The MIME mappings for this web application, keyed by extension. + */ + private Map mimeMappings = new HashMap<>(); + + + /** + * The context initialization parameters for this web application, keyed by name. + */ + private final Map parameters = new ConcurrentHashMap<>(); + + + /** + * The request processing pause flag (while reloading occurs) + */ + private volatile boolean paused = false; + + + /** + * The public identifier of the DTD for the web application deployment descriptor version we are currently parsing. + * This is used to support relaxed validation rules when processing version 2.2 web.xml files. + */ + private String publicId = null; + + + /** + * The reloadable flag for this web application. + */ + private boolean reloadable = false; + + + /** + * Unpack WAR property. + */ + private boolean unpackWAR = true; + + + /** + * Context level override for default {@link StandardHost#isCopyXML()}. + */ + private boolean copyXML = false; + + + /** + * The default context override flag for this web application. + */ + private boolean override = false; + + + /** + * The original document root for this web application. + */ + private String originalDocBase = null; + + + /** + * The privileged flag for this web application. + */ + private boolean privileged = false; + + + /** + * Should the next call to addWelcomeFile() cause replacement of any existing welcome files? This will + * be set before processing the web application's deployment descriptor, so that application specified choices + * replace, rather than append to, those defined in the global descriptor. + */ + private boolean replaceWelcomeFiles = false; + + + /** + * The security role mappings for this application, keyed by role name (as used within the application). + */ + private Map roleMappings = new HashMap<>(); + + + /** + * The security roles for this application, keyed by role name. + */ + private String securityRoles[] = new String[0]; + + private final Object securityRolesLock = new Object(); + + + /** + * The servlet mappings for this web application, keyed by matching pattern. + */ + private Map servletMappings = new HashMap<>(); + + private final Object servletMappingsLock = new Object(); + + + /** + * The session timeout (in minutes) for this web application. + */ + private int sessionTimeout = 30; + + /** + * The notification sequence number. + */ + private AtomicLong sequenceNumber = new AtomicLong(0); + + + /** + * Set flag to true to cause the system.out and system.err to be redirected to the logger when executing a servlet. + */ + private boolean swallowOutput = false; + + + /** + * Amount of ms that the container will wait for servlets to unload. + */ + private long unloadDelay = 2000; + + + /** + * The watched resources for this application. + */ + private String watchedResources[] = new String[0]; + + private final Object watchedResourcesLock = new Object(); + + + /** + * The welcome files for this application. + */ + private String welcomeFiles[] = new String[0]; + + private final Object welcomeFilesLock = new Object(); + + + /** + * The set of classnames of LifecycleListeners that will be added to each newly created Wrapper by + * createWrapper(). + */ + private String wrapperLifecycles[] = new String[0]; + + private final Object wrapperLifecyclesLock = new Object(); + + /** + * The set of classnames of ContainerListeners that will be added to each newly created Wrapper by + * createWrapper(). + */ + private String wrapperListeners[] = new String[0]; + + private final Object wrapperListenersLock = new Object(); + + /** + * The pathname to the work directory for this context (relative to the server's home if not absolute). + */ + private String workDir = null; + + + /** + * Java class name of the Wrapper class implementation we use. + */ + private String wrapperClassName = StandardWrapper.class.getName(); + private Class wrapperClass = null; + + + /** + * JNDI use flag. + */ + private boolean useNaming = true; + + + /** + * Name of the associated naming context. + */ + private String namingContextName = null; + + + private WebResourceRoot resources; + private final ReadWriteLock resourcesLock = new ReentrantReadWriteLock(); + + private long startupTime; + private long startTime; + private long tldScanTime; + + /** + * Name of the engine. If null, the domain is used. + */ + private String j2EEApplication = "none"; + private String j2EEServer = "none"; + + + /** + * Attribute value used to turn on/off XML validation for web.xml and web-fragment.xml files. + */ + private boolean webXmlValidation = Globals.STRICT_SERVLET_COMPLIANCE; + + + /** + * Attribute value used to turn on/off XML namespace validation + */ + private boolean webXmlNamespaceAware = Globals.STRICT_SERVLET_COMPLIANCE; + + + /** + * Attribute used to turn on/off the use of external entities. + */ + private boolean xmlBlockExternal = true; + + + /** + * Attribute value used to turn on/off XML validation + */ + private boolean tldValidation = Globals.STRICT_SERVLET_COMPLIANCE; + + + /** + * The name to use for session cookies. null indicates that the name is controlled by the application. + */ + private String sessionCookieName; + + + /** + * The flag that indicates that session cookies should use HttpOnly + */ + private boolean useHttpOnly = true; + + private boolean usePartitioned = false; + + + /** + * The domain to use for session cookies. null indicates that the domain is controlled by the + * application. + */ + private String sessionCookieDomain; + + + /** + * The path to use for session cookies. null indicates that the path is controlled by the application. + */ + private String sessionCookiePath; + + + /** + * Is a / added to the end of the session cookie path to ensure browsers, particularly IE, don't send a session + * cookie for context /foo with requests intended for context /foobar. + */ + private boolean sessionCookiePathUsesTrailingSlash = false; + + + /** + * The Jar scanner to use to search for Jars that might contain configuration information such as TLDs or + * web-fragment.xml files. + */ + private JarScanner jarScanner = null; + + /** + * Enables the RMI Target memory leak detection to be controlled. This is necessary since the detection can only + * work if some of the modularity checks are disabled. + */ + private boolean clearReferencesRmiTargets = true; + + /** + * Should Tomcat attempt to terminate threads that have been started by the web application? Stopping threads is + * performed via the deprecated (for good reason) Thread.stop() method and is likely to result in + * instability. As such, enabling this should be viewed as an option of last resort in a development environment and + * is not recommended in a production environment. If not specified, the default value of false will be + * used. + */ + private boolean clearReferencesStopThreads = false; + + /** + * Should Tomcat attempt to terminate any {@link java.util.TimerThread}s that have been started by the web + * application? If not specified, the default value of false will be used. + */ + private boolean clearReferencesStopTimerThreads = false; + + /** + * If an HttpClient keep-alive timer thread has been started by this web application and is still running, should + * Tomcat change the context class loader from the current {@link ClassLoader} to {@link ClassLoader#getParent()} to + * prevent a memory leak? Note that the keep-alive timer thread will stop on its own once the keep-alives all expire + * however, on a busy system that might not happen for some time. + */ + private boolean clearReferencesHttpClientKeepAliveThread = true; + + /** + * Should Tomcat renew the threads of the thread pool when the application is stopped to avoid memory leaks because + * of uncleaned ThreadLocal variables. This also requires that the threadRenewalDelay property of the + * StandardThreadExecutor or ThreadPoolExecutor be set to a positive value. + */ + private boolean renewThreadsWhenStoppingContext = true; + + /** + * Should Tomcat attempt to clear references to classes loaded by the web application class loader from the + * ObjectStreamClass caches? + */ + private boolean clearReferencesObjectStreamClassCaches = true; + + /** + * Should Tomcat attempt to clear references to classes loaded by this class loader from ThreadLocals? + */ + private boolean clearReferencesThreadLocals = true; + + /** + * Should Tomcat skip the memory leak checks when the web application is stopped as part of the process of shutting + * down the JVM? + */ + private boolean skipMemoryLeakChecksOnJvmShutdown = false; + + /** + * Should the effective web.xml be logged when the context starts? + */ + private boolean logEffectiveWebXml = false; + + private int effectiveMajorVersion = 3; + + private int effectiveMinorVersion = 0; + + private JspConfigDescriptor jspConfigDescriptor = null; + + private Set resourceOnlyServlets = new HashSet<>(); + + private String webappVersion = ""; + + private boolean addWebinfClassesResources = false; + + private boolean fireRequestListenersOnForwards = false; + + /** + * Servlets created via {@link ApplicationContext#createServlet(Class)} for tracking purposes. + */ + private Set createdServlets = new HashSet<>(); + + private boolean preemptiveAuthentication = false; + + private boolean sendRedirectBody = false; + + private boolean jndiExceptionOnFailedWrite = true; + + private Map postConstructMethods = new HashMap<>(); + private Map preDestroyMethods = new HashMap<>(); + + private String containerSciFilter; + + private Boolean failCtxIfServletStartFails; + + protected static final ThreadBindingListener DEFAULT_NAMING_LISTENER = (new ThreadBindingListener() { + @Override + public void bind() { + } + + @Override + public void unbind() { + } + }); + protected ThreadBindingListener threadBindingListener = DEFAULT_NAMING_LISTENER; + + private final Object namingToken = new Object(); + + private CookieProcessor cookieProcessor; + + private boolean validateClientProvidedNewSessionId = true; + + private boolean mapperContextRootRedirectEnabled = true; + + private boolean mapperDirectoryRedirectEnabled = false; + + private boolean useRelativeRedirects = !Globals.STRICT_SERVLET_COMPLIANCE; + + private boolean dispatchersUseEncodedPaths = true; + + private String requestEncoding = null; + + private String responseEncoding = null; + + private boolean allowMultipleLeadingForwardSlashInPath = false; + + private final AtomicLong inProgressAsyncCount = new AtomicLong(0); + + private boolean createUploadTargets = false; + + private boolean alwaysAccessSession = Globals.STRICT_SERVLET_COMPLIANCE; + + private boolean contextGetResourceRequiresSlash = Globals.STRICT_SERVLET_COMPLIANCE; + + private boolean dispatcherWrapsSameObject = Globals.STRICT_SERVLET_COMPLIANCE; + + private boolean suspendWrappedResponseAfterForward = false; + + private boolean parallelAnnotationScanning = false; + + private boolean useBloomFilterForArchives = false; + + // ----------------------------------------------------- Context Properties + + @Override + public void setCreateUploadTargets(boolean createUploadTargets) { + this.createUploadTargets = createUploadTargets; + } + + + @Override + public boolean getCreateUploadTargets() { + return createUploadTargets; + } + + + @Override + public void incrementInProgressAsyncCount() { + inProgressAsyncCount.incrementAndGet(); + } + + + @Override + public void decrementInProgressAsyncCount() { + inProgressAsyncCount.decrementAndGet(); + } + + + public long getInProgressAsyncCount() { + return inProgressAsyncCount.get(); + } + + + @Override + public void setAllowMultipleLeadingForwardSlashInPath(boolean allowMultipleLeadingForwardSlashInPath) { + this.allowMultipleLeadingForwardSlashInPath = allowMultipleLeadingForwardSlashInPath; + } + + + @Override + public boolean getAllowMultipleLeadingForwardSlashInPath() { + return allowMultipleLeadingForwardSlashInPath; + } + + + @Override + public boolean getAlwaysAccessSession() { + return alwaysAccessSession; + } + + + @Override + public void setAlwaysAccessSession(boolean alwaysAccessSession) { + this.alwaysAccessSession = alwaysAccessSession; + } + + + @Override + public boolean getContextGetResourceRequiresSlash() { + return contextGetResourceRequiresSlash; + } + + + @Override + public void setContextGetResourceRequiresSlash(boolean contextGetResourceRequiresSlash) { + this.contextGetResourceRequiresSlash = contextGetResourceRequiresSlash; + } + + + @Override + public boolean getDispatcherWrapsSameObject() { + return dispatcherWrapsSameObject; + } + + + @Override + public void setDispatcherWrapsSameObject(boolean dispatcherWrapsSameObject) { + this.dispatcherWrapsSameObject = dispatcherWrapsSameObject; + } + + + @Override + public boolean getSuspendWrappedResponseAfterForward() { + return suspendWrappedResponseAfterForward; + } + + + @Override + public void setSuspendWrappedResponseAfterForward(boolean suspendWrappedResponseAfterForward) { + this.suspendWrappedResponseAfterForward = suspendWrappedResponseAfterForward; + } + + + @Override + public String getRequestCharacterEncoding() { + return requestEncoding; + } + + + @Override + public void setRequestCharacterEncoding(String requestEncoding) { + this.requestEncoding = requestEncoding; + } + + + @Override + public String getResponseCharacterEncoding() { + return responseEncoding; + } + + + @Override + public void setResponseCharacterEncoding(String responseEncoding) { + /* + * This ensures that the context response encoding is represented by a unique String object. This enables the + * Default Servlet to differentiate between a Response using this default encoding and one that has been + * explicitly configured. + */ + if (responseEncoding == null) { + this.responseEncoding = null; + } else { + this.responseEncoding = new String(responseEncoding); + } + } + + + @Override + public void setDispatchersUseEncodedPaths(boolean dispatchersUseEncodedPaths) { + this.dispatchersUseEncodedPaths = dispatchersUseEncodedPaths; + } + + + /** + * {@inheritDoc} + *

    + * The default value for this implementation is {@code true}. + */ + @Override + public boolean getDispatchersUseEncodedPaths() { + return dispatchersUseEncodedPaths; + } + + + @Override + public void setUseRelativeRedirects(boolean useRelativeRedirects) { + this.useRelativeRedirects = useRelativeRedirects; + } + + + /** + * {@inheritDoc} + *

    + * The default value for this implementation is {@code true}. + */ + @Override + public boolean getUseRelativeRedirects() { + return useRelativeRedirects; + } + + + @Override + public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) { + this.mapperContextRootRedirectEnabled = mapperContextRootRedirectEnabled; + } + + + /** + * {@inheritDoc} + *

    + * The default value for this implementation is {@code false}. + */ + @Override + public boolean getMapperContextRootRedirectEnabled() { + return mapperContextRootRedirectEnabled; + } + + + @Override + public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) { + this.mapperDirectoryRedirectEnabled = mapperDirectoryRedirectEnabled; + } + + + /** + * {@inheritDoc} + *

    + * The default value for this implementation is {@code false}. + */ + @Override + public boolean getMapperDirectoryRedirectEnabled() { + return mapperDirectoryRedirectEnabled; + } + + + @Override + public void setValidateClientProvidedNewSessionId(boolean validateClientProvidedNewSessionId) { + this.validateClientProvidedNewSessionId = validateClientProvidedNewSessionId; + } + + + /** + * {@inheritDoc} + *

    + * The default value for this implementation is {@code true}. + */ + @Override + public boolean getValidateClientProvidedNewSessionId() { + return validateClientProvidedNewSessionId; + } + + + @Override + public void setCookieProcessor(CookieProcessor cookieProcessor) { + if (cookieProcessor == null) { + throw new IllegalArgumentException(sm.getString("standardContext.cookieProcessor.null")); + } + this.cookieProcessor = cookieProcessor; + } + + + @Override + public CookieProcessor getCookieProcessor() { + return cookieProcessor; + } + + + @Override + public Object getNamingToken() { + return namingToken; + } + + + @Override + public void setContainerSciFilter(String containerSciFilter) { + this.containerSciFilter = containerSciFilter; + } + + + @Override + public String getContainerSciFilter() { + return containerSciFilter; + } + + + @Override + public boolean getSendRedirectBody() { + return sendRedirectBody; + } + + + @Override + public void setSendRedirectBody(boolean sendRedirectBody) { + this.sendRedirectBody = sendRedirectBody; + } + + + @Override + public boolean getPreemptiveAuthentication() { + return preemptiveAuthentication; + } + + + @Override + public void setPreemptiveAuthentication(boolean preemptiveAuthentication) { + this.preemptiveAuthentication = preemptiveAuthentication; + } + + + @Override + public void setFireRequestListenersOnForwards(boolean enable) { + fireRequestListenersOnForwards = enable; + } + + + @Override + public boolean getFireRequestListenersOnForwards() { + return fireRequestListenersOnForwards; + } + + + @Override + public void setAddWebinfClassesResources(boolean addWebinfClassesResources) { + this.addWebinfClassesResources = addWebinfClassesResources; + } + + + @Override + public boolean getAddWebinfClassesResources() { + return addWebinfClassesResources; + } + + + @Override + public void setWebappVersion(String webappVersion) { + if (null == webappVersion) { + this.webappVersion = ""; + } else { + this.webappVersion = webappVersion; + } + } + + + @Override + public String getWebappVersion() { + return webappVersion; + } + + + @Override + public String getBaseName() { + return new ContextName(path, webappVersion).getBaseName(); + } + + + @Override + public String getResourceOnlyServlets() { + return StringUtils.join(resourceOnlyServlets); + } + + + @Override + public void setResourceOnlyServlets(String resourceOnlyServlets) { + this.resourceOnlyServlets.clear(); + if (resourceOnlyServlets == null) { + return; + } + for (String servletName : resourceOnlyServlets.split(",")) { + servletName = servletName.trim(); + if (servletName.length() > 0) { + this.resourceOnlyServlets.add(servletName); + } + } + } + + + @Override + public boolean isResourceOnlyServlet(String servletName) { + return resourceOnlyServlets.contains(servletName); + } + + + @Override + public int getEffectiveMajorVersion() { + return effectiveMajorVersion; + } + + @Override + public void setEffectiveMajorVersion(int effectiveMajorVersion) { + this.effectiveMajorVersion = effectiveMajorVersion; + } + + @Override + public int getEffectiveMinorVersion() { + return effectiveMinorVersion; + } + + @Override + public void setEffectiveMinorVersion(int effectiveMinorVersion) { + this.effectiveMinorVersion = effectiveMinorVersion; + } + + @Override + public void setLogEffectiveWebXml(boolean logEffectiveWebXml) { + this.logEffectiveWebXml = logEffectiveWebXml; + } + + @Override + public boolean getLogEffectiveWebXml() { + return logEffectiveWebXml; + } + + @Override + public Authenticator getAuthenticator() { + Pipeline pipeline = getPipeline(); + if (pipeline != null) { + Valve basic = pipeline.getBasic(); + if (basic instanceof Authenticator) { + return (Authenticator) basic; + } + for (Valve valve : pipeline.getValves()) { + if (valve instanceof Authenticator) { + return (Authenticator) valve; + } + } + } + return null; + } + + @Override + public JarScanner getJarScanner() { + if (jarScanner == null) { + jarScanner = new StandardJarScanner(); + } + return jarScanner; + } + + + @Override + public void setJarScanner(JarScanner jarScanner) { + this.jarScanner = jarScanner; + } + + + @Override + public InstanceManager getInstanceManager() { + return instanceManager; + } + + + @Override + public void setInstanceManager(InstanceManager instanceManager) { + this.instanceManager = instanceManager; + } + + + @Override + public String getEncodedPath() { + return encodedPath; + } + + + /** + * Set to true to allow requests mapped to servlets that do not explicitly declare @MultipartConfig or + * have <multipart-config> specified in web.xml to parse multipart/form-data requests. + * + * @param allowCasualMultipartParsing true to allow such casual parsing, false otherwise. + */ + @Override + public void setAllowCasualMultipartParsing(boolean allowCasualMultipartParsing) { + this.allowCasualMultipartParsing = allowCasualMultipartParsing; + } + + /** + * Returns true if requests mapped to servlets without "multipart config" to parse multipart/form-data + * requests anyway. + * + * @return true if requests mapped to servlets without "multipart config" to parse multipart/form-data + * requests, false otherwise. + */ + @Override + public boolean getAllowCasualMultipartParsing() { + return this.allowCasualMultipartParsing; + } + + /** + * Set to false to disable request data swallowing after an upload was aborted due to size constraints. + * + * @param swallowAbortedUploads false to disable swallowing, true otherwise (default). + */ + @Override + public void setSwallowAbortedUploads(boolean swallowAbortedUploads) { + this.swallowAbortedUploads = swallowAbortedUploads; + } + + /** + * Returns true if remaining request data will be read (swallowed) even the request violates a data + * size constraint. + * + * @return true if data will be swallowed (default), false otherwise. + */ + @Override + public boolean getSwallowAbortedUploads() { + return this.swallowAbortedUploads; + } + + /** + * Add a ServletContainerInitializer instance to this web application. + * + * @param sci The instance to add + * @param classes The classes in which the initializer expressed an interest + */ + @Override + public void addServletContainerInitializer(ServletContainerInitializer sci, Set> classes) { + initializers.put(sci, classes); + } + + + /** + * Return the "follow standard delegation model" flag used to configure our ClassLoader. + * + * @return true if classloading delegates to the parent classloader first + */ + public boolean getDelegate() { + return this.delegate; + } + + + /** + * Set the "follow standard delegation model" flag used to configure our ClassLoader. + * + * @param delegate The new flag + */ + public void setDelegate(boolean delegate) { + + boolean oldDelegate = this.delegate; + this.delegate = delegate; + support.firePropertyChange("delegate", oldDelegate, this.delegate); + + } + + + /** + * @return true if the internal naming support is used. + */ + public boolean isUseNaming() { + return useNaming; + } + + + /** + * Enables or disables naming. + * + * @param useNaming true to enable the naming environment + */ + public void setUseNaming(boolean useNaming) { + this.useNaming = useNaming; + } + + + @Override + public Object[] getApplicationEventListeners() { + return applicationEventListenersList.toArray(); + } + + + /** + * {@inheritDoc} Note that this implementation is not thread safe. If two threads call this method concurrently, the + * result may be either set of listeners or a the union of both. + */ + @Override + public void setApplicationEventListeners(Object listeners[]) { + applicationEventListenersList.clear(); + if (listeners != null && listeners.length > 0) { + applicationEventListenersList.addAll(Arrays.asList(listeners)); + } + } + + + /** + * Add a listener to the end of the list of initialized application event listeners. + * + * @param listener The listener to add + */ + public void addApplicationEventListener(Object listener) { + applicationEventListenersList.add(listener); + } + + + @Override + public Object[] getApplicationLifecycleListeners() { + return applicationLifecycleListenersObjects; + } + + + /** + * Store the set of initialized application lifecycle listener objects, in the order they were specified in the web + * application deployment descriptor, for this application. + * + * @param listeners The set of instantiated listener objects. + */ + @Override + public void setApplicationLifecycleListeners(Object listeners[]) { + applicationLifecycleListenersObjects = listeners; + } + + + /** + * Add a listener to the end of the list of initialized application lifecycle listeners. + * + * @param listener The listener to add + */ + public void addApplicationLifecycleListener(Object listener) { + int len = applicationLifecycleListenersObjects.length; + Object[] newListeners = Arrays.copyOf(applicationLifecycleListenersObjects, len + 1); + newListeners[len] = listener; + applicationLifecycleListenersObjects = newListeners; + } + + + /** + * @return the antiResourceLocking flag for this Context. + */ + public boolean getAntiResourceLocking() { + return this.antiResourceLocking; + } + + + /** + * Set the antiResourceLocking feature for this Context. + * + * @param antiResourceLocking The new flag value + */ + public void setAntiResourceLocking(boolean antiResourceLocking) { + + boolean oldAntiResourceLocking = this.antiResourceLocking; + this.antiResourceLocking = antiResourceLocking; + support.firePropertyChange("antiResourceLocking", oldAntiResourceLocking, this.antiResourceLocking); + + } + + + @Override + @Deprecated + public boolean getUseBloomFilterForArchives() { + return this.useBloomFilterForArchives; + } + + + @Override + @Deprecated + public void setUseBloomFilterForArchives(boolean useBloomFilterForArchives) { + boolean oldUseBloomFilterForArchives = this.useBloomFilterForArchives; + this.useBloomFilterForArchives = useBloomFilterForArchives; + support.firePropertyChange("useBloomFilterForArchives", oldUseBloomFilterForArchives, + this.useBloomFilterForArchives); + } + + + @Override + public void setParallelAnnotationScanning(boolean parallelAnnotationScanning) { + + boolean oldParallelAnnotationScanning = this.parallelAnnotationScanning; + this.parallelAnnotationScanning = parallelAnnotationScanning; + support.firePropertyChange("parallelAnnotationScanning", oldParallelAnnotationScanning, + this.parallelAnnotationScanning); + + } + + + @Override + public boolean getParallelAnnotationScanning() { + return this.parallelAnnotationScanning; + } + + + /** + * @return the Locale to character set mapper for this Context. + */ + public CharsetMapper getCharsetMapper() { + + // Create a mapper the first time it is requested + if (this.charsetMapper == null) { + try { + Class clazz = Class.forName(charsetMapperClass); + this.charsetMapper = (CharsetMapper) clazz.getConstructor().newInstance(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + this.charsetMapper = new CharsetMapper(); + } + } + + return this.charsetMapper; + + } + + + /** + * Set the Locale to character set mapper for this Context. + * + * @param mapper The new mapper + */ + public void setCharsetMapper(CharsetMapper mapper) { + + CharsetMapper oldCharsetMapper = this.charsetMapper; + this.charsetMapper = mapper; + if (mapper != null) { + this.charsetMapperClass = mapper.getClass().getName(); + } + support.firePropertyChange("charsetMapper", oldCharsetMapper, this.charsetMapper); + + } + + + @Override + public String getCharset(Locale locale) { + return getCharsetMapper().getCharset(locale); + } + + + @Override + public URL getConfigFile() { + return this.configFile; + } + + + @Override + public void setConfigFile(URL configFile) { + this.configFile = configFile; + } + + + @Override + public boolean getConfigured() { + return this.configured; + } + + + /** + * Set the "correctly configured" flag for this Context. This can be set to false by startup listeners that detect a + * fatal configuration error to avoid the application from being made available. + * + * @param configured The new correctly configured flag + */ + @Override + public void setConfigured(boolean configured) { + + boolean oldConfigured = this.configured; + this.configured = configured; + support.firePropertyChange("configured", oldConfigured, this.configured); + + } + + + @Override + public boolean getCookies() { + return this.cookies; + } + + + /** + * Set the "use cookies for session ids" flag. + * + * @param cookies The new flag + */ + @Override + public void setCookies(boolean cookies) { + + boolean oldCookies = this.cookies; + this.cookies = cookies; + support.firePropertyChange("cookies", oldCookies, this.cookies); + + } + + + /** + * Gets the name to use for session cookies. Overrides any setting that may be specified by the application. + * + * @return The value of the default session cookie name or null if not specified + */ + @Override + public String getSessionCookieName() { + return sessionCookieName; + } + + + /** + * Sets the name to use for session cookies. Overrides any setting that may be specified by the application. + * + * @param sessionCookieName The name to use + */ + @Override + public void setSessionCookieName(String sessionCookieName) { + String oldSessionCookieName = this.sessionCookieName; + this.sessionCookieName = sessionCookieName; + support.firePropertyChange("sessionCookieName", oldSessionCookieName, sessionCookieName); + } + + + /** + * Gets the value of the use HttpOnly cookies for session cookies flag. + * + * @return true if the HttpOnly flag should be set on session cookies + */ + @Override + public boolean getUseHttpOnly() { + return useHttpOnly; + } + + + /** + * Sets the use HttpOnly cookies for session cookies flag. + * + * @param useHttpOnly Set to true to use HttpOnly cookies for session cookies + */ + @Override + public void setUseHttpOnly(boolean useHttpOnly) { + boolean oldUseHttpOnly = this.useHttpOnly; + this.useHttpOnly = useHttpOnly; + support.firePropertyChange("useHttpOnly", oldUseHttpOnly, this.useHttpOnly); + } + + + @Override + public boolean getUsePartitioned() { + return usePartitioned; + } + + + @Override + public void setUsePartitioned(boolean usePartitioned) { + boolean oldUsePartitioned = this.usePartitioned; + this.usePartitioned = usePartitioned; + support.firePropertyChange("usePartitioned", oldUsePartitioned, this.usePartitioned); + } + + + /** + * Gets the domain to use for session cookies. Overrides any setting that may be specified by the application. + * + * @return The value of the default session cookie domain or null if not specified + */ + @Override + public String getSessionCookieDomain() { + return sessionCookieDomain; + } + + + /** + * Sets the domain to use for session cookies. Overrides any setting that may be specified by the application. + * + * @param sessionCookieDomain The domain to use + */ + @Override + public void setSessionCookieDomain(String sessionCookieDomain) { + String oldSessionCookieDomain = this.sessionCookieDomain; + this.sessionCookieDomain = sessionCookieDomain; + support.firePropertyChange("sessionCookieDomain", oldSessionCookieDomain, sessionCookieDomain); + } + + + /** + * Gets the path to use for session cookies. Overrides any setting that may be specified by the application. + * + * @return The value of the default session cookie path or null if not specified + */ + @Override + public String getSessionCookiePath() { + return sessionCookiePath; + } + + + /** + * Sets the path to use for session cookies. Overrides any setting that may be specified by the application. + * + * @param sessionCookiePath The path to use + */ + @Override + public void setSessionCookiePath(String sessionCookiePath) { + String oldSessionCookiePath = this.sessionCookiePath; + this.sessionCookiePath = sessionCookiePath; + support.firePropertyChange("sessionCookiePath", oldSessionCookiePath, sessionCookiePath); + } + + + @Override + public boolean getSessionCookiePathUsesTrailingSlash() { + return sessionCookiePathUsesTrailingSlash; + } + + + @Override + public void setSessionCookiePathUsesTrailingSlash(boolean sessionCookiePathUsesTrailingSlash) { + this.sessionCookiePathUsesTrailingSlash = sessionCookiePathUsesTrailingSlash; + } + + + @Override + public boolean getCrossContext() { + return this.crossContext; + } + + + /** + * Set the "allow crossing servlet contexts" flag. + * + * @param crossContext The new cross contexts flag + */ + @Override + public void setCrossContext(boolean crossContext) { + + boolean oldCrossContext = this.crossContext; + this.crossContext = crossContext; + support.firePropertyChange("crossContext", oldCrossContext, this.crossContext); + + } + + public String getDefaultContextXml() { + return defaultContextXml; + } + + /** + * Set the location of the default context xml that will be used. If not absolute, it'll be made relative to the + * engine's base dir ( which defaults to catalina.base system property ). + * + * @param defaultContextXml The default web xml + */ + public void setDefaultContextXml(String defaultContextXml) { + this.defaultContextXml = defaultContextXml; + } + + public String getDefaultWebXml() { + return defaultWebXml; + } + + /** + * Set the location of the default web xml that will be used. If not absolute, it'll be made relative to the + * engine's base dir ( which defaults to catalina.base system property ). + * + * @param defaultWebXml The default web xml + */ + public void setDefaultWebXml(String defaultWebXml) { + this.defaultWebXml = defaultWebXml; + } + + /** + * Gets the time (in milliseconds) it took to start this context. + * + * @return Time (in milliseconds) it took to start this context. + */ + public long getStartupTime() { + return startupTime; + } + + public void setStartupTime(long startupTime) { + this.startupTime = startupTime; + } + + public long getTldScanTime() { + return tldScanTime; + } + + public void setTldScanTime(long tldScanTime) { + this.tldScanTime = tldScanTime; + } + + + @Override + public boolean getDenyUncoveredHttpMethods() { + return denyUncoveredHttpMethods; + } + + + @Override + public void setDenyUncoveredHttpMethods(boolean denyUncoveredHttpMethods) { + this.denyUncoveredHttpMethods = denyUncoveredHttpMethods; + } + + + /** + * @return the display name of this web application. + */ + @Override + public String getDisplayName() { + return this.displayName; + } + + + /** + * @return the alternate Deployment Descriptor name. + */ + @Override + public String getAltDDName() { + return altDDName; + } + + + /** + * Set an alternate Deployment Descriptor name. + * + * @param altDDName The new name + */ + @Override + public void setAltDDName(String altDDName) { + this.altDDName = altDDName; + if (context != null) { + context.setAttribute(Globals.ALT_DD_ATTR, altDDName); + } + } + + + /** + * Set the display name of this web application. + * + * @param displayName The new display name + */ + @Override + public void setDisplayName(String displayName) { + + String oldDisplayName = this.displayName; + this.displayName = displayName; + support.firePropertyChange("displayName", oldDisplayName, this.displayName); + } + + + /** + * @return the distributable flag for this web application. + */ + @Override + public boolean getDistributable() { + return this.distributable; + } + + /** + * Set the distributable flag for this web application. + * + * @param distributable The new distributable flag + */ + @Override + public void setDistributable(boolean distributable) { + boolean oldDistributable = this.distributable; + this.distributable = distributable; + support.firePropertyChange("distributable", oldDistributable, this.distributable); + } + + + @Override + public String getDocBase() { + return this.docBase; + } + + + @Override + public void setDocBase(String docBase) { + this.docBase = docBase; + } + + + public String getJ2EEApplication() { + return j2EEApplication; + } + + public void setJ2EEApplication(String j2EEApplication) { + this.j2EEApplication = j2EEApplication; + } + + public String getJ2EEServer() { + return j2EEServer; + } + + public void setJ2EEServer(String j2EEServer) { + this.j2EEServer = j2EEServer; + } + + + @Override + public Loader getLoader() { + Lock readLock = loaderLock.readLock(); + readLock.lock(); + try { + return loader; + } finally { + readLock.unlock(); + } + } + + @Override + public void setLoader(Loader loader) { + + Lock writeLock = loaderLock.writeLock(); + writeLock.lock(); + Loader oldLoader = null; + try { + // Change components if necessary + oldLoader = this.loader; + if (oldLoader == loader) { + return; + } + this.loader = loader; + // Start the new component if necessary + if (loader != null) { + loader.setContext(this); + } + } finally { + writeLock.unlock(); + } + + // Stop the old component if necessary + if (getState().isAvailable() && oldLoader instanceof Lifecycle) { + try { + ((Lifecycle) oldLoader).stop(); + } catch (LifecycleException e) { + log.error(sm.getString("standardContext.setLoader.stop"), e); + } + } + + if (getState().isAvailable() && loader instanceof Lifecycle) { + try { + ((Lifecycle) loader).start(); + } catch (LifecycleException e) { + log.error(sm.getString("standardContext.setLoader.start"), e); + } + } + + // Report this property change to interested listeners + support.firePropertyChange("loader", oldLoader, loader); + } + + + @Override + public Manager getManager() { + Lock readLock = managerLock.readLock(); + readLock.lock(); + try { + return manager; + } finally { + readLock.unlock(); + } + } + + + @Override + public void setManager(Manager manager) { + + Lock writeLock = managerLock.writeLock(); + writeLock.lock(); + Manager oldManager = null; + try { + // Change components if necessary + oldManager = this.manager; + if (oldManager == manager) { + return; + } + this.manager = manager; + // Start the new component if necessary + if (manager != null) { + manager.setContext(this); + } + } finally { + writeLock.unlock(); + } + + // Stop the old component if necessary + if (oldManager instanceof Lifecycle) { + try { + ((Lifecycle) oldManager).stop(); + ((Lifecycle) oldManager).destroy(); + } catch (LifecycleException e) { + log.error(sm.getString("standardContext.setManager.stop"), e); + } + } + + if (getState().isAvailable() && manager instanceof Lifecycle) { + try { + ((Lifecycle) manager).start(); + } catch (LifecycleException e) { + log.error(sm.getString("standardContext.setManager.start"), e); + } + } + + // Report this property change to interested listeners + support.firePropertyChange("manager", oldManager, manager); + } + + + /** + * @return the boolean on the annotations parsing. + */ + @Override + public boolean getIgnoreAnnotations() { + return this.ignoreAnnotations; + } + + + /** + * Set the boolean on the annotations parsing for this web application. + * + * @param ignoreAnnotations The boolean on the annotations parsing + */ + @Override + public void setIgnoreAnnotations(boolean ignoreAnnotations) { + boolean oldIgnoreAnnotations = this.ignoreAnnotations; + this.ignoreAnnotations = ignoreAnnotations; + support.firePropertyChange("ignoreAnnotations", oldIgnoreAnnotations, this.ignoreAnnotations); + } + + + /** + * @return the login configuration descriptor for this web application. + */ + @Override + public LoginConfig getLoginConfig() { + return this.loginConfig; + } + + + /** + * Set the login configuration descriptor for this web application. + * + * @param config The new login configuration + */ + @Override + public void setLoginConfig(LoginConfig config) { + + // Validate the incoming property value + if (config == null) { + throw new IllegalArgumentException(sm.getString("standardContext.loginConfig.required")); + } + String loginPage = config.getLoginPage(); + if ((loginPage != null) && !loginPage.startsWith("/")) { + if (isServlet22()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardContext.loginConfig.loginWarning", loginPage)); + } + config.setLoginPage("/" + loginPage); + } else { + throw new IllegalArgumentException(sm.getString("standardContext.loginConfig.loginPage", loginPage)); + } + } + String errorPage = config.getErrorPage(); + if ((errorPage != null) && !errorPage.startsWith("/")) { + if (isServlet22()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardContext.loginConfig.errorWarning", errorPage)); + } + config.setErrorPage("/" + errorPage); + } else { + throw new IllegalArgumentException(sm.getString("standardContext.loginConfig.errorPage", errorPage)); + } + } + + // Process the property setting change + LoginConfig oldLoginConfig = this.loginConfig; + this.loginConfig = config; + support.firePropertyChange("loginConfig", oldLoginConfig, this.loginConfig); + + } + + + /** + * @return the naming resources associated with this web application. + */ + @Override + public NamingResourcesImpl getNamingResources() { + if (namingResources == null) { + setNamingResources(new NamingResourcesImpl()); + } + return namingResources; + } + + + /** + * Set the naming resources for this web application. + * + * @param namingResources The new naming resources + */ + @Override + public void setNamingResources(NamingResourcesImpl namingResources) { + + // Process the property setting change + NamingResourcesImpl oldNamingResources = this.namingResources; + this.namingResources = namingResources; + if (namingResources != null) { + namingResources.setContainer(this); + } + support.firePropertyChange("namingResources", oldNamingResources, this.namingResources); + + if (getState() == LifecycleState.NEW || getState() == LifecycleState.INITIALIZING || + getState() == LifecycleState.INITIALIZED) { + // NEW will occur if Context is defined in server.xml + // At this point getObjectKeyPropertiesNameOnly() will trigger an + // NPE. + // INITIALIZED will occur if the Context is defined in a context.xml + // file + // If started now, a second start will be attempted when the context + // starts + + // In both cases, return and let context init the namingResources + // when it starts + return; + } + + if (oldNamingResources != null) { + try { + oldNamingResources.stop(); + oldNamingResources.destroy(); + } catch (LifecycleException e) { + log.error(sm.getString("standardContext.namingResource.destroy.fail"), e); + } + } + if (namingResources != null) { + try { + namingResources.init(); + namingResources.start(); + } catch (LifecycleException e) { + log.error(sm.getString("standardContext.namingResource.init.fail"), e); + } + } + } + + + /** + * @return the context path for this Context. + */ + @Override + public String getPath() { + return path; + } + + + /** + * Set the context path for this Context. + * + * @param path The new context path + */ + @Override + public void setPath(String path) { + boolean invalid = false; + if (path == null || path.equals("/")) { + invalid = true; + this.path = ""; + } else if (path.isEmpty() || path.startsWith("/")) { + this.path = path; + } else { + invalid = true; + this.path = "/" + path; + } + if (this.path.endsWith("/")) { + invalid = true; + this.path = this.path.substring(0, this.path.length() - 1); + } + if (invalid) { + log.warn(sm.getString("standardContext.pathInvalid", path, this.path)); + } + encodedPath = URLEncoder.DEFAULT.encode(this.path, StandardCharsets.UTF_8); + if (getName() == null) { + setName(this.path); + } + } + + + /** + * @return the public identifier of the deployment descriptor DTD that is currently being parsed. + */ + @Override + public String getPublicId() { + return this.publicId; + } + + + /** + * Set the public identifier of the deployment descriptor DTD that is currently being parsed. + * + * @param publicId The public identifier + */ + @Override + public void setPublicId(String publicId) { + + if (log.isTraceEnabled()) { + log.trace("Setting deployment descriptor public ID to '" + publicId + "'"); + } + + String oldPublicId = this.publicId; + this.publicId = publicId; + support.firePropertyChange("publicId", oldPublicId, publicId); + + } + + + /** + * @return the reloadable flag for this web application. + */ + @Override + public boolean getReloadable() { + return this.reloadable; + } + + + /** + * @return the default context override flag for this web application. + */ + @Override + public boolean getOverride() { + return this.override; + } + + + /** + * @return the original document root for this Context. This can be an absolute pathname, a relative pathname, or a + * URL. Is only set as deployment has change docRoot! + */ + public String getOriginalDocBase() { + return this.originalDocBase; + } + + /** + * Set the original document root for this Context. This can be an absolute pathname, a relative pathname, or a URL. + * + * @param docBase The original document root + */ + public void setOriginalDocBase(String docBase) { + + this.originalDocBase = docBase; + } + + + /** + * @return the parent class loader (if any) for this web application. This call is meaningful only + * after a Loader has been configured. + */ + @Override + public ClassLoader getParentClassLoader() { + if (parentClassLoader != null) { + return parentClassLoader; + } + if (getPrivileged()) { + return this.getClass().getClassLoader(); + } else if (parent != null) { + return parent.getParentClassLoader(); + } + return ClassLoader.getSystemClassLoader(); + } + + + /** + * @return the privileged flag for this web application. + */ + @Override + public boolean getPrivileged() { + return this.privileged; + } + + + /** + * Set the privileged flag for this web application. + * + * @param privileged The new privileged flag + */ + @Override + public void setPrivileged(boolean privileged) { + + boolean oldPrivileged = this.privileged; + this.privileged = privileged; + support.firePropertyChange("privileged", oldPrivileged, this.privileged); + + } + + + /** + * Set the reloadable flag for this web application. + * + * @param reloadable The new reloadable flag + */ + @Override + public void setReloadable(boolean reloadable) { + + boolean oldReloadable = this.reloadable; + this.reloadable = reloadable; + support.firePropertyChange("reloadable", oldReloadable, this.reloadable); + + } + + + /** + * Set the default context override flag for this web application. + * + * @param override The new override flag + */ + @Override + public void setOverride(boolean override) { + + boolean oldOverride = this.override; + this.override = override; + support.firePropertyChange("override", oldOverride, this.override); + + } + + + /** + * Set the "replace welcome files" property. + * + * @param replaceWelcomeFiles The new property value + */ + public void setReplaceWelcomeFiles(boolean replaceWelcomeFiles) { + + boolean oldReplaceWelcomeFiles = this.replaceWelcomeFiles; + this.replaceWelcomeFiles = replaceWelcomeFiles; + support.firePropertyChange("replaceWelcomeFiles", oldReplaceWelcomeFiles, this.replaceWelcomeFiles); + + } + + + /** + * @return the servlet context for which this Context is a facade. + */ + @Override + public ServletContext getServletContext() { + /* + * This method is called (multiple times) during context start which is single threaded so there is concurrency + * issue here. + */ + if (context == null) { + context = new ApplicationContext(this); + if (altDDName != null) { + context.setAttribute(Globals.ALT_DD_ATTR, altDDName); + } + } + return context.getFacade(); + } + + + /** + * @return the default session timeout (in minutes) for this web application. + */ + @Override + public int getSessionTimeout() { + return this.sessionTimeout; + } + + + /** + * Set the default session timeout (in minutes) for this web application. + * + * @param timeout The new default session timeout + */ + @Override + public void setSessionTimeout(int timeout) { + + int oldSessionTimeout = this.sessionTimeout; + /* + * SRV.13.4 ("Deployment Descriptor"): If the timeout is 0 or less, the container ensures the default behaviour + * of sessions is never to time out. + */ + this.sessionTimeout = (timeout == 0) ? -1 : timeout; + support.firePropertyChange("sessionTimeout", oldSessionTimeout, this.sessionTimeout); + + } + + + /** + * @return the value of the swallowOutput flag. + */ + @Override + public boolean getSwallowOutput() { + return this.swallowOutput; + } + + + /** + * Set the value of the swallowOutput flag. If set to true, the system.out and system.err will be redirected to the + * logger during a servlet execution. + * + * @param swallowOutput The new value + */ + @Override + public void setSwallowOutput(boolean swallowOutput) { + + boolean oldSwallowOutput = this.swallowOutput; + this.swallowOutput = swallowOutput; + support.firePropertyChange("swallowOutput", oldSwallowOutput, this.swallowOutput); + + } + + + /** + * @return the value of the unloadDelay flag. + */ + public long getUnloadDelay() { + return this.unloadDelay; + } + + + /** + * Set the value of the unloadDelay flag, which represents the amount of ms that the container will wait when + * unloading servlets. Setting this to a small value may cause more requests to fail to complete when stopping a web + * application. + * + * @param unloadDelay The new value + */ + public void setUnloadDelay(long unloadDelay) { + + long oldUnloadDelay = this.unloadDelay; + this.unloadDelay = unloadDelay; + support.firePropertyChange("unloadDelay", Long.valueOf(oldUnloadDelay), Long.valueOf(this.unloadDelay)); + + } + + + /** + * @return unpack WAR flag. + */ + public boolean getUnpackWAR() { + return unpackWAR; + } + + + /** + * Unpack WAR flag mutator. + * + * @param unpackWAR true to unpack WARs on deployment + */ + public void setUnpackWAR(boolean unpackWAR) { + this.unpackWAR = unpackWAR; + } + + + /** + * Flag which indicates if bundled context.xml files should be copied to the config folder. The doesn't occur by + * default. + * + * @return true if the META-INF/context.xml file included in a WAR will be copied to the + * host configuration base folder on deployment + */ + public boolean getCopyXML() { + return copyXML; + } + + + /** + * Allows copying a bundled context.xml file to the host configuration base folder on deployment. + * + * @param copyXML the new flag value + */ + public void setCopyXML(boolean copyXML) { + this.copyXML = copyXML; + } + + + /** + * @return the Java class name of the Wrapper implementation used for servlets registered in this Context. + */ + @Override + public String getWrapperClass() { + return this.wrapperClassName; + } + + + /** + * Set the Java class name of the Wrapper implementation used for servlets registered in this Context. + * + * @param wrapperClassName The new wrapper class name + * + * @throws IllegalArgumentException if the specified wrapper class cannot be found or is not a subclass of + * StandardWrapper + */ + @Override + public void setWrapperClass(String wrapperClassName) { + + this.wrapperClassName = wrapperClassName; + + try { + wrapperClass = Class.forName(wrapperClassName); + if (!StandardWrapper.class.isAssignableFrom(wrapperClass)) { + throw new IllegalArgumentException( + sm.getString("standardContext.invalidWrapperClass", wrapperClassName)); + } + } catch (ClassNotFoundException cnfe) { + throw new IllegalArgumentException(cnfe.getMessage()); + } + } + + + @Override + public WebResourceRoot getResources() { + Lock readLock = resourcesLock.readLock(); + readLock.lock(); + try { + return resources; + } finally { + readLock.unlock(); + } + } + + + @Override + public void setResources(WebResourceRoot resources) { + + Lock writeLock = resourcesLock.writeLock(); + writeLock.lock(); + WebResourceRoot oldResources = null; + try { + if (getState().isAvailable()) { + throw new IllegalStateException(sm.getString("standardContext.resourcesStart")); + } + + oldResources = this.resources; + if (oldResources == resources) { + return; + } + + this.resources = resources; + if (oldResources != null) { + oldResources.setContext(null); + } + if (resources != null) { + resources.setContext(this); + } + } finally { + writeLock.unlock(); + } + + support.firePropertyChange("resources", oldResources, resources); + + } + + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return jspConfigDescriptor; + } + + @Override + public void setJspConfigDescriptor(JspConfigDescriptor descriptor) { + this.jspConfigDescriptor = descriptor; + } + + @Override + public ThreadBindingListener getThreadBindingListener() { + return threadBindingListener; + } + + @Override + public void setThreadBindingListener(ThreadBindingListener threadBindingListener) { + this.threadBindingListener = threadBindingListener; + } + + + // ------------------------------------------------------ Public Properties + + /** + * @return whether or not an attempt to modify the JNDI context will trigger an exception or if the request will be + * ignored. + */ + public boolean getJndiExceptionOnFailedWrite() { + return jndiExceptionOnFailedWrite; + } + + + /** + * Controls whether or not an attempt to modify the JNDI context will trigger an exception or if the request will be + * ignored. + * + * @param jndiExceptionOnFailedWrite false to avoid an exception + */ + public void setJndiExceptionOnFailedWrite(boolean jndiExceptionOnFailedWrite) { + this.jndiExceptionOnFailedWrite = jndiExceptionOnFailedWrite; + } + + + /** + * @return the Locale to character set mapper class for this Context. + */ + public String getCharsetMapperClass() { + return this.charsetMapperClass; + } + + + /** + * Set the Locale to character set mapper class for this Context. + * + * @param mapper The new mapper class + */ + public void setCharsetMapperClass(String mapper) { + + String oldCharsetMapperClass = this.charsetMapperClass; + this.charsetMapperClass = mapper; + support.firePropertyChange("charsetMapperClass", oldCharsetMapperClass, this.charsetMapperClass); + + } + + + /** + * Get the absolute path to the work dir. To avoid duplication. + * + * @return The work path + */ + public String getWorkPath() { + if (getWorkDir() == null) { + return null; + } + File workDir = new File(getWorkDir()); + if (!workDir.isAbsolute()) { + try { + workDir = new File(getCatalinaBase().getCanonicalFile(), getWorkDir()); + } catch (IOException e) { + log.warn(sm.getString("standardContext.workPath", getName()), e); + } + } + return workDir.getAbsolutePath(); + } + + /** + * @return the work directory for this Context. + */ + public String getWorkDir() { + return this.workDir; + } + + + /** + * Set the work directory for this Context. + * + * @param workDir The new work directory + */ + public void setWorkDir(String workDir) { + + this.workDir = workDir; + + if (getState().isAvailable()) { + postWorkDirectory(); + } + } + + + public boolean getClearReferencesRmiTargets() { + return this.clearReferencesRmiTargets; + } + + + public void setClearReferencesRmiTargets(boolean clearReferencesRmiTargets) { + boolean oldClearReferencesRmiTargets = this.clearReferencesRmiTargets; + this.clearReferencesRmiTargets = clearReferencesRmiTargets; + support.firePropertyChange("clearReferencesRmiTargets", oldClearReferencesRmiTargets, + this.clearReferencesRmiTargets); + } + + + /** + * @return the clearReferencesStopThreads flag for this Context. + */ + public boolean getClearReferencesStopThreads() { + return this.clearReferencesStopThreads; + } + + + /** + * Set the clearReferencesStopThreads feature for this Context. + * + * @param clearReferencesStopThreads The new flag value + */ + public void setClearReferencesStopThreads(boolean clearReferencesStopThreads) { + + boolean oldClearReferencesStopThreads = this.clearReferencesStopThreads; + this.clearReferencesStopThreads = clearReferencesStopThreads; + support.firePropertyChange("clearReferencesStopThreads", oldClearReferencesStopThreads, + this.clearReferencesStopThreads); + + } + + + /** + * @return the clearReferencesStopTimerThreads flag for this Context. + */ + public boolean getClearReferencesStopTimerThreads() { + return this.clearReferencesStopTimerThreads; + } + + + /** + * Set the clearReferencesStopTimerThreads feature for this Context. + * + * @param clearReferencesStopTimerThreads The new flag value + */ + public void setClearReferencesStopTimerThreads(boolean clearReferencesStopTimerThreads) { + + boolean oldClearReferencesStopTimerThreads = this.clearReferencesStopTimerThreads; + this.clearReferencesStopTimerThreads = clearReferencesStopTimerThreads; + support.firePropertyChange("clearReferencesStopTimerThreads", oldClearReferencesStopTimerThreads, + this.clearReferencesStopTimerThreads); + } + + + /** + * @return the clearReferencesHttpClientKeepAliveThread flag for this Context. + */ + public boolean getClearReferencesHttpClientKeepAliveThread() { + return this.clearReferencesHttpClientKeepAliveThread; + } + + + /** + * Set the clearReferencesHttpClientKeepAliveThread feature for this Context. + * + * @param clearReferencesHttpClientKeepAliveThread The new flag value + */ + public void setClearReferencesHttpClientKeepAliveThread(boolean clearReferencesHttpClientKeepAliveThread) { + this.clearReferencesHttpClientKeepAliveThread = clearReferencesHttpClientKeepAliveThread; + } + + + public boolean getRenewThreadsWhenStoppingContext() { + return this.renewThreadsWhenStoppingContext; + } + + public void setRenewThreadsWhenStoppingContext(boolean renewThreadsWhenStoppingContext) { + boolean oldRenewThreadsWhenStoppingContext = this.renewThreadsWhenStoppingContext; + this.renewThreadsWhenStoppingContext = renewThreadsWhenStoppingContext; + support.firePropertyChange("renewThreadsWhenStoppingContext", oldRenewThreadsWhenStoppingContext, + this.renewThreadsWhenStoppingContext); + } + + + public boolean getClearReferencesObjectStreamClassCaches() { + return clearReferencesObjectStreamClassCaches; + } + + + public void setClearReferencesObjectStreamClassCaches(boolean clearReferencesObjectStreamClassCaches) { + boolean oldClearReferencesObjectStreamClassCaches = this.clearReferencesObjectStreamClassCaches; + this.clearReferencesObjectStreamClassCaches = clearReferencesObjectStreamClassCaches; + support.firePropertyChange("clearReferencesObjectStreamClassCaches", oldClearReferencesObjectStreamClassCaches, + this.clearReferencesObjectStreamClassCaches); + } + + + public boolean getClearReferencesThreadLocals() { + return clearReferencesThreadLocals; + } + + + public void setClearReferencesThreadLocals(boolean clearReferencesThreadLocals) { + boolean oldClearReferencesThreadLocals = this.clearReferencesThreadLocals; + this.clearReferencesThreadLocals = clearReferencesThreadLocals; + support.firePropertyChange("clearReferencesThreadLocals", oldClearReferencesThreadLocals, + this.clearReferencesThreadLocals); + } + + + public boolean getSkipMemoryLeakChecksOnJvmShutdown() { + return skipMemoryLeakChecksOnJvmShutdown; + } + + + public void setSkipMemoryLeakChecksOnJvmShutdown(boolean skipMemoryLeakChecksOnJvmShutdown) { + this.skipMemoryLeakChecksOnJvmShutdown = skipMemoryLeakChecksOnJvmShutdown; + } + + + public Boolean getFailCtxIfServletStartFails() { + return failCtxIfServletStartFails; + } + + public void setFailCtxIfServletStartFails(Boolean failCtxIfServletStartFails) { + Boolean oldFailCtxIfServletStartFails = this.failCtxIfServletStartFails; + this.failCtxIfServletStartFails = failCtxIfServletStartFails; + support.firePropertyChange("failCtxIfServletStartFails", oldFailCtxIfServletStartFails, + failCtxIfServletStartFails); + } + + protected boolean getComputedFailCtxIfServletStartFails() { + if (failCtxIfServletStartFails != null) { + return failCtxIfServletStartFails.booleanValue(); + } + // else look at Host config + if (getParent() instanceof StandardHost) { + return ((StandardHost) getParent()).isFailCtxIfServletStartFails(); + } + // else + return false; + } + + // -------------------------------------------------------- Context Methods + + /** + * Add a new Listener class name to the set of Listeners configured for this application. + * + * @param listener Java class name of a listener class + */ + @Override + public void addApplicationListener(String listener) { + if (applicationListeners.addIfAbsent(listener)) { + fireContainerEvent("addApplicationListener", listener); + } else { + log.info(sm.getString("standardContext.duplicateListener", listener)); + } + } + + + /** + * Add a new application parameter for this application. + * + * @param parameter The new application parameter + */ + @Override + public void addApplicationParameter(ApplicationParameter parameter) { + + synchronized (applicationParametersLock) { + String newName = parameter.getName(); + for (ApplicationParameter p : applicationParameters) { + if (newName.equals(p.getName()) && !p.getOverride()) { + return; + } + } + ApplicationParameter results[] = Arrays.copyOf(applicationParameters, applicationParameters.length + 1); + results[applicationParameters.length] = parameter; + applicationParameters = results; + } + fireContainerEvent("addApplicationParameter", parameter); + + } + + + /** + * Add a child Container, only if the proposed child is an implementation of Wrapper. + * + * @param child Child container to be added + * + * @exception IllegalArgumentException if the proposed container is not an implementation of Wrapper + */ + @Override + public void addChild(Container child) { + + // Global JspServlet + Wrapper oldJspServlet = null; + + if (!(child instanceof Wrapper)) { + throw new IllegalArgumentException(sm.getString("standardContext.notWrapper")); + } + + boolean isJspServlet = "jsp".equals(child.getName()); + + // Allow webapp to override JspServlet inherited from global web.xml. + if (isJspServlet) { + oldJspServlet = (Wrapper) findChild("jsp"); + if (oldJspServlet != null) { + removeChild(oldJspServlet); + } + } + + super.addChild(child); + + if (isJspServlet && oldJspServlet != null) { + /* + * The webapp-specific JspServlet inherits all the mappings specified in the global web.xml, and may add + * additional ones. + */ + String[] jspMappings = oldJspServlet.findMappings(); + for (int i = 0; jspMappings != null && i < jspMappings.length; i++) { + addServletMappingDecoded(jspMappings[i], child.getName()); + } + } + } + + + /** + * Add a security constraint to the set for this web application. + * + * @param constraint the new security constraint + */ + @Override + public void addConstraint(SecurityConstraint constraint) { + + // Validate the proposed constraint + SecurityCollection collections[] = constraint.findCollections(); + for (SecurityCollection collection : collections) { + String patterns[] = collection.findPatterns(); + for (int j = 0; j < patterns.length; j++) { + patterns[j] = adjustURLPattern(patterns[j]); + if (!validateURLPattern(patterns[j])) { + throw new IllegalArgumentException( + sm.getString("standardContext.securityConstraint.pattern", patterns[j])); + } + } + if (collection.findMethods().length > 0 && collection.findOmittedMethods().length > 0) { + throw new IllegalArgumentException(sm.getString("standardContext.securityConstraint.mixHttpMethod")); + } + } + + // Add this constraint to the set for our web application + synchronized (constraintsLock) { + SecurityConstraint[] results = Arrays.copyOf(constraints, constraints.length + 1); + results[constraints.length] = constraint; + constraints = results; + } + + } + + + /** + * Add an error page for the specified error or Java exception. + * + * @param errorPage The error page definition to be added + */ + @Override + public void addErrorPage(ErrorPage errorPage) { + // Validate the input parameters + if (errorPage == null) { + throw new IllegalArgumentException(sm.getString("standardContext.errorPage.required")); + } + String location = errorPage.getLocation(); + if ((location != null) && !location.startsWith("/")) { + if (isServlet22()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardContext.errorPage.warning", location)); + } + errorPage.setLocation("/" + location); + } else { + throw new IllegalArgumentException(sm.getString("standardContext.errorPage.error", location)); + } + } + + errorPageSupport.add(errorPage); + fireContainerEvent("addErrorPage", errorPage); + } + + + /** + * Add a filter definition to this Context. + * + * @param filterDef The filter definition to be added + */ + @Override + public void addFilterDef(FilterDef filterDef) { + + synchronized (filterDefs) { + filterDefs.put(filterDef.getFilterName(), filterDef); + } + fireContainerEvent("addFilterDef", filterDef); + + } + + + /** + * Add a filter mapping to this Context at the end of the current set of filter mappings. + * + * @param filterMap The filter mapping to be added + * + * @exception IllegalArgumentException if the specified filter name does not match an existing filter definition, or + * the filter mapping is malformed + */ + @Override + public void addFilterMap(FilterMap filterMap) { + validateFilterMap(filterMap); + // Add this filter mapping to our registered set + filterMaps.add(filterMap); + fireContainerEvent("addFilterMap", filterMap); + } + + + /** + * Add a filter mapping to this Context before the mappings defined in the deployment descriptor but after any other + * mappings added via this method. + * + * @param filterMap The filter mapping to be added + * + * @exception IllegalArgumentException if the specified filter name does not match an existing filter definition, or + * the filter mapping is malformed + */ + @Override + public void addFilterMapBefore(FilterMap filterMap) { + validateFilterMap(filterMap); + // Add this filter mapping to our registered set + filterMaps.addBefore(filterMap); + fireContainerEvent("addFilterMap", filterMap); + } + + + /** + * Validate the supplied FilterMap. + * + * @param filterMap the filter mapping + */ + private void validateFilterMap(FilterMap filterMap) { + // Validate the proposed filter mapping + String filterName = filterMap.getFilterName(); + String[] servletNames = filterMap.getServletNames(); + String[] urlPatterns = filterMap.getURLPatterns(); + if (findFilterDef(filterName) == null) { + throw new IllegalArgumentException(sm.getString("standardContext.filterMap.name", filterName)); + } + + if (!filterMap.getMatchAllServletNames() && !filterMap.getMatchAllUrlPatterns() && (servletNames.length == 0) && + (urlPatterns.length == 0)) { + throw new IllegalArgumentException(sm.getString("standardContext.filterMap.either")); + } + for (String urlPattern : urlPatterns) { + if (!validateURLPattern(urlPattern)) { + throw new IllegalArgumentException(sm.getString("standardContext.filterMap.pattern", urlPattern)); + } + } + } + + + /** + * Add a Locale Encoding Mapping (see Sec 5.4 of Servlet spec 2.4) + * + * @param locale locale to map an encoding for + * @param encoding encoding to be used for a give locale + */ + @Override + public void addLocaleEncodingMappingParameter(String locale, String encoding) { + getCharsetMapper().addCharsetMappingFromDeploymentDescriptor(locale, encoding); + } + + + /** + * Add a message destination for this web application. + * + * @param md New message destination + */ + public void addMessageDestination(MessageDestination md) { + + synchronized (messageDestinations) { + messageDestinations.put(md.getName(), md); + } + fireContainerEvent("addMessageDestination", md.getName()); + + } + + + /** + * Add a new MIME mapping, replacing any existing mapping for the specified extension. + * + * @param extension Filename extension being mapped + * @param mimeType Corresponding MIME type + */ + @Override + public void addMimeMapping(String extension, String mimeType) { + + synchronized (mimeMappings) { + mimeMappings.put(extension.toLowerCase(Locale.ENGLISH), mimeType); + } + fireContainerEvent("addMimeMapping", extension); + + } + + + /** + * Add a new context initialization parameter. + * + * @param name Name of the new parameter + * @param value Value of the new parameter + * + * @exception IllegalArgumentException if the name or value is missing, or if this context initialization parameter + * has already been registered + */ + @Override + public void addParameter(String name, String value) { + // Validate the proposed context initialization parameter + if (name == null || value == null) { + throw new IllegalArgumentException(sm.getString("standardContext.parameter.required")); + } + + // Add this parameter to our defined set if not already present + String oldValue = parameters.putIfAbsent(name, value); + + if (oldValue != null) { + throw new IllegalArgumentException(sm.getString("standardContext.parameter.duplicate", name)); + } + + fireContainerEvent("addParameter", name); + } + + + /** + * Add a security role reference for this web application. + * + * @param role Security role used in the application + * @param link Actual security role to check for + */ + @Override + public void addRoleMapping(String role, String link) { + + synchronized (roleMappings) { + roleMappings.put(role, link); + } + fireContainerEvent("addRoleMapping", role); + + } + + + /** + * Add a new security role for this web application. + * + * @param role New security role + */ + @Override + public void addSecurityRole(String role) { + + synchronized (securityRolesLock) { + String[] results = Arrays.copyOf(securityRoles, securityRoles.length + 1); + results[securityRoles.length] = role; + securityRoles = results; + } + fireContainerEvent("addSecurityRole", role); + + } + + + /** + * Add a new servlet mapping, replacing any existing mapping for the specified pattern. + * + * @param pattern URL pattern to be mapped + * @param name Name of the corresponding servlet to execute + * @param jspWildCard true if name identifies the JspServlet and pattern contains a wildcard; false otherwise + * + * @exception IllegalArgumentException if the specified servlet name is not known to this Context + */ + @Override + public void addServletMappingDecoded(String pattern, String name, boolean jspWildCard) { + // Validate the proposed mapping + if (findChild(name) == null) { + throw new IllegalArgumentException(sm.getString("standardContext.servletMap.name", name)); + } + String adjustedPattern = adjustURLPattern(pattern); + if (!validateURLPattern(adjustedPattern)) { + throw new IllegalArgumentException(sm.getString("standardContext.servletMap.pattern", adjustedPattern)); + } + + // Add this mapping to our registered set + synchronized (servletMappingsLock) { + String name2 = servletMappings.get(adjustedPattern); + if (name2 != null) { + // Don't allow more than one servlet on the same pattern + Wrapper wrapper = (Wrapper) findChild(name2); + wrapper.removeMapping(adjustedPattern); + } + servletMappings.put(adjustedPattern, name); + } + Wrapper wrapper = (Wrapper) findChild(name); + wrapper.addMapping(adjustedPattern); + + fireContainerEvent("addServletMapping", adjustedPattern); + } + + + /** + * Add a new watched resource to the set recognized by this Context. + * + * @param name New watched resource file name + */ + @Override + public void addWatchedResource(String name) { + + synchronized (watchedResourcesLock) { + String[] results = Arrays.copyOf(watchedResources, watchedResources.length + 1); + results[watchedResources.length] = name; + watchedResources = results; + } + fireContainerEvent("addWatchedResource", name); + } + + + /** + * Add a new welcome file to the set recognized by this Context. + * + * @param name New welcome file name + */ + @Override + public void addWelcomeFile(String name) { + + synchronized (welcomeFilesLock) { + // Welcome files from the application deployment descriptor + // completely replace those from the default conf/web.xml file + if (replaceWelcomeFiles) { + fireContainerEvent(CLEAR_WELCOME_FILES_EVENT, null); + welcomeFiles = new String[0]; + setReplaceWelcomeFiles(false); + } + String[] results = Arrays.copyOf(welcomeFiles, welcomeFiles.length + 1); + results[welcomeFiles.length] = name; + welcomeFiles = results; + } + if (this.getState().equals(LifecycleState.STARTED)) { + fireContainerEvent(ADD_WELCOME_FILE_EVENT, name); + } + } + + + /** + * Add the classname of a LifecycleListener to be added to each Wrapper appended to this Context. + * + * @param listener Java class name of a LifecycleListener class + */ + @Override + public void addWrapperLifecycle(String listener) { + + synchronized (wrapperLifecyclesLock) { + String[] results = Arrays.copyOf(wrapperLifecycles, wrapperLifecycles.length + 1); + results[wrapperLifecycles.length] = listener; + wrapperLifecycles = results; + } + fireContainerEvent("addWrapperLifecycle", listener); + + } + + + /** + * Add the classname of a ContainerListener to be added to each Wrapper appended to this Context. + * + * @param listener Java class name of a ContainerListener class + */ + @Override + public void addWrapperListener(String listener) { + + synchronized (wrapperListenersLock) { + String[] results = Arrays.copyOf(wrapperListeners, wrapperListeners.length + 1); + results[wrapperListeners.length] = listener; + wrapperListeners = results; + } + fireContainerEvent("addWrapperListener", listener); + + } + + + /** + * Factory method to create and return a new Wrapper instance, of the Java implementation class appropriate for this + * Context implementation. The constructor of the instantiated Wrapper will have been called, but no properties will + * have been set. + */ + @Override + public Wrapper createWrapper() { + + Wrapper wrapper = null; + if (wrapperClass != null) { + try { + wrapper = (Wrapper) wrapperClass.getConstructor().newInstance(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardContext.createWrapper.error"), t); + return null; + } + } else { + wrapper = new StandardWrapper(); + } + + synchronized (wrapperLifecyclesLock) { + for (String wrapperLifecycle : wrapperLifecycles) { + try { + Class clazz = Class.forName(wrapperLifecycle); + LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); + wrapper.addLifecycleListener(listener); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardContext.createWrapper.listenerError"), t); + return null; + } + } + } + + synchronized (wrapperListenersLock) { + for (String wrapperListener : wrapperListeners) { + try { + Class clazz = Class.forName(wrapperListener); + ContainerListener listener = (ContainerListener) clazz.getConstructor().newInstance(); + wrapper.addContainerListener(listener); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardContext.createWrapper.containerListenerError"), t); + return null; + } + } + } + + return wrapper; + } + + + /** + * Return the set of application listener class names configured for this application. + */ + @Override + public String[] findApplicationListeners() { + return applicationListeners.toArray(new String[0]); + } + + + /** + * Return the set of application parameters for this application. + */ + @Override + public ApplicationParameter[] findApplicationParameters() { + + synchronized (applicationParametersLock) { + return applicationParameters; + } + + } + + + /** + * Return the security constraints for this web application. If there are none, a zero-length array is returned. + */ + @Override + public SecurityConstraint[] findConstraints() { + return constraints; + } + + + /** + * Return the error page entry for the specified HTTP error code, if any; otherwise return null. + * + * @param errorCode Error code to look up + */ + @Override + public ErrorPage findErrorPage(int errorCode) { + return errorPageSupport.find(errorCode); + } + + + @Override + public ErrorPage findErrorPage(Throwable exceptionType) { + return errorPageSupport.find(exceptionType); + } + + + /** + * Return the set of defined error pages for all specified error codes and exception types. + */ + @Override + public ErrorPage[] findErrorPages() { + return errorPageSupport.findAll(); + } + + + /** + * Return the filter definition for the specified filter name, if any; otherwise return null. + * + * @param filterName Filter name to look up + */ + @Override + public FilterDef findFilterDef(String filterName) { + synchronized (filterDefs) { + return filterDefs.get(filterName); + } + } + + + /** + * @return the set of defined filters for this Context. + */ + @Override + public FilterDef[] findFilterDefs() { + synchronized (filterDefs) { + return filterDefs.values().toArray(new FilterDef[0]); + } + } + + + /** + * @return the set of filter mappings for this Context. + */ + @Override + public FilterMap[] findFilterMaps() { + return filterMaps.asArray(); + } + + + /** + * @return the message destination with the specified name, if any; otherwise, return null. + * + * @param name Name of the desired message destination + */ + public MessageDestination findMessageDestination(String name) { + synchronized (messageDestinations) { + return messageDestinations.get(name); + } + } + + + /** + * @return the set of defined message destinations for this web application. If none have been defined, a + * zero-length array is returned. + */ + public MessageDestination[] findMessageDestinations() { + synchronized (messageDestinations) { + return messageDestinations.values().toArray(new MessageDestination[0]); + } + } + + + /** + * @return the MIME type to which the specified extension is mapped, if any; otherwise return null. + * + * @param extension Extension to map to a MIME type + */ + @Override + public String findMimeMapping(String extension) { + return mimeMappings.get(extension.toLowerCase(Locale.ENGLISH)); + } + + + /** + * @return the extensions for which MIME mappings are defined. If there are none, a zero-length array is returned. + */ + @Override + public String[] findMimeMappings() { + synchronized (mimeMappings) { + return mimeMappings.keySet().toArray(new String[0]); + } + } + + + /** + * @return the value for the specified context initialization parameter name, if any; otherwise return + * null. + * + * @param name Name of the parameter to return + */ + @Override + public String findParameter(String name) { + return parameters.get(name); + } + + + /** + * @return the names of all defined context initialization parameters for this Context. If no parameters are + * defined, a zero-length array is returned. + */ + @Override + public String[] findParameters() { + return parameters.keySet().toArray(new String[0]); + } + + + /** + * For the given security role (as used by an application), return the corresponding role name (as defined by the + * underlying Realm) if there is one. Otherwise, return the specified role unchanged. + * + * @param role Security role to map + * + * @return the role name + */ + @Override + public String findRoleMapping(String role) { + String realRole = null; + synchronized (roleMappings) { + realRole = roleMappings.get(role); + } + if (realRole != null) { + return realRole; + } else { + return role; + } + } + + + /** + * @return true if the specified security role is defined for this application; otherwise return + * false. + * + * @param role Security role to verify + */ + @Override + public boolean findSecurityRole(String role) { + + synchronized (securityRolesLock) { + for (String securityRole : securityRoles) { + if (role.equals(securityRole)) { + return true; + } + } + } + return false; + + } + + + /** + * @return the security roles defined for this application. If none have been defined, a zero-length array is + * returned. + */ + @Override + public String[] findSecurityRoles() { + synchronized (securityRolesLock) { + return securityRoles; + } + } + + + /** + * @return the servlet name mapped by the specified pattern (if any); otherwise return null. + * + * @param pattern Pattern for which a mapping is requested + */ + @Override + public String findServletMapping(String pattern) { + synchronized (servletMappingsLock) { + return servletMappings.get(pattern); + } + } + + + /** + * @return the patterns of all defined servlet mappings for this Context. If no mappings are defined, a zero-length + * array is returned. + */ + @Override + public String[] findServletMappings() { + synchronized (servletMappingsLock) { + return servletMappings.keySet().toArray(new String[0]); + } + } + + + /** + * @return true if the specified welcome file is defined for this Context; otherwise return + * false. + * + * @param name Welcome file to verify + */ + @Override + public boolean findWelcomeFile(String name) { + + synchronized (welcomeFilesLock) { + for (String welcomeFile : welcomeFiles) { + if (name.equals(welcomeFile)) { + return true; + } + } + } + return false; + + } + + + /** + * @return the set of watched resources for this Context. If none are defined, a zero length array will be returned. + */ + @Override + public String[] findWatchedResources() { + synchronized (watchedResourcesLock) { + return watchedResources; + } + } + + + /** + * @return the set of welcome files defined for this Context. If none are defined, a zero-length array is returned. + */ + @Override + public String[] findWelcomeFiles() { + synchronized (welcomeFilesLock) { + return welcomeFiles; + } + } + + + /** + * @return the set of LifecycleListener classes that will be added to newly created Wrappers automatically. + */ + @Override + public String[] findWrapperLifecycles() { + synchronized (wrapperLifecyclesLock) { + return wrapperLifecycles; + } + } + + + /** + * @return the set of ContainerListener classes that will be added to newly created Wrappers automatically. + */ + @Override + public String[] findWrapperListeners() { + synchronized (wrapperListenersLock) { + return wrapperListeners; + } + } + + + /** + * Reload this web application, if reloading is supported. + *

    + * IMPLEMENTATION NOTE: This method is designed to deal with reloads required by changes to classes in the + * underlying repositories of our class loader and changes to the web.xml file. It does not handle changes to any + * context.xml file. If the context.xml has changed, you should stop this Context and create (and start) a new + * Context instance instead. Note that there is additional code in CoyoteAdapter#postParseRequest() to + * handle mapping requests to paused Contexts. + * + * @exception IllegalStateException if the reloadable property is set to false. + */ + @Override + public synchronized void reload() { + + // Validate our current component state + if (!getState().isAvailable()) { + throw new IllegalStateException(sm.getString("standardContext.notStarted", getName())); + } + + if (log.isInfoEnabled()) { + log.info(sm.getString("standardContext.reloadingStarted", getName())); + } + + // Stop accepting requests temporarily. + setPaused(true); + + try { + stop(); + } catch (LifecycleException e) { + log.error(sm.getString("standardContext.stoppingContext", getName()), e); + } + + try { + start(); + } catch (LifecycleException e) { + log.error(sm.getString("standardContext.startingContext", getName()), e); + } + + setPaused(false); + + if (log.isInfoEnabled()) { + log.info(sm.getString("standardContext.reloadingCompleted", getName())); + } + + } + + + /** + * Remove the specified application listener class from the set of listeners for this application. + * + * @param listener Java class name of the listener to be removed + */ + @Override + public void removeApplicationListener(String listener) { + if (applicationListeners.remove(listener)) { + // Inform interested listeners if the specified listener was present and has been removed + fireContainerEvent("removeApplicationListener", listener); + } + } + + + /** + * Remove the application parameter with the specified name from the set for this application. + * + * @param name Name of the application parameter to remove + */ + @Override + public void removeApplicationParameter(String name) { + + synchronized (applicationParametersLock) { + + // Make sure this parameter is currently present + int n = -1; + for (int i = 0; i < applicationParameters.length; i++) { + if (name.equals(applicationParameters[i].getName())) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified parameter + int j = 0; + ApplicationParameter results[] = new ApplicationParameter[applicationParameters.length - 1]; + for (int i = 0; i < applicationParameters.length; i++) { + if (i != n) { + results[j++] = applicationParameters[i]; + } + } + applicationParameters = results; + + } + + // Inform interested listeners + fireContainerEvent("removeApplicationParameter", name); + + } + + + /** + * Add a child Container, only if the proposed child is an implementation of Wrapper. + * + * @param child Child container to be added + * + * @exception IllegalArgumentException if the proposed container is not an implementation of Wrapper + */ + @Override + public void removeChild(Container child) { + + if (!(child instanceof Wrapper)) { + throw new IllegalArgumentException(sm.getString("standardContext.notWrapper")); + } + + super.removeChild(child); + + } + + + /** + * Remove the specified security constraint from this web application. + * + * @param constraint Constraint to be removed + */ + @Override + public void removeConstraint(SecurityConstraint constraint) { + + synchronized (constraintsLock) { + + // Make sure this constraint is currently present + int n = -1; + for (int i = 0; i < constraints.length; i++) { + if (constraints[i].equals(constraint)) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified constraint + int j = 0; + SecurityConstraint results[] = new SecurityConstraint[constraints.length - 1]; + for (int i = 0; i < constraints.length; i++) { + if (i != n) { + results[j++] = constraints[i]; + } + } + constraints = results; + + } + + // Inform interested listeners + fireContainerEvent("removeConstraint", constraint); + + } + + + /** + * Remove the error page for the specified error code or Java language exception, if it exists; otherwise, no action + * is taken. + * + * @param errorPage The error page definition to be removed + */ + @Override + public void removeErrorPage(ErrorPage errorPage) { + errorPageSupport.remove(errorPage); + fireContainerEvent("removeErrorPage", errorPage); + } + + + /** + * Remove the specified filter definition from this Context, if it exists; otherwise, no action is taken. + * + * @param filterDef Filter definition to be removed + */ + @Override + public void removeFilterDef(FilterDef filterDef) { + + synchronized (filterDefs) { + filterDefs.remove(filterDef.getFilterName()); + } + fireContainerEvent("removeFilterDef", filterDef); + + } + + + /** + * Remove a filter mapping from this Context. + * + * @param filterMap The filter mapping to be removed + */ + @Override + public void removeFilterMap(FilterMap filterMap) { + filterMaps.remove(filterMap); + // Inform interested listeners + fireContainerEvent("removeFilterMap", filterMap); + } + + + /** + * Remove any message destination with the specified name. + * + * @param name Name of the message destination to remove + */ + public void removeMessageDestination(String name) { + + synchronized (messageDestinations) { + messageDestinations.remove(name); + } + fireContainerEvent("removeMessageDestination", name); + + } + + + /** + * Remove the MIME mapping for the specified extension, if it exists; otherwise, no action is taken. + * + * @param extension Extension to remove the mapping for + */ + @Override + public void removeMimeMapping(String extension) { + + synchronized (mimeMappings) { + mimeMappings.remove(extension); + } + fireContainerEvent("removeMimeMapping", extension); + + } + + + /** + * Remove the context initialization parameter with the specified name, if it exists; otherwise, no action is taken. + * + * @param name Name of the parameter to remove + */ + @Override + public void removeParameter(String name) { + parameters.remove(name); + fireContainerEvent("removeParameter", name); + } + + + /** + * Remove any security role reference for the specified name + * + * @param role Security role (as used in the application) to remove + */ + @Override + public void removeRoleMapping(String role) { + + synchronized (roleMappings) { + roleMappings.remove(role); + } + fireContainerEvent("removeRoleMapping", role); + + } + + + /** + * Remove any security role with the specified name. + * + * @param role Security role to remove + */ + @Override + public void removeSecurityRole(String role) { + + synchronized (securityRolesLock) { + + // Make sure this security role is currently present + int n = -1; + for (int i = 0; i < securityRoles.length; i++) { + if (role.equals(securityRoles[i])) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified security role + int j = 0; + String results[] = new String[securityRoles.length - 1]; + for (int i = 0; i < securityRoles.length; i++) { + if (i != n) { + results[j++] = securityRoles[i]; + } + } + securityRoles = results; + + } + + // Inform interested listeners + fireContainerEvent("removeSecurityRole", role); + + } + + + /** + * Remove any servlet mapping for the specified pattern, if it exists; otherwise, no action is taken. + * + * @param pattern URL pattern of the mapping to remove + */ + @Override + public void removeServletMapping(String pattern) { + + String name = null; + synchronized (servletMappingsLock) { + name = servletMappings.remove(pattern); + } + Wrapper wrapper = (Wrapper) findChild(name); + if (wrapper != null) { + wrapper.removeMapping(pattern); + } + fireContainerEvent("removeServletMapping", pattern); + } + + + /** + * Remove the specified watched resource name from the list associated with this Context. + * + * @param name Name of the watched resource to be removed + */ + @Override + public void removeWatchedResource(String name) { + + synchronized (watchedResourcesLock) { + + // Make sure this watched resource is currently present + int n = -1; + for (int i = 0; i < watchedResources.length; i++) { + if (watchedResources[i].equals(name)) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified watched resource + int j = 0; + String results[] = new String[watchedResources.length - 1]; + for (int i = 0; i < watchedResources.length; i++) { + if (i != n) { + results[j++] = watchedResources[i]; + } + } + watchedResources = results; + + } + + fireContainerEvent("removeWatchedResource", name); + + } + + + /** + * Remove the specified welcome file name from the list recognized by this Context. + * + * @param name Name of the welcome file to be removed + */ + @Override + public void removeWelcomeFile(String name) { + + synchronized (welcomeFilesLock) { + + // Make sure this welcome file is currently present + int n = -1; + for (int i = 0; i < welcomeFiles.length; i++) { + if (welcomeFiles[i].equals(name)) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified welcome file + int j = 0; + String results[] = new String[welcomeFiles.length - 1]; + for (int i = 0; i < welcomeFiles.length; i++) { + if (i != n) { + results[j++] = welcomeFiles[i]; + } + } + welcomeFiles = results; + + } + + // Inform interested listeners + if (this.getState().equals(LifecycleState.STARTED)) { + fireContainerEvent(REMOVE_WELCOME_FILE_EVENT, name); + } + + } + + + /** + * Remove a class name from the set of LifecycleListener classes that will be added to newly created Wrappers. + * + * @param listener Class name of a LifecycleListener class to be removed + */ + @Override + public void removeWrapperLifecycle(String listener) { + + + synchronized (wrapperLifecyclesLock) { + + // Make sure this lifecycle listener is currently present + int n = -1; + for (int i = 0; i < wrapperLifecycles.length; i++) { + if (wrapperLifecycles[i].equals(listener)) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified lifecycle listener + int j = 0; + String results[] = new String[wrapperLifecycles.length - 1]; + for (int i = 0; i < wrapperLifecycles.length; i++) { + if (i != n) { + results[j++] = wrapperLifecycles[i]; + } + } + wrapperLifecycles = results; + + } + + // Inform interested listeners + fireContainerEvent("removeWrapperLifecycle", listener); + + } + + + /** + * Remove a class name from the set of ContainerListener classes that will be added to newly created Wrappers. + * + * @param listener Class name of a ContainerListener class to be removed + */ + @Override + public void removeWrapperListener(String listener) { + + + synchronized (wrapperListenersLock) { + + // Make sure this listener is currently present + int n = -1; + for (int i = 0; i < wrapperListeners.length; i++) { + if (wrapperListeners[i].equals(listener)) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified listener + int j = 0; + String results[] = new String[wrapperListeners.length - 1]; + for (int i = 0; i < wrapperListeners.length; i++) { + if (i != n) { + results[j++] = wrapperListeners[i]; + } + } + wrapperListeners = results; + + } + + // Inform interested listeners + fireContainerEvent("removeWrapperListener", listener); + + } + + + /** + * Gets the cumulative processing times of all servlets in this StandardContext. + * + * @return Cumulative processing times of all servlets in this StandardContext + */ + public long getProcessingTime() { + + long result = 0; + + Container[] children = findChildren(); + if (children != null) { + for (Container child : children) { + result += ((StandardWrapper) child).getProcessingTime(); + } + } + + return result; + } + + /** + * Gets the maximum processing time of all servlets in this StandardContext. + * + * @return Maximum processing time of all servlets in this StandardContext + */ + public long getMaxTime() { + + long result = 0; + long time; + + Container[] children = findChildren(); + if (children != null) { + for (Container child : children) { + time = ((StandardWrapper) child).getMaxTime(); + if (time > result) { + result = time; + } + } + } + + return result; + } + + /** + * Gets the minimum processing time of all servlets in this StandardContext. + * + * @return Minimum processing time of all servlets in this StandardContext + */ + public long getMinTime() { + + long result = -1; + long time; + + Container[] children = findChildren(); + if (children != null) { + for (Container child : children) { + time = ((StandardWrapper) child).getMinTime(); + if (result < 0 || time < result) { + result = time; + } + } + } + + return result; + } + + /** + * Gets the cumulative request count of all servlets in this StandardContext. + * + * @return Cumulative request count of all servlets in this StandardContext + * + * @deprecated The return type will change to long in Tomcat 11 onwards. Callers of this method should switch to + * storing the result of calls to this method in a long value rather than an int. + */ + @Deprecated + public int getRequestCount() { + + int result = 0; + + Container[] children = findChildren(); + if (children != null) { + for (Container child : children) { + result += ((StandardWrapper) child).getRequestCount(); + } + } + + return result; + } + + /** + * Gets the cumulative error count of all servlets in this StandardContext. + * + * @return Cumulative error count of all servlets in this StandardContext + * + * @deprecated The return type will change to long in Tomcat 11 onwards. Callers of this method should switch to + * storing the result of calls to this method in a long value rather than an int. + */ + @Deprecated + public int getErrorCount() { + + int result = 0; + + Container[] children = findChildren(); + if (children != null) { + for (Container child : children) { + result += ((StandardWrapper) child).getErrorCount(); + } + } + + return result; + } + + + /** + * Return the real path for a given virtual path, if possible; otherwise return null. + * + * @param path The path to the desired resource + */ + @Override + public String getRealPath(String path) { + // The WebResources API expects all paths to start with /. This is a + // special case for consistency with earlier Tomcat versions. + if ("".equals(path)) { + path = "/"; + } + if (resources != null) { + try { + WebResource resource = resources.getResource(path); + String canonicalPath = resource.getCanonicalPath(); + if (canonicalPath == null) { + return null; + } else if ((resource.isDirectory() && !canonicalPath.endsWith(File.separator) || !resource.exists()) && + path.endsWith("/")) { + return canonicalPath + File.separatorChar; + } else { + return canonicalPath; + } + } catch (IllegalArgumentException iae) { + // ServletContext.getRealPath() does not allow this to be thrown + } + } + return null; + } + + + /** + * Hook to track which Servlets were created via {@link ServletContext#createServlet(Class)}. + * + * @param servlet the created Servlet + */ + public void dynamicServletCreated(Servlet servlet) { + createdServlets.add(servlet); + } + + + public boolean wasCreatedDynamicServlet(Servlet servlet) { + return createdServlets.contains(servlet); + } + + + /** + * A helper class to manage the filter mappings in a Context. + */ + private static final class ContextFilterMaps { + private final Object lock = new Object(); + + /** + * The set of filter mappings for this application, in the order they were defined in the deployment descriptor + * with additional mappings added via the {@link ServletContext} possibly both before and after those defined in + * the deployment descriptor. + */ + private FilterMap[] array = new FilterMap[0]; + + /** + * Filter mappings added via {@link ServletContext} may have to be inserted before the mappings in the + * deployment descriptor but must be inserted in the order the {@link ServletContext} methods are called. This + * isn't an issue for the mappings added after the deployment descriptor - they are just added to the end - but + * correctly the adding mappings before the deployment descriptor mappings requires knowing where the last + * 'before' mapping was added. + */ + private int insertPoint = 0; + + /** + * @return The set of filter mappings + */ + public FilterMap[] asArray() { + synchronized (lock) { + return array; + } + } + + /** + * Add a filter mapping at the end of the current set of filter mappings. + * + * @param filterMap The filter mapping to be added + */ + public void add(FilterMap filterMap) { + synchronized (lock) { + FilterMap results[] = Arrays.copyOf(array, array.length + 1); + results[array.length] = filterMap; + array = results; + } + } + + /** + * Add a filter mapping before the mappings defined in the deployment descriptor but after any other mappings + * added via this method. + * + * @param filterMap The filter mapping to be added + */ + public void addBefore(FilterMap filterMap) { + synchronized (lock) { + FilterMap results[] = new FilterMap[array.length + 1]; + System.arraycopy(array, 0, results, 0, insertPoint); + System.arraycopy(array, insertPoint, results, insertPoint + 1, array.length - insertPoint); + results[insertPoint] = filterMap; + array = results; + insertPoint++; + } + } + + /** + * Remove a filter mapping. + * + * @param filterMap The filter mapping to be removed + */ + public void remove(FilterMap filterMap) { + synchronized (lock) { + // Make sure this filter mapping is currently present + int n = -1; + for (int i = 0; i < array.length; i++) { + if (array[i] == filterMap) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified filter mapping + FilterMap results[] = new FilterMap[array.length - 1]; + System.arraycopy(array, 0, results, 0, n); + System.arraycopy(array, n + 1, results, n, (array.length - 1) - n); + array = results; + if (n < insertPoint) { + insertPoint--; + } + } + } + } + + // --------------------------------------------------------- Public Methods + + + /** + * Configure and initialize the set of filters for this Context. + * + * @return true if all filter initialization completed successfully, or false otherwise. + */ + public boolean filterStart() { + + if (getLogger().isTraceEnabled()) { + getLogger().trace("Starting filters"); + } + // Instantiate and record a FilterConfig for each defined filter + boolean ok = true; + synchronized (filterDefs) { + filterConfigs.clear(); + for (Entry entry : filterDefs.entrySet()) { + String name = entry.getKey(); + if (getLogger().isTraceEnabled()) { + getLogger().trace(" Starting filter '" + name + "'"); + } + try { + ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, entry.getValue()); + filterConfigs.put(name, filterConfig); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + getLogger().error(sm.getString("standardContext.filterStart", name), t); + ok = false; + } + } + } + + return ok; + } + + + /** + * Finalize and release the set of filters for this Context. + * + * @return true if all filter finalization completed successfully, or false otherwise. + */ + public boolean filterStop() { + + if (getLogger().isTraceEnabled()) { + getLogger().trace("Stopping filters"); + } + + // Release all Filter and FilterConfig instances + synchronized (filterDefs) { + for (Entry entry : filterConfigs.entrySet()) { + if (getLogger().isTraceEnabled()) { + getLogger().trace(" Stopping filter '" + entry.getKey() + "'"); + } + ApplicationFilterConfig filterConfig = entry.getValue(); + filterConfig.release(); + } + filterConfigs.clear(); + } + return true; + + } + + + /** + * Find and return the initialized FilterConfig for the specified filter name, if any; otherwise return + * null. + * + * @param name Name of the desired filter + * + * @return the filter config object + */ + public FilterConfig findFilterConfig(String name) { + synchronized (filterDefs) { + return filterConfigs.get(name); + } + } + + + /** + * Configure the set of instantiated application event listeners for this Context. + * + * @return true if all listeners wre initialized successfully, or false otherwise. + */ + public boolean listenerStart() { + + if (log.isTraceEnabled()) { + log.trace("Configuring application event listeners"); + } + + // Instantiate the required listeners + String listeners[] = findApplicationListeners(); + Object results[] = new Object[listeners.length]; + boolean ok = true; + for (int i = 0; i < results.length; i++) { + if (getLogger().isTraceEnabled()) { + getLogger().trace(" Configuring event listener class '" + listeners[i] + "'"); + } + try { + String listener = listeners[i]; + results[i] = getInstanceManager().newInstance(listener); + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + getLogger().error(sm.getString("standardContext.applicationListener", listeners[i]), t); + ok = false; + } + } + if (!ok) { + getLogger().error(sm.getString("standardContext.applicationSkipped")); + return false; + } + + // Sort listeners in two arrays + List eventListeners = new ArrayList<>(); + List lifecycleListeners = new ArrayList<>(); + for (Object result : results) { + if ((result instanceof ServletContextAttributeListener) || + (result instanceof ServletRequestAttributeListener) || (result instanceof ServletRequestListener) || + (result instanceof HttpSessionIdListener) || (result instanceof HttpSessionAttributeListener)) { + eventListeners.add(result); + } + if ((result instanceof ServletContextListener) || (result instanceof HttpSessionListener)) { + lifecycleListeners.add(result); + } + } + + // Listener instances may have been added directly to this Context by + // ServletContextInitializers and other code via the pluggability APIs. + // Put them these listeners after the ones defined in web.xml and/or + // annotations then overwrite the list of instances with the new, full + // list. + eventListeners.addAll(Arrays.asList(getApplicationEventListeners())); + setApplicationEventListeners(eventListeners.toArray()); + for (Object lifecycleListener : getApplicationLifecycleListeners()) { + lifecycleListeners.add(lifecycleListener); + if (lifecycleListener instanceof ServletContextListener) { + noPluggabilityListeners.add(lifecycleListener); + } + } + setApplicationLifecycleListeners(lifecycleListeners.toArray()); + + // Send application start events + + if (getLogger().isTraceEnabled()) { + getLogger().trace("Sending application start events"); + } + + // Ensure context is not null + getServletContext(); + context.setNewServletContextListenerAllowed(false); + + Object instances[] = getApplicationLifecycleListeners(); + if (instances == null || instances.length == 0) { + return ok; + } + + ServletContextEvent event = new ServletContextEvent(getServletContext()); + ServletContextEvent tldEvent = null; + if (noPluggabilityListeners.size() > 0) { + noPluggabilityServletContext = new NoPluggabilityServletContext(getServletContext()); + tldEvent = new ServletContextEvent(noPluggabilityServletContext); + } + for (Object instance : instances) { + if (!(instance instanceof ServletContextListener)) { + continue; + } + ServletContextListener listener = (ServletContextListener) instance; + try { + fireContainerEvent("beforeContextInitialized", listener); + if (noPluggabilityListeners.contains(listener)) { + listener.contextInitialized(tldEvent); + } else { + listener.contextInitialized(event); + } + fireContainerEvent("afterContextInitialized", listener); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + fireContainerEvent("afterContextInitialized", listener); + getLogger().error(sm.getString("standardContext.listenerStart", instance.getClass().getName()), t); + ok = false; + } + } + return ok; + + } + + + /** + * Send an application stop event to all interested listeners. + * + * @return true if all events were sent successfully, or false otherwise. + */ + public boolean listenerStop() { + + if (log.isTraceEnabled()) { + log.trace("Sending application stop events"); + } + + boolean ok = true; + Object listeners[] = getApplicationLifecycleListeners(); + if (listeners != null && listeners.length > 0) { + ServletContextEvent event = new ServletContextEvent(getServletContext()); + ServletContextEvent tldEvent = null; + if (noPluggabilityServletContext != null) { + tldEvent = new ServletContextEvent(noPluggabilityServletContext); + } + for (int i = 0; i < listeners.length; i++) { + int j = (listeners.length - 1) - i; + if (listeners[j] == null) { + continue; + } + if (listeners[j] instanceof ServletContextListener) { + ServletContextListener listener = (ServletContextListener) listeners[j]; + try { + fireContainerEvent("beforeContextDestroyed", listener); + if (noPluggabilityListeners.contains(listener)) { + listener.contextDestroyed(tldEvent); + } else { + listener.contextDestroyed(event); + } + fireContainerEvent("afterContextDestroyed", listener); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + fireContainerEvent("afterContextDestroyed", listener); + getLogger().error( + sm.getString("standardContext.listenerStop", listeners[j].getClass().getName()), t); + ok = false; + } + } + try { + if (getInstanceManager() != null) { + getInstanceManager().destroyInstance(listeners[j]); + } + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + getLogger().error(sm.getString("standardContext.listenerStop", listeners[j].getClass().getName()), + t); + ok = false; + } + } + } + + // Annotation processing + listeners = getApplicationEventListeners(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + int j = (listeners.length - 1) - i; + if (listeners[j] == null) { + continue; + } + try { + if (getInstanceManager() != null) { + getInstanceManager().destroyInstance(listeners[j]); + } + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + getLogger().error(sm.getString("standardContext.listenerStop", listeners[j].getClass().getName()), + t); + ok = false; + } + } + } + + setApplicationEventListeners(null); + setApplicationLifecycleListeners(null); + + noPluggabilityServletContext = null; + noPluggabilityListeners.clear(); + + return ok; + } + + + /** + * Allocate resources, including proxy. + * + * @throws LifecycleException if a start error occurs + */ + public void resourcesStart() throws LifecycleException { + + // Check current status in case resources were added that had already + // been started + if (!resources.getState().isAvailable()) { + resources.start(); + } + + if (effectiveMajorVersion >= 3 && addWebinfClassesResources) { + WebResource webinfClassesResource = resources.getResource("/WEB-INF/classes/META-INF/resources"); + if (webinfClassesResource.isDirectory()) { + getResources().createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", + webinfClassesResource.getURL(), "/"); + } + } + } + + + /** + * Deallocate resources and destroy proxy. + * + * @return true if no error occurred + */ + public boolean resourcesStop() { + + boolean ok = true; + + try { + if (resources != null) { + resources.stop(); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardContext.resourcesStop"), t); + ok = false; + } + + return ok; + } + + + /** + * Load and initialize all servlets marked "load on startup" in the web application deployment descriptor. + * + * @param children Array of wrappers for all currently defined servlets (including those not declared load on + * startup) + * + * @return true if load on startup was considered successful + */ + public boolean loadOnStartup(Container children[]) { + + // Collect "load on startup" servlets that need to be initialized + TreeMap> map = new TreeMap<>(); + for (Container child : children) { + Wrapper wrapper = (Wrapper) child; + int loadOnStartup = wrapper.getLoadOnStartup(); + if (loadOnStartup < 0) { + continue; + } + Integer key = Integer.valueOf(loadOnStartup); + map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper); + } + + // Load the collected "load on startup" servlets + for (ArrayList list : map.values()) { + for (Wrapper wrapper : list) { + try { + wrapper.load(); + } catch (ServletException e) { + getLogger().error( + sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName()), + StandardWrapper.getRootCause(e)); + // NOTE: load errors (including a servlet that throws + // UnavailableException from the init() method) are NOT + // fatal to application startup + // unless failCtxIfServletStartFails="true" is specified + if (getComputedFailCtxIfServletStartFails()) { + return false; + } + } + } + } + return true; + + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + if (log.isTraceEnabled()) { + log.trace("Starting " + getBaseName()); + } + + // Send j2ee.state.starting notification + if (this.getObjectName() != null) { + Notification notification = + new Notification("j2ee.state.starting", this.getObjectName(), sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + + setConfigured(false); + boolean ok = true; + + // Currently this is effectively a NO-OP but needs to be called to + // ensure the NamingResources follows the correct lifecycle + if (namingResources != null) { + namingResources.start(); + } + + // Post work directory + postWorkDirectory(); + + // Add missing components as necessary + if (getResources() == null) { // (1) Required by Loader + if (log.isTraceEnabled()) { + log.trace("Configuring default Resources"); + } + + try { + setResources(new StandardRoot(this)); + } catch (IllegalArgumentException e) { + log.error(sm.getString("standardContext.resourcesInit"), e); + ok = false; + } + } + if (ok) { + resourcesStart(); + } + + if (getLoader() == null) { + WebappLoader webappLoader = new WebappLoader(); + webappLoader.setDelegate(getDelegate()); + setLoader(webappLoader); + } + + // An explicit cookie processor hasn't been specified; use the default + if (cookieProcessor == null) { + cookieProcessor = new Rfc6265CookieProcessor(); + } + + // Initialize character set mapper + getCharsetMapper(); + + // Reading the "catalina.useNaming" environment variable + String useNamingProperty = System.getProperty("catalina.useNaming"); + if ((useNamingProperty != null) && (useNamingProperty.equals("false"))) { + useNaming = false; + } + + if (ok && isUseNaming()) { + if (getNamingContextListener() == null) { + NamingContextListener ncl = new NamingContextListener(); + ncl.setName(getNamingContextName()); + ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite()); + addLifecycleListener(ncl); + setNamingContextListener(ncl); + } + } + + // Standard container startup + if (log.isTraceEnabled()) { + log.trace("Processing standard container startup"); + } + + + // Binding thread + ClassLoader oldCCL = bindThread(); + + try { + if (ok) { + // Start our subordinate components, if any + Loader loader = getLoader(); + if (loader instanceof Lifecycle) { + ((Lifecycle) loader).start(); + } + + // since the loader just started, the webapp classloader is now + // created. + if (loader.getClassLoader() instanceof WebappClassLoaderBase) { + WebappClassLoaderBase cl = (WebappClassLoaderBase) loader.getClassLoader(); + cl.setClearReferencesRmiTargets(getClearReferencesRmiTargets()); + cl.setClearReferencesStopThreads(getClearReferencesStopThreads()); + cl.setClearReferencesStopTimerThreads(getClearReferencesStopTimerThreads()); + cl.setClearReferencesHttpClientKeepAliveThread(getClearReferencesHttpClientKeepAliveThread()); + cl.setClearReferencesObjectStreamClassCaches(getClearReferencesObjectStreamClassCaches()); + cl.setClearReferencesThreadLocals(getClearReferencesThreadLocals()); + cl.setSkipMemoryLeakChecksOnJvmShutdown(getSkipMemoryLeakChecksOnJvmShutdown()); + } + + // By calling unbindThread and bindThread in a row, we setup the + // current Thread CCL to be the webapp classloader + unbindThread(oldCCL); + oldCCL = bindThread(); + + // Initialize logger again. Other components might have used it + // too early, so it should be reset. + logger = null; + getLogger(); + + Realm realm = getRealmInternal(); + if (null != realm) { + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).start(); + } + + // Place the CredentialHandler into the ServletContext so + // applications can have access to it. Wrap it in a "safe" + // handler so application's can't modify it. + CredentialHandler safeHandler = new CredentialHandler() { + @Override + public boolean matches(String inputCredentials, String storedCredentials) { + return getRealmInternal().getCredentialHandler().matches(inputCredentials, + storedCredentials); + } + + @Override + public String mutate(String inputCredentials) { + return getRealmInternal().getCredentialHandler().mutate(inputCredentials); + } + }; + context.setAttribute(Globals.CREDENTIAL_HANDLER, safeHandler); + } + + // Notify our interested LifecycleListeners + fireLifecycleEvent(CONFIGURE_START_EVENT, null); + + // Start our child containers, if not already started + for (Container child : findChildren()) { + if (!child.getState().isAvailable()) { + child.start(); + } + } + + // Start the Valves in our pipeline (including the basic), + // if any + if (pipeline instanceof Lifecycle) { + ((Lifecycle) pipeline).start(); + } + + // Acquire clustered manager + Manager contextManager = null; + Manager manager = getManager(); + if (manager == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardContext.cluster.noManager", + Boolean.valueOf((getCluster() != null)), Boolean.valueOf(distributable))); + } + if ((getCluster() != null) && distributable) { + try { + contextManager = getCluster().createManager(getName()); + } catch (Exception ex) { + log.error(sm.getString("standardContext.cluster.managerError"), ex); + ok = false; + } + } else { + contextManager = new StandardManager(); + } + } + + // Configure default manager if none was specified + if (contextManager != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardContext.manager", contextManager.getClass().getName())); + } + setManager(contextManager); + } + + if (manager != null && (getCluster() != null) && distributable) { + // let the cluster know that there is a context that is distributable + // and that it has its own manager + getCluster().registerManager(manager); + } + } + + if (!getConfigured()) { + log.error(sm.getString("standardContext.configurationFail")); + ok = false; + } + + // We put the resources into the servlet context + if (ok) { + getServletContext().setAttribute(Globals.RESOURCES_ATTR, getResources()); + + if (getInstanceManager() == null) { + setInstanceManager(createInstanceManager()); + } + getServletContext().setAttribute(InstanceManager.class.getName(), getInstanceManager()); + InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager()); + + // Create context attributes that will be required + getServletContext().setAttribute(JarScanner.class.getName(), getJarScanner()); + + // Make the version info available + getServletContext().setAttribute(Globals.WEBAPP_VERSION, getWebappVersion()); + + // Make the utility executor available + if (!Globals.IS_SECURITY_ENABLED) { + getServletContext().setAttribute(ScheduledThreadPoolExecutor.class.getName(), + Container.getService(this).getServer().getUtilityExecutor()); + } + } + + // Set up the context init params + mergeParameters(); + + // Call ServletContainerInitializers + for (Map.Entry>> entry : initializers.entrySet()) { + try { + entry.getKey().onStartup(entry.getValue(), getServletContext()); + } catch (ServletException e) { + log.error(sm.getString("standardContext.sciFail"), e); + ok = false; + break; + } + } + + // Configure and call application event listeners + if (ok) { + if (!listenerStart()) { + log.error(sm.getString("standardContext.listenerFail")); + ok = false; + } + } + + // Check constraints for uncovered HTTP methods + // Needs to be after SCIs and listeners as they may programmatically + // change constraints + if (ok) { + checkConstraintsForUncoveredMethods(findConstraints()); + } + + try { + // Start manager + Manager manager = getManager(); + if (manager instanceof Lifecycle) { + ((Lifecycle) manager).start(); + } + } catch (Exception e) { + log.error(sm.getString("standardContext.managerFail"), e); + ok = false; + } + + // Configure and call application filters + if (ok) { + if (!filterStart()) { + log.error(sm.getString("standardContext.filterFail")); + ok = false; + } + } + + // Load and initialize all "load on startup" servlets + if (ok) { + if (!loadOnStartup(findChildren())) { + log.error(sm.getString("standardContext.servletFail")); + ok = false; + } + } + + // Start ContainerBackgroundProcessor thread + super.threadStart(); + } finally { + // Unbinding thread + unbindThread(oldCCL); + } + + // Set available status depending upon startup success + if (ok) { + if (log.isTraceEnabled()) { + log.trace("Starting completed"); + } + } else { + log.error(sm.getString("standardContext.startFailed", getName())); + } + + startTime = System.currentTimeMillis(); + + // Send j2ee.state.running notification + if (ok && (this.getObjectName() != null)) { + Notification notification = + new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + + // The WebResources implementation caches references to JAR files. On + // some platforms these references may lock the JAR files. Since web + // application start is likely to have read from lots of JARs, trigger + // a clean-up now. + getResources().gc(); + + // Reinitializing if something went wrong + if (!ok) { + setState(LifecycleState.FAILED); + // Send j2ee.object.failed notification + if (this.getObjectName() != null) { + Notification notification = + new Notification("j2ee.object.failed", this.getObjectName(), sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + } else { + setState(LifecycleState.STARTING); + } + } + + + private void checkConstraintsForUncoveredMethods(SecurityConstraint[] constraints) { + SecurityConstraint[] newConstraints = + SecurityConstraint.findUncoveredHttpMethods(constraints, getDenyUncoveredHttpMethods(), getLogger()); + for (SecurityConstraint constraint : newConstraints) { + addConstraint(constraint); + } + } + + + @Override + public InstanceManager createInstanceManager() { + javax.naming.Context context = null; + if (isUseNaming() && getNamingContextListener() != null) { + context = getNamingContextListener().getEnvContext(); + } + Map> injectionMap = + buildInjectionMap(getIgnoreAnnotations() ? new NamingResourcesImpl() : getNamingResources()); + return new DefaultInstanceManager(context, injectionMap, this, this.getClass().getClassLoader()); + } + + private Map> buildInjectionMap(NamingResourcesImpl namingResources) { + Map> injectionMap = new HashMap<>(); + for (Injectable resource : namingResources.findLocalEjbs()) { + addInjectionTarget(resource, injectionMap); + } + for (Injectable resource : namingResources.findEjbs()) { + addInjectionTarget(resource, injectionMap); + } + for (Injectable resource : namingResources.findEnvironments()) { + addInjectionTarget(resource, injectionMap); + } + for (Injectable resource : namingResources.findMessageDestinationRefs()) { + addInjectionTarget(resource, injectionMap); + } + for (Injectable resource : namingResources.findResourceEnvRefs()) { + addInjectionTarget(resource, injectionMap); + } + for (Injectable resource : namingResources.findResources()) { + addInjectionTarget(resource, injectionMap); + } + for (Injectable resource : namingResources.findServices()) { + addInjectionTarget(resource, injectionMap); + } + return injectionMap; + } + + private void addInjectionTarget(Injectable resource, Map> injectionMap) { + List injectionTargets = resource.getInjectionTargets(); + if (injectionTargets != null && injectionTargets.size() > 0) { + String jndiName = resource.getName(); + for (InjectionTarget injectionTarget : injectionTargets) { + String clazz = injectionTarget.getTargetClass(); + injectionMap.computeIfAbsent(clazz, k -> new HashMap<>()).put(injectionTarget.getTargetName(), + jndiName); + } + } + } + + + /** + * Merge the context initialization parameters specified in the application deployment descriptor with the + * application parameters described in the server configuration, respecting the override property of + * the application parameters appropriately. + */ + private void mergeParameters() { + Map mergedParams = new HashMap<>(); + + String names[] = findParameters(); + for (String s : names) { + mergedParams.put(s, findParameter(s)); + } + + ApplicationParameter params[] = findApplicationParameters(); + for (ApplicationParameter param : params) { + if (param.getOverride()) { + mergedParams.computeIfAbsent(param.getName(), k -> param.getValue()); + } else { + mergedParams.put(param.getName(), param.getValue()); + } + } + + ServletContext sc = getServletContext(); + for (Map.Entry entry : mergedParams.entrySet()) { + sc.setInitParameter(entry.getKey(), entry.getValue()); + } + + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + // Send j2ee.state.stopping notification + if (this.getObjectName() != null) { + Notification notification = + new Notification("j2ee.state.stopping", this.getObjectName(), sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + + // Context has been removed from Mapper at this point (so no new + // requests will be mapped) but is still available. + + // Give the in progress async requests a chance to complete + long limit = System.currentTimeMillis() + unloadDelay; + while (inProgressAsyncCount.get() > 0 && System.currentTimeMillis() < limit) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + log.info(sm.getString("standardContext.stop.asyncWaitInterrupted"), e); + break; + } + } + + // Once the state is set to STOPPING, the Context will report itself as + // not available and any in progress async requests will timeout + setState(LifecycleState.STOPPING); + + // Binding thread + ClassLoader oldCCL = bindThread(); + + try { + // Stop our child containers, if any + final Container[] children = findChildren(); + + // Stop ContainerBackgroundProcessor thread + threadStop(); + + for (Container child : children) { + child.stop(); + } + + // Stop our filters + filterStop(); + + Manager manager = getManager(); + if (manager instanceof Lifecycle && ((Lifecycle) manager).getState().isAvailable()) { + ((Lifecycle) manager).stop(); + } + + // Stop our application listeners + listenerStop(); + + // Finalize our character set mapper + setCharsetMapper(null); + + // Normal container shutdown processing + if (log.isTraceEnabled()) { + log.trace("Processing standard container shutdown"); + } + + // JNDI resources are unbound in CONFIGURE_STOP_EVENT so stop + // naming resources before they are unbound since NamingResources + // does a JNDI lookup to retrieve the resource. This needs to be + // after the application has finished with the resource + if (namingResources != null) { + namingResources.stop(); + } + + fireLifecycleEvent(CONFIGURE_STOP_EVENT, null); + + // Stop the Valves in our pipeline (including the basic), if any + if (pipeline instanceof Lifecycle && ((Lifecycle) pipeline).getState().isAvailable()) { + ((Lifecycle) pipeline).stop(); + } + + // Clear all application-originated servlet context attributes + if (context != null) { + context.clearAttributes(); + } + + Realm realm = getRealmInternal(); + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).stop(); + } + Loader loader = getLoader(); + if (loader instanceof Lifecycle) { + ClassLoader classLoader = loader.getClassLoader(); + ((Lifecycle) loader).stop(); + if (classLoader != null) { + InstanceManagerBindings.unbind(classLoader); + } + } + + // Stop resources + resourcesStop(); + + } finally { + + // Unbinding thread + unbindThread(oldCCL); + + } + + // Send j2ee.state.stopped notification + if (this.getObjectName() != null) { + Notification notification = + new Notification("j2ee.state.stopped", this.getObjectName(), sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + + // Reset application context + context = null; + + // This object will no longer be visible or used. + try { + resetContext(); + } catch (Exception ex) { + log.error(sm.getString("standardContext.resetContextFail", getName()), ex); + } + + // reset the instance manager + setInstanceManager(null); + + if (log.isTraceEnabled()) { + log.trace("Stopping complete"); + } + + } + + /** + * Destroy needs to clean up the context completely. The problem is that undoing all the config in start() and + * restoring a 'fresh' state is impossible. After stop()/destroy()/init()/start() we should have the same state as + * if a fresh start was done - i.e read modified web.xml, etc. This can only be done by completely removing the + * context object and remapping a new one, or by cleaning up everything. + */ + @Override + protected void destroyInternal() throws LifecycleException { + + // If in state NEW when destroy is called, the object name will never + // have been set so the notification can't be created + if (getObjectName() != null) { + // Send j2ee.object.deleted notification + Notification notification = + new Notification("j2ee.object.deleted", this.getObjectName(), sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + + if (namingResources != null) { + namingResources.destroy(); + } + + Loader loader = getLoader(); + if (loader instanceof Lifecycle) { + ((Lifecycle) loader).destroy(); + } + + Manager manager = getManager(); + if (manager instanceof Lifecycle) { + ((Lifecycle) manager).destroy(); + } + + if (resources != null) { + resources.destroy(); + } + + super.destroyInternal(); + } + + + @Override + public synchronized void backgroundProcess() { + + if (!getState().isAvailable()) { + return; + } + + Loader loader = getLoader(); + if (loader != null) { + try { + loader.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString("standardContext.backgroundProcess.loader", loader), e); + } + } + Manager manager = getManager(); + if (manager != null) { + try { + manager.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString("standardContext.backgroundProcess.manager", manager), e); + } + } + WebResourceRoot resources = getResources(); + if (resources != null) { + try { + resources.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString("standardContext.backgroundProcess.resources", resources), e); + } + } + InstanceManager instanceManager = getInstanceManager(); + if (instanceManager != null) { + try { + instanceManager.backgroundProcess(); + } catch (Exception e) { + log.warn(sm.getString("standardContext.backgroundProcess.instanceManager", resources), e); + } + } + super.backgroundProcess(); + } + + + private void resetContext() throws Exception { + // Restore the original state ( pre reading web.xml in start ) + // If you extend this - override this method and make sure to clean up + + // Don't reset anything that is read from a element since + // elements are read at initialisation will not be read + // again for this object + for (Container child : findChildren()) { + removeChild(child); + } + startupTime = 0; + startTime = 0; + tldScanTime = 0; + + // Bugzilla 32867 + distributable = false; + + applicationListeners.clear(); + applicationEventListenersList.clear(); + applicationLifecycleListenersObjects = new Object[0]; + jspConfigDescriptor = null; + + initializers.clear(); + + createdServlets.clear(); + + postConstructMethods.clear(); + preDestroyMethods.clear(); + + if (log.isTraceEnabled()) { + log.trace("resetContext " + getObjectName()); + } + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Adjust the URL pattern to begin with a leading slash, if appropriate (i.e. we are running a servlet 2.2 + * application). Otherwise, return the specified URL pattern unchanged. + * + * @param urlPattern The URL pattern to be adjusted (if needed) and returned + * + * @return the URL pattern with a leading slash if needed + */ + protected String adjustURLPattern(String urlPattern) { + + if (urlPattern == null) { + return urlPattern; + } + if (urlPattern.startsWith("/") || urlPattern.startsWith("*.")) { + return urlPattern; + } + if (!isServlet22()) { + return urlPattern; + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardContext.urlPattern.patternWarning", urlPattern)); + } + return "/" + urlPattern; + + } + + + /** + * Are we processing a version 2.2 deployment descriptor? + * + * @return true if running a legacy Servlet 2.2 application + */ + @Override + public boolean isServlet22() { + return XmlIdentifiers.WEB_22_PUBLIC.equals(publicId); + } + + + @Override + public Set addServletSecurity(ServletRegistration.Dynamic registration, + ServletSecurityElement servletSecurityElement) { + + Set conflicts = new HashSet<>(); + + Collection urlPatterns = registration.getMappings(); + for (String urlPattern : urlPatterns) { + boolean foundConflict = false; + + SecurityConstraint[] securityConstraints = findConstraints(); + for (SecurityConstraint securityConstraint : securityConstraints) { + + SecurityCollection[] collections = securityConstraint.findCollections(); + for (SecurityCollection collection : collections) { + if (collection.findPattern(urlPattern)) { + // First pattern found will indicate if there is a + // conflict since for any given pattern all matching + // constraints will be from either the descriptor or + // not. It is not permitted to have a mixture + if (collection.isFromDescriptor()) { + // Skip this pattern + foundConflict = true; + conflicts.add(urlPattern); + break; + } else { + // Need to overwrite constraint for this pattern + collection.removePattern(urlPattern); + // If the collection is now empty, remove it + if (collection.findPatterns().length == 0) { + securityConstraint.removeCollection(collection); + } + } + } + } + + // If the constraint now has no collections - remove it + if (securityConstraint.findCollections().length == 0) { + removeConstraint(securityConstraint); + } + + // No need to check other constraints for the current pattern + // once a conflict has been found + if (foundConflict) { + break; + } + } + + // Note: For programmatically added Servlets this may not be the + // complete set of security constraints since additional + // URL patterns can be added after the application has called + // setSecurity. For all programmatically added servlets, the + // #dynamicServletAdded() method sets a flag that ensures that + // the constraints are re-evaluated before the servlet is + // first used + + // If the pattern did not conflict, add the new constraint(s). + if (!foundConflict) { + SecurityConstraint[] newSecurityConstraints = + SecurityConstraint.createConstraints(servletSecurityElement, urlPattern); + for (SecurityConstraint securityConstraint : newSecurityConstraints) { + addConstraint(securityConstraint); + } + } + } + + return conflicts; + } + + + /** + * Bind current thread, both for CL purposes and for JNDI ENC support during : startup, shutdown and reloading of + * the context. + * + * @return the previous context class loader + */ + protected ClassLoader bindThread() { + + ClassLoader oldContextClassLoader = bind(false, null); + + if (isUseNaming()) { + try { + ContextBindings.bindThread(this, getNamingToken()); + } catch (NamingException e) { + // Silent catch, as this is a normal case during the early + // startup stages + } + } + + return oldContextClassLoader; + } + + + /** + * Unbind thread and restore the specified context classloader. + * + * @param oldContextClassLoader the previous classloader + */ + protected void unbindThread(ClassLoader oldContextClassLoader) { + + if (isUseNaming()) { + ContextBindings.unbindThread(this, getNamingToken()); + } + + unbind(false, oldContextClassLoader); + } + + + @Override + public ClassLoader bind(boolean usePrivilegedAction, ClassLoader originalClassLoader) { + Loader loader = getLoader(); + ClassLoader webApplicationClassLoader = null; + if (loader != null) { + webApplicationClassLoader = loader.getClassLoader(); + } + + Thread currentThread = Thread.currentThread(); + if (originalClassLoader == null) { + if (usePrivilegedAction) { + PrivilegedAction pa = new PrivilegedGetTccl(currentThread); + originalClassLoader = AccessController.doPrivileged(pa); + } else { + originalClassLoader = currentThread.getContextClassLoader(); + } + } + + if (webApplicationClassLoader == null || webApplicationClassLoader == originalClassLoader) { + // Not possible or not necessary to switch class loaders. Return + // null to indicate this. + return null; + } + + ThreadBindingListener threadBindingListener = getThreadBindingListener(); + + if (usePrivilegedAction) { + PrivilegedAction pa = new PrivilegedSetTccl(currentThread, webApplicationClassLoader); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(webApplicationClassLoader); + } + if (threadBindingListener != null) { + try { + threadBindingListener.bind(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardContext.threadBindingListenerError", getName()), t); + } + } + + return originalClassLoader; + } + + + @Override + public void unbind(boolean usePrivilegedAction, ClassLoader originalClassLoader) { + if (originalClassLoader == null) { + return; + } + + if (threadBindingListener != null) { + try { + threadBindingListener.unbind(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardContext.threadBindingListenerError", getName()), t); + } + } + + Thread currentThread = Thread.currentThread(); + if (usePrivilegedAction) { + PrivilegedAction pa = new PrivilegedSetTccl(currentThread, originalClassLoader); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(originalClassLoader); + } + } + + + /** + * Get naming context full name. + * + * @return the context name + */ + private String getNamingContextName() { + if (namingContextName == null) { + Container parent = getParent(); + if (parent == null) { + namingContextName = getName(); + } else { + Deque stk = new ArrayDeque<>(); + StringBuilder buff = new StringBuilder(); + while (parent != null) { + stk.addFirst(parent.getName()); + parent = parent.getParent(); + } + while (!stk.isEmpty()) { + buff.append('/').append(stk.remove()); + } + buff.append(getName()); + namingContextName = buff.toString(); + } + } + return namingContextName; + } + + + /** + * Naming context listener accessor. + * + * @return the naming context listener associated with the webapp + */ + public NamingContextListener getNamingContextListener() { + return namingContextListener; + } + + + /** + * Naming context listener setter. + * + * @param namingContextListener the new naming context listener + */ + public void setNamingContextListener(NamingContextListener namingContextListener) { + this.namingContextListener = namingContextListener; + } + + + /** + * @return the request processing paused flag for this Context. + */ + @Override + public boolean getPaused() { + return this.paused; + } + + + @Override + public boolean fireRequestInitEvent(ServletRequest request) { + + Object instances[] = getApplicationEventListeners(); + + if ((instances != null) && (instances.length > 0)) { + + ServletRequestEvent event = new ServletRequestEvent(getServletContext(), request); + + for (Object instance : instances) { + if (instance == null) { + continue; + } + if (!(instance instanceof ServletRequestListener)) { + continue; + } + ServletRequestListener listener = (ServletRequestListener) instance; + + try { + listener.requestInitialized(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + getLogger().error( + sm.getString("standardContext.requestListener.requestInit", instance.getClass().getName()), + t); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + return false; + } + } + } + return true; + } + + + @Override + public boolean fireRequestDestroyEvent(ServletRequest request) { + Object instances[] = getApplicationEventListeners(); + + if ((instances != null) && (instances.length > 0)) { + + ServletRequestEvent event = new ServletRequestEvent(getServletContext(), request); + + for (int i = 0; i < instances.length; i++) { + int j = (instances.length - 1) - i; + if (instances[j] == null) { + continue; + } + if (!(instances[j] instanceof ServletRequestListener)) { + continue; + } + ServletRequestListener listener = (ServletRequestListener) instances[j]; + + try { + listener.requestDestroyed(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + getLogger().error(sm.getString("standardContext.requestListener.requestDestroyed", + instances[j].getClass().getName()), t); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + return false; + } + } + } + return true; + } + + + @Override + public void addPostConstructMethod(String clazz, String method) { + if (clazz == null || method == null) { + throw new IllegalArgumentException(sm.getString("standardContext.postconstruct.required")); + } + if (postConstructMethods.get(clazz) != null) { + throw new IllegalArgumentException(sm.getString("standardContext.postconstruct.duplicate", clazz)); + } + + postConstructMethods.put(clazz, method); + fireContainerEvent("addPostConstructMethod", clazz); + } + + + @Override + public void removePostConstructMethod(String clazz) { + postConstructMethods.remove(clazz); + fireContainerEvent("removePostConstructMethod", clazz); + } + + + @Override + public void addPreDestroyMethod(String clazz, String method) { + if (clazz == null || method == null) { + throw new IllegalArgumentException(sm.getString("standardContext.predestroy.required")); + } + if (preDestroyMethods.get(clazz) != null) { + throw new IllegalArgumentException(sm.getString("standardContext.predestroy.duplicate", clazz)); + } + + preDestroyMethods.put(clazz, method); + fireContainerEvent("addPreDestroyMethod", clazz); + } + + + @Override + public void removePreDestroyMethod(String clazz) { + preDestroyMethods.remove(clazz); + fireContainerEvent("removePreDestroyMethod", clazz); + } + + + @Override + public String findPostConstructMethod(String clazz) { + return postConstructMethods.get(clazz); + } + + + @Override + public String findPreDestroyMethod(String clazz) { + return preDestroyMethods.get(clazz); + } + + + @Override + public Map findPostConstructMethods() { + return postConstructMethods; + } + + + @Override + public Map findPreDestroyMethods() { + return preDestroyMethods; + } + + + /** + * Set the appropriate context attribute for our work directory. + */ + protected void postWorkDirectory() { + + // Acquire (or calculate) the work directory path + String workDir = getWorkDir(); + if (workDir == null || workDir.length() == 0) { + + // Retrieve our parent (normally a host) name + String hostName = null; + String engineName = null; + String hostWorkDir = null; + Container parentHost = getParent(); + if (parentHost != null) { + hostName = parentHost.getName(); + if (parentHost instanceof StandardHost) { + hostWorkDir = ((StandardHost) parentHost).getWorkDir(); + } + Container parentEngine = parentHost.getParent(); + if (parentEngine != null) { + engineName = parentEngine.getName(); + } + } + if ((hostName == null) || (hostName.length() < 1)) { + hostName = "_"; + } + if ((engineName == null) || (engineName.length() < 1)) { + engineName = "_"; + } + + String temp = getBaseName(); + if (temp.startsWith("/")) { + temp = temp.substring(1); + } + temp = temp.replace('/', '_'); + temp = temp.replace('\\', '_'); + if (temp.length() < 1) { + temp = ContextName.ROOT_NAME; + } + if (hostWorkDir != null) { + workDir = hostWorkDir + File.separator + temp; + } else { + workDir = "work" + File.separator + engineName + File.separator + hostName + File.separator + temp; + } + setWorkDir(workDir); + } + + // Create this directory if necessary + File dir = new File(workDir); + if (!dir.isAbsolute()) { + String catalinaHomePath = null; + try { + catalinaHomePath = getCatalinaBase().getCanonicalPath(); + dir = new File(catalinaHomePath, workDir); + } catch (IOException e) { + log.warn(sm.getString("standardContext.workCreateException", workDir, catalinaHomePath, getName()), e); + } + } + if (!dir.mkdirs() && !dir.isDirectory()) { + log.warn(sm.getString("standardContext.workCreateFail", dir, getName())); + } + + // Set the appropriate servlet context attribute + if (context == null) { + getServletContext(); + } + context.setAttribute(ServletContext.TEMPDIR, dir); + context.setAttributeReadOnly(ServletContext.TEMPDIR); + } + + + /** + * Set the request processing paused flag for this Context. + * + * @param paused The new request processing paused flag + */ + private void setPaused(boolean paused) { + + this.paused = paused; + + } + + + /** + * Validate the syntax of a proposed <url-pattern> for conformance with specification + * requirements. + * + * @param urlPattern URL pattern to be validated + * + * @return true if the URL pattern is conformant + */ + private boolean validateURLPattern(String urlPattern) { + + if (urlPattern == null) { + return false; + } + if (urlPattern.indexOf('\n') >= 0 || urlPattern.indexOf('\r') >= 0) { + return false; + } + if (urlPattern.equals("")) { + return true; + } + if (urlPattern.startsWith("*.")) { + if (urlPattern.indexOf('/') < 0) { + checkUnusualURLPattern(urlPattern); + return true; + } else { + return false; + } + } + if (urlPattern.startsWith("/") && !urlPattern.contains("*.")) { + checkUnusualURLPattern(urlPattern); + return true; + } else { + return false; + } + + } + + + /** + * Check for unusual but valid <url-pattern>s. See Bugzilla 34805, 43079 & 43080 + */ + private void checkUnusualURLPattern(String urlPattern) { + if (log.isInfoEnabled()) { + // First group checks for '*' or '/foo*' style patterns + // Second group checks for *.foo.bar style patterns + if ((urlPattern.endsWith("*") && + (urlPattern.length() < 2 || urlPattern.charAt(urlPattern.length() - 2) != '/')) || + urlPattern.startsWith("*.") && urlPattern.length() > 2 && urlPattern.lastIndexOf('.') > 1) { + log.info(sm.getString("standardContext.suspiciousUrl", urlPattern, getName())); + } + } + } + + + // ------------------------------------------------------------- Operations + + @Override + protected String getObjectNameKeyProperties() { + + StringBuilder keyProperties = new StringBuilder("j2eeType=WebModule,"); + keyProperties.append(getObjectKeyPropertiesNameOnly()); + keyProperties.append(",J2EEApplication="); + keyProperties.append(getJ2EEApplication()); + keyProperties.append(",J2EEServer="); + keyProperties.append(getJ2EEServer()); + + return keyProperties.toString(); + } + + private String getObjectKeyPropertiesNameOnly() { + StringBuilder result = new StringBuilder("name=//"); + String hostname = getParent().getName(); + if (hostname == null) { + result.append("DEFAULT"); + } else { + result.append(hostname); + } + + String contextName = getName(); + if (!contextName.startsWith("/")) { + result.append('/'); + } + result.append(contextName); + + return result.toString(); + } + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + + // Register the naming resources + if (namingResources != null) { + namingResources.init(); + } + + // Send j2ee.object.created notification + if (this.getObjectName() != null) { + Notification notification = + new Notification("j2ee.object.created", this.getObjectName(), sequenceNumber.getAndIncrement()); + broadcaster.sendNotification(notification); + } + } + + + /** + * Remove a JMX notificationListener + * + * @see javax.management.NotificationEmitter#removeNotificationListener(javax.management.NotificationListener, + * javax.management.NotificationFilter, java.lang.Object) + */ + @Override + public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object object) + throws ListenerNotFoundException { + broadcaster.removeNotificationListener(listener, filter, object); + } + + private MBeanNotificationInfo[] notificationInfo; + + /** + * Get JMX Broadcaster Info + * + * @see javax.management.NotificationBroadcaster#getNotificationInfo() + */ + @Override + public MBeanNotificationInfo[] getNotificationInfo() { + if (notificationInfo == null) { + notificationInfo = new MBeanNotificationInfo[] { + new MBeanNotificationInfo(new String[] { "j2ee.object.created" }, Notification.class.getName(), + "web application is created"), + new MBeanNotificationInfo(new String[] { "j2ee.state.starting" }, Notification.class.getName(), + "change web application is starting"), + new MBeanNotificationInfo(new String[] { "j2ee.state.running" }, Notification.class.getName(), + "web application is running"), + new MBeanNotificationInfo(new String[] { "j2ee.state.stopping" }, Notification.class.getName(), + "web application start to stopped"), + new MBeanNotificationInfo(new String[] { "j2ee.object.stopped" }, Notification.class.getName(), + "web application is stopped"), + new MBeanNotificationInfo(new String[] { "j2ee.object.deleted" }, Notification.class.getName(), + "web application is deleted"), + new MBeanNotificationInfo(new String[] { "j2ee.object.failed" }, Notification.class.getName(), + "web application failed") }; + } + + return notificationInfo; + } + + + /** + * Add a JMX NotificationListener + * + * @see javax.management.NotificationBroadcaster#addNotificationListener(javax.management.NotificationListener, + * javax.management.NotificationFilter, java.lang.Object) + */ + @Override + public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object object) + throws IllegalArgumentException { + broadcaster.addNotificationListener(listener, filter, object); + } + + + /** + * Remove a JMX-NotificationListener + * + * @see javax.management.NotificationBroadcaster#removeNotificationListener(javax.management.NotificationListener) + */ + @Override + public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException { + broadcaster.removeNotificationListener(listener); + } + + + // ------------------------------------------------------------- Attributes + + /** + * @return the naming resources associated with this web application. + */ + public String[] getWelcomeFiles() { + + return findWelcomeFiles(); + + } + + + @Override + public boolean getXmlNamespaceAware() { + return webXmlNamespaceAware; + } + + + @Override + public void setXmlNamespaceAware(boolean webXmlNamespaceAware) { + this.webXmlNamespaceAware = webXmlNamespaceAware; + } + + + @Override + public void setXmlValidation(boolean webXmlValidation) { + this.webXmlValidation = webXmlValidation; + } + + + @Override + public boolean getXmlValidation() { + return webXmlValidation; + } + + + @Override + public void setXmlBlockExternal(boolean xmlBlockExternal) { + this.xmlBlockExternal = xmlBlockExternal; + } + + + @Override + public boolean getXmlBlockExternal() { + return xmlBlockExternal; + } + + + @Override + public void setTldValidation(boolean tldValidation) { + this.tldValidation = tldValidation; + } + + + @Override + public boolean getTldValidation() { + return tldValidation; + } + + + /** + * The J2EE Server ObjectName this module is deployed on. + */ + private String server = null; + + public String getServer() { + return server; + } + + public String setServer(String server) { + return this.server = server; + } + + /** + * Gets the time this context was started. + * + * @return Time (in milliseconds since January 1, 1970, 00:00:00) when this context was started + */ + public long getStartTime() { + return startTime; + } + + + private static class NoPluggabilityServletContext implements ServletContext { + + private final ServletContext sc; + + NoPluggabilityServletContext(ServletContext sc) { + this.sc = sc; + } + + @Override + public String getContextPath() { + return sc.getContextPath(); + } + + @Override + public ServletContext getContext(String uripath) { + return sc.getContext(uripath); + } + + @Override + public int getMajorVersion() { + return sc.getMajorVersion(); + } + + @Override + public int getMinorVersion() { + return sc.getMinorVersion(); + } + + @Override + public int getEffectiveMajorVersion() { + return sc.getEffectiveMajorVersion(); + } + + @Override + public int getEffectiveMinorVersion() { + return sc.getEffectiveMinorVersion(); + } + + @Override + public String getMimeType(String file) { + return sc.getMimeType(file); + } + + @Override + public Set getResourcePaths(String path) { + return sc.getResourcePaths(path); + } + + @Override + public URL getResource(String path) throws MalformedURLException { + return sc.getResource(path); + } + + @Override + public InputStream getResourceAsStream(String path) { + return sc.getResourceAsStream(path); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return sc.getRequestDispatcher(path); + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + return sc.getNamedDispatcher(name); + } + + @Override + public void log(String msg) { + sc.log(msg); + } + + @Override + public void log(String message, Throwable throwable) { + sc.log(message, throwable); + } + + @Override + public String getRealPath(String path) { + return sc.getRealPath(path); + } + + @Override + public String getServerInfo() { + return sc.getServerInfo(); + } + + @Override + public String getInitParameter(String name) { + return sc.getInitParameter(name); + } + + @Override + public Enumeration getInitParameterNames() { + return sc.getInitParameterNames(); + } + + @Override + public boolean setInitParameter(String name, String value) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public Object getAttribute(String name) { + return sc.getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() { + return sc.getAttributeNames(); + } + + @Override + public void setAttribute(String name, Object object) { + sc.setAttribute(name, object); + } + + @Override + public void removeAttribute(String name) { + sc.removeAttribute(name); + } + + @Override + public String getServletContextName() { + return sc.getServletContextName(); + } + + @Override + public Dynamic addServlet(String servletName, String className) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public Dynamic addServlet(String servletName, Servlet servlet) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public Dynamic addServlet(String servletName, Class servletClass) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public Dynamic addJspFile(String jspName, String jspFile) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public T createServlet(Class c) throws ServletException { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public ServletRegistration getServletRegistration(String servletName) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public Map getServletRegistrations() { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, + Class filterClass) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public T createFilter(Class c) throws ServletException { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public Map getFilterRegistrations() { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public Set getDefaultSessionTrackingModes() { + return sc.getDefaultSessionTrackingModes(); + } + + @Override + public Set getEffectiveSessionTrackingModes() { + return sc.getEffectiveSessionTrackingModes(); + } + + @Override + public void addListener(String className) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public void addListener(T t) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public void addListener(Class listenerClass) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public T createListener(Class c) throws ServletException { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return sc.getJspConfigDescriptor(); + } + + @Override + public ClassLoader getClassLoader() { + return sc.getClassLoader(); + } + + @Override + public void declareRoles(String... roleNames) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public String getVirtualServerName() { + return sc.getVirtualServerName(); + } + + @Override + public int getSessionTimeout() { + return sc.getSessionTimeout(); + } + + @Override + public void setSessionTimeout(int sessionTimeout) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public String getRequestCharacterEncoding() { + return sc.getRequestCharacterEncoding(); + } + + @Override + public void setRequestCharacterEncoding(String encoding) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + + @Override + public String getResponseCharacterEncoding() { + return sc.getResponseCharacterEncoding(); + } + + @Override + public void setResponseCharacterEncoding(String encoding) { + throw new UnsupportedOperationException(sm.getString("noPluggabilityServletContext.notAllowed")); + } + } +} diff --git a/java/org/apache/catalina/core/StandardContextValve.java b/java/org/apache/catalina/core/StandardContextValve.java new file mode 100644 index 0000000..a1499db --- /dev/null +++ b/java/org/apache/catalina/core/StandardContextValve.java @@ -0,0 +1,92 @@ +/* + * 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.catalina.core; + +import java.io.IOException; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.res.StringManager; + +/** + * Valve that implements the default basic behavior for the StandardContext container implementation. + *

    + * USAGE CONSTRAINT: This implementation is likely to be useful only when processing HTTP requests. + * + * @author Craig R. McClanahan + */ +final class StandardContextValve extends ValveBase { + + private static final StringManager sm = StringManager.getManager(StandardContextValve.class); + + StandardContextValve() { + super(true); + } + + + /** + * Select the appropriate child Wrapper to process this request, based on the specified request URI. If no matching + * Wrapper can be found, return an appropriate HTTP error. + * + * @param request Request to be processed + * @param response Response to be produced + * + * @exception IOException if an input/output error occurred + * @exception ServletException if a servlet error occurred + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + // Disallow any direct access to resources under WEB-INF or META-INF + MessageBytes requestPathMB = request.getRequestPathMB(); + if ((requestPathMB.startsWithIgnoreCase("/META-INF/", 0)) || (requestPathMB.equalsIgnoreCase("/META-INF")) || + (requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0)) || (requestPathMB.equalsIgnoreCase("/WEB-INF"))) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // Select the Wrapper to be used for this Request + Wrapper wrapper = request.getWrapper(); + if (wrapper == null || wrapper.isUnavailable()) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // Acknowledge the request + try { + response.sendAcknowledgement(ContinueResponseTiming.IMMEDIATELY); + } catch (IOException ioe) { + container.getLogger().error(sm.getString("standardContextValve.acknowledgeException"), ioe); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + if (request.isAsyncSupported()) { + request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported()); + } + wrapper.getPipeline().getFirst().invoke(request, response); + } +} diff --git a/java/org/apache/catalina/core/StandardEngine.java b/java/org/apache/catalina/core/StandardEngine.java new file mode 100644 index 0000000..59cc280 --- /dev/null +++ b/java/org/apache/catalina/core/StandardEngine.java @@ -0,0 +1,468 @@ +/* + * 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.catalina.core; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Container; +import org.apache.catalina.ContainerEvent; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Realm; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.realm.NullRealm; +import org.apache.catalina.util.ServerInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Standard implementation of the Engine interface. Each child container must be a Host implementation to process + * the specific fully qualified host name of that virtual host. + * + * @author Craig R. McClanahan + */ +public class StandardEngine extends ContainerBase implements Engine { + + private static final Log log = LogFactory.getLog(StandardEngine.class); + + + // ----------------------------------------------------------- Constructors + + /** + * Create a new StandardEngine component with the default basic Valve. + */ + public StandardEngine() { + pipeline.setBasic(new StandardEngineValve()); + // By default, the engine will hold the reloading thread + backgroundProcessorDelay = 10; + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Host name to use when no server host, or an unknown host, is specified in the request. + */ + private String defaultHost = null; + + + /** + * The Service that owns this Engine, if any. + */ + private Service service = null; + + /** + * The JVM Route ID for this Tomcat instance. All Route ID's must be unique across the cluster. + */ + private String jvmRouteId; + + /** + * Default access log to use for request/response pairs where we can't ID the intended host and context. + */ + private final AtomicReference defaultAccessLog = new AtomicReference<>(); + + // ------------------------------------------------------------- Properties + + /** + * Obtain the configured Realm and provide a default Realm implementation when no explicit configuration is set. + * + * @return configured realm, or a {@link NullRealm} by default + */ + @Override + public Realm getRealm() { + Realm configured = super.getRealm(); + // If no set realm has been called - default to NullRealm + // This can be overridden at engine, context and host level + if (configured == null) { + configured = new NullRealm(); + this.setRealm(configured); + } + return configured; + } + + + /** + * Return the default host. + */ + @Override + public String getDefaultHost() { + return defaultHost; + } + + + /** + * Set the default host. + * + * @param host The new default host + */ + @Override + public void setDefaultHost(String host) { + + String oldDefaultHost = this.defaultHost; + if (host == null) { + this.defaultHost = null; + } else { + this.defaultHost = host.toLowerCase(Locale.ENGLISH); + } + if (getState().isAvailable()) { + service.getMapper().setDefaultHostName(host); + } + support.firePropertyChange("defaultHost", oldDefaultHost, this.defaultHost); + + } + + + /** + * Set the cluster-wide unique identifier for this Engine. This value is only useful in a load-balancing scenario. + *

    + * This property should not be changed once it is set. + */ + @Override + public void setJvmRoute(String routeId) { + jvmRouteId = routeId; + } + + + /** + * Retrieve the cluster-wide unique identifier for this Engine. This value is only useful in a load-balancing + * scenario. + */ + @Override + public String getJvmRoute() { + return jvmRouteId; + } + + + /** + * Return the Service with which we are associated (if any). + */ + @Override + public Service getService() { + return this.service; + } + + + /** + * Set the Service with which we are associated (if any). + * + * @param service The service that owns this Engine + */ + @Override + public void setService(Service service) { + this.service = service; + } + + // --------------------------------------------------------- Public Methods + + + /** + * Add a child Container, only if the proposed child is an implementation of Host. + * + * @param child Child container to be added + */ + @Override + public void addChild(Container child) { + + if (!(child instanceof Host)) { + throw new IllegalArgumentException(sm.getString("standardEngine.notHost")); + } + super.addChild(child); + + } + + + /** + * Disallow any attempt to set a parent for this Container, since an Engine is supposed to be at the top of the + * Container hierarchy. + * + * @param container Proposed parent Container + */ + @Override + public void setParent(Container container) { + + throw new IllegalArgumentException(sm.getString("standardEngine.notParent")); + + } + + + @Override + protected void initInternal() throws LifecycleException { + // Ensure that a Realm is present before any attempt is made to start + // one. This will create the default NullRealm if necessary. + getRealm(); + super.initInternal(); + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + // Log our server identification information + if (log.isInfoEnabled()) { + log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo())); + } + + // Standard container startup + super.startInternal(); + } + + + /** + * Override the default implementation. If no access log is defined for the Engine, look for one in the Engine's + * default host and then the default host's ROOT context. If still none is found, return the default NoOp access + * log. + */ + @Override + public void logAccess(Request request, Response response, long time, boolean useDefault) { + + boolean logged = false; + + if (getAccessLog() != null) { + accessLog.log(request, response, time); + logged = true; + } + + if (!logged && useDefault) { + AccessLog newDefaultAccessLog = defaultAccessLog.get(); + if (newDefaultAccessLog == null) { + // If we reached this point, this Engine can't have an AccessLog + // Look in the defaultHost + Host host = (Host) findChild(getDefaultHost()); + Context context = null; + if (host != null && host.getState().isAvailable()) { + newDefaultAccessLog = host.getAccessLog(); + + if (newDefaultAccessLog != null) { + if (defaultAccessLog.compareAndSet(null, newDefaultAccessLog)) { + AccessLogListener l = new AccessLogListener(this, host, null); + l.install(); + } + } else { + // Try the ROOT context of default host + context = (Context) host.findChild(""); + if (context != null && context.getState().isAvailable()) { + newDefaultAccessLog = context.getAccessLog(); + if (newDefaultAccessLog != null) { + if (defaultAccessLog.compareAndSet(null, newDefaultAccessLog)) { + AccessLogListener l = new AccessLogListener(this, null, context); + l.install(); + } + } + } + } + } + + if (newDefaultAccessLog == null) { + newDefaultAccessLog = new NoopAccessLog(); + if (defaultAccessLog.compareAndSet(null, newDefaultAccessLog)) { + AccessLogListener l = new AccessLogListener(this, host, context); + l.install(); + } + } + } + + newDefaultAccessLog.log(request, response, time); + } + } + + + /** + * Return the parent class loader for this component. + */ + @Override + public ClassLoader getParentClassLoader() { + if (parentClassLoader != null) { + return parentClassLoader; + } + if (service != null) { + return service.getParentClassLoader(); + } + return ClassLoader.getSystemClassLoader(); + } + + + @Override + public File getCatalinaBase() { + if (service != null) { + Server s = service.getServer(); + if (s != null) { + File base = s.getCatalinaBase(); + if (base != null) { + return base; + } + } + } + // Fall-back + return super.getCatalinaBase(); + } + + + @Override + public File getCatalinaHome() { + if (service != null) { + Server s = service.getServer(); + if (s != null) { + File base = s.getCatalinaHome(); + if (base != null) { + return base; + } + } + } + // Fall-back + return super.getCatalinaHome(); + } + + + // -------------------- JMX registration -------------------- + + @Override + protected String getObjectNameKeyProperties() { + return "type=Engine"; + } + + + @Override + protected String getDomainInternal() { + return getName(); + } + + + // ----------------------------------------------------------- Inner classes + protected static final class NoopAccessLog implements AccessLog { + + @Override + public void log(Request request, Response response, long time) { + // NOOP + } + + @Override + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + // NOOP + + } + + @Override + public boolean getRequestAttributesEnabled() { + // NOOP + return false; + } + } + + protected static final class AccessLogListener + implements PropertyChangeListener, LifecycleListener, ContainerListener { + + private final StandardEngine engine; + private final Host host; + private final Context context; + private volatile boolean disabled = false; + + public AccessLogListener(StandardEngine engine, Host host, Context context) { + this.engine = engine; + this.host = host; + this.context = context; + } + + public void install() { + engine.addPropertyChangeListener(this); + if (host != null) { + host.addContainerListener(this); + host.addLifecycleListener(this); + } + if (context != null) { + context.addLifecycleListener(this); + } + } + + private void uninstall() { + disabled = true; + if (context != null) { + context.removeLifecycleListener(this); + } + if (host != null) { + host.removeLifecycleListener(this); + host.removeContainerListener(this); + } + engine.removePropertyChangeListener(this); + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (disabled) { + return; + } + + String type = event.getType(); + if (AFTER_START_EVENT.equals(type) || BEFORE_STOP_EVENT.equals(type) || BEFORE_DESTROY_EVENT.equals(type)) { + // Container is being started/stopped/removed + // Force re-calculation and disable listener since it won't + // be re-used + engine.defaultAccessLog.set(null); + uninstall(); + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (disabled) { + return; + } + if ("defaultHost".equals(evt.getPropertyName())) { + // Force re-calculation and disable listener since it won't + // be re-used + engine.defaultAccessLog.set(null); + uninstall(); + } + } + + @Override + public void containerEvent(ContainerEvent event) { + // Only useful for hosts + if (disabled) { + return; + } + if (ADD_CHILD_EVENT.equals(event.getType())) { + Context context = (Context) event.getData(); + if (context.getPath().isEmpty()) { + // Force re-calculation and disable listener since it won't + // be re-used + engine.defaultAccessLog.set(null); + uninstall(); + } + } + } + } +} diff --git a/java/org/apache/catalina/core/StandardEngineValve.java b/java/org/apache/catalina/core/StandardEngineValve.java new file mode 100644 index 0000000..5c3f0ce --- /dev/null +++ b/java/org/apache/catalina/core/StandardEngineValve.java @@ -0,0 +1,76 @@ +/* + * 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.catalina.core; + +import java.io.IOException; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.Host; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; + +/** + * Valve that implements the default basic behavior for the StandardEngine container implementation. + *

    + * USAGE CONSTRAINT: This implementation is likely to be useful only when processing HTTP requests. + * + * @author Craig R. McClanahan + */ +final class StandardEngineValve extends ValveBase { + + // ------------------------------------------------------ Constructor + StandardEngineValve() { + super(true); + } + + + // --------------------------------------------------------- Public Methods + + /** + * Select the appropriate child Host to process this request, based on the requested server name. If no matching + * Host can be found, return an appropriate HTTP error. + * + * @param request Request to be processed + * @param response Response to be produced + * + * @exception IOException if an input/output error occurred + * @exception ServletException if a servlet error occurred + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + // Select the Host to be used for this Request + Host host = request.getHost(); + if (host == null) { + // HTTP 0.9 or HTTP 1.0 request without a host when no default host + // is defined. + // Don't overwrite an existing error + if (!response.isError()) { + response.sendError(404); + } + return; + } + if (request.isAsyncSupported()) { + request.setAsyncSupported(host.getPipeline().isAsyncSupported()); + } + + // Ask this Host to process this request + host.getPipeline().getFirst().invoke(request, response); + } +} diff --git a/java/org/apache/catalina/core/StandardHost.java b/java/org/apache/catalina/core/StandardHost.java new file mode 100644 index 0000000..483d6bb --- /dev/null +++ b/java/org/apache/catalina/core/StandardHost.java @@ -0,0 +1,885 @@ +/* + * 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.catalina.core; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ExecutorService; +import java.util.regex.Pattern; + +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.JmxEnabled; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Valve; +import org.apache.catalina.loader.WebappClassLoaderBase; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.valves.ErrorReportValve; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; + +/** + * Standard implementation of the Host interface. Each child container must be a Context implementation to + * process the requests directed to a particular web application. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class StandardHost extends ContainerBase implements Host { + + private static final Log log = LogFactory.getLog(StandardHost.class); + + // ----------------------------------------------------------- Constructors + + + /** + * Create a new StandardHost component with the default basic Valve. + */ + public StandardHost() { + + super(); + pipeline.setBasic(new StandardHostValve()); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The set of aliases for this Host. + */ + private String[] aliases = new String[0]; + + private final Object aliasesLock = new Object(); + + + /** + * The application root for this Host. + */ + private String appBase = "webapps"; + private volatile File appBaseFile = null; + + + /** + * The legacy (Java EE) application root for this Host. + */ + private String legacyAppBase = "webapps-javaee"; + private volatile File legacyAppBaseFile = null; + + + /** + * The XML root for this Host. + */ + private String xmlBase = null; + + /** + * host's default config path + */ + private volatile File hostConfigBase = null; + + /** + * The auto deploy flag for this Host. + */ + private boolean autoDeploy = true; + + + /** + * The Java class name of the default context configuration class for deployed web applications. + */ + private String configClass = "org.apache.catalina.startup.ContextConfig"; + + + /** + * The Java class name of the default Context implementation class for deployed web applications. + */ + private String contextClass = "org.apache.catalina.core.StandardContext"; + + + /** + * The deploy on startup flag for this Host. + */ + private boolean deployOnStartup = true; + + + /** + * deploy Context XML config files property. + */ + private boolean deployXML = !Globals.IS_SECURITY_ENABLED; + + + /** + * Should XML files be copied to $CATALINA_BASE/conf/<engine>/<host> by default when a web application + * is deployed? + */ + private boolean copyXML = false; + + + /** + * The Java class name of the default error reporter implementation class for deployed web applications. + */ + private String errorReportValveClass = "org.apache.catalina.valves.ErrorReportValve"; + + + /** + * Unpack WARs property. + */ + private boolean unpackWARs = true; + + + /** + * Work Directory base for applications. + */ + private String workDir = null; + + + /** + * Should we create directories upon startup for appBase and xmlBase + */ + private boolean createDirs = true; + + + /** + * Track the class loaders for the child web applications so memory leaks can be detected. + */ + private final Map childClassLoaders = new WeakHashMap<>(); + + + /** + * Any file or directory in {@link #appBase} that this pattern matches will be ignored by the automatic deployment + * process (both {@link #deployOnStartup} and {@link #autoDeploy}). + */ + private Pattern deployIgnore = null; + + + private boolean undeployOldVersions = false; + + private boolean failCtxIfServletStartFails = false; + + + // ------------------------------------------------------------- Properties + + @Override + public boolean getUndeployOldVersions() { + return undeployOldVersions; + } + + + @Override + public void setUndeployOldVersions(boolean undeployOldVersions) { + this.undeployOldVersions = undeployOldVersions; + } + + + @Override + public ExecutorService getStartStopExecutor() { + return startStopExecutor; + } + + + @Override + public String getAppBase() { + return this.appBase; + } + + + @Override + public File getAppBaseFile() { + + if (appBaseFile != null) { + return appBaseFile; + } + + File file = new File(getAppBase()); + + // If not absolute, make it absolute + if (!file.isAbsolute()) { + file = new File(getCatalinaBase(), file.getPath()); + } + + // Make it canonical if possible + try { + file = file.getCanonicalFile(); + } catch (IOException ioe) { + // Ignore + } + + this.appBaseFile = file; + return file; + } + + + @Override + public void setAppBase(String appBase) { + if (appBase.trim().equals("")) { + log.warn(sm.getString("standardHost.problematicAppBase", getName())); + } + String oldAppBase = this.appBase; + this.appBase = appBase; + support.firePropertyChange("appBase", oldAppBase, this.appBase); + this.appBaseFile = null; + } + + + @Override + public String getLegacyAppBase() { + return this.legacyAppBase; + } + + + @Override + public File getLegacyAppBaseFile() { + if (legacyAppBaseFile != null) { + return legacyAppBaseFile; + } + + File file = new File(getLegacyAppBase()); + + // If not absolute, make it absolute + if (!file.isAbsolute()) { + file = new File(getCatalinaBase(), file.getPath()); + } + + // Make it canonical if possible + try { + file = file.getCanonicalFile(); + } catch (IOException ioe) { + // Ignore + } + + this.legacyAppBaseFile = file; + return file; + } + + + @Override + public void setLegacyAppBase(String legacyAppBase) { + if (legacyAppBase.trim().equals("")) { + log.warn(sm.getString("standardHost.problematicLegacyAppBase", getName())); + } + String oldLegacyAppBase = this.legacyAppBase; + this.legacyAppBase = legacyAppBase; + support.firePropertyChange("legacyAppBase", oldLegacyAppBase, this.legacyAppBase); + this.legacyAppBaseFile = null; + } + + + /** + * ({@inheritDoc} + */ + @Override + public String getXmlBase() { + return this.xmlBase; + } + + + /** + * ({@inheritDoc} + */ + @Override + public void setXmlBase(String xmlBase) { + String oldXmlBase = this.xmlBase; + this.xmlBase = xmlBase; + support.firePropertyChange("xmlBase", oldXmlBase, this.xmlBase); + } + + + /** + * ({@inheritDoc} + */ + @Override + public File getConfigBaseFile() { + if (hostConfigBase != null) { + return hostConfigBase; + } + String path = null; + if (getXmlBase() != null) { + path = getXmlBase(); + } else { + StringBuilder xmlDir = new StringBuilder("conf"); + Container parent = getParent(); + if (parent instanceof Engine) { + xmlDir.append('/'); + xmlDir.append(parent.getName()); + } + xmlDir.append('/'); + xmlDir.append(getName()); + path = xmlDir.toString(); + } + File file = new File(path); + if (!file.isAbsolute()) { + file = new File(getCatalinaBase(), path); + } + try { + file = file.getCanonicalFile(); + } catch (IOException e) {// ignore + } + this.hostConfigBase = file; + return file; + } + + + /** + * @return true if the Host will attempt to create directories for appBase and xmlBase unless they + * already exist. + */ + @Override + public boolean getCreateDirs() { + return createDirs; + } + + /** + * Set to true if the Host should attempt to create directories for xmlBase and appBase upon startup + * + * @param createDirs the new flag value + */ + @Override + public void setCreateDirs(boolean createDirs) { + this.createDirs = createDirs; + } + + /** + * @return the value of the auto deploy flag. If true, it indicates that this host's child webapps will be + * dynamically deployed. + */ + @Override + public boolean getAutoDeploy() { + return this.autoDeploy; + } + + + /** + * Set the auto deploy flag value for this host. + * + * @param autoDeploy The new auto deploy flag + */ + @Override + public void setAutoDeploy(boolean autoDeploy) { + + boolean oldAutoDeploy = this.autoDeploy; + this.autoDeploy = autoDeploy; + support.firePropertyChange("autoDeploy", oldAutoDeploy, this.autoDeploy); + + } + + + /** + * @return the Java class name of the context configuration class for new web applications. + */ + @Override + public String getConfigClass() { + return this.configClass; + } + + + /** + * Set the Java class name of the context configuration class for new web applications. + * + * @param configClass The new context configuration class + */ + @Override + public void setConfigClass(String configClass) { + + String oldConfigClass = this.configClass; + this.configClass = configClass; + support.firePropertyChange("configClass", oldConfigClass, this.configClass); + + } + + + /** + * @return the Java class name of the Context implementation class for new web applications. + */ + public String getContextClass() { + return this.contextClass; + } + + + /** + * Set the Java class name of the Context implementation class for new web applications. + * + * @param contextClass The new context implementation class + */ + public void setContextClass(String contextClass) { + + String oldContextClass = this.contextClass; + this.contextClass = contextClass; + support.firePropertyChange("contextClass", oldContextClass, this.contextClass); + + } + + + /** + * @return the value of the deploy on startup flag. If true, it indicates that this host's child + * webapps should be discovered and automatically deployed at startup time. + */ + @Override + public boolean getDeployOnStartup() { + return this.deployOnStartup; + } + + + /** + * Set the deploy on startup flag value for this host. + * + * @param deployOnStartup The new deploy on startup flag + */ + @Override + public void setDeployOnStartup(boolean deployOnStartup) { + + boolean oldDeployOnStartup = this.deployOnStartup; + this.deployOnStartup = deployOnStartup; + support.firePropertyChange("deployOnStartup", oldDeployOnStartup, this.deployOnStartup); + + } + + + /** + * @return true if XML context descriptors should be deployed. + */ + public boolean isDeployXML() { + return deployXML; + } + + + /** + * Deploy XML Context config files flag mutator. + * + * @param deployXML true if context descriptors should be deployed + */ + public void setDeployXML(boolean deployXML) { + this.deployXML = deployXML; + } + + + /** + * @return the copy XML config file flag for this component. + */ + public boolean isCopyXML() { + return this.copyXML; + } + + + /** + * Set the copy XML config file flag for this component. + * + * @param copyXML The new copy XML flag + */ + public void setCopyXML(boolean copyXML) { + this.copyXML = copyXML; + } + + + /** + * @return the Java class name of the error report valve class for new web applications. + */ + public String getErrorReportValveClass() { + return this.errorReportValveClass; + } + + + /** + * Set the Java class name of the error report valve class for new web applications. + * + * @param errorReportValveClass The new error report valve class + */ + public void setErrorReportValveClass(String errorReportValveClass) { + + String oldErrorReportValveClassClass = this.errorReportValveClass; + this.errorReportValveClass = errorReportValveClass; + support.firePropertyChange("errorReportValveClass", oldErrorReportValveClassClass, this.errorReportValveClass); + + } + + + /** + * @return the canonical, fully qualified, name of the virtual host this Container represents. + */ + @Override + public String getName() { + return name; + } + + + /** + * Set the canonical, fully qualified, name of the virtual host this Container represents. + * + * @param name Virtual host name + * + * @exception IllegalArgumentException if name is null + */ + @Override + public void setName(String name) { + + if (name == null) { + throw new IllegalArgumentException(sm.getString("standardHost.nullName")); + } + + name = name.toLowerCase(Locale.ENGLISH); // Internally all names are lower case + + String oldName = this.name; + this.name = name; + support.firePropertyChange("name", oldName, this.name); + + } + + + /** + * @return true if WARs should be unpacked on deployment. + */ + public boolean isUnpackWARs() { + return unpackWARs; + } + + + /** + * Unpack WARs flag mutator. + * + * @param unpackWARs true to unpack WARs on deployment + */ + public void setUnpackWARs(boolean unpackWARs) { + this.unpackWARs = unpackWARs; + } + + + /** + * @return host work directory base. + */ + public String getWorkDir() { + return workDir; + } + + + /** + * Set host work directory base. + * + * @param workDir the new base work folder for this host + */ + public void setWorkDir(String workDir) { + this.workDir = workDir; + } + + + /** + * @return the regular expression that defines the files and directories in the host's {@link #getAppBase} that will + * be ignored by the automatic deployment process. + */ + @Override + public String getDeployIgnore() { + if (deployIgnore == null) { + return null; + } + return this.deployIgnore.toString(); + } + + + /** + * @return the compiled regular expression that defines the files and directories in the host's {@link #getAppBase} + * that will be ignored by the automatic deployment process. + */ + @Override + public Pattern getDeployIgnorePattern() { + return this.deployIgnore; + } + + + /** + * Set the regular expression that defines the files and directories in the host's {@link #getAppBase} that will be + * ignored by the automatic deployment process. + * + * @param deployIgnore the regexp + */ + @Override + public void setDeployIgnore(String deployIgnore) { + String oldDeployIgnore; + if (this.deployIgnore == null) { + oldDeployIgnore = null; + } else { + oldDeployIgnore = this.deployIgnore.toString(); + } + if (deployIgnore == null) { + this.deployIgnore = null; + } else { + this.deployIgnore = Pattern.compile(deployIgnore); + } + support.firePropertyChange("deployIgnore", oldDeployIgnore, deployIgnore); + } + + + /** + * @return true if a webapp start should fail if a Servlet startup fails + */ + public boolean isFailCtxIfServletStartFails() { + return failCtxIfServletStartFails; + } + + + /** + * Change the behavior of Servlet startup errors on web application starts. + * + * @param failCtxIfServletStartFails false to ignore errors on Servlets which are stated when the web + * application starts + */ + public void setFailCtxIfServletStartFails(boolean failCtxIfServletStartFails) { + boolean oldFailCtxIfServletStartFails = this.failCtxIfServletStartFails; + this.failCtxIfServletStartFails = failCtxIfServletStartFails; + support.firePropertyChange("failCtxIfServletStartFails", oldFailCtxIfServletStartFails, + failCtxIfServletStartFails); + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add an alias name that should be mapped to this same Host. + * + * @param alias The alias to be added + */ + @Override + public void addAlias(String alias) { + + alias = alias.toLowerCase(Locale.ENGLISH); + + synchronized (aliasesLock) { + // Skip duplicate aliases + for (String s : aliases) { + if (s.equals(alias)) { + return; + } + } + // Add this alias to the list + String newAliases[] = Arrays.copyOf(aliases, aliases.length + 1); + newAliases[aliases.length] = alias; + aliases = newAliases; + } + // Inform interested listeners + fireContainerEvent(ADD_ALIAS_EVENT, alias); + + } + + + /** + * Add a child Container, only if the proposed child is an implementation of Context. + * + * @param child Child container to be added + */ + @Override + public void addChild(Container child) { + + if (!(child instanceof Context)) { + throw new IllegalArgumentException(sm.getString("standardHost.notContext")); + } + + child.addLifecycleListener(new MemoryLeakTrackingListener()); + + // Avoid NPE for case where Context is defined in server.xml with only a + // docBase + Context context = (Context) child; + if (context.getPath() == null) { + ContextName cn = new ContextName(context.getDocBase(), true); + context.setPath(cn.getPath()); + } + + super.addChild(child); + + } + + + /** + * Used to ensure the regardless of {@link Context} implementation, a record is kept of the class loader used every + * time a context starts. + */ + private class MemoryLeakTrackingListener implements LifecycleListener { + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getType().equals(AFTER_START_EVENT)) { + if (event.getSource() instanceof Context) { + Context context = ((Context) event.getSource()); + childClassLoaders.put(context.getLoader().getClassLoader(), + context.getServletContext().getContextPath()); + } + } + } + } + + + /** + * Attempt to identify the contexts that have a class loader memory leak. This is usually triggered on context + * reload. Note: This method attempts to force a full garbage collection. This should be used with extreme caution + * on a production system. + * + * @return a list of possibly leaking contexts + */ + public String[] findReloadedContextMemoryLeaks() { + + System.gc(); + + List result = new ArrayList<>(); + + for (Map.Entry entry : childClassLoaders.entrySet()) { + ClassLoader cl = entry.getKey(); + if (cl instanceof WebappClassLoaderBase) { + if (!((WebappClassLoaderBase) cl).getState().isAvailable()) { + result.add(entry.getValue()); + } + } + } + + return result.toArray(new String[0]); + } + + /** + * @return the set of alias names for this Host. If none are defined, a zero length array is returned. + */ + @Override + public String[] findAliases() { + synchronized (aliasesLock) { + return this.aliases; + } + } + + + /** + * Remove the specified alias name from the aliases for this Host. + * + * @param alias Alias name to be removed + */ + @Override + public void removeAlias(String alias) { + + alias = alias.toLowerCase(Locale.ENGLISH); + + synchronized (aliasesLock) { + + // Make sure this alias is currently present + int n = -1; + for (int i = 0; i < aliases.length; i++) { + if (aliases[i].equals(alias)) { + n = i; + break; + } + } + if (n < 0) { + return; + } + + // Remove the specified alias + int j = 0; + String results[] = new String[aliases.length - 1]; + for (int i = 0; i < aliases.length; i++) { + if (i != n) { + results[j++] = aliases[i]; + } + } + aliases = results; + + } + + // Inform interested listeners + fireContainerEvent(REMOVE_ALIAS_EVENT, alias); + + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + // Set error report valve + String errorValve = getErrorReportValveClass(); + if ((errorValve != null) && (!errorValve.equals(""))) { + try { + boolean found = false; + Valve[] valves = getPipeline().getValves(); + for (Valve valve : valves) { + if (errorValve.equals(valve.getClass().getName())) { + found = true; + break; + } + } + if (!found) { + Valve valve = ErrorReportValve.class.getName().equals(errorValve) ? new ErrorReportValve() : + (Valve) Class.forName(errorValve).getConstructor().newInstance(); + getPipeline().addValve(valve); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardHost.invalidErrorReportValveClass", errorValve), t); + } + } + super.startInternal(); + } + + + // -------------------- JMX -------------------- + /** + * @return the MBean Names of the Valves associated with this Host + * + * @exception Exception if an MBean cannot be created or registered + */ + public String[] getValveNames() throws Exception { + Valve[] valves = this.getPipeline().getValves(); + String[] mbeanNames = new String[valves.length]; + for (int i = 0; i < valves.length; i++) { + if (valves[i] instanceof JmxEnabled) { + ObjectName oname = ((JmxEnabled) valves[i]).getObjectName(); + if (oname != null) { + mbeanNames[i] = oname.toString(); + } + } + } + + return mbeanNames; + } + + public String[] getAliases() { + synchronized (aliasesLock) { + return aliases; + } + } + + @Override + protected String getObjectNameKeyProperties() { + + StringBuilder keyProperties = new StringBuilder("type=Host"); + keyProperties.append(getMBeanKeyProperties()); + + return keyProperties.toString(); + } + +} diff --git a/java/org/apache/catalina/core/StandardHostValve.java b/java/org/apache/catalina/core/StandardHostValve.java new file mode 100644 index 0000000..dc96643 --- /dev/null +++ b/java/org/apache/catalina/core/StandardHostValve.java @@ -0,0 +1,378 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.ClientAbortException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; +import org.apache.coyote.ActionCode; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.res.StringManager; + +/** + * Valve that implements the default basic behavior for the StandardHost container implementation. + *

    + * USAGE CONSTRAINT: This implementation is likely to be useful only when processing HTTP requests. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +final class StandardHostValve extends ValveBase { + + private static final Log log = LogFactory.getLog(StandardHostValve.class); + private static final StringManager sm = StringManager.getManager(StandardHostValve.class); + + // Saves a call to getClassLoader() on very request. Under high load these + // calls took just long enough to appear as a hot spot (although a very + // minor one) in a profiler. + private static final ClassLoader MY_CLASSLOADER = StandardHostValve.class.getClassLoader(); + + // ------------------------------------------------------ Constructor + + StandardHostValve() { + super(true); + } + + + // --------------------------------------------------------- Public Methods + + /** + * Select the appropriate child Context to process this request, based on the specified request URI. If no matching + * Context can be found, return an appropriate HTTP error. + * + * @param request Request to be processed + * @param response Response to be produced + * + * @exception IOException if an input/output error occurred + * @exception ServletException if a servlet error occurred + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + // Select the Context to be used for this Request + Context context = request.getContext(); + if (context == null) { + // Don't overwrite an existing error + if (!response.isError()) { + response.sendError(404); + } + return; + } + + if (request.isAsyncSupported()) { + request.setAsyncSupported(context.getPipeline().isAsyncSupported()); + } + + boolean asyncAtStart = request.isAsync(); + + try { + context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); + + if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) { + // Don't fire listeners during async processing (the listener + // fired for the request that called startAsync()). + // If a request init listener throws an exception, the request + // is aborted. + return; + } + + // Ask this Context to process this request. Requests that are + // already in error must have been routed here to check for + // application defined error pages so DO NOT forward them to the + // application for processing. + try { + if (!response.isErrorReportRequired()) { + context.getPipeline().getFirst().invoke(request, response); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + container.getLogger().error(sm.getString("standardHostValve.exception", request.getRequestURI()), t); + // If a new error occurred while trying to report a previous + // error allow the original error to be reported. + if (!response.isErrorReportRequired()) { + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + throwable(request, response, t); + } + } + + // Now that the request/response pair is back under container + // control lift the suspension so that the error handling can + // complete and/or the container can flush any remaining data + response.setSuspended(false); + + Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + + // Protect against NPEs if the context was destroyed during a + // long running request. + if (!context.getState().isAvailable()) { + return; + } + + // Look for (and render if found) an application level error page + if (response.isErrorReportRequired()) { + // If an error has occurred that prevents further I/O, don't waste time + // producing an error report that will never be read + AtomicBoolean result = new AtomicBoolean(false); + response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result); + if (result.get()) { + if (t != null) { + throwable(request, response, t); + } else { + status(request, response); + } + } + } + + if (!request.isAsync() && !asyncAtStart) { + context.fireRequestDestroyEvent(request.getRequest()); + } + } finally { + // Access a session (if present) to update last accessed time, based + // on a strict interpretation of the specification + if (context.getAlwaysAccessSession()) { + request.getSession(false); + } + + context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); + } + } + + + // -------------------------------------------------------- Private Methods + + /** + * Handle the HTTP status code (and corresponding message) generated while processing the specified Request to + * produce the specified Response. Any exceptions that occur during generation of the error report are logged and + * swallowed. + * + * @param request The request being processed + * @param response The response being generated + */ + private void status(Request request, Response response) { + + int statusCode = response.getStatus(); + + // Handle a custom error page for this status code + Context context = request.getContext(); + if (context == null) { + return; + } + + /* + * Only look for error pages when isError() is set. isError() is set when response.sendError() is invoked. This + * allows custom error pages without relying on default from web.xml. + */ + if (!response.isError()) { + return; + } + + ErrorPage errorPage = context.findErrorPage(statusCode); + if (errorPage == null) { + // Look for a default error page + errorPage = context.findErrorPage(0); + } + if (errorPage != null && response.isErrorReportRequired()) { + response.setAppCommitted(false); + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, Integer.valueOf(statusCode)); + + String message = response.getMessage(); + if (message == null) { + message = ""; + } + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); + request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, errorPage.getLocation()); + request.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.ERROR); + + + Wrapper wrapper = request.getWrapper(); + if (wrapper != null) { + request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, wrapper.getName()); + } + request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); + if (custom(request, response, errorPage)) { + response.setErrorReported(); + try { + response.finishResponse(); + } catch (ClientAbortException e) { + // Ignore + } catch (IOException e) { + container.getLogger().warn(sm.getString("standardHostValve.exception", errorPage), e); + } + } + } + } + + + /** + * Handle the specified Throwable encountered while processing the specified Request to produce the specified + * Response. Any exceptions that occur during generation of the exception report are logged and swallowed. + * + * @param request The request being processed + * @param response The response being generated + * @param throwable The exception that occurred (which possibly wraps a root cause exception + */ + @SuppressWarnings("deprecation") + protected void throwable(Request request, Response response, Throwable throwable) { + Context context = request.getContext(); + if (context == null) { + return; + } + + Throwable realError = throwable; + + if (realError instanceof ServletException) { + realError = ((ServletException) realError).getRootCause(); + if (realError == null) { + realError = throwable; + } + } + + // If this is an aborted request from a client just log it and return + if (realError instanceof ClientAbortException) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardHost.clientAbort", realError.getCause().getMessage())); + } + return; + } + + ErrorPage errorPage = context.findErrorPage(throwable); + if ((errorPage == null) && (realError != throwable)) { + errorPage = context.findErrorPage(realError); + } + + if (errorPage != null) { + if (response.setErrorReported()) { + response.setAppCommitted(false); + request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, errorPage.getLocation()); + request.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.ERROR); + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, + Integer.valueOf(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, throwable.getMessage()); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, realError); + Wrapper wrapper = request.getWrapper(); + if (wrapper != null) { + request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, wrapper.getName()); + } + request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, realError.getClass()); + if (custom(request, response, errorPage)) { + try { + response.finishResponse(); + } catch (IOException e) { + container.getLogger().warn(sm.getString("standardHostValve.exception", errorPage), e); + } + } + } + } else { + /* + * A custom error-page has not been defined for the exception that was thrown during request processing. + * Set the status to 500 if an error status has not already been set and check for custom error-page for + * the status. + */ + if (response.getStatus() < HttpServletResponse.SC_BAD_REQUEST) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + // The response is an error + response.setError(); + + status(request, response); + } + } + + + /** + * Handle an HTTP status code or Java exception by forwarding control to the location included in the specified + * errorPage object. It is assumed that the caller has already recorded any request attributes that are to be + * forwarded to this page. Return true if we successfully utilized the specified error page location, + * or false if the default error report should be rendered. + * + * @param request The request being processed + * @param response The response being generated + * @param errorPage The errorPage directive we are obeying + */ + private boolean custom(Request request, Response response, ErrorPage errorPage) { + + if (container.getLogger().isTraceEnabled()) { + container.getLogger().trace("Processing " + errorPage); + } + + try { + // Forward control to the specified location + ServletContext servletContext = request.getContext().getServletContext(); + RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage.getLocation()); + + if (rd == null) { + container.getLogger() + .error(sm.getString("standardHostValve.customStatusFailed", errorPage.getLocation())); + return false; + } + + if (response.isCommitted()) { + // Response is committed - including the error page is the + // best we can do + rd.include(request.getRequest(), response.getResponse()); + + // Ensure the combined incomplete response and error page is + // written to the client + try { + response.flushBuffer(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + + // Now close immediately as an additional signal to the client + // that something went wrong + response.getCoyoteResponse().action(ActionCode.CLOSE_NOW, + request.getAttribute(RequestDispatcher.ERROR_EXCEPTION)); + } else { + // Reset the response (keeping the real error code and message) + response.resetBuffer(true); + response.setContentLength(-1); + + rd.forward(request.getRequest(), response.getResponse()); + + // If we forward, the response is suspended again + response.setSuspended(false); + } + + // Indicate that we have successfully processed this custom page + return true; + + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // Report our failure to process this custom page + container.getLogger().error(sm.getString("standardHostValve.exception", errorPage), t); + return false; + } + } +} diff --git a/java/org/apache/catalina/core/StandardPipeline.java b/java/org/apache/catalina/core/StandardPipeline.java new file mode 100644 index 0000000..2158a19 --- /dev/null +++ b/java/org/apache/catalina/core/StandardPipeline.java @@ -0,0 +1,459 @@ +/* + * 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.catalina.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.management.ObjectName; + +import org.apache.catalina.Contained; +import org.apache.catalina.Container; +import org.apache.catalina.JmxEnabled; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Valve; +import org.apache.catalina.util.LifecycleBase; +import org.apache.catalina.util.ToStringUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Standard implementation of a processing Pipeline that will invoke a series of Valves that have been configured + * to be called in order. This implementation can be used for any type of Container. IMPLEMENTATION WARNING - + * This implementation assumes that no calls to addValve() or removeValve are allowed while a + * request is currently being processed. Otherwise, the mechanism by which per-thread state is maintained will need to + * be modified. + * + * @author Craig R. McClanahan + */ +public class StandardPipeline extends LifecycleBase implements Pipeline { + + private static final Log log = LogFactory.getLog(StandardPipeline.class); + private static final StringManager sm = StringManager.getManager(StandardPipeline.class); + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new StandardPipeline instance with no associated Container. + */ + public StandardPipeline() { + + this(null); + + } + + + /** + * Construct a new StandardPipeline instance that is associated with the specified Container. + * + * @param container The container we should be associated with + */ + public StandardPipeline(Container container) { + + super(); + setContainer(container); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The basic Valve (if any) associated with this Pipeline. + */ + protected Valve basic = null; + + + /** + * The Container with which this Pipeline is associated. + */ + protected Container container = null; + + + /** + * The first valve associated with this Pipeline. + */ + protected Valve first = null; + + + // --------------------------------------------------------- Public Methods + + @Override + public boolean isAsyncSupported() { + Valve valve = (first != null) ? first : basic; + boolean supported = true; + while (supported && valve != null) { + supported = supported & valve.isAsyncSupported(); + valve = valve.getNext(); + } + return supported; + } + + + @Override + public void findNonAsyncValves(Set result) { + Valve valve = (first != null) ? first : basic; + while (valve != null) { + if (!valve.isAsyncSupported()) { + result.add(valve.getClass().getName()); + } + valve = valve.getNext(); + } + } + + + // ------------------------------------------------------ Contained Methods + + /** + * Return the Container with which this Pipeline is associated. + */ + @Override + public Container getContainer() { + return this.container; + } + + + /** + * Set the Container with which this Pipeline is associated. + * + * @param container The new associated container + */ + @Override + public void setContainer(Container container) { + this.container = container; + } + + + @Override + protected void initInternal() { + // NOOP + } + + + /** + * Start {@link Valve}s) in this pipeline and implement the requirements of {@link LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + // Start the Valves in our pipeline (including the basic), if any + Valve current = first; + if (current == null) { + current = basic; + } + while (current != null) { + if (current instanceof Lifecycle) { + ((Lifecycle) current).start(); + } + current = current.getNext(); + } + + setState(LifecycleState.STARTING); + } + + + /** + * Stop {@link Valve}s) in this pipeline and implement the requirements of {@link LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + setState(LifecycleState.STOPPING); + + // Stop the Valves in our pipeline (including the basic), if any + Valve current = first; + if (current == null) { + current = basic; + } + while (current != null) { + if (current instanceof Lifecycle) { + ((Lifecycle) current).stop(); + } + current = current.getNext(); + } + } + + + @Override + protected void destroyInternal() { + Valve[] valves = getValves(); + for (Valve valve : valves) { + removeValve(valve); + } + } + + + /** + * Return a String representation of this component. + */ + @Override + public String toString() { + return ToStringUtil.toString(this); + } + + + // ------------------------------------------------------- Pipeline Methods + + + /** + *

    + * Return the Valve instance that has been distinguished as the basic Valve for this Pipeline (if any). + */ + @Override + public Valve getBasic() { + return this.basic; + } + + + /** + *

    + * Set the Valve instance that has been distinguished as the basic Valve for this Pipeline (if any). Prior to + * setting the basic Valve, the Valve's setContainer() will be called, if it implements + * Contained, with the owning Container as an argument. The method may throw an + * IllegalArgumentException if this Valve chooses not to be associated with this Container, or + * IllegalStateException if it is already associated with a different Container. + *

    + * + * @param valve Valve to be distinguished as the basic Valve + */ + @Override + public void setBasic(Valve valve) { + + // Change components if necessary + Valve oldBasic = this.basic; + if (oldBasic == valve) { + return; + } + + // Stop the old component if necessary + if (oldBasic != null) { + if (getState().isAvailable() && (oldBasic instanceof Lifecycle)) { + try { + ((Lifecycle) oldBasic).stop(); + } catch (LifecycleException e) { + log.error(sm.getString("standardPipeline.basic.stop"), e); + } + } + if (oldBasic instanceof Contained) { + try { + ((Contained) oldBasic).setContainer(null); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + } + } + + // Start the new component if necessary + if (valve == null) { + return; + } + if (valve instanceof Contained) { + ((Contained) valve).setContainer(this.container); + } + if (getState().isAvailable() && valve instanceof Lifecycle) { + try { + ((Lifecycle) valve).start(); + } catch (LifecycleException e) { + log.error(sm.getString("standardPipeline.basic.start"), e); + return; + } + } + + // Update the pipeline + Valve current = first; + while (current != null) { + if (current.getNext() == oldBasic) { + current.setNext(valve); + break; + } + current = current.getNext(); + } + + this.basic = valve; + + } + + + /** + *

    + * Add a new Valve to the end of the pipeline associated with this Container. Prior to adding the Valve, the Valve's + * setContainer() method will be called, if it implements Contained, with the owning + * Container as an argument. The method may throw an IllegalArgumentException if this Valve chooses not + * to be associated with this Container, or IllegalStateException if it is already associated with a + * different Container. + *

    + * + * @param valve Valve to be added + * + * @exception IllegalArgumentException if this Container refused to accept the specified Valve + * @exception IllegalArgumentException if the specified Valve refuses to be associated with this Container + * @exception IllegalStateException if the specified Valve is already associated with a different Container + */ + @Override + public void addValve(Valve valve) { + + // Validate that we can add this Valve + if (valve instanceof Contained) { + ((Contained) valve).setContainer(this.container); + } + + // Start the new component if necessary + if (getState().isAvailable()) { + if (valve instanceof Lifecycle) { + try { + ((Lifecycle) valve).start(); + } catch (LifecycleException e) { + log.error(sm.getString("standardPipeline.valve.start"), e); + } + } + } + + // Add this Valve to the set associated with this Pipeline + if (first == null) { + first = valve; + valve.setNext(basic); + } else { + Valve current = first; + while (current != null) { + if (current.getNext() == basic) { + current.setNext(valve); + valve.setNext(basic); + break; + } + current = current.getNext(); + } + } + + container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve); + } + + + /** + * Return the set of Valves in the pipeline associated with this Container, including the basic Valve (if any). If + * there are no such Valves, a zero-length array is returned. + */ + @Override + public Valve[] getValves() { + + List valveList = new ArrayList<>(); + Valve current = first; + if (current == null) { + current = basic; + } + while (current != null) { + valveList.add(current); + current = current.getNext(); + } + + return valveList.toArray(new Valve[0]); + + } + + public ObjectName[] getValveObjectNames() { + + List valveList = new ArrayList<>(); + Valve current = first; + if (current == null) { + current = basic; + } + while (current != null) { + if (current instanceof JmxEnabled) { + valveList.add(((JmxEnabled) current).getObjectName()); + } + current = current.getNext(); + } + + return valveList.toArray(new ObjectName[0]); + + } + + /** + * Remove the specified Valve from the pipeline associated with this Container, if it is found; otherwise, do + * nothing. If the Valve is found and removed, the Valve's setContainer(null) method will be called if + * it implements Contained. + * + * @param valve Valve to be removed + */ + @Override + public void removeValve(Valve valve) { + + Valve current; + if (first == valve) { + first = first.getNext(); + current = null; + } else { + current = first; + } + while (current != null) { + if (current.getNext() == valve) { + current.setNext(valve.getNext()); + break; + } + current = current.getNext(); + } + + if (first == basic) { + first = null; + } + + if (valve instanceof Contained) { + ((Contained) valve).setContainer(null); + } + + if (valve instanceof Lifecycle) { + // Stop this valve if necessary + if (getState().isAvailable()) { + try { + ((Lifecycle) valve).stop(); + } catch (LifecycleException e) { + log.error(sm.getString("standardPipeline.valve.stop"), e); + } + } + try { + ((Lifecycle) valve).destroy(); + } catch (LifecycleException e) { + log.error(sm.getString("standardPipeline.valve.destroy"), e); + } + } + + container.fireContainerEvent(Container.REMOVE_VALVE_EVENT, valve); + } + + + @Override + public Valve getFirst() { + if (first != null) { + return first; + } + + return basic; + } +} diff --git a/java/org/apache/catalina/core/StandardServer.java b/java/org/apache/catalina/core/StandardServer.java new file mode 100644 index 0000000..f84624b --- /dev/null +++ b/java/org/apache/catalina/core/StandardServer.java @@ -0,0 +1,1121 @@ +/* + * 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.catalina.core; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.security.AccessControlException; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.catalina.mbeans.MBeanFactory; +import org.apache.catalina.startup.Catalina; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.catalina.util.ServerInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.StringCache; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.threads.TaskThreadFactory; + + +/** + * Standard implementation of the Server interface, available for use (but not required) when deploying and + * starting Catalina. + * + * @author Craig R. McClanahan + */ +public final class StandardServer extends LifecycleMBeanBase implements Server { + + private static final Log log = LogFactory.getLog(StandardServer.class); + private static final StringManager sm = StringManager.getManager(StandardServer.class); + + + // ------------------------------------------------------------ Constructor + + /** + * Construct a default instance of this class. + */ + public StandardServer() { + + super(); + + globalNamingResources = new NamingResourcesImpl(); + globalNamingResources.setContainer(this); + + if (isUseNaming()) { + namingContextListener = new NamingContextListener(); + addLifecycleListener(namingContextListener); + } else { + namingContextListener = null; + } + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Global naming resources context. + */ + private javax.naming.Context globalNamingContext = null; + + + /** + * Global naming resources. + */ + private NamingResourcesImpl globalNamingResources = null; + + + /** + * The naming context listener for this web application. + */ + private final NamingContextListener namingContextListener; + + + /** + * The port number on which we wait for shutdown commands. + */ + private int port = 8005; + + private int portOffset = 0; + + /** + * The address on which we wait for shutdown commands. + */ + private String address = "localhost"; + + + /** + * A random number generator that is only used if the shutdown command string is longer than 1024 + * characters. + */ + private Random random = null; + + + /** + * The set of Services associated with this Server. + */ + private Service[] services = new Service[0]; + + private final Lock servicesReadLock; + private final Lock servicesWriteLock; + { + ReentrantReadWriteLock servicesLock = new ReentrantReadWriteLock(); + servicesReadLock = servicesLock.readLock(); + servicesWriteLock = servicesLock.writeLock(); + } + + /** + * The shutdown command string we are looking for. + */ + private String shutdown = "SHUTDOWN"; + + + /** + * The property change support for this component. + */ + final PropertyChangeSupport support = new PropertyChangeSupport(this); + + private volatile boolean stopAwait = false; + + private Catalina catalina = null; + + private ClassLoader parentClassLoader = null; + + /** + * Thread that currently is inside our await() method. + */ + private volatile Thread awaitThread = null; + + /** + * Server socket that is used to wait for the shutdown command. + */ + private volatile ServerSocket awaitSocket = null; + + private File catalinaHome = null; + + private File catalinaBase = null; + + private final Object namingToken = new Object(); + + /** + * The number of threads available to process utility tasks in this service. + */ + private int utilityThreads = 2; + + /** + * The utility threads daemon flag. + */ + private boolean utilityThreadsAsDaemon = false; + + /** + * Utility executor with scheduling capabilities. + */ + private ScheduledThreadPoolExecutor utilityExecutor = null; + private final Object utilityExecutorLock = new Object(); + + /** + * Utility executor wrapper. + */ + private ScheduledExecutorService utilityExecutorWrapper = null; + + + /** + * Controller for the periodic lifecycle event. + */ + private ScheduledFuture periodicLifecycleEventFuture = null; + private ScheduledFuture monitorFuture; + + + /** + * The lifecycle event period in seconds. + */ + private int periodicEventDelay = 10; + + + // ------------------------------------------------------------- Properties + + @Override + public Object getNamingToken() { + return namingToken; + } + + + /** + * Return the global naming resources context. + */ + @Override + public javax.naming.Context getGlobalNamingContext() { + return this.globalNamingContext; + } + + + /** + * Set the global naming resources context. + * + * @param globalNamingContext The new global naming resource context + */ + public void setGlobalNamingContext(javax.naming.Context globalNamingContext) { + this.globalNamingContext = globalNamingContext; + } + + + /** + * Return the global naming resources. + */ + @Override + public NamingResourcesImpl getGlobalNamingResources() { + return this.globalNamingResources; + } + + + /** + * Set the global naming resources. + * + * @param globalNamingResources The new global naming resources + */ + @Override + public void setGlobalNamingResources(NamingResourcesImpl globalNamingResources) { + + NamingResourcesImpl oldGlobalNamingResources = this.globalNamingResources; + this.globalNamingResources = globalNamingResources; + this.globalNamingResources.setContainer(this); + support.firePropertyChange("globalNamingResources", oldGlobalNamingResources, this.globalNamingResources); + + } + + + /** + * Report the current Tomcat Server Release number + * + * @return Tomcat release identifier + */ + public String getServerInfo() { + return ServerInfo.getServerInfo(); + } + + + /** + * Return the current server built timestamp + * + * @return server built timestamp. + */ + public String getServerBuilt() { + return ServerInfo.getServerBuilt(); + } + + + /** + * Return the current server's version number. + * + * @return server's version number. + */ + public String getServerNumber() { + return ServerInfo.getServerNumber(); + } + + + /** + * Return the port number we listen to for shutdown commands. + */ + @Override + public int getPort() { + return this.port; + } + + + /** + * Set the port number we listen to for shutdown commands. + * + * @param port The new port number + */ + @Override + public void setPort(int port) { + this.port = port; + } + + + @Override + public int getPortOffset() { + return portOffset; + } + + + @Override + public void setPortOffset(int portOffset) { + if (portOffset < 0) { + throw new IllegalArgumentException( + sm.getString("standardServer.portOffset.invalid", Integer.valueOf(portOffset))); + } + this.portOffset = portOffset; + } + + + @Override + public int getPortWithOffset() { + // Non-positive port values have special meanings and the offset should + // not apply. + int port = getPort(); + if (port > 0) { + return port + getPortOffset(); + } else { + return port; + } + } + + + /** + * Return the address on which we listen to for shutdown commands. + */ + @Override + public String getAddress() { + return this.address; + } + + + /** + * Set the address on which we listen to for shutdown commands. + * + * @param address The new address + */ + @Override + public void setAddress(String address) { + this.address = address; + } + + /** + * Return the shutdown command string we are waiting for. + */ + @Override + public String getShutdown() { + return this.shutdown; + } + + + /** + * Set the shutdown command we are waiting for. + * + * @param shutdown The new shutdown command + */ + @Override + public void setShutdown(String shutdown) { + this.shutdown = shutdown; + } + + + /** + * Return the outer Catalina startup/shutdown component if present. + */ + @Override + public Catalina getCatalina() { + return catalina; + } + + + /** + * Set the outer Catalina startup/shutdown component if present. + */ + @Override + public void setCatalina(Catalina catalina) { + this.catalina = catalina; + } + + + @Override + public int getUtilityThreads() { + return utilityThreads; + } + + + /** + * Handles the special values. + */ + private static int getUtilityThreadsInternal(int utilityThreads) { + int result = utilityThreads; + if (result <= 0) { + result = Runtime.getRuntime().availableProcessors() + result; + if (result < 2) { + result = 2; + } + } + return result; + } + + + @Override + public void setUtilityThreads(int utilityThreads) { + // Use local copies to ensure thread safety + int oldUtilityThreads = this.utilityThreads; + if (getUtilityThreadsInternal(utilityThreads) < getUtilityThreadsInternal(oldUtilityThreads)) { + return; + } + this.utilityThreads = utilityThreads; + synchronized (utilityExecutorLock) { + if (oldUtilityThreads != utilityThreads && utilityExecutor != null) { + reconfigureUtilityExecutor(getUtilityThreadsInternal(utilityThreads)); + } + } + } + + + /* + * Callers must be holding utilityExecutorLock + */ + private void reconfigureUtilityExecutor(int threads) { + // The ScheduledThreadPoolExecutor doesn't use MaximumPoolSize, only CorePoolSize is available + if (utilityExecutor != null) { + utilityExecutor.setCorePoolSize(threads); + } else { + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(threads, + new TaskThreadFactory("Catalina-utility-", utilityThreadsAsDaemon, Thread.MIN_PRIORITY)); + scheduledThreadPoolExecutor.setKeepAliveTime(10, TimeUnit.SECONDS); + scheduledThreadPoolExecutor.setRemoveOnCancelPolicy(true); + scheduledThreadPoolExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + utilityExecutor = scheduledThreadPoolExecutor; + utilityExecutorWrapper = new org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor(utilityExecutor); + } + } + + + /** + * Get if the utility threads are daemon threads. + * + * @return the threads daemon flag + */ + public boolean getUtilityThreadsAsDaemon() { + return utilityThreadsAsDaemon; + } + + + /** + * Set the utility threads daemon flag. The default value is true. + * + * @param utilityThreadsAsDaemon the new thread daemon flag + */ + public void setUtilityThreadsAsDaemon(boolean utilityThreadsAsDaemon) { + this.utilityThreadsAsDaemon = utilityThreadsAsDaemon; + } + + + /** + * @return The period between two lifecycle events, in seconds + */ + public int getPeriodicEventDelay() { + return periodicEventDelay; + } + + + /** + * Set the new period between two lifecycle events in seconds. + * + * @param periodicEventDelay The period in seconds, negative or zero will disable events + */ + public void setPeriodicEventDelay(int periodicEventDelay) { + this.periodicEventDelay = periodicEventDelay; + } + + + // --------------------------------------------------------- Server Methods + + + /** + * Add a new Service to the set of defined Services. + * + * @param service The Service to be added + */ + @Override + public void addService(Service service) { + + service.setServer(this); + + servicesWriteLock.lock(); + try { + Service results[] = new Service[services.length + 1]; + System.arraycopy(services, 0, results, 0, services.length); + results[services.length] = service; + services = results; + } finally { + servicesWriteLock.unlock(); + } + + if (getState().isAvailable()) { + try { + service.start(); + } catch (LifecycleException e) { + // Ignore + } + } + + // Report this property change to interested listeners + support.firePropertyChange("service", null, service); + } + + public void stopAwait() { + stopAwait = true; + Thread t = awaitThread; + if (t != null) { + ServerSocket s = awaitSocket; + if (s != null) { + awaitSocket = null; + try { + s.close(); + } catch (IOException e) { + // Ignored + } + } + t.interrupt(); + try { + t.join(1000); + } catch (InterruptedException e) { + // Ignored + } + } + } + + /** + * Wait until a proper shutdown command is received, then return. This keeps the main thread alive - the thread pool + * listening for http connections is daemon threads. + */ + @Override + public void await() { + // Negative values - don't wait on port - tomcat is embedded or we just don't like ports + if (getPortWithOffset() == -2) { + // undocumented yet - for embedding apps that are around, alive. + return; + } + Thread currentThread = Thread.currentThread(); + if (getPortWithOffset() == -1) { + try { + awaitThread = currentThread; + while (!stopAwait) { + try { + Thread.sleep(10000); + } catch (InterruptedException ex) { + // continue and check the flag + } + } + } finally { + awaitThread = null; + } + return; + } + + // Set up a server socket to wait on + try { + awaitSocket = new ServerSocket(getPortWithOffset(), 1, InetAddress.getByName(address)); + } catch (IOException e) { + log.error(sm.getString("standardServer.awaitSocket.fail", address, String.valueOf(getPortWithOffset()), + String.valueOf(getPort()), String.valueOf(getPortOffset())), e); + return; + } + + try { + awaitThread = currentThread; + + // Loop waiting for a connection and a valid command + while (!stopAwait) { + ServerSocket serverSocket = awaitSocket; + if (serverSocket == null) { + break; + } + + // Wait for the next connection + Socket socket = null; + StringBuilder command = new StringBuilder(); + try { + InputStream stream; + long acceptStartTime = System.currentTimeMillis(); + try { + socket = serverSocket.accept(); + socket.setSoTimeout(10 * 1000); // Ten seconds + stream = socket.getInputStream(); + } catch (SocketTimeoutException ste) { + // This should never happen but bug 56684 suggests that + // it does. + log.warn(sm.getString("standardServer.accept.timeout", + Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste); + continue; + } catch (AccessControlException ace) { + log.warn(sm.getString("standardServer.accept.security"), ace); + continue; + } catch (IOException e) { + if (stopAwait) { + // Wait was aborted with socket.close() + break; + } + log.error(sm.getString("standardServer.accept.error"), e); + break; + } + + // Read a set of characters from the socket + int expected = 1024; // Cut off to avoid DoS attack + while (expected < shutdown.length()) { + if (random == null) { + random = new Random(); + } + expected += random.nextInt() % 1024; + } + while (expected > 0) { + int ch = -1; + try { + ch = stream.read(); + } catch (IOException e) { + log.warn(sm.getString("standardServer.accept.readError"), e); + ch = -1; + } + // Control character or EOF (-1) terminates loop + if (ch < 32 || ch == 127) { + break; + } + command.append((char) ch); + expected--; + } + } finally { + // Close the socket now that we are done with it + try { + if (socket != null) { + socket.close(); + } + } catch (IOException e) { + // Ignore + } + } + + // Match against our command string + boolean match = command.toString().equals(shutdown); + if (match) { + log.info(sm.getString("standardServer.shutdownViaPort")); + break; + } else { + log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString())); + } + } + } finally { + ServerSocket serverSocket = awaitSocket; + awaitThread = null; + awaitSocket = null; + + // Close the server socket and return + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + + + /** + * @return the specified Service (if it exists); otherwise return null. + * + * @param name Name of the Service to be returned + */ + @Override + public Service findService(String name) { + if (name == null) { + return null; + } + servicesReadLock.lock(); + + try { + for (Service service : services) { + if (name.equals(service.getName())) { + return service; + } + } + } finally { + servicesReadLock.unlock(); + } + + return null; + } + + + /** + * @return The array of Services defined within this Server. + */ + @Override + public Service[] findServices() { + servicesReadLock.lock(); + + try { + return services.clone(); + } finally { + servicesReadLock.unlock(); + } + } + + /** + * @return the JMX service names. + */ + public ObjectName[] getServiceNames() { + servicesReadLock.lock(); + + try { + ObjectName[] onames = new ObjectName[services.length]; + for (int i = 0; i < services.length; i++) { + onames[i] = ((StandardService) services[i]).getObjectName(); + } + return onames; + } finally { + servicesReadLock.unlock(); + } + } + + + /** + * Remove the specified Service from the set associated from this Server. + * + * @param service The Service to be removed + */ + @Override + public void removeService(Service service) { + + servicesWriteLock.lock(); + + try { + int j = -1; + for (int i = 0; i < services.length; i++) { + if (service == services[i]) { + j = i; + break; + } + } + if (j < 0) { + return; + } + int k = 0; + Service[] results = new Service[services.length - 1]; + for (int i = 0; i < services.length; i++) { + if (i != j) { + results[k++] = services[i]; + } + } + services = results; + } finally { + servicesWriteLock.unlock(); + } + + try { + service.stop(); + } catch (LifecycleException e) { + // Ignore + } + + // Report this property change to interested listeners + support.firePropertyChange("service", service, null); + } + + + @Override + public File getCatalinaBase() { + if (catalinaBase != null) { + return catalinaBase; + } + + catalinaBase = getCatalinaHome(); + return catalinaBase; + } + + + @Override + public void setCatalinaBase(File catalinaBase) { + this.catalinaBase = catalinaBase; + } + + + @Override + public File getCatalinaHome() { + return catalinaHome; + } + + + @Override + public void setCatalinaHome(File catalinaHome) { + this.catalinaHome = catalinaHome; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + public void addPropertyChangeListener(PropertyChangeListener listener) { + + support.addPropertyChangeListener(listener); + + } + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + public void removePropertyChangeListener(PropertyChangeListener listener) { + + support.removePropertyChangeListener(listener); + + } + + + /** + * Return a String representation of this component. + */ + @Override + public String toString() { + return "StandardServer[" + getPort() + ']'; + } + + + /** + * Write the configuration information for this entire Server out to the server.xml configuration file. + * + * @exception InstanceNotFoundException if the managed resource object cannot be found + * @exception MBeanException if the initializer of the object throws an exception, or + * persistence is not supported + * @exception javax.management.RuntimeOperationsException if an exception is reported by the persistence mechanism + */ + public synchronized void storeConfig() throws InstanceNotFoundException, MBeanException { + try { + // Note: Hard-coded domain used since this object is per Server/JVM + ObjectName sname = new ObjectName("Catalina:type=StoreConfig"); + MBeanServer server = Registry.getRegistry(null, null).getMBeanServer(); + if (server.isRegistered(sname)) { + server.invoke(sname, "storeConfig", null, null); + } else { + log.error(sm.getString("standardServer.storeConfig.notAvailable", sname)); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardServer.storeConfig.error"), t); + } + } + + + /** + * Write the configuration information for Context out to the specified configuration file. + * + * @param context the context which should save its configuration + * + * @exception InstanceNotFoundException if the managed resource object cannot be found + * @exception MBeanException if the initializer of the object throws an exception or + * persistence is not supported + * @exception javax.management.RuntimeOperationsException if an exception is reported by the persistence mechanism + */ + public synchronized void storeContext(Context context) throws InstanceNotFoundException, MBeanException { + try { + // Note: Hard-coded domain used since this object is per Server/JVM + ObjectName sname = new ObjectName("Catalina:type=StoreConfig"); + MBeanServer server = Registry.getRegistry(null, null).getMBeanServer(); + if (server.isRegistered(sname)) { + server.invoke(sname, "store", new Object[] { context }, new String[] { "java.lang.String" }); + } else { + log.error(sm.getString("standardServer.storeConfig.notAvailable", sname)); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardServer.storeConfig.contextError", context.getName()), t); + } + } + + + /** + * @return true if naming should be used. + */ + private boolean isUseNaming() { + boolean useNaming = true; + // Reading the "catalina.useNaming" environment variable + String useNamingProperty = System.getProperty("catalina.useNaming"); + if (useNamingProperty != null && useNamingProperty.equals("false")) { + useNaming = false; + } + return useNaming; + } + + + /** + * Start nested components ({@link Service}s) and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + fireLifecycleEvent(CONFIGURE_START_EVENT, null); + setState(LifecycleState.STARTING); + + // Initialize utility executor + synchronized (utilityExecutorLock) { + reconfigureUtilityExecutor(getUtilityThreadsInternal(utilityThreads)); + register(utilityExecutor, "type=UtilityExecutor"); + } + + globalNamingResources.start(); + + // Start our defined Services + for (Service service : findServices()) { + service.start(); + } + + if (periodicEventDelay > 0) { + monitorFuture = getUtilityExecutor().scheduleWithFixedDelay(this::startPeriodicLifecycleEvent, 0, 60, + TimeUnit.SECONDS); + } + } + + + private void startPeriodicLifecycleEvent() { + if (periodicLifecycleEventFuture == null || periodicLifecycleEventFuture.isDone()) { + if (periodicLifecycleEventFuture != null && periodicLifecycleEventFuture.isDone()) { + // There was an error executing the scheduled task, get it and log it + try { + periodicLifecycleEventFuture.get(); + } catch (InterruptedException | ExecutionException e) { + log.error(sm.getString("standardServer.periodicEventError"), e); + } + } + periodicLifecycleEventFuture = + getUtilityExecutor().scheduleAtFixedRate(() -> fireLifecycleEvent(PERIODIC_EVENT, null), + periodicEventDelay, periodicEventDelay, TimeUnit.SECONDS); + } + } + + + /** + * Stop nested components ({@link Service}s) and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that needs to be reported + */ + @Override + protected void stopInternal() throws LifecycleException { + + setState(LifecycleState.STOPPING); + + if (monitorFuture != null) { + monitorFuture.cancel(true); + monitorFuture = null; + } + if (periodicLifecycleEventFuture != null) { + periodicLifecycleEventFuture.cancel(false); + periodicLifecycleEventFuture = null; + } + + fireLifecycleEvent(CONFIGURE_STOP_EVENT, null); + + // Stop our defined Services + for (Service service : findServices()) { + service.stop(); + } + + synchronized (utilityExecutorLock) { + if (utilityExecutor != null) { + utilityExecutor.shutdownNow(); + unregister("type=UtilityExecutor"); + utilityExecutor = null; + } + } + + globalNamingResources.stop(); + + stopAwait(); + } + + /** + * Invoke a pre-startup initialization. This is used to allow connectors to bind to restricted ports under Unix + * operating environments. + */ + @Override + protected void initInternal() throws LifecycleException { + + super.initInternal(); + + // Register global String cache + // Note although the cache is global, if there are multiple Servers + // present in the JVM (may happen when embedding) then the same cache + // will be registered under multiple names + onameStringCache = register(new StringCache(), "type=StringCache"); + + // Register the MBeanFactory + MBeanFactory factory = new MBeanFactory(); + factory.setContainer(this); + onameMBeanFactory = register(factory, "type=MBeanFactory"); + + // Register the naming resources + globalNamingResources.init(); + + // Initialize our defined Services + for (Service service : findServices()) { + service.init(); + } + } + + @Override + protected void destroyInternal() throws LifecycleException { + // Destroy our defined Services + for (Service service : findServices()) { + service.destroy(); + } + + globalNamingResources.destroy(); + + unregister(onameMBeanFactory); + + unregister(onameStringCache); + + super.destroyInternal(); + } + + /** + * Return the parent class loader for this component. + */ + @Override + public ClassLoader getParentClassLoader() { + if (parentClassLoader != null) { + return parentClassLoader; + } + if (catalina != null) { + return catalina.getParentClassLoader(); + } + return ClassLoader.getSystemClassLoader(); + } + + /** + * Set the parent class loader for this server. + * + * @param parent The new parent class loader + */ + @Override + public void setParentClassLoader(ClassLoader parent) { + ClassLoader oldParentClassLoader = this.parentClassLoader; + this.parentClassLoader = parent; + support.firePropertyChange("parentClassLoader", oldParentClassLoader, this.parentClassLoader); + } + + + private ObjectName onameStringCache; + private ObjectName onameMBeanFactory; + + /** + * Obtain the MBean domain for this server. The domain is obtained using the following search order: + *
      + *
    1. Name of first {@link org.apache.catalina.Engine}.
    2. + *
    3. Name of first {@link Service}.
    4. + *
    + */ + @Override + protected String getDomainInternal() { + + String domain = null; + + Service[] services = findServices(); + if (services.length > 0) { + Service service = services[0]; + if (service != null) { + domain = service.getDomain(); + } + } + return domain; + } + + + @Override + protected String getObjectNameKeyProperties() { + return "type=Server"; + } + + @Override + public ScheduledExecutorService getUtilityExecutor() { + return utilityExecutorWrapper; + } +} diff --git a/java/org/apache/catalina/core/StandardService.java b/java/org/apache/catalina/core/StandardService.java new file mode 100644 index 0000000..461d8aa --- /dev/null +++ b/java/org/apache/catalina/core/StandardService.java @@ -0,0 +1,658 @@ +/* + * 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.catalina.core; + + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.ArrayList; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.Engine; +import org.apache.catalina.Executor; +import org.apache.catalina.JmxEnabled; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.mapper.Mapper; +import org.apache.catalina.mapper.MapperListener; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Standard implementation of the Service interface. The associated Container is generally an instance of + * Engine, but this is not required. + * + * @author Craig R. McClanahan + */ + +public class StandardService extends LifecycleMBeanBase implements Service { + + private static final Log log = LogFactory.getLog(StandardService.class); + private static final StringManager sm = StringManager.getManager(StandardService.class); + + + // ----------------------------------------------------- Instance Variables + + /** + * The name of this service. + */ + private String name = null; + + + /** + * The Server that owns this Service, if any. + */ + private Server server = null; + + /** + * The property change support for this component. + */ + protected final PropertyChangeSupport support = new PropertyChangeSupport(this); + + + /** + * The set of Connectors associated with this Service. + */ + protected Connector connectors[] = new Connector[0]; + private final ReadWriteLock connectorsLock = new ReentrantReadWriteLock(); + + /** + * The list of executors held by the service. + */ + protected final ArrayList executors = new ArrayList<>(); + private final ReadWriteLock executorsLock = new ReentrantReadWriteLock(); + + private Engine engine = null; + + private ClassLoader parentClassLoader = null; + + /** + * Mapper. + */ + protected final Mapper mapper = new Mapper(); + + + /** + * Mapper listener. + */ + protected final MapperListener mapperListener = new MapperListener(this); + + + private long gracefulStopAwaitMillis = 0; + + + // ------------------------------------------------------------- Properties + + public long getGracefulStopAwaitMillis() { + return gracefulStopAwaitMillis; + } + + + public void setGracefulStopAwaitMillis(long gracefulStopAwaitMillis) { + this.gracefulStopAwaitMillis = gracefulStopAwaitMillis; + } + + + @Override + public Mapper getMapper() { + return mapper; + } + + + @Override + public Engine getContainer() { + return engine; + } + + + @Override + public void setContainer(Engine engine) { + Engine oldEngine = this.engine; + if (oldEngine != null) { + oldEngine.setService(null); + } + this.engine = engine; + if (this.engine != null) { + this.engine.setService(this); + } + if (getState().isAvailable()) { + if (this.engine != null) { + try { + this.engine.start(); + } catch (LifecycleException e) { + log.error(sm.getString("standardService.engine.startFailed"), e); + } + } + // Restart MapperListener to pick up new engine. + try { + mapperListener.stop(); + } catch (LifecycleException e) { + log.error(sm.getString("standardService.mapperListener.stopFailed"), e); + } + try { + mapperListener.start(); + } catch (LifecycleException e) { + log.error(sm.getString("standardService.mapperListener.startFailed"), e); + } + if (oldEngine != null) { + try { + oldEngine.stop(); + } catch (LifecycleException e) { + log.error(sm.getString("standardService.engine.stopFailed"), e); + } + } + } + + // Report this property change to interested listeners + support.firePropertyChange("container", oldEngine, this.engine); + } + + + /** + * Return the name of this Service. + */ + @Override + public String getName() { + return name; + } + + + /** + * Set the name of this Service. + * + * @param name The new service name + */ + @Override + public void setName(String name) { + this.name = name; + } + + + /** + * Return the Server with which we are associated (if any). + */ + @Override + public Server getServer() { + return this.server; + } + + + /** + * Set the Server with which we are associated (if any). + * + * @param server The server that owns this Service + */ + @Override + public void setServer(Server server) { + this.server = server; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Add a new Connector to the set of defined Connectors, and associate it with this Service's Container. + * + * @param connector The Connector to be added + */ + @Override + public void addConnector(Connector connector) { + + Lock writeLock = connectorsLock.writeLock(); + writeLock.lock(); + try { + connector.setService(this); + Connector results[] = new Connector[connectors.length + 1]; + System.arraycopy(connectors, 0, results, 0, connectors.length); + results[connectors.length] = connector; + connectors = results; + } finally { + writeLock.unlock(); + } + + try { + if (getState().isAvailable()) { + connector.start(); + } + } catch (LifecycleException e) { + throw new IllegalArgumentException(sm.getString("standardService.connector.startFailed", connector), e); + } + + // Report this property change to interested listeners + support.firePropertyChange("connector", null, connector); + } + + + public ObjectName[] getConnectorNames() { + Lock readLock = connectorsLock.readLock(); + readLock.lock(); + try { + ObjectName results[] = new ObjectName[connectors.length]; + for (int i = 0; i < results.length; i++) { + results[i] = connectors[i].getObjectName(); + } + return results; + } finally { + readLock.unlock(); + } + } + + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + + /** + * Find and return the set of Connectors associated with this Service. + */ + @Override + public Connector[] findConnectors() { + Lock readLock = connectorsLock.readLock(); + readLock.lock(); + try { + // shallow copy + return connectors.clone(); + } finally { + readLock.unlock(); + } + } + + + /** + * Remove the specified Connector from the set associated from this Service. The removed Connector will also be + * disassociated from our Container. + * + * @param connector The Connector to be removed + */ + @Override + public void removeConnector(Connector connector) { + + Lock writeLock = connectorsLock.writeLock(); + writeLock.lock(); + try { + int j = -1; + for (int i = 0; i < connectors.length; i++) { + if (connector == connectors[i]) { + j = i; + break; + } + } + if (j < 0) { + return; + } + int k = 0; + Connector results[] = new Connector[connectors.length - 1]; + for (int i = 0; i < connectors.length; i++) { + if (i != j) { + results[k++] = connectors[i]; + } + } + connectors = results; + + } finally { + writeLock.unlock(); + } + + if (connector.getState().isAvailable()) { + try { + connector.stop(); + } catch (LifecycleException e) { + log.error(sm.getString("standardService.connector.stopFailed", connector), e); + } + } + connector.setService(null); + + // Report this property change to interested listeners + support.firePropertyChange("connector", connector, null); + } + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + public void removePropertyChangeListener(PropertyChangeListener listener) { + support.removePropertyChangeListener(listener); + } + + + /** + * Return a String representation of this component. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("StandardService["); + sb.append(getName()); + sb.append(']'); + return sb.toString(); + } + + + /** + * Adds a named executor to the service + * + * @param ex Executor + */ + @Override + public void addExecutor(Executor ex) { + boolean added = false; + executorsLock.writeLock().lock(); + try { + if (!executors.contains(ex)) { + added = true; + executors.add(ex); + } + } finally { + executorsLock.writeLock().unlock(); + } + if (added && getState().isAvailable()) { + try { + ex.start(); + } catch (LifecycleException x) { + log.error(sm.getString("standardService.executor.start"), x); + } + } + } + + + /** + * Retrieves all executors + * + * @return Executor[] + */ + @Override + public Executor[] findExecutors() { + executorsLock.readLock().lock(); + try { + return executors.toArray(new Executor[0]); + } finally { + executorsLock.readLock().unlock(); + } + } + + + /** + * Retrieves executor by name, null if not found + * + * @param executorName String + * + * @return Executor + */ + @Override + public Executor getExecutor(String executorName) { + executorsLock.readLock().lock(); + try { + for (Executor executor : executors) { + if (executorName.equals(executor.getName())) { + return executor; + } + } + } finally { + executorsLock.readLock().unlock(); + } + return null; + } + + + /** + * Removes an executor from the service + * + * @param ex Executor + */ + @Override + public void removeExecutor(Executor ex) { + boolean removed = false; + executorsLock.writeLock().lock(); + try { + removed = executors.remove(ex); + } finally { + executorsLock.writeLock().unlock(); + } + if (removed && getState().isAvailable()) { + try { + ex.stop(); + } catch (LifecycleException e) { + log.error(sm.getString("standardService.executor.stop"), e); + } + } + } + + + /** + * Start nested components ({@link Executor}s, {@link Connector}s and {@link Container}s) and implement the + * requirements of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + if (log.isInfoEnabled()) { + log.info(sm.getString("standardService.start.name", this.name)); + } + setState(LifecycleState.STARTING); + + // Start our defined Container first + if (engine != null) { + engine.start(); + } + + for (Executor executor : findExecutors()) { + executor.start(); + } + + mapperListener.start(); + + // Start our defined Connectors second + for (Connector connector : findConnectors()) { + // If it has already failed, don't try and start it + if (connector.getState() != LifecycleState.FAILED) { + connector.start(); + } + } + } + + + /** + * Stop nested components ({@link Executor}s, {@link Connector}s and {@link Container}s) and implement the + * requirements of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that needs to be reported + */ + @Override + protected void stopInternal() throws LifecycleException { + + Connector[] connectors = findConnectors(); + // Initiate a graceful stop for each connector + // This will only work if the bindOnInit==false which is not the + // default. + for (Connector connector : connectors) { + connector.getProtocolHandler().closeServerSocketGraceful(); + } + + // Wait for the graceful shutdown to complete + long waitMillis = gracefulStopAwaitMillis; + if (waitMillis > 0) { + for (Connector connector : connectors) { + waitMillis = connector.getProtocolHandler().awaitConnectionsClose(waitMillis); + } + } + + // Pause the connectors + for (Connector connector : connectors) { + connector.pause(); + } + + if (log.isInfoEnabled()) { + log.info(sm.getString("standardService.stop.name", this.name)); + } + setState(LifecycleState.STOPPING); + + // Stop our defined Container once the Connectors are all paused + if (engine != null) { + engine.stop(); + } + + // Now stop the connectors + for (Connector connector : connectors) { + if (!LifecycleState.STARTED.equals(connector.getState())) { + // Connectors only need stopping if they are currently + // started. They may have failed to start or may have been + // stopped (e.g. via a JMX call) + continue; + } + connector.stop(); + } + + // If the Server failed to start, the mapperListener won't have been + // started + if (mapperListener.getState() != LifecycleState.INITIALIZED) { + mapperListener.stop(); + } + + for (Executor executor : findExecutors()) { + executor.stop(); + } + } + + + /** + * Invoke a pre-startup initialization. This is used to allow connectors to bind to restricted ports under Unix + * operating environments. + */ + @Override + protected void initInternal() throws LifecycleException { + + super.initInternal(); + + if (engine != null) { + engine.init(); + } + + // Initialize any Executors + for (Executor executor : findExecutors()) { + if (executor instanceof JmxEnabled) { + ((JmxEnabled) executor).setDomain(getDomain()); + } + executor.init(); + } + + // Initialize mapper listener + mapperListener.init(); + + // Initialize our defined Connectors + for (Connector connector : findConnectors()) { + connector.init(); + } + } + + + @Override + protected void destroyInternal() throws LifecycleException { + mapperListener.destroy(); + + // Destroy our defined Connectors + for (Connector connector : findConnectors()) { + connector.destroy(); + } + + // Destroy any Executors + for (Executor executor : findExecutors()) { + executor.destroy(); + } + + if (engine != null) { + engine.destroy(); + } + + super.destroyInternal(); + } + + + /** + * Return the parent class loader for this component. + */ + @Override + public ClassLoader getParentClassLoader() { + if (parentClassLoader != null) { + return parentClassLoader; + } + if (server != null) { + return server.getParentClassLoader(); + } + return ClassLoader.getSystemClassLoader(); + } + + + /** + * Set the parent class loader for this server. + * + * @param parent The new parent class loader + */ + @Override + public void setParentClassLoader(ClassLoader parent) { + ClassLoader oldParentClassLoader = this.parentClassLoader; + this.parentClassLoader = parent; + support.firePropertyChange("parentClassLoader", oldParentClassLoader, this.parentClassLoader); + } + + + @Override + protected String getDomainInternal() { + String domain = null; + Container engine = getContainer(); + + // Use the engine name first + if (engine != null) { + domain = engine.getName(); + } + + // No engine or no engine name, use the service name + if (domain == null) { + domain = getName(); + } + + // No service name, return null which will trigger the use of the + // default + return domain; + } + + + @Override + public final String getObjectNameKeyProperties() { + return "type=Service"; + } +} diff --git a/java/org/apache/catalina/core/StandardThreadExecutor.java b/java/org/apache/catalina/core/StandardThreadExecutor.java new file mode 100644 index 0000000..162723d --- /dev/null +++ b/java/org/apache/catalina/core/StandardThreadExecutor.java @@ -0,0 +1,415 @@ +/* + * 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.catalina.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.catalina.Executor; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.threads.ResizableExecutor; +import org.apache.tomcat.util.threads.TaskQueue; +import org.apache.tomcat.util.threads.TaskThreadFactory; +import org.apache.tomcat.util.threads.ThreadPoolExecutor; + +public class StandardThreadExecutor extends LifecycleMBeanBase implements Executor, ExecutorService, ResizableExecutor { + + protected static final StringManager sm = StringManager.getManager(StandardThreadExecutor.class); + + // ---------------------------------------------- Properties + /** + * Default thread priority + */ + protected int threadPriority = Thread.NORM_PRIORITY; + + /** + * Run threads in daemon or non-daemon state + */ + protected boolean daemon = true; + + /** + * Default name prefix for the thread name + */ + protected String namePrefix = "tomcat-exec-"; + + /** + * max number of threads + */ + protected int maxThreads = 200; + + /** + * min number of threads + */ + protected int minSpareThreads = 25; + + /** + * idle time in milliseconds + */ + protected int maxIdleTime = 60000; + + /** + * The executor we use for this component + */ + protected ThreadPoolExecutor executor = null; + + /** + * the name of this thread pool + */ + protected String name; + + /** + * The maximum number of elements that can queue up before we reject them + */ + protected int maxQueueSize = Integer.MAX_VALUE; + + /** + * After a context is stopped, threads in the pool are renewed. To avoid renewing all threads at the same time, this + * delay is observed between 2 threads being renewed. + */ + protected long threadRenewalDelay = org.apache.tomcat.util.threads.Constants.DEFAULT_THREAD_RENEWAL_DELAY; + + private TaskQueue taskqueue = null; + + // ---------------------------------------------- Constructors + public StandardThreadExecutor() { + // empty constructor for the digester + } + + + // ---------------------------------------------- Public Methods + + /** + * Start the component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + taskqueue = new TaskQueue(maxQueueSize); + TaskThreadFactory tf = new TaskThreadFactory(namePrefix, daemon, getThreadPriority()); + executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS, + taskqueue, tf); + executor.setThreadRenewalDelay(threadRenewalDelay); + taskqueue.setParent(executor); + + setState(LifecycleState.STARTING); + } + + + /** + * Stop the component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that needs to be reported + */ + @Override + protected void stopInternal() throws LifecycleException { + + setState(LifecycleState.STOPPING); + if (executor != null) { + executor.shutdownNow(); + } + executor = null; + taskqueue = null; + } + + + @Override + public void execute(Runnable command) { + if (executor != null) { + // Note any RejectedExecutionException due to the use of TaskQueue + // will be handled by the o.a.t.u.threads.ThreadPoolExecutor + executor.execute(command); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + public void contextStopping() { + if (executor != null) { + executor.contextStopping(); + } + } + + public int getThreadPriority() { + return threadPriority; + } + + public boolean isDaemon() { + + return daemon; + } + + public String getNamePrefix() { + return namePrefix; + } + + public int getMaxIdleTime() { + return maxIdleTime; + } + + @Override + public int getMaxThreads() { + return maxThreads; + } + + public int getMinSpareThreads() { + return minSpareThreads; + } + + @Override + public String getName() { + return name; + } + + public void setThreadPriority(int threadPriority) { + this.threadPriority = threadPriority; + } + + public void setDaemon(boolean daemon) { + this.daemon = daemon; + } + + public void setNamePrefix(String namePrefix) { + this.namePrefix = namePrefix; + } + + public void setMaxIdleTime(int maxIdleTime) { + this.maxIdleTime = maxIdleTime; + if (executor != null) { + executor.setKeepAliveTime(maxIdleTime, TimeUnit.MILLISECONDS); + } + } + + public void setMaxThreads(int maxThreads) { + this.maxThreads = maxThreads; + if (executor != null) { + executor.setMaximumPoolSize(maxThreads); + } + } + + public void setMinSpareThreads(int minSpareThreads) { + this.minSpareThreads = minSpareThreads; + if (executor != null) { + executor.setCorePoolSize(minSpareThreads); + } + } + + public void setName(String name) { + this.name = name; + } + + public void setMaxQueueSize(int size) { + this.maxQueueSize = size; + } + + public int getMaxQueueSize() { + return maxQueueSize; + } + + public long getThreadRenewalDelay() { + return threadRenewalDelay; + } + + public void setThreadRenewalDelay(long threadRenewalDelay) { + this.threadRenewalDelay = threadRenewalDelay; + if (executor != null) { + executor.setThreadRenewalDelay(threadRenewalDelay); + } + } + + // Statistics from the thread pool + @Override + public int getActiveCount() { + return (executor != null) ? executor.getActiveCount() : 0; + } + + public long getCompletedTaskCount() { + return (executor != null) ? executor.getCompletedTaskCount() : 0; + } + + public int getCorePoolSize() { + return (executor != null) ? executor.getCorePoolSize() : 0; + } + + public int getLargestPoolSize() { + return (executor != null) ? executor.getLargestPoolSize() : 0; + } + + @Override + public int getPoolSize() { + return (executor != null) ? executor.getPoolSize() : 0; + } + + public int getQueueSize() { + return (executor != null) ? executor.getQueue().size() : -1; + } + + + @Override + public boolean resizePool(int corePoolSize, int maximumPoolSize) { + if (executor == null) { + return false; + } + + executor.setCorePoolSize(corePoolSize); + executor.setMaximumPoolSize(maximumPoolSize); + return true; + } + + + @Override + public boolean resizeQueue(int capacity) { + return false; + } + + + @Override + protected String getDomainInternal() { + // No way to navigate to Engine. Needs to have domain set. + return null; + } + + @Override + protected String getObjectNameKeyProperties() { + return "type=Executor,name=" + getName(); + } + + + @Override + public void shutdown() { + // Controlled by Lifecycle instead + } + + + @Override + public List shutdownNow() { + // Controlled by Lifecycle instead + return Collections.emptyList(); + } + + + @Override + public boolean isShutdown() { + if (executor != null) { + return executor.isShutdown(); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public boolean isTerminated() { + if (executor != null) { + return executor.isTerminated(); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return false; + } + + + @Override + public Future submit(Callable task) { + if (executor != null) { + return executor.submit(task); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public Future submit(Runnable task, T result) { + if (executor != null) { + return executor.submit(task, result); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public Future submit(Runnable task) { + if (executor != null) { + return executor.submit(task); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + if (executor != null) { + return executor.invokeAll(tasks); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + if (executor != null) { + return executor.invokeAll(tasks, timeout, unit); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + if (executor != null) { + return executor.invokeAny(tasks); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + if (executor != null) { + return executor.invokeAny(tasks, timeout, unit); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } +} diff --git a/java/org/apache/catalina/core/StandardVirtualThreadExecutor.java b/java/org/apache/catalina/core/StandardVirtualThreadExecutor.java new file mode 100644 index 0000000..9a5402d --- /dev/null +++ b/java/org/apache/catalina/core/StandardVirtualThreadExecutor.java @@ -0,0 +1,215 @@ +/* + * 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.catalina.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.catalina.Executor; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.threads.VirtualThreadExecutor; + +/** + * An executor that uses a new virtual thread for each task. + */ +public class StandardVirtualThreadExecutor extends LifecycleMBeanBase implements Executor, ExecutorService { + + private static final StringManager sm = StringManager.getManager(StandardVirtualThreadExecutor.class); + + private String name; + private java.util.concurrent.ExecutorService executor; + private String namePrefix = "tomcat-virt-"; + + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public String getNamePrefix() { + return namePrefix; + } + + public void setNamePrefix(String namePrefix) { + this.namePrefix = namePrefix; + } + + @Override + public void execute(Runnable command) { + if (executor == null) { + throw new IllegalStateException(sm.getString("standardVirtualThreadExecutor.notStarted")); + } else { + executor.execute(command); + } + } + + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + if (!JreCompat.isJre21Available()) { + throw new LifecycleException(sm.getString("standardVirtualThreadExecutor.noVirtualThreads")); + } + } + + @Override + protected void startInternal() throws LifecycleException { + executor = new VirtualThreadExecutor(getNamePrefix()); + setState(LifecycleState.STARTING); + } + + @Override + protected void stopInternal() throws LifecycleException { + executor = null; + setState(LifecycleState.STOPPING); + } + + @Override + protected String getDomainInternal() { + // No way to navigate to Engine. Needs to have domain set. + return null; + } + + @Override + protected String getObjectNameKeyProperties() { + return "type=Executor,name=" + getName(); + } + + @Override + public void shutdown() { + // Controlled by Lifecycle instead + } + + + @Override + public List shutdownNow() { + // Controlled by Lifecycle instead + return Collections.emptyList(); + } + + + @Override + public boolean isShutdown() { + if (executor != null) { + return executor.isShutdown(); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public boolean isTerminated() { + if (executor != null) { + return executor.isTerminated(); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return false; + } + + + @Override + public Future submit(Callable task) { + if (executor != null) { + return executor.submit(task); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public Future submit(Runnable task, T result) { + if (executor != null) { + return executor.submit(task, result); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public Future submit(Runnable task) { + if (executor != null) { + return executor.submit(task); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + if (executor != null) { + return executor.invokeAll(tasks); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + if (executor != null) { + return executor.invokeAll(tasks, timeout, unit); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + if (executor != null) { + return executor.invokeAny(tasks); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } + + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + if (executor != null) { + return executor.invokeAny(tasks, timeout, unit); + } else { + throw new IllegalStateException(sm.getString("standardThreadExecutor.notStarted")); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/core/StandardWrapper.java b/java/org/apache/catalina/core/StandardWrapper.java new file mode 100644 index 0000000..29070d4 --- /dev/null +++ b/java/org/apache/catalina/core/StandardWrapper.java @@ -0,0 +1,1497 @@ +/* + * 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.catalina.core; + +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.ListenerNotFoundException; +import javax.management.MBeanNotificationInfo; +import javax.management.Notification; +import javax.management.NotificationBroadcasterSupport; +import javax.management.NotificationEmitter; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectName; + +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.UnavailableException; +import jakarta.servlet.annotation.MultipartConfig; + +import org.apache.catalina.Container; +import org.apache.catalina.ContainerServlet; +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Wrapper; +import org.apache.catalina.security.SecurityUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.PeriodicEventListener; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.log.SystemLogHandler; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.modeler.Util; + +/** + * Standard implementation of the Wrapper interface that represents an individual servlet definition. No child + * Containers are allowed, and the parent Container must be a Context. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class StandardWrapper extends ContainerBase implements ServletConfig, Wrapper, NotificationEmitter { + + private final Log log = LogFactory.getLog(StandardWrapper.class); // must not be static + + protected static final String[] DEFAULT_SERVLET_METHODS = new String[] { "GET", "HEAD", "POST" }; + + // ----------------------------------------------------------- Constructors + + + /** + * Create a new StandardWrapper component with the default basic Valve. + */ + public StandardWrapper() { + + super(); + swValve = new StandardWrapperValve(); + pipeline.setBasic(swValve); + broadcaster = new NotificationBroadcasterSupport(); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The date and time at which this servlet will become available (in milliseconds since the epoch), or zero if the + * servlet is available. If this value equals Long.MAX_VALUE, the unavailability of this servlet is considered + * permanent. + */ + protected long available = 0L; + + /** + * The broadcaster that sends j2ee notifications. + */ + protected final NotificationBroadcasterSupport broadcaster; + + /** + * The count of allocations that are currently active. + */ + protected final AtomicInteger countAllocated = new AtomicInteger(0); + + + /** + * The facade associated with this wrapper. + */ + protected final StandardWrapperFacade facade = new StandardWrapperFacade(this); + + + /** + * The (single) possibly uninitialized instance of this servlet. + */ + protected volatile Servlet instance = null; + + + /** + * Flag that indicates if this instance has been initialized + */ + protected volatile boolean instanceInitialized = false; + + + /** + * The load-on-startup order value (negative value means load on first call) for this servlet. + */ + protected int loadOnStartup = -1; + + + /** + * Mappings associated with the wrapper. + */ + protected final ArrayList mappings = new ArrayList<>(); + + + /** + * The initialization parameters for this servlet, keyed by parameter name. + */ + protected HashMap parameters = new HashMap<>(); + + + /** + * The security role references for this servlet, keyed by role name used in the servlet. The corresponding value is + * the role name of the web application itself. + */ + protected HashMap references = new HashMap<>(); + + + /** + * The run-as identity for this servlet. + */ + protected String runAs = null; + + /** + * The notification sequence number. + */ + protected long sequenceNumber = 0; + + /** + * The fully qualified servlet class name for this servlet. + */ + protected String servletClass = null; + + + /** + * Are we unloading our servlet instance at the moment? + */ + protected volatile boolean unloading = false; + + + /** + * Wait time for servlet unload in ms. + */ + protected long unloadDelay = 2000; + + + /** + * True if this StandardWrapper is for the JspServlet + */ + protected boolean isJspServlet; + + + /** + * The ObjectName of the JSP monitoring mbean + */ + protected ObjectName jspMonitorON; + + + /** + * Should we swallow System.out + */ + protected boolean swallowOutput = false; + + // To support jmx attributes + protected StandardWrapperValve swValve; + protected long loadTime = 0; + protected int classLoadTime = 0; + + /** + * Multipart config + */ + protected MultipartConfigElement multipartConfigElement = null; + + /** + * Async support + */ + protected boolean asyncSupported = false; + + /** + * Enabled + */ + protected boolean enabled = true; + + private boolean overridable = false; + + /** + * Static class array used when the SecurityManager is turned on and Servlet.init is invoked. + */ + protected static Class[] classType = new Class[] { ServletConfig.class }; + + private final ReentrantReadWriteLock parametersLock = new ReentrantReadWriteLock(); + + private final ReentrantReadWriteLock mappingsLock = new ReentrantReadWriteLock(); + + private final ReentrantReadWriteLock referencesLock = new ReentrantReadWriteLock(); + + + // ------------------------------------------------------------- Properties + + @Override + public boolean isOverridable() { + return overridable; + } + + @Override + public void setOverridable(boolean overridable) { + this.overridable = overridable; + } + + /** + * Return the available date/time for this servlet, in milliseconds since the epoch. If this date/time is + * Long.MAX_VALUE, it is considered to mean that unavailability is permanent and any request for this servlet will + * return an SC_NOT_FOUND error. If this date/time is in the future, any request for this servlet will return an + * SC_SERVICE_UNAVAILABLE error. If it is zero, the servlet is currently available. + */ + @Override + public long getAvailable() { + return this.available; + } + + + /** + * Set the available date/time for this servlet, in milliseconds since the epoch. If this date/time is + * Long.MAX_VALUE, it is considered to mean that unavailability is permanent and any request for this servlet will + * return an SC_NOT_FOUND error. If this date/time is in the future, any request for this servlet will return an + * SC_SERVICE_UNAVAILABLE error. + * + * @param available The new available date/time + */ + @Override + public void setAvailable(long available) { + long oldAvailable = this.available; + if (available > System.currentTimeMillis()) { + this.available = available; + } else { + this.available = 0L; + } + support.firePropertyChange("available", Long.valueOf(oldAvailable), Long.valueOf(this.available)); + } + + + /** + * @return the number of active allocations of this servlet. + */ + public int getCountAllocated() { + return this.countAllocated.get(); + } + + + /** + * @return the load-on-startup order value (negative value means load on first call). + */ + @Override + public int getLoadOnStartup() { + + if (isJspServlet && loadOnStartup == -1) { + /* + * JspServlet must always be preloaded, because its instance is used during registerJMX (when registering + * the JSP monitoring mbean) + */ + return Integer.MAX_VALUE; + } else { + return this.loadOnStartup; + } + } + + + /** + * Set the load-on-startup order value (negative value means load on first call). + * + * @param value New load-on-startup value + */ + @Override + public void setLoadOnStartup(int value) { + + int oldLoadOnStartup = this.loadOnStartup; + this.loadOnStartup = value; + support.firePropertyChange("loadOnStartup", Integer.valueOf(oldLoadOnStartup), + Integer.valueOf(this.loadOnStartup)); + + } + + + /** + * Set the load-on-startup order value from a (possibly null) string. Per the specification, any missing or + * non-numeric value is converted to a zero, so that this servlet will still be loaded at startup time, but in an + * arbitrary order. + * + * @param value New load-on-startup value + */ + public void setLoadOnStartupString(String value) { + + try { + setLoadOnStartup(Integer.parseInt(value)); + } catch (NumberFormatException e) { + setLoadOnStartup(0); + } + } + + /** + * @return the load-on-startup value that was parsed + */ + public String getLoadOnStartupString() { + return Integer.toString(getLoadOnStartup()); + } + + + /** + * Set the parent Container of this Wrapper, but only if it is a Context. + * + * @param container Proposed parent Container + */ + @Override + public void setParent(Container container) { + + if ((container != null) && !(container instanceof Context)) { + throw new IllegalArgumentException(sm.getString("standardWrapper.notContext")); + } + if (container instanceof StandardContext) { + swallowOutput = ((StandardContext) container).getSwallowOutput(); + unloadDelay = ((StandardContext) container).getUnloadDelay(); + } + super.setParent(container); + + } + + + /** + * @return the run-as identity for this servlet. + */ + @Override + public String getRunAs() { + return this.runAs; + } + + + /** + * Set the run-as identity for this servlet. + * + * @param runAs New run-as identity value + */ + @Override + public void setRunAs(String runAs) { + + String oldRunAs = this.runAs; + this.runAs = runAs; + support.firePropertyChange("runAs", oldRunAs, this.runAs); + + } + + + /** + * @return the fully qualified servlet class name for this servlet. + */ + @Override + public String getServletClass() { + return this.servletClass; + } + + + /** + * Set the fully qualified servlet class name for this servlet. + * + * @param servletClass Servlet class name + */ + @Override + public void setServletClass(String servletClass) { + + String oldServletClass = this.servletClass; + this.servletClass = servletClass; + support.firePropertyChange("servletClass", oldServletClass, this.servletClass); + if (Constants.JSP_SERVLET_CLASS.equals(servletClass)) { + isJspServlet = true; + } + } + + + /** + * Set the name of this servlet. This is an alias for the normal Container.setName() method, and + * complements the getServletName() method required by the ServletConfig interface. + * + * @param name The new name of this servlet + */ + public void setServletName(String name) { + + setName(name); + + } + + + /** + * @return true if the Servlet has been marked unavailable. + */ + @Override + public boolean isUnavailable() { + + if (!isEnabled()) { + return true; + } else if (available == 0L) { + return false; + } else if (available <= System.currentTimeMillis()) { + available = 0L; + return false; + } else { + return true; + } + + } + + + @Override + public String[] getServletMethods() throws ServletException { + + instance = loadServlet(); + + Class servletClazz = instance.getClass(); + if (!jakarta.servlet.http.HttpServlet.class.isAssignableFrom(servletClazz)) { + return DEFAULT_SERVLET_METHODS; + } + + Set allow = new HashSet<>(); + allow.add("OPTIONS"); + + if (isJspServlet) { + allow.add("GET"); + allow.add("HEAD"); + allow.add("POST"); + } else { + allow.add("TRACE"); + + Method[] methods = getAllDeclaredMethods(servletClazz); + for (int i = 0; methods != null && i < methods.length; i++) { + Method m = methods[i]; + + if (m.getName().equals("doGet")) { + allow.add("GET"); + allow.add("HEAD"); + } else if (m.getName().equals("doPost")) { + allow.add("POST"); + } else if (m.getName().equals("doPut")) { + allow.add("PUT"); + } else if (m.getName().equals("doDelete")) { + allow.add("DELETE"); + } + } + } + + return allow.toArray(new String[0]); + } + + + /** + * @return the associated servlet instance. + */ + @Override + public Servlet getServlet() { + return instance; + } + + + /** + * Set the associated servlet instance. + */ + @Override + public void setServlet(Servlet servlet) { + instance = servlet; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Execute a periodic task, such as reloading, etc. This method will be invoked inside the classloading context of + * this container. Unexpected throwables will be caught and logged. + */ + @Override + public synchronized void backgroundProcess() { + super.backgroundProcess(); + + if (!getState().isAvailable()) { + return; + } + + if (getServlet() instanceof PeriodicEventListener) { + ((PeriodicEventListener) getServlet()).periodicEvent(); + } + } + + + /** + * Extract the root cause from a servlet exception. + * + * @param e The servlet exception + * + * @return the root cause of the Servlet exception + */ + public static Throwable getRootCause(ServletException e) { + Throwable rootCause = e; + Throwable rootCauseCheck = null; + // Extra aggressive rootCause finding + int loops = 0; + do { + loops++; + rootCauseCheck = rootCause.getCause(); + if (rootCauseCheck != null) { + rootCause = rootCauseCheck; + } + } while (rootCauseCheck != null && (loops < 20)); + return rootCause; + } + + + /** + * Refuse to add a child Container, because Wrappers are the lowest level of the Container hierarchy. + * + * @param child Child container to be added + */ + @Override + public void addChild(Container child) { + + throw new IllegalStateException(sm.getString("standardWrapper.notChild")); + + } + + + /** + * Add a new servlet initialization parameter for this servlet. + * + * @param name Name of this initialization parameter to add + * @param value Value of this initialization parameter to add + */ + @Override + public void addInitParameter(String name, String value) { + + parametersLock.writeLock().lock(); + try { + parameters.put(name, value); + } finally { + parametersLock.writeLock().unlock(); + } + fireContainerEvent("addInitParameter", name); + + } + + + /** + * Add a mapping associated with the Wrapper. + * + * @param mapping The new wrapper mapping + */ + @Override + public void addMapping(String mapping) { + + mappingsLock.writeLock().lock(); + try { + mappings.add(mapping); + } finally { + mappingsLock.writeLock().unlock(); + } + if (parent.getState().equals(LifecycleState.STARTED)) { + fireContainerEvent(ADD_MAPPING_EVENT, mapping); + } + + } + + + /** + * Add a new security role reference record to the set of records for this servlet. + * + * @param name Role name used within this servlet + * @param link Role name used within the web application + */ + @Override + public void addSecurityReference(String name, String link) { + + referencesLock.writeLock().lock(); + try { + references.put(name, link); + } finally { + referencesLock.writeLock().unlock(); + } + fireContainerEvent("addSecurityReference", name); + + } + + + /** + * Allocate an initialized instance of this Servlet that is ready to have its service() method called. + * + * @exception ServletException if the servlet init() method threw an exception + * @exception ServletException if a loading error occurs + */ + @Override + public Servlet allocate() throws ServletException { + + // If we are currently unloading this servlet, throw an exception + if (unloading) { + throw new ServletException(sm.getString("standardWrapper.unloading", getName())); + } + + boolean newInstance = false; + + // Load and initialize our instance if necessary + if (instance == null || !instanceInitialized) { + synchronized (this) { + if (instance == null) { + try { + if (log.isTraceEnabled()) { + log.trace("Allocating instance"); + } + instance = loadServlet(); + newInstance = true; + // Increment here to prevent a race condition + // with unload. Bug 43683, test case #3 + countAllocated.incrementAndGet(); + } catch (ServletException e) { + throw e; + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + throw new ServletException(sm.getString("standardWrapper.allocate"), e); + } + } + if (!instanceInitialized) { + initServlet(instance); + } + } + } + + if (log.isTraceEnabled()) { + log.trace(" Returning instance"); + } + // For new instances, count will have been incremented at the + // time of creation + if (!newInstance) { + countAllocated.incrementAndGet(); + } + return instance; + } + + + /** + * Decrement the allocation count for this servlet. + * + * @param servlet The servlet to be returned + * + * @exception ServletException if a deallocation error occurs + */ + @Override + public void deallocate(Servlet servlet) throws ServletException { + countAllocated.decrementAndGet(); + } + + + /** + * Return the value for the specified initialization parameter name, if any; otherwise return null. + * + * @param name Name of the requested initialization parameter + */ + @Override + public String findInitParameter(String name) { + + parametersLock.readLock().lock(); + try { + return parameters.get(name); + } finally { + parametersLock.readLock().unlock(); + } + + } + + + /** + * Return the names of all defined initialization parameters for this servlet. + */ + @Override + public String[] findInitParameters() { + + parametersLock.readLock().lock(); + try { + return parameters.keySet().toArray(new String[0]); + } finally { + parametersLock.readLock().unlock(); + } + + } + + + /** + * Return the mappings associated with this wrapper. + */ + @Override + public String[] findMappings() { + + mappingsLock.readLock().lock(); + try { + return mappings.toArray(new String[0]); + } finally { + mappingsLock.readLock().unlock(); + } + + } + + + /** + * Return the security role link for the specified security role reference name, if any; otherwise return + * null. + * + * @param name Security role reference used within this servlet + */ + @Override + public String findSecurityReference(String name) { + String reference = null; + + referencesLock.readLock().lock(); + try { + reference = references.get(name); + } finally { + referencesLock.readLock().unlock(); + } + + // If not specified on the Wrapper, check the Context + if (getParent() instanceof Context) { + Context context = (Context) getParent(); + if (reference != null) { + reference = context.findRoleMapping(reference); + } else { + reference = context.findRoleMapping(name); + } + } + + return reference; + } + + + /** + * Return the set of security role reference names associated with this servlet, if any; otherwise return a + * zero-length array. + */ + @Override + public String[] findSecurityReferences() { + + referencesLock.readLock().lock(); + try { + return references.keySet().toArray(new String[0]); + } finally { + referencesLock.readLock().unlock(); + } + + } + + + /** + * Load and initialize an instance of this servlet, if there is not already at least one initialized instance. This + * can be used, for example, to load servlets that are marked in the deployment descriptor to be loaded at server + * startup time. + *

    + * IMPLEMENTATION NOTE: Servlets whose classnames begin with org.apache.catalina. (so-called + * "container" servlets) are loaded by the same classloader that loaded this class, rather than the classloader for + * the current web application. This gives such classes access to Catalina internals, which are prevented for + * classes loaded for web applications. + * + * @exception ServletException if the servlet init() method threw an exception + * @exception ServletException if some other loading problem occurs + */ + @Override + public synchronized void load() throws ServletException { + instance = loadServlet(); + + if (!instanceInitialized) { + initServlet(instance); + } + + if (isJspServlet) { + StringBuilder oname = new StringBuilder(getDomain()); + + oname.append(":type=JspMonitor"); + + oname.append(getWebModuleKeyProperties()); + + oname.append(",name="); + oname.append(getName()); + + oname.append(getJ2EEKeyProperties()); + + try { + jspMonitorON = new ObjectName(oname.toString()); + Registry.getRegistry(null, null).registerComponent(instance, jspMonitorON, null); + } catch (Exception ex) { + log.warn(sm.getString("standardWrapper.jspMonitorError", instance)); + } + } + } + + + /** + * Load and initialize an instance of this servlet, if there is not already an initialized instance. This can be + * used, for example, to load servlets that are marked in the deployment descriptor to be loaded at server startup + * time. + * + * @return the loaded Servlet instance + * + * @throws ServletException for a Servlet load error + */ + public synchronized Servlet loadServlet() throws ServletException { + + // Nothing to do if we already have an instance or an instance pool + if (instance != null) { + return instance; + } + + PrintStream out = System.out; + if (swallowOutput) { + SystemLogHandler.startCapture(); + } + + Servlet servlet; + try { + long t1 = System.currentTimeMillis(); + // Complain if no servlet class has been specified + if (servletClass == null) { + unavailable(null); + throw new ServletException(sm.getString("standardWrapper.notClass", getName())); + } + + InstanceManager instanceManager = ((StandardContext) getParent()).getInstanceManager(); + try { + servlet = (Servlet) instanceManager.newInstance(servletClass); + } catch (ClassCastException e) { + unavailable(null); + // Restore the context ClassLoader + throw new ServletException(sm.getString("standardWrapper.notServlet", servletClass), e); + } catch (Throwable e) { + e = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(e); + unavailable(null); + + // Added extra log statement for Bugzilla 36630: + // https://bz.apache.org/bugzilla/show_bug.cgi?id=36630 + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardWrapper.instantiate", servletClass), e); + } + + // Restore the context ClassLoader + throw new ServletException(sm.getString("standardWrapper.instantiate", servletClass), e); + } + + if (multipartConfigElement == null) { + MultipartConfig annotation = servlet.getClass().getAnnotation(MultipartConfig.class); + if (annotation != null) { + multipartConfigElement = new MultipartConfigElement(annotation); + } + } + + // Special handling for ContainerServlet instances + // Note: The InstanceManager checks if the application is permitted + // to load ContainerServlets + if (servlet instanceof ContainerServlet) { + ((ContainerServlet) servlet).setWrapper(this); + } + + classLoadTime = (int) (System.currentTimeMillis() - t1); + + initServlet(servlet); + + fireContainerEvent("load", this); + + loadTime = System.currentTimeMillis() - t1; + } finally { + if (swallowOutput) { + String log = SystemLogHandler.stopCapture(); + if (log != null && log.length() > 0) { + if (getServletContext() != null) { + getServletContext().log(log); + } else { + out.println(log); + } + } + } + } + return servlet; + + } + + + private synchronized void initServlet(Servlet servlet) throws ServletException { + + if (instanceInitialized) { + return; + } + + // Call the initialization method of this servlet + try { + if (Globals.IS_SECURITY_ENABLED) { + boolean success = false; + try { + Object[] args = new Object[] { facade }; + SecurityUtil.doAsPrivilege("init", servlet, classType, args); + success = true; + } finally { + if (!success) { + // destroy() will not be called, thus clear the reference now + SecurityUtil.remove(servlet); + } + } + } else { + servlet.init(facade); + } + + instanceInitialized = true; + } catch (UnavailableException f) { + unavailable(f); + throw f; + } catch (ServletException f) { + // If the servlet wanted to be unavailable it would have + // said so, so do not call unavailable(null). + throw f; + } catch (Throwable f) { + ExceptionUtils.handleThrowable(f); + getServletContext().log(sm.getString("standardWrapper.initException", getName()), f); + // If the servlet wanted to be unavailable it would have + // said so, so do not call unavailable(null). + throw new ServletException(sm.getString("standardWrapper.initException", getName()), f); + } + } + + /** + * Remove the specified initialization parameter from this servlet. + * + * @param name Name of the initialization parameter to remove + */ + @Override + public void removeInitParameter(String name) { + + parametersLock.writeLock().lock(); + try { + parameters.remove(name); + } finally { + parametersLock.writeLock().unlock(); + } + fireContainerEvent("removeInitParameter", name); + + } + + + /** + * Remove a mapping associated with the wrapper. + * + * @param mapping The pattern to remove + */ + @Override + public void removeMapping(String mapping) { + + mappingsLock.writeLock().lock(); + try { + mappings.remove(mapping); + } finally { + mappingsLock.writeLock().unlock(); + } + if (parent.getState().equals(LifecycleState.STARTED)) { + fireContainerEvent(REMOVE_MAPPING_EVENT, mapping); + } + + } + + + /** + * Remove any security role reference for the specified role name. + * + * @param name Security role used within this servlet to be removed + */ + @Override + public void removeSecurityReference(String name) { + + referencesLock.writeLock().lock(); + try { + references.remove(name); + } finally { + referencesLock.writeLock().unlock(); + } + fireContainerEvent("removeSecurityReference", name); + + } + + + /** + * Process an UnavailableException, marking this servlet as unavailable for the specified amount of time. + * + * @param unavailable The exception that occurred, or null to mark this servlet as permanently + * unavailable + */ + @Override + public void unavailable(UnavailableException unavailable) { + getServletContext().log(sm.getString("standardWrapper.unavailable", getName())); + if (unavailable == null) { + setAvailable(Long.MAX_VALUE); + } else if (unavailable.isPermanent()) { + setAvailable(Long.MAX_VALUE); + } else { + int unavailableSeconds = unavailable.getUnavailableSeconds(); + if (unavailableSeconds <= 0) { + unavailableSeconds = 60; // Arbitrary default + } + setAvailable(System.currentTimeMillis() + (unavailableSeconds * 1000L)); + } + + } + + + /** + * Unload all initialized instances of this servlet, after calling the destroy() method for each + * instance. This can be used, for example, prior to shutting down the entire servlet engine, or prior to reloading + * all of the classes from the Loader associated with our Loader's repository. + * + * @exception ServletException if an exception is thrown by the destroy() method + */ + @Override + public synchronized void unload() throws ServletException { + + // Nothing to do if we have never loaded the instance + if (instance == null) { + return; + } + unloading = true; + + // Loaf a while if the current instance is allocated + if (countAllocated.get() > 0) { + int nRetries = 0; + long delay = unloadDelay / 20; + while ((nRetries < 21) && (countAllocated.get() > 0)) { + if ((nRetries % 10) == 0) { + log.info(sm.getString("standardWrapper.waiting", countAllocated.toString(), getName())); + } + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // Ignore + } + nRetries++; + } + } + + if (instanceInitialized) { + PrintStream out = System.out; + if (swallowOutput) { + SystemLogHandler.startCapture(); + } + + // Call the servlet destroy() method + try { + if (Globals.IS_SECURITY_ENABLED) { + try { + SecurityUtil.doAsPrivilege("destroy", instance); + } finally { + SecurityUtil.remove(instance); + } + } else { + instance.destroy(); + } + + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + fireContainerEvent("unload", this); + unloading = false; + throw new ServletException(sm.getString("standardWrapper.destroyException", getName()), t); + } finally { + // Annotation processing + if (!((Context) getParent()).getIgnoreAnnotations()) { + try { + ((Context) getParent()).getInstanceManager().destroyInstance(instance); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardWrapper.destroyInstance", getName()), t); + } + } + // Write captured output + if (swallowOutput) { + String log = SystemLogHandler.stopCapture(); + if (log != null && log.length() > 0) { + if (getServletContext() != null) { + getServletContext().log(log); + } else { + out.println(log); + } + } + } + instance = null; + instanceInitialized = false; + } + } + + // Deregister the destroyed instance + instance = null; + + if (isJspServlet && jspMonitorON != null) { + Registry.getRegistry(null, null).unregisterComponent(jspMonitorON); + } + + unloading = false; + fireContainerEvent("unload", this); + } + + + // -------------------------------------------------- ServletConfig Methods + + + @Override + public String getInitParameter(String name) { + return findInitParameter(name); + } + + + @Override + public Enumeration getInitParameterNames() { + + parametersLock.readLock().lock(); + try { + return Collections.enumeration(parameters.keySet()); + } finally { + parametersLock.readLock().unlock(); + } + + } + + + @Override + public ServletContext getServletContext() { + if (parent == null) { + return null; + } else if (!(parent instanceof Context)) { + return null; + } else { + return ((Context) parent).getServletContext(); + } + } + + + @Override + public String getServletName() { + return getName(); + } + + public long getProcessingTime() { + return swValve.getProcessingTime(); + } + + public long getMaxTime() { + return swValve.getMaxTime(); + } + + public long getMinTime() { + return swValve.getMinTime(); + } + + /** + * Returns the number of requests processed by the wrapper. + * + * @return the number of requests processed by the wrapper. + * + * @deprecated The return type will change to long in Tomcat 11 onwards. Callers of this method should switch to + * storing the result of calls to this method in a long value rather than an int. + */ + @Deprecated + public int getRequestCount() { + return swValve.getRequestCount(); + } + + /** + * Returns the number of requests processed by the wrapper that resulted in an error. + * + * @return the number of requests processed by the wrapper that resulted in an error. + * + * @deprecated The return type will change to long in Tomcat 11 onwards. Callers of this method should switch to + * storing the result of calls to this method in a long value rather than an int. + */ + @Deprecated + public int getErrorCount() { + return swValve.getErrorCount(); + } + + /** + * Increment the error count used for monitoring. + */ + @Override + public void incrementErrorCount() { + swValve.incrementErrorCount(); + } + + public long getLoadTime() { + return loadTime; + } + + public int getClassLoadTime() { + return classLoadTime; + } + + @Override + public MultipartConfigElement getMultipartConfigElement() { + return multipartConfigElement; + } + + @Override + public void setMultipartConfigElement(MultipartConfigElement multipartConfigElement) { + this.multipartConfigElement = multipartConfigElement; + } + + @Override + public boolean isAsyncSupported() { + return asyncSupported; + } + + @Override + public void setAsyncSupported(boolean asyncSupported) { + this.asyncSupported = asyncSupported; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + // -------------------------------------------------------- Package Methods + + + // -------------------------------------------------------- protected Methods + + + protected Method[] getAllDeclaredMethods(Class c) { + + if (c.equals(jakarta.servlet.http.HttpServlet.class)) { + return null; + } + + Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass()); + + Method[] thisMethods = c.getDeclaredMethods(); + if (thisMethods.length == 0) { + return parentMethods; + } + + if ((parentMethods != null) && (parentMethods.length > 0)) { + Method[] allMethods = new Method[parentMethods.length + thisMethods.length]; + System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length); + System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length); + + thisMethods = allMethods; + } + + return thisMethods; + } + + + // ------------------------------------------------------ Lifecycle Methods + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + // Send j2ee.state.starting notification + if (this.getObjectName() != null) { + Notification notification = new Notification("j2ee.state.starting", this.getObjectName(), sequenceNumber++); + broadcaster.sendNotification(notification); + } + + // Start up this component + super.startInternal(); + + setAvailable(0L); + + // Send j2ee.state.running notification + if (this.getObjectName() != null) { + Notification notification = new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber++); + broadcaster.sendNotification(notification); + } + + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + setAvailable(Long.MAX_VALUE); + + // Send j2ee.state.stopping notification + if (this.getObjectName() != null) { + Notification notification = new Notification("j2ee.state.stopping", this.getObjectName(), sequenceNumber++); + broadcaster.sendNotification(notification); + } + + // Shut down our servlet instance (if it has been initialized) + try { + unload(); + } catch (ServletException e) { + getServletContext().log(sm.getString("standardWrapper.unloadException", getName()), e); + } + + // Shut down this component + super.stopInternal(); + + // Send j2ee.state.stopped notification + if (this.getObjectName() != null) { + Notification notification = new Notification("j2ee.state.stopped", this.getObjectName(), sequenceNumber++); + broadcaster.sendNotification(notification); + } + + // Send j2ee.object.deleted notification + Notification notification = new Notification("j2ee.object.deleted", this.getObjectName(), sequenceNumber++); + broadcaster.sendNotification(notification); + + } + + + @Override + protected String getObjectNameKeyProperties() { + + StringBuilder keyProperties = new StringBuilder("j2eeType=Servlet"); + + keyProperties.append(getWebModuleKeyProperties()); + + keyProperties.append(",name="); + + String name = getName(); + if (Util.objectNameValueNeedsQuote(name)) { + name = ObjectName.quote(name); + } + keyProperties.append(name); + + keyProperties.append(getJ2EEKeyProperties()); + + return keyProperties.toString(); + } + + + private String getWebModuleKeyProperties() { + + StringBuilder keyProperties = new StringBuilder(",WebModule=//"); + String hostName = getParent().getParent().getName(); + if (hostName == null) { + keyProperties.append("DEFAULT"); + } else { + keyProperties.append(hostName); + } + + String contextName = getParent().getName(); + if (!contextName.startsWith("/")) { + keyProperties.append('/'); + } + keyProperties.append(contextName); + + return keyProperties.toString(); + } + + private String getJ2EEKeyProperties() { + + StringBuilder keyProperties = new StringBuilder(",J2EEApplication="); + + StandardContext ctx = null; + if (parent instanceof StandardContext) { + ctx = (StandardContext) getParent(); + } + + if (ctx == null) { + keyProperties.append("none"); + } else { + keyProperties.append(ctx.getJ2EEApplication()); + } + keyProperties.append(",J2EEServer="); + if (ctx == null) { + keyProperties.append("none"); + } else { + keyProperties.append(ctx.getJ2EEServer()); + } + + return keyProperties.toString(); + } + + + /** + * Remove a JMX notificationListener + * + * @see javax.management.NotificationEmitter#removeNotificationListener(javax.management.NotificationListener, + * javax.management.NotificationFilter, java.lang.Object) + */ + @Override + public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object object) + throws ListenerNotFoundException { + broadcaster.removeNotificationListener(listener, filter, object); + } + + protected MBeanNotificationInfo[] notificationInfo; + + /** + * Get JMX Broadcaster Info + * + * @see javax.management.NotificationBroadcaster#getNotificationInfo() + */ + @Override + public MBeanNotificationInfo[] getNotificationInfo() { + if (notificationInfo == null) { + notificationInfo = new MBeanNotificationInfo[] { + new MBeanNotificationInfo(new String[] { "j2ee.object.created" }, Notification.class.getName(), + "servlet is created"), + new MBeanNotificationInfo(new String[] { "j2ee.state.starting" }, Notification.class.getName(), + "servlet is starting"), + new MBeanNotificationInfo(new String[] { "j2ee.state.running" }, Notification.class.getName(), + "servlet is running"), + new MBeanNotificationInfo(new String[] { "j2ee.state.stopped" }, Notification.class.getName(), + "servlet start to stopped"), + new MBeanNotificationInfo(new String[] { "j2ee.object.stopped" }, Notification.class.getName(), + "servlet is stopped"), + new MBeanNotificationInfo(new String[] { "j2ee.object.deleted" }, Notification.class.getName(), + "servlet is deleted") }; + } + return notificationInfo; + } + + + /** + * Add a JMX-NotificationListener + * + * @see javax.management.NotificationBroadcaster#addNotificationListener(javax.management.NotificationListener, + * javax.management.NotificationFilter, java.lang.Object) + */ + @Override + public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object object) + throws IllegalArgumentException { + broadcaster.addNotificationListener(listener, filter, object); + } + + + /** + * Remove a JMX-NotificationListener + * + * @see javax.management.NotificationBroadcaster#removeNotificationListener(javax.management.NotificationListener) + */ + @Override + public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException { + broadcaster.removeNotificationListener(listener); + } +} diff --git a/java/org/apache/catalina/core/StandardWrapperFacade.java b/java/org/apache/catalina/core/StandardWrapperFacade.java new file mode 100644 index 0000000..84b3824 --- /dev/null +++ b/java/org/apache/catalina/core/StandardWrapperFacade.java @@ -0,0 +1,102 @@ +/* + * 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.catalina.core; + + +import java.util.Enumeration; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; + + +/** + * Facade for the StandardWrapper object. + * + * @author Remy Maucherat + */ +public final class StandardWrapperFacade implements ServletConfig { + + + // ----------------------------------------------------------- Constructors + + + /** + * Create a new facade around a StandardWrapper. + * + * @param config the associated wrapper + */ + public StandardWrapperFacade(StandardWrapper config) { + + super(); + this.config = config; + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Wrapped config. + */ + private final ServletConfig config; + + + /** + * Wrapped context (facade). + */ + private ServletContext context = null; + + + // -------------------------------------------------- ServletConfig Methods + + + @Override + public String getServletName() { + return config.getServletName(); + } + + + @Override + public ServletContext getServletContext() { + /* + * This method may be called concurrently but the same context object will always be returned. There is no + * concurrency issue here. + */ + if (context == null) { + context = config.getServletContext(); + if (context instanceof ApplicationContext) { + context = ((ApplicationContext) context).getFacade(); + } + } + return context; + } + + + @Override + public String getInitParameter(String name) { + return config.getInitParameter(name); + } + + + @Override + public Enumeration getInitParameterNames() { + return config.getInitParameterNames(); + } + + +} diff --git a/java/org/apache/catalina/core/StandardWrapperValve.java b/java/org/apache/catalina/core/StandardWrapperValve.java new file mode 100644 index 0000000..0eb3755 --- /dev/null +++ b/java/org/apache/catalina/core/StandardWrapperValve.java @@ -0,0 +1,338 @@ +/* + * 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.catalina.core; + + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAdder; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.UnavailableException; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; +import org.apache.coyote.BadRequestException; +import org.apache.coyote.CloseNowException; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.log.SystemLogHandler; +import org.apache.tomcat.util.res.StringManager; + +/** + * Valve that implements the default basic behavior for the StandardWrapper container implementation. + * + * @author Craig R. McClanahan + */ +final class StandardWrapperValve extends ValveBase { + + private static final StringManager sm = StringManager.getManager(StandardWrapperValve.class); + + + // ------------------------------------------------------ Constructor + + StandardWrapperValve() { + super(true); + } + + + // ----------------------------------------------------- Instance Variables + + // Some JMX statistics. This valve is associated with a StandardWrapper. + // We expose the StandardWrapper as JMX ( j2eeType=Servlet ). The fields + // are here for performance. + private final LongAdder processingTime = new LongAdder(); + private volatile long maxTime; + private volatile long minTime = Long.MAX_VALUE; + private final AtomicInteger requestCount = new AtomicInteger(0); + private final AtomicInteger errorCount = new AtomicInteger(0); + + + // --------------------------------------------------------- Public Methods + + /** + * Invoke the servlet we are managing, respecting the rules regarding servlet lifecycle support. + * + * @param request Request to be processed + * @param response Response to be produced + * + * @exception IOException if an input/output error occurred + * @exception ServletException if a servlet error occurred + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + // Initialize local variables we may need + boolean unavailable = false; + Throwable throwable = null; + // This should be a Request attribute... + long t1 = System.currentTimeMillis(); + requestCount.incrementAndGet(); + StandardWrapper wrapper = (StandardWrapper) getContainer(); + Servlet servlet = null; + Context context = (Context) wrapper.getParent(); + + // Check for the application being marked unavailable + if (!context.getState().isAvailable()) { + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + sm.getString("standardContext.isUnavailable")); + unavailable = true; + } + + // Check for the servlet being marked unavailable + if (!unavailable && wrapper.isUnavailable()) { + container.getLogger().info(sm.getString("standardWrapper.isUnavailable", wrapper.getName())); + checkWrapperAvailable(response, wrapper); + unavailable = true; + } + + // Allocate a servlet instance to process this request + try { + if (!unavailable) { + servlet = wrapper.allocate(); + } + } catch (UnavailableException e) { + container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), e); + checkWrapperAvailable(response, wrapper); + } catch (ServletException e) { + container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), + StandardWrapper.getRootCause(e)); + throwable = e; + exception(request, response, e); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), e); + throwable = e; + exception(request, response, e); + servlet = null; + } + + MessageBytes requestPathMB = request.getRequestPathMB(); + DispatcherType dispatcherType = DispatcherType.REQUEST; + if (request.getDispatcherType() == DispatcherType.ASYNC) { + dispatcherType = DispatcherType.ASYNC; + } + request.setAttribute(Globals.DISPATCHER_TYPE_ATTR, dispatcherType); + request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, requestPathMB); + // Create the filter chain for this request + ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); + + // Call the filter chain for this request + // NOTE: This also calls the servlet's service() method + Container container = this.container; + try { + if ((servlet != null) && (filterChain != null)) { + // Swallow output if needed + if (context.getSwallowOutput()) { + try { + SystemLogHandler.startCapture(); + if (request.isAsyncDispatching()) { + request.getAsyncContextInternal().doInternalDispatch(); + } else { + filterChain.doFilter(request.getRequest(), response.getResponse()); + } + } finally { + String log = SystemLogHandler.stopCapture(); + if (log != null && log.length() > 0) { + context.getLogger().info(log); + } + } + } else { + if (request.isAsyncDispatching()) { + request.getAsyncContextInternal().doInternalDispatch(); + } else { + filterChain.doFilter(request.getRequest(), response.getResponse()); + } + } + + } + } catch (BadRequestException e) { + if (container.getLogger().isDebugEnabled()) { + container.getLogger().debug( + sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e); + } + throwable = e; + exception(request, response, e, HttpServletResponse.SC_BAD_REQUEST); + } catch (CloseNowException e) { + if (container.getLogger().isDebugEnabled()) { + container.getLogger().debug( + sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e); + } + throwable = e; + exception(request, response, e); + } catch (IOException e) { + container.getLogger() + .error(sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e); + throwable = e; + exception(request, response, e); + } catch (UnavailableException e) { + container.getLogger() + .error(sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e); + wrapper.unavailable(e); + checkWrapperAvailable(response, wrapper); + // Do not save exception in 'throwable', because we + // do not want to do exception(request, response, e) processing + } catch (ServletException e) { + Throwable rootCause = StandardWrapper.getRootCause(e); + if (!(rootCause instanceof BadRequestException)) { + container.getLogger().error(sm.getString("standardWrapper.serviceExceptionRoot", wrapper.getName(), + context.getName(), e.getMessage()), rootCause); + } + throwable = e; + exception(request, response, e); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + container.getLogger() + .error(sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e); + throwable = e; + exception(request, response, e); + } finally { + // Release the filter chain (if any) for this request + if (filterChain != null) { + filterChain.release(); + } + + // Deallocate the allocated servlet instance + try { + if (servlet != null) { + wrapper.deallocate(servlet); + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + container.getLogger().error(sm.getString("standardWrapper.deallocateException", wrapper.getName()), e); + if (throwable == null) { + throwable = e; + exception(request, response, e); + } + } + + // If this servlet has been marked permanently unavailable, + // unload it and release this instance + try { + if ((servlet != null) && (wrapper.getAvailable() == Long.MAX_VALUE)) { + wrapper.unload(); + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + container.getLogger().error(sm.getString("standardWrapper.unloadException", wrapper.getName()), e); + if (throwable == null) { + exception(request, response, e); + } + } + long t2 = System.currentTimeMillis(); + + long time = t2 - t1; + processingTime.add(time); + if (time > maxTime) { + maxTime = time; + } + if (time < minTime) { + minTime = time; + } + } + } + + private void checkWrapperAvailable(Response response, StandardWrapper wrapper) throws IOException { + long available = wrapper.getAvailable(); + if ((available > 0L) && (available < Long.MAX_VALUE)) { + response.setDateHeader("Retry-After", available); + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + sm.getString("standardWrapper.isUnavailable", wrapper.getName())); + } else if (available == Long.MAX_VALUE) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + sm.getString("standardWrapper.notFound", wrapper.getName())); + } + } + + + // -------------------------------------------------------- Private Methods + + /** + * Handle the specified ServletException encountered while processing the specified Request to produce the specified + * Response. Any exceptions that occur during generation of the exception report are logged and swallowed. + * + * @param request The request being processed + * @param response The response being generated + * @param exception The exception that occurred (which possibly wraps a root cause exception + */ + private void exception(Request request, Response response, Throwable exception) { + exception(request, response, exception, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @SuppressWarnings("deprecation") + private void exception(Request request, Response response, Throwable exception, int errorCode) { + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception); + response.setStatus(errorCode); + response.setError(); + } + + public long getProcessingTime() { + return processingTime.sum(); + } + + public long getMaxTime() { + return maxTime; + } + + public long getMinTime() { + return minTime; + } + + /** + * Returns the number of requests processed by the associated wrapper. + * + * @return the number of requests processed by the associated wrapper. + * + * @deprecated The return type will change to long in Tomcat 11 onwards. Callers of this method should switch to + * storing the result of calls to this method in a long value rather than an int. + */ + @Deprecated + public int getRequestCount() { + return requestCount.get(); + } + + /** + * Returns the number of requests processed by the associated wrapper that resulted in an error. + * + * @return the number of requests processed by the associated wrapper that resulted in an error. + * + * @deprecated The return type will change to long in Tomcat 11 onwards. Callers of this method should switch to + * storing the result of calls to this method in a long value rather than an int. + */ + @Deprecated + public int getErrorCount() { + return errorCount.get(); + } + + public void incrementErrorCount() { + errorCount.incrementAndGet(); + } + + @Override + protected void initInternal() throws LifecycleException { + // NOOP - Don't register this Valve in JMX + } +} diff --git a/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java b/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java new file mode 100644 index 0000000..c413317 --- /dev/null +++ b/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java @@ -0,0 +1,137 @@ +/* + * 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.catalina.core; + +import java.util.concurrent.Executor; + +import org.apache.catalina.ContainerEvent; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Connector; +import org.apache.coyote.ProtocolHandler; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.threads.ThreadPoolExecutor; + +/** + * A {@link LifecycleListener} that triggers the renewal of threads in Executor pools when a {@link Context} is being + * stopped to avoid thread-local related memory leaks. + *

    + * Note : active threads will be renewed one by one when they come back to the pool after executing their task, see + * {@link org.apache.tomcat.util.threads.ThreadPoolExecutor}.afterExecute(). + *

    + * This listener must only be nested within {@link Server} elements. + */ +public class ThreadLocalLeakPreventionListener extends FrameworkListener { + + private static final Log log = LogFactory.getLog(ThreadLocalLeakPreventionListener.class); + + private volatile boolean serverStopping = false; + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(ThreadLocalLeakPreventionListener.class); + + /** + * Listens for {@link LifecycleEvent} for the start of the {@link Server} to initialize itself and then for + * after_stop events of each {@link Context}. + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + try { + super.lifecycleEvent(event); + + Lifecycle lifecycle = event.getLifecycle(); + if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType()) && lifecycle instanceof Server) { + // Server is shutting down, so thread pools will be shut down so + // there is no need to clean the threads + serverStopping = true; + } + + if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType()) && lifecycle instanceof Context) { + stopIdleThreads((Context) lifecycle); + } + } catch (Exception e) { + String msg = sm.getString("threadLocalLeakPreventionListener.lifecycleEvent.error", event); + log.error(msg, e); + } + } + + @Override + public void containerEvent(ContainerEvent event) { + try { + super.containerEvent(event); + } catch (Exception e) { + String msg = sm.getString("threadLocalLeakPreventionListener.containerEvent.error", event); + log.error(msg, e); + } + + } + + /** + * Updates each ThreadPoolExecutor with the current time, which is the time when a context is being stopped. + * + * @param context the context being stopped, used to discover all the Connectors of its parent Service. + */ + private void stopIdleThreads(Context context) { + if (serverStopping) { + return; + } + + if (!(context instanceof StandardContext) || + !((StandardContext) context).getRenewThreadsWhenStoppingContext()) { + if (log.isTraceEnabled()) { + log.trace("Not renewing threads when the context is stopping. It is not configured to do it."); + } + return; + } + + Engine engine = (Engine) context.getParent().getParent(); + Service service = engine.getService(); + Connector[] connectors = service.findConnectors(); + if (connectors != null) { + for (Connector connector : connectors) { + ProtocolHandler handler = connector.getProtocolHandler(); + Executor executor = null; + if (handler != null) { + executor = handler.getExecutor(); + } + + if (executor instanceof ThreadPoolExecutor) { + ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor; + threadPoolExecutor.contextStopping(); + } else if (executor instanceof StandardThreadExecutor) { + StandardThreadExecutor stdThreadExecutor = (StandardThreadExecutor) executor; + stdThreadExecutor.contextStopping(); + } + + } + } + } + + @Override + protected LifecycleListener createLifecycleListener(Context context) { + return this; + } +} diff --git a/java/org/apache/catalina/core/mbeans-descriptors.xml b/java/org/apache/catalina/core/mbeans-descriptors.xml new file mode 100644 index 0000000..92f9f9d --- /dev/null +++ b/java/org/apache/catalina/core/mbeans-descriptors.xml @@ -0,0 +1,1782 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/deploy/LocalStrings.properties b/java/org/apache/catalina/deploy/LocalStrings.properties new file mode 100644 index 0000000..11e6268 --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings.properties @@ -0,0 +1,25 @@ +# 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. + +namingResources.cleanupCloseFailed=Failed to invoke method [{0}] for resource [{1}] in container [{2}] so no cleanup was performed for that resource +namingResources.cleanupCloseSecurity=Unable to retrieve method [{0}] for resource [{1}] in container [{2}] so no cleanup was performed for that resource +namingResources.cleanupNoClose=Resource [{0}] in container [{1}] does not have a [{2}] method so no cleanup was performed for that resource +namingResources.cleanupNoContext=Failed to retrieve JNDI naming context for container [{0}] so no cleanup was performed for that container +namingResources.cleanupNoResource=Failed to retrieve JNDI resource [{0}] for container [{1}] so no cleanup was performed for that resource +namingResources.ejbLookupLink=The EJB reference [{0}] specifies both a ejb-link and a lookup-name +namingResources.envEntryLookupValue=The environment entry [{0}] specifies both a lookup-name and a value +namingResources.mbeanCreateFail=Failed to create MBean for naming resource [{0}] +namingResources.mbeanDestroyFail=Failed to destroy MBean for naming resource [{0}] +namingResources.resourceTypeFail=The JNDI resource named [{0}] is of type [{1}] but the type is inconsistent with the type(s) of the injection target(s) configured for that resource diff --git a/java/org/apache/catalina/deploy/LocalStrings_cs.properties b/java/org/apache/catalina/deploy/LocalStrings_cs.properties new file mode 100644 index 0000000..9ee0000 --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings_cs.properties @@ -0,0 +1,17 @@ +# 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. + +namingResources.cleanupCloseSecurity=Nelze získat metodu [{0}] pro zdroj [{1}] v kontejneru [{2}], takže žádný úklid pro tento zdroj nebyl proveden +namingResources.ejbLookupLink=EJB reference [{0}] specifikuje obÄ› hodnoty ejb-link a lookup-name diff --git a/java/org/apache/catalina/deploy/LocalStrings_de.properties b/java/org/apache/catalina/deploy/LocalStrings_de.properties new file mode 100644 index 0000000..935232c --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings_de.properties @@ -0,0 +1,17 @@ +# 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. + +namingResources.cleanupCloseSecurity=Kann Methode [{0}] für Ressource [{1}] in Container [{2}] nicht laden. Daher wird diese Ressource nicht aufgeräumt. +namingResources.ejbLookupLink=Die EJB Referenz [{0}] spezifiziert beides, einen ejb-link und einen lookup-name diff --git a/java/org/apache/catalina/deploy/LocalStrings_es.properties b/java/org/apache/catalina/deploy/LocalStrings_es.properties new file mode 100644 index 0000000..062d99d --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings_es.properties @@ -0,0 +1,18 @@ +# 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. + +namingResources.cleanupCloseSecurity=Incapaz de obtener el método [{0}] para el recurso [{1}] en el contenedor [{2}] debido a ello no se realizó la limpieza para ese recurso\n +namingResources.cleanupNoClose=Recurso [{0}] en contenedor [{1}] no tien un método [{2}] por lo cual no ser hace una limpieza para ese recurso +namingResources.ejbLookupLink=La referencia EJB [{0}] especifica ambas, un ejb-link y un nombre para lookup diff --git a/java/org/apache/catalina/deploy/LocalStrings_fr.properties b/java/org/apache/catalina/deploy/LocalStrings_fr.properties new file mode 100644 index 0000000..1ab2795 --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings_fr.properties @@ -0,0 +1,25 @@ +# 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. + +namingResources.cleanupCloseFailed=Impossible d''invoquer la méthode [{0}] de la ressource [{1}] dans le conteneur [{2}] donc aucun nettoyage n''a été effectué pour cette ressource +namingResources.cleanupCloseSecurity=Incapacité de récupérer la méthode [{0}] pour la ressource [{1}] dans le conteneur [{2}]. Aucun nettoyage effectué pour cette ressource. +namingResources.cleanupNoClose=La ressource [{0}] du container [{1}] n''a pas de [{2}] méthode donc aucun nettoyage de la ressource n''a pu être effectué +namingResources.cleanupNoContext=Impossible de récupérer le contexte de nommage JNDI dans le conteneur [{0}] donc aucun nettoyage de ce conteneur n''a pu être effectué +namingResources.cleanupNoResource=Impossible de récupérer la ressource JNDI [{1}] dans le conteneur [{2}] donc aucun nettoyage de la ressource n''a pu être effectué +namingResources.ejbLookupLink=La référence à un EJB [{0}] spéficie à la fois un ejb-link and et un lookup-name +namingResources.envEntryLookupValue=L''entrée d''environnement [{0}] spécifie à la fois un lookup-name et une valeur +namingResources.mbeanCreateFail=Échec de création d''un MBean pour la ressource nommée ("naming resource") [{0}] +namingResources.mbeanDestroyFail=Echec de destruction du mbean de la ressource [{0}] +namingResources.resourceTypeFail=La ressource JNDI nommée [{0}] est de type [{1}] mais ce type est inconsistant avec le(s) type(s) de cible d''injection configuré(s) pour cette ressource diff --git a/java/org/apache/catalina/deploy/LocalStrings_ja.properties b/java/org/apache/catalina/deploy/LocalStrings_ja.properties new file mode 100644 index 0000000..f65995d --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings_ja.properties @@ -0,0 +1,25 @@ +# 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. + +namingResources.cleanupCloseFailed=コンテナ [{2}] 内ã®ãƒªã‚½ãƒ¼ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ãŸãŸã‚ã€ãã®ãƒªã‚½ãƒ¼ã‚¹ã«å¯¾ã—ã¦ã‚¯ãƒªãƒ¼ãƒ³ã‚¢ãƒƒãƒ—ãŒå®Ÿè¡Œã•ã‚Œã¾ã›ã‚“ã§ã—㟠+namingResources.cleanupCloseSecurity=コンテナ [{2}] ã®ãƒªã‚½ãƒ¼ã‚¹ [{1}] ã‹ã‚‰ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã‚’å–å¾—ã§ãã¾ã›ã‚“。リソースを後始末ã—ã¾ã›ã‚“ +namingResources.cleanupNoClose=コンテナー [{1}] ã®ãƒªã‚½ãƒ¼ã‚¹ [{0}] ã«ã¯ãƒ¡ã‚½ãƒƒãƒ‰ [{2}] ãŒãªã„ãŸã‚ã€ãƒªã‚½ãƒ¼ã‚¹ã®å¾Œå§‹æœ«ã‚’è¡Œã„ã¾ã›ã‚“。 +namingResources.cleanupNoContext=コンテナ [{0}] ã®JNDIãƒãƒ¼ãƒŸãƒ³ã‚°ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã®å–å¾—ã«å¤±æ•—ã—ãŸãŸã‚ã€ãã®ã‚³ãƒ³ãƒ†ãƒŠã«å¯¾ã—ã¦ã‚¯ãƒªãƒ¼ãƒ³ã‚¢ãƒƒãƒ—ãŒå®Ÿè¡Œã•ã‚Œã¾ã›ã‚“ã§ã—㟠+namingResources.cleanupNoResource=コンテナ [{1}] ã®JNDIリソース [{0}] ã®å–å¾—ã«å¤±æ•—ã—ãŸãŸã‚ã€ãã®ãƒªã‚½ãƒ¼ã‚¹ã«å¯¾ã—ã¦ã‚¯ãƒªãƒ¼ãƒ³ã‚¢ãƒƒãƒ—ãŒå®Ÿè¡Œã•ã‚Œã¾ã›ã‚“ã§ã—㟠+namingResources.ejbLookupLink=EJB リソース [{0}] ã« ejb-link 㨠lookup-name ã®ä¸¡æ–¹ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ +namingResources.envEntryLookupValue=環境エントリ[{0}]ã¯ã€æ¤œç´¢åã¨å€¤ã®ä¸¡æ–¹ã‚’指定ã—ã¾ã™ã€‚ +namingResources.mbeanCreateFail=ãƒãƒ¼ãƒŸãƒ³ã‚°ãƒªã‚½ãƒ¼ã‚¹ [{0}] ã®MBeanã®ä½œæˆã«å¤±æ•—ã—ã¾ã—㟠+namingResources.mbeanDestroyFail=ãƒãƒ¼ãƒŸãƒ³ã‚°ãƒªã‚½ãƒ¼ã‚¹ [{0}] ã®MBeanを破棄ã§ãã¾ã›ã‚“ã§ã—㟠+namingResources.resourceTypeFail=[{0}] ã¨ã„ã†åå‰ã®JNDIリソース㯠[{1}] タイプã§ã™ãŒã€ã‚¿ã‚¤ãƒ—ã¯ãã®ãƒªã‚½ãƒ¼ã‚¹ç”¨ã«æ§‹æˆã•ã‚ŒãŸæ³¨å…¥ã‚¿ãƒ¼ã‚²ãƒƒãƒˆã®ã‚¿ã‚¤ãƒ—ã¨çŸ›ç›¾ã—ã¦ã„ã¾ã™ diff --git a/java/org/apache/catalina/deploy/LocalStrings_ko.properties b/java/org/apache/catalina/deploy/LocalStrings_ko.properties new file mode 100644 index 0000000..4014c60 --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings_ko.properties @@ -0,0 +1,25 @@ +# 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. + +namingResources.cleanupCloseFailed=컨테ì´ë„ˆ [{2}] ë‚´ì˜ ë¦¬ì†ŒìŠ¤ [{1}]ì„(를) 위한 메소드 [{0}]ì„(를) 호출하지 못했으므로, 해당 리소스를 위한 cleanupì´ ìˆ˜í–‰ë˜ì§€ 않았습니다. +namingResources.cleanupCloseSecurity=컨테ì´ë„ˆ [{2}]ì—ì„œ 리소스 [{1}]ì„(를) 위한 메소드 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없어서, 해당 ë¦¬ì†ŒìŠ¤ì— ëŒ€í•œ cleanupì´ ìˆ˜í–‰ë˜ì§€ 않았습니다. +namingResources.cleanupNoClose=컨테ì´ë„ˆ [{1}]ì˜ ë¦¬ì†ŒìŠ¤ [{0}]ì€(는) [{2}] 메소드를 가지고 있지 ì•Šì•„, 해당 ë¦¬ì†ŒìŠ¤ì— ëŒ€í•œ cleanupì´ ìˆ˜í–‰ë˜ì§€ 않았습니다. +namingResources.cleanupNoContext=컨테ì´ë„ˆ [{0}]ì„(를) 위한 JNDI Naming 컨í…스트를 조회하지 못하여, 해당 컨테ì´ë„ˆë¥¼ 위한 cleanupì´ ìˆ˜í–‰ë˜ì§€ 않았습니다. +namingResources.cleanupNoResource=컨테ì´í„° [{1}]ì„(를) 위한 JNDI 리소스 [{0}]ì„(를) 검색하지 못하였으므로, 해당 ë¦¬ì†ŒìŠ¤ì— ëŒ€í•œ cleanupì´ ìˆ˜í–‰ë˜ì§€ 않았습니다. +namingResources.ejbLookupLink=EJB ë ˆí¼ëŸ°ìŠ¤ [{0}]ì´(ê°€) ejb-link와 lookup-name 둘 다를 지정했습니다. +namingResources.envEntryLookupValue=Environment 엔트리 [{0}]ì€(는) lookup-nameê³¼ ê°’ 둘 다 지정하고 있습니다. +namingResources.mbeanCreateFail=Naming 리소스 [{0}]ì„(를) 위한 MBeanì„ ìƒì„±í•˜ì§€ 못했습니다. +namingResources.mbeanDestroyFail=Naming 리소스 [{0}]ì„(를) 위한 MBeanì„ ì†Œë©¸ì‹œí‚¤ì§€ 못했습니다. +namingResources.resourceTypeFail=[{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ JNDI 리소스는 íƒ€ìž…ì´ [{1}]ì´ì§€ë§Œ, 해당 íƒ€ìž…ì€ í•´ë‹¹ 리소스를 위해 ì„¤ì •ëœ injection 대ìƒ(들)ì˜ íƒ€ìž…(들)ê³¼ ì¼ê´€ë˜ì§€ 않습니다. diff --git a/java/org/apache/catalina/deploy/LocalStrings_pt_BR.properties b/java/org/apache/catalina/deploy/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..7f6434e --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +namingResources.ejbLookupLink=A referência EJB [{0}] especifica um ejb-link e um lookup-name diff --git a/java/org/apache/catalina/deploy/LocalStrings_zh_CN.properties b/java/org/apache/catalina/deploy/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..02e82bf --- /dev/null +++ b/java/org/apache/catalina/deploy/LocalStrings_zh_CN.properties @@ -0,0 +1,25 @@ +# 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. + +namingResources.cleanupCloseFailed=无法为容器[{2}]中的资æº[{1}]调用方法[{0}],因此未对该资æºæ‰§è¡Œæ¸…ç† +namingResources.cleanupCloseSecurity=无法检索容器[{2}]中的资æº[{1}]的方法[{0}],因此没有对该资æºè¿›è¡Œæ¸…ç† +namingResources.cleanupNoClose=容器[{1}]中的资æº[{0}]没有[{2}]方法,因此没有对该资æºæ‰§è¡Œæ¸…ç† +namingResources.cleanupNoContext=无法检索容器[{0}]çš„JNDI命åä¸Šä¸‹æ–‡ï¼Œå› æ­¤æœªå¯¹è¯¥å®¹å™¨æ‰§è¡Œæ¸…ç† +namingResources.cleanupNoResource=无法检索容器[{1}]çš„JNDI资æº[{0}],因此未对该资æºæ‰§è¡Œæ¸…ç†ã€‚ +namingResources.ejbLookupLink=EJB的引用[{0}]åŒæ—¶æŒ‡å®šäº†ejb-linkå’Œlookup-name +namingResources.envEntryLookupValue=环境å‚æ•° [{0}] 指定查询å称和值 +namingResources.mbeanCreateFail=为命å资æº[{0}]创建MBean失败 +namingResources.mbeanDestroyFail=失败的销æ¯å‘½å资æº[{0}]为MBean +namingResources.resourceTypeFail=å为[{0}]çš„JNDI资æºæ˜¯ç±»åž‹[{1}],但是该类型与为该资æºé…置的注入目标的类型ä¸ä¸€è‡´ diff --git a/java/org/apache/catalina/deploy/NamingResourcesImpl.java b/java/org/apache/catalina/deploy/NamingResourcesImpl.java new file mode 100644 index 0000000..03c660d --- /dev/null +++ b/java/org/apache/catalina/deploy/NamingResourcesImpl.java @@ -0,0 +1,1209 @@ +/* + * 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.catalina.deploy; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.naming.NamingException; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.JmxEnabled; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Server; +import org.apache.catalina.mbeans.MBeanUtils; +import org.apache.catalina.util.Introspection; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.naming.ContextBindings; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.descriptor.web.ContextEjb; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextLocalEjb; +import org.apache.tomcat.util.descriptor.web.ContextResource; +import org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef; +import org.apache.tomcat.util.descriptor.web.ContextResourceLink; +import org.apache.tomcat.util.descriptor.web.ContextService; +import org.apache.tomcat.util.descriptor.web.ContextTransaction; +import org.apache.tomcat.util.descriptor.web.InjectionTarget; +import org.apache.tomcat.util.descriptor.web.MessageDestinationRef; +import org.apache.tomcat.util.descriptor.web.NamingResources; +import org.apache.tomcat.util.descriptor.web.ResourceBase; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Holds and manages the naming resources defined in the Jakarta EE Naming Context and their associated JNDI context. + * + * @author Remy Maucherat + */ +public class NamingResourcesImpl extends LifecycleMBeanBase implements Serializable, NamingResources { + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(NamingResourcesImpl.class); + + private static final StringManager sm = StringManager.getManager(NamingResourcesImpl.class); + + private volatile boolean resourceRequireExplicitRegistration = false; + + // ----------------------------------------------------------- Constructors + + + /** + * Create a new NamingResources instance. + */ + public NamingResourcesImpl() { + // NOOP + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Associated container object. + */ + private Object container = null; + + + /** + * Set of naming entries, keyed by name. + */ + private final Set entries = new HashSet<>(); + + + /** + * The EJB resource references for this web application, keyed by name. + */ + private final Map ejbs = new HashMap<>(); + + + /** + * The environment entries for this web application, keyed by name. + */ + private final Map envs = new HashMap<>(); + + + /** + * The local EJB resource references for this web application, keyed by name. + */ + private final Map localEjbs = new HashMap<>(); + + + /** + * The message destination references for this web application, keyed by name. + */ + private final Map mdrs = new HashMap<>(); + + + /** + * The resource environment references for this web application, keyed by name. + */ + private final HashMap resourceEnvRefs = new HashMap<>(); + + + /** + * The resource references for this web application, keyed by name. + */ + private final HashMap resources = new HashMap<>(); + + + /** + * The resource links for this web application, keyed by name. + */ + private final HashMap resourceLinks = new HashMap<>(); + + + /** + * The web service references for this web application, keyed by name. + */ + private final HashMap services = new HashMap<>(); + + + /** + * The transaction for this webapp. + */ + private ContextTransaction transaction = null; + + + /** + * The property change support for this component. + */ + protected final PropertyChangeSupport support = new PropertyChangeSupport(this); + + + // ------------------------------------------------------------- Properties + + + /** + * @return the container with which the naming resources are associated. + */ + @Override + public Object getContainer() { + return container; + } + + + /** + * Set the container with which the naming resources are associated. + * + * @param container the associated with the resources + */ + public void setContainer(Object container) { + this.container = container; + } + + + /** + * Set the transaction object. + * + * @param transaction the transaction descriptor + */ + public void setTransaction(ContextTransaction transaction) { + this.transaction = transaction; + } + + + /** + * @return the transaction object. + */ + public ContextTransaction getTransaction() { + return transaction; + } + + + /** + * Add an EJB resource reference for this web application. + * + * @param ejb New EJB resource reference + */ + public void addEjb(ContextEjb ejb) { + + // Entries with lookup-name and ejb-link are an error (EE.5.5.2 / EE.5.5.3) + String ejbLink = ejb.getLink(); + String lookupName = ejb.getLookupName(); + + if (ejbLink != null && ejbLink.length() > 0 && lookupName != null && lookupName.length() > 0) { + throw new IllegalArgumentException(sm.getString("namingResources.ejbLookupLink", ejb.getName())); + } + + if (entries.contains(ejb.getName())) { + return; + } else { + entries.add(ejb.getName()); + } + + synchronized (ejbs) { + ejb.setNamingResources(this); + ejbs.put(ejb.getName(), ejb); + } + support.firePropertyChange("ejb", null, ejb); + + } + + + /** + * Add an environment entry for this web application. + * + * @param environment New environment entry + */ + @Override + public void addEnvironment(ContextEnvironment environment) { + + if (entries.contains(environment.getName())) { + ContextEnvironment ce = findEnvironment(environment.getName()); + ContextResourceLink rl = findResourceLink(environment.getName()); + if (ce != null) { + if (ce.getOverride()) { + removeEnvironment(environment.getName()); + } else { + return; + } + } else if (rl != null) { + // Link. Need to look at the global resources + NamingResourcesImpl global = getServer().getGlobalNamingResources(); + if (global.findEnvironment(rl.getGlobal()) != null) { + if (global.findEnvironment(rl.getGlobal()).getOverride()) { + removeResourceLink(environment.getName()); + } else { + return; + } + } + } else { + // It exists but it isn't an env or a res link... + return; + } + } + + List injectionTargets = environment.getInjectionTargets(); + String value = environment.getValue(); + String lookupName = environment.getLookupName(); + + // Entries with injection targets but no value are effectively ignored + if (injectionTargets != null && injectionTargets.size() > 0 && (value == null || value.length() == 0)) { + return; + } + + // Entries with lookup-name and value are an error (EE.5.4.1.3) + if (value != null && value.length() > 0 && lookupName != null && lookupName.length() > 0) { + throw new IllegalArgumentException( + sm.getString("namingResources.envEntryLookupValue", environment.getName())); + } + + if (!checkResourceType(environment)) { + throw new IllegalArgumentException( + sm.getString("namingResources.resourceTypeFail", environment.getName(), environment.getType())); + } + + entries.add(environment.getName()); + + synchronized (envs) { + environment.setNamingResources(this); + envs.put(environment.getName(), environment); + } + support.firePropertyChange("environment", null, environment); + + // Register with JMX + if (resourceRequireExplicitRegistration) { + try { + MBeanUtils.createMBean(environment); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanCreateFail", environment.getName()), e); + } + } + } + + // Container should be an instance of Server or Context. If it is anything + // else, return null which will trigger a NPE. + private Server getServer() { + if (container instanceof Server) { + return (Server) container; + } + if (container instanceof Context) { + // Could do this in one go. Lots of casts so split out for clarity + Engine engine = (Engine) ((Context) container).getParent().getParent(); + return engine.getService().getServer(); + } + return null; + } + + /** + * Add a local EJB resource reference for this web application. + * + * @param ejb New EJB resource reference + */ + public void addLocalEjb(ContextLocalEjb ejb) { + + if (entries.contains(ejb.getName())) { + return; + } else { + entries.add(ejb.getName()); + } + + synchronized (localEjbs) { + ejb.setNamingResources(this); + localEjbs.put(ejb.getName(), ejb); + } + support.firePropertyChange("localEjb", null, ejb); + + } + + + /** + * Add a message destination reference for this web application. + * + * @param mdr New message destination reference + */ + public void addMessageDestinationRef(MessageDestinationRef mdr) { + + if (entries.contains(mdr.getName())) { + return; + } else { + if (!checkResourceType(mdr)) { + throw new IllegalArgumentException( + sm.getString("namingResources.resourceTypeFail", mdr.getName(), mdr.getType())); + } + entries.add(mdr.getName()); + } + + synchronized (mdrs) { + mdr.setNamingResources(this); + mdrs.put(mdr.getName(), mdr); + } + support.firePropertyChange("messageDestinationRef", null, mdr); + + } + + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + public void addPropertyChangeListener(PropertyChangeListener listener) { + + support.addPropertyChangeListener(listener); + + } + + + /** + * Add a resource reference for this web application. + * + * @param resource New resource reference + */ + @Override + public void addResource(ContextResource resource) { + + if (entries.contains(resource.getName())) { + return; + } else { + if (!checkResourceType(resource)) { + throw new IllegalArgumentException( + sm.getString("namingResources.resourceTypeFail", resource.getName(), resource.getType())); + } + entries.add(resource.getName()); + } + + synchronized (resources) { + resource.setNamingResources(this); + resources.put(resource.getName(), resource); + } + support.firePropertyChange("resource", null, resource); + + // Register with JMX + if (resourceRequireExplicitRegistration) { + try { + MBeanUtils.createMBean(resource); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanCreateFail", resource.getName()), e); + } + } + } + + + /** + * Add a resource environment reference for this web application. + * + * @param resource The resource + */ + public void addResourceEnvRef(ContextResourceEnvRef resource) { + + if (entries.contains(resource.getName())) { + return; + } else { + if (!checkResourceType(resource)) { + throw new IllegalArgumentException( + sm.getString("namingResources.resourceTypeFail", resource.getName(), resource.getType())); + } + entries.add(resource.getName()); + } + + synchronized (resourceEnvRefs) { + resource.setNamingResources(this); + resourceEnvRefs.put(resource.getName(), resource); + } + support.firePropertyChange("resourceEnvRef", null, resource); + + } + + + /** + * Add a resource link for this web application. + * + * @param resourceLink New resource link + */ + @Override + public void addResourceLink(ContextResourceLink resourceLink) { + + if (entries.contains(resourceLink.getName())) { + return; + } else { + entries.add(resourceLink.getName()); + } + + synchronized (resourceLinks) { + resourceLink.setNamingResources(this); + resourceLinks.put(resourceLink.getName(), resourceLink); + } + support.firePropertyChange("resourceLink", null, resourceLink); + + // Register with JMX + if (resourceRequireExplicitRegistration) { + try { + MBeanUtils.createMBean(resourceLink); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanCreateFail", resourceLink.getName()), e); + } + } + } + + + /** + * Add a web service reference for this web application. + * + * @param service New web service reference + */ + public void addService(ContextService service) { + + if (entries.contains(service.getName())) { + return; + } else { + entries.add(service.getName()); + } + + synchronized (services) { + service.setNamingResources(this); + services.put(service.getName(), service); + } + support.firePropertyChange("service", null, service); + + } + + + /** + * @return the EJB resource reference with the specified name, if any; otherwise, return null. + * + * @param name Name of the desired EJB resource reference + */ + public ContextEjb findEjb(String name) { + + synchronized (ejbs) { + return ejbs.get(name); + } + + } + + + /** + * @return the defined EJB resource references for this application. If there are none, a zero-length array is + * returned. + */ + public ContextEjb[] findEjbs() { + + synchronized (ejbs) { + return ejbs.values().toArray(new ContextEjb[0]); + } + + } + + + /** + * @return the environment entry with the specified name, if any; otherwise, return null. + * + * @param name Name of the desired environment entry + */ + public ContextEnvironment findEnvironment(String name) { + + synchronized (envs) { + return envs.get(name); + } + + } + + + /** + * @return the set of defined environment entries for this web application. If none have been defined, a zero-length + * array is returned. + */ + public ContextEnvironment[] findEnvironments() { + + synchronized (envs) { + return envs.values().toArray(new ContextEnvironment[0]); + } + + } + + + /** + * @return the local EJB resource reference with the specified name, if any; otherwise, return null. + * + * @param name Name of the desired EJB resource reference + */ + public ContextLocalEjb findLocalEjb(String name) { + + synchronized (localEjbs) { + return localEjbs.get(name); + } + + } + + + /** + * @return the defined local EJB resource references for this application. If there are none, a zero-length array is + * returned. + */ + public ContextLocalEjb[] findLocalEjbs() { + + synchronized (localEjbs) { + return localEjbs.values().toArray(new ContextLocalEjb[0]); + } + + } + + + /** + * @return the message destination reference with the specified name, if any; otherwise, return null. + * + * @param name Name of the desired message destination reference + */ + public MessageDestinationRef findMessageDestinationRef(String name) { + + synchronized (mdrs) { + return mdrs.get(name); + } + + } + + + /** + * @return the defined message destination references for this application. If there are none, a zero-length array + * is returned. + */ + public MessageDestinationRef[] findMessageDestinationRefs() { + + synchronized (mdrs) { + return mdrs.values().toArray(new MessageDestinationRef[0]); + } + + } + + + /** + * @return the resource reference with the specified name, if any; otherwise return null. + * + * @param name Name of the desired resource reference + */ + public ContextResource findResource(String name) { + + synchronized (resources) { + return resources.get(name); + } + + } + + + /** + * @return the resource link with the specified name, if any; otherwise return null. + * + * @param name Name of the desired resource link + */ + public ContextResourceLink findResourceLink(String name) { + + synchronized (resourceLinks) { + return resourceLinks.get(name); + } + + } + + + /** + * @return the defined resource links for this application. If none have been defined, a zero-length array is + * returned. + */ + public ContextResourceLink[] findResourceLinks() { + + synchronized (resourceLinks) { + return resourceLinks.values().toArray(new ContextResourceLink[0]); + } + + } + + + /** + * @return the defined resource references for this application. If none have been defined, a zero-length array is + * returned. + */ + public ContextResource[] findResources() { + + synchronized (resources) { + return resources.values().toArray(new ContextResource[0]); + } + + } + + + /** + * @return the resource environment reference type for the specified name, if any; otherwise return + * null. + * + * @param name Name of the desired resource environment reference + */ + public ContextResourceEnvRef findResourceEnvRef(String name) { + + synchronized (resourceEnvRefs) { + return resourceEnvRefs.get(name); + } + + } + + + /** + * @return the set of resource environment reference names for this web application. If none have been specified, a + * zero-length array is returned. + */ + public ContextResourceEnvRef[] findResourceEnvRefs() { + + synchronized (resourceEnvRefs) { + return resourceEnvRefs.values().toArray(new ContextResourceEnvRef[0]); + } + + } + + + /** + * @return the web service reference for the specified name, if any; otherwise return null. + * + * @param name Name of the desired web service + */ + public ContextService findService(String name) { + + synchronized (services) { + return services.get(name); + } + + } + + + /** + * @return the defined web service references for this application. If none have been defined, a zero-length array + * is returned. + */ + public ContextService[] findServices() { + + synchronized (services) { + return services.values().toArray(new ContextService[0]); + } + + } + + + /** + * Remove any EJB resource reference with the specified name. + * + * @param name Name of the EJB resource reference to remove + */ + public void removeEjb(String name) { + + entries.remove(name); + + ContextEjb ejb = null; + synchronized (ejbs) { + ejb = ejbs.remove(name); + } + if (ejb != null) { + support.firePropertyChange("ejb", ejb, null); + ejb.setNamingResources(null); + } + + } + + + /** + * Remove any environment entry with the specified name. + * + * @param name Name of the environment entry to remove + */ + @Override + public void removeEnvironment(String name) { + + entries.remove(name); + + ContextEnvironment environment = null; + synchronized (envs) { + environment = envs.remove(name); + } + if (environment != null) { + support.firePropertyChange("environment", environment, null); + // De-register with JMX + if (resourceRequireExplicitRegistration) { + try { + MBeanUtils.destroyMBean(environment); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanDestroyFail", environment.getName()), e); + } + } + environment.setNamingResources(null); + } + } + + + /** + * Remove any local EJB resource reference with the specified name. + * + * @param name Name of the EJB resource reference to remove + */ + public void removeLocalEjb(String name) { + + entries.remove(name); + + ContextLocalEjb localEjb = null; + synchronized (localEjbs) { + localEjb = localEjbs.remove(name); + } + if (localEjb != null) { + support.firePropertyChange("localEjb", localEjb, null); + localEjb.setNamingResources(null); + } + + } + + + /** + * Remove any message destination reference with the specified name. + * + * @param name Name of the message destination resource reference to remove + */ + public void removeMessageDestinationRef(String name) { + + entries.remove(name); + + MessageDestinationRef mdr = null; + synchronized (mdrs) { + mdr = mdrs.remove(name); + } + if (mdr != null) { + support.firePropertyChange("messageDestinationRef", mdr, null); + mdr.setNamingResources(null); + } + + } + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + public void removePropertyChangeListener(PropertyChangeListener listener) { + + support.removePropertyChangeListener(listener); + + } + + + /** + * Remove any resource reference with the specified name. + * + * @param name Name of the resource reference to remove + */ + @Override + public void removeResource(String name) { + + entries.remove(name); + + ContextResource resource = null; + synchronized (resources) { + resource = resources.remove(name); + } + if (resource != null) { + support.firePropertyChange("resource", resource, null); + // De-register with JMX + if (resourceRequireExplicitRegistration) { + try { + MBeanUtils.destroyMBean(resource); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanDestroyFail", resource.getName()), e); + } + } + resource.setNamingResources(null); + } + } + + + /** + * Remove any resource environment reference with the specified name. + * + * @param name Name of the resource environment reference to remove + */ + public void removeResourceEnvRef(String name) { + + entries.remove(name); + + ContextResourceEnvRef resourceEnvRef = null; + synchronized (resourceEnvRefs) { + resourceEnvRef = resourceEnvRefs.remove(name); + } + if (resourceEnvRef != null) { + support.firePropertyChange("resourceEnvRef", resourceEnvRef, null); + resourceEnvRef.setNamingResources(null); + } + + } + + + /** + * Remove any resource link with the specified name. + * + * @param name Name of the resource link to remove + */ + @Override + public void removeResourceLink(String name) { + + entries.remove(name); + + ContextResourceLink resourceLink = null; + synchronized (resourceLinks) { + resourceLink = resourceLinks.remove(name); + } + if (resourceLink != null) { + support.firePropertyChange("resourceLink", resourceLink, null); + // De-register with JMX + if (resourceRequireExplicitRegistration) { + try { + MBeanUtils.destroyMBean(resourceLink); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanDestroyFail", resourceLink.getName()), e); + } + } + resourceLink.setNamingResources(null); + } + } + + + /** + * Remove any web service reference with the specified name. + * + * @param name Name of the web service reference to remove + */ + public void removeService(String name) { + + entries.remove(name); + + ContextService service = null; + synchronized (services) { + service = services.remove(name); + } + if (service != null) { + support.firePropertyChange("service", service, null); + service.setNamingResources(null); + } + + } + + + // ------------------------------------------------------- Lifecycle methods + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + + // Set this before we register currently known naming resources to avoid + // timing issues. Duplication registration is not an issue. + resourceRequireExplicitRegistration = true; + + for (ContextResource cr : resources.values()) { + try { + MBeanUtils.createMBean(cr); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanCreateFail", cr.getName()), e); + } + } + + for (ContextEnvironment ce : envs.values()) { + try { + MBeanUtils.createMBean(ce); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanCreateFail", ce.getName()), e); + } + } + + for (ContextResourceLink crl : resourceLinks.values()) { + try { + MBeanUtils.createMBean(crl); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanCreateFail", crl.getName()), e); + } + } + } + + + @Override + protected void startInternal() throws LifecycleException { + fireLifecycleEvent(CONFIGURE_START_EVENT, null); + setState(LifecycleState.STARTING); + } + + + @Override + protected void stopInternal() throws LifecycleException { + cleanUp(); + setState(LifecycleState.STOPPING); + fireLifecycleEvent(CONFIGURE_STOP_EVENT, null); + } + + /** + * Close those resources that an explicit close may help clean-up faster. + */ + private void cleanUp() { + if (resources.size() == 0) { + return; + } + javax.naming.Context ctxt; + try { + if (container instanceof Server) { + ctxt = ((Server) container).getGlobalNamingContext(); + } else { + ctxt = ContextBindings.getClassLoader(); + ctxt = (javax.naming.Context) ctxt.lookup("comp/env"); + } + } catch (NamingException e) { + log.warn(sm.getString("namingResources.cleanupNoContext", container), e); + return; + } + for (ContextResource cr : resources.values()) { + if (cr.getSingleton()) { + String closeMethod = cr.getCloseMethod(); + if (closeMethod != null && closeMethod.length() > 0) { + String name = cr.getName(); + Object resource; + try { + resource = ctxt.lookup(name); + } catch (NamingException e) { + log.warn(sm.getString("namingResources.cleanupNoResource", cr.getName(), container), e); + continue; + } + cleanUp(resource, name, closeMethod); + } + } + } + } + + + /** + * Clean up a resource by calling the defined close method. For example, closing a database connection pool will + * close it's open connections. This will happen on GC but that leaves db connections open that may cause issues. + * + * @param resource The resource to close. + */ + private void cleanUp(Object resource, String name, String closeMethod) { + // Look for a zero-arg close() method + Method m = null; + try { + m = resource.getClass().getMethod(closeMethod, (Class[]) null); + } catch (SecurityException e) { + log.debug(sm.getString("namingResources.cleanupCloseSecurity", closeMethod, name, container)); + return; + } catch (NoSuchMethodException e) { + log.debug(sm.getString("namingResources.cleanupNoClose", name, container, closeMethod)); + return; + } + try { + m.invoke(resource, (Object[]) null); + } catch (IllegalArgumentException | IllegalAccessException e) { + log.warn(sm.getString("namingResources.cleanupCloseFailed", closeMethod, name, container), e); + } catch (InvocationTargetException e) { + Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("namingResources.cleanupCloseFailed", closeMethod, name, container), t); + } + } + + @Override + protected void destroyInternal() throws LifecycleException { + + // Set this before we de-register currently known naming resources to + // avoid timing issues. Duplication de-registration is not an issue. + resourceRequireExplicitRegistration = false; + + // Destroy in reverse order to create, although it should not matter + for (ContextResourceLink crl : resourceLinks.values()) { + try { + MBeanUtils.destroyMBean(crl); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanDestroyFail", crl.getName()), e); + } + } + + for (ContextEnvironment ce : envs.values()) { + try { + MBeanUtils.destroyMBean(ce); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanDestroyFail", ce.getName()), e); + } + } + + for (ContextResource cr : resources.values()) { + try { + MBeanUtils.destroyMBean(cr); + } catch (Exception e) { + log.warn(sm.getString("namingResources.mbeanDestroyFail", cr.getName()), e); + } + } + + super.destroyInternal(); + } + + + @Override + protected String getDomainInternal() { + // Use the same domain as our associated container if we have one + Object c = getContainer(); + + if (c instanceof JmxEnabled) { + return ((JmxEnabled) c).getDomain(); + } + + return null; + } + + + @Override + protected String getObjectNameKeyProperties() { + Object c = getContainer(); + if (c instanceof Container) { + return "type=NamingResources" + ((Container) c).getMBeanKeyProperties(); + } + // Server or just unknown + return "type=NamingResources"; + } + + /** + * Checks that the configuration of the type for the specified resource is consistent with any injection targets and + * if the type is not specified, tries to configure the type based on the injection targets + * + * @param resource The resource to check + * + * @return true if the type for the resource is now valid (if previously null this means + * it is now set) or false if the current resource type is inconsistent with the injection + * targets and/or cannot be determined + */ + private boolean checkResourceType(ResourceBase resource) { + if (!(container instanceof Context)) { + // Only Context's will have injection targets + return true; + } + + if (resource.getInjectionTargets() == null || resource.getInjectionTargets().size() == 0) { + // No injection targets so use the defined type for the resource + return true; + } + + Context context = (Context) container; + + String typeName = resource.getType(); + Class typeClass = null; + if (typeName != null) { + typeClass = Introspection.loadClass(context, typeName); + if (typeClass == null) { + // Can't load the type - will trigger a failure later so don't + // fail here + return true; + } + } + + Class compatibleClass = getCompatibleType(context, resource, typeClass); + if (compatibleClass == null) { + // Indicates that a compatible type could not be identified that + // worked for all injection targets + return false; + } + + resource.setType(compatibleClass.getCanonicalName()); + return true; + } + + private Class getCompatibleType(Context context, ResourceBase resource, Class typeClass) { + + Class result = null; + + for (InjectionTarget injectionTarget : resource.getInjectionTargets()) { + Class clazz = Introspection.loadClass(context, injectionTarget.getTargetClass()); + if (clazz == null) { + // Can't load class - therefore ignore this target + continue; + } + + // Look for a match + String targetName = injectionTarget.getTargetName(); + // Look for a setter match first + Class targetType = getSetterType(clazz, targetName); + if (targetType == null) { + // Try a field match if no setter match + targetType = getFieldType(clazz, targetName); + } + if (targetType == null) { + // No match - ignore this injection target + continue; + } + targetType = Introspection.convertPrimitiveType(targetType); + + if (typeClass == null) { + // Need to find a common type amongst the injection targets + if (result == null) { + result = targetType; + } else if (targetType.isAssignableFrom(result)) { + // NO-OP - This will work + } else if (result.isAssignableFrom(targetType)) { + // Need to use more specific type + result = targetType; + } else { + // Incompatible types + return null; + } + } else { + // Each injection target needs to be consistent with the defined + // type + if (targetType.isAssignableFrom(typeClass)) { + result = typeClass; + } else { + // Incompatible types + return null; + } + } + } + return result; + } + + private Class getSetterType(Class clazz, String name) { + Method[] methods = Introspection.getDeclaredMethods(clazz); + if (methods != null && methods.length > 0) { + for (Method method : methods) { + if (Introspection.isValidSetter(method) && Introspection.getPropertyName(method).equals(name)) { + return method.getParameterTypes()[0]; + } + } + } + return null; + } + + private Class getFieldType(Class clazz, String name) { + Field[] fields = Introspection.getDeclaredFields(clazz); + if (fields != null && fields.length > 0) { + for (Field field : fields) { + if (field.getName().equals(name)) { + return field.getType(); + } + } + } + return null; + } +} diff --git a/java/org/apache/catalina/deploy/mbeans-descriptors.xml b/java/org/apache/catalina/deploy/mbeans-descriptors.xml new file mode 100644 index 0000000..fc64d1b --- /dev/null +++ b/java/org/apache/catalina/deploy/mbeans-descriptors.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/filters/AddDefaultCharsetFilter.java b/java/org/apache/catalina/filters/AddDefaultCharsetFilter.java new file mode 100644 index 0000000..3c84beb --- /dev/null +++ b/java/org/apache/catalina/filters/AddDefaultCharsetFilter.java @@ -0,0 +1,140 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.nio.charset.Charset; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + * Filter that explicitly sets the default character set for media subtypes of the "text" type to ISO-8859-1, or another + * user defined character set. RFC2616 explicitly states that browsers must use ISO-8859-1 if no character set is + * defined for media with subtype "text". However, browsers may attempt to auto-detect the character set. This may be + * exploited by an attacker to perform an XSS attack. Internet Explorer has this behaviour by default. Other browsers + * have an option to enable it.
    + * This filter prevents the attack by explicitly setting a character set. Unless the provided character set is + * explicitly overridden by the user - in which case they deserve everything they get - the browser will adhere to an + * explicitly set character set, thus preventing the XSS attack. + */ +public class AddDefaultCharsetFilter extends FilterBase { + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private final Log log = LogFactory.getLog(AddDefaultCharsetFilter.class); // must not be static + + private static final String DEFAULT_ENCODING = "ISO-8859-1"; + + private String encoding; + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + @Override + protected Log getLogger() { + return log; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); + if (encoding == null || encoding.length() == 0 || encoding.equalsIgnoreCase("default")) { + encoding = DEFAULT_ENCODING; + } else if (encoding.equalsIgnoreCase("system")) { + encoding = Charset.defaultCharset().name(); + } else if (!Charset.isSupported(encoding)) { + throw new IllegalArgumentException(sm.getString("addDefaultCharset.unsupportedCharset", encoding)); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + // Wrap the response + if (response instanceof HttpServletResponse) { + ResponseWrapper wrapped = new ResponseWrapper((HttpServletResponse) response, encoding); + chain.doFilter(request, wrapped); + } else { + chain.doFilter(request, response); + } + } + + /** + * Wrapper that adds a character set for text media types if no character set is specified. + */ + public static class ResponseWrapper extends HttpServletResponseWrapper { + + private String encoding; + + public ResponseWrapper(HttpServletResponse response, String encoding) { + super(response); + this.encoding = encoding; + } + + @Override + public void setContentType(String contentType) { + + if (contentType != null && contentType.startsWith("text/")) { + if (!contentType.contains("charset=")) { + super.setContentType(contentType + ";charset=" + encoding); + } else { + super.setContentType(contentType); + encoding = getCharacterEncoding(); + } + } else { + super.setContentType(contentType); + } + + } + + @Override + public void setHeader(String name, String value) { + if (name.trim().equalsIgnoreCase("content-type")) { + setContentType(value); + } else { + super.setHeader(name, value); + } + } + + @Override + public void addHeader(String name, String value) { + if (name.trim().equalsIgnoreCase("content-type")) { + setContentType(value); + } else { + super.addHeader(name, value); + } + } + + @Override + public void setCharacterEncoding(String charset) { + super.setCharacterEncoding(charset); + encoding = charset; + } + } +} diff --git a/java/org/apache/catalina/filters/Constants.java b/java/org/apache/catalina/filters/Constants.java new file mode 100644 index 0000000..dbec2a7 --- /dev/null +++ b/java/org/apache/catalina/filters/Constants.java @@ -0,0 +1,65 @@ +/* + * 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.catalina.filters; + + +/** + * Manifest constants for this Java package. + * + * @author Craig R. McClanahan + */ +public final class Constants { + + /** + * The session attribute key under which the CSRF nonce cache will be stored. + */ + public static final String CSRF_NONCE_SESSION_ATTR_NAME = "org.apache.catalina.filters.CSRF_NONCE"; + + /** + * The request attribute key under which the current requests's CSRF nonce can be found. + */ + public static final String CSRF_NONCE_REQUEST_ATTR_NAME = "org.apache.catalina.filters.CSRF_REQUEST_NONCE"; + + /** + * The name of the request parameter which carries CSRF nonces from the client to the server for validation. + */ + public static final String CSRF_NONCE_REQUEST_PARAM = "org.apache.catalina.filters.CSRF_NONCE"; + + /** + * The servlet context attribute key under which the CSRF request parameter name can be found. + */ + public static final String CSRF_NONCE_REQUEST_PARAM_NAME_KEY = "org.apache.catalina.filters.CSRF_NONCE_PARAM_NAME"; + + public static final String METHOD_GET = "GET"; + + public static final String CSRF_REST_NONCE_HEADER_NAME = "X-CSRF-Token"; + + public static final String CSRF_REST_NONCE_HEADER_FETCH_VALUE = "Fetch"; + + public static final String CSRF_REST_NONCE_HEADER_REQUIRED_VALUE = "Required"; + + /** + * The session attribute key under which the CSRF REST nonce cache will be stored. + */ + public static final String CSRF_REST_NONCE_SESSION_ATTR_NAME = "org.apache.catalina.filters.CSRF_REST_NONCE"; + + /** + * The servlet context attribute key under which the CSRF REST header name can be found. + */ + public static final String CSRF_REST_NONCE_HEADER_NAME_KEY = + "org.apache.catalina.filters.CSRF_REST_NONCE_HEADER_NAME"; +} diff --git a/java/org/apache/catalina/filters/CorsFilter.java b/java/org/apache/catalina/filters/CorsFilter.java new file mode 100644 index 0000000..bdc97dd --- /dev/null +++ b/java/org/apache/catalina/filters/CorsFilter.java @@ -0,0 +1,999 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.RequestUtil; +import org.apache.tomcat.util.http.ResponseUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * A {@link jakarta.servlet.Filter} that enable client-side cross-origin requests by implementing W3C's CORS + * (Cross-Origin Resource Sharing) specification for resources. Each + * {@link HttpServletRequest} request is inspected as per specification, and appropriate response headers are added to + * {@link HttpServletResponse}. + *

    + *

    + * By default, it also sets following request attributes, that help to determine the nature of the request downstream. + *

    + *
      + *
    • cors.isCorsRequest: Flag to determine if the request is a CORS request. Set to true if a CORS + * request; false otherwise.
    • + *
    • cors.request.origin: The Origin URL, i.e. the URL of the page from where the request is originated.
    • + *
    • cors.request.type: Type of request. Possible values: + *
        + *
      • SIMPLE: A request which is not preceded by a pre-flight request.
      • + *
      • ACTUAL: A request which is preceded by a pre-flight request.
      • + *
      • PRE_FLIGHT: A pre-flight request.
      • + *
      • NOT_CORS: A normal same-origin request.
      • + *
      • INVALID_CORS: A cross-origin request which is invalid.
      • + *
      + *
    • + *
    • cors.request.headers: Request headers sent as 'Access-Control-Request-Headers' header, for pre-flight + * request.
    • + *
    + * If you extend this class and override one or more of the getXxx() methods, consider whether you also need to override + * {@link CorsFilter#doFilter(ServletRequest, ServletResponse, FilterChain)} and add appropriate locking so that the + * {@code doFilter()} method executes with a consistent configuration. + * + * @see CORS specification + */ +public class CorsFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + private static final StringManager sm = StringManager.getManager(CorsFilter.class); + + private transient Log log = LogFactory.getLog(CorsFilter.class); // must not be static + + + /** + * A {@link Collection} of origins consisting of zero or more origins that are allowed access to the resource. + */ + private final Collection allowedOrigins = new HashSet<>(); + + /** + * Determines if any origin is allowed to make request. + */ + private boolean anyOriginAllowed; + + /** + * A {@link Collection} of methods consisting of zero or more methods that are supported by the resource. + */ + private final Collection allowedHttpMethods = new HashSet<>(); + + /** + * A {@link Collection} of headers consisting of zero or more header field names that are supported by the resource. + */ + private final Collection allowedHttpHeaders = new HashSet<>(); + + /** + * A {@link Collection} of exposed headers consisting of zero or more header field names of headers other than the + * simple response headers that the resource might use and can be exposed. + */ + private final Collection exposedHeaders = new HashSet<>(); + + /** + * A supports credentials flag that indicates whether the resource supports user credentials in the request. It is + * true when the resource does and false otherwise. + */ + private boolean supportsCredentials; + + /** + * Indicates (in seconds) how long the results of a pre-flight request can be cached in a pre-flight result cache. + */ + private long preflightMaxAge; + + /** + * Determines if the request should be decorated or not. + */ + private boolean decorateRequest; + + + @Override + public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, + final FilterChain filterChain) throws IOException, ServletException { + if (!(servletRequest instanceof HttpServletRequest) || !(servletResponse instanceof HttpServletResponse)) { + throw new ServletException(sm.getString("corsFilter.onlyHttp")); + } + + // Safe to downcast at this point. + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + // Determines the CORS request type. + CorsFilter.CORSRequestType requestType = checkRequestType(request); + + // Adds CORS specific attributes to request. + if (isDecorateRequest()) { + decorateCORSProperties(request, requestType); + } + switch (requestType) { + case SIMPLE: + // Handles a Simple CORS request. + case ACTUAL: + // Handles an Actual CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case PRE_FLIGHT: + // Handles a Pre-flight CORS request. + this.handlePreflightCORS(request, response, filterChain); + break; + case NOT_CORS: + // Handles a Normal request that is not a cross-origin request. + this.handleNonCORS(request, response, filterChain); + break; + default: + // Handles a CORS request that violates specification. + this.handleInvalidCORS(request, response, filterChain); + break; + } + } + + + @Override + public void init() throws ServletException { + parseAndStore(getInitParameter(PARAM_CORS_ALLOWED_ORIGINS, DEFAULT_ALLOWED_ORIGINS), + getInitParameter(PARAM_CORS_ALLOWED_METHODS, DEFAULT_ALLOWED_HTTP_METHODS), + getInitParameter(PARAM_CORS_ALLOWED_HEADERS, DEFAULT_ALLOWED_HTTP_HEADERS), + getInitParameter(PARAM_CORS_EXPOSED_HEADERS, DEFAULT_EXPOSED_HEADERS), + getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS, DEFAULT_SUPPORTS_CREDENTIALS), + getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE, DEFAULT_PREFLIGHT_MAXAGE), + getInitParameter(PARAM_CORS_REQUEST_DECORATE, DEFAULT_DECORATE_REQUEST)); + } + + + /** + * This method returns the parameter's value if it exists, or defaultValue if not. + * + * @param name The parameter's name + * @param defaultValue The default value to return if the parameter does not exist + * + * @return The parameter's value or the default value if the parameter does not exist + */ + private String getInitParameter(String name, String defaultValue) { + + String value = getInitParameter(name); + if (value != null) { + return value; + } + + return defaultValue; + } + + + /** + * Handles a CORS request of type {@link CORSRequestType}.SIMPLE. + * + * @param request The {@link HttpServletRequest} object. + * @param response The {@link HttpServletResponse} object. + * @param filterChain The {@link FilterChain} object. + * + * @throws IOException an IO error occurred + * @throws ServletException Servlet error propagation + * + * @see Simple Cross-Origin Request, Actual Request, and + * Redirects + */ + protected void handleSimpleCORS(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) throws IOException, ServletException { + + CorsFilter.CORSRequestType requestType = checkRequestType(request); + if (!(requestType == CorsFilter.CORSRequestType.SIMPLE || requestType == CorsFilter.CORSRequestType.ACTUAL)) { + throw new IllegalArgumentException(sm.getString("corsFilter.wrongType2", CorsFilter.CORSRequestType.SIMPLE, + CorsFilter.CORSRequestType.ACTUAL)); + } + + final String origin = request.getHeader(REQUEST_HEADER_ORIGIN); + final String method = request.getMethod(); + + // Section 6.1.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + if (!getAllowedHttpMethods().contains(method)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + addStandardHeaders(request, response); + + // Forward the request down the filter chain. + filterChain.doFilter(request, response); + } + + + /** + * Handles CORS pre-flight request. + * + * @param request The {@link HttpServletRequest} object. + * @param response The {@link HttpServletResponse} object. + * @param filterChain The {@link FilterChain} object. + * + * @throws IOException an IO error occurred + * @throws ServletException Servlet error propagation + */ + protected void handlePreflightCORS(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) throws IOException, ServletException { + + CORSRequestType requestType = checkRequestType(request); + if (requestType != CORSRequestType.PRE_FLIGHT) { + throw new IllegalArgumentException(sm.getString("corsFilter.wrongType1", + CORSRequestType.PRE_FLIGHT.name().toLowerCase(Locale.ENGLISH))); + } + + final String origin = request.getHeader(REQUEST_HEADER_ORIGIN); + + // Section 6.2.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.3 + String accessControlRequestMethod = request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethod == null) { + handleInvalidCORS(request, response, filterChain); + return; + } else { + accessControlRequestMethod = accessControlRequestMethod.trim(); + } + + // Section 6.2.4 + String accessControlRequestHeadersHeader = request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + List accessControlRequestHeaders = new ArrayList<>(); + if (accessControlRequestHeadersHeader != null && !accessControlRequestHeadersHeader.trim().isEmpty()) { + String[] headers = accessControlRequestHeadersHeader.trim().split(","); + for (String header : headers) { + accessControlRequestHeaders.add(header.trim().toLowerCase(Locale.ENGLISH)); + } + } + + // Section 6.2.5 + if (!getAllowedHttpMethods().contains(accessControlRequestMethod)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.6 + if (!accessControlRequestHeaders.isEmpty()) { + for (String header : accessControlRequestHeaders) { + if (!getAllowedHttpHeaders().contains(header)) { + handleInvalidCORS(request, response, filterChain); + return; + } + } + } + + addStandardHeaders(request, response); + + // Do not forward the request down the filter chain. + } + + + /** + * Handles a request, that's not a CORS request, but is a valid request i.e. it is not a cross-origin request. This + * implementation, just forwards the request down the filter chain. + * + * @param request The {@link HttpServletRequest} object. + * @param response The {@link HttpServletResponse} object. + * @param filterChain The {@link FilterChain} object. + * + * @throws IOException an IO error occurred + * @throws ServletException Servlet error propagation + */ + private void handleNonCORS(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) throws IOException, ServletException { + + if (!isAnyOriginAllowed()) { + // If only specific origins are allowed, the response will vary by + // origin + ResponseUtil.addVaryFieldName(response, REQUEST_HEADER_ORIGIN); + } + + // Let request pass. + filterChain.doFilter(request, response); + } + + + /** + * Handles a CORS request that violates specification. + * + * @param request The {@link HttpServletRequest} object. + * @param response The {@link HttpServletResponse} object. + * @param filterChain The {@link FilterChain} object. + */ + private void handleInvalidCORS(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) { + String origin = request.getHeader(REQUEST_HEADER_ORIGIN); + String method = request.getMethod(); + String accessControlRequestHeaders = request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + + response.setContentType("text/plain"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.resetBuffer(); + + if (log.isDebugEnabled()) { + sm.getString("corsFilter.invalidRequest", origin, method, accessControlRequestHeaders); + } + } + + + /* + * Sets a standard set of headers to reduce response variation which in turn is intended to aid caching. + */ + private void addStandardHeaders(final HttpServletRequest request, final HttpServletResponse response) { + + final String method = request.getMethod(); + final String origin = request.getHeader(REQUEST_HEADER_ORIGIN); + + // Local copy to avoid concurrency issues if isAnyOriginAllowed() + // is overridden. + boolean anyOriginAllowed = isAnyOriginAllowed(); + if (!anyOriginAllowed) { + // If only specific origins are allowed, the response will vary by + // origin + ResponseUtil.addVaryFieldName(response, REQUEST_HEADER_ORIGIN); + } + + // CORS requests (SIMPLE, ACTUAL, PRE_FLIGHT) set the following headers + // although non-CORS requests do not need to. The headers are always set + // as a) they do no harm in the non-CORS case and b) it allows the same + // response to be cached for CORS and non-CORS requests. + + // Add a single Access-Control-Allow-Origin header. + if (anyOriginAllowed) { + // If any origin is allowed, return header with '*'. + response.addHeader(RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + } else { + // Add a single Access-Control-Allow-Origin header, with the value + // of the Origin header as value. + response.addHeader(RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin); + } + + // If the resource supports credentials, add a single + // Access-Control-Allow-Credentials header with the case-sensitive + // string "true" as value. + if (isSupportsCredentials()) { + response.addHeader(RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } + + // If the list of exposed headers is not empty add one or more + // Access-Control-Expose-Headers headers, with as values the header + // field names given in the list of exposed headers. + // Local copy to avoid concurrency issues if getExposedHeaders() + // is overridden. + Collection exposedHeaders = getExposedHeaders(); + if (exposedHeaders != null && exposedHeaders.size() > 0) { + String exposedHeadersString = join(exposedHeaders, ","); + response.addHeader(RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, exposedHeadersString); + } + + if ("OPTIONS".equals(method)) { + // For an OPTIONS request, the response will vary based on the + // value or absence of the following headers. Hence they need be be + // included in the Vary header. + ResponseUtil.addVaryFieldName(response, REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + ResponseUtil.addVaryFieldName(response, REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + + // CORS PRE_FLIGHT (OPTIONS) requests set the following headers although + // non-CORS OPTIONS requests do not need to. The headers are always set + // as a) they do no harm in the non-CORS case and b) it allows the same + // response to be cached for CORS and non-CORS requests. + // Local copy to avoid concurrency issues if getPreflightMaxAge() + // is overridden. + long preflightMaxAge = getPreflightMaxAge(); + if (preflightMaxAge > 0) { + response.addHeader(RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE, String.valueOf(preflightMaxAge)); + } + + // Local copy to avoid concurrency issues if getAllowedHttpMethods() + // is overridden. + Collection allowedHttpMethods = getAllowedHttpMethods(); + if (allowedHttpMethods != null && !allowedHttpMethods.isEmpty()) { + response.addHeader(RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS, join(allowedHttpMethods, ",")); + } + + // Local copy to avoid concurrency issues if getAllowedHttpHeaders() + // is overridden. + Collection allowedHttpHeaders = getAllowedHttpHeaders(); + if (allowedHttpHeaders != null && !allowedHttpHeaders.isEmpty()) { + response.addHeader(RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, join(allowedHttpHeaders, ",")); + } + } + } + + + /** + * Decorates the {@link HttpServletRequest}, with CORS attributes. + *
      + *
    • cors.isCorsRequest: Flag to determine if request is a CORS request. Set to true if CORS + * request; false otherwise.
    • + *
    • cors.request.origin: The Origin URL.
    • + *
    • cors.request.type: Type of request. Values: simple or preflight or + * not_cors or invalid_cors
    • + *
    • cors.request.headers: Request headers sent as 'Access-Control-Request-Headers' header, for pre-flight + * request.
    • + *
    + * + * @param request The {@link HttpServletRequest} object. + * @param corsRequestType The {@link CORSRequestType} object. + */ + protected static void decorateCORSProperties(final HttpServletRequest request, + final CORSRequestType corsRequestType) { + if (request == null) { + throw new IllegalArgumentException(sm.getString("corsFilter.nullRequest")); + } + + if (corsRequestType == null) { + throw new IllegalArgumentException(sm.getString("corsFilter.nullRequestType")); + } + + switch (corsRequestType) { + case SIMPLE: + case ACTUAL: + request.setAttribute(HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, Boolean.TRUE); + request.setAttribute(HTTP_REQUEST_ATTRIBUTE_ORIGIN, request.getHeader(REQUEST_HEADER_ORIGIN)); + request.setAttribute(HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase(Locale.ENGLISH)); + break; + case PRE_FLIGHT: + request.setAttribute(HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, Boolean.TRUE); + request.setAttribute(HTTP_REQUEST_ATTRIBUTE_ORIGIN, request.getHeader(REQUEST_HEADER_ORIGIN)); + request.setAttribute(HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase(Locale.ENGLISH)); + String headers = request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + if (headers == null) { + headers = ""; + } + request.setAttribute(HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS, headers); + break; + case NOT_CORS: + request.setAttribute(HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, Boolean.FALSE); + break; + default: + // Don't set any attributes + break; + } + } + + + /** + * Joins elements of {@link Set} into a string, where each element is separated by the provided separator. + * + * @param elements The {@link Set} containing elements to join together. + * @param joinSeparator The character to be used for separating elements. + * + * @return The joined {@link String}; null if elements {@link Set} is null. + */ + protected static String join(final Collection elements, final String joinSeparator) { + String separator = ","; + if (elements == null) { + return null; + } + if (joinSeparator != null) { + separator = joinSeparator; + } + StringBuilder buffer = new StringBuilder(); + boolean isFirst = true; + for (String element : elements) { + if (!isFirst) { + buffer.append(separator); + } else { + isFirst = false; + } + + if (element != null) { + buffer.append(element); + } + } + + return buffer.toString(); + } + + + /** + * Determines the request type. + * + * @param request The HTTP Servlet request + * + * @return the CORS type + */ + protected CORSRequestType checkRequestType(final HttpServletRequest request) { + CORSRequestType requestType = CORSRequestType.INVALID_CORS; + if (request == null) { + throw new IllegalArgumentException(sm.getString("corsFilter.nullRequest")); + } + String originHeader = request.getHeader(REQUEST_HEADER_ORIGIN); + // Section 6.1.1 and Section 6.2.1 + if (originHeader != null) { + if (originHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else if (!RequestUtil.isValidOrigin(originHeader)) { + requestType = CORSRequestType.INVALID_CORS; + } else if (RequestUtil.isSameOrigin(request, originHeader)) { + return CORSRequestType.NOT_CORS; + } else { + String method = request.getMethod(); + if (method != null) { + if ("OPTIONS".equals(method)) { + String accessControlRequestMethodHeader = + request.getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethodHeader != null && !accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.PRE_FLIGHT; + } else if (accessControlRequestMethodHeader != null && + accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else { + requestType = CORSRequestType.ACTUAL; + } + } else if ("GET".equals(method) || "HEAD".equals(method)) { + requestType = CORSRequestType.SIMPLE; + } else if ("POST".equals(method)) { + String mediaType = getMediaType(request.getContentType()); + if (mediaType != null) { + if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES.contains(mediaType)) { + requestType = CORSRequestType.SIMPLE; + } else { + requestType = CORSRequestType.ACTUAL; + } + } + } else { + requestType = CORSRequestType.ACTUAL; + } + } + } + } else { + requestType = CORSRequestType.NOT_CORS; + } + + return requestType; + } + + + /** + * Return the lower case, trimmed value of the media type from the content type. + */ + private String getMediaType(String contentType) { + if (contentType == null) { + return null; + } + String result = contentType.toLowerCase(Locale.ENGLISH); + int firstSemiColonIndex = result.indexOf(';'); + if (firstSemiColonIndex > -1) { + result = result.substring(0, firstSemiColonIndex); + } + result = result.trim(); + return result; + } + + /** + * Checks if the Origin is allowed to make a CORS request. + * + * @param origin The Origin. + * + * @return true if origin is allowed; false otherwise. + */ + private boolean isOriginAllowed(final String origin) { + if (isAnyOriginAllowed()) { + return true; + } + + // If 'Origin' header is a case-sensitive match of any of allowed + // origins, then return true, else return false. + return getAllowedOrigins().contains(origin); + } + + + /** + * Parses each param-value and populates configuration variables. If a param is provided, it overrides the default. + * + * @param allowedOrigins A {@link String} of comma separated origins. + * @param allowedHttpMethods A {@link String} of comma separated HTTP methods. + * @param allowedHttpHeaders A {@link String} of comma separated HTTP headers. + * @param exposedHeaders A {@link String} of comma separated headers that needs to be exposed. + * @param supportsCredentials "true" if support credentials needs to be enabled. + * @param preflightMaxAge The amount of seconds the user agent is allowed to cache the result of the pre-flight + * request. + * + * @throws ServletException If the configuration is invalid + */ + private void parseAndStore(final String allowedOrigins, final String allowedHttpMethods, + final String allowedHttpHeaders, final String exposedHeaders, final String supportsCredentials, + final String preflightMaxAge, final String decorateRequest) throws ServletException { + + if (allowedOrigins.trim().equals("*")) { + this.anyOriginAllowed = true; + } else { + this.anyOriginAllowed = false; + Set setAllowedOrigins = parseStringToSet(allowedOrigins); + this.allowedOrigins.clear(); + this.allowedOrigins.addAll(setAllowedOrigins); + } + + Set setAllowedHttpMethods = parseStringToSet(allowedHttpMethods); + this.allowedHttpMethods.clear(); + this.allowedHttpMethods.addAll(setAllowedHttpMethods); + + Set setAllowedHttpHeaders = parseStringToSet(allowedHttpHeaders); + Set lowerCaseHeaders = new HashSet<>(); + for (String header : setAllowedHttpHeaders) { + String lowerCase = header.toLowerCase(Locale.ENGLISH); + lowerCaseHeaders.add(lowerCase); + } + this.allowedHttpHeaders.clear(); + this.allowedHttpHeaders.addAll(lowerCaseHeaders); + + Set setExposedHeaders = parseStringToSet(exposedHeaders); + this.exposedHeaders.clear(); + this.exposedHeaders.addAll(setExposedHeaders); + + // For any value other than 'true' this will be false. + this.supportsCredentials = Boolean.parseBoolean(supportsCredentials); + + if (this.supportsCredentials && this.anyOriginAllowed) { + throw new ServletException(sm.getString("corsFilter.invalidSupportsCredentials")); + } + + try { + if (!preflightMaxAge.isEmpty()) { + this.preflightMaxAge = Long.parseLong(preflightMaxAge); + } else { + this.preflightMaxAge = 0L; + } + } catch (NumberFormatException e) { + throw new ServletException(sm.getString("corsFilter.invalidPreflightMaxAge"), e); + } + + // For any value other than 'true' this will be false. + this.decorateRequest = Boolean.parseBoolean(decorateRequest); + } + + /** + * Takes a comma separated list and returns a Set<String>. + * + * @param data A comma separated list of strings. + * + * @return Set$lt;String> + */ + private Set parseStringToSet(final String data) { + String[] splits; + + if (data != null && data.length() > 0) { + splits = data.split(","); + } else { + splits = new String[] {}; + } + + Set set = new HashSet<>(); + if (splits.length > 0) { + for (String split : splits) { + set.add(split.trim()); + } + } + + return set; + } + + + /** + * Determines if any origin is allowed to make CORS request. + * + * @return true if it's enabled; false otherwise. + */ + public boolean isAnyOriginAllowed() { + return anyOriginAllowed; + } + + + /** + * Obtain the headers to expose. + * + * @return the headers that should be exposed by browser. + */ + public Collection getExposedHeaders() { + return exposedHeaders; + } + + + /** + * Determines is supports credentials is enabled. + * + * @return true if the use of credentials is supported otherwise false + */ + public boolean isSupportsCredentials() { + return supportsCredentials; + } + + + /** + * Returns the preflight response cache time in seconds. + * + * @return Time to cache in seconds. + */ + public long getPreflightMaxAge() { + return preflightMaxAge; + } + + + /** + * Returns the {@link Set} of allowed origins that are allowed to make requests. + * + * @return {@link Set} + */ + public Collection getAllowedOrigins() { + return allowedOrigins; + } + + + /** + * Returns a {@link Set} of HTTP methods that are allowed to make requests. + * + * @return {@link Set} + */ + public Collection getAllowedHttpMethods() { + return allowedHttpMethods; + } + + + /** + * Returns a {@link Set} of headers support by resource. + * + * @return {@link Set} + */ + public Collection getAllowedHttpHeaders() { + return allowedHttpHeaders; + } + + + /** + * Should CORS specific attributes be added to the request. + * + * @return {@code true} if the request should be decorated, otherwise {@code false} + */ + public boolean isDecorateRequest() { + return decorateRequest; + } + + + /* + * Log objects are not Serializable but this Filter is because it extends GenericFilter. Tomcat won't serialize a + * Filter but in case something else does... + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + log = LogFactory.getLog(CorsFilter.class); + } + + + // -------------------------------------------------- CORS Response Headers + + /** + * The Access-Control-Allow-Origin header indicates whether a resource can be shared based by returning the value of + * the Origin request header in the response. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + + /** + * The Access-Control-Allow-Credentials header indicates whether the response to request can be exposed when the + * omit credentials flag is unset. When part of the response to a preflight request it indicates that the actual + * request can include user credentials. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + + /** + * The Access-Control-Expose-Headers header indicates which headers are safe to expose to the API of a CORS API + * specification + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + + /** + * The Access-Control-Max-Age header indicates how long the results of a preflight request can be cached in a + * preflight result cache. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + + /** + * The Access-Control-Allow-Methods header indicates, as part of the response to a preflight request, which methods + * can be used during the actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + + /** + * The Access-Control-Allow-Headers header indicates, as part of the response to a preflight request, which header + * field names can be used during the actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + + // -------------------------------------------------- CORS Request Headers + + /** + * The Origin header indicates where the cross-origin request or preflight request originates from. + */ + public static final String REQUEST_HEADER_ORIGIN = "Origin"; + + /** + * The Access-Control-Request-Method header indicates which method will be used in the actual request as part of the + * preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + + /** + * The Access-Control-Request-Headers header indicates which headers will be used in the actual request as part of + * the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + + // ----------------------------------------------------- Request attributes + /** + * The prefix to a CORS request attribute. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_PREFIX = "cors."; + + /** + * Attribute that contains the origin of the request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_ORIGIN = HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.origin"; + + /** + * Boolean value, suggesting if the request is a CORS request or not. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST = HTTP_REQUEST_ATTRIBUTE_PREFIX + "isCorsRequest"; + + /** + * Type of CORS request, of type {@link CORSRequestType}. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE = HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.type"; + + /** + * Request headers sent as 'Access-Control-Request-Headers' header, for pre-flight request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS = + HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.headers"; + + // -------------------------------------------------------------- Constants + /** + * Enumerates varies types of CORS requests. Also, provides utility methods to determine the request type. + */ + protected enum CORSRequestType { + /** + * A simple HTTP request, i.e. it shouldn't be pre-flighted. + */ + SIMPLE, + /** + * An HTTP request that needs to be pre-flighted. + */ + ACTUAL, + /** + * A pre-flight CORS request, to get meta information, before a non-simple HTTP request is sent. + */ + PRE_FLIGHT, + /** + * Not a CORS request, but a normal request. + */ + NOT_CORS, + /** + * An invalid CORS request, i.e. it qualifies to be a CORS request, but fails to be a valid one. + */ + INVALID_CORS + } + + /** + * {@link Collection} of media type values for the Content-Type header that will be treated as 'simple'. Note + * media-type values are compared ignoring parameters and in a case-insensitive manner. + * + * @see http://www.w3.org/TR/cors/#terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("application/x-www-form-urlencoded", "multipart/form-data", "text/plain"))); + + // ------------------------------------------------ Configuration Defaults + /** + * By default, no origins are allowed to make requests. + */ + public static final String DEFAULT_ALLOWED_ORIGINS = ""; + + /** + * By default, following methods are supported: GET, POST, HEAD and OPTIONS. + */ + public static final String DEFAULT_ALLOWED_HTTP_METHODS = "GET,POST,HEAD,OPTIONS"; + + /** + * By default, time duration to cache pre-flight response is 30 mins. + */ + public static final String DEFAULT_PREFLIGHT_MAXAGE = "1800"; + + /** + * By default, support credentials is disabled. + */ + public static final String DEFAULT_SUPPORTS_CREDENTIALS = "false"; + + /** + * By default, following headers are supported: Origin,Accept,X-Requested-With, Content-Type, + * Access-Control-Request-Method, and Access-Control-Request-Headers. + */ + public static final String DEFAULT_ALLOWED_HTTP_HEADERS = "Origin,Accept,X-Requested-With,Content-Type," + + "Access-Control-Request-Method,Access-Control-Request-Headers"; + + /** + * By default, none of the headers are exposed in response. + */ + public static final String DEFAULT_EXPOSED_HEADERS = ""; + + /** + * By default, request is decorated with CORS attributes. + */ + public static final String DEFAULT_DECORATE_REQUEST = "true"; + + // ----------------------------------------Filter Config Init param-name(s) + /** + * Key to retrieve allowed origins from {@link jakarta.servlet.FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_ORIGINS = "cors.allowed.origins"; + + /** + * Key to retrieve support credentials from {@link jakarta.servlet.FilterConfig}. + */ + public static final String PARAM_CORS_SUPPORT_CREDENTIALS = "cors.support.credentials"; + + /** + * Key to retrieve exposed headers from {@link jakarta.servlet.FilterConfig}. + */ + public static final String PARAM_CORS_EXPOSED_HEADERS = "cors.exposed.headers"; + + /** + * Key to retrieve allowed headers from {@link jakarta.servlet.FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_HEADERS = "cors.allowed.headers"; + + /** + * Key to retrieve allowed methods from {@link jakarta.servlet.FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_METHODS = "cors.allowed.methods"; + + /** + * Key to retrieve preflight max age from {@link jakarta.servlet.FilterConfig}. + */ + public static final String PARAM_CORS_PREFLIGHT_MAXAGE = "cors.preflight.maxage"; + + /** + * Key to determine if request should be decorated. + */ + public static final String PARAM_CORS_REQUEST_DECORATE = "cors.request.decorate"; +} diff --git a/java/org/apache/catalina/filters/CsrfPreventionFilter.java b/java/org/apache/catalina/filters/CsrfPreventionFilter.java new file mode 100644 index 0000000..8a17270 --- /dev/null +++ b/java/org/apache/catalina/filters/CsrfPreventionFilter.java @@ -0,0 +1,690 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.http.HttpSession; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Provides basic CSRF protection for a web application. The filter assumes that: + *
      + *
    • The filter is mapped to /*
    • + *
    • {@link HttpServletResponse#encodeRedirectURL(String)} and {@link HttpServletResponse#encodeURL(String)} are used + * to encode all URLs returned to the client + *
    + * + *

    + * CSRF protection is enabled by generating random nonce values which are + * stored in the client's HTTP session. Each URL encoded using + * {@link HttpServletResponse#encodeURL(String)} has a URL parameter added + * which, when sent to the server in a future request, will be checked + * against this stored set of nonces for validity. + *

    + * + *

    + * Some URLs should be accessible even without a valid nonce parameter value. + * These URLs are known as "entry points" because clients should be able to + * "enter" the application without first establishing any valid tokens. These + * are configured with the entryPoints filter + * init-param. + *

    + * + *

    + * Some URLs should not have nonce parameters added to them at all + */ +public class CsrfPreventionFilter extends CsrfPreventionFilterBase { + + /** + * The default set of URL patterns for which nonces will not be appended. + */ + private static final String DEFAULT_NO_NONCE_URL_PATTERNS + = "*.css, *.js, *.gif, *.png, *.jpg, *.svg, *.ico, *.jpeg, *.mjs"; + + /** + * The servlet context in which this Filter is operating. + */ + private ServletContext context; + + private final Log log = LogFactory.getLog(CsrfPreventionFilter.class); + + private final Set entryPoints = new HashSet<>(); + + private int nonceCacheSize = 5; + + private String nonceRequestParameterName = Constants.CSRF_NONCE_REQUEST_PARAM; + + /** + * Flag which determines whether this Filter is in "enforcement" mode + * (the default) or in "reporting" mode. + */ + private boolean enforce = true; + + /** + * A set of comma-separated URL patterns which will have no nonce + * parameters added to them. + */ + private String noNoncePatterns = DEFAULT_NO_NONCE_URL_PATTERNS; + + private Collection> noNoncePredicates; + + /** + * Entry points are URLs that will not be tested for the presence of a valid nonce. They are used to provide a way + * to navigate back to a protected application after navigating away from it. Entry points will be limited to HTTP + * GET requests and should not trigger any security sensitive actions. + * + * @param entryPoints Comma separated list of URLs to be configured as entry points. + */ + public void setEntryPoints(String entryPoints) { + String values[] = entryPoints.split(","); + for (String value : values) { + this.entryPoints.add(value.trim()); + } + } + + /** + * Sets the number of previously issued nonces that will be cached on a LRU basis to support parallel requests, + * limited use of the refresh and back in the browser and similar behaviors that may result in the submission of a + * previous nonce rather than the current one. If not set, the default value of 5 will be used. + * + * @param nonceCacheSize The number of nonces to cache + */ + public void setNonceCacheSize(int nonceCacheSize) { + this.nonceCacheSize = nonceCacheSize; + } + + /** + * Sets the request parameter name to use for CSRF nonces. + * + * @param parameterName The request parameter name to use for CSRF nonces. + */ + public void setNonceRequestParameterName(String parameterName) { + this.nonceRequestParameterName = parameterName; + } + + /** + * Sets the flag to enforce CSRF protection or just log failures as DEBUG + * messages. + * + * @param enforce true to enforce CSRF protection or + * false to log DEBUG messages and allow + * all requests. + */ + public void setEnforce(boolean enforce) { + this.enforce = enforce; + } + + /** + * Gets the flag to enforce CSRF protection or just log failures as DEBUG + * messages. + * + * @return true if CSRF protection will be enforced or + * false if all requests will be allowed and + * failures will be logged as DEBUG messages. + */ + public boolean isEnforce() { + return this.enforce; + } + + /** + * Sets the list of URL patterns to suppress nonce-addition for. + * + * Some URLs do not need nonces added to them such as static resources. + * By not adding nonces to those URLs, HTTP caches can be more + * effective because the CSRF prevention filter won't generate what + * look like unique URLs for those commonly-reused resources. + * + * @param patterns A comma-separated list of URL patterns that will not + * have nonces added to them. Patterns may begin or end with a + * * character to denote a suffix-match or + * prefix-match. Any matched URL will not have a CSRF nonce + * added to it when passed through + * {@link HttpServletResponse#encodeURL(String)}. + */ + public void setNoNonceURLPatterns(String patterns) { + this.noNoncePatterns = patterns; + + if (null != context) { + this.noNoncePredicates = createNoNoncePredicates(context, this.noNoncePatterns); + } + } + + /** + * Creates a collection of matchers from a comma-separated string of patterns. + * + * @param context the Servlet context + * @param patterns A comma-separated string of URL matching patterns. + * + * @return A collection of predicates representing the URL patterns. + */ + protected static Collection> createNoNoncePredicates(ServletContext context, String patterns) { + if (null == patterns || 0 == patterns.trim().length()) { + return null; + } + + if (patterns.startsWith("/") && patterns.endsWith("/")) { + return Collections.singleton(new PatternPredicate(patterns.substring(1, patterns.length() - 1))); + } + + String values[] = patterns.split(","); + + ArrayList> matchers = new ArrayList<>(values.length); + for (String value : values) { + Predicate p = createNoNoncePredicate(context, value.trim()); + + if (null != p) { + matchers.add(p); + } + } + + matchers.trimToSize(); + + return matchers; + } + + /** + * Creates a predicate that can match the specified type of pattern. + * + * @param context the Servlet context + * @param pattern The pattern to match e.g. *.foo or + * /bar/*. + * + * @return A Predicate which can match the specified pattern, or + * null if the pattern is null or blank. + */ + protected static Predicate createNoNoncePredicate(ServletContext context, String pattern) { + if (null == pattern || 0 == pattern.trim().length()) { + return null; + } + if (pattern.startsWith("mime:")) { + return new MimePredicate(context, createNoNoncePredicate(context, pattern.substring(5))); + } else if (pattern.startsWith("*")) { + return new SuffixPredicate(pattern.substring(1)); + } else if (pattern.endsWith("*")) { + return new PrefixPredicate(pattern.substring(0, pattern.length() - 1)); + } else if (pattern.startsWith("/") && pattern.endsWith("/")) { + return new PatternPredicate(pattern.substring(1, pattern.length() - 1)); + } else { + throw new IllegalArgumentException(sm.getString("csrfPrevention.unsupportedPattern", pattern)); + } + } + + /** + * A no-nonce Predicate that evaluates a MIME type instead of a URL. + * + * It can be used with any other Predicate for matching + * the actual value of the MIME type. + */ + protected static class MimePredicate implements Predicate { + private final ServletContext context; + private final Predicate predicate; + + public MimePredicate(ServletContext context, Predicate predicate) { + this.context = context; + this.predicate = predicate; + } + + @Override + public boolean test(String t) { + String mimeType = context.getMimeType(t); + + return predicate.test(mimeType); + } + + public Predicate getPredicate() { + return predicate; + } + } + + /** + * A no-nonce Predicate that matches a prefix. + */ + protected static class PrefixPredicate implements Predicate { + private final String prefix; + public PrefixPredicate(String prefix) { + this.prefix = prefix; + } + + @Override + public boolean test(String t) { + return t.startsWith(this.prefix); + } + } + + /** + * A no-nonce Predicate that matches a suffix. + */ + protected static class SuffixPredicate implements Predicate { + private final String suffix; + public SuffixPredicate(String suffix) { + this.suffix = suffix; + } + + @Override + public boolean test(String t) { + return t.endsWith(this.suffix); + } + } + + /** + * A no-nonce Predicate that matches a regular expression. + */ + protected static class PatternPredicate implements Predicate { + private final Pattern pattern; + + public PatternPredicate(String regex) { + this.pattern = Pattern.compile(regex); + } + + @Override + public boolean test(String t) { + return pattern.matcher(t).matches(); + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Set the parameters + super.init(filterConfig); + + this.context = filterConfig.getServletContext(); + + this.noNoncePredicates = createNoNoncePredicates(context, this.noNoncePatterns); + + // Put the expected request parameter name into the application scope + filterConfig.getServletContext().setAttribute(Constants.CSRF_NONCE_REQUEST_PARAM_NAME_KEY, + nonceRequestParameterName); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + ServletResponse wResponse = null; + + if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) { + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + HttpSession session = req.getSession(false); + + String requestedPath = getRequestedPath(req); + boolean skipNonceCheck = skipNonceCheck(req); + NonceCache nonceCache = null; + + if (!skipNonceCheck) { + String previousNonce = req.getParameter(nonceRequestParameterName); + + if (previousNonce == null) { + if (enforce(req, requestedPath)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("csrfPrevention.rejectNoNonce", getRequestedPath(req), + (null == session ? "(null)" : session.getId()))); + } + + res.sendError(getDenyStatus()); + return; + } else { + if (log.isTraceEnabled()) { + log.trace("Would have rejected request for " + getRequestedPath(req) + ", session " + + (null == session ? "(null)" : session.getId()) + + " with no CSRF nonce found in request"); + } + } + } else { + nonceCache = getNonceCache(req, session); + if (nonceCache == null) { + if (enforce(req, requestedPath)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("csrfPrevention.rejectNoCache", getRequestedPath(req), + (null == session ? "(null)" : session.getId()))); + } + + res.sendError(getDenyStatus()); + return; + } else { + if (log.isTraceEnabled()) { + log.trace("Would have rejecting request for " + getRequestedPath(req) + ", session " + + (null == session ? "(null)" : session.getId()) + " due to empty / missing nonce cache"); + } + } + } else if (!nonceCache.contains(previousNonce)) { + if (enforce(req, requestedPath)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("csrfPrevention.rejectInvalidNonce", getRequestedPath(req), + (null == session ? "(null)" : session.getId()), previousNonce)); + } + + res.sendError(getDenyStatus()); + return; + } else { + if (log.isTraceEnabled()) { + log.trace("Would have rejecting request for " + getRequestedPath(req) + ", session " + + (null == session ? "(null)" : session.getId()) + " due to invalid nonce " + + previousNonce); + } + } + } else { + if (log.isTraceEnabled()) { + log.trace( + "Allowing request to " + getRequestedPath(req) + " with valid CSRF nonce " + previousNonce); + } + } + } + } + + if (!skipNonceGeneration(req)) { + if (skipNonceCheck) { + // Didn't look up nonce cache earlier so look it up now. + nonceCache = getNonceCache(req, session); + } + if (nonceCache == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("csrfPrevention.createCache", Integer.valueOf(nonceCacheSize), + (null == session ? "(null)" : session.getId()))); + } + + if (session == null) { + if (log.isTraceEnabled()) { + log.trace("Creating new session to store CSRF nonce cache"); + } + + session = req.getSession(true); + } + + nonceCache = createNonceCache(req, session); + } + + String newNonce = generateNonce(req); + + nonceCache.add(newNonce); + + // Take this request's nonce and put it into the request + // attributes so pages can make direct use of it, rather than + // requiring the use of response.encodeURL. + request.setAttribute(Constants.CSRF_NONCE_REQUEST_ATTR_NAME, newNonce); + + wResponse = new CsrfResponseWrapper(res, nonceRequestParameterName, newNonce, noNoncePredicates); + } + } + + chain.doFilter(request, wResponse == null ? response : wResponse); + } + + /** + * Check to see if the request and path should be enforced or only + * observed and reported. + * + * Note that the requestedPath parameter is purely + * a performance optimization to avoid calling + * {@link #getRequestedPath(HttpServletRequest)} multiple times. + * + * @param req The request. + * @param requestedPath The path of the request being evaluated. + * + * @return true if the CSRF prevention should be enforced, + * false if the CSRF prevention should only be + * logged in DEBUG mode. + */ + protected boolean enforce(HttpServletRequest req, String requestedPath) { + return isEnforce(); + } + + protected boolean skipNonceCheck(HttpServletRequest request) { + if (!Constants.METHOD_GET.equals(request.getMethod())) { + return false; + } + + String requestedPath = getRequestedPath(request); + + if (entryPoints.contains(requestedPath)) { + if (log.isTraceEnabled()) { + log.trace("Skipping CSRF nonce-check for GET request to entry point " + requestedPath); + } + + return true; + } + + if (null != noNoncePredicates && !noNoncePredicates.isEmpty()) { + for (Predicate p : noNoncePredicates) { + if (p.test(requestedPath)) { + if (log.isTraceEnabled()) { + log.trace("Skipping CSRF nonce-check for GET request to no-nonce path " + requestedPath); + } + + return true; + } + } + } + + return false; + } + + + /** + * Determines whether a nonce should be created. This method is provided primarily for the benefit of sub-classes + * that wish to customise this behaviour. + * + * @param request The request that triggered the need to potentially create the nonce. + * + * @return {@code true} if a nonce should be created, otherwise {@code false} + */ + protected boolean skipNonceGeneration(HttpServletRequest request) { + return false; + } + + + /** + * Create a new {@link NonceCache} and store in the {@link HttpSession}. This method is provided primarily for the + * benefit of sub-classes that wish to customise this behaviour. + * + * @param request The request that triggered the need to create the nonce cache. Unused by the default + * implementation. + * @param session The session associated with the request. + * + * @return A newly created {@link NonceCache} + */ + protected NonceCache createNonceCache(HttpServletRequest request, HttpSession session) { + + NonceCache nonceCache = new LruCache<>(nonceCacheSize); + + session.setAttribute(Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonceCache); + + return nonceCache; + } + + + /** + * Obtain the {@link NonceCache} associated with the request and/or session. This method is provided primarily for + * the benefit of sub-classes that wish to customise this behaviour. + * + * @param request The request that triggered the need to obtain the nonce cache. Unused by the default + * implementation. + * @param session The session associated with the request. + * + * @return The {@link NonceCache} currently associated with the request and/or session + */ + protected NonceCache getNonceCache(HttpServletRequest request, HttpSession session) { + if (session == null) { + return null; + } + @SuppressWarnings("unchecked") + NonceCache nonceCache = + (NonceCache) session.getAttribute(Constants.CSRF_NONCE_SESSION_ATTR_NAME); + return nonceCache; + } + + protected static class CsrfResponseWrapper extends HttpServletResponseWrapper { + + private final String nonceRequestParameterName; + private final String nonce; + private final Collection> noNoncePatterns; + + public CsrfResponseWrapper(HttpServletResponse response, String nonceRequestParameterName, + String nonce, Collection> noNoncePatterns) { + super(response); + this.nonceRequestParameterName = nonceRequestParameterName; + this.nonce = nonce; + this.noNoncePatterns = noNoncePatterns; + } + + @Override + public String encodeRedirectURL(String url) { + if (shouldAddNonce(url)) { + return addNonce(super.encodeRedirectURL(url)); + } else { + return url; + } + } + + @Override + public String encodeURL(String url) { + if (shouldAddNonce(url)) { + return addNonce(super.encodeURL(url)); + } else { + return url; + } + } + + private boolean shouldAddNonce(String url) { + if (null == noNoncePatterns || noNoncePatterns.isEmpty()) { + return true; + } + + if (null != noNoncePatterns) { + for (Predicate p : noNoncePatterns) { + if (p.test(url)) { + return false; + } + } + } + + return true; + } + + /* + * Return the specified URL with the nonce added to the query string. + * + * @param url URL to be modified + */ + private String addNonce(String url) { + + if (url == null || nonce == null) { + return url; + } + + String path = url; + String query = ""; + String anchor = ""; + int pound = path.indexOf('#'); + if (pound >= 0) { + anchor = path.substring(pound); + path = path.substring(0, pound); + } + int question = path.indexOf('?'); + if (question >= 0) { + query = path.substring(question); + path = path.substring(0, question); + } + StringBuilder sb = new StringBuilder(path); + if (query.length() > 0) { + sb.append(query); + sb.append('&'); + } else { + sb.append('?'); + } + sb.append(nonceRequestParameterName); + sb.append('='); + sb.append(nonce); + sb.append(anchor); + return sb.toString(); + } + } + + + protected interface NonceCache extends Serializable { + void add(T nonce); + + boolean contains(T nonce); + } + + + /** + * Despite its name, this is a FIFO cache not an LRU cache. Using an older nonce should not delay its removal from + * the cache in favour of more recent values. + * + * @param The type held by this cache. + */ + protected static class LruCache implements NonceCache { + + private static final long serialVersionUID = 1L; + + // Although the internal implementation uses a Map, this cache + // implementation is only concerned with the keys. + private final Map cache; + + public LruCache(final int cacheSize) { + cache = new LinkedHashMap<>() { + private static final long serialVersionUID = 1L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > cacheSize) { + return true; + } + return false; + } + }; + } + + @Override + public void add(T key) { + synchronized (cache) { + cache.put(key, null); + } + } + + @Override + public boolean contains(T key) { + synchronized (cache) { + return cache.containsKey(key); + } + } + } +} diff --git a/java/org/apache/catalina/filters/CsrfPreventionFilterBase.java b/java/org/apache/catalina/filters/CsrfPreventionFilterBase.java new file mode 100644 index 0000000..be4df11 --- /dev/null +++ b/java/org/apache/catalina/filters/CsrfPreventionFilterBase.java @@ -0,0 +1,135 @@ +/* + * 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.catalina.filters; + +import java.security.SecureRandom; +import java.util.Random; + +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public abstract class CsrfPreventionFilterBase extends FilterBase { + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private final Log log = LogFactory.getLog(CsrfPreventionFilterBase.class); // must not be static + + private String randomClass = SecureRandom.class.getName(); + + private Random randomSource; + + private int denyStatus = HttpServletResponse.SC_FORBIDDEN; + + @Override + protected Log getLogger() { + return log; + } + + /** + * @return response status code that is used to reject denied request. + */ + public int getDenyStatus() { + return denyStatus; + } + + /** + * Set response status code that is used to reject denied request. If none set, the default value of 403 will be + * used. + * + * @param denyStatus HTTP status code + */ + public void setDenyStatus(int denyStatus) { + this.denyStatus = denyStatus; + } + + /** + * Specify the class to use to generate the nonces. Must be in instance of {@link Random}. + * + * @param randomClass The name of the class to use + */ + public void setRandomClass(String randomClass) { + this.randomClass = randomClass; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Set the parameters + super.init(filterConfig); + + try { + Class clazz = Class.forName(randomClass); + randomSource = (Random) clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + ServletException se = + new ServletException(sm.getString("csrfPrevention.invalidRandomClass", randomClass), e); + throw se; + } + } + + @Override + protected boolean isConfigProblemFatal() { + return true; + } + + + /** + * Generate a once time token (nonce) for authenticating subsequent requests. The nonce generation is a simplified + * version of ManagerBase.generateSessionId(). + * + * @param request The request. Unused in this method but present for the the benefit of sub-classes. + * + * @return the generated nonce + */ + protected String generateNonce(HttpServletRequest request) { + byte random[] = new byte[16]; + + // Render the result as a String of hexadecimal digits + StringBuilder buffer = new StringBuilder(); + + randomSource.nextBytes(random); + + for (byte b : random) { + byte b1 = (byte) ((b & 0xf0) >> 4); + byte b2 = (byte) (b & 0x0f); + if (b1 < 10) { + buffer.append((char) ('0' + b1)); + } else { + buffer.append((char) ('A' + (b1 - 10))); + } + if (b2 < 10) { + buffer.append((char) ('0' + b2)); + } else { + buffer.append((char) ('A' + (b2 - 10))); + } + } + + return buffer.toString(); + } + + protected String getRequestedPath(HttpServletRequest request) { + String path = request.getServletPath(); + if (request.getPathInfo() != null) { + path = path + request.getPathInfo(); + } + return path; + } +} diff --git a/java/org/apache/catalina/filters/ExpiresFilter.java b/java/org/apache/catalina/filters/ExpiresFilter.java new file mode 100644 index 0000000..b2215cd --- /dev/null +++ b/java/org/apache/catalina/filters/ExpiresFilter.java @@ -0,0 +1,1591 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.http.MappingMatch; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.StringUtils; + +/** + *

    + * ExpiresFilter is a Java Servlet API port of Apache + * mod_expires to add '{@code Expires}' and '{@code Cache-Control: max-age=}' headers to HTTP response according to + * its '{@code Content-Type}'. + *

    + *

    + * Following documentation is inspired by + * mod_expires + *

    + *

    Summary

    + *

    + * This filter controls the setting of the {@code Expires} HTTP header and the {@code max-age} directive of the + * {@code Cache-Control} HTTP header in server responses. The expiration date can set to be relative to either the time + * the source file was last modified, or to the time of the client access. + *

    + *

    + * These HTTP headers are an instruction to the client about the document's validity and persistence. If cached, + * the document may be fetched from the cache rather than from the source until this time has passed. After that, the + * cache copy is considered "expired" and invalid, and a new copy must be obtained from the source. + *

    + *

    + * To modify {@code Cache-Control} directives other than {@code max-age} (see + * RFC 2616 section 14.9), you can use + * other servlet filters or Apache Httpd + * mod_headers module. + *

    + *

    Filter Configuration

    + *

    Basic configuration to add '{@code Expires}' and '{@code Cache-Control: max-age=}' headers to images, CSS and + * JavaScript

    + * + *
    + * {@code
    + * 
    + *    ...
    + *    
    + *       ExpiresFilter
    + *       org.apache.catalina.filters.ExpiresFilter
    + *       
    + *          ExpiresByType image
    + *          access plus 10 minutes
    + *       
    + *       
    + *          ExpiresByType text/css
    + *          access plus 10 minutes
    + *       
    + *       
    + *          ExpiresByType text/javascript
    + *          access plus 10 minutes
    + *       
    + *    
    + *    ...
    + *    
    + *       ExpiresFilter
    + *       /*
    + *       REQUEST
    + *    
    + *    ...
    + * 
    + * }
    + * 
    + * + *

    Configuration Parameters

    + *

    {@code ExpiresByType }

    + *

    + * This directive defines the value of the {@code Expires} header and the {@code max-age} directive of the + * {@code Cache-Control} header generated for documents of the specified type (e.g., {@code text/html}). The + * second argument sets the number of seconds that will be added to a base time to construct the expiration date. The + * {@code Cache-Control: max-age} is calculated by subtracting the request time from the expiration date and expressing + * the result in seconds. + *

    + *

    + * The base time is either the last modification time of the file, or the time of the client's access to the + * document. Which should be used is specified by the {@code } field; {@code M} means that the + * file's last modification time should be used as the base time, and + * {@code A} means the client's access time should be used. The duration + * is expressed in seconds. {@code A2592000} stands for + * {@code access plus 30 days} in alternate syntax. + *

    + *

    + * The difference in effect is subtle. If {@code M} ({@code modification} in + * alternate syntax) is used, all current copies of the document in all caches + * will expire at the same time, which can be good for something like a weekly + * notice that's always found at the same URL. If {@code A} ( + * {@code access} or {@code now} in alternate syntax) is used, the date of + * expiration is different for each client; this can be good for image files + * that don't change very often, particularly for a set of related + * documents that all refer to the same images (i.e., the images will be + * accessed repeatedly within a relatively short timespan). + *

    + *

    + * Example: + *

    + * + *
    + * {@code
    + * 
    + *    ExpiresByType text/html
    + *    access plus 1 month 15 days 2 hours
    + * 
    + *
    + * 
    + *    
    + *    ExpiresByType image/gif
    + *    A2592000
    + * 
    + * }
    + * 
    + *

    + * Note that this directive only has effect if {@code ExpiresActive On} has + * been specified. It overrides, for the specified MIME type only, any + * expiration date set by the {@code ExpiresDefault} directive. + *

    + *

    + * You can also specify the expiration time calculation using an alternate + * syntax, described earlier in this document. + *

    + *

    + * {@code ExpiresExcludedResponseStatusCodes}

    + *

    + * This directive defines the http response status codes for which the + * {@code ExpiresFilter} will not generate expiration headers. By default, the + * {@code 304} status code ("{@code Not modified}") is skipped. The + * value is a comma separated list of http status codes. + *

    + *

    + * This directive is useful to ease usage of {@code ExpiresDefault} directive. + * Indeed, the behavior of {@code 304 Not modified} (which does specify a + * {@code Content-Type} header) combined with {@code Expires} and + * {@code Cache-Control:max-age=} headers can be unnecessarily tricky to + * understand. + *

    + *

    + * Configuration sample : + *

    + * + *
    + * {@code
    + * 
    + *    ExpiresExcludedResponseStatusCodes
    + *    302, 500, 503
    + * 
    + * }
    + * 
    + * + *

    ExpiresDefault

    + *

    + * This directive sets the default algorithm for calculating the expiration time + * for all documents in the affected realm. It can be overridden on a + * type-by-type basis by the {@code ExpiresByType} directive. See the + * description of that directive for details about the syntax of the argument, + * and the "alternate syntax" description as well. + *

    + *

    Alternate Syntax

    + *

    + * The {@code ExpiresDefault} and {@code ExpiresByType} directives can also be + * defined in a more readable syntax of the form: + *

    + * + *
    + * {@code
    + * 
    + *    ExpiresDefault
    + *     [plus] ( )*
    + * 
    + *
    + * 
    + *    ExpiresByType type/encoding
    + *     [plus] ( )*
    + * 
    + * }
    + * 
    + *

    + * where {@code } is one of: + *

    + *
      + *
    • {@code access}
    • + *
    • {@code now} (equivalent to '{@code access}')
    • + *
    • {@code modification}
    • + *
    + *

    + * The {@code plus} keyword is optional. {@code } should be an + * integer value (acceptable to {@code Integer.parseInt()}), and + * {@code } is one of: + *

    + *
      + *
    • {@code years}
    • + *
    • {@code months}
    • + *
    • {@code weeks}
    • + *
    • {@code days}
    • + *
    • {@code hours}
    • + *
    • {@code minutes}
    • + *
    • {@code seconds}
    • + *
    + *

    + * For example, any of the following directives can be used to make documents + * expire 1 month after being accessed, by default: + *

    + * + *
    + * {@code
    + * 
    + *    ExpiresDefault
    + *    access plus 1 month
    + * 
    + *
    + * 
    + *    ExpiresDefault
    + *    access plus 4 weeks
    + * 
    + *
    + * 
    + *    ExpiresDefault
    + *    access plus 30 days
    + * 
    + * }
    + * 
    + *

    + * The expiry time can be fine-tuned by adding several ' + * {@code }' clauses: + *

    + * + *
    + * {@code
    + * 
    + *    ExpiresByType text/html
    + *    access plus 1 month 15 days 2 hours
    + * 
    + *
    + * 
    + *    ExpiresByType image/gif
    + *    modification plus 5 hours 3 minutes
    + * 
    + * }
    + * 
    + *

    + * Note that if you use a modification date based setting, the {@code Expires} + * header will not be added to content that does not come from + * a file on disk. This is due to the fact that there is no modification time + * for such content. + *

    + *

    Expiration headers generation eligibility

    + *

    + * A response is eligible to be enriched by {@code ExpiresFilter} if : + *

    + *
      + *
    1. no expiration header is defined ({@code Expires} header or the + * {@code max-age} directive of the {@code Cache-Control} header),
    2. + *
    3. the response status code is not excluded by the directive + * {@code ExpiresExcludedResponseStatusCodes},
    4. + *
    5. the {@code Content-Type} of the response matches one of the types + * defined the in {@code ExpiresByType} directives or the + * {@code ExpiresDefault} directive is defined.
    6. + *
    + *

    + * Note : + *

    + *
      + *
    • If {@code Cache-Control} header contains other directives than + * {@code max-age}, they are concatenated with the {@code max-age} directive + * that is added by the {@code ExpiresFilter}.
    • + *
    + *

    Expiration configuration selection

    + *

    + * The expiration configuration if elected according to the following algorithm: + *

    + *
      + *
    1. {@code ExpiresByType} matching the exact content-type returned by + * {@code HttpServletResponse.getContentType()} possibly including the charset + * (e.g. '{@code text/xml;charset=UTF-8}'),
    2. + *
    3. {@code ExpiresByType} matching the content-type without the charset if + * {@code HttpServletResponse.getContentType()} contains a charset (e.g. ' + * {@code text/xml;charset=UTF-8}' -> '{@code text/xml}'),
    4. + *
    5. {@code ExpiresByType} matching the major type (e.g. substring before + * '{@code /}') of {@code HttpServletResponse.getContentType()} + * (e.g. '{@code text/xml;charset=UTF-8}' -> '{@code text} + * '),
    6. + *
    7. {@code ExpiresDefault}
    8. + *
    + *

    Implementation Details

    When to write the expiration headers ?

    + *

    + * The {@code ExpiresFilter} traps the 'on before write response + * body' event to decide whether it should generate expiration headers or + * not. + *

    + *

    + * To trap the 'before write response body' event, the + * {@code ExpiresFilter} wraps the http servlet response's writer and + * outputStream to intercept calls to the methods {@code write()}, + * {@code print()}, {@code close()} and {@code flush()}. For empty response + * body (e.g. empty files), the {@code write()}, {@code print()}, + * {@code close()} and {@code flush()} methods are not called; to handle this + * case, the {@code ExpiresFilter}, at the end of its {@code doFilter()} + * method, manually triggers the {@code onBeforeWriteResponseBody()} method. + *

    + *

    Configuration syntax

    + *

    + * The {@code ExpiresFilter} supports the same configuration syntax as Apache + * Httpd mod_expires. + *

    + *

    + * A challenge has been to choose the name of the {@code } + * associated with {@code ExpiresByType} in the {@code } + * declaration. Indeed, Several {@code ExpiresByType} directives can be + * declared when {@code web.xml} syntax does not allow to declare several + * {@code } with the same name. + *

    + *

    + * The workaround has been to declare the content type in the + * {@code } rather than in the {@code }. + *

    + *

    Designed for extension : the open/close principle

    + *

    + * The {@code ExpiresFilter} has been designed for extension following the + * open/close principle. + *

    + *

    + * Key methods to override for extension are : + *

    + *
      + *
    • + * {@link #isEligibleToExpirationHeaderGeneration(HttpServletRequest, XHttpServletResponse)} + *
    • + *
    • + * {@link #getExpirationDate(HttpServletRequest, XHttpServletResponse)}
    • + *
    + *

    Troubleshooting

    + *

    + * To troubleshoot, enable logging on the + * {@code org.apache.catalina.filters.ExpiresFilter}. + *

    + *

    + * Extract of logging.properties + *

    + * + * + * org.apache.catalina.filters.ExpiresFilter.level = FINE + * + *

    + * Sample of initialization log message : + *

    + * + * Mar 26, 2010 2:01:41 PM org.apache.catalina.filters.ExpiresFilter init + * FINE: Filter initialized with configuration ExpiresFilter[ + * excludedResponseStatusCode=[304], + * default=null, + * byType={ + * image=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]], + * text/css=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]], + * text/javascript=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]]}] + * + *

    + * Sample of per-request log message where {@code ExpiresFilter} adds an expiration date + *

    + * + * Mar 26, 2010 2:09:47 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody + * FINE: Request "/tomcat.gif" with response status "200" content-type "image/gif", set expiration date 3/26/10 2:19 PM + * + *

    + * Sample of per-request log message where {@code ExpiresFilter} does not add an expiration date + *

    + * + * Mar 26, 2010 2:10:27 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody + * FINE: Request "/docs/config/manager.html" with response status "200" content-type "text/html", no expiration configured + * + */ +public class ExpiresFilter extends FilterBase { + + /** + * Duration composed of an {@link #amount} and a {@link #unit} + */ + protected static class Duration { + + protected final int amount; + + protected final DurationUnit unit; + + public Duration(int amount, DurationUnit unit) { + super(); + this.amount = amount; + this.unit = unit; + } + + public int getAmount() { + return amount; + } + + public DurationUnit getUnit() { + return unit; + } + + @Override + public String toString() { + return amount + " " + unit; + } + } + + /** + * Duration unit + */ + protected enum DurationUnit { + DAY(Calendar.DAY_OF_YEAR), + HOUR(Calendar.HOUR), + MINUTE(Calendar.MINUTE), + MONTH(Calendar.MONTH), + SECOND(Calendar.SECOND), + WEEK(Calendar.WEEK_OF_YEAR), + YEAR(Calendar.YEAR); + + private final int calendarField; + + DurationUnit(int calendarField) { + this.calendarField = calendarField; + } + + public int getCalendardField() { + return calendarField; + } + + } + + /** + *

    + * Main piece of configuration of the filter. + *

    + *

    + * Can be expressed like '{@code access plus 1 month 15 days 2 hours}'. + *

    + */ + protected static class ExpiresConfiguration { + /** + * List of duration elements. + */ + private final List durations; + + /** + * Starting point of the elapse to set in the response. + */ + private final StartingPoint startingPoint; + + public ExpiresConfiguration(StartingPoint startingPoint, List durations) { + super(); + this.startingPoint = startingPoint; + this.durations = durations; + } + + public List getDurations() { + return durations; + } + + public StartingPoint getStartingPoint() { + return startingPoint; + } + + @Override + public String toString() { + return "ExpiresConfiguration[startingPoint=" + startingPoint + ", duration=" + durations + "]"; + } + } + + /** + * Expiration configuration starting point. Either the time the HTML-page/servlet-response was served + * ({@link StartingPoint#ACCESS_TIME}) or the last time the HTML-page/servlet-response was modified ( + * {@link StartingPoint#LAST_MODIFICATION_TIME}). + */ + protected enum StartingPoint { + ACCESS_TIME, + LAST_MODIFICATION_TIME + } + + /** + *

    + * Wrapping extension of the {@link HttpServletResponse} to yrap the "Start Write Response Body" event. + *

    + *

    + * For performance optimization : this extended response holds the {@link #lastModifiedHeader} and + * {@link #cacheControlHeader} values access to the slow {@link #getHeader(String)} and to spare the {@code string} + * to {@code date} to {@code long} conversion. + *

    + */ + public class XHttpServletResponse extends HttpServletResponseWrapper { + + /** + * Value of the {@code Cache-Control} http response header if it has been set. + */ + private String cacheControlHeader; + + /** + * Value of the {@code Last-Modified} http response header if it has been set. + */ + private long lastModifiedHeader; + + private boolean lastModifiedHeaderSet; + + private PrintWriter printWriter; + + private final HttpServletRequest request; + + private ServletOutputStream servletOutputStream; + + /** + * Indicates whether calls to write methods ({@code write(...)}, {@code print(...)}, etc) of the response body + * have been called or not. + */ + private boolean writeResponseBodyStarted; + + public XHttpServletResponse(HttpServletRequest request, HttpServletResponse response) { + super(response); + this.request = request; + } + + @Override + public void addDateHeader(String name, long date) { + super.addDateHeader(name, date); + if (!lastModifiedHeaderSet) { + this.lastModifiedHeader = date; + this.lastModifiedHeaderSet = true; + } + } + + @Override + public void addHeader(String name, String value) { + super.addHeader(name, value); + if (HEADER_CACHE_CONTROL.equalsIgnoreCase(name) && cacheControlHeader == null) { + cacheControlHeader = value; + } + } + + public String getCacheControlHeader() { + return cacheControlHeader; + } + + public long getLastModifiedHeader() { + return lastModifiedHeader; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + if (servletOutputStream == null) { + servletOutputStream = new XServletOutputStream(super.getOutputStream(), request, this); + } + return servletOutputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (printWriter == null) { + printWriter = new XPrintWriter(super.getWriter(), request, this); + } + return printWriter; + } + + public boolean isLastModifiedHeaderSet() { + return lastModifiedHeaderSet; + } + + public boolean isWriteResponseBodyStarted() { + return writeResponseBodyStarted; + } + + @Override + public void reset() { + super.reset(); + this.lastModifiedHeader = 0; + this.lastModifiedHeaderSet = false; + this.cacheControlHeader = null; + } + + @Override + public void setDateHeader(String name, long date) { + super.setDateHeader(name, date); + if (HEADER_LAST_MODIFIED.equalsIgnoreCase(name)) { + this.lastModifiedHeader = date; + this.lastModifiedHeaderSet = true; + } + } + + @Override + public void setHeader(String name, String value) { + super.setHeader(name, value); + if (HEADER_CACHE_CONTROL.equalsIgnoreCase(name)) { + this.cacheControlHeader = value; + } + } + + public void setWriteResponseBodyStarted(boolean writeResponseBodyStarted) { + this.writeResponseBodyStarted = writeResponseBodyStarted; + } + } + + /** + * Wrapping extension of {@link PrintWriter} to trap the "Start Write Response Body" event. + */ + public class XPrintWriter extends PrintWriter { + private final PrintWriter out; + + private final HttpServletRequest request; + + private final XHttpServletResponse response; + + public XPrintWriter(PrintWriter out, HttpServletRequest request, XHttpServletResponse response) { + super(out); + this.out = out; + this.request = request; + this.response = response; + } + + @Override + public PrintWriter append(char c) { + fireBeforeWriteResponseBodyEvent(); + return out.append(c); + } + + @Override + public PrintWriter append(CharSequence csq) { + fireBeforeWriteResponseBodyEvent(); + return out.append(csq); + } + + @Override + public PrintWriter append(CharSequence csq, int start, int end) { + fireBeforeWriteResponseBodyEvent(); + return out.append(csq, start, end); + } + + @Override + public void close() { + fireBeforeWriteResponseBodyEvent(); + out.close(); + } + + private void fireBeforeWriteResponseBodyEvent() { + if (!this.response.isWriteResponseBodyStarted()) { + this.response.setWriteResponseBodyStarted(true); + onBeforeWriteResponseBody(request, response); + } + } + + @Override + public void flush() { + fireBeforeWriteResponseBodyEvent(); + out.flush(); + } + + @Override + public void print(boolean b) { + fireBeforeWriteResponseBodyEvent(); + out.print(b); + } + + @Override + public void print(char c) { + fireBeforeWriteResponseBodyEvent(); + out.print(c); + } + + @Override + public void print(char[] s) { + fireBeforeWriteResponseBodyEvent(); + out.print(s); + } + + @Override + public void print(double d) { + fireBeforeWriteResponseBodyEvent(); + out.print(d); + } + + @Override + public void print(float f) { + fireBeforeWriteResponseBodyEvent(); + out.print(f); + } + + @Override + public void print(int i) { + fireBeforeWriteResponseBodyEvent(); + out.print(i); + } + + @Override + public void print(long l) { + fireBeforeWriteResponseBodyEvent(); + out.print(l); + } + + @Override + public void print(Object obj) { + fireBeforeWriteResponseBodyEvent(); + out.print(obj); + } + + @Override + public void print(String s) { + fireBeforeWriteResponseBodyEvent(); + out.print(s); + } + + @Override + public PrintWriter printf(Locale l, String format, Object... args) { + fireBeforeWriteResponseBodyEvent(); + return out.printf(l, format, args); + } + + @Override + public PrintWriter printf(String format, Object... args) { + fireBeforeWriteResponseBodyEvent(); + return out.printf(format, args); + } + + @Override + public void println() { + fireBeforeWriteResponseBodyEvent(); + out.println(); + } + + @Override + public void println(boolean x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void println(char x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void println(char[] x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void println(double x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void println(float x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void println(int x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void println(long x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void println(Object x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void println(String x) { + fireBeforeWriteResponseBodyEvent(); + out.println(x); + } + + @Override + public void write(char[] buf) { + fireBeforeWriteResponseBodyEvent(); + out.write(buf); + } + + @Override + public void write(char[] buf, int off, int len) { + fireBeforeWriteResponseBodyEvent(); + out.write(buf, off, len); + } + + @Override + public void write(int c) { + fireBeforeWriteResponseBodyEvent(); + out.write(c); + } + + @Override + public void write(String s) { + fireBeforeWriteResponseBodyEvent(); + out.write(s); + } + + @Override + public void write(String s, int off, int len) { + fireBeforeWriteResponseBodyEvent(); + out.write(s, off, len); + } + + } + + /** + * Wrapping extension of {@link ServletOutputStream} to trap the "Start Write Response Body" event. + */ + public class XServletOutputStream extends ServletOutputStream { + + private final HttpServletRequest request; + + private final XHttpServletResponse response; + + private final ServletOutputStream servletOutputStream; + + public XServletOutputStream(ServletOutputStream servletOutputStream, HttpServletRequest request, + XHttpServletResponse response) { + super(); + this.servletOutputStream = servletOutputStream; + this.response = response; + this.request = request; + } + + @Override + public void close() throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.close(); + } + + private void fireOnBeforeWriteResponseBodyEvent() { + if (!this.response.isWriteResponseBodyStarted()) { + this.response.setWriteResponseBodyStarted(true); + onBeforeWriteResponseBody(request, response); + } + } + + @Override + public void flush() throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.flush(); + } + + @Override + public void print(boolean b) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.print(b); + } + + @Override + public void print(char c) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.print(c); + } + + @Override + public void print(double d) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.print(d); + } + + @Override + public void print(float f) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.print(f); + } + + @Override + public void print(int i) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.print(i); + } + + @Override + public void print(long l) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.print(l); + } + + @Override + public void print(String s) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.print(s); + } + + @Override + public void println() throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.println(); + } + + @Override + public void println(boolean b) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.println(b); + } + + @Override + public void println(char c) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.println(c); + } + + @Override + public void println(double d) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.println(d); + } + + @Override + public void println(float f) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.println(f); + } + + @Override + public void println(int i) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.println(i); + } + + @Override + public void println(long l) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.println(l); + } + + @Override + public void println(String s) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.println(s); + } + + @Override + public void write(byte[] b) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.write(b, off, len); + } + + @Override + public void write(int b) throws IOException { + fireOnBeforeWriteResponseBodyEvent(); + servletOutputStream.write(b); + } + + /** + * TODO SERVLET 3.1 + */ + @Override + public boolean isReady() { + return false; + } + + /** + * TODO SERVLET 3.1 + */ + @Override + public void setWriteListener(WriteListener listener) { + } + + } + + private static final String HEADER_CACHE_CONTROL = "Cache-Control"; + + private static final String HEADER_EXPIRES = "Expires"; + + private static final String HEADER_LAST_MODIFIED = "Last-Modified"; + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private final Log log = LogFactory.getLog(ExpiresFilter.class); // must not be static + + private static final String PARAMETER_EXPIRES_BY_TYPE = "ExpiresByType"; + + private static final String PARAMETER_EXPIRES_DEFAULT = "ExpiresDefault"; + + private static final String PARAMETER_EXPIRES_EXCLUDED_RESPONSE_STATUS_CODES = "ExpiresExcludedResponseStatusCodes"; + + /** + * Convert a comma delimited list of numbers into an {@code int[]}. + * + * @param commaDelimitedInts can be {@code null} + * + * @return never {@code null} array + */ + protected static int[] commaDelimitedListToIntArray(String commaDelimitedInts) { + String[] intsAsStrings = StringUtils.splitCommaSeparated(commaDelimitedInts); + int[] ints = new int[intsAsStrings.length]; + for (int i = 0; i < intsAsStrings.length; i++) { + String intAsString = intsAsStrings[i]; + try { + ints[i] = Integer.parseInt(intAsString); + } catch (NumberFormatException e) { + throw new RuntimeException( + sm.getString("expiresFilter.numberError", Integer.valueOf(i), commaDelimitedInts)); + } + } + return ints; + } + + /** + * Convert a given comma delimited list of strings into an array of String + * + * @param commaDelimitedStrings the string to be split + * + * @return array of patterns (non {@code null}) + * + * @deprecated Unused. Will be removed in Tomcat 11. + */ + @Deprecated + protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) { + return StringUtils.splitCommaSeparated(commaDelimitedStrings); + } + + /** + * @return {@code true} if the given {@code str} contains the given {@code searchStr}. + * + * @param str String that will be searched + * @param searchStr The substring to search + */ + protected static boolean contains(String str, String searchStr) { + if (str == null || searchStr == null) { + return false; + } + return str.contains(searchStr); + } + + /** + * Convert an array of ints into a comma delimited string + * + * @param ints The int array + * + * @return a comma separated string + */ + protected static String intsToCommaDelimitedString(int[] ints) { + if (ints == null) { + return ""; + } + + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < ints.length; i++) { + result.append(ints[i]); + if (i < (ints.length - 1)) { + result.append(", "); + } + } + return result.toString(); + } + + /** + * @param str The String to check + * + * @return {@code true} if the given {@code str} is {@code null} or has a zero characters length. + */ + protected static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + /** + * @param str The String to check + * + * @return {@code true} if the given {@code str} has at least one character (can be a whitespace). + */ + protected static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * @return {@code true} if the given {@code string} starts with the given {@code prefix} ignoring case. + * + * @param string can be {@code null} + * @param prefix can be {@code null} + */ + protected static boolean startsWithIgnoreCase(String string, String prefix) { + if (string == null || prefix == null) { + return string == null && prefix == null; + } + if (prefix.length() > string.length()) { + return false; + } + + return string.regionMatches(true, 0, prefix, 0, prefix.length()); + } + + /** + * @return the subset of the given {@code str} that is before the first occurrence of the given {@code separator}. + * Return {@code null} if the given {@code str} or the given {@code separator} is null. Return and empty + * string if the {@code separator} is empty. + * + * @param str can be {@code null} + * @param separator can be {@code null} + */ + protected static String substringBefore(String str, String separator) { + if (str == null || str.isEmpty() || separator == null) { + return null; + } + + if (separator.isEmpty()) { + return ""; + } + + int separatorIndex = str.indexOf(separator); + if (separatorIndex == -1) { + return str; + } + return str.substring(0, separatorIndex); + } + + /** + * Default Expires configuration. + */ + private ExpiresConfiguration defaultExpiresConfiguration; + + /** + * list of response status code for which the {@link ExpiresFilter} will not generate expiration headers. + */ + private int[] excludedResponseStatusCodes = new int[] { HttpServletResponse.SC_NOT_MODIFIED }; + + /** + * Expires configuration by content type. Visible for test. + */ + private Map expiresConfigurationByContentType = new LinkedHashMap<>(); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + if (response.isCommitted()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.responseAlreadyCommitted", httpRequest.getRequestURL())); + } + chain.doFilter(request, response); + } else { + XHttpServletResponse xResponse = new XHttpServletResponse(httpRequest, httpResponse); + chain.doFilter(request, xResponse); + if (!xResponse.isWriteResponseBodyStarted()) { + // Empty response, manually trigger + // onBeforeWriteResponseBody() + onBeforeWriteResponseBody(httpRequest, xResponse); + } + } + } else { + chain.doFilter(request, response); + } + } + + public ExpiresConfiguration getDefaultExpiresConfiguration() { + return defaultExpiresConfiguration; + } + + public String getExcludedResponseStatusCodes() { + return intsToCommaDelimitedString(excludedResponseStatusCodes); + } + + public int[] getExcludedResponseStatusCodesAsInts() { + return excludedResponseStatusCodes; + } + + + /** + * Returns the expiration date of the given {@link XHttpServletResponse} or {@code null} if no expiration date has + * been configured for the declared content type. + *

    + * {@code protected} for extension. + * + * @param request The HTTP request + * @param response The wrapped HTTP response + * + * @return the expiration date + * + * @see HttpServletResponse#getContentType() + */ + protected Date getExpirationDate(HttpServletRequest request, XHttpServletResponse response) { + String contentType = response.getContentType(); + if (contentType == null && request != null && + request.getHttpServletMapping().getMappingMatch() == MappingMatch.DEFAULT && + response.getStatus() == HttpServletResponse.SC_NOT_MODIFIED) { + // Default servlet normally sets the content type but does not for + // 304 responses. Look it up. + String servletPath = request.getServletPath(); + if (servletPath != null) { + int lastSlash = servletPath.lastIndexOf('/'); + if (lastSlash > -1) { + String fileName = servletPath.substring(lastSlash + 1); + contentType = request.getServletContext().getMimeType(fileName); + } + } + } + if (contentType != null) { + contentType = contentType.toLowerCase(Locale.ENGLISH); + } + + // lookup exact content-type match (e.g. + // "text/html; charset=iso-8859-1") + ExpiresConfiguration configuration = expiresConfigurationByContentType.get(contentType); + if (configuration != null) { + Date result = getExpirationDate(configuration, response); + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.useMatchingConfiguration", configuration, contentType, + contentType, result)); + } + return result; + } + + if (contains(contentType, ";")) { + // lookup content-type without charset match (e.g. "text/html") + String contentTypeWithoutCharset = substringBefore(contentType, ";").trim(); + configuration = expiresConfigurationByContentType.get(contentTypeWithoutCharset); + + if (configuration != null) { + Date result = getExpirationDate(configuration, response); + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.useMatchingConfiguration", configuration, + contentTypeWithoutCharset, contentType, result)); + } + return result; + } + } + + if (contains(contentType, "/")) { + // lookup major type match (e.g. "text") + String majorType = substringBefore(contentType, "/"); + configuration = expiresConfigurationByContentType.get(majorType); + if (configuration != null) { + Date result = getExpirationDate(configuration, response); + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.useMatchingConfiguration", configuration, majorType, + contentType, result)); + } + return result; + } + } + + if (defaultExpiresConfiguration != null) { + Date result = getExpirationDate(defaultExpiresConfiguration, response); + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.useDefaultConfiguration", defaultExpiresConfiguration, + contentType, result)); + } + return result; + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.noExpirationConfiguredForContentType", contentType)); + } + return null; + } + + /** + *

    + * Returns the expiration date of the given {@link ExpiresConfiguration}, {@link HttpServletRequest} and + * {@link XHttpServletResponse}. + *

    + *

    + * {@code protected} for extension. + *

    + * + * @param configuration The parsed expires + * @param response The Servlet response + * + * @return the expiration date + */ + protected Date getExpirationDate(ExpiresConfiguration configuration, XHttpServletResponse response) { + Calendar calendar; + switch (configuration.getStartingPoint()) { + case ACCESS_TIME: + calendar = Calendar.getInstance(); + break; + case LAST_MODIFICATION_TIME: + if (response.isLastModifiedHeaderSet()) { + try { + long lastModified = response.getLastModifiedHeader(); + calendar = Calendar.getInstance(); + calendar.setTimeInMillis(lastModified); + } catch (NumberFormatException e) { + // default to now + calendar = Calendar.getInstance(); + } + } else { + // Last-Modified header not found, use now + calendar = Calendar.getInstance(); + } + break; + default: + throw new IllegalStateException( + sm.getString("expiresFilter.unsupportedStartingPoint", configuration.getStartingPoint())); + } + for (Duration duration : configuration.getDurations()) { + calendar.add(duration.getUnit().getCalendardField(), duration.getAmount()); + } + + return calendar.getTime(); + } + + public Map getExpiresConfigurationByContentType() { + return expiresConfigurationByContentType; + } + + @Override + protected Log getLogger() { + return log; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + for (Enumeration names = filterConfig.getInitParameterNames(); names.hasMoreElements();) { + String name = names.nextElement(); + String value = filterConfig.getInitParameter(name); + + try { + if (name.startsWith(PARAMETER_EXPIRES_BY_TYPE)) { + String contentType = + name.substring(PARAMETER_EXPIRES_BY_TYPE.length()).trim().toLowerCase(Locale.ENGLISH); + ExpiresConfiguration expiresConfiguration = parseExpiresConfiguration(value); + this.expiresConfigurationByContentType.put(contentType, expiresConfiguration); + } else if (name.equalsIgnoreCase(PARAMETER_EXPIRES_DEFAULT)) { + ExpiresConfiguration expiresConfiguration = parseExpiresConfiguration(value); + this.defaultExpiresConfiguration = expiresConfiguration; + } else if (name.equalsIgnoreCase(PARAMETER_EXPIRES_EXCLUDED_RESPONSE_STATUS_CODES)) { + this.excludedResponseStatusCodes = commaDelimitedListToIntArray(value); + } else { + log.warn(sm.getString("expiresFilter.unknownParameterIgnored", name, value)); + } + } catch (RuntimeException e) { + throw new ServletException(sm.getString("expiresFilter.exceptionProcessingParameter", name, value), e); + } + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("expiresFilter.filterInitialized", this.toString())); + } + } + + /** + *

    + * {@code protected} for extension. + *

    + * + * @param request The Servlet request + * @param response The Servlet response + * + * @return true if an expire header may be added + */ + protected boolean isEligibleToExpirationHeaderGeneration(HttpServletRequest request, + XHttpServletResponse response) { + boolean expirationHeaderHasBeenSet = + response.containsHeader(HEADER_EXPIRES) || contains(response.getCacheControlHeader(), "max-age"); + if (expirationHeaderHasBeenSet) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.expirationHeaderAlreadyDefined", request.getRequestURI(), + Integer.valueOf(response.getStatus()), response.getContentType())); + } + return false; + } + + for (int skippedStatusCode : this.excludedResponseStatusCodes) { + if (response.getStatus() == skippedStatusCode) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.skippedStatusCode", request.getRequestURI(), + Integer.valueOf(response.getStatus()), response.getContentType())); + } + return false; + } + } + + return true; + } + + /** + *

    + * If no expiration header has been set by the servlet and an expiration has been defined in the + * {@link ExpiresFilter} configuration, sets the '{@code Expires}' header and the attribute '{@code max-age}' of the + * '{@code Cache-Control}' header. + *

    + *

    + * Must be called on the "Start Write Response Body" event. + *

    + *

    + * Invocations to {@code Logger.debug(...)} are guarded by {@link Log#isDebugEnabled()} because + * {@link HttpServletRequest#getRequestURI()} and {@link HttpServletResponse#getContentType()} costs {@code String} + * objects instantiations (as of Tomcat 7). + *

    + * + * @param request The Servlet request + * @param response The Servlet response + */ + public void onBeforeWriteResponseBody(HttpServletRequest request, XHttpServletResponse response) { + + if (!isEligibleToExpirationHeaderGeneration(request, response)) { + return; + } + + Date expirationDate = getExpirationDate(request, response); + if (expirationDate == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.noExpirationConfigured", request.getRequestURI(), + Integer.valueOf(response.getStatus()), response.getContentType())); + } + } else { + if (log.isDebugEnabled()) { + log.debug(sm.getString("expiresFilter.setExpirationDate", request.getRequestURI(), + Integer.valueOf(response.getStatus()), response.getContentType(), expirationDate)); + } + + String maxAgeDirective = "max-age=" + ((expirationDate.getTime() - System.currentTimeMillis()) / 1000); + + String cacheControlHeader = response.getCacheControlHeader(); + String newCacheControlHeader = + (cacheControlHeader == null) ? maxAgeDirective : cacheControlHeader + ", " + maxAgeDirective; + response.setHeader(HEADER_CACHE_CONTROL, newCacheControlHeader); + response.setDateHeader(HEADER_EXPIRES, expirationDate.getTime()); + } + + } + + /** + * Parse configuration lines like '{@code access plus 1 month 15 days 2 hours}' or + * '{@code modification 1 day 2 hours 5 seconds}' + * + * @param inputLine the input + * + * @return the parsed expires + */ + protected ExpiresConfiguration parseExpiresConfiguration(String inputLine) { + String line = inputLine.trim(); + + StringTokenizer tokenizer = new StringTokenizer(line, " "); + + String currentToken; + + try { + currentToken = tokenizer.nextToken(); + } catch (NoSuchElementException e) { + throw new IllegalStateException(sm.getString("expiresFilter.startingPointNotFound", line)); + } + + StartingPoint startingPoint; + if ("access".equalsIgnoreCase(currentToken) || "now".equalsIgnoreCase(currentToken)) { + startingPoint = StartingPoint.ACCESS_TIME; + } else if ("modification".equalsIgnoreCase(currentToken)) { + startingPoint = StartingPoint.LAST_MODIFICATION_TIME; + } else if (!tokenizer.hasMoreTokens() && startsWithIgnoreCase(currentToken, "a")) { + startingPoint = StartingPoint.ACCESS_TIME; + // trick : convert duration configuration from old to new style + tokenizer = new StringTokenizer(currentToken.substring(1) + " seconds", " "); + } else if (!tokenizer.hasMoreTokens() && startsWithIgnoreCase(currentToken, "m")) { + startingPoint = StartingPoint.LAST_MODIFICATION_TIME; + // trick : convert duration configuration from old to new style + tokenizer = new StringTokenizer(currentToken.substring(1) + " seconds", " "); + } else { + throw new IllegalStateException(sm.getString("expiresFilter.startingPointInvalid", currentToken, line)); + } + + try { + currentToken = tokenizer.nextToken(); + } catch (NoSuchElementException e) { + throw new IllegalStateException(sm.getString("expiresFilter.noDurationFound", line)); + } + + if ("plus".equalsIgnoreCase(currentToken)) { + // skip + try { + currentToken = tokenizer.nextToken(); + } catch (NoSuchElementException e) { + throw new IllegalStateException(sm.getString("expiresFilter.noDurationFound", line)); + } + } + + List durations = new ArrayList<>(); + + while (currentToken != null) { + int amount; + try { + amount = Integer.parseInt(currentToken); + } catch (NumberFormatException e) { + throw new IllegalStateException( + sm.getString("expiresFilter.invalidDurationNumber", currentToken, line)); + } + + try { + currentToken = tokenizer.nextToken(); + } catch (NoSuchElementException e) { + throw new IllegalStateException( + sm.getString("expiresFilter.noDurationUnitAfterAmount", Integer.valueOf(amount), line)); + } + DurationUnit durationUnit; + if ("year".equalsIgnoreCase(currentToken) || "years".equalsIgnoreCase(currentToken)) { + durationUnit = DurationUnit.YEAR; + } else if ("month".equalsIgnoreCase(currentToken) || "months".equalsIgnoreCase(currentToken)) { + durationUnit = DurationUnit.MONTH; + } else if ("week".equalsIgnoreCase(currentToken) || "weeks".equalsIgnoreCase(currentToken)) { + durationUnit = DurationUnit.WEEK; + } else if ("day".equalsIgnoreCase(currentToken) || "days".equalsIgnoreCase(currentToken)) { + durationUnit = DurationUnit.DAY; + } else if ("hour".equalsIgnoreCase(currentToken) || "hours".equalsIgnoreCase(currentToken)) { + durationUnit = DurationUnit.HOUR; + } else if ("minute".equalsIgnoreCase(currentToken) || "minutes".equalsIgnoreCase(currentToken)) { + durationUnit = DurationUnit.MINUTE; + } else if ("second".equalsIgnoreCase(currentToken) || "seconds".equalsIgnoreCase(currentToken)) { + durationUnit = DurationUnit.SECOND; + } else { + throw new IllegalStateException(sm.getString("expiresFilter.invalidDurationUnit", currentToken, line)); + } + + Duration duration = new Duration(amount, durationUnit); + durations.add(duration); + + if (tokenizer.hasMoreTokens()) { + currentToken = tokenizer.nextToken(); + } else { + currentToken = null; + } + } + + return new ExpiresConfiguration(startingPoint, durations); + } + + public void setDefaultExpiresConfiguration(ExpiresConfiguration defaultExpiresConfiguration) { + this.defaultExpiresConfiguration = defaultExpiresConfiguration; + } + + public void setExcludedResponseStatusCodes(int[] excludedResponseStatusCodes) { + this.excludedResponseStatusCodes = excludedResponseStatusCodes; + } + + public void setExpiresConfigurationByContentType( + Map expiresConfigurationByContentType) { + this.expiresConfigurationByContentType = expiresConfigurationByContentType; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[excludedResponseStatusCode=[" + + intsToCommaDelimitedString(this.excludedResponseStatusCodes) + "], default=" + + this.defaultExpiresConfiguration + ", byType=" + this.expiresConfigurationByContentType + "]"; + } +} diff --git a/java/org/apache/catalina/filters/FailedRequestFilter.java b/java/org/apache/catalina/filters/FailedRequestFilter.java new file mode 100644 index 0000000..bbed7cb --- /dev/null +++ b/java/org/apache/catalina/filters/FailedRequestFilter.java @@ -0,0 +1,115 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Globals; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.Parameters.FailReason; + +/** + * Filter that will reject requests if there was a failure during parameter parsing. This filter can be used to ensure + * that none parameter values submitted by client are lost. + *

    + * Note that parameter parsing may consume the body of an HTTP request, so caution is needed if the servlet protected by + * this filter uses request.getInputStream() or request.getReader() calls. In general the risk + * of breaking a web application by adding this filter is not so high, because parameter parsing does check content type + * of the request before consuming the request body. + */ +public class FailedRequestFilter extends FilterBase { + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private final Log log = LogFactory.getLog(FailedRequestFilter.class); // must not be static + + @Override + protected Log getLogger() { + return log; + } + + @SuppressWarnings("deprecation") + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (!isGoodRequest(request)) { + FailReason reason = (FailReason) request.getAttribute(Globals.PARAMETER_PARSE_FAILED_REASON_ATTR); + + int status; + + switch (reason) { + case IO_ERROR: + // Not the client's fault + status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + break; + case POST_TOO_LARGE: + status = HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE; + break; + case TOO_MANY_PARAMETERS: + // 413/414 aren't really correct here since the request body + // and/or URI could be well below any limits set. Use the + // default. + case UNKNOWN: // Assume the client is at fault + // Various things that the client can get wrong that don't have + // a specific status code so use the default. + case INVALID_CONTENT_TYPE: + case MULTIPART_CONFIG_INVALID: + case NO_NAME: + case REQUEST_BODY_INCOMPLETE: + case URL_DECODING: + case CLIENT_DISCONNECT: + // Client is never going to see this so this is really just + // for the access logs. The default is fine. + default: + // 400 + status = HttpServletResponse.SC_BAD_REQUEST; + break; + } + + try { + ((HttpServletResponse) response).sendError(status); + } catch (IllegalStateException e) { + // Already committed, ignore + } + return; + } + chain.doFilter(request, response); + } + + private boolean isGoodRequest(ServletRequest request) { + // Trigger parsing of parameters + request.getParameter("none"); + // Detect failure + if (request.getAttribute(Globals.PARAMETER_PARSE_FAILED_ATTR) != null) { + return false; + } + return true; + } + + @Override + protected boolean isConfigProblemFatal() { + return true; + } + +} diff --git a/java/org/apache/catalina/filters/FilterBase.java b/java/org/apache/catalina/filters/FilterBase.java new file mode 100644 index 0000000..d2522f3 --- /dev/null +++ b/java/org/apache/catalina/filters/FilterBase.java @@ -0,0 +1,73 @@ +/* + * 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.catalina.filters; + +import java.util.Enumeration; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; + +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Base class for filters that provides generic initialisation and a simple no-op destruction. + */ +public abstract class FilterBase implements Filter { + + protected static final StringManager sm = StringManager.getManager(FilterBase.class); + + protected abstract Log getLogger(); + + + /** + * Iterates over the configuration parameters and either logs a warning, or throws an exception for any parameter + * that does not have a matching setter in this filter. + * + * @param filterConfig The configuration information associated with the filter instance being initialised + * + * @throws ServletException if {@link #isConfigProblemFatal()} returns {@code true} and a configured parameter does + * not have a matching setter + */ + @Override + public void init(FilterConfig filterConfig) throws ServletException { + Enumeration paramNames = filterConfig.getInitParameterNames(); + while (paramNames.hasMoreElements()) { + String paramName = paramNames.nextElement(); + if (!IntrospectionUtils.setProperty(this, paramName, filterConfig.getInitParameter(paramName))) { + String msg = sm.getString("filterbase.noSuchProperty", paramName, this.getClass().getName()); + if (isConfigProblemFatal()) { + throw new ServletException(msg); + } else { + getLogger().warn(msg); + } + } + } + } + + /** + * Determines if an exception when calling a setter or an unknown configuration attribute triggers the failure of + * the this filter which in turn will prevent the web application from starting. + * + * @return true if a problem should trigger the failure of this filter, else false + */ + protected boolean isConfigProblemFatal() { + return false; + } +} diff --git a/java/org/apache/catalina/filters/HttpHeaderSecurityFilter.java b/java/org/apache/catalina/filters/HttpHeaderSecurityFilter.java new file mode 100644 index 0000000..e81d387 --- /dev/null +++ b/java/org/apache/catalina/filters/HttpHeaderSecurityFilter.java @@ -0,0 +1,272 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Provides a single configuration point for security measures that required the addition of one or more HTTP headers to + * the response. + */ +public class HttpHeaderSecurityFilter extends FilterBase { + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private final Log log = LogFactory.getLog(HttpHeaderSecurityFilter.class); // must not be static + + // HSTS + private static final String HSTS_HEADER_NAME = "Strict-Transport-Security"; + private boolean hstsEnabled = true; + private int hstsMaxAgeSeconds = 0; + private boolean hstsIncludeSubDomains = false; + private boolean hstsPreload = false; + private String hstsHeaderValue; + + // Click-jacking protection + private static final String ANTI_CLICK_JACKING_HEADER_NAME = "X-Frame-Options"; + private boolean antiClickJackingEnabled = true; + private XFrameOption antiClickJackingOption = XFrameOption.DENY; + private URI antiClickJackingUri; + private String antiClickJackingHeaderValue; + + // Block content sniffing + private static final String BLOCK_CONTENT_TYPE_SNIFFING_HEADER_NAME = "X-Content-Type-Options"; + private static final String BLOCK_CONTENT_TYPE_SNIFFING_HEADER_VALUE = "nosniff"; + private boolean blockContentTypeSniffingEnabled = true; + + // Cross-site scripting filter protection + @Deprecated + private static final String XSS_PROTECTION_HEADER_NAME = "X-XSS-Protection"; + @Deprecated + private static final String XSS_PROTECTION_HEADER_VALUE = "1; mode=block"; + @Deprecated + private boolean xssProtectionEnabled = false; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); + + // Build HSTS header value + StringBuilder hstsValue = new StringBuilder("max-age="); + hstsValue.append(hstsMaxAgeSeconds); + if (hstsIncludeSubDomains) { + hstsValue.append(";includeSubDomains"); + } + if (hstsPreload) { + hstsValue.append(";preload"); + } + hstsHeaderValue = hstsValue.toString(); + + // Anti click-jacking + StringBuilder cjValue = new StringBuilder(antiClickJackingOption.headerValue); + if (antiClickJackingOption == XFrameOption.ALLOW_FROM) { + cjValue.append(' '); + cjValue.append(antiClickJackingUri); + } + antiClickJackingHeaderValue = cjValue.toString(); + } + + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + if (response instanceof HttpServletResponse) { + HttpServletResponse httpResponse = (HttpServletResponse) response; + + if (response.isCommitted()) { + throw new ServletException(sm.getString("httpHeaderSecurityFilter.committed")); + } + + // HSTS + if (hstsEnabled && request.isSecure()) { + httpResponse.setHeader(HSTS_HEADER_NAME, hstsHeaderValue); + } + + // anti click-jacking + if (antiClickJackingEnabled) { + httpResponse.setHeader(ANTI_CLICK_JACKING_HEADER_NAME, antiClickJackingHeaderValue); + } + + // Block content type sniffing + if (blockContentTypeSniffingEnabled) { + httpResponse.setHeader(BLOCK_CONTENT_TYPE_SNIFFING_HEADER_NAME, + BLOCK_CONTENT_TYPE_SNIFFING_HEADER_VALUE); + } + + // cross-site scripting filter protection + if (xssProtectionEnabled) { + httpResponse.setHeader(XSS_PROTECTION_HEADER_NAME, XSS_PROTECTION_HEADER_VALUE); + } + } + + chain.doFilter(request, response); + } + + + @Override + protected Log getLogger() { + return log; + } + + + @Override + protected boolean isConfigProblemFatal() { + // This filter is security related to configuration issues always + // trigger a failure. + return true; + } + + + public boolean isHstsEnabled() { + return hstsEnabled; + } + + + public void setHstsEnabled(boolean hstsEnabled) { + this.hstsEnabled = hstsEnabled; + } + + + public int getHstsMaxAgeSeconds() { + return hstsMaxAgeSeconds; + } + + + public void setHstsMaxAgeSeconds(int hstsMaxAgeSeconds) { + if (hstsMaxAgeSeconds < 0) { + this.hstsMaxAgeSeconds = 0; + } else { + this.hstsMaxAgeSeconds = hstsMaxAgeSeconds; + } + } + + + public boolean isHstsIncludeSubDomains() { + return hstsIncludeSubDomains; + } + + + public void setHstsIncludeSubDomains(boolean hstsIncludeSubDomains) { + this.hstsIncludeSubDomains = hstsIncludeSubDomains; + } + + + public boolean isHstsPreload() { + return hstsPreload; + } + + + public void setHstsPreload(boolean hstsPreload) { + this.hstsPreload = hstsPreload; + } + + + public boolean isAntiClickJackingEnabled() { + return antiClickJackingEnabled; + } + + + public void setAntiClickJackingEnabled(boolean antiClickJackingEnabled) { + this.antiClickJackingEnabled = antiClickJackingEnabled; + } + + + public String getAntiClickJackingOption() { + return antiClickJackingOption.toString(); + } + + + public void setAntiClickJackingOption(String antiClickJackingOption) { + for (XFrameOption option : XFrameOption.values()) { + if (option.getHeaderValue().equalsIgnoreCase(antiClickJackingOption)) { + this.antiClickJackingOption = option; + return; + } + } + throw new IllegalArgumentException( + sm.getString("httpHeaderSecurityFilter.clickjack.invalid", antiClickJackingOption)); + } + + + public String getAntiClickJackingUri() { + return antiClickJackingUri.toString(); + } + + + public boolean isBlockContentTypeSniffingEnabled() { + return blockContentTypeSniffingEnabled; + } + + + public void setBlockContentTypeSniffingEnabled(boolean blockContentTypeSniffingEnabled) { + this.blockContentTypeSniffingEnabled = blockContentTypeSniffingEnabled; + } + + + public void setAntiClickJackingUri(String antiClickJackingUri) { + URI uri; + try { + uri = new URI(antiClickJackingUri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + this.antiClickJackingUri = uri; + } + + + @Deprecated + public boolean isXssProtectionEnabled() { + return xssProtectionEnabled; + } + + + @Deprecated + public void setXssProtectionEnabled(boolean xssProtectionEnabled) { + this.xssProtectionEnabled = xssProtectionEnabled; + } + + + private enum XFrameOption { + DENY("DENY"), + SAME_ORIGIN("SAMEORIGIN"), + ALLOW_FROM("ALLOW-FROM"); + + + private final String headerValue; + + XFrameOption(String headerValue) { + this.headerValue = headerValue; + } + + public String getHeaderValue() { + return headerValue; + } + } +} diff --git a/java/org/apache/catalina/filters/LocalStrings.properties b/java/org/apache/catalina/filters/LocalStrings.properties new file mode 100644 index 0000000..aab53ed --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings.properties @@ -0,0 +1,81 @@ +# 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. + +addDefaultCharset.unsupportedCharset=Specified character set [{0}] is not supported + +corsFilter.invalidPreflightMaxAge=Unable to parse preflightMaxAge +corsFilter.invalidRequest=Invalid CORS request; Origin [{0}] Method [{1}] Access-Control-Request-Headers [{2}] +corsFilter.invalidSupportsCredentials=It is not allowed to configure supportsCredentials=[true] when allowedOrigins=[*] +corsFilter.nullRequest=HttpServletRequest object is null +corsFilter.nullRequestType=CORSRequestType object is null +corsFilter.onlyHttp=CORS doesn't support non-HTTP request or response +corsFilter.wrongType1=Expects an HttpServletRequest object of type [{0}] +corsFilter.wrongType2=Expects an HttpServletRequest object of type [{0}] or [{1}] + +csrfPrevention.createCache=Creating new CSRF nonce cache with size [{0}] for session [{1}] (it will be created if null) +csrfPrevention.invalidRandomClass=Unable to create Random source using class [{0}] +csrfPrevention.rejectInvalidNonce=Rejecting request for [{0}] with session [{1}] due to invalid nonce [{2}] +csrfPrevention.rejectNoCache=Rejecting request for [{0}] with session [{1}] due to empty or missing nonce cache +csrfPrevention.rejectNoNonce=Rejecting request for [{0}] with session [{1}] because no CSRF nonce was found +csrfPrevention.unsupportedPattern=Unsupported pattern [{0}] + +expiresFilter.exceptionProcessingParameter=Exception processing configuration parameter [{0}]:[{1}] +expiresFilter.expirationHeaderAlreadyDefined=Request [{0}] with response status [{1}] content-type [{2}], expiration header already defined +expiresFilter.filterInitialized=Filter initialized with configuration [{0}] +expiresFilter.invalidDurationNumber=Invalid duration (number) [{0}] in directive [{1}] +expiresFilter.invalidDurationUnit=Invalid duration unit (years|months|weeks|days|hours|minutes|seconds) [{0}] in directive [{1}] +expiresFilter.noDurationFound=Duration not found in directive [{0}] +expiresFilter.noDurationUnitAfterAmount=Duration unit not found after amount [{0}] in directive [{1}] +expiresFilter.noExpirationConfigured=Request [{0}] with response status [{1}] content-type [{2}], no expiration configured +expiresFilter.noExpirationConfiguredForContentType=No Expires configuration found for content-type [{0}] +expiresFilter.numberError=Exception parsing number at position [{0}] (zero based) in comma delimited list [{1}] +expiresFilter.responseAlreadyCommitted=Request [{0}], cannot apply ExpiresFilter on already committed response. +expiresFilter.setExpirationDate=Request [{0}] with response status [{1}] content-type [{2}], set expiration date [{3}] +expiresFilter.skippedStatusCode=Request [{0}] with response status [{1}] content-type [{1}], skip expiration header generation for given status +expiresFilter.startingPointInvalid=Invalid starting point (access|now|modification|a|m) [{0}] in directive [{1}] +expiresFilter.startingPointNotFound=Starting point (access|now|modification|a|m) not found in directive [{0}] +expiresFilter.unknownParameterIgnored=Unknown parameter [{0}] with value [{1}] is ignored ! +expiresFilter.unsupportedStartingPoint=Unsupported startingPoint [{0}] +expiresFilter.useDefaultConfiguration=Use default [{0}] for content-type [{1}] returns [{2}] +expiresFilter.useMatchingConfiguration=Use [{0}] matching [{1}] for content-type [{2}] returns [{3}] + +filterbase.noSuchProperty=The property [{0}] is not defined for filters of type [{1}] + +http.403=Access to the specified resource [{0}] has been forbidden. + +httpHeaderSecurityFilter.clickjack.invalid=An invalid value [{0}] was specified for the anti click-jacking header +httpHeaderSecurityFilter.committed=Unable to add HTTP headers since response is already committed on entry to the HTTP header security Filter + +rateLimitFilter.initialized=RateLimitFilter [{0}] initialized with [{1}] requests per [{2}] seconds. Actual is [{3}] per [{4}] seconds. [{5}]. +rateLimitFilter.maxRequestsExceeded=[{0}] [{1}] Requests from [{2}] have exceeded the maximum allowed of [{3}] in a [{4}] second window. + +remoteCidrFilter.invalid=Invalid configuration provided for [{0}]. See previous messages for details. +remoteCidrFilter.noRemoteIp=Client does not have an IP address. Request denied. + +remoteIpFilter.invalidHostHeader=Invalid value [{0}] found for Host in HTTP header [{1}] +remoteIpFilter.invalidHostWithPort=Host value [{0}] in HTTP header [{1}] included a port number which will be ignored +remoteIpFilter.invalidNumber=Illegal number for parameter [{0}]: [{1}] +remoteIpFilter.invalidPort=Port [{0}] in HTTP header [{1}] included a port number which will be ignored +remoteIpFilter.invalidRemoteAddress=Unable to determine the remote host because the reported remote address [{0}] is not valid + +requestFilter.deny=Denied request for [{0}] based on property [{1}] + +restCsrfPreventionFilter.fetch.debug=CSRF Fetch request is successfully handled - nonce is added to the response. Request method: [{0}] and URI [{1}]. +restCsrfPreventionFilter.invalidNonce=CSRF nonce validation failed +restCsrfPreventionFilter.invalidNonce.debug=CSRF validation for REST failed! Request with method [{0}] and URI [{1}] will be rejected. Details: request has session ID [{2}]; requested session exists [{3}]; csrf nonce in request exists [{4}]; csrf nonce in session exists [{5}]. +restCsrfPreventionFilter.multipleNonce.debug=Different CSRF nonces are sent as request parameters, none of them will be used. Request method: [{0}] and URI [{1}]. + +webDavFilter.xpProblem=WebdavFixFilter: the XP-x64-SP2 client is known not to work with WebDAV Servlet +webDavFilter.xpRootContext=WebdavFixFilter: the XP-x64-SP2 client will only work with the root context diff --git a/java/org/apache/catalina/filters/LocalStrings_cs.properties b/java/org/apache/catalina/filters/LocalStrings_cs.properties new file mode 100644 index 0000000..1eb63cd --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_cs.properties @@ -0,0 +1,30 @@ +# 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. + +corsFilter.invalidPreflightMaxAge=Nelze naÄíst preflightMaxAge + +csrfPrevention.invalidRandomClass=Nelze vytvoÅ™it zdroj Random pomocí třídy [{0}] + +expiresFilter.noExpirationConfigured=Dotaz [{0}] se statusem odpovÄ›di [{1}] content-type [{2}], bez vyprÅ¡ení nakonfigurován +expiresFilter.noExpirationConfiguredForContentType=Nebyla nalezena konfigurace Expires pro content-type [{0}] +expiresFilter.startingPointInvalid=Neplatný poÄáteÄní bod (access|now|modification|a|m) [{0}] v pÅ™edpisu [{1}] +expiresFilter.unsupportedStartingPoint=Nepodporovaný startingPoint [{0}] +expiresFilter.useMatchingConfiguration=Použijte [{0}] odpovídající [{1}] pro content-type [{2}] a vracející [{3}] + +http.403=Přístup k uvedenému zdroji [{0}] byl zakázán. + +httpHeaderSecurityFilter.clickjack.invalid=Neplatná hodnota [{0}] byla specifikována pro hlaviÄku proti click-jacking + +requestFilter.deny=Dotaz pro [{0}] byl zakázán na základÄ› promÄ›nné [{1}] diff --git a/java/org/apache/catalina/filters/LocalStrings_de.properties b/java/org/apache/catalina/filters/LocalStrings_de.properties new file mode 100644 index 0000000..f0b132b --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_de.properties @@ -0,0 +1,32 @@ +# 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. + +corsFilter.invalidPreflightMaxAge=preflightMaxAge konnte nicht geparst werden. +corsFilter.nullRequestType=CORSRequestType Objekt ist null + +csrfPrevention.invalidRandomClass=Kann keine Zufallsquelle mithilfe der Klasse [{0}] generieren + +expiresFilter.noExpirationConfigured=Request [{0}] mit Response Status [{1}] und Content-Type [{2}], es wurde keine Gültigkeitsdauer konfiguriert +expiresFilter.noExpirationConfiguredForContentType=Keine Konfiguration für Expire von Content-Type [{0}] gefunden +expiresFilter.numberError=Ausnahme beim Parsen einer Zahl an Position [{0}] (mit Null beginnend) in Komma-separierter Liste [{1}] +expiresFilter.unsupportedStartingPoint=Nicht unterstützter Startpunkt [{0}] + +http.403=Zugriff auf die angeforderte Resource [{0}] wurde verboten. + +httpHeaderSecurityFilter.clickjack.invalid=Es wurde ein ungültiger Wert [{0}] für den Anti-Click-Jacking Header angegeben + +remoteCidrFilter.noRemoteIp=Client verfügt über keine IP Adresse. Zugriff verweigert. + +requestFilter.deny=Anfrage für [{0}] abgelehnt wegen Eigenschaft [{1}] diff --git a/java/org/apache/catalina/filters/LocalStrings_es.properties b/java/org/apache/catalina/filters/LocalStrings_es.properties new file mode 100644 index 0000000..4582477 --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_es.properties @@ -0,0 +1,44 @@ +# 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. + +addDefaultCharset.unsupportedCharset=El conjunto especificado de caracteres [{0}] no se encuentra soportado + +corsFilter.invalidPreflightMaxAge=Incapáz de procesar preflightMaxAge +corsFilter.nullRequestType=Objeto CORSRequestType es nulo\n + +csrfPrevention.invalidRandomClass=No se puede crear fuente aleatórea usando la clase [{0}] + +expiresFilter.exceptionProcessingParameter=Excepción al procesar parámetro de configuración [{0}]:[{1}] +expiresFilter.expirationHeaderAlreadyDefined=Ya se ha definido cabecera de expiración para el requerimiento [{0}] con status de respuesta [{1}] y content-type [{2}] +expiresFilter.filterInitialized=Filtro inicializado con configuración [{0}] +expiresFilter.noExpirationConfigured=No se ha configurado expiración para el requerimiento [{0}] con status de respuesta [{1}] y content-type [{2}] +expiresFilter.noExpirationConfiguredForContentType=No se ha hallado configuración de Expiración para content-type [{0}] +expiresFilter.responseAlreadyCommitted=El requerimiento [{0}], no puede aplicar ExpiresFilter en respuesta ya acometida. +expiresFilter.setExpirationDate=El requerimiento [{0}] con status de respuesta [{1}] y content-type [{2}], pone fecha de expiración a [{3}] +expiresFilter.skippedStatusCode=Generación de cabecera de expiración saltada para el requerimiento [{0}] con status de respuesta [{1}] y content-type [{2}] +expiresFilter.startingPointInvalid=Punto de arranque inválido (access|now|modification|a|m) [{0}] en la directiva [{1}] +expiresFilter.startingPointNotFound=Punto de Arranque (access|now|modification|a|m) no hallado en la directiva [{0}] +expiresFilter.unknownParameterIgnored=¡Se ignora el parámetro desconocido [{0}] con valor [{1}] especificado! +expiresFilter.unsupportedStartingPoint=startingPoint [{0}] no soportado +expiresFilter.useDefaultConfiguration=El uso de [{0}] por defecto para content-type [{1}] devuelve [{2}] +expiresFilter.useMatchingConfiguration=El Uso de [{0}] coincidente con [{1}] para content-type [{2}] devuelve [{3}] + +filterbase.noSuchProperty=La propiedad [{0}] no está definida para los filtros del tipo [{1}] + +http.403=El acceso al recurso especificado [{0}] ha sido prohibido. + +httpHeaderSecurityFilter.clickjack.invalid=An invalid value [{0}] was specified for the anti click-jacking header + +requestFilter.deny=Solicitud [{0}] denegada debido a propiedad [{1}] diff --git a/java/org/apache/catalina/filters/LocalStrings_fr.properties b/java/org/apache/catalina/filters/LocalStrings_fr.properties new file mode 100644 index 0000000..301272e --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_fr.properties @@ -0,0 +1,81 @@ +# 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. + +addDefaultCharset.unsupportedCharset=Le jeu de caractères [{0}] spécifié n''est pas supporté + +corsFilter.invalidPreflightMaxAge=Incapable d'analyser "preflightMaxAge" +corsFilter.invalidRequest=Requête CORS invalide; Origine [{0}] Méthode [{1}] Access-Control-Request-Headers [{2}] +corsFilter.invalidSupportsCredentials=Il est interdit de configurer supportsCredentials=[true] alors que allowedOrigins=[*] +corsFilter.nullRequest=L'objet HttpServletRequest est nul +corsFilter.nullRequestType=L'objet CORSRequestType est nul +corsFilter.onlyHttp=CORS ne supporte pas des requêtes ou des réponses non HTTP +corsFilter.wrongType1=Attente d''un objet HttpServletRequest de type [{0}] +corsFilter.wrongType2=Attendu un object HttpServletRequest de type [{0}] ou [{1}] + +csrfPrevention.createCache=Création d''un nouveau cache pour les CSRF nonce avec la taille [{0}] pour la session [{1}] (celle ci sera crée si elle est null) +csrfPrevention.invalidRandomClass=Impossible de créer une source aléatoire ("Random source") à l''aide de la classe [{0}] +csrfPrevention.rejectInvalidNonce=La requête pour [{0}] avec la session [{1}] est rejetée car le nonce [{2}] est invalide +csrfPrevention.rejectNoCache=La requête pour [{0}] avec la session [{1}] est rejetée car le cache de nonce est vide ou absent +csrfPrevention.rejectNoNonce=La requête pour [{0}] avec la session [{1}] est rejetée car aucun CSRF nonce n''a été trouvé +csrfPrevention.unsupportedPattern=Le modèle [{0}] n''est pas supporté + +expiresFilter.exceptionProcessingParameter=Erreur lors du traitement du paramètre de configuration [{0}] : [{1}] +expiresFilter.expirationHeaderAlreadyDefined=Requête [{0}] de statut de réponse [{1}] et de type de contenu ("content-type") [{2}], en-tête d''expiration déjà défini +expiresFilter.filterInitialized=Le filtre a été initialisé avec la configuration [{0}] +expiresFilter.invalidDurationNumber=Durée (nombre) invalide [{0}] dans la directive [{1}] +expiresFilter.invalidDurationUnit=Unité de durée invalide (years|months|weeks|days|hours|minutes|seconds) [{0}] dans la directive [{1}] +expiresFilter.noDurationFound=La durée n''a pas été trouvée dans la directive [{0}] +expiresFilter.noDurationUnitAfterAmount=L''unité de durée n''a pas été trouvée après le nombre [{0}] dans la directive [{1}] +expiresFilter.noExpirationConfigured=Requête [{0}] avec statut de réponse [{1}] et content-type [{2}], pas d''expiration configurée +expiresFilter.noExpirationConfiguredForContentType=La configuration "Expires" pour le type de contenu [{0}] n''est pas trouvée. +expiresFilter.numberError=Erreur lors de l''analyse du nombre à la position [{0}] (basée sur zéro) de la liste délimitée par une virgule [{1}] +expiresFilter.responseAlreadyCommitted=Impossible d''appliquer le ExpiresFilter pour la requête [{0}] car la réponse a déjà été envoyée +expiresFilter.setExpirationDate=La requête [{0}] avec le status de réponse [{1}] content-type [{2}], a fixé la date d''expiration [{3}] +expiresFilter.skippedStatusCode=La requête [{0}] avec le code de réponse [{1}] content-type [{2}], l''en-tête expiration ne sera pas généré pour ce statut +expiresFilter.startingPointInvalid=Point de départ invalide (access|now|modification|a|m) [{0}] dans la directive [{1}]\n +expiresFilter.startingPointNotFound=Le point de départ (access|now|modification|a|m) n''est pas présent dans la directive [{0}] +expiresFilter.unknownParameterIgnored=Le paramètre inconnu [{0}] dont la valeur est [{1}] est ignoré +expiresFilter.unsupportedStartingPoint=startingPoint [{0}] non supporté. +expiresFilter.useDefaultConfiguration=Utilisation du défaut [{0}] pour le content-type [{1}] qui renvoie [{2}] +expiresFilter.useMatchingConfiguration=Utilisation de [{0}], satisfaisant [{1}] pour le content-type [{2}] et retournant [{3}] + +filterbase.noSuchProperty=La propriété [{0}] n''est pas définie pour les filtres du type [{1}] + +http.403=L''accès à la ressource demandée [{0}] a été interdit. + +httpHeaderSecurityFilter.clickjack.invalid=Une valeur invalide [{0}] a été spécifiée pour le header "anti click-jacking" +httpHeaderSecurityFilter.committed=Impossible d'ajouter les en-têtes HTTP car la réponse a déjà été envoyée avant l'invocation du filtre de sécurité des en-têtes + +rateLimitFilter.initialized=RateLimitFilter [{0}] initialisé avec [{1}] requêtes toutes les [{2}] secondes. Fixé à [{3}] toutes les [{4}] secondes. [{5}]. +rateLimitFilter.maxRequestsExceeded=[{0}] [{1}] requêtes de [{2}] ont excédé le maximum autorisé de [{3}] pour une période de [{4}] secondes. + +remoteCidrFilter.invalid=Une configuration invalide a été fournie pour [{0}], voir les précédents messages pour les détails +remoteCidrFilter.noRemoteIp=Le client n'a pas d'adresse IP. Requête rejetée. + +remoteIpFilter.invalidHostHeader=La valeur invalide [{0}] a été trouvée pour le Host dans l''en-tête HTTP [{1}] +remoteIpFilter.invalidHostWithPort=La valeur de Host [{0}] dans l''en-tête HTTP [{1}] contenait un numéro de port qui sera ingnoré +remoteIpFilter.invalidNumber=Nombre invalide pour le paramètre [{0}] : [{1}] +remoteIpFilter.invalidPort=Le port [{0}] dans l''en-tête HTTP [{1}] incluait un numéro de port qui sera ignoré +remoteIpFilter.invalidRemoteAddress=Impossible de déterminer l''hôte distant car l''adresse distante [{0}] est invalide + +requestFilter.deny=Refus de la requête [{0}] d''après la propriété [{1}] + +restCsrfPreventionFilter.fetch.debug=La requête CSRF Fetch a été traitée avec succès, le nonce a été ajouté à la réponse; méthode de requête [{0}] et URI [{1}] +restCsrfPreventionFilter.invalidNonce=La validation du nonce de CSRF a échouée +restCsrfPreventionFilter.invalidNonce.debug=La validation CSRF pour REST a échoué, la requête avec la méthode [{0}] et l''URI [{1}] sera rejetée. Détails: session ID de requête [{2}]; la session demandée existe [{3}]; le nonce csrf existe [{4}]; le nonce csrf existe dans la session [{5}]. +restCsrfPreventionFilter.multipleNonce.debug=Des nonces CSRF différents ont été envoyés comme paramètres de requête, aucun d''entre eux ne sera utilisé; méthode de requête [{0}] et URI [{1}] + +webDavFilter.xpProblem=WebdavFixFilter : le client de XP-x64-SP2 est réputé ne pas fonctionner avec le Servlet WebDAV +webDavFilter.xpRootContext=WebdavFixFilter : le client de XP-x64-SP2 ne peut fonctionner que le le contexte racine diff --git a/java/org/apache/catalina/filters/LocalStrings_ja.properties b/java/org/apache/catalina/filters/LocalStrings_ja.properties new file mode 100644 index 0000000..b718a6d --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_ja.properties @@ -0,0 +1,81 @@ +# 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. + +addDefaultCharset.unsupportedCharset=指定ã•ã‚ŒãŸæ–‡å­—セット [{0}] ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“ + +corsFilter.invalidPreflightMaxAge=preflightMaxAgeを解æžã§ãã¾ã›ã‚“ +corsFilter.invalidRequest=無効㪠CORS リクエスト; Origin [{0}] Method [{1}] Access-Control-Request-Headers [{2}] +corsFilter.invalidSupportsCredentials=allowedOrigins = [*]ã®å ´åˆã€supportsCredentials = [true]を設定ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +corsFilter.nullRequest=HttpServletRequestオブジェクトãŒnullã§ã™ +corsFilter.nullRequestType=CORSRequestType オブジェクト㌠null ã§ã™ã€‚ +corsFilter.onlyHttp=CORSã¯éžHTTPリクエストã¾ãŸã¯ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 +corsFilter.wrongType1=[{0}]åž‹ã®HttpServletRequestオブジェクトãŒå¿…è¦ã§ã™ +corsFilter.wrongType2=[{0}] åž‹ã¾ãŸã¯ [{1}] åž‹ã®HttpServletRequest オブジェクトãŒå¿…è¦ã§ã™ + +csrfPrevention.createCache=セッション [{1}] 用ã«ã‚µã‚¤ã‚º [{0}] ã®æ–°ã—ã„ CSRF nonce キャッシュを作æˆã—ã¦ã„ã¾ã™ (null ã®å ´åˆã«ä½œæˆã•ã‚Œã¾ã™) +csrfPrevention.invalidRandomClass=乱数生æˆå™¨ã‚¯ãƒ©ã‚¹ [{0}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’作æˆã§ãã¾ã›ã‚“ +csrfPrevention.rejectInvalidNonce=無効ãªnonce [{2}] ã®ãŸã‚ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{1}] ã§ã® [{0}] ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’æ‹’å¦ã—ã¾ã™ +csrfPrevention.rejectNoCache=nonce キャッシュãŒç©ºã‹æ¬ è½ã—ã¦ã„ã‚‹ãŸã‚ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{1}] ã® [{0}] ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’æ‹’å¦ã—ã¦ã„ã¾ã™ +csrfPrevention.rejectNoNonce=CSRF nonce ãŒè¦‹ã¤ã‹ã‚‰ãªã‹ã£ãŸãŸã‚ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{1}] ã® [{0}] ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’æ‹’å¦ã—ã¾ã—㟠+csrfPrevention.unsupportedPattern=サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„パターン [{0}] + +expiresFilter.exceptionProcessingParameter=構æˆãƒ‘ラメータ [{0}]: [{1}] 処ç†ä¸­ã®ä¾‹å¤– +expiresFilter.expirationHeaderAlreadyDefined=レスãƒãƒ³ã‚¹ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ [{1}] ãŒã‚³ãƒ³ãƒ†ãƒ³ãƒ„タイプ [{2}] ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆ [{0}]ã€expirationヘッダーãŒã™ã§ã«å®šç¾©ã•ã‚Œã¦ã„ã¾ã™ã€‚ +expiresFilter.filterInitialized=設定[{0}]ã§åˆæœŸåŒ–ã•ã‚ŒãŸFilter +expiresFilter.invalidDurationNumber=ディレクティブ [{1}] ã®ç„¡åŠ¹ãªæœŸé–“ (number) [{0}] +expiresFilter.invalidDurationUnit=ディレクティブ [{1}] ã§æœŸé–“ã®å˜ä½ãŒç„¡åŠ¹ã§ã™ã€‚(years|months|weeks|days|hours|minutes|seconds) [{0}] +expiresFilter.noDurationFound=ディレクティブ [{0}] ã«Duration ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +expiresFilter.noDurationUnitAfterAmount=ディレクティブ [{1}] ã®amount [{0}] ã®å¾Œã«Duration å˜ä½ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +expiresFilter.noExpirationConfigured=リクエスト [{0}] ã«å¯¾ã™ã‚‹ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã¯å¿œç­”コード [{1}] コンテントタイプ [{2}] ã§ã™ã€‚有効期é™ã¯æœªè¨­å®šã§ã™ã€‚ +expiresFilter.noExpirationConfiguredForContentType=Content-Type [{0}] ã«æœ‰åŠ¹æœŸé™ãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ +expiresFilter.numberError=カンマ区切りリスト [{1}] ã® [{0}] 番目(ゼロ開始)ã®æ•°å€¤ã‚’解æžä¸­ã®ä¾‹å¤– +expiresFilter.responseAlreadyCommitted=リクエスト [{0}] ã¯ã€ã™ã§ã«ã‚³ãƒŸãƒƒãƒˆã•ã‚ŒãŸãƒ¬ã‚¹ãƒãƒ³ã‚¹ã«å¯¾ã—ã¦ExpiresFilterã‚’é©ç”¨ã§ãã¾ã›ã‚“。 +expiresFilter.setExpirationDate=レスãƒãƒ³ã‚¹ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ [{1}] ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„タイプ [{2}]ã€æœ‰åŠ¹æœŸé™ [{3}] を設定ã™ã‚‹ãƒªã‚¯ã‚¨ã‚¹ãƒˆ [{0}] +expiresFilter.skippedStatusCode=レスãƒãƒ³ã‚¹ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ [{1}] content-type [{1}] ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆ [{0}]ã€æŒ‡å®šã•ã‚ŒãŸã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã®expiration ヘッダーã®ç”Ÿæˆã‚’スキップã—ã¾ã™ +expiresFilter.startingPointInvalid=ディレクティブ [{1}] ã®ç„¡åŠ¹ãªé–‹å§‹ç‚¹ (access|now|modification|a|m) [{0}] +expiresFilter.startingPointNotFound=ディレクティブ [{0}] ã«é–‹å§‹ç‚¹ (access|now|modification|a|m) ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +expiresFilter.unknownParameterIgnored=値 [{1}] ã‚’æŒã¤ä¸æ˜Žãªãƒ‘ラメータ [{0}] ã¯ç„¡è¦–ã•ã‚Œã¾ã™ï¼ +expiresFilter.unsupportedStartingPoint=[{0}] ã¯æœªå¯¾å¿œã®é–‹å§‹ç‚¹ã§ã™ +expiresFilter.useDefaultConfiguration=コンテンツタイプ [{1}] ã«æ—¢å®š [{0}] を使用ã™ã‚‹ã¨ [{2}] ãŒè¿”ã•ã‚Œã¾ã™ +expiresFilter.useMatchingConfiguration=content-type [{2}] ㌠[{3}] ã‚’è¿”ã™ãŸã‚ã€[{1}] ã¨ä¸€è‡´ã™ã‚‹ [{0}] を使用ã—ã¾ã™ + +filterbase.noSuchProperty=[{1}] タイプã®ãƒ•ã‚£ãƒ«ã‚¿ã«ã¯ãƒ—ロパティ [{0}] ãŒå®šç¾©ã•ã‚Œã¦ã„ã¾ã›ã‚“。 + +http.403=指定ã•ã‚ŒãŸãƒªã‚½ãƒ¼ã‚¹ [{0}] ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã¯æ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ + +httpHeaderSecurityFilter.clickjack.invalid=クリックジャッキングヘッダーã«ç„¡åŠ¹ãªå€¤ [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—㟠+httpHeaderSecurityFilter.committed=HTTP ヘッダーセキュリティフィルターã¸ã®å…¥åŠ›æ™‚ã«æ—¢ã«å¿œç­”ãŒã‚³ãƒŸãƒƒãƒˆã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€HTTP ヘッダーを追加ã§ãã¾ã›ã‚“ + +rateLimitFilter.initialized=RateLimitFilter [{0}] 㯠[{2}] 秒ã‚ãŸã‚Š [{1}] リクエストã§åˆæœŸåŒ–ã•ã‚Œã¾ã—ãŸã€‚ 実際㯠[{4}] ミリ秒ã‚ãŸã‚Š [{3}] ã§ã™ã€‚ [{5}]。 +rateLimitFilter.maxRequestsExceeded=[{0}] [{1}] [{2}] ã‹ã‚‰ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒã€[{4}] 秒ã®ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã§è¨±å¯ã•ã‚Œã‚‹æœ€å¤§å€¤ [{3}] を超ãˆã¾ã—ãŸã€‚ + +remoteCidrFilter.invalid=[{0}] ã«ä¸æ­£ãªå€¤ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚詳細ã¯ç›´å‰ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å‚ç…§ã—ã¦ãã ã•ã„。 +remoteCidrFilter.noRemoteIp=クライアント㯠IP アドレスをæŒã£ã¦ã„ã¾ã›ã‚“。リクエストを拒å¦ã—ã¾ã™ã€‚ + +remoteIpFilter.invalidHostHeader=HTTP ヘッダ [{1}] 中㮠Host ã«ç„¡åŠ¹ãªå€¤ [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã—㟠+remoteIpFilter.invalidHostWithPort=HTTP ヘッダ [{1}] 中㮠Host ã®å€¤ [{0}] ã¯ãƒãƒ¼ãƒˆç•ªå·ã‚’å«ã‚“ã§ã„ã¾ã™ãŒç„¡è¦–ã•ã‚Œã¾ã™ +remoteIpFilter.invalidNumber=パラメータ [{0}]: [{1}] ã«ä¸æ­£ãªç•ªå·ãŒã‚ã‚Šã¾ã™ã€‚ +remoteIpFilter.invalidPort=HTTP ヘッダー [{1}] ã®ãƒãƒ¼ãƒˆ [{0}] ã«ç„¡è¦–ã•ã‚Œã‚‹ãƒãƒ¼ãƒˆç•ªå·ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +remoteIpFilter.invalidRemoteAddress=報告ã•ã‚ŒãŸãƒªãƒ¢ãƒ¼ãƒˆã‚¢ãƒ‰ãƒ¬ã‚¹ [{0}] ãŒç„¡åŠ¹ã§ã‚ã‚‹ãŸã‚ã€ãƒªãƒ¢ãƒ¼ãƒˆãƒ›ã‚¹ãƒˆã‚’特定ã§ãã¾ã›ã‚“ + +requestFilter.deny=プロパティ [{1}] ã«å¾“ã„ [{0}] ã¸ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’æ‹’å¦ã—ã¾ã—㟠+ +restCsrfPreventionFilter.fetch.debug=CSRF Fetchリクエストã¯æ­£å¸¸ã«å‡¦ç†ã•ã‚Œã€nonceãŒãƒ¬ã‚¹ãƒãƒ³ã‚¹ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚リクエストメソッド: [{0}] ã€URI [{1}] 。 +restCsrfPreventionFilter.invalidNonce=CSRF nonce ã®æ¤œè¨¼ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +restCsrfPreventionFilter.invalidNonce.debug=RESTã®CSRF検証ã«å¤±æ•—ã—ã¾ã—ãŸï¼ メソッド[{0}]ã¨URI [{1}] を使用ã—ãŸãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯æ‹’å¦ã•ã‚Œã¾ã™ã€‚ 詳細: リクエストã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ID㯠[{2}] ã§ã™; è¦æ±‚ã•ã‚ŒãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ [{3}] ãŒå­˜åœ¨ã—ã¾ã™; リクエストã«csrf nonce [{4}] ãŒå­˜åœ¨ã—ã¾ã™; セッション中ã®csrf nonce [{5}] ãŒå­˜åœ¨ã—ã¾ã™ã€‚ +restCsrfPreventionFilter.multipleNonce.debug=ã•ã¾ã–ã¾ãªCSRF nonceãŒãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ‘ラメータã¨ã—ã¦é€ä¿¡ã•ã‚Œã€ã„ãšã‚Œã‚‚使用ã•ã‚Œã¾ã›ã‚“。 リクエストメソッド: [{0}] ã€URI [{1}] 。 + +webDavFilter.xpProblem=WebdavFixFilter:XP-x64-SP2クライアントã¯WebDAVサーブレットã§å‹•ä½œã—ãªã„ã“ã¨ãŒçŸ¥ã‚‰ã‚Œã¦ã„ã¾ã™ +webDavFilter.xpRootContext=WebdavFixFilter:XP-x64-SP2クライアントã¯ãƒ«ãƒ¼ãƒˆã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã§ã®ã¿å‹•ä½œã—ã¾ã™ diff --git a/java/org/apache/catalina/filters/LocalStrings_ko.properties b/java/org/apache/catalina/filters/LocalStrings_ko.properties new file mode 100644 index 0000000..5067898 --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_ko.properties @@ -0,0 +1,71 @@ +# 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. + +addDefaultCharset.unsupportedCharset=ì§€ì •ëœ ë¬¸ìžì…‹ [{0}]ì€(는) 지ì›ë˜ì§€ 않습니다. + +corsFilter.invalidPreflightMaxAge=preflightMaxAge를 파싱할 수 없습니다. +corsFilter.invalidSupportsCredentials=allowedOrigins=[*]ì¸ ìƒíƒœì¼ ë•Œ, supportsCredentials=[true]ë¡œ 설정하는 ê²ƒì€ í—ˆìš©ë˜ì§€ 않습니다. +corsFilter.nullRequest=HttpServletRequest ê°ì²´ê°€ ë„입니다. +corsFilter.nullRequestType=CORSRequestType ê°ì²´ê°€ ë„입니다. +corsFilter.onlyHttp=CORS는 HttpServletRequest나 HttpServletResponseê°€ ì•„ë‹Œ 요청 ë˜ëŠ” ì‘ë‹µì„ ì§€ì›í•˜ì§€ 않습니다. +corsFilter.wrongType1=íƒ€ìž…ì´ [{0}]ì¸ HttpServletRequest ê°ì²´ê°€ 요구ë©ë‹ˆë‹¤. +corsFilter.wrongType2=íƒ€ìž…ì´ [{0}]ì´ê±°ë‚˜ [{1}]ì¸ HttpServletRequest ê°ì²´ê°€ 요구ë©ë‹ˆë‹¤. + +csrfPrevention.invalidRandomClass=randomClassë¡œ ì„¤ì •ëœ í´ëž˜ìŠ¤ [{0}]ì„(를) 사용하여, java.util.Random ê°ì²´ë¥¼ ìƒì„±í•  수 없습니다. + +expiresFilter.exceptionProcessingParameter=설정 파ë¼ë¯¸í„°ë¥¼ 처리하는 중 예외 ë°œìƒ (ì´ë¦„:[{0}], ê°’:[{1}]) +expiresFilter.expirationHeaderAlreadyDefined=요청 [{0}]ì— ëŒ€í•œ ì‘답 ìƒíƒœ [{1}], content-type [{2}], expiration í—¤ë”는 ì´ë¯¸ ì •ì˜ë˜ì—ˆìŒ. +expiresFilter.filterInitialized=설정 [{0}]와(ê³¼) 함께 í•„í„°ê°€ 초기화 ë˜ì—ˆìŠµë‹ˆë‹¤. +expiresFilter.invalidDurationNumber=지시어 [{1}] ë‚´ì—ì„œ 유효하지 ì•Šì€ duration (숫ìž) ê°’: [{0}] +expiresFilter.invalidDurationUnit=지시어 [{1}] ë‚´ì—ì„œ 유효하지 ì•Šì€ ì§€ì† ì‹œê°„ 단위 (years|months|weeks|days|hours|minutes|seconds) [{0}] +expiresFilter.noDurationFound=지시어 [{0}] ë‚´ì—ì„œ durationì„ ì°¾ì„ ìˆ˜ 없습니다. +expiresFilter.noDurationUnitAfterAmount=지시어 [{1}] ë‚´ì—ì„œ, 수량 ê°’ [{0}] ì´í›„ì—, 지ì†ì‹œê°„ 단위를 ì°¾ì„ ìˆ˜ 없습니다. +expiresFilter.noExpirationConfigured=요청 [{0}]ì— ëŒ€í•˜ì—¬, ì‘답 ìƒíƒœ: [{1}], content-type: [{2}], expirationí—¤ë”는 설정 안ë¨. +expiresFilter.noExpirationConfiguredForContentType=Content-type [{0}]ì„(를) 위한 Expires ì„¤ì •ì´ ì¡´ìž¬í•˜ì§€ 않습니다. +expiresFilter.numberError=쉼표로 êµ¬ë¶„ëœ ëª©ë¡ [{1}] ë‚´ì˜, [{0}]번째 (첫번째 ì¸ë±ìŠ¤ëŠ” 0) 숫ìžë¥¼ 파싱하는 중 예외 ë°œìƒ +expiresFilter.responseAlreadyCommitted=요청 [{0}]: ì´ë¯¸ ì»¤ë°‹ëœ ì‘ë‹µì— ExpiresFilter를 ì ìš©í•  수 없습니다. +expiresFilter.setExpirationDate=요청: [{0}], ì‘답 ìƒíƒœ: [{1}], Content-Type: [{2}]. 만료 시간 설정: [{3}] +expiresFilter.skippedStatusCode=ì‘답 ìƒíƒœê°€ [{1}](ì´)ê³  Content-Typeì´ [{1}]ì¸ ìš”ì²­ [{0}]ì— ëŒ€í•˜ì—¬, 주어진 ìƒíƒœë¥¼ 고려하여 Expiration í—¤ë” ìƒì„±ì„ 건너ëœë‹ˆë‹¤. +expiresFilter.startingPointInvalid=지시어 [{1}]ì— ìžˆëŠ” [{0}]ì€(는) Expiration ì„¤ì •ì˜ ì‹œìž‘ ì  (access|now|modification|a|m)로서 유효하지 않습니다. +expiresFilter.startingPointNotFound=지시어 [{0}] ë‚´ì—ì„œ 시작 ì (access|now|modification|a|m)ì„ ì°¾ì„ ìˆ˜ 없습니다. +expiresFilter.unknownParameterIgnored=ê°’ì´ [{1}]ì¸ ì•Œ 수 없는 파ë¼ë¯¸í„° [{0}]ì€(는) 무시ë©ë‹ˆë‹¤! +expiresFilter.unsupportedStartingPoint=지ì›ë˜ì§€ 않는 시작 ì  [{0}] +expiresFilter.useDefaultConfiguration=Content-Type [{1}]ì„(를) 위해, 기본값 [{0}]ì„(를) 사용하여 [{2}]ì„(를) 반환합니다. +expiresFilter.useMatchingConfiguration=[{0}]ì„(를) 사용하여 Content-type [{2}]ì´(ê°€) [{1}]와(ê³¼) 부합ë˜ëŠ”지 ì ê²€í•˜ì—¬ [{3}]ì„(를) 반환합니다. + +filterbase.noSuchProperty=íƒ€ìž…ì´ [{1}]ì¸ í•„í„°ë“¤ì— í”„ë¡œí¼í‹° [{0}]ì´(ê°€) ì •ì˜ë˜ì§€ 않았습니다. + +http.403=ì§€ì •ëœ ë¦¬ì†ŒìŠ¤ [{0}]ì— ì ‘ê·¼í•˜ëŠ” ê²ƒì´ ê¸ˆì§€ë˜ì–´ 있습니다. + +httpHeaderSecurityFilter.clickjack.invalid=Anti-clickjacking í—¤ë”ë¡œ 유효하지 ì•Šì€ ê°’ [{0}]ì´(ê°€) 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +httpHeaderSecurityFilter.committed=HttpHeaderSecurityFilterì— ì§„ìž…í•  ë•Œì—, ì‘ë‹µì´ ì´ë¯¸ 커밋ë˜ì—ˆê¸° 때문ì—, HTTP í—¤ë”ë“¤ì„ ì¶”ê°€í•  수 없습니다. + +remoteCidrFilter.invalid=[{0}]ì„(를) 위해, 유효하지 ì•Šì€ ì„¤ì •ì´ ì œê³µë˜ì—ˆìŠµë‹ˆë‹¤. ìƒì„¸ 정보는 ì´ì „ ë©”ì‹œì§€ë“¤ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤. +remoteCidrFilter.noRemoteIp=í´ë¼ì´ì–¸íŠ¸ê°€ IP 주소를 가지고 있지 않습니다. ìš”ì²­ì´ ê±°ì ˆë˜ì—ˆìŠµë‹ˆë‹¤. + +remoteIpFilter.invalidHostHeader=HTTP í—¤ë” [{1}] ë‚´ì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤: [{0}] +remoteIpFilter.invalidHostWithPort=HTTP í—¤ë” [{1}] ë‚´ì˜ í˜¸ìŠ¤íŠ¸ ê°’ [{0}]ì´(ê°€) í¬íŠ¸ 번호를 í¬í•¨í•˜ê³  있는ë°, ì´ëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +remoteIpFilter.invalidNumber=파ë¼ë¯¸í„° [{0}]ì„(를) 위해 불허ë˜ëŠ” 숫ìžìž…니다: [{1}] +remoteIpFilter.invalidRemoteAddress=ë³´ê³ ëœ ì›ê²© 주소 [{0}](ì´)ê°€ 유효하지 ì•Šì•„ì„œ ì›ê²© 호스트를 ì‹ë³„í•  수 없습니다. + +requestFilter.deny=프로í¼í‹° [{1}]ì— ê¸°ë°˜í•˜ì—¬, [{0}]ì„(를) 위한 ìš”ì²­ì´ ê±°ë¶€ë˜ì—ˆìŠµë‹ˆë‹¤. + +restCsrfPreventionFilter.fetch.debug=CSRF Fetch ìš”ì²­ì´ ì„±ê³µì ìœ¼ë¡œ 처리ë˜ì—ˆìŠµë‹ˆë‹¤ - ì‘ë‹µì— nonceê°€ 추가ë˜ì—ˆìŠµë‹ˆë‹¤. 요청 메소드: [{0}], URI [{1}]. +restCsrfPreventionFilter.invalidNonce=CSRF nonce validation 실패 +restCsrfPreventionFilter.invalidNonce.debug=REST를 위한 CSRF 유효성 ê²€ì¦ ì‹¤íŒ¨! 메소드가 [{0}]ì´ê³  URIê°€ [{1}]ì¸ ìš”ì²­ì´ ê±°ì ˆë  ê²ƒìž…ë‹ˆë‹¤. ìƒì„¸ì •ë³´: ìš”ì²­ì´ ì„¸ì…˜ ID를 í¬í•¨í–ˆëŠ”지 여부 [{2}]; ìš”ì²­ëœ ì„¸ì…˜ì´ ì¡´ìž¬í•˜ëŠ”ì§€ 여부 [{3}]; CSRF nonceê°€ ìš”ì²­ì— ì¡´ìž¬í•˜ëŠ”ì§€ 여부 [{4}]; CSRF nonceê°€ ì„¸ì…˜ì— ì¡´ìž¬í•˜ëŠ”ì§€ 여부 [{5}]. +restCsrfPreventionFilter.multipleNonce.debug=다른 CSRF nonceë“¤ì´ ìš”ì²­ 파ë¼ë¯¸í„°ë“¤ë¡œ 전달ë˜ì—ˆìŠµë‹ˆë‹¤ë§Œ, 그것들 중 ì–´ëŠ ê²ƒë„ ì‚¬ìš©ë˜ì§€ ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. 요청 메소드: [{0}], URI [{1}]. + +webDavFilter.xpProblem=WebdavFixFilter: XP-x64-SP2 í´ë¼ì´ì–¸íŠ¸ëŠ”, WebDAV 서블릿과 ì •ìƒ ë™ìž‘하지 않는 것으로 알려져 있습니다. +webDavFilter.xpRootContext=WebdavFixFilter: XP-x64-SP2 í´ë¼ì´ì–¸íŠ¸ëŠ”, ì˜¤ì§ ë£¨íŠ¸ 컨í…스트와 ì •ìƒ ë™ìž‘í•  것입니다. diff --git a/java/org/apache/catalina/filters/LocalStrings_pt_BR.properties b/java/org/apache/catalina/filters/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..da09f33 --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_pt_BR.properties @@ -0,0 +1,25 @@ +# 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. + +csrfPrevention.invalidRandomClass=Impossível criar fonte aleatória usando a classe [{0}] + +expiresFilter.noExpirationConfiguredForContentType=Nenhuma configuração de expiração encontrada para o content-type [{0}] +expiresFilter.unsupportedStartingPoint=startingPoint [{0}] não suportado + +http.403=Acesso ao recurso especificado [{0}] foi proibído + +httpHeaderSecurityFilter.clickjack.invalid=Um valor inválido [{0}] foi definido par o cabeçalho anti click-jacking + +requestFilter.deny=Requisição negada para [{0}] baseado na propriedade [{1}] diff --git a/java/org/apache/catalina/filters/LocalStrings_ru.properties b/java/org/apache/catalina/filters/LocalStrings_ru.properties new file mode 100644 index 0000000..c63ba2d --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_ru.properties @@ -0,0 +1,22 @@ +# 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. + +corsFilter.invalidPreflightMaxAge=Ðевозможно обработать значение preflightMaxAge + +csrfPrevention.invalidRandomClass=Ðевозможно Ñоздать Случайный иÑточник Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ [{0}] + +expiresFilter.unsupportedStartingPoint=Ðе Ð¿Ð¾Ð´Ð´ÐµÑ€Ð¶Ð¸Ð²Ð°ÐµÐ¼Ð°Ñ Ð½Ð°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ Ñ‚Ð¾Ñ‡ÐºÐ° [{0}] + +http.403=ДоÑтуп к реÑурÑу [{0}] был запрещён. diff --git a/java/org/apache/catalina/filters/LocalStrings_zh_CN.properties b/java/org/apache/catalina/filters/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..cfc12a3 --- /dev/null +++ b/java/org/apache/catalina/filters/LocalStrings_zh_CN.properties @@ -0,0 +1,73 @@ +# 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. + +addDefaultCharset.unsupportedCharset=ä¸æ”¯æŒæŒ‡å®šçš„字符集[{0}] + +corsFilter.invalidPreflightMaxAge=æ— æ³•è§£æž preflightMaxAge +corsFilter.invalidSupportsCredentials=当allowedOrigins=[*]时,ä¸å…许é…ç½®supportsCredentials=[true]。 +corsFilter.nullRequest=HttpServletRequest 对象为空 +corsFilter.nullRequestType=CORSRequestType对象为空(null) +corsFilter.onlyHttp=CORSä¸æ”¯æŒéžHTTP请求或å“应 +corsFilter.wrongType1=期望类型为[{0}]çš„HttpServletRequest对象 +corsFilter.wrongType2=期望类型为[{0}]或[{1}]çš„HttpServletRequest对象 + +csrfPrevention.invalidRandomClass=ä¸èƒ½ä½¿ç”¨class [{0}]创建éšæœºæºã€‚ + +expiresFilter.exceptionProcessingParameter=异常处ç†é…ç½®å‚æ•°[{0}]:[{1}] +expiresFilter.expirationHeaderAlreadyDefined=请求[{0}]çš„å“应状æ€ä¸º[{1}]内容类型[{2}],已ç»å®šä¹‰äº†åˆ°æœŸæ ‡å¤´ +expiresFilter.filterInitialized=使用é…ç½®[{0}]åˆå§‹åŒ–的筛选器 +expiresFilter.invalidDurationNumber=指令[{1}]中的æŒç»­æ—¶é—´ï¼ˆæ•°å­—)[{0}]无效 +expiresFilter.invalidDurationUnit=指令[{1}]中的æŒç»­æ—¶é—´å•ä½æ— æ•ˆï¼ˆå¹´|月|周|天|å°æ—¶|分钟|秒)[{0}] +expiresFilter.noDurationFound=在指令[{0}]中找ä¸åˆ°æŒç»­æ—¶é—´ +expiresFilter.noDurationUnitAfterAmount=在指令[{1}]中的amount[{0}]之åŽæ‰¾ä¸åˆ°æŒç»­æ—¶é—´å•ä½ +expiresFilter.noExpirationConfigured=请求[{0}],其å“应状æ€ä¸º[{1}]内容类型[{2}],未é…置到期日期 +expiresFilter.noExpirationConfiguredForContentType=没有为 content-type [{0}] 找到过期é…ç½® +expiresFilter.numberError=分æžé€—å·åˆ†éš”列表[{1}]中ä½ç½®[{0}](基于零)处的数字时å‘生异常 +expiresFilter.responseAlreadyCommitted=请求[{0}],无法对已æ交的å“应应用ExpiresFilter。 +expiresFilter.setExpirationDate=请求[{0}],å“应状æ€ä¸º[{1}],内容类型为[{2}],设置过期日期[{3}] +expiresFilter.skippedStatusCode=请求[{0}],å“应状æ€ä¸º[{1}],内容类型为[{1}],跳过给定状æ€çš„过期头生æˆã€‚ +expiresFilter.startingPointInvalid=在指令[{1}]中无效的起点(访问|现在|修改|a<秒>|m<秒>)[{0}] +expiresFilter.startingPointNotFound=起始点(access|now|modification|a|m)未在指令[{0}]中找到 +expiresFilter.unknownParameterIgnored=忽略值为[{1}]的未知å‚æ•°[{0}]ï¼ +expiresFilter.unsupportedStartingPoint=ä¸æ”¯æŒçš„起始点 [{0}] +expiresFilter.useDefaultConfiguration=对内容类型[{1}]使用默认的[{0}]返回[{2}] +expiresFilter.useMatchingConfiguration=对内容类型[{2}]返回[{3}]使用[{0}]匹é…[{1}] + +filterbase.noSuchProperty=未为[{1}]类型的筛选器定义属性[{0}] + +http.403=ç¦æ­¢è®¿é—®æŒ‡å®šèµ„æº [{0}] 。 + +httpHeaderSecurityFilter.clickjack.invalid=为防点击挟æŒçš„å“应消æ¯å¤´æŒ‡å®šäº†éžæ³•å€¼ [{0}] +httpHeaderSecurityFilter.committed=在进入HttpHeaderSecurityFilter的时候å“应消æ¯å·²ç»æ交导致ä¸èƒ½æ·»åŠ å“应消æ¯å¤´ + +rateLimitFilter.initialized=RateLimitFilter [{0}] åˆå§‹å€¼æ˜¯æ¯ [{2}] 秒 [{1}] ä¸ªè¯·æ±‚ã€‚å®žé™…å€¼æ˜¯æ¯ [{4}] 秒 [{3}]。 [{5}]。 + +remoteCidrFilter.invalid=为[{0}]æ供的é…置无效。有关详细信æ¯ï¼Œè¯·å‚阅以å‰çš„消æ¯ã€‚ +remoteCidrFilter.noRemoteIp=客户端没有 IP 地å€ï¼Œè¯·æ±‚被拒ç»ã€‚ + +remoteIpFilter.invalidHostHeader=HTTP头 [{1}]中,为Host找到一个无效值 [{0}] +remoteIpFilter.invalidHostWithPort=HTTP头 [{1}]中的Host值 [{0}]包å«ä¸€ä¸ªè¢«å¿½ç•¥çš„端å£å· +remoteIpFilter.invalidNumber=å‚æ•°[{0}]çš„æ•°å­—éžæ³•ï¼š[{1}] +remoteIpFilter.invalidRemoteAddress=无法确定远程主机,因为上报的远程地å€[{0}]是无效的 + +requestFilter.deny=基于属性:[{1}],[{0}]的请求被拒ç»ã€‚ + +restCsrfPreventionFilter.fetch.debug=CSRF Fetch 请求被æˆåŠŸå¤„ç† - éšæœºæ•°è¢«æ·»åŠ åˆ°å“应中。请求方法:[{0}] å’Œ URI [{1}]。 +restCsrfPreventionFilter.invalidNonce=CSRF nonce验è¯å¤±è´¥ +restCsrfPreventionFilter.invalidNonce.debug=REST çš„ CSRF 验è¯å¤±è´¥ï¼ä½¿ç”¨æ–¹æ³• [{0}] å’Œ URI [{1}] 的请求将被拒ç»ã€‚详细信æ¯ï¼šè¯·æ±‚å…·æœ‰ä¼šè¯ ID [{2}];请求的会è¯å­˜åœ¨ [{3}];请求中的 csrf éšæœºæ•°å­˜åœ¨ [{4}];会è¯ä¸­çš„ csrf éšæœºæ•°å­˜åœ¨ [{5}]。 +restCsrfPreventionFilter.multipleNonce.debug=ä¸åŒçš„ CSRF éšæœºæ•°ä½œä¸ºè¯·æ±‚å‚æ•°å‘é€ï¼Œå®ƒä»¬éƒ½ä¸ä¼šè¢«ä½¿ç”¨ã€‚请求方法:[{0}] å’Œ URI [{1}]。 + +webDavFilter.xpProblem=WebdavFixFilter:已知XP-x64-SP2客户端ä¸ä½¿ç”¨WebDAV Servlet +webDavFilter.xpRootContext=WebdavFixFilter:XP-x64-SP2客户端将仅与根上下文一起工作 diff --git a/java/org/apache/catalina/filters/RateLimitFilter.java b/java/org/apache/catalina/filters/RateLimitFilter.java new file mode 100644 index 0000000..fe4674c --- /dev/null +++ b/java/org/apache/catalina/filters/RateLimitFilter.java @@ -0,0 +1,237 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.util.TimeBucketCounter; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor; + +/** + *

    + * Servlet filter that can help mitigate Denial of Service (DoS) and Brute Force attacks by limiting the number of a + * requests that are allowed from a single IP address within a time window (also referred to as a time bucket), e.g. 300 + * Requests per 60 seconds. + *

    + *

    + * The filter works by incrementing a counter in a time bucket for each IP address, and if the counter exceeds the + * allowed limit then further requests from that IP are dropped with a "429 Too many requests" response until + * the bucket time ends and a new bucket starts. + *

    + *

    + * The filter is optimized for efficiency and low overhead, so it converts some configured values to more efficient + * values. For example, a configuration of a 60 seconds time bucket is converted to 65.536 seconds. That allows for very + * fast bucket calculation using bit shift arithmetic. In order to remain true to the user intent, the configured number + * of requests is then multiplied by the same ratio, so a configuration of 100 Requests per 60 seconds, has the real + * values of 109 Requests per 65 seconds. + *

    + *

    + * It is common to set up different restrictions for different URIs. For example, a login page or authentication script + * is typically expected to get far less requests than the rest of the application, so you can add a filter definition + * that would allow only 5 requests per 15 seconds and map those URIs to it. + *

    + *

    + * You can set enforce to false to disable the termination of requests that exceed the allowed + * limit. Then your application code can inspect the Request Attribute + * org.apache.catalina.filters.RateLimitFilter.Count and decide how to handle the request based on other + * information that it has, e.g. allow more requests to certain users based on roles, etc. + *

    + *

    + * WARNING: if Tomcat is behind a reverse proxy then you must make sure that the Rate Limit Filter sees + * the client IP address, so if for example you are using the Remote IP Filter, then the + * filter mapping for the Rate Limit Filter must come after the mapping of the Remote IP Filter to ensure that + * each request has its IP address resolved before the Rate Limit Filter is applied. Failure to do so will count + * requests from different IPs in the same bucket and will result in a self inflicted DoS attack. + *

    + */ +public class RateLimitFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + /** + * default duration in seconds + */ + public static final int DEFAULT_BUCKET_DURATION = 60; + + /** + * default number of requests per duration + */ + public static final int DEFAULT_BUCKET_REQUESTS = 300; + + /** + * default value for enforce + */ + public static final boolean DEFAULT_ENFORCE = true; + + /** + * default status code to return if requests per duration exceeded + */ + public static final int DEFAULT_STATUS_CODE = 429; + + /** + * default status message to return if requests per duration exceeded + */ + public static final String DEFAULT_STATUS_MESSAGE = "Too many requests"; + + /** + * request attribute that will contain the number of requests per duration + */ + public static final String RATE_LIMIT_ATTRIBUTE_COUNT = "org.apache.catalina.filters.RateLimitFilter.Count"; + + /** + * init-param to set the bucket duration in seconds + */ + public static final String PARAM_BUCKET_DURATION = "bucketDuration"; + + /** + * init-param to set the bucket number of requests + */ + public static final String PARAM_BUCKET_REQUESTS = "bucketRequests"; + + /** + * init-param to set the enforce flag + */ + public static final String PARAM_ENFORCE = "enforce"; + + /** + * init-param to set a custom status code if requests per duration exceeded + */ + public static final String PARAM_STATUS_CODE = "statusCode"; + + /** + * init-param to set a custom status message if requests per duration exceeded + */ + public static final String PARAM_STATUS_MESSAGE = "statusMessage"; + + transient TimeBucketCounter bucketCounter; + + private int actualRequests; + + private int bucketRequests = DEFAULT_BUCKET_REQUESTS; + + private int bucketDuration = DEFAULT_BUCKET_DURATION; + + private boolean enforce = DEFAULT_ENFORCE; + + private int statusCode = DEFAULT_STATUS_CODE; + + private String statusMessage = DEFAULT_STATUS_MESSAGE; + + private transient Log log = LogFactory.getLog(RateLimitFilter.class); + + private static final StringManager sm = StringManager.getManager(RateLimitFilter.class); + + /** + * @return the actual maximum allowed requests per time bucket + */ + public int getActualRequests() { + return actualRequests; + } + + /** + * @return the actual duration of a time bucket in milliseconds + */ + public int getActualDurationInSeconds() { + return bucketCounter.getActualDuration() / 1000; + } + + @Override + public void init() throws ServletException { + + FilterConfig config = getFilterConfig(); + + String param; + param = config.getInitParameter(PARAM_BUCKET_DURATION); + if (param != null) { + bucketDuration = Integer.parseInt(param); + } + + param = config.getInitParameter(PARAM_BUCKET_REQUESTS); + if (param != null) { + bucketRequests = Integer.parseInt(param); + } + + param = config.getInitParameter(PARAM_ENFORCE); + if (param != null) { + enforce = Boolean.parseBoolean(param); + } + + param = config.getInitParameter(PARAM_STATUS_CODE); + if (param != null) { + statusCode = Integer.parseInt(param); + } + + param = config.getInitParameter(PARAM_STATUS_MESSAGE); + if (param != null) { + statusMessage = param; + } + + ScheduledExecutorService executorService = (ScheduledExecutorService) getServletContext() + .getAttribute(ScheduledThreadPoolExecutor.class.getName()); + if (executorService == null) { + executorService = new java.util.concurrent.ScheduledThreadPoolExecutor(1); + } + bucketCounter = new TimeBucketCounter(bucketDuration, executorService); + + actualRequests = (int) Math.round(bucketCounter.getRatio() * bucketRequests); + + log.info(sm.getString("rateLimitFilter.initialized", super.getFilterName(), Integer.valueOf(bucketRequests), + Integer.valueOf(bucketDuration), Integer.valueOf(getActualRequests()), + Integer.valueOf(getActualDurationInSeconds()), (!enforce ? "Not " : "") + "enforcing")); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + String ipAddr = request.getRemoteAddr(); + int reqCount = bucketCounter.increment(ipAddr); + + request.setAttribute(RATE_LIMIT_ATTRIBUTE_COUNT, Integer.valueOf(reqCount)); + + if (enforce && (reqCount > actualRequests)) { + + ((HttpServletResponse) response).sendError(statusCode, statusMessage); + log.warn(sm.getString("rateLimitFilter.maxRequestsExceeded", super.getFilterName(), + Integer.valueOf(reqCount), ipAddr, Integer.valueOf(getActualRequests()), + Integer.valueOf(getActualDurationInSeconds()))); + + return; + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + this.bucketCounter.destroy(); + super.destroy(); + } +} diff --git a/java/org/apache/catalina/filters/RemoteAddrFilter.java b/java/org/apache/catalina/filters/RemoteAddrFilter.java new file mode 100644 index 0000000..991bcf8 --- /dev/null +++ b/java/org/apache/catalina/filters/RemoteAddrFilter.java @@ -0,0 +1,65 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Concrete implementation of RequestFilter that filters based on the string representation of the remote + * client's IP address. + * + * @author Craig R. McClanahan + */ +public final class RemoteAddrFilter extends RequestFilter { + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private final Log log = LogFactory.getLog(RemoteAddrFilter.class); // must not be static + + + /** + * Extract the desired request property, and pass it (along with the specified request and response objects and + * associated filter chain) to the protected process() method to perform the actual filtering. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * @param chain The filter chain for this request + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + process(request.getRemoteAddr(), request, response, chain); + + } + + @Override + protected Log getLogger() { + return log; + } +} diff --git a/java/org/apache/catalina/filters/RemoteCIDRFilter.java b/java/org/apache/catalina/filters/RemoteCIDRFilter.java new file mode 100644 index 0000000..3c30835 --- /dev/null +++ b/java/org/apache/catalina/filters/RemoteCIDRFilter.java @@ -0,0 +1,236 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.util.NetMask; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.StringUtils; + +public final class RemoteCIDRFilter extends FilterBase { + + /** + * text/plain MIME type: this is the MIME type we return when a {@link ServletResponse} is not an + * {@link HttpServletResponse} + */ + private static final String PLAIN_TEXT_MIME_TYPE = "text/plain"; + + /** + * Our logger + */ + private final Log log = LogFactory.getLog(RemoteCIDRFilter.class); // must not be static + + /** + * The list of allowed {@link NetMask}s + */ + private final List allow = new ArrayList<>(); + + /** + * The list of denied {@link NetMask}s + */ + private final List deny = new ArrayList<>(); + + + /** + * Return a string representation of the {@link NetMask} list in #allow. + * + * @return the #allow list as a string, without the leading '[' and trailing ']' + */ + public String getAllow() { + return allow.toString().replace("[", "").replace("]", ""); + } + + + /** + * Fill the #allow list with the list of netmasks provided as an argument, if any. Calls #fillFromInput. + * + * @param input The list of netmasks, as a comma separated string + * + * @throws IllegalArgumentException One or more netmasks are invalid + */ + public void setAllow(final String input) { + final List messages = fillFromInput(input, allow); + + if (messages.isEmpty()) { + return; + } + + for (final String message : messages) { + log.error(message); + } + + throw new IllegalArgumentException(sm.getString("remoteCidrFilter.invalid", "allow")); + } + + + /** + * Return a string representation of the {@link NetMask} list in #deny. + * + * @return the #deny list as string, without the leading '[' and trailing ']' + */ + public String getDeny() { + return deny.toString().replace("[", "").replace("]", ""); + } + + + /** + * Fill the #deny list with the list of netmasks provided as an argument, if any. Calls #fillFromInput. + * + * @param input The list of netmasks, as a comma separated string + * + * @throws IllegalArgumentException One or more netmasks are invalid + */ + public void setDeny(final String input) { + final List messages = fillFromInput(input, deny); + + if (messages.isEmpty()) { + return; + } + + for (final String message : messages) { + log.error(message); + } + + throw new IllegalArgumentException(sm.getString("remoteCidrFilter.invalid", "deny")); + } + + + @Override + protected boolean isConfigProblemFatal() { + // Failure to configure a security related component should always be + // fatal. + return true; + } + + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + + if (isAllowed(request.getRemoteAddr())) { + chain.doFilter(request, response); + return; + } + + if (!(response instanceof HttpServletResponse)) { + sendErrorWhenNotHttp(response); + return; + } + + ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN); + } + + + @Override + public Log getLogger() { + return log; + } + + + /** + * Test if a remote's IP address is allowed to proceed. + * + * @param property The remote's IP address, as a string + * + * @return true if allowed + */ + private boolean isAllowed(final String property) { + final InetAddress addr; + + try { + addr = InetAddress.getByName(property); + } catch (UnknownHostException e) { + // This should be in the 'could never happen' category but handle it + // to be safe. + log.error(sm.getString("remoteCidrFilter.noRemoteIp"), e); + return false; + } + + for (final NetMask nm : deny) { + if (nm.matches(addr)) { + return false; + } + } + + for (final NetMask nm : allow) { + if (nm.matches(addr)) { + return true; + } + } + + // Allow if deny is specified but allow isn't + if (!deny.isEmpty() && allow.isEmpty()) { + return true; + } + + // Deny this request + return false; + } + + + private void sendErrorWhenNotHttp(ServletResponse response) throws IOException { + final PrintWriter writer = response.getWriter(); + response.setContentType(PLAIN_TEXT_MIME_TYPE); + writer.write(sm.getString("http.403")); + writer.flush(); + } + + + /** + * Fill a {@link NetMask} list from a string input containing a comma-separated list of (hopefully valid) + * {@link NetMask}s. + * + * @param input The input string + * @param target The list to fill + * + * @return a string list of processing errors (empty when no errors) + */ + private List fillFromInput(final String input, final List target) { + target.clear(); + if (input == null || input.isEmpty()) { + return Collections.emptyList(); + } + + final List messages = new ArrayList<>(); + NetMask nm; + + for (final String s : StringUtils.splitCommaSeparated(input)) { + try { + nm = new NetMask(s); + target.add(nm); + } catch (IllegalArgumentException e) { + messages.add(s + ": " + e.getMessage()); + } + } + + return Collections.unmodifiableList(messages); + } +} diff --git a/java/org/apache/catalina/filters/RemoteHostFilter.java b/java/org/apache/catalina/filters/RemoteHostFilter.java new file mode 100644 index 0000000..63c4a93 --- /dev/null +++ b/java/org/apache/catalina/filters/RemoteHostFilter.java @@ -0,0 +1,64 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Concrete implementation of RequestFilter that filters based on the remote client's host name. + * + * @author Craig R. McClanahan + */ +public final class RemoteHostFilter extends RequestFilter { + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private final Log log = LogFactory.getLog(RemoteHostFilter.class); // must not be static + + + /** + * Extract the desired request property, and pass it (along with the specified request and response objects and + * associated filter chain) to the protected process() method to perform the actual filtering. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * @param chain The filter chain for this request + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + process(request.getRemoteHost(), request, response, chain); + + } + + @Override + protected Log getLogger() { + return log; + } +} diff --git a/java/org/apache/catalina/filters/RemoteIpFilter.java b/java/org/apache/catalina/filters/RemoteIpFilter.java new file mode 100644 index 0000000..9b50148 --- /dev/null +++ b/java/org/apache/catalina/filters/RemoteIpFilter.java @@ -0,0 +1,1331 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestWrapper; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.PushBuilder; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Globals; +import org.apache.catalina.connector.RequestFacade; +import org.apache.catalina.util.RequestUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.parser.Host; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * Servlet filter to integrate "X-Forwarded-For" and "X-Forwarded-Proto" HTTP headers. + *

    + *

    + * Most of the design of this Servlet Filter is a port of + * mod_remoteip, this servlet filter replaces + * the apparent client remote IP address and hostname for the request with the IP address list presented by a proxy or a + * load balancer via a request headers (e.g. "X-Forwarded-For"). + *

    + *

    + * Another feature of this servlet filter is to replace the apparent scheme (http/https) and server port with the scheme + * presented by a proxy or a load balancer via a request header (e.g. "X-Forwarded-Proto"). + *

    + *

    + * This servlet filter proceeds as follows: + *

    + *

    + * If the incoming request.getRemoteAddr() matches the servlet filter's list of internal or trusted + * proxies: + *

    + *
      + *
    • Loop on the comma delimited list of IPs and hostnames passed by the preceding load balancer or proxy in the given + * request's Http header named $remoteIpHeader (default value x-forwarded-for). Values are + * processed in right-to-left order.
    • + *
    • For each ip/host of the list: + *
        + *
      • if it matches the internal proxies list, the ip/host is swallowed
      • + *
      • if it matches the trusted proxies list, the ip/host is added to the created proxies header
      • + *
      • otherwise, the ip/host is declared to be the remote ip and looping is stopped.
      • + *
      + *
    • + *
    • If the request http header named $protocolHeader (default value X-Forwarded-Proto) + * consists only of forwards that match protocolHeaderHttpsValue configuration parameter (default + * https) then request.isSecure = true, request.scheme = https and + * request.serverPort = 443. Note that 443 can be overwritten with the $httpsServerPort + * configuration parameter.
    • + *
    • Mark the request with the attribute {@link Globals#REQUEST_FORWARDED_ATTRIBUTE} and value {@code Boolean.TRUE} to + * indicate that this request has been forwarded by one or more proxies.
    • + *
    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Configuration parameters
    XForwardedFilter propertyDescriptionEquivalent mod_remoteip directiveFormatDefault Value
    remoteIpHeaderName of the Http Header read by this servlet filter that holds the list of traversed IP addresses starting from + * the requesting clientRemoteIPHeaderCompliant http header namex-forwarded-for
    internalProxiesRegular expression that matches the IP addresses of internal proxies. If they appear in the + * remoteIpHeader value, they will be trusted and will not appear in the proxiesHeader + * valueRemoteIPInternalProxyRegular expression (in the syntax supported by {@link java.util.regex.Pattern java.util.regex})10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}| + * 169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}| + * 100\.6[4-9]{1}\.\d{1,3}\.\d{1,3}|100\.[7-9]{1}\d{1}\.\d{1,3}\.\d{1,3}| + * 100\.1[0-1]{1}\d{1}\.\d{1,3}\.\d{1,3}|100\.12[0-7]{1}\.\d{1,3}\.\d{1,3}| + * 172\.1[6-9]{1}\.\d{1,3}\.\d{1,3}|172\.2[0-9]{1}\.\d{1,3}\.\d{1,3}| 172\.3[0-1]{1}\.\d{1,3}\.\d{1,3}| + * 0:0:0:0:0:0:0:1|::1
    + * By default, 10/8, 192.168/16, 169.254/16, 127/8, 100.64/10, 172.16/12, and 0:0:0:0:0:0:0:1 are allowed.
    proxiesHeaderName of the http header created by this servlet filter to hold the list of proxies that have been processed in + * the incoming remoteIpHeaderRemoteIPProxiesHeaderCompliant http header namex-forwarded-by
    trustedProxiesRegular expression that matches the IP addresses of trusted proxies. If they appear in the + * remoteIpHeader value, they will be trusted and will appear in the proxiesHeader valueRemoteIPTrustedProxyRegular expression (in the syntax supported by {@link java.util.regex.Pattern java.util.regex}) 
    protocolHeaderName of the http header read by this servlet filter that holds the flag that this requestN/ACompliant http header name like X-Forwarded-Proto, X-Forwarded-Ssl or + * Front-End-HttpsX-Forwarded-Proto
    protocolHeaderHttpsValueValue of the protocolHeader to indicate that it is an Https requestN/AString like https or ONhttps
    httpServerPortValue returned by {@link ServletRequest#getServerPort()} when the protocolHeader indicates + * http protocolN/Ainteger80
    httpsServerPortValue returned by {@link ServletRequest#getServerPort()} when the protocolHeader indicates + * https protocolN/Ainteger443
    enableLookupsShould a DNS lookup be performed to provide a host name when calling {@link ServletRequest#getRemoteHost()}N/Abooleanfalse
    + *

    + * Regular expression vs. IP address blocks: mod_remoteip allows to use address blocks + * (e.g. 192.168/16) to configure RemoteIPInternalProxy and RemoteIPTrustedProxy + * ; as the JVM doesn't have a library similar to apr_ipsubnet_test, + * we rely on regular expressions. + *

    + *
    + *

    + * Sample with internal proxies + *

    + *

    + * XForwardedFilter configuration: + *

    + * + * <filter> + * <filter-name>RemoteIpFilter</filter-name> + * <filter-class>org.apache.catalina.filters.RemoteIpFilter</filter-class> + * <init-param> + * <param-name>internalProxies</param-name> + * <param-value>192\.168\.0\.10|192\.168\.0\.11</param-value> + * </init-param> + * <init-param> + * <param-name>remoteIpHeader</param-name> + * <param-value>x-forwarded-for</param-value> + * </init-param> + * <init-param> + * <param-name>remoteIpProxiesHeader</param-name> + * <param-value>x-forwarded-by</param-value> + * </init-param> + * <init-param> + * <param-name>protocolHeader</param-name> + * <param-value>x-forwarded-proto</param-value> + * </init-param> + * </filter> + * + * <filter-mapping> + * <filter-name>RemoteIpFilter</filter-name> + * <url-pattern>/*</url-pattern> + * <dispatcher>REQUEST</dispatcher> + * </filter-mapping> + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Request Values
    propertyValue Before RemoteIpFilterValue After RemoteIpFilter
    request.remoteAddr192.168.0.10140.211.11.130
    request.header['x-forwarded-for']140.211.11.130, 192.168.0.10null
    request.header['x-forwarded-by']nullnull
    request.header['x-forwarded-proto']httpshttps
    request.schemehttphttps
    request.securefalsetrue
    request.serverPort80443
    + * Note : x-forwarded-by header is null because only internal proxies as been traversed by the request. + * x-forwarded-by is null because all the proxies are trusted or internal. + *
    + *

    + * Sample with trusted proxies + *

    + *

    + * RemoteIpFilter configuration: + *

    + * + * <filter> + * <filter-name>RemoteIpFilter</filter-name> + * <filter-class>org.apache.catalina.filters.RemoteIpFilter</filter-class> + * <init-param> + * <param-name>internalProxies</param-name> + * <param-value>192\.168\.0\.10|192\.168\.0\.11</param-value> + * </init-param> + * <init-param> + * <param-name>remoteIpHeader</param-name> + * <param-value>x-forwarded-for</param-value> + * </init-param> + * <init-param> + * <param-name>remoteIpProxiesHeader</param-name> + * <param-value>x-forwarded-by</param-value> + * </init-param> + * <init-param> + * <param-name>trustedProxies</param-name> + * <param-value>proxy1|proxy2</param-value> + * </init-param> + * </filter> + * + * <filter-mapping> + * <filter-name>RemoteIpFilter</filter-name> + * <url-pattern>/*</url-pattern> + * <dispatcher>REQUEST</dispatcher> + * </filter-mapping> + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Request Values
    propertyValue Before RemoteIpFilterValue After RemoteIpFilter
    request.remoteAddr192.168.0.10140.211.11.130
    request.header['x-forwarded-for']140.211.11.130, proxy1, proxy2null
    request.header['x-forwarded-by']nullproxy1, proxy2
    + *

    + * Note : proxy1 and proxy2 are both trusted proxies that come in x-forwarded-for + * header, they both are migrated in x-forwarded-by header. x-forwarded-by is null because all + * the proxies are trusted or internal. + *

    + *
    + *

    + * Sample with internal and trusted proxies + *

    + *

    + * RemoteIpFilter configuration: + *

    + * + * <filter> + * <filter-name>RemoteIpFilter</filter-name> + * <filter-class>org.apache.catalina.filters.RemoteIpFilter</filter-class> + * <init-param> + * <param-name>internalProxies</param-name> + * <param-value>192\.168\.0\.10|192\.168\.0\.11</param-value> + * </init-param> + * <init-param> + * <param-name>remoteIpHeader</param-name> + * <param-value>x-forwarded-for</param-value> + * </init-param> + * <init-param> + * <param-name>remoteIpProxiesHeader</param-name> + * <param-value>x-forwarded-by</param-value> + * </init-param> + * <init-param> + * <param-name>trustedProxies</param-name> + * <param-value>proxy1|proxy2</param-value> + * </init-param> + * </filter> + * + * <filter-mapping> + * <filter-name>RemoteIpFilter</filter-name> + * <url-pattern>/*</url-pattern> + * <dispatcher>REQUEST</dispatcher> + * </filter-mapping> + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Request Values
    propertyValue Before RemoteIpFilterValue After RemoteIpFilter
    request.remoteAddr192.168.0.10140.211.11.130
    request.header['x-forwarded-for']140.211.11.130, proxy1, proxy2, 192.168.0.10null
    request.header['x-forwarded-by']nullproxy1, proxy2
    + *

    + * Note : proxy1 and proxy2 are both trusted proxies that come in x-forwarded-for + * header, they both are migrated in x-forwarded-by header. As 192.168.0.10 is an internal + * proxy, it does not appear in x-forwarded-by. x-forwarded-by is null because all the proxies + * are trusted or internal. + *

    + *
    + *

    + * Sample with an untrusted proxy + *

    + *

    + * RemoteIpFilter configuration: + *

    + * + * <filter> + * <filter-name>RemoteIpFilter</filter-name> + * <filter-class>org.apache.catalina.filters.RemoteIpFilter</filter-class> + * <init-param> + * <param-name>internalProxies</param-name> + * <param-value>192\.168\.0\.10|192\.168\.0\.11</param-value> + * </init-param> + * <init-param> + * <param-name>remoteIpHeader</param-name> + * <param-value>x-forwarded-for</param-value> + * </init-param> + * <init-param> + * <param-name>remoteIpProxiesHeader</param-name> + * <param-value>x-forwarded-by</param-value> + * </init-param> + * <init-param> + * <param-name>trustedProxies</param-name> + * <param-value>proxy1|proxy2</param-value> + * </init-param> + * </filter> + * + * <filter-mapping> + * <filter-name>RemoteIpFilter</filter-name> + * <url-pattern>/*</url-pattern> + * <dispatcher>REQUEST</dispatcher> + * </filter-mapping> + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Request Values
    propertyValue Before RemoteIpFilterValue After RemoteIpFilter
    request.remoteAddr192.168.0.10untrusted-proxy
    request.header['x-forwarded-for']140.211.11.130, untrusted-proxy, proxy1140.211.11.130
    request.header['x-forwarded-by']nullproxy1
    + *

    + * Note : x-forwarded-by holds the trusted proxy proxy1. x-forwarded-by holds + * 140.211.11.130 because untrusted-proxy is not trusted and thus, we cannot trust that + * untrusted-proxy is the actual remote ip. request.remoteAddr is untrusted-proxy + * that is an IP verified by proxy1. + *

    + *
    + */ +public class RemoteIpFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + public static class XForwardedRequest extends HttpServletRequestWrapper { + + protected final Map> headers; + + protected String localName; + + protected int localPort; + + protected String remoteAddr; + + protected String remoteHost; + + protected String scheme; + + protected boolean secure; + + protected String serverName; + + protected int serverPort; + + public XForwardedRequest(HttpServletRequest request) { + super(request); + this.localName = request.getLocalName(); + this.localPort = request.getLocalPort(); + this.remoteAddr = request.getRemoteAddr(); + this.remoteHost = request.getRemoteHost(); + this.scheme = request.getScheme(); + this.secure = request.isSecure(); + this.serverName = request.getServerName(); + this.serverPort = request.getServerPort(); + + headers = new HashMap<>(); + for (Enumeration headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) { + String header = headerNames.nextElement(); + headers.put(header, Collections.list(request.getHeaders(header))); + } + } + + @Override + public long getDateHeader(String name) { + String value = getHeader(name); + if (value == null) { + return -1; + } + long date = FastHttpDateFormat.parseDate(value); + if (date == -1) { + throw new IllegalArgumentException(value); + } + return date; + } + + @Override + public String getHeader(String name) { + Map.Entry> header = getHeaderEntry(name); + if (header == null || header.getValue() == null || header.getValue().isEmpty()) { + return null; + } + return header.getValue().get(0); + } + + protected Map.Entry> getHeaderEntry(String name) { + for (Map.Entry> entry : headers.entrySet()) { + if (entry.getKey().equalsIgnoreCase(name)) { + return entry; + } + } + return null; + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public Enumeration getHeaders(String name) { + Map.Entry> header = getHeaderEntry(name); + if (header == null || header.getValue() == null) { + return Collections.enumeration(Collections.emptyList()); + } + return Collections.enumeration(header.getValue()); + } + + @Override + public int getIntHeader(String name) { + String value = getHeader(name); + if (value == null) { + return -1; + } + return Integer.parseInt(value); + } + + @Override + public String getLocalName() { + return localName; + } + + @Override + public int getLocalPort() { + return localPort; + } + + @Override + public String getRemoteAddr() { + return this.remoteAddr; + } + + @Override + public String getRemoteHost() { + return this.remoteHost; + } + + @Override + public String getScheme() { + return scheme; + } + + @Override + public String getServerName() { + return serverName; + } + + @Override + public int getServerPort() { + return serverPort; + } + + public void removeHeader(String name) { + Map.Entry> header = getHeaderEntry(name); + if (header != null) { + headers.remove(header.getKey()); + } + } + + public void setHeader(String name, String value) { + List values = Collections.singletonList(value); + Map.Entry> header = getHeaderEntry(name); + if (header == null) { + headers.put(name, values); + } else { + header.setValue(values); + } + + } + + public void setLocalName(String localName) { + this.localName = localName; + } + + public void setLocalPort(int localPort) { + this.localPort = localPort; + } + + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public void setSecure(boolean secure) { + super.getRequest().setAttribute(Globals.REMOTE_IP_FILTER_SECURE, Boolean.valueOf(secure)); + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + @Override + public StringBuffer getRequestURL() { + return RequestUtil.getRequestURL(this); + } + + @Override + public PushBuilder newPushBuilder() { + ServletRequest current = getRequest(); + while (current instanceof ServletRequestWrapper) { + current = ((ServletRequestWrapper) current).getRequest(); + } + if (current instanceof RequestFacade) { + return ((RequestFacade) current).newPushBuilder(this); + } else { + return null; + } + } + } + + + protected static final String HTTP_SERVER_PORT_PARAMETER = "httpServerPort"; + + protected static final String HTTPS_SERVER_PORT_PARAMETER = "httpsServerPort"; + + protected static final String INTERNAL_PROXIES_PARAMETER = "internalProxies"; + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private transient Log log = LogFactory.getLog(RemoteIpFilter.class); + protected static final StringManager sm = StringManager.getManager(RemoteIpFilter.class); + + protected static final String PROTOCOL_HEADER_PARAMETER = "protocolHeader"; + + protected static final String PROTOCOL_HEADER_HTTPS_VALUE_PARAMETER = "protocolHeaderHttpsValue"; + + protected static final String HOST_HEADER_PARAMETER = "hostHeader"; + + protected static final String PORT_HEADER_PARAMETER = "portHeader"; + + protected static final String CHANGE_LOCAL_NAME_PARAMETER = "changeLocalName"; + + protected static final String CHANGE_LOCAL_PORT_PARAMETER = "changeLocalPort"; + + protected static final String PROXIES_HEADER_PARAMETER = "proxiesHeader"; + + protected static final String REMOTE_IP_HEADER_PARAMETER = "remoteIpHeader"; + + protected static final String TRUSTED_PROXIES_PARAMETER = "trustedProxies"; + + protected static final String ENABLE_LOOKUPS_PARAMETER = "enableLookups"; + + /** + * Convert a given comma delimited list of regular expressions into an array of String + * + * @param commaDelimitedStrings The string to split + * + * @return array of patterns (non null) + * + * @deprecated Unused. Will be removed in Tomcat 11 onwards. + */ + @Deprecated + protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) { + return StringUtils.splitCommaSeparated(commaDelimitedStrings); + } + + /** + * Convert a list of strings in a comma delimited string. + * + * @param stringList List of strings + * + * @return concatenated string + * + * @deprecated Unused. Will be removed in Tomcat 11 onwards + */ + @Deprecated + protected static String listToCommaDelimitedString(List stringList) { + if (stringList == null) { + return ""; + } + StringBuilder result = new StringBuilder(); + for (Iterator it = stringList.iterator(); it.hasNext();) { + Object element = it.next(); + if (element != null) { + result.append(element); + if (it.hasNext()) { + result.append(", "); + } + } + } + return result.toString(); + } + + /** + * @see #setHttpServerPort(int) + */ + private int httpServerPort = 80; + + /** + * @see #setHttpsServerPort(int) + */ + private int httpsServerPort = 443; + + /** + * @see #setInternalProxies(String) + */ + private Pattern internalProxies = + Pattern.compile("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" + + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "100\\.6[4-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + "100\\.[7-9]{1}\\d{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "100\\.1[0-1]{1}\\d{1}\\.\\d{1,3}\\.\\d{1,3}|" + "100\\.12[0-7]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|" + "0:0:0:0:0:0:0:1|::1"); + + /** + * @see #setProtocolHeader(String) + */ + private String protocolHeader = "X-Forwarded-Proto"; + + private String protocolHeaderHttpsValue = "https"; + + private String hostHeader = null; + + private boolean changeLocalName = false; + + private String portHeader = null; + + private boolean changeLocalPort = false; + + /** + * @see #setProxiesHeader(String) + */ + private String proxiesHeader = "X-Forwarded-By"; + + /** + * @see #setRemoteIpHeader(String) + */ + private String remoteIpHeader = "X-Forwarded-For"; + + /** + * @see #setRequestAttributesEnabled(boolean) + */ + private boolean requestAttributesEnabled = true; + + /** + * @see #setTrustedProxies(String) + */ + private Pattern trustedProxies = null; + + private boolean enableLookups; + + public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + + boolean isInternal = internalProxies != null && internalProxies.matcher(request.getRemoteAddr()).matches(); + + if (isInternal || (trustedProxies != null && trustedProxies.matcher(request.getRemoteAddr()).matches())) { + String remoteIp = null; + Deque proxiesHeaderValue = new ArrayDeque<>(); + StringBuilder concatRemoteIpHeaderValue = new StringBuilder(); + + for (Enumeration e = request.getHeaders(remoteIpHeader); e.hasMoreElements();) { + if (concatRemoteIpHeaderValue.length() > 0) { + concatRemoteIpHeaderValue.append(", "); + } + + concatRemoteIpHeaderValue.append(e.nextElement()); + } + + String[] remoteIpHeaderValue = StringUtils.splitCommaSeparated(concatRemoteIpHeaderValue.toString()); + int idx; + if (!isInternal) { + proxiesHeaderValue.addFirst(request.getRemoteAddr()); + } + // loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain + for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) { + String currentRemoteIp = remoteIpHeaderValue[idx]; + remoteIp = currentRemoteIp; + if (internalProxies != null && internalProxies.matcher(currentRemoteIp).matches()) { + // do nothing, internalProxies IPs are not appended to the + } else if (trustedProxies != null && trustedProxies.matcher(currentRemoteIp).matches()) { + proxiesHeaderValue.addFirst(currentRemoteIp); + } else { + idx--; // decrement idx because break statement doesn't do it + break; + } + } + // continue to loop on remoteIpHeaderValue to build the new value of the remoteIpHeader + LinkedList newRemoteIpHeaderValue = new LinkedList<>(); + for (; idx >= 0; idx--) { + String currentRemoteIp = remoteIpHeaderValue[idx]; + newRemoteIpHeaderValue.addFirst(currentRemoteIp); + } + + XForwardedRequest xRequest = new XForwardedRequest(request); + if (remoteIp != null) { + + xRequest.setRemoteAddr(remoteIp); + if (getEnableLookups()) { + // This isn't a lazy lookup but that would be a little more + // invasive - mainly in XForwardedRequest - and if + // enableLookups is true is seems reasonable that the + // hostname will be required so look it up here. + try { + InetAddress inetAddress = InetAddress.getByName(remoteIp); + // We know we need a DNS look up so use getCanonicalHostName() + xRequest.setRemoteHost(inetAddress.getCanonicalHostName()); + } catch (UnknownHostException e) { + log.debug(sm.getString("remoteIpFilter.invalidRemoteAddress", remoteIp), e); + xRequest.setRemoteHost(remoteIp); + } + } else { + xRequest.setRemoteHost(remoteIp); + } + + if (proxiesHeaderValue.size() == 0) { + xRequest.removeHeader(proxiesHeader); + } else { + String commaDelimitedListOfProxies = StringUtils.join(proxiesHeaderValue); + xRequest.setHeader(proxiesHeader, commaDelimitedListOfProxies); + } + if (newRemoteIpHeaderValue.size() == 0) { + xRequest.removeHeader(remoteIpHeader); + } else { + String commaDelimitedRemoteIpHeaderValue = StringUtils.join(newRemoteIpHeaderValue); + xRequest.setHeader(remoteIpHeader, commaDelimitedRemoteIpHeaderValue); + } + } + + if (protocolHeader != null) { + String protocolHeaderValue = request.getHeader(protocolHeader); + if (protocolHeaderValue == null) { + // Don't modify the secure, scheme and serverPort attributes + // of the request + } else if (isForwardedProtoHeaderValueSecure(protocolHeaderValue)) { + xRequest.setSecure(true); + xRequest.setScheme("https"); + setPorts(xRequest, httpsServerPort); + } else { + xRequest.setSecure(false); + xRequest.setScheme("http"); + setPorts(xRequest, httpServerPort); + } + } + + if (hostHeader != null) { + String hostHeaderValue = request.getHeader(hostHeader); + if (hostHeaderValue != null) { + try { + int portIndex = Host.parse(hostHeaderValue); + if (portIndex > -1) { + log.debug(sm.getString("remoteIpFilter.invalidHostWithPort", hostHeaderValue, hostHeader)); + hostHeaderValue = hostHeaderValue.substring(0, portIndex); + } + + xRequest.setServerName(hostHeaderValue); + if (isChangeLocalName()) { + xRequest.setLocalName(hostHeaderValue); + } + + } catch (IllegalArgumentException iae) { + log.debug(sm.getString("remoteIpFilter.invalidHostHeader", hostHeaderValue, hostHeader)); + } + } + } + request.setAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE, Boolean.TRUE); + + if (log.isTraceEnabled()) { + log.trace("Incoming request " + request.getRequestURI() + " with originalRemoteAddr [" + + request.getRemoteAddr() + "], originalRemoteHost=[" + request.getRemoteHost() + + "], originalSecure=[" + request.isSecure() + "], originalScheme=[" + request.getScheme() + + "], originalServerName=[" + request.getServerName() + "], originalServerPort=[" + + request.getServerPort() + "] will be seen as newRemoteAddr=[" + xRequest.getRemoteAddr() + + "], newRemoteHost=[" + xRequest.getRemoteHost() + "], newSecure=[" + xRequest.isSecure() + + "], newScheme=[" + xRequest.getScheme() + "], newServerName=[" + xRequest.getServerName() + + "], newServerPort=[" + xRequest.getServerPort() + "]"); + } + if (requestAttributesEnabled) { + request.setAttribute(AccessLog.REMOTE_ADDR_ATTRIBUTE, xRequest.getRemoteAddr()); + request.setAttribute(Globals.REMOTE_ADDR_ATTRIBUTE, xRequest.getRemoteAddr()); + request.setAttribute(AccessLog.REMOTE_HOST_ATTRIBUTE, xRequest.getRemoteHost()); + request.setAttribute(AccessLog.PROTOCOL_ATTRIBUTE, xRequest.getProtocol()); + request.setAttribute(AccessLog.SERVER_NAME_ATTRIBUTE, xRequest.getServerName()); + request.setAttribute(AccessLog.SERVER_PORT_ATTRIBUTE, Integer.valueOf(xRequest.getServerPort())); + } + chain.doFilter(xRequest, response); + } else { + if (log.isTraceEnabled()) { + log.trace("Skip RemoteIpFilter for request " + request.getRequestURI() + " with originalRemoteAddr '" + + request.getRemoteAddr() + "'"); + } + chain.doFilter(request, response); + } + + } + + /* + * Considers the value to be secure if it exclusively holds forwards for {@link #protocolHeaderHttpsValue}. + */ + private boolean isForwardedProtoHeaderValueSecure(String protocolHeaderValue) { + if (!protocolHeaderValue.contains(",")) { + return protocolHeaderHttpsValue.equalsIgnoreCase(protocolHeaderValue); + } + String[] forwardedProtocols = StringUtils.splitCommaSeparated(protocolHeaderValue); + if (forwardedProtocols.length == 0) { + return false; + } + for (String forwardedProtocol : forwardedProtocols) { + if (!protocolHeaderHttpsValue.equalsIgnoreCase(forwardedProtocol)) { + return false; + } + } + return true; + } + + private void setPorts(XForwardedRequest xrequest, int defaultPort) { + int port = defaultPort; + if (getPortHeader() != null) { + String portHeaderValue = xrequest.getHeader(getPortHeader()); + if (portHeaderValue != null) { + try { + port = Integer.parseInt(portHeaderValue); + } catch (NumberFormatException nfe) { + log.debug(sm.getString("remoteIpFilter.invalidPort", portHeaderValue, getPortHeader())); + } + } + } + xrequest.setServerPort(port); + if (isChangeLocalPort()) { + xrequest.setLocalPort(port); + } + } + + /** + * Wrap the incoming request in a {@link XForwardedRequest} if the http header + * x-forwarded-for is not empty. {@inheritDoc} + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) { + doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); + } else { + chain.doFilter(request, response); + } + } + + public boolean isChangeLocalName() { + return changeLocalName; + } + + public boolean isChangeLocalPort() { + return changeLocalPort; + } + + public int getHttpsServerPort() { + return httpsServerPort; + } + + public Pattern getInternalProxies() { + return internalProxies; + } + + public String getProtocolHeader() { + return protocolHeader; + } + + public String getPortHeader() { + return portHeader; + } + + public String getProtocolHeaderHttpsValue() { + return protocolHeaderHttpsValue; + } + + public String getProxiesHeader() { + return proxiesHeader; + } + + public String getRemoteIpHeader() { + return remoteIpHeader; + } + + /** + * @see #setRequestAttributesEnabled(boolean) + * + * @return true if the attributes will be logged, otherwise false + */ + public boolean getRequestAttributesEnabled() { + return requestAttributesEnabled; + } + + public Pattern getTrustedProxies() { + return trustedProxies; + } + + public boolean getEnableLookups() { + return enableLookups; + } + + @Override + public void init() throws ServletException { + if (getInitParameter(INTERNAL_PROXIES_PARAMETER) != null) { + setInternalProxies(getInitParameter(INTERNAL_PROXIES_PARAMETER)); + } + + if (getInitParameter(PROTOCOL_HEADER_PARAMETER) != null) { + setProtocolHeader(getInitParameter(PROTOCOL_HEADER_PARAMETER)); + } + + if (getInitParameter(PROTOCOL_HEADER_HTTPS_VALUE_PARAMETER) != null) { + setProtocolHeaderHttpsValue(getInitParameter(PROTOCOL_HEADER_HTTPS_VALUE_PARAMETER)); + } + + if (getInitParameter(HOST_HEADER_PARAMETER) != null) { + setHostHeader(getInitParameter(HOST_HEADER_PARAMETER)); + } + + if (getInitParameter(PORT_HEADER_PARAMETER) != null) { + setPortHeader(getInitParameter(PORT_HEADER_PARAMETER)); + } + + if (getInitParameter(CHANGE_LOCAL_NAME_PARAMETER) != null) { + setChangeLocalName(Boolean.parseBoolean(getInitParameter(CHANGE_LOCAL_NAME_PARAMETER))); + } + + if (getInitParameter(CHANGE_LOCAL_PORT_PARAMETER) != null) { + setChangeLocalPort(Boolean.parseBoolean(getInitParameter(CHANGE_LOCAL_PORT_PARAMETER))); + } + + if (getInitParameter(PROXIES_HEADER_PARAMETER) != null) { + setProxiesHeader(getInitParameter(PROXIES_HEADER_PARAMETER)); + } + + if (getInitParameter(REMOTE_IP_HEADER_PARAMETER) != null) { + setRemoteIpHeader(getInitParameter(REMOTE_IP_HEADER_PARAMETER)); + } + + if (getInitParameter(TRUSTED_PROXIES_PARAMETER) != null) { + setTrustedProxies(getInitParameter(TRUSTED_PROXIES_PARAMETER)); + } + + if (getInitParameter(HTTP_SERVER_PORT_PARAMETER) != null) { + try { + setHttpServerPort(Integer.parseInt(getInitParameter(HTTP_SERVER_PORT_PARAMETER))); + } catch (NumberFormatException e) { + throw new NumberFormatException(sm.getString("remoteIpFilter.invalidNumber", HTTP_SERVER_PORT_PARAMETER, + e.getLocalizedMessage())); + } + } + + if (getInitParameter(HTTPS_SERVER_PORT_PARAMETER) != null) { + try { + setHttpsServerPort(Integer.parseInt(getInitParameter(HTTPS_SERVER_PORT_PARAMETER))); + } catch (NumberFormatException e) { + throw new NumberFormatException(sm.getString("remoteIpFilter.invalidNumber", + HTTPS_SERVER_PORT_PARAMETER, e.getLocalizedMessage())); + } + } + + if (getInitParameter(ENABLE_LOOKUPS_PARAMETER) != null) { + setEnableLookups(Boolean.parseBoolean(getInitParameter(ENABLE_LOOKUPS_PARAMETER))); + } + } + + /** + *

    + * If true, the return values for both {@link ServletRequest#getLocalName()} and + * {@link ServletRequest#getServerName()} will be modified by this Filter rather than just + * {@link ServletRequest#getServerName()}. + *

    + *

    + * Default value : false + *

    + * + * @param changeLocalName The new flag value + */ + public void setChangeLocalName(boolean changeLocalName) { + this.changeLocalName = changeLocalName; + } + + /** + *

    + * If true, the return values for both {@link ServletRequest#getLocalPort()} and + * {@link ServletRequest#getServerPort()} will be modified by this Filter rather than just + * {@link ServletRequest#getServerPort()}. + *

    + *

    + * Default value : false + *

    + * + * @param changeLocalPort The new flag value + */ + public void setChangeLocalPort(boolean changeLocalPort) { + this.changeLocalPort = changeLocalPort; + } + + /** + *

    + * Server Port value if the {@link #protocolHeader} indicates HTTP (i.e. {@link #protocolHeader} is not null and has + * a value different of {@link #protocolHeaderHttpsValue}). + *

    + *

    + * Default value : 80 + *

    + * + * @param httpServerPort The server port to use + */ + public void setHttpServerPort(int httpServerPort) { + this.httpServerPort = httpServerPort; + } + + /** + *

    + * Server Port value if the {@link #protocolHeader} indicates HTTPS + *

    + *

    + * Default value : 443 + *

    + * + * @param httpsServerPort The server port to use + */ + public void setHttpsServerPort(int httpsServerPort) { + this.httpsServerPort = httpsServerPort; + } + + /** + *

    + * Regular expression that defines the internal proxies. + *

    + *

    + * Default value : + * 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254.\d{1,3}.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1 + *

    + * + * @param internalProxies The regexp + */ + public void setInternalProxies(String internalProxies) { + if (internalProxies == null || internalProxies.length() == 0) { + this.internalProxies = null; + } else { + this.internalProxies = Pattern.compile(internalProxies); + } + } + + /** + *

    + * Header that holds the incoming host, usually named X-Forwarded-Host. + *

    + *

    + * Default value : null + *

    + * + * @param hostHeader The header name + */ + public void setHostHeader(String hostHeader) { + this.hostHeader = hostHeader; + } + + /** + *

    + * Header that holds the incoming port, usually named X-Forwarded-Port. If null, + * {@link #httpServerPort} or {@link #httpsServerPort} will be used. + *

    + *

    + * Default value : null + *

    + * + * @param portHeader The header name + */ + public void setPortHeader(String portHeader) { + this.portHeader = portHeader; + } + + /** + *

    + * Header that holds the incoming protocol, usually named X-Forwarded-Proto. If null, + * request.scheme and request.secure will not be modified. + *

    + *

    + * Default value : X-Forwarded-Proto + *

    + * + * @param protocolHeader The header name + */ + public void setProtocolHeader(String protocolHeader) { + this.protocolHeader = protocolHeader; + } + + /** + *

    + * Case insensitive value of the protocol header to indicate that the incoming http request uses HTTPS. + *

    + *

    + * Default value : https + *

    + * + * @param protocolHeaderHttpsValue The header value + */ + public void setProtocolHeaderHttpsValue(String protocolHeaderHttpsValue) { + this.protocolHeaderHttpsValue = protocolHeaderHttpsValue; + } + + /** + *

    + * The proxiesHeader directive specifies a header into which mod_remoteip will collect a list of all of the + * intermediate client IP addresses trusted to resolve the actual remote IP. Note that intermediate + * RemoteIPTrustedProxy addresses are recorded in this header, while any intermediate RemoteIPInternalProxy + * addresses are discarded. + *

    + *

    + * Name of the http header that holds the list of trusted proxies that has been traversed by the http request. + *

    + *

    + * The value of this header can be comma delimited. + *

    + *

    + * Default value : X-Forwarded-By + *

    + * + * @param proxiesHeader The header name + */ + public void setProxiesHeader(String proxiesHeader) { + this.proxiesHeader = proxiesHeader; + } + + /** + *

    + * Name of the http header from which the remote ip is extracted. + *

    + *

    + * The value of this header can be comma delimited. + *

    + *

    + * Default value : X-Forwarded-For + *

    + * + * @param remoteIpHeader The header name + */ + public void setRemoteIpHeader(String remoteIpHeader) { + this.remoteIpHeader = remoteIpHeader; + } + + /** + * Should this filter set request attributes for IP address, Hostname, protocol and port used for the request? This + * are typically used in conjunction with an {@link AccessLog} which will otherwise log the original values. Default + * is true. The attributes set are: + *
      + *
    • org.apache.catalina.AccessLog.RemoteAddr
    • + *
    • org.apache.catalina.AccessLog.RemoteHost
    • + *
    • org.apache.catalina.AccessLog.Protocol
    • + *
    • org.apache.catalina.AccessLog.ServerPort
    • + *
    • org.apache.tomcat.remoteAddr
    • + *
    + * + * @param requestAttributesEnabled true causes the attributes to be set, false disables + * the setting of the attributes. + */ + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + this.requestAttributesEnabled = requestAttributesEnabled; + } + + /** + *

    + * Regular expression defining proxies that are trusted when they appear in the {@link #remoteIpHeader} header. + *

    + *

    + * Default value : empty list, no external proxy is trusted. + *

    + * + * @param trustedProxies The trusted proxies regexp + */ + public void setTrustedProxies(String trustedProxies) { + if (trustedProxies == null || trustedProxies.length() == 0) { + this.trustedProxies = null; + } else { + this.trustedProxies = Pattern.compile(trustedProxies); + } + } + + public void setEnableLookups(boolean enableLookups) { + this.enableLookups = enableLookups; + } + + /* + * Log objects are not Serializable but this Filter is because it extends GenericFilter. Tomcat won't serialize a + * Filter but in case something else does... + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + log = LogFactory.getLog(RemoteIpFilter.class); + } +} diff --git a/java/org/apache/catalina/filters/RequestDumperFilter.java b/java/org/apache/catalina/filters/RequestDumperFilter.java new file mode 100644 index 0000000..fc76581 --- /dev/null +++ b/java/org/apache/catalina/filters/RequestDumperFilter.java @@ -0,0 +1,272 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Enumeration; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + *

    + * Implementation of a Filter that logs interesting contents from the specified Request (before processing) and the + * corresponding Response (after processing). It is especially useful in debugging problems related to headers and + * cookies. + *

    + *

    + * When using this Filter, it is strongly recommended that the + * org.apache.catalina.filter.RequestDumperFilter logger is directed to a dedicated file and that the + * org.apache.juli.VerbatimFormatter is used. + *

    + * + * @author Craig R. McClanahan + */ +public class RequestDumperFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + private static final String NON_HTTP_REQ_MSG = "Not available. Non-http request."; + private static final String NON_HTTP_RES_MSG = "Not available. Non-http response."; + + private static final ThreadLocal timestamp = ThreadLocal.withInitial(Timestamp::new); + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private transient Log log = LogFactory.getLog(RequestDumperFilter.class); + + + /** + * Log the interesting request parameters, invoke the next Filter in the sequence, and log the interesting response + * parameters. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * @param chain The filter chain being processed + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest hRequest = null; + HttpServletResponse hResponse = null; + + if (request instanceof HttpServletRequest) { + hRequest = (HttpServletRequest) request; + } + if (response instanceof HttpServletResponse) { + hResponse = (HttpServletResponse) response; + } + + // Log pre-service information + doLog("START TIME ", getTimestamp()); + + if (hRequest == null) { + doLog(" requestURI", NON_HTTP_REQ_MSG); + doLog(" authType", NON_HTTP_REQ_MSG); + } else { + doLog(" requestURI", hRequest.getRequestURI()); + doLog(" authType", hRequest.getAuthType()); + } + + doLog(" characterEncoding", request.getCharacterEncoding()); + doLog(" contentLength", Long.toString(request.getContentLengthLong())); + doLog(" contentType", request.getContentType()); + + if (hRequest == null) { + doLog(" contextPath", NON_HTTP_REQ_MSG); + doLog(" cookie", NON_HTTP_REQ_MSG); + doLog(" header", NON_HTTP_REQ_MSG); + } else { + doLog(" contextPath", hRequest.getContextPath()); + Cookie cookies[] = hRequest.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + doLog(" cookie", cookie.getName() + "=" + cookie.getValue()); + } + } + Enumeration hnames = hRequest.getHeaderNames(); + while (hnames.hasMoreElements()) { + String hname = hnames.nextElement(); + Enumeration hvalues = hRequest.getHeaders(hname); + while (hvalues.hasMoreElements()) { + String hvalue = hvalues.nextElement(); + doLog(" header", hname + "=" + hvalue); + } + } + } + + doLog(" locale", request.getLocale().toString()); + + if (hRequest == null) { + doLog(" method", NON_HTTP_REQ_MSG); + } else { + doLog(" method", hRequest.getMethod()); + } + + Enumeration pnames = request.getParameterNames(); + while (pnames.hasMoreElements()) { + String pname = pnames.nextElement(); + String pvalues[] = request.getParameterValues(pname); + StringBuilder result = new StringBuilder(pname); + result.append('='); + for (int i = 0; i < pvalues.length; i++) { + if (i > 0) { + result.append(", "); + } + result.append(pvalues[i]); + } + doLog(" parameter", result.toString()); + } + + if (hRequest == null) { + doLog(" pathInfo", NON_HTTP_REQ_MSG); + } else { + doLog(" pathInfo", hRequest.getPathInfo()); + } + + doLog(" protocol", request.getProtocol()); + + if (hRequest == null) { + doLog(" queryString", NON_HTTP_REQ_MSG); + } else { + doLog(" queryString", hRequest.getQueryString()); + } + + doLog(" remoteAddr", request.getRemoteAddr()); + doLog(" remoteHost", request.getRemoteHost()); + + if (hRequest == null) { + doLog(" remoteUser", NON_HTTP_REQ_MSG); + doLog("requestedSessionId", NON_HTTP_REQ_MSG); + } else { + doLog(" remoteUser", hRequest.getRemoteUser()); + doLog("requestedSessionId", hRequest.getRequestedSessionId()); + } + + doLog(" scheme", request.getScheme()); + doLog(" serverName", request.getServerName()); + doLog(" serverPort", Integer.toString(request.getServerPort())); + + if (hRequest == null) { + doLog(" servletPath", NON_HTTP_REQ_MSG); + } else { + doLog(" servletPath", hRequest.getServletPath()); + } + + doLog(" isSecure", Boolean.valueOf(request.isSecure()).toString()); + doLog("------------------", "--------------------------------------------"); + + // Perform the request + chain.doFilter(request, response); + + // Log post-service information + doLog("------------------", "--------------------------------------------"); + if (hRequest == null) { + doLog(" authType", NON_HTTP_REQ_MSG); + } else { + doLog(" authType", hRequest.getAuthType()); + } + + doLog(" contentType", response.getContentType()); + + if (hResponse == null) { + doLog(" header", NON_HTTP_RES_MSG); + } else { + Iterable rhnames = hResponse.getHeaderNames(); + for (String rhname : rhnames) { + Iterable rhvalues = hResponse.getHeaders(rhname); + for (String rhvalue : rhvalues) { + doLog(" header", rhname + "=" + rhvalue); + } + } + } + + if (hRequest == null) { + doLog(" remoteUser", NON_HTTP_REQ_MSG); + } else { + doLog(" remoteUser", hRequest.getRemoteUser()); + } + + if (hResponse == null) { + doLog(" status", NON_HTTP_RES_MSG); + } else { + doLog(" status", Integer.toString(hResponse.getStatus())); + } + + doLog("END TIME ", getTimestamp()); + doLog("==================", "============================================"); + } + + private void doLog(String attribute, String value) { + StringBuilder sb = new StringBuilder(80); + sb.append(Thread.currentThread().getName()); + sb.append(' '); + sb.append(attribute); + sb.append('='); + sb.append(value); + log.info(sb.toString()); + } + + private String getTimestamp() { + Timestamp ts = timestamp.get(); + long currentTime = System.currentTimeMillis(); + + if ((ts.date.getTime() + 999) < currentTime) { + ts.date.setTime(currentTime - (currentTime % 1000)); + ts.update(); + } + return ts.dateString; + } + + + /* + * Log objects are not Serializable but this Filter is because it extends GenericFilter. Tomcat won't serialize a + * Filter but in case something else does... + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + log = LogFactory.getLog(RequestDumperFilter.class); + } + + + private static final class Timestamp { + private final Date date = new Date(0); + private final SimpleDateFormat format = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss"); + private String dateString = format.format(date); + + private void update() { + dateString = format.format(date); + } + } +} diff --git a/java/org/apache/catalina/filters/RequestFilter.java b/java/org/apache/catalina/filters/RequestFilter.java new file mode 100644 index 0000000..de1651c --- /dev/null +++ b/java/org/apache/catalina/filters/RequestFilter.java @@ -0,0 +1,239 @@ +/* + * 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.catalina.filters; + + +import java.io.IOException; +import java.util.regex.Pattern; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Implementation of a Filter that performs filtering based on comparing the appropriate request property (selected + * based on which subclass you choose to configure into your Container's pipeline) against the regular expressions + * configured for this Filter. + *

    + * This filter is configured by setting the allow and/or deny properties to a regular + * expressions (in the syntax supported by {@link Pattern}) to which the appropriate request property will be compared. + * Evaluation proceeds as follows: + *

      + *
    • The subclass extracts the request property to be filtered, and calls the common process() method. + *
    • If there is a deny expression configured, the property will be compared to the expression. If a match is found, + * this request will be rejected with a "Forbidden" HTTP response.
    • + *
    • If there is a allow expression configured, the property will be compared to the expression. If a match is found, + * this request will be allowed to pass through to the next filter in the current pipeline.
    • + *
    • If a deny expression was specified but no allow expression, allow this request to pass through (because none of + * the deny expressions matched it). + *
    • The request will be rejected with a "Forbidden" HTTP response.
    • + *
    + */ +public abstract class RequestFilter extends FilterBase { + + + // ----------------------------------------------------- Instance Variables + + /** + * The regular expression used to test for allowed requests. + */ + protected Pattern allow = null; + + /** + * The regular expression used to test for denied requests. + */ + protected Pattern deny = null; + + /** + * The HTTP response status code that is used when rejecting denied request. It is 403 by default, but may be + * changed to be 404. + */ + protected int denyStatus = HttpServletResponse.SC_FORBIDDEN; + + /** + * mime type -- "text/plain" + */ + private static final String PLAIN_TEXT_MIME_TYPE = "text/plain"; + + + // ------------------------------------------------------------- Properties + + + /** + * @return the regular expression used to test for allowed requests for this Filter, if any; otherwise, return + * null. + */ + public String getAllow() { + if (allow == null) { + return null; + } + return allow.toString(); + } + + + /** + * Set the regular expression used to test for allowed requests for this Filter, if any. + * + * @param allow The new allow expression + */ + public void setAllow(String allow) { + if (allow == null || allow.length() == 0) { + this.allow = null; + } else { + this.allow = Pattern.compile(allow); + } + } + + + /** + * @return the regular expression used to test for denied requests for this Filter, if any; otherwise, return + * null. + */ + public String getDeny() { + if (deny == null) { + return null; + } + return deny.toString(); + } + + + /** + * Set the regular expression used to test for denied requests for this Filter, if any. + * + * @param deny The new deny expression + */ + public void setDeny(String deny) { + if (deny == null || deny.length() == 0) { + this.deny = null; + } else { + this.deny = Pattern.compile(deny); + } + } + + + /** + * @return response status code that is used to reject denied request. + */ + public int getDenyStatus() { + return denyStatus; + } + + + /** + * Set response status code that is used to reject denied request. + * + * @param denyStatus The status code for deny + */ + public void setDenyStatus(int denyStatus) { + this.denyStatus = denyStatus; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Extract the desired request property, and pass it (along with the specified request and response objects) to the + * protected process() method to perform the actual filtering. This method must be implemented by a + * concrete subclass. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * @param chain The filter chain + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public abstract void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException; + + + // ------------------------------------------------------ Protected Methods + + + @Override + protected boolean isConfigProblemFatal() { + return true; + } + + + /** + * Perform the filtering that has been configured for this Filter, matching against the specified request property. + * + * @param property The request property on which to filter + * @param request The servlet request to be processed + * @param response The servlet response to be processed + * @param chain The filter chain + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + protected void process(String property, ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + if (isAllowed(property)) { + chain.doFilter(request, response); + } else { + if (response instanceof HttpServletResponse) { + if (getLogger().isDebugEnabled()) { + getLogger().debug(sm.getString("requestFilter.deny", ((HttpServletRequest) request).getRequestURI(), + property)); + } + ((HttpServletResponse) response).sendError(denyStatus); + } else { + sendErrorWhenNotHttp(response); + } + } + } + + + /** + * Process the allow and deny rules for the provided property. + * + * @param property The property to test against the allow and deny lists + * + * @return true if this request should be allowed, false otherwise + */ + private boolean isAllowed(String property) { + if (deny != null && deny.matcher(property).matches()) { + return false; + } + + // Check the allow patterns, if any + if (allow != null && allow.matcher(property).matches()) { + return true; + } + + // Allow if denies specified but not allows + if (deny != null && allow == null) { + return true; + } + + // Deny this request + return false; + } + + private void sendErrorWhenNotHttp(ServletResponse response) throws IOException { + response.setContentType(PLAIN_TEXT_MIME_TYPE); + response.getWriter().write(sm.getString("http.403")); + response.getWriter().flush(); + } +} diff --git a/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java b/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java new file mode 100644 index 0000000..5e543da --- /dev/null +++ b/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java @@ -0,0 +1,251 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +/** + * Provides basic CSRF protection for REST APIs. The filter assumes that the clients have adapted the transfer of the + * nonce through the 'X-CSRF-Token' header. + * + *
    + * Positive scenario:
    + *           Client                            Server
    + *              |                                 |
    + *              | GET Fetch Request              \| JSESSIONID
    + *              |---------------------------------| X-CSRF-Token
    + *              |                                /| pair generation
    + *              |/Response to Fetch Request       |
    + *              |---------------------------------|
    + * JSESSIONID   |\                                |
    + * X-CSRF-Token |                                 |
    + * pair cached  | POST Request with valid nonce  \| JSESSIONID
    + *              |---------------------------------| X-CSRF-Token
    + *              |                                /| pair validation
    + *              |/ Response to POST Request       |
    + *              |---------------------------------|
    + *              |\                                |
    + *
    + * Negative scenario:
    + *           Client                            Server
    + *              |                                 |
    + *              | POST Request without nonce     \| JSESSIONID
    + *              |---------------------------------| X-CSRF-Token
    + *              |                                /| pair validation
    + *              |/Request is rejected             |
    + *              |---------------------------------|
    + *              |\                                |
    + *
    + *           Client                            Server
    + *              |                                 |
    + *              | POST Request with invalid nonce\| JSESSIONID
    + *              |---------------------------------| X-CSRF-Token
    + *              |                                /| pair validation
    + *              |/Request is rejected             |
    + *              |---------------------------------|
    + *              |\                                |
    + * 
    + */ +public class RestCsrfPreventionFilter extends CsrfPreventionFilterBase { + private enum MethodType { + NON_MODIFYING_METHOD, + MODIFYING_METHOD + } + + private static final Pattern NON_MODIFYING_METHODS_PATTERN = Pattern.compile("GET|HEAD|OPTIONS"); + private static final Predicate nonModifyingMethods = + m -> Objects.nonNull(m) && NON_MODIFYING_METHODS_PATTERN.matcher(m).matches(); + + private Set pathsAcceptingParams = new HashSet<>(); + + private String pathsDelimiter = ","; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Set the parameters + super.init(filterConfig); + + // Put the expected request header name into the application scope + filterConfig.getServletContext().setAttribute(Constants.CSRF_REST_NONCE_HEADER_NAME_KEY, + Constants.CSRF_REST_NONCE_HEADER_NAME); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) { + MethodType mType = MethodType.MODIFYING_METHOD; + if (nonModifyingMethods.test(((HttpServletRequest) request).getMethod())) { + mType = MethodType.NON_MODIFYING_METHOD; + } + + RestCsrfPreventionStrategy strategy; + switch (mType) { + case NON_MODIFYING_METHOD: + strategy = new FetchRequest(); + break; + default: + strategy = new StateChangingRequest(); + break; + } + + if (!strategy.apply((HttpServletRequest) request, (HttpServletResponse) response)) { + return; + } + } + chain.doFilter(request, response); + } + + private interface RestCsrfPreventionStrategy { + NonceSupplier nonceFromRequestHeader = HttpServletRequest::getHeader; + NonceSupplier nonceFromRequestParams = ServletRequest::getParameterValues; + NonceSupplier nonceFromSession = + (s, k) -> Objects.isNull(s) ? null : (String) s.getAttribute(k); + + NonceConsumer nonceToResponse = HttpServletResponse::setHeader; + NonceConsumer nonceToSession = HttpSession::setAttribute; + + boolean apply(HttpServletRequest request, HttpServletResponse response) throws IOException; + } + + private class StateChangingRequest implements RestCsrfPreventionStrategy { + + @Override + public boolean apply(HttpServletRequest request, HttpServletResponse response) throws IOException { + + String nonceRequest = extractNonceFromRequest(request); + HttpSession session = request.getSession(false); + String nonceSession = nonceFromSession.getNonce(session, Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME); + + if (isValidStateChangingRequest(nonceRequest, nonceSession)) { + return true; + } + + nonceToResponse.setNonce(response, Constants.CSRF_REST_NONCE_HEADER_NAME, + Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + response.sendError(getDenyStatus(), sm.getString("restCsrfPreventionFilter.invalidNonce")); + + if (getLogger().isDebugEnabled()) { + getLogger().debug(sm.getString("restCsrfPreventionFilter.invalidNonce.debug", request.getMethod(), + request.getRequestURI(), Boolean.valueOf(request.getRequestedSessionId() != null), session, + Boolean.valueOf(nonceRequest != null), Boolean.valueOf(nonceSession != null))); + } + return false; + } + + private boolean isValidStateChangingRequest(String reqNonce, String sessionNonce) { + return Objects.nonNull(reqNonce) && Objects.nonNull(sessionNonce) && Objects.equals(reqNonce, sessionNonce); + } + + private String extractNonceFromRequest(HttpServletRequest request) { + String nonceFromRequest = nonceFromRequestHeader.getNonce(request, Constants.CSRF_REST_NONCE_HEADER_NAME); + if ((Objects.isNull(nonceFromRequest) || Objects.equals("", nonceFromRequest)) && + !getPathsAcceptingParams().isEmpty() && + getPathsAcceptingParams().contains(getRequestedPath(request))) { + nonceFromRequest = extractNonceFromRequestParams(request); + } + return nonceFromRequest; + } + + private String extractNonceFromRequestParams(HttpServletRequest request) { + String[] params = nonceFromRequestParams.getNonce(request, Constants.CSRF_REST_NONCE_HEADER_NAME); + if (Objects.nonNull(params) && params.length > 0) { + String nonce = params[0]; + for (String param : params) { + if (!Objects.equals(param, nonce)) { + if (getLogger().isDebugEnabled()) { + getLogger().debug(sm.getString("restCsrfPreventionFilter.multipleNonce.debug", + request.getMethod(), request.getRequestURI())); + } + return null; + } + } + return nonce; + } + return null; + } + } + + private class FetchRequest implements RestCsrfPreventionStrategy { + private final Predicate fetchRequest = Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE::equalsIgnoreCase; + + @Override + public boolean apply(HttpServletRequest request, HttpServletResponse response) { + if (fetchRequest.test(nonceFromRequestHeader.getNonce(request, Constants.CSRF_REST_NONCE_HEADER_NAME))) { + String nonceFromSessionStr = nonceFromSession.getNonce(request.getSession(false), + Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME); + if (nonceFromSessionStr == null) { + nonceFromSessionStr = generateNonce(request); + nonceToSession.setNonce(Objects.requireNonNull(request.getSession(true)), + Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME, nonceFromSessionStr); + } + nonceToResponse.setNonce(response, Constants.CSRF_REST_NONCE_HEADER_NAME, nonceFromSessionStr); + if (getLogger().isDebugEnabled()) { + getLogger().debug(sm.getString("restCsrfPreventionFilter.fetch.debug", request.getMethod(), + request.getRequestURI())); + } + } + return true; + } + + } + + @FunctionalInterface + private interface NonceSupplier { + R getNonce(T supplier, String key); + } + + @FunctionalInterface + private interface NonceConsumer { + void setNonce(T consumer, String key, String value); + } + + /** + * A comma separated list of URLs that can accept nonces via request parameter 'X-CSRF-Token'. For use cases when a + * nonce information cannot be provided via header, one can provide it via request parameters. If there is a + * X-CSRF-Token header, it will be taken with preference over any parameter with the same name in the request. + * Request parameters cannot be used to fetch new nonce, only header. + * + * @param pathsList Comma separated list of URLs to be configured as paths accepting request parameters with nonce + * information. + */ + public void setPathsAcceptingParams(String pathsList) { + if (Objects.nonNull(pathsList)) { + Arrays.asList(pathsList.split(pathsDelimiter)).forEach(e -> pathsAcceptingParams.add(e.trim())); + } + } + + public Set getPathsAcceptingParams() { + return pathsAcceptingParams; + } +} diff --git a/java/org/apache/catalina/filters/SessionInitializerFilter.java b/java/org/apache/catalina/filters/SessionInitializerFilter.java new file mode 100644 index 0000000..db7347c --- /dev/null +++ b/java/org/apache/catalina/filters/SessionInitializerFilter.java @@ -0,0 +1,58 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +/** + * A {@link jakarta.servlet.Filter} that initializes the {@link HttpSession} for the {@link HttpServletRequest} by + * calling its getSession() method. + *

    + * This is required for some operations with WebSocket requests, where it is too late to initialize the HttpSession + * object, and the current Java WebSocket specification does not provide a way to do so. + */ +public class SessionInitializerFilter implements Filter { + + /** + * Calls {@link HttpServletRequest}'s getSession() to initialize the HttpSession and continues processing the chain. + * + * @param request The request to process + * @param response The response associated with the request + * @param chain Provides access to the next filter in the chain for this filter to pass the request and response + * to for further processing + * + * @throws IOException if an I/O error occurs during this filter's processing of the request + * @throws ServletException if the processing fails for any other reason + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + ((HttpServletRequest) request).getSession(); + + chain.doFilter(request, response); + } + +} diff --git a/java/org/apache/catalina/filters/SetCharacterEncodingFilter.java b/java/org/apache/catalina/filters/SetCharacterEncodingFilter.java new file mode 100644 index 0000000..cb20a20 --- /dev/null +++ b/java/org/apache/catalina/filters/SetCharacterEncodingFilter.java @@ -0,0 +1,141 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + *

    + * Example filter that sets the character encoding to be used in parsing the incoming request, either unconditionally or + * only if the client did not specify a character encoding. Configuration of this filter is based on the following + * initialization parameters: + *

    + *
      + *
    • encoding - The character encoding to be configured for this request, either conditionally or + * unconditionally based on the ignore initialization parameter. This parameter is required, so there is no + * default.
    • + *
    • ignore - If set to "true", any character encoding specified by the client is ignored, and the + * value returned by the selectEncoding() method is set. If set to "false, selectEncoding() is + * called only if the client has not already specified an encoding. By default, this parameter is set + * to "false".
    • + *
    + *

    + * Although this filter can be used unchanged, it is also easy to subclass it and make the selectEncoding() + * method more intelligent about what encoding to choose, based on characteristics of the incoming request (such as the + * values of the Accept-Language and User-Agent headers, or a value stashed in the current + * user's session. + *

    + */ +public class SetCharacterEncodingFilter extends FilterBase { + + // Log must be non-static as loggers are created per class-loader and this + // Filter may be used in multiple class loaders + private final Log log = LogFactory.getLog(SetCharacterEncodingFilter.class); // must not be static + + + // ----------------------------------------------------- Instance Variables + + /** + * The default character encoding to set for requests that pass through this filter. + */ + private String encoding = null; + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return encoding; + } + + + /** + * Should a character encoding specified by the client be ignored? + */ + private boolean ignore = false; + + public void setIgnore(boolean ignore) { + this.ignore = ignore; + } + + public boolean isIgnore() { + return ignore; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Select and set (if specified) the character encoding to be used to interpret request parameters for this request. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param chain The filter chain we are processing + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + // Conditionally select and set the character encoding to be used + if (ignore || (request.getCharacterEncoding() == null)) { + String characterEncoding = selectEncoding(request); + if (characterEncoding != null) { + request.setCharacterEncoding(characterEncoding); + } + } + + // Pass control on to the next filter + chain.doFilter(request, response); + } + + + // ------------------------------------------------------ Protected Methods + + @Override + protected Log getLogger() { + return log; + } + + + /** + * Select an appropriate character encoding to be used, based on the characteristics of the current request and/or + * filter initialization parameters. If no character encoding should be set, return null. + *

    + * The default implementation unconditionally returns the value configured by the encoding + * initialization parameter for this filter. + * + * @param request The servlet request we are processing + * + * @return the encoding that was configured + */ + protected String selectEncoding(ServletRequest request) { + return this.encoding; + } +} diff --git a/java/org/apache/catalina/filters/WebdavFixFilter.java b/java/org/apache/catalina/filters/WebdavFixFilter.java new file mode 100644 index 0000000..7e0c119 --- /dev/null +++ b/java/org/apache/catalina/filters/WebdavFixFilter.java @@ -0,0 +1,122 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Filter that attempts to force MS WebDAV clients connecting on port 80 to use a WebDAV client that actually works. + * Other workarounds that might help include: + *

      + *
    • Specifying the port, even if it is port 80, when trying to connect.
    • + *
    • Cancelling the first authentication dialog box and then trying to reconnect.
    • + *
    + * Generally each different version of the MS client has a different set of problems. + *

    + * TODO: Update this filter to recognise specific MS clients and apply the appropriate workarounds for that particular + * client + *

    + * As a filter, this is configured in web.xml like any other Filter. You usually want to map this filter to whatever + * your WebDAV servlet is mapped to. + *

    + * In addition to the issues fixed by this Filter, the following issues have also been observed that cannot be fixed by + * this filter. Where possible the filter will add an message to the logs. + *

    + * XP x64 SP2 (MiniRedir Version 3790) + *

      + *
    • Only connects to port 80
    • + *
    • Unknown issue means it doesn't work
    • + *
    + */ +public class WebdavFixFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + protected static final StringManager sm = StringManager.getManager(WebdavFixFilter.class); + + /* Start string for all versions */ + private static final String UA_MINIDIR_START = "Microsoft-WebDAV-MiniRedir"; + /* XP 32-bit SP3 */ + private static final String UA_MINIDIR_5_1_2600 = "Microsoft-WebDAV-MiniRedir/5.1.2600"; + + /* XP 64-bit SP2 */ + private static final String UA_MINIDIR_5_2_3790 = "Microsoft-WebDAV-MiniRedir/5.2.3790"; + + /** + * Check for the broken MS WebDAV client and if detected issue a re-direct that hopefully will cause the non-broken + * client to be used. + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + chain.doFilter(request, response); + return; + } + HttpServletRequest httpRequest = ((HttpServletRequest) request); + HttpServletResponse httpResponse = ((HttpServletResponse) response); + String ua = httpRequest.getHeader("User-Agent"); + + if (ua == null || ua.length() == 0 || !ua.startsWith(UA_MINIDIR_START)) { + // No UA or starts with non MS value + // Hope everything just works... + chain.doFilter(request, response); + } else if (ua.startsWith(UA_MINIDIR_5_1_2600)) { + // XP 32-bit SP3 - needs redirect with explicit port + httpResponse.sendRedirect(buildRedirect(httpRequest)); + } else if (ua.startsWith(UA_MINIDIR_5_2_3790)) { + // XP 64-bit SP2 + if (!httpRequest.getContextPath().isEmpty()) { + getServletContext().log(sm.getString("webDavFilter.xpRootContext")); + } + // Namespace issue maybe + // see http://greenbytes.de/tech/webdav/webdav-redirector-list.html + getServletContext().log(sm.getString("webDavFilter.xpProblem")); + + chain.doFilter(request, response); + } else { + // Don't know which MS client it is - try the redirect with an + // explicit port in the hope that it moves the client to a different + // WebDAV implementation that works + httpResponse.sendRedirect(buildRedirect(httpRequest)); + } + } + + private String buildRedirect(HttpServletRequest request) { + StringBuilder location = new StringBuilder(request.getRequestURL().length()); + location.append(request.getScheme()); + location.append("://"); + location.append(request.getServerName()); + location.append(':'); + // If we include the port, even if it is 80, then MS clients will use + // a WebDAV client that works rather than the MiniRedir that has + // problems with BASIC authentication + location.append(request.getServerPort()); + location.append(request.getRequestURI()); + return location.toString(); + } + +} diff --git a/java/org/apache/catalina/ha/CatalinaCluster.java b/java/org/apache/catalina/ha/CatalinaCluster.java new file mode 100644 index 0000000..af53bba --- /dev/null +++ b/java/org/apache/catalina/ha/CatalinaCluster.java @@ -0,0 +1,114 @@ +/* + * 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.catalina.ha; + +import java.util.Map; + +import org.apache.catalina.Cluster; +import org.apache.catalina.Manager; +import org.apache.catalina.Valve; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.Member; + + +/** + * A CatalinaCluster interface allows to plug in and out the different cluster implementations + */ +public interface CatalinaCluster extends Cluster { + // ----------------------------------------------------- Instance Variables + + /** + * Sends a message to all the members in the cluster + * + * @param msg ClusterMessage + */ + void send(ClusterMessage msg); + + /** + * Sends a message to a specific member in the cluster. + * + * @param msg ClusterMessage + * @param dest Member + */ + void send(ClusterMessage msg, Member dest); + + /** + * Sends a message with the specified sendOptions to a specific member in the cluster. + * + * @param msg ClusterMessage + * @param dest Member + * @param sendOptions sendOptions + */ + void send(ClusterMessage msg, Member dest, int sendOptions); + + /** + * @return true if the cluster has members. + */ + boolean hasMembers(); + + /** + * @return an array containing all the members currently participating in the cluster. + */ + Member[] getMembers(); + + /** + * @return the member that represents this node. + */ + Member getLocalMember(); + + void addValve(Valve valve); + + void addClusterListener(ClusterListener listener); + + void removeClusterListener(ClusterListener listener); + + void setClusterDeployer(ClusterDeployer deployer); + + ClusterDeployer getClusterDeployer(); + + /** + * @return The map of managers + */ + Map getManagers(); + + /** + * Get Manager + * + * @param name The manager name + * + * @return The manager + */ + Manager getManager(String name); + + /** + * Get a new cluster name for a manager. + * + * @param name Override name (optional) + * @param manager The manager + * + * @return the manager name in the cluster + */ + String getManagerName(String name, Manager manager); + + Valve[] getValves(); + + void setChannel(Channel channel); + + Channel getChannel(); + + +} diff --git a/java/org/apache/catalina/ha/ClusterDeployer.java b/java/org/apache/catalina/ha/ClusterDeployer.java new file mode 100644 index 0000000..8e8e33d --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterDeployer.java @@ -0,0 +1,96 @@ +/* + * 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.catalina.ha; + +import java.io.File; +import java.io.IOException; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.tribes.ChannelListener; + +/** + * A ClusterDeployer interface allows to plug in and out the different deployment implementations + */ +public interface ClusterDeployer extends ChannelListener { + + /** + * Start the cluster deployer, the owning container will invoke this + * + * @throws Exception - if failure to start cluster + */ + void start() throws Exception; + + /** + * Stops the cluster deployer, the owning container will invoke this + * + * @throws LifecycleException Error stopping cluster deployer + */ + void stop() throws LifecycleException; + + /** + * Install a new web application, whose web application archive is at the specified URL, into this container and all + * the other members of the cluster with the specified context name. + *

    + * If this application is successfully installed locally, a ContainerEvent of type INSTALL_EVENT will + * be sent to all registered listeners, with the newly created Context as an argument. + * + * @param contextName The context name to which this application should be installed (must be unique) + * @param webapp A WAR file or unpacked directory structure containing the web application to be installed + * + * @exception IllegalArgumentException if the specified context name is malformed + * @exception IllegalStateException if the specified context name is already attached to an existing web + * application + * @exception IOException if an input/output error was encountered during installation + */ + void install(String contextName, File webapp) throws IOException; + + /** + * Remove an existing web application, attached to the specified context name. If this application is successfully + * removed, a ContainerEvent of type REMOVE_EVENT will be sent to all registered listeners, with the + * removed Context as an argument. Deletes the web application war file and/or directory if they exist + * in the Host's appBase. + * + * @param contextName The context name of the application to be removed + * @param undeploy boolean flag to remove web application from server + * + * @exception IllegalArgumentException if the specified context name is malformed + * @exception IllegalArgumentException if the specified context name does not identify a currently installed web + * application + * @exception IOException if an input/output error occurs during removal + */ + void remove(String contextName, boolean undeploy) throws IOException; + + /** + * call from container Background Process + */ + void backgroundProcess(); + + /** + * Returns the cluster the cluster deployer is associated with + * + * @return CatalinaCluster + */ + CatalinaCluster getCluster(); + + /** + * Associates the cluster deployer with a cluster + * + * @param cluster CatalinaCluster + */ + void setCluster(CatalinaCluster cluster); + +} diff --git a/java/org/apache/catalina/ha/ClusterListener.java b/java/org/apache/catalina/ha/ClusterListener.java new file mode 100644 index 0000000..0206cbb --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterListener.java @@ -0,0 +1,104 @@ +/* + * 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.catalina.ha; + +import java.io.Serializable; + +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.Member; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + * Receive SessionID cluster change from other backup node after primary session node is failed. + * + * @author Peter Rossbach + */ +public abstract class ClusterListener implements ChannelListener { + + private static final Log log = LogFactory.getLog(ClusterListener.class); + + // --Instance Variables-------------------------------------- + + /** + * The string manager for this package. + */ + + protected CatalinaCluster cluster = null; + + // --Constructor--------------------------------------------- + + public ClusterListener() { + // NO-OP + } + + // --Instance Getters/Setters-------------------------------- + + public CatalinaCluster getCluster() { + return cluster; + } + + public void setCluster(CatalinaCluster cluster) { + if (log.isTraceEnabled()) { + if (cluster != null) { + log.trace("add ClusterListener " + this.toString() + " to cluster" + cluster); + } else { + log.trace("remove ClusterListener " + this.toString() + " from cluster"); + } + } + this.cluster = cluster; + } + + // --Logic--------------------------------------------------- + + @Override + public final void messageReceived(Serializable msg, Member member) { + if (msg instanceof ClusterMessage) { + messageReceived((ClusterMessage) msg); + } + } + + @Override + public final boolean accept(Serializable msg, Member member) { + if (msg instanceof ClusterMessage) { + return true; + } + return false; + } + + + /** + * Callback from the cluster, when a message is received, The cluster will broadcast it invoking the messageReceived + * on the receiver. + * + * @param msg ClusterMessage - the message received from the cluster + */ + public abstract void messageReceived(ClusterMessage msg); + + + /** + * Accept only SessionIDMessages + * + * @param msg ClusterMessage + * + * @return boolean - returns true to indicate that messageReceived should be invoked. If false is returned, the + * messageReceived method will not be invoked. + */ + public abstract boolean accept(ClusterMessage msg); + +} diff --git a/java/org/apache/catalina/ha/ClusterManager.java b/java/org/apache/catalina/ha/ClusterManager.java new file mode 100644 index 0000000..6487dcd --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterManager.java @@ -0,0 +1,99 @@ +/* + * 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.catalina.ha; + + +import java.io.IOException; + +import org.apache.catalina.Manager; +import org.apache.catalina.tribes.io.ReplicationStream; + + +/** + * The common interface used by all cluster manager. This is so that we can have a more pluggable way of swapping + * session managers for different algorithms. + * + * @author Peter Rossbach + */ +public interface ClusterManager extends Manager { + + /** + * A message was received from another node, this is the callback method to implement if you are interested in + * receiving replication messages. + * + * @param msg - the message received. + */ + void messageDataReceived(ClusterMessage msg); + + /** + * When the request has been completed, the replication valve will notify the manager, and the manager will decide + * whether any replication is needed or not. If there is a need for replication, the manager will create a session + * message and that will be replicated. The cluster determines where it gets sent. + * + * @param sessionId - the sessionId that just completed. + * + * @return a SessionMessage to be sent. + */ + ClusterMessage requestCompleted(String sessionId); + + /** + * When the manager expires session not tied to a request. The cluster will periodically ask for a list of sessions + * that should expire and that should be sent across the wire. + * + * @return String[] The invalidated sessions + */ + String[] getInvalidatedSessions(); + + /** + * Return the name of the manager, at host /context name and at engine hostname+/context. + * + * @return String + * + * @since 5.5.10 + */ + String getName(); + + /** + * Set the name of the manager, at host /context name and at engine hostname+/context + * + * @param name The manager name + * + * @since 5.5.10 + */ + void setName(String name); + + CatalinaCluster getCluster(); + + void setCluster(CatalinaCluster cluster); + + /** + * Open stream and use correct ClassLoader (Container), switching thread context class loader. + * + * @param data The data + * + * @return The object input stream + * + * @throws IOException An error occurred + */ + ReplicationStream getReplicationStream(byte[] data) throws IOException; + + ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException; + + boolean isNotifyListenersOnReplication(); + + ClusterManager cloneFromTemplate(); +} diff --git a/java/org/apache/catalina/ha/ClusterMessage.java b/java/org/apache/catalina/ha/ClusterMessage.java new file mode 100644 index 0000000..c1b0966 --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterMessage.java @@ -0,0 +1,33 @@ +/* + * 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.catalina.ha; + +import java.io.Serializable; + +import org.apache.catalina.tribes.Member; + +public interface ClusterMessage extends Serializable { + Member getAddress(); + + void setAddress(Member member); + + String getUniqueId(); + + long getTimestamp(); + + void setTimestamp(long timestamp); +} diff --git a/java/org/apache/catalina/ha/ClusterMessageBase.java b/java/org/apache/catalina/ha/ClusterMessageBase.java new file mode 100644 index 0000000..794ebd6 --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterMessageBase.java @@ -0,0 +1,51 @@ +/* + * 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.catalina.ha; + +import org.apache.catalina.tribes.Member; + +public abstract class ClusterMessageBase implements ClusterMessage { + + private static final long serialVersionUID = 1L; + + private long timestamp; + protected transient Member address; + + public ClusterMessageBase() { + // NO-OP + } + + @Override + public Member getAddress() { + return address; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public void setAddress(Member member) { + this.address = member; + } + + @Override + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } +} diff --git a/java/org/apache/catalina/ha/ClusterRuleSet.java b/java/org/apache/catalina/ha/ClusterRuleSet.java new file mode 100644 index 0000000..63081c9 --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterRuleSet.java @@ -0,0 +1,184 @@ +/* + * 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.catalina.ha; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; + +/** + *

    + * RuleSet for processing the contents of a Cluster definition element. + *

    + * + * @author Peter Rossbach + */ +public class ClusterRuleSet implements RuleSet { + + + // ----------------------------------------------------- Instance Variables + + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + + // ------------------------------------------------------------ Constructor + + + /** + * Construct an instance of this RuleSet with the default matching pattern prefix. + */ + public ClusterRuleSet() { + this(""); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + */ + public ClusterRuleSet(String prefix) { + this.prefix = prefix; + } + + + // --------------------------------------------------------- Public Methods + + + /** + *

    + * Add the set of Rule instances defined in this RuleSet to the specified Digester instance, + * associating them with our namespace URI (if any). This method should only be called by a Digester instance. + *

    + * + * @param digester Digester instance to which the new Rule instances should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + // Cluster configuration start + digester.addObjectCreate(prefix + "Manager", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Manager"); + digester.addSetNext(prefix + "Manager", "setManagerTemplate", "org.apache.catalina.ha.ClusterManager"); + digester.addObjectCreate(prefix + "Manager/SessionIdGenerator", + "org.apache.catalina.util.StandardSessionIdGenerator", "className"); + digester.addSetProperties(prefix + "Manager/SessionIdGenerator"); + digester.addSetNext(prefix + "Manager/SessionIdGenerator", "setSessionIdGenerator", + "org.apache.catalina.SessionIdGenerator"); + + digester.addObjectCreate(prefix + "Channel", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Channel"); + digester.addSetNext(prefix + "Channel", "setChannel", "org.apache.catalina.tribes.Channel"); + + + String channelPrefix = prefix + "Channel/"; + + // channel properties + digester.addObjectCreate(channelPrefix + "Membership", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Membership"); + digester.addSetNext(channelPrefix + "Membership", "setMembershipService", + "org.apache.catalina.tribes.MembershipService"); + + // add + digester.addObjectCreate(channelPrefix + "Membership/LocalMember", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Membership/LocalMember"); + digester.addSetNext(channelPrefix + "Membership/LocalMember", "setLocalMember", + "org.apache.catalina.tribes.membership.StaticMember"); + digester.addObjectCreate(channelPrefix + "Membership/Member", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Membership/Member"); + digester.addSetNext(channelPrefix + "Membership/Member", "addStaticMember", + "org.apache.catalina.tribes.membership.StaticMember"); + // add end + + digester.addObjectCreate(channelPrefix + "MembershipListener", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "MembershipListener"); + digester.addSetNext(channelPrefix + "MembershipListener", "addMembershipListener", + "org.apache.catalina.tribes.MembershipListener"); + + digester.addObjectCreate(channelPrefix + "Sender", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Sender"); + digester.addSetNext(channelPrefix + "Sender", "setChannelSender", "org.apache.catalina.tribes.ChannelSender"); + + digester.addObjectCreate(channelPrefix + "Sender/Transport", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Sender/Transport"); + digester.addSetNext(channelPrefix + "Sender/Transport", "setTransport", + "org.apache.catalina.tribes.transport.MultiPointSender"); + + + digester.addObjectCreate(channelPrefix + "Receiver", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Receiver"); + digester.addSetNext(channelPrefix + "Receiver", "setChannelReceiver", + "org.apache.catalina.tribes.ChannelReceiver"); + + digester.addObjectCreate(channelPrefix + "Interceptor", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Interceptor"); + digester.addSetNext(channelPrefix + "Interceptor", "addInterceptor", + "org.apache.catalina.tribes.ChannelInterceptor"); + + digester.addObjectCreate(channelPrefix + "Interceptor/LocalMember", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Interceptor/LocalMember"); + digester.addSetNext(channelPrefix + "Interceptor/LocalMember", "setLocalMember", + "org.apache.catalina.tribes.Member"); + + digester.addObjectCreate(channelPrefix + "Interceptor/Member", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Interceptor/Member"); + digester.addSetNext(channelPrefix + "Interceptor/Member", "addStaticMember", + "org.apache.catalina.tribes.Member"); + + digester.addObjectCreate(channelPrefix + "ChannelListener", null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "ChannelListener"); + digester.addSetNext(channelPrefix + "ChannelListener", "addChannelListener", + "org.apache.catalina.tribes.ChannelListener"); + + digester.addObjectCreate(prefix + "Valve", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Valve"); + digester.addSetNext(prefix + "Valve", "addValve", "org.apache.catalina.Valve"); + + digester.addObjectCreate(prefix + "Deployer", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Deployer"); + digester.addSetNext(prefix + "Deployer", "setClusterDeployer", "org.apache.catalina.ha.ClusterDeployer"); + + digester.addObjectCreate(prefix + "Listener", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Listener"); + digester.addSetNext(prefix + "Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); + + digester.addObjectCreate(prefix + "ClusterListener", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "ClusterListener"); + digester.addSetNext(prefix + "ClusterListener", "addClusterListener", "org.apache.catalina.ha.ClusterListener"); + // Cluster configuration end + } + +} diff --git a/java/org/apache/catalina/ha/ClusterSession.java b/java/org/apache/catalina/ha/ClusterSession.java new file mode 100644 index 0000000..db455b8 --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterSession.java @@ -0,0 +1,39 @@ +/* + * 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.catalina.ha; + +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.Session; + +public interface ClusterSession extends Session, HttpSession { + /** + * returns true if this session is the primary session, if that is the case, the manager can expire it upon timeout. + * + * @return True if this session is primary + */ + boolean isPrimarySession(); + + /** + * Sets whether this is the primary session or not. + * + * @param primarySession Flag value + */ + void setPrimarySession(boolean primarySession); + + +} diff --git a/java/org/apache/catalina/ha/ClusterValve.java b/java/org/apache/catalina/ha/ClusterValve.java new file mode 100644 index 0000000..d2c339e --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterValve.java @@ -0,0 +1,41 @@ +/* + * 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.catalina.ha; + +import org.apache.catalina.Valve; + +/** + * Cluster valves are a simple extension to the Tomcat valve architecture with a small addition of being able to + * reference the cluster component in the container it sits in. + * + * @author Peter Rossbach + */ +public interface ClusterValve extends Valve { + /** + * Returns the cluster the cluster deployer is associated with + * + * @return CatalinaCluster + */ + CatalinaCluster getCluster(); + + /** + * Associates the cluster deployer with a cluster + * + * @param cluster CatalinaCluster + */ + void setCluster(CatalinaCluster cluster); +} diff --git a/java/org/apache/catalina/ha/authenticator/ClusterSingleSignOn.java b/java/org/apache/catalina/ha/authenticator/ClusterSingleSignOn.java new file mode 100644 index 0000000..6e8e9ab --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/ClusterSingleSignOn.java @@ -0,0 +1,205 @@ +/* + * 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.catalina.ha.authenticator; + +import java.security.Principal; + +import org.apache.catalina.Container; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Session; +import org.apache.catalina.SessionListener; +import org.apache.catalina.authenticator.SingleSignOn; +import org.apache.catalina.authenticator.SingleSignOnEntry; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.ha.ClusterValve; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.tipis.AbstractReplicatedMap.MapOwner; +import org.apache.catalina.tribes.tipis.ReplicatedMap; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * A Valve that supports a "single sign on" user experience on each nodes of a cluster, where the + * security identity of a user who successfully authenticates to one web application is propagated to other web + * applications and to other nodes cluster in the same security domain. For successful use, the following requirements + * must be met: + *
      + *
    • This Valve must be configured on the Container that represents a virtual host (typically an implementation of + * Host).
    • + *
    • The Realm that contains the shared user and role information must be configured on the same + * Container (or a higher one), and not overridden at the web application level.
    • + *
    • The web applications themselves must use one of the standard Authenticators found in the + * org.apache.catalina.authenticator package.
    • + *
    + * + * @author Fabien Carrion + */ +public class ClusterSingleSignOn extends SingleSignOn implements ClusterValve, MapOwner { + + private static final StringManager sm = StringManager.getManager(ClusterSingleSignOn.class); + + // -------------------------------------------------------------- Properties + + private CatalinaCluster cluster = null; + + @Override + public CatalinaCluster getCluster() { + return cluster; + } + + @Override + public void setCluster(CatalinaCluster cluster) { + this.cluster = cluster; + } + + + private long rpcTimeout = 15000; + + public long getRpcTimeout() { + return rpcTimeout; + } + + public void setRpcTimeout(long rpcTimeout) { + this.rpcTimeout = rpcTimeout; + } + + + private int mapSendOptions = Channel.SEND_OPTIONS_SYNCHRONIZED_ACK | Channel.SEND_OPTIONS_USE_ACK; + + public int getMapSendOptions() { + return mapSendOptions; + } + + public void setMapSendOptions(int mapSendOptions) { + this.mapSendOptions = mapSendOptions; + } + + + private boolean terminateOnStartFailure = false; + + public boolean getTerminateOnStartFailure() { + return terminateOnStartFailure; + } + + public void setTerminateOnStartFailure(boolean terminateOnStartFailure) { + this.terminateOnStartFailure = terminateOnStartFailure; + } + + private long accessTimeout = 5000; + + public long getAccessTimeout() { + return accessTimeout; + } + + public void setAccessTimeout(long accessTimeout) { + this.accessTimeout = accessTimeout; + } + + // ---------------------------------------------------- SingleSignOn Methods + + @Override + protected boolean associate(String ssoId, Session session) { + boolean result = super.associate(ssoId, session); + if (result) { + ((ReplicatedMap) cache).replicate(ssoId, true); + } + return result; + } + + @Override + protected boolean update(String ssoId, Principal principal, String authType, String username, String password) { + boolean result = super.update(ssoId, principal, authType, username, password); + if (result) { + ((ReplicatedMap) cache).replicate(ssoId, true); + } + return result; + } + + @Override + protected SessionListener getSessionListener(String ssoId) { + return new ClusterSingleSignOnListener(ssoId); + } + + + // -------------------------------------------------------- MapOwner Methods + + @Override + public void objectMadePrimary(Object key, Object value) { + // NO-OP + } + + + // ------------------------------------------------------- Lifecycle Methods + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected synchronized void startInternal() throws LifecycleException { + + // Load the cluster component, if any + try { + if (cluster == null) { + Container host = getContainer(); + if (host instanceof Host) { + if (host.getCluster() instanceof CatalinaCluster) { + setCluster((CatalinaCluster) host.getCluster()); + } + } + } + if (cluster == null) { + throw new LifecycleException(sm.getString("clusterSingleSignOn.nocluster")); + } + + ClassLoader[] cls = new ClassLoader[] { this.getClass().getClassLoader() }; + + ReplicatedMap cache = new ReplicatedMap<>(this, cluster.getChannel(), rpcTimeout, + cluster.getClusterName() + "-SSO-cache", cls, terminateOnStartFailure); + cache.setChannelSendOptions(mapSendOptions); + cache.setAccessTimeout(accessTimeout); + this.cache = cache; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + throw new LifecycleException(sm.getString("clusterSingleSignOn.clusterLoad.fail"), t); + } + + super.startInternal(); + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected synchronized void stopInternal() throws LifecycleException { + + super.stopInternal(); + + if (getCluster() != null) { + ((ReplicatedMap) cache).breakdown(); + } + } +} diff --git a/java/org/apache/catalina/ha/authenticator/ClusterSingleSignOnListener.java b/java/org/apache/catalina/ha/authenticator/ClusterSingleSignOnListener.java new file mode 100644 index 0000000..eb9e0e6 --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/ClusterSingleSignOnListener.java @@ -0,0 +1,34 @@ +/* + * 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.catalina.ha.authenticator; + +import org.apache.catalina.authenticator.SingleSignOnListener; +import org.apache.catalina.ha.session.ReplicatedSessionListener; + +/** + * Cluster extension of {@link SingleSignOnListener} that simply adds the marker interface + * {@link ReplicatedSessionListener} which allows the listener to be replicated across the cluster along with the + * session. + */ +public class ClusterSingleSignOnListener extends SingleSignOnListener implements ReplicatedSessionListener { + + private static final long serialVersionUID = 1L; + + public ClusterSingleSignOnListener(String ssoId) { + super(ssoId); + } +} diff --git a/java/org/apache/catalina/ha/authenticator/LocalStrings.properties b/java/org/apache/catalina/ha/authenticator/LocalStrings.properties new file mode 100644 index 0000000..7910f0c --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/LocalStrings.properties @@ -0,0 +1,17 @@ +# 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. + +clusterSingleSignOn.clusterLoad.fail=ClusterSingleSignOn exception during clusterLoad +clusterSingleSignOn.nocluster=There is no Cluster for ClusterSingleSignOn diff --git a/java/org/apache/catalina/ha/authenticator/LocalStrings_cs.properties b/java/org/apache/catalina/ha/authenticator/LocalStrings_cs.properties new file mode 100644 index 0000000..883cab9 --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/LocalStrings_cs.properties @@ -0,0 +1,16 @@ +# 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. + +clusterSingleSignOn.clusterLoad.fail=Výjimka ClusterSingleSignOn bÄ›hem clusterLoad diff --git a/java/org/apache/catalina/ha/authenticator/LocalStrings_de.properties b/java/org/apache/catalina/ha/authenticator/LocalStrings_de.properties new file mode 100644 index 0000000..482dc1e --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +clusterSingleSignOn.clusterLoad.fail=ClusterSingleSignOn Fehler bei clusterLoad diff --git a/java/org/apache/catalina/ha/authenticator/LocalStrings_es.properties b/java/org/apache/catalina/ha/authenticator/LocalStrings_es.properties new file mode 100644 index 0000000..1fd1420 --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/LocalStrings_es.properties @@ -0,0 +1,16 @@ +# 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. + +clusterSingleSignOn.clusterLoad.fail=ClusterSingleSignOn excepción durante clusterLoad diff --git a/java/org/apache/catalina/ha/authenticator/LocalStrings_fr.properties b/java/org/apache/catalina/ha/authenticator/LocalStrings_fr.properties new file mode 100644 index 0000000..900300e --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/LocalStrings_fr.properties @@ -0,0 +1,17 @@ +# 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. + +clusterSingleSignOn.clusterLoad.fail=Exception ClusterSingleSignOn pendant clusterLoad +clusterSingleSignOn.nocluster=Il n'y a pas de cluster pour ClusterSingleSignOn diff --git a/java/org/apache/catalina/ha/authenticator/LocalStrings_ja.properties b/java/org/apache/catalina/ha/authenticator/LocalStrings_ja.properties new file mode 100644 index 0000000..764d048 --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/LocalStrings_ja.properties @@ -0,0 +1,17 @@ +# 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. + +clusterSingleSignOn.clusterLoad.fail=ClusterSingleSignOnã§ã‚¯ãƒ©ã‚¹ã‚¿ãƒ­ãƒ¼ãƒ‰ä¸­ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—㟠+clusterSingleSignOn.nocluster=ClusterSingleSignOnã«é–¢é€£ã™ã‚‹ã‚¯ãƒ©ã‚¹ã‚¿ãŒã‚ã‚Šã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/ha/authenticator/LocalStrings_ko.properties b/java/org/apache/catalina/ha/authenticator/LocalStrings_ko.properties new file mode 100644 index 0000000..b1bcc4c --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/LocalStrings_ko.properties @@ -0,0 +1,17 @@ +# 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. + +clusterSingleSignOn.clusterLoad.fail=ClusterSingleSignOnì—ì„œ clusterLoad 오í¼ë ˆì´ì…˜ 수행 중 예외 ë°œìƒ +clusterSingleSignOn.nocluster=ClusterSingleSignOnì„ ìœ„í•œ í´ëŸ¬ìŠ¤í„°ê°€ 없습니다. diff --git a/java/org/apache/catalina/ha/authenticator/LocalStrings_zh_CN.properties b/java/org/apache/catalina/ha/authenticator/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..9090f91 --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/LocalStrings_zh_CN.properties @@ -0,0 +1,17 @@ +# 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. + +clusterSingleSignOn.clusterLoad.fail=在集群加载时, 集群å•ç‚¹ç™»å½•å¼‚常 +clusterSingleSignOn.nocluster=没有ClusterSingleSignOn的集群。 diff --git a/java/org/apache/catalina/ha/authenticator/mbeans-descriptors.xml b/java/org/apache/catalina/ha/authenticator/mbeans-descriptors.xml new file mode 100644 index 0000000..d9034b5 --- /dev/null +++ b/java/org/apache/catalina/ha/authenticator/mbeans-descriptors.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/ha/backend/CollectedInfo.java b/java/org/apache/catalina/ha/backend/CollectedInfo.java new file mode 100644 index 0000000..ab24367 --- /dev/null +++ b/java/org/apache/catalina/ha/backend/CollectedInfo.java @@ -0,0 +1,119 @@ +/* + * 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.catalina.ha.backend; + +/* for MBean to read ready and busy */ + +import java.util.Set; + +import javax.management.MBeanServer; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/* + * Listener to provider informations to mod_heartbeat.c + * *msg_format = "v=%u&ready=%u&busy=%u"; (message to send). + * send the multicast message using the format... + * what about the bind(IP. port) only IP makes sense (for the moment). + * BTW:v = version :-) + */ +public class CollectedInfo { + + private static final StringManager sm = StringManager.getManager(CollectedInfo.class); + + /* Collect info via JMX */ + protected MBeanServer mBeanServer = null; + protected ObjectName objName = null; + + int ready; + int busy; + + int port = 0; + String host = null; + + public CollectedInfo(String host, int port) throws Exception { + init(host, port); + } + + public void init(String host, int port) throws Exception { + int iport = 0; + String shost = null; + mBeanServer = Registry.getRegistry(null, null).getMBeanServer(); + String onStr = "*:type=ThreadPool,*"; + ObjectName objectName = new ObjectName(onStr); + Set set = mBeanServer.queryMBeans(objectName, null); + for (ObjectInstance oi : set) { + objName = oi.getObjectName(); + String subtype = objName.getKeyProperty("subType"); + if (subtype != null && subtype.equals("SocketProperties")) { + objName = null; + continue; + } + String name = objName.getKeyProperty("name"); + name = name.replace("\"", ""); + + // Example names: + // ajp-nio-8009 + // ajp-nio-127.0.0.1-8009 + // ajp-nio-0:0:0:0:0:0:0:1-8009 + // ajp-nio-10.36.116.209-8009 + String[] elenames = name.split("-"); + String sport = elenames[elenames.length - 1]; + iport = Integer.parseInt(sport); + if (elenames.length == 4) { + shost = elenames[2]; + } + + if (port == 0 && host == null) { + break; /* Done: take the first one */ + } + if (iport == port) { + if (host == null) { + break; /* Done: return the first with the right port */ + } else if (shost != null && shost.compareTo(host) == 0) { + break; /* Done port and host are the expected ones */ + } + } + objName = null; + shost = null; + } + if (objName == null) { + throw new Exception(sm.getString("collectedInfo.noConnector", host, Integer.valueOf(port))); + } + this.port = iport; + this.host = shost; + + } + + public void refresh() throws Exception { + if (mBeanServer == null || objName == null) { + throw new Exception(sm.getString("collectedInfo.notInitialized")); + } + Integer imax = (Integer) mBeanServer.getAttribute(objName, "maxThreads"); + + // the currentThreadCount could be 0 before the threads are created... + // Integer iready = (Integer) mBeanServer.getAttribute(objName, "currentThreadCount"); + + Integer ibusy = (Integer) mBeanServer.getAttribute(objName, "currentThreadsBusy"); + + busy = ibusy.intValue(); + ready = imax.intValue() - ibusy.intValue(); + } +} diff --git a/java/org/apache/catalina/ha/backend/HeartbeatListener.java b/java/org/apache/catalina/ha/backend/HeartbeatListener.java new file mode 100644 index 0000000..117b1fa --- /dev/null +++ b/java/org/apache/catalina/ha/backend/HeartbeatListener.java @@ -0,0 +1,235 @@ +/* + * 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.catalina.ha.backend; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/* + * Listener to provider informations to mod_heartbeat.c + * *msg_format = "v=%u&ready=%u&busy=%u"; (message to send). + * send the multicast message using the format... + * what about the bind(IP. port) only IP makes sense (for the moment). + * BTW:v = version :-) + */ +public class HeartbeatListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(HeartbeatListener.class); + private static final StringManager sm = StringManager.getManager(HeartbeatListener.class); + + /* To allow to select the connector */ + protected int port = 8009; + protected String host = null; + + /** + * @return the host corresponding to the connector we want to proxy. + */ + public String getHost() { + return this.host; + } + + /** + * Set the host corresponding to the connector. + * + * @param host the hostname or ip string. + */ + public void setHost(String host) { + this.host = host; + } + + /** + * @return the port of the connector we want to proxy. + */ + public int getPort() { + return this.port; + } + + /** + * Set the port corresponding to the connector. + * + * @param port default 8009 the ajp one. + */ + public void setPort(int port) { + this.port = port; + } + + /* for multicasting stuff */ + protected String ip = "224.0.1.105"; /* Multicast IP */ + protected int multiport = 23364; /* Multicast Port */ + protected int ttl = 16; + + /* corresponding setters and getters */ + + /** + * @return the Multicast IP we are using for Multicast + */ + public String getGroup() { + return ip; + } + + /** + * Set the Multicast IP to use for Multicast + * + * @param group the multi address to use. + */ + public void setGroup(String group) { + this.ip = group; + } + + /** + * @return the Multicast Port we are using for Multicast. + */ + public int getMultiport() { + return multiport; + } + + /** + * Set the Port to use for Multicast + * + * @param port the port to use. + */ + public void setMultiport(int port) { + this.multiport = port; + } + + /** + * @return the TTL for Multicast packets. + */ + public int getTtl() { + return ttl; + } + + /** + * Set the TTL for Multicast packets. + * + * @param ttl value for TTL. + */ + public void setTtl(int ttl) { + this.ttl = ttl; + } + + /** + * Proxy list, format "address:port,address:port". + */ + protected String proxyList = null; + + /** + * @return the list of proxies that send us requests. + */ + public String getProxyList() { + return proxyList; + } + + /** + * Set the list of Proxies that send is requests, when not empty it toggles the multi to off. A SetHandler heartbeat + * must be existing in httpd.conf. + * + * @param proxyList the list of proxy, format "address:port,address:port". + */ + public void setProxyList(String proxyList) { + this.proxyList = proxyList; + } + + /** + * URL prefix. + */ + protected String proxyURL = "/HeartbeatListener"; + + /** + * @return the URL specified in <Location/> for the SetHandler heartbeat. + */ + public String getProxyURL() { + return proxyURL; + } + + /** + * Set the URL of receiver in httpd. That is the location used in + * + *
    +     * <Location "/HeartbeatListener">
    +     *    SetHandler heartbeat
    +     * </Location>
    +     * 
    + * + * All proxies MUST use the same location. + * + * @param proxyURL a String with the URL starting with / + */ + public void setProxyURLString(String proxyURL) { + this.proxyURL = proxyURL; + } + + private CollectedInfo coll = null; + + private Sender sender = null; + + @Override + public void lifecycleEvent(LifecycleEvent event) { + + if (Lifecycle.PERIODIC_EVENT.equals(event.getType())) { + if (sender == null) { + if (proxyList == null) { + sender = new MultiCastSender(); + } else { + sender = new TcpSender(); + } + } + + /* Read busy and ready */ + if (coll == null) { + try { + coll = new CollectedInfo(host, port); + this.port = coll.port; + this.host = coll.host; + } catch (Exception ex) { + log.error(sm.getString("heartbeatListener.errorCollectingInfo"), ex); + coll = null; + return; + } + } + + /* Start or restart sender */ + try { + sender.init(this); + } catch (Exception ex) { + log.error(sm.getString("heartbeatListener.senderInitError"), ex); + sender = null; + return; + } + + /* refresh the connector information and send it */ + try { + coll.refresh(); + } catch (Exception ex) { + log.error(sm.getString("heartbeatListener.refreshError"), ex); + coll = null; + return; + } + String output = "v=1&ready=" + coll.ready + "&busy=" + coll.busy + "&port=" + port; + try { + sender.send(output); + } catch (Exception ex) { + log.error(sm.getString("heartbeatListener.sendError"), ex); + } + } + } + +} diff --git a/java/org/apache/catalina/ha/backend/LocalStrings.properties b/java/org/apache/catalina/ha/backend/LocalStrings.properties new file mode 100644 index 0000000..3bba325 --- /dev/null +++ b/java/org/apache/catalina/ha/backend/LocalStrings.properties @@ -0,0 +1,33 @@ +# 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. + +collectedInfo.noConnector=Cannot find connector for [{0}]:[{1}] +collectedInfo.notInitialized=Not initialized + +heartbeatListener.errorCollectingInfo=Unable to initialize info collection +heartbeatListener.refreshError=Unable to collect load information +heartbeatListener.sendError=Unable to send collected load information +heartbeatListener.senderInitError=Unable to initialize Sender + +multiCastSender.multiCastFailed=Unable to use multicast +multiCastSender.sendFailed=Unable to send multicast message + +tcpSender.connectionFailed=Unable to connect to proxy +tcpSender.invalidProxyList=Invalid proxy list +tcpSender.notInitialized=Not initialized +tcpSender.readError=Error reading response content +tcpSender.responseError=Unable to read response from proxy +tcpSender.responseErrorCode=Response error with code [{0}] +tcpSender.sendFailed=Unable to send collected load information to proxy diff --git a/java/org/apache/catalina/ha/backend/LocalStrings_de.properties b/java/org/apache/catalina/ha/backend/LocalStrings_de.properties new file mode 100644 index 0000000..8806d96 --- /dev/null +++ b/java/org/apache/catalina/ha/backend/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +multiCastSender.sendFailed=Kann Multitcast Nachricht nicht senden diff --git a/java/org/apache/catalina/ha/backend/LocalStrings_fr.properties b/java/org/apache/catalina/ha/backend/LocalStrings_fr.properties new file mode 100644 index 0000000..103245e --- /dev/null +++ b/java/org/apache/catalina/ha/backend/LocalStrings_fr.properties @@ -0,0 +1,33 @@ +# 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. + +collectedInfo.noConnector=Impossible de trouver le connecteur pour [{0}] : [{1}] +collectedInfo.notInitialized=Non initialisé + +heartbeatListener.errorCollectingInfo=Impossible d’initialiser la collection d'informations de charge +heartbeatListener.refreshError=Impossible de collecter les informations sur la charge +heartbeatListener.sendError=Impossible d'envoyer les informations de charge collectées +heartbeatListener.senderInitError=Impossible d'initialiser l'expéditeur + +multiCastSender.multiCastFailed=Impossible d'utiliser le multicast +multiCastSender.sendFailed=Impossible d'envoyer le message multicast + +tcpSender.connectionFailed=Impossible de se connecter au proxy +tcpSender.invalidProxyList=Liste de proxies invalide +tcpSender.notInitialized=Non initialisé +tcpSender.readError=Erreur en lisant le contenu de la réponse +tcpSender.responseError=Impossible de lire la réponse du proxy +tcpSender.responseErrorCode=Erreur de réponse avec le code [{0}] +tcpSender.sendFailed=Impossible d'envoyer les informations de charge collectées au proxy diff --git a/java/org/apache/catalina/ha/backend/LocalStrings_ja.properties b/java/org/apache/catalina/ha/backend/LocalStrings_ja.properties new file mode 100644 index 0000000..c652d52 --- /dev/null +++ b/java/org/apache/catalina/ha/backend/LocalStrings_ja.properties @@ -0,0 +1,33 @@ +# 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. + +collectedInfo.noConnector=[{0}]:[{1}]ã®ã‚³ãƒã‚¯ã‚¿ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +collectedInfo.notInitialized=åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã›ã‚“ + +heartbeatListener.errorCollectingInfo=情報åŽé›†ã‚’åˆæœŸåŒ–ã§ãã¾ã›ã‚“。 +heartbeatListener.refreshError=è² è·æƒ…報をåŽé›†ã§ãã¾ã›ã‚“ +heartbeatListener.sendError=åŽé›†ã•ã‚ŒãŸè² è·æƒ…報をé€ä¿¡ã§ãã¾ã›ã‚“。 +heartbeatListener.senderInitError=Sender ã‚’åˆæœŸåŒ–ã§ãã¾ã›ã‚“。 + +multiCastSender.multiCastFailed=マルãƒã‚­ãƒ£ã‚¹ãƒˆã‚’利用ã§ãã¾ã›ã‚“。 +multiCastSender.sendFailed=マルãƒã‚­ãƒ£ã‚¹ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 + +tcpSender.connectionFailed=プロキシサーãƒãƒ¼ã«æŽ¥ç¶šã§ãã¾ã›ã‚“。 +tcpSender.invalidProxyList=ä¸æ­£ãªãƒ—ロキシリスト +tcpSender.notInitialized=åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã›ã‚“ +tcpSender.readError=レスãƒãƒ³ã‚¹èª­ã¿å–り中ã®ã‚¨ãƒ©ãƒ¼ +tcpSender.responseError=プロキシサーãƒãƒ¼ã‹ã‚‰ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚’読ã¿å–ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。 +tcpSender.responseErrorCode=コード [{0}] ã«ã‚ˆã‚‹ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚¨ãƒ©ãƒ¼ +tcpSender.sendFailed=åŽé›†ã—ãŸè² è·æƒ…報をプロキシã«é€ä¿¡ã§ãã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/ha/backend/LocalStrings_ko.properties b/java/org/apache/catalina/ha/backend/LocalStrings_ko.properties new file mode 100644 index 0000000..8cf4622 --- /dev/null +++ b/java/org/apache/catalina/ha/backend/LocalStrings_ko.properties @@ -0,0 +1,33 @@ +# 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. + +collectedInfo.noConnector=[{0}]:[{1}]ì„(를) 위한 connector를 ì°¾ì„ ìˆ˜ 없습니다. +collectedInfo.notInitialized=초기화 ì•ˆë¨ + +heartbeatListener.errorCollectingInfo=시스템 정보를 취합할 수 없습니다. +heartbeatListener.refreshError=시스템 부하 정보를 취합할 수 없습니다. +heartbeatListener.sendError=ì·¨í•©ëœ ì‹œìŠ¤í…œ 부하 정보를 보낼 수 없습니다. +heartbeatListener.senderInitError=Sender를 초기화할 수 없습니다. + +multiCastSender.multiCastFailed=멀티ìºìŠ¤íŠ¸ë¥¼ 사용할 수 없습니다. +multiCastSender.sendFailed=멀티ìºìŠ¤íŠ¸ 메시지를 전송할 수 없습니다. + +tcpSender.connectionFailed=프ë¡ì‹œì— ì—°ê²°í•  수 없습니다. +tcpSender.invalidProxyList=유효하지 ì•Šì€ í”„ë¡ì‹œ ëª©ë¡ +tcpSender.notInitialized=초기화ë˜ì§€ ì•Šì•˜ìŒ +tcpSender.readError=ì‘답 컨í…트를 ì½ëŠ” 중 오류 ë°œìƒ +tcpSender.responseError=프ë¡ì‹œë¡œë¶€í„° ì‘ë‹µì„ ì½ì„ 수 없습니다. +tcpSender.responseErrorCode=코드 [{0}]와(ê³¼) 함께 ì‘답 오류 ë°œìƒ +tcpSender.sendFailed=ì·¨í•©ëœ ì‹œìŠ¤í…œ 부하 정보를 프ë¡ì‹œì— 전송할 수 없습니다. diff --git a/java/org/apache/catalina/ha/backend/LocalStrings_zh_CN.properties b/java/org/apache/catalina/ha/backend/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..517f5b3 --- /dev/null +++ b/java/org/apache/catalina/ha/backend/LocalStrings_zh_CN.properties @@ -0,0 +1,33 @@ +# 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. + +collectedInfo.noConnector=找ä¸åˆ°[{0}]的连接器:[{1}] +collectedInfo.notInitialized=未åˆå§‹åŒ– + +heartbeatListener.errorCollectingInfo=无法åˆå§‹åŒ–ä¿¡æ¯æ”¶é›† +heartbeatListener.refreshError=æ— æ³•æ”¶é›†åŠ è½½ä¿¡æ¯ +heartbeatListener.sendError=无法å‘é€æ”¶é›†çš„åŠ è½½ä¿¡æ¯ +heartbeatListener.senderInitError=无法åˆå§‹åŒ–å‘件人 + +multiCastSender.multiCastFailed=无法使用多播 +multiCastSender.sendFailed=无法å‘é€å¤šæ’­ä¿¡æ¯ + +tcpSender.connectionFailed=æ— æ³•è¿žæŽ¥åˆ°ä»£ç† +tcpSender.invalidProxyList=无效的代ç†åˆ—表 +tcpSender.notInitialized=未åˆå§‹åŒ– +tcpSender.readError=读å–å“应内容时出错 +tcpSender.responseError=无法从代ç†è¯»å–å“应 +tcpSender.responseErrorCode=代ç ä¸º[{0}]çš„å“应错误 +tcpSender.sendFailed=无法将收集的加载信æ¯å‘é€åˆ°ä»£ç† diff --git a/java/org/apache/catalina/ha/backend/MultiCastSender.java b/java/org/apache/catalina/ha/backend/MultiCastSender.java new file mode 100644 index 0000000..424ed20 --- /dev/null +++ b/java/org/apache/catalina/ha/backend/MultiCastSender.java @@ -0,0 +1,84 @@ +/* + * 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.catalina.ha.backend; + +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.nio.charset.StandardCharsets; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/* + * Sender to proxies using multicast socket. + */ +public class MultiCastSender implements Sender { + + private static final Log log = LogFactory.getLog(HeartbeatListener.class); + private static final StringManager sm = StringManager.getManager(MultiCastSender.class); + + HeartbeatListener config = null; + + /* for multicasting stuff */ + MulticastSocket s = null; + InetAddress group = null; + + @Override + public void init(HeartbeatListener config) throws Exception { + this.config = config; + } + + @Override + public int send(String mess) throws Exception { + if (s == null) { + try { + group = InetAddress.getByName(config.getGroup()); + if (config.getHost() != null) { + InetAddress addr = InetAddress.getByName(config.getHost()); + InetSocketAddress addrs = new InetSocketAddress(addr, config.getMultiport()); + s = new MulticastSocket(addrs); + } else { + s = new MulticastSocket(config.getMultiport()); + } + + s.setTimeToLive(config.getTtl()); + s.joinGroup(new InetSocketAddress(group, 0), null); + } catch (Exception ex) { + log.error(sm.getString("multiCastSender.multiCastFailed"), ex); + s = null; + return -1; + } + } + + byte[] buf; + buf = mess.getBytes(StandardCharsets.US_ASCII); + DatagramPacket data = new DatagramPacket(buf, buf.length, group, config.getMultiport()); + try { + s.send(data); + } catch (Exception ex) { + log.error(sm.getString("multiCastSender.sendFailed"), ex); + s.close(); + s = null; + return -1; + } + return 0; + } + +} diff --git a/java/org/apache/catalina/ha/backend/Proxy.java b/java/org/apache/catalina/ha/backend/Proxy.java new file mode 100644 index 0000000..3acc811 --- /dev/null +++ b/java/org/apache/catalina/ha/backend/Proxy.java @@ -0,0 +1,29 @@ +/* + * 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.catalina.ha.backend; + +import java.net.InetAddress; + +/* + * This class represents a front-end httpd server. + * + */ +public class Proxy { + + public InetAddress address = null; + public int port = 80; +} diff --git a/java/org/apache/catalina/ha/backend/Sender.java b/java/org/apache/catalina/ha/backend/Sender.java new file mode 100644 index 0000000..b85a30a --- /dev/null +++ b/java/org/apache/catalina/ha/backend/Sender.java @@ -0,0 +1,43 @@ +/* + * 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.catalina.ha.backend; + +/** + * Interface to send data to proxies. + */ +public interface Sender { + + /** + * Set the configuration parameters + * + * @param config The heartbeat listener configuration + * + * @throws Exception An error occurred + */ + void init(HeartbeatListener config) throws Exception; + + /** + * Send the message to the proxies + * + * @param mess The message that will be sent + * + * @return 0 if no error occurred, -1 otherwise + * + * @throws Exception An error occurred + */ + int send(String mess) throws Exception; +} diff --git a/java/org/apache/catalina/ha/backend/TcpSender.java b/java/org/apache/catalina/ha/backend/TcpSender.java new file mode 100644 index 0000000..345224d --- /dev/null +++ b/java/org/apache/catalina/ha/backend/TcpSender.java @@ -0,0 +1,213 @@ +/* + * 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.catalina.ha.backend; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.StringTokenizer; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/* + * Sender to proxies using multicast socket. + */ +public class TcpSender implements Sender { + + private static final Log log = LogFactory.getLog(HeartbeatListener.class); + private static final StringManager sm = StringManager.getManager(TcpSender.class); + + HeartbeatListener config = null; + + /** + * Proxies. + */ + protected Proxy[] proxies = null; + + + /** + * Active connections. + */ + + protected Socket[] connections = null; + protected BufferedReader[] connectionReaders = null; + protected BufferedWriter[] connectionWriters = null; + + + @Override + public void init(HeartbeatListener config) throws Exception { + this.config = config; + StringTokenizer tok = new StringTokenizer(config.getProxyList(), ","); + proxies = new Proxy[tok.countTokens()]; + int i = 0; + while (tok.hasMoreTokens()) { + String token = tok.nextToken().trim(); + int pos = token.indexOf(':'); + if (pos <= 0) { + throw new Exception(sm.getString("tcpSender.invalidProxyList")); + } + proxies[i] = new Proxy(); + proxies[i].port = Integer.parseInt(token.substring(pos + 1)); + try { + proxies[i].address = InetAddress.getByName(token.substring(0, pos)); + } catch (Exception e) { + throw new Exception(sm.getString("tcpSender.invalidProxyList")); + } + i++; + } + connections = new Socket[proxies.length]; + connectionReaders = new BufferedReader[proxies.length]; + connectionWriters = new BufferedWriter[proxies.length]; + + } + + @Override + public int send(String mess) throws Exception { + if (connections == null) { + log.error(sm.getString("tcpSender.notInitialized")); + return -1; + } + String requestLine = "POST " + config.getProxyURL() + " HTTP/1.0"; + + for (int i = 0; i < connections.length; i++) { + if (connections[i] == null) { + try { + if (config.getHost() != null) { + connections[i] = new Socket(); + InetAddress addr = InetAddress.getByName(config.getHost()); + InetSocketAddress addrs = new InetSocketAddress(addr, 0); + connections[i].setReuseAddress(true); + connections[i].bind(addrs); + addrs = new InetSocketAddress(proxies[i].address, proxies[i].port); + connections[i].connect(addrs); + } else { + connections[i] = new Socket(proxies[i].address, proxies[i].port); + } + connectionReaders[i] = new BufferedReader(new InputStreamReader(connections[i].getInputStream())); + connectionWriters[i] = new BufferedWriter(new OutputStreamWriter(connections[i].getOutputStream())); + } catch (Exception ex) { + log.error(sm.getString("tcpSender.connectionFailed"), ex); + close(i); + } + } + if (connections[i] == null) { + continue; // try next proxy in the list + } + BufferedWriter writer = connectionWriters[i]; + try { + writer.write(requestLine); + writer.write("\r\n"); + writer.write("Content-Length: " + mess.length() + "\r\n"); + writer.write("User-Agent: HeartbeatListener/1.0\r\n"); + writer.write("Connection: Keep-Alive\r\n"); + writer.write("\r\n"); + writer.write(mess); + writer.write("\r\n"); + writer.flush(); + } catch (Exception ex) { + log.error(sm.getString("tcpSender.sendFailed"), ex); + close(i); + } + if (connections[i] == null) { + continue; // try next proxy in the list + } + + /* Read httpd answer */ + String responseStatus = connectionReaders[i].readLine(); + if (responseStatus == null) { + log.error(sm.getString("tcpSender.responseError")); + close(i); + continue; + } else { + responseStatus = responseStatus.substring(responseStatus.indexOf(' ') + 1, + responseStatus.indexOf(' ', responseStatus.indexOf(' ') + 1)); + int status = Integer.parseInt(responseStatus); + if (status != 200) { + log.error(sm.getString("tcpSender.responseErrorCode", Integer.valueOf(status))); + close(i); + continue; + } + + // read all the headers. + String header = connectionReaders[i].readLine(); + int contentLength = 0; + while (header != null && !header.isEmpty()) { + int colon = header.indexOf(':'); + String headerName = header.substring(0, colon).trim(); + String headerValue = header.substring(colon + 1).trim(); + if ("content-length".equalsIgnoreCase(headerName)) { + contentLength = Integer.parseInt(headerValue); + } + header = connectionReaders[i].readLine(); + } + if (contentLength > 0) { + char[] buf = new char[512]; + while (contentLength > 0) { + int thisTime = (contentLength > buf.length) ? buf.length : contentLength; + int n = connectionReaders[i].read(buf, 0, thisTime); + if (n <= 0) { + log.error(sm.getString("tcpSender.readError")); + close(i); + break; + } else { + contentLength -= n; + } + } + } + } + + } + + return 0; + } + + /** + * Close connection. + * + * @param i The index of the connection that will be closed + */ + protected void close(int i) { + try { + if (connectionReaders[i] != null) { + connectionReaders[i].close(); + } + } catch (IOException e) { + } + connectionReaders[i] = null; + try { + if (connectionWriters[i] != null) { + connectionWriters[i].close(); + } + } catch (IOException e) { + } + connectionWriters[i] = null; + try { + if (connections[i] != null) { + connections[i].close(); + } + } catch (IOException e) { + } + connections[i] = null; + } +} diff --git a/java/org/apache/catalina/ha/context/LocalStrings.properties b/java/org/apache/catalina/ha/context/LocalStrings.properties new file mode 100644 index 0000000..7b11133 --- /dev/null +++ b/java/org/apache/catalina/ha/context/LocalStrings.properties @@ -0,0 +1,19 @@ +# 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. + +applicationContext.setAttribute.namenull=Name cannot be null + +replicatedContext.startFailed=Failed to start ReplicatedContext: [{0}] +replicatedContext.startUnable=Unable to start ReplicatedContext: [{0}] diff --git a/java/org/apache/catalina/ha/context/LocalStrings_fr.properties b/java/org/apache/catalina/ha/context/LocalStrings_fr.properties new file mode 100644 index 0000000..5eb4245 --- /dev/null +++ b/java/org/apache/catalina/ha/context/LocalStrings_fr.properties @@ -0,0 +1,19 @@ +# 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. + +applicationContext.setAttribute.namenull=Le nom ne peut être null + +replicatedContext.startFailed=Echec de démarrage du ReplicatedContext : [{0}] +replicatedContext.startUnable=Impossible de démarrer le ReplicatedContext : [{0}] diff --git a/java/org/apache/catalina/ha/context/LocalStrings_ja.properties b/java/org/apache/catalina/ha/context/LocalStrings_ja.properties new file mode 100644 index 0000000..2496158 --- /dev/null +++ b/java/org/apache/catalina/ha/context/LocalStrings_ja.properties @@ -0,0 +1,19 @@ +# 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. + +applicationContext.setAttribute.namenull=åå‰ã¯nullã«ã§ãã¾ã›ã‚“ + +replicatedContext.startFailed=ReplicatedContext: [{0}]ã®èµ·å‹•ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +replicatedContext.startUnable=ReplicatedContext:[{0}]を開始ã§ãã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/ha/context/LocalStrings_ko.properties b/java/org/apache/catalina/ha/context/LocalStrings_ko.properties new file mode 100644 index 0000000..48f99ef --- /dev/null +++ b/java/org/apache/catalina/ha/context/LocalStrings_ko.properties @@ -0,0 +1,19 @@ +# 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. + +applicationContext.setAttribute.namenull=ì´ë¦„ì´ ë„ì¼ ìˆ˜ 없습니다. + +replicatedContext.startFailed=ReplicatedContext를 시작하지 못했습니다: [{0}] +replicatedContext.startUnable=ReplicatedContext [{0}]ì„(를) 시작할 수 없습니다. diff --git a/java/org/apache/catalina/ha/context/LocalStrings_zh_CN.properties b/java/org/apache/catalina/ha/context/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..5c49312 --- /dev/null +++ b/java/org/apache/catalina/ha/context/LocalStrings_zh_CN.properties @@ -0,0 +1,19 @@ +# 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. + +applicationContext.setAttribute.namenull=å称ä¸èƒ½ä¸ºç©º + +replicatedContext.startFailed=å¯åŠ¨ReplicatedContext失败:[{0}] +replicatedContext.startUnable=无法å¯åŠ¨ReplicatedContext:[{0}] diff --git a/java/org/apache/catalina/ha/context/ReplicatedContext.java b/java/org/apache/catalina/ha/context/ReplicatedContext.java new file mode 100644 index 0000000..fbb0670 --- /dev/null +++ b/java/org/apache/catalina/ha/context/ReplicatedContext.java @@ -0,0 +1,233 @@ +/* + * 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.catalina.ha.context; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.ServletContext; + +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Loader; +import org.apache.catalina.core.ApplicationContext; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.tipis.AbstractReplicatedMap.MapOwner; +import org.apache.catalina.tribes.tipis.ReplicatedMap; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class ReplicatedContext extends StandardContext implements MapOwner { + private int mapSendOptions = Channel.SEND_OPTIONS_DEFAULT; + private static final Log log = LogFactory.getLog(ReplicatedContext.class); + protected static final long DEFAULT_REPL_TIMEOUT = 15000;// 15 seconds + private static final StringManager sm = StringManager.getManager(ReplicatedContext.class); + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + super.startInternal(); + try { + CatalinaCluster catclust = (CatalinaCluster) this.getCluster(); + if (catclust != null) { + ReplicatedMap map = new ReplicatedMap<>(this, catclust.getChannel(), + DEFAULT_REPL_TIMEOUT, getName(), getClassLoaders()); + map.setChannelSendOptions(mapSendOptions); + ((ReplApplContext) this.context).setAttributeMap(map); + } + } catch (Exception x) { + log.error(sm.getString("replicatedContext.startUnable", getName()), x); + throw new LifecycleException(sm.getString("replicatedContext.startFailed", getName()), x); + } + } + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + Map map = ((ReplApplContext) this.context).getAttributeMap(); + + super.stopInternal(); + + if (map instanceof ReplicatedMap) { + ((ReplicatedMap) map).breakdown(); + } + + } + + + public void setMapSendOptions(int mapSendOptions) { + this.mapSendOptions = mapSendOptions; + } + + public int getMapSendOptions() { + return mapSendOptions; + } + + public ClassLoader[] getClassLoaders() { + Loader loader = null; + ClassLoader classLoader = null; + loader = this.getLoader(); + if (loader != null) { + classLoader = loader.getClassLoader(); + } + Thread currentThread = Thread.currentThread(); + if (classLoader == null) { + classLoader = currentThread.getContextClassLoader(); + } + if (classLoader == currentThread.getContextClassLoader()) { + return new ClassLoader[] { classLoader }; + } else { + return new ClassLoader[] { classLoader, currentThread.getContextClassLoader() }; + } + } + + @Override + public ServletContext getServletContext() { + if (context == null) { + context = new ReplApplContext(this); + if (getAltDDName() != null) { + context.setAttribute(Globals.ALT_DD_ATTR, getAltDDName()); + } + } + + return ((ReplApplContext) context).getFacade(); + + } + + + protected static class ReplApplContext extends ApplicationContext { + protected final Map tomcatAttributes = new ConcurrentHashMap<>(); + + public ReplApplContext(ReplicatedContext context) { + super(context); + } + + protected ReplicatedContext getParent() { + return (ReplicatedContext) getContext(); + } + + @Override + protected ServletContext getFacade() { + return super.getFacade(); + } + + public Map getAttributeMap() { + return this.attributes; + } + + public void setAttributeMap(Map map) { + this.attributes = map; + } + + @Override + public void removeAttribute(String name) { + tomcatAttributes.remove(name); + // do nothing + super.removeAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + if (name == null) { + throw new IllegalArgumentException(sm.getString("applicationContext.setAttribute.namenull")); + } + if (value == null) { + removeAttribute(name); + return; + } + if ((!getParent().getState().isAvailable()) || + "org.apache.jasper.runtime.JspApplicationContextImpl".equals(name)) { + tomcatAttributes.put(name, value); + } else { + super.setAttribute(name, value); + } + } + + @Override + public Object getAttribute(String name) { + Object obj = tomcatAttributes.get(name); + if (obj == null) { + return super.getAttribute(name); + } else { + return obj; + } + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration getAttributeNames() { + Set names = new HashSet<>(attributes.keySet()); + + return new MultiEnumeration<>( + new Enumeration[] { super.getAttributeNames(), Collections.enumeration(names) }); + } + } + + protected static class MultiEnumeration implements Enumeration { + private final Enumeration[] enumerations; + + public MultiEnumeration(Enumeration[] enumerations) { + this.enumerations = enumerations; + } + + @Override + public boolean hasMoreElements() { + for (Enumeration enumeration : enumerations) { + if (enumeration.hasMoreElements()) { + return true; + } + } + return false; + } + + @Override + public T nextElement() { + for (Enumeration enumeration : enumerations) { + if (enumeration.hasMoreElements()) { + return enumeration.nextElement(); + } + } + return null; + + } + } + + @Override + public void objectMadePrimary(Object key, Object value) { + // noop + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ha/deploy/FarmWarDeployer.java b/java/org/apache/catalina/ha/deploy/FarmWarDeployer.java new file mode 100644 index 0000000..227fe0a --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/FarmWarDeployer.java @@ -0,0 +1,752 @@ +/* + * 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.catalina.ha.deploy; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.ha.ClusterDeployer; +import org.apache.catalina.ha.ClusterListener; +import org.apache.catalina.ha.ClusterMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.util.ContextName; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + + +/** + *

    + * A farm war deployer is a class that is able to deploy/undeploy web applications in WAR from within the cluster. + *

    + * Any host can act as the admin, and will have three directories + *
      + *
    • watchDir - the directory where we watch for changes
    • + *
    • deployDir - the directory where we install applications
    • + *
    • tempDir - a temporaryDirectory to store binary data when downloading a war from the cluster
    • + *
    + * Currently we only support deployment of WAR files since they are easier to send across the wire. + * + * @author Peter Rossbach + */ +public class FarmWarDeployer extends ClusterListener implements ClusterDeployer, FileChangeListener { + /*--Static Variables----------------------------------------*/ + private static final Log log = LogFactory.getLog(FarmWarDeployer.class); + private static final StringManager sm = StringManager.getManager(FarmWarDeployer.class); + + /*--Instance Variables--------------------------------------*/ + protected boolean started = false; + + protected final HashMap fileFactories = new HashMap<>(); + + /** + * Deployment directory. + */ + protected String deployDir; + private File deployDirFile = null; + + /** + * Temporary directory. + */ + protected String tempDir; + private File tempDirFile = null; + + /** + * Watch directory. + */ + protected String watchDir; + private File watchDirFile = null; + + protected boolean watchEnabled = false; + + protected WarWatcher watcher = null; + + /** + * Iteration count for background processing. + */ + private int count = 0; + + /** + * Frequency of the Farm watchDir check. Cluster wide deployment will be done once for the specified amount of + * backgroundProcess calls (ie, the lower the amount, the most often the checks will occur). + */ + protected int processDeployFrequency = 2; + + /** + * Path where context descriptors should be deployed. + */ + protected File configBase = null; + + /** + * The associated host. + */ + protected Host host = null; + + /** + * MBean server. + */ + protected MBeanServer mBeanServer = null; + + /** + * The associated deployer ObjectName. + */ + protected ObjectName oname = null; + + /** + * The maximum valid time(in seconds) for FileMessageFactory. + */ + protected int maxValidTime = 5 * 60; + + /*--Constructor---------------------------------------------*/ + public FarmWarDeployer() { + } + + /*--Logic---------------------------------------------------*/ + @Override + public void start() throws Exception { + if (started) { + return; + } + Container hcontainer = getCluster().getContainer(); + if (!(hcontainer instanceof Host)) { + log.error(sm.getString("farmWarDeployer.hostOnly")); + return; + } + host = (Host) hcontainer; + + // Check to correct engine and host setup + Container econtainer = host.getParent(); + if (!(econtainer instanceof Engine)) { + log.error(sm.getString("farmWarDeployer.hostParentEngine", host.getName())); + return; + } + Engine engine = (Engine) econtainer; + String hostname = null; + hostname = host.getName(); + try { + oname = new ObjectName(engine.getName() + ":type=Deployer,host=" + hostname); + } catch (Exception e) { + log.error(sm.getString("farmWarDeployer.mbeanNameFail", engine.getName(), hostname), e); + return; + } + if (watchEnabled) { + watcher = new WarWatcher(this, getWatchDirFile()); + if (log.isInfoEnabled()) { + log.info(sm.getString("farmWarDeployer.watchDir", getWatchDir())); + } + } + + configBase = host.getConfigBaseFile(); + + // Retrieve the MBean server + mBeanServer = Registry.getRegistry(null, null).getMBeanServer(); + + started = true; + count = 0; + + getCluster().addClusterListener(this); + + if (log.isInfoEnabled()) { + log.info(sm.getString("farmWarDeployer.started")); + } + } + + /* + * stop cluster wide deployments + * + * @see org.apache.catalina.ha.ClusterDeployer#stop() + */ + @Override + public void stop() throws LifecycleException { + started = false; + getCluster().removeClusterListener(this); + count = 0; + if (watcher != null) { + watcher.clear(); + watcher = null; + + } + if (log.isInfoEnabled()) { + log.info(sm.getString("farmWarDeployer.stopped")); + } + } + + /** + * Callback from the cluster, when a message is received, The cluster will broadcast it invoking the messageReceived + * on the receiver. + * + * @param msg ClusterMessage - the message received from the cluster + */ + @Override + public void messageReceived(ClusterMessage msg) { + try { + if (msg instanceof FileMessage) { + FileMessage fmsg = (FileMessage) msg; + if (log.isTraceEnabled()) { + log.trace(sm.getString("farmWarDeployer.msgRxDeploy", fmsg.getContextName(), fmsg.getFileName())); + } + FileMessageFactory factory = getFactory(fmsg); + // TODO correct second try after app is in service! + if (factory.writeMessage(fmsg)) { + // last message received war file is completed + String name = factory.getFile().getName(); + if (!name.endsWith(".war")) { + name = name + ".war"; + } + File deployable = new File(getDeployDirFile(), name); + try { + String contextName = fmsg.getContextName(); + if (tryAddServiced(contextName)) { + try { + remove(contextName); + if (!factory.getFile().renameTo(deployable)) { + log.error( + sm.getString("farmWarDeployer.renameFail", factory.getFile(), deployable)); + } + } finally { + removeServiced(contextName); + } + check(contextName); + if (log.isTraceEnabled()) { + log.trace(sm.getString("farmWarDeployer.deployEnd", contextName)); + } + } else { + log.error(sm.getString("farmWarDeployer.servicingDeploy", contextName, name)); + } + } catch (Exception ex) { + log.error(sm.getString("farmWarDeployer.fileMessageError"), ex); + } finally { + removeFactory(fmsg); + } + } + } else if (msg instanceof UndeployMessage) { + try { + UndeployMessage umsg = (UndeployMessage) msg; + String contextName = umsg.getContextName(); + if (log.isTraceEnabled()) { + log.trace(sm.getString("farmWarDeployer.msgRxUndeploy", contextName)); + } + if (tryAddServiced(contextName)) { + try { + remove(contextName); + } finally { + removeServiced(contextName); + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("farmWarDeployer.undeployEnd", contextName)); + } + } else { + log.error(sm.getString("farmWarDeployer.servicingUndeploy", contextName)); + } + } catch (Exception ex) { + log.error(sm.getString("farmWarDeployer.undeployMessageError"), ex); + } + } + } catch (IOException x) { + log.error(sm.getString("farmWarDeployer.msgIoe"), x); + } + } + + /** + * Create factory for all transported war files + * + * @param msg The file + * + * @return Factory for all app message (war files) + * + * @throws FileNotFoundException Missing file error + * @throws IOException Other IO error + */ + public synchronized FileMessageFactory getFactory(FileMessage msg) throws FileNotFoundException, IOException { + File writeToFile = new File(getTempDirFile(), msg.getFileName()); + FileMessageFactory factory = fileFactories.get(msg.getFileName()); + if (factory == null) { + factory = FileMessageFactory.getInstance(writeToFile, true); + factory.setMaxValidTime(maxValidTime); + fileFactories.put(msg.getFileName(), factory); + } + return factory; + } + + /** + * Remove file (war) from messages + * + * @param msg The file + */ + public void removeFactory(FileMessage msg) { + fileFactories.remove(msg.getFileName()); + } + + /** + * Before the cluster invokes messageReceived the cluster will ask the receiver to accept or decline the message, In + * the future, when messages get big, the accept method will only take a message header + * + * @param msg ClusterMessage + * + * @return boolean - returns true to indicate that messageReceived should be invoked. If false is returned, the + * messageReceived method will not be invoked. + */ + @Override + public boolean accept(ClusterMessage msg) { + return msg instanceof FileMessage || msg instanceof UndeployMessage; + } + + /** + * Install a new web application, whose web application archive is at the specified URL, into this container and all + * the other members of the cluster with the specified context name. + *

    + * If this application is successfully installed locally, a ContainerEvent of type INSTALL_EVENT will + * be sent to all registered listeners, with the newly created Context as an argument. + * + * @param contextName The context name to which this application should be installed (must be unique) + * @param webapp A WAR file or unpacked directory structure containing the web application to be installed + * + * @exception IllegalArgumentException if the specified context name is malformed + * @exception IllegalStateException if the specified context name is already deployed + * @exception IOException if an input/output error was encountered during installation + */ + @Override + public void install(String contextName, File webapp) throws IOException { + Member[] members = getCluster().getMembers(); + if (members.length == 0) { + return; + } + + Member localMember = getCluster().getLocalMember(); + FileMessageFactory factory = FileMessageFactory.getInstance(webapp, false); + FileMessage msg = new FileMessage(localMember, webapp.getName(), contextName); + if (log.isTraceEnabled()) { + log.trace(sm.getString("farmWarDeployer.sendStart", contextName, webapp)); + } + msg = factory.readMessage(msg); + while (msg != null) { + for (Member member : members) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("farmWarDeployer.sendFragment", contextName, webapp, member)); + } + getCluster().send(msg, member); + } + msg = factory.readMessage(msg); + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("farmWarDeployer.sendEnd", contextName, webapp)); + } + } + + /** + * Remove an existing web application, attached to the specified context name. If this application is successfully + * removed, a ContainerEvent of type REMOVE_EVENT will be sent to all registered listeners, with the + * removed Context as an argument. Deletes the web application war file and/or directory if they exist + * in the Host's appBase. + * + * @param contextName The context name of the application to be removed + * @param undeploy boolean flag to remove web application from server + * + * @exception IllegalArgumentException if the specified context name is malformed + * @exception IllegalArgumentException if the specified context name does not identify a currently installed web + * application + * @exception IOException if an input/output error occurs during removal + */ + @Override + public void remove(String contextName, boolean undeploy) throws IOException { + if (getCluster().getMembers().length > 0) { + if (log.isInfoEnabled()) { + log.info(sm.getString("farmWarDeployer.removeStart", contextName)); + } + Member localMember = getCluster().getLocalMember(); + UndeployMessage msg = new UndeployMessage(localMember, System.currentTimeMillis(), + "Undeploy:" + contextName + ":" + System.currentTimeMillis(), contextName); + if (log.isTraceEnabled()) { + log.trace(sm.getString("farmWarDeployer.removeTxMsg", contextName)); + } + cluster.send(msg); + } + // remove locally + if (undeploy) { + try { + if (tryAddServiced(contextName)) { + try { + remove(contextName); + } finally { + removeServiced(contextName); + } + check(contextName); + } else { + log.error(sm.getString("farmWarDeployer.removeFailRemote", contextName)); + } + + } catch (Exception ex) { + log.error(sm.getString("farmWarDeployer.removeFailLocal", contextName), ex); + } + } + + } + + /** + * Modification from watchDir war detected! + * + * @see org.apache.catalina.ha.deploy.FileChangeListener#fileModified(File) + */ + @Override + public void fileModified(File newWar) { + try { + File deployWar = new File(getDeployDirFile(), newWar.getName()); + ContextName cn = new ContextName(deployWar.getName(), true); + if (deployWar.exists() && deployWar.lastModified() > newWar.lastModified()) { + if (log.isInfoEnabled()) { + log.info(sm.getString("farmWarDeployer.alreadyDeployed", cn.getName())); + } + return; + } + if (log.isInfoEnabled()) { + log.info(sm.getString("farmWarDeployer.modInstall", cn.getName(), deployWar.getAbsolutePath())); + } + // install local + if (tryAddServiced(cn.getName())) { + try { + copy(newWar, deployWar); + } finally { + removeServiced(cn.getName()); + } + check(cn.getName()); + } else { + log.error(sm.getString("farmWarDeployer.servicingDeploy", cn.getName(), deployWar.getName())); + } + install(cn.getName(), deployWar); + } catch (Exception x) { + log.error(sm.getString("farmWarDeployer.modInstallFail"), x); + } + } + + /** + * War remove from watchDir + * + * @see org.apache.catalina.ha.deploy.FileChangeListener#fileRemoved(File) + */ + @Override + public void fileRemoved(File removeWar) { + try { + ContextName cn = new ContextName(removeWar.getName(), true); + if (log.isInfoEnabled()) { + log.info(sm.getString("farmWarDeployer.removeLocal", cn.getName())); + } + remove(cn.getName(), true); + } catch (Exception x) { + log.error(sm.getString("farmWarDeployer.removeLocalFail"), x); + } + } + + /** + * Invoke the remove method on the deployer. + * + * @param contextName The context to remove + * + * @throws Exception If an error occurs removing the context + */ + protected void remove(String contextName) throws Exception { + // TODO Handle remove also work dir content ! + // Stop the context first to be nicer + Context context = (Context) host.findChild(contextName); + if (context != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("farmWarDeployer.undeployLocal", contextName)); + } + context.stop(); + String baseName = context.getBaseName(); + File war = new File(host.getAppBaseFile(), baseName + ".war"); + File dir = new File(host.getAppBaseFile(), baseName); + File xml = new File(configBase, baseName + ".xml"); + if (war.exists()) { + if (!war.delete()) { + log.error(sm.getString("farmWarDeployer.deleteFail", war)); + } + } else if (dir.exists()) { + undeployDir(dir); + } else { + if (!xml.delete()) { + log.error(sm.getString("farmWarDeployer.deleteFail", xml)); + } + } + } + } + + /** + * Delete the specified directory, including all of its contents and subdirectories recursively. + * + * @param dir File object representing the directory to be deleted + */ + protected void undeployDir(File dir) { + + String files[] = dir.list(); + if (files == null) { + files = new String[0]; + } + for (String s : files) { + File file = new File(dir, s); + if (file.isDirectory()) { + undeployDir(file); + } else { + if (!file.delete()) { + log.error(sm.getString("farmWarDeployer.deleteFail", file)); + } + } + } + if (!dir.delete()) { + log.error(sm.getString("farmWarDeployer.deleteFail", dir)); + } + } + + /** + * Call watcher to check for deploy changes + * + * @see org.apache.catalina.ha.ClusterDeployer#backgroundProcess() + */ + @Override + public void backgroundProcess() { + if (started) { + if (watchEnabled) { + count = (count + 1) % processDeployFrequency; + if (count == 0) { + watcher.check(); + } + } + removeInvalidFileFactories(); + } + + } + + /*--Deployer Operations ------------------------------------*/ + + /** + * Check a context for deployment operations. + * + * @param name The context name + * + * @throws Exception Error invoking the deployer + */ + protected void check(String name) throws Exception { + String[] params = { name }; + String[] signature = { "java.lang.String" }; + mBeanServer.invoke(oname, "check", params, signature); + } + + /** + * Attempt to mark a context as being serviced + * + * @param name The context name + * + * @return {@code true} if the application was marked as being serviced and {@code false} if the application was + * already marked as being serviced + * + * @throws Exception Error invoking the deployer + */ + protected boolean tryAddServiced(String name) throws Exception { + String[] params = { name }; + String[] signature = { "java.lang.String" }; + Boolean result = (Boolean) mBeanServer.invoke(oname, "tryAddServiced", params, signature); + return result.booleanValue(); + } + + /** + * Mark a context as no longer being serviced. + * + * @param name The context name + * + * @throws Exception Error invoking the deployer + */ + protected void removeServiced(String name) throws Exception { + String[] params = { name }; + String[] signature = { "java.lang.String" }; + mBeanServer.invoke(oname, "removeServiced", params, signature); + } + + /*--Instance Getters/Setters--------------------------------*/ + @Override + public boolean equals(Object listener) { + return super.equals(listener); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + public String getDeployDir() { + return deployDir; + } + + public File getDeployDirFile() { + if (deployDirFile != null) { + return deployDirFile; + } + + File dir = getAbsolutePath(getDeployDir()); + this.deployDirFile = dir; + return dir; + } + + public void setDeployDir(String deployDir) { + this.deployDir = deployDir; + } + + public String getTempDir() { + return tempDir; + } + + public File getTempDirFile() { + if (tempDirFile != null) { + return tempDirFile; + } + + File dir = getAbsolutePath(getTempDir()); + this.tempDirFile = dir; + return dir; + } + + public void setTempDir(String tempDir) { + this.tempDir = tempDir; + } + + public String getWatchDir() { + return watchDir; + } + + public File getWatchDirFile() { + if (watchDirFile != null) { + return watchDirFile; + } + + File dir = getAbsolutePath(getWatchDir()); + this.watchDirFile = dir; + return dir; + } + + public void setWatchDir(String watchDir) { + this.watchDir = watchDir; + } + + public boolean isWatchEnabled() { + return watchEnabled; + } + + public boolean getWatchEnabled() { + return watchEnabled; + } + + public void setWatchEnabled(boolean watchEnabled) { + this.watchEnabled = watchEnabled; + } + + /** + * @return the frequency of watcher checks. + */ + public int getProcessDeployFrequency() { + return this.processDeployFrequency; + } + + /** + * Set the watcher checks frequency. + * + * @param processExpiresFrequency the new manager checks frequency + */ + public void setProcessDeployFrequency(int processExpiresFrequency) { + + if (processExpiresFrequency <= 0) { + return; + } + this.processDeployFrequency = processExpiresFrequency; + } + + public int getMaxValidTime() { + return maxValidTime; + } + + public void setMaxValidTime(int maxValidTime) { + this.maxValidTime = maxValidTime; + } + + /** + * Copy a file to the specified temp directory. + * + * @param from copy from temp + * @param to to host appBase directory + * + * @return true, copy successful + */ + protected boolean copy(File from, File to) { + try { + if (!to.exists()) { + if (!to.createNewFile()) { + log.error(sm.getString("fileNewFail", to)); + return false; + } + } + } catch (IOException e) { + log.error(sm.getString("farmWarDeployer.fileCopyFail", from, to), e); + return false; + } + + try (java.io.FileInputStream is = new java.io.FileInputStream(from); + java.io.FileOutputStream os = new java.io.FileOutputStream(to, false)) { + byte[] buf = new byte[4096]; + while (true) { + int len = is.read(buf); + if (len < 0) { + break; + } + os.write(buf, 0, len); + } + } catch (IOException e) { + log.error(sm.getString("farmWarDeployer.fileCopyFail", from, to), e); + return false; + } + return true; + } + + protected void removeInvalidFileFactories() { + String[] fileNames = fileFactories.keySet().toArray(new String[0]); + for (String fileName : fileNames) { + FileMessageFactory factory = fileFactories.get(fileName); + if (!factory.isValid()) { + fileFactories.remove(fileName); + } + } + } + + private File getAbsolutePath(String path) { + File dir = new File(path); + if (!dir.isAbsolute()) { + dir = new File(getCluster().getContainer().getCatalinaBase(), dir.getPath()); + } + try { + dir = dir.getCanonicalFile(); + } catch (IOException e) {// ignore + } + return dir; + } +} diff --git a/java/org/apache/catalina/ha/deploy/FileChangeListener.java b/java/org/apache/catalina/ha/deploy/FileChangeListener.java new file mode 100644 index 0000000..5f2d5dd --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/FileChangeListener.java @@ -0,0 +1,25 @@ +/* + * 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.catalina.ha.deploy; + +import java.io.File; + +public interface FileChangeListener { + void fileModified(File f); + + void fileRemoved(File f); +} diff --git a/java/org/apache/catalina/ha/deploy/FileMessage.java b/java/org/apache/catalina/ha/deploy/FileMessage.java new file mode 100644 index 0000000..bac4275 --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/FileMessage.java @@ -0,0 +1,91 @@ +/* + * 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.catalina.ha.deploy; + +import org.apache.catalina.ha.ClusterMessageBase; +import org.apache.catalina.tribes.Member; + +/** + * Contains the data for a file being transferred over TCP, this is essentially a fragment of a file, read and written + * by the FileMessageFactory. + */ + +public class FileMessage extends ClusterMessageBase { + private static final long serialVersionUID = 2L; + + private int messageNumber; + private byte[] data; + private int dataLength; + + private long totalNrOfMsgs; + private final String fileName; + private final String contextName; + + public FileMessage(Member source, String fileName, String contextName) { + this.address = source; + this.fileName = fileName; + this.contextName = contextName; + } + + public int getMessageNumber() { + return messageNumber; + } + + public void setMessageNumber(int messageNumber) { + this.messageNumber = messageNumber; + } + + public long getTotalNrOfMsgs() { + return totalNrOfMsgs; + } + + public void setTotalNrOfMsgs(long totalNrOfMsgs) { + this.totalNrOfMsgs = totalNrOfMsgs; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data, int length) { + this.data = data; + this.dataLength = length; + } + + public int getDataLength() { + return dataLength; + } + + @Override + public String getUniqueId() { + StringBuilder result = new StringBuilder(getFileName()); + result.append("#-#"); + result.append(getMessageNumber()); + result.append("#-#"); + result.append(System.currentTimeMillis()); + return result.toString(); + } + + + public String getFileName() { + return fileName; + } + + public String getContextName() { + return contextName; + } +} diff --git a/java/org/apache/catalina/ha/deploy/FileMessageFactory.java b/java/org/apache/catalina/ha/deploy/FileMessageFactory.java new file mode 100644 index 0000000..fb831d1 --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/FileMessageFactory.java @@ -0,0 +1,362 @@ +/* + * 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.catalina.ha.deploy; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * This factory is used to read files and write files by splitting them up into smaller messages. So that entire files + * don't have to be read into memory.
    + * The factory can be used as a reader or writer but not both at the same time. When done reading or writing the factory + * will close the input or output streams and mark the factory as closed. It is not possible to use it after that.
    + * To force a cleanup, call cleanup() from the calling object.
    + * This class is not thread safe. + */ +public class FileMessageFactory { + /*--Static Variables----------------------------------------*/ + private static final Log log = LogFactory.getLog(FileMessageFactory.class); + private static final StringManager sm = StringManager.getManager(FileMessageFactory.class); + + /** + * The number of bytes that we read from file + */ + public static final int READ_SIZE = 1024 * 10; // 10 KiB + + /** + * The file that we are reading/writing + */ + protected final File file; + + /** + * True means that we are writing with this factory. False means that we are reading with this factory + */ + protected final boolean openForWrite; + + /** + * Once the factory is used, it cannot be reused. + */ + protected boolean closed = false; + + /** + * When openForWrite=false, the input stream is held by this variable + */ + protected FileInputStream in; + + /** + * When openForWrite=true, the output stream is held by this variable + */ + protected FileOutputStream out; + + /** + * The number of messages we have written + */ + protected int nrOfMessagesProcessed = 0; + + /** + * The total size of the file + */ + protected long size = 0; + + /** + * The total number of packets that we split this file into + */ + protected long totalNrOfMessages = 0; + + /** + * The number of the last message processed. Message IDs are 1 based. + */ + protected AtomicLong lastMessageProcessed = new AtomicLong(0); + + /** + * Messages received out of order are held in the buffer until required. If everything is worked as expected, + * messages will spend very little time in the buffer. + */ + protected final Map msgBuffer = new ConcurrentHashMap<>(); + + /** + * The bytes that we hold the data in, not thread safe. + */ + protected byte[] data = new byte[READ_SIZE]; + + /** + * Flag that indicates if a thread is writing messages to disk. Access to this flag must be synchronised. + */ + protected boolean isWriting = false; + + /** + * The time this instance was created. (in milliseconds) + * + * @deprecated Unused. This will be removed in Tomcat 11. + */ + @Deprecated + protected long creationTime = 0; + + + /** + * The time this instance was last modified. + */ + protected long lastModified = 0; + + /** + * The maximum time (in seconds) this instance will be allowed to exist from lastModifiedTime. + */ + protected int maxValidTime = -1; + + /** + * Private constructor, either instantiates a factory to read or write.
    + * When openForWrite==true, then a the file, f, will be created and an output stream is opened to write to it.
    + * When openForWrite==false, an input stream is opened, the file has to exist. + * + * @param f File - the file to be read/written + * @param openForWrite boolean - true means we are writing to the file, false means we are reading from the file + * + * @throws FileNotFoundException - if the file to be read doesn't exist + * @throws IOException - if the system fails to open input/output streams to the file or if it fails to + * create the file to be written to. + */ + private FileMessageFactory(File f, boolean openForWrite) throws FileNotFoundException, IOException { + this.file = f; + this.openForWrite = openForWrite; + if (log.isTraceEnabled()) { + log.trace("FileMessageFactory open file " + f + " write " + openForWrite); + } + if (openForWrite) { + if (!file.exists()) { + if (!file.createNewFile()) { + throw new IOException(sm.getString("fileNewFail", file)); + } + } + out = new FileOutputStream(f); + } else { + size = file.length(); + totalNrOfMessages = (size / READ_SIZE) + 1; + in = new FileInputStream(f); + } // end if + creationTime = System.currentTimeMillis(); + lastModified = System.currentTimeMillis(); + } + + /** + * Creates a factory to read or write from a file. When opening for read, the readMessage can be invoked, and when + * opening for write the writeMessage can be invoked. + * + * @param f File - the file to be read or written + * @param openForWrite boolean - true, means we are writing to the file, false means we are reading from it + * + * @throws FileNotFoundException - if the file to be read doesn't exist + * @throws IOException - if it fails to create the file that is to be written + * + * @return FileMessageFactory + */ + public static FileMessageFactory getInstance(File f, boolean openForWrite) + throws FileNotFoundException, IOException { + return new FileMessageFactory(f, openForWrite); + } + + /** + * Reads file data into the file message and sets the size, totalLength, totalNrOfMsgs and the message number
    + * If EOF is reached, the factory returns null, and closes itself, otherwise the same message is returned as was + * passed in. This makes sure that not more memory is ever used. To remember, neither the file message or the + * factory are thread safe. Don't hand off the message to one thread and read the same with another. + * + * @param f FileMessage - the message to be populated with file data + * + * @throws IllegalArgumentException - if the factory is for writing or is closed + * @throws IOException - if a file read exception occurs + * + * @return FileMessage - returns the same message passed in as a parameter, or null if EOF + */ + public FileMessage readMessage(FileMessage f) throws IllegalArgumentException, IOException { + checkState(false); + int length = in.read(data); + if (length == -1) { + cleanup(); + return null; + } else { + f.setData(data, length); + f.setTotalNrOfMsgs(totalNrOfMessages); + f.setMessageNumber(++nrOfMessagesProcessed); + return f; + } // end if + } + + /** + * Writes a message to file. If (msg.getMessageNumber() == msg.getTotalNrOfMsgs()) the output stream will be closed + * after writing. + * + * @param msg FileMessage - message containing data to be written + * + * @throws IllegalArgumentException - if the factory is opened for read or closed + * @throws IOException - if a file write error occurs + * + * @return returns true if the file is complete and outputstream is closed, false otherwise. + */ + public boolean writeMessage(FileMessage msg) throws IllegalArgumentException, IOException { + if (!openForWrite) { + throw new IllegalArgumentException(sm.getString("fileMessageFactory.cannotWrite")); + } + if (log.isTraceEnabled()) { + log.trace("Message " + msg + " data " + HexUtils.toHexString(msg.getData()) + " data length " + + msg.getDataLength() + " out " + out); + } + + if (msg.getMessageNumber() <= lastMessageProcessed.get()) { + // Duplicate of message already processed + log.warn(sm.getString("fileMessageFactory.duplicateMessage", msg.getContextName(), msg.getFileName(), + HexUtils.toHexString(msg.getData()), Integer.valueOf(msg.getDataLength()))); + return false; + } + + FileMessage previous = msgBuffer.put(Long.valueOf(msg.getMessageNumber()), msg); + if (previous != null) { + // Duplicate of message not yet processed + log.warn(sm.getString("fileMessageFactory.duplicateMessage", msg.getContextName(), msg.getFileName(), + HexUtils.toHexString(msg.getData()), Integer.valueOf(msg.getDataLength()))); + return false; + } + + // Have received a new message. Update the last modified time (even if the message is being buffered for now). + lastModified = System.currentTimeMillis(); + + FileMessage next = null; + synchronized (this) { + if (!isWriting) { + next = msgBuffer.get(Long.valueOf(lastMessageProcessed.get() + 1)); + if (next != null) { + isWriting = true; + } else { + return false; + } + } else { + return false; + } + } + + while (next != null) { + out.write(next.getData(), 0, next.getDataLength()); + lastMessageProcessed.incrementAndGet(); + out.flush(); + if (next.getMessageNumber() == next.getTotalNrOfMsgs()) { + out.close(); + cleanup(); + return true; + } + synchronized (this) { + next = msgBuffer.get(Long.valueOf(lastMessageProcessed.get() + 1)); + if (next == null) { + isWriting = false; + } + } + } + + return false; + }// writeMessage + + /** + * Closes the factory, its streams and sets all its references to null + */ + public void cleanup() { + if (in != null) { + try { + in.close(); + } catch (IOException ignore) { + } + } + if (out != null) { + try { + out.close(); + } catch (IOException ignore) { + } + } + in = null; + out = null; + size = 0; + closed = true; + data = null; + nrOfMessagesProcessed = 0; + totalNrOfMessages = 0; + msgBuffer.clear(); + lastMessageProcessed = null; + } + + /** + * Check to make sure the factory is able to perform the function it is asked to do. Invoked by + * readMessage/writeMessage before those methods proceed. + * + * @param openForWrite The value to check + * + * @throws IllegalArgumentException if the state is not the expected one + */ + protected void checkState(boolean openForWrite) throws IllegalArgumentException { + if (this.openForWrite != openForWrite) { + cleanup(); + if (openForWrite) { + throw new IllegalArgumentException(sm.getString("fileMessageFactory.cannotWrite")); + } else { + throw new IllegalArgumentException(sm.getString("fileMessageFactory.cannotRead")); + } + } + if (this.closed) { + cleanup(); + throw new IllegalArgumentException(sm.getString("fileMessageFactory.closed")); + } + } + + public File getFile() { + return file; + } + + public boolean isValid() { + if (maxValidTime > 0) { + long timeNow = System.currentTimeMillis(); + long timeIdle = (timeNow - lastModified) / 1000L; + if (timeIdle > maxValidTime) { + cleanup(); + if (file.exists()) { + if (file.delete()) { + log.warn(sm.getString("fileMessageFactory.delete", file, Long.toString(maxValidTime))); + } else { + log.warn(sm.getString("fileMessageFactory.deleteFail", file)); + } + } + return false; + } + } + return true; + } + + public int getMaxValidTime() { + return maxValidTime; + } + + public void setMaxValidTime(int maxValidTime) { + this.maxValidTime = maxValidTime; + } + +} diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings.properties b/java/org/apache/catalina/ha/deploy/LocalStrings.properties new file mode 100644 index 0000000..8b2df01 --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings.properties @@ -0,0 +1,61 @@ +# 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. + +farmWarDeployer.alreadyDeployed=webapp [{0}] are already deployed. +farmWarDeployer.delete=Deleted [{0}] before the full file was received as the maxValidTime of [{1}] seconds has expired +farmWarDeployer.deleteFail=Failed to delete [{0}] +farmWarDeployer.deployEnd=Deployment from [{0}] finished. +farmWarDeployer.fileCopyFail=Unable to copy from [{0}] to [{1}] +farmWarDeployer.fileMessageError=Error processing file message +farmWarDeployer.hostOnly=FarmWarDeployer can only work as host cluster subelement! +farmWarDeployer.hostParentEngine=FarmWarDeployer can only work if parent of [{0}] is an engine! +farmWarDeployer.mbeanNameFail=Cannot construct MBean object name for engine [{0}] and host [{1}] +farmWarDeployer.modInstall=Installing webapp [{0}] from [{1}] +farmWarDeployer.modInstallFail=Unable to install WAR file +farmWarDeployer.msgIoe=Unable to read farm deploy file message. +farmWarDeployer.msgRxDeploy=Receive cluster deployment path [{0}], war [{1}] +farmWarDeployer.msgRxUndeploy=Receive cluster undeployment from path [{0}] +farmWarDeployer.removeFailLocal=Local remove from [{0}] failed +farmWarDeployer.removeFailRemote=Local remove from [{0}] failed, other manager has app in service! +farmWarDeployer.removeLocal=Removing webapp [{0}] +farmWarDeployer.removeLocalFail=Unable to remove WAR file +farmWarDeployer.removeStart=Cluster wide remove of web app [{0}] +farmWarDeployer.removeTxMsg=Send cluster wide undeployment from [{0}] +farmWarDeployer.renameFail=Failed to rename [{0}] to [{1}] +farmWarDeployer.sendEnd=Send cluster war deployment path [{0}], war [{1}] finished. +farmWarDeployer.sendFragment=Send cluster war fragment path [{0}], war [{1}] to [{2}] +farmWarDeployer.sendStart=Send cluster war deployment path [{0}], war [{1}] started. +farmWarDeployer.servicingDeploy=Application [{0}] is being serviced. Touch war file [{1}] again! +farmWarDeployer.servicingUndeploy=Application [{0}] is being serviced and can''t be removed from backup cluster node +farmWarDeployer.started=Cluster FarmWarDeployer started. +farmWarDeployer.stopped=Cluster FarmWarDeployer stopped. +farmWarDeployer.undeployEnd=Undeployment from [{0}] finished. +farmWarDeployer.undeployLocal=Undeploy local context [{0}] +farmWarDeployer.undeployMessageError=Error processing undeploy message +farmWarDeployer.watchDir=Cluster deployment is watching [{0}] for changes. + +fileMessageFactory.cannotRead=Cannot read message, this factory is writing +fileMessageFactory.cannotWrite=Cannot write message, this factory is reading +fileMessageFactory.closed=Factory has been closed +fileMessageFactory.deleteFail=Failed to delete [{0}] +fileMessageFactory.duplicateMessage=Received duplicate message. Is the Sender timeout too low? context: [{0}] filename: [{1}] data: [{2}] data length: [{3}] + +fileNewFail=Unable to create [{0}] + +warWatcher.cantListWatchDir=Cannot list files in WatchDir [{0}]: check to see if it is a directory and has read permissions. +warWatcher.checkWarResult=WarInfo.check() returned [{0}] for [{1}] +warWatcher.checkingWar=Checking WAR file [{0}] +warWatcher.checkingWars=Checking WARs in [{0}] +warWatcher.listedFileDoesNotExist=[{0}] was detected in [{1}] but does not exist. Check directory permissions on [{1}]? diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_cs.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_cs.properties new file mode 100644 index 0000000..29caa5c --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_cs.properties @@ -0,0 +1,25 @@ +# 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. + +farmWarDeployer.hostOnly=FarmWarDeployer může pracovat pouze jako subelement host clusteru! +farmWarDeployer.modInstall=Instaluji webapp [{0}] z [{1}] +farmWarDeployer.modInstallFail=Nelze nainstalovat WAR soubor +farmWarDeployer.msgIoe=Nelze pÅ™eÄíst zprávu z farmy pro nasazení WAR. +farmWarDeployer.removeFailRemote=Lokální odstranÄ›ní z [{0}] selhalo, jiný manager má aplikaci v provozu! +farmWarDeployer.servicingUndeploy=Aplikace [{0}] je provozována a nemůže být odebrána ze záložního uzlu clusteru +farmWarDeployer.undeployEnd=OdstranÄ›ní [{0}] ukonÄeno. + +warWatcher.cantListWatchDir=Nelze vypsat soubory v WatchDir [{0}]: pÅ™ekontrolujte, zda jde o adresář a má nastaveny práva pro Ätení. +warWatcher.checkingWar=Kontrola WAR souboru [{0}] diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_de.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_de.properties new file mode 100644 index 0000000..09d9d19 --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_de.properties @@ -0,0 +1,27 @@ +# 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. + +farmWarDeployer.hostOnly=FarmWarDeployer kann nur als Cluster-Unterelement arbeiten! +farmWarDeployer.modInstall=Installiere Webapplikation [{0}] von [{1}] +farmWarDeployer.modInstallFail=WAR-Datei konnte nicht installiert werden +farmWarDeployer.msgIoe=Die Farm-Deploy-Datei-Nachricht kann nicht gelesen werden. +farmWarDeployer.servicingUndeploy=Applikation [{0}] ist noch aktiv und kann nicht vom Backup-Cluster-Knoten entfernt werden +farmWarDeployer.undeployEnd=Undeployment von [{0}] beendet. + +fileMessageFactory.duplicateMessage=Doppelte Nachricht empfangen. Ist der Timeout für den Sender zu niedrig? Context: [{0}] Dateiname: [{1}] Daten: [{2}] Datenlänge [{3}] + +warWatcher.cantListWatchDir=Dateien in WatchDir [{0}] können nicht gelistet werden: Prüfen Sie dass Lesezugriff auf das Verzeichnis besteht. +warWatcher.checkWarResult=WarInfo.check() lieferte [{0}] für [{1}] +warWatcher.checkingWar=WAR-Datei [{0}] wird geprüft. diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_es.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_es.properties new file mode 100644 index 0000000..77e9273 --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_es.properties @@ -0,0 +1,31 @@ +# 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. + +farmWarDeployer.hostOnly=FarmWarDeployer sólo puede operar como un subelemento de una máquina del cluster! +farmWarDeployer.modInstall=Installando webapp [{0}] desde [{1}]\n +farmWarDeployer.modInstallFail=Incapaz de instalar WAR file +farmWarDeployer.msgIoe=Incapáz de leer el archivo de despliegue de la granja +farmWarDeployer.msgRxDeploy=Recibe el camino de despliegue del cluster [{0}], war [{1}] +farmWarDeployer.removeFailLocal=Borrado local de [{0}] fallido +farmWarDeployer.removeFailRemote=El borrado local de [{0}] falló, otro manager tiene la aplicación en servicio! +farmWarDeployer.sendFragment=Fragmento war enviado al cluster con camino [{0}], war [{1}] a [{2}]\n +farmWarDeployer.servicingUndeploy=La applicación [{0}] esta en servicion y no pude ser removida del nodo de respaldo del cluster +farmWarDeployer.undeployEnd=El revertimiendo del despliegue de [{0}] ha terminado.\n + +fileMessageFactory.deleteFail=Fallo al borrar [{0}]\n + +warWatcher.cantListWatchDir=No se pueden listar archivos en WatchDir [{0}]: verifique si es un directorio y tiene permisos de lectura.\n +warWatcher.checkWarResult=WarInfo.check() devolvió [{0}] para [{1}] +warWatcher.checkingWar=Verificando archivo WAR [{0}] diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_fr.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_fr.properties new file mode 100644 index 0000000..4264fe7 --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_fr.properties @@ -0,0 +1,61 @@ +# 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. + +farmWarDeployer.alreadyDeployed=l''application web [{0}] est déjà déployée +farmWarDeployer.delete=[{0}] a été supprimé avant que le fichier complet n''ait été recu car le maxValidTime de [{1}] secondes a expiré +farmWarDeployer.deleteFail=Pas réussi à supprimer [{0}] +farmWarDeployer.deployEnd=Le déploiement de [{0}] est terminé +farmWarDeployer.fileCopyFail=Impossible de copier depuis [{0}] vers [{1}] +farmWarDeployer.fileMessageError=Erreur lors du traitement du message fichier +farmWarDeployer.hostOnly=Le FarmWarDeployer ne fonctionne qu'en tant que sous-élément d'un "host cluster" ! +farmWarDeployer.hostParentEngine=FarmWarDeployer peut fonctionner uniquement si le parent de [{0}] est un moteur +farmWarDeployer.mbeanNameFail=Impossible de construire le nom d''objet du mbean pour le moteur [{0}] et l''hôte [{1}] +farmWarDeployer.modInstall=Installation en cours pour la webapp [{0}] depuis [{1}] +farmWarDeployer.modInstallFail=Incapable d'installer le fichier WAR +farmWarDeployer.msgIoe=Incapable de lire le message de déploiement dans la ferme +farmWarDeployer.msgRxDeploy=Recu le chemin de déploiement [{0}] du cluster, war [{1}] +farmWarDeployer.msgRxUndeploy=Réception d''un retrait de cluster ("cluster undeployment") du chemin [{0}] +farmWarDeployer.removeFailLocal=Impossible d''enlever localement de [{0}] +farmWarDeployer.removeFailRemote=La suppression locale de [{0}] a échouée, l''autre gestionnaire (manager) a l''app en fonction ! +farmWarDeployer.removeLocal=Retrait de l''application web [{0}] +farmWarDeployer.removeLocalFail=Impossible d'enlever le fichier WAR +farmWarDeployer.removeStart=Retrait de l''application web [{0}] dans tout le cluster +farmWarDeployer.removeTxMsg=Envoi à tout le cluster du déploiement à partir de [{0}] +farmWarDeployer.renameFail=Echec du renommage de [{0}] en [{1}] +farmWarDeployer.sendEnd=Envoi du chemin de déploiement du war au cluster, war [{1}] terminé +farmWarDeployer.sendFragment=Envoi du chemin du fragment du war du cluster [{0}], war [{1}] vers [{2}] +farmWarDeployer.sendStart=Envoi du déploiement war en cluster chemin [{0}], war [{1}] démarré +farmWarDeployer.servicingDeploy=L''application [{0}] est en cours de maintenance, mettez de nouveau à jour la date du fichier war [{1}] +farmWarDeployer.servicingUndeploy=L''application [{0}] est en maintenance et ne peut être retirée du node backup du cluster +farmWarDeployer.started=Le FarmWarDeployer du cluster a démarré +farmWarDeployer.stopped=Le FarmWarDeployer du cluster a été arrêté +farmWarDeployer.undeployEnd=Retrait de [{0}] terminé +farmWarDeployer.undeployLocal=Le contexte local [{0}] est retiré +farmWarDeployer.undeployMessageError=Erreur lors du traitement du message de retrait +farmWarDeployer.watchDir=Le déploiement du cluster surveille [{0}] pour des modifications + +fileMessageFactory.cannotRead=Impossible de lire un message, cette fabrique est en train d'écrire +fileMessageFactory.cannotWrite=Impossible d'écrire un message, cette fabrique est en train de lire +fileMessageFactory.closed=La fabrique a été fermée +fileMessageFactory.deleteFail=Impossible de supprimer [{0}] +fileMessageFactory.duplicateMessage=Réception de message en double, le délai d''attente maximum de l''expéditeur pourrait être trop court ; contexte : [{0}] nom de fichier : [{1}] données : [{2}] longueur des données : [{3}] + +fileNewFail=Impossible de créer [{0}] + +warWatcher.cantListWatchDir=Incapacité de lister les fichiers dans le répertoire WatchDir [{0}] : vérifiez qu''il s''agit d''un répertoire et qu''il a la permission de lecture. +warWatcher.checkWarResult=WarInfo.check() a retourné [{0}] pour [{1}] +warWatcher.checkingWar=Vérification du fichier WAR [{0}] +warWatcher.checkingWars=Vérification des WARs dans [{0}] +warWatcher.listedFileDoesNotExist=[{0}] a été détecté dans [{1}] mais n''existe pas, les permissions du répertoire pourraient être incorrectes diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_ja.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_ja.properties new file mode 100644 index 0000000..5c5c280 --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_ja.properties @@ -0,0 +1,61 @@ +# 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. + +farmWarDeployer.alreadyDeployed=webapp [{0}]ã¯æ—¢ã«é…å‚™ã•ã‚Œã¦ã„ã¾ã™ã€‚ +farmWarDeployer.delete=[{1}] 秒㮠maxValidTime ãŒæœŸé™åˆ‡ã‚Œã«ãªã£ãŸãŸã‚ã€å®Œå…¨ãªãƒ•ã‚¡ã‚¤ãƒ«ã‚’å—ä¿¡ã™ã‚‹å‰ã« [{0}] を削除ã—ã¾ã—㟠+farmWarDeployer.deleteFail=[{0}] を削除ã§ãã¾ã›ã‚“。 +farmWarDeployer.deployEnd=[{0}] ã‹ã‚‰ã®é…å‚™ãŒå®Œäº†ã—ã¾ã—ãŸã€‚ +farmWarDeployer.fileCopyFail=[{0}] ã‹ã‚‰ [{1}] ã¸ã‚³ãƒ”ーã§ãã¾ã›ã‚“。 +farmWarDeployer.fileMessageError=ファイルメッセージ処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +farmWarDeployer.hostOnly=FarmWarDeployer ã¯Hostクラスタã®ã‚µãƒ–エレメントã¨ã—ã¦ã®ã¿æ©Ÿèƒ½ã—ã¾ã™ +farmWarDeployer.hostParentEngine=FarmWarDeployer ã¯è¦ª [{0}] ㌠Engine ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã§ãªã‘ã‚Œã°æ©Ÿèƒ½ã—ã¾ã›ã‚“。 +farmWarDeployer.mbeanNameFail=エンジン [{0}] ã¨ãƒ›ã‚¹ãƒˆ [{1}] ã®MBeanオブジェクトåを構築ã§ãã¾ã›ã‚“ +farmWarDeployer.modInstall=[{1}]ã‹ã‚‰webapp [{0}]をインストールã—ã¦ã„ã¾ã™ã€‚ +farmWarDeployer.modInstallFail=WAR ファイルをインストールã§ãã¾ã›ã‚“ +farmWarDeployer.msgIoe=ファームé…備ファイルメッセージã®èª­ã¿å–ã‚Šã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +farmWarDeployer.msgRxDeploy=クラスタé…備パス [{0}]ã€WAR [{1}] ã‚’å—ä¿¡ã—ã¾ã—㟠+farmWarDeployer.msgRxUndeploy=パス [{0}] ã‹ã‚‰ã‚¯ãƒ©ã‚¹ã‚¿ã®é…備解除をå—ä¿¡ã—ã¾ã—㟠+farmWarDeployer.removeFailLocal=[{0}]ã‹ã‚‰ã®ãƒ­ãƒ¼ã‚«ãƒ«å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—㟠+farmWarDeployer.removeFailRemote=ローカル㮠[{0}] を削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ä»–ã®ãƒžãƒãƒ¼ã‚¸ãƒ£ãƒ¼ãƒŽãƒ¼ãƒ‰ã§ã‚µãƒ¼ãƒ“ス中ã§ã™ ! +farmWarDeployer.removeLocal=webapp [{0}]を削除ã—ã¦ã„ã¾ã™ +farmWarDeployer.removeLocalFail=WAR ファイルを削除ã§ãã¾ã›ã‚“。 +farmWarDeployer.removeStart=Webアプリケーション[{0}]ã®ã‚¯ãƒ©ã‚¹ã‚¿å‰Šé™¤ã€‚ +farmWarDeployer.removeTxMsg=コンテキスト [{0}] ã‹ã‚‰ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ã«é…備解除メッセージをé€ä¿¡ã—ã¾ã—ãŸã€‚ +farmWarDeployer.renameFail=[{0}] ã‹ã‚‰ [{1}] ã¸åå‰ã‚’変更ã§ãã¾ã›ã‚“。 +farmWarDeployer.sendEnd=クラスタWaré…å‚™ パス [{0}] ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚WAR [{1}] を完了ã—ã¾ã—ãŸã€‚ +farmWarDeployer.sendFragment=コンテキスト [{0}] ã® war [{1}] をクラスターメンãƒãƒ¼ [{2}] ã¸é€ä¿¡ã—ã¾ã™ã€‚ +farmWarDeployer.sendStart=クラスタWaré…備パス [{0}] ã‚’é€ä¿¡ã—ã€War [{1}] を開始ã—ã¾ã—ãŸã€‚ +farmWarDeployer.servicingDeploy=アプリケーション [{0}] ã¯ã™ã§ã«ã‚µãƒ¼ãƒ“スを開始ã—ã¦ã„ã¾ã™ã€‚ã‚‚ã†ä¸€åº¦ WAR ファイル [{1}] ã‚’æ›´æ–°ã—ã¦ãã ã•ã„。 +farmWarDeployer.servicingUndeploy=アプリケーション [{0}] ã¯ã‚µãƒ¼ãƒ“ス中ã®ãŸã‚ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—クラスタノードã‹ã‚‰å‰Šé™¤ã§ãã¾ã›ã‚“ +farmWarDeployer.started=クラスター㮠FarmWarDeployer を開始ã—ã¾ã—ãŸã€‚ +farmWarDeployer.stopped=Cluster FarmWarDeployer ãŒåœæ­¢ã—ã¾ã—ãŸã€‚ +farmWarDeployer.undeployEnd=コンテキスト [{0}] ã®é…備解除ãŒå®Œäº†ã—ã¾ã—ãŸã€‚ +farmWarDeployer.undeployLocal=ローカルコンテキスト [{0}] ã‚’é…備解除ã—ã¾ã™ã€‚ +farmWarDeployer.undeployMessageError=é…備解除メッセージ処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +farmWarDeployer.watchDir=クラスタé…å‚™ã®ç›£è¦–[{0}]ãŒå¤‰æ›´ã•ã‚Œã¦ã„ã¾ã™ã€‚ + +fileMessageFactory.cannotRead=メッセージを読むã“ã¨ãŒã§ãã¾ã›ã‚“。ã“ã®Factoryã¯æ›¸ãè¾¼ã¿ä¸­ã§ã™ã€‚ +fileMessageFactory.cannotWrite=メッセージを書ãè¾¼ã‚ã¾ã›ã‚“ã€ã“ã®Factoryã¯èª­ã¿è¾¼ã¿ä¸­ã§ã™ +fileMessageFactory.closed=FileMessageFactoryã¯ã‚¯ãƒ­ãƒ¼ã‚ºã•ã‚Œã¦ã„ã¾ã™ã€‚ +fileMessageFactory.deleteFail=[{0}]を削除ã§ãã¾ã›ã‚“ã§ã—㟠+fileMessageFactory.duplicateMessage=メッセージをもã†ä¸€åº¦å—ä¿¡ã—ã¾ã™ã€‚Senderã®ActTimeoutãŒçŸ­ã™ãŽã¾ã™ã€‚ åå‰ï¼š[{0}] war:[{1}] データ:[{2}] データ長:[{3}] + +fileNewFail=[{0}] を作æˆã§ãã¾ã›ã‚“ + +warWatcher.cantListWatchDir=監視ディレクトリ [{0}] ã®ãƒ•ã‚¡ã‚¤ãƒ«ä¸€è¦§ã‚’å–å¾—ã§ãã¾ã›ã‚“ : ディレクトリã®èª­ã¿å–り権é™ã‚’確èªã—ã¦ãã ã•ã„。 +warWatcher.checkWarResult=WAR ファイル [{1}] ã«ã¤ã„㦠WarInfo.check() 㯠[{0}] ã‚’è¿”ã—ã¾ã—ãŸã€‚ +warWatcher.checkingWar=WAR ファイル [{0}] ã‚’ãƒã‚§ãƒƒã‚¯ã—ã¦ã„ã¾ã™ +warWatcher.checkingWars=[{0}]ã®WARを確èªã—ã¾ã™ +warWatcher.listedFileDoesNotExist=[{1}] ã«ã‚ã‚‹ã¯ãšã® [{0}] ãŒå­˜åœ¨ã—ã¾ã›ã‚“。[{1}] ã®ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªæ¨©é™ã‚’確èªã—ã¦ãã ã•ã„。 diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_ko.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_ko.properties new file mode 100644 index 0000000..512827b --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_ko.properties @@ -0,0 +1,60 @@ +# 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. + +farmWarDeployer.alreadyDeployed=webapp [{0}](들)ì´ ì´ë¯¸ 배치ë˜ì–´ 있습니다. +farmWarDeployer.deleteFail=[{0}]ì„(를) 삭제하지 못했습니다. +farmWarDeployer.deployEnd=[{0}](으)ë¡œë¶€í„°ì˜ ë°°ì¹˜ ìž‘ì—…ì´ ì™„ë£Œë습니다. +farmWarDeployer.fileCopyFail=[{0}](으)로부터 [{1}](으)ë¡œ 복사할 수 없습니다. +farmWarDeployer.fileMessageError=íŒŒì¼ ë©”ì‹œì§€ë¥¼ 처리하는 중 오류 ë°œìƒ +farmWarDeployer.hostOnly=FarmWarDeployer는 ì˜¤ì§ í•˜ìœ„ ì—˜ë¦¬ë¨¼íŠ¸ì¸ í˜¸ìŠ¤íŠ¸ í´ëŸ¬ìŠ¤í„° 엘리먼트ì—ì„œ 존재해야 합니다. +farmWarDeployer.hostParentEngine=FarmWarDeployer는, ì˜¤ì§ [{0}]ì˜ ë¶€ëª¨ê°€ ì—”ì§„ì¼ ë•Œì—만, ì •ìƒ ë™ìž‘í•  수 있습니다! +farmWarDeployer.mbeanNameFail=엔진 [{0}]와(ê³¼) 호스트 [{1}]ì„(를) 위한 MBean ê°ì²´ ì´ë¦„ì„ êµ¬ì„±í•  수 없습니다. +farmWarDeployer.modInstall=[{1}](으)로부터 웹 애플리케ì´ì…˜ [{0}]ì„(를) 설치합니다. +farmWarDeployer.modInstallFail=WAR 파ì¼ì„ 설치할 수 없습니다. +farmWarDeployer.msgIoe=farm deploy íŒŒì¼ ë©”ì‹œì§€ë¥¼ ì½ì„ 수 없습니다. +farmWarDeployer.msgRxDeploy=í´ëŸ¬ìŠ¤í„° 배치 경로 [{0}]ì„(를) 받았ìŒ, war: [{1}] +farmWarDeployer.msgRxUndeploy=경로 [{0}]ì— ëŒ€í•œ í´ëŸ¬ìŠ¤í„° 배치 제거 메시지를 수신했습니다. +farmWarDeployer.removeFailLocal=[{0}](으)로부터 로컬 삭제가 실패했습니다. +farmWarDeployer.removeFailRemote=FarmWarDeployerê°€ 컨í…스트 [{0}]ì„(를) 로컬ì—ì„œ 제거하지 못하였습니다. 다른 Managerê°€ 해당 애플리케ì´ì…˜ì„ 서비스 ì¤‘ì— ìžˆìŠµë‹ˆë‹¤! +farmWarDeployer.removeLocal=웹 애플리케ì´ì…˜ [{0}]ì„(를) 제거합니다. +farmWarDeployer.removeLocalFail=WAR 파ì¼ì„ 제거할 수 없습니다. +farmWarDeployer.removeStart=웹 애플리케ì´ì…˜ [{0}]ì„(를) ì „ í´ëŸ¬ìŠ¤í„°ì—ì„œ 제거 +farmWarDeployer.removeTxMsg=ì „ í´ëŸ¬ìŠ¤í„°ì—ì„œ [{0}]ì— ëŒ€í•œ 배치를 제거 í•  ê²ƒì„ ì „ì†¡í•©ë‹ˆë‹¤. +farmWarDeployer.renameFail=[{0}]ì„(를) [{1}](으)ë¡œ ì´ë¦„ì„ ë³€ê²½í•˜ì§€ 못했습니다. +farmWarDeployer.sendEnd=í´ëŸ¬ìŠ¤í„° war 배치 경로 [{0}]ì„(를) 전송합니다. war [{1}]ì€(는) 완료ë˜ì—ˆìŠµë‹ˆë‹¤. +farmWarDeployer.sendFragment=í´ëŸ¬ìŠ¤í„° war íŒŒì¼ (경로: [{0}]) [{1}]ì„(를) [{2}](으)ë¡œ 전송합니다. +farmWarDeployer.sendStart=í´ëŸ¬ìŠ¤í„° war 배치 경로 [{0}]ì„(를) 전송합니다, war [{1}]ì´(ê°€) 시작ë˜ì—ˆìŠµë‹ˆë‹¤. +farmWarDeployer.servicingDeploy=애플리케ì´ì…˜ [{0}]ì´(ê°€) 서비스ë˜ê³  있습니다. War íŒŒì¼ [{1}]ì„(를) 다시 touch 하십시오! +farmWarDeployer.servicingUndeploy=애플리케ì´ì…˜ [{0}]ì´(ê°€) 서비스 ë˜ê³  있는 중ì´ì–´ì„œ, 백업 í´ëŸ¬ìŠ¤í„° 노드로부터 ì œê±°ë  ìˆ˜ 없습니다. +farmWarDeployer.started=í´ëŸ¬ìŠ¤í„° FarmWarDeployerê°€ 시작ë˜ì—ˆìŠµë‹ˆë‹¤. +farmWarDeployer.stopped=í´ëŸ¬ìŠ¤í„° FarmWarDeployerê°€ 중지ë˜ì—ˆìŠµë‹ˆë‹¤. +farmWarDeployer.undeployEnd=컨í…스트 [{0}]ì˜ ë°°ì¹˜ë¥¼ 제거했습니다. +farmWarDeployer.undeployLocal=로컬 컨í…스트 [{0}]ì˜ ë°°ì¹˜ë¥¼ 제거합니다. +farmWarDeployer.undeployMessageError=배치를 제거하ë¼ëŠ” 메시지를 처리 중 오류 ë°œìƒ +farmWarDeployer.watchDir=í´ëŸ¬ìŠ¤í„° 배치관리ìžê°€ ë³€ê²½ì‚¬í•­ë“¤ì„ íƒì§€í•˜ê¸° 위해 [{0}]ì„(를) ê°ì‹œí•©ë‹ˆë‹¤. + +fileMessageFactory.cannotRead=팩토리가 ì“°ê³  있는 중ì´ë¼ì„œ, 메시지를 ì½ì„ 수 없습니다. +fileMessageFactory.cannotWrite=팩토리가 ì½ê³  있는 중ì´ë¼ì„œ, 메시지를 쓸 수 없습니다. +fileMessageFactory.closed=팩토리가 ì´ë¯¸ 닫혀 있습니다. +fileMessageFactory.deleteFail=[{0}]ì„(를) 삭제하지 못했습니다. +fileMessageFactory.duplicateMessage=ì¤‘ë³µëœ ë©”ì‹œì§€ë¥¼ 받았습니다. Senderì˜ ì œí•œ 시간 초과 ê°’ì´ ë„ˆë¬´ 작게 설정ë˜ì—ˆë‚˜ìš”? 컨í…스트: [{0}], 파ì¼ëª…: [{1}], ë°ì´í„°: [{2}], ë°ì´í„° 길ì´: [{3}] + +fileNewFail=[{0}]ì„(를) ìƒì„±í•  수 없습니다. + +warWatcher.cantListWatchDir=WatchDir [{0}] ë‚´ì˜ íŒŒì¼ ëª©ë¡ì„ 구할 수 없습니다. 해당 디렉토리가 존재하는지 그리고 ì½ê¸° ê¶Œí•œì´ ìžˆëŠ”ì§€ ì ê²€í•˜ì‹­ì‹œì˜¤. +warWatcher.checkWarResult=WarInfo.check()ê°€ [{1}]ì„(를) 위해 [{0}]ì„(를) 반환했습니다. +warWatcher.checkingWar=WAR íŒŒì¼ [{0}]ì„(를) ì ê²€í•©ë‹ˆë‹¤. +warWatcher.checkingWars=[{0}] ë‚´ì˜ WARë“¤ì„ ì ê²€í•©ë‹ˆë‹¤. +warWatcher.listedFileDoesNotExist=[{0}]ì´(ê°€) [{1}]ì—ì„œ íƒì§€ë˜ì—ˆìœ¼ë‚˜, 존재하지 않습니다. [{1}]ì— ëŒ€í•œ 디렉토리 ì ‘ê·¼ 허가 ì„¤ì •ì„ ì ê²€í•´ 보시겠습니까? diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_pt_BR.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..2df57e5 --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_pt_BR.properties @@ -0,0 +1,19 @@ +# 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. + +farmWarDeployer.removeFailRemote=Remoção local de [{0}] falhou, outro manager tem o app em serviço! +farmWarDeployer.servicingUndeploy=Aplicação [{0}] está sendo servida e não pode ser removida do nó do cluter de backup + +warWatcher.checkingWar=Verificando arquivo WAR [{0}] diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_ru.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_ru.properties new file mode 100644 index 0000000..80747ec --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_ru.properties @@ -0,0 +1,22 @@ +# 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. + +farmWarDeployer.modInstall=УÑтановка веб Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ [{0}] из [{1}] +farmWarDeployer.modInstallFail=Ðе возможно уÑтановить WAR файл + +fileMessageFactory.deleteFail=Ðе удалоÑÑŒ удалить [{0}] + +warWatcher.cantListWatchDir=Ðе возможно Ñоздать ÑпиÑок файлов Ð´Ð»Ñ WatchDir [{0}]: Проверьте что Ñто Ð´Ð¸Ñ€ÐµÐºÑ‚Ð¾Ñ€Ð¸Ñ Ð¸ у нее еÑÑ‚ÑŒ права на чтение. +warWatcher.checkingWar=ПроверÑем WAR файл [{}] diff --git a/java/org/apache/catalina/ha/deploy/LocalStrings_zh_CN.properties b/java/org/apache/catalina/ha/deploy/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..c5f34cf --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/LocalStrings_zh_CN.properties @@ -0,0 +1,60 @@ +# 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. + +farmWarDeployer.alreadyDeployed=webapp[{0}]已部署。 +farmWarDeployer.deleteFail=无法删除 [{0}] +farmWarDeployer.deployEnd=[{0}]中的部署已完æˆã€‚ +farmWarDeployer.fileCopyFail=无法从[{0}]å¤åˆ¶åˆ°[{1}] +farmWarDeployer.fileMessageError=处ç†æ–‡ä»¶æ¶ˆæ¯æ—¶å‡ºé”™ +farmWarDeployer.hostOnly=FarmWarDeployer åªæœ‰åšä¸º host cluster çš„å­å…ƒç´ æ˜¯æ‰ç”Ÿæ•ˆ +farmWarDeployer.hostParentEngine=FarmWarDeployeråªèƒ½åœ¨[{0}]çš„çˆ¶çº§æ˜¯å¼•æ“Žæ—¶å·¥ä½œï¼ +farmWarDeployer.mbeanNameFail=无法为引擎[{0}]和主机[{1}]构造MBean对象å +farmWarDeployer.modInstall=从 [{1}] 安装 webapp [{0}] +farmWarDeployer.modInstallFail=无法安装 WAR 文件 +farmWarDeployer.msgIoe=无法读å–æœåŠ¡å™¨åœºéƒ¨ç½²æ–‡ä»¶æ¶ˆæ¯ã€‚ +farmWarDeployer.msgRxDeploy=接收集群部署路径[{0}],war[{1}] +farmWarDeployer.msgRxUndeploy=从路径[{0}]接收未部署集群 +farmWarDeployer.removeFailLocal=从[{0}]本地移除失败 +farmWarDeployer.removeFailRemote=本地从[{0}]删除失败,其他ç»ç†æœ‰app在æœåŠ¡ï¼ +farmWarDeployer.removeLocal=正在删除webapp[{0}] +farmWarDeployer.removeLocalFail=无法移除WAR文件 +farmWarDeployer.removeStart=集群范围内删除web应用程åº[{0}] +farmWarDeployer.removeTxMsg=从[{0}]å‘é€é›†ç¾¤èŒƒå›´çš„å–消部署 +farmWarDeployer.renameFail=å°† [{0}] é‡å‘½å为 [{1}] 失败 +farmWarDeployer.sendEnd=å‘é€é›†ç¾¤war部署路径[{0}],war[{1}]已完æˆã€‚ +farmWarDeployer.sendFragment=将集群war片段路径[{0}],war[{1}]å‘é€åˆ°[{2}] +farmWarDeployer.sendStart=å‘é€é›†ç¾¤war部署路径[{0}],war[{1}]å·²å¯åŠ¨ +farmWarDeployer.servicingDeploy=应用程åº[{0}]正在æœåŠ¡ã€‚å†æ¬¡è§¦æ‘¸WAR文件[{1}]ï¼ +farmWarDeployer.servicingUndeploy=正在处ç†åº”用程åº[{0}],无法从备份集群节点中删除它 +farmWarDeployer.started=集群FarmWarDeployerå·²å¯åŠ¨ã€‚ +farmWarDeployer.stopped=集群FarmWarDeployerå·²åœæ­¢ã€‚ +farmWarDeployer.undeployEnd=从[{0}]å–消部署完æˆã€‚ +farmWarDeployer.undeployLocal=ä¸èƒ½éƒ¨ç½²æœ¬åœ°ä¸Šä¸‹æ–‡[{0}] +farmWarDeployer.undeployMessageError=处ç†å–消部署消æ¯æ—¶å‡ºé”™ +farmWarDeployer.watchDir=集群部署正在监视[{0}]的更改 + +fileMessageFactory.cannotRead=无法读å–消æ¯ï¼Œæ­¤å·¥åŽ‚正在写入 +fileMessageFactory.cannotWrite=无法写入消æ¯ï¼Œæ­¤å·¥åŽ‚æ­£åœ¨è¯»å– +fileMessageFactory.closed=工厂已ç»å…³é—­ +fileMessageFactory.deleteFail=无法删除 [{0}] +fileMessageFactory.duplicateMessage=收到é‡å¤æ¶ˆæ¯ã€‚å‘件人超时是å¦å¤ªä½Žï¼Ÿä¸Šä¸‹æ–‡ï¼š[{0}]文件å:[{1}]æ•°æ®ï¼š[{2}]æ•°æ®é•¿åº¦ï¼š[{3}] + +fileNewFail=无法创建[{0}] + +warWatcher.cantListWatchDir=无法列出WatchDir文件夹 [{0}] 中的文件:检查该路径是å¦ä¸ºç›®å½•ä¸”应用具有读å–æƒé™ã€‚ +warWatcher.checkWarResult=WarInfo.check() 为[{1}]返回[{0}] +warWatcher.checkingWar=检查 WAR 文件 [{0}] +warWatcher.checkingWars=正在检查[{0}]中的wars +warWatcher.listedFileDoesNotExist=[{1}]中检测到[{0}],但ä¸å­˜åœ¨ã€‚在[{1}]上检查目录æƒé™ï¼Ÿ diff --git a/java/org/apache/catalina/ha/deploy/UndeployMessage.java b/java/org/apache/catalina/ha/deploy/UndeployMessage.java new file mode 100644 index 0000000..63f22ad --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/UndeployMessage.java @@ -0,0 +1,65 @@ +/* + * 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.catalina.ha.deploy; + +import org.apache.catalina.ha.ClusterMessage; +import org.apache.catalina.tribes.Member; + +public class UndeployMessage implements ClusterMessage { + private static final long serialVersionUID = 2L; + + private Member address; + private long timestamp; + private String uniqueId; + private final String contextName; + + public UndeployMessage(Member address, long timestamp, String uniqueId, String contextName) { + this.address = address; + this.timestamp = timestamp; + this.uniqueId = uniqueId; + this.contextName = contextName; + } + + @Override + public Member getAddress() { + return address; + } + + @Override + public void setAddress(Member address) { + this.address = address; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @Override + public String getUniqueId() { + return uniqueId; + } + + public String getContextName() { + return contextName; + } +} diff --git a/java/org/apache/catalina/ha/deploy/WarWatcher.java b/java/org/apache/catalina/ha/deploy/WarWatcher.java new file mode 100644 index 0000000..bd9e4ad --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/WarWatcher.java @@ -0,0 +1,223 @@ +/* + * 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.catalina.ha.deploy; + +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * The WarWatcher watches the deployDir for changes made to the directory (adding new WAR files->deploy or + * remove WAR files->undeploy) and notifies a listener of the changes made. + * + * @author Peter Rossbach + */ +public class WarWatcher { + + /*--Static Variables----------------------------------------*/ + private static final Log log = LogFactory.getLog(WarWatcher.class); + private static final StringManager sm = StringManager.getManager(WarWatcher.class); + + /*--Instance Variables--------------------------------------*/ + /** + * Directory to watch for war files + */ + protected final File watchDir; + + /** + * Parent to be notified of changes + */ + protected final FileChangeListener listener; + + /** + * Currently deployed files + */ + protected final Map currentStatus = new HashMap<>(); + + /*--Constructor---------------------------------------------*/ + + public WarWatcher(FileChangeListener listener, File watchDir) { + this.listener = listener; + this.watchDir = watchDir; + } + + /*--Logic---------------------------------------------------*/ + + /** + * check for modification and send notification to listener + */ + public void check() { + if (log.isDebugEnabled()) { + log.debug(sm.getString("warWatcher.checkingWars", watchDir)); + } + File[] list = watchDir.listFiles(new WarFilter()); + if (list == null) { + log.warn(sm.getString("warWatcher.cantListWatchDir", watchDir)); + + list = new File[0]; + } + // first make sure all the files are listed in our current status + for (File file : list) { + if (!file.exists()) { + log.warn(sm.getString("warWatcher.listedFileDoesNotExist", file, watchDir)); + } + + addWarInfo(file); + } + + // Check all the status codes and update the FarmDeployer + for (Iterator> i = currentStatus.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = i.next(); + WarInfo info = entry.getValue(); + if (log.isTraceEnabled()) { + log.trace(sm.getString("warWatcher.checkingWar", info.getWar())); + } + int check = info.check(); + if (check == 1) { + listener.fileModified(info.getWar()); + } else if (check == -1) { + listener.fileRemoved(info.getWar()); + // no need to keep in memory + i.remove(); + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("warWatcher.checkWarResult", Integer.valueOf(check), info.getWar())); + } + } + + } + + /** + * add cluster war to the watcher state + * + * @param warfile The WAR to add + */ + protected void addWarInfo(File warfile) { + WarInfo info = currentStatus.get(warfile.getAbsolutePath()); + if (info == null) { + info = new WarInfo(warfile); + info.setLastState(-1); // assume file is non existent + currentStatus.put(warfile.getAbsolutePath(), info); + } + } + + /** + * clear watcher state + */ + public void clear() { + currentStatus.clear(); + } + + + /*--Inner classes-------------------------------------------*/ + + /** + * File name filter for war files + */ + protected static class WarFilter implements java.io.FilenameFilter { + @Override + public boolean accept(File path, String name) { + if (name == null) { + return false; + } + return name.endsWith(".war"); + } + } + + /** + * File information on existing WAR files + */ + protected static class WarInfo { + protected final File war; + + protected long lastChecked = 0; + + protected long lastState = 0; + + public WarInfo(File war) { + this.war = war; + this.lastChecked = war.lastModified(); + if (!war.exists()) { + lastState = -1; + } + } + + public boolean modified() { + return war.exists() && war.lastModified() > lastChecked; + } + + public boolean exists() { + return war.exists(); + } + + /** + * Returns 1 if the file has been added/modified, 0 if the file is unchanged and -1 if the file has been removed + * + * @return int 1=file added; 0=unchanged; -1=file removed + */ + public int check() { + // file unchanged by default + int result = 0; + + if (modified()) { + // file has changed - timestamp + result = 1; + lastState = result; + } else if ((!exists()) && (!(lastState == -1))) { + // file was removed + result = -1; + lastState = result; + } else if ((lastState == -1) && exists()) { + // file was added + result = 1; + lastState = result; + } + this.lastChecked = System.currentTimeMillis(); + return result; + } + + public File getWar() { + return war; + } + + @Override + public int hashCode() { + return war.getAbsolutePath().hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof WarInfo) { + WarInfo wo = (WarInfo) other; + return wo.getWar().equals(getWar()); + } else { + return false; + } + } + + protected void setLastState(int lastState) { + this.lastState = lastState; + } + + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/ha/deploy/mbeans-descriptors.xml b/java/org/apache/catalina/ha/deploy/mbeans-descriptors.xml new file mode 100644 index 0000000..7adbfee --- /dev/null +++ b/java/org/apache/catalina/ha/deploy/mbeans-descriptors.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/ha/package.html b/java/org/apache/catalina/ha/package.html new file mode 100644 index 0000000..936fb35 --- /dev/null +++ b/java/org/apache/catalina/ha/package.html @@ -0,0 +1,23 @@ + + + +

    This package contains code for Clustering, the base class +of a Cluster is org.apache.catalina.Cluster implementations +of this class is done when implementing a new Cluster protocol

    + + diff --git a/java/org/apache/catalina/ha/session/BackupManager.java b/java/org/apache/catalina/ha/session/BackupManager.java new file mode 100644 index 0000000..ce02baa --- /dev/null +++ b/java/org/apache/catalina/ha/session/BackupManager.java @@ -0,0 +1,272 @@ +/* + * 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.catalina.ha.session; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.catalina.DistributedManager; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Session; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.ClusterMessage; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.tipis.AbstractReplicatedMap.MapOwner; +import org.apache.catalina.tribes.tipis.LazyReplicatedMap; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class BackupManager extends ClusterManagerBase implements MapOwner, DistributedManager { + + private final Log log = LogFactory.getLog(BackupManager.class); // must not be static + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(BackupManager.class); + + protected static final long DEFAULT_REPL_TIMEOUT = 15000;// 15 seconds + + /** + * The name of this manager + */ + protected String name; + + /** + * Flag for how this map sends messages. + */ + private int mapSendOptions = Channel.SEND_OPTIONS_SYNCHRONIZED_ACK | Channel.SEND_OPTIONS_USE_ACK; + + /** + * Timeout for RPC messages. + */ + private long rpcTimeout = DEFAULT_REPL_TIMEOUT; + + /** + * Flag for whether to terminate this map that failed to start. + */ + private boolean terminateOnStartFailure = false; + + /** + * The timeout for a ping message in replication map. + */ + private long accessTimeout = 5000; + + /** + * Constructor, just calls super() + */ + public BackupManager() { + super(); + } + + + // ******************************************************************************/ + // ClusterManager Interface + // ******************************************************************************/ + + @Override + public void messageDataReceived(ClusterMessage msg) { + } + + @Override + public ClusterMessage requestCompleted(String sessionId) { + if (!getState().isAvailable()) { + return null; + } + LazyReplicatedMap map = (LazyReplicatedMap) sessions; + map.replicate(sessionId, false); + return null; + } + + + // ========================================================================= + // OVERRIDE THESE METHODS TO IMPLEMENT THE REPLICATION + // ========================================================================= + @Override + public void objectMadePrimary(Object key, Object value) { + if (value instanceof DeltaSession) { + DeltaSession session = (DeltaSession) value; + synchronized (session) { + session.access(); + session.setPrimarySession(true); + session.endAccess(); + } + } + } + + @Override + public Session createEmptySession() { + return new DeltaSession(this); + } + + + @Override + public String getName() { + return this.name; + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. Starts the cluster communication channel, this + * will connect with the other nodes in the cluster, and request the current session state to be transferred to this + * node. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + super.startInternal(); + + try { + if (cluster == null) { + throw new LifecycleException(sm.getString("backupManager.noCluster", getName())); + } + LazyReplicatedMap map = new LazyReplicatedMap<>(this, cluster.getChannel(), rpcTimeout, + getMapName(), getClassLoaders(), terminateOnStartFailure); + map.setChannelSendOptions(mapSendOptions); + map.setAccessTimeout(accessTimeout); + this.sessions = map; + } catch (Exception x) { + log.error(sm.getString("backupManager.startUnable", getName()), x); + throw new LifecycleException(sm.getString("backupManager.startFailed", getName()), x); + } + setState(LifecycleState.STARTING); + } + + public String getMapName() { + String name = cluster.getManagerName(getName(), this) + "-" + "map"; + if (log.isTraceEnabled()) { + log.trace("Backup manager, Setting map name to:" + name); + } + return name; + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. This will disconnect the cluster communication + * channel and stop the listener thread. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("backupManager.stopped", getName())); + } + + setState(LifecycleState.STOPPING); + + if (sessions instanceof LazyReplicatedMap) { + LazyReplicatedMap map = (LazyReplicatedMap) sessions; + map.breakdown(); + } + + super.stopInternal(); + } + + @Override + public void setName(String name) { + this.name = name; + } + + public void setMapSendOptions(int mapSendOptions) { + this.mapSendOptions = mapSendOptions; + } + + public void setMapSendOptions(String mapSendOptions) { + + int value = Channel.parseSendOptions(mapSendOptions); + if (value > 0) { + this.setMapSendOptions(value); + } + } + + public int getMapSendOptions() { + return mapSendOptions; + } + + /** + * returns the SendOptions as a comma separated list of names + * + * @return a comma separated list of the option names + */ + public String getMapSendOptionsName() { + return Channel.getSendOptionsAsString(mapSendOptions); + } + + public void setRpcTimeout(long rpcTimeout) { + this.rpcTimeout = rpcTimeout; + } + + public long getRpcTimeout() { + return rpcTimeout; + } + + public void setTerminateOnStartFailure(boolean terminateOnStartFailure) { + this.terminateOnStartFailure = terminateOnStartFailure; + } + + public boolean isTerminateOnStartFailure() { + return terminateOnStartFailure; + } + + public long getAccessTimeout() { + return accessTimeout; + } + + public void setAccessTimeout(long accessTimeout) { + this.accessTimeout = accessTimeout; + } + + @Override + public String[] getInvalidatedSessions() { + return new String[0]; + } + + @Override + public ClusterManager cloneFromTemplate() { + BackupManager result = new BackupManager(); + clone(result); + result.mapSendOptions = mapSendOptions; + result.rpcTimeout = rpcTimeout; + result.terminateOnStartFailure = terminateOnStartFailure; + result.accessTimeout = accessTimeout; + return result; + } + + @Override + public int getActiveSessionsFull() { + LazyReplicatedMap map = (LazyReplicatedMap) sessions; + return map.sizeFull(); + } + + @Override + public Set getSessionIdsFull() { + LazyReplicatedMap map = (LazyReplicatedMap) sessions; + Set sessionIds = new HashSet<>(map.keySetFull()); + return sessionIds; + } + +} diff --git a/java/org/apache/catalina/ha/session/ClusterManagerBase.java b/java/org/apache/catalina/ha/session/ClusterManagerBase.java new file mode 100644 index 0000000..b642178 --- /dev/null +++ b/java/org/apache/catalina/ha/session/ClusterManagerBase.java @@ -0,0 +1,228 @@ +/* + * 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.catalina.ha.session; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.catalina.Cluster; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Loader; +import org.apache.catalina.SessionIdGenerator; +import org.apache.catalina.Valve; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.tcp.ReplicationValve; +import org.apache.catalina.session.ManagerBase; +import org.apache.catalina.tribes.io.ReplicationStream; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.res.StringManager; + +public abstract class ClusterManagerBase extends ManagerBase implements ClusterManager { + + protected static final StringManager sm = StringManager.getManager(ClusterManagerBase.class); + private final Log log = LogFactory.getLog(ClusterManagerBase.class); // must not be static + + /** + * A reference to the cluster + */ + protected CatalinaCluster cluster = null; + + /** + * Should listeners be notified? + */ + private boolean notifyListenersOnReplication = true; + + /** + * cached replication valve cluster container! + */ + private volatile ReplicationValve replicationValve = null; + + /** + * send all actions of session attributes. + */ + private boolean recordAllActions = false; + + private SynchronizedStack deltaRequestPool = new SynchronizedStack<>(); + + + protected SynchronizedStack getDeltaRequestPool() { + return deltaRequestPool; + } + + + @Override + public CatalinaCluster getCluster() { + return cluster; + } + + @Override + public void setCluster(CatalinaCluster cluster) { + this.cluster = cluster; + } + + @Override + public boolean isNotifyListenersOnReplication() { + return notifyListenersOnReplication; + } + + public void setNotifyListenersOnReplication(boolean notifyListenersOnReplication) { + this.notifyListenersOnReplication = notifyListenersOnReplication; + } + + + public boolean isRecordAllActions() { + return recordAllActions; + } + + public void setRecordAllActions(boolean recordAllActions) { + this.recordAllActions = recordAllActions; + } + + + public static ClassLoader[] getClassLoaders(Context context) { + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + Loader loader = context.getLoader(); + ClassLoader classLoader = null; + if (loader != null) { + classLoader = loader.getClassLoader(); + } + if (classLoader == null) { + classLoader = tccl; + } + if (classLoader == tccl) { + return new ClassLoader[] { classLoader }; + } else { + return new ClassLoader[] { classLoader, tccl }; + } + } + + + public ClassLoader[] getClassLoaders() { + return getClassLoaders(getContext()); + } + + @Override + public ReplicationStream getReplicationStream(byte[] data) throws IOException { + return getReplicationStream(data, 0, data.length); + } + + @Override + public ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException { + ByteArrayInputStream fis = new ByteArrayInputStream(data, offset, length); + return new ReplicationStream(fis, getClassLoaders()); + } + + + // ---------------------------------------------------- persistence handler + + /** + * {@link org.apache.catalina.Manager} implementations that also implement {@link ClusterManager} do not support + * local session persistence. + */ + @Override + public void load() { + // NOOP + } + + /** + * {@link org.apache.catalina.Manager} implementations that also implement {@link ClusterManager} do not support + * local session persistence. + */ + @Override + public void unload() { + // NOOP + } + + protected void clone(ClusterManagerBase copy) { + copy.setName("Clone-from-" + getName()); + copy.setMaxActiveSessions(getMaxActiveSessions()); + copy.setProcessExpiresFrequency(getProcessExpiresFrequency()); + copy.setNotifyListenersOnReplication(isNotifyListenersOnReplication()); + copy.setSessionAttributeNameFilter(getSessionAttributeNameFilter()); + copy.setSessionAttributeValueClassNameFilter(getSessionAttributeValueClassNameFilter()); + copy.setWarnOnSessionAttributeFilterFailure(getWarnOnSessionAttributeFilterFailure()); + copy.setSecureRandomClass(getSecureRandomClass()); + copy.setSecureRandomProvider(getSecureRandomProvider()); + copy.setSecureRandomAlgorithm(getSecureRandomAlgorithm()); + if (getSessionIdGenerator() != null) { + try { + SessionIdGenerator copyIdGenerator = sessionIdGeneratorClass.getConstructor().newInstance(); + copyIdGenerator.setSessionIdLength(getSessionIdGenerator().getSessionIdLength()); + copyIdGenerator.setJvmRoute(getSessionIdGenerator().getJvmRoute()); + copy.setSessionIdGenerator(copyIdGenerator); + } catch (ReflectiveOperationException e) { + // Ignore + } + } + copy.setRecordAllActions(isRecordAllActions()); + } + + /** + * Register cross context session at replication valve thread local + * + * @param session cross context session + */ + protected void registerSessionAtReplicationValve(DeltaSession session) { + if (replicationValve == null) { + CatalinaCluster cluster = getCluster(); + if (cluster != null) { + Valve[] valves = cluster.getValves(); + if (valves != null && valves.length > 0) { + for (int i = 0; replicationValve == null && i < valves.length; i++) { + if (valves[i] instanceof ReplicationValve) { + replicationValve = (ReplicationValve) valves[i]; + } + } // for + + if (replicationValve == null && log.isDebugEnabled()) { + log.debug(sm.getString("clusterManager.noValve")); + } // endif + } // end if + } // endif + } // end if + if (replicationValve != null) { + replicationValve.registerReplicationSession(session); + } + } + + @Override + protected void startInternal() throws LifecycleException { + super.startInternal(); + if (getCluster() == null) { + Cluster cluster = getContext().getCluster(); + if (cluster instanceof CatalinaCluster) { + setCluster((CatalinaCluster) cluster); + } + } + if (cluster != null) { + cluster.registerManager(this); + } + } + + @Override + protected void stopInternal() throws LifecycleException { + if (cluster != null) { + cluster.removeManager(this); + } + replicationValve = null; + super.stopInternal(); + } +} diff --git a/java/org/apache/catalina/ha/session/ClusterSessionListener.java b/java/org/apache/catalina/ha/session/ClusterSessionListener.java new file mode 100644 index 0000000..efaba44 --- /dev/null +++ b/java/org/apache/catalina/ha/session/ClusterSessionListener.java @@ -0,0 +1,107 @@ +/* + * 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.catalina.ha.session; + +import java.util.Map; + +import org.apache.catalina.ha.ClusterListener; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.ClusterMessage; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Receive replicated SessionMessage form other cluster node. + * + * @author Peter Rossbach + */ +public class ClusterSessionListener extends ClusterListener { + + private static final Log log = LogFactory.getLog(ClusterSessionListener.class); + private static final StringManager sm = StringManager.getManager(ClusterSessionListener.class); + + // --Constructor--------------------------------------------- + + public ClusterSessionListener() { + // NO-OP + } + + // --Logic--------------------------------------------------- + + /** + * Callback from the cluster, when a message is received, The cluster will broadcast it invoking the messageReceived + * on the receiver. + * + * @param myobj ClusterMessage - the message received from the cluster + */ + @Override + public void messageReceived(ClusterMessage myobj) { + if (myobj instanceof SessionMessage) { + SessionMessage msg = (SessionMessage) myobj; + String ctxname = msg.getContextName(); + // check if the message is an EVT_GET_ALL_SESSIONS, + // if so, wait until we are fully started up + Map managers = cluster.getManagers(); + if (ctxname == null) { + for (Map.Entry entry : managers.entrySet()) { + if (entry.getValue() != null) { + entry.getValue().messageDataReceived(msg); + } else { + // this happens a lot before the system has started + // up + if (log.isDebugEnabled()) { + log.debug(sm.getString("clusterSessionListener.noManager", entry.getKey())); + } + } + } + } else { + ClusterManager mgr = managers.get(ctxname); + if (mgr != null) { + mgr.messageDataReceived(msg); + } else { + if (log.isWarnEnabled()) { + log.warn(sm.getString("clusterSessionListener.noManager", ctxname)); + } + + // A no context manager message is replied in order to avoid + // timeout of GET_ALL_SESSIONS sync phase. + if (msg.getEventType() == SessionMessage.EVT_GET_ALL_SESSIONS) { + SessionMessage replymsg = + new SessionMessageImpl(ctxname, SessionMessage.EVT_ALL_SESSION_NOCONTEXTMANAGER, null, + "NO-CONTEXT-MANAGER", "NO-CONTEXT-MANAGER-" + ctxname); + cluster.send(replymsg, msg.getAddress()); + } + } + } + } + } + + /** + * Accept only SessionMessage + * + * @param msg ClusterMessage + * + * @return boolean - returns true to indicate that messageReceived should be invoked. If false is returned, the + * messageReceived method will not be invoked. + */ + @Override + public boolean accept(ClusterMessage msg) { + return msg instanceof SessionMessage; + } +} + diff --git a/java/org/apache/catalina/ha/session/DeltaManager.java b/java/org/apache/catalina/ha/session/DeltaManager.java new file mode 100644 index 0000000..3fd0b96 --- /dev/null +++ b/java/org/apache/catalina/ha/session/DeltaManager.java @@ -0,0 +1,1373 @@ +/* + * 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.catalina.ha.session; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Date; + +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Session; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.ClusterMessage; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.io.ReplicationStream; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * The DeltaManager manages replicated sessions by only replicating the deltas in data. For applications written to + * handle this, the DeltaManager is the optimal way of replicating data. + *

    + * This code is almost identical to StandardManager with a difference in how it persists sessions and some modifications + * to it. + *

    + * IMPLEMENTATION NOTE : Correct behavior of session storing and reloading depends upon external calls to the + * start() and stop() methods of this class at the correct times. + * + * @author Craig R. McClanahan + * @author Peter Rossbach + */ +public class DeltaManager extends ClusterManagerBase { + + // ---------------------------------------------------- Security Classes + public final Log log = LogFactory.getLog(DeltaManager.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(DeltaManager.class); + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + // ----------------------------------------------------- Instance Variables + + protected String name = null; + + private boolean expireSessionsOnShutdown = false; + private boolean notifySessionListenersOnReplication = true; + private boolean notifyContainerListenersOnReplication = true; + private volatile boolean stateTransferred = false; + private volatile boolean noContextManagerReceived = false; + private int stateTransferTimeout = 60; + private boolean sendAllSessions = true; + private int sendAllSessionsSize = 1000; + + /** + * wait time between send session block (default 2 sec) + */ + private int sendAllSessionsWaitTime = 2 * 1000; + private final ArrayList receivedMessageQueue = new ArrayList<>(); + private boolean receiverQueue = false; + private boolean stateTimestampDrop = true; + private volatile long stateTransferCreateSendTime; + + // -------------------------------------------------------- stats attributes + + private volatile long sessionReplaceCounter = 0; + private volatile long counterReceive_EVT_GET_ALL_SESSIONS = 0; + private volatile long counterReceive_EVT_ALL_SESSION_DATA = 0; + private volatile long counterReceive_EVT_SESSION_CREATED = 0; + private volatile long counterReceive_EVT_SESSION_EXPIRED = 0; + private volatile long counterReceive_EVT_SESSION_ACCESSED = 0; + private volatile long counterReceive_EVT_SESSION_DELTA = 0; + private volatile int counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0; + private volatile long counterReceive_EVT_CHANGE_SESSION_ID = 0; + private volatile long counterReceive_EVT_ALL_SESSION_NOCONTEXTMANAGER = 0; + private volatile long counterSend_EVT_GET_ALL_SESSIONS = 0; + private volatile long counterSend_EVT_ALL_SESSION_DATA = 0; + private volatile long counterSend_EVT_SESSION_CREATED = 0; + private volatile long counterSend_EVT_SESSION_DELTA = 0; + private volatile long counterSend_EVT_SESSION_ACCESSED = 0; + private volatile long counterSend_EVT_SESSION_EXPIRED = 0; + private volatile int counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0; + private volatile long counterSend_EVT_CHANGE_SESSION_ID = 0; + private volatile int counterNoStateTransferred = 0; + + + // ------------------------------------------------------------- Constructor + public DeltaManager() { + super(); + } + + // ------------------------------------------------------------- Properties + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + /** + * @return Returns the counterSend_EVT_GET_ALL_SESSIONS. + */ + public long getCounterSend_EVT_GET_ALL_SESSIONS() { + return counterSend_EVT_GET_ALL_SESSIONS; + } + + /** + * @return Returns the counterSend_EVT_SESSION_ACCESSED. + */ + public long getCounterSend_EVT_SESSION_ACCESSED() { + return counterSend_EVT_SESSION_ACCESSED; + } + + /** + * @return Returns the counterSend_EVT_SESSION_CREATED. + */ + public long getCounterSend_EVT_SESSION_CREATED() { + return counterSend_EVT_SESSION_CREATED; + } + + /** + * @return Returns the counterSend_EVT_SESSION_DELTA. + */ + public long getCounterSend_EVT_SESSION_DELTA() { + return counterSend_EVT_SESSION_DELTA; + } + + /** + * @return Returns the counterSend_EVT_SESSION_EXPIRED. + */ + public long getCounterSend_EVT_SESSION_EXPIRED() { + return counterSend_EVT_SESSION_EXPIRED; + } + + /** + * @return Returns the counterSend_EVT_ALL_SESSION_DATA. + */ + public long getCounterSend_EVT_ALL_SESSION_DATA() { + return counterSend_EVT_ALL_SESSION_DATA; + } + + /** + * @return Returns the counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE. + */ + public int getCounterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE() { + return counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE; + } + + /** + * @return Returns the counterSend_EVT_CHANGE_SESSION_ID. + */ + public long getCounterSend_EVT_CHANGE_SESSION_ID() { + return counterSend_EVT_CHANGE_SESSION_ID; + } + + /** + * @return Returns the counterReceive_EVT_ALL_SESSION_DATA. + */ + public long getCounterReceive_EVT_ALL_SESSION_DATA() { + return counterReceive_EVT_ALL_SESSION_DATA; + } + + /** + * @return Returns the counterReceive_EVT_GET_ALL_SESSIONS. + */ + public long getCounterReceive_EVT_GET_ALL_SESSIONS() { + return counterReceive_EVT_GET_ALL_SESSIONS; + } + + /** + * @return Returns the counterReceive_EVT_SESSION_ACCESSED. + */ + public long getCounterReceive_EVT_SESSION_ACCESSED() { + return counterReceive_EVT_SESSION_ACCESSED; + } + + /** + * @return Returns the counterReceive_EVT_SESSION_CREATED. + */ + public long getCounterReceive_EVT_SESSION_CREATED() { + return counterReceive_EVT_SESSION_CREATED; + } + + /** + * @return Returns the counterReceive_EVT_SESSION_DELTA. + */ + public long getCounterReceive_EVT_SESSION_DELTA() { + return counterReceive_EVT_SESSION_DELTA; + } + + /** + * @return Returns the counterReceive_EVT_SESSION_EXPIRED. + */ + public long getCounterReceive_EVT_SESSION_EXPIRED() { + return counterReceive_EVT_SESSION_EXPIRED; + } + + + /** + * @return Returns the counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE. + */ + public int getCounterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE() { + return counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE; + } + + /** + * @return Returns the counterReceive_EVT_CHANGE_SESSION_ID. + */ + public long getCounterReceive_EVT_CHANGE_SESSION_ID() { + return counterReceive_EVT_CHANGE_SESSION_ID; + } + + /** + * @return Returns the counterReceive_EVT_ALL_SESSION_NOCONTEXTMANAGER. + */ + public long getCounterReceive_EVT_ALL_SESSION_NOCONTEXTMANAGER() { + return counterReceive_EVT_ALL_SESSION_NOCONTEXTMANAGER; + } + + /** + * @return Returns the sessionReplaceCounter. + */ + public long getSessionReplaceCounter() { + return sessionReplaceCounter; + } + + /** + * @return Returns the counterNoStateTransferred. + */ + public int getCounterNoStateTransferred() { + return counterNoStateTransferred; + } + + public int getReceivedQueueSize() { + synchronized (receivedMessageQueue) { + return receivedMessageQueue.size(); + } + } + + /** + * @return Returns the stateTransferTimeout. + */ + public int getStateTransferTimeout() { + return stateTransferTimeout; + } + + /** + * @param timeoutAllSession The timeout + */ + public void setStateTransferTimeout(int timeoutAllSession) { + this.stateTransferTimeout = timeoutAllSession; + } + + /** + * @return true if the state transfer is complete. + */ + public boolean getStateTransferred() { + return stateTransferred; + } + + /** + * Set that state transferred is complete + * + * @param stateTransferred Flag value + */ + public void setStateTransferred(boolean stateTransferred) { + this.stateTransferred = stateTransferred; + } + + public boolean isNoContextManagerReceived() { + return noContextManagerReceived; + } + + public void setNoContextManagerReceived(boolean noContextManagerReceived) { + this.noContextManagerReceived = noContextManagerReceived; + } + + /** + * @return the sendAllSessionsWaitTime in msec + */ + public int getSendAllSessionsWaitTime() { + return sendAllSessionsWaitTime; + } + + /** + * @param sendAllSessionsWaitTime The sendAllSessionsWaitTime to set at msec. + */ + public void setSendAllSessionsWaitTime(int sendAllSessionsWaitTime) { + this.sendAllSessionsWaitTime = sendAllSessionsWaitTime; + } + + /** + * @return the stateTimestampDrop. + */ + public boolean isStateTimestampDrop() { + return stateTimestampDrop; + } + + /** + * @param isTimestampDrop The new flag value + */ + public void setStateTimestampDrop(boolean isTimestampDrop) { + this.stateTimestampDrop = isTimestampDrop; + } + + /** + * @return the sendAllSessions. + */ + public boolean isSendAllSessions() { + return sendAllSessions; + } + + /** + * @param sendAllSessions The sendAllSessions to set. + */ + public void setSendAllSessions(boolean sendAllSessions) { + this.sendAllSessions = sendAllSessions; + } + + /** + * @return the sendAllSessionsSize. + */ + public int getSendAllSessionsSize() { + return sendAllSessionsSize; + } + + /** + * @param sendAllSessionsSize The sendAllSessionsSize to set. + */ + public void setSendAllSessionsSize(int sendAllSessionsSize) { + this.sendAllSessionsSize = sendAllSessionsSize; + } + + /** + * @return the notifySessionListenersOnReplication. + */ + public boolean isNotifySessionListenersOnReplication() { + return notifySessionListenersOnReplication; + } + + /** + * @param notifyListenersCreateSessionOnReplication The notifySessionListenersOnReplication to set. + */ + public void setNotifySessionListenersOnReplication(boolean notifyListenersCreateSessionOnReplication) { + this.notifySessionListenersOnReplication = notifyListenersCreateSessionOnReplication; + } + + + public boolean isExpireSessionsOnShutdown() { + return expireSessionsOnShutdown; + } + + public void setExpireSessionsOnShutdown(boolean expireSessionsOnShutdown) { + this.expireSessionsOnShutdown = expireSessionsOnShutdown; + } + + public boolean isNotifyContainerListenersOnReplication() { + return notifyContainerListenersOnReplication; + } + + public void setNotifyContainerListenersOnReplication(boolean notifyContainerListenersOnReplication) { + this.notifyContainerListenersOnReplication = notifyContainerListenersOnReplication; + } + + + // --------------------------------------------------------- Public Methods + + @Override + public Session createSession(String sessionId) { + return createSession(sessionId, true); + } + + /** + * Create new session with check maxActiveSessions and send session creation to other cluster nodes. + * + * @param sessionId The session id that should be used for the session + * @param distribute true to replicate the new session + * + * @return The session + */ + public Session createSession(String sessionId, boolean distribute) { + DeltaSession session = (DeltaSession) super.createSession(sessionId); + if (distribute) { + sendCreateSession(session.getId(), session); + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.createSession.newSession", session.getId(), + Integer.valueOf(sessions.size()))); + } + return session; + } + + /** + * Send create session event to all backup node + * + * @param sessionId The session id of the session + * @param session The session object + */ + protected void sendCreateSession(String sessionId, DeltaSession session) { + if (cluster.getMembers().length > 0) { + SessionMessage msg = new SessionMessageImpl(getName(), SessionMessage.EVT_SESSION_CREATED, null, sessionId, + sessionId + "-" + System.currentTimeMillis()); + if (log.isTraceEnabled()) { + log.trace(sm.getString("deltaManager.sendMessage.newSession", name, sessionId)); + } + msg.setTimestamp(session.getCreationTime()); + counterSend_EVT_SESSION_CREATED++; + send(msg); + } + } + + /** + * Send messages to other backup member (domain or all) + * + * @param msg Session message + */ + protected void send(SessionMessage msg) { + if (cluster != null) { + cluster.send(msg); + } + } + + /** + * {@inheritDoc} + *

    + * Creates new DeltaSession instance. + */ + @Override + public Session createEmptySession() { + return new DeltaSession(this); + } + + @Override + public String rotateSessionId(Session session) { + return rotateSessionId(session, true); + } + + @Override + public void changeSessionId(Session session, String newId) { + changeSessionId(session, newId, true); + } + + protected String rotateSessionId(Session session, boolean notify) { + String orgSessionID = session.getId(); + String newId = super.rotateSessionId(session); + if (notify) { + sendChangeSessionId(session.getId(), orgSessionID); + } + return newId; + } + + protected void changeSessionId(Session session, String newId, boolean notify) { + String orgSessionID = session.getId(); + super.changeSessionId(session, newId); + if (notify) { + sendChangeSessionId(session.getId(), orgSessionID); + } + } + + protected void sendChangeSessionId(String newSessionID, String orgSessionID) { + if (cluster.getMembers().length > 0) { + try { + // serialize sessionID + byte[] data = serializeSessionId(newSessionID); + // notify change sessionID + SessionMessage msg = new SessionMessageImpl(getName(), SessionMessage.EVT_CHANGE_SESSION_ID, data, + orgSessionID, orgSessionID + "-" + System.currentTimeMillis()); + msg.setTimestamp(System.currentTimeMillis()); + counterSend_EVT_CHANGE_SESSION_ID++; + send(msg); + } catch (IOException e) { + log.error(sm.getString("deltaManager.unableSerializeSessionID", newSessionID), e); + } + } + } + + /** + * serialize sessionID + * + * @param sessionId Session id to serialize + * + * @return byte array with serialized session id + * + * @throws IOException if an input/output error occurs + */ + protected byte[] serializeSessionId(String sessionId) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeUTF(sessionId); + oos.flush(); + oos.close(); + return bos.toByteArray(); + } + + /** + * Load sessionID + * + * @param data serialized session id + * + * @return session id + * + * @throws IOException if an input/output error occurs + */ + protected String deserializeSessionId(byte[] data) throws IOException { + ReplicationStream ois = getReplicationStream(data); + String sessionId = ois.readUTF(); + ois.close(); + return sessionId; + } + + /** + * Load sessions from other cluster node. + *

    + * FIXME replace currently sessions with same id without notification. + *

    + * FIXME SSO handling is not really correct with the session replacement! + * + * @param data Serialized data + * + * @exception ClassNotFoundException if a serialized class cannot be found during the reload + * @exception IOException if an input/output error occurs + */ + protected void deserializeSessions(byte[] data) throws ClassNotFoundException, IOException { + + // Open an input stream to the specified pathname, if any + // Load the previously unloaded active sessions + try (ObjectInputStream ois = getReplicationStream(data)) { + Integer count = (Integer) ois.readObject(); + int n = count.intValue(); + for (int i = 0; i < n; i++) { + DeltaSession session = (DeltaSession) createEmptySession(); + session.readObjectData(ois); + session.setManager(this); + session.setValid(true); + session.setPrimarySession(false); + // in case the nodes in the cluster are out of + // time synch, this will make sure that we have the + // correct timestamp, isValid returns true, cause + // accessCount=1 + session.access(); + // make sure that the session gets ready to expire if + // needed + session.setAccessCount(0); + session.resetDeltaRequest(); + // FIXME How inform other session id cache like SingleSignOn + if (findSession(session.getIdInternal()) != null) { + sessionReplaceCounter++; + // FIXME better is to grap this sessions again ! + if (log.isWarnEnabled()) { + log.warn(sm.getString("deltaManager.loading.existing.session", session.getIdInternal())); + } + } + add(session); + if (notifySessionListenersOnReplication) { + session.tellNew(); + } + } + } catch (ClassNotFoundException e) { + log.error(sm.getString("deltaManager.loading.cnfe", e), e); + throw e; + } catch (IOException e) { + log.error(sm.getString("deltaManager.loading.ioe", e), e); + throw e; + } + } + + + /** + * Save any currently active sessions in the appropriate persistence mechanism, if any. If persistence is not + * supported, this method returns without doing anything. + * + * @param currentSessions Sessions to serialize + * + * @return serialized data + * + * @exception IOException if an input/output error occurs + */ + protected byte[] serializeSessions(Session[] currentSessions) throws IOException { + + // Open an output stream to the specified pathname, if any + ByteArrayOutputStream fos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(fos))) { + oos.writeObject(Integer.valueOf(currentSessions.length)); + for (Session currentSession : currentSessions) { + ((DeltaSession) currentSession).writeObjectData(oos); + } + // Flush and close the output stream + oos.flush(); + } catch (IOException e) { + log.error(sm.getString("deltaManager.unloading.ioe", e), e); + throw e; + } + + // send object data as byte[] + return fos.toByteArray(); + } + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + super.startInternal(); + + // Load unloaded sessions, if any + try { + if (cluster == null) { + log.error(sm.getString("deltaManager.noCluster", getName())); + return; + } else { + if (log.isInfoEnabled()) { + String type = "unknown"; + if (cluster.getContainer() instanceof Host) { + type = "Host"; + } else if (cluster.getContainer() instanceof Engine) { + type = "Engine"; + } + log.info(sm.getString("deltaManager.registerCluster", getName(), type, cluster.getClusterName())); + } + } + if (log.isInfoEnabled()) { + log.info(sm.getString("deltaManager.startClustering", getName())); + } + + getAllClusterSessions(); + + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("deltaManager.managerLoad"), t); + } + + setState(LifecycleState.STARTING); + } + + /** + * get from first session master the backup from all clustered sessions + * + * @see #findSessionMasterMember() + */ + public synchronized void getAllClusterSessions() { + if (cluster != null && cluster.getMembers().length > 0) { + long beforeSendTime = System.currentTimeMillis(); + Member mbr = findSessionMasterMember(); + if (mbr == null) { // No domain member found + return; + } + SessionMessage msg = new SessionMessageImpl(this.getName(), SessionMessage.EVT_GET_ALL_SESSIONS, null, + "GET-ALL", "GET-ALL-" + getName()); + msg.setTimestamp(beforeSendTime); + // set reference time + stateTransferCreateSendTime = beforeSendTime; + // request session state + counterSend_EVT_GET_ALL_SESSIONS++; + stateTransferred = false; + // FIXME This send call block the deploy thread, when sender waitForAck is enabled + try { + synchronized (receivedMessageQueue) { + receiverQueue = true; + } + cluster.send(msg, mbr, Channel.SEND_OPTIONS_ASYNCHRONOUS); + if (log.isInfoEnabled()) { + log.info(sm.getString("deltaManager.waitForSessionState", getName(), mbr, + Integer.valueOf(getStateTransferTimeout()))); + } + // FIXME At sender ack mode this method check only the state + // transfer and resend is a problem! + waitForSendAllSessions(beforeSendTime); + } finally { + synchronized (receivedMessageQueue) { + for (SessionMessage smsg : receivedMessageQueue) { + if (!stateTimestampDrop) { + messageReceived(smsg, smsg.getAddress()); + } else { + if (smsg.getEventType() != SessionMessage.EVT_GET_ALL_SESSIONS && + smsg.getTimestamp() >= stateTransferCreateSendTime) { + // FIXME handle EVT_GET_ALL_SESSIONS later + messageReceived(smsg, smsg.getAddress()); + } else { + if (log.isWarnEnabled()) { + log.warn(sm.getString("deltaManager.dropMessage", getName(), + smsg.getEventTypeString(), new Date(stateTransferCreateSendTime), + new Date(smsg.getTimestamp()))); + } + } + } + } + receivedMessageQueue.clear(); + receiverQueue = false; + } + } + } else { + if (log.isInfoEnabled()) { + log.info(sm.getString("deltaManager.noMembers", getName())); + } + } + } + + /** + * Find the master of the session state + * + * @return master member of sessions + */ + protected Member findSessionMasterMember() { + Member mbr = null; + Member mbrs[] = cluster.getMembers(); + if (mbrs.length != 0) { + mbr = mbrs[0]; + } + if (mbr == null && log.isWarnEnabled()) { + log.warn(sm.getString("deltaManager.noMasterMember", getName(), "")); + } + if (mbr != null && log.isTraceEnabled()) { + log.trace(sm.getString("deltaManager.foundMasterMember", getName(), mbr)); + } + return mbr; + } + + /** + * Wait that cluster session state is transferred or timeout after 60 Sec With stateTransferTimeout == -1 wait that + * backup is transferred (forever mode) + * + * @param beforeSendTime Start instant of the operation + */ + protected void waitForSendAllSessions(long beforeSendTime) { + long reqStart = System.currentTimeMillis(); + long reqNow = reqStart; + boolean isTimeout = false; + if (getStateTransferTimeout() > 0) { + // wait that state is transferred with timeout check + do { + try { + Thread.sleep(100); + } catch (Exception sleep) { + // + } + reqNow = System.currentTimeMillis(); + isTimeout = ((reqNow - reqStart) > (1000L * getStateTransferTimeout())); + } while ((!getStateTransferred()) && (!isTimeout) && (!isNoContextManagerReceived())); + } else { + if (getStateTransferTimeout() == -1) { + // wait that state is transferred + do { + try { + Thread.sleep(100); + } catch (Exception sleep) { + } + } while ((!getStateTransferred()) && (!isNoContextManagerReceived())); + reqNow = System.currentTimeMillis(); + } + } + if (isTimeout) { + counterNoStateTransferred++; + log.error(sm.getString("deltaManager.noSessionState", getName(), new Date(beforeSendTime), + Long.valueOf(reqNow - beforeSendTime))); + } else if (isNoContextManagerReceived()) { + if (log.isWarnEnabled()) { + log.warn(sm.getString("deltaManager.noContextManager", getName(), new Date(beforeSendTime), + Long.valueOf(reqNow - beforeSendTime))); + } + } else { + if (log.isInfoEnabled()) { + log.info(sm.getString("deltaManager.sessionReceived", getName(), new Date(beforeSendTime), + Long.valueOf(reqNow - beforeSendTime))); + } + } + } + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.stopped", getName())); + } + + setState(LifecycleState.STOPPING); + + // Expire all active sessions + if (log.isInfoEnabled()) { + log.info(sm.getString("deltaManager.expireSessions", getName())); + } + Session sessions[] = findSessions(); + for (Session value : sessions) { + DeltaSession session = (DeltaSession) value; + if (!session.isValid()) { + continue; + } + try { + session.expire(true, isExpireSessionsOnShutdown()); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + } + + // Require a new random number generator if we are restarted + super.stopInternal(); + } + + // -------------------------------------------------------- Replication + // Methods + + /** + * A message was received from another node, this is the callback method to implement if you are interested in + * receiving replication messages. + * + * @param cmsg - the message received. + */ + @Override + public void messageDataReceived(ClusterMessage cmsg) { + if (cmsg instanceof SessionMessage) { + SessionMessage msg = (SessionMessage) cmsg; + switch (msg.getEventType()) { + case SessionMessage.EVT_GET_ALL_SESSIONS: + case SessionMessage.EVT_SESSION_CREATED: + case SessionMessage.EVT_SESSION_EXPIRED: + case SessionMessage.EVT_SESSION_ACCESSED: + case SessionMessage.EVT_SESSION_DELTA: + case SessionMessage.EVT_CHANGE_SESSION_ID: + synchronized (receivedMessageQueue) { + if (receiverQueue) { + receivedMessageQueue.add(msg); + return; + } + } + break; + default: + // we didn't queue, do nothing + break; + } // switch + + messageReceived(msg, msg.getAddress()); + } + } + + /** + * When the request has been completed, the replication valve will notify the manager, and the manager will decide + * whether any replication is needed or not. If there is a need for replication, the manager will create a session + * message and that will be replicated. The cluster determines where it gets sent. + * + * @param sessionId - the sessionId that just completed. + * + * @return a SessionMessage to be sent, + */ + @Override + public ClusterMessage requestCompleted(String sessionId) { + return requestCompleted(sessionId, false); + } + + /** + * When the request has been completed, the replication valve will notify the manager, and the manager will decide + * whether any replication is needed or not. If there is a need for replication, the manager will create a session + * message and that will be replicated. The cluster determines where it gets sent. Session expiration also calls + * this method, but with expires == true. + * + * @param sessionId - the sessionId that just completed. + * @param expires - whether this method has been called during session expiration + * + * @return a SessionMessage to be sent, + */ + public ClusterMessage requestCompleted(String sessionId, boolean expires) { + DeltaSession session = null; + SessionMessage msg = null; + try { + session = (DeltaSession) findSession(sessionId); + if (session == null) { + // A parallel request has called session.invalidate() which has + // removed the session from the Manager. + return null; + } + if (session.isDirty()) { + counterSend_EVT_SESSION_DELTA++; + msg = new SessionMessageImpl(getName(), SessionMessage.EVT_SESSION_DELTA, session.getDiff(), sessionId, + sessionId + "-" + System.currentTimeMillis()); + } + } catch (IOException x) { + log.error(sm.getString("deltaManager.createMessage.unableCreateDeltaRequest", sessionId), x); + return null; + } + if (msg == null) { + if (!expires && !session.isPrimarySession()) { + counterSend_EVT_SESSION_ACCESSED++; + msg = new SessionMessageImpl(getName(), SessionMessage.EVT_SESSION_ACCESSED, null, sessionId, + sessionId + "-" + System.currentTimeMillis()); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.createMessage.accessChangePrimary", getName(), sessionId)); + } + } + } else { // log only outside synch block! + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.createMessage.delta", getName(), sessionId)); + } + } + if (!expires) { + session.setPrimarySession(true); + } + // check to see if we need to send out an access message + if (!expires && (msg == null)) { + long replDelta = System.currentTimeMillis() - session.getLastTimeReplicated(); + if (session.getMaxInactiveInterval() >= 0 && replDelta > (session.getMaxInactiveInterval() * 1000L)) { + counterSend_EVT_SESSION_ACCESSED++; + msg = new SessionMessageImpl(getName(), SessionMessage.EVT_SESSION_ACCESSED, null, sessionId, + sessionId + "-" + System.currentTimeMillis()); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.createMessage.access", getName(), sessionId)); + } + } + } + + // update last replicated time + if (msg != null) { + session.setLastTimeReplicated(System.currentTimeMillis()); + msg.setTimestamp(session.getLastTimeReplicated()); + } + return msg; + } + + /** + * Reset manager statistics + */ + public synchronized void resetStatistics() { + processingTime = 0; + expiredSessions.set(0); + synchronized (sessionCreationTiming) { + sessionCreationTiming.clear(); + while (sessionCreationTiming.size() < TIMING_STATS_CACHE_SIZE) { + sessionCreationTiming.add(null); + } + } + synchronized (sessionExpirationTiming) { + sessionExpirationTiming.clear(); + while (sessionExpirationTiming.size() < TIMING_STATS_CACHE_SIZE) { + sessionExpirationTiming.add(null); + } + } + rejectedSessions = 0; + sessionReplaceCounter = 0; + counterNoStateTransferred = 0; + setMaxActive(getActiveSessions()); + counterReceive_EVT_ALL_SESSION_DATA = 0; + counterReceive_EVT_GET_ALL_SESSIONS = 0; + counterReceive_EVT_SESSION_ACCESSED = 0; + counterReceive_EVT_SESSION_CREATED = 0; + counterReceive_EVT_SESSION_DELTA = 0; + counterReceive_EVT_SESSION_EXPIRED = 0; + counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0; + counterReceive_EVT_CHANGE_SESSION_ID = 0; + counterSend_EVT_ALL_SESSION_DATA = 0; + counterSend_EVT_GET_ALL_SESSIONS = 0; + counterSend_EVT_SESSION_ACCESSED = 0; + counterSend_EVT_SESSION_CREATED = 0; + counterSend_EVT_SESSION_DELTA = 0; + counterSend_EVT_SESSION_EXPIRED = 0; + counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0; + counterSend_EVT_CHANGE_SESSION_ID = 0; + + } + + // -------------------------------------------------------- expire + + /** + * send session expired to other cluster nodes + * + * @param id session id + */ + protected void sessionExpired(String id) { + if (cluster.getMembers().length > 0) { + counterSend_EVT_SESSION_EXPIRED++; + SessionMessage msg = new SessionMessageImpl(getName(), SessionMessage.EVT_SESSION_EXPIRED, null, id, + id + "-EXPIRED-MSG"); + msg.setTimestamp(System.currentTimeMillis()); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.createMessage.expire", getName(), id)); + } + send(msg); + } + } + + /** + * Expire all find sessions. + */ + public void expireAllLocalSessions() { + Session sessions[] = findSessions(); + int expireDirect = 0; + int expireIndirect = 0; + + long timeNow = 0; + if (log.isTraceEnabled()) { + timeNow = System.currentTimeMillis(); + log.trace("Start expire all sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length); + } + for (Session value : sessions) { + if (value instanceof DeltaSession) { + DeltaSession session = (DeltaSession) value; + if (session.isPrimarySession()) { + if (session.isValid()) { + session.expire(); + expireDirect++; + } else { + expireIndirect++; + } // end if + } // end if + } // end if + } // for + if (log.isTraceEnabled()) { + long timeEnd = System.currentTimeMillis(); + log.trace("End expire sessions " + getName() + " expire processingTime " + (timeEnd - timeNow) + + " expired direct sessions: " + expireDirect + " expired direct sessions: " + expireIndirect); + } + } + + @Override + public String[] getInvalidatedSessions() { + return EMPTY_STRING_ARRAY; + } + + // -------------------------------------------------------- message receive + + /** + * This method is called by the received thread when a SessionMessage has been received from one of the other nodes + * in the cluster. + * + * @param msg - the message received + * @param sender - the sender of the message, this is used if we receive a EVT_GET_ALL_SESSION message, so that we + * only reply to the requesting node + */ + protected void messageReceived(SessionMessage msg, Member sender) { + Thread currentThread = Thread.currentThread(); + ClassLoader contextLoader = currentThread.getContextClassLoader(); + try { + + ClassLoader[] loaders = getClassLoaders(); + currentThread.setContextClassLoader(loaders[0]); + if (log.isTraceEnabled()) { + log.trace(sm.getString("deltaManager.receiveMessage.eventType", getName(), msg.getEventTypeString(), + sender)); + } + + switch (msg.getEventType()) { + case SessionMessage.EVT_GET_ALL_SESSIONS: + handleGET_ALL_SESSIONS(msg, sender); + break; + case SessionMessage.EVT_ALL_SESSION_DATA: + handleALL_SESSION_DATA(msg, sender); + break; + case SessionMessage.EVT_ALL_SESSION_TRANSFERCOMPLETE: + handleALL_SESSION_TRANSFERCOMPLETE(msg, sender); + break; + case SessionMessage.EVT_SESSION_CREATED: + handleSESSION_CREATED(msg, sender); + break; + case SessionMessage.EVT_SESSION_EXPIRED: + handleSESSION_EXPIRED(msg, sender); + break; + case SessionMessage.EVT_SESSION_ACCESSED: + handleSESSION_ACCESSED(msg, sender); + break; + case SessionMessage.EVT_SESSION_DELTA: + handleSESSION_DELTA(msg, sender); + break; + case SessionMessage.EVT_CHANGE_SESSION_ID: + handleCHANGE_SESSION_ID(msg, sender); + break; + case SessionMessage.EVT_ALL_SESSION_NOCONTEXTMANAGER: + handleALL_SESSION_NOCONTEXTMANAGER(msg, sender); + break; + default: + // we didn't recognize the message type, do nothing + break; + } // switch + } catch (Exception x) { + log.error(sm.getString("deltaManager.receiveMessage.error", getName()), x); + } finally { + currentThread.setContextClassLoader(contextLoader); + } + } + + // -------------------------------------------------------- message receiver handler + + + /** + * handle receive session state is complete transferred + * + * @param msg Session message + * @param sender Member which sent the message + */ + protected void handleALL_SESSION_TRANSFERCOMPLETE(SessionMessage msg, Member sender) { + counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE++; + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.transfercomplete", getName(), sender.getHost(), + Integer.valueOf(sender.getPort()))); + } + stateTransferCreateSendTime = msg.getTimestamp(); + stateTransferred = true; + } + + /** + * handle receive session delta + * + * @param msg Session message + * @param sender Member which sent the message + * + * @throws IOException IO error with serialization + * @throws ClassNotFoundException Serialization error + */ + protected void handleSESSION_DELTA(SessionMessage msg, Member sender) throws IOException, ClassNotFoundException { + counterReceive_EVT_SESSION_DELTA++; + byte[] delta = msg.getSession(); + DeltaSession session = (DeltaSession) findSession(msg.getSessionID()); + if (session == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.delta.unknown", getName(), msg.getSessionID())); + } + } else { + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.delta", getName(), msg.getSessionID())); + } + + session.deserializeAndExecuteDeltaRequest(delta); + } + } + + /** + * handle receive session is access at other node ( primary session is now false) + * + * @param msg Session message + * @param sender Member which sent the message + * + * @throws IOException Propagated IO error + */ + protected void handleSESSION_ACCESSED(SessionMessage msg, Member sender) throws IOException { + counterReceive_EVT_SESSION_ACCESSED++; + DeltaSession session = (DeltaSession) findSession(msg.getSessionID()); + if (session != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.accessed", getName(), msg.getSessionID())); + } + session.access(); + session.setPrimarySession(false); + session.endAccess(); + } + } + + /** + * handle receive session is expire at other node ( expire session also here) + * + * @param msg Session message + * @param sender Member which sent the message + * + * @throws IOException Propagated IO error + */ + protected void handleSESSION_EXPIRED(SessionMessage msg, Member sender) throws IOException { + counterReceive_EVT_SESSION_EXPIRED++; + DeltaSession session = (DeltaSession) findSession(msg.getSessionID()); + if (session != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.expired", getName(), msg.getSessionID())); + } + session.expire(notifySessionListenersOnReplication, false); + } + } + + /** + * handle receive new session is created at other node (create backup - primary false) + * + * @param msg Session message + * @param sender Member which sent the message + */ + protected void handleSESSION_CREATED(SessionMessage msg, Member sender) { + counterReceive_EVT_SESSION_CREATED++; + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.createNewSession", getName(), msg.getSessionID())); + } + DeltaSession session = (DeltaSession) createEmptySession(); + session.setValid(true); + session.setPrimarySession(false); + session.setCreationTime(msg.getTimestamp()); + // use container maxInactiveInterval so that session will expire correctly + // in case of primary transfer + session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60, false); + session.access(); + session.setId(msg.getSessionID(), notifySessionListenersOnReplication); + session.endAccess(); + + } + + /** + * handle receive sessions from other not ( restart ) + * + * @param msg Session message + * @param sender Member which sent the message + * + * @throws ClassNotFoundException Serialization error + * @throws IOException IO error with serialization + */ + protected void handleALL_SESSION_DATA(SessionMessage msg, Member sender) + throws ClassNotFoundException, IOException { + counterReceive_EVT_ALL_SESSION_DATA++; + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.allSessionDataBegin", getName())); + } + byte[] data = msg.getSession(); + deserializeSessions(data); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.allSessionDataAfter", getName())); + } + // stateTransferred = true; + } + + /** + * Handle a get all sessions message from another node. Depending on {@link #sendAllSessions}, sessions are either + * sent in a single message or in batches. Sending is complete when this method exits. + * + * @param msg Session message + * @param sender Member which sent the message + * + * @throws IOException IO error sending messages + */ + protected void handleGET_ALL_SESSIONS(SessionMessage msg, Member sender) throws IOException { + counterReceive_EVT_GET_ALL_SESSIONS++; + // get a list of all the session from this manager + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.unloadingBegin", getName())); + } + // Write the number of active sessions, followed by the details + // get all sessions and serialize without sync + Session[] currentSessions = findSessions(); + long findSessionTimestamp = System.currentTimeMillis(); + if (isSendAllSessions()) { + sendSessions(sender, currentSessions, findSessionTimestamp); + } else { + // send sessions in batches + int remain = currentSessions.length; + for (int i = 0; i < currentSessions.length; i += getSendAllSessionsSize()) { + int len = i + getSendAllSessionsSize() > currentSessions.length ? currentSessions.length - i : + getSendAllSessionsSize(); + Session[] sendSessions = new Session[len]; + System.arraycopy(currentSessions, i, sendSessions, 0, len); + sendSessions(sender, sendSessions, findSessionTimestamp); + remain = remain - len; + if (getSendAllSessionsWaitTime() > 0 && remain > 0) { + try { + Thread.sleep(getSendAllSessionsWaitTime()); + } catch (Exception sleep) { + } + } + } + } + + SessionMessage newmsg = new SessionMessageImpl(name, SessionMessage.EVT_ALL_SESSION_TRANSFERCOMPLETE, null, + "SESSION-STATE-TRANSFERRED", "SESSION-STATE-TRANSFERRED" + getName()); + newmsg.setTimestamp(findSessionTimestamp); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.createMessage.allSessionTransferred", getName())); + } + counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE++; + cluster.send(newmsg, sender); + } + + /** + * handle receive change sessionID at other node + * + * @param msg Session message + * @param sender Member which sent the message + * + * @throws IOException IO error with serialization + */ + protected void handleCHANGE_SESSION_ID(SessionMessage msg, Member sender) throws IOException { + counterReceive_EVT_CHANGE_SESSION_ID++; + DeltaSession session = (DeltaSession) findSession(msg.getSessionID()); + if (session != null) { + String newSessionID = deserializeSessionId(msg.getSession()); + session.setPrimarySession(false); + // change session id + changeSessionId(session, newSessionID, notifySessionListenersOnReplication, + notifyContainerListenersOnReplication); + } + } + + /** + * handle receive no context manager. + * + * @param msg Session message + * @param sender Member which sent the message + */ + protected void handleALL_SESSION_NOCONTEXTMANAGER(SessionMessage msg, Member sender) { + counterReceive_EVT_ALL_SESSION_NOCONTEXTMANAGER++; + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.noContextManager", getName(), sender.getHost(), + Integer.valueOf(sender.getPort()))); + } + noContextManagerReceived = true; + } + + /** + * send a block of session to sender + * + * @param sender Sender member + * @param currentSessions Sessions to send + * @param sendTimestamp Timestamp + * + * @throws IOException IO error sending messages + */ + protected void sendSessions(Member sender, Session[] currentSessions, long sendTimestamp) throws IOException { + byte[] data = serializeSessions(currentSessions); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.receiveMessage.unloadingAfter", getName())); + } + SessionMessage newmsg = new SessionMessageImpl(name, SessionMessage.EVT_ALL_SESSION_DATA, data, "SESSION-STATE", + "SESSION-STATE-" + getName()); + newmsg.setTimestamp(sendTimestamp); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaManager.createMessage.allSessionData", getName())); + } + counterSend_EVT_ALL_SESSION_DATA++; + int sendOptions = Channel.SEND_OPTIONS_SYNCHRONIZED_ACK | Channel.SEND_OPTIONS_USE_ACK; + cluster.send(newmsg, sender, sendOptions); + } + + @Override + public ClusterManager cloneFromTemplate() { + DeltaManager result = new DeltaManager(); + clone(result); + result.expireSessionsOnShutdown = expireSessionsOnShutdown; + result.notifySessionListenersOnReplication = notifySessionListenersOnReplication; + result.notifyContainerListenersOnReplication = notifyContainerListenersOnReplication; + result.stateTransferTimeout = stateTransferTimeout; + result.sendAllSessions = sendAllSessions; + result.sendAllSessionsSize = sendAllSessionsSize; + result.sendAllSessionsWaitTime = sendAllSessionsWaitTime; + result.stateTimestampDrop = stateTimestampDrop; + return result; + } +} diff --git a/java/org/apache/catalina/ha/session/DeltaRequest.java b/java/org/apache/catalina/ha/session/DeltaRequest.java new file mode 100644 index 0000000..bbf48ba --- /dev/null +++ b/java/org/apache/catalina/ha/session/DeltaRequest.java @@ -0,0 +1,445 @@ +/* + * 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.catalina.ha.session; + +import java.io.ByteArrayOutputStream; +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.security.Principal; +import java.util.ArrayDeque; +import java.util.Deque; + +import org.apache.catalina.SessionListener; +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * This class is used to track the series of actions that happens when a request is executed. These actions will then + * translate into invocations of methods on the actual session. + *

    + * This class is NOT thread safe. One DeltaRequest per session. + */ +public class DeltaRequest implements Externalizable { + + public static final Log log = LogFactory.getLog(DeltaRequest.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(DeltaRequest.class); + + public static final int TYPE_ATTRIBUTE = 0; + public static final int TYPE_PRINCIPAL = 1; + public static final int TYPE_ISNEW = 2; + public static final int TYPE_MAXINTERVAL = 3; + public static final int TYPE_AUTHTYPE = 4; + public static final int TYPE_LISTENER = 5; + public static final int TYPE_NOTE = 6; + + public static final int ACTION_SET = 0; + public static final int ACTION_REMOVE = 1; + + public static final String NAME_PRINCIPAL = "__SET__PRINCIPAL__"; + public static final String NAME_MAXINTERVAL = "__SET__MAXINTERVAL__"; + public static final String NAME_ISNEW = "__SET__ISNEW__"; + public static final String NAME_AUTHTYPE = "__SET__AUTHTYPE__"; + public static final String NAME_LISTENER = "__SET__LISTENER__"; + + private String sessionId; + private final Deque actions = new ArrayDeque<>(); + private final Deque actionPool = new ArrayDeque<>(); + + private boolean recordAllActions = false; + + public DeltaRequest() { + + } + + public DeltaRequest(String sessionId, boolean recordAllActions) { + this.recordAllActions = recordAllActions; + if (sessionId != null) { + setSessionId(sessionId); + } + } + + + public void setAttribute(String name, Object value) { + int action = (value == null) ? ACTION_REMOVE : ACTION_SET; + addAction(TYPE_ATTRIBUTE, action, name, value); + } + + public void removeAttribute(String name) { + addAction(TYPE_ATTRIBUTE, ACTION_REMOVE, name, null); + } + + public void setNote(String name, Object value) { + int action = (value == null) ? ACTION_REMOVE : ACTION_SET; + addAction(TYPE_NOTE, action, name, value); + } + + public void removeNote(String name) { + addAction(TYPE_NOTE, ACTION_REMOVE, name, null); + } + + public void setMaxInactiveInterval(int interval) { + addAction(TYPE_MAXINTERVAL, ACTION_SET, NAME_MAXINTERVAL, Integer.valueOf(interval)); + } + + /** + * Only support principals from type {@link GenericPrincipal GenericPrincipal} + * + * @param p Session principal + * + * @see GenericPrincipal + */ + public void setPrincipal(Principal p) { + int action = (p == null) ? ACTION_REMOVE : ACTION_SET; + GenericPrincipal gp = null; + if (p != null) { + if (p instanceof GenericPrincipal) { + gp = (GenericPrincipal) p; + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaRequest.showPrincipal", p.getName(), getSessionId())); + } + } else { + log.error(sm.getString("deltaRequest.wrongPrincipalClass", p.getClass().getName())); + } + } + addAction(TYPE_PRINCIPAL, action, NAME_PRINCIPAL, gp); + } + + public void setNew(boolean n) { + int action = ACTION_SET; + addAction(TYPE_ISNEW, action, NAME_ISNEW, Boolean.valueOf(n)); + } + + public void setAuthType(String authType) { + int action = (authType == null) ? ACTION_REMOVE : ACTION_SET; + addAction(TYPE_AUTHTYPE, action, NAME_AUTHTYPE, authType); + } + + public void addSessionListener(SessionListener listener) { + addAction(TYPE_LISTENER, ACTION_SET, NAME_LISTENER, listener); + } + + public void removeSessionListener(SessionListener listener) { + addAction(TYPE_LISTENER, ACTION_REMOVE, NAME_LISTENER, listener); + } + + protected void addAction(int type, int action, String name, Object value) { + AttributeInfo info = null; + if (this.actionPool.size() > 0) { + try { + info = actionPool.removeFirst(); + } catch (Exception x) { + log.error(sm.getString("deltaRequest.removeUnable"), x); + info = new AttributeInfo(type, action, name, value); + } + info.init(type, action, name, value); + } else { + info = new AttributeInfo(type, action, name, value); + } + // if we have already done something to this attribute, make sure + // we don't send multiple actions across the wire + if (!recordAllActions) { + try { + actions.remove(info); + } catch (java.util.NoSuchElementException x) { + // do nothing, we wanted to remove it anyway + } + } + // add the action + actions.addLast(info); + } + + public void execute(DeltaSession session, boolean notifyListeners) { + if (!this.sessionId.equals(session.getId())) { + throw new IllegalArgumentException(sm.getString("deltaRequest.ssid.mismatch")); + } + session.access(); + for (AttributeInfo info : actions) { + switch (info.getType()) { + case TYPE_ATTRIBUTE: + if (info.getAction() == ACTION_SET) { + if (log.isTraceEnabled()) { + log.trace("Session.setAttribute('" + info.getName() + "', '" + info.getValue() + "')"); + } + session.setAttribute(info.getName(), info.getValue(), notifyListeners, false); + } else { + if (log.isTraceEnabled()) { + log.trace("Session.removeAttribute('" + info.getName() + "')"); + } + session.removeAttribute(info.getName(), notifyListeners, false); + } + + break; + case TYPE_ISNEW: + if (log.isTraceEnabled()) { + log.trace("Session.setNew('" + info.getValue() + "')"); + } + session.setNew(((Boolean) info.getValue()).booleanValue(), false); + break; + case TYPE_MAXINTERVAL: + if (log.isTraceEnabled()) { + log.trace("Session.setMaxInactiveInterval('" + info.getValue() + "')"); + } + session.setMaxInactiveInterval(((Integer) info.getValue()).intValue(), false); + break; + case TYPE_PRINCIPAL: + Principal p = null; + if (info.getAction() == ACTION_SET) { + p = (Principal) info.getValue(); + } + session.setPrincipal(p, false); + break; + case TYPE_AUTHTYPE: + String authType = null; + if (info.getAction() == ACTION_SET) { + authType = (String) info.getValue(); + } + session.setAuthType(authType, false); + break; + case TYPE_LISTENER: + SessionListener listener = (SessionListener) info.getValue(); + if (info.getAction() == ACTION_SET) { + session.addSessionListener(listener, false); + } else { + session.removeSessionListener(listener, false); + } + break; + case TYPE_NOTE: + if (info.getAction() == ACTION_SET) { + if (log.isTraceEnabled()) { + log.trace("Session.setNote('" + info.getName() + "', '" + info.getValue() + "')"); + } + session.setNote(info.getName(), info.getValue(), false); + } else { + if (log.isTraceEnabled()) { + log.trace("Session.removeNote('" + info.getName() + "')"); + } + session.removeNote(info.getName(), false); + } + + break; + default: + log.warn(sm.getString("deltaRequest.invalidAttributeInfoType", info)); + }// switch + } // for + session.endAccess(); + reset(); + } + + public void reset() { + while (actions.size() > 0) { + try { + AttributeInfo info = actions.removeFirst(); + info.recycle(); + actionPool.addLast(info); + } catch (Exception x) { + log.error(sm.getString("deltaRequest.removeUnable"), x); + } + } + actions.clear(); + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + if (sessionId == null) { + Exception e = new Exception(sm.getString("deltaRequest.ssid.null")); + log.error(sm.getString("deltaRequest.ssid.null"), e.fillInStackTrace()); + } + } + + public int getSize() { + return actions.size(); + } + + public void clear() { + actions.clear(); + actionPool.clear(); + } + + @Override + public void readExternal(java.io.ObjectInput in) throws IOException, ClassNotFoundException { + // sessionId - String + // recordAll - boolean + // size - int + // AttributeInfo - in an array + reset(); + sessionId = in.readUTF(); + recordAllActions = in.readBoolean(); + int cnt = in.readInt(); + for (int i = 0; i < cnt; i++) { + AttributeInfo info = null; + if (this.actionPool.size() > 0) { + try { + info = actionPool.removeFirst(); + } catch (Exception x) { + log.error(sm.getString("deltaRequest.removeUnable"), x); + info = new AttributeInfo(); + } + } else { + info = new AttributeInfo(); + } + info.readExternal(in); + actions.addLast(info); + } // for + } + + + @Override + public void writeExternal(java.io.ObjectOutput out) throws IOException { + // sessionId - String + // recordAll - boolean + // size - int + // AttributeInfo - in an array + out.writeUTF(getSessionId()); + out.writeBoolean(recordAllActions); + out.writeInt(getSize()); + for (AttributeInfo info : actions) { + info.writeExternal(out); + } + } + + /** + * serialize DeltaRequest + * + * @see DeltaRequest#writeExternal(java.io.ObjectOutput) + * + * @return serialized delta request + * + * @throws IOException IO error serializing + */ + protected byte[] serialize() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + writeExternal(oos); + oos.flush(); + oos.close(); + return bos.toByteArray(); + } + + private static class AttributeInfo implements Externalizable { + private String name = null; + private Object value = null; + private int action; + private int type; + + AttributeInfo() { + this(-1, -1, null, null); + } + + AttributeInfo(int type, int action, String name, Object value) { + super(); + init(type, action, name, value); + } + + public void init(int type, int action, String name, Object value) { + this.name = name; + this.value = value; + this.action = action; + this.type = type; + } + + public int getType() { + return type; + } + + public int getAction() { + return action; + } + + public Object getValue() { + return value; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + public String getName() { + return name; + } + + public void recycle() { + name = null; + value = null; + type = -1; + action = -1; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AttributeInfo)) { + return false; + } + AttributeInfo other = (AttributeInfo) o; + return other.getName().equals(this.getName()); + } + + @Override + public void readExternal(java.io.ObjectInput in) throws IOException, ClassNotFoundException { + // type - int + // action - int + // name - String + // hasvalue - boolean + // value - object + type = in.readInt(); + action = in.readInt(); + name = in.readUTF(); + boolean hasValue = in.readBoolean(); + if (hasValue) { + value = in.readObject(); + } + } + + @Override + public void writeExternal(java.io.ObjectOutput out) throws IOException { + // type - int + // action - int + // name - String + // hasvalue - boolean + // value - object + out.writeInt(getType()); + out.writeInt(getAction()); + out.writeUTF(getName()); + out.writeBoolean(getValue() != null); + if (getValue() != null) { + out.writeObject(getValue()); + } + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("AttributeInfo[type="); + buf.append(getType()).append(", action=").append(getAction()); + buf.append(", name=").append(getName()).append(", value=").append(getValue()); + buf.append(", addr=").append(super.toString()).append(']'); + return buf.toString(); + } + + } + +} diff --git a/java/org/apache/catalina/ha/session/DeltaSession.java b/java/org/apache/catalina/ha/session/DeltaSession.java new file mode 100644 index 0000000..6e18731 --- /dev/null +++ b/java/org/apache/catalina/ha/session/DeltaSession.java @@ -0,0 +1,922 @@ +/* + * 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.catalina.ha.session; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.io.WriteAbortedException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.catalina.Manager; +import org.apache.catalina.SessionListener; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.ClusterMessage; +import org.apache.catalina.ha.ClusterSession; +import org.apache.catalina.session.ManagerBase; +import org.apache.catalina.session.StandardSession; +import org.apache.catalina.tribes.io.ReplicationStream; +import org.apache.catalina.tribes.tipis.ReplicatedMapEntry; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.res.StringManager; + +/** + * Similar to the StandardSession except that this session will keep track of deltas during a request. + */ +public class DeltaSession extends StandardSession implements Externalizable, ClusterSession, ReplicatedMapEntry { + + public static final Log log = LogFactory.getLog(DeltaSession.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(DeltaSession.class); + + // ----------------------------------------------------- Instance Variables + + /** + * only the primary session will expire, or be able to expire due to inactivity. This is set to false as soon as I + * receive this session over the wire in a session message. That means that someone else has made a request on + * another server. + */ + private transient boolean isPrimarySession = true; + + /** + * The delta request contains all the action info + */ + private transient DeltaRequest deltaRequest = null; + + /** + * Last time the session was replicated, used for distributed expiring of session + */ + private transient long lastTimeReplicated = System.currentTimeMillis(); + + + protected final Lock diffLock = new ReentrantReadWriteLock().writeLock(); + + private long version; + + // ----------------------------------------------------------- Constructors + + public DeltaSession() { + this(null); + } + + /** + * Construct a new Session associated with the specified Manager. + * + * @param manager The manager with which this Session is associated + */ + public DeltaSession(Manager manager) { + super(manager); + boolean recordAllActions = + manager instanceof ClusterManagerBase && ((ClusterManagerBase) manager).isRecordAllActions(); + deltaRequest = createRequest(getIdInternal(), recordAllActions); + } + + private DeltaRequest createRequest() { + return createRequest(null, false); + } + + /* + * DeltaRequest instances are created via this protected method to enable sub-classes to over-ride the method to use + * custom DeltaRequest implementations. + */ + protected DeltaRequest createRequest(String sessionId, boolean recordAllActions) { + return new DeltaRequest(sessionId, recordAllActions); + } + + // ----------------------------------------------------- ReplicatedMapEntry + + @Override + public boolean isDirty() { + return deltaRequest.getSize() > 0; + } + + @Override + public boolean isDiffable() { + return true; + } + + @Override + public byte[] getDiff() throws IOException { + SynchronizedStack deltaRequestPool = null; + DeltaRequest newDeltaRequest = null; + + if (manager instanceof ClusterManagerBase) { + deltaRequestPool = ((ClusterManagerBase) manager).getDeltaRequestPool(); + newDeltaRequest = deltaRequestPool.pop(); + if (newDeltaRequest == null) { + newDeltaRequest = createRequest(null, ((ClusterManagerBase) manager).isRecordAllActions()); + } + } else { + newDeltaRequest = createRequest(); + } + + DeltaRequest oldDeltaRequest = replaceDeltaRequest(newDeltaRequest); + + byte[] result = oldDeltaRequest.serialize(); + + if (deltaRequestPool != null) { + // Only need to reset the old request if it is going to be pooled. + // Otherwise let GC do its thing. + oldDeltaRequest.reset(); + deltaRequestPool.push(oldDeltaRequest); + } + + return result; + } + + public ClassLoader[] getClassLoaders() { + if (manager instanceof ClusterManagerBase) { + return ((ClusterManagerBase) manager).getClassLoaders(); + } else if (manager instanceof ManagerBase) { + ManagerBase mb = (ManagerBase) manager; + return ClusterManagerBase.getClassLoaders(mb.getContext()); + } + return null; + } + + @Override + public void applyDiff(byte[] diff, int offset, int length) throws IOException, ClassNotFoundException { + Thread currentThread = Thread.currentThread(); + ClassLoader contextLoader = currentThread.getContextClassLoader(); + lockInternal(); + try (ObjectInputStream stream = ((ClusterManager) getManager()).getReplicationStream(diff, offset, length)) { + ClassLoader[] loaders = getClassLoaders(); + if (loaders != null && loaders.length > 0) { + currentThread.setContextClassLoader(loaders[0]); + } + deltaRequest.readExternal(stream); + deltaRequest.execute(this, ((ClusterManager) getManager()).isNotifyListenersOnReplication()); + } finally { + unlockInternal(); + currentThread.setContextClassLoader(contextLoader); + } + } + + /** + * {@inheritDoc} + *

    + * This implementation is a NO-OP. The diff is reset in {@link #getDiff()}. + */ + @Override + public void resetDiff() { + resetDeltaRequest(); + } + + /** + * {@inheritDoc} + *

    + * This implementation is a NO-OP. Any required locking takes place in the methods that make modifications. + */ + @Override + public void lock() { + // NO-OP + } + + /** + * {@inheritDoc} + *

    + * This implementation is a NO-OP. Any required unlocking takes place in the methods that make modifications. + */ + @Override + public void unlock() { + // NO-OP + } + + /** + * Lock during serialization. + */ + private void lockInternal() { + diffLock.lock(); + } + + /** + * Unlock after serialization. + */ + private void unlockInternal() { + diffLock.unlock(); + } + + @Override + public void setOwner(Object owner) { + if (owner instanceof ClusterManager && getManager() == null) { + ClusterManager cm = (ClusterManager) owner; + this.setManager(cm); + this.setValid(true); + this.setPrimarySession(false); + this.access(); + this.resetDeltaRequest(); + this.endAccess(); + } + } + + @Override + public boolean isAccessReplicate() { + long replDelta = System.currentTimeMillis() - getLastTimeReplicated(); + if (maxInactiveInterval >= 0 && replDelta > (maxInactiveInterval * 1000L)) { + return true; + } + return false; + } + + @Override + public void accessEntry() { + this.access(); + this.setPrimarySession(false); + this.endAccess(); + } + + // ----------------------------------------------------- Session Properties + + @Override + public boolean isPrimarySession() { + return isPrimarySession; + } + + @Override + public void setPrimarySession(boolean primarySession) { + this.isPrimarySession = primarySession; + } + + + @Override + public void setId(String id, boolean notify) { + super.setId(id, notify); + lockInternal(); + try { + deltaRequest.setSessionId(getIdInternal()); + } finally { + unlockInternal(); + } + } + + + @Override + public void setId(String id) { + setId(id, true); + } + + + @Override + public void setMaxInactiveInterval(int interval) { + this.setMaxInactiveInterval(interval, true); + } + + + public void setMaxInactiveInterval(int interval, boolean addDeltaRequest) { + super.maxInactiveInterval = interval; + if (addDeltaRequest) { + lockInternal(); + try { + deltaRequest.setMaxInactiveInterval(interval); + } finally { + unlockInternal(); + } + } + } + + @Override + public void setNew(boolean isNew) { + setNew(isNew, true); + } + + public void setNew(boolean isNew, boolean addDeltaRequest) { + super.setNew(isNew); + if (addDeltaRequest) { + lockInternal(); + try { + deltaRequest.setNew(isNew); + } finally { + unlockInternal(); + } + } + } + + @Override + public void setPrincipal(Principal principal) { + setPrincipal(principal, true); + } + + public void setPrincipal(Principal principal, boolean addDeltaRequest) { + lockInternal(); + try { + super.setPrincipal(principal); + if (addDeltaRequest) { + deltaRequest.setPrincipal(principal); + } + } finally { + unlockInternal(); + } + } + + @Override + public void setAuthType(String authType) { + setAuthType(authType, true); + } + + public void setAuthType(String authType, boolean addDeltaRequest) { + lockInternal(); + try { + super.setAuthType(authType); + if (addDeltaRequest) { + deltaRequest.setAuthType(authType); + } + } finally { + unlockInternal(); + } + } + + @Override + public boolean isValid() { + if (!this.isValid) { + return false; + } + if (this.expiring) { + return true; + } + if (activityCheck && accessCount.get() > 0) { + return true; + } + if (maxInactiveInterval > 0) { + int timeIdle = (int) (getIdleTimeInternal() / 1000L); + if (isPrimarySession()) { + if (timeIdle >= maxInactiveInterval) { + expire(true); + } + } else { + if (timeIdle >= (2 * maxInactiveInterval)) { + // if the session has been idle twice as long as allowed, + // the primary session has probably crashed, and no other + // requests are coming in. that is why we do this. otherwise + // we would have a memory leak + expire(true, false); + } + } + } + + return this.isValid; + } + + @Override + public void endAccess() { + super.endAccess(); + if (manager instanceof ClusterManagerBase) { + ((ClusterManagerBase) manager).registerSessionAtReplicationValve(this); + } + } + + // ------------------------------------------------- Session Public Methods + + @Override + public void expire(boolean notify) { + expire(notify, true); + } + + public void expire(boolean notify, boolean notifyCluster) { + + // Check to see if session has already been invalidated. + // Do not check expiring at this point as expire should not return until + // isValid is false + if (!isValid) { + return; + } + + synchronized (this) { + // Check again, now we are inside the sync so this code only runs once + // Double check locking - isValid needs to be volatile + if (!isValid) { + return; + } + + if (manager == null) { + return; + } + + String expiredId = getIdInternal(); + + if (notifyCluster && expiredId != null && manager instanceof DeltaManager) { + DeltaManager dmanager = (DeltaManager) manager; + CatalinaCluster cluster = dmanager.getCluster(); + ClusterMessage msg = dmanager.requestCompleted(expiredId, true); + if (msg != null) { + cluster.send(msg); + } + } + + super.expire(notify); + + if (notifyCluster) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaSession.notifying", ((ClusterManager) manager).getName(), + Boolean.valueOf(isPrimarySession()), expiredId)); + } + if (manager instanceof DeltaManager) { + ((DeltaManager) manager).sessionExpired(expiredId); + } + } + } + } + + @Override + public void recycle() { + lockInternal(); + try { + super.recycle(); + deltaRequest.clear(); + } finally { + unlockInternal(); + } + } + + + /** + * Return a string representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("DeltaSession["); + sb.append(id); + sb.append(']'); + return sb.toString(); + } + + @Override + public void addSessionListener(SessionListener listener) { + addSessionListener(listener, true); + } + + public void addSessionListener(SessionListener listener, boolean addDeltaRequest) { + lockInternal(); + try { + super.addSessionListener(listener); + if (addDeltaRequest && listener instanceof ReplicatedSessionListener) { + deltaRequest.addSessionListener(listener); + } + } finally { + unlockInternal(); + } + } + + @Override + public void removeSessionListener(SessionListener listener) { + removeSessionListener(listener, true); + } + + public void removeSessionListener(SessionListener listener, boolean addDeltaRequest) { + lockInternal(); + try { + super.removeSessionListener(listener); + if (addDeltaRequest && listener instanceof ReplicatedSessionListener) { + deltaRequest.removeSessionListener(listener); + } + } finally { + unlockInternal(); + } + } + + + // ------------------------------------------------ Session Package Methods + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + lockInternal(); + try { + readObjectData(in); + } finally { + unlockInternal(); + } + } + + + @Override + public void readObjectData(ObjectInputStream stream) throws ClassNotFoundException, IOException { + doReadObject((ObjectInput) stream); + } + + public void readObjectData(ObjectInput stream) throws ClassNotFoundException, IOException { + doReadObject(stream); + } + + @Override + public void writeObjectData(ObjectOutputStream stream) throws IOException { + writeObjectData((ObjectOutput) stream); + } + + public void writeObjectData(ObjectOutput stream) throws IOException { + doWriteObject(stream); + } + + public void resetDeltaRequest() { + lockInternal(); + try { + deltaRequest.reset(); + deltaRequest.setSessionId(getIdInternal()); + } finally { + unlockInternal(); + } + } + + /** + * Get the request. + * @return the request + * @deprecated Unused. This method will be removed in Tomcat 12. + */ + @Deprecated + public DeltaRequest getDeltaRequest() { + return deltaRequest; + } + + /** + * Replace the existing deltaRequest with the provided replacement. + * + * @param deltaRequest The new deltaRequest. Expected to be either a newly created object or an instance that has + * been reset. + * + * @return The old deltaRequest + */ + DeltaRequest replaceDeltaRequest(DeltaRequest deltaRequest) { + lockInternal(); + try { + DeltaRequest oldDeltaRequest = this.deltaRequest; + this.deltaRequest = deltaRequest; + this.deltaRequest.setSessionId(getIdInternal()); + return oldDeltaRequest; + } finally { + unlockInternal(); + } + } + + + protected void deserializeAndExecuteDeltaRequest(byte[] delta) throws IOException, ClassNotFoundException { + if (manager instanceof ClusterManagerBase) { + SynchronizedStack deltaRequestPool = ((ClusterManagerBase) manager).getDeltaRequestPool(); + + DeltaRequest newDeltaRequest = deltaRequestPool.pop(); + if (newDeltaRequest == null) { + newDeltaRequest = createRequest(null, ((ClusterManagerBase) manager).isRecordAllActions()); + } + + ReplicationStream ois = ((ClusterManagerBase) manager).getReplicationStream(delta); + newDeltaRequest.readExternal(ois); + ois.close(); + + DeltaRequest oldDeltaRequest = null; + lockInternal(); + try { + oldDeltaRequest = replaceDeltaRequest(newDeltaRequest); + newDeltaRequest.execute(this, ((ClusterManagerBase) manager).isNotifyListenersOnReplication()); + setPrimarySession(false); + } finally { + unlockInternal(); + if (oldDeltaRequest != null) { + oldDeltaRequest.reset(); + deltaRequestPool.push(oldDeltaRequest); + } + } + } + } + // ------------------------------------------------- HttpSession Properties + + // ----------------------------------------------HttpSession Public Methods + + @Override + public void removeAttribute(String name, boolean notify) { + removeAttribute(name, notify, true); + } + + public void removeAttribute(String name, boolean notify, boolean addDeltaRequest) { + // Validate our current state + if (!isValid()) { + throw new IllegalStateException(sm.getString("standardSession.removeAttribute.ise")); + } + removeAttributeInternal(name, notify, addDeltaRequest); + } + + @Override + public void setAttribute(String name, Object value) { + setAttribute(name, value, true, true); + } + + public void setAttribute(String name, Object value, boolean notify, boolean addDeltaRequest) { + + // Name cannot be null + if (name == null) { + throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.namenull")); + } + + // Null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + lockInternal(); + try { + super.setAttribute(name, value, notify); + if (addDeltaRequest && !exclude(name, value)) { + deltaRequest.setAttribute(name, value); + } + } finally { + unlockInternal(); + } + } + + + @Override + public void removeNote(String name) { + removeNote(name, true); + } + + public void removeNote(String name, boolean addDeltaRequest) { + lockInternal(); + try { + super.removeNote(name); + if (addDeltaRequest) { + deltaRequest.removeNote(name); + } + } finally { + unlockInternal(); + } + } + + + @Override + public void setNote(String name, Object value) { + setNote(name, value, true); + } + + public void setNote(String name, Object value, boolean addDeltaRequest) { + + if (value == null) { + removeNote(name, addDeltaRequest); + return; + } + + lockInternal(); + try { + super.setNote(name, value); + if (addDeltaRequest) { + deltaRequest.setNote(name, value); + } + } finally { + unlockInternal(); + } + } + + + // -------------------------------------------- HttpSession Private Methods + + @Override + protected void doReadObject(ObjectInputStream stream) throws ClassNotFoundException, IOException { + doReadObject((ObjectInput) stream); + } + + private void doReadObject(ObjectInput stream) throws ClassNotFoundException, IOException { + + // Deserialize the scalar instance variables (except Manager) + authType = null; // Transient only + creationTime = ((Long) stream.readObject()).longValue(); + lastAccessedTime = ((Long) stream.readObject()).longValue(); + maxInactiveInterval = ((Integer) stream.readObject()).intValue(); + isNew = ((Boolean) stream.readObject()).booleanValue(); + isValid = ((Boolean) stream.readObject()).booleanValue(); + thisAccessedTime = ((Long) stream.readObject()).longValue(); + version = ((Long) stream.readObject()).longValue(); + boolean hasPrincipal = stream.readBoolean(); + principal = null; + if (hasPrincipal) { + principal = (Principal) stream.readObject(); + } + + id = (String) stream.readObject(); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaSession.readSession", id)); + } + + Object nextObject = stream.readObject(); + + // Compatibility with versions that do not persist the authentication + // notes + if (!(nextObject instanceof Integer)) { + // Not an Integer so the next two objects will be + // 'expected session ID' and 'saved request' + if (nextObject != null) { + notes.put(org.apache.catalina.authenticator.Constants.SESSION_ID_NOTE, nextObject); + } + nextObject = stream.readObject(); + if (nextObject != null) { + notes.put(org.apache.catalina.authenticator.Constants.FORM_REQUEST_NOTE, nextObject); + } + + // Next object will be the number of attributes + nextObject = stream.readObject(); + } + + // Deserialize the attribute count and attribute values + if (attributes == null) { + attributes = new ConcurrentHashMap<>(); + } + int n = ((Integer) nextObject).intValue(); + boolean isValidSave = isValid; + isValid = true; + for (int i = 0; i < n; i++) { + String name = (String) stream.readObject(); + final Object value; + try { + value = stream.readObject(); + } catch (WriteAbortedException wae) { + if (wae.getCause() instanceof NotSerializableException) { + // Skip non serializable attributes + continue; + } + throw wae; + } + // Handle the case where the filter configuration was changed while + // the web application was stopped. + if (exclude(name, value)) { + continue; + } + // ConcurrentHashMap does not allow null keys or values + if (null != value) { + attributes.put(name, value); + } + } + isValid = isValidSave; + + // Session listeners + n = ((Integer) stream.readObject()).intValue(); + if (listeners == null || n > 0) { + listeners = new ArrayList<>(); + } + for (int i = 0; i < n; i++) { + SessionListener listener = (SessionListener) stream.readObject(); + listeners.add(listener); + } + + if (notes == null) { + notes = new ConcurrentHashMap<>(); + } + activate(); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + lockInternal(); + try { + doWriteObject(out); + } finally { + unlockInternal(); + } + } + + + @Override + protected void doWriteObject(ObjectOutputStream stream) throws IOException { + doWriteObject((ObjectOutput) stream); + } + + private void doWriteObject(ObjectOutput stream) throws IOException { + // Write the scalar instance variables (except Manager) + stream.writeObject(Long.valueOf(creationTime)); + stream.writeObject(Long.valueOf(lastAccessedTime)); + stream.writeObject(Integer.valueOf(maxInactiveInterval)); + stream.writeObject(Boolean.valueOf(isNew)); + stream.writeObject(Boolean.valueOf(isValid)); + stream.writeObject(Long.valueOf(thisAccessedTime)); + stream.writeObject(Long.valueOf(version)); + stream.writeBoolean(getPrincipal() instanceof Serializable); + if (getPrincipal() instanceof Serializable) { + stream.writeObject(getPrincipal()); + } + + stream.writeObject(id); + if (log.isDebugEnabled()) { + log.debug(sm.getString("deltaSession.writeSession", id)); + } + + // Write the notes associated with authentication. Without these, + // authentication can fail without sticky sessions or if there is a + // fail-over during authentication. + stream.writeObject(notes.get(org.apache.catalina.authenticator.Constants.SESSION_ID_NOTE)); + stream.writeObject(notes.get(org.apache.catalina.authenticator.Constants.FORM_REQUEST_NOTE)); + + // Accumulate the names of serializable and non-serializable attributes + String keys[] = keys(); + List saveNames = new ArrayList<>(); + List saveValues = new ArrayList<>(); + for (String key : keys) { + Object value = null; + value = attributes.get(key); + if (value != null && !exclude(key, value) && isAttributeDistributable(key, value)) { + saveNames.add(key); + saveValues.add(value); + } + } + + // Serialize the attribute count and the Serializable attributes + int n = saveNames.size(); + stream.writeObject(Integer.valueOf(n)); + for (int i = 0; i < n; i++) { + stream.writeObject(saveNames.get(i)); + try { + stream.writeObject(saveValues.get(i)); + } catch (NotSerializableException e) { + log.error(sm.getString("standardSession.notSerializable", saveNames.get(i), id), e); + } + } + + // Serializable listeners + ArrayList saveListeners = new ArrayList<>(); + for (SessionListener listener : listeners) { + if (listener instanceof ReplicatedSessionListener) { + saveListeners.add(listener); + } + } + stream.writeObject(Integer.valueOf(saveListeners.size())); + for (SessionListener listener : saveListeners) { + stream.writeObject(listener); + } + } + + + // -------------------------------------------------------- Private Methods + + protected void removeAttributeInternal(String name, boolean notify, boolean addDeltaRequest) { + lockInternal(); + try { + // Remove this attribute from our collection + Object value = attributes.get(name); + if (value == null) { + return; + } + + super.removeAttributeInternal(name, notify); + if (addDeltaRequest && !exclude(name, null)) { + deltaRequest.removeAttribute(name); + } + + } finally { + unlockInternal(); + } + } + + @Override + public long getLastTimeReplicated() { + return lastTimeReplicated; + } + + @Override + public long getVersion() { + return version; + } + + @Override + public void setLastTimeReplicated(long lastTimeReplicated) { + this.lastTimeReplicated = lastTimeReplicated; + } + + @Override + public void setVersion(long version) { + this.version = version; + } + + protected void setAccessCount(int count) { + if (accessCount == null && activityCheck) { + accessCount = new AtomicInteger(); + } + if (accessCount != null) { + accessCount.set(count); + } + } +} diff --git a/java/org/apache/catalina/ha/session/JvmRouteBinderValve.java b/java/org/apache/catalina/ha/session/JvmRouteBinderValve.java new file mode 100644 index 0000000..1742846 --- /dev/null +++ b/java/org/apache/catalina/ha/session/JvmRouteBinderValve.java @@ -0,0 +1,398 @@ +/* + * 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.catalina.ha.session; + +import java.io.IOException; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.Cluster; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.authenticator.Constants; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.ClusterValve; +import org.apache.catalina.session.ManagerBase; +import org.apache.catalina.session.PersistentManager; +import org.apache.catalina.valves.ValveBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Valve to handle Tomcat jvmRoute takeover using mod_jk module after node failure. After a node crashes, subsequent + * requests go to other cluster nodes. That incurs a drop in performance. When this Valve is enabled on a backup node + * and sees a request, which was intended for another (thus failed) node, it will rewrite the cookie jsessionid + * information to use the route to this backup cluster node, that answered the request. After the response is delivered + * to the client, all subsequent client requests will go directly to the backup node. The change of sessionid is also + * sent to all other cluster nodes. After all that, the session stickiness will work directly to the backup node and the + * traffic will not go back to the failed node after it is restarted! + *

    + * Add this Valve to your cluster definition at conf/server.xml . + * + *

    + *  <Cluster>
    + *  <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
    + *  </Cluster>
    + * 
    + * + * A Trick:
    + * You can enable this mod_jk turnover mode via JMX before you drop a node to all backup nodes! Set enable true on all + * JvmRouteBinderValve backups, disable worker at mod_jk and then drop node and restart it! Then enable mod_jk worker + * and disable JvmRouteBinderValves again. This use case means that only requested sessions are migrated. + * + * @author Peter Rossbach + */ +public class JvmRouteBinderValve extends ValveBase implements ClusterValve { + + /*--Static Variables----------------------------------------*/ + public static final Log log = LogFactory.getLog(JvmRouteBinderValve.class); + + // ------------------------------------------------------ Constructor + public JvmRouteBinderValve() { + super(true); + } + + /*--Instance Variables--------------------------------------*/ + + /** + * the cluster + */ + protected CatalinaCluster cluster; + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(JvmRouteBinderValve.class); + + /** + * enabled this component + */ + protected boolean enabled = true; + + /** + * number of session that no at this tomcat instance hosted + */ + protected long numberOfSessions = 0; + + protected String sessionIdAttribute = "org.apache.catalina.ha.session.JvmRouteOriginalSessionID"; + + + /*--Logic---------------------------------------------------*/ + + /** + * set session id attribute to failed node for request. + * + * @return Returns the sessionIdAttribute. + */ + public String getSessionIdAttribute() { + return sessionIdAttribute; + } + + /** + * get name of failed request session attribute + * + * @param sessionIdAttribute The sessionIdAttribute to set. + */ + public void setSessionIdAttribute(String sessionIdAttribute) { + this.sessionIdAttribute = sessionIdAttribute; + } + + /** + * @return Returns the number of migrated sessions. + */ + public long getNumberOfSessions() { + return numberOfSessions; + } + + /** + * @return Returns the enabled. + */ + public boolean getEnabled() { + return enabled; + } + + /** + * @param enabled The enabled to set. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Detect possible the JVMRoute change at cluster backup node.. + * + * @param request tomcat request being processed + * @param response tomcat response being processed + * + * @exception IOException if an input/output error has occurred + * @exception ServletException if a servlet error has occurred + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + if (getEnabled() && request.getContext() != null && request.getContext().getDistributable() && + !request.isAsyncDispatching()) { + // valve cluster can access manager - other cluster handle turnover + // at host level - hopefully! + Manager manager = request.getContext().getManager(); + + if (manager != null && ((manager instanceof ClusterManager && getCluster() != null && + getCluster().getManager(((ClusterManager) manager).getName()) != null) || + (manager instanceof PersistentManager))) { + handlePossibleTurnover(request); + } + } + // Pass this request on to the next valve in our pipeline + getNext().invoke(request, response); + } + + /** + * handle possible session turn over. + * + * @see JvmRouteBinderValve#handleJvmRoute(Request, String, String) + * + * @param request current request + */ + protected void handlePossibleTurnover(Request request) { + String sessionID = request.getRequestedSessionId(); + if (sessionID != null) { + long t1 = System.currentTimeMillis(); + String jvmRoute = getLocalJvmRoute(request); + if (jvmRoute == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jvmRoute.missingJvmRouteAttribute")); + } + return; + } + handleJvmRoute(request, sessionID, jvmRoute); + if (log.isTraceEnabled()) { + long t2 = System.currentTimeMillis(); + long time = t2 - t1; + log.trace(sm.getString("jvmRoute.turnoverInfo", Long.valueOf(time))); + } + } + } + + /** + * get jvmroute from engine + * + * @param request current request + * + * @return return jvmRoute from ManagerBase or null + */ + protected String getLocalJvmRoute(Request request) { + Manager manager = getManager(request); + if (manager instanceof ManagerBase) { + return ((ManagerBase) manager).getJvmRoute(); + } + return null; + } + + /** + * get ClusterManager + * + * @param request current request + * + * @return manager or null + */ + protected Manager getManager(Request request) { + Manager manager = request.getContext().getManager(); + if (log.isDebugEnabled()) { + if (manager != null) { + log.trace(sm.getString("jvmRoute.foundManager", manager, request.getContext().getName())); + } else { + log.debug(sm.getString("jvmRoute.notFoundManager", request.getContext().getName())); + } + } + return manager; + } + + /** + * @return Returns the cluster. + */ + @Override + public CatalinaCluster getCluster() { + return cluster; + } + + /** + * @param cluster The cluster to set. + */ + @Override + public void setCluster(CatalinaCluster cluster) { + this.cluster = cluster; + } + + /** + * Handle jvmRoute stickiness after tomcat instance failed. After this correction a new Cookie send to client with + * new jvmRoute and the SessionID change propagate to the other cluster nodes. + * + * @param request current request + * @param sessionId request SessionID from Cookie + * @param localJvmRoute local jvmRoute + */ + protected void handleJvmRoute(Request request, String sessionId, String localJvmRoute) { + // get requested jvmRoute. + String requestJvmRoute = null; + int index = sessionId.indexOf('.'); + if (index > 0) { + requestJvmRoute = sessionId.substring(index + 1); + } + if (requestJvmRoute != null && !requestJvmRoute.equals(localJvmRoute)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jvmRoute.failover", requestJvmRoute, localJvmRoute, sessionId)); + } + Session catalinaSession = null; + try { + catalinaSession = getManager(request).findSession(sessionId); + } catch (IOException e) { + // Hups! + } + String id = sessionId.substring(0, index); + String newSessionID = id + "." + localJvmRoute; + // OK - turnover the session and inform other cluster nodes + if (catalinaSession != null) { + changeSessionID(request, sessionId, newSessionID, catalinaSession); + numberOfSessions++; + } else { + try { + catalinaSession = getManager(request).findSession(newSessionID); + } catch (IOException e) { + // Hups! + } + if (catalinaSession != null) { + // session is rewrite at other request, rewrite this also + changeRequestSessionID(request, sessionId, newSessionID); + } else { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jvmRoute.cannotFindSession", sessionId)); + } + } + } + } + } + + /** + * change session id and send to all cluster nodes + * + * @param request current request + * @param sessionId original session id + * @param newSessionID new session id for node migration + * @param catalinaSession current session with original session id + */ + protected void changeSessionID(Request request, String sessionId, String newSessionID, Session catalinaSession) { + fireLifecycleEvent("Before session migration", catalinaSession); + getManager(request).changeSessionId(catalinaSession, newSessionID); + changeRequestSessionID(request, sessionId, newSessionID); + changeSessionAuthenticationNote(sessionId, newSessionID, catalinaSession); + fireLifecycleEvent("After session migration", catalinaSession); + if (log.isDebugEnabled()) { + log.debug(sm.getString("jvmRoute.changeSession", sessionId, newSessionID)); + } + } + + /** + * Change Request Session id + * + * @param request current request + * @param sessionId original session id + * @param newSessionID new session id for node migration + */ + protected void changeRequestSessionID(Request request, String sessionId, String newSessionID) { + request.changeSessionId(newSessionID); + + // set original sessionid at request, to allow application detect the + // change + if (sessionIdAttribute != null && !sessionIdAttribute.isEmpty()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jvmRoute.set.originalsessionid", sessionIdAttribute, sessionId)); + } + request.setAttribute(sessionIdAttribute, sessionId); + } + } + + + /** + * Change the current session ID that is stored in a session note during authentication. It is part of the CSRF + * protection. + * + * @param sessionId The original session ID + * @param newSessionID The new session ID for node migration + * @param catalinaSession The session object (that will be using the new session ID at the point this method is + * called) + */ + protected void changeSessionAuthenticationNote(String sessionId, String newSessionID, Session catalinaSession) { + if (sessionId.equals(catalinaSession.getNote(Constants.SESSION_ID_NOTE))) { + catalinaSession.setNote(Constants.SESSION_ID_NOTE, newSessionID); + } + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + if (cluster == null) { + Cluster containerCluster = getContainer().getCluster(); + if (containerCluster instanceof CatalinaCluster) { + setCluster((CatalinaCluster) containerCluster); + } + } + + if (log.isInfoEnabled()) { + log.info(sm.getString("jvmRoute.valve.started")); + if (cluster == null) { + log.info(sm.getString("jvmRoute.noCluster")); + } + } + + super.startInternal(); + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + super.stopInternal(); + + cluster = null; + numberOfSessions = 0; + if (log.isInfoEnabled()) { + log.info(sm.getString("jvmRoute.valve.stopped")); + } + + } + +} diff --git a/java/org/apache/catalina/ha/session/LocalStrings.properties b/java/org/apache/catalina/ha/session/LocalStrings.properties new file mode 100644 index 0000000..0b6a7b3 --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings.properties @@ -0,0 +1,92 @@ +# 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. + +backupManager.noCluster=no cluster associated with this context: [{0}] +backupManager.startFailed=Failed to start BackupManager: [{0}] +backupManager.startUnable=Unable to start BackupManager: [{0}] +backupManager.stopped=Manager [{0}] is stopping + +clusterManager.noValve=No ReplicationValve found for cross context support + +clusterSessionListener.noManager=Context manager doesn''t exist:[{0}] + +deltaManager.createMessage.access=Manager [{0}]: create session access message for session [{1}] +deltaManager.createMessage.accessChangePrimary=Manager [{0}]: create change primary node message for session [{1}] +deltaManager.createMessage.allSessionData=Manager [{0}] sent all session data. +deltaManager.createMessage.allSessionTransferred=Manager [{0}] sent all session data transferred +deltaManager.createMessage.delta=Manager [{0}]: create delta request message for session [{1}] +deltaManager.createMessage.expire=Manager [{0}]: create session expire message for session [{1}] +deltaManager.createMessage.unableCreateDeltaRequest=Unable to serialize delta request for sessionid [{0}] +deltaManager.createSession.newSession=Created a new DeltaSession with Id [{0}] Total count=[{1}] +deltaManager.dropMessage=Manager [{0}]: Drop message [{1}] inside GET_ALL_SESSIONS sync phase start date [{2}] message date [{3}] +deltaManager.expireSessions=Manager [{0}] expiring sessions upon shutdown +deltaManager.foundMasterMember=Found for context [{0}] the replication master member [{1}] +deltaManager.loading.cnfe=ClassNotFoundException while loading persisted sessions: [{0}] +deltaManager.loading.existing.session=overload existing session [{0}] +deltaManager.loading.ioe=IOException while loading persisted sessions: [{0}] +deltaManager.managerLoad=Exception loading sessions from persistent storage +deltaManager.noCluster=Starting... no cluster associated with this context: [{0}] +deltaManager.noContextManager=Manager [{0}]: In reply to the ''Get all session data'' message sent at [{1}], a ''No matching context manager'' message was received after [{2}] ms. +deltaManager.noMasterMember=Starting... with no other member for context [{0}] at domain [{1}] +deltaManager.noMembers=Manager [{0}]: skipping state transfer. No members active in cluster group. +deltaManager.noSessionState=Manager [{0}]: No session state sent at [{1}] received, timing out after [{2}] ms. +deltaManager.receiveMessage.accessed=Manager [{0}]: received session accessed message for session [{1}] +deltaManager.receiveMessage.allSessionDataAfter=Manager [{0}]: all session state deserialized +deltaManager.receiveMessage.allSessionDataBegin=Manager [{0}]: received all session state data +deltaManager.receiveMessage.createNewSession=Manager [{0}]: received session created message for session [{1}] +deltaManager.receiveMessage.delta=Manager [{0}]: received session delta message for session [{1}] +deltaManager.receiveMessage.delta.unknown=Manager [{0}]: received session delta for unknown session [{1}] +deltaManager.receiveMessage.error=Manager [{0}]: Unable to receive message through TCP channel +deltaManager.receiveMessage.eventType=Manager [{0}]: Received SessionMessage of type=[{1}] from [{2}] +deltaManager.receiveMessage.expired=Manager [{0}]: received session expired message for session [{1}] +deltaManager.receiveMessage.noContextManager=Manager [{0}] received from node [{1}:{2}] no context manager. +deltaManager.receiveMessage.transfercomplete=Manager [{0}] received from node [{1}:{2}] session state transferred. +deltaManager.receiveMessage.unloadingAfter=Manager [{0}]: unloading sessions complete +deltaManager.receiveMessage.unloadingBegin=Manager [{0}]: start unloading sessions +deltaManager.registerCluster=Register manager [{0}] to cluster element [{1}] with name [{2}] +deltaManager.sendMessage.newSession=Manager [{0}] send new session [{1}] +deltaManager.sessionReceived=Manager [{0}]; session state sent at [{1}] received in [{2}] ms. +deltaManager.startClustering=Starting clustering manager at [{0}] +deltaManager.stopped=Manager [{0}] is stopping +deltaManager.unableSerializeSessionID=Unable to serialize sessionID [{0}] +deltaManager.unloading.ioe=IOException while saving persisted sessions: [{0}] +deltaManager.waitForSessionState=Manager [{0}], requesting session state from [{1}]. This operation will timeout if no session state has been received within [{2}] seconds. + +deltaRequest.invalidAttributeInfoType=Invalid attribute info type=[{0}] +deltaRequest.removeUnable=Unable to remove element: +deltaRequest.showPrincipal=Principal [{0}] is set to session [{1}] +deltaRequest.ssid.mismatch=Session id mismatch, not executing the delta request +deltaRequest.ssid.null=Session Id is null for setSessionId +deltaRequest.wrongPrincipalClass=ClusterManager only support GenericPrincipal. Your realm used principal class [{0}]. + +deltaSession.notifying=Notifying cluster of session expiration: manager [{0}], primary [{1}], sessionId [{2}] +deltaSession.readSession=readObject() loading session [{0}] +deltaSession.writeSession=writeObject() storing session [{0}] + +jvmRoute.cannotFindSession=Cannot find session [{0}] +jvmRoute.changeSession=Changed session from [{0}] to [{1}] +jvmRoute.failover=Detected a failover with different jvmRoute - original route: [{0}] new one: [{1}] at session id [{2}] +jvmRoute.foundManager=Found Cluster Manager [{0}] at [{1}] +jvmRoute.missingJvmRouteAttribute=No engine jvmRoute attribute configured! +jvmRoute.noCluster=The JvmRouterBinderValve is configured, but clustering is not being used. Fail over will still work, providing a PersistentManager is used. +jvmRoute.notFoundManager=Not found Cluster Manager at [{0}] +jvmRoute.set.originalsessionid=Set Original Session id at request attribute [{0}] value: [{1}] +jvmRoute.turnoverInfo=Turnover Check time [{0}] msec +jvmRoute.valve.started=JvmRouteBinderValve started +jvmRoute.valve.stopped=JvmRouteBinderValve stopped + +standardSession.notSerializable=Cannot serialize session attribute [{0}] for session [{1}] +standardSession.removeAttribute.ise=removeAttribute: Session already invalidated +standardSession.setAttribute.namenull=setAttribute: name parameter cannot be null diff --git a/java/org/apache/catalina/ha/session/LocalStrings_cs.properties b/java/org/apache/catalina/ha/session/LocalStrings_cs.properties new file mode 100644 index 0000000..226ec81 --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_cs.properties @@ -0,0 +1,25 @@ +# 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. + +deltaManager.createSession.newSession=Nová DeltaSession vytoÅ™ena s ID [{0}] Celkový poÄet=[{1}] +deltaManager.foundMasterMember=V kontextu [{0}] byl nalezen hlavní Älen replikace [{1}] +deltaManager.noContextManager=Manager [{0}]: OdpovÄ›Ä na zprávu ''Dej vÅ¡echna session data'' poslanou na [{1}] byla pÅ™ijata zpráva ''Žádný odpovídající kontext manager'' po [{2}] ms +deltaManager.receiveMessage.delta.unknown=Manager [{0}]: obdrženy delta zmÄ›ny pro neznámou session [{1}] +deltaManager.registerCluster=Registrace manageru [{0}] do cluster elementu [{1}] se jménem [{2}] +deltaManager.unloading.ioe=IOException pÅ™i ukládání uložené relace: [{0}] + +deltaRequest.removeUnable=Nelze odstranit element: +deltaRequest.showPrincipal=Principal [{0}] je nastaven na relaci [{1}] +deltaRequest.wrongPrincipalClass=ClusterManager podporuje jenom GenericPrincipal. Váš realm použil principal třídu [{0}]. diff --git a/java/org/apache/catalina/ha/session/LocalStrings_de.properties b/java/org/apache/catalina/ha/session/LocalStrings_de.properties new file mode 100644 index 0000000..1b93cc2 --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_de.properties @@ -0,0 +1,26 @@ +# 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. + +deltaManager.foundMasterMember=Für den Kontext [{0}] wurde der Replikationsmaster [{1}] gefunden +deltaManager.noContextManager=Manager [{0}]: Als Antwort auf eine um [{1}] gesendete "Hole alle Sitzungsdaten"-Nachricht, wurde nach [{2}] ms eine "Kein passender Context-Manager"-Nachricht empfangen. +deltaManager.receiveMessage.allSessionDataBegin=Manager [{0}]: alle Sitzungsdaten empfangen +deltaManager.receiveMessage.delta.unknown=Manager [{0}]: Habe Session-Delta für unbekannte Session [{1}] empfangen +deltaManager.receiveMessage.unloadingBegin=Manager [{0}]: Beginne die Sessions zu entladen +deltaManager.unloading.ioe=IOExceptio während des Speichers der Persisted Sessions: [{0}] + +deltaRequest.removeUnable=Kann Element nicht entfernen: +deltaRequest.wrongPrincipalClass=Der ClusterManager unterstützt nur GenericPrincipal. Ihr Realm benutzt die Principal Klasse [{0}]. + +jvmRoute.valve.started=JvmRouteBinderValve gestartet diff --git a/java/org/apache/catalina/ha/session/LocalStrings_es.properties b/java/org/apache/catalina/ha/session/LocalStrings_es.properties new file mode 100644 index 0000000..0241e69 --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_es.properties @@ -0,0 +1,81 @@ +# 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. + +backupManager.startUnable=Imposible de iniciar BackupManager: [{0}]\n + +deltaManager.createMessage.access=Gestor [{0}]: creado mensaje de sesión [{1}] acceso. +deltaManager.createMessage.accessChangePrimary=Gestor [{0}]: creado mensaje de sesión [{1}] acceso para cambiar el primario. +deltaManager.createMessage.allSessionData=Gestor [{0}] envía todos los datos de sesión. +deltaManager.createMessage.allSessionTransferred=Gestor [{0}] envía todos los datos de sesión transferidos +deltaManager.createMessage.delta=Gestor [{0}]: crea mensaje de sesión [{1}] de requerimiento delta. +deltaManager.createMessage.expire=Gestor [{0}]: crea mensaje de sesión [{1}] de expiración. +deltaManager.createMessage.unableCreateDeltaRequest=No puedo serializar requerimiento delta para la id de sesión [{0}] +deltaManager.createSession.newSession=Creada una DeltaSession con Id [{0}] Total contador=[{1}] +deltaManager.dropMessage=Gestor [{0}]: Quita mensaje [{1}] dentro de fase sincronizada GET_ALL_SESSIONS fecha inicio [{2}] fecha mensaje [{3}] +deltaManager.expireSessions=Gestor [{0}] expirando sesiones al apagar +deltaManager.foundMasterMember=Hallado para contexto [{0}] el miembro maestro de réplica [{1}] +deltaManager.loading.cnfe=ClassNotFoundException al cargar sesiones persistentes: [{0}] +deltaManager.loading.existing.session=sobrecarga en sesión existente [{0}] +deltaManager.loading.ioe=IOException al cargar sesiones persistentes: [{0}] +deltaManager.managerLoad=Excepción cargando sesiones desde almacenaje persistente +deltaManager.noCluster=Arrancando... no hay clúster asociado con este contexto: [{0}] +deltaManager.noContextManager=Manejador [{0}]: En respuesta al mensaje ''Tomar todos los datos de sesión (Get all session data)'' enviado a [{1}], recibión el mensaje ''El manejador no machea con ningún contexto (No matching context manager)'' luego de [{2}] ms.\n +deltaManager.noMasterMember=Arrancando... sin otro miembro para el contexto [{0}] en dominio [{1}] +deltaManager.noMembers=Gestor [{0}]: saltando estado de transferencia. No hay miembros activos en grupo de clúster. +deltaManager.noSessionState=Gestor [{0}]: No se ha recibido estado de sesión a las [{1}], agotando tiempo tras [{2}] ms. +deltaManager.receiveMessage.accessed=Gestor [{0}]: accedida sesión [{1}] recibida. +deltaManager.receiveMessage.allSessionDataAfter=Gestor [{0}]: estado de sesión deserializado +deltaManager.receiveMessage.allSessionDataBegin=Gestor [{0}]: recibidos datos de estado de sesión +deltaManager.receiveMessage.createNewSession=Gestor [{0}]: creada sesión [{1}] recibida. +deltaManager.receiveMessage.delta=Gestor [{0}]: delta sesión [{1}] recibida. +deltaManager.receiveMessage.delta.unknown=Manejador [{0}]: recibió una diferencia de sesión para una sesión desconocida [{1}] +deltaManager.receiveMessage.error=Gestor [{0}]: No puedo recibir mensaje a través del canal TCP +deltaManager.receiveMessage.eventType=Gestor [{0}]: recibido SessionMessage de tipo=[{1}] desde [{2}] +deltaManager.receiveMessage.expired=Gestor [{0}]: expirada sesión [{1}] recibida. +deltaManager.receiveMessage.transfercomplete=Gestor [{0}] recibido desde nodo [{1}:{2}] estado de sesión transferido. +deltaManager.receiveMessage.unloadingAfter=Gestor [{0}]: completada la descarga de sesiones +deltaManager.receiveMessage.unloadingBegin=Gestor [{0}]: iniciada descarga de sesiones +deltaManager.registerCluster=Registrar gestor [{0}] a elemento de clúster [{1}] con nombre [{2}] +deltaManager.sendMessage.newSession=El gestor [{0}] envía nueva sesión [{1}] +deltaManager.sessionReceived=Gestor [{0}]; estado de sesión enviado a las [{1}] recibido en [{2}] ms. +deltaManager.startClustering=Iniciando gestor de clúster a las [{0}] +deltaManager.stopped=El gestor [{0}] se está parando +deltaManager.unableSerializeSessionID=No puedo seriallizar la ID de sesión [{0}] +deltaManager.unloading.ioe=IOException al grabar sesiones persistentes: [{0}] +deltaManager.waitForSessionState=Gestor [{0}], requiriendo estado de sesión desde [{1}]. Esta operación se agotará si no se recibe estado de sesión dentro de [{2}] segundos. + +deltaRequest.removeUnable=Imposible eliminar elemento: +deltaRequest.showPrincipal=El Principal [{0}] está puesto a sesión [{1}] +deltaRequest.wrongPrincipalClass=DeltaManager sólo soporta GenericPrincipal. Tu reino utilizó clase principal [{0}]. + +deltaSession.notifying=Notificando clúster de expiración manager [{0}], primary [{1}], sessionId [{2}] +deltaSession.readSession=readObject() cargando sesión [{0}] +deltaSession.writeSession=writeObject() guardando sesión [{0}] + +jvmRoute.cannotFindSession=No puedo hallar sesión [{0}] +jvmRoute.changeSession=Cambiada sesión desde [{0}] a [{1}] +jvmRoute.failover=Detectada una caída con diferente jvmRoute - ruta original: [{0}] nueva: [{1}] en id de sesión [{2}] +jvmRoute.foundManager=Hallado Clúster DeltaManager [{0}] en [{1}] +jvmRoute.missingJvmRouteAttribute=¡No se ha configurado atributo de motor jvmRoute! +jvmRoute.noCluster=La válvula JvmRouterBinderValve se encuentra configurada, pero no se usa el clúster. Aún funcionará la tolerancia a fallos, siempre que se esté usando PersistentManager. +jvmRoute.notFoundManager=No hallado Clúster DeltaManager [{0}] en [{1}] +jvmRoute.set.originalsessionid=Puesta id Orginal de Sesión en atributo de requerimiento [{0}] valor: [{1}] +jvmRoute.turnoverInfo=Ajustado tiempo de Chequeo a [{0}] mseg +jvmRoute.valve.started=JvmRouteBinderValve arrancada +jvmRoute.valve.stopped=JvmRouteBinderValve parada + +standardSession.notSerializable=No puedo serializar atributo de sesión [{0}] para sesión [{1}] +standardSession.removeAttribute.ise=removeAttribute: Sesión ya invalidada +standardSession.setAttribute.namenull=setAttribute: parámetro de nombre no puede ser nulo diff --git a/java/org/apache/catalina/ha/session/LocalStrings_fr.properties b/java/org/apache/catalina/ha/session/LocalStrings_fr.properties new file mode 100644 index 0000000..4854cda --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_fr.properties @@ -0,0 +1,92 @@ +# 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. + +backupManager.noCluster=pas de groupe (cluster) associé à ce contexte : [{0}] +backupManager.startFailed=Impossible de démarrer le BackupManager : [{0}] +backupManager.startUnable=Impossible de démarrer le BackupManager : [{0}] +backupManager.stopped=Le gestionnaire de session [{0}] s''est arrêté + +clusterManager.noValve=Aucune ReplicationValve n'a été trouvée pour le support du cross contexte + +clusterSessionListener.noManager=Le gestionnaire de session du contexte n''existe pas : [{0}] + +deltaManager.createMessage.access=Gestionnaire de session [{0}] : création du message de session [{1}] d''accès +deltaManager.createMessage.accessChangePrimary=Gestionnaire de session [{0}] : création du message de session [{1}] accès pour changer le primaire +deltaManager.createMessage.allSessionData=Gestionnaire de session [{0}] envoyé toutes les données de session +deltaManager.createMessage.allSessionTransferred=Gestionnaire de session [{0}] envoi du message signalant le transfert de toutes les données de session +deltaManager.createMessage.delta=Gestionnaire de session [{0}] : création du message [{0}] de requête delta +deltaManager.createMessage.expire=Gestionnaire de session [{0}] : création du message [{1}] d''expiration de session +deltaManager.createMessage.unableCreateDeltaRequest=Impossible de sérialiser la requête delta pour l''id de session [{0}] +deltaManager.createSession.newSession=Crée une DeltaSession avec Id [{0}] Nombre total=[{1}] +deltaManager.dropMessage=Gestionnaire de session [{0}] : Abandon du message [{1}] dans GET_ALL_SESSIONS début de la phase de sync [{2}] date du message [{3}] +deltaManager.expireSessions=Gestionnaire de session [{0}] expiration des sessions lors de l''arrêt +deltaManager.foundMasterMember=Le membre maître de réplication [{1}] a été trouvé pour le contexte [{0}] +deltaManager.loading.cnfe=Exception ClassNotFoundException lors du chargement des sessions persistantes : [{0}] +deltaManager.loading.existing.session=la session existante [{0}] est surchargée +deltaManager.loading.ioe=IOException lors du chargement des session persistées : [{0}] +deltaManager.managerLoad=Exception lors du chargement des sessions depuis le stockage persistant +deltaManager.noCluster=Démarrage, pas de cluster associé à ce contexte [{0}] +deltaManager.noContextManager=Gestionnaire de session [{0}] : En réponse à l''envoi d''un message demandant toutes les données des sessions à [{0}], un message indiquant l''absence d''un gestionnaire de sessions correspondant à été reçu au bout de [{2}] ms +deltaManager.noMasterMember=Démarrage sans autre membre pour le contexte [{0}] du domaine [{1}] +deltaManager.noMembers=Gestionnaire de session [{0}] : pas de transfert d''état, il n''y a pas de membres actifs dans le cluster +deltaManager.noSessionState=Gestionnaire de session [{0}] : pas de statut de session envoyé à [{1}] reçu, délai d''attente maximum de [{2}] ms. +deltaManager.receiveMessage.accessed=Gestionnaire de session [{0}] : reçu un accès à la session [{1}] +deltaManager.receiveMessage.allSessionDataAfter=Gestionnaire de session [{0}] : l''état de la session a été désérialisé +deltaManager.receiveMessage.allSessionDataBegin=Gestionnaire de session [{0}] : : reçu les données d''état des sessions +deltaManager.receiveMessage.createNewSession=Gestionnaire de session [{0}] : reçu la création de la session [{1}] +deltaManager.receiveMessage.delta=Gestionnaire de session [{0}] : reçu le delta de session [{1}] +deltaManager.receiveMessage.delta.unknown=Gestionnaire de session [{0}] : reçu un delta pour une session inconnue [{1}] +deltaManager.receiveMessage.error=Gestionnaire de session [{0}] : impossible de recevoir un message par le canal TCP +deltaManager.receiveMessage.eventType=Gestionnaire de session [{0}] : recu un SessionMessage de type=[{1}] de [{2}] +deltaManager.receiveMessage.expired=Gestionnaire de session [{0}] : reçu l''expiration de la session [{1}] +deltaManager.receiveMessage.noContextManager=Gestionnaire de session [{0}] a reçu d''un nÅ“ud [{1} : {2}] sans gestionnaire de contexte +deltaManager.receiveMessage.transfercomplete=Gestionnaire de session [{0}] reçu du nÅ“ud [{1} : {2}] l''état de la session a été transféré +deltaManager.receiveMessage.unloadingAfter=Gestionnaire de session [{0}] : fin du déchargement des sessions +deltaManager.receiveMessage.unloadingBegin=Gestionnaire de session [{0}] : début du déchargement des sessions +deltaManager.registerCluster=Enregistrement du gestionnaire [{0}] dans l''élément du cluster [{1}] avec le nom [{2}] +deltaManager.sendMessage.newSession=Gestionnaire de session [{0}] : envoi de la nouvelle session [{1}] +deltaManager.sessionReceived=Gestionnaire de session [{0}] : l''état de session envoyé à [{0}] a été reçu en [{2}] ms +deltaManager.startClustering=Démarrage du gestionnaire du cluster à [{0}] +deltaManager.stopped=Le gestionnaire de session [{0}] s''arrête +deltaManager.unableSerializeSessionID=Impossible de sérialiser le sessionID [{0}] +deltaManager.unloading.ioe=IOException lors de la sauvegarde des sessions persistantes : [{0}] +deltaManager.waitForSessionState=Gestionnaire de session [{0}], demande de l''état de session depuis [{1}], cette opération fera un timeout si l''état de la session n''a pas été reçu en moins de [{2}] secondes + +deltaRequest.invalidAttributeInfoType=Info d''attribut invalide = [{0}] +deltaRequest.removeUnable=N'a pas pu enlever l'élément : +deltaRequest.showPrincipal=Le principal [{0}] est associé à la session [{1}] +deltaRequest.ssid.mismatch=L'id de session ne correspond pas, la requête delta ne sera pas exécutée +deltaRequest.ssid.null=L'id de session est null pour setSessionId +deltaRequest.wrongPrincipalClass=Un ClusterManager n''accepte que des GenericPrincipal. Votre realm a utilisé la classe de "principal" [{0}] + +deltaSession.notifying=Notification du cluster de l''expiration de la session : manager [{0}], primaire=[{1}] sessionId [{2}] +deltaSession.readSession=readObject() charge la session [{0}] +deltaSession.writeSession=writeObject() stocke la session [{0}] + +jvmRoute.cannotFindSession=Impossible de trouver la session [{0}] +jvmRoute.changeSession=Changé la session de [{0}] vers [{1}] +jvmRoute.failover=Un changement de serveur a été détecté avec une jvmRoute différente, route originale : [{0}] nouvelle : [{1}] pour l''id de session [{2}] +jvmRoute.foundManager=Trouvé le gestionnaire de session du cluster [{0}] à [{1}] +jvmRoute.missingJvmRouteAttribute=Pas d'attribut jvmRoute configuré sur le moteur +jvmRoute.noCluster=La JvmRouterBinderValve est configurée mais le cluster n'est pas activé, la bascule vers un autre serveur fonctionnera tout de même à condition qu'un PersistentManager soit utilisé +jvmRoute.notFoundManager=Gestionnaire de cluster ("Cluster Manager") non trouvé à [{0}] +jvmRoute.set.originalsessionid=Fixe l''id de session d''origine dans l''attribut de requête [{0}] valeur : [{1}] +jvmRoute.turnoverInfo=Temps de vérification de turnover [{0}] ms +jvmRoute.valve.started=La JvmRouteBinderValve a démarrée +jvmRoute.valve.stopped=JvmRouteBinderValve s'est arrêté + +standardSession.notSerializable=Impossible de sérialiser l''attribut de session [{0}] pour la session [{1}] +standardSession.removeAttribute.ise=removeAttribute : session déjà invalidée +standardSession.setAttribute.namenull=setAttribute : le paramètre nom ne peut pas être null diff --git a/java/org/apache/catalina/ha/session/LocalStrings_ja.properties b/java/org/apache/catalina/ha/session/LocalStrings_ja.properties new file mode 100644 index 0000000..1b96b24 --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_ja.properties @@ -0,0 +1,92 @@ +# 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. + +backupManager.noCluster=コンテキスト: [{0}]ã«ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ãŒé–¢é€£ä»˜ã‘られã¦ã„ã¾ã›ã‚“。 +backupManager.startFailed=BackupManager: [{0}]ã®èµ·å‹•ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +backupManager.startUnable=BackupManager を開始ã§ãã¾ã›ã‚“: [{0}] +backupManager.stopped=マãƒãƒ¼ã‚¸ãƒ£ [{0}] ã‚’åœæ­¢ã—ã¾ã™ + +clusterManager.noValve=クロスコンテキストサãƒãƒ¼ãƒˆç”¨ã® ReplicationValve ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ + +clusterSessionListener.noManager=Context ManagerãŒå­˜åœ¨ã—ã¾ã›ã‚“。 + +deltaManager.createMessage.access=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッション [{1}] ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’作æˆã—ã¾ã™ +deltaManager.createMessage.accessChangePrimary=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッション [{1}] ã§ãƒ—ライマリノード変更メッセージを作æˆã—ã¾ã—㟠+deltaManager.createMessage.allSessionData=マãƒãƒ¼ã‚¸ãƒ£ [{0}] ã¯ã™ã¹ã¦ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ãƒ‡ãƒ¼ã‚¿ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚ +deltaManager.createMessage.allSessionTransferred=マãƒãƒ¼ã‚¸ãƒ£ [{0}] ã¯ã™ã¹ã¦ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ãƒ‡ãƒ¼ã‚¿è»¢é€ã‚’é€ä¿¡ã—ã¾ã—㟠+deltaManager.createMessage.delta=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッション [{1}] ã®ãƒ‡ãƒ«ã‚¿ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’作æˆã—ã¾ã™ +deltaManager.createMessage.expire=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッション [{1}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®æœŸé™åˆ‡ã‚Œãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’作æˆã—ã¾ã™ +deltaManager.createMessage.unableCreateDeltaRequest=セッションID [{0}]ã®DeltaRequestシリアライズã§ãã¾ã›ã‚“。 +deltaManager.createSession.newSession=DeltaSession (ID 㯠[{0}]) を作æˆã—ã¾ã—ãŸã€‚ç·æ•°ã¯ [{1}] ã§ã™ã€‚ +deltaManager.dropMessage=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: GET_ALL_SESSIONSåŒæœŸãƒ•ã‚§ãƒ¼ã‚ºã®é–‹å§‹æ—¥ [{2}] メッセージã®æ—¥ä»˜ [{3}] 内ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ [{1}] をドロップã—ã¾ã™ +deltaManager.expireSessions=シャットダウン時ã«ãƒžãƒãƒ¼ã‚¸ãƒ£ [{0}] ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’満了ã—ã¾ã™ +deltaManager.foundMasterMember=コンテキスト [{0}] ã®ãƒ¬ãƒ—リケーションマスターメンãƒãƒ¼ [{1}] を発見ã—ã¾ã—㟠+deltaManager.loading.cnfe=永続セッションã®ãƒ­ãƒ¼ãƒ‰ä¸­ã«ClassNotFoundExceptionãŒç™ºç”Ÿã—ã¾ã—ãŸï¼š[{0}] +deltaManager.loading.existing.session=既存セッション[{0}]ã®ã‚ªãƒ¼ãƒãƒ¼ãƒ­ãƒ¼ãƒ‰ +deltaManager.loading.ioe=永続化セッションã®èª­ã¿è¾¼ã¿ä¸­ã« IOException ãŒç™ºç”Ÿã—ã¾ã—ãŸ: [{0}] +deltaManager.managerLoad=永続化ストレージã‹ã‚‰ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®èª­ã¿è¾¼ã¿ä¸­ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +deltaManager.noCluster=開始中... ã“ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] ã«é–¢é€£ä»˜ã‘られãŸã‚¯ãƒ©ã‚¹ã‚¿ã¯ã‚ã‚Šã¾ã›ã‚“。 +deltaManager.noContextManager=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: [{1}] ã§é€ä¿¡ã•ã‚ŒãŸ ''Get all session data'' メッセージã®è¿”ä¿¡ã«ãŠã„ã¦ã€[{2}] ミリ秒後㫠''No Context Manager'' メッセージãŒå—ä¿¡ã•ã‚Œã¾ã—ãŸã€‚ +deltaManager.noMasterMember=ドメイン [{1}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] ã«ä»–ã®ãƒ¡ãƒ³ãƒãƒ¼ãŒã„ãªã„状態ã§é–‹å§‹ã—ã¦ã„ã¾ã™ +deltaManager.noMembers=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: 状態転é€ã‚’スキップã—ã¾ã™ã€‚ クラスタグループ内ã§ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªãƒ¡ãƒ³ãƒãƒ¼ã¯ã„ã¾ã›ã‚“。 +deltaManager.noSessionState=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: [{1}] ã§é€ä¿¡ã•ã‚ŒãŸã‚»ãƒƒã‚·ãƒ§ãƒ³çŠ¶æ…‹ã¯ã‚ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚[{2}] ミリ秒後ã«ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã—ãŸã€‚ +deltaManager.receiveMessage.accessed=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッション[{1}]ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¢ã‚¯ã‚»ã‚¹ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ã¾ã—㟠+deltaManager.receiveMessage.allSessionDataAfter=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: ã™ã¹ã¦ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³çŠ¶æ…‹ã‚’デシリアライズã—ã¾ã—㟠+deltaManager.receiveMessage.allSessionDataBegin=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: ã™ã¹ã¦ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³çŠ¶æ…‹ãƒ‡ãƒ¼ã‚¿ã‚’å—ä¿¡ã—ã¾ã—㟠+deltaManager.receiveMessage.createNewSession=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッション [{1}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ä½œæˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ã¾ã—㟠+deltaManager.receiveMessage.delta=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッション [{1}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ãƒ‡ãƒ«ã‚¿ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ã¾ã—㟠+deltaManager.receiveMessage.delta.unknown=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: 未知ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{1}] デルタをå—ä¿¡ã—ã¾ã—㟠+deltaManager.receiveMessage.error=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: TCP ãƒãƒ£ãƒ³ãƒãƒ«ã‹ã‚‰ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã§ãã¾ã›ã‚“ +deltaManager.receiveMessage.eventType=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: [{2}] ã‹ã‚‰ã‚»ãƒƒã‚·ãƒ§ãƒ³ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ [{1}] ã‚’å—ä¿¡ã—ã¾ã—㟠+deltaManager.receiveMessage.expired=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッション [{1}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®æœŸé™åˆ‡ã‚Œãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ã¾ã—㟠+deltaManager.receiveMessage.noContextManager=マãƒãƒ¼ã‚¸ãƒ£ [{0}] ã¯ãƒŽãƒ¼ãƒ‰ [{1}: {2}] ã‹ã‚‰ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãƒžãƒãƒ¼ã‚¸ãƒ£ç„¡ã—メッセージをå—ä¿¡ã—ã¾ã—ãŸã€‚ +deltaManager.receiveMessage.transfercomplete=マãƒãƒ¼ã‚¸ãƒ£ [{0}] ã¯ãƒŽãƒ¼ãƒ‰ [{1}: {2}] ã‹ã‚‰ã‚»ãƒƒã‚·ãƒ§ãƒ³çŠ¶æ…‹ãŒè»¢é€ã‚’å—ä¿¡ã—ã¾ã—ãŸã€‚ +deltaManager.receiveMessage.unloadingAfter=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッションã®ã‚¢ãƒ³ãƒ­ãƒ¼ãƒ‰ãŒå®Œäº†ã—ã¾ã—㟠+deltaManager.receiveMessage.unloadingBegin=マãƒãƒ¼ã‚¸ãƒ£ [{0}]: セッションã®ã‚¢ãƒ³ãƒ­ãƒ¼ãƒ‰ã‚’開始ã—ã¾ã™ +deltaManager.registerCluster=マãƒãƒ¼ã‚¸ãƒ£ [{0}] をクラスターã®æ§‹æˆè¦ç´  [{1}] ã«åå‰ [{2}] ã§ç™»éŒ²ã—ã¾ã—㟠+deltaManager.sendMessage.newSession=マãƒãƒ¼ã‚¸ãƒ£ [{0}] ãŒæ–°ã—ã„セッションをé€ä¿¡ã—ã¾ã™ [{1}] +deltaManager.sessionReceived=マãƒãƒ¼ã‚¸ãƒ£ [{0}]; [{2}] ミリ秒ã§å—ä¿¡ã—㟠[{1}] ã§é€ä¿¡ã•ã‚ŒãŸã‚»ãƒƒã‚·ãƒ§ãƒ³çŠ¶æ…‹ã€‚ +deltaManager.startClustering=[{0}]ã§ã‚¯ãƒ©ã‚¹ã‚¿ãƒªãƒ³ã‚°ãƒžãƒãƒ¼ã‚¸ãƒ£ã‚’開始ã—ã¦ã„ã¾ã™ã€‚ +deltaManager.stopped=マãƒãƒ¼ã‚¸ãƒ£ [{0}] ãŒåœæ­¢ã—ã¦ã„ã¾ã™ +deltaManager.unableSerializeSessionID=セッション ID [{0}] をシリアライズã§ãã¾ã›ã‚“ +deltaManager.unloading.ioe=永続セッションをä¿å­˜ä¸­ã®IOException:[{0}] +deltaManager.waitForSessionState=マãƒãƒ¼ã‚¸ãƒ£ [{0}]ã€[{1}] ã‹ã‚‰ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³çŠ¶æ…‹ã‚’è¦æ±‚ã—ã¦ã„ã¾ã™ã€‚ [{2}] 秒以内ã«ã‚»ãƒƒã‚·ãƒ§ãƒ³çŠ¶æ…‹ãŒå—ä¿¡ã•ã‚Œãªã‹ã£ãŸå ´åˆã€ã“ã®æ“作ã¯ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã«ãªã‚Šã¾ã™ã€‚ + +deltaRequest.invalidAttributeInfoType=無効ãªå±žæ€§æƒ…報タイプ= [{0}] +deltaRequest.removeUnable=è¦ç´ ã‚’削除ã§ãã¾ã›ã‚“: +deltaRequest.showPrincipal=プリンシパル [{0}]ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{1}]ã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ +deltaRequest.ssid.mismatch=セッションIDãŒä¸€è‡´ã—ã¾ã›ã‚“。デルタリクエストãŒå®Ÿè¡Œã•ã‚Œã¦ã„ã¾ã›ã‚“。 +deltaRequest.ssid.null=setSessionId ã«æŒ‡å®šã—ãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ ID ㌠null ã§ã™ã€‚ +deltaRequest.wrongPrincipalClass=ClusterManagerã¯GenericPrincipalã®ã¿ã‚’サãƒãƒ¼ãƒˆã—ã¾ã™ã€‚ã‚ãªãŸã®Realmã¯ãƒ—リンシパルクラス [{0}] を使用ã—ã¾ã—ãŸã€‚ + +deltaSession.notifying=クラスタã«ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®æœ‰åŠ¹æœŸé™ã‚’通知ã™ã‚‹: マãƒãƒ¼ã‚¸ãƒ£ [{0}]ã€primary [{1}]ã€sessionId [{2}] +deltaSession.readSession=readObject() ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] を読ã¿è¾¼ã¿ã¾ã—ãŸã€‚ +deltaSession.writeSession=writeObject() ã«ã‚ˆã‚Šã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] ã‚’æ ¼ç´ã—ã¾ã—ãŸã€‚ + +jvmRoute.cannotFindSession=セッション [{0}] ãŒã‚ã‚Šã¾ã›ã‚“。 +jvmRoute.changeSession=セッションを [{0}] ã‹ã‚‰ [{1}] ã¸å¤‰æ›´ã—ã¾ã—ãŸã€‚ +jvmRoute.failover=ä»–ã® jvmRoute ã¸ã®ãƒ•ã‚§ãƒ¼ãƒ«ã‚ªãƒ¼ãƒãƒ¼ã‚’検出ã—ã¾ã—ãŸã€‚å…ƒã®ãƒ«ãƒ¼ãƒˆã¯ [{0}]ã€æ–°ã—ã„ルート㯠[{1}]ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ ID 㯠[{2}] ã§ã™ã€‚ +jvmRoute.foundManager=Cluster Manager [{0}] ã‚’ [{1}] ã§ç™ºè¦‹ã—ã¾ã—㟠+jvmRoute.missingJvmRouteAttribute=jvmRoute 属性ã«ã‚¨ãƒ³ã‚¸ãƒ³ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“。 +jvmRoute.noCluster=JvmRouterBinderValveã¯è¨­å®šã•ã‚Œã¦ã„ã¾ã™ãŒã€ã‚¯ãƒ©ã‚¹ã‚¿ãƒªãƒ³ã‚°ã¯ä½¿ç”¨ã•ã‚Œã¦ã„ã¾ã›ã‚“。 PersistentManagerãŒä½¿ç”¨ã•ã‚Œã¦ã„ã‚‹å ´åˆã€ãƒ•ã‚§ãƒ¼ãƒ«ã‚ªãƒ¼ãƒãƒ¼ã¯å¼•ã続ã機能ã—ã¾ã™ã€‚ +jvmRoute.notFoundManager=[{0}]ã§Cluster ManagerãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +jvmRoute.set.originalsessionid=オリジナルSession idをリクエスト属性[{0}]ã®å€¤ï¼š[{1}]ã§è¨­å®šã—ã¾ã™ã€‚ +jvmRoute.turnoverInfo=折り返ã—ã®æ‰€è¦æ™‚間㯠[{0}] ミリ秒ã§ã—ãŸã€‚ +jvmRoute.valve.started=JvmRouteBinderValve ãŒèµ·å‹•ã—ã¾ã—ãŸã€‚ +jvmRoute.valve.stopped=JvmRouteBinderValve ãŒåœæ­¢ã—ã¾ã—ãŸã€‚ + +standardSession.notSerializable=セッション[{1}]ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³å±žæ€§[{0}]をシリアライズã§ãã¾ã›ã‚“。 +standardSession.removeAttribute.ise=removeAttribute: Session already invalidated +standardSession.setAttribute.namenull=setAttribute:nameパラメータをnullã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/ha/session/LocalStrings_ko.properties b/java/org/apache/catalina/ha/session/LocalStrings_ko.properties new file mode 100644 index 0000000..e77d7a1 --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_ko.properties @@ -0,0 +1,90 @@ +# 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. + +backupManager.noCluster=ì´ ì»¨í…스트 [{0}]와(ê³¼) ì—°ê´€ëœ í´ëŸ¬ìŠ¤í„°ê°€ 없습니다. +backupManager.startFailed=백업매니저 [{0}]ì„(를) 시작하지 못했습니다. +backupManager.startUnable=BackupManager를 시작할 수 없습니다: [{0}] +backupManager.stopped=매니저 [{0}]ì´(ê°€) 중지ë˜ê³  있습니다. + +clusterSessionListener.noManager=컨í…스트 매니저가 존재하지 않습니다: [{0}] + +deltaManager.createMessage.access=매니저 [{0}]: 세션(ID: [{1}])ì„ ìœ„í•œ 세션 ì ‘ê·¼ 메시지를 ìƒì„±í•©ë‹ˆë‹¤. +deltaManager.createMessage.accessChangePrimary=매니저 [{0}]: 세션(ID: [{1}])ì„ ìœ„í•´ Primary 노드 변경 메시지를 ìƒì„±í•©ë‹ˆë‹¤. +deltaManager.createMessage.allSessionData=매니저 [{0}]ì´(ê°€) 모든 세션 ë°ì´í„°ë¥¼ 전송했습니다. +deltaManager.createMessage.allSessionTransferred=매니저 [{0}]ì´(ê°€), 모든 세션 ë°ì´í„° 전송 완료 메시지를 보냈습니다. +deltaManager.createMessage.delta=매니저 [{0}]: 세션(ID: [{1}])ì„ ìœ„í•œ ë¸íƒ€ 요청 메시지를 ìƒì„±í•©ë‹ˆë‹¤. +deltaManager.createMessage.expire=매니저 [{0}]: 세션(ID: [{1}])ì„ ìœ„í•œ 세션 만료 메시지를 ìƒì„±í•©ë‹ˆë‹¤. +deltaManager.createMessage.unableCreateDeltaRequest=세션 ID [{0}]ì„(를) 위한 ë¸íƒ€ ìš”ì²­ì„ ì§ë ¬í™”í•  수 없습니다. +deltaManager.createSession.newSession=IDê°€ [{0}]ì¸ DeltaSessionì„ ìƒì„±í–ˆìŠµë‹ˆë‹¤. ì´ ê°œìˆ˜=[{1}] +deltaManager.dropMessage=매니저 [{0}]: GET_ALL_SESSIONS ë™ê¸°í™” êµ­ë©´ ë‚´ì—ì„œ, 메시지 [{1}]ì„(를) 무시합니다. 시작 시간: [{2}], ë©”ì‹œì§€ì˜ íƒ€ìž„ìŠ¤íƒ¬í”„: [{3}] +deltaManager.expireSessions=매니저 [{0}]ì´(ê°€) 셧다운 ì‹œì— ì„¸ì…˜ë“¤ì„ ë§Œë£Œì‹œí‚µë‹ˆë‹¤. +deltaManager.foundMasterMember=컨í…스트 [{0}]ì„(를) 위한 복제 마스터 멤버 [{1}]ì„(를) 찾았습니다. +deltaManager.loading.cnfe=ì €ìž¥ëœ ì„¸ì…˜ë“¤ì„ ë¡œë“œí•˜ëŠ” 중 ClassNotFoundException ë°œìƒ: [{0}] +deltaManager.loading.existing.session=기존 세션 [{0}]ì„(를) 오버로드합니다. +deltaManager.loading.ioe=ì €ìž¥ëœ ì„¸ì…˜ë“¤ì„ ë¡œë“œí•˜ëŠ” 중 IOException ë°œìƒ: [{0}] +deltaManager.managerLoad=저장소로부터 ì„¸ì…˜ë“¤ì„ ë¡œë“œí•˜ëŠ” 중 예외 ë°œìƒ +deltaManager.noCluster=시작 중... ì´ ì»¨í…스트와 ì—°ê´€ëœ í´ëŸ¬ìŠ¤í„°ê°€ 없습니다: [{0}] +deltaManager.noContextManager=매니저 [{0}]: [{1}]ì—ì„œ 전송ë˜ì—ˆë˜ ''Get all session data'' ë©”ì‹œì§€ì— ì‘답하여, ''No matching context manager'' 메시지를 [{2}] 밀리초 í›„ì— ë°›ì•˜ìŠµë‹ˆë‹¤. +deltaManager.noMasterMember=시작 중... ë„ë©”ì¸ [{1}]ì— ì»¨í…스트 [{0}]ì„(를) 위한 다른 ë©¤ë²„ë“¤ì€ ì—†ëŠ” ìƒíƒœìž…니다. +deltaManager.noMembers=매니저 [{0}]: ìƒíƒœ ì´ì „ ìž‘ì—…ì„ ê±´ë„ˆëœë‹ˆë‹¤. í´ëŸ¬ìŠ¤í„° 그룹 ë‚´ì— í™œì„±í™”ëœ ë©¤ë²„ê°€ 없습니다. +deltaManager.noSessionState=매니저 [{0}]: [{1}]ì—ì„œ 보낸 세션 ìƒíƒœ 메시지를 받지 못했습니다. [{2}] 밀리초 ì´í›„ 제한 시간 초과ë˜ì—ˆìŠµë‹ˆë‹¤. +deltaManager.receiveMessage.accessed=매니저 [{0}]: 세션(ID: [{1}])ì„ ìœ„í•œ 세션 ì ‘ê·¼ 메시지를 받았습니다. +deltaManager.receiveMessage.allSessionDataAfter=매니저 [{0}]: 모든 세션 ìƒíƒœê°€ ì—­ì§ë ¬í™”ë˜ì—ˆìŠµë‹ˆë‹¤. +deltaManager.receiveMessage.allSessionDataBegin=매니저 [{0}]: 모든 세션 ìƒíƒœ ë°ì´í„°ë¥¼ 받았습니다. +deltaManager.receiveMessage.createNewSession=매니저 [{0}]: 세션 [{1}]ì„(를) 위한 세션 ìƒì„±ë¨ 메시지를 수신했습니다. +deltaManager.receiveMessage.delta=매니저 [{0}]: 세션(ID: [{1}])ì„ ìœ„í•œ 세션 ë¸íƒ€ 메시지를 받았습니다. +deltaManager.receiveMessage.delta.unknown=매니저 [{0}]: ì•Œ 수 없는 세션 [{1}]ì„(를) 위한 세션 ë¸íƒ€ë¥¼ 받았습니다. +deltaManager.receiveMessage.error=매니저 [{0}]: TCP 채ë„ì„ í†µí•´ 메시지를 ë°›ì„ ìˆ˜ 없습니다. +deltaManager.receiveMessage.eventType=매니저 [{0}]: íƒ€ìž…ì´ [{1}]ì¸ SessionMessage를 [{2}](으)로부터 받았습니다. +deltaManager.receiveMessage.expired=매니저 [{0}]: 세션 [{1}]ì„(를) 위한 세션 만료 메시지를 받았습니다. +deltaManager.receiveMessage.noContextManager=매니저 [{0}]ì´(ê°€) 노드 [{1}:{2}](으)로부터 no context manager 메시지를 받았습니다. +deltaManager.receiveMessage.transfercomplete=매니저 [{0}]ì´(ê°€) 노드 [{1}:{2}](으)로부터, 세션 ìƒíƒœ ì´ì „ 완료 메시지를 받았습니다. +deltaManager.receiveMessage.unloadingAfter=매니저 [{0}]: ì„¸ì…˜ë“¤ì„ ì–¸ë¡œë“œí•˜ëŠ” ìž‘ì—…ì´ ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. +deltaManager.receiveMessage.unloadingBegin=매니저 [{0}]: ì„¸ì…˜ë“¤ì— ëŒ€í•´ 언로드를 시작합니다. +deltaManager.registerCluster=매니저 [{0}]ì„(를), [{2}](ì´)ë¼ëŠ” ì´ë¦„ì˜ í´ëŸ¬ìŠ¤í„° 엘리먼트 [{1}](으)ë¡œ 등ë¡í•©ë‹ˆë‹¤. +deltaManager.sendMessage.newSession=매니저 [{0}]ì´(ê°€) 새로운 세션 [{1}]ì„(를) 전송합니다. +deltaManager.sessionReceived=매니저 [{0}]; [{1}]ì—ì„œ ì „ì†¡ëœ ì„¸ì…˜ ìƒíƒœë¥¼ [{2}] 밀리초 ì´ë‚´ì— ë°›ìŒ +deltaManager.startClustering=[{0}]ì—ì„œ í´ëŸ¬ìŠ¤í„° 매니저를 시작합니다. +deltaManager.stopped=매니저 [{0}]ì´(ê°€) 중지ë©ë‹ˆë‹¤. +deltaManager.unableSerializeSessionID=세션 ID [{0}]ì„(를) ì§ë ¬í™”í•  수 없습니다. +deltaManager.unloading.ioe=ì„¸ì…˜ë“¤ì„ ì €ìž¥í•˜ëŠ” 중 IOException ë°œìƒ: [{0}] +deltaManager.waitForSessionState=매니저 [{0}]: [{1}](으)로부터 세션 ìƒíƒœë¥¼ 요청합니다. ë§Œì¼ [{2}]ì´ˆ ì´ë‚´ì— 세션 ìƒíƒœë¥¼ 받지 못하면, ì´ ì˜¤í¼ë ˆì´ì…˜ì€ 제한 시간 초과 ì²˜ë¦¬ë  ê²ƒìž…ë‹ˆë‹¤. + +deltaRequest.invalidAttributeInfoType=유효하지 ì•Šì€ AttributeInfo 타입=[{0}] +deltaRequest.removeUnable=í´ëŸ¬ìŠ¤í„° 엘리먼트를 제거할 수 없습니다: +deltaRequest.showPrincipal=Principal [{0}]ì´(ê°€) 세션 [{1}]ì— ì„¤ì •ë˜ì—ˆìŠµë‹ˆë‹¤. +deltaRequest.ssid.mismatch=세션 IDê°€ ì¼ì¹˜í•˜ì§€ ì•Šì•„, ë¸íƒ€ ìš”ì²­ì„ ì‹¤í–‰í•˜ì§€ 않습니다. +deltaRequest.ssid.null=setSessionId를 위한 세션 IDê°€ ë„입니다. +deltaRequest.wrongPrincipalClass=ClusterManager는 ì˜¤ì§ GenericPrincipalë§Œì„ ì§€ì›í•©ë‹ˆë‹¤. ì‚¬ìš©ëœ realmì€ principal í´ëž˜ìŠ¤ [{0}]ì„(를) 사용했습니다. + +deltaSession.notifying=í´ëŸ¬ìŠ¤í„°ì— 세션 만료를 통지합니다: primary여부: [{1}], 세션ID: [{2}] +deltaSession.readSession=readObject()ê°€ 세션 [{0}]ì„(를) 로드합니다. +deltaSession.writeSession=writeObject()ê°€ 세션 [{0}]ì„(를) 저장합니다. + +jvmRoute.cannotFindSession=세션 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +jvmRoute.changeSession=ì„¸ì…˜ì„ [{0}]ì—ì„œ [{1}](으)ë¡œ 변경했습니다. +jvmRoute.failover=다른 jvmRouteë¡œ Failover를 íƒì§€í–ˆìŠµë‹ˆë‹¤. ì›ëž˜ì˜ ë¼ìš°íŠ¸: [{0}], 새로운 ë¼ìš°íŠ¸: [{1}]. 세션 ID: [{2}] +jvmRoute.foundManager=[{1}]ì—ì„œ í´ëŸ¬ìŠ¤í„° 매니저 [{0}]ì„(를) 찾았습니다. +jvmRoute.missingJvmRouteAttribute=ì—”ì§„ì˜ jvmRoute ì†ì„±ì´ 설정ë˜ì§€ 않았습니다! +jvmRoute.noCluster=JvmRouterBinderValveê°€ 설정ë˜ì—ˆì§€ë§Œ, í´ëŸ¬ìŠ¤í„°ë§ì´ 사용ë˜ê³  있지 않습니다. PersistentManagerê°€ 사용ë˜ëŠ” 경우, Fail over는 여전히 ì •ìƒ ë™ìž‘í•  것입니다. +jvmRoute.notFoundManager=[{0}]ì—ì„œ í´ëŸ¬ìŠ¤í„° 매니저를 ì°¾ì„ ìˆ˜ 없습니다. +jvmRoute.set.originalsessionid=ìš”ì²­ì˜ ì†ì„± [{0}]ì— ì›ëž˜ì˜ 세션 ID를 설정합니다: [{1}] +jvmRoute.turnoverInfo=Failover를 위한 jvmRoute êµì²´ 수행 시간: [{0}] 밀리초 +jvmRoute.valve.started=JvmRouteBinderValveê°€ 시작ë습니다. +jvmRoute.valve.stopped=JvmRouteBinderValveê°€ 중지ë˜ì—ˆìŠµë‹ˆë‹¤. + +standardSession.notSerializable=세션 [{1}]ì„ ìœ„í•œ 세션 ì†ì„± [{0}]ì„(를) ì§ë ¬í™”í•  수 없습니다. +standardSession.removeAttribute.ise=removeAttribute: ì„¸ì…˜ì´ ì´ë¯¸ 무효화ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.setAttribute.namenull=setAttribute: name 파ë¼ë¯¸í„°ëŠ” ë„ì¼ ìˆ˜ 없습니다. diff --git a/java/org/apache/catalina/ha/session/LocalStrings_pt_BR.properties b/java/org/apache/catalina/ha/session/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..cd5d427 --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_pt_BR.properties @@ -0,0 +1,17 @@ +# 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. + +deltaManager.foundMasterMember=Encontrado para contexto [{0}] a replicação master, o membro [{1}] +deltaManager.unloading.ioe=IOException ao salvar sessões persistentes: [{}] diff --git a/java/org/apache/catalina/ha/session/LocalStrings_ru.properties b/java/org/apache/catalina/ha/session/LocalStrings_ru.properties new file mode 100644 index 0000000..19df498 --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +deltaRequest.removeUnable=Ðе возможно удалить Ñлемент: diff --git a/java/org/apache/catalina/ha/session/LocalStrings_zh_CN.properties b/java/org/apache/catalina/ha/session/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..182d27e --- /dev/null +++ b/java/org/apache/catalina/ha/session/LocalStrings_zh_CN.properties @@ -0,0 +1,90 @@ +# 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. + +backupManager.noCluster=没有与此上下文关è”的集群:[{0}] +backupManager.startFailed=å¯åŠ¨BackupManager: [{0}]失败 +backupManager.startUnable=无法å¯åŠ¨BackupManager: [{0}] +backupManager.stopped=管ç†è€…[{0}]正在åœæ­¢ã€‚ + +clusterSessionListener.noManager=上下文管ç†å™¨ä¸å­˜åœ¨ï¼š[{0}] + +deltaManager.createMessage.access=管ç†å™¨[{0}]:创建会è¯ä¸ºä¼šè¯[{1}]å­˜å–æ¶ˆæ¯ +deltaManager.createMessage.accessChangePrimary=管ç†å™¨[{0}]:为会è¯[{1}]创建更改主节点消æ¯ã€‚ +deltaManager.createMessage.allSessionData=管ç†å™¨[{0}]å°†å‘é€æ‰€æœ‰ä¼šè¯æ•°æ®ã€‚ +deltaManager.createMessage.allSessionTransferred=管ç†å™¨[{0}]å‘é€äº†æ‰€æœ‰ä¼ è¾“的会è¯æ•°æ® +deltaManager.createMessage.delta=管ç†å™¨[{0}] ):为会è¯[{1}]创建增é‡è¯·æ±‚æ¶ˆæ¯ +deltaManager.createMessage.expire=管ç†å™¨[{0}] (:为会è¯[{1}]创建会è¯è¿‡æœŸæ¶ˆæ¯ +deltaManager.createMessage.unableCreateDeltaRequest=无法åºåˆ—化sessionid[{0}]的增é‡è¯·æ±‚ +deltaManager.createSession.newSession=用id[{0}]创建一个扩展会è¯(DeltaSession),总数为 [{1}] +deltaManager.dropMessage=管ç†å™¨[{0}]:将消æ¯[{1}]放入GET_所有会è¯åŒæ­¥é˜¶æ®µå¼€å§‹æ—¥æœŸ[{2}]消æ¯æ—¥æœŸ[{3}]。 +deltaManager.expireSessions=管ç†å™¨[{0}]关闭时使会è¯è¿‡æœŸ +deltaManager.foundMasterMember=å¤åˆ¶ä¸»masteræˆå‘˜[{1}]在上下文[{0}]中被å‘现.\n +deltaManager.loading.cnfe=加载æŒä¹…åŒ–ä¼šè¯ [{0}] 时出现ClassNotFoundException +deltaManager.loading.existing.session=é‡è½½çŽ°æœ‰ä¼šè¯[{0}]。 +deltaManager.loading.ioe=加载æŒä¹… session 时出现 IOException:[{0}] +deltaManager.managerLoad=从永久存储加载会è¯æ—¶å‘生异常 +deltaManager.noCluster=å¯åŠ¨ä¸­...没有集群与此上下文关è”:[{0}]。 +deltaManager.noContextManager=管ç†å™¨[{0}]:回å¤[{1}]å‘é€çš„“获å–所有会è¯æ•°æ®â€æ¶ˆæ¯ï¼Œåœ¨[{2}] msåŽæ”¶åˆ°â€œæ— åŒ¹é…的上下文管ç†å™¨â€æ¶ˆæ¯ +deltaManager.noMasterMember=å¯åŠ¨ã€‚。。在域[{1}]没有上下文[{0}]的其他æˆå‘˜ +deltaManager.noMembers=管ç†å™¨[{0}]:正在跳过状æ€ä¼ è¾“。集群组中没有活动的æˆå‘˜ã€‚ +deltaManager.noSessionState=管ç†è€…[{0}]:没有收到[{1}]å‘é€çš„会è¯çŠ¶æ€ï¼Œåœ¨[{2}]毫秒之åŽè¶…时。 +deltaManager.receiveMessage.accessed=管ç†å™¨[{0}]:接收会è¯ä¸ºä¼šè¯[{1}]å­˜å–æ¶ˆæ¯ +deltaManager.receiveMessage.allSessionDataAfter=Manager [{0}]: session 状æ€ååºåˆ—化 +deltaManager.receiveMessage.allSessionDataBegin=管ç†è€…[{0}]:接收到所有会è¯æ•°æ®çŠ¶æ€ +deltaManager.receiveMessage.createNewSession=管ç†å™¨[{0}]:已收到会è¯ä¸ºä¼šè¯[{1}]åˆ›å»ºçš„æ¶ˆæ¯ +deltaManager.receiveMessage.delta=管ç†å™¨[{0}]:已收到会è¯[{1}的会è¯å¢žé‡æ¶ˆæ¯ã€‚ +deltaManager.receiveMessage.delta.unknown=管ç†å™¨[{0}]:未知会è¯çš„接收会è¯å¢žé‡[{1}] +deltaManager.receiveMessage.error=管ç†å™¨[{0}]:无法通过TCP通é“æŽ¥æ”¶æ¶ˆæ¯ +deltaManager.receiveMessage.eventType=管ç†å™¨[{0}]:从[{2}]接收到类型为[{1}]çš„SessionMessage +deltaManager.receiveMessage.expired=管ç†å™¨[{0}]: æŽ¥æ”¶åˆ°çš„ä¼šè¯ [{1}] 已过期。 +deltaManager.receiveMessage.noContextManager=从节点[{1}:{2}]接收的管ç†å™¨[{0}]没有上下文管ç†å™¨ +deltaManager.receiveMessage.transfercomplete=从节点[{1}:{2}]接收的管ç†å™¨[{0}]会è¯çŠ¶æ€å·²ä¼ è¾“。 +deltaManager.receiveMessage.unloadingAfter=管ç†å™¨[{0}]:å¸è½½ä¼šè¯å®Œæˆ +deltaManager.receiveMessage.unloadingBegin=管ç†å™¨[{0}]: 开始å¸è½½ä¼šè¯ +deltaManager.registerCluster=将管ç†å™¨[{0}]注册到å为[{2}]的集群元素[{1}] +deltaManager.sendMessage.newSession=\ 管ç†å™¨ [{0}] å‘é€æ–°çš„ä¼šè¯ [{1}] +deltaManager.sessionReceived=管ç†å™¨[{0}];在[{1}]å‘é€çš„会è¯çŠ¶æ€åœ¨[{2}]毫秒内收到。 +deltaManager.startClustering=在[{0}]å¯åŠ¨é›†ç¾¤ç®¡ç†å™¨ +deltaManager.stopped=管ç†å™¨[{0}]å·²åœæ­¢ +deltaManager.unableSerializeSessionID=无法åºåˆ—化会è¯ID [{0}] +deltaManager.unloading.ioe=当ä¿å­˜æ°¸ä¹…回è¯:[{0}] 时,抛出 IOException +deltaManager.waitForSessionState=管ç†å™¨[{0}],正在从[{1}]请求会è¯çŠ¶æ€ã€‚如果在[{2}]秒内未收到会è¯çŠ¶æ€ï¼Œåˆ™æ­¤æ“作将超时 + +deltaRequest.invalidAttributeInfoType=无效的属性信æ¯ç±»åž‹=[{0}] +deltaRequest.removeUnable=ä¸èƒ½ç§»é™¤å…ƒç´  +deltaRequest.showPrincipal=Principal [{0}] å’Œsession [{1}]产生关è”。 +deltaRequest.ssid.mismatch=回è¯IDä¸åŒ¹é…,ä¸æ‰§è¡Œdelta请求 +deltaRequest.ssid.null=setSessionId的会è¯Id为空 +deltaRequest.wrongPrincipalClass=ClusterManager仅支æŒGenericPrincipal。 ä½ çš„Realm使用的Principal类为[{0}]。 + +deltaSession.notifying=通知集群会è¯è¿‡æœŸï¼šprimary=[{1}],sessionId[{2}] +deltaSession.readSession=readObject()正在加载会è¯[{0}] +deltaSession.writeSession=writeObject()存储会è¯[{0}] + +jvmRoute.cannotFindSession=找ä¸åˆ°ä¼šè¯[{0}] +jvmRoute.changeSession=会è¯ä»Ž[{0}]切æ¢åˆ°[{1}] +jvmRoute.failover=在会è¯id[{2}]检测到具有ä¸åŒjvmRoute的故障转移-原始路由:[{0}]新路由:[{1}] +jvmRoute.foundManager=在[{1}]找到集群管ç†å™¨[{0}] +jvmRoute.missingJvmRouteAttribute=没有é…置引擎jvmRouteå±žæ€§ï¼ +jvmRoute.noCluster=å·²é…ç½®JvmRouterBinderValve,但未使用集群。如果使用了PersistentManager,故障转移ä»ç„¶æœ‰æ•ˆã€‚ +jvmRoute.notFoundManager=没有在 [{0}] 找到Cluster Manager +jvmRoute.set.originalsessionid=在请求属性[{0}]值:[{1}]处设置原始会è¯ID +jvmRoute.turnoverInfo=周转检查时间[{0}]msec +jvmRoute.valve.started=JvmRouteBinderValve å¯åŠ¨ +jvmRoute.valve.stopped=JvmRouteBinderValveåœæ­¢ + +standardSession.notSerializable=无法åºåˆ—化会è¯[{1}]的会è¯å±žæ€§[{0}]。 +standardSession.removeAttribute.ise=removeAttribute:会è¯å·²å¤±æ•ˆã€‚ +standardSession.setAttribute.namenull=setAttribute:å称属性ä¸èƒ½ä¸ºç©º diff --git a/java/org/apache/catalina/ha/session/ReplicatedSessionListener.java b/java/org/apache/catalina/ha/session/ReplicatedSessionListener.java new file mode 100644 index 0000000..a27ac99 --- /dev/null +++ b/java/org/apache/catalina/ha/session/ReplicatedSessionListener.java @@ -0,0 +1,28 @@ +/* + * 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.catalina.ha.session; + +import java.io.Serializable; + +import org.apache.catalina.SessionListener; + +/** + * This is a marker interface used to indicate an implementation of {@link SessionListener} that should be replicated + * with the session across the cluster. + */ +public interface ReplicatedSessionListener extends SessionListener, Serializable { +} diff --git a/java/org/apache/catalina/ha/session/SessionMessage.java b/java/org/apache/catalina/ha/session/SessionMessage.java new file mode 100644 index 0000000..2688b29 --- /dev/null +++ b/java/org/apache/catalina/ha/session/SessionMessage.java @@ -0,0 +1,107 @@ +/* + * 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.catalina.ha.session; + +import org.apache.catalina.ha.ClusterMessage; + +/** + * The SessionMessage interface is used when a session has been created, modified, expired in a Tomcat cluster node. + *

    + * The following events are currently available: + *

      + *
    • {@code public static final int EVT_SESSION_CREATED}
    • + *
    • {@code public static final int EVT_SESSION_EXPIRED}
    • + *
    • {@code public static final int EVT_SESSION_ACCESSED}
    • + *
    • {@code public static final int EVT_GET_ALL_SESSIONS}
    • + *
    • {@code public static final int EVT_SESSION_DELTA}
    • + *
    • {@code public static final int EVT_ALL_SESSION_DATA}
    • + *
    • {@code public static final int EVT_ALL_SESSION_TRANSFERCOMPLETE}
    • + *
    • {@code public static final int EVT_CHANGE_SESSION_ID}
    • + *
    • {@code public static final int EVT_ALL_SESSION_NOCONTEXTMANAGER}
    • + *
    + */ +public interface SessionMessage extends ClusterMessage { + + /** + * Event type used when a session has been created on a node + */ + int EVT_SESSION_CREATED = 1; + /** + * Event type used when a session has expired + */ + int EVT_SESSION_EXPIRED = 2; + + /** + * Event type used when a session has been accessed (ie, last access time has been updated. This is used so that the + * replicated sessions will not expire on the network + */ + int EVT_SESSION_ACCESSED = 3; + /** + * Event type used when a server comes online for the first time. The first thing the newly started server wants to + * do is to grab the all the sessions from one of the nodes and keep the same state in there + */ + int EVT_GET_ALL_SESSIONS = 4; + /** + * Event type used when an attribute has been added to a session, the attribute will be sent to all the other nodes + * in the cluster + */ + int EVT_SESSION_DELTA = 13; + + /** + * When a session state is transferred, this is the event. + */ + int EVT_ALL_SESSION_DATA = 12; + + /** + * When a session state is complete transferred, this is the event. + */ + int EVT_ALL_SESSION_TRANSFERCOMPLETE = 14; + + /** + * Event type used when a sessionID has been changed. + */ + int EVT_CHANGE_SESSION_ID = 15; + + /** + * Event type used when context manager doesn't exist. This is used when the manager which send a session state does + * not exist. + */ + int EVT_ALL_SESSION_NOCONTEXTMANAGER = 16; + + String getContextName(); + + String getEventTypeString(); + + /** + * returns the event type + * + * @return one of the event types EVT_XXXX + */ + int getEventType(); + + /** + * @return the serialized data for the session + */ + byte[] getSession(); + + /** + * @return the session ID for the session + */ + String getSessionID(); + + +}// SessionMessage diff --git a/java/org/apache/catalina/ha/session/SessionMessageImpl.java b/java/org/apache/catalina/ha/session/SessionMessageImpl.java new file mode 100644 index 0000000..bb54b5d --- /dev/null +++ b/java/org/apache/catalina/ha/session/SessionMessageImpl.java @@ -0,0 +1,175 @@ +/* + * 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.catalina.ha.session; + + +import org.apache.catalina.ha.ClusterMessageBase; + +/** + * Session cluster message + * + * @author Peter Rossbach + */ +public class SessionMessageImpl extends ClusterMessageBase implements SessionMessage { + + private static final long serialVersionUID = 2L; + + + /* + * Private serializable variables to keep the messages state + */ + private final int mEvtType; + private final byte[] mSession; + private final String mSessionID; + + private final String mContextName; + private long serializationTimestamp; + private boolean timestampSet = false; + private String uniqueId; + + + private SessionMessageImpl(String contextName, int eventtype, byte[] session, String sessionID) { + mEvtType = eventtype; + mSession = session; + mSessionID = sessionID; + mContextName = contextName; + uniqueId = sessionID; + } + + /** + * Creates a session message. Depending on what event type you want this message to represent, you populate the + * different parameters in the constructor
    + * The following rules apply dependent on what event type argument you use:
    + * EVT_SESSION_CREATED
    + * The parameters: session, sessionID must be set.
    + * EVT_SESSION_EXPIRED
    + * The parameters: sessionID must be set.
    + * EVT_SESSION_ACCESSED
    + * The parameters: sessionID must be set.
    + * EVT_GET_ALL_SESSIONS
    + * get all sessions from from one of the nodes.
    + * EVT_SESSION_DELTA
    + * Send attribute delta (add,update,remove attribute or principal, ...).
    + * EVT_ALL_SESSION_DATA
    + * Send complete serializes session list
    + * EVT_ALL_SESSION_TRANSFERCOMPLETE
    + * send that all session state information are transferred after GET_ALL_SESSION received from this sender.
    + * EVT_CHANGE_SESSION_ID
    + * send original sessionID and new sessionID.
    + * EVT_ALL_SESSION_NOCONTEXTMANAGER
    + * send that context manager does not exist after GET_ALL_SESSION received from this sender.
    + * + * @param contextName - the name of the context (application + * @param eventtype - one of the 8 event type defined in this class + * @param session - the serialized byte array of the session itself + * @param sessionID - the id that identifies this session + * @param uniqueID - the id that identifies this message + */ + public SessionMessageImpl(String contextName, int eventtype, byte[] session, String sessionID, String uniqueID) { + this(contextName, eventtype, session, sessionID); + uniqueId = uniqueID; + } + + /** + * returns the event type + * + * @return one of the event types EVT_XXXX + */ + @Override + public int getEventType() { + return mEvtType; + } + + /** + * @return the serialized data for the session + */ + @Override + public byte[] getSession() { + return mSession; + } + + /** + * @return the session ID for the session + */ + @Override + public String getSessionID() { + return mSessionID; + } + + /** + * set message send time but only the first setting works (one shot) + * @param time the timestamp + */ + @Override + public void setTimestamp(long time) { + if (!timestampSet) { + serializationTimestamp = time; + timestampSet = true; + } + } + + @Override + public long getTimestamp() { + return serializationTimestamp; + } + + /** + * clear text event type name (for logging purpose only) + * + * @return the event type in a string representation, useful for debugging + */ + @Override + public String getEventTypeString() { + switch (mEvtType) { + case EVT_SESSION_CREATED: + return "SESSION-MODIFIED"; + case EVT_SESSION_EXPIRED: + return "SESSION-EXPIRED"; + case EVT_SESSION_ACCESSED: + return "SESSION-ACCESSED"; + case EVT_GET_ALL_SESSIONS: + return "SESSION-GET-ALL"; + case EVT_SESSION_DELTA: + return "SESSION-DELTA"; + case EVT_ALL_SESSION_DATA: + return "ALL-SESSION-DATA"; + case EVT_ALL_SESSION_TRANSFERCOMPLETE: + return "SESSION-STATE-TRANSFERRED"; + case EVT_CHANGE_SESSION_ID: + return "SESSION-ID-CHANGED"; + case EVT_ALL_SESSION_NOCONTEXTMANAGER: + return "NO-CONTEXT-MANAGER"; + default: + return "UNKNOWN-EVENT-TYPE"; + } + } + + @Override + public String getContextName() { + return mContextName; + } + + @Override + public String getUniqueId() { + return uniqueId; + } + + @Override + public String toString() { + return getEventTypeString() + "#" + getContextName() + "#" + getSessionID(); + } +} diff --git a/java/org/apache/catalina/ha/session/mbeans-descriptors.xml b/java/org/apache/catalina/ha/session/mbeans-descriptors.xml new file mode 100644 index 0000000..c3df0ed --- /dev/null +++ b/java/org/apache/catalina/ha/session/mbeans-descriptors.xml @@ -0,0 +1,634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/ha/tcp/Constants.java b/java/org/apache/catalina/ha/tcp/Constants.java new file mode 100644 index 0000000..7b7c34c --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/Constants.java @@ -0,0 +1,28 @@ +/* + * 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.catalina.ha.tcp; + +/** + * Manifest constants for the org.apache.catalina.ha.tcp package. + * + * @author Peter Rossbach + */ +public class Constants { + + public static final String Package = "org.apache.catalina.ha.tcp"; + +} diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings.properties b/java/org/apache/catalina/ha/tcp/LocalStrings.properties new file mode 100644 index 0000000..e852548 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings.properties @@ -0,0 +1,45 @@ +# 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. + +ReplicationValve.crossContext.add=add Cross Context session replication container to replicationValve threadlocal +ReplicationValve.crossContext.registerSession=register Cross context session id=[{0}] from context [{1}] +ReplicationValve.crossContext.remove=remove Cross Context session replication container from replicationValve threadlocal +ReplicationValve.crossContext.sendDelta=send Cross Context session delta from context [{0}]. +ReplicationValve.filter.failure=Unable to compile filter=[{0}] +ReplicationValve.filter.loading=Loading request filter=[{0}] +ReplicationValve.invoke.uri=Invoking replication request on [{0}] +ReplicationValve.nocluster=No cluster configured for this request. +ReplicationValve.resetDeltaRequest=Cluster is standalone: reset Session Request Delta at context [{0}] +ReplicationValve.send.failure=Unable to perform replication request. +ReplicationValve.send.invalid.failure=Unable to send session [id={0}] invalid message over cluster. +ReplicationValve.session.found=Context [{0}]: Found session [{1}] but it isn''t a ClusterSession. +ReplicationValve.session.indicator=Context [{0}]: Primarity of session [{1}] in request attribute [{2}] is [{3}]. +ReplicationValve.session.invalid=Context [{0}]: Requested session [{1}] is invalid, removed or not replicated at this node. +ReplicationValve.stats=Average request time=[{0}] ms with cluster overhead time=[{1}] ms for [{2}] requests, [{3}] send requests, [{4}] cross context requests, and [{5}] filter requests (Total request=[{6}] ms, total cluster request=[{7}] ms). + +simpleTcpCluster.clustermanager.cloneFailed=Unable to clone cluster manager, defaulting to org.apache.catalina.ha.session.DeltaManager +simpleTcpCluster.clustermanager.notImplement=Manager [{0}] does not implement ClusterManager, addition to cluster has been aborted. +simpleTcpCluster.createManager=Creating ClusterManager for context [{0}] using class [{1}] +simpleTcpCluster.member.addFailed=Unable to connect to replication system. +simpleTcpCluster.member.added=Replication member added:[{0}] +simpleTcpCluster.member.disappeared=Received member disappeared:[{0}] +simpleTcpCluster.member.removeFailed=Unable remove cluster node from replication system. +simpleTcpCluster.noListener=Message [{0}] from type [{1}] transferred but no listener registered +simpleTcpCluster.noMembers=No members in cluster, ignoring message [{0}] +simpleTcpCluster.sendFailed=Unable to send message through cluster sender. +simpleTcpCluster.start=Cluster is about to start +simpleTcpCluster.startUnable=Unable to start cluster. +simpleTcpCluster.stopUnable=Unable to stop cluster. +simpleTcpCluster.unableSend.localMember=Unable to send message to local member [{0}] diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_cs.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_cs.properties new file mode 100644 index 0000000..e384e29 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_cs.properties @@ -0,0 +1,21 @@ +# 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. + +ReplicationValve.filter.failure=Nelze zkompilovat filtr=[{0}] +ReplicationValve.session.found=Kontext [{0}]: Nalezena relace [{1}], která není ClusterSession. + +simpleTcpCluster.clustermanager.notImplement=Manager [{0}] neimplementuje ClusterManager, pÅ™idání do clusteru bylo pÅ™eruÅ¡eno. +simpleTcpCluster.member.removeFailed=Nelze odebrat nód clusteru z replikaÄního systému. +simpleTcpCluster.stopUnable=Nelze zastavit cluster diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_de.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_de.properties new file mode 100644 index 0000000..ca8a9bb --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_de.properties @@ -0,0 +1,20 @@ +# 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. + +ReplicationValve.filter.failure=Kann Filter [{0}] nicht kompilieren +ReplicationValve.session.indicator=Context [{0}]: Primärität der Session [{1}] in Request Attribut [{2}] ist [{3}]. + +simpleTcpCluster.clustermanager.notImplement=Manager [{0}] implementiert nicht ClusterManager. Das Hinzufügen dieses Managers zum Cluster wurde daher abgebrochen. +simpleTcpCluster.stopUnable=Cluster kann nicht gestoppt werden. diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_es.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_es.properties new file mode 100644 index 0000000..4e106ec --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_es.properties @@ -0,0 +1,34 @@ +# 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. + +ReplicationValve.crossContext.add=añadir contenedor de réplica de sesión de Contexto Cruzado a replicationValve threadlocal +ReplicationValve.crossContext.registerSession=retistrar id de sesión de Contexto Cruzado=[{0}] desde contexto [{1}] +ReplicationValve.crossContext.remove=quitar contenedor de réplica de sesión de Contexto Cruzado a replicationValve threadlocal +ReplicationValve.crossContext.sendDelta=enviar delta de sesión de Contexto Cruzado desde contexto [{0}]. +ReplicationValve.filter.failure=No puedo compilar filtror=[{0}] +ReplicationValve.filter.loading=Cargando filtros de requerimiento=[{0}] +ReplicationValve.invoke.uri=Invocando requerimiento de réplica en [{0}] +ReplicationValve.nocluster=No cluster configured for this request. +ReplicationValve.resetDeltaRequest=Cluster is standalone: reset Session Request Delta at context [{0}] +ReplicationValve.send.failure=Unable to perform replication request. +ReplicationValve.send.invalid.failure=Unable to send session [id={0}] invalid message over cluster. +ReplicationValve.session.found=Context [{0}]: Found session [{1}] but it isn''t a ClusterSession. +ReplicationValve.session.invalid=Context [{0}]: Requested session [{1}] is invalid, removed or not replicated at this node. +ReplicationValve.stats=Average request time= [{0}] ms for Cluster overhead time=[{1}] ms for [{2}] requests [{3}] filter requests [{4}] send requests [{5}] cross context requests (Request=[{6}] ms Cluster=[{7}] ms). + +simpleTcpCluster.clustermanager.notImplement=Manejador [{0}] no implementa ClusterManager, la adición al cluster ha sido abortada.\n +simpleTcpCluster.member.addFailed=Incapaz de conectar con el sistema de replicación +simpleTcpCluster.member.removeFailed=Imposible remover el nodo del sistema de replicación +simpleTcpCluster.stopUnable=Inmposible deterner el cluster diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_fr.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_fr.properties new file mode 100644 index 0000000..9fb368d --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_fr.properties @@ -0,0 +1,45 @@ +# 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. + +ReplicationValve.crossContext.add=Ajout du conteneur de réplication de la session multi contexte au ThreadLocal de replicationValve +ReplicationValve.crossContext.registerSession=enregistrement de la session multi contexte id=[{0}] du contexte [{1}] +ReplicationValve.crossContext.remove=Retrait du conteneur de réplication de la session multi contexte au ThreadLocal de replicationValve +ReplicationValve.crossContext.sendDelta=Envoi du delta de la session multi contexte du contexte [{0}] +ReplicationValve.filter.failure=Incapacité de compiler le filtre=[{0}] +ReplicationValve.filter.loading=Chargement du filtre de requête [{0}] +ReplicationValve.invoke.uri=Invocation de la requête de réplication sur [{0}] +ReplicationValve.nocluster=Aucun cluster de configuré pour cette requête +ReplicationValve.resetDeltaRequest=Le cluster se suffit à lui-même : réinitialisation du delta de la requête de session [{0}] +ReplicationValve.send.failure=Impossible d'effectuer la requête de réplication +ReplicationValve.send.invalid.failure=Incapable d''envoyer le message invalide de la session [id={0}] sur le cluster +ReplicationValve.session.found=Le Contexte [{0}] a touvé la session [{1}] mais ce n''est pas une ClusterSession. +ReplicationValve.session.indicator=Contexte [{0}] : la primarité de la session [{1}] dans l''attribut de requête [{2}] est [{3}]. +ReplicationValve.session.invalid=Contexte [{0}] : la session demandée [{1}] est invalide, non répliquée, ou enlevée sur ce nÅ“ud +ReplicationValve.stats=Temps de requête moyen= [{0}] ms pour le Cluster le temps ajouté est de=[{1}] ms pour [{2}] requêtes [{3}] requêtes d''envoi [{4}] requêtes multi contextes et [{5}] requêtes fitrées (Total requêtes=[{6}] ms total requêtes du cluster=[{7}] ms) + +simpleTcpCluster.clustermanager.cloneFailed=Impossible de cloner le gestionnaire du cluster, le org.apache.catalina.ha.session.DeltaManager par défaut sera utilisé +simpleTcpCluster.clustermanager.notImplement=Le gestionnaire ("Manager") [{0}] n''implémente pas ClusterManager. Son ajout au cluster a été abandonné. +simpleTcpCluster.createManager=Création d''un ClusterManager pour le contexte [{0}] en utilisant la classe [{1}] +simpleTcpCluster.member.addFailed=Impossible de se connecter au système de réplication +simpleTcpCluster.member.added=Membre de réplication ajouté : [{0}] +simpleTcpCluster.member.disappeared=Le membre recu a disparu : [{0}] +simpleTcpCluster.member.removeFailed=Impossible d'enlever un nÅ“ud du cluster du système de réplication +simpleTcpCluster.noListener=Le message [{0}] de type [{1}] a été transféré mais aucun écouteur n''est enregistré +simpleTcpCluster.noMembers=Aucun membre dans le cluster, le message [{0}] est ignoré +simpleTcpCluster.sendFailed=Impossible d'envoyer un message à travers l'expéditeur du cluster +simpleTcpCluster.start=Le cluster va démarrer +simpleTcpCluster.startUnable=Impossible de démarre le cluster +simpleTcpCluster.stopUnable=Incapable d'arrêter le cluster +simpleTcpCluster.unableSend.localMember=Impossible d''envoyer un message au membre local [{0}] diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_ja.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_ja.properties new file mode 100644 index 0000000..1946be9 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_ja.properties @@ -0,0 +1,45 @@ +# 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. + +ReplicationValve.crossContext.add=クロスコンテキストセッションレプリケーションコンテナをreplicationValveスレッドローカルã«è¿½åŠ  +ReplicationValve.crossContext.registerSession=コンテキスト [{1}] ã‹ã‚‰ã‚¯ãƒ­ã‚¹ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚»ãƒƒã‚·ãƒ§ãƒ³ID = [{0}] を登録ã™ã‚‹ +ReplicationValve.crossContext.remove=replication Contextセッションレプリケーションコンテナをスレッドローカルã‹ã‚‰å‰Šé™¤ã—ã¾ã™ã€‚ +ReplicationValve.crossContext.sendDelta=コンテキスト [{0}] ã‹ã‚‰ã®ã‚¯ãƒ­ã‚¹ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚»ãƒƒã‚·ãƒ§ãƒ³ãƒ‡ãƒ«ã‚¿ã‚’é€ä¿¡ã—ã¾ã™ã€‚ +ReplicationValve.filter.failure=フィルター文字列=[{0}] ãŒã‚³ãƒ³ãƒ‘イルã§ãã¾ã›ã‚“ +ReplicationValve.filter.loading=リクエストフィルタ= [{0}]ã®ãƒ­ãƒ¼ãƒ‰ +ReplicationValve.invoke.uri=[{0}]ã®ãƒ¬ãƒ—リケーションリクエストを呼ã³å‡ºã—ã¾ã™ã€‚ +ReplicationValve.nocluster=ã“ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾ã—ã¦æ§‹æˆã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ã‚¿ã¯ã‚ã‚Šã¾ã›ã‚“。 +ReplicationValve.resetDeltaRequest=クラスタã¯ã‚¹ã‚¿ãƒ³ãƒ‰ã‚¢ãƒ­ãƒ³ã§ã‚る:コンテキスト [{0}] ã§ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®ãƒ‡ãƒ«ã‚¿ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’リセットã—ã¾ã™ +ReplicationValve.send.failure=レプリケーションリクエストを実行ã§ãã¾ã›ã‚“。 +ReplicationValve.send.invalid.failure=セッション[id = {0}]無効メッセージをクラスタã«é€ä¿¡ã§ãã¾ã›ã‚“。 +ReplicationValve.session.found=コンテキスト [{0}]: セッション [{1}] ã¯è¦‹ã¤ã‹ã‚Šã¾ã—ãŸãŒã€ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ã‚»ãƒƒã‚·ãƒ§ãƒ³ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +ReplicationValve.session.indicator=Context [{0}]:リクエスト属性 [{2}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{1}] ã®ãƒ—ライマリ㯠[{3}] ã§ã™ã€‚ +ReplicationValve.session.invalid=コンテキスト [{0}]: 無効ãªã‚»ãƒƒã‚·ãƒ§ãƒ³ [{1}] ãŒè¦æ±‚ã•ã‚Œã¾ã—ãŸã€‚消去ã•ã‚ŒãŸã€ã‚ã‚‹ã„ã¯ã€ã“ã®ãƒŽãƒ¼ãƒ‰ã«è¤‡è£½ã•ã‚Œãªã‹ã£ãŸå¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +ReplicationValve.stats=[{2}] リクエストã®å¹³å‡è¦æ±‚時間= [{0}] ミリ秒ã€ã‚¯ãƒ©ã‚¹ã‚¿ã‚ªãƒ¼ãƒãƒ¼ãƒ˜ãƒƒãƒ‰æ™‚é–“= [{1}] ミリ秒ã€[{3}] リクエストã®é€ä¿¡ã€[{4}] クロスコンテキストリクエストã€[{5}] フィルタリクエスト(åˆè¨ˆãƒªã‚¯ã‚¨ã‚¹ãƒˆ= [{6}] ミリ秒ã€ã‚¯ãƒ©ã‚¹ã‚¿å…¨ä½“リクエスト= [{7}] ミリ秒)。 + +simpleTcpCluster.clustermanager.cloneFailed=クラスタマãƒãƒ¼ã‚¸ãƒ£ã‚’クローンã§ãã¾ã›ã‚“。既定ã¯org.apache.catalina.ha.session.DeltaManagerã§ã™ +simpleTcpCluster.clustermanager.notImplement=マãƒãƒ¼ã‚¸ãƒ£ [{0}] 㯠ClusterManager を実装ã—ã¦ã„ã¾ã›ã‚“。ãã‚Œã«ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ã¯ã™ã§ã«åœæ­¢ã—ã¦ã„ã¾ã™ã€‚ +simpleTcpCluster.createManager=クラス [{1}] を使用ã—ã¦ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] ã® ClusterManager を作æˆã—ã¦ã„ã¾ã™ +simpleTcpCluster.member.addFailed=レプリケーションシステムã«æŽ¥ç¶šã§ãã¾ã›ã‚“。 +simpleTcpCluster.member.added=レプリケーションメンãƒãƒ¼ã‚’追加ã—ã¾ã—ãŸ: [{0}] +simpleTcpCluster.member.disappeared=メッセージ消失をå—ä¿¡ã—ã¾ã—ãŸ: [{0}] +simpleTcpCluster.member.removeFailed=レプリケーションシステムã‹ã‚‰ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ãƒŽãƒ¼ãƒ‰ã‚’削除ã§ãã¾ã›ã‚“。 +simpleTcpCluster.noListener=タイプ [{1}] ã‹ã‚‰ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ [{0}] ãŒè»¢é€ã•ã‚Œã¾ã—ãŸãŒã€ãƒªã‚¹ãƒŠãƒ¼ãŒç™»éŒ²ã•ã‚Œã¦ã„ã¾ã›ã‚“ +simpleTcpCluster.noMembers=クラスタã«ãƒ¡ãƒ³ãƒãŒã„ã¾ã›ã‚“。メッセージ [{0}] ã¯ç„¡è¦–ã•ã‚Œã¾ã™ +simpleTcpCluster.sendFailed=クラスタセンダ経由ã§ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 +simpleTcpCluster.start=Clusterã‚’èµ·å‹•ã—ã¾ã™ã€‚ +simpleTcpCluster.startUnable=クラスタを起動ã§ãã¾ã›ã‚“。 +simpleTcpCluster.stopUnable=クラスタをåœæ­¢ã§ãã¾ã›ã‚“。 +simpleTcpCluster.unableSend.localMember=ローカルメンãƒãƒ¼ [{0}] ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ãã¾ã›ã‚“ diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_ko.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_ko.properties new file mode 100644 index 0000000..0194309 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_ko.properties @@ -0,0 +1,42 @@ +# 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. + +ReplicationValve.crossContext.add=êµì°¨ 컨í…스트 세션 복제 컨테ì´ë„ˆë¥¼ replicationValveì˜ threadlocalì— ì¶”ê°€í•©ë‹ˆë‹¤. +ReplicationValve.crossContext.registerSession=컨í…스트 [{1}](으)로부터 세션 IDê°€ [{0}]ì¸ êµì°¨ 컨í…스트 ì„¸ì…˜ì„ ë“±ë¡í•©ë‹ˆë‹¤. +ReplicationValve.crossContext.remove=replicationValveì˜ threadlocal로부터, êµì°¨ 컨í…스트 세션 복제 컨테ì´ë„ˆë¥¼ 제거합니다. +ReplicationValve.crossContext.sendDelta=컨í…스트 [{0}](으)로부터 êµì°¨ 컨í…스트 세션 ë¸íƒ€ë¥¼ 보냅니다. +ReplicationValve.filter.failure=í•„í„° 컴파ì¼ì„ í•  수 없습니다. filter=[{0}] +ReplicationValve.filter.loading=요청 필터를 로드합니다: [{0}] +ReplicationValve.invoke.uri=[{0}]ì— ë³µì œ ìš”ì²­ì„ í˜¸ì¶œí•©ë‹ˆë‹¤. +ReplicationValve.nocluster=ì´ ìš”ì²­ì„ ìœ„í•´ ì„¤ì •ëœ í´ëŸ¬ìŠ¤í„°ê°€ 없습니다. +ReplicationValve.resetDeltaRequest=í´ëŸ¬ìŠ¤í„°ê°€ ë…립형(standalone)입니다: 컨í…스트 [{0}]ì—ì„œ 세션 요청 ë¸íƒ€ë¥¼ 재설정(reset)합니다. +ReplicationValve.send.failure=복제 ìš”ì²­ì„ ìˆ˜í–‰ í•  수 없습니다. +ReplicationValve.send.invalid.failure=세션 [id={0}] 유효하지 ì•ŠìŒ ë©”ì‹œì§€ë¥¼ í´ëŸ¬ìŠ¤í„°ì— 전송할 수 없습니다. +ReplicationValve.session.found=컨í…스트 [{0}]ì—ì„œ 세션 [{1}]ì„(를) 발견했으나, ì´ëŠ” ClusterSessionì´ ì•„ë‹™ë‹ˆë‹¤. +ReplicationValve.session.indicator=컨í…스트 [{0}]: 요청 ì†ì„± [{2}]ì— ìžˆëŠ” 세션 [{1}]ì˜ Primary 여부: [{3}] +ReplicationValve.session.invalid=컨í…스트 [{0}]: ìš”ì²­ëœ ì„¸ì…˜ [{1}]ì´(ê°€), 유효하지 않거나, 제거ë˜ì—ˆê±°ë‚˜, ë˜ëŠ” ì´ í´ëŸ¬ìŠ¤í„° 노드로 복제ë˜ì§€ 않았습니다. +ReplicationValve.stats=[{2}]ê°œì˜ ìš”ì²­ë“¤, [{3}]ê°œì˜ ì „ì†¡ 요청들, [{4}]ê°œì˜ êµì°¨ 컨í…스트 요청들, 그리고 [{5}]ê°œì˜ í•„í„° ìš”ì²­ë“¤ì„ ì²˜ë¦¬í•˜ëŠ” ë™ì•ˆ, í‰ê·  요청 시간=[{0}] 밀리초, í´ëŸ¬ìŠ¤í„° 오버헤드 시간=[{1}] 밀리초가 소요ë˜ì—ˆìŠµë‹ˆë‹¤. (ì´ ìš”ì²­ 처리 시간=[{6}] 밀리초, ì´ í´ëŸ¬ìŠ¤í„° 요청 처리 시간=[{7}] 밀리초) + +simpleTcpCluster.clustermanager.cloneFailed=í´ëŸ¬ìŠ¤í„° 매니저를 복제할 수 없습니다. 기본 ê°’ì¸ org.apache.catalina.ha.session.DeltaManager를 사용합니다. +simpleTcpCluster.clustermanager.notImplement=매니저 [{0}]ì´(ê°€) ClusterManager ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 구현하지 않습니다. í´ëŸ¬ìŠ¤í„°ì— 추가하려는 ì‹œë„는 중단ë©ë‹ˆë‹¤. +simpleTcpCluster.member.addFailed=복제 ì‹œìŠ¤í…œì— ì—°ê²°í•  수 없습니다. +simpleTcpCluster.member.added=복제 멤버가 추가ë¨: [{0}] +simpleTcpCluster.member.disappeared=멤버 사ë¼ì§ 메시지를 수신했습니다: [{0}] +simpleTcpCluster.member.removeFailed=복제 시스템으로부터 í´ëŸ¬ìŠ¤í„° 노드를 제거할 수 없습니다. +simpleTcpCluster.sendFailed=í´ëŸ¬ìŠ¤í„° sender를 통해 메시지를 보낼 수 없습니다. +simpleTcpCluster.start=í´ëŸ¬ìŠ¤í„°ê°€ 막 시작하려 합니다. +simpleTcpCluster.startUnable=í´ëŸ¬ìŠ¤í„°ë¥¼ 시작할 수 없습니다. +simpleTcpCluster.stopUnable=í´ëŸ¬ìŠ¤í„°ë¥¼ 중지시킬 수 없습니다. +simpleTcpCluster.unableSend.localMember=로컬 멤버 [{0}]ì—게 메시지를 보낼 수 없습니다. diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_pt_BR.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..a2ab44a --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +ReplicationValve.session.found=Contexto [{0}]: encontrada sessão [{1}] mas não é uma "ClusterSession" diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_ru.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_ru.properties new file mode 100644 index 0000000..97ce091 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_ru.properties @@ -0,0 +1,19 @@ +# 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. + +ReplicationValve.filter.failure=Ðе возможно Ñкомпилировать фильтр=[{0}] + +simpleTcpCluster.member.addFailed=Ðевозможно подключитьÑÑ Ðº ÑиÑтеме репликации +simpleTcpCluster.stopUnable=Ðевозможно оÑтановить клаÑтер diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings_zh_CN.properties b/java/org/apache/catalina/ha/tcp/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..ce7af72 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/LocalStrings_zh_CN.properties @@ -0,0 +1,42 @@ +# 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. + +ReplicationValve.crossContext.add=将跨上下文会è¯å¤åˆ¶å®¹å™¨æ·»åŠ åˆ°replicationValve threadlocal +ReplicationValve.crossContext.registerSession=注册交å‰ä¸Šä¸‹æ–‡ä¼šè¯id =[{0}]æ¥è‡ªä¸Šä¸‹æ–‡[{1}] +ReplicationValve.crossContext.remove=从replicationValve threadlocal中删除跨上下文会è¯å¤åˆ¶å®¹å™¨ +ReplicationValve.crossContext.sendDelta=从上下文[{0}]å‘é€è·¨ä¸Šä¸‹æ–‡ä¼šè¯å¢žé‡ã€‚ +ReplicationValve.filter.failure=无法编译 filter = [{0}] +ReplicationValve.filter.loading=正在加载请求筛选器=[{0}] +ReplicationValve.invoke.uri=在[{0}]上执行åŒæ­¥è¯·æ±‚ +ReplicationValve.nocluster=没有为此请求é…置集群。 +ReplicationValve.resetDeltaRequest=集群是独立的:在上下文[{0}]处é‡ç½®ä¼šè¯è¯·æ±‚å¢žé‡ +ReplicationValve.send.failure=无法执行åŒæ­¥è¯·æ±‚。 +ReplicationValve.send.invalid.failure=无法通过集群å‘é€ä¼šè¯[id={0}]无效消æ¯ã€‚ +ReplicationValve.session.found=上下文[{0}]:找到会è¯[{1}]但它ä¸æ˜¯ClusterSession。 +ReplicationValve.session.indicator=上下文[{0}]:请求属性[{2}]中会è¯[{1}]的优先级为[{3}]。 +ReplicationValve.session.invalid=上下文[{0}]:请求的会è¯[{1}]在此节点上无效,已删除或未å¤åˆ¶ã€‚ +ReplicationValve.stats=对于[{2}]请求ã€[{3}]å‘é€è¯·æ±‚ã€[{4}]跨上下文请求和[{5}]筛选请求,平å‡è¯·æ±‚时间为[{0}]ms,集群开销时间为[{1}]ms(总请求时间为[{6}]ms,总集群请求时间为[{7}]ms)。 + +simpleTcpCluster.clustermanager.cloneFailed=无法克隆集群管ç†å™¨ï¼Œé»˜è®¤ä¸ºorg.apache.catalina.ha.session.DeltaManager。 +simpleTcpCluster.clustermanager.notImplement=连接器 [{0}] ä¸èƒ½ç»§æ‰¿ ClusterManager,除éžé›†ç¾¤è¢«åœæ­¢ã€‚ +simpleTcpCluster.member.addFailed=无法连接到å¤åˆ¶ç³»ç»Ÿã€‚ +simpleTcpCluster.member.added=添加åŒæ­¥æˆå‘˜ï¼š[{0}] +simpleTcpCluster.member.disappeared=收到æˆå‘˜æ¶ˆå¤±:[{0}] +simpleTcpCluster.member.removeFailed=无法从å¤åˆ¶ç³»ç»Ÿä¸­ç§»é™¤é›†ç¾¤èŠ‚点 +simpleTcpCluster.sendFailed=无法使用集群å‘é€å™¨å‘é€æ¶ˆæ¯ +simpleTcpCluster.start=集群å³å°†å¯åŠ¨ +simpleTcpCluster.startUnable=无法å¯åŠ¨é›†ç¾¤ã€‚ +simpleTcpCluster.stopUnable=无法åœæ­¢é›†ç¾¤ +simpleTcpCluster.unableSend.localMember=无法将消æ¯å‘é€åˆ°æœ¬åœ°æˆå‘˜[{0}] diff --git a/java/org/apache/catalina/ha/tcp/ReplicationValve.java b/java/org/apache/catalina/ha/tcp/ReplicationValve.java new file mode 100644 index 0000000..4470d50 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/ReplicationValve.java @@ -0,0 +1,611 @@ +/* + * 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.catalina.ha.tcp; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.Cluster; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.ClusterMessage; +import org.apache.catalina.ha.ClusterSession; +import org.apache.catalina.ha.ClusterValve; +import org.apache.catalina.ha.session.DeltaManager; +import org.apache.catalina.ha.session.DeltaSession; +import org.apache.catalina.valves.ValveBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * Implementation of a Valve that logs interesting contents from the specified Request (before processing) and the + * corresponding Response (after processing). It is especially useful in debugging problems related to headers and + * cookies. + *

    + *

    + * This Valve may be attached to any Container, depending on the granularity of the logging you wish to perform. + *

    + *

    + * primaryIndicator=true, then the request attribute org.apache.catalina.ha.tcp.isPrimarySession. is set true, + * when request processing is at sessions primary node. + *

    + * + * @author Craig R. McClanahan + * @author Peter Rossbach + */ +public class ReplicationValve extends ValveBase implements ClusterValve { + + private static final Log log = LogFactory.getLog(ReplicationValve.class); + + // ----------------------------------------------------- Instance Variables + + /** + * The StringManager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + private CatalinaCluster cluster = null; + + /** + * Filter expression + */ + protected Pattern filter = null; + + /** + * crossContext session container + */ + protected final ThreadLocal> crossContextSessions = new ThreadLocal<>(); + + /** + * doProcessingStats (default = off) + */ + protected boolean doProcessingStats = false; + + protected LongAdder totalRequestTime = new LongAdder(); + protected LongAdder totalSendTime = new LongAdder(); + protected LongAdder nrOfRequests = new LongAdder(); + protected AtomicLong lastSendTime = new AtomicLong(); + protected LongAdder nrOfFilterRequests = new LongAdder(); + protected LongAdder nrOfSendRequests = new LongAdder(); + protected LongAdder nrOfCrossContextSendRequests = new LongAdder(); + + /** + * must primary change indicator set + */ + protected boolean primaryIndicator = false; + + /** + * Name of primary change indicator as request attribute + */ + protected String primaryIndicatorName = "org.apache.catalina.ha.tcp.isPrimarySession"; + + // ------------------------------------------------------------- Properties + + public ReplicationValve() { + super(true); + } + + /** + * @return the cluster. + */ + @Override + public CatalinaCluster getCluster() { + return cluster; + } + + /** + * @param cluster The cluster to set. + */ + @Override + public void setCluster(CatalinaCluster cluster) { + this.cluster = cluster; + } + + /** + * @return the filter + */ + public String getFilter() { + if (filter == null) { + return null; + } + return filter.toString(); + } + + /** + * compile filter string to regular expression + * + * @see Pattern#compile(String) + * + * @param filter The filter to set. + */ + public void setFilter(String filter) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("ReplicationValve.filter.loading", filter)); + } + + if (filter == null || filter.length() == 0) { + this.filter = null; + } else { + try { + this.filter = Pattern.compile(filter); + } catch (PatternSyntaxException pse) { + log.error(sm.getString("ReplicationValve.filter.failure", filter), pse); + } + } + } + + /** + * @return the primaryIndicator. + */ + public boolean isPrimaryIndicator() { + return primaryIndicator; + } + + /** + * @param primaryIndicator The primaryIndicator to set. + */ + public void setPrimaryIndicator(boolean primaryIndicator) { + this.primaryIndicator = primaryIndicator; + } + + /** + * @return the primaryIndicatorName. + */ + public String getPrimaryIndicatorName() { + return primaryIndicatorName; + } + + /** + * @param primaryIndicatorName The primaryIndicatorName to set. + */ + public void setPrimaryIndicatorName(String primaryIndicatorName) { + this.primaryIndicatorName = primaryIndicatorName; + } + + /** + * Calc processing stats + * + * @return true if statistics are enabled + */ + public boolean doStatistics() { + return doProcessingStats; + } + + /** + * Set Calc processing stats + * + * @param doProcessingStats New flag value + * + * @see #resetStatistics() + */ + public void setStatistics(boolean doProcessingStats) { + this.doProcessingStats = doProcessingStats; + } + + /** + * @return the lastSendTime. + */ + public long getLastSendTime() { + return lastSendTime.longValue(); + } + + /** + * @return the nrOfRequests. + */ + public long getNrOfRequests() { + return nrOfRequests.longValue(); + } + + /** + * @return the nrOfFilterRequests. + */ + public long getNrOfFilterRequests() { + return nrOfFilterRequests.longValue(); + } + + /** + * @return the nrOfCrossContextSendRequests. + */ + public long getNrOfCrossContextSendRequests() { + return nrOfCrossContextSendRequests.longValue(); + } + + /** + * @return the nrOfSendRequests. + */ + public long getNrOfSendRequests() { + return nrOfSendRequests.longValue(); + } + + /** + * @return the totalRequestTime. + */ + public long getTotalRequestTime() { + return totalRequestTime.longValue(); + } + + /** + * @return the totalSendTime. + */ + public long getTotalSendTime() { + return totalSendTime.longValue(); + } + + // --------------------------------------------------------- Public Methods + + /** + * Register all cross context sessions inside endAccess. Use a list with contains check, that the Portlet API can + * include a lot of fragments from same or different applications with session changes. + * + * @param session cross context session + */ + public void registerReplicationSession(DeltaSession session) { + List sessions = crossContextSessions.get(); + if (sessions != null) { + if (!sessions.contains(session)) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("ReplicationValve.crossContext.registerSession", session.getIdInternal(), + session.getManager().getContext().getName())); + } + sessions.add(session); + } + } + } + + /** + * Log the interesting request parameters, invoke the next Valve in the sequence, and log the interesting response + * parameters. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + long totalstart = 0; + + // this happens before the request + if (doStatistics()) { + totalstart = System.currentTimeMillis(); + } + if (primaryIndicator) { + createPrimaryIndicator(request); + } + Context context = request.getContext(); + boolean isCrossContext = context != null && context instanceof StandardContext && context.getCrossContext(); + boolean isAsync = request.getAsyncContextInternal() != null; + try { + if (isCrossContext) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("ReplicationValve.crossContext.add")); + } + crossContextSessions.set(new ArrayList<>()); + } + getNext().invoke(request, response); + if (context != null && cluster != null && context.getManager() instanceof ClusterManager) { + ClusterManager clusterManager = (ClusterManager) context.getManager(); + + // valve cluster can access manager - other cluster handle replication + // at host level - hopefully! + if (cluster.getManager(clusterManager.getName()) == null) { + return; + } + if (cluster.hasMembers()) { + sendReplicationMessage(request, totalstart, isCrossContext, isAsync, clusterManager); + } else { + resetReplicationRequest(request, isCrossContext); + } + } + } finally { + // Array must be remove: Current master request send endAccess at recycle. + // Don't register this request session again! + if (isCrossContext) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("ReplicationValve.crossContext.remove")); + } + crossContextSessions.remove(); + } + } + } + + + /** + * reset the active statistics + */ + public void resetStatistics() { + totalRequestTime.reset(); + totalSendTime.reset(); + lastSendTime.set(0); + nrOfFilterRequests.reset(); + nrOfRequests.reset(); + nrOfSendRequests.reset(); + nrOfCrossContextSendRequests.reset(); + } + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + if (cluster == null) { + Cluster containerCluster = getContainer().getCluster(); + if (containerCluster instanceof CatalinaCluster) { + setCluster((CatalinaCluster) containerCluster); + } else { + if (log.isWarnEnabled()) { + log.warn(sm.getString("ReplicationValve.nocluster")); + } + } + } + super.startInternal(); + } + + + // --------------------------------------------------------- Protected Methods + + protected void sendReplicationMessage(Request request, long totalstart, boolean isCrossContext, boolean isAsync, + ClusterManager clusterManager) { + // this happens after the request + long start = 0; + if (doStatistics()) { + start = System.currentTimeMillis(); + } + try { + // send invalid sessions + sendInvalidSessions(clusterManager); + // send replication + sendSessionReplicationMessage(request, clusterManager); + if (isCrossContext) { + sendCrossContextSession(); + } + } catch (Exception x) { + // FIXME we have a lot of sends, but the trouble with one node stops the correct replication to other nodes! + log.error(sm.getString("ReplicationValve.send.failure"), x); + } finally { + if (doStatistics()) { + updateStats(totalstart, start, isAsync); + } + } + } + + /** + * Send all changed cross context sessions to backups + */ + protected void sendCrossContextSession() { + List sessions = crossContextSessions.get(); + if (sessions != null && sessions.size() > 0) { + for (DeltaSession session : sessions) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("ReplicationValve.crossContext.sendDelta", + session.getManager().getContext().getName())); + } + sendMessage(session, (ClusterManager) session.getManager()); + if (doStatistics()) { + nrOfCrossContextSendRequests.increment(); + } + } + } + } + + /** + * Fix memory leak for long sessions with many changes, when no backup member exists! + * + * @param request current request after response is generated + * @param isCrossContext check crosscontext threadlocal + */ + protected void resetReplicationRequest(Request request, boolean isCrossContext) { + Session contextSession = request.getSessionInternal(false); + if (contextSession instanceof DeltaSession) { + resetDeltaRequest(contextSession); + ((DeltaSession) contextSession).setPrimarySession(true); + } + if (isCrossContext) { + List sessions = crossContextSessions.get(); + if (sessions != null && sessions.size() > 0) { + Iterator iter = sessions.iterator(); + for (; iter.hasNext();) { + Session session = iter.next(); + resetDeltaRequest(session); + if (session instanceof DeltaSession) { + ((DeltaSession) contextSession).setPrimarySession(true); + } + + } + } + } + } + + /** + * Reset DeltaRequest from session + * + * @param session HttpSession from current request or cross context session + */ + protected void resetDeltaRequest(Session session) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("ReplicationValve.resetDeltaRequest", session.getManager().getContext().getName())); + } + ((DeltaSession) session).resetDeltaRequest(); + } + + /** + * Send Cluster Replication Request + * + * @param request current request + * @param manager session manager + */ + protected void sendSessionReplicationMessage(Request request, ClusterManager manager) { + Session session = request.getSessionInternal(false); + if (session != null) { + String uri = request.getDecodedRequestURI(); + // request without session change + if (!isRequestWithoutSessionChange(uri)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("ReplicationValve.invoke.uri", uri)); + } + sendMessage(session, manager); + } else if (doStatistics()) { + nrOfFilterRequests.increment(); + } + } + + } + + /** + * Send message delta message from request session + * + * @param session current session + * @param manager session manager + */ + protected void sendMessage(Session session, ClusterManager manager) { + String id = session.getIdInternal(); + if (id != null) { + send(manager, id); + } + } + + /** + * send manager requestCompleted message to cluster + * + * @param manager SessionManager + * @param sessionId sessionid from the manager + * + * @see DeltaManager#requestCompleted(String) + * @see SimpleTcpCluster#send(ClusterMessage) + */ + protected void send(ClusterManager manager, String sessionId) { + ClusterMessage msg = manager.requestCompleted(sessionId); + if (msg != null && cluster != null) { + cluster.send(msg); + if (doStatistics()) { + nrOfSendRequests.increment(); + } + } + } + + /** + * check for session invalidations + * + * @param manager Associated manager + */ + protected void sendInvalidSessions(ClusterManager manager) { + String[] invalidIds = manager.getInvalidatedSessions(); + if (invalidIds.length > 0) { + for (String invalidId : invalidIds) { + try { + send(manager, invalidId); + } catch (Exception x) { + log.error(sm.getString("ReplicationValve.send.invalid.failure", invalidId), x); + } + } + } + } + + /** + * is request without possible session change + * + * @param uri The request uri + * + * @return True if no session change + */ + protected boolean isRequestWithoutSessionChange(String uri) { + Pattern f = filter; + return f != null && f.matcher(uri).matches(); + } + + /** + * Protocol cluster replications stats + * + * @param requestTime Request time + * @param clusterTime Cluster time + * @param isAsync if the request was in async mode + */ + protected void updateStats(long requestTime, long clusterTime, boolean isAsync) { + long currentTime = System.currentTimeMillis(); + lastSendTime.set(currentTime); + totalSendTime.add(currentTime - clusterTime); + totalRequestTime.add(currentTime - requestTime); + if (!isAsync) { + nrOfRequests.increment(); + if (log.isDebugEnabled()) { + if ((nrOfRequests.longValue() % 100) == 0) { + log.debug(sm.getString("ReplicationValve.stats", + new Object[] { Long.valueOf(totalRequestTime.longValue() / nrOfRequests.longValue()), + Long.valueOf(totalSendTime.longValue() / nrOfRequests.longValue()), Long.valueOf(nrOfRequests.longValue()), + Long.valueOf(nrOfSendRequests.longValue()), Long.valueOf(nrOfCrossContextSendRequests.longValue()), + Long.valueOf(nrOfFilterRequests.longValue()), Long.valueOf(totalRequestTime.longValue()), + Long.valueOf(totalSendTime.longValue()) })); + } + } + } + } + + + /** + * Mark Request that processed at primary node with attribute primaryIndicatorName + * + * @param request The Servlet request + * + * @throws IOException IO error finding session + */ + protected void createPrimaryIndicator(Request request) throws IOException { + String id = request.getRequestedSessionId(); + if ((id != null) && (id.length() > 0)) { + Manager manager = request.getContext().getManager(); + Session session = manager.findSession(id); + if (session instanceof ClusterSession) { + ClusterSession cses = (ClusterSession) session; + if (log.isDebugEnabled()) { + log.debug(sm.getString("ReplicationValve.session.indicator", request.getContext().getName(), id, + primaryIndicatorName, Boolean.valueOf(cses.isPrimarySession()))); + } + request.setAttribute(primaryIndicatorName, cses.isPrimarySession() ? Boolean.TRUE : Boolean.FALSE); + } else { + if (log.isDebugEnabled()) { + if (session != null) { + log.debug(sm.getString("ReplicationValve.session.found", request.getContext().getName(), id)); + } else { + log.debug(sm.getString("ReplicationValve.session.invalid", request.getContext().getName(), id)); + } + } + } + } + } + +} diff --git a/java/org/apache/catalina/ha/tcp/SendMessageData.java b/java/org/apache/catalina/ha/tcp/SendMessageData.java new file mode 100644 index 0000000..12e3a54 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/SendMessageData.java @@ -0,0 +1,63 @@ +/* + * 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.catalina.ha.tcp; + +import org.apache.catalina.tribes.Member; + +/** + * @author Peter Rossbach + */ +public class SendMessageData { + + private Object message; + private Member destination; + private Exception exception; + + + /** + * @param message The message to send + * @param destination Member destination + * @param exception Associated error + */ + public SendMessageData(Object message, Member destination, Exception exception) { + super(); + this.message = message; + this.destination = destination; + this.exception = exception; + } + + /** + * @return the destination. + */ + public Member getDestination() { + return destination; + } + + /** + * @return the exception. + */ + public Exception getException() { + return exception; + } + + /** + * @return the message. + */ + public Object getMessage() { + return message; + } +} diff --git a/java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java b/java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java new file mode 100644 index 0000000..7e787dc --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java @@ -0,0 +1,878 @@ +/* + * 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.catalina.ha.tcp; + +import java.beans.PropertyChangeSupport; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Manager; +import org.apache.catalina.Valve; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.ha.ClusterDeployer; +import org.apache.catalina.ha.ClusterListener; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.ClusterMessage; +import org.apache.catalina.ha.ClusterValve; +import org.apache.catalina.ha.session.ClusterSessionListener; +import org.apache.catalina.ha.session.DeltaManager; +import org.apache.catalina.ha.session.JvmRouteBinderValve; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.group.GroupChannel; +import org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor; +import org.apache.catalina.tribes.group.interceptors.TcpFailureDetector; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.catalina.util.ToStringUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * A Cluster implementation using simple multicast. Responsible for setting up a cluster and provides callers + * with a valid multicast receiver/sender. + * + * @author Remy Maucherat + * @author Peter Rossbach + */ +public class SimpleTcpCluster extends LifecycleMBeanBase + implements CatalinaCluster, MembershipListener, ChannelListener { + + public static final Log log = LogFactory.getLog(SimpleTcpCluster.class); + + // ----------------------------------------------------- Instance Variables + + public static final String BEFORE_MEMBERREGISTER_EVENT = "before_member_register"; + + public static final String AFTER_MEMBERREGISTER_EVENT = "after_member_register"; + + public static final String BEFORE_MANAGERREGISTER_EVENT = "before_manager_register"; + + public static final String AFTER_MANAGERREGISTER_EVENT = "after_manager_register"; + + public static final String BEFORE_MANAGERUNREGISTER_EVENT = "before_manager_unregister"; + + public static final String AFTER_MANAGERUNREGISTER_EVENT = "after_manager_unregister"; + + public static final String BEFORE_MEMBERUNREGISTER_EVENT = "before_member_unregister"; + + public static final String AFTER_MEMBERUNREGISTER_EVENT = "after_member_unregister"; + + public static final String SEND_MESSAGE_FAILURE_EVENT = "send_message_failure"; + + public static final String RECEIVE_MESSAGE_FAILURE_EVENT = "receive_message_failure"; + + /** + * Group channel. + */ + protected Channel channel = new GroupChannel(); + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + /** + * The cluster name to join + */ + protected String clusterName; + + /** + * call Channel.heartbeat() at container background thread + * + * @see org.apache.catalina.tribes.group.GroupChannel#heartbeat() + */ + protected boolean heartbeatBackgroundEnabled = false; + + /** + * The Container associated with this Cluster. + */ + protected Container container = null; + + /** + * The property change support for this component. + */ + protected final PropertyChangeSupport support = new PropertyChangeSupport(this); + + /** + * The context name <-> manager association for distributed contexts. + */ + protected final Map managers = new HashMap<>(); + + protected ClusterManager managerTemplate = new DeltaManager(); + + private final List valves = new ArrayList<>(); + + private ClusterDeployer clusterDeployer; + private ObjectName onameClusterDeployer; + + /** + * Listeners of messages + */ + protected final List clusterListeners = new ArrayList<>(); + + /** + * Comment for notifyLifecycleListenerOnFailure + */ + private boolean notifyLifecycleListenerOnFailure = false; + + private int channelSendOptions = Channel.SEND_OPTIONS_ASYNCHRONOUS; + + private int channelStartOptions = Channel.DEFAULT; + + private final Map memberOnameMap = new ConcurrentHashMap<>(); + + // ------------------------------------------------------------- Properties + + public SimpleTcpCluster() { + // NO-OP + } + + /** + * Return heartbeat enable flag (default false) + * + * @return the heartbeatBackgroundEnabled + */ + public boolean isHeartbeatBackgroundEnabled() { + return heartbeatBackgroundEnabled; + } + + /** + * enabled that container backgroundThread call heartbeat at channel + * + * @param heartbeatBackgroundEnabled the heartbeatBackgroundEnabled to set + */ + public void setHeartbeatBackgroundEnabled(boolean heartbeatBackgroundEnabled) { + this.heartbeatBackgroundEnabled = heartbeatBackgroundEnabled; + } + + /** + * Set the name of the cluster to join, if no cluster with this name is present create one. + * + * @param clusterName The clustername to join + */ + @Override + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + /** + * Return the name of the cluster that this Server is currently configured to operate within. + * + * @return The name of the cluster associated with this server + */ + @Override + public String getClusterName() { + if (clusterName == null && container != null) { + return container.getName(); + } + return clusterName; + } + + /** + * Set the Container associated with our Cluster + * + * @param container The Container to use + */ + @Override + public void setContainer(Container container) { + Container oldContainer = this.container; + this.container = container; + support.firePropertyChange("container", oldContainer, this.container); + } + + /** + * Get the Container associated with our Cluster + * + * @return The Container associated with our Cluster + */ + @Override + public Container getContainer() { + return this.container; + } + + /** + * @return Returns the notifyLifecycleListenerOnFailure. + */ + public boolean isNotifyLifecycleListenerOnFailure() { + return notifyLifecycleListenerOnFailure; + } + + /** + * @param notifyListenerOnFailure The notifyLifecycleListenerOnFailure to set. + */ + public void setNotifyLifecycleListenerOnFailure(boolean notifyListenerOnFailure) { + boolean oldNotifyListenerOnFailure = this.notifyLifecycleListenerOnFailure; + this.notifyLifecycleListenerOnFailure = notifyListenerOnFailure; + support.firePropertyChange("notifyLifecycleListenerOnFailure", oldNotifyListenerOnFailure, + this.notifyLifecycleListenerOnFailure); + } + + /** + * Add cluster valve Cluster Valves are only add to container when cluster is started! + * + * @param valve The new cluster Valve. + */ + @Override + public void addValve(Valve valve) { + if (valve instanceof ClusterValve && (!valves.contains(valve))) { + valves.add(valve); + } + } + + /** + * get all cluster valves + * + * @return current cluster valves + */ + @Override + public Valve[] getValves() { + return valves.toArray(new Valve[0]); + } + + /** + * Get the cluster listeners associated with this cluster. If this Array has no listeners registered, a zero-length + * array is returned. + * + * @return the listener array + */ + public ClusterListener[] findClusterListeners() { + return clusterListeners.toArray(new ClusterListener[0]); + } + + /** + * Add cluster message listener and register cluster to this listener. + * + * @param listener The new listener + * + * @see org.apache.catalina.ha.CatalinaCluster#addClusterListener(org.apache.catalina.ha.ClusterListener) + */ + @Override + public void addClusterListener(ClusterListener listener) { + if (listener != null && !clusterListeners.contains(listener)) { + clusterListeners.add(listener); + listener.setCluster(this); + } + } + + /** + * Remove message listener and deregister Cluster from listener. + * + * @param listener The listener to remove + * + * @see org.apache.catalina.ha.CatalinaCluster#removeClusterListener(org.apache.catalina.ha.ClusterListener) + */ + @Override + public void removeClusterListener(ClusterListener listener) { + if (listener != null) { + clusterListeners.remove(listener); + listener.setCluster(null); + } + } + + /** + * @return the current Deployer + */ + @Override + public ClusterDeployer getClusterDeployer() { + return clusterDeployer; + } + + /** + * set a new Deployer, must be set before cluster started! + * + * @param clusterDeployer The associated deployer + */ + @Override + public void setClusterDeployer(ClusterDeployer clusterDeployer) { + this.clusterDeployer = clusterDeployer; + } + + @Override + public void setChannel(Channel channel) { + this.channel = channel; + } + + public void setManagerTemplate(ClusterManager managerTemplate) { + this.managerTemplate = managerTemplate; + } + + public void setChannelSendOptions(int channelSendOptions) { + this.channelSendOptions = channelSendOptions; + } + + public void setChannelSendOptions(String channelSendOptions) { + + int value = Channel.parseSendOptions(channelSendOptions); + if (value > 0) { + this.setChannelSendOptions(value); + } + } + + /** + * has members + */ + protected boolean hasMembers = false; + + @Override + public boolean hasMembers() { + return hasMembers; + } + + /** + * Get all current cluster members + * + * @return all members or empty array + */ + @Override + public Member[] getMembers() { + return channel.getMembers(); + } + + /** + * Return the member that represents this node. + * + * @return Member + */ + @Override + public Member getLocalMember() { + return channel.getLocalMember(true); + } + + + // --------------------------------------------------------- Public Methods + + /** + * @return Returns the managers. + */ + @Override + public Map getManagers() { + return managers; + } + + @Override + public Channel getChannel() { + return channel; + } + + public ClusterManager getManagerTemplate() { + return managerTemplate; + } + + public int getChannelSendOptions() { + return channelSendOptions; + } + + /** + * returns the SendOptions as a comma separated list of names for use by JMX + * + * @return a comma separated list of the option names + */ + public String getChannelSendOptionsName() { + return Channel.getSendOptionsAsString(channelSendOptions); + } + + /** + * Create new Manager without add to cluster (comes with start the manager) + * + * @param name Context Name of this manager + * + * @see org.apache.catalina.Cluster#createManager(String) + * @see DeltaManager#start() + */ + @Override + public synchronized Manager createManager(String name) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("simpleTcpCluster.createManager", name, getManagerTemplate().getClass().getName())); + } + ClusterManager manager = null; + try { + manager = managerTemplate.cloneFromTemplate(); + manager.setName(name); + } catch (Exception x) { + log.error(sm.getString("simpleTcpCluster.clustermanager.cloneFailed"), x); + manager = new DeltaManager(); + } finally { + if (manager != null) { + manager.setCluster(this); + } + } + return manager; + } + + @Override + public void registerManager(Manager manager) { + + if (!(manager instanceof ClusterManager)) { + log.warn(sm.getString("simpleTcpCluster.clustermanager.notImplement", manager)); + return; + } + ClusterManager cmanager = (ClusterManager) manager; + // Notify our interested LifecycleListeners + fireLifecycleEvent(BEFORE_MANAGERREGISTER_EVENT, manager); + String clusterName = getManagerName(cmanager.getName(), manager); + cmanager.setName(clusterName); + cmanager.setCluster(this); + + managers.put(clusterName, cmanager); + // Notify our interested LifecycleListeners + fireLifecycleEvent(AFTER_MANAGERREGISTER_EVENT, manager); + } + + /** + * Remove an application from cluster replication bus. + * + * @param manager The manager + * + * @see org.apache.catalina.Cluster#removeManager(Manager) + */ + @Override + public void removeManager(Manager manager) { + if (manager instanceof ClusterManager) { + ClusterManager cmgr = (ClusterManager) manager; + // Notify our interested LifecycleListeners + fireLifecycleEvent(BEFORE_MANAGERUNREGISTER_EVENT, manager); + managers.remove(getManagerName(cmgr.getName(), manager)); + cmgr.setCluster(null); + // Notify our interested LifecycleListeners + fireLifecycleEvent(AFTER_MANAGERUNREGISTER_EVENT, manager); + } + } + + @Override + public String getManagerName(String name, Manager manager) { + String clusterName = name; + if (clusterName == null) { + clusterName = manager.getContext().getName(); + } + if (getContainer() instanceof Engine) { + Context context = manager.getContext(); + Container host = context.getParent(); + if (host instanceof Host && clusterName != null && !(clusterName.startsWith(host.getName() + "#"))) { + clusterName = host.getName() + "#" + clusterName; + } + } + return clusterName; + } + + @Override + public Manager getManager(String name) { + return managers.get(name); + } + + // ------------------------------------------------------ Lifecycle Methods + + /** + * Execute a periodic task, such as reloading, etc. This method will be invoked inside the classloading context of + * this container. Unexpected throwables will be caught and logged. + * + * @see org.apache.catalina.ha.deploy.FarmWarDeployer#backgroundProcess() + * @see org.apache.catalina.tribes.group.GroupChannel#heartbeat() + */ + @Override + public void backgroundProcess() { + if (clusterDeployer != null) { + clusterDeployer.backgroundProcess(); + } + + // send a heartbeat through the channel + if (isHeartbeatBackgroundEnabled() && channel != null) { + channel.heartbeat(); + } + + // periodic event + fireLifecycleEvent(PERIODIC_EVENT, null); + } + + + // ------------------------------------------------------ public + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + if (clusterDeployer != null) { + StringBuilder name = new StringBuilder("type=Cluster"); + Container container = getContainer(); + if (container != null) { + name.append(container.getMBeanKeyProperties()); + } + name.append(",component=Deployer"); + onameClusterDeployer = register(clusterDeployer, name.toString()); + } + } + + + /** + * Start Cluster and implement the requirements of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + if (log.isInfoEnabled()) { + log.info(sm.getString("simpleTcpCluster.start")); + } + + channel.setUtilityExecutor(Container.getService(getContainer()).getServer().getUtilityExecutor()); + + try { + checkDefaults(); + registerClusterValve(); + channel.addMembershipListener(this); + channel.addChannelListener(this); + channel.setName(getClusterName() + "-Channel"); + channel.start(channelStartOptions); + if (clusterDeployer != null) { + clusterDeployer.start(); + } + registerMember(channel.getLocalMember(false)); + } catch (Exception x) { + log.error(sm.getString("simpleTcpCluster.startUnable"), x); + throw new LifecycleException(x); + } + + setState(LifecycleState.STARTING); + } + + protected void checkDefaults() { + if (clusterListeners.size() == 0 && managerTemplate instanceof DeltaManager) { + addClusterListener(new ClusterSessionListener()); + } + if (valves.size() == 0) { + addValve(new JvmRouteBinderValve()); + addValve(new ReplicationValve()); + } + if (clusterDeployer != null) { + clusterDeployer.setCluster(this); + } + if (channel == null) { + channel = new GroupChannel(); + } + if (channel instanceof GroupChannel && !((GroupChannel) channel).getInterceptors().hasNext()) { + channel.addInterceptor(new MessageDispatchInterceptor()); + channel.addInterceptor(new TcpFailureDetector()); + } + if (heartbeatBackgroundEnabled) { + channel.setHeartbeat(false); + } + } + + /** + * register all cluster valve to host or engine + */ + protected void registerClusterValve() { + if (container != null) { + for (Valve v : valves) { + ClusterValve valve = (ClusterValve) v; + if (log.isTraceEnabled()) { + log.trace("Invoking addValve on " + getContainer() + " with class=" + valve.getClass().getName()); + } + if (valve != null) { + container.getPipeline().addValve(valve); + valve.setCluster(this); + } + } + } + } + + /** + * unregister all cluster valve to host or engine + */ + protected void unregisterClusterValve() { + for (Valve v : valves) { + ClusterValve valve = (ClusterValve) v; + if (log.isTraceEnabled()) { + log.trace("Invoking removeValve on " + getContainer() + " with class=" + valve.getClass().getName()); + } + if (valve != null) { + container.getPipeline().removeValve(valve); + valve.setCluster(null); + } + } + } + + + /** + * Stop Cluster and implement the requirements of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + setState(LifecycleState.STOPPING); + + unregisterMember(channel.getLocalMember(false)); + if (clusterDeployer != null) { + clusterDeployer.stop(); + } + this.managers.clear(); + try { + if (clusterDeployer != null) { + clusterDeployer.setCluster(null); + } + channel.stop(channelStartOptions); + channel.removeChannelListener(this); + channel.removeMembershipListener(this); + this.unregisterClusterValve(); + } catch (Exception x) { + log.error(sm.getString("simpleTcpCluster.stopUnable"), x); + } + + channel.setUtilityExecutor(null); + } + + + @Override + protected void destroyInternal() throws LifecycleException { + if (onameClusterDeployer != null) { + unregister(onameClusterDeployer); + onameClusterDeployer = null; + } + super.destroyInternal(); + } + + + /** + * Return a String rendering of this object. + */ + @Override + public String toString() { + return ToStringUtil.toString(this); + } + + + /** + * send message to all cluster members + * + * @param msg message to transfer + * + * @see org.apache.catalina.ha.CatalinaCluster#send(org.apache.catalina.ha.ClusterMessage) + */ + @Override + public void send(ClusterMessage msg) { + send(msg, null); + } + + /** + * send a cluster message to one member + * + * @param msg message to transfer + * @param dest Receiver member + * + * @see org.apache.catalina.ha.CatalinaCluster#send(org.apache.catalina.ha.ClusterMessage, + * org.apache.catalina.tribes.Member) + */ + @Override + public void send(ClusterMessage msg, Member dest) { + send(msg, dest, this.channelSendOptions); + } + + @Override + public void send(ClusterMessage msg, Member dest, int sendOptions) { + try { + msg.setAddress(getLocalMember()); + if (dest != null) { + if (!getLocalMember().equals(dest)) { + channel.send(new Member[] { dest }, msg, sendOptions); + } else { + log.error(sm.getString("simpleTcpCluster.unableSend.localMember", msg)); + } + } else { + Member[] destmembers = channel.getMembers(); + if (destmembers.length > 0) { + channel.send(destmembers, msg, sendOptions); + } else if (log.isDebugEnabled()) { + log.debug(sm.getString("simpleTcpCluster.noMembers", msg)); + } + } + } catch (Exception x) { + log.error(sm.getString("simpleTcpCluster.sendFailed"), x); + } + } + + /** + * New cluster member is registered + * + * @see org.apache.catalina.tribes.MembershipListener#memberAdded(org.apache.catalina.tribes.Member) + */ + @Override + public void memberAdded(Member member) { + try { + hasMembers = channel.hasMembers(); + if (log.isInfoEnabled()) { + log.info(sm.getString("simpleTcpCluster.member.added", member)); + } + // Notify our interested LifecycleListeners + fireLifecycleEvent(BEFORE_MEMBERREGISTER_EVENT, member); + + registerMember(member); + + // Notify our interested LifecycleListeners + fireLifecycleEvent(AFTER_MEMBERREGISTER_EVENT, member); + } catch (Exception x) { + log.error(sm.getString("simpleTcpCluster.member.addFailed"), x); + } + + } + + /** + * Cluster member is gone + * + * @see org.apache.catalina.tribes.MembershipListener#memberDisappeared(org.apache.catalina.tribes.Member) + */ + @Override + public void memberDisappeared(Member member) { + try { + hasMembers = channel.hasMembers(); + if (log.isInfoEnabled()) { + log.info(sm.getString("simpleTcpCluster.member.disappeared", member)); + } + // Notify our interested LifecycleListeners + fireLifecycleEvent(BEFORE_MEMBERUNREGISTER_EVENT, member); + + unregisterMember(member); + + // Notify our interested LifecycleListeners + fireLifecycleEvent(AFTER_MEMBERUNREGISTER_EVENT, member); + } catch (Exception x) { + log.error(sm.getString("simpleTcpCluster.member.removeFailed"), x); + } + } + + // --------------------------------------------------------- receiver + // messages + + /** + * notify all listeners from receiving a new message is not ClusterMessage emit Failure Event to LifecycleListener + * + * @param msg received Message + */ + @Override + public boolean accept(Serializable msg, Member sender) { + return (msg instanceof ClusterMessage); + } + + + @Override + public void messageReceived(Serializable message, Member sender) { + ClusterMessage fwd = (ClusterMessage) message; + fwd.setAddress(sender); + messageReceived(fwd); + } + + public void messageReceived(ClusterMessage message) { + + if (log.isTraceEnabled() && message != null) { + log.trace("Assuming clocks are synched: Replication for " + message.getUniqueId() + " took=" + + (System.currentTimeMillis() - (message).getTimestamp()) + " ms."); + } + + // invoke all the listeners + boolean accepted = false; + if (message != null) { + for (ClusterListener listener : clusterListeners) { + if (listener.accept(message)) { + accepted = true; + listener.messageReceived(message); + } + } + if (!accepted && notifyLifecycleListenerOnFailure) { + Member dest = message.getAddress(); + // Notify our interested LifecycleListeners + fireLifecycleEvent(RECEIVE_MESSAGE_FAILURE_EVENT, new SendMessageData(message, dest, null)); + if (log.isDebugEnabled()) { + log.debug(sm.getString("simpleTcpCluster.noListener", message, message.getClass().getName())); + } + } + } + } + + public int getChannelStartOptions() { + return channelStartOptions; + } + + public void setChannelStartOptions(int channelStartOptions) { + this.channelStartOptions = channelStartOptions; + } + + + // --------------------------------------------------------------------- JMX + + @Override + protected String getDomainInternal() { + Container container = getContainer(); + if (container == null) { + return null; + } + return container.getDomain(); + } + + @Override + protected String getObjectNameKeyProperties() { + StringBuilder name = new StringBuilder("type=Cluster"); + + Container container = getContainer(); + if (container != null) { + name.append(container.getMBeanKeyProperties()); + } + + return name.toString(); + } + + private void registerMember(Member member) { + // JMX registration + StringBuilder name = new StringBuilder("type=Cluster"); + Container container = getContainer(); + if (container != null) { + name.append(container.getMBeanKeyProperties()); + } + name.append(",component=Member,name="); + name.append(ObjectName.quote(member.getName())); + + ObjectName oname = register(member, name.toString()); + memberOnameMap.put(member, oname); + } + + private void unregisterMember(Member member) { + if (member == null) { + return; + } + ObjectName oname = memberOnameMap.remove(member); + if (oname != null) { + unregister(oname); + } + } +} diff --git a/java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml b/java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml new file mode 100644 index 0000000..40a019c --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/loader/JdbcLeakPrevention.java b/java/org/apache/catalina/loader/JdbcLeakPrevention.java new file mode 100644 index 0000000..cdd5b8b --- /dev/null +++ b/java/org/apache/catalina/loader/JdbcLeakPrevention.java @@ -0,0 +1,67 @@ +/* + * 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.catalina.loader; + +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * This class is loaded by {@link WebappClassLoaderBase} to enable it to deregister JDBC drivers forgotten by the web + * application. There are some classloading hacks involved - see {@link WebappClassLoaderBase#clearReferences()} for + * details - but the short version is do not just create a new instance of this class with the new keyword. Since this + * class is loaded by {@link WebappClassLoaderBase}, it cannot refer to any internal Tomcat classes as that will cause + * the security manager to complain. + */ +public class JdbcLeakPrevention { + + public List clearJdbcDriverRegistrations() throws SQLException { + List driverNames = new ArrayList<>(); + + /* + * DriverManager.getDrivers() has a nasty side-effect of registering drivers that are visible to this class + * loader but haven't yet been loaded. Therefore, the first call to this method a) gets the list of originally + * loaded drivers and b) triggers the unwanted side-effect. The second call gets the complete list of drivers + * ensuring that both original drivers and any loaded as a result of the side-effects are all de-registered. + */ + Set originalDrivers = new HashSet<>(); + Enumeration drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + originalDrivers.add(drivers.nextElement()); + } + drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + // Only unload the drivers this web app loaded + if (driver.getClass().getClassLoader() != this.getClass().getClassLoader()) { + continue; + } + // Only report drivers that were originally registered. Skip any + // that were registered as a side-effect of this code. + if (originalDrivers.contains(driver)) { + driverNames.add(driver.getClass().getCanonicalName()); + } + DriverManager.deregisterDriver(driver); + } + return driverNames; + } +} diff --git a/java/org/apache/catalina/loader/LocalStrings.properties b/java/org/apache/catalina/loader/LocalStrings.properties new file mode 100644 index 0000000..b861f8b --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings.properties @@ -0,0 +1,70 @@ +# 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. + +webappClassLoader.addExportsJavaIo=You need to add "--add-opens=java.base/java.io={0}" to the JVM command line arguments to enable ObjectStream cache memory leak protection. Alternatively, you can suppress this warning by disabling ObjectStream class cache memory leak protection. +webappClassLoader.addExportsRmi=You need to add "--add-opens=java.rmi/sun.rmi.transport={0}" to the JVM command line arguments to enable RMI Target memory leak detection. Alternatively, you can suppress this warning by disabling RMI Target memory leak detection. +webappClassLoader.addExportsThreadLocal=You need to add "--add-opens=java.base/java.lang={0}" to the JVM command line arguments to enable ThreadLocal memory leak detection. Alternatively, you can suppress this warning by disabling ThreadLocal memory leak detection. +webappClassLoader.addPermissionNoCanonicalFile=Unable to obtain a canonical file path from the URL [{0}] +webappClassLoader.addPermissionNoProtocol=The protocol [{0}] in the URL [{1}] is not supported so no read permission was granted for resources located at this URL +webappClassLoader.addTransformer=Added class file transformer [{0}] to web application [{1}]. +webappClassLoader.addTransformer.duplicate=Duplicate call to add class file transformer [{0}] to web application [{1}] ignored. +webappClassLoader.addTransformer.illegalArgument=The web application [{0}] attempted to add a null class file transformer. +webappClassLoader.checkThreadLocalsForLeaks=The web application [{0}] created a ThreadLocal with key of type [{1}] (value [{2}]) and a value of type [{3}] (value [{4}]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak. +webappClassLoader.checkThreadLocalsForLeaks.badKey=Unable to determine string representation of key of type [{0}] +webappClassLoader.checkThreadLocalsForLeaks.badValue=Unable to determine string representation of value of type [{0}] +webappClassLoader.checkThreadLocalsForLeaks.unknown=Unknown +webappClassLoader.checkThreadLocalsForLeaksFail=Failed to check for ThreadLocal references for web application [{0}] +webappClassLoader.checkThreadLocalsForLeaksNone=The web application [{0}] created a ThreadLocal with key of type [{1}] (value [{2}]) and a value of type [{3}] (value [{4}]). Since keys are only weakly held by the ThreadLocal Map this is not a memory leak. +webappClassLoader.checkThreadLocalsForLeaksNull=The web application [{0}] created a ThreadLocal with key of type [{1}] (value [{2}]). The ThreadLocal has been correctly set to null and the key will be removed by GC. +webappClassLoader.checkThreadsHttpClient=Found HttpClient keep-alive thread using web application class loader. Fixed by switching thread to the parent class loader. +webappClassLoader.clearJdbc=The web application [{0}] registered the JDBC driver [{1}] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered. +webappClassLoader.clearObjectStreamClassCachesFail=Failed to clear soft references from ObjectStreamClass$Caches for web application [{0}] +webappClassLoader.clearRmi=Found RMI Target with stub class class [{0}] and value [{1}]. This RMI Target has been forcibly removed to prevent a memory leak. +webappClassLoader.clearRmiFail=Failed to clear context class loader referenced from sun.rmi.transport.Target for web application [{0}] +webappClassLoader.clearRmiInfo=Failed to find class sun.rmi.transport.Target to clear context class loader for web application [{0}]. This is expected on non-Sun JVMs. +webappClassLoader.getThreadGroupError=Unable to obtain the parent for ThreadGroup [{0}]. It will not be possible to check all threads for potential memory leaks +webappClassLoader.jarsAdded=One or more JARs have been added to the web application [{0}] +webappClassLoader.jarsModified=One or more JARs have been modified in the web application [{0}] +webappClassLoader.jarsRemoved=One or more JARs have been removed from the web application [{0}] +webappClassLoader.javaseClassLoaderNull=The j2seClassLoader attribute may not be null +webappClassLoader.jdbcRemoveFailed=JDBC driver de-registration failed for web application [{0}] +webappClassLoader.loadedByThisOrChildFail=Failed to fully check the entries in an instance of [{0}] for potential memory leaks in context [{1}] +webappClassLoader.readError=Resource read error: Could not load [{0}]. +webappClassLoader.removeTransformer=Removed class file transformer [{0}] from web application [{1}]. +webappClassLoader.resourceModified=Resource [{0}] has been modified. The last modified time was [{1}] and is now [{2}] +webappClassLoader.restrictedPackage=Security violation, attempt to use restricted class [{0}] +webappClassLoader.securityException=Security exception trying to find class [{0}] in findClassInternal [{1}] +webappClassLoader.stackTrace=The web application [{0}] appears to have started a thread named [{1}] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:{2} +webappClassLoader.stackTraceRequestThread=The web application [{0}] is still processing a request that has yet to finish. This is very likely to create a memory leak. You can control the time allowed for requests to finish by using the unloadDelay attribute of the standard Context implementation. Stack trace of request processing thread:[{2}] +webappClassLoader.stopThreadFail=Failed to terminate thread named [{0}] for web application [{1}] +webappClassLoader.stopTimerThreadFail=Failed to terminate TimerThread named [{0}] for web application [{1}] +webappClassLoader.stopped=Illegal access: this web application instance has been stopped already. Could not load [{0}]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access. +webappClassLoader.superCloseFail=Failure calling close() on super class +webappClassLoader.transformError=Instrumentation error: could not transform class [{0}] because its class file format is not legal. +webappClassLoader.warnTimerThread=The web application [{0}] appears to have started a TimerThread named [{1}] via the java.util.Timer API but has failed to stop it. To prevent a memory leak, the timer (and hence the associated thread) has been forcibly canceled. +webappClassLoader.wrongVersion=(unable to load class [{0}]) + +webappClassLoaderParallel.registrationFailed=Registration of org.apache.catalina.loader.ParallelWebappClassLoader as capable of loading classes in parallel failed + +webappLoader.deploy=Deploying class repositories to work directory [{0}] +webappLoader.noResources=No resources found for context [{0}] +webappLoader.reloadable=Cannot set reloadable property to [{0}] +webappLoader.setContext.ise=Setting the Context is not permitted while the loader is started. +webappLoader.startError=Error starting the loader +webappLoader.starting=Starting this Loader +webappLoader.stopError=Error stopping the loader +webappLoader.stopping=Stopping this Loader +webappLoader.unknownClassLoader=Unknown class loader [{0}] of class [{1}] +webappLoader.unknownProfile=Unknown Jakarta profile [{0}] specified, the default profile will be used instead diff --git a/java/org/apache/catalina/loader/LocalStrings_cs.properties b/java/org/apache/catalina/loader/LocalStrings_cs.properties new file mode 100644 index 0000000..aa68660 --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_cs.properties @@ -0,0 +1,19 @@ +# 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. + +webappClassLoader.checkThreadLocalsForLeaks.unknown=Neznámý +webappClassLoader.readError=Chyba Ätení zdroje: nelze naÄíst [{0}]. + +webappLoader.setContext.ise=Nastavení kontextu není dovolení bÄ›hem zavádÄ›ní. diff --git a/java/org/apache/catalina/loader/LocalStrings_de.properties b/java/org/apache/catalina/loader/LocalStrings_de.properties new file mode 100644 index 0000000..415b6f0 --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_de.properties @@ -0,0 +1,21 @@ +# 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. + +webappClassLoader.checkThreadLocalsForLeaks.unknown=Unbekannt +webappClassLoader.jarsRemoved=Eine oder mehrere JARs wurden von der Web-Applikation [{0}] entfernt +webappClassLoader.readError=Fehler beim Lesen einer Ressource: Konnte [{0}] nicht laden +webappClassLoader.superCloseFail=Fehler beim Aufruf von close() auf der Super-Klasse + +webappLoader.setContext.ise=Es ist nicht erlaubt, den Context zu setzen, während der Loader startet diff --git a/java/org/apache/catalina/loader/LocalStrings_es.properties b/java/org/apache/catalina/loader/LocalStrings_es.properties new file mode 100644 index 0000000..5f11dea --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_es.properties @@ -0,0 +1,41 @@ +# 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. + +webappClassLoader.addExportsThreadLocal=Necesitas agregar "--add-opens=java.base/java.lang={0}" a los argumentos JVM en la línea de comandos, para habilitar la detección de fuga de memoria ThreadLocal. Alternativamente, puedes suprimir esta advertencia, deshabilitando la detección de fuga de memoria ThreadLocal. +webappClassLoader.checkThreadLocalsForLeaks=La aplicación web [{0}] creó un ThreadLocal con clave del tipo [{1}] (valor [{2}]) y un valor del tipo [{3}] (valor [{4}]) pero no pudo quitarlo cuando la aplicación web se paró. Los hilos se van a renovar con el tiempo para intentar evitar in posible fallo de memoria. +webappClassLoader.checkThreadLocalsForLeaks.badKey=No puedo determinar la representación de cadena de la clave del tipo [{0}] +webappClassLoader.checkThreadLocalsForLeaks.badValue=No puedo determinar la representación de cadena del valor del tipo [{0}] +webappClassLoader.checkThreadLocalsForLeaks.unknown=Desconocido +webappClassLoader.checkThreadLocalsForLeaksFail=No pude revisar las referencias a ThreadLocal para la aplicación web [{0}] +webappClassLoader.checkThreadLocalsForLeaksNull=La aplicación web [{0}] creó un ThreadLocal con clave del tipo [{1}] (valor [{2}]). El Threadlocal ha sido puesto correctamente a nulo y la clave será qutada por el GC. +webappClassLoader.checkThreadsHttpClient=Hallado hilo keep-alive de HttpClient usando cargador de clase de aplicación web. Fijado por el hilo de conmutación al cargador de la clase padre. +webappClassLoader.clearJdbc=La aplicación web [{0}] registró el conductor JDBC [{1}] pero falló al anular el registro mientras la aplicación web estaba parada. Para prevenir un fallo de memoria, se ha anulado el registro del conductor JDBC por la fuerza. +webappClassLoader.clearRmiFail=No pude limpiar el cargador del contexto de clase referenciado desde sun.rmi.transport.Target para la aplicación web [{0}] +webappClassLoader.clearRmiInfo=No pude hallar la clase sun.rmi.transport.Target para limpiar el cargador de contexto de clase para la aplicación web [{0}]. Esto es lo esperado em máquinas que no son de Sun. +webappClassLoader.jarsRemoved=Uno o más JARs han sido eliminados de la aplicación web [{0}]\n +webappClassLoader.jdbcRemoveFailed=Ha fallado el desregistro del conductor JDBC para la aplicación web [{0}] +webappClassLoader.readError=Error de lectura de recurso: No pude cargar [{0}]. +webappClassLoader.stopThreadFail=No pude terminar el hilo con nombre [{0}] para la aplicación web [{1}] +webappClassLoader.stopTimerThreadFail=No pude terminar TimerThread con nombre [{0}] para la aplicación web [{1}] +webappClassLoader.stopped=Acceso ilegal: esta instancia de aplicación web ya ha sido parada. Could not load [{0}]. La eventual traza de pila que sigue ha sido motivada por un error lanzado con motivos de depuración así como para intentar terminar el hilo que motivó el acceso ilegal y no tiene impacto funcional. +webappClassLoader.superCloseFail=Fallo a llamar close() en la clase super +webappClassLoader.warnTimerThread=La aplicación web [{0}] parece haber arrancado un TimerThread con nombre [{1}] vía de la API java.util.Timer, pero no ha podido pararlo. Para prevenir un fallo de memoria, el cronómetro (y el hilo asociado) hasido cancelado a la fuerza. +webappClassLoader.wrongVersion=(no puedo cargar clase [{0}]) + +webappLoader.deploy=Desplegando repositorios de clase en directorio de trabajo [{0}] +webappLoader.reloadable=No puedo poner la propiedad recargable a [{0}] +webappLoader.setContext.ise=No esta permitido fijar el Contexto mientras el cargador esta iniciado.\n +webappLoader.starting=Arrancando este Cargador +webappLoader.stopping=Parando este Cargador diff --git a/java/org/apache/catalina/loader/LocalStrings_fr.properties b/java/org/apache/catalina/loader/LocalStrings_fr.properties new file mode 100644 index 0000000..9c7fc6e --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_fr.properties @@ -0,0 +1,70 @@ +# 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. + +webappClassLoader.addExportsJavaIo=Lorsque Java 9 ou ultérieur est utilisé, "--add-opens=java.base/java.io={0}" doit être ajouté aux arguments de la ligne de commande de la JVM pour activer la protection contre les fuites de mémoire du cache de ObjectStream; cet avertissement peut aussi être supprimé en désactivant cette protection +webappClassLoader.addExportsRmi=Quand Java 9 est utilisé, il faut utiliser "--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED" sur la ligne de commande de la JVM pour activer la détection de fuites de mémoire des cibles RMI ; sinon cet avertissement peut être supprimé en désactivant cette détection +webappClassLoader.addExportsThreadLocal=Il est nécessaire d''ajouter "--add-opens=java.base/java.lang={0}" aux arguments de la ligne de commande de la JVM pour activer la détection des fuites de mémoire des ThreadLocal; il est aussi possible d''éviter cet avertissement en désactivant cette détection +webappClassLoader.addPermissionNoCanonicalFile=Impossible d’obtenir le chemin de fichier canonique pour l''URL [{0}] +webappClassLoader.addPermissionNoProtocol=Le protocole [{0}] dans l''URL [{1}] n''est pas supporté donc aucune permission de lecture n''a été assignée pour les ressources situées à cette URL +webappClassLoader.addTransformer=Ajout d''un transformateur de fichier de class [{0}] pour l''application web [{1}] +webappClassLoader.addTransformer.duplicate=Ajout en double ignoré du transformateur de fichiers de classe [{0}] à l''application web [{1}] +webappClassLoader.addTransformer.illegalArgument=L''application web [{0}] a essayé d''ajouter un transformateur de fichiers de classe null +webappClassLoader.checkThreadLocalsForLeaks=L''application web [{0}] a crée un ThreadLocal avec une clé de type [{1}] (valeur [{2}]) et une valeur de type [{3}] (valeur [{4}]) mais ne l''a pas supprimé lorsqu''elle a été arrêtée, les threads seront graduellement renouvelés pour éviter une probable fuite de mémoire +webappClassLoader.checkThreadLocalsForLeaks.badKey=Impossible de déterminer une représentation sous forme de chaîne de caractères d''une clé du type [{0}] +webappClassLoader.checkThreadLocalsForLeaks.badValue=Impossible de déterminer la représentation sous forme de chaîne de caractère de la valeur du type [{0}] +webappClassLoader.checkThreadLocalsForLeaks.unknown=Inconnu +webappClassLoader.checkThreadLocalsForLeaksFail=Echec de vérfication des références ThreadLocal pour l''application web [{0}] +webappClassLoader.checkThreadLocalsForLeaksNone=L''application web [{0}] a crée un ThreadLocal avec une clé de type [{1}] (valeur [{2}]) et une valeur de type [{3}] (valeur [{4}]) mais comme les clés n''ont que des références faibles dans la structure des ThreadLocal cela ne causera pas de fuite de mémoire +webappClassLoader.checkThreadLocalsForLeaksNull=L''application web [{0}] a crée un ThreadLocal avec une clé de type [{1}] (valeur [{2}]), ce ThreadLocal a correctement été fixé à null et la valeur sera enlevée par la GC +webappClassLoader.checkThreadsHttpClient=Trouvé une thread utilisé par HttpClient pour maintenir les connections actives, corrigé en associant le thread avec le chargeur de classe parent +webappClassLoader.clearJdbc=L''application web [{0}] a enregistré un pilote JDBC [{1}], mais ne l''a pas désenregistré avant l''arrêt de l''application. Pour éviter une fuite de mémoire, le pilote JDBC a été désenregistré de force. +webappClassLoader.clearObjectStreamClassCachesFail=Impossible d''effacer les références faibles de ObjectStreamClass$Caches pour l''application web [{0}] +webappClassLoader.clearRmi=Trouvé une cible RMI avec une classe squelette de classe [{0}] et une valeur [{1}], celle ci a été enlevée de force pour prévenir ue fuite de mémoir +webappClassLoader.clearRmiFail=Impossible d''effacer le chargeur de classes de contexte référencé depuis sun.rmi.transport.Target pour l''application web [{0}] +webappClassLoader.clearRmiInfo=Impossible de trouver la classe sun.rmi.transport.Target pour nettoyer le chargeur de classes du contexte pour l''application web [{0}], c''est normal pour les JVMs non Oracle +webappClassLoader.getThreadGroupError=Impossible d''obtenir le parent pour le ThreadGroup [{0}], il ne sera pas possible de vérifier tous les threads pour des fuites de mémoire +webappClassLoader.jarsAdded=Un ou plusieurs JARs ont été ajoutés à l''application web [{0}] +webappClassLoader.jarsModified=Un ou plusieurs JARs ont été modifiés dans l''application web [{0}] +webappClassLoader.jarsRemoved=Un ou plusieurs JARs ont été enlevés de l''application web [{0}] +webappClassLoader.javaseClassLoaderNull=L'attribut j2seClassLoader ne doit pas être null +webappClassLoader.jdbcRemoveFailed=Le désenregistrement du pilote JDBC a échoué pour l''application web [{0}] +webappClassLoader.loadedByThisOrChildFail=Impossible de vérifier complètement les entrées d''une instance de [{0}] pour des fuites de mémoire potentielles dans le contexte [{1}] +webappClassLoader.readError=Erreur lors de la lecture de la ressource : impossible de charger [{0}]. +webappClassLoader.removeTransformer=Enlevé le transformateur de fichiers de classe [{0}] de l''application web [{1}] +webappClassLoader.resourceModified=La ressource [{0}] a été modifiée, la date de dernière modification était [{1}] et est désormais [{2}] +webappClassLoader.restrictedPackage=Violation de sécurité en essayant d''utiliser à une classe à accès restreint [{0}] +webappClassLoader.securityException=Exception de sécurité en essayant de trouver la classe [{0}] dans findClassInternal [{1}] +webappClassLoader.stackTrace=L''application web [{0}] semble avoir démarré un thread nommé [{1}] mais ne l''a pas arrêté, ce qui va probablement créer une fuite de mémoire ; la trace du thread est : {2} +webappClassLoader.stackTraceRequestThread=Une requête de l''application web [{0}] est toujours en cours, ce qui causera certainement une fuite de mémoire, vous pouvez contrôler le temps alloué en utilisant l''attribut unloadDelay de l''implémentation standard de Context ; trace du fil d’exécution de la requête : [{2}] +webappClassLoader.stopThreadFail=Impossible de terminer le thread nommé [{0}] pour l''application [{1}] +webappClassLoader.stopTimerThreadFail=Echec de l''arrêt du TimerThread nommé [{0}] pour l''application web [{1}] +webappClassLoader.stopped=Impossible de charger [{0}], ce chargeur de classes a déjà été arrêté +webappClassLoader.superCloseFail=Echec lors de l'appel de close() dans la superclasse +webappClassLoader.transformError=Erreur d''instrumentation : impossible de transformer la classe [{0}] parce que son format est illégal +webappClassLoader.warnTimerThread=L''application [{0}] semble avoir démarré un TimerThread nommé [{1}] en utilisant java.util.Timer mais ne l''a pas stoppé, le timer ainsi que le thread associé ont été arrêtés pour éviter une fuite de mémoire +webappClassLoader.wrongVersion=(impossible de charger la classe [{0}]) + +webappClassLoaderParallel.registrationFailed=L'enregistrement de org.apache.catalina.loader.ParallelWebappClassLoader comme pouvant charger des classes en parallèle a échoué + +webappLoader.deploy=Déploiement des classes des réceptacles (class repositories) vers le répertoire de travail [{0}] +webappLoader.noResources=Pas de ressources trouvées pour le contexte [{0}] +webappLoader.reloadable=Impossible de mettre la propriété rechargeable à [{0}] +webappLoader.setContext.ise=Il est interdit de définir le Context lorsque le chargeur de classes a déjà été démarré +webappLoader.startError=Erreur lors du démarrage du chargeur +webappLoader.starting=Démarrage de ce chargeur (loader) +webappLoader.stopError=Erreur lors de l'arrêt du chargeur +webappLoader.stopping=Arrêt de ce chargeur (loader) +webappLoader.unknownClassLoader=Le chargeur de classes [{0}] de la classe [{1}] est inconnu +webappLoader.unknownProfile=Un nom de profil Jakarta [{0}] inconnu a été spécifié, le profil par défaut sera utilisé à la place diff --git a/java/org/apache/catalina/loader/LocalStrings_ja.properties b/java/org/apache/catalina/loader/LocalStrings_ja.properties new file mode 100644 index 0000000..b13b88a --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_ja.properties @@ -0,0 +1,70 @@ +# 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. + +webappClassLoader.addExportsJavaIo=Java 9以é™ã§å®Ÿè¡Œã™ã‚‹å ´åˆã¯ã€JVMコマンドライン引数㫠"--add-opens=java.base/java.io={0}" を追加ã—ã¦ã€ObjectStreamキャッシュメモリリークä¿è­·ã‚’有効ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ ã¾ãŸã¯ã€ObjectStreamクラスã®ã‚­ãƒ£ãƒƒã‚·ãƒ¥ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ä¿è­·ã‚’無効ã«ã™ã‚‹ã“ã¨ã§ã€ã“ã®è­¦å‘Šã‚’抑制ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ +webappClassLoader.addExportsRmi=Java 9以é™ã§å®Ÿè¡Œã™ã‚‹å ´åˆã¯ã€JVMコマンドライン引数㫠"-add-opens=java.rmi/sun.rmi.transport={0}" を追加ã—ã¦ã€RMIターゲットメモリリーク検出を有効ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ ã¾ãŸã€RMIターゲットメモリリーク検出を無効ã«ã™ã‚‹ã“ã¨ã§ã€ã“ã®è­¦å‘Šã‚’抑制ã™ã‚‹ã“ã¨ã‚‚å¯èƒ½ã§ã™ã€‚ +webappClassLoader.addExportsThreadLocal=ThreadLocalメモリリーク検出を有効ã«ã™ã‚‹ã«ã¯ã€JVMコマンドライン引数㫠"--add-opens=java.base/java.lang={0}"を追加ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ã‚ã‚‹ã„ã¯ã€ThreadLocalメモリリーク検出を無効ã«ã™ã‚‹ã“ã¨ã§ã€ã“ã®è­¦å‘Šã‚’抑制ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ +webappClassLoader.addPermissionNoCanonicalFile=URL [{0}] ã®æ­£è¦åŒ–パスをå–å¾—ã§ãã¾ã›ã‚“ +webappClassLoader.addPermissionNoProtocol=URL [{1}] ã«å«ã¾ã‚Œã‚‹ãƒ—ロトコル [{0}] ã¯æœªå¯¾å¿œã§ã™ã€‚ã“ã® URL ã§å‚ç…§ã§ãるリソースã«ã¯èª­ã¿å–り権é™ã‚’付与ã§ãã¾ã›ã‚“。 +webappClassLoader.addTransformer=Web アプリケーション [{1}] ã«ã‚¯ãƒ©ã‚¹ãƒ•ã‚¡ã‚¤ãƒ«å¤‰æ›å™¨ [{0}] を追加ã—ã¾ã—ãŸã€‚ +webappClassLoader.addTransformer.duplicate=クラスファイルトランスフォーマー[{0}]ã‚’Webアプリケーション[{1}]ã«è¿½åŠ ã™ã‚‹å‘¼ã³å‡ºã—ãŒé‡è¤‡ã—ã¾ã—ãŸã€‚ +webappClassLoader.addTransformer.illegalArgument=Webアプリケーション[{0}]ãŒNullクラスファイルトランスフォーマーを追加ã—よã†ã¨ã—ã¾ã—ãŸã€‚ +webappClassLoader.checkThreadLocalsForLeaks=Webアプリケーション [{0}] ã¯ã‚¿ã‚¤ãƒ— [{1}] (値[{2}]) ã®ã‚­ãƒ¼ã¨å€¤ã‚¿ã‚¤ãƒ— [{3}] (値[{4}]) ã®ThreadLocalを作æˆã—ã¾ã—ãŸãŒã€ãã‚Œã¯Webアプリケーションã®åœæ­¢æ™‚ã«å‰Šé™¤ã•ã‚Œã¦ã„ã¾ã›ã‚“。スレッドã¯æ™‚é–“ã®çµŒéŽã¨ã¨ã‚‚ã«æ›´æ–°ã•ã‚Œã€ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã®å¯èƒ½æ€§ã‚’回é¿ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ +webappClassLoader.checkThreadLocalsForLeaks.badKey=キー値ã®ã‚¯ãƒ©ã‚¹ [{0}] ã®æ–‡å­—列表ç¾ã‚’å–å¾—ã§ãã¾ã›ã‚“。 +webappClassLoader.checkThreadLocalsForLeaks.badValue=クラス [{0}] ã®æ–‡å­—列表ç¾ã‚’å–å¾—ã§ãã¾ã›ã‚“。 +webappClassLoader.checkThreadLocalsForLeaks.unknown=ä¸æ˜Ž +webappClassLoader.checkThreadLocalsForLeaksFail=Webアプリケーション[{0}]ã®ThreadLocalå‚照を確èªã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +webappClassLoader.checkThreadLocalsForLeaksNone=Web アプリケーション [{0}] ã¯ã‚­ãƒ¼ [{1}] (値㯠[{1}])ã€å€¤ [{3}](値㯠[{4}])をスレッドローカルã«ä½œæˆã—ã¾ã—ãŸã€‚キー㯠ThreadLocalMap ã®å¼±å‚ç…§ã¨ã—ã¦ä¿æŒã•ã‚Œã‚‹ãŸã‚ã€ãƒ¡ãƒ¢ãƒªãƒ¼ãƒªãƒ¼ã‚¯ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +webappClassLoader.checkThreadLocalsForLeaksNull=Webアプリケーション [{0}] ã¯ã‚¿ã‚¤ãƒ— [{1}] (値 [{2}]) ã®ã‚­ãƒ¼ã‚’æŒã¤ThreadLocalを作æˆã—ã¾ã—ãŸã€‚ThreadLocalã¯æ­£ã—ãnullã«è¨­å®šã•ã‚Œã€ã‚­ãƒ¼ã¯GCã«ã‚ˆã£ã¦å‰Šé™¤ã•ã‚Œã¾ã™ã€‚ +webappClassLoader.checkThreadsHttpClient=Webアプリケーションクラスローダーを使用ã—ã¦ã„ã‚‹HttpClientキープアライブスレッドを検出ã—ã¾ã—ãŸã€‚ スレッドを親クラスローダã«åˆ‡ã‚Šæ›¿ãˆã‚‹ã“ã¨ã§ä¿®æ­£ã•ã‚Œã¾ã—ãŸã€‚ +webappClassLoader.clearJdbc=Web アプリケーション [{0}] ã¯è‡ªèº«ã§ç™»éŒ²ã—㟠JDBC ドライãƒãƒ¼ [{1}] ã‚’åœæ­¢æ™‚ã«è§£é™¤ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚メモリーリークã®ç™ºç”Ÿã‚’防ãã«ã¯ JDBC ドライãƒãƒ¼ã‚’強制的ã«è§£é™¤ã—ã¦ãã ã•ã„。 +webappClassLoader.clearObjectStreamClassCachesFail=Web アプリケーション [{0}] ã® ObjectStreamClass$Caches ã«ã¤ã„ã¦ã‚½ãƒ•ãƒˆå‚照を除去ã§ãã¾ã›ã‚“。 +webappClassLoader.clearRmi=クラス [{0}] 値 [{1}] ã®ã‚¹ã‚¿ãƒ–クラスをæŒã¤ RMI ターゲットを発見ã—ã¾ã—ãŸã€‚メモリーリークを防ããŸã‚発見ã—㟠RMI ターゲットã¯å¼·åˆ¶çš„ã«å‰Šé™¤ã—ã¾ã™ã€‚ +webappClassLoader.clearRmiFail=Webアプリケーション[{0}]ã®sun.rmi.transport.Targetã‹ã‚‰å‚ç…§ã•ã‚Œã‚‹ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ãƒ¼ã®ã‚¯ãƒªã‚¢ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +webappClassLoader.clearRmiInfo=Web アプリケーション [{0}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ãƒ¼ã‚’ãã‚Œã„ã«ã™ã‚‹ãŸã‚ã® sun.rmi.transport.Target クラスãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。Sun 以外㮠JVM ã§å®Ÿè¡Œã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +webappClassLoader.getThreadGroupError=スレッドグループ [{0}] ã®è¦ªã‚¹ãƒ¬ãƒƒãƒ‰ã‚°ãƒ«ãƒ¼ãƒ—ã‚’å–å¾—ã§ãã¾ã›ã‚“。潜在的ãªãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã‚’ã™ã¹ã¦ã®ã‚¹ãƒ¬ãƒƒãƒ‰ã§ãƒã‚§ãƒƒã‚¯ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +webappClassLoader.jarsAdded=1ã¤ä»¥ä¸Šã®JARãŒWebアプリケーション[{0}]ã«è¿½åŠ ã•ã‚Œã¾ã—ãŸã€‚ +webappClassLoader.jarsModified=1ã¤ä»¥ä¸Šã®JARãŒWebアプリケーション[{0}]ã§å¤‰æ›´ã•ã‚Œã¾ã—ãŸã€‚ +webappClassLoader.jarsRemoved=1ã¤ä»¥ä¸Šã®JARãŒWebアプリケーション[{0}]ã§å‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚ +webappClassLoader.javaseClassLoaderNull=j2seClassLoader属性ã¯nullã§ãªã„å ´åˆãŒã‚ã‚Šã¾ã™ã€‚ +webappClassLoader.jdbcRemoveFailed=Web アプリケーション [{0}] 㯠JDBC ドライãƒãƒ¼ã®ç™»éŒ²ã‚’解除ã§ãã¾ã›ã‚“。 +webappClassLoader.loadedByThisOrChildFail=クラス [{0}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®å…¨ã¦ã®è¦ç´ ã‚’ãƒã‚§ãƒƒã‚¯ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚コンテキスト [{1}] ã§ãƒ¡ãƒ¢ãƒªãƒ¼ãƒªãƒ¼ã‚¯ã®ç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +webappClassLoader.readError=リソース読ã¿è¾¼ã¿ã‚¨ãƒ©ãƒ¼: [{0}] ãŒèª­ã¿è¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸã€‚ +webappClassLoader.removeTransformer=クラスファイル変æ›å™¨ [{0}] ã‚’ Web アプリケーション [{1}] ã‹ã‚‰å‰Šé™¤ã—ã¾ã—ãŸã€‚ +webappClassLoader.resourceModified=リソース [{0}] ã¯å¤‰æ›´ã•ã‚Œã¦ã„ã¾ã™ã€‚ç›´å‰ã®æ›´æ–°æ—¥æ™‚㯠[{1}]ã€æœ€æ–°ã®æ›´æ–°æ—¥æ™‚㯠[{2}] ã§ã™ã€‚ +webappClassLoader.restrictedPackage=セキュリティーé•å。制é™ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{0}] を使ãŠã†ã¨ã—ã¾ã—ãŸã€‚ +webappClassLoader.securityException=indClassInternal [{1}] ã§ã‚¯ãƒ©ã‚¹ [{0}] を検索中ã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ä¾‹å¤–ã§ã™ +webappClassLoader.stackTrace=Webアプリケーション [{0}] 㯠[{1}] ã¨ã„ã†åå‰ã®ã‚¹ãƒ¬ãƒƒãƒ‰ã‚’開始ã—ãŸã‚ˆã†ã§ã™ãŒã€åœæ­¢ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ã“ã‚Œã¯ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã‚’引ãèµ·ã“ã™å¯èƒ½æ€§ãŒéžå¸¸ã«é«˜ã„ã§ã™ã€‚スレッドã®ã‚¹ã‚¿ãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹: {2} +webappClassLoader.stackTraceRequestThread=Webアプリケーション[{0}]ã¯ã¾ã å®Œäº†ã—ã¦ã„ãªã„リクエストを処ç†ã—ã¦ã„ã¾ã™ã€‚ ã“ã‚Œã¯ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã‚’引ãèµ·ã“ã™å¯èƒ½æ€§ãŒéžå¸¸ã«é«˜ã„ã§ã™ã€‚ リクエストã®çµ‚了時間ã¯ã€StandardContext実装ã®unloadDelay属性を使用ã—ã¦åˆ¶å¾¡ã§ãã¾ã™ã€‚ リクエスト処ç†ã‚¹ãƒ¬ãƒƒãƒ‰ã®ã‚¹ã‚¿ãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹ï¼š[{2}] +webappClassLoader.stopThreadFail=Web アプリケーション [{1}] ã®ã‚¹ãƒ¬ãƒƒãƒ‰ [{0}] ã¯çµ‚了ã§ãã¾ã›ã‚“。 +webappClassLoader.stopTimerThreadFail=Webアプリケーション [{1}] ã® [{0}] ã¨ã„ã†åå‰ã®TimerThreadを終了ã§ãã¾ã›ã‚“ã§ã—㟠+webappClassLoader.stopped=ä¸æ­£ãªã‚¢ã‚¯ã‚»ã‚¹: ã“ã®Webアプリケーションã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¯æ—¢ã«åœæ­¢ã•ã‚Œã¦ã„ã¾ã™ Could not load [{0}]. ä¸æ­£ãªã‚¢ã‚¯ã‚»ã‚¹ã‚’引ãèµ·ã“ã—ãŸã‚¹ãƒ¬ãƒƒãƒ‰ã‚’終了ã•ã›ã€æŠ•ã’られãŸã‚¨ãƒ©ãƒ¼ã«ã‚ˆã‚Šãƒ‡ãƒãƒƒã‚°ç”¨ã«æ¬¡ã®ã‚¹ã‚¿ãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹ãŒç”Ÿæˆã•ã‚Œã¾ã—ãŸãŒï¼Œæ©Ÿèƒ½ã«å½±éŸ¿ã¯ã‚ã‚Šã¾ã›ã‚“ +webappClassLoader.superCloseFail=基底クラス㧠close() ã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +webappClassLoader.transformError=instrumentation エラー: クラスファイル形å¼ãŒæ­£å½“ã§ãªã„ãŸã‚ã€ã‚¯ãƒ©ã‚¹ [{0}] を変æ›ã§ãã¾ã›ã‚“ã§ã—㟠+webappClassLoader.warnTimerThread=Web アプリケーション [{0}] ㌠java.util.Timer API ã§é–‹å§‹ã—㟠TimerThread [{1}] ã‚’åœæ­¢ã§ãã¾ã›ã‚“。メモリーリークを防ãã«ã¯ã‚¿ã‚¤ãƒžãƒ¼(ã¨ãã‚Œã«ä¼´ã£ã¦é–¢é€£ä»˜ã‘られãŸã‚¹ãƒ¬ãƒƒãƒ‰)を強制的ã«ã‚­ãƒ£ãƒ³ã‚»ãƒ«ã—ã¦ãã ã•ã„。 +webappClassLoader.wrongVersion=(クラス [{0}] をロードã§ãã¾ã›ã‚“) + +webappClassLoaderParallel.registrationFailed=並列ã«ã‚¯ãƒ©ã‚¹ã‚’ロードã§ãã‚‹org.apache.catalina.loader.ParallelWebappClassLoaderã®ç™»éŒ²ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ + +webappLoader.deploy=クラスリãƒã‚¸ãƒˆãƒªã‚’作業ディレクトリ [{0}] ã«é…å‚™ã—ã¾ã™ +webappLoader.noResources=コンテキスト [{0}] ã«ã¯ãƒªã‚½ãƒ¼ã‚¹ãŒã‚ã‚Šã¾ã›ã‚“。 +webappLoader.reloadable=reloadableプロパティを [{0}] ã«è¨­å®šã§ãã¾ã›ã‚“ +webappLoader.setContext.ise=クラスローダーãŒé–‹å§‹ã—ãŸã‚ã¨ã¯ Context を構æˆã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +webappLoader.startError=webappクラスローダー開始中ã®ã‚¨ãƒ©ãƒ¼ +webappLoader.starting=ã“ã®ãƒ­ãƒ¼ãƒ€ã‚’èµ·å‹•ã—ã¾ã™ +webappLoader.stopError=webappクラスローダåœæ­¢ä¸­ã®ã‚¨ãƒ©ãƒ¼ +webappLoader.stopping=ã“ã®ãƒ­ãƒ¼ãƒ€ã‚’åœæ­¢ã—ã¾ã™ +webappLoader.unknownClassLoader=クラス [{1}] ã®ã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ãƒ¼ [{0}] ã¯æœªçŸ¥ã®ã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ãƒ¼ã§ã™ã€‚ +webappLoader.unknownProfile=ä¸æ˜Žãª Jakarta プロファイル [{0}]ãŒæŒ‡å®šã•ã‚ŒãŸãŸã‚ã€ä»£ã‚ã‚Šã«æ—¢å®šã®ãƒ—ロファイルãŒä½¿ç”¨ã•ã‚Œã¾ã™ diff --git a/java/org/apache/catalina/loader/LocalStrings_ko.properties b/java/org/apache/catalina/loader/LocalStrings_ko.properties new file mode 100644 index 0000000..438086b --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_ko.properties @@ -0,0 +1,70 @@ +# 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. + +webappClassLoader.addExportsJavaIo=Java 9 ë˜ëŠ” ì´í›„ ë²„ì „ì„ ì‚¬ìš©í•  ë•Œì—는, "--add-opens=java.base/java.io={0}" ì„(를) JVM 명령행 ì•„ê·œë¨¼íŠ¸ë“¤ì— ì¶”ê°€í•´ì„œ, ObjectStream ìºì‹œ 메모리 릭 방지 ì˜µì…˜ì„ í™œì„±í™”í•  필요가 있습니다. ë˜ëŠ” ì›í•˜ì‹œëŠ” 경우, ObjectStream í´ëž˜ìŠ¤ ìºì‹œ 메모리 릭 방지 ì˜µì…˜ì„ ë¹„í™œì„±í™”í•´ì„œ ì´ ê²½ê³  메시지를 없앨 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. +webappClassLoader.addExportsRmi=Java 9ì—ì„œ 실행하면서 RMI Target 메모리 누수 íƒì§€ë¥¼ 사용 가능하게 하려면, "--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED"를 JVM 명령 í–‰ ì•„ê·œë¨¼íŠ¸ì— ì¶”ê°€í•´ì•¼ 합니다. ë˜ëŠ”, RMI Target 메모리 누수 íƒì§€ë¥¼ 사용불능 ìƒíƒœë¡œ 설정함으로ì¨, ì´ ê²½ê³ ë¥¼ 없앨 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. +webappClassLoader.addExportsThreadLocal=ThreadLocal 메모리 누수 íƒì§€ë¥¼ 위해서는 "--add-opens=java.base/java.lang={0}" ìžë°” 명령 í–‰ 아규먼트를 추가하십시오. ë˜ëŠ” ì´ ê²½ê³  메시지를 없애고 싶다면 ThreadLocal 메모리 누수 íƒì§€ ì˜µì…˜ì„ ë¹„í™œì„±í™” 시키십시오. +webappClassLoader.addPermissionNoCanonicalFile=URL [{0}](으)로부터 canonical íŒŒì¼ ê²½ë¡œë¥¼ ì–»ì„ ìˆ˜ 없습니다. +webappClassLoader.addPermissionNoProtocol=URL [{1}] ë‚´ì˜ í”„ë¡œí† ì½œ [{0}]ì€(는) 지ì›ë˜ì§€ 않으므로, ì´ URLì˜ ë¦¬ì†ŒìŠ¤ì— ëŒ€í•œ ì½ê¸°ê°€ 승ì¸ë˜ì§€ 않았습니다. +webappClassLoader.addTransformer=í´ëž˜ìŠ¤ íŒŒì¼ Transformer [{0}]ì„(를) 웹 애플리케ì´ì…˜ [{1}]ì— ì¶”ê°€í–ˆìŠµë‹ˆë‹¤. +webappClassLoader.addTransformer.duplicate=웹 애플리케ì´ì…˜ [{1}]ì— í´ëž˜ìŠ¤ íŒŒì¼ ë³€í™˜ê¸° [{0}]ì„(를) 추가하기 위한, ì¤‘ë³µëœ í˜¸ì¶œì„ ë¬´ì‹œí•©ë‹ˆë‹¤. +webappClassLoader.addTransformer.illegalArgument=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€) ë„ì¸, í´ëž˜ìŠ¤ íŒŒì¼ Transformerì„ ì¶”ê°€í•˜ë ¤ ì‹œë„했습니다. +webappClassLoader.checkThreadLocalsForLeaks=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€), 타입 [{1}]ì¸ í‚¤ì™€ (ê°’: [{2}]) 타입 [{3}]ì¸ ê°’ì„ (ê°’: [{4}]) 사용하여 ThreadLocal ê°ì²´ë¥¼ ìƒì„±í–ˆì§€ë§Œ, 웹 애플리케ì´ì…˜ì´ ì¤‘ì§€ë  ë•Œ ê·¸ê²ƒì„ ì œê±°í•˜ì§€ 못했습니다. 혹시 ìžˆì„ ë²•í•œ 메모리 누수를 방지하기 위하여, ì‹œê°„ì„ ë‘ê³  ì“°ë ˆë“œë“¤ì„ ìž¬ìƒì„±í•  것입니다. +webappClassLoader.checkThreadLocalsForLeaks.badKey=íƒ€ìž…ì´ [{0}]ì¸ í‚¤ì˜ ë¬¸ìžì—´ representationì„ ê²°ì •í•  수 없습니다. +webappClassLoader.checkThreadLocalsForLeaks.badValue=íƒ€ìž…ì´ [{0}]ì¸ ê°’ì˜ ë¬¸ìžì—´ representationì„ ê²°ì •í•  수 없습니다. +webappClassLoader.checkThreadLocalsForLeaks.unknown=ì•Œ 수 ì—†ìŒ +webappClassLoader.checkThreadLocalsForLeaksFail=웹 애플리케ì´ì…˜ [{0}]ì„(를) 위한 ThreadLocal ì°¸ì¡°ë“¤ì— ëŒ€í•œ ì ê²€ì´ 실패했습니다. +webappClassLoader.checkThreadLocalsForLeaksNone=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€), 타입 [{1}]ì˜ í‚¤(ê°’: [{2}])와 타입 [{3}]ì˜ ê°’(ê°’: [{4}])ì„ ì‚¬ìš©í•˜ì—¬, ThreadLocal Mapì„ ìƒì„±í–ˆìŠµë‹ˆë‹¤. 해당 ThreadLocal Mapì€ weak í‚¤ë“¤ì„ ìœ ì§€í•˜ê³  있기 때문ì—, ì´ëŠ” 메모리 누수가 아닙니다. +webappClassLoader.checkThreadLocalsForLeaksNull=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€), 타입 [{1}]ì¸ í‚¤ë¥¼ (ê°’: [{2}]) 사용하여 ThreadLocal ê°ì²´ë¥¼ ìƒì„±í–ˆìŠµë‹ˆë‹¤. 해당 ThreadLocal ê°ì²´ê°€ 올바르게 ë„ë¡œ 설정ë˜ì—ˆìœ¼ë¯€ë¡œ, 해당 키는 GCì— ì˜í•´ 제거 ë  ê²ƒìž…ë‹ˆë‹¤. +webappClassLoader.checkThreadsHttpClient=웹 애플리케ì´ì…˜ í´ëž˜ìŠ¤ë¡œë”를 사용하는 HttpClient keep-alive 쓰레드를 발견했습니다. ì“°ë ˆë“œì˜ í´ëž˜ìŠ¤ë¡œë”를 부모 í´ëž˜ìŠ¤ë¡œë”ë¡œ 전환시켰습니다. +webappClassLoader.clearJdbc=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€) JDBC ë“œë¼ì´ë²„ [{1}]ì„(를) 등ë¡í–ˆì§€ë§Œ, 웹 애플리케ì´ì…˜ì´ ì¤‘ì§€ë  ë•Œ, 해당 JDBC ë“œë¼ì´ë²„ì˜ ë“±ë¡ì„ 제거하지 못했습니다. 메모리 누수를 방지하기 위하여, 등ë¡ì„ 강제로 제거했습니다. +webappClassLoader.clearObjectStreamClassCachesFail=웹 애플리케ì´ì…˜ [{0}]ì„(를) 위해, ObjectStreamClass$Caches로부터 soft references를 í기하지 못했습니다. +webappClassLoader.clearRmi=ìŠ¤í… í´ëž˜ìŠ¤ [{0}]와(ê³¼) ê°’ [{1}]ì„(를) 가진 RMI Targetì„ ë°œê²¬í–ˆìŠµë‹ˆë‹¤. ì´ RMI Targetì€ ë©”ëª¨ë¦¬ 누수를 방지하기 위하여 강제로 제거ë˜ì—ˆìŠµë‹ˆë‹¤. +webappClassLoader.clearRmiFail=sun.rmi.transport.Target으로부터 ì°¸ì¡°ëœ ì»¨í…스트 í´ëž˜ìŠ¤ë¡œë”를, 웹 애플리케ì´ì…˜ [{0}]ì„(를) 위해, í기하지 못했습니다. +webappClassLoader.clearRmiInfo=웹 애플리케ì´ì…˜ [{0}]ì„(를) 위한 컨í…스트 í´ëž˜ìŠ¤ë¡œë”를 í기하기 위한, í´ëž˜ìŠ¤ sun.rmi.transport.Targetì„ ì°¾ì§€ 못했습니다. ì´ëŠ” Sun JVMë“¤ì´ ì•„ë‹Œ 환경ì—ì„œ ë°œìƒí•  수 있습니다. +webappClassLoader.getThreadGroupError=ThreadGroup [{0}]ì˜ ë¶€ëª¨ ThreadGroupì„ ì–»ì„ ìˆ˜ 없습니다. 잠재ì ì¸ 메모리 누수 문제를 찾기 위해 모든 ì“°ë ˆë“œë“¤ì„ ì ê²€í•˜ëŠ” ê²ƒì´ ë¶ˆê°€ëŠ¥í•©ë‹ˆë‹¤. +webappClassLoader.jarsAdded=하나 ì´ìƒì˜ JARë“¤ì´ ì›¹ 애플리케ì´ì…˜ [{0}]ì— ì¶”ê°€ë습니다. +webappClassLoader.jarsModified=웹 애플리케ì´ì…˜ [{0}]ì—ì„œ 하나 ì´ìƒì˜ JAR 파ì¼(들)ì´ ë³€ê²½ë˜ì—ˆìŠµë‹ˆë‹¤. +webappClassLoader.jarsRemoved=하나 ì´ìƒì˜ JARë“¤ì´ ì›¹ 애플리케ì´ì…˜ [{0}](으)로부터 제거ë˜ì—ˆìŠµë‹ˆë‹¤. +webappClassLoader.javaseClassLoaderNull=j2seClassLoader ì†ì„±ì´ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +webappClassLoader.jdbcRemoveFailed=웹 애플리케ì´ì…˜ [{0}]ì„(를) 위한 JDBC ë“œë¼ì´ë²„ì˜ ë“±ë¡ì„ 제거하지 못했습니다. +webappClassLoader.loadedByThisOrChildFail=컨í…스트 [{1}]ì—ì„œ ìž ìž¬ì  ë©”ëª¨ë¦¬ 누수를 방지하기 위해, [{0}] í´ëž˜ìŠ¤ì˜ ì¸ìŠ¤í„´ìŠ¤ ë‚´ì— ìžˆëŠ” ì—”íŠ¸ë¦¬ë“¤ì„ ëª¨ë‘ ì ê²€í•˜ë ¤ëŠ” ì‹œë„ê°€ 실패했습니다. +webappClassLoader.readError=리소스 ì½ê¸° 오류 : [{0}]ì„(를) 로드할 수 없었습니다. +webappClassLoader.removeTransformer=웹 애플리케ì´ì…˜ [{1}](으)로부터 í´ëž˜ìŠ¤ íŒŒì¼ Transformer [{0}]ì„(를) 제거했습니다. +webappClassLoader.resourceModified=리소스 [{0}]ì´(ê°€) ë³€ê²½ëœ ì ì´ 있습니다. 최종 변경 ì‹œê°„ì´ [{1}]ì´ì—ˆëŠ”ë°, ì´ì œ [{2}](으)ë¡œ 바뀌었습니다. +webappClassLoader.restrictedPackage=보안 위반 행위: ì œí•œëœ í´ëž˜ìŠ¤ [{0}]ì„(를) 사용하려 ì‹œë„했습니다. +webappClassLoader.securityException=findClassInternalì—ì„œ, í´ëž˜ìŠ¤ [{0}]ì„(를) 찾으려 ì‹œë„ ì¤‘ 보안 예외 ë°œìƒ: [{1}] +webappClassLoader.stackTrace=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€) [{1}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì“°ë ˆë“œë¥¼ 시작시킨 것으로 ë³´ì´ì§€ë§Œ, 해당 쓰레드를 중지시키지 못했습니다. ì´ëŠ” 메모리 누수를 유발할 ê°€ëŠ¥ì„±ì´ í½ë‹ˆë‹¤. 해당 ì“°ë ˆë“œì˜ ìŠ¤íƒ íŠ¸ë ˆì´ìŠ¤:{2} +webappClassLoader.stackTraceRequestThread=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€) 여전히 완료ë˜ì§€ ì•Šì€ ìš”ì²­ì„ ì²˜ë¦¬í•˜ê³  있습니다. ì´ëŠ” 메모리 누수를 유발할 ê°€ëŠ¥ì„±ì´ ë†’ìŠµë‹ˆë‹¤. 표준 컨í…스트 êµ¬í˜„ì˜ unloadDelay ì†ì„±ì„ ì´ìš©í•˜ì—¬, 요청 완료 허용 ì‹œê°„ì„ í†µì œí•  수 있습니다. 요청 처리 ì“°ë ˆë“œì˜ ìŠ¤íƒ íŠ¸ë ˆì´ìŠ¤:[{2}] +webappClassLoader.stopThreadFail=웹 애플리케ì´ì…˜ [{1}]ì„ ìœ„í•œ, [{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì“°ë ˆë“œë¥¼ 종료시키지 못했습니다. +webappClassLoader.stopTimerThreadFail=웹 애플리케ì´ì…˜ [{1}]ì„(를) 위한, [{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ TimerThread를 종료시키지 못했습니다. +webappClassLoader.stopped=불허ë˜ëŠ” ì ‘ê·¼: ì´ ì›¹ 애플리케ì´ì…˜ ì¸ìŠ¤í„´ìŠ¤ëŠ” ì´ë¯¸ 중지ë˜ì—ˆìŠµë‹ˆë‹¤. [{0}]ì„(를) 로드할 수 없습니다. 디버그 ëª©ì  ë° ë¶ˆí—ˆë˜ëŠ” ì ‘ê·¼ì„ ë°œìƒì‹œí‚¨ 해당 쓰레드를 종료시키기 위한 ì‹œë„로서, ë‹¤ìŒ ìŠ¤íƒ íŠ¸ë ˆì´ìŠ¤ê°€ ìƒì„±ë©ë‹ˆë‹¤. +webappClassLoader.superCloseFail=부모 í´ëž˜ìŠ¤ì˜ close() 호출 ì‹œ 실패 ë°œìƒ +webappClassLoader.transformError=Instrumentation 오류: í´ëž˜ìŠ¤ íŒŒì¼ í¬ë§·ì´ ê·œì•½ì„ ë”°ë¥´ì§€ 않기 때문ì—, í´ëž˜ìŠ¤ [{0}]ì„(를) 변환시킬 수 없었습니다. +webappClassLoader.warnTimerThread=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€) java.util.Timer API를 통해 [{1}](ì´)ë¼ëŠ” ì´ë¦„ì„ ê°€ì§„ TimerThread를 시작한 것으로 ë³´ì´ì§€ë§Œ, ê·¸ 쓰레드를 중지시키지 못했습니다. 메모리 누수를 방지하기 위해, 타ì´ë¨¸ê°€ (ì—°ê´€ëœ ì“°ë ˆë“œì™€ 함께) 강제로 취소ë˜ì—ˆìŠµë‹ˆë‹¤. +webappClassLoader.wrongVersion=(í´ëž˜ìŠ¤ [{0}]ì„(를) 로드할 수 없습니다) + +webappClassLoaderParallel.registrationFailed=org.apache.catalina.loader.ParallelWebappClassLoader를 병렬 í´ëž˜ìŠ¤ ë¡œë”©ì´ ê°€ëŠ¥í•˜ê²Œ 등ë¡í•˜ë ¤ 했지만 실패했습니다. + +webappLoader.deploy=ìž‘ì—… 디렉토리 [{0}]ì— í´ëž˜ìŠ¤ ë ˆíŒŒì§€í† ë¦¬ë“¤ì„ ë°°ì¹˜í•©ë‹ˆë‹¤. +webappLoader.noResources=컨í…스트 [{0}]ì„(를) 위한 ë¦¬ì†ŒìŠ¤ë“¤ì„ ì°¾ì„ ìˆ˜ 없습니다. +webappLoader.reloadable=reloadable 프로í¼í‹°ë¥¼ [{0}](으)ë¡œ 설정할 수 없습니다. +webappLoader.setContext.ise=웹 애플리케ì´ì…˜ ë¡œë”ê°€ 시작ë˜ê³  있는 ë™ì•ˆ, 컨í…스트를 설정하는 ê²ƒì€ í—ˆìš©ë˜ì§€ 않습니다. +webappLoader.startError=ë¡œë”를 시작하는 중 오류 ë°œìƒ +webappLoader.starting=ì´ ë¡œë”를 시작합니다. +webappLoader.stopError=ë¡œë”를 중지시키는 중 오류 ë°œìƒ +webappLoader.stopping=ì´ ë¡œë”를 중지시키는 중 +webappLoader.unknownClassLoader=í´ëž˜ìŠ¤ íƒ€ìž…ì´ [{1}]ì¸, ì•Œ 수 없는 í´ëž˜ìŠ¤ ë¡œë”: [{0}] +webappLoader.unknownProfile=ì•Œ 수 없는 Jakarta 프로파ì¼ì´ 지정ë˜ì—ˆìŠµë‹ˆë‹¤: [{0}]. 기본 프로파ì¼ì„ 대신 사용합니다. diff --git a/java/org/apache/catalina/loader/LocalStrings_pt_BR.properties b/java/org/apache/catalina/loader/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..4676ea2 --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_pt_BR.properties @@ -0,0 +1,17 @@ +# 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. + +webappClassLoader.checkThreadLocalsForLeaks.unknown=Desconhecido +webappClassLoader.readError=Erro ao ler recurso: impossível carregar [{0}] diff --git a/java/org/apache/catalina/loader/LocalStrings_ru.properties b/java/org/apache/catalina/loader/LocalStrings_ru.properties new file mode 100644 index 0000000..40e5d92 --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_ru.properties @@ -0,0 +1,20 @@ +# 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. + +webappClassLoader.addExportsThreadLocal=Вам нужно добавить "--add-opens=java.base/java.lang={0}" к аргументам командной Ñтроки JVM, чтобы включить обнаружение утечек памÑти ThreadLocal. Также Ð’Ñ‹ можете преÑечь поÑвление Ñтого предупреждениÑ, отключив обнаружение утечек памÑти ThreadLocal. +webappClassLoader.checkThreadLocalsForLeaks.unknown=ÐеизвеÑтно +webappClassLoader.readError=Ошибка Ñ‡Ñ‚ÐµÐ½Ð¸Ñ Ñ€ÐµÑурÑа: Ðе возможно загрузить [{0}]. + +webappLoader.setContext.ise=УÑтановка КонтекÑта не разрешена пока запуÑкаетÑÑ Ð·Ð°Ð³Ñ€ÑƒÐ·Ñ‡Ð¸Ðº. diff --git a/java/org/apache/catalina/loader/LocalStrings_zh_CN.properties b/java/org/apache/catalina/loader/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..4661de7 --- /dev/null +++ b/java/org/apache/catalina/loader/LocalStrings_zh_CN.properties @@ -0,0 +1,70 @@ +# 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. + +webappClassLoader.addExportsJavaIo=在Java 9或更高版本上è¿è¡Œæ—¶,需è¦å‘JVM命令行å‚数中添加"--add opens=Java.base/Java.io={0}",以å¯ç”¨ObjectStream缓存内存泄æ¼ä¿æŠ¤.或者,å¯ä»¥é€šè¿‡ç¦ç”¨ObjectStream类缓存内存泄æ¼ä¿æŠ¤æ¥æŠ‘制此警告. +webappClassLoader.addExportsRmi=在Java 9上è¿è¡Œæ—¶ï¼Œéœ€è¦åœ¨JVM命令行å‚数中添加“-add opens=Java.rmi/sun.rmi.transport=ALL-UNNAMEDâ€ï¼Œä»¥å¯ç”¨rmi目标内存泄æ¼æ£€æµ‹ã€‚或者,å¯ä»¥é€šè¿‡ç¦ç”¨RMI目标内存泄æ¼æ£€æµ‹æ¥æŠ‘制此警告。 +webappClassLoader.addExportsThreadLocal=你需è¦åœ¨JVM的命令行å‚数中添加"--add-opens=java.base/java.lang={0}",以开å¯å¯¹ThreadLocal内存泄æ¼çš„监测。或者你å¯ä»¥ç¦ç”¨ThreadLocal内存泄æ¼ç›‘测,以便å–æ¶ˆè­¦å‘Šä¿¡æ¯ +webappClassLoader.addPermissionNoCanonicalFile=无法从URL[{0}]获å–规范文件路径 +webappClassLoader.addPermissionNoProtocol=ä¸æ”¯æŒURL[{1}]中的åè®®[{0}],因此未授予ä½äºŽæ­¤URL的资æºçš„读å–æƒé™ +webappClassLoader.addTransformer=将类文件转æ¢å™¨[{0}]添加到Web应用程åº[{1}]。 +webappClassLoader.addTransformer.duplicate=将类文件转æ¢å™¨[{0}]添加到web应用程åº[{1}]çš„é‡å¤è°ƒç”¨è¢«å¿½ç•¥ã€‚ +webappClassLoader.addTransformer.illegalArgument=Web应用程åº[{0}]试图添加空类文件转æ¢å™¨ã€‚ +webappClassLoader.checkThreadLocalsForLeaks=web应用程åº[{0}]创建了一个ThreadLocal,其键类型为[{1}](值为[{2}]),值类型为[{3}](值为[{4}),但在åœæ­¢web应用程åºæ—¶æœªèƒ½å°†å…¶åˆ é™¤ã€‚线程将éšç€æ—¶é—´çš„推移而更新,以å°è¯•é¿å…å¯èƒ½çš„å†…å­˜æ³„æ¼ +webappClassLoader.checkThreadLocalsForLeaks.badKey=无法确定类型为[{0}]çš„é”®çš„å­—ç¬¦ä¸²è¡¨ç¤ºå½¢å¼ +webappClassLoader.checkThreadLocalsForLeaks.badValue=无法确定类型为 [{0}] çš„å€¼çš„å­—ç¬¦ä¸²è¡¨ç¤ºå½¢å¼ +webappClassLoader.checkThreadLocalsForLeaks.unknown=未知 +webappClassLoader.checkThreadLocalsForLeaksFail=检查ThreadLocal引用失败,web应用程åºï¼š[{0}] +webappClassLoader.checkThreadLocalsForLeaksNone=webåº”ç”¨ç¨‹åº [{0}] 创建了1个ThreadLocalå˜é‡(键:[{2}] (类型[{1}]) ,值:[{4}](类型[{3}]) )。键仅被ThreadLocal Map弱引用,所以ä¸æ˜¯å†…存泄露。 +webappClassLoader.checkThreadLocalsForLeaksNull=web应用程åº[{0}]创建了一个ThreadLocal,其键类型为[{1}](值为[{2}])。ThreadLocal已正确设置为null,GC将删除该键 +webappClassLoader.checkThreadsHttpClient=找到使用web应用程åºç±»åŠ è½½å™¨çš„HttpClientä¿æŒæ´»åŠ¨çº¿ç¨‹ã€‚通过将线程切æ¢åˆ°çˆ¶ç±»åŠ è½½å™¨ä¿®å¤ã€‚ +webappClassLoader.clearJdbc=Webåº”ç”¨ç¨‹åº [{0}] 注册了JDBCé©±åŠ¨ç¨‹åº [{1}],但在Web应用程åºåœæ­¢æ—¶æ— æ³•æ³¨é”€å®ƒã€‚ 为防止内存泄æ¼ï¼ŒJDBC驱动程åºå·²è¢«å¼ºåˆ¶å–消注册。 +webappClassLoader.clearObjectStreamClassCachesFail=无法清除web应用程åº[{0}]çš„ObjectStreamClass$缓存中的软引用 +webappClassLoader.clearRmi=找到具有存根类类[{0}]和值[{1}]çš„RMI目标。已强制移除此RMI目标,以防止内存泄æ¼ã€‚ +webappClassLoader.clearRmiFail=无法清除从web应用程åº[{0}]çš„sun.rmi.transport.Target引用的上下文类加载器 +webappClassLoader.clearRmiInfo=找ä¸åˆ°ç±»sun.rmi.transport.Target以清除web应用程åº[{0}]的上下文类加载器。这在éžSun jvm上是预期的。 +webappClassLoader.getThreadGroupError=无法获得线程组[{0}]的父级。ä¸å¯èƒ½æ£€æŸ¥æ‰€æœ‰çº¿ç¨‹æ˜¯å¦å­˜åœ¨æ½œåœ¨çš„内存泄æ¼ã€‚ +webappClassLoader.jarsAdded=一个或多个jar已添加到web应用程åº[{0}] +webappClassLoader.jarsModified=一个或多个jar已在web应用程åºä¸­ä¿®æ”¹[{0}] +webappClassLoader.jarsRemoved=一个或多个 JAR 已被从 Web åº”ç”¨ç¨‹åº [{0}] 中删除 +webappClassLoader.javaseClassLoaderNull=j2seClassLoader属性ä¸èƒ½ä¸ºç©º +webappClassLoader.jdbcRemoveFailed=Webåº”ç”¨ç¨‹åº [{0}] çš„JDBC驱动程åºæ³¨é”€å¤±è´¥ +webappClassLoader.loadedByThisOrChildFail=无法完全检查[{0}]实例中的æ¡ç›®ï¼Œçœ‹ä¸Šä¸‹æ–‡[{1}]中是å¦å­˜åœ¨æ½œåœ¨çš„å†…å­˜æ³„æ¼ +webappClassLoader.readError=资æºè¯»å–错误:ä¸èƒ½åŠ è½½ [{0}]. +webappClassLoader.removeTransformer=已从web应用程åº[{1}]中删除类文件转æ¢å™¨[{0}]。 +webappClassLoader.resourceModified=资æº[{0}]已被修改。上次修改时间是[{1}],现在是[{2}] +webappClassLoader.restrictedPackage=安全冲çªï¼Œå°è¯•ä½¿ç”¨å—é™ç±»[{0}] +webappClassLoader.securityException=å°è¯•åœ¨findClassInternal[{1}]中查找类[{0}]时出现安全异常 +webappClassLoader.stackTrace=Web应用程åº[{0}]似乎å¯åŠ¨äº†ä¸€ä¸ªå为[{1}]的线程,但未能åœæ­¢å®ƒã€‚这很å¯èƒ½ä¼šé€ æˆå†…存泄æ¼ã€‚线程的堆栈跟踪:[{2}] +webappClassLoader.stackTraceRequestThread=web应用程åº[{0}]ä»åœ¨å¤„ç†ä¸€ä¸ªå°šæœªå®Œæˆçš„请求。这很å¯èƒ½ä¼šé€ æˆå†…存泄æ¼ã€‚您å¯ä»¥ä½¿ç”¨æ ‡å‡†ä¸Šä¸‹æ–‡å®žçŽ°çš„unloadDelay属性æ¥æŽ§åˆ¶è¯·æ±‚完æˆæ‰€å…许的时间。请求处ç†çº¿ç¨‹çš„堆栈跟踪:[{2}] +webappClassLoader.stopThreadFail=为web应用程åº[{1}]终止线程[{0}]失败 +webappClassLoader.stopTimerThreadFail=无法终止å为[{0}]çš„TimerThread,web应用程åºï¼š[{1}] +webappClassLoader.stopped=éžæ³•è®¿é—®ï¼šæ­¤Web应用程åºå®žä¾‹å·²åœæ­¢ã€‚无法加载[{0}]。为了调试以åŠç»ˆæ­¢å¯¼è‡´éžæ³•è®¿é—®çš„线程,将抛出以下堆栈跟踪。 +webappClassLoader.superCloseFail=调用父类的close()方法出现异常。 +webappClassLoader.transformError=检测错误:无法转æ¢ç±»[{0}],因为它的类文件格å¼æ˜¯ä¸åˆæ³•çš„。 +webappClassLoader.warnTimerThread=Web应用程åº[{0}]似乎已通过java.util.Timer APIå¯åŠ¨äº†å为[{1}]çš„TimerThread,但未能将其åœæ­¢ã€‚ 为防止内存泄æ¼ï¼Œè®¡æ—¶å™¨ï¼ˆä»¥åŠç›¸å…³è”的线程)已被强制å–消。 +webappClassLoader.wrongVersion=(无法载入的类 [{0}]) + +webappClassLoaderParallel.registrationFailed=å°†org.apache.catalina.loader.ParallelWebappClassLoader注册为能够并行加载类失败 + +webappLoader.deploy=将类存储库部署到工作目录[{0}] +webappLoader.noResources=找ä¸åˆ°ä¸Šä¸‹æ–‡[{0}]çš„èµ„æº +webappLoader.reloadable=无法将å¯é‡è½½å±žæ€§è®¾ç½®ä¸º[{0}] +webappLoader.setContext.ise=当加载器å¯åŠ¨çš„时候设置上下文是ä¸è¢«å…许的 +webappLoader.startError=å¯åŠ¨åŠ è½½ç¨‹åºæ—¶å‡ºé”™ +webappLoader.starting=å¯åŠ¨æ­¤åŠ è½½ç¨‹åº +webappLoader.stopError=åœæ­¢åŠ è½½ç¨‹åºæ—¶å‡ºé”™ +webappLoader.stopping=åœæ­¢æ­¤åŠ è½½ç¨‹åº +webappLoader.unknownClassLoader=ç±»[{1}]的未知类加载器[{0}] +webappLoader.unknownProfile=指定了未知的Jakarta概述文件[{0}],将改用默认的概述文件 diff --git a/java/org/apache/catalina/loader/ParallelWebappClassLoader.java b/java/org/apache/catalina/loader/ParallelWebappClassLoader.java new file mode 100644 index 0000000..119b31f --- /dev/null +++ b/java/org/apache/catalina/loader/ParallelWebappClassLoader.java @@ -0,0 +1,73 @@ +/* + * 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.catalina.loader; + +import org.apache.catalina.LifecycleException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.compat.JreCompat; + +public class ParallelWebappClassLoader extends WebappClassLoaderBase { + + private static final Log log = LogFactory.getLog(ParallelWebappClassLoader.class); + + static { + if (!JreCompat.isGraalAvailable()) { + if (!registerAsParallelCapable()) { + log.warn(sm.getString("webappClassLoaderParallel.registrationFailed")); + } + } + } + + public ParallelWebappClassLoader() { + super(); + } + + + public ParallelWebappClassLoader(ClassLoader parent) { + super(parent); + } + + + /** + * Returns a copy of this class loader without any class file transformers. This is a tool often used by Java + * Persistence API providers to inspect entity classes in the absence of any instrumentation, something that can't + * be guaranteed within the context of a {@link java.lang.instrument.ClassFileTransformer}'s + * {@link java.lang.instrument.ClassFileTransformer#transform(ClassLoader, String, Class, java.security.ProtectionDomain, byte[]) + * transform} method. + *

    + * The returned class loader's resource cache will have been cleared so that classes already instrumented will not + * be retained or returned. + * + * @return the transformer-free copy of this class loader. + */ + @Override + public ParallelWebappClassLoader copyWithoutTransformers() { + + ParallelWebappClassLoader result = new ParallelWebappClassLoader(getParent()); + + super.copyStateWithoutTransformers(result); + + try { + result.start(); + } catch (LifecycleException e) { + throw new IllegalStateException(e); + } + + return result; + } +} diff --git a/java/org/apache/catalina/loader/ResourceEntry.java b/java/org/apache/catalina/loader/ResourceEntry.java new file mode 100644 index 0000000..b9eeff7 --- /dev/null +++ b/java/org/apache/catalina/loader/ResourceEntry.java @@ -0,0 +1,38 @@ +/* + * 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.catalina.loader; + +/** + * Resource entry. + * + * @author Remy Maucherat + */ +public class ResourceEntry { + + /** + * The "last modified" time of the origin file at the time this resource was loaded, in milliseconds since the + * epoch. + */ + public long lastModified = -1; + + + /** + * Loaded class. + */ + public volatile Class loadedClass = null; +} + diff --git a/java/org/apache/catalina/loader/WebappClassLoader.java b/java/org/apache/catalina/loader/WebappClassLoader.java new file mode 100644 index 0000000..58a84a2 --- /dev/null +++ b/java/org/apache/catalina/loader/WebappClassLoader.java @@ -0,0 +1,69 @@ +/* + * 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.catalina.loader; + +import org.apache.catalina.LifecycleException; + +public class WebappClassLoader extends WebappClassLoaderBase { + + public WebappClassLoader() { + super(); + } + + + public WebappClassLoader(ClassLoader parent) { + super(parent); + } + + + /** + * Returns a copy of this class loader without any class file transformers. This is a tool often used by Java + * Persistence API providers to inspect entity classes in the absence of any instrumentation, something that can't + * be guaranteed within the context of a {@link java.lang.instrument.ClassFileTransformer}'s + * {@link java.lang.instrument.ClassFileTransformer#transform(ClassLoader, String, Class, java.security.ProtectionDomain, byte[]) + * transform} method. + *

    + * The returned class loader's resource cache will have been cleared so that classes already instrumented will not + * be retained or returned. + * + * @return the transformer-free copy of this class loader. + */ + @Override + public WebappClassLoader copyWithoutTransformers() { + + WebappClassLoader result = new WebappClassLoader(getParent()); + + super.copyStateWithoutTransformers(result); + + try { + result.start(); + } catch (LifecycleException e) { + throw new IllegalStateException(e); + } + + return result; + } + + + /** + * This class loader is not parallel capable so lock on the class loader rather than a per-class lock. + */ + @Override + protected Object getClassLoadingLock(String className) { + return this; + } +} diff --git a/java/org/apache/catalina/loader/WebappClassLoaderBase.java b/java/org/apache/catalina/loader/WebappClassLoaderBase.java new file mode 100644 index 0000000..6e9e403 --- /dev/null +++ b/java/org/apache/catalina/loader/WebappClassLoaderBase.java @@ -0,0 +1,2658 @@ +/* + * 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.catalina.loader; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.ref.Reference; +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.AccessControlException; +import java.security.AccessController; +import java.security.CodeSource; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Policy; +import java.security.PrivilegedAction; +import java.security.ProtectionDomain; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.Manifest; + +import org.apache.catalina.Container; +import org.apache.catalina.Globals; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; +import org.apache.juli.WebappProperties; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstrumentableClassLoader; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.PermissionCheck; +import org.apache.tomcat.util.threads.ThreadPoolExecutor; + +/** + * Specialized web application class loader. + *

    + * This class loader is a full reimplementation of the URLClassLoader from the JDK. It is designed to be + * fully compatible with a normal URLClassLoader, although its internal behavior may be completely + * different. + *

    + * IMPLEMENTATION NOTE - By default, this class loader follows the delegation model required by the + * specification. The bootstrap class loader will be queried first, then the local repositories, and only then + * delegation to the parent class loader will occur. This allows the web application to override any shared class except + * the classes from J2SE. Special handling is provided from the JAXP XML parser interfaces, the JNDI interfaces, and the + * classes from the servlet API, which are never loaded from the webapp repositories. The delegate property + * allows an application to modify this behavior to move the parent class loader ahead of the local repositories. + *

    + * IMPLEMENTATION NOTE - Due to limitations in Jasper compilation technology, any repository which + * contains classes from the servlet API will be ignored by the class loader. + *

    + * IMPLEMENTATION NOTE - The class loader generates source URLs which include the full JAR URL when a + * class is loaded from a JAR file, which allows setting security permission at the class level, even when a class is + * contained inside a JAR. + *

    + * IMPLEMENTATION NOTE - Local repositories are searched in the order they are added via the initial + * constructor. + *

    + * IMPLEMENTATION NOTE - No check for sealing violations or security is made unless a security manager + * is present. + *

    + * IMPLEMENTATION NOTE - As of 8.0, this class loader implements {@link InstrumentableClassLoader}, + * permitting web application classes to instrument other classes in the same web application. It does not permit + * instrumentation of system or container classes or classes in other web apps. + * + * @author Remy Maucherat + * @author Craig R. McClanahan + */ +public abstract class WebappClassLoaderBase extends URLClassLoader + implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck { + + private static final Log log = LogFactory.getLog(WebappClassLoaderBase.class); + + /** + * List of ThreadGroup names to ignore when scanning for web application started threads that need to be shut down. + */ + private static final List JVM_THREAD_GROUP_NAMES = new ArrayList<>(); + + private static final String JVM_THREAD_GROUP_SYSTEM = "system"; + + private static final String CLASS_FILE_SUFFIX = ".class"; + + static { + if (!JreCompat.isGraalAvailable()) { + registerAsParallelCapable(); + } + JVM_THREAD_GROUP_NAMES.add(JVM_THREAD_GROUP_SYSTEM); + JVM_THREAD_GROUP_NAMES.add("RMI Runtime"); + } + + protected class PrivilegedFindClassByName implements PrivilegedAction> { + + private final String name; + + PrivilegedFindClassByName(String name) { + this.name = name; + } + + @Override + public Class run() { + return findClassInternal(name); + } + } + + + protected static final class PrivilegedGetClassLoader implements PrivilegedAction { + + private final Class clazz; + + public PrivilegedGetClassLoader(Class clazz) { + this.clazz = clazz; + } + + @Override + public ClassLoader run() { + return clazz.getClassLoader(); + } + } + + + protected final class PrivilegedJavaseGetResource implements PrivilegedAction { + + private final String name; + + public PrivilegedJavaseGetResource(String name) { + this.name = name; + } + + @Override + public URL run() { + return javaseClassLoader.getResource(name); + } + } + + + // ------------------------------------------------------- Static Variables + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(WebappClassLoaderBase.class); + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new ClassLoader with no defined repositories and no parent ClassLoader. + */ + protected WebappClassLoaderBase() { + + super(new URL[0]); + + ClassLoader p = getParent(); + if (p == null) { + p = getSystemClassLoader(); + } + this.parent = p; + + ClassLoader j = String.class.getClassLoader(); + if (j == null) { + j = getSystemClassLoader(); + while (j.getParent() != null) { + j = j.getParent(); + } + } + this.javaseClassLoader = j; + + securityManager = System.getSecurityManager(); + if (securityManager != null) { + refreshPolicy(); + } + } + + + /** + * Construct a new ClassLoader with no defined repositories and the given parent ClassLoader. + *

    + * Method is used via reflection - see {@link WebappLoader#createClassLoader()} + * + * @param parent Our parent class loader + */ + protected WebappClassLoaderBase(ClassLoader parent) { + + super(new URL[0], parent); + + ClassLoader p = getParent(); + if (p == null) { + p = getSystemClassLoader(); + } + this.parent = p; + + ClassLoader j = String.class.getClassLoader(); + if (j == null) { + j = getSystemClassLoader(); + while (j.getParent() != null) { + j = j.getParent(); + } + } + this.javaseClassLoader = j; + + securityManager = System.getSecurityManager(); + if (securityManager != null) { + refreshPolicy(); + } + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Associated web resources for this webapp. + */ + protected WebResourceRoot resources = null; + + + /** + * The cache of ResourceEntry for classes and resources we have loaded, keyed by resource path, not binary name. + * Path is used as the key since resources may be requested by binary name (classes) or path (other resources such + * as property files) and the mapping from binary name to path is unambiguous but the reverse mapping is ambiguous. + */ + protected final Map resourceEntries = new ConcurrentHashMap<>(); + + + /** + * Should this class loader delegate to the parent class loader before searching its own + * repositories (i.e. the usual Java2 delegation model)? If set to false, this class loader will search + * its own repositories first, and delegate to the parent only if the class or resource is not found locally. Note + * that the default, false, is the behavior called for by the servlet specification. + */ + protected boolean delegate = false; + + + private final Map jarModificationTimes = new HashMap<>(); + + + /** + * A list of read File Permission's required if this loader is for a web application context. + */ + protected final ArrayList permissionList = new ArrayList<>(); + + + /** + * The PermissionCollection for each CodeSource for a web application context. + */ + protected final HashMap loaderPC = new HashMap<>(); + + + /** + * Instance of the SecurityManager installed. + */ + protected final SecurityManager securityManager; + + + /** + * The parent class loader. + */ + protected final ClassLoader parent; + + + /** + * The bootstrap class loader used to load the JavaSE classes. In some implementations this class loader is always + * null and in those cases {@link ClassLoader#getParent()} will be called recursively on the system + * class loader and the last non-null result used. + */ + private ClassLoader javaseClassLoader; + + + /** + * Enables the RMI Target memory leak detection to be controlled. This is necessary since the detection can only + * work if some of the modularity checks are disabled. + */ + private boolean clearReferencesRmiTargets = true; + + /** + * Should Tomcat attempt to terminate threads that have been started by the web application? Stopping threads is + * performed via the deprecated (for good reason) Thread.stop() method and is likely to result in + * instability. As such, enabling this should be viewed as an option of last resort in a development environment and + * is not recommended in a production environment. If not specified, the default value of false will be + * used. + */ + private boolean clearReferencesStopThreads = false; + + /** + * Should Tomcat attempt to terminate any {@link java.util.TimerThread}s that have been started by the web + * application? If not specified, the default value of false will be used. + */ + private boolean clearReferencesStopTimerThreads = false; + + /** + * Should Tomcat call {@link org.apache.juli.logging.LogFactory#release(ClassLoader)} when the class loader is + * stopped? If not specified, the default value of true is used. Changing the default setting is likely + * to lead to memory leaks and other issues. + */ + private boolean clearReferencesLogFactoryRelease = true; + + /** + * If an HttpClient keep-alive timer thread has been started by this web application and is still running, should + * Tomcat change the context class loader from the current {@link ClassLoader} to {@link ClassLoader#getParent()} to + * prevent a memory leak? Note that the keep-alive timer thread will stop on its own once the keep-alives all expire + * however, on a busy system that might not happen for some time. + */ + private boolean clearReferencesHttpClientKeepAliveThread = true; + + /** + * Should Tomcat attempt to clear references to classes loaded by this class loader from the ObjectStreamClass + * caches? + */ + private boolean clearReferencesObjectStreamClassCaches = true; + + /** + * Should Tomcat attempt to clear references to classes loaded by this class loader from ThreadLocals? + */ + private boolean clearReferencesThreadLocals = true; + + /** + * Should Tomcat skip the memory leak checks when the web application is stopped as part of the process of shutting + * down the JVM? + */ + private boolean skipMemoryLeakChecksOnJvmShutdown = false; + + /** + * Holds the class file transformers decorating this class loader. The CopyOnWriteArrayList is thread safe. It is + * expensive on writes, but those should be rare. It is very fast on reads, since synchronization is not actually + * used. Importantly, the ClassLoader will never block iterating over the transformers while loading a class. + */ + private final List transformers = new CopyOnWriteArrayList<>(); + + + /** + * Flag that indicates that {@link #addURL(URL)} has been called which creates a requirement to check the super + * class when searching for resources. + */ + private boolean hasExternalRepositories = false; + + + /** + * Repositories managed by this class rather than the super class. + */ + private List localRepositories = new ArrayList<>(); + + + private volatile LifecycleState state = LifecycleState.NEW; + + + // ------------------------------------------------------------- Properties + + /** + * Set associated resources. + * + * @param resources the resources from which the classloader will load the classes + */ + public void setResources(WebResourceRoot resources) { + this.resources = resources; + } + + + /** + * @return the context name for this class loader. + */ + public String getContextName() { + if (resources == null) { + return "Unknown"; + } else { + return resources.getContext().getBaseName(); + } + } + + + /** + * Return the "delegate first" flag for this class loader. + * + * @return true if the class lookup will delegate to the parent first. The default in Tomcat is + * false. + */ + public boolean getDelegate() { + return this.delegate; + } + + + /** + * Set the "delegate first" flag for this class loader. If this flag is true, this class loader delegates to the + * parent class loader before searching its own repositories, as in an ordinary (non-servlet) chain + * of Java class loaders. If set to false (the default), this class loader will search its own + * repositories first, and delegate to the parent only if the class or resource is not found locally, as per the + * servlet specification. + * + * @param delegate The new "delegate first" flag + */ + public void setDelegate(boolean delegate) { + this.delegate = delegate; + } + + + /** + * If there is a Java SecurityManager create a read permission for the target of the given URL as appropriate. + * + * @param url URL for a file or directory on local system + */ + void addPermission(URL url) { + if (url == null) { + return; + } + if (securityManager != null) { + String protocol = url.getProtocol(); + if ("file".equalsIgnoreCase(protocol)) { + URI uri; + File f; + String path; + try { + uri = url.toURI(); + f = new File(uri); + path = f.getCanonicalPath(); + } catch (IOException | URISyntaxException e) { + log.warn(sm.getString("webappClassLoader.addPermissionNoCanonicalFile", url.toExternalForm())); + return; + } + if (f.isFile()) { + // Allow the file to be read + addPermission(new FilePermission(path, "read")); + } else if (f.isDirectory()) { + addPermission(new FilePermission(path, "read")); + addPermission(new FilePermission(path + File.separator + "-", "read")); + } else { + // File does not exist - ignore (shouldn't happen) + } + } else { + // Unsupported URL protocol + log.warn(sm.getString("webappClassLoader.addPermissionNoProtocol", protocol, url.toExternalForm())); + } + } + } + + + /** + * If there is a Java SecurityManager create a Permission. + * + * @param permission The permission + */ + void addPermission(Permission permission) { + if (securityManager != null && permission != null) { + permissionList.add(permission); + } + } + + + public boolean getClearReferencesRmiTargets() { + return this.clearReferencesRmiTargets; + } + + + public void setClearReferencesRmiTargets(boolean clearReferencesRmiTargets) { + this.clearReferencesRmiTargets = clearReferencesRmiTargets; + } + + + /** + * @return the clearReferencesStopThreads flag for this Context. + */ + public boolean getClearReferencesStopThreads() { + return this.clearReferencesStopThreads; + } + + + /** + * Set the clearReferencesStopThreads feature for this Context. + * + * @param clearReferencesStopThreads The new flag value + */ + public void setClearReferencesStopThreads(boolean clearReferencesStopThreads) { + this.clearReferencesStopThreads = clearReferencesStopThreads; + } + + + /** + * @return the clearReferencesStopTimerThreads flag for this Context. + */ + public boolean getClearReferencesStopTimerThreads() { + return this.clearReferencesStopTimerThreads; + } + + + /** + * Set the clearReferencesStopTimerThreads feature for this Context. + * + * @param clearReferencesStopTimerThreads The new flag value + */ + public void setClearReferencesStopTimerThreads(boolean clearReferencesStopTimerThreads) { + this.clearReferencesStopTimerThreads = clearReferencesStopTimerThreads; + } + + + /** + * @return the clearReferencesLogFactoryRelease flag for this Context. + */ + public boolean getClearReferencesLogFactoryRelease() { + return this.clearReferencesLogFactoryRelease; + } + + + /** + * Set the clearReferencesLogFactoryRelease feature for this Context. + * + * @param clearReferencesLogFactoryRelease The new flag value + */ + public void setClearReferencesLogFactoryRelease(boolean clearReferencesLogFactoryRelease) { + this.clearReferencesLogFactoryRelease = clearReferencesLogFactoryRelease; + } + + + /** + * @return the clearReferencesHttpClientKeepAliveThread flag for this Context. + */ + public boolean getClearReferencesHttpClientKeepAliveThread() { + return this.clearReferencesHttpClientKeepAliveThread; + } + + + /** + * Set the clearReferencesHttpClientKeepAliveThread feature for this Context. + * + * @param clearReferencesHttpClientKeepAliveThread The new flag value + */ + public void setClearReferencesHttpClientKeepAliveThread(boolean clearReferencesHttpClientKeepAliveThread) { + this.clearReferencesHttpClientKeepAliveThread = clearReferencesHttpClientKeepAliveThread; + } + + + public boolean getClearReferencesObjectStreamClassCaches() { + return clearReferencesObjectStreamClassCaches; + } + + + public void setClearReferencesObjectStreamClassCaches(boolean clearReferencesObjectStreamClassCaches) { + this.clearReferencesObjectStreamClassCaches = clearReferencesObjectStreamClassCaches; + } + + + public boolean getClearReferencesThreadLocals() { + return clearReferencesThreadLocals; + } + + + public void setClearReferencesThreadLocals(boolean clearReferencesThreadLocals) { + this.clearReferencesThreadLocals = clearReferencesThreadLocals; + } + + + public boolean getSkipMemoryLeakChecksOnJvmShutdown() { + return skipMemoryLeakChecksOnJvmShutdown; + } + + + public void setSkipMemoryLeakChecksOnJvmShutdown(boolean skipMemoryLeakChecksOnJvmShutdown) { + this.skipMemoryLeakChecksOnJvmShutdown = skipMemoryLeakChecksOnJvmShutdown; + } + + + // ------------------------------------------------------- Reloader Methods + + /** + * Adds the specified class file transformer to this class loader. The transformer will then be able to modify the + * bytecode of any classes loaded by this class loader after the invocation of this method. + * + * @param transformer The transformer to add to the class loader + */ + @Override + public void addTransformer(ClassFileTransformer transformer) { + + if (transformer == null) { + throw new IllegalArgumentException( + sm.getString("webappClassLoader.addTransformer.illegalArgument", getContextName())); + } + + if (this.transformers.contains(transformer)) { + // if the same instance of this transformer was already added, bail out + log.warn(sm.getString("webappClassLoader.addTransformer.duplicate", transformer, getContextName())); + return; + } + this.transformers.add(transformer); + + log.info(sm.getString("webappClassLoader.addTransformer", transformer, getContextName())); + } + + /** + * Removes the specified class file transformer from this class loader. It will no longer be able to modify the byte + * code of any classes loaded by the class loader after the invocation of this method. However, any classes already + * modified by this transformer will remain transformed. + * + * @param transformer The transformer to remove + */ + @Override + public void removeTransformer(ClassFileTransformer transformer) { + + if (transformer == null) { + return; + } + + if (this.transformers.remove(transformer)) { + log.info(sm.getString("webappClassLoader.removeTransformer", transformer, getContextName())); + } + } + + protected void copyStateWithoutTransformers(WebappClassLoaderBase base) { + base.resources = this.resources; + base.delegate = this.delegate; + base.state = LifecycleState.NEW; + base.clearReferencesStopThreads = this.clearReferencesStopThreads; + base.clearReferencesStopTimerThreads = this.clearReferencesStopTimerThreads; + base.clearReferencesLogFactoryRelease = this.clearReferencesLogFactoryRelease; + base.clearReferencesHttpClientKeepAliveThread = this.clearReferencesHttpClientKeepAliveThread; + base.jarModificationTimes.putAll(this.jarModificationTimes); + base.permissionList.addAll(this.permissionList); + base.loaderPC.putAll(this.loaderPC); + } + + /** + * Have one or more classes or resources been modified so that a reload is appropriate? + * + * @return true if there's been a modification + */ + public boolean modified() { + + if (log.isTraceEnabled()) { + log.trace("modified()"); + } + + for (Entry entry : resourceEntries.entrySet()) { + long cachedLastModified = entry.getValue().lastModified; + long lastModified = resources.getClassLoaderResource(entry.getKey()).getLastModified(); + if (lastModified != cachedLastModified) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("webappClassLoader.resourceModified", entry.getKey(), + new Date(cachedLastModified), new Date(lastModified))); + } + return true; + } + } + + // Check if JARs have been added or removed + WebResource[] jars = resources.listResources("/WEB-INF/lib"); + // Filter out non-JAR resources + + int jarCount = 0; + for (WebResource jar : jars) { + if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) { + jarCount++; + Long recordedLastModified = jarModificationTimes.get(jar.getName()); + if (recordedLastModified == null) { + // Jar has been added + log.info(sm.getString("webappClassLoader.jarsAdded", resources.getContext().getName())); + return true; + } + if (recordedLastModified.longValue() != jar.getLastModified()) { + // Jar has been changed + log.info(sm.getString("webappClassLoader.jarsModified", resources.getContext().getName())); + return true; + } + } + } + + if (jarCount < jarModificationTimes.size()) { + log.info(sm.getString("webappClassLoader.jarsRemoved", resources.getContext().getName())); + return true; + } + + + // No classes have been modified + return false; + } + + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()); + sb.append("\r\n context: "); + sb.append(getContextName()); + sb.append("\r\n delegate: "); + sb.append(delegate); + sb.append("\r\n"); + if (this.parent != null) { + sb.append("----------> Parent Classloader:\r\n"); + sb.append(this.parent.toString()); + sb.append("\r\n"); + } + if (this.transformers.size() > 0) { + sb.append("----------> Class file transformers:\r\n"); + for (ClassFileTransformer transformer : this.transformers) { + sb.append(transformer).append("\r\n"); + } + } + return sb.toString(); + } + + + // ---------------------------------------------------- ClassLoader Methods + + + // Note: exposed for use by tests + protected final Class doDefineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) { + return super.defineClass(name, b, off, len, protectionDomain); + } + + /** + * Find the specified class in our local repositories, if possible. If not found, throw + * ClassNotFoundException. + * + * @param name The binary name of the class to be loaded + * + * @exception ClassNotFoundException if the class was not found + */ + @Override + public Class findClass(String name) throws ClassNotFoundException { + + if (log.isTraceEnabled()) { + log.trace(" findClass(" + name + ")"); + } + + checkStateForClassLoading(name); + + // (1) Permission to define this class when using a SecurityManager + if (securityManager != null) { + int i = name.lastIndexOf('.'); + if (i >= 0) { + try { + if (log.isTraceEnabled()) { + log.trace(" securityManager.checkPackageDefinition"); + } + securityManager.checkPackageDefinition(name.substring(0, i)); + } catch (Exception se) { + if (log.isTraceEnabled()) { + log.trace(" -->Exception-->ClassNotFoundException", se); + } + throw new ClassNotFoundException(name, se); + } + } + } + + // Ask our superclass to locate this class, if possible + // (throws ClassNotFoundException if it is not found) + Class clazz = null; + try { + if (log.isTraceEnabled()) { + log.trace(" findClassInternal(" + name + ")"); + } + try { + if (securityManager != null) { + PrivilegedAction> dp = new PrivilegedFindClassByName(name); + clazz = AccessController.doPrivileged(dp); + } else { + clazz = findClassInternal(name); + } + } catch (AccessControlException ace) { + log.warn(sm.getString("webappClassLoader.securityException", name, ace.getMessage()), ace); + throw new ClassNotFoundException(name, ace); + } catch (RuntimeException e) { + if (log.isTraceEnabled()) { + log.trace(" -->RuntimeException Rethrown", e); + } + throw e; + } + if (clazz == null && hasExternalRepositories) { + try { + clazz = super.findClass(name); + } catch (AccessControlException ace) { + log.warn(sm.getString("webappClassLoader.securityException", name, ace.getMessage()), ace); + throw new ClassNotFoundException(name, ace); + } catch (RuntimeException e) { + if (log.isTraceEnabled()) { + log.trace(" -->RuntimeException Rethrown", e); + } + throw e; + } + } + if (clazz == null) { + if (log.isTraceEnabled()) { + log.trace(" --> Returning ClassNotFoundException"); + } + throw new ClassNotFoundException(name); + } + } catch (ClassNotFoundException e) { + if (log.isTraceEnabled()) { + log.trace(" --> Passing on ClassNotFoundException"); + } + throw e; + } + + // Return the class we have located + if (log.isTraceEnabled()) { + log.trace(" Returning class " + clazz); + } + + if (log.isTraceEnabled()) { + ClassLoader cl; + if (Globals.IS_SECURITY_ENABLED) { + cl = AccessController.doPrivileged(new PrivilegedGetClassLoader(clazz)); + } else { + cl = clazz.getClassLoader(); + } + log.trace(" Loaded by " + cl.toString()); + } + return clazz; + + } + + + /** + * Find the specified resource in our local repository, and return a URL referring to it, or + * null if this resource cannot be found. + * + * @param name Name of the resource to be found + */ + @Override + public URL findResource(final String name) { + + if (log.isTraceEnabled()) { + log.trace(" findResource(" + name + ")"); + } + + checkStateForResourceLoading(name); + + URL url = null; + + String path = nameToPath(name); + + WebResource resource = resources.getClassLoaderResource(path); + if (resource.exists()) { + url = resource.getURL(); + trackLastModified(path, resource); + } + + if (url == null && hasExternalRepositories) { + url = super.findResource(name); + } + + if (log.isTraceEnabled()) { + if (url != null) { + log.trace(" --> Returning '" + url.toString() + "'"); + } else { + log.trace(" --> Resource not found, returning null"); + } + } + return url; + } + + + private void trackLastModified(String path, WebResource resource) { + if (resourceEntries.containsKey(path)) { + return; + } + ResourceEntry entry = new ResourceEntry(); + entry.lastModified = resource.getLastModified(); + synchronized (resourceEntries) { + resourceEntries.putIfAbsent(path, entry); + } + } + + + /** + * Return an enumeration of URLs representing all of the resources with the given name. If no resources + * with this name are found, return an empty enumeration. + * + * @param name Name of the resources to be found + * + * @exception IOException if an input/output error occurs + */ + @Override + public Enumeration findResources(String name) throws IOException { + + if (log.isTraceEnabled()) { + log.trace(" findResources(" + name + ")"); + } + + checkStateForResourceLoading(name); + + LinkedHashSet result = new LinkedHashSet<>(); + + String path = nameToPath(name); + + WebResource[] webResources = resources.getClassLoaderResources(path); + for (WebResource webResource : webResources) { + if (webResource.exists()) { + result.add(webResource.getURL()); + } + } + + // Adding the results of a call to the superclass + if (hasExternalRepositories) { + Enumeration otherResourcePaths = super.findResources(name); + while (otherResourcePaths.hasMoreElements()) { + result.add(otherResourcePaths.nextElement()); + } + } + + return Collections.enumeration(result); + } + + + /** + * Find the resource with the given name. A resource is some data (images, audio, text, etc.) that can be accessed + * by class code in a way that is independent of the location of the code. The name of a resource is a "/"-separated + * path name that identifies the resource. If the resource cannot be found, return null. + *

    + * This method searches according to the following algorithm, returning as soon as it finds the appropriate URL. If + * the resource cannot be found, returns null. + *

      + *
    • If the delegate property is set to true, call the getResource() method + * of the parent class loader, if any.
    • + *
    • Call findResource() to find this resource in our locally defined repositories.
    • + *
    • Call the getResource() method of the parent class loader, if any.
    • + *
    + * + * @param name Name of the resource to return a URL for + */ + @Override + public URL getResource(String name) { + + if (log.isTraceEnabled()) { + log.trace("getResource(" + name + ")"); + } + + checkStateForResourceLoading(name); + + URL url = null; + + boolean delegateFirst = delegate || filter(name, false); + + // (1) Delegate to parent if requested + if (delegateFirst) { + if (log.isTraceEnabled()) { + log.trace(" Delegating to parent classloader " + parent); + } + url = parent.getResource(name); + if (url != null) { + if (log.isTraceEnabled()) { + log.trace(" --> Returning '" + url.toString() + "'"); + } + return url; + } + } + + // (2) Search local repositories + url = findResource(name); + if (url != null) { + if (log.isTraceEnabled()) { + log.trace(" --> Returning '" + url.toString() + "'"); + } + return url; + } + + // (3) Delegate to parent unconditionally if not already attempted + if (!delegateFirst) { + url = parent.getResource(name); + if (url != null) { + if (log.isTraceEnabled()) { + log.trace(" --> Returning '" + url.toString() + "'"); + } + return url; + } + } + + // (4) Resource was not found + if (log.isTraceEnabled()) { + log.trace(" --> Resource not found, returning null"); + } + return null; + + } + + + @Override + public Enumeration getResources(String name) throws IOException { + + Enumeration parentResources = parent.getResources(name); + Enumeration localResources = findResources(name); + + // Need to combine these enumerations. The order in which the + // Enumerations are combined depends on how delegation is configured + boolean delegateFirst = delegate || filter(name, false); + + if (delegateFirst) { + return new CombinedEnumeration(parentResources, localResources); + } else { + return new CombinedEnumeration(localResources, parentResources); + } + } + + + /** + * Find the resource with the given name, and return an input stream that can be used for reading it. The search + * order is as described for getResource(), after checking to see if the resource data has been + * previously cached. If the resource cannot be found, return null. + * + * @param name Name of the resource to return an input stream for + */ + @Override + public InputStream getResourceAsStream(String name) { + + if (log.isTraceEnabled()) { + log.trace("getResourceAsStream(" + name + ")"); + } + + checkStateForResourceLoading(name); + + InputStream stream = null; + + boolean delegateFirst = delegate || filter(name, false); + + // (1) Delegate to parent if requested + if (delegateFirst) { + if (log.isTraceEnabled()) { + log.trace(" Delegating to parent classloader " + parent); + } + stream = parent.getResourceAsStream(name); + if (stream != null) { + if (log.isTraceEnabled()) { + log.trace(" --> Returning stream from parent"); + } + return stream; + } + } + + // (2) Search local repositories + if (log.isTraceEnabled()) { + log.trace(" Searching local repositories"); + } + String path = nameToPath(name); + WebResource resource = resources.getClassLoaderResource(path); + if (resource.exists()) { + stream = resource.getInputStream(); + // Filter out .class resources through the ClassFileTranformer + if (name.endsWith(CLASS_FILE_SUFFIX) && transformers.size() > 0) { + // If the resource is a class, decorate it with any attached transformers + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int numRead; + try { + while ((numRead = stream.read(buf)) >= 0) { + baos.write(buf, 0, numRead); + } + } catch (IOException e) { + log.error(sm.getString("webappClassLoader.transformError", name), e); + return null; + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + byte[] binaryContent = baos.toByteArray(); + String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length()); + for (ClassFileTransformer transformer : this.transformers) { + try { + byte[] transformed = transformer.transform(this, internalName, null, null, binaryContent); + if (transformed != null) { + binaryContent = transformed; + } + } catch (IllegalClassFormatException e) { + log.error(sm.getString("webappClassLoader.transformError", name), e); + return null; + } + } + stream = new ByteArrayInputStream(binaryContent); + } + trackLastModified(path, resource); + } + try { + if (hasExternalRepositories && stream == null) { + URL url = super.findResource(name); + if (url != null) { + stream = url.openStream(); + } + } + } catch (IOException e) { + // Ignore + } + if (stream != null) { + if (log.isTraceEnabled()) { + log.trace(" --> Returning stream from local"); + } + return stream; + } + + // (3) Delegate to parent unconditionally + if (!delegateFirst) { + if (log.isTraceEnabled()) { + log.trace(" Delegating to parent classloader unconditionally " + parent); + } + stream = parent.getResourceAsStream(name); + if (stream != null) { + if (log.isTraceEnabled()) { + log.trace(" --> Returning stream from parent"); + } + return stream; + } + } + + // (4) Resource was not found + if (log.isTraceEnabled()) { + log.trace(" --> Resource not found, returning null"); + } + return null; + } + + + /** + * Load the class with the specified name. This method searches for classes in the same manner as + * loadClass(String, boolean) with false as the second argument. + * + * @param name The binary name of the class to be loaded + * + * @exception ClassNotFoundException if the class was not found + */ + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return loadClass(name, false); + } + + + /** + * Load the class with the specified name, searching using the following algorithm until it finds and returns the + * class. If the class cannot be found, returns ClassNotFoundException. + *
      + *
    • Call findLoadedClass(String) to check if the class has already been loaded. If it has, the same + * Class object is returned.
    • + *
    • If the delegate property is set to true, call the loadClass() method + * of the parent class loader, if any.
    • + *
    • Call findClass() to find this class in our locally defined repositories.
    • + *
    • Call the loadClass() method of our parent class loader, if any.
    • + *
    + * If the class was found using the above steps, and the resolve flag is true, this method + * will then call resolveClass(Class) on the resulting Class object. + * + * @param name The binary name of the class to be loaded + * @param resolve If true then resolve the class + * + * @exception ClassNotFoundException if the class was not found + */ + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + + synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) { + if (log.isTraceEnabled()) { + log.trace("loadClass(" + name + ", " + resolve + ")"); + } + Class clazz = null; + + // Log access to stopped class loader + checkStateForClassLoading(name); + + // (0) Check our previously loaded local class cache + clazz = findLoadedClass0(name); + if (clazz != null) { + if (log.isTraceEnabled()) { + log.trace(" Returning class from cache"); + } + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + + // (0.1) Check our previously loaded class cache + clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name); + if (clazz != null) { + if (log.isTraceEnabled()) { + log.trace(" Returning class from cache"); + } + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + + /* + * (0.2) Try loading the class with the bootstrap class loader, to prevent the webapp from overriding Java + * SE classes. This implements SRV.10.7.2 + */ + String resourceName = binaryNameToPath(name, false); + + ClassLoader javaseLoader = getJavaseClassLoader(); + boolean tryLoadingFromJavaseLoader; + try { + /* + * Use getResource as it won't trigger an expensive ClassNotFoundException if the resource is not + * available from the Java SE class loader. However (see + * https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for details) when running under a security + * manager in rare cases this call may trigger a ClassCircularityError. + * + * See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for details of how this may trigger a + * StackOverflowError. + * + * Given these reported errors, catch Throwable to ensure all edge cases are caught. + */ + URL url; + if (securityManager != null) { + PrivilegedAction dp = new PrivilegedJavaseGetResource(resourceName); + url = AccessController.doPrivileged(dp); + } else { + url = javaseLoader.getResource(resourceName); + } + tryLoadingFromJavaseLoader = url != null; + } catch (Throwable t) { + // Swallow all exceptions apart from those that must be re-thrown + ExceptionUtils.handleThrowable(t); + // The getResource() trick won't work for this class. We have to + // try loading it directly and accept that we might get a + // ClassNotFoundException. + tryLoadingFromJavaseLoader = true; + } + + if (tryLoadingFromJavaseLoader) { + try { + clazz = javaseLoader.loadClass(name); + if (clazz != null) { + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + + // (0.5) Permission to access this class when using a SecurityManager + if (securityManager != null) { + int i = name.lastIndexOf('.'); + if (i >= 0) { + try { + securityManager.checkPackageAccess(name.substring(0, i)); + } catch (SecurityException se) { + String error = sm.getString("webappClassLoader.restrictedPackage", name); + log.info(error, se); + throw new ClassNotFoundException(error, se); + } + } + } + + boolean delegateLoad = delegate || filter(name, true); + + // (1) Delegate to our parent if requested + if (delegateLoad) { + if (log.isTraceEnabled()) { + log.trace(" Delegating to parent classloader1 " + parent); + } + try { + clazz = Class.forName(name, false, parent); + if (clazz != null) { + if (log.isTraceEnabled()) { + log.trace(" Loading class from parent"); + } + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + + // (2) Search local repositories + if (log.isTraceEnabled()) { + log.trace(" Searching local repositories"); + } + try { + clazz = findClass(name); + if (clazz != null) { + if (log.isTraceEnabled()) { + log.trace(" Loading class from local repository"); + } + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + + // (3) Delegate to parent unconditionally + if (!delegateLoad) { + if (log.isTraceEnabled()) { + log.trace(" Delegating to parent classloader at end: " + parent); + } + try { + clazz = Class.forName(name, false, parent); + if (clazz != null) { + if (log.isTraceEnabled()) { + log.trace(" Loading class from parent"); + } + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + } + + throw new ClassNotFoundException(name); + } + + + protected void checkStateForClassLoading(String className) throws ClassNotFoundException { + // It is not permitted to load new classes once the web application has + // been stopped. + try { + checkStateForResourceLoading(className); + } catch (IllegalStateException ise) { + throw new ClassNotFoundException(ise.getMessage(), ise); + } + } + + + protected void checkStateForResourceLoading(String resource) throws IllegalStateException { + // It is not permitted to load resources once the web application has + // been stopped. + if (!state.isAvailable()) { + String msg = sm.getString("webappClassLoader.stopped", resource); + IllegalStateException ise = new IllegalStateException(msg); + log.info(msg, ise); + throw ise; + } + } + + /** + * Get the Permissions for a CodeSource. If this instance of WebappClassLoaderBase is for a web application context, + * add read FilePermission for the appropriate resources. + * + * @param codeSource where the code was loaded from + * + * @return PermissionCollection for CodeSource + */ + @Override + protected PermissionCollection getPermissions(CodeSource codeSource) { + String codeUrl = codeSource.getLocation().toString(); + PermissionCollection pc; + if ((pc = loaderPC.get(codeUrl)) == null) { + pc = super.getPermissions(codeSource); + if (pc != null) { + for (Permission p : permissionList) { + pc.add(p); + } + loaderPC.put(codeUrl, pc); + } + } + return pc; + } + + + @Override + public boolean check(Permission permission) { + if (!Globals.IS_SECURITY_ENABLED) { + return true; + } + Policy currentPolicy = Policy.getPolicy(); + if (currentPolicy != null) { + URL contextRootUrl = resources.getResource("/").getCodeBase(); + CodeSource cs = new CodeSource(contextRootUrl, (Certificate[]) null); + PermissionCollection pc = currentPolicy.getPermissions(cs); + if (pc.implies(permission)) { + return true; + } + } + return false; + } + + + /** + * {@inheritDoc} + *

    + * Note that list of URLs returned by this method may not be complete. The web application class loader accesses + * class loader resources via the {@link WebResourceRoot} which supports the arbitrary mapping of additional files, + * directories and contents of JAR files under WEB-INF/classes. Any such resources will not be included in the URLs + * returned here. + */ + @Override + public URL[] getURLs() { + ArrayList result = new ArrayList<>(); + result.addAll(localRepositories); + result.addAll(Arrays.asList(super.getURLs())); + return result.toArray(new URL[0]); + } + + + // ------------------------------------------------------ Lifecycle Methods + + + /** + * Add a lifecycle event listener to this component. + * + * @param listener The listener to add + */ + @Override + public void addLifecycleListener(LifecycleListener listener) { + // NOOP + } + + + /** + * Get the lifecycle listeners associated with this lifecycle. If this Lifecycle has no listeners registered, a + * zero-length array is returned. + */ + @Override + public LifecycleListener[] findLifecycleListeners() { + return new LifecycleListener[0]; + } + + + /** + * Remove a lifecycle event listener from this component. + * + * @param listener The listener to remove + */ + @Override + public void removeLifecycleListener(LifecycleListener listener) { + // NOOP + } + + + /** + * Obtain the current state of the source component. + * + * @return The current state of the source component. + */ + @Override + public LifecycleState getState() { + return state; + } + + + @Override + public String getStateName() { + return getState().toString(); + } + + + @Override + public void init() { + state = LifecycleState.INITIALIZED; + } + + + /** + * Start the class loader. + * + * @exception LifecycleException if a lifecycle error occurs + */ + @Override + public void start() throws LifecycleException { + + state = LifecycleState.STARTING_PREP; + + WebResource[] classesResources = resources.getResources("/WEB-INF/classes"); + for (WebResource classes : classesResources) { + if (classes.isDirectory() && classes.canRead()) { + localRepositories.add(classes.getURL()); + } + } + WebResource[] jars = resources.listResources("/WEB-INF/lib"); + for (WebResource jar : jars) { + if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) { + localRepositories.add(jar.getURL()); + jarModificationTimes.put(jar.getName(), Long.valueOf(jar.getLastModified())); + } + } + + state = LifecycleState.STARTED; + } + + + /** + * Stop the class loader. + * + * @exception LifecycleException if a lifecycle error occurs + */ + @Override + public void stop() throws LifecycleException { + + state = LifecycleState.STOPPING_PREP; + + // Clearing references should be done before setting started to + // false, due to possible side effects + clearReferences(); + + state = LifecycleState.STOPPING; + + resourceEntries.clear(); + jarModificationTimes.clear(); + resources = null; + + permissionList.clear(); + loaderPC.clear(); + + state = LifecycleState.STOPPED; + } + + + @Override + public void destroy() { + state = LifecycleState.DESTROYING; + + try { + super.close(); + } catch (IOException ioe) { + log.warn(sm.getString("webappClassLoader.superCloseFail"), ioe); + } + state = LifecycleState.DESTROYED; + } + + + // ------------------------------------------------------ Protected Methods + + protected ClassLoader getJavaseClassLoader() { + return javaseClassLoader; + } + + protected void setJavaseClassLoader(ClassLoader classLoader) { + if (classLoader == null) { + throw new IllegalArgumentException(sm.getString("webappClassLoader.javaseClassLoaderNull")); + } + javaseClassLoader = classLoader; + } + + /** + * Clear references. + */ + protected void clearReferences() { + + // If the JVM is shutting down, skip the memory leak checks + if (skipMemoryLeakChecksOnJvmShutdown && !resources.getContext().getParent().getState().isAvailable()) { + // During reloading / redeployment the parent is expected to be + // available. Parent is not available so this might be a JVM + // shutdown. + try { + Thread dummyHook = new Thread(); + Runtime.getRuntime().addShutdownHook(dummyHook); + Runtime.getRuntime().removeShutdownHook(dummyHook); + } catch (IllegalStateException ise) { + return; + } + } + + if (!JreCompat.isGraalAvailable()) { + // De-register any remaining JDBC drivers + clearReferencesJdbc(); + } + + // Stop any threads the web application started + clearReferencesThreads(); + + // Clear any references retained in the serialization caches + if (clearReferencesObjectStreamClassCaches && !JreCompat.isGraalAvailable()) { + clearReferencesObjectStreamClassCaches(); + } + + // Check for leaks triggered by ThreadLocals loaded by this class loader + if (clearReferencesThreadLocals && !JreCompat.isGraalAvailable()) { + checkThreadLocalsForLeaks(); + } + + // Clear RMI Targets loaded by this class loader + if (clearReferencesRmiTargets) { + clearReferencesRmiTargets(); + } + + // Clear the IntrospectionUtils cache. + IntrospectionUtils.clear(); + + // Clear the classloader reference in common-logging + if (clearReferencesLogFactoryRelease) { + LogFactory.release(this); + } + + // Clear the classloader reference in the VM's bean introspector + java.beans.Introspector.flushCaches(); + + // Clear any custom URLStreamHandlers + TomcatURLStreamHandlerFactory.release(this); + } + + + /** + * Deregister any JDBC drivers registered by the webapp that the webapp forgot. This is made unnecessary complex + * because a) DriverManager checks the class loader of the calling class (it would be much easier if it checked the + * context class loader) b) using reflection would create a dependency on the DriverManager implementation which + * can, and has, changed. + *

    + * We can't just create an instance of JdbcLeakPrevention as it will be loaded by the common class loader (since + * it's .class file is in the $CATALINA_HOME/lib directory). This would fail DriverManager's check on the class + * loader of the calling class. So, we load the bytes via our parent class loader but define the class with this + * class loader so the JdbcLeakPrevention looks like a webapp class to the DriverManager. + *

    + * If only apps cleaned up after themselves... + */ + private void clearReferencesJdbc() { + // We know roughly how big the class will be (~ 1K) so allow 2k as a + // starting point + byte[] classBytes = new byte[2048]; + int offset = 0; + try (InputStream is = getResourceAsStream("org/apache/catalina/loader/JdbcLeakPrevention.class")) { + int read = is.read(classBytes, offset, classBytes.length - offset); + while (read > -1) { + offset += read; + if (offset == classBytes.length) { + // Buffer full - double size + byte[] tmp = new byte[classBytes.length * 2]; + System.arraycopy(classBytes, 0, tmp, 0, classBytes.length); + classBytes = tmp; + } + read = is.read(classBytes, offset, classBytes.length - offset); + } + Class lpClass = defineClass("org.apache.catalina.loader.JdbcLeakPrevention", classBytes, 0, offset, + this.getClass().getProtectionDomain()); + Object obj = lpClass.getConstructor().newInstance(); + @SuppressWarnings("unchecked") + List driverNames = + (List) obj.getClass().getMethod("clearJdbcDriverRegistrations").invoke(obj); + for (String name : driverNames) { + log.warn(sm.getString("webappClassLoader.clearJdbc", getContextName(), name)); + } + } catch (Exception e) { + // So many things to go wrong above... + Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("webappClassLoader.jdbcRemoveFailed", getContextName()), t); + } + } + + + @SuppressWarnings("deprecation") // thread.stop() + private void clearReferencesThreads() { + Thread[] threads = getThreads(); + List threadsToStop = new ArrayList<>(); + + // Iterate over the set of threads + for (Thread thread : threads) { + if (thread != null) { + ClassLoader ccl = thread.getContextClassLoader(); + if (ccl == this) { + // Don't warn about this thread + if (thread == Thread.currentThread()) { + continue; + } + + final String threadName = thread.getName(); + + // JVM controlled threads + ThreadGroup tg = thread.getThreadGroup(); + if (tg != null && JVM_THREAD_GROUP_NAMES.contains(tg.getName())) { + // HttpClient keep-alive threads + if (clearReferencesHttpClientKeepAliveThread && threadName.equals("Keep-Alive-Timer")) { + thread.setContextClassLoader(parent); + log.debug(sm.getString("webappClassLoader.checkThreadsHttpClient")); + } + + // Don't warn about remaining JVM controlled threads + continue; + } + + // Skip threads that have already died + if (!thread.isAlive()) { + continue; + } + + // TimerThread can be stopped safely so treat separately + // "java.util.TimerThread" in Sun/Oracle JDK + // "java.util.Timer$TimerImpl" in Apache Harmony and in IBM JDK + if (thread.getClass().getName().startsWith("java.util.Timer") && clearReferencesStopTimerThreads) { + clearReferencesStopTimerThread(thread); + continue; + } + + if (isRequestThread(thread)) { + log.warn(sm.getString("webappClassLoader.stackTraceRequestThread", getContextName(), threadName, + getStackTrace(thread))); + } else { + log.warn(sm.getString("webappClassLoader.stackTrace", getContextName(), threadName, + getStackTrace(thread))); + } + + // Don't try and stop the threads unless explicitly + // configured to do so + if (!clearReferencesStopThreads) { + continue; + } + + // If the thread has been started via an executor, try + // shutting down the executor + boolean usingExecutor = false; + try { + Object executor = JreCompat.getInstance().getExecutor(thread); + if (executor instanceof ThreadPoolExecutor) { + ((ThreadPoolExecutor) executor).shutdownNow(); + usingExecutor = true; + } else if (executor instanceof java.util.concurrent.ThreadPoolExecutor) { + ((java.util.concurrent.ThreadPoolExecutor) executor).shutdownNow(); + usingExecutor = true; + } + } catch (SecurityException | NoSuchFieldException | IllegalArgumentException | + IllegalAccessException | InaccessibleObjectException e) { + log.warn(sm.getString("webappClassLoader.stopThreadFail", thread.getName(), getContextName()), + e); + } + + // Stopping an executor automatically interrupts the + // associated threads. For non-executor threads, interrupt + // them here. + if (!usingExecutor && !thread.isInterrupted()) { + thread.interrupt(); + } + + // Threads are expected to take a short time to stop after + // being interrupted. Make a note of all threads that are + // expected to stop to enable them to be checked at the end + // of this method. + threadsToStop.add(thread); + } + } + } + + // If thread stopping is enabled, threads should have been stopped above + // when the executor was shut down or the thread was interrupted but + // that depends on the thread correctly handling the interrupt. Check + // each thread and if any are still running give all threads up to a + // total of 2 seconds to shutdown. + int count = 0; + for (Thread t : threadsToStop) { + while (t.isAlive() && count < 100) { + try { + Thread.sleep(20); + } catch (InterruptedException e) { + // Quit the while loop + break; + } + count++; + } + if (t.isAlive()) { + // This method is deprecated and for good reason. This is + // very risky code but is the only option at this point. + // A *very* good reason for apps to do this clean-up + // themselves. + t.stop(); + } + } + } + + + /* + * Look at a threads stack trace to see if it is a request thread or not. It isn't perfect, but it should be + * good-enough for most cases. + */ + private boolean isRequestThread(Thread thread) { + + StackTraceElement[] elements = thread.getStackTrace(); + + if (elements == null || elements.length == 0) { + // Must have stopped already. Too late to ignore it. Assume not a + // request processing thread. + return false; + } + + // Step through the methods in reverse order looking for calls to any + // CoyoteAdapter method. All request threads will have this unless + // Tomcat has been heavily modified - in which case there isn't much we + // can do. + for (int i = 0; i < elements.length; i++) { + StackTraceElement element = elements[elements.length - (i + 1)]; + if ("org.apache.catalina.connector.CoyoteAdapter".equals(element.getClassName())) { + return true; + } + } + return false; + } + + + private void clearReferencesStopTimerThread(Thread thread) { + + // Need to get references to: + // in Sun/Oracle JDK: + // - newTasksMayBeScheduled field (in java.util.TimerThread) + // - queue field + // - queue.clear() + // in IBM JDK, Apache Harmony: + // - cancel() method (in java.util.Timer$TimerImpl) + + try { + + try { + Field newTasksMayBeScheduledField = thread.getClass().getDeclaredField("newTasksMayBeScheduled"); + newTasksMayBeScheduledField.setAccessible(true); + Field queueField = thread.getClass().getDeclaredField("queue"); + queueField.setAccessible(true); + + Object queue = queueField.get(thread); + + Method clearMethod = queue.getClass().getDeclaredMethod("clear"); + clearMethod.setAccessible(true); + + synchronized (queue) { + newTasksMayBeScheduledField.setBoolean(thread, false); + clearMethod.invoke(queue); + // In case queue was already empty. Should only be one + // thread waiting but use notifyAll() to be safe. + queue.notifyAll(); + } + + } catch (NoSuchFieldException nfe) { + Method cancelMethod = thread.getClass().getDeclaredMethod("cancel"); + synchronized (thread) { + cancelMethod.setAccessible(true); + cancelMethod.invoke(thread); + } + } + + log.warn(sm.getString("webappClassLoader.warnTimerThread", getContextName(), thread.getName())); + + } catch (Exception e) { + // So many things to go wrong above... + Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("webappClassLoader.stopTimerThreadFail", thread.getName(), getContextName()), t); + } + } + + private void checkThreadLocalsForLeaks() { + Thread[] threads = getThreads(); + + try { + // Make the fields in the Thread class that store ThreadLocals + // accessible + Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); + threadLocalsField.setAccessible(true); + Field inheritableThreadLocalsField = Thread.class.getDeclaredField("inheritableThreadLocals"); + inheritableThreadLocalsField.setAccessible(true); + // Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects + // accessible + Class tlmClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); + Field tableField = tlmClass.getDeclaredField("table"); + tableField.setAccessible(true); + Method expungeStaleEntriesMethod = tlmClass.getDeclaredMethod("expungeStaleEntries"); + expungeStaleEntriesMethod.setAccessible(true); + + for (Thread thread : threads) { + Object threadLocalMap; + if (thread != null) { + + // Clear the first map + threadLocalMap = threadLocalsField.get(thread); + if (null != threadLocalMap) { + expungeStaleEntriesMethod.invoke(threadLocalMap); + checkThreadLocalMapForLeaks(threadLocalMap, tableField); + } + + // Clear the second map + threadLocalMap = inheritableThreadLocalsField.get(thread); + if (null != threadLocalMap) { + expungeStaleEntriesMethod.invoke(threadLocalMap); + checkThreadLocalMapForLeaks(threadLocalMap, tableField); + } + } + } + } catch (InaccessibleObjectException e) { + // Must be running on without the necessary command line options. + log.warn(sm.getString("webappClassLoader.addExportsThreadLocal", getCurrentModuleName())); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", getContextName()), t); + } + } + + + /** + * Analyzes the given thread local map object. Also pass in the field that points to the internal table to save + * re-calculating it on every call to this method. + */ + private void checkThreadLocalMapForLeaks(Object map, Field internalTableField) + throws IllegalAccessException, NoSuchFieldException { + if (map != null) { + Object[] table = (Object[]) internalTableField.get(map); + if (table != null) { + for (Object obj : table) { + if (obj != null) { + boolean keyLoadedByWebapp = false; + boolean valueLoadedByWebapp = false; + // Check the key + Object key = ((Reference) obj).get(); + if (this.equals(key) || loadedByThisOrChild(key)) { + keyLoadedByWebapp = true; + } + // Check the value + Field valueField = obj.getClass().getDeclaredField("value"); + valueField.setAccessible(true); + Object value = valueField.get(obj); + if (this.equals(value) || loadedByThisOrChild(value)) { + valueLoadedByWebapp = true; + } + if (keyLoadedByWebapp || valueLoadedByWebapp) { + Object[] args = new Object[5]; + args[0] = getContextName(); + if (key != null) { + args[1] = getPrettyClassName(key.getClass()); + try { + args[2] = key.toString(); + } catch (Exception e) { + log.warn( + sm.getString("webappClassLoader.checkThreadLocalsForLeaks.badKey", args[1]), + e); + args[2] = sm.getString("webappClassLoader.checkThreadLocalsForLeaks.unknown"); + } + } + if (value != null) { + args[3] = getPrettyClassName(value.getClass()); + try { + args[4] = value.toString(); + } catch (Exception e) { + log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaks.badValue", + args[3]), e); + args[4] = sm.getString("webappClassLoader.checkThreadLocalsForLeaks.unknown"); + } + } + if (valueLoadedByWebapp) { + log.error(sm.getString("webappClassLoader.checkThreadLocalsForLeaks", args)); + } else if (value == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("webappClassLoader.checkThreadLocalsForLeaksNull", args)); + } + } else { + if (log.isDebugEnabled()) { + log.debug(sm.getString("webappClassLoader.checkThreadLocalsForLeaksNone", args)); + } + } + } + } + } + } + } + } + + private String getPrettyClassName(Class clazz) { + String name = clazz.getCanonicalName(); + if (name == null) { + name = clazz.getName(); + } + return name; + } + + private String getStackTrace(Thread thread) { + StringBuilder builder = new StringBuilder(); + for (StackTraceElement ste : thread.getStackTrace()) { + builder.append("\n ").append(ste); + } + return builder.toString(); + } + + /** + * @param o object to test, may be null + * + * @return true if o has been loaded by the current classloader or one of its descendants. + */ + private boolean loadedByThisOrChild(Object o) { + if (o == null) { + return false; + } + + Class clazz; + if (o instanceof Class) { + clazz = (Class) o; + } else { + clazz = o.getClass(); + } + + ClassLoader cl = clazz.getClassLoader(); + while (cl != null) { + if (cl == this) { + return true; + } + cl = cl.getParent(); + } + + if (o instanceof Collection) { + try { + for (Object entry : (Collection) o) { + if (loadedByThisOrChild(entry)) { + return true; + } + } + } catch (ConcurrentModificationException e) { + log.warn(sm.getString("webappClassLoader.loadedByThisOrChildFail", clazz.getName(), getContextName()), + e); + } + } + return false; + } + + /* + * Get the set of current threads as an array. + */ + private Thread[] getThreads() { + // Get the current thread group + ThreadGroup tg = Thread.currentThread().getThreadGroup(); + // Find the root thread group + try { + while (tg.getParent() != null) { + tg = tg.getParent(); + } + } catch (SecurityException se) { + String msg = sm.getString("webappClassLoader.getThreadGroupError", tg.getName()); + if (log.isDebugEnabled()) { + log.debug(msg, se); + } else { + log.warn(msg); + } + } + + int threadCountGuess = tg.activeCount() + 50; + Thread[] threads = new Thread[threadCountGuess]; + int threadCountActual = tg.enumerate(threads); + // Make sure we don't miss any threads + while (threadCountActual == threadCountGuess) { + threadCountGuess *= 2; + threads = new Thread[threadCountGuess]; + // Note tg.enumerate(Thread[]) silently ignores any threads that + // can't fit into the array + threadCountActual = tg.enumerate(threads); + } + + return threads; + } + + + /** + * This depends on the internals of the Sun JVM so it does everything by reflection. + */ + private void clearReferencesRmiTargets() { + try { + // Need access to the ccl field of sun.rmi.transport.Target to find + // the leaks + Class objectTargetClass = Class.forName("sun.rmi.transport.Target"); + Field cclField = objectTargetClass.getDeclaredField("ccl"); + cclField.setAccessible(true); + // Need access to the stub field to report the leaks + Field stubField = objectTargetClass.getDeclaredField("stub"); + stubField.setAccessible(true); + + // Clear the objTable map + Class objectTableClass = Class.forName("sun.rmi.transport.ObjectTable"); + Field objTableField = objectTableClass.getDeclaredField("objTable"); + objTableField.setAccessible(true); + Object objTable = objTableField.get(null); + if (objTable == null) { + return; + } + Field tableLockField = objectTableClass.getDeclaredField("tableLock"); + tableLockField.setAccessible(true); + Object tableLock = tableLockField.get(null); + + synchronized (tableLock) { + // Iterate over the values in the table + if (objTable instanceof Map) { + Iterator iter = ((Map) objTable).values().iterator(); + while (iter.hasNext()) { + Object obj = iter.next(); + Object cclObject = cclField.get(obj); + if (this == cclObject) { + iter.remove(); + Object stubObject = stubField.get(obj); + log.error(sm.getString("webappClassLoader.clearRmi", stubObject.getClass().getName(), + stubObject)); + } + } + } + + // Clear the implTable map + Field implTableField = objectTableClass.getDeclaredField("implTable"); + implTableField.setAccessible(true); + Object implTable = implTableField.get(null); + if (implTable == null) { + return; + } + + // Iterate over the values in the table + if (implTable instanceof Map) { + Iterator iter = ((Map) implTable).values().iterator(); + while (iter.hasNext()) { + Object obj = iter.next(); + Object cclObject = cclField.get(obj); + if (this == cclObject) { + iter.remove(); + } + } + } + } + } catch (ClassNotFoundException e) { + log.info(sm.getString("webappClassLoader.clearRmiInfo", getContextName()), e); + } catch (SecurityException | NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { + log.warn(sm.getString("webappClassLoader.clearRmiFail", getContextName()), e); + } catch (InaccessibleObjectException e) { + // Must be running on without the necessary command line options. + log.warn(sm.getString("webappClassLoader.addExportsRmi", getCurrentModuleName())); + } + } + + + private void clearReferencesObjectStreamClassCaches() { + if (JreCompat.isJre19Available()) { + // The memory leak this fixes has been fixed in Java 19 onwards, + // 17.0.4 onwards and 11.0.16 onwards + // See https://bugs.openjdk.java.net/browse/JDK-8277072 + return; + } + try { + Class clazz = Class.forName("java.io.ObjectStreamClass$Caches"); + clearCache(clazz, "localDescs"); + clearCache(clazz, "reflectors"); + } catch (ReflectiveOperationException | SecurityException | ClassCastException e) { + log.warn(sm.getString("webappClassLoader.clearObjectStreamClassCachesFail", getContextName()), e); + } catch (InaccessibleObjectException e) { + // Must be running on without the necessary command line options. + log.warn(sm.getString("webappClassLoader.addExportsJavaIo", getCurrentModuleName())); + } + } + + + private String getCurrentModuleName() { + String moduleName = this.getClass().getModule().getName(); + if (moduleName == null) { + moduleName = "ALL-UNNAMED"; + } + return moduleName; + } + + + private void clearCache(Class target, String mapName) + throws ReflectiveOperationException, SecurityException, ClassCastException { + Field f = target.getDeclaredField(mapName); + f.setAccessible(true); + Object map = f.get(null); + // Avoid trying to clear references if Tomcat is running on a JRE that + // includes the fix for this memory leak + // See https://bugs.openjdk.java.net/browse/JDK-8277072 + if (map instanceof Map) { + Iterator keys = ((Map) map).keySet().iterator(); + while (keys.hasNext()) { + Object key = keys.next(); + if (key instanceof Reference) { + Object clazz = ((Reference) key).get(); + if (loadedByThisOrChild(clazz)) { + keys.remove(); + } + } + } + } + } + + + /** + * Find specified class in local repositories. + * + * @param name The binary name of the class to be loaded + * + * @return the loaded class, or null if the class isn't found + */ + /* + * The use of getPackage() is appropriate given that the code is checking if the package is sealed. Therefore, + * parent class loaders need to be checked. + */ + @SuppressWarnings("deprecation") + protected Class findClassInternal(String name) { + + checkStateForResourceLoading(name); + + if (name == null) { + return null; + } + String path = binaryNameToPath(name, true); + + ResourceEntry entry = resourceEntries.get(path); + WebResource resource = null; + + if (entry == null) { + resource = resources.getClassLoaderResource(path); + + if (!resource.exists()) { + return null; + } + + entry = new ResourceEntry(); + entry.lastModified = resource.getLastModified(); + + // Add the entry in the local resource repository + synchronized (resourceEntries) { + // Ensures that all the threads which may be in a race to load + // a particular class all end up with the same ResourceEntry + // instance + ResourceEntry entry2 = resourceEntries.get(path); + if (entry2 == null) { + resourceEntries.put(path, entry); + } else { + entry = entry2; + } + } + } + + Class clazz = entry.loadedClass; + if (clazz != null) { + return clazz; + } + + synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) { + clazz = entry.loadedClass; + if (clazz != null) { + return clazz; + } + + if (resource == null) { + resource = resources.getClassLoaderResource(path); + } + + if (!resource.exists()) { + return null; + } + + byte[] binaryContent = resource.getContent(); + if (binaryContent == null) { + // Something went wrong reading the class bytes (and will have + // been logged at debug level). + return null; + } + Manifest manifest = resource.getManifest(); + URL codeBase = resource.getCodeBase(); + Certificate[] certificates = resource.getCertificates(); + + if (transformers.size() > 0) { + // If the resource is a class just being loaded, decorate it + // with any attached transformers + + // Ignore leading '/' and trailing CLASS_FILE_SUFFIX + // Should be cheaper than replacing '.' by '/' in class name. + String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length()); + + for (ClassFileTransformer transformer : this.transformers) { + try { + byte[] transformed = transformer.transform(this, internalName, null, null, binaryContent); + if (transformed != null) { + binaryContent = transformed; + } + } catch (IllegalClassFormatException e) { + log.error(sm.getString("webappClassLoader.transformError", name), e); + return null; + } + } + } + + // Looking up the package + String packageName = null; + int pos = name.lastIndexOf('.'); + if (pos != -1) { + packageName = name.substring(0, pos); + } + + Package pkg = null; + + if (packageName != null) { + pkg = getPackage(packageName); + + // Define the package (if null) + if (pkg == null) { + try { + if (manifest == null) { + definePackage(packageName, null, null, null, null, null, null, null); + } else { + definePackage(packageName, manifest, codeBase); + } + } catch (IllegalArgumentException e) { + // Ignore: normal error due to dual definition of package + } + pkg = getPackage(packageName); + } + } + + if (securityManager != null) { + // Checking sealing + if (pkg != null) { + boolean sealCheck = true; + if (pkg.isSealed()) { + sealCheck = pkg.isSealed(codeBase); + } else { + sealCheck = manifest == null || !isPackageSealed(packageName, manifest); + } + if (!sealCheck) { + throw new SecurityException( + "Sealing violation loading " + name + " : Package " + packageName + " is sealed."); + } + } + } + + try { + clazz = defineClass(name, binaryContent, 0, binaryContent.length, + new CodeSource(codeBase, certificates)); + } catch (UnsupportedClassVersionError ucve) { + throw new UnsupportedClassVersionError( + ucve.getLocalizedMessage() + " " + sm.getString("webappClassLoader.wrongVersion", name)); + } catch (LinkageError e) { + // May be caused by the transformation also triggering loading of the class - BZ 68721 + try { + // Try and load the already defined class + clazz = findLoadedClass0(name); + } catch (Throwable t) { + // Not BZ 68721 + ExceptionUtils.handleThrowable(t); + // Re-throw the original exception + throw e; + } + } + entry.loadedClass = clazz; + } + + return clazz; + } + + + private String binaryNameToPath(String binaryName, boolean withLeadingSlash) { + // 1 for leading '/', 6 for ".class" + StringBuilder path = new StringBuilder(7 + binaryName.length()); + if (withLeadingSlash) { + path.append('/'); + } + path.append(binaryName.replace('.', '/')); + path.append(CLASS_FILE_SUFFIX); + return path.toString(); + } + + + private String nameToPath(String name) { + if (name.startsWith("/")) { + return name; + } + StringBuilder path = new StringBuilder(1 + name.length()); + path.append('/'); + path.append(name); + return path.toString(); + } + + + /** + * Returns true if the specified package name is sealed according to the given manifest. + * + * @param name Path name to check + * @param man Associated manifest + * + * @return true if the manifest associated says it is sealed + */ + protected boolean isPackageSealed(String name, Manifest man) { + + String path = name.replace('.', '/') + '/'; + Attributes attr = man.getAttributes(path); + String sealed = null; + if (attr != null) { + sealed = attr.getValue(Name.SEALED); + } + if (sealed == null) { + if ((attr = man.getMainAttributes()) != null) { + sealed = attr.getValue(Name.SEALED); + } + } + return "true".equalsIgnoreCase(sealed); + + } + + + /** + * Finds the class with the given name if it has previously been loaded and cached by this class loader, and return + * the Class object. If this class has not been cached, return null. + * + * @param name The binary name of the resource to return + * + * @return a loaded class + */ + protected Class findLoadedClass0(String name) { + + String path = binaryNameToPath(name, true); + + ResourceEntry entry = resourceEntries.get(path); + if (entry != null) { + return entry.loadedClass; + } + return null; + } + + + /** + * Refresh the system policy file, to pick up eventual changes. + */ + protected void refreshPolicy() { + + try { + // The policy file may have been modified to adjust + // permissions, so we're reloading it when loading or + // reloading a Context + Policy policy = Policy.getPolicy(); + policy.refresh(); + } catch (AccessControlException e) { + // Some policy files may restrict this, even for the core, + // so this exception is ignored + } + + } + + + /** + * Filter classes. + * + * @param name class name + * @param isClassName true if name is a class name, false if name is a resource name + * + * @return true if the class should be filtered + */ + protected boolean filter(String name, boolean isClassName) { + + if (name == null) { + return false; + } + + char ch; + if (name.startsWith("jakarta")) { + /* 7 == length("jakarta") */ + if (name.length() == 7) { + return false; + } + ch = name.charAt(7); + if (isClassName && ch == '.') { + /* 8 == length("jakarta.") */ + if (name.startsWith("servlet.jsp.jstl.", 8)) { + return false; + } + if (name.startsWith("annotation.", 8) || name.startsWith("el.", 8) || name.startsWith("servlet.", 8) || + name.startsWith("websocket.", 8) || name.startsWith("security.auth.message.", 8)) { + return true; + } + } else if (!isClassName && ch == '/') { + /* 8 == length("jakarta/") */ + if (name.startsWith("servlet/jsp/jstl/", 8)) { + return false; + } + if (name.startsWith("annotation/", 8) || name.startsWith("el/", 8) || name.startsWith("servlet/", 8) || + name.startsWith("websocket/", 8) || name.startsWith("security/auth/message/", 8)) { + return true; + } + } + } else if (name.startsWith("javax")) { + /* 5 == length("javax") */ + if (name.length() == 5) { + return false; + } + ch = name.charAt(5); + if (isClassName && ch == '.') { + /* 6 == length("javax.") */ + if (name.startsWith("websocket.", 6)) { + return true; + } + } else if (!isClassName && ch == '/') { + /* 6 == length("javax/") */ + if (name.startsWith("websocket/", 6)) { + return true; + } + } + } else if (name.startsWith("org")) { + /* 3 == length("org") */ + if (name.length() == 3) { + return false; + } + ch = name.charAt(3); + if (isClassName && ch == '.') { + /* 4 == length("org.") */ + if (name.startsWith("apache.", 4)) { + /* 11 == length("org.apache.") */ + if (name.startsWith("tomcat.jdbc.", 11)) { + return false; + } + if (name.startsWith("el.", 11) || name.startsWith("catalina.", 11) || + name.startsWith("jasper.", 11) || name.startsWith("juli.", 11) || + name.startsWith("tomcat.", 11) || name.startsWith("naming.", 11) || + name.startsWith("coyote.", 11)) { + return true; + } + } + } else if (!isClassName && ch == '/') { + /* 4 == length("org/") */ + if (name.startsWith("apache/", 4)) { + /* 11 == length("org/apache/") */ + if (name.startsWith("tomcat/jdbc/", 11)) { + return false; + } + if (name.startsWith("el/", 11) || name.startsWith("catalina/", 11) || + name.startsWith("jasper/", 11) || name.startsWith("juli/", 11) || + name.startsWith("tomcat/", 11) || name.startsWith("naming/", 11) || + name.startsWith("coyote/", 11)) { + return true; + } + } + } + } + return false; + } + + + @Override + protected void addURL(URL url) { + super.addURL(url); + hasExternalRepositories = true; + } + + + @Override + public String getWebappName() { + return getContextName(); + } + + + @Override + public String getHostName() { + if (resources != null) { + Container host = resources.getContext().getParent(); + if (host != null) { + return host.getName(); + } + } + return null; + } + + + @Override + public String getServiceName() { + if (resources != null) { + Container host = resources.getContext().getParent(); + if (host != null) { + Container engine = host.getParent(); + if (engine != null) { + return engine.getName(); + } + } + } + return null; + } + + + @Override + public boolean hasLoggingConfig() { + if (Globals.IS_SECURITY_ENABLED) { + Boolean result = AccessController.doPrivileged(new PrivilegedHasLoggingConfig()); + return result.booleanValue(); + } else { + return findResource("logging.properties") != null; + } + } + + + private class PrivilegedHasLoggingConfig implements PrivilegedAction { + + @Override + public Boolean run() { + return Boolean.valueOf(findResource("logging.properties") != null); + } + } + + + private static class CombinedEnumeration implements Enumeration { + + private final Enumeration[] sources; + private int index = 0; + + CombinedEnumeration(Enumeration enum1, Enumeration enum2) { + @SuppressWarnings("unchecked") + Enumeration[] sources = new Enumeration[] { enum1, enum2 }; + this.sources = sources; + } + + + @Override + public boolean hasMoreElements() { + return inc(); + } + + + @Override + public URL nextElement() { + if (inc()) { + return sources[index].nextElement(); + } + throw new NoSuchElementException(); + } + + + private boolean inc() { + while (index < sources.length) { + if (sources[index].hasMoreElements()) { + return true; + } + index++; + } + return false; + } + } +} diff --git a/java/org/apache/catalina/loader/WebappLoader.java b/java/org/apache/catalina/loader/WebappLoader.java new file mode 100644 index 0000000..a7b4760 --- /dev/null +++ b/java/org/apache/catalina/loader/WebappLoader.java @@ -0,0 +1,623 @@ +/* + * 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.catalina.loader; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import java.io.FilePermission; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; + +import javax.management.ObjectName; + +import jakarta.servlet.ServletContext; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Loader; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.catalina.util.ToStringUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jakartaee.ClassConverter; +import org.apache.tomcat.jakartaee.EESpecProfile; +import org.apache.tomcat.jakartaee.EESpecProfiles; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + * Classloader implementation which is specialized for handling web applications in the most efficient way, while being + * Catalina aware (all accesses to resources are made through {@link org.apache.catalina.WebResourceRoot}). This class + * loader supports detection of modified Java classes, which can be used to implement auto-reload support. + *

    + * This class loader is configured via the Resources children of its Context prior to calling start(). When + * a new class is required, these Resources will be consulted first to locate the class. If it is not present, the + * system class loader will be used instead. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class WebappLoader extends LifecycleMBeanBase implements Loader { + + private static final Log log = LogFactory.getLog(WebappLoader.class); + + // ----------------------------------------------------- Instance Variables + + /** + * The class loader being managed by this Loader component. + */ + private WebappClassLoaderBase classLoader = null; + + + /** + * The Context with which this Loader has been associated. + */ + private Context context = null; + + + /** + * The "follow standard delegation model" flag that will be used to configure our ClassLoader. + */ + private boolean delegate = false; + + + /** + * The profile name which will be used by the converter, or null if not used. Any invalid profile value will default + * to the TOMCAT profile, which converts all packages used by Tomcat. + */ + private String jakartaConverter = null; + + + /** + * The Java class name of the ClassLoader implementation to be used. This class should extend WebappClassLoaderBase, + * otherwise, a different loader implementation must be used. + */ + private String loaderClass = ParallelWebappClassLoader.class.getName(); + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(WebappLoader.class); + + + /** + * The property change support for this component. + */ + protected final PropertyChangeSupport support = new PropertyChangeSupport(this); + + + /** + * Classpath set in the loader. + */ + private String classpath = null; + + + // ------------------------------------------------------------- Properties + + /** + * Return the Java class loader to be used by this Container. + */ + @Override + public ClassLoader getClassLoader() { + return classLoader; + } + + + @Override + public Context getContext() { + return context; + } + + + @Override + public void setContext(Context context) { + + if (this.context == context) { + return; + } + + if (getState().isAvailable()) { + throw new IllegalStateException(sm.getString("webappLoader.setContext.ise")); + } + + // Process this property change + Context oldContext = this.context; + this.context = context; + support.firePropertyChange("context", oldContext, this.context); + } + + + /** + * Return the "follow standard delegation model" flag used to configure our ClassLoader. + */ + @Override + public boolean getDelegate() { + return this.delegate; + } + + + /** + * Set the "follow standard delegation model" flag used to configure our ClassLoader. + * + * @param delegate The new flag + */ + @Override + public void setDelegate(boolean delegate) { + boolean oldDelegate = this.delegate; + this.delegate = delegate; + support.firePropertyChange("delegate", Boolean.valueOf(oldDelegate), Boolean.valueOf(this.delegate)); + } + + + /** + * @return a non null String if the loader will attempt to use the Jakarta converter. The String is the name of the + * profile used for conversion. + */ + public String getJakartaConverter() { + return jakartaConverter; + } + + + /** + * Set the Jakarta converter. + * + * @param jakartaConverter The profile name which will be used by the converter Any invalid profile value will + * default to the TOMCAT profile, which converts all packages used by Tomcat. + */ + public void setJakartaConverter(String jakartaConverter) { + String oldJakartaConverter = this.jakartaConverter; + this.jakartaConverter = jakartaConverter; + support.firePropertyChange("jakartaConverter", oldJakartaConverter, this.jakartaConverter); + } + + + /** + * @return the ClassLoader class name. + */ + public String getLoaderClass() { + return this.loaderClass; + } + + + /** + * Set the ClassLoader class name. + * + * @param loaderClass The new ClassLoader class name + */ + public void setLoaderClass(String loaderClass) { + this.loaderClass = loaderClass; + } + + /** + * Set the ClassLoader instance, without relying on reflection This method will also invoke + * {@link #setLoaderClass(String)} with {@code loaderInstance.getClass().getName()} as an argument + * + * @param loaderInstance The new ClassLoader instance to use + */ + public void setLoaderInstance(WebappClassLoaderBase loaderInstance) { + this.classLoader = loaderInstance; + setLoaderClass(loaderInstance.getClass().getName()); + } + + // --------------------------------------------------------- Public Methods + + /** + * Add a property change listener to this component. + * + * @param listener The listener to add + */ + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + + support.addPropertyChangeListener(listener); + + } + + + /** + * Execute a periodic task, such as reloading, etc. This method will be invoked inside the classloading context of + * this container. Unexpected throwables will be caught and logged. + */ + @Override + public void backgroundProcess() { + Context context = getContext(); + if (context != null) { + if (context.getReloadable() && modified()) { + Thread currentThread = Thread.currentThread(); + ClassLoader originalTccl = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(WebappLoader.class.getClassLoader()); + context.reload(); + } finally { + currentThread.setContextClassLoader(originalTccl); + } + } + } + } + + + public String[] getLoaderRepositories() { + if (classLoader == null) { + return new String[0]; + } + URL[] urls = classLoader.getURLs(); + String[] result = new String[urls.length]; + for (int i = 0; i < urls.length; i++) { + result[i] = urls[i].toExternalForm(); + } + return result; + } + + public String getLoaderRepositoriesString() { + String repositories[] = getLoaderRepositories(); + StringBuilder sb = new StringBuilder(); + for (String repository : repositories) { + sb.append(repository).append(':'); + } + return sb.toString(); + } + + + /** + * Classpath, as set in org.apache.catalina.jsp_classpath context property + * + * @return The classpath + */ + public String getClasspath() { + return classpath; + } + + + /** + * Has the internal repository associated with this Loader been modified, such that the loaded classes should be + * reloaded? + */ + @Override + public boolean modified() { + return classLoader != null ? classLoader.modified() : false; + } + + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + support.removePropertyChangeListener(listener); + } + + + /** + * Return a String representation of this component. + */ + @Override + public String toString() { + return ToStringUtil.toString(this, context); + } + + + /** + * Start associated {@link ClassLoader} and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("webappLoader.starting")); + } + + if (context.getResources() == null) { + log.info(sm.getString("webappLoader.noResources", context)); + setState(LifecycleState.STARTING); + return; + } + + // Construct a class loader based on our current repositories list + try { + + classLoader = createClassLoader(); + classLoader.setResources(context.getResources()); + classLoader.setDelegate(this.delegate); + + // Set Jakarta class converter + if (getJakartaConverter() != null) { + MigrationUtil.addJakartaEETransformer(classLoader, getJakartaConverter()); + } + + // Configure our repositories + setClassPath(); + + setPermissions(); + + classLoader.start(); + + String contextName = context.getName(); + if (!contextName.startsWith("/")) { + contextName = "/" + contextName; + } + ObjectName cloname = + new ObjectName(context.getDomain() + ":type=" + classLoader.getClass().getSimpleName() + ",host=" + + context.getParent().getName() + ",context=" + contextName); + Registry.getRegistry(null, null).registerComponent(classLoader, cloname, null); + + } catch (Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + throw new LifecycleException(sm.getString("webappLoader.startError"), t); + } + + setState(LifecycleState.STARTING); + } + + + /** + * Stop associated {@link ClassLoader} and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("webappLoader.stopping")); + } + + setState(LifecycleState.STOPPING); + + // Remove context attributes as appropriate + ServletContext servletContext = context.getServletContext(); + servletContext.removeAttribute(Globals.CLASS_PATH_ATTR); + + // Throw away our current class loader if any + if (classLoader != null) { + try { + classLoader.stop(); + } finally { + classLoader.destroy(); + } + + // classLoader must be non-null to have been registered + try { + String contextName = context.getName(); + if (!contextName.startsWith("/")) { + contextName = "/" + contextName; + } + ObjectName cloname = + new ObjectName(context.getDomain() + ":type=" + classLoader.getClass().getSimpleName() + + ",host=" + context.getParent().getName() + ",context=" + contextName); + Registry.getRegistry(null, null).unregisterComponent(cloname); + } catch (Exception e) { + log.warn(sm.getString("webappLoader.stopError"), e); + } + } + + + classLoader = null; + } + + + // ------------------------------------------------------- Private Methods + + /** + * Create associated classLoader. + */ + private WebappClassLoaderBase createClassLoader() throws Exception { + + if (classLoader != null) { + return classLoader; + } + + if (ParallelWebappClassLoader.class.getName().equals(loaderClass)) { + return new ParallelWebappClassLoader(context.getParentClassLoader()); + } + + Class clazz = Class.forName(loaderClass); + WebappClassLoaderBase classLoader = null; + + ClassLoader parentClassLoader = context.getParentClassLoader(); + + Class[] argTypes = { ClassLoader.class }; + Object[] args = { parentClassLoader }; + Constructor constr = clazz.getConstructor(argTypes); + classLoader = (WebappClassLoaderBase) constr.newInstance(args); + + return classLoader; + } + + + /** + * Configure associated class loader permissions. + */ + private void setPermissions() { + + if (!Globals.IS_SECURITY_ENABLED) { + return; + } + if (context == null) { + return; + } + + // Tell the class loader the root of the context + ServletContext servletContext = context.getServletContext(); + + // Assigning permissions for the work directory + File workDir = (File) servletContext.getAttribute(ServletContext.TEMPDIR); + if (workDir != null) { + try { + String workDirPath = workDir.getCanonicalPath(); + classLoader.addPermission(new FilePermission(workDirPath, "read,write")); + classLoader.addPermission(new FilePermission(workDirPath + File.separator + "-", "read,write,delete")); + } catch (IOException e) { + // Ignore + } + } + + for (URL url : context.getResources().getBaseUrls()) { + classLoader.addPermission(url); + } + } + + + /** + * Set the appropriate context attribute for our class path. This is required only because Jasper depends on it. + */ + private void setClassPath() { + + // Validate our current state information + if (context == null) { + return; + } + ServletContext servletContext = context.getServletContext(); + if (servletContext == null) { + return; + } + + StringBuilder classpath = new StringBuilder(); + + // Assemble the class path information from our class loader chain + ClassLoader loader = getClassLoader(); + + if (delegate && loader != null) { + // Skip the webapp loader for now as delegation is enabled + loader = loader.getParent(); + } + + while (loader != null) { + if (!buildClassPath(classpath, loader)) { + break; + } + loader = loader.getParent(); + } + + if (delegate) { + // Delegation was enabled, go back and add the webapp paths + loader = getClassLoader(); + if (loader != null) { + buildClassPath(classpath, loader); + } + } + + this.classpath = classpath.toString(); + + // Store the assembled class path as a servlet context attribute + servletContext.setAttribute(Globals.CLASS_PATH_ATTR, this.classpath); + } + + + private boolean buildClassPath(StringBuilder classpath, ClassLoader loader) { + if (loader instanceof URLClassLoader) { + URL repositories[] = ((URLClassLoader) loader).getURLs(); + for (URL url : repositories) { + String repository = url.toString(); + if (repository.startsWith("file://")) { + repository = UDecoder.URLDecode(repository.substring(7), StandardCharsets.UTF_8); + } else if (repository.startsWith("file:")) { + repository = UDecoder.URLDecode(repository.substring(5), StandardCharsets.UTF_8); + } else { + continue; + } + if (repository == null) { + continue; + } + if (classpath.length() > 0) { + classpath.append(File.pathSeparator); + } + classpath.append(repository); + } + } else if (loader == ClassLoader.getSystemClassLoader()) { + // From Java 9 the internal class loaders no longer extend + // URLCLassLoader + String cp = System.getProperty("java.class.path"); + if (cp != null && cp.length() > 0) { + if (classpath.length() > 0) { + classpath.append(File.pathSeparator); + } + classpath.append(cp); + } + return false; + } else { + // Ignore Graal "unknown" classloader + if (!JreCompat.isGraalAvailable()) { + log.info(sm.getString("webappLoader.unknownClassLoader", loader, loader.getClass())); + } + return false; + } + return true; + } + + @Override + protected String getDomainInternal() { + return context.getDomain(); + } + + + @Override + protected String getObjectNameKeyProperties() { + + StringBuilder name = new StringBuilder("type=Loader"); + + name.append(",host="); + name.append(context.getParent().getName()); + + name.append(",context="); + + String contextName = context.getName(); + if (!contextName.startsWith("/")) { + name.append('/'); + } + name.append(contextName); + + return name.toString(); + } + + + /* + * Implemented in a sub-class so EESpecProfile and EESpecProfiles are not loaded unless a profile is configured. + * Otherwise, tomcat-embed-core.jar has a runtime dependency on the migration tool whether it is used or not. + */ + private static class MigrationUtil { + + public static void addJakartaEETransformer(WebappClassLoaderBase webappClassLoader, String profileName) { + EESpecProfile profile = null; + try { + profile = EESpecProfiles.valueOf(profileName); + } catch (IllegalArgumentException ignored) { + // Use default value + log.warn(sm.getString("webappLoader.unknownProfile", profileName)); + } + webappClassLoader.addTransformer(profile != null ? new ClassConverter(profile) : new ClassConverter()); + } + } +} diff --git a/java/org/apache/catalina/loader/mbeans-descriptors.xml b/java/org/apache/catalina/loader/mbeans-descriptors.xml new file mode 100644 index 0000000..89e8d50 --- /dev/null +++ b/java/org/apache/catalina/loader/mbeans-descriptors.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/manager/Constants.java b/java/org/apache/catalina/manager/Constants.java new file mode 100644 index 0000000..5fd102a --- /dev/null +++ b/java/org/apache/catalina/manager/Constants.java @@ -0,0 +1,146 @@ +/* + * 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.catalina.manager; + + +public class Constants { + + public static final String Package = "org.apache.catalina.manager"; + + public static final String REL_EXTERNAL = "rel=\"noopener noreferrer\""; + + public static final String HTML_HEADER_SECTION; + public static final String BODY_HEADER_SECTION; + public static final String MESSAGE_SECTION; + public static final String MANAGER_SECTION; + public static final String SERVER_HEADER_SECTION; + public static final String SERVER_ROW_SECTION; + public static final String HTML_TAIL_SECTION; + + static { + //@formatter:off + HTML_HEADER_SECTION = + "\n" + + "\n" + + "\n"; + + BODY_HEADER_SECTION = + "{0}\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \"The\n" + + " \n" + + "
    \n" + + "


    \n" + + "\n" + + " \n" + + " \n" + + " \n" + + "
    \n" + + " {1}\n" + + "
    \n" + + "
    \n" + + "\n"; + + MESSAGE_SECTION = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
    " + + "{0} 
    {1}
    \n" + + "
    \n" + + "\n"; + + MANAGER_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
    {0}
    {2}{4}{6}{8}
    \n" + + "
    \n" + + "\n"; + + SERVER_HEADER_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + SERVER_ROW_SECTION = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "
    {0}
    {1}{2}{3}{4}{5}{6}{7}{8}
    {0}{1}{2}{3}{4}{5}{6}{7}
    \n" + + "
    \n" + + "\n"; + + HTML_TAIL_SECTION = + "
    \n" + + "
    \n" + + " Copyright © 1999-2024, Apache Software Foundation" + + "
    \n" + + "\n" + + "\n" + + ""; + //@formatter:on + } + + public static final String CHARSET = "utf-8"; + + public static final String XML_DECLARATION = ""; + + public static final String XML_STYLE = "\n"; +} diff --git a/java/org/apache/catalina/manager/DummyProxySession.java b/java/org/apache/catalina/manager/DummyProxySession.java new file mode 100644 index 0000000..30b1fae --- /dev/null +++ b/java/org/apache/catalina/manager/DummyProxySession.java @@ -0,0 +1,222 @@ +/* + * 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.catalina.manager; + +import java.security.Principal; +import java.util.Iterator; + +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.SessionListener; + +public class DummyProxySession implements Session { + + private String sessionId; + + public DummyProxySession(String sessionId) { + this.sessionId = sessionId; + } + + @Override + public void access() { + // NOOP + } + + @Override + public void addSessionListener(SessionListener listener) { + // NOOP + } + + @Override + public void endAccess() { + // NOOP + } + + @Override + public void expire() { + // NOOP + } + + @Override + public String getAuthType() { + return null; + } + + @Override + public long getCreationTime() { + return 0; + } + + @Override + public long getCreationTimeInternal() { + return 0; + } + + @Override + public String getId() { + return sessionId; + } + + @Override + public String getIdInternal() { + return sessionId; + } + + @Override + public long getLastAccessedTime() { + return 0; + } + + @Override + public long getLastAccessedTimeInternal() { + return 0; + } + + @Override + public long getIdleTime() { + return 0; + } + + @Override + public long getIdleTimeInternal() { + return 0; + } + + @Override + public Manager getManager() { + return null; + } + + @Override + public int getMaxInactiveInterval() { + return 0; + } + + @Override + public Object getNote(String name) { + return null; + } + + @Override + public Iterator getNoteNames() { + return null; + } + + @Override + public Principal getPrincipal() { + return null; + } + + @Override + public HttpSession getSession() { + return null; + } + + @Override + public long getThisAccessedTime() { + return 0; + } + + @Override + public long getThisAccessedTimeInternal() { + return 0; + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public void recycle() { + // NOOP + } + + @Override + public void removeNote(String name) { + // NOOP + } + + @Override + public void removeSessionListener(SessionListener listener) { + // NOOP + } + + @Override + public void setAuthType(String authType) { + // NOOP + } + + @Override + public void setCreationTime(long time) { + // NOOP + } + + @Override + public void setId(String id) { + this.sessionId = id; + } + + @Override + public void setId(String id, boolean notify) { + this.sessionId = id; + // Ignore notify + } + + @Override + public void setManager(Manager manager) { + // NOOP + } + + @Override + public void setMaxInactiveInterval(int interval) { + // NOOP + } + + @Override + public void setNew(boolean isNew) { + // NOOP + } + + @Override + public void setNote(String name, Object value) { + // NOOP + } + + @Override + public void setPrincipal(Principal principal) { + // NOOP + } + + @Override + public void setValid(boolean isValid) { + // NOOP + } + + @Override + public void tellChangedSessionId(String newId, String oldId, boolean notifySessionListeners, + boolean notifyContainerListeners) { + // NOOP + } + + @Override + public boolean isAttributeDistributable(String name, Object value) { + return false; + } +} diff --git a/java/org/apache/catalina/manager/HTMLManagerServlet.java b/java/org/apache/catalina/manager/HTMLManagerServlet.java new file mode 100644 index 0000000..b1d3e90 --- /dev/null +++ b/java/org/apache/catalina/manager/HTMLManagerServlet.java @@ -0,0 +1,1369 @@ +/* + * 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.catalina.manager; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Function; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.Part; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.DistributedManager; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.manager.util.SessionUtils; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.util.ServerInfo; +import org.apache.catalina.util.URLEncoder; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; + +/** + * Servlet that enables remote management of the web applications deployed within the same virtual host as this web + * application is. Normally, this functionality will be protected by a security constraint in the web application + * deployment descriptor. However, this requirement can be relaxed during testing. + *

    + * The difference between the ManagerServlet and this Servlet is that this Servlet prints out an HTML + * interface which makes it easier to administrate. + *

    + * However if you use a software that parses the output of ManagerServlet you won't be able to upgrade to + * this Servlet since the output are not in the same format ar from ManagerServlet + * + * @author Bip Thelin + * @author Malcolm Edgar + * @author Glenn L. Nielsen + * + * @see ManagerServlet + */ +public final class HTMLManagerServlet extends ManagerServlet { + + private static final long serialVersionUID = 1L; + + static final String APPLICATION_MESSAGE = "message"; + static final String APPLICATION_ERROR = "error"; + + static final String sessionsListJspPath = "/WEB-INF/jsp/sessionsList.jsp"; + static final String sessionDetailJspPath = "/WEB-INF/jsp/sessionDetail.jsp"; + static final String connectorCiphersJspPath = "/WEB-INF/jsp/connectorCiphers.jsp"; + static final String connectorCertsJspPath = "/WEB-INF/jsp/connectorCerts.jsp"; + static final String connectorTrustedCertsJspPath = "/WEB-INF/jsp/connectorTrustedCerts.jsp"; + + private boolean showProxySessions = false; + + // --------------------------------------------------------- Public Methods + + /** + * Process a GET request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + + // Identify the request parameters that we need + // By obtaining the command from the pathInfo, per-command security can + // be configured in web.xml + String command = request.getPathInfo(); + + String path = request.getParameter("path"); + ContextName cn = null; + if (path != null) { + cn = new ContextName(path, request.getParameter("version")); + } + + // Prepare our output writer to generate the response message + response.setContentType("text/html; charset=" + Constants.CHARSET); + + String message = ""; + // Process the requested command + if (command == null || command.equals("/")) { + // No command == list + } else if (command.equals("/list")) { + // List always displayed - nothing to do here + } else if (command.equals("/sessions")) { + try { + doSessions(cn, request, response, smClient); + return; + } catch (Exception e) { + log(sm.getString("htmlManagerServlet.error.sessions", cn), e); + message = smClient.getString("managerServlet.exception", e.toString()); + } + } else if (command.equals("/sslConnectorCiphers")) { + sslConnectorCiphers(request, response, smClient); + } else if (command.equals("/sslConnectorCerts")) { + sslConnectorCerts(request, response, smClient); + } else if (command.equals("/sslConnectorTrustedCerts")) { + sslConnectorTrustedCerts(request, response, smClient); + } else if (command.equals("/upload") || command.equals("/deploy") || command.equals("/reload") || + command.equals("/undeploy") || command.equals("/expire") || command.equals("/start") || + command.equals("/stop")) { + message = smClient.getString("managerServlet.postCommand", command); + } else { + message = smClient.getString("managerServlet.unknownCommand", command); + } + + list(request, response, message, smClient); + } + + /** + * Process a POST request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + + // Identify the request parameters that we need + // By obtaining the command from the pathInfo, per-command security can + // be configured in web.xml + String command = request.getPathInfo(); + + String path = request.getParameter("path"); + ContextName cn = null; + if (path != null) { + cn = new ContextName(path, request.getParameter("version")); + } + + String deployPath = request.getParameter("deployPath"); + String deployWar = request.getParameter("deployWar"); + String deployConfig = request.getParameter("deployConfig"); + ContextName deployCn = null; + if (deployPath != null && deployPath.length() > 0) { + deployCn = new ContextName(deployPath, request.getParameter("deployVersion")); + } else if (deployConfig != null && deployConfig.length() > 0) { + deployCn = ContextName.extractFromPath(deployConfig); + } else if (deployWar != null && deployWar.length() > 0) { + deployCn = ContextName.extractFromPath(deployWar); + } + + String tlsHostName = request.getParameter("tlsHostName"); + + // Prepare our output writer to generate the response message + response.setContentType("text/html; charset=" + Constants.CHARSET); + + String message = ""; + + if (command == null || command.length() == 0) { + // No command == list + // List always displayed -> do nothing + } else if (command.equals("/upload")) { + message = upload(request, smClient); + } else if (command.equals("/deploy")) { + message = deployInternal(deployConfig, deployCn, deployWar, smClient); + } else if (command.equals("/reload")) { + message = reload(cn, smClient); + } else if (command.equals("/undeploy")) { + message = undeploy(cn, smClient); + } else if (command.equals("/expire")) { + message = expireSessions(cn, request, smClient); + } else if (command.equals("/start")) { + message = start(cn, smClient); + } else if (command.equals("/stop")) { + message = stop(cn, smClient); + } else if (command.equals("/findleaks")) { + message = findleaks(smClient); + } else if (command.equals("/sslReload")) { + message = sslReload(tlsHostName, smClient); + } else { + // Try GET + doGet(request, response); + return; + } + + list(request, response, message, smClient); + } + + + protected String upload(HttpServletRequest request, StringManager smClient) { + String message = ""; + + try { + while (true) { + Part warPart = request.getPart("deployWar"); + if (warPart == null) { + message = smClient.getString("htmlManagerServlet.deployUploadNoFile"); + break; + } + String filename = warPart.getSubmittedFileName(); + if (filename == null || !filename.toLowerCase(Locale.ENGLISH).endsWith(".war")) { + message = smClient.getString("htmlManagerServlet.deployUploadNotWar", filename); + break; + } + // Get the filename if uploaded name includes a path + if (filename.lastIndexOf('\\') >= 0) { + filename = filename.substring(filename.lastIndexOf('\\') + 1); + } + if (filename.lastIndexOf('/') >= 0) { + filename = filename.substring(filename.lastIndexOf('/') + 1); + } + + // Identify the appBase of the owning Host of this Context + // (if any) + File file = new File(host.getAppBaseFile(), filename); + if (file.exists()) { + message = smClient.getString("htmlManagerServlet.deployUploadWarExists", filename); + break; + } + + ContextName cn = new ContextName(filename, true); + String name = cn.getName(); + + if (host.findChild(name) != null && !isDeployed(name)) { + message = smClient.getString("htmlManagerServlet.deployUploadInServerXml", filename); + break; + } + + if (tryAddServiced(name)) { + try { + warPart.write(file.getAbsolutePath()); + } finally { + removeServiced(name); + } + // Perform new deployment + check(name); + } else { + message = smClient.getString("managerServlet.inService", name); + } + break; + } + } catch (Exception e) { + message = smClient.getString("htmlManagerServlet.deployUploadFail", e.getMessage()); + log(message, e); + } + return message; + } + + /** + * Deploy an application for the specified path from the specified web application archive. + * + * @param config URL of the context configuration file to be deployed + * @param cn Name of the application to be deployed + * @param war URL of the web application archive to be deployed + * @param smClient internationalized strings + * + * @return message String + */ + protected String deployInternal(String config, ContextName cn, String war, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.deploy(printWriter, config, cn, war, false, smClient); + + return stringWriter.toString(); + } + + /** + * Render an HTML list of the currently active Contexts in our virtual host, and memory and server status + * information. + * + * @param request The request + * @param response The response + * @param message a message to display + * @param smClient internationalized strings + * + * @throws IOException an IO error occurred + */ + protected void list(HttpServletRequest request, HttpServletResponse response, String message, + StringManager smClient) throws IOException { + + if (debug >= 1) { + log("list: Listing contexts for virtual host '" + host.getName() + "'"); + } + + PrintWriter writer = response.getWriter(); + + Object[] args = new Object[2]; + args[0] = getServletContext().getContextPath(); + args[1] = smClient.getString("htmlManagerServlet.title"); + + // HTML Header Section + writer.print(MessageFormat.format(Constants.HTML_HEADER_SECTION, args)); + + // Body Header Section + writer.print(MessageFormat.format(Constants.BODY_HEADER_SECTION, args)); + + // Message Section + args = new Object[3]; + args[0] = smClient.getString("htmlManagerServlet.messageLabel"); + if (message == null || message.length() == 0) { + args[1] = "OK"; + } else { + args[1] = Escape.htmlElementContent(message); + } + writer.print(MessageFormat.format(Constants.MESSAGE_SECTION, args)); + + // Manager Section + args = new Object[9]; + args[0] = smClient.getString("htmlManagerServlet.manager"); + args[1] = response.encodeURL(getServletContext().getContextPath() + "/html/list"); + args[2] = smClient.getString("htmlManagerServlet.list"); + args[3] = // External link + getServletContext().getContextPath() + "/" + + smClient.getString("htmlManagerServlet.helpHtmlManagerFile"); + args[4] = smClient.getString("htmlManagerServlet.helpHtmlManager"); + args[5] = // External link + getServletContext().getContextPath() + "/" + smClient.getString("htmlManagerServlet.helpManagerFile"); + args[6] = smClient.getString("htmlManagerServlet.helpManager"); + args[7] = response.encodeURL(getServletContext().getContextPath() + "/status"); + args[8] = smClient.getString("statusServlet.title"); + writer.print(MessageFormat.format(Constants.MANAGER_SECTION, args)); + + // Apps Header Section + args = new Object[7]; + args[0] = smClient.getString("htmlManagerServlet.appsTitle"); + args[1] = smClient.getString("htmlManagerServlet.appsPath"); + args[2] = smClient.getString("htmlManagerServlet.appsVersion"); + args[3] = smClient.getString("htmlManagerServlet.appsName"); + args[4] = smClient.getString("htmlManagerServlet.appsAvailable"); + args[5] = smClient.getString("htmlManagerServlet.appsSessions"); + args[6] = smClient.getString("htmlManagerServlet.appsTasks"); + writer.print(MessageFormat.format(APPS_HEADER_SECTION, args)); + + // Apps Row Section + // Create sorted map of deployed applications by context name. + Container children[] = host.findChildren(); + String contextNames[] = new String[children.length]; + for (int i = 0; i < children.length; i++) { + contextNames[i] = children[i].getName(); + } + + Arrays.sort(contextNames); + + String appsStart = smClient.getString("htmlManagerServlet.appsStart"); + String appsStop = smClient.getString("htmlManagerServlet.appsStop"); + String appsReload = smClient.getString("htmlManagerServlet.appsReload"); + String appsUndeploy = smClient.getString("htmlManagerServlet.appsUndeploy"); + String appsExpire = smClient.getString("htmlManagerServlet.appsExpire"); + String noVersion = "" + smClient.getString("htmlManagerServlet.noVersion") + ""; + + boolean isHighlighted = true; + boolean isDeployed = true; + String highlightColor = null; + + for (String contextName : contextNames) { + Context ctxt = (Context) host.findChild(contextName); + + if (ctxt != null) { + // Bugzilla 34818, alternating row colors + isHighlighted = !isHighlighted; + if (isHighlighted) { + highlightColor = "#C3F3C3"; + } else { + highlightColor = "#FFFFFF"; + } + + String contextPath = ctxt.getPath(); + String displayPath = contextPath; + if (displayPath.equals("")) { + displayPath = "/"; + } + + StringBuilder tmp = new StringBuilder(); + tmp.append("path="); + tmp.append(URLEncoder.DEFAULT.encode(displayPath, StandardCharsets.UTF_8)); + final String webappVersion = ctxt.getWebappVersion(); + if (webappVersion != null && webappVersion.length() > 0) { + tmp.append("&version="); + tmp.append(URLEncoder.DEFAULT.encode(webappVersion, StandardCharsets.UTF_8)); + } + String pathVersion = tmp.toString(); + + try { + isDeployed = isDeployed(contextName); + } catch (Exception e) { + // Assume false on failure for safety + isDeployed = false; + } + + args = new Object[7]; + args[0] = // External link + "" + Escape.htmlElementContent(displayPath) + ""; + if (webappVersion == null || webappVersion.isEmpty()) { + args[1] = noVersion; + } else { + args[1] = Escape.htmlElementContent(webappVersion); + } + if (ctxt.getDisplayName() == null) { + args[2] = " "; + } else { + args[2] = Escape.htmlElementContent(ctxt.getDisplayName()); + } + args[3] = Boolean.valueOf(ctxt.getState().isAvailable()); + args[4] = Escape.htmlElementContent( + response.encodeURL(getServletContext().getContextPath() + "/html/sessions?" + pathVersion)); + Manager manager = ctxt.getManager(); + if (manager instanceof DistributedManager && showProxySessions) { + args[5] = Integer.valueOf(((DistributedManager) manager).getActiveSessionsFull()); + } else if (manager != null) { + args[5] = Integer.valueOf(manager.getActiveSessions()); + } else { + args[5] = Integer.valueOf(0); + } + + args[6] = highlightColor; + + writer.print(MessageFormat.format(APPS_ROW_DETAILS_SECTION, args)); + + args = new Object[14]; + args[0] = Escape.htmlElementContent( + response.encodeURL(request.getContextPath() + "/html/start?" + pathVersion)); + args[1] = appsStart; + args[2] = Escape + .htmlElementContent(response.encodeURL(request.getContextPath() + "/html/stop?" + pathVersion)); + args[3] = appsStop; + args[4] = Escape.htmlElementContent( + response.encodeURL(request.getContextPath() + "/html/reload?" + pathVersion)); + args[5] = appsReload; + args[6] = Escape.htmlElementContent( + response.encodeURL(request.getContextPath() + "/html/undeploy?" + pathVersion)); + args[7] = appsUndeploy; + args[8] = Escape.htmlElementContent( + response.encodeURL(request.getContextPath() + "/html/expire?" + pathVersion)); + args[9] = appsExpire; + args[10] = smClient.getString("htmlManagerServlet.expire.explain"); + if (manager == null) { + args[11] = smClient.getString("htmlManagerServlet.noManager"); + } else { + args[11] = Integer.valueOf(ctxt.getSessionTimeout()); + } + args[12] = smClient.getString("htmlManagerServlet.expire.unit"); + args[13] = highlightColor; + + if (ctxt.getName().equals(this.context.getName())) { + writer.print(MessageFormat.format(MANAGER_APP_ROW_BUTTON_SECTION, args)); + } else if (ctxt.getState().isAvailable() && isDeployed) { + writer.print(MessageFormat.format(STARTED_DEPLOYED_APPS_ROW_BUTTON_SECTION, args)); + } else if (ctxt.getState().isAvailable() && !isDeployed) { + writer.print(MessageFormat.format(STARTED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION, args)); + } else if (!ctxt.getState().isAvailable() && isDeployed) { + writer.print(MessageFormat.format(STOPPED_DEPLOYED_APPS_ROW_BUTTON_SECTION, args)); + } else { + writer.print(MessageFormat.format(STOPPED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION, args)); + } + + } + } + + // Deploy Section + args = new Object[8]; + args[0] = smClient.getString("htmlManagerServlet.deployTitle"); + args[1] = smClient.getString("htmlManagerServlet.deployServer"); + args[2] = response.encodeURL(getServletContext().getContextPath() + "/html/deploy"); + args[3] = smClient.getString("htmlManagerServlet.deployPath"); + args[4] = smClient.getString("htmlManagerServlet.deployVersion"); + args[5] = smClient.getString("htmlManagerServlet.deployConfig"); + args[6] = smClient.getString("htmlManagerServlet.deployWar"); + args[7] = smClient.getString("htmlManagerServlet.deployButton"); + writer.print(MessageFormat.format(DEPLOY_SECTION, args)); + + args = new Object[4]; + args[0] = smClient.getString("htmlManagerServlet.deployUpload"); + args[1] = response.encodeURL(getServletContext().getContextPath() + "/html/upload"); + args[2] = smClient.getString("htmlManagerServlet.deployUploadFile"); + args[3] = smClient.getString("htmlManagerServlet.deployButton"); + writer.print(MessageFormat.format(UPLOAD_SECTION, args)); + + // Config section + args = new Object[5]; + args[0] = smClient.getString("htmlManagerServlet.configTitle"); + args[1] = smClient.getString("htmlManagerServlet.configSslReloadTitle"); + args[2] = response.encodeURL(getServletContext().getContextPath() + "/html/sslReload"); + args[3] = smClient.getString("htmlManagerServlet.configSslHostName"); + args[4] = smClient.getString("htmlManagerServlet.configReloadButton"); + writer.print(MessageFormat.format(CONFIG_SECTION, args)); + + // Diagnostics section + args = new Object[15]; + args[0] = smClient.getString("htmlManagerServlet.diagnosticsTitle"); + args[1] = smClient.getString("htmlManagerServlet.diagnosticsLeak"); + args[2] = response.encodeURL(getServletContext().getContextPath() + "/html/findleaks"); + args[3] = smClient.getString("htmlManagerServlet.diagnosticsLeakWarning"); + args[4] = smClient.getString("htmlManagerServlet.diagnosticsLeakButton"); + args[5] = smClient.getString("htmlManagerServlet.diagnosticsSsl"); + args[6] = response.encodeURL(getServletContext().getContextPath() + "/html/sslConnectorCiphers"); + args[7] = smClient.getString("htmlManagerServlet.diagnosticsSslConnectorCipherButton"); + args[8] = smClient.getString("htmlManagerServlet.diagnosticsSslConnectorCipherText"); + args[9] = response.encodeURL(getServletContext().getContextPath() + "/html/sslConnectorCerts"); + args[10] = smClient.getString("htmlManagerServlet.diagnosticsSslConnectorCertsButton"); + args[11] = smClient.getString("htmlManagerServlet.diagnosticsSslConnectorCertsText"); + args[12] = response.encodeURL(getServletContext().getContextPath() + "/html/sslConnectorTrustedCerts"); + args[13] = smClient.getString("htmlManagerServlet.diagnosticsSslConnectorTrustedCertsButton"); + args[14] = smClient.getString("htmlManagerServlet.diagnosticsSslConnectorTrustedCertsText"); + writer.print(MessageFormat.format(DIAGNOSTICS_SECTION, args)); + + // Server Header Section + args = new Object[9]; + args[0] = smClient.getString("htmlManagerServlet.serverTitle"); + args[1] = smClient.getString("htmlManagerServlet.serverVersion"); + args[2] = smClient.getString("htmlManagerServlet.serverJVMVersion"); + args[3] = smClient.getString("htmlManagerServlet.serverJVMVendor"); + args[4] = smClient.getString("htmlManagerServlet.serverOSName"); + args[5] = smClient.getString("htmlManagerServlet.serverOSVersion"); + args[6] = smClient.getString("htmlManagerServlet.serverOSArch"); + args[7] = smClient.getString("htmlManagerServlet.serverHostname"); + args[8] = smClient.getString("htmlManagerServlet.serverIPAddress"); + writer.print(MessageFormat.format(Constants.SERVER_HEADER_SECTION, args)); + + // Server Row Section + args = new Object[8]; + args[0] = ServerInfo.getServerInfo(); + args[1] = System.getProperty("java.runtime.version"); + args[2] = System.getProperty("java.vm.vendor"); + args[3] = System.getProperty("os.name"); + args[4] = System.getProperty("os.version"); + args[5] = System.getProperty("os.arch"); + try { + InetAddress address = InetAddress.getLocalHost(); + args[6] = address.getHostName(); + args[7] = address.getHostAddress(); + } catch (UnknownHostException e) { + args[6] = "-"; + args[7] = "-"; + } + writer.print(MessageFormat.format(Constants.SERVER_ROW_SECTION, args)); + + // HTML Tail Section + writer.print(Constants.HTML_TAIL_SECTION); + + // Finish up the response + writer.flush(); + writer.close(); + } + + /** + * Reload the web application at the specified context path. + * + * @see ManagerServlet#reload(PrintWriter, ContextName, StringManager) + * + * @param cn Name of the application to be restarted + * @param smClient StringManager for the client's locale + * + * @return message String + */ + protected String reload(ContextName cn, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.reload(printWriter, cn, smClient); + + return stringWriter.toString(); + } + + /** + * Undeploy the web application at the specified context path. + * + * @see ManagerServlet#undeploy(PrintWriter, ContextName, StringManager) + * + * @param cn Name of the application to be undeployed + * @param smClient StringManager for the client's locale + * + * @return message String + */ + protected String undeploy(ContextName cn, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.undeploy(printWriter, cn, smClient); + + return stringWriter.toString(); + } + + /** + * Display session information and invoke list. + * + * @see ManagerServlet#sessions(PrintWriter, ContextName, int, StringManager) + * + * @param cn Name of the application to list session information + * @param idle Expire all sessions with idle time ≥ idle for this context + * @param smClient StringManager for the client's locale + * + * @return message String + */ + protected String sessions(ContextName cn, int idle, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.sessions(printWriter, cn, idle, smClient); + + return stringWriter.toString(); + } + + /** + * Start the web application at the specified context path. + * + * @see ManagerServlet#start(PrintWriter, ContextName, StringManager) + * + * @param cn Name of the application to be started + * @param smClient StringManager for the client's locale + * + * @return message String + */ + protected String start(ContextName cn, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.start(printWriter, cn, smClient); + + return stringWriter.toString(); + } + + /** + * Stop the web application at the specified context path. + * + * @see ManagerServlet#stop(PrintWriter, ContextName, StringManager) + * + * @param cn Name of the application to be stopped + * @param smClient StringManager for the client's locale + * + * @return message String + */ + protected String stop(ContextName cn, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.stop(printWriter, cn, smClient); + + return stringWriter.toString(); + } + + /** + * Find potential memory leaks caused by web application reload. + * + * @see ManagerServlet#findleaks(boolean, PrintWriter, StringManager) + * + * @param smClient StringManager for the client's locale + * + * @return message String + */ + protected String findleaks(StringManager smClient) { + + StringBuilder msg = new StringBuilder(); + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.findleaks(false, printWriter, smClient); + + String writerText = stringWriter.toString(); + + if (writerText.length() > 0) { + if (!writerText.startsWith("FAIL -")) { + msg.append(smClient.getString("htmlManagerServlet.findleaksList")); + } + msg.append(writerText); + } else { + msg.append(smClient.getString("htmlManagerServlet.findleaksNone")); + } + + return msg.toString(); + } + + + protected String sslReload(String tlsHostName, StringManager smClient) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.sslReload(printWriter, tlsHostName, smClient); + + return stringWriter.toString(); + } + + + protected void sslConnectorCiphers(HttpServletRequest request, HttpServletResponse response, StringManager smClient) + throws ServletException, IOException { + request.setAttribute("cipherList", getConnectorCiphers(smClient)); + getServletContext().getRequestDispatcher(connectorCiphersJspPath).forward(request, response); + } + + + protected void sslConnectorCerts(HttpServletRequest request, HttpServletResponse response, StringManager smClient) + throws ServletException, IOException { + request.setAttribute("certList", getConnectorCerts(smClient)); + getServletContext().getRequestDispatcher(connectorCertsJspPath).forward(request, response); + } + + + protected void sslConnectorTrustedCerts(HttpServletRequest request, HttpServletResponse response, + StringManager smClient) throws ServletException, IOException { + request.setAttribute("trustedCertList", getConnectorTrustedCerts(smClient)); + getServletContext().getRequestDispatcher(connectorTrustedCertsJspPath).forward(request, response); + } + + + /** + * @see jakarta.servlet.Servlet#getServletInfo() + */ + @Override + public String getServletInfo() { + return "HTMLManagerServlet, Copyright (c) 1999-2024, The Apache Software Foundation"; + } + + /** + * @see jakarta.servlet.GenericServlet#init() + */ + @Override + public void init() throws ServletException { + super.init(); + + // Set our properties from the initialization parameters + String value = null; + value = getServletConfig().getInitParameter("showProxySessions"); + showProxySessions = Boolean.parseBoolean(value); + } + + // ------------------------------------------------ Sessions administration + + /** + * Extract the expiration request parameter + * + * @param cn Name of the application from which to expire sessions + * @param req The Servlet request + * @param smClient StringManager for the client's locale + * + * @return message string + */ + protected String expireSessions(ContextName cn, HttpServletRequest req, StringManager smClient) { + int idle = -1; + String idleParam = req.getParameter("idle"); + if (idleParam != null) { + try { + idle = Integer.parseInt(idleParam); + } catch (NumberFormatException e) { + log(sm.getString("managerServlet.error.idleParam", idleParam)); + } + } + return sessions(cn, idle, smClient); + } + + /** + * Handle session operations. + * + * @param cn Name of the application for the sessions operation + * @param req The Servlet request + * @param resp The Servlet response + * @param smClient StringManager for the client's locale + * + * @throws ServletException Propagated Servlet error + * @throws IOException An IO error occurred + */ + protected void doSessions(ContextName cn, HttpServletRequest req, HttpServletResponse resp, StringManager smClient) + throws ServletException, IOException { + req.setAttribute("path", cn.getPath()); + req.setAttribute("version", cn.getVersion()); + String action = req.getParameter("action"); + if (debug >= 1) { + log("sessions: Session action '" + action + "' for web application '" + cn.getDisplayName() + "'"); + } + if ("sessionDetail".equals(action)) { + String sessionId = req.getParameter("sessionId"); + displaySessionDetailPage(req, resp, cn, sessionId, smClient); + return; + } else if ("invalidateSessions".equals(action)) { + String[] sessionIds = req.getParameterValues("sessionIds"); + int i = invalidateSessions(cn, sessionIds, smClient); + req.setAttribute(APPLICATION_MESSAGE, "" + i + " sessions invalidated."); + } else if ("removeSessionAttribute".equals(action)) { + String sessionId = req.getParameter("sessionId"); + String name = req.getParameter("attributeName"); + boolean removed = removeSessionAttribute(cn, sessionId, name, smClient); + String outMessage = removed ? "Session attribute '" + name + "' removed." : + "Session did not contain any attribute named '" + name + "'"; + req.setAttribute(APPLICATION_MESSAGE, outMessage); + displaySessionDetailPage(req, resp, cn, sessionId, smClient); + return; + } // else + displaySessionsListPage(cn, req, resp, smClient); + } + + protected List getSessionsForName(ContextName cn, StringManager smClient) { + if (cn == null || !(cn.getPath().startsWith("/") || cn.getPath().equals(""))) { + String path = null; + if (cn != null) { + path = cn.getPath(); + } + throw new IllegalArgumentException( + smClient.getString("managerServlet.invalidPath", Escape.htmlElementContent(path))); + } + + Context ctxt = (Context) host.findChild(cn.getName()); + if (null == ctxt) { + throw new IllegalArgumentException( + smClient.getString("managerServlet.noContext", Escape.htmlElementContent(cn.getDisplayName()))); + } + Manager manager = ctxt.getManager(); + List sessions = new ArrayList<>(Arrays.asList(manager.findSessions())); + if (manager instanceof DistributedManager && showProxySessions) { + // Add dummy proxy sessions + Set sessionIds = ((DistributedManager) manager).getSessionIdsFull(); + // Remove active (primary and backup) session IDs from full list + for (Session session : sessions) { + sessionIds.remove(session.getId()); + } + // Left with just proxy sessions - add them + for (String sessionId : sessionIds) { + sessions.add(new DummyProxySession(sessionId)); + } + } + return sessions; + } + + protected Session getSessionForNameAndId(ContextName cn, String id, StringManager smClient) { + + List sessions = getSessionsForName(cn, smClient); + if (sessions.isEmpty()) { + return null; + } + for (Session session : sessions) { + if (session.getId().equals(id)) { + return session; + } + } + return null; + } + + /** + * List session. + * + * @param cn Name of the application for which the sessions will be listed + * @param req The Servlet request + * @param resp The Servlet response + * @param smClient StringManager for the client's locale + * + * @throws ServletException Propagated Servlet error + * @throws IOException An IO error occurred + */ + protected void displaySessionsListPage(ContextName cn, HttpServletRequest req, HttpServletResponse resp, + StringManager smClient) throws ServletException, IOException { + List sessions = getSessionsForName(cn, smClient); + String sortBy = req.getParameter("sort"); + String orderBy = null; + if (null != sortBy && !"".equals(sortBy.trim())) { + Comparator comparator = getComparator(sortBy); + if (comparator != null) { + orderBy = req.getParameter("order"); + if ("DESC".equalsIgnoreCase(orderBy)) { + comparator = Collections.reverseOrder(comparator); + orderBy = "ASC"; + } else { + orderBy = "DESC"; + } + try { + sessions.sort(comparator); + } catch (IllegalStateException ise) { + // at least 1 of the sessions is invalidated + req.setAttribute(APPLICATION_ERROR, "Can't sort session list: one session is invalidated"); + } + } else { + log(sm.getString("htmlManagerServlet.error.sortOrder", sortBy)); + } + } + // keep sort order + req.setAttribute("sort", sortBy); + req.setAttribute("order", orderBy); + req.setAttribute("activeSessions", sessions); + // strong>NOTE - This header will be overridden + // automatically if a RequestDispatcher.forward() call is + // ultimately invoked. + resp.setHeader("Pragma", "No-cache"); // HTTP 1.0 + resp.setHeader("Cache-Control", "no-cache,no-store,max-age=0"); // HTTP 1.1 + resp.setDateHeader("Expires", 0); // 0 means now + getServletContext().getRequestDispatcher(sessionsListJspPath).include(req, resp); + } + + /** + * Display session details. + * + * @param req The Servlet request + * @param resp The Servlet response + * @param cn Name of the application for which the sessions will be listed + * @param sessionId the session id + * @param smClient StringManager for the client's locale + * + * @throws ServletException Propagated Servlet error + * @throws IOException An IO error occurred + */ + protected void displaySessionDetailPage(HttpServletRequest req, HttpServletResponse resp, ContextName cn, + String sessionId, StringManager smClient) throws ServletException, IOException { + Session session = getSessionForNameAndId(cn, sessionId, smClient); + // strong>NOTE - This header will be overridden + // automatically if a RequestDispatcher.forward() call is + // ultimately invoked. + resp.setHeader("Pragma", "No-cache"); // HTTP 1.0 + resp.setHeader("Cache-Control", "no-cache,no-store,max-age=0"); // HTTP 1.1 + resp.setDateHeader("Expires", 0); // 0 means now + req.setAttribute("currentSession", session); + getServletContext().getRequestDispatcher(resp.encodeURL(sessionDetailJspPath)).include(req, resp); + } + + /** + * Invalidate specified sessions. + * + * @param cn Name of the application for which sessions are to be invalidated + * @param sessionIds the session ids of the sessions + * @param smClient StringManager for the client's locale + * + * @return number of invalidated sessions + */ + protected int invalidateSessions(ContextName cn, String[] sessionIds, StringManager smClient) { + if (null == sessionIds) { + return 0; + } + int nbAffectedSessions = 0; + for (String sessionId : sessionIds) { + Session session = getSessionForNameAndId(cn, sessionId, smClient); + if (null == session) { + // Shouldn't happen, but let's play nice... + if (debug >= 1) { + log("Cannot invalidate null session " + sessionId); + } + continue; + } + try { + session.getSession().invalidate(); + ++nbAffectedSessions; + if (debug >= 1) { + log("Invalidating session id " + sessionId); + } + } catch (IllegalStateException ise) { + if (debug >= 1) { + log("Cannot invalidate already invalidated session id " + sessionId); + } + } + } + return nbAffectedSessions; + } + + /** + * Removes an attribute from an HttpSession + * + * @param cn Name of the application hosting the session from which the attribute is to be removed + * @param sessionId the session id + * @param attributeName the attribute name + * @param smClient StringManager for the client's locale + * + * @return true if there was an attribute removed, false otherwise + */ + protected boolean removeSessionAttribute(ContextName cn, String sessionId, String attributeName, + StringManager smClient) { + Session session = getSessionForNameAndId(cn, sessionId, smClient); + if (null == session) { + // Shouldn't happen, but let's play nice... + if (debug >= 1) { + log("Cannot remove attribute '" + attributeName + "' for null session " + sessionId); + } + return false; + } + HttpSession httpSession = session.getSession(); + boolean wasPresent = null != httpSession.getAttribute(attributeName); + try { + httpSession.removeAttribute(attributeName); + } catch (IllegalStateException ise) { + if (debug >= 1) { + log("Cannot remote attribute '" + attributeName + "' for invalidated session id " + sessionId); + } + } + return wasPresent; + } + + protected Comparator getComparator(String sortBy) { + Comparator comparator = null; + if ("CreationTime".equalsIgnoreCase(sortBy)) { + return Comparator.comparingLong(Session::getCreationTime); + + } else if ("id".equalsIgnoreCase(sortBy)) { + return comparingNullable(Session::getId); + + } else if ("LastAccessedTime".equalsIgnoreCase(sortBy)) { + return Comparator.comparingLong(Session::getLastAccessedTime); + + } else if ("MaxInactiveInterval".equalsIgnoreCase(sortBy)) { + return Comparator.comparingInt(Session::getMaxInactiveInterval); + + } else if ("new".equalsIgnoreCase(sortBy)) { + return Comparator.comparing(s -> Boolean.valueOf(s.getSession().isNew())); + + } else if ("locale".equalsIgnoreCase(sortBy)) { + return Comparator.comparing(JspHelper::guessDisplayLocaleFromSession); + + } else if ("user".equalsIgnoreCase(sortBy)) { + return comparingNullable(JspHelper::guessDisplayUserFromSession); + + } else if ("UsedTime".equalsIgnoreCase(sortBy)) { + return Comparator.comparingLong(SessionUtils::getUsedTimeForSession); + + } else if ("InactiveTime".equalsIgnoreCase(sortBy)) { + return Comparator.comparingLong(SessionUtils::getInactiveTimeForSession); + + } else if ("TTL".equalsIgnoreCase(sortBy)) { + return Comparator.comparingLong(SessionUtils::getTTLForSession); + + } + return comparator; + } + + + /* + * Like Comparator.comparing() but allows objects being compared to be null. null values are ordered before all + * other values. + */ + private static > Comparator comparingNullable( + Function keyExtractor) { + return (s1, s2) -> { + U c1 = keyExtractor.apply(s1); + U c2 = keyExtractor.apply(s2); + return c1 == null ? c2 == null ? 0 : -1 : c2 == null ? 1 : c1.compareTo(c2); + }; + } + + + // ------------------------------------------------------ Private Constants + + // These HTML sections are broken in relatively small sections, because of + // limited number of substitutions MessageFormat can process + // (maximum of 10). + + //@formatter:off + private static final String APPS_HEADER_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + private static final String APPS_ROW_DETAILS_SECTION = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n"; + + private static final String MANAGER_APP_ROW_BUTTON_SECTION = + " \n" + + "\n" + + " \n" + + "\n"; + + private static final String STARTED_DEPLOYED_APPS_ROW_BUTTON_SECTION = + " \n" + + " \n" + + " \n" + + "\n"; + + private static final String STOPPED_DEPLOYED_APPS_ROW_BUTTON_SECTION = + " \n" + + "\n\n"; + + private static final String STARTED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION = + " \n" + + " \n" + + " \n" + + "\n"; + + private static final String STOPPED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION = + " \n" + + "\n\n"; + + private static final String DEPLOY_SECTION = + "
    {0}
    {1}{2}{3}{4}{5}{6}
    {0}{1}{2}{3}" + + "{5}\n" + + " \n" + + "  {1} \n" + + "  {3} \n" + + "  {5} \n" + + "  {7} \n" + + " \n" + + "
    \n" + + "
    \n" + + " \n" + + "   {10}  {12} \n" + + " \n" + + "
    \n" + + "
    \n" + + "  {1} \n" + + "
    " + + " " + + "
    \n" + + "
    " + + " " + + "
    \n" + + "
    " + + "   " + + "
    \n" + + "
    \n" + + "
    \n" + + " \n" + + "   {10}  {12} \n" + + " \n" + + "
    \n" + + "
    \n" + + "
    " + + " " + + "
    \n" + + "  {3} \n" + + "  {5} \n" + + "
    " + + " " + + "
    \n" + + "
    \n" + + "  {1} \n" + + "
    " + + " " + + "
    \n" + + "
    " + + " " + + "
    \n" + + "  {7} \n" + + "
    \n" + + "
    \n" + + " \n" + + "   {10}  {12} \n" + + " \n" + + "
    \n" + + "
    \n" + + "
    " + + " " + + "
    \n" + + "  {3} \n" + + "  {5} \n" + + "  {7} \n" + + "
    \n" + + "
    \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n"; + + private static final String UPLOAD_SECTION = + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "
    {0}
    {1}
    \n" + + "
    \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "
    \n" + + " {3}\n" + + " \n" + + " \n" + + "
    \n" + + " {4}\n" + + " \n" + + " \n" + + "
    \n" + + " {5}\n" + + " \n" + + " \n" + + "
    \n" + + " {6}\n" + + " \n" + + " \n" + + "
    \n" + + "  \n" + + " \n" + + " \n" + + "
    \n" + + "
    \n" + + "
    {0}
    \n" + + "
    \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "
    \n" + + " {2}\n" + + " \n" + + " \n" + + "
    \n" + + "  \n" + + " \n" + + " \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "\n"; + + private static final String CONFIG_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "
    {0}
    {1}
    \n" + + "
    \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "
    \n" + + " {3}\n" + + " \n" + + " \n" + + "
    \n" + + "  \n" + + " \n" + + " \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "
    "; + + private static final String DIAGNOSTICS_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "
    {0}
    {1}
    \n" + + "
    \n" + + " \n" + + "
    \n" + + "
    \n" + + " {3}\n" + + "
    {5}
    \n" + + "
    \n" + + " \n" + + "
    \n" + + "
    \n" + + " {8}\n" + + "
    \n" + + "
    \n" + + " \n" + + "
    \n" + + "
    \n" + + " {11}\n" + + "
    \n" + + "
    \n" + + " \n" + + "
    \n" + + "
    \n" + + " {14}\n" + + "
    \n" + + "
    "; + //@formatter:on +} diff --git a/java/org/apache/catalina/manager/JMXProxyServlet.java b/java/org/apache/catalina/manager/JMXProxyServlet.java new file mode 100644 index 0000000..50e6b2b --- /dev/null +++ b/java/org/apache/catalina/manager/JMXProxyServlet.java @@ -0,0 +1,313 @@ +/* + * 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.catalina.manager; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Set; + +import javax.management.Attribute; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.OperationsException; +import javax.management.ReflectionException; +import javax.management.openmbean.CompositeData; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.mbeans.MBeanDumper; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + * This servlet will dump JMX attributes in a simple format and implement proxy services for modeler. + * + * @author Costin Manolache + */ +public class JMXProxyServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + // Constant for "no parameters" when invoking a JMX operation + // without any parameters. + private static final String[] NO_PARAMETERS = new String[0]; + + private static final StringManager sm = StringManager.getManager(JMXProxyServlet.class); + + // ----------------------------------------------------- Instance Variables + /** + * MBean server. + */ + protected transient MBeanServer mBeanServer = null; + protected transient Registry registry; + + + // --------------------------------------------------------- Public Methods + /** + * Initialize this servlet. + */ + @Override + public void init() throws ServletException { + // Retrieve the MBean server + registry = Registry.getRegistry(null, null); + mBeanServer = Registry.getRegistry(null, null).getMBeanServer(); + } + + + /** + * Process a GET request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + response.setContentType("text/plain;charset=" + Constants.CHARSET); + // Stop older versions of IE thinking they know best. We set text/plain + // in the line above for a reason. IE's behaviour is unwanted at best + // and dangerous at worst. + response.setHeader("X-Content-Type-Options", "nosniff"); + PrintWriter writer = response.getWriter(); + + if (mBeanServer == null) { + writer.println("Error - No mbean server"); + return; + } + + String qry = request.getParameter("set"); + if (qry != null) { + String name = request.getParameter("att"); + String val = request.getParameter("val"); + + setAttribute(writer, qry, name, val); + return; + } + qry = request.getParameter("get"); + if (qry != null) { + String name = request.getParameter("att"); + getAttribute(writer, qry, name, request.getParameter("key")); + return; + } + qry = request.getParameter("invoke"); + if (qry != null) { + String opName = request.getParameter("op"); + String[] params = getInvokeParameters(request.getParameter("ps")); + invokeOperation(writer, qry, opName, params); + return; + } + qry = request.getParameter("qry"); + if (qry == null) { + qry = "*:*"; + } + + listBeans(writer, qry); + } + + + public void getAttribute(PrintWriter writer, String onameStr, String att, String key) { + try { + ObjectName oname = new ObjectName(onameStr); + Object value = mBeanServer.getAttribute(oname, att); + + if (null != key && value instanceof CompositeData) { + value = ((CompositeData) value).get(key); + } + + String valueStr; + if (value != null) { + valueStr = value.toString(); + } else { + valueStr = ""; + } + + writer.print("OK - Attribute get '"); + writer.print(onameStr); + writer.print("' - "); + writer.print(att); + + if (null != key) { + writer.print(" - key '"); + writer.print(key); + writer.print("'"); + } + + writer.print(" = "); + + writer.println(MBeanDumper.escape(valueStr)); + } catch (Exception ex) { + writer.println("Error - " + ex.toString()); + ex.printStackTrace(writer); + } + } + + + public void setAttribute(PrintWriter writer, String onameStr, String att, String val) { + try { + setAttributeInternal(onameStr, att, val); + writer.println("OK - Attribute set"); + } catch (Exception ex) { + writer.println("Error - " + ex.toString()); + ex.printStackTrace(writer); + } + } + + + public void listBeans(PrintWriter writer, String qry) { + + Set names = null; + try { + names = mBeanServer.queryNames(new ObjectName(qry), null); + writer.println("OK - Number of results: " + names.size()); + writer.println(); + } catch (Exception ex) { + writer.println("Error - " + ex.toString()); + ex.printStackTrace(writer); + return; + } + + String dump = MBeanDumper.dumpBeans(mBeanServer, names); + writer.print(dump); + } + + + /** + * Determines if a type is supported by the {@link JMXProxyServlet}. + * + * @param type The type to check + * + * @return Always returns true + */ + public boolean isSupported(String type) { + return true; + } + + + private void invokeOperation(PrintWriter writer, String onameStr, String op, String[] valuesStr) { + try { + Object retVal = invokeOperationInternal(onameStr, op, valuesStr); + if (retVal != null) { + writer.println("OK - Operation " + op + " returned:"); + output("", writer, retVal); + } else { + writer.println("OK - Operation " + op + " without return value"); + } + } catch (Exception ex) { + writer.println("Error - " + ex.toString()); + ex.printStackTrace(writer); + } + } + + + /** + * Parses parameter values from a parameter string. + * + * @param paramString The string containing comma-separated operation-invocation parameters, or null if + * there are no parameters. + * + * @return An array of String parameters (empty array if paramString was null). + */ + private String[] getInvokeParameters(String paramString) { + if (paramString == null) { + return NO_PARAMETERS; + } else { + return paramString.split(","); + } + } + + + /** + * Sets an MBean attribute's value. + */ + private void setAttributeInternal(String onameStr, String attributeName, String value) + throws OperationsException, MBeanException, ReflectionException { + ObjectName oname = new ObjectName(onameStr); + String type = registry.getType(oname, attributeName); + Object valueObj = registry.convertValue(type, value); + mBeanServer.setAttribute(oname, new Attribute(attributeName, valueObj)); + } + + + /** + * Invokes an operation on an MBean. + * + * @param onameStr The name of the MBean. + * @param operation The name of the operation to invoke. + * @param parameters An array of Strings containing the parameters to the operation. They will be converted to the + * appropriate types to call the requested operation. + * + * @return The value returned by the requested operation. + */ + @SuppressWarnings("null") // parameters can't be null if signature.length > 0 + private Object invokeOperationInternal(String onameStr, String operation, String[] parameters) + throws OperationsException, MBeanException, ReflectionException { + ObjectName oname = new ObjectName(onameStr); + int paramCount = null == parameters ? 0 : parameters.length; + MBeanOperationInfo methodInfo = registry.getMethodInfo(oname, operation, paramCount); + if (null == methodInfo) { + // getMethodInfo returns null for both "object not found" and "operation not found" + MBeanInfo info = null; + try { + info = registry.getMBeanServer().getMBeanInfo(oname); + } catch (InstanceNotFoundException infe) { + throw infe; + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("jmxProxyServlet.noBeanFound", onameStr), e); + } + throw new IllegalArgumentException(sm.getString("jmxProxyServlet.noOperationOnBean", operation, + Integer.valueOf(paramCount), onameStr, info.getClassName())); + } + + MBeanParameterInfo[] signature = methodInfo.getSignature(); + String[] signatureTypes = new String[signature.length]; + Object[] values = new Object[signature.length]; + for (int i = 0; i < signature.length; i++) { + MBeanParameterInfo pi = signature[i]; + signatureTypes[i] = pi.getType(); + values[i] = registry.convertValue(pi.getType(), parameters[i]); + } + + return mBeanServer.invoke(oname, operation, values, signatureTypes); + } + + + private void output(String indent, PrintWriter writer, Object result) { + if (result instanceof Object[]) { + for (Object obj : (Object[]) result) { + output(" " + indent, writer, obj); + } + } else { + String strValue; + if (result != null) { + strValue = result.toString(); + } else { + strValue = ""; + } + writer.println(indent + strValue); + } + } +} diff --git a/java/org/apache/catalina/manager/JspHelper.java b/java/org/apache/catalina/manager/JspHelper.java new file mode 100644 index 0000000..6d70aee --- /dev/null +++ b/java/org/apache/catalina/manager/JspHelper.java @@ -0,0 +1,223 @@ +/* + * 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.catalina.manager; + +import java.text.DateFormat; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import org.apache.catalina.Session; +import org.apache.catalina.manager.util.SessionUtils; +import org.apache.tomcat.util.security.Escape; + + +/** + * Helper JavaBean for JSPs, because JSTL 1.1/EL 2.0 is too dumb to to what I need (call methods with parameters), or I + * am too dumb to use it correctly. :) + * + * @author Cédrik LIME + */ +public class JspHelper { + + private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + /** + * Public constructor, so that this class can be considered a JavaBean + */ + private JspHelper() { + super(); + } + + /** + * Try to get user locale from the session, if possible. + *

    + * IMPLEMENTATION NOTE: this method has explicit support for Tapestry 3 and Struts 1.x + * + * @param in_session Session from which the locale should be guessed + * + * @return String + */ + public static String guessDisplayLocaleFromSession(Session in_session) { + return localeToString(SessionUtils.guessLocaleFromSession(in_session)); + } + + private static String localeToString(Locale locale) { + if (locale != null) { + return escapeXml(locale.toString()); // locale.getDisplayName(); + } else { + return ""; + } + } + + /** + * Try to get user name from the session, if possible. + * + * @param in_session The Servlet session + * + * @return the user name + */ + public static String guessDisplayUserFromSession(Session in_session) { + Object user = SessionUtils.guessUserFromSession(in_session); + return escapeXml(user); + } + + + public static String getDisplayCreationTimeForSession(Session in_session) { + try { + if (in_session.getCreationTime() == 0) { + return ""; + } + DateFormat formatter = new SimpleDateFormat(DATE_TIME_FORMAT); + return formatter.format(new Date(in_session.getCreationTime())); + } catch (IllegalStateException ise) { + // ignore: invalidated session + return ""; + } + } + + public static String getDisplayLastAccessedTimeForSession(Session in_session) { + try { + if (in_session.getLastAccessedTime() == 0) { + return ""; + } + DateFormat formatter = new SimpleDateFormat(DATE_TIME_FORMAT); + return formatter.format(new Date(in_session.getLastAccessedTime())); + } catch (IllegalStateException ise) { + // ignore: invalidated session + return ""; + } + } + + public static String getDisplayUsedTimeForSession(Session in_session) { + try { + if (in_session.getCreationTime() == 0) { + return ""; + } + } catch (IllegalStateException ise) { + // ignore: invalidated session + return ""; + } + return secondsToTimeString(SessionUtils.getUsedTimeForSession(in_session) / 1000); + } + + public static String getDisplayTTLForSession(Session in_session) { + try { + if (in_session.getCreationTime() == 0) { + return ""; + } + } catch (IllegalStateException ise) { + // ignore: invalidated session + return ""; + } + return secondsToTimeString(SessionUtils.getTTLForSession(in_session) / 1000); + } + + public static String getDisplayInactiveTimeForSession(Session in_session) { + try { + if (in_session.getCreationTime() == 0) { + return ""; + } + } catch (IllegalStateException ise) { + // ignore: invalidated session + return ""; + } + return secondsToTimeString(SessionUtils.getInactiveTimeForSession(in_session) / 1000); + } + + public static String secondsToTimeString(long in_seconds) { + StringBuilder buff = new StringBuilder(9); + if (in_seconds < 0) { + buff.append('-'); + in_seconds = -in_seconds; + } + long rest = in_seconds; + long hour = rest / 3600; + rest = rest % 3600; + long minute = rest / 60; + rest = rest % 60; + long second = rest; + if (hour < 10) { + buff.append('0'); + } + buff.append(hour); + buff.append(':'); + if (minute < 10) { + buff.append('0'); + } + buff.append(minute); + buff.append(':'); + if (second < 10) { + buff.append('0'); + } + buff.append(second); + return buff.toString(); + } + + + /* + * Following copied from org.apache.taglibs.standard.tag.common.core.Util v1.1.2 + */ + + private static final int HIGHEST_SPECIAL = '>'; + private static final char[][] specialCharactersRepresentation = new char[HIGHEST_SPECIAL + 1][]; + static { + specialCharactersRepresentation['&'] = "&".toCharArray(); + specialCharactersRepresentation['<'] = "<".toCharArray(); + specialCharactersRepresentation['>'] = ">".toCharArray(); + specialCharactersRepresentation['"'] = """.toCharArray(); + specialCharactersRepresentation['\''] = "'".toCharArray(); + } + + public static String escapeXml(Object obj) { + String value = null; + try { + value = obj == null ? null : obj.toString(); + } catch (Exception e) { + // Ignore + } + return escapeXml(value); + } + + /** + * Performs the following substring replacements (to facilitate output to XML/HTML pages): + *

      + *
    • & -> &amp;
    • + *
    • < -> &lt;
    • + *
    • > -> &gt;
    • + *
    • " -> &#034;
    • + *
    • ' -> &#039;
    • + *
    + * + * @param buffer The XML to escape + * + * @return the escaped XML + */ + public static String escapeXml(String buffer) { + + if (buffer == null) { + return ""; + } + + return Escape.xml(buffer); + } + + public static String formatNumber(long number) { + return NumberFormat.getNumberInstance().format(number); + } +} diff --git a/java/org/apache/catalina/manager/LocalStrings.properties b/java/org/apache/catalina/manager/LocalStrings.properties new file mode 100644 index 0000000..148833d --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings.properties @@ -0,0 +1,198 @@ +# 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. + +htmlManagerServlet.appsAvailable=Running +htmlManagerServlet.appsExpire=Expire sessions +htmlManagerServlet.appsName=Display Name +htmlManagerServlet.appsPath=Path +htmlManagerServlet.appsReload=Reload +htmlManagerServlet.appsSessions=Sessions +htmlManagerServlet.appsStart=Start +htmlManagerServlet.appsStop=Stop +htmlManagerServlet.appsTasks=Commands +htmlManagerServlet.appsTitle=Applications +htmlManagerServlet.appsUndeploy=Undeploy +htmlManagerServlet.appsVersion=Version +htmlManagerServlet.configReloadButton=Re-read +htmlManagerServlet.configSslHostName=TLS host name (optional) +htmlManagerServlet.configSslReloadTitle=Re-read TLS configuration files +htmlManagerServlet.configTitle=Configuration +htmlManagerServlet.connectorStateAliveSocketCount=Keep alive sockets count: +htmlManagerServlet.connectorStateBytesReceived=Bytes received: +htmlManagerServlet.connectorStateBytesSent=Bytes sent: +htmlManagerServlet.connectorStateErrorCount=Error count: +htmlManagerServlet.connectorStateHint=P: Parse and prepare request S: Service F: Finishing R: Ready K: Keepalive +htmlManagerServlet.connectorStateMaxProcessingTime=Max processing time: +htmlManagerServlet.connectorStateMaxThreads=Max threads: +htmlManagerServlet.connectorStateProcessingTime=Processing time: +htmlManagerServlet.connectorStateRequestCount=Request count: +htmlManagerServlet.connectorStateTableTitleBRecv=Bytes Recv +htmlManagerServlet.connectorStateTableTitleBSent=Bytes Sent +htmlManagerServlet.connectorStateTableTitleClientAct=Client (Actual) +htmlManagerServlet.connectorStateTableTitleClientForw=Client (Forwarded) +htmlManagerServlet.connectorStateTableTitleRequest=Request +htmlManagerServlet.connectorStateTableTitleStage=Stage +htmlManagerServlet.connectorStateTableTitleTime=Time +htmlManagerServlet.connectorStateTableTitleVHost=VHost +htmlManagerServlet.connectorStateThreadBusy=Current threads busy: +htmlManagerServlet.connectorStateThreadCount=Current thread count: +htmlManagerServlet.deployButton=Deploy +htmlManagerServlet.deployConfig=XML Configuration file path: +htmlManagerServlet.deployPath=Context Path: +htmlManagerServlet.deployServer=Deploy directory or WAR file located on server +htmlManagerServlet.deployTitle=Deploy +htmlManagerServlet.deployUpload=WAR file to deploy +htmlManagerServlet.deployUploadFail=FAIL - Deploy Upload Failed, Exception: [{0}] +htmlManagerServlet.deployUploadFile=Select WAR file to upload +htmlManagerServlet.deployUploadInServerXml=FAIL - War file [{0}] cannot be uploaded if context is defined in server.xml +htmlManagerServlet.deployUploadNoFile=FAIL - File upload failed, no file +htmlManagerServlet.deployUploadNotWar=FAIL - File uploaded [{0}] must be a .war +htmlManagerServlet.deployUploadWarExists=FAIL - War file [{0}] already exists on server +htmlManagerServlet.deployVersion=Version (for parallel deployment): +htmlManagerServlet.deployWar=WAR or Directory path: +htmlManagerServlet.diagnosticsLeak=Check to see if a web application has caused a memory leak on stop, reload or undeploy +htmlManagerServlet.diagnosticsLeakButton=Find leaks +htmlManagerServlet.diagnosticsLeakWarning=This diagnostic check will trigger a full garbage collection. Use it with extreme caution on production systems. +htmlManagerServlet.diagnosticsSsl=TLS connector configuration diagnostics +htmlManagerServlet.diagnosticsSslConnectorCertsButton=Certificates +htmlManagerServlet.diagnosticsSslConnectorCertsText=List the configured TLS virtual hosts and the certificate chain for each. +htmlManagerServlet.diagnosticsSslConnectorCipherButton=Ciphers +htmlManagerServlet.diagnosticsSslConnectorCipherText=List the configured TLS virtual hosts and the ciphers for each. +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsButton=Trusted Certificates +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsText=List the configured TLS virtual hosts and the trusted certificates for each. +htmlManagerServlet.diagnosticsTitle=Diagnostics +htmlManagerServlet.error.sessions=Error processing sessions command for context [{0}] +htmlManagerServlet.error.sortOrder=Unknown sort order [{0}] +htmlManagerServlet.expire.explain=with idle ≥ +htmlManagerServlet.expire.unit=minutes +htmlManagerServlet.findleaksList=The following web applications were stopped (reloaded, undeployed), but their\n\ +classes from previous runs are still loaded in memory, thus causing a memory\n\ +leak (use a profiler to confirm):\n +htmlManagerServlet.findleaksNone=No web applications appear to have triggered a memory leak on stop, reload or undeploy. +htmlManagerServlet.helpHtmlManager=HTML Manager Help +htmlManagerServlet.helpHtmlManagerFile=../docs/html-manager-howto.html +htmlManagerServlet.helpManager=Manager Help +htmlManagerServlet.helpManagerFile=../docs/manager-howto.html +htmlManagerServlet.jvmFreeMemory=Free Memory: +htmlManagerServlet.jvmMaxMemory=Max Memory: +htmlManagerServlet.jvmTableTitleInitial=Initial +htmlManagerServlet.jvmTableTitleMaximum=Maximum +htmlManagerServlet.jvmTableTitleMemoryPool=Memory Pool +htmlManagerServlet.jvmTableTitleTotal=Total +htmlManagerServlet.jvmTableTitleType=Type +htmlManagerServlet.jvmTableTitleUsed=Used +htmlManagerServlet.jvmTotalMemory=Total Memory: +htmlManagerServlet.list=List Applications +htmlManagerServlet.manager=Manager +htmlManagerServlet.messageLabel=Message: +htmlManagerServlet.noManager=- +htmlManagerServlet.noVersion=None specified +htmlManagerServlet.osAvailableMemory=Available memory: +htmlManagerServlet.osFreePageFile=Free page file: +htmlManagerServlet.osKernelTime=Process kernel time: +htmlManagerServlet.osMemoryLoad=Memory load: +htmlManagerServlet.osPhysicalMemory=Physical memory: +htmlManagerServlet.osTotalPageFile=Total page file: +htmlManagerServlet.osUserTime=Process user time: +htmlManagerServlet.serverHostname=Hostname +htmlManagerServlet.serverIPAddress=IP Address +htmlManagerServlet.serverJVMVendor=JVM Vendor +htmlManagerServlet.serverJVMVersion=JVM Version +htmlManagerServlet.serverOSArch=OS Architecture +htmlManagerServlet.serverOSName=OS Name +htmlManagerServlet.serverOSVersion=OS Version +htmlManagerServlet.serverTitle=Server Information +htmlManagerServlet.serverVersion=Tomcat Version +htmlManagerServlet.title=Tomcat Web Application Manager + +jmxProxyServlet.noBeanFound=Cannot find MBean with object name [{0}] +jmxProxyServlet.noOperationOnBean=Cannot find operation [{0}] with [{1}] arguments on object name [{2}], which is a [{3}] + +managerServlet.alreadyContext=FAIL - Application already exists at path [{0}] +managerServlet.certsNotAvailable=Certificate information cannot be obtained from this connector at runtime +managerServlet.copyError=Could not copy configuration file from path [{0}] +managerServlet.deleteFail=FAIL - Unable to delete [{0}]. The continued presence of this file may cause problems. +managerServlet.deployFailed=FAIL - Failed to deploy application at context path [{0}] +managerServlet.deployed=OK - Deployed application at context path [{0}] +managerServlet.deployedButNotStarted=FAIL - Deployed application at context path [{0}] but context failed to start +managerServlet.error.deploy=Error deploying [{0}] +managerServlet.error.idleParam=Could not parse integer idle parameter [{0}] +managerServlet.error.jmx=JMX error +managerServlet.error.reload=Error reloading [{0}] +managerServlet.error.resources=Error displaying resources of type [{0}] +managerServlet.error.serverInfo=Error displaying server information +managerServlet.error.sessions=Error displaying sessions information for context [{0}] +managerServlet.error.start=Error starting [{0}] +managerServlet.error.stop=Error stopping [{0}] +managerServlet.error.storeConfig=Error storing configuration +managerServlet.error.storeContextConfig=Error storing configuration for context [{0}] +managerServlet.error.undeploy=Error undeploying [{0}] +managerServlet.exception=FAIL - Encountered exception [{0}] +managerServlet.findleaksFail=FAIL - Find leaks failed: Host not instance of StandardHost +managerServlet.findleaksList=OK - Found potential memory leaks in the following applications: +managerServlet.findleaksNone=OK - No memory leaks found +managerServlet.inService=FAIL - The application [{0}] is already being serviced +managerServlet.invalidCommand=FAIL - Invalid parameters supplied for command [{0}] +managerServlet.invalidPath=FAIL - Invalid context path [{0}] was specified +managerServlet.listed=OK - Listed applications for virtual host [{0}] +managerServlet.mkdirFail=FAIL - Unable to create directory [{0}] +managerServlet.noCommand=FAIL - No command was specified +managerServlet.noContext=FAIL - No context exists named [{0}] +managerServlet.noGlobal=FAIL - No global JNDI resources are available +managerServlet.noManager=FAIL - No manager exists for path [{0}] +managerServlet.noSelf=FAIL - The manager cannot reload, undeploy, stop, or undeploy itself +managerServlet.noWrapper=Container has not called setWrapper() for this servlet +managerServlet.notDeployed=FAIL - Context [{0}] is defined in server.xml and may not be undeployed +managerServlet.notSslConnector=SSL is not enabled for this connector +managerServlet.objectNameFail=FAIL - Unable to register object name [{0}] for Manager Servlet +managerServlet.postCommand=FAIL - Tried to use command [{0}] via a GET request but POST is required +managerServlet.reloaded=OK - Reloaded application at context path [{0}] +managerServlet.renameFail=FAIL - Unable to rename [{0}] to [{1}]. This may cause problems for future deployments. +managerServlet.resourcesAll=OK - Listed global resources of all types +managerServlet.resourcesType=OK - Listed global resources of type [{0}] +managerServlet.saveFail=FAIL - Configuration save failed: [{0}] +managerServlet.saved=OK - Server configuration saved +managerServlet.savedContext=OK - Context [{0}] configuration saved +managerServlet.savedContextFail=FAIL - Context [{0}] configuration save failed +managerServlet.serverInfo=OK - Server info\n\ +Tomcat Version: [{0}]\n\ +OS Name: [{1}]\n\ +OS Version: [{2}]\n\ +OS Architecture: [{3}]\n\ +JVM Version: [{4}]\n\ +JVM Vendor: [{5}] +managerServlet.sessiondefaultmax=Default maximum session inactive interval is [{0}] minutes +managerServlet.sessions=OK - Session information for application at context path [{0}] +managerServlet.sessiontimeout=Inactive for [{0}] minutes: [{1}] sessions +managerServlet.sessiontimeout.expired=Inactive for [{0}] minutes: [{1}] sessions were expired +managerServlet.sessiontimeout.unlimited=Unlimited time: [{0}] sessions +managerServlet.sslConnectorCerts=OK - Connector / Certificate Chain information +managerServlet.sslConnectorCiphers=OK - Connector / SSL Cipher information +managerServlet.sslConnectorTrustedCerts=OK - Connector / Trusted Certificate information +managerServlet.sslReload=OK - Reloaded TLS configuration for [{0}] +managerServlet.sslReloadAll=OK - Reloaded TLS configuration for all TLS virtual hosts +managerServlet.sslReloadFail=FAIL - Failed to reload TLS configuration +managerServlet.startFailed=FAIL - Application at context path [{0}] could not be started +managerServlet.started=OK - Started application at context path [{0}] +managerServlet.stopped=OK - Stopped application at context path [{0}] +managerServlet.storeConfig.noMBean=FAIL - No StoreConfig MBean registered at [{0}]. Registration is typically performed by the StoreConfigLifecycleListener. +managerServlet.threaddump=OK - JVM thread dump +managerServlet.trustedCertsNotConfigured=No trusted certificates are configured for this virtual host +managerServlet.undeployed=OK - Undeployed application at context path [{0}] +managerServlet.unknownCommand=FAIL - Unknown command [{0}] +managerServlet.vminfo=OK - VM info + +statusServlet.complete=Complete Server Status +statusServlet.title=Server Status diff --git a/java/org/apache/catalina/manager/LocalStrings_cs.properties b/java/org/apache/catalina/manager/LocalStrings_cs.properties new file mode 100644 index 0000000..faeeaa3 --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_cs.properties @@ -0,0 +1,37 @@ +# 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. + +htmlManagerServlet.appsAvailable=Běží +htmlManagerServlet.appsSessions=Relace +htmlManagerServlet.appsTasks=Příkazy +htmlManagerServlet.connectorStateProcessingTime=ÄŒas zpracování: +htmlManagerServlet.connectorStateTableTitleRequest=Dotaz +htmlManagerServlet.connectorStateTableTitleVHost=VHost +htmlManagerServlet.deployButton=Nasazení +htmlManagerServlet.deployUploadNoFile=FAIL - nahrání souboru selhalo, žádný soubor +htmlManagerServlet.deployWar=WAR nebo cesta adresáře: +htmlManagerServlet.diagnosticsSslConnectorCertsText=Vypsat nakonfigurované TLS virtual hosts a Å™etÄ›zce jejich certifikátů. +htmlManagerServlet.expire.explain=s neÄinností ≥ +htmlManagerServlet.jvmFreeMemory=Volná paměť: +htmlManagerServlet.noManager=- +htmlManagerServlet.serverIPAddress=IP adresa +htmlManagerServlet.serverJVMVersion=Verze JVM +htmlManagerServlet.serverOSName=Jméno OS + +managerServlet.deployed=OK - Aplikace nasazena na cestÄ› [{0}] +managerServlet.mkdirFail=FAIL - Nelze vytvoÅ™it adresář [{0}] +managerServlet.noGlobal=Selhání - nejsou dostupné žádné globalní JNDI zdroje +managerServlet.resourcesAll=OK - Vypsány globální zdroje vÅ¡ech typů +managerServlet.trustedCertsNotConfigured=Žádný důveryhodný certifikát nebyl nakonfigurován pro virtuálního hosta diff --git a/java/org/apache/catalina/manager/LocalStrings_de.properties b/java/org/apache/catalina/manager/LocalStrings_de.properties new file mode 100644 index 0000000..bbe8e22 --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_de.properties @@ -0,0 +1,97 @@ +# 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. + +htmlManagerServlet.appsAvailable=gestartet +htmlManagerServlet.appsExpire=Lösche Sitzungen +htmlManagerServlet.appsName=Anzeigename +htmlManagerServlet.appsPath=Kontext Pfad +htmlManagerServlet.appsReload=Neu laden +htmlManagerServlet.appsSessions=Sitzungen +htmlManagerServlet.appsStart=Start +htmlManagerServlet.appsStop=Stop +htmlManagerServlet.appsTasks=Kommandos +htmlManagerServlet.appsTitle=Anwendungen +htmlManagerServlet.appsUndeploy=Entfernen +htmlManagerServlet.connectorStateProcessingTime=Verarbeitungszeit: +htmlManagerServlet.connectorStateTableTitleRequest=Anfrage +htmlManagerServlet.connectorStateTableTitleVHost=VHost +htmlManagerServlet.deployButton=Installieren +htmlManagerServlet.deployConfig=XML Konfigurationsdatei URL: +htmlManagerServlet.deployPath=Kontext Pfad (optional): +htmlManagerServlet.deployServer=Verzeichnis oder WAR Datei auf Server installieren +htmlManagerServlet.deployTitle=Installieren +htmlManagerServlet.deployUpload=Lokale WAR Datei zur Installation hochladen +htmlManagerServlet.deployUploadFail=FEHLER - Hochladen zur Installation fehlgeschlagen, Ausnahme: [{0}] +htmlManagerServlet.deployUploadFile=WAR Datei auswählen +htmlManagerServlet.deployUploadNoFile=FEHLER - Hochladen fehlgeschlagen, keine Datei vorhanden +htmlManagerServlet.deployUploadNotWar=FEHLER - Hochgeladene Datei [{0}] muss ein .war sein +htmlManagerServlet.deployUploadWarExists=FEHLER - WAR Datei [{0}] existiert bereits auf Server +htmlManagerServlet.deployWar=WAR oder Verzeichnis URL: +htmlManagerServlet.diagnosticsSslConnectorCertsButton=Zertifikate +htmlManagerServlet.diagnosticsSslConnectorCertsText=Liste die konfigurierten TLS Virtual Hosts und deren Zertifikats-Ketten. +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsButton=Vertrauenswürdige Zertifikate +htmlManagerServlet.expire.explain=mit Inaktivität ≥ +htmlManagerServlet.expire.unit=Minuten +htmlManagerServlet.helpHtmlManager=Hilfeseite HTML Manager (englisch) +htmlManagerServlet.helpHtmlManagerFile=../docs/html-manager-howto.html +htmlManagerServlet.helpManager=Hilfeseite Manager (englisch) +htmlManagerServlet.helpManagerFile=../docs/manager-howto.html +htmlManagerServlet.jvmFreeMemory=Freier Speicher: +htmlManagerServlet.jvmTableTitleMaximum=Maximum +htmlManagerServlet.jvmTableTitleMemoryPool=Memory Pool +htmlManagerServlet.jvmTableTitleType=Typ +htmlManagerServlet.list=Anwendungen auflisten +htmlManagerServlet.manager=Manager +htmlManagerServlet.messageLabel=Nachricht: +htmlManagerServlet.noManager=- +htmlManagerServlet.serverIPAddress=IP-Adresse +htmlManagerServlet.serverJVMVendor=JVM Hersteller +htmlManagerServlet.serverJVMVersion=JVM Version +htmlManagerServlet.serverOSArch=OS Architektur +htmlManagerServlet.serverOSName=OS Name +htmlManagerServlet.serverOSVersion=OS Version +htmlManagerServlet.serverTitle=Server Informationen +htmlManagerServlet.serverVersion=Tomcat Version +htmlManagerServlet.title=Tomcat Webanwendungs-Manager + +managerServlet.alreadyContext=FEHLER - Anwendung existiert bereits für Kontext Pfad [{0}] +managerServlet.deployed=OK - Anwendung mit Kontext Pfad [{0}] installiert +managerServlet.exception=FEHLER - Ausnahme aufgetreten [{0}] +managerServlet.invalidPath=FEHLER - Ungültiger Kontext Pfad [{0}] angegeben +managerServlet.listed=OK - Auflistung der Webanwendungen für virtuellen Server [{0}] +managerServlet.mkdirFail=Fehler - Das Verzeichnis [{0}] konnte nicht erstellt werden. +managerServlet.noCommand=FEHLER - Es wurde kein Kommando angegeben +managerServlet.noContext=FEHLER - Es existiert kein Kontext für Pfad [{0}] +managerServlet.noGlobal=FEHLER - Keine globalen JNDI Ressourcen verfügbar +managerServlet.noSelf=FEHLER - Manager-Kommandos können nicht auf die Manager-Anwendung selbst angewendet werden +managerServlet.noWrapper=Container hat setWrapper() für dieses Servlet nicht aufgerufen +managerServlet.reloaded=OK - Anwendung mit Kontext Pfad [{0}] neu geladen +managerServlet.resourcesAll=OK - Auflistung globaler Ressourcen (alle Typen) +managerServlet.resourcesType=OK - Auflistung globaler Ressourcen von Typ [{0}] +managerServlet.saveFail=FEHLER - Speichern der Konfiguration fehlgeschlagen: [{0}] +managerServlet.sessiondefaultmax=Voreingestellter Sitzungsablauf nach maximal [{0}] Minuten Inaktivität +managerServlet.sessions=OK - Sitzungs-Informationen für Anwendung mit Kontext Pfad [{0}] +managerServlet.sessiontimeout=Inaktiv für [{0}] Minuten: [{1}] Sitzungen +managerServlet.sessiontimeout.expired=Inaktiv für [{0}] Minuten: [{1}] Sitzungen sind abgelaufen +managerServlet.sessiontimeout.unlimited=unlimited Minuten: [{0}] Sitzungen +managerServlet.startFailed=FEHLER - Anwendung mit Kontext Pfad [{0}] konnte nicht gestartet werden +managerServlet.started=OK - Anwendung mit Kontext Pfad [{0}] gestartet +managerServlet.stopped=OK - Anwendung mit Kontext Pfad [{0}] gestoppt +managerServlet.trustedCertsNotConfigured=Für diesen virtuellen Host wurden keine vertrauenswürdige Zertifikate konfiguriert +managerServlet.undeployed=OK - Anwendung mit Kontext Pfad [{0}] entfernt +managerServlet.unknownCommand=FEHLER - Unbekanntes Kommando [{0}] + +statusServlet.complete=Ausführlicher Server Status +statusServlet.title=Server Status diff --git a/java/org/apache/catalina/manager/LocalStrings_es.properties b/java/org/apache/catalina/manager/LocalStrings_es.properties new file mode 100644 index 0000000..5fabfeb --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_es.properties @@ -0,0 +1,121 @@ +# 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. + +htmlManagerServlet.appsAvailable=Ejecutándose +htmlManagerServlet.appsExpire=Expirar sesiones +htmlManagerServlet.appsName=Nombre a Mostrar +htmlManagerServlet.appsPath=Ruta +htmlManagerServlet.appsReload=Recargar +htmlManagerServlet.appsSessions=Sesiones +htmlManagerServlet.appsStart=Arrancar +htmlManagerServlet.appsStop=Parar +htmlManagerServlet.appsTasks=Comandos +htmlManagerServlet.appsTitle=Aplicaciones +htmlManagerServlet.appsUndeploy=Replegar +htmlManagerServlet.appsVersion=Versión +htmlManagerServlet.connectorStateProcessingTime=Tiempo de procesamiento: +htmlManagerServlet.connectorStateTableTitleRequest=Solicitud +htmlManagerServlet.connectorStateTableTitleVHost=VHost +htmlManagerServlet.connectorStateThreadBusy=Hilos ocupados actualmente: +htmlManagerServlet.deployButton=Desplegar +htmlManagerServlet.deployConfig=URL de archivo de Configuración XML: +htmlManagerServlet.deployPath=Trayectoria de Contexto (opcional): +htmlManagerServlet.deployServer=Desplegar directorio o archivo WAR localizado en servidor +htmlManagerServlet.deployTitle=Desplegar +htmlManagerServlet.deployUpload=Archivo WAR a desplegar +htmlManagerServlet.deployUploadFail=FALLO - Falló Carga de Despliegue, Excepción: [{0}] +htmlManagerServlet.deployUploadFile=Seleccione archivo WAR a cargar +htmlManagerServlet.deployUploadInServerXml=FALLO - El fichero war [{0}] no se puede cargar si se define el contexto en server.xml +htmlManagerServlet.deployUploadNoFile=FALLO - Falló la carga del fichero, no hay fichero +htmlManagerServlet.deployUploadNotWar=FALLO - El fichero cargado [{0}] debe de ser un .war +htmlManagerServlet.deployUploadWarExists=FALLO - El fichero war [{0}] ya existe en el servidor +htmlManagerServlet.deployWar=URL de WAR o Directorio: +htmlManagerServlet.diagnosticsLeak=Revisa a ver si una aplicación web ha causado fallos de memoria al parar, recargar o replegarse. +htmlManagerServlet.diagnosticsLeakButton=Halla fallos de memoria +htmlManagerServlet.diagnosticsLeakWarning=Este chequeo de diagnóstico disparará una colección completa de basura. Utilízalo con extremo cuidado en sistemas en producción. +htmlManagerServlet.diagnosticsSslConnectorCertsText=Lista los virtual hosts configurados con TLS y la cadena de ceritifaco para cada uno de ellos.\n +htmlManagerServlet.diagnosticsSslConnectorCipherButton=Cifrados +htmlManagerServlet.diagnosticsTitle=Diagnósticos +htmlManagerServlet.expire.explain=sin trabajar ≥ +htmlManagerServlet.expire.unit=minutos +htmlManagerServlet.findleaksList=Las siguientes aplicaciones web fueron paradas (recargadas, replegadas), pero sus clases de las ejecuciones previas aún se encuentran en memoria, causando así un fallo de memoria (usa un perfilador para confirmarlo): +htmlManagerServlet.findleaksNone=No parece haber aplicaciones web que hayan disparado un fallo de memoria al ser paradas, recargadas o replegadas. +htmlManagerServlet.helpHtmlManager=Ayuda HTML de Gestor +htmlManagerServlet.helpHtmlManagerFile=../docs/html-manager-howto.html +htmlManagerServlet.helpManager=Ayuda de Gestor +htmlManagerServlet.helpManagerFile=../docs/manager-howto.html +htmlManagerServlet.jvmFreeMemory=Memoria disponible: +htmlManagerServlet.jvmTableTitleMemoryPool=Pool de Memoria +htmlManagerServlet.list=Listar Aplicaciones +htmlManagerServlet.manager=Gestor +htmlManagerServlet.messageLabel=Mensaje: +htmlManagerServlet.noManager=- +htmlManagerServlet.noVersion=Ninguno especificado +htmlManagerServlet.osFreePageFile=Archivo de página gratis: +htmlManagerServlet.osTotalPageFile=Tamaño total de archivo de página: +htmlManagerServlet.serverHostname=NombreDeMáquina +htmlManagerServlet.serverIPAddress=Dirección IP +htmlManagerServlet.serverJVMVendor=Vendedor JVM +htmlManagerServlet.serverJVMVersion=Versión JVM +htmlManagerServlet.serverOSArch=Arquitectura de SO +htmlManagerServlet.serverOSName=Nombre del SO +htmlManagerServlet.serverOSVersion=Versión de SO +htmlManagerServlet.serverTitle=Información de Servidor +htmlManagerServlet.serverVersion=Versión de Tomcat +htmlManagerServlet.title=Gestor de Aplicaciones Web de Tomcat + +managerServlet.alreadyContext=FALLO - Ya existe la aplicación en la trayectoria [{0}] +managerServlet.deleteFail=FALLO - No pude borrar [{0}]. La presencia continua de este fichero puede causar problemas. +managerServlet.deployFailed=FALLO - No pude desplegar la aplicación en ruta de contexto [{0}] +managerServlet.deployed=OK - Desplegada aplicación en trayectoria de contexto [{0}] +managerServlet.deployedButNotStarted=FALLO - Apliación desplegada en la ruta de contexto [{0}], pero el contexto no pudo arrancar +managerServlet.exception=FALLO - Encontrada excepción [{0}] +managerServlet.findleaksFail=FALLO - Ha fallado la búsqueda de fallos: La Máquina no es una instancia de StandardHost +managerServlet.findleaksList=OK - Hallados fallos potenciales de memoria en las siguientes aplicaciones: +managerServlet.findleaksNone=OK - No se han hallado fallos de memoria +managerServlet.invalidCommand=Fallo - Se proveiron parámetros inválidos para el comando [{0}] +managerServlet.invalidPath=FALLO - Se ha especificado una trayectoria inválida de contexto [{0}] +managerServlet.listed=OK - Aplicaciones listadas para máquina virtual [{0}] +managerServlet.mkdirFail=FALLO - No pude crear directorio [{0}] +managerServlet.noCommand=FALLO - No se ha especificado comando +managerServlet.noContext=FALLO - No existe contexto para trayectoria [{0}] +managerServlet.noGlobal=FALLO - No hay disponibles recursos globales JNDI +managerServlet.noManager=FALLO - No existe gestor para ruta [{0}] +managerServlet.noSelf=FALLO - El gestor no puede recargarse, replegarse, pararse o replegarse a sí mismo +managerServlet.noWrapper=El Contenedor no ha llamado a setWrapper() para este servlet +managerServlet.notDeployed=FALLO - El contexto [{0}] está definido en server.xml y puede que no esté desplegado +managerServlet.objectNameFail=FALLO - No pude registrar objeto de nombre [{0}] para Gestor de Servlet +managerServlet.postCommand=FALLO - Intenté usar el comando [{0}] vía un requerimiento GET pero se necesita POST +managerServlet.reloaded=OK - Recargada aplicación en trayectoria de contexto [{0}] +managerServlet.resourcesAll=OK - Listados recursos globales de todos los tipos +managerServlet.resourcesType=OK - Listados recursos globales de tipo [{0}] +managerServlet.saveFail=FAIL - Fallo al guardar la configuración: [{0}] +managerServlet.saved=OK - Configuración de Servidor guardada +managerServlet.savedContext=OK - Configuración de Contexto [{0}] guardada +managerServlet.sessiondefaultmax=Intervalo máximo por defecto de sesión inactiva [{0}] minutos +managerServlet.sessions=OK - Información de sesión para aplicación en trayectoria de contexto [{0}] +managerServlet.sessiontimeout=[{0}] minutos: [{1}] sesiones +managerServlet.sessiontimeout.expired=[{0}] minutos: expired [{1}] sesiones +managerServlet.sessiontimeout.unlimited=unlimited minutos: [{0}] sesiones +managerServlet.sslConnectorCiphers=OK - Connector / Información cifrada con SSL +managerServlet.startFailed=FALLO - No se pudo arrancar la aplicación en trayectoria de contexto [{0}] +managerServlet.started=OK - Arrancada aplicación en trayectoria de contexto [{0}] +managerServlet.stopped=OK - Parada aplicación en trayectoria de contexto [{0}] +managerServlet.trustedCertsNotConfigured=No se configuraron certificados confiables para este virtual host +managerServlet.undeployed=OK - Replegada aplicación en trayectoria de contexto [{0}] +managerServlet.unknownCommand=FALLO - Comando desconocido [{0}] + +statusServlet.complete=Estado Completo de Servidor +statusServlet.title=Estado de Servidor diff --git a/java/org/apache/catalina/manager/LocalStrings_fr.properties b/java/org/apache/catalina/manager/LocalStrings_fr.properties new file mode 100644 index 0000000..d8c506c --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_fr.properties @@ -0,0 +1,196 @@ +# 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. + +htmlManagerServlet.appsAvailable=Fonctionnelle +htmlManagerServlet.appsExpire=Expirer les sessions +htmlManagerServlet.appsName=Nom d'affichage +htmlManagerServlet.appsPath=Chemin +htmlManagerServlet.appsReload=Recharger +htmlManagerServlet.appsSessions=Sessions +htmlManagerServlet.appsStart=Démarrer +htmlManagerServlet.appsStop=Arrêter +htmlManagerServlet.appsTasks=Commandes +htmlManagerServlet.appsTitle=Applications +htmlManagerServlet.appsUndeploy=Retirer +htmlManagerServlet.appsVersion=Version +htmlManagerServlet.configReloadButton=Relire +htmlManagerServlet.configSslHostName=Nom d'hôte TLS (optionnel) +htmlManagerServlet.configSslReloadTitle=Relisant les fichiers de configuration TLS +htmlManagerServlet.configTitle=Configuration +htmlManagerServlet.connectorStateAliveSocketCount=Nombre de sockets connectés : +htmlManagerServlet.connectorStateBytesReceived=Octets reçus : +htmlManagerServlet.connectorStateBytesSent=Octets envoyés : +htmlManagerServlet.connectorStateErrorCount=Nombre d'erreurs : +htmlManagerServlet.connectorStateHint=P : Traitement et préparation de la requête S : Service F : Fin R : Prêt K : Connecté +htmlManagerServlet.connectorStateMaxProcessingTime=Temps de traitement maximal : +htmlManagerServlet.connectorStateMaxThreads=Nombre de threads maximum : +htmlManagerServlet.connectorStateProcessingTime=Temps de traitement : +htmlManagerServlet.connectorStateRequestCount=Nombre de requêtes : +htmlManagerServlet.connectorStateTableTitleBRecv=Octets Reçus +htmlManagerServlet.connectorStateTableTitleBSent=Octets Envoyés +htmlManagerServlet.connectorStateTableTitleClientAct=Client (Réel) +htmlManagerServlet.connectorStateTableTitleClientForw=Client (Forwardé) +htmlManagerServlet.connectorStateTableTitleRequest=Requête +htmlManagerServlet.connectorStateTableTitleStage=Etape +htmlManagerServlet.connectorStateTableTitleTime=Temps +htmlManagerServlet.connectorStateTableTitleVHost=Hôte virtuel (VHost) +htmlManagerServlet.connectorStateThreadBusy=Nombre de threads utilisés : +htmlManagerServlet.connectorStateThreadCount=Nombre de threads actuel : +htmlManagerServlet.deployButton=Deployer +htmlManagerServlet.deployConfig=URL du fichier XML de configuration : +htmlManagerServlet.deployPath=Chemin de context (requis) : +htmlManagerServlet.deployServer=Emplacement du répertoire ou fichier WAR de déploiement sur le serveur +htmlManagerServlet.deployTitle=Deployer +htmlManagerServlet.deployUpload=Fichier WAR à déployer +htmlManagerServlet.deployUploadFail=ECHEC - Téléversement pour déploiement a échoué, exception : [{0}] +htmlManagerServlet.deployUploadFile=Choisir le fichier WAR à téléverser +htmlManagerServlet.deployUploadInServerXml=ECHEC - Fichier WAR [{0}] ne peut être téléversé lorsque le contexte est défini dans server.xml +htmlManagerServlet.deployUploadNoFile=ECHEC - Téléversement a échoué, aucun fichier +htmlManagerServlet.deployUploadNotWar=ECHEC - Fichier à téléverser, [{0}], doit être un .war +htmlManagerServlet.deployUploadWarExists=ECHEC - Fichier War [{0}] déjà existant sur le serveur +htmlManagerServlet.deployVersion=Version (pour les déploiements en parallèle) : +htmlManagerServlet.deployWar=URL vers WAR ou répertoire : +htmlManagerServlet.diagnosticsLeak=Vérifiez si une application web a causé une fuite de mémoire lors de son arrêt, rechargement ou déchargement +htmlManagerServlet.diagnosticsLeakButton=Trouver des fuites +htmlManagerServlet.diagnosticsLeakWarning=Le diagnostic doit démarrer une collecte complète de la mémoire, utilisez le avec précaution dans un environnement de production +htmlManagerServlet.diagnosticsSsl=Diagnostics de configuration TLS du connecteur +htmlManagerServlet.diagnosticsSslConnectorCertsButton=Certificats +htmlManagerServlet.diagnosticsSslConnectorCertsText=Entrez la liste des hôtes virtuels TLS et la chaîne de certificats pour chacun. +htmlManagerServlet.diagnosticsSslConnectorCipherButton=Chiffres +htmlManagerServlet.diagnosticsSslConnectorCipherText=Lister les hôtes virtuels TLS configurés et les chiffres utilisés par chacun +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsButton=Certificats de confiance +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsText=Lister les hôtes virtuels TLS configurés et les certificats de confiance utilisés par chacun +htmlManagerServlet.diagnosticsTitle=Diagnostics +htmlManagerServlet.error.sessions=Erreur lors du traitement de la commande sessions pour le contexte [{0}] +htmlManagerServlet.error.sortOrder=Ordre de tri [{0}] inconnu +htmlManagerServlet.expire.explain=inactives depuis ≥ +htmlManagerServlet.expire.unit=minutes +htmlManagerServlet.findleaksList=Les applications suivantes ont été arrêtées (redémarrées, retirées), mais certaines de leurs classes sont toujours présentes en mémoire, cela pourrait donc causer une fuite de mémoire (utiliser un profileur pour le confirmer) : +htmlManagerServlet.findleaksNone=Aucune application n'a apparemment causé de fuite de mémoire en l'arrêtant, en la rechargeant ou en la retirant +htmlManagerServlet.helpHtmlManager=Aide HTML Gestionnaire +htmlManagerServlet.helpHtmlManagerFile=../docs/html-manager-howto.html +htmlManagerServlet.helpManager=Aide Gestionnaire +htmlManagerServlet.helpManagerFile=../docs/manager-howto.html +htmlManagerServlet.jvmFreeMemory=Mémoire disponible : +htmlManagerServlet.jvmMaxMemory=Mémoire maximale : +htmlManagerServlet.jvmTableTitleInitial=Initial +htmlManagerServlet.jvmTableTitleMaximum=Maximum +htmlManagerServlet.jvmTableTitleMemoryPool=Pool mémoire +htmlManagerServlet.jvmTableTitleTotal=Total +htmlManagerServlet.jvmTableTitleType=Type +htmlManagerServlet.jvmTableTitleUsed=Utilisé +htmlManagerServlet.jvmTotalMemory=Mémoire totale : +htmlManagerServlet.list=Lister les applications +htmlManagerServlet.manager=Gestionnaire +htmlManagerServlet.messageLabel=Message : +htmlManagerServlet.noManager=- +htmlManagerServlet.noVersion=Aucun spécifié +htmlManagerServlet.osAvailableMemory=Mémoire disponible : +htmlManagerServlet.osFreePageFile=Fichier de page disponible : +htmlManagerServlet.osKernelTime=Temps noyau du processus : +htmlManagerServlet.osMemoryLoad=Charge mémoire : +htmlManagerServlet.osPhysicalMemory=Mémoire physique : +htmlManagerServlet.osTotalPageFile=Fichier de page total : +htmlManagerServlet.osUserTime=Temps utilisateur du processus : +htmlManagerServlet.serverHostname=Nom d'hôte +htmlManagerServlet.serverIPAddress=Adresse IP +htmlManagerServlet.serverJVMVendor=Fournisseur de la JVM +htmlManagerServlet.serverJVMVersion=Version de la JVM +htmlManagerServlet.serverOSArch=Architecture d'OS +htmlManagerServlet.serverOSName=Nom d'OS +htmlManagerServlet.serverOSVersion=Version d'OS +htmlManagerServlet.serverTitle=Serveur +htmlManagerServlet.serverVersion=Version de serveur +htmlManagerServlet.title=Gestionnaire d'applications WEB Tomcat + +jmxProxyServlet.noBeanFound=Impossible de trouver de MBean avec le nom d''objet [{0}] +jmxProxyServlet.noOperationOnBean=Impossible de trouver l''opération [{0}] avec [{1}] arguments sur le nom d''objet [{2}], qui est un [{3}] + +managerServlet.alreadyContext=ECHEC - l''application existe déjà dans le chemin [{0}] +managerServlet.certsNotAvailable=L'information sur les certificats ne peut pas être obtenu de ce connecteur au cours de son exécution +managerServlet.copyError=Impossible de copier le fichier de configuration à partir du chemin [{0}] +managerServlet.deleteFail=ECHEC - Impossible de supprimer [{0}], ce qui pourrait causer des problèmes +managerServlet.deployFailed=ECHEC - Echec au déploiement de l''application pour le chemin de contexte [{0}] +managerServlet.deployed=OK - Application déployée pour le chemin de contexte [{0}] +managerServlet.deployedButNotStarted=ECHEC - Application déployée pour le chemin de contexte [{0}] mais le démarrage du contexte a échoué +managerServlet.error.deploy=Erreur lors du déploiement de [{0}] +managerServlet.error.idleParam=Impossible d''analyser le paramètre de temps d''attente maximum [{0}] qui devrait être un nombre entier +managerServlet.error.jmx=Erreur JMX +managerServlet.error.reload=Erreur lors du rechargement de [{0}] +managerServlet.error.resources=Erreur lors de l''affichage des ressources de type [{0}] +managerServlet.error.serverInfo=Erreur lors de l'affichage des informations sur le serveur +managerServlet.error.sessions=Erreur lors de l''affichage des informations sur les sessions pour le contexte [{0}] +managerServlet.error.start=Erreur lors du démarrage de [{0}] +managerServlet.error.stop=Erreur lors de l''arrêt de [{0}] +managerServlet.error.storeConfig=Erreur lors de l'enregistrement de la configuration +managerServlet.error.storeContextConfig=Erreur lors de l''enregistrement de la configuration du contexte [{0}] +managerServlet.error.undeploy=Erreur lors du retrait de [{0}] +managerServlet.exception=ECHEC - L''exception [{0}] a été rencontrée +managerServlet.findleaksFail=ECHEC - Echec de la recherche de fuites, car l'hôte n'est pas un StandardHost +managerServlet.findleaksList=OK - De possibles fuites de mémoire ont été trouvées dans les applications suivantes : +managerServlet.findleaksNone=OK - Aucune fuite de mémoire trouvée +managerServlet.inService=ECHEC - Une opération de gestion est déjà en cours pour l''application [{0}] +managerServlet.invalidCommand=ECHEC - Des paramètres incorrects ont été fournis pour la commande [{0}] +managerServlet.invalidPath=ECHEC - Un chemin de contexte invalide [{0}] a été spécifié +managerServlet.listed=OK - Applications listées pour l''hôte virtuel (virtual host) [{0}] +managerServlet.mkdirFail=ECHEC - Le répertoire [{0}] n''a pas pu être créé +managerServlet.noCommand=ECHEC - Aucune commande n'a été spécifiée +managerServlet.noContext=ECHEC - Aucun contexte n''existe pour le chemin [{0}] +managerServlet.noGlobal=ECHEC - Aucune ressource JNDI globale n'est disponible +managerServlet.noManager=ECHEC - Aucun gestionnaire n''existe pour le chemin [{0}] +managerServlet.noSelf=ECHEC - Le gestionnaire ne peut se recharger, se retirer, s'arrêter, ou se déployer lui-même +managerServlet.noWrapper=Le conteneur n'a pas appelé "setWrapper()" pour cette servlet +managerServlet.notDeployed=ECHEC - Le contexte [{0}] est défini dans server.xml et ne peut être retiré +managerServlet.notSslConnector=SSL n'est pas activé pour ce connecteur +managerServlet.objectNameFail=ECHEC - Le nom d''objet [{0}] n''a pas pu être enregistré pour le Servlet de Gestion +managerServlet.postCommand=ECHEC - Tentative d''utilisation de la commande [{0}] via une requête GET, mais POST est requis +managerServlet.reloaded=OK - L''application associée au chemin de contexte [{0}] a été rechargée +managerServlet.renameFail=ECHEC - N''a pas pu renommer [{0}] vers [{1}], cela pourrait causer des problèmes pour de prochains déploiements +managerServlet.resourcesAll=OK - Liste des ressources globales de tout type +managerServlet.resourcesType=OK - Liste des ressources globales de type [{0}] +managerServlet.saveFail=ECHEC - La sauvegarde de la configuration a échoué : [{0}] +managerServlet.saved=OK - Configuration serveur sauvegardée +managerServlet.savedContext=OK - Configuration du contexte [{0}] sauvegardée +managerServlet.savedContextFail=ECHEC - L''enregistrement de la configuration du Contexte [{0}] a échoué +managerServlet.serverInfo=OK - Informations sur le serveur\n\ +Version de Tomcat : [{0}]\n\ +Nom de l''OS : [{1}]\n\ +Version de l''OS : [{2}]\n\ +Architecture de l''OS : [{3}]\n\ +Version de la JVM : [{4}]\n\ +Fournisseur de la JVM : [{5}] +managerServlet.sessiondefaultmax=La valeur par défaut du délai d''inactivité maximum d''une sessions est de [{0}] minutes +managerServlet.sessions=OK - Information de session pour l''application au chemin de contexte [{0}] +managerServlet.sessiontimeout=Inactivité pendant [{0}] minutes : [{1}] sessions +managerServlet.sessiontimeout.expired=Inactivité pendant [{0}] minutes : [{1}] sessions ont été expirées +managerServlet.sessiontimeout.unlimited=Délai illimité : [{0}] sessions +managerServlet.sslConnectorCerts=OK - Information sur le connecteur et la chaîne de certificats +managerServlet.sslConnectorCiphers=OK - Information sur le connecteur et les chiffres SSL +managerServlet.sslConnectorTrustedCerts=OK - Information sur le Connecteur / Certificat de confiance +managerServlet.sslReload=OK - La configuration TLS de [{0}] a été rechargée +managerServlet.sslReloadAll=OK - Configuration TLS rechargée pour tous les hôtes virtuels TLS +managerServlet.sslReloadFail=ECHEC - Echec lors du rechargement de la configuration TLS +managerServlet.startFailed=ECHEC - L''application pour le chemin de contexte [{0}] n''a pas pu être démarrée +managerServlet.started=OK - Application démarrée pour le chemin de contexte [{0}] +managerServlet.stopped=OK - Application arrêtée pour le chemin de contexte [{0}] +managerServlet.storeConfig.noMBean=ECHEC - Pas de mbean StoreConfig enregistré à [{0}], l''enregistrement est effectué par StoreConfigLifecycleListener +managerServlet.threaddump=OK - Etat des threads de la JVM +managerServlet.trustedCertsNotConfigured=Aucun certificat de confiance n'est configuré pour cet hôte virtuel +managerServlet.undeployed=OK - Application non déployée pour le chemin de contexte [{0}] +managerServlet.unknownCommand=ECHEC - Commande inconnue [{0}] +managerServlet.vminfo=OK - Informations sur la VM + +statusServlet.complete=Etat complet du serveur +statusServlet.title=Etat du serveur diff --git a/java/org/apache/catalina/manager/LocalStrings_ja.properties b/java/org/apache/catalina/manager/LocalStrings_ja.properties new file mode 100644 index 0000000..8cb2f1f --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_ja.properties @@ -0,0 +1,198 @@ +# 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. + +htmlManagerServlet.appsAvailable=実行中 +htmlManagerServlet.appsExpire=以下ã«è©²å½“ã™ã‚‹ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’破棄 +htmlManagerServlet.appsName=表示å +htmlManagerServlet.appsPath=パス +htmlManagerServlet.appsReload=å†ãƒ­ãƒ¼ãƒ‰ +htmlManagerServlet.appsSessions=セッション +htmlManagerServlet.appsStart=èµ·å‹• +htmlManagerServlet.appsStop=åœæ­¢ +htmlManagerServlet.appsTasks=コマンド +htmlManagerServlet.appsTitle=アプリケーション +htmlManagerServlet.appsUndeploy=é…備解除 +htmlManagerServlet.appsVersion=ãƒãƒ¼ã‚¸ãƒ§ãƒ³ +htmlManagerServlet.configReloadButton=å†èª­ã¿è¾¼ã¿ +htmlManagerServlet.configSslHostName=TLSホストå(オプション) +htmlManagerServlet.configSslReloadTitle=TLS構æˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’å†èª­ã¿è¾¼ã¿ã—ã¾ã™ã€‚ +htmlManagerServlet.configTitle=構æˆãƒ•ã‚¡ã‚¤ãƒ« +htmlManagerServlet.connectorStateAliveSocketCount=キープアライブソケット数: +htmlManagerServlet.connectorStateBytesReceived=å—ä¿¡ãƒã‚¤ãƒˆæ•°ï¼š +htmlManagerServlet.connectorStateBytesSent=é€ä¿¡ãƒã‚¤ãƒˆï¼š +htmlManagerServlet.connectorStateErrorCount=エラー数: +htmlManagerServlet.connectorStateHint=P:パースã¨ãƒªã‚¯ã‚¨ã‚¹ãƒˆæº–å‚™ S:サービス F:終了 R:Ready K:キープアライブ +htmlManagerServlet.connectorStateMaxProcessingTime=最大処ç†æ™‚間: +htmlManagerServlet.connectorStateMaxThreads=最大スレッド: +htmlManagerServlet.connectorStateProcessingTime=処ç†æ™‚間: +htmlManagerServlet.connectorStateRequestCount=リクエスト数: +htmlManagerServlet.connectorStateTableTitleBRecv=B Recv +htmlManagerServlet.connectorStateTableTitleBSent=é€ä¿¡ãƒã‚¤ãƒˆæ•° +htmlManagerServlet.connectorStateTableTitleClientAct=Client (Actual) +htmlManagerServlet.connectorStateTableTitleClientForw=Client (Forwarded) +htmlManagerServlet.connectorStateTableTitleRequest=リクエスト +htmlManagerServlet.connectorStateTableTitleStage=ステージ +htmlManagerServlet.connectorStateTableTitleTime=時間 +htmlManagerServlet.connectorStateTableTitleVHost=仮想ホスト +htmlManagerServlet.connectorStateThreadBusy=ç¾åœ¨ã®Busyスレッド: +htmlManagerServlet.connectorStateThreadCount=ç¾åœ¨ã®ã‚¹ãƒ¬ãƒƒãƒ‰æ•°ï¼š +htmlManagerServlet.deployButton=é…å‚™ +htmlManagerServlet.deployConfig=XML設定ファイルã®URL: +htmlManagerServlet.deployPath=コンテキストパス (çœç•¥å¯): +htmlManagerServlet.deployServer=サーãƒä¸Šã®WARファイルã¾ãŸã¯ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã®é…å‚™ +htmlManagerServlet.deployTitle=é…å‚™ +htmlManagerServlet.deployUpload=WARファイルã®é…å‚™ +htmlManagerServlet.deployUploadFail=FAIL - é…å‚™ã®ã‚¢ãƒƒãƒ—ロードãŒå¤±æ•—ã—ã¾ã—ãŸã€ä¾‹å¤–: [{0}] +htmlManagerServlet.deployUploadFile=アップロードã™ã‚‹WARファイルã®é¸æŠž +htmlManagerServlet.deployUploadInServerXml=FAIL - server.xmlã§ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãŒå®šç¾©ã•ã‚Œã¦ã„ã‚‹å ´åˆã€warファイル[{0}]をアップロードã§ãã¾ã›ã‚“ +htmlManagerServlet.deployUploadNoFile=FAIL - ファイルã®ã‚¢ãƒƒãƒ—ロードãŒå¤±æ•—ã—ã¾ã—ãŸã€ãƒ•ã‚¡ã‚¤ãƒ«ãŒå­˜åœ¨ã—ã¾ã›ã‚“ +htmlManagerServlet.deployUploadNotWar=FAIL - アップロードã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ« [{0}] ã¯WARファイルã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +htmlManagerServlet.deployUploadWarExists=FAIL - WARファイル [{0}] ã¯æ—¢ã«ã‚µãƒ¼ãƒä¸Šã«å­˜åœ¨ã—ã¾ã™ +htmlManagerServlet.deployVersion=ãƒãƒ¼ã‚¸ãƒ§ãƒ³ (パラレルé…備用): +htmlManagerServlet.deployWar=WARファイルã¾ãŸã¯ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã®URL: +htmlManagerServlet.diagnosticsLeak=Web アプリケーションã®åœæ­¢ã‚„å†èª­ã¿è¾¼ã¿ã€é…備解除ã§ãƒ¡ãƒ¢ãƒªãƒ¼ãƒªãƒ¼ã‚¯ãŒç™ºç”Ÿã—ã¦ã„ã‚‹ã‹ç¢ºèªã—ã¾ã™ã€‚ +htmlManagerServlet.diagnosticsLeakButton=メモリリーク発見 +htmlManagerServlet.diagnosticsLeakWarning=ã“ã®è¨ºæ–­ãƒã‚§ãƒƒã‚¯ã«ã‚ˆã‚Šã€Full GCãŒãƒˆãƒªã‚¬ãƒ¼ã•ã‚Œã¾ã™ã€‚本番システムã«ã¯éžå¸¸ã«æ³¨æ„ã—ã¦ä½¿ç”¨ã—ã¦ãã ã•ã„。 +htmlManagerServlet.diagnosticsSsl=TLSコãƒã‚¯ã‚¿æ§‹æˆã®è¨ºæ–­ +htmlManagerServlet.diagnosticsSslConnectorCertsButton=証明書 +htmlManagerServlet.diagnosticsSslConnectorCertsText=設定済ã¿ã®TLS仮想ホストã¨ãã‚Œãžã‚Œã®è¨¼æ˜Žæ›¸ãƒã‚§ãƒ¼ãƒ³ã‚’一覧表示ã—ã¾ã™ã€‚ +htmlManagerServlet.diagnosticsSslConnectorCipherButton=æš—å· +htmlManagerServlet.diagnosticsSslConnectorCipherText=設定済ã¿ã®TLS仮想ホストã¨ãã‚Œãžã‚Œã®æš—å·ã‚’一覧表示ã—ã¾ã™ã€‚ +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsButton=ä¿¡é ¼ã§ãる証明書 +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsText=設定済ã¿ã®TLS仮想ホストã¨ãã‚Œãžã‚Œã®ä¿¡é ¼ã§ãる証明書を一覧表示ã—ã¾ã™ã€‚ +htmlManagerServlet.diagnosticsTitle=診断 +htmlManagerServlet.error.sessions=コンテキスト [{0}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚³ãƒžãƒ³ãƒ‰å‡¦ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +htmlManagerServlet.error.sortOrder=ä¸æ˜Žãªã‚½ãƒ¼ãƒˆé †[{0}] +htmlManagerServlet.expire.explain=éžã‚¢ã‚¯ãƒ†ã‚£ãƒ– ≥ +htmlManagerServlet.expire.unit=分 +htmlManagerServlet.findleaksList=次ã®Webアプリケーションã¯åœæ­¢ã—ã¾ã—㟠(å†ãƒ­ãƒ¼ãƒ‰ã€é…備解除)。\n\ +以å‰ã®èµ·å‹•ã‹ã‚‰ã®ã‚¯ãƒ©ã‚¹ã¯ã¾ã ãƒ¡ãƒ¢ãƒªã«ãƒ­ãƒ¼ãƒ‰ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€ãƒ¡ãƒ¢ãƒª\n\ +リークを引ãèµ·ã“ã—ã¾ã™ã€‚(確èªã®ãŸã‚ã«ãƒ—ロファイラを使用ã—ã¦ä¸‹ã•ã„): +htmlManagerServlet.findleaksNone=åœæ­¢ã€å†èª­ã¿è¾¼ã¿ã€é…備解除ã«ã‚ˆã‚Šãƒ¡ãƒ¢ãƒªãƒ¼ãƒªãƒ¼ã‚¯ã®ç™ºç”Ÿã—㟠Web アプリケーションã¯ã‚ã‚Šã¾ã›ã‚“。 +htmlManagerServlet.helpHtmlManager=HTMLマãƒãƒ¼ã‚¸ãƒ£ãƒ˜ãƒ«ãƒ— +htmlManagerServlet.helpHtmlManagerFile=../docs/html-manager-howto.html +htmlManagerServlet.helpManager=マãƒãƒ¼ã‚¸ãƒ£ãƒ˜ãƒ«ãƒ— +htmlManagerServlet.helpManagerFile=../docs/manager-howto.html +htmlManagerServlet.jvmFreeMemory=フリーメモリ: +htmlManagerServlet.jvmMaxMemory=最大メモリ: +htmlManagerServlet.jvmTableTitleInitial=åˆæœŸ +htmlManagerServlet.jvmTableTitleMaximum=最大値 +htmlManagerServlet.jvmTableTitleMemoryPool=メモリプール +htmlManagerServlet.jvmTableTitleTotal=åˆè¨ˆ +htmlManagerServlet.jvmTableTitleType=Type +htmlManagerServlet.jvmTableTitleUsed=使用中 +htmlManagerServlet.jvmTotalMemory=åˆè¨ˆãƒ¡ãƒ¢ãƒªï¼š +htmlManagerServlet.list=アプリケーションã®ä¸€è¦§ +htmlManagerServlet.manager=マãƒãƒ¼ã‚¸ãƒ£ +htmlManagerServlet.messageLabel=メッセージ +htmlManagerServlet.noManager=- +htmlManagerServlet.noVersion=指定ãªã— +htmlManagerServlet.osAvailableMemory=利用å¯èƒ½ãªãƒ¡ãƒ¢ãƒª +htmlManagerServlet.osFreePageFile=フリーページファイル: +htmlManagerServlet.osKernelTime=プロセスã®ã‚«ãƒ¼ãƒãƒ«æ™‚間: +htmlManagerServlet.osMemoryLoad=メモリロード: +htmlManagerServlet.osPhysicalMemory=物ç†ãƒ¡ãƒ¢ãƒªï¼š +htmlManagerServlet.osTotalPageFile=åˆè¨ˆãƒšãƒ¼ã‚¸ãƒ•ã‚¡ã‚¤ãƒ«ï¼š +htmlManagerServlet.osUserTime=プロセスユーザå: +htmlManagerServlet.serverHostname=ホストå +htmlManagerServlet.serverIPAddress=IP アドレス +htmlManagerServlet.serverJVMVendor=JVMベンダ +htmlManagerServlet.serverJVMVersion=JVMãƒãƒ¼ã‚¸ãƒ§ãƒ³ +htmlManagerServlet.serverOSArch=OSアーキテクãƒãƒ£ +htmlManagerServlet.serverOSName=OSå +htmlManagerServlet.serverOSVersion=OSãƒãƒ¼ã‚¸ãƒ§ãƒ³ +htmlManagerServlet.serverTitle=サーãƒæƒ…å ± +htmlManagerServlet.serverVersion=Tomcatãƒãƒ¼ã‚¸ãƒ§ãƒ³ +htmlManagerServlet.title=Tomcat Webアプリケーションマãƒãƒ¼ã‚¸ãƒ£ + +jmxProxyServlet.noBeanFound=オブジェクトå [{0}] ã® MBean ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +jmxProxyServlet.noOperationOnBean=クラスå [{3}]ã€ã‚ªãƒ–ジェクトå [{2}] ã® MBean ã§å¼•æ•°ãŒ [{1}] 個ã®æ“作 [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ + +managerServlet.alreadyContext=FAIL - アプリケーションã¯ã€æ—¢ã«ãƒ‘ス [{0}] ã«å­˜åœ¨ã—ã¾ã™ +managerServlet.certsNotAvailable=実行時ã«ã“ã®ã‚³ãƒã‚¯ã‚¿ã‹ã‚‰è¨¼æ˜Žæ›¸æƒ…報をå–å¾—ã§ãã¾ã›ã‚“。 +managerServlet.copyError=パス [{0}] ã‹ã‚‰è¨­å®šãƒ•ã‚¡ã‚¤ãƒ«ã‚’コピーã§ãã¾ã›ã‚“ +managerServlet.deleteFail=FAIL - [{0}]を削除ã§ãã¾ã›ã‚“。 ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒç¶™ç¶šã—ã¦å­˜åœ¨ã™ã‚‹ã¨ã€å•é¡ŒãŒç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +managerServlet.deployFailed=FAIL - コンテキストパス [{0}] ã«ã‚¢ãƒ—リケーションをé…å‚™ã§ãã¾ã›ã‚“。 +managerServlet.deployed=OK - コンテキストパス [{0}] ã§ã‚¢ãƒ—リケーションをé…å‚™ã—ã¾ã—㟠+managerServlet.deployedButNotStarted=FAIL - コンテキストパス [{0}] ã«ã‚¢ãƒ—リケーションをé…å‚™ã—ã¾ã—ãŸãŒã€ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’開始ã§ãã¾ã›ã‚“ã§ã—㟠+managerServlet.error.deploy=[{0}]ã®é…備中ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.error.idleParam=アイドルパラメータ [{0}] ã‚’æ•´æ•°ã¨ã—ã¦è§£æžã§ãã¾ã›ã‚“ã§ã—㟠+managerServlet.error.jmx=JMXエラー +managerServlet.error.reload=[{0}]ã®å†èª­ã¿è¾¼ã¿ã‚¨ãƒ©ãƒ¼ +managerServlet.error.resources=タイプ [{0}] ã®ãƒªã‚½ãƒ¼ã‚¹è¡¨ç¤ºä¸­ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.error.serverInfo=サーãƒãƒ¼æƒ…報表示中ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.error.sessions=コンテキスト [{0}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³æƒ…報表示中ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.error.start=[{0}]を開始中ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.error.stop=[{0}]ã‚’åœæ­¢ä¸­ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.error.storeConfig=設定ä¿å­˜ä¸­ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.error.storeContextConfig=コンテキスト [{0}] ã®è¨­å®šä¿å­˜ä¸­ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.error.undeploy=[{0}]ã®é…備解除中ã®ã‚¨ãƒ©ãƒ¼ +managerServlet.exception=FAIL - 例外 [{0}] ãŒç™ºç”Ÿã—ã¾ã—㟠+managerServlet.findleaksFail=FAIL - リークを検出ã§ãã¾ã›ã‚“。Host㯠StandardHost ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚ã¯ã‚ã‚Šã¾ã›ã‚“。 +managerServlet.findleaksList=OK - 次ã®ã‚¢ãƒ—リケーションã§æ½œåœ¨çš„ãªãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸï¼š +managerServlet.findleaksNone=OK - メモリリークã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+managerServlet.inService=FAIL - アプリケーション [{0}] ã¯ã™ã§ã«ã‚µãƒ¼ãƒ“スを開始ã—ã¦ã„ã¾ã™ã€‚ +managerServlet.invalidCommand=FAIL - コマンド [{0}] ã«ç„¡åŠ¹ãªãƒ‘ラメータãŒæŒ‡å®šã•ã‚Œã¾ã—㟠+managerServlet.invalidPath=FAIL - 無効ãªã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãƒ‘ス [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—㟠+managerServlet.listed=OK - ãƒãƒ¼ãƒãƒ£ãƒ«ãƒ›ã‚¹ãƒˆ [{0}] ã®ã‚¢ãƒ—リケーション一覧ã§ã™ +managerServlet.mkdirFail=FAIL - ディレクトリ [{0}] ã¯ä½œæˆã§ãã¾ã›ã‚“ +managerServlet.noCommand=FAIL - コマンドãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ +managerServlet.noContext=FAIL - パス [{0}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãŒå­˜åœ¨ã—ã¾ã›ã‚“ +managerServlet.noGlobal=FAIL - グローãƒãƒ«ãªJNDIリソースãŒåˆ©ç”¨ã§ãã¾ã›ã‚“ +managerServlet.noManager=FAIL - パス [{0}] ã®ãƒžãƒãƒ¼ã‚¸ãƒ£ãŒå­˜åœ¨ã—ã¾ã›ã‚“ +managerServlet.noSelf=FAIL - マãƒãƒ¼ã‚¸ãƒ£è‡ªèº«ã‚’å†ãƒ­ãƒ¼ãƒ‰ã€å‰Šé™¤ã€åœæ­¢ã€ã¾ãŸã¯é…備解除ã§ãã¾ã›ã‚“ +managerServlet.noWrapper=コンテナã¯ã“ã®ã‚µãƒ¼ãƒ–レットã«å¯¾ã—ã¦å‘¼ã³å‡ºã•ã‚ŒãŸsetWrapper()ã‚’æŒã£ã¦ã„ã¾ã›ã‚“ +managerServlet.notDeployed=FAIL - Context[{0}]ã¯server.xmlã§å®šç¾©ã•ã‚Œã¦ãŠã‚Šã€å±•é–‹ã•ã‚Œã¦ã„ãªã„å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +managerServlet.notSslConnector=ã“ã®ã‚³ãƒã‚¯ã‚¿ãƒ¼ã§ã¯ SSL ãŒæœ‰åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +managerServlet.objectNameFail=FAIL - オブジェクトå [{0}] ã‚’ ManagerServlet ã¨ã—ã¦ç™»éŒ²ã§ãã¾ã›ã‚“ +managerServlet.postCommand=FAIL - コマンド [{0}] ã‚’ GET リクエストã§å®Ÿè¡Œã—よã†ã¨ã—ã¾ã—ãŸãŒã€POST リクエストã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +managerServlet.reloaded=OK - コンテキストパス [{0}] ã®ã‚¢ãƒ—リケーションをå†ãƒ­ãƒ¼ãƒ‰ã—ã¾ã—㟠+managerServlet.renameFail=FAIL - [{0}]ã®åå‰ã‚’[{1}]ã«å¤‰æ›´ã§ãã¾ã›ã‚“。 ã“ã‚Œã«ã‚ˆã‚Šã€ä»Šå¾Œã®å±•é–‹ã§å•é¡ŒãŒç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +managerServlet.resourcesAll=OK - ã™ã¹ã¦ã®ã‚¿ã‚¤ãƒ—ã®ã‚°ãƒ­ãƒ¼ãƒãƒ«ãƒªã‚½ãƒ¼ã‚¹ã‚’列挙ã—ã¾ã—㟠+managerServlet.resourcesType=OK - タイプ [{0}] ã®ã‚°ãƒ­ãƒ¼ãƒãƒ«ãƒªã‚½ãƒ¼ã‚¹ã‚’列挙ã—ã¾ã—㟠+managerServlet.saveFail=FAIL - 設定ã®ä¿å­˜ã«å¤±æ•—ã—ã¾ã—ãŸ: [{0}] +managerServlet.saved=OK - サーãƒã®è¨­å®šã‚’ä¿å­˜ã—ã¾ã—㟠+managerServlet.savedContext=OK - コンテキスト [{0}] ã®è¨­å®šã‚’ä¿å­˜ã—ã¾ã—㟠+managerServlet.savedContextFail=FAIL - コンテキスト [{0}] ã®æ§‹æˆã‚’ä¿å­˜ã§ãã¾ã›ã‚“ã§ã—㟠+managerServlet.serverInfo=OK - サーãƒãƒ¼æƒ…å ±\n\ +Tomcatãƒãƒ¼ã‚¸ãƒ§ãƒ³ï¼š[{0}]\n\ +OSå:[{1}]\n\ +OSãƒãƒ¼ã‚¸ãƒ§ãƒ³ï¼š[{2}]\n\ +OSアーキテクãƒãƒ£ï¼š[{3}]\n\ +JVMãƒãƒ¼ã‚¸ãƒ§ãƒ³ï¼š[{4}]\n\ +JVMベンダー:[{5}] +managerServlet.sessiondefaultmax=既定ã®æœ€å¤§ã‚»ãƒƒã‚·ãƒ§ãƒ³éžæ´»æ€§æ™‚間㯠[{0}] 分ã§ã™ +managerServlet.sessions=OK - コンテキストパス [{0}] ã®ã‚¢ãƒ—リケーションã®ã‚»ãƒƒã‚·ãƒ§ãƒ³æƒ…å ±ã§ã™ +managerServlet.sessiontimeout=[{0}]分: [{1}]セッション +managerServlet.sessiontimeout.expired=[{0}]分: expired [{1}]セッション +managerServlet.sessiontimeout.unlimited=unlimited 分: [{0}]セッション +managerServlet.sslConnectorCerts=OK - コãƒã‚¯ã‚¿/証明書ãƒã‚§ãƒ¼ãƒ³ã®æƒ…å ± +managerServlet.sslConnectorCiphers=OK - Connector/ SSLæš—å·æƒ…å ± +managerServlet.sslConnectorTrustedCerts=OK - コãƒã‚¯ã‚¿/ä¿¡é ¼ã•ã‚ŒãŸè¨¼æ˜Žæ›¸æƒ…å ± +managerServlet.sslReload=OK - [{0}]ã®TLS構æˆã‚’リロードã—ã¾ã—㟠+managerServlet.sslReloadAll=OK - ã™ã¹ã¦ã®TLS仮想ホストã®TLS構æˆã‚’リロードã—ã¾ã—㟠+managerServlet.sslReloadFail=FAIL - TLS設定をå†ãƒ­ãƒ¼ãƒ‰ã§ãã¾ã›ã‚“ã§ã—㟠+managerServlet.startFailed=FAIL - コンテキストパス [{0}] ã®ã‚¢ãƒ—リケーションãŒèµ·å‹•ã§ãã¾ã›ã‚“ +managerServlet.started=OK - コンテキストパス [{0}] ã§ã‚¢ãƒ—リケーションを起動ã—ã¾ã—㟠+managerServlet.stopped=OK - コンテキストパス [{0}] ã§ã‚¢ãƒ—リケーションをåœæ­¢ã—ã¾ã—㟠+managerServlet.storeConfig.noMBean=FAIL - [{0}] ã« StoreConfig Mbean ãŒç™»éŒ²ã•ã‚Œã¦ã„ã¾ã›ã‚“。一般的ã«ã¯ StoreConfigLifecycleListener ãŒç™»éŒ²ã—ã¾ã™ã€‚ +managerServlet.threaddump=OK - JVMスレッドダンプ +managerServlet.trustedCertsNotConfigured=ã“ã®ä»®æƒ³ãƒ›ã‚¹ãƒˆã«ã¯ä¿¡é ¼ã§ãる証明書ãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +managerServlet.undeployed=OK - コンテキストパス [{0}] ã®ã‚¢ãƒ—リケーションをé…備解除ã—ã¾ã—㟠+managerServlet.unknownCommand=FAIL - 未知ã®ã‚³ãƒžãƒ³ãƒ‰ [{0}] ã§ã™ +managerServlet.vminfo=OK - VM 情報 + +statusServlet.complete=サーãƒã®å…¨çŠ¶æ…‹ +statusServlet.title=サーãƒã®çŠ¶æ…‹ diff --git a/java/org/apache/catalina/manager/LocalStrings_ko.properties b/java/org/apache/catalina/manager/LocalStrings_ko.properties new file mode 100644 index 0000000..f30df40 --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_ko.properties @@ -0,0 +1,196 @@ +# 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. + +htmlManagerServlet.appsAvailable=실행 중 +htmlManagerServlet.appsExpire=ì„¸ì…˜ë“¤ì„ ë§Œë£Œì‹œí‚¤ê¸° +htmlManagerServlet.appsName=표시 ì´ë¦„ +htmlManagerServlet.appsPath=경로 +htmlManagerServlet.appsReload=다시 로드 +htmlManagerServlet.appsSessions=세션들 +htmlManagerServlet.appsStart=시작 +htmlManagerServlet.appsStop=중지 +htmlManagerServlet.appsTasks=명령들 +htmlManagerServlet.appsTitle=애플리케ì´ì…˜ë“¤ +htmlManagerServlet.appsUndeploy=ë°°ì¹˜ëœ ê²ƒì„ ì œê±° +htmlManagerServlet.appsVersion=버전 +htmlManagerServlet.configReloadButton=다시 ì½ê¸° +htmlManagerServlet.configSslHostName=TLS 호스트 ì´ë¦„ (ì„ íƒ ì‚¬í•­) +htmlManagerServlet.configSslReloadTitle=TLS 설정 파ì¼ë“¤ì„ 다시 ì½ìŠµë‹ˆë‹¤. +htmlManagerServlet.configTitle=설정 +htmlManagerServlet.connectorStateAliveSocketCount=Keep alive 소켓 개수: +htmlManagerServlet.connectorStateBytesReceived=ìˆ˜ì‹ ëœ ë°”ì´íŠ¸ í¬ê¸°: +htmlManagerServlet.connectorStateBytesSent=ì „ì†¡ëœ ë°”ì´íŠ¸ í¬ê¸°: +htmlManagerServlet.connectorStateErrorCount=오류 개수: +htmlManagerServlet.connectorStateHint=P: ìš”ì²­ì„ íŒŒì‹± ë˜ëŠ” 준비, S: 서비스, F: 완료 R: 준비 K: Keepalive +htmlManagerServlet.connectorStateMaxProcessingTime=최대 처리 시간: +htmlManagerServlet.connectorStateMaxThreads=최대 쓰레드 개수: +htmlManagerServlet.connectorStateProcessingTime=처리 시간: +htmlManagerServlet.connectorStateRequestCount=요청 회수: +htmlManagerServlet.connectorStateTableTitleBRecv=ë°›ì€ ë°”ì´íŠ¸ í¬ê¸° +htmlManagerServlet.connectorStateTableTitleBSent=ì „ì†¡ëœ ë°”ì´íŠ¸ í¬ê¸° +htmlManagerServlet.connectorStateTableTitleClientAct=í´ë¼ì´ì–¸íŠ¸ (실제) +htmlManagerServlet.connectorStateTableTitleClientForw=í´ë¼ì´ì–¸íŠ¸ (Forwarded) +htmlManagerServlet.connectorStateTableTitleRequest=요청 +htmlManagerServlet.connectorStateTableTitleStage=단계 +htmlManagerServlet.connectorStateTableTitleTime=시간 +htmlManagerServlet.connectorStateTableTitleVHost=ê°€ìƒí˜¸ìŠ¤íŠ¸ +htmlManagerServlet.connectorStateThreadBusy=현재 ì‚¬ìš©ì¤‘ì¸ ì“°ë ˆë“œë“¤: +htmlManagerServlet.connectorStateThreadCount=현재 쓰레드 개수: +htmlManagerServlet.deployButton=배치 +htmlManagerServlet.deployConfig=XML 설정 íŒŒì¼ ê²½ë¡œ: +htmlManagerServlet.deployPath=컨í…스트 경로: +htmlManagerServlet.deployServer=ì„œë²„ì— ìžˆëŠ” 디렉토리 ë˜ëŠ” WAR 파ì¼ì„ 배치합니다. +htmlManagerServlet.deployTitle=배치 +htmlManagerServlet.deployUpload=배치할 WAR íŒŒì¼ +htmlManagerServlet.deployUploadFail=실패 - 배치관리ìžì—ì„œ 업로드 실패, 예외: [{0}] +htmlManagerServlet.deployUploadFile=업로드할 WAR 파ì¼ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. +htmlManagerServlet.deployUploadInServerXml=실패 - 컨í…스트가 server.xmlì— ì •ì˜ë˜ì–´ 있다면, War íŒŒì¼ [{0}]ì€(는) ì—…ë¡œë“œë  ìˆ˜ 없습니다. +htmlManagerServlet.deployUploadNoFile=실패 - íŒŒì¼ ì—…ë¡œë“œ 실패. 파ì¼ì´ 없습니다. +htmlManagerServlet.deployUploadNotWar=실패 - ì—…ë¡œë“œëœ íŒŒì¼ [{0}]ì€(는) 반드시 .warì´ì–´ì•¼ 합니다. +htmlManagerServlet.deployUploadWarExists=실패 - War íŒŒì¼ [{0}]ì´(ê°€) ì´ë¯¸ ì„œë²„ì— ì¡´ìž¬í•©ë‹ˆë‹¤. +htmlManagerServlet.deployVersion=버전 (병렬 배치용): +htmlManagerServlet.deployWar=WAR ë˜ëŠ” 디렉토리 경로: +htmlManagerServlet.diagnosticsLeak=웹 애플리케ì´ì…˜ì´ 중지ë˜ê±°ë‚˜, 다시 로드ë˜ê±°ë‚˜, ë˜ëŠ” 배치 ì œê±°ë  ë•Œ, 메모리 누수를 유발하는지 여부를 살펴보려 ì ê²€í•©ë‹ˆë‹¤. +htmlManagerServlet.diagnosticsLeakButton=메모리 누수 찾기 +htmlManagerServlet.diagnosticsLeakWarning=ì´ ì§„ë‹¨ ì ê²€ì€ Full Garbage Collectionì„ ê°œì‹œí•  것입니다. 프로ë•ì…˜ 시스템들ì—서는 ê·¹ë„ì˜ ì£¼ì˜ë¥¼ 기울여 사용하십시오. +htmlManagerServlet.diagnosticsSsl=TLS Connector 설정 진단 +htmlManagerServlet.diagnosticsSslConnectorCertsButton=ì¸ì¦ì„œë“¤ +htmlManagerServlet.diagnosticsSslConnectorCertsText=ì„¤ì •ëœ TLS ê°€ìƒ í˜¸ìŠ¤íŠ¸ë“¤ê³¼ 그들 ê°ê°ì˜ ì¸ì¦ì„œ ì²´ì¸ì˜ ëª©ë¡ +htmlManagerServlet.diagnosticsSslConnectorCipherButton=Cipher들 +htmlManagerServlet.diagnosticsSslConnectorCipherText=ì„¤ì •ëœ TLS ê°€ìƒ í˜¸ìŠ¤íŠ¸ë“¤ê³¼ ê°ê°ì„ 위한 cipherë“¤ì˜ ëª©ë¡ì„ 표시합니다. +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsButton=신뢰ë˜ëŠ” ì¸ì¦ì„œë“¤ +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsText=ì„¤ì •ëœ TLS ê°€ìƒ í˜¸ìŠ¤íŠ¸ë“¤ê³¼ ê°ê°ì„ 위한 신뢰ë˜ëŠ” ì¸ì¦ì„œë“¤ì˜ 목ë¡ì„ 표시합니다. +htmlManagerServlet.diagnosticsTitle=시스템 진단 +htmlManagerServlet.error.sessions=컨í…스트 [{0}]ì„(를) 위한 sessions 명령 처리 중 오류 ë°œìƒ +htmlManagerServlet.error.sortOrder=ì•Œ 수 없는 ì •ë ¬ 순서 [{0}] +htmlManagerServlet.expire.explain=idle ê°’ ≥ +htmlManagerServlet.expire.unit=분 +htmlManagerServlet.findleaksList=ë‹¤ìŒ ì›¹ 애플리케ì´ì…˜ë“¤ì´ 중지ë˜ì—ˆì§€ë§Œ (다시 로드ë˜ê±°ë‚˜, 배치가 제거ë˜ì–´), ì´ì „ 실행 ì‹œì— ë¡œë“œë˜ì—ˆë˜ í´ëž˜ìŠ¤ë“¤ì´ 여전히 ë©”ëª¨ë¦¬ì— ë‚¨ì•„ 있어서, 메모리 누수를 유발할 수 있습니다. (확ì¸í•˜ë ¤ë©´ 프로파ì¼ëŸ¬ë¥¼ 사용하십시오):\n\ +\n +htmlManagerServlet.findleaksNone=ì–´ë–¤ 웹 애플리케ì´ì…˜ë„, 중지 ì‹œ ë˜ëŠ” 다시 ë¡œë“œë  ë•Œ ë˜ëŠ” 배치로부터 제거 ë  ë•Œ, 메모리 누수를 유발하지 ì•Šì€ ê²ƒ 같습니다. +htmlManagerServlet.helpHtmlManager=HTML 매니저 ë„ì›€ë§ +htmlManagerServlet.helpHtmlManagerFile=../docs/html-manager-howto.html +htmlManagerServlet.helpManager=매니저 ë„ì›€ë§ +htmlManagerServlet.helpManagerFile=../docs/manager-howto.html +htmlManagerServlet.jvmFreeMemory=유휴 메모리: +htmlManagerServlet.jvmMaxMemory=최대 메모리: +htmlManagerServlet.jvmTableTitleInitial=초기 +htmlManagerServlet.jvmTableTitleMaximum=최대값 +htmlManagerServlet.jvmTableTitleMemoryPool=메모리 í’€ +htmlManagerServlet.jvmTableTitleTotal=ì „ì²´ +htmlManagerServlet.jvmTableTitleType=타입 +htmlManagerServlet.jvmTableTitleUsed=ì‚¬ìš©ëœ ë©”ëª¨ë¦¬ +htmlManagerServlet.jvmTotalMemory=ì „ì²´ 메모리: +htmlManagerServlet.list=애플리케ì´ì…˜ë“¤ì˜ 목ë¡ì„ 표시 +htmlManagerServlet.manager=매니저 +htmlManagerServlet.messageLabel=메시지: +htmlManagerServlet.noManager=- +htmlManagerServlet.noVersion=지정 ì•ˆë¨ +htmlManagerServlet.osAvailableMemory=가용 메모리: +htmlManagerServlet.osFreePageFile=유휴 페ì´ì§€ 파ì¼: +htmlManagerServlet.osKernelTime=프로세스 ì»¤ë„ íƒ€ìž„: +htmlManagerServlet.osMemoryLoad=메모리 로드: +htmlManagerServlet.osPhysicalMemory=ë¬¼ë¦¬ì  ë©”ëª¨ë¦¬: +htmlManagerServlet.osTotalPageFile=ì „ì²´ 페ì´ì§€ 파ì¼: +htmlManagerServlet.osUserTime=프로세스 User Time: +htmlManagerServlet.serverHostname=호스트명 +htmlManagerServlet.serverIPAddress=IP 주소 +htmlManagerServlet.serverJVMVendor=JVM ë²¤ë” +htmlManagerServlet.serverJVMVersion=JVM 버전 +htmlManagerServlet.serverOSArch=ìš´ì˜ì²´ì œ 아키í…처 +htmlManagerServlet.serverOSName=ìš´ì˜ì²´ì œ ì´ë¦„ +htmlManagerServlet.serverOSVersion=ìš´ì˜ì²´ì œ 버전 +htmlManagerServlet.serverTitle=서버 ì •ë³´ +htmlManagerServlet.serverVersion=Tomcat 버전 +htmlManagerServlet.title=Tomcat 웹 애플리케ì´ì…˜ 매니저 + +jmxProxyServlet.noBeanFound=ê°ì²´ ì´ë¦„ [{0}]ì¸ MBeanì„ ì°¾ì„ ìˆ˜ 없습니다 +jmxProxyServlet.noOperationOnBean=[{3}] ì¸ ê°ì²´ ì´ë¦„ [{2}] ì—ì„œ [{1}] 아규먼트가 있는 [{0}] 오í¼ë ˆì´ì…˜ì„ ì°¾ì„ ìˆ˜ê°€ 없습니다 + +managerServlet.alreadyContext=실패 - 애플리케ì´ì…˜ì´ ì´ë¯¸ 경로 [{0}]ì— ì¡´ìž¬í•©ë‹ˆë‹¤. +managerServlet.certsNotAvailable=ì´ Connector로부터, ì¸ì¦ì„œ 정보를 ëŸ°íƒ€ìž„ì— êµ¬í•  수 없습니다. +managerServlet.copyError=경로 [{0}](으)로부터 설정 파ì¼ì„ 복사할 수 없었습니다. +managerServlet.deleteFail=실패 - [{0}]ì„(를) 삭제할 수 없습니다. ì´ íŒŒì¼ì´ 계ì†í•´ì„œ 존재하면 ë¬¸ì œë“¤ì„ ì¼ìœ¼í‚¬ 수 있습니다. +managerServlet.deployFailed=실패 - 컨í…스트 경로 [{0}]ì—, 애플리케ì´ì…˜ì„ 배치하지 못했습니다. +managerServlet.deployed=OK - 컨í…스트 경로 [{0}]ì— ì• í”Œë¦¬ì¼€ì´ì…˜ì„ 배치했습니다. +managerServlet.deployedButNotStarted=실패 - 컨í…스트 경로 [{0}]ì— ìžˆëŠ” 애플리케ì´ì…˜ì„ 배치했으나, 컨í…스트가 시작ë˜ì§€ 못했습니다. +managerServlet.error.deploy=[{0}]ì„(를) 배치하는 중 오류 ë°œìƒ +managerServlet.error.idleParam=정수 idle 파ë¼ë¯¸í„° [{0}]ì„(를) 파싱할 수 없었습니다. +managerServlet.error.reload=[{0}]ì„(를) 다시 로드하는 중 오류 ë°œìƒ +managerServlet.error.resources=타입 [{0}]ì˜ ë¦¬ì†ŒìŠ¤ë“¤ì„ í‘œì‹œí•˜ëŠ” 중 오류 ë°œìƒ +managerServlet.error.serverInfo=서버 정보를 표시하는 중 오류 ë°œìƒ +managerServlet.error.sessions=컨í…스트 [{0}]ì„(를) 위한 ì„¸ì…˜ë“¤ì˜ ì •ë³´ë¥¼ 표시하는 중 오류 ë°œìƒ +managerServlet.error.start=[{0}]ì„(를) 시작하는 중 오류 ë°œìƒ +managerServlet.error.stop=[{0}]ì„(를) 중지시키는 중 오류 ë°œìƒ +managerServlet.error.storeConfig=설정 저장 중 오류 ë°œìƒ +managerServlet.error.storeContextConfig=컨í…스트 [{0}]ì„(를) 위한 ì„¤ì •ì„ ì €ìž¥í•˜ëŠ” 중 오류 ë°œìƒ +managerServlet.error.undeploy=[{0}]ì„(를) 배치ì—ì„œ 제거하는 중 오류 ë°œìƒ +managerServlet.exception=실패 - 예외 ë°œìƒ [{0}] +managerServlet.findleaksFail=실패 - 잠재 메모리 누수 찾기 실패: 호스트가 StandardHostì˜ ì¸ìŠ¤í„´ìŠ¤ê°€ 아닙니다. +managerServlet.findleaksList=OK - ë‹¤ìŒ ì• í”Œë¦¬ì¼€ì´ì…˜ë“¤ì—ì„œ 잠재ì ì¸ 메모리 ëˆ„ìˆ˜ë“¤ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤: +managerServlet.findleaksNone=OK - 메모리 누수가 발견 ì•ˆë¨ +managerServlet.inService=실패 - 애플리케ì´ì…˜ [{0}]ì´(ê°€) ì´ë¯¸ 서비스ë˜ê³  있습니다. +managerServlet.invalidCommand=실패 - 명령 [{0}]ì„(를) 위해 유효하지 ì•Šì€ íŒŒë¼ë¯¸í„°ë“¤ì´ 제공ë˜ì—ˆìŠµë‹ˆë‹¤. +managerServlet.invalidPath=실패 - 유효하지 ì•Šì€ ì»¨í…스트 경로 [{0}]ì´(ê°€) 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +managerServlet.listed=OK - ê°€ìƒ í˜¸ìŠ¤íŠ¸ [{0}]ì„(를) 위한 애플리케ì´ì…˜ë“¤ì˜ 목ë¡ì´ 표시ë˜ì—ˆìŠµë‹ˆë‹¤. +managerServlet.mkdirFail=실패 - 디렉토리 [{0}]ì„(를) ìƒì„±í•  수 없습니다. +managerServlet.noCommand=실패 - ëª…ë ¹ì´ ì§€ì •ë˜ì§€ 않았습니다. +managerServlet.noContext=실패 - [{0}](ì´)ë¼ëŠ” ì´ë¦„ì„ ê°€ì§„ 컨í…스트가 없습니다. +managerServlet.noGlobal=실패 - 가용한 글로벌 JNDI ë¦¬ì†ŒìŠ¤ë“¤ì´ ì—†ìŠµë‹ˆë‹¤. +managerServlet.noManager=실패 - 경로 [{0}]ì„(를) 위한 매니저가 없습니다. +managerServlet.noSelf=실패 - 매니저는 ìžê¸° ìžì‹ ì„ 다시 로드하거나, 중지시키거나, ìžì‹ ì˜ 배치를 제거할 수 없습니다. +managerServlet.noWrapper=컨테ì´ë„ˆê°€ ì´ ì„œë¸”ë¦¿ì„ ìœ„í•´ setWrapper()를 호출하지 않았습니다. +managerServlet.notDeployed=실패 - 컨í…스트 [{0}]ì´(ê°€) server.xmlì— ì •ì˜ë˜ì–´ 있어, 배치를 제거할 수 없습니다. +managerServlet.notSslConnector=SSLì´ ì´ connector를 위해 사용 가능 ìƒíƒœê°€ 아닙니다. +managerServlet.objectNameFail=실패 - 매니저 ì„œë¸”ë¦¿ì„ ìœ„í•œ ê°ì²´ ì´ë¦„ [{0}]ì„(를) 등ë¡í•  수 없습니다. +managerServlet.postCommand=실패 - GET ìš”ì²­ì„ í†µí•´ 명령 [{0}]ì„(를) 사용하려 ì‹œë„했으나, POST ìš”ì²­ì´ ìš”êµ¬ë©ë‹ˆë‹¤. +managerServlet.reloaded=OK - 컨í…스트 경로 [{0}]ì˜ ì• í”Œë¦¬ì¼€ì´ì…˜ì„ 다시 로드했습니다. +managerServlet.renameFail=실패 - [{0}]ì„(를) [{1}](으)ë¡œ ì´ë¦„ì„ ë³€ê²½í•  수 없습니다. ì´ëŠ” ì´í›„ì˜ ë°°ì¹˜ 작업들ì—ì„œ ë¬¸ì œë“¤ì„ ì¼ìœ¼í‚¬ 수 있습니다. +managerServlet.resourcesAll=OK - 모든 íƒ€ìž…ë“¤ì˜ ê¸€ë¡œë²Œ ë¦¬ì†ŒìŠ¤ë“¤ì´ ëª©ë¡ìœ¼ë¡œ 표시ë˜ì—ˆìŠµë‹ˆë‹¤. +managerServlet.resourcesType=OK - íƒ€ìž…ì´ [{0}]ì¸ ê¸€ë¡œë²Œ ë¦¬ì†ŒìŠ¤ë“¤ì˜ ëª©ë¡ì„ 표시했습니다. +managerServlet.saveFail=실패 - ì„¤ì •ì„ ì €ìž¥í•˜ì§€ 못했습니다: [{0}] +managerServlet.saved=OK - 서버 ì„¤ì •ì´ ì €ìž¥ë˜ì—ˆìŠµë‹ˆë‹¤. +managerServlet.savedContext=OK - 컨í…스트 [{0}]ì˜ ì„¤ì •ì´ ì €ìž¥ë˜ì—ˆìŠµë‹ˆë‹¤. +managerServlet.savedContextFail=실패 - Context [{0}] ì„¤ì •ì„ ì €ìž¥í•˜ì§€ 못했습니다. +managerServlet.serverInfo=OK - 서버 ì •ë³´\n\ +Tomcat 버전: [{0}]\n\ +ìš´ì˜ì²´ì œ ì´ë¦„: [{1}]\n\ +ìš´ì˜ì²´ì œ 버전: [{2}]\n\ +ìš´ì˜ì²´ì œ 아키í…처: [{3}]\n\ +JVM 버전: [{4}]\n\ +JVM 벤ë”: [{5}] +managerServlet.sessiondefaultmax=세션 비활성화 최대 ì‹œê°„ì˜ ê¸°ë³¸ ê°’ì€ [{0}]분입니다. +managerServlet.sessions=OK - 컨í…스트 경로 [{0}]ì˜ ì• í”Œë¦¬ì¼€ì´ì…˜ì„ 위한 세션 ì •ë³´ +managerServlet.sessiontimeout=[{0}]분: [{1}]ê°œì˜ ì„¸ì…˜ë“¤ +managerServlet.sessiontimeout.expired=[{0}]분: [{1}]ê°œì˜ ì„¸ì…˜ë“¤ì´ ë§Œë£Œë˜ì—ˆìŠµë‹ˆë‹¤. +managerServlet.sessiontimeout.unlimited=무제한 시간: [{0}] 세션들 +managerServlet.sslConnectorCerts=OK - Connector / ì¸ì¦ì„œ ì²´ì¸ ì •ë³´ +managerServlet.sslConnectorCiphers=OK - Connector / SSL Cipher ì •ë³´ +managerServlet.sslConnectorTrustedCerts=OK - Connector / 신뢰ë˜ëŠ” ì¸ì¦ì„œ ì •ë³´ +managerServlet.sslReload=OK - [{0}]ì„(를) 위해 TLS ì„¤ì •ì„ ë‹¤ì‹œ 로드했습니다. +managerServlet.sslReloadAll=OK - 모든 TLS ê°€ìƒ í˜¸ìŠ¤íŠ¸ë“¤ì„ ìœ„í•œ TLS ì„¤ì •ì„ ë‹¤ì‹œ 로드했습니다. +managerServlet.sslReloadFail=실패 - TLS ì„¤ì •ì„ ë‹¤ì‹œ 로드하지 못했습니다. +managerServlet.startFailed=실패 - 컨í…스트 경로 [{0}]ì˜ ì• í”Œë¦¬ì¼€ì´ì…˜ì´ ì‹œìž‘ë  ìˆ˜ 없었습니다. +managerServlet.started=OK - 컨í…스트 경로 [{0}]ì˜ ì• í”Œë¦¬ì¼€ì´ì…˜ì´ 시작ë˜ì—ˆìŠµë‹ˆë‹¤. +managerServlet.stopped=OK - 컨í…스트 경로 [{0}]ì˜ ì• í”Œë¦¬ì¼€ì´ì…˜ì„ 중지시켰습니다. +managerServlet.storeConfig.noMBean=실패 - [{0}]ì—ì„œ 등ë¡ëœ StoreConfig MBeanì´ ì—†ìŠµë‹ˆë‹¤. 보통 StoreConfigLifecycleListenerì— ì˜í•´ 등ë¡ì´ 수행ë©ë‹ˆë‹¤. +managerServlet.threaddump=OK - JVM 쓰레드 ë¤í”„ +managerServlet.trustedCertsNotConfigured=ì´ ê°€ìƒ í˜¸ìŠ¤íŠ¸ë¥¼ 위한 신뢰ë˜ëŠ” ì¸ì¦ì„œë“¤ì´ 설정ë˜ì–´ 있지 않습니다. +managerServlet.undeployed=OK - 컨í…스트 경로 [{0}]ì— ë°°ì¹˜ëœ ì• í”Œë¦¬ì¼€ì´ì…˜ì„ 제거했습니다. +managerServlet.unknownCommand=실패 - ì•Œ 수 없는 명령: [{0}] +managerServlet.vminfo=OK - VM ì •ë³´ + +statusServlet.complete=ì„œë²„ì˜ ì „ì²´ ìƒíƒœ +statusServlet.title=서버 ìƒíƒœ diff --git a/java/org/apache/catalina/manager/LocalStrings_pt.properties b/java/org/apache/catalina/manager/LocalStrings_pt.properties new file mode 100644 index 0000000..89a1a28 --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_pt.properties @@ -0,0 +1,16 @@ +# 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. + +htmlManagerServlet.serverIPAddress=Endereço IP diff --git a/java/org/apache/catalina/manager/LocalStrings_pt_BR.properties b/java/org/apache/catalina/manager/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..4babdbc --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_pt_BR.properties @@ -0,0 +1,23 @@ +# 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. + +htmlManagerServlet.appsAvailable=Executando +htmlManagerServlet.appsSessions=Sessões +htmlManagerServlet.serverIPAddress=Endereço IP + +managerServlet.copyError=Não foi possível copiar o arquivo de configuração do caminho [{0}] +managerServlet.deployed=OK - Instalada aplicação no path de contexto [{0}] +managerServlet.mkdirFail=FALHA - Incapaz de criar o diretório [{0}] +managerServlet.resourcesAll=Recursos de todos os tipos, listados. diff --git a/java/org/apache/catalina/manager/LocalStrings_ru.properties b/java/org/apache/catalina/manager/LocalStrings_ru.properties new file mode 100644 index 0000000..3bed140 --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_ru.properties @@ -0,0 +1,172 @@ +# 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. + +htmlManagerServlet.appsAvailable=Запущено +htmlManagerServlet.appsExpire=Завершить ÑеанÑÑ‹ +htmlManagerServlet.appsName=Ðазвание +htmlManagerServlet.appsPath=Путь +htmlManagerServlet.appsReload=Перезагрузить +htmlManagerServlet.appsSessions=СеанÑÑ‹ +htmlManagerServlet.appsStart=Старт +htmlManagerServlet.appsStop=Стоп +htmlManagerServlet.appsTasks=Команды +htmlManagerServlet.appsTitle=ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +htmlManagerServlet.appsUndeploy=Удалить +htmlManagerServlet.appsVersion=ВерÑÐ¸Ñ +htmlManagerServlet.configReloadButton=Перечитать +htmlManagerServlet.configSslHostName=Ð˜Ð¼Ñ TLS хоÑта (не обÑзательно) +htmlManagerServlet.configSslReloadTitle=Перечитать конфигурационные файлы TLS +htmlManagerServlet.configTitle=ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ +htmlManagerServlet.connectorStateAliveSocketCount=КоличеÑтво вÑÑ‘ ещё живых Ñокетов: +htmlManagerServlet.connectorStateBytesReceived=Байтов получено: +htmlManagerServlet.connectorStateBytesSent=Байтов отправлено: +htmlManagerServlet.connectorStateErrorCount=КоличеÑтво ошибок: +htmlManagerServlet.connectorStateHint=P: РазбираетÑÑ Ð¸ готовитÑÑ Ðº обработке, S: СервиÑ, F: Завершение, R: Готов, K: Продолжает ÑущеÑтвовать +htmlManagerServlet.connectorStateMaxProcessingTime=МакÑимальное Ð²Ñ€ÐµÐ¼Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸ запроÑа: +htmlManagerServlet.connectorStateMaxThreads=МакÑимум потоков: +htmlManagerServlet.connectorStateProcessingTime=Ð’Ñ€ÐµÐ¼Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸: +htmlManagerServlet.connectorStateRequestCount=КоличеÑтво запроÑов: +htmlManagerServlet.connectorStateTableTitleBRecv=Б Получено +htmlManagerServlet.connectorStateTableTitleBSent=Б Отправлено +htmlManagerServlet.connectorStateTableTitleClientAct=Клиент (ÐаÑтоÑщий) +htmlManagerServlet.connectorStateTableTitleClientForw=Клиент (Переданный) +htmlManagerServlet.connectorStateTableTitleRequest=Ð—Ð°Ð¿Ñ€Ð¾Ñ +htmlManagerServlet.connectorStateTableTitleStage=Этап +htmlManagerServlet.connectorStateTableTitleTime=Ð’Ñ€ÐµÐ¼Ñ +htmlManagerServlet.connectorStateTableTitleVHost=ВХоÑÑ‚ +htmlManagerServlet.connectorStateThreadBusy=Текущее чиÑло занÑÑ‚Ñ‹Ñ… потоков: +htmlManagerServlet.connectorStateThreadCount=Текущее чиÑло потоков: +htmlManagerServlet.deployButton=Развернуть +htmlManagerServlet.deployConfig=Путь XML файла конфигурации контекÑта: +htmlManagerServlet.deployPath=Путь: +htmlManagerServlet.deployServer=Развернуть Ñерверный WAR файл +htmlManagerServlet.deployTitle=Развернуть +htmlManagerServlet.deployUpload=WAR файл Ð´Ð»Ñ Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ +htmlManagerServlet.deployUploadFail=ОШИБКР- Ошибка при развёртывании: [{0}] +htmlManagerServlet.deployUploadFile=Выберите WAR файл Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸ +htmlManagerServlet.deployUploadInServerXml=ОШИБКР- War файл [{0}] не может быть загружен, eÑли он уже задан в файле server.xml +htmlManagerServlet.deployUploadNoFile=ОШИБКР- Ошибка при загрузке файла. Файла нет +htmlManagerServlet.deployUploadNotWar=ОШИБКР- Загружаемый файл должен быть Ñ Ñ€Ð°Ñширением .war +htmlManagerServlet.deployUploadWarExists=ОШИБКР- War файл [{0}] уже ÑущеÑтвует на Ñервере +htmlManagerServlet.deployVersion=ВерÑÐ¸Ñ (при параллельном развёртывании): +htmlManagerServlet.deployWar=WAR или путь до директории: +htmlManagerServlet.diagnosticsLeak=ПроверÑет, произошла ли утечка памÑти поÑле оÑтановки, перезагрузки или ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð²ÐµÐ±-приложений +htmlManagerServlet.diagnosticsLeakButton=Ðайти утечки памÑти +htmlManagerServlet.diagnosticsLeakWarning=Ð”Ð°Ð½Ð½Ð°Ñ Ð´Ð¸Ð°Ð³Ð½Ð¾Ñтика запуÑкает Ñборку муÑора. Будьте оÑторожны при иÑпользовании её на продуктивных ÑиÑтемах. +htmlManagerServlet.diagnosticsSsl=ДиагноÑтика конфигурации TLS Ð´Ð»Ñ ÐºÐ¾Ð½Ð½ÐµÐºÑ‚Ð¾Ñ€Ð¾Ð² +htmlManagerServlet.diagnosticsSslConnectorCertsButton=Сертификаты безопаÑноÑти +htmlManagerServlet.diagnosticsSslConnectorCertsText=СпиÑок Ñконфигурированных виртуальных TLS хоÑтов и цепочки Ñертификатов безопаÑноÑти Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ из них +htmlManagerServlet.diagnosticsSslConnectorCipherButton=Шифры +htmlManagerServlet.diagnosticsSslConnectorCipherText=СпиÑок виртуальных TLS хоÑтов и их шифры +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsButton=Доверенные Ñертификаты +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsText=СпиÑок виртуальных TLS хоÑтов и их доверенных Ñертификатов безопаÑноÑти +htmlManagerServlet.diagnosticsTitle=ДиагноÑтика +htmlManagerServlet.expire.explain=Ñ Ð½ÐµÐ°ÐºÑ‚Ð¸Ð²Ð½Ð¾Ñтью ≥ +htmlManagerServlet.expire.unit=минут +htmlManagerServlet.findleaksList=Следующие веб-Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð±Ñ‹Ð»Ð¸ оÑтановлены (перезагружены, удалены),\n\ +но их клаÑÑÑ‹ Ñ Ð¿Ñ€ÐµÐ´Ñ‹Ð´ÑƒÑ‰Ð¸Ñ… запуÑков до Ñих пор приÑутÑтвуют в памÑти\n\ +(иÑпользуйте программу-профайлер, чтобы подтвердить наличие проблемы):\n +htmlManagerServlet.findleaksNone=КажетÑÑ, что веб-Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½Ðµ вызвали утечку памÑти при оÑтановке, перезагрузке или удалении. +htmlManagerServlet.helpHtmlManager=Справка Ð´Ð»Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»ÐµÐ¹ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +htmlManagerServlet.helpHtmlManagerFile=../docs/html-manager-howto.html +htmlManagerServlet.helpManager=Справка по API Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +htmlManagerServlet.helpManagerFile=../docs/manager-howto.html +htmlManagerServlet.jvmFreeMemory=Ð¡Ð²Ð¾Ð±Ð¾Ð´Ð½Ð°Ñ Ð¿Ð°Ð¼ÑÑ‚ÑŒ: +htmlManagerServlet.jvmMaxMemory=МакÑимум памÑти: +htmlManagerServlet.jvmTableTitleInitial=Изначально +htmlManagerServlet.jvmTableTitleMaximum=МакÑимум +htmlManagerServlet.jvmTableTitleMemoryPool=ОблаÑти памÑти +htmlManagerServlet.jvmTableTitleTotal=Ð’Ñего +htmlManagerServlet.jvmTableTitleType=Тип +htmlManagerServlet.jvmTableTitleUsed=ИÑпользуетÑÑ +htmlManagerServlet.jvmTotalMemory=Ð’Ñего памÑти: +htmlManagerServlet.list=СпиÑок приложений +htmlManagerServlet.manager=Менеджер +htmlManagerServlet.messageLabel=Сообщение: +htmlManagerServlet.noManager=- +htmlManagerServlet.noVersion=Ðе указано +htmlManagerServlet.osAvailableMemory=ДоÑÑ‚ÑƒÐ¿Ð½Ð°Ñ Ð¿Ð°Ð¼ÑÑ‚ÑŒ: +htmlManagerServlet.osFreePageFile=Свободные файловые Ñтраницы: +htmlManagerServlet.osKernelTime=Ð’Ñ€ÐµÐ¼Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸ процеÑÑа Ñдром: +htmlManagerServlet.osMemoryLoad=ПамÑти загружено: +htmlManagerServlet.osPhysicalMemory=ФизичеÑÐºÐ°Ñ Ð¿Ð°Ð¼ÑÑ‚ÑŒ: +htmlManagerServlet.osTotalPageFile=Ð’Ñего файловых Ñтраниц: +htmlManagerServlet.osUserTime=Ð’Ñ€ÐµÐ¼Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸ пользователÑ: +htmlManagerServlet.serverHostname=Ð˜Ð¼Ñ Ñ…Ð¾Ñта +htmlManagerServlet.serverIPAddress=IP ÐÐ´Ñ€ÐµÑ +htmlManagerServlet.serverJVMVendor=ПоÑтавщик JVM +htmlManagerServlet.serverJVMVersion=ВерÑÐ¸Ñ JVM +htmlManagerServlet.serverOSArch=Ðрхитектура ОС +htmlManagerServlet.serverOSName=ОС +htmlManagerServlet.serverOSVersion=ВерÑÐ¸Ñ ÐžÐ¡ +htmlManagerServlet.serverTitle=Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ Ñервере +htmlManagerServlet.serverVersion=ВерÑÐ¸Ñ Tomcat +htmlManagerServlet.title=Управление веб-приложениÑми Tomcat + +managerServlet.alreadyContext=ОШИБКР- Приложение уже ÑущеÑтвует на пути [{0}] +managerServlet.certsNotAvailable=Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ Ñертификате не может быть получена во Ð²Ñ€ÐµÐ¼Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ +managerServlet.deleteFail=ОШИБКР- Ðе удалоÑÑŒ удалить [{0}]. +managerServlet.deployFailed=ОШИБКР- Ðе удалоÑÑŒ развернуть приложение на контекÑтном пути [{0}] +managerServlet.deployed=OK - Приложение уÑпешно развёрнуто в контекÑтном пути [{0}] +managerServlet.deployedButNotStarted=ОШИБКР- Приложение было развёрнуто в контекÑтном пути [{0}], но не Ñтартовало +managerServlet.exception=ОШИБКР- Ð’ÑтретилоÑÑŒ иÑключение [{0}] +managerServlet.findleaksFail=ОШИБКР- Ðе удалоÑÑŒ найти утечки памÑти: Host not instance of StandardHost +managerServlet.findleaksList=OK - Ðайдены потенциальные утечки памÑти в Ñледующих приложениÑÑ…: +managerServlet.findleaksNone=OK - Ðе найдено утечек памÑти +managerServlet.inService=ОШИБКР- Приложение [{0}] уже обÑлуживаетÑÑ +managerServlet.invalidCommand=ОШИБКР- ÐедопуÑтимые параметры, предоÑтавленные Ð´Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ñ‹ [{0}] +managerServlet.invalidPath=ОШИБКР- Указан недопуÑтимый контекÑтный путь [{0}] +managerServlet.listed=OK - СпиÑок приложений Ð´Ð»Ñ Ð²Ð¸Ñ€Ñ‚ÑƒÐ°Ð»ÑŒÐ½Ð¾Ð³Ð¾ хоÑта [{0}] +managerServlet.mkdirFail=ОШИБКР- Ðе удалоÑÑŒ Ñоздать директорию [{0}] +managerServlet.noCommand=ОШИБКР- Команда не указана. +managerServlet.noContext=ОШИБКР- КонтекÑÑ‚ не ÑущеÑтвует [{0}] +managerServlet.noGlobal=ОШИБКР- Глобальные реÑурÑÑ‹ JNDI недоÑтупны +managerServlet.noManager=ОШИБКР- ОтÑутÑтвует менеджер Ð´Ð»Ñ Ð¿ÑƒÑ‚Ð¸ [{0}] +managerServlet.noSelf=ОШИБКР- Менеджер не может перезагрузить, развернуть, оÑтановить или удалить ÑÐµÐ±Ñ +managerServlet.noWrapper=Контейнер не вызвал setWrapper() Ð´Ð»Ñ Ñтого Ñервлета +managerServlet.notDeployed=ОШИБКР- [{0}] определен в файле server.xml и не может быть развёрнут +managerServlet.notSslConnector=Протокол SSL/TLS Ð´Ð»Ñ Ñтого коннектора не включен +managerServlet.objectNameFail=ОШИБКР- Ðе удалоÑÑŒ зарегиÑтрировать Ð¸Ð¼Ñ Ð¾Ð±ÑŠÐµÐºÑ‚Ð° [{0}] Ð´Ð»Ñ Manager Servlet +managerServlet.postCommand=ОШИБКР- Попытка иÑпользовать команду [{0}] через Ð·Ð°Ð¿Ñ€Ð¾Ñ GET но требуетÑÑ POST +managerServlet.reloaded=OK - Приложение по пути контекÑта [{0}] было перезагружено +managerServlet.renameFail=ОШИБКР- Ðевозможно переименовать [{0}] в [{1}]. Это может вызвать проблемы Ð´Ð»Ñ Ð±ÑƒÐ´ÑƒÑ‰Ð¸Ñ… развертываний. +managerServlet.resourcesAll=OK - ПеречиÑлены глобальные реÑурÑÑ‹ вÑех видов +managerServlet.resourcesType=OK - ПеречиÑлены глобальные реÑурÑÑ‹ вида [{0}] +managerServlet.saveFail=ОШИБКР- Ðе удалоÑÑŒ Ñохранить наÑтройки: [{0}] +managerServlet.saved=OK - ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ñервера Ñохранена +managerServlet.savedContext=OK - КонтекÑтные наÑтройки Ð´Ð»Ñ [{0}] Ñохранены +managerServlet.sessiondefaultmax=Стандартный макÑимальный период неактивного ÑеанÑа: [{0}] минут +managerServlet.sessions=OK - Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ ÑеанÑах Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾ пути контекÑта [{0}] +managerServlet.sessiontimeout=Ðеактивные [{0}] минут: [{1}] ÑеанÑ(ов) +managerServlet.sessiontimeout.expired=Ðеактивные [{0}] минут: [{1}] ÑеанÑ(ов) были завершены +managerServlet.sessiontimeout.unlimited=Ðеограниченное времÑ: [{0}] ÑеанÑов +managerServlet.sslConnectorCerts=OK - Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ цепочке Ñертификатов +managerServlet.sslConnectorCiphers=OK - Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ шифровании SSL +managerServlet.sslConnectorTrustedCerts=OK - Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ доверенном Ñертификате безопаÑноÑти +managerServlet.sslReload=OK - Перезагрузка конфигурации TLS Ð´Ð»Ñ [{0}] +managerServlet.sslReloadAll=OK - Перезагрузка конфигурации TLS Ð´Ð»Ñ Ð²Ñех виртуальных хоÑтов TLS +managerServlet.sslReloadFail=ОШИБКР- Ðе удалоÑÑŒ перезагрузить конфигурацию TLS +managerServlet.startFailed=ОШИБКР- Приложение по контекÑтному пути [{0}] не запуÑтилоÑÑŒ +managerServlet.started=OK - Запущено приложение по пути контекÑта [{0}] +managerServlet.stopped=OK - ОÑтановлено приложение по пути контекÑта [{0}] +managerServlet.storeConfig.noMBean=ОШИБКР- Ðет StoreConfig MBean, зарегиÑтрированного на [{0}]. РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ð¾Ð±Ñ‹Ñ‡Ð½Ð¾ проводитÑÑ StoreConfigLifecycleListener. +managerServlet.threaddump=OK - JVM thread dump +managerServlet.trustedCertsNotConfigured=Ð”Ð»Ñ Ñтого виртуального хоÑта не наÑтроены доверенные Ñертификаты безопаÑноÑти +managerServlet.undeployed=OK - Удалено приложение по пути контекÑта [{0}] +managerServlet.unknownCommand=ОШИБКР- ÐеизвеÑÑ‚Ð½Ð°Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° [{0}] +managerServlet.vminfo=OK - VM инфо + +statusServlet.complete=Подробный отчёт о ÑоÑтоÑнии +statusServlet.title=СоÑтоÑние Ñервера diff --git a/java/org/apache/catalina/manager/LocalStrings_zh_CN.properties b/java/org/apache/catalina/manager/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..02dc6b8 --- /dev/null +++ b/java/org/apache/catalina/manager/LocalStrings_zh_CN.properties @@ -0,0 +1,195 @@ +# 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. + +htmlManagerServlet.appsAvailable=è¿è¡Œä¸­ +htmlManagerServlet.appsExpire=è¿‡æœŸä¼šè¯ +htmlManagerServlet.appsName=显示.å称 +htmlManagerServlet.appsPath=路径 +htmlManagerServlet.appsReload=é‡æ–°åŠ è½½ +htmlManagerServlet.appsSessions=ä¼šè¯ +htmlManagerServlet.appsStart=å¯åŠ¨ +htmlManagerServlet.appsStop=åœæ­¢ +htmlManagerServlet.appsTasks=命令 +htmlManagerServlet.appsTitle=åº”ç”¨ç¨‹åº +htmlManagerServlet.appsUndeploy=å¸è½½ +htmlManagerServlet.appsVersion=ç‰ˆæœ¬å· +htmlManagerServlet.configReloadButton=é‡å¤è¯» +htmlManagerServlet.configSslHostName=TLS 主机å字(å¯é€‰) +htmlManagerServlet.configSslReloadTitle=é‡æ–°è¯»å– TLS é…置文件 +htmlManagerServlet.configTitle=é…ç½® +htmlManagerServlet.connectorStateAliveSocketCount=存活套接字总数: +htmlManagerServlet.connectorStateBytesReceived=收到字节: +htmlManagerServlet.connectorStateBytesSent=å‘é€.字节: +htmlManagerServlet.connectorStateErrorCount=错误数: +htmlManagerServlet.connectorStateHint=P:解æžå’Œå‡†å¤‡request S:æœåŠ¡ Fï¼šç»“æŸ R:就绪 K:存活 +htmlManagerServlet.connectorStateMaxProcessingTime=最大处ç†æ—¶é—´ï¼š +htmlManagerServlet.connectorStateMaxThreads=最大线程: +htmlManagerServlet.connectorStateProcessingTime=处ç†æ—¶é—´ï¼š +htmlManagerServlet.connectorStateRequestCount=请求总数: +htmlManagerServlet.connectorStateTableTitleBRecv=接收字节 +htmlManagerServlet.connectorStateTableTitleBSent=å‘é€å­—节: +htmlManagerServlet.connectorStateTableTitleClientAct=客户端(实际) +htmlManagerServlet.connectorStateTableTitleClientForw=客户端(转å‘) +htmlManagerServlet.connectorStateTableTitleRequest=请求 +htmlManagerServlet.connectorStateTableTitleStage=阶段 +htmlManagerServlet.connectorStateTableTitleTime=时间 +htmlManagerServlet.connectorStateTableTitleVHost=虚拟主机 +htmlManagerServlet.connectorStateThreadBusy=当å‰çº¿ç¨‹ç¹å¿™ï¼š +htmlManagerServlet.connectorStateThreadCount=当å‰çº¿ç¨‹æ•°ï¼š +htmlManagerServlet.deployButton=部署 +htmlManagerServlet.deployConfig=XMLé…置文件路径: +htmlManagerServlet.deployPath=Context路径: +htmlManagerServlet.deployServer=æœåŠ¡å™¨ä¸Š.部署的目录或WAR文件 +htmlManagerServlet.deployTitle=部署 +htmlManagerServlet.deployUpload=è¦éƒ¨ç½²çš„WAR文件 +htmlManagerServlet.deployUploadFail=失败 - 部署上传失败,异常信æ¯ï¼š[{0}] +htmlManagerServlet.deployUploadFile=选择è¦ä¸Šä¼ çš„WAR文件 +htmlManagerServlet.deployUploadInServerXml=失败 - 如果context是定义在server.xml中,War文件[{0}]无法上传 +htmlManagerServlet.deployUploadNoFile=失败 - 文件上传失败,没有文件 +htmlManagerServlet.deployUploadNotWar=失败 - 上传的文件[{0}]必须是一个.war文件 +htmlManagerServlet.deployUploadWarExists=失败 - War文件 [{0}] 已存在于æœåŠ¡å™¨ä¸Š +htmlManagerServlet.deployVersion=版本å·ï¼ˆå¹¶è¡Œéƒ¨ç½²ï¼‰ +htmlManagerServlet.deployWar=WAR文件或文件夹路径: +htmlManagerServlet.diagnosticsLeak=检查Web应用程åºæ˜¯å¦åœ¨åœæ­¢ã€é‡æ–°åŠ è½½æˆ–å–æ¶ˆéƒ¨ç½²æ—¶å¯¼è‡´å†…å­˜æ³„æ¼ +htmlManagerServlet.diagnosticsLeakButton=å‘çŽ°æ³„æ¼ +htmlManagerServlet.diagnosticsLeakWarning=诊断检查将触å‘完整的垃圾收集,在生产系统中使用时è¦æ ¼å¤–å°å¿ƒã€‚ +htmlManagerServlet.diagnosticsSsl=TLS连接器é…置诊断 +htmlManagerServlet.diagnosticsSslConnectorCertsButton=è¯ä¹¦ +htmlManagerServlet.diagnosticsSslConnectorCertsText=列出已é…置的TLS虚拟主机以åŠå„自的è¯ä¹¦é“¾ +htmlManagerServlet.diagnosticsSslConnectorCipherButton=å¯†ç  +htmlManagerServlet.diagnosticsSslConnectorCipherText=列出æ¯ä¸ªé…置好的TLS虚拟主机和密ç ã€‚ +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsButton=认è¯è¯ä¹¦ +htmlManagerServlet.diagnosticsSslConnectorTrustedCertsText=列出æ¯ä¸ªé…置好的TLS虚拟主机和认è¯è¯ä¹¦ã€‚ +htmlManagerServlet.diagnosticsTitle=诊断 +htmlManagerServlet.error.sessions=错误的处ç†ä¼šè¯çš„命令,context [{0}] +htmlManagerServlet.error.sortOrder=未知排åº[{0}] +htmlManagerServlet.expire.explain=闲置 ≥ +htmlManagerServlet.expire.unit=分钟 +htmlManagerServlet.findleaksList=以下web应用程åºå·²åœæ­¢ï¼ˆé‡æ–°åŠ è½½ã€å¸è½½ï¼‰ï¼Œä½†æ˜¯å®ƒä»¬çš„ç±»ä»ç„¶åŠ è½½åœ¨å†…存中,因此导致了内存泄æ¼ï¼ˆä½¿ç”¨åˆ†æžå™¨ç¡®è®¤ï¼‰ï¼š +htmlManagerServlet.findleaksNone=似乎没有任何Web应用程åºåœ¨åœæ­¢ã€é‡æ–°åŠ è½½æˆ–å–消部署时触å‘内存泄æ¼ã€‚ +htmlManagerServlet.helpHtmlManager=HTML管ç†å™¨å¸®åŠ© +htmlManagerServlet.helpHtmlManagerFile=../docs/html-manager-howto.html +htmlManagerServlet.helpManager=管ç†è€…帮助 +htmlManagerServlet.helpManagerFile=../docs/manager-howto.html +htmlManagerServlet.jvmFreeMemory=剩余内存: +htmlManagerServlet.jvmMaxMemory=最大内存 +htmlManagerServlet.jvmTableTitleInitial=åˆå§‹åŒ– +htmlManagerServlet.jvmTableTitleMaximum=最大.值 +htmlManagerServlet.jvmTableTitleMemoryPool=内存.æ±  +htmlManagerServlet.jvmTableTitleTotal=总共 +htmlManagerServlet.jvmTableTitleType=类型 +htmlManagerServlet.jvmTableTitleUsed=已用 +htmlManagerServlet.jvmTotalMemory=总内存: +htmlManagerServlet.list=应用程åºåˆ—表 +htmlManagerServlet.manager=管ç†å™¨ +htmlManagerServlet.messageLabel=消æ¯ï¼š +htmlManagerServlet.noManager=- +htmlManagerServlet.noVersion=未指定 +htmlManagerServlet.osAvailableMemory=å¯ç”¨å†…存: +htmlManagerServlet.osFreePageFile=å¯ç”¨é¡µæ–‡ä»¶ï¼š +htmlManagerServlet.osKernelTime=内核处ç†æ—¶é—´ï¼š +htmlManagerServlet.osMemoryLoad=加载内存: +htmlManagerServlet.osPhysicalMemory=物ç†å†…å­˜ +htmlManagerServlet.osTotalPageFile=最大页文件: +htmlManagerServlet.osUserTime=用户æ€å¤„ç†å®žé™…: +htmlManagerServlet.serverHostname=主机å +htmlManagerServlet.serverIPAddress=IPåœ°å€ +htmlManagerServlet.serverJVMVendor=JVMæ供商 +htmlManagerServlet.serverJVMVersion=JVM.版本 +htmlManagerServlet.serverOSArch=æ“作系统架构 +htmlManagerServlet.serverOSName=OS.å称 +htmlManagerServlet.serverOSVersion=æ“作系统版本 +htmlManagerServlet.serverTitle=æœåŠ¡å™¨ä¿¡æ¯ +htmlManagerServlet.serverVersion=Tomcat.版本 +htmlManagerServlet.title=Tomcat Web应用程åºç®¡ç†è€… + +jmxProxyServlet.noBeanFound=无法找到å为[{0}]çš„MBean对象 +jmxProxyServlet.noOperationOnBean=在对象å为[{2}]çš„æ“作[{0}]中找ä¸åˆ°å‚数为[{1}]çš„æ“作[{0}],该æ“作是[{3}] + +managerServlet.alreadyContext=失败 - 应用程åºå·²å­˜åœ¨äºŽè·¯å¾„ [{0}] +managerServlet.certsNotAvailable=在è¿è¡ŒæœŸï¼Œæ— æ³•ä»Žè¿žæŽ¥å™¨ä¸­èŽ·å–认è¯ä¿¡æ¯ +managerServlet.copyError=无法从路径[{0}]æ‹·è´é…置文件 +managerServlet.deleteFail=失败 - ä¸èƒ½åˆ é™¤[{0}]。这个文件一直存在会出现问题。 +managerServlet.deployFailed=FAIL - 在上下文路径[{0}]下部署应用失败 +managerServlet.deployed=OK - 以应用path [{0}] 部署应用 +managerServlet.deployedButNotStarted=失败 - 在上下文[{0}]下部署应用程åºï¼Œä½†æ˜¯ä¸Šä¸‹æ–‡å¯åŠ¨å¤±è´¥ã€‚ +managerServlet.error.deploy=部署[{0}]错误 +managerServlet.error.idleParam=无法解æžé—²ç½®æ•´æ•°å‚æ•°[{0}] +managerServlet.error.reload=é‡æ–°åŠ è½½[{0}]错误 +managerServlet.error.resources=显示类型[{0}]的资æºé”™è¯¯ +managerServlet.error.serverInfo=显示æœåŠ¡å™¨ä¿¡æ¯é”™è¯¯ +managerServlet.error.sessions=显示上下文[{0}]的会è¯ä¿¡æ¯é”™è¯¯ +managerServlet.error.start=å¯åŠ¨[{0}]失败 +managerServlet.error.stop=åœæ­¢[{0}]错误 +managerServlet.error.storeConfig=ä¿å­˜é…置错误 +managerServlet.error.storeContextConfig=上下文[{0}]存储é…置错误 +managerServlet.error.undeploy=å¸è½½[{0}]错误 +managerServlet.exception=失败 - å‘生异常[{0}] +managerServlet.findleaksFail=失败 - 查找泄æ¼å¤±è´¥ï¼šä¸»æœºéžStandardHost示例 +managerServlet.findleaksList=OK - 在以下应用程åºä¸­å‘çŽ°æ½œåœ¨çš„å†…å­˜æ³„æ¼ +managerServlet.findleaksNone=OK - 没有å‘çŽ°å†…å­˜æ³„æ¼ +managerServlet.inService=失败 - 应用程åº[{0}]å·²æœåŠ¡ +managerServlet.invalidCommand=失败 - 无效å‚数,命令[{0}] +managerServlet.invalidPath=失败 - 指定了无效到上下文路径[{0}] +managerServlet.listed=OK - 虚拟主机[{0}]下的应用程åºåˆ—表 +managerServlet.mkdirFail=失败 - 无法创建目录 [{0}] +managerServlet.noCommand=失败 - 未指定命令 +managerServlet.noContext=失败 - 上下文环境[{0}]ä¸å­˜åœ¨ +managerServlet.noGlobal=失败 - 没有å¯ç”¨çš„全局 JNDI èµ„æº +managerServlet.noManager=失败 - 路径[{0}]下无管ç†å™¨å­˜åœ¨ +managerServlet.noSelf=失败 - 管ç†å™¨ä¸èƒ½é‡æ–°åŠ è½½ã€å¸è½½ã€åœæ­¢æˆ–者å¸è½½å®ƒè‡ªèº« +managerServlet.noWrapper=容器未为当å‰servlet调用setWrapper() +managerServlet.notDeployed=失败 - 上下文[{0}]定义在server.xml中且å¯èƒ½æœªå¸è½½ +managerServlet.notSslConnector=ä¸å…许SSL连接 +managerServlet.objectNameFail=FAIL - ä¸èƒ½å°†ä¸ºManager Servlet 注册 object name [{0}] +managerServlet.postCommand=失败 - å°è¯•é€šè¿‡GET请求使用命令[{0}],但需è¦POST +managerServlet.reloaded=OK - 上下文路径[{0}]é‡æ–°åŠ è½½åº”ç”¨ç¨‹åº +managerServlet.renameFail=失败 - 无法é‡å‘½å[{0}]为[{1}],这å¯èƒ½è¿™æœªæ¥éƒ¨ç½²æ—¶å‡ºçŽ°é—®é¢˜ã€‚ +managerServlet.resourcesAll=OK - åˆ—å‡ºæ‰€æœ‰ç±»åž‹çš„å…¨éƒ¨èµ„æº +managerServlet.resourcesType=OK - [{0}]类型全局资æºåˆ—å…¥æ¸…å• +managerServlet.saveFail=失败 - é…ç½®ä¿å­˜å¤±è´¥ï¼š[{0}] +managerServlet.saved=OK - æœåŠ¡å™¨é…置已ä¿å­˜ +managerServlet.savedContext=OK - 上下文[{0}]é…置已ä¿å­˜ +managerServlet.savedContextFail=失败 - 上下文[{0}]é…ç½®ä¿å­˜å¤±è´¥ +managerServlet.serverInfo=OK - æœåŠ¡å™¨ä¿¡æ¯\n\ +Tomcat版本: [{0}]\n\ +æ“作系统å称: [{1}]\n\ +æ“作系统版本: [{2}]\n\ +æ“作系统架构: [{3}]\n\ +JVM版本: [{4}]\n\ +JVMæ供商: [{5}] +managerServlet.sessiondefaultmax=最大会è¯æ—¶é—´å¤±æ´»é—´éš”是[{0}]分钟 +managerServlet.sessions=OK - 上下文路径[{0}]的会è¯ä¿¡æ¯ +managerServlet.sessiontimeout=失活[{0}]分钟:[{1}]ä¼šè¯ +managerServlet.sessiontimeout.expired=失活[{0}]分钟:[{1}]会è¯è¿‡æœŸ +managerServlet.sessiontimeout.unlimited=æ— é™åˆ¶åˆ°æ—¶é—´ï¼š[{0}]ä¼šè¯ +managerServlet.sslConnectorCerts=OK - 连接器/认è¯é“¾ä¿¡æ¯ +managerServlet.sslConnectorCiphers=OK - Connector/SSL 密ç .ä¿¡æ¯ +managerServlet.sslConnectorTrustedCerts=OK - 连接器/认è¯è¯ä¹¦ä¿¡æ¯ +managerServlet.sslReload=OK - 为[{0}]é‡æ–°åŠ è½½TLSé…ç½® +managerServlet.sslReloadAll=OK - 未所有TLS虚拟主机é‡æ–°åŠ è½½TLSé…ç½® +managerServlet.sslReloadFail=FAIL - é‡æ–°åŠ è½½TLSé…制失败 +managerServlet.startFailed=失败 - 上下文路径[{0}]下,应用程åºæ— æ³•å¯åŠ¨ +managerServlet.started=OK - 在上下文路径[{0}]下å¯åŠ¨åº”ç”¨ç¨‹åº +managerServlet.stopped=OK - 上下文路径[{0}]下åœæ­¢åº”ç”¨ç¨‹åº +managerServlet.storeConfig.noMBean=失败 - [{0}]未å‘现注册的StoreConfig MBean,注册一般是由StoreConfigLifecycleListener执行的。 +managerServlet.threaddump=OK - JVM线程转储 +managerServlet.trustedCertsNotConfigured=没有为此虚拟主机é…ç½®å—信任的è¯ä¹¦ +managerServlet.undeployed=OK - 未部署的应用ä½äºŽä¸Šä¸‹æ–‡è·¯å¾„[{0}] +managerServlet.unknownCommand=FAIL - 未知命令 [{0}] +managerServlet.vminfo=OK - VMä¿¡æ¯ + +statusServlet.complete=完整的æœåŠ¡å™¨çŠ¶æ€ +statusServlet.title=æœåŠ¡å™¨çŠ¶æ€ diff --git a/java/org/apache/catalina/manager/ManagerServlet.java b/java/org/apache/catalina/manager/ManagerServlet.java new file mode 100644 index 0000000..8b458c6 --- /dev/null +++ b/java/org/apache/catalina/manager/ManagerServlet.java @@ -0,0 +1,1673 @@ +/* + * 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.catalina.manager; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.naming.Binding; +import javax.naming.NamingEnumeration; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.UnavailableException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.ContainerServlet; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.Manager; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.Session; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.util.IOTools; +import org.apache.catalina.util.ServerInfo; +import org.apache.coyote.ProtocolHandler; +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.tomcat.util.Diagnostics; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.net.SSLContext; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLUtilBase; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; + + +/** + * Servlet that enables remote management of the web applications installed within the same virtual host as this web + * application is. Normally, this functionality will be protected by a security constraint in the web application + * deployment descriptor. However, this requirement can be relaxed during testing. + *

    + * This servlet examines the value returned by getPathInfo() and related query parameters to determine what + * action is being requested. The following actions and parameters (starting after the servlet path) are supported: + *

      + *
    • /deploy?config={config-url} - Install and start a new web application, based on the contents of the + * context configuration file found at the specified URL. The docBase attribute of the context + * configuration file is used to locate the actual WAR or directory containing the application.
    • + *
    • /deploy?config={config-url}&war={war-url}/ - Install and start a new web application, based on the + * contents of the context configuration file found at {config-url}, overriding the docBase + * attribute with the contents of the web application archive found at {war-url}.
    • + *
    • /deploy?path=/xxx&war={war-url} - Install and start a new web application attached to context path + * /xxx, based on the contents of the web application archive found at the specified URL.
    • + *
    • /list - List the context paths of all currently installed web applications for this virtual host. Each + * context will be listed with the following format path:status:sessions. Where path is the context path. + * Status is either running or stopped. Sessions is the number of active Sessions.
    • + *
    • /reload?path=/xxx - Reload the Java classes and resources for the application at the specified path.
    • + *
    • /resources?type=xxxx - Enumerate the available global JNDI resources, optionally limited to those of the + * specified type (fully qualified Java class name), if available.
    • + *
    • /serverinfo - Display system OS and JVM properties. + *
    • /sessions - Deprecated. Use expire. + *
    • /expire?path=/xxx - List session idle time information about the web application attached to context path + * /xxx for this virtual host.
    • + *
    • /expire?path=/xxx&idle=mm - Expire sessions for the context path /xxx which were idle for + * at least mm minutes.
    • + *
    • /sslConnectorCiphers - Display diagnostic info on SSL/TLS ciphers that are currently configured for each + * connector. + *
    • /start?path=/xxx - Start the web application attached to context path /xxx for this virtual + * host.
    • + *
    • /stop?path=/xxx - Stop the web application attached to context path /xxx for this virtual + * host.
    • + *
    • /threaddump - Write a JVM thread dump.
    • + *
    • /undeploy?path=/xxx - Shutdown and remove the web application attached to context path /xxx + * for this virtual host, and remove the underlying WAR file or document base directory. (NOTE - This is only + * allowed if the WAR file or document base is stored in the appBase directory of this host, typically as a + * result of being placed there via the /deploy command.
    • + *
    • /vminfo - Write some VM info.
    • + *
    • /save - Save the current server configuration to server.xml
    • + *
    • /save?path=/xxx - Save the context configuration for the web application deployed with path + * /xxx to an appropriately named context.xml file in the xmlBase for the associated + * Host.
    • + *
    + *

    + * Use path=/ for the ROOT context. + *

    + *

    + * The syntax of the URL for a web application archive must conform to one of the following patterns to be successfully + * deployed: + *

    + *
      + *
    • file:/absolute/path/to/a/directory - You can specify the absolute path of a directory that contains the + * unpacked version of a web application. This directory will be attached to the context path you specify without any + * changes.
    • + *
    + *

    + * NOTE - Attempting to reload or remove the application containing this servlet itself will not succeed. + * Therefore, this servlet should generally be deployed as a separate web application within the virtual host to be + * managed. + *

    + * The following servlet initialization parameters are recognized: + *

      + *
    • debug - The debugging detail level that controls the amount of information that is logged by this servlet. + * Default is zero. + *
    + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class ManagerServlet extends HttpServlet implements ContainerServlet { + + private static final long serialVersionUID = 1L; + + // ----------------------------------------------------- Instance Variables + + + /** + * Path where context descriptors should be deployed. + */ + protected File configBase = null; + + + /** + * The Context container associated with our web application. + */ + protected transient Context context = null; + + + /** + * The debugging detail level for this servlet. + */ + protected int debug = 1; + + + /** + * Path used to store revisions of webapps. + */ + protected File versioned = null; + + + /** + * The associated host. + */ + protected transient Host host = null; + + + /** + * MBean server. + */ + protected transient MBeanServer mBeanServer = null; + + + /** + * The associated deployer ObjectName. + */ + protected ObjectName oname = null; + + + /** + * The global JNDI NamingContext for this server, if available. + */ + protected transient javax.naming.Context global = null; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + + /** + * The Wrapper container associated with this servlet. + */ + protected transient Wrapper wrapper = null; + + + // ----------------------------------------------- ContainerServlet Methods + + + /** + * Return the Wrapper with which we are associated. + */ + @Override + public Wrapper getWrapper() { + return this.wrapper; + } + + + /** + * Set the Wrapper with which we are associated. + * + * @param wrapper The new wrapper + */ + @Override + public void setWrapper(Wrapper wrapper) { + + this.wrapper = wrapper; + if (wrapper == null) { + context = null; + host = null; + oname = null; + } else { + context = (Context) wrapper.getParent(); + host = (Host) context.getParent(); + Engine engine = (Engine) host.getParent(); + String name = engine.getName() + ":type=Deployer,host=" + host.getName(); + try { + oname = new ObjectName(name); + } catch (Exception e) { + log(sm.getString("managerServlet.objectNameFail", name), e); + } + } + + // Retrieve the MBean server + mBeanServer = Registry.getRegistry(null, null).getMBeanServer(); + + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Finalize this servlet. + */ + @Override + public void destroy() { + + // No actions necessary + + } + + + /** + * Process a GET request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + + // Identify the request parameters that we need + String command = request.getPathInfo(); + if (command == null) { + command = request.getServletPath(); + } + + String path = request.getParameter("path"); + String war = request.getParameter("war"); + String config = request.getParameter("config"); + ContextName cn = null; + if (path != null) { + cn = new ContextName(path, request.getParameter("version")); + } else if (config != null) { + cn = ContextName.extractFromPath(config); + } else if (war != null) { + cn = ContextName.extractFromPath(war); + } + + String type = request.getParameter("type"); + String tag = request.getParameter("tag"); + boolean update = false; + if (request.getParameter("update") != null && request.getParameter("update").equals("true")) { + update = true; + } + String tlsHostName = request.getParameter("tlsHostName"); + + boolean statusLine = false; + if ("true".equals(request.getParameter("statusLine"))) { + statusLine = true; + } + + // Prepare our output writer to generate the response message + response.setContentType("text/plain; charset=" + Constants.CHARSET); + // Stop older versions of IE thinking they know best. We set text/plain + // in the line above for a reason. IE's behaviour is unwanted at best + // and dangerous at worst. + response.setHeader("X-Content-Type-Options", "nosniff"); + PrintWriter writer = response.getWriter(); + + // Process the requested command + if (command == null) { + writer.println(smClient.getString("managerServlet.noCommand")); + } else if (command.equals("/deploy")) { + if (war != null || config != null) { + deploy(writer, config, cn, war, update, smClient); + } else if (tag != null) { + deploy(writer, cn, tag, smClient); + } else { + writer.println(smClient.getString("managerServlet.invalidCommand", command)); + } + } else if (command.equals("/list")) { + list(writer, smClient); + } else if (command.equals("/reload")) { + reload(writer, cn, smClient); + } else if (command.equals("/resources")) { + resources(writer, type, smClient); + } else if (command.equals("/save")) { + save(writer, path, smClient); + } else if (command.equals("/serverinfo")) { + serverinfo(writer, smClient); + } else if (command.equals("/sessions")) { + expireSessions(writer, cn, request, smClient); + } else if (command.equals("/expire")) { + expireSessions(writer, cn, request, smClient); + } else if (command.equals("/start")) { + start(writer, cn, smClient); + } else if (command.equals("/stop")) { + stop(writer, cn, smClient); + } else if (command.equals("/undeploy")) { + undeploy(writer, cn, smClient); + } else if (command.equals("/findleaks")) { + findleaks(statusLine, writer, smClient); + } else if (command.equals("/vminfo")) { + vmInfo(writer, smClient, request.getLocales()); + } else if (command.equals("/threaddump")) { + threadDump(writer, smClient, request.getLocales()); + } else if (command.equals("/sslConnectorCiphers")) { + sslConnectorCiphers(writer, smClient); + } else if (command.equals("/sslConnectorCerts")) { + sslConnectorCerts(writer, smClient); + } else if (command.equals("/sslConnectorTrustedCerts")) { + sslConnectorTrustedCerts(writer, smClient); + } else if (command.equals("/sslReload")) { + sslReload(writer, tlsHostName, smClient); + } else { + writer.println(smClient.getString("managerServlet.unknownCommand", command)); + } + + // Finish up the response + writer.flush(); + writer.close(); + + } + + + /** + * Process a PUT request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + + // Identify the request parameters that we need + String command = request.getPathInfo(); + if (command == null) { + command = request.getServletPath(); + } + String path = request.getParameter("path"); + ContextName cn = null; + if (path != null) { + cn = new ContextName(path, request.getParameter("version")); + } + String config = request.getParameter("config"); + String tag = request.getParameter("tag"); + boolean update = false; + if (request.getParameter("update") != null && request.getParameter("update").equals("true")) { + update = true; + } + + // Prepare our output writer to generate the response message + response.setContentType("text/plain;charset=" + Constants.CHARSET); + // Stop older versions of IE thinking they know best. We set text/plain + // in the line above for a reason. IE's behaviour is unwanted at best + // and dangerous at worst. + response.setHeader("X-Content-Type-Options", "nosniff"); + PrintWriter writer = response.getWriter(); + + // Process the requested command + if (command == null) { + writer.println(smClient.getString("managerServlet.noCommand")); + } else if (command.equals("/deploy")) { + deploy(writer, config, cn, tag, update, request, smClient); + } else { + writer.println(smClient.getString("managerServlet.unknownCommand", command)); + } + + // Finish up the response + writer.flush(); + writer.close(); + + } + + + /** + * Initialize this servlet. + */ + @Override + public void init() throws ServletException { + + // Ensure that our ContainerServlet properties have been set + if (wrapper == null || context == null) { + throw new UnavailableException(sm.getString("managerServlet.noWrapper")); + } + + // Set our properties from the initialization parameters + String value = null; + try { + value = getServletConfig().getInitParameter("debug"); + debug = Integer.parseInt(value); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + + // Acquire global JNDI resources if available + Server server = ((Engine) host.getParent()).getService().getServer(); + if (server != null) { + global = server.getGlobalNamingContext(); + } + + // Calculate the directory into which we will be deploying applications + versioned = (File) getServletContext().getAttribute(ServletContext.TEMPDIR); + + configBase = new File(context.getCatalinaBase(), "conf"); + Container container = context; + Container host = null; + Container engine = null; + while (container != null) { + if (container instanceof Host) { + host = container; + } + if (container instanceof Engine) { + engine = container; + } + container = container.getParent(); + } + if (engine != null) { + configBase = new File(configBase, engine.getName()); + } + if (host != null) { + configBase = new File(configBase, host.getName()); + } + // Note: The directory must exist for this to work. + + // Log debugging messages as necessary + if (debug >= 1) { + log("init: Associated with Deployer '" + oname + "'"); + if (global != null) { + log("init: Global resources are available"); + } + } + + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Find potential memory leaks caused by web application reload. + * + * @param statusLine Print a status line + * @param writer The output writer + * @param smClient StringManager for the client's locale + */ + protected void findleaks(boolean statusLine, PrintWriter writer, StringManager smClient) { + + if (!(host instanceof StandardHost)) { + writer.println(smClient.getString("managerServlet.findleaksFail")); + return; + } + + String[] results = ((StandardHost) host).findReloadedContextMemoryLeaks(); + + if (results.length > 0) { + if (statusLine) { + writer.println(smClient.getString("managerServlet.findleaksList")); + } + for (String result : results) { + if (result.isEmpty()) { + result = "/"; + } + writer.println(result); + } + } else if (statusLine) { + writer.println(smClient.getString("managerServlet.findleaksNone")); + } + } + + + protected void sslReload(PrintWriter writer, String tlsHostName, StringManager smClient) { + Connector connectors[] = getConnectors(); + boolean found = false; + for (Connector connector : connectors) { + if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) { + ProtocolHandler protocol = connector.getProtocolHandler(); + if (protocol instanceof AbstractHttp11Protocol) { + AbstractHttp11Protocol http11Protoocol = (AbstractHttp11Protocol) protocol; + if (tlsHostName == null || tlsHostName.length() == 0) { + found = true; + http11Protoocol.reloadSslHostConfigs(); + } else { + SSLHostConfig[] sslHostConfigs = http11Protoocol.findSslHostConfigs(); + for (SSLHostConfig sslHostConfig : sslHostConfigs) { + // tlsHostName is as provided by the user so use a case insensitive + // comparison as host names are case insensitive. + if (sslHostConfig.getHostName().equalsIgnoreCase(tlsHostName)) { + found = true; + http11Protoocol.reloadSslHostConfig(tlsHostName); + } + } + } + } + } + } + if (found) { + if (tlsHostName == null || tlsHostName.length() == 0) { + writer.println(smClient.getString("managerServlet.sslReloadAll")); + } else { + writer.println(smClient.getString("managerServlet.sslReload", tlsHostName)); + } + } else { + writer.println(smClient.getString("managerServlet.sslReloadFail")); + } + } + + + /** + * Write some VM info. + * + * @param writer The output writer + * @param smClient StringManager for the client's locale + * @param requestedLocales the client's locales + */ + protected void vmInfo(PrintWriter writer, StringManager smClient, Enumeration requestedLocales) { + writer.println(smClient.getString("managerServlet.vminfo")); + writer.print(Diagnostics.getVMInfo(requestedLocales)); + } + + /** + * Write a JVM thread dump. + * + * @param writer The output writer + * @param smClient StringManager for the client's locale + * @param requestedLocales the client's locales + */ + protected void threadDump(PrintWriter writer, StringManager smClient, Enumeration requestedLocales) { + writer.println(smClient.getString("managerServlet.threaddump")); + writer.print(Diagnostics.getThreadDump(requestedLocales)); + } + + + protected void sslConnectorCiphers(PrintWriter writer, StringManager smClient) { + writer.println(smClient.getString("managerServlet.sslConnectorCiphers")); + Map> connectorCiphers = getConnectorCiphers(smClient); + for (Map.Entry> entry : connectorCiphers.entrySet()) { + writer.println(entry.getKey()); + for (String cipher : entry.getValue()) { + writer.print(" "); + writer.println(cipher); + } + } + } + + + private void sslConnectorCerts(PrintWriter writer, StringManager smClient) { + writer.println(smClient.getString("managerServlet.sslConnectorCerts")); + Map> connectorCerts = getConnectorCerts(smClient); + for (Map.Entry> entry : connectorCerts.entrySet()) { + writer.println(entry.getKey()); + for (String cert : entry.getValue()) { + writer.println(cert); + } + } + } + + + private void sslConnectorTrustedCerts(PrintWriter writer, StringManager smClient) { + writer.println(smClient.getString("managerServlet.sslConnectorTrustedCerts")); + Map> connectorTrustedCerts = getConnectorTrustedCerts(smClient); + for (Map.Entry> entry : connectorTrustedCerts.entrySet()) { + writer.println(entry.getKey()); + for (String cert : entry.getValue()) { + writer.println(cert); + } + } + } + + + /** + * Store server configuration. + * + * @param writer Destination for any user message(s) during this operation + * @param path Optional context path to save + * @param smClient i18n support for current client's locale + */ + protected synchronized void save(PrintWriter writer, String path, StringManager smClient) { + + ObjectName storeConfigOname; + try { + // Note: Hard-coded domain used since this object is per Server/JVM + storeConfigOname = new ObjectName("Catalina:type=StoreConfig"); + } catch (MalformedObjectNameException e) { + // Should never happen. The name above is valid. + log(sm.getString("managerServlet.exception"), e); + writer.println(smClient.getString("managerServlet.exception", e.toString())); + return; + } + + if (!mBeanServer.isRegistered(storeConfigOname)) { + writer.println(smClient.getString("managerServlet.storeConfig.noMBean", storeConfigOname)); + return; + } + + if (path == null || path.length() == 0 || !path.startsWith("/")) { + try { + mBeanServer.invoke(storeConfigOname, "storeConfig", null, null); + writer.println(smClient.getString("managerServlet.saved")); + } catch (Exception e) { + log(sm.getString("managerServlet.error.storeConfig"), e); + writer.println(smClient.getString("managerServlet.exception", e.toString())); + } + } else { + String contextPath = path; + if (path.equals("/")) { + contextPath = ""; + } + Context context = (Context) host.findChild(contextPath); + if (context == null) { + writer.println(smClient.getString("managerServlet.noContext", path)); + return; + } + try { + Boolean result = (Boolean) mBeanServer.invoke(storeConfigOname, "store", new Object[] { context }, + new String[] { "org.apache.catalina.Context" }); + if (result.booleanValue()) { + writer.println(smClient.getString("managerServlet.savedContext", path)); + } else { + writer.println(smClient.getString("managerServlet.savedContextFail", path)); + } + } catch (Exception e) { + log(sm.getString("managerServlet.error.storeContextConfig", path), e); + writer.println(smClient.getString("managerServlet.exception", e.toString())); + } + } + } + + + /** + * Deploy a web application archive (included in the current request) at the specified context path. + * + * @param writer Writer to render results to + * @param config URL of the context configuration file to be installed + * @param cn Name of the application to be installed + * @param tag Tag to be associated with the webapp + * @param update Flag that indicates that any existing app should be replaced + * @param request Servlet request we are processing + * @param smClient i18n messages using the locale of the client + */ + protected void deploy(PrintWriter writer, String config, ContextName cn, String tag, boolean update, + HttpServletRequest request, StringManager smClient) { + + if (config != null && config.length() == 0) { + config = null; + } + + if (debug >= 1) { + if (config == null) { + log("deploy: Deploying web application '" + cn + "'"); + } else { + log("deploy: Deploying web application '" + cn + "' " + "with context configuration at '" + config + + "'"); + } + } + + // Validate the requested context path + if (!validateContextName(cn, writer, smClient)) { + return; + } + String name = cn.getName(); + String baseName = cn.getBaseName(); + String displayPath = cn.getDisplayName(); + + // If app exists deployment can only proceed if update is true + // Note existing WAR will be deleted and then replaced + Context context = (Context) host.findChild(name); + if (context != null && !update) { + writer.println(smClient.getString("managerServlet.alreadyContext", displayPath)); + return; + } + + if (config != null && config.startsWith("file:")) { + config = config.substring("file:".length()); + } + + File deployedWar = new File(host.getAppBaseFile(), baseName + ".war"); + + // Determine full path for uploaded WAR + File uploadedWar; + if (tag == null) { + if (update) { + // Append ".tmp" to the file name so it won't get deployed if auto + // deployment is enabled. It also means the old war won't get + // deleted if the upload fails + uploadedWar = new File(deployedWar.getAbsolutePath() + ".tmp"); + if (uploadedWar.exists() && !uploadedWar.delete()) { + writer.println(smClient.getString("managerServlet.deleteFail", uploadedWar)); + } + } else { + uploadedWar = deployedWar; + } + } else { + File uploadPath = new File(versioned, tag); + if (!uploadPath.mkdirs() && !uploadPath.isDirectory()) { + writer.println(smClient.getString("managerServlet.mkdirFail", uploadPath)); + return; + } + uploadedWar = new File(uploadPath, baseName + ".war"); + } + if (debug >= 2) { + log("Uploading WAR file to " + uploadedWar); + } + + try { + if (tryAddServiced(name)) { + try { + if (config != null) { + if (!configBase.mkdirs() && !configBase.isDirectory()) { + writer.println(smClient.getString("managerServlet.mkdirFail", configBase)); + return; + } + if (ExpandWar.copy(new File(config), new File(configBase, baseName + ".xml")) == false) { + throw new Exception(sm.getString("managerServlet.copyError", config)); + } + } + // Upload WAR + uploadWar(writer, request, uploadedWar, smClient); + if (update && tag == null) { + if (deployedWar.exists() && !deployedWar.delete()) { + writer.println(smClient.getString("managerServlet.deleteFail", deployedWar)); + return; + } + // Rename uploaded WAR file + if (!uploadedWar.renameTo(deployedWar)) { + writer.println(smClient.getString("managerServlet.renameFail", uploadedWar, deployedWar)); + return; + } + } + if (tag != null) { + // Copy WAR to the host's appBase + ExpandWar.copy(uploadedWar, deployedWar); + } + } finally { + removeServiced(name); + } + // Perform new deployment + check(name); + } else { + writer.println(smClient.getString("managerServlet.inService", displayPath)); + } + } catch (Exception e) { + log(sm.getString("managerServlet.error.deploy", displayPath), e); + writer.println(smClient.getString("managerServlet.exception", e.toString())); + return; + } + + writeDeployResult(writer, smClient, name, displayPath); + } + + + /** + * Install an application for the specified path from the specified web application archive. + * + * @param writer Writer to render results to + * @param tag Revision tag to deploy from + * @param cn Name of the application to be installed + * @param smClient i18n messages using the locale of the client + */ + protected void deploy(PrintWriter writer, ContextName cn, String tag, StringManager smClient) { + + // NOTE: It is assumed that update is always true in this method. + + // Validate the requested context path + if (!validateContextName(cn, writer, smClient)) { + return; + } + + String baseName = cn.getBaseName(); + String name = cn.getName(); + String displayPath = cn.getDisplayName(); + + // Find the local WAR file + File localWar = new File(new File(versioned, tag), baseName + ".war"); + + File deployedWar = new File(host.getAppBaseFile(), baseName + ".war"); + + // Copy WAR to appBase + try { + if (tryAddServiced(name)) { + try { + if (!deployedWar.delete()) { + writer.println(smClient.getString("managerServlet.deleteFail", deployedWar)); + return; + } + ExpandWar.copy(localWar, deployedWar); + } finally { + removeServiced(name); + } + // Perform new deployment + check(name); + } else { + writer.println(smClient.getString("managerServlet.inService", displayPath)); + } + } catch (Exception e) { + log(sm.getString("managerServlet.error.deploy", displayPath), e); + writer.println(smClient.getString("managerServlet.exception", e.toString())); + return; + } + + writeDeployResult(writer, smClient, name, displayPath); + } + + + /** + * Install an application for the specified path from the specified web application archive. + * + * @param writer Writer to render results to + * @param config URL of the context configuration file to be installed + * @param cn Name of the application to be installed + * @param war URL of the web application archive to be installed + * @param update true to override any existing webapp on the path + * @param smClient i18n messages using the locale of the client + */ + protected void deploy(PrintWriter writer, String config, ContextName cn, String war, boolean update, + StringManager smClient) { + + if (config != null && config.length() == 0) { + config = null; + } + if (war != null && war.length() == 0) { + war = null; + } + + if (debug >= 1) { + if (config != null) { + if (war != null) { + log("install: Installing context configuration at '" + config + "' from '" + war + "'"); + } else { + log("install: Installing context configuration at '" + config + "'"); + } + } else { + if (cn != null) { + log("install: Installing web application '" + cn + "' from '" + war + "'"); + } else { + log("install: Installing web application from '" + war + "'"); + } + } + } + + if (!validateContextName(cn, writer, smClient)) { + return; + } + @SuppressWarnings("null") // checked in call above + String name = cn.getName(); + String baseName = cn.getBaseName(); + String displayPath = cn.getDisplayName(); + + // If app exists deployment can only proceed if update is true + // Note existing files will be deleted and then replaced + Context context = (Context) host.findChild(name); + if (context != null && !update) { + writer.println(smClient.getString("managerServlet.alreadyContext", displayPath)); + return; + } + + if (config != null && config.startsWith("file:")) { + config = config.substring("file:".length()); + } + if (war != null && war.startsWith("file:")) { + war = war.substring("file:".length()); + } + + try { + if (tryAddServiced(name)) { + try { + if (config != null) { + if (!configBase.mkdirs() && !configBase.isDirectory()) { + writer.println(smClient.getString("managerServlet.mkdirFail", configBase)); + return; + } + File localConfigFile = new File(configBase, baseName + ".xml"); + File configFile = new File(config); + // Skip delete and copy if source == destination + if (!configFile.getCanonicalPath().equals(localConfigFile.getCanonicalPath())) { + if (localConfigFile.isFile() && !localConfigFile.delete()) { + writer.println(smClient.getString("managerServlet.deleteFail", localConfigFile)); + return; + } + ExpandWar.copy(configFile, localConfigFile); + } + } + if (war != null) { + File localWarFile; + if (war.endsWith(".war")) { + localWarFile = new File(host.getAppBaseFile(), baseName + ".war"); + } else { + localWarFile = new File(host.getAppBaseFile(), baseName); + } + File warFile = new File(war); + if (!warFile.isAbsolute()) { + warFile = new File(host.getAppBaseFile(), war); + } + // Skip delete and copy if source == destination + if (!warFile.getCanonicalPath().equals(localWarFile.getCanonicalPath())) { + if (localWarFile.exists() && !ExpandWar.delete(localWarFile)) { + writer.println(smClient.getString("managerServlet.deleteFail", localWarFile)); + return; + } + ExpandWar.copy(warFile, localWarFile); + } + } + } finally { + removeServiced(name); + } + // Perform new deployment + check(name); + } else { + writer.println(smClient.getString("managerServlet.inService", displayPath)); + } + writeDeployResult(writer, smClient, name, displayPath); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log(sm.getString("managerServlet.error.deploy", displayPath), t); + writer.println(smClient.getString("managerServlet.exception", t.toString())); + } + + } + + + private void writeDeployResult(PrintWriter writer, StringManager smClient, String name, String displayPath) { + Context deployed = (Context) host.findChild(name); + if (deployed != null && deployed.getConfigured() && deployed.getState().isAvailable()) { + writer.println(smClient.getString("managerServlet.deployed", displayPath)); + } else if (deployed != null && !deployed.getState().isAvailable()) { + writer.println(smClient.getString("managerServlet.deployedButNotStarted", displayPath)); + } else { + // Something failed + writer.println(smClient.getString("managerServlet.deployFailed", displayPath)); + } + } + + + /** + * Render a list of the currently active Contexts in our virtual host. + * + * @param writer Writer to render to + * @param smClient i18n support for current client's locale + */ + protected void list(PrintWriter writer, StringManager smClient) { + + if (debug >= 1) { + log("list: Listing contexts for virtual host '" + host.getName() + "'"); + } + + writer.println(smClient.getString("managerServlet.listed", host.getName())); + Container[] contexts = host.findChildren(); + for (Container container : contexts) { + Context context = (Context) container; + if (context != null) { + String displayPath = context.getPath(); + if (displayPath.equals("")) { + displayPath = "/"; + } + List parts = null; + if (context.getState().isAvailable()) { + parts = Arrays.asList(displayPath, "running", "" + context.getManager().findSessions().length, + context.getDocBase()); + } else { + parts = Arrays.asList(displayPath, "stopped", "0", context.getDocBase()); + } + writer.println(StringUtils.join(parts, ':')); + } + } + } + + + /** + * Reload the web application at the specified context path. + * + * @param writer Writer to render to + * @param cn Name of the application to be restarted + * @param smClient i18n support for current client's locale + */ + protected void reload(PrintWriter writer, ContextName cn, StringManager smClient) { + + if (debug >= 1) { + log("restart: Reloading web application '" + cn + "'"); + } + + if (!validateContextName(cn, writer, smClient)) { + return; + } + + try { + Context context = (Context) host.findChild(cn.getName()); + if (context == null) { + writer.println( + smClient.getString("managerServlet.noContext", Escape.htmlElementContent(cn.getDisplayName()))); + return; + } + // It isn't possible for the manager to reload itself + if (context.getName().equals(this.context.getName())) { + writer.println(smClient.getString("managerServlet.noSelf")); + return; + } + context.reload(); + writer.println(smClient.getString("managerServlet.reloaded", cn.getDisplayName())); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log(sm.getString("managerServlet.error.reload", cn.getDisplayName()), t); + writer.println(smClient.getString("managerServlet.exception", t.toString())); + } + + } + + + /** + * Render a list of available global JNDI resources. + * + * @param writer Writer to render to + * @param type Fully qualified class name of the resource type of interest, or null to list + * resources of all types + * @param smClient i18n support for current client's locale + */ + protected void resources(PrintWriter writer, String type, StringManager smClient) { + + if (debug >= 1) { + if (type != null) { + log("resources: Listing resources of type " + type); + } else { + log("resources: Listing resources of all types"); + } + } + + // Is the global JNDI resources context available? + if (global == null) { + writer.println(smClient.getString("managerServlet.noGlobal")); + return; + } + + // Enumerate the global JNDI resources of the requested type + if (type != null) { + writer.println(smClient.getString("managerServlet.resourcesType", type)); + } else { + writer.println(smClient.getString("managerServlet.resourcesAll")); + } + + printResources(writer, "", global, type, smClient); + + } + + + /** + * List the resources of the given context. + * + * @param writer Writer to render to + * @param prefix Path for recursion + * @param namingContext The naming context for lookups + * @param type Fully qualified class name of the resource type of interest, or null to list + * resources of all types + * @param smClient i18n support for current client's locale + */ + protected void printResources(PrintWriter writer, String prefix, javax.naming.Context namingContext, String type, + StringManager smClient) { + try { + NamingEnumeration items = namingContext.listBindings(""); + while (items.hasMore()) { + Binding item = items.next(); + Object obj = item.getObject(); + if (obj instanceof javax.naming.Context) { + printResources(writer, prefix + item.getName() + "/", (javax.naming.Context) obj, type, smClient); + } else { + if (type != null && (obj == null || !IntrospectionUtils.isInstance(obj.getClass(), type))) { + continue; + } + writer.print(prefix + item.getName()); + writer.print(':'); + writer.print(item.getClassName()); + // Do we want a description if available? + writer.println(); + } + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log(sm.getString("managerServlet.error.resources", type), t); + writer.println(smClient.getString("managerServlet.exception", t.toString())); + } + } + + + /** + * Writes System OS and JVM properties. + * + * @param writer Writer to render to + * @param smClient i18n support for current client's locale + */ + protected void serverinfo(PrintWriter writer, StringManager smClient) { + if (debug >= 1) { + log("serverinfo"); + } + try { + writer.println(smClient.getString("managerServlet.serverInfo", ServerInfo.getServerInfo(), + System.getProperty("os.name"), System.getProperty("os.version"), System.getProperty("os.arch"), + System.getProperty("java.runtime.version"), System.getProperty("java.vm.vendor"))); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log(sm.getString("managerServlet.error.serverInfo"), t); + writer.println(smClient.getString("managerServlet.exception", t.toString())); + } + } + + /** + * Session information for the web application at the specified context path. Displays a profile of session + * thisAccessedTime listing number of sessions for each 10 minute interval up to 10 hours. + * + * @param writer Writer to render to + * @param cn Name of the application to list session information for + * @param idle Expire all sessions with idle time > idle for this context + * @param smClient i18n support for current client's locale + */ + protected void sessions(PrintWriter writer, ContextName cn, int idle, StringManager smClient) { + + if (debug >= 1) { + log("sessions: Session information for web application '" + cn + "'"); + if (idle >= 0) { + log("sessions: Session expiration for " + idle + " minutes '" + cn + "'"); + } + } + + if (!validateContextName(cn, writer, smClient)) { + return; + } + + String displayPath = cn.getDisplayName(); + + try { + Context context = (Context) host.findChild(cn.getName()); + if (context == null) { + writer.println(smClient.getString("managerServlet.noContext", Escape.htmlElementContent(displayPath))); + return; + } + Manager manager = context.getManager(); + if (manager == null) { + writer.println(smClient.getString("managerServlet.noManager", Escape.htmlElementContent(displayPath))); + return; + } + int maxCount = 60; + int histoInterval = 1; + int maxInactiveInterval = context.getSessionTimeout(); + if (maxInactiveInterval > 0) { + histoInterval = maxInactiveInterval / maxCount; + if (histoInterval * maxCount < maxInactiveInterval) { + histoInterval++; + } + if (0 == histoInterval) { + histoInterval = 1; + } + maxCount = maxInactiveInterval / histoInterval; + if (histoInterval * maxCount < maxInactiveInterval) { + maxCount++; + } + } + + writer.println(smClient.getString("managerServlet.sessions", displayPath)); + writer.println(smClient.getString("managerServlet.sessiondefaultmax", "" + maxInactiveInterval)); + Session[] sessions = manager.findSessions(); + int[] timeout = new int[maxCount + 1]; + int notimeout = 0; + int expired = 0; + for (Session session : sessions) { + int time = (int) (session.getIdleTimeInternal() / 1000L); + if (idle >= 0 && time >= idle * 60) { + session.expire(); + expired++; + } + time = time / 60 / histoInterval; + if (time < 0) { + notimeout++; + } else if (time >= maxCount) { + timeout[maxCount]++; + } else { + timeout[time]++; + } + } + if (timeout[0] > 0) { + writer.println( + smClient.getString("managerServlet.sessiontimeout", "<" + histoInterval, "" + timeout[0])); + } + for (int i = 1; i < maxCount; i++) { + if (timeout[i] > 0) { + writer.println(smClient.getString("managerServlet.sessiontimeout", + "" + i * histoInterval + " - <" + (i + 1) * histoInterval, "" + timeout[i])); + } + } + if (timeout[maxCount] > 0) { + writer.println(smClient.getString("managerServlet.sessiontimeout", ">=" + maxCount * histoInterval, + "" + timeout[maxCount])); + } + if (notimeout > 0) { + writer.println(smClient.getString("managerServlet.sessiontimeout.unlimited", "" + notimeout)); + } + if (idle >= 0) { + writer.println(smClient.getString("managerServlet.sessiontimeout.expired", ">" + idle, "" + expired)); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log(sm.getString("managerServlet.error.sessions", displayPath), t); + writer.println(smClient.getString("managerServlet.exception", t.toString())); + } + + } + + + /** + * Extract the expiration request parameter + * + * @param writer Writer to render to + * @param cn Name of the application to list session information for + * @param req The Servlet request + * @param smClient i18n support for current client's locale + */ + protected void expireSessions(PrintWriter writer, ContextName cn, HttpServletRequest req, StringManager smClient) { + int idle = -1; + String idleParam = req.getParameter("idle"); + if (idleParam != null) { + try { + idle = Integer.parseInt(idleParam); + } catch (NumberFormatException e) { + log(sm.getString("managerServlet.error.idleParam", idleParam)); + } + } + sessions(writer, cn, idle, smClient); + } + + /** + * Start the web application at the specified context path. + * + * @param writer Writer to render to + * @param cn Name of the application to be started + * @param smClient i18n support for current client's locale + */ + protected void start(PrintWriter writer, ContextName cn, StringManager smClient) { + + if (debug >= 1) { + log("start: Starting web application '" + cn + "'"); + } + + if (!validateContextName(cn, writer, smClient)) { + return; + } + + String displayPath = cn.getDisplayName(); + + try { + Context context = (Context) host.findChild(cn.getName()); + if (context == null) { + writer.println(smClient.getString("managerServlet.noContext", Escape.htmlElementContent(displayPath))); + return; + } + context.start(); + if (context.getState().isAvailable()) { + writer.println(smClient.getString("managerServlet.started", displayPath)); + } else { + writer.println(smClient.getString("managerServlet.startFailed", displayPath)); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log(sm.getString("managerServlet.error.start", displayPath), t); + writer.println(smClient.getString("managerServlet.startFailed", displayPath)); + writer.println(smClient.getString("managerServlet.exception", t.toString())); + } + + } + + + /** + * Stop the web application at the specified context path. + * + * @param writer Writer to render to + * @param cn Name of the application to be stopped + * @param smClient i18n support for current client's locale + */ + protected void stop(PrintWriter writer, ContextName cn, StringManager smClient) { + + if (debug >= 1) { + log("stop: Stopping web application '" + cn + "'"); + } + + if (!validateContextName(cn, writer, smClient)) { + return; + } + + String displayPath = cn.getDisplayName(); + + try { + Context context = (Context) host.findChild(cn.getName()); + if (context == null) { + writer.println(smClient.getString("managerServlet.noContext", Escape.htmlElementContent(displayPath))); + return; + } + // It isn't possible for the manager to stop itself + if (context.getName().equals(this.context.getName())) { + writer.println(smClient.getString("managerServlet.noSelf")); + return; + } + context.stop(); + writer.println(smClient.getString("managerServlet.stopped", displayPath)); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log(sm.getString("managerServlet.error.stop", displayPath), t); + writer.println(smClient.getString("managerServlet.exception", t.toString())); + } + + } + + + /** + * Undeploy the web application at the specified context path. + * + * @param writer Writer to render to + * @param cn Name of the application to be removed + * @param smClient i18n support for current client's locale + */ + protected void undeploy(PrintWriter writer, ContextName cn, StringManager smClient) { + + if (debug >= 1) { + log("undeploy: Undeploying web application at '" + cn + "'"); + } + + if (!validateContextName(cn, writer, smClient)) { + return; + } + + String name = cn.getName(); + String baseName = cn.getBaseName(); + String displayPath = cn.getDisplayName(); + + try { + + // Validate the Context of the specified application + Context context = (Context) host.findChild(name); + if (context == null) { + writer.println(smClient.getString("managerServlet.noContext", Escape.htmlElementContent(displayPath))); + return; + } + + if (!isDeployed(name)) { + writer.println( + smClient.getString("managerServlet.notDeployed", Escape.htmlElementContent(displayPath))); + return; + } + + if (tryAddServiced(name)) { + try { + // Try to stop the context first to be nicer + context.stop(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + try { + File war = new File(host.getAppBaseFile(), baseName + ".war"); + File dir = new File(host.getAppBaseFile(), baseName); + File xml = new File(configBase, baseName + ".xml"); + if (war.exists() && !war.delete()) { + writer.println(smClient.getString("managerServlet.deleteFail", war)); + return; + } else if (dir.exists() && !ExpandWar.delete(dir, false)) { + writer.println(smClient.getString("managerServlet.deleteFail", dir)); + return; + } else if (xml.exists() && !xml.delete()) { + writer.println(smClient.getString("managerServlet.deleteFail", xml)); + return; + } + } finally { + removeServiced(name); + } + // Perform new deployment + check(name); + } else { + writer.println(smClient.getString("managerServlet.inService", displayPath)); + } + writer.println(smClient.getString("managerServlet.undeployed", displayPath)); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log(sm.getString("managerServlet.error.undeploy", displayPath), t); + writer.println(smClient.getString("managerServlet.exception", t.toString())); + } + + } + + + // -------------------------------------------------------- Support Methods + + + /** + * Invoke the isDeployed method on the deployer. + * + * @param name The webapp name + * + * @return true if a webapp with that name is deployed + * + * @throws Exception Propagate JMX invocation error + */ + protected boolean isDeployed(String name) throws Exception { + String[] params = { name }; + String[] signature = { "java.lang.String" }; + Boolean result = (Boolean) mBeanServer.invoke(oname, "isDeployed", params, signature); + return result.booleanValue(); + } + + + /** + * Invoke the check method on the deployer. + * + * @param name The webapp name + * + * @throws Exception Propagate JMX invocation error + */ + protected void check(String name) throws Exception { + String[] params = { name }; + String[] signature = { "java.lang.String" }; + mBeanServer.invoke(oname, "check", params, signature); + } + + + /** + * Attempt to mark a context as being serviced + * + * @param name The context name + * + * @return {@code true} if the application was marked as being serviced and {@code false} if the application was + * already marked as being serviced + * + * @throws Exception Error invoking the deployer + */ + protected boolean tryAddServiced(String name) throws Exception { + String[] params = { name }; + String[] signature = { "java.lang.String" }; + Boolean result = (Boolean) mBeanServer.invoke(oname, "tryAddServiced", params, signature); + return result.booleanValue(); + } + + + /** + * Invoke the removeServiced method on the deployer. + * + * @param name The webapp name + * + * @throws Exception Propagate JMX invocation error + */ + protected void removeServiced(String name) throws Exception { + String[] params = { name }; + String[] signature = { "java.lang.String" }; + mBeanServer.invoke(oname, "removeServiced", params, signature); + } + + + /** + * Upload the WAR file included in this request, and store it at the specified file location. + * + * @param writer Writer to render to + * @param request The servlet request we are processing + * @param war The file into which we should store the uploaded WAR + * @param smClient The StringManager used to construct i18n messages based on the Locale of the client + * + * @exception IOException if an I/O error occurs during processing + */ + protected void uploadWar(PrintWriter writer, HttpServletRequest request, File war, StringManager smClient) + throws IOException { + + if (war.exists() && !war.delete()) { + String msg = smClient.getString("managerServlet.deleteFail", war); + throw new IOException(msg); + } + + try (ServletInputStream istream = request.getInputStream(); OutputStream ostream = new FileOutputStream(war)) { + IOTools.flow(istream, ostream); + } catch (IOException e) { + if (war.exists() && !war.delete()) { + writer.println(smClient.getString("managerServlet.deleteFail", war)); + } + throw e; + } + } + + + protected static boolean validateContextName(ContextName cn, PrintWriter writer, StringManager smClient) { + + // ContextName should be non-null with a path that is empty or starts + // with / + if (cn != null && (cn.getPath().startsWith("/") || cn.getPath().equals(""))) { + return true; + } + + String path = null; + if (cn != null) { + path = Escape.htmlElementContent(cn.getPath()); + } + writer.println(smClient.getString("managerServlet.invalidPath", path)); + return false; + } + + protected Map> getConnectorCiphers(StringManager smClient) { + Map> result = new HashMap<>(); + + Connector connectors[] = getConnectors(); + for (Connector connector : connectors) { + if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) { + SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + for (SSLHostConfig sslHostConfig : sslHostConfigs) { + String name = connector.toString() + "-" + sslHostConfig.getHostName(); + /* Add cipher list, keep order but remove duplicates */ + result.put(name, + new ArrayList<>(new LinkedHashSet<>(Arrays.asList(sslHostConfig.getEnabledCiphers())))); + } + } else { + ArrayList cipherList = new ArrayList<>(1); + cipherList.add(smClient.getString("managerServlet.notSslConnector")); + result.put(connector.toString(), cipherList); + } + } + return result; + } + + + protected Map> getConnectorCerts(StringManager smClient) { + Map> result = new HashMap<>(); + + Connector connectors[] = getConnectors(); + for (Connector connector : connectors) { + if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) { + SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + for (SSLHostConfig sslHostConfig : sslHostConfigs) { + if (sslHostConfig.getOpenSslContext().longValue() == 0) { + // Not set. Must be JSSE based. + Set sslHostConfigCerts = sslHostConfig.getCertificates(); + for (SSLHostConfigCertificate sslHostConfigCert : sslHostConfigCerts) { + String name = connector.toString() + "-" + sslHostConfig.getHostName() + "-" + + sslHostConfigCert.getType(); + List certList = new ArrayList<>(); + SSLContext sslContext = sslHostConfigCert.getSslContext(); + String alias = sslHostConfigCert.getCertificateKeyAlias(); + if (alias == null) { + alias = SSLUtilBase.DEFAULT_KEY_ALIAS; + } + X509Certificate[] certs = sslContext.getCertificateChain(alias); + if (certs == null) { + certList.add(smClient.getString("managerServlet.certsNotAvailable")); + } else { + for (Certificate cert : certs) { + certList.add(cert.toString()); + } + } + result.put(name, certList); + } + } else { + List certList = new ArrayList<>(); + certList.add(smClient.getString("managerServlet.certsNotAvailable")); + String name = connector.toString() + "-" + sslHostConfig.getHostName(); + result.put(name, certList); + } + } + } else { + List certList = new ArrayList<>(1); + certList.add(smClient.getString("managerServlet.notSslConnector")); + result.put(connector.toString(), certList); + } + } + + return result; + } + + + protected Map> getConnectorTrustedCerts(StringManager smClient) { + Map> result = new HashMap<>(); + + Connector connectors[] = getConnectors(); + for (Connector connector : connectors) { + if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) { + SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + for (SSLHostConfig sslHostConfig : sslHostConfigs) { + String name = connector.toString() + "-" + sslHostConfig.getHostName(); + List certList = new ArrayList<>(); + if (sslHostConfig.getOpenSslContext().longValue() == 0) { + // Not set. Must be JSSE based. + SSLContext sslContext = sslHostConfig.getCertificates().iterator().next().getSslContext(); + X509Certificate[] certs = sslContext.getAcceptedIssuers(); + if (certs == null) { + certList.add(smClient.getString("managerServlet.certsNotAvailable")); + } else if (certs.length == 0) { + certList.add(smClient.getString("managerServlet.trustedCertsNotConfigured")); + } else { + for (Certificate cert : certs) { + certList.add(cert.toString()); + } + } + } else { + certList.add(smClient.getString("managerServlet.certsNotAvailable")); + } + result.put(name, certList); + } + } else { + List certList = new ArrayList<>(1); + certList.add(smClient.getString("managerServlet.notSslConnector")); + result.put(connector.toString(), certList); + } + } + + return result; + } + + + private Connector[] getConnectors() { + Engine e = (Engine) host.getParent(); + Service s = e.getService(); + return s.findConnectors(); + } +} diff --git a/java/org/apache/catalina/manager/StatusManagerServlet.java b/java/org/apache/catalina/manager/StatusManagerServlet.java new file mode 100644 index 0000000..9129731 --- /dev/null +++ b/java/org/apache/catalina/manager/StatusManagerServlet.java @@ -0,0 +1,348 @@ +/* + * 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.catalina.manager; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.management.MBeanServer; +import javax.management.MBeanServerNotification; +import javax.management.Notification; +import javax.management.NotificationListener; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.util.ServerInfo; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + * This servlet will display a complete status of the HTTP/1.1 connector. + * + * @author Remy Maucherat + */ +public class StatusManagerServlet extends HttpServlet implements NotificationListener { + + private static final long serialVersionUID = 1L; + + // ----------------------------------------------------- Instance Variables + /** + * MBean server. + */ + protected MBeanServer mBeanServer = null; + + + /** + * Vector of thread pools object names. + */ + protected final List threadPools = Collections.synchronizedList(new ArrayList<>()); + + + /** + * Vector of request processors object names. + */ + protected final List requestProcessors = Collections.synchronizedList(new ArrayList<>()); + + + /** + * Vector of global request processors object names. + */ + protected final List globalRequestProcessors = Collections.synchronizedList(new ArrayList<>()); + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + + // --------------------------------------------------------- Public Methods + + /** + * Initialize this servlet. + */ + @Override + public void init() throws ServletException { + + // Retrieve the MBean server + mBeanServer = Registry.getRegistry(null, null).getMBeanServer(); + + try { + + // Query Thread Pools + String onStr = "*:type=ThreadPool,*"; + ObjectName objectName = new ObjectName(onStr); + Set set = mBeanServer.queryMBeans(objectName, null); + Iterator iterator = set.iterator(); + while (iterator.hasNext()) { + ObjectInstance oi = iterator.next(); + threadPools.add(oi.getObjectName()); + } + + // Query Global Request Processors + onStr = "*:type=GlobalRequestProcessor,*"; + objectName = new ObjectName(onStr); + set = mBeanServer.queryMBeans(objectName, null); + iterator = set.iterator(); + while (iterator.hasNext()) { + ObjectInstance oi = iterator.next(); + globalRequestProcessors.add(oi.getObjectName()); + } + + // Query Request Processors + onStr = "*:type=RequestProcessor,*"; + objectName = new ObjectName(onStr); + set = mBeanServer.queryMBeans(objectName, null); + iterator = set.iterator(); + while (iterator.hasNext()) { + ObjectInstance oi = iterator.next(); + requestProcessors.add(oi.getObjectName()); + } + + // Register with MBean server + onStr = "JMImplementation:type=MBeanServerDelegate"; + objectName = new ObjectName(onStr); + mBeanServer.addNotificationListener(objectName, this, null, null); + + } catch (Exception e) { + log(sm.getString("managerServlet.error.jmx"), e); + } + + } + + + /** + * Finalize this servlet. + */ + @Override + public void destroy() { + + // Unregister with MBean server + String onStr = "JMImplementation:type=MBeanServerDelegate"; + ObjectName objectName; + try { + objectName = new ObjectName(onStr); + mBeanServer.removeNotificationListener(objectName, this, null, null); + } catch (Exception e) { + log(sm.getString("managerServlet.error.jmx"), e); + } + + } + + + /** + * Process a GET request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + + // mode is flag for HTML, JSON or XML output + int mode = 0; + // if ?XML=true, set the mode to XML + if (request.getParameter("XML") != null && request.getParameter("XML").equals("true")) { + mode = 1; + } + // if ?JSON=true, set the mode to JSON + if (request.getParameter("JSON") != null && request.getParameter("JSON").equals("true")) { + mode = 2; + } + StatusTransformer.setContentType(response, mode); + + PrintWriter writer = response.getWriter(); + + boolean completeStatus = false; + if (request.getPathInfo() != null && request.getPathInfo().equals("/all")) { + completeStatus = true; + } + // use StatusTransformer to output status + Object[] args = new Object[1]; + args[0] = getServletContext().getContextPath(); + StatusTransformer.writeHeader(writer, args, mode); + + // Body Header Section + args = new Object[2]; + args[0] = getServletContext().getContextPath(); + if (completeStatus) { + args[1] = smClient.getString("statusServlet.complete"); + } else { + args[1] = smClient.getString("statusServlet.title"); + } + // use StatusTransformer to output status + StatusTransformer.writeBody(writer, args, mode); + + // Manager Section + args = new Object[9]; + args[0] = smClient.getString("htmlManagerServlet.manager"); + args[1] = response.encodeURL(getServletContext().getContextPath() + "/html/list"); + args[2] = smClient.getString("htmlManagerServlet.list"); + args[3] = // External link + getServletContext().getContextPath() + "/" + + smClient.getString("htmlManagerServlet.helpHtmlManagerFile"); + args[4] = smClient.getString("htmlManagerServlet.helpHtmlManager"); + args[5] = // External link + getServletContext().getContextPath() + "/" + smClient.getString("htmlManagerServlet.helpManagerFile"); + args[6] = smClient.getString("htmlManagerServlet.helpManager"); + if (completeStatus) { + args[7] = response.encodeURL(getServletContext().getContextPath() + "/status"); + args[8] = smClient.getString("statusServlet.title"); + } else { + args[7] = response.encodeURL(getServletContext().getContextPath() + "/status/all"); + args[8] = smClient.getString("statusServlet.complete"); + } + // use StatusTransformer to output status + StatusTransformer.writeManager(writer, args, mode); + + // Server Header Section + args = new Object[9]; + args[0] = smClient.getString("htmlManagerServlet.serverTitle"); + args[1] = smClient.getString("htmlManagerServlet.serverVersion"); + args[2] = smClient.getString("htmlManagerServlet.serverJVMVersion"); + args[3] = smClient.getString("htmlManagerServlet.serverJVMVendor"); + args[4] = smClient.getString("htmlManagerServlet.serverOSName"); + args[5] = smClient.getString("htmlManagerServlet.serverOSVersion"); + args[6] = smClient.getString("htmlManagerServlet.serverOSArch"); + args[7] = smClient.getString("htmlManagerServlet.serverHostname"); + args[8] = smClient.getString("htmlManagerServlet.serverIPAddress"); + // use StatusTransformer to output status + StatusTransformer.writePageHeading(writer, args, mode); + + // Server Row Section + args = new Object[8]; + args[0] = ServerInfo.getServerInfo(); + args[1] = System.getProperty("java.runtime.version"); + args[2] = System.getProperty("java.vm.vendor"); + args[3] = System.getProperty("os.name"); + args[4] = System.getProperty("os.version"); + args[5] = System.getProperty("os.arch"); + try { + InetAddress address = InetAddress.getLocalHost(); + args[6] = address.getHostName(); + args[7] = address.getHostAddress(); + } catch (UnknownHostException e) { + args[6] = "-"; + args[7] = "-"; + } + // use StatusTransformer to output status + StatusTransformer.writeServerInfo(writer, args, mode); + + try { + + // Display virtual machine statistics + args = new Object[9]; + args[0] = smClient.getString("htmlManagerServlet.jvmFreeMemory"); + args[1] = smClient.getString("htmlManagerServlet.jvmTotalMemory"); + args[2] = smClient.getString("htmlManagerServlet.jvmMaxMemory"); + args[3] = smClient.getString("htmlManagerServlet.jvmTableTitleMemoryPool"); + args[4] = smClient.getString("htmlManagerServlet.jvmTableTitleType"); + args[5] = smClient.getString("htmlManagerServlet.jvmTableTitleInitial"); + args[6] = smClient.getString("htmlManagerServlet.jvmTableTitleTotal"); + args[7] = smClient.getString("htmlManagerServlet.jvmTableTitleMaximum"); + args[8] = smClient.getString("htmlManagerServlet.jvmTableTitleUsed"); + // use StatusTransformer to output status + StatusTransformer.writeVMState(writer, mode, args); + + args = new Object[19]; + args[0] = smClient.getString("htmlManagerServlet.connectorStateMaxThreads"); + args[1] = smClient.getString("htmlManagerServlet.connectorStateThreadCount"); + args[2] = smClient.getString("htmlManagerServlet.connectorStateThreadBusy"); + args[3] = smClient.getString("htmlManagerServlet.connectorStateAliveSocketCount"); + args[4] = smClient.getString("htmlManagerServlet.connectorStateMaxProcessingTime"); + args[5] = smClient.getString("htmlManagerServlet.connectorStateProcessingTime"); + args[6] = smClient.getString("htmlManagerServlet.connectorStateRequestCount"); + args[7] = smClient.getString("htmlManagerServlet.connectorStateErrorCount"); + args[8] = smClient.getString("htmlManagerServlet.connectorStateBytesReceived"); + args[9] = smClient.getString("htmlManagerServlet.connectorStateBytesSent"); + args[10] = smClient.getString("htmlManagerServlet.connectorStateTableTitleStage"); + args[11] = smClient.getString("htmlManagerServlet.connectorStateTableTitleTime"); + args[12] = smClient.getString("htmlManagerServlet.connectorStateTableTitleBSent"); + args[13] = smClient.getString("htmlManagerServlet.connectorStateTableTitleBRecv"); + args[14] = smClient.getString("htmlManagerServlet.connectorStateTableTitleClientForw"); + args[15] = smClient.getString("htmlManagerServlet.connectorStateTableTitleClientAct"); + args[16] = smClient.getString("htmlManagerServlet.connectorStateTableTitleVHost"); + args[17] = smClient.getString("htmlManagerServlet.connectorStateTableTitleRequest"); + args[18] = smClient.getString("htmlManagerServlet.connectorStateHint"); + StatusTransformer.writeConnectorsState(writer, mBeanServer, threadPools, globalRequestProcessors, + requestProcessors, mode, args); + + if (request.getPathInfo() != null && request.getPathInfo().equals("/all")) { + // Note: Retrieving the full status is much slower + // use StatusTransformer to output status + StatusTransformer.writeDetailedState(writer, mBeanServer, mode); + } + + } catch (Exception e) { + throw new ServletException(e); + } + + // use StatusTransformer to output status + StatusTransformer.writeFooter(writer, mode); + } + + + // ------------------------------------------- NotificationListener Methods + + @Override + public void handleNotification(Notification notification, Object handback) { + + if (notification instanceof MBeanServerNotification) { + ObjectName objectName = ((MBeanServerNotification) notification).getMBeanName(); + if (notification.getType().equals(MBeanServerNotification.REGISTRATION_NOTIFICATION)) { + String type = objectName.getKeyProperty("type"); + if (type != null) { + if (type.equals("ThreadPool")) { + threadPools.add(objectName); + } else if (type.equals("GlobalRequestProcessor")) { + globalRequestProcessors.add(objectName); + } else if (type.equals("RequestProcessor")) { + requestProcessors.add(objectName); + } + } + } else if (notification.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) { + String type = objectName.getKeyProperty("type"); + if (type != null) { + if (type.equals("ThreadPool")) { + threadPools.remove(objectName); + } else if (type.equals("GlobalRequestProcessor")) { + globalRequestProcessors.remove(objectName); + } else if (type.equals("RequestProcessor")) { + requestProcessors.remove(objectName); + } + } + } + } + } +} diff --git a/java/org/apache/catalina/manager/StatusTransformer.java b/java/org/apache/catalina/manager/StatusTransformer.java new file mode 100644 index 0000000..d6719d7 --- /dev/null +++ b/java/org/apache/catalina/manager/StatusTransformer.java @@ -0,0 +1,1083 @@ +/* + * 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.catalina.manager; + +import java.io.PrintWriter; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.text.MessageFormat; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.management.MBeanServer; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.tomcat.util.json.JSONFilter; +import org.apache.tomcat.util.security.Escape; + +/** + * This is a refactoring of the servlet to externalize the output into a simple class. Although we could use XSLT, that + * is unnecessarily complex. + * + * @author Peter Lin + */ +public class StatusTransformer { + + // --------------------------------------------------------- Public Methods + + public static void setContentType(HttpServletResponse response, int mode) { + if (mode == 0) { + response.setContentType("text/html;charset=" + Constants.CHARSET); + } else if (mode == 1) { + response.setContentType("text/xml;charset=" + Constants.CHARSET); + } else if (mode == 2) { + response.setContentType("application/json;charset=" + Constants.CHARSET); + } + } + + + /** + * Write an HTML or XML header. + * + * @param writer the PrintWriter to use + * @param args Path prefix for URLs + * @param mode - 0 = HTML header, 1 = XML declaration, 2 = JSON + */ + public static void writeHeader(PrintWriter writer, Object[] args, int mode) { + if (mode == 0) { + // HTML Header Section + writer.print(MessageFormat.format(Constants.HTML_HEADER_SECTION, args)); + } else if (mode == 1) { + writer.write(Constants.XML_DECLARATION); + writer.print(MessageFormat.format(Constants.XML_STYLE, args)); + writer.write(""); + } else if (mode == 2) { + writer.append('{').append('"').append("tomcat").append('"').append(':').append('{').println(); + } + } + + + /** + * Write the header body. XML output doesn't bother to output this stuff, since it's just title. + * + * @param writer The output writer + * @param args What to write + * @param mode 0 means write + */ + public static void writeBody(PrintWriter writer, Object[] args, int mode) { + if (mode == 0) { + writer.print(MessageFormat.format(Constants.BODY_HEADER_SECTION, args)); + } + } + + + /** + * Write the manager webapp information. + * + * @param writer The output writer + * @param args What to write + * @param mode 0 means write + */ + public static void writeManager(PrintWriter writer, Object[] args, int mode) { + if (mode == 0) { + writer.print(MessageFormat.format(Constants.MANAGER_SECTION, args)); + } + } + + + public static void writePageHeading(PrintWriter writer, Object[] args, int mode) { + if (mode == 0) { + writer.print(MessageFormat.format(Constants.SERVER_HEADER_SECTION, args)); + } + } + + + public static void writeServerInfo(PrintWriter writer, Object[] args, int mode) { + if (mode == 0) { + writer.print(MessageFormat.format(Constants.SERVER_ROW_SECTION, args)); + } + } + + + public static void writeFooter(PrintWriter writer, int mode) { + if (mode == 0) { + // HTML Tail Section + writer.print(Constants.HTML_TAIL_SECTION); + } else if (mode == 1) { + writer.write(""); + } else if (mode == 2) { + writer.append('}').append('}'); + } + } + + + /** + * Write the VM state. + * + * @param writer The output writer + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * @param args I18n labels for the VM state values + * + * @throws Exception Propagated JMX error + */ + public static void writeVMState(PrintWriter writer, int mode, Object[] args) throws Exception { + + SortedMap memoryPoolMBeans = new TreeMap<>(); + for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) { + String sortKey = mbean.getType() + ":" + mbean.getName(); + memoryPoolMBeans.put(sortKey, mbean); + } + + if (mode == 0) { + writer.print("

    JVM

    "); + + writer.print("

    "); + writer.print(args[0]); + writer.print(' '); + writer.print(formatSize(Long.valueOf(Runtime.getRuntime().freeMemory()), true)); + writer.print(' '); + writer.print(args[1]); + writer.print(' '); + writer.print(formatSize(Long.valueOf(Runtime.getRuntime().totalMemory()), true)); + writer.print(' '); + writer.print(args[2]); + writer.print(' '); + writer.print(formatSize(Long.valueOf(Runtime.getRuntime().maxMemory()), true)); + writer.print("

    "); + + writer.write(""); + for (MemoryPoolMXBean memoryPoolMBean : memoryPoolMBeans.values()) { + MemoryUsage usage = memoryPoolMBean.getUsage(); + writer.write(""); + } + writer.write("
    " + args[3] + "" + args[4] + "" + + args[5] + "" + args[6] + "" + args[7] + "" + args[8] + + "
    "); + writer.print(memoryPoolMBean.getName()); + writer.write(""); + writer.print(memoryPoolMBean.getType()); + writer.write(""); + writer.print(formatSize(Long.valueOf(usage.getInit()), true)); + writer.write(""); + writer.print(formatSize(Long.valueOf(usage.getCommitted()), true)); + writer.write(""); + writer.print(formatSize(Long.valueOf(usage.getMax()), true)); + writer.write(""); + writer.print(formatSize(Long.valueOf(usage.getUsed()), true)); + if (usage.getMax() > 0) { + writer.write(" (" + usage.getUsed() * 100 / usage.getMax() + "%)"); + } + writer.write("
    "); + } else if (mode == 1) { + writer.write(""); + + writer.write(""); + + for (MemoryPoolMXBean memoryPoolMBean : memoryPoolMBeans.values()) { + MemoryUsage usage = memoryPoolMBean.getUsage(); + writer.write(""); + } + + writer.write(""); + } else if (mode == 2) { + indent(writer, 1).append('"').append("jvm").append('"').append(':').append('{').println(); + + indent(writer, 2).append('"').append("memory").append('"').append(':').append('{'); + appendJSonValue(writer, "free", Long.toString(Runtime.getRuntime().freeMemory())).append(','); + appendJSonValue(writer, "total", Long.toString(Runtime.getRuntime().totalMemory())).append(','); + appendJSonValue(writer, "max", Long.toString(Runtime.getRuntime().maxMemory())); + writer.append('}').append(',').println(); + + indent(writer, 2).append('"').append("memorypool").append('"').append(':').append('['); + boolean first = true; + for (MemoryPoolMXBean memoryPoolMBean : memoryPoolMBeans.values()) { + MemoryUsage usage = memoryPoolMBean.getUsage(); + if (first) { + first = false; + writer.println(); + } else { + writer.append(',').println(); + } + indent(writer, 3).append('{'); + appendJSonValue(writer, "name", JSONFilter.escape(memoryPoolMBean.getName())).append(','); + appendJSonValue(writer, "type", memoryPoolMBean.getType().toString()).append(','); + appendJSonValue(writer, "usageInit", Long.toString(usage.getInit())).append(','); + appendJSonValue(writer, "usageCommitted", Long.toString(usage.getCommitted())).append(','); + appendJSonValue(writer, "usageMax", Long.toString(usage.getMax())).append(','); + appendJSonValue(writer, "usageUsed", Long.toString(usage.getUsed())); + writer.append('}'); + } + writer.println(); + indent(writer, 2).append(']').println(); + + indent(writer, 1).append('}'); + } + + } + + + private static PrintWriter appendJSonValue(PrintWriter writer, String name, String value) { + return writer.append('"').append(name).append('"').append(':').append('"').append(value).append('"'); + } + + + private static PrintWriter indent(PrintWriter writer, int count) { + for (int i = 0; i < count; i++) { + writer.append(' ').append(' '); + } + return writer; + } + + + /** + * Write connector state. + * + * @param writer The output writer + * @param mBeanServer MBean server + * @param threadPools MBean names for the thread pools of the connectors + * @param globalRequestProcessors MBean names for the global request processors + * @param requestProcessors MBean names for the request processors + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * @param args I18n labels for the Connector state values + * + * @throws Exception Propagated JMX error + */ + public static void writeConnectorsState(PrintWriter writer, MBeanServer mBeanServer, + List threadPools, + List globalRequestProcessors, List requestProcessors, int mode, Object[] args) + throws Exception { + if (mode == 2) { + writer.append(',').println(); + indent(writer, 1).append('"').append("connector").append('"').append(':').append('[').println(); + } + boolean first = true; + for (ObjectName objectName : threadPools) { + if (first) { + first = false; + } else { + if (mode == 2) { + writer.append(',').println(); + } + } + String name = objectName.getKeyProperty("name"); + // use StatusTransformer to output status + writeConnectorState( + writer, objectName, name, mBeanServer, globalRequestProcessors, requestProcessors, mode, args); + } + if (mode == 2) { + writer.append(']'); + } + } + + + /** + * Write connector state. + * + * @param writer The output writer + * @param tpName MBean name of the thread pool + * @param name Connector name + * @param mBeanServer MBean server + * @param globalRequestProcessors MBean names for the global request processors + * @param requestProcessors MBean names for the request processors + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * @param args I18n labels for the Connector state values + * + * @throws Exception Propagated JMX error + */ + public static void writeConnectorState(PrintWriter writer, ObjectName tpName, String name, MBeanServer mBeanServer, + List globalRequestProcessors, List requestProcessors, int mode, Object[] args) + throws Exception { + + if (mode == 0) { + writer.print("

    "); + writer.print(name); + writer.print("

    "); + + writer.print("

    "); + writer.print(args[0]); + writer.print(' '); + writer.print(mBeanServer.getAttribute(tpName, "maxThreads")); + writer.print(' '); + writer.print(args[1]); + writer.print(' '); + writer.print(mBeanServer.getAttribute(tpName, "currentThreadCount")); + writer.print(' '); + writer.print(args[2]); + writer.print(' '); + writer.print(mBeanServer.getAttribute(tpName, "currentThreadsBusy")); + writer.print(' '); + writer.print(args[3]); + writer.print(' '); + writer.print(mBeanServer.getAttribute(tpName, "keepAliveCount")); + + writer.print("
    "); + + ObjectName grpName = null; + + for (ObjectName objectName : globalRequestProcessors) { + // Find the HTTP/1.1 RequestGroupInfo - BZ 65404 + if (name.equals(objectName.getKeyProperty("name")) && objectName.getKeyProperty("Upgrade") == null) { + grpName = objectName; + } + } + + if (grpName == null) { + return; + } + + writer.print(args[4]); + writer.print(' '); + writer.print(formatTime(mBeanServer.getAttribute(grpName, "maxTime"), false)); + writer.print(' '); + writer.print(args[5]); + writer.print(' '); + writer.print(formatTime(mBeanServer.getAttribute(grpName, "processingTime"), true)); + writer.print(' '); + writer.print(args[6]); + writer.print(' '); + writer.print(mBeanServer.getAttribute(grpName, "requestCount")); + writer.print(' '); + writer.print(args[7]); + writer.print(' '); + writer.print(mBeanServer.getAttribute(grpName, "errorCount")); + writer.print(' '); + writer.print(args[8]); + writer.print(' '); + writer.print(formatSize(mBeanServer.getAttribute(grpName, "bytesReceived"), true)); + writer.print(' '); + writer.print(args[9]); + writer.print(' '); + writer.print(formatSize(mBeanServer.getAttribute(grpName, "bytesSent"), true)); + writer.print("

    "); + + writer.print(""); + + for (ObjectName objectName : requestProcessors) { + if (name.equals(objectName.getKeyProperty("worker"))) { + writer.print(""); + writeProcessorState(writer, objectName, mBeanServer, mode); + writer.print(""); + } + } + + writer.print("
    " + args[10] + "" + args[11] + "" + args[12] + + "" + args[13] + "" + args[14] + "" + args[15] + "" + args[16] + + "" + args[17] + "
    "); + + writer.print("

    "); + writer.print(args[18]); + writer.print("

    "); + } else if (mode == 1) { + writer.write(""); + + writer.write(""); + + ObjectName grpName = null; + + for (ObjectName objectName : globalRequestProcessors) { + // Find the HTTP/1.1 RequestGroupInfo - BZ 65404 + if (name.equals(objectName.getKeyProperty("name")) && objectName.getKeyProperty("Upgrade") == null) { + grpName = objectName; + } + } + + if (grpName != null) { + + writer.write(""); + + writer.write(""); + for (ObjectName objectName : requestProcessors) { + if (name.equals(objectName.getKeyProperty("worker"))) { + writeProcessorState(writer, objectName, mBeanServer, mode); + } + } + writer.write(""); + } + + writer.write(""); + } else if (mode == 2) { + indent(writer, 2).append('{').println(); + + indent(writer, 3); + String jsonName = JSONFilter.escape(name); + if (jsonName.length() > 4 && jsonName.startsWith("\\\"") && jsonName.endsWith("\\\"")) { + jsonName = jsonName.substring(2, jsonName.length() - 2); + } + appendJSonValue(writer, "name", jsonName).append(',').println(); + indent(writer, 3).append('"').append("threadInfo").append('"').append(':').append('{'); + appendJSonValue(writer, "maxThreads", mBeanServer.getAttribute(tpName, "maxThreads").toString()).append(','); + appendJSonValue(writer, "currentThreadCount", mBeanServer.getAttribute(tpName, "currentThreadCount").toString()).append(','); + appendJSonValue(writer, "currentThreadsBusy", mBeanServer.getAttribute(tpName, "currentThreadsBusy").toString()); + writer.append('}'); + + ObjectName grpName = null; + for (ObjectName objectName : globalRequestProcessors) { + // Find the HTTP/1.1 RequestGroupInfo - BZ 65404 + if (name.equals(objectName.getKeyProperty("name")) && objectName.getKeyProperty("Upgrade") == null) { + grpName = objectName; + } + } + + if (grpName != null) { + writer.append(',').println(); + indent(writer, 3).append('"').append("requestInfo").append('"').append(':').append('{'); + appendJSonValue(writer, "maxTime", mBeanServer.getAttribute(grpName, "maxTime").toString()).append(','); + appendJSonValue(writer, "processingTime", mBeanServer.getAttribute(grpName, "processingTime").toString()).append(','); + appendJSonValue(writer, "requestCount", mBeanServer.getAttribute(grpName, "requestCount").toString()).append(','); + appendJSonValue(writer, "errorCount", mBeanServer.getAttribute(grpName, "errorCount").toString()).append(','); + appendJSonValue(writer, "bytesReceived", mBeanServer.getAttribute(grpName, "bytesReceived").toString()).append(','); + appendJSonValue(writer, "bytesSent", mBeanServer.getAttribute(grpName, "bytesSent").toString()); + writer.append('}').println(); + // Note: No detailed per processor info + } + + indent(writer, 2).append('}'); + } + } + + + /** + * Write processor state. + * + * @param writer The output writer + * @param pName MBean name of the processor + * @param mBeanServer MBean server + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * + * @throws Exception Propagated JMX error + */ + protected static void writeProcessorState(PrintWriter writer, ObjectName pName, MBeanServer mBeanServer, int mode) + throws Exception { + + Integer stageValue = (Integer) mBeanServer.getAttribute(pName, "stage"); + int stage = stageValue.intValue(); + boolean fullStatus = true; + boolean showRequest = true; + String stageStr = null; + + switch (stage) { + + case org.apache.coyote.Constants.STAGE_PARSE: + stageStr = "P"; + fullStatus = false; + break; + case org.apache.coyote.Constants.STAGE_PREPARE: + stageStr = "P"; + fullStatus = false; + break; + case org.apache.coyote.Constants.STAGE_SERVICE: + stageStr = "S"; + break; + case org.apache.coyote.Constants.STAGE_ENDINPUT: + stageStr = "F"; + break; + case org.apache.coyote.Constants.STAGE_ENDOUTPUT: + stageStr = "F"; + break; + case org.apache.coyote.Constants.STAGE_ENDED: + stageStr = "R"; + fullStatus = false; + break; + case org.apache.coyote.Constants.STAGE_KEEPALIVE: + stageStr = "K"; + showRequest = false; + break; + case org.apache.coyote.Constants.STAGE_NEW: + stageStr = "R"; + fullStatus = false; + break; + default: + // Unknown stage + stageStr = "?"; + fullStatus = false; + + } + + if (mode == 0) { + writer.write(""); + writer.write(stageStr); + writer.write(""); + + if (fullStatus) { + writer.write(""); + writer.print(formatTime(mBeanServer.getAttribute(pName, "requestProcessingTime"), false)); + writer.write(""); + writer.write(""); + if (showRequest) { + writer.print(formatSize(mBeanServer.getAttribute(pName, "requestBytesSent"), false)); + } else { + writer.write("?"); + } + writer.write(""); + writer.write(""); + if (showRequest) { + writer.print(formatSize(mBeanServer.getAttribute(pName, "requestBytesReceived"), false)); + } else { + writer.write("?"); + } + writer.write(""); + writer.write(""); + writer.print(Escape.htmlElementContent(mBeanServer.getAttribute(pName, "remoteAddrForwarded"))); + writer.write(""); + writer.write(""); + writer.print(Escape.htmlElementContent(mBeanServer.getAttribute(pName, "remoteAddr"))); + writer.write(""); + writer.write(""); + writer.write(Escape.htmlElementContent(mBeanServer.getAttribute(pName, "virtualHost"))); + writer.write(""); + writer.write(""); + if (showRequest) { + writer.write(Escape.htmlElementContent(mBeanServer.getAttribute(pName, "method"))); + writer.write(' '); + writer.write(Escape.htmlElementContent(mBeanServer.getAttribute(pName, "currentUri"))); + String queryString = (String) mBeanServer.getAttribute(pName, "currentQueryString"); + if (queryString != null && !queryString.equals("")) { + writer.write("?"); + writer.print(Escape.htmlElementContent(queryString)); + } + writer.write(' '); + writer.write(Escape.htmlElementContent(mBeanServer.getAttribute(pName, "protocol"))); + } else { + writer.write("?"); + } + writer.write(""); + } else { + writer.write("??????"); + } + } else if (mode == 1) { + writer.write(""); + } + + } + + + /** + * Write applications state. + * + * @param writer The output writer + * @param mBeanServer MBean server + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * + * @throws Exception Propagated JMX error + */ + public static void writeDetailedState(PrintWriter writer, MBeanServer mBeanServer, int mode) throws Exception { + + ObjectName queryHosts = new ObjectName("*:j2eeType=WebModule,*"); + Set hostsON = mBeanServer.queryNames(queryHosts, null); + if (mode == 0) { + + // Navigation menu + writer.print("

    "); + writer.print("Application list"); + writer.print("

    "); + + writer.print("

    "); + int count = 0; + Iterator iterator = hostsON.iterator(); + while (iterator.hasNext()) { + ObjectName contextON = iterator.next(); + String webModuleName = contextON.getKeyProperty("name"); + if (webModuleName.startsWith("//")) { + webModuleName = webModuleName.substring(2); + } + int slash = webModuleName.indexOf('/'); + if (slash == -1) { + count++; + continue; + } + + writer.print(""); + writer.print(Escape.htmlElementContent(webModuleName)); + writer.print(""); + if (iterator.hasNext()) { + writer.print("
    "); + } + + } + writer.print("

    "); + + // Webapp list + count = 0; + iterator = hostsON.iterator(); + while (iterator.hasNext()) { + ObjectName contextON = iterator.next(); + writer.print(""); + writeContext(writer, contextON, mBeanServer, mode); + } + + } else if (mode == 1) { + // for now we don't write out the Detailed state in XML + } else if (mode == 2) { + writer.append(',').println(); + indent(writer, 1).append('"').append("context").append('"').append(':').append('[').println(); + Iterator iterator = hostsON.iterator(); + boolean first = true; + while (iterator.hasNext()) { + if (first) { + first = false; + } else { + writer.append(',').println(); + } + ObjectName contextON = iterator.next(); + writeContext(writer, contextON, mBeanServer, mode); + } + writer.println(); + indent(writer, 1).append(']').println(); + } + + } + + + /** + * Write context state. + * + * @param writer The output writer + * @param objectName The context MBean name + * @param mBeanServer MBean server + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * + * @throws Exception Propagated JMX error + */ + protected static void writeContext(PrintWriter writer, ObjectName objectName, MBeanServer mBeanServer, int mode) + throws Exception { + + String webModuleName = objectName.getKeyProperty("name"); + String name = webModuleName; + if (name == null) { + return; + } + + String hostName = null; + String contextName = null; + if (name.startsWith("//")) { + name = name.substring(2); + } + int slash = name.indexOf('/'); + if (slash != -1) { + hostName = name.substring(0, slash); + contextName = name.substring(slash); + } else { + return; + } + + ObjectName queryManager = new ObjectName( + objectName.getDomain() + ":type=Manager,context=" + contextName + ",host=" + hostName + ",*"); + Set managersON = mBeanServer.queryNames(queryManager, null); + ObjectName managerON = null; + for (ObjectName aManagersON : managersON) { + managerON = aManagersON; + } + + ObjectName queryJspMonitor = + new ObjectName(objectName.getDomain() + ":type=JspMonitor,WebModule=" + webModuleName + ",*"); + Set jspMonitorONs = mBeanServer.queryNames(queryJspMonitor, null); + + // Special case for the root context + if (contextName.equals("/")) { + contextName = ""; + } + + if (mode == 0) { + + writer.print("

    "); + writer.print(Escape.htmlElementContent(name)); + writer.print("

    "); + writer.print("
    "); + + writer.print("

    "); + Object startTime = mBeanServer.getAttribute(objectName, "startTime"); + writer.print(" Start time: " + new Date(((Long) startTime).longValue())); + writer.print(" Startup time: "); + writer.print(formatTime(mBeanServer.getAttribute(objectName, "startupTime"), false)); + writer.print(" TLD scan time: "); + writer.print(formatTime(mBeanServer.getAttribute(objectName, "tldScanTime"), false)); + if (managerON != null) { + writeManager(writer, managerON, mBeanServer, mode); + } + if (jspMonitorONs != null) { + writeJspMonitor(writer, jspMonitorONs, mBeanServer, mode); + } + writer.print("

    "); + + String onStr = objectName.getDomain() + ":j2eeType=Servlet,WebModule=" + webModuleName + ",*"; + ObjectName servletObjectName = new ObjectName(onStr); + Set set = mBeanServer.queryMBeans(servletObjectName, null); + for (ObjectInstance oi : set) { + writeWrapper(writer, oi.getObjectName(), mBeanServer, mode); + } + + } else if (mode == 1) { + // for now we don't write out the context in XML + } else if (mode == 2) { + indent(writer, 2).append('{').println(); + appendJSonValue(indent(writer, 3), "name", JSONFilter.escape(JSONFilter.escape(name))).append(','); + appendJSonValue(writer, "startTime", + new Date(((Long) mBeanServer.getAttribute(objectName, "startTime")).longValue()).toString()).append(','); + appendJSonValue(writer, "startupTime", mBeanServer.getAttribute(objectName, "startupTime").toString()).append(','); + appendJSonValue(writer, "tldScanTime", mBeanServer.getAttribute(objectName, "tldScanTime").toString()); + if (managerON != null) { + writeManager(writer, managerON, mBeanServer, mode); + } + if (jspMonitorONs != null) { + writeJspMonitor(writer, jspMonitorONs, mBeanServer, mode); + } + writer.append(',').println(); + indent(writer, 3).append('"').append("wrapper").append('"').append(':').append('[').println(); + String onStr = objectName.getDomain() + ":j2eeType=Servlet,WebModule=" + webModuleName + ",*"; + ObjectName servletObjectName = new ObjectName(onStr); + Set set = mBeanServer.queryMBeans(servletObjectName, null); + boolean first = true; + for (ObjectInstance oi : set) { + if (first) { + first = false; + } else { + writer.append(',').println(); + } + writeWrapper(writer, oi.getObjectName(), mBeanServer, mode); + } + writer.println(); + indent(writer, 3).append(']').println(); + indent(writer, 2).append('}'); + } + + } + + + /** + * Write detailed information about a manager. + * + * @param writer The output writer + * @param objectName The manager MBean name + * @param mBeanServer MBean server + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * + * @throws Exception Propagated JMX error + */ + public static void writeManager(PrintWriter writer, ObjectName objectName, MBeanServer mBeanServer, int mode) + throws Exception { + + if (mode == 0) { + writer.print("
    "); + writer.print(" Active sessions: "); + writer.print(mBeanServer.getAttribute(objectName, "activeSessions")); + writer.print(" Session count: "); + writer.print(mBeanServer.getAttribute(objectName, "sessionCounter")); + writer.print(" Max active sessions: "); + writer.print(mBeanServer.getAttribute(objectName, "maxActive")); + writer.print(" Rejected session creations: "); + writer.print(mBeanServer.getAttribute(objectName, "rejectedSessions")); + writer.print(" Expired sessions: "); + writer.print(mBeanServer.getAttribute(objectName, "expiredSessions")); + writer.print(" Longest session alive time: "); + writer.print(formatSeconds(mBeanServer.getAttribute(objectName, "sessionMaxAliveTime"))); + writer.print(" Average session alive time: "); + writer.print(formatSeconds(mBeanServer.getAttribute(objectName, "sessionAverageAliveTime"))); + writer.print(" Processing time: "); + writer.print(formatTime(mBeanServer.getAttribute(objectName, "processingTime"), false)); + } else if (mode == 1) { + // for now we don't write out the wrapper details + } else if (mode == 2) { + writer.append(',').println(); + indent(writer, 3).append('"').append("manager").append('"').append(':').append('{'); + appendJSonValue(writer, "activeSessions", mBeanServer.getAttribute(objectName, "activeSessions").toString()).append(','); + appendJSonValue(writer, "sessionCounter", mBeanServer.getAttribute(objectName, "sessionCounter").toString()).append(','); + appendJSonValue(writer, "maxActive", mBeanServer.getAttribute(objectName, "maxActive").toString()).append(','); + appendJSonValue(writer, "rejectedSessions", mBeanServer.getAttribute(objectName, "rejectedSessions").toString()).append(','); + appendJSonValue(writer, "expiredSessions", mBeanServer.getAttribute(objectName, "expiredSessions").toString()).append(','); + appendJSonValue(writer, "sessionMaxAliveTime", mBeanServer.getAttribute(objectName, "sessionMaxAliveTime").toString()).append(','); + appendJSonValue(writer, "sessionAverageAliveTime", mBeanServer.getAttribute(objectName, "sessionAverageAliveTime").toString()).append(','); + appendJSonValue(writer, "processingTime", mBeanServer.getAttribute(objectName, "processingTime").toString()); + writer.append('}'); + } + + } + + + /** + * Write JSP monitoring information. + * + * @param writer The output writer + * @param jspMonitorONs The JSP MBean names + * @param mBeanServer MBean server + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * + * @throws Exception Propagated JMX error + */ + public static void writeJspMonitor(PrintWriter writer, Set jspMonitorONs, MBeanServer mBeanServer, + int mode) throws Exception { + + int jspCount = 0; + int jspReloadCount = 0; + + for (ObjectName jspMonitorON : jspMonitorONs) { + Object obj = mBeanServer.getAttribute(jspMonitorON, "jspCount"); + jspCount += ((Integer) obj).intValue(); + obj = mBeanServer.getAttribute(jspMonitorON, "jspReloadCount"); + jspReloadCount += ((Integer) obj).intValue(); + } + + if (mode == 0) { + writer.print("
    "); + writer.print(" JSPs loaded: "); + writer.print(jspCount); + writer.print(" JSPs reloaded: "); + writer.print(jspReloadCount); + } else if (mode == 1) { + // for now we don't write out anything + } else if (mode == 2) { + writer.append(',').println(); + indent(writer, 3).append('"').append("jsp").append('"').append(':').append('{'); + appendJSonValue(writer, "jspCount", Integer.toString(jspCount)).append(','); + appendJSonValue(writer, "jspReloadCount", Integer.toString(jspReloadCount)); + writer.append('}'); + } + } + + + /** + * Write detailed information about a wrapper. + * + * @param writer The output writer + * @param objectName The wrapper MBean names + * @param mBeanServer MBean server + * @param mode Mode 0 will generate HTML. Mode 1 will generate XML. + * + * @throws Exception Propagated JMX error + */ + public static void writeWrapper(PrintWriter writer, ObjectName objectName, MBeanServer mBeanServer, int mode) + throws Exception { + + String servletName = objectName.getKeyProperty("name"); + + String[] mappings = (String[]) mBeanServer.invoke(objectName, "findMappings", null, null); + + if (mode == 0) { + writer.print("

    "); + writer.print(Escape.htmlElementContent(servletName)); + if (mappings != null && mappings.length > 0) { + writer.print(" [ "); + for (int i = 0; i < mappings.length; i++) { + writer.print(Escape.htmlElementContent(mappings[i])); + if (i < mappings.length - 1) { + writer.print(" , "); + } + } + writer.print(" ] "); + } + writer.print("

    "); + + writer.print("

    "); + writer.print(" Processing time: "); + writer.print(formatTime(mBeanServer.getAttribute(objectName, "processingTime"), true)); + writer.print(" Max time: "); + writer.print(formatTime(mBeanServer.getAttribute(objectName, "maxTime"), false)); + writer.print(" Request count: "); + writer.print(mBeanServer.getAttribute(objectName, "requestCount")); + writer.print(" Error count: "); + writer.print(mBeanServer.getAttribute(objectName, "errorCount")); + writer.print(" Load time: "); + writer.print(formatTime(mBeanServer.getAttribute(objectName, "loadTime"), false)); + writer.print(" Classloading time: "); + writer.print(formatTime(mBeanServer.getAttribute(objectName, "classLoadTime"), false)); + writer.print("

    "); + } else if (mode == 1) { + // for now we don't write out the wrapper details + } else if (mode == 2) { + indent(writer, 4).append('{'); + appendJSonValue(writer, "servletName", JSONFilter.escape(servletName)).append(','); + appendJSonValue(writer, "processingTime", mBeanServer.getAttribute(objectName, "processingTime").toString()).append(','); + appendJSonValue(writer, "maxTime", mBeanServer.getAttribute(objectName, "maxTime").toString()).append(','); + appendJSonValue(writer, "requestCount", mBeanServer.getAttribute(objectName, "requestCount").toString()).append(','); + appendJSonValue(writer, "errorCount", mBeanServer.getAttribute(objectName, "errorCount").toString()).append(','); + appendJSonValue(writer, "loadTime", mBeanServer.getAttribute(objectName, "loadTime").toString()).append(','); + appendJSonValue(writer, "classLoadTime", mBeanServer.getAttribute(objectName, "classLoadTime").toString()); + writer.append('}'); + } + + } + + + /** + * Display the given size in bytes, either as KiB or MiB. + * + * @param obj The object to format + * @param mb true to display MiB, false for KiB + * + * @return formatted size + */ + public static String formatSize(Object obj, boolean mb) { + + long bytes = -1L; + + if (obj instanceof Long) { + bytes = ((Long) obj).longValue(); + } else if (obj instanceof Integer) { + bytes = ((Integer) obj).intValue(); + } + + if (mb) { + StringBuilder buff = new StringBuilder(); + if (bytes < 0) { + buff.append('-'); + bytes = -bytes; + } + long mbytes = bytes / (1024 * 1024); + long rest = (bytes - mbytes * (1024 * 1024)) * 100 / (1024 * 1024); + buff.append(mbytes).append('.'); + if (rest < 10) { + buff.append('0'); + } + buff.append(rest).append(" MiB"); + return buff.toString(); + } else { + return bytes / 1024 + " KiB"; + } + + } + + + /** + * Display the given time in ms, either as ms or s. + * + * @param obj The object to format + * @param seconds true to display seconds, false for milliseconds + * + * @return formatted time + */ + public static String formatTime(Object obj, boolean seconds) { + + long time = -1L; + + if (obj instanceof Long) { + time = ((Long) obj).longValue(); + } else if (obj instanceof Integer) { + time = ((Integer) obj).intValue(); + } + + if (seconds) { + return (float) time / 1000 + " s"; + } else { + return time + " ms"; + } + } + + + /** + * Formats the given time (given in seconds) as a string. + * + * @param obj Time object to be formatted as string + * + * @return formatted time + */ + public static String formatSeconds(Object obj) { + + long time = -1L; + + if (obj instanceof Long) { + time = ((Long) obj).longValue(); + } else if (obj instanceof Integer) { + time = ((Integer) obj).intValue(); + } + + return time + " s"; + } + +} diff --git a/java/org/apache/catalina/manager/host/Constants.java b/java/org/apache/catalina/manager/host/Constants.java new file mode 100644 index 0000000..85435fb --- /dev/null +++ b/java/org/apache/catalina/manager/host/Constants.java @@ -0,0 +1,92 @@ +/* + * 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.catalina.manager.host; + + +public class Constants { + + public static final String Package = "org.apache.catalina.manager.host"; + + public static final String REL_EXTERNAL = org.apache.catalina.manager.Constants.REL_EXTERNAL; + + //@formatter:off + public static final String MESSAGE_SECTION = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
    " + + "{0} 
    {1}
    \n" + + "
    \n" + + "\n"; + + public static final String MANAGER_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
    {0}
    {2}{4}{6}{8}
    \n" + + "
    \n" + + "\n"; + + public static final String SERVER_HEADER_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + public static final String SERVER_ROW_SECTION = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "
    {0}
    {1}{2}{3}{4}{5}{6}
    {0}{1}{2}{3}{4}{5}
    \n" + + "
    \n" + + "\n"; + + public static final String HTML_TAIL_SECTION = + "
    \n" + + "
    \n" + + " Copyright © 1999-2024, Apache Software Foundation" + + "
    \n" + + "\n" + + "\n" + + ""; + //@formatter:on + + public static final String CHARSET = "utf-8"; +} + diff --git a/java/org/apache/catalina/manager/host/HTMLHostManagerServlet.java b/java/org/apache/catalina/manager/host/HTMLHostManagerServlet.java new file mode 100644 index 0000000..688ba4f --- /dev/null +++ b/java/org/apache/catalina/manager/host/HTMLHostManagerServlet.java @@ -0,0 +1,570 @@ +/* + * 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.catalina.manager.host; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URLEncoder; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.SortedSet; +import java.util.TreeSet; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.Host; +import org.apache.catalina.util.ServerInfo; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; + +/** + * Servlet that enables remote management of the virtual hosts deployed on the server. Normally, this functionality will + * be protected by a security constraint in the web application deployment descriptor. However, this requirement can be + * relaxed during testing. + *

    + * The difference between the HostManagerServlet and this Servlet is that this Servlet prints out an HTML + * interface which makes it easier to administrate. + *

    + * However if you use a software that parses the output of HostManagerServlet you won't be able to upgrade + * to this Servlet since the output are not in the same format as from HostManagerServlet + * + * @author Bip Thelin + * @author Malcolm Edgar + * @author Glenn L. Nielsen + * @author Peter Rossbach + * + * @see org.apache.catalina.manager.ManagerServlet + */ +public final class HTMLHostManagerServlet extends HostManagerServlet { + + private static final long serialVersionUID = 1L; + + // --------------------------------------------------------- Public Methods + + /** + * Process a GET request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + + // Identify the request parameters that we need + String command = request.getPathInfo(); + + // Prepare our output writer to generate the response message + response.setContentType("text/html; charset=" + Constants.CHARSET); + + String message = ""; + // Process the requested command + if (command == null) { + // No command == list + } else if (command.equals("/list")) { + // Nothing to do - always generate list + } else if (command.equals("/add") || command.equals("/remove") || command.equals("/start") || + command.equals("/stop") || command.equals("/persist")) { + message = smClient.getString("hostManagerServlet.postCommand", command); + } else { + message = smClient.getString("hostManagerServlet.unknownCommand", command); + } + + list(request, response, message, smClient); + } + + + /** + * Process a POST request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + + // Identify the request parameters that we need + String command = request.getPathInfo(); + + String name = request.getParameter("name"); + + // Prepare our output writer to generate the response message + response.setContentType("text/html; charset=" + Constants.CHARSET); + + String message = ""; + + // Process the requested command + if (command == null) { + // No command == list + } else if (command.equals("/add")) { + message = add(request, name, smClient); + } else if (command.equals("/remove")) { + message = remove(name, smClient); + } else if (command.equals("/start")) { + message = start(name, smClient); + } else if (command.equals("/stop")) { + message = stop(name, smClient); + } else if (command.equals("/persist")) { + message = persist(smClient); + } else { + // Try GET + doGet(request, response); + } + + list(request, response, message, smClient); + } + + + /** + * Add a host using the specified parameters. + * + * @param request The Servlet request + * @param name Host name + * @param smClient StringManager for the client's locale + * + * @return output + */ + protected String add(HttpServletRequest request, String name, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.add(request, printWriter, name, true, smClient); + + return stringWriter.toString(); + } + + + /** + * Remove the specified host. + * + * @param name Host name + * @param smClient StringManager for the client's locale + * + * @return output + */ + protected String remove(String name, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.remove(printWriter, name, smClient); + + return stringWriter.toString(); + } + + + /** + * Start the host with the specified name. + * + * @param name Host name + * @param smClient StringManager for the client's locale + * + * @return output + */ + protected String start(String name, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.start(printWriter, name, smClient); + + return stringWriter.toString(); + } + + + /** + * Stop the host with the specified name. + * + * @param name Host name + * @param smClient StringManager for the client's locale + * + * @return output + */ + protected String stop(String name, StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.stop(printWriter, name, smClient); + + return stringWriter.toString(); + } + + + /** + * Persist the current configuration to server.xml. + * + * @param smClient i18n resources localized for the client + * + * @return output + */ + protected String persist(StringManager smClient) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + super.persist(printWriter, smClient); + + return stringWriter.toString(); + } + + + /** + * Render an HTML list of the currently active Contexts in our virtual host, and memory and server status + * information. + * + * @param request The request + * @param response The response + * @param message a message to display + * @param smClient StringManager for the client's locale + * + * @throws IOException An IO error occurred + */ + public void list(HttpServletRequest request, HttpServletResponse response, String message, StringManager smClient) + throws IOException { + + if (debug >= 1) { + log(sm.getString("hostManagerServlet.list", engine.getName())); + } + + PrintWriter writer = response.getWriter(); + + Object[] args = new Object[2]; + args[0] = getServletContext().getContextPath(); + args[1] = smClient.getString("htmlHostManagerServlet.title"); + + // HTML Header Section + writer.print(MessageFormat.format(org.apache.catalina.manager.Constants.HTML_HEADER_SECTION, args)); + + // Body Header Section + writer.print(MessageFormat.format(org.apache.catalina.manager.Constants.BODY_HEADER_SECTION, args)); + + // Message Section + args = new Object[3]; + args[0] = smClient.getString("htmlHostManagerServlet.messageLabel"); + if (message == null || message.length() == 0) { + args[1] = "OK"; + } else { + args[1] = Escape.htmlElementContent(message); + } + writer.print(MessageFormat.format(Constants.MESSAGE_SECTION, args)); + + // Manager Section + args = new Object[9]; + args[0] = smClient.getString("htmlHostManagerServlet.manager"); + args[1] = response.encodeURL(getServletContext().getContextPath() + "/html/list"); + args[2] = smClient.getString("htmlHostManagerServlet.list"); + args[3] = // External link + getServletContext().getContextPath() + "/" + + smClient.getString("htmlHostManagerServlet.helpHtmlManagerFile"); + args[4] = smClient.getString("htmlHostManagerServlet.helpHtmlManager"); + args[5] = // External link + getServletContext().getContextPath() + "/" + + smClient.getString("htmlHostManagerServlet.helpManagerFile"); + args[6] = smClient.getString("htmlHostManagerServlet.helpManager"); + args[7] = response.encodeURL("/manager/status"); + args[8] = smClient.getString("statusServlet.title"); + writer.print(MessageFormat.format(Constants.MANAGER_SECTION, args)); + + // Hosts Header Section + args = new Object[3]; + args[0] = smClient.getString("htmlHostManagerServlet.hostName"); + args[1] = smClient.getString("htmlHostManagerServlet.hostAliases"); + args[2] = smClient.getString("htmlHostManagerServlet.hostTasks"); + writer.print(MessageFormat.format(HOSTS_HEADER_SECTION, args)); + + // Hosts Row Section + // Create sorted set of host names. + Container[] children = engine.findChildren(); + String hostNames[] = new String[children.length]; + for (int i = 0; i < children.length; i++) { + hostNames[i] = children[i].getName(); + } + + SortedSet sortedHostNames = new TreeSet<>(); + sortedHostNames.addAll(Arrays.asList(hostNames)); + + String hostsStart = smClient.getString("htmlHostManagerServlet.hostsStart"); + String hostsStop = smClient.getString("htmlHostManagerServlet.hostsStop"); + String hostsRemove = smClient.getString("htmlHostManagerServlet.hostsRemove"); + String hostThis = smClient.getString("htmlHostManagerServlet.hostThis"); + + for (String hostName : sortedHostNames) { + Host host = (Host) engine.findChild(hostName); + + if (host != null) { + args = new Object[2]; + args[0] = // External link + Escape.htmlElementContent(hostName); + String[] aliases = host.findAliases(); + StringBuilder buf = new StringBuilder(); + if (aliases.length > 0) { + buf.append(aliases[0]); + for (int j = 1; j < aliases.length; j++) { + buf.append(", ").append(aliases[j]); + } + } + + if (buf.length() == 0) { + buf.append(" "); + args[1] = buf.toString(); + } else { + args[1] = Escape.htmlElementContent(buf.toString()); + } + + writer.print(MessageFormat.format(HOSTS_ROW_DETAILS_SECTION, args)); + + args = new Object[5]; + if (host.getState().isAvailable()) { + args[0] = response.encodeURL(getServletContext().getContextPath() + "/html/stop?name=" + + URLEncoder.encode(hostName, "UTF-8")); + args[1] = hostsStop; + } else { + args[0] = response.encodeURL(getServletContext().getContextPath() + "/html/start?name=" + + URLEncoder.encode(hostName, "UTF-8")); + args[1] = hostsStart; + } + args[2] = response.encodeURL(getServletContext().getContextPath() + "/html/remove?name=" + + URLEncoder.encode(hostName, "UTF-8")); + args[3] = hostsRemove; + args[4] = hostThis; + if (host == this.installedHost) { + writer.print(MessageFormat.format(MANAGER_HOST_ROW_BUTTON_SECTION, args)); + } else { + writer.print(MessageFormat.format(HOSTS_ROW_BUTTON_SECTION, args)); + } + } + } + + // Add Section + args = new Object[6]; + args[0] = smClient.getString("htmlHostManagerServlet.addTitle"); + args[1] = smClient.getString("htmlHostManagerServlet.addHost"); + args[2] = response.encodeURL(getServletContext().getContextPath() + "/html/add"); + args[3] = smClient.getString("htmlHostManagerServlet.addName"); + args[4] = smClient.getString("htmlHostManagerServlet.addAliases"); + args[5] = smClient.getString("htmlHostManagerServlet.addAppBase"); + writer.print(MessageFormat.format(ADD_SECTION_START, args)); + + args = new Object[3]; + args[0] = smClient.getString("htmlHostManagerServlet.addAutoDeploy"); + args[1] = "autoDeploy"; + args[2] = "checked"; + writer.print(MessageFormat.format(ADD_SECTION_BOOLEAN, args)); + args[0] = smClient.getString("htmlHostManagerServlet.addDeployOnStartup"); + args[1] = "deployOnStartup"; + args[2] = "checked"; + writer.print(MessageFormat.format(ADD_SECTION_BOOLEAN, args)); + args[0] = smClient.getString("htmlHostManagerServlet.addDeployXML"); + args[1] = "deployXML"; + args[2] = "checked"; + writer.print(MessageFormat.format(ADD_SECTION_BOOLEAN, args)); + args[0] = smClient.getString("htmlHostManagerServlet.addUnpackWARs"); + args[1] = "unpackWARs"; + args[2] = "checked"; + writer.print(MessageFormat.format(ADD_SECTION_BOOLEAN, args)); + + args[0] = smClient.getString("htmlHostManagerServlet.addManager"); + args[1] = "manager"; + args[2] = "checked"; + writer.print(MessageFormat.format(ADD_SECTION_BOOLEAN, args)); + + args[0] = smClient.getString("htmlHostManagerServlet.addCopyXML"); + args[1] = "copyXML"; + args[2] = ""; + writer.print(MessageFormat.format(ADD_SECTION_BOOLEAN, args)); + + args = new Object[1]; + args[0] = smClient.getString("htmlHostManagerServlet.addButton"); + writer.print(MessageFormat.format(ADD_SECTION_END, args)); + + // Persist Configuration Section + args = new Object[4]; + args[0] = smClient.getString("htmlHostManagerServlet.persistTitle"); + args[1] = response.encodeURL(getServletContext().getContextPath() + "/html/persist"); + args[2] = smClient.getString("htmlHostManagerServlet.persistAllButton"); + args[3] = smClient.getString("htmlHostManagerServlet.persistAll"); + writer.print(MessageFormat.format(PERSIST_SECTION, args)); + + // Server Header Section + args = new Object[7]; + args[0] = smClient.getString("htmlHostManagerServlet.serverTitle"); + args[1] = smClient.getString("htmlHostManagerServlet.serverVersion"); + args[2] = smClient.getString("htmlHostManagerServlet.serverJVMVersion"); + args[3] = smClient.getString("htmlHostManagerServlet.serverJVMVendor"); + args[4] = smClient.getString("htmlHostManagerServlet.serverOSName"); + args[5] = smClient.getString("htmlHostManagerServlet.serverOSVersion"); + args[6] = smClient.getString("htmlHostManagerServlet.serverOSArch"); + writer.print(MessageFormat.format(Constants.SERVER_HEADER_SECTION, args)); + + // Server Row Section + args = new Object[6]; + args[0] = ServerInfo.getServerInfo(); + args[1] = System.getProperty("java.runtime.version"); + args[2] = System.getProperty("java.vm.vendor"); + args[3] = System.getProperty("os.name"); + args[4] = System.getProperty("os.version"); + args[5] = System.getProperty("os.arch"); + writer.print(MessageFormat.format(Constants.SERVER_ROW_SECTION, args)); + + // HTML Tail Section + writer.print(Constants.HTML_TAIL_SECTION); + + // Finish up the response + writer.flush(); + writer.close(); + } + + + // ------------------------------------------------------ Private Constants + + // These HTML sections are broken in relatively small sections, because of + // limited number of substitutions MessageFormat can process + // (maximum of 10). + + //@formatter:off + private static final String HOSTS_HEADER_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "\n"; + + private static final String HOSTS_ROW_DETAILS_SECTION = + "\n" + + " \n" + + " \n"; + + private static final String MANAGER_HOST_ROW_BUTTON_SECTION = + " \n" + + "\n"; + + private static final String HOSTS_ROW_BUTTON_SECTION = + " \n" + + "\n"; + + private static final String ADD_SECTION_START = + "
    {0}
    {0}{1}{2}
    {0}" + + "{1}\n" + + " {4}\n" + + "
    \n" + + "
    " + + " " + + "
    \n" + + "
    " + + " " + + "
    \n" + + "
    \n" + + "
    \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "
    {0}
    {1}
    \n" + + "
    \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" + + "\n" + + " \n" + + " \n" + + "\n" ; + + private static final String ADD_SECTION_BOOLEAN = + "\n" + + " \n" + + " \n" + + "\n" ; + + private static final String ADD_SECTION_END = + "\n" + + " \n" + + " \n" + + "\n" + + "
    \n" + + " {3}\n" + + " \n" + + " \n" + + "
    \n" + + " {4}\n" + + " \n" + + " \n" + + "
    \n" + + " {5}\n" + + " \n" + + " \n" + + "
    \n" + + " {0}\n" + + " \n" + + " \n" + + "
    \n" + + "  \n" + + " \n" + + " \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "
    \n" + + "\n"; + + private static final String PERSIST_SECTION = + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + " \n" + + "\n" + + "
    {0}
    \n" + + "
    " + + " " + + "
    {3}\n" + + "
    \n" + + "
    \n" + + "\n"; + //@formatter:on +} diff --git a/java/org/apache/catalina/manager/host/HostManagerServlet.java b/java/org/apache/catalina/manager/host/HostManagerServlet.java new file mode 100644 index 0000000..49206c4 --- /dev/null +++ b/java/org/apache/catalina/manager/host/HostManagerServlet.java @@ -0,0 +1,658 @@ +/* + * 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.catalina.manager.host; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.StringTokenizer; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import jakarta.servlet.ServletException; +import jakarta.servlet.UnavailableException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.ContainerServlet; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.ContainerBase; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.HostConfig; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Servlet that enables remote management of the virtual hosts installed on the server. Normally, this functionality + * will be protected by a security constraint in the web application deployment descriptor. However, this requirement + * can be relaxed during testing. + *

    + * This servlet examines the value returned by getPathInfo() and related query parameters to determine what + * action is being requested. The following actions and parameters (starting after the servlet path) are supported: + *

      + *
    • /add?name={host-name}&aliases={host-aliases}&manager={manager} - Create and add a new virtual + * host. The host-name attribute indicates the name of the new host. The host-aliases + * attribute is a comma separated list of the host alias names. The manager attribute is a boolean value + * indicating if the webapp manager will be installed in the newly created host (optional, false by default).
    • + *
    • /remove?name={host-name} - Remove a virtual host. The host-name attribute indicates the name + * of the host.
    • + *
    • /list - List the virtual hosts installed on the server. Each host will be listed with the following format + * host-name#host-aliases.
    • + *
    • /start?name={host-name} - Start the virtual host.
    • + *
    • /stop?name={host-name} - Stop the virtual host.
    • + *
    + *

    + * NOTE - Attempting to stop or remove the host containing this servlet itself will not succeed. Therefore, this + * servlet should generally be deployed in a separate virtual host. + *

    + * The following servlet initialization parameters are recognized: + *

      + *
    • debug - The debugging detail level that controls the amount of information that is logged by this servlet. + * Default is zero. + *
    + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class HostManagerServlet extends HttpServlet implements ContainerServlet { + + private static final long serialVersionUID = 1L; + + // ----------------------------------------------------- Instance Variables + + + /** + * The Context container associated with our web application. + */ + protected transient Context context = null; + + + /** + * The debugging detail level for this servlet. + */ + protected int debug = 1; + + + /** + * The associated host. + */ + protected transient Host installedHost = null; + + + /** + * The associated engine. + */ + protected transient Engine engine = null; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + + /** + * The Wrapper container associated with this servlet. + */ + protected transient Wrapper wrapper = null; + + + // ----------------------------------------------- ContainerServlet Methods + + + /** + * Return the Wrapper with which we are associated. + */ + @Override + public Wrapper getWrapper() { + return this.wrapper; + } + + + /** + * Set the Wrapper with which we are associated. + * + * @param wrapper The new wrapper + */ + @Override + public void setWrapper(Wrapper wrapper) { + + this.wrapper = wrapper; + if (wrapper == null) { + context = null; + installedHost = null; + engine = null; + } else { + context = (Context) wrapper.getParent(); + installedHost = (Host) context.getParent(); + engine = (Engine) installedHost.getParent(); + } + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Finalize this servlet. + */ + @Override + public void destroy() { + + // No actions necessary + + } + + + /** + * Process a GET request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + + // Identify the request parameters that we need + String command = request.getPathInfo(); + if (command == null) { + command = request.getServletPath(); + } + String name = request.getParameter("name"); + + // Prepare our output writer to generate the response message + response.setContentType("text/plain; charset=" + Constants.CHARSET); + // Stop older versions of IE thinking they know best. We set text/plain + // in the line above for a reason. IE's behaviour is unwanted at best + // and dangerous at worst. + response.setHeader("X-Content-Type-Options", "nosniff"); + PrintWriter writer = response.getWriter(); + + // Process the requested command + if (command == null) { + writer.println(smClient.getString("hostManagerServlet.noCommand")); + } else if (command.equals("/add")) { + add(request, writer, name, false, smClient); + } else if (command.equals("/remove")) { + remove(writer, name, smClient); + } else if (command.equals("/list")) { + list(writer, smClient); + } else if (command.equals("/start")) { + start(writer, name, smClient); + } else if (command.equals("/stop")) { + stop(writer, name, smClient); + } else if (command.equals("/persist")) { + persist(writer, smClient); + } else { + writer.println(smClient.getString("hostManagerServlet.unknownCommand", command)); + } + + // Finish up the response + writer.flush(); + writer.close(); + + } + + /** + * Add host with the given parameters. + * + * @param request The request + * @param writer The output writer + * @param name The host name + * @param htmlMode Flag value + * @param smClient StringManager for the client's locale + */ + protected void add(HttpServletRequest request, PrintWriter writer, String name, boolean htmlMode, + StringManager smClient) { + String aliases = request.getParameter("aliases"); + String appBase = request.getParameter("appBase"); + boolean manager = booleanParameter(request, "manager", false, htmlMode); + boolean autoDeploy = booleanParameter(request, "autoDeploy", true, htmlMode); + boolean deployOnStartup = booleanParameter(request, "deployOnStartup", true, htmlMode); + boolean deployXML = booleanParameter(request, "deployXML", true, htmlMode); + boolean unpackWARs = booleanParameter(request, "unpackWARs", true, htmlMode); + boolean copyXML = booleanParameter(request, "copyXML", false, htmlMode); + add(writer, name, aliases, appBase, manager, autoDeploy, deployOnStartup, deployXML, unpackWARs, copyXML, + smClient); + } + + + /** + * Extract boolean value from checkbox with default. + * + * @param request The Servlet request + * @param parameter The parameter name + * @param theDefault Default value + * @param htmlMode Flag value + * + * @return the boolean value for the parameter + */ + protected boolean booleanParameter(HttpServletRequest request, String parameter, boolean theDefault, + boolean htmlMode) { + String value = request.getParameter(parameter); + boolean booleanValue = theDefault; + if (value != null) { + if (htmlMode) { + if (value.equals("on")) { + booleanValue = true; + } + } else if (theDefault) { + if (value.equals("false")) { + booleanValue = false; + } + } else if (value.equals("true")) { + booleanValue = true; + } + } else if (htmlMode) { + booleanValue = false; + } + return booleanValue; + } + + + @Override + public void init() throws ServletException { + + // Ensure that our ContainerServlet properties have been set + if (wrapper == null || context == null) { + throw new UnavailableException(sm.getString("hostManagerServlet.noWrapper")); + } + + // Set our properties from the initialization parameters + String value = null; + try { + value = getServletConfig().getInitParameter("debug"); + debug = Integer.parseInt(value); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Add a host using the specified parameters. + * + * @param writer Writer to render results to + * @param name host name + * @param aliases comma separated alias list + * @param appBase application base for the host + * @param manager should the manager webapp be deployed to the new host ? + * @param autoDeploy Flag value + * @param deployOnStartup Flag value + * @param deployXML Flag value + * @param unpackWARs Flag value + * @param copyXML Flag value + * @param smClient StringManager for the client's locale + */ + protected synchronized void add(PrintWriter writer, String name, String aliases, String appBase, boolean manager, + boolean autoDeploy, boolean deployOnStartup, boolean deployXML, boolean unpackWARs, boolean copyXML, + StringManager smClient) { + if (debug >= 1) { + log(sm.getString("hostManagerServlet.add", name)); + } + + // Validate the requested host name + if (name == null || name.length() == 0) { + writer.println(smClient.getString("hostManagerServlet.invalidHostName", name)); + return; + } + + // Check if host already exists + if (engine.findChild(name) != null) { + writer.println(smClient.getString("hostManagerServlet.alreadyHost", name)); + return; + } + + // Validate and create appBase + File appBaseFile = null; + File file = null; + String applicationBase = appBase; + if (applicationBase == null || applicationBase.length() == 0) { + applicationBase = name; + } + file = new File(applicationBase); + if (!file.isAbsolute()) { + file = new File(engine.getCatalinaBase(), file.getPath()); + } + try { + appBaseFile = file.getCanonicalFile(); + } catch (IOException e) { + appBaseFile = file; + } + if (!appBaseFile.mkdirs() && !appBaseFile.isDirectory()) { + writer.println(smClient.getString("hostManagerServlet.appBaseCreateFail", appBaseFile.toString(), name)); + return; + } + + // Create base for config files + File configBaseFile = getConfigBase(name); + + // Copy manager.xml if requested + if (manager) { + if (configBaseFile == null) { + writer.println(smClient.getString("hostManagerServlet.configBaseCreateFail", name)); + return; + } + try (InputStream is = getServletContext().getResourceAsStream("/WEB-INF/manager.xml")) { + if (is == null) { + writer.println(smClient.getString("hostManagerServlet.managerXml")); + return; + } + Path dest = new File(configBaseFile, "manager.xml").toPath(); + Files.copy(is, dest); + } catch (IOException e) { + writer.println(smClient.getString("hostManagerServlet.managerXml")); + return; + } + } + + StandardHost host = new StandardHost(); + host.setAppBase(applicationBase); + host.setName(name); + + host.addLifecycleListener(new HostConfig()); + + // Add host aliases + if (aliases != null && !aliases.isEmpty()) { + StringTokenizer tok = new StringTokenizer(aliases, ", "); + while (tok.hasMoreTokens()) { + host.addAlias(tok.nextToken()); + } + } + host.setAutoDeploy(autoDeploy); + host.setDeployOnStartup(deployOnStartup); + host.setDeployXML(deployXML); + host.setUnpackWARs(unpackWARs); + host.setCopyXML(copyXML); + + // Add new host + try { + engine.addChild(host); + } catch (Exception e) { + writer.println(smClient.getString("hostManagerServlet.exception", e.toString())); + return; + } + + host = (StandardHost) engine.findChild(name); + if (host != null) { + writer.println(smClient.getString("hostManagerServlet.addSuccess", name)); + } else { + // Something failed + writer.println(smClient.getString("hostManagerServlet.addFailed", name)); + } + + } + + + /** + * Remove the specified host. + * + * @param writer Writer to render results to + * @param name host name + * @param smClient StringManager for the client's locale + */ + protected synchronized void remove(PrintWriter writer, String name, StringManager smClient) { + + if (debug >= 1) { + log(sm.getString("hostManagerServlet.remove", name)); + } + + // Validate the requested host name + if (name == null || name.length() == 0) { + writer.println(smClient.getString("hostManagerServlet.invalidHostName", name)); + return; + } + + // Check if host exists + if (engine.findChild(name) == null) { + writer.println(smClient.getString("hostManagerServlet.noHost", name)); + return; + } + + // Prevent removing our own host + if (engine.findChild(name) == installedHost) { + writer.println(smClient.getString("hostManagerServlet.cannotRemoveOwnHost", name)); + return; + } + + // Remove host + // Note that the host will not get physically removed + try { + Container child = engine.findChild(name); + engine.removeChild(child); + if (child instanceof ContainerBase) { + child.destroy(); + } + } catch (Exception e) { + writer.println(smClient.getString("hostManagerServlet.exception", e.toString())); + return; + } + + Host host = (StandardHost) engine.findChild(name); + if (host == null) { + writer.println(smClient.getString("hostManagerServlet.removeSuccess", name)); + } else { + // Something failed + writer.println(smClient.getString("hostManagerServlet.removeFailed", name)); + } + + } + + + /** + * Render a list of the currently active Contexts in our virtual host. + * + * @param writer Writer to render to + * @param smClient StringManager for the client's locale + */ + protected void list(PrintWriter writer, StringManager smClient) { + + if (debug >= 1) { + log(sm.getString("hostManagerServlet.list", engine.getName())); + } + + writer.println(smClient.getString("hostManagerServlet.listed", engine.getName())); + Container[] hosts = engine.findChildren(); + for (Container container : hosts) { + Host host = (Host) container; + String name = host.getName(); + String[] aliases = host.findAliases(); + writer.println(String.format("[%s]:[%s]", name, StringUtils.join(aliases))); + } + } + + + /** + * Start the host with the specified name. + * + * @param writer Writer to render to + * @param name Host name + * @param smClient StringManager for the client's locale + */ + protected void start(PrintWriter writer, String name, StringManager smClient) { + + if (debug >= 1) { + log(sm.getString("hostManagerServlet.start", name)); + } + + // Validate the requested host name + if (name == null || name.length() == 0) { + writer.println(smClient.getString("hostManagerServlet.invalidHostName", name)); + return; + } + + Container host = engine.findChild(name); + + // Check if host exists + if (host == null) { + writer.println(smClient.getString("hostManagerServlet.noHost", name)); + return; + } + + // Prevent starting our own host + if (host == installedHost) { + writer.println(smClient.getString("hostManagerServlet.cannotStartOwnHost", name)); + return; + } + + // Don't start host if already started + if (host.getState().isAvailable()) { + writer.println(smClient.getString("hostManagerServlet.alreadyStarted", name)); + return; + } + + // Start host + try { + host.start(); + writer.println(smClient.getString("hostManagerServlet.started", name)); + } catch (Exception e) { + getServletContext().log(sm.getString("hostManagerServlet.startFailed", name), e); + writer.println(smClient.getString("hostManagerServlet.startFailed", name)); + writer.println(smClient.getString("hostManagerServlet.exception", e.toString())); + } + } + + + /** + * Stop the host with the specified name. + * + * @param writer Writer to render to + * @param name Host name + * @param smClient StringManager for the client's locale + */ + protected void stop(PrintWriter writer, String name, StringManager smClient) { + + if (debug >= 1) { + log(sm.getString("hostManagerServlet.stop", name)); + } + + // Validate the requested host name + if (name == null || name.length() == 0) { + writer.println(smClient.getString("hostManagerServlet.invalidHostName", name)); + return; + } + + Container host = engine.findChild(name); + + // Check if host exists + if (host == null) { + writer.println(smClient.getString("hostManagerServlet.noHost", name)); + return; + } + + // Prevent stopping our own host + if (host == installedHost) { + writer.println(smClient.getString("hostManagerServlet.cannotStopOwnHost", name)); + return; + } + + // Don't stop host if already stopped + if (!host.getState().isAvailable()) { + writer.println(smClient.getString("hostManagerServlet.alreadyStopped", name)); + return; + } + + // Stop host + try { + host.stop(); + writer.println(smClient.getString("hostManagerServlet.stopped", name)); + } catch (Exception e) { + getServletContext().log(sm.getString("hostManagerServlet.stopFailed", name), e); + writer.println(smClient.getString("hostManagerServlet.stopFailed", name)); + writer.println(smClient.getString("hostManagerServlet.exception", e.toString())); + } + } + + + /** + * Persist the current configuration to server.xml. + * + * @param writer Writer to render to + * @param smClient i18n resources localized for the client + */ + protected void persist(PrintWriter writer, StringManager smClient) { + + if (debug >= 1) { + log(sm.getString("hostManagerServlet.persist")); + } + + try { + MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer(); + ObjectName oname = new ObjectName(engine.getDomain() + ":type=StoreConfig"); + platformMBeanServer.invoke(oname, "storeConfig", null, null); + writer.println(smClient.getString("hostManagerServlet.persisted")); + } catch (Exception e) { + getServletContext().log(sm.getString("hostManagerServlet.persistFailed"), e); + writer.println(smClient.getString("hostManagerServlet.persistFailed")); + // catch InstanceNotFoundException when StoreConfig is not enabled instead of printing + // the failure message + if (e instanceof InstanceNotFoundException) { + writer.println(smClient.getString("hostManagerServlet.noStoreConfig")); + } else { + writer.println(smClient.getString("hostManagerServlet.exception", e.toString())); + } + } + } + + + // -------------------------------------------------------- Support Methods + + /** + * Get config base. + * + * @param hostName The host name + * + * @return the config base for the host + */ + protected File getConfigBase(String hostName) { + File configBase = new File(context.getCatalinaBase(), "conf"); + if (!configBase.exists()) { + return null; + } + if (engine != null) { + configBase = new File(configBase, engine.getName()); + } + if (installedHost != null) { + configBase = new File(configBase, hostName); + } + if (!configBase.mkdirs() && !configBase.isDirectory()) { + return null; + } + return configBase; + } +} diff --git a/java/org/apache/catalina/manager/host/LocalStrings.properties b/java/org/apache/catalina/manager/host/LocalStrings.properties new file mode 100644 index 0000000..8be8a00 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings.properties @@ -0,0 +1,90 @@ +# 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. + +hostManagerServlet.add=add: Adding host [{0}] +hostManagerServlet.addFailed=FAIL - Failed to add host [{0}] +hostManagerServlet.addSuccess=OK - Host [{0}] added +hostManagerServlet.alreadyHost=FAIL - Host already exists with host name [{0}] +hostManagerServlet.alreadyStarted=FAIL - Host [{0}] is already started +hostManagerServlet.alreadyStopped=FAIL - Host [{0}] is already stopped +hostManagerServlet.appBaseCreateFail=FAIL - Failed to create appBase directory [{0}] for host [{1}] +hostManagerServlet.cannotRemoveOwnHost=FAIL - Cannot remove your own host [{0}] +hostManagerServlet.cannotStartOwnHost=FAIL - Cannot start your own host [{0}] +hostManagerServlet.cannotStopOwnHost=FAIL - Cannot stop your own host [{0}] +hostManagerServlet.configBaseCreateFail=FAIL - Failed to identify configBase directory for host [{0}] +hostManagerServlet.exception=FAIL - Encountered exception [{0}] +hostManagerServlet.invalidHostName=FAIL - Invalid host name [{0}] was specified +hostManagerServlet.list=list: Listing hosts for engine [{0}] +hostManagerServlet.listed=OK - Listed hosts +hostManagerServlet.managerXml=FAIL - Couldn't install manager.xml +hostManagerServlet.noCommand=FAIL - No command was specified +hostManagerServlet.noHost=FAIL - Host name [{0}] does not exist +hostManagerServlet.noStoreConfig=FAIL - Please enable StoreConfig to use this feature +hostManagerServlet.noWrapper=Container has not called setWrapper() for this servlet +hostManagerServlet.persist=persist: Persisting current configuration +hostManagerServlet.persistFailed=FAIL - Failed to persist configuration +hostManagerServlet.persisted=OK - Configuration persisted +hostManagerServlet.postCommand=FAIL - Tried to use command [{0}] via a GET request but POST is required +hostManagerServlet.remove=remove: Removing host [{0}] +hostManagerServlet.removeFailed=FAIL - Failed to remove host [{0}] +hostManagerServlet.removeSuccess=OK - Removed host [{0}] +hostManagerServlet.start=start: Starting host with name [{0}] +hostManagerServlet.startFailed=FAIL - Failed to start host [{0}] +hostManagerServlet.started=OK - Host [{0}] started +hostManagerServlet.stop=stop: Stopping host with name [{0}] +hostManagerServlet.stopFailed=FAIL - Failed to stop host [{0}] +hostManagerServlet.stopped=OK - Host [{0}] stopped +hostManagerServlet.unknownCommand=FAIL - Unknown command [{0}] + +htmlHostManagerServlet.addAliases=Aliases: +htmlHostManagerServlet.addAppBase=App base: +htmlHostManagerServlet.addAutoDeploy=AutoDeploy +htmlHostManagerServlet.addButton=Add +htmlHostManagerServlet.addCopyXML=CopyXML +htmlHostManagerServlet.addDeployOnStartup=DeployOnStartup +htmlHostManagerServlet.addDeployXML=DeployXML +htmlHostManagerServlet.addHost=Host +htmlHostManagerServlet.addManager=Manager App +htmlHostManagerServlet.addName=Name: +htmlHostManagerServlet.addTitle=Add Virtual Host +htmlHostManagerServlet.addUnpackWARs=UnpackWARs +htmlHostManagerServlet.helpHtmlManager=HTML Host Manager Help +htmlHostManagerServlet.helpHtmlManagerFile=../docs/html-host-manager-howto.html +htmlHostManagerServlet.helpManager=Host Manager Help +htmlHostManagerServlet.helpManagerFile=../docs/host-manager-howto.html +htmlHostManagerServlet.hostAliases=Host aliases +htmlHostManagerServlet.hostName=Host name +htmlHostManagerServlet.hostTasks=Commands +htmlHostManagerServlet.hostThis=Host Manager installed - commands disabled +htmlHostManagerServlet.hostsRemove=Remove +htmlHostManagerServlet.hostsStart=Start +htmlHostManagerServlet.hostsStop=Stop +htmlHostManagerServlet.list=List Virtual Hosts +htmlHostManagerServlet.manager=Host Manager +htmlHostManagerServlet.messageLabel=Message: +htmlHostManagerServlet.persistAll=Save current configuration (including virtual hosts) to server.xml and per web application context.xml files +htmlHostManagerServlet.persistAllButton=All +htmlHostManagerServlet.persistTitle=Persist configuration +htmlHostManagerServlet.serverJVMVendor=JVM Vendor +htmlHostManagerServlet.serverJVMVersion=JVM Version +htmlHostManagerServlet.serverOSArch=OS Architecture +htmlHostManagerServlet.serverOSName=OS Name +htmlHostManagerServlet.serverOSVersion=OS Version +htmlHostManagerServlet.serverTitle=Server Information +htmlHostManagerServlet.serverVersion=Tomcat Version +htmlHostManagerServlet.title=Tomcat Virtual Host Manager + +statusServlet.complete=Complete Server Status +statusServlet.title=Server Status diff --git a/java/org/apache/catalina/manager/host/LocalStrings_cs.properties b/java/org/apache/catalina/manager/host/LocalStrings_cs.properties new file mode 100644 index 0000000..7f09913 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_cs.properties @@ -0,0 +1,29 @@ +# 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. + +hostManagerServlet.configBaseCreateFail=Selhání - Selahlo rozpoznání configBase adresáře pro host [{0}] +hostManagerServlet.listed=OK - Vypsané hosty +hostManagerServlet.managerXml=FAIL - Nelze nainstalovat manager.xml +hostManagerServlet.remove=odstranÄ›ní: Odstraňuji hosta [{0}] +hostManagerServlet.stopFailed=FAIL - Zastavení hostu [{0}] selhalo + +htmlHostManagerServlet.addHost=Host +htmlHostManagerServlet.addManager=Manažer aplikací +htmlHostManagerServlet.addTitle=Přídání virtuálního hosta +htmlHostManagerServlet.helpHtmlManagerFile=../docs/html-host-manager-howto.html +htmlHostManagerServlet.hostName=Host name +htmlHostManagerServlet.hostsRemove=Odstranit +htmlHostManagerServlet.manager=Host manager +htmlHostManagerServlet.serverOSArch=Architektura OS diff --git a/java/org/apache/catalina/manager/host/LocalStrings_de.properties b/java/org/apache/catalina/manager/host/LocalStrings_de.properties new file mode 100644 index 0000000..d9430a4 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_de.properties @@ -0,0 +1,40 @@ +# 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. + +hostManagerServlet.alreadyStarted=FEHLER - Host [{0}] wurde bereits gestartet +hostManagerServlet.listed=OK - Hosts aufgelistet +hostManagerServlet.managerXml=FAIL - Konnte manager.xml nicht installieren +hostManagerServlet.noCommand=Fehler - es wurde kein Kommando angegeben +hostManagerServlet.start=start: Starte Host mit Name [{0}] +hostManagerServlet.started=OK - Host [{0}] gestartet +hostManagerServlet.stopFailed=FEHLER - der Host [{0}] konnte nicht gestoppt werden + +htmlHostManagerServlet.addAutoDeploy=AutoDeploy +htmlHostManagerServlet.addDeployOnStartup=DeployOnStartup +htmlHostManagerServlet.addHost=Host +htmlHostManagerServlet.addManager=Manager-Anwendung +htmlHostManagerServlet.addTitle=Füge virtuellen Host hinzu +htmlHostManagerServlet.helpHtmlManager=HTML Host-Manager-Hilfe +htmlHostManagerServlet.helpHtmlManagerFile=../docs/html-host-manager-howto.html +htmlHostManagerServlet.helpManager=Host-Manager-Hilfe +htmlHostManagerServlet.hostName=Hostname +htmlHostManagerServlet.hostTasks=Kommandos +htmlHostManagerServlet.hostsRemove=Entferne +htmlHostManagerServlet.hostsStart=Start +htmlHostManagerServlet.hostsStop=Stopp +htmlHostManagerServlet.manager=Host-Manager +htmlHostManagerServlet.messageLabel=Nachricht: +htmlHostManagerServlet.persistTitle=Speichere Konfiguration +htmlHostManagerServlet.serverOSArch=Betriebssystemarchitektur diff --git a/java/org/apache/catalina/manager/host/LocalStrings_es.properties b/java/org/apache/catalina/manager/host/LocalStrings_es.properties new file mode 100644 index 0000000..732a0f4 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_es.properties @@ -0,0 +1,83 @@ +# 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. + +hostManagerServlet.add=añadir: Añadiendo máquina [{0}] +hostManagerServlet.addFailed=FALLO - No pude añadir máquina [{0}] +hostManagerServlet.addSuccess=OK - Máquina añadida [{0}] +hostManagerServlet.alreadyHost=FALLO - Ya existe máquina con nombre de máquina [{0}] +hostManagerServlet.alreadyStarted=FALLO - La máqiuina [{0}] ya ha arrancado +hostManagerServlet.alreadyStopped=FALLO - La máquina [{0}] ya se ha parado +hostManagerServlet.appBaseCreateFail=FALLO - No pude crear appBase [{0}] para la máquina [{1}] +hostManagerServlet.cannotRemoveOwnHost=FALLO - No puedo quitar máquina propia [{0}] +hostManagerServlet.cannotStartOwnHost=FALLO - No puedo empezar máquina propia [{0}] +hostManagerServlet.cannotStopOwnHost=FALLO - No puedo para máquina propia [{0}] +hostManagerServlet.configBaseCreateFail=FALLO - No pude identificar configBase para la máquina [{0}] +hostManagerServlet.exception=FALLO - Encontrada excepción [{0}] +hostManagerServlet.invalidHostName=FALLO - Se ha especificado un nombre inválido de máquina [{0}] +hostManagerServlet.list=listar: Listando máquinas para motor [{0}] +hostManagerServlet.listed=OK - Máquinas listadas +hostManagerServlet.managerXml=FALLO - no pude instalar manager.xml +hostManagerServlet.noCommand=FALLO - No se ha especificado comando +hostManagerServlet.noHost=FALLO - El nombre de máquina [{0}] no existe +hostManagerServlet.noWrapper=El contenedor no ha llamado a setWrapper() para este servlet +hostManagerServlet.postCommand=FALLO - Intenté usar el comando [{0}] vía un requerimiento GET pero es necesario POST +hostManagerServlet.remove=quitar: Quitando máquina [{0}] +hostManagerServlet.removeFailed=FALLO - No pude quitar máquina [{0}] +hostManagerServlet.removeSuccess=OK - Máquina removida [{0}] +hostManagerServlet.start=arrancar: Arrancando máquina con nombre [{0}] +hostManagerServlet.startFailed=FALLO - No pude arrancar máquina [{0}] +hostManagerServlet.started=OK - Máquina [{0}] arrancada +hostManagerServlet.stop=parar: Parando máquina con nombre [{0}] +hostManagerServlet.stopFailed=FALLO - No pude parar máquina [{0}] +hostManagerServlet.stopped=OK - Máquina [{0}] parada +hostManagerServlet.unknownCommand=FALLO - Comando desconocido [{0}] + +htmlHostManagerServlet.addAliases=Aliases: +htmlHostManagerServlet.addAppBase=App base: +htmlHostManagerServlet.addAutoDeploy=AutoDeploy +htmlHostManagerServlet.addButton=Añadir +htmlHostManagerServlet.addDeployOnStartup=DeployOnStartup +htmlHostManagerServlet.addDeployXML=DeployXML +htmlHostManagerServlet.addHost=Maquina +htmlHostManagerServlet.addManager=App de Gestor +htmlHostManagerServlet.addName=Nombre: +htmlHostManagerServlet.addTitle=Añadir Máquina Virtual +htmlHostManagerServlet.addUnpackWARs=UnpackWARs +htmlHostManagerServlet.helpHtmlManager=Ayuda de Gestor de Máquina HTML (¡En breve!) +htmlHostManagerServlet.helpHtmlManagerFile=html-host-manager-howto.html +htmlHostManagerServlet.helpManager=Ayuda de Gestor de Máquina +htmlHostManagerServlet.helpManagerFile=../docs/host-manager-howto.html +htmlHostManagerServlet.hostAliases=Aliases de Máquina +htmlHostManagerServlet.hostName=Nombre de Máquina +htmlHostManagerServlet.hostTasks=Comandos +htmlHostManagerServlet.hostThis=Instalado Gestor de Máquinas - comandos deactivados +htmlHostManagerServlet.hostsRemove=Quitar +htmlHostManagerServlet.hostsStart=Iniciar +htmlHostManagerServlet.hostsStop=Parar +htmlHostManagerServlet.list=Lista de Máquinas Virtuales +htmlHostManagerServlet.manager=Gestor de Máquina +htmlHostManagerServlet.messageLabel=Mensaje: +htmlHostManagerServlet.persistTitle=Configuración peristente +htmlHostManagerServlet.serverJVMVendor=Vendedor JVM +htmlHostManagerServlet.serverJVMVersion=Versión de JVM +htmlHostManagerServlet.serverOSArch=Arquitectura del SO +htmlHostManagerServlet.serverOSName=Nombre de SO +htmlHostManagerServlet.serverOSVersion=Versión de SO +htmlHostManagerServlet.serverTitle=Información de Servidor +htmlHostManagerServlet.serverVersion=Versión de Tomcat +htmlHostManagerServlet.title=Gestor de Máquina Virtual de Tomcat + +statusServlet.complete=Completar Estado de Servidor +statusServlet.title=Estado de Servidor diff --git a/java/org/apache/catalina/manager/host/LocalStrings_fr.properties b/java/org/apache/catalina/manager/host/LocalStrings_fr.properties new file mode 100644 index 0000000..84b5bd4 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_fr.properties @@ -0,0 +1,90 @@ +# 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. + +hostManagerServlet.add=add : Ajouter l''hôte [{0}] +hostManagerServlet.addFailed=ECHEC - L''hôte [{0}] n''a pas pu être ajouté +hostManagerServlet.addSuccess=OK - L''hôte [{0}] a été ajouté +hostManagerServlet.alreadyHost=ECHEC - Un hôte existe déjà pour le nom [{0}] +hostManagerServlet.alreadyStarted=ECHEC - L''hôte [{0}] est déjà démarré +hostManagerServlet.alreadyStopped=ECHEC - L''hôte [{0}] est déjà arrêté +hostManagerServlet.appBaseCreateFail=ECHEC - Echec de création du répertoire de base (appBase) [{0}] de l''hôte [{1}] +hostManagerServlet.cannotRemoveOwnHost=ECHEC - Impossible de retirer son propre hôte +hostManagerServlet.cannotStartOwnHost=ECHEC - Ne peut pas démarrer son propre hôte +hostManagerServlet.cannotStopOwnHost=ECHEC - Impossible d'arrêter son propre hôte +hostManagerServlet.configBaseCreateFail=ECHEC - N''a pas pu trouver le configBase de l''hôte [{0}] +hostManagerServlet.exception=ECHEC - Exception [{0}] +hostManagerServlet.invalidHostName=ECHEC - Un nom d''hôte [{0}] invalide a été spécifié +hostManagerServlet.list=list : Liste des hôte pour le moteur [{0}] +hostManagerServlet.listed=OK - Liste des hôtes +hostManagerServlet.managerXml=ECHEC - "manager.xml" n'a pas pu être installé +hostManagerServlet.noCommand=ECHEC - Aucune commande n'a été spécifiée +hostManagerServlet.noHost=ECHEC - Le nom d''hôte [{0}] n''existe pas +hostManagerServlet.noStoreConfig=FAIL - StoreConfig doit être activé pour pouvoir utiliser cette fonctionnalité +hostManagerServlet.noWrapper=Le conteneur n'a pas appelé setWrapper() pour ce Servlet +hostManagerServlet.persist=persist : Persister la configuration actuelle +hostManagerServlet.persistFailed=ECHEC - N'a pas pu persister la configuration +hostManagerServlet.persisted=OK - Configuration persistée +hostManagerServlet.postCommand=ECHEC - Essai d''utilisation de GET pour la commande [{0}] mais POST est nécessaire +hostManagerServlet.remove=supprimer : suppression de l''hôte [{0}] +hostManagerServlet.removeFailed=ECHEC - N''a pas pu retirer l''hôte [{0}] +hostManagerServlet.removeSuccess=OK - L''hôte [{0}] a été enlevé +hostManagerServlet.start=start : Démarrage de l''hôte [{0}] +hostManagerServlet.startFailed=ECHEC - Impossible de démarrer l''hôte [{0}] +hostManagerServlet.started=OK - L''hôte [{0}] est démarré +hostManagerServlet.stop=stop : Arrêt de l''hôte [{0}] +hostManagerServlet.stopFailed=ECHEC - L''arrêt de l''hôte [{0}] a échoué +hostManagerServlet.stopped=OK - L''hôte [{0}] est arrêté +hostManagerServlet.unknownCommand=ECHEC - Commande inconnue [{0}] + +htmlHostManagerServlet.addAliases=Alias : +htmlHostManagerServlet.addAppBase=Répertoire de base : +htmlHostManagerServlet.addAutoDeploy=Déploiement automatique +htmlHostManagerServlet.addButton=Ajouter +htmlHostManagerServlet.addCopyXML=Copier le XML +htmlHostManagerServlet.addDeployOnStartup=Déploiement au démarrage +htmlHostManagerServlet.addDeployXML=Déployer le XML +htmlHostManagerServlet.addHost=Hôte +htmlHostManagerServlet.addManager=App gestionnaire +htmlHostManagerServlet.addName=Nom : +htmlHostManagerServlet.addTitle=Ajouter un hôte virtuel (Virtual Host) +htmlHostManagerServlet.addUnpackWARs=Décompresser les WARs +htmlHostManagerServlet.helpHtmlManager=Aide HTML du manager d'hôte +htmlHostManagerServlet.helpHtmlManagerFile=../docs/html-host-manager-howto.html +htmlHostManagerServlet.helpManager=Aide du Gestionnaire d'Hôtes +htmlHostManagerServlet.helpManagerFile=../docs/host-manager-howto.html +htmlHostManagerServlet.hostAliases=Alias de l'hôte +htmlHostManagerServlet.hostName=Nom d'hôte +htmlHostManagerServlet.hostTasks=Commandes +htmlHostManagerServlet.hostThis=Gestionnaire d'Hôtes installé, commandes désactivées +htmlHostManagerServlet.hostsRemove=Retirer +htmlHostManagerServlet.hostsStart=Démarrer +htmlHostManagerServlet.hostsStop=Arrêt +htmlHostManagerServlet.list=Liste des Hôtes Virtuels +htmlHostManagerServlet.manager=Gestionaire d'hôte ("Host Manager") +htmlHostManagerServlet.messageLabel=Message : +htmlHostManagerServlet.persistAll=Enregistrer la configuration, y compris les hôtes virtuels, dans server.xml et les fichiers context.xml pour chaque application +htmlHostManagerServlet.persistAllButton=Tout +htmlHostManagerServlet.persistTitle=Persister la configuration +htmlHostManagerServlet.serverJVMVendor=Fournisseur de la JVM +htmlHostManagerServlet.serverJVMVersion=Version de la JVM +htmlHostManagerServlet.serverOSArch=Architecture du système +htmlHostManagerServlet.serverOSName=Nom de l''OS +htmlHostManagerServlet.serverOSVersion=Version de l'OS +htmlHostManagerServlet.serverTitle=Information sur le serveur +htmlHostManagerServlet.serverVersion=Version de Tomcat +htmlHostManagerServlet.title=Gestionnaire d'Hôtes Virtuels de Tomcat + +statusServlet.complete=Etat complet du serveur +statusServlet.title=Etat du serveur diff --git a/java/org/apache/catalina/manager/host/LocalStrings_ja.properties b/java/org/apache/catalina/manager/host/LocalStrings_ja.properties new file mode 100644 index 0000000..2d4dc30 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_ja.properties @@ -0,0 +1,90 @@ +# 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. + +hostManagerServlet.add=add: ホスト [{0}] を追加 +hostManagerServlet.addFailed=FAIL - ホスト [{0}] を追加ã§ãã¾ã›ã‚“。 +hostManagerServlet.addSuccess=OK - ホスト [{0}] を追加ã—ã¾ã—㟠+hostManagerServlet.alreadyHost=FAIL - ホストå[{0}]ã®ãƒ›ã‚¹ãƒˆãŒæ—¢ã«å­˜åœ¨ã—ã¾ã™ +hostManagerServlet.alreadyStarted=FAIL - ホスト [{0}] ã¯ã™ã§ã«é–‹å§‹ã—ã¦ã„ã¾ã™ã€‚ +hostManagerServlet.alreadyStopped=FAIL - Host [{0}] ã¯ã™ã§ã«åœæ­¢ã—ã¦ã„ã¾ã™ã€‚ +hostManagerServlet.appBaseCreateFail=FAIL - ホスト [{1}] ã® appBase [{0}] を作æˆã§ãã¾ã›ã‚“。 +hostManagerServlet.cannotRemoveOwnHost=FAIL - 自身ã®ãƒ›ã‚¹ãƒˆ [{0}] を削除ã§ãã¾ã›ã‚“ +hostManagerServlet.cannotStartOwnHost=FAIL - 自身ã®ãƒ›ã‚¹ãƒˆ [{0}] ã¯é–‹å§‹ã§ãã¾ã›ã‚“。 +hostManagerServlet.cannotStopOwnHost=FAIL - 自分自身ã®ãƒ›ã‚¹ãƒˆ [{0}] ã‚’åœæ­¢ã§ãã¾ã›ã‚“。 +hostManagerServlet.configBaseCreateFail=FAIL - ホスト [{0}] ã® configBase ãŒç‰¹å®šã§ãã¾ã›ã‚“。 +hostManagerServlet.exception=FAIL - 例外[{0}]ãŒç™ºç”Ÿã—ã¾ã—㟠+hostManagerServlet.invalidHostName=FAIL - 無効ãªãƒ›ã‚¹ãƒˆå[{0}]ãŒæŒ‡å®šã•ã‚Œã¾ã—㟠+hostManagerServlet.list=リスト:Engine[{0}]ã®Hostã®ãƒªã‚¹ãƒˆ +hostManagerServlet.listed=OK - 列挙ã•ã‚ŒãŸHost +hostManagerServlet.managerXml=FAIL - manager.xml をインストールã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +hostManagerServlet.noCommand=FAIL - コマンドãŒæŒ‡å®šã•ã‚Œã¾ã›ã‚“ã§ã—ãŸã€‚ +hostManagerServlet.noHost=FAIL - ホストå [{0}] ã¯å­˜åœ¨ã—ã¾ã›ã‚“。 +hostManagerServlet.noStoreConfig=失敗 - ã“ã®æ©Ÿèƒ½ã‚’使用ã™ã‚‹ãŸã‚ã« StoreConfig を有効ã«ã—ã¦ãã ã•ã„ +hostManagerServlet.noWrapper=Container ã¯ã“ã®ã‚µãƒ¼ãƒ–レット㮠setWrapper() を呼ã³å‡ºã—ã¦ã„ã¾ã›ã‚“。 +hostManagerServlet.persist=永続化:ç¾åœ¨ã®è¨­å®šã‚’ä¿æŒã™ã‚‹ +hostManagerServlet.persistFailed=FAIL - 構æˆã‚’永続化ã§ãã¾ã›ã‚“ã§ã—㟠+hostManagerServlet.persisted=OK - 構æˆãŒæ°¸ç¶šåŒ–ã•ã‚Œã¾ã—㟠+hostManagerServlet.postCommand=FAIL - コマンド [{0}] ã‚’GETリクエストã§ä½¿ç”¨ã—よã†ã¨ã—ã¾ã—ãŸãŒã€POSTãŒå¿…è¦ã§ã™ +hostManagerServlet.remove=remove: ホスト [{0}] を削除ã—ã¾ã™ã€‚ +hostManagerServlet.removeFailed=FAIL - Host [{0}] を削除ã§ãã¾ã›ã‚“。 +hostManagerServlet.removeSuccess=OK - ホスト [{0}] を削除ã—ã¾ã—㟠+hostManagerServlet.start=開始:åå‰[{0}]ã®ãƒ›ã‚¹ãƒˆã‚’èµ·å‹•ã—ã¦ã„ã¾ã™ +hostManagerServlet.startFailed=FAIL - ホスト [{0}] ã®èµ·å‹•ã«å¤±æ•—ã—ã¾ã—㟠+hostManagerServlet.started=OK - ホスト [{0}] を開始ã—ã¾ã—㟠+hostManagerServlet.stop=åœæ­¢ï¼šåå‰[{0}]ã®ãƒ›ã‚¹ãƒˆã‚’åœæ­¢ã—ã¦ã„ã¾ã™ +hostManagerServlet.stopFailed=FAIL- ホスト [{0}] ã‚’åœæ­¢ã§ãã¾ã›ã‚“。 +hostManagerServlet.stopped=OK - ホスト [{0}] ãŒåœæ­¢ã—ã¾ã—㟠+hostManagerServlet.unknownCommand=FAIL - ä¸æ˜Žãªã‚³ãƒžãƒ³ãƒ‰ [{0}] + +htmlHostManagerServlet.addAliases=エイリアス: +htmlHostManagerServlet.addAppBase=App base: +htmlHostManagerServlet.addAutoDeploy=自動é…å‚™ +htmlHostManagerServlet.addButton=追加 +htmlHostManagerServlet.addCopyXML=CopyXML +htmlHostManagerServlet.addDeployOnStartup=DeployOnStartup +htmlHostManagerServlet.addDeployXML=DeployXML +htmlHostManagerServlet.addHost=ホスト +htmlHostManagerServlet.addManager=Manager アプリケーション +htmlHostManagerServlet.addName=åå‰ï¼š +htmlHostManagerServlet.addTitle=仮想ホスト追加 +htmlHostManagerServlet.addUnpackWARs=UnpackWARs +htmlHostManagerServlet.helpHtmlManager=HTMLホストマãƒãƒ¼ã‚¸ãƒ£ ヘルプ +htmlHostManagerServlet.helpHtmlManagerFile=../docs/html-host-manager-howto.html +htmlHostManagerServlet.helpManager=Host Manager ヘルプ +htmlHostManagerServlet.helpManagerFile=../docs/host-manager-howto.html +htmlHostManagerServlet.hostAliases=Hostエイリアス +htmlHostManagerServlet.hostName=ホストå +htmlHostManagerServlet.hostTasks=コマンド +htmlHostManagerServlet.hostThis=Host ManagerãŒã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã¾ã™ - コマンドãŒç„¡åŠ¹ã§ã™ +htmlHostManagerServlet.hostsRemove=削除 +htmlHostManagerServlet.hostsStart=èµ·å‹• +htmlHostManagerServlet.hostsStop=åœæ­¢ +htmlHostManagerServlet.list=仮想ホスト一覧 +htmlHostManagerServlet.manager=Host Manager +htmlHostManagerServlet.messageLabel=メッセージ: +htmlHostManagerServlet.persistAll=ç¾åœ¨ã®è¨­å®šï¼ˆä»®æƒ³ãƒ›ã‚¹ãƒˆã‚’å«ã‚€ï¼‰ã‚’server.xmlãŠã‚ˆã³webアプリケーションcontext.xmlファイルã«ä¿å­˜ã—ã¾ã™ã€‚ +htmlHostManagerServlet.persistAllButton=全㦠+htmlHostManagerServlet.persistTitle=構æˆç¶­æŒ +htmlHostManagerServlet.serverJVMVendor=JVMベンダ +htmlHostManagerServlet.serverJVMVersion=JVM ãƒãƒ¼ã‚¸ãƒ§ãƒ³ +htmlHostManagerServlet.serverOSArch=OS アーキテクãƒãƒ£ +htmlHostManagerServlet.serverOSName=OS å +htmlHostManagerServlet.serverOSVersion=OS ãƒãƒ¼ã‚¸ãƒ§ãƒ³ +htmlHostManagerServlet.serverTitle=Server情報 +htmlHostManagerServlet.serverVersion=Tomcatãƒãƒ¼ã‚¸ãƒ§ãƒ³ +htmlHostManagerServlet.title=Tomcat仮想ホストマãƒãƒ¼ã‚¸ãƒ£ + +statusServlet.complete=完全ãªã‚µãƒ¼ãƒã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ +statusServlet.title=サーãƒã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ diff --git a/java/org/apache/catalina/manager/host/LocalStrings_ko.properties b/java/org/apache/catalina/manager/host/LocalStrings_ko.properties new file mode 100644 index 0000000..8ce25c0 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_ko.properties @@ -0,0 +1,89 @@ +# 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. + +hostManagerServlet.add=추가: 호스트 [{0}]ì„(를) 추가합니다. +hostManagerServlet.addFailed=실패 - 호스트 [{0}]ì„(를) 추가하지 못했습니다. +hostManagerServlet.addSuccess=OK - 호스트 [{0}]ì´(ê°€) 추가ë˜ì—ˆìŠµë‹ˆë‹¤. +hostManagerServlet.alreadyHost=실패 - ì´ë¦„ì´ [{0}]ì¸ í˜¸ìŠ¤íŠ¸ê°€ ì´ë¯¸ 존재합니다. +hostManagerServlet.alreadyStarted=실패 - 호스트 [{0}]ì€(는) ì´ë¯¸ 시작ë˜ì—ˆìŠµë‹ˆë‹¤. +hostManagerServlet.alreadyStopped=실패 - 호스트 [{0}]ì€(는) ì´ë¯¸ 중지ë˜ì—ˆìŠµë‹ˆë‹¤. +hostManagerServlet.appBaseCreateFail=실패 - 호스트 [{1}]ì„(를) 위한 appBase [{0}]ì„(를) ìƒì„±í•˜ì§€ 못했습니다. +hostManagerServlet.cannotRemoveOwnHost=실패 - ìžì‹ ì˜ 호스트 [{0}]ì„(를) 제거할 수는 없습니다. +hostManagerServlet.cannotStartOwnHost=실패 - ìžê¸° ìžì‹ ì˜ 호스트 [{0}]ì„(를) 시작할 수 없습니다. +hostManagerServlet.cannotStopOwnHost=실패 - ìžì‹ ì˜ 호스트 [{0}]ì„(를) 중지시킬 수 없습니다. +hostManagerServlet.configBaseCreateFail=실패 - 호스트 [{0}]ì„(를) 위한 configBase를 ì‹ë³„하지 못했습니다. +hostManagerServlet.exception=실패 - 예외 ë°œìƒ [{0}] +hostManagerServlet.invalidHostName=실패 - 유효하지 ì•Šì€ í˜¸ìŠ¤íŠ¸ ì´ë¦„ [{0}]ì´(ê°€) 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +hostManagerServlet.list=목ë¡: 엔진 [{0}]ì„(를) 위한 í˜¸ìŠ¤íŠ¸ë“¤ì˜ ëª©ë¡ì„ 표시합니다. +hostManagerServlet.listed=OK - 호스트 ëª©ë¡ +hostManagerServlet.managerXml=실패 - manager.xmlì„ ì„¤ì¹˜í•  수 없었습니다. +hostManagerServlet.noCommand=실패 - ëª…ë ¹ì´ ì§€ì •ë˜ì§€ 않았습니다. +hostManagerServlet.noHost=실패 - 호스트 ì´ë¦„ [{0}]ì€(는) 존재하지 않습니다. +hostManagerServlet.noWrapper=ì´ ì„œë¸”ë¦¿ì„ ìœ„í•´, 컨테ì´ë„ˆê°€ setWrapper()를 호출한 ì ì´ 없습니다. +hostManagerServlet.persist=저장: 현재 ì„¤ì •ì„ ì €ìž¥í•©ë‹ˆë‹¤. +hostManagerServlet.persistFailed=실패 - ì„¤ì •ì„ ì €ìž¥í•˜ì§€ 못했습니다. +hostManagerServlet.persisted=OK - ì„¤ì •ì´ ì €ìž¥ë˜ì—ˆìŠµë‹ˆë‹¤. +hostManagerServlet.postCommand=실패 - GET ìš”ì²­ì„ í†µí•´ 명령 [{0}]ì„(를) 사용하려 ì‹œë„했지만, POST 메소드가 필수ì ìž…니다. +hostManagerServlet.remove=제거: 호스트 [{0}]ì„(를) 제거합니다. +hostManagerServlet.removeFailed=실패 - 호스트 [{0}]ì„(를) 제거하지 못했습니다. +hostManagerServlet.removeSuccess=OK - 호스트 [{0}]ì„(를) 제거했습니다. +hostManagerServlet.start=시작: ì´ë¦„ì´ [{0}]ì¸ í˜¸ìŠ¤íŠ¸ë¥¼ 시작합니다. +hostManagerServlet.startFailed=실패 - 호스트 [{0}]ì„(를) 시작하지 못했습니다. +hostManagerServlet.started=OK - 호스트 [{0}](ì´)ê°€ 시작ë˜ì—ˆìŠµë‹ˆë‹¤. +hostManagerServlet.stop=중지: [{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ í˜¸ìŠ¤íŠ¸ë¥¼ 중지합니다. +hostManagerServlet.stopFailed=실패 - 호스트 [{0}]ì„(를) 중지시키지 못했습니다. +hostManagerServlet.stopped=OK - 호스트 [{0}]ì´(ê°€) 중지ë˜ì—ˆìŠµë‹ˆë‹¤. +hostManagerServlet.unknownCommand=실패 - ì•Œ 수 없는 명령 [{0}] + +htmlHostManagerServlet.addAliases=별칭들: +htmlHostManagerServlet.addAppBase=앱 base: +htmlHostManagerServlet.addAutoDeploy=ìžë™ë°°ì¹˜ +htmlHostManagerServlet.addButton=추가 +htmlHostManagerServlet.addCopyXML=XML복사 +htmlHostManagerServlet.addDeployOnStartup=시작 ì‹œ 배치하기 +htmlHostManagerServlet.addDeployXML=XML배치 +htmlHostManagerServlet.addHost=호스트 +htmlHostManagerServlet.addManager=매니저 앱 +htmlHostManagerServlet.addName=ì´ë¦„: +htmlHostManagerServlet.addTitle=ê°€ìƒ í˜¸ìŠ¤íŠ¸ 추가 +htmlHostManagerServlet.addUnpackWARs=WARë“¤ì˜ ì••ì¶• 풀기 +htmlHostManagerServlet.helpHtmlManager=HTML 호스트 매니저 ë„ì›€ë§ +htmlHostManagerServlet.helpHtmlManagerFile=../docs/html-host-manager-howto.html +htmlHostManagerServlet.helpManager=호스트 매니저 ë„ì›€ë§ +htmlHostManagerServlet.helpManagerFile=../docs/host-manager-howto.html +htmlHostManagerServlet.hostAliases=í˜¸ìŠ¤íŠ¸ì˜ ë³„ì¹­ë“¤ +htmlHostManagerServlet.hostName=호스트 ì´ë¦„ +htmlHostManagerServlet.hostTasks=명령들 +htmlHostManagerServlet.hostThis=호스트 매니저가 설치ë˜ì—ˆìŠµë‹ˆë‹¤ - ëª…ë ¹ë“¤ì€ ì‚¬ìš© 불능 ìƒíƒœìž…니다. +htmlHostManagerServlet.hostsRemove=제거 +htmlHostManagerServlet.hostsStart=시작 +htmlHostManagerServlet.hostsStop=중지 +htmlHostManagerServlet.list=ê°€ìƒ í˜¸ìŠ¤íŠ¸ë“¤ì˜ ëª©ë¡ì„ 표시 +htmlHostManagerServlet.manager=호스트 ê´€ë¦¬ìž +htmlHostManagerServlet.messageLabel=메시지: +htmlHostManagerServlet.persistAll=현재 ì„¤ì •ì„ (ê°€ìƒ í˜¸ìŠ¤íŠ¸ë“¤ í¬í•¨) server.xmlê³¼ ê° ì›¹ 애플리케ì´ì…˜ì˜ context.xml 파ì¼ë“¤ì— 저장합니다. +htmlHostManagerServlet.persistAllButton=전부 +htmlHostManagerServlet.persistTitle=저장 환경 설정 +htmlHostManagerServlet.serverJVMVendor=JVM ë²¤ë” +htmlHostManagerServlet.serverJVMVersion=JVM 버전 +htmlHostManagerServlet.serverOSArch=ìš´ì˜ì²´ì œ 아키첵처 +htmlHostManagerServlet.serverOSName=ìš´ì˜ì²´ì œ ì´ë¦„ +htmlHostManagerServlet.serverOSVersion=ìš´ì˜ì²´ì œ 버전 +htmlHostManagerServlet.serverTitle=서버 ì •ë³´ +htmlHostManagerServlet.serverVersion=Tomcat 버전 +htmlHostManagerServlet.title=Tomcat ê°€ìƒ í˜¸ìŠ¤íŠ¸ 매니저 + +statusServlet.complete=서버 ìƒíƒœ 전부 +statusServlet.title=서버 ìƒíƒœ diff --git a/java/org/apache/catalina/manager/host/LocalStrings_pt_BR.properties b/java/org/apache/catalina/manager/host/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..eaaa194 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_pt_BR.properties @@ -0,0 +1,20 @@ +# 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. + +hostManagerServlet.managerXml=FALHA - Não foi possível instalar manager.xml + +htmlHostManagerServlet.addHost=Host +htmlHostManagerServlet.addManager=Aplicativo gerenciador. +htmlHostManagerServlet.hostName=Host diff --git a/java/org/apache/catalina/manager/host/LocalStrings_ru.properties b/java/org/apache/catalina/manager/host/LocalStrings_ru.properties new file mode 100644 index 0000000..8a9b557 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_ru.properties @@ -0,0 +1,89 @@ +# 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. + +hostManagerServlet.add=add: Добавление Ñервера [{0}] +hostManagerServlet.addFailed=Ошибка - Ðе удалоÑÑŒ добавить Ñервер [{0}] +hostManagerServlet.addSuccess=OK - Сервер [{0}] добавлен +hostManagerServlet.alreadyHost=Ошибка - Уже еÑÑ‚ÑŒ Ñервер Ñ Ñ‚Ð°ÐºÐ¸Ð¼ именем [{0}] +hostManagerServlet.alreadyStarted=Ошибка - Сервер [{0}] уже запущен +hostManagerServlet.alreadyStopped=Ошибка - Сервер [{0}] уже оÑтановлен +hostManagerServlet.appBaseCreateFail=Ошибка - Ðе удалоÑÑŒ Ñоздать директорию Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹ (appBase) [{0}] Ð´Ð»Ñ Ñервера [{1}] +hostManagerServlet.cannotRemoveOwnHost=Ошибка - ÐÐµÐ»ÑŒÐ·Ñ ÑƒÐ´Ð°Ð»Ð¸Ñ‚ÑŒ Ñвой ÑобÑтвенный Ñервер [{0}] +hostManagerServlet.cannotStartOwnHost=Ошибка - ÐÐµÐ»ÑŒÐ·Ñ Ð·Ð°Ð¿ÑƒÑтить Ñвой ÑобÑтвенный Ñервер [{0}] +hostManagerServlet.cannotStopOwnHost=Ошибка - ÐÐµÐ»ÑŒÐ·Ñ Ð¾Ñтановить Ñвой ÑобÑтвенный Ñервер [{0}] +hostManagerServlet.configBaseCreateFail=Ошибка - Ðе удалоÑÑŒ определить директорию Ñ Ñ„Ð°Ð¹Ð»Ð°Ð¼Ð¸ конфигурации (configBase) Ð´Ð»Ñ Ñервера [{0}] +hostManagerServlet.exception=Ошибка - ÐÐµÐ¾Ð±Ñ‹Ñ‡Ð½Ð°Ñ ÑÐ¸Ñ‚ÑƒÐ°Ñ†Ð¸Ñ [{0}] +hostManagerServlet.invalidHostName=Ошибка - Указано недопуÑтимое Ð¸Ð¼Ñ Ñервера [{0}] +hostManagerServlet.list=list: СпиÑок Ñерверов Ð´Ð»Ñ Ð´Ð²Ð¸Ð¶ÐºÐ° [{0}] +hostManagerServlet.listed=OK - СпиÑок Ñерверов +hostManagerServlet.managerXml=Ошибка - Ðе удалоÑÑŒ Ñоздать файл manager.xml +hostManagerServlet.noCommand=Ошибка - Команда не указана. +hostManagerServlet.noHost=Ошибка - Сервер Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ñ‹Ð¼ именем [{0}] не ÑущеÑтвует +hostManagerServlet.noWrapper=Метод setWrapper() у данного Ñервлета ещё не был вызван контейнером +hostManagerServlet.persist=persist: Сохранение текущей конфигурации +hostManagerServlet.persistFailed=Ошибка - Ðе удалоÑÑŒ Ñохранить конфигурацию +hostManagerServlet.persisted=OK - ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ñохранена +hostManagerServlet.postCommand=Ошибка - Команда [{0}] была подана при помощи запроÑа GET, но требуетÑÑ POST +hostManagerServlet.remove=remove: Удаление Ñервера [{0}] +hostManagerServlet.removeFailed=Ошибка - Ðе удалоÑÑŒ удалить Ñервер [{0}] +hostManagerServlet.removeSuccess=OK - Сервер удалён [{0}] +hostManagerServlet.start=start: ЗапуÑк Ñервера Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ [{0}] +hostManagerServlet.startFailed=Ошибка - Ðе удалоÑÑŒ запуÑтить Ñервер [{0}] +hostManagerServlet.started=OK - Сервер [{0}] запущен +hostManagerServlet.stop=stop: ОÑтановка Ñервера Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ [{0}] +hostManagerServlet.stopFailed=Ошибка - Ðе удалоÑÑŒ оÑтановить Ñервер [{0}] +hostManagerServlet.stopped=OK - Сервер [{0}] оÑтановлен +hostManagerServlet.unknownCommand=Ошибка - ÐеизвеÑÑ‚Ð½Ð°Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° [{0}] + +htmlHostManagerServlet.addAliases=ПÑевдонимы: +htmlHostManagerServlet.addAppBase=Ð”Ð¸Ñ€ÐµÐºÑ‚Ð¾Ñ€Ð¸Ñ Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹ (appBase): +htmlHostManagerServlet.addAutoDeploy=ÐвтоматичеÑкое развёртывание (AutoDeploy) +htmlHostManagerServlet.addButton=Добавить +htmlHostManagerServlet.addCopyXML=Включить опцию CopyXML +htmlHostManagerServlet.addDeployOnStartup=Развёртывание при Ñтарте Ñервера (DeployOnStartup) +htmlHostManagerServlet.addDeployXML=Включить опцию DeployXML +htmlHostManagerServlet.addHost=Сервер +htmlHostManagerServlet.addManager=УÑтановить приложение Manager +htmlHostManagerServlet.addName=ИмÑ: +htmlHostManagerServlet.addTitle=Добавить виртуальный Ñервер +htmlHostManagerServlet.addUnpackWARs=Включить опцию UnpackWARs +htmlHostManagerServlet.helpHtmlManager=Справка Ð´Ð»Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»ÐµÐ¹ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +htmlHostManagerServlet.helpHtmlManagerFile=../docs/html-host-manager-howto.html +htmlHostManagerServlet.helpManager=Справка по API Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +htmlHostManagerServlet.helpManagerFile=../docs/host-manager-howto.html +htmlHostManagerServlet.hostAliases=ПÑевдонимы Ñервера +htmlHostManagerServlet.hostName=Ð˜Ð¼Ñ Ñервера +htmlHostManagerServlet.hostTasks=Управление +htmlHostManagerServlet.hostThis=Приложение Host Manager уÑтановлено здеÑÑŒ - команды ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð½Ðµ поддерживаютÑÑ +htmlHostManagerServlet.hostsRemove=Удалить +htmlHostManagerServlet.hostsStart=Старт +htmlHostManagerServlet.hostsStop=Стоп +htmlHostManagerServlet.list=СпиÑок виртуальных Ñерверов +htmlHostManagerServlet.manager=Управление Ñервером +htmlHostManagerServlet.messageLabel=Сообщение: +htmlHostManagerServlet.persistAll=СохранÑет текущие наÑтройки (Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ Ð²Ð¸Ñ€Ñ‚ÑƒÐ°Ð»ÑŒÐ½Ñ‹Ðµ хоÑÑ‚Ñ‹) в файл server.xml и в файлы context.xml Ð´Ð»Ñ Ð²ÐµÐ±-приложений +htmlHostManagerServlet.persistAllButton=Сохранить вÑÑ‘ +htmlHostManagerServlet.persistTitle=Сохранить наÑтройки +htmlHostManagerServlet.serverJVMVendor=ПоÑтавщик JVM +htmlHostManagerServlet.serverJVMVersion=ВерÑÐ¸Ñ JVM +htmlHostManagerServlet.serverOSArch=Ðрхитектура ОС +htmlHostManagerServlet.serverOSName=ОС +htmlHostManagerServlet.serverOSVersion=ВерÑÐ¸Ñ ÐžÐ¡ +htmlHostManagerServlet.serverTitle=Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ Ñервере +htmlHostManagerServlet.serverVersion=ВерÑÐ¸Ñ Tomcat +htmlHostManagerServlet.title=Управление виртуальными Ñерверами Tomcat + +statusServlet.complete=Подробный отчёт о ÑоÑтоÑнии +statusServlet.title=СоÑтоÑние Ñервера diff --git a/java/org/apache/catalina/manager/host/LocalStrings_zh_CN.properties b/java/org/apache/catalina/manager/host/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..37cd365 --- /dev/null +++ b/java/org/apache/catalina/manager/host/LocalStrings_zh_CN.properties @@ -0,0 +1,89 @@ +# 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. + +hostManagerServlet.add=添加:添加主机[{0}] +hostManagerServlet.addFailed=失败 - 添加主机 [{0}] 失败 +hostManagerServlet.addSuccess=确定-添加主机[{0}] +hostManagerServlet.alreadyHost=失败 - 主机å称[{0}]å·²ç»å­˜åœ¨ +hostManagerServlet.alreadyStarted=失败 - Host[{0}]å·²ç»å¯åŠ¨ã€‚ +hostManagerServlet.alreadyStopped=失败 - 主机[{0}]å·²ç»åœæ­¢ +hostManagerServlet.appBaseCreateFail=失败 - 创建appBase目录[{0}]失败,主机:[{1}] +hostManagerServlet.cannotRemoveOwnHost=失败 - 无法移除你的主机[{0}] +hostManagerServlet.cannotStartOwnHost=失败 - 无法å¯åŠ¨ä¸»æœº[{0}] +hostManagerServlet.cannotStopOwnHost=失败 - 无法åœæ­¢ä¸»æœº[{0}] +hostManagerServlet.configBaseCreateFail=失败 - 无法识别主机[{0}]的基础é…ç½® +hostManagerServlet.exception=失败 - 出现异常[{0}] +hostManagerServlet.invalidHostName=失败 - 指定的主机å称[{0}]无效 +hostManagerServlet.list=列表:列出引擎[{0}]的主机 +hostManagerServlet.listed=OK - 已列出Host +hostManagerServlet.managerXml=FAIL - 无法安装manager.xml +hostManagerServlet.noCommand=失败 - 未指定命令 +hostManagerServlet.noHost=失败 - 主机å称[{0}]ä¸å­˜åœ¨ +hostManagerServlet.noWrapper=容器未给当å‰servlet设置setWrapper() +hostManagerServlet.persist=æŒä¹…化: 正在æŒä¹…化当å‰é…ç½® +hostManagerServlet.persistFailed=失败 - 无法æŒä¹…化é…ç½® +hostManagerServlet.persisted=OK - é…ç½®æŒä¹…化了. +hostManagerServlet.postCommand=失败 - å°è¯•é€šè¿‡GET请求使用命令[{0}],但是需è¦ä½¿ç”¨POST请求 +hostManagerServlet.remove=移除:正在移除主机 [{0}] +hostManagerServlet.removeFailed=失败 - 无法移除主机 [{0}] +hostManagerServlet.removeSuccess=确定-已删除主机[{0}] +hostManagerServlet.start=å¯åŠ¨ï¼šå¯åŠ¨ä¸»æœº[{0}] +hostManagerServlet.startFailed=失败 - 无法å¯åŠ¨ä¸»æœº [{0}] +hostManagerServlet.started=OK - 主机 [{0}] å·²å¯åŠ¨ +hostManagerServlet.stop=åœæ­¢ï¼šåœæ­¢ä¸»æœº[{0}] +hostManagerServlet.stopFailed=失败 - 无法åœæ­¢ä¸»æœº [{0}] +hostManagerServlet.stopped=OK - 主机 [{0}] å·²åœæ­¢ +hostManagerServlet.unknownCommand=失败 - 未知命令 [{0}] + +htmlHostManagerServlet.addAliases=别å: +htmlHostManagerServlet.addAppBase=应用程åºåº“: +htmlHostManagerServlet.addAutoDeploy=自动.部署 +htmlHostManagerServlet.addButton=添加 +htmlHostManagerServlet.addCopyXML=æ‹·è´XML +htmlHostManagerServlet.addDeployOnStartup=å¯åŠ¨.部署 +htmlHostManagerServlet.addDeployXML=部署XML +htmlHostManagerServlet.addHost=主机 +htmlHostManagerServlet.addManager=ç®¡ç† App +htmlHostManagerServlet.addName=å称: +htmlHostManagerServlet.addTitle=添加.虚拟主机 +htmlHostManagerServlet.addUnpackWARs=解压WARs +htmlHostManagerServlet.helpHtmlManager=HTML主机管ç†å™¨å¸®åŠ© +htmlHostManagerServlet.helpHtmlManagerFile=../docs/html-host-manager-howto.html +htmlHostManagerServlet.helpManager=主机管ç†å™¨å¸®åŠ© +htmlHostManagerServlet.helpManagerFile=../docs/host-manager-howto.html +htmlHostManagerServlet.hostAliases=主机别å +htmlHostManagerServlet.hostName=主机å称 +htmlHostManagerServlet.hostTasks=命令 +htmlHostManagerServlet.hostThis=主机管ç†å™¨å·²å®‰è£… - 命令ä¸å¯ç”¨ +htmlHostManagerServlet.hostsRemove=移除 +htmlHostManagerServlet.hostsStart=å¯åŠ¨ +htmlHostManagerServlet.hostsStop=åœæ­¢ +htmlHostManagerServlet.list=列出虚拟主机 +htmlHostManagerServlet.manager=主机.管ç†å™¨ +htmlHostManagerServlet.messageLabel=消æ¯ï¼š +htmlHostManagerServlet.persistAll=ä¿å­˜å½“å‰é…置信æ¯ï¼ˆåŒ…括虚拟主机)到server.xmlå’Œæ¯ä¸ªweb应用程åºcontext.xml文件里。 +htmlHostManagerServlet.persistAllButton=全部 +htmlHostManagerServlet.persistTitle=æŒä¹…化é…ç½® +htmlHostManagerServlet.serverJVMVendor=JVM供应商 +htmlHostManagerServlet.serverJVMVersion=JVM版本 +htmlHostManagerServlet.serverOSArch=æ“作系统架构 +htmlHostManagerServlet.serverOSName=æ“作系统å称 +htmlHostManagerServlet.serverOSVersion=æ“作系统版本 +htmlHostManagerServlet.serverTitle=æœåŠ¡å™¨ä¿¡æ¯ +htmlHostManagerServlet.serverVersion=Tomcat版本 +htmlHostManagerServlet.title=Tomcat虚拟主机管ç†å‘˜ + +statusServlet.complete=æœåŠ¡å™¨å…¨éƒ¨çŠ¶æ€ +statusServlet.title=æœåŠ¡å™¨çŠ¶æ€ diff --git a/java/org/apache/catalina/manager/util/SessionUtils.java b/java/org/apache/catalina/manager/util/SessionUtils.java new file mode 100644 index 0000000..787d603 --- /dev/null +++ b/java/org/apache/catalina/manager/util/SessionUtils.java @@ -0,0 +1,261 @@ +/* + * 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.catalina.manager.util; + +import java.lang.reflect.Method; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; + +import javax.security.auth.Subject; + +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.Session; +import org.apache.tomcat.util.ExceptionUtils; + +/** + * Utility methods on HttpSessions. + * + * @author Cédrik LIME + */ +public class SessionUtils { + + private SessionUtils() { + super(); + } + + /** + * The session attributes key under which the user's selected java.util.Locale is stored, if any. + */ + // org.apache.struts.Globals.LOCALE_KEY + private static final String STRUTS_LOCALE_KEY = "org.apache.struts.action.LOCALE";//$NON-NLS-1$ + // jakarta.servlet.jsp.jstl.core.Config.FMT_LOCALE + private static final String JSTL_LOCALE_KEY = "jakarta.servlet.jsp.jstl.fmt.locale";//$NON-NLS-1$ + // org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME + private static final String SPRING_LOCALE_KEY = "org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE";//$NON-NLS-1$ + /** + * Lower and upper-case strings will be dynamically generated. Put mid-capitalised strings here! + */ + private static final String[] LOCALE_TEST_ATTRIBUTES = + new String[] { STRUTS_LOCALE_KEY, SPRING_LOCALE_KEY, JSTL_LOCALE_KEY, "Locale", "java.util.Locale" }; + /** + * For efficient operation, list the attributes here with the typically used capitalisation. This will be tried + * first and then the auto-generated upper and lower case versions will be tried. + */ + private static final String[] USER_TEST_ATTRIBUTES = + new String[] { "Login", "User", "userName", "UserName", "Utilisateur", "SPRING_SECURITY_LAST_USERNAME" }; + + /** + * Try to get user locale from the session, if possible. IMPLEMENTATION NOTE: this method has explicit support for + * Tapestry 3, Struts 1.x and Spring JSF check the browser meta tag "accept languages" to choose what language to + * display. + * + * @param in_session The session + * + * @return the locale + */ + public static Locale guessLocaleFromSession(final Session in_session) { + return guessLocaleFromSession(in_session.getSession()); + } + + public static Locale guessLocaleFromSession(final HttpSession in_session) { + if (null == in_session) { + return null; + } + try { + Locale locale = null; + + // First search "known locations" + for (String localeTestAttribute : LOCALE_TEST_ATTRIBUTES) { + Object obj = in_session.getAttribute(localeTestAttribute); + if (obj instanceof Locale) { + locale = (Locale) obj; + break; + } + obj = in_session.getAttribute(localeTestAttribute.toLowerCase(Locale.ENGLISH)); + if (obj instanceof Locale) { + locale = (Locale) obj; + break; + } + obj = in_session.getAttribute(localeTestAttribute.toUpperCase(Locale.ENGLISH)); + if (obj instanceof Locale) { + locale = (Locale) obj; + break; + } + } + + if (null != locale) { + return locale; + } + + // Tapestry 3.0: Engine stored in session under "org.apache.tapestry.engine:" + config.getServletName() + // TODO: Tapestry 4+ + final List tapestryArray = new ArrayList<>(); + for (Enumeration enumeration = in_session.getAttributeNames(); enumeration.hasMoreElements();) { + String name = enumeration.nextElement(); + if (name.contains("tapestry") && name.contains("engine") && null != in_session.getAttribute(name)) {//$NON-NLS-1$ //$NON-NLS-2$ + tapestryArray.add(in_session.getAttribute(name)); + } + } + if (tapestryArray.size() == 1) { + // found a potential Engine! Let's call getLocale() on it. + Object probableEngine = tapestryArray.get(0); + if (null != probableEngine) { + try { + Method readMethod = probableEngine.getClass().getMethod("getLocale", (Class[]) null);//$NON-NLS-1$ + // Call the property getter and return the value + Object possibleLocale = readMethod.invoke(probableEngine, (Object[]) null); + if (possibleLocale instanceof Locale) { + locale = (Locale) possibleLocale; + } + } catch (Exception e) { + Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + // stay silent + } + } + } + + if (null != locale) { + return locale; + } + + // Last guess: iterate over all attributes, to find a Locale + // If there is only one, consider it to be /the/ locale + final List localeArray = new ArrayList<>(); + for (Enumeration enumeration = in_session.getAttributeNames(); enumeration.hasMoreElements();) { + String name = enumeration.nextElement(); + Object obj = in_session.getAttribute(name); + if (obj instanceof Locale) { + localeArray.add(obj); + } + } + if (localeArray.size() == 1) { + locale = (Locale) localeArray.get(0); + } + + return locale; + } catch (IllegalStateException ise) { + // ignore: invalidated session + return null; + } + } + + /** + * Try to get user from the session, if possible. + * + * @param in_session The session + * + * @return the user + */ + public static Object guessUserFromSession(final Session in_session) { + if (null == in_session) { + return null; + } + if (in_session.getPrincipal() != null) { + return in_session.getPrincipal().getName(); + } + HttpSession httpSession = in_session.getSession(); + if (httpSession == null) { + return null; + } + + try { + Object user = null; + // First search "known locations" + for (String userTestAttribute : USER_TEST_ATTRIBUTES) { + Object obj = httpSession.getAttribute(userTestAttribute); + if (null != obj) { + user = obj; + break; + } + obj = httpSession.getAttribute(userTestAttribute.toLowerCase(Locale.ENGLISH)); + if (null != obj) { + user = obj; + break; + } + obj = httpSession.getAttribute(userTestAttribute.toUpperCase(Locale.ENGLISH)); + if (null != obj) { + user = obj; + break; + } + } + + if (null != user) { + return user; + } + + // Last guess: iterate over all attributes, to find a java.security.Principal or javax.security.auth.Subject + // If there is only one, consider it to be /the/ user + final List principalArray = new ArrayList<>(); + for (Enumeration enumeration = httpSession.getAttributeNames(); enumeration.hasMoreElements();) { + String name = enumeration.nextElement(); + Object obj = httpSession.getAttribute(name); + if (obj instanceof Principal || obj instanceof Subject) { + principalArray.add(obj); + } + } + if (principalArray.size() == 1) { + user = principalArray.get(0); + } + + if (null != user) { + return user; + } + + return user; + } catch (IllegalStateException ise) { + // ignore: invalidated session + return null; + } + } + + + public static long getUsedTimeForSession(Session in_session) { + try { + long diffMilliSeconds = in_session.getThisAccessedTime() - in_session.getCreationTime(); + return diffMilliSeconds; + } catch (IllegalStateException ise) { + // ignore: invalidated session + return -1; + } + } + + public static long getTTLForSession(Session in_session) { + try { + long diffMilliSeconds = 1000 * in_session.getMaxInactiveInterval() - + (System.currentTimeMillis() - in_session.getThisAccessedTime()); + return diffMilliSeconds; + } catch (IllegalStateException ise) { + // ignore: invalidated session + return -1; + } + } + + public static long getInactiveTimeForSession(Session in_session) { + try { + long diffMilliSeconds = System.currentTimeMillis() - in_session.getThisAccessedTime(); + return diffMilliSeconds; + } catch (IllegalStateException ise) { + // ignore: invalidated session + return -1; + } + } +} diff --git a/java/org/apache/catalina/mapper/Constants.java b/java/org/apache/catalina/mapper/Constants.java new file mode 100644 index 0000000..44a74bc --- /dev/null +++ b/java/org/apache/catalina/mapper/Constants.java @@ -0,0 +1,26 @@ +/* + * 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.catalina.mapper; + +/** + * Static constants for this package. + */ +public final class Constants { + + public static final String Package = "org.apache.catalina.mapper"; + +} diff --git a/java/org/apache/catalina/mapper/LocalStrings.properties b/java/org/apache/catalina/mapper/LocalStrings.properties new file mode 100644 index 0000000..b5544fc --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings.properties @@ -0,0 +1,36 @@ +# 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. + +mapper.addContext.hostIsAlias=Host [{0}] found is an alias +mapper.addContext.noHost=No host found [{0}] +mapper.addHost.sameHost=Duplicate registration of the same host [{0}]. Ignored. +mapper.addHost.success=Registered host [{0}] +mapper.addHostAlias.sameHost=Duplicate registration of alias [{0}] for the same host [{1}]. Ignored. +mapper.addHostAlias.success=Registered alias [{0}] for host [{1}] +mapper.duplicateHost=Duplicate Host [{0}]. The name is already used by Host [{1}]. This Host will be ignored. +mapper.duplicateHostAlias=Duplicate host Alias [{0}] in Host [{1}]. The name is already used by Host [{2}]. This Alias will be ignored. +mapper.findContext.noContext=No context found [{0}] +mapper.findContext.noContextVersion=No context version found [{0}] [{1}] +mapper.findContext.noHostOrAlias=No host [{0}] or is an alias +mapper.removeWrapper=Removing wrapper from Context [{0}] with path [{1}] + +mapperListener.pauseContext=Register Context [{0}] as being reloaded for service [{1}] +mapperListener.registerContext=Register Context [{0}] for service [{1}] +mapperListener.registerHost=Register host [{0}] at domain [{1}] for service [{2}] +mapperListener.registerWrapper=Register Wrapper [{0}] in Context [{1}] for service [{2}] +mapperListener.unknownDefaultHost=Unknown default host [{0}] for service [{1}]. Tomcat will not be able process HTTP/1.0 requests that do not specify a host name. +mapperListener.unregisterContext=Unregister Context [{0}] for service [{1}] +mapperListener.unregisterHost=Unregister host [{0}] at domain [{1}] for service [{2}] +mapperListener.unregisterWrapper=Unregister Wrapper [{0}] in Context [{1}] for service [{2}] diff --git a/java/org/apache/catalina/mapper/LocalStrings_cs.properties b/java/org/apache/catalina/mapper/LocalStrings_cs.properties new file mode 100644 index 0000000..b2bda61 --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings_cs.properties @@ -0,0 +1,19 @@ +# 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. + +mapper.addHostAlias.success=Registrovaný alias [{0}] pro host [{1}] + +mapperListener.pauseContext=Registrace kontextu [{0}] pÅ™i pÅ™enaÄtení pro service [{1}] +mapperListener.registerWrapper=Registrace obálky [{0}] v kontextu [{1}] pro službu [{2}] diff --git a/java/org/apache/catalina/mapper/LocalStrings_de.properties b/java/org/apache/catalina/mapper/LocalStrings_de.properties new file mode 100644 index 0000000..4bf02a4 --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings_de.properties @@ -0,0 +1,19 @@ +# 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. + +mapper.addHostAlias.success=Alias [{0}] für Host [{1}] registriert + +mapperListener.pauseContext=Registriere Context [{0}] als neu geladen für Service [{1}] +mapperListener.registerWrapper=Registriere Wrapper [{0}] im Context [{1}] für Service [{2}] diff --git a/java/org/apache/catalina/mapper/LocalStrings_es.properties b/java/org/apache/catalina/mapper/LocalStrings_es.properties new file mode 100644 index 0000000..dbcc254 --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings_es.properties @@ -0,0 +1,22 @@ +# 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. + +mapper.addHostAlias.success=Alias [{0}] registrado para el servidor [{1}] + +mapperListener.pauseContext=El registro de contexto [{0}] como siendo recargado para el servicio [{1}]\n +mapperListener.registerHost=Registrar máquina [{0}] en dominio [{1}] para el conector [{2}] +mapperListener.registerWrapper=Registre la envoltura [{0}] en el contexto [{1}] para el servicio [{2}] +mapperListener.unknownDefaultHost=Máquina por defecto desconocida: [{0}] para el conector [{1}] +mapperListener.unregisterHost=Desregistrar máquina [{0}] en dominio [{1}] para el conector [{2}] diff --git a/java/org/apache/catalina/mapper/LocalStrings_fr.properties b/java/org/apache/catalina/mapper/LocalStrings_fr.properties new file mode 100644 index 0000000..78d3b40 --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings_fr.properties @@ -0,0 +1,36 @@ +# 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. + +mapper.addContext.hostIsAlias=L''hôte [{0}] trouvé est un alias +mapper.addContext.noHost=Pas d''hôte [{0}] +mapper.addHost.sameHost=L''enregistrement en double de l''hôte [{0}] est ignoré +mapper.addHost.success=Enregistré l''hôte [{0}] +mapper.addHostAlias.sameHost=L''enregistrement en double de l''alias [{0}] de l''hôte [{1}] est ignoré +mapper.addHostAlias.success=L''alias [{0}] pour le hôte [{1}] a été enregistré +mapper.duplicateHost=L''hôte [{0}] est en double et sera ignoré, le nom est déjà utilisé par l''hôte [{1}] +mapper.duplicateHostAlias=L''alias [{0}] de l''hôte [{1}] est en double et sera ignoré, le nom est déjà utilisé par l''hôte [{2}] +mapper.findContext.noContext=Pas de contexte trouvé [{0}] +mapper.findContext.noContextVersion=Pas de version du contexte trouvée [{0}] [{1}] +mapper.findContext.noHostOrAlias=Pas d''hôte [{0}] ou c''est un alias +mapper.removeWrapper=Retire l''enrobeur du contexte [{0}] avec le chemin [{1}] + +mapperListener.pauseContext=Enregistrement du contexte [{0}] comme étant en cours de rechargement dans le service [{1}] +mapperListener.registerContext=Enregistrement du contexte [{0}] pour le service [{1}] +mapperListener.registerHost=Enregistrement de l''hôte [{0}] dans le domaine [{1}] pour le service [{2}] +mapperListener.registerWrapper=Enregistrement du wrapper [{0}] dans le contexte [{1}] pour le service [{2}] +mapperListener.unknownDefaultHost=L''hôte par défaut [{0}] est inconnu dans le service [{1}], Tomcat ne sera pas capable de traiter les requêtes HTTP/1.0 qui ne spécifient pas de nom d''hôte +mapperListener.unregisterContext=Retrait du Context [{0}] pour le service [{1}] +mapperListener.unregisterHost=Retrait de l''hôte [{0}] dans le domaine [{1}] pour le service [{2}] +mapperListener.unregisterWrapper=Désenregitrement du wrapper [{0}] dans le contexte [{1}] pour le service [{2}] diff --git a/java/org/apache/catalina/mapper/LocalStrings_ja.properties b/java/org/apache/catalina/mapper/LocalStrings_ja.properties new file mode 100644 index 0000000..2c4f66c --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings_ja.properties @@ -0,0 +1,36 @@ +# 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. + +mapper.addContext.hostIsAlias=見ã¤ã‹ã£ãŸHost[{0}]ã¯ã‚¨ã‚¤ãƒªã‚¢ã‚¹ã§ã™ã€‚ +mapper.addContext.noHost=ホストãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ [{0}] +mapper.addHost.sameHost=åŒã˜ãƒ›ã‚¹ãƒˆ [{0}] ã®ç™»éŒ²ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ã€‚ 無視ã•ã‚Œã¾ã™ã€‚ +mapper.addHost.success=登録ã•ã‚ŒãŸãƒ›ã‚¹ãƒˆ [{0}] +mapper.addHostAlias.sameHost=ホスト [{1}] ã«ã‚¨ã‚¤ãƒªã‚¢ã‚¹ [{0}] ãŒé‡è¤‡ã—ã¦ç™»éŒ²ã•ã‚Œã¾ã—ãŸã€‚無視ã—ã¾ã™ã€‚ +mapper.addHostAlias.success=ホスト [{1}] ã®ã‚¨ã‚¤ãƒªã‚¢ã‚¹ [{0}] を登録ã—ã¾ã—ãŸã€‚ +mapper.duplicateHost=Hoståã®é‡è¤‡ [{0}]。ã“ã®åå‰ã¯Host[{1}] ã§ä½¿ç”¨ã•ã‚Œã¦ã„ã‚‹ãŸã‚無視ã•ã‚Œã¾ã™ã€‚ +mapper.duplicateHostAlias=Host [{1}] ã®ãƒ›ã‚¹ãƒˆã‚¨ã‚¤ãƒªã‚¢ã‚¹ [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ã€‚ åå‰ã¯ã™ã§ã«Host [{2}] ã«ã‚ˆã£ã¦ä½¿ç”¨ã•ã‚Œã¦ã„ã¾ã™ã€‚ ã“ã®ã‚¨ã‚¤ãƒªã‚¢ã‚¹ã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +mapper.findContext.noContext=コンテキストãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ [{0}] +mapper.findContext.noContextVersion=コンテキストãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“[{0}] [{1}] +mapper.findContext.noHostOrAlias=ホスト [{0}] ãŒãªã„ã‹ã€ã‚¨ã‚¤ãƒªã‚¢ã‚¹ã§ã™ +mapper.removeWrapper=コンテキスト [{0}] ã‹ã‚‰ãƒ‘ス [{1}] を使用ã—ã¦ãƒ©ãƒƒãƒ‘ーを削除ã—ã¾ã™ + +mapperListener.pauseContext=コンテキスト [{0}] をサービス [{1}] ã®ãŸã‚ã«å†ãƒ­ãƒ¼ãƒ‰ã™ã‚‹ã‚‚ã®ã¨ã—ã¦ç™»éŒ²ã—ã¾ã™ +mapperListener.registerContext=コンテキスト [{0}] ã«ã‚µãƒ¼ãƒ“ス [{1}] を登録ã—ã¾ã™ã€‚ +mapperListener.registerHost=サービス [{2}] ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ [{1}] ã«ãƒ›ã‚¹ãƒˆ [{0}] を登録 +mapperListener.registerWrapper=サービス [{2}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{1}] ã«ãƒ©ãƒƒãƒ‘ー [{0}] を登録ã—ã¾ã—ãŸã€‚ +mapperListener.unknownDefaultHost=サービス [{1}] ã«æ—¢å®šãƒ›ã‚¹ãƒˆ [{0}] ã¯å­˜åœ¨ã—ã¾ã›ã‚“。Tomcat ã¯ãƒ›ã‚¹ãƒˆåを指定ã—ãªã„ HTTP/1.0 ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã§ãã¾ã›ã‚“。 +mapperListener.unregisterContext=サービス [{1}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] ã®ç™»éŒ²ã‚’解除ã—ã¾ã™ +mapperListener.unregisterHost=サービス [{2}] ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ [{1}] ã®ãƒ›ã‚¹ãƒˆ [{0}] ã®ç™»éŒ²ã‚’解除 +mapperListener.unregisterWrapper=サービス [{2}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{1}] ã®ãƒ©ãƒƒãƒ‘ー [{0}] ã®ç™»éŒ²ã‚’解除ã—ã¾ã™ diff --git a/java/org/apache/catalina/mapper/LocalStrings_ko.properties b/java/org/apache/catalina/mapper/LocalStrings_ko.properties new file mode 100644 index 0000000..6ef6767 --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings_ko.properties @@ -0,0 +1,36 @@ +# 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. + +mapper.addContext.hostIsAlias=ë°œê²¬ëœ í˜¸ìŠ¤íŠ¸ [{0}]ì€(는) 별칭입니다. +mapper.addContext.noHost=호스트 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +mapper.addHost.sameHost=ë™ì¼í•œ 호스트 [{0}]ì„(를) 중복하여 등ë¡ì„ ì‹œë„했는ë°, ì´ëŠ” 무시ë©ë‹ˆë‹¤. +mapper.addHost.success=등ë¡ëœ 호스트 [{0}] +mapper.addHostAlias.sameHost=ë™ì¼í•œ 호스트 [{1}]ì„(를) 위한 별칭 [{0}]ì´(ê°€) 중복해서 ë“±ë¡ ì‹œë„ ë˜ì—ˆëŠ”ë°, ì´ëŠ” 무시ë©ë‹ˆë‹¤. +mapper.addHostAlias.success=호스트 [{1}]ì„(를) 위해 별칭 [{0}]ì´(ê°€) 등ë¡ë˜ì—ˆìŠµë‹ˆë‹¤. +mapper.duplicateHost=ì¤‘ë³µëœ í˜¸ìŠ¤íŠ¸ [{0}]. 해당 ì´ë¦„ì€ ì´ë¯¸ 호스트 [{1}]ì— ì˜í•´ 사용ë˜ê³  있습니다. ì´ í˜¸ìŠ¤íŠ¸ëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +mapper.duplicateHostAlias=호스트 [{1}] ë‚´ì— ì¤‘ë³µëœ í˜¸ìŠ¤íŠ¸ 별칭 [{0}]. 해당 ì´ë¦„ì´ ì´ë¯¸ 호스트 [{2}]ì— ì˜í•´ 사용ë˜ê³  있습니다. ì´ ë³„ì¹­ì€ ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +mapper.findContext.noContext=컨í…스트 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +mapper.findContext.noContextVersion=컨í…스트 ë²„ì „ì„ ì°¾ì„ ìˆ˜ 없습니다: [{0}] [{1}] +mapper.findContext.noHostOrAlias=[{0}](ì´)ë¼ëŠ” 호스트가 없거나, ê·¸ê²ƒì€ ë³„ì¹­ìž…ë‹ˆë‹¤. +mapper.removeWrapper=경로 [{1}]ì˜ ì»¨í…스트 [{0}](으)로부터 wapper를 제거합니다. + +mapperListener.pauseContext=컨í…스트 [{0}]ì„(를), 서비스 [{1}]ì„(를) 위해 다시 로드ë˜ê³  있는 것으로 등ë¡í•©ë‹ˆë‹¤. +mapperListener.registerContext=서비스 [{1}]ì„(를) 위한 컨í…스트 [{0}]ì„(를) 등ë¡í•©ë‹ˆë‹¤. +mapperListener.registerHost=서비스 [{2}]ì„(를) 위해 ë„ë©”ì¸ [{1}]ì— ìžˆëŠ” 호스트 [{0}]ì„(를) 등ë¡í•©ë‹ˆë‹¤. +mapperListener.registerWrapper=서비스 [{2}]ì„(를) 위한 컨í…스트 [{1}]ì—ì„œ Wrapper [{0}]ì„(를) 등ë¡í•©ë‹ˆë‹¤. +mapperListener.unknownDefaultHost=서비스 [{1}]ì„(를) 위해, ì•Œ 수 없는 기본 호스트 [{0}]. Tomcatì€ í˜¸ìŠ¤íŠ¸ ì´ë¦„ì„ ì§€ì •í•˜ì§€ ì•Šì€ HTTP/1.0 ìš”ì²­ë“¤ì„ ì²˜ë¦¬í•  수 ì—†ì„ ê²ƒìž…ë‹ˆë‹¤. +mapperListener.unregisterContext=서비스 [{1}]ì„(를) 위한 컨í…스트 [{0}]ì— ëŒ€í•œ 등ë¡ì„ 제거합니다. +mapperListener.unregisterHost=서비스 [{2}]ì„(를) 위한 ë„ë©”ì¸ [{1}]ì—ì„œ 호스트 [{0}]ì˜ ë“±ë¡ì„ 제거합니다. +mapperListener.unregisterWrapper=서비스 [{2}]ì„(를) 위한 컨í…스트 [{1}] ë‚´ì˜ Wrapper [{0}]ì— ëŒ€í•œ 등ë¡ì„ 제거합니다. diff --git a/java/org/apache/catalina/mapper/LocalStrings_pt_BR.properties b/java/org/apache/catalina/mapper/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..bba2f56 --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +mapper.addHostAlias.success=Registrado alias [{0}] para host [{1}] diff --git a/java/org/apache/catalina/mapper/LocalStrings_zh_CN.properties b/java/org/apache/catalina/mapper/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..10098ee --- /dev/null +++ b/java/org/apache/catalina/mapper/LocalStrings_zh_CN.properties @@ -0,0 +1,36 @@ +# 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. + +mapper.addContext.hostIsAlias=找到的主机[{0}]是别å +mapper.addContext.noHost=找ä¸åˆ°ä¸»æœº [{0}] +mapper.addHost.sameHost=åŒä¸€ä¸»æœº[{0}]çš„é‡å¤æ³¨å†Œã€‚忽略。 +mapper.addHost.success=注册主机[{0}] +mapper.addHostAlias.sameHost=为åŒä¸€ä¸»æœº[{1}]é‡å¤æ³¨å†Œåˆ«å[{0}]。忽略。 +mapper.addHostAlias.success=为虚拟主机 [{1}] 注册了别å [{0}] +mapper.duplicateHost=é‡å¤çš„主机[{0}]。主机[{1}]已使用该å称。此主机将被忽略。 +mapper.duplicateHostAlias=主机[{1}]中的主机别å[{0}]é‡å¤ã€‚主机[{2}]已使用该å称。将忽略此别å。 +mapper.findContext.noContext=找ä¸åˆ°ä¸Šä¸‹æ–‡[{0}] +mapper.findContext.noContextVersion=找ä¸åˆ°ä¸Šä¸‹æ–‡ç‰ˆæœ¬[{0}][{1}] +mapper.findContext.noHostOrAlias=没有主机[{0}]或是别å +mapper.removeWrapper=正在从路径为[{1}]的上下文[{0}]中删除包装。 + +mapperListener.pauseContext=æ ¹æ®æœåŠ¡éœ€è¦ï¼Œæ³¨å†Œå†…容[{0}]å·²ç»é‡æ–°åŠ è½½ +mapperListener.registerContext=为æœåŠ¡[{1}]注册上下文[{0}] +mapperListener.registerHost=这域å[{1}]注册主机[{0}],æœåŠ¡ï¼š[{2}] +mapperListener.registerWrapper=为æœåŠ¡Service[{2}]在上下文Context[{1}]注册Wrapper[{0}] +mapperListener.unknownDefaultHost=æœåŠ¡[{1}]的默认主机[{0}]未知。Tomcat将无法处ç†æœªæŒ‡å®šä¸»æœºåçš„HTTP/1.0请求。 +mapperListener.unregisterContext=注销æœåŠ¡[{1}]的上下文[{0}] +mapperListener.unregisterHost=在域[{1}]中.,ä¸èƒ½æ³¨å†Œä¸»æœº[{0}]为æœåŠ¡[{2}] +mapperListener.unregisterWrapper=在上下文[{1}]中注销æœåŠ¡[{2}]的包装程åº[{0}] diff --git a/java/org/apache/catalina/mapper/Mapper.java b/java/org/apache/catalina/mapper/Mapper.java new file mode 100644 index 0000000..3edecb1 --- /dev/null +++ b/java/org/apache/catalina/mapper/Mapper.java @@ -0,0 +1,1655 @@ +/* + * 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.catalina.mapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.servlet.http.MappingMatch; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.Wrapper; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.Ascii; +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.res.StringManager; + +/** + * Mapper, which implements the servlet API mapping rules (which are derived from the HTTP rules). + * + * @author Remy Maucherat + */ +public final class Mapper { + + + private static final Log log = LogFactory.getLog(Mapper.class); + + private static final StringManager sm = StringManager.getManager(Mapper.class); + + + // ----------------------------------------------------- Instance Variables + + /** + * Array containing the virtual hosts definitions. + */ + // Package private to facilitate testing + volatile MappedHost[] hosts = new MappedHost[0]; + + + /** + * Default host name. + */ + private volatile String defaultHostName = null; + private volatile MappedHost defaultHost = null; + + + /** + * Mapping from Context object to Context version to support RequestDispatcher mappings. + */ + private final Map contextObjectToContextVersionMap = new ConcurrentHashMap<>(); + + + // --------------------------------------------------------- Public Methods + + /** + * Set default host. + * + * @param defaultHostName Default host name + */ + public synchronized void setDefaultHostName(String defaultHostName) { + this.defaultHostName = renameWildcardHost(defaultHostName); + if (this.defaultHostName == null) { + defaultHost = null; + } else { + defaultHost = exactFind(hosts, this.defaultHostName); + } + } + + + /** + * Add a new host to the mapper. + * + * @param name Virtual host name + * @param aliases Alias names for the virtual host + * @param host Host object + */ + public synchronized void addHost(String name, String[] aliases, Host host) { + name = renameWildcardHost(name); + MappedHost[] newHosts = new MappedHost[hosts.length + 1]; + MappedHost newHost = new MappedHost(name, host); + if (insertMap(hosts, newHosts, newHost)) { + hosts = newHosts; + if (newHost.name.equals(defaultHostName)) { + defaultHost = newHost; + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapper.addHost.success", name)); + } + } else { + MappedHost duplicate = hosts[find(hosts, name)]; + if (duplicate.object == host) { + // The host is already registered in the mapper. + // E.g. it might have been added by addContextVersion() + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapper.addHost.sameHost", name)); + } + newHost = duplicate; + } else { + log.error(sm.getString("mapper.duplicateHost", name, duplicate.getRealHostName())); + // Do not add aliases, as removeHost(hostName) won't be able to + // remove them + return; + } + } + List newAliases = new ArrayList<>(aliases.length); + for (String alias : aliases) { + alias = renameWildcardHost(alias); + MappedHost newAlias = new MappedHost(alias, newHost); + if (addHostAliasImpl(newAlias)) { + newAliases.add(newAlias); + } + } + newHost.addAliases(newAliases); + } + + + /** + * Remove a host from the mapper. + * + * @param name Virtual host name + */ + public synchronized void removeHost(String name) { + name = renameWildcardHost(name); + // Find and remove the old host + MappedHost host = exactFind(hosts, name); + if (host == null || host.isAlias()) { + return; + } + MappedHost[] newHosts = hosts.clone(); + // Remove real host and all its aliases + int j = 0; + for (int i = 0; i < newHosts.length; i++) { + if (newHosts[i].getRealHost() != host) { + newHosts[j++] = newHosts[i]; + } + } + hosts = Arrays.copyOf(newHosts, j); + } + + /** + * Add an alias to an existing host. + * + * @param name The name of the host + * @param alias The alias to add + */ + public synchronized void addHostAlias(String name, String alias) { + MappedHost realHost = exactFind(hosts, name); + if (realHost == null) { + // Should not be adding an alias for a host that doesn't exist but + // just in case... + return; + } + alias = renameWildcardHost(alias); + MappedHost newAlias = new MappedHost(alias, realHost); + if (addHostAliasImpl(newAlias)) { + realHost.addAlias(newAlias); + } + } + + private synchronized boolean addHostAliasImpl(MappedHost newAlias) { + MappedHost[] newHosts = new MappedHost[hosts.length + 1]; + if (insertMap(hosts, newHosts, newAlias)) { + hosts = newHosts; + if (newAlias.name.equals(defaultHostName)) { + defaultHost = newAlias; + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapper.addHostAlias.success", newAlias.name, newAlias.getRealHostName())); + } + return true; + } else { + MappedHost duplicate = hosts[find(hosts, newAlias.name)]; + if (duplicate.getRealHost() == newAlias.getRealHost()) { + // A duplicate Alias for the same Host. + // A harmless redundancy. E.g. + // localhost + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapper.addHostAlias.sameHost", newAlias.name, newAlias.getRealHostName())); + } + return false; + } + log.error(sm.getString("mapper.duplicateHostAlias", newAlias.name, newAlias.getRealHostName(), + duplicate.getRealHostName())); + return false; + } + } + + /** + * Remove a host alias + * + * @param alias The alias to remove + */ + public synchronized void removeHostAlias(String alias) { + alias = renameWildcardHost(alias); + // Find and remove the alias + MappedHost hostMapping = exactFind(hosts, alias); + if (hostMapping == null || !hostMapping.isAlias()) { + return; + } + MappedHost[] newHosts = new MappedHost[hosts.length - 1]; + if (removeMap(hosts, newHosts, alias)) { + hosts = newHosts; + hostMapping.getRealHost().removeAlias(hostMapping); + } + + } + + /** + * Replace {@link MappedHost#contextList} field in realHost and all its aliases with a new value. + */ + private void updateContextList(MappedHost realHost, ContextList newContextList) { + + realHost.contextList = newContextList; + for (MappedHost alias : realHost.getAliases()) { + alias.contextList = newContextList; + } + } + + /** + * Add a new Context to an existing Host. + * + * @param hostName Virtual host name this context belongs to + * @param host Host object + * @param path Context path + * @param version Context version + * @param context Context object + * @param welcomeResources Welcome files defined for this context + * @param resources Static resources of the context + * @param wrappers Information on wrapper mappings + */ + public void addContextVersion(String hostName, Host host, String path, String version, Context context, + String[] welcomeResources, WebResourceRoot resources, Collection wrappers) { + + hostName = renameWildcardHost(hostName); + + MappedHost mappedHost = exactFind(hosts, hostName); + if (mappedHost == null) { + addHost(hostName, new String[0], host); + mappedHost = exactFind(hosts, hostName); + if (mappedHost == null) { + log.error(sm.getString("mapper.addContext.noHost", hostName)); + return; + } + } + if (mappedHost.isAlias()) { + log.error(sm.getString("mapper.addContext.hostIsAlias", hostName)); + return; + } + int slashCount = slashCount(path); + synchronized (mappedHost) { + ContextVersion newContextVersion = + new ContextVersion(version, path, slashCount, context, resources, welcomeResources); + if (wrappers != null) { + addWrappers(newContextVersion, wrappers); + } + + ContextList contextList = mappedHost.contextList; + MappedContext mappedContext = exactFind(contextList.contexts, path); + if (mappedContext == null) { + mappedContext = new MappedContext(path, newContextVersion); + ContextList newContextList = contextList.addContext(mappedContext, slashCount); + if (newContextList != null) { + updateContextList(mappedHost, newContextList); + contextObjectToContextVersionMap.put(context, newContextVersion); + } + } else { + ContextVersion[] contextVersions = mappedContext.versions; + ContextVersion[] newContextVersions = new ContextVersion[contextVersions.length + 1]; + if (insertMap(contextVersions, newContextVersions, newContextVersion)) { + mappedContext.versions = newContextVersions; + contextObjectToContextVersionMap.put(context, newContextVersion); + } else { + // Re-registration after Context.reload() + // Replace ContextVersion with the new one + int pos = find(contextVersions, version); + if (pos >= 0 && contextVersions[pos].name.equals(version)) { + contextVersions[pos] = newContextVersion; + contextObjectToContextVersionMap.put(context, newContextVersion); + } + } + } + } + + } + + + /** + * Remove a context from an existing host. + * + * @param ctxt The actual context + * @param hostName Virtual host name this context belongs to + * @param path Context path + * @param version Context version + */ + public void removeContextVersion(Context ctxt, String hostName, String path, String version) { + + hostName = renameWildcardHost(hostName); + contextObjectToContextVersionMap.remove(ctxt); + + MappedHost host = exactFind(hosts, hostName); + if (host == null || host.isAlias()) { + return; + } + + synchronized (host) { + ContextList contextList = host.contextList; + MappedContext context = exactFind(contextList.contexts, path); + if (context == null) { + return; + } + + ContextVersion[] contextVersions = context.versions; + ContextVersion[] newContextVersions = new ContextVersion[contextVersions.length - 1]; + if (removeMap(contextVersions, newContextVersions, version)) { + if (newContextVersions.length == 0) { + // Remove the context + ContextList newContextList = contextList.removeContext(path); + if (newContextList != null) { + updateContextList(host, newContextList); + } + } else { + context.versions = newContextVersions; + } + } + } + } + + + /** + * Mark a context as being reloaded. Reversion of this state is performed by calling + * addContextVersion(...) when context starts up. + * + * @param ctxt The actual context + * @param hostName Virtual host name this context belongs to + * @param contextPath Context path + * @param version Context version + */ + public void pauseContextVersion(Context ctxt, String hostName, String contextPath, String version) { + hostName = renameWildcardHost(hostName); + ContextVersion contextVersion = findContextVersion(hostName, contextPath, version, true); + if (contextVersion == null || !ctxt.equals(contextVersion.object)) { + return; + } + contextVersion.markPaused(); + } + + + private ContextVersion findContextVersion(String hostName, String contextPath, String version, boolean silent) { + MappedHost host = exactFind(hosts, hostName); + if (host == null || host.isAlias()) { + if (!silent) { + log.error(sm.getString("mapper.findContext.noHostOrAlias", hostName)); + } + return null; + } + MappedContext context = exactFind(host.contextList.contexts, contextPath); + if (context == null) { + if (!silent) { + log.error(sm.getString("mapper.findContext.noContext", contextPath)); + } + return null; + } + ContextVersion contextVersion = exactFind(context.versions, version); + if (contextVersion == null) { + if (!silent) { + log.error(sm.getString("mapper.findContext.noContextVersion", contextPath, version)); + } + return null; + } + return contextVersion; + } + + + public void addWrapper(String hostName, String contextPath, String version, String path, Wrapper wrapper, + boolean jspWildCard, boolean resourceOnly) { + hostName = renameWildcardHost(hostName); + ContextVersion contextVersion = findContextVersion(hostName, contextPath, version, false); + if (contextVersion == null) { + return; + } + addWrapper(contextVersion, path, wrapper, jspWildCard, resourceOnly); + } + + public void addWrappers(String hostName, String contextPath, String version, + Collection wrappers) { + hostName = renameWildcardHost(hostName); + ContextVersion contextVersion = findContextVersion(hostName, contextPath, version, false); + if (contextVersion == null) { + return; + } + addWrappers(contextVersion, wrappers); + } + + /** + * Adds wrappers to the given context. + * + * @param contextVersion The context to which to add the wrappers + * @param wrappers Information on wrapper mappings + */ + private void addWrappers(ContextVersion contextVersion, Collection wrappers) { + for (WrapperMappingInfo wrapper : wrappers) { + addWrapper(contextVersion, wrapper.getMapping(), wrapper.getWrapper(), wrapper.isJspWildCard(), + wrapper.isResourceOnly()); + } + } + + /** + * Adds a wrapper to the given context. + * + * @param context The context to which to add the wrapper + * @param path Wrapper mapping + * @param wrapper The Wrapper object + * @param jspWildCard true if the wrapper corresponds to the JspServlet and the mapping path contains a wildcard; + * false otherwise + * @param resourceOnly true if this wrapper always expects a physical resource to be present (such as a JSP) + */ + protected void addWrapper(ContextVersion context, String path, Wrapper wrapper, boolean jspWildCard, + boolean resourceOnly) { + + synchronized (context) { + if (path.endsWith("/*")) { + // Wildcard wrapper + String name = path.substring(0, path.length() - 2); + MappedWrapper newWrapper = new MappedWrapper(name, wrapper, jspWildCard, resourceOnly); + MappedWrapper[] oldWrappers = context.wildcardWrappers; + MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.wildcardWrappers = newWrappers; + int slashCount = slashCount(newWrapper.name); + if (slashCount > context.nesting) { + context.nesting = slashCount; + } + } + } else if (path.startsWith("*.")) { + // Extension wrapper + String name = path.substring(2); + MappedWrapper newWrapper = new MappedWrapper(name, wrapper, jspWildCard, resourceOnly); + MappedWrapper[] oldWrappers = context.extensionWrappers; + MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.extensionWrappers = newWrappers; + } + } else if (path.equals("/")) { + // Default wrapper + MappedWrapper newWrapper = new MappedWrapper("", wrapper, jspWildCard, resourceOnly); + context.defaultWrapper = newWrapper; + } else { + // Exact wrapper + final String name; + if (path.length() == 0) { + // Special case for the Context Root mapping which is + // treated as an exact match + name = "/"; + } else { + name = path; + } + MappedWrapper newWrapper = new MappedWrapper(name, wrapper, jspWildCard, resourceOnly); + MappedWrapper[] oldWrappers = context.exactWrappers; + MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1]; + if (insertMap(oldWrappers, newWrappers, newWrapper)) { + context.exactWrappers = newWrappers; + } + } + } + } + + + /** + * Remove a wrapper from an existing context. + * + * @param hostName Virtual host name this wrapper belongs to + * @param contextPath Context path this wrapper belongs to + * @param version Context version this wrapper belongs to + * @param path Wrapper mapping + */ + public void removeWrapper(String hostName, String contextPath, String version, String path) { + hostName = renameWildcardHost(hostName); + ContextVersion contextVersion = findContextVersion(hostName, contextPath, version, true); + if (contextVersion == null || contextVersion.isPaused()) { + return; + } + removeWrapper(contextVersion, path); + } + + protected void removeWrapper(ContextVersion context, String path) { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("mapper.removeWrapper", context.name, path)); + } + + synchronized (context) { + if (path.endsWith("/*")) { + // Wildcard wrapper + String name = path.substring(0, path.length() - 2); + MappedWrapper[] oldWrappers = context.wildcardWrappers; + if (oldWrappers.length == 0) { + return; + } + MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length - 1]; + if (removeMap(oldWrappers, newWrappers, name)) { + // Recalculate nesting + context.nesting = 0; + for (MappedWrapper newWrapper : newWrappers) { + int slashCount = slashCount(newWrapper.name); + if (slashCount > context.nesting) { + context.nesting = slashCount; + } + } + context.wildcardWrappers = newWrappers; + } + } else if (path.startsWith("*.")) { + // Extension wrapper + String name = path.substring(2); + MappedWrapper[] oldWrappers = context.extensionWrappers; + if (oldWrappers.length == 0) { + return; + } + MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length - 1]; + if (removeMap(oldWrappers, newWrappers, name)) { + context.extensionWrappers = newWrappers; + } + } else if (path.equals("/")) { + // Default wrapper + context.defaultWrapper = null; + } else { + // Exact wrapper + String name; + if (path.length() == 0) { + // Special case for the Context Root mapping which is + // treated as an exact match + name = "/"; + } else { + name = path; + } + MappedWrapper[] oldWrappers = context.exactWrappers; + if (oldWrappers.length == 0) { + return; + } + MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length - 1]; + if (removeMap(oldWrappers, newWrappers, name)) { + context.exactWrappers = newWrappers; + } + } + } + } + + + /** + * Add a welcome file to the given context. + * + * @param hostName The host where the given context can be found + * @param contextPath The path of the given context + * @param version The version of the given context + * @param welcomeFile The welcome file to add + */ + public void addWelcomeFile(String hostName, String contextPath, String version, String welcomeFile) { + hostName = renameWildcardHost(hostName); + ContextVersion contextVersion = findContextVersion(hostName, contextPath, version, false); + if (contextVersion == null) { + return; + } + int len = contextVersion.welcomeResources.length + 1; + String[] newWelcomeResources = new String[len]; + System.arraycopy(contextVersion.welcomeResources, 0, newWelcomeResources, 0, len - 1); + newWelcomeResources[len - 1] = welcomeFile; + contextVersion.welcomeResources = newWelcomeResources; + } + + + /** + * Remove a welcome file from the given context. + * + * @param hostName The host where the given context can be found + * @param contextPath The path of the given context + * @param version The version of the given context + * @param welcomeFile The welcome file to remove + */ + public void removeWelcomeFile(String hostName, String contextPath, String version, String welcomeFile) { + hostName = renameWildcardHost(hostName); + ContextVersion contextVersion = findContextVersion(hostName, contextPath, version, false); + if (contextVersion == null || contextVersion.isPaused()) { + return; + } + int match = -1; + for (int i = 0; i < contextVersion.welcomeResources.length; i++) { + if (welcomeFile.equals(contextVersion.welcomeResources[i])) { + match = i; + break; + } + } + if (match > -1) { + int len = contextVersion.welcomeResources.length - 1; + String[] newWelcomeResources = new String[len]; + System.arraycopy(contextVersion.welcomeResources, 0, newWelcomeResources, 0, match); + if (match < len) { + System.arraycopy(contextVersion.welcomeResources, match + 1, newWelcomeResources, match, len - match); + } + contextVersion.welcomeResources = newWelcomeResources; + } + } + + + /** + * Clear the welcome files for the given context. + * + * @param hostName The host where the context to be cleared can be found + * @param contextPath The path of the context to be cleared + * @param version The version of the context to be cleared + */ + public void clearWelcomeFiles(String hostName, String contextPath, String version) { + hostName = renameWildcardHost(hostName); + ContextVersion contextVersion = findContextVersion(hostName, contextPath, version, false); + if (contextVersion == null) { + return; + } + contextVersion.welcomeResources = new String[0]; + } + + + /** + * Map the specified host name and URI, mutating the given mapping data. + * + * @param host Virtual host name + * @param uri URI + * @param version The version, if any, included in the request to be mapped + * @param mappingData This structure will contain the result of the mapping operation + * + * @throws IOException if the buffers are too small to hold the results of the mapping. + */ + public void map(MessageBytes host, MessageBytes uri, String version, MappingData mappingData) throws IOException { + + if (host.isNull()) { + String defaultHostName = this.defaultHostName; + if (defaultHostName == null) { + return; + } + host.setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + host.getCharChunk().append(defaultHostName); + } + host.toChars(); + uri.toChars(); + internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData); + } + + + /** + * Map the specified URI relative to the context, mutating the given mapping data. + * + * @param context The actual context + * @param uri URI + * @param mappingData This structure will contain the result of the mapping operation + * + * @throws IOException if the buffers are too small to hold the results of the mapping. + */ + public void map(Context context, MessageBytes uri, MappingData mappingData) throws IOException { + + ContextVersion contextVersion = contextObjectToContextVersionMap.get(context); + uri.toChars(); + CharChunk uricc = uri.getCharChunk(); + uricc.setLimit(-1); + internalMapWrapper(contextVersion, uricc, mappingData); + } + + + // -------------------------------------------------------- Private Methods + + /** + * Map the specified URI. + * + * @throws IOException If an error occurs while manipulating the URI during the mapping + */ + private void internalMap(CharChunk host, CharChunk uri, String version, MappingData mappingData) + throws IOException { + + if (mappingData.host != null) { + // The legacy code (dating down at least to Tomcat 4.1) just + // skipped all mapping work in this case. That behaviour has a risk + // of returning an inconsistent result. + // I do not see a valid use case for it. + throw new AssertionError(); + } + + // Virtual host mapping + MappedHost[] hosts = this.hosts; + MappedHost mappedHost = exactFindIgnoreCase(hosts, host); + if (mappedHost == null) { + // Note: Internally, the Mapper does not use the leading * on a + // wildcard host. This is to allow this shortcut. + int firstDot = host.indexOf('.'); + if (firstDot > -1) { + int offset = host.getOffset(); + try { + host.setOffset(firstDot + offset); + mappedHost = exactFindIgnoreCase(hosts, host); + } finally { + // Make absolutely sure this gets reset + host.setOffset(offset); + } + } + if (mappedHost == null) { + mappedHost = defaultHost; + if (mappedHost == null) { + return; + } + } + } + mappingData.host = mappedHost.object; + + if (uri.isNull()) { + // Can't map context or wrapper without a uri + return; + } + + uri.setLimit(-1); + + // Context mapping + ContextList contextList = mappedHost.contextList; + MappedContext[] contexts = contextList.contexts; + int pos = find(contexts, uri); + if (pos == -1) { + return; + } + + int lastSlash = -1; + int uriEnd = uri.getEnd(); + int length = -1; + boolean found = false; + MappedContext context = null; + while (pos >= 0) { + context = contexts[pos]; + if (uri.startsWith(context.name)) { + length = context.name.length(); + if (uri.getLength() == length) { + found = true; + break; + } else if (uri.startsWithIgnoreCase("/", length)) { + found = true; + break; + } + } + if (lastSlash == -1) { + lastSlash = nthSlash(uri, contextList.nesting + 1); + } else { + lastSlash = lastSlash(uri); + } + uri.setEnd(lastSlash); + pos = find(contexts, uri); + } + uri.setEnd(uriEnd); + + if (!found) { + if (contexts[0].name.equals("")) { + context = contexts[0]; + } else { + context = null; + } + } + if (context == null) { + return; + } + + ContextVersion contextVersion = null; + ContextVersion[] contextVersions = context.versions; + final int versionCount = contextVersions.length; + if (versionCount > 1) { + Context[] contextObjects = new Context[contextVersions.length]; + for (int i = 0; i < contextObjects.length; i++) { + contextObjects[i] = contextVersions[i].object; + } + mappingData.contexts = contextObjects; + if (version != null) { + contextVersion = exactFind(contextVersions, version); + } + } + if (contextVersion == null) { + // Return the latest version + // The versions array is known to contain at least one element + contextVersion = contextVersions[versionCount - 1]; + } + mappingData.context = contextVersion.object; + mappingData.contextSlashCount = contextVersion.slashCount; + + // Wrapper mapping + if (!contextVersion.isPaused()) { + internalMapWrapper(contextVersion, uri, mappingData); + } + + } + + + /** + * Wrapper mapping. + * + * @throws IOException if the buffers are too small to hold the results of the mapping. + */ + private void internalMapWrapper(ContextVersion contextVersion, CharChunk path, MappingData mappingData) + throws IOException { + + int pathOffset = path.getOffset(); + int pathEnd = path.getEnd(); + boolean noServletPath = false; + + int length = contextVersion.path.length(); + if (length == (pathEnd - pathOffset)) { + noServletPath = true; + } + int servletPath = pathOffset + length; + path.setOffset(servletPath); + + // Rule 1 -- Exact Match + MappedWrapper[] exactWrappers = contextVersion.exactWrappers; + internalMapExactWrapper(exactWrappers, path, mappingData); + + // Rule 2 -- Prefix Match + boolean checkJspWelcomeFiles = false; + MappedWrapper[] wildcardWrappers = contextVersion.wildcardWrappers; + if (mappingData.wrapper == null) { + internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting, path, mappingData); + if (mappingData.wrapper != null && mappingData.jspWildCard) { + char[] buf = path.getBuffer(); + if (buf[pathEnd - 1] == '/') { + /* + * Path ending in '/' was mapped to JSP servlet based on wildcard match (e.g., as specified in + * url-pattern of a jsp-property-group. Force the context's welcome files, which are interpreted as + * JSP files (since they match the url-pattern), to be considered. See Bugzilla 27664. + */ + mappingData.wrapper = null; + checkJspWelcomeFiles = true; + } else { + // See Bugzilla 27704 + mappingData.wrapperPath.setChars(buf, path.getStart(), path.getLength()); + mappingData.pathInfo.recycle(); + } + } + } + + if (mappingData.wrapper == null && noServletPath && + contextVersion.object.getMapperContextRootRedirectEnabled()) { + // The path is empty, redirect to "/" + path.append('/'); + pathEnd = path.getEnd(); + mappingData.redirectPath.setChars(path.getBuffer(), pathOffset, pathEnd - pathOffset); + path.setEnd(pathEnd - 1); + return; + } + + // Rule 3 -- Extension Match + MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers; + if (mappingData.wrapper == null && !checkJspWelcomeFiles) { + internalMapExtensionWrapper(extensionWrappers, path, mappingData, true); + } + + // Rule 4 -- Welcome resources processing for servlets + if (mappingData.wrapper == null) { + boolean checkWelcomeFiles = checkJspWelcomeFiles; + if (!checkWelcomeFiles) { + char[] buf = path.getBuffer(); + checkWelcomeFiles = (buf[pathEnd - 1] == '/'); + } + if (checkWelcomeFiles) { + for (int i = 0; (i < contextVersion.welcomeResources.length) && (mappingData.wrapper == null); i++) { + path.setOffset(pathOffset); + path.setEnd(pathEnd); + path.append(contextVersion.welcomeResources[i], 0, contextVersion.welcomeResources[i].length()); + path.setOffset(servletPath); + + // Rule 4a -- Welcome resources processing for exact macth + internalMapExactWrapper(exactWrappers, path, mappingData); + + // Rule 4b -- Welcome resources processing for prefix match + if (mappingData.wrapper == null) { + internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting, path, mappingData); + } + + // Rule 4c -- Welcome resources processing + // for physical folder + if (mappingData.wrapper == null && contextVersion.resources != null) { + String pathStr = path.toString(); + WebResource file = contextVersion.resources.getResource(pathStr); + if (file != null && file.isFile()) { + internalMapExtensionWrapper(extensionWrappers, path, mappingData, true); + if (mappingData.wrapper == null && contextVersion.defaultWrapper != null) { + mappingData.wrapper = contextVersion.defaultWrapper.object; + mappingData.requestPath.setChars(path.getBuffer(), path.getStart(), path.getLength()); + mappingData.wrapperPath.setChars(path.getBuffer(), path.getStart(), path.getLength()); + mappingData.requestPath.setString(pathStr); + mappingData.wrapperPath.setString(pathStr); + } + } + } + } + + path.setOffset(servletPath); + path.setEnd(pathEnd); + } + + } + + /* + * welcome file processing - take 2 Now that we have looked for welcome files with a physical backing, now look + * for an extension mapping listed but may not have a physical backing to it. This is for the case of index.jsf, + * index.do, etc. A watered down version of rule 4 + */ + if (mappingData.wrapper == null) { + boolean checkWelcomeFiles = checkJspWelcomeFiles; + if (!checkWelcomeFiles) { + char[] buf = path.getBuffer(); + checkWelcomeFiles = (buf[pathEnd - 1] == '/'); + } + if (checkWelcomeFiles) { + for (int i = 0; (i < contextVersion.welcomeResources.length) && (mappingData.wrapper == null); i++) { + path.setOffset(pathOffset); + path.setEnd(pathEnd); + path.append(contextVersion.welcomeResources[i], 0, contextVersion.welcomeResources[i].length()); + path.setOffset(servletPath); + internalMapExtensionWrapper(extensionWrappers, path, mappingData, false); + } + + path.setOffset(servletPath); + path.setEnd(pathEnd); + } + } + + + // Rule 7 -- Default servlet + if (mappingData.wrapper == null && !checkJspWelcomeFiles) { + if (contextVersion.defaultWrapper != null) { + mappingData.wrapper = contextVersion.defaultWrapper.object; + mappingData.requestPath.setChars(path.getBuffer(), path.getStart(), path.getLength()); + mappingData.wrapperPath.setChars(path.getBuffer(), path.getStart(), path.getLength()); + mappingData.matchType = MappingMatch.DEFAULT; + } + // Redirection to a folder + char[] buf = path.getBuffer(); + if (contextVersion.resources != null && buf[pathEnd - 1] != '/') { + String pathStr = path.toString(); + // Note: Check redirect first to save unnecessary getResource() + // call. See BZ 62968. + if (contextVersion.object.getMapperDirectoryRedirectEnabled()) { + WebResource file; + // Handle context root + if (pathStr.length() == 0) { + file = contextVersion.resources.getResource("/"); + } else { + file = contextVersion.resources.getResource(pathStr); + } + if (file != null && file.isDirectory()) { + // Note: this mutates the path: do not do any processing + // after this (since we set the redirectPath, there + // shouldn't be any) + path.setOffset(pathOffset); + path.append('/'); + mappingData.redirectPath.setChars(path.getBuffer(), path.getStart(), path.getLength()); + } else { + mappingData.requestPath.setString(pathStr); + mappingData.wrapperPath.setString(pathStr); + } + } else { + mappingData.requestPath.setString(pathStr); + mappingData.wrapperPath.setString(pathStr); + } + } + } + + path.setOffset(pathOffset); + path.setEnd(pathEnd); + } + + + /** + * Exact mapping. + */ + private void internalMapExactWrapper(MappedWrapper[] wrappers, CharChunk path, MappingData mappingData) { + MappedWrapper wrapper = exactFind(wrappers, path); + if (wrapper != null) { + mappingData.requestPath.setString(wrapper.name); + mappingData.wrapper = wrapper.object; + if (path.equals("/")) { + // Special handling for Context Root mapped servlet + mappingData.pathInfo.setString("/"); + mappingData.wrapperPath.setString(""); + mappingData.matchType = MappingMatch.CONTEXT_ROOT; + } else { + mappingData.wrapperPath.setString(wrapper.name); + mappingData.matchType = MappingMatch.EXACT; + } + } + } + + + /** + * Wildcard mapping. + */ + private void internalMapWildcardWrapper(MappedWrapper[] wrappers, int nesting, CharChunk path, + MappingData mappingData) { + + int pathEnd = path.getEnd(); + + int lastSlash = -1; + int length = -1; + int pos = find(wrappers, path); + if (pos != -1) { + boolean found = false; + while (pos >= 0) { + if (path.startsWith(wrappers[pos].name)) { + length = wrappers[pos].name.length(); + if (path.getLength() == length) { + found = true; + break; + } else if (path.startsWithIgnoreCase("/", length)) { + found = true; + break; + } + } + if (lastSlash == -1) { + lastSlash = nthSlash(path, nesting + 1); + } else { + lastSlash = lastSlash(path); + } + path.setEnd(lastSlash); + pos = find(wrappers, path); + } + path.setEnd(pathEnd); + if (found) { + mappingData.wrapperPath.setString(wrappers[pos].name); + if (path.getLength() > length) { + mappingData.pathInfo.setChars(path.getBuffer(), path.getOffset() + length, + path.getLength() - length); + } + mappingData.requestPath.setChars(path.getBuffer(), path.getOffset(), path.getLength()); + mappingData.wrapper = wrappers[pos].object; + mappingData.jspWildCard = wrappers[pos].jspWildCard; + mappingData.matchType = MappingMatch.PATH; + } + } + } + + + /** + * Extension mappings. + * + * @param wrappers Set of wrappers to check for matches + * @param path Path to map + * @param mappingData Mapping data for result + * @param resourceExpected Is this mapping expecting to find a resource + */ + private void internalMapExtensionWrapper(MappedWrapper[] wrappers, CharChunk path, MappingData mappingData, + boolean resourceExpected) { + char[] buf = path.getBuffer(); + int pathEnd = path.getEnd(); + int servletPath = path.getOffset(); + int slash = -1; + for (int i = pathEnd - 1; i >= servletPath; i--) { + if (buf[i] == '/') { + slash = i; + break; + } + } + if (slash >= 0) { + int period = -1; + for (int i = pathEnd - 1; i > slash; i--) { + if (buf[i] == '.') { + period = i; + break; + } + } + if (period >= 0) { + path.setOffset(period + 1); + path.setEnd(pathEnd); + MappedWrapper wrapper = exactFind(wrappers, path); + if (wrapper != null && (resourceExpected || !wrapper.resourceOnly)) { + mappingData.wrapperPath.setChars(buf, servletPath, pathEnd - servletPath); + mappingData.requestPath.setChars(buf, servletPath, pathEnd - servletPath); + mappingData.wrapper = wrapper.object; + mappingData.matchType = MappingMatch.EXTENSION; + } + path.setOffset(servletPath); + path.setEnd(pathEnd); + } + } + } + + + /** + * Find a map element given its name in a sorted array of map elements. This will return the index for the closest + * inferior or equal item in the given array. + */ + private static int find(MapElement[] map, CharChunk name) { + return find(map, name, name.getStart(), name.getEnd()); + } + + + /** + * Find a map element given its name in a sorted array of map elements. This will return the index for the closest + * inferior or equal item in the given array. + */ + private static int find(MapElement[] map, CharChunk name, int start, int end) { + + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (compare(name, start, end, map[0].name) < 0) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) >>> 1; + int result = compare(name, start, end, map[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compare(name, start, end, map[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + /** + * Find a map element given its name in a sorted array of map elements. This will return the index for the closest + * inferior or equal item in the given array. + */ + private static int findIgnoreCase(MapElement[] map, CharChunk name) { + return findIgnoreCase(map, name, name.getStart(), name.getEnd()); + } + + + /** + * Find a map element given its name in a sorted array of map elements. This will return the index for the closest + * inferior or equal item in the given array. + */ + private static int findIgnoreCase(MapElement[] map, CharChunk name, int start, int end) { + + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + if (compareIgnoreCase(name, start, end, map[0].name) < 0) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) >>> 1; + int result = compareIgnoreCase(name, start, end, map[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compareIgnoreCase(name, start, end, map[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + + /** + * Find a map element given its name in a sorted array of map elements. This will return the index for the closest + * inferior or equal item in the given array. + * + * @see #exactFind(MapElement[], String) + */ + private static int find(MapElement[] map, String name) { + + int a = 0; + int b = map.length - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (name.compareTo(map[0].name) < 0) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) >>> 1; + int result = name.compareTo(map[i].name); + if (result > 0) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = name.compareTo(map[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + + /** + * Find a map element given its name in a sorted array of map elements. This will return the element that you were + * searching for. Otherwise it will return null. + * + * @see #find(MapElement[], String) + */ + private static > E exactFind(E[] map, String name) { + int pos = find(map, name); + if (pos >= 0) { + E result = map[pos]; + if (name.equals(result.name)) { + return result; + } + } + return null; + } + + /** + * Find a map element given its name in a sorted array of map elements. This will return the element that you were + * searching for. Otherwise it will return null. + */ + private static > E exactFind(E[] map, CharChunk name) { + int pos = find(map, name); + if (pos >= 0) { + E result = map[pos]; + if (name.equals(result.name)) { + return result; + } + } + return null; + } + + /** + * Find a map element given its name in a sorted array of map elements. This will return the element that you were + * searching for. Otherwise it will return null. + * + * @see #findIgnoreCase(MapElement[], CharChunk) + */ + private static > E exactFindIgnoreCase(E[] map, CharChunk name) { + int pos = findIgnoreCase(map, name); + if (pos >= 0) { + E result = map[pos]; + if (name.equalsIgnoreCase(result.name)) { + return result; + } + } + return null; + } + + + /** + * Compare given char chunk with String. Return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + private static int compare(CharChunk name, int start, int end, String compareTo) { + int result = 0; + char[] c = name.getBuffer(); + int compareLen = compareTo.length(); + int len = compareLen; + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + char nameChar = c[i + start]; + char compareToChar = compareTo.charAt(i); + if (nameChar > compareToChar) { + result = 1; + } else if (nameChar < compareToChar) { + result = -1; + } + } + if (result == 0) { + if (compareLen > (end - start)) { + result = -1; + } else if (compareLen < (end - start)) { + result = 1; + } + } + return result; + } + + + /** + * Compare given char chunk with String ignoring case. Return -1, 0 or +1 if inferior, equal, or superior to the + * String. + */ + private static int compareIgnoreCase(CharChunk name, int start, int end, String compareTo) { + int result = 0; + char[] c = name.getBuffer(); + int compareLen = compareTo.length(); + int len = compareLen; + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + int nameLower = Ascii.toLower(c[i + start]); + int compareLower = Ascii.toLower(compareTo.charAt(i)); + if (nameLower > compareLower) { + result = 1; + } else if (nameLower < compareLower) { + result = -1; + } + } + if (result == 0) { + if (compareLen > (end - start)) { + result = -1; + } else if (compareLen < (end - start)) { + result = 1; + } + } + return result; + } + + + /** + * Find the position of the last slash in the given char chunk. + */ + private static int lastSlash(CharChunk name) { + char[] c = name.getBuffer(); + int end = name.getEnd(); + int start = name.getStart(); + int pos = end; + + while (pos > start) { + if (c[--pos] == '/') { + break; + } + } + + return pos; + } + + + /** + * Find the position of the nth slash, in the given char chunk. + */ + private static int nthSlash(CharChunk name, int n) { + char[] c = name.getBuffer(); + int end = name.getEnd(); + int start = name.getStart(); + int pos = start; + int count = 0; + + while (pos < end) { + if ((c[pos++] == '/') && ((++count) == n)) { + pos--; + break; + } + } + + return pos; + } + + + /** + * Return the slash count in a given string. + */ + private static int slashCount(String name) { + int pos = -1; + int count = 0; + while ((pos = name.indexOf('/', pos + 1)) != -1) { + count++; + } + return count; + } + + + /** + * Insert into the right place in a sorted MapElement array, and prevent duplicates. + */ + private static boolean insertMap(MapElement[] oldMap, MapElement[] newMap, MapElement newElement) { + int pos = find(oldMap, newElement.name); + if ((pos != -1) && (newElement.name.equals(oldMap[pos].name))) { + return false; + } + System.arraycopy(oldMap, 0, newMap, 0, pos + 1); + newMap[pos + 1] = newElement; + System.arraycopy(oldMap, pos + 1, newMap, pos + 2, oldMap.length - pos - 1); + return true; + } + + + /** + * Insert into the right place in a sorted MapElement array. + */ + private static boolean removeMap(MapElement[] oldMap, MapElement[] newMap, String name) { + int pos = find(oldMap, name); + if ((pos != -1) && (name.equals(oldMap[pos].name))) { + System.arraycopy(oldMap, 0, newMap, 0, pos); + System.arraycopy(oldMap, pos + 1, newMap, pos, oldMap.length - pos - 1); + return true; + } + return false; + } + + + /* + * To simplify the mapping process, wild card hosts take the form ".apache.org" rather than "*.apache.org" + * internally. However, for ease of use the external form remains "*.apache.org". Any host name passed into this + * class needs to be passed through this method to rename and wild card host names from the external to internal + * form. + */ + private static String renameWildcardHost(String hostName) { + if (hostName != null && hostName.startsWith("*.")) { + return hostName.substring(1); + } else { + return hostName; + } + } + + + // ------------------------------------------------- MapElement Inner Class + + + protected abstract static class MapElement { + + public final String name; + public final T object; + + public MapElement(String name, T object) { + this.name = name; + this.object = object; + } + } + + + // ------------------------------------------------------- Host Inner Class + + + protected static final class MappedHost extends MapElement { + + public volatile ContextList contextList; + + /** + * Link to the "real" MappedHost, shared by all aliases. + */ + private final MappedHost realHost; + + /** + * Links to all registered aliases, for easy enumeration. This field is available only in the "real" MappedHost. + * In an alias this field is null. + */ + private final List aliases; + + /** + * Constructor used for the primary Host + * + * @param name The name of the virtual host + * @param host The host + */ + public MappedHost(String name, Host host) { + super(name, host); + realHost = this; + contextList = new ContextList(); + aliases = new CopyOnWriteArrayList<>(); + } + + /** + * Constructor used for an Alias + * + * @param alias The alias of the virtual host + * @param realHost The host the alias points to + */ + public MappedHost(String alias, MappedHost realHost) { + super(alias, realHost.object); + this.realHost = realHost; + this.contextList = realHost.contextList; + this.aliases = null; + } + + public boolean isAlias() { + return realHost != this; + } + + public MappedHost getRealHost() { + return realHost; + } + + public String getRealHostName() { + return realHost.name; + } + + public Collection getAliases() { + return aliases; + } + + public void addAlias(MappedHost alias) { + aliases.add(alias); + } + + public void addAliases(Collection c) { + aliases.addAll(c); + } + + public void removeAlias(MappedHost alias) { + aliases.remove(alias); + } + } + + + // ------------------------------------------------ ContextList Inner Class + + + protected static final class ContextList { + + public final MappedContext[] contexts; + public final int nesting; + + public ContextList() { + this(new MappedContext[0], 0); + } + + private ContextList(MappedContext[] contexts, int nesting) { + this.contexts = contexts; + this.nesting = nesting; + } + + public ContextList addContext(MappedContext mappedContext, int slashCount) { + MappedContext[] newContexts = new MappedContext[contexts.length + 1]; + if (insertMap(contexts, newContexts, mappedContext)) { + return new ContextList(newContexts, Math.max(nesting, slashCount)); + } + return null; + } + + public ContextList removeContext(String path) { + MappedContext[] newContexts = new MappedContext[contexts.length - 1]; + if (removeMap(contexts, newContexts, path)) { + int newNesting = 0; + for (MappedContext context : newContexts) { + newNesting = Math.max(newNesting, slashCount(context.name)); + } + return new ContextList(newContexts, newNesting); + } + return null; + } + } + + + // ---------------------------------------------------- Context Inner Class + + + protected static final class MappedContext extends MapElement { + public volatile ContextVersion[] versions; + + public MappedContext(String name, ContextVersion firstVersion) { + super(name, null); + this.versions = new ContextVersion[] { firstVersion }; + } + } + + protected static final class ContextVersion extends MapElement { + public final String path; + public final int slashCount; + public final WebResourceRoot resources; + public String[] welcomeResources; + public MappedWrapper defaultWrapper = null; + public MappedWrapper[] exactWrappers = new MappedWrapper[0]; + public MappedWrapper[] wildcardWrappers = new MappedWrapper[0]; + public MappedWrapper[] extensionWrappers = new MappedWrapper[0]; + public int nesting = 0; + private volatile boolean paused; + + public ContextVersion(String version, String path, int slashCount, Context context, WebResourceRoot resources, + String[] welcomeResources) { + super(version, context); + this.path = path; + this.slashCount = slashCount; + this.resources = resources; + this.welcomeResources = welcomeResources; + } + + public boolean isPaused() { + return paused; + } + + public void markPaused() { + paused = true; + } + } + + // ---------------------------------------------------- Wrapper Inner Class + + + protected static class MappedWrapper extends MapElement { + + public final boolean jspWildCard; + public final boolean resourceOnly; + + public MappedWrapper(String name, Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) { + super(name, wrapper); + this.jspWildCard = jspWildCard; + this.resourceOnly = resourceOnly; + } + } +} diff --git a/java/org/apache/catalina/mapper/MapperListener.java b/java/org/apache/catalina/mapper/MapperListener.java new file mode 100644 index 0000000..464fe49 --- /dev/null +++ b/java/org/apache/catalina/mapper/MapperListener.java @@ -0,0 +1,512 @@ +/* + * 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.catalina.mapper; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.Container; +import org.apache.catalina.ContainerEvent; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Service; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.Wrapper; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Mapper listener. + * + * @author Remy Maucherat + * @author Costin Manolache + */ +public class MapperListener extends LifecycleMBeanBase implements ContainerListener, LifecycleListener { + + + private static final Log log = LogFactory.getLog(MapperListener.class); + + + // ----------------------------------------------------- Instance Variables + /** + * Associated mapper. + */ + private final Mapper mapper; + + /** + * Associated service + */ + private final Service service; + + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(Constants.Package); + + /** + * The domain (effectively the engine) this mapper is associated with + */ + private final String domain = null; + + + // ----------------------------------------------------------- Constructors + + /** + * Create mapper listener. + * + * @param service The service this listener is associated with + */ + public MapperListener(Service service) { + this.service = service; + this.mapper = service.getMapper(); + } + + + // ------------------------------------------------------- Lifecycle Methods + + @Override + public void startInternal() throws LifecycleException { + + setState(LifecycleState.STARTING); + + Engine engine = service.getContainer(); + if (engine == null) { + return; + } + + findDefaultHost(); + + addListeners(engine); + + Container[] conHosts = engine.findChildren(); + for (Container conHost : conHosts) { + Host host = (Host) conHost; + if (!LifecycleState.NEW.equals(host.getState())) { + // Registering the host will register the context and wrappers + registerHost(host); + } + } + } + + + @Override + public void stopInternal() throws LifecycleException { + setState(LifecycleState.STOPPING); + + Engine engine = service.getContainer(); + if (engine == null) { + return; + } + removeListeners(engine); + } + + + @Override + protected String getDomainInternal() { + if (service instanceof LifecycleMBeanBase) { + return service.getDomain(); + } else { + return null; + } + } + + + @Override + protected String getObjectNameKeyProperties() { + // Same as connector but Mapper rather than Connector + return "type=Mapper"; + } + + // --------------------------------------------- Container Listener methods + + @Override + public void containerEvent(ContainerEvent event) { + + if (Container.ADD_CHILD_EVENT.equals(event.getType())) { + Container child = (Container) event.getData(); + addListeners(child); + // If child is started then it is too late for life-cycle listener + // to register the child so register it here + if (child.getState().isAvailable()) { + if (child instanceof Host) { + registerHost((Host) child); + } else if (child instanceof Context) { + registerContext((Context) child); + } else if (child instanceof Wrapper) { + // Only if the Context has started. If it has not, then it + // will have its own "after_start" life-cycle event later. + if (child.getParent().getState().isAvailable()) { + registerWrapper((Wrapper) child); + } + } + } + } else if (Container.REMOVE_CHILD_EVENT.equals(event.getType())) { + Container child = (Container) event.getData(); + removeListeners(child); + // No need to unregister - life-cycle listener will handle this when + // the child stops + } else if (Host.ADD_ALIAS_EVENT.equals(event.getType())) { + // Handle dynamically adding host aliases + mapper.addHostAlias(((Host) event.getSource()).getName(), event.getData().toString()); + } else if (Host.REMOVE_ALIAS_EVENT.equals(event.getType())) { + // Handle dynamically removing host aliases + mapper.removeHostAlias(event.getData().toString()); + } else if (Wrapper.ADD_MAPPING_EVENT.equals(event.getType())) { + // Handle dynamically adding wrappers + Wrapper wrapper = (Wrapper) event.getSource(); + Context context = (Context) wrapper.getParent(); + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = ""; + } + String version = context.getWebappVersion(); + String hostName = context.getParent().getName(); + String wrapperName = wrapper.getName(); + String mapping = (String) event.getData(); + boolean jspWildCard = ("jsp".equals(wrapperName) && mapping.endsWith("/*")); + mapper.addWrapper(hostName, contextPath, version, mapping, wrapper, jspWildCard, + context.isResourceOnlyServlet(wrapperName)); + } else if (Wrapper.REMOVE_MAPPING_EVENT.equals(event.getType())) { + // Handle dynamically removing wrappers + Wrapper wrapper = (Wrapper) event.getSource(); + + Context context = (Context) wrapper.getParent(); + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = ""; + } + String version = context.getWebappVersion(); + String hostName = context.getParent().getName(); + + String mapping = (String) event.getData(); + + mapper.removeWrapper(hostName, contextPath, version, mapping); + } else if (Context.ADD_WELCOME_FILE_EVENT.equals(event.getType())) { + // Handle dynamically adding welcome files + Context context = (Context) event.getSource(); + + String hostName = context.getParent().getName(); + + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = ""; + } + + String welcomeFile = (String) event.getData(); + + mapper.addWelcomeFile(hostName, contextPath, context.getWebappVersion(), welcomeFile); + } else if (Context.REMOVE_WELCOME_FILE_EVENT.equals(event.getType())) { + // Handle dynamically removing welcome files + Context context = (Context) event.getSource(); + + String hostName = context.getParent().getName(); + + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = ""; + } + + String welcomeFile = (String) event.getData(); + + mapper.removeWelcomeFile(hostName, contextPath, context.getWebappVersion(), welcomeFile); + } else if (Context.CLEAR_WELCOME_FILES_EVENT.equals(event.getType())) { + // Handle dynamically clearing welcome files + Context context = (Context) event.getSource(); + + String hostName = context.getParent().getName(); + + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = ""; + } + + mapper.clearWelcomeFiles(hostName, contextPath, context.getWebappVersion()); + } + } + + + // ------------------------------------------------------ Protected Methods + + private void findDefaultHost() { + + Engine engine = service.getContainer(); + String defaultHost = engine.getDefaultHost(); + + boolean found = false; + + if (defaultHost != null && defaultHost.length() > 0) { + Container[] containers = engine.findChildren(); + + for (Container container : containers) { + Host host = (Host) container; + if (defaultHost.equalsIgnoreCase(host.getName())) { + found = true; + break; + } + + String[] aliases = host.findAliases(); + for (String alias : aliases) { + if (defaultHost.equalsIgnoreCase(alias)) { + found = true; + break; + } + } + } + } + + if (found) { + mapper.setDefaultHostName(defaultHost); + } else { + log.error(sm.getString("mapperListener.unknownDefaultHost", defaultHost, service)); + } + } + + + /** + * Register host. + */ + private void registerHost(Host host) { + + String[] aliases = host.findAliases(); + mapper.addHost(host.getName(), aliases, host); + + for (Container container : host.findChildren()) { + if (container.getState().isAvailable()) { + registerContext((Context) container); + } + } + + // Default host may have changed + findDefaultHost(); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapperListener.registerHost", host.getName(), domain, service)); + } + } + + + /** + * Unregister host. + */ + private void unregisterHost(Host host) { + + String hostname = host.getName(); + + mapper.removeHost(hostname); + + // Default host may have changed + findDefaultHost(); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapperListener.unregisterHost", hostname, domain, service)); + } + } + + + /** + * Unregister wrapper. + */ + private void unregisterWrapper(Wrapper wrapper) { + + Context context = ((Context) wrapper.getParent()); + String contextPath = context.getPath(); + String wrapperName = wrapper.getName(); + + if ("/".equals(contextPath)) { + contextPath = ""; + } + String version = context.getWebappVersion(); + String hostName = context.getParent().getName(); + + String[] mappings = wrapper.findMappings(); + + for (String mapping : mappings) { + mapper.removeWrapper(hostName, contextPath, version, mapping); + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapperListener.unregisterWrapper", wrapperName, contextPath, service)); + } + } + + + /** + * Register context. + */ + private void registerContext(Context context) { + + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = ""; + } + Host host = (Host) context.getParent(); + + WebResourceRoot resources = context.getResources(); + String[] welcomeFiles = context.findWelcomeFiles(); + List wrappers = new ArrayList<>(); + + for (Container container : context.findChildren()) { + prepareWrapperMappingInfo(context, (Wrapper) container, wrappers); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapperListener.registerWrapper", container.getName(), contextPath, service)); + } + } + + mapper.addContextVersion(host.getName(), host, contextPath, context.getWebappVersion(), context, welcomeFiles, + resources, wrappers); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapperListener.registerContext", contextPath, service)); + } + } + + + /** + * Unregister context. + */ + private void unregisterContext(Context context) { + + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = ""; + } + String hostName = context.getParent().getName(); + + if (context.getPaused()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapperListener.pauseContext", contextPath, service)); + } + + mapper.pauseContextVersion(context, hostName, contextPath, context.getWebappVersion()); + } else { + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapperListener.unregisterContext", contextPath, service)); + } + + mapper.removeContextVersion(context, hostName, contextPath, context.getWebappVersion()); + } + } + + + /** + * Register wrapper. + */ + private void registerWrapper(Wrapper wrapper) { + + Context context = (Context) wrapper.getParent(); + String contextPath = context.getPath(); + if ("/".equals(contextPath)) { + contextPath = ""; + } + String version = context.getWebappVersion(); + String hostName = context.getParent().getName(); + + List wrappers = new ArrayList<>(); + prepareWrapperMappingInfo(context, wrapper, wrappers); + mapper.addWrappers(hostName, contextPath, version, wrappers); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("mapperListener.registerWrapper", wrapper.getName(), contextPath, service)); + } + } + + /* + * Populate wrappers list with information for registration of mappings for this wrapper in this + * context. + */ + private void prepareWrapperMappingInfo(Context context, Wrapper wrapper, List wrappers) { + String wrapperName = wrapper.getName(); + boolean resourceOnly = context.isResourceOnlyServlet(wrapperName); + String[] mappings = wrapper.findMappings(); + for (String mapping : mappings) { + boolean jspWildCard = (wrapperName.equals("jsp") && mapping.endsWith("/*")); + wrappers.add(new WrapperMappingInfo(mapping, wrapper, jspWildCard, resourceOnly)); + } + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getType().equals(AFTER_START_EVENT)) { + Object obj = event.getSource(); + if (obj instanceof Wrapper) { + Wrapper w = (Wrapper) obj; + // Only if the Context has started. If it has not, then it will + // have its own "after_start" event later. + if (w.getParent().getState().isAvailable()) { + registerWrapper(w); + } + } else if (obj instanceof Context) { + Context c = (Context) obj; + // Only if the Host has started. If it has not, then it will + // have its own "after_start" event later. + if (c.getParent().getState().isAvailable()) { + registerContext(c); + } + } else if (obj instanceof Host) { + registerHost((Host) obj); + } + } else if (event.getType().equals(BEFORE_STOP_EVENT)) { + Object obj = event.getSource(); + if (obj instanceof Wrapper) { + unregisterWrapper((Wrapper) obj); + } else if (obj instanceof Context) { + unregisterContext((Context) obj); + } else if (obj instanceof Host) { + unregisterHost((Host) obj); + } + } + } + + + /** + * Add this mapper to the container and all child containers + * + * @param container the container (and any associated children) to which the mapper is to be added + */ + private void addListeners(Container container) { + container.addContainerListener(this); + container.addLifecycleListener(this); + for (Container child : container.findChildren()) { + addListeners(child); + } + } + + + /** + * Remove this mapper from the container and all child containers + * + * @param container the container (and any associated children) from which the mapper is to be removed + */ + private void removeListeners(Container container) { + container.removeContainerListener(this); + container.removeLifecycleListener(this); + for (Container child : container.findChildren()) { + removeListeners(child); + } + } +} diff --git a/java/org/apache/catalina/mapper/MappingData.java b/java/org/apache/catalina/mapper/MappingData.java new file mode 100644 index 0000000..777fab9 --- /dev/null +++ b/java/org/apache/catalina/mapper/MappingData.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.mapper; + +import jakarta.servlet.http.MappingMatch; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Wrapper; +import org.apache.tomcat.util.buf.MessageBytes; + +/** + * Mapping data. + * + * @author Remy Maucherat + */ +public class MappingData { + + public Host host = null; + public Context context = null; + public int contextSlashCount = 0; + public Context[] contexts = null; + public Wrapper wrapper = null; + public boolean jspWildCard = false; + + public final MessageBytes requestPath = MessageBytes.newInstance(); + public final MessageBytes wrapperPath = MessageBytes.newInstance(); + public final MessageBytes pathInfo = MessageBytes.newInstance(); + + public final MessageBytes redirectPath = MessageBytes.newInstance(); + + // Fields used by ApplicationMapping to implement jakarta.servlet.http.HttpServletMapping + public MappingMatch matchType = null; + + public void recycle() { + host = null; + context = null; + contextSlashCount = 0; + contexts = null; + wrapper = null; + jspWildCard = false; + requestPath.recycle(); + wrapperPath.recycle(); + pathInfo.recycle(); + redirectPath.recycle(); + matchType = null; + } +} diff --git a/java/org/apache/catalina/mapper/WrapperMappingInfo.java b/java/org/apache/catalina/mapper/WrapperMappingInfo.java new file mode 100644 index 0000000..1b2f0d4 --- /dev/null +++ b/java/org/apache/catalina/mapper/WrapperMappingInfo.java @@ -0,0 +1,54 @@ +/* + * 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.catalina.mapper; + +import org.apache.catalina.Wrapper; + +/** + * Encapsulates information used to register a Wrapper mapping. + */ +public class WrapperMappingInfo { + + private final String mapping; + private final Wrapper wrapper; + private final boolean jspWildCard; + private final boolean resourceOnly; + + public WrapperMappingInfo(String mapping, Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) { + this.mapping = mapping; + this.wrapper = wrapper; + this.jspWildCard = jspWildCard; + this.resourceOnly = resourceOnly; + } + + public String getMapping() { + return mapping; + } + + public Wrapper getWrapper() { + return wrapper; + } + + public boolean isJspWildCard() { + return jspWildCard; + } + + public boolean isResourceOnly() { + return resourceOnly; + } + +} diff --git a/java/org/apache/catalina/mapper/mbeans-descriptors.xml b/java/org/apache/catalina/mapper/mbeans-descriptors.xml new file mode 100644 index 0000000..8cbc77e --- /dev/null +++ b/java/org/apache/catalina/mapper/mbeans-descriptors.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/mbeans-descriptors.xml b/java/org/apache/catalina/mbeans-descriptors.xml new file mode 100644 index 0000000..c2584d8 --- /dev/null +++ b/java/org/apache/catalina/mbeans-descriptors.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/mbeans/BaseCatalinaMBean.java b/java/org/apache/catalina/mbeans/BaseCatalinaMBean.java new file mode 100644 index 0000000..590c707 --- /dev/null +++ b/java/org/apache/catalina/mbeans/BaseCatalinaMBean.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.mbeans; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanException; +import javax.management.RuntimeOperationsException; +import javax.management.modelmbean.InvalidTargetObjectTypeException; + +import org.apache.tomcat.util.modeler.BaseModelMBean; + +public abstract class BaseCatalinaMBean extends BaseModelMBean { + + protected T doGetManagedResource() throws MBeanException { + try { + @SuppressWarnings("unchecked") + T resource = (T) getManagedResource(); + return resource; + } catch (InstanceNotFoundException | RuntimeOperationsException | InvalidTargetObjectTypeException e) { + throw new MBeanException(e); + } + } + + + protected static Object newInstance(String type) throws MBeanException { + try { + return Class.forName(type).getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new MBeanException(e); + } + } +} diff --git a/java/org/apache/catalina/mbeans/ClassNameMBean.java b/java/org/apache/catalina/mbeans/ClassNameMBean.java new file mode 100644 index 0000000..bdf7820 --- /dev/null +++ b/java/org/apache/catalina/mbeans/ClassNameMBean.java @@ -0,0 +1,41 @@ +/* + * 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.catalina.mbeans; + +/** + *

    + * A convenience base class for ModelMBean implementations where the underlying base class (and + * therefore the set of supported properties) is different for varying implementations of a standard interface. For + * Catalina, that includes at least the following: Connector, Logger, Realm, and Valve. This class creates an artificial + * MBean attribute named className, which reports the fully qualified class name of the managed object as + * its value. + *

    + * + * @param The type that this bean represents. + * + * @author Craig R. McClanahan + */ +public class ClassNameMBean extends BaseCatalinaMBean { + + /** + * Return the fully qualified Java class name of the managed object for this MBean. + */ + @Override + public String getClassName() { + return this.resource.getClass().getName(); + } +} diff --git a/java/org/apache/catalina/mbeans/ConnectorMBean.java b/java/org/apache/catalina/mbeans/ConnectorMBean.java new file mode 100644 index 0000000..7cd1a45 --- /dev/null +++ b/java/org/apache/catalina/mbeans/ConnectorMBean.java @@ -0,0 +1,92 @@ +/* + * 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.catalina.mbeans; + +import javax.management.Attribute; +import javax.management.AttributeNotFoundException; +import javax.management.MBeanException; +import javax.management.ReflectionException; +import javax.management.RuntimeOperationsException; + +import org.apache.catalina.connector.Connector; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * A ModelMBean implementation for the org.apache.coyote.tomcat5.CoyoteConnector + * component. + *

    + * + * @author Amy Roh + */ +public class ConnectorMBean extends ClassNameMBean { + + private static final StringManager sm = StringManager.getManager(ConnectorMBean.class); + + /** + * Obtain and return the value of a specific attribute of this MBean. + * + * @param name Name of the requested attribute + * + * @exception AttributeNotFoundException if this attribute is not supported by this MBean + * @exception MBeanException if the initializer of an object throws an exception + * @exception ReflectionException if a Java reflection exception occurs when invoking the getter + */ + @Override + public Object getAttribute(String name) throws AttributeNotFoundException, MBeanException, ReflectionException { + + // Validate the input parameters + if (name == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullName")), + sm.getString("mBean.nullName")); + } + + Connector connector = doGetManagedResource(); + return IntrospectionUtils.getProperty(connector, name); + } + + + /** + * Set the value of a specific attribute of this MBean. + * + * @param attribute The identification of the attribute to be set and the new value + * + * @exception AttributeNotFoundException if this attribute is not supported by this MBean + * @exception MBeanException if the initializer of an object throws an exception + * @exception ReflectionException if a Java reflection exception occurs when invoking the getter + */ + @Override + public void setAttribute(Attribute attribute) + throws AttributeNotFoundException, MBeanException, ReflectionException { + + // Validate the input parameters + if (attribute == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullAttribute")), + sm.getString("mBean.nullAttribute")); + } + String name = attribute.getName(); + Object value = attribute.getValue(); + if (name == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullName")), + sm.getString("mBean.nullName")); + } + + Connector connector = doGetManagedResource(); + IntrospectionUtils.setProperty(connector, name, String.valueOf(value)); + } +} diff --git a/java/org/apache/catalina/mbeans/ContainerMBean.java b/java/org/apache/catalina/mbeans/ContainerMBean.java new file mode 100644 index 0000000..3e08e41 --- /dev/null +++ b/java/org/apache/catalina/mbeans/ContainerMBean.java @@ -0,0 +1,222 @@ +/* + * 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.catalina.mbeans; + +import java.util.ArrayList; +import java.util.List; + +import javax.management.MBeanException; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.JmxEnabled; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Valve; +import org.apache.catalina.core.ContainerBase; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.ContextConfig; +import org.apache.catalina.startup.HostConfig; + +public class ContainerMBean extends BaseCatalinaMBean { + + /** + * Add a new child Container to those associated with this Container, if supported. Won't start the child yet. Has + * to be started with a call to Start method after necessary configurations are done. + * + * @param type ClassName of the child to be added + * @param name Name of the child to be added + * + * @exception MBeanException if the child cannot be added + */ + public void addChild(String type, String name) throws MBeanException { + + Container contained = (Container) newInstance(type); + contained.setName(name); + + if (contained instanceof StandardHost) { + HostConfig config = new HostConfig(); + contained.addLifecycleListener(config); + } else if (contained instanceof StandardContext) { + ContextConfig config = new ContextConfig(); + contained.addLifecycleListener(config); + } + + boolean oldValue = true; + + ContainerBase container = doGetManagedResource(); + try { + oldValue = container.getStartChildren(); + container.setStartChildren(false); + container.addChild(contained); + contained.init(); + } catch (LifecycleException e) { + throw new MBeanException(e); + } finally { + if (container != null) { + container.setStartChildren(oldValue); + } + } + } + + + /** + * Remove an existing child Container from association with this parent Container. + * + * @param name Name of the existing child Container to be removed + * + * @throws MBeanException if the child cannot be removed + */ + public void removeChild(String name) throws MBeanException { + if (name != null) { + Container container = doGetManagedResource(); + Container contained = container.findChild(name); + container.removeChild(contained); + } + } + + + /** + * Adds a valve to this Container instance. + * + * @param valveType ClassName of the valve to be added + * + * @return the MBean name of the new valve + * + * @throws MBeanException if adding the valve failed + */ + public String addValve(String valveType) throws MBeanException { + Valve valve = (Valve) newInstance(valveType); + + Container container = doGetManagedResource(); + container.getPipeline().addValve(valve); + + if (valve instanceof JmxEnabled) { + return ((JmxEnabled) valve).getObjectName().toString(); + } else { + return null; + } + } + + + /** + * Remove an existing Valve. + * + * @param valveName MBean Name of the Valve to remove + * + * @exception MBeanException if a component cannot be removed + */ + public void removeValve(String valveName) throws MBeanException { + Container container = doGetManagedResource(); + + ObjectName oname; + try { + oname = new ObjectName(valveName); + } catch (MalformedObjectNameException | NullPointerException e) { + throw new MBeanException(e); + } + + if (container != null) { + Valve[] valves = container.getPipeline().getValves(); + for (Valve valve : valves) { + if (valve instanceof JmxEnabled) { + ObjectName voname = ((JmxEnabled) valve).getObjectName(); + if (voname.equals(oname)) { + container.getPipeline().removeValve(valve); + } + } + } + } + } + + + /** + * Add a LifecycleEvent listener to this component. + * + * @param type ClassName of the listener to add + * + * @throws MBeanException if adding the listener failed + */ + public void addLifecycleListener(String type) throws MBeanException { + LifecycleListener listener = (LifecycleListener) newInstance(type); + Container container = doGetManagedResource(); + container.addLifecycleListener(listener); + } + + + /** + * Remove a LifecycleEvent listeners from this component. + * + * @param type The ClassName of the listeners to be removed. Note that all the listeners having given ClassName will + * be removed. + * + * @throws MBeanException propagated from the managed resource access + */ + public void removeLifecycleListeners(String type) throws MBeanException { + Container container = doGetManagedResource(); + + LifecycleListener[] listeners = container.findLifecycleListeners(); + for (LifecycleListener listener : listeners) { + if (listener.getClass().getName().equals(type)) { + container.removeLifecycleListener(listener); + } + } + } + + + /** + * List the class name of each of the lifecycle listeners added to this container. + * + * @return the lifecycle listeners class names + * + * @throws MBeanException propagated from the managed resource access + */ + public String[] findLifecycleListenerNames() throws MBeanException { + Container container = doGetManagedResource(); + List result = new ArrayList<>(); + + LifecycleListener[] listeners = container.findLifecycleListeners(); + for (LifecycleListener listener : listeners) { + result.add(listener.getClass().getName()); + } + + return result.toArray(new String[0]); + } + + + /** + * List the class name of each of the container listeners added to this container. + * + * @return the container listeners class names + * + * @throws MBeanException propagated from the managed resource access + */ + public String[] findContainerListenerNames() throws MBeanException { + Container container = doGetManagedResource(); + List result = new ArrayList<>(); + + ContainerListener[] listeners = container.findContainerListeners(); + for (ContainerListener listener : listeners) { + result.add(listener.getClass().getName()); + } + + return result.toArray(new String[0]); + } +} diff --git a/java/org/apache/catalina/mbeans/ContextEnvironmentMBean.java b/java/org/apache/catalina/mbeans/ContextEnvironmentMBean.java new file mode 100644 index 0000000..797f0e9 --- /dev/null +++ b/java/org/apache/catalina/mbeans/ContextEnvironmentMBean.java @@ -0,0 +1,61 @@ +/* + * 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.catalina.mbeans; + +import javax.management.Attribute; +import javax.management.AttributeNotFoundException; +import javax.management.MBeanException; +import javax.management.ReflectionException; + +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.NamingResources; + +/** + *

    + * A ModelMBean implementation for the + * org.apache.tomcat.util.descriptor.web.ContextEnvironment component. + *

    + * + * @author Amy Roh + */ +public class ContextEnvironmentMBean extends BaseCatalinaMBean { + + + /** + * Set the value of a specific attribute of this MBean. + * + * @param attribute The identification of the attribute to be set and the new value + * + * @exception AttributeNotFoundException if this attribute is not supported by this MBean + * @exception MBeanException if the initializer of an object throws an exception + * @exception ReflectionException if a Java reflection exception occurs when invoking the getter + */ + @Override + public void setAttribute(Attribute attribute) + throws AttributeNotFoundException, MBeanException, ReflectionException { + + super.setAttribute(attribute); + + ContextEnvironment ce = doGetManagedResource(); + + // cannot use side-effects. It's removed and added back each time + // there is a modification in a resource. + NamingResources nr = ce.getNamingResources(); + nr.removeEnvironment(ce.getName()); + nr.addEnvironment(ce); + } +} diff --git a/java/org/apache/catalina/mbeans/ContextMBean.java b/java/org/apache/catalina/mbeans/ContextMBean.java new file mode 100644 index 0000000..2ee81ab --- /dev/null +++ b/java/org/apache/catalina/mbeans/ContextMBean.java @@ -0,0 +1,195 @@ +/* + * 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.catalina.mbeans; + +import javax.management.MBeanException; + +import org.apache.catalina.Context; +import org.apache.tomcat.util.descriptor.web.ApplicationParameter; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +public class ContextMBean extends BaseCatalinaMBean { + + /** + * Return the set of application parameters for this application. + * + * @return a string array with a representation of each parameter + * + * @throws MBeanException propagated from the managed resource access + */ + public String[] findApplicationParameters() throws MBeanException { + + Context context = doGetManagedResource(); + + ApplicationParameter[] params = context.findApplicationParameters(); + String[] stringParams = new String[params.length]; + for (int counter = 0; counter < params.length; counter++) { + stringParams[counter] = params[counter].toString(); + } + + return stringParams; + } + + + /** + * Return the security constraints for this web application. If there are none, a zero-length array is returned. + * + * @return a string array with a representation of each security constraint + * + * @throws MBeanException propagated from the managed resource access + */ + public String[] findConstraints() throws MBeanException { + + Context context = doGetManagedResource(); + + SecurityConstraint[] constraints = context.findConstraints(); + String[] stringConstraints = new String[constraints.length]; + for (int counter = 0; counter < constraints.length; counter++) { + stringConstraints[counter] = constraints[counter].toString(); + } + + return stringConstraints; + } + + + /** + * Return the error page entry for the specified HTTP error code, if any; otherwise return null. + * + * @param errorCode Error code to look up + * + * @return a string representation of the error page + * + * @throws MBeanException propagated from the managed resource access + */ + public String findErrorPage(int errorCode) throws MBeanException { + Context context = doGetManagedResource(); + ErrorPage errorPage = context.findErrorPage(errorCode); + if (errorPage != null) { + return errorPage.toString(); + } else { + return null; + } + } + + + /** + * Return the error page entry for the specified Java exception type, if any; otherwise return null. + * + * @param exceptionType Exception type to look up + * + * @return a string representation of the error page + * + * @throws MBeanException propagated from the managed resource access + */ + public String findErrorPage(Throwable exceptionType) throws MBeanException { + Context context = doGetManagedResource(); + ErrorPage errorPage = context.findErrorPage(exceptionType); + if (errorPage != null) { + return errorPage.toString(); + } else { + return null; + } + } + + + /** + * Return the set of defined error pages for all specified error codes and exception types. + * + * @return a string array with a representation of each error page + * + * @throws MBeanException propagated from the managed resource access + */ + public String[] findErrorPages() throws MBeanException { + + Context context = doGetManagedResource(); + + ErrorPage[] pages = context.findErrorPages(); + String[] stringPages = new String[pages.length]; + for (int counter = 0; counter < pages.length; counter++) { + stringPages[counter] = pages[counter].toString(); + } + + return stringPages; + } + + + /** + * Return the filter definition for the specified filter name, if any; otherwise return null. + * + * @param name Filter name to look up + * + * @return a string representation of the filter definition + * + * @throws MBeanException propagated from the managed resource access + */ + public String findFilterDef(String name) throws MBeanException { + + Context context = doGetManagedResource(); + + FilterDef filterDef = context.findFilterDef(name); + if (filterDef != null) { + return filterDef.toString(); + } else { + return null; + } + } + + + /** + * Return the set of defined filters for this Context. + * + * @return a string array with a representation of all the filter definitions + * + * @throws MBeanException propagated from the managed resource access + */ + public String[] findFilterDefs() throws MBeanException { + + Context context = doGetManagedResource(); + + FilterDef[] filterDefs = context.findFilterDefs(); + String[] stringFilters = new String[filterDefs.length]; + for (int counter = 0; counter < filterDefs.length; counter++) { + stringFilters[counter] = filterDefs[counter].toString(); + } + + return stringFilters; + } + + + /** + * Return the set of filter mappings for this Context. + * + * @return a string array with a representation of all the filter mappings + * + * @throws MBeanException propagated from the managed resource access + */ + public String[] findFilterMaps() throws MBeanException { + + Context context = doGetManagedResource(); + + FilterMap[] maps = context.findFilterMaps(); + String[] stringMaps = new String[maps.length]; + for (int counter = 0; counter < maps.length; counter++) { + stringMaps[counter] = maps[counter].toString(); + } + + return stringMaps; + } +} diff --git a/java/org/apache/catalina/mbeans/ContextResourceLinkMBean.java b/java/org/apache/catalina/mbeans/ContextResourceLinkMBean.java new file mode 100644 index 0000000..ae125c9 --- /dev/null +++ b/java/org/apache/catalina/mbeans/ContextResourceLinkMBean.java @@ -0,0 +1,127 @@ +/* + * 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.catalina.mbeans; + +import javax.management.Attribute; +import javax.management.AttributeNotFoundException; +import javax.management.MBeanException; +import javax.management.ReflectionException; +import javax.management.RuntimeOperationsException; + +import org.apache.tomcat.util.descriptor.web.ContextResourceLink; +import org.apache.tomcat.util.descriptor.web.NamingResources; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * A ModelMBean implementation for the + * org.apache.tomcat.util.descriptor.web.ContextResourceLink component. + *

    + * + * @author Amy Roh + */ +public class ContextResourceLinkMBean extends BaseCatalinaMBean { + + private static final StringManager sm = StringManager.getManager(ContextResourceLinkMBean.class); + + /** + * Obtain and return the value of a specific attribute of this MBean. + * + * @param name Name of the requested attribute + * + * @exception AttributeNotFoundException if this attribute is not supported by this MBean + * @exception MBeanException if the initializer of an object throws an exception + * @exception ReflectionException if a Java reflection exception occurs when invoking the getter + */ + @Override + public Object getAttribute(String name) throws AttributeNotFoundException, MBeanException, ReflectionException { + + // Validate the input parameters + if (name == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullName")), + sm.getString("mBean.nullName")); + } + + ContextResourceLink cl = doGetManagedResource(); + + String value = null; + if ("global".equals(name)) { + return cl.getGlobal(); + } else if ("description".equals(name)) { + return cl.getDescription(); + } else if ("name".equals(name)) { + return cl.getName(); + } else if ("type".equals(name)) { + return cl.getType(); + } else { + value = (String) cl.getProperty(name); + if (value == null) { + throw new AttributeNotFoundException(sm.getString("mBean.attributeNotFound", name)); + } + } + + return value; + } + + + /** + * Set the value of a specific attribute of this MBean. + * + * @param attribute The identification of the attribute to be set and the new value + * + * @exception AttributeNotFoundException if this attribute is not supported by this MBean + * @exception MBeanException if the initializer of an object throws an exception + * @exception ReflectionException if a Java reflection exception occurs when invoking the getter + */ + @Override + public void setAttribute(Attribute attribute) + throws AttributeNotFoundException, MBeanException, ReflectionException { + + // Validate the input parameters + if (attribute == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullAttribute")), + sm.getString("mBean.nullAttribute")); + } + + String name = attribute.getName(); + Object value = attribute.getValue(); + if (name == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullName")), + sm.getString("mBean.nullName")); + } + + ContextResourceLink crl = doGetManagedResource(); + + if ("global".equals(name)) { + crl.setGlobal((String) value); + } else if ("description".equals(name)) { + crl.setDescription((String) value); + } else if ("name".equals(name)) { + crl.setName((String) value); + } else if ("type".equals(name)) { + crl.setType((String) value); + } else { + crl.setProperty(name, "" + value); + } + + // cannot use side-effects. It's removed and added back each time + // there is a modification in a resource. + NamingResources nr = crl.getNamingResources(); + nr.removeResourceLink(crl.getName()); + nr.addResourceLink(crl); + } +} diff --git a/java/org/apache/catalina/mbeans/ContextResourceMBean.java b/java/org/apache/catalina/mbeans/ContextResourceMBean.java new file mode 100644 index 0000000..bad2ec2 --- /dev/null +++ b/java/org/apache/catalina/mbeans/ContextResourceMBean.java @@ -0,0 +1,130 @@ +/* + * 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.catalina.mbeans; + +import javax.management.Attribute; +import javax.management.AttributeNotFoundException; +import javax.management.MBeanException; +import javax.management.ReflectionException; +import javax.management.RuntimeOperationsException; + +import org.apache.tomcat.util.descriptor.web.ContextResource; +import org.apache.tomcat.util.descriptor.web.NamingResources; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * A ModelMBean implementation for the + * org.apache.tomcat.util.descriptor.web.ContextResource component. + *

    + * + * @author Amy Roh + */ +public class ContextResourceMBean extends BaseCatalinaMBean { + + private static final StringManager sm = StringManager.getManager(ContextResourceMBean.class); + + /** + * Obtain and return the value of a specific attribute of this MBean. + * + * @param name Name of the requested attribute + * + * @exception AttributeNotFoundException if this attribute is not supported by this MBean + * @exception MBeanException if the initializer of an object throws an exception + * @exception ReflectionException if a Java reflection exception occurs when invoking the getter + */ + @Override + public Object getAttribute(String name) throws AttributeNotFoundException, MBeanException, ReflectionException { + + // Validate the input parameters + if (name == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullName")), + sm.getString("mBean.nullName")); + } + + ContextResource cr = doGetManagedResource(); + + String value = null; + if ("auth".equals(name)) { + return cr.getAuth(); + } else if ("description".equals(name)) { + return cr.getDescription(); + } else if ("name".equals(name)) { + return cr.getName(); + } else if ("scope".equals(name)) { + return cr.getScope(); + } else if ("type".equals(name)) { + return cr.getType(); + } else { + value = (String) cr.getProperty(name); + if (value == null) { + throw new AttributeNotFoundException(sm.getString("mBean.attributeNotFound", name)); + } + } + + return value; + } + + + /** + * Set the value of a specific attribute of this MBean. + * + * @param attribute The identification of the attribute to be set and the new value + * + * @exception AttributeNotFoundException if this attribute is not supported by this MBean + * @exception MBeanException if the initializer of an object throws an exception + * @exception ReflectionException if a Java reflection exception occurs when invoking the getter + */ + @Override + public void setAttribute(Attribute attribute) + throws AttributeNotFoundException, MBeanException, ReflectionException { + + // Validate the input parameters + if (attribute == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullAttribute")), + sm.getString("mBean.nullAttribute")); + } + String name = attribute.getName(); + Object value = attribute.getValue(); + if (name == null) { + throw new RuntimeOperationsException(new IllegalArgumentException(sm.getString("mBean.nullName")), + sm.getString("mBean.nullName")); + } + + ContextResource cr = doGetManagedResource(); + + if ("auth".equals(name)) { + cr.setAuth((String) value); + } else if ("description".equals(name)) { + cr.setDescription((String) value); + } else if ("name".equals(name)) { + cr.setName((String) value); + } else if ("scope".equals(name)) { + cr.setScope((String) value); + } else if ("type".equals(name)) { + cr.setType((String) value); + } else { + cr.setProperty(name, "" + value); + } + + // cannot use side-effects. It's removed and added back each time + // there is a modification in a resource. + NamingResources nr = cr.getNamingResources(); + nr.removeResource(cr.getName()); + nr.addResource(cr); + } +} diff --git a/java/org/apache/catalina/mbeans/DataSourceUserDatabaseMBean.java b/java/org/apache/catalina/mbeans/DataSourceUserDatabaseMBean.java new file mode 100644 index 0000000..bfbe1cf --- /dev/null +++ b/java/org/apache/catalina/mbeans/DataSourceUserDatabaseMBean.java @@ -0,0 +1,378 @@ +/* + * 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.catalina.mbeans; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.catalina.UserDatabase; +import org.apache.tomcat.util.modeler.BaseModelMBean; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; + +/** + *

    + * A ModelMBean implementation for the org.apache.catalina.users.DataSourceUserDatabase + * component. + *

    + * + * @author Craig R. McClanahan + */ +public class DataSourceUserDatabaseMBean extends BaseModelMBean { + + // ----------------------------------------------------- Instance Variables + + /** + * The configuration information registry for our managed beans. + */ + protected final Registry registry = MBeanUtils.createRegistry(); + + + /** + * The ManagedBean information describing this MBean. + */ + protected final ManagedBean managed = registry.findManagedBean("DataSourceUserDatabase"); + + + // ------------------------------------------------------------- Attributes + + /** + * @return the names of all groups defined in this database. + */ + public String[] getGroups() { + UserDatabase database = (UserDatabase) this.resource; + List results = new ArrayList<>(); + Iterator groups = database.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + results.add(group.getGroupname()); + } + return results.toArray(new String[0]); + } + + + /** + * @return the names of all roles defined in this database. + */ + public String[] getRoles() { + UserDatabase database = (UserDatabase) this.resource; + List results = new ArrayList<>(); + Iterator roles = database.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + results.add(role.getRolename()); + } + return results.toArray(new String[0]); + } + + + /** + * @return the names of all users defined in this database. + */ + public String[] getUsers() { + UserDatabase database = (UserDatabase) this.resource; + List results = new ArrayList<>(); + Iterator users = database.getUsers(); + while (users.hasNext()) { + User user = users.next(); + results.add(user.getUsername()); + } + return results.toArray(new String[0]); + } + + + // ------------------------------------------------------------- Operations + + /** + * Create a new Group and return the corresponding name. + * + * @param groupname Group name of the new group + * @param description Description of the new group + * + * @return the new group name + */ + public String createGroup(String groupname, String description) { + UserDatabase database = (UserDatabase) this.resource; + Group group = database.createGroup(groupname, description); + return group.getGroupname(); + } + + + /** + * Create a new Role and return the corresponding name. + * + * @param rolename Group name of the new group + * @param description Description of the new group + * + * @return the new role name + */ + public String createRole(String rolename, String description) { + UserDatabase database = (UserDatabase) this.resource; + Role role = database.createRole(rolename, description); + return role.getRolename(); + } + + + /** + * Create a new User and return the corresponding name. + * + * @param username User name of the new user + * @param password Password for the new user + * @param fullName Full name for the new user + * + * @return the new user name + */ + public String createUser(String username, String password, String fullName) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.createUser(username, password, fullName); + return user.getUsername(); + } + + + /** + * Remove an existing group. + * + * @param groupname Group name to remove + */ + public void removeGroup(String groupname) { + UserDatabase database = (UserDatabase) this.resource; + Group group = database.findGroup(groupname); + if (group == null) { + return; + } + database.removeGroup(group); + } + + + /** + * Remove an existing role. + * + * @param rolename Role name to remove + */ + public void removeRole(String rolename) { + UserDatabase database = (UserDatabase) this.resource; + Role role = database.findRole(rolename); + if (role == null) { + return; + } + database.removeRole(role); + } + + + /** + * Remove an existing user. + * + * @param username User name to remove + */ + public void removeUser(String username) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + if (user == null) { + return; + } + database.removeUser(user); + } + + + /** + * Change user credentials. + * + * @param username The user name + * @param password The new credentials + */ + public void changeUserPassword(String username, String password) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + if (user != null) { + user.setPassword(password); + } + } + + + /** + * Add specified role to the user. + * + * @param username The user name + * @param rolename The role name + */ + public void addUserRole(String username, String rolename) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + Role role = database.findRole(rolename); + if (user != null && role != null) { + user.addRole(role); + } + } + + + /** + * Remove specified role from the user. + * + * @param username The user name + * @param rolename The role name + */ + public void removeUserRole(String username, String rolename) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + Role role = database.findRole(rolename); + if (user != null && role != null) { + user.removeRole(role); + } + } + + + /** + * Get roles for a user. + * + * @param username The user name + * + * @return Array of role names + */ + public String[] getUserRoles(String username) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + if (user != null) { + List results = new ArrayList<>(); + Iterator roles = user.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + results.add(role.getRolename()); + } + return results.toArray(new String[0]); + } else { + return null; + } + } + + + /** + * Add group to user. + * + * @param username The user name + * @param groupname The group name + */ + public void addUserGroup(String username, String groupname) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + Group group = database.findGroup(groupname); + if (user != null && group != null) { + user.addGroup(group); + } + } + + + /** + * Remove group from user. + * + * @param username The user name + * @param groupname The group name + */ + public void removeUserGroup(String username, String groupname) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + Group group = database.findGroup(groupname); + if (user != null && group != null) { + user.removeGroup(group); + } + } + + + /** + * Get groups for a user. + * + * @param username The user name + * + * @return Array of group names + */ + public String[] getUserGroups(String username) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + if (user != null) { + List results = new ArrayList<>(); + Iterator groups = user.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + results.add(group.getGroupname()); + } + return results.toArray(new String[0]); + } else { + return null; + } + } + + + /** + * Add role to a group. + * + * @param groupname The group name + * @param rolename The role name + */ + public void addGroupRole(String groupname, String rolename) { + UserDatabase database = (UserDatabase) this.resource; + Group group = database.findGroup(groupname); + Role role = database.findRole(rolename); + if (group != null && role != null) { + group.addRole(role); + } + } + + + /** + * Remove role from a group. + * + * @param groupname The group name + * @param rolename The role name + */ + public void removeGroupRole(String groupname, String rolename) { + UserDatabase database = (UserDatabase) this.resource; + Group group = database.findGroup(groupname); + Role role = database.findRole(rolename); + if (group != null && role != null) { + group.removeRole(role); + } + } + + + /** + * Get roles for a group. + * + * @param groupname The group name + * + * @return Array of role names + */ + public String[] getGroupRoles(String groupname) { + UserDatabase database = (UserDatabase) this.resource; + Group group = database.findGroup(groupname); + if (group != null) { + List results = new ArrayList<>(); + Iterator roles = group.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + results.add(role.getRolename()); + } + return results.toArray(new String[0]); + } else { + return null; + } + } + + +} diff --git a/java/org/apache/catalina/mbeans/GlobalResourcesLifecycleListener.java b/java/org/apache/catalina/mbeans/GlobalResourcesLifecycleListener.java new file mode 100644 index 0000000..94f0bf3 --- /dev/null +++ b/java/org/apache/catalina/mbeans/GlobalResourcesLifecycleListener.java @@ -0,0 +1,235 @@ +/* + * 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.catalina.mbeans; + + +import java.util.Iterator; + +import javax.naming.Binding; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.OperationNotSupportedException; + +import org.apache.catalina.Group; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Role; +import org.apache.catalina.Server; +import org.apache.catalina.User; +import org.apache.catalina.UserDatabase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Implementation of LifecycleListener that instantiates the set of MBeans associated with global JNDI + * resources that are subject to management. + *

    + * This listener must only be nested within {@link Server} elements. + * + * @author Craig R. McClanahan + * + * @since 4.1 + */ +public class GlobalResourcesLifecycleListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(GlobalResourcesLifecycleListener.class); + protected static final StringManager sm = StringManager.getManager(GlobalResourcesLifecycleListener.class); + + + // ----------------------------------------------------- Instance Variables + + /** + * The owning Catalina component that we are attached to. + */ + protected Lifecycle component = null; + + + // ---------------------------------------------- LifecycleListener Methods + + /** + * Primary entry point for startup and shutdown events. + * + * @param event The event that has occurred + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + + if (Lifecycle.START_EVENT.equals(event.getType())) { + if (!(event.getLifecycle() instanceof Server)) { + log.warn(sm.getString("listener.notServer", event.getLifecycle().getClass().getSimpleName())); + } + component = event.getLifecycle(); + createMBeans(); + } else if (Lifecycle.STOP_EVENT.equals(event.getType())) { + destroyMBeans(); + component = null; + } + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Create the MBeans for the interesting global JNDI resources. + */ + protected void createMBeans() { + // Look up our global naming context + Context context = null; + try { + context = (Context) (new InitialContext()).lookup("java:/"); + } catch (NamingException e) { + log.error(sm.getString("globalResources.noNamingContext")); + return; + } + + // Recurse through the defined global JNDI resources context + try { + createMBeans("", context); + } catch (NamingException e) { + log.error(sm.getString("globalResources.createError"), e); + } + } + + + /** + * Create the MBeans for the interesting global JNDI resources in the specified naming context. + * + * @param prefix Prefix for complete object name paths + * @param context Context to be scanned + * + * @exception NamingException if a JNDI exception occurs + */ + protected void createMBeans(String prefix, Context context) throws NamingException { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("globalResources.create", prefix)); + } + + try { + NamingEnumeration bindings = context.listBindings(""); + while (bindings.hasMore()) { + Binding binding = bindings.next(); + String name = prefix + binding.getName(); + Object value = context.lookup(binding.getName()); + if (log.isTraceEnabled()) { + log.trace("Checking resource " + name); + } + if (value instanceof Context) { + createMBeans(name + "/", (Context) value); + } else if (value instanceof UserDatabase) { + try { + createMBeans(name, (UserDatabase) value); + } catch (Exception e) { + log.error(sm.getString("globalResources.userDatabaseCreateError", name), e); + } + } + } + } catch (RuntimeException ex) { + log.error(sm.getString("globalResources.createError.runtime"), ex); + } catch (OperationNotSupportedException ex) { + log.error(sm.getString("globalResources.createError.operation"), ex); + } + } + + + /** + * Create the MBeans for the specified UserDatabase and its contents. + * + * @param name Complete resource name of this UserDatabase + * @param database The UserDatabase to be processed + * + * @exception Exception if an exception occurs while creating MBeans + */ + protected void createMBeans(String name, UserDatabase database) throws Exception { + + // Create the MBean for the UserDatabase itself + if (log.isTraceEnabled()) { + log.trace("Creating UserDatabase MBeans for resource " + name); + log.trace("Database=" + database); + } + try { + MBeanUtils.createMBean(database); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("globalResources.createError.userDatabase", name), e); + } + + if (database.isSparse()) { + // Avoid loading all the database as mbeans + return; + } + + // Create the MBeans for each defined Role + Iterator roles = database.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + if (log.isTraceEnabled()) { + log.trace(" Creating Role MBean for role " + role); + } + try { + MBeanUtils.createMBean(role); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("globalResources.createError.userDatabase.role", role), + e); + } + } + + // Create the MBeans for each defined Group + Iterator groups = database.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + if (log.isTraceEnabled()) { + log.trace(" Creating Group MBean for group " + group); + } + try { + MBeanUtils.createMBean(group); + } catch (Exception e) { + throw new IllegalArgumentException( + sm.getString("globalResources.createError.userDatabase.group", group), e); + } + } + + // Create the MBeans for each defined User + Iterator users = database.getUsers(); + while (users.hasNext()) { + User user = users.next(); + if (log.isTraceEnabled()) { + log.trace(" Creating User MBean for user " + user); + } + try { + MBeanUtils.createMBean(user); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("globalResources.createError.userDatabase.user", user), + e); + } + } + } + + + /** + * Destroy the MBeans for the interesting global JNDI resources. + */ + protected void destroyMBeans() { + if (log.isTraceEnabled()) { + log.trace("Destroying MBeans for Global JNDI Resources"); + } + } +} diff --git a/java/org/apache/catalina/mbeans/GroupMBean.java b/java/org/apache/catalina/mbeans/GroupMBean.java new file mode 100644 index 0000000..f0a4601 --- /dev/null +++ b/java/org/apache/catalina/mbeans/GroupMBean.java @@ -0,0 +1,137 @@ +/* + * 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.catalina.mbeans; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.tomcat.util.modeler.BaseModelMBean; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * A ModelMBean implementation for the org.apache.catalina.Group component. + *

    + * + * @author Craig R. McClanahan + */ +public class GroupMBean extends BaseModelMBean { + + private static final StringManager sm = StringManager.getManager(GroupMBean.class); + + /** + * The configuration information registry for our managed beans. + */ + protected final Registry registry = MBeanUtils.createRegistry(); + + + /** + * The ManagedBean information describing this MBean. + */ + protected final ManagedBean managed = registry.findManagedBean("Group"); + + + /** + * @return the MBean Names of all authorized roles for this group. + */ + public String[] getRoles() { + + Group group = (Group) this.resource; + List results = new ArrayList<>(); + Iterator roles = group.getRoles(); + while (roles.hasNext()) { + Role role = null; + try { + role = roles.next(); + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), role); + results.add(oname.toString()); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException(sm.getString("userMBean.createError.role", role), e); + } + } + return results.toArray(new String[0]); + } + + + /** + * @return the MBean Names of all users that are members of this group. + */ + public String[] getUsers() { + + Group group = (Group) this.resource; + List results = new ArrayList<>(); + Iterator users = group.getUsers(); + while (users.hasNext()) { + User user = null; + try { + user = users.next(); + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), user); + results.add(oname.toString()); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException(sm.getString("userMBean.createError.user", user), e); + } + } + return results.toArray(new String[0]); + } + + + /** + * Add a new {@link Role} to those this group belongs to. + * + * @param rolename Role name of the new role + */ + public void addRole(String rolename) { + + Group group = (Group) this.resource; + if (group == null) { + return; + } + Role role = group.getUserDatabase().findRole(rolename); + if (role == null) { + throw new IllegalArgumentException(sm.getString("userMBean.invalidRole", rolename)); + } + group.addRole(role); + } + + + /** + * Remove a {@link Role} from those this group belongs to. + * + * @param rolename Role name of the old role + */ + public void removeRole(String rolename) { + + Group group = (Group) this.resource; + if (group == null) { + return; + } + Role role = group.getUserDatabase().findRole(rolename); + if (role == null) { + throw new IllegalArgumentException(sm.getString("userMBean.invalidRole", rolename)); + } + group.removeRole(role); + } +} diff --git a/java/org/apache/catalina/mbeans/LocalStrings.properties b/java/org/apache/catalina/mbeans/LocalStrings.properties new file mode 100644 index 0000000..138b3a3 --- /dev/null +++ b/java/org/apache/catalina/mbeans/LocalStrings.properties @@ -0,0 +1,67 @@ +# 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. + +globalResources.create=Creating MBeans for Global JNDI Resources in Context [{0}] +globalResources.createError=Exception processing global JNDI Resources +globalResources.createError.operation=Operation not supported error creating MBeans +globalResources.createError.runtime=Unexpected error creating MBeans +globalResources.createError.userDatabase=Cannot create UserDatabase MBean for resource [{0}] +globalResources.createError.userDatabase.group=Cannot create Group MBean for group [{0}] +globalResources.createError.userDatabase.role=Cannot create Role MBean for role [{0}] +globalResources.createError.userDatabase.user=Cannot create User MBean for user [{0}] +globalResources.noNamingContext=No global naming context defined for server +globalResources.userDatabaseCreateError=Exception creating UserDatabase MBeans for [{0}] + +listener.notServer=This listener must only be nested within Server elements, but is in [{0}]. + +mBean.attributeNotFound=Cannot find attribute [{0}] +mBean.nullAttribute=Attribute is null +mBean.nullName=Attribute name is null + +mBeanDumper.getAttributeError=Error getting attribute [{0}] for object name [{1}] + +mBeanFactory.contextCreate.addServicedFail=Unable to create context [{0}] as another component is currently servicing a context with that name +mBeanFactory.contextDestroyError=Error during context [{0}] destroy +mBeanFactory.contextRemove.addServicedFail=Unable to remove context [{0}] as another component is currently servicing that context +mBeanFactory.managerContext=Manager components may only be added to Contexts. +mBeanFactory.noDeployer=Deployer not found for host [{0}] +mBeanFactory.noParent=No parent found with object name [{0}] +mBeanFactory.noService=Service with the domain [{0}] was not found +mBeanFactory.notServer=The container is not a Server + +mBeanUtils.noManagedBean=ManagedBean is not found with [{0}] + +namingResourcesMBean.addAlreadyExists.environment=Specified environment already exists with name [{0}] +namingResourcesMBean.addAlreadyExists.resource=Specified resource name already exists with name [{0}] +namingResourcesMBean.addAlreadyExists.resourceLink=Specified resource link already exists with name [{0}] +namingResourcesMBean.createObjectNameError.environment=Cannot create object name for environment [{0}] +namingResourcesMBean.createObjectNameError.resource=Cannot create object name for resource [{0}] +namingResourcesMBean.createObjectNameError.resourceLink=Cannot create object name for resource link [{0}] +namingResourcesMBean.removeNotFound.environment=Specified environment does not exist with name [{0}] +namingResourcesMBean.removeNotFound.resource=Specified resource does not exist with name [{0}] +namingResourcesMBean.removeNotFound.resourceLink=Specified resource link does not exist with name [{0}] + +userMBean.createError.group=Cannot create object name for group [{0}] +userMBean.createError.role=Cannot create object name for role [{0}] +userMBean.createError.user=Cannot create object name for user [{0}] +userMBean.createMBeanError.group=Exception creating group [{0}] MBean +userMBean.createMBeanError.role=Exception creating role [{0}] MBean +userMBean.createMBeanError.user=Exception creating user [{0}] MBean +userMBean.destroyError.group=Exception destroying group [{0}] MBean +userMBean.destroyError.role=Exception destroying role [{0}] MBean +userMBean.destroyError.user=Exception destroying user [{0}] MBean +userMBean.invalidGroup=Invalid group name [{0}] +userMBean.invalidRole=Invalid role name [{0}] +userMBean.saveError=Error during save operation diff --git a/java/org/apache/catalina/mbeans/LocalStrings_fr.properties b/java/org/apache/catalina/mbeans/LocalStrings_fr.properties new file mode 100644 index 0000000..4a6dcf1 --- /dev/null +++ b/java/org/apache/catalina/mbeans/LocalStrings_fr.properties @@ -0,0 +1,67 @@ +# 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. + +globalResources.create=Création des MBeans pour les ressources JNDI globales dans le contexte [{0}] +globalResources.createError=Erreur lors du traitement des ressources JNDI globales +globalResources.createError.operation=Une erreur d'opération non supportée s'est produite lors de la création des MBeans +globalResources.createError.runtime=Erreur inattendue lors de la création des MBeans +globalResources.createError.userDatabase=Impossible de créer un MBean UserDatabase pour la ressource [{0}] +globalResources.createError.userDatabase.group=Impossible de créer un MBean Groupe pour le groupe [{0}] +globalResources.createError.userDatabase.role=Impossible de créer un MBean Role pour le rôle [{0}] +globalResources.createError.userDatabase.user=Impossible de créer un MBean User pour l''utilisateur [{0}] +globalResources.noNamingContext=Aucun contexte de nommage global n'a été défini pour le serveur +globalResources.userDatabaseCreateError=Erreur lors de la création des MBeans d''une UserDatabase pour [{0}] + +listener.notServer=Ce listener ne peut être ajouté qu''à des éléments Server, mais est dans [{0}] + +mBean.attributeNotFound=Impossible de trouver l''attribut [{0}] +mBean.nullAttribute=L'attribut est null +mBean.nullName=Le nom d'attribut est null + +mBeanDumper.getAttributeError=Erreur en obtenant l''attribut [{0}] pour le nom d''objet [{1}] + +mBeanFactory.contextCreate.addServicedFail=Impossible de créer le contexte [{0}] car un autre composant est en train d''effectuer des opérations de service avec ce nom +mBeanFactory.contextDestroyError=Erreur lors de la destruction du contexte [{0}] +mBeanFactory.contextRemove.addServicedFail=Impossible de supprimer le contexte [{0}] car un autre composant est en train d''effectuer des opérations de service avec ce nom +mBeanFactory.managerContext=Un gestionnaire de sessions ne peut être ajouté qu'à un contexte +mBeanFactory.noDeployer=Un déployeur n''a pas été trouvé pour l''hôte [{0}] +mBeanFactory.noParent=Aucun parent n''a été trouvé avec le nom d''objet [{0}] +mBeanFactory.noService=Un Service ayant le domaine [{0}] n''a pas été trouvé +mBeanFactory.notServer=Le conteneur n'est pas un serveur + +mBeanUtils.noManagedBean=Un ManagedBean n''a pas été trouvé avec [{0}] + +namingResourcesMBean.addAlreadyExists.environment=L''environnement spécifié existe déjà avec le nom [{0}] +namingResourcesMBean.addAlreadyExists.resource=La ressource spécifiée existe déjà avec le nom [{0}] +namingResourcesMBean.addAlreadyExists.resourceLink=Le lien de ressource spécifié existe déjà avec le nom [{0}] +namingResourcesMBean.createObjectNameError.environment=Impossible de créer un nom d''objet pour l''environnement [{0}] +namingResourcesMBean.createObjectNameError.resource=Impossible de créer un nom d''objet pour la ressource [{0}] +namingResourcesMBean.createObjectNameError.resourceLink=Impossible de créer un nom d''objet pour le lien de ressource [{0}] +namingResourcesMBean.removeNotFound.environment=L''environnement spécifié n''existe pas avec le nom [{0}] +namingResourcesMBean.removeNotFound.resource=La ressource spécifiée n''existe pas avec le nom [{0}] +namingResourcesMBean.removeNotFound.resourceLink=Le lien de ressource spécifié n''existe pas avec le nom [{0}] + +userMBean.createError.group=Impossible de créer le nom d''objet pour le groupe [{0}] +userMBean.createError.role=Impossible de créer le nom d''objet pour le rôle [{0}] +userMBean.createError.user=Impossible de créer le nom d''objet pour l''utilisateur [{0}] +userMBean.createMBeanError.group=Erreur lors de la création du MBean du groupe [{0}] +userMBean.createMBeanError.role=Erreur lors de la création du MBean du rôle [{0}] +userMBean.createMBeanError.user=Erreur lors de la création du MBean de l''utilisateur [{0}] +userMBean.destroyError.group=Erreur lors de la destruction du MBean dus groupe [{0}] +userMBean.destroyError.role=Erreur lors de la destruction du MBean du rôle [{0}] +userMBean.destroyError.user=Erreur lors de la destruction du MBean de l''utilisateur [{0}] +userMBean.invalidGroup=Nom de groupe [{0}] invalide +userMBean.invalidRole=Nom de rôle [{0}] invalide +userMBean.saveError=Erreur lors de l'opération de sauvegarde diff --git a/java/org/apache/catalina/mbeans/LocalStrings_ja.properties b/java/org/apache/catalina/mbeans/LocalStrings_ja.properties new file mode 100644 index 0000000..7e50979 --- /dev/null +++ b/java/org/apache/catalina/mbeans/LocalStrings_ja.properties @@ -0,0 +1,67 @@ +# 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. + +globalResources.create=コンテキスト [{0}] 㧠Global JNDI リソース用㮠MBean を作æˆã—ã¦ã„ã¾ã™ +globalResources.createError=グローãƒãƒ« JNDI リソース処ç†ä¸­ã®ä¾‹å¤– +globalResources.createError.operation=MBean 作æˆä¸­ã®æ“作未サãƒãƒ¼ãƒˆã‚¨ãƒ©ãƒ¼ +globalResources.createError.runtime=MBeans 作æˆä¸­ã®äºˆæœŸã›ã¬ã‚¨ãƒ©ãƒ¼ +globalResources.createError.userDatabase=リソース [{0}] ã®UserDatabase MBeanを作æˆã§ãã¾ã›ã‚“ +globalResources.createError.userDatabase.group=グループ [{0}] ã®Group MBeanを作æˆã§ãã¾ã›ã‚“ +globalResources.createError.userDatabase.role=ロール [{0}] ã®Role MBeanãŒä½œæˆã§ãã¾ã›ã‚“ +globalResources.createError.userDatabase.user=ユーザー[{0}]ã®User MBeanを作æˆã§ãã¾ã›ã‚“。 +globalResources.noNamingContext=サーãƒãƒ¼ç”¨ã«å®šç¾©ã•ã‚ŒãŸã‚°ãƒ­ãƒ¼ãƒãƒ«ãƒ»ãƒãƒ¼ãƒŸãƒ³ã‚°ãƒ»ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãŒã‚ã‚Šã¾ã›ã‚“。 +globalResources.userDatabaseCreateError=[{0}]ã®UserDatabase MBeanを作æˆã™ã‚‹éš›ã®ä¾‹å¤– + +listener.notServer=ã“ã®listenerã¯Serverè¦ç´ å†…ã«ã®ã¿ãƒã‚¹ãƒˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ãŒã€[{0}] ã«ã‚ã‚Šã¾ã™ã€‚ + +mBean.attributeNotFound=属性 [{0}] を見ã¤ã‘られã¾ã›ã‚“。 +mBean.nullAttribute=属性ãŒnullã§ã™ +mBean.nullName=属性åãŒNulã§ã™ã€‚ + +mBeanDumper.getAttributeError=ObjectName [{1}] ã®å±žæ€§ [{0}] ã‚’å–得中ã®ã‚¨ãƒ©ãƒ¼ + +mBeanFactory.contextCreate.addServicedFail=別ã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆãŒç¾åœ¨ãã®åå‰ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’処ç†ã—ã¦ã„ã‚‹ãŸã‚ã€ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] を作æˆã§ãã¾ã›ã‚“ +mBeanFactory.contextDestroyError=コンテキスト [{0}] 破棄中ã®ã‚¨ãƒ©ãƒ¼ +mBeanFactory.contextRemove.addServicedFail=別ã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆãŒç¾åœ¨ãã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’処ç†ã—ã¦ã„ã‚‹ãŸã‚ã€ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] を削除ã§ãã¾ã›ã‚“ +mBeanFactory.managerContext=Managerコンãƒãƒ¼ãƒãƒ³ãƒˆã¯Contextã«ã®ã¿è¿½åŠ ã§ãã¾ã™ã€‚ +mBeanFactory.noDeployer=ホスト [{0}] Deployer ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +mBeanFactory.noParent=オブジェクトå [{0}] ã®è¦ªãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +mBeanFactory.noService=ドメイン [{0}] ã«ã‚µãƒ¼ãƒ“スãŒã‚ã‚Šã¾ã›ã‚“。 +mBeanFactory.notServer=コンテナ㯠Server ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 + +mBeanUtils.noManagedBean=[{0}]ã®ManagedBeanãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ + +namingResourcesMBean.addAlreadyExists.environment=指定ã•ã‚ŒãŸç’°å¢ƒã¯åå‰[{0}]ãŒæ—¢ã«å­˜åœ¨ã—ã¾ã™ã€‚ +namingResourcesMBean.addAlreadyExists.resource=指定ã•ã‚ŒãŸåå‰ã®ãƒªã‚½ãƒ¼ã‚¹ã¯ [{0}] ã¨ã„ã†åå‰ã§ã™ã§ã«å­˜åœ¨ã—ã¦ã„ã¾ã™ã€‚ +namingResourcesMBean.addAlreadyExists.resourceLink=指定ã•ã‚ŒãŸãƒªã‚½ãƒ¼ã‚¹ãƒªãƒ³ã‚¯ã¯ã™ã§ã«åå‰[{0}]ã§å­˜åœ¨ã—ã¾ã™ã€‚ +namingResourcesMBean.createObjectNameError.environment=environment [{0}]ã®ObjectNameを作æˆã§ãã¾ã›ã‚“。 +namingResourcesMBean.createObjectNameError.resource=リソース [{0}] ã®ObjectNameを作æˆã§ãã¾ã›ã‚“ +namingResourcesMBean.createObjectNameError.resourceLink=リソースリンク [{0}] ã®ObjectNameを作æˆã§ãã¾ã›ã‚“ +namingResourcesMBean.removeNotFound.environment=指定ã•ã‚ŒãŸenvironment ãŒåå‰[{0}]ã§å­˜åœ¨ã—ã¾ã›ã‚“。 +namingResourcesMBean.removeNotFound.resource=指定ã•ã‚ŒãŸãƒªã‚½ãƒ¼ã‚¹ãŒåå‰[{0}]ã§å­˜åœ¨ã—ã¾ã›ã‚“。 +namingResourcesMBean.removeNotFound.resourceLink=指定ã•ã‚ŒãŸãƒªã‚½ãƒ¼ã‚¹ãƒªãƒ³ã‚¯ãŒåå‰[{0}]ã§å­˜åœ¨ã—ã¾ã›ã‚“ + +userMBean.createError.group=グループ [{0}] ã®ObjectNameを作æˆã§ãã¾ã›ã‚“ +userMBean.createError.role=ロール [{0}] ã®ObjectNameを作æˆã§ãã¾ã›ã‚“ +userMBean.createError.user=ユーザー[{0}]ã®ObjectNameを作æˆã§ãã¾ã›ã‚“。 +userMBean.createMBeanError.group=グループ [{0}] MBean作æˆä¸­ã®ä¾‹å¤– +userMBean.createMBeanError.role=Role [{0}] MBean作æˆä¸­ã®ä¾‹å¤– +userMBean.createMBeanError.user=ユーザ [{0}] MBean作æˆä¸­ã®ä¾‹å¤– +userMBean.destroyError.group=グループ [{0}] MBean破棄中ã®ä¾‹å¤– +userMBean.destroyError.role=ロール [{0}] MBeanを破棄ã™ã‚‹éš›ã®ä¾‹å¤– +userMBean.destroyError.user=ユーザ [{0}] MBean 破棄中ã®ä¾‹å¤– +userMBean.invalidGroup=ä¸æ­£ãªã‚°ãƒ«ãƒ¼ãƒ—å [{0}] +userMBean.invalidRole=ä¸æ­£ãªãƒ­ãƒ¼ãƒ«å [{0}] +userMBean.saveError=saveオペレーション中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿ diff --git a/java/org/apache/catalina/mbeans/LocalStrings_ko.properties b/java/org/apache/catalina/mbeans/LocalStrings_ko.properties new file mode 100644 index 0000000..377dbbc --- /dev/null +++ b/java/org/apache/catalina/mbeans/LocalStrings_ko.properties @@ -0,0 +1,65 @@ +# 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. + +globalResources.createError=글로벌 JNDI ë¦¬ì†ŒìŠ¤ë“¤ì„ ì²˜ë¦¬í•˜ëŠ” 중 예외 ë°œìƒ +globalResources.createError.operation=MBeanë“¤ì„ ìƒì„±í•˜ëŠ” 중 OperationNotSupportedException ë°œìƒ +globalResources.createError.runtime=MBeanë“¤ì„ ìƒì„±í•˜ëŠ” 중 예기치 ì•Šì€ ì˜¤ë¥˜ ë°œìƒ +globalResources.createError.userDatabase=리소스 [{0}]ì„(를) 위한 ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ MBeanì„ ìƒì„±í•  수 없습니다. +globalResources.createError.userDatabase.group=그룹 [{0}]ì„(를) 위한 그룹 MBeanì„ ìƒì„±í•  수 없습니다. +globalResources.createError.userDatabase.role=ì—­í•  [{0}]ì„(를) 위한 ì—­í•  MBeanì„ ìƒì„±í•  수 없습니다. +globalResources.createError.userDatabase.user=ì‚¬ìš©ìž [{0}]ì„(를) 위한 ì‚¬ìš©ìž MBeanì„ ìƒì„±í•  수 없습니다. +globalResources.noNamingContext=서버를 위한 글로벌 Naming 컨í…스트가 ì •ì˜ë˜ì§€ 않았습니다. +globalResources.userDatabaseCreateError=[{0}]ì„(를) 위한 ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ MBeanë“¤ì„ ìƒì„±í•˜ëŠ” 중 예외 ë°œìƒ + +listener.notServer=리스너 엘리먼트는 서버 엘리먼트 ë‚´ì— ìœ„ì¹˜í•´ì•¼ 합니다만, 현재 [{0}] ë‚´ì— ìžˆìŠµë‹ˆë‹¤. + +mBean.attributeNotFound=ì†ì„± [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +mBean.nullAttribute=ì†ì„±ì´ ë„입니다. +mBean.nullName=ì†ì„± ì´ë¦„ì´ ë„입니다. + +mBeanDumper.getAttributeError=ê°ì²´ ì´ë¦„ [{1}]ì„(를) 위한 ì†ì„± [{0}]ì„(를) 얻는 중 오류 ë°œìƒ + +mBeanFactory.contextCreate.addServicedFail=다른 구성요소가 í•œ 컨í…스트를 해당 ì´ë¦„으로 현재 서비스 중ì´ë¯€ë¡œ, 컨í…스트 [{0}]ì„(를) ìƒì„±í•  수 없습니다. +mBeanFactory.contextDestroyError=컨í…스트 [{0}]ì„(를) 소멸시키는 중 오류 ë°œìƒ +mBeanFactory.contextRemove.addServicedFail=다른 구성요소가 해당 컨í…스트를 현재 서비스 중ì´ë¯€ë¡œ, 컨í…스트 [{0}]ì„(를) 제거할 수 없습니다. +mBeanFactory.managerContext=매니저 êµ¬ì„±ìš”ì†Œë“¤ì€ ì»¨í…스트들ì—만 ì¶”ê°€ë  ìˆ˜ 있습니다. +mBeanFactory.noDeployer=호스트 [{0}]ì„(를) 위한 배치관리ìž(Deployer)를 ì°¾ì„ ìˆ˜ 없습니다. +mBeanFactory.noService=ë„ë©”ì¸ [{0}]ì— ëŒ€í•œ 서비스를 ì°¾ì„ ìˆ˜ 없었습니다. +mBeanFactory.notServer=컨테ì´ë„ˆê°€ 서버 ê°ì²´ê°€ 아닙니다. + +mBeanUtils.noManagedBean=[{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ManagedBeanì„ ì°¾ì„ ìˆ˜ 없습니다. + +namingResourcesMBean.addAlreadyExists.environment=[{0}](ì´)ë¼ëŠ” ì´ë¦„으로 ì§€ì •ëœ environmentê°€ ì´ë¯¸ 존재합니다. +namingResourcesMBean.addAlreadyExists.resource=[{0}](ì´)ë¼ëŠ” ì´ë¦„으로 ì§€ì •ëœ ë¦¬ì†ŒìŠ¤ ì´ë¦„ì´ ì´ë¯¸ 존재합니다. +namingResourcesMBean.addAlreadyExists.resourceLink=[{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì§€ì •ëœ ë¦¬ì†ŒìŠ¤ ë§í¬ê°€ ì´ë¯¸ 존재합니다. +namingResourcesMBean.createObjectNameError.environment=Environment [{0}]ì„(를) 위한 ê°ì²´ ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +namingResourcesMBean.createObjectNameError.resource=리소스 [{0}]ì„(를) 위한 ê°ì²´ ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +namingResourcesMBean.createObjectNameError.resourceLink=리소스 ë§í¬ [{0}]ì„(를) 위한 ê°ì²´ ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +namingResourcesMBean.removeNotFound.environment=[{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì§€ì •ëœ environmentê°€ 존재하지 않습니다. +namingResourcesMBean.removeNotFound.resource=[{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì§€ì •ëœ ë¦¬ì†ŒìŠ¤ê°€ 존재하지 않습니다. +namingResourcesMBean.removeNotFound.resourceLink=[{0}](ì´)ë¼ëŠ” ì´ë¦„으로 ì§€ì •ëœ ë¦¬ì†ŒìŠ¤ ë§í¬ê°€ 존재하지 않습니다. + +userMBean.createError.group=그룹 [{0}]ì„(를) 위한 ê°ì²´ ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +userMBean.createError.role=ì—­í•  [{0}]ì„(를) 위한 ê°ì²´ ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +userMBean.createError.user=ì‚¬ìš©ìž [{0}]ì„(를) 위한 ê°ì²´ ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +userMBean.createMBeanError.group=그룹 [{0}] MBeanì„ ìƒì„±í•˜ëŠ” 중 예외 ë°œìƒ +userMBean.createMBeanError.role=ì—­í•  [{0}] MBeanì„ ìƒì„±í•˜ëŠ” 중 예외 ë°œìƒ +userMBean.createMBeanError.user=ì‚¬ìš©ìž [{0}] MBeanì„ ìƒì„±í•˜ëŠ” 중 예외 ë°œìƒ +userMBean.destroyError.group=그룹 [{0}] MBeanì„ ì†Œë©¸ì‹œí‚¤ëŠ” 중 예외 ë°œìƒ +userMBean.destroyError.role=ì—­í•  [{0}] MBeanì„ ì†Œë©¸ì‹œí‚¤ëŠ” 중 예외 ë°œìƒ +userMBean.destroyError.user=ì‚¬ìš©ìž [{0}] MBeanì„ ì†Œë©¸ì‹œí‚¤ëŠ” 중 예외 ë°œìƒ +userMBean.invalidGroup=유효하지 ì•Šì€ ê·¸ë£¹ ì´ë¦„ [{0}] +userMBean.invalidRole=유효하지 ì•Šì€ ì—­í•  ì´ë¦„ [{0}] +userMBean.saveError=저장 오í¼ë ˆì´ì…˜ì—ì„œ 오류 ë°œìƒ diff --git a/java/org/apache/catalina/mbeans/LocalStrings_zh_CN.properties b/java/org/apache/catalina/mbeans/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..024915d --- /dev/null +++ b/java/org/apache/catalina/mbeans/LocalStrings_zh_CN.properties @@ -0,0 +1,65 @@ +# 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. + +globalResources.createError=处ç†å…¨å±€JNDI资æºæ—¶å‘生异常 +globalResources.createError.operation=创建MBeanæ—¶ä¸æ”¯æŒæ“作 +globalResources.createError.runtime=创建MBean时出现æ„外错误 +globalResources.createError.userDatabase=无法为资æº[{0}]创建用户数æ®åº“MBean +globalResources.createError.userDatabase.group=无法为组[{0}]创建组MBean +globalResources.createError.userDatabase.role=无法为角色[{0}]创建角色MBean +globalResources.createError.userDatabase.user=无法为用户[{0}]创建用户MBean +globalResources.noNamingContext=没有为æœåŠ¡å™¨å®šä¹‰å…¨å±€å‘½å上下文 +globalResources.userDatabaseCreateError=为[{0}]创建用户数æ®åº“MBeanæ—¶å‘生异常 + +listener.notServer=此侦å¬å™¨åªèƒ½åµŒå¥—在 Server 元素中,但ä½äºŽ [{0}] 中。 + +mBean.attributeNotFound=找ä¸åˆ°å±žæ€§[{0}] +mBean.nullAttribute=属性为空 +mBean.nullName=属性å为空 + +mBeanDumper.getAttributeError=获å–对象å[{1}]的属性[{0}]时出错 + +mBeanFactory.contextCreate.addServicedFail=无法创建上下文[{0}],因为å¦ä¸€ä¸ªç»„件正在为具有该å称的上下文æä¾›æœåŠ¡ +mBeanFactory.contextDestroyError=上下文[{0}]销æ¯æœŸé—´å‡ºé”™ +mBeanFactory.contextRemove.addServicedFail=无法移除上下文[{0}],因为å¦ä¸€ä¸ªç»„件正在为具有该å称的上下文æä¾›æœåŠ¡ +mBeanFactory.managerContext=管ç†å™¨ç»„件åªèƒ½æ·»åŠ åˆ°ä¸Šä¸‹æ–‡ä¸­ã€‚ +mBeanFactory.noDeployer=找ä¸åˆ°ä¸»æœº[{0}]çš„éƒ¨ç½²ç¨‹åº +mBeanFactory.noService=找ä¸åˆ°åŸŸä¸º[{0}]çš„æœåŠ¡ +mBeanFactory.notServer=容器ä¸æ˜¯æœåŠ¡å™¨ + +mBeanUtils.noManagedBean=找ä¸åˆ°å…·æœ‰[{0}]çš„ManagedBean + +namingResourcesMBean.addAlreadyExists.environment=已存在å为[{0}]的指定环境 +namingResourcesMBean.addAlreadyExists.resource=指定的资æºå已存在,å称为[{0}] +namingResourcesMBean.addAlreadyExists.resourceLink=指定的资æºé“¾æŽ¥å·²å­˜åœ¨ï¼Œå称为[{0}] +namingResourcesMBean.createObjectNameError.environment=无法为环境[{0}]创建对象å +namingResourcesMBean.createObjectNameError.resource=无法为资æº[{0}]创建对象å +namingResourcesMBean.createObjectNameError.resourceLink=无法为资æºé“¾æŽ¥[{0}]创建对象å +namingResourcesMBean.removeNotFound.environment=å为[{0}]的指定环境ä¸å­˜åœ¨ +namingResourcesMBean.removeNotFound.resource=å为[{0}]的指定资æºä¸å­˜åœ¨ +namingResourcesMBean.removeNotFound.resourceLink=å为[{0}]的指定资æºé“¾æŽ¥ä¸å­˜åœ¨ + +userMBean.createError.group=无法为组[{0}]创建对象å +userMBean.createError.role=无法为角色[{0}]创建对象å +userMBean.createError.user=无法为用户[{0}]创建对象å +userMBean.createMBeanError.group=创建组[{0}]MBeanæ—¶å‘生异常 +userMBean.createMBeanError.role=创建角色[{0}]MBeanæ—¶å‘生异常 +userMBean.createMBeanError.user=创建用户[{0}]MBeanæ—¶å‘生异常 +userMBean.destroyError.group=销æ¯ç»„[{0}]MBeanæ—¶å‘生异常 +userMBean.destroyError.role=销æ¯è§’色[{0}]MBeanæ—¶å‘生异常 +userMBean.destroyError.user=销æ¯ç”¨æˆ·[{0}]MBeanæ—¶å‘生异常 +userMBean.invalidGroup=无效的组å[{0}] +userMBean.invalidRole=无效的角色å[{0}] +userMBean.saveError=ä¿å­˜æ“作时出错 diff --git a/java/org/apache/catalina/mbeans/MBeanDumper.java b/java/org/apache/catalina/mbeans/MBeanDumper.java new file mode 100644 index 0000000..289358d --- /dev/null +++ b/java/org/apache/catalina/mbeans/MBeanDumper.java @@ -0,0 +1,233 @@ +/* + * 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.catalina.mbeans; + +import java.lang.reflect.Array; +import java.util.Set; +import java.util.StringJoiner; + +import javax.management.JMRuntimeException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.TabularData; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * General helper to dump MBean contents to the log. + */ +public class MBeanDumper { + + private static final Log log = LogFactory.getLog(MBeanDumper.class); + protected static final StringManager sm = StringManager.getManager(MBeanDumper.class); + + private static final String CRLF = "\r\n"; + + + /** + * The following code to dump MBeans has been copied from JMXProxyServlet. + * + * @param mbeanServer the MBean server + * @param names a set of object names for which to dump the info + * + * @return a string representation of the MBeans + */ + public static String dumpBeans(MBeanServer mbeanServer, Set names) { + StringBuilder buf = new StringBuilder(); + for (ObjectName oname : names) { + buf.append("Name: "); + buf.append(oname.toString()); + buf.append(CRLF); + + try { + MBeanInfo minfo = mbeanServer.getMBeanInfo(oname); + // can't be null - I think + String code = minfo.getClassName(); + if ("org.apache.commons.modeler.BaseModelMBean".equals(code)) { + code = (String) mbeanServer.getAttribute(oname, "modelerType"); + } + buf.append("modelerType: "); + buf.append(code); + buf.append(CRLF); + + MBeanAttributeInfo attrs[] = minfo.getAttributes(); + Object value = null; + + for (MBeanAttributeInfo attr : attrs) { + if (!attr.isReadable()) { + continue; + } + String attName = attr.getName(); + if ("modelerType".equals(attName)) { + continue; + } + if (attName.indexOf('=') >= 0 || attName.indexOf(':') >= 0 || attName.indexOf(' ') >= 0) { + continue; + } + + try { + value = mbeanServer.getAttribute(oname, attName); + } catch (JMRuntimeException rme) { + Throwable cause = rme.getCause(); + if (cause instanceof UnsupportedOperationException) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("mBeanDumper.getAttributeError", attName, oname), rme); + } + } else if (cause instanceof NullPointerException) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("mBeanDumper.getAttributeError", attName, oname), rme); + } + } else { + log.error(sm.getString("mBeanDumper.getAttributeError", attName, oname), rme); + } + continue; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("mBeanDumper.getAttributeError", attName, oname), t); + continue; + } + if (value == null) { + continue; + } + String valueString; + try { + Class c = value.getClass(); + if (c.isArray()) { + int len = Array.getLength(value); + StringBuilder sb = + new StringBuilder("Array[" + c.getComponentType().getName() + "] of length " + len); + if (len > 0) { + sb.append(CRLF); + } + for (int j = 0; j < len; j++) { + Object item = Array.get(value, j); + sb.append(tableItemToString(item)); + if (j < len - 1) { + sb.append(CRLF); + } + } + valueString = sb.toString(); + } else if (TabularData.class.isInstance(value)) { + TabularData tab = TabularData.class.cast(value); + StringJoiner joiner = new StringJoiner(CRLF); + joiner.add("TabularData[" + tab.getTabularType().getRowType().getTypeName() + + "] of length " + tab.size()); + for (Object item : tab.values()) { + joiner.add(tableItemToString(item)); + } + valueString = joiner.toString(); + } else { + valueString = valueToString(value); + } + buf.append(attName); + buf.append(": "); + buf.append(valueString); + buf.append(CRLF); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + buf.append(CRLF); + } + return buf.toString(); + } + + + public static String escape(String value) { + // The only invalid char is \n + // We also need to keep the string short and split it with \nSPACE + // XXX TODO + int idx = value.indexOf('\n'); + if (idx < 0) { + return value; + } + + int prev = 0; + StringBuilder sb = new StringBuilder(); + while (idx >= 0) { + appendHead(sb, value, prev, idx); + sb.append("\\n\n "); + prev = idx + 1; + if (idx == value.length() - 1) { + break; + } + idx = value.indexOf('\n', idx + 1); + } + if (prev < value.length()) { + appendHead(sb, value, prev, value.length()); + } + return sb.toString(); + } + + + private static void appendHead(StringBuilder sb, String value, int start, int end) { + if (end < 1) { + return; + } + + int pos = start; + while (end - pos > 78) { + sb.append(value.substring(pos, pos + 78)); + sb.append("\n "); + pos = pos + 78; + } + sb.append(value.substring(pos, end)); + } + + + private static String tableItemToString(Object item) { + if (item == null) { + return "\t" + "NULL VALUE"; + } else { + try { + return "\t" + valueToString(item); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + return "\t" + "NON-STRINGABLE VALUE"; + } + } + } + + + private static String valueToString(Object value) { + String valueString; + if (CompositeData.class.isInstance(value)) { + StringBuilder sb = new StringBuilder("{"); + String sep = ""; + CompositeData composite = CompositeData.class.cast(value); + Set keys = composite.getCompositeType().keySet(); + for (String key : keys) { + sb.append(sep).append(key).append('=').append(composite.get(key)); + sep = ", "; + } + sb.append('}'); + valueString = sb.toString(); + } else { + valueString = value.toString(); + } + return escape(valueString); + } +} diff --git a/java/org/apache/catalina/mbeans/MBeanFactory.java b/java/org/apache/catalina/mbeans/MBeanFactory.java new file mode 100644 index 0000000..9763fe6 --- /dev/null +++ b/java/org/apache/catalina/mbeans/MBeanFactory.java @@ -0,0 +1,847 @@ +/* + * 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.catalina.mbeans; + +import java.io.File; +import java.net.InetAddress; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.JmxEnabled; +import org.apache.catalina.Realm; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.core.StandardService; +import org.apache.catalina.loader.WebappLoader; +import org.apache.catalina.realm.DataSourceRealm; +import org.apache.catalina.realm.JNDIRealm; +import org.apache.catalina.realm.MemoryRealm; +import org.apache.catalina.realm.UserDatabaseRealm; +import org.apache.catalina.session.StandardManager; +import org.apache.catalina.startup.ContextConfig; +import org.apache.catalina.startup.HostConfig; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + + +/** + * @author Amy Roh + */ +public class MBeanFactory { + + private static final Log log = LogFactory.getLog(MBeanFactory.class); + + protected static final StringManager sm = StringManager.getManager(MBeanFactory.class); + + /** + * The MBeanServer for this application. + */ + private static final MBeanServer mserver = MBeanUtils.createServer(); + + + // ------------------------------------------------------------- Attributes + + /** + * The container (Server/Service) for which this factory was created. + */ + private Object container; + + + // ------------------------------------------------------------- Operations + + /** + * Set the container that this factory was created for. + * + * @param container The associated container + */ + public void setContainer(Object container) { + this.container = container; + } + + + /** + * Little convenience method to remove redundant code when retrieving the path string + * + * @param t path string + * + * @return empty string if t==null || t.equals("/") + */ + private String getPathStr(String t) { + if (t == null || t.equals("/")) { + return ""; + } + return t; + } + + /** + * Get Parent Container to add its child component from parent's ObjectName + */ + private Container getParentContainerFromParent(ObjectName pname) throws Exception { + + String type = pname.getKeyProperty("type"); + String j2eeType = pname.getKeyProperty("j2eeType"); + Service service = getService(pname); + StandardEngine engine = (StandardEngine) service.getContainer(); + if ((j2eeType != null) && (j2eeType.equals("WebModule"))) { + String name = pname.getKeyProperty("name"); + name = name.substring(2); + int i = name.indexOf('/'); + String hostName = name.substring(0, i); + String path = name.substring(i); + Container host = engine.findChild(hostName); + String pathStr = getPathStr(path); + Container context = host.findChild(pathStr); + return context; + } else if (type != null) { + if (type.equals("Engine")) { + return engine; + } else if (type.equals("Host")) { + String hostName = pname.getKeyProperty("host"); + Container host = engine.findChild(hostName); + return host; + } + } + return null; + + } + + + /** + * Get Parent ContainerBase to add its child component from child component's ObjectName as a String + */ + private Container getParentContainerFromChild(ObjectName oname) throws Exception { + + String hostName = oname.getKeyProperty("host"); + String path = oname.getKeyProperty("path"); + Service service = getService(oname); + Container engine = service.getContainer(); + if (hostName == null) { + // child's container is Engine + return engine; + } else if (path == null) { + // child's container is Host + Container host = engine.findChild(hostName); + return host; + } else { + // child's container is Context + Container host = engine.findChild(hostName); + path = getPathStr(path); + Container context = host.findChild(path); + return context; + } + } + + + private Service getService(ObjectName oname) throws Exception { + + if (container instanceof Service) { + // Don't bother checking the domain - this is the only option + return (Service) container; + } + + StandardService service = null; + String domain = oname.getDomain(); + if (container instanceof Server) { + Service[] services = ((Server) container).findServices(); + for (Service value : services) { + service = (StandardService) value; + if (domain.equals(service.getObjectName().getDomain())) { + break; + } + } + } + if (service == null || !service.getObjectName().getDomain().equals(domain)) { + throw new Exception(sm.getString("mBeanFactory.noService", domain)); + } + return service; + + } + + + /** + * Create a new AjpConnector + * + * @param parent MBean Name of the associated parent component + * @param address The IP address on which to bind + * @param port TCP port number to listen on + * + * @return the object name of the created connector + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createAjpConnector(String parent, String address, int port) throws Exception { + + return createConnector(parent, address, port, true, false); + } + + + /** + * Create a new DataSource Realm. + * + * @param parent MBean Name of the associated parent component + * @param dataSourceName the datasource name + * @param roleNameCol the column name for the role names + * @param userCredCol the column name for the user credentials + * @param userNameCol the column name for the user names + * @param userRoleTable the table name for the roles table + * @param userTable the table name for the users + * + * @return the object name of the created realm + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createDataSourceRealm(String parent, String dataSourceName, String roleNameCol, String userCredCol, + String userNameCol, String userRoleTable, String userTable) throws Exception { + + // Create a new DataSourceRealm instance + DataSourceRealm realm = new DataSourceRealm(); + realm.setDataSourceName(dataSourceName); + realm.setRoleNameCol(roleNameCol); + realm.setUserCredCol(userCredCol); + realm.setUserNameCol(userNameCol); + realm.setUserRoleTable(userRoleTable); + realm.setUserTable(userTable); + + // Add the new instance to its parent component + return addRealmToParent(parent, realm); + } + + + private String addRealmToParent(String parent, Realm realm) throws Exception { + ObjectName pname = new ObjectName(parent); + Container container = getParentContainerFromParent(pname); + if (container == null) { + throw new IllegalArgumentException(sm.getString("mBeanFactory.noParent", parent)); + } + // Add the new instance to its parent component + container.setRealm(realm); + // Return the corresponding MBean name + ObjectName oname = null; + if (realm instanceof JmxEnabled) { + oname = ((JmxEnabled) realm).getObjectName(); + } + if (oname != null) { + return oname.toString(); + } else { + return null; + } + } + + + /** + * Create a new HttpConnector + * + * @param parent MBean Name of the associated parent component + * @param address The IP address on which to bind + * @param port TCP port number to listen on + * + * @return the object name of the created connector + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createHttpConnector(String parent, String address, int port) throws Exception { + return createConnector(parent, address, port, false, false); + } + + + /** + * Create a new Connector + * + * @param parent MBean Name of the associated parent component + * @param address The IP address on which to bind + * @param port TCP port number to listen on + * @param isAjp Create a AJP/1.3 Connector + * @param isSSL Create a secure Connector + * + * @exception Exception if an MBean cannot be created or registered + */ + private String createConnector(String parent, String address, int port, boolean isAjp, boolean isSSL) + throws Exception { + // Set the protocol in the constructor + String protocol = isAjp ? "AJP/1.3" : "HTTP/1.1"; + Connector retobj = new Connector(protocol); + if ((address != null) && (address.length() > 0)) { + retobj.setProperty("address", address); + } + // Set port number + retobj.setPort(port); + // Set SSL + retobj.setSecure(isSSL); + retobj.setScheme(isSSL ? "https" : "http"); + // Add the new instance to its parent component + // FIX ME - addConnector will fail + ObjectName pname = new ObjectName(parent); + Service service = getService(pname); + service.addConnector(retobj); + + // Return the corresponding MBean name + ObjectName coname = retobj.getObjectName(); + + return coname.toString(); + } + + + /** + * Create a new HttpsConnector + * + * @param parent MBean Name of the associated parent component + * @param address The IP address on which to bind + * @param port TCP port number to listen on + * + * @return the object name of the created connector + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createHttpsConnector(String parent, String address, int port) throws Exception { + return createConnector(parent, address, port, false, true); + } + + + /** + * Create a new JNDI Realm. + * + * @param parent MBean Name of the associated parent component + * + * @return the object name of the created realm + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createJNDIRealm(String parent) throws Exception { + + // Create a new JNDIRealm instance + JNDIRealm realm = new JNDIRealm(); + + // Add the new instance to its parent component + return addRealmToParent(parent, realm); + } + + + /** + * Create a new Memory Realm. + * + * @param parent MBean Name of the associated parent component + * + * @return the object name of the created realm + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createMemoryRealm(String parent) throws Exception { + + // Create a new MemoryRealm instance + MemoryRealm realm = new MemoryRealm(); + + // Add the new instance to its parent component + return addRealmToParent(parent, realm); + } + + + /** + * Create a new StandardContext. + * + * @param parent MBean Name of the associated parent component + * @param path The context path for this Context + * @param docBase Document base directory (or WAR) for this Context + * + * @return the object name of the created context + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createStandardContext(String parent, String path, String docBase) throws Exception { + + return createStandardContext(parent, path, docBase, false, false); + } + + + /** + * Create a new StandardContext. + * + * @param parent MBean Name of the associated parent component + * @param path The context path for this Context + * @param docBase Document base directory (or WAR) for this Context + * @param xmlValidation if XML descriptors should be validated + * @param xmlNamespaceAware if the XML processor should namespace aware + * + * @return the object name of the created context + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createStandardContext(String parent, String path, String docBase, boolean xmlValidation, + boolean xmlNamespaceAware) throws Exception { + + // Create a new StandardContext instance + StandardContext context = new StandardContext(); + path = getPathStr(path); + context.setPath(path); + context.setDocBase(docBase); + context.setXmlValidation(xmlValidation); + context.setXmlNamespaceAware(xmlNamespaceAware); + + ContextConfig contextConfig = new ContextConfig(); + context.addLifecycleListener(contextConfig); + + // Add the new instance to its parent component + ObjectName pname = new ObjectName(parent); + ObjectName deployer = new ObjectName(pname.getDomain() + ":type=Deployer,host=" + pname.getKeyProperty("host")); + if (mserver.isRegistered(deployer)) { + String contextName = context.getName(); + Boolean result = (Boolean) mserver.invoke(deployer, "tryAddServiced", new Object[] { contextName }, + new String[] { "java.lang.String" }); + if (result.booleanValue()) { + try { + String configPath = (String) mserver.getAttribute(deployer, "configBaseName"); + String baseName = context.getBaseName(); + File configFile = new File(new File(configPath), baseName + ".xml"); + if (configFile.isFile()) { + context.setConfigFile(configFile.toURI().toURL()); + } + mserver.invoke(deployer, "manageApp", new Object[] { context }, + new String[] { "org.apache.catalina.Context" }); + } finally { + mserver.invoke(deployer, "removeServiced", new Object[] { contextName }, + new String[] { "java.lang.String" }); + } + } else { + throw new IllegalStateException( + sm.getString("mBeanFactory.contextCreate.addServicedFail", contextName)); + } + } else { + log.warn(sm.getString("mBeanFactory.noDeployer", pname.getKeyProperty("host"))); + Service service = getService(pname); + Engine engine = service.getContainer(); + Host host = (Host) engine.findChild(pname.getKeyProperty("host")); + host.addChild(context); + } + + // Return the corresponding MBean name + return context.getObjectName().toString(); + } + + + /** + * Create a new StandardHost. + * + * @param parent MBean Name of the associated parent component + * @param name Unique name of this Host + * @param appBase Application base directory name + * @param autoDeploy Should we auto deploy? + * @param deployOnStartup Deploy on server startup? + * @param deployXML Should we deploy Context XML config files property? + * @param unpackWARs Should we unpack WARs when auto deploying? + * + * @return the object name of the created host + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createStandardHost(String parent, String name, String appBase, boolean autoDeploy, + boolean deployOnStartup, boolean deployXML, boolean unpackWARs) throws Exception { + + // Create a new StandardHost instance + StandardHost host = new StandardHost(); + host.setName(name); + host.setAppBase(appBase); + host.setAutoDeploy(autoDeploy); + host.setDeployOnStartup(deployOnStartup); + host.setDeployXML(deployXML); + host.setUnpackWARs(unpackWARs); + + // add HostConfig for active reloading + HostConfig hostConfig = new HostConfig(); + host.addLifecycleListener(hostConfig); + + // Add the new instance to its parent component + ObjectName pname = new ObjectName(parent); + Service service = getService(pname); + Engine engine = service.getContainer(); + engine.addChild(host); + + // Return the corresponding MBean name + return host.getObjectName().toString(); + + } + + + /** + * Creates a new StandardService and StandardEngine. + * + * @param domain Domain name for the container instance + * @param defaultHost Name of the default host to be used in the Engine + * @param baseDir Base directory value for Engine + * + * @return the object name of the created service + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createStandardServiceEngine(String domain, String defaultHost, String baseDir) throws Exception { + + if (!(container instanceof Server)) { + throw new Exception(sm.getString("mBeanFactory.notServer")); + } + + StandardEngine engine = new StandardEngine(); + engine.setDomain(domain); + engine.setName(domain); + engine.setDefaultHost(defaultHost); + + Service service = new StandardService(); + service.setContainer(engine); + service.setName(domain); + + ((Server) container).addService(service); + + return engine.getObjectName().toString(); + } + + + /** + * Create a new StandardManager. + * + * @param parent MBean Name of the associated parent component + * + * @return the object name of the created manager + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createStandardManager(String parent) throws Exception { + + // Create a new StandardManager instance + StandardManager manager = new StandardManager(); + + // Add the new instance to its parent component + ObjectName pname = new ObjectName(parent); + Container container = getParentContainerFromParent(pname); + if (container instanceof Context) { + ((Context) container).setManager(manager); + } else { + throw new Exception(sm.getString("mBeanFactory.managerContext")); + } + ObjectName oname = manager.getObjectName(); + if (oname != null) { + return oname.toString(); + } else { + return null; + } + + } + + + /** + * Create a new UserDatabaseRealm. + * + * @param parent MBean Name of the associated parent component + * @param resourceName Global JNDI resource name of the associated UserDatabase + * + * @return the object name of the created realm + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createUserDatabaseRealm(String parent, String resourceName) throws Exception { + + // Create a new UserDatabaseRealm instance + UserDatabaseRealm realm = new UserDatabaseRealm(); + realm.setResourceName(resourceName); + + // Add the new instance to its parent component + return addRealmToParent(parent, realm); + } + + + /** + * Create a new Valve and associate it with a {@link Container}. + * + * @param className The fully qualified class name of the {@link Valve} to create + * @param parent The MBean name of the associated parent {@link Container}. + * + * @return The MBean name of the {@link Valve} that was created or null if the {@link Valve} does not + * implement {@link JmxEnabled}. + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createValve(String className, String parent) throws Exception { + + // Look for the parent + ObjectName parentName = new ObjectName(parent); + Container container = getParentContainerFromParent(parentName); + + if (container == null) { + throw new IllegalArgumentException(sm.getString("mBeanFactory.noParent", parent)); + } + + Valve valve = (Valve) Class.forName(className).getConstructor().newInstance(); + + container.getPipeline().addValve(valve); + + if (valve instanceof JmxEnabled) { + return ((JmxEnabled) valve).getObjectName().toString(); + } else { + return null; + } + } + + + /** + * Create a new Web Application Loader. + * + * @param parent MBean Name of the associated parent component + * + * @return the object name of the created loader + * + * @exception Exception if an MBean cannot be created or registered + */ + public String createWebappLoader(String parent) throws Exception { + + // Create a new WebappLoader instance + WebappLoader loader = new WebappLoader(); + + // Add the new instance to its parent component + ObjectName pname = new ObjectName(parent); + Container container = getParentContainerFromParent(pname); + if (container instanceof Context) { + ((Context) container).setLoader(loader); + } + return loader.getObjectName().toString(); + + } + + + /** + * Remove an existing Connector. + * + * @param name MBean Name of the component to remove + * + * @exception Exception if a component cannot be removed + */ + public void removeConnector(String name) throws Exception { + + // Acquire a reference to the component to be removed + ObjectName oname = new ObjectName(name); + Service service = getService(oname); + String port = oname.getKeyProperty("port"); + String address = oname.getKeyProperty("address"); + if (address != null) { + address = ObjectName.unquote(address); + } + + Connector conns[] = service.findConnectors(); + + for (Connector conn : conns) { + String connAddress = null; + Object objConnAddress = conn.getProperty("address"); + if (objConnAddress != null) { + connAddress = ((InetAddress) objConnAddress).getHostAddress(); + } + String connPort = "" + conn.getPortWithOffset(); + + if (address == null) { + // Don't combine this with outer if or we could get an NPE in + // 'else if' below + if (connAddress == null && port.equals(connPort)) { + service.removeConnector(conn); + conn.destroy(); + break; + } + } else if (address.equals(connAddress) && port.equals(connPort)) { + service.removeConnector(conn); + conn.destroy(); + break; + } + } + } + + + /** + * Remove an existing Context. + * + * @param contextName MBean Name of the component to remove + * + * @exception Exception if a component cannot be removed + */ + public void removeContext(String contextName) throws Exception { + + // Acquire a reference to the component to be removed + ObjectName oname = new ObjectName(contextName); + String domain = oname.getDomain(); + StandardService service = (StandardService) getService(oname); + + Engine engine = service.getContainer(); + String name = oname.getKeyProperty("name"); + name = name.substring(2); + int i = name.indexOf('/'); + String hostName = name.substring(0, i); + String path = name.substring(i); + ObjectName deployer = new ObjectName(domain + ":type=Deployer,host=" + hostName); + String pathStr = getPathStr(path); + if (mserver.isRegistered(deployer)) { + Boolean result = (Boolean) mserver.invoke(deployer, "tryAddServiced", new Object[] { pathStr }, + new String[] { "java.lang.String" }); + if (result.booleanValue()) { + try { + mserver.invoke(deployer, "unmanageApp", new Object[] { pathStr }, + new String[] { "java.lang.String" }); + } finally { + mserver.invoke(deployer, "removeServiced", new Object[] { pathStr }, + new String[] { "java.lang.String" }); + } + } else { + throw new IllegalStateException(sm.getString("mBeanFactory.removeContext.addServicedFail", pathStr)); + } + } else { + log.warn(sm.getString("mBeanFactory.noDeployer", hostName)); + Host host = (Host) engine.findChild(hostName); + Context context = (Context) host.findChild(pathStr); + // Remove this component from its parent component + host.removeChild(context); + if (context instanceof StandardContext) { + try { + context.destroy(); + } catch (Exception e) { + log.warn(sm.getString("mBeanFactory.contextDestroyError"), e); + } + } + + } + } + + + /** + * Remove an existing Host. + * + * @param name MBean Name of the component to remove + * + * @exception Exception if a component cannot be removed + */ + public void removeHost(String name) throws Exception { + + // Acquire a reference to the component to be removed + ObjectName oname = new ObjectName(name); + String hostName = oname.getKeyProperty("host"); + Service service = getService(oname); + Engine engine = service.getContainer(); + Host host = (Host) engine.findChild(hostName); + + // Remove this component from its parent component + if (host != null) { + engine.removeChild(host); + } + } + + + /** + * Remove an existing Loader. + * + * @param name MBean Name of the component to remove + * + * @exception Exception if a component cannot be removed + */ + public void removeLoader(String name) throws Exception { + + ObjectName oname = new ObjectName(name); + // Acquire a reference to the component to be removed + Container container = getParentContainerFromChild(oname); + if (container instanceof Context) { + ((Context) container).setLoader(null); + } + } + + + /** + * Remove an existing Manager. + * + * @param name MBean Name of the component to remove + * + * @exception Exception if a component cannot be removed + */ + public void removeManager(String name) throws Exception { + + ObjectName oname = new ObjectName(name); + // Acquire a reference to the component to be removed + Container container = getParentContainerFromChild(oname); + if (container instanceof Context) { + ((Context) container).setManager(null); + } + } + + + /** + * Remove an existing Realm. + * + * @param name MBean Name of the component to remove + * + * @exception Exception if a component cannot be removed + */ + public void removeRealm(String name) throws Exception { + + ObjectName oname = new ObjectName(name); + // Acquire a reference to the component to be removed + Container container = getParentContainerFromChild(oname); + container.setRealm(null); + } + + + /** + * Remove an existing Service. + * + * @param name MBean Name of the component to remove + * + * @exception Exception if a component cannot be removed + */ + public void removeService(String name) throws Exception { + + if (!(container instanceof Server)) { + throw new Exception(sm.getString("mBeanFactory.notServer")); + } + + // Acquire a reference to the component to be removed + ObjectName oname = new ObjectName(name); + Service service = getService(oname); + ((Server) container).removeService(service); + } + + + /** + * Remove an existing Valve. + * + * @param name MBean Name of the component to remove + * + * @exception Exception if a component cannot be removed + */ + public void removeValve(String name) throws Exception { + + // Acquire a reference to the component to be removed + ObjectName oname = new ObjectName(name); + Container container = getParentContainerFromChild(oname); + Valve[] valves = container.getPipeline().getValves(); + for (Valve valve : valves) { + ObjectName voname = ((JmxEnabled) valve).getObjectName(); + if (voname.equals(oname)) { + container.getPipeline().removeValve(valve); + } + } + } + +} + diff --git a/java/org/apache/catalina/mbeans/MBeanUtils.java b/java/org/apache/catalina/mbeans/MBeanUtils.java new file mode 100644 index 0000000..7ebf332 --- /dev/null +++ b/java/org/apache/catalina/mbeans/MBeanUtils.java @@ -0,0 +1,762 @@ +/* + * 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.catalina.mbeans; + +import java.util.Set; + +import javax.management.DynamicMBean; +import javax.management.MBeanException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.Server; +import org.apache.catalina.User; +import org.apache.catalina.UserDatabase; +import org.apache.catalina.util.ContextName; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextResource; +import org.apache.tomcat.util.descriptor.web.ContextResourceLink; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Public utility methods in support of the server side MBeans implementation. + * + * @author Craig R. McClanahan + * @author Amy Roh + */ +public class MBeanUtils { + + // ------------------------------------------------------- Static Variables + + protected static final StringManager sm = StringManager.getManager(MBeanUtils.class); + + /** + * The set of exceptions to the normal rules used by createManagedBean(). The first element of each + * pair is a class name, and the second element is the managed bean name. + */ + private static final String exceptions[][] = { { "org.apache.catalina.users.MemoryGroup", "Group" }, + { "org.apache.catalina.users.MemoryRole", "Role" }, { "org.apache.catalina.users.MemoryUser", "User" }, + { "org.apache.catalina.users.GenericGroup", "Group" }, { "org.apache.catalina.users.GenericRole", "Role" }, + { "org.apache.catalina.users.GenericUser", "User" } }; + + + /** + * The configuration information registry for our managed beans. + */ + private static Registry registry = createRegistry(); + + + /** + * The MBeanServer for this application. + */ + private static MBeanServer mserver = createServer(); + + + // --------------------------------------------------------- Static Methods + + /** + * Create and return the name of the ManagedBean that corresponds to this Catalina component. + * + * @param component The component for which to create a name + */ + static String createManagedName(Object component) { + + // Deal with exceptions to the standard rule + String className = component.getClass().getName(); + for (String[] exception : exceptions) { + if (className.equals(exception[0])) { + return exception[1]; + } + } + + // Perform the standard transformation + int period = className.lastIndexOf('.'); + if (period >= 0) { + className = className.substring(period + 1); + } + return className; + + } + + + /** + * Create, register, and return an MBean for this ContextEnvironment object. + * + * @param environment The ContextEnvironment to be managed + * + * @return a new MBean + * + * @exception Exception if an MBean cannot be created or registered + */ + public static DynamicMBean createMBean(ContextEnvironment environment) throws Exception { + + String mname = createManagedName(environment); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname)); + throw new MBeanException(e); + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + DynamicMBean mbean = managed.createMBean(environment); + ObjectName oname = createObjectName(domain, environment); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + mserver.registerMBean(mbean, oname); + return mbean; + + } + + + /** + * Create, register, and return an MBean for this ContextResource object. + * + * @param resource The ContextResource to be managed + * + * @return a new MBean + * + * @exception Exception if an MBean cannot be created or registered + */ + public static DynamicMBean createMBean(ContextResource resource) throws Exception { + + String mname = createManagedName(resource); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname)); + throw new MBeanException(e); + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + DynamicMBean mbean = managed.createMBean(resource); + ObjectName oname = createObjectName(domain, resource); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + mserver.registerMBean(mbean, oname); + return mbean; + + } + + + /** + * Create, register, and return an MBean for this ContextResourceLink object. + * + * @param resourceLink The ContextResourceLink to be managed + * + * @return a new MBean + * + * @exception Exception if an MBean cannot be created or registered + */ + public static DynamicMBean createMBean(ContextResourceLink resourceLink) throws Exception { + + String mname = createManagedName(resourceLink); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname)); + throw new MBeanException(e); + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + DynamicMBean mbean = managed.createMBean(resourceLink); + ObjectName oname = createObjectName(domain, resourceLink); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + mserver.registerMBean(mbean, oname); + return mbean; + + } + + + /** + * Create, register, and return an MBean for this Group object. + * + * @param group The Group to be managed + * + * @return a new MBean + * + * @exception Exception if an MBean cannot be created or registered + */ + static DynamicMBean createMBean(Group group) throws Exception { + + String mname = createManagedName(group); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname)); + throw new MBeanException(e); + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + DynamicMBean mbean = managed.createMBean(group); + ObjectName oname = createObjectName(domain, group); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + mserver.registerMBean(mbean, oname); + return mbean; + + } + + + /** + * Create, register, and return an MBean for this Role object. + * + * @param role The Role to be managed + * + * @return a new MBean + * + * @exception Exception if an MBean cannot be created or registered + */ + static DynamicMBean createMBean(Role role) throws Exception { + + String mname = createManagedName(role); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname)); + throw new MBeanException(e); + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + DynamicMBean mbean = managed.createMBean(role); + ObjectName oname = createObjectName(domain, role); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + mserver.registerMBean(mbean, oname); + return mbean; + + } + + + /** + * Create, register, and return an MBean for this User object. + * + * @param user The User to be managed + * + * @return a new MBean + * + * @exception Exception if an MBean cannot be created or registered + */ + static DynamicMBean createMBean(User user) throws Exception { + + String mname = createManagedName(user); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname)); + throw new MBeanException(e); + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + DynamicMBean mbean = managed.createMBean(user); + ObjectName oname = createObjectName(domain, user); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + mserver.registerMBean(mbean, oname); + return mbean; + + } + + + /** + * Create, register, and return an MBean for this UserDatabase object. + * + * @param userDatabase The UserDatabase to be managed + * + * @return a new MBean + * + * @exception Exception if an MBean cannot be created or registered + */ + static DynamicMBean createMBean(UserDatabase userDatabase) throws Exception { + + if (userDatabase.isSparse()) { + // Register a sparse database bean as well + ManagedBean managed = registry.findManagedBean("SparseUserDatabase"); + if (managed == null) { + Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", "SparseUserDatabase")); + throw new MBeanException(e); + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + DynamicMBean mbean = managed.createMBean(userDatabase); + ObjectName oname = createObjectName(domain, userDatabase); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + mserver.registerMBean(mbean, oname); + } + + String mname = createManagedName(userDatabase); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname)); + throw new MBeanException(e); + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + DynamicMBean mbean = managed.createMBean(userDatabase); + ObjectName oname = createObjectName(domain, userDatabase); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + mserver.registerMBean(mbean, oname); + return mbean; + + } + + + /** + * Create an ObjectName for this Service object. + * + * @param domain Domain in which this name is to be created + * @param environment The ContextEnvironment to be named + * + * @return a new object name + * + * @exception MalformedObjectNameException if a name cannot be created + */ + public static ObjectName createObjectName(String domain, ContextEnvironment environment) + throws MalformedObjectNameException { + + ObjectName name = null; + Object container = environment.getNamingResources().getContainer(); + if (container instanceof Server) { + name = new ObjectName(domain + ":type=Environment" + ",resourcetype=Global,name=" + environment.getName()); + } else if (container instanceof Context) { + Context context = ((Context) container); + ContextName cn = new ContextName(context.getName(), false); + Container host = context.getParent(); + name = new ObjectName(domain + ":type=Environment" + ",resourcetype=Context,host=" + host.getName() + + ",context=" + cn.getDisplayName() + ",name=" + environment.getName()); + } + return name; + + } + + + /** + * Create an ObjectName for this ContextResource object. + * + * @param domain Domain in which this name is to be created + * @param resource The ContextResource to be named + * + * @return a new object name + * + * @exception MalformedObjectNameException if a name cannot be created + */ + public static ObjectName createObjectName(String domain, ContextResource resource) + throws MalformedObjectNameException { + + ObjectName name = null; + String quotedResourceName = ObjectName.quote(resource.getName()); + Object container = resource.getNamingResources().getContainer(); + if (container instanceof Server) { + name = new ObjectName(domain + ":type=Resource" + ",resourcetype=Global,class=" + resource.getType() + + ",name=" + quotedResourceName); + } else if (container instanceof Context) { + Context context = ((Context) container); + ContextName cn = new ContextName(context.getName(), false); + Container host = context.getParent(); + name = new ObjectName(domain + ":type=Resource" + ",resourcetype=Context,host=" + host.getName() + + ",context=" + cn.getDisplayName() + ",class=" + resource.getType() + ",name=" + quotedResourceName); + } + + return name; + + } + + + /** + * Create an ObjectName for this ContextResourceLink object. + * + * @param domain Domain in which this name is to be created + * @param resourceLink The ContextResourceLink to be named + * + * @return a new object name + * + * @exception MalformedObjectNameException if a name cannot be created + */ + public static ObjectName createObjectName(String domain, ContextResourceLink resourceLink) + throws MalformedObjectNameException { + + ObjectName name = null; + String quotedResourceLinkName = ObjectName.quote(resourceLink.getName()); + Object container = resourceLink.getNamingResources().getContainer(); + if (container instanceof Server) { + name = new ObjectName( + domain + ":type=ResourceLink" + ",resourcetype=Global" + ",name=" + quotedResourceLinkName); + } else if (container instanceof Context) { + Context context = ((Context) container); + ContextName cn = new ContextName(context.getName(), false); + Container host = context.getParent(); + name = new ObjectName(domain + ":type=ResourceLink" + ",resourcetype=Context,host=" + host.getName() + + ",context=" + cn.getDisplayName() + ",name=" + quotedResourceLinkName); + } + + return name; + + } + + + /** + * Create an ObjectName for this Group object. + * + * @param domain Domain in which this name is to be created + * @param group The Group to be named + * + * @return a new object name + * + * @exception MalformedObjectNameException if a name cannot be created + */ + static ObjectName createObjectName(String domain, Group group) throws MalformedObjectNameException { + + ObjectName name = null; + name = new ObjectName(domain + ":type=Group,groupname=" + ObjectName.quote(group.getGroupname()) + + ",database=" + group.getUserDatabase().getId()); + return name; + + } + + + /** + * Create an ObjectName for this Role object. + * + * @param domain Domain in which this name is to be created + * @param role The Role to be named + * + * @return a new object name + * + * @exception MalformedObjectNameException if a name cannot be created + */ + static ObjectName createObjectName(String domain, Role role) throws MalformedObjectNameException { + + ObjectName name = new ObjectName(domain + ":type=Role,rolename=" + ObjectName.quote(role.getRolename()) + + ",database=" + role.getUserDatabase().getId()); + return name; + } + + + /** + * Create an ObjectName for this User object. + * + * @param domain Domain in which this name is to be created + * @param user The User to be named + * + * @return a new object name + * + * @exception MalformedObjectNameException if a name cannot be created + */ + static ObjectName createObjectName(String domain, User user) throws MalformedObjectNameException { + + ObjectName name = new ObjectName(domain + ":type=User,username=" + ObjectName.quote(user.getUsername()) + + ",database=" + user.getUserDatabase().getId()); + return name; + } + + + /** + * Create an ObjectName for this UserDatabase object. + * + * @param domain Domain in which this name is to be created + * @param userDatabase The UserDatabase to be named + * + * @return a new object name + * + * @exception MalformedObjectNameException if a name cannot be created + */ + static ObjectName createObjectName(String domain, UserDatabase userDatabase) throws MalformedObjectNameException { + + ObjectName name = null; + name = new ObjectName(domain + ":type=UserDatabase,database=" + userDatabase.getId()); + return name; + + } + + /** + * Create and configure (if necessary) and return the registry of managed object descriptions. + * + * @return the singleton registry + */ + public static synchronized Registry createRegistry() { + if (registry == null) { + registry = Registry.getRegistry(null, null); + ClassLoader cl = MBeanUtils.class.getClassLoader(); + + registry.loadDescriptors("org.apache.catalina.mbeans", cl); + registry.loadDescriptors("org.apache.catalina.authenticator", cl); + registry.loadDescriptors("org.apache.catalina.core", cl); + registry.loadDescriptors("org.apache.catalina", cl); + registry.loadDescriptors("org.apache.catalina.deploy", cl); + registry.loadDescriptors("org.apache.catalina.loader", cl); + registry.loadDescriptors("org.apache.catalina.realm", cl); + registry.loadDescriptors("org.apache.catalina.session", cl); + registry.loadDescriptors("org.apache.catalina.startup", cl); + registry.loadDescriptors("org.apache.catalina.users", cl); + registry.loadDescriptors("org.apache.catalina.ha", cl); + registry.loadDescriptors("org.apache.catalina.connector", cl); + registry.loadDescriptors("org.apache.catalina.valves", cl); + registry.loadDescriptors("org.apache.catalina.storeconfig", cl); + registry.loadDescriptors("org.apache.tomcat.util.descriptor.web", cl); + } + return registry; + } + + + /** + * Create and configure (if necessary) and return the MBeanServer with which we will be registering our + * DynamicMBean implementations. + * + * @return the singleton MBean server + */ + public static synchronized MBeanServer createServer() { + if (mserver == null) { + mserver = Registry.getRegistry(null, null).getMBeanServer(); + } + return mserver; + } + + + /** + * Deregister the MBean for this ContextEnvironment object. + * + * @param environment The ContextEnvironment to be managed + * + * @exception Exception if an MBean cannot be deregistered + */ + public static void destroyMBean(ContextEnvironment environment) throws Exception { + + String mname = createManagedName(environment); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + return; + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + ObjectName oname = createObjectName(domain, environment); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + + } + + + /** + * Deregister the MBean for this ContextResource object. + * + * @param resource The ContextResource to be managed + * + * @exception Exception if an MBean cannot be deregistered + */ + public static void destroyMBean(ContextResource resource) throws Exception { + + // If this is a user database resource need to destroy groups, roles, + // users and UserDatabase mbean + if ("org.apache.catalina.UserDatabase".equals(resource.getType())) { + destroyMBeanUserDatabase(resource.getName()); + } + + String mname = createManagedName(resource); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + return; + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + ObjectName oname = createObjectName(domain, resource); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + + } + + + /** + * Deregister the MBean for this ContextResourceLink object. + * + * @param resourceLink The ContextResourceLink to be managed + * + * @exception Exception if an MBean cannot be deregistered + */ + public static void destroyMBean(ContextResourceLink resourceLink) throws Exception { + + String mname = createManagedName(resourceLink); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + return; + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + ObjectName oname = createObjectName(domain, resourceLink); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + + } + + /** + * Deregister the MBean for this Group object. + * + * @param group The Group to be managed + * + * @exception Exception if an MBean cannot be deregistered + */ + static void destroyMBean(Group group) throws Exception { + + String mname = createManagedName(group); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + return; + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + ObjectName oname = createObjectName(domain, group); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + + } + + + /** + * Deregister the MBean for this Role object. + * + * @param role The Role to be managed + * + * @exception Exception if an MBean cannot be deregistered + */ + static void destroyMBean(Role role) throws Exception { + + String mname = createManagedName(role); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + return; + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + ObjectName oname = createObjectName(domain, role); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + + } + + + /** + * Deregister the MBean for this User object. + * + * @param user The User to be managed + * + * @exception Exception if an MBean cannot be deregistered + */ + static void destroyMBean(User user) throws Exception { + + String mname = createManagedName(user); + ManagedBean managed = registry.findManagedBean(mname); + if (managed == null) { + return; + } + String domain = managed.getDomain(); + if (domain == null) { + domain = mserver.getDefaultDomain(); + } + ObjectName oname = createObjectName(domain, user); + if (mserver.isRegistered(oname)) { + mserver.unregisterMBean(oname); + } + + } + + + /** + * Deregister the MBean for the UserDatabase object with this name. + * + * @param userDatabase The UserDatabase to be managed + * + * @exception Exception if an MBean cannot be deregistered + */ + static void destroyMBeanUserDatabase(String userDatabase) throws Exception { + + ObjectName query = null; + Set results = null; + + // Groups + query = new ObjectName("Users:type=Group,database=" + userDatabase + ",*"); + results = mserver.queryNames(query, null); + for (ObjectName result : results) { + mserver.unregisterMBean(result); + } + + // Roles + query = new ObjectName("Users:type=Role,database=" + userDatabase + ",*"); + results = mserver.queryNames(query, null); + for (ObjectName result : results) { + mserver.unregisterMBean(result); + } + + // Users + query = new ObjectName("Users:type=User,database=" + userDatabase + ",*"); + results = mserver.queryNames(query, null); + for (ObjectName result : results) { + mserver.unregisterMBean(result); + } + + // The database itself + ObjectName db = new ObjectName("Users:type=UserDatabase,database=" + userDatabase); + if (mserver.isRegistered(db)) { + mserver.unregisterMBean(db); + } + db = new ObjectName("Catalina:type=UserDatabase,database=" + userDatabase); + if (mserver.isRegistered(db)) { + mserver.unregisterMBean(db); + } + } +} diff --git a/java/org/apache/catalina/mbeans/MemoryUserDatabaseMBean.java b/java/org/apache/catalina/mbeans/MemoryUserDatabaseMBean.java new file mode 100644 index 0000000..950a567 --- /dev/null +++ b/java/org/apache/catalina/mbeans/MemoryUserDatabaseMBean.java @@ -0,0 +1,36 @@ +/* + * 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.catalina.mbeans; + +import org.apache.tomcat.util.modeler.ManagedBean; + +/** + *

    + * A ModelMBean implementation for the org.apache.catalina.users.MemoryUserDatabase + * component. + *

    + * + * @author Craig R. McClanahan + */ +public class MemoryUserDatabaseMBean extends SparseUserDatabaseMBean { + + /** + * The ManagedBean information describing this MBean. + */ + protected final ManagedBean managed = registry.findManagedBean("MemoryUserDatabase"); + +} diff --git a/java/org/apache/catalina/mbeans/NamingResourcesMBean.java b/java/org/apache/catalina/mbeans/NamingResourcesMBean.java new file mode 100644 index 0000000..0a5c231 --- /dev/null +++ b/java/org/apache/catalina/mbeans/NamingResourcesMBean.java @@ -0,0 +1,285 @@ +/* + * 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.catalina.mbeans; + +import java.util.ArrayList; +import java.util.List; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextResource; +import org.apache.tomcat.util.descriptor.web.ContextResourceLink; +import org.apache.tomcat.util.modeler.BaseModelMBean; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * A ModelMBean implementation for the org.apache.catalina.deploy.NamingResourcesImpl + * component. + *

    + * + * @author Amy Roh + */ +public class NamingResourcesMBean extends BaseModelMBean { + + private static final StringManager sm = StringManager.getManager(NamingResourcesMBean.class); + + // ----------------------------------------------------- Instance Variables + + /** + * The configuration information registry for our managed beans. + */ + protected final Registry registry = MBeanUtils.createRegistry(); + + + /** + * The ManagedBean information describing this MBean. + */ + protected final ManagedBean managed = registry.findManagedBean("NamingResources"); + + + // ------------------------------------------------------------- Attributes + + /** + * Return the MBean Names of the set of defined environment entries for this web application + * + * @return an array of object names as strings + */ + public String[] getEnvironments() { + ContextEnvironment[] envs = ((NamingResourcesImpl) this.resource).findEnvironments(); + List results = new ArrayList<>(); + for (ContextEnvironment env : envs) { + try { + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), env); + results.add(oname.toString()); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.createObjectNameError.environment", env), e); + } + } + return results.toArray(new String[0]); + } + + + /** + * Return the MBean Names of all the defined resource references for this application. + * + * @return an array of object names as strings + */ + public String[] getResources() { + ContextResource[] resources = ((NamingResourcesImpl) this.resource).findResources(); + List results = new ArrayList<>(); + for (ContextResource contextResource : resources) { + try { + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), contextResource); + results.add(oname.toString()); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.createObjectNameError.resource", contextResource), e); + } + } + return results.toArray(new String[0]); + } + + + /** + * Return the MBean Names of all the defined resource link references for this application. + * + * @return an array of object names as strings + */ + public String[] getResourceLinks() { + ContextResourceLink[] resourceLinks = ((NamingResourcesImpl) this.resource).findResourceLinks(); + List results = new ArrayList<>(); + for (ContextResourceLink resourceLink : resourceLinks) { + try { + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), resourceLink); + results.add(oname.toString()); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.createObjectNameError.resourceLink", resourceLink), e); + } + } + return results.toArray(new String[0]); + } + + + // ------------------------------------------------------------- Operations + + /** + * Add an environment entry for this web application. + * + * @param envName New environment entry name + * @param type The type of the new environment entry + * @param value The value of the new environment entry + * + * @return the object name of the new environment entry + * + * @throws MalformedObjectNameException if the object name was invalid + */ + public String addEnvironment(String envName, String type, String value) throws MalformedObjectNameException { + + NamingResourcesImpl nresources = (NamingResourcesImpl) this.resource; + if (nresources == null) { + return null; + } + ContextEnvironment env = nresources.findEnvironment(envName); + if (env != null) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.addAlreadyExists.environment", envName)); + } + env = new ContextEnvironment(); + env.setName(envName); + env.setType(type); + env.setValue(value); + nresources.addEnvironment(env); + + // Return the corresponding MBean name + ManagedBean managed = registry.findManagedBean("ContextEnvironment"); + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), env); + return oname.toString(); + } + + + /** + * Add a resource reference for this web application. + * + * @param resourceName New resource reference name + * @param type New resource reference type + * + * @return the object name of the new resource + * + * @throws MalformedObjectNameException if the object name was invalid + */ + public String addResource(String resourceName, String type) throws MalformedObjectNameException { + + NamingResourcesImpl nresources = (NamingResourcesImpl) this.resource; + if (nresources == null) { + return null; + } + ContextResource resource = nresources.findResource(resourceName); + if (resource != null) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.addAlreadyExists.resource", resourceName)); + } + resource = new ContextResource(); + resource.setName(resourceName); + resource.setType(type); + nresources.addResource(resource); + + // Return the corresponding MBean name + ManagedBean managed = registry.findManagedBean("ContextResource"); + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), resource); + return oname.toString(); + } + + + /** + * Add a resource link reference for this web application. + * + * @param resourceLinkName New resource link reference name + * @param type New resource link reference type + * + * @return the object name of the new resource link + * + * @throws MalformedObjectNameException if the object name was invalid + */ + public String addResourceLink(String resourceLinkName, String type) throws MalformedObjectNameException { + + NamingResourcesImpl nresources = (NamingResourcesImpl) this.resource; + if (nresources == null) { + return null; + } + ContextResourceLink resourceLink = nresources.findResourceLink(resourceLinkName); + if (resourceLink != null) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.addAlreadyExists.resourceLink", resourceLinkName)); + } + resourceLink = new ContextResourceLink(); + resourceLink.setName(resourceLinkName); + resourceLink.setType(type); + nresources.addResourceLink(resourceLink); + + // Return the corresponding MBean name + ManagedBean managed = registry.findManagedBean("ContextResourceLink"); + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), resourceLink); + return oname.toString(); + } + + + /** + * Remove any environment entry with the specified name. + * + * @param envName Name of the environment entry to remove + */ + public void removeEnvironment(String envName) { + NamingResourcesImpl nresources = (NamingResourcesImpl) this.resource; + if (nresources == null) { + return; + } + ContextEnvironment env = nresources.findEnvironment(envName); + if (env == null) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.removeNotFound.environment", envName)); + } + nresources.removeEnvironment(envName); + } + + + /** + * Remove any resource reference with the specified name. + * + * @param resourceName Name of the resource reference to remove + */ + public void removeResource(String resourceName) { + resourceName = ObjectName.unquote(resourceName); + NamingResourcesImpl nresources = (NamingResourcesImpl) this.resource; + if (nresources == null) { + return; + } + ContextResource resource = nresources.findResource(resourceName); + if (resource == null) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.removeNotFound.resource", resourceName)); + } + nresources.removeResource(resourceName); + } + + + /** + * Remove any resource link reference with the specified name. + * + * @param resourceLinkName Name of the resource link reference to remove + */ + public void removeResourceLink(String resourceLinkName) { + resourceLinkName = ObjectName.unquote(resourceLinkName); + NamingResourcesImpl nresources = (NamingResourcesImpl) this.resource; + if (nresources == null) { + return; + } + ContextResourceLink resourceLink = nresources.findResourceLink(resourceLinkName); + if (resourceLink == null) { + throw new IllegalArgumentException( + sm.getString("namingResourcesMBean.removeNotFound.resourceLink", resourceLinkName)); + } + nresources.removeResourceLink(resourceLinkName); + } +} diff --git a/java/org/apache/catalina/mbeans/RoleMBean.java b/java/org/apache/catalina/mbeans/RoleMBean.java new file mode 100644 index 0000000..7ffd9f4 --- /dev/null +++ b/java/org/apache/catalina/mbeans/RoleMBean.java @@ -0,0 +1,44 @@ +/* + * 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.catalina.mbeans; + +import org.apache.tomcat.util.modeler.BaseModelMBean; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; + +/** + *

    + * A ModelMBean implementation for the org.apache.catalina.Role component. + *

    + * + * @author Craig R. McClanahan + */ +public class RoleMBean extends BaseModelMBean { + + // ----------------------------------------------------- Instance Variables + + /** + * The configuration information registry for our managed beans. + */ + protected final Registry registry = MBeanUtils.createRegistry(); + + + /** + * The ManagedBean information describing this MBean. + */ + protected final ManagedBean managed = registry.findManagedBean("Role"); +} diff --git a/java/org/apache/catalina/mbeans/ServiceMBean.java b/java/org/apache/catalina/mbeans/ServiceMBean.java new file mode 100644 index 0000000..719783d --- /dev/null +++ b/java/org/apache/catalina/mbeans/ServiceMBean.java @@ -0,0 +1,129 @@ +/* + * 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.catalina.mbeans; + +import javax.management.MBeanException; + +import org.apache.catalina.Executor; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Connector; + +public class ServiceMBean extends BaseCatalinaMBean { + + /** + * Add a new Connector to the set of defined Connectors, and associate it with this Service's Container. + * + * @param address The IP address on which to bind + * @param port TCP port number to listen on + * @param isAjp Create a AJP/1.3 Connector + * @param isSSL Create a secure Connector + * + * @throws MBeanException error creating the connector + */ + public void addConnector(String address, int port, boolean isAjp, boolean isSSL) throws MBeanException { + + Service service = doGetManagedResource(); + String protocol = isAjp ? "AJP/1.3" : "HTTP/1.1"; + Connector connector = new Connector(protocol); + if ((address != null) && (address.length() > 0)) { + connector.setProperty("address", address); + } + connector.setPort(port); + connector.setSecure(isSSL); + connector.setScheme(isSSL ? "https" : "http"); + + service.addConnector(connector); + } + + + /** + * Adds a named executor to the service + * + * @param type Classname of the Executor to be added + * + * @throws MBeanException error creating the executor + */ + public void addExecutor(String type) throws MBeanException { + Service service = doGetManagedResource(); + Executor executor = (Executor) newInstance(type); + service.addExecutor(executor); + } + + + /** + * Find and return the set of Connectors associated with this Service. + * + * @return an array of string representations of the connectors + * + * @throws MBeanException error accessing the associated service + */ + public String[] findConnectors() throws MBeanException { + + Service service = doGetManagedResource(); + + Connector[] connectors = service.findConnectors(); + String[] str = new String[connectors.length]; + + for (int i = 0; i < connectors.length; i++) { + str[i] = connectors[i].toString(); + } + + return str; + } + + + /** + * Retrieves all executors. + * + * @return an array of string representations of the executors + * + * @throws MBeanException error accessing the associated service + */ + public String[] findExecutors() throws MBeanException { + + Service service = doGetManagedResource(); + + Executor[] executors = service.findExecutors(); + String[] str = new String[executors.length]; + + for (int i = 0; i < executors.length; i++) { + str[i] = executors[i].toString(); + } + + return str; + } + + + /** + * Retrieves executor by name + * + * @param name Name of the executor to be retrieved + * + * @return a string representation of the executor + * + * @throws MBeanException error accessing the associated service + */ + public String getExecutor(String name) throws MBeanException { + Service service = doGetManagedResource(); + Executor executor = service.getExecutor(name); + if (executor != null) { + return executor.toString(); + } else { + return null; + } + } +} diff --git a/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java b/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java new file mode 100644 index 0000000..9f1a767 --- /dev/null +++ b/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java @@ -0,0 +1,370 @@ +/* + * 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.catalina.mbeans; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.catalina.UserDatabase; +import org.apache.tomcat.util.modeler.BaseModelMBean; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * A ModelMBean implementation for the org.apache.catalina.users.SparseUserDatabase + * component. The main difference is that the MBeans are created on demand (for example, the findUser method would + * register the corresponding user and make it available for management. All the MBeans created for users, groups and + * roles are then discarded when save is invoked. + *

    + * + * @author Craig R. McClanahan + */ +public class SparseUserDatabaseMBean extends BaseModelMBean { + + private static final StringManager sm = StringManager.getManager(SparseUserDatabaseMBean.class); + + // ----------------------------------------------------- Instance Variables + + /** + * The configuration information registry for our managed beans. + */ + protected final Registry registry = MBeanUtils.createRegistry(); + + + /** + * The MBeanServer for this application. + */ + protected final MBeanServer mserver = MBeanUtils.createServer(); + + + /** + * The ManagedBean information describing this MBean. + */ + protected final ManagedBean managed = registry.findManagedBean("SparseUserDatabase"); + + + /** + * The ManagedBean information describing Group MBeans. + */ + protected final ManagedBean managedGroup = registry.findManagedBean("Group"); + + + /** + * The ManagedBean information describing Group MBeans. + */ + protected final ManagedBean managedRole = registry.findManagedBean("Role"); + + + /** + * The ManagedBean information describing User MBeans. + */ + protected final ManagedBean managedUser = registry.findManagedBean("User"); + + + // ------------------------------------------------------------- Attributes + + /** + * @return the MBean Names of all groups defined in this database. + */ + public String[] getGroups() { + UserDatabase database = (UserDatabase) this.resource; + List results = new ArrayList<>(); + Iterator groups = database.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + results.add(findGroup(group.getGroupname())); + } + return results.toArray(new String[0]); + } + + + /** + * @return the MBean Names of all roles defined in this database. + */ + public String[] getRoles() { + UserDatabase database = (UserDatabase) this.resource; + List results = new ArrayList<>(); + Iterator roles = database.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + results.add(findRole(role.getRolename())); + } + return results.toArray(new String[0]); + } + + + /** + * @return the MBean Names of all users defined in this database. + */ + public String[] getUsers() { + UserDatabase database = (UserDatabase) this.resource; + List results = new ArrayList<>(); + Iterator users = database.getUsers(); + while (users.hasNext()) { + User user = users.next(); + results.add(findUser(user.getUsername())); + } + return results.toArray(new String[0]); + } + + + // ------------------------------------------------------------- Operations + + /** + * Create a new Group and return the corresponding MBean Name. + * + * @param groupname Group name of the new group + * @param description Description of the new group + * + * @return the new group object name + */ + public String createGroup(String groupname, String description) { + UserDatabase database = (UserDatabase) this.resource; + Group group = database.createGroup(groupname, description); + try { + MBeanUtils.createMBean(group); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.group", groupname), e); + } + return findGroup(groupname); + } + + + /** + * Create a new Role and return the corresponding MBean Name. + * + * @param rolename Group name of the new group + * @param description Description of the new group + * + * @return the new role object name + */ + public String createRole(String rolename, String description) { + UserDatabase database = (UserDatabase) this.resource; + Role role = database.createRole(rolename, description); + try { + MBeanUtils.createMBean(role); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.role", rolename), e); + } + return findRole(rolename); + } + + + /** + * Create a new User and return the corresponding MBean Name. + * + * @param username User name of the new user + * @param password Password for the new user + * @param fullName Full name for the new user + * + * @return the new user object name + */ + public String createUser(String username, String password, String fullName) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.createUser(username, password, fullName); + try { + MBeanUtils.createMBean(user); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.user", username), e); + } + return findUser(username); + } + + + /** + * Return the MBean Name for the specified group name (if any); otherwise return null. + * + * @param groupname Group name to look up + * + * @return the group object name + */ + public String findGroup(String groupname) { + UserDatabase database = (UserDatabase) this.resource; + Group group = database.findGroup(groupname); + if (group == null) { + return null; + } + try { + ObjectName oname = MBeanUtils.createObjectName(managedGroup.getDomain(), group); + if (database.isSparse() && !mserver.isRegistered(oname)) { + MBeanUtils.createMBean(group); + } + return oname.toString(); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.createError.group", groupname), e); + } + } + + + /** + * Return the MBean Name for the specified role name (if any); otherwise return null. + * + * @param rolename Role name to look up + * + * @return the role object name + */ + public String findRole(String rolename) { + UserDatabase database = (UserDatabase) this.resource; + Role role = database.findRole(rolename); + if (role == null) { + return null; + } + try { + ObjectName oname = MBeanUtils.createObjectName(managedRole.getDomain(), role); + if (database.isSparse() && !mserver.isRegistered(oname)) { + MBeanUtils.createMBean(role); + } + return oname.toString(); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.createError.role", rolename), e); + } + + } + + + /** + * Return the MBean Name for the specified user name (if any); otherwise return null. + * + * @param username User name to look up + * + * @return the user object name + */ + public String findUser(String username) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + if (user == null) { + return null; + } + try { + ObjectName oname = MBeanUtils.createObjectName(managedUser.getDomain(), user); + if (database.isSparse() && !mserver.isRegistered(oname)) { + MBeanUtils.createMBean(user); + } + return oname.toString(); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.createError.user", username), e); + } + } + + + /** + * Remove an existing group and destroy the corresponding MBean. + * + * @param groupname Group name to remove + */ + public void removeGroup(String groupname) { + UserDatabase database = (UserDatabase) this.resource; + Group group = database.findGroup(groupname); + if (group == null) { + return; + } + try { + MBeanUtils.destroyMBean(group); + database.removeGroup(group); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.destroyError.group", groupname), e); + } + } + + + /** + * Remove an existing role and destroy the corresponding MBean. + * + * @param rolename Role name to remove + */ + public void removeRole(String rolename) { + UserDatabase database = (UserDatabase) this.resource; + Role role = database.findRole(rolename); + if (role == null) { + return; + } + try { + MBeanUtils.destroyMBean(role); + database.removeRole(role); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.destroyError.role", rolename), e); + } + } + + + /** + * Remove an existing user and destroy the corresponding MBean. + * + * @param username User name to remove + */ + public void removeUser(String username) { + UserDatabase database = (UserDatabase) this.resource; + User user = database.findUser(username); + if (user == null) { + return; + } + try { + MBeanUtils.destroyMBean(user); + database.removeUser(user); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.destroyError.user", username), e); + } + } + + + /** + * Call actual save and unregister all obsolete beans. + */ + public void save() { + try { + UserDatabase database = (UserDatabase) this.resource; + if (database.isSparse()) { + ObjectName query = null; + Set results = null; + + // Groups + query = new ObjectName("Users:type=Group,database=" + database.getId() + ",*"); + results = mserver.queryNames(query, null); + for (ObjectName result : results) { + mserver.unregisterMBean(result); + } + + // Roles + query = new ObjectName("Users:type=Role,database=" + database.getId() + ",*"); + results = mserver.queryNames(query, null); + for (ObjectName result : results) { + mserver.unregisterMBean(result); + } + + // Users + query = new ObjectName("Users:type=User,database=" + database.getId() + ",*"); + results = mserver.queryNames(query, null); + for (ObjectName result : results) { + mserver.unregisterMBean(result); + } + } + database.save(); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("userMBean.saveError"), e); + } + } +} diff --git a/java/org/apache/catalina/mbeans/UserMBean.java b/java/org/apache/catalina/mbeans/UserMBean.java new file mode 100644 index 0000000..5d9afa9 --- /dev/null +++ b/java/org/apache/catalina/mbeans/UserMBean.java @@ -0,0 +1,182 @@ +/* + * 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.catalina.mbeans; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.tomcat.util.modeler.BaseModelMBean; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * A ModelMBean implementation for the org.apache.catalina.User component. + *

    + * + * @author Craig R. McClanahan + */ +public class UserMBean extends BaseModelMBean { + + private static final StringManager sm = StringManager.getManager(UserMBean.class); + + // ----------------------------------------------------- Instance Variables + + /** + * The configuration information registry for our managed beans. + */ + protected final Registry registry = MBeanUtils.createRegistry(); + + + /** + * The ManagedBean information describing this MBean. + */ + protected final ManagedBean managed = registry.findManagedBean("User"); + + + // ------------------------------------------------------------- Attributes + + + /** + * @return the MBean Names of all groups this user is a member of. + */ + public String[] getGroups() { + + User user = (User) this.resource; + List results = new ArrayList<>(); + Iterator groups = user.getGroups(); + while (groups.hasNext()) { + Group group = null; + try { + group = groups.next(); + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), group); + results.add(oname.toString()); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException(sm.getString("userMBean.createError.group", group), e); + } + } + return results.toArray(new String[0]); + } + + + /** + * @return the MBean Names of all roles assigned to this user. + */ + public String[] getRoles() { + + User user = (User) this.resource; + List results = new ArrayList<>(); + Iterator roles = user.getRoles(); + while (roles.hasNext()) { + Role role = null; + try { + role = roles.next(); + ObjectName oname = MBeanUtils.createObjectName(managed.getDomain(), role); + results.add(oname.toString()); + } catch (MalformedObjectNameException e) { + throw new IllegalArgumentException(sm.getString("userMBean.createError.role", role), e); + } + } + return results.toArray(new String[0]); + } + + + // ------------------------------------------------------------- Operations + + /** + * Add a new {@link Group} to those this user belongs to. + * + * @param groupname Group name of the new group + */ + public void addGroup(String groupname) { + + User user = (User) this.resource; + if (user == null) { + return; + } + Group group = user.getUserDatabase().findGroup(groupname); + if (group == null) { + throw new IllegalArgumentException(sm.getString("userMBean.invalidGroup", groupname)); + } + user.addGroup(group); + } + + + /** + * Add a new {@link Role} to those this user belongs to. + * + * @param rolename Role name of the new role + */ + public void addRole(String rolename) { + + User user = (User) this.resource; + if (user == null) { + return; + } + Role role = user.getUserDatabase().findRole(rolename); + if (role == null) { + throw new IllegalArgumentException(sm.getString("userMBean.invalidRole", rolename)); + } + user.addRole(role); + } + + + /** + * Remove a {@link Group} from those this user belongs to. + * + * @param groupname Group name of the old group + */ + public void removeGroup(String groupname) { + + User user = (User) this.resource; + if (user == null) { + return; + } + Group group = user.getUserDatabase().findGroup(groupname); + if (group == null) { + throw new IllegalArgumentException(sm.getString("userMBean.invalidGroup", groupname)); + } + user.removeGroup(group); + } + + + /** + * Remove a {@link Role} from those this user belongs to. + * + * @param rolename Role name of the old role + */ + public void removeRole(String rolename) { + + User user = (User) this.resource; + if (user == null) { + return; + } + Role role = user.getUserDatabase().findRole(rolename); + if (role == null) { + throw new IllegalArgumentException(sm.getString("userMBean.invalidRole", rolename)); + } + user.removeRole(role); + } +} diff --git a/java/org/apache/catalina/mbeans/mbeans-descriptors.xml b/java/org/apache/catalina/mbeans/mbeans-descriptors.xml new file mode 100644 index 0000000..9597210 --- /dev/null +++ b/java/org/apache/catalina/mbeans/mbeans-descriptors.xml @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/realm/AuthenticatedUserRealm.java b/java/org/apache/catalina/realm/AuthenticatedUserRealm.java new file mode 100644 index 0000000..44cf8da --- /dev/null +++ b/java/org/apache/catalina/realm/AuthenticatedUserRealm.java @@ -0,0 +1,44 @@ +/* + * 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.catalina.realm; + +import java.security.Principal; + +/** + * This Realm is intended for use with Authenticator implementations + * ({@link org.apache.catalina.authenticator.SSLAuthenticator}, + * {@link org.apache.catalina.authenticator.SpnegoAuthenticator}) that authenticate the user as well as obtain the user + * credentials. An authenticated Principal is always created from the user name presented to without further validation. + *

    + * Note: It is unsafe to use this Realm with Authenticator implementations that do not validate the + * provided credentials. + */ +public class AuthenticatedUserRealm extends RealmBase { + + @Override + protected String getPassword(String username) { + // Passwords never need validating so always return null + return null; + } + + @Override + protected Principal getPrincipal(String username) { + // The authentication mechanism has authenticated the user so create + // the Principal directly + return new GenericPrincipal(username); + } +} diff --git a/java/org/apache/catalina/realm/CombinedRealm.java b/java/org/apache/catalina/realm/CombinedRealm.java new file mode 100644 index 0000000..0a97c34 --- /dev/null +++ b/java/org/apache/catalina/realm/CombinedRealm.java @@ -0,0 +1,422 @@ +/* + * 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.catalina.realm; + +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.CredentialHandler; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Realm; +import org.apache.catalina.Wrapper; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSName; + +/** + * Realm implementation that contains one or more realms. Authentication is attempted for each realm in the order they + * were configured. If any realm authenticates the user then the authentication succeeds. When combining realms + * usernames should be unique across all combined realms. + */ +public class CombinedRealm extends RealmBase { + + private static final Log log = LogFactory.getLog(CombinedRealm.class); + + /** + * The list of Realms contained by this Realm. + */ + protected final List realms = new ArrayList<>(); + + /** + * Add a realm to the list of realms that will be used to authenticate users. + * + * @param theRealm realm which should be wrapped by the combined realm + */ + public void addRealm(Realm theRealm) { + realms.add(theRealm); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("combinedRealm.addRealm", theRealm.getClass().getName(), Integer.toString(realms.size()))); + } + } + + + /** + * @return the set of Realms that this Realm is wrapping + */ + public ObjectName[] getRealms() { + ObjectName[] result = new ObjectName[realms.size()]; + for (Realm realm : realms) { + if (realm instanceof RealmBase) { + result[realms.indexOf(realm)] = ((RealmBase) realm).getObjectName(); + } + } + return result; + } + + + /** + * @return the list of Realms contained by this Realm. + */ + public Realm[] getNestedRealms() { + return realms.toArray(new Realm[0]); + } + + + @Override + public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, + String qop, String realmName, String digestA2, String algorithm) { + Principal authenticatedUser = null; + + for (Realm realm : realms) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authStart", username, realm.getClass().getName())); + } + + authenticatedUser = + realm.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realmName, digestA2, algorithm); + + if (authenticatedUser == null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authFail", username, realm.getClass().getName())); + } + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authSuccess", username, realm.getClass().getName())); + } + break; + } + } + return authenticatedUser; + } + + + @Override + public Principal authenticate(String username) { + Principal authenticatedUser = null; + + for (Realm realm : realms) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authStart", username, realm.getClass().getName())); + } + + authenticatedUser = realm.authenticate(username); + + if (authenticatedUser == null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authFail", username, realm.getClass().getName())); + } + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authSuccess", username, realm.getClass().getName())); + } + break; + } + } + return authenticatedUser; + } + + + @Override + public Principal authenticate(String username, String credentials) { + Principal authenticatedUser = null; + + for (Realm realm : realms) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authStart", username, realm.getClass().getName())); + } + + authenticatedUser = realm.authenticate(username, credentials); + + if (authenticatedUser == null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authFail", username, realm.getClass().getName())); + } + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authSuccess", username, realm.getClass().getName())); + } + break; + } + } + return authenticatedUser; + } + + + @Override + public void setContainer(Container container) { + for (Realm realm : realms) { + // Set the realmPath for JMX naming + if (realm instanceof RealmBase) { + ((RealmBase) realm).setRealmPath(getRealmPath() + "/realm" + realms.indexOf(realm)); + } + + // Set the container for sub-realms. Mainly so logging works. + realm.setContainer(container); + } + super.setContainer(container); + } + + + @Override + protected void startInternal() throws LifecycleException { + // Start 'sub-realms' then this one + Iterator iter = realms.iterator(); + + while (iter.hasNext()) { + Realm realm = iter.next(); + if (realm instanceof Lifecycle) { + try { + ((Lifecycle) realm).start(); + } catch (LifecycleException e) { + // If realm doesn't start can't authenticate against it + iter.remove(); + log.error(sm.getString("combinedRealm.realmStartFail", realm.getClass().getName()), e); + } + } + } + + if (getCredentialHandler() == null) { + // Set a credential handler that will ask the nested realms so that it can + // be set by the context in the attributes, it won't be used directly + super.setCredentialHandler(new CombinedRealmCredentialHandler()); + } + super.startInternal(); + } + + + @Override + protected void stopInternal() throws LifecycleException { + // Stop this realm, then the sub-realms (reverse order to start) + super.stopInternal(); + for (Realm realm : realms) { + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).stop(); + } + } + } + + + /** + * Ensure child Realms are destroyed when this Realm is destroyed. + */ + @Override + protected void destroyInternal() throws LifecycleException { + for (Realm realm : realms) { + if (realm instanceof Lifecycle) { + ((Lifecycle) realm).destroy(); + } + } + super.destroyInternal(); + } + + + /** + * Delegate the backgroundProcess call to all sub-realms. + */ + @Override + public void backgroundProcess() { + super.backgroundProcess(); + + for (Realm r : realms) { + r.backgroundProcess(); + } + } + + + @Override + public Principal authenticate(X509Certificate[] certs) { + Principal authenticatedUser = null; + String username = null; + if (certs != null && certs.length > 0) { + username = certs[0].getSubjectX500Principal().toString(); + } + + for (Realm realm : realms) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authStart", username, realm.getClass().getName())); + } + + authenticatedUser = realm.authenticate(certs); + + if (authenticatedUser == null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authFail", username, realm.getClass().getName())); + } + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authSuccess", username, realm.getClass().getName())); + } + break; + } + } + return authenticatedUser; + } + + + @Override + public Principal authenticate(GSSContext gssContext, boolean storeCred) { + if (gssContext.isEstablished()) { + Principal authenticatedUser = null; + GSSName gssName = null; + try { + gssName = gssContext.getSrcName(); + } catch (GSSException e) { + log.warn(sm.getString("realmBase.gssNameFail"), e); + return null; + } + + for (Realm realm : realms) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authStart", gssName, realm.getClass().getName())); + } + + authenticatedUser = realm.authenticate(gssContext, storeCred); + + if (authenticatedUser == null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authFail", gssName, realm.getClass().getName())); + } + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authSuccess", gssName, realm.getClass().getName())); + } + break; + } + } + return authenticatedUser; + } + + // Fail in all other cases + return null; + } + + + @Override + public Principal authenticate(GSSName gssName, GSSCredential gssCredential) { + Principal authenticatedUser = null; + + for (Realm realm : realms) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authStart", gssName, realm.getClass().getName())); + } + + authenticatedUser = realm.authenticate(gssName, gssCredential); + + if (authenticatedUser == null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authFail", gssName, realm.getClass().getName())); + } + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("combinedRealm.authSuccess", gssName, realm.getClass().getName())); + } + break; + } + } + return authenticatedUser; + } + + + @Override + public boolean hasRole(Wrapper wrapper, Principal principal, String role) { + for (Realm realm : realms) { + if (realm.hasRole(wrapper, principal, role)) { + return true; + } + } + return false; + } + + @Override + protected String getPassword(String username) { + // This method should never be called + // Stack trace will show where this was called from + UnsupportedOperationException uoe = + new UnsupportedOperationException(sm.getString("combinedRealm.getPassword")); + log.error(sm.getString("combinedRealm.unexpectedMethod"), uoe); + throw uoe; + } + + @Override + protected Principal getPrincipal(String username) { + // This method should never be called + // Stack trace will show where this was called from + UnsupportedOperationException uoe = + new UnsupportedOperationException(sm.getString("combinedRealm.getPrincipal")); + log.error(sm.getString("combinedRealm.unexpectedMethod"), uoe); + throw uoe; + } + + + @Override + public boolean isAvailable() { + for (Realm realm : realms) { + if (!realm.isAvailable()) { + return false; + } + } + return true; + } + + + @Override + public void setCredentialHandler(CredentialHandler credentialHandler) { + // This is unusual for a CombinedRealm as it does not use + // CredentialHandlers. It might be a mis-configuration so warn the user. + log.warn(sm.getString("combinedRealm.setCredentialHandler")); + super.setCredentialHandler(credentialHandler); + } + + private class CombinedRealmCredentialHandler implements CredentialHandler { + + @Override + public boolean matches(String inputCredentials, String storedCredentials) { + for (Realm realm : realms) { + if (realm.getCredentialHandler().matches(inputCredentials, storedCredentials)) { + return true; + } + } + return false; + } + + @Override + public String mutate(String inputCredentials) { + if (realms.isEmpty()) { + return null; + } + for (Realm realm : realms) { + String mutatedCredentials = realm.getCredentialHandler().mutate(inputCredentials); + if (mutatedCredentials != null) { + return mutatedCredentials; + } + } + return null; + } + + } +} diff --git a/java/org/apache/catalina/realm/DataSourceRealm.java b/java/org/apache/catalina/realm/DataSourceRealm.java new file mode 100644 index 0000000..ef98280 --- /dev/null +++ b/java/org/apache/catalina/realm/DataSourceRealm.java @@ -0,0 +1,560 @@ +/* + * 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.catalina.realm; + + +import java.security.Principal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; + +import javax.naming.Context; +import javax.sql.DataSource; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Server; +import org.apache.naming.ContextBindings; + +/** + * Implementation of Realm that works with any JDBC JNDI DataSource. See the Realm How-To for more details on how + * to set up the database and for configuration options. + * + * @author Glenn L. Nielsen + * @author Craig R. McClanahan + * @author Carson McDonald + * @author Ignacio Ortega + */ +public class DataSourceRealm extends RealmBase { + + + // ----------------------------------------------------- Instance Variables + + + /** + * The generated string for the roles PreparedStatement + */ + private String preparedRoles = null; + + + /** + * The generated string for the credentials PreparedStatement + */ + private String preparedCredentials = null; + + + /** + * The name of the JNDI JDBC DataSource + */ + protected String dataSourceName = null; + + + /** + * Context local datasource. + */ + protected boolean localDataSource = false; + + + /** + * The column in the user role table that names a role + */ + protected String roleNameCol = null; + + + /** + * The column in the user table that holds the user's credentials + */ + protected String userCredCol = null; + + + /** + * The column in the user table that holds the user's name + */ + protected String userNameCol = null; + + + /** + * The table that holds the relation between user's and roles + */ + protected String userRoleTable = null; + + + /** + * The table that holds user data. + */ + protected String userTable = null; + + + /** + * Last connection attempt. + */ + private volatile boolean connectionSuccess = true; + + + // ------------------------------------------------------------- Properties + + + /** + * @return the name of the JNDI JDBC DataSource. + */ + public String getDataSourceName() { + return dataSourceName; + } + + /** + * Set the name of the JNDI JDBC DataSource. + * + * @param dataSourceName the name of the JNDI JDBC DataSource + */ + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + /** + * @return if the datasource will be looked up in the webapp JNDI Context. + */ + public boolean getLocalDataSource() { + return localDataSource; + } + + /** + * Set to true to cause the datasource to be looked up in the webapp JNDI Context. + * + * @param localDataSource the new flag value + */ + public void setLocalDataSource(boolean localDataSource) { + this.localDataSource = localDataSource; + } + + /** + * @return the column in the user role table that names a role. + */ + public String getRoleNameCol() { + return roleNameCol; + } + + /** + * Set the column in the user role table that names a role. + * + * @param roleNameCol The column name + */ + public void setRoleNameCol(String roleNameCol) { + this.roleNameCol = roleNameCol; + } + + /** + * @return the column in the user table that holds the user's credentials. + */ + public String getUserCredCol() { + return userCredCol; + } + + /** + * Set the column in the user table that holds the user's credentials. + * + * @param userCredCol The column name + */ + public void setUserCredCol(String userCredCol) { + this.userCredCol = userCredCol; + } + + /** + * @return the column in the user table that holds the user's name. + */ + public String getUserNameCol() { + return userNameCol; + } + + /** + * Set the column in the user table that holds the user's name. + * + * @param userNameCol The column name + */ + public void setUserNameCol(String userNameCol) { + this.userNameCol = userNameCol; + } + + /** + * @return the table that holds the relation between user's and roles. + */ + public String getUserRoleTable() { + return userRoleTable; + } + + /** + * Set the table that holds the relation between user's and roles. + * + * @param userRoleTable The table name + */ + public void setUserRoleTable(String userRoleTable) { + this.userRoleTable = userRoleTable; + } + + /** + * @return the table that holds user data.. + */ + public String getUserTable() { + return userTable; + } + + /** + * Set the table that holds user data. + * + * @param userTable The table name + */ + public void setUserTable(String userTable) { + this.userTable = userTable; + } + + + // --------------------------------------------------------- Public Methods + + /** + * {@inheritDoc} + *

    + * If there are any errors with the JDBC connection, executing the query or anything this method returns null + * (doesn't authenticate). This event is also logged, and the connection will be closed so that a subsequent request + * will automatically re-open it. + */ + @Override + public Principal authenticate(String username, String credentials) { + + // No user or no credentials + // Can't possibly authenticate, don't bother the database then + if (username == null || credentials == null) { + return null; + } + + Connection dbConnection = null; + + // Ensure that we have an open database connection + dbConnection = open(); + if (dbConnection == null) { + // If the db connection open fails, return "not authenticated" + return null; + } + + try { + // Acquire a Principal object for this user + return authenticate(dbConnection, username, credentials); + } finally { + close(dbConnection); + } + } + + + @Override + public boolean isAvailable() { + return connectionSuccess; + } + + // -------------------------------------------------------- Package Methods + + + // ------------------------------------------------------ Protected Methods + + + /** + * Return the Principal associated with the specified username and credentials, if there is one; otherwise return + * null. + * + * @param dbConnection The database connection to be used + * @param username Username of the Principal to look up + * @param credentials Password or other credentials to use in authenticating this username + * + * @return the associated principal, or null if there is none. + */ + protected Principal authenticate(Connection dbConnection, String username, String credentials) { + // No user or no credentials + // Can't possibly authenticate, don't bother the database then + if (username == null || credentials == null) { + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure", username)); + } + return null; + } + + // Look up the user's credentials + String dbCredentials = getPassword(dbConnection, username); + + if (dbCredentials == null) { + // User was not found in the database. + // Waste a bit of time as not to reveal that the user does not exist. + getCredentialHandler().mutate(credentials); + + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure", username)); + } + return null; + } + + // Validate the user's credentials + boolean validated = getCredentialHandler().matches(credentials, dbCredentials); + + if (validated) { + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("dataSourceRealm.authenticateSuccess", username)); + } + } else { + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure", username)); + } + return null; + } + + ArrayList list = getRoles(dbConnection, username); + + // Create and return a suitable Principal for this user + return new GenericPrincipal(username, list); + } + + + /** + * Close the specified database connection. + * + * @param dbConnection The connection to be closed + */ + protected void close(Connection dbConnection) { + + // Do nothing if the database connection is already closed + if (dbConnection == null) { + return; + } + + // Commit if not auto committed + try { + if (!dbConnection.getAutoCommit()) { + dbConnection.commit(); + } + } catch (SQLException e) { + containerLog.error(sm.getString("dataSourceRealm.commit"), e); + } + + // Close this database connection, and log any errors + try { + dbConnection.close(); + } catch (SQLException e) { + containerLog.error(sm.getString("dataSourceRealm.close"), e); // Just log it here + } + + } + + /** + * Open the specified database connection. + * + * @return Connection to the database + */ + protected Connection open() { + + try { + Context context = null; + if (localDataSource) { + context = ContextBindings.getClassLoader(); + context = (Context) context.lookup("comp/env"); + } else { + Server server = getServer(); + if (server == null) { + connectionSuccess = false; + containerLog.error(sm.getString("dataSourceRealm.noNamingContext")); + return null; + } + context = server.getGlobalNamingContext(); + } + DataSource dataSource = (DataSource) context.lookup(dataSourceName); + Connection connection = dataSource.getConnection(); + connectionSuccess = true; + return connection; + } catch (Exception e) { + connectionSuccess = false; + // Log the problem for posterity + containerLog.error(sm.getString("dataSourceRealm.exception"), e); + } + return null; + } + + /** + * @return the password associated with the given principal's user name. + */ + @Override + protected String getPassword(String username) { + + Connection dbConnection = null; + + // Ensure that we have an open database connection + dbConnection = open(); + if (dbConnection == null) { + return null; + } + + try { + return getPassword(dbConnection, username); + } finally { + close(dbConnection); + } + } + + + /** + * Return the password associated with the given principal's user name. + * + * @param dbConnection The database connection to be used + * @param username Username for which password should be retrieved + * + * @return the password for the specified user + */ + protected String getPassword(Connection dbConnection, String username) { + + String dbCredentials = null; + + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedCredentials)) { + stmt.setString(1, username); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + dbCredentials = rs.getString(1); + } + + return (dbCredentials != null) ? dbCredentials.trim() : null; + } + } catch (SQLException e) { + containerLog.error(sm.getString("dataSourceRealm.getPassword.exception", username), e); + } + + return null; + } + + + /** + * Return the Principal associated with the given user name. + * + * @param username the user name + * + * @return the principal object + */ + @Override + protected Principal getPrincipal(String username) { + Connection dbConnection = open(); + if (dbConnection == null) { + return new GenericPrincipal(username, null); + } + try { + return new GenericPrincipal(username, getRoles(dbConnection, username)); + } finally { + close(dbConnection); + } + + } + + /** + * Return the roles associated with the given user name. + * + * @param username User name for which roles should be retrieved + * + * @return an array list of the role names + */ + protected ArrayList getRoles(String username) { + + Connection dbConnection = null; + + // Ensure that we have an open database connection + dbConnection = open(); + if (dbConnection == null) { + return null; + } + + try { + return getRoles(dbConnection, username); + } finally { + close(dbConnection); + } + } + + + /** + * Return the roles associated with the given user name. + * + * @param dbConnection The database connection to be used + * @param username User name for which roles should be retrieved + * + * @return an array list of the role names + */ + protected ArrayList getRoles(Connection dbConnection, String username) { + + if (allRolesMode != AllRolesMode.STRICT_MODE && !isRoleStoreDefined()) { + // Using an authentication only configuration and no role store has + // been defined so don't spend cycles looking + return null; + } + + ArrayList list = null; + + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedRoles)) { + stmt.setString(1, username); + + try (ResultSet rs = stmt.executeQuery()) { + list = new ArrayList<>(); + + while (rs.next()) { + String role = rs.getString(1); + if (role != null) { + list.add(role.trim()); + } + } + return list; + } + } catch (SQLException e) { + containerLog.error(sm.getString("dataSourceRealm.getRoles.exception", username), e); + } + + return null; + } + + + private boolean isRoleStoreDefined() { + return userRoleTable != null || roleNameCol != null; + } + + + // ------------------------------------------------------ Lifecycle Methods + + @Override + protected void startInternal() throws LifecycleException { + + // Create the roles PreparedStatement string + StringBuilder temp = new StringBuilder("SELECT "); + temp.append(roleNameCol); + temp.append(" FROM "); + temp.append(userRoleTable); + temp.append(" WHERE "); + temp.append(userNameCol); + temp.append(" = ?"); + preparedRoles = temp.toString(); + + // Create the credentials PreparedStatement string + temp = new StringBuilder("SELECT "); + temp.append(userCredCol); + temp.append(" FROM "); + temp.append(userTable); + temp.append(" WHERE "); + temp.append(userNameCol); + temp.append(" = ?"); + preparedCredentials = temp.toString(); + + super.startInternal(); + } +} diff --git a/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java b/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java new file mode 100644 index 0000000..03db3b5 --- /dev/null +++ b/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java @@ -0,0 +1,347 @@ +/* + * 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.catalina.realm; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +import org.apache.catalina.CredentialHandler; +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Base implementation for the Tomcat provided {@link CredentialHandler}s. + */ +public abstract class DigestCredentialHandlerBase implements CredentialHandler { + + protected static final StringManager sm = StringManager.getManager(DigestCredentialHandlerBase.class); + + public static final int DEFAULT_SALT_LENGTH = 32; + + private int iterations = getDefaultIterations(); + private int saltLength = getDefaultSaltLength(); + private final Object randomLock = new Object(); + private volatile Random random = null; + private boolean logInvalidStoredCredentials = false; + + + /** + * @return the number of iterations of the associated algorithm that will be used when creating a new stored + * credential for a given input credential. + */ + public int getIterations() { + return iterations; + } + + + /** + * Set the number of iterations of the associated algorithm that will be used when creating a new stored credential + * for a given input credential. + * + * @param iterations the iterations count + */ + public void setIterations(int iterations) { + this.iterations = iterations; + } + + + /** + * @return the salt length that will be used when creating a new stored credential for a given input credential. + */ + public int getSaltLength() { + return saltLength; + } + + + /** + * Set the salt length that will be used when creating a new stored credential for a given input credential. + * + * @param saltLength the salt length + */ + public void setSaltLength(int saltLength) { + this.saltLength = saltLength; + } + + + /** + * When checking input credentials against stored credentials will a warning message be logged if invalid stored + * credentials are discovered? + * + * @return true if logging will occur + */ + public boolean getLogInvalidStoredCredentials() { + return logInvalidStoredCredentials; + } + + + /** + * Set whether a warning message will be logged if invalid stored credentials are discovered while checking input + * credentials against stored credentials? + * + * @param logInvalidStoredCredentials true to log, the default value is false + */ + public void setLogInvalidStoredCredentials(boolean logInvalidStoredCredentials) { + this.logInvalidStoredCredentials = logInvalidStoredCredentials; + } + + + @Override + public String mutate(String userCredential) { + byte[] salt = null; + int iterations = getIterations(); + int saltLength = getSaltLength(); + if (saltLength == 0) { + salt = new byte[0]; + } else if (saltLength > 0) { + // Double checked locking. OK since random is volatile. + if (random == null) { + synchronized (randomLock) { + if (random == null) { + random = new SecureRandom(); + } + } + } + salt = new byte[saltLength]; + // Concurrent use of this random is unlikely to be a performance + // issue as it is only used during stored password generation. + random.nextBytes(salt); + } + + String serverCredential = mutate(userCredential, salt, iterations); + + // Failed to generate server credential from user credential. Points to + // a configuration issue. The root cause should have been logged in the + // mutate() method. + if (serverCredential == null) { + return null; + } + + if (saltLength == 0 && iterations == 1) { + // Output the simple/old format for backwards compatibility + return serverCredential; + } else { + StringBuilder result = new StringBuilder((saltLength << 1) + 10 + serverCredential.length() + 2); + result.append(HexUtils.toHexString(salt)); + result.append('$'); + result.append(iterations); + result.append('$'); + result.append(serverCredential); + + return result.toString(); + } + } + + + /** + * Checks whether the provided credential matches the stored credential when the stored credential is in the form + * salt$iteration-count$credential + * + * @param inputCredentials The input credential + * @param storedCredentials The stored credential + * + * @return true if they match, otherwise false + */ + protected boolean matchesSaltIterationsEncoded(String inputCredentials, String storedCredentials) { + + if (storedCredentials == null) { + // Stored credentials are invalid + // This may be expected if nested credential handlers are being used + logInvalidStoredCredentials(null); + return false; + } + + int sep1 = storedCredentials.indexOf('$'); + int sep2 = storedCredentials.indexOf('$', sep1 + 1); + + if (sep1 < 0 || sep2 < 0) { + // Stored credentials are invalid + // This may be expected if nested credential handlers are being used + logInvalidStoredCredentials(storedCredentials); + return false; + } + + String hexSalt = storedCredentials.substring(0, sep1); + + int iterations = Integer.parseInt(storedCredentials.substring(sep1 + 1, sep2)); + + String storedHexEncoded = storedCredentials.substring(sep2 + 1); + byte[] salt; + try { + salt = HexUtils.fromHexString(hexSalt); + } catch (IllegalArgumentException iae) { + logInvalidStoredCredentials(storedCredentials); + return false; + } + + String inputHexEncoded = + mutate(inputCredentials, salt, iterations, HexUtils.fromHexString(storedHexEncoded).length * Byte.SIZE); + if (inputHexEncoded == null) { + // Failed to mutate user credentials. Automatic fail. + // Root cause should be logged by mutate() + return false; + } + + return DigestCredentialHandlerBase.equals(storedHexEncoded, inputHexEncoded, true); + } + + + private void logInvalidStoredCredentials(String storedCredentials) { + if (logInvalidStoredCredentials) { + // Logging credentials could be a security concern but they are + // invalid and that is probably a bigger problem + getLog().warn(sm.getString("credentialHandler.invalidStoredCredential", storedCredentials)); + } + } + + + /** + * @return the default salt length used by the {@link CredentialHandler}. + */ + protected int getDefaultSaltLength() { + return DEFAULT_SALT_LENGTH; + } + + + /** + * Generates the equivalent stored credentials for the given input credentials, salt and iterations. If the + * algorithm requires a key length, the default will be used. + * + * @param inputCredentials User provided credentials + * @param salt Salt, if any + * @param iterations Number of iterations of the algorithm associated with this CredentialHandler applied to + * the inputCredentials to generate the equivalent stored credentials + * + * @return The equivalent stored credentials for the given input credentials or null if the generation + * fails + */ + protected abstract String mutate(String inputCredentials, byte[] salt, int iterations); + + + /** + * Generates the equivalent stored credentials for the given input credentials, salt, iterations and key length. The + * default implementation calls ignores the key length and calls {@link #mutate(String, byte[], int)}. Sub-classes + * that use the key length should override this method. + * + * @param inputCredentials User provided credentials + * @param salt Salt, if any + * @param iterations Number of iterations of the algorithm associated with this CredentialHandler applied to + * the inputCredentials to generate the equivalent stored credentials + * @param keyLength Length of the produced digest in bits for implementations where it's applicable + * + * @return The equivalent stored credentials for the given input credentials or null if the generation + * fails + */ + protected String mutate(String inputCredentials, byte[] salt, int iterations, int keyLength) { + return mutate(inputCredentials, salt, iterations); + } + + + /** + * Set the algorithm used to convert input credentials to stored credentials. + * + * @param algorithm the algorithm + * + * @throws NoSuchAlgorithmException if the specified algorithm is not supported + */ + public abstract void setAlgorithm(String algorithm) throws NoSuchAlgorithmException; + + + /** + * @return the algorithm used to convert input credentials to stored credentials. + */ + public abstract String getAlgorithm(); + + + /** + * @return the default number of iterations used by the {@link CredentialHandler}. + */ + protected abstract int getDefaultIterations(); + + + /** + * @return the logger for the CredentialHandler instance. + */ + protected abstract Log getLog(); + + /** + * Implements String equality which always compares all characters in the string, without stopping early if any + * characters do not match. + *

    + * Note: This implementation was adapted from {@link MessageDigest#isEqual} which we assume is as + * optimizer-defeating as possible. + * + * @param s1 The first string to compare. + * @param s2 The second string to compare. + * @param ignoreCase true if the strings should be compared without regard to case. Note that "true" + * here is only guaranteed to work with plain ASCII characters. + * + * @return true if the strings are equal to each other, false otherwise. + */ + public static boolean equals(final String s1, final String s2, final boolean ignoreCase) { + if (s1 == s2) { + return true; + } + if (s1 == null || s2 == null) { + return false; + } + + final int len1 = s1.length(); + final int len2 = s2.length(); + + if (len2 == 0) { + return len1 == 0; + } + + int result = 0; + result |= len1 - len2; + + // time-constant comparison + for (int i = 0; i < len1; i++) { + // If i >= len2, index2 is 0; otherwise, i. + final int index2 = ((i - len2) >>> 31) * i; + char c1 = s1.charAt(i); + char c2 = s2.charAt(index2); + if (ignoreCase) { + c1 = Character.toLowerCase(c1); + c2 = Character.toLowerCase(c2); + } + result |= c1 ^ c2; + } + return result == 0; + } + + /** + * Implements byte-array equality which always compares all bytes in the array, without stopping early if any bytes + * do not match. + *

    + * Note: Implementation note: this method delegates to {@link MessageDigest#isEqual} under the assumption + * that it provides a constant-time comparison of the bytes in the arrays. Java 7+ has such an implementation, but + * neither the Javadoc nor any specification requires it. Therefore, Tomcat should continue to use this + * method internally in case the JDK implementation changes so this method can be re-implemented properly. + * + * @param b1 The first array to compare. + * @param b2 The second array to compare. + * + * @return true if the arrays are equal to each other, false otherwise. + */ + public static boolean equals(final byte[] b1, final byte[] b2) { + return MessageDigest.isEqual(b1, b2); + } +} diff --git a/java/org/apache/catalina/realm/GenericPrincipal.java b/java/org/apache/catalina/realm/GenericPrincipal.java new file mode 100644 index 0000000..5d2ca67 --- /dev/null +++ b/java/org/apache/catalina/realm/GenericPrincipal.java @@ -0,0 +1,352 @@ +/* + * 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.catalina.realm; + +import java.io.Serializable; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import javax.security.auth.login.LoginContext; + +import org.apache.catalina.TomcatPrincipal; +import org.ietf.jgss.GSSCredential; + +/** + * Generic implementation of java.security.Principal that is available for use by Realm + * implementations. + * + * @author Craig R. McClanahan + */ +public class GenericPrincipal implements TomcatPrincipal, Serializable { + + private static final long serialVersionUID = 1L; + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with no roles. + * + * @param name The username of the user represented by this Principal + */ + public GenericPrincipal(String name) { + this(name, null); + } + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with the specified + * role names (as Strings). + * + * @param name The username of the user represented by this Principal + * @param roles List of roles (must be Strings) possessed by this user + */ + public GenericPrincipal(String name, List roles) { + this(name, roles, null); + } + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with the specified + * role names (as Strings). + * + * @param name The username of the user represented by this Principal + * @param password Unused + * @param roles List of roles (must be Strings) possessed by this user + * + * @deprecated This method will be removed in Tomcat 11 onwards + */ + @Deprecated + public GenericPrincipal(String name, String password, List roles) { + this(name, roles, null); + } + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with the specified + * role names (as Strings). + * + * @param name The username of the user represented by this Principal + * @param roles List of roles (must be Strings) possessed by this user + * @param userPrincipal - the principal to be returned from the request getUserPrincipal call if not null; if null, + * this will be returned + */ + public GenericPrincipal(String name, List roles, Principal userPrincipal) { + this(name, roles, userPrincipal, null); + } + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with the specified + * role names (as Strings). + * + * @param name The username of the user represented by this Principal + * @param password Unused + * @param roles List of roles (must be Strings) possessed by this user + * @param userPrincipal - the principal to be returned from the request getUserPrincipal call if not null; if null, + * this will be returned + * + * @deprecated This method will be removed in Tomcat 11 onwards + */ + @Deprecated + public GenericPrincipal(String name, String password, List roles, Principal userPrincipal) { + this(name, roles, userPrincipal, null); + } + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with the specified + * role names (as Strings). + * + * @param name The username of the user represented by this Principal + * @param roles List of roles (must be Strings) possessed by this user + * @param userPrincipal - the principal to be returned from the request getUserPrincipal call if not null; if null, + * this will be returned + * @param loginContext - If provided, this will be used to log out the user at the appropriate time + */ + public GenericPrincipal(String name, List roles, Principal userPrincipal, LoginContext loginContext) { + this(name, roles, userPrincipal, loginContext, null, null); + } + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with the specified + * role names (as Strings). + * + * @param name The username of the user represented by this Principal + * @param password Unused + * @param roles List of roles (must be Strings) possessed by this user + * @param userPrincipal - the principal to be returned from the request getUserPrincipal call if not null; if null, + * this will be returned + * @param loginContext - If provided, this will be used to log out the user at the appropriate time + * + * @deprecated This method will be removed in Tomcat 11 onwards + */ + @Deprecated + public GenericPrincipal(String name, String password, List roles, Principal userPrincipal, + LoginContext loginContext) { + this(name, roles, userPrincipal, loginContext, null, null); + } + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with the specified + * role names (as Strings). + * + * @param name The username of the user represented by this Principal + * @param roles List of roles (must be Strings) possessed by this user + * @param userPrincipal - the principal to be returned from the request getUserPrincipal call if not null; if null, + * this will be returned + * @param loginContext - If provided, this will be used to log out the user at the appropriate time + * @param gssCredential - If provided, the user's delegated credentials + * @param attributes - If provided, additional attributes associated with this Principal + */ + public GenericPrincipal(String name, List roles, Principal userPrincipal, LoginContext loginContext, + GSSCredential gssCredential, Map attributes) { + super(); + this.name = name; + this.userPrincipal = userPrincipal; + if (roles == null) { + this.roles = new String[0]; + } else { + this.roles = roles.toArray(new String[0]); + if (this.roles.length > 1) { + Arrays.sort(this.roles); + } + } + this.loginContext = loginContext; + this.gssCredential = gssCredential; + this.attributes = attributes != null ? Collections.unmodifiableMap(attributes) : null; + } + + + /** + * Construct a new Principal, associated with the specified Realm, for the specified username, with the specified + * role names (as Strings). + * + * @param name The username of the user represented by this Principal + * @param password Unused + * @param roles List of roles (must be Strings) possessed by this user + * @param userPrincipal - the principal to be returned from the request getUserPrincipal call if not null; if null, + * this will be returned + * @param loginContext - If provided, this will be used to log out the user at the appropriate time + * @param gssCredential - If provided, the user's delegated credentials + * + * @deprecated This method will be removed in Tomcat 11 onwards + */ + @Deprecated + public GenericPrincipal(String name, String password, List roles, Principal userPrincipal, + LoginContext loginContext, GSSCredential gssCredential) { + this(name, roles, userPrincipal, loginContext, gssCredential, null); + } + + + // -------------------------------------------------------------- Properties + + /** + * The username of the user represented by this Principal. + */ + protected final String name; + + @Override + public String getName() { + return this.name; + } + + /** + * The set of roles associated with this user. + */ + protected final String[] roles; + + public String[] getRoles() { + return this.roles.clone(); + } + + + /** + * The authenticated Principal to be exposed to applications. + */ + protected final Principal userPrincipal; + + @Override + public Principal getUserPrincipal() { + if (userPrincipal != null) { + return userPrincipal; + } else { + return this; + } + } + + + /** + * The JAAS LoginContext, if any, used to authenticate this Principal. Kept so we can call logout(). + */ + protected final transient LoginContext loginContext; + + + /** + * The user's delegated credentials. + */ + protected transient GSSCredential gssCredential = null; + + @Override + public GSSCredential getGssCredential() { + return this.gssCredential; + } + + protected void setGssCredential(GSSCredential gssCredential) { + this.gssCredential = gssCredential; + } + + /** + * The additional attributes associated with this Principal. + */ + protected final Map attributes; + + + // ---------------------------------------------------------- Public Methods + + /** + * Does the user represented by this Principal possess the specified role? + * + * @param role Role to be tested + * + * @return true if this Principal has been assigned the given role, otherwise false + */ + public boolean hasRole(String role) { + if ("*".equals(role)) { // Special 2.4 role meaning everyone + return true; + } + if (role == null) { + return false; + } + return Arrays.binarySearch(roles, role) >= 0; + } + + + /** + * Return a String representation of this object, which exposes only information that should be public. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("GenericPrincipal["); + sb.append(this.name); + sb.append('('); + for (String role : roles) { + sb.append(role).append(','); + } + sb.append(")]"); + return sb.toString(); + } + + + @Override + public void logout() throws Exception { + if (loginContext != null) { + loginContext.logout(); + } + if (gssCredential != null) { + gssCredential.dispose(); + } + } + + + @Override + public Object getAttribute(String name) { + if (attributes == null || name == null) { + return null; + } + return attributes.get(name); + } + + + @Override + public Enumeration getAttributeNames() { + if (attributes == null) { + return Collections.emptyEnumeration(); + } + return Collections.enumeration(attributes.keySet()); + } + + + // ----------------------------------------------------------- Serialization + + private Object writeReplace() { + return new SerializablePrincipal(name, roles, userPrincipal, attributes); + } + + private static class SerializablePrincipal implements Serializable { + private static final long serialVersionUID = 1L; + + private final String name; + private final String[] roles; + private final Principal principal; + private final Map attributes; + + SerializablePrincipal(String name, String[] roles, Principal principal, Map attributes) { + this.name = name; + this.roles = roles; + if (principal instanceof Serializable) { + this.principal = principal; + } else { + this.principal = null; + } + this.attributes = attributes; + } + + private Object readResolve() { + return new GenericPrincipal(name, Arrays.asList(roles), principal, null, null, attributes); + } + } +} diff --git a/java/org/apache/catalina/realm/JAASCallbackHandler.java b/java/org/apache/catalina/realm/JAASCallbackHandler.java new file mode 100644 index 0000000..5d540b0 --- /dev/null +++ b/java/org/apache/catalina/realm/JAASCallbackHandler.java @@ -0,0 +1,217 @@ +/* + * 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.catalina.realm; + + +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.TextInputCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * Implementation of the JAAS CallbackHandler interface, used to negotiate delivery of the username and + * credentials that were specified to our constructor. No interaction with the user is required (or possible). + *

    + *

    + * This CallbackHandler will pre-digest the supplied password, if required by the + * <Realm> element in server.xml. + *

    + *

    + * At present, JAASCallbackHandler knows how to handle callbacks of type + * javax.security.auth.callback.NameCallback and + * javax.security.auth.callback.PasswordCallback. + *

    + * + * @author Craig R. McClanahan + * @author Andrew R. Jaquith + */ +public class JAASCallbackHandler implements CallbackHandler { + + // ------------------------------------------------------------ Constructor + + + /** + * Construct a callback handler configured with the specified values. Note that if the JAASRealm + * instance specifies digested passwords, the password parameter will be pre-digested here. + * + * @param realm Our associated JAASRealm instance + * @param username Username to be authenticated with + * @param password Password to be authenticated with + */ + public JAASCallbackHandler(JAASRealm realm, String username, String password) { + + this(realm, username, password, null, null, null, null, null, null, null, null); + } + + + /** + * Construct a callback handler for DIGEST authentication. + * + * @param realm Our associated JAASRealm instance + * @param username Username to be authenticated with + * @param password Password to be authenticated with + * @param nonce Server generated nonce + * @param nc Nonce count + * @param cnonce Client generated nonce + * @param qop Quality of protection applied to the message + * @param realmName Realm name + * @param digestA2 Second digest calculated as digest(Method + ":" + uri) + * @param algorithm The digest algorithm to use + * @param authMethod The authentication method in use + */ + public JAASCallbackHandler(JAASRealm realm, String username, String password, String nonce, String nc, + String cnonce, String qop, String realmName, String digestA2, String algorithm, String authMethod) { + this.realm = realm; + this.username = username; + + if (password != null && realm.hasMessageDigest(algorithm)) { + this.password = realm.getCredentialHandler().mutate(password); + } else { + this.password = password; + } + this.nonce = nonce; + this.nc = nc; + this.cnonce = cnonce; + this.qop = qop; + this.realmName = realmName; + this.digestA2 = digestA2; + this.authMethod = authMethod; + } + + // ----------------------------------------------------- Instance Variables + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(JAASCallbackHandler.class); + + /** + * The password to be authenticated with. + */ + protected final String password; + + + /** + * The associated JAASRealm instance. + */ + protected final JAASRealm realm; + + /** + * The username to be authenticated with. + */ + protected final String username; + + /** + * Server generated nonce. + */ + protected final String nonce; + + /** + * Nonce count. + */ + protected final String nc; + + /** + * Client generated nonce. + */ + protected final String cnonce; + + /** + * Quality of protection applied to the message. + */ + protected final String qop; + + /** + * Realm name. + */ + protected final String realmName; + + /** + * Second digest. + */ + protected final String digestA2; + + /** + * The authentication method to be used. If null, assume BASIC/FORM. + */ + protected final String authMethod; + + // --------------------------------------------------------- Public Methods + + + /** + * Retrieve the information requested in the provided Callbacks. This implementation only recognizes + * {@link NameCallback}, {@link PasswordCallback} and {@link TextInputCallback}. {@link TextInputCallback} is used + * to pass the various additional parameters required for DIGEST authentication. + * + * @param callbacks The set of Callbacks to be processed + * + * @exception IOException if an input/output error occurs + * @exception UnsupportedCallbackException if the login method requests an unsupported callback type + */ + @Override + public void handle(Callback callbacks[]) throws IOException, UnsupportedCallbackException { + + for (Callback callback : callbacks) { + + if (callback instanceof NameCallback) { + if (realm.getContainer().getLogger().isTraceEnabled()) { + realm.getContainer().getLogger().trace(sm.getString("jaasCallback.username", username)); + } + ((NameCallback) callback).setName(username); + } else if (callback instanceof PasswordCallback) { + final char[] passwordcontents; + if (password != null) { + passwordcontents = password.toCharArray(); + } else { + passwordcontents = new char[0]; + } + ((PasswordCallback) callback).setPassword(passwordcontents); + } else if (callback instanceof TextInputCallback) { + TextInputCallback cb = ((TextInputCallback) callback); + if (cb.getPrompt().equals("nonce")) { + cb.setText(nonce); + } else if (cb.getPrompt().equals("nc")) { + cb.setText(nc); + } else if (cb.getPrompt().equals("cnonce")) { + cb.setText(cnonce); + } else if (cb.getPrompt().equals("qop")) { + cb.setText(qop); + } else if (cb.getPrompt().equals("realmName")) { + cb.setText(realmName); + } else if (cb.getPrompt().equals("digestA2")) { + cb.setText(digestA2); + } else if (cb.getPrompt().equals("authMethod")) { + cb.setText(authMethod); + } else if (cb.getPrompt().equals("catalinaBase")) { + cb.setText(realm.getContainer().getCatalinaBase().getAbsolutePath()); + } else { + throw new UnsupportedCallbackException(callback); + } + } else { + throw new UnsupportedCallbackException(callback); + } + } + } +} diff --git a/java/org/apache/catalina/realm/JAASMemoryLoginModule.java b/java/org/apache/catalina/realm/JAASMemoryLoginModule.java new file mode 100644 index 0000000..6500f97 --- /dev/null +++ b/java/org/apache/catalina/realm/JAASMemoryLoginModule.java @@ -0,0 +1,377 @@ +/* + * 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.catalina.realm; + +import java.io.File; +import java.io.IOException; +import java.security.Principal; +import java.util.Map; +import java.util.Map.Entry; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.TextInputCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.catalina.CredentialHandler; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.digester.Digester; + +/** + *

    + * Implementation of the JAAS LoginModule interface, primarily for use in testing + * JAASRealm. It utilizes an XML-format data file of username/password/role information identical to that + * supported by org.apache.catalina.realm.MemoryRealm. + *

    + *

    + * This class recognizes the following string-valued options, which are specified in the configuration file and passed + * to {@link #initialize(Subject, CallbackHandler, Map, Map)} in the options argument: + *

    + *
      + *
    • pathname - Relative (to the pathname specified by the "catalina.base" system property) or + * absolute pathname to the XML file containing our user information, in the format supported by {@link MemoryRealm}. + * The default value matches the MemoryRealm default.
    • + *
    • credentialHandlerClassName - The fully qualified class name of the CredentialHandler to use. If + * not specified, {@link MessageDigestCredentialHandler} will be used.
    • + *
    • Any additional options will be used to identify and call setters on the {@link CredentialHandler}. For example, + * algorithm=SHA256 would result in a call to {@link MessageDigestCredentialHandler#setAlgorithm(String)} + * with a parameter of "SHA256"
    • + *
    + *

    + * IMPLEMENTATION NOTE - This class implements Realm only to satisfy the calling + * requirements of the GenericPrincipal constructor. It does not actually perform the functionality + * required of a Realm implementation. + *

    + * + * @author Craig R. McClanahan + */ +public class JAASMemoryLoginModule extends MemoryRealm implements LoginModule { + // We need to extend MemoryRealm to avoid class cast + + private static final Log log = LogFactory.getLog(JAASMemoryLoginModule.class); + + // ----------------------------------------------------- Instance Variables + + + /** + * The callback handler responsible for answering our requests. + */ + protected CallbackHandler callbackHandler = null; + + + /** + * Has our own commit() returned successfully? + */ + protected boolean committed = false; + + + /** + * The configuration information for this LoginModule. + */ + protected Map options = null; + + + /** + * The absolute or relative pathname to the XML configuration file. + */ + protected String pathname = "conf/tomcat-users.xml"; + + + /** + * The Principal identified by our validation, or null if validation failed. + */ + protected Principal principal = null; + + + /** + * The state information that is shared with other configured LoginModule instances. + */ + protected Map sharedState = null; + + + /** + * The subject for which we are performing authentication. + */ + protected Subject subject = null; + + + // --------------------------------------------------------- Public Methods + + public JAASMemoryLoginModule() { + if (log.isTraceEnabled()) { + log.trace("MEMORY LOGIN MODULE"); + } + } + + + @Override + public boolean abort() throws LoginException { + + // If our authentication was not successful, just return false + if (principal == null) { + return false; + } + + // Clean up if overall authentication failed + if (committed) { + logout(); + } else { + committed = false; + principal = null; + } + if (log.isTraceEnabled()) { + log.trace("Abort"); + } + return true; + } + + + @Override + public boolean commit() throws LoginException { + if (log.isTraceEnabled()) { + log.trace("commit " + principal); + } + + // If authentication was not successful, just return false + if (principal == null) { + return false; + } + + // Add our Principal to the Subject if needed + if (!subject.getPrincipals().contains(principal)) { + subject.getPrincipals().add(principal); + // Add the roles as additional subjects as per the contract with the + // JAASRealm + if (principal instanceof GenericPrincipal) { + String roles[] = ((GenericPrincipal) principal).getRoles(); + for (String role : roles) { + subject.getPrincipals().add(new GenericPrincipal(role)); + } + + } + } + + committed = true; + return true; + } + + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map options) { + if (log.isTraceEnabled()) { + log.trace("Init"); + } + + // Save configuration values + this.subject = subject; + this.callbackHandler = callbackHandler; + this.sharedState = sharedState; + this.options = options; + + // Perform instance-specific initialization + Object option = options.get("pathname"); + if (option instanceof String) { + this.pathname = (String) option; + } + + CredentialHandler credentialHandler = null; + option = options.get("credentialHandlerClassName"); + if (option instanceof String) { + try { + Class clazz = Class.forName((String) option); + credentialHandler = (CredentialHandler) clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException(e); + } + } + if (credentialHandler == null) { + credentialHandler = new MessageDigestCredentialHandler(); + } + + for (Entry entry : options.entrySet()) { + if ("pathname".equals(entry.getKey())) { + continue; + } + if ("credentialHandlerClassName".equals(entry.getKey())) { + continue; + } + // Skip any non-String values since any value we are interested in + // will be a String. + if (entry.getValue() instanceof String) { + IntrospectionUtils.setProperty(credentialHandler, entry.getKey(), (String) entry.getValue()); + } + } + setCredentialHandler(credentialHandler); + + // Load our defined Principals + load(); + } + + + @Override + public boolean login() throws LoginException { + // Set up our CallbackHandler requests + if (callbackHandler == null) { + throw new LoginException(sm.getString("jaasMemoryLoginModule.noCallbackHandler")); + } + Callback callbacks[] = new Callback[10]; + callbacks[0] = new NameCallback("Username: "); + callbacks[1] = new PasswordCallback("Password: ", false); + callbacks[2] = new TextInputCallback("nonce"); + callbacks[3] = new TextInputCallback("nc"); + callbacks[4] = new TextInputCallback("cnonce"); + callbacks[5] = new TextInputCallback("qop"); + callbacks[6] = new TextInputCallback("realmName"); + callbacks[7] = new TextInputCallback("digestA2"); + callbacks[8] = new TextInputCallback("algorithm"); + callbacks[9] = new TextInputCallback("authMethod"); + + // Interact with the user to retrieve the username and password + String username = null; + String password = null; + String nonce = null; + String nc = null; + String cnonce = null; + String qop = null; + String realmName = null; + String digestA2 = null; + String algorithm = null; + String authMethod = null; + + try { + callbackHandler.handle(callbacks); + username = ((NameCallback) callbacks[0]).getName(); + char[] passwordArray = ((PasswordCallback) callbacks[1]).getPassword(); + password = (passwordArray == null) ? null : new String(passwordArray); + nonce = ((TextInputCallback) callbacks[2]).getText(); + nc = ((TextInputCallback) callbacks[3]).getText(); + cnonce = ((TextInputCallback) callbacks[4]).getText(); + qop = ((TextInputCallback) callbacks[5]).getText(); + realmName = ((TextInputCallback) callbacks[6]).getText(); + digestA2 = ((TextInputCallback) callbacks[7]).getText(); + algorithm = ((TextInputCallback) callbacks[8]).getText(); + authMethod = ((TextInputCallback) callbacks[9]).getText(); + } catch (IOException | UnsupportedCallbackException e) { + throw new LoginException(sm.getString("jaasMemoryLoginModule.callbackHandlerError", e.toString())); + } + + // Validate the username and password we have received + if (authMethod == null) { + // BASIC or FORM + principal = super.authenticate(username, password); + } else if (authMethod.equals(HttpServletRequest.DIGEST_AUTH)) { + principal = super.authenticate(username, password, nonce, nc, cnonce, qop, realmName, digestA2, algorithm); + } else if (authMethod.equals(HttpServletRequest.CLIENT_CERT_AUTH)) { + principal = super.getPrincipal(username); + } else { + throw new LoginException(sm.getString("jaasMemoryLoginModule.unknownAuthenticationMethod")); + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasMemoryLoginModule.login", username, principal)); + } + + // Report results based on success or failure + if (principal != null) { + return true; + } else { + throw new FailedLoginException(sm.getString("jaasMemoryLoginModule.invalidCredentials")); + } + } + + + @Override + public boolean logout() throws LoginException { + subject.getPrincipals().remove(principal); + committed = false; + principal = null; + return true; + } + + + // ---------------------------------------------------------- Realm Methods + // ------------------------------------------------------ Protected Methods + + /** + * Load the contents of our configuration file. + */ + protected void load() { + // Validate the existence of our configuration file + File file = new File(pathname); + if (!file.isAbsolute()) { + String catalinaBase = getCatalinaBase(); + if (catalinaBase == null) { + log.error(sm.getString("jaasMemoryLoginModule.noCatalinaBase", pathname)); + return; + } else { + file = new File(catalinaBase, pathname); + } + } + if (!file.canRead()) { + log.error(sm.getString("jaasMemoryLoginModule.noConfig", file.getAbsolutePath())); + return; + } + + // Load the contents of our configuration file + Digester digester = new Digester(); + digester.setValidating(false); + digester.addRuleSet(new MemoryRuleSet()); + try { + digester.push(this); + digester.parse(file); + } catch (Exception e) { + log.error(sm.getString("jaasMemoryLoginModule.parseError", file.getAbsolutePath()), e); + } finally { + digester.reset(); + } + } + + private String getCatalinaBase() { + // Have to get this via a callback as that is the only link we have back + // to the defining Realm. Can't use the system property as that may not + // be set/correct in an embedded scenario + + if (callbackHandler == null) { + return null; + } + + Callback callbacks[] = new Callback[1]; + callbacks[0] = new TextInputCallback("catalinaBase"); + + String result = null; + + try { + callbackHandler.handle(callbacks); + result = ((TextInputCallback) callbacks[0]).getText(); + } catch (IOException | UnsupportedCallbackException e) { + return null; + } + + return result; + } +} diff --git a/java/org/apache/catalina/realm/JAASRealm.java b/java/org/apache/catalina/realm/JAASRealm.java new file mode 100644 index 0000000..cc80eb7 --- /dev/null +++ b/java/org/apache/catalina/realm/JAASRealm.java @@ -0,0 +1,622 @@ +/* + * 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.catalina.realm; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.AccountExpiredException; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.CredentialExpiredException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.catalina.Container; +import org.apache.catalina.LifecycleException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; + +/** + *

    + * Implementation of Realm that authenticates users via the Java Authentication and Authorization + * Service (JAAS). JAAS support requires either JDK 1.4 (which includes it as part of the standard platform) or JDK + * 1.3 (with the plug-in jaas.jar file). + *

    + *

    + * The value configured for the appName property is passed to the + * javax.security.auth.login.LoginContext constructor, to specify the application name used to + * select the set of relevant LoginModules required. + *

    + *

    + * The JAAS Specification describes the result of a successful login as a javax.security.auth.Subject + * instance, which can contain zero or more java.security.Principal objects in the return value of the + * Subject.getPrincipals() method. However, it provides no guidance on how to distinguish Principals that + * describe the individual user (and are thus appropriate to return as the value of request.getUserPrincipal() in a web + * application) from the Principal(s) that describe the authorized roles for this user. To maintain as much independence + * as possible from the underlying LoginMethod implementation executed by JAAS, the following policy is + * implemented by this Realm: + *

    + *
      + *
    • The JAAS LoginModule is assumed to return a Subject with at least one + * Principal instance representing the user himself or herself, and zero or more separate + * Principals representing the security roles authorized for this user.
    • + *
    • On the Principal representing the user, the Principal name is an appropriate value to return via the + * Servlet API method HttpServletRequest.getRemoteUser().
    • + *
    • On the Principals representing the security roles, the name is the name of the authorized security + * role.
    • + *
    • This Realm will be configured with two lists of fully qualified Java class names of classes that implement + * java.security.Principal - one that identifies class(es) representing a user, and one that identifies + * class(es) representing a security role.
    • + *
    • As this Realm iterates over the Principals returned by Subject.getPrincipals(), it will + * identify the first Principal that matches the "user classes" list as the Principal for this + * user.
    • + *
    • As this Realm iterates over the Principals returned by Subject.getPrincipals(), it will + * accumulate the set of all Principals matching the "role classes" list as identifying the security roles + * for this user.
    • + *
    • It is a configuration error for the JAAS login method to return a validated Subject without a + * Principal that matches the "user classes" list.
    • + *
    • By default, the enclosing Container's name serves as the application name used to obtain the JAAS LoginContext + * ("Catalina" in a default installation). Tomcat must be able to find an application with this name in the JAAS + * configuration file. Here is a hypothetical JAAS configuration file entry for a database-oriented login module that + * uses a Tomcat-managed JNDI database resource:
      + * + *
      + * Catalina {
      + * org.foobar.auth.DatabaseLoginModule REQUIRED
      + *   JNDI_RESOURCE=jdbc/AuthDB
      + *   USER_TABLE=users
      + *   USER_ID_COLUMN=id
      + *   USER_NAME_COLUMN=name
      + *   USER_CREDENTIAL_COLUMN=password
      + *   ROLE_TABLE=roles
      + *   ROLE_NAME_COLUMN=name
      + *   PRINCIPAL_FACTORY=org.foobar.auth.impl.SimplePrincipalFactory;
      + * };
      + * 
      + * + *
    • + *
    • To set the JAAS configuration file location, set the CATALINA_OPTS environment variable similar to + * the following: + *
      CATALINA_OPTS="-Djava.security.auth.login.config=$CATALINA_HOME/conf/jaas.config"
      + *
    • + *
    • As part of the login process, JAASRealm registers its own CallbackHandler, called (unsurprisingly) + * JAASCallbackHandler. This handler supplies the HTTP requests's username and credentials to the + * user-supplied LoginModule
    • + *
    • As with other Realm implementations, digested passwords are supported if the + * <Realm> element in server.xml contains a digest attribute; + * JAASCallbackHandler will digest the password prior to passing it back to the + * LoginModule
    • + *
    + * + * @author Craig R. McClanahan + * @author Yoav Shapira + */ +public class JAASRealm extends RealmBase { + + private static final Log log = LogFactory.getLog(JAASRealm.class); + + // ----------------------------------------------------- Instance Variables + + + /** + * The application name passed to the JAAS LoginContext, which uses it to select the set of relevant + * LoginModules. + */ + protected String appName = null; + + + /** + * The list of role class names, split out for easy processing. + */ + protected final List roleClasses = new ArrayList<>(); + + + /** + * The set of user class names, split out for easy processing. + */ + protected final List userClasses = new ArrayList<>(); + + + /** + * Whether to use context ClassLoader or default ClassLoader. True means use context ClassLoader, and True is the + * default value. + */ + protected boolean useContextClassLoader = true; + + + /** + * Path to find a JAAS configuration file, if not set global JVM JAAS configuration will be used. + */ + protected String configFile; + + protected volatile Configuration jaasConfiguration; + protected volatile boolean jaasConfigurationLoaded = false; + + /** + * Keeps track if JAAS invocation of login modules was successful or not. By default it is true unless we detect + * JAAS login module can't perform the login. This will be used for realm's {@link #isAvailable()} status so that + * {@link LockOutRealm} will not lock the user out if JAAS login modules are unavailable to perform the actual + * login. + */ + private volatile boolean invocationSuccess = true; + + // ------------------------------------------------------------- Properties + + /** + * @return the path of the JAAS configuration file. + */ + public String getConfigFile() { + return configFile; + } + + /** + * Set the JAAS configuration file. + * + * @param configFile The JAAS configuration file + */ + public void setConfigFile(String configFile) { + this.configFile = configFile; + } + + /** + * Set the JAAS LoginContext app name. + * + * @param name The application name that will be used to retrieve the set of relevant LoginModules + */ + public void setAppName(String name) { + appName = name; + } + + /** + * @return the application name. + */ + public String getAppName() { + return appName; + } + + /** + * Sets whether to use the context or default ClassLoader. True means use context ClassLoader. + * + * @param useContext True means use context ClassLoader + */ + public void setUseContextClassLoader(boolean useContext) { + useContextClassLoader = useContext; + } + + /** + * Returns whether to use the context or default ClassLoader. True means to use the context ClassLoader. + * + * @return The value of useContextClassLoader + */ + public boolean isUseContextClassLoader() { + return useContextClassLoader; + } + + @Override + public void setContainer(Container container) { + super.setContainer(container); + + if (appName == null) { + appName = makeLegalForJAAS(container.getName()); + log.info(sm.getString("jaasRealm.appName", appName)); + } + } + + /** + * Comma-delimited list of java.security.Principal classes that represent security roles. + */ + protected String roleClassNames = null; + + public String getRoleClassNames() { + return this.roleClassNames; + } + + /** + * Sets the list of comma-delimited classes that represent roles. The classes in the list must implement + * java.security.Principal. The supplied list of classes will be parsed when {@link #start()} is + * called. + * + * @param roleClassNames The class names list + */ + public void setRoleClassNames(String roleClassNames) { + this.roleClassNames = roleClassNames; + } + + /** + * Parses a comma-delimited list of class names, and store the class names in the provided List. Each class must + * implement java.security.Principal. + * + * @param classNamesString a comma-delimited list of fully qualified class names. + * @param classNamesList the list in which the class names will be stored. The list is cleared before being + * populated. + */ + protected void parseClassNames(String classNamesString, List classNamesList) { + classNamesList.clear(); + if (classNamesString == null) { + return; + } + + ClassLoader loader = this.getClass().getClassLoader(); + if (isUseContextClassLoader()) { + loader = Thread.currentThread().getContextClassLoader(); + } + + String[] classNames = classNamesString.split("[ ]*,[ ]*"); + for (String className : classNames) { + if (className.length() == 0) { + continue; + } + try { + Class principalClass = Class.forName(className, false, loader); + if (Principal.class.isAssignableFrom(principalClass)) { + classNamesList.add(className); + } else { + log.error(sm.getString("jaasRealm.notPrincipal", className)); + } + } catch (ClassNotFoundException e) { + log.error(sm.getString("jaasRealm.classNotFound", className)); + } + } + } + + /** + * Comma-delimited list of java.security.Principal classes that represent individual users. + */ + protected String userClassNames = null; + + public String getUserClassNames() { + return this.userClassNames; + } + + /** + * Sets the list of comma-delimited classes that represent individual users. The classes in the list must implement + * java.security.Principal. The supplied list of classes will be parsed when {@link #start()} is + * called. + * + * @param userClassNames The class names list + */ + public void setUserClassNames(String userClassNames) { + this.userClassNames = userClassNames; + } + + + // --------------------------------------------------------- Public Methods + + @Override + public Principal authenticate(String username, String credentials) { + return authenticate(username, new JAASCallbackHandler(this, username, credentials)); + } + + + @Override + public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, + String qop, String realmName, String digestA2, String algorithm) { + return authenticate(username, new JAASCallbackHandler(this, username, clientDigest, nonce, nc, cnonce, qop, + realmName, digestA2, algorithm, HttpServletRequest.DIGEST_AUTH)); + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Perform the actual JAAS authentication. + * + * @param username The user name + * @param callbackHandler The callback handler + * + * @return the associated principal, or null if there is none. + */ + protected Principal authenticate(String username, CallbackHandler callbackHandler) { + + // Establish a LoginContext to use for authentication + try { + LoginContext loginContext = null; + if (appName == null) { + appName = "Tomcat"; + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("jaasRealm.beginLogin", username, appName)); + } + + // What if the LoginModule is in the container class loader ? + ClassLoader ocl = null; + Thread currentThread = null; + + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + + try { + Configuration config = getConfig(); + loginContext = new LoginContext(appName, null, callbackHandler, config); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + log.error(sm.getString("jaasRealm.unexpectedError"), e); + // There is configuration issue with JAAS so mark the realm as + // unavailable + invocationSuccess = false; + return null; + } finally { + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + + if (log.isTraceEnabled()) { + log.trace("Login context created " + username); + } + + // Negotiate a login via this LoginContext + Subject subject = null; + try { + loginContext.login(); + subject = loginContext.getSubject(); + // We were able to perform login successfully so mark JAAS realm as + // available as it could have been set to false in prior attempts. + // Change invocationSuccess variable only when we know the outcome + // of the JAAS operation to keep variable consistent. + invocationSuccess = true; + if (subject == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.failedLogin", username)); + } + return null; + } + } catch (AccountExpiredException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.accountExpired", username)); + } + // JAAS checked LoginExceptions are successful authentication + // invocations so mark JAAS realm as available + invocationSuccess = true; + return null; + } catch (CredentialExpiredException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.credentialExpired", username)); + } + // JAAS checked LoginExceptions are successful authentication + // invocations so mark JAAS realm as available + invocationSuccess = true; + return null; + } catch (FailedLoginException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.failedLogin", username)); + } + // JAAS checked LoginExceptions are successful authentication + // invocations so mark JAAS realm as available + invocationSuccess = true; + return null; + } catch (LoginException e) { + log.warn(sm.getString("jaasRealm.loginException", username), e); + // JAAS checked LoginExceptions are successful authentication + // invocations so mark JAAS realm as available + invocationSuccess = true; + return null; + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + log.error(sm.getString("jaasRealm.unexpectedError"), e); + // JAAS throws exception different than LoginException so mark the + // realm as unavailable + invocationSuccess = false; + return null; + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("jaasRealm.loginContextCreated", username)); + } + + // Return the appropriate Principal for this authenticated Subject + Principal principal = createPrincipal(username, subject, loginContext); + if (principal == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.authenticateFailure", username)); + } + return null; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("jaasRealm.authenticateSuccess", username, principal)); + } + + return principal; + } catch (Throwable t) { + log.error(sm.getString("jaasRealm.unexpectedError"), t); + // JAAS throws exception different than LoginException so mark the realm as unavailable + invocationSuccess = false; + return null; + } + } + + + /** + * @return the password associated with the given principal's user name. This always returns null as the JAASRealm + * has no way of obtaining this information. + */ + @Override + protected String getPassword(String username) { + return null; + } + + + /** + * @return the Principal associated with the given user name. + */ + @Override + protected Principal getPrincipal(String username) { + + return authenticate(username, new JAASCallbackHandler(this, username, null, null, null, null, null, null, null, + null, HttpServletRequest.CLIENT_CERT_AUTH)); + + } + + + /** + * Identify and return a java.security.Principal instance representing the authenticated user for the + * specified Subject. The Principal is constructed by scanning the list of Principals returned by the + * JAASLoginModule. The first Principal object that matches one of the class names supplied as a "user + * class" is the user Principal. This object is returned to the caller. Any remaining principal objects returned by + * the LoginModules are mapped to roles, but only if their respective classes match one of the "role class" classes. + * If a user Principal cannot be constructed, return null. + * + * @param username The associated user name + * @param subject The Subject representing the logged-in user + * @param loginContext Associated with the Principal so {@link LoginContext#logout()} can be called later + * + * @return the principal object + */ + protected Principal createPrincipal(String username, Subject subject, LoginContext loginContext) { + // Prepare to scan the Principals for this Subject + + List roles = new ArrayList<>(); + Principal userPrincipal = null; + + // Scan the Principals for this Subject + for (Principal principal : subject.getPrincipals()) { + String principalClass = principal.getClass().getName(); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("jaasRealm.checkPrincipal", principal, principalClass)); + } + + if (userPrincipal == null && userClasses.contains(principalClass)) { + userPrincipal = principal; + if (log.isTraceEnabled()) { + log.trace(sm.getString("jaasRealm.userPrincipalSuccess", principal.getName())); + } + } + + if (roleClasses.contains(principalClass)) { + roles.add(principal.getName()); + if (log.isTraceEnabled()) { + log.trace(sm.getString("jaasRealm.rolePrincipalAdd", principal.getName())); + } + } + } + + // Print failure message if needed + if (userPrincipal == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.userPrincipalFailure")); + log.debug(sm.getString("jaasRealm.rolePrincipalFailure")); + } + return null; + } else { + if (roles.size() == 0) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.rolePrincipalFailure")); + } + } + } + + // Return the resulting Principal for our authenticated user + return new GenericPrincipal(username, roles, userPrincipal, loginContext); + } + + /** + * Ensure the given name is legal for JAAS configuration. Added for Bugzilla 30869, made protected for easy + * customization in case my implementation is insufficient, which I think is very likely. + * + * @param src The name to validate + * + * @return A string that's a valid JAAS realm name + */ + protected String makeLegalForJAAS(final String src) { + String result = src; + + // Default name is "other" per JAAS spec + if (result == null) { + result = "other"; + } + + // Strip leading slash if present, as Sun JAAS impl + // barfs on it (see Bugzilla 30869 bug report). + if (result.startsWith("/")) { + result = result.substring(1); + } + + return result; + } + + + // ------------------------------------------------------ Lifecycle Methods + + @Override + protected void startInternal() throws LifecycleException { + + // These need to be called after loading configuration, in case + // useContextClassLoader appears after them in xml config + parseClassNames(userClassNames, userClasses); + parseClassNames(roleClassNames, roleClasses); + + super.startInternal(); + } + + + /** + * Load custom JAAS Configuration. + * + * @return the loaded configuration + */ + protected Configuration getConfig() { + // Local copy to avoid possible NPE due to concurrent change + String configFile = this.configFile; + try { + if (jaasConfigurationLoaded) { + return jaasConfiguration; + } + synchronized (this) { + if (configFile == null) { + jaasConfigurationLoaded = true; + return null; + } + URL resource = Thread.currentThread().getContextClassLoader().getResource(configFile); + URI uri = resource.toURI(); + @SuppressWarnings("unchecked") + Class sunConfigFile = + (Class) Class.forName("com.sun.security.auth.login.ConfigFile"); + Constructor constructor = sunConfigFile.getConstructor(URI.class); + Configuration config = constructor.newInstance(uri); + this.jaasConfiguration = config; + this.jaasConfigurationLoaded = true; + return this.jaasConfiguration; + } + } catch (InvocationTargetException ex) { + throw new RuntimeException(ex.getCause()); + } catch (SecurityException | URISyntaxException | ReflectiveOperationException | IllegalArgumentException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public boolean isAvailable() { + return invocationSuccess; + } +} diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java new file mode 100644 index 0000000..90a5d26 --- /dev/null +++ b/java/org/apache/catalina/realm/JNDIRealm.java @@ -0,0 +1,3178 @@ +/* + * 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.catalina.realm; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import javax.naming.AuthenticationException; +import javax.naming.CompositeName; +import javax.naming.Context; +import javax.naming.InvalidNameException; +import javax.naming.Name; +import javax.naming.NameNotFoundException; +import javax.naming.NameParser; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.PartialResultException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.StartTlsRequest; +import javax.naming.ldap.StartTlsResponse; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; + +import org.apache.catalina.LifecycleException; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSName; + +/** + *

    + * Implementation of Realm that works with a directory server accessed via the Java Naming and + * Directory Interface (JNDI) APIs. The following constraints are imposed on the data structure in the underlying + * directory server: + *

    + *
      + *
    • Each user that can be authenticated is represented by an individual element in the top level + * DirContext that is accessed via the connectionURL property.
    • + *
    • If a socket connection cannot be made to the connectURL an attempt will be made to use the + * alternateURL if it exists.
    • + *
    • Each user element has a distinguished name that can be formed by substituting the presented username into a + * pattern configured by the userPattern property.
    • + *
    • Alternatively, if the userPattern property is not specified, a unique element can be located by + * searching the directory context. In this case: + *
        + *
      • The userSearch pattern specifies the search filter after substitution of the username.
      • + *
      • The userBase property can be set to the element that is the base of the subtree containing users. If + * not specified, the search base is the top-level context.
      • + *
      • The userSubtree property can be set to true if you wish to search the entire subtree of + * the directory context. The default value of false requests a search of only the current level.
      • + *
      + *
    • + *
    • The user may be authenticated by binding to the directory with the username and password presented. This method + * is used when the userPassword property is not specified.
    • + *
    • The user may be authenticated by retrieving the value of an attribute from the directory and comparing it + * explicitly with the value presented by the user. This method is used when the userPassword property is + * specified, in which case: + *
        + *
      • The element for this user must contain an attribute named by the userPassword property. + *
      • The value of the user password attribute is either a cleartext String, or the result of passing a cleartext + * String through the RealmBase.digest() method (using the standard digest support included in + * RealmBase). + *
      • The user is considered to be authenticated if the presented credentials (after being passed through + * RealmBase.digest()) are equal to the retrieved value for the user password attribute.
      • + *
      + *
    • + *
    • Each group of users that has been assigned a particular role may be represented by an individual element in the + * top level DirContext that is accessed via the connectionURL property. This element has the + * following characteristics: + *
        + *
      • The set of all possible groups of interest can be selected by a search pattern configured by the + * roleSearch property.
      • + *
      • The roleSearch pattern optionally includes pattern replacements "{0}" for the distinguished name, + * and/or "{1}" for the username, and/or "{2}" the value of an attribute from the user's directory entry (the attribute + * is specified by the userRoleAttribute property), of the authenticated user for which roles will be + * retrieved.
      • + *
      • The roleBase property can be set to the element that is the base of the search for matching roles. + * If not specified, the entire context will be searched.
      • + *
      • The roleSubtree property can be set to true if you wish to search the entire subtree of + * the directory context. The default value of false requests a search of only the current level.
      • + *
      • The element includes an attribute (whose name is configured by the roleName property) containing the + * name of the role represented by this element.
      • + *
      + *
    • + *
    • In addition, roles may be represented by the values of an attribute in the user's element whose name is + * configured by the userRoleName property.
    • + *
    • A default role can be assigned to each user that was successfully authenticated by setting the + * commonRole property to the name of this role. The role doesn't have to exist in the directory.
    • + *
    • If the directory server contains nested roles, you can search for them by setting roleNested to + * true. The default value is false, so role searches will not find nested roles.
    • + *
    • Note that the standard <security-role-ref> element in the web application deployment + * descriptor allows applications to refer to roles programmatically by names other than those used in the directory + * server itself.
    • + *
    + *

    + * WARNING - There is a reported bug against the Netscape provider code + * (com.netscape.jndi.ldap.LdapContextFactory) with respect to successfully authenticated a non-existing user. The + * report is here: https://bz.apache.org/bugzilla/show_bug.cgi?id=11210 . With luck, Netscape has updated their provider + * code and this is not an issue. + *

    + * + * @author John Holman + * @author Craig R. McClanahan + */ +public class JNDIRealm extends RealmBase { + + // ----------------------------------------------------- Instance Variables + + /** + * The type of authentication to use + */ + protected String authentication = null; + + /** + * The connection username for the server we will contact. + */ + protected String connectionName = null; + + /** + * The connection password for the server we will contact. + */ + protected String connectionPassword = null; + + /** + * The connection URL for the server we will contact. + */ + protected String connectionURL = null; + + /** + * The JNDI context factory used to acquire our InitialContext. By default, assumes use of an LDAP server using the + * standard JNDI LDAP provider. + */ + protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; + + /** + * How aliases should be dereferenced during search operations. + */ + protected String derefAliases = null; + + /** + * Constant that holds the name of the environment property for specifying the manner in which aliases should be + * dereferenced. + */ + public static final String DEREF_ALIASES = "java.naming.ldap.derefAliases"; + + /** + * The protocol that will be used in the communication with the directory server. + */ + protected String protocol = null; + + /** + * Should we ignore PartialResultExceptions when iterating over NamingEnumerations? Microsoft Active Directory often + * returns referrals, which lead to PartialResultExceptions. Unfortunately there's no stable way to detect, if the + * Exceptions really come from an AD referral. Set to true to ignore PartialResultExceptions. + */ + protected boolean adCompat = false; + + /** + * How should we handle referrals? Microsoft Active Directory often returns referrals. If you need to follow them + * set referrals to "follow". Caution: if your DNS is not part of AD, the LDAP client lib might try to resolve your + * domain name in DNS to find another LDAP server. + */ + protected String referrals = null; + + /** + * The base element for user searches. + */ + protected String userBase = ""; + + /** + * The message format used to search for a user, with "{0}" marking the spot where the username goes. + */ + protected String userSearch = null; + + /** + * When searching for users, should the search be performed as the user currently being authenticated? If false, + * {@link #connectionName} and {@link #connectionPassword} will be used if specified, else an anonymous connection + * will be used. + */ + private boolean userSearchAsUser = false; + + /** + * Should we search the entire subtree for matching users? + */ + protected boolean userSubtree = false; + + /** + * The attribute name used to retrieve the user password. + */ + protected String userPassword = null; + + /** + * The name of the attribute inside the users directory entry where the value will be taken to search for roles This + * attribute is not used during a nested search + */ + protected String userRoleAttribute = null; + + /** + * A string of LDAP user patterns or paths, ":"-separated These will be used to form the distinguished name of a + * user, with "{0}" marking the spot where the specified username goes. This is similar to userPattern, but allows + * for multiple searches for a user. + */ + protected String[] userPatternArray = null; + + /** + * The message format used to form the distinguished name of a user, with "{0}" marking the spot where the specified + * username goes. + */ + protected String userPattern = null; + + /** + * The base element for role searches. + */ + protected String roleBase = ""; + + /** + * The name of an attribute in the user's entry containing roles for that user + */ + protected String userRoleName = null; + + /** + * The name of the attribute containing roles held elsewhere + */ + protected String roleName = null; + + /** + * The message format used to select roles for a user, with "{0}" marking the spot where the distinguished name of + * the user goes. The "{1}" and "{2}" are described in the Configuration Reference. + */ + protected String roleSearch = null; + + /** + * Should we search the entire subtree for matching memberships? + */ + protected boolean roleSubtree = false; + + /** + * Should we look for nested group in order to determine roles? + */ + protected boolean roleNested = false; + + /** + * When searching for user roles, should the search be performed as the user currently being authenticated? If + * false, {@link #connectionName} and {@link #connectionPassword} will be used if specified, else an anonymous + * connection will be used. + */ + protected boolean roleSearchAsUser = false; + + /** + * An alternate URL, to which, we should connect if connectionURL fails. + */ + protected String alternateURL; + + /** + * The number of connection attempts. If greater than zero we use the alternate url. + */ + protected int connectionAttempt = 0; + + /** + * Add this role to every authenticated user + */ + protected String commonRole = null; + + /** + * The timeout, in milliseconds, to use when trying to create a connection to the directory. The default is 5000 (5 + * seconds). + */ + protected String connectionTimeout = "5000"; + + /** + * The timeout, in milliseconds, to use when trying to read from a connection to the directory. The default is 5000 + * (5 seconds). + */ + protected String readTimeout = "5000"; + + /** + * The sizeLimit (also known as the countLimit) to use when the realm is configured with {@link #userSearch}. Zero + * for no limit. + */ + protected long sizeLimit = 0; + + /** + * The timeLimit (in milliseconds) to use when the realm is configured with {@link #userSearch}. Zero for no limit. + */ + protected int timeLimit = 0; + + /** + * Should delegated credentials from the SPNEGO authenticator be used if available + */ + protected boolean useDelegatedCredential = true; + + /** + * The QOP that should be used for the connection to the LDAP server after authentication. This value is used to set + * the javax.security.sasl.qop environment property for the LDAP connection. + */ + protected String spnegoDelegationQop = "auth-conf"; + + /** + * Whether to use TLS for connections + */ + private boolean useStartTls = false; + + private StartTlsResponse tls = null; + + /** + * The list of enabled cipher suites used for establishing tls connections. null means to use the + * default cipher suites. + */ + private String[] cipherSuitesArray = null; + + /** + * Verifier for hostnames in a StartTLS secured connection. null means to use the default verifier. + */ + private HostnameVerifier hostnameVerifier = null; + + /** + * {@link SSLSocketFactory} to use when connection with StartTLS enabled. + */ + private SSLSocketFactory sslSocketFactory = null; + + /** + * Name of the class of the {@link SSLSocketFactory}. null means to use the default factory. + */ + private String sslSocketFactoryClassName; + + /** + * Comma separated list of cipher suites to use for StartTLS. If empty, the default suites are used. + */ + private String cipherSuites; + + /** + * Name of the class of the {@link HostnameVerifier}. null means to use the default verifier. + */ + private String hostNameVerifierClassName; + + /** + * The ssl Protocol which will be used by StartTLS. + */ + private String sslProtocol; + + private boolean forceDnHexEscape = false; + + /** + * Non pooled connection to our directory server. + */ + protected JNDIConnection singleConnection; + + /** + * The lock to ensure single connection thread safety. + */ + protected final Lock singleConnectionLock = new ReentrantLock(); + + /** + * Connection pool. + */ + protected SynchronizedStack connectionPool = null; + + /** + * The pool size limit. If 1, pooling is not used. + */ + protected int connectionPoolSize = 1; + + /** + * Whether to use context ClassLoader or default ClassLoader. True means use context ClassLoader, and True is the + * default value. + */ + protected boolean useContextClassLoader = true; + + + // ------------------------------------------------------------- Properties + + public boolean getForceDnHexEscape() { + return forceDnHexEscape; + } + + + public void setForceDnHexEscape(boolean forceDnHexEscape) { + this.forceDnHexEscape = forceDnHexEscape; + } + + + /** + * @return the type of authentication to use. + */ + public String getAuthentication() { + return authentication; + } + + + /** + * Set the type of authentication to use. + * + * @param authentication The authentication + */ + public void setAuthentication(String authentication) { + this.authentication = authentication; + } + + + /** + * @return the connection username for this Realm. + */ + public String getConnectionName() { + return this.connectionName; + } + + + /** + * Set the connection username for this Realm. + * + * @param connectionName The new connection username + */ + public void setConnectionName(String connectionName) { + this.connectionName = connectionName; + } + + + /** + * @return the connection password for this Realm. + */ + public String getConnectionPassword() { + return this.connectionPassword; + } + + + /** + * Set the connection password for this Realm. + * + * @param connectionPassword The new connection password + */ + public void setConnectionPassword(String connectionPassword) { + this.connectionPassword = connectionPassword; + } + + + /** + * @return the connection URL for this Realm. + */ + public String getConnectionURL() { + return this.connectionURL; + } + + + /** + * Set the connection URL for this Realm. + * + * @param connectionURL The new connection URL + */ + public void setConnectionURL(String connectionURL) { + this.connectionURL = connectionURL; + } + + + /** + * @return the JNDI context factory for this Realm. + */ + public String getContextFactory() { + return this.contextFactory; + } + + + /** + * Set the JNDI context factory for this Realm. + * + * @param contextFactory The new context factory + */ + public void setContextFactory(String contextFactory) { + this.contextFactory = contextFactory; + } + + + /** + * @return the derefAliases setting to be used. + */ + public String getDerefAliases() { + return derefAliases; + } + + + /** + * Set the value for derefAliases to be used when searching the directory. + * + * @param derefAliases New value of property derefAliases. + */ + public void setDerefAliases(String derefAliases) { + this.derefAliases = derefAliases; + } + + + /** + * @return the protocol to be used. + */ + public String getProtocol() { + return protocol; + } + + + /** + * Set the protocol for this Realm. + * + * @param protocol The new protocol. + */ + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + + /** + * @return the current settings for handling PartialResultExceptions + */ + public boolean getAdCompat() { + return adCompat; + } + + + /** + * How do we handle PartialResultExceptions? True: ignore all PartialResultExceptions. + * + * @param adCompat true to ignore partial results + */ + public void setAdCompat(boolean adCompat) { + this.adCompat = adCompat; + } + + + /** + * @return the current settings for handling JNDI referrals. + */ + public String getReferrals() { + return referrals; + } + + + /** + * How do we handle JNDI referrals? ignore, follow, or throw (see javax.naming.Context.REFERRAL for more + * information). + * + * @param referrals The referral handling + */ + public void setReferrals(String referrals) { + this.referrals = referrals; + } + + + /** + * @return the base element for user searches. + */ + public String getUserBase() { + return this.userBase; + } + + + /** + * Set the base element for user searches. + * + * @param userBase The new base element + */ + public void setUserBase(String userBase) { + this.userBase = userBase; + } + + + /** + * @return the message format pattern for selecting users in this Realm. + */ + public String getUserSearch() { + return this.userSearch; + } + + + /** + * Set the message format pattern for selecting users in this Realm. + * + * @param userSearch The new user search pattern + */ + public void setUserSearch(String userSearch) { + this.userSearch = userSearch; + singleConnection = create(); + } + + + public boolean isUserSearchAsUser() { + return userSearchAsUser; + } + + + public void setUserSearchAsUser(boolean userSearchAsUser) { + this.userSearchAsUser = userSearchAsUser; + } + + + /** + * @return the "search subtree for users" flag. + */ + public boolean getUserSubtree() { + return this.userSubtree; + } + + + /** + * Set the "search subtree for users" flag. + * + * @param userSubtree The new search flag + */ + public void setUserSubtree(boolean userSubtree) { + this.userSubtree = userSubtree; + } + + + /** + * @return the user role name attribute name for this Realm. + */ + public String getUserRoleName() { + return userRoleName; + } + + + /** + * Set the user role name attribute name for this Realm. + * + * @param userRoleName The new userRole name attribute name + */ + public void setUserRoleName(String userRoleName) { + this.userRoleName = userRoleName; + } + + + /** + * @return the base element for role searches. + */ + public String getRoleBase() { + return this.roleBase; + } + + + /** + * Set the base element for role searches. + * + * @param roleBase The new base element + */ + public void setRoleBase(String roleBase) { + this.roleBase = roleBase; + singleConnection = create(); + } + + + /** + * @return the role name attribute name for this Realm. + */ + public String getRoleName() { + return this.roleName; + } + + + /** + * Set the role name attribute name for this Realm. + * + * @param roleName The new role name attribute name + */ + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + + /** + * @return the message format pattern for selecting roles in this Realm. + */ + public String getRoleSearch() { + return this.roleSearch; + } + + + /** + * Set the message format pattern for selecting roles in this Realm. + * + * @param roleSearch The new role search pattern + */ + public void setRoleSearch(String roleSearch) { + this.roleSearch = roleSearch; + singleConnection = create(); + } + + + public boolean isRoleSearchAsUser() { + return roleSearchAsUser; + } + + + public void setRoleSearchAsUser(boolean roleSearchAsUser) { + this.roleSearchAsUser = roleSearchAsUser; + } + + + /** + * @return the "search subtree for roles" flag. + */ + public boolean getRoleSubtree() { + return this.roleSubtree; + } + + + /** + * Set the "search subtree for roles" flag. + * + * @param roleSubtree The new search flag + */ + public void setRoleSubtree(boolean roleSubtree) { + this.roleSubtree = roleSubtree; + } + + + /** + * @return the "The nested group search flag" flag. + */ + public boolean getRoleNested() { + return this.roleNested; + } + + + /** + * Set the "search subtree for roles" flag. + * + * @param roleNested The nested group search flag + */ + public void setRoleNested(boolean roleNested) { + this.roleNested = roleNested; + } + + + /** + * @return the password attribute used to retrieve the user password. + */ + public String getUserPassword() { + return this.userPassword; + } + + + /** + * Set the password attribute used to retrieve the user password. + * + * @param userPassword The new password attribute + */ + public void setUserPassword(String userPassword) { + this.userPassword = userPassword; + } + + + public String getUserRoleAttribute() { + return userRoleAttribute; + } + + + public void setUserRoleAttribute(String userRoleAttribute) { + this.userRoleAttribute = userRoleAttribute; + } + + /** + * @return the message format pattern for selecting users in this Realm. + */ + public String getUserPattern() { + return this.userPattern; + } + + + /** + * Set the message format pattern for selecting users in this Realm. This may be one simple pattern, or multiple + * patterns to be tried, separated by parentheses. (for example, either "cn={0}", or "(cn={0})(cn={0},o=myorg)" Full + * LDAP search strings are also supported, but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is also + * valid. Complex search strings with &, etc are NOT supported. + * + * @param userPattern The new user pattern + */ + public void setUserPattern(String userPattern) { + this.userPattern = userPattern; + if (userPattern == null) { + userPatternArray = null; + } else { + userPatternArray = parseUserPatternString(userPattern); + singleConnection = create(); + } + } + + + /** + * Getter for property alternateURL. + * + * @return Value of property alternateURL. + */ + public String getAlternateURL() { + return this.alternateURL; + } + + + /** + * Setter for property alternateURL. + * + * @param alternateURL New value of property alternateURL. + */ + public void setAlternateURL(String alternateURL) { + this.alternateURL = alternateURL; + } + + + /** + * @return the common role + */ + public String getCommonRole() { + return commonRole; + } + + + /** + * Set the common role + * + * @param commonRole The common role + */ + public void setCommonRole(String commonRole) { + this.commonRole = commonRole; + } + + + /** + * @return the connection timeout. + */ + public String getConnectionTimeout() { + return connectionTimeout; + } + + + /** + * Set the connection timeout. + * + * @param timeout The new connection timeout + */ + public void setConnectionTimeout(String timeout) { + this.connectionTimeout = timeout; + } + + + /** + * @return the read timeout. + */ + public String getReadTimeout() { + return readTimeout; + } + + + /** + * Set the read timeout. + * + * @param timeout The new read timeout + */ + public void setReadTimeout(String timeout) { + this.readTimeout = timeout; + } + + + public long getSizeLimit() { + return sizeLimit; + } + + + public void setSizeLimit(long sizeLimit) { + this.sizeLimit = sizeLimit; + } + + + public int getTimeLimit() { + return timeLimit; + } + + + public void setTimeLimit(int timeLimit) { + this.timeLimit = timeLimit; + } + + + public boolean isUseDelegatedCredential() { + return useDelegatedCredential; + } + + + public void setUseDelegatedCredential(boolean useDelegatedCredential) { + this.useDelegatedCredential = useDelegatedCredential; + } + + + public String getSpnegoDelegationQop() { + return spnegoDelegationQop; + } + + + public void setSpnegoDelegationQop(String spnegoDelegationQop) { + this.spnegoDelegationQop = spnegoDelegationQop; + } + + + /** + * @return flag whether to use StartTLS for connections to the ldap server + */ + public boolean getUseStartTls() { + return useStartTls; + } + + + /** + * Flag whether StartTLS should be used when connecting to the ldap server + * + * @param useStartTls {@code true} when StartTLS should be used. Default is {@code false}. + */ + public void setUseStartTls(boolean useStartTls) { + this.useStartTls = useStartTls; + } + + + /** + * @return list of the allowed cipher suites when connections are made using StartTLS + */ + private String[] getCipherSuitesArray() { + if (cipherSuites == null || cipherSuitesArray != null) { + return cipherSuitesArray; + } + this.cipherSuites = this.cipherSuites.trim(); + if (this.cipherSuites.isEmpty()) { + containerLog.warn(sm.getString("jndiRealm.emptyCipherSuites")); + this.cipherSuitesArray = null; + } else { + this.cipherSuitesArray = StringUtils.splitCommaSeparated(cipherSuites); + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("jndiRealm.cipherSuites", Arrays.toString(this.cipherSuitesArray))); + } + } + return this.cipherSuitesArray; + } + + + /** + * Set the allowed cipher suites when opening a connection using StartTLS. The cipher suites are expected as a comma + * separated list. + * + * @param suites comma separated list of allowed cipher suites + */ + public void setCipherSuites(String suites) { + this.cipherSuites = suites; + } + + + /** + * @return the connection pool size, or the default value 1 if pooling is disabled + */ + public int getConnectionPoolSize() { + return connectionPoolSize; + } + + + /** + * Set the connection pool size + * + * @param connectionPoolSize the new pool size + */ + public void setConnectionPoolSize(int connectionPoolSize) { + this.connectionPoolSize = connectionPoolSize; + } + + + /** + * @return name of the {@link HostnameVerifier} class used for connections using StartTLS, or the empty string, if + * the default verifier should be used. + */ + public String getHostnameVerifierClassName() { + if (this.hostnameVerifier == null) { + return ""; + } + return this.hostnameVerifier.getClass().getCanonicalName(); + } + + + /** + * Set the {@link HostnameVerifier} to be used when opening connections using StartTLS. An instance of the given + * class name will be constructed using the default constructor. + * + * @param verifierClassName class name of the {@link HostnameVerifier} to be constructed + */ + public void setHostnameVerifierClassName(String verifierClassName) { + if (verifierClassName != null) { + this.hostNameVerifierClassName = verifierClassName.trim(); + } else { + this.hostNameVerifierClassName = null; + } + } + + + /** + * @return the {@link HostnameVerifier} to use for peer certificate verification when opening connections using + * StartTLS. + */ + public HostnameVerifier getHostnameVerifier() { + if (this.hostnameVerifier != null) { + return this.hostnameVerifier; + } + if (this.hostNameVerifierClassName == null || hostNameVerifierClassName.equals("")) { + return null; + } + try { + Object o = constructInstance(hostNameVerifierClassName); + if (o instanceof HostnameVerifier) { + this.hostnameVerifier = (HostnameVerifier) o; + return this.hostnameVerifier; + } else { + throw new IllegalArgumentException( + sm.getString("jndiRealm.invalidHostnameVerifier", hostNameVerifierClassName)); + } + } catch (ReflectiveOperationException | SecurityException e) { + throw new IllegalArgumentException( + sm.getString("jndiRealm.invalidHostnameVerifier", hostNameVerifierClassName), e); + } + } + + + /** + * Set the {@link SSLSocketFactory} to be used when opening connections using StartTLS. An instance of the factory + * with the given name will be created using the default constructor. The SSLSocketFactory can also be set using + * {@link JNDIRealm#setSslProtocol(String) setSslProtocol(String)}. + * + * @param factoryClassName class name of the factory to be constructed + */ + public void setSslSocketFactoryClassName(String factoryClassName) { + this.sslSocketFactoryClassName = factoryClassName; + } + + + /** + * Set the ssl protocol to be used for connections using StartTLS. + * + * @param protocol one of the allowed ssl protocol names + */ + public void setSslProtocol(String protocol) { + this.sslProtocol = protocol; + } + + + /** + * @return the list of supported ssl protocols by the default {@link SSLContext} + */ + private String[] getSupportedSslProtocols() { + try { + SSLContext sslContext = SSLContext.getDefault(); + return sslContext.getSupportedSSLParameters().getProtocols(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(sm.getString("jndiRealm.exception"), e); + } + } + + + private Object constructInstance(String className) throws ReflectiveOperationException { + Class clazz = Class.forName(className); + return clazz.getConstructor().newInstance(); + } + + + /** + * Sets whether to use the context or default ClassLoader. True means use context ClassLoader. + * + * @param useContext True means use context ClassLoader + */ + public void setUseContextClassLoader(boolean useContext) { + useContextClassLoader = useContext; + } + + + /** + * Returns whether to use the context or default ClassLoader. True means to use the context ClassLoader. + * + * @return The value of useContextClassLoader + */ + public boolean isUseContextClassLoader() { + return useContextClassLoader; + } + + + // ---------------------------------------------------------- Realm Methods + + /** + * {@inheritDoc} + *

    + * If there are any errors with the JNDI connection, executing the query or anything we return null (don't + * authenticate). This event is also logged, and the connection will be closed so that a subsequent request will + * automatically re-open it. + */ + @Override + public Principal authenticate(String username, String credentials) { + + ClassLoader ocl = null; + Thread currentThread = null; + JNDIConnection connection = null; + Principal principal = null; + + try { + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65553 + // This can move back to open() once it is known that Tomcat must be + // running on a JVM that includes a fix for + // https://bugs.openjdk.java.net/browse/JDK-8273874 + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + + // Ensure that we have a directory context available + connection = get(); + + try { + + // Occasionally the directory context will timeout. Try one more + // time before giving up. + + // Authenticate the specified username if possible + principal = authenticate(connection, username, credentials); + + } catch (NullPointerException | NamingException e) { + /* + * BZ 61313 NamingException may or may not indicate an error that is recoverable via fail over. + * Therefore a decision needs to be made whether to fail over or not. Generally, attempting to fail over + * when it is not appropriate is better than not failing over when it is appropriate so the code always + * attempts to fail over for NamingExceptions. + */ + + /* + * BZ 42449 Catch NPE - Kludge Sun's LDAP provider with broken SSL. + */ + + // log the exception so we know it's there. + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); + + // close the connection so we know it will be reopened. + close(connection); + closePooledConnections(); + + // open a new directory context. + connection = get(); + + // Try the authentication again. + principal = authenticate(connection, username, credentials); + } + + + // Release this context + release(connection); + + // Return the authenticated Principal (if any) + return principal; + + } catch (Exception e) { + + // Log the problem for posterity + containerLog.error(sm.getString("jndiRealm.exception"), e); + + // close the connection so we know it will be reopened. + close(connection); + closePooledConnections(); + + // Return "not authenticated" for this request + if (containerLog.isTraceEnabled()) { + containerLog.trace("Returning null principal."); + } + return null; + } finally { + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + } + + + /** + * Return the Principal associated with the specified username and credentials, if there is one; otherwise return + * null. + * + * @param connection The directory context + * @param username Username of the Principal to look up + * @param credentials Password or other credentials to use in authenticating this username + * + * @return the associated principal, or null if there is none. + * + * @exception NamingException if a directory server error occurs + */ + public Principal authenticate(JNDIConnection connection, String username, String credentials) + throws NamingException { + + if (username == null || username.equals("") || credentials == null || credentials.equals("")) { + if (containerLog.isTraceEnabled()) { + containerLog.trace("username null or empty: returning null principal."); + } + return null; + } + + ClassLoader ocl = null; + Thread currentThread = null; + try { + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65553 + // This can move back to open() once it is known that Tomcat must be + // running on a JVM that includes a fix for + // https://bugs.openjdk.java.net/browse/JDK-8273874 + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + + if (userPatternArray != null) { + for (int curUserPattern = 0; curUserPattern < userPatternArray.length; curUserPattern++) { + // Retrieve user information + User user = getUser(connection, username, credentials, curUserPattern); + if (user != null) { + try { + // Check the user's credentials + if (checkCredentials(connection.context, user, credentials)) { + // Search for additional roles + List roles = getRoles(connection, user); + if (containerLog.isTraceEnabled()) { + containerLog.trace("Found roles: " + ((roles == null) ? "" : roles.toString())); + } + return new GenericPrincipal(username, roles); + } + } catch (InvalidNameException ine) { + // Log the problem for posterity + containerLog.warn(sm.getString("jndiRealm.exception"), ine); + // ignore; this is probably due to a name not fitting + // the search path format exactly, as in a fully- + // qualified name being munged into a search path + // that already contains cn= or vice-versa + } + } + } + return null; + } else { + // Retrieve user information + User user = getUser(connection, username, credentials); + if (user == null) { + return null; + } + + // Check the user's credentials + if (!checkCredentials(connection.context, user, credentials)) { + return null; + } + + // Search for additional roles + List roles = getRoles(connection, user); + if (containerLog.isTraceEnabled()) { + containerLog.trace("Found roles: " + ((roles == null) ? "" : roles.toString())); + } + + // Create and return a suitable Principal for this user + return new GenericPrincipal(username, roles); + } + } finally { + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553 This method can be removed and the class loader switch moved + * back to open() once it is known that Tomcat must be running on a JVM that includes a fix for + * https://bugs.openjdk.java.net/browse/JDK-8273874 + */ + @Override + public Principal authenticate(String username) { + ClassLoader ocl = null; + Thread currentThread = null; + try { + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + return super.authenticate(username); + } finally { + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553 This method can be removed and the class loader switch moved + * back to open() once it is known that Tomcat must be running on a JVM that includes a fix for + * https://bugs.openjdk.java.net/browse/JDK-8273874 + */ + @Override + public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, + String qop, String realm, String digestA2, String algorithm) { + ClassLoader ocl = null; + Thread currentThread = null; + try { + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + return super.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realm, digestA2, algorithm); + } finally { + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553 This method can be removed and the class loader switch moved + * back to open() once it is known that Tomcat must be running on a JVM that includes a fix for + * https://bugs.openjdk.java.net/browse/JDK-8273874 + */ + @Override + public Principal authenticate(X509Certificate[] certs) { + ClassLoader ocl = null; + Thread currentThread = null; + try { + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + return super.authenticate(certs); + } finally { + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553 This method can be removed and the class loader switch moved + * back to open() once it is known that Tomcat must be running on a JVM that includes a fix for + * https://bugs.openjdk.java.net/browse/JDK-8273874 + */ + @Override + public Principal authenticate(GSSContext gssContext, boolean storeCred) { + ClassLoader ocl = null; + Thread currentThread = null; + try { + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + return super.authenticate(gssContext, storeCred); + } finally { + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553 This method can be removed and the class loader switch moved + * back to open() once it is known that Tomcat must be running on a JVM that includes a fix for + * https://bugs.openjdk.java.net/browse/JDK-8273874 + */ + @Override + public Principal authenticate(GSSName gssName, GSSCredential gssCredential) { + ClassLoader ocl = null; + Thread currentThread = null; + try { + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + return super.authenticate(gssName, gssCredential); + } finally { + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Return a User object containing information about the user with the specified username, if found in the + * directory; otherwise return null. + * + * @param connection The directory context + * @param username Username to be looked up + * + * @return the User object + * + * @exception NamingException if a directory server error occurs + * + * @see #getUser(JNDIConnection, String, String, int) + */ + protected User getUser(JNDIConnection connection, String username) throws NamingException { + return getUser(connection, username, null, -1); + } + + + /** + * Return a User object containing information about the user with the specified username, if found in the + * directory; otherwise return null. + * + * @param connection The directory context + * @param username Username to be looked up + * @param credentials User credentials (optional) + * + * @return the User object + * + * @exception NamingException if a directory server error occurs + * + * @see #getUser(JNDIConnection, String, String, int) + */ + protected User getUser(JNDIConnection connection, String username, String credentials) throws NamingException { + return getUser(connection, username, credentials, -1); + } + + + /** + * Return a User object containing information about the user with the specified username, if found in the + * directory; otherwise return null. If the userPassword configuration attribute is + * specified, the value of that attribute is retrieved from the user's directory entry. If the + * userRoleName configuration attribute is specified, all values of that attribute are retrieved from + * the directory entry. + * + * @param connection The directory context + * @param username Username to be looked up + * @param credentials User credentials (optional) + * @param curUserPattern Index into userPatternFormatArray + * + * @return the User object + * + * @exception NamingException if a directory server error occurs + */ + protected User getUser(JNDIConnection connection, String username, String credentials, int curUserPattern) + throws NamingException { + + User user = null; + + // Get attributes to retrieve from user entry + List list = new ArrayList<>(); + if (userPassword != null) { + list.add(userPassword); + } + if (userRoleName != null) { + list.add(userRoleName); + } + if (userRoleAttribute != null) { + list.add(userRoleAttribute); + } + String[] attrIds = list.toArray(new String[0]); + + // Use pattern or search for user entry + if (userPatternArray != null && curUserPattern >= 0) { + user = getUserByPattern(connection, username, credentials, attrIds, curUserPattern); + if (containerLog.isTraceEnabled()) { + containerLog.trace("Found user by pattern [" + user + "]"); + } + } else { + boolean thisUserSearchAsUser = isUserSearchAsUser(); + try { + if (thisUserSearchAsUser) { + userCredentialsAdd(connection.context, username, credentials); + } + user = getUserBySearch(connection, username, attrIds); + } finally { + if (thisUserSearchAsUser) { + userCredentialsRemove(connection.context); + } + } + if (containerLog.isTraceEnabled()) { + containerLog.trace("Found user by search [" + user + "]"); + } + } + if (userPassword == null && credentials != null && user != null) { + // The password is available. Insert it since it may be required for + // role searches. + return new User(user.getUserName(), user.getDN(), credentials, user.getRoles(), user.getUserRoleId()); + } + + return user; + } + + + /** + * Use the distinguished name to locate the directory entry for the user with the specified username and return a + * User object; otherwise return null. + * + * @param context The directory context + * @param username The username + * @param attrIds String[]containing names of attributes to + * @param dn Distinguished name of the user retrieve. + * + * @return the User object + * + * @exception NamingException if a directory server error occurs + */ + protected User getUserByPattern(DirContext context, String username, String[] attrIds, String dn) + throws NamingException { + + // If no attributes are requested, no need to look for them + if (attrIds == null || attrIds.length == 0) { + return new User(username, dn, null, null, null); + } + + // Get required attributes from user entry + Attributes attrs = null; + try { + attrs = context.getAttributes(dn, attrIds); + } catch (NameNotFoundException e) { + return null; + } + if (attrs == null) { + return null; + } + + // Retrieve value of userPassword + String password = null; + if (userPassword != null) { + password = getAttributeValue(userPassword, attrs); + } + + String userRoleAttrValue = null; + if (userRoleAttribute != null) { + userRoleAttrValue = getAttributeValue(userRoleAttribute, attrs); + } + + // Retrieve values of userRoleName attribute + ArrayList roles = null; + if (userRoleName != null) { + roles = addAttributeValues(userRoleName, attrs, roles); + } + + return new User(username, dn, password, roles, userRoleAttrValue); + } + + + /** + * Use the UserPattern configuration attribute to locate the directory entry for the user with the + * specified username and return a User object; otherwise return null. + * + * @param connection The directory context + * @param username The username + * @param credentials User credentials (optional) + * @param attrIds String[]containing names of attributes to + * @param curUserPattern Index into userPatternFormatArray + * + * @return the User object + * + * @exception NamingException if a directory server error occurs + * + * @see #getUserByPattern(DirContext, String, String[], String) + */ + protected User getUserByPattern(JNDIConnection connection, String username, String credentials, String[] attrIds, + int curUserPattern) throws NamingException { + + User user = null; + + if (username == null || userPatternArray[curUserPattern] == null) { + return null; + } + + // Form the DistinguishedName from the user pattern. + // Escape in case username contains a character with special meaning in + // an attribute value. + String dn = connection.userPatternFormatArray[curUserPattern] + .format(new String[] { doAttributeValueEscaping(username) }); + + try { + user = getUserByPattern(connection.context, username, attrIds, dn); + } catch (NameNotFoundException e) { + return null; + } catch (NamingException e) { + // If the getUserByPattern() call fails, try it again with the + // credentials of the user that we're searching for + try { + userCredentialsAdd(connection.context, dn, credentials); + + user = getUserByPattern(connection.context, username, attrIds, dn); + } finally { + userCredentialsRemove(connection.context); + } + } + return user; + } + + + /** + * Search the directory to return a User object containing information about the user with the specified username, + * if found in the directory; otherwise return null. + * + * @param connection The directory context + * @param username The username + * @param attrIds String[]containing names of attributes to retrieve. + * + * @return the User object + * + * @exception NamingException if a directory server error occurs + */ + protected User getUserBySearch(JNDIConnection connection, String username, String[] attrIds) + throws NamingException { + + if (username == null || connection.userSearchFormat == null) { + return null; + } + + // Form the search filter + // Escape in case username contains a character with special meaning in + // a search filter. + String filter = connection.userSearchFormat.format(new String[] { doFilterEscaping(username) }); + + // Set up the search controls + SearchControls constraints = new SearchControls(); + + if (userSubtree) { + constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); + } else { + constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); + } + + constraints.setCountLimit(sizeLimit); + constraints.setTimeLimit(timeLimit); + + // Specify the attributes to be retrieved + if (attrIds == null) { + attrIds = new String[0]; + } + constraints.setReturningAttributes(attrIds); + + NamingEnumeration results = connection.context.search(userBase, filter, constraints); + + try { + // Fail if no entries found + try { + if (results == null || !results.hasMore()) { + return null; + } + } catch (PartialResultException ex) { + if (!adCompat) { + throw ex; + } else { + return null; + } + } + + // Get result for the first entry found + SearchResult result = results.next(); + + // Check no further entries were found + try { + if (results.hasMore()) { + if (containerLog.isInfoEnabled()) { + containerLog.info(sm.getString("jndiRealm.multipleEntries", username)); + } + return null; + } + } catch (PartialResultException ex) { + if (!adCompat) { + throw ex; + } + } + + String dn = getDistinguishedName(connection.context, userBase, result); + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" entry found for " + username + " with dn " + dn); + } + + // Get the entry's attributes + Attributes attrs = result.getAttributes(); + if (attrs == null) { + return null; + } + + // Retrieve value of userPassword + String password = null; + if (userPassword != null) { + password = getAttributeValue(userPassword, attrs); + } + + String userRoleAttrValue = null; + if (userRoleAttribute != null) { + userRoleAttrValue = getAttributeValue(userRoleAttribute, attrs); + } + + // Retrieve values of userRoleName attribute + ArrayList roles = null; + if (userRoleName != null) { + roles = addAttributeValues(userRoleName, attrs, roles); + } + + return new User(username, dn, password, roles, userRoleAttrValue); + } finally { + if (results != null) { + results.close(); + } + } + } + + + /** + * Check whether the given User can be authenticated with the given credentials. If the userPassword + * configuration attribute is specified, the credentials previously retrieved from the directory are compared + * explicitly with those presented by the user. Otherwise the presented credentials are checked by binding to the + * directory as the user. + * + * @param context The directory context + * @param user The User to be authenticated + * @param credentials The credentials presented by the user + * + * @return true if the credentials are validated + * + * @exception NamingException if a directory server error occurs + */ + protected boolean checkCredentials(DirContext context, User user, String credentials) throws NamingException { + + boolean validated = false; + + if (userPassword == null) { + validated = bindAsUser(context, user, credentials); + } else { + validated = compareCredentials(context, user, credentials); + } + + if (containerLog.isTraceEnabled()) { + if (validated) { + containerLog.trace(sm.getString("jndiRealm.authenticateSuccess", user.getUserName())); + } else { + containerLog.trace(sm.getString("jndiRealm.authenticateFailure", user.getUserName())); + } + } + return validated; + } + + + /** + * Check whether the credentials presented by the user match those retrieved from the directory. + * + * @param context The directory context + * @param info The User to be authenticated + * @param credentials Authentication credentials + * + * @return true if the credentials are validated + * + * @exception NamingException if a directory server error occurs + */ + protected boolean compareCredentials(DirContext context, User info, String credentials) throws NamingException { + // Validate the credentials specified by the user + if (containerLog.isTraceEnabled()) { + containerLog.trace(" validating credentials"); + } + + if (info == null || credentials == null) { + return false; + } + + String password = info.getPassword(); + + return getCredentialHandler().matches(credentials, password); + } + + + /** + * Check credentials by binding to the directory as the user + * + * @param context The directory context + * @param user The User to be authenticated + * @param credentials Authentication credentials + * + * @return true if the credentials are validated + * + * @exception NamingException if a directory server error occurs + */ + protected boolean bindAsUser(DirContext context, User user, String credentials) throws NamingException { + + if (credentials == null || user == null) { + return false; + } + + // This is returned from the directory so will be attribute value + // escaped if required + String dn = user.getDN(); + if (dn == null) { + return false; + } + + // Validate the credentials specified by the user + if (containerLog.isTraceEnabled()) { + containerLog.trace(" validating credentials by binding as the user"); + } + + userCredentialsAdd(context, dn, credentials); + + // Elicit an LDAP bind operation + boolean validated = false; + try { + if (containerLog.isTraceEnabled()) { + containerLog.trace(" binding as " + dn); + } + context.getAttributes("", null); + validated = true; + } catch (AuthenticationException e) { + if (containerLog.isTraceEnabled()) { + containerLog.trace(" bind attempt failed"); + } + } + + userCredentialsRemove(context); + + return validated; + } + + + /** + * Configure the context to use the provided credentials for authentication. + * + * @param context DirContext to configure + * @param dn Distinguished name of user + * @param credentials Credentials of user + * + * @exception NamingException if a directory server error occurs + */ + private void userCredentialsAdd(DirContext context, String dn, String credentials) throws NamingException { + // Set up security environment to bind as the user + context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); + context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); + } + + + /** + * Configure the context to use {@link #connectionName} and {@link #connectionPassword} if specified or an anonymous + * connection if those attributes are not specified. + * + * @param context DirContext to configure + * + * @exception NamingException if a directory server error occurs + */ + private void userCredentialsRemove(DirContext context) throws NamingException { + // Restore the original security environment + if (connectionName != null) { + context.addToEnvironment(Context.SECURITY_PRINCIPAL, connectionName); + } else { + context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); + } + + if (connectionPassword != null) { + context.addToEnvironment(Context.SECURITY_CREDENTIALS, connectionPassword); + } else { + context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); + } + } + + + /** + * Return a List of roles associated with the given User. Any roles present in the user's directory entry are + * supplemented by a directory search. If no roles are associated with this user, a zero-length List is returned. + * + * @param connection The directory context we are searching + * @param user The User to be checked + * + * @return the list of role names + * + * @exception NamingException if a directory server error occurs + */ + protected List getRoles(JNDIConnection connection, User user) throws NamingException { + + if (user == null) { + return null; + } + + // This is returned from the directory so will be attribute value + // escaped if required + String dn = user.getDN(); + // This is the name the user provided to the authentication process so + // it will not be escaped + String username = user.getUserName(); + String userRoleId = user.getUserRoleId(); + + if (dn == null || username == null) { + return null; + } + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" getRoles(" + dn + ")"); + } + + // Start with roles retrieved from the user entry + List list = new ArrayList<>(); + List userRoles = user.getRoles(); + if (userRoles != null) { + list.addAll(userRoles); + } + if (commonRole != null) { + list.add(commonRole); + } + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" Found " + list.size() + " user internal roles"); + containerLog.trace(" Found user internal roles " + list.toString()); + } + + // Are we configured to do role searches? + if (connection.roleFormat == null || roleName == null) { + return list; + } + + // Set up parameters for an appropriate search filter + // The dn is already attribute value escaped but the others are not + // This is a filter so all input will require filter escaping + String filter = connection.roleFormat + .format(new String[] { doFilterEscaping(dn), doFilterEscaping(doAttributeValueEscaping(username)), + doFilterEscaping(doAttributeValueEscaping(userRoleId)) }); + SearchControls controls = new SearchControls(); + if (roleSubtree) { + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); + } else { + controls.setSearchScope(SearchControls.ONELEVEL_SCOPE); + } + controls.setReturningAttributes(new String[] { roleName }); + + String base = null; + if (connection.roleBaseFormat != null) { + NameParser np = connection.context.getNameParser(""); + Name name = np.parse(dn); + String nameParts[] = new String[name.size()]; + for (int i = 0; i < name.size(); i++) { + // May have been returned with \ escaping rather than + // \. Make sure it is \. + nameParts[i] = convertToHexEscape(name.get(i)); + } + base = connection.roleBaseFormat.format(nameParts); + } else { + base = ""; + } + + // Perform the configured search and process the results + NamingEnumeration results = + searchAsUser(connection.context, user, base, filter, controls, isRoleSearchAsUser()); + + if (results == null) { + return list; // Should never happen, but just in case ... + } + + Map groupMap = new HashMap<>(); + try { + while (results.hasMore()) { + SearchResult result = results.next(); + Attributes attrs = result.getAttributes(); + if (attrs == null) { + continue; + } + String dname = getDistinguishedName(connection.context, base, result); + String name = getAttributeValue(roleName, attrs); + if (name != null && dname != null) { + groupMap.put(dname, name); + } + } + } catch (PartialResultException ex) { + if (!adCompat) { + throw ex; + } + } finally { + results.close(); + } + + if (containerLog.isTraceEnabled()) { + Set> entries = groupMap.entrySet(); + containerLog.trace(" Found " + entries.size() + " direct roles"); + for (Entry entry : entries) { + containerLog.trace(" Found direct role " + entry.getKey() + " -> " + entry.getValue()); + } + } + + // if nested group search is enabled, perform searches for nested groups until no new group is found + if (getRoleNested()) { + + // The following efficient algorithm is known as memberOf Algorithm, as described in "Practices in + // Directory Groups". It avoids group slurping and handles cyclic group memberships as well. + // See http://middleware.internet2.edu/dir/ for details + + Map newGroups = new HashMap<>(groupMap); + while (!newGroups.isEmpty()) { + Map newThisRound = new HashMap<>(); // Stores the groups we find in this iteration + + for (Entry group : newGroups.entrySet()) { + // Group key is already value escaped if required + // Group value is not value escaped + // Everything needs to be filter escaped + filter = connection.roleFormat.format(new String[] { doFilterEscaping(group.getKey()), + doFilterEscaping(doAttributeValueEscaping(group.getValue())), + doFilterEscaping(doAttributeValueEscaping(group.getValue())) }); + + if (containerLog.isTraceEnabled()) { + containerLog + .trace("Perform a nested group search with base " + roleBase + " and filter " + filter); + } + + results = searchAsUser(connection.context, user, base, filter, controls, isRoleSearchAsUser()); + + try { + while (results.hasMore()) { + SearchResult result = results.next(); + Attributes attrs = result.getAttributes(); + if (attrs == null) { + continue; + } + String dname = getDistinguishedName(connection.context, roleBase, result); + String name = getAttributeValue(roleName, attrs); + if (name != null && dname != null && !groupMap.keySet().contains(dname)) { + groupMap.put(dname, name); + newThisRound.put(dname, name); + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" Found nested role " + dname + " -> " + name); + } + } + } + } catch (PartialResultException ex) { + if (!adCompat) { + throw ex; + } + } finally { + results.close(); + } + } + + newGroups = newThisRound; + } + } + + list.addAll(groupMap.values()); + return list; + } + + + /** + * Perform the search on the context as the {@code dn}, when {@code searchAsUser} is {@code true}, otherwise search + * the context with the default credentials. + * + * @param context context to search on + * @param user user to bind on + * @param base base to start the search from + * @param filter filter to use for the search + * @param controls controls to use for the search + * @param searchAsUser {@code true} when the search should be done as user, or {@code false} for using the default + * credentials + * + * @return enumeration with all found entries + * + * @throws NamingException if a directory server error occurs + */ + private NamingEnumeration searchAsUser(DirContext context, User user, String base, String filter, + SearchControls controls, boolean searchAsUser) throws NamingException { + NamingEnumeration results; + try { + if (searchAsUser) { + userCredentialsAdd(context, user.getDN(), user.getPassword()); + } + results = context.search(base, filter, controls); + } finally { + if (searchAsUser) { + userCredentialsRemove(context); + } + } + return results; + } + + + /** + * Return a String representing the value of the specified attribute. + * + * @param attrId Attribute name + * @param attrs Attributes containing the required value + * + * @return the attribute value + * + * @exception NamingException if a directory server error occurs + */ + private String getAttributeValue(String attrId, Attributes attrs) throws NamingException { + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" retrieving attribute " + attrId); + } + + if (attrId == null || attrs == null) { + return null; + } + + Attribute attr = attrs.get(attrId); + if (attr == null) { + return null; + } + Object value = attr.get(); + if (value == null) { + return null; + } + String valueString = null; + if (value instanceof byte[]) { + valueString = new String((byte[]) value); + } else { + valueString = value.toString(); + } + + return valueString; + } + + + /** + * Add values of a specified attribute to a list + * + * @param attrId Attribute name + * @param attrs Attributes containing the new values + * @param values ArrayList containing values found so far + * + * @return the list of attribute values + * + * @exception NamingException if a directory server error occurs + */ + private ArrayList addAttributeValues(String attrId, Attributes attrs, ArrayList values) + throws NamingException { + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" retrieving values for attribute " + attrId); + } + if (attrId == null || attrs == null) { + return values; + } + if (values == null) { + values = new ArrayList<>(); + } + Attribute attr = attrs.get(attrId); + if (attr == null) { + return values; + } + NamingEnumeration e = attr.getAll(); + try { + while (e.hasMore()) { + String value = (String) e.next(); + values.add(value); + } + } catch (PartialResultException ex) { + if (!adCompat) { + throw ex; + } + } finally { + e.close(); + } + return values; + } + + + /** + * Close any open connection to the directory server for this Realm. + * + * @param connection The directory context to be closed + */ + protected void close(JNDIConnection connection) { + + // Do nothing if there is no opened connection + if (connection == null || connection.context == null) { + if (connectionPool == null) { + singleConnectionLock.unlock(); + } + return; + } + + // Close tls startResponse if used + if (tls != null) { + try { + tls.close(); + } catch (IOException e) { + containerLog.error(sm.getString("jndiRealm.tlsClose"), e); + } + } + // Close our opened connection + try { + if (containerLog.isTraceEnabled()) { + containerLog.trace("Closing directory context"); + } + connection.context.close(); + } catch (NamingException e) { + containerLog.error(sm.getString("jndiRealm.close"), e); + } + connection.context = null; + // The lock will be reacquired before any manipulation of the connection + if (connectionPool == null) { + singleConnectionLock.unlock(); + } + } + + + /** + * Close all pooled connections. + */ + protected void closePooledConnections() { + if (connectionPool != null) { + // Close any pooled connections as they might be bad as well + synchronized (connectionPool) { + JNDIConnection connection = null; + while ((connection = connectionPool.pop()) != null) { + close(connection); + } + } + } + } + + + /** + * Get the password for the specified user. + * + * @param username The user name + * + * @return the password associated with the given principal's user name. + */ + @Override + protected String getPassword(String username) { + String userPassword = getUserPassword(); + if (userPassword == null || userPassword.isEmpty()) { + return null; + } + + JNDIConnection connection = null; + User user = null; + try { + // Ensure that we have a directory context available + connection = get(); + + // Occasionally the directory context will timeout. Try one more + // time before giving up. + try { + user = getUser(connection, username, null); + } catch (NullPointerException | NamingException e) { + // log the exception so we know it's there. + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); + + // close the connection so we know it will be reopened. + close(connection); + closePooledConnections(); + + // open a new directory context. + connection = get(); + + // Try the authentication again. + user = getUser(connection, username, null); + } + + // Release this context + release(connection); + + if (user == null) { + // User should be found... + return null; + } else { + // ... and have a password + return user.getPassword(); + } + } catch (Exception e) { + // Log the problem for posterity + containerLog.error(sm.getString("jndiRealm.exception"), e); + // close the connection so we know it will be reopened. + close(connection); + closePooledConnections(); + return null; + } + } + + + /** + * Get the principal associated with the specified certificate. + * + * @param username The user name + * + * @return the Principal associated with the given certificate. + */ + @Override + protected Principal getPrincipal(String username) { + return getPrincipal(username, null); + } + + + @Override + protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) { + String name = gssName.toString(); + + if (isStripRealmForGss()) { + int i = name.indexOf('@'); + if (i > 0) { + // Zero so we don't leave a zero length name + name = name.substring(0, i); + } + } + + return getPrincipal(name, gssCredential); + } + + + protected Principal getPrincipal(String username, GSSCredential gssCredential) { + + JNDIConnection connection = null; + Principal principal = null; + + try { + // Ensure that we have a directory context available + connection = get(); + + // Occasionally the directory context will timeout. Try one more + // time before giving up. + try { + + // Authenticate the specified username if possible + principal = getPrincipal(connection, username, gssCredential); + + } catch (NamingException e) { + /* + * While we would like to catch specialized exceptions like CommunicationException and + * ServiceUnavailableException, some network communication problems are reported as this general + * exception. This is fixed in Java 18 by https://bugs.openjdk.org/browse/JDK-8273402 + */ + // log the exception so we know it's there. + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); + + // close the connection so we know it will be reopened. + close(connection); + closePooledConnections(); + + // open a new directory context. + connection = get(); + + // Try the authentication again. + principal = getPrincipal(connection, username, gssCredential); + } + + // Release this context + release(connection); + + // Return the authenticated Principal (if any) + return principal; + + } catch (Exception e) { + // Log the problem for posterity + containerLog.error(sm.getString("jndiRealm.exception"), e); + + // close the connection so we know it will be reopened. + close(connection); + closePooledConnections(); + + // Return "not authenticated" for this request + return null; + } + } + + + /** + * Get the principal associated with the specified certificate. + * + * @param connection The directory context + * @param username The user name + * @param gssCredential The credentials + * + * @return the Principal associated with the given certificate. + * + * @exception NamingException if a directory server error occurs + */ + protected Principal getPrincipal(JNDIConnection connection, String username, GSSCredential gssCredential) + throws NamingException { + + User user = null; + List roles = null; + Hashtable preservedEnvironment = null; + DirContext context = connection.context; + + try { + if (gssCredential != null && isUseDelegatedCredential()) { + // Preserve the current context environment parameters + preservedEnvironment = context.getEnvironment(); + // Set up context + context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + context.addToEnvironment("javax.security.sasl.server.authentication", "true"); + context.addToEnvironment("javax.security.sasl.qop", spnegoDelegationQop); + // Note: Subject already set in SPNEGO authenticator so no need + // for Subject.doAs() here + } + user = getUser(connection, username); + if (user != null) { + roles = getRoles(connection, user); + } + } finally { + if (gssCredential != null && isUseDelegatedCredential()) { + restoreEnvironmentParameter(context, Context.SECURITY_AUTHENTICATION, preservedEnvironment); + restoreEnvironmentParameter(context, "javax.security.sasl.server.authentication", preservedEnvironment); + restoreEnvironmentParameter(context, "javax.security.sasl.qop", preservedEnvironment); + } + } + + if (user != null) { + return new GenericPrincipal(user.getUserName(), roles, null, null, gssCredential, null); + } + + return null; + } + + + private void restoreEnvironmentParameter(DirContext context, String parameterName, + Hashtable preservedEnvironment) { + try { + context.removeFromEnvironment(parameterName); + if (preservedEnvironment != null && preservedEnvironment.containsKey(parameterName)) { + context.addToEnvironment(parameterName, preservedEnvironment.get(parameterName)); + } + } catch (NamingException e) { + // Ignore + } + } + + + /** + * Open (if necessary) and return a connection to the configured directory server for this Realm. + * + * @return the connection + * + * @exception NamingException if a directory server error occurs + */ + protected JNDIConnection get() throws NamingException { + JNDIConnection connection = null; + // Use the pool if available, otherwise use the single connection + if (connectionPool != null) { + connection = connectionPool.pop(); + if (connection == null) { + connection = create(); + } + } else { + singleConnectionLock.lock(); + if (singleConnection == null) { + singleConnection = create(); + } + connection = singleConnection; + } + if (connection.context == null) { + open(connection); + } + return connection; + } + + + /** + * Release our use of this connection so that it can be recycled. + * + * @param connection The directory context to release + */ + protected void release(JNDIConnection connection) { + if (connectionPool != null) { + if (connection != null) { + if (!connectionPool.push(connection)) { + // Any connection that doesn't end back to the pool must be closed + close(connection); + } + } + } else { + singleConnectionLock.unlock(); + } + } + + + /** + * Create a new connection wrapper, along with the message formats. + * + * @return the new connection + */ + protected JNDIConnection create() { + return new JNDIConnection(userSearch, userPatternArray, roleBase, roleSearch); + } + + + /** + * Create a new connection to the directory server. + * + * @param connection The directory server connection wrapper + * + * @throws NamingException if a directory server error occurs + */ + protected void open(JNDIConnection connection) throws NamingException { + try { + // Ensure that we have a directory context available + connection.context = createDirContext(getDirectoryContextEnvironment()); + } catch (Exception e) { + if (alternateURL == null || alternateURL.length() == 0) { + // No alternate URL. Re-throw the exception. + throw e; + } + connectionAttempt = 1; + // log the first exception. + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); + // Try connecting to the alternate url. + connection.context = createDirContext(getDirectoryContextEnvironment()); + } finally { + // reset it in case the connection times out. + // the primary may come back. + connectionAttempt = 0; + } + } + + + @Override + public boolean isAvailable() { + // Simple best effort check + return (connectionPool != null || singleConnection.context != null); + } + + + private DirContext createDirContext(Hashtable env) throws NamingException { + if (useStartTls) { + return createTlsDirContext(env); + } else { + return new InitialDirContext(env); + } + } + + + private SSLSocketFactory getSSLSocketFactory() { + if (sslSocketFactory != null) { + return sslSocketFactory; + } + final SSLSocketFactory result; + if (this.sslSocketFactoryClassName != null && !sslSocketFactoryClassName.trim().equals("")) { + result = createSSLSocketFactoryFromClassName(this.sslSocketFactoryClassName); + } else { + result = createSSLContextFactoryFromProtocol(sslProtocol); + } + this.sslSocketFactory = result; + return result; + } + + + private SSLSocketFactory createSSLSocketFactoryFromClassName(String className) { + try { + Object o = constructInstance(className); + if (o instanceof SSLSocketFactory) { + return sslSocketFactory; + } else { + throw new IllegalArgumentException(sm.getString("jndiRealm.invalidSslSocketFactory", className)); + } + } catch (ReflectiveOperationException | SecurityException e) { + throw new IllegalArgumentException(sm.getString("jndiRealm.invalidSslSocketFactory", className), e); + } + } + + + private SSLSocketFactory createSSLContextFactoryFromProtocol(String protocol) { + try { + SSLContext sslContext; + if (protocol != null) { + sslContext = SSLContext.getInstance(protocol); + sslContext.init(null, null, null); + } else { + sslContext = SSLContext.getDefault(); + } + return sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + List allowedProtocols = Arrays.asList(getSupportedSslProtocols()); + throw new IllegalArgumentException(sm.getString("jndiRealm.invalidSslProtocol", protocol, allowedProtocols), + e); + } + } + + + /** + * Create a tls enabled LdapContext and set the StartTlsResponse tls instance variable. + * + * @param env Environment to use for context creation + * + * @return configured {@link LdapContext} + * + * @throws NamingException when something goes wrong while negotiating the connection + */ + private DirContext createTlsDirContext(Hashtable env) throws NamingException { + Map savedEnv = new HashMap<>(); + for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, Context.SECURITY_CREDENTIALS, + Context.SECURITY_PRINCIPAL, Context.SECURITY_PROTOCOL)) { + Object entry = env.remove(key); + if (entry != null) { + savedEnv.put(key, entry); + } + } + LdapContext result = null; + try { + result = new InitialLdapContext(env, null); + tls = (StartTlsResponse) result.extendedOperation(new StartTlsRequest()); + if (getHostnameVerifier() != null) { + tls.setHostnameVerifier(getHostnameVerifier()); + } + if (getCipherSuitesArray() != null) { + tls.setEnabledCipherSuites(getCipherSuitesArray()); + } + try { + SSLSession negotiate = tls.negotiate(getSSLSocketFactory()); + containerLog.debug(sm.getString("jndiRealm.negotiatedTls", negotiate.getProtocol())); + } catch (IOException e) { + throw new NamingException(e.getMessage()); + } + } finally { + if (result != null) { + for (Map.Entry savedEntry : savedEnv.entrySet()) { + result.addToEnvironment(savedEntry.getKey(), savedEntry.getValue()); + } + } + } + return result; + } + + + /** + * Create our directory context configuration. + * + * @return java.util.Hashtable the configuration for the directory context. + */ + protected Hashtable getDirectoryContextEnvironment() { + + Hashtable env = new Hashtable<>(); + + // Configure our directory context environment. + if (containerLog.isTraceEnabled() && connectionAttempt == 0) { + containerLog.trace("Connecting to URL " + connectionURL); + } else if (containerLog.isTraceEnabled() && connectionAttempt > 0) { + containerLog.trace("Connecting to URL " + alternateURL); + } + env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory); + if (connectionName != null) { + env.put(Context.SECURITY_PRINCIPAL, connectionName); + } + if (connectionPassword != null) { + env.put(Context.SECURITY_CREDENTIALS, connectionPassword); + } + if (connectionURL != null && connectionAttempt == 0) { + env.put(Context.PROVIDER_URL, connectionURL); + } else if (alternateURL != null && connectionAttempt > 0) { + env.put(Context.PROVIDER_URL, alternateURL); + } + if (authentication != null) { + env.put(Context.SECURITY_AUTHENTICATION, authentication); + } + if (protocol != null) { + env.put(Context.SECURITY_PROTOCOL, protocol); + } + if (referrals != null) { + env.put(Context.REFERRAL, referrals); + } + if (derefAliases != null) { + env.put(DEREF_ALIASES, derefAliases); + } + if (connectionTimeout != null) { + env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout); + } + if (readTimeout != null) { + env.put("com.sun.jndi.ldap.read.timeout", readTimeout); + } + + return env; + } + + + // ------------------------------------------------------ Lifecycle Methods + + /** + * Prepare for the beginning of active use of the public methods of this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + if (connectionPoolSize != 1) { + connectionPool = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, connectionPoolSize); + } + + // Check to see if the connection to the directory can be opened + ClassLoader ocl = null; + Thread currentThread = null; + JNDIConnection connection = null; + try { + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65553 + // This can move back to open() once it is known that Tomcat must be + // running on a JVM that includes a fix for + // https://bugs.openjdk.java.net/browse/JDK-8273874 + if (!isUseContextClassLoader()) { + currentThread = Thread.currentThread(); + ocl = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + connection = get(); + } catch (NamingException e) { + // A failure here is not fatal as the directory may be unavailable + // now but available later. Unavailability of the directory is not + // fatal once the Realm has started so there is no reason for it to + // be fatal when the Realm starts. + containerLog.error(sm.getString("jndiRealm.open"), e); + } finally { + release(connection); + if (currentThread != null) { + currentThread.setContextClassLoader(ocl); + } + } + + super.startInternal(); + } + + + /** + * Gracefully terminate the active use of the public methods of this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that needs to be reported + */ + @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + // Close any open directory server connection + if (connectionPool == null) { + singleConnectionLock.lock(); + close(singleConnection); + } else { + closePooledConnections(); + connectionPool = null; + } + } + + + /** + * Given a string containing LDAP patterns for user locations (separated by parentheses in a pseudo-LDAP search + * string format - "(location1)(location2)", returns an array of those paths. Real LDAP search strings are supported + * as well (though only the "|" "OR" type). + * + * @param userPatternString - a string LDAP search paths surrounded by parentheses + * + * @return a parsed string array + */ + protected String[] parseUserPatternString(String userPatternString) { + + if (userPatternString != null) { + List pathList = new ArrayList<>(); + int startParenLoc = userPatternString.indexOf('('); + if (startParenLoc == -1) { + // no parens here; return whole thing + return new String[] { userPatternString }; + } + int startingPoint = 0; + while (startParenLoc > -1) { + int endParenLoc = 0; + // weed out escaped open parens and parens enclosing the + // whole statement (in the case of valid LDAP search + // strings: (|(something)(somethingelse)) + while ((userPatternString.charAt(startParenLoc + 1) == '|') || + (startParenLoc != 0 && userPatternString.charAt(startParenLoc - 1) == '\\')) { + startParenLoc = userPatternString.indexOf('(', startParenLoc + 1); + } + endParenLoc = userPatternString.indexOf(')', startParenLoc + 1); + // weed out escaped end-parens + while (userPatternString.charAt(endParenLoc - 1) == '\\') { + endParenLoc = userPatternString.indexOf(')', endParenLoc + 1); + } + String nextPathPart = userPatternString.substring(startParenLoc + 1, endParenLoc); + pathList.add(nextPathPart); + startingPoint = endParenLoc + 1; + startParenLoc = userPatternString.indexOf('(', startingPoint); + } + return pathList.toArray(new String[0]); + } + return null; + } + + + /** + * Given an LDAP search string, returns the string with certain characters escaped according to RFC 2254 guidelines. + * The character mapping is as follows: char -> Replacement --------------------------- * -> \2a ( -> \28 ) + * -> \29 \ -> \5c \0 -> \00 + * + * @param inString string to escape according to RFC 2254 guidelines + * + * @return String the escaped/encoded result + */ + protected String doFilterEscaping(String inString) { + if (inString == null) { + return null; + } + StringBuilder buf = new StringBuilder(inString.length()); + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); + switch (c) { + case '\\': + buf.append("\\5c"); + break; + case '*': + buf.append("\\2a"); + break; + case '(': + buf.append("\\28"); + break; + case ')': + buf.append("\\29"); + break; + case '\0': + buf.append("\\00"); + break; + default: + buf.append(c); + break; + } + } + return buf.toString(); + } + + + /** + * Returns the distinguished name of a search result. + * + * @param context Our DirContext + * @param base The base DN + * @param result The search result + * + * @return String containing the distinguished name + * + * @exception NamingException if a directory server error occurs + */ + protected String getDistinguishedName(DirContext context, String base, SearchResult result) throws NamingException { + // Get the entry's distinguished name. For relative results, this means + // we need to composite a name with the base name, the context name, and + // the result name. For non-relative names, use the returned name. + String resultName = result.getName(); + Name name; + if (result.isRelative()) { + if (containerLog.isTraceEnabled()) { + containerLog.trace(" search returned relative name: " + resultName); + } + NameParser parser = context.getNameParser(""); + Name contextName = parser.parse(context.getNameInNamespace()); + Name baseName = parser.parse(base); + + // Bugzilla 32269 + Name entryName = parser.parse(new CompositeName(resultName).get(0)); + + name = contextName.addAll(baseName); + name = name.addAll(entryName); + } else { + if (containerLog.isTraceEnabled()) { + containerLog.trace(" search returned absolute name: " + resultName); + } + try { + // Normalize the name by running it through the name parser. + NameParser parser = context.getNameParser(""); + URI userNameUri = new URI(resultName); + String pathComponent = userNameUri.getPath(); + // Should not ever have an empty path component, since that is /{DN} + if (pathComponent.length() < 1) { + throw new InvalidNameException(sm.getString("jndiRealm.invalidName", resultName)); + } + name = parser.parse(pathComponent.substring(1)); + } catch (URISyntaxException e) { + throw new InvalidNameException(sm.getString("jndiRealm.invalidName", resultName)); + } + } + + if (getForceDnHexEscape()) { + // Bug 63026 + return convertToHexEscape(name.toString()); + } else { + return name.toString(); + } + } + + + /** + * Implements the necessary escaping to represent an attribute value as a String as per RFC 4514. + * + * @param input The original attribute value + * + * @return The string representation of the attribute value + */ + protected String doAttributeValueEscaping(String input) { + if (input == null) { + return null; + } + int len = input.length(); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < len; i++) { + char c = input.charAt(i); + switch (c) { + case ' ': { + if (i == 0 || i == (len - 1)) { + result.append("\\20"); + } else { + result.append(c); + } + break; + } + case '#': { + if (i == 0) { + result.append("\\23"); + } else { + result.append(c); + } + break; + } + case '\"': { + result.append("\\22"); + break; + } + case '+': { + result.append("\\2B"); + break; + } + case ',': { + result.append("\\2C"); + break; + } + case ';': { + result.append("\\3B"); + break; + } + case '<': { + result.append("\\3C"); + break; + } + case '>': { + result.append("\\3E"); + break; + } + case '\\': { + result.append("\\5C"); + break; + } + case '\u0000': { + result.append("\\00"); + break; + } + default: + result.append(c); + } + + } + + return result.toString(); + } + + + protected static String convertToHexEscape(String input) { + if (input.indexOf('\\') == -1) { + // No escaping present. Return original. + return input; + } + + // +6 allows for 3 escaped characters by default + StringBuilder result = new StringBuilder(input.length() + 6); + boolean previousSlash = false; + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + + if (previousSlash) { + switch (c) { + case ' ': { + result.append("\\20"); + break; + } + case '\"': { + result.append("\\22"); + break; + } + case '#': { + result.append("\\23"); + break; + } + case '+': { + result.append("\\2B"); + break; + } + case ',': { + result.append("\\2C"); + break; + } + case ';': { + result.append("\\3B"); + break; + } + case '<': { + result.append("\\3C"); + break; + } + case '=': { + result.append("\\3D"); + break; + } + case '>': { + result.append("\\3E"); + break; + } + case '\\': { + result.append("\\5C"); + break; + } + default: + result.append('\\'); + result.append(c); + } + previousSlash = false; + } else { + if (c == '\\') { + previousSlash = true; + } else { + result.append(c); + } + } + } + + if (previousSlash) { + result.append('\\'); + } + + return result.toString(); + } + + + // ------------------------------------------------------ Protected Classes + + /** + * A protected class representing a User + */ + protected static class User { + + private final String username; + private final String dn; + private final String password; + private final List roles; + private final String userRoleId; + + public User(String username, String dn, String password, List roles, String userRoleId) { + this.username = username; + this.dn = dn; + this.password = password; + if (roles == null) { + this.roles = Collections.emptyList(); + } else { + this.roles = Collections.unmodifiableList(roles); + } + this.userRoleId = userRoleId; + } + + public String getUserName() { + return username; + } + + public String getDN() { + return dn; + } + + public String getPassword() { + return password; + } + + public List getRoles() { + return roles; + } + + public String getUserRoleId() { + return userRoleId; + } + } + + + /** + * Class holding the connection to the directory plus the associated non thread safe message formats. + */ + protected static class JNDIConnection { + + /** + * The MessageFormat object associated with the current userSearch. + */ + public final MessageFormat userSearchFormat; + + /** + * An array of MessageFormat objects associated with the current userPatternArray. + */ + public final MessageFormat[] userPatternFormatArray; + + /** + * The MessageFormat object associated with the current roleBase. + */ + public final MessageFormat roleBaseFormat; + + /** + * The MessageFormat object associated with the current roleSearch. + */ + public final MessageFormat roleFormat; + + /** + * The directory context linking us to our directory server. + */ + public volatile DirContext context = null; + + + public JNDIConnection(String userSearch, String[] userPatternArray, String roleBase, String roleSearch) { + if (userSearch == null) { + userSearchFormat = null; + } else { + userSearchFormat = new MessageFormat(userSearch); + } + + if (userPatternArray == null) { + userPatternFormatArray = null; + } else { + int len = userPatternArray.length; + userPatternFormatArray = new MessageFormat[len]; + for (int i = 0; i < len; i++) { + userPatternFormatArray[i] = new MessageFormat(userPatternArray[i]); + } + } + + if (roleBase == null) { + roleBaseFormat = null; + } else { + roleBaseFormat = new MessageFormat(roleBase); + } + + if (roleSearch == null) { + roleFormat = null; + } else { + roleFormat = new MessageFormat(roleSearch); + } + } + } +} diff --git a/java/org/apache/catalina/realm/LocalStrings.properties b/java/org/apache/catalina/realm/LocalStrings.properties new file mode 100644 index 0000000..c72c39a --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings.properties @@ -0,0 +1,118 @@ +# 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. + +combinedRealm.addRealm=Add [{0}] realm, making a total of [{1}] realms +combinedRealm.authFail=Failed to authenticate user [{0}] with realm [{1}] +combinedRealm.authStart=Attempting to authenticate user [{0}] with realm [{1}] +combinedRealm.authSuccess=Authenticated user [{0}] with realm [{1}] +combinedRealm.getPassword=The getPassword() method should never be called +combinedRealm.getPrincipal=The getPrincipal() method should never be called +combinedRealm.realmStartFail=Failed to start [{0}] realm +combinedRealm.setCredentialHandler=A CredentialHandler was set on an instance of the CombinedRealm (or a sub-class of CombinedRealm). CombinedRealm doesn't use a configured CredentialHandler. Is this a configuration error? +combinedRealm.unexpectedMethod=An unexpected call was made to a method on the combined realm + +credentialHandler.invalidStoredCredential=The invalid stored credential string [{0}] was provided by the Realm to match with the user provided credentials + +dataSourceRealm.authenticateFailure=Username [{0}] NOT successfully authenticated +dataSourceRealm.authenticateSuccess=Username [{0}] successfully authenticated +dataSourceRealm.close=Exception closing database connection +dataSourceRealm.commit=Exception committing connection before closing +dataSourceRealm.exception=Exception performing authentication +dataSourceRealm.getPassword.exception=Exception retrieving password for [{0}] +dataSourceRealm.getRoles.exception=Exception retrieving roles for [{0}] +dataSourceRealm.noNamingContext=Cannot access the server to retrieve the naming context + +jaasCallback.username=Returned username [{0}] + +jaasMemoryLoginModule.callbackHandlerError=Error invoking callback handler: [{0}] +jaasMemoryLoginModule.invalidCredentials=User name or password is incorrect +jaasMemoryLoginModule.login=Login for user [{0}] as principal [{1}] +jaasMemoryLoginModule.noCallbackHandler=No CallbackHandler specified +jaasMemoryLoginModule.noCatalinaBase=Unable to determine Catalina base to load file [{0}] +jaasMemoryLoginModule.noConfig=Cannot load configuration file [{0}] +jaasMemoryLoginModule.parseError=Error processing configuration file [{0}] +jaasMemoryLoginModule.unknownAuthenticationMethod=Unknown authentication method + +jaasRealm.accountExpired=Username [{0}] NOT authenticated due to expired account +jaasRealm.appName=Set JAAS app name [{0}] +jaasRealm.authenticateFailure=Username [{0}] NOT successfully authenticated +jaasRealm.authenticateSuccess=Username [{0}] successfully authenticated as Principal [{1}] -- Subject was created too +jaasRealm.beginLogin=JAASRealm login requested for username [{0}] using LoginContext for application [{1}] +jaasRealm.checkPrincipal=Checking Principal [{0}] [{1}] +jaasRealm.classNotFound=Class [{0}] was not found +jaasRealm.credentialExpired=Username [{0}] NOT authenticated due to expired credential +jaasRealm.failedLogin=Username [{0}] NOT authenticated due to failed login +jaasRealm.loginContextCreated=JAAS LoginContext created for username [{0}] +jaasRealm.loginException=Login exception authenticating username [{0}] +jaasRealm.notPrincipal=Class [{0}] not added as it does not implement java.security.Principal +jaasRealm.rolePrincipalAdd=Adding role Principal [{0}] to this user Principal''s roles +jaasRealm.rolePrincipalFailure=No valid role Principals found. +jaasRealm.unexpectedError=Unexpected error +jaasRealm.userPrincipalFailure=No valid user Principal found +jaasRealm.userPrincipalSuccess=Principal [{0}] is a valid user class. We will use this as the user Principal. + +jndiRealm.authenticateFailure=Username [{0}] NOT successfully authenticated +jndiRealm.authenticateSuccess=Username [{0}] successfully authenticated +jndiRealm.cipherSuites=Enable [{0}] as cipher suites for tls connection. +jndiRealm.close=Exception closing directory server connection +jndiRealm.emptyCipherSuites=Empty String for cipher suites given. Using default cipher suites. +jndiRealm.exception=Exception performing authentication +jndiRealm.exception.retry=Exception performing authentication. Retrying... +jndiRealm.invalidHostnameVerifier=[{0}] not a valid class name for a HostnameVerifier +jndiRealm.invalidName=Search returned unparsable absolute name: [{0}] +jndiRealm.invalidSslProtocol=Given protocol [{0}] is invalid. It has to be one of [{1}] +jndiRealm.invalidSslSocketFactory=[{0}] not a valid class name for an SSLSocketFactory +jndiRealm.multipleEntries=User name [{0}] has multiple entries +jndiRealm.negotiatedTls=Negotiated tls connection using protocol [{0}] +jndiRealm.open=Exception opening directory server connection +jndiRealm.tlsClose=Exception closing tls response + +lockOutRealm.authLockedUser=An attempt was made to authenticate the locked user [{0}] +lockOutRealm.removeWarning=User [{0}] was removed from the failed users cache after [{1}] seconds to keep the cache size within the limit set + +mdCredentialHandler.unknownEncoding=The encoding [{0}] is not supported so the current setting of [{1}] will still be used + +memoryRealm.authenticateFailure=Username [{0}] NOT successfully authenticated +memoryRealm.authenticateSuccess=Username [{0}] successfully authenticated +memoryRealm.loadExist=Memory database file [{0}] cannot be read +memoryRealm.loadPath=Loading users from memory database file [{0}] +memoryRealm.readXml=Exception while reading memory database file +memoryRealm.xmlFeatureEncoding=Exception configuring digester to permit java encoding names in XML files. Only IANA encoding names will be supported. + +pbeCredentialHandler.invalidKeySpec=Unable to generate a password based key + +realmBase.algorithm=Invalid message digest algorithm [{0}] specified +realmBase.authenticateFailure=Username [{0}] NOT successfully authenticated +realmBase.authenticateSuccess=Username [{0}] successfully authenticated +realmBase.cannotGetRoles=Cannot get roles from principal [{0}] +realmBase.createUsernameRetriever.ClassCastException=Class [{0}] is not an X509UsernameRetriever. +realmBase.createUsernameRetriever.newInstance=Cannot create object of type [{0}]. +realmBase.credentialNotDelegated=Credential for user [{0}] has not been delegated though storing was requested +realmBase.delegatedCredentialFail=Unable to obtain delegated credential for user [{0}] +realmBase.digest=Error digesting user credentials +realmBase.digestMismatch=Unable to authenticate user as DIGEST authentication used [{0}] but password was stored in Realm using [{1}] +realmBase.forbidden=Access to the requested resource has been denied +realmBase.gotX509Username=Got user name from X509 certificate: [{0}] +realmBase.gssContextNotEstablished=Authenticator implementation error: the passed security context is not fully established +realmBase.gssNameFail=Failed to extract name from established GSSContext +realmBase.hasRoleFailure=Username [{0}] does NOT have role [{1}] +realmBase.hasRoleSuccess=Username [{0}] has role [{1}] +realmBase.invalidDigestEncoding=Invalid digest encoding [{0}] +realmBase.unknownAllRolesMode=Unknown mode [{0}], must be one of: strict, authOnly, strictAuthOnly +realmBase.validity=Error checking certificate validity + +userDatabaseRealm.lookup=Exception looking up UserDatabase under key [{0}] +userDatabaseRealm.noDatabase=No UserDatabase component found under key [{0}] +userDatabaseRealm.noNamingContext=Cannot access the server to retrieve the naming context diff --git a/java/org/apache/catalina/realm/LocalStrings_cs.properties b/java/org/apache/catalina/realm/LocalStrings_cs.properties new file mode 100644 index 0000000..14a9d58 --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_cs.properties @@ -0,0 +1,31 @@ +# 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. + +combinedRealm.authFail=Selhala autentikaze uživatele [{0}] pro realm [{1}] +combinedRealm.authSuccess=Autentizovaný uživatel [{0}] s REALMem [{1}] + +dataSourceRealm.getPassword.exception=Výjima získán hesla pro [{0}] +dataSourceRealm.getRoles.exception=Chyba naÄtení rolí pro [{0}] + +jaasRealm.loginException=Výjimka pÅ™ihlášení autentizace uživatele [{0}] + +lockOutRealm.authLockedUser=Byl uÄinÄ›n pokus autentizovat uzamknutého uživatele [{0}] + +memoryRealm.loadExist=Soubor databáze v pamÄ›ti [{0}] nelze pÅ™eÄíst + +realmBase.authenticateFailure=Uživatel [{0}] nebyl úspěšnÄ› autentizován +realmBase.createUsernameRetriever.ClassCastException=Třída [{0}] není X509UsernameRetriever. +realmBase.digest=Chyba vyhodnocení uživatelských pÅ™ihlášovacích údajů +realmBase.forbidden=Přístup k požadovanému zdroji byl zamítnut diff --git a/java/org/apache/catalina/realm/LocalStrings_de.properties b/java/org/apache/catalina/realm/LocalStrings_de.properties new file mode 100644 index 0000000..8d51653 --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_de.properties @@ -0,0 +1,48 @@ +# 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. + +combinedRealm.authFail=Kann Benutzer [{0}] mit Realm [{1}] nicht authentisieren +combinedRealm.authSuccess=Authentifizierter Benutzer [{0}] mit Realm [{1}] + +dataSourceRealm.authenticateFailure=Benutzername [{0}] konnte NICHT authentifiziert werden +dataSourceRealm.authenticateSuccess=Benutzername [{0}] konnte erfolgreich authentifiziert werden +dataSourceRealm.getPassword.exception=Ausnahme beim Holen des Passwortes für [{0}] +dataSourceRealm.getRoles.exception=Ausnahme beim Holen der Rollen für [{0}] + +jaasRealm.accountExpired=Benutzername [{0}] konnte auf Grund eines abgelaufenen Kontos NICHT authentifiziert werden +jaasRealm.authenticateFailure=Benutzername [{0}] konnte NICHT authentifiziert werden +jaasRealm.credentialExpired=Benutzername [{0}] konnte auf Grund abgelaufener Zugangsdaten NICHT authentifiziert werden +jaasRealm.failedLogin=Benutzername [{0}] konnte auf Grund einer fehlerhaften Anmeldung NICHT authentifiziert werden +jaasRealm.loginContextCreated=JAAS LoginContext für Benutzername [{0}] erzeugt + +jndiRealm.authenticateFailure=Benutzername [{0}] konnte NICHT authentifiziert werden +jndiRealm.authenticateSuccess=Benutzername [{0}] konnte erfolgreich authentifiziert werden + +lockOutRealm.authLockedUser=Es wurde versucht den gesperrten Benutzer [{0}] zu authentifizieren + +memoryRealm.authenticateFailure=Benutzername [{0}] konnte NICHT authentifiziert werden +memoryRealm.authenticateSuccess=Benutzername [{0}] konnte erfolgreich authentifiziert werden.\n +memoryRealm.loadExist=Datei [{0}] für Memory Database kann nicht gelesen werden + +realmBase.authenticateFailure=Benutzername [{0}] konnte NICHT authentifiziert werden +realmBase.authenticateSuccess=Benutzername [{0}] konnte erfolgreich authentifiziert werden +realmBase.createUsernameRetriever.ClassCastException=Klasse [{0}] ist keine X509UsernameRetriever Klasse. +realmBase.digest=Fehler beim Anwenden des Digest auf die Benutzer-Credentials +realmBase.forbidden=Zugriff auf die gewünschte Resource wurde verweigert. +realmBase.gotX509Username=Benutzername aus dem X.509 Zertifikate extrahiert: [{0}] +realmBase.hasRoleFailure=Benutzername [{0}] hat NICHT die Rolle [{1}] +realmBase.hasRoleSuccess=Benutzername [{0}] hat die Rolle [{1}] + +userDatabaseRealm.noDatabase=Keine UserDatabase Komponente unter dem Schlüssel [{0}] gefunden diff --git a/java/org/apache/catalina/realm/LocalStrings_es.properties b/java/org/apache/catalina/realm/LocalStrings_es.properties new file mode 100644 index 0000000..c915fee --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_es.properties @@ -0,0 +1,77 @@ +# 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. + +combinedRealm.addRealm=Añadir reino [{0}], totalizando [{1}] reinos +combinedRealm.authFail=No pude autenticar al usuario [{0}] con el reino [{1}] +combinedRealm.authStart=Intentando autentica al usuario [{0}] con el reino [{1}] +combinedRealm.authSuccess=Usuario autenticado [{0}] con reino [{1}] +combinedRealm.getPassword=No se debería de llamar nunca al método getPassword() +combinedRealm.getPrincipal=No se debería de llamar nunca al método getPrincipal() +combinedRealm.realmStartFail=No pude arrancar reino [{0}] +combinedRealm.unexpectedMethod=Una llamada inesperada se realizó a un método del reino combinado + +dataSourceRealm.authenticateFailure=Nombre de usuario [{0}] NO autenticado con éxito +dataSourceRealm.authenticateSuccess=Nombre de usuario [{0}] autenticado con éxito +dataSourceRealm.close=Excepción cerrando conexión a base de datos +dataSourceRealm.exception=Excepción realizando autenticación +dataSourceRealm.getPassword.exception=Excepción recuperando contraseña para [{0}] +dataSourceRealm.getRoles.exception=Excepción recuperando papeles para [{0}] + +jaasCallback.username=Devuelto nombre de usuario [{0}] + +jaasRealm.accountExpired=El usuario [{0}] NO ha sido autentificado porque ha expirado su cuenta +jaasRealm.authenticateFailure=Nombre de usuario [{0}] NO autenticado con éxito +jaasRealm.authenticateSuccess=Nombre de usuario [{0}] autenticado con éxito como Principal [{1}] -- También se ha creado el Asunto +jaasRealm.beginLogin=JAASRealm login requerido para nombre de usuario [{0}] usando LoginContext para aplicación [{1}] +jaasRealm.checkPrincipal=Revisando Principal [{0}] [{1}] +jaasRealm.credentialExpired=El usuario [{0}] NO ha sido autentificado porque ha expirado su credencial +jaasRealm.failedLogin=El usuario [{0}] NO ha sido autentificado porque ha fallado el login +jaasRealm.loginContextCreated=JAAS LoginContext creado para nombre de usuario [{0}] +jaasRealm.loginException=Excepción de Login autenticando nombre de usuario [{0}] +jaasRealm.rolePrincipalAdd=Añadiend papel Principal [{0}] a los papeles de este usuario Principal +jaasRealm.rolePrincipalFailure=No se han hallado papeles de Principal válidos. +jaasRealm.unexpectedError=Error inesperado +jaasRealm.userPrincipalFailure=No se ha hallado usuario Principal +jaasRealm.userPrincipalSuccess=El Principal [{0}] es una clase válida de usuario. La vamos a usar como usuario Principal. + +jndiRealm.authenticateFailure=Autentificación fallida para el usuario [{0}] +jndiRealm.authenticateSuccess=Autentificación correcta para el usuario [{0}] +jndiRealm.close=Excepción al cerrar la conexión al servidor de directorio +jndiRealm.exception=Excepción al realizar la autentificación +jndiRealm.open=Excepción al abrir la conexión al servidor de directorio + +lockOutRealm.authLockedUser=Se ha intentado autenticar al usuario bloqueado [{0}] +lockOutRealm.removeWarning=Se ha quitado al usuario [{0}] de la caché de usuarios fallidos tras [{1}] secgundos para mantener la medida de caché dentro de los límites + +memoryRealm.authenticateFailure=Autentificación fallida para el usuario [{0}] +memoryRealm.authenticateSuccess=Autentificación correcta para el usuario [{0}] +memoryRealm.loadExist=El fichero de usuarios [{0}] no ha podido ser leído +memoryRealm.loadPath=Cargando los usuarios desde el fichero [{0}] +memoryRealm.readXml=Excepción mientras se leía el fichero de usuarios +memoryRealm.xmlFeatureEncoding=Excepción al configurar resumidor para permitir nombres con codificación java en ficheros XML. Sóllo se soportan nombres con codificación IANA. + +realmBase.algorithm=El algoritmo resumen (digest) [{0}] es inválido +realmBase.authenticateFailure=Nombre de usuario [{0}] NO autenticado con éxito +realmBase.authenticateSuccess=Nombre de usuario [{0}] autenticado con éxito +realmBase.createUsernameRetriever.ClassCastException=La clase [{0}] no es una X509UsernameRetriever. +realmBase.delegatedCredentialFail=No pude obtener credenciales de delegado para el usuario [{0}] +realmBase.digest=Error procesando las credenciales del usuario +realmBase.forbidden=El acceso al recurso pedido ha sido denegado +realmBase.gssNameFail=No pude extraer el nombre desde el GSSContext establecido +realmBase.hasRoleFailure=El usuario [{0}] NO desempeña el papel de [{1}] +realmBase.hasRoleSuccess=El usuario [{0}] desempeña el papel de [{1}] + +userDatabaseRealm.lookup=Excepción buscando en Base de datos de Usuario mediante la clave [{0}] +userDatabaseRealm.noDatabase=No se ha hallado componente de Base de datos de Usuario mediante la clave [{0}] diff --git a/java/org/apache/catalina/realm/LocalStrings_fr.properties b/java/org/apache/catalina/realm/LocalStrings_fr.properties new file mode 100644 index 0000000..b24afbe --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_fr.properties @@ -0,0 +1,118 @@ +# 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. + +combinedRealm.addRealm=Ajout du royaume [{0}], [{1}] royaumes au total +combinedRealm.authFail=Echec d''authentification de l''utilisateur [{0}] avec le domaine [{1}] +combinedRealm.authStart=Tentative d''authentifier l''utilisateur [{0}] avec le royaume [{1}] +combinedRealm.authSuccess=Authentifié l''utilisateur [{0}] avec le domaine [{1}] +combinedRealm.getPassword=La méthode getPassword() ne doit jamais être appelée +combinedRealm.getPrincipal=La méthode getPrincipal() ne devrait jamais être appelée +combinedRealm.realmStartFail=Impossible de démarrer le royaume [{0}] +combinedRealm.setCredentialHandler=Un CredentialHandler a été défini sur une instance de CombinedRealm (ou une sous classe de CombinedRealm), et CombinedRealm ne supporte pas de CredentialHandler +combinedRealm.unexpectedMethod=Un appel de méthode inattendu à été effectué sur le royaumes combiné + +credentialHandler.invalidStoredCredential=Le royaume a fourni des identifiants [{0}] invalides à comparer avec ceux fournis par le client + +dataSourceRealm.authenticateFailure=Le nom d''utilisateur [{0}] n''a pas été authentifié +dataSourceRealm.authenticateSuccess=Le nom d''utilisateur [{0}] a été authentifié avec succès +dataSourceRealm.close=Exception lors de la fermeture de la connection vers la base de données +dataSourceRealm.commit=Exception lors du commit de la connection avant sa fermeture +dataSourceRealm.exception=Exception lors de l'anthentification +dataSourceRealm.getPassword.exception=Exception lors de la récupération du mot de passe pour [{0}] +dataSourceRealm.getRoles.exception=Exception lors de la récupération des rôles de [{0}] +dataSourceRealm.noNamingContext=Impossible d'accéder au serveur pour obtenir le contexte JNDI + +jaasCallback.username=Nom d''utilisateur renvoyé [{0}] + +jaasMemoryLoginModule.callbackHandlerError=Erreur lors de l''invocation du CallbackHandler : [{0}] +jaasMemoryLoginModule.invalidCredentials=Le nom d'utilisateur ou le mot de passe est incorrect +jaasMemoryLoginModule.login=Connection de l''utilisateur [{0}] comme principal [{1}] +jaasMemoryLoginModule.noCallbackHandler=Aucun CallbackHandler n'a été spécifié +jaasMemoryLoginModule.noCatalinaBase=Impossible de déterminer le répertoire de base de Catalina pour pouvoir charger le fichier [{0}] +jaasMemoryLoginModule.noConfig=Impossible de charger le fichier de configuration [{0}] +jaasMemoryLoginModule.parseError=Erreur de traitement du fichier de configuration [{0}] +jaasMemoryLoginModule.unknownAuthenticationMethod=Méthode d'authentification inconnue + +jaasRealm.accountExpired=le nom d''utilisateur [{0}] N''A PAS été authentifié car le compte a expiré +jaasRealm.appName=Définition du nom d''application JAAS comme [{0}] +jaasRealm.authenticateFailure=Le nom d''utilisateur [{0}] n''a pas été authentifié avec succès +jaasRealm.authenticateSuccess=le nom d''utilisateur [{0}] a été authentifié avec succès +jaasRealm.beginLogin=La connection avec le JAASRealm a demandé le nom d''utilisateur [{0}] en utilisant le LoginContext de l''application [{1}] +jaasRealm.checkPrincipal=Vérification du principal [{0}] [{1}] +jaasRealm.classNotFound=La classe [{0}] n''a pas été trouvée +jaasRealm.credentialExpired=le nom d''utilisateur [{0}] N''A PAS été authentifié car son crédit a expiré (expired credential) +jaasRealm.failedLogin=le nom d''utilisateur [{0}] N''A PAS été authentifié car son contrôle d''accès (login) a échoué +jaasRealm.loginContextCreated=Le LoginContext JAAS a été crée pour le nom d''utilisateur [{0}] +jaasRealm.loginException=Exception lors de l''authentification par login du nom d''utilisateur [{0}] +jaasRealm.notPrincipal=La classe [{0}] n''a pas été ajoutée car elle n''implémente pas java.security.Principal +jaasRealm.rolePrincipalAdd=Ajout du rôle Pincipal [{0}] aux rôles du principal de l''utilisateur +jaasRealm.rolePrincipalFailure=Aucun principal avec un rôle valide trouvé +jaasRealm.unexpectedError=Erreur inattendue +jaasRealm.userPrincipalFailure=Aucun principal valide trouvé +jaasRealm.userPrincipalSuccess=Le principal [{0}] est une classe utilisateur valide, elle sera utilisée comme principal de l''utilisateur + +jndiRealm.authenticateFailure=Le nom d''utilisateur [{0}] N''A PAS été authentifié +jndiRealm.authenticateSuccess=Le nom d''utilisateur [{0}] a été authentifié avec succès +jndiRealm.cipherSuites=La suite de chiffres [{0}] a été activée pour la connection TLS +jndiRealm.close=Exception lors de la fermeture de la connexion au serveur d'accès (directory server) +jndiRealm.emptyCipherSuites=Une chaîne vide est donnée comme suite de chiffres, la suite de chiffres par défaut sera utilisée +jndiRealm.exception=Exception pendant le traitement de l'authentification +jndiRealm.exception.retry=Erreur pendant l'authentification, nouvel essai +jndiRealm.invalidHostnameVerifier=[{0}] n''est pas un nom de classe valide pour un HostnameVerifier +jndiRealm.invalidName=La recherche a retourné un nom absolu qui ne peut pas être traité: [{0}] +jndiRealm.invalidSslProtocol=Le protocole fourni [{0}] est invalide, il doit être parmi [{1}] +jndiRealm.invalidSslSocketFactory=[{0}] n''est pas un nom de classe valide pour une SSLSocketFactory +jndiRealm.multipleEntries=Le nom d''utilisateur [{0}] a plusieurs entrées +jndiRealm.negotiatedTls=La connection TLS a été négociée en utilisant le protocole [{0}] +jndiRealm.open=Exception lors de l'ouverture de la connexion au serveur d'accès (directory server) +jndiRealm.tlsClose=Exception en fermant la réponse TLS + +lockOutRealm.authLockedUser=Une tentative d''authentification a été effectuée pour l''utilisateur verrouillé ("locked user") [{0}] +lockOutRealm.removeWarning=L''utilisateur [{0}] a été enlevé du cache des utilisateurs en échec après [{1}] secondes pour garder la taille du cache dans les limites définies + +mdCredentialHandler.unknownEncoding=L''encodage [{0}] n''est pas supporté donc la configuration actuelle [{1}] va continuer à être utilisée + +memoryRealm.authenticateFailure=le nom d''utilisateur [{0}] N''A PAS été authentifié +memoryRealm.authenticateSuccess=le nom d''utilisateur [{0}] a été authentifié avec succès +memoryRealm.loadExist=Le fichier base de données mémoire (memory database) [{0}] ne peut être lu +memoryRealm.loadPath=Chargement des utilisateurs depuis le fichier base de données mémoire (memory database) [{0}] +memoryRealm.readXml=Exception lors de la lecture du fichier base de données mémoire (memory database) +memoryRealm.xmlFeatureEncoding=Exception lors de la configuration du Digester pour permettre des noms d'encodage Java dans les fichiers XML, seuls le noms IANA seront supportés + +pbeCredentialHandler.invalidKeySpec=Impossible de générer une clé basée sur le mot de passe + +realmBase.algorithm=L''algorithme de hachage de message [{0}] indiqué est invalide +realmBase.authenticateFailure=Le nom d''utilisateur [{0}] N''A PAS été authentifié +realmBase.authenticateSuccess=Le nom d''utilisateur [{0}] a été authentifié avec succès +realmBase.cannotGetRoles=Impossible d''obtenir les rôles du principal [{0}] +realmBase.createUsernameRetriever.ClassCastException=La classe [{0}] n''est pas un X509UsernameRetriever. +realmBase.createUsernameRetriever.newInstance=Impossible de créer un objet du type [{0}] +realmBase.credentialNotDelegated=Les identifiants de l''utilisateur [{0}] n''ont pas été délégués alors que leur stockage a été requis +realmBase.delegatedCredentialFail=Impossible d''obtenir les identifiants délégués pour l''utilisateur [{0}] +realmBase.digest=Erreur lors du hachage de l''identifiant utilisateur +realmBase.digestMismatch=Impossible d''authentifier l''utilisateur car l''autentification DIGEST utilisait [{0}] alors que le mot de passe stocké dans le Realm utilisait [{1}] +realmBase.forbidden=L'accès à la ressource demandée a été interdit +realmBase.gotX509Username=Obtenu le nom d''utilisateur dans le certificat X509 : [{0}] +realmBase.gssContextNotEstablished=Erreur d'implémentation de l'authenticateur : le contexte de sécurité passé n'est pas complètement établi +realmBase.gssNameFail=Impossible d'extraire le nom du GSSContext qui a été établi +realmBase.hasRoleFailure=Le nom d''utilisateur [{0}] N''A PAS de rôle [{1}] +realmBase.hasRoleSuccess=Le nom d''utilisateur [{0}] a pour rôle [{1}] +realmBase.invalidDigestEncoding=L''encodage de digest spécifié [{0}] est invalide +realmBase.unknownAllRolesMode=Le mode [{0}] est inconnu, il doit être : strict, authOnly ou strictAuthOnly +realmBase.validity=Erreur lors de la vérification de la validité du certificat + +userDatabaseRealm.lookup=Exception lors de la recherche dans la base de données utilisateurs avec la clé [{0}] +userDatabaseRealm.noDatabase=Aucun composant base de données utilisateurs trouvé pour la clé [{0}] +userDatabaseRealm.noNamingContext=Impossible d'accéder au serveur pour obtenir le contexte JNDI diff --git a/java/org/apache/catalina/realm/LocalStrings_ja.properties b/java/org/apache/catalina/realm/LocalStrings_ja.properties new file mode 100644 index 0000000..967705b --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_ja.properties @@ -0,0 +1,118 @@ +# 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. + +combinedRealm.addRealm=Realm[{0}]を追加ã—ã€åˆè¨ˆ[{1}]Realmを作æˆã—ã¾ã™ã€‚ +combinedRealm.authFail=レルム [{1}] ã§ãƒ¦ãƒ¼ã‚¶ãƒ¼ [{0}] ã®èªè¨¼ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +combinedRealm.authStart=ユーザー [{0}] ã‚’Realm [{1}] ã§èªè¨¼ã—ã¾ã™ã€‚ +combinedRealm.authSuccess=レルム [{1}] ã§ãƒ¦ãƒ¼ã‚¶ãƒ¼ [{0}] ã‚’èªè¨¼ã—ã¾ã—ãŸã€‚ +combinedRealm.getPassword=getPassword() メソッドを呼ã³å‡ºã—ã¦ã¯ã„ã‘ã¾ã›ã‚“。 +combinedRealm.getPrincipal=getPrincipal()メソッドã¯æ±ºã—ã¦å‘¼ã³å‡ºã•ã‚Œã‚‹ã¹ãã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +combinedRealm.realmStartFail=Realm[{0}]ã®é–‹å§‹ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +combinedRealm.setCredentialHandler=資格情報ãƒãƒ³ãƒ‰ãƒ©ãŒè¤‡åˆãƒ¬ãƒ«ãƒ ï¼ˆã¾ãŸã¯ãã®ã‚µãƒ–クラス)ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã§è¨­å®šã•ã‚Œã¾ã—ãŸãŒã€è¤‡åˆãƒ¬ãƒ«ãƒ ã¯è³‡æ ¼æƒ…å ±ãƒãƒ³ãƒ‰ãƒ©ã‚’使用ã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“。構æˆãŒèª¤ã£ã¦ã„ã¾ã›ã‚“ã‹ï¼Ÿ +combinedRealm.unexpectedMethod=Combined Realmã«ã¤ã„ã¦äºˆæœŸã›ã¬ãƒ¡ã‚½ãƒƒãƒ‰å‘¼ã³å‡ºã—ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ + +credentialHandler.invalidStoredCredential=ユーザæä¾›ã®è³‡æ ¼æƒ…å ±ã¨ãƒžãƒƒãƒã™ã‚‹ãŸã‚ã«Realm ã«ã‚ˆã£ã¦æä¾›ã•ã‚ŒãŸç„¡åŠ¹ãªæ ¼ç´ã•ã‚ŒãŸè³‡æ ¼æƒ…報文字列[[{0}] + +dataSourceRealm.authenticateFailure=ユーザå [{0}] ã¯èªè¨¼ã«å¤±æ•—ã—ã¾ã—㟠+dataSourceRealm.authenticateSuccess=ユーザå [{0}] ã¯èªè¨¼ã«æˆåŠŸã—ã¾ã—㟠+dataSourceRealm.close=データベース接続をクローズ中ã®ä¾‹å¤–ã§ã™ +dataSourceRealm.commit=クローズå‰ã«æŽ¥ç¶šã‚’コミットã—ãŸéš›ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—㟠+dataSourceRealm.exception=èªè¨¼ã‚’実行中ã®ä¾‹å¤–ã§ã™ +dataSourceRealm.getPassword.exception=[{0}] ã®ãƒ‘スワードをå–得中ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +dataSourceRealm.getRoles.exception=[{0}] ã®ãƒ­ãƒ¼ãƒ«ã‚’å–得中ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +dataSourceRealm.noNamingContext=ãƒãƒ¼ãƒŸãƒ³ã‚°ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’å–å¾—ã™ã‚‹ãŸã‚ã«ã‚µãƒ¼ãƒãƒ¼ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã›ã‚“ + +jaasCallback.username=è¿”ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼å[{0}] + +jaasMemoryLoginModule.callbackHandlerError=コールãƒãƒƒã‚¯ãƒãƒ³ãƒ‰ãƒ©ãƒ¼ã®å‘¼ã³å‡ºã—中ã®ã‚¨ãƒ©ãƒ¼: [{0}] +jaasMemoryLoginModule.invalidCredentials=ユーザーåã€ã¾ãŸã¯ã€ãƒ‘スワードãŒé–“é•ã£ã¦ã„ã¾ã™ã€‚ +jaasMemoryLoginModule.login=ユーザ [{0}] ã¯ãƒ—リンシパル [{1}] ã¨ã—ã¦ãƒ­ã‚°ã‚¤ãƒ³ã—ã¾ã™ +jaasMemoryLoginModule.noCallbackHandler=CallbackHandler ãŒæœªæŒ‡å®šã§ã™ã€‚ +jaasMemoryLoginModule.noCatalinaBase=ファイル[{0}]をロードã™ã‚‹Catalina baseを特定ã§ãã¾ã›ã‚“ +jaasMemoryLoginModule.noConfig=設定ファイルを読ã¿è¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸã€‚ [{0}] +jaasMemoryLoginModule.parseError=設定ファイル [{0}] 処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +jaasMemoryLoginModule.unknownAuthenticationMethod=未知ã®èªè¨¼æ–¹æ³•ã§ã™ + +jaasRealm.accountExpired=ユーザå [{0}] ã¯ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®æœŸé™ãŒåˆ‡ã‚Œã¦ã„ã‚‹ãŸã‚ã«èªè¨¼ã•ã‚Œã¾ã›ã‚“ +jaasRealm.appName=JAASアプリケーションå[{0}]を設定ã—ã¾ã™ã€‚ +jaasRealm.authenticateFailure=ユーザーå[{0}]ã¯æ­£å¸¸ã«èªè¨¼ã•ã‚Œã¾ã›ã‚“ã§ã—㟠+jaasRealm.authenticateSuccess=ユーザå [{0}] ã¯èªè¨¼ã«æˆåŠŸã—ã¾ã—㟠+jaasRealm.beginLogin=アプリケーション[{1}]ã®LoginContextを使用ã—ã¦ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼å[{0}]ã«å¯¾ã™ã‚‹JAASRealmログインãŒè¦æ±‚ã•ã‚Œã¾ã—ãŸã€‚ +jaasRealm.checkPrincipal=プリンシパル [{0}] [{1}] ã‚’ãƒã‚§ãƒƒã‚¯ã—ã¾ã™ +jaasRealm.classNotFound=クラス [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+jaasRealm.credentialExpired=ユーザå [{0}] ã¯è¨¼æ˜Žæ›¸ã®æœŸé™ãŒåˆ‡ã‚ŒãŸãŸã‚ã«èªè¨¼ã•ã‚Œã¾ã›ã‚“ +jaasRealm.failedLogin=ユーザå [{0}] ã¯ãƒ­ã‚°ã‚¤ãƒ³ã«å¤±æ•—ã—ãŸãŸã‚ã«èªè¨¼ã•ã‚Œã¾ã›ã‚“ã§ã—㟠+jaasRealm.loginContextCreated=ユーザーå[{0}]用ã«ä½œæˆã•ã‚ŒãŸJAAS LoginContext +jaasRealm.loginException=ユーザå [{0}] ã®èªè¨¼ä¸­ã«ãƒ­ã‚°ã‚¤ãƒ³ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—㟠+jaasRealm.notPrincipal=クラス [{0}] ã¯java.security.Principalを実装ã—ã¦ã„ãªã„ãŸã‚追加ã•ã‚Œã¾ã›ã‚“ +jaasRealm.rolePrincipalAdd=ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒ—リンシパルロールã«ãƒ­ãƒ¼ãƒ«ãƒ—リンシパル [{0}] を追加 +jaasRealm.rolePrincipalFailure=有効ãªå½¹å‰²ã¯ã‚ã‚Šã¾ã›ã‚“。 +jaasRealm.unexpectedError=予期ã›ã¬ã‚¨ãƒ©ãƒ¼ +jaasRealm.userPrincipalFailure=有効ãªãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒ—リンシパルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +jaasRealm.userPrincipalSuccess=プリンシパル [{0}] ã¯æœ‰åŠ¹ãªãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚¯ãƒ©ã‚¹ã§ã™ã€‚ã“れをユーザープリンシパルã¨ã—ã¦ä½¿ç”¨ã—ã¾ã™ã€‚ + +jndiRealm.authenticateFailure=ユーザå [{0}] ã¯èªè¨¼ã«å¤±æ•—ã—ã¾ã—㟠+jndiRealm.authenticateSuccess=ユーザå [{0}] ã¯èªè¨¼ã«æˆåŠŸã—ã¾ã—㟠+jndiRealm.cipherSuites=TLS 接続ã§æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆ [{0}] を有効化ã—ã¾ã—ãŸã€‚ +jndiRealm.close=ディレクトリサーãƒæŽ¥ç¶šã‚¯ãƒ­ãƒ¼ã‚ºä¸­ã®ä¾‹å¤–ã§ã™ +jndiRealm.emptyCipherSuites=指定ã•ã‚ŒãŸæš—å·ã‚¹ã‚¤ãƒ¼ãƒˆã®ç©ºã®æ–‡å­—列。 既定ã®æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆã‚’使用ã—ã¾ã™ã€‚ +jndiRealm.exception=èªè¨¼å®Ÿè¡Œä¸­ã®ä¾‹å¤–ã§ã™ +jndiRealm.exception.retry=èªè¨¼ä¸­ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚å†è©¦è¡Œã—ã¾ã™... +jndiRealm.invalidHostnameVerifier=[{0}]ã¯HostnameVerifierã®æœ‰åŠ¹ãªã‚¯ãƒ©ã‚¹åã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +jndiRealm.invalidName=検索ã§è§£æžã§ããªã„絶対åãŒè¿”ã•ã‚Œã¾ã—ãŸ: [{0}] +jndiRealm.invalidSslProtocol=指定ã•ã‚ŒãŸãƒ—ロトコル[{0}]ã¯ç„¡åŠ¹ã§ã™ã€‚ [{1}]ã®ã„ãšã‚Œã‹ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +jndiRealm.invalidSslSocketFactory=[{0}]ã¯SSLSocketFactoryã®æœ‰åŠ¹ãªã‚¯ãƒ©ã‚¹åã§ã¯ã‚ã‚Šã¾ã›ã‚“ +jndiRealm.multipleEntries=ユーザå [{0}] ã¯è¤‡æ•°ã®ã‚¨ãƒ³ãƒˆãƒªã‚’æŒã£ã¦ã„ã¾ã™ +jndiRealm.negotiatedTls=プロトコル[{0}]を使用ã—ã¦äº¤æ¸‰ã•ã‚ŒãŸTLS接続 +jndiRealm.open=ディレクトリサーãƒæŽ¥ç¶šã‚ªãƒ¼ãƒ—ン中ã®ä¾‹å¤–ã§ã™ +jndiRealm.tlsClose=TLSレスãƒãƒ³ã‚¹ã‚’é–‰ã˜ã‚‹éš›ã®ä¾‹å¤– + +lockOutRealm.authLockedUser=ロックã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ [{0}] ã®èªè¨¼ãŒè©¦è¡Œã•ã‚Œã¾ã—㟠+lockOutRealm.removeWarning=キャッシュサイズãŒåˆ¶é™å†…ã«åŽã¾ã‚‹ã‚ˆã†ã«ã™ã‚‹ãŸã‚ã€[{1}]秒後ã«ãƒ¦ãƒ¼ã‚¶ãƒ¼[{0}]ãŒå¤±æ•—ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã‹ã‚‰å‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚ + +mdCredentialHandler.unknownEncoding=エンコーディング [{0}] ã«ã¯æœªå¯¾å¿œã®ãŸã‚ç¾åœ¨ã®è¨­å®š [{1}] を使用ã—ã¾ã™ã€‚ + +memoryRealm.authenticateFailure=ユーザå [{0}] ã¯èªè¨¼ã«å¤±æ•—ã—ã¾ã—㟠+memoryRealm.authenticateSuccess=ユーザå [{0}] ã¯èªè¨¼ã«æˆåŠŸã—ã¾ã—㟠+memoryRealm.loadExist=メモリデータベースファイル [{0}] を読むã“ã¨ãŒã§ãã¾ã›ã‚“ +memoryRealm.loadPath=メモリデータベースファイル [{0}] ã‹ã‚‰ãƒ¦ãƒ¼ã‚¶ã‚’ロードã—ã¾ã™ +memoryRealm.readXml=メモリデータベースファイルを読ã¿è¾¼ã¿ä¸­ã®ä¾‹å¤–ã§ã™ +memoryRealm.xmlFeatureEncoding=XMLファイルã®Javaエンコーディングåを許å¯ã™ã‚‹ãŸã‚ã«digesterを設定ã™ã‚‹ä¾‹å¤–。IANAã®ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‡ã‚£ãƒ³ã‚°åã®ã¿ãŒã‚µãƒãƒ¼ãƒˆã•ã‚Œã¾ã™ã€‚ + +pbeCredentialHandler.invalidKeySpec=パスワードベースã®éµã‚’生æˆã§ãã¾ã›ã‚“。 + +realmBase.algorithm=無効ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ€ã‚¤ã‚¸ã‚§ã‚¹ãƒˆã‚¢ãƒ«ã‚´ãƒªã‚ºãƒ  [{0}] ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ +realmBase.authenticateFailure=ユーザå [{0}] ã¯èªè¨¼ã«å¤±æ•—ã—ã¾ã—㟠+realmBase.authenticateSuccess=ユーザå [{0}] ã¯èªè¨¼ã«æˆåŠŸã—ã¾ã—㟠+realmBase.cannotGetRoles=プリンシパル [{0}] ã‹ã‚‰ãƒ­ãƒ¼ãƒ«ã‚’å–å¾—ã§ãã¾ã›ã‚“ +realmBase.createUsernameRetriever.ClassCastException=クラス [{0}] 㯠X509UsernameRetriever ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +realmBase.createUsernameRetriever.newInstance=タイプ [{0}] ã®ã‚ªãƒ–ジェクトを作æˆã§ãã¾ã›ã‚“。 +realmBase.credentialNotDelegated=ユーザ [{0}] ã®è³‡æ ¼æƒ…å ±ã¯å§”ä»»ã•ã‚Œã¦ã„ã¾ã›ã‚“ãŒã€ä¿å­˜ãŒè¦æ±‚ã•ã‚Œã¾ã—ãŸã€‚ +realmBase.delegatedCredentialFail=ユーザー[{0}]ã®å§”ä»»ã•ã‚ŒãŸè³‡æ ¼æƒ…報をå–å¾—ã§ãã¾ã›ã‚“ +realmBase.digest=ユーザ証明書ã®ãƒ€ã‚¤ã‚¸ã‚§ã‚¹ãƒˆã‚¨ãƒ©ãƒ¼ +realmBase.digestMismatch=[{0}] を使用ã—㟠DIGEST èªè¨¼ã¨ã—ã¦ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’èªè¨¼ã§ãã¾ã›ã‚“ã§ã—ãŸãŒã€ãƒ‘スワード㯠[{1}] を使用ã—ã¦ãƒ¬ãƒ«ãƒ ã«ä¿å­˜ã•ã‚Œã¦ã„ã¾ã—㟠+realmBase.forbidden=è¦æ±‚ã•ã‚ŒãŸãƒªã‚½ãƒ¼ã‚¹ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãŒæ‹’å¦ã•ã‚Œã¾ã—㟠+realmBase.gotX509Username=X509証明書ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åã‚’å–å¾—ã—ã¾ã—ãŸï¼š[{0}] +realmBase.gssContextNotEstablished=Authenticator 実装エラー:渡ã•ã‚ŒãŸã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãŒå®Œå…¨ã«ç¢ºç«‹ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +realmBase.gssNameFail=確立ã•ã‚ŒãŸGSSContextã‹ã‚‰åå‰ã‚’抽出ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +realmBase.hasRoleFailure=ユーザå [{0}] ã¯ãƒ­ãƒ¼ãƒ« [{1}] ã‚’æŒã£ã¦ã„ã¾ã›ã‚“ +realmBase.hasRoleSuccess=ユーザå [{0}] ã¯ãƒ­ãƒ¼ãƒ« [{1}] ã‚’æŒã£ã¦ã„ã¾ã™ +realmBase.invalidDigestEncoding=無効ãªãƒ€ã‚¤ã‚¸ã‚§ã‚¹ãƒˆã‚¨ãƒ³ã‚³ãƒ¼ãƒ‡ã‚£ãƒ³ã‚° [{0}] +realmBase.unknownAllRolesMode=未知ã®ãƒ¢ãƒ¼ãƒ‰ [{0}]。strictã€authOnlyã€strictAuthOnly ã®ã©ã‚Œã‹1ã¤ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +realmBase.validity=証明書ã®æœ‰åŠ¹æ€§ãƒã‚§ãƒƒã‚¯ä¸­ã®ã‚¨ãƒ©ãƒ¼ + +userDatabaseRealm.lookup=キー [{0}] ã§ãƒ¦ãƒ¼ã‚¶ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã‚’検索中ã®ä¾‹å¤–ã§ã™ +userDatabaseRealm.noDatabase=キー [{0}] ã§ãƒ¦ãƒ¼ã‚¶ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +userDatabaseRealm.noNamingContext=ãƒãƒ¼ãƒŸãƒ³ã‚°ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚’å–å¾—ã™ã‚‹ãŸã‚ã«ã‚µãƒ¼ãƒãƒ¼ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã›ã‚“ diff --git a/java/org/apache/catalina/realm/LocalStrings_ko.properties b/java/org/apache/catalina/realm/LocalStrings_ko.properties new file mode 100644 index 0000000..414d491 --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_ko.properties @@ -0,0 +1,112 @@ +# 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. + +combinedRealm.addRealm=Realm [{0}]ì„(를) 추가하여, ì „ì²´ Realm 개수는 [{1}]ì´(ê°€) ë©ë‹ˆë‹¤. +combinedRealm.authFail=ì‚¬ìš©ìž [{0}]ì„(를) realm [{1}]ì„(를) 사용하여 ì¸ì¦í•˜ì§€ 못했습니다. +combinedRealm.authStart=ì‚¬ìš©ìž [{0}]ì„(를), realm [{1}]ì„(를) 사용하여 ì¸ì¦ ì‹œë„ ì¤‘ +combinedRealm.authSuccess=ì‚¬ìš©ìž [{0}]ì„(를) realm [{1}]ì„(를) 사용하여 ì¸ì¦í–ˆìŠµë‹ˆë‹¤. +combinedRealm.getPassword=getPassword() 메소드는 절대 호출ë˜ì„œëŠ” 안ë©ë‹ˆë‹¤. +combinedRealm.getPrincipal=getPrincipal() 메소드는 절대로 호출ë˜ì„œëŠ” 안ë©ë‹ˆë‹¤. +combinedRealm.realmStartFail=[{0}] realmì„ ì‹œìž‘í•˜ì§€ 못했습니다. +combinedRealm.setCredentialHandler=CredentialHandlerê°€ 해당 CombinedRealm (í˜¹ì€ CombinedRealmì˜ ì„œë¸Œí´ëž˜ìŠ¤) ê°ì²´ì— 설정 ë˜ì—ˆìŠµë‹ˆë‹¤ë§Œ, CombinedRealm í´ëž˜ìŠ¤ëŠ” CredentialHandler를 사용하지 않습니다. 설정 오류ì¸ê°€ìš”? +combinedRealm.unexpectedMethod=CombinedRealmì˜ ë©”ì†Œë“œì— ì˜ˆê¸°ì¹˜ ì•Šì€ í˜¸ì¶œì´ì—ˆìŠµë‹ˆë‹¤. + +credentialHandler.invalidStoredCredential=사용ìžê°€ 제공한 credentialsê³¼ 부합하는지 검사하기 위하여, 유효하지 ì•Šì€ ì €ìž¥ëœ credentials 문ìžì—´ [{0}]ì´(ê°€), Realmì— ì˜í•´ 제공ë˜ì—ˆìŠµë‹ˆë‹¤. + +dataSourceRealm.authenticateFailure=사용ìžëª… [{0}]ì€(는) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì§€ 않았습니다. +dataSourceRealm.authenticateSuccess=사용ìžëª… [{0}]ì´(ê°€) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì—ˆìŠµë‹ˆë‹¤. +dataSourceRealm.close=ë°ì´í„°ë² ì´ìŠ¤ ì—°ê²°ì„ ë‹«ëŠ” 중 예외 ë°œìƒ +dataSourceRealm.commit=ë°ì´í„°ë² ì´ìŠ¤ ì—°ê²°ì„ ë‹«ê¸° ì „, ì»¤ë°‹ì„ ì‹œë„하는 중 SQLException ë°œìƒ +dataSourceRealm.exception=ì¸ì¦ 처리 수행 중 예외 ë°œìƒ +dataSourceRealm.getPassword.exception=[{0}]ì„(를) 위한 비밀번호를 ì¡°íšŒí•˜ë˜ ì¤‘ 예외 ë°œìƒ +dataSourceRealm.getRoles.exception=ì‚¬ìš©ìž [{0}]ì„(를) 위한 ì—­í• ë“¤ì„ ì¡°íšŒí•˜ëŠ” 중 예외 ë°œìƒ + +jaasCallback.username=ë°˜í™˜ëœ ì‚¬ìš©ìžëª… [{0}] + +jaasMemoryLoginModule.callbackHandlerError=콜백 핸들러 호출 중 오류 ë°œìƒ: [{0}] +jaasMemoryLoginModule.invalidCredentials=ì‚¬ìš©ìž ì´ë¦„ ë˜ëŠ” 비밀번호가 올바르지 않습니다. +jaasMemoryLoginModule.noCallbackHandler=CallbackHandlerê°€ 지정ë˜ì§€ 않았습니다. +jaasMemoryLoginModule.noCatalinaBase=íŒŒì¼ [{0}]ì„(를) 로드하기 위한 Catalina base를 ê²°ì •í•  수 없습니다. +jaasMemoryLoginModule.noConfig=설정 íŒŒì¼ [{0}]ì„(를) 로드할 수 없습니다. +jaasMemoryLoginModule.parseError=설정 íŒŒì¼ [{0}]ì„(를) 처리하는 중 오류 ë°œìƒ +jaasMemoryLoginModule.unknownAuthenticationMethod=ì•Œ 수 없는 ì¸ì¦ 메소드 + +jaasRealm.accountExpired=사용ìžëª… [{0}]ì€(는) ë§Œë£Œëœ ê³„ì •ì´ë¼ì„œ ì¸ì¦ë˜ì§€ 않았습니다. +jaasRealm.appName=JAAS 앱 ì´ë¦„ [{0}]ì„(를) 설정합니다. +jaasRealm.authenticateFailure=사용ìžëª… [{0}]ì€(는) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì§€ 못했습니다. +jaasRealm.authenticateSuccess=사용ìžëª… [{0}]ì´(ê°€) 성공ì ìœ¼ë¡œ Principal [{1}](으)로서 ì¸ì¦ë˜ì—ˆìŠµë‹ˆë‹¤ -- Subject ë˜í•œ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. +jaasRealm.beginLogin=애플리케ì´ì…˜ [{1}]ì„(를) 위한 LoginContext를 사용하여, 사용ìžëª… [{0}]ì„(를) 위한 JAASRealm 로그ì¸ì´ 요청ë˜ì—ˆìŠµë‹ˆë‹¤. +jaasRealm.checkPrincipal=Principal [{0}] [{1}]ì„(를) ì ê²€í•©ë‹ˆë‹¤. +jaasRealm.classNotFound=í´ëž˜ìŠ¤ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없었습니다. +jaasRealm.credentialExpired=ë§Œë£Œëœ credentialsë¡œ ì¸í•˜ì—¬, 사용ìžëª… [{0}]ì´(ê°€) ì¸ì¦ë˜ì§€ 않았습니다. +jaasRealm.failedLogin=사용ìžëª… [{0}]ì€(는) ë¡œê·¸ì¸ ì‹¤íŒ¨ë¡œ ì¸í•˜ì—¬ ì¸ì¦ë˜ì§€ 않았습니다. +jaasRealm.loginContextCreated=사용ìžëª… [{0}]ì„(를) 위해 ìƒì„±ëœ JAAS LoginContext +jaasRealm.loginException=ì‚¬ìš©ìž ì´ë¦„ [{0}]ì„(를) ì¸ì¦í•˜ëŠ” 중 ë¡œê·¸ì¸ ì˜ˆì™¸ ë°œìƒ +jaasRealm.notPrincipal=java.security.Principalì„ êµ¬í˜„í•˜ê³  있지 ì•Šì•„, í´ëž˜ìŠ¤ [{0}]ì´(ê°€) 추가ë˜ì§€ 않았습니다. +jaasRealm.rolePrincipalAdd=ì´ ì‚¬ìš©ìž Principalì˜ ì—­í• ë“¤ì—, ì—­í•  Principal [{0}]ì„(를) 추가합니다. +jaasRealm.rolePrincipalFailure=유효한 ì—­í•  Principalë“¤ì„ ì°¾ì„ ìˆ˜ 없습니다. +jaasRealm.unexpectedError=예기치 ì•Šì€ ì˜¤ë¥˜ +jaasRealm.userPrincipalFailure=유효한 ì‚¬ìš©ìž Principalì„ ì°¾ì„ ìˆ˜ 없습니다. +jaasRealm.userPrincipalSuccess=Principal [{0}]ì€(는) 유효한 ì‚¬ìš©ìž í´ëž˜ìŠ¤ìž…니다. ì´ë¥¼ ì‚¬ìš©ìž Principalë¡œ 사용하겠습니다. + +jndiRealm.authenticateFailure=사용ìžëª… [{0}]ì´(ê°€) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì§€ 못했습니다. +jndiRealm.authenticateSuccess=사용ìžëª… [{0}]ì´(ê°€) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì—ˆìŠµë‹ˆë‹¤. +jndiRealm.cipherSuites=ì´ tls ì—°ê²°ì„ ìœ„í•œ cipher suite들로서, [{0}]ì„(를) 사용 가능하게 합니다. +jndiRealm.close=디렉토리 서버 ì—°ê²°ì„ ë‹«ëŠ” 중 예외 ë°œìƒ +jndiRealm.emptyCipherSuites=주어진 cipher suiteë“¤ì— ë¹ˆ 문ìžì—´ì´ 설정ë˜ì—ˆìŠµë‹ˆë‹¤. 기본 cipher suiteë“¤ì„ ì‚¬ìš©í•©ë‹ˆë‹¤. +jndiRealm.exception=ì¸ì¦ 처리 수행 중 예외 ë°œìƒ +jndiRealm.exception.retry=ì¸ì¦ 처리 수행 중 예외 ë°œìƒ. 재시ë„합니다... +jndiRealm.invalidHostnameVerifier=[{0}]ì€(는), HostnameVerifier를 위한 í´ëž˜ìŠ¤ ì´ë¦„으로서, 유효하지 않습니다. +jndiRealm.invalidSslProtocol=주어진 프로토콜 [{0}]ì€(는) 유효하지 않습니다. 반드시 [{1}] ì¤‘ì˜ í•˜ë‚˜ì—¬ì•¼ 합니다. +jndiRealm.invalidSslSocketFactory=[{0}]ì€(는) SSLSocketFactory ê°ì²´ë¥¼ 위해 유효한 í´ëž˜ìŠ¤ ì´ë¦„ì´ ì•„ë‹™ë‹ˆë‹¤. +jndiRealm.multipleEntries=[{0}] ì‚¬ìš©ìž ì´ë¦„ì€ ì—¬ëŸ¬ ê°œì˜ ì—”íŠ¸ë¦¬ë“¤ì„ ê°€ì§€ê³  있습니다 +jndiRealm.negotiatedTls=프로토콜 [{0}]ì„(를) 사용하여 TLS ì—°ê²°ì„ negotiate 했습니다. +jndiRealm.open=디렉토리 서버 ì—°ê²°ì„ ì—¬ëŠ” 중 예외 ë°œìƒ +jndiRealm.tlsClose=tls ì‘ë‹µì„ ë‹«ëŠ” 중 예외 ë°œìƒ + +lockOutRealm.authLockedUser=잠금 ìƒíƒœì¸ ì‚¬ìš©ìž [{0}]ì„(를) ì¸ì¦í•˜ë ¤ëŠ” ì‹œë„ê°€ ì´ë£¨ì–´ì¡ŒìŠµë‹ˆë‹¤. +lockOutRealm.removeWarning=ìºì‹œ í¬ê¸°ë¥¼ 한계값 ë‚´ì—ì„œ 유지하기 위하여, ì‚¬ìš©ìž [{0}]ì„(를), [{1}]ì´ˆ í›„ì— ì‹¤íŒ¨ ì‚¬ìš©ìž ìºì‹œë¡œë¶€í„° 제거했습니다. + +mdCredentialHandler.unknownEncoding=ì¸ì½”딩 [{0}]ì´(ê°€) 지ì›ë˜ì§€ ì•Šì•„ì„œ, 현 설정 [{1}]ì´(ê°€) ê³„ì† ì‚¬ìš©ë  ê²ƒìž…ë‹ˆë‹¤. + +memoryRealm.authenticateFailure=사용ìžëª… [{0}]ì´(ê°€) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì§€ 못했습니다. +memoryRealm.authenticateSuccess=사용ìžëª… [{0}]ì´(ê°€) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì—ˆìŠµë‹ˆë‹¤. +memoryRealm.loadExist=메모리 ë°ì´í„°ë² ì´ìŠ¤ íŒŒì¼ [{0}]ì„(를) ì½ì„ 수 없습니다. +memoryRealm.loadPath=메모리 ë°ì´í„°ë² ì´ìŠ¤ íŒŒì¼ [{0}](으)로부터 사용ìžë“¤ì„ 로드합니다. +memoryRealm.readXml=메모리 ë°ì´í„°ë² ì´ìŠ¤ 파ì¼ì„ ì½ëŠ” 중 예외 ë°œìƒ +memoryRealm.xmlFeatureEncoding=XML 파ì¼ë“¤ì—ì„œ ìžë°” ì¸ì½”딩 ì´ë¦„ë“¤ì„ í—ˆìš©í•˜ê¸° 위해 digester를 설정하는 중 예외 ë°œìƒ. ì˜¤ì§ IANA ì¸ì½”딩 ì´ë¦„들만 지ì›ë  것입니다. + +pbeCredentialHandler.invalidKeySpec=비밀번호 ê¸°ë°˜ì˜ í‚¤ë¥¼ ìƒì„±í•  수 없습니다. + +realmBase.algorithm=유효하지 ì•Šì€ ë©”ì‹œì§€ Digest 알고리즘 [{0}]ì´(ê°€) 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +realmBase.authenticateFailure=사용ìžëª… [{0}]ì´(ê°€) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì§€ 못했습니다. +realmBase.authenticateSuccess=사용ìžëª… [{0}]ì´(ê°€) 성공ì ìœ¼ë¡œ ì¸ì¦ë˜ì—ˆìŠµë‹ˆë‹¤. +realmBase.cannotGetRoles=Principal [{0}](으)로부터 ì—­í• ë“¤ì„ ì–»ì„ ìˆ˜ 없습니다. +realmBase.createUsernameRetriever.ClassCastException=í´ëž˜ìŠ¤ [{0}]ì´(ê°€) X509UsernameRetriever íƒ€ìž…ì´ ì•„ë‹™ë‹ˆë‹¤. +realmBase.createUsernameRetriever.newInstance=íƒ€ìž…ì´ [{0}]ì¸ ê°ì²´ë¥¼ ìƒì„±í•  수 없습니다. +realmBase.credentialNotDelegated=ì¸ì¦ì„œ 저장 ì˜µì…˜ì´ ìš”ì²­ë˜ì—ˆì§€ë§Œ, ì‚¬ìš©ìž [{0}]ì„(를) 위한 ì¸ì¦ì„œ 대리 처리가 사용 가능하지 않습니다. +realmBase.delegatedCredentialFail=ì‚¬ìš©ìž [{0}]ì„(를) 위한 대리 ì¸ì¦ì„œë¥¼ ì–»ì„ ìˆ˜ 없습니다. +realmBase.digest=사용ìžì˜ credentials를 digest하는 중 오류 ë°œìƒ +realmBase.forbidden=ìš”ì²­ëœ ë¦¬ì†ŒìŠ¤ì— ëŒ€í•œ ì ‘ê·¼ì´ ê±°ë¶€ë˜ì—ˆìŠµë‹ˆë‹¤. +realmBase.gotX509Username=X509 ì¸ì¦ì„œë¡œë¶€í„° ì‚¬ìš©ìž ì´ë¦„ì„ êµ¬í–ˆìŠµë‹ˆë‹¤: [{0}] +realmBase.gssContextNotEstablished=Authenticator 구현 오류: ì „ë‹¬ëœ ë³´ì•ˆ 컨í…스트가 완전히 확립ë˜ì§€ 않았습니다. +realmBase.gssNameFail=í™•ë¦½ëœ GSSContext로부터, ì´ë¦„ì„ ì¶”ì¶œí•˜ì§€ 못했습니다. +realmBase.hasRoleFailure=사용ìžëª… [{0}]ì€(는) ì—­í•  [{1}]ì„(를) 가지고 있지 않습니다. +realmBase.hasRoleSuccess=사용ìžëª… [{0}]ì´(ê°€) ì—­í•  [{1}]ì„(를) 가지고 있습니다. +realmBase.invalidDigestEncoding=유효하지 ì•Šì€ digest ì¸ì½”딩: [{0}] +realmBase.unknownAllRolesMode=ì•Œ 수 없는 모드 [{0}]. 모드는 반드시 ë‹¤ìŒ ì¤‘ 하나여야만 합니다: strict, authOnly, strictAuthOnly + +userDatabaseRealm.lookup=키 [{0}]ì„(를) 사용하여 ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ë¥¼ 찾는 중 예외 ë°œìƒ +userDatabaseRealm.noDatabase=키 [{0}]ì„(를) 사용하여 UserDatabase 구성요소를 ì°¾ì„ ìˆ˜ 없습니다. diff --git a/java/org/apache/catalina/realm/LocalStrings_pt_BR.properties b/java/org/apache/catalina/realm/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..5450806 --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_pt_BR.properties @@ -0,0 +1,25 @@ +# 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. + +dataSourceRealm.getRoles.exception=Exceção ao receber papéis para [{0}] + +jaasMemoryLoginModule.unknownAuthenticationMethod=Método de autenticação desconhecido + +lockOutRealm.authLockedUser=Foi feita uma tentativa de autenticação do usuário já bloqueado [{0}] + +memoryRealm.loadExist=Arquivo de banco de dados de memória [{0}] ilegível. + +realmBase.authenticateFailure=Usuário [{0}] NÃO autenticado +realmBase.createUsernameRetriever.ClassCastException=Classe [{0}] não é um X509UsernameRetriever diff --git a/java/org/apache/catalina/realm/LocalStrings_ru.properties b/java/org/apache/catalina/realm/LocalStrings_ru.properties new file mode 100644 index 0000000..ab90977 --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_ru.properties @@ -0,0 +1,21 @@ +# 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. + +dataSourceRealm.getPassword.exception=ИÑключение при получении Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð´Ð»Ñ [{0}] + +lockOutRealm.authLockedUser=Заблокированный пользователь [{0}] Ñовершил попытку авторизоватьÑÑ + +realmBase.createUsernameRetriever.ClassCastException=КлаÑÑ [{0}] не ÑвлÑетÑÑ X509UsernameRetriever. +realmBase.forbidden=ДоÑтуп к запрашиваемому реÑурÑу был заблокирован diff --git a/java/org/apache/catalina/realm/LocalStrings_zh_CN.properties b/java/org/apache/catalina/realm/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..0421561 --- /dev/null +++ b/java/org/apache/catalina/realm/LocalStrings_zh_CN.properties @@ -0,0 +1,112 @@ +# 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. + +combinedRealm.addRealm=添加[{0}]领域,共有[{1}]个领域。 +combinedRealm.authFail=无法使用域[{1}]对用户[{0}]è¿›è¡Œèº«ä»½éªŒè¯ +combinedRealm.authStart=正在å°è¯•ä½¿ç”¨é¢†åŸŸ[{1}]对用户[{0}]è¿›è¡Œèº«ä»½éªŒè¯ +combinedRealm.authSuccess=认è¯ç”¨æˆ·[{0}],æƒé™[{1}] +combinedRealm.getPassword=永远ä¸åº”该调用getPassword()方法 +combinedRealm.getPrincipal=方法getPrincipal()永远ä¸åº”该被调用 +combinedRealm.realmStartFail=无法å¯åŠ¨[{0}]领域 +combinedRealm.setCredentialHandler=在CombinedRealm的实例(或CombinedRealmçš„å­ç±»)上设置了CredentialHandler.CombinedRealmä¸ä½¿ç”¨é…置的CredentialHandler.这是é…置错误å—? +combinedRealm.unexpectedMethod=对组åˆåŸŸä¸Šçš„方法进行了æ„外调用 + +credentialHandler.invalidStoredCredential=域æ供了无效的存储凭æ®å­—符串[{0}],以便与用户æ供的凭æ®åŒ¹é… + +dataSourceRealm.authenticateFailure=用户å[{0}]未æˆåŠŸéªŒè¯ +dataSourceRealm.authenticateSuccess=用户å[{0}]å·²æˆåŠŸé€šè¿‡èº«ä»½éªŒè¯ã€‚ +dataSourceRealm.close=关闭数æ®åº“连接时å‘生异常 +dataSourceRealm.commit=关闭å‰æ交连接异常 +dataSourceRealm.exception=认è¯å¼‚常 +dataSourceRealm.getPassword.exception=获å–用户å[{0}]对应的密ç å¤±è´¥ã€‚ +dataSourceRealm.getRoles.exception=检索角色[{0}]异常 + +jaasCallback.username=返回用户å [{0}] + +jaasMemoryLoginModule.callbackHandlerError=调用回调处ç†ç¨‹åºæ—¶å‡ºé”™ï¼š[{0}] +jaasMemoryLoginModule.invalidCredentials=用户å或密ç ä¸æ­£ç¡® +jaasMemoryLoginModule.noCallbackHandler=未指定回调处ç†ç¨‹åº +jaasMemoryLoginModule.noCatalinaBase=无法确定è¦åŠ è½½æ–‡ä»¶çš„Catalina基[{0}] +jaasMemoryLoginModule.noConfig=无法加载é…置文件 [{0}] +jaasMemoryLoginModule.parseError=处ç†é…置文件[{0}]时出错 +jaasMemoryLoginModule.unknownAuthenticationMethod=未知的身份验è¯æ–¹æ³• + +jaasRealm.accountExpired=由于å¸æˆ·è¿‡æœŸï¼Œç”¨æˆ·å[{0}]æœªé€šè¿‡èº«ä»½éªŒè¯ +jaasRealm.appName=设置JAAS应用程åºå称[{0}] +jaasRealm.authenticateFailure=用户 [{0}] 认è¯å¤±è´¥ +jaasRealm.authenticateSuccess=用户å [{0}] 已被æˆåŠŸè®¤è¯ä¸ºèº«ä»½ [{1}] -- 主体也已创建 +jaasRealm.beginLogin=使用应用程åº[{1}]çš„LoginContext为用户å[{0}]请求JAASRealm登录 +jaasRealm.checkPrincipal=正在检查主体[{0}][{1}] +jaasRealm.classNotFound=找ä¸åˆ°ç±» [{0}] +jaasRealm.credentialExpired=由于凭æ®è¿‡æœŸï¼Œç”¨æˆ·å[{0}]æœªé€šè¿‡èº«ä»½éªŒè¯ +jaasRealm.failedLogin=由于登录失败,用户å [{0}] æ— æ³•æŽˆæƒ +jaasRealm.loginContextCreated=为用户å创建的JAAS 登陆上下文[{0}] +jaasRealm.loginException=登录异常,认è¯ç”¨æˆ·å [{0}] +jaasRealm.notPrincipal=ç±»[{0}]未添加,因为它未实现java.security.Principal +jaasRealm.rolePrincipalAdd=å·²ç»å°†è§’色主体[{0}]添加到此用户主体的角色 +jaasRealm.rolePrincipalFailure=找ä¸åˆ°æœ‰æ•ˆçš„角色主体。 +jaasRealm.unexpectedError=æ„外错误 +jaasRealm.userPrincipalFailure=未å‘现有效的用户Principal +jaasRealm.userPrincipalSuccess=主体[{0}]是有效的用户类。我们将其用作用户主体。 + +jndiRealm.authenticateFailure=用户å[{0}]没有æˆåŠŸè®¤è¯ +jndiRealm.authenticateSuccess=用户å[{0}]æˆåŠŸè®¤è¯ +jndiRealm.cipherSuites=å¯ç”¨ [{0}] 作为 TLS 连接的加密套件。 +jndiRealm.close=关闭目录æœåŠ¡å™¨è¿žæŽ¥æ—¶å‘生异常 +jndiRealm.emptyCipherSuites=给定密ç å¥—件的空字符串。使用默认密ç å¥—件 +jndiRealm.exception=执行认è¯å¼‚常 +jndiRealm.exception.retry=执行身份验è¯æ—¶å‘生异常。正在é‡è¯•ã€‚。。 +jndiRealm.invalidHostnameVerifier=[{0}]ä¸æ˜¯HostnameVerifier的有效类å +jndiRealm.invalidSslProtocol=给定的åè®®[{0}]无效。它必须是[{1}]之一 +jndiRealm.invalidSslSocketFactory=[{0}]ä¸æ˜¯SSLSocketFactory的有效类å。 +jndiRealm.multipleEntries=用户å称[{0}]拥有多个实体 +jndiRealm.negotiatedTls=使用åè®®[{0}]å商的TLS连接 +jndiRealm.open=打开目录æœåŠ¡å™¨é“¾æŽ¥å¼‚常 +jndiRealm.tlsClose=关闭tlså“应时出现异常 + +lockOutRealm.authLockedUser=å°è¯•å¯¹é”定的用户[{0}]è¿›è¡Œèº«ä»½éªŒè¯ +lockOutRealm.removeWarning=用户[{0}]在[{1}]秒åŽä»Žå¤±è´¥çš„用户缓存中删除,以将缓存大å°ä¿æŒåœ¨é™åˆ¶é›†å†… + +mdCredentialHandler.unknownEncoding=ä¸æ”¯æŒç¼–ç [{0}],因此ä»å°†ä½¿ç”¨å½“å‰çš„设置[{1}] + +memoryRealm.authenticateFailure=用户å[{0}]未æˆåŠŸé€šè¿‡èº«ä»½éªŒè¯ +memoryRealm.authenticateSuccess=用户å称[{0}]认è¯æˆåŠŸ +memoryRealm.loadExist=内存数æ®åº“文件[{0}]æ— æ³•è¯»å– +memoryRealm.loadPath=从内存数æ®åº“文件 [{0}] 加载用户 +memoryRealm.readXml=读å–内存数æ®åº“文件时出现异常 +memoryRealm.xmlFeatureEncoding=é…ç½®Digester以å…许XML文件中的javaç¼–ç å称的异常。åªæ”¯æŒIANAç¼–ç å称。 + +pbeCredentialHandler.invalidKeySpec=无法生æˆåŸºäºŽå¯†ç çš„密钥 + +realmBase.algorithm=无效的消æ¯æ‘˜è¦ç®—法[{0}] +realmBase.authenticateFailure=用户å [{0}] 认è¯å¤±è´¥ +realmBase.authenticateSuccess=用户å[{0}]å·²æˆåŠŸé€šè¿‡èº«ä»½éªŒè¯ +realmBase.cannotGetRoles=无法从主体[{0}]获å–角色 +realmBase.createUsernameRetriever.ClassCastException=ç±»[{0}] ä¸æ˜¯ä¸€ä¸ªX509UsernameRetriever. +realmBase.createUsernameRetriever.newInstance=无法创建类型为[{0}]的对象。 +realmBase.credentialNotDelegated=虽然已请求存储,但用户[{0}]的凭æ®å°šæœªå§”æ´¾ +realmBase.delegatedCredentialFail=无法获å–用户[{0}]的委派凭æ®ã€‚ +realmBase.digest=对用户凭è¯æ‘˜è¦å‘生错误 +realmBase.forbidden=已拒ç»è®¿é—®æ‰€è¯·æ±‚çš„èµ„æº +realmBase.gotX509Username=从X509è¯ä¹¦ä¸­èŽ·å–用户å:[{0}] +realmBase.gssContextNotEstablished=身份验è¯å™¨å®žçŽ°é”™è¯¯ï¼šä¼ é€’的安全上下文未完全建立 +realmBase.gssNameFail=无法从已建立的GSSContext中æå–å称 +realmBase.hasRoleFailure=用户[{0}]没有角色[{1}] +realmBase.hasRoleSuccess=用户å[{0}] 有角色[{1}] +realmBase.invalidDigestEncoding=无效的摘è¦ç¼–ç [{0}] +realmBase.unknownAllRolesMode=未知模å¼[{0}],必须是以下之一:strictã€authOnlyã€strictAuthOnly + +userDatabaseRealm.lookup=在键[{0}]下查找用户数æ®åº“æ—¶å‘生异常 +userDatabaseRealm.noDatabase=未找到key[{0}]对应的UserDatabase组件。 diff --git a/java/org/apache/catalina/realm/LockOutRealm.java b/java/org/apache/catalina/realm/LockOutRealm.java new file mode 100644 index 0000000..5c74c64 --- /dev/null +++ b/java/org/apache/catalina/realm/LockOutRealm.java @@ -0,0 +1,364 @@ +/* + * 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.catalina.realm; + +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.catalina.LifecycleException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSName; + +/** + * This class extends the CombinedRealm (hence it can wrap other Realms) to provide a user lock out mechanism if there + * are too many failed authentication attempts in a given period of time. To ensure correct operation, there is a + * reasonable degree of synchronisation in this Realm. This Realm does not require modification to the underlying Realms + * or the associated user storage mechanisms. It achieves this by recording all failed logins, including those for users + * that do not exist. To prevent a DOS by deliberating making requests with invalid users (and hence causing this cache + * to grow) the size of the list of users that have failed authentication is limited. + */ +public class LockOutRealm extends CombinedRealm { + + private static final Log log = LogFactory.getLog(LockOutRealm.class); + + /** + * The number of times in a row a user has to fail authentication to be locked out. Defaults to 5. + */ + protected int failureCount = 5; + + /** + * The time (in seconds) a user is locked out for after too many authentication failures. Defaults to 300 (5 + * minutes). + */ + protected int lockOutTime = 300; + + /** + * Number of users that have failed authentication to keep in cache. Over time the cache will grow to this size and + * may not shrink. Defaults to 1000. + */ + protected int cacheSize = 1000; + + /** + * If a failed user is removed from the cache because the cache is too big before it has been in the cache for at + * least this period of time (in seconds) a warning message will be logged. Defaults to 3600 (1 hour). + */ + protected int cacheRemovalWarningTime = 3600; + + /** + * Users whose last authentication attempt failed. Entries will be ordered in access order from least recent to most + * recent. + */ + protected Map failedUsers = null; + + + @Override + protected void startInternal() throws LifecycleException { + /* + * Configure the list of failed users to delete the oldest entry once it exceeds the specified size. This is an + * LRU cache so if the cache size is exceeded the users who most recently failed authentication will be + * retained. + */ + failedUsers = new LinkedHashMap<>(cacheSize, 0.75f, true) { + private static final long serialVersionUID = 1L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > cacheSize) { + // Check to see if this element has been removed too quickly + long timeInCache = (System.currentTimeMillis() - eldest.getValue().getLastFailureTime()) / 1000; + + if (timeInCache < cacheRemovalWarningTime) { + log.warn( + sm.getString("lockOutRealm.removeWarning", eldest.getKey(), Long.valueOf(timeInCache))); + } + return true; + } + return false; + } + }; + + super.startInternal(); + } + + + @Override + public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, + String qop, String realmName, String digestA2, String algorithm) { + + Principal authenticatedUser = + super.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realmName, digestA2, algorithm); + return filterLockedAccounts(username, authenticatedUser); + } + + + @Override + public Principal authenticate(String username, String credentials) { + Principal authenticatedUser = super.authenticate(username, credentials); + return filterLockedAccounts(username, authenticatedUser); + } + + + @Override + public Principal authenticate(X509Certificate[] certs) { + String username = null; + if (certs != null && certs.length > 0) { + username = certs[0].getSubjectX500Principal().toString(); + } + + Principal authenticatedUser = super.authenticate(certs); + return filterLockedAccounts(username, authenticatedUser); + } + + + @Override + public Principal authenticate(GSSContext gssContext, boolean storeCreds) { + if (gssContext.isEstablished()) { + String username = null; + GSSName name = null; + try { + name = gssContext.getSrcName(); + } catch (GSSException e) { + log.warn(sm.getString("realmBase.gssNameFail"), e); + return null; + } + + username = name.toString(); + + Principal authenticatedUser = super.authenticate(gssContext, storeCreds); + + return filterLockedAccounts(username, authenticatedUser); + } + + // Fail in all other cases + return null; + } + + + @Override + public Principal authenticate(GSSName gssName, GSSCredential gssCredential) { + String username = gssName.toString(); + + Principal authenticatedUser = super.authenticate(gssName, gssCredential); + + return filterLockedAccounts(username, authenticatedUser); + } + + + /* + * Filters authenticated principals to ensure that null is returned for any user that is currently + * locked out. + */ + private Principal filterLockedAccounts(String username, Principal authenticatedUser) { + // Register all failed authentications + if (authenticatedUser == null && isAvailable()) { + registerAuthFailure(username); + } + + if (isLocked(username)) { + // If the user is currently locked, authentication will always fail + log.warn(sm.getString("lockOutRealm.authLockedUser", username)); + return null; + } + + if (authenticatedUser != null) { + registerAuthSuccess(username); + } + + return authenticatedUser; + } + + + /** + * Unlock the specified username. This will remove all records of authentication failures for this user. + * + * @param username The user to unlock + */ + public void unlock(String username) { + // Auth success clears the lock record so... + registerAuthSuccess(username); + } + + + /* + * Checks to see if the current user is locked. If this is associated with a login attempt, then the last access + * time will be recorded and any attempt to authenticated a locked user will log a warning. + */ + public boolean isLocked(String username) { + LockRecord lockRecord = null; + synchronized (this) { + lockRecord = failedUsers.get(username); + } + + // No lock record means user can't be locked + if (lockRecord == null) { + return false; + } + + // Check to see if user is locked + if (lockRecord.getFailures() >= failureCount && + (System.currentTimeMillis() - lockRecord.getLastFailureTime()) / 1000 < lockOutTime) { + return true; + } + + // User has not, yet, exceeded lock thresholds + return false; + } + + + /* + * After successful authentication, any record of previous authentication failure is removed. + */ + private synchronized void registerAuthSuccess(String username) { + // Successful authentication means removal from the list of failed users + failedUsers.remove(username); + } + + + /* + * After a failed authentication, add the record of the failed authentication. + */ + private void registerAuthFailure(String username) { + LockRecord lockRecord = null; + synchronized (this) { + if (!failedUsers.containsKey(username)) { + lockRecord = new LockRecord(); + failedUsers.put(username, lockRecord); + } else { + lockRecord = failedUsers.get(username); + if (lockRecord.getFailures() >= failureCount && + ((System.currentTimeMillis() - lockRecord.getLastFailureTime()) / 1000) > lockOutTime) { + // User was previously locked out but lockout has now + // expired so reset failure count + lockRecord.setFailures(0); + } + } + } + lockRecord.registerFailure(); + } + + + /** + * Get the number of failed authentication attempts required to lock the user account. + * + * @return the failureCount + */ + public int getFailureCount() { + return failureCount; + } + + + /** + * Set the number of failed authentication attempts required to lock the user account. + * + * @param failureCount the failureCount to set + */ + public void setFailureCount(int failureCount) { + this.failureCount = failureCount; + } + + + /** + * Get the period for which an account will be locked. + * + * @return the lockOutTime + */ + public int getLockOutTime() { + return lockOutTime; + } + + + /** + * Set the period for which an account will be locked. + * + * @param lockOutTime the lockOutTime to set + */ + public void setLockOutTime(int lockOutTime) { + this.lockOutTime = lockOutTime; + } + + + /** + * Get the maximum number of users for which authentication failure will be kept in the cache. + * + * @return the cacheSize + */ + public int getCacheSize() { + return cacheSize; + } + + + /** + * Set the maximum number of users for which authentication failure will be kept in the cache. + * + * @param cacheSize the cacheSize to set + */ + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + } + + + /** + * Get the minimum period a failed authentication must remain in the cache to avoid generating a warning if it is + * removed from the cache to make space for a new entry. + * + * @return the cacheRemovalWarningTime + */ + public int getCacheRemovalWarningTime() { + return cacheRemovalWarningTime; + } + + + /** + * Set the minimum period a failed authentication must remain in the cache to avoid generating a warning if it is + * removed from the cache to make space for a new entry. + * + * @param cacheRemovalWarningTime the cacheRemovalWarningTime to set + */ + public void setCacheRemovalWarningTime(int cacheRemovalWarningTime) { + this.cacheRemovalWarningTime = cacheRemovalWarningTime; + } + + + protected static class LockRecord { + private final AtomicInteger failures = new AtomicInteger(0); + private long lastFailureTime = 0; + + public int getFailures() { + return failures.get(); + } + + public void setFailures(int theFailures) { + failures.set(theFailures); + } + + public long getLastFailureTime() { + return lastFailureTime; + } + + public void registerFailure() { + failures.incrementAndGet(); + lastFailureTime = System.currentTimeMillis(); + } + } +} diff --git a/java/org/apache/catalina/realm/MemoryRealm.java b/java/org/apache/catalina/realm/MemoryRealm.java new file mode 100644 index 0000000..cf9f155 --- /dev/null +++ b/java/org/apache/catalina/realm/MemoryRealm.java @@ -0,0 +1,247 @@ +/* + * 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.catalina.realm; + +import java.io.IOException; +import java.io.InputStream; +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.catalina.LifecycleException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.file.ConfigFileLoader; + + +/** + * Simple implementation of Realm that reads an XML file to configure the valid users, passwords, and roles. The + * file format (and default file location) are identical to those currently supported by Tomcat 3.X. + *

    + * IMPLEMENTATION NOTE: It is assumed that the in-memory collection representing our defined users (and + * their roles) is initialized at application startup and never modified again. Therefore, no thread synchronization is + * performed around accesses to the principals collection. + * + * @author Craig R. McClanahan + */ +public class MemoryRealm extends RealmBase { + + private static final Log log = LogFactory.getLog(MemoryRealm.class); + + // ----------------------------------------------------- Instance Variables + + + /** + * The Digester we will use to process in-memory database files. + */ + private static Digester digester = null; + private static final Object digesterLock = new Object(); + + + /** + * The pathname (absolute or relative to Catalina's current working directory) of the XML file containing our + * database information. + */ + private String pathname = "conf/tomcat-users.xml"; + + + /** + * The set of valid Principals for this Realm, keyed by user name. + */ + private final Map principals = new HashMap<>(); + + + /** + * The set of credentials for this Realm, keyed by user name. + */ + private final Map credentials = new HashMap<>(); + + + // ------------------------------------------------------------- Properties + + /** + * @return the pathname of our XML file containing user definitions. + */ + public String getPathname() { + + return pathname; + + } + + + /** + * Set the pathname of our XML file containing user definitions. If a relative pathname is specified, it is resolved + * against "catalina.base". + * + * @param pathname The new pathname + */ + public void setPathname(String pathname) { + + this.pathname = pathname; + + } + + + // --------------------------------------------------------- Public Methods + + @Override + public Principal authenticate(String username, String credentials) { + + // No user or no credentials + // Can't possibly authenticate, don't bother the database then + if (username == null || credentials == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("memoryRealm.authenticateFailure", username)); + } + return null; + } + + GenericPrincipal principal = principals.get(username); + String password = null; + if (principal != null) { + password = this.credentials.get(username); + } + + if (principal == null || password == null) { + // User was not found in the database or the password was null + // Waste a bit of time as not to reveal that the user does not exist. + getCredentialHandler().mutate(credentials); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("memoryRealm.authenticateFailure", username)); + } + return null; + } + + boolean validated = getCredentialHandler().matches(credentials, password); + + if (validated) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("memoryRealm.authenticateSuccess", username)); + } + return principal; + } else { + if (log.isDebugEnabled()) { + log.debug(sm.getString("memoryRealm.authenticateFailure", username)); + } + return null; + } + } + + + // -------------------------------------------------------- Package Methods + + + /** + * Add a new user to the in-memory database. + * + * @param username User's username + * @param password User's password (clear text) + * @param roles Comma-delimited set of roles associated with this user + */ + void addUser(String username, String password, String roles) { + + // Accumulate the list of roles for this user + List list = new ArrayList<>(); + roles += ","; + while (true) { + int comma = roles.indexOf(','); + if (comma < 0) { + break; + } + String role = roles.substring(0, comma).trim(); + list.add(role); + roles = roles.substring(comma + 1); + } + + // Construct and cache the Principal for this user + GenericPrincipal principal = new GenericPrincipal(username, list); + principals.put(username, principal); + credentials.put(username, password); + + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * @return a configured Digester to use for processing the XML input file, creating a new one if + * necessary. + */ + protected Digester getDigester() { + // Keep locking for subclass compatibility + synchronized (digesterLock) { + if (digester == null) { + digester = new Digester(); + digester.setValidating(false); + try { + digester.setFeature("http://apache.org/xml/features/allow-java-encodings", true); + } catch (Exception e) { + log.warn(sm.getString("memoryRealm.xmlFeatureEncoding"), e); + } + digester.addRuleSet(new MemoryRuleSet()); + } + } + return digester; + } + + + @Override + protected String getPassword(String username) { + return credentials.get(username); + } + + + @Override + protected Principal getPrincipal(String username) { + return principals.get(username); + } + + + // ------------------------------------------------------ Lifecycle Methods + + @Override + protected void startInternal() throws LifecycleException { + String pathName = getPathname(); + try (InputStream is = ConfigFileLoader.getSource().getResource(pathName).getInputStream()) { + // Load the contents of the database file + if (log.isTraceEnabled()) { + log.trace(sm.getString("memoryRealm.loadPath", pathName)); + } + + synchronized (digesterLock) { + Digester digester = getDigester(); + try { + digester.push(this); + digester.parse(is); + } catch (Exception e) { + throw new LifecycleException(sm.getString("memoryRealm.readXml"), e); + } finally { + digester.reset(); + } + } + } catch (IOException ioe) { + throw new LifecycleException(sm.getString("memoryRealm.loadExist", pathName), ioe); + } + + super.startInternal(); + } +} diff --git a/java/org/apache/catalina/realm/MemoryRuleSet.java b/java/org/apache/catalina/realm/MemoryRuleSet.java new file mode 100644 index 0000000..9d795bf --- /dev/null +++ b/java/org/apache/catalina/realm/MemoryRuleSet.java @@ -0,0 +1,119 @@ +/* + * 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.catalina.realm; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.Rule; +import org.apache.tomcat.util.digester.RuleSet; +import org.xml.sax.Attributes; + +/** + *

    + * RuleSet for recognizing the users defined in the XML file processed by MemoryRealm. + *

    + * + * @author Craig R. McClanahan + */ +public class MemoryRuleSet implements RuleSet { + + + // ----------------------------------------------------- Instance Variables + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + + // ------------------------------------------------------------ Constructor + + /** + * Construct an instance of this RuleSet with the default matching pattern prefix. + */ + public MemoryRuleSet() { + this("tomcat-users/"); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + */ + public MemoryRuleSet(String prefix) { + this.prefix = prefix; + } + + + // --------------------------------------------------------- Public Methods + + + /** + *

    + * Add the set of Rule instances defined in this RuleSet to the specified Digester instance, + * associating them with our namespace URI (if any). This method should only be called by a Digester instance. + *

    + * + * @param digester Digester instance to which the new Rule instances should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + + digester.addRule(prefix + "user", new MemoryUserRule()); + + } + + +} + + +/** + * Private class used when parsing the XML database file. + */ +final class MemoryUserRule extends Rule { + + + /** + * Construct a new instance of this Rule. + */ + MemoryUserRule() { + // No initialisation required + } + + + /** + * Process a <user> element from the XML database file. + * + * @param attributes The attribute list for this element + */ + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + + String username = attributes.getValue("username"); + if (username == null) { + username = attributes.getValue("name"); + } + String password = attributes.getValue("password"); + String roles = attributes.getValue("roles"); + + MemoryRealm realm = (MemoryRealm) digester.peek(digester.getCount() - 1); + realm.addUser(username, password, roles); + + } + + +} diff --git a/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java b/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java new file mode 100644 index 0000000..7945af8 --- /dev/null +++ b/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java @@ -0,0 +1,182 @@ +/* + * 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.catalina.realm; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; + +/** + * This credential handler supports the following forms of stored passwords: + *
      + *
    • encodedCredential - a hex encoded digest of the password digested using the configured digest
    • + *
    • {MD5}encodedCredential - a Base64 encoded MD5 digest of the password
    • + *
    • {SHA}encodedCredential - a Base64 encoded SHA1 digest of the password
    • + *
    • {SSHA}encodedCredential - 20 byte Base64 encoded SHA1 digest followed by variable length salt. + * + *
      + * {SSHA}<sha-1 digest:20><salt:n>
      + * 
      + * + *
    • + *
    • salt$iterationCount$encodedCredential - a hex encoded salt, iteration code and a hex encoded credential, + * each separated by $
    • + *
    + *

    + * If the stored password form does not include an iteration count then an iteration count of 1 is used. + *

    + * If the stored password form does not include salt then no salt is used. + */ +public class MessageDigestCredentialHandler extends DigestCredentialHandlerBase { + + private static final Log log = LogFactory.getLog(MessageDigestCredentialHandler.class); + + public static final int DEFAULT_ITERATIONS = 1; + + private Charset encoding = StandardCharsets.UTF_8; + private String algorithm = null; + + + public String getEncoding() { + return encoding.name(); + } + + + public void setEncoding(String encodingName) { + if (encodingName == null) { + encoding = StandardCharsets.UTF_8; + } else { + try { + this.encoding = B2CConverter.getCharset(encodingName); + } catch (UnsupportedEncodingException e) { + log.error(sm.getString("mdCredentialHandler.unknownEncoding", encodingName, encoding.name())); + } + } + } + + + @Override + public String getAlgorithm() { + return algorithm; + } + + + @Override + public void setAlgorithm(String algorithm) throws NoSuchAlgorithmException { + ConcurrentMessageDigest.init(algorithm); + this.algorithm = algorithm; + } + + + @Override + public boolean matches(String inputCredentials, String storedCredentials) { + if (inputCredentials == null || storedCredentials == null) { + return false; + } + + if (getAlgorithm() == null) { + // No digests, compare directly + return DigestCredentialHandlerBase.equals(inputCredentials, storedCredentials, false); + } else { + // Some directories and databases prefix the password with the hash + // type. The string is in a format compatible with Base64.encode not + // the normal hex encoding of the digest + if (storedCredentials.startsWith("{MD5}") || storedCredentials.startsWith("{SHA}")) { + // Server is storing digested passwords with a prefix indicating + // the digest type + String base64ServerDigest = storedCredentials.substring(5); + byte[] userDigest = ConcurrentMessageDigest.digest(getAlgorithm(), + inputCredentials.getBytes(StandardCharsets.ISO_8859_1)); + String base64UserDigest = Base64.encodeBase64String(userDigest); + + return DigestCredentialHandlerBase.equals(base64UserDigest, base64ServerDigest, false); + } else if (storedCredentials.startsWith("{SSHA}")) { + // "{SSHA}" + // Need to convert the salt to bytes to apply it to the user's + // digested password. + String serverDigestPlusSalt = storedCredentials.substring(6); + byte[] serverDigestPlusSaltBytes = Base64.decodeBase64(serverDigestPlusSalt); + + // Extract the first 20 bytes containing the SHA-1 digest + final int digestLength = 20; + byte[] serverDigestBytes = new byte[digestLength]; + System.arraycopy(serverDigestPlusSaltBytes, 0, serverDigestBytes, 0, digestLength); + + // the remaining bytes are the salt + final int saltLength = serverDigestPlusSaltBytes.length - digestLength; + byte[] serverSaltBytes = new byte[saltLength]; + System.arraycopy(serverDigestPlusSaltBytes, digestLength, serverSaltBytes, 0, saltLength); + + // Generate the digested form of the user provided password + // using the salt + byte[] userDigestBytes = ConcurrentMessageDigest.digest(getAlgorithm(), + inputCredentials.getBytes(StandardCharsets.ISO_8859_1), serverSaltBytes); + + return Arrays.equals(userDigestBytes, serverDigestBytes); + } else if (storedCredentials.indexOf('$') > -1) { + return matchesSaltIterationsEncoded(inputCredentials, storedCredentials); + } else { + // Hex hashes should be compared case-insensitively + String userDigest = mutate(inputCredentials, null, 1); + if (userDigest == null) { + // Failed to mutate user credentials. Automatic fail. + // Root cause should be logged by mutate() + return false; + } + return storedCredentials.equalsIgnoreCase(userDigest); + } + } + } + + + @Override + protected String mutate(String inputCredentials, byte[] salt, int iterations) { + if (algorithm == null) { + return inputCredentials; + } else { + byte[] inputCredentialbytes = inputCredentials.getBytes(encoding); + byte[] userDigest; + if (salt == null) { + userDigest = ConcurrentMessageDigest.digest(algorithm, iterations, inputCredentialbytes); + } else { + userDigest = ConcurrentMessageDigest.digest(algorithm, iterations, salt, inputCredentialbytes); + } + return HexUtils.toHexString(userDigest); + } + } + + + @Override + protected int getDefaultIterations() { + return DEFAULT_ITERATIONS; + } + + + @Override + protected Log getLog() { + return log; + } +} diff --git a/java/org/apache/catalina/realm/NestedCredentialHandler.java b/java/org/apache/catalina/realm/NestedCredentialHandler.java new file mode 100644 index 0000000..286c3e0 --- /dev/null +++ b/java/org/apache/catalina/realm/NestedCredentialHandler.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.realm; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.CredentialHandler; + +public class NestedCredentialHandler implements CredentialHandler { + + private final List credentialHandlers = new ArrayList<>(); + + + @Override + public boolean matches(String inputCredentials, String storedCredentials) { + for (CredentialHandler handler : credentialHandlers) { + if (handler.matches(inputCredentials, storedCredentials)) { + return true; + } + } + return false; + } + + + /** + * The input credentials will be passed to the first nested {@link CredentialHandler}. If no nested + * {@link CredentialHandler} are configured then null will be returned. {@inheritDoc} + */ + @Override + public String mutate(String inputCredentials) { + if (credentialHandlers.isEmpty()) { + return null; + } + + return credentialHandlers.get(0).mutate(inputCredentials); + } + + + public void addCredentialHandler(CredentialHandler handler) { + credentialHandlers.add(handler); + } + + public CredentialHandler[] getCredentialHandlers() { + return credentialHandlers.toArray(new CredentialHandler[0]); + } + +} diff --git a/java/org/apache/catalina/realm/NullRealm.java b/java/org/apache/catalina/realm/NullRealm.java new file mode 100644 index 0000000..83e4693 --- /dev/null +++ b/java/org/apache/catalina/realm/NullRealm.java @@ -0,0 +1,38 @@ +/* + * 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.catalina.realm; + +import java.security.Principal; + +/** + * Minimal Realm implementation that always returns null when an attempt is made to validate a user name and password. + * It is intended to be used as a default Realm implementation when no other Realm is specified. + */ +public class NullRealm extends RealmBase { + + @Override + protected String getPassword(String username) { + // Always return null + return null; + } + + @Override + protected Principal getPrincipal(String username) { + // Always return null + return null; + } +} diff --git a/java/org/apache/catalina/realm/RealmBase.java b/java/org/apache/catalina/realm/RealmBase.java new file mode 100644 index 0000000..a71a4cd --- /dev/null +++ b/java/org/apache/catalina/realm/RealmBase.java @@ -0,0 +1,1593 @@ +/* + * 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.catalina.realm; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import jakarta.servlet.annotation.ServletSecurity.TransportGuarantee; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.CredentialHandler; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Realm; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.catalina.util.SessionConfig; +import org.apache.catalina.util.ToStringUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSName; + +/** + * Simple implementation of Realm that reads an XML file to configure the valid users, passwords, and roles. The + * file format (and default file location) are identical to those currently supported by Tomcat 3.X. + * + * @author Craig R. McClanahan + */ +public abstract class RealmBase extends LifecycleMBeanBase implements Realm { + + private static final Log log = LogFactory.getLog(RealmBase.class); + + /** + * The character used for delimiting user attribute names. + *

    + * Applies to some of the Realm implementations only. + */ + protected static final String USER_ATTRIBUTES_DELIMITER = ","; + + /** + * The character used as wildcard in user attribute lists. Using it means query all available user + * attributes. + *

    + * Applies to some of the Realm implementations only. + */ + protected static final String USER_ATTRIBUTES_WILDCARD = "*"; + + private static final List> credentialHandlerClasses = + new ArrayList<>(); + + static { + // Order is important since it determines the search order for a + // matching handler if only an algorithm is specified when calling + // main() + credentialHandlerClasses.add(MessageDigestCredentialHandler.class); + credentialHandlerClasses.add(SecretKeyCredentialHandler.class); + } + + // ----------------------------------------------------- Instance Variables + + + /** + * The Container with which this Realm is associated. + */ + protected Container container = null; + + + /** + * Container log + */ + protected Log containerLog = null; + + + private CredentialHandler credentialHandler; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(RealmBase.class); + + + /** + * The property change support for this component. + */ + protected final PropertyChangeSupport support = new PropertyChangeSupport(this); + + + /** + * Should we validate client certificate chains when they are presented? + */ + protected boolean validate = true; + + /** + * The name of the class to use for retrieving user names from X509 certificates. + */ + protected String x509UsernameRetrieverClassName; + + /** + * The object that will extract user names from X509 client certificates. + */ + protected X509UsernameRetriever x509UsernameRetriever; + + /** + * The all role mode. + */ + protected AllRolesMode allRolesMode = AllRolesMode.STRICT_MODE; + + + /** + * When processing users authenticated via the GSS-API, should any "@..." be stripped from the end of the + * user name? + */ + protected boolean stripRealmForGss = true; + + + private int transportGuaranteeRedirectStatus = HttpServletResponse.SC_FOUND; + + + /** + * The comma separated names of user attributes to additionally query from the realm. These will be provided to the + * user through the created Principal's attributes map. Support for this feature is optional. + */ + protected String userAttributes = null; + + + /** + * The list of user attributes to additionally query from the realm. These will be provided to the user through the + * created Principal's attributes map. Support for this feature is optional. + */ + protected List userAttributesList = null; + + + // ------------------------------------------------------------- Properties + + /** + * @return The HTTP status code used when the container needs to issue an HTTP redirect to meet the requirements of + * a configured transport guarantee. + */ + public int getTransportGuaranteeRedirectStatus() { + return transportGuaranteeRedirectStatus; + } + + + /** + * Set the HTTP status code used when the container needs to issue an HTTP redirect to meet the requirements of a + * configured transport guarantee. + * + * @param transportGuaranteeRedirectStatus The status to use. This value is not validated + */ + public void setTransportGuaranteeRedirectStatus(int transportGuaranteeRedirectStatus) { + this.transportGuaranteeRedirectStatus = transportGuaranteeRedirectStatus; + } + + + @Override + public CredentialHandler getCredentialHandler() { + return credentialHandler; + } + + + @Override + public void setCredentialHandler(CredentialHandler credentialHandler) { + this.credentialHandler = credentialHandler; + } + + + @Override + public Container getContainer() { + return container; + } + + + @Override + public void setContainer(Container container) { + + Container oldContainer = this.container; + this.container = container; + support.firePropertyChange("container", oldContainer, this.container); + + } + + /** + * Return the all roles mode. + * + * @return A string representation of the current all roles mode + */ + public String getAllRolesMode() { + return allRolesMode.toString(); + } + + + /** + * Set the all roles mode. + * + * @param allRolesMode A string representation of the new all roles mode + */ + public void setAllRolesMode(String allRolesMode) { + this.allRolesMode = AllRolesMode.toMode(allRolesMode); + } + + + /** + * Return the "validate certificate chains" flag. + * + * @return The value of the validate certificate chains flag + */ + public boolean getValidate() { + return validate; + } + + + /** + * Set the "validate certificate chains" flag. + * + * @param validate The new validate certificate chains flag + */ + public void setValidate(boolean validate) { + + this.validate = validate; + + } + + /** + * Gets the name of the class that will be used to extract user names from X509 client certificates. + * + * @return The name of the class that will be used to extract user names from X509 client certificates. + */ + public String getX509UsernameRetrieverClassName() { + return x509UsernameRetrieverClassName; + } + + /** + * Sets the name of the class that will be used to extract user names from X509 client certificates. The class must + * implement X509UsernameRetriever. + * + * @param className The name of the class that will be used to extract user names from X509 client certificates. + * + * @see X509UsernameRetriever + */ + public void setX509UsernameRetrieverClassName(String className) { + this.x509UsernameRetrieverClassName = className; + } + + public boolean isStripRealmForGss() { + return stripRealmForGss; + } + + + public void setStripRealmForGss(boolean stripRealmForGss) { + this.stripRealmForGss = stripRealmForGss; + } + + + /** + * @return the comma separated names of user attributes to additionally query from realm + */ + public String getUserAttributes() { + return userAttributes; + } + + /** + * Set the comma separated names of user attributes to additionally query from the realm. These will be provided to + * the user through the created Principal's attributes map. In this map, each field value is bound to the + * field's name, that is, the name of the field serves as the key of the mapping. + *

    + * If set to the wildcard character, or, if the wildcard character is part of the comma separated list, all + * available attributes - except the password attribute (as specified by userCredCol) - are + * queried. The wildcard character is defined by constant {@link RealmBase#USER_ATTRIBUTES_WILDCARD}. It defaults to + * the asterisk (*) character. + * + * @param userAttributes the comma separated names of user attributes + */ + public void setUserAttributes(String userAttributes) { + this.userAttributes = userAttributes; + } + + // --------------------------------------------------------- Public Methods + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + + @Override + public Principal authenticate(String username) { + + if (username == null) { + return null; + } + + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("realmBase.authenticateSuccess", username)); + } + + return getPrincipal(username); + } + + + @Override + public Principal authenticate(String username, String credentials) { + // No user or no credentials + // Can't possibly authenticate, don't bother doing anything. + if (username == null || credentials == null) { + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("realmBase.authenticateFailure", username)); + } + return null; + } + + // Look up the user's credentials + String serverCredentials = getPassword(username); + + if (serverCredentials == null) { + // User was not found + // Waste a bit of time as not to reveal that the user does not exist. + getCredentialHandler().mutate(credentials); + + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("realmBase.authenticateFailure", username)); + } + return null; + } + + boolean validated = getCredentialHandler().matches(credentials, serverCredentials); + + if (validated) { + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("realmBase.authenticateSuccess", username)); + } + return getPrincipal(username); + } else { + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("realmBase.authenticateFailure", username)); + } + return null; + } + } + + + @Deprecated + @Override + public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, + String qop, String realm, String digestA2) { + return authenticate(username, clientDigest, nonce, nc, cnonce, qop, realm, digestA2, "MD5"); + } + + + @Override + public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, + String qop, String realm, String digestA2, String algorithm) { + + // In digest auth, digests are always lower case + String digestA1 = getDigest(username, realm, algorithm); + if (digestA1 == null) { + return null; + } + digestA1 = digestA1.toLowerCase(Locale.ENGLISH); + String serverDigestValue; + if (qop == null) { + serverDigestValue = digestA1 + ":" + nonce + ":" + digestA2; + } else { + serverDigestValue = digestA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + digestA2; + } + + byte[] valueBytes = null; + try { + valueBytes = serverDigestValue.getBytes(getDigestCharset()); + } catch (UnsupportedEncodingException uee) { + throw new IllegalArgumentException(sm.getString("realmBase.invalidDigestEncoding", getDigestEncoding()), + uee); + } + + String serverDigest = HexUtils.toHexString(ConcurrentMessageDigest.digest(algorithm, valueBytes)); + + if (log.isTraceEnabled()) { + log.trace("Digest : " + clientDigest + " Username:" + username + " ClientDigest:" + clientDigest + + " nonce:" + nonce + " nc:" + nc + " cnonce:" + cnonce + " qop:" + qop + " realm:" + realm + + "digestA2:" + digestA2 + " Server digest:" + serverDigest); + } + + if (serverDigest.equals(clientDigest)) { + return getPrincipal(username); + } + + return null; + } + + + @Override + public Principal authenticate(X509Certificate certs[]) { + + if ((certs == null) || (certs.length < 1)) { + return null; + } + + // Check the validity of each certificate in the chain + if (log.isTraceEnabled()) { + log.trace("Authenticating client certificate chain"); + } + if (validate) { + for (X509Certificate cert : certs) { + if (log.isTraceEnabled()) { + log.trace(" Checking validity for '" + cert.getSubjectX500Principal().toString() + "'"); + } + try { + cert.checkValidity(); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("realmBase.validity"), e); + } + return null; + } + } + } + + // Check the existence of the client Principal in our database + return getPrincipal(certs[0]); + } + + + @Override + public Principal authenticate(GSSContext gssContext, boolean storeCred) { + if (gssContext.isEstablished()) { + GSSName gssName = null; + try { + gssName = gssContext.getSrcName(); + } catch (GSSException e) { + log.warn(sm.getString("realmBase.gssNameFail"), e); + } + + if (gssName != null) { + GSSCredential gssCredential = null; + if (storeCred) { + if (gssContext.getCredDelegState()) { + try { + gssCredential = gssContext.getDelegCred(); + } catch (GSSException e) { + log.warn(sm.getString("realmBase.delegatedCredentialFail", gssName), e); + } + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("realmBase.credentialNotDelegated", gssName)); + } + } + } + + return getPrincipal(gssName, gssCredential); + } + } else { + log.error(sm.getString("realmBase.gssContextNotEstablished")); + } + + // Fail in all other cases + return null; + } + + + @Override + public Principal authenticate(GSSName gssName, GSSCredential gssCredential) { + if (gssName == null) { + return null; + } + + return getPrincipal(gssName, gssCredential); + } + + + /** + * {@inheritDoc} + *

    + * The default implementation is NO-OP. + */ + @Override + public void backgroundProcess() { + // NOOP in base class + } + + + @Override + public SecurityConstraint[] findSecurityConstraints(Request request, Context context) { + + ArrayList results = null; + // Are there any defined security constraints? + SecurityConstraint constraints[] = context.findConstraints(); + if (constraints == null || constraints.length == 0) { + if (log.isTraceEnabled()) { + log.trace(" No applicable constraints defined"); + } + return null; + } + + // Check each defined security constraint + String uri = request.getRequestPathMB().toString(); + // Bug47080 - in rare cases this may be null or "" + // Mapper treats as '/' do the same to prevent NPE + if (uri == null || uri.length() == 0) { + uri = "/"; + } + + String method = request.getMethod(); + int i; + boolean found = false; + for (i = 0; i < constraints.length; i++) { + SecurityCollection[] collections = constraints[i].findCollections(); + + // If collection is null, continue to avoid an NPE + // See Bugzilla 30624 + if (collections == null) { + continue; + } + + if (log.isTraceEnabled()) { + log.trace(" Checking constraint '" + constraints[i] + "' against " + method + " " + uri + " --> " + + constraints[i].included(uri, method)); + } + + for (SecurityCollection securityCollection : collections) { + String[] patterns = securityCollection.findPatterns(); + + // If patterns is null, continue to avoid an NPE + // See Bugzilla 30624 + if (patterns == null) { + continue; + } + + for (String pattern : patterns) { + // Exact match including special case for the context root. + if (uri.equals(pattern) || pattern.length() == 0 && uri.equals("/")) { + found = true; + if (securityCollection.findMethod(method)) { + if (results == null) { + results = new ArrayList<>(); + } + results.add(constraints[i]); + } + } + } + } + } + + if (found) { + return resultsToArray(results); + } + + int longest = -1; + + for (i = 0; i < constraints.length; i++) { + SecurityCollection[] collection = constraints[i].findCollections(); + + // If collection is null, continue to avoid an NPE + // See Bugzilla 30624 + if (collection == null) { + continue; + } + + if (log.isTraceEnabled()) { + log.trace(" Checking constraint '" + constraints[i] + "' against " + method + " " + uri + " --> " + + constraints[i].included(uri, method)); + } + + for (SecurityCollection securityCollection : collection) { + String[] patterns = securityCollection.findPatterns(); + + // If patterns is null, continue to avoid an NPE + // See Bugzilla 30624 + if (patterns == null) { + continue; + } + + boolean matched = false; + int length = -1; + for (String pattern : patterns) { + if (pattern.startsWith("/") && pattern.endsWith("/*") && pattern.length() >= longest) { + + if (pattern.length() == 2) { + matched = true; + length = pattern.length(); + } else if (pattern.regionMatches(0, uri, 0, pattern.length() - 1) || + (pattern.length() - 2 == uri.length() && + pattern.regionMatches(0, uri, 0, pattern.length() - 2))) { + matched = true; + length = pattern.length(); + } + } + } + if (matched) { + if (length > longest) { + found = false; + if (results != null) { + results.clear(); + } + longest = length; + } + if (securityCollection.findMethod(method)) { + found = true; + if (results == null) { + results = new ArrayList<>(); + } + results.add(constraints[i]); + } + } + } + } + + if (found) { + return resultsToArray(results); + } + + for (i = 0; i < constraints.length; i++) { + SecurityCollection[] collection = constraints[i].findCollections(); + + // If collection is null, continue to avoid an NPE + // See Bugzilla 30624 + if (collection == null) { + continue; + } + + if (log.isTraceEnabled()) { + log.trace(" Checking constraint '" + constraints[i] + "' against " + method + " " + uri + " --> " + + constraints[i].included(uri, method)); + } + + boolean matched = false; + int pos = -1; + for (int j = 0; j < collection.length; j++) { + String[] patterns = collection[j].findPatterns(); + + // If patterns is null, continue to avoid an NPE + // See Bugzilla 30624 + if (patterns == null) { + continue; + } + + for (int k = 0; k < patterns.length && !matched; k++) { + String pattern = patterns[k]; + if (pattern.startsWith("*.")) { + int slash = uri.lastIndexOf('/'); + int dot = uri.lastIndexOf('.'); + if (slash >= 0 && dot > slash && dot != uri.length() - 1 && + uri.length() - dot == pattern.length() - 1) { + if (pattern.regionMatches(1, uri, dot, uri.length() - dot)) { + matched = true; + pos = j; + } + } + } + } + } + if (matched) { + found = true; + if (collection[pos].findMethod(method)) { + if (results == null) { + results = new ArrayList<>(); + } + results.add(constraints[i]); + } + } + } + + if (found) { + return resultsToArray(results); + } + + for (i = 0; i < constraints.length; i++) { + SecurityCollection[] collection = constraints[i].findCollections(); + + // If collection is null, continue to avoid an NPE + // See Bugzilla 30624 + if (collection == null) { + continue; + } + + if (log.isTraceEnabled()) { + log.trace(" Checking constraint '" + constraints[i] + "' against " + method + " " + uri + " --> " + + constraints[i].included(uri, method)); + } + + for (SecurityCollection securityCollection : collection) { + String[] patterns = securityCollection.findPatterns(); + + // If patterns is null, continue to avoid an NPE + // See Bugzilla 30624 + if (patterns == null) { + continue; + } + + boolean matched = false; + for (String pattern : patterns) { + if (pattern.equals("/")) { + matched = true; + break; + } + } + if (matched) { + if (results == null) { + results = new ArrayList<>(); + } + results.add(constraints[i]); + } + } + } + + if (results == null) { + // No applicable security constraint was found + if (log.isTraceEnabled()) { + log.trace(" No applicable constraint located"); + } + } + return resultsToArray(results); + } + + /** + * Convert an ArrayList to a SecurityConstraint []. + */ + private SecurityConstraint[] resultsToArray(ArrayList results) { + if (results == null || results.size() == 0) { + return null; + } + return results.toArray(new SecurityConstraint[0]); + } + + + @Override + public boolean hasResourcePermission(Request request, Response response, SecurityConstraint[] constraints, + Context context) throws IOException { + + if (constraints == null || constraints.length == 0) { + return true; + } + + // Which user principal have we already authenticated? + Principal principal = request.getPrincipal(); + boolean status = false; + boolean denyfromall = false; + for (SecurityConstraint constraint : constraints) { + String roles[]; + if (constraint.getAllRoles()) { + // * means all roles defined in web.xml + roles = request.getContext().findSecurityRoles(); + } else { + roles = constraint.findAuthRoles(); + } + + if (roles == null) { + roles = new String[0]; + } + + if (log.isTraceEnabled()) { + log.trace(" Checking roles " + principal); + } + + if (constraint.getAuthenticatedUsers() && principal != null) { + if (log.isTraceEnabled()) { + log.trace("Passing all authenticated users"); + } + status = true; + } else if (roles.length == 0 && !constraint.getAllRoles() && !constraint.getAuthenticatedUsers()) { + if (constraint.getAuthConstraint()) { + if (log.isTraceEnabled()) { + log.trace("No roles"); + } + status = false; // No listed roles means no access at all + denyfromall = true; + break; + } + + if (log.isTraceEnabled()) { + log.trace("Passing all access"); + } + status = true; + } else if (principal == null) { + if (log.isTraceEnabled()) { + log.trace(" No user authenticated, cannot grant access"); + } + } else { + for (String role : roles) { + if (hasRole(request.getWrapper(), principal, role)) { + status = true; + if (log.isTraceEnabled()) { + log.trace("Role found: " + role); + } + } else if (log.isTraceEnabled()) { + log.trace("No role found: " + role); + } + } + } + } + + if (!denyfromall && allRolesMode != AllRolesMode.STRICT_MODE && !status && principal != null) { + if (log.isTraceEnabled()) { + log.trace("Checking for all roles mode: " + allRolesMode); + } + // Check for an all roles(role-name="*") + for (SecurityConstraint constraint : constraints) { + String roles[]; + // If the all roles mode exists, sets + if (constraint.getAllRoles()) { + if (allRolesMode == AllRolesMode.AUTH_ONLY_MODE) { + if (log.isTraceEnabled()) { + log.trace("Granting access for role-name=*, auth-only"); + } + status = true; + break; + } + + // For AllRolesMode.STRICT_AUTH_ONLY_MODE there must be zero roles + roles = request.getContext().findSecurityRoles(); + if (roles == null) { + roles = new String[0]; + } + if (roles.length == 0 && allRolesMode == AllRolesMode.STRICT_AUTH_ONLY_MODE) { + if (log.isTraceEnabled()) { + log.trace("Granting access for role-name=*, strict auth-only"); + } + status = true; + break; + } + } + } + } + + // Return a "Forbidden" message denying access to this resource + if (!status) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, sm.getString("realmBase.forbidden")); + } + return status; + + } + + + /** + * {@inheritDoc} + *

    + * This method or {@link #hasRoleInternal(Principal, String)} can be overridden by Realm implementations, but the + * default is adequate when an instance of GenericPrincipal is used to represent authenticated + * Principals from this Realm. + */ + @Override + public boolean hasRole(Wrapper wrapper, Principal principal, String role) { + // Check for a role alias + if (wrapper != null) { + String realRole = wrapper.findSecurityReference(role); + if (realRole != null) { + role = realRole; + } + } + + // Should be overridden in JAASRealm - to avoid pretty inefficient conversions + if (principal == null || role == null) { + return false; + } + + boolean result = hasRoleInternal(principal, role); + + if (log.isTraceEnabled()) { + String name = principal.getName(); + if (result) { + log.trace(sm.getString("realmBase.hasRoleSuccess", name, role)); + } else { + log.trace(sm.getString("realmBase.hasRoleFailure", name, role)); + } + } + + return result; + } + + + /** + * Parse the specified delimiter separated attribute names and return a list of that names or null, if + * no attributes have been specified. + *

    + * If a wildcard character is found, return a list consisting of a single wildcard character only. + * + * @param userAttributes comma separated names of attributes to parse + * + * @return a list containing the parsed attribute names or null, if no attributes have been specified + */ + protected List parseUserAttributes(String userAttributes) { + if (userAttributes == null) { + return null; + } + List attrs = new ArrayList<>(); + for (String name : userAttributes.split(USER_ATTRIBUTES_DELIMITER)) { + name = name.trim(); + if (name.length() == 0) { + continue; + } + if (name.equals(USER_ATTRIBUTES_WILDCARD)) { + return Collections.singletonList(USER_ATTRIBUTES_WILDCARD); + } + if (attrs.contains(name)) { + // skip duplicates + continue; + } + attrs.add(name); + } + return attrs.size() > 0 ? attrs : null; + } + + + /** + * Check if the specified Principal has the specified security role, within the context of this Realm. This method + * or {@link #hasRoleInternal(Principal, String)} can be overridden by Realm implementations, but the default is + * adequate when an instance of GenericPrincipal is used to represent authenticated Principals from + * this Realm. + * + * @param principal Principal for whom the role is to be checked + * @param role Security role to be checked + * + * @return true if the specified Principal has the specified security role, within the context of this + * Realm; otherwise return false. + */ + protected boolean hasRoleInternal(Principal principal, String role) { + // Should be overridden in JAASRealm - to avoid pretty inefficient conversions + if (!(principal instanceof GenericPrincipal)) { + return false; + } + + GenericPrincipal gp = (GenericPrincipal) principal; + return gp.hasRole(role); + } + + + @Override + public boolean hasUserDataPermission(Request request, Response response, SecurityConstraint[] constraints) + throws IOException { + + // Is there a relevant user data constraint? + if (constraints == null || constraints.length == 0) { + if (log.isTraceEnabled()) { + log.trace(" No applicable security constraint defined"); + } + return true; + } + for (SecurityConstraint constraint : constraints) { + String userConstraint = constraint.getUserConstraint(); + if (userConstraint == null) { + if (log.isTraceEnabled()) { + log.trace(" No applicable user data constraint defined"); + } + return true; + } + if (userConstraint.equals(TransportGuarantee.NONE.name())) { + if (log.isTraceEnabled()) { + log.trace(" User data constraint has no restrictions"); + } + return true; + } + + } + // Validate the request against the user data constraint + if (request.getRequest().isSecure()) { + if (log.isTraceEnabled()) { + log.trace(" User data constraint already satisfied"); + } + return true; + } + // Initialize variables we need to determine the appropriate action + int redirectPort = request.getConnector().getRedirectPortWithOffset(); + + // Is redirecting disabled? + if (redirectPort <= 0) { + if (log.isTraceEnabled()) { + log.trace(" SSL redirect is disabled"); + } + response.sendError(HttpServletResponse.SC_FORBIDDEN, request.getRequestURI()); + return false; + } + + // Redirect to the corresponding SSL port + StringBuilder file = new StringBuilder(); + String protocol = "https"; + String host = request.getServerName(); + // Protocol + file.append(protocol).append("://").append(host); + // Host with port + if (redirectPort != 443) { + file.append(':').append(redirectPort); + } + // URI + file.append(request.getRequestURI()); + String requestedSessionId = request.getRequestedSessionId(); + if ((requestedSessionId != null) && request.isRequestedSessionIdFromURL()) { + file.append(';'); + file.append(SessionConfig.getSessionUriParamName(request.getContext())); + file.append('='); + file.append(requestedSessionId); + } + String queryString = request.getQueryString(); + if (queryString != null) { + file.append('?'); + file.append(queryString); + } + if (log.isTraceEnabled()) { + log.trace(" Redirecting to " + file.toString()); + } + response.sendRedirect(file.toString(), transportGuaranteeRedirectStatus); + return false; + + } + + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + support.removePropertyChangeListener(listener); + } + + + @Override + protected void initInternal() throws LifecycleException { + + super.initInternal(); + + // We want logger as soon as possible + if (container != null) { + this.containerLog = container.getLogger(); + } + + x509UsernameRetriever = createUsernameRetriever(x509UsernameRetrieverClassName); + } + + + /** + * Prepare for the beginning of active use of the public methods of this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + if (credentialHandler == null) { + credentialHandler = new MessageDigestCredentialHandler(); + } + if (userAttributes != null) { + userAttributesList = parseUserAttributes(userAttributes); + } + setState(LifecycleState.STARTING); + } + + + /** + * Gracefully terminate the active use of the public methods of this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that needs to be reported + */ + @Override + protected void stopInternal() throws LifecycleException { + setState(LifecycleState.STOPPING); + } + + + @Override + public String toString() { + return ToStringUtil.toString(this); + } + + + // ------------------------------------------------------ Protected Methods + + protected boolean hasMessageDigest(String algorithm) { + CredentialHandler ch = credentialHandler; + if (ch instanceof MessageDigestCredentialHandler) { + String realmAlgorithm = ((MessageDigestCredentialHandler) ch).getAlgorithm(); + if (realmAlgorithm != null) { + if (realmAlgorithm.equals(algorithm)) { + return true; + } else { + log.debug(sm.getString("relamBase.digestMismatch", algorithm, realmAlgorithm)); + } + } + } + return false; + } + + + /** + * Return the digest associated with given principal's user name. + * + * @param username The user name + * @param realmName The realm name + * + * @return the digest for the specified user + * + * @deprecated Unused. Use {@link #getDigest(String, String, String)}. Will be removed in Tomcat 11. + */ + @Deprecated + protected String getDigest(String username, String realmName) { + return getDigest(username, realmName, "MD5"); + } + + + /** + * Return the digest associated with given principal's user name. + * + * @param username The user name + * @param realmName The realm name + * @param algorithm The name of the message digest algorithm to use + * + * @return the digest for the specified user + */ + protected String getDigest(String username, String realmName, String algorithm) { + if (hasMessageDigest(algorithm)) { + // Use pre-generated digest + return getPassword(username); + } + + String digestValue = username + ":" + realmName + ":" + getPassword(username); + + byte[] valueBytes = null; + try { + valueBytes = digestValue.getBytes(getDigestCharset()); + } catch (UnsupportedEncodingException uee) { + throw new IllegalArgumentException(sm.getString("realmBase.invalidDigestEncoding", getDigestEncoding()), + uee); + } + + return HexUtils.toHexString(ConcurrentMessageDigest.digest(algorithm, valueBytes)); + } + + + private String getDigestEncoding() { + CredentialHandler ch = credentialHandler; + if (ch instanceof MessageDigestCredentialHandler) { + return ((MessageDigestCredentialHandler) ch).getEncoding(); + } + return null; + } + + + private Charset getDigestCharset() throws UnsupportedEncodingException { + String charset = getDigestEncoding(); + if (charset == null) { + return StandardCharsets.ISO_8859_1; + } else { + return B2CConverter.getCharset(charset); + } + } + + + /** + * Get the password for the specified user. + * + * @param username The user name + * + * @return the password associated with the given principal's user name. + */ + protected abstract String getPassword(String username); + + + /** + * Get the principal associated with the specified certificate. + * + * @param usercert The user certificate + * + * @return the Principal associated with the given certificate. + */ + protected Principal getPrincipal(X509Certificate usercert) { + String username = x509UsernameRetriever.getUsername(usercert); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("realmBase.gotX509Username", username)); + } + + return getPrincipal(username); + } + + + /** + * Get the principal associated with the specified user. + * + * @param username The user name + * + * @return the Principal associated with the given user name. + */ + protected abstract Principal getPrincipal(String username); + + + /** + * Get the principal associated with the specified {@link GSSName}. + * + * @param gssName The GSS name + * @param gssCredential the GSS credential of the principal + * + * @return the principal associated with the given user name. + */ + protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) { + String name = gssName.toString(); + + if (isStripRealmForGss()) { + int i = name.indexOf('@'); + if (i > 0) { + // Zero so we don't leave a zero length name + name = name.substring(0, i); + } + } + + Principal p = getPrincipal(name); + + if (p instanceof GenericPrincipal) { + ((GenericPrincipal) p).setGssCredential(gssCredential); + } + + return p; + } + + + /** + * Return the Server object that is the ultimate parent for the container with which this Realm is associated. If + * the server cannot be found (eg because the container hierarchy is not complete), null is returned. + * + * @return the Server associated with the realm + */ + protected Server getServer() { + Container c = container; + if (c instanceof Context) { + c = c.getParent(); + } + if (c instanceof Host) { + c = c.getParent(); + } + if (c instanceof Engine) { + Service s = ((Engine) c).getService(); + if (s != null) { + return s.getServer(); + } + } + return null; + } + + + // --------------------------------------------------------- Static Methods + + /** + * Generate a stored credential string for the given password and associated parameters. + *

    + * The following parameters are supported: + *

    + *
      + *
    • -a - The algorithm to use to generate the stored credential. If not specified a default of SHA-512 + * will be used.
    • + *
    • -e - The encoding to use for any byte to/from character conversion that may be necessary. If not + * specified, the system encoding ({@link Charset#defaultCharset()}) will be used.
    • + *
    • -i - The number of iterations to use when generating the stored credential. If not specified, the + * default for the CredentialHandler will be used.
    • + *
    • -s - The length (in bytes) of salt to generate and store as part of the credential. If not specified, + * the default for the CredentialHandler will be used.
    • + *
    • -k - The length (in bits) of the key(s), if any, created while generating the credential. If not + * specified, the default for the CredentialHandler will be used.
    • + *
    • -h - The fully qualified class name of the CredentialHandler to use. If not specified, the built-in + * handlers will be tested in turn and the first one to accept the specified algorithm will be used.
    • + *
    • -f - The name of the file that contains passwords to encode. Each + * line in the file should contain only one password. Using this + * option ignores other password input.
    • + *
    + *

    + * This generation process currently supports the following CredentialHandlers, the correct one being selected based + * on the algorithm specified: + *

    + *
      + *
    • {@link MessageDigestCredentialHandler}
    • + *
    • {@link SecretKeyCredentialHandler}
    • + *
    + * + * @param args The parameters passed on the command line + * + * @throws IOException If an error occurs reading the password file + */ + public static void main(String args[]) throws IOException { + + // Use negative values since null is not an option to indicate 'not set' + int saltLength = -1; + int iterations = -1; + int keyLength = -1; + // Default + String encoding = Charset.defaultCharset().name(); + // Default values for these depend on whether either of them are set on + // the command line + String algorithm = null; + String handlerClassName = null; + // File name to read password(s) from + String passwordFile = null; + + if (args.length == 0) { + usage(); + return; + } + + int argIndex = 0; + // Boolean to check and see if we've reached the -- option + boolean endOfList = false; + + // Note: Reducing args.length requirement to argIndex+1 so that -f works and ignores + // trailing words + while (args.length > argIndex + 1 && args[argIndex].length() == 2 && args[argIndex].charAt(0) == '-' && !endOfList) { + switch (args[argIndex].charAt(1)) { + case 'a': { + algorithm = args[argIndex + 1]; + break; + } + case 'e': { + encoding = args[argIndex + 1]; + break; + } + case 'i': { + iterations = Integer.parseInt(args[argIndex + 1]); + break; + } + case 's': { + saltLength = Integer.parseInt(args[argIndex + 1]); + break; + } + case 'k': { + keyLength = Integer.parseInt(args[argIndex + 1]); + break; + } + case 'h': { + handlerClassName = args[argIndex + 1]; + break; + } + case 'f': { + passwordFile = args[argIndex + 1]; + break; + } + case '-': { + // When encountering -- option don't parse anything else as an option + endOfList = true; + // The -- opt doesn't take an argument, decrement the argIndex so that it parses + // all remaining args + argIndex--; + break; + } + default: { + usage(); + return; + } + } + argIndex += 2; + } + + // Determine defaults for -a and -h. The rules are more complex to + // express than the implementation: + // - if neither -a nor -h is set, use SHA-512 and + // MessageDigestCredentialHandler + // - if only -a is set the built-in handlers will be searched in order + // (MessageDigestCredentialHandler, SecretKeyCredentialHandler) and + // the first handler that supports the algorithm will be used + // - if only -h is set no default will be used for -a. The handler may + // or may nor support -a and may or may not supply a sensible default + if (algorithm == null && handlerClassName == null) { + algorithm = "SHA-512"; + } + + CredentialHandler handler = null; + + if (handlerClassName == null) { + for (Class clazz : credentialHandlerClasses) { + try { + handler = clazz.getConstructor().newInstance(); + if (IntrospectionUtils.setProperty(handler, "algorithm", algorithm)) { + break; + } + } catch (ReflectiveOperationException e) { + // This isn't good. + throw new RuntimeException(e); + } + } + } else { + try { + Class clazz = Class.forName(handlerClassName); + handler = (DigestCredentialHandlerBase) clazz.getConstructor().newInstance(); + IntrospectionUtils.setProperty(handler, "algorithm", algorithm); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + if (handler == null) { + throw new RuntimeException(new NoSuchAlgorithmException(algorithm)); + } + + IntrospectionUtils.setProperty(handler, "encoding", encoding); + if (iterations > 0) { + IntrospectionUtils.setProperty(handler, "iterations", Integer.toString(iterations)); + } + if (saltLength > -1) { + IntrospectionUtils.setProperty(handler, "saltLength", Integer.toString(saltLength)); + } + if (keyLength > 0) { + IntrospectionUtils.setProperty(handler, "keyLength", Integer.toString(keyLength)); + } + if (passwordFile != null) { + // If the file name is used, then don't parse the trailing arguments + argIndex = args.length; + + try { + BufferedReader br; + // Special case, allow for - filename to refer to stdin + if (passwordFile.equals("-")) { + br = new BufferedReader(new InputStreamReader(System.in)); + } else { + br = new BufferedReader(new FileReader(passwordFile)); + } + + String line; + while ((line = br.readLine()) != null) { + // Mutate each line in the file, or stdin + mutateCredential(line, handler); + } + } catch (Exception e) { + // A FileNotFound is the likely exception here and self-explanatory. Softly + // reporting it and exit 1 so that you can tell it failed from the command line. + if (e instanceof java.io.FileNotFoundException) { + System.err.println("cannot stat '" + passwordFile + + "': No such file or directory"); + // Not sure if using an exit here is OK, but I wanted to return a code that + // showed failure. + System.exit(1); + } else { + throw e; + } + } + } + for (; argIndex < args.length; argIndex++) { + mutateCredential(args[argIndex], handler); + } + } + + private static void mutateCredential(String credential, CredentialHandler handler) { + System.out.print(credential + ":"); + System.out.println(handler.mutate(credential)); + } + + private static void usage() { + System.out.println("Usage: RealmBase [-a ] [-e ]" + + " [-i ] [-s ] [-k ]" + + " [-h ] | "); + } + + + // -------------------- JMX and Registration -------------------- + + @Override + public String getObjectNameKeyProperties() { + + StringBuilder keyProperties = new StringBuilder("type=Realm"); + keyProperties.append(getRealmSuffix()); + keyProperties.append(container.getMBeanKeyProperties()); + + return keyProperties.toString(); + } + + @Override + public String getDomainInternal() { + return container.getDomain(); + } + + protected String realmPath = "/realm0"; + + public String getRealmPath() { + return realmPath; + } + + public void setRealmPath(String theRealmPath) { + realmPath = theRealmPath; + } + + protected String getRealmSuffix() { + return ",realmPath=" + getRealmPath(); + } + + + protected static class AllRolesMode { + + private final String name; + /** + * Use the strict servlet spec interpretation which requires that the user have one of the + * web-app/security-role/role-name + */ + public static final AllRolesMode STRICT_MODE = new AllRolesMode("strict"); + /** Allow any authenticated user */ + public static final AllRolesMode AUTH_ONLY_MODE = new AllRolesMode("authOnly"); + /** + * Allow any authenticated user only if there are no web-app/security-roles + */ + public static final AllRolesMode STRICT_AUTH_ONLY_MODE = new AllRolesMode("strictAuthOnly"); + + static AllRolesMode toMode(String name) { + AllRolesMode mode; + if (name.equalsIgnoreCase(STRICT_MODE.name)) { + mode = STRICT_MODE; + } else if (name.equalsIgnoreCase(AUTH_ONLY_MODE.name)) { + mode = AUTH_ONLY_MODE; + } else if (name.equalsIgnoreCase(STRICT_AUTH_ONLY_MODE.name)) { + mode = STRICT_AUTH_ONLY_MODE; + } else { + throw new IllegalStateException(sm.getString("realmBase.unknownAllRolesMode", name)); + } + return mode; + } + + private AllRolesMode(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + boolean equals = false; + if (o instanceof AllRolesMode) { + AllRolesMode mode = (AllRolesMode) o; + equals = name.equals(mode.name); + } + return equals; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return name; + } + } + + private static X509UsernameRetriever createUsernameRetriever(String className) throws LifecycleException { + if (null == className || className.trim().isEmpty()) { + return new X509SubjectDnRetriever(); + } + + try { + @SuppressWarnings("unchecked") + Class clazz = + (Class) Class.forName(className); + return clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new LifecycleException(sm.getString("realmBase.createUsernameRetriever.newInstance", className), e); + } catch (ClassCastException e) { + throw new LifecycleException( + sm.getString("realmBase.createUsernameRetriever.ClassCastException", className), e); + } + } +} diff --git a/java/org/apache/catalina/realm/SecretKeyCredentialHandler.java b/java/org/apache/catalina/realm/SecretKeyCredentialHandler.java new file mode 100644 index 0000000..de81d47 --- /dev/null +++ b/java/org/apache/catalina/realm/SecretKeyCredentialHandler.java @@ -0,0 +1,105 @@ +/* + * 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.catalina.realm; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.HexUtils; + +public class SecretKeyCredentialHandler extends DigestCredentialHandlerBase { + + private static final Log log = LogFactory.getLog(SecretKeyCredentialHandler.class); + + public static final String DEFAULT_ALGORITHM = "PBKDF2WithHmacSHA1"; + public static final int DEFAULT_KEY_LENGTH = 160; + public static final int DEFAULT_ITERATIONS = 20000; + + + private SecretKeyFactory secretKeyFactory; + private int keyLength = DEFAULT_KEY_LENGTH; + + + public SecretKeyCredentialHandler() throws NoSuchAlgorithmException { + setAlgorithm(DEFAULT_ALGORITHM); + } + + + @Override + public String getAlgorithm() { + return secretKeyFactory.getAlgorithm(); + } + + + @Override + public void setAlgorithm(String algorithm) throws NoSuchAlgorithmException { + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm); + this.secretKeyFactory = secretKeyFactory; + } + + + public int getKeyLength() { + return keyLength; + } + + + public void setKeyLength(int keyLength) { + this.keyLength = keyLength; + } + + + @Override + public boolean matches(String inputCredentials, String storedCredentials) { + return matchesSaltIterationsEncoded(inputCredentials, storedCredentials); + } + + + @Override + protected String mutate(String inputCredentials, byte[] salt, int iterations) { + return mutate(inputCredentials, salt, iterations, getKeyLength()); + } + + + @Override + protected String mutate(String inputCredentials, byte[] salt, int iterations, int keyLength) { + try { + KeySpec spec = new PBEKeySpec(inputCredentials.toCharArray(), salt, iterations, keyLength); + return HexUtils.toHexString(secretKeyFactory.generateSecret(spec).getEncoded()); + } catch (InvalidKeySpecException | IllegalArgumentException e) { + log.warn(sm.getString("pbeCredentialHandler.invalidKeySpec"), e); + return null; + } + } + + + @Override + protected int getDefaultIterations() { + return DEFAULT_ITERATIONS; + } + + + @Override + protected Log getLog() { + return log; + } +} diff --git a/java/org/apache/catalina/realm/UserDatabaseRealm.java b/java/org/apache/catalina/realm/UserDatabaseRealm.java new file mode 100644 index 0000000..82c6c0d --- /dev/null +++ b/java/org/apache/catalina/realm/UserDatabaseRealm.java @@ -0,0 +1,354 @@ +/* + * 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.catalina.realm; + +import java.io.ObjectStreamException; +import java.security.Principal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.naming.Context; + +import org.apache.catalina.Group; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Role; +import org.apache.catalina.Server; +import org.apache.catalina.User; +import org.apache.catalina.UserDatabase; +import org.apache.naming.ContextBindings; +import org.apache.tomcat.util.ExceptionUtils; + +/** + * Implementation of {@link org.apache.catalina.Realm} that is based on an implementation of {@link UserDatabase} made + * available through the JNDI resources configured for this instance of Catalina. Set the resourceName + * parameter to the JNDI resources name for the configured instance of UserDatabase that we should consult. + * + * @author Craig R. McClanahan + * + * @since 4.1 + */ +public class UserDatabaseRealm extends RealmBase { + + // ----------------------------------------------------- Instance Variables + + /** + * The UserDatabase we will use to authenticate users and identify associated roles. + */ + protected volatile UserDatabase database = null; + private final Object databaseLock = new Object(); + + /** + * The global JNDI name of the UserDatabase resource we will be utilizing. + */ + protected String resourceName = "UserDatabase"; + + /** + * Obtain the UserDatabase from the context (rather than global) JNDI. + */ + private boolean localJndiResource = false; + + /** + * Use a static principal disconnected from the database. This prevents live updates to users and roles having an + * effect on authenticated principals, but reduces use of the database. + */ + private boolean useStaticPrincipal = false; + + + // ------------------------------------------------------------- Properties + + /** + * @return the global JNDI name of the UserDatabase resource we will be using. + */ + public String getResourceName() { + return resourceName; + } + + + /** + * Set the global JNDI name of the UserDatabase resource we will be using. + * + * @param resourceName The new global JNDI name + */ + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + + /** + * @return the useStaticPrincipal flag + */ + public boolean getUseStaticPrincipal() { + return this.useStaticPrincipal; + } + + + /** + * Allows using a static principal disconnected from the user database. + * + * @param useStaticPrincipal the new value + */ + public void setUseStaticPrincipal(boolean useStaticPrincipal) { + this.useStaticPrincipal = useStaticPrincipal; + } + + + /** + * Determines whether this Realm is configured to obtain the associated {@link UserDatabase} from the global JNDI + * context or a local (web application) JNDI context. + * + * @return {@code true} if a local JNDI context will be used, {@code false} if the the global JNDI context will be + * used + */ + public boolean getLocalJndiResource() { + return localJndiResource; + } + + + /** + * Configure whether this Realm obtains the associated {@link UserDatabase} from the global JNDI context or a local + * (web application) JNDI context. + * + * @param localJndiResource {@code true} to use a local JNDI context, {@code false} to use the global JNDI context + */ + public void setLocalJndiResource(boolean localJndiResource) { + this.localJndiResource = localJndiResource; + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Calls {@link UserDatabase#backgroundProcess()}. + */ + @Override + public void backgroundProcess() { + UserDatabase database = getUserDatabase(); + if (database != null) { + database.backgroundProcess(); + } + } + + + @Override + protected String getPassword(String username) { + UserDatabase database = getUserDatabase(); + if (database == null) { + return null; + } + + User user = database.findUser(username); + + if (user == null) { + return null; + } + + return user.getPassword(); + } + + + public static String[] getRoles(User user) { + Set roles = new HashSet<>(); + Iterator uroles = user.getRoles(); + while (uroles.hasNext()) { + Role role = uroles.next(); + roles.add(role.getName()); + } + Iterator groups = user.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + uroles = group.getRoles(); + while (uroles.hasNext()) { + Role role = uroles.next(); + roles.add(role.getName()); + } + } + return roles.toArray(new String[0]); + } + + + @Override + protected Principal getPrincipal(String username) { + UserDatabase database = getUserDatabase(); + if (database == null) { + return null; + } + User user = database.findUser(username); + if (user == null) { + return null; + } else { + if (useStaticPrincipal) { + return new GenericPrincipal(username, Arrays.asList(getRoles(user))); + } else { + return new UserDatabasePrincipal(user, database); + } + } + } + + + /* + * Can't do this in startInternal() with local JNDI as the local JNDI context won't be initialised at this point. + */ + private UserDatabase getUserDatabase() { + // DCL so database MUST be volatile + if (database == null) { + synchronized (databaseLock) { + if (database == null) { + try { + Context context = null; + if (localJndiResource) { + context = ContextBindings.getClassLoader(); + context = (Context) context.lookup("comp/env"); + } else { + Server server = getServer(); + if (server == null) { + containerLog.error(sm.getString("userDatabaseRealm.noNamingContext")); + return null; + } + context = getServer().getGlobalNamingContext(); + } + database = (UserDatabase) context.lookup(resourceName); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + if (containerLog != null) { + containerLog.error(sm.getString("userDatabaseRealm.lookup", resourceName), e); + } + database = null; + } + } + } + } + return database; + } + + + // ------------------------------------------------------ Lifecycle Methods + + @Override + protected void startInternal() throws LifecycleException { + // If the JNDI resource is global, check it here and fail the context + // start if it is not valid. Local JNDI resources can't be validated + // this way because the JNDI context isn't available at Realm start. + if (!localJndiResource) { + UserDatabase database = getUserDatabase(); + if (database == null) { + throw new LifecycleException(sm.getString("userDatabaseRealm.noDatabase", resourceName)); + } + } + + super.startInternal(); + } + + + @Override + protected void stopInternal() throws LifecycleException { + + // Perform normal superclass finalization + super.stopInternal(); + + // Release reference to our user database + database = null; + } + + + @Override + public boolean isAvailable() { + return (database == null) ? false : database.isAvailable(); + } + + + public static final class UserDatabasePrincipal extends GenericPrincipal { + private static final long serialVersionUID = 1L; + private final transient UserDatabase database; + + public UserDatabasePrincipal(User user, UserDatabase database) { + super(user.getName()); + this.database = database; + } + + @Override + public String[] getRoles() { + if (database == null) { + return new String[0]; + } + User user = database.findUser(name); + if (user == null) { + return new String[0]; + } + Set roles = new HashSet<>(); + Iterator uroles = user.getRoles(); + while (uroles.hasNext()) { + Role role = uroles.next(); + roles.add(role.getName()); + } + Iterator groups = user.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + uroles = group.getRoles(); + while (uroles.hasNext()) { + Role role = uroles.next(); + roles.add(role.getName()); + } + } + return roles.toArray(new String[0]); + } + + @Override + public boolean hasRole(String role) { + if ("*".equals(role)) { + return true; + } else if (role == null) { + return false; + } + if (database == null) { + return super.hasRole(role); + } + Role dbrole = database.findRole(role); + if (dbrole == null) { + return false; + } + User user = database.findUser(name); + if (user == null) { + return false; + } + if (user.isInRole(dbrole)) { + return true; + } + Iterator groups = user.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + if (group.isInRole(dbrole)) { + return true; + } + } + return false; + } + + /** + * Magic method from {@link java.io.Serializable}. + * + * @return The object to serialize instead of this object + * + * @throws ObjectStreamException Not thrown by this implementation + */ + private Object writeReplace() throws ObjectStreamException { + // Replace with a static principal disconnected from the database + return new GenericPrincipal(getName(), Arrays.asList(getRoles())); + } + } +} diff --git a/java/org/apache/catalina/realm/X509SubjectDnRetriever.java b/java/org/apache/catalina/realm/X509SubjectDnRetriever.java new file mode 100644 index 0000000..f8d3a4a --- /dev/null +++ b/java/org/apache/catalina/realm/X509SubjectDnRetriever.java @@ -0,0 +1,30 @@ +/* + * 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.catalina.realm; + +import java.security.cert.X509Certificate; + +/** + * An X509UsernameRetriever that returns a certificate's entire SubjectDN as the username. + */ +public class X509SubjectDnRetriever implements X509UsernameRetriever { + + @Override + public String getUsername(X509Certificate clientCert) { + return clientCert.getSubjectX500Principal().toString(); + } +} diff --git a/java/org/apache/catalina/realm/X509UsernameRetriever.java b/java/org/apache/catalina/realm/X509UsernameRetriever.java new file mode 100644 index 0000000..f8042c8 --- /dev/null +++ b/java/org/apache/catalina/realm/X509UsernameRetriever.java @@ -0,0 +1,33 @@ +/* + * 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.catalina.realm; + +import java.security.cert.X509Certificate; + +/** + * Provides an interface for retrieving a user name from an X509Certificate. + */ +public interface X509UsernameRetriever { + /** + * Gets a user name from an X509Certificate. + * + * @param cert The certificate containing the user name. + * + * @return An appropriate user name obtained from one or more fields in the certificate. + */ + String getUsername(X509Certificate cert); +} diff --git a/java/org/apache/catalina/realm/mbeans-descriptors.xml b/java/org/apache/catalina/realm/mbeans-descriptors.xml new file mode 100644 index 0000000..9885e74 --- /dev/null +++ b/java/org/apache/catalina/realm/mbeans-descriptors.xml @@ -0,0 +1,483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/realm/package.html b/java/org/apache/catalina/realm/package.html new file mode 100644 index 0000000..06a88eb --- /dev/null +++ b/java/org/apache/catalina/realm/package.html @@ -0,0 +1,30 @@ + + + +

    This package contains Realm implementations for the +various supported realm technologies for authenticating users and +identifying their associated roles. The Realm that is +associated with a web application's Context (or a hierarchically +superior Container) is used to resolve authentication and role presence +questions when a web application uses container managed security as described +in the Servlet API Specification.

    + +

    The implementations share a common base class that supports basic +functionality for all of the standard Realm implementations.

    + + diff --git a/java/org/apache/catalina/security/Constants.java b/java/org/apache/catalina/security/Constants.java new file mode 100644 index 0000000..8419706 --- /dev/null +++ b/java/org/apache/catalina/security/Constants.java @@ -0,0 +1,24 @@ +/* + * 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.catalina.security; + +public class Constants { + + public static final String PACKAGE = "org.apache.catalina.security"; + + public static final String CRLF = "\r\n"; +} diff --git a/java/org/apache/catalina/security/DeployXmlPermission.java b/java/org/apache/catalina/security/DeployXmlPermission.java new file mode 100644 index 0000000..7509be5 --- /dev/null +++ b/java/org/apache/catalina/security/DeployXmlPermission.java @@ -0,0 +1,37 @@ +/* + * 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.catalina.security; + +import java.security.BasicPermission; + +/** + * Grant this permission to a docBase to permit the web application to use any META-INF/context.xml that + * might be present with in the application when deployXML has been disabled at the Host level. The name of + * the permission should be the base name for the web application. + */ +public class DeployXmlPermission extends BasicPermission { + + private static final long serialVersionUID = 1L; + + public DeployXmlPermission(String name) { + super(name); + } + + public DeployXmlPermission(String name, String actions) { + super(name, actions); + } +} diff --git a/java/org/apache/catalina/security/LocalStrings.properties b/java/org/apache/catalina/security/LocalStrings.properties new file mode 100644 index 0000000..e356c44 --- /dev/null +++ b/java/org/apache/catalina/security/LocalStrings.properties @@ -0,0 +1,31 @@ +# 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. + +SecurityListener.checkUmaskFail=Start attempted with umask setting of [{0}]. Running Tomcat without a umask at least as restrictive as [{1}] has been blocked by the Lifecycle listener org.apache.catalina.security.SecurityListener (usually configured in CATALINA_BASE/conf/server.xml) +SecurityListener.checkUmaskNone=No umask setting was found in system property [{0}]. However, it appears Tomcat is running on a platform that supports umask. The system property is typically set in CATALINA_HOME/bin/catalina.sh. The Lifecycle listener org.apache.catalina.security.SecurityListener (usually configured in CATALINA_BASE/conf/server.xml) expects a umask at least as restrictive as [{1}] +SecurityListener.checkUmaskParseFail=Failed to parse value [{0}] as a valid umask. +SecurityListener.checkUmaskSkip=Unable to determine umask. It appears Tomcat is running on Windows so skip the umask check. +SecurityListener.checkUserWarning=Start attempted while running as user [{0}]. Running Tomcat as this user has been blocked by the Lifecycle listener org.apache.catalina.security.SecurityListener (usually configured in CATALINA_BASE/conf/server.xml) +SecurityListener.buildDateAgeUnreadable=Unable to read configured buildDateWarningAgeDays [{0}], using default of [{1}] days. +SecurityListener.buildDateUnreadable=Server build date [{0}] is unreadable as an ISO-8601 date. +SecurityListener.buildDateIsOld=This version of Tomcat was built more than {0} days ago. You should consider upgrading to the current version. + +SecurityUtil.doAsPrivilege=An exception occurs when running the PrivilegedExceptionAction block. + +listener.notServer=This listener must only be nested within Server elements, but is in [{0}]. + +tlsCertRenewalListener.notRenewed=[{0}], TLS virtual host [{1}] with name [{2}] that expires on [{3}] is overdue for renewal +tlsCertRenewalListener.reloadFailed=[{0}], TLS virtual host [{1}] reload of TLS configuration failed +tlsCertRenewalListener.reloadSuccess=[{0}], TLS virtual host [{1}] reloaded TLS configuration diff --git a/java/org/apache/catalina/security/LocalStrings_de.properties b/java/org/apache/catalina/security/LocalStrings_de.properties new file mode 100644 index 0000000..9d394ce --- /dev/null +++ b/java/org/apache/catalina/security/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +SecurityListener.checkUmaskSkip=Kann die umask nicht ermitteln. Es scheint Tomcat läuft auf Windows. Ãœberspringe den umask Test. diff --git a/java/org/apache/catalina/security/LocalStrings_es.properties b/java/org/apache/catalina/security/LocalStrings_es.properties new file mode 100644 index 0000000..a3f2be9 --- /dev/null +++ b/java/org/apache/catalina/security/LocalStrings_es.properties @@ -0,0 +1,22 @@ +# 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. + +SecurityListener.checkUmaskFail=Intentado arranque con valor de umask de [{0}]. Ejecutando Tomcat sin umask al menos tan restrictivo como [{1}] ha sido bloqueado por el oyente de Ciclo de Vida org.apache.catalina.security.SecurityListener (normalmente configurado en CATALINA_BASE/conf/server.xml) +SecurityListener.checkUmaskNone=No se ha hallado valor de umask en propiedad de sistema [{0}]. Si embargo, parece que Tomcat está siendo ejecutado en una plataforma que soporta umask. La propiedad del sistema se pone normalmente en CATALINA_HOME/bin/catalina.sh. El oyente de Ciclo de Vida org.apache.catalina.security.SecurityListener (normalmente configurado en CATALINA_BASE/conf/server.xml) espera un umask al menos tan restrictivo como [{1}] +SecurityListener.checkUmaskParseFail=No pude anallizar el valor [{0}] como in válido umask. +SecurityListener.checkUmaskSkip=No pude determinar umask. Parece que Tomcat se está ejecutando en Windows, por lo que se salta el chequeo de umsak. +SecurityListener.checkUserWarning=Se ha intentado arrancar mientras se ejecutaba como usuario [{0}]. Ejecutando Tomcat como este usuario user ha sido bloqueado por el oyente de Ciclos de Vida org.apache.catalina.security.SecurityListener (normalmente configurado en CATALINA_BASE/conf/server.xml) + +SecurityUtil.doAsPrivilege=Ha tenido lugar una excepción al ejecutar el bloque PrivilegedExceptionAction. diff --git a/java/org/apache/catalina/security/LocalStrings_fr.properties b/java/org/apache/catalina/security/LocalStrings_fr.properties new file mode 100644 index 0000000..79df9d3 --- /dev/null +++ b/java/org/apache/catalina/security/LocalStrings_fr.properties @@ -0,0 +1,31 @@ +# 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. + +SecurityListener.buildDateAgeUnreadable=Impossible de lire buildDateWarningAgeDays [{0}] qui a été configuré, le défaut de [{1}] jours sera utilisé +SecurityListener.buildDateIsOld=La version de Tomcat a été compilée il y a plus de {0} jours, vous devriez songer à la mettre à jour +SecurityListener.buildDateUnreadable=La date de compilation [{0}] du serveur n''est pas lisible en tant que date ISO-8601 +SecurityListener.checkUmaskFail=Tentative de démarrage avec un paramètre umask [{0}], qui a été bloquée par l''écouteur org.apache.catalina.security.SecurityListener (configuré habituellement dans CATALINA_BASE/conf/server.xml) car l''umask doit être au moins aussi restreint que [{1}] +SecurityListener.checkUmaskNone=Pas de définition du "umask" trouvée dans la propriété système [{0}]. Il apparaît toutefois que Tomcat tourne sur une plateforme qui supporte l''utilisation de umask. La propriété système est typiquement définie dans CATALINA_HOME/bin/catalina.sh. Le Lifecycle Listener org.apache.catalina.security.SecurityListener (généralement configuré dans CATALINA_BASE/conf/server.xml) s''attend à un umask au moins aussi restrictif que [{1}] +SecurityListener.checkUmaskParseFail=Impossible de traiter la valeur [{0}] comme un umask valide +SecurityListener.checkUmaskSkip=Impossible de déterminer le "umask". Il semble que Tomcat tourne ici sous Windows, donc évitez la vérification du "umask". +SecurityListener.checkUserWarning=Tentative de démarrage avec l''utilisateur [{0}], qui a été bloquée par l''écouteur org.apache.catalina.security.SecurityListener (configuré habituellement dans CATALINA_BASE/conf/server.xml) + +SecurityUtil.doAsPrivilege=Une exception s'est produite lors de l'exécution du bloc "PrivilegedExceptionAction". + +listener.notServer=Ce listener ne peut être ajouté qu''à des éléments Server, mais est dans [{0}] + +tlsCertRenewalListener.notRenewed=[{0}], hôte virtuel TLS [{1}] avec nom [{2}] qui expire le [{3}] nécessite un renouvellement +tlsCertRenewalListener.reloadFailed=[{0}], hôte virtuel TLS [{1}] le redémarrage de la configuration TLS a échoué +tlsCertRenewalListener.reloadSuccess=[{0}], hôte virtuel TLS [{1}] la configuration TLS a été redémarrée diff --git a/java/org/apache/catalina/security/LocalStrings_ja.properties b/java/org/apache/catalina/security/LocalStrings_ja.properties new file mode 100644 index 0000000..33ff55d --- /dev/null +++ b/java/org/apache/catalina/security/LocalStrings_ja.properties @@ -0,0 +1,28 @@ +# 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. + +SecurityListener.checkUmaskFail=[{0}] ã®umask設定ã§é–‹å§‹ã—よã†ã¨ã—ã¾ã—ãŸã€‚ å°‘ãªãã¨ã‚‚ [{1}] ã¨åŒã˜ã‚ˆã†ã«umaskを指定ã—ãªã„ã§Tomcatを実行ã™ã‚‹ã¨ã€ãƒ©ã‚¤ãƒ•ã‚µã‚¤ã‚¯ãƒ«ãƒªã‚¹ãƒŠãƒ¼ã®org.apache.catalina.security.SecurityListener(通常ã¯CATALINA_BASE/conf/server.xmlã§æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ï¼‰ã«ã‚ˆã£ã¦ãƒ–ロックã•ã‚Œã¾ã™ +SecurityListener.checkUmaskNone=システムプロパティ [{0}] ã«umask設定ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ã—ã‹ã—ã€Tomcatã¯umaskをサãƒãƒ¼ãƒˆã™ã‚‹ãƒ—ラットフォームã§å‹•ä½œã—ã¦ã„るよã†ã§ã™ã€‚システムプロパティã¯é€šå¸¸ã€CATALINA_HOME/bin/catalina.shã«è¨­å®šã•ã‚Œã¾ã™ã€‚ライフサイクルリスナーã®org.apache.catalina.security.SecurityListener(通常ã¯CATALINA_BASE/conf/server.xmlã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ï¼‰ã§ã¯ã€å°‘ãªãã¨ã‚‚ [{1}] ã¨åŒã˜ãらã„拘æŸã•ã‚ŒãŸumaskãŒå¿…è¦ã§ã™ã€‚ +SecurityListener.checkUmaskParseFail=値[{0}]を有効ãªumaskã¨ã—ã¦è§£æžã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +SecurityListener.checkUmaskSkip=umask ã‚’å–å¾—ã§ãã¾ã›ã‚“。Tomcat ã‚’ Windows ã§å®Ÿè¡Œã™ã‚‹ã¨ã㯠umask ã‚’ãƒã‚§ãƒƒã‚¯ã—ã¾ã›ã‚“。 +SecurityListener.checkUserWarning=ユーザー[{0}]ã¨ã—ã¦å®Ÿè¡Œä¸­ã«é–‹å§‹ã—よã†ã¨ã—ã¾ã—ãŸã€‚ ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ã®Tomcatã®å®Ÿè¡Œã¯ãƒ©ã‚¤ãƒ•ã‚µã‚¤ã‚¯ãƒ«ãƒªã‚¹ãƒŠãƒ¼org.apache.catalina.security.SecurityListener(通常ã¯CATALINA_BASE/conf /server.xmlã§æ§‹æˆã•ã‚Œã¦ã„る)ã«ã‚ˆã£ã¦ãƒ–ロックã•ã‚Œã¦ã„ã¾ã™ã€‚ + +SecurityUtil.doAsPrivilege=PrivilegedExceptionActionブロックを実行中ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ + +listener.notServer=ã“ã®listenerã¯Serverè¦ç´ å†…ã«ã®ã¿ãƒã‚¹ãƒˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ãŒã€[{0}] ã«ã‚ã‚Šã¾ã™ã€‚ + +tlsCertRenewalListener.notRenewed=[{0}]ã€[{3}] ã«æœŸé™åˆ‡ã‚Œã«ãªã‚‹åå‰ [{2}] ã® TLS 仮想ホスト [{1}] ã¯æ›´æ–°æœŸé™ã‚’éŽãŽã¦ã„ã¾ã™ +tlsCertRenewalListener.reloadFailed=[{0}]ã€TLS 仮想ホスト [{1}] ㌠TLS 構æˆã®ãƒªãƒ­ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—㟠+tlsCertRenewalListener.reloadSuccess=[{0}]ã€TLS 仮想ホスト [{1}] ㌠TLS 構æˆã‚’リロードã—ã¾ã—㟠diff --git a/java/org/apache/catalina/security/LocalStrings_ko.properties b/java/org/apache/catalina/security/LocalStrings_ko.properties new file mode 100644 index 0000000..0448fb2 --- /dev/null +++ b/java/org/apache/catalina/security/LocalStrings_ko.properties @@ -0,0 +1,24 @@ +# 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. + +SecurityListener.checkUmaskFail=umask 설정 [{0}]ì„(를) 사용하여 ì‹œìž‘ì„ ì‹œë„했습니다. ì ì–´ë„ umask를 [{1}] ë§Œí¼ ì œí•œì ìœ¼ë¡œ 설정하지 ì•Šê³  Tomcatì„ ì‹œìž‘í•˜ëŠ” 것ì€, Lifecycle ë¦¬ìŠ¤ë„ˆì¸ org.apache.catalina.security.SecurityListener (í†µìƒ CATALINA_BASE/conf/server.xmlì—ì„œ 설정)ì— ì˜í•´ 차단ë˜ì—ˆìŠµë‹ˆë‹¤. +SecurityListener.checkUmaskNone=시스템 프로í¼í‹° [{0}]ì— umask ì„¤ì •ì´ ì—†ìŠµë‹ˆë‹¤. 하지만, Tomcatì€ umask를 지ì›í•˜ëŠ” 플랫í¼ì—ì„œ 실행 ì¤‘ì¸ ê²ƒìœ¼ë¡œ 보입니다. 해당 시스템 프로í¼í‹°ëŠ” 보통 CATALINA_HOME/bin/catalina.shì—ì„œ 설정ë©ë‹ˆë‹¤. Lifecycle ë¦¬ìŠ¤ë„ˆì¸ org.apache.catalina.security.SecurityListener(í†µìƒ CATALINA_BASE/conf/server.xmlì—ì„œ 설정)는, umask ê°’ì´ ì ì–´ë„ [{1}] ë§Œí¼ ì œí•œì ìœ¼ë¡œ 설정ë˜ê¸°ë¥¼ 요구합니다. +SecurityListener.checkUmaskParseFail=ê°’ [{0}]ì´(ê°€) 유효한 umask ê°’ì´ ì•„ë‹ˆì–´ì„œ, 파싱하지 못했습니다. +SecurityListener.checkUmaskSkip=umask를 ê²°ì •í•  수 없습니다. Tomcatì´ Windowsì—ì„œ 실행ë˜ëŠ” 것으로 ë³´ì´ë¯€ë¡œ, umask ì ê²€ì„ 건너ëœë‹ˆë‹¤. +SecurityListener.checkUserWarning=ì‚¬ìš©ìž [{0}](으)로서 실행하면서, 프로그램 ì‹œìž‘ì´ ì‹œë„ ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ ì‚¬ìš©ìžë¡œì„œ Tomcatì„ ì‹¤í–‰í•˜ëŠ” 것ì€, Lifecycle ë¦¬ìŠ¤ë„ˆì¸ org.apache.catalina.security.SecurityListener (보통 CATALINA_BASE/conf/server.xmlì—ì„œ 설정)ì— ì˜í•´ 차단ë˜ì—ˆìŠµë‹ˆë‹¤. + +SecurityUtil.doAsPrivilege=PrivilegedExceptionAction 블ë¡ì„ 실행하는 중 예외가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. + +listener.notServer=리스너 엘리먼트는 서버 엘리먼트 ë‚´ì— ìœ„ì¹˜í•´ì•¼ 합니다만, 현재 [{0}] ë‚´ì— ìžˆìŠµë‹ˆë‹¤. diff --git a/java/org/apache/catalina/security/LocalStrings_zh_CN.properties b/java/org/apache/catalina/security/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..8a44aea --- /dev/null +++ b/java/org/apache/catalina/security/LocalStrings_zh_CN.properties @@ -0,0 +1,24 @@ +# 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. + +SecurityListener.checkUmaskFail=å°è¯•ä½¿ç”¨umask设置[{0}]å¯åŠ¨ã€‚生命周期侦å¬å™¨org.apache.catalina.security.security listener(通常在catalina-BASE/conf/server.xml中é…置)已阻止在没有umask的情况下è¿è¡ŒTomcat,其é™åˆ¶è‡³å°‘与[{1}]ç›¸åŒ +SecurityListener.checkUmaskNone=在系统属性[{0}]中找ä¸åˆ°umask设置。但是,看æ¥Tomcat在支æŒumaskçš„å¹³å°ä¸Šè¿è¡Œã€‚系统属性通常在CATALINA_HOME / bin / catalina.sh中设置。生命周期侦å¬å™¨org.apache.catalina.security.SecurityListener(通常在CATALINA_BASE / conf / server.xml中é…置)期望umaskçš„é™åˆ¶è‡³å°‘与[{1}]ç›¸åŒ +SecurityListener.checkUmaskParseFail=无法将值[{0}]分æžä¸ºæœ‰æ•ˆçš„umask。 +SecurityListener.checkUmaskSkip=无法确定æƒé™ã€‚这表示 Tomcat 正在 Windows 上è¿è¡Œï¼Œæ‰€ä»¥è·³è¿‡æƒé™æ£€æŸ¥ã€‚ +SecurityListener.checkUserWarning=以用户[{0}]身份è¿è¡Œæ—¶å°è¯•å¯åŠ¨ã€‚作为此用户è¿è¡ŒTomcat已被生命周期侦å¬å™¨org.apache.catalina.security.security listener(通常在catalina_BASE/conf/server.xml中é…置)阻止 + +SecurityUtil.doAsPrivilege=è¿è¡Œprivilegedexceptionå—æ—¶å‘生异常。 + +listener.notServer=此侦å¬å™¨åªèƒ½åµŒå¥—在 Server 元素中,但ä½äºŽ [{0}] 中。\n diff --git a/java/org/apache/catalina/security/SecurityClassLoad.java b/java/org/apache/catalina/security/SecurityClassLoad.java new file mode 100644 index 0000000..cae3005 --- /dev/null +++ b/java/org/apache/catalina/security/SecurityClassLoad.java @@ -0,0 +1,203 @@ +/* + * 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.catalina.security; + +/** + * Static class used to preload java classes when using the Java SecurityManager so that the defineClassInPackage + * RuntimePermission does not trigger an AccessControlException. + * + * @author Glenn L. Nielsen + */ +public final class SecurityClassLoad { + + public static void securityClassLoad(ClassLoader loader) throws Exception { + securityClassLoad(loader, true); + } + + + static void securityClassLoad(ClassLoader loader, boolean requireSecurityManager) throws Exception { + + if (requireSecurityManager && System.getSecurityManager() == null) { + return; + } + + loadCorePackage(loader); + loadCoyotePackage(loader); + loadLoaderPackage(loader); + loadRealmPackage(loader); + loadServletsPackage(loader); + loadSessionPackage(loader); + loadUtilPackage(loader); + loadJakartaPackage(loader); + loadConnectorPackage(loader); + loadTomcatPackage(loader); + } + + + private static void loadCorePackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.catalina.core."; + loader.loadClass(basePackage + "AccessLogAdapter"); + loader.loadClass(basePackage + "ApplicationContextFacade$PrivilegedExecuteMethod"); + loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedForward"); + loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedInclude"); + loader.loadClass(basePackage + "ApplicationPushBuilder"); + loader.loadClass(basePackage + "AsyncContextImpl"); + loader.loadClass(basePackage + "AsyncContextImpl$AsyncRunnable"); + loader.loadClass(basePackage + "AsyncContextImpl$DebugException"); + loader.loadClass(basePackage + "AsyncListenerWrapper"); + loader.loadClass(basePackage + "ContainerBase$PrivilegedAddChild"); + loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntry"); + loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntryType"); + loader.loadClass(basePackage + "DefaultInstanceManager$PrivilegedGetField"); + loader.loadClass(basePackage + "DefaultInstanceManager$PrivilegedGetMethod"); + loader.loadClass(basePackage + "DefaultInstanceManager$PrivilegedLoadClass"); + loader.loadClass(basePackage + "ApplicationHttpRequest$AttributeNamesEnumerator"); + } + + + private static void loadLoaderPackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.catalina.loader."; + loader.loadClass(basePackage + "WebappClassLoaderBase$PrivilegedFindClassByName"); + loader.loadClass(basePackage + "WebappClassLoaderBase$PrivilegedHasLoggingConfig"); + } + + + private static void loadRealmPackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.catalina.realm."; + loader.loadClass(basePackage + "LockOutRealm$LockRecord"); + } + + + private static void loadServletsPackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.catalina.servlets."; + // Avoid a possible memory leak in the DefaultServlet when running with + // a security manager. The DefaultServlet needs to load an XML parser + // when running under a security manager. We want this to be loaded by + // the container rather than a web application to prevent a memory leak + // via web application class loader. + loader.loadClass(basePackage + "DefaultServlet"); + } + + + private static void loadSessionPackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.catalina.session."; + loader.loadClass(basePackage + "StandardSession"); + loader.loadClass(basePackage + "StandardSession$PrivilegedNewSessionFacade"); + loader.loadClass(basePackage + "StandardManager$PrivilegedDoUnload"); + } + + + private static void loadUtilPackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.catalina.util."; + loader.loadClass(basePackage + "ParameterMap"); + loader.loadClass(basePackage + "RequestUtil"); + loader.loadClass(basePackage + "TLSUtil"); + } + + + private static void loadCoyotePackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.coyote."; + loader.loadClass(basePackage + "http11.Constants"); + // Make sure system property is read at this point + Class clazz = loader.loadClass(basePackage + "Constants"); + clazz.getConstructor().newInstance(); + loader.loadClass(basePackage + "http2.Stream$PrivilegedPush"); + } + + + private static void loadJakartaPackage(ClassLoader loader) throws Exception { + loader.loadClass("jakarta.servlet.http.Cookie"); + } + + + private static void loadConnectorPackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.catalina.connector."; + loader.loadClass(basePackage + "RequestFacade$GetAttributePrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetParameterMapPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetRequestDispatcherPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetParameterPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetParameterNamesPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetParameterValuePrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetCharacterEncodingPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetHeadersPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetHeaderNamesPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetCookiesPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetLocalePrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetLocalesPrivilegedAction"); + loader.loadClass(basePackage + "ResponseFacade$SetContentTypePrivilegedAction"); + loader.loadClass(basePackage + "ResponseFacade$DateHeaderPrivilegedAction"); + loader.loadClass(basePackage + "RequestFacade$GetSessionPrivilegedAction"); + loader.loadClass(basePackage + "ResponseFacade$FlushBufferPrivilegedAction"); + loader.loadClass(basePackage + "OutputBuffer$PrivilegedCreateConverter"); + loader.loadClass(basePackage + "CoyoteInputStream$PrivilegedAvailable"); + loader.loadClass(basePackage + "CoyoteInputStream$PrivilegedClose"); + loader.loadClass(basePackage + "CoyoteInputStream$PrivilegedRead"); + loader.loadClass(basePackage + "CoyoteInputStream$PrivilegedReadArray"); + loader.loadClass(basePackage + "CoyoteInputStream$PrivilegedReadBuffer"); + loader.loadClass(basePackage + "CoyoteOutputStream"); + loader.loadClass(basePackage + "InputBuffer$PrivilegedCreateConverter"); + loader.loadClass(basePackage + "Response$PrivilegedDoIsEncodable"); + loader.loadClass(basePackage + "Response$PrivilegedGenerateCookieString"); + loader.loadClass(basePackage + "Response$PrivilegedEncodeUrl"); + } + + + private static void loadTomcatPackage(ClassLoader loader) throws Exception { + final String basePackage = "org.apache.tomcat."; + // buf + loader.loadClass(basePackage + "util.buf.B2CConverter"); + loader.loadClass(basePackage + "util.buf.ByteBufferUtils"); + loader.loadClass(basePackage + "util.buf.C2BConverter"); + loader.loadClass(basePackage + "util.buf.HexUtils"); + loader.loadClass(basePackage + "util.buf.StringCache"); + loader.loadClass(basePackage + "util.buf.StringCache$ByteEntry"); + loader.loadClass(basePackage + "util.buf.StringCache$CharEntry"); + loader.loadClass(basePackage + "util.buf.UriUtil"); + // collections + loader.loadClass(basePackage + "util.collections.CaseInsensitiveKeyMap"); + loader.loadClass(basePackage + "util.collections.CaseInsensitiveKeyMap$EntryImpl"); + loader.loadClass(basePackage + "util.collections.CaseInsensitiveKeyMap$EntryIterator"); + loader.loadClass(basePackage + "util.collections.CaseInsensitiveKeyMap$EntrySet"); + loader.loadClass(basePackage + "util.collections.CaseInsensitiveKeyMap$Key"); + // http + loader.loadClass(basePackage + "util.http.CookieProcessor"); + loader.loadClass(basePackage + "util.http.NamesEnumerator"); + // Make sure system property is read at this point + Class clazz = loader.loadClass(basePackage + "util.http.FastHttpDateFormat"); + clazz.getConstructor().newInstance(); + loader.loadClass(basePackage + "util.http.parser.HttpParser"); + loader.loadClass(basePackage + "util.http.parser.MediaType"); + loader.loadClass(basePackage + "util.http.parser.MediaTypeCache"); + loader.loadClass(basePackage + "util.http.parser.SkipResult"); + // net + loader.loadClass(basePackage + "util.net.Constants"); + loader.loadClass(basePackage + "util.net.DispatchType"); + loader.loadClass(basePackage + "util.net.NioEndpoint$NioSocketWrapper$NioOperationState"); + loader.loadClass(basePackage + "util.net.Nio2Endpoint$Nio2SocketWrapper$Nio2OperationState"); + loader.loadClass(basePackage + "util.net.SocketWrapperBase$BlockingMode"); + loader.loadClass(basePackage + "util.net.SocketWrapperBase$CompletionCheck"); + loader.loadClass(basePackage + "util.net.SocketWrapperBase$CompletionHandlerCall"); + loader.loadClass(basePackage + "util.net.SocketWrapperBase$CompletionState"); + loader.loadClass(basePackage + "util.net.SocketWrapperBase$VectoredIOCompletionHandler"); + loader.loadClass(basePackage + "util.net.TLSClientHelloExtractor"); + loader.loadClass(basePackage + "util.net.TLSClientHelloExtractor$ExtractorResult"); + // security + loader.loadClass(basePackage + "util.security.PrivilegedGetTccl"); + loader.loadClass(basePackage + "util.security.PrivilegedSetTccl"); + loader.loadClass(basePackage + "util.security.PrivilegedSetAccessControlContext"); + } +} diff --git a/java/org/apache/catalina/security/SecurityConfig.java b/java/org/apache/catalina/security/SecurityConfig.java new file mode 100644 index 0000000..89d3d1b --- /dev/null +++ b/java/org/apache/catalina/security/SecurityConfig.java @@ -0,0 +1,142 @@ +/* + * 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.catalina.security; + +import java.security.Security; + +import org.apache.catalina.startup.CatalinaProperties; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Util class to protect Catalina against package access and insertion. The code are been moved from Catalina.java + * + * @author the Catalina.java authors + */ +public final class SecurityConfig { + + private static final Object singletonLock = new Object(); + private static volatile SecurityConfig singleton = null; + + private static final Log log = LogFactory.getLog(SecurityConfig.class); + + + private static final String PACKAGE_ACCESS = + "sun.," + "org.apache.catalina." + ",org.apache.jasper." + ",org.apache.coyote." + ",org.apache.tomcat."; + + // FIX ME package "javax." was removed to prevent HotSpot + // fatal internal errors + private static final String PACKAGE_DEFINITION = "java.,sun." + ",org.apache.catalina." + ",org.apache.coyote." + + ",org.apache.tomcat." + ",org.apache.jasper."; + /** + * List of protected package from conf/catalina.properties + */ + private final String packageDefinition; + + + /** + * List of protected package from conf/catalina.properties + */ + private final String packageAccess; + + + /** + * Create a single instance of this class. + */ + private SecurityConfig() { + String definition = null; + String access = null; + try { + definition = CatalinaProperties.getProperty("package.definition"); + access = CatalinaProperties.getProperty("package.access"); + } catch (java.lang.Exception ex) { + if (log.isDebugEnabled()) { + log.debug("Unable to load properties using CatalinaProperties", ex); + } + } finally { + packageDefinition = definition; + packageAccess = access; + } + } + + + /** + * Returns the singleton instance of that class. + * + * @return an instance of that class. + */ + public static SecurityConfig newInstance() { + if (singleton == null) { + synchronized (singletonLock) { + if (singleton == null) { + singleton = new SecurityConfig(); + } + } + } + return singleton; + } + + + /** + * Set the security package.access value. + */ + public void setPackageAccess() { + // If catalina.properties is missing, protect all by default. + if (packageAccess == null) { + setSecurityProperty("package.access", PACKAGE_ACCESS); + } else { + setSecurityProperty("package.access", packageAccess); + } + } + + + /** + * Set the security package.definition value. + */ + public void setPackageDefinition() { + // If catalina.properties is missing, protect all by default. + if (packageDefinition == null) { + setSecurityProperty("package.definition", PACKAGE_DEFINITION); + } else { + setSecurityProperty("package.definition", packageDefinition); + } + } + + + /** + * Set the proper security property + * + * @param properties the package.* property. + */ + private void setSecurityProperty(String properties, String packageList) { + if (System.getSecurityManager() != null) { + String definition = Security.getProperty(properties); + if (definition != null && definition.length() > 0) { + if (packageList.length() > 0) { + definition = definition + ',' + packageList; + } + } else { + definition = packageList; + } + + Security.setProperty(properties, definition); + } + } + + +} + diff --git a/java/org/apache/catalina/security/SecurityListener.java b/java/org/apache/catalina/security/SecurityListener.java new file mode 100644 index 0000000..7917b9a --- /dev/null +++ b/java/org/apache/catalina/security/SecurityListener.java @@ -0,0 +1,247 @@ +/* + * 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.catalina.security; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.catalina.util.ServerInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * This listener must only be nested within {@link Server} elements. + */ +public class SecurityListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(SecurityListener.class); + + private static final StringManager sm = StringManager.getManager(Constants.PACKAGE); + + private static final String UMASK_PROPERTY_NAME = Constants.PACKAGE + ".SecurityListener.UMASK"; + + private static final String UMASK_FORMAT = "%04o"; + + private static final int DEFAULT_BUILD_DATE_WARNING_AGE_DAYS = -1; + + /** + * The list of operating system users not permitted to run Tomcat. + */ + private final Set checkedOsUsers = new HashSet<>(); + + /** + * The number of days this Tomcat build can go without warning upon startup. + */ + private int buildDateWarningAgeDays = DEFAULT_BUILD_DATE_WARNING_AGE_DAYS; + + /** + * The minimum umask that must be configured for the operating system user running Tomcat. The umask is handled as + * an octal. + */ + private Integer minimumUmask = Integer.valueOf(7); + + + public SecurityListener() { + checkedOsUsers.add("root"); + } + + + @Override + public void lifecycleEvent(LifecycleEvent event) { + // This is the earliest event in Lifecycle + if (event.getType().equals(Lifecycle.BEFORE_INIT_EVENT)) { + if (!(event.getLifecycle() instanceof Server)) { + log.warn(sm.getString("listener.notServer", event.getLifecycle().getClass().getSimpleName())); + } + doChecks(); + } + } + + + /** + * Set the list of operating system users not permitted to run Tomcat. By default, only root is prevented from + * running Tomcat. Calling this method with null or the empty string will clear the list of users and effectively + * disables this check. User names will always be checked in a case insensitive manner using the system default + * Locale. + * + * @param userNameList A comma separated list of operating system users not permitted to run Tomcat + */ + public void setCheckedOsUsers(String userNameList) { + if (userNameList == null || userNameList.length() == 0) { + checkedOsUsers.clear(); + } else { + String[] userNames = userNameList.split(","); + for (String userName : userNames) { + if (userName.length() > 0) { + checkedOsUsers.add(userName.toLowerCase(Locale.getDefault())); + } + } + } + } + + + /** + * Returns the current list of operating system users not permitted to run Tomcat. + * + * @return A comma separated list of operating system user names. + */ + public String getCheckedOsUsers() { + return StringUtils.join(checkedOsUsers); + } + + + /** + * Set the minimum umask that must be configured before Tomcat will start. + * + * @param umask The 4-digit umask as returned by the OS command umask + */ + public void setMinimumUmask(String umask) { + if (umask == null || umask.length() == 0) { + minimumUmask = Integer.valueOf(0); + } else { + minimumUmask = Integer.valueOf(umask, 8); + } + } + + + /** + * Get the minimum umask that must be configured before Tomcat will start. + * + * @return The 4-digit umask as used by the OS command umask + */ + public String getMinimumUmask() { + return String.format(UMASK_FORMAT, minimumUmask); + } + + /** + * Sets the number of days that may pass between the build-date of this + * Tomcat instance before warnings are printed. + * + * @param ageDays The number of days a Tomcat build is allowed to age + * before logging warnings. + */ + public void setBuildDateWarningAgeDays(String ageDays) { + try { + buildDateWarningAgeDays = Integer.parseInt(ageDays); + } catch (NumberFormatException nfe) { + // Just use the default and warn the user + log.warn(sm.getString("SecurityListener.buildDateAgeUnreadable", + ageDays, String.valueOf(DEFAULT_BUILD_DATE_WARNING_AGE_DAYS))); + } + } + + /** + * Gets the number of days that may pass between the build-date of this + * Tomcat instance before warnings are printed. + * + * @return The number of days a Tomcat build is allowed to age + * before logging warnings. + */ + public int getBuildDateWarningAgeDays() { + return buildDateWarningAgeDays; + } + + /** + * Execute the security checks. Each check should be in a separate method. + */ + protected void doChecks() { + checkOsUser(); + checkUmask(); + checkServerBuildAge(); + } + + + protected void checkOsUser() { + String userName = System.getProperty("user.name"); + if (userName != null) { + String userNameLC = userName.toLowerCase(Locale.getDefault()); + + if (checkedOsUsers.contains(userNameLC)) { + // Have to throw Error to force start process to be aborted + throw new Error(sm.getString("SecurityListener.checkUserWarning", userName)); + } + } + } + + + protected void checkUmask() { + String prop = System.getProperty(UMASK_PROPERTY_NAME); + Integer umask = null; + if (prop != null) { + try { + umask = Integer.valueOf(prop, 8); + } catch (NumberFormatException nfe) { + log.warn(sm.getString("SecurityListener.checkUmaskParseFail", prop)); + } + } + if (umask == null) { + if (Constants.CRLF.equals(System.lineSeparator())) { + // Probably running on Windows so no umask + if (log.isDebugEnabled()) { + log.debug(sm.getString("SecurityListener.checkUmaskSkip")); + } + return; + } else { + if (minimumUmask.intValue() > 0) { + log.warn(sm.getString("SecurityListener.checkUmaskNone", UMASK_PROPERTY_NAME, getMinimumUmask())); + } + return; + } + } + + if ((umask.intValue() & minimumUmask.intValue()) != minimumUmask.intValue()) { + throw new Error(sm.getString("SecurityListener.checkUmaskFail", String.format(UMASK_FORMAT, umask), + getMinimumUmask())); + } + } + + protected void checkServerBuildAge() { + int allowedAgeDays = getBuildDateWarningAgeDays(); + + if (allowedAgeDays >= 0) { + String buildDateString = ServerInfo.getServerBuiltISO(); + + if (null == buildDateString || buildDateString.length() < 1 || !Character.isDigit(buildDateString.charAt(0))) { + log.warn(sm.getString("SecurityListener.buildDateUnreadable", buildDateString)); + } else { + try { + Date buildDate = new SimpleDateFormat("yyyy-MM-dd").parse(buildDateString); + + Calendar old = Calendar.getInstance(); + old.add(Calendar.DATE, -allowedAgeDays); // Subtract X days from today + + if (buildDate.before(old.getTime())) { + log.warn(sm.getString("SecurityListener.buildDateIsOld", String.valueOf(allowedAgeDays))); + } + } catch (ParseException pe) { + log.warn(sm.getString("SecurityListener.buildDateUnreadable", buildDateString)); + } + } + } + } +} diff --git a/java/org/apache/catalina/security/SecurityUtil.java b/java/org/apache/catalina/security/SecurityUtil.java new file mode 100644 index 0000000..3a1dcff --- /dev/null +++ b/java/org/apache/catalina/security/SecurityUtil.java @@ -0,0 +1,370 @@ +/* + * 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.catalina.security; + + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.security.auth.Subject; + +import jakarta.servlet.Filter; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.UnavailableException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.Globals; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * This utility class associates a Subject to the current AccessControlContext. When a + * SecurityManager is used, the container will always associate the called thread with an + * AccessControlContext containing only the principal of the requested Servlet/Filter. This class uses reflection to + * invoke the methods. + */ + +public final class SecurityUtil { + + // Note that indexes overlap. + // A Servlet uses "init", "service", "event", "destroy". + // A Filter uses "doFilter", "doFilterEvent", "destroy". + private static final int INIT = 0; + private static final int SERVICE = 1; + private static final int DOFILTER = 1; + private static final int EVENT = 2; + private static final int DOFILTEREVENT = 2; + private static final int DESTROY = 3; + + private static final String INIT_METHOD = "init"; + private static final String DOFILTER_METHOD = "doFilter"; + private static final String SERVICE_METHOD = "service"; + private static final String EVENT_METHOD = "event"; + private static final String DOFILTEREVENT_METHOD = "doFilterEvent"; + private static final String DESTROY_METHOD = "destroy"; + + /** + * Cache every class for which we are creating methods. + */ + private static final Map,Method[]> classCache = new ConcurrentHashMap<>(); + + private static final Log log = LogFactory.getLog(SecurityUtil.class); + + private static final boolean packageDefinitionEnabled = + (System.getProperty("package.definition") == null && System.getProperty("package.access") == null) ? false : + true; + + /** + * The string resources for this package. + */ + private static final StringManager sm = StringManager.getManager(Constants.PACKAGE); + + + /** + * Perform work as a particular Subject. Here the work will be granted to a null subject. + * + * @param methodName the method to apply the security restriction + * @param targetObject the Servlet on which the method will be called. + * + * @throws Exception an execution error occurred + */ + public static void doAsPrivilege(final String methodName, final Servlet targetObject) throws Exception { + doAsPrivilege(methodName, targetObject, null, null, null); + } + + + /** + * Perform work as a particular Subject. Here the work will be granted to a null subject. + * + * @param methodName the method to apply the security restriction + * @param targetObject the Servlet on which the method will be called. + * @param targetType Class array used to instantiate a Method object. + * @param targetArguments Object array contains the runtime parameters instance. + * + * @throws Exception an execution error occurred + */ + public static void doAsPrivilege(final String methodName, final Servlet targetObject, final Class[] targetType, + final Object[] targetArguments) throws Exception { + + doAsPrivilege(methodName, targetObject, targetType, targetArguments, null); + } + + + /** + * Perform work as a particular Subject. Here the work will be granted to a null subject. + * + * @param methodName the method to apply the security restriction + * @param targetObject the Servlet on which the method will be called. + * @param targetParameterTypes Class array used to instantiate a Method object. + * @param targetArguments Object array contains the runtime parameters instance. + * @param principal the Principal to which the security privilege applies + * + * @throws Exception an execution error occurred + */ + public static void doAsPrivilege(final String methodName, final Servlet targetObject, + final Class[] targetParameterTypes, final Object[] targetArguments, Principal principal) + throws Exception { + + Method method = null; + Method[] methodsCache = classCache.get(Servlet.class); + if (methodsCache == null) { + method = createMethodAndCacheIt(null, Servlet.class, methodName, targetParameterTypes); + } else { + method = findMethod(methodsCache, methodName); + if (method == null) { + method = createMethodAndCacheIt(methodsCache, Servlet.class, methodName, targetParameterTypes); + } + } + + execute(method, targetObject, targetArguments, principal); + } + + + /** + * Perform work as a particular Subject. Here the work will be granted to a null subject. + * + * @param methodName the method to apply the security restriction + * @param targetObject the Filter on which the method will be called. + * + * @throws Exception an execution error occurred + */ + public static void doAsPrivilege(final String methodName, final Filter targetObject) throws Exception { + + doAsPrivilege(methodName, targetObject, null, null); + } + + + /** + * Perform work as a particular Subject. Here the work will be granted to a null subject. + * + * @param methodName the method to apply the security restriction + * @param targetObject the Filter on which the method will be called. + * @param targetType Class array used to instantiate a Method object. + * @param targetArguments Object array contains the runtime parameters instance. + * + * @throws Exception an execution error occurred + */ + public static void doAsPrivilege(final String methodName, final Filter targetObject, final Class[] targetType, + final Object[] targetArguments) throws Exception { + + doAsPrivilege(methodName, targetObject, targetType, targetArguments, null); + } + + /** + * Perform work as a particular Subject. Here the work will be granted to a null subject. + * + * @param methodName the method to apply the security restriction + * @param targetObject the Filter on which the method will be called. + * @param targetParameterTypes Class array used to instantiate a Method object. + * @param targetParameterValues Object array contains the runtime parameters instance. + * @param principal the Principal to which the security privilege applies + * + * @throws Exception an execution error occurred + */ + public static void doAsPrivilege(final String methodName, final Filter targetObject, + final Class[] targetParameterTypes, final Object[] targetParameterValues, Principal principal) + throws Exception { + + Method method = null; + Method[] methodsCache = classCache.get(Filter.class); + if (methodsCache == null) { + method = createMethodAndCacheIt(null, Filter.class, methodName, targetParameterTypes); + } else { + method = findMethod(methodsCache, methodName); + if (method == null) { + method = createMethodAndCacheIt(methodsCache, Filter.class, methodName, targetParameterTypes); + } + } + + execute(method, targetObject, targetParameterValues, principal); + } + + + /** + * Perform work as a particular Subject. Here the work will be granted to a null subject. + * + * @param method the method to apply the security restriction + * @param targetObject the Servlet on which the method will be called. + * @param targetArguments Object array contains the runtime parameters instance. + * @param principal the Principal to which the security privilege applies + * + * @throws Exception an execution error occurred + */ + private static void execute(final Method method, final Object targetObject, final Object[] targetArguments, + Principal principal) throws Exception { + + try { + Subject subject = null; + PrivilegedExceptionAction pea = () -> { + method.invoke(targetObject, targetArguments); + return null; + }; + + // The first argument is always the request object + if (targetArguments != null && targetArguments[0] instanceof HttpServletRequest) { + HttpServletRequest request = (HttpServletRequest) targetArguments[0]; + + boolean hasSubject = false; + HttpSession session = request.getSession(false); + if (session != null) { + subject = (Subject) session.getAttribute(Globals.SUBJECT_ATTR); + hasSubject = (subject != null); + } + + if (subject == null) { + subject = new Subject(); + + if (principal != null) { + subject.getPrincipals().add(principal); + } + } + + if (session != null && !hasSubject) { + session.setAttribute(Globals.SUBJECT_ATTR, subject); + } + } + + Subject.doAsPrivileged(subject, pea, null); + } catch (PrivilegedActionException pe) { + Throwable e; + if (pe.getException() instanceof InvocationTargetException) { + e = pe.getException().getCause(); + ExceptionUtils.handleThrowable(e); + } else { + e = pe; + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("SecurityUtil.doAsPrivilege"), e); + } + + if (e instanceof UnavailableException) { + throw (UnavailableException) e; + } else if (e instanceof ServletException) { + throw (ServletException) e; + } else if (e instanceof IOException) { + throw (IOException) e; + } else if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new ServletException(e.getMessage(), e); + } + } + } + + + /** + * Find a method stored within the cache. + * + * @param methodsCache the cache used to store method instance + * @param methodName the method to apply the security restriction + * + * @return the method instance, null if not yet created. + */ + private static Method findMethod(Method[] methodsCache, String methodName) { + if (methodName.equals(INIT_METHOD)) { + return methodsCache[INIT]; + } else if (methodName.equals(DESTROY_METHOD)) { + return methodsCache[DESTROY]; + } else if (methodName.equals(SERVICE_METHOD)) { + return methodsCache[SERVICE]; + } else if (methodName.equals(DOFILTER_METHOD)) { + return methodsCache[DOFILTER]; + } else if (methodName.equals(EVENT_METHOD)) { + return methodsCache[EVENT]; + } else if (methodName.equals(DOFILTEREVENT_METHOD)) { + return methodsCache[DOFILTEREVENT]; + } + return null; + } + + + /** + * Create the method and cache it for further re-use. + * + * @param methodsCache the cache used to store method instance + * @param targetType the class on which the method will be called. + * @param methodName the method to apply the security restriction + * @param parameterTypes Class array used to instantiate a Method object. + * + * @return the method instance. + * + * @throws Exception an execution error occurred + */ + private static Method createMethodAndCacheIt(Method[] methodsCache, Class targetType, String methodName, + Class[] parameterTypes) throws Exception { + + if (methodsCache == null) { + methodsCache = new Method[4]; + } + + Method method = targetType.getMethod(methodName, parameterTypes); + + if (methodName.equals(INIT_METHOD)) { + methodsCache[INIT] = method; + } else if (methodName.equals(DESTROY_METHOD)) { + methodsCache[DESTROY] = method; + } else if (methodName.equals(SERVICE_METHOD)) { + methodsCache[SERVICE] = method; + } else if (methodName.equals(DOFILTER_METHOD)) { + methodsCache[DOFILTER] = method; + } else if (methodName.equals(EVENT_METHOD)) { + methodsCache[EVENT] = method; + } else if (methodName.equals(DOFILTEREVENT_METHOD)) { + methodsCache[DOFILTEREVENT] = method; + } + + classCache.put(targetType, methodsCache); + + return method; + } + + + /** + * Remove the object from the cache. + * + * @param cachedObject The object to remove + */ + public static void remove(Object cachedObject) { + classCache.remove(cachedObject); + } + + + /** + * Return the SecurityManager only if Security is enabled AND package protection mechanism is enabled. + * + * @return true if package level protection is enabled + */ + public static boolean isPackageProtectionEnabled() { + if (packageDefinitionEnabled && Globals.IS_SECURITY_ENABLED) { + return true; + } + return false; + } + + +} diff --git a/java/org/apache/catalina/security/TLSCertificateReloadListener.java b/java/org/apache/catalina/security/TLSCertificateReloadListener.java new file mode 100644 index 0000000..3db6e28 --- /dev/null +++ b/java/org/apache/catalina/security/TLSCertificateReloadListener.java @@ -0,0 +1,177 @@ +/* + * 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.catalina.security; + +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Set; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Connector; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.res.StringManager; + +/** + * A {@link LifecycleListener} that may be used to monitor the expiration dates of TLS certificates and trigger + * automatic reloading of the TLS configuration a set number of days before the TLS certificate expires. + *

    + * This listener assumes there is some other process (certbot, cloud infrastructure, etc) that renews the certificate on + * a regular basis and replaces the current certificate with the new one. + *

    + * This listener does NOT re-read the Tomcat configuration from server.xml. If you make changes to server.xml you + * must restart the Tomcat process to pick up those changes. + */ +public class TLSCertificateReloadListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(TLSCertificateReloadListener.class); + private static final StringManager sm = StringManager.getManager(TLSCertificateReloadListener.class); + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + + // Configuration + private int checkPeriod = 24 * 60 * 60; + private int daysBefore = 14; + + // State + private Calendar nextCheck = Calendar.getInstance(); + + + /** + * Get the time, in seconds, between reloading checks. + *

    + * The periodic process for {@code LifecycleListener} typically runs much more frequently than this listener + * requires. This attribute controls the period between checks. + *

    + * If not specified, a default of 86,400 seconds (24 hours) is used. + * + * @return The time, in seconds, between reloading checks + */ + public int getCheckPeriod() { + return checkPeriod; + } + + + /** + * Set the time, in seconds, between reloading checks. + * + * @param checkPeriod The new time, in seconds, between reloading checks + */ + public void setCheckPeriod(int checkPeriod) { + this.checkPeriod = checkPeriod; + } + + + /** + * Get the number of days before the expiry of a TLS certificate that it is expected that the new certificate will + * be in place and the reloading can be triggered. + *

    + * If not specified, a default of 14 days is used. + * + * @return The number of days before the expiry of a TLS certificate that the reloading will be triggered + */ + public int getDaysBefore() { + return daysBefore; + } + + + /** + * Set the number of days before the expiry of a TLS certificate that it is expected that the new certificate will + * be in place and the reloading can be triggered. + * + * @param daysBefore the number of days before the expiry of the current certificate that reloading will be + * triggered + */ + public void setDaysBefore(int daysBefore) { + this.daysBefore = daysBefore; + } + + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { + Server server; + if (event.getSource() instanceof Server) { + server = (Server) event.getSource(); + } else { + return; + } + checkCertificatesForRenewal(server); + } else if (event.getType().equals(Lifecycle.BEFORE_INIT_EVENT)) { + // This is the earliest event in Lifecycle + if (!(event.getLifecycle() instanceof Server)) { + log.warn(sm.getString("listener.notServer", event.getLifecycle().getClass().getSimpleName())); + } + } + } + + + private void checkCertificatesForRenewal(Server server) { + // Only run the check once every checkPeriod (seconds) + Calendar calendar = Calendar.getInstance(); + if (calendar.compareTo(nextCheck) > 0) { + nextCheck.add(Calendar.SECOND, getCheckPeriod()); + } else { + return; + } + + /* + * Advance current date by "daysBefore". Any certificates that expire before this time should have been renewed + * by now so reloading the associated SSLHostConfig should pick up the new certificate. + */ + calendar.add(Calendar.DAY_OF_MONTH, getDaysBefore()); + + // Check all of the certificates + Service[] services = server.findServices(); + for (Service service : services) { + Connector[] connectors = service.findConnectors(); + for (Connector connector : connectors) { + SSLHostConfig[] sslHostConfigs = connector.findSslHostConfigs(); + for (SSLHostConfig sslHostConfig : sslHostConfigs) { + if (!sslHostConfig.certificatesExpiringBefore(calendar.getTime()).isEmpty()) { + // One or more certificates is due to expire and should have been renewed + // Reload the configuration + try { + connector.getProtocolHandler().addSslHostConfig(sslHostConfig, true); + // Now check again + Set expiringCertificates = + sslHostConfig.certificatesExpiringBefore(calendar.getTime()); + log.info(sm.getString("tlsCertRenewalListener.reloadSuccess", connector, + sslHostConfig.getHostName())); + if (!expiringCertificates.isEmpty()) { + for (X509Certificate expiringCertificate : expiringCertificates) { + log.warn(sm.getString("tlsCertRenewalListener.notRenewed", connector, + sslHostConfig.getHostName(), + expiringCertificate.getSubjectX500Principal().getName(), + dateFormat.format(expiringCertificate.getNotAfter()))); + } + } + } catch (IllegalArgumentException iae) { + log.error(sm.getString("tlsCertRenewalListener.reloadFailed", connector, + sslHostConfig.getHostName()), iae); + } + } + } + } + } + } +} diff --git a/java/org/apache/catalina/servlets/CGIServlet.java b/java/org/apache/catalina/servlets/CGIServlet.java new file mode 100644 index 0000000..7458b5a --- /dev/null +++ b/java/org/apache/catalina/servlets/CGIServlet.java @@ -0,0 +1,1771 @@ +/* + * 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.catalina.servlets; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.regex.Pattern; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.util.IOTools; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.compat.JrePlatform; +import org.apache.tomcat.util.res.StringManager; + + +/** + * CGI-invoking servlet for web applications, used to execute scripts which comply to the Common Gateway Interface (CGI) + * specification and are named in the path-info used to invoke this servlet. + *

    + * Note: This code compiles and even works for simple CGI cases. Exhaustive testing has not been done. Please + * consider it beta quality. Feedback is appreciated to the author (see below). + *

    + *

    + * Example:
    + * If an instance of this servlet was mapped (using <web-app>/WEB-INF/web.xml) to: + *

    + *

    + * + * <web-app>/cgi-bin/* + * + *

    + *

    + * then the following request: + *

    + *

    + * + * http://localhost:8080/<web-app>/cgi-bin/dir1/script/pathinfo1 + * + *

    + *

    + * would result in the execution of the script + *

    + *

    + * + * <web-app-root>/WEB-INF/cgi/dir1/script + * + *

    + *

    + * with the script's PATH_INFO set to /pathinfo1. + *

    + *

    + * Recommendation: House all your CGI scripts under <webapp>/WEB-INF/cgi. This will ensure that you + * do not accidentally expose your cgi scripts' code to the outside world and that your cgis will be cleanly ensconced + * underneath the WEB-INF (i.e., non-content) area. + *

    + *

    + * The default CGI location is mentioned above. You have the flexibility to put CGIs wherever you want, however: + *

    + *

    + * The CGI search path will start at webAppRootDir + File.separator + cgiPathPrefix (or webAppRootDir alone if + * cgiPathPrefix is null). + *

    + *

    + * cgiPathPrefix is defined by setting this servlet's cgiPathPrefix init parameter + *

    + *

    + * CGI Specification:
    + * derived from http://cgi-spec.golux.com. A work-in-progress & expired + * Internet Draft. Note no actual RFC describing the CGI specification exists. Where the behavior of this servlet + * differs from the specification cited above, it is either documented here, a bug, or an instance where the + * specification cited differs from Best Community Practice (BCP). Such instances should be well-documented here. Please + * email the Tomcat group with amendments. + *

    + *

    + * Canonical metavariables:
    + * The CGI specification defines the following canonical metavariables:
    + * [excerpt from CGI specification] + * + *

    + *  AUTH_TYPE
    + *  CONTENT_LENGTH
    + *  CONTENT_TYPE
    + *  GATEWAY_INTERFACE
    + *  PATH_INFO
    + *  PATH_TRANSLATED
    + *  QUERY_STRING
    + *  REMOTE_ADDR
    + *  REMOTE_HOST
    + *  REMOTE_IDENT
    + *  REMOTE_USER
    + *  REQUEST_METHOD
    + *  SCRIPT_NAME
    + *  SERVER_NAME
    + *  SERVER_PORT
    + *  SERVER_PROTOCOL
    + *  SERVER_SOFTWARE
    + * 
    + *

    + * Metavariables with names beginning with the protocol name (e.g., "HTTP_ACCEPT") are also canonical in their + * description of request header fields. The number and meaning of these fields may change independently of this + * specification. (See also section 6.1.5 [of the CGI specification].) + *

    + * [end excerpt] + *

    Implementation notes

    + *

    + * standard input handling: If your script accepts standard input, then the client must start sending input + * within a certain timeout period, otherwise the servlet will assume no input is coming and carry on running the + * script. The script's the standard input will be closed and handling of any further input from the client is + * undefined. Most likely it will be ignored. If this behavior becomes undesirable, then this servlet needs to be + * enhanced to handle threading of the spawned process' stdin, stdout, and stderr (which should not be too hard).
    + * If you find your cgi scripts are timing out receiving input, you can set the init parameter + * stderrTimeout of your webapps' cgi-handling servlet. + *

    + *

    + * Metavariable Values: According to the CGI specification, implementations may choose to represent both null or + * missing values in an implementation-specific manner, but must define that manner. This implementation chooses to + * always define all required metavariables, but set the value to "" for all metavariables whose value is either null or + * undefined. PATH_TRANSLATED is the sole exception to this rule, as per the CGI Specification. + *

    + *

    + * NPH -- Non-parsed-header implementation: This implementation does not support the CGI NPH concept, whereby + * server ensures that the data supplied to the script are precisely as supplied by the client and unaltered by the + * server. + *

    + *

    + * The function of a servlet container (including Tomcat) is specifically designed to parse and possible alter + * CGI-specific variables, and as such makes NPH functionality difficult to support. + *

    + *

    + * The CGI specification states that compliant servers MAY support NPH output. It does not state servers MUST support + * NPH output to be unconditionally compliant. Thus, this implementation maintains unconditional compliance with the + * specification though NPH support is not present. + *

    + *

    + * The CGI specification is located at http://cgi-spec.golux.com. + *

    + *

    TODO:

    + *
      + *
    • Support for setting headers (for example, Location headers don't work) + *
    • Support for collapsing multiple header lines (per RFC 2616) + *
    • Ensure handling of POST method does not interfere with 2.3 Filters + *
    • Refactor some debug code out of core + *
    • Ensure header handling preserves encoding + *
    • Possibly rewrite CGIRunner.run()? + *
    • Possibly refactor CGIRunner and CGIEnvironment as non-inner classes? + *
    • Document handling of cgi stdin when there is no stdin + *
    • Revisit IOException handling in CGIRunner.run() + *
    • Better documentation + *
    • Confirm use of ServletInputStream.available() in CGIRunner.run() is not needed + *
    • [add more to this TODO list] + *
    + * + * @author Martin T Dengler [root@martindengler.com] + * @author Amy Roh + */ +public final class CGIServlet extends HttpServlet { + + private static final Log log = LogFactory.getLog(CGIServlet.class); + private static final StringManager sm = StringManager.getManager(CGIServlet.class); + + /* some vars below copied from Craig R. McClanahan's InvokerServlet */ + + private static final long serialVersionUID = 1L; + + private static final Set DEFAULT_SUPER_METHODS = new HashSet<>(); + private static final Pattern DEFAULT_CMD_LINE_ARGUMENTS_DECODED_PATTERN; + private static final String ALLOW_ANY_PATTERN = ".*"; + + static { + DEFAULT_SUPER_METHODS.add("HEAD"); + DEFAULT_SUPER_METHODS.add("OPTIONS"); + DEFAULT_SUPER_METHODS.add("TRACE"); + + if (JrePlatform.IS_WINDOWS) { + DEFAULT_CMD_LINE_ARGUMENTS_DECODED_PATTERN = Pattern.compile("[\\w\\Q-.\\/:\\E]+"); + } else { + // No restrictions + DEFAULT_CMD_LINE_ARGUMENTS_DECODED_PATTERN = null; + } + + } + + + /** + * The CGI search path will start at webAppRootDir + File.separator + cgiPathPrefix (or webAppRootDir alone if + * cgiPathPrefix is null) + */ + private String cgiPathPrefix = null; + + /** the executable to use with the script */ + private String cgiExecutable = "perl"; + + /** additional arguments for the executable */ + private List cgiExecutableArgs = null; + + /** the encoding to use for parameters */ + private String parameterEncoding = System.getProperty("file.encoding", "UTF-8"); + + /* The HTTP methods this Servlet will pass to the CGI script */ + private Set cgiMethods = new HashSet<>(); + private boolean cgiMethodsAll = false; + + + /** + * The time (in milliseconds) to wait for the reading of stderr to complete before terminating the CGI process. + */ + private long stderrTimeout = 2000; + + /** + * The regular expression used to select HTTP headers to be passed to the CGI process as environment variables. The + * name of the environment variable will be the name of the HTTP header converter to upper case, prefixed with + * HTTP_ and with all - characters converted to _. + */ + private Pattern envHttpHeadersPattern = + Pattern.compile("ACCEPT[-0-9A-Z]*|CACHE-CONTROL|COOKIE|HOST|IF-[-0-9A-Z]*|REFERER|USER-AGENT"); + + /** object used to ensure multiple threads don't try to expand same file */ + private static final Object expandFileLock = new Object(); + + /** the shell environment variables to be passed to the CGI script */ + private final Map shellEnv = new HashMap<>(); + + /** + * Enable creation of script command line arguments from query-string. See + * https://tools.ietf.org/html/rfc3875#section-4.4 4.4. The Script Command Line + */ + private boolean enableCmdLineArguments = false; + + /** + * Limits the encoded form of individual command line arguments. By default values are limited to those allowed by + * the RFC. See https://tools.ietf.org/html/rfc3875#section-4.4 Uses \Q...\E to avoid individual quoting. + */ + private Pattern cmdLineArgumentsEncodedPattern = Pattern.compile("[\\w\\Q%;/?:@&,$-.!~*'()\\E]+"); + + /** + * Limits the decoded form of individual command line arguments. Default varies by platform. + */ + private Pattern cmdLineArgumentsDecodedPattern = DEFAULT_CMD_LINE_ARGUMENTS_DECODED_PATTERN; + + + /** + * Sets instance variables. + *

    + * Modified from Craig R. McClanahan's InvokerServlet + *

    + * + * @param config a ServletConfig object containing the servlet's configuration and initialization + * parameters + * + * @exception ServletException if an exception has occurred that interferes with the servlet's normal operation + */ + @Override + public void init(ServletConfig config) throws ServletException { + + super.init(config); + + // Set our properties from the initialization parameters + cgiPathPrefix = getServletConfig().getInitParameter("cgiPathPrefix"); + boolean passShellEnvironment = + Boolean.parseBoolean(getServletConfig().getInitParameter("passShellEnvironment")); + + if (passShellEnvironment) { + shellEnv.putAll(System.getenv()); + } + + Enumeration e = config.getInitParameterNames(); + while (e.hasMoreElements()) { + String initParamName = e.nextElement(); + if (initParamName.startsWith("environment-variable-")) { + if (initParamName.length() == 21) { + throw new ServletException(sm.getString("cgiServlet.emptyEnvVarName")); + } + shellEnv.put(initParamName.substring(21), config.getInitParameter(initParamName)); + } + } + + if (getServletConfig().getInitParameter("executable") != null) { + cgiExecutable = getServletConfig().getInitParameter("executable"); + } + + if (getServletConfig().getInitParameter("executable-arg-1") != null) { + List args = new ArrayList<>(); + for (int i = 1;; i++) { + String arg = getServletConfig().getInitParameter("executable-arg-" + i); + if (arg == null) { + break; + } + args.add(arg); + } + cgiExecutableArgs = args; + } + + if (getServletConfig().getInitParameter("parameterEncoding") != null) { + parameterEncoding = getServletConfig().getInitParameter("parameterEncoding"); + } + + if (getServletConfig().getInitParameter("stderrTimeout") != null) { + stderrTimeout = Long.parseLong(getServletConfig().getInitParameter("stderrTimeout")); + } + + if (getServletConfig().getInitParameter("envHttpHeaders") != null) { + envHttpHeadersPattern = Pattern.compile(getServletConfig().getInitParameter("envHttpHeaders")); + } + + if (getServletConfig().getInitParameter("enableCmdLineArguments") != null) { + enableCmdLineArguments = Boolean.parseBoolean(config.getInitParameter("enableCmdLineArguments")); + } + + if (getServletConfig().getInitParameter("cgiMethods") != null) { + String paramValue = getServletConfig().getInitParameter("cgiMethods"); + paramValue = paramValue.trim(); + if ("*".equals(paramValue)) { + cgiMethodsAll = true; + } else { + String[] methods = paramValue.split(","); + for (String method : methods) { + String trimmedMethod = method.trim(); + cgiMethods.add(trimmedMethod); + } + } + } else { + cgiMethods.add("GET"); + cgiMethods.add("POST"); + } + + if (getServletConfig().getInitParameter("cmdLineArgumentsEncoded") != null) { + cmdLineArgumentsEncodedPattern = + Pattern.compile(getServletConfig().getInitParameter("cmdLineArgumentsEncoded")); + } + + String value = getServletConfig().getInitParameter("cmdLineArgumentsDecoded"); + if (ALLOW_ANY_PATTERN.equals(value)) { + // Optimisation for case where anything is allowed + cmdLineArgumentsDecodedPattern = null; + } else if (value != null) { + cmdLineArgumentsDecodedPattern = Pattern.compile(value); + } + } + + + /** + * Logs important Servlet API and container information. + *

    + * Based on SnoopAllServlet by Craig R. McClanahan + *

    + * + * @param req HttpServletRequest object used as source of information + * + * @exception IOException if a write operation exception occurs + */ + private void printServletEnvironment(HttpServletRequest req) throws IOException { + + // Document the properties from ServletRequest + log.trace("ServletRequest Properties"); + Enumeration attrs = req.getAttributeNames(); + while (attrs.hasMoreElements()) { + String attr = attrs.nextElement(); + log.trace("Request Attribute: " + attr + ": [ " + req.getAttribute(attr) + "]"); + } + log.trace("Character Encoding: [" + req.getCharacterEncoding() + "]"); + log.trace("Content Length: [" + req.getContentLengthLong() + "]"); + log.trace("Content Type: [" + req.getContentType() + "]"); + Enumeration locales = req.getLocales(); + while (locales.hasMoreElements()) { + Locale locale = locales.nextElement(); + log.trace("Locale: [" + locale + "]"); + } + Enumeration params = req.getParameterNames(); + while (params.hasMoreElements()) { + String param = params.nextElement(); + for (String value : req.getParameterValues(param)) { + log.trace("Request Parameter: " + param + ": [" + value + "]"); + } + } + log.trace("Protocol: [" + req.getProtocol() + "]"); + log.trace("Remote Address: [" + req.getRemoteAddr() + "]"); + log.trace("Remote Host: [" + req.getRemoteHost() + "]"); + log.trace("Scheme: [" + req.getScheme() + "]"); + log.trace("Secure: [" + req.isSecure() + "]"); + log.trace("Server Name: [" + req.getServerName() + "]"); + log.trace("Server Port: [" + req.getServerPort() + "]"); + + // Document the properties from HttpServletRequest + log.trace("HttpServletRequest Properties"); + log.trace("Auth Type: [" + req.getAuthType() + "]"); + log.trace("Context Path: [" + req.getContextPath() + "]"); + Cookie cookies[] = req.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + log.trace("Cookie: " + cookie.getName() + ": [" + cookie.getValue() + "]"); + } + } + Enumeration headers = req.getHeaderNames(); + while (headers.hasMoreElements()) { + String header = headers.nextElement(); + log.trace("HTTP Header: " + header + ": [" + req.getHeader(header) + "]"); + } + log.trace("Method: [" + req.getMethod() + "]"); + log.trace("Path Info: [" + req.getPathInfo() + "]"); + log.trace("Path Translated: [" + req.getPathTranslated() + "]"); + log.trace("Query String: [" + req.getQueryString() + "]"); + log.trace("Remote User: [" + req.getRemoteUser() + "]"); + log.trace("Requested Session ID: [" + req.getRequestedSessionId() + "]"); + log.trace("Requested Session ID From Cookie: [" + req.isRequestedSessionIdFromCookie() + "]"); + log.trace("Requested Session ID From URL: [" + req.isRequestedSessionIdFromURL() + "]"); + log.trace("Requested Session ID Valid: [" + req.isRequestedSessionIdValid() + "]"); + log.trace("Request URI: [" + req.getRequestURI() + "]"); + log.trace("Servlet Path: [" + req.getServletPath() + "]"); + log.trace("User Principal: [" + req.getUserPrincipal() + "]"); + + // Process the current session (if there is one) + HttpSession session = req.getSession(false); + if (session != null) { + + // Document the session properties + log.trace("HttpSession Properties"); + log.trace("ID: [" + session.getId() + "]"); + log.trace("Creation Time: [" + new Date(session.getCreationTime()) + "]"); + log.trace("Last Accessed Time: [" + new Date(session.getLastAccessedTime()) + "]"); + log.trace("Max Inactive Interval: [" + session.getMaxInactiveInterval() + "]"); + + // Document the session attributes + attrs = session.getAttributeNames(); + while (attrs.hasMoreElements()) { + String attr = attrs.nextElement(); + log.trace("Session Attribute: " + attr + ": [" + session.getAttribute(attr) + "]"); + } + } + + // Document the servlet configuration properties + log.trace("ServletConfig Properties"); + log.trace("Servlet Name: [" + getServletConfig().getServletName() + "]"); + + // Document the servlet configuration initialization parameters + params = getServletConfig().getInitParameterNames(); + while (params.hasMoreElements()) { + String param = params.nextElement(); + String value = getServletConfig().getInitParameter(param); + log.trace("Servlet Init Param: " + param + ": [" + value + "]"); + } + + // Document the servlet context properties + log.trace("ServletContext Properties"); + log.trace("Major Version: [" + getServletContext().getMajorVersion() + "]"); + log.trace("Minor Version: [" + getServletContext().getMinorVersion() + "]"); + log.trace("Real Path for '/': [" + getServletContext().getRealPath("/") + "]"); + log.trace("Server Info: [" + getServletContext().getServerInfo() + "]"); + + // Document the servlet context initialization parameters + log.trace("ServletContext Initialization Parameters"); + params = getServletContext().getInitParameterNames(); + while (params.hasMoreElements()) { + String param = params.nextElement(); + String value = getServletContext().getInitParameter(param); + log.trace("Servlet Context Init Param: " + param + ": [" + value + "]"); + } + + // Document the servlet context attributes + log.trace("ServletContext Attributes"); + attrs = getServletContext().getAttributeNames(); + while (attrs.hasMoreElements()) { + String attr = attrs.nextElement(); + log.trace("Servlet Context Attribute: " + attr + ": [" + getServletContext().getAttribute(attr) + "]"); + } + } + + + @Override + protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { + + String method = req.getMethod(); + if (cgiMethodsAll || cgiMethods.contains(method)) { + doGet(req, res); + } else if (DEFAULT_SUPER_METHODS.contains(method)) { + // If the CGI servlet is explicitly configured to handle one of + // these methods it will be handled in the previous condition + super.service(req, res); + } else { + // Unsupported method + res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + } + + + /** + * Provides CGI Gateway service. + * + * @param req HttpServletRequest passed in by servlet container + * @param res HttpServletResponse passed in by servlet container + * + * @exception ServletException if a servlet-specific exception occurs + * @exception IOException if a read/write exception occurs + */ + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { + + CGIEnvironment cgiEnv = new CGIEnvironment(req, getServletContext()); + + if (cgiEnv.isValid()) { + CGIRunner cgi = new CGIRunner(cgiEnv.getCommand(), cgiEnv.getEnvironment(), cgiEnv.getWorkingDirectory(), + cgiEnv.getParameters()); + + if ("POST".equals(req.getMethod())) { + cgi.setInput(req.getInputStream()); + } + cgi.setResponse(res); + cgi.run(); + } else { + res.sendError(404); + } + + if (log.isTraceEnabled()) { + String[] cgiEnvLines = cgiEnv.toString().split(System.lineSeparator()); + for (String cgiEnvLine : cgiEnvLines) { + log.trace(cgiEnvLine); + } + + printServletEnvironment(req); + } + } + + + @Override + protected void doOptions(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { + // Note: This method will never be called if cgiMethods is "*" so that + // case does nto need to be handled here. + Set allowedMethods = new HashSet<>(); + allowedMethods.addAll(cgiMethods); + allowedMethods.addAll(DEFAULT_SUPER_METHODS); + + StringBuilder headerValue = new StringBuilder(); + + for (String method : allowedMethods) { + headerValue.append(method); + headerValue.append(','); + } + + // Remove trailing comma + headerValue.deleteCharAt(headerValue.length() - 1); + + res.setHeader("allow", headerValue.toString()); + } + + + /* + * Behaviour depends on the status code. + * + * Status < 400 - Calls setStatus. Returns false. CGI servlet will provide the response body. + * + * Status >= 400 - Calls sendError(status), returns true. Standard error page mechanism will provide the response + * body. + */ + private boolean setStatus(HttpServletResponse response, int status) throws IOException { + if (status >= HttpServletResponse.SC_BAD_REQUEST) { + response.sendError(status); + return true; + } else { + response.setStatus(status); + return false; + } + } + + + /** + * Encapsulates the CGI environment and rules to derive that environment from the servlet container and request + * information. + */ + protected class CGIEnvironment { + + + /** context of the enclosing servlet */ + private ServletContext context = null; + + /** context path of enclosing servlet */ + private String contextPath = null; + + /** servlet URI of the enclosing servlet */ + private String servletPath = null; + + /** pathInfo for the current request */ + private String pathInfo = null; + + /** real file system directory of the enclosing servlet's web app */ + private String webAppRootDir = null; + + /** tempdir for context - used to expand scripts in unexpanded wars */ + private File tmpDir = null; + + /** derived cgi environment */ + private Map env = null; + + /** cgi command to be invoked */ + private String command = null; + + /** cgi command's desired working directory */ + private final File workingDirectory; + + /** cgi command's command line parameters */ + private final ArrayList cmdLineParameters = new ArrayList<>(); + + /** whether or not this object is valid or not */ + private final boolean valid; + + + /** + * Creates a CGIEnvironment and derives the necessary environment, query parameters, working directory, cgi + * command, etc. + * + * @param req HttpServletRequest for information provided by the Servlet API + * @param context ServletContext for information provided by the Servlet API + * + * @throws IOException an IO error occurred + */ + protected CGIEnvironment(HttpServletRequest req, ServletContext context) throws IOException { + setupFromContext(context); + boolean valid = setupFromRequest(req); + + if (valid) { + valid = setCGIEnvironment(req); + } + + if (valid) { + workingDirectory = new File(command.substring(0, command.lastIndexOf(File.separator))); + } else { + workingDirectory = null; + } + + this.valid = valid; + } + + + /** + * Uses the ServletContext to set some CGI variables + * + * @param context ServletContext for information provided by the Servlet API + */ + protected void setupFromContext(ServletContext context) { + this.context = context; + this.webAppRootDir = context.getRealPath("/"); + this.tmpDir = (File) context.getAttribute(ServletContext.TEMPDIR); + } + + + /** + * Uses the HttpServletRequest to set most CGI variables + * + * @param req HttpServletRequest for information provided by the Servlet API + * + * @return true if the request was parsed without error, false if there was a problem + * + * @throws UnsupportedEncodingException Unknown encoding + */ + protected boolean setupFromRequest(HttpServletRequest req) throws UnsupportedEncodingException { + + boolean isIncluded = false; + + // Look to see if this request is an include + if (req.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) { + isIncluded = true; + } + if (isIncluded) { + this.contextPath = (String) req.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH); + this.servletPath = (String) req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + this.pathInfo = (String) req.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); + } else { + this.contextPath = req.getContextPath(); + this.servletPath = req.getServletPath(); + this.pathInfo = req.getPathInfo(); + } + // If getPathInfo() returns null, must be using extension mapping + // In this case, pathInfo should be same as servletPath + if (this.pathInfo == null) { + this.pathInfo = this.servletPath; + } + + // If the request method is GET, POST or HEAD and the query string + // does not contain an unencoded "=" this is an indexed query. + // The parsed query string becomes the command line parameters + // for the cgi command. + if (enableCmdLineArguments && (req.getMethod().equals("GET") || req.getMethod().equals("POST") || + req.getMethod().equals("HEAD"))) { + String qs; + if (isIncluded) { + qs = (String) req.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING); + } else { + qs = req.getQueryString(); + } + if (qs != null && qs.indexOf('=') == -1) { + StringTokenizer qsTokens = new StringTokenizer(qs, "+"); + while (qsTokens.hasMoreTokens()) { + String encodedArgument = qsTokens.nextToken(); + if (!cmdLineArgumentsEncodedPattern.matcher(encodedArgument).matches()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("cgiServlet.invalidArgumentEncoded", encodedArgument, + cmdLineArgumentsEncodedPattern.toString())); + } + return false; + } + + String decodedArgument = URLDecoder.decode(encodedArgument, parameterEncoding); + if (cmdLineArgumentsDecodedPattern != null && + !cmdLineArgumentsDecodedPattern.matcher(decodedArgument).matches()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("cgiServlet.invalidArgumentDecoded", decodedArgument, + cmdLineArgumentsDecodedPattern.toString())); + } + return false; + } + + cmdLineParameters.add(decodedArgument); + } + } + } + + return true; + } + + + /** + * Resolves core information about the cgi script. + *

    + * Example URI: + *

    + * + *
    +         *  /servlet/cgigateway/dir1/realCGIscript/pathinfo1
    +         * 
    + *
      + *
    • path = $CATALINA_HOME/mywebapp/dir1/realCGIscript + *
    • scriptName = /servlet/cgigateway/dir1/realCGIscript + *
    • cgiName = /dir1/realCGIscript + *
    • name = realCGIscript + *
    + *

    + * CGI search algorithm: search the real path below <my-webapp-root> and find the first non-directory in + * the getPathTranslated("/"), reading/searching from left-to-right. + *

    + *

    + * The CGI search path will start at webAppRootDir + File.separator + cgiPathPrefix (or webAppRootDir alone if + * cgiPathPrefix is null). + *

    + *

    + * cgiPathPrefix is defined by setting this servlet's cgiPathPrefix init parameter + *

    + * + * @param pathInfo String from HttpServletRequest.getPathInfo() + * @param webAppRootDir String from context.getRealPath("/") + * @param contextPath String as from HttpServletRequest.getContextPath() + * @param servletPath String as from HttpServletRequest.getServletPath() + * @param cgiPathPrefix subdirectory of webAppRootDir below which the web app's CGIs may be stored; can be null. + * The CGI search path will start at webAppRootDir + File.separator + cgiPathPrefix (or + * webAppRootDir alone if cgiPathPrefix is null). cgiPathPrefix is defined by setting + * the servlet's cgiPathPrefix init parameter. + * + * @return + *
      + *
    • path - full file-system path to valid cgi script, or null if no cgi was found + *
    • scriptName - CGI variable SCRIPT_NAME; the full URL path to valid cgi script or + * null if no cgi was found + *
    • cgiName - servlet pathInfo fragment corresponding to the cgi script itself, or + * null if not found + *
    • name - simple name (no directories) of the cgi script, or null if no cgi was + * found + *
    + */ + protected String[] findCGI(String pathInfo, String webAppRootDir, String contextPath, String servletPath, + String cgiPathPrefix) { + String path = null; + String name = null; + String scriptname = null; + + if (webAppRootDir.lastIndexOf(File.separator) == (webAppRootDir.length() - 1)) { + // strip the trailing "/" from the webAppRootDir + webAppRootDir = webAppRootDir.substring(0, (webAppRootDir.length() - 1)); + } + + if (cgiPathPrefix != null) { + webAppRootDir = webAppRootDir + File.separator + cgiPathPrefix; + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("cgiServlet.find.path", pathInfo, webAppRootDir)); + } + + File currentLocation = new File(webAppRootDir); + StringTokenizer dirWalker = new StringTokenizer(pathInfo, "/"); + if (log.isTraceEnabled()) { + log.trace(sm.getString("cgiServlet.find.location", currentLocation.getAbsolutePath())); + } + StringBuilder cginameBuilder = new StringBuilder(); + while (!currentLocation.isFile() && dirWalker.hasMoreElements()) { + String nextElement = (String) dirWalker.nextElement(); + currentLocation = new File(currentLocation, nextElement); + cginameBuilder.append('/').append(nextElement); + if (log.isTraceEnabled()) { + log.trace(sm.getString("cgiServlet.find.location", currentLocation.getAbsolutePath())); + } + } + String cginame = cginameBuilder.toString(); + if (!currentLocation.isFile()) { + return new String[] { null, null, null, null }; + } + + path = currentLocation.getAbsolutePath(); + name = currentLocation.getName(); + + if (servletPath.startsWith(cginame)) { + scriptname = contextPath + cginame; + } else { + scriptname = contextPath + servletPath + cginame; + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("cgiServlet.find.found", name, path, scriptname, cginame)); + } + return new String[] { path, scriptname, cginame, name }; + } + + /** + * Constructs the CGI environment to be supplied to the invoked CGI script; relies heavily on Servlet API + * methods and findCGI + * + * @param req request associated with the CGI Invocation + * + * @return true if environment was set OK, false if there was a problem and no environment was set + * + * @throws IOException an IO error occurred + */ + protected boolean setCGIEnvironment(HttpServletRequest req) throws IOException { + + /* + * This method is slightly ugly; c'est la vie. + * "You cannot stop [ugliness], you can only hope to contain [it]" (apologies to Marv Albert regarding MJ) + */ + + // Add the shell environment variables (if any) + Map envp = new HashMap<>(shellEnv); + + // Add the CGI environment variables + String sPathInfoOrig = null; + String sPathInfoCGI = null; + String sPathTranslatedCGI = null; + String sCGIFullPath = null; + String sCGIScriptName = null; + String sCGIFullName = null; + String sCGIName = null; + String[] sCGINames; + + + sPathInfoOrig = this.pathInfo; + sPathInfoOrig = sPathInfoOrig == null ? "" : sPathInfoOrig; + + if (webAppRootDir == null) { + // The app has not been deployed in exploded form + webAppRootDir = tmpDir.toString(); + expandCGIScript(); + } + + sCGINames = findCGI(sPathInfoOrig, webAppRootDir, contextPath, servletPath, cgiPathPrefix); + + sCGIFullPath = sCGINames[0]; + sCGIScriptName = sCGINames[1]; + sCGIFullName = sCGINames[2]; + sCGIName = sCGINames[3]; + + if (sCGIFullPath == null || sCGIScriptName == null || sCGIFullName == null || sCGIName == null) { + return false; + } + + envp.put("SERVER_SOFTWARE", "TOMCAT"); + + envp.put("SERVER_NAME", nullsToBlanks(req.getServerName())); + + envp.put("GATEWAY_INTERFACE", "CGI/1.1"); + + envp.put("SERVER_PROTOCOL", nullsToBlanks(req.getProtocol())); + + int port = req.getServerPort(); + Integer iPort = (port == 0 ? Integer.valueOf(-1) : Integer.valueOf(port)); + envp.put("SERVER_PORT", iPort.toString()); + + envp.put("REQUEST_METHOD", nullsToBlanks(req.getMethod())); + + envp.put("REQUEST_URI", nullsToBlanks(req.getRequestURI())); + + + /*- + * PATH_INFO should be determined by using sCGIFullName: + * 1) Let sCGIFullName not end in a "/" (see method findCGI) + * 2) Let sCGIFullName equal the pathInfo fragment which + * corresponds to the actual cgi script. + * 3) Thus, PATH_INFO = request.getPathInfo().substring( + * sCGIFullName.length()) + * + * (see method findCGI, where the real work is done) + * + */ + if (pathInfo == null || (pathInfo.substring(sCGIFullName.length()).length() <= 0)) { + sPathInfoCGI = ""; + } else { + sPathInfoCGI = pathInfo.substring(sCGIFullName.length()); + } + envp.put("PATH_INFO", sPathInfoCGI); + + + /*- + * PATH_TRANSLATED must be determined after PATH_INFO (and the + * implied real cgi-script) has been taken into account. + * + * The following example demonstrates: + * + * servlet info = /servlet/cgigw/dir1/dir2/cgi1/trans1/trans2 + * cgifullpath = /servlet/cgigw/dir1/dir2/cgi1 + * path_info = /trans1/trans2 + * webAppRootDir = servletContext.getRealPath("/") + * + * path_translated = servletContext.getRealPath("/trans1/trans2") + * + * That is, PATH_TRANSLATED = webAppRootDir + sPathInfoCGI + * (unless sPathInfoCGI is null or blank, then the CGI + * specification dictates that the PATH_TRANSLATED metavariable + * SHOULD NOT be defined. + * + */ + if (!sPathInfoCGI.isEmpty()) { + sPathTranslatedCGI = context.getRealPath(sPathInfoCGI); + } + if (sPathTranslatedCGI == null || "".equals(sPathTranslatedCGI)) { + // NOOP + } else { + envp.put("PATH_TRANSLATED", nullsToBlanks(sPathTranslatedCGI)); + } + + + envp.put("SCRIPT_NAME", nullsToBlanks(sCGIScriptName)); + + envp.put("QUERY_STRING", nullsToBlanks(req.getQueryString())); + + envp.put("REMOTE_HOST", nullsToBlanks(req.getRemoteHost())); + + envp.put("REMOTE_ADDR", nullsToBlanks(req.getRemoteAddr())); + + envp.put("AUTH_TYPE", nullsToBlanks(req.getAuthType())); + + envp.put("REMOTE_USER", nullsToBlanks(req.getRemoteUser())); + + envp.put("REMOTE_IDENT", ""); // not necessary for full compliance + + envp.put("CONTENT_TYPE", nullsToBlanks(req.getContentType())); + + + /* + * Note CGI spec says CONTENT_LENGTH must be NULL ("") or undefined if there is no content, so we cannot put + * 0 or -1 in as per the Servlet API spec. + */ + long contentLength = req.getContentLengthLong(); + String sContentLength = (contentLength <= 0 ? "" : Long.toString(contentLength)); + envp.put("CONTENT_LENGTH", sContentLength); + + + Enumeration headers = req.getHeaderNames(); + String header = null; + while (headers.hasMoreElements()) { + header = null; + header = headers.nextElement().toUpperCase(Locale.ENGLISH); + // REMIND: rewrite multiple headers as if received as single + // REMIND: change character set + // REMIND: I forgot what the previous REMIND means + if (envHttpHeadersPattern.matcher(header).matches()) { + envp.put("HTTP_" + header.replace('-', '_'), req.getHeader(header)); + } + } + + File fCGIFullPath = new File(sCGIFullPath); + command = fCGIFullPath.getCanonicalPath(); + + envp.put("X_TOMCAT_SCRIPT_PATH", command); // for kicks + + envp.put("SCRIPT_FILENAME", command); // for PHP + + this.env = envp; + + return true; + } + + /** + * Extracts requested resource from web app archive to context work directory to enable CGI script to be + * executed. + */ + protected void expandCGIScript() { + StringBuilder srcPath = new StringBuilder(); + StringBuilder destPath = new StringBuilder(); + InputStream is = null; + + // paths depend on mapping + if (cgiPathPrefix == null) { + srcPath.append(pathInfo); + is = context.getResourceAsStream(srcPath.toString()); + destPath.append(tmpDir); + destPath.append(pathInfo); + } else { + // essentially same search algorithm as findCGI() + srcPath.append(cgiPathPrefix); + StringTokenizer pathWalker = new StringTokenizer(pathInfo, "/"); + // start with first element + while (pathWalker.hasMoreElements() && (is == null)) { + srcPath.append('/'); + srcPath.append(pathWalker.nextElement()); + is = context.getResourceAsStream(srcPath.toString()); + } + destPath.append(tmpDir); + destPath.append('/'); + destPath.append(srcPath); + } + + if (is == null) { + // didn't find anything, give up now + log.warn(sm.getString("cgiServlet.expandNotFound", srcPath)); + return; + } + + try { + File f = new File(destPath.toString()); + if (f.exists()) { + // Don't need to expand if it already exists + return; + } + + // create directories + File dir = f.getParentFile(); + if (!dir.mkdirs() && !dir.isDirectory()) { + log.warn(sm.getString("cgiServlet.expandCreateDirFail", dir.getAbsolutePath())); + return; + } + + try { + synchronized (expandFileLock) { + // make sure file doesn't exist + if (f.exists()) { + return; + } + + // create file + if (!f.createNewFile()) { + return; + } + + Files.copy(is, f.toPath()); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("cgiServlet.expandOk", srcPath, destPath)); + } + } + } catch (IOException ioe) { + log.warn(sm.getString("cgiServlet.expandFail", srcPath, destPath), ioe); + // delete in case file is corrupted + if (f.exists()) { + if (!f.delete()) { + log.warn(sm.getString("cgiServlet.expandDeleteFail", f.getAbsolutePath())); + } + } + } + } finally { + try { + is.close(); + } catch (IOException e) { + log.warn(sm.getString("cgiServlet.expandCloseFail", srcPath), e); + } + } + } + + + /** + * Returns important CGI environment information in a multi-line text format. + * + * @return CGI environment info + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + + sb.append("CGIEnvironment Info:"); + sb.append(System.lineSeparator()); + + if (isValid()) { + sb.append("Validity: [true]"); + sb.append(System.lineSeparator()); + + sb.append("Environment values:"); + sb.append(System.lineSeparator()); + for (Entry entry : env.entrySet()) { + sb.append(" "); + sb.append(entry.getKey()); + sb.append(": ["); + sb.append(blanksToString(entry.getValue(), "will be set to blank")); + sb.append(']'); + sb.append(System.lineSeparator()); + } + + sb.append("Derived Command :["); + sb.append(nullsToBlanks(command)); + sb.append(']'); + sb.append(System.lineSeparator()); + + + sb.append("Working Directory: ["); + if (workingDirectory != null) { + sb.append(workingDirectory.toString()); + } + sb.append(']'); + sb.append(System.lineSeparator()); + + sb.append("Command Line Params:"); + sb.append(System.lineSeparator()); + for (String param : cmdLineParameters) { + sb.append(" ["); + sb.append(param); + sb.append(']'); + sb.append(System.lineSeparator()); + } + } else { + sb.append("Validity: [false]"); + sb.append(System.lineSeparator()); + sb.append("CGI script not found or not specified."); + sb.append(System.lineSeparator()); + sb.append("Check the HttpServletRequest pathInfo property to see if it is what "); + sb.append(System.lineSeparator()); + sb.append("you meant it to be. You must specify an existent and executable file "); + sb.append(System.lineSeparator()); + sb.append("as part of the path-info."); + sb.append(System.lineSeparator()); + } + + return sb.toString(); + } + + + /** + * Gets derived command string + * + * @return command string + */ + protected String getCommand() { + return command; + } + + + /** + * Gets derived CGI working directory + * + * @return working directory + */ + protected File getWorkingDirectory() { + return workingDirectory; + } + + + /** + * Gets derived CGI environment + * + * @return CGI environment + */ + protected Map getEnvironment() { + return env; + } + + + /** + * Gets derived CGI query parameters + * + * @return CGI query parameters + */ + protected ArrayList getParameters() { + return cmdLineParameters; + } + + + /** + * Gets validity status + * + * @return true if this environment is valid, false otherwise + */ + protected boolean isValid() { + return valid; + } + + + /** + * Converts null strings to blank strings ("") + * + * @param s string to be converted if necessary + * + * @return a non-null string, either the original or the empty string ("") if the original was null + */ + protected String nullsToBlanks(String s) { + return nullsToString(s, ""); + } + + + /** + * Converts null strings to another string + * + * @param couldBeNull string to be converted if necessary + * @param subForNulls string to return instead of a null string + * + * @return a non-null string, either the original or the substitute string if the original was null + */ + protected String nullsToString(String couldBeNull, String subForNulls) { + return (couldBeNull == null ? subForNulls : couldBeNull); + } + + + /** + * Converts blank strings to another string + * + * @param couldBeBlank string to be converted if necessary + * @param subForBlanks string to return instead of a blank string + * + * @return a non-null string, either the original or the substitute string if the original was null + * or empty ("") + */ + protected String blanksToString(String couldBeBlank, String subForBlanks) { + return (couldBeBlank == null || couldBeBlank.isEmpty()) ? subForBlanks : couldBeBlank; + } + + + } // class CGIEnvironment + + + /** + * Encapsulates the knowledge of how to run a CGI script, given the script's desired environment and (optionally) + * input/output streams + *

    + * Exposes a run method used to actually invoke the CGI. + *

    + *

    + * The CGI environment and settings are derived from the information passed to the constructor. + *

    + *

    + * The input and output streams can be set by the setInput and setResponse methods, + * respectively. + *

    + */ + protected class CGIRunner { + + /** script/command to be executed */ + private final String command; + + /** environment used when invoking the cgi script */ + private final Map env; + + /** working directory used when invoking the cgi script */ + private final File wd; + + /** command line parameters to be passed to the invoked script */ + private final ArrayList params; + + /** stdin to be passed to cgi script */ + private InputStream stdin = null; + + /** response object used to set headers & get output stream */ + private HttpServletResponse response = null; + + /** boolean tracking whether this object has enough info to run() */ + private boolean readyToRun = false; + + + /** + * Creates a CGIRunner and initializes its environment, working directory, and query parameters.
    + * Input/output streams (optional) are set using the setInput and setResponse methods, + * respectively. + * + * @param command string full path to command to be executed + * @param env Map with the desired script environment + * @param wd File with the script's desired working directory + * @param params ArrayList with the script's query command line parameters as strings + */ + protected CGIRunner(String command, Map env, File wd, ArrayList params) { + this.command = command; + this.env = env; + this.wd = wd; + this.params = params; + updateReadyStatus(); + } + + + /** + * Checks and sets ready status + */ + protected void updateReadyStatus() { + if (command != null && env != null && wd != null && params != null && response != null) { + readyToRun = true; + } else { + readyToRun = false; + } + } + + + /** + * Gets ready status + * + * @return false if not ready (run will throw an exception), true if ready + */ + protected boolean isReady() { + return readyToRun; + } + + + /** + * Sets HttpServletResponse object used to set headers and send output to + * + * @param response HttpServletResponse to be used + */ + protected void setResponse(HttpServletResponse response) { + this.response = response; + updateReadyStatus(); + } + + + /** + * Sets standard input to be passed on to the invoked cgi script + * + * @param stdin InputStream to be used + */ + protected void setInput(InputStream stdin) { + this.stdin = stdin; + updateReadyStatus(); + } + + + /** + * Converts a Map to a String array by converting each key/value pair in the Map to a String in the form + * "key=value" (key + "=" + map.get(key).toString()) + * + * @param map Map to convert + * + * @return converted string array + * + * @exception NullPointerException if a hash key has a null value + */ + protected String[] mapToStringArray(Map map) throws NullPointerException { + List list = new ArrayList<>(map.size()); + for (Entry entry : map.entrySet()) { + list.add(entry.getKey() + "=" + entry.getValue().toString()); + } + return list.toArray(new String[0]); + } + + + /** + * Executes a CGI script with the desired environment, current working directory, and input/output streams + *

    + * This implements the following CGI specification recommendations: + *

    + *
      + *
    • Servers SHOULD provide the "query" component of the script-URI as command-line arguments to + * scripts if it does not contain any unencoded "=" characters and the command-line arguments can be generated + * in an unambiguous manner. + *
    • Servers SHOULD set the AUTH_TYPE metavariable to the value of the "auth-scheme" token of the + * "Authorization" if it was supplied as part of the request header. See + * getCGIEnvironment method. + *
    • Where applicable, servers SHOULD set the current working directory to the directory in which the script + * is located before invoking it. + *
    • Server implementations SHOULD define their behavior for the following cases: + *
        + *
      • Allowed characters in pathInfo: This implementation does not allow ASCII NUL nor any character + * which cannot be URL-encoded according to internet standards; + *
      • Allowed characters in path segments: This implementation does not allow non-terminal NULL segments + * in the the path -- IOExceptions may be thrown; + *
      • "." and ".." path segments: This implementation does not allow + * "." and ".." in the the path, and such characters will result in an IOException + * being thrown (this should never happen since Tomcat normalises the requestURI before determining the + * contextPath, servletPath and pathInfo); + *
      • Implementation limitations: This implementation does not impose any limitations except as + * documented above. This implementation may be limited by the servlet container used to house this + * implementation. In particular, all the primary CGI variable values are derived either directly or indirectly + * from the container's implementation of the Servlet API methods. + *
      + *
    + * + * @exception IOException if problems during reading/writing occur + * + * @see java.lang.Runtime#exec(String command, String[] envp, File dir) + */ + protected void run() throws IOException { + + /* + * REMIND: this method feels too big; should it be re-written? + */ + + if (!isReady()) { + throw new IOException(sm.getString("cgiServlet.notReady")); + } + + if (log.isTraceEnabled()) { + log.trace("envp: [" + env + "], command: [" + command + "]"); + } + + if ((command.contains(File.separator + "." + File.separator)) || + (command.contains(File.separator + "..")) || (command.contains(".." + File.separator))) { + throw new IOException(sm.getString("cgiServlet.invalidCommand", command)); + } + + /* + * original content/structure of this section taken from + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4216884 with major modifications by Martin Dengler + */ + Runtime rt = null; + BufferedReader cgiHeaderReader = null; + InputStream cgiOutput = null; + BufferedReader commandsStdErr = null; + Thread errReaderThread = null; + BufferedOutputStream commandsStdIn = null; + Process proc = null; + int bufRead = -1; + + List cmdAndArgs = new ArrayList<>(); + if (cgiExecutable.length() != 0) { + cmdAndArgs.add(cgiExecutable); + } + if (cgiExecutableArgs != null) { + cmdAndArgs.addAll(cgiExecutableArgs); + } + cmdAndArgs.add(command); + cmdAndArgs.addAll(params); + + try { + rt = Runtime.getRuntime(); + proc = rt.exec(cmdAndArgs.toArray(new String[0]), mapToStringArray(env), wd); + + String sContentLength = env.get("CONTENT_LENGTH"); + + if (!"".equals(sContentLength)) { + commandsStdIn = new BufferedOutputStream(proc.getOutputStream()); + IOTools.flow(stdin, commandsStdIn); + commandsStdIn.flush(); + commandsStdIn.close(); + } + + /* + * we want to wait for the process to exit, Process.waitFor() is useless in our situation; see + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4223650 + */ + + boolean isRunning = true; + commandsStdErr = new BufferedReader(new InputStreamReader(proc.getErrorStream())); + final BufferedReader stdErrRdr = commandsStdErr; + + errReaderThread = new Thread(() -> sendToLog(stdErrRdr)); + errReaderThread.start(); + + InputStream cgiHeaderStream = new HTTPHeaderInputStream(proc.getInputStream()); + cgiHeaderReader = new BufferedReader(new InputStreamReader(cgiHeaderStream)); + + // Need to be careful here. If sendError() is called the + // response body should be provided by the standard error page + // process. But, if the output of the CGI process isn't read + // then that process can hang. + boolean skipBody = false; + + while (isRunning) { + try { + // set headers + String line = null; + while (((line = cgiHeaderReader.readLine()) != null) && !line.isEmpty()) { + if (log.isTraceEnabled()) { + log.trace("addHeader(\"" + line + "\")"); + } + if (line.startsWith("HTTP")) { + skipBody = setStatus(response, getSCFromHttpStatusLine(line)); + } else if (line.indexOf(':') >= 0) { + String header = line.substring(0, line.indexOf(':')).trim(); + String value = line.substring(line.indexOf(':') + 1).trim(); + if (header.equalsIgnoreCase("status")) { + skipBody = setStatus(response, getSCFromCGIStatusHeader(value)); + } else { + response.addHeader(header, value); + } + } else { + log.info(sm.getString("cgiServlet.runBadHeader", line)); + } + } + + // write output + byte[] bBuf = new byte[2048]; + + OutputStream out = response.getOutputStream(); + cgiOutput = proc.getInputStream(); + + try { + while (!skipBody && (bufRead = cgiOutput.read(bBuf)) != -1) { + if (log.isTraceEnabled()) { + log.trace("output " + bufRead + " bytes of data"); + } + out.write(bBuf, 0, bufRead); + } + } finally { + // Attempt to consume any leftover byte if something bad happens, + // such as a socket disconnect on the servlet side; otherwise, the + // external process could hang + if (bufRead != -1) { + while ((bufRead = cgiOutput.read(bBuf)) != -1) { + // NOOP - just read the data + } + } + } + + proc.exitValue(); // Throws exception if alive + + isRunning = false; + + } catch (IllegalThreadStateException e) { + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + // Ignore + } + } + } // replacement for Process.waitFor() + + } catch (IOException e) { + log.warn(sm.getString("cgiServlet.runFail"), e); + throw e; + } finally { + // Close the header reader + if (cgiHeaderReader != null) { + try { + cgiHeaderReader.close(); + } catch (IOException ioe) { + log.warn(sm.getString("cgiServlet.runHeaderReaderFail"), ioe); + } + } + // Close the output stream if used + if (cgiOutput != null) { + try { + cgiOutput.close(); + } catch (IOException ioe) { + log.warn(sm.getString("cgiServlet.runOutputStreamFail"), ioe); + } + } + // Make sure the error stream reader has finished + if (errReaderThread != null) { + try { + errReaderThread.join(stderrTimeout); + } catch (InterruptedException e) { + log.warn(sm.getString("cgiServlet.runReaderInterrupt")); + } + } + if (proc != null) { + proc.destroy(); + proc = null; + } + } + } + + /** + * Parses the Status-Line and extracts the status code. + * + * @param line The HTTP Status-Line (RFC2616, section 6.1) + * + * @return The extracted status code or the code representing an internal error if a valid status code cannot be + * extracted. + */ + private int getSCFromHttpStatusLine(String line) { + int statusStart = line.indexOf(' ') + 1; + + if (statusStart < 1 || line.length() < statusStart + 3) { + // Not a valid HTTP Status-Line + log.warn(sm.getString("cgiServlet.runInvalidStatus", line)); + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } + + String status = line.substring(statusStart, statusStart + 3); + + int statusCode; + try { + statusCode = Integer.parseInt(status); + } catch (NumberFormatException nfe) { + // Not a valid status code + log.warn(sm.getString("cgiServlet.runInvalidStatus", status)); + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } + + return statusCode; + } + + /** + * Parses the CGI Status Header value and extracts the status code. + * + * @param value The CGI Status value of the form + * digit digit digit SP reason-phrase + * + * @return The extracted status code or the code representing an internal error if a valid status code cannot be + * extracted. + */ + private int getSCFromCGIStatusHeader(String value) { + if (value.length() < 3) { + // Not a valid status value + log.warn(sm.getString("cgiServlet.runInvalidStatus", value)); + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } + + String status = value.substring(0, 3); + + int statusCode; + try { + statusCode = Integer.parseInt(status); + } catch (NumberFormatException nfe) { + // Not a valid status code + log.warn(sm.getString("cgiServlet.runInvalidStatus", status)); + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } + + return statusCode; + } + + private void sendToLog(BufferedReader rdr) { + String line = null; + int lineCount = 0; + try { + while ((line = rdr.readLine()) != null) { + log.warn(sm.getString("cgiServlet.runStdErr", line)); + lineCount++; + } + } catch (IOException e) { + log.warn(sm.getString("cgiServlet.runStdErrFail"), e); + } finally { + try { + rdr.close(); + } catch (IOException e) { + log.warn(sm.getString("cgiServlet.runStdErrFail"), e); + } + } + if (lineCount > 0) { + log.warn(sm.getString("cgiServlet.runStdErrCount", Integer.valueOf(lineCount))); + } + } + } // class CGIRunner + + /** + * This is an input stream specifically for reading HTTP headers. It reads up to and including the two blank lines + * terminating the headers. It allows the content to be read using bytes or characters as appropriate. + */ + protected static class HTTPHeaderInputStream extends InputStream { + private static final int STATE_CHARACTER = 0; + private static final int STATE_FIRST_CR = 1; + private static final int STATE_FIRST_LF = 2; + private static final int STATE_SECOND_CR = 3; + private static final int STATE_HEADER_END = 4; + + private final InputStream input; + private int state; + + HTTPHeaderInputStream(InputStream theInput) { + input = theInput; + state = STATE_CHARACTER; + } + + /** + * @see java.io.InputStream#read() + */ + @Override + public int read() throws IOException { + if (state == STATE_HEADER_END) { + return -1; + } + + int i = input.read(); + + /* + * Update the state + * State machine looks like this + * @formatter:off + * + * -------->-------- + * | (CR) | + * | | + * CR1--->--- | + * | | | + * ^(CR) |(LF) | + * | | | + * CHAR--->--LF1--->--EOH + * (LF) | (LF) | + * |(CR) ^(LF) + * | | + * (CR2)-->--- + * + * @formatter:on + */ + if (i == 10) { + // LF + switch (state) { + case STATE_CHARACTER: + state = STATE_FIRST_LF; + break; + case STATE_FIRST_CR: + state = STATE_FIRST_LF; + break; + case STATE_FIRST_LF: + case STATE_SECOND_CR: + state = STATE_HEADER_END; + break; + } + + } else if (i == 13) { + // CR + switch (state) { + case STATE_CHARACTER: + state = STATE_FIRST_CR; + break; + case STATE_FIRST_CR: + state = STATE_HEADER_END; + break; + case STATE_FIRST_LF: + state = STATE_SECOND_CR; + break; + } + + } else { + state = STATE_CHARACTER; + } + + return i; + } + } // class HTTPHeaderInputStream + +} // class CGIServlet diff --git a/java/org/apache/catalina/servlets/DefaultServlet.java b/java/org/apache/catalina/servlets/DefaultServlet.java new file mode 100644 index 0000000..67cd9e0 --- /dev/null +++ b/java/org/apache/catalina/servlets/DefaultServlet.java @@ -0,0 +1,2892 @@ +/* + * 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.catalina.servlets; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.Reader; +import java.io.Serializable; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.ServletResponseWrapper; +import jakarta.servlet.UnavailableException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.connector.RequestFacade; +import org.apache.catalina.connector.ResponseFacade; +import org.apache.catalina.util.IOTools; +import org.apache.catalina.util.ServerInfo; +import org.apache.catalina.util.URLEncoder; +import org.apache.catalina.webresources.CachedResource; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.http.ResponseUtil; +import org.apache.tomcat.util.http.parser.ContentRange; +import org.apache.tomcat.util.http.parser.EntityTag; +import org.apache.tomcat.util.http.parser.Ranges; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; +import org.apache.tomcat.util.security.PrivilegedGetTccl; +import org.apache.tomcat.util.security.PrivilegedSetTccl; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.ext.EntityResolver2; + + +/** + *

    + * The default resource-serving servlet for most web applications, used to serve static resources such as HTML pages and + * images. + *

    + *

    + * This servlet is intended to be mapped to / e.g.: + *

    + * + *
    + *   <servlet-mapping>
    + *       <servlet-name>default</servlet-name>
    + *       <url-pattern>/</url-pattern>
    + *   </servlet-mapping>
    + * 
    + *

    + * It can be mapped to sub-paths, however in all cases resources are served from the web application resource root using + * the full path from the root of the web application context.
    + * e.g. given a web application structure: + *

    + * + *
    + * /context
    + *   /images
    + *     tomcat2.jpg
    + *   /static
    + *     /images
    + *       tomcat.jpg
    + * 
    + *

    + * ... and a servlet mapping that maps only /static/* to the default servlet: + *

    + * + *
    + *   <servlet-mapping>
    + *       <servlet-name>default</servlet-name>
    + *       <url-pattern>/static/*</url-pattern>
    + *   </servlet-mapping>
    + * 
    + *

    + * Then a request to /context/static/images/tomcat.jpg will succeed while a request to + * /context/images/tomcat2.jpg will fail. + *

    + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class DefaultServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(DefaultServlet.class); + + private static final DocumentBuilderFactory factory; + + private static final SecureEntityResolver secureEntityResolver; + + /** + * Full range marker. + */ + protected static final Ranges FULL = new Ranges(null, new ArrayList<>()); + + private static final ContentRange IGNORE = new ContentRange(null, 0, 0, 0); + + /** + * MIME multipart separation string + */ + protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY"; + + /** + * Size of file transfer buffer in bytes. + */ + protected static final int BUFFER_SIZE = 4096; + + + // ----------------------------------------------------- Static Initializer + + static { + if (Globals.IS_SECURITY_ENABLED) { + factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + secureEntityResolver = new SecureEntityResolver(); + } else { + factory = null; + secureEntityResolver = null; + } + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The debugging detail level for this servlet. + */ + protected int debug = 0; + + /** + * The input buffer size to use when serving resources. + */ + protected int input = 2048; + + /** + * Should we generate directory listings? + */ + protected boolean listings = false; + + /** + * Read only flag. By default, it's set to true. + */ + protected boolean readOnly = true; + + /** + * List of compression formats to serve and their preference order. + */ + protected CompressionFormat[] compressionFormats; + + /** + * The output buffer size to use when serving resources. + */ + protected int output = 2048; + + /** + * Allow customized directory listing per directory. + */ + protected String localXsltFile = null; + + /** + * Allow customized directory listing per context. + */ + protected String contextXsltFile = null; + + /** + * Allow customized directory listing per instance. + */ + protected String globalXsltFile = null; + + /** + * Allow a readme file to be included. + */ + protected String readmeFile = null; + + /** + * The complete set of web application resources + */ + protected transient WebResourceRoot resources = null; + + /** + * File encoding to be used when reading static files. If none is specified the platform default is used. + */ + protected String fileEncoding = null; + private transient Charset fileEncodingCharset = null; + + /** + * If a file has a BOM, should that be used in preference to fileEncoding? Will default to {@link BomConfig#TRUE} in + * {@link #init()}. + */ + private BomConfig useBomIfPresent = null; + + /** + * Minimum size for sendfile usage in bytes. + */ + protected int sendfileSize = 48 * 1024; + + /** + * Should the Accept-Ranges: bytes header be send with static resources? + */ + protected boolean useAcceptRanges = true; + + /** + * Flag to determine if server information is presented. + */ + protected boolean showServerInfo = true; + + /** + * Flag to determine if resources should be sorted. + */ + protected boolean sortListings = false; + + /** + * The sorting manager for sorting files and directories. + */ + protected transient SortManager sortManager; + + /** + * Flag that indicates whether partial PUTs are permitted. + */ + private boolean allowPartialPut = true; + + + // --------------------------------------------------------- Public Methods + + /** + * Finalize this servlet. + */ + @Override + public void destroy() { + // NOOP + } + + + /** + * Initialize this servlet. + */ + @Override + public void init() throws ServletException { + + if (getServletConfig().getInitParameter("debug") != null) { + debug = Integer.parseInt(getServletConfig().getInitParameter("debug")); + } + + if (getServletConfig().getInitParameter("input") != null) { + input = Integer.parseInt(getServletConfig().getInitParameter("input")); + } + + if (getServletConfig().getInitParameter("output") != null) { + output = Integer.parseInt(getServletConfig().getInitParameter("output")); + } + + listings = Boolean.parseBoolean(getServletConfig().getInitParameter("listings")); + + if (getServletConfig().getInitParameter("readonly") != null) { + readOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("readonly")); + } + + compressionFormats = parseCompressionFormats(getServletConfig().getInitParameter("precompressed"), + getServletConfig().getInitParameter("gzip")); + + if (getServletConfig().getInitParameter("sendfileSize") != null) { + sendfileSize = Integer.parseInt(getServletConfig().getInitParameter("sendfileSize")) * 1024; + } + + fileEncoding = getServletConfig().getInitParameter("fileEncoding"); + if (fileEncoding == null) { + fileEncodingCharset = Charset.defaultCharset(); + fileEncoding = fileEncodingCharset.name(); + } else { + try { + fileEncodingCharset = B2CConverter.getCharset(fileEncoding); + } catch (UnsupportedEncodingException e) { + throw new ServletException(e); + } + } + + String useBomIfPresent = getServletConfig().getInitParameter("useBomIfPresent"); + if (useBomIfPresent == null) { + // Use default + this.useBomIfPresent = BomConfig.TRUE; + } else { + for (BomConfig bomConfig : BomConfig.values()) { + if (bomConfig.configurationValue.equalsIgnoreCase(useBomIfPresent)) { + this.useBomIfPresent = bomConfig; + break; + } + } + if (this.useBomIfPresent == null) { + // Unrecognised configuration value + IllegalArgumentException iae = + new IllegalArgumentException(sm.getString("defaultServlet.unknownBomConfig", useBomIfPresent)); + throw new ServletException(iae); + } + } + + globalXsltFile = getServletConfig().getInitParameter("globalXsltFile"); + contextXsltFile = getServletConfig().getInitParameter("contextXsltFile"); + localXsltFile = getServletConfig().getInitParameter("localXsltFile"); + readmeFile = getServletConfig().getInitParameter("readmeFile"); + + if (getServletConfig().getInitParameter("useAcceptRanges") != null) { + useAcceptRanges = Boolean.parseBoolean(getServletConfig().getInitParameter("useAcceptRanges")); + } + + // Prevent the use of buffer sizes that are too small + if (input < 256) { + input = 256; + } + if (output < 256) { + output = 256; + } + + if (debug > 0) { + log("DefaultServlet.init: input buffer size=" + input + ", output buffer size=" + output); + } + + // Load the web resources + resources = (WebResourceRoot) getServletContext().getAttribute(Globals.RESOURCES_ATTR); + + if (resources == null) { + throw new UnavailableException(sm.getString("defaultServlet.noResources")); + } + + if (getServletConfig().getInitParameter("showServerInfo") != null) { + showServerInfo = Boolean.parseBoolean(getServletConfig().getInitParameter("showServerInfo")); + } + + if (getServletConfig().getInitParameter("sortListings") != null) { + sortListings = Boolean.parseBoolean(getServletConfig().getInitParameter("sortListings")); + + if (sortListings) { + boolean sortDirectoriesFirst; + if (getServletConfig().getInitParameter("sortDirectoriesFirst") != null) { + sortDirectoriesFirst = + Boolean.parseBoolean(getServletConfig().getInitParameter("sortDirectoriesFirst")); + } else { + sortDirectoriesFirst = false; + } + + sortManager = new SortManager(sortDirectoriesFirst); + } + } + + if (getServletConfig().getInitParameter("allowPartialPut") != null) { + allowPartialPut = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPartialPut")); + } + } + + private CompressionFormat[] parseCompressionFormats(String precompressed, String gzip) { + List ret = new ArrayList<>(); + if (precompressed != null && precompressed.indexOf('=') > 0) { + for (String pair : precompressed.split(",")) { + String[] setting = pair.split("="); + String encoding = setting[0]; + String extension = setting[1]; + ret.add(new CompressionFormat(extension, encoding)); + } + } else if (precompressed != null) { + if (Boolean.parseBoolean(precompressed)) { + ret.add(new CompressionFormat(".br", "br")); + ret.add(new CompressionFormat(".gz", "gzip")); + } + } else if (Boolean.parseBoolean(gzip)) { + // gzip handling is for backwards compatibility with Tomcat 8.x + ret.add(new CompressionFormat(".gz", "gzip")); + } + return ret.toArray(new CompressionFormat[0]); + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Return the relative path associated with this servlet. + * + * @param request The servlet request we are processing + * + * @return the relative path + */ + protected String getRelativePath(HttpServletRequest request) { + return getRelativePath(request, false); + } + + protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) { + // IMPORTANT: DefaultServlet can be mapped to '/' or '/path/*' but always + // serves resources from the web app root with context rooted paths. + // i.e. it cannot be used to mount the web app root under a sub-path + // This method must construct a complete context rooted path, although + // subclasses can change this behaviour. + + String servletPath; + String pathInfo; + + if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) { + // For includes, get the info from the attributes + pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); + servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + } else { + pathInfo = request.getPathInfo(); + servletPath = request.getServletPath(); + } + + StringBuilder result = new StringBuilder(); + if (servletPath.length() > 0) { + result.append(servletPath); + } + if (pathInfo != null) { + result.append(pathInfo); + } + if (result.length() == 0 && !allowEmptyPath) { + result.append('/'); + } + + return result.toString(); + } + + + /** + * Determines the appropriate path to prepend resources with when generating directory listings. Depending on the + * behaviour of {@link #getRelativePath(HttpServletRequest)} this will change. + * + * @param request the request to determine the path for + * + * @return the prefix to apply to all resources in the listing. + */ + protected String getPathPrefix(final HttpServletRequest request) { + return request.getContextPath(); + } + + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (req.getDispatcherType() == DispatcherType.ERROR) { + doGet(req, resp); + } else { + super.service(req, resp); + } + } + + + /** + * Process a GET request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + // Serve the requested resource, including the data content + serveResource(request, response, true, fileEncoding); + + } + + + /** + * Process a HEAD request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + protected void doHead(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + // Serve the requested resource, without the data content unless we are + // being included since in that case the content needs to be provided so + // the correct content length is reported for the including resource + boolean serveContent = DispatcherType.INCLUDE.equals(request.getDispatcherType()); + serveResource(request, response, serveContent, fileEncoding); + } + + + /** + * Override default implementation to ensure that TRACE is correctly handled. + * + * @param req the {@link HttpServletRequest} object that contains the request the client made of the servlet + * @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client + * + * @exception IOException if an input or output error occurs while the servlet is handling the OPTIONS request + * @exception ServletException if the request for the OPTIONS cannot be handled + */ + @Override + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + resp.setHeader("Allow", determineMethodsAllowed(req)); + } + + + protected String determineMethodsAllowed(HttpServletRequest req) { + StringBuilder allow = new StringBuilder(); + + // Start with methods that are always allowed + allow.append("OPTIONS, GET, HEAD, POST"); + + // PUT and DELETE depend on readonly + if (!readOnly) { + allow.append(", PUT, DELETE"); + } + + // Trace - assume disabled unless we can prove otherwise + if (req instanceof RequestFacade && ((RequestFacade) req).getAllowTrace()) { + allow.append(", TRACE"); + } + + return allow.toString(); + } + + + protected void sendNotAllowed(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.addHeader("Allow", determineMethodsAllowed(req)); + resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + + /** + * Process a POST request for the specified resource. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + doGet(request, response); + } + + + /** + * Process a PUT request for the specified resource. + * + * @param req The servlet request we are processing + * @param resp The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (readOnly) { + sendNotAllowed(req, resp); + return; + } + + String path = getRelativePath(req); + + WebResource resource = resources.getResource(path); + + ContentRange range = parseContentRange(req, resp); + + if (range == null) { + // Processing error. parseContentRange() set the error code + return; + } + + InputStream resourceInputStream = null; + + try { + // Append data specified in ranges to existing content for this + // resource - create a temp. file on the local filesystem to + // perform this operation + // Assume just one range is specified for now + if (range == IGNORE) { + resourceInputStream = req.getInputStream(); + } else { + File contentFile = executePartialPut(req, range, path); + resourceInputStream = new FileInputStream(contentFile); + } + + if (resources.write(path, resourceInputStream, true)) { + if (resource.exists()) { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + } else { + resp.setStatus(HttpServletResponse.SC_CREATED); + } + } else { + resp.sendError(HttpServletResponse.SC_CONFLICT); + } + } finally { + if (resourceInputStream != null) { + try { + resourceInputStream.close(); + } catch (IOException ioe) { + // Ignore + } + } + } + } + + + /** + * Handle a partial PUT. New content specified in request is appended to existing content in oldRevisionContent (if + * present). This code does not support simultaneous partial updates to the same resource. + * + * @param req The Servlet request + * @param range The range that will be written + * @param path The path + * + * @return the associated file object + * + * @throws IOException an IO error occurred + */ + protected File executePartialPut(HttpServletRequest req, ContentRange range, String path) throws IOException { + + // Append data specified in ranges to existing content for this + // resource - create a temp. file on the local filesystem to + // perform this operation + File tempDir = (File) getServletContext().getAttribute(ServletContext.TEMPDIR); + // Convert all '/' characters to '.' in resourcePath + String convertedResourcePath = path.replace('/', '.'); + File contentFile = new File(tempDir, convertedResourcePath); + if (contentFile.createNewFile()) { + // Clean up contentFile when Tomcat is terminated + contentFile.deleteOnExit(); + } + + try (RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw")) { + + WebResource oldResource = resources.getResource(path); + + // Copy data in oldRevisionContent to contentFile + if (oldResource.isFile()) { + try (BufferedInputStream bufOldRevStream = + new BufferedInputStream(oldResource.getInputStream(), BUFFER_SIZE)) { + + int numBytesRead; + byte[] copyBuffer = new byte[BUFFER_SIZE]; + while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) { + randAccessContentFile.write(copyBuffer, 0, numBytesRead); + } + + } + } + + randAccessContentFile.setLength(range.getLength()); + + // Append data in request input stream to contentFile + randAccessContentFile.seek(range.getStart()); + int numBytesRead; + byte[] transferBuffer = new byte[BUFFER_SIZE]; + try (BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE)) { + while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) { + randAccessContentFile.write(transferBuffer, 0, numBytesRead); + } + } + } + + return contentFile; + } + + + /** + * Process a DELETE request for the specified resource. + * + * @param req The servlet request we are processing + * @param resp The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (readOnly) { + sendNotAllowed(req, resp); + return; + } + + String path = getRelativePath(req); + + WebResource resource = resources.getResource(path); + + if (resource.exists()) { + if (resource.delete()) { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + } else { + resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + } else { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + } + + } + + + /** + * Check if the conditions specified in the optional If headers are satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resource The resource + * + * @return true if the resource meets all the specified conditions, and false if any of + * the conditions is not satisfied, in which case request processing is stopped + * + * @throws IOException an IO error occurred + */ + protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response, WebResource resource) + throws IOException { + + return checkIfMatch(request, response, resource) && checkIfModifiedSince(request, response, resource) && + checkIfNoneMatch(request, response, resource) && checkIfUnmodifiedSince(request, response, resource); + + } + + + /** + * URL rewriter. + * + * @param path Path which has to be rewritten + * + * @return the rewritten path + */ + protected String rewriteUrl(String path) { + return URLEncoder.DEFAULT.encode(path, StandardCharsets.UTF_8); + } + + + /** + * Serve the specified resource, optionally including the data content. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param content Should the content be included? + * @param inputEncoding The encoding to use if it is necessary to access the source as characters rather than as + * bytes + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content, + String inputEncoding) throws IOException, ServletException { + + boolean serveContent = content; + + // Identify the requested resource path + String path = getRelativePath(request, true); + + if (debug > 0) { + if (serveContent) { + log("DefaultServlet.serveResource: Serving resource '" + path + "' headers and data"); + } else { + log("DefaultServlet.serveResource: Serving resource '" + path + "' headers only"); + } + } + + if (path.length() == 0) { + // Context root redirect + doDirectoryRedirect(request, response); + return; + } + + WebResource resource = resources.getResource(path); + boolean isError = DispatcherType.ERROR == request.getDispatcherType(); + + if (!resource.exists()) { + // Check if we're included so we can return the appropriate + // missing resource name in the error + String requestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); + if (requestUri == null) { + requestUri = request.getRequestURI(); + } else { + // We're included + // SRV.9.3 says we must throw a FNFE + throw new FileNotFoundException(sm.getString("defaultServlet.missingResource", requestUri)); + } + + if (isError) { + response.sendError(((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).intValue()); + } else { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + sm.getString("defaultServlet.missingResource", requestUri)); + } + return; + } + + if (!resource.canRead()) { + // Check if we're included so we can return the appropriate + // missing resource name in the error + String requestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); + if (requestUri == null) { + requestUri = request.getRequestURI(); + } else { + // We're included + // Spec doesn't say what to do in this case but a FNFE seems + // reasonable + throw new FileNotFoundException(sm.getString("defaultServlet.missingResource", requestUri)); + } + + if (isError) { + response.sendError(((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).intValue()); + } else { + response.sendError(HttpServletResponse.SC_FORBIDDEN, requestUri); + } + return; + } + + boolean included = false; + // Check if the conditions specified in the optional If headers are + // satisfied. + if (resource.isFile()) { + // Checking If headers + included = (request.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH) != null); + if (!included && !isError && !checkIfHeaders(request, response, resource)) { + return; + } + } + + // Find content type. + String contentType = resource.getMimeType(); + if (contentType == null) { + contentType = getServletContext().getMimeType(resource.getName()); + resource.setMimeType(contentType); + } + + // These need to reflect the original resource, not the potentially + // precompressed version of the resource so get them now if they are going to + // be needed later + String eTag = null; + String lastModifiedHttp = null; + if (resource.isFile() && !isError) { + eTag = generateETag(resource); + lastModifiedHttp = resource.getLastModifiedHttp(); + } + + + // Serve a precompressed version of the file if present + boolean usingPrecompressedVersion = false; + if (compressionFormats.length > 0 && !included && resource.isFile() && !pathEndsWithCompressedExtension(path)) { + List precompressedResources = getAvailablePrecompressedResources(path); + if (!precompressedResources.isEmpty()) { + ResponseUtil.addVaryFieldName(response, "accept-encoding"); + PrecompressedResource bestResource = getBestPrecompressedResource(request, precompressedResources); + if (bestResource != null) { + response.addHeader("Content-Encoding", bestResource.format.encoding); + resource = bestResource.resource; + usingPrecompressedVersion = true; + } + } + } + + Ranges ranges = FULL; + long contentLength = -1L; + + if (resource.isDirectory()) { + if (!path.endsWith("/")) { + doDirectoryRedirect(request, response); + return; + } + + // Skip directory listings if we have been configured to + // suppress them + if (!listings) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, + sm.getString("defaultServlet.missingResource", request.getRequestURI())); + return; + } + contentType = "text/html;charset=UTF-8"; + } else { + if (!isError) { + if (useAcceptRanges) { + // Accept ranges header + response.setHeader("Accept-Ranges", "bytes"); + } + + // Parse range specifier + ranges = parseRange(request, response, resource); + if (ranges == null) { + return; + } + + // ETag header + response.setHeader("ETag", eTag); + + // Last-Modified header + response.setHeader("Last-Modified", lastModifiedHttp); + } + + // Get content length + contentLength = resource.getContentLength(); + // Special case for zero length files, which would cause a + // (silent) ISE when setting the output buffer size + if (contentLength == 0L) { + serveContent = false; + } + } + + ServletOutputStream ostream = null; + PrintWriter writer = null; + + if (serveContent) { + // Trying to retrieve the servlet output stream + try { + ostream = response.getOutputStream(); + } catch (IllegalStateException e) { + // If it fails, we try to get a Writer instead if we're + // trying to serve a text file + if (!usingPrecompressedVersion && isText(contentType)) { + writer = response.getWriter(); + // Cannot reliably serve partial content with a Writer + ranges = FULL; + } else { + throw e; + } + } + } + + // Check to see if a Filter, Valve or wrapper has written some content. + // If it has, disable range requests and setting of a content length + // since neither can be done reliably. + ServletResponse r = response; + long contentWritten = 0; + while (r instanceof ServletResponseWrapper) { + r = ((ServletResponseWrapper) r).getResponse(); + } + if (r instanceof ResponseFacade) { + contentWritten = ((ResponseFacade) r).getContentWritten(); + } + if (contentWritten > 0) { + ranges = FULL; + } + + String outputEncoding = response.getCharacterEncoding(); + Charset charset = B2CConverter.getCharset(outputEncoding); + boolean conversionRequired; + /* + * The test below deliberately uses != to compare two Strings. This is because the code is looking to see if the + * default character encoding has been returned because no explicit character encoding has been defined. There + * is no clean way of doing this via the Servlet API. It would be possible to add a Tomcat specific API but that + * would require quite a bit of code to get to the Tomcat specific request object that may have been wrapped. + * The != test is a (slightly hacky) quick way of doing this. + */ + boolean outputEncodingSpecified = outputEncoding != org.apache.coyote.Constants.DEFAULT_BODY_CHARSET.name() && + outputEncoding != resources.getContext().getResponseCharacterEncoding(); + if (!usingPrecompressedVersion && isText(contentType) && outputEncodingSpecified && + !charset.equals(fileEncodingCharset)) { + conversionRequired = true; + // Conversion often results fewer/more/different bytes. + // That does not play nicely with range requests. + ranges = FULL; + } else { + conversionRequired = false; + } + + if (resource.isDirectory() || isError || ranges == FULL) { + // Set the appropriate output headers + if (contentType != null) { + if (debug > 0) { + log("DefaultServlet.serveFile: contentType='" + contentType + "'"); + } + // Don't override a previously set content type + if (response.getContentType() == null) { + response.setContentType(contentType); + } + } + if (resource.isFile() && contentLength >= 0 && (!serveContent || ostream != null)) { + if (debug > 0) { + log("DefaultServlet.serveFile: contentLength=" + contentLength); + } + // Don't set a content length if something else has already + // written to the response or if conversion will be taking place + if (contentWritten == 0 && !conversionRequired) { + response.setContentLengthLong(contentLength); + } + } + + if (serveContent) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + InputStream renderResult = null; + if (ostream == null) { + // Output via a writer so can't use sendfile or write + // content directly. + if (resource.isDirectory()) { + renderResult = render(request, getPathPrefix(request), resource, inputEncoding); + } else { + renderResult = resource.getInputStream(); + if (included) { + // Need to make sure any BOM is removed + if (!renderResult.markSupported()) { + renderResult = new BufferedInputStream(renderResult); + } + Charset bomCharset = processBom(renderResult, useBomIfPresent.stripBom); + if (bomCharset != null && useBomIfPresent.useBomEncoding) { + inputEncoding = bomCharset.name(); + } + } + } + copy(renderResult, writer, inputEncoding); + } else { + // Output is via an OutputStream + if (resource.isDirectory()) { + renderResult = render(request, getPathPrefix(request), resource, inputEncoding); + } else { + // Output is content of resource + // Check to see if conversion is required + if (conversionRequired || included) { + // When including a file, we need to check for a BOM + // to determine if a conversion is required, so we + // might as well always convert + InputStream source = resource.getInputStream(); + if (!source.markSupported()) { + source = new BufferedInputStream(source); + } + Charset bomCharset = processBom(source, useBomIfPresent.stripBom); + if (bomCharset != null && useBomIfPresent.useBomEncoding) { + inputEncoding = bomCharset.name(); + } + // Following test also ensures included resources + // are converted if an explicit output encoding was + // specified + if (outputEncodingSpecified) { + OutputStreamWriter osw = new OutputStreamWriter(ostream, charset); + PrintWriter pw = new PrintWriter(osw); + copy(source, pw, inputEncoding); + pw.flush(); + } else { + // Just included but no conversion + renderResult = source; + } + } else { + if (!checkSendfile(request, response, resource, contentLength, null)) { + // sendfile not possible so check if resource + // content is available directly via + // CachedResource. Do not want to call + // getContent() on other resource + // implementations as that could trigger loading + // the contents of a very large file into memory + byte[] resourceBody = null; + if (resource instanceof CachedResource) { + resourceBody = resource.getContent(); + } + if (resourceBody == null) { + // Resource content not directly available, + // use InputStream + renderResult = resource.getInputStream(); + } else { + // Use the resource content directly + ostream.write(resourceBody); + } + } + } + } + // If a stream was configured, it needs to be copied to + // the output (this method closes the stream) + if (renderResult != null) { + copy(renderResult, ostream); + } + } + } + + } else { + + if ((ranges == null) || (ranges.getEntries().isEmpty())) { + return; + } + + // Partial content response. + + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + + if (ranges.getEntries().size() == 1) { + + Ranges.Entry range = ranges.getEntries().get(0); + long start = getStart(range, contentLength); + long end = getEnd(range, contentLength); + response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + contentLength); + long length = end - start + 1; + response.setContentLengthLong(length); + + if (contentType != null) { + if (debug > 0) { + log("DefaultServlet.serveFile: contentType='" + contentType + "'"); + } + response.setContentType(contentType); + } + + if (serveContent) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + if (ostream != null) { + if (!checkSendfile(request, response, resource, contentLength, range)) { + copy(resource, contentLength, ostream, range); + } + } else { + // we should not get here + throw new IllegalStateException(); + } + } + } else { + response.setContentType("multipart/byteranges; boundary=" + mimeSeparation); + if (serveContent) { + try { + response.setBufferSize(output); + } catch (IllegalStateException e) { + // Silent catch + } + if (ostream != null) { + copy(resource, contentLength, ostream, ranges, contentType); + } else { + // we should not get here + throw new IllegalStateException(); + } + } + } + } + } + + + /* + * Code borrowed heavily from Jasper's EncodingDetector + */ + private static Charset processBom(InputStream is, boolean stripBom) throws IOException { + // Java supported character sets do not use BOMs longer than 4 bytes + byte[] bom = new byte[4]; + is.mark(bom.length); + + int count = is.read(bom); + + // BOMs are at least 2 bytes + if (count < 2) { + skip(is, 0, stripBom); + return null; + } + + // Look for two byte BOMs + int b0 = bom[0] & 0xFF; + int b1 = bom[1] & 0xFF; + if (b0 == 0xFE && b1 == 0xFF) { + skip(is, 2, stripBom); + return StandardCharsets.UTF_16BE; + } + // Delay the UTF_16LE check if there are more that 2 bytes since it + // overlaps with UTF-32LE. + if (count == 2 && b0 == 0xFF && b1 == 0xFE) { + skip(is, 2, stripBom); + return StandardCharsets.UTF_16LE; + } + + // Remaining BOMs are at least 3 bytes + if (count < 3) { + skip(is, 0, stripBom); + return null; + } + + // UTF-8 is only 3-byte BOM + int b2 = bom[2] & 0xFF; + if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) { + skip(is, 3, stripBom); + return StandardCharsets.UTF_8; + } + + if (count < 4) { + skip(is, 0, stripBom); + return null; + } + + // Look for 4-byte BOMs + int b3 = bom[3] & 0xFF; + if (b0 == 0x00 && b1 == 0x00 && b2 == 0xFE && b3 == 0xFF) { + return Charset.forName("UTF-32BE"); + } + if (b0 == 0xFF && b1 == 0xFE && b2 == 0x00 && b3 == 0x00) { + return Charset.forName("UTF-32LE"); + } + + // Now we can check for UTF16-LE. There is an assumption here that we + // won't see a UTF16-LE file with a BOM where the first real data is + // 0x00 0x00 + if (b0 == 0xFF && b1 == 0xFE) { + skip(is, 2, stripBom); + return StandardCharsets.UTF_16LE; + } + + skip(is, 0, stripBom); + return null; + } + + + private static void skip(InputStream is, int skip, boolean stripBom) throws IOException { + is.reset(); + if (stripBom) { + while (skip-- > 0) { + if (is.read() < 0) { + // Ignore since included + break; + } + } + } + } + + + private static boolean isText(String contentType) { + return contentType == null || contentType.startsWith("text") || contentType.endsWith("xml") || + contentType.contains("/javascript"); + } + + private static boolean validate(ContentRange range) { + // bytes is the only range unit supported + return (range != null) && ("bytes".equals(range.getUnits())) && (range.getStart() >= 0) && + (range.getEnd() >= 0) && (range.getStart() <= range.getEnd()) && (range.getLength() > 0); + } + + private static boolean validate(Ranges.Entry range, long length) { + long start = getStart(range, length); + long end = getEnd(range, length); + return (start >= 0) && (end >= 0) && (start <= end); + } + + private static long getStart(Ranges.Entry range, long length) { + long start = range.getStart(); + if (start == -1) { + long end = range.getEnd(); + // If there is no start, then the start is based on the end + if (end >= length) { + return 0; + } else { + return length - end; + } + } else { + return start; + } + } + + private static long getEnd(Ranges.Entry range, long length) { + long end = range.getEnd(); + if (range.getStart() == -1 || end == -1 || end >= length) { + return length - 1; + } else { + return end; + } + } + + private boolean pathEndsWithCompressedExtension(String path) { + for (CompressionFormat format : compressionFormats) { + if (path.endsWith(format.extension)) { + return true; + } + } + return false; + } + + private List getAvailablePrecompressedResources(String path) { + List ret = new ArrayList<>(compressionFormats.length); + for (CompressionFormat format : compressionFormats) { + WebResource precompressedResource = resources.getResource(path + format.extension); + if (precompressedResource.exists() && precompressedResource.isFile()) { + ret.add(new PrecompressedResource(precompressedResource, format)); + } + } + return ret; + } + + /** + * Match the client preferred encoding formats to the available precompressed resources. + * + * @param request The servlet request we are processing + * @param precompressedResources List of available precompressed resources. + * + * @return The best matching precompressed resource or null if no match was found. + */ + private PrecompressedResource getBestPrecompressedResource(HttpServletRequest request, + List precompressedResources) { + Enumeration headers = request.getHeaders("Accept-Encoding"); + PrecompressedResource bestResource = null; + double bestResourceQuality = 0; + int bestResourcePreference = Integer.MAX_VALUE; + while (headers.hasMoreElements()) { + String header = headers.nextElement(); + for (String preference : header.split(",")) { + double quality = 1; + int qualityIdx = preference.indexOf(';'); + if (qualityIdx > 0) { + int equalsIdx = preference.indexOf('=', qualityIdx + 1); + if (equalsIdx == -1) { + continue; + } + quality = Double.parseDouble(preference.substring(equalsIdx + 1).trim()); + } + if (quality >= bestResourceQuality) { + String encoding = preference; + if (qualityIdx > 0) { + encoding = encoding.substring(0, qualityIdx); + } + encoding = encoding.trim(); + if ("identity".equals(encoding)) { + bestResource = null; + bestResourceQuality = quality; + bestResourcePreference = Integer.MAX_VALUE; + continue; + } + if ("*".equals(encoding)) { + bestResource = precompressedResources.get(0); + bestResourceQuality = quality; + bestResourcePreference = 0; + continue; + } + for (int i = 0; i < precompressedResources.size(); ++i) { + PrecompressedResource resource = precompressedResources.get(i); + if (encoding.equals(resource.format.encoding)) { + if (quality > bestResourceQuality || i < bestResourcePreference) { + bestResource = resource; + bestResourceQuality = quality; + bestResourcePreference = i; + } + break; + } + } + } + } + } + return bestResource; + } + + private void doDirectoryRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { + StringBuilder location = new StringBuilder(request.getRequestURI()); + location.append('/'); + if (request.getQueryString() != null) { + location.append('?'); + location.append(request.getQueryString()); + } + // Avoid protocol relative redirects + while (location.length() > 1 && location.charAt(1) == '/') { + location.deleteCharAt(0); + } + response.sendRedirect(response.encodeRedirectURL(location.toString())); + } + + /** + * Parse the content-range header. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * + * @return the partial content-range, {@code null} if the content-range header was invalid or {@code #IGNORE} if + * there is no header to process + * + * @throws IOException an IO error occurred + */ + protected ContentRange parseContentRange(HttpServletRequest request, HttpServletResponse response) + throws IOException { + + // Retrieving the content-range header (if any is specified + String contentRangeHeader = request.getHeader("Content-Range"); + + if (contentRangeHeader == null) { + return IGNORE; + } + + if (!allowPartialPut) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + + ContentRange contentRange = ContentRange.parse(new StringReader(contentRangeHeader)); + + if (contentRange == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + + if (!validate(contentRange)) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + + return contentRange; + } + + + /** + * Parse the range header. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resource The resource + * + * @return a list of ranges, {@code null} if the range header was invalid or {@code #FULL} if the Range header + * should be ignored. + * + * @throws IOException an IO error occurred + */ + protected Ranges parseRange(HttpServletRequest request, HttpServletResponse response, WebResource resource) + throws IOException { + + // Range headers are only valid on GET requests. That implies they are + // also valid on HEAD requests. This method is only called by doGet() + // and doHead() so no need to check the request method. + + // Checking If-Range + String headerValue = request.getHeader("If-Range"); + + if (headerValue != null) { + + long headerValueTime = (-1L); + try { + headerValueTime = request.getDateHeader("If-Range"); + } catch (IllegalArgumentException e) { + // Ignore + } + + String eTag = generateETag(resource); + long lastModified = resource.getLastModified(); + + if (headerValueTime == (-1L)) { + // If the ETag the client gave does not match the entity + // etag, then the entire entity is returned. + if (!eTag.equals(headerValue.trim())) { + return FULL; + } + } else { + // If the timestamp of the entity the client got differs from + // the last modification date of the entity, the entire entity + // is returned. + if (Math.abs(lastModified - headerValueTime) > 1000) { + return FULL; + } + } + } + + long fileLength = resource.getContentLength(); + + if (fileLength == 0) { + // Range header makes no sense for a zero length resource. Tomcat + // therefore opts to ignore it. + return FULL; + } + + // Retrieving the range header (if any is specified + String rangeHeader = request.getHeader("Range"); + + if (rangeHeader == null) { + // No Range header is the same as ignoring any Range header + return FULL; + } + + Ranges ranges = Ranges.parse(new StringReader(rangeHeader)); + + if (ranges == null) { + // The Range header is present but not formatted correctly. + // Could argue for a 400 response but 416 is more specific. + // There is also the option to ignore the (invalid) Range header. + // RFC7233#4.4 notes that many servers do ignore the Range header in + // these circumstances but Tomcat has always returned a 416. + response.addHeader("Content-Range", "bytes */" + fileLength); + response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return null; + } + + // bytes is the only range unit supported (and I don't see the point + // of adding new ones). + if (!ranges.getUnits().equals("bytes")) { + // RFC7233#3.1 Servers must ignore range units they don't understand + return FULL; + } + + for (Ranges.Entry range : ranges.getEntries()) { + if (!validate(range, fileLength)) { + response.addHeader("Content-Range", "bytes */" + fileLength); + response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return null; + } + } + + return ranges; + } + + + /** + * Decide which way to render. HTML or XML. + * + * @param request The HttpServletRequest being served + * @param contextPath The path + * @param resource The resource + * @param encoding The encoding to use to process the readme (if any) + * + * @return the input stream with the rendered output + * + * @throws IOException an IO error occurred + * @throws ServletException rendering error + */ + protected InputStream render(HttpServletRequest request, String contextPath, WebResource resource, String encoding) + throws IOException, ServletException { + + Source xsltSource = findXsltSource(resource); + + if (xsltSource == null) { + return renderHtml(request, contextPath, resource, encoding); + } + return renderXml(request, contextPath, resource, xsltSource, encoding); + } + + + /** + * Return an InputStream to an XML representation of the contents this directory. + * + * @param request The HttpServletRequest being served + * @param contextPath Context path to which our internal paths are relative + * @param resource The associated resource + * @param xsltSource The XSL stylesheet + * @param encoding The encoding to use to process the readme (if any) + * + * @return the XML data + * + * @throws IOException an IO error occurred + * @throws ServletException rendering error + */ + protected InputStream renderXml(HttpServletRequest request, String contextPath, WebResource resource, + Source xsltSource, String encoding) throws IOException, ServletException { + + StringBuilder sb = new StringBuilder(); + + sb.append(""); + sb.append(""); + + sb.append(""); + + String[] entries = resources.list(resource.getWebappPath()); + + // rewriteUrl(contextPath) is expensive. cache result for later reuse + String rewrittenContextPath = rewriteUrl(contextPath); + String directoryWebappPath = resource.getWebappPath(); + + for (String entry : entries) { + + if (entry.equalsIgnoreCase("WEB-INF") || entry.equalsIgnoreCase("META-INF") || + entry.equalsIgnoreCase(localXsltFile)) { + continue; + } + + if ((directoryWebappPath + entry).equals(contextXsltFile)) { + continue; + } + + WebResource childResource = resources.getResource(directoryWebappPath + entry); + if (!childResource.exists()) { + continue; + } + + sb.append("'); + sb.append(Escape.htmlElementContent(entry)); + if (childResource.isDirectory()) { + sb.append('/'); + } + sb.append(""); + } + sb.append(""); + + String readme = getReadme(resource, encoding); + + if (readme != null) { + sb.append(""); + } + + sb.append(""); + + // Prevent possible memory leak. Ensure Transformer and + // TransformerFactory are not loaded from the web application. + ClassLoader original; + Thread currentThread = Thread.currentThread(); + if (Globals.IS_SECURITY_ENABLED) { + PrivilegedGetTccl pa = new PrivilegedGetTccl(currentThread); + original = AccessController.doPrivileged(pa); + } else { + original = currentThread.getContextClassLoader(); + } + try { + if (Globals.IS_SECURITY_ENABLED) { + PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, DefaultServlet.class.getClassLoader()); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(DefaultServlet.class.getClassLoader()); + } + + TransformerFactory tFactory = TransformerFactory.newInstance(); + Source xmlSource = new StreamSource(new StringReader(sb.toString())); + Transformer transformer = tFactory.newTransformer(xsltSource); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8); + StreamResult out = new StreamResult(osWriter); + transformer.transform(xmlSource, out); + osWriter.flush(); + return new ByteArrayInputStream(stream.toByteArray()); + } catch (TransformerException e) { + throw new ServletException(sm.getString("defaultServlet.xslError"), e); + } finally { + if (Globals.IS_SECURITY_ENABLED) { + PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, original); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(original); + } + } + } + + /** + * Return an InputStream to an HTML representation of the contents of this directory. + * + * @param request The HttpServletRequest being served + * @param contextPath Context path to which our internal paths are relative + * @param resource The associated resource + * @param encoding The encoding to use to process the readme (if any) + * + * @return the HTML data + * + * @throws IOException an IO error occurred + */ + protected InputStream renderHtml(HttpServletRequest request, String contextPath, WebResource resource, + String encoding) throws IOException { + + // Prepare a writer to a buffered area + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8); + PrintWriter writer = new PrintWriter(osWriter); + + StringBuilder sb = new StringBuilder(); + + String directoryWebappPath = resource.getWebappPath(); + WebResource[] entries = resources.listResources(directoryWebappPath); + + // rewriteUrl(contextPath) is expensive. cache result for later reuse + String rewrittenContextPath = rewriteUrl(contextPath); + + // Render the page header + sb.append("\r\n"); + /* + * TODO Activate this as soon as we use smClient with the request locales + * sb.append("\r\n"); + */ + sb.append("\r\n"); + sb.append(""); + sb.append(sm.getString("directory.title", directoryWebappPath)); + sb.append("\r\n"); + sb.append(" "); + sb.append("\r\n"); + sb.append(""); + sb.append("

    "); + sb.append(sm.getString("directory.title", directoryWebappPath)); + + // Render the link to our parent (if required) + String parentDirectory = directoryWebappPath; + if (parentDirectory.endsWith("/")) { + parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1); + } + int slash = parentDirectory.lastIndexOf('/'); + if (slash >= 0) { + String parent = directoryWebappPath.substring(0, slash); + sb.append(" - "); + sb.append(""); + sb.append(sm.getString("directory.parent", parent)); + sb.append(""); + sb.append(""); + } + + sb.append("

    "); + sb.append("
    "); + + sb.append("\r\n"); + + SortManager.Order order; + if (sortListings && null != request) { + order = sortManager.getOrder(request.getQueryString()); + } else { + order = null; + } + // Render the column headings + sb.append("\r\n"); + sb.append("\r\n"); + sb.append("\r\n"); + sb.append("\r\n"); + sb.append(""); + + if (null != sortManager && null != request) { + sortManager.sort(entries, request.getQueryString()); + } + + boolean shade = false; + for (WebResource childResource : entries) { + String filename = childResource.getName(); + if (filename.equalsIgnoreCase("WEB-INF") || filename.equalsIgnoreCase("META-INF")) { + continue; + } + + if (!childResource.exists()) { + continue; + } + + sb.append("\r\n"); + shade = !shade; + + sb.append("\r\n"); + + sb.append("\r\n"); + + sb.append("\r\n"); + + sb.append("\r\n"); + } + + // Render the page footer + sb.append("
    "); + if (sortListings && null != request) { + sb.append(""); + sb.append(sm.getString("directory.filename")); + sb.append(""); + } else { + sb.append(sm.getString("directory.filename")); + } + sb.append(""); + if (sortListings && null != request) { + sb.append(""); + sb.append(sm.getString("directory.size")); + sb.append(""); + } else { + sb.append(sm.getString("directory.size")); + } + sb.append(""); + if (sortListings && null != request) { + sb.append(""); + sb.append(sm.getString("directory.lastModified")); + sb.append(""); + } else { + sb.append(sm.getString("directory.lastModified")); + } + sb.append("
      \r\n"); + sb.append(""); + sb.append(Escape.htmlElementContent(filename)); + if (childResource.isDirectory()) { + sb.append('/'); + } + sb.append(""); + if (childResource.isDirectory()) { + sb.append(" "); + } else { + sb.append(renderSize(childResource.getContentLength())); + } + sb.append(""); + sb.append(childResource.getLastModifiedHttp()); + sb.append("
    \r\n"); + + sb.append("
    "); + + String readme = getReadme(resource, encoding); + if (readme != null) { + sb.append(readme); + sb.append("
    "); + } + + if (showServerInfo) { + sb.append("

    ").append(ServerInfo.getServerInfo()).append("

    "); + } + sb.append("\r\n"); + sb.append("\r\n"); + + // Return an input stream to the underlying bytes + writer.write(sb.toString()); + writer.flush(); + return new ByteArrayInputStream(stream.toByteArray()); + + } + + + /** + * Render the specified file size (in bytes). + * + * @param size File size (in bytes) + * + * @return the formatted size + */ + protected String renderSize(long size) { + + long leftSide = size / 1024; + long rightSide = (size % 1024) / 103; // Makes 1 digit + if ((leftSide == 0) && (rightSide == 0) && (size > 0)) { + rightSide = 1; + } + + return ("" + leftSide + "." + rightSide + " KiB"); + + } + + + /** + * Get the readme file as a string. + * + * @param directory The directory to search + * @param encoding The readme encoding + * + * @return the readme for the specified directory + */ + protected String getReadme(WebResource directory, String encoding) { + + if (readmeFile != null) { + WebResource resource = resources.getResource(directory.getWebappPath() + readmeFile); + if (resource.isFile()) { + StringWriter buffer = new StringWriter(); + InputStreamReader reader = null; + try (InputStream is = resource.getInputStream()) { + if (encoding != null) { + reader = new InputStreamReader(is, encoding); + } else { + reader = new InputStreamReader(is); + } + copyRange(reader, new PrintWriter(buffer)); + } catch (IOException e) { + log(sm.getString("defaultServlet.readerCloseFailed"), e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + } + } + } + return buffer.toString(); + } else { + if (debug > 10) { + log("readme '" + readmeFile + "' not found"); + } + + return null; + } + } + + return null; + } + + + /** + * Return a Source for the xsl template (if possible). + * + * @param directory The directory to search + * + * @return the source for the specified directory + * + * @throws IOException an IO error occurred + */ + protected Source findXsltSource(WebResource directory) throws IOException { + + if (localXsltFile != null) { + WebResource resource = resources.getResource(directory.getWebappPath() + localXsltFile); + if (resource.isFile()) { + InputStream is = resource.getInputStream(); + if (is != null) { + if (Globals.IS_SECURITY_ENABLED) { + return secureXslt(is); + } else { + return new StreamSource(is); + } + } + } + if (debug > 10) { + log("localXsltFile '" + localXsltFile + "' not found"); + } + } + + if (contextXsltFile != null) { + InputStream is = getServletContext().getResourceAsStream(contextXsltFile); + if (is != null) { + if (Globals.IS_SECURITY_ENABLED) { + return secureXslt(is); + } else { + return new StreamSource(is); + } + } + + if (debug > 10) { + log("contextXsltFile '" + contextXsltFile + "' not found"); + } + } + + /* + * Open and read in file in one fell swoop to reduce chance chance of leaving handle open. + */ + if (globalXsltFile != null) { + File f = validateGlobalXsltFile(); + if (f != null) { + long globalXsltFileSize = f.length(); + if (globalXsltFileSize > Integer.MAX_VALUE) { + log("globalXsltFile [" + f.getAbsolutePath() + "] is too big to buffer"); + } else { + try (FileInputStream fis = new FileInputStream(f)) { + byte b[] = new byte[(int) f.length()]; + IOTools.readFully(fis, b); + return new StreamSource(new ByteArrayInputStream(b)); + } + } + } + } + + return null; + } + + + private File validateGlobalXsltFile() { + Context context = resources.getContext(); + + File baseConf = new File(context.getCatalinaBase(), "conf"); + File result = validateGlobalXsltFile(baseConf); + if (result == null) { + File homeConf = new File(context.getCatalinaHome(), "conf"); + if (!baseConf.equals(homeConf)) { + result = validateGlobalXsltFile(homeConf); + } + } + + return result; + } + + + private File validateGlobalXsltFile(File base) { + File candidate = new File(globalXsltFile); + if (!candidate.isAbsolute()) { + candidate = new File(base, globalXsltFile); + } + + if (!candidate.isFile()) { + return null; + } + + // First check that the resulting path is under the provided base + try { + if (!candidate.getCanonicalFile().toPath().startsWith(base.getCanonicalFile().toPath())) { + return null; + } + } catch (IOException ioe) { + return null; + } + + // Next check that an .xsl or .xslt file has been specified + String nameLower = candidate.getName().toLowerCase(Locale.ENGLISH); + if (!nameLower.endsWith(".xslt") && !nameLower.endsWith(".xsl")) { + return null; + } + + return candidate; + } + + + private Source secureXslt(InputStream is) { + // Need to filter out any external entities + Source result = null; + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(secureEntityResolver); + Document document = builder.parse(is); + result = new DOMSource(document); + } catch (ParserConfigurationException | SAXException | IOException e) { + if (debug > 0) { + log(e.getMessage(), e); + } + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Ignore + } + } + } + return result; + } + + + // -------------------------------------------------------- protected Methods + + /** + * Check if sendfile can be used. + * + * @param request The Servlet request + * @param response The Servlet response + * @param resource The resource + * @param length The length which will be written (will be used only if range is null) + * @param range The range that will be written + * + * @return true if sendfile should be used (writing is then delegated to the endpoint) + */ + protected boolean checkSendfile(HttpServletRequest request, HttpServletResponse response, WebResource resource, + long length, Ranges.Entry range) { + String canonicalPath; + if (sendfileSize > 0 && length > sendfileSize && + (Boolean.TRUE.equals(request.getAttribute(Globals.SENDFILE_SUPPORTED_ATTR))) && + (request.getClass().getName().equals("org.apache.catalina.connector.RequestFacade")) && + (response.getClass().getName().equals("org.apache.catalina.connector.ResponseFacade")) && + resource.isFile() && ((canonicalPath = resource.getCanonicalPath()) != null)) { + request.setAttribute(Globals.SENDFILE_FILENAME_ATTR, canonicalPath); + if (range == null) { + request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0L)); + request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(length)); + } else { + request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(getStart(range, length))); + request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(getEnd(range, length) + 1)); + } + return true; + } + return false; + } + + + /** + * Check if the if-match condition is satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resource The resource + * + * @return true if the resource meets the specified condition, and false if the condition + * is not satisfied, in which case request processing is stopped + * + * @throws IOException an IO error occurred + */ + protected boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response, WebResource resource) + throws IOException { + + String headerValue = request.getHeader("If-Match"); + if (headerValue != null) { + + boolean conditionSatisfied; + + if (!headerValue.equals("*")) { + String resourceETag = generateETag(resource); + if (resourceETag == null) { + conditionSatisfied = false; + } else { + // RFC 7232 requires strong comparison for If-Match headers + Boolean matched = EntityTag.compareEntityTag(new StringReader(headerValue), false, resourceETag); + if (matched == null) { + if (debug > 10) { + log("DefaultServlet.checkIfMatch: Invalid header value [" + headerValue + "]"); + } + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return false; + } + conditionSatisfied = matched.booleanValue(); + } + } else { + conditionSatisfied = true; + } + + if (!conditionSatisfied) { + response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); + return false; + } + } + return true; + } + + + /** + * Check if the if-modified-since condition is satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resource The resource + * + * @return true if the resource meets the specified condition, and false if the condition + * is not satisfied, in which case request processing is stopped + */ + protected boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response, + WebResource resource) { + try { + long headerValue = request.getDateHeader("If-Modified-Since"); + long lastModified = resource.getLastModified(); + if (headerValue != -1) { + + // If an If-None-Match header has been specified, if modified since + // is ignored. + if ((request.getHeader("If-None-Match") == null) && (lastModified < headerValue + 1000)) { + // The entity has not been modified since the date + // specified by the client. This is not an error case. + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + response.setHeader("ETag", generateETag(resource)); + + return false; + } + } + } catch (IllegalArgumentException illegalArgument) { + return true; + } + return true; + } + + + /** + * Check if the if-none-match condition is satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resource The resource + * + * @return true if the resource meets the specified condition, and false if the condition + * is not satisfied, in which case request processing is stopped + * + * @throws IOException an IO error occurred + */ + protected boolean checkIfNoneMatch(HttpServletRequest request, HttpServletResponse response, WebResource resource) + throws IOException { + + String headerValue = request.getHeader("If-None-Match"); + if (headerValue != null) { + + boolean conditionSatisfied; + + String resourceETag = generateETag(resource); + if (!headerValue.equals("*")) { + if (resourceETag == null) { + conditionSatisfied = false; + } else { + // RFC 7232 requires weak comparison for If-None-Match headers + Boolean matched = EntityTag.compareEntityTag(new StringReader(headerValue), true, resourceETag); + if (matched == null) { + if (debug > 10) { + log("DefaultServlet.checkIfNoneMatch: Invalid header value [" + headerValue + "]"); + } + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return false; + } + conditionSatisfied = matched.booleanValue(); + } + } else { + conditionSatisfied = true; + } + + if (conditionSatisfied) { + // For GET and HEAD, we should respond with + // 304 Not Modified. + // For every other method, 412 Precondition Failed is sent + // back. + if ("GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod())) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + response.setHeader("ETag", resourceETag); + } else { + response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); + } + return false; + } + } + return true; + } + + + /** + * Check if the if-unmodified-since condition is satisfied. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param resource The resource + * + * @return true if the resource meets the specified condition, and false if the condition + * is not satisfied, in which case request processing is stopped + * + * @throws IOException an IO error occurred + */ + protected boolean checkIfUnmodifiedSince(HttpServletRequest request, HttpServletResponse response, + WebResource resource) throws IOException { + try { + long lastModified = resource.getLastModified(); + long headerValue = request.getDateHeader("If-Unmodified-Since"); + if (headerValue != -1) { + if (lastModified >= (headerValue + 1000)) { + // The entity has not been modified since the date + // specified by the client. This is not an error case. + response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); + return false; + } + } + } catch (IllegalArgumentException illegalArgument) { + return true; + } + return true; + } + + + /** + * Provides the entity tag (the ETag header) for the given resource. Intended to be over-ridden by custom + * DefaultServlet implementations that wish to use an alternative format for the entity tag. + * + * @param resource The resource for which an entity tag is required. + * + * @return The result of calling {@link WebResource#getETag()} on the given resource + */ + protected String generateETag(WebResource resource) { + return resource.getETag(); + } + + + /** + * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are + * closed before returning (even in the face of an exception). + * + * @param is The input stream to read the source resource from + * @param ostream The output stream to write to + * + * @exception IOException if an input/output error occurs + */ + protected void copy(InputStream is, ServletOutputStream ostream) throws IOException { + + IOException exception = null; + InputStream istream = new BufferedInputStream(is, input); + + // Copy the input stream to the output stream + exception = copyRange(istream, ostream); + + // Clean up the input stream + istream.close(); + + // Rethrow any exception that has occurred + if (exception != null) { + throw exception; + } + } + + + /** + * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are + * closed before returning (even in the face of an exception). + * + * @param is The input stream to read the source resource from + * @param writer The writer to write to + * @param encoding The encoding to use when reading the source input stream + * + * @exception IOException if an input/output error occurs + */ + protected void copy(InputStream is, PrintWriter writer, String encoding) throws IOException { + IOException exception = null; + + Reader reader; + if (encoding == null) { + reader = new InputStreamReader(is); + } else { + reader = new InputStreamReader(is, encoding); + } + + // Copy the input stream to the output stream + exception = copyRange(reader, writer); + + // Clean up the reader + reader.close(); + + // Rethrow any exception that has occurred + if (exception != null) { + throw exception; + } + } + + + /** + * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are + * closed before returning (even in the face of an exception). + * + * @param resource The source resource + * @param length the resource length + * @param ostream The output stream to write to + * @param range Range the client wanted to retrieve + * + * @exception IOException if an input/output error occurs + */ + protected void copy(WebResource resource, long length, ServletOutputStream ostream, Ranges.Entry range) + throws IOException { + + IOException exception = null; + + InputStream resourceInputStream = resource.getInputStream(); + InputStream istream = new BufferedInputStream(resourceInputStream, input); + exception = copyRange(istream, ostream, getStart(range, length), getEnd(range, length)); + + // Clean up the input stream + istream.close(); + + // Rethrow any exception that has occurred + if (exception != null) { + throw exception; + } + + } + + + /** + * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are + * closed before returning (even in the face of an exception). + * + * @param resource The source resource + * @param length the resource length + * @param ostream The output stream to write to + * @param ranges Enumeration of the ranges the client wanted to retrieve + * @param contentType Content type of the resource + * + * @exception IOException if an input/output error occurs + */ + protected void copy(WebResource resource, long length, ServletOutputStream ostream, Ranges ranges, + String contentType) throws IOException { + + IOException exception = null; + + for (Ranges.Entry range : ranges.getEntries()) { + if (exception != null) { + break; + } + InputStream resourceInputStream = resource.getInputStream(); + try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) { + + // Writing MIME header. + ostream.println(); + ostream.println("--" + mimeSeparation); + if (contentType != null) { + ostream.println("Content-Type: " + contentType); + } + long start = getStart(range, length); + long end = getEnd(range, length); + ostream.println("Content-Range: bytes " + start + "-" + end + "/" + (end - start)); + ostream.println(); + + // Printing content + exception = copyRange(istream, ostream, start, end); + } + } + + ostream.println(); + ostream.print("--" + mimeSeparation + "--"); + + // Rethrow any exception that has occurred + if (exception != null) { + throw exception; + } + + } + + + /** + * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are + * closed before returning (even in the face of an exception). + * + * @param istream The input stream to read from + * @param ostream The output stream to write to + * + * @return Exception which occurred during processing + */ + protected IOException copyRange(InputStream istream, ServletOutputStream ostream) { + + // Copy the input stream to the output stream + IOException exception = null; + byte buffer[] = new byte[input]; + int len = buffer.length; + while (true) { + try { + len = istream.read(buffer); + if (len == -1) { + break; + } + ostream.write(buffer, 0, len); + } catch (IOException e) { + exception = e; + len = -1; + break; + } + } + return exception; + + } + + + /** + * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are + * closed before returning (even in the face of an exception). + * + * @param reader The reader to read from + * @param writer The writer to write to + * + * @return Exception which occurred during processing + */ + protected IOException copyRange(Reader reader, PrintWriter writer) { + + // Copy the input stream to the output stream + IOException exception = null; + char buffer[] = new char[input]; + int len = buffer.length; + while (true) { + try { + len = reader.read(buffer); + if (len == -1) { + break; + } + writer.write(buffer, 0, len); + } catch (IOException e) { + exception = e; + len = -1; + break; + } + } + return exception; + + } + + + /** + * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are + * closed before returning (even in the face of an exception). + * + * @param istream The input stream to read from + * @param ostream The output stream to write to + * @param start Start of the range which will be copied + * @param end End of the range which will be copied + * + * @return Exception which occurred during processing + */ + protected IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) { + + if (debug > 10) { + log("Serving bytes:" + start + "-" + end); + } + + long skipped = 0; + try { + skipped = istream.skip(start); + } catch (IOException e) { + return e; + } + if (skipped < start) { + return new IOException(sm.getString("defaultServlet.skipfail", Long.valueOf(skipped), Long.valueOf(start))); + } + + IOException exception = null; + long bytesToRead = end - start + 1; + + byte buffer[] = new byte[input]; + int len = buffer.length; + while ((bytesToRead > 0) && (len >= buffer.length)) { + try { + len = istream.read(buffer); + if (bytesToRead >= len) { + ostream.write(buffer, 0, len); + bytesToRead -= len; + } else { + ostream.write(buffer, 0, (int) bytesToRead); + bytesToRead = 0; + } + } catch (IOException e) { + exception = e; + len = -1; + } + if (len < buffer.length) { + break; + } + } + + return exception; + + } + + + protected static class CompressionFormat implements Serializable { + private static final long serialVersionUID = 1L; + public final String extension; + public final String encoding; + + public CompressionFormat(String extension, String encoding) { + this.extension = extension; + this.encoding = encoding; + } + } + + + private static class PrecompressedResource { + public final WebResource resource; + public final CompressionFormat format; + + private PrecompressedResource(WebResource resource, CompressionFormat format) { + this.resource = resource; + this.format = format; + } + } + + + /** + * This is secure in the sense that any attempt to use an external entity will trigger an exception. + */ + private static class SecureEntityResolver implements EntityResolver2 { + + @Override + public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { + throw new SAXException(sm.getString("defaultServlet.blockExternalEntity", publicId, systemId)); + } + + @Override + public InputSource getExternalSubset(String name, String baseURI) throws SAXException, IOException { + throw new SAXException(sm.getString("defaultServlet.blockExternalSubset", name, baseURI)); + } + + @Override + public InputSource resolveEntity(String name, String publicId, String baseURI, String systemId) + throws SAXException, IOException { + throw new SAXException( + sm.getString("defaultServlet.blockExternalEntity2", name, publicId, baseURI, systemId)); + } + } + + + /** + * Gets the ordering character to be used for a particular column. + * + * @param order The order that is currently being applied + * @param column The column that will be rendered. + * + * @return Either 'A' or 'D', to indicate "ascending" or "descending" sort order. + */ + private char getOrderChar(SortManager.Order order, char column) { + if (column == order.column) { + if (order.ascending) { + return 'D'; + } else { + return 'A'; + } + } else { + return 'D'; + } + } + + + /** + * A class encapsulating the sorting of resources. + */ + private static class SortManager { + /** + * The default sort. + */ + protected Comparator defaultResourceComparator; + + /** + * Comparator to use when sorting resources by name. + */ + protected Comparator resourceNameComparator; + + /** + * Comparator to use when sorting files by name, ascending (reverse). + */ + protected Comparator resourceNameComparatorAsc; + + /** + * Comparator to use when sorting resources by size. + */ + protected Comparator resourceSizeComparator; + + /** + * Comparator to use when sorting files by size, ascending (reverse). + */ + protected Comparator resourceSizeComparatorAsc; + + /** + * Comparator to use when sorting resources by last-modified date. + */ + protected Comparator resourceLastModifiedComparator; + + /** + * Comparator to use when sorting files by last-modified date, ascending (reverse). + */ + protected Comparator resourceLastModifiedComparatorAsc; + + SortManager(boolean directoriesFirst) { + resourceNameComparator = Comparator.comparing(WebResource::getName); + resourceNameComparatorAsc = resourceNameComparator.reversed(); + resourceSizeComparator = + Comparator.comparing(WebResource::getContentLength).thenComparing(resourceNameComparator); + resourceSizeComparatorAsc = resourceSizeComparator.reversed(); + resourceLastModifiedComparator = + Comparator.comparing(WebResource::getLastModified).thenComparing(resourceNameComparator); + resourceLastModifiedComparatorAsc = resourceLastModifiedComparator.reversed(); + + if (directoriesFirst) { + Comparator dirsFirst = comparingTrueFirst(WebResource::isDirectory); + resourceNameComparator = dirsFirst.thenComparing(resourceNameComparator); + resourceNameComparatorAsc = dirsFirst.thenComparing(resourceNameComparatorAsc); + resourceSizeComparator = dirsFirst.thenComparing(resourceSizeComparator); + resourceSizeComparatorAsc = dirsFirst.thenComparing(resourceSizeComparatorAsc); + resourceLastModifiedComparator = dirsFirst.thenComparing(resourceLastModifiedComparator); + resourceLastModifiedComparatorAsc = dirsFirst.thenComparing(resourceLastModifiedComparatorAsc); + } + + defaultResourceComparator = resourceNameComparator; + } + + /** + * Sorts an array of resources according to an ordering string. + * + * @param resources The array to sort. + * @param order The ordering string. + * + * @see #getOrder(String) + */ + public void sort(WebResource[] resources, String order) { + Comparator comparator = getComparator(order); + + if (null != comparator) { + Arrays.sort(resources, comparator); + } + } + + public Comparator getComparator(String order) { + return getComparator(getOrder(order)); + } + + public Comparator getComparator(Order order) { + if (null == order) { + return defaultResourceComparator; + } + + if ('N' == order.column) { + if (order.ascending) { + return resourceNameComparatorAsc; + } else { + return resourceNameComparator; + } + } + + if ('S' == order.column) { + if (order.ascending) { + return resourceSizeComparatorAsc; + } else { + return resourceSizeComparator; + } + } + + if ('M' == order.column) { + if (order.ascending) { + return resourceLastModifiedComparatorAsc; + } else { + return resourceLastModifiedComparator; + } + } + + return defaultResourceComparator; + } + + /** + * Gets the Order to apply given an ordering-string. This ordering-string matches a subset of the + * ordering-strings supported by Apache + * httpd. + * + * @param order The ordering-string provided by the client. + * + * @return An Order specifying the column and ascending/descending to be applied to resources. + */ + public Order getOrder(String order) { + if (null == order || 0 == order.trim().length()) { + return Order.DEFAULT; + } + + String[] options = order.split(";"); + + if (0 == options.length) { + return Order.DEFAULT; + } + + char column = '\0'; + boolean ascending = false; + + for (String option : options) { + option = option.trim(); + + if (2 < option.length()) { + char opt = option.charAt(0); + if ('C' == opt) { + column = option.charAt(2); + } else if ('O' == opt) { + ascending = ('A' == option.charAt(2)); + } + } + } + + if ('N' == column) { + if (ascending) { + return Order.NAME_ASC; + } else { + return Order.NAME; + } + } + + if ('S' == column) { + if (ascending) { + return Order.SIZE_ASC; + } else { + return Order.SIZE; + } + } + + if ('M' == column) { + if (ascending) { + return Order.LAST_MODIFIED_ASC; + } else { + return Order.LAST_MODIFIED; + } + } + + return Order.DEFAULT; + } + + public static class Order { + final char column; + final boolean ascending; + + Order(char column, boolean ascending) { + this.column = column; + this.ascending = ascending; + } + + public static final Order NAME = new Order('N', false); + public static final Order NAME_ASC = new Order('N', true); + public static final Order SIZE = new Order('S', false); + public static final Order SIZE_ASC = new Order('S', true); + public static final Order LAST_MODIFIED = new Order('M', false); + public static final Order LAST_MODIFIED_ASC = new Order('M', true); + + public static final Order DEFAULT = NAME; + } + } + + + private static Comparator comparingTrueFirst(Function keyExtractor) { + return (s1, s2) -> { + Boolean r1 = keyExtractor.apply(s1); + Boolean r2 = keyExtractor.apply(s2); + if (r1.booleanValue()) { + if (r2.booleanValue()) { + return 0; + } else { + return -1; // r1 (property is true) first + } + } else if (r2.booleanValue()) { + return 1; // r2 (property is true) first + } else { + return 0; + } + }; + } + + + enum BomConfig { + /** + * BoM is stripped if present and any BoM found used to determine the encoding used to read the resource. + */ + TRUE("true", true, true), + /** + * BoM is stripped if present but the configured file encoding is used to read the resource. + */ + FALSE("false", true, false), + /** + * BoM is not stripped and the configured file encoding is used to read the resource. + */ + PASS_THROUGH("pass-through", false, false); + + final String configurationValue; + final boolean stripBom; + final boolean useBomEncoding; + + BomConfig(String configurationValue, boolean stripBom, boolean useBomEncoding) { + this.configurationValue = configurationValue; + this.stripBom = stripBom; + this.useBomEncoding = useBomEncoding; + } + } +} diff --git a/java/org/apache/catalina/servlets/LocalStrings.properties b/java/org/apache/catalina/servlets/LocalStrings.properties new file mode 100644 index 0000000..3880765 --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings.properties @@ -0,0 +1,58 @@ +# 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. + +cgiServlet.emptyEnvVarName=Empty environment variable name in initialisation parameter [environment-variable-] +cgiServlet.expandCloseFail=Failed to close input stream for script at path [{0}] +cgiServlet.expandCreateDirFail=Failed to create destination directory [{0}] for script expansion +cgiServlet.expandDeleteFail=Failed to delete file at [{0}] after IOException during expansion +cgiServlet.expandFail=Failed to expand script at path [{0}] to [{1}] +cgiServlet.expandNotFound=Unable to expand [{0}] as it could not be found +cgiServlet.expandOk=Expanded script at path [{0}] to [{1}] +cgiServlet.find.found=Found CGI: name [{0}], path [{1}], script name [{2}] and CGI name [{3}] +cgiServlet.find.location=Looking for a file at [{0}] +cgiServlet.find.path=CGI script requested at path [{0}] relative to CGI location [{1}] +cgiServlet.invalidArgumentDecoded=The decoded command line argument [{0}] did not match the configured cmdLineArgumentsDecoded pattern [{1}] +cgiServlet.invalidArgumentEncoded=The encoded command line argument [{0}] did not match the configured cmdLineArgumentsEncoded pattern [{1}] +cgiServlet.invalidCommand=Illegal Character in CGI command path ('.' or '..') detected, not running CGI [{0}] +cgiServlet.notReady=CGI Servlet is not ready to run +cgiServlet.runBadHeader=Bad header line [{0}] +cgiServlet.runFail=I/O problems processing CGI +cgiServlet.runHeaderReaderFail=I/O problems closing header reader +cgiServlet.runInvalidStatus=Invalid status [{0}] +cgiServlet.runOutputStreamFail=I/O problems closing output stream +cgiServlet.runReaderInterrupt=Interrupted waiting for stderr reader thread +cgiServlet.runStdErr=stderr line: [{0}] +cgiServlet.runStdErrCount=Received [{0}] lines on stderr +cgiServlet.runStdErrFail=I/O problems with stderr + +defaultServlet.blockExternalEntity=Blocked access to external entity with publicId [{0}] and systemId [{0}] +defaultServlet.blockExternalEntity2=Blocked access to external entity with name [{0}], publicId [{1}], baseURI [{2}] and systemId [{3}] +defaultServlet.blockExternalSubset=Blocked access to external subset with name [{0}] and baseURI [{1}] +defaultServlet.missingResource=The requested resource [{0}] is not available +defaultServlet.noResources=No static resources were found +defaultServlet.readerCloseFailed=Failed to close reader +defaultServlet.skipfail=Read failed because only [{0}] bytes were available but needed to skip [{1}] bytes to reach the start of the requested range +defaultServlet.unknownBomConfig=Unrecognised value of [{0}] provided for useBomIfPresent initialization parameter +defaultServlet.xslError=XSL transformer error + +directory.filename=Filename +directory.lastModified=Last Modified +directory.parent=Up To [{0}] +directory.size=Size +directory.title=Directory Listing For [{0}] + +webdavservlet.externalEntityIgnored=The request included a reference to an external entity with PublicID [{0}] and SystemID [{1}] which was ignored +webdavservlet.inputstreamclosefail=Failed to close the inputStream of [{0}] +webdavservlet.jaxpfailed=JAXP initialization failed diff --git a/java/org/apache/catalina/servlets/LocalStrings_cs.properties b/java/org/apache/catalina/servlets/LocalStrings_cs.properties new file mode 100644 index 0000000..d02972b --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_cs.properties @@ -0,0 +1,21 @@ +# 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. + +cgiServlet.expandFail=Selhalo rozbalení skriptu z cesty [{0}] na [{1}] +cgiServlet.runHeaderReaderFail=I/O problémy pÅ™i uzavírání ÄteÄky hlaviÄek +cgiServlet.runInvalidStatus=Neplatný status [{0}] +cgiServlet.runReaderInterrupt=PÅ™eruÅ¡eno Äekání na stderr Ätecí vlákno + +defaultServlet.blockExternalSubset=Blokovaný přístup na externí podmnožinu se jménem [{0}] a baseURI [{1}] diff --git a/java/org/apache/catalina/servlets/LocalStrings_de.properties b/java/org/apache/catalina/servlets/LocalStrings_de.properties new file mode 100644 index 0000000..9e6a204 --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_de.properties @@ -0,0 +1,24 @@ +# 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. + +cgiServlet.expandFail=Fehler beim Expandieren des Skript-Pfades [{0}] zu [{1}] +cgiServlet.find.location=Suche nach einem File unter [{0}] +cgiServlet.runHeaderReaderFail=I/O Problem beim Schließen des Header-Readers +cgiServlet.runInvalidStatus=Ungültiger Status [{0}] +cgiServlet.runOutputStreamFail=I/O Probleme beim Schließen des Ausgabestroms +cgiServlet.runReaderInterrupt=Das Warten auf den STDERR Lesethread wurde unterbrochen +cgiServlet.runStdErrFail=I/O Probleme mit stderr + +directory.filename=Dateiname diff --git a/java/org/apache/catalina/servlets/LocalStrings_es.properties b/java/org/apache/catalina/servlets/LocalStrings_es.properties new file mode 100644 index 0000000..d56dc2c --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_es.properties @@ -0,0 +1,36 @@ +# 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. + +cgiServlet.expandFail=Fallo al expandir el script [{0}] en [{1}]\n +cgiServlet.expandOk=Expandiendo script en el path [{0}] hacia [{1}]\n +cgiServlet.find.location=Buscando archivo en [{0}]\n +cgiServlet.runHeaderReaderFail=Problemas de I/O cerrando la cabecera del lector +cgiServlet.runInvalidStatus=Estado inválido [{0}] +cgiServlet.runOutputStreamFail=Errores I/O cerrando el flujo de salida +cgiServlet.runReaderInterrupt=Detenido esperando por el hilo lector stderr +cgiServlet.runStdErrFail=Problemas de I/O con stderr + +defaultServlet.blockExternalSubset=Se bloqueó el acceso al subconjunt externo con nombre [{0}] y URI base [{1}]\n +defaultServlet.missingResource=El recurso requerido [{0}] no se encuentra disponible +defaultServlet.skipfail=Sólo se han saltado [{0}] cuando se requirieron [{1}] + +directory.filename=Nombre de Fichero: +directory.lastModified=Última Modificación +directory.parent=Atrás A [{0}] +directory.size=Medida +directory.title=Listado de Directorio Para [{0}] + +webdavservlet.externalEntityIgnored=El requerimiento incluía una referencia a una entidad externa con PublicID [{0}] y SystemID [{1}] que fue ignorada +webdavservlet.jaxpfailed=Falló la inicialización de JAXP diff --git a/java/org/apache/catalina/servlets/LocalStrings_fr.properties b/java/org/apache/catalina/servlets/LocalStrings_fr.properties new file mode 100644 index 0000000..904faf0 --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_fr.properties @@ -0,0 +1,58 @@ +# 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. + +cgiServlet.emptyEnvVarName=la nom de variable d'environnement est vide dans le paramètre d'initialisation [environment-variable-] +cgiServlet.expandCloseFail=Impossible de fermer le flux d''entrée du script avec le chemin [{0}] +cgiServlet.expandCreateDirFail=Echec de la création du répertoire de destination [{0}] pour la décompression du script +cgiServlet.expandDeleteFail=Impossible d''effacer le fichier [{0}] suite à une IOException pendant la décompression +cgiServlet.expandFail=Impossible de faire l''expansion du script au chemin [{0}] vers [{1}] +cgiServlet.expandNotFound=Impossible de décompresser [{0}] car il n''a pas été trouvé +cgiServlet.expandOk=Extrait le script du chemin [{0}] vers [{1}] +cgiServlet.find.found=Trouvé le CGI : nom [{0}], chemin [{1}], nom de script [{2}] et nom du CGI [{3}] +cgiServlet.find.location=Recherche d''un fichier en [{0}] +cgiServlet.find.path=Script CGI demandé au chemin [{0}] relatif au CGI à [{1}] +cgiServlet.invalidArgumentDecoded=Les paramètres de ligne de commande décodés [{0}] ne correspondent pas au modèle cmdLineArgumentsDecoded configuré [{1}] +cgiServlet.invalidArgumentEncoded=Les paramètres de ligne de commande encodés [{0}] ne correspondent pas au modèle cmdLineArgumentsEncoded configuré [{1}] +cgiServlet.invalidCommand=Un caractère illégal (''.'' or ''..'') a été trouvé dans le chemin de commande CGI, le CGI [{0}] n''est pas exécuté +cgiServlet.notReady=Le Servlet CGI n'est pas prêt à fonctionner +cgiServlet.runBadHeader=Mauvaise ligne d''en-tête [{0}] +cgiServlet.runFail=Problèmes d'IO lors de l'exécution du CGI +cgiServlet.runHeaderReaderFail=Problème d'E/S lors de la fermeture du lecteur de headers +cgiServlet.runInvalidStatus=Statut invalide [{0}] +cgiServlet.runOutputStreamFail=Problème d'E/S à la fermeture du flux de sortie +cgiServlet.runReaderInterrupt=Interrompu pendant l'attente du thread de lecture de la sortie d'erreur (stderr reader thread) +cgiServlet.runStdErr=ligne stderr : [{0}] +cgiServlet.runStdErrCount=Reçues [{0}] lignes sur le stderr +cgiServlet.runStdErrFail=Problème d'entrée sortie pour le stderr + +defaultServlet.blockExternalEntity=L''accès aux entités externes avec le publicId [{0}] et le systemId [{1}] est bloqué +defaultServlet.blockExternalEntity2=L''accès à l''entité externe nommée [{0}], publicId [{1}], baseURI [{2}], systemId [{3}] a été bloqué +defaultServlet.blockExternalSubset=L''accès au sous-ensemble externe de nom [{0}] et de baseURI [{1}] a été bloqué +defaultServlet.missingResource=La ressource demandée [{0}] n''est pas disponible +defaultServlet.noResources=Pas de ressources statiques +defaultServlet.readerCloseFailed=Impossible de fermer le lecteur +defaultServlet.skipfail=La lecture a échouée parce que seuls [{0}] octets étaient disponibles alors qu''il était nécessaire d''en sauter [{1}] pour atteindre le début de la plage demandée +defaultServlet.unknownBomConfig=La valeur [{0}] inconnue a été donnée pour le paramètre d’initialisation useBomIfPresent +defaultServlet.xslError=Erreur de transformation XSL + +directory.filename=Nom de fichier +directory.lastModified=Dernière modification +directory.parent=Jusqu''à [{0}] +directory.size=Taille +directory.title=Liste du répertoire pour [{0}] + +webdavservlet.externalEntityIgnored=La requête a inclus une référence à une entité externe avec publicId [{0}] et systemId [{1}] qui a été ignorée +webdavservlet.inputstreamclosefail=Impossible de fermer le flux d''entrée pour [{0}] +webdavservlet.jaxpfailed=Erreur d'initialisation de JAXP diff --git a/java/org/apache/catalina/servlets/LocalStrings_ja.properties b/java/org/apache/catalina/servlets/LocalStrings_ja.properties new file mode 100644 index 0000000..0ab8dae --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_ja.properties @@ -0,0 +1,58 @@ +# 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. + +cgiServlet.emptyEnvVarName=åˆæœŸåŒ–パラメータã®ç©ºã®ç’°å¢ƒå¤‰æ•°å[環境変数] +cgiServlet.expandCloseFail=パス [{0}] ã®ã‚¹ã‚¯ãƒªãƒ—トã®å…¥åŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—㟠+cgiServlet.expandCreateDirFail=スクリプトã®å±•é–‹å…ˆãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª[{0}]ã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +cgiServlet.expandDeleteFail=拡張中ã«IOExceptionã®å¾Œã« [{0}] ã§ãƒ•ã‚¡ã‚¤ãƒ«ã‚’削除ã§ãã¾ã›ã‚“ã§ã—㟠+cgiServlet.expandFail=パス [{0}] ã®ã‚¹ã‚¯ãƒªãƒ—トを [{1}] ã«å±•é–‹ã§ãã¾ã›ã‚“ã§ã—㟠+cgiServlet.expandNotFound=見ã¤ã‘ã‚‹ã“ã¨ãŒã§ããªã‹ã£ãŸãŸã‚ [{0}] を展開ã§ãã¾ã›ã‚“ +cgiServlet.expandOk=パス [{0}] ã® [{1}] ã«å±•é–‹ã•ã‚ŒãŸã‚¹ã‚¯ãƒªãƒ—ト +cgiServlet.find.found=見ã¤ã‹ã£ãŸCGI: åå‰ [{0}]ã€ãƒ‘ス [{1}]ã€ã‚¹ã‚¯ãƒªãƒ—トå [{2}]ã€CGIå [{3}] +cgiServlet.find.location=ファイル [{0}] を探ã—ã¦ã„ã¾ã™ã€‚ +cgiServlet.find.path=パス [{0}] ã§CGIロケーション [{1}] ã«å¯¾ã—ã¦ç›¸å¯¾çš„ã«è¦æ±‚ã•ã‚ŒãŸCGIスクリプト +cgiServlet.invalidArgumentDecoded=デコードã•ã‚ŒãŸã‚³ãƒžãƒ³ãƒ‰ãƒ©ã‚¤ãƒ³å¼•æ•° [{0}] ã¯ã€æ§‹æˆã•ã‚ŒãŸcmdLineArgumentsDecoded パターン [{1}] ã«ãƒžãƒƒãƒã—ã¾ã›ã‚“ +cgiServlet.invalidArgumentEncoded=エンコードã•ã‚ŒãŸã‚³ãƒžãƒ³ãƒ‰ãƒ©ã‚¤ãƒ³å¼•æ•° [{0}] ã¯ã€æ§‹æˆã•ã‚ŒãŸcmdLineArgumentsEncoded パターン [{1}] ã«ãƒžãƒƒãƒã—ã¾ã›ã‚“ +cgiServlet.invalidCommand=CGI コマンド パスã«ä¸æ­£ãªæ–‡å­— (''.'' ã¾ãŸã¯ ''..'') ãŒæ¤œå‡ºã•ã‚Œã¾ã—ãŸã€‚CGI [{0}] ã¯å®Ÿè¡Œã•ã‚Œã¦ã„ã¾ã›ã‚“ +cgiServlet.notReady=CGI サーブレットã¯å®Ÿè¡Œã®æº–å‚™ãŒã§ãã¦ã„ã¾ã›ã‚“ +cgiServlet.runBadHeader=悪ã„ヘッダライン [{0}] +cgiServlet.runFail=CGI処ç†ä¸­ã®IOå•é¡Œ +cgiServlet.runHeaderReaderFail=ヘッダーリーダーを閉ã˜ã‚‹æ™‚ã® I/O å•é¡Œ +cgiServlet.runInvalidStatus=無効ãªã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ [{0}] +cgiServlet.runOutputStreamFail=出力ストリームを切断ã™ã‚‹ã¨ã入出力エラーãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +cgiServlet.runReaderInterrupt=標準エラー出力ã®èª­ã¿å–りスレッドã¯å‰²ã‚Šè¾¼ã¿ã‚’å¾…æ©Ÿã—ã¦ã„ã¾ã™ã€‚ +cgiServlet.runStdErr=標準エラー 行:[{0}] +cgiServlet.runStdErrCount=stderrã® [{0}] 行をå—ä¿¡ã—ã¾ã—㟠+cgiServlet.runStdErrFail=stderrã§I/Oå•é¡Œ + +defaultServlet.blockExternalEntity=publicId [{0}]ãŠã‚ˆã³systemId [{0}]ã‚’æŒã¤å¤–部エンティティã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãŒãƒ–ロックã•ã‚Œã¾ã—㟠+defaultServlet.blockExternalEntity2=エンティティå [{0}]ã€publicId [{1}]ã€baseURI [{2}] ãŠã‚ˆã³ systemId [{3}] ã‚’æŒã¤å¤–部エンティティã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãŒãƒ–ロックã•ã‚Œã¾ã—㟠+defaultServlet.blockExternalSubset=åå‰[{0}]ãŠã‚ˆã³ãƒ™ãƒ¼ã‚¹URI [{1}]ã‚’æŒã¤å¤–部サブセットã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãŒãƒ–ロックã•ã‚Œã¾ã—㟠+defaultServlet.missingResource=è¦æ±‚ã•ã‚ŒãŸãƒªã‚½ãƒ¼ã‚¹ [{0}] ã¯åˆ©ç”¨ã§ãã¾ã›ã‚“。 +defaultServlet.noResources=é™çš„リソースãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +defaultServlet.readerCloseFailed=readerã®ã‚¯ãƒ­ãƒ¼ã‚ºã«å¤±æ•—ã—ã¾ã—㟠+defaultServlet.skipfail=[{1}]ãƒã‚¤ãƒˆã‚’スキップã—ã¦è¦æ±‚ã•ã‚ŒãŸç¯„囲ã®å…ˆé ­ã«åˆ°é”ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã—ãŸãŒã€[{0}]ãƒã‚¤ãƒˆã—ã‹åˆ©ç”¨ã§ããªã‹ã£ãŸãŸã‚読ã¿å–ã‚Šã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +defaultServlet.unknownBomConfig=useBomIfPresentã®åˆæœŸåŒ–パラメーターã«æä¾›ã•ã‚ŒãŸèªè­˜ã•ã‚Œãªã„値 [{0}] +defaultServlet.xslError=XSL変æ›ã‚¨ãƒ©ãƒ¼ + +directory.filename=ファイルå +directory.lastModified=最終更新 +directory.parent=[{0}] ã«ç§»å‹• +directory.size=サイズ +directory.title=[{0}] ã®ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã®ä¸€è¦§ + +webdavservlet.externalEntityIgnored=PublicID [{0}]ãŠã‚ˆã³SystemID [{1}]ã‚’æŒã¤å¤–部エンティティã¸ã®å‚照をå«ã‚€ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒç„¡è¦–ã•ã‚Œã¾ã—㟠+webdavservlet.inputstreamclosefail=入力ストリーム [{0}] を切断ã§ãã¾ã›ã‚“。 +webdavservlet.jaxpfailed=JAXPã®åˆæœŸåŒ–ã«å¤±æ•—ã—ã¾ã—㟠diff --git a/java/org/apache/catalina/servlets/LocalStrings_ko.properties b/java/org/apache/catalina/servlets/LocalStrings_ko.properties new file mode 100644 index 0000000..cd152b3 --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_ko.properties @@ -0,0 +1,56 @@ +# 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. + +cgiServlet.emptyEnvVarName=초기화 파ë¼ë¯¸í„° [environment-variable-] ë‚´ì—, 빈 문ìžì—´ì˜ 환경 변수 ì´ë¦„입니다. +cgiServlet.expandCloseFail=경로 [{0}]ì— ìœ„ì¹˜í•œ 스í¬ë¦½íŠ¸ë¥¼ 위한, ìž…ë ¥ ìŠ¤íŠ¸ë¦¼ì„ ë‹«ì§€ 못했습니다. +cgiServlet.expandCreateDirFail=스í¬ë¦½íŠ¸ë¥¼ 압축해제 하기 위한 ëŒ€ìƒ ë””ë ‰í† ë¦¬ [{0}]ì„(를) ìƒì„±í•˜ì§€ 못했습니다. +cgiServlet.expandDeleteFail=압축해제 중 IOExceptionì´ ë°œìƒí•œ 후, [{0}]ì— ìœ„ì¹˜í•œ 해당 파ì¼ì„ 삭제하지 못했습니다. +cgiServlet.expandFail=경로 [{0}]ì˜ ìŠ¤í¬ë¦½íŠ¸ë¥¼ [{1}](으)ë¡œ 압축해제 하지 못했습니다. +cgiServlet.expandNotFound=[{0}]ì„(를) ì°¾ì„ ìˆ˜ 없어서 압축해제 í•  수 없습니다. +cgiServlet.expandOk=[{0}] ê²½ë¡œì— ìžˆëŠ” 스트립트가 [{1}](으)ë¡œ 압축 í•´ì œë˜ì—ˆìŠµë‹ˆë‹¤. +cgiServlet.find.found=CGI 발견: ì´ë¦„ [{0}], 경로 [{1}], 스í¬ë¦½íŠ¸ ì´ë¦„ [{2}], CGI ì´ë¦„ [{3}] +cgiServlet.find.location=[{0}]ì— ìœ„ì¹˜í•œ 파ì¼ì„ 찾는 중 +cgiServlet.find.path=CGI 위치 [{1}]ì— ëŒ€í•´ ìƒëŒ€ì  경로 [{0}]ì— ìœ„ì¹˜í•œ, CGI 스í¬ë¦½íŠ¸ê°€ 요청ë˜ì—ˆìŠµë‹ˆë‹¤. +cgiServlet.invalidArgumentDecoded=ë””ì½”ë“œëœ ëª…ë ¹ í–‰ 아규먼트 [{0}]ì´(ê°€), ì„¤ì •ëœ cmdLineArgumentsDecoded 패턴 [{1}]ê³¼(와) 부합ë˜ì§€ 않습니다. +cgiServlet.invalidArgumentEncoded=ì¸ì½”ë“œëœ ëª…ë ¹ í–‰ 아규먼트 [{0}]ì´(ê°€), ì„¤ì •ëœ cmdLineArgumentsEncoded 패턴 [{1}]ê³¼(와) 부합ë˜ì§€ 않습니다. +cgiServlet.runBadHeader=ìž˜ëª»ëœ í—¤ë” í–‰: [{0}] +cgiServlet.runFail=CGI 처리 중 I/O 문제 ë°œìƒ +cgiServlet.runHeaderReaderFail=í—¤ë”를 ì½ê¸° 위한 reader를 닫는 중 I/O 문제 ë°œìƒ +cgiServlet.runInvalidStatus=유효하지 ì•Šì€ HTTP ìƒíƒœ: [{0}] +cgiServlet.runOutputStreamFail=출력 ìŠ¤íŠ¸ë¦¼ì„ ë‹«ëŠ” 중 I/O 문제 ë°œìƒ +cgiServlet.runReaderInterrupt=stderrì— ëŒ€í•œ ì½ê¸° 쓰레드를 기다리는 중 ì¤‘ë‹¨ë¨ +cgiServlet.runStdErr=stderr í–‰: [{0}] +cgiServlet.runStdErrCount=stderrì—ì„œ [{0}] í–‰ë“¤ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +cgiServlet.runStdErrFail=stderrì—ì„œ I/O 문제 ë°œìƒ + +defaultServlet.blockExternalEntity=PublicIdê°€ [{0}](ì´)ê³  systemIdê°€ [{0}]ì¸ ì™¸ë¶€ ì—”í‹°í‹°ì— ëŒ€í•œ ì ‘ê·¼ì„ ì°¨ë‹¨í–ˆìŠµë‹ˆë‹¤. +defaultServlet.blockExternalEntity2=ì´ë¦„ì´ [{0}], publicIdê°€ [{1}], baseURIê°€ [{2}]ì´ë©° systemIdê°€ [{3}]ì¸, 외부 ì—”í‹°í‹°ì— ëŒ€í•œ ì ‘ê·¼ì„ ì°¨ë‹¨í–ˆìŠµë‹ˆë‹¤. +defaultServlet.blockExternalSubset=ì´ë¦„ì´ [{0}](ì´)ê³  baseURIê°€ [{1}]ì¸ ì™¸ë¶€ 하위 ì§‘í•©ì— ëŒ€í•œ ì ‘ê·¼ì´ ì°¨ë‹¨ë˜ì—ˆìŠµë‹ˆë‹¤. +defaultServlet.missingResource=ìš”ì²­ëœ ë¦¬ì†ŒìŠ¤ [{0}]ì€(는) 가용하지 않습니다. +defaultServlet.noResources=ì •ì  ë¦¬ì†ŒìŠ¤ë“¤ì„ ì°¾ì„ ìˆ˜ 없었습니다. +defaultServlet.readerCloseFailed=Reader를 닫지 못했습니다. +defaultServlet.skipfail=단지 [{0}] ë°”ì´íŠ¸ë“¤ë§Œì´ 가용하기 때문ì—, ì½ê¸°ê°€ 실패했습니다. ìš”ì²­ëœ ë²”ìœ„ì˜ ì‹œìž‘ ìœ„ì¹˜ì— ë„달하기 위하여, [{1}] ë°”ì´íŠ¸ë“¤ì„ 건너뛰어야 했습니다. +defaultServlet.unknownBomConfig=useBomIfPresent 초기화 파ë¼ë¯¸í„°ë¡œ ì¸ì‹í•  수 없는 ê°’ì´ ì œê³µë˜ì—ˆìŠµë‹ˆë‹¤: [{0}] +defaultServlet.xslError=XSL Transformer 오류 + +directory.filename=파ì¼ëª… +directory.lastModified=최종변경시간 +directory.parent=ìƒìœ„ë¡œ: [{0}] +directory.size=í¬ê¸° +directory.title=[{0}]ì„(를) 위한 디렉토리 ëª©ë¡ í‘œì‹œ + +webdavservlet.externalEntityIgnored=요청ì´, PublicIDê°€ [{0}]ì´ê³  SystemIDê°€ [{1}]ì¸ ì™¸ë¶€ ì—”í‹°í‹°ì— ëŒ€í•œ 참조를 í¬í•¨í–ˆëŠ”ë°, ì´ëŠ” 무시ë˜ì—ˆìŠµë‹ˆë‹¤. +webdavservlet.inputstreamclosefail=[{0}]ì˜ ìž…ë ¥ ìŠ¤íŠ¸ë¦¼ì„ ë‹«ì§€ 못했습니다. +webdavservlet.jaxpfailed=JAXPì˜ ì´ˆê¸°í™”ê°€ 실패했습니다. diff --git a/java/org/apache/catalina/servlets/LocalStrings_pt_BR.properties b/java/org/apache/catalina/servlets/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..c314f68 --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_pt_BR.properties @@ -0,0 +1,19 @@ +# 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. + +cgiServlet.expandFail=Falha ao expandir script no path [{0}] para [{1}] +cgiServlet.runHeaderReaderFail=Problemas de E/S ao fechar cabeçalho + +defaultServlet.xslError=Erro ao transformar o XSL diff --git a/java/org/apache/catalina/servlets/LocalStrings_ru.properties b/java/org/apache/catalina/servlets/LocalStrings_ru.properties new file mode 100644 index 0000000..dbdf381 --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_ru.properties @@ -0,0 +1,19 @@ +# 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. + +cgiServlet.expandFail=Ðевозможно развернуть Ñкрипт [{0}] в [{1}] +cgiServlet.runInvalidStatus=Ðеверный ÑÑ‚Ð°Ñ‚ÑƒÑ [{0}] + +defaultServlet.skipfail=Чтение завершилоÑÑŒ ошибкой, потому что только [{0}] байт было доÑтупно, а требовалоÑÑŒ пропуÑтить [{1}] байт, чтобы доÑтигнуть начала требуемоего диапазона diff --git a/java/org/apache/catalina/servlets/LocalStrings_zh_CN.properties b/java/org/apache/catalina/servlets/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..44f53d1 --- /dev/null +++ b/java/org/apache/catalina/servlets/LocalStrings_zh_CN.properties @@ -0,0 +1,56 @@ +# 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. + +cgiServlet.emptyEnvVarName=åˆå§‹åŒ–å‚数中的环境å˜é‡å为空[环境å˜é‡-] +cgiServlet.expandCloseFail=无法关闭路径[{0}]å¤„è„šæœ¬çš„è¾“å…¥æµ +cgiServlet.expandCreateDirFail=无法为脚本扩展创建目标目录[{0}] +cgiServlet.expandDeleteFail=扩展期间,å‘生IOException异常åŽåˆ é™¤æ–‡ä»¶[{0}]失败 +cgiServlet.expandFail=在路径[{0}] 到[{1}] 展开脚本失败. +cgiServlet.expandNotFound=无法展开[{0}],因为找ä¸åˆ°å®ƒã€‚ +cgiServlet.expandOk=从路径[{0}]到[{1}]扩展脚本 +cgiServlet.find.found=找到CGI:name[{0}]ã€path[{1}]ã€script name[{2}]å’ŒCGI name[{3}] +cgiServlet.find.location=在 [{0}] 查找文件 +cgiServlet.find.path=在相对于CGIä½ç½®[{1}]的路径[{0}]处请求的CGI脚本 +cgiServlet.invalidArgumentDecoded=解ç çš„命令行å‚æ•°[{0}]与é…置的cmdLineArgumentsDecoded模å¼[{1}]ä¸åŒ¹é… +cgiServlet.invalidArgumentEncoded=ç¼–ç çš„命令行å‚æ•°[{0}]与é…置的cmdLineArgumentsEncoded模å¼[{1}]ä¸åŒ¹é… +cgiServlet.runBadHeader=标头行[{0}]错误 +cgiServlet.runFail=处ç†CGIå‘生I/O问题 +cgiServlet.runHeaderReaderFail=I/O 问题关闭请求头读æ“作 +cgiServlet.runInvalidStatus=æ— æ•ˆçŠ¶æ€ [{0}] +cgiServlet.runOutputStreamFail=关闭输出æµæ—¶å‘生I/O问题 +cgiServlet.runReaderInterrupt=对于标准错误的读线程,中断并等待。 +cgiServlet.runStdErr=标准行:[{0}]。 +cgiServlet.runStdErrCount=在stderr上收到了[{0}]è¡Œ +cgiServlet.runStdErrFail=I/O标准错误问题 + +defaultServlet.blockExternalEntity=阻止访问publicId[{0}]å’ŒsystemId[{0}]的外部实体。 +defaultServlet.blockExternalEntity2=阻止访问å为[{0}]ã€publicId[{1}]ã€baseURI[{2}]å’ŒsystemId[{3}]的外部实体。 +defaultServlet.blockExternalSubset=用å称[{0}]å’ŒbaseURI[{1}]阻止对外部å­é›†çš„访问 +defaultServlet.missingResource=请求的资æº[{0}]ä¸å¯ç”¨ +defaultServlet.noResources=找ä¸åˆ°é™æ€èµ„æº +defaultServlet.readerCloseFailed=无法关闭读å¡å™¨ +defaultServlet.skipfail=读å–失败,因为åªæœ‰[{0}]个字节å¯ç”¨ï¼Œä½†éœ€è¦è·³è¿‡[{1}]个字节æ‰èƒ½åˆ°è¾¾è¯·æ±‚范围的开始 +defaultServlet.unknownBomConfig=为useBomIfPresentåˆå§‹åŒ–å‚æ•°æ供的[{0}]值无法识别 +defaultServlet.xslError=XSL转æ¢å™¨é”™è¯¯ + +directory.filename=文件å +directory.lastModified=上次修改时间。 +directory.parent=最多[{0}] +directory.size=å¤§å° +directory.title=[{0}]的目录列表 + +webdavservlet.externalEntityIgnored=请求包å«å¯¹PublicID[{0}]å’ŒSystemID[{1}]的外部实体的引用,该引用被忽略。 +webdavservlet.inputstreamclosefail=无法关闭[{0}]çš„è¾“å…¥æµ +webdavservlet.jaxpfailed=JAXP åˆå§‹åŒ–失败 diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java new file mode 100644 index 0000000..1f1aae1 --- /dev/null +++ b/java/org/apache/catalina/servlets/WebdavServlet.java @@ -0,0 +1,2527 @@ +/* + * 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.catalina.servlets; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.WebResource; +import org.apache.catalina.connector.RequestFacade; +import org.apache.catalina.util.DOMWriter; +import org.apache.catalina.util.XMLWriter; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.http.ConcurrentDateFormat; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.RequestUtil; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Servlet which adds support for WebDAV + * level 2. All the basic HTTP requests are handled by the + * DefaultServlet. The WebDAVServlet must not be used as the default servlet (ie mapped to '/') as it will not work in + * this configuration. + *

    + * Mapping a subpath (e.g. /webdav/* to this servlet has the effect of re-mounting the entire web + * application under that sub-path, with WebDAV access to all the resources. The WEB-INF and + * META-INF directories are protected in this re-mounted resource tree. + *

    + * To enable WebDAV for a context add the following to web.xml: + * + *

    + * <servlet>
    + *  <servlet-name>webdav</servlet-name>
    + *  <servlet-class>org.apache.catalina.servlets.WebdavServlet</servlet-class>
    + *    <init-param>
    + *      <param-name>debug</param-name>
    + *      <param-value>0</param-value>
    + *    </init-param>
    + *    <init-param>
    + *      <param-name>listings</param-name>
    + *      <param-value>false</param-value>
    + *    </init-param>
    + *  </servlet>
    + *  <servlet-mapping>
    + *    <servlet-name>webdav</servlet-name>
    + *    <url-pattern>/*</url-pattern>
    + *  </servlet-mapping>
    + * 
    + * + * This will enable read only access. To enable read-write access add: + * + *
    + *  <init-param>
    + *    <param-name>readonly</param-name>
    + *    <param-value>false</param-value>
    + *  </init-param>
    + * 
    + * + * To make the content editable via a different URL, use the following mapping: + * + *
    + *  <servlet-mapping>
    + *    <servlet-name>webdav</servlet-name>
    + *    <url-pattern>/webdavedit/*</url-pattern>
    + *  </servlet-mapping>
    + * 
    + * + * By default access to /WEB-INF and META-INF are not available via WebDAV. To enable access to these URLs, use add: + * + *
    + *  <init-param>
    + *    <param-name>allowSpecialPaths</param-name>
    + *    <param-value>true</param-value>
    + *  </init-param>
    + * 
    + * + * Don't forget to secure access appropriately to the editing URLs, especially if allowSpecialPaths is used. With the + * mapping configuration above, the context will be accessible to normal users as before. Those users with the necessary + * access will be able to edit content available via http://host:port/context/content using + * http://host:port/context/webdavedit/content + * + * @author Remy Maucherat + * + * @see RFC 4918 + */ +public class WebdavServlet extends DefaultServlet { + + private static final long serialVersionUID = 1L; + + + // -------------------------------------------------------------- Constants + + private static final String METHOD_PROPFIND = "PROPFIND"; + private static final String METHOD_PROPPATCH = "PROPPATCH"; + private static final String METHOD_MKCOL = "MKCOL"; + private static final String METHOD_COPY = "COPY"; + private static final String METHOD_MOVE = "MOVE"; + private static final String METHOD_LOCK = "LOCK"; + private static final String METHOD_UNLOCK = "UNLOCK"; + + + /** + * PROPFIND - Specify a property mask. + */ + private static final int FIND_BY_PROPERTY = 0; + + + /** + * PROPFIND - Display all properties. + */ + private static final int FIND_ALL_PROP = 1; + + + /** + * PROPFIND - Return property names. + */ + private static final int FIND_PROPERTY_NAMES = 2; + + + /** + * Create a new lock. + */ + private static final int LOCK_CREATION = 0; + + + /** + * Refresh lock. + */ + private static final int LOCK_REFRESH = 1; + + + /** + * Default lock timeout value. + */ + private static final int DEFAULT_TIMEOUT = 3600; + + + /** + * Maximum lock timeout. + */ + private static final int MAX_TIMEOUT = 604800; + + + /** + * Default namespace. + */ + protected static final String DEFAULT_NAMESPACE = "DAV:"; + + + /** + * Simple date format for the creation date ISO representation (partial). + */ + protected static final ConcurrentDateFormat creationDateFormat = + new ConcurrentDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US, TimeZone.getTimeZone("GMT")); + + + // ----------------------------------------------------- Instance Variables + + /** + * Repository of the locks put on single resources. + *

    + * Key : path
    + * Value : LockInfo + */ + private final Map resourceLocks = new ConcurrentHashMap<>(); + + + /** + * Repository of the lock-null resources. + *

    + * Key : path of the collection containing the lock-null resource
    + * Value : List of lock-null resource which are members of the collection. Each element of the List is the path + * associated with the lock-null resource. + */ + private final Map> lockNullResources = new ConcurrentHashMap<>(); + + + /** + * List of the inheritable collection locks. + */ + private final List collectionLocks = Collections.synchronizedList(new ArrayList<>()); + + + /** + * Secret information used to generate reasonably secure lock ids. + */ + private String secret = "catalina"; + + + /** + * Default depth in spec is infinite. Limit depth to 3 by default as infinite depth makes operations very expensive. + */ + private int maxDepth = 3; + + + /** + * Is access allowed via WebDAV to the special paths (/WEB-INF and /META-INF)? + */ + private boolean allowSpecialPaths = false; + + + // --------------------------------------------------------- Public Methods + + /** + * Initialize this servlet. + */ + @Override + public void init() throws ServletException { + + super.init(); + + if (getServletConfig().getInitParameter("secret") != null) { + secret = getServletConfig().getInitParameter("secret"); + } + + if (getServletConfig().getInitParameter("maxDepth") != null) { + maxDepth = Integer.parseInt(getServletConfig().getInitParameter("maxDepth")); + } + + if (getServletConfig().getInitParameter("allowSpecialPaths") != null) { + allowSpecialPaths = Boolean.parseBoolean(getServletConfig().getInitParameter("allowSpecialPaths")); + } + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Return JAXP document builder instance. + * + * @return the document builder + * + * @throws ServletException document builder creation failed (wrapped ParserConfigurationException + * exception) + */ + protected DocumentBuilder getDocumentBuilder() throws ServletException { + DocumentBuilder documentBuilder = null; + DocumentBuilderFactory documentBuilderFactory = null; + try { + documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + documentBuilderFactory.setExpandEntityReferences(false); + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + documentBuilder.setEntityResolver(new WebdavResolver(this.getServletContext())); + } catch (ParserConfigurationException e) { + throw new ServletException(sm.getString("webdavservlet.jaxpfailed")); + } + return documentBuilder; + } + + + /** + * Handles the special WebDAV methods. + */ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + final String path = getRelativePath(req); + + // Error page check needs to come before special path check since + // custom error pages are often located below WEB-INF so they are + // not directly accessible. + if (req.getDispatcherType() == DispatcherType.ERROR) { + doGet(req, resp); + return; + } + + // Block access to special subdirectories. + // DefaultServlet assumes it services resources from the root of the web app + // and doesn't add any special path protection + // WebdavServlet remounts the webapp under a new path, so this check is + // necessary on all methods (including GET). + if (isSpecialPath(path)) { + resp.sendError(WebdavStatus.SC_NOT_FOUND); + return; + } + + final String method = req.getMethod(); + + if (debug > 0) { + log("[" + method + "] " + path); + } + + if (method.equals(METHOD_PROPFIND)) { + doPropfind(req, resp); + } else if (method.equals(METHOD_PROPPATCH)) { + doProppatch(req, resp); + } else if (method.equals(METHOD_MKCOL)) { + doMkcol(req, resp); + } else if (method.equals(METHOD_COPY)) { + doCopy(req, resp); + } else if (method.equals(METHOD_MOVE)) { + doMove(req, resp); + } else if (method.equals(METHOD_LOCK)) { + doLock(req, resp); + } else if (method.equals(METHOD_UNLOCK)) { + doUnlock(req, resp); + } else { + // DefaultServlet processing + super.service(req, resp); + } + } + + + /** + * Checks whether a given path refers to a resource under WEB-INF or META-INF. + * + * @param path the full path of the resource being accessed + * + * @return true if the resource specified is under a special path + */ + private boolean isSpecialPath(final String path) { + return !allowSpecialPaths && (path.toUpperCase(Locale.ENGLISH).startsWith("/WEB-INF") || + path.toUpperCase(Locale.ENGLISH).startsWith("/META-INF")); + } + + + @Override + protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response, WebResource resource) + throws IOException { + + if (!super.checkIfHeaders(request, response, resource)) { + return false; + } + + // TODO : Checking the WebDAV If header + return true; + } + + + /** + * Override the DefaultServlet implementation and only use the PathInfo. If the ServletPath is non-null, it will be + * because the WebDAV servlet has been mapped to a url other than /* to configure editing at different url than + * normal viewing. + * + * @param request The servlet request we are processing + */ + @Override + protected String getRelativePath(HttpServletRequest request) { + return getRelativePath(request, false); + } + + @Override + protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) { + String pathInfo; + + if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) { + // For includes, get the info from the attributes + pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); + } else { + pathInfo = request.getPathInfo(); + } + + StringBuilder result = new StringBuilder(); + if (pathInfo != null) { + result.append(pathInfo); + } + if (result.length() == 0) { + result.append('/'); + } + + return result.toString(); + } + + + /** + * Determines the prefix for standard directory GET listings. + */ + @Override + protected String getPathPrefix(final HttpServletRequest request) { + // Repeat the servlet path (e.g. /webdav/) in the listing path + String contextPath = request.getContextPath(); + if (request.getServletPath() != null) { + contextPath = contextPath + request.getServletPath(); + } + return contextPath; + } + + + /** + * OPTIONS Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws ServletException If an error occurs + * @throws IOException If an IO error occurs + */ + @Override + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.addHeader("DAV", "1,2"); + resp.addHeader("Allow", determineMethodsAllowed(req)); + resp.addHeader("MS-Author-Via", "DAV"); + } + + + /** + * PROPFIND Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws ServletException If an error occurs + * @throws IOException If an IO error occurs + */ + protected void doPropfind(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (!listings) { + sendNotAllowed(req, resp); + return; + } + + String path = getRelativePath(req); + if (path.length() > 1 && path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + // Properties which are to be displayed. + List properties = null; + // Propfind depth + int depth = maxDepth; + // Propfind type + int type = FIND_ALL_PROP; + + String depthStr = req.getHeader("Depth"); + + if (depthStr == null) { + depth = maxDepth; + } else { + if (depthStr.equals("0")) { + depth = 0; + } else if (depthStr.equals("1")) { + depth = 1; + } else if (depthStr.equals("infinity")) { + depth = maxDepth; + } + } + + Node propNode = null; + + if (req.getContentLengthLong() > 0) { + DocumentBuilder documentBuilder = getDocumentBuilder(); + + try { + Document document = documentBuilder.parse(new InputSource(req.getInputStream())); + + // Get the root element of the document + Element rootElement = document.getDocumentElement(); + NodeList childList = rootElement.getChildNodes(); + + for (int i = 0; i < childList.getLength(); i++) { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + if (currentNode.getNodeName().endsWith("prop")) { + type = FIND_BY_PROPERTY; + propNode = currentNode; + } + if (currentNode.getNodeName().endsWith("propname")) { + type = FIND_PROPERTY_NAMES; + } + if (currentNode.getNodeName().endsWith("allprop")) { + type = FIND_ALL_PROP; + } + break; + } + } + } catch (SAXException | IOException e) { + // Something went wrong - bad request + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return; + } + } + + if (type == FIND_BY_PROPERTY) { + properties = new ArrayList<>(); + // propNode must be non-null if type == FIND_BY_PROPERTY + @SuppressWarnings("null") + NodeList childList = propNode.getChildNodes(); + + for (int i = 0; i < childList.getLength(); i++) { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + String nodeName = currentNode.getNodeName(); + String propertyName = null; + if (nodeName.indexOf(':') != -1) { + propertyName = nodeName.substring(nodeName.indexOf(':') + 1); + } else { + propertyName = nodeName; + } + // href is a live property which is handled differently + properties.add(propertyName); + break; + } + } + } + + WebResource resource = resources.getResource(path); + + if (!resource.exists()) { + int slash = path.lastIndexOf('/'); + if (slash != -1) { + String parentPath = path.substring(0, slash); + List currentLockNullResources = lockNullResources.get(parentPath); + if (currentLockNullResources != null) { + for (String lockNullPath : currentLockNullResources) { + if (lockNullPath.equals(path)) { + resp.setStatus(WebdavStatus.SC_MULTI_STATUS); + resp.setContentType("text/xml; charset=UTF-8"); + // Create multistatus object + XMLWriter generatedXML = new XMLWriter(resp.getWriter()); + generatedXML.writeXMLHeader(); + generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); + parseLockNullProperties(req, generatedXML, lockNullPath, type, properties); + generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); + generatedXML.sendData(); + return; + } + } + } + } + } + + if (!resource.exists()) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + resp.setStatus(WebdavStatus.SC_MULTI_STATUS); + + resp.setContentType("text/xml; charset=UTF-8"); + + // Create multistatus object + XMLWriter generatedXML = new XMLWriter(resp.getWriter()); + generatedXML.writeXMLHeader(); + + generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); + + if (depth == 0) { + parseProperties(req, generatedXML, path, type, properties); + } else { + // The stack always contains the object of the current level + Deque stack = new ArrayDeque<>(); + stack.addFirst(path); + + // Stack of the objects one level below + Deque stackBelow = new ArrayDeque<>(); + + while ((!stack.isEmpty()) && (depth >= 0)) { + + String currentPath = stack.remove(); + parseProperties(req, generatedXML, currentPath, type, properties); + + resource = resources.getResource(currentPath); + + if (resource.isDirectory() && (depth > 0)) { + + String[] entries = resources.list(currentPath); + for (String entry : entries) { + String newPath = currentPath; + if (!(newPath.endsWith("/"))) { + newPath += "/"; + } + newPath += entry; + stackBelow.addFirst(newPath); + } + + // Displaying the lock-null resources present in that + // collection + String lockPath = currentPath; + if (lockPath.endsWith("/")) { + lockPath = lockPath.substring(0, lockPath.length() - 1); + } + List currentLockNullResources = lockNullResources.get(lockPath); + if (currentLockNullResources != null) { + for (String lockNullPath : currentLockNullResources) { + parseLockNullProperties(req, generatedXML, lockNullPath, type, properties); + } + } + } + + if (stack.isEmpty()) { + depth--; + stack = stackBelow; + stackBelow = new ArrayDeque<>(); + } + + generatedXML.sendData(); + + } + } + + generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); + + generatedXML.sendData(); + + } + + + /** + * PROPPATCH Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws IOException If an IO error occurs + */ + protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); + } + + + /** + * MKCOL Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws ServletException If an error occurs + * @throws IOException If an IO error occurs + */ + protected void doMkcol(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String path = getRelativePath(req); + + WebResource resource = resources.getResource(path); + + // Can't create a collection if a resource already exists at the given + // path + if (resource.exists()) { + sendNotAllowed(req, resp); + return; + } + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + if (req.getContentLengthLong() > 0) { + DocumentBuilder documentBuilder = getDocumentBuilder(); + try { + // Document document = + documentBuilder.parse(new InputSource(req.getInputStream())); + // TODO : Process this request body + resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED); + return; + + } catch (SAXException saxe) { + // Parse error - assume invalid content + resp.sendError(WebdavStatus.SC_UNSUPPORTED_MEDIA_TYPE); + return; + } + } + + if (resources.mkdir(path)) { + resp.setStatus(WebdavStatus.SC_CREATED); + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } else { + resp.sendError(WebdavStatus.SC_CONFLICT); + } + } + + + /** + * DELETE Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws ServletException If an error occurs + * @throws IOException If an IO error occurs + */ + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (readOnly) { + sendNotAllowed(req, resp); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + deleteResource(req, resp); + } + + + /** + * Process a PUT request for the specified resource. + * + * @param req The servlet request we are processing + * @param resp The servlet response we are creating + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet-specified error occurs + */ + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + String path = getRelativePath(req); + WebResource resource = resources.getResource(path); + if (resource.isDirectory()) { + sendNotAllowed(req, resp); + return; + } + + super.doPut(req, resp); + + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } + + + /** + * COPY Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws IOException If an IO error occurs + */ + protected void doCopy(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + copyResource(req, resp); + } + + + /** + * MOVE Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws IOException If an IO error occurs + */ + protected void doMove(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + String path = getRelativePath(req); + + if (copyResource(req, resp)) { + deleteResource(path, req, resp, false); + } + } + + + /** + * LOCK Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws ServletException If an error occurs + * @throws IOException If an IO error occurs + */ + protected void doLock(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + LockInfo lock = new LockInfo(maxDepth); + + // Parsing lock request + + // Parsing depth header + + String depthStr = req.getHeader("Depth"); + + if (depthStr == null) { + lock.depth = maxDepth; + } else { + if (depthStr.equals("0")) { + lock.depth = 0; + } else { + lock.depth = maxDepth; + } + } + + // Parsing timeout header + + int lockDuration = DEFAULT_TIMEOUT; + String lockDurationStr = req.getHeader("Timeout"); + if (lockDurationStr != null) { + int commaPos = lockDurationStr.indexOf(','); + // If multiple timeouts, just use the first + if (commaPos != -1) { + lockDurationStr = lockDurationStr.substring(0, commaPos); + } + if (lockDurationStr.startsWith("Second-")) { + lockDuration = Integer.parseInt(lockDurationStr.substring(7)); + } else { + if (lockDurationStr.equalsIgnoreCase("infinity")) { + lockDuration = MAX_TIMEOUT; + } else { + try { + lockDuration = Integer.parseInt(lockDurationStr); + } catch (NumberFormatException e) { + lockDuration = MAX_TIMEOUT; + } + } + } + if (lockDuration == 0) { + lockDuration = DEFAULT_TIMEOUT; + } + if (lockDuration > MAX_TIMEOUT) { + lockDuration = MAX_TIMEOUT; + } + } + lock.expiresAt = System.currentTimeMillis() + (lockDuration * 1000); + + int lockRequestType = LOCK_CREATION; + + Node lockInfoNode = null; + + DocumentBuilder documentBuilder = getDocumentBuilder(); + + try { + Document document = documentBuilder.parse(new InputSource(req.getInputStream())); + + // Get the root element of the document + Element rootElement = document.getDocumentElement(); + lockInfoNode = rootElement; + } catch (IOException | SAXException e) { + lockRequestType = LOCK_REFRESH; + } + + if (lockInfoNode != null) { + + // Reading lock information + + NodeList childList = lockInfoNode.getChildNodes(); + StringWriter strWriter = null; + DOMWriter domWriter = null; + + Node lockScopeNode = null; + Node lockTypeNode = null; + Node lockOwnerNode = null; + + for (int i = 0; i < childList.getLength(); i++) { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + String nodeName = currentNode.getNodeName(); + if (nodeName.endsWith("lockscope")) { + lockScopeNode = currentNode; + } + if (nodeName.endsWith("locktype")) { + lockTypeNode = currentNode; + } + if (nodeName.endsWith("owner")) { + lockOwnerNode = currentNode; + } + break; + } + } + + if (lockScopeNode != null) { + + childList = lockScopeNode.getChildNodes(); + for (int i = 0; i < childList.getLength(); i++) { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + String tempScope = currentNode.getNodeName(); + if (tempScope.indexOf(':') != -1) { + lock.scope = tempScope.substring(tempScope.indexOf(':') + 1); + } else { + lock.scope = tempScope; + } + break; + } + } + + if (lock.scope == null) { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + } else { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + if (lockTypeNode != null) { + + childList = lockTypeNode.getChildNodes(); + for (int i = 0; i < childList.getLength(); i++) { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + String tempType = currentNode.getNodeName(); + if (tempType.indexOf(':') != -1) { + lock.type = tempType.substring(tempType.indexOf(':') + 1); + } else { + lock.type = tempType; + } + break; + } + } + + if (lock.type == null) { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + } else { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + if (lockOwnerNode != null) { + + childList = lockOwnerNode.getChildNodes(); + for (int i = 0; i < childList.getLength(); i++) { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) { + case Node.TEXT_NODE: + lock.owner += currentNode.getNodeValue(); + break; + case Node.ELEMENT_NODE: + strWriter = new StringWriter(); + domWriter = new DOMWriter(strWriter); + domWriter.print(currentNode); + lock.owner += strWriter.toString(); + break; + } + } + + if (lock.owner == null) { + // Bad request + resp.setStatus(WebdavStatus.SC_BAD_REQUEST); + } + + } else { + lock.owner = ""; + } + } + + String path = getRelativePath(req); + + lock.path = path; + + WebResource resource = resources.getResource(path); + + if (lockRequestType == LOCK_CREATION) { + + // Generating lock id + String lockTokenStr = req.getServletPath() + "-" + lock.type + "-" + lock.scope + "-" + + req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.tokens + "-" + + lock.expiresAt + "-" + System.currentTimeMillis() + "-" + secret; + String lockToken = HexUtils + .toHexString(ConcurrentMessageDigest.digestMD5(lockTokenStr.getBytes(StandardCharsets.ISO_8859_1))); + + if (resource.isDirectory() && lock.depth == maxDepth) { + + // Locking a collection (and all its member resources) + + // Checking if a child resource of this collection is + // already locked + List lockPaths = new ArrayList<>(); + Iterator collectionLocksIterator = collectionLocks.iterator(); + while (collectionLocksIterator.hasNext()) { + LockInfo currentLock = collectionLocksIterator.next(); + if (currentLock.hasExpired()) { + collectionLocksIterator.remove(); + continue; + } + if (currentLock.path.startsWith(lock.path) && (currentLock.isExclusive() || lock.isExclusive())) { + // A child collection of this collection is locked + lockPaths.add(currentLock.path); + } + } + for (LockInfo currentLock : resourceLocks.values()) { + if (currentLock.hasExpired()) { + resourceLocks.remove(currentLock.path); + continue; + } + if (currentLock.path.startsWith(lock.path) && (currentLock.isExclusive() || lock.isExclusive())) { + // A child resource of this collection is locked + lockPaths.add(currentLock.path); + } + } + + if (!lockPaths.isEmpty()) { + + // One of the child paths was locked + // We generate a multistatus error report + + resp.setStatus(WebdavStatus.SC_CONFLICT); + + XMLWriter generatedXML = new XMLWriter(); + generatedXML.writeXMLHeader(); + + generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); + + for (String lockPath : lockPaths) { + generatedXML.writeElement("D", "response", XMLWriter.OPENING); + generatedXML.writeElement("D", "href", XMLWriter.OPENING); + generatedXML.writeText(lockPath); + generatedXML.writeElement("D", "href", XMLWriter.CLOSING); + generatedXML.writeElement("D", "status", XMLWriter.OPENING); + generatedXML.writeText("HTTP/1.1 " + WebdavStatus.SC_LOCKED + " "); + generatedXML.writeElement("D", "status", XMLWriter.CLOSING); + generatedXML.writeElement("D", "response", XMLWriter.CLOSING); + } + + generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); + + Writer writer = resp.getWriter(); + writer.write(generatedXML.toString()); + writer.close(); + + return; + } + + boolean addLock = true; + + // Checking if there is already a shared lock on this path + for (LockInfo currentLock : collectionLocks) { + if (currentLock.path.equals(lock.path)) { + + if (currentLock.isExclusive()) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } else { + if (lock.isExclusive()) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + } + + currentLock.tokens.add(lockToken); + lock = currentLock; + addLock = false; + } + } + + if (addLock) { + lock.tokens.add(lockToken); + collectionLocks.add(lock); + } + + } else { + + // Locking a single resource + + // Retrieving an already existing lock on that resource + LockInfo presentLock = resourceLocks.get(lock.path); + if (presentLock != null) { + + if ((presentLock.isExclusive()) || (lock.isExclusive())) { + // If either lock is exclusive, the lock can't be + // granted + resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); + return; + } else { + presentLock.tokens.add(lockToken); + lock = presentLock; + } + + } else { + + lock.tokens.add(lockToken); + resourceLocks.put(lock.path, lock); + + // Checking if a resource exists at this path + if (!resource.exists()) { + + // "Creating" a lock-null resource + int slash = lock.path.lastIndexOf('/'); + String parentPath = lock.path.substring(0, slash); + + lockNullResources.computeIfAbsent(parentPath, k -> new ArrayList<>()).add(lock.path); + } + + // Add the Lock-Token header as by RFC 2518 8.10.1 + // - only do this for newly created locks + resp.addHeader("Lock-Token", ""); + } + } + } + + if (lockRequestType == LOCK_REFRESH) { + + String ifHeader = req.getHeader("If"); + if (ifHeader == null) { + ifHeader = ""; + } + + // Checking resource locks + + LockInfo toRenew = resourceLocks.get(path); + + if (toRenew != null) { + // At least one of the tokens of the locks must have been given + for (String token : toRenew.tokens) { + if (ifHeader.contains(token)) { + toRenew.expiresAt = lock.expiresAt; + lock = toRenew; + } + } + } + + // Checking inheritable collection locks + for (LockInfo collecionLock : collectionLocks) { + if (path.equals(collecionLock.path)) { + for (String token : collecionLock.tokens) { + if (ifHeader.contains(token)) { + collecionLock.expiresAt = lock.expiresAt; + lock = collecionLock; + } + } + } + } + } + + // Set the status, then generate the XML response containing + // the lock information + XMLWriter generatedXML = new XMLWriter(); + generatedXML.writeXMLHeader(); + generatedXML.writeElement("D", DEFAULT_NAMESPACE, "prop", XMLWriter.OPENING); + + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); + + lock.toXML(generatedXML); + + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); + + resp.setStatus(WebdavStatus.SC_OK); + resp.setContentType("text/xml; charset=UTF-8"); + Writer writer = resp.getWriter(); + writer.write(generatedXML.toString()); + writer.close(); + } + + + /** + * UNLOCK Method. + * + * @param req The Servlet request + * @param resp The Servlet response + * + * @throws IOException If an IO error occurs + */ + protected void doUnlock(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (isLocked(req)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return; + } + + String path = getRelativePath(req); + + String lockTokenHeader = req.getHeader("Lock-Token"); + if (lockTokenHeader == null) { + lockTokenHeader = ""; + } + + // Checking resource locks + + LockInfo lock = resourceLocks.get(path); + if (lock != null) { + + // At least one of the tokens of the locks must have been given + Iterator tokenList = lock.tokens.iterator(); + while (tokenList.hasNext()) { + String token = tokenList.next(); + if (lockTokenHeader.contains(token)) { + tokenList.remove(); + } + } + + if (lock.tokens.isEmpty()) { + resourceLocks.remove(path); + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } + + } + + // Checking inheritable collection locks + Iterator collectionLocksList = collectionLocks.iterator(); + while (collectionLocksList.hasNext()) { + lock = collectionLocksList.next(); + if (path.equals(lock.path)) { + Iterator tokenList = lock.tokens.iterator(); + while (tokenList.hasNext()) { + String token = tokenList.next(); + if (lockTokenHeader.contains(token)) { + tokenList.remove(); + break; + } + } + if (lock.tokens.isEmpty()) { + collectionLocksList.remove(); + // Removing any lock-null resource which would be present + lockNullResources.remove(path); + } + } + } + + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + } + + + // -------------------------------------------------------- Private Methods + + /** + * Check to see if a resource is currently write locked. The method will look at the "If" header to make sure the + * client has give the appropriate lock tokens. + * + * @param req Servlet request + * + * @return true if the resource is locked (and no appropriate lock token has been found for at least + * one of the non-shared locks which are present on the resource). + */ + private boolean isLocked(HttpServletRequest req) { + + String path = getRelativePath(req); + + String ifHeader = req.getHeader("If"); + if (ifHeader == null) { + ifHeader = ""; + } + + String lockTokenHeader = req.getHeader("Lock-Token"); + if (lockTokenHeader == null) { + lockTokenHeader = ""; + } + + return isLocked(path, ifHeader + lockTokenHeader); + } + + + /** + * Check to see if a resource is currently write locked. + * + * @param path Path of the resource + * @param ifHeader "If" HTTP header which was included in the request + * + * @return true if the resource is locked (and no appropriate lock token has been found for at least + * one of the non-shared locks which are present on the resource). + */ + private boolean isLocked(String path, String ifHeader) { + + // Checking resource locks + + LockInfo lock = resourceLocks.get(path); + if ((lock != null) && (lock.hasExpired())) { + resourceLocks.remove(path); + } else if (lock != null) { + + // At least one of the tokens of the locks must have been given + + boolean tokenMatch = false; + for (String token : lock.tokens) { + if (ifHeader.contains(token)) { + tokenMatch = true; + break; + } + } + if (!tokenMatch) { + return true; + } + } + + // Checking inheritable collection locks + Iterator collectionLockList = collectionLocks.iterator(); + while (collectionLockList.hasNext()) { + lock = collectionLockList.next(); + if (lock.hasExpired()) { + collectionLockList.remove(); + } else if (path.startsWith(lock.path)) { + boolean tokenMatch = false; + for (String token : lock.tokens) { + if (ifHeader.contains(token)) { + tokenMatch = true; + break; + } + } + if (!tokenMatch) { + return true; + } + } + } + + return false; + } + + + /** + * Copy a resource. + * + * @param req Servlet request + * @param resp Servlet response + * + * @return boolean true if the copy is successful + * + * @throws IOException If an IO error occurs + */ + private boolean copyResource(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + // Check the source exists + String path = getRelativePath(req); + WebResource source = resources.getResource(path); + if (!source.exists()) { + resp.sendError(WebdavStatus.SC_NOT_FOUND); + return false; + } + + // Parsing destination header + // See RFC 4918 + String destinationHeader = req.getHeader("Destination"); + + if (destinationHeader == null || destinationHeader.isEmpty()) { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return false; + } + + URI destinationUri; + try { + destinationUri = new URI(destinationHeader); + } catch (URISyntaxException e) { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return false; + } + + String destinationPath = destinationUri.getPath(); + + // Destination isn't allowed to use '.' or '..' segments + if (!destinationPath.equals(RequestUtil.normalize(destinationPath))) { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return false; + } + + if (destinationUri.isAbsolute()) { + // Scheme and host need to match + if (!req.getScheme().equals(destinationUri.getScheme()) || + !req.getServerName().equals(destinationUri.getHost())) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + // Port needs to match too but handled separately as the logic is a + // little more complicated + if (req.getServerPort() != destinationUri.getPort()) { + if (destinationUri.getPort() == -1 && ("http".equals(req.getScheme()) && req.getServerPort() == 80 || + "https".equals(req.getScheme()) && req.getServerPort() == 443)) { + // All good. + } else { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + } + } + + // Cross-context operations aren't supported + String reqContextPath = req.getContextPath(); + if (!destinationPath.startsWith(reqContextPath + "/")) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + // Remove context path & servlet path + destinationPath = destinationPath.substring(reqContextPath.length() + req.getServletPath().length()); + + if (debug > 0) { + log("Dest path :" + destinationPath); + } + + // Check destination path to protect special subdirectories + if (isSpecialPath(destinationPath)) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + if (destinationPath.equals(path)) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + // Check src / dest are not sub-dirs of each other + if (destinationPath.startsWith(path) && destinationPath.charAt(path.length()) == '/' || + path.startsWith(destinationPath) && path.charAt(destinationPath.length()) == '/') { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + boolean overwrite = true; + String overwriteHeader = req.getHeader("Overwrite"); + if (overwriteHeader != null) { + if (overwriteHeader.equalsIgnoreCase("T")) { + overwrite = true; + } else { + overwrite = false; + } + } + + // Overwriting the destination + WebResource destination = resources.getResource(destinationPath); + if (overwrite) { + // Delete destination resource, if it exists + if (destination.exists()) { + if (!deleteResource(destinationPath, req, resp, true)) { + return false; + } + } else { + resp.setStatus(WebdavStatus.SC_CREATED); + } + } else { + // If the destination exists, then it's a conflict + if (destination.exists()) { + resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); + return false; + } + } + + // Copying source to destination + + Map errorList = new HashMap<>(); + + boolean result = copyResource(errorList, path, destinationPath); + + if ((!result) || (!errorList.isEmpty())) { + if (errorList.size() == 1) { + resp.sendError(errorList.values().iterator().next().intValue()); + } else { + sendReport(req, resp, errorList); + } + return false; + } + + // Copy was successful + if (destination.exists()) { + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + } else { + resp.setStatus(WebdavStatus.SC_CREATED); + } + + // Removing any lock-null resource which would be present at + // the destination path + lockNullResources.remove(destinationPath); + + return true; + } + + + /** + * Copy a collection. + * + * @param errorList Map containing the list of errors which occurred during the copy operation + * @param source Path of the resource to be copied + * @param dest Destination path + * + * @return true if the copy was successful + */ + private boolean copyResource(Map errorList, String source, String dest) { + + if (debug > 1) { + log("Copy: " + source + " To: " + dest); + } + + WebResource sourceResource = resources.getResource(source); + + if (sourceResource.isDirectory()) { + if (!resources.mkdir(dest)) { + WebResource destResource = resources.getResource(dest); + if (!destResource.isDirectory()) { + errorList.put(dest, Integer.valueOf(WebdavStatus.SC_CONFLICT)); + return false; + } + } + + String[] entries = resources.list(source); + for (String entry : entries) { + String childDest = dest; + if (!childDest.equals("/")) { + childDest += "/"; + } + childDest += entry; + String childSrc = source; + if (!childSrc.equals("/")) { + childSrc += "/"; + } + childSrc += entry; + copyResource(errorList, childSrc, childDest); + } + } else if (sourceResource.isFile()) { + WebResource destResource = resources.getResource(dest); + if (!destResource.exists() && !destResource.getWebappPath().endsWith("/")) { + int lastSlash = destResource.getWebappPath().lastIndexOf('/'); + if (lastSlash > 0) { + String parent = destResource.getWebappPath().substring(0, lastSlash); + WebResource parentResource = resources.getResource(parent); + if (!parentResource.isDirectory()) { + errorList.put(source, Integer.valueOf(WebdavStatus.SC_CONFLICT)); + return false; + } + } + } + // WebDAV Litmus test attempts to copy/move a file over a collection + // Need to remove trailing / from destination to enable test to pass + if (!destResource.exists() && dest.endsWith("/") && dest.length() > 1) { + // Convert destination name from collection (with trailing '/') + // to file (without trailing '/') + dest = dest.substring(0, dest.length() - 1); + } + try (InputStream is = sourceResource.getInputStream()) { + if (!resources.write(dest, is, false)) { + errorList.put(source, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + return false; + } + } catch (IOException e) { + log(sm.getString("webdavservlet.inputstreamclosefail", source), e); + } + } else { + errorList.put(source, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + return false; + } + return true; + } + + + /** + * Delete a resource. + * + * @param req Servlet request + * @param resp Servlet response + * + * @return true if the delete is successful + * + * @throws IOException If an IO error occurs + */ + private boolean deleteResource(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String path = getRelativePath(req); + return deleteResource(path, req, resp, true); + } + + + /** + * Delete a resource. + * + * @param path Path of the resource which is to be deleted + * @param req Servlet request + * @param resp Servlet response + * @param setStatus Should the response status be set on successful completion + * + * @return true if the delete is successful + * + * @throws IOException If an IO error occurs + */ + private boolean deleteResource(String path, HttpServletRequest req, HttpServletResponse resp, boolean setStatus) + throws IOException { + + String ifHeader = req.getHeader("If"); + if (ifHeader == null) { + ifHeader = ""; + } + + String lockTokenHeader = req.getHeader("Lock-Token"); + if (lockTokenHeader == null) { + lockTokenHeader = ""; + } + + if (isLocked(path, ifHeader + lockTokenHeader)) { + resp.sendError(WebdavStatus.SC_LOCKED); + return false; + } + + WebResource resource = resources.getResource(path); + + if (!resource.exists()) { + resp.sendError(WebdavStatus.SC_NOT_FOUND); + return false; + } + + if (!resource.isDirectory()) { + if (!resource.delete()) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + return false; + } + } else { + + Map errorList = new HashMap<>(); + + deleteCollection(req, path, errorList); + if (!resource.delete()) { + errorList.put(path, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + } + + if (!errorList.isEmpty()) { + sendReport(req, resp, errorList); + return false; + } + } + if (setStatus) { + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + } + return true; + } + + + /** + * Deletes a collection. + * + * @param req The Servlet request + * @param path Path to the collection to be deleted + * @param errorList Contains the list of the errors which occurred + */ + private void deleteCollection(HttpServletRequest req, String path, Map errorList) { + + if (debug > 1) { + log("Delete:" + path); + } + + // Prevent deletion of special subdirectories + if (isSpecialPath(path)) { + errorList.put(path, Integer.valueOf(WebdavStatus.SC_FORBIDDEN)); + return; + } + + String ifHeader = req.getHeader("If"); + if (ifHeader == null) { + ifHeader = ""; + } + + String lockTokenHeader = req.getHeader("Lock-Token"); + if (lockTokenHeader == null) { + lockTokenHeader = ""; + } + + String[] entries = resources.list(path); + + for (String entry : entries) { + String childName = path; + if (!childName.equals("/")) { + childName += "/"; + } + childName += entry; + + if (isLocked(childName, ifHeader + lockTokenHeader)) { + + errorList.put(childName, Integer.valueOf(WebdavStatus.SC_LOCKED)); + + } else { + WebResource childResource = resources.getResource(childName); + if (childResource.isDirectory()) { + deleteCollection(req, childName, errorList); + } + + if (!childResource.delete()) { + if (!childResource.isDirectory()) { + // If it's not a collection, then it's an unknown + // error + errorList.put(childName, Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + } + } + } + } + } + + + /** + * Send a multistatus element containing a complete error report to the client. + * + * @param req Servlet request + * @param resp Servlet response + * @param errorList List of error to be displayed + * + * @throws IOException If an IO error occurs + */ + private void sendReport(HttpServletRequest req, HttpServletResponse resp, Map errorList) + throws IOException { + + resp.setStatus(WebdavStatus.SC_MULTI_STATUS); + + XMLWriter generatedXML = new XMLWriter(); + generatedXML.writeXMLHeader(); + + generatedXML.writeElement("D", DEFAULT_NAMESPACE, "multistatus", XMLWriter.OPENING); + + for (Map.Entry errorEntry : errorList.entrySet()) { + String errorPath = errorEntry.getKey(); + int errorCode = errorEntry.getValue().intValue(); + + generatedXML.writeElement("D", "response", XMLWriter.OPENING); + + generatedXML.writeElement("D", "href", XMLWriter.OPENING); + generatedXML.writeText(getServletContext().getContextPath() + errorPath); + generatedXML.writeElement("D", "href", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "status", XMLWriter.OPENING); + generatedXML.writeText("HTTP/1.1 " + errorCode + " "); + generatedXML.writeElement("D", "status", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "response", XMLWriter.CLOSING); + } + + generatedXML.writeElement("D", "multistatus", XMLWriter.CLOSING); + + Writer writer = resp.getWriter(); + writer.write(generatedXML.toString()); + writer.close(); + } + + + /** + * Propfind helper method. + * + * @param req The servlet request + * @param generatedXML XML response to the Propfind request + * @param path Path of the current resource + * @param type Propfind type + * @param properties If the propfind type is find properties by name, then this List contains those properties + */ + private void parseProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, + List properties) { + + // Exclude any resource in the /WEB-INF and /META-INF subdirectories + if (isSpecialPath(path)) { + return; + } + + WebResource resource = resources.getResource(path); + if (!resource.exists()) { + // File is in directory listing but doesn't appear to exist + // Broken symlink or odd permission settings? + return; + } + + String href = req.getContextPath() + req.getServletPath(); + if ((href.endsWith("/")) && (path.startsWith("/"))) { + href += path.substring(1); + } else { + href += path; + } + if (resource.isDirectory() && (!href.endsWith("/"))) { + href += "/"; + } + + String rewrittenUrl = rewriteUrl(href); + + generatePropFindResponse(generatedXML, rewrittenUrl, path, type, properties, resource.isFile(), false, + resource.getCreation(), resource.getLastModified(), resource.getContentLength(), + getServletContext().getMimeType(resource.getName()), generateETag(resource)); + } + + + /** + * Propfind helper method. Displays the properties of a lock-null resource. + * + * @param req The servlet request + * @param generatedXML XML response to the Propfind request + * @param path Path of the current resource + * @param type Propfind type + * @param properties If the propfind type is find properties by name, then this List contains those properties + */ + private void parseLockNullProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, + List properties) { + + // Exclude any resource in the /WEB-INF and /META-INF subdirectories + if (isSpecialPath(path)) { + return; + } + + // Retrieving the lock associated with the lock-null resource + LockInfo lock = resourceLocks.get(path); + + if (lock == null) { + return; + } + + String absoluteUri = req.getRequestURI(); + String relativePath = getRelativePath(req); + String toAppend = path.substring(relativePath.length()); + if (!toAppend.startsWith("/")) { + toAppend = "/" + toAppend; + } + + String rewrittenUrl = rewriteUrl(RequestUtil.normalize(absoluteUri + toAppend)); + + generatePropFindResponse(generatedXML, rewrittenUrl, path, type, properties, true, true, + lock.creationDate.getTime(), lock.creationDate.getTime(), 0, "", ""); + } + + + private void generatePropFindResponse(XMLWriter generatedXML, String rewrittenUrl, String path, int propFindType, + List properties, boolean isFile, boolean isLockNull, long created, long lastModified, + long contentLength, String contentType, String eTag) { + + generatedXML.writeElement("D", "response", XMLWriter.OPENING); + String status = "HTTP/1.1 " + WebdavStatus.SC_OK + " "; + + // Generating href element + generatedXML.writeElement("D", "href", XMLWriter.OPENING); + generatedXML.writeText(rewrittenUrl); + generatedXML.writeElement("D", "href", XMLWriter.CLOSING); + + String resourceName = path; + int lastSlash = path.lastIndexOf('/'); + if (lastSlash != -1) { + resourceName = resourceName.substring(lastSlash + 1); + } + + switch (propFindType) { + + case FIND_ALL_PROP: + + generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); + generatedXML.writeElement("D", "prop", XMLWriter.OPENING); + + generatedXML.writeProperty("D", "creationdate", getISOCreationDate(created)); + generatedXML.writeElement("D", "displayname", XMLWriter.OPENING); + generatedXML.writeData(resourceName); + generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING); + if (isFile) { + generatedXML.writeProperty("D", "getlastmodified", FastHttpDateFormat.formatDate(lastModified)); + generatedXML.writeProperty("D", "getcontentlength", Long.toString(contentLength)); + if (contentType != null) { + generatedXML.writeProperty("D", "getcontenttype", contentType); + } + generatedXML.writeProperty("D", "getetag", eTag); + if (isLockNull) { + generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); + generatedXML.writeElement("D", "lock-null", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); + } else { + generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT); + } + } else { + generatedXML.writeProperty("D", "getlastmodified", FastHttpDateFormat.formatDate(lastModified)); + generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); + generatedXML.writeElement("D", "collection", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); + } + + generatedXML.writeProperty("D", "source", ""); + + String supportedLocks = "" + "" + + "" + "" + "" + + "" + "" + + ""; + generatedXML.writeElement("D", "supportedlock", XMLWriter.OPENING); + generatedXML.writeRaw(supportedLocks); + generatedXML.writeElement("D", "supportedlock", XMLWriter.CLOSING); + + generateLockDiscovery(path, generatedXML); + + generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); + generatedXML.writeElement("D", "status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("D", "status", XMLWriter.CLOSING); + generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); + + break; + + case FIND_PROPERTY_NAMES: + + generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); + generatedXML.writeElement("D", "prop", XMLWriter.OPENING); + + generatedXML.writeElement("D", "creationdate", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "displayname", XMLWriter.NO_CONTENT); + if (isFile) { + generatedXML.writeElement("D", "getcontentlanguage", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "getcontentlength", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "getcontenttype", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "getetag", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "getlastmodified", XMLWriter.NO_CONTENT); + } + generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "source", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.NO_CONTENT); + + generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); + generatedXML.writeElement("D", "status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("D", "status", XMLWriter.CLOSING); + generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); + + break; + + case FIND_BY_PROPERTY: + + List propertiesNotFound = new ArrayList<>(); + + // Parse the list of properties + + generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); + generatedXML.writeElement("D", "prop", XMLWriter.OPENING); + + for (String property : properties) { + if (property.equals("creationdate")) { + generatedXML.writeProperty("D", "creationdate", getISOCreationDate(created)); + } else if (property.equals("displayname")) { + generatedXML.writeElement("D", "displayname", XMLWriter.OPENING); + generatedXML.writeData(resourceName); + generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING); + } else if (property.equals("getcontentlanguage")) { + if (isFile) { + generatedXML.writeElement("D", "getcontentlanguage", XMLWriter.NO_CONTENT); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("getcontentlength")) { + if (isFile) { + generatedXML.writeProperty("D", "getcontentlength", Long.toString(contentLength)); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("getcontenttype")) { + if (isFile) { + generatedXML.writeProperty("D", "getcontenttype", contentType); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("getetag")) { + if (isFile) { + generatedXML.writeProperty("D", "getetag", eTag); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("getlastmodified")) { + if (isFile) { + generatedXML.writeProperty("D", "getlastmodified", + FastHttpDateFormat.formatDate(lastModified)); + } else { + propertiesNotFound.add(property); + } + } else if (property.equals("resourcetype")) { + if (isFile) { + if (isLockNull) { + generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); + generatedXML.writeElement("D", "lock-null", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); + } else { + generatedXML.writeElement("D", "resourcetype", XMLWriter.NO_CONTENT); + } + } else { + generatedXML.writeElement("D", "resourcetype", XMLWriter.OPENING); + generatedXML.writeElement("D", "collection", XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "resourcetype", XMLWriter.CLOSING); + } + } else if (property.equals("source")) { + generatedXML.writeProperty("D", "source", ""); + } else if (property.equals("supportedlock")) { + supportedLocks = "" + "" + + "" + "" + "" + + "" + "" + + ""; + generatedXML.writeElement("D", "supportedlock", XMLWriter.OPENING); + generatedXML.writeRaw(supportedLocks); + generatedXML.writeElement("D", "supportedlock", XMLWriter.CLOSING); + } else if (property.equals("lockdiscovery")) { + if (!generateLockDiscovery(path, generatedXML)) { + propertiesNotFound.add(property); + } + } else { + propertiesNotFound.add(property); + } + } + + generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); + generatedXML.writeElement("D", "status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("D", "status", XMLWriter.CLOSING); + generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); + + if (!propertiesNotFound.isEmpty()) { + + status = "HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + " "; + + generatedXML.writeElement("D", "propstat", XMLWriter.OPENING); + generatedXML.writeElement("D", "prop", XMLWriter.OPENING); + + for (String propertyNotFound : propertiesNotFound) { + generatedXML.writeElement("D", propertyNotFound, XMLWriter.NO_CONTENT); + } + + generatedXML.writeElement("D", "prop", XMLWriter.CLOSING); + generatedXML.writeElement("D", "status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("D", "status", XMLWriter.CLOSING); + generatedXML.writeElement("D", "propstat", XMLWriter.CLOSING); + } + + break; + } + + generatedXML.writeElement("D", "response", XMLWriter.CLOSING); + } + + + /** + * Print the lock discovery information associated with a path. + * + * @param path Path + * @param generatedXML XML data to which the locks info will be appended + * + * @return true if at least one lock was displayed + */ + private boolean generateLockDiscovery(String path, XMLWriter generatedXML) { + + LockInfo resourceLock = resourceLocks.get(path); + + boolean wroteStart = false; + + if (resourceLock != null) { + wroteStart = true; + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); + resourceLock.toXML(generatedXML); + } + + for (LockInfo currentLock : collectionLocks) { + if (path.startsWith(currentLock.path)) { + if (!wroteStart) { + wroteStart = true; + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.OPENING); + } + currentLock.toXML(generatedXML); + } + } + + if (wroteStart) { + generatedXML.writeElement("D", "lockdiscovery", XMLWriter.CLOSING); + } else { + return false; + } + + return true; + } + + + /** + * Get creation date in ISO format. + * + * @return the formatted creation date + */ + private String getISOCreationDate(long creationDate) { + return creationDateFormat.format(new Date(creationDate)); + } + + + /** + * Determines the methods normally allowed for the resource. + * + * @param req The Servlet request + * + * @return The allowed HTTP methods + */ + @Override + protected String determineMethodsAllowed(HttpServletRequest req) { + + WebResource resource = resources.getResource(getRelativePath(req)); + + // These methods are always allowed. They may return a 404 (not a 405) + // if the resource does not exist. + StringBuilder methodsAllowed = new StringBuilder("OPTIONS, GET, POST, HEAD"); + + if (!readOnly) { + methodsAllowed.append(", DELETE"); + if (!resource.isDirectory()) { + methodsAllowed.append(", PUT"); + } + } + + // Trace - assume disabled unless we can prove otherwise + if (req instanceof RequestFacade && ((RequestFacade) req).getAllowTrace()) { + methodsAllowed.append(", TRACE"); + } + + methodsAllowed.append(", LOCK, UNLOCK, PROPPATCH, COPY, MOVE"); + + if (listings) { + methodsAllowed.append(", PROPFIND"); + } + + if (!resource.exists()) { + methodsAllowed.append(", MKCOL"); + } + + return methodsAllowed.toString(); + } + + + // -------------------------------------------------- LockInfo Inner Class + + /** + * Holds a lock information. + */ + private static class LockInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + LockInfo(int maxDepth) { + this.maxDepth = maxDepth; + } + + + // ------------------------------------------------- Instance Variables + + private final int maxDepth; + + String path = "/"; + String type = "write"; + String scope = "exclusive"; + int depth = 0; + String owner = ""; + List tokens = Collections.synchronizedList(new ArrayList<>()); + long expiresAt = 0; + Date creationDate = new Date(); + + + // ----------------------------------------------------- Public Methods + + /** + * Get a String representation of this lock token. + */ + @Override + public String toString() { + + StringBuilder result = new StringBuilder("Type:"); + result.append(type); + result.append("\nScope:"); + result.append(scope); + result.append("\nDepth:"); + result.append(depth); + result.append("\nOwner:"); + result.append(owner); + result.append("\nExpiration:"); + result.append(FastHttpDateFormat.formatDate(expiresAt)); + for (String token : tokens) { + result.append("\nToken:"); + result.append(token); + } + result.append("\n"); + return result.toString(); + } + + + /** + * @return true if the lock has expired. + */ + public boolean hasExpired() { + return System.currentTimeMillis() > expiresAt; + } + + + /** + * @return true if the lock is exclusive. + */ + public boolean isExclusive() { + return scope.equals("exclusive"); + } + + + /** + * Get an XML representation of this lock token. + * + * @param generatedXML The XML write to which the fragment will be appended + */ + public void toXML(XMLWriter generatedXML) { + + generatedXML.writeElement("D", "activelock", XMLWriter.OPENING); + + generatedXML.writeElement("D", "locktype", XMLWriter.OPENING); + generatedXML.writeElement("D", type, XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "locktype", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "lockscope", XMLWriter.OPENING); + generatedXML.writeElement("D", scope, XMLWriter.NO_CONTENT); + generatedXML.writeElement("D", "lockscope", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "depth", XMLWriter.OPENING); + if (depth == maxDepth) { + generatedXML.writeText("Infinity"); + } else { + generatedXML.writeText("0"); + } + generatedXML.writeElement("D", "depth", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "owner", XMLWriter.OPENING); + generatedXML.writeText(owner); + generatedXML.writeElement("D", "owner", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "timeout", XMLWriter.OPENING); + long timeout = (expiresAt - System.currentTimeMillis()) / 1000; + generatedXML.writeText("Second-" + timeout); + generatedXML.writeElement("D", "timeout", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "locktoken", XMLWriter.OPENING); + for (String token : tokens) { + generatedXML.writeElement("D", "href", XMLWriter.OPENING); + generatedXML.writeText("opaquelocktoken:" + token); + generatedXML.writeElement("D", "href", XMLWriter.CLOSING); + } + generatedXML.writeElement("D", "locktoken", XMLWriter.CLOSING); + + generatedXML.writeElement("D", "activelock", XMLWriter.CLOSING); + } + } + + + // --------------------------------------------- WebdavResolver Inner Class + /** + * Work around for XML parsers that don't fully respect + * {@link DocumentBuilderFactory#setExpandEntityReferences(boolean)} when called with false. External + * references are filtered out for security reasons. See CVE-2007-5461. + */ + private static class WebdavResolver implements EntityResolver { + private ServletContext context; + + WebdavResolver(ServletContext theContext) { + context = theContext; + } + + @Override + public InputSource resolveEntity(String publicId, String systemId) { + context.log(sm.getString("webdavservlet.externalEntityIgnored", publicId, systemId)); + return new InputSource(new StringReader("Ignored external entity")); + } + } +} + + +// -------------------------------------------------------- WebdavStatus Class + +/** + * Wraps the HttpServletResponse class to abstract the specific protocol used. To support other protocols we would only + * need to modify this class and the WebDavRetCode classes. + * + * @author Marc Eaddy + */ +class WebdavStatus { + + // ------------------------------------------------------ HTTP Status Codes + + + /** + * Status code (200) indicating the request succeeded normally. + */ + public static final int SC_OK = HttpServletResponse.SC_OK; + + + /** + * Status code (201) indicating the request succeeded and created a new resource on the server. + */ + public static final int SC_CREATED = HttpServletResponse.SC_CREATED; + + + /** + * Status code (202) indicating that a request was accepted for processing, but was not completed. + */ + public static final int SC_ACCEPTED = HttpServletResponse.SC_ACCEPTED; + + + /** + * Status code (204) indicating that the request succeeded but that there was no new information to return. + */ + public static final int SC_NO_CONTENT = HttpServletResponse.SC_NO_CONTENT; + + + /** + * Status code (301) indicating that the resource has permanently moved to a new location, and that future + * references should use a new URI with their requests. + */ + public static final int SC_MOVED_PERMANENTLY = HttpServletResponse.SC_MOVED_PERMANENTLY; + + + /** + * Status code (302) indicating that the resource has temporarily moved to another location, but that future + * references should still use the original URI to access the resource. + */ + public static final int SC_MOVED_TEMPORARILY = HttpServletResponse.SC_MOVED_TEMPORARILY; + + + /** + * Status code (304) indicating that a conditional GET operation found that the resource was available and not + * modified. + */ + public static final int SC_NOT_MODIFIED = HttpServletResponse.SC_NOT_MODIFIED; + + + /** + * Status code (400) indicating the request sent by the client was syntactically incorrect. + */ + public static final int SC_BAD_REQUEST = HttpServletResponse.SC_BAD_REQUEST; + + + /** + * Status code (401) indicating that the request requires HTTP authentication. + */ + public static final int SC_UNAUTHORIZED = HttpServletResponse.SC_UNAUTHORIZED; + + + /** + * Status code (403) indicating the server understood the request but refused to fulfill it. + */ + public static final int SC_FORBIDDEN = HttpServletResponse.SC_FORBIDDEN; + + + /** + * Status code (404) indicating that the requested resource is not available. + */ + public static final int SC_NOT_FOUND = HttpServletResponse.SC_NOT_FOUND; + + + /** + * Status code (500) indicating an error inside the HTTP service which prevented it from fulfilling the request. + */ + public static final int SC_INTERNAL_SERVER_ERROR = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + + + /** + * Status code (501) indicating the HTTP service does not support the functionality needed to fulfill the request. + */ + public static final int SC_NOT_IMPLEMENTED = HttpServletResponse.SC_NOT_IMPLEMENTED; + + + /** + * Status code (502) indicating that the HTTP server received an invalid response from a server it consulted when + * acting as a proxy or gateway. + */ + public static final int SC_BAD_GATEWAY = HttpServletResponse.SC_BAD_GATEWAY; + + + /** + * Status code (503) indicating that the HTTP service is temporarily overloaded, and unable to handle the request. + */ + public static final int SC_SERVICE_UNAVAILABLE = HttpServletResponse.SC_SERVICE_UNAVAILABLE; + + + /** + * Status code (100) indicating the client may continue with its request. This interim response is used to inform + * the client that the initial part of the request has been received and has not yet been rejected by the server. + */ + public static final int SC_CONTINUE = HttpServletResponse.SC_CONTINUE; + + + /** + * Status code (405) indicating the method specified is not allowed for the resource. + */ + public static final int SC_METHOD_NOT_ALLOWED = HttpServletResponse.SC_METHOD_NOT_ALLOWED; + + + /** + * Status code (409) indicating that the request could not be completed due to a conflict with the current state of + * the resource. + */ + public static final int SC_CONFLICT = HttpServletResponse.SC_CONFLICT; + + + /** + * Status code (412) indicating the precondition given in one or more of the request-header fields evaluated to + * false when it was tested on the server. + */ + public static final int SC_PRECONDITION_FAILED = HttpServletResponse.SC_PRECONDITION_FAILED; + + + /** + * Status code (413) indicating the server is refusing to process a request because the request entity is larger + * than the server is willing or able to process. + */ + public static final int SC_REQUEST_TOO_LONG = HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE; + + + /** + * Status code (415) indicating the server is refusing to service the request because the entity of the request is + * in a format not supported by the requested resource for the requested method. + */ + public static final int SC_UNSUPPORTED_MEDIA_TYPE = HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE; + + + // -------------------------------------------- Extended WebDav status code + + + /** + * Status code (207) indicating that the response requires providing status for multiple independent operations. + */ + public static final int SC_MULTI_STATUS = 207; + // This one collides with HTTP 1.1 + // "207 Partial Update OK" + + + /** + * Status code (418) indicating the entity body submitted with the PATCH method was not understood by the resource. + */ + public static final int SC_UNPROCESSABLE_ENTITY = 418; + // This one collides with HTTP 1.1 + // "418 Reauthentication Required" + + + /** + * Status code (419) indicating that the resource does not have sufficient space to record the state of the resource + * after the execution of this method. + */ + public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419; + // This one collides with HTTP 1.1 + // "419 Proxy Reauthentication Required" + + + /** + * Status code (420) indicating the method was not executed on a particular resource within its scope because some + * part of the method's execution failed causing the entire method to be aborted. + */ + public static final int SC_METHOD_FAILURE = 420; + + + /** + * Status code (423) indicating the destination resource of a method is locked, and either the request did not + * contain a valid Lock-Info header, or the Lock-Info header identifies a lock held by another principal. + */ + public static final int SC_LOCKED = 423; +} diff --git a/java/org/apache/catalina/servlets/package.html b/java/org/apache/catalina/servlets/package.html new file mode 100644 index 0000000..c4c7527 --- /dev/null +++ b/java/org/apache/catalina/servlets/package.html @@ -0,0 +1,33 @@ + + + +

    This package contains Servlets that implement some of the +standard functionality provided by the Catalina servlet container. Because +these servlets are in the org.apache.catalina package hierarchy, +they are in the privileged position of being able to reference internal server +data structures, which application level servlets are prevented from +accessing (by the application class loader implementation).

    + +

    To the extent that these servlets depend upon internal Catalina data +structures, they are obviously not portable to other servlet container +environments. However, they can be used as models for creating application +level servlets that provide similar capabilities -- most obviously the +DefaultServlet implementation, which +serves static resources when Catalina runs stand-alone.

    + + diff --git a/java/org/apache/catalina/session/Constants.java b/java/org/apache/catalina/session/Constants.java new file mode 100644 index 0000000..3948fe8 --- /dev/null +++ b/java/org/apache/catalina/session/Constants.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.session; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.apache.catalina.Globals; +import org.apache.catalina.valves.CrawlerSessionManagerValve; + +/** + * Manifest constants for the org.apache.catalina.session package. + * + * @author Craig R. McClanahan + */ + +public class Constants { + + /** + * Set of session attribute names used internally by Tomcat that should always be removed from the session before it + * is persisted, replicated or equivalent. + */ + public static final Set excludedAttributeNames; + + static { + Set names = new HashSet<>(); + names.add(Globals.SUBJECT_ATTR); + names.add(CrawlerSessionManagerValve.class.getName()); + excludedAttributeNames = Collections.unmodifiableSet(names); + } +} diff --git a/java/org/apache/catalina/session/DataSourceStore.java b/java/org/apache/catalina/session/DataSourceStore.java new file mode 100644 index 0000000..b6c489a --- /dev/null +++ b/java/org/apache/catalina/session/DataSourceStore.java @@ -0,0 +1,733 @@ +/* + * 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.catalina.session; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +import org.apache.catalina.Container; +import org.apache.catalina.Globals; +import org.apache.catalina.Session; +import org.apache.juli.logging.Log; + +/** + * Implementation of the {@link org.apache.catalina.Store Store} interface that stores serialized session objects in a + * database. Sessions that are saved are still subject to being expired based on inactivity. + * + * @author Bip Thelin + */ +public class DataSourceStore extends StoreBase { + + /** + * Context name associated with this Store + */ + private String name = null; + + /** + * Name to register for this Store, used for logging. + */ + protected static final String storeName = "dataSourceStore"; + + /** + * name of the JNDI resource + */ + protected String dataSourceName = null; + + /** + * Context local datasource. + */ + private boolean localDataSource = false; + + /** + * DataSource to use + */ + protected DataSource dataSource = null; + + + // ------------------------------------------------------------ Table & cols + + /** + * Table to use. + */ + protected String sessionTable = "tomcat$sessions"; + + /** + * Column to use for /Engine/Host/Context name + */ + protected String sessionAppCol = "app"; + + /** + * Id column to use. + */ + protected String sessionIdCol = "id"; + + /** + * Data column to use. + */ + protected String sessionDataCol = "data"; + + /** + * {@code Is Valid} column to use. + */ + protected String sessionValidCol = "valid"; + + /** + * Max Inactive column to use. + */ + protected String sessionMaxInactiveCol = "maxinactive"; + + /** + * Last Accessed column to use. + */ + protected String sessionLastAccessedCol = "lastaccess"; + + // -------------------------------------------------------------- Properties + + /** + * @return the name for this instance (built from container name) + */ + public String getName() { + if (name == null) { + Container container = manager.getContext(); + String contextName = container.getName(); + if (!contextName.startsWith("/")) { + contextName = "/" + contextName; + } + String hostName = ""; + String engineName = ""; + + if (container.getParent() != null) { + Container host = container.getParent(); + hostName = host.getName(); + if (host.getParent() != null) { + engineName = host.getParent().getName(); + } + } + name = "/" + engineName + "/" + hostName + contextName; + } + return name; + } + + /** + * @return the name for this Store, used for logging. + */ + @Override + public String getStoreName() { + return storeName; + } + + /** + * Set the table for this Store. + * + * @param sessionTable The new table + */ + public void setSessionTable(String sessionTable) { + String oldSessionTable = this.sessionTable; + this.sessionTable = sessionTable; + support.firePropertyChange("sessionTable", oldSessionTable, this.sessionTable); + } + + /** + * @return the table for this Store. + */ + public String getSessionTable() { + return sessionTable; + } + + /** + * Set the App column for the table. + * + * @param sessionAppCol the column name + */ + public void setSessionAppCol(String sessionAppCol) { + String oldSessionAppCol = this.sessionAppCol; + this.sessionAppCol = sessionAppCol; + support.firePropertyChange("sessionAppCol", oldSessionAppCol, this.sessionAppCol); + } + + /** + * @return the web application name column for the table. + */ + public String getSessionAppCol() { + return this.sessionAppCol; + } + + /** + * Set the Id column for the table. + * + * @param sessionIdCol the column name + */ + public void setSessionIdCol(String sessionIdCol) { + String oldSessionIdCol = this.sessionIdCol; + this.sessionIdCol = sessionIdCol; + support.firePropertyChange("sessionIdCol", oldSessionIdCol, this.sessionIdCol); + } + + /** + * @return the Id column for the table. + */ + public String getSessionIdCol() { + return this.sessionIdCol; + } + + /** + * Set the Data column for the table + * + * @param sessionDataCol the column name + */ + public void setSessionDataCol(String sessionDataCol) { + String oldSessionDataCol = this.sessionDataCol; + this.sessionDataCol = sessionDataCol; + support.firePropertyChange("sessionDataCol", oldSessionDataCol, this.sessionDataCol); + } + + /** + * @return the data column for the table + */ + public String getSessionDataCol() { + return this.sessionDataCol; + } + + /** + * Set the {@code Is Valid} column for the table + * + * @param sessionValidCol The column name + */ + public void setSessionValidCol(String sessionValidCol) { + String oldSessionValidCol = this.sessionValidCol; + this.sessionValidCol = sessionValidCol; + support.firePropertyChange("sessionValidCol", oldSessionValidCol, this.sessionValidCol); + } + + /** + * @return the {@code Is Valid} column + */ + public String getSessionValidCol() { + return this.sessionValidCol; + } + + /** + * Set the {@code Max Inactive} column for the table + * + * @param sessionMaxInactiveCol The column name + */ + public void setSessionMaxInactiveCol(String sessionMaxInactiveCol) { + String oldSessionMaxInactiveCol = this.sessionMaxInactiveCol; + this.sessionMaxInactiveCol = sessionMaxInactiveCol; + support.firePropertyChange("sessionMaxInactiveCol", oldSessionMaxInactiveCol, this.sessionMaxInactiveCol); + } + + /** + * @return the {@code Max Inactive} column + */ + public String getSessionMaxInactiveCol() { + return this.sessionMaxInactiveCol; + } + + /** + * Set the {@code Last Accessed} column for the table + * + * @param sessionLastAccessedCol The column name + */ + public void setSessionLastAccessedCol(String sessionLastAccessedCol) { + String oldSessionLastAccessedCol = this.sessionLastAccessedCol; + this.sessionLastAccessedCol = sessionLastAccessedCol; + support.firePropertyChange("sessionLastAccessedCol", oldSessionLastAccessedCol, this.sessionLastAccessedCol); + } + + /** + * @return the {@code Last Accessed} column + */ + public String getSessionLastAccessedCol() { + return this.sessionLastAccessedCol; + } + + /** + * Set the JNDI name of a DataSource-factory to use for db access + * + * @param dataSourceName The JNDI name of the DataSource-factory + */ + public void setDataSourceName(String dataSourceName) { + if (dataSourceName == null || dataSourceName.trim().isEmpty()) { + manager.getContext().getLogger().warn(sm.getString(getStoreName() + ".missingDataSourceName")); + return; + } + this.dataSourceName = dataSourceName; + } + + /** + * @return the name of the JNDI DataSource-factory + */ + public String getDataSourceName() { + return this.dataSourceName; + } + + /** + * @return if the datasource will be looked up in the webapp JNDI Context. + */ + public boolean getLocalDataSource() { + return localDataSource; + } + + /** + * Set to {@code true} to cause the datasource to be looked up in the webapp JNDI Context. + * + * @param localDataSource the new flag value + */ + public void setLocalDataSource(boolean localDataSource) { + this.localDataSource = localDataSource; + } + + + // --------------------------------------------------------- Public Methods + + @Override + public String[] expiredKeys() throws IOException { + return keys(true); + } + + @Override + public String[] keys() throws IOException { + return keys(false); + } + + /** + * Return an array containing the session identifiers of all Sessions currently saved in this Store. If there are no + * such Sessions, a zero-length array is returned. + * + * @param expiredOnly flag, whether only keys of expired sessions should be returned + * + * @return array containing the list of session IDs + * + * @exception IOException if an input/output error occurred + */ + private String[] keys(boolean expiredOnly) throws IOException { + String keys[] = null; + int numberOfTries = 2; + while (numberOfTries > 0) { + + Connection _conn = getConnection(); + if (_conn == null) { + return new String[0]; + } + try { + + String keysSql = + "SELECT " + sessionIdCol + " FROM " + sessionTable + " WHERE " + sessionAppCol + " = ?"; + if (expiredOnly) { + keysSql += " AND (" + sessionLastAccessedCol + " + " + sessionMaxInactiveCol + " * 1000 < ?)"; + } + try (PreparedStatement preparedKeysSql = _conn.prepareStatement(keysSql)) { + preparedKeysSql.setString(1, getName()); + if (expiredOnly) { + preparedKeysSql.setLong(2, System.currentTimeMillis()); + } + try (ResultSet rst = preparedKeysSql.executeQuery()) { + List tmpkeys = new ArrayList<>(); + if (rst != null) { + while (rst.next()) { + tmpkeys.add(rst.getString(1)); + } + } + keys = tmpkeys.toArray(new String[0]); + // Break out after the finally block + numberOfTries = 0; + } + } + } catch (SQLException e) { + manager.getContext().getLogger().error(sm.getString("dataSourceStore.SQLException", e)); + keys = new String[0]; + // Close the connection so that it gets reopened next time + } finally { + release(_conn); + } + numberOfTries--; + } + return keys; + } + + /** + * Return an integer containing a count of all Sessions currently saved in this Store. If there are no Sessions, + * 0 is returned. + * + * @return the count of all sessions currently saved in this Store + * + * @exception IOException if an input/output error occurred + */ + @Override + public int getSize() throws IOException { + int size = 0; + String sizeSql = "SELECT COUNT(" + sessionIdCol + ") FROM " + sessionTable + " WHERE " + sessionAppCol + " = ?"; + + int numberOfTries = 2; + while (numberOfTries > 0) { + Connection _conn = getConnection(); + + if (_conn == null) { + return size; + } + + try (PreparedStatement preparedSizeSql = _conn.prepareStatement(sizeSql)) { + preparedSizeSql.setString(1, getName()); + try (ResultSet rst = preparedSizeSql.executeQuery()) { + if (rst.next()) { + size = rst.getInt(1); + } + // Break out after the finally block + numberOfTries = 0; + } + } catch (SQLException e) { + manager.getContext().getLogger().error(sm.getString("dataSourceStore.SQLException", e)); + } finally { + release(_conn); + } + numberOfTries--; + } + return size; + } + + /** + * Load the Session associated with the id id. If no such session is found null is + * returned. + * + * @param id a value of type String + * + * @return the stored Session + * + * @exception ClassNotFoundException if an error occurs + * @exception IOException if an input/output error occurred + */ + @Override + public Session load(String id) throws ClassNotFoundException, IOException { + StandardSession _session = null; + org.apache.catalina.Context context = getManager().getContext(); + Log contextLog = context.getLogger(); + + int numberOfTries = 2; + String loadSql = "SELECT " + sessionIdCol + ", " + sessionDataCol + " FROM " + sessionTable + " WHERE " + + sessionIdCol + " = ? AND " + sessionAppCol + " = ?"; + while (numberOfTries > 0) { + Connection _conn = getConnection(); + if (_conn == null) { + return null; + } + + ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, null); + + try (PreparedStatement preparedLoadSql = _conn.prepareStatement(loadSql)) { + preparedLoadSql.setString(1, id); + preparedLoadSql.setString(2, getName()); + try (ResultSet rst = preparedLoadSql.executeQuery()) { + if (rst.next()) { + try (ObjectInputStream ois = getObjectInputStream(rst.getBinaryStream(2))) { + if (contextLog.isTraceEnabled()) { + contextLog.trace(sm.getString("dataSourceStore.loading", id, sessionTable)); + } + + _session = (StandardSession) manager.createEmptySession(); + _session.readObjectData(ois); + _session.setManager(manager); + } + } else if (context.getLogger().isDebugEnabled()) { + contextLog.debug(sm.getString("dataSourceStore.noObject", id)); + } + // Break out after the finally block + numberOfTries = 0; + } + } catch (SQLException e) { + contextLog.error(sm.getString("dataSourceStore.SQLException", e)); + } finally { + context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL); + release(_conn); + } + numberOfTries--; + } + return _session; + } + + /** + * Remove the Session with the specified session identifier from this Store, if present. If no such Session is + * present, this method takes no action. + * + * @param id Session identifier of the Session to be removed + * + * @exception IOException if an input/output error occurs + */ + @Override + public void remove(String id) throws IOException { + + int numberOfTries = 2; + while (numberOfTries > 0) { + Connection _conn = getConnection(); + + if (_conn == null) { + return; + } + + try { + remove(id, _conn); + // Break out after the finally block + numberOfTries = 0; + } catch (SQLException e) { + manager.getContext().getLogger().error(sm.getString("dataSourceStore.SQLException", e)); + } finally { + release(_conn); + } + numberOfTries--; + } + + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger().trace(sm.getString("dataSourceStore.removing", id, sessionTable)); + } + } + + /** + * Remove the Session with the specified session identifier from this Store, if present. If no such Session is + * present, this method takes no action. + * + * @param id Session identifier of the Session to be removed + * @param _conn open connection to be used + * + * @throws SQLException if an error occurs while talking to the database + */ + private void remove(String id, Connection _conn) throws SQLException { + String removeSql = + "DELETE FROM " + sessionTable + " WHERE " + sessionIdCol + " = ? AND " + sessionAppCol + " = ?"; + try (PreparedStatement preparedRemoveSql = _conn.prepareStatement(removeSql)) { + preparedRemoveSql.setString(1, id); + preparedRemoveSql.setString(2, getName()); + preparedRemoveSql.execute(); + } + } + + /** + * Remove all of the Sessions in this Store. + * + * @exception IOException if an input/output error occurs + */ + @Override + public void clear() throws IOException { + String clearSql = "DELETE FROM " + sessionTable + " WHERE " + sessionAppCol + " = ?"; + + int numberOfTries = 2; + while (numberOfTries > 0) { + Connection _conn = getConnection(); + if (_conn == null) { + return; + } + + try (PreparedStatement preparedClearSql = _conn.prepareStatement(clearSql)) { + preparedClearSql.setString(1, getName()); + preparedClearSql.execute(); + // Break out after the finally block + numberOfTries = 0; + } catch (SQLException e) { + manager.getContext().getLogger().error(sm.getString("dataSourceStore.SQLException", e)); + } finally { + release(_conn); + } + numberOfTries--; + } + } + + /** + * Save a session to the Store. + * + * @param session the session to be stored + * + * @exception IOException if an input/output error occurs + */ + @Override + public void save(Session session) throws IOException { + ByteArrayOutputStream bos = null; + String saveSql = "INSERT INTO " + sessionTable + " (" + sessionIdCol + ", " + sessionAppCol + ", " + + sessionDataCol + ", " + sessionValidCol + ", " + sessionMaxInactiveCol + ", " + sessionLastAccessedCol + + ") VALUES (?, ?, ?, ?, ?, ?)"; + + synchronized (session) { + int numberOfTries = 2; + while (numberOfTries > 0) { + Connection _conn = getConnection(); + if (_conn == null) { + return; + } + + try { + // If sessions already exist in DB, remove and insert again. + // TODO: + // * Check if ID exists in database and if so use UPDATE. + remove(session.getIdInternal(), _conn); + + bos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) { + ((StandardSession) session).writeObjectData(oos); + } + byte[] obs = bos.toByteArray(); + int size = obs.length; + try (ByteArrayInputStream bis = new ByteArrayInputStream(obs, 0, size); + InputStream in = new BufferedInputStream(bis, size); + PreparedStatement preparedSaveSql = _conn.prepareStatement(saveSql)) { + preparedSaveSql.setString(1, session.getIdInternal()); + preparedSaveSql.setString(2, getName()); + preparedSaveSql.setBinaryStream(3, in, size); + preparedSaveSql.setString(4, session.isValid() ? "1" : "0"); + preparedSaveSql.setInt(5, session.getMaxInactiveInterval()); + preparedSaveSql.setLong(6, session.getLastAccessedTime()); + preparedSaveSql.execute(); + // Break out after the finally block + numberOfTries = 0; + } + } catch (SQLException e) { + manager.getContext().getLogger().error(sm.getString("dataSourceStore.SQLException", e)); + } catch (IOException e) { + // Ignore + } finally { + release(_conn); + } + numberOfTries--; + } + } + + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger() + .trace(sm.getString("dataSourceStore.saving", session.getIdInternal(), sessionTable)); + } + } + + + // --------------------------------------------------------- Protected Methods + + /** + * Check the connection associated with this store, if it's null or closed try to reopen it. Returns + * null if the connection could not be established. + * + * @return Connection if the connection succeeded + */ + protected Connection getConnection() { + Connection conn = null; + try { + conn = open(); + if (conn == null || conn.isClosed()) { + manager.getContext().getLogger().info(sm.getString("dataSourceStore.checkConnectionDBClosed")); + conn = open(); + if (conn == null || conn.isClosed()) { + manager.getContext().getLogger() + .info(sm.getString("dataSourceStore.checkConnectionDBReOpenFail")); + } + } + } catch (SQLException ex) { + manager.getContext().getLogger() + .error(sm.getString("dataSourceStore.checkConnectionSQLException", ex)); + } + + return conn; + } + + /** + * Open (if necessary) and return a database connection for use by this Store. + * + * @return database connection ready to use + * + * @exception SQLException if a database error occurs + */ + protected Connection open() throws SQLException { + if (dataSourceName != null && dataSource == null) { + org.apache.catalina.Context context = getManager().getContext(); + ClassLoader oldThreadContextCL = null; + if (getLocalDataSource()) { + oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, null); + } + + Context initCtx; + try { + initCtx = new InitialContext(); + Context envCtx = (Context) initCtx.lookup("java:comp/env"); + this.dataSource = (DataSource) envCtx.lookup(this.dataSourceName); + } catch (NamingException e) { + context.getLogger().error(sm.getString("dataSourceStore.wrongDataSource", this.dataSourceName), e); + } finally { + if (getLocalDataSource()) { + context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL); + } + } + } + + if (dataSource != null) { + return dataSource.getConnection(); + } else { + throw new IllegalStateException(sm.getString("dataSourceStore.missingDataSource")); + } + } + + /** + * Close the specified database connection. + * + * @param dbConnection The connection to be closed + */ + protected void close(Connection dbConnection) { + + // Do nothing if the database connection is already closed + if (dbConnection == null) { + return; + } + + // Commit if autoCommit is false + try { + if (!dbConnection.getAutoCommit()) { + dbConnection.commit(); + } + } catch (SQLException e) { + manager.getContext().getLogger().error(sm.getString("dataSourceStore.commitSQLException"), e); + } + + // Close this database connection, and log any errors + try { + dbConnection.close(); + } catch (SQLException e) { + manager.getContext().getLogger().error(sm.getString("dataSourceStore.close", e)); + } + } + + /** + * Release the connection, if it is associated with a connection pool. + * + * @param conn The connection to be released + */ + protected void release(Connection conn) { + if (dataSource != null) { + close(conn); + } + } + +} diff --git a/java/org/apache/catalina/session/FileStore.java b/java/org/apache/catalina/session/FileStore.java new file mode 100644 index 0000000..7399d43 --- /dev/null +++ b/java/org/apache/catalina/session/FileStore.java @@ -0,0 +1,354 @@ +/* + * 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.catalina.session; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.ServletContext; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.Session; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Concrete implementation of the Store interface that utilizes a file per saved Session in a configured + * directory. Sessions that are saved are still subject to being expired based on inactivity. + * + * @author Craig R. McClanahan + */ +public final class FileStore extends StoreBase { + + private static final Log log = LogFactory.getLog(FileStore.class); + private static final StringManager sm = StringManager.getManager(FileStore.class); + + + // ----------------------------------------------------- Constants + + /** + * The extension to use for serialized session filenames. + */ + private static final String FILE_EXT = ".session"; + + + // ----------------------------------------------------- Instance Variables + + /** + * The pathname of the directory in which Sessions are stored. This may be an absolute pathname, or a relative path + * that is resolved against the temporary work directory for this application. + */ + private String directory = "."; + + + /** + * A File representing the directory in which Sessions are stored. + */ + private File directoryFile = null; + + + /** + * Name to register for this Store, used for logging. + */ + private static final String storeName = "fileStore"; + + + /** + * Name to register for the background thread. + */ + private static final String threadName = "FileStore"; + + + // ------------------------------------------------------------- Properties + + /** + * @return The directory path for this Store. + */ + public String getDirectory() { + return directory; + } + + + /** + * Set the directory path for this Store. + * + * @param path The new directory path + */ + public void setDirectory(String path) { + String oldDirectory = this.directory; + this.directory = path; + this.directoryFile = null; + support.firePropertyChange("directory", oldDirectory, this.directory); + } + + + /** + * @return The thread name for this Store. + */ + public String getThreadName() { + return threadName; + } + + + /** + * Return the name for this Store, used for logging. + */ + @Override + public String getStoreName() { + return storeName; + } + + + /** + * Return the number of Sessions present in this Store. + * + * @exception IOException if an input/output error occurs + */ + @Override + public int getSize() throws IOException { + // Acquire the list of files in our storage directory + File dir = directory(); + if (dir == null) { + return 0; + } + String files[] = dir.list(); + + // Figure out which files are sessions + int keycount = 0; + if (files != null) { + for (String file : files) { + if (file.endsWith(FILE_EXT)) { + keycount++; + } + } + } + return keycount; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Remove all of the Sessions in this Store. + * + * @exception IOException if an input/output error occurs + */ + @Override + public void clear() throws IOException { + String[] keys = keys(); + for (String key : keys) { + remove(key); + } + } + + + /** + * Return an array containing the session identifiers of all Sessions currently saved in this Store. If there are no + * such Sessions, a zero-length array is returned. + * + * @exception IOException if an input/output error occurred + */ + @Override + public String[] keys() throws IOException { + // Acquire the list of files in our storage directory + File dir = directory(); + if (dir == null) { + return new String[0]; + } + String files[] = dir.list(); + + // Bugzilla 32130 + if (files == null || files.length < 1) { + return new String[0]; + } + + // Build and return the list of session identifiers + List list = new ArrayList<>(); + int n = FILE_EXT.length(); + for (String file : files) { + if (file.endsWith(FILE_EXT)) { + list.add(file.substring(0, file.length() - n)); + } + } + return list.toArray(new String[0]); + } + + + /** + * Load and return the Session associated with the specified session identifier from this Store, without removing + * it. If there is no such stored Session, return null. + * + * @param id Session identifier of the session to load + * + * @exception ClassNotFoundException if a deserialization error occurs + * @exception IOException if an input/output error occurs + */ + @Override + public Session load(String id) throws ClassNotFoundException, IOException { + // Open an input stream to the specified pathname, if any + File file = file(id); + if (file == null || !file.exists()) { + return null; + } + + Context context = getManager().getContext(); + Log contextLog = context.getLogger(); + + if (contextLog.isTraceEnabled()) { + contextLog.trace(sm.getString(getStoreName() + ".loading", id, file.getAbsolutePath())); + } + + ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, null); + + try (FileInputStream fis = new FileInputStream(file.getAbsolutePath()); + ObjectInputStream ois = getObjectInputStream(fis)) { + + StandardSession session = (StandardSession) manager.createEmptySession(); + session.readObjectData(ois); + session.setManager(manager); + return session; + } catch (FileNotFoundException e) { + if (contextLog.isDebugEnabled()) { + contextLog.debug(sm.getString("fileStore.noFile", id, file.getAbsolutePath())); + } + return null; + } finally { + context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL); + } + } + + + /** + * Remove the Session with the specified session identifier from this Store, if present. If no such Session is + * present, this method takes no action. + * + * @param id Session identifier of the Session to be removed + * + * @exception IOException if an input/output error occurs + */ + @Override + public void remove(String id) throws IOException { + File file = file(id); + if (file == null) { + return; + } + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger() + .trace(sm.getString(getStoreName() + ".removing", id, file.getAbsolutePath())); + } + + if (file.exists() && !file.delete()) { + throw new IOException(sm.getString("fileStore.deleteSessionFailed", file)); + } + } + + + /** + * Save the specified Session into this Store. Any previously saved information for the associated session + * identifier is replaced. + * + * @param session Session to be saved + * + * @exception IOException if an input/output error occurs + */ + @Override + public void save(Session session) throws IOException { + // Open an output stream to the specified pathname, if any + File file = file(session.getIdInternal()); + if (file == null) { + return; + } + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger() + .trace(sm.getString(getStoreName() + ".saving", session.getIdInternal(), file.getAbsolutePath())); + } + + try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath()); + ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(fos))) { + ((StandardSession) session).writeObjectData(oos); + } + } + + + // -------------------------------------------------------- Private Methods + + /** + * Return a File object representing the pathname to our session persistence directory, if any. The directory will + * be created if it does not already exist. + */ + private File directory() throws IOException { + if (this.directory == null) { + return null; + } + if (this.directoryFile != null) { + // NOTE: Race condition is harmless, so do not synchronize + return this.directoryFile; + } + File file = new File(this.directory); + if (!file.isAbsolute()) { + Context context = manager.getContext(); + ServletContext servletContext = context.getServletContext(); + File work = (File) servletContext.getAttribute(ServletContext.TEMPDIR); + file = new File(work, this.directory); + } + if (!file.exists() || !file.isDirectory()) { + if (!file.delete() && file.exists()) { + throw new IOException(sm.getString("fileStore.deleteFailed", file)); + } + if (!file.mkdirs() && !file.isDirectory()) { + throw new IOException(sm.getString("fileStore.createFailed", file)); + } + } + this.directoryFile = file; + return file; + } + + + /** + * Return a File object representing the pathname to our session persistence file, if any. + * + * @param id The ID of the Session to be retrieved. This is used in the file naming. + */ + private File file(String id) throws IOException { + File storageDir = directory(); + if (storageDir == null) { + return null; + } + + String filename = id + FILE_EXT; + File file = new File(storageDir, filename); + File canonicalFile = file.getCanonicalFile(); + + // Check the file is within the storage directory + if (!canonicalFile.toPath().startsWith(storageDir.getCanonicalFile().toPath())) { + log.warn(sm.getString("fileStore.invalid", file.getPath(), id)); + return null; + } + + return canonicalFile; + } +} diff --git a/java/org/apache/catalina/session/LocalStrings.properties b/java/org/apache/catalina/session/LocalStrings.properties new file mode 100644 index 0000000..b5f654f --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings.properties @@ -0,0 +1,103 @@ +# 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. + +dataSourceStore.SQLException=SQL Error [{0}] +dataSourceStore.checkConnectionDBClosed=The database connection is null or was found to be closed. Trying to re-open it. +dataSourceStore.checkConnectionDBReOpenFail=The re-open on the database failed. The database could be down. +dataSourceStore.checkConnectionSQLException=A SQL exception occurred [{0}] +dataSourceStore.close=Exception closing database connection [{0}] +dataSourceStore.commitSQLException=SQLException committing connection before closing +dataSourceStore.loading=Loading Session [{0}] from database [{1}] +dataSourceStore.missingDataSource=No data source available +dataSourceStore.missingDataSourceName=No valid JNDI name was given +dataSourceStore.noObject=No persisted data object found for session [{0}] +dataSourceStore.removing=Removing Session [{0}] at database [{1}] +dataSourceStore.saving=Saving Session [{0}] to database [{1}] +dataSourceStore.wrongDataSource=Cannot open JNDI DataSource [{0}] + +fileStore.createFailed=Unable to create directory [{0}] for the storage of session data +fileStore.deleteFailed=Unable to delete file [{0}] which is preventing the creation of the session storage location +fileStore.deleteSessionFailed=Unable to delete file [{0}] which is no longer required +fileStore.invalid=Invalid persistence file [{0}] for session ID [{1}] +fileStore.loading=Loading Session [{0}] from file [{1}] +fileStore.noFile=Persistence file [{1}] for session ID [{0}] was not found +fileStore.removing=Removing Session [{0}] at file [{1}] +fileStore.saving=Saving Session [{0}] to file [{1}] + +managerBase.container.noop=Managers added to containers other than Contexts will never be used +managerBase.contextNull=The Context must be set to a non-null value before the Manager is used +managerBase.createSession.ise=createSession: Too many active sessions +managerBase.sessionAttributeNameFilter=Skipped session attribute named [{0}] because it did not match the name filter [{1}] +managerBase.sessionAttributeValueClassNameFilter=Skipped session attribute named [{0}] because the value type [{1}] did not match the filter [{2}] +managerBase.sessionNotFound=The session [{0}] was not found +managerBase.sessionTimeout=Invalid session timeout setting [{0}] +managerBase.setContextNotNew=It is illegal to call setContext() to change the Context associated with a Manager if the Manager is not in the NEW state + +persistentManager.backupMaxIdle=Backing up session [{0}] to Store, idle for [{1}] seconds +persistentManager.deserializeError=Error deserializing Session [{0}] +persistentManager.isLoadedError=Error checking if session [{0}] is loaded in memory +persistentManager.loading=Loading [{0}] persisted sessions +persistentManager.noStore=No Store configured, persistence disabled +persistentManager.removeError=Error removing session [{0}] from the store +persistentManager.serializeError=Error serializing Session [{0}]: [{1}] +persistentManager.storeClearError=Error clearning all sessions from the store +persistentManager.storeKeysException=Unable to determine the list of session IDs for sessions in the session store, assuming that the store is empty +persistentManager.storeLoadError=Error swapping in sessions from the store +persistentManager.storeLoadKeysError=Error loading sessions keys from the store +persistentManager.storeSizeException=Unable to determine the number of sessions in the session store, assuming that the store is empty +persistentManager.swapIn=Swapping session [{0}] in from Store +persistentManager.swapInException=Exception in the Store during swapIn: [{0}] +persistentManager.swapInInvalid=Swapped session [{0}] is invalid +persistentManager.swapMaxIdle=Swapping session [{0}] to Store, idle for [{1}] seconds +persistentManager.swapTooManyActive=Swapping out session [{0}], idle for [{1}] seconds too many sessions active +persistentManager.tooManyActive=Too many active sessions, [{0}], looking for idle sessions to swap out +persistentManager.unloading=Saving [{0}] persisted sessions + +standardManager.deletePersistedFileFail=Unable to delete [{0}] after reading the persisted sessions. The continued presence of this file may cause future attempts to persist sessions to fail. +standardManager.expiringSessions=Expiring [{0}] persisted sessions +standardManager.loading=Loading persisted sessions from [{0}] +standardManager.loading.exception=Exception while loading persisted sessions +standardManager.managerLoad=Exception loading sessions from persistent storage +standardManager.managerUnload=Exception unloading sessions to persistent storage +standardManager.noFile=Persisted data file [{0}] was not found +standardManager.unloading=Saving persisted sessions to [{0}] +standardManager.unloading.debug=Unloading persisted sessions +standardManager.unloading.nosessions=No persisted sessions to unload + +standardSession.attributeEvent=Session attribute event listener threw exception +standardSession.bindingEvent=Session binding event listener threw exception +standardSession.getAttribute.ise=getAttribute: Session already invalidated +standardSession.getAttributeNames.ise=getAttributeNames: Session already invalidated +standardSession.getCreationTime.ise=getCreationTime: Session already invalidated +standardSession.getIdleTime.ise=getIdleTime: Session already invalidated +standardSession.getLastAccessedTime.ise=getLastAccessedTime: Session already invalidated +standardSession.getThisAccessedTime.ise=getThisAccessedTime: Session already invalidated +standardSession.getValueNames.ise=getValueNames: Session already invalidated +standardSession.invalidate.ise=invalidate: Session already invalidated +standardSession.isNew.ise=isNew: Session already invalidated +standardSession.logoutfail=Exception logging out user when expiring session +standardSession.notDeserializable=Cannot deserialize session attribute [{0}] for session [{1}] +standardSession.notSerializable=Cannot serialize session attribute [{0}] for session [{1}] +standardSession.principalNotDeserializable=Cannot deserialize Principal object for session [{0}] +standardSession.principalNotSerializable=Cannot serialize Principal object for session [{0}] +standardSession.removeAttribute.ise=removeAttribute: Session already invalidated +standardSession.sessionEvent=Session event listener threw exception +standardSession.setAttribute.iae=setAttribute: Non-serializable attribute [{0}] +standardSession.setAttribute.ise=setAttribute: Session [{0}] has already been invalidated +standardSession.setAttribute.namenull=setAttribute: name parameter cannot be null + +store.expireFail=Error processing session expiration for key [{0}] +store.keysFail=Error getting keys +store.removeFail=Error removing key [{0}] diff --git a/java/org/apache/catalina/session/LocalStrings_cs.properties b/java/org/apache/catalina/session/LocalStrings_cs.properties new file mode 100644 index 0000000..bae2fb4 --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_cs.properties @@ -0,0 +1,25 @@ +# 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. + +dataSourceStore.saving=Ukládám Session [{0}] do databáze [{1}] + +managerBase.contextNull=Context musí být nastaven na nenulovou hodnotu pÅ™edtím než je Manager použit + +persistentManager.storeKeysException=Nelze urÄit seznam session ID pro session v uložiÅ¡ti, uložiÅ¡tÄ› je pravdÄ›podobnÄ› prázdné +persistentManager.storeSizeException=Nelze zjistit poÄet relací v úložiÅ¡ti, považuje se za prázdné +persistentManager.swapTooManyActive=VýmÄ›na session [{0}], Äekání [{1}] sekund - příliÅ¡ mnoho aktivních session + +standardManager.loading.exception=Výjimka bÄ›hem naÄítaní uložených session +standardManager.managerUnload=Výjimka pÅ™i ukládání sessions do trvalého úložiÅ¡tÄ› diff --git a/java/org/apache/catalina/session/LocalStrings_de.properties b/java/org/apache/catalina/session/LocalStrings_de.properties new file mode 100644 index 0000000..1e1cc89 --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_de.properties @@ -0,0 +1,32 @@ +# 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. + +dataSourceStore.missingDataSourceName=Kein gültiger JNDI Namen wurde übergeben. +dataSourceStore.saving=Speichere Session [{0}] in Datenbank [{1}] + +fileStore.deleteFailed=Kann Datei [{0}] nicht löschen. Das verhindert das Erzeugen des Ablageortes für die Session-Ablage + +managerBase.contextNull=Der Context muss auf einen nicht-null Wert gesetzt sein, bevor der Manager benutzt werden kann + +persistentManager.storeKeysException=Kann die Liste der Session IDs für die Sessions in der Session-Ablage nicht ermitteln, nehme an, dass die Ablage leer ist. +persistentManager.storeSizeException=Konnte die Anzahl der Sessions im Session-Store nicht ermitteln, gehe davon aus, dass der Store leer ist. +persistentManager.swapTooManyActive=Lagere Session [{0}] aus, Untätig seit [{1}] Sekunden und zu viele Sessions aktiv + +standardManager.loading.exception=Ausnahme beim Laden der persistierten Sitzungen +standardManager.managerLoad=Ausnahme beim Laden der Sitzungen aus dem Persistenten Speicher +standardManager.managerUnload=Fehler beim Entladen der Session zu persistenten Speicher + +standardSession.getAttributeNames.ise=getAttributeNames: Session bereits ungültig +standardSession.notSerializable=Kann Session Attribut [{0}] für Sitzung [{1}] nicht serialisieren diff --git a/java/org/apache/catalina/session/LocalStrings_es.properties b/java/org/apache/catalina/session/LocalStrings_es.properties new file mode 100644 index 0000000..aa393e9 --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_es.properties @@ -0,0 +1,69 @@ +# 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. + +dataSourceStore.SQLException=Error SQL [{0}] +dataSourceStore.checkConnectionDBClosed=La conexióna a base de datos es nula o está cerrada. Intentando reabrirla. +dataSourceStore.checkConnectionDBReOpenFail=Falló la reapertura de la base de datos. Puede que la base de datos esté caída. +dataSourceStore.checkConnectionSQLException=Ha tenido lugar una excepción SQL [{0}] +dataSourceStore.close=Excepción cerrando conexión a base de datos [{0}] +dataSourceStore.loading=Cargando Sesión [{0}] desde base de datos [{1}] +dataSourceStore.missingDataSourceName=No se proporcionó un nombre JNDI válido +dataSourceStore.removing=Quitando Sesión [{0}] en base de datos [{1}] +dataSourceStore.saving=Salvando Sesión [{0}] en base de datos [{1}] + +fileStore.loading=Cargando Sesión [{0}] desde archivo [{1}] +fileStore.removing=Quitando Sesión [{0}] en archivo [{1}] +fileStore.saving=Salvando Sesión [{0}] en archivo [{1}] + +managerBase.contextNull=El Çontexto debe ser fijado a un valor no nulo antes the fijar el Gerenciador +managerBase.createSession.ise=createSession: Demasiadas sesiones activas +managerBase.sessionTimeout=Valor inválido de Tiempo Agotado de sesión [{0}] +managerBase.setContextNotNew=Es ilegal llamar a setContext() para cambiar el Contexto associado con un Manejador si el Manejador no esta en el estado NEW. + +persistentManager.backupMaxIdle=Respaldando sesión [{0}] a Almacén, ociosa durante [{1}] segundos +persistentManager.deserializeError=Error des-serializando Sesión [{0}]: [{1}] +persistentManager.loading=Cargando [{0}] sesiones persistidas +persistentManager.serializeError=Error serializando Sesión [{0}]: [{1}] +persistentManager.storeKeysException=Imposible determinar la lista de IDs de sesiones en la tienda de sesiones, asumiendo que la tienda esta vacia +persistentManager.storeSizeException=No se puede determinar el numero de sesiones en el almacenamiento de sesiones, asumiendo que el almacenamiento esta vacío +persistentManager.swapIn=Intercambiando sesión [{0}] a dentro desde Almacén +persistentManager.swapMaxIdle=Intercambiando sesión [{0}] a fuera a Almacén, ociosa durante [{1}] segundos +persistentManager.swapTooManyActive=Intercambiando sesión [{0}] a fuera, ociosa durante [{1}] segundos: Demasiadas sesiones activas +persistentManager.tooManyActive=Demasiadas sesiones activas, [{0}], buscando sesiones ociosas para intercambiar +persistentManager.unloading=Salvando [{0}] sesiones persistidas + +standardManager.deletePersistedFileFail=Imposible borrar [{0}] luego de leer las sessiones persistentes. La prensencia continua de este archivo causará que los intentos futuros de sesiones persistentes fallen. +standardManager.loading=Cargando sesiones persistidas desde [{0}] +standardManager.loading.exception=Exception al cargar sesiones persistidas +standardManager.managerLoad=Excepción cargando sesiones desde almacenamiento persistente +standardManager.managerUnload=Excepción descargando sesiones a almacenamiento persistente +standardManager.unloading=Salvando sesiones persistidas a [{0}] + +standardSession.attributeEvent=El oyente de eventos de atributo de Sesión lanzó una excepción +standardSession.bindingEvent=El oyente de eventos de ligado de Sesión lanzó una excepción +standardSession.getAttribute.ise=getAttribute: La Sesión ya ha sido invalidada +standardSession.getAttributeNames.ise=getAttributeNames: La Sesión ya ha sido invalidada +standardSession.getCreationTime.ise=getCreationTime: La Sesión ya ha sido invalidada +standardSession.getLastAccessedTime.ise=getLastAccessedTime: La Sesión ya ha sido invalidada +standardSession.getThisAccessedTime.ise=getThisAccessedTime: La Sesión ya ha sido invalidada +standardSession.getValueNames.ise=getValueNames: La Sesión ya ha sido invalidada +standardSession.invalidate.ise=invalidate: La Sesión ya ha sido invalidada +standardSession.isNew.ise=isNew: La Sesión ya ha sido invalidada +standardSession.notSerializable=No puedo serializar atributo de sesión [{0}] para sesión [{1}] +standardSession.removeAttribute.ise=removeAttribute: La Sesión ya ha sido invalidada +standardSession.sessionEvent=El oyente de evento de Sesión lanzó una execpción +standardSession.setAttribute.iae=setAttribute: Atributo [{0}] no serializable +standardSession.setAttribute.ise=setAttribute: La Sesión ya ha sido invalidada +standardSession.setAttribute.namenull=setAttribute: el nuevo parámetro no puede ser nulo diff --git a/java/org/apache/catalina/session/LocalStrings_fr.properties b/java/org/apache/catalina/session/LocalStrings_fr.properties new file mode 100644 index 0000000..3700fc1 --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_fr.properties @@ -0,0 +1,103 @@ +# 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. + +dataSourceStore.SQLException=Erreur SQL [{0}] +dataSourceStore.checkConnectionDBClosed=La connexion à la base de données est nulle ou a été trouvée fermée. Tentative de réouverture. +dataSourceStore.checkConnectionDBReOpenFail=La tentative de réouverture de la base de données a échoué. La base de données est peut-être arrêtée. +dataSourceStore.checkConnectionSQLException=Une exception SQL s''est produite [{0}] +dataSourceStore.close=Exception lors de la fermeture de la connection vers la base de donnée [{0}] +dataSourceStore.commitSQLException=Une SQLException a été retournée lors du commit de la connection avant sa fermeture +dataSourceStore.loading=Chargement de la Session [{0}] depuis la base de données [{1}] +dataSourceStore.missingDataSource=Aucune source de données n'est disponible +dataSourceStore.missingDataSourceName=Aucun nom JNDI valide n'a été donné. +dataSourceStore.noObject=Aucun objet de persistence de données trouvé pour la session [{0}] +dataSourceStore.removing=Retrait de la Session [{0}] de la base de données [{1}] +dataSourceStore.saving=Sauvegarde de la Session [{0}] vers la base de données [{1}] +dataSourceStore.wrongDataSource=Impossible d''ouvrir la DataSource JNDI [{0}] + +fileStore.createFailed=Impossible de créer le répertoire [{0}] pour stocker les données de session +fileStore.deleteFailed=Impossible d''effacer le fichier [{0}] qui empêche la création du support de stockage de sessions +fileStore.deleteSessionFailed=Impossible d''effacer le fichier [{0}] qui n''est plus nécessaire +fileStore.invalid=Fichier de persistance [{0}] invalide pour la session avec id [{1}] +fileStore.loading=Chargement de la Session [{0}] depuis le fichier [{1}] +fileStore.noFile=Le fichier de persistence [{1}] pour la session [{0}] n''a pas été trouvé +fileStore.removing=Retrait de la Session [{0}] du fichier [{1}] +fileStore.saving=Sauvegarde de la Session [{0}] vers le fichier [{1}] + +managerBase.container.noop=Les gestionnaires de session ajoutés à des conteneurs qui ne sont pas des contextes ne seront jamais utilisés +managerBase.contextNull=Le contexte (Context) doit être mis à une valeur non-nulle avant l'usage du Manager +managerBase.createSession.ise="createSession" : Trop de sessions actives +managerBase.sessionAttributeNameFilter=L''attribut de session nommé [{0}] sera sauté car il ne correspond pas au filtre sur les noms [{1}] +managerBase.sessionAttributeValueClassNameFilter=L''attribut de session nommé [{0}] a été passé parce que le type [{1}] de la valeur ne correspond pas au filtre [{2}] +managerBase.sessionNotFound=La session [{0}] n''a pas été trouvée +managerBase.sessionTimeout=Réglage du délai d''inactivité (timeout) de session invalide [{0}] +managerBase.setContextNotNew=Il est illégal d'appeler setContext() pour changer le contexte associé avec un gestionnaire (Manager) si le genstionnaire n'est pas dans l'état nouveau + +persistentManager.backupMaxIdle=Sauvegarde de la session [{0}] vers le stockage (Store), en attente pour [{1}] secondes +persistentManager.deserializeError=Erreur lors de la désérialisation de la session [{0}] +persistentManager.isLoadedError=Erreur en vérifiant si la session [{0}] est chargée en mémoire +persistentManager.loading=Chargement de [{0}] sessions persistantes +persistentManager.noStore=Aucun stockage (Store) n'a été configuré, la persistence est désactivée +persistentManager.removeError=Erreur en enlevant la session [{0}] du stockage +persistentManager.serializeError=Erreur lors de la sérialisation de la session [{0}] : [{1}] +persistentManager.storeClearError=Erreur en supprimant toutes les sessions du stockage +persistentManager.storeKeysException=Incapacité de déterminer la liste des ID de session, pour les sessions dans le magasin de sessions. Supposant le magasin vide. +persistentManager.storeLoadError=Erreur en déplaçant les sessions à partir du stockage +persistentManager.storeLoadKeysError=Erreur lors du chargement des clés des sessions du stockage +persistentManager.storeSizeException=Impossible de déterminer le nombre de sessions dans le magasin de sessions, le magasin doit être vide. +persistentManager.swapIn=Basculement depuis le stockage (swap in) de la session [{0}] +persistentManager.swapInException=Exception dans la Store lors du swapIn : [{0}] +persistentManager.swapInInvalid=La session échangée [{0}] est invalide +persistentManager.swapMaxIdle=Basculement de la session [{0}] vers le stockage (Store), en attente pour [{1}] secondes +persistentManager.swapTooManyActive=Basculement vers stockage (swap out) de la session [{0}], en attente pour [{1}] secondes trop de sessions actives +persistentManager.tooManyActive=Trop de sessions actives, [{0}], à la recherche de sessions en attente pour basculement vers stockage (swap out) +persistentManager.unloading=Sauvegarde de [{0}] sessions persistantes + +standardManager.deletePersistedFileFail=Impossible de supprimer [{0}] après avoir lu les sessions persistées, cela pourrait empêcher la future persistance des sessions +standardManager.expiringSessions=Expiration de [{0}] sessions persistées +standardManager.loading=Chargement des sessions qui ont persisté depuis [{0}] +standardManager.loading.exception="Exception" lors du chargement de sessions persistantes +standardManager.managerLoad=Exception au chargement des sessions depuis le stockage persistant (persistent storage) +standardManager.managerUnload=Exception au déchargement des sessions vers le stockage persistant (persistent storage) +standardManager.noFile=Le fichier de données de persistence [{0}] n''a pas été trouvé +standardManager.unloading=Sauvegarde des sessions ayant persisté vers [{0}] +standardManager.unloading.debug=Déchargement des session persistées +standardManager.unloading.nosessions=Aucune session persistée à décharger + +standardSession.attributeEvent=L'écouteur d'évènement Attribut de Session (attribute event listener) a généré une exception +standardSession.bindingEvent=L'écouteur d'évènements d'association de session a renvoyé une exception +standardSession.getAttribute.ise="getAttribute" : Session déjà invalidée +standardSession.getAttributeNames.ise="getAttributeNames" : Session déjà invalidée +standardSession.getCreationTime.ise="getCreationTime" : Session déjà invalidée +standardSession.getIdleTime.ise=getIdleTime : la session a déjà été invalidée +standardSession.getLastAccessedTime.ise="getLastAccessedTime" : Session déjà invalidée +standardSession.getThisAccessedTime.ise="getThisAccessedTime" : Session déjà invalidée +standardSession.getValueNames.ise="getValueNames" : Session déjà invalidée +standardSession.invalidate.ise="invalidate" : Session déjà invalidée +standardSession.isNew.ise="isNew" : Session déjà invalidée +standardSession.logoutfail=Exception lors de la déconnection de l'utilisateur, lors de l'expiration de la session +standardSession.notDeserializable=Impossible de désérialiser l''attribut de session [{0}] pour la session [{1}] +standardSession.notSerializable=Impossible de sérialiser l''attribut de session [{0}] pour la session [{1}] +standardSession.principalNotDeserializable=Impossible de désérialiser l''objet Principal pour la session [{0}] +standardSession.principalNotSerializable=Impossible de sérialiser l''objet Principal pour la session [{0}] +standardSession.removeAttribute.ise="removeAttribute" : Session déjà invalidée +standardSession.sessionEvent=L'écouteur d'évènement de session (session event listener) a généré une exception +standardSession.setAttribute.iae="setAttribute" : Attribut [{0}] non sérialisable +standardSession.setAttribute.ise="setAttribute" : Session déjà invalidée +standardSession.setAttribute.namenull="setAttribute" : le nom de paramètre ne peut être nul + +store.expireFail=Erreur de traitement de l''expiration de la session pour la clé [{0}] +store.keysFail=Erreur lors de la récupération des clés +store.removeFail=Erreur lors du retrait de la clé [{0}] diff --git a/java/org/apache/catalina/session/LocalStrings_ja.properties b/java/org/apache/catalina/session/LocalStrings_ja.properties new file mode 100644 index 0000000..f4bb2eb --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_ja.properties @@ -0,0 +1,103 @@ +# 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. + +dataSourceStore.SQLException=SQLエラー [{0}] +dataSourceStore.checkConnectionDBClosed=データベース接続ãŒnullã§ã‚ã‚‹ã‹ã€ã‚¯ãƒ­ãƒ¼ã‚ºã•ã‚Œã¦ã„ã‚‹ã®ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚å†ã‚ªãƒ¼ãƒ—ンã—ã¦ãã ã•ã„。 +dataSourceStore.checkConnectionDBReOpenFail=データベースã®å†ã‚ªãƒ¼ãƒ—ンãŒå¤±æ•—ã—ã¾ã—ãŸã€‚データベースãŒãƒ€ã‚¦ãƒ³ã—ã¦ã„ã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。 +dataSourceStore.checkConnectionSQLException=SQL例外ãŒç™ºç”Ÿã—ã¾ã—㟠[{0}] +dataSourceStore.close=データベース接続 [{0}] をクローズ中ã®ä¾‹å¤–ã§ã™ +dataSourceStore.commitSQLException=クローズå‰ã®ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹æŽ¥ç¶šã®ã‚³ãƒŸãƒƒãƒˆä¸­ã«SQL例外ãŒç™ºç”Ÿã—ã¾ã—㟠+dataSourceStore.loading=セッション [{0}] をデータベース [{1}] ã‹ã‚‰ãƒ­ãƒ¼ãƒ‰ã—ã¾ã™ +dataSourceStore.missingDataSource=利用å¯èƒ½ãªãƒ‡ãƒ¼ã‚¿ã‚½ãƒ¼ã‚¹ãŒã‚ã‚Šã¾ã›ã‚“ +dataSourceStore.missingDataSourceName=指定ã•ã‚ŒãŸ JNDI åã¯æ­£å¸¸ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +dataSourceStore.noObject=セッション [{0}] ã®æ°¸ç¶šåŒ–データ オブジェクトãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+dataSourceStore.removing=セッション [{0}] をデータベース [{1}] ã‹ã‚‰å‰Šé™¤ã—ã¾ã™ +dataSourceStore.saving=セッション [{0}] をデータベース [{1}] ã«ä¿å­˜ã—ã¾ã™ +dataSourceStore.wrongDataSource=JNDIデータソース [{0}] ã‚’é–‹ãã“ã¨ãŒã§ãã¾ã›ã‚“ + +fileStore.createFailed=セッションデータを格ç´ã™ã‚‹ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª[{0}]を作æˆã§ãã¾ã›ã‚“ +fileStore.deleteFailed=ファイル [{0}] を削除ã§ããªã‹ã£ãŸãŸã‚ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã‚’作æˆã§ãã¾ã›ã‚“。 +fileStore.deleteSessionFailed=ä¸è¦ã«ãªã£ãŸãƒ•ã‚¡ã‚¤ãƒ«[{0}]を削除ã§ãã¾ã›ã‚“ +fileStore.invalid=セッションID [{1}]ã®æ°¸ç¶šãƒ•ã‚¡ã‚¤ãƒ«[{0}]ãŒç„¡åŠ¹ã§ã™ +fileStore.loading=セッション [{0}] をファイル [{1}] ã‹ã‚‰ãƒ­ãƒ¼ãƒ‰ã—ã¾ã™ +fileStore.noFile=セッション ID [{0}] ã®æ°¸ç¶šãƒ•ã‚¡ã‚¤ãƒ« [{1}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+fileStore.removing=セッション [{0}] をファイル [{1}] ã‹ã‚‰å‰Šé™¤ã—ã¾ã™ +fileStore.saving=セッション [{0}] をファイル [{1}] ã«ä¿å­˜ã—ã¾ã™ + +managerBase.container.noop=Contexts 以外ã®ã‚³ãƒ³ãƒ†ãƒŠã«è¿½åŠ ã•ã‚ŒãŸManagerã¯æ±ºã—ã¦ä½¿ç”¨ã•ã‚Œã¾ã›ã‚“。 +managerBase.contextNull=マãƒãƒ¼ã‚¸ãƒ£ãƒ¼ãŒä½¿ã†å‰ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã¯ null ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +managerBase.createSession.ise=createSession: アクティブセッションãŒå¤šã™ãŽã¾ã™ +managerBase.sessionAttributeNameFilter=åå‰ãƒ•ã‚£ãƒ«ã‚¿ [{1}] ã¨ä¸€è‡´ã—ãªã‹ã£ãŸãŸã‚ã€[{0}]ã¨ã„ã†ã‚»ãƒƒã‚·ãƒ§ãƒ³å±žæ€§ã‚’スキップã—ã¾ã—ãŸã€‚ +managerBase.sessionAttributeValueClassNameFilter=値タイプ [{1}] ãŒãƒ•ã‚£ãƒ«ã‚¿ [{2}] ã¨ä¸€è‡´ã—ãªã‹ã£ãŸãŸã‚ã€[{0}] ã¨ã„ã†åå‰ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³å±žæ€§ã‚’スキップã—ã¾ã—㟠+managerBase.sessionNotFound=セッション [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +managerBase.sessionTimeout=無効ãªã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆè¨­å®šã§ã™ [{0}] +managerBase.setContextNotNew=NEW 状態ã§ã¯ãªã„マãƒãƒ¼ã‚¸ãƒ£ãƒ¼ã«é–¢é€£ä»˜ã‘られ㟠Context を変更ã™ã‚‹ãŸã‚ã« setContext() を呼ã³å‡ºã™ã“ã¨ã¯ç¦æ­¢ã•ã‚Œã¦ã„ã¾ã™ã€‚ + +persistentManager.backupMaxIdle=[{1}]秒間アイドルã—ã¦ã„るセッション [{0}] ã‚’ä¿å­˜ã™ã‚‹ãŸã‚ã«ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—ã—ã¦ã„ã¾ã™ +persistentManager.deserializeError=セッション [{0}] をデシリアライズ中ã®ã‚¨ãƒ©ãƒ¼ +persistentManager.isLoadedError=セッション [{0}] ãŒãƒ¡ãƒ¢ãƒªãƒ¼ã«èª­ã¿è¾¼ã¿æ¸ˆã¿ã‹ãƒã‚§ãƒƒã‚¯ä¸­ã®ã‚¨ãƒ©ãƒ¼ +persistentManager.loading=[{0}] ã®æ°¸ç¶šåŒ–セッションをロードã—ã¾ã™ +persistentManager.noStore=ストアãŒæ§‹æˆã•ã‚Œã¦ãŠã‚‰ãšã€æ°¸ç¶šæ€§ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™ +persistentManager.removeError=ストアã‹ã‚‰ã‚»ãƒƒã‚·ãƒ§ãƒ³[{0}]削除中ã®ã‚¨ãƒ©ãƒ¼ +persistentManager.serializeError=セッション [{0}] をシリアライズ中ã®ã‚¨ãƒ©ãƒ¼ã§ã™: [{1}] +persistentManager.storeClearError=ストア上ã®å…¨ã‚»ãƒƒã‚·ãƒ§ãƒ³æ¶ˆåŽ»ä¸­ã®ã‚¨ãƒ©ãƒ¼ +persistentManager.storeKeysException=セッションストアã‹ã‚‰ã‚»ãƒƒã‚·ãƒ§ãƒ³IDã®ãƒªã‚¹ãƒˆã‚’å–å¾—ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚セッションストアãŒç©ºã®å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ +persistentManager.storeLoadError=ストアã‹ã‚‰ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¹ãƒ¯ãƒƒãƒ—イン中ã®ã‚¨ãƒ©ãƒ¼ +persistentManager.storeLoadKeysError=データストアã‹ã‚‰ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚­ãƒ¼ã‚’読ã¿è¾¼ã¿ä¸­ã®ã‚¨ãƒ©ãƒ¼ +persistentManager.storeSizeException=セッションストアã«æ ¼ç´ã—ã¦ã„るセッションã®ç·æ•°ã‚’確èªã§ããªã‹ã£ãŸãŸã‚ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¹ãƒˆã‚¢ã¯ç©ºã«ãªã£ã¦ã„ã‚‹ã‚‚ã®ã¨ã—ã¦æ‰±ã„ã¾ã™ +persistentManager.swapIn=セッション [{0}] をスワップインã—ã¦ã„ã¾ã™ +persistentManager.swapInException=スワップイン時ã®ã‚¹ãƒˆã‚¢å†…ã®ä¾‹å¤–:[{0}] +persistentManager.swapInInvalid=スワップã—ãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] ã¯ä¸æ­£ã§ã™ã€‚ +persistentManager.swapMaxIdle=[{1}]秒間アイドルã—ã¦ã„るセッション [{0}] ã‚’ä¿å­˜ã™ã‚‹ãŸã‚ã«ã‚¹ãƒ¯ãƒƒãƒ—ã—ã¦ã„ã¾ã™ +persistentManager.swapTooManyActive=セッションãŒå¤šã™ãŽã‚‹ã®ã§ã€[{1}]秒アイドルã—ã¦ã„るセッション [{0}] をスワップアウトã—ã¾ã™ +persistentManager.tooManyActive=アクティブセッションãŒå¤šã™ãŽã¾ã™ã€[{0}]ã€ã‚¹ãƒ¯ãƒƒãƒ—アウトã™ã‚‹ãŸã‚ã«ã‚¢ã‚¤ãƒ‰ãƒ«ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’探ã—ã¦ã„ã¾ã™ +persistentManager.unloading=[{0}] ã®æ°¸ç¶šåŒ–セッションをä¿å­˜ã—ã¾ã™ + +standardManager.deletePersistedFileFail=永続化セッションを読ã¿è¾¼ã‚“ã  [{0}] を削減ã§ãã¾ã›ã‚“。ファイルãŒæ®‹ã£ã¦ã„ã‚‹ã¨å°†æ¥ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®æ°¸ç¶šåŒ–ã«å¤±æ•—ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +standardManager.expiringSessions=[{0}] 個ã®æ°¸ç¶šã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒæœŸé™åˆ‡ã‚Œã«ãªã‚Šã¾ã™ +standardManager.loading=[{0}] ã‹ã‚‰æ°¸ç¶šåŒ–セッションをロードã—ã¦ã„ã¾ã™ +standardManager.loading.exception=æŒç¶šã•ã‚ŒãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’ロード中ã«ExceptionãŒç™ºç”Ÿã—ã¾ã—㟠+standardManager.managerLoad=永続記憶装置ã‹ã‚‰ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’ロード中ã®ä¾‹å¤–ã§ã™ +standardManager.managerUnload=永続記憶装置ã«ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’アンロード中ã®ä¾‹å¤–ã§ã™ +standardManager.noFile=永続化データファイル [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+standardManager.unloading=æŒç¶šã•ã‚ŒãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’ [{0}] ã«ä¿å­˜ã—ã¾ã™ +standardManager.unloading.debug=永続セッションã®ã‚¢ãƒ³ãƒ­ãƒ¼ãƒ‰ +standardManager.unloading.nosessions=アンロードã™ã‚‹æ°¸ç¶šã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒã‚ã‚Šã¾ã›ã‚“ + +standardSession.attributeEvent=セッション属性イベントリスナãŒä¾‹å¤–を投ã’ã¾ã—㟠+standardSession.bindingEvent=セッションãƒã‚¤ãƒ³ãƒ‡ã‚£ãƒ³ã‚°ã‚¤ãƒ™ãƒ³ãƒˆãƒªã‚¹ãƒŠãŒä¾‹å¤–を投ã’ã¾ã—㟠+standardSession.getAttribute.ise=getAttribute: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.getAttributeNames.ise=getAttributeNames: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.getCreationTime.ise=getCreationTime: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.getIdleTime.ise=getIdleTime: 無効ãªã‚»ãƒƒã‚·ãƒ§ãƒ³ã§ã™ã€‚ +standardSession.getLastAccessedTime.ise=getLastAccessedTime: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.getThisAccessedTime.ise=getThisAccessedTime: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.getValueNames.ise=getValueNames: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.invalidate.ise=invalidate: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.isNew.ise=isNew: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.logoutfail=有効期é™ã‚’超éŽã—ãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ã‹ã‚‰ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒ­ã‚°ã‚¢ã‚¦ãƒˆã™ã‚‹æ™‚ã€ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +standardSession.notDeserializable=セッション [{1}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³å±žæ€§ [{0}] をデシリアライズã§ãã¾ã›ã‚“ +standardSession.notSerializable=セッション [{1}] ã®ãŸã‚ã«ã‚»ãƒƒã‚·ãƒ§ãƒ³å±žæ€§ [{0}] をシリアライズã§ãã¾ã›ã‚“ +standardSession.principalNotDeserializable=セッション [{0}] ã®ãƒ—リンシパルオブジェクトをデシリアライズã§ãã¾ã›ã‚“ +standardSession.principalNotSerializable=セッション [{0}] ã®ãƒ—リンシパルオブジェクトをシリアライズã§ãã¾ã›ã‚“ +standardSession.removeAttribute.ise=removeAttribute: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.sessionEvent=セッションイベントリスナãŒä¾‹å¤–を投ã’ã¾ã—㟠+standardSession.setAttribute.iae=setAttribute: シリアライズã§ããªã„属性ã§ã™ +standardSession.setAttribute.ise=setAttribute: セッションã¯æ—¢ã«ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã™ +standardSession.setAttribute.namenull=setAttribute: nameパラメタã¯nullã§ã‚ã£ã¦ã¯ã„ã‘ã¾ã›ã‚“ + +store.expireFail=キー [{0}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³æœŸé™åˆ‡ã‚Œã®å‡¦ç†ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+store.keysFail=キーå–得エラー +store.removeFail=キー [{0}] 削除中ã®ã‚¨ãƒ©ãƒ¼ diff --git a/java/org/apache/catalina/session/LocalStrings_ko.properties b/java/org/apache/catalina/session/LocalStrings_ko.properties new file mode 100644 index 0000000..568c1cf --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_ko.properties @@ -0,0 +1,94 @@ +# 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. + +dataSourceStore.SQLException=SQL 오류 [{0}] +dataSourceStore.checkConnectionDBClosed=ë°ì´í„°ë² ì´ìŠ¤ ì—°ê²°ì´ ë„ì´ê±°ë‚˜ 닫힌 ìƒíƒœìž…니다. 다시 열려고 ì‹œë„합니다. +dataSourceStore.checkConnectionDBReOpenFail=ë°ì´í„°ë² ì´ìŠ¤ì— 대해 다시 ì—°ê²°ì„ ë§ºì§€ 못했습니다. ë°ì´í„°ë² ì´ìŠ¤ê°€ 다운ë˜ì—ˆì„ 수 있습니다. +dataSourceStore.checkConnectionSQLException=SQL 예외 ë°œìƒ [{0}] +dataSourceStore.close=ë°ì´í„°ë² ì´ìŠ¤ ì—°ê²° [{0}]ì„(를) 닫는 ë™ì•ˆ 예외 ë°œìƒ +dataSourceStore.commitSQLException=ë°ì´í„°ë² ì´ìŠ¤ ì—°ê²°ì„ ë‹«ê¸° ì „, ì»¤ë°‹ì„ ì‹œë„하는 중 SQLException ë°œìƒ +dataSourceStore.loading=ë°ì´í„°ë² ì´ìŠ¤ [{1}](으)로부터 세션 [{0}]ì„(를) 로드합니다. +dataSourceStore.missingDataSource=DataSource를 사용할 수 없습니다. +dataSourceStore.missingDataSourceName=유효한 JNDI ì´ë¦„ì´ ì£¼ì–´ì§€ì§€ 않았습니다. +dataSourceStore.removing=ë°ì´í„°ë² ì´ìŠ¤ [{1}]ì—ì„œ 세션 [{0}]ì„(를) 제거합니다. +dataSourceStore.saving=세션 [{0}]ì„(를) ë°ì´í„°ë² ì´ìŠ¤ [{1}]ì— ì €ìž¥í•©ë‹ˆë‹¤. +dataSourceStore.wrongDataSource=JNDI DataSource [{0}]ì„(를) ì—´ 수 없습니다. + +fileStore.createFailed=세션 ë°ì´í„° 저장소를 위한 디렉토리[{0}]ì„(를) ìƒì„±í•  수 없습니다. +fileStore.deleteFailed=íŒŒì¼ [{0}]ì„(를) 삭제할 수 없습니다. ì´ëŠ” 세션 저장소 ìœ„ì¹˜ì˜ ìƒì„±ì„ 방해하고 있습니다. +fileStore.deleteSessionFailed=ë” ì´ìƒ 필요하지 ì•Šì€ íŒŒì¼ [{0}]ì„(를) 삭제할 수 없습니다. +fileStore.invalid=세션 ID [{1}]ì„(를) 위한 세션 저장소 íŒŒì¼ [{0}]ì´(ê°€) 유효하지 않습니다. +fileStore.loading=íŒŒì¼ [{1}](으)로부터 세션 [{0}]ì„(를) 로드합니다. +fileStore.removing=íŒŒì¼ [{1}]ì— ì €ìž¥ëœ ì„¸ì…˜ [{0}]ì„(를) 제거합니다. +fileStore.saving=세션 [{0}]ì„(를) íŒŒì¼ [{1}]ì— ì €ìž¥í•©ë‹ˆë‹¤. + +managerBase.container.noop=컨í…ìŠ¤íŠ¸ë“¤ì´ ì•„ë‹Œ, 컨테ì´ë„ˆë“¤ì— ì¶”ê°€ëœ ë§¤ë‹ˆì €ë“¤ì€ ì „í˜€ 사용ë˜ì§€ ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. +managerBase.contextNull=매니저가 사용ë˜ê¸° ì „ì—, 컨í…스트가 반드시 ë„ì´ ì•„ë‹Œ 값으로 설정ë˜ì–´ì•¼ 합니다. +managerBase.createSession.ise=createSession: í™œì„±í™”ëœ ì„¸ì…˜ì´ ë„ˆë¬´ 많습니다. +managerBase.sessionAttributeNameFilter=ì´ë¦„ í•„í„° [{1}]와(ê³¼) 부합ë˜ì§€ 않기 때문ì—, [{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì„¸ì…˜ ì†ì„±ì„ 건너뛰었습니다. +managerBase.sessionAttributeValueClassNameFilter=ê°’ì˜ íƒ€ìž… [{1}]ì´(ê°€) í•„í„° [{2}]와(ê³¼) 부합하지 않기 때문ì—, [{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì„¸ì…˜ ì†ì„±ì„ 건너뛰었습니다. +managerBase.sessionNotFound=세션 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없었습니다. +managerBase.sessionTimeout=유효하지 ì•Šì€, 세션 제한 시간 초과 설정입니다: [{0}] +managerBase.setContextNotNew=ë§Œì¼ ë§¤ë‹ˆì €ê°€ NEW ìƒíƒœì— 있지 않다면, 매니저와 ì—°ê´€ëœ ì»¨í…스트를 변경하기 위해 setContext()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. + +persistentManager.backupMaxIdle=세션 [{0}]ì„(를) 세션 ì €ìž¥ì†Œì— ë°±ì—…í•©ë‹ˆë‹¤. [{1}]ì´ˆ ë™ì•ˆ 유휴 ìƒíƒœì˜€ìŠµë‹ˆë‹¤. +persistentManager.deserializeError=세션 [{0}]ì„(를) ì—­ì§ë ¬í™”하는 중 오류 ë°œìƒ +persistentManager.isLoadedError=세션 [{0}]ì´(ê°€) ë©”ëª¨ë¦¬ì— ë¡œë“œë˜ì—ˆëŠ”지 ì ê²€ 중 오류 ë°œìƒ +persistentManager.loading=[{0}]ê°œì˜ ì €ìž¥ëœ ì„¸ì…˜ë“¤ì„ ë¡œë“œí•©ë‹ˆë‹¤. +persistentManager.removeError=세션 [{0}]ì„(를) 저장소로부터 제거하는 중 오류 ë°œìƒ +persistentManager.serializeError=ì„¸ì…˜ì„ ì§ë ¬í™”하는 중 오류 ë°œìƒ [{0}]: [{1}] +persistentManager.storeClearError=저장소로부터 모든 ì„¸ì…˜ë“¤ì„ í•´ì œí•˜ëŠ” 중 오류 ë°œìƒ +persistentManager.storeKeysException=세션 ì €ìž¥ì†Œì— ìžˆëŠ” ì„¸ì…˜ë“¤ì˜ ID 목ë¡ì„ ê²°ì •í•  수 없습니다. ì•„ë§ˆë„ ì„¸ì…˜ 저장소가 비어 있는 것 같습니다. +persistentManager.storeLoadError=저장소로부터 ì„¸ì…˜ë“¤ì„ ë©”ëª¨ë¦¬ë¡œ 로드하는 중 오류 ë°œìƒ +persistentManager.storeLoadKeysError=저장소로부터 세션 í‚¤ë“¤ì„ ë¡œë“œí•˜ëŠ” 중 오류 ë°œìƒ +persistentManager.storeSizeException=세션 ì €ìž¥ì†Œì— ì–¼ë§ˆë‚˜ ë§Žì€ ì„¸ì…˜ì´ ì¡´ìž¬í•˜ëŠ”ì§€ 알아낼 수 없습니다. ì•„ë§ˆë„ ì„¸ì…˜ 저장소가 비어 있는 것 같습니다. +persistentManager.swapIn=저장소로부터 세션 [{0}]ì„(를) 다시 로드하여 활성화시킵니다. +persistentManager.swapInException=ì €ìž¥ì†Œì— ì €ìž¥ëœ ì„¸ì…˜ì„ ë©”ëª¨ë¦¬ë¡œ 로드하는 중, 저장소ì—ì„œ 예외 ë°œìƒ: [{0}] +persistentManager.swapInInvalid=세션 저장소로부터 ë¡œë“œëœ ì„¸ì…˜ [{0}]ì€(는) 유효하지 않습니다. +persistentManager.swapMaxIdle=[{1}]ì´ˆ ë™ì•ˆ 유휴 ìƒíƒœì— 있ë˜, 세션 [{0}]ì„(를) 저장소로 옮ê¹ë‹ˆë‹¤. +persistentManager.swapTooManyActive=[{1}]ì´ˆ ë™ì•ˆ 비활성화 ìƒíƒœì— ìžˆë˜ ì„¸ì…˜ [{0}]ì„(를) 매니저로부터 저장소로 ì´ë™í•©ë‹ˆë‹¤. 너무 ë§Žì€ ì„¸ì…˜ë“¤ì´ í™œì„±í™”ë˜ì–´ 있습니다. +persistentManager.tooManyActive=í™œì„±í™”ëœ ì„¸ì…˜ë“¤ì´ ë„ˆë¬´ 많습니다: [{0}]. 세션 저장소로 내보낼 만한 유휴 ì„¸ì…˜ë“¤ì„ ì°¾ìŠµë‹ˆë‹¤. +persistentManager.unloading=[{0}]ê°œì˜ ì„¸ì…˜ë“¤ì„ ì €ìž¥í•©ë‹ˆë‹¤. + +standardManager.deletePersistedFileFail=ì €ìž¥ëœ ì„¸ì…˜ë“¤ì„ ì½ì€ 후 [{0}]ì„(를) 삭제할 수 없습니다. ì´ íŒŒì¼ì´ ê³„ì† ì¡´ìž¬í•œë‹¤ë©´, ì´í›„ ì„¸ì…˜ì„ ì €ìž¥í•˜ë ¤ëŠ” ì‹œë„ë“¤ì´ ì´ë¡œ ì¸í•´ 실패할 수 있습니다. +standardManager.loading=[{0}](으)로부터 ì €ìž¥ëœ ì„¸ì…˜ë“¤ì„ ë¡œë“œí•©ë‹ˆë‹¤. +standardManager.loading.exception=ì €ìž¥ëœ ì„¸ì…˜ë“¤ì„ ë¡œë“œí•˜ëŠ” 중 예외 ë°œìƒ +standardManager.managerLoad=세션 저장소로부터 ì„¸ì…˜ë“¤ì„ ë¡œë“œí•˜ëŠ” 중 예외 ë°œìƒ +standardManager.managerUnload=ì„¸ì…˜ë“¤ì„ ì €ìž¥ì†Œë¡œ 언로드하는 중 예외 ë°œìƒ +standardManager.unloading=ì„¸ì…˜ë“¤ì„ [{0}]ì— ì €ìž¥í•©ë‹ˆë‹¤. +standardManager.unloading.debug=ì €ìž¥ëœ ì„¸ì…˜ë“¤ì„ ì–¸ë¡œë“œí•©ë‹ˆë‹¤. +standardManager.unloading.nosessions=언로드할 수 있는 ì €ìž¥ëœ ì„¸ì…˜ë“¤ì´ ì—†ìŠµë‹ˆë‹¤. + +standardSession.attributeEvent=세션 ì†ì„± ì´ë²¤íŠ¸ 리스너가 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardSession.bindingEvent=세션 ë°”ì¸ë”© ì´ë²¤íŠ¸ 리스너가 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardSession.getAttribute.ise=getAttribute: ì„¸ì…˜ì´ ì´ë¯¸ 무효화ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.getAttributeNames.ise=getAttributeNames: ì„¸ì…˜ì´ ì´ë¯¸ 무효화ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.getCreationTime.ise=getCreationTime: ì„¸ì…˜ì´ ì´ë¯¸ 무효화ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.getIdleTime.ise=getIdleTime: ì„¸ì…˜ì´ ì´ë¯¸ 무효화 ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.getLastAccessedTime.ise=getLastAccessedTime: ì„¸ì…˜ì´ ì´ë¯¸ 무효화 ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.getThisAccessedTime.ise=getThisAccessedTime: ì„¸ì…˜ì´ ì´ë¯¸ 만료ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.getValueNames.ise=getValueNames: ì„¸ì…˜ì´ ì´ë¯¸ 무효화 ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.invalidate.ise=invalidate: ì„¸ì…˜ì´ ì´ë¯¸ 무효화ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.isNew.ise=isNew: ì„¸ì…˜ì´ ì´ë¯¸ 무효화 ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.logoutfail=ì„¸ì…˜ì„ ë§Œë£Œì‹œí‚¬ ë•Œ, 사용ìžë¥¼ 로그아웃 하는 중 예외 ë°œìƒ +standardSession.notDeserializable=세션 [{1}]ì„(를) 위한 세션 ì†ì„± [{0}]ì„(를) ì—­ì§ë ¬í™”í•  수 없습니다. +standardSession.notSerializable=세션 [{1}]ì„(를) 위한 세션 ì†ì„± [{0}]ì„(를) ì§ë ¬í™”í•  수 없습니다. +standardSession.principalNotDeserializable=세션 [{0}]ì„(를) 위한 Principal ê°ì²´ë¥¼ ì—­ì§ë ¬í™”í•  수 없습니다. +standardSession.principalNotSerializable=세션 [{0}]ì„(를) 위한 Principal ê°ì²´ë¥¼ ì§ë ¬í™”í•  수 없습니다. +standardSession.removeAttribute.ise=removeAttribute: ì„¸ì…˜ì´ ì´ë¯¸ 무효화ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.sessionEvent=세션 ì´ë²¤íŠ¸ 리스너가 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +standardSession.setAttribute.iae=setAttribute: ì§ë ¬í™”í•  수 없는 ì†ì„± [{0}] +standardSession.setAttribute.ise=setAttribute: 세션 [{0}]ì´(ê°€) ì´ë¯¸ 무효화ë˜ì—ˆìŠµë‹ˆë‹¤. +standardSession.setAttribute.namenull=setAttribute: name 파ë¼ë¯¸í„°ëŠ” ë„ì¼ ìˆ˜ 없습니다. diff --git a/java/org/apache/catalina/session/LocalStrings_pt_BR.properties b/java/org/apache/catalina/session/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..459885e --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_pt_BR.properties @@ -0,0 +1,19 @@ +# 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. + +persistentManager.isLoadedError=Erro ao checar ser a sessão [{0}] foi carregada na memória +persistentManager.removeError=Erro ao remover a sessão [{0}] do armazenamento +persistentManager.storeSizeException=Impossível determinar o número de sessões no cache de sessão, assumindo que o cache está vazio +persistentManager.swapTooManyActive=Trocando sessão [{0}], ocioso por [{1}] segundos, há muitas sessões ativas. diff --git a/java/org/apache/catalina/session/LocalStrings_ru.properties b/java/org/apache/catalina/session/LocalStrings_ru.properties new file mode 100644 index 0000000..825a717 --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_ru.properties @@ -0,0 +1,21 @@ +# 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. + +dataSourceStore.removing=Удаление СеÑÑии [{0}] в базе данных [{1}] +dataSourceStore.saving=Сохранение Session [{0}] в базу данных [{1}] + +managerBase.contextNull=Перед иÑпользованием Менеджера контекÑту необходимо приÑвоить не пуÑтое значение + +persistentManager.deserializeError=Ошибка деÑериализации СеÑÑии [{0}] diff --git a/java/org/apache/catalina/session/LocalStrings_zh_CN.properties b/java/org/apache/catalina/session/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..58cd3bd --- /dev/null +++ b/java/org/apache/catalina/session/LocalStrings_zh_CN.properties @@ -0,0 +1,94 @@ +# 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. + +dataSourceStore.SQLException=SQL错误[{0}] +dataSourceStore.checkConnectionDBClosed=æ•°æ®åº“连接为空或已关闭。正在å°è¯•é‡æ–°è¿žæŽ¥ã€‚ +dataSourceStore.checkConnectionDBReOpenFail=é‡æ–°æ‰“开数æ®åº“失败,数æ®åº“å¯èƒ½å·²ç»å®•æœºã€‚ +dataSourceStore.checkConnectionSQLException=å‘生 SQL 异常 [{0}] +dataSourceStore.close=关闭数æ®åº“连接[{0}]æ—¶å‘生异常 +dataSourceStore.commitSQLException=关闭å‰æ交连接的SQLException +dataSourceStore.loading=正在从数æ®åº“[{1}]加载会è¯[{0}] +dataSourceStore.missingDataSource=没有å¯ç”¨çš„æ•°æ®æº +dataSourceStore.missingDataSourceName=没有给出有效的 JNDI å称。 +dataSourceStore.removing=å·²ç»åˆ é™¤æ•°æ®åº“ [{1}] ä¸Šçš„ä¼šè¯ [{0}] +dataSourceStore.saving=ä¿å­˜Session [{0}] 到数æ®åº“ [{1}] +dataSourceStore.wrongDataSource=无法打开 JNDI æ•°æ®æº [{0}] + +fileStore.createFailed=无法创建用于存储会è¯æ•°æ®çš„目录[{0}]。 +fileStore.deleteFailed=无法删除阻止创建会è¯å­˜å‚¨ä½ç½®çš„文件 [{0}] +fileStore.deleteSessionFailed=无法删除ä¸å†éœ€è¦çš„文件[{0}] +fileStore.invalid=无效的æŒä¹…化文件[{0}],会è¯ID为[{1}] +fileStore.loading=正在从文件[{1}]加载会è¯[{0}] +fileStore.removing=正在删除文件[{1}]处的会è¯[{0}] +fileStore.saving=ä¿å­˜ä¼šè¯[{0}]到文件[{1}] + +managerBase.container.noop=添加到上下文以外的容器的管ç†å™¨å°†æ°¸è¿œä¸ä¼šè¢«ä½¿ç”¨ +managerBase.contextNull=使用 Manager 之å‰ï¼Œå¿…须将 Context è®¾ç½®ä¸ºéž null 值 +managerBase.createSession.ise=createSession:活跃session过多 +managerBase.sessionAttributeNameFilter=已跳过å为[{0}]的会è¯å±žæ€§ï¼Œå› ä¸ºå®ƒä¸Žå称筛选器[{1}]ä¸åŒ¹é…。 +managerBase.sessionAttributeValueClassNameFilter=已跳过å为[{0}]的会è¯å±žæ€§ï¼Œå› ä¸ºå€¼ç±»åž‹[{1}]与筛选器[{2}]ä¸åŒ¹é… +managerBase.sessionNotFound=找ä¸åˆ°ä¼šè¯ [{0}] +managerBase.sessionTimeout=无效的会è¯è¶…时设置[{0}] +managerBase.setContextNotNew=如果Manager未处于NEW状æ€ï¼Œåˆ™è°ƒç”¨setContext()以更改与Managerå…³è”çš„Context是éžæ³•çš„ + +persistentManager.backupMaxIdle=正在将会è¯[{0}]备份到存储区,空闲时间为[{1}]秒。 +persistentManager.deserializeError=错误ååºåˆ—化会è¯[{0}]: [{1}] +persistentManager.isLoadedError=检查内存中是å¦åŠ è½½äº†ä¼šè¯[{0}]时出错 +persistentManager.loading=正在加载[{0}]æŒä¹…åŒ–ä¼šè¯ +persistentManager.removeError=从存储中删除会è¯[{0}]时出错 +persistentManager.serializeError=错误的åºåˆ—åŒ–ä¼šè¯ [{0}]:[{1}] +persistentManager.storeClearError=清除存储区中的所有会è¯æ—¶å‡ºé”™ +persistentManager.storeKeysException=ä¸èƒ½ä»Ž session存储中获å–session ID 的列表,å‡è®¾å­˜å‚¨ä¸ºç©º +persistentManager.storeLoadError=从存储区交æ¢ä¼šè¯æ—¶å‡ºé”™ +persistentManager.storeLoadKeysError=从存储加载会è¯å¯†é’¥æ—¶å‡ºé”™ +persistentManager.storeSizeException=无法确定 session 存储区的会è¯æ•°ï¼Œå‡å®šå­˜å‚¨åŒºä¸ºç©º +persistentManager.swapIn=在表å•å­˜å‚¨ä¸­,交æ¢ä¼šè¯[{0}] +persistentManager.swapInException=交æ¢æœŸé—´å­˜å‚¨åŒºä¸­å‡ºçŽ°å¼‚常:[{0}] +persistentManager.swapInInvalid=交æ¢ä¼šè¯[{0}]无效。 +persistentManager.swapMaxIdle=交æ¢ä¼šè¯[{0}]以存储,空闲为[{1}]秒 +persistentManager.swapTooManyActive=太多活跃会è¯,替æ¢é—²ç½® [{1}] ç§’çš„ä¼šè¯ [{0}] +persistentManager.tooManyActive=活跃会è¯å¤ªå¤šï¼Œ[{0}],寻找闲置的会è¯æ¥äº¤æ¢ +persistentManager.unloading=正在ä¿å­˜[{0}]æŒç»­ä¼šè¯ + +standardManager.deletePersistedFileFail=读å–æŒä¹…会è¯åŽæ— æ³•åˆ é™¤[{0}]。 此文件的æŒç»­å­˜åœ¨å¯èƒ½å¯¼è‡´å°†æ¥å°è¯•æŒç»­ä¼šè¯å¤±è´¥ã€‚ +standardManager.loading=正在从[{0}]加载æŒä¹…åŒ–ä¼šè¯ +standardManager.loading.exception=加载æŒä¹…化会è¯æ—¶å‘生异常 +standardManager.managerLoad=从æŒä¹…化存储加载会è¯å‘生异常 +standardManager.managerUnload=å¸è½½ä¼šè¯åˆ°æŒä¹…存储的异常 +standardManager.unloading=ä¿å­˜æŒä¹…化会è¯åˆ°[{0}] +standardManager.unloading.debug=å¸è½½æŒç»­ä¼šè¯ +standardManager.unloading.nosessions=没有è¦å¸è½½çš„æŒä¹…ä¼šè¯ + +standardSession.attributeEvent=会è¯å±žæ€§äº‹ä»¶ä¾¦å¬å™¨å¼•å‘异常 +standardSession.bindingEvent=会è¯ç»‘定事件侦å¬å™¨å¼•å‘异常 +standardSession.getAttribute.ise=getAttribute: 会è¯å·²å¤±æ•ˆ +standardSession.getAttributeNames.ise=getAttributeNames:会è¯å·²å¤±æ•ˆ +standardSession.getCreationTime.ise=getCreataionTime:会è¯å·²ç»æ— æ•ˆ +standardSession.getIdleTime.ise=getIdleTime: å·²å¤±æ•ˆçš„ä¼šè¯ +standardSession.getLastAccessedTime.ise=getLastAccessedTime: 会è¯å·²å¤±æ•ˆ +standardSession.getThisAccessedTime.ise=getThisAccessedTime:会è¯å·²ç»å¤±æ•ˆ +standardSession.getValueNames.ise=getValueNames:会è¯å·²ç»å¤±æ•ˆ +standardSession.invalidate.ise=无效:会è¯å·²æ— æ•ˆã€‚ +standardSession.isNew.ise=isNew:会è¯å·²å¤±æ•ˆ +standardSession.logoutfail=当回è¯å°†è¿‡æœŸç™»å‡ºç”¨æˆ·å¼‚常 +standardSession.notDeserializable=无法ååºåˆ—åŒ–ä¼šè¯ [{1}] 的属性 [{0}] +standardSession.notSerializable=ä¸èƒ½åºåˆ—化会è¯[{1}]的属性[{0}] +standardSession.principalNotDeserializable=无法为会è¯[{0}]ååºåˆ—化Principal对象 +standardSession.principalNotSerializable=无法为会è¯[{0}]åºåˆ—化Principal对象 +standardSession.removeAttribute.ise=删除属性:会è¯å·²å¤±æ•ˆ +standardSession.sessionEvent=会è¯æ—¶é—´ç›‘å¬æŠ›å‡ºå¼‚常 +standardSession.setAttribute.iae=setAttribute:ä¸å¯åºåˆ—化的属性[{0}]。 +standardSession.setAttribute.ise=setAttribute:会è¯[{0}]已无效 +standardSession.setAttribute.namenull=setAttribute:nameå‚æ•°ä¸èƒ½ä¸ºç©º diff --git a/java/org/apache/catalina/session/ManagerBase.java b/java/org/apache/catalina/session/ManagerBase.java new file mode 100644 index 0000000..7949720 --- /dev/null +++ b/java/org/apache/catalina/session/ManagerBase.java @@ -0,0 +1,1320 @@ +/* + * 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.catalina.session; + + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Deque; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.SessionIdGenerator; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.catalina.util.SessionIdGeneratorBase; +import org.apache.catalina.util.StandardSessionIdGenerator; +import org.apache.catalina.util.ToStringUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Minimal implementation of the Manager interface that supports no session persistence or distributable + * capabilities. This class may be subclassed to create more sophisticated Manager implementations. + * + * @author Craig R. McClanahan + */ +public abstract class ManagerBase extends LifecycleMBeanBase implements Manager { + + private final Log log = LogFactory.getLog(ManagerBase.class); // must not be static + + // ----------------------------------------------------- Instance Variables + + /** + * The Context with which this Manager is associated. + */ + private Context context; + + + /** + * The descriptive name of this Manager implementation (for logging). + */ + private static final String name = "ManagerBase"; + + + /** + * The Java class name of the secure random number generator class to be used when generating session identifiers. + * The random number generator class must be self-seeding and have a zero-argument constructor. If not specified, an + * instance of {@link java.security.SecureRandom} will be generated. + */ + protected String secureRandomClass = null; + + /** + * The name of the algorithm to use to create instances of {@link java.security.SecureRandom} which are used to + * generate session IDs. If no algorithm is specified, SHA1PRNG is used. If SHA1PRNG is not available, the platform + * default will be used. To use the platform default (which may be SHA1PRNG), specify the empty string. If an + * invalid algorithm and/or provider is specified the SecureRandom instances will be created using the defaults. If + * that fails, the SecureRandom instances will be created using platform defaults. + */ + protected String secureRandomAlgorithm = SessionIdGeneratorBase.DEFAULT_SECURE_RANDOM_ALGORITHM; + + /** + * The name of the provider to use to create instances of {@link java.security.SecureRandom} which are used to + * generate session IDs. If no provider is specified the platform default is used. If an invalid algorithm and/or + * provider is specified the SecureRandom instances will be created using the defaults. If that fails, the + * SecureRandom instances will be created using platform defaults. + */ + protected String secureRandomProvider = null; + + protected SessionIdGenerator sessionIdGenerator = null; + protected Class sessionIdGeneratorClass = null; + + /** + * The longest time (in seconds) that an expired session had been alive. + */ + protected volatile int sessionMaxAliveTime; + private final Object sessionMaxAliveTimeUpdateLock = new Object(); + + + protected static final int TIMING_STATS_CACHE_SIZE = 100; + + // Use LinkedList as the Deques are initialised by filling with null + protected final Deque sessionCreationTiming = new LinkedList<>(); + protected final Deque sessionExpirationTiming = new LinkedList<>(); + + /** + * Number of sessions that have expired. + */ + protected final AtomicLong expiredSessions = new AtomicLong(0); + + + /** + * The set of currently active Sessions for this Manager, keyed by session identifier. + */ + protected Map sessions = new ConcurrentHashMap<>(); + + /** + * Number of sessions created by this manager. + * @deprecated This will be removed in Tomcat 11 + */ + @Deprecated + protected long sessionCounter = 0; + + protected volatile int maxActive = 0; + + private final Object maxActiveUpdateLock = new Object(); + + /** + * The maximum number of active Sessions allowed, or -1 for no limit. + */ + protected int maxActiveSessions = -1; + + /** + * Number of session creations that failed due to maxActiveSessions. + */ + protected int rejectedSessions = 0; + + /** + * Number of duplicated session ids, anything > 0 means we have problems. + * @deprecated This will be removed in Tomcat 11 + */ + @Deprecated + protected volatile int duplicates = 0; + + /** + * Processing time during session expiration. + */ + protected long processingTime = 0; + + /** + * Iteration count for background processing. + */ + private int count = 0; + + + /** + * Frequency of the session expiration, and related manager operations. Manager operations will be done once for the + * specified amount of backgroundProcess calls (ie, the lower the amount, the most often the checks will occur). + */ + protected int processExpiresFrequency = 6; + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(ManagerBase.class); + + /** + * The property change support for this component. + */ + protected final PropertyChangeSupport support = new PropertyChangeSupport(this); + + private Pattern sessionAttributeNamePattern; + + private Pattern sessionAttributeValueClassNamePattern; + + private boolean warnOnSessionAttributeFilterFailure; + + private boolean notifyBindingListenerOnUnchangedValue; + + private boolean notifyAttributeListenerOnUnchangedValue = true; + + /** + * Determines whether sessions managed by this manager shall persist (serialize) authentication information or not. + */ + private boolean persistAuthentication = false; + + private boolean sessionActivityCheck = Globals.STRICT_SERVLET_COMPLIANCE; + + private boolean sessionLastAccessAtStart = Globals.STRICT_SERVLET_COMPLIANCE; + + // ------------------------------------------------------------ Constructors + + public ManagerBase() { + if (Globals.IS_SECURITY_ENABLED) { + // Minimum set required for default distribution/persistence to work + // plus String + // plus SerializablePrincipal and String[] (required for authentication persistence) + setSessionAttributeValueClassNameFilter("java\\.lang\\.(?:Boolean|Integer|Long|Number|String)" + + "|org\\.apache\\.catalina\\.realm\\.GenericPrincipal\\$SerializablePrincipal" + + "|\\[Ljava.lang.String;"); + setWarnOnSessionAttributeFilterFailure(true); + } + } + + + // -------------------------------------------------------------- Properties + + @Override + public boolean getNotifyAttributeListenerOnUnchangedValue() { + return notifyAttributeListenerOnUnchangedValue; + } + + + @Override + public void setNotifyAttributeListenerOnUnchangedValue(boolean notifyAttributeListenerOnUnchangedValue) { + this.notifyAttributeListenerOnUnchangedValue = notifyAttributeListenerOnUnchangedValue; + } + + + @Override + public boolean getNotifyBindingListenerOnUnchangedValue() { + return notifyBindingListenerOnUnchangedValue; + } + + + @Override + public void setNotifyBindingListenerOnUnchangedValue(boolean notifyBindingListenerOnUnchangedValue) { + this.notifyBindingListenerOnUnchangedValue = notifyBindingListenerOnUnchangedValue; + } + + + @Override + public boolean getSessionActivityCheck() { + return sessionActivityCheck; + } + + + @Override + public void setSessionActivityCheck(boolean sessionActivityCheck) { + this.sessionActivityCheck = sessionActivityCheck; + } + + + @Override + public boolean getSessionLastAccessAtStart() { + return sessionLastAccessAtStart; + } + + + @Override + public void setSessionLastAccessAtStart(boolean sessionLastAccessAtStart) { + this.sessionLastAccessAtStart = sessionLastAccessAtStart; + } + + + /** + * Obtain the regular expression used to filter session attribute based on attribute name. The regular expression is + * anchored so it must match the entire name + * + * @return The regular expression currently used to filter attribute names. {@code null} means no filter is applied. + * If an empty string is specified then no names will match the filter and all attributes will be + * blocked. + */ + public String getSessionAttributeNameFilter() { + if (sessionAttributeNamePattern == null) { + return null; + } + return sessionAttributeNamePattern.toString(); + } + + + /** + * Set the regular expression to use to filter session attributes based on attribute name. The regular expression is + * anchored so it must match the entire name. + * + * @param sessionAttributeNameFilter The regular expression to use to filter session attributes based on attribute + * name. Use {@code null} if no filtering is required. If an empty string is + * specified then no names will match the filter and all attributes will be + * blocked. + * + * @throws PatternSyntaxException If the expression is not valid + */ + public void setSessionAttributeNameFilter(String sessionAttributeNameFilter) throws PatternSyntaxException { + if (sessionAttributeNameFilter == null || sessionAttributeNameFilter.length() == 0) { + sessionAttributeNamePattern = null; + } else { + sessionAttributeNamePattern = Pattern.compile(sessionAttributeNameFilter); + } + } + + + /** + * Provides {@link #getSessionAttributeNameFilter()} as a pre-compiled regular expression pattern. + * + * @return The pre-compiled pattern used to filter session attributes based on attribute name. {@code null} means no + * filter is applied. + */ + protected Pattern getSessionAttributeNamePattern() { + return sessionAttributeNamePattern; + } + + + /** + * Obtain the regular expression used to filter session attribute based on the implementation class of the value. + * The regular expression is anchored and must match the fully qualified class name. + * + * @return The regular expression currently used to filter class names. {@code null} means no filter is applied. If + * an empty string is specified then no names will match the filter and all attributes will be blocked. + */ + public String getSessionAttributeValueClassNameFilter() { + if (sessionAttributeValueClassNamePattern == null) { + return null; + } + return sessionAttributeValueClassNamePattern.toString(); + } + + + /** + * Provides {@link #getSessionAttributeValueClassNameFilter()} as a pre-compiled regular expression pattern. + * + * @return The pre-compiled pattern used to filter session attributes based on the implementation class name of the + * value. {@code null} means no filter is applied. + */ + protected Pattern getSessionAttributeValueClassNamePattern() { + return sessionAttributeValueClassNamePattern; + } + + + /** + * Set the regular expression to use to filter classes used for session attributes. The regular expression is + * anchored and must match the fully qualified class name. + * + * @param sessionAttributeValueClassNameFilter The regular expression to use to filter session attributes based on + * class name. Use {@code + * null} if no filtering is required. If an empty string is specified then no + * names will match the filter and all attributes will be blocked. + * + * @throws PatternSyntaxException If the expression is not valid + */ + public void setSessionAttributeValueClassNameFilter(String sessionAttributeValueClassNameFilter) + throws PatternSyntaxException { + if (sessionAttributeValueClassNameFilter == null || sessionAttributeValueClassNameFilter.length() == 0) { + sessionAttributeValueClassNamePattern = null; + } else { + sessionAttributeValueClassNamePattern = Pattern.compile(sessionAttributeValueClassNameFilter); + } + } + + + /** + * Should a warn level log message be generated if a session attribute is not persisted / replicated / restored. + * + * @return {@code true} if a warn level log message should be generated + */ + public boolean getWarnOnSessionAttributeFilterFailure() { + return warnOnSessionAttributeFilterFailure; + } + + + /** + * Configure whether or not a warn level log message should be generated if a session attribute is not persisted / + * replicated / restored. + * + * @param warnOnSessionAttributeFilterFailure {@code true} if the warn level message should be generated + */ + public void setWarnOnSessionAttributeFilterFailure(boolean warnOnSessionAttributeFilterFailure) { + this.warnOnSessionAttributeFilterFailure = warnOnSessionAttributeFilterFailure; + } + + + @Override + public Context getContext() { + return context; + } + + + @Override + public void setContext(Context context) { + if (this.context == context) { + // NO-OP + return; + } + if (!getState().equals(LifecycleState.NEW)) { + throw new IllegalStateException(sm.getString("managerBase.setContextNotNew")); + } + Context oldContext = this.context; + this.context = context; + support.firePropertyChange("context", oldContext, this.context); + } + + + /** + * @return The name of the implementation class. + */ + public String getClassName() { + return this.getClass().getName(); + } + + + @Override + public SessionIdGenerator getSessionIdGenerator() { + if (sessionIdGenerator != null) { + return sessionIdGenerator; + } else if (sessionIdGeneratorClass != null) { + try { + sessionIdGenerator = sessionIdGeneratorClass.getConstructor().newInstance(); + return sessionIdGenerator; + } catch (ReflectiveOperationException ex) { + // Ignore + } + } + return null; + } + + + @Override + public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { + this.sessionIdGenerator = sessionIdGenerator; + sessionIdGeneratorClass = sessionIdGenerator.getClass(); + } + + + /** + * @return The descriptive short name of this Manager implementation. + */ + public String getName() { + return name; + } + + /** + * @return The secure random number generator class name. + */ + public String getSecureRandomClass() { + return this.secureRandomClass; + } + + + /** + * Set the secure random number generator class name. + * + * @param secureRandomClass The new secure random number generator class name + */ + public void setSecureRandomClass(String secureRandomClass) { + + String oldSecureRandomClass = this.secureRandomClass; + this.secureRandomClass = secureRandomClass; + support.firePropertyChange("secureRandomClass", oldSecureRandomClass, this.secureRandomClass); + + } + + + /** + * @return The secure random number generator algorithm name. + */ + public String getSecureRandomAlgorithm() { + return secureRandomAlgorithm; + } + + + /** + * Set the secure random number generator algorithm name. + * + * @param secureRandomAlgorithm The new secure random number generator algorithm name + */ + public void setSecureRandomAlgorithm(String secureRandomAlgorithm) { + this.secureRandomAlgorithm = secureRandomAlgorithm; + } + + + /** + * @return The secure random number generator provider name. + */ + public String getSecureRandomProvider() { + return secureRandomProvider; + } + + + /** + * Set the secure random number generator provider name. + * + * @param secureRandomProvider The new secure random number generator provider name + */ + public void setSecureRandomProvider(String secureRandomProvider) { + this.secureRandomProvider = secureRandomProvider; + } + + + @Override + public int getRejectedSessions() { + return rejectedSessions; + } + + + @Override + public long getExpiredSessions() { + return expiredSessions.get(); + } + + + @Override + public void setExpiredSessions(long expiredSessions) { + this.expiredSessions.set(expiredSessions); + } + + public long getProcessingTime() { + return processingTime; + } + + + public void setProcessingTime(long processingTime) { + this.processingTime = processingTime; + } + + /** + * @return The frequency of manager checks. + */ + public int getProcessExpiresFrequency() { + return this.processExpiresFrequency; + } + + /** + * Set the manager checks frequency. + * + * @param processExpiresFrequency the new manager checks frequency + */ + public void setProcessExpiresFrequency(int processExpiresFrequency) { + + if (processExpiresFrequency <= 0) { + return; + } + + int oldProcessExpiresFrequency = this.processExpiresFrequency; + this.processExpiresFrequency = processExpiresFrequency; + support.firePropertyChange("processExpiresFrequency", Integer.valueOf(oldProcessExpiresFrequency), + Integer.valueOf(this.processExpiresFrequency)); + + } + + + /** + * Return whether sessions managed by this manager shall persist authentication information or not. + * + * @return {@code true}, sessions managed by this manager shall persist authentication information; {@code false} + * otherwise + */ + public boolean getPersistAuthentication() { + return this.persistAuthentication; + } + + /** + * Set whether sessions managed by this manager shall persist authentication information or not. + * + * @param persistAuthentication if {@code true}, sessions managed by this manager shall persist authentication + * information + */ + public void setPersistAuthentication(boolean persistAuthentication) { + this.persistAuthentication = persistAuthentication; + } + + + // --------------------------------------------------------- Public Methods + + /** + * {@inheritDoc} + *

    + * Direct call to {@link #processExpires()} + */ + @Override + public void backgroundProcess() { + count = (count + 1) % processExpiresFrequency; + if (count == 0) { + processExpires(); + } + } + + /** + * Invalidate all sessions that have expired. + */ + public void processExpires() { + + long timeNow = System.currentTimeMillis(); + Session sessions[] = findSessions(); + int expireHere = 0; + + if (log.isTraceEnabled()) { + log.trace("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length); + } + for (Session session : sessions) { + if (session != null && !session.isValid()) { + expireHere++; + } + } + long timeEnd = System.currentTimeMillis(); + if (log.isTraceEnabled()) { + log.trace("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + + " expired sessions: " + expireHere); + } + processingTime += (timeEnd - timeNow); + + } + + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + + if (context == null) { + throw new LifecycleException(sm.getString("managerBase.contextNull")); + } + } + + + @Override + protected void startInternal() throws LifecycleException { + + // Ensure caches for timing stats are the right size by filling with + // nulls. + synchronized (sessionCreationTiming) { + while (sessionCreationTiming.size() < TIMING_STATS_CACHE_SIZE) { + sessionCreationTiming.add(null); + } + } + synchronized (sessionExpirationTiming) { + while (sessionExpirationTiming.size() < TIMING_STATS_CACHE_SIZE) { + sessionExpirationTiming.add(null); + } + } + + /* Create sessionIdGenerator if not explicitly configured */ + SessionIdGenerator sessionIdGenerator = getSessionIdGenerator(); + if (sessionIdGenerator == null) { + sessionIdGenerator = new StandardSessionIdGenerator(); + setSessionIdGenerator(sessionIdGenerator); + } + + sessionIdGenerator.setJvmRoute(getJvmRoute()); + if (sessionIdGenerator instanceof SessionIdGeneratorBase) { + SessionIdGeneratorBase sig = (SessionIdGeneratorBase) sessionIdGenerator; + sig.setSecureRandomAlgorithm(getSecureRandomAlgorithm()); + sig.setSecureRandomClass(getSecureRandomClass()); + sig.setSecureRandomProvider(getSecureRandomProvider()); + } + + if (sessionIdGenerator instanceof Lifecycle) { + ((Lifecycle) sessionIdGenerator).start(); + } else { + // Force initialization of the random number generator + if (log.isTraceEnabled()) { + log.trace("Force random number initialization starting"); + } + sessionIdGenerator.generateSessionId(); + if (log.isTraceEnabled()) { + log.trace("Force random number initialization completed"); + } + } + } + + + @Override + protected void stopInternal() throws LifecycleException { + if (sessionIdGenerator instanceof Lifecycle) { + ((Lifecycle) sessionIdGenerator).stop(); + } + } + + + @Override + public void add(Session session) { + sessions.put(session.getIdInternal(), session); + int size = getActiveSessions(); + if (size > maxActive) { + synchronized (maxActiveUpdateLock) { + if (size > maxActive) { + maxActive = size; + } + } + } + } + + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + + @Override + public Session createSession(String sessionId) { + + if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) { + rejectedSessions++; + throw new TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"), maxActiveSessions); + } + + // Recycle or create a Session instance + Session session = createEmptySession(); + + // Initialize the properties of the new session and return it + session.setNew(true); + session.setValid(true); + session.setCreationTime(System.currentTimeMillis()); + session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60); + String id = sessionId; + if (id == null) { + id = generateSessionId(); + } + session.setId(id); + + SessionTiming timing = new SessionTiming(session.getCreationTime(), 0); + synchronized (sessionCreationTiming) { + sessionCreationTiming.add(timing); + sessionCreationTiming.poll(); + } + return session; + } + + + @Override + public Session createEmptySession() { + return getNewSession(); + } + + + @Override + public Session findSession(String id) throws IOException { + if (id == null) { + return null; + } + return sessions.get(id); + } + + + @Override + public Session[] findSessions() { + return sessions.values().toArray(new Session[0]); + } + + + @Override + public void remove(Session session) { + remove(session, false); + } + + + @Override + public void remove(Session session, boolean update) { + // If the session has expired - as opposed to just being removed from + // the manager because it is being persisted - update the expired stats + if (update) { + long timeNow = System.currentTimeMillis(); + int timeAlive = (int) (timeNow - session.getCreationTimeInternal()) / 1000; + updateSessionMaxAliveTime(timeAlive); + expiredSessions.incrementAndGet(); + SessionTiming timing = new SessionTiming(timeNow, timeAlive); + synchronized (sessionExpirationTiming) { + sessionExpirationTiming.add(timing); + sessionExpirationTiming.poll(); + } + } + + if (session.getIdInternal() != null) { + sessions.remove(session.getIdInternal()); + } + } + + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + support.removePropertyChangeListener(listener); + } + + + @Override + public String rotateSessionId(Session session) { + String newId = generateSessionId(); + changeSessionId(session, newId, true, true); + return newId; + } + + + @Override + public void changeSessionId(Session session, String newId) { + changeSessionId(session, newId, true, true); + } + + + protected void changeSessionId(Session session, String newId, boolean notifySessionListeners, + boolean notifyContainerListeners) { + String oldId = session.getIdInternal(); + session.setId(newId, false); + session.tellChangedSessionId(newId, oldId, notifySessionListeners, notifyContainerListeners); + } + + + /** + * {@inheritDoc} + *

    + * This implementation excludes session attributes from distribution if the: + *

      + *
    • attribute name matches {@link #getSessionAttributeNameFilter()}
    • + *
    + */ + @Override + public boolean willAttributeDistribute(String name, Object value) { + Pattern sessionAttributeNamePattern = getSessionAttributeNamePattern(); + if (sessionAttributeNamePattern != null) { + if (!sessionAttributeNamePattern.matcher(name).matches()) { + if (getWarnOnSessionAttributeFilterFailure() || log.isDebugEnabled()) { + String msg = + sm.getString("managerBase.sessionAttributeNameFilter", name, sessionAttributeNamePattern); + if (getWarnOnSessionAttributeFilterFailure()) { + log.warn(msg); + } else { + log.debug(msg); + } + } + return false; + } + } + + Pattern sessionAttributeValueClassNamePattern = getSessionAttributeValueClassNamePattern(); + if (value != null && sessionAttributeValueClassNamePattern != null) { + if (!sessionAttributeValueClassNamePattern.matcher(value.getClass().getName()).matches()) { + if (getWarnOnSessionAttributeFilterFailure() || log.isDebugEnabled()) { + String msg = sm.getString("managerBase.sessionAttributeValueClassNameFilter", name, + value.getClass().getName(), sessionAttributeValueClassNamePattern); + if (getWarnOnSessionAttributeFilterFailure()) { + log.warn(msg); + } else { + log.debug(msg); + } + } + return false; + } + } + + return true; + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Get new session class to be used in the doLoad() method. + * + * @return a new session for use with this manager + */ + protected StandardSession getNewSession() { + return new StandardSession(this); + } + + + /** + * Generate and return a new session identifier. + * + * @return a new session id + */ + protected String generateSessionId() { + return sessionIdGenerator.generateSessionId(); + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Retrieve the enclosing Engine for this Manager. + * + * @return an Engine object (or null). + */ + public Engine getEngine() { + Engine e = null; + for (Container c = getContext(); e == null && c != null; c = c.getParent()) { + if (c instanceof Engine) { + e = (Engine) c; + } + } + return e; + } + + + /** + * Retrieve the JvmRoute for the enclosing Engine. + * + * @return the JvmRoute or null. + */ + public String getJvmRoute() { + Engine e = getEngine(); + return e == null ? null : e.getJvmRoute(); + } + + + // -------------------------------------------------------- Package Methods + + + @Override + public void setSessionCounter(long sessionCounter) { + this.sessionCounter = sessionCounter; + } + + + @Override + public long getSessionCounter() { + return getActiveSessions() + getExpiredSessions(); + } + + + /** + * Number of duplicated session IDs generated by the random source. Anything bigger than 0 means problems. + * + * @return The count of duplicates + * @deprecated This will be removed in Tomcat 11 + */ + @Deprecated + public int getDuplicates() { + return duplicates; + } + + + /** + * Set duplicates count. + * @param duplicates the new duplicates count + * @deprecated This will be removed in Tomcat 11 + */ + @Deprecated + public void setDuplicates(int duplicates) { + this.duplicates = duplicates; + } + + + @Override + public int getActiveSessions() { + return sessions.size(); + } + + + @Override + public int getMaxActive() { + return maxActive; + } + + + @Override + public void setMaxActive(int maxActive) { + synchronized (maxActiveUpdateLock) { + this.maxActive = maxActive; + } + } + + + /** + * @return The maximum number of active Sessions allowed, or -1 for no limit. + */ + public int getMaxActiveSessions() { + return this.maxActiveSessions; + } + + + /** + * Set the maximum number of active Sessions allowed, or -1 for no limit. + * + * @param max The new maximum number of sessions + */ + public void setMaxActiveSessions(int max) { + + int oldMaxActiveSessions = this.maxActiveSessions; + this.maxActiveSessions = max; + support.firePropertyChange("maxActiveSessions", Integer.valueOf(oldMaxActiveSessions), + Integer.valueOf(this.maxActiveSessions)); + + } + + + @Override + public int getSessionMaxAliveTime() { + return sessionMaxAliveTime; + } + + + @Override + public void setSessionMaxAliveTime(int sessionMaxAliveTime) { + synchronized (sessionMaxAliveTimeUpdateLock) { + this.sessionMaxAliveTime = sessionMaxAliveTime; + } + } + + + /** + * Updates the sessionMaxAliveTime attribute if the candidate value is larger than the current value. + * + * @param sessionAliveTime The candidate value (in seconds) for the new sessionMaxAliveTime value. + */ + public void updateSessionMaxAliveTime(int sessionAliveTime) { + if (sessionAliveTime > this.sessionMaxAliveTime) { + synchronized (sessionMaxAliveTimeUpdateLock) { + if (sessionAliveTime > this.sessionMaxAliveTime) { + this.sessionMaxAliveTime = sessionAliveTime; + } + } + } + } + + /** + * {@inheritDoc} + *

    + * Based on the last 100 sessions to expire. If less than 100 sessions have expired then all available data is used. + */ + @Override + public int getSessionAverageAliveTime() { + // Copy current stats + List copy; + synchronized (sessionExpirationTiming) { + copy = new ArrayList<>(sessionExpirationTiming); + } + + // Init + int counter = 0; + int result = 0; + + // Calculate average + for (SessionTiming timing : copy) { + if (timing != null) { + int timeAlive = timing.getDuration(); + counter++; + // Very careful not to overflow - probably not necessary + result = (result * ((counter - 1) / counter)) + (timeAlive / counter); + } + } + return result; + } + + + /** + * {@inheritDoc} + *

    + * Based on the creation time of the previous 100 sessions created. If less than 100 sessions have been created then + * all available data is used. + */ + @Override + public int getSessionCreateRate() { + // Copy current stats + List copy; + synchronized (sessionCreationTiming) { + copy = new ArrayList<>(sessionCreationTiming); + } + + return calculateRate(copy); + } + + + /** + * {@inheritDoc} + *

    + * Based on the expiry time of the previous 100 sessions expired. If less than 100 sessions have expired then all + * available data is used. + * + * @return The current rate (in sessions per minute) of session expiration + */ + @Override + public int getSessionExpireRate() { + // Copy current stats + List copy; + synchronized (sessionExpirationTiming) { + copy = new ArrayList<>(sessionExpirationTiming); + } + + return calculateRate(copy); + } + + + private static int calculateRate(List sessionTiming) { + // Init + long now = System.currentTimeMillis(); + long oldest = now; + int counter = 0; + int result = 0; + + // Calculate rate + for (SessionTiming timing : sessionTiming) { + if (timing != null) { + counter++; + if (timing.getTimestamp() < oldest) { + oldest = timing.getTimestamp(); + } + } + } + if (counter > 0) { + if (oldest < now) { + result = (1000 * 60 * counter) / (int) (now - oldest); + } else { + // Better than reporting zero + result = Integer.MAX_VALUE; + } + } + return result; + } + + + /** + * For debugging. + * + * @return A space separated list of all session IDs currently active + */ + public String listSessionIds() { + StringBuilder sb = new StringBuilder(); + for (String s : sessions.keySet()) { + sb.append(s).append(' '); + } + return sb.toString(); + } + + + /** + * For debugging. + * + * @param sessionId The ID for the session of interest + * @param key The key for the attribute to obtain + * + * @return The attribute value for the specified session, if found, null otherwise + */ + public String getSessionAttribute(String sessionId, String key) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return null; + } + Object o = s.getSession().getAttribute(key); + if (o == null) { + return null; + } + return o.toString(); + } + + + /** + * Returns information about the session with the given session id. + *

    + * The session information is organized as a HashMap, mapping session attribute names to the String representation + * of their values. + * + * @param sessionId Session id + * + * @return HashMap mapping session attribute names to the String representation of their values, or null if no + * session with the specified id exists, or if the session does not have any attributes + */ + public HashMap getSession(String sessionId) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return null; + } + + Enumeration ee = s.getSession().getAttributeNames(); + if (ee == null || !ee.hasMoreElements()) { + return null; + } + + HashMap map = new HashMap<>(); + while (ee.hasMoreElements()) { + String attrName = ee.nextElement(); + map.put(attrName, getSessionAttribute(sessionId, attrName)); + } + + return map; + } + + + public void expireSession(String sessionId) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return; + } + s.expire(); + } + + public long getThisAccessedTimestamp(String sessionId) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return -1; + } + return s.getThisAccessedTime(); + } + + public String getThisAccessedTime(String sessionId) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return ""; + } + return new Date(s.getThisAccessedTime()).toString(); + } + + public long getLastAccessedTimestamp(String sessionId) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return -1; + } + return s.getLastAccessedTime(); + } + + public String getLastAccessedTime(String sessionId) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return ""; + } + return new Date(s.getLastAccessedTime()).toString(); + } + + public String getCreationTime(String sessionId) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return ""; + } + return new Date(s.getCreationTime()).toString(); + } + + public long getCreationTimestamp(String sessionId) { + Session s = sessions.get(sessionId); + if (s == null) { + if (log.isInfoEnabled()) { + log.info(sm.getString("managerBase.sessionNotFound", sessionId)); + } + return -1; + } + return s.getCreationTime(); + } + + + @Override + public String toString() { + return ToStringUtil.toString(this, context); + } + + + // -------------------- JMX and Registration -------------------- + @Override + public String getObjectNameKeyProperties() { + + StringBuilder name = new StringBuilder("type=Manager"); + + name.append(",host="); + name.append(context.getParent().getName()); + + name.append(",context="); + String contextName = context.getName(); + if (!contextName.startsWith("/")) { + name.append('/'); + } + name.append(contextName); + + return name.toString(); + } + + @Override + public String getDomainInternal() { + return context.getDomain(); + } + + + // ----------------------------------------------------------- Inner classes + + protected static final class SessionTiming { + private final long timestamp; + private final int duration; + + public SessionTiming(long timestamp, int duration) { + this.timestamp = timestamp; + this.duration = duration; + } + + /** + * @return Time stamp associated with this piece of timing information in milliseconds. + */ + public long getTimestamp() { + return timestamp; + } + + /** + * @return Duration associated with this piece of timing information in seconds. + */ + public int getDuration() { + return duration; + } + } +} diff --git a/java/org/apache/catalina/session/PersistentManager.java b/java/org/apache/catalina/session/PersistentManager.java new file mode 100644 index 0000000..815c9e3 --- /dev/null +++ b/java/org/apache/catalina/session/PersistentManager.java @@ -0,0 +1,49 @@ +/* + * 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.catalina.session; + +/** + * Implementation of the Manager interface that makes use of a Store to swap active Sessions to disk. It can be + * configured to achieve several different goals: + *

      + *
    • Persist sessions across restarts of the Container
    • + *
    • Fault tolerance, keep sessions backed up on disk to allow recovery in the event of unplanned restarts.
    • + *
    • Limit the number of active sessions kept in memory by swapping less active sessions out to disk.
    • + *
    + * If used with a load-balancer, the load-balancer must be configured to use sticky sessions for this manager to operate + * correctly. + * + * @author Kief Morris (kief@kief.com) + */ +public final class PersistentManager extends PersistentManagerBase { + + // ----------------------------------------------------- Instance Variables + + /** + * The descriptive name of this Manager implementation (for logging). + */ + private static final String name = "PersistentManager"; + + + // ------------------------------------------------------------- Properties + + @Override + public String getName() { + return name; + } +} + diff --git a/java/org/apache/catalina/session/PersistentManagerBase.java b/java/org/apache/catalina/session/PersistentManagerBase.java new file mode 100644 index 0000000..0f21277 --- /dev/null +++ b/java/org/apache/catalina/session/PersistentManagerBase.java @@ -0,0 +1,1017 @@ +/* + * 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.catalina.session; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Session; +import org.apache.catalina.Store; +import org.apache.catalina.StoreManager; +import org.apache.catalina.security.SecurityUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Extends the {@link ManagerBase} class to implement most of the functionality required by a Manager which supports any + * kind of persistence, even if only for restarts. + *

    + * IMPLEMENTATION NOTE: Correct behavior of session storing and reloading depends upon external calls to the + * {@link Lifecycle#start()} and {@link Lifecycle#stop()} methods of this class at the correct times. + * + * @author Craig R. McClanahan + */ +public abstract class PersistentManagerBase extends ManagerBase implements StoreManager { + + private final Log log = LogFactory.getLog(PersistentManagerBase.class); // must not be static + + // ---------------------------------------------------- Security Classes + + private class PrivilegedStoreClear implements PrivilegedExceptionAction { + + PrivilegedStoreClear() { + // NOOP + } + + @Override + public Void run() throws Exception { + store.clear(); + return null; + } + } + + private class PrivilegedStoreRemove implements PrivilegedExceptionAction { + + private String id; + + PrivilegedStoreRemove(String id) { + this.id = id; + } + + @Override + public Void run() throws Exception { + store.remove(id); + return null; + } + } + + private class PrivilegedStoreLoad implements PrivilegedExceptionAction { + + private String id; + + PrivilegedStoreLoad(String id) { + this.id = id; + } + + @Override + public Session run() throws Exception { + return store.load(id); + } + } + + private class PrivilegedStoreSave implements PrivilegedExceptionAction { + + private Session session; + + PrivilegedStoreSave(Session session) { + this.session = session; + } + + @Override + public Void run() throws Exception { + store.save(session); + return null; + } + } + + private class PrivilegedStoreKeys implements PrivilegedExceptionAction { + + PrivilegedStoreKeys() { + // NOOP + } + + @Override + public String[] run() throws Exception { + return store.keys(); + } + } + + // ----------------------------------------------------- Instance Variables + + /** + * The descriptive name of this Manager implementation (for logging). + */ + private static final String name = "PersistentManagerBase"; + + /** + * Key of the note of a session in which the timestamp of last backup is stored. + */ + private static final String PERSISTED_LAST_ACCESSED_TIME = + "org.apache.catalina.session.PersistentManagerBase.persistedLastAccessedTime"; + + + /** + * Store object which will manage the Session store. + */ + protected Store store = null; + + + /** + * Whether to save and reload sessions when the Manager unload and load methods are + * called. + */ + protected boolean saveOnRestart = true; + + + /** + * How long a session must be idle before it should be backed up. {@code -1} means sessions won't be backed up. + */ + protected int maxIdleBackup = -1; + + + /** + * The minimum time in seconds a session must be idle before it is eligible to be swapped to disk to keep the active + * session count below maxActiveSessions. Setting to {@code -1} means sessions will not be swapped out to keep the + * active session count down. + */ + protected int minIdleSwap = -1; + + + /** + * The maximum time in seconds a session may be idle before it is eligible to be swapped to disk due to inactivity. + * Setting this to {@code -1} means sessions should not be swapped out just because of inactivity. + */ + protected int maxIdleSwap = -1; + + + /** + * Sessions currently being swapped in and the associated locks + */ + private final Map sessionSwapInLocks = new HashMap<>(); + + /* + * Session that is currently getting swapped in to prevent loading it more than once concurrently + */ + private final ThreadLocal sessionToSwapIn = new ThreadLocal<>(); + + + // ------------------------------------------------------------- Properties + + + /** + * Indicates how many seconds old a session can get, after its last use in a request, before it should be backed up + * to the store. {@code -1} means sessions are not backed up. + * + * @return the timeout after which sessions are ripe for back up + */ + public int getMaxIdleBackup() { + + return maxIdleBackup; + + } + + + /** + * Sets the option to back sessions up to the Store after they are used in a request. Sessions remain available in + * memory after being backed up, so they are not passivated as they are when swapped out. The value set indicates + * how old a session may get (since its last use) before it must be backed up: {@code -1} means sessions are not + * backed up. + *

    + * Note that this is not a hard limit: sessions are checked against this age limit periodically according to + * {@code processExpiresFrequency}. This value should be considered to indicate when a session is ripe for backing + * up. + *

    + * So it is possible that a session may be idle for {@code maxIdleBackup + + * processExpiresFrequency * engine.backgroundProcessorDelay} seconds, plus the time it takes to handle other + * session expiration, swapping, etc. tasks. + * + * @param backup The number of seconds after their last accessed time when they should be written to the Store. + */ + public void setMaxIdleBackup(int backup) { + + if (backup == this.maxIdleBackup) { + return; + } + int oldBackup = this.maxIdleBackup; + this.maxIdleBackup = backup; + support.firePropertyChange("maxIdleBackup", Integer.valueOf(oldBackup), Integer.valueOf(this.maxIdleBackup)); + + } + + + /** + * @return The maximum time in seconds a session may be idle before it is eligible to be swapped to disk due to + * inactivity. A value of {@code -1} means sessions should not be swapped out just because of + * inactivity. + */ + public int getMaxIdleSwap() { + return maxIdleSwap; + } + + + /** + * Sets the maximum time in seconds a session may be idle before it is eligible to be swapped to disk due to + * inactivity. Setting this to {@code -1} means sessions should not be swapped out just because of inactivity. + * + * @param max time in seconds to wait for possible swap out + */ + public void setMaxIdleSwap(int max) { + + if (max == this.maxIdleSwap) { + return; + } + int oldMaxIdleSwap = this.maxIdleSwap; + this.maxIdleSwap = max; + support.firePropertyChange("maxIdleSwap", Integer.valueOf(oldMaxIdleSwap), Integer.valueOf(this.maxIdleSwap)); + } + + + /** + * @return The minimum time in seconds a session must be idle before it is eligible to be swapped to disk to keep + * the active session count below maxActiveSessions. A value of {@code -1} means sessions will not be + * swapped out to keep the active session count down. + */ + public int getMinIdleSwap() { + return minIdleSwap; + } + + + /** + * Sets the minimum time in seconds a session must be idle before it is eligible to be swapped to disk to keep the + * active session count below maxActiveSessions. Setting to {@code -1} means sessions will not be swapped out to + * keep the active session count down. + * + * @param min time in seconds before a possible swap out + */ + public void setMinIdleSwap(int min) { + + if (this.minIdleSwap == min) { + return; + } + int oldMinIdleSwap = this.minIdleSwap; + this.minIdleSwap = min; + support.firePropertyChange("minIdleSwap", Integer.valueOf(oldMinIdleSwap), Integer.valueOf(this.minIdleSwap)); + + } + + + /** + * Check, whether a session is loaded in memory + * + * @param id The session id for the session to be searched for + * + * @return {@code true}, if the session id is loaded in memory otherwise {@code false} is returned + */ + public boolean isLoaded(String id) { + try { + if (super.findSession(id) != null) { + return true; + } + } catch (IOException e) { + log.error(sm.getString("persistentManager.isLoadedError", id), e); + } + return false; + } + + + @Override + public String getName() { + return name; + } + + + /** + * Set the Store object which will manage persistent Session storage for this Manager. + * + * @param store the associated Store + */ + public void setStore(Store store) { + this.store = store; + store.setManager(this); + } + + + /** + * @return the Store object which manages persistent Session storage for this Manager. + */ + @Override + public Store getStore() { + return this.store; + } + + + /** + * Indicates whether sessions are saved when the Manager is shut down properly. This requires the {@link #unload()} + * method to be called. + * + * @return {@code true}, when sessions should be saved on restart, {code false} otherwise + */ + public boolean getSaveOnRestart() { + + return saveOnRestart; + + } + + + /** + * Set the option to save sessions to the Store when the Manager is shut down, then loaded when the Manager starts + * again. If set to false, any sessions found in the Store may still be picked up when the Manager is started again. + * + * @param saveOnRestart {@code true} if sessions should be saved on restart, {@code false} if they should be + * ignored. + */ + public void setSaveOnRestart(boolean saveOnRestart) { + + if (saveOnRestart == this.saveOnRestart) { + return; + } + + boolean oldSaveOnRestart = this.saveOnRestart; + this.saveOnRestart = saveOnRestart; + support.firePropertyChange("saveOnRestart", Boolean.valueOf(oldSaveOnRestart), + Boolean.valueOf(this.saveOnRestart)); + + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Clear all sessions from the Store. + */ + public void clearStore() { + + if (store == null) { + return; + } + + try { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + AccessController.doPrivileged(new PrivilegedStoreClear()); + } catch (PrivilegedActionException e) { + log.error(sm.getString("persistentManager.storeClearError"), e.getException()); + } + } else { + store.clear(); + } + } catch (IOException e) { + log.error(sm.getString("persistentManager.storeClearError"), e); + } + + } + + + /** + * {@inheritDoc} + *

    + * Direct call to processExpires and processPersistenceChecks + */ + @Override + public void processExpires() { + + long timeNow = System.currentTimeMillis(); + Session sessions[] = findSessions(); + int expireHere = 0; + if (log.isTraceEnabled()) { + log.trace("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length); + } + for (Session session : sessions) { + if (!session.isValid()) { + expiredSessions.incrementAndGet(); + expireHere++; + } + } + processPersistenceChecks(); + if (getStore() instanceof StoreBase) { + ((StoreBase) getStore()).processExpires(); + } + + long timeEnd = System.currentTimeMillis(); + if (log.isTraceEnabled()) { + log.trace("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + + " expired sessions: " + expireHere); + } + processingTime += (timeEnd - timeNow); + + } + + + /** + * Called by the background thread after active sessions have been checked for expiration, to allow sessions to be + * swapped out, backed up, etc. + */ + public void processPersistenceChecks() { + + processMaxIdleSwaps(); + processMaxActiveSwaps(); + processMaxIdleBackups(); + + } + + + /** + * {@inheritDoc} + *

    + * This method checks the persistence store if persistence is enabled, otherwise just uses the functionality from + * ManagerBase. + */ + @Override + public Session findSession(String id) throws IOException { + + Session session = super.findSession(id); + // OK, at this point, we're not sure if another thread is trying to + // remove the session or not so the only way around this is to lock it + // (or attempt to) and then try to get it by this session id again. If + // the other code ran swapOut, then we should get a null back during + // this run, and if not, we lock it out so we can access the session + // safely. + if (session != null) { + synchronized (session) { + session = super.findSession(session.getIdInternal()); + if (session != null) { + // To keep any external calling code from messing up the + // concurrency. + session.access(); + session.endAccess(); + } + } + } + if (session != null) { + return session; + } + + // See if the Session is in the Store + session = swapIn(id); + return session; + } + + /** + * Remove this Session from the active Sessions for this Manager, but not from the Store. (Used by the + * PersistentValve) + * + * @param session Session to be removed + */ + @Override + public void removeSuper(Session session) { + super.remove(session, false); + } + + /** + * Load all sessions found in the persistence mechanism, assuming they are marked as valid and have not passed their + * expiration limit. If persistence is not supported, this method returns without doing anything. + *

    + * Note that by default, this method is not called by the MiddleManager class. In order to use it, a subclass must + * specifically call it, for example in the start() and/or processPersistenceChecks() methods. + */ + @Override + public void load() { + + // Initialize our internal data structures + sessions.clear(); + + if (store == null) { + return; + } + + String[] ids = null; + try { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + ids = AccessController.doPrivileged(new PrivilegedStoreKeys()); + } catch (PrivilegedActionException e) { + log.error(sm.getString("persistentManager.storeLoadKeysError"), e.getException()); + return; + } + } else { + ids = store.keys(); + } + } catch (IOException e) { + log.error(sm.getString("persistentManager.storeLoadKeysError"), e); + return; + } + + int n = ids.length; + if (n == 0) { + return; + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("persistentManager.loading", String.valueOf(n))); + } + + for (String id : ids) { + try { + swapIn(id); + } catch (IOException e) { + log.error(sm.getString("persistentManager.storeLoadError"), e); + } + } + + } + + + /** + * {@inheritDoc} + *

    + * Remove this Session from the Store. + */ + @Override + public void remove(Session session, boolean update) { + + super.remove(session, update); + + if (store != null) { + removeSession(session.getIdInternal()); + } + } + + + /** + * Remove this Session from the active Sessions for this Manager, and from the Store. + * + * @param id Session's id to be removed + */ + protected void removeSession(String id) { + try { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + AccessController.doPrivileged(new PrivilegedStoreRemove(id)); + } catch (PrivilegedActionException e) { + log.error(sm.getString("persistentManager.removeError"), e.getException()); + } + } else { + store.remove(id); + } + } catch (IOException e) { + log.error(sm.getString("persistentManager.removeError"), e); + } + } + + /** + * Save all currently active sessions in the appropriate persistence mechanism, if any. If persistence is not + * supported, this method returns without doing anything. + *

    + * Note that by default, this method is not called by the MiddleManager class. In order to use it, a subclass must + * specifically call it, for example in the stop() and/or processPersistenceChecks() methods. + */ + @Override + public void unload() { + + if (store == null) { + return; + } + + Session sessions[] = findSessions(); + int n = sessions.length; + if (n == 0) { + return; + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("persistentManager.unloading", String.valueOf(n))); + } + + for (Session session : sessions) { + try { + swapOut(session); + } catch (IOException e) { + // This is logged in writeSession() + } + } + + } + + + @Override + public int getActiveSessionsFull() { + // In memory session count + int result = getActiveSessions(); + try { + // Store session count + result += getStore().getSize(); + } catch (IOException ioe) { + log.warn(sm.getString("persistentManager.storeSizeException")); + } + return result; + } + + + @Override + public Set getSessionIdsFull() { + // In memory session ID list + Set sessionIds = new HashSet<>(sessions.keySet()); + try { + // Store session ID list + sessionIds.addAll(Arrays.asList(getStore().keys())); + } catch (IOException e) { + log.warn(sm.getString("persistentManager.storeKeysException")); + } + return sessionIds; + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Look for a session in the Store and, if found, restore it in the Manager's list of active sessions if + * appropriate. The session will be removed from the Store after swapping in, but will not be added to the active + * session list if it is invalid or past its expiration. + * + * @param id The id of the session that should be swapped in + * + * @return restored session, or {@code null}, if none is found + * + * @throws IOException an IO error occurred + */ + protected Session swapIn(String id) throws IOException { + + if (store == null) { + return null; + } + + Object swapInLock = null; + + /* + * The purpose of this sync and these locks is to make sure that a session is only loaded once. It doesn't + * matter if the lock is removed and then another thread enters this method and tries to load the same session. + * That thread will re-create a swapIn lock for that session, quickly find that the session is already in + * sessions, use it and carry on. + */ + synchronized (this) { + swapInLock = sessionSwapInLocks.computeIfAbsent(id, k -> new Object()); + } + + Session session = null; + + synchronized (swapInLock) { + // First check to see if another thread has loaded the session into + // the manager + session = sessions.get(id); + + if (session == null) { + Session currentSwapInSession = sessionToSwapIn.get(); + try { + if (currentSwapInSession == null || !id.equals(currentSwapInSession.getId())) { + session = loadSessionFromStore(id); + sessionToSwapIn.set(session); + + if (session != null && !session.isValid()) { + log.error(sm.getString("persistentManager.swapInInvalid", id)); + session.expire(); + removeSession(id); + session = null; + } + + if (session != null) { + reactivateLoadedSession(id, session); + } + } + } finally { + sessionToSwapIn.remove(); + } + } + } + + // Make sure the lock is removed + synchronized (this) { + sessionSwapInLocks.remove(id); + } + + return session; + + } + + private void reactivateLoadedSession(String id, Session session) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("persistentManager.swapIn", id)); + } + + session.setManager(this); + // make sure the listeners know about it. + ((StandardSession) session).tellNew(); + add(session); + ((StandardSession) session).activate(); + // endAccess() to ensure timeouts happen correctly. + // access() to keep access count correct or it will end up + // negative + session.access(); + session.endAccess(); + } + + private Session loadSessionFromStore(String id) throws IOException { + try { + if (SecurityUtil.isPackageProtectionEnabled()) { + return securedStoreLoad(id); + } else { + return store.load(id); + } + } catch (ClassNotFoundException e) { + String msg = sm.getString("persistentManager.deserializeError", id); + log.error(msg, e); + throw new IllegalStateException(msg, e); + } + } + + + private Session securedStoreLoad(String id) throws IOException, ClassNotFoundException { + try { + return AccessController.doPrivileged(new PrivilegedStoreLoad(id)); + } catch (PrivilegedActionException ex) { + Exception e = ex.getException(); + log.error(sm.getString("persistentManager.swapInException", id), e); + if (e instanceof IOException) { + throw (IOException) e; + } else if (e instanceof ClassNotFoundException) { + throw (ClassNotFoundException) e; + } + } + return null; + } + + + /** + * Remove the session from the Manager's list of active sessions and write it out to the Store. If the session is + * past its expiration or invalid, this method does nothing. + * + * @param session The Session to write out + * + * @throws IOException an IO error occurred + */ + protected void swapOut(Session session) throws IOException { + + if (store == null || !session.isValid()) { + return; + } + + ((StandardSession) session).passivate(); + writeSession(session); + super.remove(session, true); + session.recycle(); + + } + + + /** + * Write the provided session to the Store without modifying the copy in memory or triggering passivation events. + * Does nothing if the session is invalid or past its expiration. + * + * @param session The session that should be written + * + * @throws IOException an IO error occurred + */ + protected void writeSession(Session session) throws IOException { + + if (store == null || !session.isValid()) { + return; + } + + try { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + AccessController.doPrivileged(new PrivilegedStoreSave(session)); + } catch (PrivilegedActionException ex) { + Exception exception = ex.getException(); + if (exception instanceof IOException) { + throw (IOException) exception; + } + log.error(sm.getString("persistentManager.serializeError", session.getIdInternal(), exception)); + } + } else { + store.save(session); + } + } catch (IOException e) { + log.error(sm.getString("persistentManager.serializeError", session.getIdInternal(), e)); + throw e; + } + + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + super.startInternal(); + + if (store == null) { + log.error(sm.getString("persistentManager.noStore")); + } else if (store instanceof Lifecycle) { + ((Lifecycle) store).start(); + } + + setState(LifecycleState.STARTING); + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + if (log.isTraceEnabled()) { + log.trace("Stopping"); + } + + setState(LifecycleState.STOPPING); + + if (getStore() != null && saveOnRestart) { + unload(); + } else { + // Expire all active sessions + Session sessions[] = findSessions(); + for (Session value : sessions) { + StandardSession session = (StandardSession) value; + if (!session.isValid()) { + continue; + } + session.expire(); + } + } + + if (getStore() instanceof Lifecycle) { + ((Lifecycle) getStore()).stop(); + } + + // Require a new random number generator if we are restarted + super.stopInternal(); + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Swap idle sessions out to Store if they are idle too long. + */ + protected void processMaxIdleSwaps() { + + if (!getState().isAvailable() || maxIdleSwap < 0) { + return; + } + + Session sessions[] = findSessions(); + + // Swap out all sessions idle longer than maxIdleSwap + if (maxIdleSwap >= 0) { + for (Session value : sessions) { + StandardSession session = (StandardSession) value; + synchronized (session) { + if (!session.isValid()) { + continue; + } + int timeIdle = (int) (session.getIdleTimeInternal() / 1000L); + if (timeIdle >= maxIdleSwap && timeIdle >= minIdleSwap) { + if (session.accessCount != null && session.accessCount.get() > 0) { + // Session is currently being accessed - skip it + continue; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("persistentManager.swapMaxIdle", session.getIdInternal(), + Integer.valueOf(timeIdle))); + } + try { + swapOut(session); + } catch (IOException e) { + // This is logged in writeSession() + } + } + } + } + } + + } + + + /** + * Swap idle sessions out to Store if too many are active + */ + protected void processMaxActiveSwaps() { + + if (!getState().isAvailable() || minIdleSwap < 0 || getMaxActiveSessions() < 0) { + return; + } + + Session sessions[] = findSessions(); + + // FIXME: Smarter algorithm (LRU) + int limit = (int) (getMaxActiveSessions() * 0.9); + + if (limit >= sessions.length) { + return; + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("persistentManager.tooManyActive", Integer.valueOf(sessions.length))); + } + + int toswap = sessions.length - limit; + + for (int i = 0; i < sessions.length && toswap > 0; i++) { + StandardSession session = (StandardSession) sessions[i]; + synchronized (session) { + int timeIdle = (int) (session.getIdleTimeInternal() / 1000L); + if (timeIdle >= minIdleSwap) { + if (session.accessCount != null && session.accessCount.get() > 0) { + // Session is currently being accessed - skip it + continue; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("persistentManager.swapTooManyActive", session.getIdInternal(), + Integer.valueOf(timeIdle))); + } + try { + swapOut(session); + } catch (IOException e) { + // This is logged in writeSession() + } + toswap--; + } + } + } + + } + + + /** + * Back up idle sessions. + */ + protected void processMaxIdleBackups() { + + if (!getState().isAvailable() || maxIdleBackup < 0) { + return; + } + + Session sessions[] = findSessions(); + + // Back up all sessions idle longer than maxIdleBackup + if (maxIdleBackup >= 0) { + for (Session value : sessions) { + StandardSession session = (StandardSession) value; + synchronized (session) { + if (!session.isValid()) { + continue; + } + long lastAccessedTime = session.getLastAccessedTimeInternal(); + Long persistedLastAccessedTime = (Long) session.getNote(PERSISTED_LAST_ACCESSED_TIME); + if (persistedLastAccessedTime != null && + lastAccessedTime == persistedLastAccessedTime.longValue()) { + continue; + } + int timeIdle = (int) (session.getIdleTimeInternal() / 1000L); + if (timeIdle >= maxIdleBackup) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("persistentManager.backupMaxIdle", session.getIdInternal(), + Integer.valueOf(timeIdle))); + } + + try { + writeSession(session); + } catch (IOException e) { + // This is logged in writeSession() + } + session.setNote(PERSISTED_LAST_ACCESSED_TIME, Long.valueOf(lastAccessedTime)); + } + } + } + } + + } + +} diff --git a/java/org/apache/catalina/session/StandardManager.java b/java/org/apache/catalina/session/StandardManager.java new file mode 100644 index 0000000..465c122 --- /dev/null +++ b/java/org/apache/catalina/session/StandardManager.java @@ -0,0 +1,422 @@ +/* + * 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.catalina.session; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.ServletContext; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Loader; +import org.apache.catalina.Session; +import org.apache.catalina.security.SecurityUtil; +import org.apache.catalina.util.CustomObjectInputStream; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; + +/** + * Standard implementation of the Manager interface that provides simple session persistence across restarts of + * this component (such as when the entire server is shut down and restarted, or when a particular web application is + * reloaded. + *

    + * IMPLEMENTATION NOTE: Correct behavior of session storing and reloading depends upon external calls to the + * start() and stop() methods of this class at the correct times. + * + * @author Craig R. McClanahan + */ +public class StandardManager extends ManagerBase { + + private final Log log = LogFactory.getLog(StandardManager.class); // must not be static + + // ---------------------------------------------------- Security Classes + + private class PrivilegedDoLoad implements PrivilegedExceptionAction { + + PrivilegedDoLoad() { + // NOOP + } + + @Override + public Void run() throws Exception { + doLoad(); + return null; + } + } + + private class PrivilegedDoUnload implements PrivilegedExceptionAction { + + PrivilegedDoUnload() { + // NOOP + } + + @Override + public Void run() throws Exception { + doUnload(); + return null; + } + + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The descriptive name of this Manager implementation (for logging). + */ + protected static final String name = "StandardManager"; + + + /** + * Path name of the disk file in which active sessions are saved when we stop, and from which these sessions are + * loaded when we start. A null value indicates that no persistence is desired. If this pathname is + * relative, it will be resolved against the temporary working directory provided by our context, available via the + * jakarta.servlet.context.tempdir context attribute. + */ + protected String pathname = null; + + + // ------------------------------------------------------------- Properties + + @Override + public String getName() { + return name; + } + + + /** + * @return The session persistence pathname, if any. + */ + public String getPathname() { + return pathname; + } + + + /** + * Set the session persistence pathname to the specified value. If no persistence support is desired, set the + * pathname to null. + * + * @param pathname New session persistence pathname + */ + public void setPathname(String pathname) { + String oldPathname = this.pathname; + this.pathname = pathname; + support.firePropertyChange("pathname", oldPathname, this.pathname); + } + + + // --------------------------------------------------------- Public Methods + + @Override + public void load() throws ClassNotFoundException, IOException { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + AccessController.doPrivileged(new PrivilegedDoLoad()); + } catch (PrivilegedActionException ex) { + Exception exception = ex.getException(); + if (exception instanceof ClassNotFoundException) { + throw (ClassNotFoundException) exception; + } else if (exception instanceof IOException) { + throw (IOException) exception; + } + if (log.isDebugEnabled()) { + log.debug("Unreported exception in load() ", exception); + } + } + } else { + doLoad(); + } + } + + + /** + * Load any currently active sessions that were previously unloaded to the appropriate persistence mechanism, if + * any. If persistence is not supported, this method returns without doing anything. + * + * @exception ClassNotFoundException if a serialized class cannot be found during the reload + * @exception IOException if an input/output error occurs + */ + protected void doLoad() throws ClassNotFoundException, IOException { + if (log.isTraceEnabled()) { + log.trace("Start: Loading persisted sessions"); + } + + // Initialize our internal data structures + sessions.clear(); + + // Open an input stream to the specified pathname, if any + File file = file(); + if (file == null) { + return; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("standardManager.loading", pathname)); + } + Loader loader = null; + ClassLoader classLoader = null; + Log logger = null; + try (FileInputStream fis = new FileInputStream(file.getAbsolutePath()); + BufferedInputStream bis = new BufferedInputStream(fis)) { + Context c = getContext(); + loader = c.getLoader(); + logger = c.getLogger(); + if (loader != null) { + classLoader = loader.getClassLoader(); + } + if (classLoader == null) { + classLoader = getClass().getClassLoader(); + } + + // Load the previously unloaded active sessions + synchronized (sessions) { + try (ObjectInputStream ois = new CustomObjectInputStream(bis, classLoader, logger, + getSessionAttributeValueClassNamePattern(), getWarnOnSessionAttributeFilterFailure())) { + Integer count = (Integer) ois.readObject(); + int n = count.intValue(); + if (log.isTraceEnabled()) { + log.trace("Loading " + n + " persisted sessions"); + } + for (int i = 0; i < n; i++) { + StandardSession session = getNewSession(); + session.readObjectData(ois); + session.setManager(this); + sessions.put(session.getIdInternal(), session); + session.activate(); + if (!session.isValidInternal()) { + // If session is already invalid, + // expire session to prevent memory leak. + session.setValid(true); + session.expire(); + } + } + } finally { + // Delete the persistent storage file + if (file.exists()) { + if (!file.delete()) { + log.warn(sm.getString("standardManager.deletePersistedFileFail", file)); + } + } + } + } + } catch (FileNotFoundException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardManager.noFile", file.getAbsolutePath())); + } + return; + } + + if (log.isTraceEnabled()) { + log.trace("Finish: Loading persisted sessions"); + } + } + + + @Override + public void unload() throws IOException { + if (SecurityUtil.isPackageProtectionEnabled()) { + try { + AccessController.doPrivileged(new PrivilegedDoUnload()); + } catch (PrivilegedActionException ex) { + Exception exception = ex.getException(); + if (exception instanceof IOException) { + throw (IOException) exception; + } + if (log.isDebugEnabled()) { + log.debug("Unreported exception in unLoad()", exception); + } + } + } else { + doUnload(); + } + } + + + /** + * Save any currently active sessions in the appropriate persistence mechanism, if any. If persistence is not + * supported, this method returns without doing anything. + * + * @exception IOException if an input/output error occurs + */ + protected void doUnload() throws IOException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("standardManager.unloading.debug")); + } + + if (sessions.isEmpty()) { + log.debug(sm.getString("standardManager.unloading.nosessions")); + return; // nothing to do + } + + // Open an output stream to the specified pathname, if any + File file = file(); + if (file == null) { + return; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("standardManager.unloading", pathname)); + } + + // Keep a note of sessions that are expired + List list = new ArrayList<>(); + + try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath()); + BufferedOutputStream bos = new BufferedOutputStream(fos); + ObjectOutputStream oos = new ObjectOutputStream(bos)) { + + synchronized (sessions) { + if (log.isTraceEnabled()) { + log.trace("Unloading " + sessions.size() + " sessions"); + } + // Write the number of active sessions, followed by the details + oos.writeObject(Integer.valueOf(sessions.size())); + for (Session s : sessions.values()) { + StandardSession session = (StandardSession) s; + list.add(session); + session.passivate(); + session.writeObjectData(oos); + } + } + } + + // Expire all the sessions we just wrote + if (log.isDebugEnabled()) { + log.debug(sm.getString("standardManager.expiringSessions", Integer.toString(list.size()))); + } + for (StandardSession session : list) { + try { + session.expire(false); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } finally { + session.recycle(); + } + } + + if (log.isTraceEnabled()) { + log.trace("Unloading complete"); + } + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + super.startInternal(); + + // Load unloaded sessions, if any + try { + load(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardManager.managerLoad"), t); + } + + setState(LifecycleState.STARTING); + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + if (log.isTraceEnabled()) { + log.trace("Stopping"); + } + + setState(LifecycleState.STOPPING); + + // Write out sessions + try { + unload(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("standardManager.managerUnload"), t); + } + + // Expire all active sessions + Session sessions[] = findSessions(); + for (Session session : sessions) { + try { + if (session.isValid()) { + session.expire(); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } finally { + // Measure against memory leaking if references to the session + // object are kept in a shared field somewhere + session.recycle(); + } + } + + // Require a new random number generator if we are restarted + super.stopInternal(); + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Return a File object representing the pathname to our persistence file, if any. + * + * @return the file + */ + protected File file() { + if (pathname == null || pathname.length() == 0) { + return null; + } + File file = new File(pathname); + if (!file.isAbsolute()) { + Context context = getContext(); + ServletContext servletContext = context.getServletContext(); + File tempdir = (File) servletContext.getAttribute(ServletContext.TEMPDIR); + if (tempdir != null) { + file = new File(tempdir, pathname); + } + } + return file; + } +} diff --git a/java/org/apache/catalina/session/StandardSession.java b/java/org/apache/catalina/session/StandardSession.java new file mode 100644 index 0000000..7aa62e8 --- /dev/null +++ b/java/org/apache/catalina/session/StandardSession.java @@ -0,0 +1,1482 @@ +/* + * 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.catalina.session; + +import java.beans.PropertyChangeSupport; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.io.WriteAbortedException; +import java.security.AccessController; +import java.security.Principal; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionActivationListener; +import jakarta.servlet.http.HttpSessionAttributeListener; +import jakarta.servlet.http.HttpSessionBindingEvent; +import jakarta.servlet.http.HttpSessionBindingListener; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionIdListener; +import jakarta.servlet.http.HttpSessionListener; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.SessionEvent; +import org.apache.catalina.SessionListener; +import org.apache.catalina.TomcatPrincipal; +import org.apache.catalina.authenticator.SavedRequest; +import org.apache.catalina.security.SecurityUtil; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Standard implementation of the Session interface. This object is serializable, so that it can be stored in + * persistent storage or transferred to a different JVM for distributable session support. + *

    + * IMPLEMENTATION NOTE: An instance of this class represents both the internal (Session) and application level + * (HttpSession) view of the session. However, because the class itself is not declared public, Java logic outside of + * the org.apache.catalina.session package cannot cast an HttpSession view of this instance back to a + * Session view. + *

    + * IMPLEMENTATION NOTE: If you add fields to this class, you must make sure that you carry them over in the + * read/writeObject methods so that this class is properly serialized. + * + * @author Craig R. McClanahan + * @author Sean Legassick + * @author Jon S. Stevens + */ +public class StandardSession implements HttpSession, Session, Serializable { + + private static final long serialVersionUID = 1L; + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new Session associated with the specified Manager. + * + * @param manager The manager with which this Session is associated + */ + public StandardSession(Manager manager) { + + super(); + this.manager = manager; + + if (manager != null) { + // Manager could be null in test environments + activityCheck = manager.getSessionActivityCheck(); + lastAccessAtStart = manager.getSessionLastAccessAtStart(); + } + + // Initialize access count + if (activityCheck) { + accessCount = new AtomicInteger(); + } + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Type array. + */ + protected static final String EMPTY_ARRAY[] = new String[0]; + + + /** + * The collection of user data attributes associated with this Session. + */ + protected ConcurrentMap attributes = new ConcurrentHashMap<>(); + + + /** + * The authentication type used to authenticate our cached Principal, if any. NOTE: This value is not included in + * the serialized version of this object. + */ + protected transient String authType = null; + + + /** + * The time this session was created, in milliseconds since midnight, January 1, 1970 GMT. + */ + protected long creationTime = 0L; + + + /** + * We are currently processing a session expiration, so bypass certain IllegalStateException tests. NOTE: This value + * is not included in the serialized version of this object. + */ + protected transient volatile boolean expiring = false; + + + /** + * The facade associated with this session. NOTE: This value is not included in the serialized version of this + * object. + */ + protected transient StandardSessionFacade facade = null; + + + /** + * The session identifier of this Session. + */ + protected String id = null; + + + /** + * The last accessed time for this Session. + */ + protected volatile long lastAccessedTime = creationTime; + + + /** + * The session event listeners for this Session. + */ + protected transient ArrayList listeners = new ArrayList<>(); + + + /** + * The Manager with which this Session is associated. + */ + protected transient Manager manager = null; + + + /** + * The maximum time interval, in seconds, between client requests before the servlet container may invalidate this + * session. A negative time indicates that the session should never time out. + */ + protected volatile int maxInactiveInterval = -1; + + + /** + * Flag indicating whether this session is new or not. + */ + protected volatile boolean isNew = false; + + + /** + * Flag indicating whether this session is valid or not. + */ + protected volatile boolean isValid = false; + + + /** + * Internal notes associated with this session by Catalina components and event listeners. IMPLEMENTATION + * NOTE: This object is not saved and restored across session serializations! + */ + protected transient Map notes = new ConcurrentHashMap<>(); + + + /** + * The authenticated Principal associated with this session, if any. IMPLEMENTATION NOTE: This object is + * not saved and restored across session serializations! + */ + protected transient Principal principal = null; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(StandardSession.class); + + + /** + * The property change support for this component. NOTE: This value is not included in the serialized version of + * this object. + */ + protected final transient PropertyChangeSupport support = new PropertyChangeSupport(this); + + + /** + * The current accessed time for this session. + */ + protected volatile long thisAccessedTime = creationTime; + + + /** + * The access count for this session. + */ + protected transient AtomicInteger accessCount = null; + + + /** + * The activity check for this session. + */ + protected transient boolean activityCheck; + + + /** + * The behavior of the last access check. + */ + protected transient boolean lastAccessAtStart; + + + // ----------------------------------------------------- Session Properties + + + @Override + public String getAuthType() { + return this.authType; + } + + + @Override + public void setAuthType(String authType) { + String oldAuthType = this.authType; + this.authType = authType; + support.firePropertyChange("authType", oldAuthType, this.authType); + } + + + @Override + public void setCreationTime(long time) { + + this.creationTime = time; + this.lastAccessedTime = time; + this.thisAccessedTime = time; + + } + + + @Override + public String getId() { + return this.id; + } + + + @Override + public String getIdInternal() { + return this.id; + } + + + @Override + public void setId(String id) { + setId(id, true); + } + + + @Override + public void setId(String id, boolean notify) { + + if ((this.id != null) && (manager != null)) { + manager.remove(this); + } + + this.id = id; + + if (manager != null) { + manager.add(this); + } + + if (notify) { + tellNew(); + } + } + + + /** + * Inform the listeners about the new session. + */ + public void tellNew() { + + // Notify interested session event listeners + fireSessionEvent(SESSION_CREATED_EVENT, null); + + // Notify interested application event listeners + Context context = manager.getContext(); + Object listeners[] = context.getApplicationLifecycleListeners(); + if (listeners != null && listeners.length > 0) { + HttpSessionEvent event = new HttpSessionEvent(getSession()); + for (Object o : listeners) { + if (!(o instanceof HttpSessionListener)) { + continue; + } + HttpSessionListener listener = (HttpSessionListener) o; + try { + context.fireContainerEvent("beforeSessionCreated", listener); + listener.sessionCreated(event); + context.fireContainerEvent("afterSessionCreated", listener); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + try { + context.fireContainerEvent("afterSessionCreated", listener); + } catch (Exception e) { + // Ignore + } + manager.getContext().getLogger().error(sm.getString("standardSession.sessionEvent"), t); + } + } + } + + } + + @Override + public void tellChangedSessionId(String newId, String oldId, boolean notifySessionListeners, + boolean notifyContainerListeners) { + Context context = manager.getContext(); + // notify ContainerListeners + if (notifyContainerListeners) { + context.fireContainerEvent(Context.CHANGE_SESSION_ID_EVENT, new String[] { oldId, newId }); + } + + // notify HttpSessionIdListener + if (notifySessionListeners) { + Object listeners[] = context.getApplicationEventListeners(); + if (listeners != null && listeners.length > 0) { + HttpSessionEvent event = new HttpSessionEvent(getSession()); + + for (Object listener : listeners) { + if (!(listener instanceof HttpSessionIdListener)) { + continue; + } + + HttpSessionIdListener idListener = (HttpSessionIdListener) listener; + try { + idListener.sessionIdChanged(event, oldId); + } catch (Throwable t) { + manager.getContext().getLogger().error(sm.getString("standardSession.sessionEvent"), t); + } + } + } + } + } + + + @Override + public long getThisAccessedTime() { + + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.getThisAccessedTime.ise")); + } + + return this.thisAccessedTime; + } + + @Override + public long getThisAccessedTimeInternal() { + return this.thisAccessedTime; + } + + @Override + public long getLastAccessedTime() { + + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.getLastAccessedTime.ise")); + } + + return this.lastAccessedTime; + } + + @Override + public long getLastAccessedTimeInternal() { + return this.lastAccessedTime; + } + + @Override + public long getIdleTime() { + + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.getIdleTime.ise")); + } + + return getIdleTimeInternal(); + } + + @Override + public long getIdleTimeInternal() { + long timeNow = System.currentTimeMillis(); + long timeIdle; + if (lastAccessAtStart) { + timeIdle = timeNow - lastAccessedTime; + } else { + timeIdle = timeNow - thisAccessedTime; + } + return timeIdle; + } + + @Override + public Manager getManager() { + return this.manager; + } + + + @Override + public void setManager(Manager manager) { + this.manager = manager; + } + + + @Override + public int getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + + @Override + public void setMaxInactiveInterval(int interval) { + this.maxInactiveInterval = interval; + } + + + @Override + public void setNew(boolean isNew) { + this.isNew = isNew; + } + + + @Override + public Principal getPrincipal() { + return this.principal; + } + + + @Override + public void setPrincipal(Principal principal) { + + Principal oldPrincipal = this.principal; + this.principal = principal; + support.firePropertyChange("principal", oldPrincipal, this.principal); + + } + + + @Override + public HttpSession getSession() { + if (facade == null) { + if (SecurityUtil.isPackageProtectionEnabled()) { + facade = AccessController.doPrivileged(new PrivilegedNewSessionFacade(this)); + } else { + facade = new StandardSessionFacade(this); + } + } + return facade; + } + + + @Override + public boolean isValid() { + + if (!this.isValid) { + return false; + } + + if (this.expiring) { + return true; + } + + if (activityCheck && accessCount.get() > 0) { + return true; + } + + if (maxInactiveInterval > 0) { + int timeIdle = (int) (getIdleTimeInternal() / 1000L); + if (timeIdle >= maxInactiveInterval) { + expire(true); + } + } + + return this.isValid; + } + + + @Override + public void setValid(boolean isValid) { + this.isValid = isValid; + } + + + // ------------------------------------------------- Session Public Methods + + + @Override + public void access() { + + this.thisAccessedTime = System.currentTimeMillis(); + + if (activityCheck) { + accessCount.incrementAndGet(); + } + + } + + + @Override + public void endAccess() { + + isNew = false; + + /* + * The servlet spec mandates to ignore request handling time in lastAccessedTime. + */ + if (lastAccessAtStart) { + this.lastAccessedTime = this.thisAccessedTime; + this.thisAccessedTime = System.currentTimeMillis(); + } else { + this.thisAccessedTime = System.currentTimeMillis(); + this.lastAccessedTime = this.thisAccessedTime; + } + + if (activityCheck) { + accessCount.decrementAndGet(); + } + + } + + + @Override + public void addSessionListener(SessionListener listener) { + + listeners.add(listener); + + } + + + @Override + public void expire() { + + expire(true); + + } + + + /** + * Perform the internal processing required to invalidate this session, without triggering an exception if the + * session has already expired. + * + * @param notify Should we notify listeners about the demise of this session? + */ + public void expire(boolean notify) { + + // Check to see if session has already been invalidated. + // Do not check expiring at this point as expire should not return until + // isValid is false + if (!isValid) { + return; + } + + synchronized (this) { + // Check again, now we are inside the sync so this code only runs once + // Double check locking - isValid needs to be volatile + // The check of expiring is to ensure that an infinite loop is not + // entered as per bug 56339 + if (expiring || !isValid) { + return; + } + + if (manager == null) { + return; + } + + // Mark this session as "being expired" + expiring = true; + + // Notify interested application event listeners + // Call listeners in reverse order + Context context = manager.getContext(); + + // The call to expire() may not have been triggered by the webapp. + // Make sure the webapp's class loader is set when calling the + // listeners + if (notify) { + ClassLoader oldContextClassLoader = null; + try { + oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null); + Object listeners[] = context.getApplicationLifecycleListeners(); + if (listeners != null && listeners.length > 0) { + HttpSessionEvent event = new HttpSessionEvent(getSession()); + for (int i = 0; i < listeners.length; i++) { + int j = (listeners.length - 1) - i; + if (!(listeners[j] instanceof HttpSessionListener)) { + continue; + } + HttpSessionListener listener = (HttpSessionListener) listeners[j]; + try { + context.fireContainerEvent("beforeSessionDestroyed", listener); + listener.sessionDestroyed(event); + context.fireContainerEvent("afterSessionDestroyed", listener); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + try { + context.fireContainerEvent("afterSessionDestroyed", listener); + } catch (Exception e) { + // Ignore + } + manager.getContext().getLogger().error(sm.getString("standardSession.sessionEvent"), t); + } + } + } + } finally { + context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader); + } + } + + if (activityCheck) { + accessCount.set(0); + } + + // Remove this session from our manager's active sessions + manager.remove(this, true); + + // Notify interested session event listeners + if (notify) { + fireSessionEvent(SESSION_DESTROYED_EVENT, null); + } + + // Call the logout method + if (principal instanceof TomcatPrincipal) { + TomcatPrincipal gp = (TomcatPrincipal) principal; + try { + gp.logout(); + } catch (Exception e) { + manager.getContext().getLogger().error(sm.getString("standardSession.logoutfail"), e); + } + } + + // We have completed expire of this session + setValid(false); + expiring = false; + + // Unbind any objects associated with this session + String keys[] = keys(); + ClassLoader oldContextClassLoader = null; + try { + oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null); + for (String key : keys) { + removeAttributeInternal(key, notify); + } + } finally { + context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader); + } + } + + } + + + /** + * Perform the internal processing required to passivate this session. + */ + public void passivate() { + + // Notify interested session event listeners + fireSessionEvent(SESSION_PASSIVATED_EVENT, null); + + // Notify ActivationListeners + HttpSessionEvent event = null; + String keys[] = keys(); + for (String key : keys) { + Object attribute = attributes.get(key); + if (attribute instanceof HttpSessionActivationListener) { + if (event == null) { + event = new HttpSessionEvent(getSession()); + } + try { + ((HttpSessionActivationListener) attribute).sessionWillPassivate(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + manager.getContext().getLogger().error(sm.getString("standardSession.attributeEvent"), t); + } + } + } + + } + + + /** + * Perform internal processing required to activate this session. + */ + public void activate() { + + // Initialize access count + if (activityCheck) { + accessCount = new AtomicInteger(); + } + + // Notify interested session event listeners + fireSessionEvent(SESSION_ACTIVATED_EVENT, null); + + // Notify ActivationListeners + HttpSessionEvent event = null; + String keys[] = keys(); + for (String key : keys) { + Object attribute = attributes.get(key); + if (attribute instanceof HttpSessionActivationListener) { + if (event == null) { + event = new HttpSessionEvent(getSession()); + } + try { + ((HttpSessionActivationListener) attribute).sessionDidActivate(event); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + manager.getContext().getLogger().error(sm.getString("standardSession.attributeEvent"), t); + } + } + } + } + + + @Override + public Object getNote(String name) { + return notes.get(name); + } + + + @Override + public Iterator getNoteNames() { + return notes.keySet().iterator(); + } + + + @Override + public void recycle() { + + // Reset the instance variables associated with this Session + attributes.clear(); + setAuthType(null); + creationTime = 0L; + expiring = false; + id = null; + lastAccessedTime = 0L; + maxInactiveInterval = -1; + notes.clear(); + setPrincipal(null); + isNew = false; + isValid = false; + manager = null; + + } + + + @Override + public void removeNote(String name) { + + notes.remove(name); + + } + + + @Override + public void removeSessionListener(SessionListener listener) { + + listeners.remove(listener); + + } + + + @Override + public void setNote(String name, Object value) { + + notes.put(name, value); + + } + + + /** + * Return a string representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("StandardSession["); + sb.append(id); + sb.append(']'); + return sb.toString(); + } + + + // ------------------------------------------------ Session Package Methods + + + /** + * Read a serialized version of the contents of this session object from the specified object input stream, without + * requiring that the StandardSession itself have been serialized. + * + * @param stream The object input stream to read from + * + * @exception ClassNotFoundException if an unknown class is specified + * @exception IOException if an input/output error occurs + */ + public void readObjectData(ObjectInputStream stream) throws ClassNotFoundException, IOException { + + doReadObject(stream); + + } + + + /** + * Write a serialized version of the contents of this session object to the specified object output stream, without + * requiring that the StandardSession itself have been serialized. + * + * @param stream The object output stream to write to + * + * @exception IOException if an input/output error occurs + */ + public void writeObjectData(ObjectOutputStream stream) throws IOException { + + doWriteObject(stream); + + } + + + // ------------------------------------------------- HttpSession Properties + + + @Override + public long getCreationTime() { + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.getCreationTime.ise")); + } + + return this.creationTime; + } + + + @Override + public long getCreationTimeInternal() { + return this.creationTime; + } + + + @Override + public ServletContext getServletContext() { + if (manager == null) { + return null; + } + Context context = manager.getContext(); + return context.getServletContext(); + } + + + // ----------------------------------------------HttpSession Public Methods + + @Override + public Object getAttribute(String name) { + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.getAttribute.ise")); + } + + if (name == null) { + return null; + } + + return attributes.get(name); + } + + + @Override + public Enumeration getAttributeNames() { + + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.getAttributeNames.ise")); + } + + Set names = new HashSet<>(attributes.keySet()); + return Collections.enumeration(names); + } + + + @Override + public void invalidate() { + + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.invalidate.ise")); + } + + // Cause this session to expire + expire(); + + } + + + @Override + public boolean isNew() { + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.isNew.ise")); + } + + return this.isNew; + } + + + @Override + public void removeAttribute(String name) { + + removeAttribute(name, true); + + } + + + /** + * Remove the object bound with the specified name from this session. If the session does not have an object bound + * with this name, this method does nothing. + *

    + * After this method executes, and if the object implements HttpSessionBindingListener, the container + * calls valueUnbound() on the object. + * + * @param name Name of the object to remove from this session. + * @param notify Should we notify interested listeners that this attribute is being removed? + * + * @exception IllegalStateException if this method is called on an invalidated session + */ + public void removeAttribute(String name, boolean notify) { + + // Validate our current state + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.removeAttribute.ise")); + } + + removeAttributeInternal(name, notify); + + } + + + @Override + public void setAttribute(String name, Object value) { + setAttribute(name, value, true); + } + + + /** + * Bind an object to this session, using the specified name. If an object of the same name is already bound to this + * session, the object is replaced. + *

    + * After this method executes, and if the object implements HttpSessionBindingListener, the container + * calls valueBound() on the object. + * + * @param name Name to which the object is bound, cannot be null + * @param value Object to be bound, cannot be null + * @param notify whether to notify session listeners + * + * @exception IllegalArgumentException if an attempt is made to add a non-serializable object in an environment + * marked distributable. + * @exception IllegalStateException if this method is called on an invalidated session + */ + public void setAttribute(String name, Object value, boolean notify) { + + // Name cannot be null + if (name == null) { + throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.namenull")); + } + + // Null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + // Validate our current state + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.setAttribute.ise", getIdInternal())); + } + + Context context = manager.getContext(); + + if (context.getDistributable() && !isAttributeDistributable(name, value) && !exclude(name, value)) { + throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.iae", name)); + } + // Construct an event with the new value + HttpSessionBindingEvent event = null; + + // Call the valueBound() method if necessary + if (notify && value instanceof HttpSessionBindingListener) { + // Don't call any notification if replacing with the same value + // unless configured to do so + Object oldValue = attributes.get(name); + if (value != oldValue || manager.getNotifyBindingListenerOnUnchangedValue()) { + event = new HttpSessionBindingEvent(getSession(), name, value); + try { + ((HttpSessionBindingListener) value).valueBound(event); + } catch (Throwable t) { + manager.getContext().getLogger().error(sm.getString("standardSession.bindingEvent"), t); + } + } + } + + // Replace or add this attribute + Object unbound = attributes.put(name, value); + + // Call the valueUnbound() method if necessary + if (notify && unbound instanceof HttpSessionBindingListener) { + // Don't call any notification if replacing with the same value + // unless configured to do so + if (unbound != value || manager.getNotifyBindingListenerOnUnchangedValue()) { + try { + ((HttpSessionBindingListener) unbound) + .valueUnbound(new HttpSessionBindingEvent(getSession(), name)); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + manager.getContext().getLogger().error(sm.getString("standardSession.bindingEvent"), t); + } + } + } + + if (!notify) { + return; + } + + // Notify interested application event listeners + Object listeners[] = context.getApplicationEventListeners(); + if (listeners == null) { + return; + } + for (Object o : listeners) { + if (!(o instanceof HttpSessionAttributeListener)) { + continue; + } + HttpSessionAttributeListener listener = (HttpSessionAttributeListener) o; + try { + if (unbound != null) { + if (unbound != value || manager.getNotifyAttributeListenerOnUnchangedValue()) { + context.fireContainerEvent("beforeSessionAttributeReplaced", listener); + if (event == null) { + event = new HttpSessionBindingEvent(getSession(), name, unbound); + } + listener.attributeReplaced(event); + context.fireContainerEvent("afterSessionAttributeReplaced", listener); + } + } else { + context.fireContainerEvent("beforeSessionAttributeAdded", listener); + if (event == null) { + event = new HttpSessionBindingEvent(getSession(), name, value); + } + listener.attributeAdded(event); + context.fireContainerEvent("afterSessionAttributeAdded", listener); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + try { + if (unbound != null) { + if (unbound != value || manager.getNotifyAttributeListenerOnUnchangedValue()) { + context.fireContainerEvent("afterSessionAttributeReplaced", listener); + } + } else { + context.fireContainerEvent("afterSessionAttributeAdded", listener); + } + } catch (Exception e) { + // Ignore + } + manager.getContext().getLogger().error(sm.getString("standardSession.attributeEvent"), t); + } + } + } + + + // ------------------------------------------ HttpSession Protected Methods + + /** + * @return the isValid flag for this session without any expiration check. + */ + protected boolean isValidInternal() { + return this.isValid; + } + + /** + * {@inheritDoc} + *

    + * This implementation simply checks the value for serializability. Sub-classes might use other distribution + * technology not based on serialization and can override this check. + */ + @Override + public boolean isAttributeDistributable(String name, Object value) { + return value instanceof Serializable; + } + + + /** + * Read a serialized version of this session object from the specified object input stream. + *

    + * IMPLEMENTATION NOTE: The reference to the owning Manager is not restored by this method, and must be set + * explicitly. + * + * @param stream The input stream to read from + * + * @exception ClassNotFoundException if an unknown class is specified + * @exception IOException if an input/output error occurs + */ + protected void doReadObject(ObjectInputStream stream) throws ClassNotFoundException, IOException { + + // Deserialize the scalar instance variables (except Manager) + authType = null; // Transient (may be set later) + creationTime = ((Long) stream.readObject()).longValue(); + lastAccessedTime = ((Long) stream.readObject()).longValue(); + maxInactiveInterval = ((Integer) stream.readObject()).intValue(); + isNew = ((Boolean) stream.readObject()).booleanValue(); + isValid = ((Boolean) stream.readObject()).booleanValue(); + thisAccessedTime = ((Long) stream.readObject()).longValue(); + principal = null; // Transient (may be set later) + // setId((String) stream.readObject()); + id = (String) stream.readObject(); + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger().trace("readObject() loading session " + id); + } + + if (notes == null) { + notes = new ConcurrentHashMap<>(); + } + /* + * The next object read could either be the number of attributes (Integer) or if authentication information is + * persisted then: - authType (String) - always present - Principal object - always present - expected session + * ID - present if BZ 66120 is fixed - saved request - present if BZ 66120 is fixed + * + * Note: Some, all or none of the above objects may be null + */ + Object nextObject = stream.readObject(); + if (!(nextObject instanceof Integer)) { + // Not an Integer so the next two objects will be authType and + // Principal + setAuthType((String) nextObject); + try { + setPrincipal((Principal) stream.readObject()); + } catch (ClassNotFoundException | ObjectStreamException e) { + String msg = sm.getString("standardSession.principalNotDeserializable", id); + if (manager.getContext().getLogger().isDebugEnabled()) { + manager.getContext().getLogger().debug(msg, e); + } else { + manager.getContext().getLogger().warn(msg); + } + throw e; + } + + nextObject = stream.readObject(); + if (!(nextObject instanceof Integer)) { + // Not an Integer so the next two objects will be + // 'expected session ID' and 'saved request' + if (nextObject != null) { + notes.put(org.apache.catalina.authenticator.Constants.SESSION_ID_NOTE, nextObject); + } + nextObject = stream.readObject(); + if (nextObject != null) { + notes.put(org.apache.catalina.authenticator.Constants.FORM_REQUEST_NOTE, nextObject); + } + + // Next object will be the number of attributes + nextObject = stream.readObject(); + } + } + + // Deserialize the attribute count and attribute values + if (attributes == null) { + attributes = new ConcurrentHashMap<>(); + } + int n = ((Integer) nextObject).intValue(); + boolean isValidSave = isValid; + isValid = true; + for (int i = 0; i < n; i++) { + String name = (String) stream.readObject(); + final Object value; + try { + value = stream.readObject(); + } catch (WriteAbortedException wae) { + if (wae.getCause() instanceof NotSerializableException) { + String msg = sm.getString("standardSession.notDeserializable", name, id); + if (manager.getContext().getLogger().isDebugEnabled()) { + manager.getContext().getLogger().debug(msg, wae); + } else { + manager.getContext().getLogger().warn(msg); + } + // Skip non serializable attributes + continue; + } + throw wae; + } + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger().trace(" loading attribute '" + name + "' with value '" + value + "'"); + } + // Handle the case where the filter configuration was changed while + // the web application was stopped. + if (exclude(name, value)) { + continue; + } + // ConcurrentHashMap does not allow null keys or values + if (null != value) { + attributes.put(name, value); + } + } + isValid = isValidSave; + + if (listeners == null) { + listeners = new ArrayList<>(); + } + } + + + /** + * Write a serialized version of this session object to the specified object output stream. + *

    + * IMPLEMENTATION NOTE: The owning Manager will not be stored in the serialized representation of this + * Session. After calling readObject(), you must set the associated Manager explicitly. + *

    + * IMPLEMENTATION NOTE: Any attribute that is not Serializable will be unbound from the session, with + * appropriate actions if it implements HttpSessionBindingListener. If you do not want any such attributes, be sure + * the distributable property of the associated Manager is set to true. + * + * @param stream The output stream to write to + * + * @exception IOException if an input/output error occurs + */ + protected void doWriteObject(ObjectOutputStream stream) throws IOException { + + // Write the scalar instance variables (except Manager) + stream.writeObject(Long.valueOf(creationTime)); + stream.writeObject(Long.valueOf(lastAccessedTime)); + stream.writeObject(Integer.valueOf(maxInactiveInterval)); + stream.writeObject(Boolean.valueOf(isNew)); + stream.writeObject(Boolean.valueOf(isValid)); + stream.writeObject(Long.valueOf(thisAccessedTime)); + stream.writeObject(id); + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger().trace("writeObject() storing session " + id); + } + + // Gather authentication information (if configured) + String sessionAuthType = null; + Principal sessionPrincipal = null; + String expectedSessionId = null; + SavedRequest savedRequest = null; + if (getPersistAuthentication()) { + sessionAuthType = getAuthType(); + sessionPrincipal = getPrincipal(); + if (sessionPrincipal != null && !(sessionPrincipal instanceof Serializable)) { + sessionPrincipal = null; + manager.getContext().getLogger().warn(sm.getString("standardSession.principalNotSerializable", id)); + } + expectedSessionId = (String) notes.get(org.apache.catalina.authenticator.Constants.SESSION_ID_NOTE); + savedRequest = (SavedRequest) notes.get(org.apache.catalina.authenticator.Constants.FORM_REQUEST_NOTE); + } + + // Write authentication information (may be null values) + stream.writeObject(sessionAuthType); + try { + stream.writeObject(sessionPrincipal); + } catch (NotSerializableException e) { + manager.getContext().getLogger().warn(sm.getString("standardSession.principalNotSerializable", id), e); + } + stream.writeObject(expectedSessionId); + stream.writeObject(savedRequest); + + // Accumulate the names of serializable and non-serializable attributes + String keys[] = keys(); + List saveNames = new ArrayList<>(); + List saveValues = new ArrayList<>(); + for (String key : keys) { + Object value = attributes.get(key); + if (value == null) { + continue; + } else if (isAttributeDistributable(key, value) && !exclude(key, value)) { + saveNames.add(key); + saveValues.add(value); + } else { + removeAttributeInternal(key, true); + } + } + + // Serialize the attribute count and the Serializable attributes + int n = saveNames.size(); + stream.writeObject(Integer.valueOf(n)); + for (int i = 0; i < n; i++) { + stream.writeObject(saveNames.get(i)); + try { + stream.writeObject(saveValues.get(i)); + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger().trace( + " storing attribute '" + saveNames.get(i) + "' with value '" + saveValues.get(i) + "'"); + } + } catch (NotSerializableException e) { + manager.getContext().getLogger() + .warn(sm.getString("standardSession.notSerializable", saveNames.get(i), id), e); + } + } + + } + + /** + * Return whether authentication information shall be persisted or not. + * + * @return {@code true}, if authentication information shall be persisted; {@code false} otherwise + */ + private boolean getPersistAuthentication() { + if (manager instanceof ManagerBase) { + return ((ManagerBase) manager).getPersistAuthentication(); + } + return false; + } + + /** + * Should the given session attribute be excluded? This implementation checks: + *
      + *
    • {@link Constants#excludedAttributeNames}
    • + *
    • {@link Manager#willAttributeDistribute(String, Object)}
    • + *
    + * Note: This method deliberately does not check {@link #isAttributeDistributable(String, Object)} which is kept + * separate to support the checks required in {@link #setAttribute(String, Object, boolean)} + * + * @param name The attribute name + * @param value The attribute value + * + * @return {@code true} if the attribute should be excluded from distribution, otherwise {@code false} + */ + protected boolean exclude(String name, Object value) { + if (Constants.excludedAttributeNames.contains(name)) { + return true; + } + + // Manager is required for remaining check + Manager manager = getManager(); + if (manager == null) { + // Manager may be null during replication of new sessions in a + // cluster. Avoid the NPE. + return false; + } + + // Last check so use a short-cut + return !manager.willAttributeDistribute(name, value); + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Notify all session event listeners that a particular event has occurred for this Session. The default + * implementation performs this notification synchronously using the calling thread. + * + * @param type Event type + * @param data Event data + */ + public void fireSessionEvent(String type, Object data) { + if (listeners.size() < 1) { + return; + } + SessionEvent event = new SessionEvent(this, type, data); + SessionListener list[] = new SessionListener[0]; + synchronized (listeners) { + list = listeners.toArray(list); + } + + for (SessionListener sessionListener : list) { + sessionListener.sessionEvent(event); + } + + } + + + /** + * @return the names of all currently defined session attributes as an array of Strings. If there are no defined + * attributes, a zero-length array is returned. + */ + protected String[] keys() { + + return attributes.keySet().toArray(EMPTY_ARRAY); + + } + + + /** + * Remove the object bound with the specified name from this session. If the session does not have an object bound + * with this name, this method does nothing. + *

    + * After this method executes, and if the object implements HttpSessionBindingListener, the container + * calls valueUnbound() on the object. + * + * @param name Name of the object to remove from this session. + * @param notify Should we notify interested listeners that this attribute is being removed? + */ + protected void removeAttributeInternal(String name, boolean notify) { + + // Avoid NPE + if (name == null) { + return; + } + + // Remove this attribute from our collection + Object value = attributes.remove(name); + + // Do we need to do valueUnbound() and attributeRemoved() notification? + if (!notify || (value == null)) { + return; + } + + // Call the valueUnbound() method if necessary + HttpSessionBindingEvent event = null; + if (value instanceof HttpSessionBindingListener) { + event = new HttpSessionBindingEvent(getSession(), name, value); + ((HttpSessionBindingListener) value).valueUnbound(event); + } + + // Notify interested application event listeners + Context context = manager.getContext(); + Object listeners[] = context.getApplicationEventListeners(); + if (listeners == null) { + return; + } + for (Object o : listeners) { + if (!(o instanceof HttpSessionAttributeListener)) { + continue; + } + HttpSessionAttributeListener listener = (HttpSessionAttributeListener) o; + try { + context.fireContainerEvent("beforeSessionAttributeRemoved", listener); + if (event == null) { + event = new HttpSessionBindingEvent(getSession(), name, value); + } + listener.attributeRemoved(event); + context.fireContainerEvent("afterSessionAttributeRemoved", listener); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + try { + context.fireContainerEvent("afterSessionAttributeRemoved", listener); + } catch (Exception e) { + // Ignore + } + manager.getContext().getLogger().error(sm.getString("standardSession.attributeEvent"), t); + } + } + } + + + private static class PrivilegedNewSessionFacade implements PrivilegedAction { + + private final HttpSession session; + + PrivilegedNewSessionFacade(HttpSession session) { + this.session = session; + } + + @Override + public StandardSessionFacade run() { + return new StandardSessionFacade(session); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/session/StandardSessionFacade.java b/java/org/apache/catalina/session/StandardSessionFacade.java new file mode 100644 index 0000000..c0cc5c9 --- /dev/null +++ b/java/org/apache/catalina/session/StandardSessionFacade.java @@ -0,0 +1,123 @@ +/* + * 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.catalina.session; + +import java.util.Enumeration; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpSession; + +/** + * Facade for the StandardSession object. + * + * @author Remy Maucherat + */ +public class StandardSessionFacade implements HttpSession { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new session facade. + * + * @param session The session instance to wrap + */ + public StandardSessionFacade(HttpSession session) { + this.session = session; + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Wrapped session object. + */ + private final HttpSession session; + + + // ---------------------------------------------------- HttpSession Methods + + @Override + public long getCreationTime() { + return session.getCreationTime(); + } + + + @Override + public String getId() { + return session.getId(); + } + + + @Override + public long getLastAccessedTime() { + return session.getLastAccessedTime(); + } + + + @Override + public ServletContext getServletContext() { + return session.getServletContext(); + } + + + @Override + public void setMaxInactiveInterval(int interval) { + session.setMaxInactiveInterval(interval); + } + + + @Override + public int getMaxInactiveInterval() { + return session.getMaxInactiveInterval(); + } + + + @Override + public Object getAttribute(String name) { + return session.getAttribute(name); + } + + + @Override + public Enumeration getAttributeNames() { + return session.getAttributeNames(); + } + + + @Override + public void setAttribute(String name, Object value) { + session.setAttribute(name, value); + } + + + @Override + public void removeAttribute(String name) { + session.removeAttribute(name); + } + + + @Override + public void invalidate() { + session.invalidate(); + } + + + @Override + public boolean isNew() { + return session.isNew(); + } +} diff --git a/java/org/apache/catalina/session/StoreBase.java b/java/org/apache/catalina/session/StoreBase.java new file mode 100644 index 0000000..31ddfe9 --- /dev/null +++ b/java/org/apache/catalina/session/StoreBase.java @@ -0,0 +1,276 @@ +/* + * 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.catalina.session; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Manager; +import org.apache.catalina.Store; +import org.apache.catalina.util.CustomObjectInputStream; +import org.apache.catalina.util.LifecycleBase; +import org.apache.catalina.util.ToStringUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + * Abstract implementation of the {@link Store} interface to support most of the functionality required by a + * {@link Store}. + * + * @author Bip Thelin + */ +public abstract class StoreBase extends LifecycleBase implements Store { + + // ----------------------------------------------------- Instance Variables + + /** + * Name to register for this Store, used for logging. + */ + protected static final String storeName = "StoreBase"; + + /** + * The property change support for this component. + */ + protected final PropertyChangeSupport support = new PropertyChangeSupport(this); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(StoreBase.class); + + /** + * The Manager with which this Store is associated. + */ + protected Manager manager; + + + // ------------------------------------------------------------- Properties + + /** + * @return the name for this Store, used for logging. + */ + public String getStoreName() { + return storeName; + } + + + /** + * Set the Manager with which this Store is associated. + * + * @param manager The newly associated Manager + */ + @Override + public void setManager(Manager manager) { + Manager oldManager = this.manager; + this.manager = manager; + support.firePropertyChange("manager", oldManager, this.manager); + } + + /** + * @return the Manager with which the Store is associated. + */ + @Override + public Manager getManager() { + return this.manager; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Add a property change listener to this component. + * + * @param listener a value of type {@link PropertyChangeListener} + */ + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + /** + * Remove a property change listener from this component. + * + * @param listener The listener to remove + */ + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + support.removePropertyChangeListener(listener); + } + + /** + * Get only those keys of sessions, that are saved in the Store and are to be expired. + * + * @return list of session keys, that are to be expired + * + * @throws IOException if an input-/output error occurred + */ + public String[] expiredKeys() throws IOException { + return keys(); + } + + /** + * Called by our background reaper thread to check if Sessions saved in our store are subject of being expired. If + * so expire the Session and remove it from the Store. + */ + public void processExpires() { + String[] keys = null; + + if (!getState().isAvailable()) { + return; + } + + try { + keys = expiredKeys(); + } catch (IOException e) { + manager.getContext().getLogger().error(sm.getString("store.keysFail"), e); + return; + } + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger() + .trace(getStoreName() + ": processExpires check number of " + keys.length + " sessions"); + } + + long timeNow = System.currentTimeMillis(); + + for (String key : keys) { + try { + StandardSession session = (StandardSession) load(key); + if (session == null) { + continue; + } + int timeIdle = (int) ((timeNow - session.getThisAccessedTime()) / 1000L); + if (timeIdle < session.getMaxInactiveInterval()) { + continue; + } + if (manager.getContext().getLogger().isTraceEnabled()) { + manager.getContext().getLogger() + .trace(getStoreName() + ": processExpires expire store session " + key); + } + boolean isLoaded = false; + if (manager instanceof PersistentManagerBase) { + isLoaded = ((PersistentManagerBase) manager).isLoaded(key); + } else { + try { + if (manager.findSession(key) != null) { + isLoaded = true; + } + } catch (IOException ioe) { + // Ignore - session will be expired + } + } + if (isLoaded) { + // recycle old backup session + session.recycle(); + } else { + // expire swapped out session + session.expire(); + } + remove(key); + } catch (Exception e) { + manager.getContext().getLogger().error(sm.getString("store.expireFail", key), e); + try { + remove(key); + } catch (IOException e2) { + manager.getContext().getLogger().error(sm.getString("store.removeFail", key), e2); + } + } + } + } + + + // --------------------------------------------------------- Protected Methods + + /** + * Create the object input stream to use to read a session from the store. Sub-classes must have set the + * thread context class loader before calling this method. + * + * @param is The input stream provided by the sub-class that will provide the data for a session + * + * @return An appropriately configured ObjectInputStream from which the session can be read. + * + * @throws IOException if a problem occurs creating the ObjectInputStream + */ + protected ObjectInputStream getObjectInputStream(InputStream is) throws IOException { + BufferedInputStream bis = new BufferedInputStream(is); + + CustomObjectInputStream ois; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + if (manager instanceof ManagerBase) { + ManagerBase managerBase = (ManagerBase) manager; + ois = new CustomObjectInputStream(bis, classLoader, manager.getContext().getLogger(), + managerBase.getSessionAttributeValueClassNamePattern(), + managerBase.getWarnOnSessionAttributeFilterFailure()); + } else { + ois = new CustomObjectInputStream(bis, classLoader); + } + + return ois; + } + + + @Override + protected void initInternal() { + // NOOP + } + + + /** + * Start this component and implement the requirements of {@link LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + setState(LifecycleState.STARTING); + } + + + /** + * Stop this component and implement the requirements of {@link LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + setState(LifecycleState.STOPPING); + } + + + @Override + protected void destroyInternal() { + // NOOP + } + + + /** + * @return a String rendering of this object. + */ + @Override + public String toString() { + return ToStringUtil.toString(this, manager); + } +} diff --git a/java/org/apache/catalina/session/TooManyActiveSessionsException.java b/java/org/apache/catalina/session/TooManyActiveSessionsException.java new file mode 100644 index 0000000..25e23fd --- /dev/null +++ b/java/org/apache/catalina/session/TooManyActiveSessionsException.java @@ -0,0 +1,51 @@ +/* + * 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.catalina.session; + +/** + * An exception that indicates the maximum number of active sessions has been reached and the server is refusing to + * create any new sessions. + */ +public class TooManyActiveSessionsException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + /** + * The maximum number of active sessions the server will tolerate. + */ + private final int maxActiveSessions; + + /** + * Creates a new TooManyActiveSessionsException. + * + * @param message A description for the exception. + * @param maxActive The maximum number of active sessions allowed by the session manager. + */ + public TooManyActiveSessionsException(String message, int maxActive) { + super(message); + maxActiveSessions = maxActive; + } + + /** + * Gets the maximum number of sessions allowed by the session manager. + * + * @return The maximum number of sessions allowed by the session manager. + */ + public int getMaxActiveSessions() { + return maxActiveSessions; + } +} diff --git a/java/org/apache/catalina/session/mbeans-descriptors.xml b/java/org/apache/catalina/session/mbeans-descriptors.xml new file mode 100644 index 0000000..f46cf0c --- /dev/null +++ b/java/org/apache/catalina/session/mbeans-descriptors.xml @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/session/package.html b/java/org/apache/catalina/session/package.html new file mode 100644 index 0000000..f618525 --- /dev/null +++ b/java/org/apache/catalina/session/package.html @@ -0,0 +1,63 @@ + + + +

    This package contains the standard Manager and +Session implementations that represent the collection of +active sessions and the individual sessions themselves, respectively, +that are associated with a Context. Additional implementations +of the Manager interface can be based upon the supplied +convenience base class (ManagerBase), if desired. Different +implementations of Session are possible, but a need for +functionality beyond what is provided by the standard implementation +(StandardSession) is not expected.

    + +

    The convenience ManagerBase base class is configured by +setting the following properties:

    +
      +
    • algorithm - Message digest algorithm to be used when + generating session identifiers. This must be the name of an + algorithm supported by the java.security.MessageDigest + class on your platform. [DEFAULT_ALGORITHM]
    • +
    • debug - Debugging detail level for this component. [0]
    • +
    • distributable - Has the web application we are associated with + been marked as "distributable"? If it has, attempts to add or replace + a session attribute object that does not implement the + java.io.Serializable interface will be rejected. + [false]
    • +
    • maxInactiveInterval - The default maximum inactive interval, + in minutes, for sessions created by this Manager. The standard + implementation automatically updates this value based on the configuration + settings in the web application deployment descriptor. [60]
    • +
    • randomClass - The Java class name of the random number generator + to be used when creating session identifiers for this Manager. + [java.security.SecureRandom]
    • +
    + +

    The standard implementation of the Manager interface +(StandardManager) supports the following additional configuration +properties:

    +
      +
    • maxActiveSessions - The maximum number of active sessions that + will be allowed, or -1 for no limit. [-1]
    • +
    • pathname - Pathname to the file that is used to store session + data persistently across container restarts. If this pathname is relative, + it is resolved against the temporary working directory provided by our + associated Context, if any. ["sessions.ser"]
    • +
    + + diff --git a/java/org/apache/catalina/ssi/ByteArrayServletOutputStream.java b/java/org/apache/catalina/ssi/ByteArrayServletOutputStream.java new file mode 100644 index 0000000..aa713db --- /dev/null +++ b/java/org/apache/catalina/ssi/ByteArrayServletOutputStream.java @@ -0,0 +1,85 @@ +/* + * 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.catalina.ssi; + +import java.io.ByteArrayOutputStream; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; + + +/** + * Class that extends ServletOutputStream, used as a wrapper from within SsiInclude + * + * @author Bip Thelin + * + * @see ServletOutputStream and ByteArrayOutputStream + */ +public class ByteArrayServletOutputStream extends ServletOutputStream { + /** + * Our buffer to hold the stream. + */ + protected final ByteArrayOutputStream buf; + + + /** + * Construct a new ServletOutputStream. + */ + public ByteArrayServletOutputStream() { + buf = new ByteArrayOutputStream(); + } + + + /** + * @return the byte array. + */ + public byte[] toByteArray() { + return buf.toByteArray(); + } + + + /** + * Write to our buffer. + * + * @param b The parameter to write + */ + @Override + public void write(int b) { + buf.write(b); + } + + /** + * TODO SERVLET 3.1 + */ + @Override + public boolean isReady() { + // TODO Auto-generated method stub + return false; + } + + + /** + * TODO SERVLET 3.1 + */ + @Override + public void setWriteListener(WriteListener listener) { + // TODO Auto-generated method stub + + } + + +} diff --git a/java/org/apache/catalina/ssi/ExpressionParseTree.java b/java/org/apache/catalina/ssi/ExpressionParseTree.java new file mode 100644 index 0000000..af18105 --- /dev/null +++ b/java/org/apache/catalina/ssi/ExpressionParseTree.java @@ -0,0 +1,475 @@ +/* + * 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.catalina.ssi; + + +import java.text.ParseException; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Represents a parsed expression. + * + * @author Paul Speed + */ +public class ExpressionParseTree { + private static final StringManager sm = StringManager.getManager(ExpressionParseTree.class); + /** + * Contains the current set of completed nodes. This is a workspace for the parser. Needs to be LinkedList since it + * can contain {@code null}s. + */ + private final LinkedList nodeStack = new LinkedList<>(); + /** + * Contains operator nodes that don't yet have values. This is a workspace for the parser. Needs to be LinkedList + * since it can contain {@code null}s. + */ + private final LinkedList oppStack = new LinkedList<>(); + /** + * The root node after the expression has been parsed. + */ + private Node root; + /** + * The SSIMediator to use when evaluating the expressions. + */ + private final SSIMediator ssiMediator; + + + /** + * Creates a new parse tree for the specified expression. + * + * @param expr The expression string + * @param ssiMediator Used to evaluated the expressions + * + * @throws ParseException a parsing error occurred + */ + public ExpressionParseTree(String expr, SSIMediator ssiMediator) throws ParseException { + this.ssiMediator = ssiMediator; + parseExpression(expr); + } + + + /** + * Evaluates the tree and returns true or false. The specified SSIMediator is used to resolve variable references. + * + * @return the evaluation result + * + * @throws SSIStopProcessingException If an error occurs evaluating the tree + */ + public boolean evaluateTree() throws SSIStopProcessingException { + try { + return root.evaluate(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + throw new SSIStopProcessingException(t); + } + } + + + /** + * Pushes a new operator onto the opp stack, resolving existing opps as needed. + * + * @param node The operator node + */ + private void pushOpp(OppNode node) { + // If node is null then it's just a group marker + if (node == null) { + oppStack.add(0, node); + return; + } + while (true) { + if (oppStack.size() == 0) { + break; + } + OppNode top = oppStack.get(0); + // If the top is a spacer then don't pop + // anything + if (top == null) { + break; + } + // If the top node has a lower precedence then + // let it stay + if (top.getPrecedence() < node.getPrecedence()) { + break; + } + // Remove the top node + oppStack.remove(0); + // Let it fill its branches + top.popValues(nodeStack); + // Stick it on the resolved node stack + nodeStack.add(0, top); + } + // Add the new node to the opp stack + oppStack.add(0, node); + } + + + /** + * Resolves all pending opp nodes on the stack until the next group marker is reached. + */ + private void resolveGroup() { + OppNode top = null; + while ((top = oppStack.remove(0)) != null) { + // Let it fill its branches + top.popValues(nodeStack); + // Stick it on the resolved node stack + nodeStack.add(0, top); + } + } + + + /** + * Parses the specified expression into a tree of parse nodes. + * + * @param expr The expression to parse + * + * @throws ParseException a parsing error occurred + */ + private void parseExpression(String expr) throws ParseException { + StringNode currStringNode = null; + // We cheat a little and start an artificial + // group right away. It makes finishing easier. + pushOpp(null); + ExpressionTokenizer et = new ExpressionTokenizer(expr); + while (et.hasMoreTokens()) { + int token = et.nextToken(); + if (token != ExpressionTokenizer.TOKEN_STRING) { + currStringNode = null; + } + switch (token) { + case ExpressionTokenizer.TOKEN_STRING: + if (currStringNode == null) { + currStringNode = new StringNode(et.getTokenValue()); + nodeStack.add(0, currStringNode); + } else { + // Add to the existing + currStringNode.value.append(' '); + currStringNode.value.append(et.getTokenValue()); + } + break; + case ExpressionTokenizer.TOKEN_AND: + pushOpp(new AndNode()); + break; + case ExpressionTokenizer.TOKEN_OR: + pushOpp(new OrNode()); + break; + case ExpressionTokenizer.TOKEN_NOT: + pushOpp(new NotNode()); + break; + case ExpressionTokenizer.TOKEN_EQ: + pushOpp(new EqualNode()); + break; + case ExpressionTokenizer.TOKEN_NOT_EQ: + pushOpp(new NotNode()); + // Sneak the regular node in. The NOT will + // be resolved when the next opp comes along. + oppStack.add(0, new EqualNode()); + break; + case ExpressionTokenizer.TOKEN_RBRACE: + // Closeout the current group + resolveGroup(); + break; + case ExpressionTokenizer.TOKEN_LBRACE: + // Push a group marker + pushOpp(null); + break; + case ExpressionTokenizer.TOKEN_GE: + pushOpp(new NotNode()); + // Similar strategy to NOT_EQ above, except this + // is NOT less than + oppStack.add(0, new LessThanNode()); + break; + case ExpressionTokenizer.TOKEN_LE: + pushOpp(new NotNode()); + // Similar strategy to NOT_EQ above, except this + // is NOT greater than + oppStack.add(0, new GreaterThanNode()); + break; + case ExpressionTokenizer.TOKEN_GT: + pushOpp(new GreaterThanNode()); + break; + case ExpressionTokenizer.TOKEN_LT: + pushOpp(new LessThanNode()); + break; + case ExpressionTokenizer.TOKEN_END: + break; + } + } + // Finish off the rest of the opps + resolveGroup(); + if (nodeStack.size() == 0) { + throw new ParseException(sm.getString("expressionParseTree.noNodes"), et.getIndex()); + } + if (nodeStack.size() > 1) { + throw new ParseException(sm.getString("expressionParseTree.extraNodes"), et.getIndex()); + } + if (oppStack.size() != 0) { + throw new ParseException(sm.getString("expressionParseTree.unusedOpCodes"), et.getIndex()); + } + root = nodeStack.get(0); + } + + /** + * A node in the expression parse tree. + */ + private abstract static class Node { + /** + * @return {@code true} if the node evaluates to true. + */ + public abstract boolean evaluate(); + } + + /** + * A node the represents a String value + */ + private class StringNode extends Node { + StringBuilder value; + String resolved = null; + + + StringNode(String value) { + this.value = new StringBuilder(value); + } + + + /** + * Resolves any variable references and returns the value string. + * + * @return the value string + */ + public String getValue() { + if (resolved == null) { + resolved = ssiMediator.substituteVariables(value.toString()); + } + return resolved; + } + + + /** + * Returns true if the string is not empty. + */ + @Override + public boolean evaluate() { + return !(getValue().length() == 0); + } + + + @Override + public String toString() { + return value.toString(); + } + } + + private static final int PRECEDENCE_NOT = 5; + private static final int PRECEDENCE_COMPARE = 4; + private static final int PRECEDENCE_LOGICAL = 1; + + /** + * A node implementation that represents an operation. + */ + private abstract static class OppNode extends Node { + /** + * The left branch. + */ + Node left; + /** + * The right branch. + */ + Node right; + + + /** + * @return a precedence level suitable for comparison to other OppNode preference levels. + */ + public abstract int getPrecedence(); + + + /** + * Lets the node pop its own branch nodes off the front of the specified list. The default pulls two. + * + * @param values The list from which to pop the values + */ + public void popValues(List values) { + right = values.remove(0); + left = values.remove(0); + } + } + + private static final class NotNode extends OppNode { + @Override + public boolean evaluate() { + return !left.evaluate(); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_NOT; + } + + + /** + * Overridden to pop only one value. + */ + @Override + public void popValues(List values) { + left = values.remove(0); + } + + + @Override + public String toString() { + return left + " NOT"; + } + } + + private static final class AndNode extends OppNode { + @Override + public boolean evaluate() { + if (!left.evaluate()) { + return false; + } + return right.evaluate(); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_LOGICAL; + } + + + @Override + public String toString() { + return left + " " + right + " AND"; + } + } + + private static final class OrNode extends OppNode { + @Override + public boolean evaluate() { + if (left.evaluate()) { + return true; + } + return right.evaluate(); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_LOGICAL; + } + + + @Override + public String toString() { + return left + " " + right + " OR"; + } + } + + private abstract class CompareNode extends OppNode { + protected int compareBranches() { + String val1 = ((StringNode) left).getValue(); + String val2 = ((StringNode) right).getValue(); + + int val2Len = val2.length(); + if (val2Len > 1 && val2.charAt(0) == '/' && val2.charAt(val2Len - 1) == '/') { + // Treat as a regular expression + String expr = val2.substring(1, val2Len - 1); + ssiMediator.clearMatchGroups(); + try { + Pattern pattern = Pattern.compile(expr); + // Regular expressions will only ever be used with EqualNode + // so return zero for equal and non-zero for not equal + Matcher matcher = pattern.matcher(val1); + if (matcher.find()) { + ssiMediator.populateMatchGroups(matcher); + return 0; + } else { + return -1; + } + } catch (PatternSyntaxException pse) { + ssiMediator.log(sm.getString("expressionParseTree.invalidExpression", expr), pse); + return 0; + } + } + return val1.compareTo(val2); + } + } + + private final class EqualNode extends CompareNode { + @Override + public boolean evaluate() { + return (compareBranches() == 0); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_COMPARE; + } + + + @Override + public String toString() { + return left + " " + right + " EQ"; + } + } + + private final class GreaterThanNode extends CompareNode { + @Override + public boolean evaluate() { + return (compareBranches() > 0); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_COMPARE; + } + + + @Override + public String toString() { + return left + " " + right + " GT"; + } + } + + private final class LessThanNode extends CompareNode { + @Override + public boolean evaluate() { + return (compareBranches() < 0); + } + + + @Override + public int getPrecedence() { + return PRECEDENCE_COMPARE; + } + + + @Override + public String toString() { + return left + " " + right + " LT"; + } + } +} diff --git a/java/org/apache/catalina/ssi/ExpressionTokenizer.java b/java/org/apache/catalina/ssi/ExpressionTokenizer.java new file mode 100644 index 0000000..59e9dbe --- /dev/null +++ b/java/org/apache/catalina/ssi/ExpressionTokenizer.java @@ -0,0 +1,195 @@ +/* + * 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.catalina.ssi; + + +/** + * Parses an expression string to return the individual tokens. This is patterned similar to the StreamTokenizer in the + * JDK but customized for SSI conditional expression parsing. + * + * @author Paul Speed + */ +public class ExpressionTokenizer { + public static final int TOKEN_STRING = 0; + public static final int TOKEN_AND = 1; + public static final int TOKEN_OR = 2; + public static final int TOKEN_NOT = 3; + public static final int TOKEN_EQ = 4; + public static final int TOKEN_NOT_EQ = 5; + public static final int TOKEN_RBRACE = 6; + public static final int TOKEN_LBRACE = 7; + public static final int TOKEN_GE = 8; + public static final int TOKEN_LE = 9; + public static final int TOKEN_GT = 10; + public static final int TOKEN_LT = 11; + public static final int TOKEN_END = 12; + private final char[] expr; + private String tokenVal = null; + private int index; + private final int length; + + + /** + * Creates a new parser for the specified expression. + * + * @param expr The expression + */ + public ExpressionTokenizer(String expr) { + this.expr = expr.trim().toCharArray(); + this.length = this.expr.length; + } + + + /** + * @return true if there are more tokens. + */ + public boolean hasMoreTokens() { + return index < length; + } + + + /** + * @return the current index for error reporting purposes. + */ + public int getIndex() { + return index; + } + + + protected boolean isMetaChar(char c) { + return Character.isWhitespace(c) || c == '(' || c == ')' || c == '!' || c == '<' || c == '>' || c == '|' || + c == '&' || c == '='; + } + + + /** + * @return the next token type and initializes any state variables accordingly. + */ + public int nextToken() { + // Skip any leading white space + while (index < length && Character.isWhitespace(expr[index])) { + index++; + } + // Clear the current token val + tokenVal = null; + if (index == length) { + return TOKEN_END; // End of string + } + int start = index; + char currentChar = expr[index]; + char nextChar = (char) 0; + index++; + if (index < length) { + nextChar = expr[index]; + } + // Check for a known token start + switch (currentChar) { + case '(': + return TOKEN_LBRACE; + case ')': + return TOKEN_RBRACE; + case '=': + return TOKEN_EQ; + case '!': + if (nextChar == '=') { + index++; + return TOKEN_NOT_EQ; + } + return TOKEN_NOT; + case '|': + if (nextChar == '|') { + index++; + return TOKEN_OR; + } + break; + case '&': + if (nextChar == '&') { + index++; + return TOKEN_AND; + } + break; + case '>': + if (nextChar == '=') { + index++; + return TOKEN_GE; // Greater than or equal + } + return TOKEN_GT; // Greater than + case '<': + if (nextChar == '=') { + index++; + return TOKEN_LE; // Less than or equal + } + return TOKEN_LT; // Less than + default: + // Otherwise it's a string + break; + } + int end = index; + if (currentChar == '"' || currentChar == '\'') { + // It's a quoted string and the end is the next unescaped quote + char endChar = currentChar; + boolean escaped = false; + start++; + for (; index < length; index++) { + if (expr[index] == '\\' && !escaped) { + escaped = true; + continue; + } + if (expr[index] == endChar && !escaped) { + break; + } + escaped = false; + } + end = index; + index++; // Skip the end quote + } else if (currentChar == '/') { + // It's a regular expression and the end is the next unescaped / + char endChar = currentChar; + boolean escaped = false; + for (; index < length; index++) { + if (expr[index] == '\\' && !escaped) { + escaped = true; + continue; + } + if (expr[index] == endChar && !escaped) { + break; + } + escaped = false; + } + end = ++index; + } else { + // End is the next whitespace character + for (; index < length; index++) { + if (isMetaChar(expr[index])) { + break; + } + } + end = index; + } + // Extract the string from the array + this.tokenVal = new String(expr, start, end - start); + return TOKEN_STRING; + } + + + /** + * @return the String value of the token if it was type TOKEN_STRING. Otherwise null is returned. + */ + public String getTokenValue() { + return tokenVal; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/LocalStrings.properties b/java/org/apache/catalina/ssi/LocalStrings.properties new file mode 100644 index 0000000..68736ee --- /dev/null +++ b/java/org/apache/catalina/ssi/LocalStrings.properties @@ -0,0 +1,46 @@ +# 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. + +expressionParseTree.extraNodes=Extra nodes created +expressionParseTree.invalidExpression=Invalid expression [{0}] +expressionParseTree.noNodes=No nodes created +expressionParseTree.unusedOpCodes=Unused nodes exist + +ssiCommand.invalidAttribute=Invalid attribute [{0}] + +ssiEcho.invalidEncoding=Invalid encoding [{0}] + +ssiExec.executeFailed=Cannot execute file [{0}] + +ssiFlastmod.noLastModified=Cannot get last modification date for file [{0}] + +ssiFsize.invalidNumChars=The number of characters cannot be negative +ssiFsize.noSize=Cannot get size for file [{0}] + +ssiInclude.includeFailed=Cannot include file [{0}] + +ssiMediator.unknownEncoding=Unknown encoding [{0}] + +ssiServletExternalResolver.absoluteNonVirtualPath=Non virtual [{0}] path cannot be absolute +ssiServletExternalResolver.noContext=No context for path normalized to [{0}] +ssiServletExternalResolver.noFile=File [{0}] not found +ssiServletExternalResolver.noIncludeFile=Include file [{0}] not found +ssiServletExternalResolver.noResource=Context did not contain resource [{0}] +ssiServletExternalResolver.normalizationError=Normalization returned null for path [{0}] +ssiServletExternalResolver.pathTraversalNonVirtualPath=Non virtual path [{0}] cannot contain path traversal sequences +ssiServletExternalResolver.removeFilenameError=Cannot remove filename from path [{0}] +ssiServletExternalResolver.requestDispatcherError=Cannot get request dispatcher for path [{0}] + +ssiSet.noVariable=No variable specified diff --git a/java/org/apache/catalina/ssi/LocalStrings_fr.properties b/java/org/apache/catalina/ssi/LocalStrings_fr.properties new file mode 100644 index 0000000..b856366 --- /dev/null +++ b/java/org/apache/catalina/ssi/LocalStrings_fr.properties @@ -0,0 +1,46 @@ +# 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. + +expressionParseTree.extraNodes=Des nÅ“uds supplémentaires ont été crées +expressionParseTree.invalidExpression=L''expression [{0}] est invalide +expressionParseTree.noNodes=Aucun nÅ“ud n'a été crée +expressionParseTree.unusedOpCodes=Il y a des nÅ“uds non utilisés + +ssiCommand.invalidAttribute=L''attribut [{0}] est invalide + +ssiEcho.invalidEncoding=L''encodage [{0}] est invalide + +ssiExec.executeFailed=Impossible d''exécuter le fichier [{0}] + +ssiFlastmod.noLastModified=Impossible d''obtenir la date de dernière modification du fichier [{0}] + +ssiFsize.invalidNumChars=Le nombre de caractères ne peut être négatif +ssiFsize.noSize=Impossible d''obtenir la taille du fichier [{0}] + +ssiInclude.includeFailed=Impossible d''inclure le fichier [{0}] + +ssiMediator.unknownEncoding=L''encodage [{0}] est inconnu + +ssiServletExternalResolver.absoluteNonVirtualPath=Le chemin non virtuel [{0}] ne peut être absolu +ssiServletExternalResolver.noContext=Pas de contexte dans chemin normalisé en [{0}] +ssiServletExternalResolver.noFile=Le fichier [{0}] n''a pas été trouvé +ssiServletExternalResolver.noIncludeFile=Le fichier inclus [{0}] n''a pas été trouvé +ssiServletExternalResolver.noResource=Le contexte ne contenait pas la ressource [{0}] +ssiServletExternalResolver.normalizationError=La normalisation du chemin [{0}] a retourné null +ssiServletExternalResolver.pathTraversalNonVirtualPath=Le chemin non virtuel [{0}] ne peut contenir des séquences de navigation dans le chemin +ssiServletExternalResolver.removeFilenameError=Impossible de supprimer le nom de fichier du chemin [{0}] +ssiServletExternalResolver.requestDispatcherError=Impossible d''obtenir le dispatcher de requêtes pour le chemin [{0}] + +ssiSet.noVariable=Une variable n'a pas été spécifiée diff --git a/java/org/apache/catalina/ssi/LocalStrings_ja.properties b/java/org/apache/catalina/ssi/LocalStrings_ja.properties new file mode 100644 index 0000000..c445dbf --- /dev/null +++ b/java/org/apache/catalina/ssi/LocalStrings_ja.properties @@ -0,0 +1,46 @@ +# 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. + +expressionParseTree.extraNodes=余分ãªãƒŽãƒ¼ãƒ‰ãŒä½œæˆã•ã‚Œã¾ã—㟠+expressionParseTree.invalidExpression=無効ãªå¼[{0}] +expressionParseTree.noNodes=ノードãŒä½œæˆã•ã‚Œã¾ã›ã‚“ã§ã—㟠+expressionParseTree.unusedOpCodes=未使用ã®ãƒŽãƒ¼ãƒ‰ãŒå­˜åœ¨ã—ã¾ã™ + +ssiCommand.invalidAttribute=無効ãªå±žæ€§[{0}] + +ssiEcho.invalidEncoding=無効ãªã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ [{0}] + +ssiExec.executeFailed=ファイル[{0}]を実行ã§ãã¾ã›ã‚“ + +ssiFlastmod.noLastModified=ファイル[{0}]ã®æœ€çµ‚変更日をå–å¾—ã§ãã¾ã›ã‚“ + +ssiFsize.invalidNumChars=文字数ã¯è² æ•°ã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +ssiFsize.noSize=ファイル[{0}]ã®ã‚µã‚¤ã‚ºã‚’å–å¾—ã§ãã¾ã›ã‚“ + +ssiInclude.includeFailed=ファイル[{0}]ã‚’å«ã‚ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ + +ssiMediator.unknownEncoding=ä¸æ˜Žãªã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ [{0}] + +ssiServletExternalResolver.absoluteNonVirtualPath=éžä»®æƒ³[{0}]パスを絶対パスã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +ssiServletExternalResolver.noContext=[{0}]ã«æ­£è¦åŒ–ã•ã‚ŒãŸãƒ‘スã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãŒã‚ã‚Šã¾ã›ã‚“。 +ssiServletExternalResolver.noFile=ファイル[{0}]ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +ssiServletExternalResolver.noIncludeFile=インクルードファイル[{0}]ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +ssiServletExternalResolver.noResource=コンテキストã«ãƒªã‚½ãƒ¼ã‚¹ [{0}] ãŒå«ã¾ã‚Œã¦ã„ã¾ã›ã‚“ +ssiServletExternalResolver.normalizationError=パス [{0}] ã®æ­£è¦åŒ–ã«ã‚ˆã£ã¦NULLãŒè¿”ã•ã‚Œã¾ã—㟠+ssiServletExternalResolver.pathTraversalNonVirtualPath=éžä»®æƒ³ãƒ‘ス [{0}] ã«ã¯ãƒ‘ストラãƒãƒ¼ã‚µãƒ«ã‚·ãƒ¼ã‚±ãƒ³ã‚¹ã‚’å«ã‚ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +ssiServletExternalResolver.removeFilenameError=パス [{0}] ã®ãƒ•ã‚¡ã‚¤ãƒ«åを削除ã§ãã¾ã›ã‚“ +ssiServletExternalResolver.requestDispatcherError=パス [{0}] ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ‡ã‚£ã‚¹ãƒ‘ッãƒãƒ£ã‚’å–å¾—ã§ãã¾ã›ã‚“ + +ssiSet.noVariable=変数ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ diff --git a/java/org/apache/catalina/ssi/LocalStrings_ko.properties b/java/org/apache/catalina/ssi/LocalStrings_ko.properties new file mode 100644 index 0000000..e6da65c --- /dev/null +++ b/java/org/apache/catalina/ssi/LocalStrings_ko.properties @@ -0,0 +1,46 @@ +# 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. + +expressionParseTree.extraNodes=파싱 처리 중 ìƒì„±ëœ ë…¸ë“œë“¤ì˜ ì—¬ë¶„ì´ ì²˜ë¦¬ë˜ì§€ ì•Šê³  남아있습니다. +expressionParseTree.invalidExpression=유효하지 ì•Šì€ í‘œí˜„ì‹ [{0}] +expressionParseTree.noNodes=파싱 처리 중 ì–´ë–¤ ë…¸ë“œë„ ìƒì„±ë˜ì§€ 않았습니다. +expressionParseTree.unusedOpCodes=파싱 처리 중 사용ë˜ì§€ ì•Šì€ ë…¸ë“œë“¤ì´ ì¡´ìž¬í•©ë‹ˆë‹¤. + +ssiCommand.invalidAttribute=유효하지 ì•Šì€ ì†ì„± [{0}] + +ssiEcho.invalidEncoding=유효하지 ì•Šì€ ì¸ì½”딩 [{0}] + +ssiExec.executeFailed=íŒŒì¼ [{0}]ì„(를) 실행할 수 없습니다. + +ssiFlastmod.noLastModified=íŒŒì¼ [{0}]ì˜ ìµœì¢… 변경 ì¼ìžë¥¼ 구할 수 없습니다. + +ssiFsize.invalidNumChars=문ìžë“¤ì˜ 개수가 ìŒìˆ˜ì¼ 수는 없습니다. +ssiFsize.noSize=íŒŒì¼ [{0}]ì˜ í¬ê¸°ë¥¼ 구할 수 없습니다. + +ssiInclude.includeFailed=íŒŒì¼ [{0}]ì„(를) includeí•  수 없습니다. + +ssiMediator.unknownEncoding=ì•Œ 수 없는 ì¸ì½”딩 [{0}] + +ssiServletExternalResolver.absoluteNonVirtualPath=비가ìƒê²½ë¡œ [{0}]ì€(는) 절대 경로ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +ssiServletExternalResolver.noContext=[{0}](으)ë¡œ ì •ê·œí™”ëœ ê²½ë¡œë¥¼ 위한 컨í…스트가 없습니다. +ssiServletExternalResolver.noFile=íŒŒì¼ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +ssiServletExternalResolver.noIncludeFile=Includeí•  íŒŒì¼ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +ssiServletExternalResolver.noResource=컨í…스트가 리소스 [{0}]ì„(를) í¬í•¨í•˜ì§€ 않았습니다. +ssiServletExternalResolver.normalizationError=경로 [{0}]ì„(를) 위한 정규화가 ë„ì„ ë°˜í™˜í–ˆìŠµë‹ˆë‹¤. +ssiServletExternalResolver.pathTraversalNonVirtualPath=비가ìƒê²½ë¡œ [{0}]ì€(는), 디렉토리를 ì´ë™í•˜ëŠ” 문ìžì—´ 시퀀스를 í¬í•¨í•´ì„œëŠ” 안ë©ë‹ˆë‹¤. +ssiServletExternalResolver.removeFilenameError=경로 [{0}](으)로부터, íŒŒì¼ ì´ë¦„ì„ ì œì™¸í•œ 나머지 경로를 구할 수 없습니다. +ssiServletExternalResolver.requestDispatcherError=경로 [{0}]ì„(를) 위한 요청 디스패처를 ì–»ì„ ìˆ˜ 없습니다. + +ssiSet.noVariable=변수 ì´ë¦„ì´ ì§€ì •ë˜ì§€ 않았습니다. diff --git a/java/org/apache/catalina/ssi/LocalStrings_zh_CN.properties b/java/org/apache/catalina/ssi/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..69617f1 --- /dev/null +++ b/java/org/apache/catalina/ssi/LocalStrings_zh_CN.properties @@ -0,0 +1,46 @@ +# 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. + +expressionParseTree.extraNodes=创建é¢å¤–节点 +expressionParseTree.invalidExpression=无效表达å¼[{0}] +expressionParseTree.noNodes=未创建节点 +expressionParseTree.unusedOpCodes=存在未使用的节点 + +ssiCommand.invalidAttribute=无效属性[{0}] + +ssiEcho.invalidEncoding=无效编ç [{0}] + +ssiExec.executeFailed=无法执行文件[{0}] + +ssiFlastmod.noLastModified=无法获å–文件[{0}]的最åŽä¿®æ”¹æ—¥æœŸ + +ssiFsize.invalidNumChars=字符数ä¸èƒ½ä¸ºè´Ÿ +ssiFsize.noSize=无法获å–文件[{0}]çš„å¤§å° + +ssiInclude.includeFailed=ä¸èƒ½åŒ…å«æ–‡ä»¶[{0}] + +ssiMediator.unknownEncoding=未知编ç [{0}] + +ssiServletExternalResolver.absoluteNonVirtualPath=éžè™š[{0}]路径ä¸èƒ½æ˜¯ç»å¯¹è·¯å¾„ +ssiServletExternalResolver.noContext=没有将路径规范化为[{0}]的上下文 +ssiServletExternalResolver.noFile=找ä¸åˆ°æ–‡ä»¶[{0}] +ssiServletExternalResolver.noIncludeFile=未找到包å«æ–‡ä»¶[{0}] +ssiServletExternalResolver.noResource=上下文ä¸åŒ…å«èµ„æº[{0}] +ssiServletExternalResolver.normalizationError=规范化为路径[{0}]返回了空值 +ssiServletExternalResolver.pathTraversalNonVirtualPath=éžè™šæ‹Ÿè·¯å¾„[{0}]ä¸èƒ½åŒ…å«è·¯å¾„é历åºåˆ— +ssiServletExternalResolver.removeFilenameError=无法从路径[{0}]中删除文件å +ssiServletExternalResolver.requestDispatcherError=无法获å–路径[{0}]çš„è¯·æ±‚è°ƒåº¦ç¨‹åº + +ssiSet.noVariable=未指定å˜é‡ diff --git a/java/org/apache/catalina/ssi/ResponseIncludeWrapper.java b/java/org/apache/catalina/ssi/ResponseIncludeWrapper.java new file mode 100644 index 0000000..ed5629d --- /dev/null +++ b/java/org/apache/catalina/ssi/ResponseIncludeWrapper.java @@ -0,0 +1,171 @@ +/* + * 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.catalina.ssi; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.Locale; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import org.apache.tomcat.util.http.FastHttpDateFormat; + +/** + * An HttpServletResponseWrapper, used from SSIServletExternalResolver + * + * @author Bip Thelin + * @author David Becker + */ +public class ResponseIncludeWrapper extends HttpServletResponseWrapper { + /** + * The names of some headers we want to capture. + */ + private static final String LAST_MODIFIED = "last-modified"; + + protected long lastModified = -1; + + /** + * Our ServletOutputStream + */ + protected final ServletOutputStream captureServletOutputStream; + protected ServletOutputStream servletOutputStream; + protected PrintWriter printWriter; + + /** + * Initialize our wrapper with the current HttpServletResponse and ServletOutputStream. + * + * @param response The response to use + * @param captureServletOutputStream The ServletOutputStream to use + */ + public ResponseIncludeWrapper(HttpServletResponse response, ServletOutputStream captureServletOutputStream) { + super(response); + this.captureServletOutputStream = captureServletOutputStream; + } + + + /** + * Flush the servletOutputStream or printWriter ( only one will be non-null ) This must be called after a + * requestDispatcher.include, since we can't assume that the included servlet flushed its stream. + * + * @throws IOException an IO error occurred + */ + public void flushOutputStreamOrWriter() throws IOException { + if (servletOutputStream != null) { + servletOutputStream.flush(); + } + if (printWriter != null) { + printWriter.flush(); + } + } + + + /** + * Return a printwriter, throws an exception if an OutputStream already been returned. + * + * @return a PrintWriter object + * + * @exception java.io.IOException if the outputstream already been called + */ + @Override + public PrintWriter getWriter() throws IOException { + if (servletOutputStream == null) { + if (printWriter == null) { + setCharacterEncoding(getCharacterEncoding()); + printWriter = + new PrintWriter(new OutputStreamWriter(captureServletOutputStream, getCharacterEncoding())); + } + return printWriter; + } + throw new IllegalStateException(); + } + + + /** + * Return an OutputStream, throws an exception if a printwriter already been returned. + * + * @return an OutputStream object + * + * @exception java.io.IOException if the printwriter already been called + */ + @Override + public ServletOutputStream getOutputStream() throws IOException { + if (printWriter == null) { + if (servletOutputStream == null) { + servletOutputStream = captureServletOutputStream; + } + return servletOutputStream; + } + throw new IllegalStateException(); + } + + + /** + * Returns the value of the last-modified header field. The result is the number of milliseconds since + * January 1, 1970 GMT. + * + * @return the date the resource referenced by this ResponseIncludeWrapper was last modified, or -1 if + * not known. + */ + public long getLastModified() { + return lastModified; + } + + @Override + public void addDateHeader(String name, long value) { + super.addDateHeader(name, value); + String lname = name.toLowerCase(Locale.ENGLISH); + if (lname.equals(LAST_MODIFIED)) { + lastModified = value; + } + } + + @Override + public void addHeader(String name, String value) { + super.addHeader(name, value); + String lname = name.toLowerCase(Locale.ENGLISH); + if (lname.equals(LAST_MODIFIED)) { + long lastModified = FastHttpDateFormat.parseDate(value); + if (lastModified != -1) { + this.lastModified = lastModified; + } + } + } + + @Override + public void setDateHeader(String name, long value) { + super.setDateHeader(name, value); + String lname = name.toLowerCase(Locale.ENGLISH); + if (lname.equals(LAST_MODIFIED)) { + lastModified = value; + } + } + + @Override + public void setHeader(String name, String value) { + super.setHeader(name, value); + String lname = name.toLowerCase(Locale.ENGLISH); + if (lname.equals(LAST_MODIFIED)) { + long lastModified = FastHttpDateFormat.parseDate(value); + if (lastModified != -1) { + this.lastModified = lastModified; + } + } + } +} diff --git a/java/org/apache/catalina/ssi/SSICommand.java b/java/org/apache/catalina/ssi/SSICommand.java new file mode 100644 index 0000000..83b630f --- /dev/null +++ b/java/org/apache/catalina/ssi/SSICommand.java @@ -0,0 +1,45 @@ +/* + * 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.catalina.ssi; + + +import java.io.PrintWriter; + +/** + * The interface that all SSI commands ( SSIEcho, SSIInclude, ...) must implement. + * + * @author Bip Thelin + * @author Dan Sandberg + * @author David Becker + */ +public interface SSICommand { + /** + * Write the output of the command to the writer. + * + * @param ssiMediator the ssi mediator + * @param commandName the name of the actual command ( ie. echo ) + * @param paramNames The parameter names + * @param paramValues The parameter values + * @param writer the writer to output to + * + * @return the most current modified date resulting from any SSI commands + * + * @throws SSIStopProcessingException if SSI processing should be aborted + */ + long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) throws SSIStopProcessingException; +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSIConditional.java b/java/org/apache/catalina/ssi/SSIConditional.java new file mode 100644 index 0000000..780c9c3 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIConditional.java @@ -0,0 +1,142 @@ +/* + * 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.catalina.ssi; + + +import java.io.PrintWriter; +import java.text.ParseException; + +/** + * SSI command that handles all conditional directives. + * + * @author Paul Speed + * @author David Becker + */ +public class SSIConditional implements SSICommand { + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) throws SSIStopProcessingException { + // Assume anything using conditionals was modified by it + long lastModified = System.currentTimeMillis(); + // Retrieve the current state information + SSIConditionalState state = ssiMediator.getConditionalState(); + if ("if".equalsIgnoreCase(commandName)) { + // Do nothing if we are nested in a false branch + // except count it + if (state.processConditionalCommandsOnly) { + state.nestingCount++; + return lastModified; + } + state.nestingCount = 0; + // Evaluate the expression + if (evaluateArguments(paramNames, paramValues, ssiMediator)) { + // No more branches can be taken for this if block + state.branchTaken = true; + } else { + // Do not process this branch + state.processConditionalCommandsOnly = true; + state.branchTaken = false; + } + } else if ("elif".equalsIgnoreCase(commandName)) { + // No need to even execute if we are nested in + // a false branch + if (state.nestingCount > 0) { + return lastModified; + } + // If a branch was already taken in this if block + // then disable output and return + if (state.branchTaken) { + state.processConditionalCommandsOnly = true; + return lastModified; + } + // Evaluate the expression + if (evaluateArguments(paramNames, paramValues, ssiMediator)) { + // Turn back on output and mark the branch + state.processConditionalCommandsOnly = false; + state.branchTaken = true; + } else { + // Do not process this branch + state.processConditionalCommandsOnly = true; + state.branchTaken = false; + } + } else if ("else".equalsIgnoreCase(commandName)) { + // No need to even execute if we are nested in + // a false branch + if (state.nestingCount > 0) { + return lastModified; + } + // If we've already taken another branch then + // disable output otherwise enable it. + state.processConditionalCommandsOnly = state.branchTaken; + // And in any case, it's safe to say a branch + // has been taken. + state.branchTaken = true; + } else if ("endif".equalsIgnoreCase(commandName)) { + // If we are nested inside a false branch then pop out + // one level on the nesting count + if (state.nestingCount > 0) { + state.nestingCount--; + return lastModified; + } + // Turn output back on + state.processConditionalCommandsOnly = false; + // Reset the branch status for any outer if blocks, + // since clearly we took a branch to have gotten here + // in the first place. + state.branchTaken = true; + } else { + throw new SSIStopProcessingException(); + // throw new SsiCommandException( "Not a conditional command:" + + // cmdName ); + } + return lastModified; + } + + + /** + * Retrieves the expression from the specified arguments and performs the necessary evaluation steps. + */ + private boolean evaluateArguments(String[] names, String[] values, SSIMediator ssiMediator) + throws SSIStopProcessingException { + String expr = getExpression(names, values); + if (expr == null) { + throw new SSIStopProcessingException(); + // throw new SsiCommandException( "No expression specified." ); + } + try { + ExpressionParseTree tree = new ExpressionParseTree(expr, ssiMediator); + return tree.evaluateTree(); + } catch (ParseException e) { + // throw new SsiCommandException( "Error parsing expression." ); + throw new SSIStopProcessingException(); + } + } + + + /** + * Returns the "expr" if the arg name is appropriate, otherwise returns null. + */ + private String getExpression(String[] paramNames, String[] paramValues) { + if ("expr".equalsIgnoreCase(paramNames[0])) { + return paramValues[0]; + } + return null; + } +} diff --git a/java/org/apache/catalina/ssi/SSIConditionalState.java b/java/org/apache/catalina/ssi/SSIConditionalState.java new file mode 100644 index 0000000..e678dd9 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIConditionalState.java @@ -0,0 +1,40 @@ +/* + * 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.catalina.ssi; + + +/** + * This class is used by SSIMediator and SSIConditional to keep track of state information necessary to process the + * nested conditional commands ( if, elif, else, endif ). + * + * @author Dan Sandberg + * @author Paul Speed + */ +class SSIConditionalState { + /** + * Set to true if the current conditional has already been completed, i.e.: a branch was taken. + */ + boolean branchTaken = false; + /** + * Counts the number of nested false branches. + */ + int nestingCount = 0; + /** + * Set to true if only conditional commands ( if, elif, else, endif ) should be processed. + */ + boolean processConditionalCommandsOnly = false; +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSIConfig.java b/java/org/apache/catalina/ssi/SSIConfig.java new file mode 100644 index 0000000..e033155 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIConfig.java @@ -0,0 +1,61 @@ +/* + * 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.catalina.ssi; + + +import java.io.PrintWriter; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Implements the Server-side #exec command + * + * @author Bip Thelin + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +public final class SSIConfig implements SSICommand { + private static final StringManager sm = StringManager.getManager(SSIConfig.class); + + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) { + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + String paramValue = paramValues[i]; + String substitutedValue = ssiMediator.substituteVariables(paramValue); + if (paramName.equalsIgnoreCase("errmsg")) { + ssiMediator.setConfigErrMsg(substitutedValue); + } else if (paramName.equalsIgnoreCase("sizefmt")) { + ssiMediator.setConfigSizeFmt(substitutedValue); + } else if (paramName.equalsIgnoreCase("timefmt")) { + ssiMediator.setConfigTimeFmt(substitutedValue); + } else { + ssiMediator.log(sm.getString("ssiCommand.invalidAttribute", paramName)); + // We need to fetch this value each time, since it may change during the loop + String configErrMsg = ssiMediator.getConfigErrMsg(); + writer.write(configErrMsg); + } + } + // Setting config options doesn't really change the page + return 0; + } +} diff --git a/java/org/apache/catalina/ssi/SSIEcho.java b/java/org/apache/catalina/ssi/SSIEcho.java new file mode 100644 index 0000000..c0e90b7 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIEcho.java @@ -0,0 +1,79 @@ +/* + * 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.catalina.ssi; + + +import java.io.PrintWriter; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Return the result associated with the supplied Server Variable. + * + * @author Bip Thelin + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +public class SSIEcho implements SSICommand { + private static final StringManager sm = StringManager.getManager(SSIEcho.class); + protected static final String DEFAULT_ENCODING = SSIMediator.ENCODING_ENTITY; + protected static final String MISSING_VARIABLE_VALUE = "(none)"; + + + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) { + String encoding = DEFAULT_ENCODING; + String originalValue = null; + String errorMessage = ssiMediator.getConfigErrMsg(); + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + String paramValue = paramValues[i]; + if (paramName.equalsIgnoreCase("var")) { + originalValue = paramValue; + } else if (paramName.equalsIgnoreCase("encoding")) { + if (isValidEncoding(paramValue)) { + encoding = paramValue; + } else { + ssiMediator.log(sm.getString("ssiEcho.invalidEncoding", paramValue)); + writer.write(ssiMediator.encode(errorMessage, SSIMediator.ENCODING_ENTITY)); + } + } else { + ssiMediator.log(sm.getString("ssiCommand.invalidAttribute", paramName)); + writer.write(ssiMediator.encode(errorMessage, SSIMediator.ENCODING_ENTITY)); + } + } + String variableValue = (originalValue == null) ? MISSING_VARIABLE_VALUE : + ssiMediator.getVariableValue(originalValue, encoding); + if (variableValue == null) { + variableValue = MISSING_VARIABLE_VALUE; + } + writer.write(variableValue); + return System.currentTimeMillis(); + } + + + protected boolean isValidEncoding(String encoding) { + return encoding.equalsIgnoreCase(SSIMediator.ENCODING_URL) || + encoding.equalsIgnoreCase(SSIMediator.ENCODING_ENTITY) || + encoding.equalsIgnoreCase(SSIMediator.ENCODING_NONE); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSIExec.java b/java/org/apache/catalina/ssi/SSIExec.java new file mode 100644 index 0000000..6c597d1 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIExec.java @@ -0,0 +1,84 @@ +/* + * 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.catalina.ssi; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; + +import org.apache.catalina.util.IOTools; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implements the Server-side #exec command + * + * @author Bip Thelin + * @author Amy Roh + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +public class SSIExec implements SSICommand { + private static final StringManager sm = StringManager.getManager(SSIExec.class); + protected final SSIInclude ssiInclude = new SSIInclude(); + protected static final int BUFFER_SIZE = 1024; + + + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) { + long lastModified = 0; + String configErrMsg = ssiMediator.getConfigErrMsg(); + String paramName = paramNames[0]; + String paramValue = paramValues[0]; + String substitutedValue = ssiMediator.substituteVariables(paramValue); + if (paramName.equalsIgnoreCase("cgi")) { + lastModified = ssiInclude.process(ssiMediator, "include", new String[] { "virtual" }, + new String[] { substitutedValue }, writer); + } else if (paramName.equalsIgnoreCase("cmd")) { + boolean foundProgram = false; + try { + Runtime rt = Runtime.getRuntime(); + Process proc = rt.exec(substitutedValue); + foundProgram = true; + char[] buf = new char[BUFFER_SIZE]; + try (BufferedReader stdOutReader = new BufferedReader(new InputStreamReader(proc.getInputStream())); + BufferedReader stdErrReader = + new BufferedReader(new InputStreamReader(proc.getErrorStream()));) { + IOTools.flow(stdErrReader, writer, buf); + IOTools.flow(stdOutReader, writer, buf); + } + proc.waitFor(); + lastModified = System.currentTimeMillis(); + } catch (InterruptedException e) { + ssiMediator.log(sm.getString("ssiExec.executeFailed", substitutedValue), e); + writer.write(configErrMsg); + } catch (IOException e) { + if (!foundProgram) { + // Apache doesn't output an error message if it can't find + // a program + } + ssiMediator.log(sm.getString("ssiExec.executeFailed", substitutedValue), e); + } + } + return lastModified; + } +} diff --git a/java/org/apache/catalina/ssi/SSIExternalResolver.java b/java/org/apache/catalina/ssi/SSIExternalResolver.java new file mode 100644 index 0000000..5d0a672 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIExternalResolver.java @@ -0,0 +1,70 @@ +/* + * 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.catalina.ssi; + + +import java.io.IOException; +import java.util.Collection; +import java.util.Date; + +/** + * Interface used by SSIMediator to talk to the 'outside world' ( usually a servlet ) + * + * @author Dan Sandberg + */ +public interface SSIExternalResolver { + /** + * Adds any external variables to the variableNames collection. + * + * @param variableNames the collection to add to + */ + void addVariableNames(Collection variableNames); + + + String getVariableValue(String name); + + + /** + * Set the named variable to the specified value. If value is null, then the variable will be removed ( ie. a call + * to getVariableValue will return null ) + * + * @param name of the variable + * @param value of the variable + */ + void setVariableValue(String name, String value); + + + /** + * Returns the current date. This is useful for putting the SSI stuff in a regression test. Since you can make the + * current date a constant, it makes testing easier since the output won't change. + * + * @return the data + */ + Date getCurrentDate(); + + + long getFileSize(String path, boolean virtual) throws IOException; + + + long getFileLastModified(String path, boolean virtual) throws IOException; + + + String getFileText(String path, boolean virtual) throws IOException; + + + void log(String message, Throwable throwable); +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSIFilter.java b/java/org/apache/catalina/ssi/SSIFilter.java new file mode 100644 index 0000000..25a247f --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIFilter.java @@ -0,0 +1,160 @@ +/* + * 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.catalina.ssi; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Filter to process SSI requests within a webpage. Mapped to a content types from within web.xml. + * + * @author David Becker + * + * @see org.apache.catalina.ssi.SSIServlet + */ +public class SSIFilter extends GenericFilter { + private static final long serialVersionUID = 1L; + /** Debug level for this servlet. */ + protected int debug = 0; + /** Expiration time in seconds for the doc. */ + protected Long expires = null; + /** virtual path can be webapp-relative */ + protected boolean isVirtualWebappRelative = false; + /** regex pattern to match when evaluating content types */ + protected Pattern contentTypeRegEx = null; + /** default pattern for ssi filter content type matching */ + protected final Pattern shtmlRegEx = Pattern.compile("text/x-server-parsed-html(;.*)?"); + /** Allow exec (normally blocked for security) */ + protected boolean allowExec = false; + + + @Override + public void init() throws ServletException { + if (getInitParameter("debug") != null) { + debug = Integer.parseInt(getInitParameter("debug")); + } + + if (getInitParameter("contentType") != null) { + contentTypeRegEx = Pattern.compile(getInitParameter("contentType")); + } else { + contentTypeRegEx = shtmlRegEx; + } + + isVirtualWebappRelative = Boolean.parseBoolean(getInitParameter("isVirtualWebappRelative")); + + if (getInitParameter("expires") != null) { + expires = Long.valueOf(getInitParameter("expires")); + } + + allowExec = Boolean.parseBoolean(getInitParameter("allowExec")); + + if (debug > 0) { + getServletContext().log("SSIFilter.init() SSI invoker started with 'debug'=" + debug); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + // cast once + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + // setup to capture output + ByteArrayServletOutputStream basos = new ByteArrayServletOutputStream(); + ResponseIncludeWrapper responseIncludeWrapper = new ResponseIncludeWrapper(res, basos); + + // process remainder of filter chain + chain.doFilter(req, responseIncludeWrapper); + + // we can't assume the chain flushed its output + responseIncludeWrapper.flushOutputStreamOrWriter(); + byte[] bytes = basos.toByteArray(); + + // get content type + String contentType = responseIncludeWrapper.getContentType(); + + // is this an allowed type for SSI processing? + if (contentType != null && contentTypeRegEx.matcher(contentType).matches()) { + String encoding = res.getCharacterEncoding(); + + // set up SSI processing + SSIExternalResolver ssiExternalResolver = new SSIServletExternalResolver(getServletContext(), req, res, + isVirtualWebappRelative, debug, encoding); + SSIProcessor ssiProcessor = new SSIProcessor(ssiExternalResolver, debug, allowExec); + + // prepare readers/writers + Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes), encoding); + ByteArrayOutputStream ssiout = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(ssiout, encoding)); + + // do SSI processing + long lastModified = ssiProcessor.process(reader, responseIncludeWrapper.getLastModified(), writer); + + // set output bytes + writer.flush(); + bytes = ssiout.toByteArray(); + + // override headers + if (expires != null) { + res.setDateHeader("expires", (new java.util.Date()).getTime() + expires.longValue() * 1000); + } + if (lastModified > 0) { + res.setDateHeader("last-modified", lastModified); + } + res.setContentLength(bytes.length); + + Matcher shtmlMatcher = shtmlRegEx.matcher(responseIncludeWrapper.getContentType()); + if (shtmlMatcher.matches()) { + // Convert SHTML mime type to ordinary HTML mime type but preserve + // encoding, if any. + String enc = shtmlMatcher.group(1); + res.setContentType("text/html" + ((enc != null) ? enc : "")); + } + } + + // write output + OutputStream out = null; + try { + out = res.getOutputStream(); + } catch (IllegalStateException e) { + // Ignore, will try to use a writer + } + if (out == null) { + res.getWriter().write(new String(bytes)); + } else { + out.write(bytes); + } + } +} diff --git a/java/org/apache/catalina/ssi/SSIFlastmod.java b/java/org/apache/catalina/ssi/SSIFlastmod.java new file mode 100644 index 0000000..ff14afd --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIFlastmod.java @@ -0,0 +1,74 @@ +/* + * 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.catalina.ssi; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; +import java.util.Locale; + +import org.apache.catalina.util.Strftime; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implements the Server-side #flastmod command + * + * @author Bip Thelin + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +public final class SSIFlastmod implements SSICommand { + private static final StringManager sm = StringManager.getManager(SSIFlastmod.class); + + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) { + long lastModified = 0; + String configErrMsg = ssiMediator.getConfigErrMsg(); + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + String paramValue = paramValues[i]; + String substitutedValue = ssiMediator.substituteVariables(paramValue); + try { + if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) { + boolean virtual = paramName.equalsIgnoreCase("virtual"); + lastModified = ssiMediator.getFileLastModified(substitutedValue, virtual); + Date date = new Date(lastModified); + String configTimeFmt = ssiMediator.getConfigTimeFmt(); + writer.write(formatDate(date, configTimeFmt)); + } else { + ssiMediator.log(sm.getString("ssiCommand.invalidAttribute", paramName)); + writer.write(configErrMsg); + } + } catch (IOException e) { + ssiMediator.log(sm.getString("ssiFlastmod.noLastModified", substitutedValue), e); + writer.write(configErrMsg); + } + } + return lastModified; + } + + + protected String formatDate(Date date, String configTimeFmt) { + Strftime strftime = new Strftime(configTimeFmt, Locale.US); + return strftime.format(date); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSIFsize.java b/java/org/apache/catalina/ssi/SSIFsize.java new file mode 100644 index 0000000..cc03b85 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIFsize.java @@ -0,0 +1,123 @@ +/* + * 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.catalina.ssi; + + +import java.io.IOException; +import java.io.PrintWriter; +import java.text.DecimalFormat; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Implements the Server-side #fsize command + * + * @author Bip Thelin + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +public final class SSIFsize implements SSICommand { + private static final StringManager sm = StringManager.getManager(SSIFsize.class); + static final int ONE_KIBIBYTE = 1024; + static final int ONE_MEBIBYTE = 1024 * 1024; + + + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) { + long lastModified = 0; + String configErrMsg = ssiMediator.getConfigErrMsg(); + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + String paramValue = paramValues[i]; + String substitutedValue = ssiMediator.substituteVariables(paramValue); + try { + if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) { + boolean virtual = paramName.equalsIgnoreCase("virtual"); + lastModified = ssiMediator.getFileLastModified(substitutedValue, virtual); + long size = ssiMediator.getFileSize(substitutedValue, virtual); + String configSizeFmt = ssiMediator.getConfigSizeFmt(); + writer.write(formatSize(size, configSizeFmt)); + } else { + ssiMediator.log(sm.getString("ssiCommand.invalidAttribute", paramName)); + writer.write(configErrMsg); + } + } catch (IOException e) { + ssiMediator.log(sm.getString("ssiFsize.noSize", substitutedValue), e); + writer.write(configErrMsg); + } + } + return lastModified; + } + + + public String repeat(char aChar, int numChars) { + if (numChars < 0) { + throw new IllegalArgumentException(sm.getString("ssiFsize.invalidNumChars")); + } + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < numChars; i++) { + buf.append(aChar); + } + return buf.toString(); + } + + + public String padLeft(String str, int maxChars) { + String result = str; + int charsToAdd = maxChars - str.length(); + if (charsToAdd > 0) { + result = repeat(' ', charsToAdd) + str; + } + return result; + } + + + // We try to mimic httpd here, as we do everywhere. + // All the 'magic' numbers are from the util_script.c httpd source file. + // Should use KiB and MiB in output but use k and M for consistency with httpd. + protected String formatSize(long size, String format) { + String retString = ""; + if (format.equalsIgnoreCase("bytes")) { + DecimalFormat decimalFormat = new DecimalFormat("#,##0"); + retString = decimalFormat.format(size); + } else { + if (size < 0) { + retString = "-"; + } else if (size == 0) { + retString = "0k"; + } else if (size < ONE_KIBIBYTE) { + retString = "1k"; + } else if (size < ONE_MEBIBYTE) { + retString = Long.toString((size + 512) / ONE_KIBIBYTE); + retString += "k"; + } else if (size < 99 * ONE_MEBIBYTE) { + DecimalFormat decimalFormat = new DecimalFormat("0.0M"); + retString = decimalFormat.format(size / (double) ONE_MEBIBYTE); + } else { + retString = Long.toString((size + (529 * ONE_KIBIBYTE)) / ONE_MEBIBYTE); + retString += "M"; + } + retString = padLeft(retString, 5); + } + return retString; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSIInclude.java b/java/org/apache/catalina/ssi/SSIInclude.java new file mode 100644 index 0000000..b1eb84b --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIInclude.java @@ -0,0 +1,65 @@ +/* + * 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.catalina.ssi; + + +import java.io.IOException; +import java.io.PrintWriter; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Implements the Server-side #include command + * + * @author Bip Thelin + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +public final class SSIInclude implements SSICommand { + private static final StringManager sm = StringManager.getManager(SSIInclude.class); + + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) { + long lastModified = 0; + String configErrMsg = ssiMediator.getConfigErrMsg(); + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + String paramValue = paramValues[i]; + String substitutedValue = ssiMediator.substituteVariables(paramValue); + try { + if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) { + boolean virtual = paramName.equalsIgnoreCase("virtual"); + lastModified = ssiMediator.getFileLastModified(substitutedValue, virtual); + String text = ssiMediator.getFileText(substitutedValue, virtual); + writer.write(text); + } else { + ssiMediator.log(sm.getString("ssiCommand.invalidAttribute", paramName)); + writer.write(configErrMsg); + } + } catch (IOException e) { + ssiMediator.log(sm.getString("ssiInclude.includeFailed", substitutedValue), e); + writer.write(configErrMsg); + } + } + return lastModified; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSIMediator.java b/java/org/apache/catalina/ssi/SSIMediator.java new file mode 100644 index 0000000..baad95c --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIMediator.java @@ -0,0 +1,350 @@ +/* + * 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.catalina.ssi; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.TimeZone; +import java.util.regex.Matcher; + +import org.apache.catalina.util.Strftime; +import org.apache.catalina.util.URLEncoder; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; + +/** + * Allows the different SSICommand implementations to share data/talk to each other + * + * @author Bip Thelin + * @author Amy Roh + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +public class SSIMediator { + private static final StringManager sm = StringManager.getManager(SSIMediator.class); + + protected static final String ENCODING_NONE = "none"; + protected static final String ENCODING_ENTITY = "entity"; + protected static final String ENCODING_URL = "url"; + + protected static final String DEFAULT_CONFIG_ERR_MSG = "[an error occurred while processing this directive]"; + protected static final String DEFAULT_CONFIG_TIME_FMT = "%A, %d-%b-%Y %T %Z"; + protected static final String DEFAULT_CONFIG_SIZE_FMT = "abbrev"; + + protected String configErrMsg = DEFAULT_CONFIG_ERR_MSG; + protected String configTimeFmt = DEFAULT_CONFIG_TIME_FMT; + protected String configSizeFmt = DEFAULT_CONFIG_SIZE_FMT; + protected final String className = getClass().getName(); + protected final SSIExternalResolver ssiExternalResolver; + protected final long lastModifiedDate; + protected Strftime strftime; + protected final SSIConditionalState conditionalState = new SSIConditionalState(); + protected int lastMatchCount = 0; + + + public SSIMediator(SSIExternalResolver ssiExternalResolver, long lastModifiedDate) { + this.ssiExternalResolver = ssiExternalResolver; + this.lastModifiedDate = lastModifiedDate; + setConfigTimeFmt(DEFAULT_CONFIG_TIME_FMT, true); + } + + + public void setConfigErrMsg(String configErrMsg) { + this.configErrMsg = configErrMsg; + } + + + public void setConfigTimeFmt(String configTimeFmt) { + setConfigTimeFmt(configTimeFmt, false); + } + + + public void setConfigTimeFmt(String configTimeFmt, boolean fromConstructor) { + this.configTimeFmt = configTimeFmt; + this.strftime = new Strftime(configTimeFmt, Locale.US); + /* + * Variables like DATE_LOCAL, DATE_GMT, and LAST_MODIFIED need to be updated when the timefmt changes. This is + * what Apache SSI does. + */ + setDateVariables(fromConstructor); + } + + + public void setConfigSizeFmt(String configSizeFmt) { + this.configSizeFmt = configSizeFmt; + } + + + public String getConfigErrMsg() { + return configErrMsg; + } + + + public String getConfigTimeFmt() { + return configTimeFmt; + } + + + public String getConfigSizeFmt() { + return configSizeFmt; + } + + + public SSIConditionalState getConditionalState() { + return conditionalState; + } + + + public Collection getVariableNames() { + Set variableNames = new HashSet<>(); + // These built-in variables are supplied by the mediator (if not over-written by the user) and always exist + variableNames.add("DATE_GMT"); + variableNames.add("DATE_LOCAL"); + variableNames.add("LAST_MODIFIED"); + ssiExternalResolver.addVariableNames(variableNames); + // Remove any variables that are reserved by this class + variableNames.removeIf(this::isNameReserved); + return variableNames; + } + + + public long getFileSize(String path, boolean virtual) throws IOException { + return ssiExternalResolver.getFileSize(path, virtual); + } + + + public long getFileLastModified(String path, boolean virtual) throws IOException { + return ssiExternalResolver.getFileLastModified(path, virtual); + } + + + public String getFileText(String path, boolean virtual) throws IOException { + return ssiExternalResolver.getFileText(path, virtual); + } + + + protected boolean isNameReserved(String name) { + return name.startsWith(className + "."); + } + + + public String getVariableValue(String variableName) { + return getVariableValue(variableName, ENCODING_NONE); + } + + + public void setVariableValue(String variableName, String variableValue) { + if (!isNameReserved(variableName)) { + ssiExternalResolver.setVariableValue(variableName, variableValue); + } + } + + + public String getVariableValue(String variableName, String encoding) { + String lowerCaseVariableName = variableName.toLowerCase(Locale.ENGLISH); + String variableValue = null; + if (!isNameReserved(lowerCaseVariableName)) { + // Try getting it externally first, if it fails, try getting the 'built-in' value + variableValue = ssiExternalResolver.getVariableValue(variableName); + if (variableValue == null) { + variableName = variableName.toUpperCase(Locale.ENGLISH); + variableValue = ssiExternalResolver.getVariableValue(className + "." + variableName); + } + if (variableValue != null) { + variableValue = encode(variableValue, encoding); + } + } + return variableValue; + } + + + /** + * Applies variable substitution to the specified String and returns the new resolved string. + * + * @param val The value which should be checked + * + * @return the value after variable substitution + */ + public String substituteVariables(String val) { + // If it has no references or HTML entities then no work need to be done + if (val.indexOf('$') < 0 && val.indexOf('&') < 0) { + return val; + } + + // HTML decoding + val = val.replace("<", "<"); + val = val.replace(">", ">"); + val = val.replace(""", "\""); + val = val.replace("&", "&"); + + StringBuilder sb = new StringBuilder(val); + int charStart = sb.indexOf("&#"); + while (charStart > -1) { + int charEnd = sb.indexOf(";", charStart); + if (charEnd > -1) { + char c = (char) Integer.parseInt(sb.substring(charStart + 2, charEnd)); + sb.delete(charStart, charEnd + 1); + sb.insert(charStart, c); + charStart = sb.indexOf("&#"); + } else { + break; + } + } + + for (int i = 0; i < sb.length();) { + // Find the next $ + for (; i < sb.length(); i++) { + if (sb.charAt(i) == '$') { + i++; + break; + } + } + if (i == sb.length()) { + break; + } + // Check to see if the $ is escaped + if (i > 1 && sb.charAt(i - 2) == '\\') { + sb.deleteCharAt(i - 2); + i--; + continue; + } + int nameStart = i; + int start = i - 1; + int end = -1; + int nameEnd = -1; + char endChar = ' '; + // Check for {} wrapped var + if (sb.charAt(i) == '{') { + nameStart++; + endChar = '}'; + } + // Find the end of the var reference + for (; i < sb.length(); i++) { + if (sb.charAt(i) == endChar) { + break; + } + } + end = i; + nameEnd = end; + if (endChar == '}') { + end++; + } + // We should now have enough to extract the var name + String varName = sb.substring(nameStart, nameEnd); + String value = getVariableValue(varName); + if (value == null) { + value = ""; + } + // Replace the var name with its value + sb.replace(start, end, value); + // Start searching for the next $ after the value that was just substituted. + i = start + value.length(); + } + return sb.toString(); + } + + + protected String formatDate(Date date, TimeZone timeZone) { + String retVal; + if (timeZone != null) { + // we temporarily change strftime. Since SSIMediator is inherently single-threaded, this isn't a problem + TimeZone oldTimeZone = strftime.getTimeZone(); + strftime.setTimeZone(timeZone); + retVal = strftime.format(date); + strftime.setTimeZone(oldTimeZone); + } else { + retVal = strftime.format(date); + } + return retVal; + } + + + protected String encode(String value, String encoding) { + String retVal = null; + if (encoding.equalsIgnoreCase(ENCODING_URL)) { + retVal = URLEncoder.DEFAULT.encode(value, StandardCharsets.UTF_8); + } else if (encoding.equalsIgnoreCase(ENCODING_NONE)) { + retVal = value; + } else if (encoding.equalsIgnoreCase(ENCODING_ENTITY)) { + retVal = Escape.htmlElementContent(value); + } else { + // This shouldn't be possible + throw new IllegalArgumentException(sm.getString("ssiMediator.unknownEncoding", encoding)); + } + return retVal; + } + + + public void log(String message) { + ssiExternalResolver.log(message, null); + } + + + public void log(String message, Throwable throwable) { + ssiExternalResolver.log(message, throwable); + } + + + protected void setDateVariables(boolean fromConstructor) { + boolean alreadySet = ssiExternalResolver.getVariableValue(className + ".alreadyset") != null; + // skip this if we are being called from the constructor, and this has already been set + if (!(fromConstructor && alreadySet)) { + ssiExternalResolver.setVariableValue(className + ".alreadyset", "true"); + Date date = new Date(); + TimeZone timeZone = TimeZone.getTimeZone("GMT"); + String retVal = formatDate(date, timeZone); + /* + * If we are setting on of the date variables, we want to remove them from the user defined list of + * variables, because this is what Apache does. + */ + setVariableValue("DATE_GMT", null); + ssiExternalResolver.setVariableValue(className + ".DATE_GMT", retVal); + retVal = formatDate(date, null); + setVariableValue("DATE_LOCAL", null); + ssiExternalResolver.setVariableValue(className + ".DATE_LOCAL", retVal); + retVal = formatDate(new Date(lastModifiedDate), null); + setVariableValue("LAST_MODIFIED", null); + ssiExternalResolver.setVariableValue(className + ".LAST_MODIFIED", retVal); + } + } + + + protected void clearMatchGroups() { + for (int i = 1; i <= lastMatchCount; i++) { + setVariableValue(Integer.toString(i), ""); + } + lastMatchCount = 0; + } + + + protected void populateMatchGroups(Matcher matcher) { + lastMatchCount = matcher.groupCount(); + // $0 is not used + if (lastMatchCount == 0) { + return; + } + for (int i = 1; i <= lastMatchCount; i++) { + setVariableValue(Integer.toString(i), matcher.group(i)); + } + } +} diff --git a/java/org/apache/catalina/ssi/SSIPrintenv.java b/java/org/apache/catalina/ssi/SSIPrintenv.java new file mode 100644 index 0000000..84d35f9 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIPrintenv.java @@ -0,0 +1,59 @@ +/* + * 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.catalina.ssi; + + +import java.io.PrintWriter; +import java.util.Collection; + +/** + * Implements the Server-side #printenv command + * + * @author Dan Sandberg + * @author David Becker + */ +public class SSIPrintenv implements SSICommand { + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) { + long lastModified = 0; + // any arguments should produce an error + if (paramNames.length > 0) { + String errorMessage = ssiMediator.getConfigErrMsg(); + writer.write(errorMessage); + } else { + Collection variableNames = ssiMediator.getVariableNames(); + for (String variableName : variableNames) { + String variableValue = ssiMediator.getVariableValue(variableName, SSIMediator.ENCODING_ENTITY); + // This shouldn't happen, since all the variable names must + // have values + if (variableValue == null) { + variableValue = "(none)"; + } + writer.write(variableName); + writer.write('='); + writer.write(variableValue); + writer.write('\n'); + lastModified = System.currentTimeMillis(); + } + } + return lastModified; + } +} diff --git a/java/org/apache/catalina/ssi/SSIProcessor.java b/java/org/apache/catalina/ssi/SSIProcessor.java new file mode 100644 index 0000000..f4e2dc9 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIProcessor.java @@ -0,0 +1,328 @@ +/* + * 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.catalina.ssi; + + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Locale; +import java.util.StringTokenizer; + +import org.apache.catalina.util.IOTools; + +/** + * The entry point to SSI processing. This class does the actual parsing, delegating to the SSIMediator, SSICommand, and + * SSIExternalResolver as necessary. + * + * @author Dan Sandberg + * @author David Becker + */ +public class SSIProcessor { + /** The start pattern */ + protected static final String COMMAND_START = ""; + protected final SSIExternalResolver ssiExternalResolver; + protected final HashMap commands = new HashMap<>(); + protected final int debug; + protected final boolean allowExec; + + + public SSIProcessor(SSIExternalResolver ssiExternalResolver, int debug, boolean allowExec) { + this.ssiExternalResolver = ssiExternalResolver; + this.debug = debug; + this.allowExec = allowExec; + addBuiltinCommands(); + } + + + protected void addBuiltinCommands() { + addCommand("config", new SSIConfig()); + addCommand("echo", new SSIEcho()); + if (allowExec) { + addCommand("exec", new SSIExec()); + } + addCommand("include", new SSIInclude()); + addCommand("flastmod", new SSIFlastmod()); + addCommand("fsize", new SSIFsize()); + addCommand("printenv", new SSIPrintenv()); + addCommand("set", new SSISet()); + SSIConditional ssiConditional = new SSIConditional(); + addCommand("if", ssiConditional); + addCommand("elif", ssiConditional); + addCommand("endif", ssiConditional); + addCommand("else", ssiConditional); + } + + + public void addCommand(String name, SSICommand command) { + commands.put(name, command); + } + + + /** + * Process a file with server-side commands, reading from reader and writing the processed version to writer. NOTE: + * We really should be doing this in a streaming way rather than converting it to an array first. + * + * @param reader the reader to read the file containing SSIs from + * @param lastModifiedDate resource last modification date + * @param writer the writer to write the file with the SSIs processed. + * + * @return the most current modified date resulting from any SSI commands + * + * @throws IOException when things go horribly awry. Should be unlikely since the SSICommand usually catches + * 'normal' IOExceptions. + */ + public long process(Reader reader, long lastModifiedDate, PrintWriter writer) throws IOException { + SSIMediator ssiMediator = new SSIMediator(ssiExternalResolver, lastModifiedDate); + StringWriter stringWriter = new StringWriter(); + IOTools.flow(reader, stringWriter); + String fileContents = stringWriter.toString(); + stringWriter = null; + int index = 0; + boolean inside = false; + StringBuilder command = new StringBuilder(); + try { + while (index < fileContents.length()) { + char c = fileContents.charAt(index); + if (!inside) { + if (c == COMMAND_START.charAt(0) && charCmp(fileContents, index, COMMAND_START)) { + inside = true; + index += COMMAND_START.length(); + command.setLength(0); // clear the command string + } else { + if (!ssiMediator.getConditionalState().processConditionalCommandsOnly) { + writer.write(c); + } + index++; + } + } else { + if (c == COMMAND_END.charAt(0) && charCmp(fileContents, index, COMMAND_END)) { + inside = false; + index += COMMAND_END.length(); + String strCmd = parseCmd(command); + if (debug > 0) { + ssiExternalResolver.log("SSIProcessor.process -- processing command: " + strCmd, null); + } + String[] paramNames = parseParamNames(command, strCmd.length()); + String[] paramValues = parseParamValues(command, strCmd.length(), paramNames.length); + // We need to fetch this value each time, since it may change during the loop + String configErrMsg = ssiMediator.getConfigErrMsg(); + SSICommand ssiCommand = commands.get(strCmd.toLowerCase(Locale.ENGLISH)); + String errorMessage = null; + if (ssiCommand == null) { + errorMessage = "Unknown command: " + strCmd; + } else if (paramValues == null) { + errorMessage = "Error parsing directive parameters."; + } else if (paramNames.length != paramValues.length) { + errorMessage = + "Parameter names count does not match parameter values count on command: " + strCmd; + } else { + /* + * Don't process the command if we are processing conditional commands only and the command + * is not conditional + */ + if (!ssiMediator.getConditionalState().processConditionalCommandsOnly || + ssiCommand instanceof SSIConditional) { + long lmd = ssiCommand.process(ssiMediator, strCmd, paramNames, paramValues, writer); + if (lmd > lastModifiedDate) { + lastModifiedDate = lmd; + } + } + } + if (errorMessage != null) { + ssiExternalResolver.log(errorMessage, null); + writer.write(configErrMsg); + } + } else { + command.append(c); + index++; + } + } + } + } catch (SSIStopProcessingException e) { + // If we are here, then we have already stopped processing, so all is good + } + return lastModifiedDate; + } + + + /** + * Parse a StringBuilder and take out the param type token. Called from requestHandler + * + * @param cmd a value of type 'StringBuilder' + * @param start index on which parsing will start + * + * @return an array with the parameter names + */ + protected String[] parseParamNames(StringBuilder cmd, int start) { + int bIdx = start; + int i = 0; + int quotes = 0; + boolean inside = false; + StringBuilder retBuf = new StringBuilder(); + while (bIdx < cmd.length()) { + if (!inside) { + while (bIdx < cmd.length() && isSpace(cmd.charAt(bIdx))) { + bIdx++; + } + if (bIdx >= cmd.length()) { + break; + } + inside = !inside; + } else { + while (bIdx < cmd.length() && cmd.charAt(bIdx) != '=') { + retBuf.append(cmd.charAt(bIdx)); + bIdx++; + } + retBuf.append('='); + inside = !inside; + quotes = 0; + boolean escaped = false; + for (; bIdx < cmd.length() && quotes != 2; bIdx++) { + char c = cmd.charAt(bIdx); + // Need to skip escaped characters + if (c == '\\' && !escaped) { + escaped = true; + continue; + } + if (c == '"' && !escaped) { + quotes++; + } + escaped = false; + } + } + } + StringTokenizer str = new StringTokenizer(retBuf.toString(), "="); + String[] retString = new String[str.countTokens()]; + while (str.hasMoreTokens()) { + retString[i++] = str.nextToken().trim(); + } + return retString; + } + + + /** + * Parse a StringBuilder and take out the param token. Called from requestHandler + * + * @param cmd a value of type 'StringBuilder' + * @param start index on which parsing will start + * @param count number of values which should be parsed + * + * @return an array with the parameter values + */ + protected String[] parseParamValues(StringBuilder cmd, int start, int count) { + int valIndex = 0; + boolean inside = false; + String[] vals = new String[count]; + StringBuilder sb = new StringBuilder(); + char endQuote = 0; + for (int bIdx = start; bIdx < cmd.length(); bIdx++) { + if (!inside) { + while (bIdx < cmd.length() && !isQuote(cmd.charAt(bIdx))) { + bIdx++; + } + if (bIdx >= cmd.length()) { + break; + } + inside = !inside; + endQuote = cmd.charAt(bIdx); + } else { + boolean escaped = false; + for (; bIdx < cmd.length(); bIdx++) { + char c = cmd.charAt(bIdx); + // Check for escapes + if (c == '\\' && !escaped) { + escaped = true; + continue; + } + // If we reach the other " then stop + if (c == endQuote && !escaped) { + break; + } + /* + * Since parsing of attributes and var substitution is done in separate places, we need to leave + * escape in the string + */ + if (c == '$' && escaped) { + sb.append('\\'); + } + escaped = false; + sb.append(c); + } + // If we hit the end without seeing a quote the signal an error + if (bIdx == cmd.length()) { + return null; + } + vals[valIndex++] = sb.toString(); + sb.delete(0, sb.length()); // clear the buffer + inside = !inside; + } + } + return vals; + } + + + /** + * Parse a StringBuilder and take out the command token. Called from requestHandler + * + * @param cmd a value of type 'StringBuilder' + * + * @return a value of type 'String', or null if there is none + */ + private String parseCmd(StringBuilder cmd) { + int firstLetter = -1; + int lastLetter = -1; + for (int i = 0; i < cmd.length(); i++) { + char c = cmd.charAt(i); + if (Character.isLetter(c)) { + if (firstLetter == -1) { + firstLetter = i; + } + lastLetter = i; + } else if (isSpace(c)) { + if (lastLetter > -1) { + break; + } + } else { + break; + } + } + if (firstLetter == -1) { + return ""; + } else { + return cmd.substring(firstLetter, lastLetter + 1); + } + } + + + protected boolean charCmp(String buf, int index, String command) { + return buf.regionMatches(index, command, 0, command.length()); + } + + + protected boolean isSpace(char c) { + return c == ' ' || c == '\n' || c == '\t' || c == '\r'; + } + + protected boolean isQuote(char c) { + return c == '\'' || c == '\"' || c == '`'; + } +} diff --git a/java/org/apache/catalina/ssi/SSIServlet.java b/java/org/apache/catalina/ssi/SSIServlet.java new file mode 100644 index 0000000..3c9c5e0 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIServlet.java @@ -0,0 +1,213 @@ +/* + * 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.catalina.ssi; + + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URL; +import java.net.URLConnection; +import java.util.Locale; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Servlet to process SSI requests within a webpage. Mapped to a path from within web.xml. + * + * @author Bip Thelin + * @author Amy Roh + * @author Dan Sandberg + * @author David Becker + */ +public class SSIServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + /** Debug level for this servlet. */ + protected int debug = 0; + /** Should the output be buffered. */ + protected boolean buffered = false; + /** Expiration time in seconds for the doc. */ + protected Long expires = null; + /** virtual path can be webapp-relative */ + protected boolean isVirtualWebappRelative = false; + /** Input encoding. If not specified, uses platform default */ + protected String inputEncoding = null; + /** Output encoding. If not specified, uses platform default */ + protected String outputEncoding = "UTF-8"; + /** Allow exec (normally blocked for security) */ + protected boolean allowExec = false; + + + // ----------------- Public methods. + /** + * Initialize this servlet. + * + * @exception ServletException if an error occurs + */ + @Override + public void init() throws ServletException { + + if (getServletConfig().getInitParameter("debug") != null) { + debug = Integer.parseInt(getServletConfig().getInitParameter("debug")); + } + + isVirtualWebappRelative = Boolean.parseBoolean(getServletConfig().getInitParameter("isVirtualWebappRelative")); + + if (getServletConfig().getInitParameter("expires") != null) { + expires = Long.valueOf(getServletConfig().getInitParameter("expires")); + } + + buffered = Boolean.parseBoolean(getServletConfig().getInitParameter("buffered")); + + inputEncoding = getServletConfig().getInitParameter("inputEncoding"); + + if (getServletConfig().getInitParameter("outputEncoding") != null) { + outputEncoding = getServletConfig().getInitParameter("outputEncoding"); + } + + allowExec = Boolean.parseBoolean(getServletConfig().getInitParameter("allowExec")); + + if (debug > 0) { + log("SSIServlet.init() SSI invoker started with 'debug'=" + debug); + } + + } + + + /** + * Process and forward the GET request to our requestHandler()* + * + * @param req a value of type 'HttpServletRequest' + * @param res a value of type 'HttpServletResponse' + * + * @exception IOException if an error occurs + * @exception ServletException if an error occurs + */ + @Override + public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { + if (debug > 0) { + log("SSIServlet.doGet()"); + } + requestHandler(req, res); + } + + + /** + * Process and forward the POST request to our requestHandler(). + * + * @param req a value of type 'HttpServletRequest' + * @param res a value of type 'HttpServletResponse' + * + * @exception IOException if an error occurs + * @exception ServletException if an error occurs + */ + @Override + public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { + if (debug > 0) { + log("SSIServlet.doPost()"); + } + requestHandler(req, res); + } + + + /** + * Process our request and locate right SSI command. + * + * @param req a value of type 'HttpServletRequest' + * @param res a value of type 'HttpServletResponse' + * + * @throws IOException an IO error occurred + */ + protected void requestHandler(HttpServletRequest req, HttpServletResponse res) throws IOException { + ServletContext servletContext = getServletContext(); + String path = SSIServletRequestUtil.getRelativePath(req); + if (debug > 0) { + log("SSIServlet.requestHandler()\n" + "Serving " + (buffered ? "buffered " : "unbuffered ") + "resource '" + + path + "'"); + } + // Exclude any resource in the /WEB-INF and /META-INF subdirectories + // (the "toUpperCase()" avoids problems on Windows systems) + if (path == null || path.toUpperCase(Locale.ENGLISH).startsWith("/WEB-INF") || + path.toUpperCase(Locale.ENGLISH).startsWith("/META-INF")) { + res.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + URL resource = servletContext.getResource(path); + if (resource == null) { + res.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + String resourceMimeType = servletContext.getMimeType(path); + if (resourceMimeType == null) { + resourceMimeType = "text/html"; + } + res.setContentType(resourceMimeType + ";charset=" + outputEncoding); + if (expires != null) { + res.setDateHeader("Expires", (new java.util.Date()).getTime() + expires.longValue() * 1000); + } + processSSI(req, res, resource); + } + + + protected void processSSI(HttpServletRequest req, HttpServletResponse res, URL resource) throws IOException { + SSIExternalResolver ssiExternalResolver = new SSIServletExternalResolver(getServletContext(), req, res, + isVirtualWebappRelative, debug, inputEncoding); + SSIProcessor ssiProcessor = new SSIProcessor(ssiExternalResolver, debug, allowExec); + PrintWriter printWriter = null; + StringWriter stringWriter = null; + if (buffered) { + stringWriter = new StringWriter(); + printWriter = new PrintWriter(stringWriter); + } else { + printWriter = res.getWriter(); + } + + URLConnection resourceInfo = resource.openConnection(); + InputStream resourceInputStream = resourceInfo.getInputStream(); + String encoding = resourceInfo.getContentEncoding(); + if (encoding == null) { + encoding = inputEncoding; + } + InputStreamReader isr; + if (encoding == null) { + isr = new InputStreamReader(resourceInputStream); + } else { + isr = new InputStreamReader(resourceInputStream, encoding); + } + + try (BufferedReader bufferedReader = new BufferedReader(isr)) { + long lastModified = ssiProcessor.process(bufferedReader, resourceInfo.getLastModified(), printWriter); + if (lastModified > 0) { + res.setDateHeader("last-modified", lastModified); + } + if (buffered) { + printWriter.flush(); + @SuppressWarnings("null") + String text = stringWriter.toString(); + res.getWriter().write(text); + } + } + } +} diff --git a/java/org/apache/catalina/ssi/SSIServletExternalResolver.java b/java/org/apache/catalina/ssi/SSIServletExternalResolver.java new file mode 100644 index 0000000..3d5b3a8 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIServletExternalResolver.java @@ -0,0 +1,544 @@ +/* + * 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.catalina.ssi; + + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Date; +import java.util.Enumeration; +import java.util.Locale; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.http.RequestUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + * An implementation of SSIExternalResolver that is used with servlets. + * + * @author Dan Sandberg + * @author David Becker + */ +public class SSIServletExternalResolver implements SSIExternalResolver { + private static final StringManager sm = StringManager.getManager(SSIServletExternalResolver.class); + protected final String VARIABLE_NAMES[] = { "AUTH_TYPE", "CONTENT_LENGTH", "CONTENT_TYPE", "DOCUMENT_NAME", + "DOCUMENT_URI", "GATEWAY_INTERFACE", "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE", + "HTTP_CONNECTION", "HTTP_HOST", "HTTP_REFERER", "HTTP_USER_AGENT", "PATH_INFO", "PATH_TRANSLATED", + "QUERY_STRING", "QUERY_STRING_UNESCAPED", "REMOTE_ADDR", "REMOTE_HOST", "REMOTE_PORT", "REMOTE_USER", + "REQUEST_METHOD", "REQUEST_URI", "SCRIPT_FILENAME", "SCRIPT_NAME", "SERVER_ADDR", "SERVER_NAME", + "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE", "UNIQUE_ID" }; + protected final ServletContext context; + protected final HttpServletRequest req; + protected final HttpServletResponse res; + protected final boolean isVirtualWebappRelative; + protected final int debug; + protected final String inputEncoding; + + public SSIServletExternalResolver(ServletContext context, HttpServletRequest req, HttpServletResponse res, + boolean isVirtualWebappRelative, int debug, String inputEncoding) { + this.context = context; + this.req = req; + this.res = res; + this.isVirtualWebappRelative = isVirtualWebappRelative; + this.debug = debug; + this.inputEncoding = inputEncoding; + } + + + @Override + public void log(String message, Throwable throwable) { + /* + * We can't assume that Servlet.log(message, null) is the same as Servlet.log( message ), since API doesn't seem + * to say so. + */ + if (throwable != null) { + context.log(message, throwable); + } else { + context.log(message); + } + } + + + @Override + public void addVariableNames(Collection variableNames) { + for (String variableName : VARIABLE_NAMES) { + String variableValue = getVariableValue(variableName); + if (variableValue != null) { + variableNames.add(variableName); + } + } + Enumeration e = req.getAttributeNames(); + while (e.hasMoreElements()) { + String name = e.nextElement(); + if (!isNameReserved(name)) { + variableNames.add(name); + } + } + } + + + protected Object getReqAttributeIgnoreCase(String targetName) { + Object object = null; + if (!isNameReserved(targetName)) { + object = req.getAttribute(targetName); + if (object == null) { + Enumeration e = req.getAttributeNames(); + while (e.hasMoreElements()) { + String name = e.nextElement(); + if (targetName.equalsIgnoreCase(name) && !isNameReserved(name)) { + object = req.getAttribute(name); + if (object != null) { + break; + } + } + } + } + } + return object; + } + + + protected boolean isNameReserved(String name) { + return name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun."); + } + + + @Override + public void setVariableValue(String name, String value) { + if (!isNameReserved(name)) { + req.setAttribute(name, value); + } + } + + + @Override + public String getVariableValue(String name) { + String retVal = null; + Object object = getReqAttributeIgnoreCase(name); + if (object != null) { + retVal = object.toString(); + } else { + retVal = getCGIVariable(name); + } + return retVal; + } + + + protected String getCGIVariable(String name) { + String retVal = null; + String[] nameParts = name.toUpperCase(Locale.ENGLISH).split("_"); + int requiredParts = 2; + if (nameParts.length == 1) { + if (nameParts[0].equals("PATH")) { + requiredParts = 1; + } + } else if (nameParts[0].equals("AUTH")) { + if (nameParts[1].equals("TYPE")) { + retVal = req.getAuthType(); + } + } else if (nameParts[0].equals("CONTENT")) { + if (nameParts[1].equals("LENGTH")) { + long contentLength = req.getContentLengthLong(); + if (contentLength >= 0) { + retVal = Long.toString(contentLength); + } + } else if (nameParts[1].equals("TYPE")) { + retVal = req.getContentType(); + } + } else if (nameParts[0].equals("DOCUMENT")) { + if (nameParts[1].equals("NAME")) { + String requestURI = req.getRequestURI(); + retVal = requestURI.substring(requestURI.lastIndexOf('/') + 1); + } else if (nameParts[1].equals("URI")) { + retVal = req.getRequestURI(); + } + } else if (name.equalsIgnoreCase("GATEWAY_INTERFACE")) { + retVal = "CGI/1.1"; + } else if (nameParts[0].equals("HTTP")) { + if (nameParts[1].equals("ACCEPT")) { + String accept = null; + if (nameParts.length == 2) { + accept = "Accept"; + } else if (nameParts[2].equals("ENCODING")) { + requiredParts = 3; + accept = "Accept-Encoding"; + } else if (nameParts[2].equals("LANGUAGE")) { + requiredParts = 3; + accept = "Accept-Language"; + } + if (accept != null) { + Enumeration acceptHeaders = req.getHeaders(accept); + if (acceptHeaders != null) { + if (acceptHeaders.hasMoreElements()) { + StringBuilder rv = new StringBuilder(acceptHeaders.nextElement()); + while (acceptHeaders.hasMoreElements()) { + rv.append(", "); + rv.append(acceptHeaders.nextElement()); + } + retVal = rv.toString(); + } + } + } + } else if (nameParts[1].equals("CONNECTION")) { + retVal = req.getHeader("Connection"); + } else if (nameParts[1].equals("HOST")) { + retVal = req.getHeader("Host"); + } else if (nameParts[1].equals("REFERER")) { + retVal = req.getHeader("Referer"); + } else if (nameParts[1].equals("USER")) { + if (nameParts.length == 3) { + if (nameParts[2].equals("AGENT")) { + requiredParts = 3; + retVal = req.getHeader("User-Agent"); + } + } + } + + } else if (nameParts[0].equals("PATH")) { + if (nameParts[1].equals("INFO")) { + retVal = req.getPathInfo(); + } else if (nameParts[1].equals("TRANSLATED")) { + retVal = req.getPathTranslated(); + } + } else if (nameParts[0].equals("QUERY")) { + if (nameParts[1].equals("STRING")) { + String queryString = req.getQueryString(); + if (nameParts.length == 2) { + // apache displays this as an empty string rather than (none) + retVal = nullToEmptyString(queryString); + } else if (nameParts[2].equals("UNESCAPED")) { + requiredParts = 3; + if (queryString != null) { + Charset uriCharset = null; + Charset requestCharset = null; + boolean useBodyEncodingForURI = false; + + // Get encoding settings from request / connector if possible + if (req instanceof Request) { + try { + requestCharset = ((Request) req).getCoyoteRequest().getCharset(); + } catch (UnsupportedEncodingException e) { + // Ignore + } + Connector connector = ((Request) req).getConnector(); + uriCharset = connector.getURICharset(); + useBodyEncodingForURI = connector.getUseBodyEncodingForURI(); + } + + Charset queryStringCharset; + + // If valid, apply settings from request / connector + if (useBodyEncodingForURI && requestCharset != null) { + queryStringCharset = requestCharset; + } else if (uriCharset != null) { + queryStringCharset = uriCharset; + } else { + // Use default as a last resort + queryStringCharset = StandardCharsets.UTF_8; + } + + retVal = UDecoder.URLDecode(queryString, queryStringCharset); + } + } + } + } else if (nameParts[0].equals("REMOTE")) { + if (nameParts[1].equals("ADDR")) { + retVal = req.getRemoteAddr(); + } else if (nameParts[1].equals("HOST")) { + retVal = req.getRemoteHost(); + } else if (nameParts[1].equals("IDENT")) { + // Not implemented + } else if (nameParts[1].equals("PORT")) { + retVal = Integer.toString(req.getRemotePort()); + } else if (nameParts[1].equals("USER")) { + retVal = req.getRemoteUser(); + } + } else if (nameParts[0].equals("REQUEST")) { + if (nameParts[1].equals("METHOD")) { + retVal = req.getMethod(); + } else if (nameParts[1].equals("URI")) { + // If this is an error page, get the original URI + retVal = (String) req.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI); + if (retVal == null) { + retVal = req.getRequestURI(); + } + } + } else if (nameParts[0].equals("SCRIPT")) { + String scriptName = req.getServletPath(); + if (nameParts[1].equals("FILENAME")) { + retVal = context.getRealPath(scriptName); + } else if (nameParts[1].equals("NAME")) { + retVal = scriptName; + } + } else if (nameParts[0].equals("SERVER")) { + if (nameParts[1].equals("ADDR")) { + retVal = req.getLocalAddr(); + } + if (nameParts[1].equals("NAME")) { + retVal = req.getServerName(); + } else if (nameParts[1].equals("PORT")) { + retVal = Integer.toString(req.getServerPort()); + } else if (nameParts[1].equals("PROTOCOL")) { + retVal = req.getProtocol(); + } else if (nameParts[1].equals("SOFTWARE")) { + StringBuilder rv = new StringBuilder(context.getServerInfo()); + rv.append(' '); + rv.append(System.getProperty("java.vm.name")); + rv.append('/'); + rv.append(System.getProperty("java.vm.version")); + rv.append(' '); + rv.append(System.getProperty("os.name")); + retVal = rv.toString(); + } + } else if (name.equalsIgnoreCase("UNIQUE_ID")) { + retVal = req.getRequestedSessionId(); + } + if (requiredParts != nameParts.length) { + return null; + } + return retVal; + } + + @Override + public Date getCurrentDate() { + return new Date(); + } + + + protected String nullToEmptyString(String string) { + String retVal = string; + if (retVal == null) { + retVal = ""; + } + return retVal; + } + + + protected String getPathWithoutFileName(String servletPath) { + String retVal = null; + int lastSlash = servletPath.lastIndexOf('/'); + if (lastSlash >= 0) { + // cut off file name + retVal = servletPath.substring(0, lastSlash + 1); + } + return retVal; + } + + + protected String getPathWithoutContext(final String contextPath, final String servletPath) { + if (servletPath.startsWith(contextPath)) { + return servletPath.substring(contextPath.length()); + } + return servletPath; + } + + + protected String getAbsolutePath(String path) throws IOException { + String pathWithoutContext = SSIServletRequestUtil.getRelativePath(req); + String prefix = getPathWithoutFileName(pathWithoutContext); + if (prefix == null) { + throw new IOException(sm.getString("ssiServletExternalResolver.removeFilenameError", pathWithoutContext)); + } + String fullPath = prefix + path; + String retVal = RequestUtil.normalize(fullPath); + if (retVal == null) { + throw new IOException(sm.getString("ssiServletExternalResolver.normalizationError", fullPath)); + } + return retVal; + } + + + protected ServletContextAndPath getServletContextAndPathFromNonVirtualPath(String nonVirtualPath) + throws IOException { + if (nonVirtualPath.startsWith("/") || nonVirtualPath.startsWith("\\")) { + throw new IOException(sm.getString("ssiServletExternalResolver.absoluteNonVirtualPath", nonVirtualPath)); + } + if (nonVirtualPath.contains("../")) { + throw new IOException( + sm.getString("ssiServletExternalResolver.pathTraversalNonVirtualPath", nonVirtualPath)); + } + String path = getAbsolutePath(nonVirtualPath); + ServletContextAndPath csAndP = new ServletContextAndPath(context, path); + return csAndP; + } + + + protected ServletContextAndPath getServletContextAndPathFromVirtualPath(String virtualPath) throws IOException { + + if (!virtualPath.startsWith("/") && !virtualPath.startsWith("\\")) { + return new ServletContextAndPath(context, getAbsolutePath(virtualPath)); + } + + String normalized = RequestUtil.normalize(virtualPath); + if (isVirtualWebappRelative) { + return new ServletContextAndPath(context, normalized); + } + + ServletContext normContext = context.getContext(normalized); + if (normContext == null) { + throw new IOException(sm.getString("ssiServletExternalResolver.noContext", normalized)); + } + // If it's the root context, then there is no context element to remove. + // ie: '/file1.shtml' vs '/appName1/file1.shtml' + if (!isRootContext(normContext)) { + String noContext = getPathWithoutContext(normContext.getContextPath(), normalized); + return new ServletContextAndPath(normContext, noContext); + } + + return new ServletContextAndPath(normContext, normalized); + } + + + // Assumes servletContext is not-null + // Assumes that identity comparison will be true for the same context + // Assuming the above, getContext("/") will be non-null as long as the root context is accessible. + // If it isn't, then servletContext can't be the root context anyway, hence they will not match. + protected boolean isRootContext(ServletContext servletContext) { + return servletContext == servletContext.getContext("/"); + } + + + protected ServletContextAndPath getServletContextAndPath(String originalPath, boolean virtual) throws IOException { + ServletContextAndPath csAndP = null; + if (debug > 0) { + log("SSIServletExternalResolver.getServletContextAndPath( " + originalPath + ", " + virtual + ")", null); + } + if (virtual) { + csAndP = getServletContextAndPathFromVirtualPath(originalPath); + } else { + csAndP = getServletContextAndPathFromNonVirtualPath(originalPath); + } + return csAndP; + } + + + protected URLConnection getURLConnection(String originalPath, boolean virtual) throws IOException { + ServletContextAndPath csAndP = getServletContextAndPath(originalPath, virtual); + ServletContext context = csAndP.getServletContext(); + String path = csAndP.getPath(); + URL url = context.getResource(path); + if (url == null) { + throw new IOException(sm.getString("ssiServletExternalResolver.noResource", path)); + } + URLConnection urlConnection = url.openConnection(); + return urlConnection; + } + + + @Override + public long getFileLastModified(String path, boolean virtual) throws IOException { + long lastModified = 0; + try { + URLConnection urlConnection = getURLConnection(path, virtual); + lastModified = urlConnection.getLastModified(); + } catch (IOException e) { + // Ignore this. It will always fail for non-file based includes + } + return lastModified; + } + + + @Override + public long getFileSize(String path, boolean virtual) throws IOException { + long fileSize = -1; + try { + URLConnection urlConnection = getURLConnection(path, virtual); + fileSize = urlConnection.getContentLengthLong(); + } catch (IOException e) { + // Ignore this. It will always fail for non-file based includes + } + return fileSize; + } + + + /* + * We are making lots of unnecessary copies of the included data here. If someone ever complains that this is slow, + * we should connect the included stream to the print writer that SSICommand uses. + */ + @Override + public String getFileText(String originalPath, boolean virtual) throws IOException { + try { + ServletContextAndPath csAndP = getServletContextAndPath(originalPath, virtual); + ServletContext context = csAndP.getServletContext(); + String path = csAndP.getPath(); + RequestDispatcher rd = context.getRequestDispatcher(path); + if (rd == null) { + throw new IOException(sm.getString("ssiServletExternalResolver.requestDispatcherError", path)); + } + ByteArrayServletOutputStream basos = new ByteArrayServletOutputStream(); + ResponseIncludeWrapper responseIncludeWrapper = new ResponseIncludeWrapper(res, basos); + rd.include(req, responseIncludeWrapper); + // We can't assume the included servlet flushed its output + responseIncludeWrapper.flushOutputStreamOrWriter(); + byte[] bytes = basos.toByteArray(); + + // Assume platform default encoding unless otherwise specified + String retVal; + if (inputEncoding == null) { + retVal = new String(bytes); + } else { + retVal = new String(bytes, B2CConverter.getCharset(inputEncoding)); + } + + /* + * Make an assumption that an empty response is a failure. This is a problem if a truly empty file were + * included, but not sure how else to tell. + */ + if (retVal.equals("") && !req.getMethod().equalsIgnoreCase("HEAD")) { + throw new IOException(sm.getString("ssiServletExternalResolver.noFile", path)); + } + return retVal; + } catch (ServletException e) { + throw new IOException(sm.getString("ssiServletExternalResolver.noIncludeFile", originalPath), e); + } + } + + protected static class ServletContextAndPath { + protected final ServletContext servletContext; + protected final String path; + + + public ServletContextAndPath(ServletContext servletContext, String path) { + this.servletContext = servletContext; + this.path = path; + } + + + public ServletContext getServletContext() { + return servletContext; + } + + + public String getPath() { + return path; + } + } +} diff --git a/java/org/apache/catalina/ssi/SSIServletRequestUtil.java b/java/org/apache/catalina/ssi/SSIServletRequestUtil.java new file mode 100644 index 0000000..f664c41 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIServletRequestUtil.java @@ -0,0 +1,56 @@ +/* + * 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.catalina.ssi; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.tomcat.util.http.RequestUtil; + +public class SSIServletRequestUtil { + /** + * Return the relative path associated with this servlet. Taken from DefaultServlet.java. Perhaps this should be put + * in org.apache.catalina.util somewhere? Seems like it would be widely used. + * + * @param request The servlet request we are processing + * + * @return the relative path + */ + public static String getRelativePath(HttpServletRequest request) { + // Are we being processed by a RequestDispatcher.include()? + if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) { + String result = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); + if (result == null) { + result = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + } + if ((result == null) || (result.equals(""))) { + result = "/"; + } + return result; + } + // No, extract the desired path directly from the request + String result = request.getPathInfo(); + if (result == null) { + result = request.getServletPath(); + } + if ((result == null) || (result.equals(""))) { + result = "/"; + } + return RequestUtil.normalize(result); + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSISet.java b/java/org/apache/catalina/ssi/SSISet.java new file mode 100644 index 0000000..8817e66 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSISet.java @@ -0,0 +1,66 @@ +/* + * 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.catalina.ssi; + + +import java.io.PrintWriter; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Implements the Server-side #set command + * + * @author Paul Speed + * @author Dan Sandberg + * @author David Becker + */ +public class SSISet implements SSICommand { + private static final StringManager sm = StringManager.getManager(SSISet.class); + + /** + * @see SSICommand + */ + @Override + public long process(SSIMediator ssiMediator, String commandName, String[] paramNames, String[] paramValues, + PrintWriter writer) throws SSIStopProcessingException { + long lastModified = 0; + String errorMessage = ssiMediator.getConfigErrMsg(); + String variableName = null; + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + String paramValue = paramValues[i]; + if (paramName.equalsIgnoreCase("var")) { + variableName = paramValue; + } else if (paramName.equalsIgnoreCase("value")) { + if (variableName != null) { + String substitutedValue = ssiMediator.substituteVariables(paramValue); + ssiMediator.setVariableValue(variableName, substitutedValue); + lastModified = System.currentTimeMillis(); + } else { + ssiMediator.log(sm.getString("ssiSet.noVariable")); + writer.write(errorMessage); + throw new SSIStopProcessingException(); + } + } else { + ssiMediator.log(sm.getString("ssiCommand.invalidAttribute", paramName)); + writer.write(errorMessage); + throw new SSIStopProcessingException(); + } + } + return lastModified; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/SSIStopProcessingException.java b/java/org/apache/catalina/ssi/SSIStopProcessingException.java new file mode 100644 index 0000000..be8c700 --- /dev/null +++ b/java/org/apache/catalina/ssi/SSIStopProcessingException.java @@ -0,0 +1,39 @@ +/* + * 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.catalina.ssi; + + +/** + * Exception used to tell SSIProcessor that it should stop processing SSI commands. This is used to mimic the Apache + * behavior in #set with invalid attributes. + * + * @author Paul Speed + * @author Dan Sandberg + */ +public class SSIStopProcessingException extends Exception { + + public SSIStopProcessingException() { + super(); + } + + public SSIStopProcessingException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = 1L; + // No specific functionality for this class +} \ No newline at end of file diff --git a/java/org/apache/catalina/ssi/package.html b/java/org/apache/catalina/ssi/package.html new file mode 100644 index 0000000..61ed14a --- /dev/null +++ b/java/org/apache/catalina/ssi/package.html @@ -0,0 +1,32 @@ + + +

    This package contains code that is used by the SsiInvoker.

    +

    This class consists of SsiMediator.java which works as a +mediator between the different SsiCommands. To add a command you have to +implement the SsiCommand interface and extend the +SsiMediator. Commands currently implemented are

    + +
      +
    • SsiConfig - Implementation of the NCSA command Config i.e. <!--#config errmsg="error?"-->
    • +
    • SsiEcho - Implementation of the NCSA command Echo i.e. <!--#echo var="SERVER_NAME"-->
    • +
    • SsiExec - Not implemented
    • +
    • SsiFlastMod - Implementation of the NCSA command flastmod i.e. <!--#flastmod virtual="file"-->
    • +
    • SsiFsize - Implementation of the NCSA command fsize i.e. <!--#fsize file="file"-->
    • +
    • SsiInclude - Implementation of the NCSA command Include i.e. <!--#config virtual="includefile"-->
    • +
    + diff --git a/java/org/apache/catalina/startup/AddPortOffsetRule.java b/java/org/apache/catalina/startup/AddPortOffsetRule.java new file mode 100644 index 0000000..50f3fc0 --- /dev/null +++ b/java/org/apache/catalina/startup/AddPortOffsetRule.java @@ -0,0 +1,43 @@ +/* + * 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.catalina.startup; + +import org.apache.catalina.Server; +import org.apache.catalina.connector.Connector; +import org.apache.tomcat.util.digester.Rule; +import org.xml.sax.Attributes; + +public class AddPortOffsetRule extends Rule { + + // Set portOffset on all the connectors based on portOffset in the Server + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + + Connector conn = (Connector) digester.peek(); + Server server = (Server) digester.peek(2); + + int portOffset = server.getPortOffset(); + conn.setPortOffset(portOffset); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(conn)).append(".setPortOffset("); + code.append(digester.toVariableName(server)).append(".getPortOffset());"); + code.append(System.lineSeparator()); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/startup/Authenticators.properties b/java/org/apache/catalina/startup/Authenticators.properties new file mode 100644 index 0000000..70b5239 --- /dev/null +++ b/java/org/apache/catalina/startup/Authenticators.properties @@ -0,0 +1,21 @@ +# 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. + +BASIC=org.apache.catalina.authenticator.BasicAuthenticator +CLIENT-CERT=org.apache.catalina.authenticator.SSLAuthenticator +DIGEST=org.apache.catalina.authenticator.DigestAuthenticator +FORM=org.apache.catalina.authenticator.FormAuthenticator +NONE=org.apache.catalina.authenticator.NonLoginAuthenticator +SPNEGO=org.apache.catalina.authenticator.SpnegoAuthenticator diff --git a/java/org/apache/catalina/startup/Bootstrap.java b/java/org/apache/catalina/startup/Bootstrap.java new file mode 100644 index 0000000..0410db4 --- /dev/null +++ b/java/org/apache/catalina/startup/Bootstrap.java @@ -0,0 +1,605 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.catalina.security.SecurityClassLoad; +import org.apache.catalina.startup.ClassLoaderFactory.Repository; +import org.apache.catalina.startup.ClassLoaderFactory.RepositoryType; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Bootstrap loader for Catalina. This application constructs a class loader for use in loading the Catalina internal + * classes (by accumulating all of the JAR files found in the "server" directory under "catalina.home"), and starts the + * regular execution of the container. The purpose of this roundabout approach is to keep the Catalina internal classes + * (and any other classes they depend on, such as an XML parser) out of the system class path and therefore not visible + * to application level classes. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public final class Bootstrap { + + private static final Log log = LogFactory.getLog(Bootstrap.class); + + /** + * Daemon object used by main. + */ + private static final Object daemonLock = new Object(); + private static volatile Bootstrap daemon = null; + + private static final File catalinaBaseFile; + private static final File catalinaHomeFile; + + private static final Pattern PATH_PATTERN = Pattern.compile("(\"[^\"]*\")|(([^,])*)"); + + static { + // Will always be non-null + String userDir = System.getProperty("user.dir"); + + // Home first + String home = System.getProperty(Constants.CATALINA_HOME_PROP); + File homeFile = null; + + if (home != null) { + File f = new File(home); + try { + homeFile = f.getCanonicalFile(); + } catch (IOException ioe) { + homeFile = f.getAbsoluteFile(); + } + } + + if (homeFile == null) { + // First fall-back. See if current directory is a bin directory + // in a normal Tomcat install + File bootstrapJar = new File(userDir, "bootstrap.jar"); + + if (bootstrapJar.exists()) { + File f = new File(userDir, ".."); + try { + homeFile = f.getCanonicalFile(); + } catch (IOException ioe) { + homeFile = f.getAbsoluteFile(); + } + } + } + + if (homeFile == null) { + // Second fall-back. Use current directory + File f = new File(userDir); + try { + homeFile = f.getCanonicalFile(); + } catch (IOException ioe) { + homeFile = f.getAbsoluteFile(); + } + } + + catalinaHomeFile = homeFile; + System.setProperty(Constants.CATALINA_HOME_PROP, catalinaHomeFile.getPath()); + + // Then base + String base = System.getProperty(Constants.CATALINA_BASE_PROP); + if (base == null) { + catalinaBaseFile = catalinaHomeFile; + } else { + File baseFile = new File(base); + try { + baseFile = baseFile.getCanonicalFile(); + } catch (IOException ioe) { + baseFile = baseFile.getAbsoluteFile(); + } + catalinaBaseFile = baseFile; + } + System.setProperty(Constants.CATALINA_BASE_PROP, catalinaBaseFile.getPath()); + } + + // -------------------------------------------------------------- Variables + + + /** + * Daemon reference. + */ + private Object catalinaDaemon = null; + + ClassLoader commonLoader = null; + ClassLoader catalinaLoader = null; + ClassLoader sharedLoader = null; + + + // -------------------------------------------------------- Private Methods + + + private void initClassLoaders() { + try { + commonLoader = createClassLoader("common", null); + if (commonLoader == null) { + // no config file, default to this loader - we might be in a 'single' env. + commonLoader = this.getClass().getClassLoader(); + } + catalinaLoader = createClassLoader("server", commonLoader); + sharedLoader = createClassLoader("shared", commonLoader); + } catch (Throwable t) { + handleThrowable(t); + log.error("Class loader creation threw exception", t); + System.exit(1); + } + } + + + private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { + + String value = CatalinaProperties.getProperty(name + ".loader"); + if ((value == null) || (value.equals(""))) { + return parent; + } + + value = replace(value); + + List repositories = new ArrayList<>(); + + String[] repositoryPaths = getPaths(value); + + for (String repository : repositoryPaths) { + // Check for a JAR URL repository + try { + URI uri = new URI(repository); + @SuppressWarnings("unused") + URL url = uri.toURL(); + repositories.add(new Repository(repository, RepositoryType.URL)); + continue; + } catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) { + // Ignore + } + + // Local repository + if (repository.endsWith("*.jar")) { + repository = repository.substring(0, repository.length() - "*.jar".length()); + repositories.add(new Repository(repository, RepositoryType.GLOB)); + } else if (repository.endsWith(".jar")) { + repositories.add(new Repository(repository, RepositoryType.JAR)); + } else { + repositories.add(new Repository(repository, RepositoryType.DIR)); + } + } + + return ClassLoaderFactory.createClassLoader(repositories, parent); + } + + + /** + * System property replacement in the given string. + * + * @param str The original string + * + * @return the modified string + */ + protected String replace(String str) { + // Implementation is copied from ClassLoaderLogManager.replace(), + // but added special processing for catalina.home and catalina.base. + String result = str; + int pos_start = str.indexOf("${"); + if (pos_start >= 0) { + StringBuilder builder = new StringBuilder(); + int pos_end = -1; + while (pos_start >= 0) { + builder.append(str, pos_end + 1, pos_start); + pos_end = str.indexOf('}', pos_start + 2); + if (pos_end < 0) { + pos_end = pos_start - 1; + break; + } + String propName = str.substring(pos_start + 2, pos_end); + String replacement; + if (propName.length() == 0) { + replacement = null; + } else if (Constants.CATALINA_HOME_PROP.equals(propName)) { + replacement = getCatalinaHome(); + } else if (Constants.CATALINA_BASE_PROP.equals(propName)) { + replacement = getCatalinaBase(); + } else { + replacement = System.getProperty(propName); + } + if (replacement != null) { + builder.append(replacement); + } else { + builder.append(str, pos_start, pos_end + 1); + } + pos_start = str.indexOf("${", pos_end + 1); + } + builder.append(str, pos_end + 1, str.length()); + result = builder.toString(); + } + return result; + } + + + /** + * Initialize daemon. + * + * @throws Exception Fatal initialization error + */ + public void init() throws Exception { + + initClassLoaders(); + + Thread.currentThread().setContextClassLoader(catalinaLoader); + + SecurityClassLoad.securityClassLoad(catalinaLoader); + + // Load our startup class and call its process() method + if (log.isTraceEnabled()) { + log.trace("Loading startup class"); + } + Class startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); + Object startupInstance = startupClass.getConstructor().newInstance(); + + // Set the shared extensions class loader + if (log.isTraceEnabled()) { + log.trace("Setting startup class properties"); + } + String methodName = "setParentClassLoader"; + Class paramTypes[] = new Class[1]; + paramTypes[0] = Class.forName("java.lang.ClassLoader"); + Object paramValues[] = new Object[1]; + paramValues[0] = sharedLoader; + Method method = startupInstance.getClass().getMethod(methodName, paramTypes); + method.invoke(startupInstance, paramValues); + + catalinaDaemon = startupInstance; + } + + + /** + * Load daemon. + */ + private void load(String[] arguments) throws Exception { + + // Call the load() method + String methodName = "load"; + Object param[]; + Class paramTypes[]; + if (arguments == null || arguments.length == 0) { + paramTypes = null; + param = null; + } else { + paramTypes = new Class[1]; + paramTypes[0] = arguments.getClass(); + param = new Object[1]; + param[0] = arguments; + } + Method method = catalinaDaemon.getClass().getMethod(methodName, paramTypes); + if (log.isTraceEnabled()) { + log.trace("Calling startup class " + method); + } + method.invoke(catalinaDaemon, param); + } + + + /** + * getServer() for configtest + */ + private Object getServer() throws Exception { + + String methodName = "getServer"; + Method method = catalinaDaemon.getClass().getMethod(methodName); + return method.invoke(catalinaDaemon); + } + + + // ----------------------------------------------------------- Main Program + + + /** + * Load the Catalina daemon. + * + * @param arguments Initialization arguments + * + * @throws Exception Fatal initialization error + */ + public void init(String[] arguments) throws Exception { + + init(); + load(arguments); + } + + + /** + * Start the Catalina daemon. + * + * @throws Exception Fatal start error + */ + public void start() throws Exception { + if (catalinaDaemon == null) { + init(); + } + + Method method = catalinaDaemon.getClass().getMethod("start", (Class[]) null); + method.invoke(catalinaDaemon, (Object[]) null); + } + + + /** + * Stop the Catalina Daemon. + * + * @throws Exception Fatal stop error + */ + public void stop() throws Exception { + Method method = catalinaDaemon.getClass().getMethod("stop", (Class[]) null); + method.invoke(catalinaDaemon, (Object[]) null); + } + + + /** + * Stop the standalone server. + * + * @throws Exception Fatal stop error + */ + public void stopServer() throws Exception { + + Method method = catalinaDaemon.getClass().getMethod("stopServer", (Class[]) null); + method.invoke(catalinaDaemon, (Object[]) null); + } + + + /** + * Stop the standalone server. + * + * @param arguments Command line arguments + * + * @throws Exception Fatal stop error + */ + public void stopServer(String[] arguments) throws Exception { + + Object param[]; + Class paramTypes[]; + if (arguments == null || arguments.length == 0) { + paramTypes = null; + param = null; + } else { + paramTypes = new Class[1]; + paramTypes[0] = arguments.getClass(); + param = new Object[1]; + param[0] = arguments; + } + Method method = catalinaDaemon.getClass().getMethod("stopServer", paramTypes); + method.invoke(catalinaDaemon, param); + } + + + /** + * Set flag. + * + * @param await true if the daemon should block + * + * @throws Exception Reflection error + */ + public void setAwait(boolean await) throws Exception { + + Class paramTypes[] = new Class[1]; + paramTypes[0] = Boolean.TYPE; + Object paramValues[] = new Object[1]; + paramValues[0] = Boolean.valueOf(await); + Method method = catalinaDaemon.getClass().getMethod("setAwait", paramTypes); + method.invoke(catalinaDaemon, paramValues); + } + + public boolean getAwait() throws Exception { + Class paramTypes[] = new Class[0]; + Object paramValues[] = new Object[0]; + Method method = catalinaDaemon.getClass().getMethod("getAwait", paramTypes); + Boolean b = (Boolean) method.invoke(catalinaDaemon, paramValues); + return b.booleanValue(); + } + + + /** + * Destroy the Catalina Daemon. + */ + public void destroy() { + } + + + /** + * Main method and entry point when starting Tomcat via the provided scripts. + * + * @param args Command line arguments to be processed + */ + public static void main(String args[]) { + + synchronized (daemonLock) { + if (daemon == null) { + // Don't set daemon until init() has completed + Bootstrap bootstrap = new Bootstrap(); + try { + bootstrap.init(); + } catch (Throwable t) { + handleThrowable(t); + log.error("Init exception", t); + return; + } + daemon = bootstrap; + } else { + // When running as a service the call to stop will be on a new + // thread so make sure the correct class loader is used to + // prevent a range of class not found exceptions. + Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); + } + } + + try { + String command = "start"; + if (args.length > 0) { + command = args[args.length - 1]; + } + + if (command.equals("startd")) { + args[args.length - 1] = "start"; + daemon.load(args); + daemon.start(); + } else if (command.equals("stopd")) { + args[args.length - 1] = "stop"; + daemon.stop(); + } else if (command.equals("start")) { + daemon.setAwait(true); + daemon.load(args); + daemon.start(); + if (null == daemon.getServer()) { + System.exit(1); + } + } else if (command.equals("stop")) { + daemon.stopServer(args); + } else if (command.equals("configtest")) { + daemon.load(args); + if (null == daemon.getServer()) { + System.exit(1); + } + System.exit(0); + } else { + log.warn("Bootstrap: command \"" + command + "\" does not exist."); + } + } catch (Throwable t) { + // Unwrap the Exception for clearer error reporting + if (t instanceof InvocationTargetException && t.getCause() != null) { + t = t.getCause(); + } + handleThrowable(t); + log.error("Error running command", t); + System.exit(1); + } + } + + + /** + * Obtain the name of configured home (binary) directory. Note that home and base may be the same (and are by + * default). + * + * @return the catalina home + */ + public static String getCatalinaHome() { + return catalinaHomeFile.getPath(); + } + + + /** + * Obtain the name of the configured base (instance) directory. Note that home and base may be the same (and are by + * default). If this is not set the value returned by {@link #getCatalinaHome()} will be used. + * + * @return the catalina base + */ + public static String getCatalinaBase() { + return catalinaBaseFile.getPath(); + } + + + /** + * Obtain the configured home (binary) directory. Note that home and base may be the same (and are by default). + * + * @return the catalina home as a file + */ + public static File getCatalinaHomeFile() { + return catalinaHomeFile; + } + + + /** + * Obtain the configured base (instance) directory. Note that home and base may be the same (and are by default). If + * this is not set the value returned by {@link #getCatalinaHomeFile()} will be used. + * + * @return the catalina base as a file + */ + public static File getCatalinaBaseFile() { + return catalinaBaseFile; + } + + + // Copied from ExceptionUtils since that class is not visible during start + static void handleThrowable(Throwable t) { + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof StackOverflowError) { + // Swallow silently - it should be recoverable + return; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + // All other instances of Throwable will be silently swallowed + } + + // Copied from ExceptionUtils so that there is no dependency on utils + static Throwable unwrapInvocationTargetException(Throwable t) { + if (t instanceof InvocationTargetException && t.getCause() != null) { + return t.getCause(); + } + return t; + } + + // Protected for unit testing + protected static String[] getPaths(String value) { + + List result = new ArrayList<>(); + Matcher matcher = PATH_PATTERN.matcher(value); + + while (matcher.find()) { + String path = value.substring(matcher.start(), matcher.end()); + + path = path.trim(); + if (path.length() == 0) { + continue; + } + + char first = path.charAt(0); + char last = path.charAt(path.length() - 1); + + if (first == '"' && last == '"' && path.length() > 1) { + path = path.substring(1, path.length() - 1); + path = path.trim(); + if (path.length() == 0) { + continue; + } + } else if (path.contains("\"")) { + // Unbalanced quotes + // Too early to use standard i18n support. The class path hasn't + // been configured. + throw new IllegalArgumentException( + "The double quote [\"] character can only be used to quote paths. It must " + + "not appear in a path. This loader path is not valid: [" + value + "]"); + } else { + // Not quoted - NO-OP + } + + result.add(path); + } + + return result.toArray(new String[0]); + } +} diff --git a/java/org/apache/catalina/startup/Catalina.java b/java/org/apache/catalina/startup/Catalina.java new file mode 100644 index 0000000..01d7d3c --- /dev/null +++ b/java/org/apache/catalina/startup/Catalina.java @@ -0,0 +1,1023 @@ +/* + * 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.catalina.startup; + + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.net.ConnectException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.logging.LogManager; + +import org.apache.catalina.Container; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Server; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.security.SecurityConfig; +import org.apache.juli.ClassLoaderLogManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.Rule; +import org.apache.tomcat.util.digester.RuleSet; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource; +import org.apache.tomcat.util.log.SystemLogHandler; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; + + +/** + * Startup/Shutdown shell program for Catalina. The following command line options are recognized: + *
      + *
    • -config {pathname} - Set the pathname of the configuration file to be processed. If a relative path is + * specified, it will be interpreted as relative to the directory pathname specified by the "catalina.base" system + * property. [conf/server.xml]
    • + *
    • -help - Display usage information.
    • + *
    • -nonaming - Disable naming support.
    • + *
    • configtest - Try to test the config
    • + *
    • start - Start an instance of Catalina.
    • + *
    • stop - Stop the currently running instance of Catalina.
    • + *
    + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class Catalina { + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + public static final String SERVER_XML = "conf/server.xml"; + + // ----------------------------------------------------- Instance Variables + + /** + * Use await. + */ + protected boolean await = false; + + /** + * Pathname to the server configuration file. + */ + protected String configFile = SERVER_XML; + + // XXX Should be moved to embedded + /** + * The shared extensions class loader for this server. + */ + protected ClassLoader parentClassLoader = Catalina.class.getClassLoader(); + + + /** + * The server component we are starting or stopping. + */ + protected Server server = null; + + + /** + * Use shutdown hook flag. + */ + protected boolean useShutdownHook = true; + + + /** + * Shutdown hook. + */ + protected Thread shutdownHook = null; + + + /** + * Is naming enabled ? + */ + protected boolean useNaming = true; + + + /** + * Prevent duplicate loads. + */ + protected boolean loaded = false; + + + /** + * Rethrow exceptions on init failure. + */ + protected boolean throwOnInitFailure = Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"); + + + /** + * Generate Tomcat embedded code from configuration files. + */ + protected boolean generateCode = false; + + + /** + * Location of generated sources. + */ + protected File generatedCodeLocation = null; + + + /** + * Value of the argument. + */ + protected String generatedCodeLocationParameter = null; + + + /** + * Top package name for generated source. + */ + protected String generatedCodePackage = "catalinaembedded"; + + + /** + * Use generated code as a replacement for configuration files. + */ + protected boolean useGeneratedCode = false; + + + // ----------------------------------------------------------- Constructors + + public Catalina() { + setSecurityProtection(); + ExceptionUtils.preload(); + } + + + // ------------------------------------------------------------- Properties + + public void setConfigFile(String file) { + configFile = file; + } + + + public String getConfigFile() { + return configFile; + } + + + public void setUseShutdownHook(boolean useShutdownHook) { + this.useShutdownHook = useShutdownHook; + } + + + public boolean getUseShutdownHook() { + return useShutdownHook; + } + + + public boolean getGenerateCode() { + return this.generateCode; + } + + + public void setGenerateCode(boolean generateCode) { + this.generateCode = generateCode; + } + + + public boolean getUseGeneratedCode() { + return this.useGeneratedCode; + } + + + public void setUseGeneratedCode(boolean useGeneratedCode) { + this.useGeneratedCode = useGeneratedCode; + } + + + public File getGeneratedCodeLocation() { + return this.generatedCodeLocation; + } + + + public void setGeneratedCodeLocation(File generatedCodeLocation) { + this.generatedCodeLocation = generatedCodeLocation; + } + + + public String getGeneratedCodePackage() { + return this.generatedCodePackage; + } + + + public void setGeneratedCodePackage(String generatedCodePackage) { + this.generatedCodePackage = generatedCodePackage; + } + + + /** + * @return true if an exception should be thrown if an error occurs during server init + */ + public boolean getThrowOnInitFailure() { + return throwOnInitFailure; + } + + + /** + * Set the behavior regarding errors that could occur during server init. + * + * @param throwOnInitFailure the new flag value + */ + public void setThrowOnInitFailure(boolean throwOnInitFailure) { + this.throwOnInitFailure = throwOnInitFailure; + } + + + /** + * Set the shared extensions class loader. + * + * @param parentClassLoader The shared extensions class loader. + */ + public void setParentClassLoader(ClassLoader parentClassLoader) { + this.parentClassLoader = parentClassLoader; + } + + public ClassLoader getParentClassLoader() { + if (parentClassLoader != null) { + return parentClassLoader; + } + return ClassLoader.getSystemClassLoader(); + } + + public void setServer(Server server) { + this.server = server; + } + + + public Server getServer() { + return server; + } + + + /** + * @return true if naming is enabled. + */ + public boolean isUseNaming() { + return this.useNaming; + } + + + /** + * Enables or disables naming support. + * + * @param useNaming The new use naming value + */ + public void setUseNaming(boolean useNaming) { + this.useNaming = useNaming; + } + + public void setAwait(boolean b) { + await = b; + } + + public boolean isAwait() { + return await; + } + + // ------------------------------------------------------ Protected Methods + + + /** + * Process the specified command line arguments. + * + * @param args Command line arguments to process + * + * @return true if we should continue processing + */ + protected boolean arguments(String args[]) { + + boolean isConfig = false; + boolean isGenerateCode = false; + + if (args.length < 1) { + usage(); + return false; + } + + for (String arg : args) { + if (isConfig) { + configFile = arg; + isConfig = false; + } else if (arg.equals("-config")) { + isConfig = true; + } else if (arg.equals("-generateCode")) { + setGenerateCode(true); + isGenerateCode = true; + } else if (arg.equals("-useGeneratedCode")) { + setUseGeneratedCode(true); + isGenerateCode = false; + } else if (arg.equals("-nonaming")) { + setUseNaming(false); + isGenerateCode = false; + } else if (arg.equals("-help")) { + usage(); + return false; + } else if (arg.equals("start")) { + isGenerateCode = false; + // NOOP + } else if (arg.equals("configtest")) { + isGenerateCode = false; + // NOOP + } else if (arg.equals("stop")) { + isGenerateCode = false; + // NOOP + } else if (isGenerateCode) { + generatedCodeLocationParameter = arg; + isGenerateCode = false; + } else { + usage(); + return false; + } + } + + return true; + } + + + /** + * Return a File object representing our configuration file. + * + * @return the main configuration file + */ + protected File configFile() { + + File file = new File(configFile); + if (!file.isAbsolute()) { + file = new File(Bootstrap.getCatalinaBase(), configFile); + } + return file; + + } + + + /** + * Create and configure the Digester we will be using for startup. + * + * @return the main digester to parse server.xml + */ + protected Digester createStartDigester() { + // Initialize the digester + Digester digester = new Digester(); + digester.setValidating(false); + digester.setRulesValidation(true); + Map,List> fakeAttributes = new HashMap<>(); + // Ignore className on all elements + List objectAttrs = new ArrayList<>(); + objectAttrs.add("className"); + fakeAttributes.put(Object.class, objectAttrs); + // Ignore attribute added by Eclipse for its internal tracking + List contextAttrs = new ArrayList<>(); + contextAttrs.add("source"); + fakeAttributes.put(StandardContext.class, contextAttrs); + // Ignore Connector attribute used internally but set on Server + List connectorAttrs = new ArrayList<>(); + connectorAttrs.add("portOffset"); + fakeAttributes.put(Connector.class, connectorAttrs); + digester.setFakeAttributes(fakeAttributes); + digester.setUseContextClassLoader(true); + + // Configure the actions we will be using + digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className"); + digester.addSetProperties("Server"); + digester.addSetNext("Server", "setServer", "org.apache.catalina.Server"); + + digester.addObjectCreate("Server/GlobalNamingResources", "org.apache.catalina.deploy.NamingResourcesImpl"); + digester.addSetProperties("Server/GlobalNamingResources"); + digester.addSetNext("Server/GlobalNamingResources", "setGlobalNamingResources", + "org.apache.catalina.deploy.NamingResourcesImpl"); + + digester.addRule("Server/Listener", new ListenerCreateRule(null, "className")); + digester.addSetProperties("Server/Listener"); + digester.addSetNext("Server/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); + + digester.addObjectCreate("Server/Service", "org.apache.catalina.core.StandardService", "className"); + digester.addSetProperties("Server/Service"); + digester.addSetNext("Server/Service", "addService", "org.apache.catalina.Service"); + + digester.addObjectCreate("Server/Service/Listener", null, // MUST be specified in the element + "className"); + digester.addSetProperties("Server/Service/Listener"); + digester.addSetNext("Server/Service/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); + + // Executor + digester.addObjectCreate("Server/Service/Executor", "org.apache.catalina.core.StandardThreadExecutor", + "className"); + digester.addSetProperties("Server/Service/Executor"); + + digester.addSetNext("Server/Service/Executor", "addExecutor", "org.apache.catalina.Executor"); + + digester.addRule("Server/Service/Connector", new ConnectorCreateRule()); + digester.addSetProperties("Server/Service/Connector", + new String[] { "executor", "sslImplementationName", "protocol" }); + digester.addSetNext("Server/Service/Connector", "addConnector", "org.apache.catalina.connector.Connector"); + + digester.addRule("Server/Service/Connector", new AddPortOffsetRule()); + + digester.addObjectCreate("Server/Service/Connector/SSLHostConfig", "org.apache.tomcat.util.net.SSLHostConfig"); + digester.addSetProperties("Server/Service/Connector/SSLHostConfig"); + digester.addSetNext("Server/Service/Connector/SSLHostConfig", "addSslHostConfig", + "org.apache.tomcat.util.net.SSLHostConfig"); + + digester.addRule("Server/Service/Connector/SSLHostConfig/Certificate", new CertificateCreateRule()); + digester.addSetProperties("Server/Service/Connector/SSLHostConfig/Certificate", new String[] { "type" }); + digester.addSetNext("Server/Service/Connector/SSLHostConfig/Certificate", "addCertificate", + "org.apache.tomcat.util.net.SSLHostConfigCertificate"); + + digester.addObjectCreate("Server/Service/Connector/SSLHostConfig/OpenSSLConf", + "org.apache.tomcat.util.net.openssl.OpenSSLConf"); + digester.addSetProperties("Server/Service/Connector/SSLHostConfig/OpenSSLConf"); + digester.addSetNext("Server/Service/Connector/SSLHostConfig/OpenSSLConf", "setOpenSslConf", + "org.apache.tomcat.util.net.openssl.OpenSSLConf"); + + digester.addObjectCreate("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd", + "org.apache.tomcat.util.net.openssl.OpenSSLConfCmd"); + digester.addSetProperties("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd"); + digester.addSetNext("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd", "addCmd", + "org.apache.tomcat.util.net.openssl.OpenSSLConfCmd"); + + digester.addObjectCreate("Server/Service/Connector/Listener", null, // MUST be specified in the element + "className"); + digester.addSetProperties("Server/Service/Connector/Listener"); + digester.addSetNext("Server/Service/Connector/Listener", "addLifecycleListener", + "org.apache.catalina.LifecycleListener"); + + digester.addObjectCreate("Server/Service/Connector/UpgradeProtocol", null, // MUST be specified in the element + "className"); + digester.addSetProperties("Server/Service/Connector/UpgradeProtocol"); + digester.addSetNext("Server/Service/Connector/UpgradeProtocol", "addUpgradeProtocol", + "org.apache.coyote.UpgradeProtocol"); + + // Add RuleSets for nested elements + digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/")); + digester.addRuleSet(new EngineRuleSet("Server/Service/")); + digester.addRuleSet(new HostRuleSet("Server/Service/Engine/")); + digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/")); + addClusterRuleSet(digester, "Server/Service/Engine/Host/Cluster/"); + digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/")); + + // When the 'engine' is found, set the parentClassLoader. + digester.addRule("Server/Service/Engine", new SetParentClassLoaderRule(parentClassLoader)); + addClusterRuleSet(digester, "Server/Service/Engine/Cluster/"); + + return digester; + + } + + /** + * Cluster support is optional. The JARs may have been removed. + */ + private void addClusterRuleSet(Digester digester, String prefix) { + Class clazz = null; + Constructor constructor = null; + try { + clazz = Class.forName("org.apache.catalina.ha.ClusterRuleSet"); + constructor = clazz.getConstructor(String.class); + RuleSet ruleSet = (RuleSet) constructor.newInstance(prefix); + digester.addRuleSet(ruleSet); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("catalina.noCluster", e.getClass().getName() + ": " + e.getMessage()), e); + } else if (log.isInfoEnabled()) { + log.info(sm.getString("catalina.noCluster", e.getClass().getName() + ": " + e.getMessage())); + } + } + } + + /** + * Create and configure the Digester we will be using for shutdown. + * + * @return the digester to process the stop operation + */ + protected Digester createStopDigester() { + + // Initialize the digester + Digester digester = new Digester(); + digester.setUseContextClassLoader(true); + + // Configure the rules we need for shutting down + digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className"); + digester.addSetProperties("Server"); + digester.addSetNext("Server", "setServer", "org.apache.catalina.Server"); + + return digester; + + } + + + protected void parseServerXml(boolean start) { + // Set configuration source + ConfigFileLoader + .setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile())); + File file = configFile(); + + if (useGeneratedCode && !Digester.isGeneratedCodeLoaderSet()) { + // Load loader + String loaderClassName = generatedCodePackage + ".DigesterGeneratedCodeLoader"; + try { + Digester.GeneratedCodeLoader loader = (Digester.GeneratedCodeLoader) Catalina.class.getClassLoader() + .loadClass(loaderClassName).getDeclaredConstructor().newInstance(); + Digester.setGeneratedCodeLoader(loader); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.info(sm.getString("catalina.noLoader", loaderClassName), e); + } else { + log.info(sm.getString("catalina.noLoader", loaderClassName)); + } + // No loader so don't use generated code + useGeneratedCode = false; + } + } + + // Init source location + File serverXmlLocation = null; + String xmlClassName = null; + if (generateCode || useGeneratedCode) { + xmlClassName = start ? generatedCodePackage + ".ServerXml" : generatedCodePackage + ".ServerXmlStop"; + } + if (generateCode) { + if (generatedCodeLocationParameter != null) { + generatedCodeLocation = new File(generatedCodeLocationParameter); + if (!generatedCodeLocation.isAbsolute()) { + generatedCodeLocation = new File(Bootstrap.getCatalinaHomeFile(), generatedCodeLocationParameter); + } + } else { + generatedCodeLocation = new File(Bootstrap.getCatalinaHomeFile(), "work"); + } + serverXmlLocation = new File(generatedCodeLocation, generatedCodePackage); + if (!serverXmlLocation.isDirectory() && !serverXmlLocation.mkdirs()) { + log.warn(sm.getString("catalina.generatedCodeLocationError", generatedCodeLocation.getAbsolutePath())); + // Disable code generation + generateCode = false; + } + } + + ServerXml serverXml = null; + if (useGeneratedCode) { + serverXml = (ServerXml) Digester.loadGeneratedClass(xmlClassName); + } + + if (serverXml != null) { + serverXml.load(this); + } else { + try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) { + // Create and execute our Digester + Digester digester = start ? createStartDigester() : createStopDigester(); + InputStream inputStream = resource.getInputStream(); + InputSource inputSource = new InputSource(resource.getURI().toURL().toString()); + inputSource.setByteStream(inputStream); + digester.push(this); + if (generateCode) { + digester.startGeneratingCode(); + generateClassHeader(digester, start); + } + digester.parse(inputSource); + if (generateCode) { + generateClassFooter(digester); + try (FileWriter writer = new FileWriter( + new File(serverXmlLocation, start ? "ServerXml.java" : "ServerXmlStop.java"))) { + writer.write(digester.getGeneratedCode().toString()); + } + digester.endGeneratingCode(); + Digester.addGeneratedClass(xmlClassName); + } + } catch (Exception e) { + log.warn(sm.getString("catalina.configFail", file.getAbsolutePath()), e); + if (file.exists() && !file.canRead()) { + log.warn(sm.getString("catalina.incorrectPermissions")); + } + } + } + } + + public void stopServer() { + stopServer(null); + } + + public void stopServer(String[] arguments) { + + if (arguments != null) { + arguments(arguments); + } + + Server s = getServer(); + if (s == null) { + parseServerXml(false); + if (getServer() == null) { + log.error(sm.getString("catalina.stopError")); + System.exit(1); + } + } else { + // Server object already present. Must be running as a service + try { + s.stop(); + s.destroy(); + } catch (LifecycleException e) { + log.error(sm.getString("catalina.stopError"), e); + } + return; + } + + // Stop the existing server + s = getServer(); + if (s.getPortWithOffset() > 0) { + try (Socket socket = new Socket(s.getAddress(), s.getPortWithOffset()); + OutputStream stream = socket.getOutputStream()) { + String shutdown = s.getShutdown(); + for (int i = 0; i < shutdown.length(); i++) { + stream.write(shutdown.charAt(i)); + } + stream.flush(); + } catch (ConnectException ce) { + log.error(sm.getString("catalina.stopServer.connectException", s.getAddress(), + String.valueOf(s.getPortWithOffset()), String.valueOf(s.getPort()), + String.valueOf(s.getPortOffset()))); + log.error(sm.getString("catalina.stopError"), ce); + System.exit(1); + } catch (IOException e) { + log.error(sm.getString("catalina.stopError"), e); + System.exit(1); + } + } else { + log.error(sm.getString("catalina.stopServer")); + System.exit(1); + } + } + + + /** + * Start a new server instance. + */ + public void load() { + + if (loaded) { + return; + } + loaded = true; + + long t1 = System.nanoTime(); + + // Before digester - it may be needed + initNaming(); + + // Parse main server.xml + parseServerXml(true); + Server s = getServer(); + if (s == null) { + return; + } + + getServer().setCatalina(this); + getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile()); + getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile()); + + // Stream redirection + initStreams(); + + // Start the new server + try { + getServer().init(); + } catch (LifecycleException e) { + if (throwOnInitFailure) { + throw new Error(e); + } else { + log.error(sm.getString("catalina.initError"), e); + } + } + + if (log.isInfoEnabled()) { + log.info(sm.getString("catalina.init", + Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1)))); + } + } + + + /* + * Load using arguments + */ + public void load(String args[]) { + + try { + if (arguments(args)) { + load(); + } + } catch (Exception e) { + e.printStackTrace(System.out); + } + } + + + /** + * Start a new server instance. + */ + public void start() { + + if (getServer() == null) { + load(); + } + + if (getServer() == null) { + log.fatal(sm.getString("catalina.noServer")); + return; + } + + long t1 = System.nanoTime(); + + // Start the new server + try { + getServer().start(); + } catch (LifecycleException e) { + log.fatal(sm.getString("catalina.serverStartFail"), e); + try { + getServer().destroy(); + } catch (LifecycleException e1) { + log.debug(sm.getString("catalina.destroyFail"), e1); + } + return; + } + + if (log.isInfoEnabled()) { + log.info(sm.getString("catalina.startup", + Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1)))); + } + + if (generateCode) { + // Generate loader which will load all generated classes + generateLoader(); + } + + // Register shutdown hook + if (useShutdownHook) { + if (shutdownHook == null) { + shutdownHook = new CatalinaShutdownHook(); + } + Runtime.getRuntime().addShutdownHook(shutdownHook); + + // If JULI is being used, disable JULI's shutdown hook since + // shutdown hooks run in parallel and log messages may be lost + // if JULI's hook completes before the CatalinaShutdownHook() + LogManager logManager = LogManager.getLogManager(); + if (logManager instanceof ClassLoaderLogManager) { + ((ClassLoaderLogManager) logManager).setUseShutdownHook(false); + } + } + + if (await) { + await(); + stop(); + } + } + + + /** + * Stop an existing server instance. + */ + public void stop() { + + try { + // Remove the ShutdownHook first so that server.stop() + // doesn't get invoked twice + if (useShutdownHook) { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + + // If JULI is being used, re-enable JULI's shutdown to ensure + // log messages are not lost + LogManager logManager = LogManager.getLogManager(); + if (logManager instanceof ClassLoaderLogManager) { + ((ClassLoaderLogManager) logManager).setUseShutdownHook(true); + } + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // This will fail on JDK 1.2. Ignoring, as Tomcat can run + // fine without the shutdown hook. + } + + // Shut down the server + try { + Server s = getServer(); + LifecycleState state = s.getState(); + if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0 && LifecycleState.DESTROYED.compareTo(state) >= 0) { + // Nothing to do. stop() was already called + } else { + s.stop(); + s.destroy(); + } + } catch (LifecycleException e) { + log.error(sm.getString("catalina.stopError"), e); + } + + } + + + /** + * Await and shutdown. + */ + public void await() { + + getServer().await(); + + } + + + /** + * Print usage information for this application. + */ + protected void usage() { + + System.out.println(sm.getString("catalina.usage")); + + } + + + protected void initStreams() { + // Replace System.out and System.err with a custom PrintStream + System.setOut(new SystemLogHandler(System.out)); + System.setErr(new SystemLogHandler(System.err)); + } + + + protected void initNaming() { + // Setting additional variables + if (!useNaming) { + log.info(sm.getString("catalina.noNaming")); + System.setProperty("catalina.useNaming", "false"); + } else { + System.setProperty("catalina.useNaming", "true"); + String value = "org.apache.naming"; + String oldValue = System.getProperty(javax.naming.Context.URL_PKG_PREFIXES); + if (oldValue != null) { + value = value + ":" + oldValue; + } + System.setProperty(javax.naming.Context.URL_PKG_PREFIXES, value); + if (log.isDebugEnabled()) { + log.debug(sm.getString("catalina.namingPrefix", value)); + } + value = System.getProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY); + if (value == null) { + System.setProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY, + "org.apache.naming.java.javaURLContextFactory"); + } else { + log.debug(sm.getString("catalina.initialContextFactory", value)); + } + } + } + + + /** + * Set the security package access/protection. + */ + protected void setSecurityProtection() { + SecurityConfig securityConfig = SecurityConfig.newInstance(); + securityConfig.setPackageDefinition(); + securityConfig.setPackageAccess(); + } + + + protected void generateLoader() { + String loaderClassName = "DigesterGeneratedCodeLoader"; + StringBuilder code = new StringBuilder(); + code.append("package ").append(generatedCodePackage).append(';').append(System.lineSeparator()); + code.append("public class ").append(loaderClassName); + code.append(" implements org.apache.tomcat.util.digester.Digester.GeneratedCodeLoader {") + .append(System.lineSeparator()); + code.append("public Object loadGeneratedCode(String className) {").append(System.lineSeparator()); + code.append("switch (className) {").append(System.lineSeparator()); + for (String generatedClassName : Digester.getGeneratedClasses()) { + code.append("case \"").append(generatedClassName).append("\" : return new ").append(generatedClassName); + code.append("();").append(System.lineSeparator()); + } + code.append("default: return null; }").append(System.lineSeparator()); + code.append("}}").append(System.lineSeparator()); + File loaderLocation = new File(generatedCodeLocation, generatedCodePackage); + try (FileWriter writer = new FileWriter(new File(loaderLocation, loaderClassName + ".java"))) { + writer.write(code.toString()); + } catch (IOException e) { + // Should not happen + log.debug(sm.getString("catalina.loaderWriteFail"), e); + } + } + + + protected void generateClassHeader(Digester digester, boolean start) { + StringBuilder code = digester.getGeneratedCode(); + code.append("package ").append(generatedCodePackage).append(';').append(System.lineSeparator()); + code.append("public class ServerXml"); + if (!start) { + code.append("Stop"); + } + code.append(" implements "); + code.append(ServerXml.class.getName().replace('$', '.')).append(" {").append(System.lineSeparator()); + code.append("public void load(").append(Catalina.class.getName()); + code.append(' ').append(digester.toVariableName(this)).append(") {").append(System.lineSeparator()); + } + + + protected void generateClassFooter(Digester digester) { + StringBuilder code = digester.getGeneratedCode(); + code.append('}').append(System.lineSeparator()); + code.append('}').append(System.lineSeparator()); + } + + + public interface ServerXml { + void load(Catalina catalina); + } + + + // --------------------------------------- CatalinaShutdownHook Inner Class + + // XXX Should be moved to embedded ! + /** + * Shutdown hook which will perform a clean shutdown of Catalina if needed. + */ + protected class CatalinaShutdownHook extends Thread { + + @Override + public void run() { + try { + if (getServer() != null) { + Catalina.this.stop(); + } + } catch (Throwable ex) { + ExceptionUtils.handleThrowable(ex); + log.error(sm.getString("catalina.shutdownHookFail"), ex); + } finally { + // If JULI is used, shut JULI down *after* the server shuts down + // so log messages aren't lost + LogManager logManager = LogManager.getLogManager(); + if (logManager instanceof ClassLoaderLogManager) { + ((ClassLoaderLogManager) logManager).shutdown(); + } + } + } + } + + + private static final Log log = LogFactory.getLog(Catalina.class); + + + /** + * Rule that sets the parent class loader for the top object on the stack, which must be a Container. + */ + + final class SetParentClassLoaderRule extends Rule { + + SetParentClassLoaderRule(ClassLoader parentClassLoader) { + + this.parentClassLoader = parentClassLoader; + + } + + ClassLoader parentClassLoader = null; + + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace("Setting parent class loader"); + } + + Container top = (Container) digester.peek(); + top.setParentClassLoader(parentClassLoader); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(top)).append(".setParentClassLoader("); + code.append(digester.toVariableName(Catalina.this)).append(".getParentClassLoader());"); + code.append(System.lineSeparator()); + } + } + + } + +} diff --git a/java/org/apache/catalina/startup/CatalinaBaseConfigurationSource.java b/java/org/apache/catalina/startup/CatalinaBaseConfigurationSource.java new file mode 100644 index 0000000..cedc1cd --- /dev/null +++ b/java/org/apache/catalina/startup/CatalinaBaseConfigurationSource.java @@ -0,0 +1,165 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.file.ConfigurationSource; +import org.apache.tomcat.util.res.StringManager; + +public class CatalinaBaseConfigurationSource implements ConfigurationSource { + + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + private final String serverXmlPath; + private final File catalinaBaseFile; + private final URI catalinaBaseUri; + + public CatalinaBaseConfigurationSource(File catalinaBaseFile, String serverXmlPath) { + this.catalinaBaseFile = catalinaBaseFile; + catalinaBaseUri = catalinaBaseFile.toURI(); + this.serverXmlPath = serverXmlPath; + } + + @Override + public Resource getServerXml() throws IOException { + IOException ioe = null; + Resource result = null; + try { + if (serverXmlPath == null || serverXmlPath.equals(Catalina.SERVER_XML)) { + result = ConfigurationSource.super.getServerXml(); + } else { + result = getResource(serverXmlPath); + } + } catch (IOException e) { + ioe = e; + } + if (result == null) { + // Compatibility with legacy server-embed.xml location + InputStream stream = getClass().getClassLoader().getResourceAsStream("server-embed.xml"); + if (stream != null) { + try { + result = new Resource(stream, getClass().getClassLoader().getResource("server-embed.xml").toURI()); + } catch (URISyntaxException e) { + stream.close(); + } + } + } + + if (result == null && ioe != null) { + throw ioe; + } else { + return result; + } + } + + @Override + public Resource getResource(String name) throws IOException { + // Originally only File was supported. Class loader and URI were added + // later. However (see bug 65106) treating some URIs as files can cause + // problems. Therefore, if path starts with a valid URI scheme then skip + // straight to processing this as a URI. + if (!UriUtil.isAbsoluteURI(name)) { + File f = new File(name); + if (!f.isAbsolute()) { + f = new File(catalinaBaseFile, name); + } + if (f.isFile()) { + FileInputStream fis = new FileInputStream(f); + return new Resource(fis, f.toURI()); + } + + // Try classloader + InputStream stream = null; + try { + stream = getClass().getClassLoader().getResourceAsStream(name); + if (stream != null) { + return new Resource(stream, getClass().getClassLoader().getResource(name).toURI()); + } + } catch (URISyntaxException e) { + stream.close(); + throw new IOException(sm.getString("catalinaConfigurationSource.cannotObtainURL", name), e); + } + } + + // Then try URI. + URI uri = null; + try { + uri = getURIInternal(name); + } catch (IllegalArgumentException e) { + throw new IOException(sm.getString("catalinaConfigurationSource.cannotObtainURL", name)); + } + + // Obtain the input stream we need + try { + URL url = uri.toURL(); + return new Resource(url.openConnection().getInputStream(), uri); + } catch (MalformedURLException e) { + throw new IOException(sm.getString("catalinaConfigurationSource.cannotObtainURL", name), e); + } + } + + @Override + public URI getURI(String name) { + // Originally only File was supported. Class loader and URI were added + // later. However (see bug 65106) treating some URIs as files can cause + // problems. Therefore, if path starts with a valid URI scheme then skip + // straight to processing this as a URI. + if (!UriUtil.isAbsoluteURI(name)) { + File f = new File(name); + if (!f.isAbsolute()) { + f = new File(catalinaBaseFile, name); + } + if (f.isFile()) { + return f.toURI(); + } + + // Try classloader + try { + URL resource = getClass().getClassLoader().getResource(name); + if (resource != null) { + return resource.toURI(); + } + } catch (Exception e) { + // Ignore + } + } + + return getURIInternal(name); + } + + private URI getURIInternal(String name) { + // Then try URI. + // Using resolve() enables the code to handle relative paths that did + // not point to a file + URI uri; + if (catalinaBaseUri != null) { + uri = catalinaBaseUri.resolve(name); + } else { + uri = URI.create(name); + } + return uri; + } +} diff --git a/java/org/apache/catalina/startup/CatalinaProperties.java b/java/org/apache/catalina/startup/CatalinaProperties.java new file mode 100644 index 0000000..3566e1a --- /dev/null +++ b/java/org/apache/catalina/startup/CatalinaProperties.java @@ -0,0 +1,144 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Enumeration; +import java.util.Properties; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + * Utility class to read the bootstrap Catalina configuration. + * + * @author Remy Maucherat + */ +public class CatalinaProperties { + + private static final Log log = LogFactory.getLog(CatalinaProperties.class); + + private static Properties properties = null; + + + static { + loadProperties(); + } + + + /** + * @param name The property name + * + * @return specified property value + */ + public static String getProperty(String name) { + return properties.getProperty(name); + } + + + /** + * Load properties. + */ + private static void loadProperties() { + + InputStream is = null; + String fileName = "catalina.properties"; + + try { + String configUrl = System.getProperty("catalina.config"); + if (configUrl != null) { + if (configUrl.indexOf('/') == -1) { + // No '/'. Must be a file name rather than a URL + fileName = configUrl; + } else { + is = new URI(configUrl).toURL().openStream(); + } + } + } catch (Throwable t) { + handleThrowable(t); + } + + if (is == null) { + try { + File home = new File(Bootstrap.getCatalinaBase()); + File conf = new File(home, "conf"); + File propsFile = new File(conf, fileName); + is = new FileInputStream(propsFile); + } catch (Throwable t) { + handleThrowable(t); + } + } + + if (is == null) { + try { + is = CatalinaProperties.class.getResourceAsStream("/org/apache/catalina/startup/catalina.properties"); + } catch (Throwable t) { + handleThrowable(t); + } + } + + if (is != null) { + try { + properties = new Properties(); + properties.load(is); + } catch (Throwable t) { + handleThrowable(t); + log.warn(t); + } finally { + try { + is.close(); + } catch (IOException ioe) { + log.warn("Could not close catalina properties file", ioe); + } + } + } + + if ((is == null)) { + // Do something + log.warn("Failed to load catalina properties file"); + // That's fine - we have reasonable defaults. + properties = new Properties(); + } + + // Register the properties as system properties + Enumeration enumeration = properties.propertyNames(); + while (enumeration.hasMoreElements()) { + String name = (String) enumeration.nextElement(); + String value = properties.getProperty(name); + if (value != null) { + System.setProperty(name, value); + } + } + } + + + // Copied from ExceptionUtils since that class is not visible during start + private static void handleThrowable(Throwable t) { + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + // All other instances of Throwable will be silently swallowed + } +} diff --git a/java/org/apache/catalina/startup/CertificateCreateRule.java b/java/org/apache/catalina/startup/CertificateCreateRule.java new file mode 100644 index 0000000..a6f1760 --- /dev/null +++ b/java/org/apache/catalina/startup/CertificateCreateRule.java @@ -0,0 +1,69 @@ +/* + * 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.catalina.startup; + +import org.apache.tomcat.util.digester.Rule; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.xml.sax.Attributes; + +/** + * Rule implementation that creates an SSLHostConfigCertificate. + */ +public class CertificateCreateRule extends Rule { + + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + SSLHostConfig sslHostConfig = (SSLHostConfig) digester.peek(); + + Type type; + String typeValue = attributes.getValue("type"); + if (typeValue == null || typeValue.length() == 0) { + type = Type.UNDEFINED; + } else { + type = Type.valueOf(typeValue); + } + + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, type); + + digester.push(certificate); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(SSLHostConfigCertificate.class.getName()).append(' ') + .append(digester.toVariableName(certificate)); + code.append(" = new ").append(SSLHostConfigCertificate.class.getName()); + code.append('(').append(digester.toVariableName(sslHostConfig)); + code.append(", ").append(Type.class.getName().replace('$', '.')).append('.').append(type).append(");"); + code.append(System.lineSeparator()); + } + } + + + /** + * Process the end of this element. + * + * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace + * aware or the element has no namespace + * @param name the local name if the parser is namespace aware, or just the element name otherwise + */ + @Override + public void end(String namespace, String name) throws Exception { + digester.pop(); + } +} diff --git a/java/org/apache/catalina/startup/ClassLoaderFactory.java b/java/org/apache/catalina/startup/ClassLoaderFactory.java new file mode 100644 index 0000000..5965baa --- /dev/null +++ b/java/org/apache/catalina/startup/ClassLoaderFactory.java @@ -0,0 +1,316 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + *

    + * Utility class for building class loaders for Catalina. The factory method requires the following parameters in order + * to build a new class loader (with suitable defaults in all cases): + *

    + *
      + *
    • A set of directories containing unpacked classes (and resources) that should be included in the class loader's + * repositories.
    • + *
    • A set of directories containing classes and resources in JAR files. Each readable JAR file discovered in these + * directories will be added to the class loader's repositories.
    • + *
    • ClassLoader instance that should become the parent of the new class loader.
    • + *
    + * + * @author Craig R. McClanahan + */ +public final class ClassLoaderFactory { + + + private static final Log log = LogFactory.getLog(ClassLoaderFactory.class); + + // --------------------------------------------------------- Public Methods + + + /** + * Create and return a new class loader, based on the configuration defaults and the specified directory paths: + * + * @param unpacked Array of pathnames to unpacked directories that should be added to the repositories of the class + * loader, or null for no unpacked directories to be considered + * @param packed Array of pathnames to directories containing JAR files that should be added to the repositories + * of the class loader, or null for no directories of JAR files to be considered + * @param parent Parent class loader for the new class loader, or null for the system class loader. + * + * @return the new class loader + * + * @exception Exception if an error occurs constructing the class loader + */ + public static ClassLoader createClassLoader(File unpacked[], File packed[], final ClassLoader parent) + throws Exception { + + if (log.isDebugEnabled()) { + log.debug("Creating new class loader"); + } + + // Construct the "class path" for this class loader + Set set = new LinkedHashSet<>(); + + // Add unpacked directories + if (unpacked != null) { + for (File file : unpacked) { + if (!file.canRead()) { + continue; + } + file = new File(file.getCanonicalPath()); + URL url = file.toURI().toURL(); + if (log.isDebugEnabled()) { + log.debug(" Including directory " + url); + } + set.add(url); + } + } + + // Add packed directory JAR files + if (packed != null) { + for (File directory : packed) { + if (!directory.isDirectory() || !directory.canRead()) { + continue; + } + String filenames[] = directory.list(); + if (filenames == null) { + continue; + } + for (String s : filenames) { + String filename = s.toLowerCase(Locale.ENGLISH); + if (!filename.endsWith(".jar")) { + continue; + } + File file = new File(directory, s); + if (log.isDebugEnabled()) { + log.debug(" Including jar file " + file.getAbsolutePath()); + } + URL url = file.toURI().toURL(); + set.add(url); + } + } + } + + // Construct the class loader itself + final URL[] array = set.toArray(new URL[0]); + return AccessController.doPrivileged((PrivilegedAction) () -> { + if (parent == null) { + return new URLClassLoader(array); + } else { + return new URLClassLoader(array, parent); + } + }); + } + + + /** + * Create and return a new class loader, based on the configuration defaults and the specified directory paths: + * + * @param repositories List of class directories, jar files, jar directories or URLS that should be added to the + * repositories of the class loader. + * @param parent Parent class loader for the new class loader, or null for the system class + * loader. + * + * @return the new class loader + * + * @exception Exception if an error occurs constructing the class loader + */ + public static ClassLoader createClassLoader(List repositories, final ClassLoader parent) + throws Exception { + + if (log.isDebugEnabled()) { + log.debug("Creating new class loader"); + } + + // Construct the "class path" for this class loader + Set set = new LinkedHashSet<>(); + + if (repositories != null) { + for (Repository repository : repositories) { + if (repository.getType() == RepositoryType.URL) { + URL url = buildClassLoaderUrl(repository.getLocation()); + if (log.isDebugEnabled()) { + log.debug(" Including URL " + url); + } + set.add(url); + } else if (repository.getType() == RepositoryType.DIR) { + File directory = new File(repository.getLocation()); + directory = directory.getCanonicalFile(); + if (!validateFile(directory, RepositoryType.DIR)) { + continue; + } + URL url = buildClassLoaderUrl(directory); + if (log.isDebugEnabled()) { + log.debug(" Including directory " + url); + } + set.add(url); + } else if (repository.getType() == RepositoryType.JAR) { + File file = new File(repository.getLocation()); + file = file.getCanonicalFile(); + if (!validateFile(file, RepositoryType.JAR)) { + continue; + } + URL url = buildClassLoaderUrl(file); + if (log.isDebugEnabled()) { + log.debug(" Including jar file " + url); + } + set.add(url); + } else if (repository.getType() == RepositoryType.GLOB) { + File directory = new File(repository.getLocation()); + directory = directory.getCanonicalFile(); + if (!validateFile(directory, RepositoryType.GLOB)) { + continue; + } + if (log.isDebugEnabled()) { + log.debug(" Including directory glob " + directory.getAbsolutePath()); + } + String filenames[] = directory.list(); + if (filenames == null) { + continue; + } + for (String s : filenames) { + String filename = s.toLowerCase(Locale.ENGLISH); + if (!filename.endsWith(".jar")) { + continue; + } + File file = new File(directory, s); + file = file.getCanonicalFile(); + if (!validateFile(file, RepositoryType.JAR)) { + continue; + } + if (log.isDebugEnabled()) { + log.debug(" Including glob jar file " + file.getAbsolutePath()); + } + URL url = buildClassLoaderUrl(file); + set.add(url); + } + } + } + } + + // Construct the class loader itself + final URL[] array = set.toArray(new URL[0]); + if (log.isTraceEnabled()) { + for (int i = 0; i < array.length; i++) { + log.trace(" location " + i + " is " + array[i]); + } + } + + return AccessController.doPrivileged((PrivilegedAction) () -> { + if (parent == null) { + return new URLClassLoader(array); + } else { + return new URLClassLoader(array, parent); + } + }); + } + + private static boolean validateFile(File file, RepositoryType type) throws IOException { + if (RepositoryType.DIR == type || RepositoryType.GLOB == type) { + if (!file.isDirectory() || !file.canRead()) { + String msg = "Problem with directory [" + file + "], exists: [" + file.exists() + "], isDirectory: [" + + file.isDirectory() + "], canRead: [" + file.canRead() + "]"; + + File home = new File(Bootstrap.getCatalinaHome()); + home = home.getCanonicalFile(); + File base = new File(Bootstrap.getCatalinaBase()); + base = base.getCanonicalFile(); + File defaultValue = new File(base, "lib"); + + // Existence of ${catalina.base}/lib directory is optional. + // Hide the warning if Tomcat runs with separate catalina.home + // and catalina.base and that directory is absent. + if (!home.getPath().equals(base.getPath()) && file.getPath().equals(defaultValue.getPath()) && + !file.exists()) { + log.debug(msg); + } else { + log.warn(msg); + } + return false; + } + } else if (RepositoryType.JAR == type) { + if (!file.canRead()) { + log.warn("Problem with JAR file [" + file + "], exists: [" + file.exists() + "], canRead: [" + + file.canRead() + "]"); + return false; + } + } + return true; + } + + + /* + * These two methods would ideally be in the utility class org.apache.tomcat.util.buf.UriUtil but that class is not + * visible until after the class loaders have been constructed. + */ + private static URL buildClassLoaderUrl(String urlString) throws MalformedURLException, URISyntaxException { + // URLs passed to class loaders may point to directories that contain + // JARs. If these URLs are used to construct URLs for resources in a JAR + // the URL will be used as is. It is therefore necessary to ensure that + // the sequence "!/" is not present in a class loader URL. + String result = urlString.replace("!/", "%21/"); + return new URI(result).toURL(); + } + + + private static URL buildClassLoaderUrl(File file) throws MalformedURLException, URISyntaxException { + // Could be a directory or a file + String fileUrlString = file.toURI().toString(); + fileUrlString = fileUrlString.replace("!/", "%21/"); + return new URI(fileUrlString).toURL(); + } + + + public enum RepositoryType { + DIR, + GLOB, + JAR, + URL + } + + public static class Repository { + private final String location; + private final RepositoryType type; + + public Repository(String location, RepositoryType type) { + this.location = location; + this.type = type; + } + + public String getLocation() { + return location; + } + + public RepositoryType getType() { + return type; + } + } +} diff --git a/java/org/apache/catalina/startup/ConnectorCreateRule.java b/java/org/apache/catalina/startup/ConnectorCreateRule.java new file mode 100644 index 0000000..d1eae04 --- /dev/null +++ b/java/org/apache/catalina/startup/ConnectorCreateRule.java @@ -0,0 +1,127 @@ +/* + * 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.catalina.startup; + + +import java.lang.reflect.Method; + +import org.apache.catalina.Executor; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.AbstractHttp11JsseProtocol; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.digester.Rule; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.Attributes; + + +/** + * Rule implementation that creates a connector. + */ + +public class ConnectorCreateRule extends Rule { + + private static final Log log = LogFactory.getLog(ConnectorCreateRule.class); + protected static final StringManager sm = StringManager.getManager(ConnectorCreateRule.class); + // --------------------------------------------------------- Public Methods + + + /** + * Process the beginning of this element. + * + * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace + * aware or the element has no namespace + * @param name the local name if the parser is namespace aware, or just the element name otherwise + * @param attributes The attribute list for this element + */ + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + Service svc = (Service) digester.peek(); + Executor ex = null; + String executorName = attributes.getValue("executor"); + if (executorName != null) { + ex = svc.getExecutor(executorName); + } + String protocolName = attributes.getValue("protocol"); + Connector con = new Connector(protocolName); + if (ex != null) { + setExecutor(con, ex); + } + String sslImplementationName = attributes.getValue("sslImplementationName"); + if (sslImplementationName != null) { + setSSLImplementationName(con, sslImplementationName); + } + digester.push(con); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(Connector.class.getName()).append(' ').append(digester.toVariableName(con)); + code.append(" = new ").append(Connector.class.getName()); + code.append("(new ").append(con.getProtocolHandlerClassName()).append("());"); + code.append(System.lineSeparator()); + if (ex != null) { + code.append(digester.toVariableName(con)).append(".getProtocolHandler().setExecutor("); + code.append(digester.toVariableName(svc)).append(".getExecutor(").append(executorName); + code.append("));"); + code.append(System.lineSeparator()); + } + if (sslImplementationName != null) { + code.append("((").append(AbstractHttp11JsseProtocol.class.getName()).append(") "); + code.append(digester.toVariableName(con)).append(".getProtocolHandler()).setSslImplementationName(\""); + code.append(sslImplementationName).append("\");"); + code.append(System.lineSeparator()); + } + } + } + + private static void setExecutor(Connector con, Executor ex) throws Exception { + Method m = IntrospectionUtils.findMethod(con.getProtocolHandler().getClass(), "setExecutor", + new Class[] { java.util.concurrent.Executor.class }); + if (m != null) { + m.invoke(con.getProtocolHandler(), new Object[] { ex }); + } else { + log.warn(sm.getString("connector.noSetExecutor", con)); + } + } + + private static void setSSLImplementationName(Connector con, String sslImplementationName) throws Exception { + Method m = IntrospectionUtils.findMethod(con.getProtocolHandler().getClass(), "setSslImplementationName", + new Class[] { String.class }); + if (m != null) { + m.invoke(con.getProtocolHandler(), new Object[] { sslImplementationName }); + } else { + log.warn(sm.getString("connector.noSetSSLImplementationName", con)); + } + } + + /** + * Process the end of this element. + * + * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace + * aware or the element has no namespace + * @param name the local name if the parser is namespace aware, or just the element name otherwise + */ + @Override + public void end(String namespace, String name) throws Exception { + digester.pop(); + } + + +} diff --git a/java/org/apache/catalina/startup/Constants.java b/java/org/apache/catalina/startup/Constants.java new file mode 100644 index 0000000..1a59380 --- /dev/null +++ b/java/org/apache/catalina/startup/Constants.java @@ -0,0 +1,58 @@ +/* + * 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.catalina.startup; + +/** + * String constants for the startup package.
    + * Note that some values include a leading '/' and that some do not. This is intentional based on how the values are + * used. + * + * @author Craig R. McClanahan + */ +public final class Constants { + + public static final String Package = "org.apache.catalina.startup"; + + public static final String ApplicationContextXml = "META-INF/context.xml"; + public static final String ApplicationWebXml = "/WEB-INF/web.xml"; + public static final String TomcatWebXml = "/WEB-INF/tomcat-web.xml"; + public static final String DefaultContextXml = "conf/context.xml"; + public static final String DefaultWebXml = "conf/web.xml"; + public static final String HostContextXml = "context.xml.default"; + public static final String HostWebXml = "web.xml.default"; + public static final String WarTracker = "/META-INF/war-tracker"; + + /** + * A value that points to a non-existent file used to suppress loading the default web.xml file. + *

    + * It is useful when embedding Tomcat, when the default configuration is done programmatically, e.g. by calling + * Tomcat.initWebappDefaults(context). + * + * @see Tomcat + */ + public static final String NoDefaultWebXml = "org/apache/catalina/startup/NO_DEFAULT_XML"; + + /** + * Name of the system property containing the tomcat product installation path + */ + public static final String CATALINA_HOME_PROP = "catalina.home"; + + /** + * Name of the system property containing the tomcat instance installation path + */ + public static final String CATALINA_BASE_PROP = "catalina.base"; +} diff --git a/java/org/apache/catalina/startup/ContextConfig.java b/java/org/apache/catalina/startup/ContextConfig.java new file mode 100644 index 0000000..8b2e3de --- /dev/null +++ b/java/org/apache/catalina/startup/ContextConfig.java @@ -0,0 +1,2786 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.annotation.HandlesTypes; + +import org.apache.catalina.Authenticator; +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.Valve; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.util.Introspection; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.Jar; +import org.apache.tomcat.JarScanType; +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.bcel.classfile.AnnotationElementValue; +import org.apache.tomcat.util.bcel.classfile.AnnotationEntry; +import org.apache.tomcat.util.bcel.classfile.ArrayElementValue; +import org.apache.tomcat.util.bcel.classfile.ClassFormatException; +import org.apache.tomcat.util.bcel.classfile.ClassParser; +import org.apache.tomcat.util.bcel.classfile.ElementValue; +import org.apache.tomcat.util.bcel.classfile.ElementValuePair; +import org.apache.tomcat.util.bcel.classfile.JavaClass; +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.descriptor.InputSourceUtil; +import org.apache.tomcat.util.descriptor.XmlErrorHandler; +import org.apache.tomcat.util.descriptor.web.ContextEjb; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextLocalEjb; +import org.apache.tomcat.util.descriptor.web.ContextResource; +import org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef; +import org.apache.tomcat.util.descriptor.web.ContextService; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.FragmentJarScannerCallback; +import org.apache.tomcat.util.descriptor.web.JspPropertyGroup; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.MessageDestinationRef; +import org.apache.tomcat.util.descriptor.web.MultipartDef; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.descriptor.web.SecurityRoleRef; +import org.apache.tomcat.util.descriptor.web.ServletDef; +import org.apache.tomcat.util.descriptor.web.SessionConfig; +import org.apache.tomcat.util.descriptor.web.WebXml; +import org.apache.tomcat.util.descriptor.web.WebXmlParser; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.scan.JarFactory; +import org.xml.sax.InputSource; +import org.xml.sax.SAXParseException; + +/** + * Startup event listener for a Context that configures the properties of that Context, and the associated + * defined servlets. + * + * @author Craig R. McClanahan + */ +public class ContextConfig implements LifecycleListener { + + private static final Log log = LogFactory.getLog(ContextConfig.class); + + /** + * The string resources for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + + protected static final LoginConfig DUMMY_LOGIN_CONFIG = new LoginConfig("NONE", null, null, null); + + + /** + * The set of Authenticators that we know how to configure. The key is the name of the implemented authentication + * method, and the value is the fully qualified Java class name of the corresponding Valve. + */ + protected static final Properties authenticators; + + static { + // Load our mapping properties for the standard authenticators + Properties props = new Properties(); + try (InputStream is = ContextConfig.class.getClassLoader() + .getResourceAsStream("org/apache/catalina/startup/Authenticators.properties")) { + if (is != null) { + props.load(is); + } + } catch (IOException ioe) { + props = null; + } + authenticators = props; + } + + /** + * Deployment count. + */ + protected static long deploymentCount = 0L; + + + /** + * Cache of default web.xml fragments per Host + */ + protected static final Map hostWebXmlCache = new ConcurrentHashMap<>(); + + + /** + * Set used as the value for {@code JavaClassCacheEntry.sciSet} when there are no SCIs associated with a class. + */ + private static final Set EMPTY_SCI_SET = Collections.emptySet(); + + + // ----------------------------------------------------- Instance Variables + /** + * Custom mappings of login methods to authenticators + */ + protected Map customAuthenticators; + + + /** + * The Context we are associated with. + */ + protected volatile Context context = null; + + + /** + * The default web application's deployment descriptor location. + */ + protected String defaultWebXml = null; + + + /** + * Track any fatal errors during startup configuration processing. + */ + protected boolean ok = false; + + + /** + * Original docBase. + */ + protected String originalDocBase = null; + + + /** + * Anti-locking docBase. It is a path to a copy of the web application in the java.io.tmpdir directory. This path is + * always an absolute one. + */ + private File antiLockingDocBase = null; + + + /** + * Map of ServletContainerInitializer to classes they expressed interest in. + */ + protected final Map>> initializerClassMap = new LinkedHashMap<>(); + + /** + * Map of Types to ServletContainerInitializer that are interested in those types. + */ + protected final Map,Set> typeInitializerMap = new HashMap<>(); + + /** + * Flag that indicates if at least one {@link HandlesTypes} entry is present that represents an annotation. + */ + protected boolean handlesTypesAnnotations = false; + + /** + * Flag that indicates if at least one {@link HandlesTypes} entry is present that represents a non-annotation. + */ + protected boolean handlesTypesNonAnnotations = false; + + + // ------------------------------------------------------------- Properties + + /** + * Obtain the location of the default deployment descriptor. + * + * @return The path to the default web.xml. If not absolute, it is relative to CATALINA_BASE. + */ + public String getDefaultWebXml() { + if (defaultWebXml == null) { + defaultWebXml = Constants.DefaultWebXml; + } + return defaultWebXml; + } + + + /** + * Set the location of the default deployment descriptor. + * + * @param path The path to the default web.xml. If not absolute, it is relative to CATALINA_BASE. + */ + public void setDefaultWebXml(String path) { + this.defaultWebXml = path; + } + + + /** + * Sets custom mappings of login methods to authenticators. + * + * @param customAuthenticators Custom mappings of login methods to authenticators + */ + public void setCustomAuthenticators(Map customAuthenticators) { + this.customAuthenticators = customAuthenticators; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Process events for an associated Context. + * + * @param event The lifecycle event that has occurred + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + + // Identify the context we are associated with + try { + context = (Context) event.getLifecycle(); + } catch (ClassCastException e) { + log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e); + return; + } + + // Process the event that has occurred + if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { + configureStart(); + } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) { + beforeStart(); + } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) { + // Restore docBase for management tools + if (originalDocBase != null) { + context.setDocBase(originalDocBase); + } + } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) { + configureStop(); + } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) { + init(); + } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) { + destroy(); + } + + } + + + // -------------------------------------------------------- protected Methods + + + /** + * Process the application classes annotations, if it exists. + */ + protected void applicationAnnotationsConfig() { + + long t1 = System.currentTimeMillis(); + + WebAnnotationSet.loadApplicationAnnotations(context); + + long t2 = System.currentTimeMillis(); + if (context instanceof StandardContext) { + ((StandardContext) context).setStartupTime(t2 - t1 + ((StandardContext) context).getStartupTime()); + } + } + + + /** + * Set up an Authenticator automatically if required, and one has not already been configured. + */ + protected void authenticatorConfig() { + + LoginConfig loginConfig = context.getLoginConfig(); + if (loginConfig == null) { + // Need an authenticator to support HttpServletRequest.login() + loginConfig = DUMMY_LOGIN_CONFIG; + context.setLoginConfig(loginConfig); + } + + // Has an authenticator been configured already? + if (context.getAuthenticator() != null) { + return; + } + + // Has a Realm been configured for us to authenticate against? + if (context.getRealm() == null) { + log.error(sm.getString("contextConfig.missingRealm")); + ok = false; + return; + } + + /* + * First check to see if there is a custom mapping for the login method. If so, use it. Otherwise, check if + * there is a mapping in org/apache/catalina/startup/Authenticators.properties. + */ + Valve authenticator = null; + if (customAuthenticators != null) { + authenticator = (Valve) customAuthenticators.get(loginConfig.getAuthMethod()); + } + + if (authenticator == null) { + if (authenticators == null) { + log.error(sm.getString("contextConfig.authenticatorResources")); + ok = false; + return; + } + + // Identify the class name of the Valve we should configure + String authenticatorName = authenticators.getProperty(loginConfig.getAuthMethod()); + if (authenticatorName == null) { + log.error(sm.getString("contextConfig.authenticatorMissing", loginConfig.getAuthMethod())); + ok = false; + return; + } + + // Instantiate and install an Authenticator of the requested class + try { + Class authenticatorClass = Class.forName(authenticatorName); + authenticator = (Valve) authenticatorClass.getConstructor().newInstance(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("contextConfig.authenticatorInstantiate", authenticatorName), t); + ok = false; + } + } + + if (authenticator != null) { + Pipeline pipeline = context.getPipeline(); + if (pipeline != null) { + pipeline.addValve(authenticator); + if (log.isDebugEnabled()) { + log.debug(sm.getString("contextConfig.authenticatorConfigured", loginConfig.getAuthMethod())); + } + } + } + } + + + /** + * Create (if necessary) and return a Digester configured to process the context configuration descriptor for an + * application. + * + * @return the digester for context.xml files + */ + protected Digester createContextDigester() { + Digester digester = new Digester(); + digester.setValidating(false); + digester.setRulesValidation(true); + Map,List> fakeAttributes = new HashMap<>(); + List objectAttrs = new ArrayList<>(); + objectAttrs.add("className"); + fakeAttributes.put(Object.class, objectAttrs); + // Ignore attribute added by Eclipse for its internal tracking + List contextAttrs = new ArrayList<>(); + contextAttrs.add("source"); + fakeAttributes.put(StandardContext.class, contextAttrs); + digester.setFakeAttributes(fakeAttributes); + RuleSet contextRuleSet = new ContextRuleSet("", false); + digester.addRuleSet(contextRuleSet); + RuleSet namingRuleSet = new NamingRuleSet("Context/"); + digester.addRuleSet(namingRuleSet); + return digester; + } + + + protected boolean getGenerateCode() { + Catalina catalina = Container.getService(context).getServer().getCatalina(); + if (catalina != null) { + return catalina.getGenerateCode(); + } else { + return false; + } + } + + + protected boolean getUseGeneratedCode() { + Catalina catalina = Container.getService(context).getServer().getCatalina(); + if (catalina != null) { + return catalina.getUseGeneratedCode(); + } else { + return false; + } + } + + + protected File getGeneratedCodeLocation() { + Catalina catalina = Container.getService(context).getServer().getCatalina(); + if (catalina != null) { + return catalina.getGeneratedCodeLocation(); + } else { + // Cannot happen + return null; + } + } + + + protected String getGeneratedCodePackage() { + Catalina catalina = Container.getService(context).getServer().getCatalina(); + if (catalina != null) { + return catalina.getGeneratedCodePackage(); + } else { + return "generatedCodePackage"; + } + } + + + protected static String getContextXmlPackageName(String generatedCodePackage, Container container) { + StringBuilder result = new StringBuilder(); + Container host = null; + Container engine = null; + while (container != null) { + if (container instanceof Host) { + host = container; + } else if (container instanceof Engine) { + engine = container; + } + container = container.getParent(); + } + result.append(generatedCodePackage); + if (engine != null) { + result.append('.'); + } + if (engine != null) { + result.append(engine.getName()); + if (host != null) { + result.append('.'); + } + } + if (host != null) { + result.append(host.getName()); + } + return result.toString(); + } + + + protected File getContextXmlJavaSource(String contextXmlPackageName, String contextXmlSimpleClassName) { + File generatedSourceFolder = getGeneratedCodeLocation(); + String path = contextXmlPackageName.replace('.', File.separatorChar); + File packageFolder = new File(generatedSourceFolder, path); + if (packageFolder.isDirectory() || packageFolder.mkdirs()) { + return new File(packageFolder, contextXmlSimpleClassName + ".java"); + } + return null; + } + + + protected void generateClassHeader(Digester digester, String packageName, String resourceName) { + StringBuilder code = digester.getGeneratedCode(); + code.append("package ").append(packageName).append(';').append(System.lineSeparator()); + code.append("public class ").append(resourceName).append(" implements "); + code.append(ContextXml.class.getName().replace('$', '.')); + code.append(" {").append(System.lineSeparator()); + code.append("public void load("); + code.append(Context.class.getName()); + String contextArgument = digester.toVariableName(context); + code.append(' ').append(contextArgument).append(") {").append(System.lineSeparator()); + // Create a new variable with the concrete type + digester.setKnown(context); + code.append(context.getClass().getName()).append(' ').append(digester.toVariableName(context)); + code.append(" = (").append(context.getClass().getName()).append(") ").append(contextArgument); + code.append(';').append(System.lineSeparator()); + } + + + protected void generateClassFooter(Digester digester) { + StringBuilder code = digester.getGeneratedCode(); + code.append('}').append(System.lineSeparator()); + code.append('}').append(System.lineSeparator()); + } + + + public interface ContextXml { + void load(Context context); + } + + + /** + * Process the default configuration file, if it exists. + * + * @param digester The digester that will be used for XML parsing + */ + protected void contextConfig(Digester digester) { + + String defaultContextXml = null; + + boolean generateCode = getGenerateCode(); + boolean useGeneratedCode = getUseGeneratedCode(); + + String contextXmlPackageName = null; + String contextXmlSimpleClassName = null; + String contextXmlClassName = null; + File contextXmlJavaSource = null; + + // Open the default context.xml file, if it exists + if (context instanceof StandardContext) { + defaultContextXml = ((StandardContext) context).getDefaultContextXml(); + } + // set the default if we don't have any overrides + if (defaultContextXml == null) { + defaultContextXml = Constants.DefaultContextXml; + } + + ContextXml contextXml = null; + + if (!context.getOverride()) { + + if (useGeneratedCode || generateCode) { + contextXmlPackageName = getGeneratedCodePackage(); + contextXmlSimpleClassName = "ContextXmlDefault"; + contextXmlClassName = contextXmlPackageName + "." + contextXmlSimpleClassName; + } + if (useGeneratedCode) { + contextXml = (ContextXml) Digester.loadGeneratedClass(contextXmlClassName); + } + if (contextXml != null) { + contextXml.load(context); + contextXml = null; + } else if (!useGeneratedCode) { + try (ConfigurationSource.Resource contextXmlResource = + ConfigFileLoader.getSource().getResource(defaultContextXml)) { + if (generateCode) { + contextXmlJavaSource = + getContextXmlJavaSource(contextXmlPackageName, contextXmlSimpleClassName); + if (contextXmlJavaSource != null) { + digester.startGeneratingCode(); + generateClassHeader(digester, contextXmlPackageName, contextXmlSimpleClassName); + } else { + generateCode = false; + } + } + URL defaultContextUrl = contextXmlResource.getURI().toURL(); + processContextConfig(digester, defaultContextUrl, contextXmlResource.getInputStream()); + if (generateCode) { + generateClassFooter(digester); + try (FileWriter writer = new FileWriter(contextXmlJavaSource)) { + writer.write(digester.getGeneratedCode().toString()); + } + digester.endGeneratingCode(); + Digester.addGeneratedClass(contextXmlClassName); + } + } catch (MalformedURLException e) { + log.error(sm.getString("contextConfig.badUrl", defaultContextXml), e); + } catch (IOException e) { + // Not found + } + } + + if (useGeneratedCode || generateCode) { + contextXmlPackageName = getContextXmlPackageName(getGeneratedCodePackage(), context); + contextXmlSimpleClassName = "ContextXmlDefault"; + contextXmlClassName = contextXmlPackageName + "." + contextXmlSimpleClassName; + } + if (useGeneratedCode) { + contextXml = (ContextXml) Digester.loadGeneratedClass(contextXmlClassName); + } + if (contextXml != null) { + contextXml.load(context); + contextXml = null; + } else if (!useGeneratedCode) { + String hostContextFile = Container.getConfigPath(context, Constants.HostContextXml); + try (ConfigurationSource.Resource contextXmlResource = + ConfigFileLoader.getSource().getResource(hostContextFile)) { + if (generateCode) { + contextXmlJavaSource = + getContextXmlJavaSource(contextXmlPackageName, contextXmlSimpleClassName); + digester.startGeneratingCode(); + generateClassHeader(digester, contextXmlPackageName, contextXmlSimpleClassName); + } + URL defaultContextUrl = contextXmlResource.getURI().toURL(); + processContextConfig(digester, defaultContextUrl, contextXmlResource.getInputStream()); + if (generateCode) { + generateClassFooter(digester); + try (FileWriter writer = new FileWriter(contextXmlJavaSource)) { + writer.write(digester.getGeneratedCode().toString()); + } + digester.endGeneratingCode(); + Digester.addGeneratedClass(contextXmlClassName); + } + } catch (MalformedURLException e) { + log.error(sm.getString("contextConfig.badUrl", hostContextFile), e); + } catch (IOException e) { + // Not found + } + } + } + + if (context.getConfigFile() != null) { + if (useGeneratedCode || generateCode) { + contextXmlPackageName = getContextXmlPackageName(getGeneratedCodePackage(), context); + contextXmlSimpleClassName = "ContextXml_" + context.getName().replace('/', '_').replace("-", "__"); + contextXmlClassName = contextXmlPackageName + "." + contextXmlSimpleClassName; + } + if (useGeneratedCode) { + contextXml = (ContextXml) Digester.loadGeneratedClass(contextXmlClassName); + } + if (contextXml != null) { + contextXml.load(context); + contextXml = null; + } else if (!useGeneratedCode) { + if (generateCode) { + contextXmlJavaSource = getContextXmlJavaSource(contextXmlPackageName, contextXmlSimpleClassName); + digester.startGeneratingCode(); + generateClassHeader(digester, contextXmlPackageName, contextXmlSimpleClassName); + } + processContextConfig(digester, context.getConfigFile(), null); + if (generateCode) { + generateClassFooter(digester); + try (FileWriter writer = new FileWriter(contextXmlJavaSource)) { + writer.write(digester.getGeneratedCode().toString()); + } catch (IOException e) { + // Ignore + } + digester.endGeneratingCode(); + Digester.addGeneratedClass(contextXmlClassName); + } + } + } + + } + + + /** + * Process a context.xml. + * + * @param digester The digester that will be used for XML parsing + * @param contextXml The URL to the context.xml configuration + * @param stream The XML resource stream + */ + protected void processContextConfig(Digester digester, URL contextXml, InputStream stream) { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("contextConfig.processContext", context.getName(), contextXml)); + } + + InputSource source = null; + + try { + source = new InputSource(contextXml.toString()); + if (stream == null) { + URLConnection xmlConn = contextXml.openConnection(); + xmlConn.setUseCaches(false); + stream = xmlConn.getInputStream(); + } + } catch (Exception e) { + log.error(sm.getString("contextConfig.contextMissing", contextXml), e); + } + + if (source == null) { + return; + } + + try { + source.setByteStream(stream); + digester.setClassLoader(this.getClass().getClassLoader()); + digester.setUseContextClassLoader(false); + digester.push(context.getParent()); + digester.push(context); + XmlErrorHandler errorHandler = new XmlErrorHandler(); + digester.setErrorHandler(errorHandler); + digester.parse(source); + if (errorHandler.getWarnings().size() > 0 || errorHandler.getErrors().size() > 0) { + errorHandler.logFindings(log, contextXml.toString()); + ok = false; + } + if (log.isTraceEnabled()) { + log.trace("Successfully processed context [" + context.getName() + "] configuration file [" + + contextXml + "]"); + } + } catch (SAXParseException e) { + log.error(sm.getString("contextConfig.contextParse", context.getName()), e); + log.error(sm.getString("contextConfig.defaultPosition", "" + e.getLineNumber(), "" + e.getColumnNumber())); + ok = false; + } catch (Exception e) { + log.error(sm.getString("contextConfig.contextParse", context.getName()), e); + ok = false; + } finally { + try { + if (stream != null) { + stream.close(); + } + } catch (IOException e) { + log.error(sm.getString("contextConfig.contextClose"), e); + } + } + } + + + /** + * Adjust docBase. + * + * @throws IOException cannot access the context base path + */ + protected void fixDocBase() throws IOException { + + Host host = (Host) context.getParent(); + File appBase = host.getAppBaseFile(); + + // This could be blank, relative, absolute or canonical + String docBaseConfigured = context.getDocBase(); + // If there is no explicit docBase, derive it from the path and version + if (docBaseConfigured == null) { + // Trying to guess the docBase according to the path + String path = context.getPath(); + if (path == null) { + return; + } + ContextName cn = new ContextName(path, context.getWebappVersion()); + docBaseConfigured = cn.getBaseName(); + } + + // Obtain the absolute docBase in String and File form + String docBaseAbsolute; + File docBaseConfiguredFile = new File(docBaseConfigured); + if (!docBaseConfiguredFile.isAbsolute()) { + docBaseAbsolute = (new File(appBase, docBaseConfigured)).getAbsolutePath(); + } else { + docBaseAbsolute = docBaseConfiguredFile.getAbsolutePath(); + } + File docBaseAbsoluteFile = new File(docBaseAbsolute); + String originalDocBase = docBaseAbsolute; + + ContextName cn = new ContextName(context.getPath(), context.getWebappVersion()); + String pathName = cn.getBaseName(); + + boolean unpackWARs = true; + if (host instanceof StandardHost) { + unpackWARs = ((StandardHost) host).isUnpackWARs(); + if (unpackWARs && context instanceof StandardContext) { + unpackWARs = ((StandardContext) context).getUnpackWAR(); + } + } + + // At this point we need to determine if we have a WAR file in the + // appBase that needs to be expanded. Therefore we consider the absolute + // docBase NOT the canonical docBase. This is because some users symlink + // WAR files into the appBase and we want this to work correctly. + boolean docBaseAbsoluteInAppBase = docBaseAbsolute.startsWith(appBase.getPath() + File.separatorChar); + if (docBaseAbsolute.toLowerCase(Locale.ENGLISH).endsWith(".war") && !docBaseAbsoluteFile.isDirectory()) { + URL war = UriUtil.buildJarUrl(docBaseAbsoluteFile); + if (unpackWARs) { + docBaseAbsolute = ExpandWar.expand(host, war, pathName); + docBaseAbsoluteFile = new File(docBaseAbsolute); + if (context instanceof StandardContext) { + ((StandardContext) context).setOriginalDocBase(originalDocBase); + } + } else { + ExpandWar.validate(host, war, pathName); + } + } else { + File docBaseAbsoluteFileWar = new File(docBaseAbsolute + ".war"); + URL war = null; + if (docBaseAbsoluteFileWar.exists() && docBaseAbsoluteInAppBase) { + war = UriUtil.buildJarUrl(docBaseAbsoluteFileWar); + } + if (docBaseAbsoluteFile.exists()) { + if (war != null && unpackWARs) { + // Check if WAR needs to be re-expanded (e.g. if it has + // changed). Note: HostConfig.deployWar() takes care of + // ensuring that the correct XML file is used. + // This will be a NO-OP if the WAR is unchanged. + ExpandWar.expand(host, war, pathName); + } + } else { + if (war != null) { + if (unpackWARs) { + docBaseAbsolute = ExpandWar.expand(host, war, pathName); + docBaseAbsoluteFile = new File(docBaseAbsolute); + } else { + docBaseAbsoluteFile = docBaseAbsoluteFileWar; + ExpandWar.validate(host, war, pathName); + } + } + if (context instanceof StandardContext) { + ((StandardContext) context).setOriginalDocBase(originalDocBase); + } + } + } + + String docBaseCanonical = docBaseAbsoluteFile.getCanonicalPath(); + + // Re-calculate now docBase is a canonical path + boolean docBaseCanonicalInAppBase = + docBaseAbsoluteFile.getCanonicalFile().toPath().startsWith(appBase.toPath()); + String docBase; + if (docBaseCanonicalInAppBase) { + docBase = docBaseCanonical.substring(appBase.getPath().length()); + docBase = docBase.replace(File.separatorChar, '/'); + if (docBase.startsWith("/")) { + docBase = docBase.substring(1); + } + } else { + docBase = docBaseCanonical.replace(File.separatorChar, '/'); + } + + context.setDocBase(docBase); + } + + + protected void antiLocking() { + + if ((context instanceof StandardContext) && ((StandardContext) context).getAntiResourceLocking()) { + + Host host = (Host) context.getParent(); + String docBase = context.getDocBase(); + if (docBase == null) { + return; + } + originalDocBase = docBase; + + File docBaseFile = new File(docBase); + if (!docBaseFile.isAbsolute()) { + docBaseFile = new File(host.getAppBaseFile(), docBase); + } + + String path = context.getPath(); + if (path == null) { + return; + } + ContextName cn = new ContextName(path, context.getWebappVersion()); + docBase = cn.getBaseName(); + + String tmp = System.getProperty("java.io.tmpdir"); + File tmpFile = new File(tmp); + if (!tmpFile.isDirectory()) { + log.error(sm.getString("contextConfig.noAntiLocking", tmp, context.getName())); + return; + } + + if (originalDocBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) { + antiLockingDocBase = new File(tmpFile, deploymentCount++ + "-" + docBase + ".war"); + } else { + antiLockingDocBase = new File(tmpFile, deploymentCount++ + "-" + docBase); + } + antiLockingDocBase = antiLockingDocBase.getAbsoluteFile(); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("contextConfig.antiLocking", context.getName(), antiLockingDocBase.getPath())); + } + + // Cleanup just in case an old deployment is lying around + ExpandWar.delete(antiLockingDocBase); + if (ExpandWar.copy(docBaseFile, antiLockingDocBase)) { + context.setDocBase(antiLockingDocBase.getPath()); + } + } + } + + + /** + * Process a "init" event for this Context. + */ + protected synchronized void init() { + // Called from StandardContext.init() + + Digester contextDigester = null; + if (!getUseGeneratedCode()) { + contextDigester = createContextDigester(); + contextDigester.getParser(); + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("contextConfig.init")); + } + context.setConfigured(false); + ok = true; + + contextConfig(contextDigester); + } + + + /** + * Process a "before start" event for this Context. + */ + protected synchronized void beforeStart() { + + try { + fixDocBase(); + } catch (IOException e) { + log.error(sm.getString("contextConfig.fixDocBase", context.getName()), e); + } + + antiLocking(); + } + + + /** + * Process a "contextConfig" event for this Context. + */ + protected synchronized void configureStart() { + // Called from StandardContext.start() + + if (log.isTraceEnabled()) { + log.trace(sm.getString("contextConfig.start")); + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("contextConfig.xmlSettings", context.getName(), + Boolean.valueOf(context.getXmlValidation()), Boolean.valueOf(context.getXmlNamespaceAware()))); + } + + webConfig(); + + if (!context.getIgnoreAnnotations()) { + applicationAnnotationsConfig(); + } + if (ok) { + validateSecurityRoles(); + } + + // Configure an authenticator if we need one + if (ok) { + authenticatorConfig(); + } + + // Dump the contents of this pipeline if requested + if (log.isTraceEnabled()) { + log.trace("Pipeline Configuration:"); + Pipeline pipeline = context.getPipeline(); + Valve valves[] = null; + if (pipeline != null) { + valves = pipeline.getValves(); + } + if (valves != null) { + for (Valve valve : valves) { + log.trace(" " + valve.getClass().getName()); + } + } + log.trace("======================"); + } + + // Make our application available if no problems were encountered + if (ok) { + context.setConfigured(true); + } else { + log.error(sm.getString("contextConfig.unavailable")); + context.setConfigured(false); + } + + } + + + /** + * Process a "stop" event for this Context. + */ + protected synchronized void configureStop() { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("contextConfig.stop")); + } + + int i; + + // Removing children + Container[] children = context.findChildren(); + for (i = 0; i < children.length; i++) { + context.removeChild(children[i]); + } + + // Removing security constraints + SecurityConstraint[] securityConstraints = context.findConstraints(); + for (i = 0; i < securityConstraints.length; i++) { + context.removeConstraint(securityConstraints[i]); + } + + // Removing errors pages + ErrorPage[] errorPages = context.findErrorPages(); + for (i = 0; i < errorPages.length; i++) { + context.removeErrorPage(errorPages[i]); + } + + // Removing filter defs + FilterDef[] filterDefs = context.findFilterDefs(); + for (i = 0; i < filterDefs.length; i++) { + context.removeFilterDef(filterDefs[i]); + } + + // Removing filter maps + FilterMap[] filterMaps = context.findFilterMaps(); + for (i = 0; i < filterMaps.length; i++) { + context.removeFilterMap(filterMaps[i]); + } + + // Removing Mime mappings + String[] mimeMappings = context.findMimeMappings(); + for (i = 0; i < mimeMappings.length; i++) { + context.removeMimeMapping(mimeMappings[i]); + } + + // Removing parameters + String[] parameters = context.findParameters(); + for (i = 0; i < parameters.length; i++) { + context.removeParameter(parameters[i]); + } + + // Removing security role + String[] securityRoles = context.findSecurityRoles(); + for (i = 0; i < securityRoles.length; i++) { + context.removeSecurityRole(securityRoles[i]); + } + + // Removing servlet mappings + String[] servletMappings = context.findServletMappings(); + for (i = 0; i < servletMappings.length; i++) { + context.removeServletMapping(servletMappings[i]); + } + + // Removing welcome files + String[] welcomeFiles = context.findWelcomeFiles(); + for (i = 0; i < welcomeFiles.length; i++) { + context.removeWelcomeFile(welcomeFiles[i]); + } + + // Removing wrapper lifecycles + String[] wrapperLifecycles = context.findWrapperLifecycles(); + for (i = 0; i < wrapperLifecycles.length; i++) { + context.removeWrapperLifecycle(wrapperLifecycles[i]); + } + + // Removing wrapper listeners + String[] wrapperListeners = context.findWrapperListeners(); + for (i = 0; i < wrapperListeners.length; i++) { + context.removeWrapperListener(wrapperListeners[i]); + } + + // Remove (partially) folders and files created by antiLocking + if (antiLockingDocBase != null) { + // No need to log failure - it is expected in this case + ExpandWar.delete(antiLockingDocBase, false); + } + + // Reset ServletContextInitializer scanning + initializerClassMap.clear(); + typeInitializerMap.clear(); + + ok = true; + + } + + + /** + * Process a "destroy" event for this Context. + */ + protected synchronized void destroy() { + // Called from StandardContext.destroy() + if (log.isTraceEnabled()) { + log.trace(sm.getString("contextConfig.destroy")); + } + + // Skip clearing the work directory if Tomcat is being shutdown + Server s = getServer(); + if (s != null && !s.getState().isAvailable()) { + return; + } + + // Changed to getWorkPath per Bugzilla 35819. + if (context instanceof StandardContext) { + String workDir = ((StandardContext) context).getWorkPath(); + if (workDir != null) { + ExpandWar.delete(new File(workDir)); + } + } + } + + + private Server getServer() { + Container c = context; + while (c != null && !(c instanceof Engine)) { + c = c.getParent(); + } + + if (c == null) { + return null; + } + + Service s = ((Engine) c).getService(); + + if (s == null) { + return null; + } + + return s.getServer(); + } + + /** + * Validate the usage of security role names in the web application deployment descriptor. If any problems are + * found, issue warning messages (for backwards compatibility) and add the missing roles. (To make these problems + * fatal instead, simply set the ok instance variable to false as well). + */ + protected void validateSecurityRoles() { + + // Check role names used in elements + SecurityConstraint constraints[] = context.findConstraints(); + for (SecurityConstraint constraint : constraints) { + String roles[] = constraint.findAuthRoles(); + for (String role : roles) { + if (!"*".equals(role) && !context.findSecurityRole(role)) { + log.warn(sm.getString("contextConfig.role.auth", role)); + context.addSecurityRole(role); + } + } + } + + // Check role names used in elements + Container wrappers[] = context.findChildren(); + for (Container container : wrappers) { + Wrapper wrapper = (Wrapper) container; + String runAs = wrapper.getRunAs(); + if ((runAs != null) && !context.findSecurityRole(runAs)) { + log.warn(sm.getString("contextConfig.role.runas", runAs)); + context.addSecurityRole(runAs); + } + String names[] = wrapper.findSecurityReferences(); + for (String name : names) { + String link = wrapper.findSecurityReference(name); + if ((link != null) && !context.findSecurityRole(link)) { + log.warn(sm.getString("contextConfig.role.link", link)); + context.addSecurityRole(link); + } + } + } + + } + + + protected File getHostConfigBase() { + File file = null; + if (context.getParent() instanceof Host) { + file = ((Host) context.getParent()).getConfigBaseFile(); + } + return file; + } + + /** + * Scan the web.xml files that apply to the web application and merge them using the rules defined in the spec. For + * the global web.xml files, where there is duplicate configuration, the most specific level wins. ie an + * application's web.xml takes precedence over the host level or global web.xml file. + */ + protected void webConfig() { + /* + * Anything and everything can override the global and host defaults. This is implemented in two parts: + * + * - Handle as a web fragment that gets added after everything else so everything else takes priority + * + * - Mark Servlets as overridable so SCI configuration can replace configuration from the defaults + */ + + /* + * The rules for annotation scanning are not as clear-cut as one might think. Tomcat implements the following + * process: + * + * - As per SRV.1.6.2, Tomcat will scan for annotations regardless of which Servlet spec version is declared in + * web.xml. The EG has confirmed this is the expected behaviour. + * + * - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main web.xml is marked as metadata-complete, + * JARs are still processed for SCIs. + * + * - If metadata-complete=true and an absolute ordering is specified, JARs excluded from the ordering are also + * excluded from the SCI processing. + * + * - If an SCI has a @HandlesType annotation then all classes (except those in JARs excluded from an absolute + * ordering) need to be scanned to check if they match. + */ + WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(), context.getXmlValidation(), + context.getXmlBlockExternal()); + + Set defaults = new HashSet<>(); + defaults.add(getDefaultWebXmlFragment(webXmlParser)); + + Set tomcatWebXml = new HashSet<>(); + tomcatWebXml.add(getTomcatWebXmlFragment(webXmlParser)); + + WebXml webXml = createWebXml(); + + // Parse context level web.xml + InputSource contextWebXml = getContextWebXmlSource(); + if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) { + ok = false; + } + + ServletContext sContext = context.getServletContext(); + + // Ordering is important here + + // Step 1. Identify all the JARs packaged with the application and those + // provided by the container. If any of the application JARs have a + // web-fragment.xml it will be parsed at this point. web-fragment.xml + // files are ignored for container provided JARs. + Map fragments = processJarsForWebFragments(webXml, webXmlParser); + + // Step 2. Order the fragments. + Set orderedFragments = null; + orderedFragments = WebXml.orderWebFragments(webXml, fragments, sContext); + + // Step 3. Look for ServletContainerInitializer implementations + if (ok) { + processServletContainerInitializers(); + } + + if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) { + // Steps 4 & 5. + processClasses(webXml, orderedFragments); + } + + if (!webXml.isMetadataComplete()) { + // Step 6. Merge web-fragment.xml files into the main web.xml + // file. + if (ok) { + ok = webXml.merge(orderedFragments); + } + + // Step 7a + // merge tomcat-web.xml + webXml.merge(tomcatWebXml); + + // Step 7b. Apply global defaults + // Have to merge defaults before JSP conversion since defaults + // provide JSP servlet definition. + webXml.merge(defaults); + + // Step 8. Convert explicitly mentioned jsps to servlets + if (ok) { + convertJsps(webXml); + } + + // Step 9. Apply merged web.xml to Context + if (ok) { + configureContext(webXml); + } + } else { + webXml.merge(tomcatWebXml); + webXml.merge(defaults); + convertJsps(webXml); + configureContext(webXml); + } + + if (context.getLogEffectiveWebXml()) { + log.info(sm.getString("contextConfig.effectiveWebXml", webXml.toXml())); + } + + // Always need to look for static resources + // Step 10. Look for static resources packaged in JARs + if (ok) { + // Spec does not define an order. + // Use ordered JARs followed by remaining JARs + Set resourceJars = new LinkedHashSet<>(orderedFragments); + for (WebXml fragment : fragments.values()) { + if (!resourceJars.contains(fragment)) { + resourceJars.add(fragment); + } + } + processResourceJARs(resourceJars); + // See also StandardContext.resourcesStart() for + // WEB-INF/classes/META-INF/resources configuration + } + + // Step 11. Apply the ServletContainerInitializer config to the + // context + if (ok) { + for (Map.Entry>> entry : initializerClassMap.entrySet()) { + if (entry.getValue().isEmpty()) { + context.addServletContainerInitializer(entry.getKey(), null); + } else { + context.addServletContainerInitializer(entry.getKey(), entry.getValue()); + } + } + } + } + + + protected void processClasses(WebXml webXml, Set orderedFragments) { + // Step 4. Process /WEB-INF/classes for annotations and + // @HandlesTypes matches + + Map javaClassCache; + + if (context.getParallelAnnotationScanning()) { + javaClassCache = new ConcurrentHashMap<>(); + } else { + javaClassCache = new HashMap<>(); + } + + if (ok) { + WebResource[] webResources = context.getResources().listResources("/WEB-INF/classes"); + + for (WebResource webResource : webResources) { + // Skip the META-INF directory from any JARs that have been + // expanded in to WEB-INF/classes (sometimes IDEs do this). + if ("META-INF".equals(webResource.getName())) { + continue; + } + processAnnotationsWebResource(webResource, webXml, webXml.isMetadataComplete(), javaClassCache); + } + } + + // Step 5. Process JARs for annotations and + // @HandlesTypes matches - only need to process those fragments we + // are going to use (remember orderedFragments includes any + // container fragments) + if (ok) { + processAnnotations(orderedFragments, webXml.isMetadataComplete(), javaClassCache); + } + + // Cache, if used, is no longer required so clear it + javaClassCache.clear(); + } + + + private void configureContext(WebXml webxml) { + // As far as possible, process in alphabetical order so it is easy to + // check everything is present + // Some validation depends on correct public ID + context.setPublicId(webxml.getPublicId()); + + // Everything else in order + context.setEffectiveMajorVersion(webxml.getMajorVersion()); + context.setEffectiveMinorVersion(webxml.getMinorVersion()); + + for (Entry entry : webxml.getContextParams().entrySet()) { + context.addParameter(entry.getKey(), entry.getValue()); + } + context.setDenyUncoveredHttpMethods(webxml.getDenyUncoveredHttpMethods()); + context.setDisplayName(webxml.getDisplayName()); + context.setDistributable(webxml.isDistributable()); + for (ContextLocalEjb ejbLocalRef : webxml.getEjbLocalRefs().values()) { + context.getNamingResources().addLocalEjb(ejbLocalRef); + } + for (ContextEjb ejbRef : webxml.getEjbRefs().values()) { + context.getNamingResources().addEjb(ejbRef); + } + for (ContextEnvironment environment : webxml.getEnvEntries().values()) { + context.getNamingResources().addEnvironment(environment); + } + for (ErrorPage errorPage : webxml.getErrorPages().values()) { + context.addErrorPage(errorPage); + } + for (FilterDef filter : webxml.getFilters().values()) { + if (filter.getAsyncSupported() == null) { + filter.setAsyncSupported("false"); + } + context.addFilterDef(filter); + } + for (FilterMap filterMap : webxml.getFilterMappings()) { + context.addFilterMap(filterMap); + } + context.setJspConfigDescriptor(webxml.getJspConfigDescriptor()); + for (String listener : webxml.getListeners()) { + context.addApplicationListener(listener); + } + for (Entry entry : webxml.getLocaleEncodingMappings().entrySet()) { + context.addLocaleEncodingMappingParameter(entry.getKey(), entry.getValue()); + } + // Prevents IAE + if (webxml.getLoginConfig() != null) { + context.setLoginConfig(webxml.getLoginConfig()); + } + for (MessageDestinationRef mdr : webxml.getMessageDestinationRefs().values()) { + context.getNamingResources().addMessageDestinationRef(mdr); + } + + // messageDestinations were ignored in Tomcat 6, so ignore here + + context.setIgnoreAnnotations(webxml.isMetadataComplete()); + for (Entry entry : webxml.getMimeMappings().entrySet()) { + context.addMimeMapping(entry.getKey(), entry.getValue()); + } + context.setRequestCharacterEncoding(webxml.getRequestCharacterEncoding()); + // Name is just used for ordering + for (ContextResourceEnvRef resource : webxml.getResourceEnvRefs().values()) { + context.getNamingResources().addResourceEnvRef(resource); + } + for (ContextResource resource : webxml.getResourceRefs().values()) { + context.getNamingResources().addResource(resource); + } + context.setResponseCharacterEncoding(webxml.getResponseCharacterEncoding()); + boolean allAuthenticatedUsersIsAppRole = + webxml.getSecurityRoles().contains(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + for (SecurityConstraint constraint : webxml.getSecurityConstraints()) { + if (allAuthenticatedUsersIsAppRole) { + constraint.treatAllAuthenticatedUsersAsApplicationRole(); + } + context.addConstraint(constraint); + } + for (String role : webxml.getSecurityRoles()) { + context.addSecurityRole(role); + } + for (ContextService service : webxml.getServiceRefs().values()) { + context.getNamingResources().addService(service); + } + for (ServletDef servlet : webxml.getServlets().values()) { + Wrapper wrapper = context.createWrapper(); + // Description is ignored + // Display name is ignored + // Icons are ignored + + // jsp-file gets passed to the JSP Servlet as an init-param + + if (servlet.getLoadOnStartup() != null) { + wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); + } + if (servlet.getEnabled() != null) { + wrapper.setEnabled(servlet.getEnabled().booleanValue()); + } + wrapper.setName(servlet.getServletName()); + Map params = servlet.getParameterMap(); + for (Entry entry : params.entrySet()) { + wrapper.addInitParameter(entry.getKey(), entry.getValue()); + } + wrapper.setRunAs(servlet.getRunAs()); + Set roleRefs = servlet.getSecurityRoleRefs(); + for (SecurityRoleRef roleRef : roleRefs) { + wrapper.addSecurityReference(roleRef.getName(), roleRef.getLink()); + } + wrapper.setServletClass(servlet.getServletClass()); + MultipartDef multipartdef = servlet.getMultipartDef(); + if (multipartdef != null) { + long maxFileSize = -1; + long maxRequestSize = -1; + int fileSizeThreshold = 0; + + if (null != multipartdef.getMaxFileSize()) { + maxFileSize = Long.parseLong(multipartdef.getMaxFileSize()); + } + if (null != multipartdef.getMaxRequestSize()) { + maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize()); + } + if (null != multipartdef.getFileSizeThreshold()) { + fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold()); + } + + wrapper.setMultipartConfigElement(new MultipartConfigElement(multipartdef.getLocation(), maxFileSize, + maxRequestSize, fileSizeThreshold)); + } + if (servlet.getAsyncSupported() != null) { + wrapper.setAsyncSupported(servlet.getAsyncSupported().booleanValue()); + } + wrapper.setOverridable(servlet.isOverridable()); + context.addChild(wrapper); + } + for (Entry entry : webxml.getServletMappings().entrySet()) { + context.addServletMappingDecoded(entry.getKey(), entry.getValue()); + } + SessionConfig sessionConfig = webxml.getSessionConfig(); + if (sessionConfig != null) { + if (sessionConfig.getSessionTimeout() != null) { + context.setSessionTimeout(sessionConfig.getSessionTimeout().intValue()); + } + SessionCookieConfig scc = context.getServletContext().getSessionCookieConfig(); + scc.setName(sessionConfig.getCookieName()); + Map attributes = sessionConfig.getCookieAttributes(); + for (Map.Entry attribute : attributes.entrySet()) { + scc.setAttribute(attribute.getKey(), attribute.getValue()); + } + if (sessionConfig.getSessionTrackingModes().size() > 0) { + context.getServletContext().setSessionTrackingModes(sessionConfig.getSessionTrackingModes()); + } + } + + // Context doesn't use version directly + + for (String welcomeFile : webxml.getWelcomeFiles()) { + /* + * The following will result in a welcome file of "" so don't add that to the context + * + */ + if (welcomeFile != null && welcomeFile.length() > 0) { + context.addWelcomeFile(welcomeFile); + } + } + + // Do this last as it depends on servlets + for (JspPropertyGroup jspPropertyGroup : webxml.getJspPropertyGroups()) { + String jspServletName = context.findServletMapping("*.jsp"); + if (jspServletName == null) { + jspServletName = "jsp"; + } + if (context.findChild(jspServletName) != null) { + for (String urlPattern : jspPropertyGroup.getUrlPatterns()) { + context.addServletMappingDecoded(urlPattern, jspServletName, true); + } + } else { + if (log.isDebugEnabled()) { + for (String urlPattern : jspPropertyGroup.getUrlPatterns()) { + log.debug(sm.getString("contextConfig.noJsp", urlPattern, jspServletName)); + } + } + } + } + + for (Entry entry : webxml.getPostConstructMethods().entrySet()) { + context.addPostConstructMethod(entry.getKey(), entry.getValue()); + } + + for (Entry entry : webxml.getPreDestroyMethods().entrySet()) { + context.addPreDestroyMethod(entry.getKey(), entry.getValue()); + } + } + + + private WebXml getTomcatWebXmlFragment(WebXmlParser webXmlParser) { + + WebXml webXmlTomcatFragment = createWebXml(); + webXmlTomcatFragment.setOverridable(true); + + // Set to distributable else every app will be prevented from being + // distributable when the Tomcat fragment is merged with the main + // web.xml + webXmlTomcatFragment.setDistributable(true); + // When merging, the default welcome files are only used if the app has + // not defined any welcomes files. + webXmlTomcatFragment.setAlwaysAddWelcomeFiles(false); + + WebResource resource = context.getResources().getResource(Constants.TomcatWebXml); + if (resource.isFile()) { + try { + InputSource source = new InputSource(resource.getURL().toURI().toString()); + source.setByteStream(resource.getInputStream()); + if (!webXmlParser.parseWebXml(source, webXmlTomcatFragment, false)) { + ok = false; + } + } catch (URISyntaxException e) { + log.error(sm.getString("contextConfig.tomcatWebXmlError"), e); + } + } + return webXmlTomcatFragment; + } + + + private WebXml getDefaultWebXmlFragment(WebXmlParser webXmlParser) { + + // Host should never be null + Host host = (Host) context.getParent(); + + DefaultWebXmlCacheEntry entry = hostWebXmlCache.get(host); + + InputSource globalWebXml = getGlobalWebXmlSource(); + InputSource hostWebXml = getHostWebXmlSource(); + + long globalTimeStamp = 0; + long hostTimeStamp = 0; + + if (globalWebXml != null) { + URLConnection uc = null; + try { + URI uri = new URI(globalWebXml.getSystemId()); + URL url = uri.toURL(); + uc = url.openConnection(); + globalTimeStamp = uc.getLastModified(); + } catch (IOException | URISyntaxException | IllegalArgumentException e) { + globalTimeStamp = -1; + } finally { + if (uc != null) { + try { + uc.getInputStream().close(); + } catch (IOException e) { + ExceptionUtils.handleThrowable(e); + globalTimeStamp = -1; + } + } + } + } + + if (hostWebXml != null) { + URLConnection uc = null; + try { + URI uri = new URI(hostWebXml.getSystemId()); + URL url = uri.toURL(); + uc = url.openConnection(); + hostTimeStamp = uc.getLastModified(); + } catch (IOException | URISyntaxException | IllegalArgumentException e) { + hostTimeStamp = -1; + } finally { + if (uc != null) { + try { + uc.getInputStream().close(); + } catch (IOException e) { + ExceptionUtils.handleThrowable(e); + hostTimeStamp = -1; + } + } + } + } + + if (entry != null && entry.getGlobalTimeStamp() == globalTimeStamp && + entry.getHostTimeStamp() == hostTimeStamp) { + InputSourceUtil.close(globalWebXml); + InputSourceUtil.close(hostWebXml); + return entry.getWebXml(); + } + + // Parsing global web.xml is relatively expensive. Use a sync block to + // make sure it only happens once. Use the pipeline since a lock will + // already be held on the host by another thread + synchronized (host.getPipeline()) { + entry = hostWebXmlCache.get(host); + if (entry != null && entry.getGlobalTimeStamp() == globalTimeStamp && + entry.getHostTimeStamp() == hostTimeStamp) { + return entry.getWebXml(); + } + + WebXml webXmlDefaultFragment = createWebXml(); + webXmlDefaultFragment.setOverridable(true); + // Set to distributable else every app will be prevented from being + // distributable when the default fragment is merged with the main + // web.xml + webXmlDefaultFragment.setDistributable(true); + // When merging, the default welcome files are only used if the app has + // not defined any welcomes files. + webXmlDefaultFragment.setAlwaysAddWelcomeFiles(false); + + // Parse global web.xml if present + if (globalWebXml == null) { + // This is unusual enough to log + log.info(sm.getString("contextConfig.defaultMissing")); + } else { + if (!webXmlParser.parseWebXml(globalWebXml, webXmlDefaultFragment, false)) { + ok = false; + } + } + + // Parse host level web.xml if present + // Additive apart from welcome pages + webXmlDefaultFragment.setReplaceWelcomeFiles(true); + + if (!webXmlParser.parseWebXml(hostWebXml, webXmlDefaultFragment, false)) { + ok = false; + } + + // Don't update the cache if an error occurs + if (globalTimeStamp != -1 && hostTimeStamp != -1) { + entry = new DefaultWebXmlCacheEntry(webXmlDefaultFragment, globalTimeStamp, hostTimeStamp); + hostWebXmlCache.put(host, entry); + // Add a Lifecycle listener to the Host that will remove it from + // the hostWebXmlCache once the Host is destroyed + host.addLifecycleListener(new HostWebXmlCacheCleaner()); + } + + return webXmlDefaultFragment; + } + } + + + private void convertJsps(WebXml webXml) { + Map jspInitParams; + ServletDef jspServlet = webXml.getServlets().get("jsp"); + if (jspServlet == null) { + jspInitParams = new HashMap<>(); + Wrapper w = (Wrapper) context.findChild("jsp"); + if (w != null) { + String[] params = w.findInitParameters(); + for (String param : params) { + jspInitParams.put(param, w.findInitParameter(param)); + } + } + } else { + jspInitParams = jspServlet.getParameterMap(); + } + for (ServletDef servletDef : webXml.getServlets().values()) { + if (servletDef.getJspFile() != null) { + convertJsp(servletDef, jspInitParams); + } + } + } + + private void convertJsp(ServletDef servletDef, Map jspInitParams) { + servletDef.setServletClass(org.apache.catalina.core.Constants.JSP_SERVLET_CLASS); + String jspFile = servletDef.getJspFile(); + if ((jspFile != null) && !jspFile.startsWith("/")) { + if (context.isServlet22()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("contextConfig.jspFile.warning", jspFile)); + } + jspFile = "/" + jspFile; + } else { + throw new IllegalArgumentException(sm.getString("contextConfig.jspFile.error", jspFile)); + } + } + servletDef.getParameterMap().put("jspFile", jspFile); + servletDef.setJspFile(null); + for (Map.Entry initParam : jspInitParams.entrySet()) { + servletDef.addInitParameter(initParam.getKey(), initParam.getValue()); + } + } + + protected WebXml createWebXml() { + return new WebXml(); + } + + /** + * Scan JARs for ServletContainerInitializer implementations. + */ + protected void processServletContainerInitializers() { + + List detectedScis; + try { + WebappServiceLoader loader = new WebappServiceLoader<>(context); + detectedScis = loader.load(ServletContainerInitializer.class); + } catch (IOException e) { + log.error(sm.getString("contextConfig.servletContainerInitializerFail", context.getName()), e); + ok = false; + return; + } + + for (ServletContainerInitializer sci : detectedScis) { + initializerClassMap.put(sci, new HashSet<>()); + + HandlesTypes ht; + try { + ht = sci.getClass().getAnnotation(HandlesTypes.class); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.info(sm.getString("contextConfig.sci.debug", sci.getClass().getName()), e); + } else { + log.info(sm.getString("contextConfig.sci.info", sci.getClass().getName())); + } + continue; + } + if (ht == null) { + continue; + } + Class[] types = ht.value(); + if (types == null) { + continue; + } + + for (Class type : types) { + if (type.isAnnotation()) { + handlesTypesAnnotations = true; + } else { + handlesTypesNonAnnotations = true; + } + typeInitializerMap.computeIfAbsent(type, k -> new HashSet<>()).add(sci); + } + } + } + + + /** + * Scan JARs that contain web-fragment.xml files that will be used to configure this application to see if they also + * contain static resources. If static resources are found, add them to the context. Resources are added in + * web-fragment.xml priority order. + * + * @param fragments The set of fragments that will be scanned for static resources + */ + protected void processResourceJARs(Set fragments) { + for (WebXml fragment : fragments) { + URL url = fragment.getURL(); + try { + if ("jar".equals(url.getProtocol()) || url.toString().endsWith(".jar")) { + try (Jar jar = JarFactory.newInstance(url)) { + jar.nextEntry(); + String entryName = jar.getEntryName(); + while (entryName != null) { + if (entryName.startsWith("META-INF/resources/")) { + context.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", url, "/META-INF/resources"); + break; + } + jar.nextEntry(); + entryName = jar.getEntryName(); + } + } + } else if ("file".equals(url.getProtocol())) { + File file = new File(url.toURI()); + File resources = new File(file, "META-INF/resources/"); + if (resources.isDirectory()) { + context.getResources().createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", + resources.getAbsolutePath(), null, "/"); + } + } + } catch (IOException | URISyntaxException e) { + log.error(sm.getString("contextConfig.resourceJarFail", url, context.getName())); + } + } + } + + + /** + * Identify the default web.xml to be used and obtain an input source for it. + * + * @return an input source to the default web.xml + */ + protected InputSource getGlobalWebXmlSource() { + // Is a default web.xml specified for the Context? + if (defaultWebXml == null && context instanceof StandardContext) { + defaultWebXml = ((StandardContext) context).getDefaultWebXml(); + } + // Set the default if we don't have any overrides + if (defaultWebXml == null) { + getDefaultWebXml(); + } + + // Is it explicitly suppressed, e.g. in embedded environment? + if (Constants.NoDefaultWebXml.equals(defaultWebXml)) { + return null; + } + return getWebXmlSource(defaultWebXml, true); + } + + + /** + * Identify the host web.xml to be used and obtain an input source for it. + * + * @return an input source to the default per host web.xml + */ + protected InputSource getHostWebXmlSource() { + File hostConfigBase = getHostConfigBase(); + if (hostConfigBase == null) { + return null; + } + + return getWebXmlSource(hostConfigBase.getPath(), false); + } + + /** + * Identify the application web.xml to be used and obtain an input source for it. + * + * @return an input source to the context web.xml + */ + protected InputSource getContextWebXmlSource() { + InputStream stream = null; + InputSource source = null; + URL url = null; + + String altDDName = null; + + // Open the application web.xml file, if it exists + ServletContext servletContext = context.getServletContext(); + try { + if (servletContext != null) { + altDDName = (String) servletContext.getAttribute(Globals.ALT_DD_ATTR); + if (altDDName != null) { + try { + stream = new FileInputStream(altDDName); + url = new File(altDDName).toURI().toURL(); + } catch (FileNotFoundException e) { + log.error(sm.getString("contextConfig.altDDNotFound", altDDName)); + } catch (MalformedURLException e) { + log.error(sm.getString("contextConfig.applicationUrl")); + } + } else { + stream = servletContext.getResourceAsStream(Constants.ApplicationWebXml); + try { + url = servletContext.getResource(Constants.ApplicationWebXml); + } catch (MalformedURLException e) { + log.error(sm.getString("contextConfig.applicationUrl")); + } + } + } + if (stream == null || url == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("contextConfig.applicationMissing") + " " + context); + } + } else { + source = new InputSource(url.toExternalForm()); + source.setByteStream(stream); + } + } finally { + if (source == null && stream != null) { + try { + stream.close(); + } catch (IOException e) { + // Ignore + } + } + } + + return source; + } + + public String getConfigBasePath() { + String path = null; + if (context.getParent() instanceof Host) { + Host host = (Host) context.getParent(); + if (host.getXmlBase() != null) { + path = host.getXmlBase(); + } else { + StringBuilder xmlDir = new StringBuilder("conf"); + Container parent = host.getParent(); + if (parent instanceof Engine) { + xmlDir.append('/'); + xmlDir.append(parent.getName()); + } + xmlDir.append('/'); + xmlDir.append(host.getName()); + path = xmlDir.toString(); + } + } + return path; + } + + /** + * Utility method to create an input source from the specified XML file. + * + * @param filename Name of the file (possibly with one or more leading path segments) to read + * @param global true if processing a shared resource, false if processing a host based resource + * + * @return the input source + */ + protected InputSource getWebXmlSource(String filename, boolean global) { + ConfigurationSource.Resource webXmlResource = null; + try { + if (global) { + if (Constants.DefaultWebXml.equals(filename)) { + webXmlResource = ConfigFileLoader.getSource().getSharedWebXml(); + } else { + webXmlResource = ConfigFileLoader.getSource().getResource(filename); + } + } else { + String hostWebXml = Container.getConfigPath(context, Constants.HostWebXml); + webXmlResource = ConfigFileLoader.getSource().getResource(hostWebXml); + } + } catch (IOException e) { + // Ignore if not found + return null; + } + + InputStream stream = null; + InputSource source = null; + + try { + stream = webXmlResource.getInputStream(); + source = new InputSource(webXmlResource.getURI().toString()); + if (stream != null) { + source.setByteStream(stream); + } + } catch (Exception e) { + log.error(sm.getString("contextConfig.defaultError", filename, webXmlResource.getURI()), e); + } finally { + if (source == null && stream != null) { + try { + stream.close(); + } catch (IOException e) { + // Ignore + } + } + } + + return source; + } + + + /** + * Scan /WEB-INF/lib for JARs and for each one found add it and any /META-INF/web-fragment.xml to the resulting Map. + * web-fragment.xml files will be parsed before being added to the map. Every JAR will be added and + * null will be used if no web-fragment.xml was found. Any JARs known not contain fragments will be + * skipped. + * + * @param application The main web.xml metadata + * @param webXmlParser The parser to use to process the web.xml file + * + * @return A map of JAR name to processed web fragment (if any) + */ + protected Map processJarsForWebFragments(WebXml application, WebXmlParser webXmlParser) { + + JarScanner jarScanner = context.getJarScanner(); + boolean delegate = false; + if (context instanceof StandardContext) { + delegate = ((StandardContext) context).getDelegate(); + } + boolean parseRequired = true; + Set absoluteOrder = application.getAbsoluteOrdering(); + if (absoluteOrder != null && absoluteOrder.isEmpty() && !context.getXmlValidation()) { + // Skip parsing when there is an empty absolute ordering and + // validation is not enabled + parseRequired = false; + } + + FragmentJarScannerCallback callback = new FragmentJarScannerCallback(webXmlParser, delegate, parseRequired); + + jarScanner.scan(JarScanType.PLUGGABILITY, context.getServletContext(), callback); + + if (!callback.isOk()) { + ok = false; + } + return callback.getFragments(); + } + + protected void processAnnotations(Set fragments, boolean handlesTypesOnly, + Map javaClassCache) { + + if (context.getParallelAnnotationScanning()) { + processAnnotationsInParallel(fragments, handlesTypesOnly, javaClassCache); + } else { + for (WebXml fragment : fragments) { + scanWebXmlFragment(handlesTypesOnly, fragment, javaClassCache); + } + } + } + + private void scanWebXmlFragment(boolean handlesTypesOnly, WebXml fragment, + Map javaClassCache) { + + // Only need to scan for @HandlesTypes matches if any of the + // following are true: + // - it has already been determined only @HandlesTypes is required + // (e.g. main web.xml has metadata-complete="true" + // - this fragment is for a container JAR (Servlet 3.1 section 8.1) + // - this fragment has metadata-complete="true" + boolean htOnly = handlesTypesOnly || !fragment.getWebappJar() || fragment.isMetadataComplete(); + + WebXml annotations = new WebXml(); + // no impact on distributable + annotations.setDistributable(true); + URL url = fragment.getURL(); + processAnnotationsUrl(url, annotations, htOnly, javaClassCache); + Set set = new HashSet<>(); + set.add(annotations); + // Merge annotations into fragment - fragment takes priority + fragment.merge(set); + } + + /** + * Executable task to scan a segment for annotations. Each task does the same work as the for loop inside + * processAnnotations(); + */ + private class AnnotationScanTask implements Runnable { + private final WebXml fragment; + private final boolean handlesTypesOnly; + private Map javaClassCache; + + private AnnotationScanTask(WebXml fragment, boolean handlesTypesOnly, + Map javaClassCache) { + this.fragment = fragment; + this.handlesTypesOnly = handlesTypesOnly; + this.javaClassCache = javaClassCache; + } + + @Override + public void run() { + scanWebXmlFragment(handlesTypesOnly, fragment, javaClassCache); + } + + } + + /** + * Parallelized version of processAnnotationsInParallel(). Constructs tasks, submits them as they're created, then + * waits for completion. + * + * @param fragments Set of parallelizable scans + * @param handlesTypesOnly Important parameter for the underlying scan + * @param javaClassCache The class cache + */ + protected void processAnnotationsInParallel(Set fragments, boolean handlesTypesOnly, + Map javaClassCache) { + Server s = getServer(); + ExecutorService pool = (s == null) ? null : s.getUtilityExecutor(); + if (pool != null) { + List> futures = new ArrayList<>(fragments.size()); + for (WebXml fragment : fragments) { + Runnable task = new AnnotationScanTask(fragment, handlesTypesOnly, javaClassCache); + futures.add(pool.submit(task)); + } + try { + for (Future future : futures) { + future.get(); + } + } catch (Exception e) { + throw new RuntimeException(sm.getString("contextConfig.processAnnotationsInParallelFailure"), e); + } + } else { + // Fallback to regular processing + for (WebXml fragment : fragments) { + scanWebXmlFragment(handlesTypesOnly, fragment, javaClassCache); + } + } + } + + protected void processAnnotationsWebResource(WebResource webResource, WebXml fragment, boolean handlesTypesOnly, + Map javaClassCache) { + + if (webResource.isDirectory()) { + WebResource[] webResources = webResource.getWebResourceRoot().listResources(webResource.getWebappPath()); + if (webResources.length > 0) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("contextConfig.processAnnotationsWebDir.debug", webResource.getURL())); + } + for (WebResource r : webResources) { + processAnnotationsWebResource(r, fragment, handlesTypesOnly, javaClassCache); + } + } + } else if (webResource.isFile() && webResource.getName().endsWith(".class")) { + try (InputStream is = webResource.getInputStream()) { + processAnnotationsStream(is, fragment, handlesTypesOnly, javaClassCache); + } catch (IOException | ClassFormatException e) { + log.error(sm.getString("contextConfig.inputStreamWebResource", webResource.getWebappPath()), e); + } + } + } + + + protected void processAnnotationsUrl(URL url, WebXml fragment, boolean handlesTypesOnly, + Map javaClassCache) { + if (url == null) { + // Nothing to do. + return; + } else if ("jar".equals(url.getProtocol()) || url.toString().endsWith(".jar")) { + processAnnotationsJar(url, fragment, handlesTypesOnly, javaClassCache); + } else if ("file".equals(url.getProtocol())) { + try { + processAnnotationsFile(new File(url.toURI()), fragment, handlesTypesOnly, javaClassCache); + } catch (URISyntaxException e) { + log.error(sm.getString("contextConfig.fileUrl", url), e); + } + } else { + log.error(sm.getString("contextConfig.unknownUrlProtocol", url.getProtocol(), url)); + } + + } + + + protected void processAnnotationsJar(URL url, WebXml fragment, boolean handlesTypesOnly, + Map javaClassCache) { + + try (Jar jar = JarFactory.newInstance(url)) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("contextConfig.processAnnotationsJar.debug", url)); + } + + jar.nextEntry(); + String entryName = jar.getEntryName(); + while (entryName != null) { + if (entryName.endsWith(".class")) { + try (InputStream is = jar.getEntryInputStream()) { + processAnnotationsStream(is, fragment, handlesTypesOnly, javaClassCache); + } catch (IOException | ClassFormatException e) { + log.error(sm.getString("contextConfig.inputStreamJar", entryName, url), e); + } + } + jar.nextEntry(); + entryName = jar.getEntryName(); + } + } catch (IOException e) { + log.error(sm.getString("contextConfig.jarFile", url), e); + } + } + + + protected void processAnnotationsFile(File file, WebXml fragment, boolean handlesTypesOnly, + Map javaClassCache) { + + if (file.isDirectory()) { + // Returns null if directory is not readable + String[] dirs = file.list(); + if (dirs != null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("contextConfig.processAnnotationsDir.debug", file)); + } + for (String dir : dirs) { + processAnnotationsFile(new File(file, dir), fragment, handlesTypesOnly, javaClassCache); + } + } + } else if (file.getName().endsWith(".class") && file.canRead()) { + try (FileInputStream fis = new FileInputStream(file)) { + processAnnotationsStream(fis, fragment, handlesTypesOnly, javaClassCache); + } catch (IOException | ClassFormatException e) { + log.error(sm.getString("contextConfig.inputStreamFile", file.getAbsolutePath()), e); + } + } + } + + + protected void processAnnotationsStream(InputStream is, WebXml fragment, boolean handlesTypesOnly, + Map javaClassCache) throws ClassFormatException, IOException { + + ClassParser parser = new ClassParser(is); + JavaClass clazz = parser.parse(); + checkHandlesTypes(clazz, javaClassCache); + + if (handlesTypesOnly) { + return; + } + + processClass(fragment, clazz); + } + + + protected void processClass(WebXml fragment, JavaClass clazz) { + AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries(); + if (annotationsEntries != null) { + String className = clazz.getClassName(); + for (AnnotationEntry ae : annotationsEntries) { + String type = ae.getAnnotationType(); + if ("Ljakarta/servlet/annotation/WebServlet;".equals(type)) { + processAnnotationWebServlet(className, ae, fragment); + } else if ("Ljakarta/servlet/annotation/WebFilter;".equals(type)) { + processAnnotationWebFilter(className, ae, fragment); + } else if ("Ljakarta/servlet/annotation/WebListener;".equals(type)) { + fragment.addListener(className); + } else { + // Unknown annotation - ignore + } + } + } + } + + + /** + * For classes packaged with the web application, the class and each super class needs to be checked for a match + * with {@link HandlesTypes} or for an annotation that matches {@link HandlesTypes}. + * + * @param javaClass the class to check + * @param javaClassCache a class cache + */ + protected void checkHandlesTypes(JavaClass javaClass, Map javaClassCache) { + + // Skip this if we can + if (typeInitializerMap.size() == 0) { + return; + } + + if ((javaClass.getAccessFlags() & org.apache.tomcat.util.bcel.Const.ACC_ANNOTATION) != 0) { + // Skip annotations. + return; + } + + String className = javaClass.getClassName(); + + Class clazz = null; + if (handlesTypesNonAnnotations) { + // This *might* be match for a HandlesType. + populateJavaClassCache(className, javaClass, javaClassCache); + JavaClassCacheEntry entry = javaClassCache.get(className); + if (entry.getSciSet() == null) { + try { + populateSCIsForCacheEntry(entry, javaClassCache); + } catch (StackOverflowError soe) { + throw new IllegalStateException(sm.getString("contextConfig.annotationsStackOverflow", + context.getName(), classHierarchyToString(className, entry, javaClassCache))); + } + } + if (!entry.getSciSet().isEmpty()) { + // Need to try and load the class + clazz = Introspection.loadClass(context, className); + if (clazz == null) { + // Can't load the class so no point continuing + return; + } + + for (ServletContainerInitializer sci : entry.getSciSet()) { + Set> classes = initializerClassMap.computeIfAbsent(sci, k -> new HashSet<>()); + classes.add(clazz); + } + } + } + + if (handlesTypesAnnotations) { + AnnotationEntry[] annotationEntries = javaClass.getAllAnnotationEntries(); + if (annotationEntries != null) { + for (Map.Entry,Set> entry : typeInitializerMap.entrySet()) { + if (entry.getKey().isAnnotation()) { + String entryClassName = entry.getKey().getName(); + for (AnnotationEntry annotationEntry : annotationEntries) { + if (entryClassName.equals(getClassName(annotationEntry.getAnnotationType()))) { + if (clazz == null) { + clazz = Introspection.loadClass(context, className); + if (clazz == null) { + // Can't load the class so no point + // continuing + return; + } + } + for (ServletContainerInitializer sci : entry.getValue()) { + initializerClassMap.get(sci).add(clazz); + } + break; + } + } + } + } + } + } + } + + + private String classHierarchyToString(String className, JavaClassCacheEntry entry, + Map javaClassCache) { + JavaClassCacheEntry start = entry; + StringBuilder msg = new StringBuilder(className); + msg.append("->"); + + String parentName = entry.getSuperclassName(); + JavaClassCacheEntry parent = javaClassCache.get(parentName); + int count = 0; + + while (count < 100 && parent != null && parent != start) { + msg.append(parentName); + msg.append("->"); + + count++; + parentName = parent.getSuperclassName(); + parent = javaClassCache.get(parentName); + } + + msg.append(parentName); + + return msg.toString(); + } + + private void populateJavaClassCache(String className, JavaClass javaClass, + Map javaClassCache) { + if (javaClassCache.containsKey(className)) { + return; + } + + // Add this class to the cache + javaClassCache.put(className, new JavaClassCacheEntry(javaClass)); + + populateJavaClassCache(javaClass.getSuperclassName(), javaClassCache); + + for (String interfaceName : javaClass.getInterfaceNames()) { + populateJavaClassCache(interfaceName, javaClassCache); + } + } + + private void populateJavaClassCache(String className, Map javaClassCache) { + if (!javaClassCache.containsKey(className)) { + String name = className.replace('.', '/') + ".class"; + try (InputStream is = context.getLoader().getClassLoader().getResourceAsStream(name)) { + if (is == null) { + return; + } + ClassParser parser = new ClassParser(is); + JavaClass clazz = parser.parse(); + populateJavaClassCache(clazz.getClassName(), clazz, javaClassCache); + } catch (ClassFormatException | IOException e) { + log.debug(sm.getString("contextConfig.invalidSciHandlesTypes", className), e); + } + } + } + + private void populateSCIsForCacheEntry(JavaClassCacheEntry cacheEntry, + Map javaClassCache) { + Set result = new HashSet<>(); + + // Super class + String superClassName = cacheEntry.getSuperclassName(); + JavaClassCacheEntry superClassCacheEntry = javaClassCache.get(superClassName); + + // Avoid an infinite loop with java.lang.Object + if (cacheEntry.equals(superClassCacheEntry)) { + cacheEntry.setSciSet(EMPTY_SCI_SET); + return; + } + + // May be null of the class is not present or could not be loaded. + if (superClassCacheEntry != null) { + if (superClassCacheEntry.getSciSet() == null) { + populateSCIsForCacheEntry(superClassCacheEntry, javaClassCache); + } + result.addAll(superClassCacheEntry.getSciSet()); + } + result.addAll(getSCIsForClass(superClassName)); + + // Interfaces + for (String interfaceName : cacheEntry.getInterfaceNames()) { + JavaClassCacheEntry interfaceEntry = javaClassCache.get(interfaceName); + // A null could mean that the class not present in application or + // that there is nothing of interest. Either way, nothing to do here + // so move along + if (interfaceEntry != null) { + if (interfaceEntry.getSciSet() == null) { + populateSCIsForCacheEntry(interfaceEntry, javaClassCache); + } + result.addAll(interfaceEntry.getSciSet()); + } + result.addAll(getSCIsForClass(interfaceName)); + } + + cacheEntry.setSciSet(result.isEmpty() ? EMPTY_SCI_SET : result); + } + + private Set getSCIsForClass(String className) { + for (Map.Entry,Set> entry : typeInitializerMap.entrySet()) { + Class clazz = entry.getKey(); + if (!clazz.isAnnotation()) { + if (clazz.getName().equals(className)) { + return entry.getValue(); + } + } + } + return EMPTY_SCI_SET; + } + + private static String getClassName(String internalForm) { + if (!internalForm.startsWith("L")) { + return internalForm; + } + + // Assume starts with L, ends with ; and uses / rather than . + return internalForm.substring(1, internalForm.length() - 1).replace('/', '.'); + } + + protected void processAnnotationWebServlet(String className, AnnotationEntry ae, WebXml fragment) { + String servletName = null; + // must search for name s. Spec Servlet API 3.0 - 8.2.3.3.n.ii page 81 + List evps = ae.getElementValuePairs(); + for (ElementValuePair evp : evps) { + String name = evp.getNameString(); + if ("name".equals(name)) { + servletName = evp.getValue().stringifyValue(); + break; + } + } + if (servletName == null) { + // classname is default servletName as annotation has no name! + servletName = className; + } + ServletDef servletDef = fragment.getServlets().get(servletName); + + boolean isWebXMLservletDef; + if (servletDef == null) { + servletDef = new ServletDef(); + servletDef.setServletName(servletName); + servletDef.setServletClass(className); + isWebXMLservletDef = false; + } else { + isWebXMLservletDef = true; + } + + boolean urlPatternsSet = false; + String[] urlPatterns = null; + + // List evps = ae.getElementValuePairs(); + for (ElementValuePair evp : evps) { + String name = evp.getNameString(); + if ("value".equals(name) || "urlPatterns".equals(name)) { + if (urlPatternsSet) { + throw new IllegalArgumentException( + sm.getString("contextConfig.urlPatternValue", "WebServlet", className)); + } + urlPatternsSet = true; + urlPatterns = processAnnotationsStringArray(evp.getValue()); + } else if ("description".equals(name)) { + if (servletDef.getDescription() == null) { + servletDef.setDescription(evp.getValue().stringifyValue()); + } + } else if ("displayName".equals(name)) { + if (servletDef.getDisplayName() == null) { + servletDef.setDisplayName(evp.getValue().stringifyValue()); + } + } else if ("largeIcon".equals(name)) { + if (servletDef.getLargeIcon() == null) { + servletDef.setLargeIcon(evp.getValue().stringifyValue()); + } + } else if ("smallIcon".equals(name)) { + if (servletDef.getSmallIcon() == null) { + servletDef.setSmallIcon(evp.getValue().stringifyValue()); + } + } else if ("asyncSupported".equals(name)) { + if (servletDef.getAsyncSupported() == null) { + servletDef.setAsyncSupported(evp.getValue().stringifyValue()); + } + } else if ("loadOnStartup".equals(name)) { + if (servletDef.getLoadOnStartup() == null) { + servletDef.setLoadOnStartup(evp.getValue().stringifyValue()); + } + } else if ("initParams".equals(name)) { + Map initParams = processAnnotationWebInitParams(evp.getValue()); + if (isWebXMLservletDef) { + Map webXMLInitParams = servletDef.getParameterMap(); + for (Map.Entry entry : initParams.entrySet()) { + if (webXMLInitParams.get(entry.getKey()) == null) { + servletDef.addInitParameter(entry.getKey(), entry.getValue()); + } + } + } else { + for (Map.Entry entry : initParams.entrySet()) { + servletDef.addInitParameter(entry.getKey(), entry.getValue()); + } + } + } + } + if (!isWebXMLservletDef && urlPatterns != null) { + fragment.addServlet(servletDef); + } + if (urlPatterns != null) { + if (!fragment.getServletMappings().containsValue(servletName)) { + for (String urlPattern : urlPatterns) { + fragment.addServletMapping(urlPattern, servletName); + } + } + } + + } + + /** + * Process filter annotation and merge with existing one + * + * @param className The filter class name + * @param ae The filter annotation + * @param fragment The corresponding fragment + */ + protected void processAnnotationWebFilter(String className, AnnotationEntry ae, WebXml fragment) { + String filterName = null; + // must search for name s. Spec Servlet API 3.0 - 8.2.3.3.n.ii page 81 + List evps = ae.getElementValuePairs(); + for (ElementValuePair evp : evps) { + String name = evp.getNameString(); + if ("filterName".equals(name)) { + filterName = evp.getValue().stringifyValue(); + break; + } + } + if (filterName == null) { + // classname is default filterName as annotation has no name! + filterName = className; + } + FilterDef filterDef = fragment.getFilters().get(filterName); + FilterMap filterMap = new FilterMap(); + + boolean isWebXMLfilterDef; + if (filterDef == null) { + filterDef = new FilterDef(); + filterDef.setFilterName(filterName); + filterDef.setFilterClass(className); + isWebXMLfilterDef = false; + } else { + isWebXMLfilterDef = true; + } + + boolean urlPatternsSet = false; + boolean servletNamesSet = false; + boolean dispatchTypesSet = false; + String[] urlPatterns = null; + + for (ElementValuePair evp : evps) { + String name = evp.getNameString(); + if ("value".equals(name) || "urlPatterns".equals(name)) { + if (urlPatternsSet) { + throw new IllegalArgumentException( + sm.getString("contextConfig.urlPatternValue", "WebFilter", className)); + } + urlPatterns = processAnnotationsStringArray(evp.getValue()); + urlPatternsSet = urlPatterns.length > 0; + for (String urlPattern : urlPatterns) { + // % decoded (if required) using UTF-8 + filterMap.addURLPattern(urlPattern); + } + } else if ("servletNames".equals(name)) { + String[] servletNames = processAnnotationsStringArray(evp.getValue()); + servletNamesSet = servletNames.length > 0; + for (String servletName : servletNames) { + filterMap.addServletName(servletName); + } + } else if ("dispatcherTypes".equals(name)) { + String[] dispatcherTypes = processAnnotationsStringArray(evp.getValue()); + dispatchTypesSet = dispatcherTypes.length > 0; + for (String dispatcherType : dispatcherTypes) { + filterMap.setDispatcher(dispatcherType); + } + } else if ("description".equals(name)) { + if (filterDef.getDescription() == null) { + filterDef.setDescription(evp.getValue().stringifyValue()); + } + } else if ("displayName".equals(name)) { + if (filterDef.getDisplayName() == null) { + filterDef.setDisplayName(evp.getValue().stringifyValue()); + } + } else if ("largeIcon".equals(name)) { + if (filterDef.getLargeIcon() == null) { + filterDef.setLargeIcon(evp.getValue().stringifyValue()); + } + } else if ("smallIcon".equals(name)) { + if (filterDef.getSmallIcon() == null) { + filterDef.setSmallIcon(evp.getValue().stringifyValue()); + } + } else if ("asyncSupported".equals(name)) { + if (filterDef.getAsyncSupported() == null) { + filterDef.setAsyncSupported(evp.getValue().stringifyValue()); + } + } else if ("initParams".equals(name)) { + Map initParams = processAnnotationWebInitParams(evp.getValue()); + if (isWebXMLfilterDef) { + Map webXMLInitParams = filterDef.getParameterMap(); + for (Map.Entry entry : initParams.entrySet()) { + if (webXMLInitParams.get(entry.getKey()) == null) { + filterDef.addInitParameter(entry.getKey(), entry.getValue()); + } + } + } else { + for (Map.Entry entry : initParams.entrySet()) { + filterDef.addInitParameter(entry.getKey(), entry.getValue()); + } + } + + } + } + if (!isWebXMLfilterDef) { + fragment.addFilter(filterDef); + if (urlPatternsSet || servletNamesSet) { + filterMap.setFilterName(filterName); + fragment.addFilterMapping(filterMap); + } + } + if (urlPatternsSet || dispatchTypesSet) { + Set fmap = fragment.getFilterMappings(); + FilterMap descMap = null; + for (FilterMap map : fmap) { + if (filterName.equals(map.getFilterName())) { + descMap = map; + break; + } + } + if (descMap != null) { + String[] urlsPatterns = descMap.getURLPatterns(); + if (urlPatternsSet && (urlsPatterns == null || urlsPatterns.length == 0)) { + for (String urlPattern : filterMap.getURLPatterns()) { + // % decoded (if required) using UTF-8 + descMap.addURLPattern(urlPattern); + } + } + String[] dispatcherNames = descMap.getDispatcherNames(); + if (dispatchTypesSet && (dispatcherNames == null || dispatcherNames.length == 0)) { + for (String dis : filterMap.getDispatcherNames()) { + descMap.setDispatcher(dis); + } + } + } + } + + } + + protected String[] processAnnotationsStringArray(ElementValue ev) { + List values = new ArrayList<>(); + if (ev instanceof ArrayElementValue) { + ElementValue[] arrayValues = ((ArrayElementValue) ev).getElementValuesArray(); + for (ElementValue value : arrayValues) { + values.add(value.stringifyValue()); + } + } else { + values.add(ev.stringifyValue()); + } + return values.toArray(new String[0]); + } + + protected Map processAnnotationWebInitParams(ElementValue ev) { + Map result = new HashMap<>(); + if (ev instanceof ArrayElementValue) { + ElementValue[] arrayValues = ((ArrayElementValue) ev).getElementValuesArray(); + for (ElementValue value : arrayValues) { + if (value instanceof AnnotationElementValue) { + List evps = + ((AnnotationElementValue) value).getAnnotationEntry().getElementValuePairs(); + String initParamName = null; + String initParamValue = null; + for (ElementValuePair evp : evps) { + if ("name".equals(evp.getNameString())) { + initParamName = evp.getValue().stringifyValue(); + } else if ("value".equals(evp.getNameString())) { + initParamValue = evp.getValue().stringifyValue(); + } else { + // Ignore + } + } + result.put(initParamName, initParamValue); + } + } + } + return result; + } + + private static class DefaultWebXmlCacheEntry { + private final WebXml webXml; + private final long globalTimeStamp; + private final long hostTimeStamp; + + DefaultWebXmlCacheEntry(WebXml webXml, long globalTimeStamp, long hostTimeStamp) { + this.webXml = webXml; + this.globalTimeStamp = globalTimeStamp; + this.hostTimeStamp = hostTimeStamp; + } + + public WebXml getWebXml() { + return webXml; + } + + public long getGlobalTimeStamp() { + return globalTimeStamp; + } + + public long getHostTimeStamp() { + return hostTimeStamp; + } + } + + private static class HostWebXmlCacheCleaner implements LifecycleListener { + + @Override + public void lifecycleEvent(LifecycleEvent event) { + + if (Lifecycle.AFTER_DESTROY_EVENT.equals(event.getType())) { + Host host = (Host) event.getSource(); + hostWebXmlCache.remove(host); + } + } + } + + static class JavaClassCacheEntry { + public final String superclassName; + + public final String[] interfaceNames; + + private Set sciSet = null; + + JavaClassCacheEntry(JavaClass javaClass) { + superclassName = javaClass.getSuperclassName(); + interfaceNames = javaClass.getInterfaceNames(); + } + + public String getSuperclassName() { + return superclassName; + } + + public String[] getInterfaceNames() { + return interfaceNames; + } + + public Set getSciSet() { + return sciSet; + } + + public void setSciSet(Set sciSet) { + this.sciSet = sciSet; + } + } +} diff --git a/java/org/apache/catalina/startup/ContextRuleSet.java b/java/org/apache/catalina/startup/ContextRuleSet.java new file mode 100644 index 0000000..4009b6d --- /dev/null +++ b/java/org/apache/catalina/startup/ContextRuleSet.java @@ -0,0 +1,199 @@ +/* + * 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.catalina.startup; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; + +/** + * RuleSet for processing the contents of a Context definition element. + * + * @author Craig R. McClanahan + */ +public class ContextRuleSet implements RuleSet { + + // ----------------------------------------------------- Instance Variables + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + + /** + * Should the context be created. + */ + protected final boolean create; + + + // ------------------------------------------------------------ Constructor + + /** + * Construct an instance of this RuleSet with the default matching pattern prefix. + */ + public ContextRuleSet() { + this(""); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + */ + public ContextRuleSet(String prefix) { + this(prefix, true); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + * @param create true if the main context instance should be created + */ + public ContextRuleSet(String prefix, boolean create) { + this.prefix = prefix; + this.create = create; + } + + + // --------------------------------------------------------- Public Methods + + /** + *

    + * Add the set of Rule instances defined in this RuleSet to the specified Digester instance, + * associating them with our namespace URI (if any). This method should only be called by a Digester instance. + *

    + * + * @param digester Digester instance to which the new Rule instances should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + + if (create) { + digester.addObjectCreate(prefix + "Context", "org.apache.catalina.core.StandardContext", "className"); + digester.addSetProperties(prefix + "Context"); + } else { + digester.addSetProperties(prefix + "Context", new String[] { "path", "docBase" }); + } + + if (create) { + digester.addRule(prefix + "Context", + new LifecycleListenerRule("org.apache.catalina.startup.ContextConfig", "configClass")); + digester.addSetNext(prefix + "Context", "addChild", "org.apache.catalina.Container"); + } + + digester.addObjectCreate(prefix + "Context/Listener", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Context/Listener"); + digester.addSetNext(prefix + "Context/Listener", "addLifecycleListener", + "org.apache.catalina.LifecycleListener"); + + digester.addObjectCreate(prefix + "Context/Loader", "org.apache.catalina.loader.WebappLoader", "className"); + digester.addSetProperties(prefix + "Context/Loader"); + digester.addSetNext(prefix + "Context/Loader", "setLoader", "org.apache.catalina.Loader"); + + digester.addObjectCreate(prefix + "Context/Manager", "org.apache.catalina.session.StandardManager", + "className"); + digester.addSetProperties(prefix + "Context/Manager"); + digester.addSetNext(prefix + "Context/Manager", "setManager", "org.apache.catalina.Manager"); + + digester.addObjectCreate(prefix + "Context/Manager/Store", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Context/Manager/Store"); + digester.addSetNext(prefix + "Context/Manager/Store", "setStore", "org.apache.catalina.Store"); + + digester.addObjectCreate(prefix + "Context/Manager/SessionIdGenerator", + "org.apache.catalina.util.StandardSessionIdGenerator", "className"); + digester.addSetProperties(prefix + "Context/Manager/SessionIdGenerator"); + digester.addSetNext(prefix + "Context/Manager/SessionIdGenerator", "setSessionIdGenerator", + "org.apache.catalina.SessionIdGenerator"); + + digester.addObjectCreate(prefix + "Context/Parameter", + "org.apache.tomcat.util.descriptor.web.ApplicationParameter"); + digester.addSetProperties(prefix + "Context/Parameter"); + digester.addSetNext(prefix + "Context/Parameter", "addApplicationParameter", + "org.apache.tomcat.util.descriptor.web.ApplicationParameter"); + + digester.addRuleSet(new RealmRuleSet(prefix + "Context/")); + + digester.addObjectCreate(prefix + "Context/Resources", "org.apache.catalina.webresources.StandardRoot", + "className"); + digester.addSetProperties(prefix + "Context/Resources"); + digester.addSetNext(prefix + "Context/Resources", "setResources", "org.apache.catalina.WebResourceRoot"); + + digester.addObjectCreate(prefix + "Context/Resources/CacheStrategy", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Context/Resources/CacheStrategy"); + digester.addSetNext(prefix + "Context/Resources/CacheStrategy", "setCacheStrategy", + "org.apache.catalina.WebResourceRoot$CacheStrategy"); + + digester.addObjectCreate(prefix + "Context/Resources/PreResources", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Context/Resources/PreResources"); + digester.addSetNext(prefix + "Context/Resources/PreResources", "addPreResources", + "org.apache.catalina.WebResourceSet"); + + digester.addObjectCreate(prefix + "Context/Resources/JarResources", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Context/Resources/JarResources"); + digester.addSetNext(prefix + "Context/Resources/JarResources", "addJarResources", + "org.apache.catalina.WebResourceSet"); + + digester.addObjectCreate(prefix + "Context/Resources/PostResources", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Context/Resources/PostResources"); + digester.addSetNext(prefix + "Context/Resources/PostResources", "addPostResources", + "org.apache.catalina.WebResourceSet"); + + + digester.addObjectCreate(prefix + "Context/ResourceLink", + "org.apache.tomcat.util.descriptor.web.ContextResourceLink"); + digester.addSetProperties(prefix + "Context/ResourceLink"); + digester.addRule(prefix + "Context/ResourceLink", + new SetNextNamingRule("addResourceLink", "org.apache.tomcat.util.descriptor.web.ContextResourceLink")); + + digester.addObjectCreate(prefix + "Context/Valve", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Context/Valve"); + digester.addSetNext(prefix + "Context/Valve", "addValve", "org.apache.catalina.Valve"); + + digester.addCallMethod(prefix + "Context/WatchedResource", "addWatchedResource", 0); + + digester.addCallMethod(prefix + "Context/WrapperLifecycle", "addWrapperLifecycle", 0); + + digester.addCallMethod(prefix + "Context/WrapperListener", "addWrapperListener", 0); + + digester.addObjectCreate(prefix + "Context/JarScanner", "org.apache.tomcat.util.scan.StandardJarScanner", + "className"); + digester.addSetProperties(prefix + "Context/JarScanner"); + digester.addSetNext(prefix + "Context/JarScanner", "setJarScanner", "org.apache.tomcat.JarScanner"); + + digester.addObjectCreate(prefix + "Context/JarScanner/JarScanFilter", + "org.apache.tomcat.util.scan.StandardJarScanFilter", "className"); + digester.addSetProperties(prefix + "Context/JarScanner/JarScanFilter"); + digester.addSetNext(prefix + "Context/JarScanner/JarScanFilter", "setJarScanFilter", + "org.apache.tomcat.JarScanFilter"); + + digester.addObjectCreate(prefix + "Context/CookieProcessor", + "org.apache.tomcat.util.http.Rfc6265CookieProcessor", "className"); + digester.addSetProperties(prefix + "Context/CookieProcessor"); + digester.addSetNext(prefix + "Context/CookieProcessor", "setCookieProcessor", + "org.apache.tomcat.util.http.CookieProcessor"); + } +} diff --git a/java/org/apache/catalina/startup/CopyParentClassLoaderRule.java b/java/org/apache/catalina/startup/CopyParentClassLoaderRule.java new file mode 100644 index 0000000..c6640d4 --- /dev/null +++ b/java/org/apache/catalina/startup/CopyParentClassLoaderRule.java @@ -0,0 +1,77 @@ +/* + * 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.catalina.startup; + + +import java.lang.reflect.Method; + +import org.apache.catalina.Container; +import org.apache.tomcat.util.digester.Rule; +import org.xml.sax.Attributes; + + +/** + * Rule that copies the parentClassLoader property from the next-to-top item on the stack (which must be a + * Container) to the top item on the stack (which must also be a Container). + * + * @author Craig R. McClanahan + */ +public class CopyParentClassLoaderRule extends Rule { + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new instance of this Rule. + */ + public CopyParentClassLoaderRule() { + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Handle the beginning of an XML element. + * + * @param attributes The attributes of this element + * + * @exception Exception if a processing error occurs + */ + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace("Copying parent class loader"); + } + Container child = (Container) digester.peek(0); + Object parent = digester.peek(1); + Method method = parent.getClass().getMethod("getParentClassLoader", new Class[0]); + ClassLoader classLoader = (ClassLoader) method.invoke(parent, new Object[0]); + child.setParentClassLoader(classLoader); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(child)).append(".setParentClassLoader("); + code.append(digester.toVariableName(parent)).append(".getParentClassLoader());"); + code.append(System.lineSeparator()); + } + } + + +} diff --git a/java/org/apache/catalina/startup/CredentialHandlerRuleSet.java b/java/org/apache/catalina/startup/CredentialHandlerRuleSet.java new file mode 100644 index 0000000..4d804f0 --- /dev/null +++ b/java/org/apache/catalina/startup/CredentialHandlerRuleSet.java @@ -0,0 +1,91 @@ +/* + * 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.catalina.startup; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; + +/** + * RuleSet for processing the contents of a CredentialHandler definition element. This + * RuleSet supports CredentialHandler such as the NestedCredentialHandler that used nested + * CredentialHandlers. + */ +public class CredentialHandlerRuleSet implements RuleSet { + + + private static final int MAX_NESTED_LEVELS = + Integer.getInteger("org.apache.catalina.startup.CredentialHandlerRuleSet.MAX_NESTED_LEVELS", 3).intValue(); + + // ----------------------------------------------------- Instance Variables + + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + + // ------------------------------------------------------------ Constructor + + + /** + * Construct an instance of this RuleSet with the default matching pattern prefix. + */ + public CredentialHandlerRuleSet() { + this(""); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + */ + public CredentialHandlerRuleSet(String prefix) { + this.prefix = prefix; + } + + + // --------------------------------------------------------- Public Methods + + + /** + *

    + * Add the set of Rule instances defined in this RuleSet to the specified Digester instance, + * associating them with our namespace URI (if any). This method should only be called by a Digester instance. + *

    + * + * @param digester Digester instance to which the new Rule instances should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + StringBuilder pattern = new StringBuilder(prefix); + for (int i = 0; i < MAX_NESTED_LEVELS; i++) { + if (i > 0) { + pattern.append('/'); + } + pattern.append("CredentialHandler"); + addRuleInstances(digester, pattern.toString(), i == 0 ? "setCredentialHandler" : "addCredentialHandler"); + } + } + + private void addRuleInstances(Digester digester, String pattern, String methodName) { + digester.addObjectCreate(pattern, null /* MUST be specified in the element */, "className"); + digester.addSetProperties(pattern); + digester.addSetNext(pattern, methodName, "org.apache.catalina.CredentialHandler"); + } +} diff --git a/java/org/apache/catalina/startup/EngineConfig.java b/java/org/apache/catalina/startup/EngineConfig.java new file mode 100644 index 0000000..941c1ce --- /dev/null +++ b/java/org/apache/catalina/startup/EngineConfig.java @@ -0,0 +1,111 @@ +/* + * 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.catalina.startup; + + +import org.apache.catalina.Engine; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Startup event listener for an Engine that configures the properties of that Engine, and the associated defined + * contexts. + * + * @author Craig R. McClanahan + */ +public class EngineConfig implements LifecycleListener { + + + private static final Log log = LogFactory.getLog(EngineConfig.class); + + // ----------------------------------------------------- Instance Variables + + + /** + * The Engine we are associated with. + */ + protected Engine engine = null; + + + /** + * The string resources for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + + // --------------------------------------------------------- Public Methods + + + /** + * Process the START event for an associated Engine. + * + * @param event The lifecycle event that has occurred + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + + // Identify the engine we are associated with + try { + engine = (Engine) event.getLifecycle(); + } catch (ClassCastException e) { + log.error(sm.getString("engineConfig.cce", event.getLifecycle()), e); + return; + } + + // Process the event that has occurred + if (event.getType().equals(Lifecycle.START_EVENT)) { + start(); + } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { + stop(); + } + + } + + + // -------------------------------------------------------- Protected Methods + + + /** + * Process a "start" event for this Engine. + */ + protected void start() { + + if (engine.getLogger().isTraceEnabled()) { + engine.getLogger().trace(sm.getString("engineConfig.start")); + } + + } + + + /** + * Process a "stop" event for this Engine. + */ + protected void stop() { + + if (engine.getLogger().isTraceEnabled()) { + engine.getLogger().trace(sm.getString("engineConfig.stop")); + } + + } + + +} diff --git a/java/org/apache/catalina/startup/EngineRuleSet.java b/java/org/apache/catalina/startup/EngineRuleSet.java new file mode 100644 index 0000000..0e1bc5d --- /dev/null +++ b/java/org/apache/catalina/startup/EngineRuleSet.java @@ -0,0 +1,98 @@ +/* + * 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.catalina.startup; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; + +/** + * RuleSet for processing the contents of a Engine definition element. This RuleSet does + * NOT include any rules for nested Host elements, which should be added via instances of HostRuleSet. + * + * @author Craig R. McClanahan + */ +public class EngineRuleSet implements RuleSet { + + // ----------------------------------------------------- Instance Variables + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + + // ------------------------------------------------------------ Constructor + + /** + * Construct an instance of this RuleSet with the default matching pattern prefix. + */ + public EngineRuleSet() { + this(""); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + */ + public EngineRuleSet(String prefix) { + this.prefix = prefix; + } + + + // --------------------------------------------------------- Public Methods + + /** + *

    + * Add the set of Rule instances defined in this RuleSet to the specified Digester instance, + * associating them with our namespace URI (if any). This method should only be called by a Digester instance. + *

    + * + * @param digester Digester instance to which the new Rule instances should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + + digester.addObjectCreate(prefix + "Engine", "org.apache.catalina.core.StandardEngine", "className"); + digester.addSetProperties(prefix + "Engine"); + digester.addRule(prefix + "Engine", + new LifecycleListenerRule("org.apache.catalina.startup.EngineConfig", "engineConfigClass")); + digester.addSetNext(prefix + "Engine", "setContainer", "org.apache.catalina.Engine"); + + // Cluster configuration start + digester.addObjectCreate(prefix + "Engine/Cluster", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Engine/Cluster"); + digester.addSetNext(prefix + "Engine/Cluster", "setCluster", "org.apache.catalina.Cluster"); + // Cluster configuration end + + digester.addObjectCreate(prefix + "Engine/Listener", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Engine/Listener"); + digester.addSetNext(prefix + "Engine/Listener", "addLifecycleListener", + "org.apache.catalina.LifecycleListener"); + + + digester.addRuleSet(new RealmRuleSet(prefix + "Engine/")); + + digester.addObjectCreate(prefix + "Engine/Valve", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Engine/Valve"); + digester.addSetNext(prefix + "Engine/Valve", "addValve", "org.apache.catalina.Valve"); + } +} diff --git a/java/org/apache/catalina/startup/ExpandWar.java b/java/org/apache/catalina/startup/ExpandWar.java new file mode 100644 index 0000000..a100dfd --- /dev/null +++ b/java/org/apache/catalina/startup/ExpandWar.java @@ -0,0 +1,388 @@ +/* + * 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.catalina.startup; + +import java.io.BufferedOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipException; + +import org.apache.catalina.Host; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Expand out a WAR in a Host's appBase. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + * @author Glenn L. Nielsen + */ +public class ExpandWar { + + private static final Log log = LogFactory.getLog(ExpandWar.class); + + /** + * The string resources for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + + /** + * Expand the WAR file found at the specified URL into an unpacked directory structure. + * + * @param host Host war is being installed for + * @param war URL of the web application archive to be expanded (must start with "jar:") + * @param pathname Context path name for web application + * + * @exception IllegalArgumentException if this is not a "jar:" URL or if the WAR file is invalid + * @exception IOException if an input/output error was encountered during expansion + * + * @return The absolute path to the expanded directory for the given WAR + */ + public static String expand(Host host, URL war, String pathname) throws IOException { + + /* + * Obtaining the last modified time opens an InputStream and there is no explicit close method. We have to + * obtain and then close the InputStream to avoid a file leak and the associated locked file. + */ + JarURLConnection juc = (JarURLConnection) war.openConnection(); + juc.setUseCaches(false); + URL jarFileUrl = juc.getJarFileURL(); + URLConnection jfuc = jarFileUrl.openConnection(); + + boolean success = false; + File docBase = new File(host.getAppBaseFile(), pathname); + File warTracker = new File(host.getAppBaseFile(), pathname + Constants.WarTracker); + long warLastModified = -1; + + try (InputStream is = jfuc.getInputStream()) { + // Get the last modified time for the WAR + warLastModified = jfuc.getLastModified(); + } + + // Check to see of the WAR has been expanded previously + if (docBase.exists()) { + // A WAR was expanded. Tomcat will have set the last modified + // time of warTracker file to the last modified time of the WAR so + // changes to the WAR while Tomcat is stopped can be detected + if (!warTracker.exists() || warTracker.lastModified() == warLastModified) { + // No (detectable) changes to the WAR + success = true; + return docBase.getAbsolutePath(); + } + + // WAR must have been modified. Remove expanded directory. + log.info(sm.getString("expandWar.deleteOld", docBase)); + if (!delete(docBase)) { + throw new IOException(sm.getString("expandWar.deleteFailed", docBase)); + } + } + + // Create the new document base directory + if (!docBase.mkdir() && !docBase.isDirectory()) { + throw new IOException(sm.getString("expandWar.createFailed", docBase)); + } + + // Expand the WAR into the new document base directory + Path canonicalDocBasePath = docBase.getCanonicalFile().toPath(); + + // Creating war tracker parent (normally META-INF) + File warTrackerParent = warTracker.getParentFile(); + if (!warTrackerParent.isDirectory() && !warTrackerParent.mkdirs()) { + throw new IOException(sm.getString("expandWar.createFailed", warTrackerParent.getAbsolutePath())); + } + + try (JarFile jarFile = juc.getJarFile()) { + + Enumeration jarEntries = jarFile.entries(); + while (jarEntries.hasMoreElements()) { + JarEntry jarEntry = jarEntries.nextElement(); + String name = jarEntry.getName(); + File expandedFile = new File(docBase, name); + if (!expandedFile.getCanonicalFile().toPath().startsWith(canonicalDocBasePath)) { + // Trying to expand outside the docBase + // Throw an exception to stop the deployment + throw new IllegalArgumentException(sm.getString("expandWar.illegalPath", war, name, + expandedFile.getCanonicalPath(), canonicalDocBasePath)); + } + int last = name.lastIndexOf('/'); + if (last >= 0) { + File parent = new File(docBase, name.substring(0, last)); + if (!parent.mkdirs() && !parent.isDirectory()) { + throw new IOException(sm.getString("expandWar.createFailed", parent)); + } + } + if (name.endsWith("/")) { + continue; + } + + try (InputStream input = jarFile.getInputStream(jarEntry)) { + if (null == input) { + throw new ZipException(sm.getString("expandWar.missingJarEntry", jarEntry.getName())); + } + + // Bugzilla 33636 + expand(input, expandedFile); + long lastModified = jarEntry.getTime(); + if ((lastModified != -1) && (lastModified != 0)) { + if (!expandedFile.setLastModified(lastModified)) { + throw new IOException(sm.getString("expandWar.lastModifiedFailed", expandedFile)); + } + } + } + } + + // Create the warTracker file and align the last modified time + // with the last modified time of the WAR + if (!warTracker.createNewFile()) { + throw new IOException(sm.getString("expandWar.createFileFailed", warTracker)); + } + if (!warTracker.setLastModified(warLastModified)) { + throw new IOException(sm.getString("expandWar.lastModifiedFailed", warTracker)); + } + + success = true; + } catch (IOException e) { + throw e; + } finally { + if (!success) { + // If something went wrong, delete expanded dir to keep things + // clean + deleteDir(docBase); + } + } + + // Return the absolute path to our new document base directory + return docBase.getAbsolutePath(); + } + + + /** + * Validate the WAR file found at the specified URL. + * + * @param host Host war is being installed for + * @param war URL of the web application archive to be validated (must start with "jar:") + * @param pathname Context path name for web application + * + * @exception IllegalArgumentException if this is not a "jar:" URL or if the WAR file is invalid + * @exception IOException if an input/output error was encountered during validation + */ + public static void validate(Host host, URL war, String pathname) throws IOException { + + File docBase = new File(host.getAppBaseFile(), pathname); + + // Calculate the document base directory + Path canonicalDocBasePath = docBase.getCanonicalFile().toPath(); + JarURLConnection juc = (JarURLConnection) war.openConnection(); + juc.setUseCaches(false); + try (JarFile jarFile = juc.getJarFile()) { + Enumeration jarEntries = jarFile.entries(); + while (jarEntries.hasMoreElements()) { + JarEntry jarEntry = jarEntries.nextElement(); + String name = jarEntry.getName(); + File expandedFile = new File(docBase, name); + if (!expandedFile.getCanonicalFile().toPath().startsWith(canonicalDocBasePath)) { + // Entry located outside the docBase + // Throw an exception to stop the deployment + throw new IllegalArgumentException(sm.getString("expandWar.illegalPath", war, name, + expandedFile.getCanonicalPath(), canonicalDocBasePath)); + } + } + } catch (IOException e) { + throw e; + } + } + + + /** + * Copy the specified file or directory to the destination. + * + * @param src File object representing the source + * @param dest File object representing the destination + * + * @return true if the copy was successful + */ + public static boolean copy(File src, File dest) { + + boolean result = true; + + String files[] = null; + if (src.isDirectory()) { + files = src.list(); + result = dest.mkdir(); + } else { + files = new String[1]; + files[0] = ""; + } + if (files == null) { + files = new String[0]; + } + for (int i = 0; (i < files.length) && result; i++) { + File fileSrc = new File(src, files[i]); + File fileDest = new File(dest, files[i]); + if (fileSrc.isDirectory()) { + result = copy(fileSrc, fileDest); + } else { + try (FileChannel ic = (new FileInputStream(fileSrc)).getChannel(); + FileChannel oc = (new FileOutputStream(fileDest)).getChannel()) { + long size = ic.size(); + long position = 0; + while (size > 0) { + long count = ic.transferTo(position, size, oc); + if (count > 0) { + position += count; + size -= count; + } else { + throw new EOFException(); + } + } + } catch (IOException e) { + log.error(sm.getString("expandWar.copy", fileSrc, fileDest), e); + result = false; + } + } + } + return result; + } + + + /** + * Delete the specified directory, including all of its contents and sub-directories recursively. Any failure will + * be logged. + * + * @param dir File object representing the directory to be deleted + * + * @return true if the deletion was successful + */ + public static boolean delete(File dir) { + // Log failure by default + return delete(dir, true); + } + + + /** + * Delete the specified directory, including all of its contents and sub-directories recursively. + * + * @param dir File object representing the directory to be deleted + * @param logFailure true if failure to delete the resource should be logged + * + * @return true if the deletion was successful + */ + public static boolean delete(File dir, boolean logFailure) { + boolean result; + if (dir.isDirectory()) { + result = deleteDir(dir, logFailure); + } else { + if (dir.exists()) { + result = dir.delete(); + } else { + result = true; + } + } + if (logFailure && !result) { + log.error(sm.getString("expandWar.deleteFailed", dir.getAbsolutePath())); + } + return result; + } + + + /** + * Delete the specified directory, including all of its contents and sub-directories recursively. Any failure will + * be logged. + * + * @param dir File object representing the directory to be deleted + * + * @return true if the deletion was successful + */ + public static boolean deleteDir(File dir) { + return deleteDir(dir, true); + } + + + /** + * Delete the specified directory, including all of its contents and sub-directories recursively. + * + * @param dir File object representing the directory to be deleted + * @param logFailure true if failure to delete the resource should be logged + * + * @return true if the deletion was successful + */ + public static boolean deleteDir(File dir, boolean logFailure) { + + String files[] = dir.list(); + if (files == null) { + files = new String[0]; + } + for (String s : files) { + File file = new File(dir, s); + if (file.isDirectory()) { + deleteDir(file, logFailure); + } else { + file.delete(); + } + } + + boolean result; + if (dir.exists()) { + result = dir.delete(); + } else { + result = true; + } + + if (logFailure && !result) { + log.error(sm.getString("expandWar.deleteFailed", dir.getAbsolutePath())); + } + + return result; + } + + + /** + * Expand the specified input stream into the specified file. + * + * @param input InputStream to be copied + * @param file The file to be created + * + * @exception IOException if an input/output error occurs + */ + private static void expand(InputStream input, File file) throws IOException { + try (BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(file))) { + byte buffer[] = new byte[2048]; + while (true) { + int n = input.read(buffer); + if (n <= 0) { + break; + } + output.write(buffer, 0, n); + } + } + } +} diff --git a/java/org/apache/catalina/startup/FailedContext.java b/java/org/apache/catalina/startup/FailedContext.java new file mode 100644 index 0000000..6ca2063 --- /dev/null +++ b/java/org/apache/catalina/startup/FailedContext.java @@ -0,0 +1,1437 @@ +/* + * 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.catalina.startup; + +import java.beans.PropertyChangeListener; +import java.io.File; +import java.net.URL; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.descriptor.JspConfigDescriptor; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Authenticator; +import org.apache.catalina.Cluster; +import org.apache.catalina.Container; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Loader; +import org.apache.catalina.Manager; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Realm; +import org.apache.catalina.ThreadBindingListener; +import org.apache.catalina.Valve; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.juli.logging.Log; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.util.descriptor.web.ApplicationParameter; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.http.CookieProcessor; +import org.apache.tomcat.util.res.StringManager; + +/** + * An implementation of {@link Context} that is used as a place-holder for deployed applications when their deployment + * fails and a Context instance (usually {@link org.apache.catalina.core.StandardContext} but may be any Context + * implementation) cannot be created. + */ +public class FailedContext extends LifecycleMBeanBase implements Context { + + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + + // --------------------- Methods that need to work even for a failed context + + private URL configFile; + + @Override + public URL getConfigFile() { + return configFile; + } + + @Override + public void setConfigFile(URL configFile) { + this.configFile = configFile; + } + + + private String docBase; + + @Override + public String getDocBase() { + return docBase; + } + + @Override + public void setDocBase(String docBase) { + this.docBase = docBase; + } + + + private String name = null; + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + + private Container parent; + + @Override + public Container getParent() { + return parent; + } + + @Override + public void setParent(Container parent) { + this.parent = parent; + } + + + private String path = null; + + @Override + public String getPath() { + return path; + } + + @Override + public void setPath(String path) { + this.path = path; + } + + + private String webappVersion = null; + + @Override + public String getWebappVersion() { + return webappVersion; + } + + @Override + public void setWebappVersion(String webappVersion) { + this.webappVersion = webappVersion; + } + + + @Override + protected String getDomainInternal() { + Container p = getParent(); + if (p == null) { + return null; + } else { + return p.getDomain(); + } + } + + + @Override + public String getMBeanKeyProperties() { + Container c = this; + StringBuilder keyProperties = new StringBuilder(); + int containerCount = 0; + + // Work up container hierarchy, add a component to the name for + // each container + while (!(c instanceof Engine)) { + if (c instanceof Context) { + keyProperties.append(",context="); + ContextName cn = new ContextName(c.getName(), false); + keyProperties.append(cn.getDisplayName()); + } else if (c instanceof Host) { + keyProperties.append(",host="); + keyProperties.append(c.getName()); + } else if (c == null) { + // May happen in unit testing and/or some embedding scenarios + keyProperties.append(",container"); + keyProperties.append(containerCount++); + keyProperties.append("=null"); + break; + } else { + // Should never happen... + keyProperties.append(",container"); + keyProperties.append(containerCount++); + keyProperties.append('='); + keyProperties.append(c.getName()); + } + c = c.getParent(); + } + return keyProperties.toString(); + } + + + @Override + protected String getObjectNameKeyProperties() { + + StringBuilder keyProperties = new StringBuilder("j2eeType=WebModule,name=//"); + + String hostname = getParent().getName(); + if (hostname == null) { + keyProperties.append("DEFAULT"); + } else { + keyProperties.append(hostname); + } + + String contextName = getName(); + if (!contextName.startsWith("/")) { + keyProperties.append('/'); + } + keyProperties.append(contextName); + + keyProperties.append(",J2EEApplication=none,J2EEServer=none"); + + return keyProperties.toString(); + } + + + @Override + protected void startInternal() throws LifecycleException { + throw new LifecycleException(sm.getString("failedContext.start", getName())); + } + + + @Override + protected void stopInternal() throws LifecycleException { + // NO-OP + // Allow stop to complete since it is used for clean-up + } + + + // Only need to read these + @Override + public void addWatchedResource(String name) { + // NO-OP + } + + @Override + public String[] findWatchedResources() { + return new String[0]; + } + + @Override + public void removeWatchedResource(String name) { + // NO-OP + } + + + @Override + public void addChild(Container child) { + // NO-OP + } + + @Override + public Container findChild(String name) { + return null; + } + + @Override + public Container[] findChildren() { + return new Container[0]; + } + + @Override + public void removeChild(Container child) { + // NO-OP + } + + @Override + public String toString() { + return getName(); + } + + // -------------------------------------------- All NO-OPs beyond this point + @Override + public Loader getLoader() { + return null; + } + + @Override + public void setLoader(Loader loader) { + // NO-OP + } + + @Override + public Log getLogger() { + return null; + } + + @Override + public String getLogName() { + return null; + } + + @Override + public Manager getManager() { + return null; + } + + @Override + public void setManager(Manager manager) { + // NO-OP + } + + @Override + public Pipeline getPipeline() { + return null; + } + + @Override + public Cluster getCluster() { + return null; + } + + @Override + public void setCluster(Cluster cluster) { + // NO-OP + } + + @Override + public int getBackgroundProcessorDelay() { + return -1; + } + + @Override + public void setBackgroundProcessorDelay(int delay) { + // NO-OP + } + + @Override + public ClassLoader getParentClassLoader() { + return null; + } + + @Override + public void setParentClassLoader(ClassLoader parent) { + // NO-OP + } + + @Override + public Realm getRealm() { + return null; + } + + @Override + public void setRealm(Realm realm) { + // NO-OP + } + + @Override + public WebResourceRoot getResources() { + return null; + } + + @Override + public void setResources(WebResourceRoot resources) { + // NO-OP + } + + @Override + public void backgroundProcess() { + // NO-OP + } + + @Override + public void addContainerListener(ContainerListener listener) { + // NO-OP + } + + @Override + public ContainerListener[] findContainerListeners() { + return null; + } + + @Override + public void removeContainerListener(ContainerListener listener) { + // NO-OP + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + // NO-OP + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + // NO-OP + } + + @Override + public void fireContainerEvent(String type, Object data) { + // NO-OP + } + + @Override + public void logAccess(Request request, Response response, long time, boolean useDefault) { + // NO-OP + } + + @Override + public AccessLog getAccessLog() { + return null; + } + + @Override + public int getStartStopThreads() { + return 0; + } + + @Override + public void setStartStopThreads(int startStopThreads) { + // NO-OP + } + + @Override + public boolean getAllowCasualMultipartParsing() { + return false; + } + + @Override + public void setAllowCasualMultipartParsing(boolean allowCasualMultipartParsing) { + // NO-OP + } + + @Override + public Object[] getApplicationEventListeners() { + return null; + } + + @Override + public void setApplicationEventListeners(Object[] listeners) { + // NO-OP + } + + @Override + public Object[] getApplicationLifecycleListeners() { + return null; + } + + @Override + public void setApplicationLifecycleListeners(Object[] listeners) { + // NO-OP + } + + @Override + public String getCharset(Locale locale) { + return null; + } + + @Override + public boolean getConfigured() { + return false; + } + + @Override + public void setConfigured(boolean configured) { + // NO-OP + } + + @Override + public boolean getCookies() { + return false; + } + + @Override + public void setCookies(boolean cookies) { + // NO-OP + } + + @Override + public String getSessionCookieName() { + return null; + } + + @Override + public void setSessionCookieName(String sessionCookieName) { + // NO-OP + } + + @Override + public boolean getUseHttpOnly() { + return false; + } + + @Override + public void setUseHttpOnly(boolean useHttpOnly) { + // NO-OP + } + + @Override + public boolean getUsePartitioned() { + return false; + } + + @Override + public void setUsePartitioned(boolean usePartitioned) { + // NO-OP + } + + @Override + public String getSessionCookieDomain() { + return null; + } + + @Override + public void setSessionCookieDomain(String sessionCookieDomain) { + // NO-OP + } + + @Override + public String getSessionCookiePath() { + return null; + } + + @Override + public void setSessionCookiePath(String sessionCookiePath) { + // NO-OP + } + + @Override + public boolean getSessionCookiePathUsesTrailingSlash() { + return false; + } + + @Override + public void setSessionCookiePathUsesTrailingSlash(boolean sessionCookiePathUsesTrailingSlash) { + // NO-OP + } + + @Override + public boolean getCrossContext() { + return false; + } + + @Override + public void setCrossContext(boolean crossContext) { + // NO-OP + } + + @Override + public String getAltDDName() { + return null; + } + + @Override + public void setAltDDName(String altDDName) { + // NO-OP + } + + @Override + public boolean getDenyUncoveredHttpMethods() { + return false; + } + + @Override + public void setDenyUncoveredHttpMethods(boolean denyUncoveredHttpMethods) { + // NO-OP + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public void setDisplayName(String displayName) { + // NO-OP + } + + @Override + public boolean getDistributable() { + return false; + } + + @Override + public void setDistributable(boolean distributable) { + // NO-OP + } + + @Override + public String getEncodedPath() { + return null; + } + + @Override + public boolean getIgnoreAnnotations() { + return false; + } + + @Override + public void setIgnoreAnnotations(boolean ignoreAnnotations) { + // NO-OP + } + + @Override + public LoginConfig getLoginConfig() { + return null; + } + + @Override + public void setLoginConfig(LoginConfig config) { + // NO-OP + } + + @Override + public NamingResourcesImpl getNamingResources() { + return null; + } + + @Override + public void setNamingResources(NamingResourcesImpl namingResources) { + // NO-OP + } + + @Override + public String getPublicId() { + return null; + } + + @Override + public void setPublicId(String publicId) { + // NO-OP + } + + @Override + public boolean getReloadable() { + return false; + } + + @Override + public void setReloadable(boolean reloadable) { + // NO-OP + } + + @Override + public boolean getOverride() { + return false; + } + + @Override + public void setOverride(boolean override) { + // NO-OP + } + + @Override + public boolean getPrivileged() { + return false; + } + + @Override + public void setPrivileged(boolean privileged) { + // NO-OP + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public int getSessionTimeout() { + return 0; + } + + @Override + public void setSessionTimeout(int timeout) { + // NO-OP + } + + @Override + public boolean getSwallowAbortedUploads() { + return false; + } + + @Override + public void setSwallowAbortedUploads(boolean swallowAbortedUploads) { + // NO-OP + } + + @Override + public boolean getSwallowOutput() { + return false; + } + + @Override + public void setSwallowOutput(boolean swallowOutput) { + // NO-OP + } + + @Override + public String getWrapperClass() { + return null; + } + + @Override + public void setWrapperClass(String wrapperClass) { + // NO-OP + } + + @Override + public boolean getXmlNamespaceAware() { + return false; + } + + @Override + public void setXmlNamespaceAware(boolean xmlNamespaceAware) { + // NO-OP + } + + @Override + public boolean getXmlValidation() { + return false; + } + + @Override + public void setXmlValidation(boolean xmlValidation) { + // NO-OP + } + + @Override + public boolean getXmlBlockExternal() { + return true; + } + + @Override + public void setXmlBlockExternal(boolean xmlBlockExternal) { + // NO-OP + } + + @Override + public boolean getTldValidation() { + return false; + } + + @Override + public void setTldValidation(boolean tldValidation) { + // NO-OP + } + + @Override + public JarScanner getJarScanner() { + return null; + } + + @Override + public void setJarScanner(JarScanner jarScanner) { + // NO-OP + } + + @Override + public Authenticator getAuthenticator() { + return null; + } + + @Override + public void setLogEffectiveWebXml(boolean logEffectiveWebXml) { + // NO-OP + } + + @Override + public boolean getLogEffectiveWebXml() { + return false; + } + + @Override + public void addApplicationListener(String listener) { + // NO-OP + } + + @Override + public String[] findApplicationListeners() { + return null; + } + + @Override + public void removeApplicationListener(String listener) { + // NO-OP + } + + @Override + public void addApplicationParameter(ApplicationParameter parameter) { + // NO-OP + } + + @Override + public ApplicationParameter[] findApplicationParameters() { + return null; + } + + @Override + public void removeApplicationParameter(String name) { + // NO-OP + } + + @Override + public void addConstraint(SecurityConstraint constraint) { + // NO-OP + } + + @Override + public SecurityConstraint[] findConstraints() { + return null; + } + + @Override + public void removeConstraint(SecurityConstraint constraint) { + // NO-OP + } + + @Override + public void addErrorPage(ErrorPage errorPage) { + // NO-OP + } + + @Override + public ErrorPage findErrorPage(int errorCode) { + return null; + } + + @Override + public ErrorPage findErrorPage(Throwable throwable) { + return null; + } + + @Override + public ErrorPage[] findErrorPages() { + return null; + } + + @Override + public void removeErrorPage(ErrorPage errorPage) { + // NO-OP + } + + @Override + public void addFilterDef(FilterDef filterDef) { + // NO-OP + } + + @Override + public FilterDef findFilterDef(String filterName) { + return null; + } + + @Override + public FilterDef[] findFilterDefs() { + return null; + } + + @Override + public void removeFilterDef(FilterDef filterDef) { + // NO-OP + } + + @Override + public void addFilterMap(FilterMap filterMap) { + // NO-OP + } + + @Override + public void addFilterMapBefore(FilterMap filterMap) { + // NO-OP + } + + @Override + public FilterMap[] findFilterMaps() { + return null; + } + + @Override + public void removeFilterMap(FilterMap filterMap) { + // NO-OP + } + + @Override + public void addLocaleEncodingMappingParameter(String locale, String encoding) { + // NO-OP + } + + @Override + public void addMimeMapping(String extension, String mimeType) { + // NO-OP + } + + @Override + public String findMimeMapping(String extension) { + return null; + } + + @Override + public String[] findMimeMappings() { + return null; + } + + @Override + public void removeMimeMapping(String extension) { + // NO-OP + } + + @Override + public void addParameter(String name, String value) { + // NO-OP + } + + @Override + public String findParameter(String name) { + return null; + } + + @Override + public String[] findParameters() { + return null; + } + + @Override + public void removeParameter(String name) { + // NO-OP + } + + @Override + public void addRoleMapping(String role, String link) { + // NO-OP + } + + @Override + public String findRoleMapping(String role) { + return null; + } + + @Override + public void removeRoleMapping(String role) { + // NO-OP + } + + @Override + public void addSecurityRole(String role) { + // NO-OP + } + + @Override + public boolean findSecurityRole(String role) { + return false; + } + + @Override + public String[] findSecurityRoles() { + return null; + } + + @Override + public void removeSecurityRole(String role) { + // NO-OP + } + + @Override + public void addServletMappingDecoded(String pattern, String name, boolean jspWildcard) { + // NO-OP + } + + @Override + public String findServletMapping(String pattern) { + return null; + } + + @Override + public String[] findServletMappings() { + return null; + } + + @Override + public void removeServletMapping(String pattern) { + // NO-OP + } + + @Override + public void addWelcomeFile(String name) { + // NO-OP + } + + @Override + public boolean findWelcomeFile(String name) { + return false; + } + + @Override + public String[] findWelcomeFiles() { + return null; + } + + @Override + public void removeWelcomeFile(String name) { + // NO-OP + } + + @Override + public void addWrapperLifecycle(String listener) { + // NO-OP + } + + @Override + public String[] findWrapperLifecycles() { + return null; + } + + @Override + public void removeWrapperLifecycle(String listener) { + // NO-OP + } + + @Override + public void addWrapperListener(String listener) { + // NO-OP + } + + @Override + public String[] findWrapperListeners() { + return null; + } + + @Override + public void removeWrapperListener(String listener) { + // NO-OP + } + + @Override + public InstanceManager createInstanceManager() { + return null; + } + + @Override + public Wrapper createWrapper() { + return null; + } + + @Override + public boolean fireRequestInitEvent(ServletRequest request) { + return false; + } + + @Override + public boolean fireRequestDestroyEvent(ServletRequest request) { + return false; + } + + @Override + public void reload() { + // NO-OP + } + + @Override + public String getRealPath(String path) { + return null; + } + + @Override + public int getEffectiveMajorVersion() { + return 0; + } + + @Override + public void setEffectiveMajorVersion(int major) { + // NO-OP + } + + @Override + public int getEffectiveMinorVersion() { + return 0; + } + + @Override + public void setEffectiveMinorVersion(int minor) { + // NO-OP + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return null; + } + + @Override + public void setJspConfigDescriptor(JspConfigDescriptor descriptor) { + // NO-OP + } + + @Override + public void addServletContainerInitializer(ServletContainerInitializer sci, Set> classes) { + // NO-OP + } + + @Override + public boolean getPaused() { + return false; + } + + @Override + public boolean isServlet22() { + return false; + } + + @Override + public Set addServletSecurity(ServletRegistration.Dynamic registration, + ServletSecurityElement servletSecurityElement) { + return null; + } + + @Override + public void setResourceOnlyServlets(String resourceOnlyServlets) { + // NO-OP + } + + @Override + public String getResourceOnlyServlets() { + return null; + } + + @Override + public boolean isResourceOnlyServlet(String servletName) { + return false; + } + + @Override + public String getBaseName() { + return null; + } + + @Override + public void setFireRequestListenersOnForwards(boolean enable) { + // NO-OP + } + + @Override + public boolean getFireRequestListenersOnForwards() { + return false; + } + + @Override + public void setPreemptiveAuthentication(boolean enable) { + // NO-OP + } + + @Override + public boolean getPreemptiveAuthentication() { + return false; + } + + @Override + public void setSendRedirectBody(boolean enable) { + // NO-OP + } + + @Override + public boolean getSendRedirectBody() { + return false; + } + + @SuppressWarnings("unused") + public synchronized void addValve(Valve valve) { + // NO-OP + } + + @Override + public File getCatalinaBase() { + return null; + } + + @Override + public File getCatalinaHome() { + return null; + } + + @Override + public void setAddWebinfClassesResources(boolean addWebinfClassesResources) { + // NO-OP + } + + @Override + public boolean getAddWebinfClassesResources() { + return false; + } + + @Override + public void addPostConstructMethod(String clazz, String method) { + // NO-OP + } + + @Override + public void addPreDestroyMethod(String clazz, String method) { + // NO-OP + } + + @Override + public void removePostConstructMethod(String clazz) { + // NO-OP + } + + @Override + public void removePreDestroyMethod(String clazz) { + // NO-OP + } + + @Override + public String findPostConstructMethod(String clazz) { + return null; + } + + @Override + public String findPreDestroyMethod(String clazz) { + return null; + } + + @Override + public Map findPostConstructMethods() { + return null; + } + + @Override + public Map findPreDestroyMethods() { + return null; + } + + @Override + public InstanceManager getInstanceManager() { + return null; + } + + @Override + public void setInstanceManager(InstanceManager instanceManager) { + // NO-OP + } + + @Override + public void setContainerSciFilter(String containerSciFilter) { + // NO-OP + } + + @Override + public String getContainerSciFilter() { + return null; + } + + @Override + public ThreadBindingListener getThreadBindingListener() { + return null; + } + + @Override + public void setThreadBindingListener(ThreadBindingListener threadBindingListener) { + // NO-OP + } + + @Override + public ClassLoader bind(boolean usePrivilegedAction, ClassLoader originalClassLoader) { + return null; + } + + @Override + public void unbind(boolean usePrivilegedAction, ClassLoader originalClassLoader) { + // NO-OP + } + + @Override + public Object getNamingToken() { + return null; + } + + @Override + public void setCookieProcessor(CookieProcessor cookieProcessor) { + // NO-OP + } + + @Override + public CookieProcessor getCookieProcessor() { + return null; + } + + @Override + public void setValidateClientProvidedNewSessionId(boolean validateClientProvidedNewSessionId) { + // NO-OP + } + + @Override + public boolean getValidateClientProvidedNewSessionId() { + return false; + } + + @Override + public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) { + // NO-OP + } + + @Override + public boolean getMapperContextRootRedirectEnabled() { + return false; + } + + @Override + public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) { + // NO-OP + } + + @Override + public boolean getMapperDirectoryRedirectEnabled() { + return false; + } + + @Override + public void setUseRelativeRedirects(boolean useRelativeRedirects) { + // NO-OP + } + + @Override + public boolean getUseRelativeRedirects() { + return true; + } + + @Override + public void setDispatchersUseEncodedPaths(boolean dispatchersUseEncodedPaths) { + // NO-OP + } + + @Override + public boolean getDispatchersUseEncodedPaths() { + return true; + } + + @Override + public void setRequestCharacterEncoding(String encoding) { + // NO-OP + } + + @Override + public String getRequestCharacterEncoding() { + return null; + } + + @Override + public void setResponseCharacterEncoding(String encoding) { + // NO-OP + } + + @Override + public String getResponseCharacterEncoding() { + return null; + } + + @Override + public void setAllowMultipleLeadingForwardSlashInPath(boolean allowMultipleLeadingForwardSlashInPath) { + // NO-OP + } + + @Override + public boolean getAllowMultipleLeadingForwardSlashInPath() { + return false; + } + + @Override + public void incrementInProgressAsyncCount() { + // NO-OP + } + + @Override + public void decrementInProgressAsyncCount() { + // NO-OP + } + + @Override + public void setCreateUploadTargets(boolean createUploadTargets) { + /* NO-OP */} + + @Override + public boolean getCreateUploadTargets() { + return false; + } + + @Override + public boolean getAlwaysAccessSession() { + return false; + } + + @Override + public void setAlwaysAccessSession(boolean alwaysAccessSession) { + } + + @Override + public boolean getContextGetResourceRequiresSlash() { + return false; + } + + @Override + public void setContextGetResourceRequiresSlash(boolean contextGetResourceRequiresSlash) { + } + + @Override + public boolean getDispatcherWrapsSameObject() { + return false; + } + + @Override + public void setDispatcherWrapsSameObject(boolean dispatcherWrapsSameObject) { + } + + @Override + public boolean getParallelAnnotationScanning() { + return false; + } + + @Override + public void setParallelAnnotationScanning(boolean parallelAnnotationScanning) { + } + + @Override + public boolean getUseBloomFilterForArchives() { + return false; + } + + @Override + public void setUseBloomFilterForArchives(boolean useBloomFilterForArchives) { + } + + @Override + public boolean getSuspendWrappedResponseAfterForward() { + return false; + } + + @Override + public void setSuspendWrappedResponseAfterForward(boolean suspendWrappedResponseAfterForward) { + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/startup/HomesUserDatabase.java b/java/org/apache/catalina/startup/HomesUserDatabase.java new file mode 100644 index 0000000..fd8caf7 --- /dev/null +++ b/java/org/apache/catalina/startup/HomesUserDatabase.java @@ -0,0 +1,108 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * Concrete implementation of the UserDatabase interface considers all directories in a directory whose + * pathname is specified to our constructor to be "home" directories for those users. + * + * @author Craig R. McClanahan + */ +public final class HomesUserDatabase implements UserDatabase { + + /** + * The set of home directories for all defined users, keyed by username. + */ + private final Map homes = new HashMap<>(); + + /** + * The UserConfig listener with which we are associated. + */ + private UserConfig userConfig = null; + + + /** + * Return the UserConfig listener with which we are associated. + */ + @Override + public UserConfig getUserConfig() { + return this.userConfig; + } + + + /** + * Set the UserConfig listener with which we are associated. + * + * @param userConfig The new UserConfig listener + */ + @Override + public void setUserConfig(UserConfig userConfig) { + this.userConfig = userConfig; + init(); + } + + + /** + * Return an absolute pathname to the home directory for the specified user. + * + * @param user User for which a home directory should be retrieved + */ + @Override + public String getHome(String user) { + return homes.get(user); + } + + + /** + * Return an enumeration of the user names defined on this server. + */ + @Override + public Enumeration getUsers() { + return Collections.enumeration(homes.keySet()); + } + + + /** + * Initialize our set of users and home directories. + */ + private void init() { + + String homeBase = userConfig.getHomeBase(); + File homeBaseDir = new File(homeBase); + if (!homeBaseDir.exists() || !homeBaseDir.isDirectory()) { + return; + } + String homeBaseFiles[] = homeBaseDir.list(); + if (homeBaseFiles == null) { + return; + } + + for (String homeBaseFile : homeBaseFiles) { + File homeDir = new File(homeBaseDir, homeBaseFile); + if (!homeDir.isDirectory() || !homeDir.canRead()) { + continue; + } + homes.put(homeBaseFile, homeDir.toString()); + } + } +} diff --git a/java/org/apache/catalina/startup/HostConfig.java b/java/org/apache/catalina/startup/HostConfig.java new file mode 100644 index 0000000..8377e57 --- /dev/null +++ b/java/org/apache/catalina/startup/HostConfig.java @@ -0,0 +1,2009 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.security.CodeSource; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Policy; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.management.ObjectName; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.DistributedManager; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Manager; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.security.DeployXmlPermission; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.util.IOTools; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jakartaee.Migration; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + * Startup event listener for a Host that configures the properties of that Host, and the associated defined + * contexts. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class HostConfig implements LifecycleListener { + + private static final Log log = LogFactory.getLog(HostConfig.class); + + /** + * The string resources for this package. + */ + protected static final StringManager sm = StringManager.getManager(HostConfig.class); + + /** + * The resolution, in milliseconds, of file modification times. + */ + protected static final long FILE_MODIFICATION_RESOLUTION_MS = 1000; + + + // ----------------------------------------------------- Instance Variables + + /** + * The Java class name of the Context implementation we should use. + */ + protected String contextClass = "org.apache.catalina.core.StandardContext"; + + + /** + * The Host we are associated with. + */ + protected Host host = null; + + + /** + * The JMX ObjectName of this component. + */ + protected ObjectName oname = null; + + + /** + * Should we deploy XML Context config files packaged with WAR files and directories? + */ + protected boolean deployXML = false; + + + /** + * Should XML files be copied to $CATALINA_BASE/conf/<engine>/<host> by default when a web application + * is deployed? + */ + protected boolean copyXML = false; + + + /** + * Should we unpack WAR files when auto-deploying applications in the appBase directory? + */ + protected boolean unpackWARs = false; + + + /** + * Map of deployed applications. + */ + protected final Map deployed = new ConcurrentHashMap<>(); + + + /** + * Set of applications which are being serviced, and shouldn't be deployed/undeployed/redeployed at the moment. + */ + private Set servicedSet = ConcurrentHashMap.newKeySet(); + + /** + * The Digester instance used to parse context descriptors. + */ + protected Digester digester = createDigester(contextClass); + private final Object digesterLock = new Object(); + + /** + * The list of Wars in the appBase to be ignored because they are invalid (e.g. contain /../ sequences). + */ + protected final Set invalidWars = new HashSet<>(); + + // ------------------------------------------------------------- Properties + + + /** + * @return the Context implementation class name. + */ + public String getContextClass() { + return this.contextClass; + } + + + /** + * Set the Context implementation class name. + * + * @param contextClass The new Context implementation class name. + */ + public void setContextClass(String contextClass) { + + String oldContextClass = this.contextClass; + this.contextClass = contextClass; + + if (!oldContextClass.equals(contextClass)) { + synchronized (digesterLock) { + digester = createDigester(getContextClass()); + } + } + } + + + /** + * @return the deploy XML config file flag for this component. + */ + public boolean isDeployXML() { + return this.deployXML; + } + + + /** + * Set the deploy XML config file flag for this component. + * + * @param deployXML The new deploy XML flag + */ + public void setDeployXML(boolean deployXML) { + this.deployXML = deployXML; + } + + + private boolean isDeployThisXML(File docBase, ContextName cn) { + boolean deployThisXML = isDeployXML(); + if (Globals.IS_SECURITY_ENABLED && !deployThisXML) { + // When running under a SecurityManager, deployXML may be overridden + // on a per Context basis by the granting of a specific permission + Policy currentPolicy = Policy.getPolicy(); + if (currentPolicy != null) { + URL contextRootUrl; + try { + contextRootUrl = docBase.toURI().toURL(); + CodeSource cs = new CodeSource(contextRootUrl, (Certificate[]) null); + PermissionCollection pc = currentPolicy.getPermissions(cs); + Permission p = new DeployXmlPermission(cn.getBaseName()); + if (pc.implies(p)) { + deployThisXML = true; + } + } catch (MalformedURLException e) { + // Should never happen + log.warn(sm.getString("hostConfig.docBaseUrlInvalid"), e); + } + } + } + + return deployThisXML; + } + + + /** + * @return the copy XML config file flag for this component. + */ + public boolean isCopyXML() { + return this.copyXML; + } + + + /** + * Set the copy XML config file flag for this component. + * + * @param copyXML The new copy XML flag + */ + public void setCopyXML(boolean copyXML) { + + this.copyXML = copyXML; + + } + + + /** + * @return the unpack WARs flag. + */ + public boolean isUnpackWARs() { + return this.unpackWARs; + } + + + /** + * Set the unpack WARs flag. + * + * @param unpackWARs The new unpack WARs flag + */ + public void setUnpackWARs(boolean unpackWARs) { + this.unpackWARs = unpackWARs; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Process the START event for an associated Host. + * + * @param event The lifecycle event that has occurred + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + + // Identify the host we are associated with + try { + host = (Host) event.getLifecycle(); + if (host instanceof StandardHost) { + setCopyXML(((StandardHost) host).isCopyXML()); + setDeployXML(((StandardHost) host).isDeployXML()); + setUnpackWARs(((StandardHost) host).isUnpackWARs()); + setContextClass(((StandardHost) host).getContextClass()); + } + } catch (ClassCastException e) { + log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e); + return; + } + + // Process the event that has occurred + if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { + check(); + } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) { + beforeStart(); + } else if (event.getType().equals(Lifecycle.START_EVENT)) { + start(); + } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { + stop(); + } + } + + + /** + * Add a serviced application to the list and indicates if the application was already present in the list. + * + * @param name the context name + * + * @return {@code true} if the application was not already in the list + */ + public boolean tryAddServiced(String name) { + if (servicedSet.add(name)) { + return true; + } + return false; + } + + + /** + * Removed a serviced application from the list. + * + * @param name the context name + */ + public void removeServiced(String name) { + servicedSet.remove(name); + } + + + /** + * Get the instant where an application was deployed. + * + * @param name the context name + * + * @return 0L if no application with that name is deployed, or the instant on which the application was deployed + */ + public long getDeploymentTime(String name) { + synchronized (host) { + DeployedApplication app = deployed.get(name); + if (app == null) { + return 0L; + } + + return app.timestamp; + } + } + + + /** + * Has the specified application been deployed? Note applications defined in server.xml will not have been deployed. + * + * @param name the context name + * + * @return true if the application has been deployed and false if the application has not + * been deployed or does not exist + */ + public boolean isDeployed(String name) { + return deployed.containsKey(name); + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Create the digester which will be used to parse context config files. + * + * @param contextClassName The class which will be used to create the context instance + * + * @return the digester + */ + protected static Digester createDigester(String contextClassName) { + Digester digester = new Digester(); + digester.setValidating(false); + // Add object creation rule + digester.addObjectCreate("Context", contextClassName, "className"); + // Set the properties on that object (it doesn't matter if extra + // properties are set) + digester.addSetProperties("Context"); + return digester; + } + + protected File returnCanonicalPath(String path) { + File file = new File(path); + if (!file.isAbsolute()) { + file = new File(host.getCatalinaBase(), path); + } + try { + return file.getCanonicalFile(); + } catch (IOException e) { + return file; + } + } + + + /** + * Get the name of the configBase. For use with JMX management. + * + * @return the config base + */ + public String getConfigBaseName() { + return host.getConfigBaseFile().getAbsolutePath(); + } + + + /** + * Deploy applications for any directories or WAR files that are found in our "application root" directory. + */ + protected void deployApps() { + // Migrate legacy Java EE apps from legacyAppBase + migrateLegacyApps(); + File appBase = host.getAppBaseFile(); + File configBase = host.getConfigBaseFile(); + String[] filteredAppPaths = filterAppPaths(appBase.list()); + // Deploy XML descriptors from configBase + deployDescriptors(configBase, configBase.list()); + // Deploy WARs + deployWARs(appBase, filteredAppPaths); + // Deploy expanded folders + deployDirectories(appBase, filteredAppPaths); + } + + + /** + * Filter the list of application file paths to remove those that match the regular expression defined by + * {@link Host#getDeployIgnore()}. + * + * @param unfilteredAppPaths The list of application paths to filter + * + * @return The filtered list of application paths + */ + protected String[] filterAppPaths(String[] unfilteredAppPaths) { + Pattern filter = host.getDeployIgnorePattern(); + if (filter == null || unfilteredAppPaths == null) { + return unfilteredAppPaths; + } + + List filteredList = new ArrayList<>(); + Matcher matcher = null; + for (String appPath : unfilteredAppPaths) { + if (matcher == null) { + matcher = filter.matcher(appPath); + } else { + matcher.reset(appPath); + } + if (matcher.matches()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("hostConfig.ignorePath", appPath)); + } + } else { + filteredList.add(appPath); + } + } + return filteredList.toArray(new String[0]); + } + + + /** + * Deploy applications for any directories or WAR files that are found in our "application root" directory. + *

    + * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. + * + * @param name The context name which should be deployed + */ + protected void deployApps(String name) { + + File appBase = host.getAppBaseFile(); + File configBase = host.getConfigBaseFile(); + ContextName cn = new ContextName(name, false); + String baseName = cn.getBaseName(); + + if (deploymentExists(cn.getName())) { + return; + } + + // Deploy XML descriptor from configBase + File xml = new File(configBase, baseName + ".xml"); + if (xml.exists()) { + deployDescriptor(cn, xml); + return; + } + // Deploy WAR + File war = new File(appBase, baseName + ".war"); + if (war.exists()) { + deployWAR(cn, war); + return; + } + // Deploy expanded folder + File dir = new File(appBase, baseName); + if (dir.exists()) { + deployDirectory(cn, dir); + } + } + + + /** + * Deploy XML context descriptors. + * + * @param configBase The config base + * @param files The XML descriptors which should be deployed + */ + protected void deployDescriptors(File configBase, String[] files) { + + if (files == null) { + return; + } + + ExecutorService es = host.getStartStopExecutor(); + List> results = new ArrayList<>(); + + for (String file : files) { + File contextXml = new File(configBase, file); + + if (file.toLowerCase(Locale.ENGLISH).endsWith(".xml")) { + ContextName cn = new ContextName(file, true); + + if (tryAddServiced(cn.getName())) { + try { + if (deploymentExists(cn.getName())) { + removeServiced(cn.getName()); + continue; + } + + // DeployDescriptor will call removeServiced + results.add(es.submit(new DeployDescriptor(this, cn, contextXml))); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + removeServiced(cn.getName()); + throw t; + } + } + } + } + + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString("hostConfig.deployDescriptor.threaded.error"), e); + } + } + } + + + /** + * Deploy specified context descriptor. + *

    + * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. + * + * @param cn The context name + * @param contextXml The descriptor + */ + @SuppressWarnings("null") // context is not null + protected void deployDescriptor(ContextName cn, File contextXml) { + + DeployedApplication deployedApp = new DeployedApplication(cn.getName(), true); + + long startTime = 0; + // Assume this is a configuration descriptor and deploy it + if (log.isInfoEnabled()) { + startTime = System.currentTimeMillis(); + log.info(sm.getString("hostConfig.deployDescriptor", contextXml.getAbsolutePath())); + } + + Context context = null; + boolean isExternalWar = false; + boolean isExternal = false; + File expandedDocBase = null; + + try { + synchronized (digesterLock) { + try (FileInputStream fis = new FileInputStream(contextXml)) { + context = (Context) digester.parse(fis); + } catch (Exception e) { + log.error(sm.getString("hostConfig.deployDescriptor.error", contextXml.getAbsolutePath()), e); + } finally { + digester.reset(); + if (context == null) { + context = new FailedContext(); + } + } + } + + if (context.getPath() != null) { + log.warn(sm.getString("hostConfig.deployDescriptor.path", context.getPath(), + contextXml.getAbsolutePath())); + } + + Class clazz = Class.forName(host.getConfigClass()); + LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); + context.addLifecycleListener(listener); + + context.setConfigFile(contextXml.toURI().toURL()); + context.setName(cn.getName()); + context.setPath(cn.getPath()); + context.setWebappVersion(cn.getVersion()); + // Add the associated docBase to the redeployed list if it's a WAR + if (context.getDocBase() != null) { + File docBase = new File(context.getDocBase()); + if (!docBase.isAbsolute()) { + docBase = new File(host.getAppBaseFile(), context.getDocBase()); + } + // If external docBase, register .xml as redeploy first + if (!docBase.getCanonicalFile().toPath().startsWith(host.getAppBaseFile().toPath())) { + isExternal = true; + deployedApp.redeployResources.put(contextXml.getAbsolutePath(), + Long.valueOf(contextXml.lastModified())); + deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); + if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) { + isExternalWar = true; + } + // Check that a WAR or DIR in the appBase is not 'hidden' + File war = new File(host.getAppBaseFile(), cn.getBaseName() + ".war"); + if (war.exists()) { + log.warn(sm.getString("hostConfig.deployDescriptor.hiddenWar", contextXml.getAbsolutePath(), + war.getAbsolutePath())); + } + File dir = new File(host.getAppBaseFile(), cn.getBaseName()); + if (dir.exists()) { + log.warn(sm.getString("hostConfig.deployDescriptor.hiddenDir", contextXml.getAbsolutePath(), + dir.getAbsolutePath())); + } + } else { + log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified", docBase)); + // Ignore specified docBase + context.setDocBase(null); + } + } + + host.addChild(context); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("hostConfig.deployDescriptor.error", contextXml.getAbsolutePath()), t); + } finally { + // Get paths for WAR and expanded WAR in appBase + + // default to appBase dir + name + expandedDocBase = new File(host.getAppBaseFile(), cn.getBaseName()); + if (context.getDocBase() != null && !context.getDocBase().toLowerCase(Locale.ENGLISH).endsWith(".war")) { + // first assume docBase is absolute + expandedDocBase = new File(context.getDocBase()); + if (!expandedDocBase.isAbsolute()) { + // if docBase specified and relative, it must be relative to appBase + expandedDocBase = new File(host.getAppBaseFile(), context.getDocBase()); + } + } + + boolean unpackWAR = unpackWARs; + if (unpackWAR && context instanceof StandardContext) { + unpackWAR = ((StandardContext) context).getUnpackWAR(); + } + + // Add the eventual unpacked WAR and all the resources which will be + // watched inside it + if (isExternalWar) { + if (unpackWAR) { + deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(), + Long.valueOf(expandedDocBase.lastModified())); + addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context); + } else { + addWatchedResources(deployedApp, null, context); + } + } else { + // Find an existing matching war and expanded folder + if (!isExternal) { + File warDocBase = new File(expandedDocBase.getAbsolutePath() + ".war"); + if (warDocBase.exists()) { + deployedApp.redeployResources.put(warDocBase.getAbsolutePath(), + Long.valueOf(warDocBase.lastModified())); + } else { + // Trigger a redeploy if a WAR is added + deployedApp.redeployResources.put(warDocBase.getAbsolutePath(), Long.valueOf(0)); + } + } + if (unpackWAR) { + deployedApp.redeployResources.put(expandedDocBase.getAbsolutePath(), + Long.valueOf(expandedDocBase.lastModified())); + addWatchedResources(deployedApp, expandedDocBase.getAbsolutePath(), context); + } else { + addWatchedResources(deployedApp, null, context); + } + if (!isExternal) { + // For external docBases, the context.xml will have been + // added above. + deployedApp.redeployResources.put(contextXml.getAbsolutePath(), + Long.valueOf(contextXml.lastModified())); + } + } + // Add the global redeploy resources (which are never deleted) at + // the end so they don't interfere with the deletion process + addGlobalRedeployResources(deployedApp); + } + + if (host.findChild(context.getName()) != null) { + deployed.put(context.getName(), deployedApp); + } + + if (log.isInfoEnabled()) { + log.info(sm.getString("hostConfig.deployDescriptor.finished", contextXml.getAbsolutePath(), + Long.valueOf(System.currentTimeMillis() - startTime))); + } + } + + + /** + * Deploy WAR files. + * + * @param appBase The base path for applications + * @param files The WARs to deploy + */ + protected void deployWARs(File appBase, String[] files) { + + if (files == null) { + return; + } + + ExecutorService es = host.getStartStopExecutor(); + List> results = new ArrayList<>(); + + for (String file : files) { + if (file.equalsIgnoreCase("META-INF")) { + continue; + } + if (file.equalsIgnoreCase("WEB-INF")) { + continue; + } + + File war = new File(appBase, file); + if (file.toLowerCase(Locale.ENGLISH).endsWith(".war") && war.isFile() && !invalidWars.contains(file)) { + ContextName cn = new ContextName(file, true); + if (tryAddServiced(cn.getName())) { + try { + if (deploymentExists(cn.getName())) { + DeployedApplication app = deployed.get(cn.getName()); + boolean unpackWAR = unpackWARs; + if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) { + unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR(); + } + if (!unpackWAR && app != null) { + // Need to check for a directory that should not be + // there + File dir = new File(appBase, cn.getBaseName()); + if (dir.exists()) { + if (!app.loggedDirWarning) { + log.warn(sm.getString("hostConfig.deployWar.hiddenDir", dir.getAbsoluteFile(), + war.getAbsoluteFile())); + app.loggedDirWarning = true; + } + } else { + app.loggedDirWarning = false; + } + } + removeServiced(cn.getName()); + continue; + } + + // Check for WARs with /../ /./ or similar sequences in the name + if (!validateContextPath(appBase, cn.getBaseName())) { + log.error(sm.getString("hostConfig.illegalWarName", file)); + invalidWars.add(file); + removeServiced(cn.getName()); + continue; + } + + // DeployWAR will call removeServiced + results.add(es.submit(new DeployWar(this, cn, war))); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + removeServiced(cn.getName()); + throw t; + } + } + } + } + + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString("hostConfig.deployWar.threaded.error"), e); + } + } + } + + + private boolean validateContextPath(File appBase, String contextPath) { + // More complicated than the ideal as the canonical path may or may + // not end with File.separator for a directory + + StringBuilder docBase; + String canonicalDocBase = null; + + try { + String canonicalAppBase = appBase.getCanonicalPath(); + docBase = new StringBuilder(canonicalAppBase); + if (canonicalAppBase.endsWith(File.separator)) { + docBase.append(contextPath.substring(1).replace('/', File.separatorChar)); + } else { + docBase.append(contextPath.replace('/', File.separatorChar)); + } + // At this point docBase should be canonical but will not end + // with File.separator + + canonicalDocBase = (new File(docBase.toString())).getCanonicalPath(); + + // If the canonicalDocBase ends with File.separator, add one to + // docBase before they are compared + if (canonicalDocBase.endsWith(File.separator)) { + docBase.append(File.separator); + } + } catch (IOException ioe) { + return false; + } + + // Compare the two. If they are not the same, the contextPath must + // have /../ like sequences in it + return canonicalDocBase.equals(docBase.toString()); + } + + /** + * Deploy packed WAR. + *

    + * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. + * + * @param cn The context name + * @param war The WAR file + */ + protected void deployWAR(ContextName cn, File war) { + + File xml = new File(host.getAppBaseFile(), cn.getBaseName() + "/" + Constants.ApplicationContextXml); + + File warTracker = new File(host.getAppBaseFile(), cn.getBaseName() + Constants.WarTracker); + + boolean xmlInWar = false; + try (JarFile jar = new JarFile(war)) { + JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); + if (entry != null) { + xmlInWar = true; + } + } catch (IOException e) { + /* Ignore */ + } + + // If there is an expanded directory then any xml in that directory + // should only be used if the directory is not out of date and + // unpackWARs is true. Note the code below may apply further limits + boolean useXml = false; + // If the xml file exists then expandedDir must exists so no need to + // test that here + if (xml.exists() && unpackWARs && (!warTracker.exists() || warTracker.lastModified() == war.lastModified())) { + useXml = true; + } + + Context context = null; + boolean deployThisXML = isDeployThisXML(war, cn); + + try { + if (deployThisXML && useXml && !copyXML) { + synchronized (digesterLock) { + try { + context = (Context) digester.parse(xml); + } catch (Exception e) { + log.error(sm.getString("hostConfig.deployDescriptor.error", war.getAbsolutePath()), e); + } finally { + digester.reset(); + if (context == null) { + context = new FailedContext(); + } + } + } + context.setConfigFile(xml.toURI().toURL()); + } else if (deployThisXML && xmlInWar) { + synchronized (digesterLock) { + try (JarFile jar = new JarFile(war)) { + JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); + try (InputStream istream = jar.getInputStream(entry)) { + context = (Context) digester.parse(istream); + } + } catch (Exception e) { + log.error(sm.getString("hostConfig.deployDescriptor.error", war.getAbsolutePath()), e); + } finally { + digester.reset(); + if (context == null) { + context = new FailedContext(); + } + context.setConfigFile(UriUtil.buildJarUrl(war, Constants.ApplicationContextXml)); + } + } + } else if (!deployThisXML && xmlInWar) { + // Block deployment as META-INF/context.xml may contain security + // configuration necessary for a secure deployment. + log.error(sm.getString("hostConfig.deployDescriptor.blocked", cn.getPath(), + Constants.ApplicationContextXml, + new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml"))); + } else { + context = (Context) Class.forName(contextClass).getConstructor().newInstance(); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t); + } finally { + if (context == null) { + context = new FailedContext(); + } + } + + boolean copyThisXml = false; + if (deployThisXML) { + if (host instanceof StandardHost) { + copyThisXml = ((StandardHost) host).isCopyXML(); + } + + // If Host is using default value Context can override it. + if (!copyThisXml && context instanceof StandardContext) { + copyThisXml = ((StandardContext) context).getCopyXML(); + } + + if (xmlInWar && copyThisXml) { + // Change location of XML file to config base + xml = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml"); + try (JarFile jar = new JarFile(war)) { + JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); + try (InputStream istream = jar.getInputStream(entry); + OutputStream ostream = new FileOutputStream(xml)) { + IOTools.flow(istream, ostream); + } + } catch (IOException e) { + /* Ignore */ + } + } + } + + DeployedApplication deployedApp = + new DeployedApplication(cn.getName(), xml.exists() && deployThisXML && copyThisXml); + + long startTime = 0; + // Deploy the application in this WAR file + if (log.isInfoEnabled()) { + startTime = System.currentTimeMillis(); + log.info(sm.getString("hostConfig.deployWar", war.getAbsolutePath())); + } + + try { + // Populate redeploy resources with the WAR file + deployedApp.redeployResources.put(war.getAbsolutePath(), Long.valueOf(war.lastModified())); + + if (deployThisXML && xml.exists() && copyThisXml) { + deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); + } else { + // In case an XML file is added to the config base later + deployedApp.redeployResources.put( + (new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml")).getAbsolutePath(), + Long.valueOf(0)); + } + + Class clazz = Class.forName(host.getConfigClass()); + LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); + context.addLifecycleListener(listener); + + context.setName(cn.getName()); + context.setPath(cn.getPath()); + context.setWebappVersion(cn.getVersion()); + context.setDocBase(cn.getBaseName() + ".war"); + host.addChild(context); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t); + } finally { + // If we're unpacking WARs, the docBase will be mutated after + // starting the context + boolean unpackWAR = unpackWARs; + if (unpackWAR && context instanceof StandardContext) { + unpackWAR = ((StandardContext) context).getUnpackWAR(); + } + if (unpackWAR && context.getDocBase() != null) { + File docBase = new File(host.getAppBaseFile(), cn.getBaseName()); + deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); + addWatchedResources(deployedApp, docBase.getAbsolutePath(), context); + if (deployThisXML && !copyThisXml && (xmlInWar || xml.exists())) { + deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); + } + } else { + // Passing null for docBase means that no resources will be + // watched. This will be logged at debug level. + addWatchedResources(deployedApp, null, context); + } + // Add the global redeploy resources (which are never deleted) at + // the end so they don't interfere with the deletion process + addGlobalRedeployResources(deployedApp); + } + + deployed.put(cn.getName(), deployedApp); + + if (log.isInfoEnabled()) { + log.info(sm.getString("hostConfig.deployWar.finished", war.getAbsolutePath(), + Long.valueOf(System.currentTimeMillis() - startTime))); + } + } + + + /** + * Deploy exploded webapps. + * + * @param appBase The base path for applications + * @param files The exploded webapps that should be deployed + */ + protected void deployDirectories(File appBase, String[] files) { + + if (files == null) { + return; + } + + ExecutorService es = host.getStartStopExecutor(); + List> results = new ArrayList<>(); + + for (String file : files) { + if (file.equalsIgnoreCase("META-INF")) { + continue; + } + if (file.equalsIgnoreCase("WEB-INF")) { + continue; + } + + File dir = new File(appBase, file); + if (dir.isDirectory()) { + ContextName cn = new ContextName(file, false); + + if (tryAddServiced(cn.getName())) { + try { + if (deploymentExists(cn.getName())) { + removeServiced(cn.getName()); + continue; + } + + // DeployDirectory will call removeServiced + results.add(es.submit(new DeployDirectory(this, cn, dir))); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + removeServiced(cn.getName()); + throw t; + } + } + } + } + + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString("hostConfig.deployDir.threaded.error"), e); + } + } + } + + + /** + * Deploy exploded webapp. + *

    + * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. + * + * @param cn The context name + * @param dir The path to the root folder of the webapp + */ + protected void deployDirectory(ContextName cn, File dir) { + + long startTime = 0; + // Deploy the application in this directory + if (log.isInfoEnabled()) { + startTime = System.currentTimeMillis(); + log.info(sm.getString("hostConfig.deployDir", dir.getAbsolutePath())); + } + + Context context = null; + File xml = new File(dir, Constants.ApplicationContextXml); + File xmlCopy = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml"); + + DeployedApplication deployedApp; + boolean copyThisXml = isCopyXML(); + boolean deployThisXML = isDeployThisXML(dir, cn); + + try { + if (deployThisXML && xml.exists()) { + synchronized (digesterLock) { + try { + context = (Context) digester.parse(xml); + } catch (Exception e) { + log.error(sm.getString("hostConfig.deployDescriptor.error", xml), e); + context = new FailedContext(); + } finally { + digester.reset(); + if (context == null) { + context = new FailedContext(); + } + } + } + + if (copyThisXml == false && context instanceof StandardContext) { + // Host is using default value. Context may override it. + copyThisXml = ((StandardContext) context).getCopyXML(); + } + + if (copyThisXml) { + Files.copy(xml.toPath(), xmlCopy.toPath()); + context.setConfigFile(xmlCopy.toURI().toURL()); + } else { + context.setConfigFile(xml.toURI().toURL()); + } + } else if (!deployThisXML && xml.exists()) { + // Block deployment as META-INF/context.xml may contain security + // configuration necessary for a secure deployment. + log.error(sm.getString("hostConfig.deployDescriptor.blocked", cn.getPath(), xml, xmlCopy)); + context = new FailedContext(); + } else { + context = (Context) Class.forName(contextClass).getConstructor().newInstance(); + } + + Class clazz = Class.forName(host.getConfigClass()); + LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); + context.addLifecycleListener(listener); + + context.setName(cn.getName()); + context.setPath(cn.getPath()); + context.setWebappVersion(cn.getVersion()); + context.setDocBase(cn.getBaseName()); + host.addChild(context); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("hostConfig.deployDir.error", dir.getAbsolutePath()), t); + } finally { + deployedApp = new DeployedApplication(cn.getName(), xml.exists() && deployThisXML && copyThisXml); + + // Fake re-deploy resource to detect if a WAR is added at a later + // point + deployedApp.redeployResources.put(dir.getAbsolutePath() + ".war", Long.valueOf(0)); + deployedApp.redeployResources.put(dir.getAbsolutePath(), Long.valueOf(dir.lastModified())); + if (deployThisXML && xml.exists()) { + if (copyThisXml) { + deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(xmlCopy.lastModified())); + } else { + deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); + // Fake re-deploy resource to detect if a context.xml file is + // added at a later point + deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(0)); + } + } else { + // Fake re-deploy resource to detect if a context.xml file is + // added at a later point + deployedApp.redeployResources.put(xmlCopy.getAbsolutePath(), Long.valueOf(0)); + if (!xml.exists()) { + deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(0)); + } + } + addWatchedResources(deployedApp, dir.getAbsolutePath(), context); + // Add the global redeploy resources (which are never deleted) at + // the end so they don't interfere with the deletion process + addGlobalRedeployResources(deployedApp); + } + + deployed.put(cn.getName(), deployedApp); + + if (log.isInfoEnabled()) { + log.info(sm.getString("hostConfig.deployDir.finished", dir.getAbsolutePath(), + Long.valueOf(System.currentTimeMillis() - startTime))); + } + } + + + protected void migrateLegacyApps() { + File appBase = host.getAppBaseFile(); + File legacyAppBase = host.getLegacyAppBaseFile(); + if (!legacyAppBase.isDirectory()) { + return; + } + + ExecutorService es = host.getStartStopExecutor(); + List> results = new ArrayList<>(); + + // Should not be null as we test above if this is a directory + String[] migrationCandidates = legacyAppBase.list(); + if (migrationCandidates == null) { + return; + } + for (String migrationCandidate : migrationCandidates) { + File source = new File(legacyAppBase, migrationCandidate); + File destination = new File(appBase, migrationCandidate); + + ContextName cn; + if (source.lastModified() > destination.lastModified()) { + if (source.isFile() && source.getName().toLowerCase(Locale.ENGLISH).endsWith(".war")) { + cn = new ContextName(migrationCandidate, true); + } else if (source.isDirectory()) { + cn = new ContextName(migrationCandidate, false); + } else { + continue; + } + + if (tryAddServiced(cn.getBaseName())) { + try { + // MigrateApp will call removeServiced + results.add(es.submit(new MigrateApp(this, cn, source, destination))); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + removeServiced(cn.getName()); + throw t; + } + } + } + } + + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString("hostConfig.migrateApp.threaded.error"), e); + } + } + } + + + protected void migrateLegacyApp(File source, File destination) { + File tempNew = null; + File tempOld = null; + try { + tempNew = File.createTempFile("new", null, host.getLegacyAppBaseFile()); + tempOld = File.createTempFile("old", null, host.getLegacyAppBaseFile()); + // createTempFile is not directly compatible with directories, so cleanup + Files.delete(tempNew.toPath()); + Files.delete(tempOld.toPath()); + + // The use of defaults is deliberate here to avoid having to + // recreate every configuration option on the host. Better to change + // the defaults if necessary than to start adding configuration + // options. Users that need non-default options can convert manually + // via migration.[sh|bat] + Migration migration = new Migration(); + migration.setSource(source); + migration.setDestination(tempNew); + migration.execute(); + + // Use rename + if (destination.exists()) { + Files.move(destination.toPath(), tempOld.toPath()); + } + Files.move(tempNew.toPath(), destination.toPath()); + ExpandWar.delete(tempOld); + + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("hostConfig.migrateError"), t); + } finally { + if (tempNew != null && tempNew.exists()) { + ExpandWar.delete(tempNew); + } + } + } + + + /** + * Check if a webapp is already deployed in this host. + * + * @param contextName of the context which will be checked + * + * @return true if the specified deployment exists + */ + protected boolean deploymentExists(String contextName) { + return deployed.containsKey(contextName) || (host.findChild(contextName) != null); + } + + + /** + * Add watched resources to the specified Context. + * + * @param app HostConfig deployed app + * @param docBase web app docBase + * @param context web application context + */ + protected void addWatchedResources(DeployedApplication app, String docBase, Context context) { + // FIXME: Feature idea. Add support for patterns (ex: WEB-INF/*, + // WEB-INF/*.xml), where we would only check if at least one + // resource is newer than app.timestamp + File docBaseFile = null; + if (docBase != null) { + docBaseFile = new File(docBase); + if (!docBaseFile.isAbsolute()) { + docBaseFile = new File(host.getAppBaseFile(), docBase); + } + } + String[] watchedResources = context.findWatchedResources(); + for (String watchedResource : watchedResources) { + File resource = new File(watchedResource); + if (!resource.isAbsolute()) { + if (docBase != null) { + resource = new File(docBaseFile, watchedResource); + } else { + if (log.isTraceEnabled()) { + log.trace("Ignoring non-existent WatchedResource '" + resource.getAbsolutePath() + "'"); + } + continue; + } + } + if (log.isTraceEnabled()) { + log.trace("Watching WatchedResource '" + resource.getAbsolutePath() + "'"); + } + app.reloadResources.put(resource.getAbsolutePath(), Long.valueOf(resource.lastModified())); + } + } + + + protected void addGlobalRedeployResources(DeployedApplication app) { + // Redeploy resources processing is hard-coded to never delete this file + File hostContextXml = new File(getConfigBaseName(), Constants.HostContextXml); + if (hostContextXml.isFile()) { + app.redeployResources.put(hostContextXml.getAbsolutePath(), Long.valueOf(hostContextXml.lastModified())); + } + + // Redeploy resources in CATALINA_BASE/conf are never deleted + File globalContextXml = returnCanonicalPath(Constants.DefaultContextXml); + if (globalContextXml.isFile()) { + app.redeployResources.put(globalContextXml.getAbsolutePath(), + Long.valueOf(globalContextXml.lastModified())); + } + } + + + /** + * Check resources for redeployment and reloading. + * + * @param app The web application to check + * @param skipFileModificationResolutionCheck When checking files for modification should the check that requires + * that any file modification must have occurred at least as long ago + * as the resolution of the file time stamp be skipped + */ + protected void checkResources(DeployedApplication app, boolean skipFileModificationResolutionCheck) { + String[] resources = app.redeployResources.keySet().toArray(new String[0]); + // Offset the current time by the resolution of File.lastModified() + long currentTimeWithResolutionOffset = System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS; + for (int i = 0; i < resources.length; i++) { + File resource = new File(resources[i]); + if (log.isTraceEnabled()) { + log.trace("Checking context[" + app.name + "] redeploy resource " + resource); + } + long lastModified = app.redeployResources.get(resources[i]).longValue(); + if (resource.exists() || lastModified == 0) { + // File.lastModified() has a resolution of 1s (1000ms). The last + // modified time has to be more than 1000ms ago to ensure that + // modifications that take place in the same second are not + // missed. See Bug 57765. + if (resource.lastModified() != lastModified && + (!host.getAutoDeploy() || resource.lastModified() < currentTimeWithResolutionOffset || + skipFileModificationResolutionCheck)) { + if (resource.isDirectory()) { + // No action required for modified directory + app.redeployResources.put(resources[i], Long.valueOf(resource.lastModified())); + } else if (app.hasDescriptor && resource.getName().toLowerCase(Locale.ENGLISH).endsWith(".war")) { + // Modified WAR triggers a reload if there is an XML + // file present + // The only resource that should be deleted is the + // expanded WAR (if any) + Context context = (Context) host.findChild(app.name); + String docBase = context.getDocBase(); + if (!docBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) { + // This is an expanded directory + File docBaseFile = new File(docBase); + if (!docBaseFile.isAbsolute()) { + docBaseFile = new File(host.getAppBaseFile(), docBase); + } + reload(app, docBaseFile, resource.getAbsolutePath()); + } else { + reload(app, null, null); + } + // Update times + app.redeployResources.put(resources[i], Long.valueOf(resource.lastModified())); + app.timestamp = System.currentTimeMillis(); + boolean unpackWAR = unpackWARs; + if (unpackWAR && context instanceof StandardContext) { + unpackWAR = ((StandardContext) context).getUnpackWAR(); + } + if (unpackWAR) { + addWatchedResources(app, context.getDocBase(), context); + } else { + addWatchedResources(app, null, context); + } + return; + } else { + // Everything else triggers a redeploy + // (just need to undeploy here, deploy will follow) + undeploy(app); + deleteRedeployResources(app, resources, i, false); + return; + } + } + } else { + // There is a chance the the resource was only missing + // temporarily eg renamed during a text editor save + if (resource.exists() || !resource.getName().toLowerCase(Locale.ENGLISH).endsWith(".war")) { + try { + Thread.sleep(500); + } catch (InterruptedException e1) { + // Ignore + } + } + // Recheck the resource to see if it was really deleted + if (resource.exists()) { + continue; + } + // Undeploy application + undeploy(app); + deleteRedeployResources(app, resources, i, true); + return; + } + } + resources = app.reloadResources.keySet().toArray(new String[0]); + boolean update = false; + for (String s : resources) { + File resource = new File(s); + if (log.isTraceEnabled()) { + log.trace("Checking context[" + app.name + "] reload resource " + resource); + } + long lastModified = app.reloadResources.get(s).longValue(); + // File.lastModified() has a resolution of 1s (1000ms). The last + // modified time has to be more than 1000ms ago to ensure that + // modifications that take place in the same second are not + // missed. See Bug 57765. + if ((resource.lastModified() != lastModified && + (!host.getAutoDeploy() || resource.lastModified() < currentTimeWithResolutionOffset || + skipFileModificationResolutionCheck)) || + update) { + if (!update) { + // Reload application + reload(app, null, null); + update = true; + } + // Update times. More than one file may have been updated. We + // don't want to trigger a series of reloads. + app.reloadResources.put(s, Long.valueOf(resource.lastModified())); + } + app.timestamp = System.currentTimeMillis(); + } + } + + + /* + * Note: If either of fileToRemove and newDocBase are null, both will be ignored. + */ + private void reload(DeployedApplication app, File fileToRemove, String newDocBase) { + if (log.isInfoEnabled()) { + log.info(sm.getString("hostConfig.reload", app.name)); + } + Context context = (Context) host.findChild(app.name); + if (context.getState().isAvailable()) { + if (fileToRemove != null && newDocBase != null) { + context.addLifecycleListener(new ExpandedDirectoryRemovalListener(fileToRemove, newDocBase)); + } + // Reload catches and logs exceptions + context.reload(); + } else { + // If the context was not started (for example an error + // in web.xml) we'll still get to try to start + if (fileToRemove != null && newDocBase != null) { + ExpandWar.delete(fileToRemove); + context.setDocBase(newDocBase); + } + try { + context.start(); + } catch (Exception e) { + log.error(sm.getString("hostConfig.context.restart", app.name), e); + } + } + } + + + private void undeploy(DeployedApplication app) { + if (log.isInfoEnabled()) { + log.info(sm.getString("hostConfig.undeploy", app.name)); + } + Container context = host.findChild(app.name); + try { + host.removeChild(context); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("hostConfig.context.remove", app.name), t); + } + deployed.remove(app.name); + } + + + private void deleteRedeployResources(DeployedApplication app, String[] resources, int i, + boolean deleteReloadResources) { + + // Delete other redeploy resources + for (int j = i + 1; j < resources.length; j++) { + File current = new File(resources[j]); + // Never delete per host context.xml defaults + if (Constants.HostContextXml.equals(current.getName())) { + continue; + } + // Only delete resources in the appBase or the + // host's configBase + if (isDeletableResource(app, current)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("hostConfig.delete", current)); + } + ExpandWar.delete(current); + } + } + + // Delete reload resources (to remove any remaining .xml descriptor) + if (deleteReloadResources) { + String[] resources2 = app.reloadResources.keySet().toArray(new String[0]); + for (String s : resources2) { + File current = new File(s); + // Never delete per host context.xml defaults + if (Constants.HostContextXml.equals(current.getName())) { + continue; + } + // Only delete resources in the appBase or the host's + // configBase + if (isDeletableResource(app, current)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("hostConfig.delete", current)); + } + ExpandWar.delete(current); + } + } + } + } + + + /* + * Delete any resource that would trigger the automatic deployment code to re-deploy the application. This means + * deleting: + * + * - any resource located in the appBase + * + * - any deployment descriptor located under the configBase + * + * - symlinks in the appBase or configBase for either of the above + */ + private boolean isDeletableResource(DeployedApplication app, File resource) { + // The resource may be a file, a directory or a symlink to a file or directory. + + // Check that the resource is absolute. This should always be the case. + if (!resource.isAbsolute()) { + log.warn(sm.getString("hostConfig.resourceNotAbsolute", app.name, resource)); + return false; + } + + // Determine where the resource is located + String canonicalLocation; + try { + canonicalLocation = resource.getParentFile().getCanonicalPath(); + } catch (IOException e) { + log.warn(sm.getString("hostConfig.canonicalizing", resource.getParentFile(), app.name), e); + return false; + } + + String canonicalAppBase; + try { + canonicalAppBase = host.getAppBaseFile().getCanonicalPath(); + } catch (IOException e) { + log.warn(sm.getString("hostConfig.canonicalizing", host.getAppBaseFile(), app.name), e); + return false; + } + + if (canonicalLocation.equals(canonicalAppBase)) { + // Resource is located in the appBase so it may be deleted + return true; + } + + String canonicalConfigBase; + try { + canonicalConfigBase = host.getConfigBaseFile().getCanonicalPath(); + } catch (IOException e) { + log.warn(sm.getString("hostConfig.canonicalizing", host.getConfigBaseFile(), app.name), e); + return false; + } + + if (canonicalLocation.equals(canonicalConfigBase) && resource.getName().endsWith(".xml")) { + // Resource is an xml file in the configBase so it may be deleted + return true; + } + + // All other resources should not be deleted + return false; + } + + + public void beforeStart() { + if (host.getCreateDirs()) { + File[] dirs = new File[] { host.getAppBaseFile(), host.getConfigBaseFile() }; + for (File dir : dirs) { + if (!dir.mkdirs() && !dir.isDirectory()) { + log.error(sm.getString("hostConfig.createDirs", dir)); + } + } + } + } + + + /** + * Process a "start" event for this Host. + */ + public void start() { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("hostConfig.start")); + } + + try { + ObjectName hostON = host.getObjectName(); + oname = new ObjectName(hostON.getDomain() + ":type=Deployer,host=" + host.getName()); + Registry.getRegistry(null, null).registerComponent(this, oname, this.getClass().getName()); + } catch (Exception e) { + log.warn(sm.getString("hostConfig.jmx.register", oname), e); + } + + if (!host.getAppBaseFile().isDirectory()) { + log.error(sm.getString("hostConfig.appBase", host.getName(), host.getAppBaseFile().getPath())); + host.setDeployOnStartup(false); + host.setAutoDeploy(false); + } + + if (host.getDeployOnStartup()) { + deployApps(); + } + } + + + /** + * Process a "stop" event for this Host. + */ + public void stop() { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("hostConfig.stop")); + } + + if (oname != null) { + try { + Registry.getRegistry(null, null).unregisterComponent(oname); + } catch (Exception e) { + log.warn(sm.getString("hostConfig.jmx.unregister", oname), e); + } + } + oname = null; + } + + + /** + * Check status of all webapps. + */ + protected void check() { + + if (host.getAutoDeploy()) { + // Check for resources modification to trigger redeployment + DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]); + for (DeployedApplication app : apps) { + if (tryAddServiced(app.name)) { + try { + checkResources(app, false); + } finally { + removeServiced(app.name); + } + } + } + + // Check for old versions of applications that can now be undeployed + if (host.getUndeployOldVersions()) { + checkUndeploy(); + } + + // Hotdeploy applications + deployApps(); + } + } + + + /** + * Check status of a specific web application and reload, redeploy or deploy it as necessary. This method is for use + * with functionality such as management web applications that upload new/updated web applications and need to + * trigger the appropriate action to deploy them. This method assumes that any uploading/updating has been completed + * before this method is called. Any action taken as a result of the checks will complete before this method + * returns. + * + * @param name The name of the web application to check + */ + public void check(String name) { + synchronized (host) { + if (!((Lifecycle) host).getState().isAvailable()) { + return; + } + if (tryAddServiced(name)) { + try { + DeployedApplication app = deployed.get(name); + if (app != null) { + checkResources(app, true); + } + deployApps(name); + } finally { + removeServiced(name); + } + } + } + } + + /** + * Check for old versions of applications using parallel deployment that are now unused (have no active sessions) + * and undeploy any that are found. + */ + public void checkUndeploy() { + synchronized (host) { + if (deployed.size() < 2) { + return; + } + + // Need ordered set of names + SortedSet sortedAppNames = new TreeSet<>(deployed.keySet()); + + Iterator iter = sortedAppNames.iterator(); + + ContextName previous = new ContextName(iter.next(), false); + do { + ContextName current = new ContextName(iter.next(), false); + + if (current.getPath().equals(previous.getPath())) { + // Current and previous are same path - current will always + // be a later version + Context previousContext = (Context) host.findChild(previous.getName()); + Context currentContext = (Context) host.findChild(current.getName()); + if (previousContext != null && currentContext != null && currentContext.getState().isAvailable() && + tryAddServiced(previous.getName())) { + try { + Manager manager = previousContext.getManager(); + if (manager != null) { + int sessionCount; + if (manager instanceof DistributedManager) { + sessionCount = ((DistributedManager) manager).getActiveSessionsFull(); + } else { + sessionCount = manager.getActiveSessions(); + } + if (sessionCount == 0) { + if (log.isInfoEnabled()) { + log.info(sm.getString("hostConfig.undeployVersion", previous.getName())); + } + DeployedApplication app = deployed.get(previous.getName()); + String[] resources = app.redeployResources.keySet().toArray(new String[0]); + // Version is unused - undeploy it completely + // The -1 is a 'trick' to ensure all redeploy + // resources are removed + undeploy(app); + deleteRedeployResources(app, resources, -1, true); + } + } + } finally { + removeServiced(previous.getName()); + } + } + } + previous = current; + } while (iter.hasNext()); + } + } + + /** + * Add a new Context to be managed by us. Entry point for the admin webapp, and other JMX Context controllers. + * + * @param context The context instance + */ + public void manageApp(Context context) { + + String contextName = context.getName(); + + if (deployed.containsKey(contextName)) { + return; + } + + DeployedApplication deployedApp = new DeployedApplication(contextName, false); + + // Add the associated docBase to the redeployed list if it's a WAR + boolean isWar = false; + if (context.getDocBase() != null) { + File docBase = new File(context.getDocBase()); + if (!docBase.isAbsolute()) { + docBase = new File(host.getAppBaseFile(), context.getDocBase()); + } + deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); + if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) { + isWar = true; + } + } + host.addChild(context); + // Add the eventual unpacked WAR and all the resources which will be + // watched inside it + boolean unpackWAR = unpackWARs; + if (unpackWAR && context instanceof StandardContext) { + unpackWAR = ((StandardContext) context).getUnpackWAR(); + } + if (isWar && unpackWAR) { + File docBase = new File(host.getAppBaseFile(), context.getBaseName()); + deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); + addWatchedResources(deployedApp, docBase.getAbsolutePath(), context); + } else { + addWatchedResources(deployedApp, null, context); + } + deployed.put(contextName, deployedApp); + } + + /** + * Remove a webapp from our control. Entry point for the admin webapp, and other JMX Context controllers. + *

    + * Note: It is expected that the caller has successfully added the app to servicedSet before calling this method. + * + * @param contextName The context name + */ + public void unmanageApp(String contextName) { + deployed.remove(contextName); + host.removeChild(host.findChild(contextName)); + } + + // ----------------------------------------------------- Instance Variables + + + /** + * This class represents the state of a deployed application, as well as the monitored resources. + */ + protected static class DeployedApplication { + public DeployedApplication(String name, boolean hasDescriptor) { + this.name = name; + this.hasDescriptor = hasDescriptor; + } + + /** + * Application context path. The assertion is that (host.getChild(name) != null). + */ + public final String name; + + /** + * Does this application have a context.xml descriptor file on the host's configBase? + */ + public final boolean hasDescriptor; + + /** + * Any modification of the specified (static) resources will cause a redeployment of the application. If any of + * the specified resources is removed, the application will be undeployed. Typically, this will contain + * resources like the context.xml file, a compressed WAR path. The value is the last modification time. + */ + public final LinkedHashMap redeployResources = new LinkedHashMap<>(); + + /** + * Any modification of the specified (static) resources will cause a reload of the application. This will + * typically contain resources such as the web.xml of a webapp, but can be configured to contain additional + * descriptors. The value is the last modification time. + */ + public final HashMap reloadResources = new HashMap<>(); + + /** + * Instant where the application was last put in service. + */ + public long timestamp = System.currentTimeMillis(); + + /** + * In some circumstances, such as when unpackWARs is true, a directory may be added to the appBase that is + * ignored. This flag indicates that the user has been warned so that the warning is not logged on every run of + * the auto deployer. + */ + public boolean loggedDirWarning = false; + } + + private static class DeployDescriptor implements Runnable { + + private HostConfig config; + private ContextName cn; + private File descriptor; + + DeployDescriptor(HostConfig config, ContextName cn, File descriptor) { + this.config = config; + this.cn = cn; + this.descriptor = descriptor; + } + + @Override + public void run() { + try { + config.deployDescriptor(cn, descriptor); + } finally { + config.removeServiced(cn.getName()); + } + } + } + + private static class DeployWar implements Runnable { + + private HostConfig config; + private ContextName cn; + private File war; + + DeployWar(HostConfig config, ContextName cn, File war) { + this.config = config; + this.cn = cn; + this.war = war; + } + + @Override + public void run() { + try { + config.deployWAR(cn, war); + } finally { + config.removeServiced(cn.getName()); + } + } + } + + private static class DeployDirectory implements Runnable { + + private HostConfig config; + private ContextName cn; + private File dir; + + DeployDirectory(HostConfig config, ContextName cn, File dir) { + this.config = config; + this.cn = cn; + this.dir = dir; + } + + @Override + public void run() { + try { + config.deployDirectory(cn, dir); + } finally { + config.removeServiced(cn.getName()); + } + } + } + + + private static class MigrateApp implements Runnable { + + private HostConfig config; + private ContextName cn; + private File source; + private File destination; + + MigrateApp(HostConfig config, ContextName cn, File source, File destination) { + this.config = config; + this.cn = cn; + this.source = source; + this.destination = destination; + } + + @Override + public void run() { + try { + config.migrateLegacyApp(source, destination); + } finally { + config.removeServiced(cn.getName()); + } + } + } + + + /* + * The purpose of this class is to provide a way for HostConfig to get a Context to delete an expanded WAR after the + * Context stops. This is to resolve this issue described in Bug 57772. The alternative solutions require either + * duplicating a lot of the Context.reload() code in HostConfig or adding a new reload(boolean) method to Context + * that allows the caller to optionally delete any expanded WAR. + * + * The LifecycleListener approach offers greater flexibility and enables the behaviour to be changed / extended / + * removed in future without changing the Context API. + */ + private static class ExpandedDirectoryRemovalListener implements LifecycleListener { + + private final File toDelete; + private final String newDocBase; + + /** + * Create a listener that will ensure that any expanded WAR is removed and the docBase set to the specified WAR. + * + * @param toDelete The file (a directory representing an expanded WAR) to be deleted + * @param newDocBase The new docBase for the Context + */ + ExpandedDirectoryRemovalListener(File toDelete, String newDocBase) { + this.toDelete = toDelete; + this.newDocBase = newDocBase; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType())) { + // The context has stopped. + Context context = (Context) event.getLifecycle(); + + // Remove the old expanded WAR. + ExpandWar.delete(toDelete); + + // Reset the docBase to trigger re-expansion of the WAR. + context.setDocBase(newDocBase); + + // Remove this listener from the Context else it will run every + // time the Context is stopped. + context.removeLifecycleListener(this); + } + } + } +} diff --git a/java/org/apache/catalina/startup/HostRuleSet.java b/java/org/apache/catalina/startup/HostRuleSet.java new file mode 100644 index 0000000..c40f5bc --- /dev/null +++ b/java/org/apache/catalina/startup/HostRuleSet.java @@ -0,0 +1,97 @@ +/* + * 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.catalina.startup; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; + +/** + * RuleSet for processing the contents of a Host definition element. This RuleSet does NOT + * include any rules for nested Context which should be added via instances of ContextRuleSet. + * + * @author Craig R. McClanahan + */ +public class HostRuleSet implements RuleSet { + + // ----------------------------------------------------- Instance Variables + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + + // ------------------------------------------------------------ Constructor + + /** + * Construct an instance of this RuleSet with the default matching pattern prefix. + */ + public HostRuleSet() { + this(""); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + */ + public HostRuleSet(String prefix) { + this.prefix = prefix; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Add the set of Rule instances defined in this RuleSet to the specified Digester instance, + * associating them with our namespace URI (if any). This method should only be called by a Digester instance. + * + * @param digester Digester instance to which the new Rule instances should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + + digester.addObjectCreate(prefix + "Host", "org.apache.catalina.core.StandardHost", "className"); + digester.addSetProperties(prefix + "Host"); + digester.addRule(prefix + "Host", new CopyParentClassLoaderRule()); + digester.addRule(prefix + "Host", + new LifecycleListenerRule("org.apache.catalina.startup.HostConfig", "hostConfigClass")); + digester.addSetNext(prefix + "Host", "addChild", "org.apache.catalina.Container"); + + digester.addCallMethod(prefix + "Host/Alias", "addAlias", 0); + + // Cluster configuration start + digester.addObjectCreate(prefix + "Host/Cluster", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Host/Cluster"); + digester.addSetNext(prefix + "Host/Cluster", "setCluster", "org.apache.catalina.Cluster"); + // Cluster configuration end + + digester.addObjectCreate(prefix + "Host/Listener", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Host/Listener"); + digester.addSetNext(prefix + "Host/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); + + digester.addRuleSet(new RealmRuleSet(prefix + "Host/")); + + digester.addObjectCreate(prefix + "Host/Valve", null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Host/Valve"); + digester.addSetNext(prefix + "Host/Valve", "addValve", "org.apache.catalina.Valve"); + } +} diff --git a/java/org/apache/catalina/startup/LifecycleListenerRule.java b/java/org/apache/catalina/startup/LifecycleListenerRule.java new file mode 100644 index 0000000..023cf90 --- /dev/null +++ b/java/org/apache/catalina/startup/LifecycleListenerRule.java @@ -0,0 +1,132 @@ +/* + * 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.catalina.startup; + + +import org.apache.catalina.Container; +import org.apache.catalina.LifecycleListener; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.digester.Rule; +import org.xml.sax.Attributes; + + +/** + * Rule that creates a new {@link LifecycleListener} and associates it with the top object on the stack which must + * implement {@link Container}. The implementation class to be used is determined by: + *

      + *
    1. Does the top element on the stack specify an implementation class using the attribute specified when this rule + * was created?
    2. + *
    3. Does the parent {@link Container} of the {@link Container} on the top of the stack specify an implementation + * class using the attribute specified when this rule was created?
    4. + *
    5. Use the default implementation class specified when this rule was created.
    6. + *
    + */ +public class LifecycleListenerRule extends Rule { + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new instance of this Rule. + * + * @param listenerClass Default name of the LifecycleListener implementation class to be created + * @param attributeName Name of the attribute that optionally includes an override name of the LifecycleListener + * class + */ + public LifecycleListenerRule(String listenerClass, String attributeName) { + + this.listenerClass = listenerClass; + this.attributeName = attributeName; + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The attribute name of an attribute that can override the implementation class name. + */ + private final String attributeName; + + + /** + * The name of the LifecycleListener implementation class. + */ + private final String listenerClass; + + + // --------------------------------------------------------- Public Methods + + + /** + * Handle the beginning of an XML element. + * + * @param attributes The attributes of this element + * + * @exception Exception if a processing error occurs + */ + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + + Container c = (Container) digester.peek(); + Container p = null; + Object obj = digester.peek(1); + if (obj instanceof Container) { + p = (Container) obj; + } + + String className = null; + + // Check the container for the specified attribute + if (attributeName != null) { + String value = attributes.getValue(attributeName); + if (value != null) { + className = value; + } + } + + // Check the container's parent for the specified attribute + if (p != null && className == null) { + String configClass = (String) IntrospectionUtils.getProperty(p, attributeName); + if (configClass != null && configClass.length() > 0) { + className = configClass; + } + } + + // Use the default + if (className == null) { + className = listenerClass; + } + + // Instantiate a new LifecycleListener implementation object + Class clazz = Class.forName(className); + LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); + + // Add this LifecycleListener to our associated component + c.addLifecycleListener(listener); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(c)).append(".addLifecycleListener("); + code.append("new ").append(className).append("());").append(System.lineSeparator()); + } + } + + +} diff --git a/java/org/apache/catalina/startup/ListenerCreateRule.java b/java/org/apache/catalina/startup/ListenerCreateRule.java new file mode 100644 index 0000000..6cfb75c --- /dev/null +++ b/java/org/apache/catalina/startup/ListenerCreateRule.java @@ -0,0 +1,125 @@ +/* + * 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.catalina.startup; + + +import java.util.HashMap; +import java.util.Set; + +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.digester.ObjectCreateRule; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.Attributes; + + +/** + * Rule implementation that creates a server listener. + */ +public class ListenerCreateRule extends ObjectCreateRule { + + private static final Log log = LogFactory.getLog(ListenerCreateRule.class); + protected static final StringManager sm = StringManager.getManager(ListenerCreateRule.class); + + public ListenerCreateRule(String className, String attributeName) { + super(className, attributeName); + } + + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + if ("true".equals(attributes.getValue("optional"))) { + try { + super.begin(namespace, name, attributes); + } catch (Exception e) { + String className = getRealClassName(attributes); + if (log.isDebugEnabled()) { + log.info(sm.getString("listener.createFailed", className), e); + } else { + log.info(sm.getString("listener.createFailed", className)); + } + Object instance = new OptionalListener(className); + digester.push(instance); + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(OptionalListener.class.getName().replace('$', '.')).append(' '); + code.append(digester.toVariableName(instance)).append(" = new "); + code.append(OptionalListener.class.getName().replace('$', '.')).append("(\"").append(className) + .append("\");"); + code.append(System.lineSeparator()); + } + } + } else { + super.begin(namespace, name, attributes); + } + } + + public static class OptionalListener implements LifecycleListener { + protected final String className; + protected final HashMap properties = new HashMap<>(); + + public OptionalListener(String className) { + this.className = className; + } + + /** + * @return the className + */ + public String getClassName() { + return className; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + // Do nothing + } + + /** + * Return a set of the property keys. + * + * @return the set + */ + public Set getProperties() { + return properties.keySet(); + } + + /** + * Return a property from the protocol handler. + * + * @param name the property name + * + * @return the property value + */ + public Object getProperty(String name) { + return properties.get(name); + } + + /** + * Set the given property. + * + * @param name the property name + * @param value the property value + * + * @return true + */ + public boolean setProperty(String name, String value) { + properties.put(name, value); + return true; + } + } +} diff --git a/java/org/apache/catalina/startup/LocalStrings.properties b/java/org/apache/catalina/startup/LocalStrings.properties new file mode 100644 index 0000000..d6144f9 --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings.properties @@ -0,0 +1,195 @@ +# 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. + +catalina.configFail=Unable to load server configuration from [{0}] +catalina.destroyFail=Error destroying failed server +catalina.generatedCodeLocationError=Error using configured location for generated Tomcat embedded code [{0}] +catalina.incorrectPermissions=Permissions incorrect, read permission is not allowed on the file +catalina.init=Server initialization in [{0}] milliseconds +catalina.initError=Error initializing Catalina +catalina.initialContextFactory=The system property INITIAL_CONTEXT_FACTORY is already set to [{0}] +catalina.loaderWriteFail=Error writing code loader +catalina.namingPrefix=Set naming prefix [{0}] +catalina.noCluster=Cluster RuleSet not found due to [{0}]. Cluster configuration disabled. +catalina.noLoader=Configuration code loader [{0}] was not found, generated code will not be used +catalina.noNaming=Naming environment is disabled +catalina.noServer=Cannot start server, server instance is not configured +catalina.serverStartFail=The required Server component failed to start so Tomcat is unable to start. +catalina.shutdownHookFail=The shutdown hook experienced an error while trying to stop the server +catalina.startup=Server startup in [{0}] milliseconds +catalina.stopError=Error stopping Catalina +catalina.stopServer=No shutdown port configured. Shut down server through OS signal. Server not shut down. +catalina.stopServer.connectException=Could not contact [{0}:{1}] (base port [{2}] and offset [{3}]). Tomcat may not be running. +catalina.usage=usage: java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -nonaming ] [ -generateCode [ {pathname} ] ] [ -useGeneratedCode ] { -help | start | stop } + +catalinaConfigurationSource.cannotObtainURL=Cannot obtain resource for specified location [{0}]: no readable file, classloader resource, or this is not a resolvable URI + +connector.noSetExecutor=Connector [{0}] does not support external executors. Method setExecutor(java.util.concurrent.Executor) not found. +connector.noSetSSLImplementationName=Connector [{0}] does not support changing the SSL implementation. Method setSslImplementationName(String) not found. + +contextConfig.altDDNotFound=alt-dd file [{0}] not found +contextConfig.annotationsStackOverflow=Unable to complete the scan for annotations for web application [{0}] due to a StackOverflowError. Possible root causes include a too low setting for -Xss and illegal cyclic inheritance dependencies. The class hierarchy being processed was [{1}] +contextConfig.antiLocking=Anti locking for context [{0}] setting docBase to [{1}] +contextConfig.applicationMissing=Missing application web.xml, using defaults only +contextConfig.applicationParse=Parse error in application web.xml file at [{0}] +contextConfig.applicationPosition=Occurred at line [{0}] column [{1}] +contextConfig.applicationStart=Parsing application web.xml file at [{0}] +contextConfig.applicationUrl=Unable to determine URL for application web.xml +contextConfig.authenticatorConfigured=Configured an authenticator for method [{0}] +contextConfig.authenticatorInstantiate=Cannot instantiate an authenticator of class [{0}] +contextConfig.authenticatorMissing=Cannot configure an authenticator for method [{0}] +contextConfig.authenticatorResources=Cannot load authenticators mapping list +contextConfig.badUrl=Unable to process context descriptor [{0}] +contextConfig.cce=Lifecycle event data object [{0}] is not a Context +contextConfig.contextClose=Error closing context.xml +contextConfig.contextMissing=Missing context.xml: [{0}] +contextConfig.contextParse=Parse error in context.xml for [{0}] +contextConfig.defaultError=Error processed default web.xml named [{0}] at [{1}] +contextConfig.defaultMissing=No global web.xml found +contextConfig.defaultPosition=Occurred at line [{0}] column [{1}] +contextConfig.destroy=ContextConfig: Destroying +contextConfig.effectiveWebXml=Effective web.xml:\n\ +{0} +contextConfig.fileUrl=Unable to create a File object from the URL [{0}] +contextConfig.fixDocBase=Exception fixing docBase for context [{0}] +contextConfig.init=ContextConfig: Initializing +contextConfig.inputStreamFile=Unable to process file [{0}] for annotations +contextConfig.inputStreamJar=Unable to process Jar entry [{0}] from Jar [{1}] for annotations +contextConfig.inputStreamWebResource=Unable to process web resource [{0}] for annotations +contextConfig.invalidSciHandlesTypes=Unable to load class [{0}] to check against the @HandlesTypes annotation of one or more ServletContentInitializers. +contextConfig.jarFile=Unable to process Jar [{0}] for annotations +contextConfig.jspFile.error=JSP file [{0}] must start with a ''/'' +contextConfig.jspFile.warning=WARNING: JSP file [{0}] must start with a ''/'' in Servlet 2.4 +contextConfig.missingRealm=No Realm has been configured to authenticate against +contextConfig.noAntiLocking=The value [{0}] configured for java.io.tmpdir does not point to a valid directory. The antiResourceLocking setting for the web application [{1}] will be ignored. +contextConfig.noJsp=Skipping JSP property group for URL [{0}], no JSP Servlet found for name [{1}] +contextConfig.processAnnotationsDir.debug=Scanning directory for class files with annotations [{0}] +contextConfig.processAnnotationsInParallelFailure=Parallel execution failed +contextConfig.processAnnotationsJar.debug=Scanning jar file for class files with annotations [{0}] +contextConfig.processAnnotationsWebDir.debug=Scanning web application directory for class files with annotations [{0}] +contextConfig.processContext=Processing context [{0}] with configuration [{1}] +contextConfig.resourceJarFail=Failed to process JAR found at URL [{0}] for static resources to be included in context with name [{1}] +contextConfig.role.auth=Security role name [{0}] used in an without being defined in a +contextConfig.role.link=Security role name [{0}] used in a without being defined in a +contextConfig.role.runas=Security role name [{0}] used in a without being defined in a +contextConfig.sci.debug=Unable to process ServletContainerInitializer for [{0}]. This is most likely due to a class defined in the @HandlesTypes annotation being missing +contextConfig.sci.info=Unable to process ServletContainerInitializer for [{0}]. This is most likely due to a class defined in the @HandlesTypes annotation being missing. Enable DEBUG level logging for the full stack trace. +contextConfig.servletContainerInitializerFail=Failed to detect ServletContainerInitializers for context with name [{0}] +contextConfig.start=ContextConfig: Processing START +contextConfig.stop=ContextConfig: Processing STOP +contextConfig.tomcatWebXmlError=Error processing /WEB-INF/tomcat-web.xml +contextConfig.unavailable=Marking this application unavailable due to previous error(s) +contextConfig.unknownUrlProtocol=The URL protocol [{0}] was not recognised during annotation processing. URL [{1}] was ignored. +contextConfig.urlPatternValue=Both the urlPatterns and value attributes were set for the [{0}] annotation on class [{1}] +contextConfig.xmlSettings=Context [{0}] will parse web.xml and web-fragment.xml files with validation:[{1}] and namespaceAware:[{2}] + +engineConfig.cce=Lifecycle event data object [{0}] is not an Engine +engineConfig.start=EngineConfig: Processing START +engineConfig.stop=EngineConfig: Processing STOP + +expandWar.copy=Error copying [{0}] to [{1}] +expandWar.createFailed=Unable to create the directory [{0}] +expandWar.createFileFailed=Unable to create the file [{0}] +expandWar.deleteFailed=[{0}] could not be completely deleted. The presence of the remaining files may cause problems +expandWar.deleteOld=An expanded directory [{0}] was found with a last modified time that did not match the associated WAR. It will be deleted. +expandWar.illegalPath=The archive [{0}] is malformed and will be ignored: an entry contains an illegal path [{1}] which was not expanded to [{2}] since that is outside of the defined docBase [{3}] +expandWar.lastModifiedFailed=Unable to set the last modified time for [{0}] +expandWar.missingJarEntry=Cannot get input stream for JarEntry [{0}] - broken WAR file? + +failedContext.start=Failed to process either the global, per-host or context-specific context.xml file therefore the [{0}] Context cannot be started. + +hostConfig.appBase=Application base [{1}] for host [{0}] does not exist or is not a directory. deployOnStartUp and autoDeploy have been set to false to prevent deployment errors. Other errors may still occur. +hostConfig.canonicalizing=Unable to determine canonical path for [{0}] while attempting to undeploy [{1}] +hostConfig.cce=Lifecycle event data object [{0}] is not a Host +hostConfig.context.remove=Error while removing context [{0}] +hostConfig.context.restart=Error during context [{0}] restart +hostConfig.createDirs=Unable to create directory for deployment: [{0}] +hostConfig.delete=Delete resource [{0}] during application reload +hostConfig.deploy.error=Exception while deploying web application directory [{0}] +hostConfig.deployDescriptor=Deploying deployment descriptor [{0}] +hostConfig.deployDescriptor.blocked=The web application with context path [{0}] was not deployed because it contained a deployment descriptor [{1}] which may include configuration necessary for the secure deployment of the application but processing of deployment descriptors is prevented by the deployXML setting of this host. An appropriate descriptor should be created at [{2}] to deploy this application. +hostConfig.deployDescriptor.error=Error deploying deployment descriptor [{0}] +hostConfig.deployDescriptor.finished=Deployment of deployment descriptor [{0}] has finished in [{1}] ms +hostConfig.deployDescriptor.hiddenDir=Deployment of deployment descriptor [{0}] with an external docBase means the directory [{1}] in the appBase will be ignored +hostConfig.deployDescriptor.hiddenWar=Deployment of deployment descriptor [{0}] with an external docBase means the WAR [{1}] in the appBase will be ignored +hostConfig.deployDescriptor.localDocBaseSpecified=A docBase [{0}] inside the host appBase has been specified, and will be ignored +hostConfig.deployDescriptor.path=The path attribute with value [{0}] in deployment descriptor [{1}] has been ignored +hostConfig.deployDescriptor.threaded.error=Error waiting for multi-thread deployment of deployment descriptors to complete +hostConfig.deployDir=Deploying web application directory [{0}] +hostConfig.deployDir.error=Error deploying web application directory [{0}] +hostConfig.deployDir.finished=Deployment of web application directory [{0}] has finished in [{1}] ms +hostConfig.deployDir.threaded.error=Error waiting for multi-thread deployment of directories to complete +hostConfig.deployWar=Deploying web application archive [{0}] +hostConfig.deployWar.error=Error deploying web application archive [{0}] +hostConfig.deployWar.finished=Deployment of web application archive [{0}] has finished in [{1}] ms +hostConfig.deployWar.hiddenDir=The directory [{0}] will be ignored because the WAR [{1}] takes priority and unpackWARs is false +hostConfig.deployWar.threaded.error=Error waiting for multi-thread deployment of WAR files to complete +hostConfig.deploying=Deploying discovered web applications +hostConfig.docBaseUrlInvalid=The provided docBase cannot be expressed as a URL +hostConfig.expand=Expanding web application archive [{0}] +hostConfig.expand.error=Exception while expanding web application archive [{0}] +hostConfig.ignorePath=Ignoring path [{0}] in appBase for automatic deployment +hostConfig.illegalWarName=The war name [{0}] is invalid. The archive will be ignored. +hostConfig.jmx.register=Register context [{0}] failed +hostConfig.jmx.unregister=Unregister context [{0}] failed +hostConfig.migrateApp.threaded.error=Error waiting for multi-thread migration of legacy applications to complete +hostConfig.migrateError=Migration failure +hostConfig.reload=Reloading context [{0}] +hostConfig.resourceNotAbsolute=Unable to remove resource from context [{0}] since [{1}] is not an absolute path +hostConfig.start=HostConfig: Processing START +hostConfig.stop=HostConfig: Processing STOP +hostConfig.undeploy=Undeploying context [{0}] +hostConfig.undeployVersion=Undeploying old version of context [{0}] which has no active session + +listener.createFailed=Optional listener [{0}] is not enabled +listener.notServer=This listener must only be nested within Server elements, but is in [{0}]. + +passwdUserDatabase.readFail=Failed to obtain a complete set of users from /etc/passwd + +tomcat.addWebapp.conflictChild=Unable to deploy WAR at [{0}] to context path [{1}] because of existing context [{2}] +tomcat.addWebapp.conflictFile=Unable to deploy WAR at [{0}] to context path [{1}] because of existing file [{2}] +tomcat.baseDirMakeFail=Unable to create the directory [{0}] to use as the base directory +tomcat.baseDirNotDir=The location [{0}] specified for the base directory is not a directory +tomcat.defaultMimeTypeMappingsFail=Unable to load the default MIME types +tomcat.homeDirMakeFail=Unable to create the directory [{0}] to use as the home directory +tomcat.invalidCommandLine=Invalid command line arguments [{0}] +tomcat.noContextClass=Failed to instantiate context class [{0}] for host [{1}] and url [{2}] +tomcat.noContextXml=Unable to determine web application context.xml [{0}] +tomcat.noWrapper=Failed to create wrapper + +userConfig.database=Exception loading user database +userConfig.deploy=Deploying web application for user [{0}] +userConfig.deploy.threaded.error=Error waiting for multi-thread deployment of user directories to complete +userConfig.deploying=Deploying user web applications +userConfig.error=Error deploying web application for user [{0}] +userConfig.start=UserConfig: Processing START +userConfig.stop=UserConfig: Processing STOP + +versionLoggerListener.arg=Command line argument: {0} +versionLoggerListener.catalina.base=CATALINA_BASE: {0} +versionLoggerListener.catalina.home=CATALINA_HOME: {0} +versionLoggerListener.env=Environment variable: {0} = {1} +versionLoggerListener.java.home=Java Home: {0} +versionLoggerListener.os.arch=Architecture: {0} +versionLoggerListener.os.name=OS Name: {0} +versionLoggerListener.os.version=OS Version: {0} +versionLoggerListener.prop=System property: {0} = {1} +versionLoggerListener.serverInfo.server.built=Server built: {0} +versionLoggerListener.serverInfo.server.number=Server version number: {0} +versionLoggerListener.serverInfo.server.version=Server version name: {0} +versionLoggerListener.vm.vendor=JVM Vendor: {0} +versionLoggerListener.vm.version=JVM Version: {0} + +webAnnotationSet.invalidInjection=Invalid method resource injection annotation. diff --git a/java/org/apache/catalina/startup/LocalStrings_cs.properties b/java/org/apache/catalina/startup/LocalStrings_cs.properties new file mode 100644 index 0000000..accc098 --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_cs.properties @@ -0,0 +1,40 @@ +# 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. + +catalina.serverStartFail=Selhal start požadované Server komponenty, proto Tomcat nelze nastartovat. + +contextConfig.altDDNotFound=Soubor alt-dd [{0}] nenalezen +contextConfig.applicationUrl=Nelze urÄit URL pro aplikaÄní web.xml +contextConfig.authenticatorConfigured=Nakonfigurována autentizace pro metodu [{0}] +contextConfig.contextMissing=Chybí context.xml: [{0}] +contextConfig.defaultMissing=Nenalezen globální web.xml +contextConfig.defaultPosition=Stalo se na řádce [{0}], sloupec [{1}] +contextConfig.inputStreamWebResource=Nelze zpracovat webový zdroj [{0}] pro anotaci +contextConfig.processAnnotationsDir.debug=Prohledání adresáře pro soubory tříd s anotacemi [{0}] +contextConfig.role.auth=Název bezpeÄnostní role [{0}] byl použit v bez pÅ™edchozí definice v +contextConfig.tomcatWebXmlError=Chyba zpracování /WEB-INF/tomcat-web.xml + +engineConfig.stop=EngineConfig: Zpracování STOP + +hostConfig.deployDir=Nasazování adresáře webové aplikace [{0}] +hostConfig.deployWar.error=Chyba pro nasazování webevé aplikace [{0}] +hostConfig.docBaseUrlInvalid=Poskytnutou hodnotu docBase nelze vyjádÅ™it jako URL +hostConfig.stop=HostConfig: Zpracování zastaveno + +userConfig.database=Výjimka plnÄ›ní uživatelské databáze +userConfig.error=Chyba nasazení webové aplikace pro uživatele [{0}] +userConfig.start=UserConfig: Zpracování START + +webAnnotationSet.invalidInjection=Neplatná anotace metody pro vložení zdroje. diff --git a/java/org/apache/catalina/startup/LocalStrings_de.properties b/java/org/apache/catalina/startup/LocalStrings_de.properties new file mode 100644 index 0000000..c97193f --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_de.properties @@ -0,0 +1,51 @@ +# 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. + +catalina.serverStartFail=Tomcat kann nicht starten, da eine benötigte Server Komponente Startprobleme hat + +contextConfig.altDDNotFound=alt-dd Datei [{0}] konnte nicht gefunden werden +contextConfig.applicationUrl=Kann die URL für web.xml der Applikation nicht ermitteln +contextConfig.authenticatorConfigured=Es wurde ein Authenticator für Methode [{0}] konfiguriert +contextConfig.contextMissing=Fehlende context.xml: [{0}] +contextConfig.defaultMissing=Keine globale web.xml gefunden +contextConfig.defaultPosition=Vorgekommen in Zeile [{0}] Spalte [{1}] +contextConfig.inputStreamWebResource=Kann Annotationen für Web Ressource [{0}] nicht verarbeiten +contextConfig.jspFile.error=JSP Datei [{0}] muss mit ''/'' beginnen +contextConfig.missingRealm=Es wurde kein Realm konfiguriert gegen den man sich authentifizieren kann +contextConfig.processAnnotationsDir.debug=Durchsuche Verzeichnis nach Klassen mit Annotationen [{0}] +contextConfig.tomcatWebXmlError=Fehler beim Verarbeiten von /WEB-INF/tomcat-web.xml + +engineConfig.start=EngineConfig: verarbeite START +engineConfig.stop=EngineConfig: verarbeite STOP + +expandWar.createFailed=Das Verzeichnis [{0}] kann nicht erstellt werden + +hostConfig.deployDir=Deploye Web-Applikations-Verzeichnis [{0}] +hostConfig.deployWar.error=Fehler beim Deployen des Web-Applikationsarchivs [{0}] +hostConfig.docBaseUrlInvalid=Die angegebene docBase kann nicht als URL ausgedrückt werden +hostConfig.ignorePath=Ignoriere Pfad [{0}] in appBase für das automatische Deployment +hostConfig.jmx.unregister=Unregistrierter Kontext [{0}] fehlgeschlagen +hostConfig.stop=HostConfig: Verarbeitung GESTOPPT + +userConfig.database=Fehler beim Laden der Benutzer Datenbank. +userConfig.error=Fehler beim Deployen einer Web-Applikation für den Benutzer [{0}] +userConfig.start=UserConfig: Verarbeite START + +versionLoggerListener.catalina.base=CATALINA_BASE: {0} +versionLoggerListener.catalina.home=CATALINA_HOME: {0} +versionLoggerListener.os.arch=Architektur: {0} +versionLoggerListener.os.version=OS Version: {0} +versionLoggerListener.serverInfo.server.version=Server Version: {0} +versionLoggerListener.vm.vendor=JVM Hersteller: {0} diff --git a/java/org/apache/catalina/startup/LocalStrings_es.properties b/java/org/apache/catalina/startup/LocalStrings_es.properties new file mode 100644 index 0000000..999926f --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_es.properties @@ -0,0 +1,118 @@ +# 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. + +catalina.configFail=No pude cargar la configuración del servidor desde [{0}] +catalina.serverStartFail=Tomcat no puede iniciar porque el componente Server requerido fallo al iniciar. +catalina.shutdownHookFail=El gancho de apagado ha experimentado un error al intentar para el servidor +catalina.stopServer=No se ha configurado puerto de apagado. Apagando el servidor a través de señal de SO. Servidor no apagado. + +contextConfig.altDDNotFound=fichero alt-dd [{0}] no hallado +contextConfig.applicationMissing=Falta el archivo web.xml de la aplicación. Utilizando los parámetros por defecto +contextConfig.applicationParse=Error de evaluación (parse) en el archivo web.xml de la aplicación a [{0}] +contextConfig.applicationPosition=Se ha producido en la línea [{0}] columna [{1}] +contextConfig.applicationStart=Analizando fichero de aplicación web.xml en [{0}] +contextConfig.applicationUrl=No pude determinar la URL para la aplicación web.xml +contextConfig.authenticatorConfigured=Configuración de un autentificador (authenticator) para el método [{0}] +contextConfig.authenticatorInstantiate=Imposible de instanciar un autenticador (authenticator) para la clase [{0}] +contextConfig.authenticatorMissing=Imposible de configurar un autentificador (authenticator) para el método [{0}] +contextConfig.authenticatorResources=Imposible de cargar la lista de correspondencia de autenticadores (authenticators) +contextConfig.badUrl=No pude procesar descriptor de contextor [{0}] +contextConfig.cce=El objeto de los datos de evento de ciclo de vida (Lifecycle event data object) [{0}] no es un Contexto +contextConfig.contextClose=Error cerrando context.xml: [{0}] +contextConfig.contextMissing=Falta context.xml: [{0}] +contextConfig.contextParse=Error de análisis en context.xml: [{0}] +contextConfig.defaultError=Error al procesar web.xml por defecto con nombre [{0}] en [{1}] +contextConfig.defaultMissing=No se ha hallado web.xml global +contextConfig.defaultPosition=Se ha producido en la línea [{0}] columna [{1}] +contextConfig.destroy=ContextConfig: Destruyendo +contextConfig.fileUrl=No puedo crear un objeto Fichero desde la URL [{0}] +contextConfig.fixDocBase=Excepción arreglando docBase: [{0}] +contextConfig.init=ContextConfig: Inicializando +contextConfig.inputStreamFile=No puedo procesar el fichero [{0}] para las anotaciones +contextConfig.inputStreamJar=No puedo procesar la entrada de Jar [{0}] desde el Jar [{1}] para las anotaciones +contextConfig.inputStreamWebResource=Imposible procesar el recurso web [{0}] para anotaciones +contextConfig.invalidSciHandlesTypes=No puedo cargar la clase [{0}] para revisar contra la anotación @HandlesTypes de uno o más ServletContentInitializers. +contextConfig.jspFile.error=El archivo JSP [{0}] debe de comenzar con ''/'' +contextConfig.jspFile.warning=AVISO: El archivo JSP [{0}] debe de comenzar con ''/'' en Servlet 2.4 +contextConfig.missingRealm=Algún reino (realm) no ha sido configurado para realizar la autenticación +contextConfig.processAnnotationsDir.debug=Escaneando el directorio por archivos de clase con anotaciones [{0}]\n +contextConfig.resourceJarFail=Hallado JAR fallido a los procesos en URL [{0}] para recursos estáticos a ser incluidos en contexto con nombre [{0}] +contextConfig.role.auth=El nombre de papel de seguridad [{0}] es usado en un sin haber sido definido en +contextConfig.role.link=El nombre de papel de seguridad [{0}] es usado en un sin haber sido definido en +contextConfig.role.runas=El nombre de papel de seguridad [{0}] es usado en un sin haber sido definido en +contextConfig.servletContainerInitializerFail=Hallado JAR fallido a proceso en URL [{0}] para ServletContainerInitializers para el contexto con nombre [{1}] +contextConfig.start="ContextConfig": Procesando "START" +contextConfig.stop="ContextConfig": Procesando "STOP" +contextConfig.tomcatWebXmlError=Error procesando /WEB-INF/tomcat-web.xml +contextConfig.unavailable=Esta aplicación está marcada como no disponible debido a los errores precedentes +contextConfig.unknownUrlProtocol=El protocolo de URL [{0}] no fue reconocido durante el proceso de anotaciones. Se ignoró la URL [{1}]. +contextConfig.urlPatternValue=Ambis valores de urlPatterns y atributo fuerno puestos para anotación de [{0}] de la clase [{1}] +contextConfig.xmlSettings=El contexto [{0}] analizará los ficheros web.xml y web-fragment.xml con validación:[{1}] y namespaceAware:[{2}] + +engineConfig.cce=El objeto de los datos de evento de ciclo de vida (Lifecycle event data object) [{0}] no es un motor (engine) +engineConfig.start="EngineConfig": Procesando "START" +engineConfig.stop="EngineConfig": Procesando "STOP" + +expandWar.copy=Error copiando [{0}] a [{1}] +expandWar.createFailed=No se pudo crear el directorio [{0}] +expandWar.deleteFailed=[{0}] no pudo ser completamente borrado. La presencia de los ficheros restantes puede causar problemas +expandWar.illegalPath=The archive [{0}] is malformed and will be ignored: an entry contains an illegal path [{1}] which was not expanded to [{2}] since that is outside of the defined docBase [{3}] + +hostConfig.canonicalizing=Error al borrar redespliegue de recursos desde contexto [{0}] +hostConfig.cce=El objeto de los datos de evento de ciclo de vida (Lifecycle event data object) [{0}] no es una máquina (host) +hostConfig.context.remove=Error al quitar contexto [{0}] +hostConfig.context.restart=Error durante el arranque del contexto [{0}] +hostConfig.createDirs=No puedo crear directorio para despliegue: [{0}] +hostConfig.deployDescriptor=Desplieque del descriptor de configuración [{0}] +hostConfig.deployDescriptor.error=Error durante el despliegue del descriptor de configuración [{0}] +hostConfig.deployDescriptor.localDocBaseSpecified=Se ha especificado un docBase [{0}] dentro del appBase de la máquina y será ignorado +hostConfig.deployDir=Desplegando el directorio [{0}] de la aplicación web +hostConfig.deployDir.error=Error durante el despliegue del directorio [{0}] de la aplicación web +hostConfig.deployWar=Despliegue del archivo [{0}] de la aplicación web +hostConfig.deployWar.error=Error durante el despliegue del archivo [{0}] de la aplicación web +hostConfig.deployWar.hiddenDir=El directorio [{0}] será ignorado debido a que el WAR [{1}] tiene prioridad y unpackWARs es falso +hostConfig.docBaseUrlInvalid=El docBase proporcionado no puede ser expresado como una URL +hostConfig.ignorePath=Ignorando ruta [{0}] en appBase para despliegue automático +hostConfig.illegalWarName=El nombre de war [{0}] es inválido. El archivo será ignorado. +hostConfig.jmx.register=Falló el registro del contexto [{0}] +hostConfig.jmx.unregister=Falló el desregistro del contexto [{0}] +hostConfig.reload=Falló la recarga del contexto [{0}] +hostConfig.start="HostConfig": Procesando "START" +hostConfig.stop="HostConfig": Procesando "STOP" +hostConfig.undeploy=Repliegue (undeploy) de la aplicación web que tiene como trayectoria de contexto [{0}] + +userConfig.database=Excepción durante la carga de base de datos de usuario +userConfig.deploy=Despliegue de la aplicación web para el usuario [{0}] +userConfig.deploying=Desplegando aplicaciones web para el usuario +userConfig.error=Error durante el despliegue de la aplicación web para el usario [{0}] +userConfig.start="UserConfig": Procesando "START" +userConfig.stop="UserConfig": Tratamiento del "STOP" + +versionLoggerListener.arg=Command line argument: {0} +versionLoggerListener.catalina.base=CATALINA_BASE: {0} +versionLoggerListener.catalina.home=CATALINA_HOME: {0} +versionLoggerListener.env=Environment variable: {0} = {1} +versionLoggerListener.java.home=Java Home: {0} +versionLoggerListener.os.arch=Arquitectura: {0} +versionLoggerListener.os.name=OS Name: {0} +versionLoggerListener.os.version=Versión de Systema Operativo: {0} +versionLoggerListener.prop=System property: {0} = {1} +versionLoggerListener.serverInfo.server.built=Server built: {0} +versionLoggerListener.serverInfo.server.number=Número de versión de servidor: {0} +versionLoggerListener.serverInfo.server.version=Nombre de la versión del servidor: {0} +versionLoggerListener.vm.vendor=Vededor JVM: {0} +versionLoggerListener.vm.version=JVM Version: {0} + +webAnnotationSet.invalidInjection=El método de inyección de anotación no es un recurso válido diff --git a/java/org/apache/catalina/startup/LocalStrings_fr.properties b/java/org/apache/catalina/startup/LocalStrings_fr.properties new file mode 100644 index 0000000..cfd849d --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_fr.properties @@ -0,0 +1,195 @@ +# 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. + +catalina.configFail=Impossible de charger la configuration du serveur depuis [{0}] +catalina.destroyFail=Erreur lors de la destruction du serveur ayant échoué +catalina.generatedCodeLocationError=Impossible d''utiliser l''emplacement configuré [{0}] pour générer le code utilisant Tomcat embedded +catalina.incorrectPermissions=Les permissions sont incorrectes, la lecture n'est pas autorisée sur le fichier +catalina.init=L''initialisation du serveur a pris [{0}] millisecondes +catalina.initError=Erreur lors de l'initialisation de Catalina +catalina.initialContextFactory=La propriété système INITIAL_CONTEXT_FACTORY est déjà fixée à [{0}] +catalina.loaderWriteFail=Erreur lors de l'écriture du code de chargement +catalina.namingPrefix=Le préfixe de nom est défini [{0}] +catalina.noCluster=le RuleSet du cluster n''a pas été trouvé à cause de [{0}], la configuration du cluster est désactivée +catalina.noLoader=Le chargeur du code de configuration [{0}] n''a pas été trouvé, le code généré ne sera pas utilisé +catalina.noNaming=L'environnement de noms JNDI est désactivé +catalina.noServer=Impossible de démarrer le serveur, l'instance n'est pas configurée +catalina.serverStartFail=Le composant Server requis n'a pas démarré, en conséquence Tomcat ne peut démarrer. +catalina.shutdownHookFail=Le crochet d'arrêt a rencontré une erreur en tentant d'arrêter le serveur +catalina.startup=Le démarrage du serveur a pris [{0}] millisecondes +catalina.stopError=Erreur lors de l'arrêt de Catalina +catalina.stopServer=Pas de port d'arrêt configuré, l'arrêt du serveur se fera via un signal du système d'exploitation ; le serveur est en cours d'exécution +catalina.stopServer.connectException=Impossible de se connecter à [{0} : {1}] (port de base [{2}] et offset [{3}]), Tomcat peut ne pas être en cours d''exécution +catalina.usage=utilisation : java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -nonaming ] { -help | start | stop } + +catalinaConfigurationSource.cannotObtainURL=Pas de ressource à l''emplacement [{0}] spécifié: ni fichier, ni ressource du classloader, et ce n''est pas une URI pouvant être résolue + +connector.noSetExecutor=Le connecteur [{0}] ne supporte pas les exécuteurs externes, la méthode setExecutor(java.util.concurrent.Executor) n''a pas été trouvée +connector.noSetSSLImplementationName=Le connecteur [{0}] ne supporte pas le changement de l''implémentation SSL, car la méthode setSslImplementationName(String) n''a pas été trouvée + +contextConfig.altDDNotFound=fichier alt-dd [{0}] pas trouvé +contextConfig.annotationsStackOverflow=Impossible de finir l''analyse des annotations de l''application web [{0}] à cause d''une StackOverflowError, les causes possibles sont une valeur trop petite pour -Xss et des dépendances d''héritage cycliques ; la hiérarchie de classe qui était traitée était [{1}] +contextConfig.antiLocking=L''anti verrouillage de fichiers pour le contexte [{0}] change docBase en [{1}] +contextConfig.applicationMissing=Le fichier web.xml de l'application est absent, utilisation des paramètres par défaut +contextConfig.applicationParse=Erreur d''évaluation (parse) dans le fichier web.xml de l''application à [{0}] +contextConfig.applicationPosition=S''est produite à la ligne [{0}] colonne [{1}] +contextConfig.applicationStart=Traitement du fichier web.xml de l''application à [{0}] +contextConfig.applicationUrl=Impossible de déterminer l'URL pour le fichier d'application web.xml +contextConfig.authenticatorConfigured=Configuration d''un authentificateur (authenticator) pour la méthode [{0}] +contextConfig.authenticatorInstantiate=Impossible d''instancier un authentificateur (authenticator) pour la classe [{0}] +contextConfig.authenticatorMissing=Impossible de configurer un authentificateur (authenticator) pour la méthode [{0}] +contextConfig.authenticatorResources=Impossible de charger la liste de correspondance des authentificateurs (authenticators) +contextConfig.badUrl=Impossible de traiter le descripteur de contexte [{0}] +contextConfig.cce=L''objet donnée évènement cycle de vie (Lifecycle event data object) [{0}] n''est pas un Contexte +contextConfig.contextClose=Erreur lors de la fermeture de context.xml +contextConfig.contextMissing=context.xml manquant : [{0}] +contextConfig.contextParse=Erreur de traitement de context.xml pour [{0}] +contextConfig.defaultError=Erreur de traitement du web.xml par défaut appelé [{0}] à [{1}] +contextConfig.defaultMissing=Fichier web.xml global non trouvé +contextConfig.defaultPosition=S''est produite à la ligne [{0}] colonne [{1}] +contextConfig.destroy=ContextConfig : Destruction +contextConfig.effectiveWebXml=web.xml effectif :\n\ +{0} +contextConfig.fileUrl=Impossible de créer un objet fichier à partir de l''URL [{0}] +contextConfig.fixDocBase=Exception durant la fixation du "docBase" pour le contexte [{0}] +contextConfig.init=ContextConfig : Initialisation +contextConfig.inputStreamFile=Impossible de traiter les annotations du fichier [{0}] +contextConfig.inputStreamJar=Impossible de traiter l''entrée [{0}] du JAR [{1}] pour les annotations +contextConfig.inputStreamWebResource=Incapable de traiter les annotations de la ressource web [{0}] +contextConfig.invalidSciHandlesTypes=Impossible de charger la classe [{0}] pour la vérifier avec l''annotation @HandlesTypes d''un ou plusieurs ServletContainerInitializer +contextConfig.jarFile=Impossible de traiter les annotations du JAR [{0}] +contextConfig.jspFile.error=Le fichier JSP [{0}] doit commencer par un ''/'' +contextConfig.jspFile.warning=WARNING : Le fichier JSP [{0}] doit commencer par un ''/'' dans l''API Servlet 2.4 +contextConfig.missingRealm=Aucun royaume (realm) n'a été configuré pour réaliser l'authentification +contextConfig.noAntiLocking=La valeur [{0}] configurée pour java.io.tmpdir ne correspond pas à un répertoire valide, le paramètre antiResourceLocking configuré pour l''application [{1}] sera ignoré +contextConfig.noJsp=Le groupe de propriétés JSP pour l''URL [{0}] sera sauté, le Servlet JSP avec le nom [{1}] n''a pas été trouvé +contextConfig.processAnnotationsDir.debug=Balayage du répertoire pour trouver des fichiers de classe avec annotations [{0}] +contextConfig.processAnnotationsInParallelFailure=L'exécution en parallèle a échoué +contextConfig.processAnnotationsJar.debug=Analyse du fichier jars pour des classes annotées avec [{0}] +contextConfig.processAnnotationsWebDir.debug=Balayage du répertoire d''applications web, pour fichiers de classe avec annotations [{0}] +contextConfig.processContext=Traitement du contexte [{0}] avec la configuration [{1}] +contextConfig.resourceJarFail=Echec du traitement du JAR trouvé à l''URL [{0}] pour les ressources statiques qui devront être incluses dans le contexte avec le nom [{1}] +contextConfig.role.auth=Le nom de rôle de sécurité [{0}] est utilisé dans un sans avoir été défini dans +contextConfig.role.link=Le nom de rôle de sécurité [{0}] est utilisé dans un sans avoir été défini dans +contextConfig.role.runas=Le nom de rôle de sécurité [{0}] est utilisé dans un sans avoir été défini dans +contextConfig.sci.debug=Impossible de traiter le ServletContainerInitializaer pour [{0}], c''est probablement dû au fait que la classe dans l''annotation @HandlesTypes est manquante +contextConfig.sci.info=Impossible de traiter le ServletContainerInitializaer pour [{0}], c''est probablement dû au fait que la classe dans l''annotation @HandlesTypes est manquante (activer le niveau de log DEBUG pour voir la trace complète) +contextConfig.servletContainerInitializerFail=Impossible de détecter les ServletContainerInitializers pour le contexte nommé [{0}] +contextConfig.start=ContextConfig : Traitement du "START" +contextConfig.stop="ContextConfig" : Traitement du "STOP" +contextConfig.tomcatWebXmlError=Echec de traitement de /WEB-INF/tomcat-web.xml +contextConfig.unavailable=Cette application est marquée comme non disponible suite aux erreurs précédentes +contextConfig.unknownUrlProtocol=Le protocole de l''URL [{0}] n''a pas été reconnu pendant le traitement des annotations, l''URL [{1}] a été ignorée +contextConfig.urlPatternValue=A la fois les attributs urlPatterns et la valeur ont été fixés pour l''annotation [{0}] de la classe [{1}] +contextConfig.xmlSettings=Le contexte [{0}] va traiter les fichiers web.xml et le web-fragment.xml avec la validation [{1}] et namespaceAware [{2}] + +engineConfig.cce=L''objet donnée évènement cycle de vie (Lifecycle event data object) [{0}] n''est pas un moteur (engine) +engineConfig.start="EngineConfig" : Traitement du "START" +engineConfig.stop="EngineConfig" : Traitement du "STOP" + +expandWar.copy=Erreur lors de la copie de [{0}] vers [{1}] +expandWar.createFailed=Impossible de créer le répertoire [{0}] +expandWar.createFileFailed=Impossible de créer le fichier [{0}] +expandWar.deleteFailed=[{0}] n''a pas pu être complètement effacé. La présence des fichiers résiduels peut causer des problèmes ultérieurs. +expandWar.deleteOld=Un répertoire décompressé [{0}] a été trouvé avec une date de dernière modification qui ne correspond pas au WAR associé, il sera effacé +expandWar.illegalPath=L''archive [{0}] est invalide est sera ignorée : une entrée contient un chemin invalide [{1}] qui n''a pas été extrait vers [{2}] puisque c''est en dehors du docBase [{3}] qui a été défini +expandWar.lastModifiedFailed=Impossible de fixer la date de dernière modification pour [{0}] +expandWar.missingJarEntry=Impossible d''obtenir un flux d''entrée pour le JarEntry [{0}], le WAR peut être invalide + +failedContext.start=Impossible de traiter le context.xml soit global, par hôte ou spécifique au contexte, donc le contexte [{0}] ne peut pas être démarré + +hostConfig.appBase=La base d''application [{1}] pour le hôte [{0}] n''existe pas ou n''est pas un répertoire. deployOnStartUp et autoDeploy ont été remis à "false" pour éviter des erreurs de déployement. D''autres erreurs peuvent suivre. +hostConfig.canonicalizing=Impossible de déterminer le chemin canonique pour [{0}] pendant qu''on essaie de retirer [{1}] +hostConfig.cce=L''objet donnée évènement cycle de vie (Lifecycle event data object) [{0}] n''est pas un hôte +hostConfig.context.remove=Erreur en enlevant le contexte [{0}] +hostConfig.context.restart=Erreur pendant le redémarrage du contexte [{0}] +hostConfig.createDirs=Impossible de créer un répertoire pour le déploiement : [{0}] +hostConfig.delete=Suppression de la ressource [{0}] lors du rechargement de l''application +hostConfig.deploy.error=Exception lors du déploiement du répertoire [{0}] de l''application web +hostConfig.deployDescriptor=Déploiement du descripteur de configuration [{0}] +hostConfig.deployDescriptor.blocked=L''application web dont le chemin est [{0}] n''a pas été déployée car elle contenait un descripteur de déploiement [{1}] qui pourrait inclure de la configuration nécessaire pour le déploiement sécurisé de l''application mais ce traitement est empêché par le paramètre deployXML pour cet hôte, un descripteur approprié devrait être crée à [{2}] pour déployer cette application +hostConfig.deployDescriptor.error=Erreur lors du déploiement du descripteur de configuration [{0}] +hostConfig.deployDescriptor.finished=Le traitement du descripteur de déploiement [{0}] a pris [{1}] ms +hostConfig.deployDescriptor.hiddenDir=Le déploiement du descripteur [{0}] avec une docBase externe signifie que le répertoire [{1}] qui est dans appBase sera ignoré +hostConfig.deployDescriptor.hiddenWar=Le déploiement du descripteur [{0}] avec une docBase externe signifie que le WAR [{1}] qui est dans appBase sera ignoré +hostConfig.deployDescriptor.localDocBaseSpecified=Un docBase [{0}] dans l''appBase de l''hôte a été spécifié et sera ignoré +hostConfig.deployDescriptor.path=L''attribut path avec la valeur [{0}] dans le descripteur de déploiement [{1}] a été ignoré +hostConfig.deployDescriptor.threaded.error=Erreur en attendant la fin du déploiement de descripteurs en parallèle +hostConfig.deployDir=Déploiement du répertoire d''application web [{0}] +hostConfig.deployDir.error=Erreur lors du déploiement du répertoire [{0}] de l''application web +hostConfig.deployDir.finished=Le déploiement du répertoire [{0}] de l''application web s''est terminé en [{1}] ms +hostConfig.deployDir.threaded.error=Erreur en attendant la fin du déploiement de répertoires en parallèle +hostConfig.deployWar=Déploiement de l''archive [{0}] de l''application web +hostConfig.deployWar.error=Erreur lors du déploiement de l''archive [{0}] de l''application web +hostConfig.deployWar.finished=Le déploiement de l''archive de l''application web [{0}] s''est terminé en [{1}] ms +hostConfig.deployWar.hiddenDir=Le répertoire [{0}] sera ignoré parce que le WAR [{1}] a la priorité et que unpackWARs est faux +hostConfig.deployWar.threaded.error=Erreur en attendant la fin du déploiement de fichiers WAR en parallèle +hostConfig.deploying=Déploiement des applications web trouvées +hostConfig.docBaseUrlInvalid=La "docBase" fournie ne peut pas être exprimée comme URL +hostConfig.expand=Décompression de l''archive [{0}] de l''application web +hostConfig.expand.error=Exception lors de la décompression de l''archive d''application web [{0}] +hostConfig.ignorePath=Le chemin [{0}] est ignoré pour le déploiement automatique dans appBase +hostConfig.illegalWarName=Le nom du war [{0}] est invalide, l''archive sera ignorée +hostConfig.jmx.register=Echec d''enregistrement du contexte [{0}] +hostConfig.jmx.unregister=Le désenregistrement du contexte [{0}] a échoué +hostConfig.migrateApp.threaded.error=Erreur lors de l'attente de la fin de la migration sur des threads multiples des applications legacy +hostConfig.migrateError=Erreur de migration +hostConfig.reload=Rechargement du contexte [{0}] +hostConfig.resourceNotAbsolute=Impossible d''enlever la ressource du contexte [{0}] car [{1}] n''est pas absolu +hostConfig.start="HostConfig" : Traitement du "START" +hostConfig.stop="HostConfig" : Traitement du "STOP" +hostConfig.undeploy=Retrait de l''application web ayant pour chemin de contexte [{0}] +hostConfig.undeployVersion=Retrait de l''ancienne version du contexte [{0}] car elle n''a pas de session active + +listener.createFailed=L''écouteur optionnel [{0}] n''est pas activé +listener.notServer=Ce listener ne peut être ajouté qu''à des éléments Server, mais est dans [{0}] + +passwdUserDatabase.readFail=Echec d'obtention de la liste complète des utilisateurs depuis /etc/passwd + +tomcat.addWebapp.conflictChild=Impossible de déployer le WAR à [{0}] sur le chemin de contexte [{1}] à cause du contexte existant [{2}] +tomcat.addWebapp.conflictFile=Impossible de déployer le WAR à [{0}] sur le chemin de contexte [{1}] à cause du fichier existant [{2}] +tomcat.baseDirMakeFail=Impossible de créer le répertoire [{0}] pour utiliser comme répertoire de base +tomcat.baseDirNotDir=L''emplacement [{0}] spécifié comme répertoire de base n''est pas un répertoire +tomcat.defaultMimeTypeMappingsFail=Impossible de charger les types MIME par défaut +tomcat.homeDirMakeFail=Impossible de créer le répertoire [{0}] pour l''utiliser comme répertoire d''origine +tomcat.invalidCommandLine=Arguments de ligne de commande [{0}] invalides +tomcat.noContextClass=Impossible d''instancier la classe du contexte [{0}] pour l''hôte [{1}] et l''URL [{2}] +tomcat.noContextXml=Impossible de trouver le context.xml de l''application web [{0}] +tomcat.noWrapper=Echec de la création de l'enrobeur (wrapper) + +userConfig.database=Exception lors du chargement de la base de données utilisateur +userConfig.deploy=Déploiement de l''application web pour l''utilisateur [{0}] +userConfig.deploy.threaded.error=Erreur en attendant la fin du déploiement de répertoires utilisateur en parallèle +userConfig.deploying=Déploiement des applications web utilisateur +userConfig.error=Erreur lors du déploiement de l''application web pour l''utilisateur [{0}] +userConfig.start="UserConfig" : Traitement du "START" +userConfig.stop="UserConfig" : Traitement du "STOP" + +versionLoggerListener.arg=Argument de la ligne de commande : {0} +versionLoggerListener.catalina.base=CATALINA_BASE : {0} +versionLoggerListener.catalina.home=CATALINA_HOME : {0} +versionLoggerListener.env=Variable d''environnement : {0} = {1} +versionLoggerListener.java.home=Java Home : {0} +versionLoggerListener.os.arch=Architecture : {0} +versionLoggerListener.os.name=Nom de l''OS : {0} +versionLoggerListener.os.version=Version OS : {0} +versionLoggerListener.prop=Propriété système : {0} = {1} +versionLoggerListener.serverInfo.server.built=Serveur compilé : {0} +versionLoggerListener.serverInfo.server.number=Version du serveur : {0} +versionLoggerListener.serverInfo.server.version=Nom version serveur : {0} +versionLoggerListener.vm.vendor=Fournisseur de la JVM : {0} +versionLoggerListener.vm.version=Version JVM : {0} + +webAnnotationSet.invalidInjection=L'annotation d'injection de ressource de la méthode est invalide diff --git a/java/org/apache/catalina/startup/LocalStrings_ja.properties b/java/org/apache/catalina/startup/LocalStrings_ja.properties new file mode 100644 index 0000000..830776c --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_ja.properties @@ -0,0 +1,196 @@ +# 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. + +catalina.configFail=[{0}]ã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼è¨­å®šã‚’読ã¿è¾¼ã‚ã¾ã›ã‚“ +catalina.destroyFail=障害ãŒç™ºç”Ÿã—㟠server ã®ç ´æ£„エラー +catalina.generatedCodeLocationError=生æˆã•ã‚ŒãŸçµ„è¾¼ã¿Tomcaコードã®æ§‹æˆæ¸ˆã¿ã®ãƒ­ã‚±ãƒ¼ã‚·ãƒ§ãƒ³[{0}]ã®ä½¿ç”¨ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+catalina.incorrectPermissions=権é™ãŒé–“é•ã£ã¦ã„ã¾ã™ã€‚ファイルã«å¯¾ã—ã¦èª­ã¿å–り権é™ãŒè¨±å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +catalina.init=サーãƒãƒ¼ã®åˆæœŸåŒ– [{0}] ミリ秒 +catalina.initError=Catalinaã®åˆæœŸåŒ–エラー +catalina.initialContextFactory=システムプロパティ INITIAL_CONTEXT_FACTORY ã¯ã™ã§ã« [{0}] ã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ +catalina.loaderWriteFail=コードローダã®æ›¸ãè¾¼ã¿ã‚¨ãƒ©ãƒ¼ +catalina.namingPrefix=java.naming.factory.url.pkgs ã« [{0}] を設定 +catalina.noCluster=[{0}]ã®ãŸã‚ã«ã‚¯ãƒ©ã‚¹ã‚¿ãƒ«ãƒ¼ãƒ«ã‚»ãƒƒãƒˆãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。ラスタ構æˆãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™ã€‚ +catalina.noLoader=コンフィグレーションコードローダ [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚生æˆã•ã‚ŒãŸã‚³ãƒ¼ãƒ‰ã¯ä½¿ç”¨ã•ã‚Œã¾ã›ã‚“ +catalina.noNaming=ãƒãƒ¼ãƒŸãƒ³ã‚°ç’°å¢ƒãŒç„¡åŠ¹ã§ã™ã€‚ +catalina.noServer=サーãƒãƒ¼ã‚’èµ·å‹•ã§ãã¾ã›ã‚“。サーãƒãƒ¼ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +catalina.serverStartFail=å¿…è¦ãªã‚µãƒ¼ãƒãƒ¼ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆã‚’開始ã§ããªã„ãŸã‚ã€Tomcat を開始ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +catalina.shutdownHookFail=サーãƒãƒ¼ã®åœæ­¢ä¸­ã«ã‚·ãƒ£ãƒƒãƒˆãƒ€ã‚¦ãƒ³ãƒ•ãƒƒã‚¯ã§ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +catalina.startup=サーãƒãƒ¼ã®èµ·å‹• [{0}] ミリ秒 +catalina.stopError=Catalinaã®åœæ­¢ã‚¨ãƒ©ãƒ¼ +catalina.stopServer=シャットダウンãƒãƒ¼ãƒˆãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã›ã‚“。OSシグナルã§Serverをシャットダウンã—ã¾ã™ã€‚サーãƒã¯ã‚·ãƒ£ãƒƒãƒˆãƒ€ã‚¦ãƒ³ã—ã¾ã›ã‚“。 +catalina.stopServer.connectException=[{0}:{1}] ã«æŽ¥ç¶šã§ãã¾ã›ã‚“ã§ã—㟠(base ãƒãƒ¼ãƒˆ [{2}]ã€offset [{3}])。TomcatãŒå®Ÿè¡Œä¸­ã§ãªã„å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +catalina.usage=使用法:java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -nonaming ] { -help | start | stop } + +catalinaConfigurationSource.cannotObtainURL=指定ã•ã‚ŒãŸå ´æ‰€ã®ãƒªã‚½ãƒ¼ã‚¹ã‚’å–å¾—ã§ãã¾ã›ã‚“[{0}]:読ã¿å–ã‚Šå¯èƒ½ãªãƒ•ã‚¡ã‚¤ãƒ«ã€ã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ãƒ¼ãƒªã‚½ãƒ¼ã‚¹ãŒãªã„ã€ã¾ãŸã¯ã“ã‚Œã¯è§£æ±ºå¯èƒ½ãªURIã§ã¯ã‚ã‚Šã¾ã›ã‚“ + +connector.noSetExecutor=Connector [{0}]ã¯å¤–部エグゼキュータをサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 メソッドsetExecutor(java.util.concurrent.Executor)ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +connector.noSetSSLImplementationName=コãƒã‚¯ã‚¿ãƒ¼ [{0}] 㯠SSL 実装ã®å¤‰æ›´ã«å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“。setSslImplementationName(String) メソッドãŒã‚ã‚Šã¾ã›ã‚“。 + +contextConfig.altDDNotFound=代替é…備記述å­ãƒ•ã‚¡ã‚¤ãƒ« [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +contextConfig.annotationsStackOverflow=StackOverflowErrorã®ãŸã‚ã€Webアプリケーション [{0}] ã®ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã®ã‚¹ã‚­ãƒ£ãƒ³ã‚’完了ã§ãã¾ã›ã‚“。考ãˆã‚‰ã‚Œã‚‹æ ¹æœ¬çš„ãªåŽŸå› ã«ã¯ã€-Xssã®è¨­å®šãŒä½Žã™ãŽã‚‹äº‹ã‚„ä¸æ­£ãªå¾ªç’°ç¶™æ‰¿ãŒè€ƒãˆã‚‰ã‚Œã¾ã™ã€‚処ç†ä¸­ã®ã‚¯ãƒ©ã‚¹éšŽå±¤ã¯ [{1}] ã§ã—㟠+contextConfig.antiLocking=コンテキスト [{0}] ã®ã‚¢ãƒ³ãƒãƒ­ãƒƒã‚¯ãŒ docBase ã‚’ [{1}] ã«è¨­å®šã—ã¦ã„ã¾ã™ +contextConfig.applicationMissing=アプリケーションã®web.xmlãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã€æ—¢å®šå€¤ã®ã¿ã‚’使用ã—ã¾ã™ +contextConfig.applicationParse=アプリケーションã®web.xmlファイル [{0}] ã®è§£æžã‚¨ãƒ©ãƒ¼ã§ã™ +contextConfig.applicationPosition=[{0}] 行㮠[{1}] 列目ã§ç™ºç”Ÿã—ã¾ã—㟠+contextConfig.applicationStart=アプリケーションã®web.xml [{0}] を解æžã—ã¾ã™ã€‚ +contextConfig.applicationUrl=アプリケーション web.xml ã® URL ã‚’å–å¾—ã§ãã¾ã›ã‚“ +contextConfig.authenticatorConfigured=メソッド [{0}] ã®ã‚ªãƒ¼ã‚»ãƒ³ãƒ†ã‚£ã‚±ãƒ¼ã‚¿ã‚’設定ã—ã¾ã™ +contextConfig.authenticatorInstantiate=クラス [{0}] ã®ã‚ªãƒ¼ã‚»ãƒ³ãƒ†ã‚£ã‚±ãƒ¼ã‚¿ã‚’インスタンス化ã§ãã¾ã›ã‚“ +contextConfig.authenticatorMissing=メソッド [{0}] ã®ã‚ªãƒ¼ã‚»ãƒ³ãƒ†ã‚£ã‚±ãƒ¼ã‚¿ã‚’設定ã§ãã¾ã›ã‚“ +contextConfig.authenticatorResources=Authenticators ã®ãƒžãƒƒãƒ—リストをロードã§ãã¾ã›ã‚“。 +contextConfig.badUrl=ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆè¨˜è¿°å­ [{0}] を処ç†ã§ãã¾ã›ã‚“。 +contextConfig.cce=ライフサイクルイベントデータオブジェクト [{0}] ã¯ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã§ã¯ã‚ã‚Šã¾ã›ã‚“ +contextConfig.contextClose=context.xmlã‚’é–‰ã˜ã‚‹éš›ã®ã‚¨ãƒ©ãƒ¼ +contextConfig.contextMissing=context.xml ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“: [{0}] +contextConfig.contextParse=[{0}]ã®context.xmlã®è§£æžã‚¨ãƒ©ãƒ¼ +contextConfig.defaultError=[{1}] 㧠[{0}] ã¨ã„ã†åå‰ã®æ—¢å®šweb.xmlã®å‡¦ç†ã§ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+contextConfig.defaultMissing=グローãƒãƒ« web.xml ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +contextConfig.defaultPosition=[{0}] 行㮠[{1}] 列目ã§ç™ºç”Ÿã—ã¾ã—㟠+contextConfig.destroy=ContextConfig:破棄中 +contextConfig.effectiveWebXml=有効ãªweb.xml:\n\ +{0} +contextConfig.fileUrl=URL [{0}] ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚ªãƒ–ジェクトを作æˆã§ãã¾ã›ã‚“ +contextConfig.fixDocBase=コンテキスト [{0}] ã® docBase を修復中ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +contextConfig.init=ContextConfig: åˆæœŸåŒ–中\n +contextConfig.inputStreamFile=ファイル [{0}] ã®ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã‚’処ç†ã§ãã¾ã›ã‚“ +contextConfig.inputStreamJar=Jar [{1}] ã® Jar エントリ [{0}] ã®ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã‚’処ç†ã§ãã¾ã›ã‚“ +contextConfig.inputStreamWebResource=Web リソース [{0}] ã®ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã‚’処ç†ã§ãã¾ã›ã‚“ +contextConfig.invalidSciHandlesTypes=1ã¤ä»¥ä¸Šã®ServletContentInitializersã®@HandlesTypesアノテーションã«å¯¾ã—ã¦ãƒã‚§ãƒƒã‚¯ã™ã‚‹ãŸã‚ã«ã‚¯ãƒ©ã‚¹ [{0}] をロードã§ãã¾ã›ã‚“。 +contextConfig.jarFile=Jar [{0}] ã®ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã‚’処ç†ã§ãã¾ã›ã‚“ +contextConfig.jspFile.error=JSPファイル [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +contextConfig.jspFile.warning=警告: Servlet 2.4ã§ã¯JSPファイル [{0}] ã¯''/''ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +contextConfig.missingRealm=èªè¨¼ã™ã‚‹ãŸã‚ã«ãƒ¬ãƒ«ãƒ ãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ +contextConfig.noAntiLocking=java.io.tmpdirã«è¨­å®šã•ã‚ŒãŸå€¤[{0}]ãŒæœ‰åŠ¹ãªãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’指ã—ã¦ã„ã¾ã›ã‚“。 Webアプリケーション[{1}]ã®antiResourceLocking設定ã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +contextConfig.noJsp=URL [{0}] ã® JSP プロパティ グループをスキップã—ã¦ã„ã¾ã™ã€‚åå‰ [{1}] ã® JSP サーブレットãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +contextConfig.processAnnotationsDir.debug=[{0}] é…下ã®ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‹ã‚‰ã‚¯ãƒ©ã‚¹ãƒ•ã‚¡ã‚¤ãƒ«ã‚’スキャンã—ã¾ã™ +contextConfig.processAnnotationsInParallelFailure=並列実行ã«å¤±æ•—ã—ã¾ã—㟠+contextConfig.processAnnotationsJar.debug=アノテーション付ãã®ã‚¯ãƒ©ã‚¹ãƒ•ã‚¡ã‚¤ãƒ«ã®jarファイルã®ã‚¹ã‚­ãƒ£ãƒ³[{0}] +contextConfig.processAnnotationsWebDir.debug=アノテーション[{0}]ã®ã‚¯ãƒ©ã‚¹ãƒ•ã‚¡ã‚¤ãƒ«ã®Webアプリケーションディレクトリã®ã‚¹ã‚­ãƒ£ãƒ³ +contextConfig.processContext=コンテキスト [{0}] ã‚’æ§‹æˆ [{1}] ã§å‡¦ç†ã—ã¦ã„ã¾ã™ +contextConfig.resourceJarFail=コンテキスト [{1}] ã¸é™çš„リソースã¨ã—ã¦å«ã‚ã‚‹ URL [{0}] ã«é…ç½®ã•ã‚ŒãŸ JAR ファイルを処ç†ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +contextConfig.role.auth=ã«å®šç¾©ã•ã‚Œã¦ã„ãªã„セキュリティロールå [{0}] ãŒã®ä¸­ã§ä½¿ç”¨ã•ã‚Œã¾ã—㟠+contextConfig.role.link=ã«å®šç¾©ã•ã‚Œã¦ã„ãªã„セキュリティロールå [{0}] ãŒã®ä¸­ã§ä½¿ç”¨ã•ã‚Œã¾ã—㟠+contextConfig.role.runas=ã«å®šç¾©ã•ã‚Œã¦ã„ãªã„セキュリティロールå [{0}] ãŒã®ä¸­ã§ä½¿ç”¨ã•ã‚Œã¾ã—㟠+contextConfig.sci.debug=[{0}]ã«å¯¾ã—ã¦ServletContainerInitializerを処ç†ã§ãã¾ã›ã‚“。@HandlesTypesアノテーションã§å®šç¾©ã•ã‚Œã¦ã„るクラスãŒè¦‹ã¤ã‹ã‚‰ãªã„ã“ã¨ãŒåŽŸå› ã§ã™ +contextConfig.sci.info=[{0}]ã«å¯¾ã—ã¦ServletContainerInitializerを処ç†ã§ãã¾ã›ã‚“。 ã“ã‚Œã¯@HandlesTypesアノテーションã«å®šç¾©ã•ã‚Œã¦ã„るクラスãŒè¦‹ã¤ã‹ã‚‰ãªã„ã“ã¨ãŒåŽŸå› ã§ã™ã€‚ 完全ãªã‚¹ã‚¿ãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹ã®DEBUGレベルロギングを有効ã«ã—ã¾ã™ã€‚ +contextConfig.servletContainerInitializerFail=åå‰[{0}]ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã®ServletContainerInitializersã®æ¤œå‡ºã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +contextConfig.start=ContextConfig: 処ç†ã‚’開始ã—ã¾ã™ +contextConfig.stop=ContextConfig: 処ç†ã‚’åœæ­¢ã—ã¾ã™ +contextConfig.tomcatWebXmlError=/WEB-INF/tomcat-web.xml 処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +contextConfig.unavailable=以å‰ã®ã‚¨ãƒ©ãƒ¼ã®ãŸã‚ã«ã“ã®ã‚¢ãƒ—リケーションã¯åˆ©ç”¨ã§ããªã„よã†ã«ãƒžãƒ¼ã‚¯ã—ã¾ã™ +contextConfig.unknownUrlProtocol=アノテーション処ç†ä¸­ã«URLプロトコル[{0}]ãŒèªè­˜ã•ã‚Œã¾ã›ã‚“ã§ã—ãŸã€‚ URL [{1}]ã¯ç„¡è¦–ã•ã‚Œã¾ã—ãŸã€‚ +contextConfig.urlPatternValue=urlPatterns属性ã¨value属性ã®ä¸¡æ–¹ãŒã€ã‚¯ãƒ©ã‚¹ [{1}] ã® [{0}] アノテーションã«å¯¾ã—ã¦è¨­å®šã•ã‚Œã¦ã„ã¾ã™ +contextConfig.xmlSettings=Context[{0}]ã¯ã€validation:[{1}]ãŠã‚ˆã³namespaceAware:[{2}]を使用ã—ã¦web.xmlãŠã‚ˆã³web-fragment.xmlファイルを解æžã—ã¾ã™ + +engineConfig.cce=ライフサイクルイベントデータオブジェクト [{0}] ã¯ã‚¨ãƒ³ã‚¸ãƒ³ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +engineConfig.start=EngineConfig: 処ç†ã‚’開始ã—ã¾ã™ +engineConfig.stop=EngineConfig: 処ç†ã‚’åœæ­¢ã—ã¾ã™ + +expandWar.copy=[{0}] ã‹ã‚‰ [{1}] ã¸ã®ã‚³ãƒ”ー中ã®ã‚¨ãƒ©ãƒ¼ +expandWar.createFailed=ディレクトリ [{0}] を作æˆã§ãã¾ã›ã‚“ +expandWar.createFileFailed=ファイル [{0}] を作æˆã§ãã¾ã›ã‚“ +expandWar.deleteFailed=[{0}] を削除ã§ãã¾ã›ã‚“。残ã£ãŸãƒ•ã‚¡ã‚¤ãƒ«ã«ã‚ˆã‚Šå•é¡ŒãŒç”Ÿã˜ã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。 +expandWar.deleteOld=関連付ã‘られ㟠WAR ファイルã¨æœ€çµ‚更新日時ã®ç•°ãªã‚‹å±•é–‹å…ˆãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚削除ã—ã¾ã™ã€‚ +expandWar.illegalPath=アーカイブ [{0}] ã¯å½¢å¼ãŒæ­£ã—ããªã„ãŸã‚無視ã•ã‚Œã¾ã™: 定義ã•ã‚ŒãŸdocBase [{3}] ã®å¤–ã«ã‚ã‚‹ã®ã§ã€[{2}] ã«æ‹¡å¼µã•ã‚Œã¦ã„ãªã„ä¸æ­£ãªãƒ‘ス [{1}] ãŒã‚¨ãƒ³ãƒˆãƒªã«å«ã¾ã‚Œã¦ã„ã¾ã™ +expandWar.lastModifiedFailed=[{0}] ã«æœ€çµ‚更新時刻を設定ã§ãã¾ã›ã‚“ +expandWar.missingJarEntry=JarEntry [{0}] ã®å…¥åŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’å–å¾—ã§ãã¾ã›ã‚“。WAR ファイルãŒç ´æã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ + +failedContext.start=グローãƒãƒ«ã€ãƒ›ã‚¹ãƒˆå˜ä½ã¾ãŸã¯ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆå›ºæœ‰ã®context.xmlファイルã®å‡¦ç†ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ã—ãŸãŒã£ã¦ã€ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] を開始ã§ãã¾ã›ã‚“。 + +hostConfig.appBase=ホスト [{0}] ã®ã‚¢ãƒ—リケーションベースディレクトリ [{1}] ã¯å­˜åœ¨ã—ãªã„ã‹ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã§ã¯ã‚ã‚Šã¾ã›ã‚“。é…備エラーを防ããŸã‚ deployOnStartUp ãŠã‚ˆã³ autoDeploy 㯠false ã«è¨­å®šã—ã¾ã—ãŸã€‚ä»–ã®ã‚¨ãƒ©ãƒ¼ã¯å¼•ã続ã発生ã™ã‚‹ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。 +hostConfig.canonicalizing=[{1}] ã®é…備解除中㫠[{0}] ã®æ­£è¦åŒ–パスをå–å¾—ã§ãã¾ã›ã‚“ +hostConfig.cce=ライフサイクルイベントデータオブジェクト [{0}] ã¯ãƒ›ã‚¹ãƒˆã§ã¯ã‚ã‚Šã¾ã›ã‚“ +hostConfig.context.remove=コンテキスト [{0}] ã®å‰Šé™¤ä¸­ã«ç•°å¸¸ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +hostConfig.context.restart=コンテキスト [{0}] ã‚’å†èµ·å‹•ä¸­ã®ã‚¨ãƒ©ãƒ¼ +hostConfig.createDirs=é…備用ã®ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’作æˆã§ãã¾ã›ã‚“:[{0}] +hostConfig.delete=アプリケーションã®ãƒªãƒ­ãƒ¼ãƒ‰ä¸­ã«ãƒªã‚½ãƒ¼ã‚¹ [{0}] を削除ã—ã¾ã™ +hostConfig.deploy.error=Webアプリケーションディレクトリ[{0}]ã®é…備中ã®ä¾‹å¤– +hostConfig.deployDescriptor=é…å‚™è¨˜è¿°å­ [{0}] ã‚’é…å‚™ã—ã¾ã™ +hostConfig.deployDescriptor.blocked=コンテキストパス [{0}] ã‚’æŒã¤Webアプリケーションã¯ã€ã‚¢ãƒ—リケーションã®ã‚»ã‚­ãƒ¥ã‚¢ãªé…å‚™ã«å¿…è¦ãªè¨­å®šãŒå«ã¾ã‚Œã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ãŒã€ã“ã®ãƒ›ã‚¹ãƒˆã®deployXML設定ã«ã‚ˆã£ã¦å‡¦ç†ãŒå¦¨ã’られるé…å‚™è¨˜è¿°å­ [{1}] ãŒå«ã¾ã‚Œã¦ã„ãŸãŸã‚é…å‚™ã•ã‚Œã¦ã„ã¾ã›ã‚“。ã“ã®ã‚¢ãƒ—リケーションをé…å‚™ã™ã‚‹ã«ã¯ã€[{2}] ã«é©åˆ‡ãªè¨˜è¿°å­ã‚’作æˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +hostConfig.deployDescriptor.error=é…å‚™è¨˜è¿°å­ [{0}] ã‚’é…備中ã®ã‚¨ãƒ©ãƒ¼ã§ã™ +hostConfig.deployDescriptor.finished=é…å‚™è¨˜è¿°å­ [{0}] ã®å±•é–‹ãŒ [{1}] ミリ秒ã§å®Œäº†ã—ã¾ã—㟠+hostConfig.deployDescriptor.hiddenDir=外部ã®docBaseを使用ã—ãŸé…å‚™è¨˜è¿°å­ [{0}] ã®é…å‚™ã¯ã€appBaseã® [{1}] ディレクトリãŒç„¡è¦–ã•ã‚Œã‚‹ã“ã¨ã‚’æ„味ã—ã¾ã™ +hostConfig.deployDescriptor.hiddenWar=é…å‚™è¨˜è¿°å­ [{0}] を外部ã®docBaseã¨ã¨ã‚‚ã«é…å‚™ã™ã‚‹ã¨appBaseã®WAR [{1}] ãŒç„¡è¦–ã•ã‚Œã¾ã™ +hostConfig.deployDescriptor.localDocBaseSpecified=docBase [{0}] ã¯ãƒ›ã‚¹ãƒˆã® appBase ã«å«ã¾ã‚Œã‚‹ãŸã‚無視ã—ã¾ã™ã€‚ +hostConfig.deployDescriptor.path=é…å‚™è¨˜è¿°å­ [{1}] ã® path 属性ã«æŒ‡å®šã•ã‚ŒãŸ [{0}] ã¯ç„¡è¦–ã•ã‚Œã¾ã—㟠+hostConfig.deployDescriptor.threaded.error=é…備記述å­ã®ãƒžãƒ«ãƒã‚¹ãƒ¬ãƒƒãƒ‰é…å‚™ã®å®Œäº†å¾…機中ã®ã‚¨ãƒ©ãƒ¼ +hostConfig.deployDir=Web アプリケーションディレクトリ [{0}] ã‚’é…å‚™ã—ã¾ã™ +hostConfig.deployDir.error=Webアプリケーションディレクトリ [{0}] ã‚’é…備中ã®ã‚¨ãƒ©ãƒ¼ +hostConfig.deployDir.finished=ディレクトリ [{0}] ã® Web アプリケーションã®é…備㯠[{1}] ミリ秒ã§å®Œäº†ã—ã¾ã—ãŸã€‚ +hostConfig.deployDir.threaded.error=ディレクトリã®ãƒžãƒ«ãƒã‚¹ãƒ¬ãƒƒãƒ‰é…å‚™ã®\n\ +完了待機中ã®ã‚¨ãƒ©ãƒ¼ +hostConfig.deployWar=Webアプリケーションアーカイブ [{0}] ã‚’é…å‚™ã—ã¾ã™ +hostConfig.deployWar.error=Webアプリケーションアーカイブ [{0}] ã‚’é…備中ã®ã‚¨ãƒ©ãƒ¼ +hostConfig.deployWar.finished=Web アプリケーションアーカイブ [{0}] ã®é…備㯠[{1}] ミリ秒ã§å®Œäº†ã—ã¾ã—㟠+hostConfig.deployWar.hiddenDir=WAR [{1}] ãŒå„ªå…ˆã•ã‚Œã€unpackWARs ㌠false ã§ã‚ã‚‹ãŸã‚ã€ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª [{0}] ã¯ç„¡è¦–ã•ã‚Œã¾ã™ +hostConfig.deployWar.threaded.error=WARファイルã®ãƒžãƒ«ãƒã‚¹ãƒ¬ãƒƒãƒ‰é…å‚™ã®å®Œäº†å¾…機中ã®ã‚¨ãƒ©ãƒ¼ +hostConfig.deploying=発見ã•ã‚ŒãŸWebアプリケーションã®é…å‚™ +hostConfig.docBaseUrlInvalid=docBase ã«æŒ‡å®šã•ã‚ŒãŸæ–‡å­—列㯠URL ã¨ã—ã¦è§£é‡ˆã§ãã¾ã›ã‚“ +hostConfig.expand=Web アプリケーションアーカイブ [{0}] を展開ã—ã¾ã™ã€‚ +hostConfig.expand.error=Web アプリケーションアーカイブ [{0}] ã®å±•é–‹ä¸­ã«ç•°å¸¸ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +hostConfig.ignorePath=自動é…å‚™ã§ã¯ appBase 内ã®ãƒ‘ス [{0}] を無視ã—ã¾ã™ +hostConfig.illegalWarName=Warå[{0}]ã¯ç„¡åŠ¹ã§ã™ã€‚アーカイブã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +hostConfig.jmx.register=コンテキスト [{0}] を登録ã§ãã¾ã›ã‚“ã§ã—㟠+hostConfig.jmx.unregister=コンテキスト [{0}] ã®ç™»éŒ²ã‚’解除ã§ãã¾ã›ã‚“ã§ã—㟠+hostConfig.migrateApp.threaded.error=レガシーアプリケーションã®ãƒžãƒ«ãƒã‚¹ãƒ¬ãƒƒãƒ‰ãƒžã‚¤ã‚°ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãŒå®Œäº†ã™ã‚‹ã®ã‚’待機中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+hostConfig.migrateError=マイグレーション失敗 +hostConfig.reload=リロード中ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] +hostConfig.resourceNotAbsolute=[{1}] ã¯å®Œå…¨ãƒ‘スã§ã¯ãªã„ãŸã‚コンテキスト [{0}] ã‹ã‚‰ãƒªã‚½ãƒ¼ã‚¹ã‚’削除ã§ãã¾ã›ã‚“ +hostConfig.start=HostConfig: 処ç†ã‚’åœæ­¢ã—ã¾ã™ +hostConfig.stop=HostConfig: 処ç†ã‚’åœæ­¢ã—ã¾ã™ +hostConfig.undeploy=コンテキストパス [{0}] ã®Webアプリケーションã®é…備を解除ã—ã¾ã™ +hostConfig.undeployVersion=コンテキスト [{0}] ã«ã¤ã„ã¦æœ‰åŠ¹ãªã‚»ãƒƒã‚·ãƒ§ãƒ³ã®å­˜åœ¨ã—ãªã„å¤ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®é…備を解除ã—ã¾ã™ã€‚ + +listener.createFailed=オプションã®ãƒªã‚¹ãƒŠãƒ¼ [{0}] ã¯æœ‰åŠ¹åŒ–ã•ã‚Œã¦ã„ã¾ã›ã‚“ +listener.notServer=ã“ã®listenerã¯Serverè¦ç´ å†…ã«ã®ã¿ãƒã‚¹ãƒˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ãŒã€[{0}] ã«ã‚ã‚Šã¾ã™ã€‚ + +passwdUserDatabase.readFail=/etc/passwd ã‹ã‚‰å…¨ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚»ãƒƒãƒˆã‚’å–å¾—ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ + +tomcat.addWebapp.conflictChild=コンテキスト [{2}] ãŒå­˜åœ¨ã™ã‚‹ãŸã‚WAR ファイル [{0}] をコンテキストパス [{1}] ã¸é…å‚™ã§ãã¾ã›ã‚“。 +tomcat.addWebapp.conflictFile=ファイル [{2}] ãŒå­˜åœ¨ã™ã‚‹ãŸã‚ WAR ファイル [{0}] をコンテキストパス [{1}] ã¸é…å‚™ã§ãã¾ã›ã‚“。 +tomcat.baseDirMakeFail=ベースディレクトリã¨ã—ã¦ä½¿ç”¨ã™ã‚‹ [{0}] を作æˆã§ãã¾ã›ã‚“ +tomcat.baseDirNotDir=ベースディレクトリã«æŒ‡å®šã•ã‚ŒãŸ [{0}] ã¯ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +tomcat.defaultMimeTypeMappingsFail=既定㮠MIME タイプを読ã¿è¾¼ã‚ã¾ã›ã‚“ +tomcat.homeDirMakeFail=ホームディレクトリã¨ã—ã¦ä½¿ç”¨ã™ã‚‹ [{0}] を作æˆã§ãã¾ã›ã‚“。 +tomcat.invalidCommandLine=無効ãªã‚³ãƒžãƒ³ãƒ‰ãƒ©ã‚¤ãƒ³å¼•æ•°[{0}] +tomcat.noContextClass=ホスト [{1}] ã¨URL [{2}] ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚¯ãƒ©ã‚¹ [{0}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹åŒ–ã«å¤±æ•—ã—ã¾ã—㟠+tomcat.noContextXml=Webアプリケーションcontext.xml [{0}]を特定ã§ãã¾ã›ã‚“ +tomcat.noWrapper=ラッパã®ä½œæˆã«å¤±æ•—ã—ã¾ã—㟠+ +userConfig.database=ユーザデータベースã®ãƒ­ãƒ¼ãƒ‰ä¸­ã®ä¾‹å¤–ã§ã™ +userConfig.deploy=ユーザ [{0}] ã®Webアプリケーションをé…å‚™ã—ã¾ã™ +userConfig.deploy.threaded.error=ユーザーディレクトリã®ãƒžãƒ«ãƒã‚¹ãƒ¬ãƒƒãƒ‰é…å‚™ã®å®Œäº†å¾…機中ã®ã‚¨ãƒ©ãƒ¼ +userConfig.deploying=ユーザã®Webアプリケーションをé…å‚™ã—ã¾ã™ +userConfig.error=ユーザ [{0}] ã®Webアプリケーションをé…備中ã®ã‚¨ãƒ©ãƒ¼ +userConfig.start=UserConfig: 処ç†ã‚’開始ã—ã¾ã™ +userConfig.stop=UserConfig: 処ç†ã‚’åœæ­¢ã—ã¾ã™ + +versionLoggerListener.arg=コマンドライン引数: {0} +versionLoggerListener.catalina.base=CATALINA_BASE: {0} +versionLoggerListener.catalina.home=CATALINA_HOME: {0} +versionLoggerListener.env=環境変数: {0} = {1} +versionLoggerListener.java.home=Java Home: {0} +versionLoggerListener.os.arch=アーキテクãƒãƒ£: {0} +versionLoggerListener.os.name=OS å: {0} +versionLoggerListener.os.version=OS ãƒãƒ¼ã‚¸ãƒ§ãƒ³: {0} +versionLoggerListener.prop=システムプロパティ: {0} = {1} +versionLoggerListener.serverInfo.server.built=Server ビルド: {0} +versionLoggerListener.serverInfo.server.number=サーãƒãƒ¼ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ç•ªå·: {0} +versionLoggerListener.serverInfo.server.version=Serverã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³å: {0} +versionLoggerListener.vm.vendor=JVM ベンダ: {0} +versionLoggerListener.vm.version=JVM ãƒãƒ¼ã‚¸ãƒ§ãƒ³: {0} + +webAnnotationSet.invalidInjection=メソッドã«ç„¡åŠ¹ãªãƒªã‚½ãƒ¼ã‚¹æ³¨å…¥ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ diff --git a/java/org/apache/catalina/startup/LocalStrings_ko.properties b/java/org/apache/catalina/startup/LocalStrings_ko.properties new file mode 100644 index 0000000..967dc36 --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_ko.properties @@ -0,0 +1,186 @@ +# 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. + +catalina.configFail=[{0}](으)로부터 서버 ì„¤ì •ì„ ë¡œë“œí•  수 없습니다. +catalina.generatedCodeLocationError=설정 파ì¼ë“¤ë¡œë¶€í„° 톰캣 임베디드 코드를 ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ (코드 ìƒì„± 위치: [{0}]) +catalina.incorrectPermissions=권한 ì„¤ì •ë“¤ì´ ì˜¬ë°”ë¥´ì§€ 않습니다. 해당 파ì¼ì— ì½ê¸° ê¶Œí•œì´ í—ˆìš©ë˜ì§€ 않습니다. +catalina.init=[{0}] 밀리초 ë‚´ì— ì„œë²„ê°€ 초기화ë˜ì—ˆìŠµë‹ˆë‹¤. +catalina.initError=Catalina를 초기화하는 중 오류 ë°œìƒ +catalina.noCluster=[{0}](으)ë¡œ ì¸í•˜ì—¬ í´ëŸ¬ìŠ¤í„° RuleSetì„ ì°¾ì„ ìˆ˜ 없습니다. í´ëŸ¬ìŠ¤í„° ì„¤ì •ì€ ì‚¬ìš©ë¶ˆëŠ¥ ìƒíƒœìž…니다. +catalina.noLoader=설정 íŒŒì¼ ì½”ë“œ ë¡œë” [{0}]ì´(ê°€) 발견ë˜ì§€ 않습니다. ìƒì„±ëœ 코드는 사용ë˜ì§€ ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. +catalina.noNaming=Naming í™˜ê²½ì€ ì‚¬ìš© 불능 ìƒíƒœìž…니다. +catalina.noServer=서버를 시작할 수 없습니다. 서버 ì¸ìŠ¤í„´ìŠ¤ê°€ 설정ë˜ì§€ 않았습니다. +catalina.serverStartFail=필수 í•­ëª©ì¸ ì„œë²„ 구성요소가 제대로 시작ë˜ì§€ 못하여, Tomcatì´ ì‹œìž‘ë  ìˆ˜ 없습니다. +catalina.shutdownHookFail=서버를 중지시키려는 과정ì—ì„œ, 셧다운 í›…ì—ì„œ 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. +catalina.startup=서버가 [{0}] 밀리초 ë‚´ì— ì‹œìž‘ë˜ì—ˆìŠµë‹ˆë‹¤. +catalina.stopError=Catalina를 중지시키는 중 오류 ë°œìƒ +catalina.stopServer=셧다운 í¬íŠ¸ê°€ 설정ë˜ì§€ 않았습니다. OS 시그ë„ì„ í†µí•´ 서버를 셧다운합니다. 서버는 ì•„ì§ ì…§ë‹¤ìš´ë˜ì§€ 않았습니다. +catalina.stopServer.connectException=[{0}:{1}] (base í¬íŠ¸ [{2}] 그리고 offset [{3}])와(ê³¼) ì—°ê²°í•  수 없었습니다. Tomcatì´ ì‹¤í–‰ 중ì´ì§€ ì•Šì„ ìˆ˜ 있습니다. +catalina.usage=사용법: java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -nonaming ] { -help | start | stop } + +catalinaConfigurationSource.cannotObtainURL=ì§€ì •ëœ ìœ„ì¹˜ [{0}]ì—ì„œ 리소스를 ì½ì„ 수 없습니다: ì½ì„ 수 없는 파ì¼ì´ê±°ë‚˜, ì½ì„ 수 없는 í´ëž˜ìŠ¤ë¡œë” 리소스, ë˜ëŠ” ê²°ì •í•  수 없는 URI입니다. + +connector.noSetExecutor=Connector [{0}]ì€(는) 외부 Executorë“¤ì„ ì§€ì›í•˜ì§€ 않습니다. 메소드 setExecutor(java.util.concurrent.Executor)를 ì°¾ì„ ìˆ˜ 없습니다. +connector.noSetSSLImplementationName=Connector [{0}]ì€(는) SSL êµ¬í˜„ì„ ë³€ê²½í•˜ëŠ” ê²ƒì„ ì§€ì›í•˜ì§€ 않습니다. setSslImplementationName(String) 메소드를 ì°¾ì„ ìˆ˜ 없습니다. + +contextConfig.altDDNotFound=alt-dd íŒŒì¼ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +contextConfig.annotationsStackOverflow=StackOverflowErrorë¡œ ì¸í•˜ì—¬, 웹 애플리케ì´ì…˜ [{0}]ì—ì„œ annotation ìŠ¤ìº”ì„ ì™„ë£Œí•˜ì§€ 못했습니다. 가능성 있는 근본 ì›ì¸(root cause)들 ì¤‘ì˜ í•˜ë‚˜ëŠ” -Xssê°€ 너무 ì ê²Œ 설정ë˜ì–´ 있거나 ë¶ˆí—ˆëœ ìˆœí™˜ ìƒì† ì˜ì¡´ê´€ê³„ë“¤ì¼ ìˆ˜ 있습니다. 처리ë˜ëŠ” í´ëž˜ìŠ¤ì˜ ìƒì† 계층구조는 [{1}]입니다. +contextConfig.applicationMissing=애플리케ì´ì…˜ web.xmlì´ ì—†ìŠµë‹ˆë‹¤. 기본 ì„¤ì •ë“¤ë§Œì„ ì‚¬ìš©í•  것입니다. +contextConfig.applicationParse=[{0}]ì— ìœ„ì¹˜í•œ 애플리케ì´ì…˜ web.xmlì—ì„œ 파싱 오류 ë°œìƒ +contextConfig.applicationPosition=[{0}]í–‰ [{1}]ì—´ì—ì„œ ë°œìƒí–ˆìŒ +contextConfig.applicationStart=[{0}]ì— ìœ„ì¹˜í•œ 애플리케ì´ì…˜ web.xmlì„ íŒŒì‹±í•©ë‹ˆë‹¤. +contextConfig.applicationUrl=애플리케ì´ì…˜ web.xmlì˜ URLì„ ê²°ì •í•  수 없습니다. +contextConfig.authenticatorConfigured=메소드 [{0}]ì„(를) 위한 Authenticator를 설정했습니다. +contextConfig.authenticatorInstantiate=í´ëž˜ìŠ¤ [{0}]ì˜ Authenticator ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•  수 없습니다, +contextConfig.authenticatorMissing=ì¸ì¦ 메소드 [{0}]ì„(를) 위한 Authenticator를 설정할 수 없습니다. +contextConfig.authenticatorResources=Authenticatorë“¤ì˜ ë§¤í•‘ 목ë¡ì„ 로드할 수 없습니다. +contextConfig.badUrl=컨í…스트 descriptor [{0}]ì„(를) 처리할 수 없습니다. +contextConfig.cce=Lifecycle ì´ë²¤íŠ¸ ë°ì´í„° ê°ì²´ [{0}]ì´(ê°€) Context ê°ì²´ê°€ 아닙니다. +contextConfig.contextClose=context.xmlì„ ë‹«ëŠ” 중 오류 ë°œìƒ +contextConfig.contextMissing=context.xmlì´ ì¡´ìž¬í•˜ì§€ 않습니다: [{0}] +contextConfig.contextParse=컨í…스트 [{0}]ì„(를) 위한 context.xml ë‚´ì—ì„œ 파싱 오류 ë°œìƒ +contextConfig.defaultError=[{1}]ì— ìœ„ì¹˜í•˜ê³  [{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ê¸°ë³¸ web.xmlì„ ì²˜ë¦¬í•˜ëŠ” 중 오류 ë°œìƒ +contextConfig.defaultMissing=글로벌 web.xml 파ì¼ì„ ì°¾ì„ ìˆ˜ 없습니다. +contextConfig.defaultPosition=[{0}] í–‰, [{1}] ì—´ì—ì„œ ë°œìƒ +contextConfig.destroy=ContextConfig: 소멸시키는 중 +contextConfig.effectiveWebXml=실질ì ìœ¼ë¡œ 유효한 web.xml:\n\ +{0} +contextConfig.fileUrl=URL [{0}](으)로부터 File ê°ì²´ë¥¼ ìƒì„±í•  수 없습니다. +contextConfig.fixDocBase=컨í…스트 [{0}]ì„(를) 위한 docBase를 조정하는 중 예외 ë°œìƒ +contextConfig.init=ContextConfig: 초기화 중 +contextConfig.inputStreamFile=íŒŒì¼ [{0}]ì— ëŒ€í•˜ì—¬ annotationë“¤ì„ ì²˜ë¦¬í•  수 없습니다. +contextConfig.inputStreamJar=Annotationë“¤ì„ ìŠ¤ìº”í•˜ê¸° 위해, Jar [{1}](으)ë¡œë¶€í„°ì˜ Jar 엔트리 [{0}]ì„(를) 처리할 수 없습니다. +contextConfig.inputStreamWebResource=Annotationë“¤ì„ ìœ„í•´ 웹 리소스 [{0}]ì„(를) 처리할 수 없습니다. +contextConfig.invalidSciHandlesTypes=하나 ì´ìƒì˜ ServletContentInitializerë“¤ì˜ @HandlesTypes annotationì— ëŒ€í•œ ì ê²€ì„ 위해, í´ëž˜ìŠ¤ [{0}]ì„(를) 로드할 수 없습니다. +contextConfig.jarFile=Annotationë“¤ì„ ìœ„í•´ Jar [{0}]ì„(를) 처리할 수 없습니다. +contextConfig.jspFile.error=JSP íŒŒì¼ [{0}]ì€(는) 반드시 ''/''ë¡œ 시작해야 합니다. +contextConfig.jspFile.warning=경고: Servlet 2.4ì—ì„œ JSP íŒŒì¼ [{0}]ì€(는) 반드시 ''/''ë¡œ 시작해야 합니다. +contextConfig.missingRealm=ì¸ì¦ 처리 ì‹œ 사용할 Realmì´ ì„¤ì •ë˜ì§€ 않았습니다. +contextConfig.noAntiLocking=java.io.tmpdir 프로í¼í‹° ê°’ [{0}]ì´(ê°€) 유효한 디렉토리 경로가 아닙니다. 해당 웹 애플리케ì´ì…˜ [{1}]ì„(를) 위한 antiResourceLocking ì„¤ì •ì€ ë¬´ì‹œë©ë‹ˆë‹¤. +contextConfig.processAnnotationsDir.debug=Annotationë“¤ì„ ê°€ì§„ í´ëž˜ìŠ¤ 파ì¼ë“¤ì„ 찾기 위해 디렉토리 [{0}]ì„(를) 스캔합니다. +contextConfig.processAnnotationsInParallelFailure=병렬 처리 실패 +contextConfig.processAnnotationsJar.debug=Annotation들, [{0}]ì„(를) 가진 í´ëž˜ìŠ¤ 파ì¼ë“¤ì„ 찾기 위해, JAR 파ì¼ì„ 스캔합니다. +contextConfig.processAnnotationsWebDir.debug=Annotationë“¤ì¸ [{0}]ì„(를) 가진 í´ëž˜ìŠ¤ 파ì¼ë“¤ì„ 찾기 위해, 웹 애플리케ì´ì…˜ 디렉토리를 스캔합니다. +contextConfig.resourceJarFail=ì •ì  ë¦¬ì†ŒìŠ¤ë“¤ì´, [{1}](ì´)ë¼ëŠ” ì´ë¦„ì˜ ì»¨í…ìŠ¤íŠ¸ì— í¬í•¨ë˜ê²Œ 하기 위하여, URL [{0}]ì—ì„œ ë°œê²¬ëœ JAR를 처리하지 못했습니다. +contextConfig.role.auth=보안 ì—­í•  ì´ë¦„ [{0}]ì´(ê°€), ì—ì„œ ì •ì˜ë˜ì§€ ì•Šì€ ì±„ë¡œ, ì—ì„œ 사용ë˜ì—ˆìŠµë‹ˆë‹¤. +contextConfig.role.link=보안 ì—­í•  ì´ë¦„ [{0}]ì´(ê°€) ì—ì„œ ì •ì˜ëœ ì ì´ 없는ë°, ì—ì„œ 사용ë˜ì—ˆìŠµë‹ˆë‹¤. +contextConfig.role.runas= ë‚´ì— ì •ì˜ë˜ì§€ ì•Šê³ , 보안 ì—­í•  ì´ë¦„ [{0}]ì´(ê°€) ì—ì„œ 사용ë˜ì—ˆìŠµë‹ˆë‹¤. +contextConfig.sci.debug=[{0}]ì„(를) 위해 ServletContainerInitializer를 처리할 수 없습니다. ì´ëŠ” í•„ì‹œ @HandlesTypes annotation ë‚´ì— ì •ì˜ëœ í´ëž˜ìŠ¤ê°€ 존재하지 않기 ë•Œë¬¸ì¼ ê²ƒìž…ë‹ˆë‹¤. +contextConfig.sci.info=[{0}]ì„(를) 위한 ServletContainerInitializer를 처리할 수 없습니다. ì´ëŠ” í•„ì‹œ @HandlesTypes annotationì— ì •ì˜ëœ í´ëž˜ìŠ¤ê°€ 존재하지 않기 ë•Œë¬¸ì¼ ê²ƒìž…ë‹ˆë‹¤. ì „ì²´ ìŠ¤íƒ íŠ¸ë ˆì´ìŠ¤ë¥¼ 보시려면, 로그 ë ˆë²¨ì„ ë””ë²„ê·¸ 레벨로 설정하십시오. +contextConfig.servletContainerInitializerFail=ì´ë¦„ì´ [{0}]ì¸ ì»¨í…스트를 위한 ServletContainerInitializerë“¤ì„ íƒì§€í•˜ì§€ 못했습니다. +contextConfig.start=ContextConfig: 시작 처리 중 +contextConfig.stop=ContextConfig: STOP 처리 중 +contextConfig.tomcatWebXmlError=/WEB-INF/tomcat-web.xmlì„ ì²˜ë¦¬ 중 오류 ë°œìƒ +contextConfig.unavailable=ì´ì „ 오류(들)ë¡œ ì¸í•˜ì—¬, ì´ ì• í”Œë¦¬ì¼€ì´ì…˜ì´ 가용하지 ì•Šì€ ê²ƒìœ¼ë¡œ 표시합니다. +contextConfig.unknownUrlProtocol=Annotation 처리 중, ì¸ì‹ë˜ì§€ 않는 프로토콜 [{0}]ì„(를) í¬í•¨í•˜ì—¬, URL [{1}]ì´(ê°€) 무시ë˜ì—ˆìŠµë‹ˆë‹¤. +contextConfig.urlPatternValue=í´ëž˜ìŠ¤ [{1}]ì˜ [{0}] annotationì„ ìœ„í•´, urlPatterns와 value ì†ì„±, 둘 다 설정ë˜ì—ˆìŠµë‹ˆë‹¤. +contextConfig.xmlSettings=컨í…스트 [{0}]ì´(ê°€), validation:[{1}]와(ê³¼) namespaceAware:[{2}]ì„ ì‚¬ìš©í•˜ì—¬, web.xmlê³¼ web-fragment.xml 파ì¼ë“¤ì„ 파싱합니다. + +engineConfig.cce=Lifecycle ì´ë²¤íŠ¸ ë°ì´í„° ê°ì²´ [{0}]ì´(ê°€) Engine ê°ì²´ê°€ 아닙니다. +engineConfig.start=EngineConfig: START 처리 중 +engineConfig.stop=EngineConfig: STOP 처리 중 + +expandWar.copy=[{0}]ì„(를) [{1}]ì— ë³µì‚¬í•˜ëŠ” 중 오류 ë°œìƒ +expandWar.createFailed=디렉토리 [{0}]ì„(를) ìƒì„±í•  수 없습니다. +expandWar.createFileFailed=íŒŒì¼ [{0}]ì„(를) ìƒì„±í•  수 없습니다. +expandWar.deleteFailed=[{0}]ì´(ê°€) 완전히 ì‚­ì œë  ìˆ˜ 없었습니다. 남아있는 파ì¼ë“¤ì˜ 존재는 ë¬¸ì œë“¤ì„ ì¼ìœ¼í‚¬ 수 있습니다. +expandWar.deleteOld=ì••ì¶•ì´ í’€ë ¤ì§„ 디렉토리 [{0}]ì˜ ìµœì¢… 변경 시간ì´, ì—°ê´€ëœ WARì˜ ìµœì¢… 변경 시간과 부합하지 않습니다. 해당 디렉토리는 ì‚­ì œë  ê²ƒìž…ë‹ˆë‹¤. +expandWar.illegalPath=ì•„ì¹´ì´ë¸Œ [{0}]ì— ë¬¸ì œê°€ 있어 ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤: 엔트리가 불허ë˜ëŠ” 경로 [{1}]ì„(를) í¬í•¨í•˜ê³  있고, ê·¸ 경로가 ì •ì˜ëœ docBase [{3}] ì™¸ë¶€ì— ì¡´ìž¬í•˜ê¸° 때문ì—, [{2}]ì— ì••ì¶•ì„ í’€ì§€ 않았습니다. +expandWar.lastModifiedFailed=[{0}]ì„(를) 위해, 최종 변경 ì‹œê°„ì„ ì„¤ì •í•  수 없습니다. +expandWar.missingJarEntry=JarEntry [{0}]ì„(를) 위한 ìž…ë ¥ ìŠ¤íŠ¸ë¦¼ì„ ì–»ì„ ìˆ˜ 없습니다 - WAR 파ì¼ì´ 깨졌나요? + +failedContext.start=글로벌, ë˜ëŠ” 호스트 별, ë˜ëŠ” 해당 컨í…ìŠ¤íŠ¸ì˜ context.xml 파ì¼ì„ 처리하지 못하였으므로, 컨í…스트 [{0}]ì€(는) ì‹œìž‘ë  ìˆ˜ 없습니다. + +hostConfig.appBase=호스트 [{0}]ì„(를) 위한 애플리케ì´ì…˜ base [{1}]ì´(ê°€), 존재하지 않거나 디렉토리가 아닙니다. 배치 ì˜¤ë¥˜ë“¤ì„ ë°©ì§€í•˜ê¸° 위하여, deployOnStartUpê³¼ autoDeployê°€ falseë¡œ 설정ë˜ì–´ 있었습니다. 다른 ì˜¤ë¥˜ë“¤ì´ ì—¬ì „ížˆ ë°œìƒí•  수 있습니다. +hostConfig.canonicalizing=[{1}]ì˜ ë°°ì¹˜ë¥¼ 제거하려 ì‹œë„하는 ë™ì•ˆ, [{0}]ì„(를) 위한 canonical 경로를 ê²°ì •í•  수 없습니다. +hostConfig.cce=Lifecycle ì´ë²¤íŠ¸ ë°ì´í„° ê°ì²´ [{0}]ì´(ê°€) 호스트 ê°ì²´ê°€ 아닙니다. +hostConfig.context.remove=컨í…스트 [{0}]ì„(를) 제거하는 중 오류 ë°œìƒ +hostConfig.context.restart=컨í…스트 [{0}]ì´(ê°€) 다시 시작하는 ë™ì•ˆ 오류 ë°œìƒ +hostConfig.createDirs=배치를 위한 디렉토리 [{0}]ì„(를) ìƒì„±í•  수 없습니다. +hostConfig.deploy.error=웹 애플리케ì´ì…˜ 디렉토리 [{0}]ì„(를) 배치하는 중 예외 ë°œìƒ +hostConfig.deployDescriptor=배치 descriptor [{0}]ì„(를) 배치합니다. +hostConfig.deployDescriptor.blocked=컨í…스트 경로 [{0}]ì˜ ì›¹ 애플리케ì´ì…˜ì´ 배치ë˜ì§€ 않았습니다. 왜ëƒí•˜ë©´ 해당 애플리케ì´ì…˜ì˜ 안전한 ë°°ì¹˜ì— í•„ìš”í•œ ì„¤ì •ë“¤ì´ ë°°ì¹˜ descriptor [{1}]ì— í¬í•¨ë˜ì–´ 있으나, ì´ í˜¸ìŠ¤íŠ¸ì˜ deployXML ì„¤ì •ì— ì˜í•´ 배치 descriptorë“¤ì´ ì²˜ë¦¬ ë˜ì§€ 않았기 때문입니다. ì´ ì• í”Œë¦¬ì¼€ì´ì…˜ì„ 배치하기 위해서는 ì ì ˆí•œ descriptorê°€ [{2}]ì— ìƒì„±ë˜ì–´ì•¼ 합니다. +hostConfig.deployDescriptor.error=배치 descriptor [{0}]ì„(를) 배치하는 중 오류 ë°œìƒ +hostConfig.deployDescriptor.finished=배치 descriptor [{0}]ì˜ ë°°ì¹˜ê°€ [{1}] 밀리초 ë‚´ì— ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. +hostConfig.deployDescriptor.hiddenDir=외부 docBase를 사용하여 배치 descriptor [{0}]ì„(를) 배치하는 것ì€, appBaseÂ ë‚´ì˜ ë””ë ‰í† ë¦¬ [{1}]ì´(ê°€) ë¬´ì‹œë  ê²ƒì„ ì˜ë¯¸í•©ë‹ˆë‹¤. +hostConfig.deployDescriptor.hiddenWar=외부 docBase를 í¬í•¨í•œ 배치 descriptor [{0}]ì˜ ë°°ì¹˜ëŠ”, appBase ë‚´ì˜ WAR [{1}]ì´(ê°€) ë¬´ì‹œë  ê²ƒì„ ì˜ë¯¸í•©ë‹ˆë‹¤. +hostConfig.deployDescriptor.localDocBaseSpecified=호스트 appBase ë‚´ì˜ docBase [{0}]ì´(ê°€) 지정ë˜ì—ˆìœ¼ë‚˜, ì´ëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +hostConfig.deployDescriptor.path=배치 descriptor [{1}] ë‚´ì—ì„œ, [{0}] ê°’ì„ ê°€ì§„ path ì†ì„±ì€ 무시ë©ë‹ˆë‹¤. +hostConfig.deployDescriptor.threaded.error=배치 descriptorë“¤ì„ ë°°ì¹˜í•˜ë ¤ëŠ” 멀티 쓰레드 ìž‘ì—…ì´ ì™„ë£Œë˜ê¸°ë¥¼ 기다리는 중 오류 ë°œìƒ +hostConfig.deployDir=웹 애플리케ì´ì…˜ 디렉토리 [{0}]ì„(를) 배치합니다. +hostConfig.deployDir.error=웹 애플리케ì´ì…˜ 디렉토리 [{0}]ì„(를) 배치하는 중 오류 ë°œìƒ +hostConfig.deployDir.finished=웹 애플리케ì´ì…˜ 디렉토리 [{0}]ì— ëŒ€í•œ 배치가 [{1}] ë°€ë¦¬ì´ˆì— ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. +hostConfig.deployDir.threaded.error=ë””ë ‰í† ë¦¬ë“¤ì˜ ë©€í‹° 쓰레드 배치 ìž‘ì—…ì´ ì™„ë£Œë˜ê¸°ë¥¼ 기다리는 중 오류 ë°œìƒ +hostConfig.deployWar=웹 애플리케ì´ì…˜ ì•„ì¹´ì´ë¸Œ [{0}]ì„(를) 배치합니다. +hostConfig.deployWar.error=웹 애플리케ì´ì…˜ ì•„ì¹´ì´ë¸Œ [{0}]ì„(를) 배치하는 중 오류 ë°œìƒ +hostConfig.deployWar.finished=웹 애플리케ì´ì…˜ ì•„ì¹´ì´ë¸Œ [{0}]ì˜ ë°°ì¹˜ê°€ [{1}] ë°€ë¦¬ì´ˆì— ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. +hostConfig.deployWar.hiddenDir=WAR [{1}]ì€(는) 우선순위가 높게 처리ë˜ì–´ì•¼ 하고, unpackWARsê°€ falseì´ê¸° 때문ì—, 디렉토리 [{0}]ì€(는) ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +hostConfig.deployWar.threaded.error=WAR 파ì¼ë“¤ì— 대해 멀티 쓰레드 배치 ìž‘ì—…ë“¤ì´ ì™„ë£Œë˜ê¸°ê¹Œì§€ 기다리는 중 오류 ë°œìƒ +hostConfig.deploying=ë°œê²¬ëœ ì›¹ 애플리케ì´ì…˜ë“¤ì„ 배치합니다. +hostConfig.docBaseUrlInvalid=ì œê³µëœ docBase는 URL로서 í‘œí˜„ë  ìˆ˜ 없습니다. +hostConfig.expand=웹 애플리케ì´ì…˜ ì•„ì¹´ì´ë¸Œ [{0}]ì˜ ì••ì¶•ì„ í’‰ë‹ˆë‹¤. +hostConfig.expand.error=웹 애플리케ì´ì…˜ ì•„ì¹´ì´ë¸Œ [{0}]ì˜ ì••ì¶•ì„ í‘¸ëŠ” 중 예외 ë°œìƒ +hostConfig.ignorePath=ìžë™ 배치를 위해 appBaseë‚´ì˜ [{0}] 경로를 무시합니다. +hostConfig.illegalWarName=War ì´ë¦„[{0}]ì´(ê°€) 유효하지 않습니다. ì´ ì•„ì¹´ì´ë¸ŒëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +hostConfig.jmx.register=컨í…스트 [{0}]ì„(를) 등ë¡í•˜ì§€ 못했습니다. +hostConfig.jmx.unregister=컨í…스트 [{0}]ì— ëŒ€í•œ 등ë¡ì„ 제거하지 못했습니다. +hostConfig.migrateApp.threaded.error=레거시 애플리케ì´ì…˜ë“¤ì„ 멀티 쓰레드로 마ì´ê·¸ë ˆì´ì…˜í•˜ëŠ” ìž‘ì—…ì˜ ì™„ë£Œë¥¼ 기다리는 중 오류 ë°œìƒ +hostConfig.migrateError=마ì´ê·¸ë ˆì´ì…˜ 실패 +hostConfig.reload=컨í…스트 [{0}]ì„(를) 다시 로드합니다. +hostConfig.resourceNotAbsolute=[{1}]ì´(ê°€) 절대 경로가 아니기 때문ì—, 컨í…스트 [{0}](으)로부터 리소스를 제거할 수 없습니다. +hostConfig.start=HostConfig: 시작 처리 중 +hostConfig.stop=HostConfig.stop() 오í¼ë ˆì´ì…˜ 처리 중 +hostConfig.undeploy=컨í…스트 [{0}]ì˜ ë°°ì¹˜ë¥¼ 제거합니다. +hostConfig.undeployVersion=í™œì„±í™”ëœ ì„¸ì…˜ì´ ì—†ëŠ”, 컨í…스트 [{0}]ì˜ ì´ì „ ë²„ì „ì˜ ë°°ì¹˜ë¥¼ 제거합니다. + +listener.createFailed=ì„ íƒ ì‚¬í•­ì¸ ë¦¬ìŠ¤í„° [{0}]ì´(ê°€) ìƒì„±ë˜ì§€ 못했습니다. +listener.notServer=리스너 엘리먼트는 서버 엘리먼트 ë‚´ì— ìœ„ì¹˜í•´ì•¼ 합니다만, 현재 [{0}] ë‚´ì— ìžˆìŠµë‹ˆë‹¤. + +passwdUserDatabase.readFail=/etc/passwd로부터 사용ìžë“¤ì˜ ì „ì²´ ì§‘í•©ì„ êµ¬í•˜ì§€ 못했습니다. + +tomcat.addWebapp.conflictChild=ì´ë¯¸ 존재하는 컨í…스트 [{2}](으)ë¡œ ì¸í•˜ì—¬, [{0}]ì— ìœ„ì¹˜í•œ WAR를 컨í…스트 경로 [{1}](으)ë¡œ 배치할 수 없습니다. +tomcat.addWebapp.conflictFile=ì´ë¯¸ 존재하는 íŒŒì¼ [{2}](으)ë¡œ ì¸í•˜ì—¬, [{0}]ì— ìœ„ì¹˜í•œ WAR를 컨í…스트 경로 [{1}](으)ë¡œ 배치할 수 없습니다. +tomcat.baseDirMakeFail=Base 디렉토리로서 사용하기 위한, 디렉토리 [{0}]ì„(를) ìƒì„±í•  수 없습니다. +tomcat.baseDirNotDir=base 디렉토리를 위해 ì§€ì •ëœ ìœ„ì¹˜ [{0}]ì€(는) 디렉토리가 아닙니다. +tomcat.defaultMimeTypeMappingsFail=기본 MIME íƒ€ìž…ë“¤ì„ ë¡œë“œí•  수 없습니다. +tomcat.homeDirMakeFail=홈 디렉토리로 사용할 디렉토리 [{0}]ì„(를) ìƒì„±í•  수 없습니다. +tomcat.invalidCommandLine=유효하지 ì•Šì€ ëª…ë ¹ í–‰ 아규먼트들 [{0}] +tomcat.noContextClass=호스트 [{1}]와(ê³¼) URL [{2}]ì„(를) 위한, 컨í…스트 í´ëž˜ìŠ¤ [{0}]ì˜ ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•˜ì§€ 못했습니다. +tomcat.noContextXml=[{0}]ì—ì„œ 웹 애플리케ì´ì…˜ context.xmlì„ ê²°ì •í•  수 없습니다. + +userConfig.database=ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ë¥¼ 로드하는 중 예외 ë°œìƒ +userConfig.deploy=ì‚¬ìš©ìž [{0}]ì„(를) 위해 웹 애플리케ì´ì…˜ì„ 배치합니다. +userConfig.deploy.threaded.error=ì‚¬ìš©ìž ë””ë ‰í† ë¦¬ë“¤ì˜ ë©€í‹° 쓰레드 배치 ìž‘ì—…ì´ ì™„ë£Œë˜ê¸°ë¥¼ 기다리는 중 오류 ë°œìƒ +userConfig.deploying=ì‚¬ìš©ìž ì›¹ 애플리케ì´ì…˜ë“¤ì„ 배치합니다. +userConfig.error=ì‚¬ìš©ìž [{0}]ì„(를) 위한 웹 애플리케ì´ì…˜ì„ 배치 중 오류 ë°œìƒ +userConfig.start=UserConfig: START 처리 중 +userConfig.stop=UserConfig: STOP 처리 중 + +versionLoggerListener.arg=명령 í–‰ 아규먼트: {0} +versionLoggerListener.catalina.base=CATALINA_BASE: {0} +versionLoggerListener.catalina.home=CATALINA_HOME: {0} +versionLoggerListener.env=환경 변수: {0} = {1} +versionLoggerListener.java.home=ìžë°” 홈: {0} +versionLoggerListener.os.arch=아키í…처: {0} +versionLoggerListener.os.name=ìš´ì˜ì²´ì œ ì´ë¦„: {0} +versionLoggerListener.os.version=ìš´ì˜ì²´ì œ 버전: {0} +versionLoggerListener.prop=시스템 프로í¼í‹°: {0} = {1} +versionLoggerListener.serverInfo.server.built=Server 빌드 ì‹œê°: {0} +versionLoggerListener.serverInfo.server.number=Server 버전 번호: {0} +versionLoggerListener.serverInfo.server.version=서버 버전 ì´ë¦„: {0} +versionLoggerListener.vm.vendor=JVM 벤ë”: {0} +versionLoggerListener.vm.version=JVM 버전: {0} + +webAnnotationSet.invalidInjection=유효하지 ì•Šì€ ë©”ì†Œë“œ 리소스 injection annotation입니다. diff --git a/java/org/apache/catalina/startup/LocalStrings_pt_BR.properties b/java/org/apache/catalina/startup/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..9d69ef9 --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_pt_BR.properties @@ -0,0 +1,22 @@ +# 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. + +catalina.serverStartFail=O componente Server requerido falhou ao iniciar, por isso, o Tomcat não pode iniciar + +contextConfig.altDDNotFound=arquivo alt-dd [{0}] não encontrado + +hostConfig.deployDir=Diretório de instalação da aplicação web [{0}] + +userConfig.database=Exceção ao carregar banco de dados do usuário. diff --git a/java/org/apache/catalina/startup/LocalStrings_ru.properties b/java/org/apache/catalina/startup/LocalStrings_ru.properties new file mode 100644 index 0000000..2bccc93 --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_ru.properties @@ -0,0 +1,42 @@ +# 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. + +catalina.serverStartFail=Томкат не Ñмог запуÑтитьÑÑ Ð¸Ð·-за того что обÑзательный компонент не Ñмог запуÑтитьÑÑ + +contextConfig.altDDNotFound=alt-dd файл [{0}] не найден +contextConfig.applicationUrl=Ðе возможно определить URL Ð´Ð»Ñ web.xml Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +contextConfig.contextMissing=ОтÑутÑтвует context.xml: [{0}] +contextConfig.defaultMissing=Ðе обнаружен глобальный web.xml +contextConfig.defaultPosition=Произошло в Ñтроке [{0}] Ñтолбце [{1}] +contextConfig.inputStreamWebResource=Ðе возможно обработать веб реÑÑƒÑ€Ñ [{0}] Ð´Ð»Ñ Ð°Ð½Ð½Ð¾Ñ‚Ð°Ñ†Ð¸Ð¹\n +contextConfig.tomcatWebXmlError=Ошибка обработки /WEB-INF/tomcat-web.xml + +hostConfig.deployDir=УÑтановка веб Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð² папку [{0}] +hostConfig.deployWar.error=Ошибка при развертывании архива Ñ Ð²ÐµÐ±-приложением [{0}] +hostConfig.docBaseUrlInvalid=ПредоÑтавленый docBase не может быть предÑтавлен в виде URL + +userConfig.database=Ошибка при загрузке базы данных пользователей +userConfig.error=Ошибка Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ web-Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ [{0}] + +versionLoggerListener.catalina.base=CATALINA_BASE: {0} +versionLoggerListener.catalina.home=CATALINA_HOME: {0} +versionLoggerListener.java.home=Java Home: {0} +versionLoggerListener.os.arch=Ðрхитектура: {0} +versionLoggerListener.os.version=ВерÑÐ¸Ñ ÐžÐ¡: {0} +versionLoggerListener.serverInfo.server.built=Сервер Ñобран: {0} +versionLoggerListener.serverInfo.server.number=Ðомер верÑии Ñервера: {0} +versionLoggerListener.serverInfo.server.version=Ð˜Ð¼Ñ Ð²ÐµÑ€Ñии Ñервера: {0} +versionLoggerListener.vm.vendor=Производитель JVM: {0} +versionLoggerListener.vm.version=ВерÑÐ¸Ñ JVM: {0} diff --git a/java/org/apache/catalina/startup/LocalStrings_zh_CN.properties b/java/org/apache/catalina/startup/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..c91b6d7 --- /dev/null +++ b/java/org/apache/catalina/startup/LocalStrings_zh_CN.properties @@ -0,0 +1,185 @@ +# 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. + +catalina.configFail=无法从[{0}]加载æœåŠ¡å™¨é…ç½® +catalina.generatedCodeLocationError=为生æˆçš„Tomcat嵌入代ç [{0}]使用é…置的ä½ç½®æ—¶å‡ºé”™ +catalina.incorrectPermissions=æƒé™é”™è¯¯ï¼Œæ­¤æ–‡ä»¶æ²¡æœ‰è¯»å–æƒé™ +catalina.init=æœåŠ¡å™¨åœ¨[{0}]毫秒内åˆå§‹åŒ– +catalina.initError=åˆå§‹åŒ– Catalina 时出错 +catalina.noCluster=由于[{0}]未找到集群Ruleset。已ç¦ç”¨é›†ç¾¤é…置。 +catalina.noLoader=é…置的代ç åŠ è½½å™¨[{0}]没有找到,生æˆçš„代ç ä¸ä¼šè¢«ä½¿ç”¨ +catalina.noNaming=命å环境已ç¦ç”¨ +catalina.noServer=无法å¯åŠ¨æœåŠ¡å™¨ï¼ŒæœåŠ¡å™¨å®žä¾‹æœªé…ç½® +catalina.serverStartFail=所必需的æœåŠ¡ç»„件å¯åŠ¨å¤±è´¥ï¼Œæ‰€ä»¥æ— æ³•å¯åŠ¨Tomcat +catalina.shutdownHookFail=关闭挂钩在å°è¯•åœæ­¢æœåŠ¡å™¨æ—¶é‡åˆ°é”™è¯¯ +catalina.startup=[{0}]毫秒åŽæœåŠ¡å™¨å¯åŠ¨ +catalina.stopError=åœæ­¢ Catalina 时出错 +catalina.stopServer=未é…置关闭端å£ã€‚通过OSä¿¡å·å…³é—­æœåŠ¡å™¨ã€‚æœåŠ¡å™¨æœªå…³é—­ã€‚ +catalina.stopServer.connectException=无法è”ç³»[{0}:{1}](基端å£[{2}]å’Œå移é‡[{3}])。Tomcatå¯èƒ½ä¸åœ¨è¿è¡Œã€‚ +catalina.usage=用法: java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -nonaming ] [ -generateCode [ {pathname} ] ] [ -useGeneratedCode ] { -help | start | stop } + +catalinaConfigurationSource.cannotObtainURL=无法从给定的å‚æ•°[{0}]处获å–资æºï¼šæ²¡æœ‰å¯è¯»æ–‡ä»¶ï¼Œæ²¡æœ‰ç±»åŠ è½½å™¨æˆ–者ä¸æ˜¯å¯è§£æžçš„URI + +connector.noSetExecutor=连接器[{0}]ä¸æ”¯æŒå¤–部执行器。找ä¸åˆ°æ–¹æ³•setExecutor(java.util.concurrent.Executor) +connector.noSetSSLImplementationName=连接器[{0}]ä¸æ”¯æŒæ›´æ”¹SSL实现。找ä¸åˆ°æ–¹æ³•setslimplementationname(String)。 + +contextConfig.altDDNotFound=未找到 alt-dd 文件 [{0}] +contextConfig.annotationsStackOverflow=由于StackOverflower错误,无法完æˆå¯¹web应用程åº[{0}]的批注的扫æ。å¯èƒ½çš„根本原因包括-Xss的设置过低和éžæ³•çš„循环继承ä¾èµ–项。正在处ç†çš„类层次结构是[{1}] +contextConfig.applicationMissing=web.xml文件丢失,åªä½¿ç”¨é»˜è®¤ã€‚ +contextConfig.applicationParse=解æžåº”用程åºçš„web.xml错误,ä½ç½®ï¼š[{0}] +contextConfig.applicationPosition=å‘生在第[{0}]行,属性[{1}] +contextConfig.applicationStart=正在解æžåº”ç”¨ç¨‹åº web.xml 文件 [{0}] +contextConfig.applicationUrl=æ— æ³•ç¡®å®šåº”ç”¨ç¨‹åº web.xml çš„URL +contextConfig.authenticatorConfigured=为方法 [{0}] é…置了验è¯å™¨ +contextConfig.authenticatorInstantiate=无法实例化认è¯ç±»[{0}] +contextConfig.authenticatorMissing=ä¸èƒ½é…置一个认è¯ä¸ºæ–¹æ³•[{0}] +contextConfig.authenticatorResources=无法加载认è¯è€…映射列表 +contextConfig.badUrl=ä¸èƒ½å¤„ç†ä¸Šä¸‹æ–‡æ述符[{0}] +contextConfig.cce=生命周期事件数æ®å¯¹è±¡[{0}] ä¸æ˜¯ä¸€ä¸ªä¸Šä¸‹æ–‡ +contextConfig.contextClose=关闭context.xml错误 +contextConfig.contextMissing=缺少 context.xml:[{0}] +contextConfig.contextParse=解æžcontext.xml错误,[{0}] +contextConfig.defaultError=处ç†é»˜è®¤web.xml错误,å称:[{0}],ä½ç½®ï¼š[{1}] +contextConfig.defaultMissing=未找到全局 web.xml +contextConfig.defaultPosition=å‘生在 [{0}] è¡Œ [{1}] 列 +contextConfig.destroy=ContextConfigï¼šæ­£åœ¨é”€æ¯ +contextConfig.effectiveWebXml=有效的web.xml:[{0}] +contextConfig.fileUrl=无法从URL[{0}]创建文件对象 +contextConfig.fixDocBase=上下文[{0}]的异常修å¤docBase +contextConfig.init=上下文é…ç½®: 正在åˆå§‹åŒ– +contextConfig.inputStreamFile=无法为注解处ç†æ–‡ä»¶[{0}] +contextConfig.inputStreamJar=无法处ç†Jar实体[{0}]的注解,Jar:[{1}] +contextConfig.inputStreamWebResource=ä¸èƒ½å¤„ç†æ³¨é‡Šçš„Web资æº[{0}] +contextConfig.invalidSciHandlesTypes=无法为核对一个或多个ServletContentInitializers的注解@HandlesTypes而加载类[{0}] +contextConfig.jarFile=无法处ç†Jar[{0}]的注解 +contextConfig.jspFile.error=JSP文件[{0}]必须以''/''开头。 +contextConfig.jspFile.warning=警告:在Servlet 2.4 中,JSP文件[{0}]必须以‘/’开头 +contextConfig.missingRealm=对应的认è¯é¢†åŸŸæœªé…ç½® +contextConfig.noAntiLocking=é…ç½® java.io.tmpdir的值[{0}]未指å‘有效路径。Web应用[{1}]antiResourceLockingé…置将被忽略 +contextConfig.processAnnotationsDir.debug=使用注解 [{0}]扫æ目录中的类文件 +contextConfig.processAnnotationsInParallelFailure=并行执行失败 +contextConfig.processAnnotationsJar.debug=扫æjar文件中注解[{0}]的类文件 +contextConfig.processAnnotationsWebDir.debug=扫æ web 应用程åºç›®å½•ä¸‹å«æœ‰ [{0}] 注解的 class 文件 +contextConfig.resourceJarFail=无法处ç†åœ¨URL[{0}]处找到的JAR,以便将é™æ€èµ„æºåŒ…å«åœ¨å为[{1}]的上下文中 +contextConfig.role.auth=在标签的å­æ ‡ç­¾ä¸­æ²¡æœ‰å®šä¹‰è§’色å[{0}] +contextConfig.role.link=中使用的安全角色[{0}]未被定义在中 +contextConfig.role.runas= 中使用的安全角色å [{0}],未在 中定义 +contextConfig.sci.debug=无法为[{0}]处ç†ServletContainerInitializer,很å¯èƒ½æ˜¯ç”±äºŽä¸¢å¤±äº†ä¸€ä¸ªä½¿ç”¨@HandlesTypes注解定义的类 +contextConfig.sci.info=无法处ç†[{0}]çš„ServletContainerInitializer。这很å¯èƒ½æ˜¯ç”±äºŽ@handleTypes注释中定义的类丢失所致。为完整堆栈跟踪å¯ç”¨è°ƒè¯•çº§åˆ«æ—¥å¿—记录。 +contextConfig.servletContainerInitializerFail=无法检测到上下文å称为[{0}]çš„ServletContainerInitializers +contextConfig.start=ContextConfigï¼šå¼€å§‹å¤„ç† +contextConfig.stop=ContextConfig:åœæ­¢å¤„ç† +contextConfig.tomcatWebXmlError=å¤„ç† /WEB-INF/tomcat-web.xml å‘生错误 +contextConfig.unavailable=由于之å‰çš„错误,标记当å‰åº”用程åºä¸å¯ç”¨ +contextConfig.unknownUrlProtocol=注解解æžè¿‡ç¨‹ä¸­ï¼ŒURLåè®®[{0}]未识别。忽略URL[{1}]。 +contextConfig.urlPatternValue=类文件[{1}]çš„urlPatterns和值属性上åŒæ—¶è®¾ç½®äº†æ³¨è§£[{0}] +contextConfig.xmlSettings=上下文[{0}]将解æžweb.xmlå’Œweb-fragment.xml文件,验è¯ä¸ºï¼š[{1}],命å空间感知为:[{2}] + +engineConfig.cce=生命周期事件数æ®å¯¹è±¡[{0}]ä¸æ˜¯ä¸€ä¸ªå¼•æ“Ž(Engine) +engineConfig.start=EngineConfig:处ç†å¼€å§‹ã€‚ +engineConfig.stop=引擎é…置:处ç†è¿›ç¨‹åœæ­¢ + +expandWar.copy=错误的拷è´[{0}] to [{1}] +expandWar.createFailed=无法创建文件夹[{0}]。 +expandWar.createFileFailed=无法创建文件[{0}] +expandWar.deleteFailed=[{0}] 无法被彻底删除。其余残留文件å¯èƒ½ä¼šå¯¼è‡´é—®é¢˜ +expandWar.deleteOld=å‘现一个展开的目录[{0}],它的最åŽä¿®æ”¹æ—¶é—´ä¸Žå…³è”çš„WARä¸ä¸€è‡´.它将被删除. +expandWar.illegalPath=å½’æ¡£[{0}]æ ¼å¼é”™è¯¯ï¼Œå°†è¢«å¿½ç•¥ï¼šæ¡ç›®åŒ…å«éžæ‰©å±•åˆ°[{2}]çš„éžæ³•è·¯å¾„[{1}],因为它超出了定义的docBase [{3}] +expandWar.lastModifiedFailed=无法为[{0}]设置上次修改时间 +expandWar.missingJarEntry=无法获得 JarEntry [{0}] çš„è¾“å…¥æµ - WAR 文件是å¦å·²æŸå? + +failedContext.start=无法处ç†å…¨å±€ï¼Œæ¯ä¸ªä¸»æœºæˆ–特定于上下文的context.xml文件,因此无法å¯åŠ¨[{0}]上下文。 + +hostConfig.appBase=主机[{0}]的应用程åºåŸºç¡€[{1}]ä¸å­˜åœ¨æˆ–ä¸æ˜¯ç›®å½•ã€‚deployOnStartUpå’ŒautoDebug已设置为false,以防止部署错误。其他错误ä»ç„¶å¯èƒ½å‘生。 +hostConfig.canonicalizing=试图å–消部署[{1}]时,无法确定[{0}]的规范路径 +hostConfig.cce=生命周期事件数æ®å¯¹è±¡[{0}]ä¸æ˜¯ä¸»æœº +hostConfig.context.remove=移除上下文[{0}]错误 +hostConfig.context.restart=上下文[{0}]é‡æ–°å¯åŠ¨æ—¶å‡ºé”™ã€‚ +hostConfig.createDirs=无法为部署创建目录:[{0}] +hostConfig.deploy.error=部署web应用程åºç›®å½•[{0}]æ—¶å‘生异常 +hostConfig.deployDescriptor=正在部署部署æ述符[{0}]。 +hostConfig.deployDescriptor.blocked=未部署上下文路径为[{0}]çš„Web应用程åºï¼Œå› ä¸ºå®ƒåŒ…å«ä¸€ä¸ªéƒ¨ç½²æ述符[{1}],该æ述符å¯èƒ½åŒ…å«å®‰å…¨éƒ¨ç½²åº”用程åºæ‰€éœ€çš„é…置,但此主机的DeployXML设置阻止了部署æ述符的处ç†ã€‚应该在[{2}]创建适当的æ述符æ¥éƒ¨ç½²æ­¤åº”用程åºã€‚ +hostConfig.deployDescriptor.error=部署æ述符[{0}]时出错 +hostConfig.deployDescriptor.finished=部署æ述符[{0}]的部署已在[{1}]mså†…å®Œæˆ +hostConfig.deployDescriptor.hiddenDir=使用外部docBase部署部署æ述符[{0}]æ„味ç€appBase中的目录[{1}]将被忽略 +hostConfig.deployDescriptor.hiddenWar=使用外部docBase部署部署æ述符[{0}]æ„味ç€appBase中的WAR[{1}]将被忽略 +hostConfig.deployDescriptor.localDocBaseSpecified=在主机appBase 中指定了docBase [{0}],将被忽略 +hostConfig.deployDescriptor.path=部署æ述符[{1}]中值为[{0}]的路径属性已被忽略 +hostConfig.deployDescriptor.threaded.error=等待部署æ述符的多线程部署完æˆæ—¶å‡ºé”™ +hostConfig.deployDir=把web 应用程åºéƒ¨ç½²åˆ°ç›®å½• [{0}] +hostConfig.deployDir.error=无法部署应用目录 [{0}] +hostConfig.deployDir.finished=Web应用程åºç›®å½•[{0}]的部署已在[{1}]æ¯«ç§’å†…å®Œæˆ +hostConfig.deployDir.threaded.error=等待目录的多线程部署完æˆæ—¶å‡ºé”™ +hostConfig.deployWar=正在部署web应用程åºå­˜æ¡£æ–‡ä»¶[{0}] +hostConfig.deployWar.error=部署 Web åº”ç”¨ç¨‹åº archive [{0}] 时出错 +hostConfig.deployWar.finished=web应用程åºå­˜æ¡£æ–‡ä»¶[{0}]的部署已在[{1}]mså†…å®Œæˆ +hostConfig.deployWar.hiddenDir=将忽略目录[{0}],因为WAR [{1}]优先,unpackWAR为false +hostConfig.deployWar.threaded.error=等待WAR文件的多线程部署完æˆæ—¶å‡ºé”™ +hostConfig.deploying=部署å‘现的web应用 +hostConfig.docBaseUrlInvalid=所æ供的部署目录无法用URLæ¥è¡¨ç¤º +hostConfig.expand=正在扩展web应用程åºå­˜æ¡£æ–‡ä»¶[{0}] +hostConfig.expand.error=解压WEB应用程åºæ–‡ä»¶[{0}]时异常 +hostConfig.ignorePath=忽略appBase中的路径[{0}]以进行自动部署 +hostConfig.illegalWarName=warå称[{0}]无效。存档将被忽略 +hostConfig.jmx.register=注册上下文[{0}]失败。 +hostConfig.jmx.unregister=移除注册上下文[{0}]失败 +hostConfig.migrateApp.threaded.error=等待é—留应用程åºçš„多线程è¿ç§»å®Œæˆæ—¶å‡ºé”™ +hostConfig.migrateError=è¿ç§»å¤±è´¥ +hostConfig.reload=é‡æ–°åŠ è½½ä¸Šä¸‹æ–‡[{0}] +hostConfig.resourceNotAbsolute=无法从上下文[{0}]中删除资æºï¼Œå› ä¸º[{1}]ä¸æ˜¯ç»å¯¹è·¯å¾„ +hostConfig.start=HostConfig: å¼€å§‹å¤„ç† +hostConfig.stop=Hosté…ç½®:åœæ­¢å¤„ç† +hostConfig.undeploy=正在å–消部署上下文[{0}] +hostConfig.undeployVersion=正在å–消部署没有活动会è¯çš„旧版本上下文[{0}] + +listener.createFailed=未å¯ç”¨å¯é€‰ä¾¦å¬å™¨[{0}] +listener.notServer=此侦å¬å™¨åªèƒ½åµŒå¥—在 Server 元素中,但ä½äºŽ [{0}] 中。\n + +passwdUserDatabase.readFail=无法从/etc/passwd获å–完整的用户集 + +tomcat.addWebapp.conflictChild=无法在[{0}]处部署到上下文路径[{1}],因为存在上下文[{2}] +tomcat.addWebapp.conflictFile=由于现有文件[{2}]的存在,无法在[{0}]å°†war部署到上下文路径[{1}] +tomcat.baseDirMakeFail=无法创建用作基本目录的目录[{0}] +tomcat.baseDirNotDir=基本目录指定的ä½ç½®[{0}]ä¸æ˜¯ä¸€ä¸ªç›®å½• +tomcat.defaultMimeTypeMappingsFail=无法加载默认MIME类型 +tomcat.homeDirMakeFail=无法创建用作主目录的目录 [{0}] +tomcat.invalidCommandLine=无效的命令行å‚æ•° [{0}] +tomcat.noContextClass=无法为主机[{1}]å’Œurl[{2}]实例化上下文类[{0}] +tomcat.noContextXml=ä¸èƒ½æ‰¾åˆ°web 应用的context.xml [{0}] + +userConfig.database=加载用户数æ®åº“异常 +userConfig.deploy=正在为用户[{0}]部署webåº”ç”¨ç¨‹åº +userConfig.deploy.threaded.error=等待用户目录的多线程部署完æˆæ—¶å‡ºé”™ +userConfig.deploying=正在部署用户 web åº”ç”¨ç¨‹åº +userConfig.error=为用户 [{0}]部署web应用å‘生错误 +userConfig.start=用户é…置:处ç†å¼€å§‹ +userConfig.stop=UserConfig:处ç†åœæ­¢ + +versionLoggerListener.arg=命令行å‚数: {0} +versionLoggerListener.catalina.base=CATALINA_BASE: {0} +versionLoggerListener.catalina.home=CATALINA_HOME: {0} +versionLoggerListener.env=环境å˜é‡ï¼š {0} = {1} +versionLoggerListener.java.home=Java 环境å˜é‡: {0} +versionLoggerListener.os.arch=架构: {0} +versionLoggerListener.os.name=æ“作系统å称: {0} +versionLoggerListener.os.version=OS.版本: {0} +versionLoggerListener.prop=系统属性: {0} = {1} +versionLoggerListener.serverInfo.server.built=æœåŠ¡å™¨æž„建: {0} +versionLoggerListener.serverInfo.server.number=æœåŠ¡å™¨ç‰ˆæœ¬å·: {0} +versionLoggerListener.serverInfo.server.version=Server.æœåŠ¡å™¨ç‰ˆæœ¬: {0} +versionLoggerListener.vm.vendor=JVM.供应商: {0} +versionLoggerListener.vm.version=Java虚拟机版本: {0} + +webAnnotationSet.invalidInjection=方法资æºæ³¨å…¥æ³¨è§£æ— æ•ˆã€‚ diff --git a/java/org/apache/catalina/startup/MimeTypeMappings.properties b/java/org/apache/catalina/startup/MimeTypeMappings.properties new file mode 100644 index 0000000..123a9af --- /dev/null +++ b/java/org/apache/catalina/startup/MimeTypeMappings.properties @@ -0,0 +1,1029 @@ +# 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. + +123=application/vnd.lotus-1-2-3 +3dml=text/vnd.in3d.3dml +3ds=image/x-3ds +3g2=video/3gpp2 +3gp=video/3gpp +7z=application/x-7z-compressed +aab=application/x-authorware-bin +aac=audio/x-aac +aam=application/x-authorware-map +aas=application/x-authorware-seg +abs=audio/x-mpeg +abw=application/x-abiword +ac=application/pkix-attr-cert +acc=application/vnd.americandynamics.acc +ace=application/x-ace-compressed +acu=application/vnd.acucobol +acutc=application/vnd.acucorp +adp=audio/adpcm +aep=application/vnd.audiograph +afm=application/x-font-type1 +afp=application/vnd.ibm.modcap +ahead=application/vnd.ahead.space +ai=application/postscript +aif=audio/x-aiff +aifc=audio/x-aiff +aiff=audio/x-aiff +aim=application/x-aim +air=application/vnd.adobe.air-application-installer-package+zip +ait=application/vnd.dvb.ait +ami=application/vnd.amiga.ami +anx=application/annodex +apk=application/vnd.android.package-archive +appcache=text/cache-manifest +application=application/x-ms-application +apr=application/vnd.lotus-approach +arc=application/x-freearc +art=image/x-jg +asc=application/pgp-signature +asf=video/x-ms-asf +asm=text/x-asm +aso=application/vnd.accpac.simply.aso +asx=video/x-ms-asf +atc=application/vnd.acucorp +atom=application/atom+xml +atomcat=application/atomcat+xml +atomsvc=application/atomsvc+xml +atx=application/vnd.antix.game-component +au=audio/basic +avi=video/x-msvideo +avx=video/x-rad-screenplay +aw=application/applixware +axa=audio/annodex +axv=video/annodex +azf=application/vnd.airzip.filesecure.azf +azs=application/vnd.airzip.filesecure.azs +azw=application/vnd.amazon.ebook +bat=application/x-msdownload +bcpio=application/x-bcpio +bdf=application/x-font-bdf +bdm=application/vnd.syncml.dm+wbxml +bed=application/vnd.realvnc.bed +bh2=application/vnd.fujitsu.oasysprs +bin=application/octet-stream +blb=application/x-blorb +blorb=application/x-blorb +bmi=application/vnd.bmi +bmp=image/bmp +body=text/html +book=application/vnd.framemaker +box=application/vnd.previewsystems.box +boz=application/x-bzip2 +bpk=application/octet-stream +btif=image/prs.btif +bz=application/x-bzip +bz2=application/x-bzip2 +c=text/x-c +c11amc=application/vnd.cluetrust.cartomobile-config +c11amz=application/vnd.cluetrust.cartomobile-config-pkg +c4d=application/vnd.clonk.c4group +c4f=application/vnd.clonk.c4group +c4g=application/vnd.clonk.c4group +c4p=application/vnd.clonk.c4group +c4u=application/vnd.clonk.c4group +cab=application/vnd.ms-cab-compressed +caf=audio/x-caf +cap=application/vnd.tcpdump.pcap +car=application/vnd.curl.car +cat=application/vnd.ms-pki.seccat +cb7=application/x-cbr +cba=application/x-cbr +cbr=application/x-cbr +cbt=application/x-cbr +cbz=application/x-cbr +cc=text/x-c +cct=application/x-director +ccxml=application/ccxml+xml +cdbcmsg=application/vnd.contact.cmsg +cdf=application/x-cdf +cdkey=application/vnd.mediastation.cdkey +cdmia=application/cdmi-capability +cdmic=application/cdmi-container +cdmid=application/cdmi-domain +cdmio=application/cdmi-object +cdmiq=application/cdmi-queue +cdx=chemical/x-cdx +cdxml=application/vnd.chemdraw+xml +cdy=application/vnd.cinderella +cer=application/pkix-cert +cfs=application/x-cfs-compressed +cgm=image/cgm +chat=application/x-chat +chm=application/vnd.ms-htmlhelp +chrt=application/vnd.kde.kchart +cif=chemical/x-cif +cii=application/vnd.anser-web-certificate-issue-initiation +cil=application/vnd.ms-artgalry +cla=application/vnd.claymore +class=application/java +clkk=application/vnd.crick.clicker.keyboard +clkp=application/vnd.crick.clicker.palette +clkt=application/vnd.crick.clicker.template +clkw=application/vnd.crick.clicker.wordbank +clkx=application/vnd.crick.clicker +clp=application/x-msclip +cmc=application/vnd.cosmocaller +cmdf=chemical/x-cmdf +cml=chemical/x-cml +cmp=application/vnd.yellowriver-custom-menu +cmx=image/x-cmx +cod=application/vnd.rim.cod +com=application/x-msdownload +conf=text/plain +cpio=application/x-cpio +cpp=text/x-c +cpt=application/mac-compactpro +crd=application/x-mscardfile +crl=application/pkix-crl +crt=application/x-x509-ca-cert +cryptonote=application/vnd.rig.cryptonote +csh=application/x-csh +csml=chemical/x-csml +csp=application/vnd.commonspace +css=text/css +cst=application/x-director +csv=text/csv +cu=application/cu-seeme +curl=text/vnd.curl +cww=application/prs.cww +cxt=application/x-director +cxx=text/x-c +dae=model/vnd.collada+xml +daf=application/vnd.mobius.daf +dart=application/vnd.dart +dataless=application/vnd.fdsn.seed +davmount=application/davmount+xml +dbk=application/docbook+xml +dcr=application/x-director +dcurl=text/vnd.curl.dcurl +dd2=application/vnd.oma.dd2+xml +ddd=application/vnd.fujixerox.ddd +deb=application/x-debian-package +def=text/plain +deploy=application/octet-stream +der=application/x-x509-ca-cert +dfac=application/vnd.dreamfactory +dgc=application/x-dgc-compressed +dib=image/bmp +dic=text/x-c +dir=application/x-director +dis=application/vnd.mobius.dis +dist=application/octet-stream +distz=application/octet-stream +djv=image/vnd.djvu +djvu=image/vnd.djvu +dll=application/x-msdownload +dmg=application/x-apple-diskimage +dmp=application/vnd.tcpdump.pcap +dms=application/octet-stream +dna=application/vnd.dna +doc=application/msword +docm=application/vnd.ms-word.document.macroenabled.12 +docx=application/vnd.openxmlformats-officedocument.wordprocessingml.document +dot=application/msword +dotm=application/vnd.ms-word.template.macroenabled.12 +dotx=application/vnd.openxmlformats-officedocument.wordprocessingml.template +dp=application/vnd.osgi.dp +dpg=application/vnd.dpgraph +dra=audio/vnd.dra +dsc=text/prs.lines.tag +dssc=application/dssc+der +dtb=application/x-dtbook+xml +dtd=application/xml-dtd +dts=audio/vnd.dts +dtshd=audio/vnd.dts.hd +dump=application/octet-stream +dv=video/x-dv +dvb=video/vnd.dvb.file +dvi=application/x-dvi +dwf=model/vnd.dwf +dwg=image/vnd.dwg +dxf=image/vnd.dxf +dxp=application/vnd.spotfire.dxp +dxr=application/x-director +ecelp4800=audio/vnd.nuera.ecelp4800 +ecelp7470=audio/vnd.nuera.ecelp7470 +ecelp9600=audio/vnd.nuera.ecelp9600 +ecma=application/ecmascript +edm=application/vnd.novadigm.edm +edx=application/vnd.novadigm.edx +efif=application/vnd.picsel +ei6=application/vnd.pg.osasli +elc=application/octet-stream +emf=application/x-msmetafile +eml=message/rfc822 +emma=application/emma+xml +emz=application/x-msmetafile +eol=audio/vnd.digital-winds +eot=application/vnd.ms-fontobject +eps=application/postscript +epub=application/epub+zip +es3=application/vnd.eszigno3+xml +esa=application/vnd.osgi.subsystem +esf=application/vnd.epson.esf +et3=application/vnd.eszigno3+xml +etx=text/x-setext +eva=application/x-eva +evy=application/x-envoy +exe=application/octet-stream +exi=application/exi +ext=application/vnd.novadigm.ext +ez=application/andrew-inset +ez2=application/vnd.ezpix-album +ez3=application/vnd.ezpix-package +f=text/x-fortran +f4v=video/x-f4v +f77=text/x-fortran +f90=text/x-fortran +fbs=image/vnd.fastbidsheet +fcdt=application/vnd.adobe.formscentral.fcdt +fcs=application/vnd.isac.fcs +fdf=application/vnd.fdf +fe_launch=application/vnd.denovo.fcselayout-link +fg5=application/vnd.fujitsu.oasysgp +fgd=application/x-director +fh=image/x-freehand +fh4=image/x-freehand +fh5=image/x-freehand +fh7=image/x-freehand +fhc=image/x-freehand +fig=application/x-xfig +flac=audio/flac +fli=video/x-fli +flo=application/vnd.micrografx.flo +flv=video/x-flv +flw=application/vnd.kde.kivio +flx=text/vnd.fmi.flexstor +fly=text/vnd.fly +fm=application/vnd.framemaker +fnc=application/vnd.frogans.fnc +for=text/x-fortran +fpx=image/vnd.fpx +frame=application/vnd.framemaker +fsc=application/vnd.fsc.weblaunch +fst=image/vnd.fst +ftc=application/vnd.fluxtime.clip +fti=application/vnd.anser-web-funds-transfer-initiation +fvt=video/vnd.fvt +fxp=application/vnd.adobe.fxp +fxpl=application/vnd.adobe.fxp +fzs=application/vnd.fuzzysheet +g2w=application/vnd.geoplan +g3=image/g3fax +g3w=application/vnd.geospace +gac=application/vnd.groove-account +gam=application/x-tads +gbr=application/rpki-ghostbusters +gca=application/x-gca-compressed +gdl=model/vnd.gdl +geo=application/vnd.dynageo +gex=application/vnd.geometry-explorer +ggb=application/vnd.geogebra.file +ggs=application/vnd.geogebra.slides +ggt=application/vnd.geogebra.tool +ghf=application/vnd.groove-help +gif=image/gif +gim=application/vnd.groove-identity-message +gml=application/gml+xml +gmx=application/vnd.gmx +gnumeric=application/x-gnumeric +gph=application/vnd.flographit +gpx=application/gpx+xml +gqf=application/vnd.grafeq +gqs=application/vnd.grafeq +gram=application/srgs +gramps=application/x-gramps-xml +gre=application/vnd.geometry-explorer +grv=application/vnd.groove-injector +grxml=application/srgs+xml +gsf=application/x-font-ghostscript +gtar=application/x-gtar +gtm=application/vnd.groove-tool-message +gtw=model/vnd.gtw +gv=text/vnd.graphviz +gxf=application/gxf +gxt=application/vnd.geonext +gz=application/x-gzip +h=text/x-c +h261=video/h261 +h263=video/h263 +h264=video/h264 +hal=application/vnd.hal+xml +hbci=application/vnd.hbci +hdf=application/x-hdf +hh=text/x-c +hlp=application/winhlp +hpgl=application/vnd.hp-hpgl +hpid=application/vnd.hp-hpid +hps=application/vnd.hp-hps +hqx=application/mac-binhex40 +htc=text/x-component +htke=application/vnd.kenameaapp +htm=text/html +html=text/html +hvd=application/vnd.yamaha.hv-dic +hvp=application/vnd.yamaha.hv-voice +hvs=application/vnd.yamaha.hv-script +i2g=application/vnd.intergeo +icc=application/vnd.iccprofile +ice=x-conference/x-cooltalk +icm=application/vnd.iccprofile +ico=image/x-icon +ics=text/calendar +ief=image/ief +ifb=text/calendar +ifm=application/vnd.shana.informed.formdata +iges=model/iges +igl=application/vnd.igloader +igm=application/vnd.insors.igm +igs=model/iges +igx=application/vnd.micrografx.igx +iif=application/vnd.shana.informed.interchange +imp=application/vnd.accpac.simply.imp +ims=application/vnd.ms-ims +in=text/plain +ink=application/inkml+xml +inkml=application/inkml+xml +install=application/x-install-instructions +iota=application/vnd.astraea-software.iota +ipfix=application/ipfix +ipk=application/vnd.shana.informed.package +irm=application/vnd.ibm.rights-management +irp=application/vnd.irepository.package+xml +iso=application/x-iso9660-image +itp=application/vnd.shana.informed.formtemplate +ivp=application/vnd.immervision-ivp +ivu=application/vnd.immervision-ivu +jad=text/vnd.sun.j2me.app-descriptor +jam=application/vnd.jam +jar=application/java-archive +java=text/x-java-source +jisp=application/vnd.jisp +jlt=application/vnd.hp-jlyt +jnlp=application/x-java-jnlp-file +joda=application/vnd.joost.joda-archive +jpe=image/jpeg +jpeg=image/jpeg +jpg=image/jpeg +jpgm=video/jpm +jpgv=video/jpeg +jpm=video/jpm +js=text/javascript +jsf=text/plain +json=application/json +jsonml=application/jsonml+json +jspf=text/plain +kar=audio/midi +karbon=application/vnd.kde.karbon +kfo=application/vnd.kde.kformula +kia=application/vnd.kidspiration +kml=application/vnd.google-earth.kml+xml +kmz=application/vnd.google-earth.kmz +kne=application/vnd.kinar +knp=application/vnd.kinar +kon=application/vnd.kde.kontour +kpr=application/vnd.kde.kpresenter +kpt=application/vnd.kde.kpresenter +kpxx=application/vnd.ds-keypoint +ksp=application/vnd.kde.kspread +ktr=application/vnd.kahootz +ktx=image/ktx +ktz=application/vnd.kahootz +kwd=application/vnd.kde.kword +kwt=application/vnd.kde.kword +lasxml=application/vnd.las.las+xml +latex=application/x-latex +lbd=application/vnd.llamagraphics.life-balance.desktop +lbe=application/vnd.llamagraphics.life-balance.exchange+xml +les=application/vnd.hhe.lesson-player +lha=application/x-lzh-compressed +link66=application/vnd.route66.link66+xml +list=text/plain +list3820=application/vnd.ibm.modcap +listafp=application/vnd.ibm.modcap +lnk=application/x-ms-shortcut +log=text/plain +lostxml=application/lost+xml +lrf=application/octet-stream +lrm=application/vnd.ms-lrm +ltf=application/vnd.frogans.ltf +lvp=audio/vnd.lucent.voice +lwp=application/vnd.lotus-wordpro +lzh=application/x-lzh-compressed +m13=application/x-msmediaview +m14=application/x-msmediaview +m1v=video/mpeg +m21=application/mp21 +m2a=audio/mpeg +m2v=video/mpeg +m3a=audio/mpeg +m3u=audio/x-mpegurl +m3u8=application/vnd.apple.mpegurl +m4a=audio/mp4 +m4b=audio/mp4 +m4r=audio/mp4 +m4u=video/vnd.mpegurl +m4v=video/mp4 +ma=application/mathematica +mac=image/x-macpaint +mads=application/mads+xml +mag=application/vnd.ecowin.chart +maker=application/vnd.framemaker +man=text/troff +mar=application/octet-stream +mathml=application/mathml+xml +mb=application/mathematica +mbk=application/vnd.mobius.mbk +mbox=application/mbox +mc1=application/vnd.medcalcdata +mcd=application/vnd.mcd +mcurl=text/vnd.curl.mcurl +mdb=application/x-msaccess +mdi=image/vnd.ms-modi +me=text/troff +mesh=model/mesh +meta4=application/metalink4+xml +metalink=application/metalink+xml +mets=application/mets+xml +mfm=application/vnd.mfmp +mft=application/rpki-manifest +mgp=application/vnd.osgeo.mapguide.package +mgz=application/vnd.proteus.magazine +mid=audio/midi +midi=audio/midi +mie=application/x-mie +mif=application/x-mif +mime=message/rfc822 +mj2=video/mj2 +mjp2=video/mj2 +mjs=text/javascript +mk3d=video/x-matroska +mka=audio/x-matroska +mks=video/x-matroska +mkv=video/x-matroska +mlp=application/vnd.dolby.mlp +mmd=application/vnd.chipnuts.karaoke-mmd +mmf=application/vnd.smaf +mmr=image/vnd.fujixerox.edmics-mmr +mng=video/x-mng +mny=application/x-msmoney +mobi=application/x-mobipocket-ebook +mods=application/mods+xml +mov=video/quicktime +movie=video/x-sgi-movie +mp1=audio/mpeg +mp2=audio/mpeg +mp21=application/mp21 +mp2a=audio/mpeg +mp3=audio/mpeg +mp4=video/mp4 +mp4a=audio/mp4 +mp4s=application/mp4 +mp4v=video/mp4 +mpa=audio/mpeg +mpc=application/vnd.mophun.certificate +mpe=video/mpeg +mpeg=video/mpeg +mpega=audio/x-mpeg +mpg=video/mpeg +mpg4=video/mp4 +mpga=audio/mpeg +mpkg=application/vnd.apple.installer+xml +mpm=application/vnd.blueice.multipass +mpn=application/vnd.mophun.application +mpp=application/vnd.ms-project +mpt=application/vnd.ms-project +mpv2=video/mpeg2 +mpy=application/vnd.ibm.minipay +mqy=application/vnd.mobius.mqy +mrc=application/marc +mrcx=application/marcxml+xml +ms=text/troff +mscml=application/mediaservercontrol+xml +mseed=application/vnd.fdsn.mseed +mseq=application/vnd.mseq +msf=application/vnd.epson.msf +msh=model/mesh +msi=application/x-msdownload +msl=application/vnd.mobius.msl +msty=application/vnd.muvee.style +mts=model/vnd.mts +mus=application/vnd.musician +musicxml=application/vnd.recordare.musicxml+xml +mvb=application/x-msmediaview +mwf=application/vnd.mfer +mxf=application/mxf +mxl=application/vnd.recordare.musicxml +mxml=application/xv+xml +mxs=application/vnd.triscape.mxs +mxu=video/vnd.mpegurl +n-gage=application/vnd.nokia.n-gage.symbian.install +n3=text/n3 +nb=application/mathematica +nbp=application/vnd.wolfram.player +nc=application/x-netcdf +ncx=application/x-dtbncx+xml +nfo=text/x-nfo +ngdat=application/vnd.nokia.n-gage.data +nitf=application/vnd.nitf +nlu=application/vnd.neurolanguage.nlu +nml=application/vnd.enliven +nnd=application/vnd.noblenet-directory +nns=application/vnd.noblenet-sealer +nnw=application/vnd.noblenet-web +npx=image/vnd.net-fpx +nsc=application/x-conference +nsf=application/vnd.lotus-notes +ntf=application/vnd.nitf +nzb=application/x-nzb +oa2=application/vnd.fujitsu.oasys2 +oa3=application/vnd.fujitsu.oasys3 +oas=application/vnd.fujitsu.oasys +obd=application/x-msbinder +obj=application/x-tgif +oda=application/oda +odb=application/vnd.oasis.opendocument.database +odc=application/vnd.oasis.opendocument.chart +odf=application/vnd.oasis.opendocument.formula +odft=application/vnd.oasis.opendocument.formula-template +odg=application/vnd.oasis.opendocument.graphics +odi=application/vnd.oasis.opendocument.image +odm=application/vnd.oasis.opendocument.text-master +odp=application/vnd.oasis.opendocument.presentation +ods=application/vnd.oasis.opendocument.spreadsheet +odt=application/vnd.oasis.opendocument.text +oga=audio/ogg +ogg=audio/ogg +ogv=video/ogg +ogx=application/ogg +omdoc=application/omdoc+xml +onepkg=application/onenote +onetmp=application/onenote +onetoc=application/onenote +onetoc2=application/onenote +opf=application/oebps-package+xml +opml=text/x-opml +oprc=application/vnd.palm +opus=audio/ogg +org=application/vnd.lotus-organizer +osf=application/vnd.yamaha.openscoreformat +osfpvg=application/vnd.yamaha.openscoreformat.osfpvg+xml +otc=application/vnd.oasis.opendocument.chart-template +otf=font/otf +otg=application/vnd.oasis.opendocument.graphics-template +oth=application/vnd.oasis.opendocument.text-web +oti=application/vnd.oasis.opendocument.image-template +otp=application/vnd.oasis.opendocument.presentation-template +ots=application/vnd.oasis.opendocument.spreadsheet-template +ott=application/vnd.oasis.opendocument.text-template +oxps=application/oxps +oxt=application/vnd.openofficeorg.extension +p=text/x-pascal +p10=application/pkcs10 +p12=application/x-pkcs12 +p7b=application/x-pkcs7-certificates +p7c=application/pkcs7-mime +p7m=application/pkcs7-mime +p7r=application/x-pkcs7-certreqresp +p7s=application/pkcs7-signature +p8=application/pkcs8 +pas=text/x-pascal +paw=application/vnd.pawaafile +pbd=application/vnd.powerbuilder6 +pbm=image/x-portable-bitmap +pcap=application/vnd.tcpdump.pcap +pcf=application/x-font-pcf +pcl=application/vnd.hp-pcl +pclxl=application/vnd.hp-pclxl +pct=image/pict +pcurl=application/vnd.curl.pcurl +pcx=image/x-pcx +pdb=application/vnd.palm +pdf=application/pdf +pfa=application/x-font-type1 +pfb=application/x-font-type1 +pfm=application/x-font-type1 +pfr=application/font-tdpfr +pfx=application/x-pkcs12 +pgm=image/x-portable-graymap +pgn=application/x-chess-pgn +pgp=application/pgp-encrypted +pic=image/pict +pict=image/pict +pkg=application/octet-stream +pki=application/pkixcmp +pkipath=application/pkix-pkipath +plb=application/vnd.3gpp.pic-bw-large +plc=application/vnd.mobius.plc +plf=application/vnd.pocketlearn +pls=audio/x-scpls +pml=application/vnd.ctc-posml +png=image/png +pnm=image/x-portable-anymap +pnt=image/x-macpaint +portpkg=application/vnd.macports.portpkg +pot=application/vnd.ms-powerpoint +potm=application/vnd.ms-powerpoint.template.macroenabled.12 +potx=application/vnd.openxmlformats-officedocument.presentationml.template +ppam=application/vnd.ms-powerpoint.addin.macroenabled.12 +ppd=application/vnd.cups-ppd +ppm=image/x-portable-pixmap +pps=application/vnd.ms-powerpoint +ppsm=application/vnd.ms-powerpoint.slideshow.macroenabled.12 +ppsx=application/vnd.openxmlformats-officedocument.presentationml.slideshow +ppt=application/vnd.ms-powerpoint +pptm=application/vnd.ms-powerpoint.presentation.macroenabled.12 +pptx=application/vnd.openxmlformats-officedocument.presentationml.presentation +pqa=application/vnd.palm +prc=application/x-mobipocket-ebook +pre=application/vnd.lotus-freelance +prf=application/pics-rules +ps=application/postscript +psb=application/vnd.3gpp.pic-bw-small +psd=image/vnd.adobe.photoshop +psf=application/x-font-linux-psf +pskcxml=application/pskc+xml +ptid=application/vnd.pvi.ptid1 +pub=application/x-mspublisher +pvb=application/vnd.3gpp.pic-bw-var +pwn=application/vnd.3m.post-it-notes +pya=audio/vnd.ms-playready.media.pya +pyv=video/vnd.ms-playready.media.pyv +qam=application/vnd.epson.quickanime +qbo=application/vnd.intu.qbo +qfx=application/vnd.intu.qfx +qps=application/vnd.publishare-delta-tree +qt=video/quicktime +qti=image/x-quicktime +qtif=image/x-quicktime +qwd=application/vnd.quark.quarkxpress +qwt=application/vnd.quark.quarkxpress +qxb=application/vnd.quark.quarkxpress +qxd=application/vnd.quark.quarkxpress +qxl=application/vnd.quark.quarkxpress +qxt=application/vnd.quark.quarkxpress +ra=audio/x-pn-realaudio +ram=audio/x-pn-realaudio +rar=application/x-rar-compressed +ras=image/x-cmu-raster +rcprofile=application/vnd.ipunplugged.rcprofile +rdf=application/rdf+xml +rdz=application/vnd.data-vision.rdz +rep=application/vnd.businessobjects +res=application/x-dtbresource+xml +rgb=image/x-rgb +rif=application/reginfo+xml +rip=audio/vnd.rip +ris=application/x-research-info-systems +rl=application/resource-lists+xml +rlc=image/vnd.fujixerox.edmics-rlc +rld=application/resource-lists-diff+xml +rm=application/vnd.rn-realmedia +rmi=audio/midi +rmp=audio/x-pn-realaudio-plugin +rms=application/vnd.jcp.javame.midlet-rms +rmvb=application/vnd.rn-realmedia-vbr +rnc=application/relax-ng-compact-syntax +roa=application/rpki-roa +roff=text/troff +rp9=application/vnd.cloanto.rp9 +rpss=application/vnd.nokia.radio-presets +rpst=application/vnd.nokia.radio-preset +rq=application/sparql-query +rs=application/rls-services+xml +rsd=application/rsd+xml +rss=application/rss+xml +rtf=application/rtf +rtx=text/richtext +s=text/x-asm +s3m=audio/s3m +saf=application/vnd.yamaha.smaf-audio +sbml=application/sbml+xml +sc=application/vnd.ibm.secure-container +scd=application/x-msschedule +scm=application/vnd.lotus-screencam +scq=application/scvp-cv-request +scs=application/scvp-cv-response +scurl=text/vnd.curl.scurl +sda=application/vnd.stardivision.draw +sdc=application/vnd.stardivision.calc +sdd=application/vnd.stardivision.impress +sdkd=application/vnd.solent.sdkm+xml +sdkm=application/vnd.solent.sdkm+xml +sdp=application/sdp +sdw=application/vnd.stardivision.writer +see=application/vnd.seemail +seed=application/vnd.fdsn.seed +sema=application/vnd.sema +semd=application/vnd.semd +semf=application/vnd.semf +ser=application/java-serialized-object +setpay=application/set-payment-initiation +setreg=application/set-registration-initiation +sfd-hdstx=application/vnd.hydrostatix.sof-data +sfs=application/vnd.spotfire.sfs +sfv=text/x-sfv +sgi=image/sgi +sgl=application/vnd.stardivision.writer-global +sgm=text/sgml +sgml=text/sgml +sh=application/x-sh +shar=application/x-shar +shf=application/shf+xml +sid=image/x-mrsid-image +sig=application/pgp-signature +sil=audio/silk +silo=model/mesh +sis=application/vnd.symbian.install +sisx=application/vnd.symbian.install +sit=application/x-stuffit +sitx=application/x-stuffitx +skd=application/vnd.koan +skm=application/vnd.koan +skp=application/vnd.koan +skt=application/vnd.koan +sldm=application/vnd.ms-powerpoint.slide.macroenabled.12 +sldx=application/vnd.openxmlformats-officedocument.presentationml.slide +slt=application/vnd.epson.salt +sm=application/vnd.stepmania.stepchart +smf=application/vnd.stardivision.math +smi=application/smil+xml +smil=application/smil+xml +smv=video/x-smv +smzip=application/vnd.stepmania.package +snd=audio/basic +snf=application/x-font-snf +so=application/octet-stream +spc=application/x-pkcs7-certificates +spf=application/vnd.yamaha.smaf-phrase +spl=application/x-futuresplash +spot=text/vnd.in3d.spot +spp=application/scvp-vp-response +spq=application/scvp-vp-request +spx=audio/ogg +sql=application/x-sql +src=application/x-wais-source +srt=application/x-subrip +sru=application/sru+xml +srx=application/sparql-results+xml +ssdl=application/ssdl+xml +sse=application/vnd.kodak-descriptor +ssf=application/vnd.epson.ssf +ssml=application/ssml+xml +st=application/vnd.sailingtracker.track +stc=application/vnd.sun.xml.calc.template +std=application/vnd.sun.xml.draw.template +stf=application/vnd.wt.stf +sti=application/vnd.sun.xml.impress.template +stk=application/hyperstudio +stl=application/vnd.ms-pki.stl +str=application/vnd.pg.format +stw=application/vnd.sun.xml.writer.template +sub=text/vnd.dvb.subtitle +sus=application/vnd.sus-calendar +susp=application/vnd.sus-calendar +sv4cpio=application/x-sv4cpio +sv4crc=application/x-sv4crc +svc=application/vnd.dvb.service +svd=application/vnd.svd +svg=image/svg+xml +svgz=image/svg+xml +swa=application/x-director +swf=application/x-shockwave-flash +swi=application/vnd.aristanetworks.swi +sxc=application/vnd.sun.xml.calc +sxd=application/vnd.sun.xml.draw +sxg=application/vnd.sun.xml.writer.global +sxi=application/vnd.sun.xml.impress +sxm=application/vnd.sun.xml.math +sxw=application/vnd.sun.xml.writer +t=text/troff +t3=application/x-t3vm-image +taglet=application/vnd.mynfc +tao=application/vnd.tao.intent-module-archive +tar=application/x-tar +tcap=application/vnd.3gpp2.tcap +tcl=application/x-tcl +teacher=application/vnd.smart.teacher +tei=application/tei+xml +teicorpus=application/tei+xml +tex=application/x-tex +texi=application/x-texinfo +texinfo=application/x-texinfo +text=text/plain +tfi=application/thraud+xml +tfm=application/x-tex-tfm +tga=image/x-tga +thmx=application/vnd.ms-officetheme +tif=image/tiff +tiff=image/tiff +tmo=application/vnd.tmobile-livetv +torrent=application/x-bittorrent +tpl=application/vnd.groove-tool-template +tpt=application/vnd.trid.tpt +tr=text/troff +tra=application/vnd.trueapp +trm=application/x-msterminal +tsd=application/timestamped-data +tsv=text/tab-separated-values +ttc=font/collection +ttf=font/ttf +ttl=text/turtle +twd=application/vnd.simtech-mindmapper +twds=application/vnd.simtech-mindmapper +txd=application/vnd.genomatix.tuxedo +txf=application/vnd.mobius.txf +txt=text/plain +u32=application/x-authorware-bin +udeb=application/x-debian-package +ufd=application/vnd.ufdl +ufdl=application/vnd.ufdl +ulw=audio/basic +ulx=application/x-glulx +umj=application/vnd.umajin +unityweb=application/vnd.unity +uoml=application/vnd.uoml+xml +uri=text/uri-list +uris=text/uri-list +urls=text/uri-list +ustar=application/x-ustar +utz=application/vnd.uiq.theme +uu=text/x-uuencode +uva=audio/vnd.dece.audio +uvd=application/vnd.dece.data +uvf=application/vnd.dece.data +uvg=image/vnd.dece.graphic +uvh=video/vnd.dece.hd +uvi=image/vnd.dece.graphic +uvm=video/vnd.dece.mobile +uvp=video/vnd.dece.pd +uvs=video/vnd.dece.sd +uvt=application/vnd.dece.ttml+xml +uvu=video/vnd.uvvu.mp4 +uvv=video/vnd.dece.video +uvva=audio/vnd.dece.audio +uvvd=application/vnd.dece.data +uvvf=application/vnd.dece.data +uvvg=image/vnd.dece.graphic +uvvh=video/vnd.dece.hd +uvvi=image/vnd.dece.graphic +uvvm=video/vnd.dece.mobile +uvvp=video/vnd.dece.pd +uvvs=video/vnd.dece.sd +uvvt=application/vnd.dece.ttml+xml +uvvu=video/vnd.uvvu.mp4 +uvvv=video/vnd.dece.video +uvvx=application/vnd.dece.unspecified +uvvz=application/vnd.dece.zip +uvx=application/vnd.dece.unspecified +uvz=application/vnd.dece.zip +vcard=text/vcard +vcd=application/x-cdlink +vcf=text/x-vcard +vcg=application/vnd.groove-vcard +vcs=text/x-vcalendar +vcx=application/vnd.vcx +vis=application/vnd.visionary +viv=video/vnd.vivo +vob=video/x-ms-vob +vor=application/vnd.stardivision.writer +vox=application/x-authorware-bin +vrml=model/vrml +vsd=application/vnd.visio +vsf=application/vnd.vsf +vss=application/vnd.visio +vst=application/vnd.visio +vsw=application/vnd.visio +vtu=model/vnd.vtu +vxml=application/voicexml+xml +w3d=application/x-director +wad=application/x-doom +wasm=application/wasm +wav=audio/x-wav +wax=audio/x-ms-wax +wbmp=image/vnd.wap.wbmp +wbs=application/vnd.criticaltools.wbs+xml +wbxml=application/vnd.wap.wbxml +wcm=application/vnd.ms-works +wdb=application/vnd.ms-works +wdp=image/vnd.ms-photo +weba=audio/webm +webm=video/webm +webp=image/webp +wg=application/vnd.pmi.widget +wgt=application/widget +wks=application/vnd.ms-works +wm=video/x-ms-wm +wma=audio/x-ms-wma +wmd=application/x-ms-wmd +wmf=application/x-msmetafile +wml=text/vnd.wap.wml +wmlc=application/vnd.wap.wmlc +wmls=text/vnd.wap.wmlscript +wmlsc=application/vnd.wap.wmlscriptc +wmv=video/x-ms-wmv +wmx=video/x-ms-wmx +wmz=application/x-msmetafile +woff=font/woff +woff2=font/woff2 +wpd=application/vnd.wordperfect +wpl=application/vnd.ms-wpl +wps=application/vnd.ms-works +wqd=application/vnd.wqd +wri=application/x-mswrite +wrl=model/vrml +wsdl=application/wsdl+xml +wspolicy=application/wspolicy+xml +wtb=application/vnd.webturbo +wvx=video/x-ms-wvx +x32=application/x-authorware-bin +x3d=model/x3d+xml +x3db=model/x3d+binary +x3dbz=model/x3d+binary +x3dv=model/x3d+vrml +x3dvz=model/x3d+vrml +x3dz=model/x3d+xml +xaml=application/xaml+xml +xap=application/x-silverlight-app +xar=application/vnd.xara +xbap=application/x-ms-xbap +xbd=application/vnd.fujixerox.docuworks.binder +xbm=image/x-xbitmap +xdf=application/xcap-diff+xml +xdm=application/vnd.syncml.dm+xml +xdp=application/vnd.adobe.xdp+xml +xdssc=application/dssc+xml +xdw=application/vnd.fujixerox.docuworks +xenc=application/xenc+xml +xer=application/patch-ops-error+xml +xfdf=application/vnd.adobe.xfdf +xfdl=application/vnd.xfdl +xht=application/xhtml+xml +xhtml=application/xhtml+xml +xhvml=application/xv+xml +xif=image/vnd.xiff +xla=application/vnd.ms-excel +xlam=application/vnd.ms-excel.addin.macroenabled.12 +xlc=application/vnd.ms-excel +xlf=application/x-xliff+xml +xlm=application/vnd.ms-excel +xls=application/vnd.ms-excel +xlsb=application/vnd.ms-excel.sheet.binary.macroenabled.12 +xlsm=application/vnd.ms-excel.sheet.macroenabled.12 +xlsx=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet +xlt=application/vnd.ms-excel +xltm=application/vnd.ms-excel.template.macroenabled.12 +xltx=application/vnd.openxmlformats-officedocument.spreadsheetml.template +xlw=application/vnd.ms-excel +xm=audio/xm +xml=application/xml +xo=application/vnd.olpc-sugar +xop=application/xop+xml +xpi=application/x-xpinstall +xpl=application/xproc+xml +xpm=image/x-xpixmap +xpr=application/vnd.is-xpr +xps=application/vnd.ms-xpsdocument +xpw=application/vnd.intercon.formnet +xpx=application/vnd.intercon.formnet +xsl=application/xml +xslt=application/xslt+xml +xsm=application/vnd.syncml+xml +xspf=application/xspf+xml +xul=application/vnd.mozilla.xul+xml +xvm=application/xv+xml +xvml=application/xv+xml +xwd=image/x-xwindowdump +xyz=chemical/x-xyz +xz=application/x-xz +yang=application/yang +yin=application/yin+xml +z=application/x-compress +z1=application/x-zmachine +z2=application/x-zmachine +z3=application/x-zmachine +z4=application/x-zmachine +z5=application/x-zmachine +z6=application/x-zmachine +z7=application/x-zmachine +z8=application/x-zmachine +zaz=application/vnd.zzazz.deck+xml +zip=application/zip +zir=application/vnd.zul +zirz=application/vnd.zul +zmm=application/vnd.handheld-entertainment+xml diff --git a/java/org/apache/catalina/startup/NamingRuleSet.java b/java/org/apache/catalina/startup/NamingRuleSet.java new file mode 100644 index 0000000..2e6c879 --- /dev/null +++ b/java/org/apache/catalina/startup/NamingRuleSet.java @@ -0,0 +1,105 @@ +/* + * 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.catalina.startup; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; + +/** + * RuleSet for processing the JNDI Enterprise Naming Context resource declaration elements. + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class NamingRuleSet implements RuleSet { + + // ----------------------------------------------------- Instance Variables + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + + // ------------------------------------------------------------ Constructor + + /** + * Construct an instance of this RuleSet with the default matching pattern prefix. + */ + public NamingRuleSet() { + this(""); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + */ + public NamingRuleSet(String prefix) { + this.prefix = prefix; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Add the set of Rule instances defined in this RuleSet to the specified Digester instance, + * associating them with our namespace URI (if any). This method should only be called by a Digester instance. + * + * @param digester Digester instance to which the new Rule instances should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + + digester.addObjectCreate(prefix + "Ejb", "org.apache.tomcat.util.descriptor.web.ContextEjb"); + digester.addSetProperties(prefix + "Ejb"); + digester.addRule(prefix + "Ejb", + new SetNextNamingRule("addEjb", "org.apache.tomcat.util.descriptor.web.ContextEjb")); + + digester.addObjectCreate(prefix + "Environment", "org.apache.tomcat.util.descriptor.web.ContextEnvironment"); + digester.addSetProperties(prefix + "Environment"); + digester.addRule(prefix + "Environment", + new SetNextNamingRule("addEnvironment", "org.apache.tomcat.util.descriptor.web.ContextEnvironment")); + + digester.addObjectCreate(prefix + "LocalEjb", "org.apache.tomcat.util.descriptor.web.ContextLocalEjb"); + digester.addSetProperties(prefix + "LocalEjb"); + digester.addRule(prefix + "LocalEjb", + new SetNextNamingRule("addLocalEjb", "org.apache.tomcat.util.descriptor.web.ContextLocalEjb")); + + digester.addObjectCreate(prefix + "Resource", "org.apache.tomcat.util.descriptor.web.ContextResource"); + digester.addSetProperties(prefix + "Resource"); + digester.addRule(prefix + "Resource", + new SetNextNamingRule("addResource", "org.apache.tomcat.util.descriptor.web.ContextResource")); + + digester.addObjectCreate(prefix + "ResourceEnvRef", + "org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef"); + digester.addSetProperties(prefix + "ResourceEnvRef"); + digester.addRule(prefix + "ResourceEnvRef", new SetNextNamingRule("addResourceEnvRef", + "org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef")); + + digester.addObjectCreate(prefix + "ServiceRef", "org.apache.tomcat.util.descriptor.web.ContextService"); + digester.addSetProperties(prefix + "ServiceRef"); + digester.addRule(prefix + "ServiceRef", + new SetNextNamingRule("addService", "org.apache.tomcat.util.descriptor.web.ContextService")); + + digester.addObjectCreate(prefix + "Transaction", "org.apache.tomcat.util.descriptor.web.ContextTransaction"); + digester.addSetProperties(prefix + "Transaction"); + digester.addRule(prefix + "Transaction", + new SetNextNamingRule("setTransaction", "org.apache.tomcat.util.descriptor.web.ContextTransaction")); + } +} diff --git a/java/org/apache/catalina/startup/PasswdUserDatabase.java b/java/org/apache/catalina/startup/PasswdUserDatabase.java new file mode 100644 index 0000000..a63284b --- /dev/null +++ b/java/org/apache/catalina/startup/PasswdUserDatabase.java @@ -0,0 +1,119 @@ +/* + * 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.catalina.startup; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.naming.StringManager; + +/** + * Concrete implementation of the UserDatabase interface that processes the /etc/passwd file + * on a Unix system. + * + * @author Craig R. McClanahan + */ +public final class PasswdUserDatabase implements UserDatabase { + + private static final Log log = LogFactory.getLog(PasswdUserDatabase.class); + private static final StringManager sm = StringManager.getManager(PasswdUserDatabase.class); + + /** + * The pathname of the Unix password file. + */ + private static final String PASSWORD_FILE = "/etc/passwd"; + + + /** + * The set of home directories for all defined users, keyed by user name. + */ + private final Map homes = new HashMap<>(); + + + /** + * The UserConfig listener with which we are associated. + */ + private UserConfig userConfig = null; + + + /** + * Return the UserConfig listener with which we are associated. + */ + @Override + public UserConfig getUserConfig() { + return userConfig; + } + + + /** + * Set the UserConfig listener with which we are associated. + * + * @param userConfig The new UserConfig listener + */ + @Override + public void setUserConfig(UserConfig userConfig) { + this.userConfig = userConfig; + init(); + } + + + /** + * Return an absolute pathname to the home directory for the specified user. + * + * @param user User for which a home directory should be retrieved + */ + @Override + public String getHome(String user) { + return homes.get(user); + } + + + /** + * Return an enumeration of the user names defined on this server. + */ + @Override + public Enumeration getUsers() { + return Collections.enumeration(homes.keySet()); + } + + + /** + * Initialize our set of users and home directories. + */ + private void init() { + try (BufferedReader reader = new BufferedReader(new FileReader(PASSWORD_FILE))) { + String line = reader.readLine(); + while (line != null) { + String tokens[] = line.split(":"); + // Need non-zero 1st and 6th tokens + if (tokens.length > 5 && tokens[0].length() > 0 && tokens[5].length() > 0) { + // Add this user and corresponding directory + homes.put(tokens[0], tokens[5]); + } + line = reader.readLine(); + } + } catch (Exception e) { + log.warn(sm.getString("passwdUserDatabase.readFail"), e); + } + } +} diff --git a/java/org/apache/catalina/startup/RealmRuleSet.java b/java/org/apache/catalina/startup/RealmRuleSet.java new file mode 100644 index 0000000..b7cd432 --- /dev/null +++ b/java/org/apache/catalina/startup/RealmRuleSet.java @@ -0,0 +1,86 @@ +/* + * 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.catalina.startup; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; + +/** + * RuleSet for processing the contents of a Realm definition element. This RuleSet + * supports Realms such as the CombinedRealm that used nested Realms. + */ +public class RealmRuleSet implements RuleSet { + + private static final int MAX_NESTED_REALM_LEVELS = + Integer.getInteger("org.apache.catalina.startup.RealmRuleSet.MAX_NESTED_REALM_LEVELS", 3).intValue(); + + + // ----------------------------------------------------- Instance Variables + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + + // ------------------------------------------------------------ Constructor + + /** + * Construct an instance of this RuleSet with the default matching pattern prefix. + */ + public RealmRuleSet() { + this(""); + } + + + /** + * Construct an instance of this RuleSet with the specified matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the trailing slash character) + */ + public RealmRuleSet(String prefix) { + this.prefix = prefix; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Add the set of Rule instances defined in this RuleSet to the specified Digester instance, + * associating them with our namespace URI (if any). This method should only be called by a Digester instance. + * + * @param digester Digester instance to which the new Rule instances should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + StringBuilder pattern = new StringBuilder(prefix); + for (int i = 0; i < MAX_NESTED_REALM_LEVELS; i++) { + if (i > 0) { + pattern.append('/'); + } + pattern.append("Realm"); + addRuleInstances(digester, pattern.toString(), i == 0 ? "setRealm" : "addRealm"); + } + } + + private void addRuleInstances(Digester digester, String pattern, String methodName) { + digester.addObjectCreate(pattern, null /* MUST be specified in the element */, "className"); + digester.addSetProperties(pattern); + digester.addSetNext(pattern, methodName, "org.apache.catalina.Realm"); + digester.addRuleSet(new CredentialHandlerRuleSet(pattern + "/")); + } +} diff --git a/java/org/apache/catalina/startup/SafeForkJoinWorkerThreadFactory.java b/java/org/apache/catalina/startup/SafeForkJoinWorkerThreadFactory.java new file mode 100644 index 0000000..719cfef --- /dev/null +++ b/java/org/apache/catalina/startup/SafeForkJoinWorkerThreadFactory.java @@ -0,0 +1,44 @@ +/* + * 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.catalina.startup; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; +import java.util.concurrent.ForkJoinWorkerThread; + +/** + * Provides a {@link ForkJoinWorkerThreadFactory} that provides {@link ForkJoinWorkerThread}s that won't trigger memory + * leaks due to retained references to web application class loaders. + *

    + * Note: This class must be available on the boot strap class path for it to be visible to {@link ForkJoinPool}. + */ +public class SafeForkJoinWorkerThreadFactory implements ForkJoinWorkerThreadFactory { + + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + return new SafeForkJoinWorkerThread(pool); + } + + + private static class SafeForkJoinWorkerThread extends ForkJoinWorkerThread { + + protected SafeForkJoinWorkerThread(ForkJoinPool pool) { + super(pool); + setContextClassLoader(ForkJoinPool.class.getClassLoader()); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/startup/SetNextNamingRule.java b/java/org/apache/catalina/startup/SetNextNamingRule.java new file mode 100644 index 0000000..0fce2dd --- /dev/null +++ b/java/org/apache/catalina/startup/SetNextNamingRule.java @@ -0,0 +1,128 @@ +/* + * 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.catalina.startup; + +import org.apache.catalina.Context; +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.digester.Rule; + + +/** + * Rule implementation that calls a method on the (top-1) (parent) object, passing the top object (child) as an + * argument. It is commonly used to establish parent-child relationships. + *

    + * This rule now supports more flexible method matching by default. It is possible that this may break (some) code + * written against release 1.1.1 or earlier. + */ + +public class SetNextNamingRule extends Rule { + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a "set next" rule with the specified method name. + * + * @param methodName Method name of the parent method to call + * @param paramType Java class of the parent method's argument (if you wish to use a primitive type, specify the + * corresponding Java wrapper class instead, such as java.lang.Boolean for a + * boolean parameter) + */ + public SetNextNamingRule(String methodName, String paramType) { + + this.methodName = methodName; + this.paramType = paramType; + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The method name to call on the parent object. + */ + protected final String methodName; + + + /** + * The Java class name of the parameter type expected by the method. + */ + protected final String paramType; + + + // --------------------------------------------------------- Public Methods + + + /** + * Process the end of this element. + * + * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace + * aware or the element has no namespace + * @param name the local name if the parser is namespace aware, or just the element name otherwise + */ + @Override + public void end(String namespace, String name) throws Exception { + + // Identify the objects to be used + Object child = digester.peek(0); + Object parent = digester.peek(1); + boolean context = false; + + NamingResourcesImpl namingResources = null; + if (parent instanceof Context) { + namingResources = ((Context) parent).getNamingResources(); + context = true; + } else { + namingResources = (NamingResourcesImpl) parent; + } + + // Call the specified method + IntrospectionUtils.callMethod1(namingResources, methodName, child, paramType, digester.getClassLoader()); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + if (context) { + code.append(digester.toVariableName(parent)).append(".getNamingResources()"); + } else { + code.append(digester.toVariableName(namingResources)); + } + code.append('.').append(methodName).append('('); + code.append(digester.toVariableName(child)).append(");"); + code.append(System.lineSeparator()); + } + } + + + /** + * Render a printable version of this Rule. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SetNextRule["); + sb.append("methodName="); + sb.append(methodName); + sb.append(", paramType="); + sb.append(paramType); + sb.append(']'); + return sb.toString(); + } + + +} diff --git a/java/org/apache/catalina/startup/Tomcat.java b/java/org/apache/catalina/startup/Tomcat.java new file mode 100644 index 0000000..0932474 --- /dev/null +++ b/java/org/apache/catalina/startup/Tomcat.java @@ -0,0 +1,1267 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Realm; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.Wrapper; +import org.apache.catalina.authenticator.NonLoginAuthenticator; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.ContainerBase; +import org.apache.catalina.core.NamingContextListener; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.core.StandardServer; +import org.apache.catalina.core.StandardService; +import org.apache.catalina.core.StandardWrapper; +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.catalina.realm.RealmBase; +import org.apache.catalina.security.SecurityClassLoad; +import org.apache.catalina.util.ContextName; +import org.apache.catalina.util.IOTools; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +// TODO: lazy init for the temp dir - only when a JSP is compiled or +// get temp dir is called we need to create it. This will avoid the +// need for the baseDir + +// TODO: allow contexts without a base dir - i.e. +// only programmatic. This would disable the default servlet. + +/** + * Minimal tomcat starter for embedding/unit tests. + *

    + * Tomcat supports multiple styles of configuration and startup - the most common and stable is server.xml-based, + * implemented in org.apache.catalina.startup.Bootstrap. + *

    + * This class is for use in apps that embed tomcat. + *

    + * Requirements: + *

      + *
    • all tomcat classes and possibly servlets are in the classpath. (for example all is in one big jar, or in eclipse + * CP, or in any other combination)
    • + *
    • we need one temporary directory for work files
    • + *
    • no config file is required. This class provides methods to use if you have a webapp with a web.xml file, but it + * is optional - you can use your own servlets.
    • + *
    + *

    + * There are a variety of 'add' methods to configure servlets and webapps. These methods, by default, create a simple + * in-memory security realm and apply it. If you need more complex security processing, you can define a subclass of + * this class. + *

    + * This class provides a set of convenience methods for configuring web application contexts; all overloads of the + * method addWebapp(). These methods are equivalent to adding a web application to the Host's appBase + * (normally the webapps directory). These methods create a Context, configure it with the equivalent of the defaults + * provided by conf/web.xml (see {@link #initWebappDefaults(String)} for details) and add the Context to a + * Host. These methods do not use a global default web.xml; rather, they add a {@link LifecycleListener} to configure + * the defaults. Any WEB-INF/web.xml and META-INF/context.xml packaged with the application will be processed normally. + * Normal web fragment and {@link jakarta.servlet.ServletContainerInitializer} processing will be applied. + *

    + * In complex cases, you may prefer to use the ordinary Tomcat API to create webapp contexts; for example, you might + * need to install a custom Loader before the call to {@link Host#addChild(Container)}. To replicate the basic behavior + * of the addWebapp methods, you may want to call two methods of this class: {@link #noDefaultWebXmlPath()} + * and {@link #getDefaultWebXmlListener()}. + *

    + * {@link #getDefaultWebXmlListener()} returns a {@link LifecycleListener} that adds the standard DefaultServlet, JSP + * processing, and welcome files. If you add this listener, you must prevent Tomcat from applying any standard global + * web.xml with ... + *

    + * {@link #noDefaultWebXmlPath()} returns a dummy pathname to configure to prevent {@link ContextConfig} from trying to + * apply a global web.xml file. + *

    + * This class provides a main() and few simple CLI arguments, see setters for doc. It can be used for simple tests and + * demo. + * + * @see TestTomcat + * + * @author Costin Manolache + */ +public class Tomcat { + + private static final StringManager sm = StringManager.getManager(Tomcat.class); + + // Some logging implementations use weak references for loggers so there is + // the possibility that logging configuration could be lost if GC runs just + // after Loggers are configured but before they are used. The purpose of + // this Map is to retain strong references to explicitly configured loggers + // so that configuration is not lost. + private final Map pinnedLoggers = new HashMap<>(); + + protected Server server; + + protected int port = 8080; + protected String hostname = "localhost"; + protected String basedir; + + private final Map userPass = new HashMap<>(); + private final Map> userRoles = new HashMap<>(); + private final Map userPrincipals = new HashMap<>(); + + private boolean addDefaultWebXmlToWebapp = true; + + public Tomcat() { + ExceptionUtils.preload(); + } + + /** + * Tomcat requires that the base directory is set because the defaults for a number of other locations, such as the + * work directory, are derived from the base directory. This should be the first method called. + *

    + * If this method is not called then Tomcat will attempt to use these locations in the following order: + *

      + *
    1. if set, the catalina.base system property
    2. + *
    3. if set, the catalina.home system property
    4. + *
    5. The user.dir system property (the directory where Java was run from) where a directory named tomcat.$PORT + * will be created. $PORT is the value configured via {@link #setPort(int)} which defaults to 8080 if not set
    6. + *
    + * The user should ensure that the file permissions for the base directory are appropriate. + *

    + * TODO: disable work dir if not needed ( no jsp, etc ). + * + * @param basedir The Tomcat base folder on which all others will be derived + */ + public void setBaseDir(String basedir) { + this.basedir = basedir; + } + + /** + * Set the port for the default connector. The default connector will only be created if getConnector is called. + * + * @param port The port number + */ + public void setPort(int port) { + this.port = port; + } + + /** + * The the hostname of the default host, default is 'localhost'. + * + * @param s The default host name + */ + public void setHostname(String s) { + hostname = s; + } + + + /** + * This is equivalent to adding a web application to a Host's appBase (usually Tomcat's webapps directory). By + * default, the equivalent of the default web.xml will be applied to the web application (see + * {@link #initWebappDefaults(String)}). This may be prevented by calling + * {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any WEB-INF/web.xml and + * META-INF/context.xml packaged with the application will always be processed and normal web fragment + * and {@link jakarta.servlet.ServletContainerInitializer} processing will always be applied. + * + * @param contextPath The context mapping to use, "" for root context. + * @param docBase Base directory for the context, for static files. Must exist and be an absolute path. + * + * @return the deployed context + */ + public Context addWebapp(String contextPath, String docBase) { + return addWebapp(getHost(), contextPath, docBase); + } + + + /** + * Copy the specified WAR file to the Host's appBase and then call {@link #addWebapp(String, String)} with the newly + * copied WAR. The WAR will NOT be removed from the Host's appBase when the Tomcat instance stops. Note that + * {@link ExpandWar} provides utility methods that may be used to delete the WAR and/or expanded directory if + * required. + * + * @param contextPath The context mapping to use, "" for root context. + * @param source The location from which the WAR should be copied + * + * @return The deployed Context + * + * @throws IOException If an I/O error occurs while copying the WAR file from the specified URL to the appBase + */ + public Context addWebapp(String contextPath, URL source) throws IOException { + + ContextName cn = new ContextName(contextPath, null); + + // Make sure a conflicting web application has not already been deployed + Host h = getHost(); + if (h.findChild(cn.getName()) != null) { + throw new IllegalArgumentException( + sm.getString("tomcat.addWebapp.conflictChild", source, contextPath, cn.getName())); + } + + // Make sure appBase does not contain a conflicting web application + File targetWar = new File(h.getAppBaseFile(), cn.getBaseName() + ".war"); + File targetDir = new File(h.getAppBaseFile(), cn.getBaseName()); + + if (targetWar.exists()) { + throw new IllegalArgumentException( + sm.getString("tomcat.addWebapp.conflictFile", source, contextPath, targetWar.getAbsolutePath())); + } + if (targetDir.exists()) { + throw new IllegalArgumentException( + sm.getString("tomcat.addWebapp.conflictFile", source, contextPath, targetDir.getAbsolutePath())); + } + + // Should be good to copy the WAR now + URLConnection uConn = source.openConnection(); + + try (InputStream is = uConn.getInputStream(); OutputStream os = new FileOutputStream(targetWar)) { + IOTools.flow(is, os); + } + + return addWebapp(contextPath, targetWar.getAbsolutePath()); + } + + + /** + * Add a context - programmatic mode, no default web.xml used. This means that there is no JSP support (no JSP + * servlet), no default servlet and no web socket support unless explicitly enabled via the programmatic interface. + * There is also no {@link jakarta.servlet.ServletContainerInitializer} processing and no annotation processing. If + * a {@link jakarta.servlet.ServletContainerInitializer} is added programmatically, there will still be no scanning + * for {@link jakarta.servlet.annotation.HandlesTypes} matches. + *

    + * API calls equivalent with web.xml: + * + *

    {@code
    +     * // context-param
    +     * ctx.addParameter("name", "value");
    +     *
    +     *
    +     * // error-page
    +     * ErrorPage ep = new ErrorPage();
    +     * ep.setErrorCode(500);
    +     * ep.setLocation("/error.html");
    +     * ctx.addErrorPage(ep);
    +     *
    +     * ctx.addMimeMapping("ext", "type");
    +     * }
    + *

    + * Note: If you reload the Context, all your configuration will be lost. If you need reload support, consider using + * a LifecycleListener to provide your configuration. + *

    + * TODO: add the rest + * + * @param contextPath The context mapping to use, "" for root context. + * @param docBase Base directory for the context, for static files. Must exist, relative to the server home + * + * @return the deployed context + */ + public Context addContext(String contextPath, String docBase) { + return addContext(getHost(), contextPath, docBase); + } + + /** + * Equivalent to <servlet><servlet-name><servlet-class>. + *

    + * In general it is better/faster to use the method that takes a Servlet as param - this one can be used if the + * servlet is not commonly used, and want to avoid loading all deps. ( for example: jsp servlet ) You can customize + * the returned servlet, ex: + * + *

    +     * wrapper.addInitParameter("name", "value");
    +     * 
    + * + * @param contextPath Context to add Servlet to + * @param servletName Servlet name (used in mappings) + * @param servletClass The class to be used for the Servlet + * + * @return The wrapper for the servlet + */ + public Wrapper addServlet(String contextPath, String servletName, String servletClass) { + Container ctx = getHost().findChild(contextPath); + return addServlet((Context) ctx, servletName, servletClass); + } + + /** + * Static version of {@link #addServlet(String, String, String)} + * + * @param ctx Context to add Servlet to + * @param servletName Servlet name (used in mappings) + * @param servletClass The class to be used for the Servlet + * + * @return The wrapper for the servlet + */ + public static Wrapper addServlet(Context ctx, String servletName, String servletClass) { + // will do class for name and set init params + Wrapper sw = ctx.createWrapper(); + if (sw == null) { + throw new IllegalStateException(sm.getString("tomcat.noWrapper")); + } + sw.setServletClass(servletClass); + sw.setName(servletName); + ctx.addChild(sw); + + return sw; + } + + /** + * Add an existing Servlet to the context with no class.forName or initialisation. + * + * @param contextPath Context to add Servlet to + * @param servletName Servlet name (used in mappings) + * @param servlet The Servlet to add + * + * @return The wrapper for the servlet + */ + public Wrapper addServlet(String contextPath, String servletName, Servlet servlet) { + Container ctx = getHost().findChild(contextPath); + return addServlet((Context) ctx, servletName, servlet); + } + + /** + * Static version of {@link #addServlet(String, String, Servlet)}. + * + * @param ctx Context to add Servlet to + * @param servletName Servlet name (used in mappings) + * @param servlet The Servlet to add + * + * @return The wrapper for the servlet + */ + public static Wrapper addServlet(Context ctx, String servletName, Servlet servlet) { + // will do class for name and set init params + Wrapper sw = new ExistingStandardWrapper(servlet); + sw.setName(servletName); + ctx.addChild(sw); + + return sw; + } + + + /** + * Initialize the server given the specified configuration source. The server will be loaded according to the Tomcat + * configuration files contained in the source (server.xml, web.xml, context.xml, SSL certificates, etc). If no + * configuration source is specified, it will use the default locations for these files. + * + * @param source The configuration source + */ + public void init(ConfigurationSource source) { + init(source, null); + } + + /** + * Initialize the server given the specified configuration source. The server will be loaded according to the Tomcat + * configuration files contained in the source (server.xml, web.xml, context.xml, SSL certificates, etc). If no + * configuration source is specified, it will use the default locations for these files. + * + * @param source The configuration source + * @param catalinaArguments The arguments that should be passed to Catalina + */ + public void init(ConfigurationSource source, String[] catalinaArguments) { + ConfigFileLoader.setSource(source); + addDefaultWebXmlToWebapp = false; + Catalina catalina = new Catalina(); + // Load the Catalina instance with the regular configuration files + // from specified source + if (catalinaArguments == null) { + catalina.load(); + } else { + catalina.load(catalinaArguments); + } + // Retrieve and set the server + server = catalina.getServer(); + } + + + /** + * Initialize the server. + * + * @throws LifecycleException Init error + */ + public void init() throws LifecycleException { + getServer(); + server.init(); + } + + + /** + * Start the server. + * + * @throws LifecycleException Start error + */ + public void start() throws LifecycleException { + getServer(); + server.start(); + } + + /** + * Stop the server. + * + * @throws LifecycleException Stop error + */ + public void stop() throws LifecycleException { + getServer(); + server.stop(); + } + + + /** + * Destroy the server. This object cannot be used once this method has been called. + * + * @throws LifecycleException Destroy error + */ + public void destroy() throws LifecycleException { + getServer(); + server.destroy(); + // Could null out objects here + } + + /** + * Add a user for the in-memory realm. All created apps use this by default, can be replaced using setRealm(). + * + * @param user The user name + * @param pass The password + */ + public void addUser(String user, String pass) { + userPass.put(user, pass); + } + + /** + * Add a role to a user. + * + * @see #addUser(String, String) + * + * @param user The user name + * @param role The role name + */ + public void addRole(String user, String role) { + userRoles.computeIfAbsent(user, k -> new ArrayList<>()).add(role); + } + + // ------- Extra customization ------- + // You can tune individual Tomcat objects, using internal APIs + + /** + * Get the default HTTP connector that is used by the embedded Tomcat. It is first configured connector in the + * service. If there's no connector defined, it will create and add a default connector using the port and address + * specified in this Tomcat instance, and return it for further customization. + * + * @return The connector object + */ + public Connector getConnector() { + Service service = getService(); + if (service.findConnectors().length > 0) { + return service.findConnectors()[0]; + } + // The same as in standard Tomcat configuration. + // This creates a NIO HTTP connector. + Connector connector = new Connector("HTTP/1.1"); + connector.setPort(port); + service.addConnector(connector); + return connector; + } + + /** + * Set the specified connector in the service, if it is not already present. + * + * @param connector The connector instance to add + */ + public void setConnector(Connector connector) { + Service service = getService(); + boolean found = false; + for (Connector serviceConnector : service.findConnectors()) { + if (connector == serviceConnector) { + found = true; + break; + } + } + if (!found) { + service.addConnector(connector); + } + } + + /** + * Get the service object. Can be used to add more connectors and few other global settings. + * + * @return The service + */ + public Service getService() { + return getServer().findServices()[0]; + } + + /** + * Sets the current host - all future webapps will be added to this host. When tomcat starts, the host will be the + * default host. + * + * @param host The current host + */ + public void setHost(Host host) { + Engine engine = getEngine(); + boolean found = false; + for (Container engineHost : engine.findChildren()) { + if (engineHost == host) { + found = true; + break; + } + } + if (!found) { + engine.addChild(host); + } + } + + public Host getHost() { + Engine engine = getEngine(); + if (engine.findChildren().length > 0) { + return (Host) engine.findChildren()[0]; + } + + Host host = new StandardHost(); + host.setName(hostname); + getEngine().addChild(host); + return host; + } + + /** + * Access to the engine, for further customization. + * + * @return The engine + */ + public Engine getEngine() { + Service service = getServer().findServices()[0]; + if (service.getContainer() != null) { + return service.getContainer(); + } + Engine engine = new StandardEngine(); + engine.setName("Tomcat"); + engine.setDefaultHost(hostname); + engine.setRealm(createDefaultRealm()); + service.setContainer(engine); + return engine; + } + + /** + * Get the server object. You can add listeners and few more customizations. JNDI is disabled by default. + * + * @return The Server + */ + public Server getServer() { + + if (server != null) { + return server; + } + + System.setProperty("catalina.useNaming", "false"); + + server = new StandardServer(); + + initBaseDir(); + + // Set configuration source + ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null)); + + server.setPort(-1); + + Service service = new StandardService(); + service.setName("Tomcat"); + server.addService(service); + return server; + } + + /** + * @param host The host in which the context will be deployed + * @param contextPath The context mapping to use, "" for root context. + * @param dir Base directory for the context, for static files. Must exist, relative to the server home + * + * @return the deployed context + * + * @see #addContext(String, String) + */ + public Context addContext(Host host, String contextPath, String dir) { + return addContext(host, contextPath, contextPath, dir); + } + + /** + * @param host The host in which the context will be deployed + * @param contextPath The context mapping to use, "" for root context. + * @param contextName The context name + * @param dir Base directory for the context, for static files. Must exist, relative to the server home + * + * @return the deployed context + * + * @see #addContext(String, String) + */ + public Context addContext(Host host, String contextPath, String contextName, String dir) { + silence(host, contextName); + Context ctx = createContext(host, contextPath); + ctx.setName(contextName); + ctx.setPath(contextPath); + ctx.setDocBase(dir); + ctx.addLifecycleListener(new FixContextListener()); + + if (host == null) { + getHost().addChild(ctx); + } else { + host.addChild(ctx); + } + return ctx; + } + + + /** + * This is equivalent to adding a web application to a Host's appBase (usually Tomcat's webapps directory). By + * default, the equivalent of the default web.xml will be applied to the web application (see + * {@link #initWebappDefaults(String)}). This may be prevented by calling + * {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any WEB-INF/web.xml and + * META-INF/context.xml packaged with the application will always be processed and normal web fragment + * and {@link jakarta.servlet.ServletContainerInitializer} processing will always be applied. + * + * @param host The host in which the context will be deployed + * @param contextPath The context mapping to use, "" for root context. + * @param docBase Base directory for the context, for static files. Must exist and be an absolute path. + * + * @return the deployed context + */ + public Context addWebapp(Host host, String contextPath, String docBase) { + LifecycleListener listener = null; + try { + Class clazz = Class.forName(getHost().getConfigClass()); + listener = (LifecycleListener) clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + // Wrap in IAE since we can't easily change the method signature to + // to throw the specific checked exceptions + throw new IllegalArgumentException(e); + } + + return addWebapp(host, contextPath, docBase, listener); + } + + + /** + * This is equivalent to adding a web application to a Host's appBase (usually Tomcat's webapps directory). By + * default, the equivalent of the default web.xml will be applied to the web application (see + * {@link #initWebappDefaults(String)}). This may be prevented by calling + * {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any WEB-INF/web.xml and + * META-INF/context.xml packaged with the application will always be processed and normal web fragment + * and {@link jakarta.servlet.ServletContainerInitializer} processing will always be applied. + * + * @param host The host in which the context will be deployed + * @param contextPath The context mapping to use, "" for root context. + * @param docBase Base directory for the context, for static files. Must exist and be an absolute path. + * @param config Custom context configuration helper. Any configuration will be in addition to equivalent of + * the default web.xml configuration described above. + * + * @return the deployed context + */ + public Context addWebapp(Host host, String contextPath, String docBase, LifecycleListener config) { + + silence(host, contextPath); + + Context ctx = createContext(host, contextPath); + ctx.setPath(contextPath); + ctx.setDocBase(docBase); + + if (addDefaultWebXmlToWebapp) { + ctx.addLifecycleListener(getDefaultWebXmlListener()); + } + + ctx.setConfigFile(getWebappConfigFile(docBase, contextPath)); + + ctx.addLifecycleListener(config); + + if (addDefaultWebXmlToWebapp && (config instanceof ContextConfig)) { + // prevent it from looking ( if it finds one - it'll have dup error ) + ((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath()); + } + + if (host == null) { + getHost().addChild(ctx); + } else { + host.addChild(ctx); + } + + return ctx; + } + + /** + * Return a listener that provides the required configuration items for JSP processing. From the standard Tomcat + * global web.xml. Pass this to {@link Context#addLifecycleListener(LifecycleListener)} and then pass the result of + * {@link #noDefaultWebXmlPath()} to {@link ContextConfig#setDefaultWebXml(String)}. + * + * @return a listener object that configures default JSP processing. + */ + public LifecycleListener getDefaultWebXmlListener() { + return new DefaultWebXmlListener(); + } + + /** + * @return a pathname to pass to {@link ContextConfig#setDefaultWebXml(String)} when using + * {@link #getDefaultWebXmlListener()}. + */ + public String noDefaultWebXmlPath() { + return Constants.NoDefaultWebXml; + } + + // ---------- Helper methods and classes ------------------- + + /** + * Create an in-memory realm. You can replace it for contexts with a real one. The Realm created here will be added + * to the Engine by default and may be replaced at the Engine level or over-ridden (as per normal Tomcat behaviour) + * at the Host or Context level. + * + * @return a realm instance + */ + protected Realm createDefaultRealm() { + return new SimpleRealm(); + } + + + private class SimpleRealm extends RealmBase { + + @Override + protected String getPassword(String username) { + return userPass.get(username); + } + + @Override + protected Principal getPrincipal(String username) { + Principal p = userPrincipals.get(username); + if (p == null) { + String pass = userPass.get(username); + if (pass != null) { + p = new GenericPrincipal(username, userRoles.get(username)); + userPrincipals.put(username, p); + } + } + return p; + } + } + + + protected void initBaseDir() { + String catalinaHome = System.getProperty(Globals.CATALINA_HOME_PROP); + if (basedir == null) { + basedir = System.getProperty(Globals.CATALINA_BASE_PROP); + } + if (basedir == null) { + basedir = catalinaHome; + } + if (basedir == null) { + // Create a temp dir. + basedir = System.getProperty("user.dir") + "/tomcat." + port; + } + + File baseFile = new File(basedir); + if (baseFile.exists()) { + if (!baseFile.isDirectory()) { + throw new IllegalArgumentException(sm.getString("tomcat.baseDirNotDir", baseFile)); + } + } else { + if (!baseFile.mkdirs()) { + // Failed to create base directory + throw new IllegalStateException(sm.getString("tomcat.baseDirMakeFail", baseFile)); + } + /* + * If file permissions were going to be set on the newly created directory, this is the place to do it. + * However, even simple calls such as File.setReadable(boolean,boolean) behaves differently on different + * platforms. Therefore, setBaseDir documents that the user needs to do this. + */ + } + try { + baseFile = baseFile.getCanonicalFile(); + } catch (IOException e) { + baseFile = baseFile.getAbsoluteFile(); + } + server.setCatalinaBase(baseFile); + System.setProperty(Globals.CATALINA_BASE_PROP, baseFile.getPath()); + basedir = baseFile.getPath(); + + if (catalinaHome == null) { + server.setCatalinaHome(baseFile); + } else { + File homeFile = new File(catalinaHome); + if (!homeFile.isDirectory() && !homeFile.mkdirs()) { + // Failed to create home directory + throw new IllegalStateException(sm.getString("tomcat.homeDirMakeFail", homeFile)); + } + try { + homeFile = homeFile.getCanonicalFile(); + } catch (IOException e) { + homeFile = homeFile.getAbsoluteFile(); + } + server.setCatalinaHome(homeFile); + } + System.setProperty(Globals.CATALINA_HOME_PROP, server.getCatalinaHome().getPath()); + } + + static final String[] silences = + new String[] { "org.apache.coyote.http11.Http11NioProtocol", "org.apache.catalina.core.StandardService", + "org.apache.catalina.core.StandardEngine", "org.apache.catalina.startup.ContextConfig", + "org.apache.catalina.core.ApplicationContext", "org.apache.catalina.core.AprLifecycleListener" }; + + private boolean silent = false; + + /** + * Controls if the loggers will be silenced or not. + * + * @param silent true sets the log level to WARN for the loggers that log information on Tomcat start + * up. This prevents the usual startup information being logged. false sets the log + * level to the default level of INFO. + */ + public void setSilent(boolean silent) { + this.silent = silent; + for (String s : silences) { + Logger logger = Logger.getLogger(s); + pinnedLoggers.put(s, logger); + if (silent) { + logger.setLevel(Level.WARNING); + } else { + logger.setLevel(Level.INFO); + } + } + } + + private void silence(Host host, String contextPath) { + String loggerName = getLoggerName(host, contextPath); + Logger logger = Logger.getLogger(loggerName); + pinnedLoggers.put(loggerName, logger); + if (silent) { + logger.setLevel(Level.WARNING); + } else { + logger.setLevel(Level.INFO); + } + } + + + /** + * By default, when calling addWebapp() to create a Context, the settings from from the default web.xml are added to + * the context. Calling this method with a false value prior to calling addWebapp() allows to opt out + * of the default settings. In that event you will need to add the configurations yourself, either programmatically + * or by using web.xml deployment descriptors. + * + * @param addDefaultWebXmlToWebapp false will prevent the class from automatically adding the default + * settings when calling addWebapp(). true will add the default + * settings and is the default behavior. + * + * @see #addWebapp(Host, String, String, LifecycleListener) + */ + public void setAddDefaultWebXmlToWebapp(boolean addDefaultWebXmlToWebapp) { + this.addDefaultWebXmlToWebapp = addDefaultWebXmlToWebapp; + } + + + /* + * Uses essentially the same logic as {@link ContainerBase#logName()}. + */ + private String getLoggerName(Host host, String contextName) { + if (host == null) { + host = getHost(); + } + StringBuilder loggerName = new StringBuilder(); + loggerName.append(ContainerBase.class.getName()); + loggerName.append(".["); + // Engine name + loggerName.append(host.getParent().getName()); + loggerName.append("].["); + // Host name + loggerName.append(host.getName()); + loggerName.append("].["); + // Context name + if (contextName == null || contextName.equals("")) { + loggerName.append('/'); + } else if (contextName.startsWith("##")) { + loggerName.append('/'); + loggerName.append(contextName); + } + loggerName.append(']'); + + return loggerName.toString(); + } + + /** + * Create the configured {@link Context} for the given host. The default constructor of the class that + * was configured with {@link StandardHost#setContextClass(String)} will be used + * + * @param host host for which the {@link Context} should be created, or null if default host should be + * used + * @param url path of the webapp which should get the {@link Context} + * + * @return newly created {@link Context} + */ + private Context createContext(Host host, String url) { + String defaultContextClass = StandardContext.class.getName(); + String contextClass = StandardContext.class.getName(); + if (host == null) { + host = this.getHost(); + } + if (host instanceof StandardHost) { + contextClass = ((StandardHost) host).getContextClass(); + } + try { + if (defaultContextClass.equals(contextClass)) { + return new StandardContext(); + } else { + return (Context) Class.forName(contextClass).getConstructor().newInstance(); + } + + } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) { + throw new IllegalArgumentException(sm.getString("tomcat.noContextClass", contextClass, host, url), e); + } + } + + /** + * Enables JNDI naming which is disabled by default. Server must implement {@link Lifecycle} in order for the + * {@link NamingContextListener} to be used. + */ + public void enableNaming() { + // Make sure getServer() has been called as that is where naming is + // disabled + getServer(); + server.addLifecycleListener(new NamingContextListener()); + + System.setProperty("catalina.useNaming", "true"); + + String value = "org.apache.naming"; + String oldValue = System.getProperty(javax.naming.Context.URL_PKG_PREFIXES); + if (oldValue != null) { + if (oldValue.contains(value)) { + value = oldValue; + } else { + value = value + ":" + oldValue; + } + } + System.setProperty(javax.naming.Context.URL_PKG_PREFIXES, value); + + value = System.getProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY); + if (value == null) { + System.setProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY, + "org.apache.naming.java.javaURLContextFactory"); + } + } + + + /** + * Provide default configuration for a context. This is broadly the programmatic equivalent of the default web.xml + * and provides the following features: + *
      + *
    • Default servlet mapped to "/"
    • + *
    • JSP servlet mapped to "*.jsp" and ""*.jspx"
    • + *
    • Session timeout of 30 minutes
    • + *
    • MIME mappings (subset of those in conf/web.xml)
    • + *
    • Welcome files
    • + *
    + * TODO: Align the MIME mappings with conf/web.xml - possibly via a common file. + * + * @param contextPath The path of the context to set the defaults for + */ + public void initWebappDefaults(String contextPath) { + Container ctx = getHost().findChild(contextPath); + initWebappDefaults((Context) ctx); + } + + + /** + * Static version of {@link #initWebappDefaults(String)}. + * + * @param ctx The context to set the defaults for + */ + public static void initWebappDefaults(Context ctx) { + // Default servlet + Wrapper servlet = addServlet(ctx, "default", "org.apache.catalina.servlets.DefaultServlet"); + servlet.setLoadOnStartup(1); + servlet.setOverridable(true); + + // JSP servlet (by class name - to avoid loading all deps) + servlet = addServlet(ctx, "jsp", "org.apache.jasper.servlet.JspServlet"); + servlet.addInitParameter("fork", "false"); + servlet.setLoadOnStartup(3); + servlet.setOverridable(true); + + // Servlet mappings + ctx.addServletMappingDecoded("/", "default"); + ctx.addServletMappingDecoded("*.jsp", "jsp"); + ctx.addServletMappingDecoded("*.jspx", "jsp"); + + // Sessions + ctx.setSessionTimeout(30); + + // MIME type mappings + addDefaultMimeTypeMappings(ctx); + + // Welcome files + ctx.addWelcomeFile("index.html"); + ctx.addWelcomeFile("index.htm"); + ctx.addWelcomeFile("index.jsp"); + } + + + /** + * Add the default MIME type mappings to the provide Context. + * + * @param context The web application to which the default MIME type mappings should be added. + */ + public static void addDefaultMimeTypeMappings(Context context) { + Properties defaultMimeMappings = new Properties(); + try (InputStream is = Tomcat.class.getResourceAsStream("MimeTypeMappings.properties")) { + defaultMimeMappings.load(is); + for (Map.Entry entry : defaultMimeMappings.entrySet()) { + context.addMimeMapping((String) entry.getKey(), (String) entry.getValue()); + } + } catch (IOException e) { + throw new IllegalStateException(sm.getString("tomcat.defaultMimeTypeMappingsFail"), e); + } + } + + + /** + * Fix startup sequence - required if you don't use web.xml. + *

    + * The start() method in context will set 'configured' to false - and expects a listener to set it back to true. + */ + public static class FixContextListener implements LifecycleListener { + + @Override + public void lifecycleEvent(LifecycleEvent event) { + try { + Context context = (Context) event.getLifecycle(); + if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { + context.setConfigured(true); + + // Process annotations + WebAnnotationSet.loadApplicationAnnotations(context); + + // LoginConfig is required to process @ServletSecurity + // annotations + if (context.getLoginConfig() == null) { + context.setLoginConfig(new LoginConfig("NONE", null, null, null)); + context.getPipeline().addValve(new NonLoginAuthenticator()); + } + } + } catch (ClassCastException e) { + } + } + } + + + /** + * Fix reload - required if reloading and using programmatic configuration. When a context is reloaded, any + * programmatic configuration is lost. This listener sets the equivalent of conf/web.xml when the context starts. + */ + public static class DefaultWebXmlListener implements LifecycleListener { + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (Lifecycle.BEFORE_START_EVENT.equals(event.getType())) { + initWebappDefaults((Context) event.getLifecycle()); + } + } + } + + + /** + * Helper class for wrapping existing servlets. This disables servlet lifecycle and normal reloading, but also + * reduces overhead and provide more direct control over the servlet. + */ + public static class ExistingStandardWrapper extends StandardWrapper { + private final Servlet existing; + + public ExistingStandardWrapper(Servlet existing) { + this.existing = existing; + this.asyncSupported = hasAsync(existing); + } + + private static boolean hasAsync(Servlet existing) { + boolean result = false; + Class clazz = existing.getClass(); + WebServlet ws = clazz.getAnnotation(WebServlet.class); + if (ws != null) { + result = ws.asyncSupported(); + } + return result; + } + + @Override + public synchronized Servlet loadServlet() throws ServletException { + if (!instanceInitialized) { + existing.init(facade); + instanceInitialized = true; + } + return existing; + } + + @Override + public long getAvailable() { + return 0; + } + + @Override + public boolean isUnavailable() { + return false; + } + + @Override + public Servlet getServlet() { + return existing; + } + + @Override + public String getServletClass() { + return existing.getClass().getName(); + } + } + + protected URL getWebappConfigFile(String path, String contextName) { + File docBase = new File(path); + if (docBase.isDirectory()) { + return getWebappConfigFileFromDirectory(docBase, contextName); + } else { + return getWebappConfigFileFromWar(docBase, contextName); + } + } + + private URL getWebappConfigFileFromDirectory(File docBase, String contextName) { + URL result = null; + File webAppContextXml = new File(docBase, Constants.ApplicationContextXml); + if (webAppContextXml.exists()) { + try { + result = webAppContextXml.toURI().toURL(); + } catch (MalformedURLException e) { + Logger.getLogger(getLoggerName(getHost(), contextName)).log(Level.WARNING, + sm.getString("tomcat.noContextXml", docBase), e); + } + } + return result; + } + + private URL getWebappConfigFileFromWar(File docBase, String contextName) { + URL result = null; + try (JarFile jar = new JarFile(docBase)) { + JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml); + if (entry != null) { + result = UriUtil.buildJarUrl(docBase, Constants.ApplicationContextXml); + } + } catch (IOException e) { + Logger.getLogger(getLoggerName(getHost(), contextName)).log(Level.WARNING, + sm.getString("tomcat.noContextXml", docBase), e); + } + return result; + } + + static { + // Graal native images don't load any configuration except the VM default + if (JreCompat.isGraalAvailable()) { + try (InputStream is = new FileInputStream( + new File(System.getProperty("java.util.logging.config.file", "conf/logging.properties")))) { + LogManager.getLogManager().readConfiguration(is); + } catch (SecurityException | IOException e) { + // Ignore, the VM default will be used + } + } + } + + /** + * Main executable method for use with a Maven packager. + * + * @param args the command line arguments + * + * @throws Exception if an error occurs + */ + public static void main(String[] args) throws Exception { + // Process some command line parameters + String[] catalinaArguments = null; + for (int i = 0; i < args.length; i++) { + if (args[i].equals("--no-jmx")) { + Registry.disableRegistry(); + } else if (args[i].equals("--catalina")) { + // This was already processed before + // Skip the rest of the arguments as they are for Catalina + ArrayList result = new ArrayList<>(); + for (int j = i + 1; j < args.length; j++) { + result.add(args[j]); + } + catalinaArguments = result.toArray(new String[0]); + break; + } + } + SecurityClassLoad.securityClassLoad(Thread.currentThread().getContextClassLoader()); + Tomcat tomcat = new Tomcat(); + // Create a Catalina instance and let it parse the configuration files + // It will also set a shutdown hook to stop the Server when needed + // Use the default configuration source + tomcat.init(null, catalinaArguments); + boolean await = false; + String path = ""; + // Process command line parameters + for (int i = 0; i < args.length; i++) { + if (args[i].equals("--war")) { + if (++i >= args.length) { + throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i - 1])); + } + File war = new File(args[i]); + tomcat.addWebapp(path, war.getAbsolutePath()); + } else if (args[i].equals("--path")) { + if (++i >= args.length) { + throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i - 1])); + } + path = args[i]; + } else if (args[i].equals("--await")) { + await = true; + } else if (args[i].equals("--no-jmx")) { + // This was already processed before + } else if (args[i].equals("--catalina")) { + // This was already processed before + // Skip the rest of the arguments as they are for Catalina + break; + } else { + throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i])); + } + } + tomcat.start(); + // Ideally the utility threads are non daemon + if (await) { + tomcat.getServer().await(); + } + } + +} diff --git a/java/org/apache/catalina/startup/Tool.java b/java/org/apache/catalina/startup/Tool.java new file mode 100644 index 0000000..9593a93 --- /dev/null +++ b/java/org/apache/catalina/startup/Tool.java @@ -0,0 +1,238 @@ +/* + * 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.catalina.startup; + + +import java.io.File; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + * General purpose wrapper for command line tools that should execute in an environment with the common class loader + * environment set up by Catalina. This should be executed from a command line script that conforms to the following + * requirements: + *

      + *
    • Passes the catalina.home system property configured with the pathname of the Tomcat installation + * directory.
    • + *
    • Sets the system classpath to include bootstrap.jar and $JAVA_HOME/lib/tools.jar.
    • + *
    + *

    + * The command line to execute the tool looks like: + * + *

    + *   java -classpath $CLASSPATH org.apache.catalina.startup.Tool \
    + *     ${options} ${classname} ${arguments}
    + * 
    + *

    + * with the following replacement contents: + *

      + *
    • ${options} - Command line options for this Tool wrapper. The following options are supported: + *
        + *
      • -ant : Set the ant.home system property to corresponding to the value of + * catalina.home (useful when your command line tool runs Ant).
      • + *
      • -common : Add common/classes and common/lib to the class loader + * repositories.
      • + *
      • -server : Add server/classes and server/lib to the class loader + * repositories.
      • + *
      • -shared : Add shared/classes and shared/lib to the class loader + * repositories.
      • + *
      + *
    • ${classname} - Fully qualified Java class name of the application's main class.
    • + *
    • ${arguments} - Command line arguments to be passed to the application's main() + * method.
    • + *
    + * + * @author Craig R. McClanahan + */ +public final class Tool { + + + private static final Log log = LogFactory.getLog(Tool.class); + + // ------------------------------------------------------- Static Variables + + + /** + * Set ant.home system property? + */ + private static boolean ant = false; + + + /** + * The pathname of our installation base directory. + */ + private static final String catalinaHome = System.getProperty(Constants.CATALINA_HOME_PROP); + + + /** + * Include common classes in the repositories? + */ + private static boolean common = false; + + + /** + * Include server classes in the repositories? + */ + private static boolean server = false; + + + /** + * Include shared classes in the repositories? + */ + private static boolean shared = false; + + + // ----------------------------------------------------------- Main Program + + + /** + * The main program for the bootstrap. + * + * @param args Command line arguments to be processed + */ + @SuppressWarnings("null") + public static void main(String args[]) { + + // Verify that "catalina.home" was passed. + if (catalinaHome == null) { + log.error("Must set '" + Constants.CATALINA_HOME_PROP + "' system property"); + System.exit(1); + } + + // Process command line options + int index = 0; + while (true) { + if (index == args.length) { + usage(); + System.exit(1); + } + if ("-ant".equals(args[index])) { + ant = true; + } else if ("-common".equals(args[index])) { + common = true; + } else if ("-server".equals(args[index])) { + server = true; + } else if ("-shared".equals(args[index])) { + shared = true; + } else { + break; + } + index++; + } + if (index > args.length) { + usage(); + System.exit(1); + } + + // Set "ant.home" if requested + if (ant) { + System.setProperty("ant.home", catalinaHome); + } + + // Construct the class loader we will be using + ClassLoader classLoader = null; + try { + List packed = new ArrayList<>(); + List unpacked = new ArrayList<>(); + unpacked.add(new File(catalinaHome, "classes")); + packed.add(new File(catalinaHome, "lib")); + if (common) { + unpacked.add(new File(catalinaHome, "common" + File.separator + "classes")); + packed.add(new File(catalinaHome, "common" + File.separator + "lib")); + } + if (server) { + unpacked.add(new File(catalinaHome, "server" + File.separator + "classes")); + packed.add(new File(catalinaHome, "server" + File.separator + "lib")); + } + if (shared) { + unpacked.add(new File(catalinaHome, "shared" + File.separator + "classes")); + packed.add(new File(catalinaHome, "shared" + File.separator + "lib")); + } + classLoader = ClassLoaderFactory.createClassLoader(unpacked.toArray(new File[0]), + packed.toArray(new File[0]), null); + } catch (Throwable t) { + Bootstrap.handleThrowable(t); + log.error("Class loader creation threw exception", t); + System.exit(1); + } + Thread.currentThread().setContextClassLoader(classLoader); + + // Load our application class + Class clazz = null; + String className = args[index++]; + try { + if (log.isDebugEnabled()) { + log.debug("Loading application class " + className); + } + clazz = classLoader.loadClass(className); + } catch (Throwable t) { + Bootstrap.handleThrowable(t); + log.error("Exception creating instance of " + className, t); + System.exit(1); + } + + Method method = null; + String params[] = new String[args.length - index]; + System.arraycopy(args, index, params, 0, params.length); + try { + if (log.isTraceEnabled()) { + log.trace("Identifying main() method"); + } + String methodName = "main"; + Class paramTypes[] = new Class[1]; + paramTypes[0] = params.getClass(); + method = clazz.getMethod(methodName, paramTypes); + } catch (Throwable t) { + Bootstrap.handleThrowable(t); + log.error("Exception locating main() method", t); + System.exit(1); + } + + // Invoke the main method of the application class + try { + if (log.isTraceEnabled()) { + log.trace("Calling main() method"); + } + Object paramValues[] = new Object[1]; + paramValues[0] = params; + method.invoke(null, paramValues); + } catch (Throwable t) { + t = Bootstrap.unwrapInvocationTargetException(t); + Bootstrap.handleThrowable(t); + log.error("Exception calling main() method", t); + System.exit(1); + } + + } + + + /** + * Display usage information about this tool. + */ + private static void usage() { + + log.info("Usage: java org.apache.catalina.startup.Tool [] []"); + + } + + +} diff --git a/java/org/apache/catalina/startup/UserConfig.java b/java/org/apache/catalina/startup/UserConfig.java new file mode 100644 index 0000000..1fd963b --- /dev/null +++ b/java/org/apache/catalina/startup/UserConfig.java @@ -0,0 +1,424 @@ +/* + * 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.catalina.startup; + + +import java.io.File; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.regex.Pattern; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Startup event listener for a Host that configures Contexts (web applications) for all defined "users" who have + * a web application in a directory with the specified name in their home directories. The context path of each deployed + * application will be set to ~xxxxx, where xxxxx is the username of the owning user for that web + * application + * + * @author Craig R. McClanahan + */ +public final class UserConfig implements LifecycleListener { + + + private static final Log log = LogFactory.getLog(UserConfig.class); + + + // ----------------------------------------------------- Instance Variables + + + /** + * The Java class name of the Context configuration class we should use. + */ + private String configClass = "org.apache.catalina.startup.ContextConfig"; + + + /** + * The Java class name of the Context implementation we should use. + */ + private String contextClass = "org.apache.catalina.core.StandardContext"; + + + /** + * The directory name to be searched for within each user home directory. + */ + private String directoryName = "public_html"; + + + /** + * The base directory containing user home directories. + */ + private String homeBase = null; + + + /** + * The Host we are associated with. + */ + private Host host = null; + + + /** + * The string resources for this package. + */ + private static final StringManager sm = StringManager.getManager(Constants.Package); + + + /** + * The Java class name of the user database class we should use. + */ + private String userClass = "org.apache.catalina.startup.PasswdUserDatabase"; + + /** + * A regular expression defining user who deployment is allowed. + */ + Pattern allow = null; + + /** + * A regular expression defining user who deployment is denied. + */ + Pattern deny = null; + + // ------------------------------------------------------------- Properties + + + /** + * @return the Context configuration class name. + */ + public String getConfigClass() { + return this.configClass; + } + + + /** + * Set the Context configuration class name. + * + * @param configClass The new Context configuration class name. + */ + public void setConfigClass(String configClass) { + this.configClass = configClass; + } + + + /** + * @return the Context implementation class name. + */ + public String getContextClass() { + return this.contextClass; + } + + + /** + * Set the Context implementation class name. + * + * @param contextClass The new Context implementation class name. + */ + public void setContextClass(String contextClass) { + this.contextClass = contextClass; + } + + + /** + * @return the directory name for user web applications. + */ + public String getDirectoryName() { + return this.directoryName; + } + + + /** + * Set the directory name for user web applications. + * + * @param directoryName The new directory name + */ + public void setDirectoryName(String directoryName) { + this.directoryName = directoryName; + } + + + /** + * @return the base directory containing user home directories. + */ + public String getHomeBase() { + return this.homeBase; + } + + + /** + * Set the base directory containing user home directories. + * + * @param homeBase The new base directory + */ + public void setHomeBase(String homeBase) { + this.homeBase = homeBase; + } + + + /** + * @return the user database class name for this component. + */ + public String getUserClass() { + return this.userClass; + } + + + /** + * Set the user database class name for this component. + * + * @param userClass The user database class name + */ + public void setUserClass(String userClass) { + this.userClass = userClass; + } + + /** + * @return the regular expression used to test for user who deployment is allowed. + */ + public String getAllow() { + if (allow == null) { + return null; + } + return allow.toString(); + } + + + /** + * Set the regular expression used to test for user who deployment is allowed. + * + * @param allow The new allow expression + */ + public void setAllow(String allow) { + if (allow == null || allow.length() == 0) { + this.allow = null; + } else { + this.allow = Pattern.compile(allow); + } + } + + + /** + * @return the regular expression used to test for user who deployment is denied. + */ + public String getDeny() { + if (deny == null) { + return null; + } + return deny.toString(); + } + + + /** + * Set the regular expression used to test for user who deployment is denied. + * + * @param deny The new deny expression + */ + public void setDeny(String deny) { + if (deny == null || deny.length() == 0) { + this.deny = null; + } else { + this.deny = Pattern.compile(deny); + } + } + + // --------------------------------------------------------- Public Methods + + + /** + * Process the START event for an associated Host. + * + * @param event The lifecycle event that has occurred + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + + // Identify the host we are associated with + try { + host = (Host) event.getLifecycle(); + } catch (ClassCastException e) { + log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e); + return; + } + + // Process the event that has occurred + if (event.getType().equals(Lifecycle.START_EVENT)) { + start(); + } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { + stop(); + } + + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Deploy a web application for any user who has a web application present in a directory with a specified name + * within their home directory. + */ + private void deploy() { + + if (host.getLogger().isTraceEnabled()) { + host.getLogger().trace(sm.getString("userConfig.deploying")); + } + + // Load the user database object for this host + UserDatabase database = null; + try { + Class clazz = Class.forName(userClass); + database = (UserDatabase) clazz.getConstructor().newInstance(); + database.setUserConfig(this); + } catch (Exception e) { + host.getLogger().error(sm.getString("userConfig.database"), e); + return; + } + + ExecutorService executor = host.getStartStopExecutor(); + List> results = new ArrayList<>(); + + // Deploy the web application (if any) for each defined user + Enumeration users = database.getUsers(); + while (users.hasMoreElements()) { + String user = users.nextElement(); + if (!isDeployAllowed(user)) { + continue; + } + String home = database.getHome(user); + results.add(executor.submit(new DeployUserDirectory(this, user, home))); + } + + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + host.getLogger().error(sm.getString("userConfig.deploy.threaded.error"), e); + } + } + } + + + /** + * Deploy a web application for the specified user if they have such an application in the defined directory within + * their home directory. + * + * @param user Username owning the application to be deployed + * @param home Home directory of this user + */ + private void deploy(String user, String home) { + + // Does this user have a web application to be deployed? + String contextPath = "/~" + user; + if (host.findChild(contextPath) != null) { + return; + } + File app = new File(home, directoryName); + if (!app.exists() || !app.isDirectory()) { + return; + } + + host.getLogger().info(sm.getString("userConfig.deploy", user)); + + // Deploy the web application for this user + try { + Class clazz = Class.forName(contextClass); + Context context = (Context) clazz.getConstructor().newInstance(); + context.setPath(contextPath); + context.setDocBase(app.toString()); + clazz = Class.forName(configClass); + LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance(); + context.addLifecycleListener(listener); + host.addChild(context); + } catch (Exception e) { + host.getLogger().error(sm.getString("userConfig.error", user), e); + } + + } + + + /** + * Process a "start" event for this Host. + */ + private void start() { + + if (host.getLogger().isDebugEnabled()) { + host.getLogger().debug(sm.getString("userConfig.start")); + } + + deploy(); + + } + + + /** + * Process a "stop" event for this Host. + */ + private void stop() { + + if (host.getLogger().isDebugEnabled()) { + host.getLogger().debug(sm.getString("userConfig.stop")); + } + + } + + /** + * Test allow and deny rules for the provided user. + * + * @return true if this user is allowed to deploy, false otherwise + */ + private boolean isDeployAllowed(String user) { + if (deny != null && deny.matcher(user).matches()) { + return false; + } + if (allow != null) { + if (allow.matcher(user).matches()) { + return true; + } else { + return false; + } + } + return true; + } + + private static class DeployUserDirectory implements Runnable { + + private UserConfig config; + private String user; + private String home; + + DeployUserDirectory(UserConfig config, String user, String home) { + this.config = config; + this.user = user; + this.home = home; + } + + @Override + public void run() { + config.deploy(user, home); + } + } + +} diff --git a/java/org/apache/catalina/startup/UserDatabase.java b/java/org/apache/catalina/startup/UserDatabase.java new file mode 100644 index 0000000..0b0d08e --- /dev/null +++ b/java/org/apache/catalina/startup/UserDatabase.java @@ -0,0 +1,65 @@ +/* + * 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.catalina.startup; + + +import java.util.Enumeration; + + +/** + * Abstraction of the set of users defined by the operating system on the current server platform. + * + * @author Craig R. McClanahan + */ +public interface UserDatabase { + + + // ----------------------------------------------------------- Properties + + + /** + * @return the UserConfig listener with which we are associated. + */ + UserConfig getUserConfig(); + + + /** + * Set the UserConfig listener with which we are associated. + * + * @param userConfig The new UserConfig listener + */ + void setUserConfig(UserConfig userConfig); + + + // ------------------------------------------------------- Public Methods + + + /** + * @return an absolute pathname to the home directory for the specified user. + * + * @param user User for which a home directory should be retrieved + */ + String getHome(String user); + + + /** + * @return an enumeration of the usernames defined on this server. + */ + Enumeration getUsers(); + + +} diff --git a/java/org/apache/catalina/startup/VersionLoggerListener.java b/java/org/apache/catalina/startup/VersionLoggerListener.java new file mode 100644 index 0000000..971baf3 --- /dev/null +++ b/java/org/apache/catalina/startup/VersionLoggerListener.java @@ -0,0 +1,131 @@ +/* + * 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.catalina.startup; + +import java.lang.management.ManagementFactory; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.catalina.util.ServerInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Logs version information on startup. + *

    + * This listener must only be nested within {@link Server} elements and should be the first listener defined. + */ +public class VersionLoggerListener implements LifecycleListener { + + private static final Log log = LogFactory.getLog(VersionLoggerListener.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + private boolean logArgs = true; + private boolean logEnv = false; + private boolean logProps = false; + + + public boolean getLogArgs() { + return logArgs; + } + + + public void setLogArgs(boolean logArgs) { + this.logArgs = logArgs; + } + + + public boolean getLogEnv() { + return logEnv; + } + + + public void setLogEnv(boolean logEnv) { + this.logEnv = logEnv; + } + + + public boolean getLogProps() { + return logProps; + } + + + public void setLogProps(boolean logProps) { + this.logProps = logProps; + } + + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) { + if (!(event.getLifecycle() instanceof Server)) { + log.warn(sm.getString("listener.notServer", event.getLifecycle().getClass().getSimpleName())); + } + log(); + } + } + + + private void log() { + log.info(sm.getString("versionLoggerListener.serverInfo.server.version", ServerInfo.getServerInfo())); + log.info(sm.getString("versionLoggerListener.serverInfo.server.built", ServerInfo.getServerBuilt())); + log.info(sm.getString("versionLoggerListener.serverInfo.server.number", ServerInfo.getServerNumber())); + log.info(sm.getString("versionLoggerListener.os.name", System.getProperty("os.name"))); + log.info(sm.getString("versionLoggerListener.os.version", System.getProperty("os.version"))); + log.info(sm.getString("versionLoggerListener.os.arch", System.getProperty("os.arch"))); + log.info(sm.getString("versionLoggerListener.java.home", System.getProperty("java.home"))); + log.info(sm.getString("versionLoggerListener.vm.version", System.getProperty("java.runtime.version"))); + log.info(sm.getString("versionLoggerListener.vm.vendor", System.getProperty("java.vm.vendor"))); + log.info(sm.getString("versionLoggerListener.catalina.base", System.getProperty(Constants.CATALINA_BASE_PROP))); + log.info(sm.getString("versionLoggerListener.catalina.home", System.getProperty(Constants.CATALINA_HOME_PROP))); + + if (logArgs) { + List args = ManagementFactory.getRuntimeMXBean().getInputArguments(); + for (String arg : args) { + log.info(sm.getString("versionLoggerListener.arg", arg)); + } + } + + if (logEnv) { + SortedMap sortedMap = new TreeMap<>(System.getenv()); + for (Map.Entry e : sortedMap.entrySet()) { + log.info(sm.getString("versionLoggerListener.env", e.getKey(), e.getValue())); + } + } + + if (logProps) { + SortedMap sortedMap = new TreeMap<>(); + for (Map.Entry e : System.getProperties().entrySet()) { + sortedMap.put(String.valueOf(e.getKey()), String.valueOf(e.getValue())); + } + for (Map.Entry e : sortedMap.entrySet()) { + log.info(sm.getString("versionLoggerListener.prop", e.getKey(), e.getValue())); + } + } + } +} diff --git a/java/org/apache/catalina/startup/WebAnnotationSet.java b/java/org/apache/catalina/startup/WebAnnotationSet.java new file mode 100644 index 0000000..abf6de8 --- /dev/null +++ b/java/org/apache/catalina/startup/WebAnnotationSet.java @@ -0,0 +1,355 @@ +/* + * 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.catalina.startup; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import jakarta.annotation.Resource; +import jakarta.annotation.Resources; +import jakarta.annotation.security.DeclareRoles; +import jakarta.annotation.security.RunAs; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.annotation.ServletSecurity; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.ApplicationServletRegistration; +import org.apache.catalina.util.Introspection; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextResource; +import org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef; +import org.apache.tomcat.util.descriptor.web.ContextService; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.MessageDestinationRef; +import org.apache.tomcat.util.res.StringManager; + +/** + * AnnotationSet for processing the annotations of the web application classes + * (/WEB-INF/classes and /WEB-INF/lib). + */ +public class WebAnnotationSet { + + private static final String SEPARATOR = "/"; + private static final String MAPPED_NAME_PROPERTY = "mappedName"; + + + /** + * The string resources for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + + // ---------------------------------------------------------- Public Methods + + /** + * Process the annotations on a context. + * + * @param context The context which will have its annotations processed + */ + public static void loadApplicationAnnotations(Context context) { + loadApplicationListenerAnnotations(context); + loadApplicationFilterAnnotations(context); + loadApplicationServletAnnotations(context); + } + + + // ------------------------------------------------------- Protected Methods + + /** + * Process the annotations for the listeners. + * + * @param context The context which will have its annotations processed + */ + protected static void loadApplicationListenerAnnotations(Context context) { + String[] applicationListeners = context.findApplicationListeners(); + for (String className : applicationListeners) { + Class clazz = Introspection.loadClass(context, className); + if (clazz == null) { + continue; + } + + loadClassAnnotation(context, clazz); + loadFieldsAnnotation(context, clazz); + loadMethodsAnnotation(context, clazz); + } + } + + + /** + * Process the annotations for the filters. + * + * @param context The context which will have its annotations processed + */ + protected static void loadApplicationFilterAnnotations(Context context) { + FilterDef[] filterDefs = context.findFilterDefs(); + for (FilterDef filterDef : filterDefs) { + Class clazz = Introspection.loadClass(context, filterDef.getFilterClass()); + if (clazz == null) { + continue; + } + + loadClassAnnotation(context, clazz); + loadFieldsAnnotation(context, clazz); + loadMethodsAnnotation(context, clazz); + } + } + + + /** + * Process the annotations for the servlets. + * + * @param context The context which will have its annotations processed + */ + protected static void loadApplicationServletAnnotations(Context context) { + + Container[] children = context.findChildren(); + for (Container child : children) { + if (child instanceof Wrapper) { + + Wrapper wrapper = (Wrapper) child; + if (wrapper.getServletClass() == null) { + continue; + } + + Class clazz = Introspection.loadClass(context, wrapper.getServletClass()); + if (clazz == null) { + continue; + } + + loadClassAnnotation(context, clazz); + loadFieldsAnnotation(context, clazz); + loadMethodsAnnotation(context, clazz); + + /* + * Process RunAs annotation which can be only on servlets. Ref JSR 250, equivalent to the run-as element + * in the deployment descriptor + */ + RunAs runAs = clazz.getAnnotation(RunAs.class); + if (runAs != null) { + wrapper.setRunAs(runAs.value()); + } + + // Process ServletSecurity annotation + ServletSecurity servletSecurity = clazz.getAnnotation(ServletSecurity.class); + if (servletSecurity != null) { + context.addServletSecurity(new ApplicationServletRegistration(wrapper, context), + new ServletSecurityElement(servletSecurity)); + } + } + } + } + + + /** + * Process the annotations on a context for a given className. + * + * @param context The context which will have its annotations processed + * @param clazz The class to examine for Servlet annotations + */ + protected static void loadClassAnnotation(Context context, Class clazz) { + /* + * Process Resource annotation. Ref JSR 250 + */ + Resource resourceAnnotation = clazz.getAnnotation(Resource.class); + if (resourceAnnotation != null) { + addResource(context, resourceAnnotation); + } + /* + * Process Resources annotation. Ref JSR 250 + */ + Resources resourcesAnnotation = clazz.getAnnotation(Resources.class); + if (resourcesAnnotation != null && resourcesAnnotation.value() != null) { + for (Resource resource : resourcesAnnotation.value()) { + addResource(context, resource); + } + } + /* + * Process DeclareRoles annotation. Ref JSR 250, equivalent to the security-role element in the deployment + * descriptor + */ + DeclareRoles declareRolesAnnotation = clazz.getAnnotation(DeclareRoles.class); + if (declareRolesAnnotation != null && declareRolesAnnotation.value() != null) { + for (String role : declareRolesAnnotation.value()) { + context.addSecurityRole(role); + } + } + } + + + protected static void loadFieldsAnnotation(Context context, Class clazz) { + // Initialize the annotations + Field[] fields = Introspection.getDeclaredFields(clazz); + if (fields != null && fields.length > 0) { + for (Field field : fields) { + Resource annotation = field.getAnnotation(Resource.class); + if (annotation != null) { + String defaultName = clazz.getName() + SEPARATOR + field.getName(); + Class defaultType = field.getType(); + addResource(context, annotation, defaultName, defaultType); + } + } + } + } + + + protected static void loadMethodsAnnotation(Context context, Class clazz) { + // Initialize the annotations + Method[] methods = Introspection.getDeclaredMethods(clazz); + if (methods != null && methods.length > 0) { + for (Method method : methods) { + Resource annotation = method.getAnnotation(Resource.class); + if (annotation != null) { + if (!Introspection.isValidSetter(method)) { + throw new IllegalArgumentException(sm.getString("webAnnotationSet.invalidInjection")); + } + + String defaultName = clazz.getName() + SEPARATOR + Introspection.getPropertyName(method); + + Class defaultType = (method.getParameterTypes()[0]); + addResource(context, annotation, defaultName, defaultType); + } + } + } + } + + + /** + * Process a Resource annotation to set up a Resource. Ref JSR 250, equivalent to the resource-ref, + * message-destination-ref, env-ref, resource-env-ref or service-ref element in the deployment descriptor. + * + * @param context The context which will have its annotations processed + * @param annotation The annotation that was found + */ + protected static void addResource(Context context, Resource annotation) { + addResource(context, annotation, null, null); + } + + + protected static void addResource(Context context, Resource annotation, String defaultName, Class defaultType) { + String name = getName(annotation, defaultName); + String type = getType(annotation, defaultType); + + if (type.equals("java.lang.String") || type.equals("java.lang.Character") || type.equals("java.lang.Integer") || + type.equals("java.lang.Boolean") || type.equals("java.lang.Double") || type.equals("java.lang.Byte") || + type.equals("java.lang.Short") || type.equals("java.lang.Long") || type.equals("java.lang.Float")) { + + // env-entry element + ContextEnvironment resource = new ContextEnvironment(); + + resource.setName(name); + resource.setType(type); + resource.setDescription(annotation.description()); + resource.setProperty(MAPPED_NAME_PROPERTY, annotation.mappedName()); + resource.setLookupName(annotation.lookup()); + + context.getNamingResources().addEnvironment(resource); + + } else if (type.equals("javax.xml.rpc.Service")) { + + // service-ref element + ContextService service = new ContextService(); + + service.setName(name); + service.setWsdlfile(annotation.mappedName()); + service.setType(type); + service.setDescription(annotation.description()); + service.setLookupName(annotation.lookup()); + + context.getNamingResources().addService(service); + + } else if (type.equals("javax.sql.DataSource") || type.equals("javax.jms.ConnectionFactory") || + type.equals("javax.jms.QueueConnectionFactory") || type.equals("javax.jms.TopicConnectionFactory") || + type.equals("jakarta.mail.Session") || type.equals("java.net.URL") || + type.equals("javax.resource.cci.ConnectionFactory") || type.equals("org.omg.CORBA_2_3.ORB") || + type.endsWith("ConnectionFactory")) { + + // resource-ref element + ContextResource resource = new ContextResource(); + + resource.setName(name); + resource.setType(type); + + if (annotation.authenticationType() == Resource.AuthenticationType.CONTAINER) { + resource.setAuth("Container"); + } else if (annotation.authenticationType() == Resource.AuthenticationType.APPLICATION) { + resource.setAuth("Application"); + } + + resource.setScope(annotation.shareable() ? "Shareable" : "Unshareable"); + resource.setProperty(MAPPED_NAME_PROPERTY, annotation.mappedName()); + resource.setDescription(annotation.description()); + resource.setLookupName(annotation.lookup()); + + context.getNamingResources().addResource(resource); + + } else if (type.equals("javax.jms.Queue") || type.equals("javax.jms.Topic")) { + + // message-destination-ref + MessageDestinationRef resource = new MessageDestinationRef(); + + resource.setName(name); + resource.setType(type); + resource.setUsage(annotation.mappedName()); + resource.setDescription(annotation.description()); + resource.setLookupName(annotation.lookup()); + + context.getNamingResources().addMessageDestinationRef(resource); + + } else { + /* + * General case. Also used for: - javax.resource.cci.InteractionSpec - jakarta.transaction.UserTransaction + */ + + // resource-env-ref + ContextResourceEnvRef resource = new ContextResourceEnvRef(); + + resource.setName(name); + resource.setType(type); + resource.setProperty(MAPPED_NAME_PROPERTY, annotation.mappedName()); + resource.setDescription(annotation.description()); + resource.setLookupName(annotation.lookup()); + + context.getNamingResources().addResourceEnvRef(resource); + } + } + + + private static String getType(Resource annotation, Class defaultType) { + Class type = annotation.type(); + if (type == null || type.equals(Object.class)) { + if (defaultType != null) { + type = defaultType; + } else { + type = Object.class; + } + } + return Introspection.convertPrimitiveType(type).getCanonicalName(); + } + + + private static String getName(Resource annotation, String defaultName) { + String name = annotation.name(); + if (name == null || name.equals("")) { + if (defaultName != null) { + name = defaultName; + } + } + return name; + } +} diff --git a/java/org/apache/catalina/startup/WebappServiceLoader.java b/java/org/apache/catalina/startup/WebappServiceLoader.java new file mode 100644 index 0000000..c58c42b --- /dev/null +++ b/java/org/apache/catalina/startup/WebappServiceLoader.java @@ -0,0 +1,239 @@ +/* + * 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.catalina.startup; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import jakarta.servlet.ServletContext; + +import org.apache.catalina.Context; +import org.apache.tomcat.util.scan.JarFactory; + +/** + * A variation of Java's JAR ServiceLoader that respects exclusion rules for web applications. + *

    + * Primarily intended for use loading ServletContainerInitializers as defined by Servlet 8.2.4. This implementation does + * not attempt lazy loading as the container is required to introspect all implementations discovered. + *

    + * If the ServletContext defines ORDERED_LIBS, then only JARs in WEB-INF/lib that are named in that set will be included + * in the search for provider configuration files; if ORDERED_LIBS is not defined then all JARs will be searched for + * provider configuration files. Providers defined by resources in the parent ClassLoader will always be returned. + *

    + * Provider classes will be loaded using the context's ClassLoader. + * + * @param The type of service to load + * + * @see jakarta.servlet.ServletContainerInitializer + * @see java.util.ServiceLoader + */ +public class WebappServiceLoader { + private static final String CLASSES = "/WEB-INF/classes/"; + private static final String LIB = "/WEB-INF/lib/"; + private static final String SERVICES = "META-INF/services/"; + + private final Context context; + private final ServletContext servletContext; + private final Pattern containerSciFilterPattern; + + + /** + * Construct a loader to load services from a ServletContext. + * + * @param context the context to use + */ + public WebappServiceLoader(Context context) { + this.context = context; + this.servletContext = context.getServletContext(); + String containerSciFilter = context.getContainerSciFilter(); + if (containerSciFilter != null && containerSciFilter.length() > 0) { + containerSciFilterPattern = Pattern.compile(containerSciFilter); + } else { + containerSciFilterPattern = null; + } + } + + + /** + * Load the providers for a service type. Container defined services will be loaded before application defined + * services in case the application depends on a Container provided service. Note that services are always loaded + * via the Context (web application) class loader so it is possible for an application to provide an alternative + * implementation of what would normally be a Container provided service. + * + * @param serviceType the type of service to load + * + * @return an unmodifiable collection of service providers + * + * @throws IOException if there was a problem loading any service + */ + public List load(Class serviceType) throws IOException { + String configFile = SERVICES + serviceType.getName(); + + // Obtain the Container provided service configuration files. + ClassLoader loader = context.getParentClassLoader(); + Enumeration containerResources; + if (loader == null) { + containerResources = ClassLoader.getSystemResources(configFile); + } else { + containerResources = loader.getResources(configFile); + } + + // Extract the Container provided service class names. Each + // configuration file may list more than one service class name. This + // uses a LinkedHashSet so if a service class name appears more than + // once in the configuration files, only the first one found is used. + LinkedHashSet containerServiceClassNames = new LinkedHashSet<>(); + Set containerServiceConfigFiles = new HashSet<>(); + while (containerResources.hasMoreElements()) { + URL containerServiceConfigFile = containerResources.nextElement(); + containerServiceConfigFiles.add(containerServiceConfigFile); + parseConfigFile(containerServiceClassNames, containerServiceConfigFile); + } + + // Filter the discovered container SCIs if required + if (containerSciFilterPattern != null) { + containerServiceClassNames.removeIf(s -> containerSciFilterPattern.matcher(s).find()); + } + + // Obtaining the application provided configuration files is a little + // more difficult for two reasons: + // - The web application may employ a custom class loader. Ideally, we + // would use ClassLoader.findResources() but that method is protected. + // We could force custom class loaders to override that method and + // make it public but that would be a new requirement and break + // backwards compatibility for what is an often customised component. + // - If the application web.xml file has defined an order for fragments + // then only those JAR files represented by fragments in that order + // (and arguably WEB-INF/classes) should be scanned for services. + LinkedHashSet applicationServiceClassNames = new LinkedHashSet<>(); + + // Check to see if the ServletContext has ORDERED_LIBS defined + @SuppressWarnings("unchecked") + List orderedLibs = (List) servletContext.getAttribute(ServletContext.ORDERED_LIBS); + + // Obtain the application provided service configuration files + if (orderedLibs == null) { + // Because a custom class loader may be being used, we have to use + // getResources() which will return application and Container files. + Enumeration allResources = servletContext.getClassLoader().getResources(configFile); + while (allResources.hasMoreElements()) { + URL serviceConfigFile = allResources.nextElement(); + // Only process the service configuration file if it is not a + // Container level file that has already been processed + if (!containerServiceConfigFiles.contains(serviceConfigFile)) { + parseConfigFile(applicationServiceClassNames, serviceConfigFile); + } + } + } else { + // Ordered libs so only use services defined in those libs and any + // in WEB-INF/classes + URL unpacked = servletContext.getResource(CLASSES + configFile); + if (unpacked != null) { + parseConfigFile(applicationServiceClassNames, unpacked); + } + + for (String lib : orderedLibs) { + URL jarUrl = servletContext.getResource(LIB + lib); + if (jarUrl == null) { + // should not happen, just ignore + continue; + } + + String base = jarUrl.toExternalForm(); + URL url; + if (base.endsWith("/")) { + URI uri; + try { + uri = new URI(base + configFile); + } catch (URISyntaxException e) { + // Not ideal but consistent with public API + throw new IOException(e); + } + url = uri.toURL(); + } else { + url = JarFactory.getJarEntryURL(jarUrl, configFile); + } + try { + parseConfigFile(applicationServiceClassNames, url); + } catch (FileNotFoundException e) { + // no provider file found, this is OK + } + } + } + + // Add the application services after the container services to ensure + // that the container services are loaded first + containerServiceClassNames.addAll(applicationServiceClassNames); + + // Short-cut if no services have been found + if (containerServiceClassNames.isEmpty()) { + return Collections.emptyList(); + } + // Load the discovered services + return loadServices(serviceType, containerServiceClassNames); + } + + + void parseConfigFile(LinkedHashSet servicesFound, URL url) throws IOException { + try (InputStream is = url.openStream(); + InputStreamReader in = new InputStreamReader(is, StandardCharsets.UTF_8); + BufferedReader reader = new BufferedReader(in)) { + String line; + while ((line = reader.readLine()) != null) { + int i = line.indexOf('#'); + if (i >= 0) { + line = line.substring(0, i); + } + line = line.trim(); + if (line.length() == 0) { + continue; + } + servicesFound.add(line); + } + } + } + + + List loadServices(Class serviceType, LinkedHashSet servicesFound) throws IOException { + ClassLoader loader = servletContext.getClassLoader(); + List services = new ArrayList<>(servicesFound.size()); + for (String serviceClass : servicesFound) { + try { + Class clazz = Class.forName(serviceClass, true, loader); + services.add(serviceType.cast(clazz.getConstructor().newInstance())); + } catch (ReflectiveOperationException | ClassCastException e) { + throw new IOException(e); + } + } + return Collections.unmodifiableList(services); + } +} diff --git a/java/org/apache/catalina/startup/mbeans-descriptors.xml b/java/org/apache/catalina/startup/mbeans-descriptors.xml new file mode 100644 index 0000000..66dd4cc --- /dev/null +++ b/java/org/apache/catalina/startup/mbeans-descriptors.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/storeconfig/CatalinaClusterSF.java b/java/org/apache/catalina/storeconfig/CatalinaClusterSF.java new file mode 100644 index 0000000..cfb8064 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/CatalinaClusterSF.java @@ -0,0 +1,95 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Valve; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.ha.ClusterDeployer; +import org.apache.catalina.ha.ClusterListener; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.tcp.SimpleTcpCluster; +import org.apache.catalina.tribes.Channel; + +/** + * Generate Cluster Element with Membership,Sender,Receiver,Deployer and + * ReplicationValve + */ +public class CatalinaClusterSF extends StoreFactoryBase { + + /** + * Store the specified Cluster children. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aCluster + * Cluster whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aCluster, + StoreDescription parentDesc) throws Exception { + if (aCluster instanceof CatalinaCluster) { + CatalinaCluster cluster = (CatalinaCluster) aCluster; + if (cluster instanceof SimpleTcpCluster) { + SimpleTcpCluster tcpCluster = (SimpleTcpCluster) cluster; + // Store nested element + ClusterManager manager = tcpCluster.getManagerTemplate(); + if (manager != null) { + storeElement(aWriter, indent, manager); + } + } + // Store nested element + Channel channel = cluster.getChannel(); + if (channel != null) { + storeElement(aWriter, indent, channel); + } + // Store nested element + ClusterDeployer deployer = cluster.getClusterDeployer(); + if (deployer != null) { + storeElement(aWriter, indent, deployer); + } + // Store nested element + // ClusterValve are not store at Hosts element, see + Valve valves[] = cluster.getValves(); + storeElementArray(aWriter, indent, valves); + + if (aCluster instanceof SimpleTcpCluster) { + // Store nested elements + LifecycleListener listeners[] = ((SimpleTcpCluster)cluster).findLifecycleListeners(); + storeElementArray(aWriter, indent, listeners); + // Store nested elements + ClusterListener mlisteners[] = ((SimpleTcpCluster)cluster).findClusterListeners(); + List clusterListeners = new ArrayList<>(); + for (ClusterListener clusterListener : mlisteners) { + if (clusterListener != deployer) { + clusterListeners.add(clusterListener); + } + } + storeElementArray(aWriter, indent, clusterListeners.toArray()); + } + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/CertificateStoreAppender.java b/java/org/apache/catalina/storeconfig/CertificateStoreAppender.java new file mode 100644 index 0000000..50bc717 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/CertificateStoreAppender.java @@ -0,0 +1,39 @@ +/** + * 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.catalina.storeconfig; + +import java.beans.PropertyDescriptor; + +import org.apache.tomcat.util.IntrospectionUtils; + +/** + * Store the Certificate attributes. + */ +public class CertificateStoreAppender extends StoreAppender { + + @Override + protected Object checkAttribute(StoreDescription desc, + PropertyDescriptor descriptor, String attributeName, Object bean, + Object bean2) { + if (attributeName.equals("type")) { + return IntrospectionUtils.getProperty(bean, descriptor.getName()); + } else { + return super.checkAttribute(desc, descriptor, attributeName, bean, bean2); + } + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/ChannelSF.java b/java/org/apache/catalina/storeconfig/ChannelSF.java new file mode 100644 index 0000000..d14c86c --- /dev/null +++ b/java/org/apache/catalina/storeconfig/ChannelSF.java @@ -0,0 +1,77 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; +import java.util.Iterator; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelReceiver; +import org.apache.catalina.tribes.ChannelSender; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.MembershipService; + +/** + * Generate Channel Element + */ +public class ChannelSF extends StoreFactoryBase { + + /** + * Store the specified Channel children. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aChannel + * Channel whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aChannel, + StoreDescription parentDesc) throws Exception { + if (aChannel instanceof Channel) { + Channel channel = (Channel) aChannel; + if (channel instanceof ManagedChannel) { + ManagedChannel managedChannel = (ManagedChannel) channel; + // Store nested element + MembershipService service = managedChannel.getMembershipService(); + if (service != null) { + storeElement(aWriter, indent, service); + } + // Store nested element + ChannelSender sender = managedChannel.getChannelSender(); + if (sender != null) { + storeElement(aWriter, indent, sender); + } + // Store nested element + ChannelReceiver receiver = managedChannel.getChannelReceiver(); + if (receiver != null) { + storeElement(aWriter, indent, receiver); + } + Iterator interceptors = managedChannel.getInterceptors(); + while (interceptors.hasNext()) { + ChannelInterceptor interceptor = interceptors.next(); + storeElement(aWriter, indent, interceptor); + } + } + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/ConnectorSF.java b/java/org/apache/catalina/storeconfig/ConnectorSF.java new file mode 100644 index 0000000..eb7b337 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/ConnectorSF.java @@ -0,0 +1,75 @@ +/** + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.connector.Connector; +import org.apache.coyote.UpgradeProtocol; +import org.apache.tomcat.util.net.SSLHostConfig; + +/** + * Store Connector and Listeners + */ +public class ConnectorSF extends StoreFactoryBase { + + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aConnector, + StoreDescription parentDesc) throws Exception { + + if (aConnector instanceof Connector) { + Connector connector = (Connector) aConnector; + // Store nested elements + LifecycleListener listeners[] = connector.findLifecycleListeners(); + storeElementArray(aWriter, indent, listeners); + // Store nested elements + UpgradeProtocol[] upgradeProtocols = connector.findUpgradeProtocols(); + storeElementArray(aWriter, indent, upgradeProtocols); + if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) { + // Store nested elements + SSLHostConfig[] hostConfigs = connector.findSslHostConfigs(); + storeElementArray(aWriter, indent, hostConfigs); + } + } + } + + protected void printOpenTag(PrintWriter aWriter, int indent, Object bean, + StoreDescription aDesc) throws Exception { + aWriter.print("<"); + aWriter.print(aDesc.getTag()); + storeConnectorAttributes(aWriter, indent, bean, aDesc); + aWriter.println(">"); + } + + protected void storeConnectorAttributes(PrintWriter aWriter, int indent, + Object bean, StoreDescription aDesc) throws Exception { + if (aDesc.isAttributes()) { + getStoreAppender().printAttributes(aWriter, indent, false, bean, + aDesc); + } + } + + protected void printTag(PrintWriter aWriter, int indent, Object bean, + StoreDescription aDesc) throws Exception { + aWriter.print("<"); + aWriter.print(aDesc.getTag()); + storeConnectorAttributes(aWriter, indent, bean, aDesc); + aWriter.println("/>"); + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/ConnectorStoreAppender.java b/java/org/apache/catalina/storeconfig/ConnectorStoreAppender.java new file mode 100644 index 0000000..2040358 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/ConnectorStoreAppender.java @@ -0,0 +1,307 @@ +/** + * 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.catalina.storeconfig; + +import java.beans.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.catalina.Globals; +import org.apache.catalina.connector.Connector; +import org.apache.coyote.ProtocolHandler; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.net.SocketProperties; + +/** + * Store the Connector attributes. Connector has really special design. A + * Connector is only a startup Wrapper for a ProtocolHandler. This meant that + * ProtocolHandler get all there attributes from the Connector attribute map. + * Strange is that some attributes change their name. + */ +public class ConnectorStoreAppender extends StoreAppender { + + protected static final HashMap replacements = new HashMap<>(); + protected static final Set internalExecutorAttributes = new HashSet<>(); + static { + replacements.put("timeout", "connectionUploadTimeout"); + replacements.put("randomfile", "randomFile"); + + internalExecutorAttributes.add("maxThreads"); + internalExecutorAttributes.add("minSpareThreads"); + internalExecutorAttributes.add("threadPriority"); + } + + @Override + public void printAttributes(PrintWriter writer, int indent, + boolean include, Object bean, StoreDescription desc) + throws Exception { + + // Render a className attribute if requested + if (include && !desc.isStandard()) { + writer.print(" className=\""); + writer.print(bean.getClass().getName()); + writer.print("\""); + } + + Connector connector = (Connector) bean; + String protocol = connector.getProtocol(); + List propertyKeys = getPropertyKeys(connector); + // Create blank instance + Object bean2 = new Connector(protocol);//defaultInstance(bean); + for (String key : propertyKeys) { + Object value = IntrospectionUtils.getProperty(bean, key); + if (desc.isTransientAttribute(key)) { + continue; // Skip the specified exceptions + } + if (value == null) { + continue; // Null values are not persisted + } + if (!isPersistable(value.getClass())) { + continue; + } + Object value2 = IntrospectionUtils.getProperty(bean2, key); + if (value.equals(value2)) { + // The property has its default value + continue; + } + if (isPrintValue(bean, bean2, key, desc)) { + printValue(writer, indent, key, value); + } + } + if (protocol != null && !"HTTP/1.1".equals(protocol)) { + super.printValue(writer, indent, "protocol", protocol); + } + String executorName = connector.getExecutorName(); + if (!Connector.INTERNAL_EXECUTOR_NAME.equals(executorName)) { + super.printValue(writer, indent, "executor", executorName); + } + + } + + /** + * Get all properties from Connector and current ProtocolHandler. + * + * @param bean The connector + * @return List of Connector property names + * @throws IntrospectionException Error introspecting connector + */ + protected List getPropertyKeys(Connector bean) + throws IntrospectionException { + List propertyKeys = new ArrayList<>(); + // Acquire the list of properties for this bean + ProtocolHandler protocolHandler = bean.getProtocolHandler(); + // Acquire the list of properties for this bean + PropertyDescriptor descriptors[] = Introspector.getBeanInfo( + bean.getClass()).getPropertyDescriptors(); + if (descriptors == null) { + descriptors = new PropertyDescriptor[0]; + } + for (PropertyDescriptor descriptor : descriptors) { + if (descriptor instanceof IndexedPropertyDescriptor) { + continue; // Indexed properties are not persisted + } + if (!isPersistable(descriptor.getPropertyType()) + || (descriptor.getReadMethod() == null) + || (descriptor.getWriteMethod() == null)) { + continue; // Must be a read-write primitive or String + } + if ("protocol".equals(descriptor.getName()) + || "protocolHandlerClassName".equals(descriptor + .getName())) { + continue; + } + propertyKeys.add(descriptor.getName()); + } + // Add the properties of the protocol handler + descriptors = Introspector.getBeanInfo( + protocolHandler.getClass()).getPropertyDescriptors(); + if (descriptors == null) { + descriptors = new PropertyDescriptor[0]; + } + for (PropertyDescriptor descriptor : descriptors) { + if (descriptor instanceof IndexedPropertyDescriptor) { + continue; // Indexed properties are not persisted + } + if (!isPersistable(descriptor.getPropertyType()) + || (descriptor.getReadMethod() == null) + || (descriptor.getWriteMethod() == null)) { + continue; // Must be a read-write primitive or String + } + String key = descriptor.getName(); + if (!Connector.INTERNAL_EXECUTOR_NAME.equals(bean.getExecutorName()) && + internalExecutorAttributes.contains(key)) { + continue; + } + if (replacements.get(key) != null) { + key = replacements.get(key); + } + if (!propertyKeys.contains(key)) { + propertyKeys.add(key); + } + } + // Add the properties for the socket + final String socketName = "socket."; + descriptors = Introspector.getBeanInfo( + SocketProperties.class).getPropertyDescriptors(); + if (descriptors == null) { + descriptors = new PropertyDescriptor[0]; + } + for (PropertyDescriptor descriptor : descriptors) { + if (descriptor instanceof IndexedPropertyDescriptor) { + continue; // Indexed properties are not persisted + } + if (!isPersistable(descriptor.getPropertyType()) + || (descriptor.getReadMethod() == null) + || (descriptor.getWriteMethod() == null)) { + continue; // Must be a read-write primitive or String + } + String key = descriptor.getName(); + if (replacements.get(key) != null) { + key = replacements.get(key); + } + if (!propertyKeys.contains(key)) { + // Add socket.[original name] if this is not a property + // that could be set elsewhere + propertyKeys.add(socketName + descriptor.getName()); + } + } + return propertyKeys; + } + + /** + * Print Attributes for the connector + * + * @param aWriter Current writer + * @param indent Indentation level + * @param bean The connector bean + * @param aDesc The connector description + * @throws Exception Store error occurred + */ + protected void storeConnectorAttributes(PrintWriter aWriter, int indent, + Object bean, StoreDescription aDesc) throws Exception { + if (aDesc.isAttributes()) { + printAttributes(aWriter, indent, false, bean, aDesc); + } + } + + /** + * Print the open tag for connector attributes (override). + * + * @see org.apache.catalina.storeconfig.StoreAppender#printOpenTag(java.io.PrintWriter, + * int, java.lang.Object, + * org.apache.catalina.storeconfig.StoreDescription) + */ + @Override + public void printOpenTag(PrintWriter aWriter, int indent, Object bean, + StoreDescription aDesc) throws Exception { + aWriter.print("<"); + aWriter.print(aDesc.getTag()); + storeConnectorAttributes(aWriter, indent, bean, aDesc); + aWriter.println(">"); + } + + /** + * Print a tag for connector attributes (override). + * + * @see org.apache.catalina.storeconfig.StoreAppender#printTag(java.io.PrintWriter, + * int, java.lang.Object, + * org.apache.catalina.storeconfig.StoreDescription) + */ + @Override + public void printTag(PrintWriter aWriter, int indent, Object bean, + StoreDescription aDesc) throws Exception { + aWriter.print("<"); + aWriter.print(aDesc.getTag()); + storeConnectorAttributes(aWriter, indent, bean, aDesc); + aWriter.println("/>"); + } + + /** + * Print a value but replace certain attribute names. + * + * @see org.apache.catalina.storeconfig.StoreAppender#printValue(java.io.PrintWriter, + * int, String, Object) + */ + @Override + public void printValue(PrintWriter writer, int indent, String name, + Object value) { + String repl = name; + if (replacements.get(name) != null) { + repl = replacements.get(name); + } + super.printValue(writer, indent, repl, value); + } + + /** + * Print Connector Values.

    • Special handling to default jkHome. + *
    • Don't save catalina.base path at server.xml
    + * + * @see org.apache.catalina.storeconfig.StoreAppender#isPrintValue(Object, + * Object, String, StoreDescription) + */ + @Override + public boolean isPrintValue(Object bean, Object bean2, String attrName, + StoreDescription desc) { + boolean isPrint = super.isPrintValue(bean, bean2, attrName, desc); + if (isPrint) { + if ("jkHome".equals(attrName)) { + Connector connector = (Connector) bean; + File catalinaBase = getCatalinaBase(); + File jkHomeBase = getJkHomeBase((String) connector + .getProperty("jkHome"), catalinaBase); + isPrint = !catalinaBase.equals(jkHomeBase); + + } + } + return isPrint; + } + + protected File getCatalinaBase() { + + File file = new File(System.getProperty(Globals.CATALINA_BASE_PROP)); + try { + file = file.getCanonicalFile(); + } catch (IOException e) { + } + return file; + } + + protected File getJkHomeBase(String jkHome, File appBase) { + + File jkHomeBase; + File file = new File(jkHome); + if (!file.isAbsolute()) { + file = new File(appBase, jkHome); + } + try { + jkHomeBase = file.getCanonicalFile(); + } catch (IOException e) { + jkHomeBase = file; + } + return jkHomeBase; + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/Constants.java b/java/org/apache/catalina/storeconfig/Constants.java new file mode 100644 index 0000000..84821cd --- /dev/null +++ b/java/org/apache/catalina/storeconfig/Constants.java @@ -0,0 +1,23 @@ +/* + * 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.catalina.storeconfig; + +public class Constants { + + public static final String Package = "org.apache.catalina.storeconfig"; + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/CredentialHandlerSF.java b/java/org/apache/catalina/storeconfig/CredentialHandlerSF.java new file mode 100644 index 0000000..57418ad --- /dev/null +++ b/java/org/apache/catalina/storeconfig/CredentialHandlerSF.java @@ -0,0 +1,87 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.CredentialHandler; +import org.apache.catalina.realm.NestedCredentialHandler; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Store server.xml Element CredentialHandler + */ +public class CredentialHandlerSF extends StoreFactoryBase { + + private static Log log = LogFactory.getLog(CredentialHandlerSF.class); + + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + if (aElement instanceof NestedCredentialHandler) { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass()); + + if (elementDesc != null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("factory.storeTag", + elementDesc.getTag(), aElement)); + } + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printOpenTag(aWriter, indent + 2, aElement, + elementDesc); + storeChildren(aWriter, indent + 2, aElement, elementDesc); + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printCloseTag(aWriter, elementDesc); + } else { + if (log.isWarnEnabled()) { + log.warn(sm.getString("factory.storeNoDescriptor", + aElement.getClass())); + } + } + } else { + super.store(aWriter, indent, aElement); + } + } + + /** + * Store the specified CredentialHandler properties and child (CredentialHandler) + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aCredentialHandler + * CredentialHandler whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aCredentialHandler, + StoreDescription parentDesc) throws Exception { + if (aCredentialHandler instanceof NestedCredentialHandler) { + NestedCredentialHandler nestedCredentialHandler = (NestedCredentialHandler) aCredentialHandler; + + // Store nested element + CredentialHandler[] credentialHandlers = nestedCredentialHandler.getCredentialHandlers(); + storeElementArray(aWriter, indent, credentialHandlers); + } + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/GlobalNamingResourcesSF.java b/java/org/apache/catalina/storeconfig/GlobalNamingResourcesSF.java new file mode 100644 index 0000000..7c86bc5 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/GlobalNamingResourcesSF.java @@ -0,0 +1,71 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * store server.xml GlobalNamingResource. + */ +public class GlobalNamingResourcesSF extends StoreFactoryBase { + private static Log log = LogFactory.getLog(GlobalNamingResourcesSF.class); + + /* + * Store with NamingResource Factory + * + * @see org.apache.catalina.storeconfig.IStoreFactory#store(java.io.PrintWriter, + * int, java.lang.Object) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + + if (aElement instanceof NamingResourcesImpl) { + + StoreDescription elementDesc = getRegistry().findDescription( + NamingResourcesImpl.class.getName() + + ".[GlobalNamingResources]"); + + if (elementDesc != null) { + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printOpenTag(aWriter, indent + 2, aElement, + elementDesc); + NamingResourcesImpl resources = (NamingResourcesImpl) aElement; + StoreDescription resourcesdesc = getRegistry().findDescription( + NamingResourcesImpl.class.getName()); + if (resourcesdesc != null) { + resourcesdesc.getStoreFactory().store(aWriter, indent + 2, + resources); + } else { + log.warn(sm.getString("globalNamingResourcesSF.noFactory")); + } + + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printCloseTag(aWriter, elementDesc); + } else { + log.warn(sm.getString("storeFactory.noDescriptor", aElement.getClass(), "GlobalNamingResources")); + } + } else { + log.warn(sm.getString("globalNamingResourcesSF.wrongElement", aElement.getClass())); + } + } +} + diff --git a/java/org/apache/catalina/storeconfig/IStoreConfig.java b/java/org/apache/catalina/storeconfig/IStoreConfig.java new file mode 100644 index 0000000..c89f4a1 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/IStoreConfig.java @@ -0,0 +1,131 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Server; +import org.apache.catalina.Service; + +public interface IStoreConfig { + + /** + * Get Configuration Registry + * + * @return aRegistry that handle the store operations + */ + StoreRegistry getRegistry(); + + /** + * Set Configuration Registry + * + * @param aRegistry + * aregistry that handle the store operations + */ + void setRegistry(StoreRegistry aRegistry); + + /** + * Get associated server + * + * @return aServer the associated server + */ + Server getServer(); + + /** + * Set associated server + * + * @param aServer the associated server + */ + void setServer(Server aServer); + + /** + * Store the current StoreFactory Server. + */ + void storeConfig(); + + /** + * Store the specified Server properties. + * + * @param aServer + * Object to be stored + * @return true if the store operation was successful + */ + boolean store(Server aServer); + + /** + * Store the specified Server properties. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aServer + * Object to be stored + * @throws Exception Store error occurred + */ + void store(PrintWriter aWriter, int indent, Server aServer) throws Exception; + + /** + * Store the specified Service properties. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aService + * Object to be stored + * @throws Exception Store error occurred + */ + void store(PrintWriter aWriter, int indent, Service aService) throws Exception; + + /** + * Store the specified Host properties. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aHost + * Object to be stored + * @throws Exception Store error occurred + */ + void store(PrintWriter aWriter, int indent, Host aHost) throws Exception; + + /** + * Store the specified Context properties. + * + * @param aContext + * Object to be stored + * @return true if the store operation was successful + */ + boolean store(Context aContext); + + /** + * Store the specified Context properties. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aContext + * Object to be stored + * @throws Exception Store error occurred + */ + void store(PrintWriter aWriter, int indent, Context aContext) throws Exception; +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/IStoreFactory.java b/java/org/apache/catalina/storeconfig/IStoreFactory.java new file mode 100644 index 0000000..cfe0f63 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/IStoreFactory.java @@ -0,0 +1,34 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +public interface IStoreFactory { + StoreAppender getStoreAppender(); + + void setStoreAppender(StoreAppender storeWriter); + + void setRegistry(StoreRegistry aRegistry); + + StoreRegistry getRegistry(); + + void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception; + + void storeXMLHead(PrintWriter aWriter); +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/InterceptorSF.java b/java/org/apache/catalina/storeconfig/InterceptorSF.java new file mode 100644 index 0000000..16d86e9 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/InterceptorSF.java @@ -0,0 +1,84 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.group.interceptors.StaticMembershipInterceptor; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Generate Interceptor Element + */ +public class InterceptorSF extends StoreFactoryBase { + + private static Log log = LogFactory.getLog(InterceptorSF.class); + + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + if (aElement instanceof StaticMembershipInterceptor) { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass()); + + if (elementDesc != null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("factory.storeTag", + elementDesc.getTag(), aElement)); + } + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printOpenTag(aWriter, indent + 2, aElement, + elementDesc); + storeChildren(aWriter, indent + 2, aElement, elementDesc); + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printCloseTag(aWriter, elementDesc); + } else { + if (log.isWarnEnabled()) { + log.warn(sm.getString("factory.storeNoDescriptor", + aElement.getClass())); + } + } + } else { + super.store(aWriter, indent, aElement); + } + } + + /** + * Store the specified Interceptor child. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aInterceptor + * Channel whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aInterceptor, + StoreDescription parentDesc) throws Exception { + if (aInterceptor instanceof StaticMembershipInterceptor) { + ChannelInterceptor interceptor = (ChannelInterceptor) aInterceptor; + // Store nested elements + storeElementArray(aWriter, indent + 2, interceptor.getMembers()); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/JarScannerSF.java b/java/org/apache/catalina/storeconfig/JarScannerSF.java new file mode 100644 index 0000000..30164c7 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/JarScannerSF.java @@ -0,0 +1,56 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.tomcat.JarScanFilter; +import org.apache.tomcat.JarScanner; + +/** + * Store server.xml Element JarScanner + */ +public class JarScannerSF extends StoreFactoryBase { + + /** + * Store the specified JarScanner properties and children + * (JarScannerFilter) + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aJarScanner + * JarScanner whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aJarScanner, + StoreDescription parentDesc) throws Exception { + if (aJarScanner instanceof JarScanner) { + JarScanner jarScanner = (JarScanner) aJarScanner; + // Store nested element + JarScanFilter jarScanFilter = jarScanner.getJarScanFilter(); + if (jarScanFilter != null) { + storeElement(aWriter, indent, jarScanFilter); + } + } + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/LoaderSF.java b/java/org/apache/catalina/storeconfig/LoaderSF.java new file mode 100644 index 0000000..6f3a4a2 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/LoaderSF.java @@ -0,0 +1,84 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.Loader; +import org.apache.catalina.loader.WebappLoader; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Store Loader Element. + */ +public class LoaderSF extends StoreFactoryBase { + + private static Log log = LogFactory.getLog(LoaderSF.class); + + /** + * Store the only the Loader elements, when not default + * + * @see NamingResourcesSF#storeChildren(PrintWriter, int, Object, StoreDescription) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass()); + if (elementDesc != null) { + Loader loader = (Loader) aElement; + if (!isDefaultLoader(loader)) { + if (log.isTraceEnabled()) { + log.trace("store " + elementDesc.getTag() + "( " + aElement + " )"); + } + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printTag(aWriter, indent + 2, loader, + elementDesc); + } + } else { + if (log.isWarnEnabled()) { + if (log.isWarnEnabled()) { + log.warn(sm.getString("factory.storeNoDescriptor", aElement + .getClass())); + } + } + } + } + + /** + * Is this an instance of the default Loader configuration, + * with all-default properties? + * + * @param loader + * Loader to be tested + * @return true if this is an instance of the default loader + */ + protected boolean isDefaultLoader(Loader loader) { + + if (!(loader instanceof WebappLoader)) { + return false; + } + WebappLoader wloader = (WebappLoader) loader; + if ((wloader.getDelegate() != false) + || !wloader.getLoaderClass().equals( + "org.apache.catalina.loader.WebappClassLoader")) { + return false; + } + return true; + } +} diff --git a/java/org/apache/catalina/storeconfig/LocalStrings.properties b/java/org/apache/catalina/storeconfig/LocalStrings.properties new file mode 100644 index 0000000..2facbe1 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/LocalStrings.properties @@ -0,0 +1,44 @@ +# 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. + +config.emptyObjectName=Invalid null or empty object name +config.missingContextFile=Missing configuration file of context [{0}] to store +config.objectNameNotFound=Object name [{0}] not found +config.storeContextError=Error storing context [{0}] +config.storeServerError=Error storing server + +factory.storeNoDescriptor=Descriptor for element class [{0}] not configured! +factory.storeTag=store tag [{0}] ( Object: [{1}] ) + +globalNamingResourcesSF.noFactory=Cannot find NamingResources store factory +globalNamingResourcesSF.wrongElement=Wrong element [{0}] + +registry.loadClassFailed=Failed to load class [{0}] +registry.noDescriptor=Can't find descriptor for key [{0}] + +standardContextSF.cannotWriteFile=Cannot write file at [{0}] +standardContextSF.canonicalPathError=Failed to obtain the canonical path of the configuration file [{0}] +standardContextSF.moveFailed=Context original file at [{0}] is null, not a file or not writable +standardContextSF.storeContext=Store context [{0}] configuration separately at path [{1}] +standardContextSF.storeContextWithBackup=Store context [{0}] configuration separately with backup at path [{1}] + +storeConfigListener.loadError=Error loading StoreConfig +storeConfigListener.notServer=This listener must only be nested within Server elements, but is in [{0}], ignoring it. +storeConfigListener.registerError=Error registering StoreConfig MBean + +storeFactory.noDescriptor=Descriptor for element [{0}].[{1}] not configured + +storeFileMover.directoryCreationError=Cannot create directory [{0}] +storeFileMover.renameError=Cannot rename [{0}] to [{1}] diff --git a/java/org/apache/catalina/storeconfig/LocalStrings_fr.properties b/java/org/apache/catalina/storeconfig/LocalStrings_fr.properties new file mode 100644 index 0000000..548d983 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/LocalStrings_fr.properties @@ -0,0 +1,44 @@ +# 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. + +config.emptyObjectName=Nom d'objet null ou vide invalide +config.missingContextFile=Le fichier de configuration du contexte [{0}] qui doit être persisté est manquant +config.objectNameNotFound=Le nom d''objet [{0}] n''a pas été trouvé +config.storeContextError=Erreur d''enregistrement du contexte [{0}] +config.storeServerError=Erreur d'enregistrement de la configuration du serveur + +factory.storeNoDescriptor=Le descripteur pour l''élément de la classe [{0}] n''est pas configuré +factory.storeTag=enregistrement du tag [{0}] (objet : [{1}]) + +globalNamingResourcesSF.noFactory=Impossible de trouver la fabrique pour enregistrer les NamingResources +globalNamingResourcesSF.wrongElement=Mauvais élément [{0}] + +registry.loadClassFailed=Echec de chargement de la classe [{0}] +registry.noDescriptor=Impossible de trouver le descripteur pour la clé [{0}] + +standardContextSF.cannotWriteFile=Impossible d''écrire le fichier à [{0}] +standardContextSF.canonicalPathError=Impossible d''obtenir le chemin canonique du fichier de configuration [{0}] +standardContextSF.moveFailed=Le fichier d''origine du contexte à [{0}] est null, n''est pas un fichier, ou il ne peut pas être écrit +standardContextSF.storeContext=Enregistrement de la configuration du contexte [{0}] dans un fichier séparé au chemin [{1}] +standardContextSF.storeContextWithBackup=Enregistrement de la configuration du contexte [{0}] dans un fichier séparé avec une sauvegarde à [{1}] + +storeConfigListener.loadError=Erreur lors du chargement de StoreConfig +storeConfigListener.notServer=L'écouteur a été ajouté à un composant autre que le Server et sera donc ignoré +storeConfigListener.registerError=Erreur lors de l'enregistrement du MBean de StoreConfig + +storeFactory.noDescriptor=Le descripteur de l''élément [{0}].[{1}] n''est pas configuré + +storeFileMover.directoryCreationError=Impossible de créer le répertoire [{0}] +storeFileMover.renameError=Impossible de renommer [{0}] en [{1}] diff --git a/java/org/apache/catalina/storeconfig/LocalStrings_ja.properties b/java/org/apache/catalina/storeconfig/LocalStrings_ja.properties new file mode 100644 index 0000000..d92f5b3 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/LocalStrings_ja.properties @@ -0,0 +1,44 @@ +# 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. + +config.emptyObjectName=nullã¾ãŸã¯ç©ºã®ã‚ªãƒ–ジェクトåã¯ç„¡åŠ¹ã§ã™ +config.missingContextFile=コンテキスト [{0}] ã®è¨­å®šãƒ•ã‚¡ã‚¤ãƒ«ãŒã‚ã‚Šã¾ã›ã‚“ +config.objectNameNotFound=ObjectName[{0}]ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +config.storeContextError=コンテキスト [{0}] ã‚’æ ¼ç´ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+config.storeServerError=server.xml 書ãè¾¼ã¿ä¸­ã®ã‚¨ãƒ©ãƒ¼ + +factory.storeNoDescriptor=è¦ç´ ã‚¯ãƒ©ã‚¹ [{0}] ã®è¨˜è¿°å­ãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“ï¼ +factory.storeTag=ストアタグ [{0}] (オブジェクト: [{1}]) + +globalNamingResourcesSF.noFactory=NamingResourcesストアファクトリãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +globalNamingResourcesSF.wrongElement=é–“é•ã£ãŸè¦ç´ [{0}] + +registry.loadClassFailed=クラス [{0}] ã®èª­ã¿è¾¼ã¿ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ +registry.noDescriptor=キー [{0}] ã®è¨˜è¿°å­ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ + +standardContextSF.cannotWriteFile=ファイル [{0}] ã¸æ›¸ãè¾¼ã¿ã§ãã¾ã›ã‚“。 +standardContextSF.canonicalPathError=設定ファイル [{0}] ã®æ­£è¦åŒ–パスをå–å¾—ã§ãã¾ã›ã‚“。 +standardContextSF.moveFailed=[{0}]ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã‚ªãƒªã‚¸ãƒŠãƒ«ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã€ãƒ•ã‚¡ã‚¤ãƒ«ã§ã¯ãªãã€æ›¸ãè¾¼ã¿å¯èƒ½ã§ã¯ãªã„nullã§ã™ã€‚ +standardContextSF.storeContext=コンテキスト [{0}] ã®è¨­å®šã‚’パス [{1}] ã«åˆ¥ã€…ã«ä¿å­˜ã—ã¾ã™ +standardContextSF.storeContextWithBackup=パス [{1}] ã®ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—を使用ã—ã¦ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{0}] ã®è¨­å®šã‚’個別ã«ä¿å­˜ã—ã¾ã™ + +storeConfigListener.loadError=StoreConfigã®èª­ã¿è¾¼ã¿ã‚¨ãƒ©ãƒ¼ +storeConfigListener.notServer=Server 以外ã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆã«æŒ‡å®šã•ã‚ŒãŸãƒªã‚¹ãƒŠãƒ¼ã¯ç„¡è¦–ã—ã¾ã™ã€‚ +storeConfigListener.registerError=StoreConfig MBean登録中ã®ã‚¨ãƒ©ãƒ¼ + +storeFactory.noDescriptor=è¦ç´ [{0}]ã®è¨˜è¿°å­[{1}]ãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 + +storeFileMover.directoryCreationError=ディレクトリ [{0}] を作æˆã§ãã¾ã›ã‚“ +storeFileMover.renameError=ファイルå [{0}] ã‚’ [{1}] ã«å¤‰æ›´ã§ãã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/storeconfig/LocalStrings_ko.properties b/java/org/apache/catalina/storeconfig/LocalStrings_ko.properties new file mode 100644 index 0000000..fd07a96 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/LocalStrings_ko.properties @@ -0,0 +1,43 @@ +# 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. + +config.emptyObjectName=ë„ì´ê±°ë‚˜ 빈 문ìžì—´ì¸ ê°ì²´ ì´ë¦„ì€ ìœ íš¨í•˜ì§€ 않습니다. +config.missingContextFile=저장해야 í•  컨í…스트 [{0}]ì˜ ì„¤ì • 파ì¼ì´ 없습니다. +config.objectNameNotFound=ê°ì²´ ì´ë¦„ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +config.storeContextError=컨í…스트 [{0}]ì„(를) 저장하는 중 오류 ë°œìƒ +config.storeServerError=서버를 중지시키는 중 오류 ë°œìƒ + +factory.storeNoDescriptor=엘리먼트 í´ëž˜ìŠ¤ [{0}]ì„(를) 위한 descriptorê°€ 설정ë˜ì§€ 않았습니다! +factory.storeTag=태그 [{0}]ì„(를) 저장합니다. ( ê°ì²´: [{1}] ) + +globalNamingResourcesSF.noFactory=NamingResources 저장소 팩토리를 ì°¾ì„ ìˆ˜ 없습니다. +globalNamingResourcesSF.wrongElement=ìž˜ëª»ëœ ì—˜ë¦¬ë¨¼íŠ¸: [{0}] + +registry.loadClassFailed=í´ëž˜ìŠ¤ [{0}]ì„(를) 로드하지 못했습니다. + +standardContextSF.cannotWriteFile=[{0}]ì— íŒŒì¼ì„ 쓸 수 없습니다. +standardContextSF.canonicalPathError=설정 íŒŒì¼ [{0}]ì˜ canonical 경로를 구하지 못했습니다. +standardContextSF.moveFailed=[{0}]ì— ìžˆëŠ” 컨í…스트 ì›ë³¸ 파ì¼ì´, ë„ì´ê±°ë‚˜, 파ì¼ì´ 아니거나, ë˜ëŠ” 쓰기 가능하지 ì•Šì€ íŒŒì¼ìž…니다. +standardContextSF.storeContext=컨í…스트 [{0}]ì˜ ì„¤ì •ì„, 경로 [{1}]ì— ë³„ë„ë¡œ 저장합니다. +standardContextSF.storeContextWithBackup=컨í…스트 [{0}]ì˜ ì„¤ì •ì„, 경로 [{1}]ì— ìœ„ì¹˜í•œ 백업과 분리하여 저장합니다. + +storeConfigListener.loadError=StoreConfig를 로드하는 중 오류 ë°œìƒ +storeConfigListener.notServer=서버가 ì•„ë‹Œ 다른 êµ¬ì„±ìš”ì†Œì— ë¦¬ìŠ¤ë„ˆê°€ 추가ë˜ì—ˆìœ¼ë¯€ë¡œ, ì´ ë¦¬ìŠ¤ë„ˆëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +storeConfigListener.registerError=StoreConfig MBeanì„ ë“±ë¡í•˜ëŠ” 중 오류 ë°œìƒ + +storeFactory.noDescriptor=엘리먼트 [{0}].[{1}]ì„(를) 위한 descriptorê°€ 설정ë˜ì§€ 않았습니다. + +storeFileMover.directoryCreationError=디렉토리 [{0}]ì„(를) ìƒì„±í•  수 없습니다. +storeFileMover.renameError=[{0}]ì„(를) [{1}](으)ë¡œ ì´ë¦„ì„ ë³€ê²½í•  수 없습니다. diff --git a/java/org/apache/catalina/storeconfig/LocalStrings_pt_BR.properties b/java/org/apache/catalina/storeconfig/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..3600300 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +standardContextSF.canonicalPathError=Erro ao obter o caminho canônico do arquivo de configuração [{0}] diff --git a/java/org/apache/catalina/storeconfig/LocalStrings_zh_CN.properties b/java/org/apache/catalina/storeconfig/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..6673d8f --- /dev/null +++ b/java/org/apache/catalina/storeconfig/LocalStrings_zh_CN.properties @@ -0,0 +1,43 @@ +# 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. + +config.emptyObjectName=无效的空对象å +config.missingContextFile=缺少è¦å­˜å‚¨çš„上下文[{0}]çš„é…置文件 +config.objectNameNotFound=目标[{0}]未找到 +config.storeContextError=存储上下文[{0}]时出错 +config.storeServerError=存储æœåŠ¡å™¨æ—¶å‡ºé”™ + +factory.storeNoDescriptor=元素类[{0}]çš„æ述符未é…ç½®ï¼ +factory.storeTag=存储标记[{0}](对象:[{1}])。 + +globalNamingResourcesSF.noFactory=找ä¸åˆ°NamingResources存储工厂 +globalNamingResourcesSF.wrongElement=错误的元素[{0}] + +registry.loadClassFailed=无法加载类 [{0}] + +standardContextSF.cannotWriteFile=无法在 [{0}] 写入文件 +standardContextSF.canonicalPathError=无法获å–é…置文件[{0}]的规范路径 +standardContextSF.moveFailed=[{0}]处的上下文原始文件为空,ä¸æ˜¯æ–‡ä»¶æˆ–ä¸å¯å†™ +standardContextSF.storeContext=将上下文[{0}]é…置分别存储在路径[{1}] +standardContextSF.storeContextWithBackup=将上下文[{0}]é…置与备份分别存储在路径[{1}] + +storeConfigListener.loadError=加载StoreConfig时出错 +storeConfigListener.notServer=侦å¬å™¨å¿…须嵌套在Server元素内,但是处于[{0}]中,忽略它 +storeConfigListener.registerError=注册StoreConfig MBean时出错 + +storeFactory.noDescriptor=元素[{0}].[{1}]çš„æ述符未é…ç½® + +storeFileMover.directoryCreationError=无法创建目录 [{0}] +storeFileMover.renameError=无法将 [{0}] é‡å‘½å为 [{1}] diff --git a/java/org/apache/catalina/storeconfig/ManagerSF.java b/java/org/apache/catalina/storeconfig/ManagerSF.java new file mode 100644 index 0000000..85f7fda --- /dev/null +++ b/java/org/apache/catalina/storeconfig/ManagerSF.java @@ -0,0 +1,96 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.Manager; +import org.apache.catalina.SessionIdGenerator; +import org.apache.catalina.session.StandardManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Store server.xml Manager element + */ +public class ManagerSF extends StoreFactoryBase { + + private static Log log = LogFactory.getLog(ManagerSF.class); + + /** + * Store the only the Manager elements + * + * @see NamingResourcesSF#storeChildren(PrintWriter, int, Object, StoreDescription) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass()); + if (elementDesc != null) { + if (aElement instanceof StandardManager) { + StandardManager manager = (StandardManager) aElement; + if (!isDefaultManager(manager)) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("factory.storeTag", elementDesc + .getTag(), aElement)); + } + super.store(aWriter, indent, aElement); + } + } else { + super.store(aWriter, indent, aElement); + } + } else { + if (log.isWarnEnabled()) { + log.warn(sm.getString("factory.storeNoDescriptor", aElement + .getClass())); + } + } + } + + /** + * Is this an instance of the default Manager configuration, + * with all-default properties? + * + * @param smanager + * Manager to be tested + * @return true if this is an instance of the default manager + */ + protected boolean isDefaultManager(StandardManager smanager) { + + if (!"SESSIONS.ser".equals(smanager.getPathname()) + || (smanager.getMaxActiveSessions() != -1)) { + return false; + } + return true; + + } + + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aManager, + StoreDescription parentDesc) throws Exception { + if (aManager instanceof Manager) { + Manager manager = (Manager) aManager; + // Store nested element; + SessionIdGenerator sessionIdGenerator = manager.getSessionIdGenerator(); + if (sessionIdGenerator != null) { + storeElement(aWriter, indent, sessionIdGenerator); + } + } + } + +} diff --git a/java/org/apache/catalina/storeconfig/NamingResourcesSF.java b/java/org/apache/catalina/storeconfig/NamingResourcesSF.java new file mode 100644 index 0000000..db49f81 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/NamingResourcesSF.java @@ -0,0 +1,105 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.descriptor.web.ContextEjb; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextLocalEjb; +import org.apache.tomcat.util.descriptor.web.ContextResource; +import org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef; +import org.apache.tomcat.util.descriptor.web.ContextResourceLink; + +/** + * Store server.xml elements Resources at context and GlobalNamingResources + */ +public class NamingResourcesSF extends StoreFactoryBase { + private static Log log = LogFactory.getLog(NamingResourcesSF.class); + + /** + * Store the only the NamingResources elements + * + * @see NamingResourcesSF#storeChildren(PrintWriter, int, Object, StoreDescription) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass()); + if (elementDesc != null) { + if (log.isTraceEnabled()) { + log.trace("store " + elementDesc.getTag() + "( " + aElement + " )"); + } + storeChildren(aWriter, indent, aElement, elementDesc); + } else { + log.warn(sm.getString("storeFactory.noDescriptor", aElement.getClass(), "NamingResources")); + } + } + + /** + * Store the specified NamingResources properties. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aElement + * Object whose properties are being stored + * @param elementDesc + * element descriptor + * + * @exception Exception + * if an exception occurs while storing + * + * @see org.apache.catalina.storeconfig.StoreFactoryBase#storeChildren(java.io.PrintWriter, + * int, java.lang.Object, StoreDescription) + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aElement, + StoreDescription elementDesc) throws Exception { + + if (aElement instanceof NamingResourcesImpl) { + NamingResourcesImpl resources = (NamingResourcesImpl) aElement; + // Store nested elements + ContextEjb[] ejbs = resources.findEjbs(); + storeElementArray(aWriter, indent, ejbs); + // Store nested elements + ContextEnvironment[] envs = resources.findEnvironments(); + storeElementArray(aWriter, indent, envs); + // Store nested elements + ContextLocalEjb[] lejbs = resources.findLocalEjbs(); + storeElementArray(aWriter, indent, lejbs); + + // Store nested elements + ContextResource[] dresources = resources.findResources(); + storeElementArray(aWriter, indent, dresources); + + // Store nested elements + ContextResourceEnvRef[] resEnv = resources.findResourceEnvRefs(); + storeElementArray(aWriter, indent, resEnv); + + // Store nested elements + ContextResourceLink[] resourceLinks = resources.findResourceLinks(); + storeElementArray(aWriter, indent, resourceLinks); + } + } +} + diff --git a/java/org/apache/catalina/storeconfig/OpenSSLConfSF.java b/java/org/apache/catalina/storeconfig/OpenSSLConfSF.java new file mode 100644 index 0000000..8fcd6e7 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/OpenSSLConfSF.java @@ -0,0 +1,44 @@ +/** + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.tomcat.util.net.openssl.OpenSSLConf; +import org.apache.tomcat.util.net.openssl.OpenSSLConfCmd; + +/** + * Store OpenSSLConf + */ +public class OpenSSLConfSF extends StoreFactoryBase { + + /** + * Store nested OpenSSLConfCmd elements. + * {@inheritDoc} + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aOpenSSLConf, + StoreDescription parentDesc) throws Exception { + if (aOpenSSLConf instanceof OpenSSLConf) { + OpenSSLConf openSslConf = (OpenSSLConf) aOpenSSLConf; + // Store nested elements + OpenSSLConfCmd[] openSSLConfCmds = openSslConf.getCommands().toArray(new OpenSSLConfCmd[0]); + storeElementArray(aWriter, indent + 2, openSSLConfCmds); + } + } + +} diff --git a/java/org/apache/catalina/storeconfig/PersistentManagerSF.java b/java/org/apache/catalina/storeconfig/PersistentManagerSF.java new file mode 100644 index 0000000..a368a9f --- /dev/null +++ b/java/org/apache/catalina/storeconfig/PersistentManagerSF.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.SessionIdGenerator; +import org.apache.catalina.Store; +import org.apache.catalina.session.PersistentManager; + +/** + * store server.xml PersistentManager element with nested "Store" + */ +public class PersistentManagerSF extends StoreFactoryBase { + + /** + * Store the specified PersistentManager properties. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aManager + * PersistentManager whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aManager, + StoreDescription parentDesc) throws Exception { + if (aManager instanceof PersistentManager) { + PersistentManager manager = (PersistentManager) aManager; + + // Store nested element + Store store = manager.getStore(); + storeElement(aWriter, indent, store); + + // Store nested element + SessionIdGenerator sessionIdGenerator = manager.getSessionIdGenerator(); + if (sessionIdGenerator != null) { + storeElement(aWriter, indent, sessionIdGenerator); + } + + } + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/RealmSF.java b/java/org/apache/catalina/storeconfig/RealmSF.java new file mode 100644 index 0000000..5da1338 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/RealmSF.java @@ -0,0 +1,94 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.CredentialHandler; +import org.apache.catalina.Realm; +import org.apache.catalina.realm.CombinedRealm; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Store server.xml Element Realm + */ +public class RealmSF extends StoreFactoryBase { + + private static Log log = LogFactory.getLog(RealmSF.class); + + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + if (aElement instanceof CombinedRealm) { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass()); + + if (elementDesc != null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("factory.storeTag", + elementDesc.getTag(), aElement)); + } + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printOpenTag(aWriter, indent + 2, aElement, + elementDesc); + storeChildren(aWriter, indent + 2, aElement, elementDesc); + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printCloseTag(aWriter, elementDesc); + } else { + if (log.isWarnEnabled()) { + log.warn(sm.getString("factory.storeNoDescriptor", + aElement.getClass())); + } + } + } else { + super.store(aWriter, indent, aElement); + } + } + + /** + * Store the specified Realm properties and child (Realm) + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aRealm + * Realm whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aRealm, + StoreDescription parentDesc) throws Exception { + if (aRealm instanceof CombinedRealm) { + CombinedRealm combinedRealm = (CombinedRealm) aRealm; + + // Store nested element + Realm[] realms = combinedRealm.getNestedRealms(); + storeElementArray(aWriter, indent, realms); + } + // Store nested element + CredentialHandler credentialHandler = ((Realm) aRealm).getCredentialHandler(); + if (credentialHandler != null + && !(credentialHandler.getClass().getName().equals("org.apache.catalina.realm.CombinedRealm$CombinedRealmCredentialHandler"))) { + storeElement(aWriter, indent, credentialHandler); + } + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/SSLHostConfigSF.java b/java/org/apache/catalina/storeconfig/SSLHostConfigSF.java new file mode 100644 index 0000000..2efdc05 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/SSLHostConfigSF.java @@ -0,0 +1,60 @@ +/** + * 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.catalina.storeconfig; + +import java.io.PrintWriter; +import java.util.ArrayList; + +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.openssl.OpenSSLConf; + +/** + * Store SSLHostConfig + */ +public class SSLHostConfigSF extends StoreFactoryBase { + + /** + * Store nested SSLHostConfigCertificate elements. + * {@inheritDoc} + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aSSLHostConfig, + StoreDescription parentDesc) throws Exception { + if (aSSLHostConfig instanceof SSLHostConfig) { + SSLHostConfig sslHostConfig = (SSLHostConfig) aSSLHostConfig; + // Store nested elements + SSLHostConfigCertificate[] hostConfigsCertificates = sslHostConfig.getCertificates().toArray(new SSLHostConfigCertificate[0]); + // Remove a possible default UNDEFINED certificate + if (hostConfigsCertificates.length > 1) { + ArrayList certificates = new ArrayList<>(); + for (SSLHostConfigCertificate certificate : hostConfigsCertificates) { + if (Type.UNDEFINED != certificate.getType()) { + certificates.add(certificate); + } + } + hostConfigsCertificates = certificates.toArray(new SSLHostConfigCertificate[0]); + } + storeElementArray(aWriter, indent, hostConfigsCertificates); + // Store nested element + OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); + storeElement(aWriter, indent, openSslConf); + } + } + +} diff --git a/java/org/apache/catalina/storeconfig/SenderSF.java b/java/org/apache/catalina/storeconfig/SenderSF.java new file mode 100644 index 0000000..8e4095b --- /dev/null +++ b/java/org/apache/catalina/storeconfig/SenderSF.java @@ -0,0 +1,54 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.tribes.transport.MultiPointSender; +import org.apache.catalina.tribes.transport.ReplicationTransmitter; + +/** + * Generate Sender Element + */ +public class SenderSF extends StoreFactoryBase { + + /** + * Store the specified Sender child. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aSender + * Channel whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aSender, + StoreDescription parentDesc) throws Exception { + if (aSender instanceof ReplicationTransmitter) { + ReplicationTransmitter transmitter = (ReplicationTransmitter) aSender; + // Store nested element + MultiPointSender transport = transmitter.getTransport(); + if (transport != null) { + storeElement(aWriter, indent, transport); + } + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/StandardContextSF.java b/java/org/apache/catalina/storeconfig/StandardContextSF.java new file mode 100644 index 0000000..e1b9e9c --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StandardContextSF.java @@ -0,0 +1,367 @@ +/* + * 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.catalina.storeconfig; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Loader; +import org.apache.catalina.Manager; +import org.apache.catalina.Realm; +import org.apache.catalina.Valve; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.ThreadLocalLeakPreventionListener; +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.catalina.util.ContextName; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.util.descriptor.web.ApplicationParameter; +import org.apache.tomcat.util.http.CookieProcessor; + +/** + * Store server.xml Context element with all children + *
      + *
    • Store all context at server.xml
    • + *
    • Store existing app.xml context a conf/enginename/hostname/app.xml
    • + *
    • Store with backup
    • + *
    + */ +public class StandardContextSF extends StoreFactoryBase { + + private static Log log = LogFactory.getLog(StandardContextSF.class); + + /** + * Store a Context as Separate file as configFile value from context exists. + * filename can be relative to catalina.base. + * + * @see org.apache.catalina.storeconfig.IStoreFactory#store(java.io.PrintWriter, + * int, java.lang.Object) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aContext) + throws Exception { + + if (aContext instanceof StandardContext) { + StoreDescription desc = getRegistry().findDescription( + aContext.getClass()); + if (desc != null && desc.isStoreSeparate()) { + URL configFile = ((StandardContext) aContext) + .getConfigFile(); + if (configFile != null) { + if (desc.isExternalAllowed()) { + if (desc.isBackup()) { + storeWithBackup((StandardContext) aContext); + } else { + storeContextSeparate(aWriter, indent, + (StandardContext) aContext); + } + return; + } + } else if (desc.isExternalOnly()) { + // Set a configFile so that the configuration is actually saved + Context context = ((StandardContext) aContext); + Host host = (Host) context.getParent(); + File configBase = host.getConfigBaseFile(); + ContextName cn = new ContextName(context.getName(), false); + String baseName = cn.getBaseName(); + File xml = new File(configBase, baseName + ".xml"); + context.setConfigFile(xml.toURI().toURL()); + if (desc.isBackup()) { + storeWithBackup((StandardContext) aContext); + } else { + storeContextSeparate(aWriter, indent, + (StandardContext) aContext); + } + return; + } + } + } + super.store(aWriter, indent, aContext); + + } + + /** + * Store a Context without backup add separate file or when configFile = + * null a aWriter. + * + * @param aWriter Current output writer + * @param indent Indentation level + * @param aContext The context which will be stored + * @throws Exception Configuration storing error + */ + protected void storeContextSeparate(PrintWriter aWriter, int indent, + StandardContext aContext) throws Exception { + URL configFile = aContext.getConfigFile(); + if (configFile != null) { + File config = new File(configFile.toURI()); + if (!config.isAbsolute()) { + config = new File(System.getProperty(Globals.CATALINA_BASE_PROP), + config.getPath()); + } + if( (!config.isFile()) || (!config.canWrite())) { + throw new IOException(sm.getString("standardContextSF.cannotWriteFile", configFile)); + } + if (log.isInfoEnabled()) { + log.info(sm.getString("standardContextSF.storeContext", aContext.getPath(), config)); + } + try (FileOutputStream fos = new FileOutputStream(config); + PrintWriter writer = new PrintWriter(new OutputStreamWriter( + fos , getRegistry().getEncoding()))) { + storeXMLHead(writer); + super.store(writer, -2, aContext); + } + } else { + super.store(aWriter, indent, aContext); + } + } + + /** + * Store the Context with a Backup. + * + * @param aContext The context which will be stored + * @throws Exception Configuration storing error + */ + protected void storeWithBackup(StandardContext aContext) throws Exception { + StoreFileMover mover = getConfigFileWriter(aContext); + if (mover != null) { + // Bugzilla 37781 Check to make sure we can write this output file + if ((mover.getConfigOld() == null) + || (mover.getConfigOld().isDirectory()) + || (mover.getConfigOld().exists() && + !mover.getConfigOld().canWrite())) { + throw new IOException(sm.getString("standardContextSF.moveFailed", mover.getConfigOld())); + } + File dir = mover.getConfigSave().getParentFile(); + if (dir != null && dir.isDirectory() && (!dir.canWrite())) { + throw new IOException(sm.getString("standardContextSF.cannotWriteFile", mover.getConfigSave())); + } + if (log.isInfoEnabled()) { + log.info(sm.getString("standardContextSF.storeContextWithBackup", + aContext.getPath(), mover.getConfigSave())); + } + try (PrintWriter writer = mover.getWriter()) { + storeXMLHead(writer); + super.store(writer, -2, aContext); + } + mover.move(); + } + } + + /** + * Get explicit writer for context (context.getConfigFile()). + * + * @param context The context which will be stored + * @return The file mover + * @throws Exception Error getting a writer for the configuration file + */ + protected StoreFileMover getConfigFileWriter(Context context) + throws Exception { + URL configFile = context.getConfigFile(); + StoreFileMover mover = null; + if (configFile != null) { + File config = new File(configFile.toURI()); + if (!config.isAbsolute()) { + config = new File(System.getProperty(Globals.CATALINA_BASE_PROP), + config.getPath()); + } + // Open an output writer for the new configuration file + mover = new StoreFileMover("", config.getCanonicalPath(), + getRegistry().getEncoding()); + } + return mover; + } + + /** + * Store the specified context element children. + * + * @param aWriter Current output writer + * @param indent Indentation level + * @param aContext Context to store + * @param parentDesc The element description + * @throws Exception Configuration storing error + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aContext, + StoreDescription parentDesc) throws Exception { + if (aContext instanceof StandardContext) { + StandardContext context = (StandardContext) aContext; + // Store nested elements + LifecycleListener listeners[] = context.findLifecycleListeners(); + List listenersArray = new ArrayList<>(); + for (LifecycleListener listener : listeners) { + if (!(listener instanceof ThreadLocalLeakPreventionListener)) { + listenersArray.add(listener); + } + } + storeElementArray(aWriter, indent, listenersArray.toArray()); + + // Store nested elements + Valve valves[] = context.getPipeline().getValves(); + storeElementArray(aWriter, indent, valves); + + // Store nested elements + Loader loader = context.getLoader(); + storeElement(aWriter, indent, loader); + + // Store nested elements + if (context.getCluster() == null || !context.getDistributable()) { + Manager manager = context.getManager(); + storeElement(aWriter, indent, manager); + } + + // Store nested element + Realm realm = context.getRealm(); + if (realm != null) { + Realm parentRealm = null; + // @TODO is this case possible? + if (context.getParent() != null) { + parentRealm = context.getParent().getRealm(); + } + if (realm != parentRealm) { + storeElement(aWriter, indent, realm); + } + } + // Store nested resources + WebResourceRoot resources = context.getResources(); + storeElement(aWriter, indent, resources); + + // Store nested elements + String wLifecycles[] = context.findWrapperLifecycles(); + getStoreAppender().printTagArray(aWriter, "WrapperListener", + indent + 2, wLifecycles); + // Store nested elements + String wListeners[] = context.findWrapperListeners(); + getStoreAppender().printTagArray(aWriter, "WrapperLifecycle", + indent + 2, wListeners); + + // Store nested elements + ApplicationParameter[] appParams = context + .findApplicationParameters(); + storeElementArray(aWriter, indent, appParams); + + // Store nested naming resources elements (EJB,Resource,...) + NamingResourcesImpl nresources = context.getNamingResources(); + storeElement(aWriter, indent, nresources); + + // Store nested watched resources + String[] wresources = context.findWatchedResources(); + wresources = filterWatchedResources(context, wresources); + getStoreAppender().printTagArray(aWriter, "WatchedResource", + indent + 2, wresources); + + // Store nested elements + JarScanner jarScanner = context.getJarScanner(); + storeElement(aWriter, indent, jarScanner); + + // Store nested elements + CookieProcessor cookieProcessor = context.getCookieProcessor(); + storeElement(aWriter, indent, cookieProcessor); + } + } + + /** + * Return a File object representing the "configuration root" directory for + * our associated Host. + * @param context The context instance + * @return a file to the configuration base path + */ + protected File configBase(Context context) { + + File file = new File(System.getProperty(Globals.CATALINA_BASE_PROP), "conf"); + Container host = context.getParent(); + + if (host instanceof Host) { + Container engine = host.getParent(); + if (engine instanceof Engine) { + file = new File(file, engine.getName()); + } + file = new File(file, host.getName()); + try { + file = file.getCanonicalFile(); + } catch (IOException e) { + log.error(sm.getString("standardContextSF.canonicalPathError"), e); + } + } + return file; + + } + + /** + * Filter out the default watched resources, to remove standard ones. + * + * @param context The context instance + * @param wresources The raw watched resources list + * @return The filtered watched resources + * @throws Exception Configuration storing error + * TODO relative watched resources + * TODO absolute handling configFile + * TODO Filename case handling for Windows? + * TODO digester variable substitution $catalina.base, $catalina.home + */ + protected String[] filterWatchedResources(StandardContext context, + String[] wresources) throws Exception { + File configBase = configBase(context); + String confContext = new File(System.getProperty(Globals.CATALINA_BASE_PROP), + "conf/context.xml").getCanonicalPath(); + String confWeb = new File(System.getProperty(Globals.CATALINA_BASE_PROP), + "conf/web.xml").getCanonicalPath(); + String confHostDefault = new File(configBase, "context.xml.default") + .getCanonicalPath(); + String configFile = (context.getConfigFile() != null ? new File(context.getConfigFile().toURI()).getCanonicalPath() : null); + String webxml = "WEB-INF/web.xml"; + String tomcatwebxml = "WEB-INF/tomcat-web.xml"; + + List resource = new ArrayList<>(); + for (String wresource : wresources) { + if (wresource.equals(confContext)) { + continue; + } + if (wresource.equals(confWeb)) { + continue; + } + if (wresource.equals(confHostDefault)) { + continue; + } + if (wresource.equals(configFile)) { + continue; + } + if (wresource.equals(webxml)) { + continue; + } + if (wresource.equals(tomcatwebxml)) { + continue; + } + resource.add(wresource); + } + return resource.toArray(new String[0]); + } + +} diff --git a/java/org/apache/catalina/storeconfig/StandardEngineSF.java b/java/org/apache/catalina/storeconfig/StandardEngineSF.java new file mode 100644 index 0000000..8963cc7 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StandardEngineSF.java @@ -0,0 +1,93 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.Cluster; +import org.apache.catalina.Container; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Realm; +import org.apache.catalina.Valve; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.ha.ClusterValve; + +/** + * Store server.xml Element Engine + */ +public class StandardEngineSF extends StoreFactoryBase { + + /** + * Store the specified Engine properties. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aEngine + * Object whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aEngine, + StoreDescription parentDesc) throws Exception { + if (aEngine instanceof StandardEngine) { + StandardEngine engine = (StandardEngine) aEngine; + // Store nested elements + LifecycleListener listeners[] = engine.findLifecycleListeners(); + storeElementArray(aWriter, indent, listeners); + + // Store nested element + Realm realm = engine.getRealm(); + Realm parentRealm = null; + // TODO is this case possible? (see it a old Server 5.0 impl) + if (engine.getParent() != null) { + parentRealm = engine.getParent().getRealm(); + } + if (realm != parentRealm) { + storeElement(aWriter, indent, realm); + + } + + // Store nested elements + Valve valves[] = engine.getPipeline().getValves(); + if(valves != null && valves.length > 0 ) { + List engineValves = new ArrayList<>() ; + for (Valve valve : valves) { + if (!(valve instanceof ClusterValve)) { + engineValves.add(valve); + } + } + storeElementArray(aWriter, indent, engineValves.toArray()); + } + + // store all elements + Cluster cluster = engine.getCluster(); + if (cluster != null) { + storeElement(aWriter, indent, cluster); + } + // store all elements + Container children[] = engine.findChildren(); + storeElementArray(aWriter, indent, children); + + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/StandardHostSF.java b/java/org/apache/catalina/storeconfig/StandardHostSF.java new file mode 100644 index 0000000..54d2aee --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StandardHostSF.java @@ -0,0 +1,106 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.Cluster; +import org.apache.catalina.Container; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Realm; +import org.apache.catalina.Valve; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.ha.ClusterValve; + +/** + * Store server.xml Element Host + */ +public class StandardHostSF extends StoreFactoryBase { + + /** + * Store the specified Host properties and children + * (Listener,Alias,Realm,Valve,Cluster, Context) + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aHost + * Host whose properties are being stored + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aHost, + StoreDescription parentDesc) throws Exception { + if (aHost instanceof StandardHost) { + StandardHost host = (StandardHost) aHost; + // Store nested elements + LifecycleListener listeners[] = host.findLifecycleListeners(); + storeElementArray(aWriter, indent, listeners); + + // Store nested elements + String aliases[] = host.findAliases(); + getStoreAppender().printTagArray(aWriter, "Alias", indent + 2, + aliases); + + // Store nested element + Realm realm = host.getRealm(); + if (realm != null) { + Realm parentRealm = null; + if (host.getParent() != null) { + parentRealm = host.getParent().getRealm(); + } + if (realm != parentRealm) { + storeElement(aWriter, indent, realm); + } + } + + // Store nested elements + Valve valves[] = host.getPipeline().getValves(); + if(valves != null && valves.length > 0 ) { + List hostValves = new ArrayList<>() ; + for (Valve valve : valves) { + if (!(valve instanceof ClusterValve)) { + hostValves.add(valve); + } + } + storeElementArray(aWriter, indent, hostValves.toArray()); + } + + // store all elements + Cluster cluster = host.getCluster(); + if (cluster != null) { + Cluster parentCluster = null; + if (host.getParent() != null) { + parentCluster = host.getParent().getCluster(); + } + if (cluster != parentCluster) { + storeElement(aWriter, indent, cluster); + } + } + + // store all elements + Container children[] = host.findChildren(); + storeElementArray(aWriter, indent, children); + } + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/StandardServerSF.java b/java/org/apache/catalina/storeconfig/StandardServerSF.java new file mode 100644 index 0000000..6db94f4 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StandardServerSF.java @@ -0,0 +1,87 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Service; +import org.apache.catalina.core.StandardServer; +import org.apache.catalina.deploy.NamingResourcesImpl; + +/** + * Store server.xml Server element and children ( + * Listener,GlobalNamingResource,Service) + */ +public class StandardServerSF extends StoreFactoryBase { + + /** + * Store the specified Server properties. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * @param aServer + * Object to be stored + * + * @exception Exception + * if an exception occurs while storing + * @see org.apache.catalina.storeconfig.IStoreFactory#store(java.io.PrintWriter, + * int, java.lang.Object) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aServer) + throws Exception { + storeXMLHead(aWriter); + super.store(aWriter, indent, aServer); + } + + /** + * Store the specified server element children. + * + * @param aWriter Current output writer + * @param indent Indentation level + * @param aObject Server to store + * @param parentDesc The element description + * @throws Exception Configuration storing error + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aObject, + StoreDescription parentDesc) throws Exception { + if (aObject instanceof StandardServer) { + StandardServer server = (StandardServer) aObject; + // Store nested elements + LifecycleListener listeners[] = server.findLifecycleListeners(); + storeElementArray(aWriter, indent, listeners); + // Store nested element + NamingResourcesImpl globalNamingResources = server + .getGlobalNamingResources(); + StoreDescription elementDesc = getRegistry().findDescription( + NamingResourcesImpl.class.getName() + + ".[GlobalNamingResources]"); + if (elementDesc != null) { + elementDesc.getStoreFactory().store(aWriter, indent, + globalNamingResources); + } + // Store nested elements + Service services[] = server.findServices(); + storeElementArray(aWriter, indent, services); + } + } + +} diff --git a/java/org/apache/catalina/storeconfig/StandardServiceSF.java b/java/org/apache/catalina/storeconfig/StandardServiceSF.java new file mode 100644 index 0000000..5700932 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StandardServiceSF.java @@ -0,0 +1,68 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.Engine; +import org.apache.catalina.Executor; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.StandardService; + +/** + * Store server.xml Element Service and all children + */ +public class StandardServiceSF extends StoreFactoryBase { + + /** + * Store the specified service element children. + * + * @param aWriter Current output writer + * @param indent Indentation level + * @param aService Service to store + * @param parentDesc The element description + * @throws Exception Configuration storing error + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aService, + StoreDescription parentDesc) throws Exception { + if (aService instanceof StandardService) { + StandardService service = (StandardService) aService; + // Store nested elements + LifecycleListener listeners[] = service.findLifecycleListeners(); + storeElementArray(aWriter, indent, listeners); + + // Store nested elements + Executor[] executors = service.findExecutors(); + storeElementArray(aWriter, indent, executors); + + Connector connectors[] = service.findConnectors(); + storeElementArray(aWriter, indent, connectors); + + // Store nested element + Engine container = service.getContainer(); + if (container != null) { + StoreDescription elementDesc = getRegistry().findDescription(container.getClass()); + if (elementDesc != null) { + IStoreFactory factory = elementDesc.getStoreFactory(); + factory.store(aWriter, indent, container); + } + } + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/StoreAppender.java b/java/org/apache/catalina/storeconfig/StoreAppender.java new file mode 100644 index 0000000..2dba031 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreAppender.java @@ -0,0 +1,387 @@ +/* + * 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.catalina.storeconfig; + +import java.beans.IndexedPropertyDescriptor; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.util.Iterator; + +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.descriptor.web.ResourceBase; +import org.apache.tomcat.util.security.Escape; + +/** + * StoreAppends generate really the xml tag elements + */ +public class StoreAppender { + + /** + * The set of classes that represent persistable properties. + */ + private static Class persistables[] = { String.class, Integer.class, + Integer.TYPE, Boolean.class, Boolean.TYPE, Byte.class, Byte.TYPE, + Character.class, Character.TYPE, Double.class, Double.TYPE, + Float.class, Float.TYPE, Long.class, Long.TYPE, Short.class, + Short.TYPE, InetAddress.class }; + + private int pos = 0; + + /** + * Print the closing tag. + * + * @param aWriter The output writer + * @param aDesc Store description of the current element + * @throws Exception A store error occurred + */ + public void printCloseTag(PrintWriter aWriter, StoreDescription aDesc) + throws Exception { + aWriter.print(""); + } + + /** + * Print only the open tag with all attributes. + * + * @param aWriter The output writer + * @param indent Indentation level + * @param bean The current bean that is stored + * @param aDesc Store description of the current element + * @throws Exception A store error occurred + */ + public void printOpenTag(PrintWriter aWriter, int indent, Object bean, + StoreDescription aDesc) throws Exception { + aWriter.print("<"); + aWriter.print(aDesc.getTag()); + if (aDesc.isAttributes() && bean != null) { + printAttributes(aWriter, indent, bean, aDesc); + } + aWriter.println(">"); + } + + /** + * Print tag with all attributes + * + * @param aWriter The output writer + * @param indent Indentation level + * @param bean The current bean that is stored + * @param aDesc Store description of the current element + * @throws Exception A store error occurred + */ + public void printTag(PrintWriter aWriter, int indent, Object bean, + StoreDescription aDesc) throws Exception { + aWriter.print("<"); + aWriter.print(aDesc.getTag()); + if (aDesc.isAttributes() && bean != null) { + printAttributes(aWriter, indent, bean, aDesc); + } + aWriter.println("/>"); + } + + /** + * Print the value from tag as content. + * + * @param aWriter The output writer + * @param tag The element name + * @param content Element content + * @throws Exception A store error occurred + */ + public void printTagContent(PrintWriter aWriter, String tag, String content) + throws Exception { + aWriter.print("<"); + aWriter.print(tag); + aWriter.print(">"); + aWriter.print(Escape.xml(content)); + aWriter.print(""); + } + + /** + * Print an array of values. + * + * @param aWriter The output writer + * @param tag The element name + * @param indent Indentation level + * @param elements Array of element values + */ + public void printTagValueArray(PrintWriter aWriter, String tag, int indent, + String[] elements) { + if (elements != null && elements.length > 0) { + printIndent(aWriter, indent + 2); + aWriter.print("<"); + aWriter.print(tag); + aWriter.print(">"); + for (int i = 0; i < elements.length; i++) { + printIndent(aWriter, indent + 4); + aWriter.print(elements[i]); + if (i + 1 < elements.length) { + aWriter.println(","); + } + } + printIndent(aWriter, indent + 2); + aWriter.print(""); + } + } + + /** + * Print an array of elements. + * + * @param aWriter The output writer + * @param tag The element name + * @param indent Indentation level + * @param elements Array of elements + * @throws Exception Store error occurred + */ + public void printTagArray(PrintWriter aWriter, String tag, int indent, + String[] elements) throws Exception { + if (elements != null) { + for (String element : elements) { + printIndent(aWriter, indent); + printTagContent(aWriter, tag, element); + } + } + } + + /** + * Print some spaces. + * + * @param aWriter The output writer + * @param indent The number of spaces + */ + public void printIndent(PrintWriter aWriter, int indent) { + for (int i = 0; i < indent; i++) { + aWriter.print(' '); + } + pos = indent; + } + + /** + * Store the relevant attributes of the specified JavaBean, plus a + * className attribute defining the fully qualified Java + * class name of the bean. + * + * @param writer PrintWriter to which we are storing + * @param indent Indentation level + * @param bean + * Bean whose properties are to be rendered as attributes, + * @param desc Store description of the current element + * + * @exception Exception + * if an exception occurs while storing + */ + public void printAttributes(PrintWriter writer, int indent, Object bean, + StoreDescription desc) throws Exception { + + printAttributes(writer, indent, true, bean, desc); + + } + + /** + * Store the relevant attributes of the specified JavaBean. + * + * @param writer PrintWriter to which we are storing + * @param indent Indentation level + * @param include + * Should we include a className attribute? + * @param bean + * Bean whose properties are to be rendered as attributes, + * @param desc + * RegistryDescriptor from this bean + * + * @exception Exception + * if an exception occurs while storing + */ + public void printAttributes(PrintWriter writer, int indent, + boolean include, Object bean, StoreDescription desc) + throws Exception { + + // Render a className attribute if requested + if (include && !desc.isStandard()) { + writer.print(" className=\""); + writer.print(bean.getClass().getName()); + writer.print("\""); + } + + // Acquire the list of properties for this bean + PropertyDescriptor descriptors[] = Introspector.getBeanInfo( + bean.getClass()).getPropertyDescriptors(); + if (descriptors == null) { + descriptors = new PropertyDescriptor[0]; + } + + // Create blank instance + Object bean2 = defaultInstance(bean); + for (PropertyDescriptor descriptor : descriptors) { + Object value = checkAttribute(desc, descriptor, descriptor.getName(), bean, bean2); + if (value != null) { + printAttribute(writer, indent, bean, desc, descriptor.getName(), bean2, value); + } + } + + if (bean instanceof ResourceBase) { + ResourceBase resource = (ResourceBase) bean; + for (Iterator iter = resource.listProperties(); iter.hasNext();) { + String name = iter.next(); + Object value = resource.getProperty(name); + if (!isPersistable(value.getClass())) { + continue; + } + if (desc.isTransientAttribute(name)) { + continue; // Skip the specified exceptions + } + printValue(writer, indent, name, value); + + } + } + } + + /** + * Check if the attribute should be printed. + * @param desc RegistryDescriptor from this bean + * @param descriptor PropertyDescriptor from this bean property + * @param attributeName The attribute name to store + * @param bean The current bean + * @param bean2 A default instance of the bean for comparison + * @return null if the value should be skipped, the value to print otherwise + */ + protected Object checkAttribute(StoreDescription desc, PropertyDescriptor descriptor, String attributeName, Object bean, Object bean2) { + if (descriptor instanceof IndexedPropertyDescriptor) { + return null; // Indexed properties are not persisted + } + if (!isPersistable(descriptor.getPropertyType()) + || (descriptor.getReadMethod() == null) + || (descriptor.getWriteMethod() == null)) { + return null; // Must be a read-write primitive or String + } + if (desc.isTransientAttribute(descriptor.getName())) { + return null; // Skip the specified exceptions + } + Object value = IntrospectionUtils.getProperty(bean, descriptor.getName()); + if (value == null) { + return null; // Null values are not persisted + } + Object value2 = IntrospectionUtils.getProperty(bean2, descriptor.getName()); + if (value.equals(value2)) { + // The property has its default value + return null; + } + return value; + } + + /** + * Store the specified of the specified JavaBean. + * + * @param writer PrintWriter to which we are storing + * @param indent Indentation level + * @param bean The current bean + * @param desc RegistryDescriptor from this bean + * @param attributeName The attribute name to store + * @param bean2 A default instance of the bean for comparison + * @param value The attribute value + */ + protected void printAttribute(PrintWriter writer, int indent, Object bean, StoreDescription desc, String attributeName, Object bean2, Object value) { + if (isPrintValue(bean, bean2, attributeName, desc)) { + printValue(writer, indent, attributeName, value); + } + } + + /** + * Determine if the attribute value needs to be stored. + * + * @param bean + * original bean + * @param bean2 + * default bean + * @param attrName + * attribute name + * @param desc + * StoreDescription from bean + * @return true if the value should be stored + */ + public boolean isPrintValue(Object bean, Object bean2, String attrName, + StoreDescription desc) { + return true; + } + + /** + * Generate default Instance for the specified bean. + * + * @param bean The bean + * @return an object from same class as bean parameter + * @throws ReflectiveOperationException Error creating a new instance + */ + public Object defaultInstance(Object bean) throws ReflectiveOperationException { + return bean.getClass().getConstructor().newInstance(); + } + + /** + * Print an attribute value. + * + * @param writer PrintWriter to which we are storing + * @param indent Indentation level + * @param name Attribute name + * @param value Attribute value + */ + public void printValue(PrintWriter writer, int indent, String name, + Object value) { + // Convert IP addresses to strings so they will be persisted + if (value instanceof InetAddress) { + value = ((InetAddress) value).getHostAddress(); + } + if (!(value instanceof String)) { + value = value.toString(); + } + String strValue = Escape.xml((String) value); + pos = pos + name.length() + strValue.length(); + if (pos > 60) { + writer.println(); + printIndent(writer, indent + 4); + } else { + writer.print(' '); + } + writer.print(name); + writer.print("=\""); + writer.print(strValue); + writer.print("\""); + } + + + /** + * Is the specified property type one for which we should generate a + * persistence attribute? + * + * @param clazz + * Java class to be tested + * @return true if the specified class should be stored + */ + protected boolean isPersistable(Class clazz) { + + for (Class persistable : persistables) { + if (persistable == clazz || persistable.isAssignableFrom(clazz)) { + return true; + } + } + return false; + + } +} diff --git a/java/org/apache/catalina/storeconfig/StoreConfig.java b/java/org/apache/catalina/storeconfig/StoreConfig.java new file mode 100644 index 0000000..fe7e728 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreConfig.java @@ -0,0 +1,336 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; +import java.net.URL; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.mbeans.MBeanUtils; +import org.apache.catalina.startup.Bootstrap; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Store Server/Service/Host/Context at file or PrintWriter. Default server.xml + * is at $catalina.base/conf/server.xml + */ +public class StoreConfig implements IStoreConfig { + private static Log log = LogFactory.getLog(StoreConfig.class); + protected static final StringManager sm = StringManager + .getManager(Constants.Package); + + private String serverFilename = "conf/server.xml"; + + private StoreRegistry registry; + + private Server server; + + /** + * Get server.xml location + * + * @return The server file name + */ + public String getServerFilename() { + return serverFilename; + } + + /** + * Set new server.xml location. + * + * @param string The server.xml location + */ + public void setServerFilename(String string) { + serverFilename = string; + } + + /** + * Get the StoreRegistry with all factory to generate the + * server.xml/context.xml files. + * + * @see org.apache.catalina.storeconfig.IStoreConfig#getRegistry() + */ + @Override + public StoreRegistry getRegistry() { + return registry; + } + + @Override + public void setServer(Server aServer) { + server = aServer; + } + + @Override + public Server getServer() { + return server; + } + + /** + * set StoreRegistry + * + * @see org.apache.catalina.storeconfig.IStoreConfig#setRegistry(org.apache.catalina.storeconfig.StoreRegistry) + */ + @Override + public void setRegistry(StoreRegistry aRegistry) { + registry = aRegistry; + } + + /** + * Store current Server. + */ + @Override + public void storeConfig() { + store(server); + } + + /** + * Store Server from Object Name (Catalina:type=Server). + * + * @param aServerName Server ObjectName + * @param backup true to backup existing configuration files + * before rewriting them + * @param externalAllowed true to allow saving webapp + * configuration for webapps that are not inside the host's app + * directory + * @throws MalformedObjectNameException Bad MBean name + */ + public synchronized void storeServer(String aServerName, boolean backup, + boolean externalAllowed) throws MalformedObjectNameException { + if (aServerName == null || aServerName.length() == 0) { + log.error(sm.getString("config.emptyObjectName")); + return; + } + MBeanServer mserver = MBeanUtils.createServer(); + ObjectName objectName = new ObjectName(aServerName); + if (mserver.isRegistered(objectName)) { + try { + Server aServer = (Server) mserver.getAttribute(objectName, + "managedResource"); + StoreDescription desc = null; + desc = getRegistry().findDescription(StandardContext.class); + if (desc != null) { + boolean oldSeparate = desc.isStoreSeparate(); + boolean oldBackup = desc.isBackup(); + boolean oldExternalAllowed = desc.isExternalAllowed(); + try { + desc.setStoreSeparate(true); + desc.setBackup(backup); + desc.setExternalAllowed(externalAllowed); + store(aServer); + } finally { + desc.setStoreSeparate(oldSeparate); + desc.setBackup(oldBackup); + desc.setExternalAllowed(oldExternalAllowed); + } + } else { + store(aServer); + } + } catch (Exception e) { + log.error(sm.getString("config.storeServerError"), e); + } + } else { + log.info(sm.getString("config.objectNameNotFound", aServerName)); + } + } + + /** + * Store a Context from ObjectName. + * + * @param aContextName MBean ObjectName + * @param backup true to backup existing configuration files + * before rewriting them + * @param externalAllowed true to allow saving webapp + * configuration for webapps that are not inside the host's app + * directory + * @throws MalformedObjectNameException Bad MBean name + */ + public synchronized void storeContext(String aContextName, boolean backup, + boolean externalAllowed) throws MalformedObjectNameException { + if (aContextName == null || aContextName.length() == 0) { + log.error(sm.getString("config.emptyObjectName")); + return; + } + MBeanServer mserver = MBeanUtils.createServer(); + ObjectName objectName = new ObjectName(aContextName); + if (mserver.isRegistered(objectName)) { + try { + Context aContext = (Context) mserver.getAttribute(objectName, + "managedResource"); + URL configFile = aContext.getConfigFile(); + if (configFile != null) { + StoreDescription desc = null; + desc = getRegistry().findDescription( + aContext.getClass()); + if (desc != null) { + boolean oldSeparate = desc.isStoreSeparate(); + boolean oldBackup = desc.isBackup(); + boolean oldExternalAllowed = desc + .isExternalAllowed(); + try { + desc.setStoreSeparate(true); + desc.setBackup(backup); + desc.setExternalAllowed(externalAllowed); + desc.getStoreFactory() + .store(null, -2, aContext); + } finally { + desc.setStoreSeparate(oldSeparate); + desc.setBackup(oldBackup); + desc.setBackup(oldExternalAllowed); + } + } + } else { + log.error(sm.getString("config.missingContextFile", aContext.getPath())); + } + } catch (Exception e) { + log.error(sm.getString("config.storeContextError", aContextName), e); + } + } else { + log.info(sm.getString("config.objectNameNotFound", aContextName)); + } + } + + /** + * Write the configuration information for this entire Server + * out to the server.xml configuration file. + * + * @param aServer Server instance + * @return true if the store operation was successful + */ + @Override + public synchronized boolean store(Server aServer) { + StoreFileMover mover = new StoreFileMover(Bootstrap.getCatalinaBase(), + getServerFilename(), getRegistry().getEncoding()); + // Open an output writer for the new configuration file + try { + try (PrintWriter writer = mover.getWriter()) { + store(writer, -2, aServer); + } + mover.move(); + return true; + } catch (Exception e) { + log.error(sm.getString("config.storeServerError"), e); + } + return false; + } + + /** + * @see org.apache.catalina.storeconfig.IStoreConfig#store(org.apache.catalina.Context) + */ + @Override + public synchronized boolean store(Context aContext) { + try { + StoreDescription desc = null; + desc = getRegistry().findDescription(aContext.getClass()); + if (desc != null) { + boolean old = desc.isStoreSeparate(); + try { + desc.setStoreSeparate(true); + desc.getStoreFactory().store(null, -2, aContext); + } finally { + desc.setStoreSeparate(old); + } + } + return true; + } catch (Exception e) { + log.error(sm.getString("config.storeContextError", aContext.getName()), e); + } + return false; + } + + /** + * @see org.apache.catalina.storeconfig.IStoreConfig#store(java.io.PrintWriter, + * int, org.apache.catalina.Context) + */ + @Override + public void store(PrintWriter aWriter, int indent, + Context aContext) throws Exception { + boolean oldSeparate = true; + StoreDescription desc = null; + try { + desc = getRegistry().findDescription(aContext.getClass()); + if (desc != null) { + oldSeparate = desc.isStoreSeparate(); + desc.setStoreSeparate(false); + desc.getStoreFactory().store(aWriter, indent, aContext); + } + } finally { + if (desc != null) { + desc.setStoreSeparate(oldSeparate); + } else { + log.warn(sm.getString("factory.storeNoDescriptor", aContext.getClass())); + } + } + } + + /** + * @see org.apache.catalina.storeconfig.IStoreConfig#store(java.io.PrintWriter, + * int, org.apache.catalina.Host) + */ + @Override + public void store(PrintWriter aWriter, int indent, Host aHost) + throws Exception { + StoreDescription desc = getRegistry().findDescription( + aHost.getClass()); + if (desc != null) { + desc.getStoreFactory().store(aWriter, indent, aHost); + } else { + log.warn(sm.getString("factory.storeNoDescriptor", aHost.getClass())); + } + } + + /** + * @see org.apache.catalina.storeconfig.IStoreConfig#store(java.io.PrintWriter, + * int, org.apache.catalina.Service) + */ + @Override + public void store(PrintWriter aWriter, int indent, + Service aService) throws Exception { + StoreDescription desc = getRegistry().findDescription( + aService.getClass()); + if (desc != null) { + desc.getStoreFactory().store(aWriter, indent, aService); + } else { + log.warn(sm.getString("factory.storeNoDescriptor", aService.getClass())); + } + } + + /** + * @see org.apache.catalina.storeconfig.IStoreConfig#store(java.io.PrintWriter, + * int, org.apache.catalina.Server) + */ + @Override + public void store(PrintWriter writer, int indent, + Server aServer) throws Exception { + StoreDescription desc = getRegistry().findDescription( + aServer.getClass()); + if (desc != null) { + desc.getStoreFactory().store(writer, indent, aServer); + } else { + log.warn(sm.getString("factory.storeNoDescriptor", aServer.getClass())); + } + } + +} diff --git a/java/org/apache/catalina/storeconfig/StoreConfigLifecycleListener.java b/java/org/apache/catalina/storeconfig/StoreConfigLifecycleListener.java new file mode 100644 index 0000000..a55c8c9 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreConfigLifecycleListener.java @@ -0,0 +1,163 @@ +/* + * 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.catalina.storeconfig; + +import javax.management.DynamicMBean; +import javax.management.ObjectName; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Server; +import org.apache.catalina.mbeans.MBeanUtils; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + * Loads and registers a StoreConfig MBean with the name + * Catalina:type=StoreConfig. + *

    + * This listener must only be nested within {@link Server} elements. + */ +public class StoreConfigLifecycleListener implements LifecycleListener { + + private static Log log = LogFactory.getLog(StoreConfigLifecycleListener.class); + private static StringManager sm = StringManager.getManager(StoreConfigLifecycleListener.class); + + /** + * The configuration information registry for our managed beans. + */ + protected final Registry registry = MBeanUtils.createRegistry(); + + + IStoreConfig storeConfig; + + private String storeConfigClass = "org.apache.catalina.storeconfig.StoreConfig"; + + private String storeRegistry = null; + private ObjectName oname = null; + + /** + * Register StoreRegistry after Start the complete Server. + * + * @see org.apache.catalina.LifecycleListener#lifecycleEvent(org.apache.catalina.LifecycleEvent) + */ + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) { + if (event.getSource() instanceof Server) { + createMBean((Server) event.getSource()); + } else { + log.warn(sm.getString("storeConfigListener.notServer", + event.getLifecycle().getClass().getSimpleName())); + } + } else if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType())) { + if (oname != null) { + registry.unregisterComponent(oname); + oname = null; + } + } + } + + /** + * Create StoreConfig MBean and load StoreRegistry MBeans name is + * Catalina:type=StoreConfig. + * @param server The Server instance + */ + protected void createMBean(Server server) { + StoreLoader loader = new StoreLoader(); + try { + Class clazz = Class.forName(getStoreConfigClass(), true, this + .getClass().getClassLoader()); + storeConfig = (IStoreConfig) clazz.getConstructor().newInstance(); + loader.load(getStoreRegistry()); + // use the loader Registry + storeConfig.setRegistry(loader.getRegistry()); + storeConfig.setServer(server); + } catch (Exception e) { + log.error(sm.getString("storeConfigListener.loadError"), e); + return; + } + try { + // Note: Hard-coded domain used since this object is per Server/JVM + oname = new ObjectName("Catalina:type=StoreConfig" ); + registry.registerComponent(storeConfig, oname, "StoreConfig"); + } catch (Exception ex) { + log.error(sm.getString("storeConfigListener.registerError"), ex); + } + } + + /** + * Create a ManagedBean (StoreConfig). + * + * @param object The object to manage + * @return an MBean wrapping the object + * @throws Exception if an error occurred + */ + protected DynamicMBean getManagedBean(Object object) throws Exception { + ManagedBean managedBean = registry.findManagedBean("StoreConfig"); + return managedBean.createMBean(object); + } + + /** + * @return the store config instance + */ + public IStoreConfig getStoreConfig() { + return storeConfig; + } + + /** + * @param storeConfig + * The storeConfig to set. + */ + public void setStoreConfig(IStoreConfig storeConfig) { + this.storeConfig = storeConfig; + } + + /** + * @return the main store config class name + */ + public String getStoreConfigClass() { + return storeConfigClass; + } + + /** + * @param storeConfigClass + * The storeConfigClass to set. + */ + public void setStoreConfigClass(String storeConfigClass) { + this.storeConfigClass = storeConfigClass; + } + + /** + * @return the store registry + */ + public String getStoreRegistry() { + return storeRegistry; + } + + /** + * @param storeRegistry + * The storeRegistry to set. + */ + public void setStoreRegistry(String storeRegistry) { + this.storeRegistry = storeRegistry; + } +} diff --git a/java/org/apache/catalina/storeconfig/StoreContextAppender.java b/java/org/apache/catalina/storeconfig/StoreContextAppender.java new file mode 100644 index 0000000..31b0983 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreContextAppender.java @@ -0,0 +1,182 @@ +/* + * 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.catalina.storeconfig; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +import org.apache.catalina.Container; +import org.apache.catalina.Globals; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; + +/** + * store StandardContext Attributes ... + */ +public class StoreContextAppender extends StoreAppender { + + /** + * {@inheritDoc} + * Adds special handling for docBase. + */ + @Override + protected void printAttribute(PrintWriter writer, int indent, Object bean, StoreDescription desc, String attributeName, Object bean2, Object value) { + if (isPrintValue(bean, bean2, attributeName, desc)) { + if(attributeName.equals("docBase")) { + if(bean instanceof StandardContext) { + String docBase = ((StandardContext)bean).getOriginalDocBase() ; + if(docBase != null) { + value = docBase ; + } + } + } + printValue(writer, indent, attributeName, value); + } + } + + /** + * Print Context Values.

    • Special handling to default workDir. + *
    • Don't save path at external context.xml
    • Don't + * generate docBase for host.appBase webapps
    + * + * @see org.apache.catalina.storeconfig.StoreAppender#isPrintValue(Object, + * Object, String, StoreDescription) + */ + @Override + public boolean isPrintValue(Object bean, Object bean2, String attrName, + StoreDescription desc) { + boolean isPrint = super.isPrintValue(bean, bean2, attrName, desc); + if (isPrint) { + StandardContext context = ((StandardContext) bean); + if ("workDir".equals(attrName)) { + String defaultWorkDir = getDefaultWorkDir(context); + if (defaultWorkDir != null) { + isPrint = !defaultWorkDir.equals(context.getWorkDir()); + } + } else if ("path".equals(attrName)) { + isPrint = desc.isStoreSeparate() + && desc.isExternalAllowed() + && context.getConfigFile() == null; + } else if ("docBase".equals(attrName)) { + Container host = context.getParent(); + if (host instanceof StandardHost) { + File appBase = getAppBase(((StandardHost) host)); + File docBase = getDocBase(context,appBase); + isPrint = !appBase.equals(docBase.getParentFile()); + } + } + } + return isPrint; + } + + protected File getAppBase(StandardHost host) { + + File appBase; + File file = new File(host.getAppBase()); + if (!file.isAbsolute()) { + file = new File(System.getProperty(Globals.CATALINA_BASE_PROP), host + .getAppBase()); + } + try { + appBase = file.getCanonicalFile(); + } catch (IOException e) { + appBase = file; + } + return appBase; + + } + + protected File getDocBase(StandardContext context, File appBase) { + File docBase; + String contextDocBase = context.getOriginalDocBase() ; + if(contextDocBase == null) { + contextDocBase = context.getDocBase() ; + } + File file = new File(contextDocBase); + if (!file.isAbsolute()) { + file = new File(appBase, contextDocBase); + } + try { + docBase = file.getCanonicalFile(); + } catch (IOException e) { + docBase = file; + } + return docBase; + } + + /** + * Make default Work Dir. + * + * @param context The context + * @return The default working directory for the context. + */ + protected String getDefaultWorkDir(StandardContext context) { + String defaultWorkDir = null; + String contextWorkDir = context.getName(); + if (contextWorkDir.length() == 0) { + contextWorkDir = "_"; + } + if (contextWorkDir.startsWith("/")) { + contextWorkDir = contextWorkDir.substring(1); + } + + Container host = context.getParent(); + if (host instanceof StandardHost) { + String hostWorkDir = ((StandardHost) host).getWorkDir(); + if (hostWorkDir != null) { + defaultWorkDir = hostWorkDir + File.separator + contextWorkDir; + } else { + String engineName = context.getParent().getParent().getName(); + String hostName = context.getParent().getName(); + defaultWorkDir = "work" + File.separator + engineName + + File.separator + hostName + File.separator + + contextWorkDir; + } + } + return defaultWorkDir; + } + + /** + * Generate a real default StandardContext TODO read and interpret the + * default context.xml and context.xml.default TODO Cache a Default + * StandardContext ( with reloading strategy) TODO remove really all + * elements, but detection is hard... To Listener or Valve from same class? + * + * @see org.apache.catalina.storeconfig.StoreAppender#defaultInstance(java.lang.Object) + */ + @Override + public Object defaultInstance(Object bean) throws ReflectiveOperationException { + if (bean instanceof StandardContext) { + StandardContext defaultContext = new StandardContext(); + /* + * if (!((StandardContext) bean).getOverride()) { + * defaultContext.setParent(((StandardContext)bean).getParent()); + * ContextConfig contextConfig = new ContextConfig(); + * defaultContext.addLifecycleListener(contextConfig); + * contextConfig.setContext(defaultContext); + * contextConfig.processContextConfig(new File(contextConfig + * .getBaseDir(), "conf/context.xml")); + * contextConfig.processContextConfig(new File(contextConfig + * .getConfigBase(), "context.xml.default")); } + */ + return defaultContext; + } else { + return super.defaultInstance(bean); + } + } +} diff --git a/java/org/apache/catalina/storeconfig/StoreDescription.java b/java/org/apache/catalina/storeconfig/StoreDescription.java new file mode 100644 index 0000000..d308a59 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreDescription.java @@ -0,0 +1,377 @@ +/* + * 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.catalina.storeconfig; + +import java.util.ArrayList; +import java.util.List; + +/** + * Bean of a StoreDescription + * + *
    + *
    + *  <Description
    + *  tag="Context"
    + *  standard="true"
    + *  default="true"
    + *  externalAllowed="true"
    + *  storeSeparate="true"
    + *  backup="true"
    + *  children="true"
    + *  tagClass="org.apache.catalina.core.StandardContext"
    + *  storeFactoryClass="org.apache.catalina.storeconfig.StandardContextSF"
    + *  storeAppenderClass="org.apache.catalina.storeconfig.StoreContextAppender">
    + *     <TransientAttribute>available</TransientAttribute>
    + *     <TransientAttribute>configFile</TransientAttribute>
    + *     <TransientAttribute>configured</TransientAttribute>
    + *     <TransientAttribute>displayName</TransientAttribute>
    + *     <TransientAttribute>distributable</TransientAttribute>
    + *     <TransientAttribute>domain</TransientAttribute>
    + *     <TransientAttribute>engineName</TransientAttribute>
    + *     <TransientAttribute>name</TransientAttribute>
    + *     <TransientAttribute>publicId</TransientAttribute>
    + *     <TransientAttribute>replaceWelcomeFiles</TransientAttribute>
    + *     <TransientAttribute>saveConfig</TransientAttribute>
    + *     <TransientAttribute>sessionTimeout</TransientAttribute>
    + *     <TransientAttribute>startupTime</TransientAttribute>
    + *     <TransientAttribute>tldScanTime</TransientAttribute>
    + *  </Description>
    + *
    + *
    + * 
    + */ +public class StoreDescription { + + private String id; + + private String tag; + + private String tagClass; + + private boolean standard = false; + + private boolean backup = false; + + private boolean externalAllowed = false; + + private boolean externalOnly = false; + + private boolean myDefault = false; + + private boolean attributes = true; + + private String storeFactoryClass; + + private IStoreFactory storeFactory; + + private String storeWriterClass; + + private boolean children = false; + + private List transientAttributes; + + private List transientChildren; + + private boolean storeSeparate = false; + + /** + * @return Returns the external. + */ + public boolean isExternalAllowed() { + return externalAllowed; + } + + /** + * @param external + * The external to set. + */ + public void setExternalAllowed(boolean external) { + this.externalAllowed = external; + } + + public boolean isExternalOnly() { + return externalOnly; + } + + public void setExternalOnly(boolean external) { + this.externalOnly = external; + } + + /** + * @return Returns the standard. + */ + public boolean isStandard() { + return standard; + } + + /** + * @param standard + * The standard to set. + */ + public void setStandard(boolean standard) { + this.standard = standard; + } + + /** + * @return Returns the backup. + */ + public boolean isBackup() { + return backup; + } + + /** + * @param backup + * The backup to set. + */ + public void setBackup(boolean backup) { + this.backup = backup; + } + + /** + * @return Returns the myDefault. + */ + public boolean isDefault() { + return myDefault; + } + + /** + * @param aDefault + * The myDefault to set. + */ + public void setDefault(boolean aDefault) { + this.myDefault = aDefault; + } + + /** + * @return Returns the storeFactory. + */ + public String getStoreFactoryClass() { + return storeFactoryClass; + } + + /** + * @param storeFactoryClass + * The storeFactory to set. + */ + public void setStoreFactoryClass(String storeFactoryClass) { + this.storeFactoryClass = storeFactoryClass; + } + + /** + * @return Returns the storeFactory. + */ + public IStoreFactory getStoreFactory() { + return storeFactory; + } + + /** + * @param storeFactory + * The storeFactory to set. + */ + public void setStoreFactory(IStoreFactory storeFactory) { + this.storeFactory = storeFactory; + } + + /** + * @return Returns the storeWriterClass. + */ + public String getStoreWriterClass() { + return storeWriterClass; + } + + /** + * @param storeWriterClass + * The storeWriterClass to set. + */ + public void setStoreWriterClass(String storeWriterClass) { + this.storeWriterClass = storeWriterClass; + } + + /** + * @return Returns the tagClass. + */ + public String getTag() { + return tag; + } + + /** + * @param tag + * The tag to set. + */ + public void setTag(String tag) { + this.tag = tag; + } + + /** + * @return Returns the tagClass. + */ + public String getTagClass() { + return tagClass; + } + + /** + * @param tagClass + * The tagClass to set. + */ + public void setTagClass(String tagClass) { + this.tagClass = tagClass; + } + + /** + * @return Returns the transientAttributes. + */ + public List getTransientAttributes() { + return transientAttributes; + } + + /** + * @param transientAttributes + * The transientAttributes to set. + */ + public void setTransientAttributes(List transientAttributes) { + this.transientAttributes = transientAttributes; + } + + public void addTransientAttribute(String attribute) { + if (transientAttributes == null) { + transientAttributes = new ArrayList<>(); + } + transientAttributes.add(attribute); + } + + public void removeTransientAttribute(String attribute) { + if (transientAttributes != null) { + transientAttributes.remove(attribute); + } + } + + /** + * @return Returns the transientChildren. + */ + public List getTransientChildren() { + return transientChildren; + } + + /** + * @param transientChildren + * The transientChildren to set. + */ + public void setTransientChildren(List transientChildren) { + this.transientChildren = transientChildren; + } + + public void addTransientChild(String classname) { + if (transientChildren == null) { + transientChildren = new ArrayList<>(); + } + transientChildren.add(classname); + } + + public void removeTransientChild(String classname) { + if (transientChildren != null) { + transientChildren.remove(classname); + } + } + + /** + * Is child transient, please don't save this. + * + * @param classname The class name to check + * @return is classname attribute? + */ + public boolean isTransientChild(String classname) { + if (transientChildren != null) { + return transientChildren.contains(classname); + } + return false; + } + + /** + * Is attribute transient, please don't save this. + * + * @param attribute The attribute name to check + * @return is transient attribute? + */ + public boolean isTransientAttribute(String attribute) { + if (transientAttributes != null) { + return transientAttributes.contains(attribute); + } + return false; + } + + /** + * Return the real id or TagClass + * + * @return Returns the id. + */ + public String getId() { + if (id != null) { + return id; + } else { + return getTagClass(); + } + } + + /** + * @param id + * The id to set. + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return Returns the attributes. + */ + public boolean isAttributes() { + return attributes; + } + + /** + * @param attributes + * The attributes to set. + */ + public void setAttributes(boolean attributes) { + this.attributes = attributes; + } + + /** + * @return True if it's a separate store + */ + public boolean isStoreSeparate() { + return storeSeparate; + } + + public void setStoreSeparate(boolean storeSeparate) { + this.storeSeparate = storeSeparate; + } + + /** + * @return Returns the children. + */ + public boolean isChildren() { + return children; + } + + /** + * @param children + * The children to set. + */ + public void setChildren(boolean children) { + this.children = children; + } +} diff --git a/java/org/apache/catalina/storeconfig/StoreFactoryBase.java b/java/org/apache/catalina/storeconfig/StoreFactoryBase.java new file mode 100644 index 0000000..1809117 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreFactoryBase.java @@ -0,0 +1,205 @@ +/* + * 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.catalina.storeconfig; + +import java.io.IOException; +import java.io.PrintWriter; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * StoreFactory saves special elements. + * Output was generate with StoreAppenders. + */ +public class StoreFactoryBase implements IStoreFactory { + private static Log log = LogFactory.getLog(StoreFactoryBase.class); + + private StoreRegistry registry; + + private StoreAppender storeAppender = new StoreAppender(); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager + .getManager(Constants.Package); + + /** + * The descriptive information string for this implementation. + */ + private static final String info = "org.apache.catalina.config.StoreFactoryBase/1.0"; + + /** + * @return descriptive information about this Factory implementation and the + * corresponding version number, in the format + * <description>/<version>. + */ + public String getInfo() { + return info; + } + + /** + * @return Returns the storeAppender. + */ + @Override + public StoreAppender getStoreAppender() { + return storeAppender; + } + + /** + * @param storeAppender + * The storeAppender to set. + */ + @Override + public void setStoreAppender(StoreAppender storeAppender) { + this.storeAppender = storeAppender; + } + + /** + * Set Registry + * + * @see org.apache.catalina.storeconfig.IStoreFactory#setRegistry(org.apache.catalina.storeconfig.StoreRegistry) + */ + @Override + public void setRegistry(StoreRegistry aRegistry) { + registry = aRegistry; + + } + + /** + * get Registry + * + * @see org.apache.catalina.storeconfig.IStoreFactory#getRegistry() + */ + @Override + public StoreRegistry getRegistry() { + + return registry; + } + + @Override + public void storeXMLHead(PrintWriter aWriter) { + // Store the beginning of this element + aWriter.print(""); + } + + /** + * Store a server.xml element with attributes and children + * + * @see org.apache.catalina.storeconfig.IStoreFactory#store(java.io.PrintWriter, + * int, java.lang.Object) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass()); + + if (elementDesc != null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("factory.storeTag", + elementDesc.getTag(), aElement)); + } + getStoreAppender().printIndent(aWriter, indent + 2); + if (!elementDesc.isChildren()) { + getStoreAppender().printTag(aWriter, indent, aElement, + elementDesc); + } else { + getStoreAppender().printOpenTag(aWriter, indent + 2, aElement, + elementDesc); + storeChildren(aWriter, indent + 2, aElement, elementDesc); + getStoreAppender().printIndent(aWriter, indent + 2); + getStoreAppender().printCloseTag(aWriter, elementDesc); + } + } else { + log.warn(sm.getString("factory.storeNoDescriptor", aElement + .getClass())); + } + } + + /** + * Must Implement at subclass for custom store children handling. + * + * @param aWriter Current output writer + * @param indent Indentation level + * @param aElement Current element + * @param elementDesc The element description + * @throws Exception Configuration storing error + */ + public void storeChildren(PrintWriter aWriter, int indent, Object aElement, + StoreDescription elementDesc) throws Exception { + } + + /** + * Store only elements from storeChildren methods that are not a transient + * child. + * + * @param aWriter Current output writer + * @param indent Indentation level + * @param aTagElement Current element + * @throws Exception Configuration storing error + */ + protected void storeElement(PrintWriter aWriter, int indent, + Object aTagElement) throws Exception { + if (aTagElement != null) { + IStoreFactory elementFactory = getRegistry().findStoreFactory( + aTagElement.getClass()); + + if (elementFactory != null) { + StoreDescription desc = getRegistry().findDescription( + aTagElement.getClass()); + if (desc != null) { + if (!desc.isTransientChild(aTagElement.getClass().getName())) { + elementFactory.store(aWriter, indent, aTagElement); + } + } else { + log.warn(sm.getString("factory.storeNoDescriptor", aTagElement + .getClass())); + } + } else { + log.warn(sm.getString("factory.storeNoDescriptor", aTagElement + .getClass())); + } + } + } + + /** + * Save an array of elements. + * @param aWriter Current output writer + * @param indent Indentation level + * @param elements Array of elements + * @throws Exception Configuration storing error + */ + protected void storeElementArray(PrintWriter aWriter, int indent, + Object[] elements) throws Exception { + if (elements != null) { + for (Object element : elements) { + try { + storeElement(aWriter, indent, element); + } catch (IOException ioe) { + // ignore children report error them self! + // see StandardContext.storeWithBackup() + } + } + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/StoreFactoryRule.java b/java/org/apache/catalina/storeconfig/StoreFactoryRule.java new file mode 100644 index 0000000..f90091d --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreFactoryRule.java @@ -0,0 +1,123 @@ +/* + * 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.catalina.storeconfig; + +import org.apache.tomcat.util.digester.Rule; +import org.xml.sax.Attributes; + +/** + *

    + * Rule that creates a new IStoreFactory instance, and associates + * it with the top object on the stack (which must implement + * IStoreFactory). + *

    + */ + +public class StoreFactoryRule extends Rule { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new instance of this Rule. + * + * @param storeFactoryClass + * Default name of the StoreFactory implementation class to be + * created + * @param attributeName + * Name of the attribute that optionally includes an override + * name of the IStoreFactory class + * @param storeAppenderClass The store appender class + * @param appenderAttributeName The attribute name for the store + * appender class + */ + public StoreFactoryRule(String storeFactoryClass, String attributeName, + String storeAppenderClass, String appenderAttributeName) { + + this.storeFactoryClass = storeFactoryClass; + this.attributeName = attributeName; + this.appenderAttributeName = appenderAttributeName; + this.storeAppenderClass = storeAppenderClass; + + } + + // ----------------------------------------------------- Instance Variables + + /** + * The attribute name of an attribute that can override the implementation + * class name. + */ + private String attributeName; + + private String appenderAttributeName; + + /** + * The name of the IStoreFactory implementation class. + */ + private String storeFactoryClass; + + private String storeAppenderClass; + + // --------------------------------------------------------- Public Methods + + /** + * Handle the beginning of an XML element. + * + * @param namespace XML namespace + * @param name The element name + * @param attributes The attributes of this element + * @exception Exception if a processing error occurs + */ + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + + IStoreFactory factory = (IStoreFactory) newInstance(attributeName, + storeFactoryClass, attributes); + StoreAppender storeAppender = (StoreAppender) newInstance( + appenderAttributeName, storeAppenderClass, attributes); + factory.setStoreAppender(storeAppender); + + // Add this StoreFactory to our associated component + StoreDescription desc = (StoreDescription) digester.peek(0); + StoreRegistry registry = (StoreRegistry) digester.peek(1); + factory.setRegistry(registry); + desc.setStoreFactory(factory); + + } + + /** + * Create new instance from attribute className! + * + * @param attr class Name attribute + * @param defaultName Default Class + * @param attributes current digester attribute elements + * @return new configured object instance + * @throws ReflectiveOperationException Error creating an instance + */ + protected Object newInstance(String attr, String defaultName, + Attributes attributes) throws ReflectiveOperationException { + String className = defaultName; + if (attr != null) { + String value = attributes.getValue(attr); + if (value != null) { + className = value; + } + } + Class clazz = Class.forName(className); + return clazz.getConstructor().newInstance(); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/StoreFileMover.java b/java/org/apache/catalina/storeconfig/StoreFileMover.java new file mode 100644 index 0000000..9d68e1c --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreFileMover.java @@ -0,0 +1,215 @@ +/* + * 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.catalina.storeconfig; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.sql.Timestamp; + +import org.apache.catalina.Globals; +import org.apache.tomcat.util.res.StringManager; + +/** + * Move server.xml or context.xml as backup + * + * TODO Get Encoding from Registry + */ +public class StoreFileMover { + + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + private String filename = "conf/server.xml"; + + private String encoding = "UTF-8"; + + private String basename = System.getProperty(Globals.CATALINA_BASE_PROP); + + private File configOld; + + private File configNew; + + private File configSave; + + /** + * @return Returns the configNew. + */ + public File getConfigNew() { + return configNew; + } + + /** + * @return Returns the configOld. + */ + public File getConfigOld() { + return configOld; + } + + /** + * @return Returns the configSave. + */ + public File getConfigSave() { + return configSave; + } + + /** + * @return Returns the basename. + */ + public String getBasename() { + return basename; + } + + /** + * @param basename + * The basename to set. + */ + public void setBasename(String basename) { + this.basename = basename; + } + + /** + * @return The file name + */ + public String getFilename() { + return filename; + } + + /** + * @param string The file name + */ + public void setFilename(String string) { + filename = string; + } + + /** + * @return The encoding + */ + public String getEncoding() { + return encoding; + } + + /** + * @param string The encoding + */ + public void setEncoding(String string) { + encoding = string; + } + + /** + * Calculate file objects for the old and new configuration files. + * @param basename The base path + * @param encoding The encoding of the file + * @param filename The file name + */ + public StoreFileMover(String basename, String filename, String encoding) { + setBasename(basename); + setEncoding(encoding); + setFilename(filename); + init(); + } + + /** + * Calculate file objects for the old and new configuration files. + */ + public StoreFileMover() { + init(); + } + + /** + * Generate the Filename to new with TimeStamp. + */ + public void init() { + String configFile = getFilename(); + configOld = new File(configFile); + if (!configOld.isAbsolute()) { + configOld = new File(getBasename(), configFile); + } + configNew = new File(configFile + ".new"); + if (!configNew.isAbsolute()) { + configNew = new File(getBasename(), configFile + ".new"); + } + if (!configNew.getParentFile().exists()) { + if (!configNew.getParentFile().mkdirs()) { + throw new IllegalStateException(sm.getString("storeFileMover.directoryCreationError", configNew)); + } + } + String sb = getTimeTag(); + configSave = new File(configFile + sb); + if (!configSave.isAbsolute()) { + configSave = new File(getBasename(), configFile + sb); + } + } + + /** + * Shuffle old->save and new->old. + * + * @throws IOException a file operation error occurred + */ + public void move() throws IOException { + if (configOld.renameTo(configSave)) { + if (!configNew.renameTo(configOld)) { + configSave.renameTo(configOld); + throw new IOException(sm.getString("storeFileMover.renameError", + configNew.getAbsolutePath(), configOld.getAbsolutePath())); + } + } else { + if (!configOld.exists()) { + if (!configNew.renameTo(configOld)) { + throw new IOException(sm.getString("storeFileMover.renameError", + configNew.getAbsolutePath(), configOld.getAbsolutePath())); + } + } else { + throw new IOException(sm.getString("storeFileMover.renameError", + configOld.getAbsolutePath(), configSave.getAbsolutePath())); + } + } + } + + /** + * Open an output writer for the new configuration file. + * + * @return The writer + * @throws IOException Failed opening a writer to the new file + */ + public PrintWriter getWriter() throws IOException { + return new PrintWriter(new OutputStreamWriter( + new FileOutputStream(configNew), getEncoding())); + } + + /** + * Time value for backup yyyy-mm-dd.hh-mm-ss. + * + * @return The time + */ + protected String getTimeTag() { + String ts = (new Timestamp(System.currentTimeMillis())).toString(); + // yyyy-mm-dd hh:mm:ss + // 0123456789012345678 + StringBuilder sb = new StringBuilder("."); + sb.append(ts, 0, 10); + sb.append('.'); + sb.append(ts, 11, 13); + sb.append('-'); + sb.append(ts, 14, 16); + sb.append('-'); + sb.append(ts, 17, 19); + return sb.toString(); + } + +} diff --git a/java/org/apache/catalina/storeconfig/StoreLoader.java b/java/org/apache/catalina/storeconfig/StoreLoader.java new file mode 100644 index 0000000..bbbfdfc --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreLoader.java @@ -0,0 +1,172 @@ +/* + * 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.catalina.storeconfig; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource.Resource; + +/** + * XML Format + * + *
    + * {@code
    + *       
    + *         
    + *           
    + *             
    + *           
    + *           
    + *             
    + *           
    + *         
    + *   ...
    + *       
    + * }
    + * 
    + * + * + * Convention: + *
      + *
    • Factories at subpackage org.apache.catalina.core.storeconfig.xxxSF + * .
    • + *
    • Element name are the unique Class name
    • + *
    • SF for StoreFactory
    • + *
    • standard implementation is false
    • + *
    + * other things: + *
      + *
    • Registry XML format is a very good option
    • + *
    • Store format is not fix
    • + *
    • We hope with the parent declaration we can build recursive child store + * operation //dream
    • + *
    • Problem is to access child data from array,collections or normal detail + * object
    • + *
    • Default definitions for Listener, Valve Resource? - Based on interface + * type!
    • + *
    + */ +public class StoreLoader { + + /** + * The Digester instance used to parse registry descriptors. + */ + protected static final Digester digester = createDigester(); + + private StoreRegistry registry; + + private URL registryResource ; + + /** + * @return Returns the registry. + */ + public StoreRegistry getRegistry() { + return registry; + } + + /** + * @param registry + * The registry to set. + */ + public void setRegistry(StoreRegistry registry) { + this.registry = registry; + } + + /** + * Create and configure the Digester we will be using for setup store + * registry. + * @return the XML digester that will be used to parse the configuration + */ + protected static Digester createDigester() { + // Initialize the digester + Digester digester = new Digester(); + digester.setValidating(false); + digester.setClassLoader(StoreRegistry.class.getClassLoader()); + + // Configure the actions we will be using + digester.addObjectCreate("Registry", + "org.apache.catalina.storeconfig.StoreRegistry", "className"); + digester.addSetProperties("Registry"); + digester.addObjectCreate("Registry/Description", + "org.apache.catalina.storeconfig.StoreDescription", + "className"); + digester.addSetProperties("Registry/Description"); + digester.addRule("Registry/Description", new StoreFactoryRule( + "org.apache.catalina.storeconfig.StoreFactoryBase", + "storeFactoryClass", + "org.apache.catalina.storeconfig.StoreAppender", + "storeAppenderClass")); + digester.addSetNext("Registry/Description", "registerDescription", + "org.apache.catalina.storeconfig.StoreDescription"); + digester.addCallMethod("Registry/Description/TransientAttribute", + "addTransientAttribute", 0); + digester.addCallMethod("Registry/Description/TransientChild", + "addTransientChild", 0); + + return digester; + + } + + /** + * Load registry configuration. + * + * @param path Path to the configuration file, may be null to use the default + * name server-registry.xml + * @throws Exception when the configuration file isn't found or a parse error occurs + */ + public void load(String path) throws Exception { + try (Resource resource = (path == null) ? + ConfigFileLoader.getSource().getConfResource("server-registry.xml") + : ConfigFileLoader.getSource().getResource(path); + InputStream is = resource.getInputStream()) { + registryResource = resource.getURI().toURL(); + synchronized (digester) { + registry = (StoreRegistry) digester.parse(is); + } + } catch (IOException e) { + // Try default classloader location + try (InputStream is = StoreLoader.class + .getResourceAsStream("/org/apache/catalina/storeconfig/server-registry.xml")) { + if (is != null) { + registryResource = StoreLoader.class + .getResource("/org/apache/catalina/storeconfig/server-registry.xml"); + synchronized (digester) { + registry = (StoreRegistry) digester.parse(is); + } + } else { + throw e; + } + } + } + } + + /** + * @return the registryResource. + */ + public URL getRegistryResource() { + return registryResource; + } +} diff --git a/java/org/apache/catalina/storeconfig/StoreRegistry.java b/java/org/apache/catalina/storeconfig/StoreRegistry.java new file mode 100644 index 0000000..4865171 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/StoreRegistry.java @@ -0,0 +1,225 @@ +/* + * 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.catalina.storeconfig; + +import java.util.HashMap; +import java.util.Map; + +import javax.naming.directory.DirContext; + +import org.apache.catalina.CredentialHandler; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Manager; +import org.apache.catalina.Realm; +import org.apache.catalina.Valve; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.ha.ClusterDeployer; +import org.apache.catalina.ha.ClusterListener; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelReceiver; +import org.apache.catalina.tribes.ChannelSender; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipService; +import org.apache.catalina.tribes.MessageListener; +import org.apache.catalina.tribes.transport.DataSender; +import org.apache.coyote.UpgradeProtocol; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.CookieProcessor; +import org.apache.tomcat.util.res.StringManager; + +/** + * Central StoreRegistry for all server.xml elements + */ +public class StoreRegistry { + private static Log log = LogFactory.getLog(StoreRegistry.class); + private static StringManager sm = StringManager.getManager(StoreRegistry.class); + + private Map descriptors = new HashMap<>(); + + private String encoding = "UTF-8"; + + private String name; + + private String version; + + // Access Information + private static Class interfaces[] = { CatalinaCluster.class, + ChannelSender.class, ChannelReceiver.class, Channel.class, + MembershipService.class, ClusterDeployer.class, Realm.class, + Manager.class, DirContext.class, LifecycleListener.class, + Valve.class, ClusterListener.class, MessageListener.class, + DataSender.class, ChannelInterceptor.class, Member.class, + WebResourceRoot.class, WebResourceSet.class, + CredentialHandler.class, UpgradeProtocol.class, + CookieProcessor.class }; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name The name to set. + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the version + */ + public String getVersion() { + return version; + } + + /** + * @param version The version to set + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * Find a description for id. Handle interface search when no direct match + * found. + * + * @param id The class name + * @return the description + */ + public StoreDescription findDescription(String id) { + if (log.isTraceEnabled()) { + log.trace("search descriptor " + id); + } + StoreDescription desc = descriptors.get(id); + if (desc == null) { + Class aClass = null; + try { + aClass = Class.forName(id, true, this.getClass().getClassLoader()); + } catch (ClassNotFoundException e) { + log.error(sm.getString("registry.loadClassFailed", id), e); + } + if (aClass != null) { + desc = descriptors.get(aClass.getName()); + for (int i = 0; desc == null && i < interfaces.length; i++) { + if (interfaces[i].isAssignableFrom(aClass)) { + desc = descriptors.get(interfaces[i].getName()); + } + } + } + } + if (log.isDebugEnabled()) { + if (desc != null) { + log.trace("find descriptor " + id + "#" + desc.getTag() + "#" + + desc.getStoreFactoryClass()); + } else { + log.debug(sm.getString("registry.noDescriptor", id)); + } + } + return desc; + } + + /** + * Find Description by class. + * + * @param aClass The class + * @return the description + */ + public StoreDescription findDescription(Class aClass) { + return findDescription(aClass.getName()); + } + + /** + * Find factory from class name. + * + * @param aClassName The class name + * @return the factory + */ + public IStoreFactory findStoreFactory(String aClassName) { + StoreDescription desc = findDescription(aClassName); + if (desc != null) { + return desc.getStoreFactory(); + } else { + return null; + } + + } + + /** + * Find factory from class. + * + * @param aClass The class + * @return the factory + */ + public IStoreFactory findStoreFactory(Class aClass) { + return findStoreFactory(aClass.getName()); + } + + /** + * Register a new description. + * + * @param desc New description + */ + public void registerDescription(StoreDescription desc) { + String key = desc.getId(); + if (key == null || key.isEmpty()) { + key = desc.getTagClass(); + } + descriptors.put(key, desc); + if (log.isTraceEnabled()) { + log.trace("register store descriptor " + key + "#" + desc.getTag() + + "#" + desc.getTagClass()); + } + } + + /** + * Unregister a description. + * + * @param desc The description + * @return the description, or null if it was not registered + */ + public StoreDescription unregisterDescription(StoreDescription desc) { + String key = desc.getId(); + if (key == null || "".equals(key)) { + key = desc.getTagClass(); + } + return descriptors.remove(key); + } + + // Attributes + + /** + * @return the encoding + */ + public String getEncoding() { + return encoding; + } + + /** + * Set the encoding to use when writing the configuration files. + * @param string The encoding + */ + public void setEncoding(String string) { + encoding = string; + } + +} diff --git a/java/org/apache/catalina/storeconfig/WatchedResourceSF.java b/java/org/apache/catalina/storeconfig/WatchedResourceSF.java new file mode 100644 index 0000000..cc7b692 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/WatchedResourceSF.java @@ -0,0 +1,53 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.core.StandardContext; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class WatchedResourceSF extends StoreFactoryBase { + private static Log log = LogFactory.getLog(WatchedResourceSF.class); + + /* + * Store nested Element Value Arrays WatchedResource + * + * @see org.apache.catalina.config.IStoreFactory#store(java.io.PrintWriter, + * int, java.lang.Object) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + if (aElement instanceof StandardContext) { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass().getName() + ".[WatchedResource]"); + String[] resources = ((StandardContext) aElement) + .findWatchedResources(); + if (elementDesc != null) { + if (log.isTraceEnabled()) { + log.trace("store " + elementDesc.getTag() + "( " + aElement + " )"); + } + getStoreAppender().printTagArray(aWriter, "WatchedResource", + indent, resources); + } + } else { + log.warn(sm.getString("storeFactory.noDescriptor", aElement.getClass(), "WatchedResource")); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/WebResourceRootSF.java b/java/org/apache/catalina/storeconfig/WebResourceRootSF.java new file mode 100644 index 0000000..05686d4 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/WebResourceRootSF.java @@ -0,0 +1,84 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; + +/** + * Generate Resources element + */ +public class WebResourceRootSF extends StoreFactoryBase { + + /** + * Store the specified Resources children. + * + * @param aWriter + * PrintWriter to which we are storing + * @param indent + * Number of spaces to indent this element + * + * @exception Exception + * if an exception occurs while storing + */ + @Override + public void storeChildren(PrintWriter aWriter, int indent, Object aResourceRoot, + StoreDescription parentDesc) throws Exception { + if (aResourceRoot instanceof WebResourceRoot) { + WebResourceRoot resourceRoot = (WebResourceRoot) aResourceRoot; + + // Store nested elements + WebResourceSet[] preResourcesArray = resourceRoot.getPreResources(); + StoreDescription preResourcesElementDesc = getRegistry().findDescription( + WebResourceSet.class.getName() + + ".[PreResources]"); + if (preResourcesElementDesc != null) { + for (WebResourceSet preResources : preResourcesArray) { + preResourcesElementDesc.getStoreFactory().store(aWriter, indent, + preResources); + } + } + + // Store nested elements + WebResourceSet[] jarResourcesArray = resourceRoot.getJarResources(); + StoreDescription jarResourcesElementDesc = getRegistry().findDescription( + WebResourceSet.class.getName() + + ".[JarResources]"); + if (jarResourcesElementDesc != null) { + for (WebResourceSet jarResources : jarResourcesArray) { + jarResourcesElementDesc.getStoreFactory().store(aWriter, indent, + jarResources); + } + } + + // Store nested elements + WebResourceSet[] postResourcesArray = resourceRoot.getPostResources(); + StoreDescription postResourcesElementDesc = getRegistry().findDescription( + WebResourceSet.class.getName() + + ".[PostResources]"); + if (postResourcesElementDesc != null) { + for (WebResourceSet postResources : postResourcesArray) { + postResourcesElementDesc.getStoreFactory().store(aWriter, indent, + postResources); + } + } + + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/WrapperLifecycleSF.java b/java/org/apache/catalina/storeconfig/WrapperLifecycleSF.java new file mode 100644 index 0000000..d06d3ba --- /dev/null +++ b/java/org/apache/catalina/storeconfig/WrapperLifecycleSF.java @@ -0,0 +1,53 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.core.StandardContext; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class WrapperLifecycleSF extends StoreFactoryBase { + private static Log log = LogFactory.getLog(WrapperLifecycleSF.class); + + /* + * Store nested Element Value Arrays + * + * @see org.apache.catalina.config.IStoreFactory#store(java.io.PrintWriter, + * int, java.lang.Object) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + if (aElement instanceof StandardContext) { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass().getName() + ".[WrapperLifecycle]"); + String[] listeners = ((StandardContext) aElement) + .findWrapperLifecycles(); + if (elementDesc != null) { + if (log.isTraceEnabled()) { + log.trace("store " + elementDesc.getTag() + "( " + aElement + " )"); + } + getStoreAppender().printTagArray(aWriter, "WrapperLifecycle", + indent, listeners); + } + } else { + log.warn(sm.getString("storeFactory.noDescriptor", aElement.getClass(), "WrapperLifecycle")); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/WrapperListenerSF.java b/java/org/apache/catalina/storeconfig/WrapperListenerSF.java new file mode 100644 index 0000000..2827554 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/WrapperListenerSF.java @@ -0,0 +1,54 @@ +/* + * 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.catalina.storeconfig; + +import java.io.PrintWriter; + +import org.apache.catalina.core.StandardContext; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class WrapperListenerSF extends StoreFactoryBase { + private static Log log = LogFactory.getLog(WrapperListenerSF.class); + + /* + * Store nested Element Value Arrays + * + * @see org.apache.catalina.config.IStoreFactory#store(java.io.PrintWriter, + * int, java.lang.Object) + */ + @Override + public void store(PrintWriter aWriter, int indent, Object aElement) + throws Exception { + if (aElement instanceof StandardContext) { + StoreDescription elementDesc = getRegistry().findDescription( + aElement.getClass().getName() + ".[WrapperListener]"); + String[] listeners = ((StandardContext) aElement) + .findWrapperListeners(); + if (elementDesc != null) { + if (log.isTraceEnabled()) { + log.trace("store " + elementDesc.getTag() + "( " + aElement + + " )"); + } + getStoreAppender().printTagArray(aWriter, "WrapperListener", + indent, listeners); + } + } else { + log.warn(sm.getString("storeFactory.noDescriptor", aElement.getClass(), "WrapperListener")); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/storeconfig/mbeans-descriptors.xml b/java/org/apache/catalina/storeconfig/mbeans-descriptors.xml new file mode 100644 index 0000000..06aad64 --- /dev/null +++ b/java/org/apache/catalina/storeconfig/mbeans-descriptors.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/storeconfig/server-registry.xml b/java/org/apache/catalina/storeconfig/server-registry.xml new file mode 100644 index 0000000..4dd7dde --- /dev/null +++ b/java/org/apache/catalina/storeconfig/server-registry.xml @@ -0,0 +1,482 @@ + + + + + + + + + domain + + + domain + + + available + configFile + configured + displayName + distributable + domain + name + publicId + originalDocBase + replaceWelcomeFiles + sessionTimeout + startupTime + tldScanTime + effectiveMajorVersion + effectiveMinorVersion + webappVersion + ignoreAnnotations + + + + + + + + + openSslContext + openSslConfContext + + + + + + + + + + + domain + + + + + + + + + clusterName + + + domain + realmPath + + + + + + + domain + + + org.apache.catalina.mbeans.ServerLifecycleListener + org.apache.catalina.core.NamingContextListener + org.apache.catalina.startup.ContextConfig + org.apache.catalina.startup.ContextConfig$HostWebXmlCacheCleaner + org.apache.catalina.startup.EngineConfig + org.apache.catalina.startup.HostConfig + org.apache.catalina.startup.ListenerCreateRule$OptionalListener + org.apache.catalina.core.StandardHost$MemoryLeakTrackingListener + org.apache.catalina.mapper.MapperListener + org.apache.catalina.core.StandardEngine$AccessLogListener + + + + + org.apache.catalina.authenticator.BasicAuthenticator + org.apache.catalina.authenticator.DigestAuthenticator + org.apache.catalina.authenticator.FormAuthenticator + org.apache.catalina.authenticator.NonLoginAuthenticator + org.apache.catalina.authenticator.SSLAuthenticator + org.apache.catalina.core.StandardContextValve + org.apache.catalina.core.StandardEngineValve + org.apache.catalina.core.StandardHostValve + org.apache.catalina.valves.CertificatesValve + org.apache.catalina.valves.ErrorReportValve + org.apache.catalina.valves.RequestListenerValve + + + + + + + + + + + domain + + + + + + + + + + + + + + + + + + + + + + + + + bind + host + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/tribes/ByteMessage.java b/java/org/apache/catalina/tribes/ByteMessage.java new file mode 100644 index 0000000..4560d87 --- /dev/null +++ b/java/org/apache/catalina/tribes/ByteMessage.java @@ -0,0 +1,102 @@ +/* + * 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.catalina.tribes; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +/** + * A byte message is not serialized and deserialized by the channel + * instead it is sent as a byte array
    + * By default Tribes uses java serialization when it receives an object + * to be sent over the wire. Java serialization is not the most + * efficient of serializing data, and Tribes might not even + * have access to the correct class loaders to deserialize the object properly. + *
    + * The ByteMessage class is a class where the channel when it receives it will + * not attempt to perform serialization, instead it will simply stream the getMessage() + * bytes.
    + * If you are using multiple applications on top of Tribes you should add some sort of header + * so that you can decide with the ChannelListener.accept() whether this message was intended + * for you. + */ +public class ByteMessage implements Externalizable { + /** + * Storage for the message to be sent + */ + private byte[] message; + + + /** + * Creates an empty byte message + * Constructor also for deserialization + */ + public ByteMessage() { + } + + /** + * Creates a byte message with + * @param data byte[] - the message contents + */ + public ByteMessage(byte[] data) { + message = data; + } + + /** + * Returns the message contents of this byte message + * @return byte[] - message contents, can be null + */ + public byte[] getMessage() { + return message; + } + + /** + * Sets the message contents of this byte message + * @param message byte[] + */ + public void setMessage(byte[] message) { + this.message = message; + } + + /** + * @see java.io.Externalizable#readExternal + * @param in ObjectInput + * @throws IOException An IO error occurred + */ + @Override + public void readExternal(ObjectInput in ) throws IOException { + int length = in.readInt(); + message = new byte[length]; + in.readFully(message); + } + + /** + * @see java.io.Externalizable#writeExternal + * @param out ObjectOutput + * @throws IOException An IO error occurred + */ + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeInt(message!=null?message.length:0); + if ( message!=null ) { + out.write(message,0,message.length); + } + } + +} diff --git a/java/org/apache/catalina/tribes/Channel.java b/java/org/apache/catalina/tribes/Channel.java new file mode 100644 index 0000000..32359bd --- /dev/null +++ b/java/org/apache/catalina/tribes/Channel.java @@ -0,0 +1,473 @@ +/* + * 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.catalina.tribes; + +import java.io.Serializable; +import java.util.StringJoiner; +import java.util.concurrent.ScheduledExecutorService; + +import org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Channel interface
    + * A channel is a representation of a group of nodes all participating in some sort of + * communication with each other.
    + * The channel is the main API class for Tribes, this is essentially the only class + * that an application needs to be aware of. Through the channel the application can:
    + * 1. send messages
    + * 2. receive message (by registering a ChannelListener
    + * 3. get all members of the group getMembers()
    + * 4. receive notifications of members added and members disappeared by + * registering a MembershipListener
    + *
    + * The channel has 5 major components:
    + * 1. Data receiver, with a built in thread pool to receive messages from other peers
    + * 2. Data sender, an implementation for sending data using NIO or java.io
    + * 3. Membership listener,listens for membership broadcasts
    + * 4. Membership broadcaster, broadcasts membership pings.
    + * 5. Channel interceptors, the ability to manipulate messages as they are sent or arrive

    + * The channel layout is: + *
    
    + *  ChannelListener_1..ChannelListener_N MembershipListener_1..MembershipListener_N [Application Layer]
    + *            \          \                  /                   /
    + *             \          \                /                   /
    + *              \          \              /                   /
    + *               \          \            /                   /
    + *                \          \          /                   /
    + *                 \          \        /                   /
    + *                  ---------------------------------------
    + *                                  |
    + *                                  |
    + *                               Channel
    + *                                  |
    + *                         ChannelInterceptor_1
    + *                                  |                                               [Channel stack]
    + *                         ChannelInterceptor_N
    + *                                  |
    + *                             Coordinator (implements MessageListener,MembershipListener,ChannelInterceptor)
    + *                          --------------------
    + *                         /        |           \
    + *                        /         |            \
    + *                       /          |             \
    + *                      /           |              \
    + *                     /            |               \
    + *           MembershipService ChannelSender ChannelReceiver                        [IO layer]
    + * 
    + * + * @see org.apache.catalina.tribes.group.GroupChannel example usage + */ +public interface Channel { + + /** + * Start and stop sequences can be controlled by these constants + * This allows you to start separate components of the channel
    + * DEFAULT - starts or stops all components in the channel + * @see #start(int) + * @see #stop(int) + */ + int DEFAULT = 15; + + /** + * Start and stop sequences can be controlled by these constants + * This allows you to start separate components of the channel
    + * SND_RX_SEQ - starts or stops the data receiver. Start means opening a server socket + * in case of a TCP implementation + * @see #start(int) + * @see #stop(int) + */ + int SND_RX_SEQ = 1; + + /** + * Start and stop sequences can be controlled by these constants + * This allows you to start separate components of the channel
    + * SND_TX_SEQ - starts or stops the data sender. This should not open any sockets, + * as sockets are opened on demand when a message is being sent + * @see #start(int) + * @see #stop(int) + */ + int SND_TX_SEQ = 2; + + /** + * Start and stop sequences can be controlled by these constants + * This allows you to start separate components of the channel
    + * MBR_RX_SEQ - starts or stops the membership listener. In a multicast implementation + * this will open a datagram socket and join a group and listen for membership messages + * members joining + * @see #start(int) + * @see #stop(int) + */ + int MBR_RX_SEQ = 4; + + /** + * Start and stop sequences can be controlled by these constants + * This allows you to start separate components of the channel
    + * MBR_TX_SEQ - starts or stops the membership broadcaster. In a multicast implementation + * this will open a datagram socket and join a group and broadcast the local member information + * @see #start(int) + * @see #stop(int) + */ + int MBR_TX_SEQ = 8; + + /** + * Send options, when a message is sent, it can have an option flag + * to trigger certain behavior. Most flags are used to trigger channel interceptors + * as the message passes through the channel stack.
    + * However, there are five default flags that every channel implementation must implement
    + * SEND_OPTIONS_BYTE_MESSAGE - The message is a pure byte message and no marshaling or unmarshaling will + * be performed.
    + * + * @see #send(Member[], Serializable , int) + * @see #send(Member[], Serializable, int, ErrorHandler) + */ + int SEND_OPTIONS_BYTE_MESSAGE = 0x0001; + + /** + * Send options, when a message is sent, it can have an option flag + * to trigger certain behavior. Most flags are used to trigger channel interceptors + * as the message passes through the channel stack.
    + * However, there are five default flags that every channel implementation must implement
    + * SEND_OPTIONS_USE_ACK - Message is sent and an ACK is received when the message has been received by the recipient
    + * If no ack is received, the message is not considered successful
    + * @see #send(Member[], Serializable , int) + * @see #send(Member[], Serializable, int, ErrorHandler) + */ + int SEND_OPTIONS_USE_ACK = 0x0002; + + /** + * Send options, when a message is sent, it can have an option flag + * to trigger certain behavior. Most flags are used to trigger channel interceptors + * as the message passes through the channel stack.
    + * However, there are five default flags that every channel implementation must implement
    + * SEND_OPTIONS_SYNCHRONIZED_ACK - Message is sent and an ACK is received when the message has been received and + * processed by the recipient
    + * If no ack is received, the message is not considered successful
    + * @see #send(Member[], Serializable , int) + * @see #send(Member[], Serializable, int, ErrorHandler) + */ + int SEND_OPTIONS_SYNCHRONIZED_ACK = 0x0004; + + /** + * Send options, when a message is sent, it can have an option flag + * to trigger certain behavior. Most flags are used to trigger channel interceptors + * as the message passes through the channel stack.
    + * However, there are five default flags that every channel implementation must implement
    + * SEND_OPTIONS_ASYNCHRONOUS - Message will be placed on a queue and sent by a separate thread
    + * If the queue is full, behaviour depends on {@link MessageDispatchInterceptor#isAlwaysSend()} + * @see #send(Member[], Serializable , int) + * @see #send(Member[], Serializable, int, ErrorHandler) + */ + int SEND_OPTIONS_ASYNCHRONOUS = 0x0008; + + /** + * Send options, when a message is sent, it can have an option flag + * to trigger certain behavior. Most flags are used to trigger channel interceptors + * as the message passes through the channel stack.
    + * However, there are five default flags that every channel implementation must implement
    + * SEND_OPTIONS_SECURE - Message is sent over an encrypted channel
    + * @see #send(Member[], Serializable , int) + * @see #send(Member[], Serializable, int, ErrorHandler) + */ + int SEND_OPTIONS_SECURE = 0x0010; + + /** + * Send options. When a message is sent with this flag on + * the system sends the message using UDP instead of TCP + * @see #send(Member[], Serializable , int) + * @see #send(Member[], Serializable, int, ErrorHandler) + */ + int SEND_OPTIONS_UDP = 0x0020; + + /** + * Send options. When a message is sent with this flag on + * the system sends a UDP message on the Multicast address instead of UDP or TCP to individual addresses + * @see #send(Member[], Serializable , int) + * @see #send(Member[], Serializable, int, ErrorHandler) + */ + int SEND_OPTIONS_MULTICAST = 0x0040; + + /** + * Send options, when a message is sent, it can have an option flag + * to trigger certain behavior. Most flags are used to trigger channel interceptors + * as the message passes through the channel stack.
    + * However, there are five default flags that every channel implementation must implement
    + * SEND_OPTIONS_DEFAULT - the default sending options, just a helper variable.
    + * The default is int SEND_OPTIONS_DEFAULT = SEND_OPTIONS_USE_ACK;
    + * @see #SEND_OPTIONS_USE_ACK + * @see #send(Member[], Serializable , int) + * @see #send(Member[], Serializable, int, ErrorHandler) + */ + int SEND_OPTIONS_DEFAULT = SEND_OPTIONS_USE_ACK; + + + /** + * Adds an interceptor to the channel message chain. + * @param interceptor ChannelInterceptor + */ + void addInterceptor(ChannelInterceptor interceptor); + + /** + * Starts up the channel. This can be called multiple times for individual services to start + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * DEFAULT - will start all services
    + * MBR_RX_SEQ - starts the membership receiver
    + * MBR_TX_SEQ - starts the membership broadcaster
    + * SND_TX_SEQ - starts the replication transmitter
    + * SND_RX_SEQ - starts the replication receiver
    + * Note: In order for the membership broadcaster to + * transmit the correct information, it has to be started after the replication receiver. + * @throws ChannelException if a startup error occurs or the service is already started or an error occurs. + */ + void start(int svc) throws ChannelException; + + /** + * Shuts down the channel. This can be called multiple times for individual services to shutdown + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * DEFAULT - will shutdown all services
    + * MBR_RX_SEQ - stops the membership receiver
    + * MBR_TX_SEQ - stops the membership broadcaster
    + * SND_TX_SEQ - stops the replication transmitter
    + * SND_RX_SEQ - stops the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already stopped or an error occurs. + */ + void stop(int svc) throws ChannelException; + + /** + * Send a message to one or more members in the cluster + * @param destination Member[] - the destinations, cannot be null or zero length, the reason for that + * is that a membership change can occur and at that time the application is uncertain what group the message + * actually got sent to. + * @param msg Serializable - the message to send, has to be serializable, or a ByteMessage to + * send a pure byte array + * @param options int - sender options, see class documentation for each interceptor that is configured in order to trigger interceptors + * @return a unique Id that identifies the message that is sent + * @throws ChannelException if a serialization error happens. + * @see ByteMessage + * @see #SEND_OPTIONS_USE_ACK + * @see #SEND_OPTIONS_ASYNCHRONOUS + * @see #SEND_OPTIONS_SYNCHRONIZED_ACK + */ + UniqueId send(Member[] destination, Serializable msg, int options) throws ChannelException; + + /** + * Send a message to one or more members in the cluster + * @param destination Member[] - the destinations, null or zero length means all + * @param msg ClusterMessage - the message to send + * @param options int - sender options, see class documentation + * @param handler ErrorHandler - handle errors through a callback, rather than throw it + * @return a unique Id that identifies the message that is sent + * @exception ChannelException - if a serialization error happens. + */ + UniqueId send(Member[] destination, Serializable msg, int options, ErrorHandler handler) throws ChannelException; + + /** + * Sends a heart beat through the interceptor stacks + * Use this method to alert interceptors and other components to + * clean up garbage, timed out messages etc.
    + * If you application has a background thread, then you can save one thread, + * by configuring your channel to not use an internal heartbeat thread + * and invoking this method. + * @see #setHeartbeat(boolean) + */ + void heartbeat(); + + /** + * Enables or disables internal heartbeat. + * @param enable boolean - default value is implementation specific + * @see #heartbeat() + */ + void setHeartbeat(boolean enable); + + /** + * Add a membership listener, will get notified when a new member joins, leaves or crashes + *
    If the membership listener implements the Heartbeat interface + * the heartbeat() method will be invoked when the heartbeat runs on the channel + * @param listener MembershipListener + * @see MembershipListener + */ + void addMembershipListener(MembershipListener listener); + + /** + * Add a channel listener, this is a callback object when messages are received + *
    If the channel listener implements the Heartbeat interface + * the heartbeat() method will be invoked when the heartbeat runs on the channel + * @param listener ChannelListener + * @see ChannelListener + * @see Heartbeat + */ + void addChannelListener(ChannelListener listener); + + /** + * remove a membership listener, listeners are removed based on Object.hashCode and Object.equals + * @param listener MembershipListener + * @see MembershipListener + */ + void removeMembershipListener(MembershipListener listener); + /** + * remove a channel listener, listeners are removed based on Object.hashCode and Object.equals + * @param listener ChannelListener + * @see ChannelListener + */ + void removeChannelListener(ChannelListener listener); + + /** + * Returns true if there are any members in the group, + * this call is the same as getMembers().length > 0 + * @return boolean - true if there are any members automatically discovered + */ + boolean hasMembers() ; + + /** + * Get all current group members + * @return all members or empty array, never null + */ + Member[] getMembers() ; + + /** + * Return the member that represents this node. This is also the data + * that gets broadcasted through the membership broadcaster component + * @param incAlive - optimization, true if you want it to calculate alive time + * since the membership service started. + * @return Member + */ + Member getLocalMember(boolean incAlive); + + /** + * Returns the member from the membership service with complete and + * recent data. Some implementations might serialize and send + * membership information along with a message, and instead of sending + * complete membership details, only send the primary identifier for the member + * but not the payload or other information. When such message is received + * the application can retrieve the cached member through this call.
    + * In most cases, this is not necessary. + * @param mbr Member + * @return Member + */ + Member getMember(Member mbr); + + /** + * Return the name of this channel. + * @return channel name + */ + String getName(); + + /** + * Set the name of this channel + * @param name The new channel name + */ + void setName(String name); + + /** + * Return executor that can be used for utility tasks. + * @return the executor + */ + ScheduledExecutorService getUtilityExecutor(); + + /** + * Set the executor that can be used for utility tasks. + * @param utilityExecutor the executor + */ + void setUtilityExecutor(ScheduledExecutorService utilityExecutor); + + /** + * Translates the name of an option to its integer value. Valid option names are "asynchronous" (alias "async"), + * "byte_message" (alias "byte"), "multicast", "secure", "synchronized_ack" (alias "sync"), "udp", "use_ack" + * @param opt The name of the option + * @return the int value of the passed option name + */ + static int getSendOptionValue(String opt){ + + switch (opt){ + + case "asynchronous": + case "async": + return SEND_OPTIONS_ASYNCHRONOUS; + + case "byte_message": + case "byte": + return SEND_OPTIONS_BYTE_MESSAGE; + + case "multicast": + return SEND_OPTIONS_MULTICAST; + + case "secure": + return SEND_OPTIONS_SECURE; + + case "synchronized_ack": + case "sync": + return SEND_OPTIONS_SYNCHRONIZED_ACK; + + case "udp": + return SEND_OPTIONS_UDP; + + case "use_ack": + return SEND_OPTIONS_USE_ACK; + } + + throw new IllegalArgumentException(String.format("[%s] is not a valid option", opt)); + } + + /** + * Translates a comma separated list of option names to their bitwise-ORd value + * @param input A comma separated list of options, e.g. "async, multicast" + * @return a bitwise ORd value of the passed option names + */ + static int parseSendOptions(String input){ + + try { + return Integer.parseInt(input); + } catch (NumberFormatException nfe){ + final Log log = LogFactory.getLog(Channel.class); + log.trace(String.format("Failed to parse [%s] as integer, channelSendOptions possibly set by name(s)", input)); + } + + String[] options = input.split("\\s*,\\s*"); + + int result = 0; + for (String opt : options) { + result |= getSendOptionValue(opt); + } + + return result; + } + + /** + * Translates an integer value of SendOptions to its human-friendly comma separated value list for use in JMX and such. + * @param input the int value of SendOptions + * @return the human-friendly string representation in a reverse order (i.e. the last option will be shown first) + */ + static String getSendOptionsAsString(int input){ + + // allOptionNames must be in order of the bits of the available options + final String[] allOptionNames = new String[]{ "byte", "use_ack", "sync", "async", "secure", "udp", "multicast" }; + + StringJoiner names = new StringJoiner(", "); + for (int bit=allOptionNames.length - 1; bit >= 0; bit--){ + + // if the bit is set then add the name to the result + if (((1 << bit) & input) > 0){ + names.add(allOptionNames[bit]); + } + } + + return names.toString(); + } + +} diff --git a/java/org/apache/catalina/tribes/ChannelException.java b/java/org/apache/catalina/tribes/ChannelException.java new file mode 100644 index 0000000..5279c35 --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelException.java @@ -0,0 +1,188 @@ +/* + * 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.catalina.tribes; + +import java.util.ArrayList; + +/** + * A channel exception is thrown when an internal error happens + * somewhere in the channel. + *

    + * When a global error happens, the cause can be retrieved using getCause()

    + * If an application is sending a message and some of the recipients fail to receive it, + * the application can retrieve what recipients failed by using the getFaultyMembers() + * method. This way, an application will always know if a message was delivered successfully or not. + */ +public class ChannelException extends Exception { + private static final long serialVersionUID = 1L; + /** + * Empty list to avoid reinstantiating lists + */ + protected static final FaultyMember[] EMPTY_LIST = new FaultyMember[0]; + /** + * Holds a list of faulty members + */ + private ArrayList faultyMembers=null; + + /** + * Constructor, creates a ChannelException + * @see java.lang.Exception#Exception() + */ + public ChannelException() { + super(); + } + + /** + * Constructor, creates a ChannelException with an error message + * @param message The error message + * @see java.lang.Exception#Exception(String) + */ + public ChannelException(String message) { + super(message); + } + + /** + * Constructor, creates a ChannelException with an error message and a cause + * @param message The error message + * @param cause Throwable + * @see java.lang.Exception#Exception(String,Throwable) + */ + public ChannelException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructor, creates a ChannelException with a cause + * @param cause Throwable + * @see java.lang.Exception#Exception(Throwable) + */ + public ChannelException(Throwable cause) { + super(cause); + } + + /** + * Returns the message for this exception + * @return the error message + * @see java.lang.Exception#getMessage() + */ + @Override + public String getMessage() { + StringBuilder buf = new StringBuilder(super.getMessage()); + if (faultyMembers==null || faultyMembers.size() == 0 ) { + buf.append("; No faulty members identified."); + } else { + buf.append("; Faulty members:"); + for (FaultyMember mbr : faultyMembers) { + buf.append(mbr.getMember().getName()); + buf.append("; "); + } + } + return buf.toString(); + } + + /** + * Adds a faulty member, and the reason the member failed. + * @param mbr Member + * @param x Exception + * @return true if the member was added + */ + public boolean addFaultyMember(Member mbr, Exception x ) { + return addFaultyMember(new FaultyMember(mbr,x)); + } + + /** + * Adds a list of faulty members + * @param mbrs FaultyMember[] + * @return the number of members added + */ + public int addFaultyMember(FaultyMember[] mbrs) { + int result = 0; + for (int i=0; mbrs!=null && itrue if the member was added + */ + public boolean addFaultyMember(FaultyMember mbr) { + if ( this.faultyMembers==null ) { + this.faultyMembers = new ArrayList<>(); + } + if ( !faultyMembers.contains(mbr) ) { + return faultyMembers.add(mbr); + } else { + return false; + } + } + + /** + * Returns an array of members that failed and the reason they failed. + * @return FaultyMember[] + */ + public FaultyMember[] getFaultyMembers() { + if ( this.faultyMembers==null ) { + return EMPTY_LIST; + } + return faultyMembers.toArray(new FaultyMember[0]); + } + + /** + * Represent a failure to a specific member when a message was sent to more + * than one member + */ + public static class FaultyMember { + protected final Exception cause; + protected final Member member; + public FaultyMember(Member mbr, Exception x) { + this.member = mbr; + this.cause = x; + } + + public Member getMember() { + return member; + } + + public Exception getCause() { + return cause; + } + + @Override + public String toString() { + return "FaultyMember:"+member.toString(); + } + + @Override + public int hashCode() { + return (member!=null)?member.hashCode():0; + } + + @Override + public boolean equals(Object o) { + if (member==null || (!(o instanceof FaultyMember)) || (((FaultyMember)o).member==null)) { + return false; + } + return member.equals(((FaultyMember)o).member); + } + } + +} diff --git a/java/org/apache/catalina/tribes/ChannelInterceptor.java b/java/org/apache/catalina/tribes/ChannelInterceptor.java new file mode 100644 index 0000000..2beb56a --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelInterceptor.java @@ -0,0 +1,189 @@ +/* + * 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.catalina.tribes; + +import org.apache.catalina.tribes.group.InterceptorPayload; + +/** + * A ChannelInterceptor is an interceptor that intercepts + * messages and membership messages in the channel stack. + * This allows interceptors to modify the message or perform + * other actions when a message is sent or received.
    + * Interceptors are tied together in a linked list. + * @see org.apache.catalina.tribes.group.ChannelInterceptorBase + */ +public interface ChannelInterceptor extends MembershipListener, Heartbeat { + + /** + * An interceptor can react to a message based on a set bit on the + * message options.
    + * When a message is sent, the options can be retrieved from ChannelMessage.getOptions() + * and if the bit is set, this interceptor will react to it.
    + * A simple evaluation if an interceptor should react to the message would be:
    + * boolean react = (getOptionFlag() == (getOptionFlag() & ChannelMessage.getOptions()));
    + * The default option is 0, meaning there is no way for the application to trigger the + * interceptor. The interceptor itself will decide.
    + * @return int + * @see ChannelMessage#getOptions() + */ + int getOptionFlag(); + + /** + * Sets the option flag + * @param flag int + * @see #getOptionFlag() + */ + void setOptionFlag(int flag); + + /** + * Set the next interceptor in the list of interceptors + * @param next ChannelInterceptor + */ + void setNext(ChannelInterceptor next) ; + + /** + * Retrieve the next interceptor in the list + * @return ChannelInterceptor - returns the next interceptor in the list or null if no more interceptors exist + */ + ChannelInterceptor getNext(); + + /** + * Set the previous interceptor in the list + * @param previous ChannelInterceptor + */ + void setPrevious(ChannelInterceptor previous); + + /** + * Retrieve the previous interceptor in the list + * @return ChannelInterceptor - returns the previous interceptor in the list or null if no more interceptors exist + */ + ChannelInterceptor getPrevious(); + + /** + * The sendMessage method is called when a message is being sent to one more destinations. + * The interceptor can modify any of the parameters and then pass on the message down the stack by + * invoking getNext().sendMessage(destination,msg,payload)
    + * Alternatively the interceptor can stop the message from being sent by not invoking + * getNext().sendMessage(destination,msg,payload)
    + * If the message is to be sent asynchronous the application can be notified of completion and + * errors by passing in an error handler attached to a payload object.
    + * The ChannelMessage.getAddress contains Channel.getLocalMember, and can be overwritten + * to simulate a message sent from another node.
    + * @param destination Member[] - the destination for this message + * @param msg ChannelMessage - the message to be sent + * @param payload InterceptorPayload - the payload, carrying an error handler and future useful data, can be null + * @throws ChannelException if a serialization error happens. + * @see ErrorHandler + * @see InterceptorPayload + */ + void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException; + + /** + * the messageReceived is invoked when a message is received. + * ChannelMessage.getAddress() is the sender, or the reply-to address + * if it has been overwritten. + * @param data ChannelMessage + */ + void messageReceived(ChannelMessage data); + + /** + * The heartbeat() method gets invoked periodically + * to allow interceptors to clean up resources, time out object and + * perform actions that are unrelated to sending/receiving data. + */ + @Override void heartbeat(); + + /** + * Intercepts the Channel.hasMembers() method + * @return boolean - if the channel has members in its membership group + * @see Channel#hasMembers() + */ + boolean hasMembers() ; + + /** + * Intercepts the Channel.getMembers() method + * @return Member[] + * @see Channel#getMembers() + */ + Member[] getMembers() ; + + /** + * Intercepts the Channel.getLocalMember(boolean) method + * @param incAliveTime boolean + * @return Member + * @see Channel#getLocalMember(boolean) + */ + Member getLocalMember(boolean incAliveTime) ; + + /** + * Intercepts the Channel.getMember(Member) method + * @param mbr Member + * @return Member - the actual member information, including stay alive + * @see Channel#getMember(Member) + */ + Member getMember(Member mbr); + + /** + * Starts up the channel. This can be called multiple times for individual services to start + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * Channel.DEFAULT - will start all services
    + * Channel.MBR_RX_SEQ - starts the membership receiver
    + * Channel.MBR_TX_SEQ - starts the membership broadcaster
    + * Channel.SND_TX_SEQ - starts the replication transmitter
    + * Channel.SND_RX_SEQ - starts the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already started. + * @see Channel + */ + void start(int svc) throws ChannelException; + + /** + * Shuts down the channel. This can be called multiple times for individual services to shutdown + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * Channel.DEFAULT - will shutdown all services
    + * Channel.MBR_RX_SEQ - stops the membership receiver
    + * Channel.MBR_TX_SEQ - stops the membership broadcaster
    + * Channel.SND_TX_SEQ - stops the replication transmitter
    + * Channel.SND_RX_SEQ - stops the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already started. + * @see Channel + */ + void stop(int svc) throws ChannelException; + + void fireInterceptorEvent(InterceptorEvent event); + + /** + * Return the channel that is related to this interceptor + * @return Channel + */ + Channel getChannel(); + + /** + * Set the channel that is related to this interceptor + * @param channel The channel + */ + void setChannel(Channel channel); + + interface InterceptorEvent { + int getEventType(); + String getEventTypeDesc(); + ChannelInterceptor getInterceptor(); + } + + +} diff --git a/java/org/apache/catalina/tribes/ChannelListener.java b/java/org/apache/catalina/tribes/ChannelListener.java new file mode 100644 index 0000000..7871271 --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelListener.java @@ -0,0 +1,61 @@ +/* + * 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.catalina.tribes; + +import java.io.Serializable; +/** + * An interface to listens to incoming messages from a channel. + *

    + * When a message is received, the Channel will invoke the channel listener in a conditional sequence. + * if ( listener.accept(msg,sender) ) listener.messageReceived(msg,sender);
    + * A ChannelListener implementation MUST NOT return true on accept(Serializable, Member) + * if it doesn't intend to process the message. The channel can this way track whether a message + * was processed by an above application or if it was just received and forgot about, a feature required + * to support message-response(RPC) calls + */ + +public interface ChannelListener { + + /** + * Receive a message from the channel + * @param msg Serializable + * @param sender - the source of the message + */ + void messageReceived(Serializable msg, Member sender); + + /** + * Invoked by the channel to determine if the listener will process this message or not. + * @param msg Serializable + * @param sender Member + * @return boolean + */ + boolean accept(Serializable msg, Member sender); + + /** + * @param listener Object + * @return boolean + * @see Object#equals(Object) + */ + @Override boolean equals(Object listener); + + /** + * @return int + * @see Object#hashCode() + */ + @Override int hashCode(); + +} diff --git a/java/org/apache/catalina/tribes/ChannelMessage.java b/java/org/apache/catalina/tribes/ChannelMessage.java new file mode 100644 index 0000000..4421c56 --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelMessage.java @@ -0,0 +1,107 @@ +/* + * 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.catalina.tribes; + +import java.io.Serializable; + +import org.apache.catalina.tribes.io.XByteBuffer; + +/** + * Message that is passed through the interceptor stack after the + * data serialized in the Channel object and then passed down to the + * interceptor and eventually down to the ChannelSender component + * + */ +public interface ChannelMessage extends Serializable, Cloneable { + + + /** + * Get the address that this message originated from. + * Almost always Channel.getLocalMember(boolean)
    + * This would be set to a different address + * if the message was being relayed from a host other than the one + * that originally sent it. + * @return the source or reply-to address of this message + */ + Member getAddress(); + + /** + * Sets the source or reply-to address of this message + * @param member Member + */ + void setAddress(Member member); + + /** + * Timestamp of when the message was created. + * @return long timestamp in milliseconds + */ + long getTimestamp(); + + /** + * Sets the timestamp of this message. + * + * @param timestamp The timestamp + */ + void setTimestamp(long timestamp); + + /** + * Each message must have a globally unique Id. + * interceptors heavily depend on this id for message processing + * @return byte + */ + byte[] getUniqueId(); + + /** + * The byte buffer that contains the actual message payload + * @param buf XByteBuffer + */ + void setMessage(XByteBuffer buf); + + /** + * returns the byte buffer that contains the actual message payload + * @return XByteBuffer + */ + XByteBuffer getMessage(); + + /** + * The message options is a 32 bit flag set + * that triggers interceptors and message behavior. + * @see Channel#send(Member[], Serializable, int) + * @see ChannelInterceptor#getOptionFlag + * @return int - the option bits set for this message + */ + int getOptions(); + + /** + * sets the option bits for this message + * @param options int + * @see #getOptions() + */ + void setOptions(int options); + + /** + * Shallow clone, what gets cloned depends on the implementation + * @return ChannelMessage + */ + Object clone(); + + /** + * Deep clone, all fields MUST get cloned + * @return ChannelMessage + */ + Object deepclone(); +} diff --git a/java/org/apache/catalina/tribes/ChannelReceiver.java b/java/org/apache/catalina/tribes/ChannelReceiver.java new file mode 100644 index 0000000..4995512 --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelReceiver.java @@ -0,0 +1,93 @@ +/* + * 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.catalina.tribes; + +import java.io.IOException; + +/** + * ChannelReceiver Interface
    + * The ChannelReceiver interface is the data receiver component + * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface). + * This class may optionally implement a thread pool for parallel processing of incoming messages. + */ +public interface ChannelReceiver extends Heartbeat { + int MAX_UDP_SIZE = 65535; + + /** + * Start listening for incoming messages on the host/port + * @throws IOException Listen failed + */ + void start() throws IOException; + + /** + * Stop listening for messages + */ + void stop(); + + /** + * String representation of the IPv4 or IPv6 address that this host is listening + * to. + * @return the host that this receiver is listening to + */ + String getHost(); + + + /** + * Returns the listening port + * @return port + */ + int getPort(); + + /** + * Returns the secure listening port + * @return port, -1 if a secure port is not activated + */ + int getSecurePort(); + + /** + * Returns the UDP port + * @return port, -1 if the UDP port is not activated. + */ + int getUdpPort(); + + /** + * Sets the message listener to receive notification of incoming + * @param listener MessageListener + * @see MessageListener + */ + void setMessageListener(MessageListener listener); + + /** + * Returns the message listener that is associated with this receiver + * @return MessageListener + * @see MessageListener + */ + MessageListener getMessageListener(); + + /** + * Return the channel that is related to this ChannelReceiver + * @return Channel + */ + Channel getChannel(); + + /** + * Set the channel that is related to this ChannelReceiver + * @param channel The channel + */ + void setChannel(Channel channel); + +} diff --git a/java/org/apache/catalina/tribes/ChannelSender.java b/java/org/apache/catalina/tribes/ChannelSender.java new file mode 100644 index 0000000..4f7ab4b --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelSender.java @@ -0,0 +1,82 @@ +/* + * 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.catalina.tribes; + +import java.io.IOException; + + +/** + * ChannelReceiver Interface
    + * The ChannelSender interface is the data sender component + * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).
    + * The channel sender must support "silent" members, ie, be able to send a message to a member + * that is not in the membership, but is part of the destination parameter + */ +public interface ChannelSender extends Heartbeat +{ + /** + * Notify the sender of a member being added to the group.
    + * Optional. This can be an empty implementation, that does nothing + * @param member Member + */ + void add(Member member); + /** + * Notification that a member has been removed or crashed. + * Can be used to clean up open connections etc + * @param member Member + */ + void remove(Member member); + + /** + * Start the channel sender + * @throws IOException if preprocessing takes place and an error happens + */ + void start() throws IOException; + + /** + * Stop the channel sender + */ + void stop(); + + /** + * A channel heartbeat, use this method to clean up resources + */ + @Override void heartbeat() ; + + /** + * Send a message to one or more recipients. + * @param message ChannelMessage - the message to be sent + * @param destination Member[] - the destinations + * @throws ChannelException - if an error happens, the ChannelSender MUST report + * individual send failures on a per member basis, using ChannelException.addFaultyMember + * @see ChannelException#addFaultyMember(Member,java.lang.Exception) + */ + void sendMessage(ChannelMessage message, Member[] destination) throws ChannelException; + + /** + * Return the channel that is related to this ChannelSender + * @return Channel + */ + Channel getChannel(); + + /** + * Set the channel that is related to this ChannelSender + * @param channel The channel + */ + void setChannel(Channel channel); + +} diff --git a/java/org/apache/catalina/tribes/ErrorHandler.java b/java/org/apache/catalina/tribes/ErrorHandler.java new file mode 100644 index 0000000..f5d24f3 --- /dev/null +++ b/java/org/apache/catalina/tribes/ErrorHandler.java @@ -0,0 +1,41 @@ +/* + * 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.catalina.tribes; + +/** + * The ErrorHandler class is used when sending messages + * that are sent asynchronously and the application still needs to get + * confirmation when the message was sent successfully or when a message errored out. + */ +public interface ErrorHandler { + + /** + * Invoked if the message is dispatched async, and an error occurs + * @param x ChannelException - the error that happened + * @param id - the unique id for the message + * @see Channel#send(Member[], java.io.Serializable, int, ErrorHandler) + */ + void handleError(ChannelException x, UniqueId id); + + /** + * Invoked when the message has been sent successfully. + * @param id - the unique id for the message + * @see Channel#send(Member[], java.io.Serializable, int, ErrorHandler) + */ + void handleCompletion(UniqueId id); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/Heartbeat.java b/java/org/apache/catalina/tribes/Heartbeat.java new file mode 100644 index 0000000..a343651 --- /dev/null +++ b/java/org/apache/catalina/tribes/Heartbeat.java @@ -0,0 +1,33 @@ +/* + * 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.catalina.tribes; + +/** + * Can be implemented by the ChannelListener and Membership listeners to receive heartbeat + * notifications from the Channel + * + * @see Channel + * @see Channel#heartbeat() + */ +public interface Heartbeat { + + /** + * Heartbeat invocation for resources cleanup etc + */ + void heartbeat(); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/JmxChannel.java b/java/org/apache/catalina/tribes/JmxChannel.java new file mode 100644 index 0000000..8dc822d --- /dev/null +++ b/java/org/apache/catalina/tribes/JmxChannel.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.tribes; + +import javax.management.MBeanRegistration; + + +public interface JmxChannel extends MBeanRegistration { + + /** + * If set to true, this channel is registered with jmx. + * @return true if this channel will be registered with jmx. + */ + boolean isJmxEnabled(); + + /** + * If set to true, this channel is registered with jmx. + * @param jmxEnabled set to true if this channel should be registered with jmx. + */ + void setJmxEnabled(boolean jmxEnabled); + + /** + * Return the jmx domain which this channel is registered. + * @return jmxDomain + */ + String getJmxDomain(); + + /** + * Set the jmx domain which this channel should be registered. + * @param jmxDomain The jmx domain which this channel should be registered. + */ + void setJmxDomain(String jmxDomain); + + /** + * Return the jmx prefix which will be used with channel ObjectName. + * @return jmxPrefix + */ + String getJmxPrefix(); + + /** + * Set the jmx prefix which will be used with channel ObjectName. + * @param jmxPrefix The jmx prefix which will be used with channel ObjectName. + */ + void setJmxPrefix(String jmxPrefix); + +} diff --git a/java/org/apache/catalina/tribes/ManagedChannel.java b/java/org/apache/catalina/tribes/ManagedChannel.java new file mode 100644 index 0000000..ef6914e --- /dev/null +++ b/java/org/apache/catalina/tribes/ManagedChannel.java @@ -0,0 +1,76 @@ +/* + * 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.catalina.tribes; + +import java.util.Iterator; + +/** + * Channel interface + * A managed channel interface gives you access to the components of the channels + * such as senders, receivers, interceptors etc for configurations purposes + */ +public interface ManagedChannel extends Channel { + + /** + * Sets the channel sender + * @param sender ChannelSender + * @see ChannelSender + */ + void setChannelSender(ChannelSender sender); + + /** + * Sets the channel receiver + * @param receiver ChannelReceiver + * @see ChannelReceiver + */ + void setChannelReceiver(ChannelReceiver receiver); + + /** + * Sets the membership service + * @param service MembershipService + * @see MembershipService + */ + void setMembershipService(MembershipService service); + + /** + * returns the channel sender + * @return ChannelSender + * @see ChannelSender + */ + ChannelSender getChannelSender(); + + /** + * returns the channel receiver + * @return ChannelReceiver + * @see ChannelReceiver + */ + ChannelReceiver getChannelReceiver(); + + /** + * Returns the membership service + * @return MembershipService + * @see MembershipService + */ + MembershipService getMembershipService(); + + /** + * Returns the interceptor stack + * @return Iterator + * @see Channel#addInterceptor(ChannelInterceptor) + */ + Iterator getInterceptors(); +} diff --git a/java/org/apache/catalina/tribes/Member.java b/java/org/apache/catalina/tribes/Member.java new file mode 100644 index 0000000..a21ad76 --- /dev/null +++ b/java/org/apache/catalina/tribes/Member.java @@ -0,0 +1,158 @@ +/* + * 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.catalina.tribes; + +import java.io.Serializable; + +/** + * The Member interface, defines a member in the group. + * Each member can carry a set of properties, defined by the actual implementation.
    + * A member is identified by the host/ip/uniqueId
    + * The host is what interface the member is listening to, to receive data
    + * The port is what port the member is listening to, to receive data
    + * The uniqueId defines the session id for the member. This is an important feature + * since a member that has crashed and the starts up again on the same port/host is + * not guaranteed to be the same member, so no state transfers will ever be confused + */ +public interface Member extends Serializable { + + /** + * When a member leaves the cluster, the payload of the memberDisappeared member + * will be the following bytes. This indicates a soft shutdown, and not a crash + */ + byte[] SHUTDOWN_PAYLOAD = new byte[] {66, 65, 66, 89, 45, 65, 76, 69, 88}; + + /** + * @return the name of this node, should be unique within the group. + */ + String getName(); + + /** + * Returns the listen host for the ChannelReceiver implementation + * @return IPv4 or IPv6 representation of the host address this member listens to incoming data + * @see ChannelReceiver + */ + byte[] getHost(); + + /** + * Returns the listen port for the ChannelReceiver implementation + * @return the listen port for this member, -1 if its not listening on an insecure port + * @see ChannelReceiver + */ + int getPort(); + + /** + * Returns the secure listen port for the ChannelReceiver implementation. + * Returns -1 if its not listening to a secure port. + * @return the listen port for this member, -1 if its not listening on a secure port + * @see ChannelReceiver + */ + int getSecurePort(); + + /** + * Returns the UDP port that this member is listening to for UDP messages. + * @return the listen UDP port for this member, -1 if its not listening on a UDP port + */ + int getUdpPort(); + + + /** + * Contains information on how long this member has been online. + * The result is the number of milli seconds this member has been + * broadcasting its membership to the group. + * @return nr of milliseconds since this member started. + */ + long getMemberAliveTime(); + + void setMemberAliveTime(long memberAliveTime); + + /** + * The current state of the member + * @return boolean - true if the member is functioning correctly + */ + boolean isReady(); + /** + * The current state of the member + * @return boolean - true if the member is suspect, but the crash has not been confirmed + */ + boolean isSuspect(); + + /** + * @return boolean - true if the member has been confirmed to malfunction + */ + boolean isFailing(); + + /** + * returns a UUID unique for this member over all sessions. + * If the member crashes and restarts, the uniqueId will be different. + * @return byte[] + */ + byte[] getUniqueId(); + + /** + * returns the payload associated with this member + * @return byte[] + */ + byte[] getPayload(); + + void setPayload(byte[] payload); + + /** + * returns the command associated with this member + * @return byte[] + */ + byte[] getCommand(); + + void setCommand(byte[] command); + + /** + * Domain for this cluster + * @return byte[] + */ + byte[] getDomain(); + + /** + * Highly optimized version of serializing a member into a byte array + * Returns a cached byte[] reference, do not modify this data + * @param getalive calculate memberAlive time + * @return the data as a byte array + */ + byte[] getData(boolean getalive); + + /** + * Highly optimized version of serializing a member into a byte array + * Returns a cached byte[] reference, do not modify this data + * @param getalive calculate memberAlive time + * @param reset reset the cached data package, and create a new one + * @return the data as a byte array + */ + byte[] getData(boolean getalive, boolean reset); + + /** + * Length of a message obtained by {@link #getData(boolean)} or + * {@link #getData(boolean, boolean)}. + * @return the data length + */ + int getDataLength(); + + /** + * @return boolean - true if the member is local member + */ + boolean isLocal(); + + void setLocal(boolean local); +} diff --git a/java/org/apache/catalina/tribes/MembershipListener.java b/java/org/apache/catalina/tribes/MembershipListener.java new file mode 100644 index 0000000..fe0c009 --- /dev/null +++ b/java/org/apache/catalina/tribes/MembershipListener.java @@ -0,0 +1,39 @@ +/* + * 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.catalina.tribes; + +/** + * The MembershipListener interface is used as a callback to the + * membership service. It has two methods that will notify the listener + * when a member has joined the group and when a member has disappeared (crashed) + */ +public interface MembershipListener { + /** + * A member was added to the group + * @param member Member - the member that was added + */ + void memberAdded(Member member); + + /** + * A member was removed from the group
    + * If the member left voluntarily, the Member.getCommand will contain the Member.SHUTDOWN_PAYLOAD data + * @param member Member + * @see Member#SHUTDOWN_PAYLOAD + */ + void memberDisappeared(Member member); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/MembershipProvider.java b/java/org/apache/catalina/tribes/MembershipProvider.java new file mode 100644 index 0000000..f9ccc5b --- /dev/null +++ b/java/org/apache/catalina/tribes/MembershipProvider.java @@ -0,0 +1,38 @@ +/* + * 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.catalina.tribes; + +import java.util.Properties; + +public interface MembershipProvider { + + void init(Properties properties) throws Exception; + + void start(int level) throws Exception; + + boolean stop(int level) throws Exception; + + void setMembershipListener(MembershipListener listener); + + void setMembershipService(MembershipService service); + + boolean hasMembers(); + + Member getMember(Member mbr); + + Member[] getMembers(); +} diff --git a/java/org/apache/catalina/tribes/MembershipService.java b/java/org/apache/catalina/tribes/MembershipService.java new file mode 100644 index 0000000..71d727f --- /dev/null +++ b/java/org/apache/catalina/tribes/MembershipService.java @@ -0,0 +1,164 @@ +/* + * 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.catalina.tribes; + + + +/** + * MembershipService Interface
    + * The MembershipService interface is the membership component + * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).
    + */ +public interface MembershipService { + + int MBR_RX = Channel.MBR_RX_SEQ; + int MBR_TX = Channel.MBR_TX_SEQ; + + /** + * Sets the properties for the membership service. This must be called before + * the start() method is called. + * The properties are implementation specific. + * @param properties - to be used to configure the membership service. + */ + void setProperties(java.util.Properties properties); + + /** + * @return the properties for the configuration used. + */ + java.util.Properties getProperties(); + + /** + * Starts the membership service. If a membership listeners is added + * the listener will start to receive membership events. + * Performs a start level 1 and 2 + * @throws Exception if the service fails to start. + */ + void start() throws Exception; + + /** + * Starts the membership service. If a membership listeners is added + * the listener will start to receive membership events. + * @param level - level MBR_RX starts listening for members, level MBR_TX + * starts broad casting the server + * @throws Exception if the service fails to start. + * @throws java.lang.IllegalArgumentException if the level is incorrect. + */ + void start(int level) throws Exception; + + + /** + * Starts the membership service. If a membership listeners is added + * the listener will start to receive membership events. + * @param level - level MBR_RX stops listening for members, level MBR_TX + * stops broad casting the server + * @throws java.lang.IllegalArgumentException if the level is incorrect. + */ + void stop(int level); + + /** + * @return true if the the group contains members + */ + boolean hasMembers(); + + /** + * Retrieve the specified member from the membership. + * @param mbr The member to retrieve + * @return the member + */ + Member getMember(Member mbr); + + /** + * @return a list of all the members in the cluster. + */ + Member[] getMembers(); + + /** + * Get the local member. + * @return the member object that defines this member + * @param incAliveTime true to set the alive time + * on the local member + */ + Member getLocalMember(boolean incAliveTime); + + /** + * @return all members by name + */ + String[] getMembersByName(); + + /** + * Get a member. + * @param name The member name + * @return the member + */ + Member findMemberByName(String name); + + /** + * Sets the local member properties for broadcasting. + * + * @param listenHost Listen to host + * @param listenPort Listen to port + * @param securePort Use a secure port + * @param udpPort Use UDP + */ + void setLocalMemberProperties(String listenHost, int listenPort, int securePort, int udpPort); + + /** + * Sets the membership listener, only one listener can be added. + * If you call this method twice, the last listener will be used. + * @param listener The listener + */ + void setMembershipListener(MembershipListener listener); + + /** + * Removes the membership listener. + */ + void removeMembershipListener(); + + /** + * Set a payload to be broadcasted with each membership + * broadcast. + * @param payload byte[] + */ + void setPayload(byte[] payload); + + void setDomain(byte[] domain); + + /** + * Broadcasts a message to all members. + * @param message The message to broadcast + * @throws ChannelException Message broadcast failed + */ + void broadcast(ChannelMessage message) throws ChannelException; + + /** + * Return the channel that is related to this MembershipService + * @return Channel + */ + Channel getChannel(); + + /** + * Set the channel that is related to this MembershipService + * @param channel The channel + */ + void setChannel(Channel channel); + + /** + * Get the MembershipProvider + * @return MembershipProvider + */ + MembershipProvider getMembershipProvider(); +} diff --git a/java/org/apache/catalina/tribes/MessageListener.java b/java/org/apache/catalina/tribes/MessageListener.java new file mode 100644 index 0000000..bcd07e9 --- /dev/null +++ b/java/org/apache/catalina/tribes/MessageListener.java @@ -0,0 +1,32 @@ +/* + * 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.catalina.tribes; + +/** + * The listener to be registered with the ChannelReceiver, internal Tribes + * component. + */ +public interface MessageListener { + + /** + * Receive a message from the IO components in the Channel stack + * @param msg ChannelMessage + */ + void messageReceived(ChannelMessage msg); + + boolean accept(ChannelMessage msg); +} diff --git a/java/org/apache/catalina/tribes/RemoteProcessException.java b/java/org/apache/catalina/tribes/RemoteProcessException.java new file mode 100644 index 0000000..8b05fc6 --- /dev/null +++ b/java/org/apache/catalina/tribes/RemoteProcessException.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.tribes; + +/** + * Message thrown by a sender when USE_SYNC_ACK receives a FAIL_ACK_COMMAND. + *
    + * This means that the message was received on the remote node but the processing of the message failed. + * This message will be embedded in a ChannelException.FaultyMember + * + * @see ChannelException + */ +public class RemoteProcessException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public RemoteProcessException() { + super(); + } + + public RemoteProcessException(String message) { + super(message); + } + + public RemoteProcessException(String message, Throwable cause) { + super(message, cause); + } + + public RemoteProcessException(Throwable cause) { + super(cause); + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/UniqueId.java b/java/org/apache/catalina/tribes/UniqueId.java new file mode 100644 index 0000000..e130015 --- /dev/null +++ b/java/org/apache/catalina/tribes/UniqueId.java @@ -0,0 +1,79 @@ +/* + * 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.catalina.tribes; + +import java.io.Serializable; + +import org.apache.catalina.tribes.util.Arrays; + +/** + * Represents a globally unique Id. + */ +public final class UniqueId implements Serializable{ + private static final long serialVersionUID = 1L; + + final byte[] id; + + public UniqueId() { + this(null); + } + + public UniqueId(byte[] id) { + this.id = id; + } + + public UniqueId(byte[] id, int offset, int length) { + this.id = new byte[length]; + System.arraycopy(id,offset,this.id,0,length); + } + + @Override + public int hashCode() { + if ( id == null ) { + return 0; + } + return Arrays.hashCode(id); + } + + @Override + public boolean equals(Object other) { + boolean result = (other instanceof UniqueId); + if ( result ) { + UniqueId uid = (UniqueId)other; + if ( this.id == null && uid.id == null ) { + result = true; + } else if ( this.id == null && uid.id != null ) { + result = false; + } else if ( this.id != null && uid.id == null ) { + result = false; + } else { + result = Arrays.equals(this.id,uid.id); + } + }//end if + return result; + } + + public byte[] getBytes() { + return id; + } + + @Override + public String toString() { + return "UniqueId" + Arrays.toString(id); + } + +} diff --git a/java/org/apache/catalina/tribes/group/AbsoluteOrder.java b/java/org/apache/catalina/tribes/group/AbsoluteOrder.java new file mode 100644 index 0000000..1e06dc1 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/AbsoluteOrder.java @@ -0,0 +1,126 @@ +/* + * 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.catalina.tribes.group; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import org.apache.catalina.tribes.Member; + +/** + *

    Title: Membership - Absolute Order

    + * + *

    Description: A simple, yet agreeable and efficient way of ordering members

    + *

    + * Ordering members can serve as a basis for electing a leader or coordinating efforts.
    + * This is stinky simple, it works on the basis of the Member interface + * and orders members in the following format: + *

    + *
      + *
    1. IP comparison - byte by byte, lower byte higher rank
    2. + *
    3. IPv4 addresses rank higher than IPv6, ie the lesser number of bytes, the higher rank
    4. + *
    5. Port comparison - lower port, higher rank
    6. + *
    7. UniqueId comparison- byte by byte, lower byte higher rank
    8. + *
    + * + * @see org.apache.catalina.tribes.Member + */ +public class AbsoluteOrder { + public static final AbsoluteComparator comp = new AbsoluteComparator(); + + protected AbsoluteOrder() { + super(); + } + + + public static void absoluteOrder(Member[] members) { + if ( members == null || members.length <= 1 ) { + return; + } + Arrays.sort(members,comp); + } + + public static void absoluteOrder(List members) { + if ( members == null || members.size() <= 1 ) { + return; + } + members.sort(comp); + } + + public static class AbsoluteComparator implements Comparator, + Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public int compare(Member m1, Member m2) { + int result = compareIps(m1,m2); + if ( result == 0 ) { + result = comparePorts(m1,m2); + } + if ( result == 0 ) { + result = compareIds(m1,m2); + } + return result; + } + + public int compareIps(Member m1, Member m2) { + return compareBytes(m1.getHost(),m2.getHost()); + } + + public int comparePorts(Member m1, Member m2) { + return compareInts(m1.getPort(),m2.getPort()); + } + + public int compareIds(Member m1, Member m2) { + return compareBytes(m1.getUniqueId(),m2.getUniqueId()); + } + + protected int compareBytes(byte[] d1, byte[] d2) { + int result = 0; + if ( d1.length == d2.length ) { + for (int i=0; (result==0) && (i + * DEFAULT - will start all services
    + * MBR_RX_SEQ - starts the membership receiver
    + * MBR_TX_SEQ - starts the membership broadcaster
    + * SND_TX_SEQ - starts the replication transmitter
    + * SND_RX_SEQ - starts the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already started. + */ + @Override + public void start(int svc) throws ChannelException { + this.internalStart(svc); + } + + /** + * Shuts down the channel. This can be called multiple times for individual services to shutdown + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * DEFAULT - will shutdown all services
    + * MBR_RX_SEQ - stops the membership receiver
    + * MBR_TX_SEQ - stops the membership broadcaster
    + * SND_TX_SEQ - stops the replication transmitter
    + * SND_RX_SEQ - stops the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already started. + */ + @Override + public void stop(int svc) throws ChannelException { + this.internalStop(svc); + } + + + /** + * Starts up the channel. This can be called multiple times for individual services to start + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * DEFAULT - will start all services
    + * MBR_RX_SEQ - starts the membership receiver
    + * MBR_TX_SEQ - starts the membership broadcaster
    + * SND_TX_SEQ - starts the replication transmitter
    + * SND_RX_SEQ - starts the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already started. + */ + protected synchronized void internalStart(int svc) throws ChannelException { + try { + boolean valid = false; + //make sure we don't pass down any flags that are unrelated to the bottom layer + svc = svc & Channel.DEFAULT; + + if (startLevel == Channel.DEFAULT) + { + return; //we have already started up all components + } + if (svc == 0 ) + { + return;//nothing to start + } + + if (svc == (svc & startLevel)) { + throw new ChannelException(sm.getString("channelCoordinator.alreadyStarted", + Integer.toString(svc))); + } + + //must start the receiver first so that we can coordinate the port it + //listens to with the local membership settings + if ( Channel.SND_RX_SEQ==(svc & Channel.SND_RX_SEQ) ) { + clusterReceiver.setMessageListener(this); + clusterReceiver.setChannel(getChannel()); + clusterReceiver.start(); + //synchronize, big time FIXME + Member localMember = getChannel().getLocalMember(false); + if (localMember instanceof StaticMember) { + // static member + StaticMember staticMember = (StaticMember)localMember; + staticMember.setHost(getClusterReceiver().getHost()); + staticMember.setPort(getClusterReceiver().getPort()); + staticMember.setSecurePort(getClusterReceiver().getSecurePort()); + } else { + // multicast member + membershipService.setLocalMemberProperties(getClusterReceiver().getHost(), + getClusterReceiver().getPort(), + getClusterReceiver().getSecurePort(), + getClusterReceiver().getUdpPort()); + } + valid = true; + } + if ( Channel.SND_TX_SEQ==(svc & Channel.SND_TX_SEQ) ) { + clusterSender.setChannel(getChannel()); + clusterSender.start(); + valid = true; + } + + if ( Channel.MBR_RX_SEQ==(svc & Channel.MBR_RX_SEQ) ) { + membershipService.setMembershipListener(this); + membershipService.setChannel(getChannel()); + if (membershipService instanceof McastService) { + ((McastService)membershipService).setMessageListener(this); + } + membershipService.start(MembershipService.MBR_RX); + valid = true; + } + if ( Channel.MBR_TX_SEQ==(svc & Channel.MBR_TX_SEQ) ) { + membershipService.setChannel(getChannel()); + membershipService.start(MembershipService.MBR_TX); + valid = true; + } + + if (!valid) { + throw new IllegalArgumentException(sm.getString("channelCoordinator.invalid.startLevel")); + } + startLevel = (startLevel | svc); + }catch ( ChannelException cx ) { + throw cx; + }catch ( Exception x ) { + throw new ChannelException(x); + } + } + + /** + * Shuts down the channel. This can be called multiple times for individual services to shutdown + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * DEFAULT - will shutdown all services
    + * MBR_RX_SEQ - starts the membership receiver
    + * MBR_TX_SEQ - starts the membership broadcaster
    + * SND_TX_SEQ - starts the replication transmitter
    + * SND_RX_SEQ - starts the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already started. + */ + protected synchronized void internalStop(int svc) throws ChannelException { + try { + //make sure we don't pass down any flags that are unrelated to the bottom layer + svc = svc & Channel.DEFAULT; + + if (startLevel == 0) + { + return; //we have already stopped up all components + } + if (svc == 0 ) + { + return;//nothing to stop + } + + boolean valid = false; + if ( Channel.MBR_TX_SEQ==(svc & Channel.MBR_TX_SEQ) ) { + membershipService.stop(MembershipService.MBR_TX); + valid = true; + } + if ( Channel.MBR_RX_SEQ==(svc & Channel.MBR_RX_SEQ) ) { + membershipService.stop(MembershipService.MBR_RX); + membershipService.setMembershipListener(null); + valid = true; + + } + if ( Channel.SND_TX_SEQ==(svc & Channel.SND_TX_SEQ) ) { + clusterSender.stop(); + valid = true; + } + if ( Channel.SND_RX_SEQ==(svc & Channel.SND_RX_SEQ) ) { + clusterReceiver.stop(); + clusterReceiver.setMessageListener(null); + valid = true; + } + if ( !valid) { + throw new IllegalArgumentException(sm.getString("channelCoordinator.invalid.startLevel")); + } + + startLevel = (startLevel & (~svc)); + setChannel(null); + } catch (Exception x) { + throw new ChannelException(x); + } + } + + @Override + public void memberAdded(Member member){ + SenderState.getSenderState(member); + super.memberAdded(member); + } + + @Override + public void memberDisappeared(Member member){ + SenderState.removeSenderState(member); + super.memberDisappeared(member); + } + + @Override + public void messageReceived(ChannelMessage msg) { + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("ChannelCoordinator - Received msg:" + + new UniqueId(msg.getUniqueId()) + " at " + + new java.sql.Timestamp(System.currentTimeMillis()) + " from " + + msg.getAddress().getName()); + } + super.messageReceived(msg); + } + + @Override + public boolean accept(ChannelMessage msg) { + return true; + } + + public ChannelReceiver getClusterReceiver() { + return clusterReceiver; + } + + public ChannelSender getClusterSender() { + return clusterSender; + } + + public MembershipService getMembershipService() { + return membershipService; + } + + public void setClusterReceiver(ChannelReceiver clusterReceiver) { + if ( clusterReceiver != null ) { + this.clusterReceiver = clusterReceiver; + this.clusterReceiver.setMessageListener(this); + } else { + if (this.clusterReceiver!=null ) { + this.clusterReceiver.setMessageListener(null); + } + this.clusterReceiver = null; + } + } + + public void setClusterSender(ChannelSender clusterSender) { + this.clusterSender = clusterSender; + } + + public void setMembershipService(MembershipService membershipService) { + this.membershipService = membershipService; + this.membershipService.setMembershipListener(this); + } + + @Override + public void heartbeat() { + if ( clusterSender!=null ) { + clusterSender.heartbeat(); + } + super.heartbeat(); + } + + /** + * has members + */ + @Override + public boolean hasMembers() { + return this.getMembershipService().hasMembers(); + } + + /** + * Get all current cluster members + * @return all members or empty array + */ + @Override + public Member[] getMembers() { + return this.getMembershipService().getMembers(); + } + + /** + * @param mbr Member + * @return Member + */ + @Override + public Member getMember(Member mbr){ + return this.getMembershipService().getMember(mbr); + } + + + /** + * Return the member that represents this node. + * + * @return Member + */ + @Override + public Member getLocalMember(boolean incAlive) { + return this.getMembershipService().getLocalMember(incAlive); + } + + +} diff --git a/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java b/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java new file mode 100644 index 0000000..b883412 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java @@ -0,0 +1,245 @@ +/* + * 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.catalina.tribes.group; + +import javax.management.ObjectName; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.jmx.JmxRegistry; + +/** + * Abstract class for the interceptor base class. + */ +public abstract class ChannelInterceptorBase implements ChannelInterceptor { + + private ChannelInterceptor next; + private ChannelInterceptor previous; + private Channel channel; + //default value, always process + protected int optionFlag = 0; + + /** + * the ObjectName of this ChannelInterceptor. + */ + private ObjectName oname = null; + + public ChannelInterceptorBase() { + + } + + public boolean okToProcess(int messageFlags) { + if (this.optionFlag == 0 ) { + return true; + } + return ((optionFlag&messageFlags) == optionFlag); + } + + @Override + public final void setNext(ChannelInterceptor next) { + this.next = next; + } + + @Override + public final ChannelInterceptor getNext() { + return next; + } + + @Override + public final void setPrevious(ChannelInterceptor previous) { + this.previous = previous; + } + + @Override + public void setOptionFlag(int optionFlag) { + this.optionFlag = optionFlag; + } + + @Override + public final ChannelInterceptor getPrevious() { + return previous; + } + + @Override + public int getOptionFlag() { + return optionFlag; + } + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws + ChannelException { + if (getNext() != null) { + getNext().sendMessage(destination, msg, payload); + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + if (getPrevious() != null) { + getPrevious().messageReceived(msg); + } + } + + @Override + public void memberAdded(Member member) { + //notify upwards + if (getPrevious() != null) { + getPrevious().memberAdded(member); + } + } + + @Override + public void memberDisappeared(Member member) { + //notify upwards + if (getPrevious() != null) { + getPrevious().memberDisappeared(member); + } + } + + @Override + public void heartbeat() { + if (getNext() != null) { + getNext().heartbeat(); + } + } + + /** + * has members + */ + @Override + public boolean hasMembers() { + if ( getNext()!=null ) { + return getNext().hasMembers(); + } else { + return false; + } + } + + /** + * Get all current cluster members + * @return all members or empty array + */ + @Override + public Member[] getMembers() { + if ( getNext()!=null ) { + return getNext().getMembers(); + } else { + return null; + } + } + + /** + * @param mbr Member + * @return Member + */ + @Override + public Member getMember(Member mbr) { + if ( getNext()!=null) { + return getNext().getMember(mbr); + } else { + return null; + } + } + + /** + * Return the member that represents this node. + * + * @return Member + */ + @Override + public Member getLocalMember(boolean incAlive) { + if ( getNext()!=null ) { + return getNext().getLocalMember(incAlive); + } else { + return null; + } + } + + /** + * Starts up the channel. This can be called multiple times for individual services to start + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * DEFAULT - will start all services
    + * MBR_RX_SEQ - starts the membership receiver
    + * MBR_TX_SEQ - starts the membership broadcaster
    + * SND_TX_SEQ - starts the replication transmitter
    + * SND_RX_SEQ - starts the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already started. + */ + @Override + public void start(int svc) throws ChannelException { + if ( getNext()!=null ) { + getNext().start(svc); + } + // register jmx + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + this.oname = jmxRegistry.registerJmx( + ",component=Interceptor,interceptorName=" + getClass().getSimpleName(), this); + } + } + + /** + * Shuts down the channel. This can be called multiple times for individual services to shutdown + * The svc parameter can be the logical or value of any constants + * @param svc int value of
    + * DEFAULT - will shutdown all services
    + * MBR_RX_SEQ - stops the membership receiver
    + * MBR_TX_SEQ - stops the membership broadcaster
    + * SND_TX_SEQ - stops the replication transmitter
    + * SND_RX_SEQ - stops the replication receiver
    + * @throws ChannelException if a startup error occurs or the service is already started. + */ + @Override + public void stop(int svc) throws ChannelException { + if (getNext() != null) { + getNext().stop(svc); + } + if (oname != null) { + JmxRegistry.getRegistry(channel).unregisterJmx(oname); + oname = null; + } + channel = null; + } + + @Override + public void fireInterceptorEvent(InterceptorEvent event) { + //empty operation + } + + /** + * Return the channel that is related to this interceptor + * @return Channel + */ + @Override + public Channel getChannel() { + return channel; + } + + /** + * Set the channel that is related to this interceptor + * @param channel The channel + */ + @Override + public void setChannel(Channel channel) { + this.channel = channel; + } + +} diff --git a/java/org/apache/catalina/tribes/group/ExtendedRpcCallback.java b/java/org/apache/catalina/tribes/group/ExtendedRpcCallback.java new file mode 100644 index 0000000..c5e460b --- /dev/null +++ b/java/org/apache/catalina/tribes/group/ExtendedRpcCallback.java @@ -0,0 +1,45 @@ +/* + * 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.catalina.tribes.group; + +import java.io.Serializable; + +import org.apache.catalina.tribes.Member; +/** + * Extension to the {@link RpcCallback} interface. Allows an RPC messenger to get a confirmation if the reply + * was sent successfully to the original sender. + * + */ +public interface ExtendedRpcCallback extends RpcCallback { + + /** + * The reply failed. + * @param request - the original message that requested the reply + * @param response - the reply message to the original message + * @param sender - the sender requested that reply + * @param reason - the reason the reply failed + */ + void replyFailed(Serializable request, Serializable response, Member sender, Exception reason); + + /** + * The reply succeeded + * @param request - the original message that requested the reply + * @param response - the reply message to the original message + * @param sender - the sender requested that reply + */ + void replySucceeded(Serializable request, Serializable response, Member sender); +} diff --git a/java/org/apache/catalina/tribes/group/GroupChannel.java b/java/org/apache/catalina/tribes/group/GroupChannel.java new file mode 100644 index 0000000..63ab2b1 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/GroupChannel.java @@ -0,0 +1,847 @@ +/* + * 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.catalina.tribes.group; + + +import java.io.IOException; +import java.io.Serializable; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.catalina.tribes.ByteMessage; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ChannelReceiver; +import org.apache.catalina.tribes.ChannelSender; +import org.apache.catalina.tribes.ErrorHandler; +import org.apache.catalina.tribes.Heartbeat; +import org.apache.catalina.tribes.JmxChannel; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.MembershipService; +import org.apache.catalina.tribes.RemoteProcessException; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor; +import org.apache.catalina.tribes.io.BufferPool; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.jmx.JmxRegistry; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.catalina.tribes.util.Logs; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * The default implementation of a Channel.
    + * The GroupChannel manages the replication channel. It coordinates + * message being sent and received with membership announcements. + * The channel has an chain of interceptors that can modify the message or perform other logic.
    + * It manages a complete group, both membership and replication. + */ +public class GroupChannel extends ChannelInterceptorBase + implements ManagedChannel, JmxChannel, GroupChannelMBean { + + private static final Log log = LogFactory.getLog(GroupChannel.class); + protected static final StringManager sm = StringManager.getManager(GroupChannel.class); + + /** + * Flag to determine if the channel manages its own heartbeat + * If set to true, the channel will start a local thread for the heart beat. + */ + protected boolean heartbeat = true; + + /** + * If heartbeat == true then how often do we want this + * heartbeat to run. The default value is 5000 milliseconds. + */ + protected long heartbeatSleeptime = 5*1000; + + /** + * Internal heartbeat future + */ + protected ScheduledFuture heartbeatFuture = null; + protected ScheduledFuture monitorFuture; + + /** + * The ChannelCoordinator coordinates the bottom layer components:
    + * - MembershipService
    + * - ChannelSender
    + * - ChannelReceiver
    + */ + protected final ChannelCoordinator coordinator = new ChannelCoordinator(); + + /** + * The first interceptor in the interceptor stack. + * The interceptors are chained in a linked list, so we only need a reference to the + * first one + */ + protected ChannelInterceptor interceptors = null; + + /** + * A list of membership listeners that subscribe to membership announcements + */ + protected final List membershipListeners = new CopyOnWriteArrayList<>(); + + /** + * A list of channel listeners that subscribe to incoming messages + */ + protected final List channelListeners = new CopyOnWriteArrayList<>(); + + /** + * If set to true, the GroupChannel will check to make sure that + */ + protected boolean optionCheck = false; + + /** + * the name of this channel. + */ + protected String name = null; + + /** + * the jmx domain which this channel is registered. + */ + private String jmxDomain = "ClusterChannel"; + + /** + * the jmx prefix which will be used with channel ObjectName. + */ + private String jmxPrefix = ""; + + /** + * If set to true, this channel is registered with jmx. + */ + private boolean jmxEnabled = true; + + /** + * Executor service. + */ + protected ScheduledExecutorService utilityExecutor = null; + + /** + * the ObjectName of this channel. + */ + private ObjectName oname = null; + + /** + * Creates a GroupChannel. This constructor will also + * add the first interceptor in the GroupChannel.
    + * The first interceptor is always the channel itself. + */ + public GroupChannel() { + addInterceptor(this); + } + + + /** + * Adds an interceptor to the stack for message processing
    + * Interceptors are ordered in the way they are added.
    + * channel.addInterceptor(A);
    + * channel.addInterceptor(C);
    + * channel.addInterceptor(B);
    + * Will result in an interceptor stack like this:
    + * A -> C -> B
    + * The complete stack will look like this:
    + * Channel -> A -> C -> B -> ChannelCoordinator
    + * @param interceptor ChannelInterceptorBase + */ + @Override + public void addInterceptor(ChannelInterceptor interceptor) { + if ( interceptors == null ) { + interceptors = interceptor; + interceptors.setNext(coordinator); + interceptors.setPrevious(null); + coordinator.setPrevious(interceptors); + } else { + ChannelInterceptor last = interceptors; + while ( last.getNext() != coordinator ) { + last = last.getNext(); + } + last.setNext(interceptor); + interceptor.setNext(coordinator); + interceptor.setPrevious(last); + coordinator.setPrevious(interceptor); + } + } + + /** + * Sends a heartbeat through the interceptor stack.
    + * Invoke this method from the application on a periodic basis if + * you have turned off internal heartbeats channel.setHeartbeat(false) + */ + @Override + public void heartbeat() { + super.heartbeat(); + + for (MembershipListener listener : membershipListeners) { + if ( listener instanceof Heartbeat ) { + ((Heartbeat)listener).heartbeat(); + } + } + + for (ChannelListener listener : channelListeners) { + if ( listener instanceof Heartbeat ) { + ((Heartbeat)listener).heartbeat(); + } + } + } + + + /** + * Send a message to the destinations specified + * @param destination Member[] - destination.length > 0 + * @param msg Serializable - the message to send + * @param options sender options, options can trigger guarantee levels and different + * interceptors to react to the message see class documentation for the + * Channel object.
    + * @return UniqueId - the unique Id that was assigned to this message + * @throws ChannelException - if an error occurs processing the message + * @see org.apache.catalina.tribes.Channel + */ + @Override + public UniqueId send(Member[] destination, Serializable msg, int options) + throws ChannelException { + return send(destination,msg,options,null); + } + + /** + * @param destination Member[] - destination.length > 0 + * @param msg Serializable - the message to send + * @param options sender options, options can trigger guarantee levels and different + * interceptors to react to the message see class documentation for the + * Channel object.
    + * @param handler - callback object for error handling and completion notification, + * used when a message is sent asynchronously using the + * Channel.SEND_OPTIONS_ASYNCHRONOUS flag enabled. + * @return UniqueId - the unique Id that was assigned to this message + * @throws ChannelException - if an error occurs processing the message + * @see org.apache.catalina.tribes.Channel + */ + @Override + public UniqueId send(Member[] destination, Serializable msg, int options, ErrorHandler handler) + throws ChannelException { + if ( msg == null ) { + throw new ChannelException(sm.getString("groupChannel.nullMessage")); + } + XByteBuffer buffer = null; + try { + if (destination == null || destination.length == 0) { + throw new ChannelException(sm.getString("groupChannel.noDestination")); + } + ChannelData data = new ChannelData(true);//generates a unique Id + data.setAddress(getLocalMember(false)); + data.setTimestamp(System.currentTimeMillis()); + byte[] b = null; + if ( msg instanceof ByteMessage ){ + b = ((ByteMessage)msg).getMessage(); + options = options | SEND_OPTIONS_BYTE_MESSAGE; + } else { + b = XByteBuffer.serialize(msg); + options = options & (~SEND_OPTIONS_BYTE_MESSAGE); + } + data.setOptions(options); + //XByteBuffer buffer = new XByteBuffer(b.length+128,false); + buffer = BufferPool.getBufferPool().getBuffer(b.length+128, false); + buffer.append(b,0,b.length); + data.setMessage(buffer); + InterceptorPayload payload = null; + if ( handler != null ) { + payload = new InterceptorPayload(); + payload.setErrorHandler(handler); + } + getFirstInterceptor().sendMessage(destination, data, payload); + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("GroupChannel - Sent msg:" + new UniqueId(data.getUniqueId()) + + " at " + new java.sql.Timestamp(System.currentTimeMillis()) + " to " + + Arrays.toNameString(destination)); + Logs.MESSAGES.trace("GroupChannel - Send Message:" + + new UniqueId(data.getUniqueId()) + " is " + msg); + } + + return new UniqueId(data.getUniqueId()); + } catch (RuntimeException | IOException e) { + throw new ChannelException(e); + } finally { + if ( buffer != null ) { + BufferPool.getBufferPool().returnBuffer(buffer); + } + } + } + + + /** + * Callback from the interceptor stack.
    + * When a message is received from a remote node, this method will be + * invoked by the previous interceptor.
    + * This method can also be used to send a message to other components + * within the same application, but its an extreme case, and you're probably + * better off doing that logic between the applications itself. + * @param msg ChannelMessage + */ + @Override + public void messageReceived(ChannelMessage msg) { + if ( msg == null ) { + return; + } + try { + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("GroupChannel - Received msg:" + + new UniqueId(msg.getUniqueId()) + " at " + + new java.sql.Timestamp(System.currentTimeMillis()) + " from " + + msg.getAddress().getName()); + } + + Serializable fwd = null; + if ( (msg.getOptions() & SEND_OPTIONS_BYTE_MESSAGE) == SEND_OPTIONS_BYTE_MESSAGE ) { + fwd = new ByteMessage(msg.getMessage().getBytes()); + } else { + try { + fwd = XByteBuffer.deserialize(msg.getMessage().getBytesDirect(), 0, + msg.getMessage().getLength()); + }catch (Exception sx) { + log.error(sm.getString("groupChannel.unable.deserialize", msg),sx); + return; + } + } + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("GroupChannel - Receive Message:" + + new UniqueId(msg.getUniqueId()) + " is " + fwd); + } + + //get the actual member with the correct alive time + Member source = msg.getAddress(); + boolean rx = false; + boolean delivered = false; + for (ChannelListener channelListener : channelListeners) { + if (channelListener != null && channelListener.accept(fwd, source)) { + channelListener.messageReceived(fwd, source); + delivered = true; + //if the message was accepted by an RPC channel, that channel + //is responsible for returning the reply, otherwise we send an absence reply + if (channelListener instanceof RpcChannel) { + rx = true; + } + } + }//for + if ((!rx) && (fwd instanceof RpcMessage)) { + //if we have a message that requires a response, + //but none was given, send back an immediate one + sendNoRpcChannelReply((RpcMessage)fwd,source); + } + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("GroupChannel delivered[" + delivered + "] id:" + + new UniqueId(msg.getUniqueId())); + } + + } catch ( Exception x ) { + //this could be the channel listener throwing an exception, we should log it + //as a warning. + if ( log.isWarnEnabled() ) { + log.warn(sm.getString("groupChannel.receiving.error"),x); + } + throw new RemoteProcessException(sm.getString("groupChannel.receiving.error"),x); + } + } + + /** + * Sends a NoRpcChannelReply message to a member
    + * This method gets invoked by the channel if an RPC message comes in + * and no channel listener accepts the message. This avoids timeout + * @param msg RpcMessage + * @param destination Member - the destination for the reply + */ + protected void sendNoRpcChannelReply(RpcMessage msg, Member destination) { + try { + //avoid circular loop + if ( msg instanceof RpcMessage.NoRpcChannelReply) { + return; + } + RpcMessage.NoRpcChannelReply reply = new RpcMessage.NoRpcChannelReply(msg.rpcId, msg.uuid); + send(new Member[]{destination}, reply, SEND_OPTIONS_ASYNCHRONOUS); + } catch ( Exception x ) { + log.error(sm.getString("groupChannel.sendFail.noRpcChannelReply"),x); + } + } + + /** + * memberAdded gets invoked by the interceptor below the channel + * and the channel will broadcast it to the membership listeners + * @param member Member - the new member + */ + @Override + public void memberAdded(Member member) { + //notify upwards + for (MembershipListener membershipListener : membershipListeners) { + if (membershipListener != null) { + membershipListener.memberAdded(member); + } + } + } + + /** + * memberDisappeared gets invoked by the interceptor below the channel + * and the channel will broadcast it to the membership listeners + * @param member Member - the member that left or crashed + */ + @Override + public void memberDisappeared(Member member) { + //notify upwards + for (MembershipListener membershipListener : membershipListeners) { + if (membershipListener != null) { + membershipListener.memberDisappeared(member); + } + } + } + + /** + * Sets up the default implementation interceptor stack + * if no interceptors have been added + * @throws ChannelException Cluster error + */ + protected synchronized void setupDefaultStack() throws ChannelException { + if (getFirstInterceptor() != null && + ((getFirstInterceptor().getNext() instanceof ChannelCoordinator))) { + addInterceptor(new MessageDispatchInterceptor()); + } + Iterator interceptors = getInterceptors(); + while (interceptors.hasNext()) { + ChannelInterceptor channelInterceptor = interceptors.next(); + channelInterceptor.setChannel(this); + } + coordinator.setChannel(this); + } + + /** + * Validates the option flags that each interceptor is using and reports + * an error if two interceptor share the same flag. + * @throws ChannelException Error with option flag + */ + protected void checkOptionFlags() throws ChannelException { + StringBuilder conflicts = new StringBuilder(); + ChannelInterceptor first = interceptors; + while ( first != null ) { + int flag = first.getOptionFlag(); + if ( flag != 0 ) { + ChannelInterceptor next = first.getNext(); + while ( next != null ) { + int nflag = next.getOptionFlag(); + if (nflag!=0 && (((flag & nflag) == flag ) || ((flag & nflag) == nflag)) ) { + conflicts.append('['); + conflicts.append(first.getClass().getName()); + conflicts.append(':'); + conflicts.append(flag); + conflicts.append(" == "); + conflicts.append(next.getClass().getName()); + conflicts.append(':'); + conflicts.append(nflag); + conflicts.append("] "); + }//end if + next = next.getNext(); + }//while + }//end if + first = first.getNext(); + }//while + if ( conflicts.length() > 0 ) { + throw new ChannelException(sm.getString("groupChannel.optionFlag.conflict", + conflicts.toString())); + } + + } + + protected boolean ownExecutor = false; + + /** + * Starts the channel. + * @param svc int - what service to start + * @throws ChannelException Start error + * @see org.apache.catalina.tribes.Channel#start(int) + */ + @Override + public synchronized void start(int svc) throws ChannelException { + setupDefaultStack(); + if (optionCheck) { + checkOptionFlags(); + } + // register jmx + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(this); + if (jmxRegistry != null) { + this.oname = jmxRegistry.registerJmx(",component=Channel", this); + } + if (utilityExecutor == null) { + log.warn(sm.getString("groupChannel.warn.noUtilityExecutor")); + utilityExecutor = new ScheduledThreadPoolExecutor(1); + ownExecutor = true; + } + super.start(svc); + monitorFuture = utilityExecutor.scheduleWithFixedDelay(this::startHeartbeat, 0, 60, TimeUnit.SECONDS); + } + + protected void startHeartbeat() { + if (heartbeat && (heartbeatFuture == null || (heartbeatFuture != null && heartbeatFuture.isDone()))) { + if (heartbeatFuture != null && heartbeatFuture.isDone()) { + // There was an error executing the scheduled task, get it and log it + try { + heartbeatFuture.get(); + } catch (InterruptedException | ExecutionException e) { + log.error(sm.getString("groupChannel.unable.sendHeartbeat"), e); + } + } + heartbeatFuture = utilityExecutor.scheduleWithFixedDelay(new HeartbeatRunnable(), + heartbeatSleeptime, heartbeatSleeptime, TimeUnit.MILLISECONDS); + } + } + + /** + * Stops the channel. + * @param svc int + * @throws ChannelException Stop error + * @see org.apache.catalina.tribes.Channel#stop(int) + */ + @Override + public synchronized void stop(int svc) throws ChannelException { + if (monitorFuture != null) { + monitorFuture.cancel(true); + monitorFuture = null; + } + if (heartbeatFuture != null) { + heartbeatFuture.cancel(true); + heartbeatFuture = null; + } + super.stop(svc); + if (ownExecutor) { + utilityExecutor.shutdown(); + utilityExecutor = null; + ownExecutor = false; + } + if (oname != null) { + JmxRegistry.getRegistry(this).unregisterJmx(oname); + oname = null; + } + } + + /** + * Returns the first interceptor of the stack. Useful for traversal. + * @return ChannelInterceptor + */ + public ChannelInterceptor getFirstInterceptor() { + if (interceptors != null) { + return interceptors; + } else { + return coordinator; + } + } + + @Override + public ScheduledExecutorService getUtilityExecutor() { + return utilityExecutor; + } + + @Override + public void setUtilityExecutor(ScheduledExecutorService utilityExecutor) { + this.utilityExecutor = utilityExecutor; + } + + /** + * Returns the channel receiver component + * @return ChannelReceiver + */ + @Override + public ChannelReceiver getChannelReceiver() { + return coordinator.getClusterReceiver(); + } + + /** + * Returns the channel sender component + * @return ChannelSender + */ + @Override + public ChannelSender getChannelSender() { + return coordinator.getClusterSender(); + } + + /** + * Returns the membership service component + * @return MembershipService + */ + @Override + public MembershipService getMembershipService() { + return coordinator.getMembershipService(); + } + + /** + * Sets the channel receiver component + * @param clusterReceiver ChannelReceiver + */ + @Override + public void setChannelReceiver(ChannelReceiver clusterReceiver) { + coordinator.setClusterReceiver(clusterReceiver); + } + + /** + * Sets the channel sender component + * @param clusterSender ChannelSender + */ + @Override + public void setChannelSender(ChannelSender clusterSender) { + coordinator.setClusterSender(clusterSender); + } + + /** + * Sets the membership component + * @param membershipService MembershipService + */ + @Override + public void setMembershipService(MembershipService membershipService) { + coordinator.setMembershipService(membershipService); + } + + /** + * Adds a membership listener to the channel.
    + * Membership listeners are uniquely identified using the equals(Object) method + * @param membershipListener MembershipListener + */ + @Override + public void addMembershipListener(MembershipListener membershipListener) { + if (!this.membershipListeners.contains(membershipListener) ) { + this.membershipListeners.add(membershipListener); + } + } + + /** + * Removes a membership listener from the channel.
    + * Membership listeners are uniquely identified using the equals(Object) method + * @param membershipListener MembershipListener + */ + + @Override + public void removeMembershipListener(MembershipListener membershipListener) { + membershipListeners.remove(membershipListener); + } + + /** + * Adds a channel listener to the channel.
    + * Channel listeners are uniquely identified using the equals(Object) method + * @param channelListener ChannelListener + */ + @Override + public void addChannelListener(ChannelListener channelListener) { + if (!this.channelListeners.contains(channelListener) ) { + this.channelListeners.add(channelListener); + } else { + throw new IllegalArgumentException(sm.getString("groupChannel.listener.alreadyExist", + channelListener,channelListener.getClass().getName())); + } + } + + /** + * Removes a channel listener from the channel.
    + * Channel listeners are uniquely identified using the equals(Object) method + * @param channelListener ChannelListener + */ + @Override + public void removeChannelListener(ChannelListener channelListener) { + channelListeners.remove(channelListener); + } + + /** + * Returns an iterator of all the interceptors in this stack + * @return Iterator + */ + @Override + public Iterator getInterceptors() { + return new InterceptorIterator(this.getNext(),this.coordinator); + } + + /** + * Enables/disables the option check
    + * Setting this to true, will make the GroupChannel perform a conflict check + * on the interceptors. If two interceptors are using the same option flag + * and throw an error upon start. + * @param optionCheck boolean + */ + public void setOptionCheck(boolean optionCheck) { + this.optionCheck = optionCheck; + } + + /** + * Configure local heartbeat sleep time
    + * Only used when getHeartbeat()==true + * @param heartbeatSleeptime long - time in milliseconds to sleep between heartbeats + */ + public void setHeartbeatSleeptime(long heartbeatSleeptime) { + this.heartbeatSleeptime = heartbeatSleeptime; + } + + /** + * Enables or disables local heartbeat. + * if setHeartbeat(true) is invoked then the channel will start an internal + * thread to invoke Channel.heartbeat() every getHeartbeatSleeptime milliseconds + * @param heartbeat boolean + */ + @Override + public void setHeartbeat(boolean heartbeat) { + this.heartbeat = heartbeat; + } + + /** + * @see #setOptionCheck(boolean) + * @return boolean + */ + @Override + public boolean getOptionCheck() { + return optionCheck; + } + + /** + * @see #setHeartbeat(boolean) + * @return boolean + */ + @Override + public boolean getHeartbeat() { + return heartbeat; + } + + /** + * Returns the sleep time in milliseconds that the internal heartbeat will + * sleep in between invocations of Channel.heartbeat() + * @return long + */ + @Override + public long getHeartbeatSleeptime() { + return heartbeatSleeptime; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public boolean isJmxEnabled() { + return jmxEnabled; + } + + @Override + public void setJmxEnabled(boolean jmxEnabled) { + this.jmxEnabled = jmxEnabled; + } + + @Override + public String getJmxDomain() { + return jmxDomain; + } + + @Override + public void setJmxDomain(String jmxDomain) { + this.jmxDomain = jmxDomain; + } + + @Override + public String getJmxPrefix() { + return jmxPrefix; + } + + @Override + public void setJmxPrefix(String jmxPrefix) { + this.jmxPrefix = jmxPrefix; + } + + @Override + public ObjectName preRegister(MBeanServer server, ObjectName name) + throws Exception { + // NOOP + return null; + } + + @Override + public void postRegister(Boolean registrationDone) { + // NOOP + } + + @Override + public void preDeregister() throws Exception { + // NOOP + } + + @Override + public void postDeregister() { + JmxRegistry.removeRegistry(this, true); + } + + /** + * An iterator to loop through the interceptors in a channel. + */ + public static class InterceptorIterator implements Iterator { + private final ChannelInterceptor end; + private ChannelInterceptor start; + public InterceptorIterator(ChannelInterceptor start, ChannelInterceptor end) { + this.end = end; + this.start = start; + } + + @Override + public boolean hasNext() { + return start!=null && start != end; + } + + @Override + public ChannelInterceptor next() { + ChannelInterceptor result = null; + if ( hasNext() ) { + result = start; + start = start.getNext(); + } + return result; + } + + @Override + public void remove() { + //empty operation + } + } + + /** + *

    Title: Internal heartbeat runnable

    + * + *

    Description: if Channel.getHeartbeat()==true then a thread of this class + * is created

    + */ + public class HeartbeatRunnable implements Runnable { + @Override + public void run() { + heartbeat(); + } + } + +} diff --git a/java/org/apache/catalina/tribes/group/GroupChannelMBean.java b/java/org/apache/catalina/tribes/group/GroupChannelMBean.java new file mode 100644 index 0000000..83bbaa6 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/GroupChannelMBean.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.tribes.group; + +import java.io.Serializable; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.ErrorHandler; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.UniqueId; + +public interface GroupChannelMBean { + + // Attributes + boolean getOptionCheck(); + + boolean getHeartbeat(); + + long getHeartbeatSleeptime(); + + // Operations + void start(int svc) throws ChannelException; + + void stop(int svc) throws ChannelException; + + UniqueId send(Member[] destination, Serializable msg, int options) + throws ChannelException; + + UniqueId send(Member[] destination, Serializable msg, int options, ErrorHandler handler) + throws ChannelException; + + void addMembershipListener(MembershipListener listener); + + void addChannelListener(ChannelListener listener); + + void removeMembershipListener(MembershipListener listener); + + void removeChannelListener(ChannelListener listener); + + boolean hasMembers() ; + + Member[] getMembers() ; + + Member getLocalMember(boolean incAlive); + +} diff --git a/java/org/apache/catalina/tribes/group/InterceptorPayload.java b/java/org/apache/catalina/tribes/group/InterceptorPayload.java new file mode 100644 index 0000000..d0b44c8 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/InterceptorPayload.java @@ -0,0 +1,31 @@ +/* + * 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.catalina.tribes.group; + +import org.apache.catalina.tribes.ErrorHandler; + +public class InterceptorPayload { + private ErrorHandler errorHandler; + + public ErrorHandler getErrorHandler() { + return errorHandler; + } + + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/LocalStrings.properties b/java/org/apache/catalina/tribes/group/LocalStrings.properties new file mode 100644 index 0000000..6a7defb --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings.properties @@ -0,0 +1,29 @@ +# 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. + +channelCoordinator.alreadyStarted=Channel already started for level:[{0}] +channelCoordinator.invalid.startLevel=Invalid start level, valid levels are:SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ + +groupChannel.listener.alreadyExist=Listener already exists:[{0}][{1}] +groupChannel.noDestination=No destination given +groupChannel.nullMessage=Cannot send a NULL message +groupChannel.optionFlag.conflict=Interceptor option flag conflict: [{0}] +groupChannel.receiving.error=Error receiving message: +groupChannel.sendFail.noRpcChannelReply=Unable to find rpc channel, failed to send NoRpcChannelReply. +groupChannel.unable.deserialize=Unable to deserialize message:[{0}] +groupChannel.unable.sendHeartbeat=Unable to send heartbeat through Tribes interceptor stack. Will try to sleep again. +groupChannel.warn.noUtilityExecutor=No utility executor was set, creating one + +rpcChannel.replyFailed=Unable to send back reply in RpcChannel. diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/group/LocalStrings_cs.properties new file mode 100644 index 0000000..ba52aec --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_cs.properties @@ -0,0 +1,16 @@ +# 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. + +channelCoordinator.invalid.startLevel=Neplatná poÄáteÄní úroveň, platné úrovnÄ›: SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_de.properties b/java/org/apache/catalina/tribes/group/LocalStrings_de.properties new file mode 100644 index 0000000..ea99a5b --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_de.properties @@ -0,0 +1,18 @@ +# 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. + +channelCoordinator.invalid.startLevel=Ungültiger Start-Level, gültige Level sind: SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ + +groupChannel.listener.alreadyExist=Listener existiert bereits:[{0}][{1}] diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_es.properties b/java/org/apache/catalina/tribes/group/LocalStrings_es.properties new file mode 100644 index 0000000..7aae463 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_es.properties @@ -0,0 +1,20 @@ +# 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. + +channelCoordinator.alreadyStarted=Canal ya iniciado para nivel:[{0}] +channelCoordinator.invalid.startLevel=Nivel de inicion inválido, los niveles válidos son:SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ\n + +groupChannel.listener.alreadyExist=El escuchador ya existe:[{0}][{1}] +groupChannel.unable.sendHeartbeat=Imposible enviar heartbeat através del Tribes interceptor stack. Tratré de dormir y esperar nuevamente.\n diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/group/LocalStrings_fr.properties new file mode 100644 index 0000000..386e006 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_fr.properties @@ -0,0 +1,29 @@ +# 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. + +channelCoordinator.alreadyStarted=Canal déjà démarré pour le niveau : [{0}] +channelCoordinator.invalid.startLevel=Niveau de départ invalide, les niveaux valides sont : SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ + +groupChannel.listener.alreadyExist=L''écouteur existe déjà : [{0}][{1}] +groupChannel.noDestination=Aucune destination donnée +groupChannel.nullMessage=Impossible d'envoyer un message null +groupChannel.optionFlag.conflict=Conflit sur le drapeau optionnel d''un intercepteur : [{0}] +groupChannel.receiving.error=Erreur lors de la réception du message : +groupChannel.sendFail.noRpcChannelReply=Incapable de trouver le canal RPM, échec d'envoi de NoRpcChannelReply +groupChannel.unable.deserialize=Impossible de désérialiser le message [{0}] +groupChannel.unable.sendHeartbeat=Impossible d'envoyer l’événement périodique dans la pile d'intercepteurs de Tribes +groupChannel.warn.noUtilityExecutor=Aucun exécuteur utilitaire configuré, un nouveau sera crée + +rpcChannel.replyFailed=Impossible de renvoyer la réponse dans le RpcChannel diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/group/LocalStrings_ja.properties new file mode 100644 index 0000000..69e413e --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_ja.properties @@ -0,0 +1,29 @@ +# 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. + +channelCoordinator.alreadyStarted=ãƒãƒ£ãƒ³ãƒãƒ«ã¯æ—¢ã«ãƒ¬ãƒ™ãƒ«ï¼š[{0}]ã§é–‹å§‹ã•ã‚Œã¾ã—ãŸã€‚ +channelCoordinator.invalid.startLevel=無効ãªé–‹å§‹ãƒ¬ãƒ™ãƒ«ã§ã™ã€‚有効ãªãƒ¬ãƒ™ãƒ«ã¯ SND_RX_SEQã€SND_TX_SEQã€MBR_TX_SEQã€MBR_RX_SEQ ã§ã™ + +groupChannel.listener.alreadyExist=ãƒãƒ£ãƒ³ãƒãƒ«ãƒªã‚¹ãƒŠãƒ¼ [{0}][{1}] ã¯ã™ã§ã«å­˜åœ¨ã—ã¾ã™ã€‚ +groupChannel.noDestination=宛先ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“。 +groupChannel.nullMessage=Null メッセージをé€ä¿¡ã§ãã¾ã›ã‚“ +groupChannel.optionFlag.conflict=Interceptor ã®option フラグãŒè¡çªã—ã¦ã„ã¾ã™: [{0}] +groupChannel.receiving.error=エラーå—信メッセージ: +groupChannel.sendFail.noRpcChannelReply=RPC ãƒãƒ£ãƒ³ãƒãƒ«ãŒè¦‹ã¤ã‹ã‚‰ãªã„ãŸã‚ NoRpcChannelReply ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 +groupChannel.unable.deserialize=メッセージをデシリアライズã§ãã¾ã›ã‚“: [{0}] +groupChannel.unable.sendHeartbeat=Tribesインターセプタスタックを介ã—ã¦ãƒãƒ¼ãƒˆãƒ“ートをé€ä¿¡ã§ãã¾ã›ã‚“。もã†ä¸€sleep ã—ã¾ã™ã€‚ +groupChannel.warn.noUtilityExecutor=ユーティリティエグゼキュータãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“。新ãŸã«ä½œæˆã—ã¾ã™ã€‚ + +rpcChannel.replyFailed=RpcChannel ã¸è¿”ä¿¡ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/group/LocalStrings_ko.properties new file mode 100644 index 0000000..b89f496 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_ko.properties @@ -0,0 +1,29 @@ +# 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. + +channelCoordinator.alreadyStarted=채ë„ì´ ì´ë¯¸ 레벨 [{0}](으)ë¡œ 시작ë˜ì—ˆìŠµë‹ˆë‹¤. +channelCoordinator.invalid.startLevel=유요하지 ì•Šì€ ì‹œìž‘ 레벨입니다. 유효한 ë ˆë²¨ì€ SND_RX_SEQ, SND_TX_SEQ, MBR_TX_SEQ, MBR_RX_SEQ입니다. + +groupChannel.listener.alreadyExist=리스너가 ì´ë¯¸ 존재합니다: [{0}][{1}] +groupChannel.noDestination=그룹 채ë„ì—ì„œ 전송 ëŒ€ìƒ ë©¤ë²„ê°€ 없습니다. +groupChannel.nullMessage=ë„ ë©”ì‹œì§€ë¥¼ 전송할 수 없습니다. +groupChannel.optionFlag.conflict=ì¸í„°ì…‰í„° 옵션 플래그가 충ëŒí•©ë‹ˆë‹¤: [{0}] +groupChannel.receiving.error=메시지 수신 중 오류 ë°œìƒ +groupChannel.sendFail.noRpcChannelReply=RPC 채ë„ì„ ì°¾ì„ ìˆ˜ 없습니다. NoRpcChannelReplyì„ ë³´ë‚´ì§€ 못했습니다. +groupChannel.unable.deserialize=메시지를 ì—­ì§ë ¬í™”í•  수 없습니다:[{0}] +groupChannel.unable.sendHeartbeat=Tribes ì¸í„°ì…‰í„° 스íƒì„ 통해 heartbeat를 보낼 수 없습니다. 다시 sleepì„ ì‹œë„í•  것입니다. +groupChannel.warn.noUtilityExecutor=UtilityExecutorê°€ 설정ë˜ì§€ ì•Šì•„, 새로 ìƒì„±í•©ë‹ˆë‹¤. + +rpcChannel.replyFailed=RpcChannelì—ì„œ ì‘ë‹µì„ ë˜ëŒë ¤ 보낼 수 없습니다. diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_pt_BR.properties b/java/org/apache/catalina/tribes/group/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..ef30b1f --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +channelCoordinator.invalid.startLevel=start level inválido, níveis válidos:SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_ru.properties b/java/org/apache/catalina/tribes/group/LocalStrings_ru.properties new file mode 100644 index 0000000..54cfa00 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_ru.properties @@ -0,0 +1,18 @@ +# 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. + +channelCoordinator.invalid.startLevel=Ðеверный Ñтартовый уровень, допуÑтимые уровни:SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ + +groupChannel.listener.alreadyExist=Слушатель уже ÑущеÑтвует:[{0}][{1}] diff --git a/java/org/apache/catalina/tribes/group/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/group/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..d2d634d --- /dev/null +++ b/java/org/apache/catalina/tribes/group/LocalStrings_zh_CN.properties @@ -0,0 +1,29 @@ +# 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. + +channelCoordinator.alreadyStarted=通é“å·²ç»å¯åŠ¨ï¼Œçº§åˆ«ï¼š[{0}] +channelCoordinator.invalid.startLevel=å¯åŠ¨çº§åˆ«æ— æ•ˆï¼Œæœ‰æ•ˆçº§åˆ«ä¸ºï¼šSND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ + +groupChannel.listener.alreadyExist=侦å¬å™¨å·²å­˜åœ¨ï¼š[{0}][{1}] +groupChannel.noDestination=没有指定目的地 +groupChannel.nullMessage=无法å‘é€ç©ºæ¶ˆæ¯ +groupChannel.optionFlag.conflict=拦截器选项标志冲çªï¼š[{0}] +groupChannel.receiving.error=接收消æ¯æ—¶å‡ºé”™ï¼š +groupChannel.sendFail.noRpcChannelReply=找ä¸åˆ°rpc通é“,无法å‘é€NoRpcChannelReply。 +groupChannel.unable.deserialize=无法ååºåˆ—化消æ¯ï¼š[{0}] +groupChannel.unable.sendHeartbeat=无法通过Tribes拦截器堆栈å‘é€å¿ƒè·³ã€‚ 会å†è¯•ä¸€æ¬¡ã€‚ +groupChannel.warn.noUtilityExecutor=没有公共的executor 被设置时,创建一个 + +rpcChannel.replyFailed=无法在RpcChannel中å‘é€å›žå¤ diff --git a/java/org/apache/catalina/tribes/group/Response.java b/java/org/apache/catalina/tribes/group/Response.java new file mode 100644 index 0000000..91aa73f --- /dev/null +++ b/java/org/apache/catalina/tribes/group/Response.java @@ -0,0 +1,52 @@ +/* + * 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.catalina.tribes.group; + +import java.io.Serializable; + +import org.apache.catalina.tribes.Member; + +/** + * A response object holds a message from a responding partner. + */ +public class Response { + private Member source; + private Serializable message; + public Response() { + } + + public Response(Member source, Serializable message) { + this.source = source; + this.message = message; + } + + public void setSource(Member source) { + this.source = source; + } + + public void setMessage(Serializable message) { + this.message = message; + } + + public Member getSource() { + return source; + } + + public Serializable getMessage() { + return message; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/RpcCallback.java b/java/org/apache/catalina/tribes/group/RpcCallback.java new file mode 100644 index 0000000..83ee4ec --- /dev/null +++ b/java/org/apache/catalina/tribes/group/RpcCallback.java @@ -0,0 +1,45 @@ +/* + * 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.catalina.tribes.group; + +import java.io.Serializable; + +import org.apache.catalina.tribes.Member; + +/** + * The RpcCallback interface is an interface for the Tribes channel to request a + * response object to a request that came in. + */ +public interface RpcCallback { + + /** + * Allows sending a response to a received message. + * @param msg The message + * @param sender Member + * @return Serializable object, null if no reply should be sent + */ + Serializable replyRequest(Serializable msg, Member sender); + + /** + * If the reply has already been sent to the requesting thread, + * the rpc callback can handle any data that comes in after the fact. + * @param msg The message + * @param sender Member + */ + void leftOver(Serializable msg, Member sender); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/RpcChannel.java b/java/org/apache/catalina/tribes/group/RpcChannel.java new file mode 100644 index 0000000..c72963f --- /dev/null +++ b/java/org/apache/catalina/tribes/group/RpcChannel.java @@ -0,0 +1,312 @@ +/* + * 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.catalina.tribes.group; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.ErrorHandler; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.catalina.tribes.util.UUIDGenerator; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A channel to handle RPC messaging + */ +public class RpcChannel implements ChannelListener { + private static final Log log = LogFactory.getLog(RpcChannel.class); + protected static final StringManager sm = StringManager.getManager(RpcChannel.class); + + public static final int FIRST_REPLY = 1; + public static final int MAJORITY_REPLY = 2; + public static final int ALL_REPLY = 3; + public static final int NO_REPLY = 4; + + private Channel channel; + private RpcCallback callback; + private byte[] rpcId; + private int replyMessageOptions = 0; + + private final ConcurrentMap responseMap = new ConcurrentHashMap<>(); + + /** + * Create an RPC channel. You can have several RPC channels attached to a group + * all separated out by the uniqueness + * @param rpcId - the unique Id for this RPC group + * @param channel Channel + * @param callback RpcCallback + */ + public RpcChannel(byte[] rpcId, Channel channel, RpcCallback callback) { + this.channel = channel; + this.callback = callback; + this.rpcId = rpcId; + channel.addChannelListener(this); + } + + + /** + * Send a message and wait for the response. + * @param destination Member[] - the destination for the message, and the members you request a reply from + * @param message Serializable - the message you are sending out + * @param rpcOptions int - FIRST_REPLY, MAJORITY_REPLY or ALL_REPLY + * @param channelOptions channel sender options + * @param timeout long - timeout in milliseconds, if no reply is received within this time null is returned + * @return Response[] - an array of response objects. + * @throws ChannelException Error sending message + */ + public Response[] send(Member[] destination, + Serializable message, + int rpcOptions, + int channelOptions, + long timeout) throws ChannelException { + + if ( destination==null || destination.length == 0 ) { + return new Response[0]; + } + + //avoid dead lock + int sendOptions = + channelOptions & ~Channel.SEND_OPTIONS_SYNCHRONIZED_ACK; + + RpcCollectorKey key = new RpcCollectorKey(UUIDGenerator.randomUUID(false)); + RpcCollector collector = new RpcCollector(key,rpcOptions,destination.length); + try { + synchronized (collector) { + if ( rpcOptions != NO_REPLY ) { + responseMap.put(key, collector); + } + RpcMessage rmsg = new RpcMessage(rpcId, key.id, message); + channel.send(destination, rmsg, sendOptions); + if ( rpcOptions != NO_REPLY ) { + collector.wait(timeout); + } + } + } catch ( InterruptedException ix ) { + Thread.currentThread().interrupt(); + } finally { + responseMap.remove(key); + } + return collector.getResponses(); + } + + @Override + public void messageReceived(Serializable msg, Member sender) { + RpcMessage rmsg = (RpcMessage)msg; + RpcCollectorKey key = new RpcCollectorKey(rmsg.uuid); + if ( rmsg.reply ) { + RpcCollector collector = responseMap.get(key); + if (collector == null) { + if (!(rmsg instanceof RpcMessage.NoRpcChannelReply)) { + callback.leftOver(rmsg.message, sender); + } + } else { + synchronized (collector) { + //make sure it hasn't been removed + if ( responseMap.containsKey(key) ) { + if ( (rmsg instanceof RpcMessage.NoRpcChannelReply) ) { + collector.destcnt--; + } else { + collector.addResponse(rmsg.message, sender); + } + if (collector.isComplete()) { + collector.notifyAll(); + } + } else { + if (! (rmsg instanceof RpcMessage.NoRpcChannelReply) ) { + callback.leftOver(rmsg.message, sender); + } + } + }//synchronized + }//end if + } else { + boolean finished = false; + final ExtendedRpcCallback excallback = (callback instanceof ExtendedRpcCallback)?((ExtendedRpcCallback)callback) : null; + boolean asyncReply = ((replyMessageOptions & Channel.SEND_OPTIONS_ASYNCHRONOUS) == Channel.SEND_OPTIONS_ASYNCHRONOUS); + Serializable reply = callback.replyRequest(rmsg.message,sender); + ErrorHandler handler = null; + final Serializable request = msg; + final Serializable response = reply; + final Member fsender = sender; + if (excallback!=null && asyncReply) { + handler = new ErrorHandler() { + @Override + public void handleError(ChannelException x, UniqueId id) { + excallback.replyFailed(request, response, fsender, x); + } + @Override + public void handleCompletion(UniqueId id) { + excallback.replySucceeded(request, response, fsender); + } + }; + } + rmsg.reply = true; + rmsg.message = reply; + try { + if (handler!=null) { + channel.send(new Member[] {sender}, rmsg,replyMessageOptions & ~Channel.SEND_OPTIONS_SYNCHRONIZED_ACK, handler); + } else { + channel.send(new Member[] {sender}, rmsg,replyMessageOptions & ~Channel.SEND_OPTIONS_SYNCHRONIZED_ACK); + } + finished = true; + } catch ( Exception x ) { + if (excallback != null && !asyncReply) { + excallback.replyFailed(rmsg.message, reply, sender, x); + } else { + log.error(sm.getString("rpcChannel.replyFailed"),x); + } + } + if (finished && excallback != null && !asyncReply) { + excallback.replySucceeded(rmsg.message, reply, sender); + } + }//end if + } + + public void breakdown() { + channel.removeChannelListener(this); + } + + @Override + public boolean accept(Serializable msg, Member sender) { + if ( msg instanceof RpcMessage ) { + RpcMessage rmsg = (RpcMessage)msg; + return Arrays.equals(rmsg.rpcId,rpcId); + } else { + return false; + } + } + + public Channel getChannel() { + return channel; + } + + public RpcCallback getCallback() { + return callback; + } + + public byte[] getRpcId() { + return rpcId; + } + + public void setChannel(Channel channel) { + this.channel = channel; + } + + public void setCallback(RpcCallback callback) { + this.callback = callback; + } + + public void setRpcId(byte[] rpcId) { + this.rpcId = rpcId; + } + + public int getReplyMessageOptions() { + return replyMessageOptions; + } + + public void setReplyMessageOptions(int replyMessageOptions) { + this.replyMessageOptions = replyMessageOptions; + } + + /** + * Class that holds all response. + */ + public static class RpcCollector { + public final ArrayList responses = new ArrayList<>(); + public final RpcCollectorKey key; + public final int options; + public int destcnt; + + public RpcCollector(RpcCollectorKey key, int options, int destcnt) { + this.key = key; + this.options = options; + this.destcnt = destcnt; + } + + public void addResponse(Serializable message, Member sender) { + Response resp = new Response(sender,message); + responses.add(resp); + } + + public boolean isComplete() { + if ( destcnt <= 0 ) { + return true; + } + switch (options) { + case ALL_REPLY: + return destcnt == responses.size(); + case MAJORITY_REPLY: + float perc = ((float)responses.size()) / ((float)destcnt); + return perc >= 0.50f; + case FIRST_REPLY: + return responses.size()>0; + default: + return false; + } + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public boolean equals(Object o) { + if ( o instanceof RpcCollector ) { + RpcCollector r = (RpcCollector)o; + return r.key.equals(this.key); + } else { + return false; + } + } + + public Response[] getResponses() { + return responses.toArray(new Response[0]); + } + } + + public static class RpcCollectorKey { + final byte[] id; + public RpcCollectorKey(byte[] id) { + this.id = id; + } + + @Override + public int hashCode() { + return id[0]+id[1]+id[2]+id[3]; + } + + @Override + public boolean equals(Object o) { + if ( o instanceof RpcCollectorKey ) { + RpcCollectorKey r = (RpcCollectorKey)o; + return Arrays.equals(id,r.id); + } else { + return false; + } + } + + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/RpcMessage.java b/java/org/apache/catalina/tribes/group/RpcMessage.java new file mode 100644 index 0000000..0fc804d --- /dev/null +++ b/java/org/apache/catalina/tribes/group/RpcMessage.java @@ -0,0 +1,110 @@ +/* + * 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.catalina.tribes.group; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.io.Serializable; + +import org.apache.catalina.tribes.util.Arrays; + +public class RpcMessage implements Externalizable { + + protected Serializable message; + protected byte[] uuid; + protected byte[] rpcId; + protected boolean reply = false; + + public RpcMessage() { + //for serialization + } + + public RpcMessage(byte[] rpcId, byte[] uuid, Serializable message) { + this.rpcId = rpcId; + this.uuid = uuid; + this.message = message; + } + + @Override + public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException { + reply = in.readBoolean(); + int length = in.readInt(); + uuid = new byte[length]; + in.readFully(uuid); + length = in.readInt(); + rpcId = new byte[length]; + in.readFully(rpcId); + message = (Serializable)in.readObject(); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeBoolean(reply); + out.writeInt(uuid.length); + out.write(uuid, 0, uuid.length); + out.writeInt(rpcId.length); + out.write(rpcId, 0, rpcId.length); + out.writeObject(message); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("RpcMessage["); + buf.append(super.toString()); + buf.append("] rpcId="); + buf.append(Arrays.toString(rpcId)); + buf.append("; uuid="); + buf.append(Arrays.toString(uuid)); + buf.append("; msg="); + buf.append(message); + return buf.toString(); + } + + public static class NoRpcChannelReply extends RpcMessage { + public NoRpcChannelReply() { + + } + + public NoRpcChannelReply(byte[] rpcid, byte[] uuid) { + super(rpcid,uuid,null); + reply = true; + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + reply = true; + int length = in.readInt(); + uuid = new byte[length]; + in.readFully(uuid); + length = in.readInt(); + rpcId = new byte[length]; + in.readFully(rpcId); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeInt(uuid.length); + out.write(uuid, 0, uuid.length); + out.writeInt(rpcId.length); + out.write(rpcId, 0, rpcId.length); + } + } + + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java new file mode 100644 index 0000000..c0c7b06 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java @@ -0,0 +1,164 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.membership.Membership; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Filters membership based on domain. + */ +public class DomainFilterInterceptor extends ChannelInterceptorBase + implements DomainFilterInterceptorMBean { + + private static final Log log = LogFactory.getLog(DomainFilterInterceptor.class); + protected static final StringManager sm = StringManager.getManager(DomainFilterInterceptor.class); + protected volatile Membership membership = null; + + protected byte[] domain = new byte[0]; + protected int logInterval = 100; + private final AtomicInteger logCounter = new AtomicInteger(logInterval); + + @Override + public void messageReceived(ChannelMessage msg) { + if (Arrays.equals(domain, msg.getAddress().getDomain())) { + super.messageReceived(msg); + } else { + if (logCounter.incrementAndGet() >= logInterval) { + logCounter.set(0); + if (log.isWarnEnabled()) { + log.warn(sm.getString("domainFilterInterceptor.message.refused", msg.getAddress())); + } + } + } + }//messageReceived + + + @Override + public void memberAdded(Member member) { + if ( membership == null ) { + setupMembership(); + } + boolean notify = false; + synchronized (membership) { + notify = Arrays.equals(domain,member.getDomain()); + if ( notify ) { + notify = membership.memberAlive(member); + } + } + if ( notify ) { + super.memberAdded(member); + } else { + if(log.isInfoEnabled()) { + log.info(sm.getString("domainFilterInterceptor.member.refused", member)); + } + } + } + + @Override + public void memberDisappeared(Member member) { + if ( membership == null ) { + setupMembership(); + } + boolean notify = false; + synchronized (membership) { + notify = Arrays.equals(domain,member.getDomain()); + if ( notify ) { + membership.removeMember(member); + } + } + if ( notify ) { + super.memberDisappeared(member); + } + } + + @Override + public boolean hasMembers() { + if ( membership == null ) { + setupMembership(); + } + return membership.hasMembers(); + } + + @Override + public Member[] getMembers() { + if ( membership == null ) { + setupMembership(); + } + return membership.getMembers(); + } + + @Override + public Member getMember(Member mbr) { + if ( membership == null ) { + setupMembership(); + } + return membership.getMember(mbr); + } + + @Override + public Member getLocalMember(boolean incAlive) { + return super.getLocalMember(incAlive); + } + + + protected synchronized void setupMembership() { + if ( membership == null ) { + membership = new Membership(super.getLocalMember(true)); + } + + } + + @Override + public byte[] getDomain() { + return domain; + } + + public void setDomain(byte[] domain) { + this.domain = domain; + } + + public void setDomain(String domain) { + if ( domain == null ) { + return; + } + if (domain.startsWith("{")) { + setDomain(org.apache.catalina.tribes.util.Arrays.fromString(domain)); + } else { + setDomain(org.apache.catalina.tribes.util.Arrays.convert(domain)); + } + } + + @Override + public int getLogInterval() { + return logInterval; + } + + @Override + public void setLogInterval(int logInterval) { + this.logInterval = logInterval; + } + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptorMBean.java new file mode 100644 index 0000000..462e23b --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptorMBean.java @@ -0,0 +1,29 @@ +/* + * 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.catalina.tribes.group.interceptors; + +public interface DomainFilterInterceptorMBean { + + int getOptionFlag(); + + byte[] getDomain(); + + int getLogInterval(); + + void setLogInterval(int logInterval); + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java new file mode 100644 index 0000000..a209864 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java @@ -0,0 +1,620 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.util.concurrent.ConcurrentLinkedQueue; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + * Adds encryption using a pre-shared key. + * + * The length of the key (in bytes) must be acceptable for the encryption + * algorithm being used. For example, for AES, you must use a key of either + * 16 bytes (128 bits, 24 bytes 192 bits), or 32 bytes (256 bits). + * + * You can supply the raw key bytes by calling {@link #setEncryptionKey(byte[])} + * or the hex-encoded binary bytes by calling + * {@link #setEncryptionKey(String)}. + */ +public class EncryptInterceptor extends ChannelInterceptorBase implements EncryptInterceptorMBean { + + private static final Log log = LogFactory.getLog(EncryptInterceptor.class); + protected static final StringManager sm = StringManager.getManager(EncryptInterceptor.class); + + private static final String DEFAULT_ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding"; + + private String providerName; + private String encryptionAlgorithm = DEFAULT_ENCRYPTION_ALGORITHM; + private byte[] encryptionKeyBytes; + private String encryptionKeyString; + + + private BaseEncryptionManager encryptionManager; + + public EncryptInterceptor() { + } + + @Override + public void start(int svc) throws ChannelException { + validateChannelChain(); + + if(Channel.SND_TX_SEQ == (svc & Channel.SND_TX_SEQ)) { + try { + encryptionManager = createEncryptionManager(getEncryptionAlgorithm(), + getEncryptionKeyInternal(), + getProviderName()); + } catch (GeneralSecurityException gse) { + throw new ChannelException(sm.getString("encryptInterceptor.init.failed"), gse); + } + } + + super.start(svc); + } + + private void validateChannelChain() throws ChannelException { + ChannelInterceptor interceptor = getPrevious(); + while(null != interceptor) { + if(interceptor instanceof TcpFailureDetector) { + throw new ChannelConfigException(sm.getString("encryptInterceptor.tcpFailureDetector.ordering")); + } + + interceptor = interceptor.getPrevious(); + } + } + + @Override + public void stop(int svc) throws ChannelException { + if(Channel.SND_TX_SEQ == (svc & Channel.SND_TX_SEQ)) { + encryptionManager.shutdown(); + } + + super.stop(svc); + } + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) + throws ChannelException { + try { + byte[] data = msg.getMessage().getBytes(); + + // See #encrypt(byte[]) for an explanation of the return value + byte[][] bytes = encryptionManager.encrypt(data); + + XByteBuffer xbb = msg.getMessage(); + + // Completely replace the message + xbb.clear(); + xbb.append(bytes[0], 0, bytes[0].length); + xbb.append(bytes[1], 0, bytes[1].length); + + super.sendMessage(destination, msg, payload); + + } catch (GeneralSecurityException gse) { + log.error(sm.getString("encryptInterceptor.encrypt.failed")); + throw new ChannelException(gse); + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + try { + byte[] data = msg.getMessage().getBytes(); + + data = encryptionManager.decrypt(data); + + XByteBuffer xbb = msg.getMessage(); + + // Completely replace the message with the decrypted one + xbb.clear(); + xbb.append(data, 0, data.length); + + super.messageReceived(msg); + } catch (GeneralSecurityException gse) { + log.error(sm.getString("encryptInterceptor.decrypt.failed"), gse); + } + } + + /** + * Sets the encryption algorithm to be used for encrypting and decrypting + * channel messages. You must specify the algorithm/mode/padding. + * Information on standard algorithm names may be found in the + * Java + * documentation. + * + * Default is AES/CBC/PKCS5Padding. + * + * @param algorithm The algorithm to use. + */ + @Override + public void setEncryptionAlgorithm(String algorithm) { + if(null == getEncryptionAlgorithm()) { + throw new IllegalStateException(sm.getString("encryptInterceptor.algorithm.required")); + } + + int pos = algorithm.indexOf('/'); + if(pos < 0) { + throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.required")); + } + pos = algorithm.indexOf('/', pos + 1); + if(pos < 0) { + throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.required")); + } + + encryptionAlgorithm = algorithm; + } + + /** + * Gets the encryption algorithm being used to encrypt and decrypt channel + * messages. + * + * @return The algorithm being used, including the algorithm mode and padding. + */ + @Override + public String getEncryptionAlgorithm() { + return encryptionAlgorithm; + } + + /** + * Sets the encryption key for encryption and decryption. The length of the + * key must be appropriate for the algorithm being used. + * + * @param key The encryption key. + */ + @Override + public void setEncryptionKey(byte[] key) { + if (null == key) { + encryptionKeyBytes = null; + } else { + encryptionKeyBytes = key.clone(); + } + } + + /** + * Gets the encryption key being used for encryption and decryption. + * The key is encoded using hex-encoding where e.g. the byte 0xab + * will be shown as "ab". The length of the string in characters will + * be twice the length of the key in bytes. + * + * @param keyBytes The encryption key. + */ + public void setEncryptionKey(String keyBytes) { + this.encryptionKeyString = keyBytes; + if (null == keyBytes) { + setEncryptionKey((byte[])null); + } else { + setEncryptionKey(fromHexString(keyBytes.trim())); + } + } + + /** + * Gets the encryption key being used for encryption and decryption. + * + * @return The encryption key. + */ + @Override + public byte[] getEncryptionKey() { + byte[] key = getEncryptionKeyInternal(); + + if(null != key) { + key = key.clone(); + } + + return key; + } + + private byte[] getEncryptionKeyInternal() { + return encryptionKeyBytes; + } + + public String getEncryptionKeyString() { + return encryptionKeyString; + } + + public void setEncryptionKeyString(String encryptionKeyString) { + setEncryptionKey(encryptionKeyString); + } + + /** + * Sets the JCA provider name used for cryptographic activities. + * + * Default is the JVM platform default. + * + * @param provider The name of the JCA provider. + */ + @Override + public void setProviderName(String provider) { + providerName = provider; + } + + /** + * Gets the JCA provider name used for cryptographic activities. + * + * Default is the JVM platform default. + * + * @return The name of the JCA provider. + */ + @Override + public String getProviderName() { + return providerName; + } + + // Copied from org.apache.tomcat.util.buf.HexUtils + + private static final int[] DEC = { + 00, 01, 02, 03, 04, 05, 06, 07, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, + }; + + + private static int getDec(int index) { + // Fast for correct values, slower for incorrect ones + try { + return DEC[index - '0']; + } catch (ArrayIndexOutOfBoundsException ex) { + return -1; + } + } + + + private static byte[] fromHexString(String input) { + if (input == null) { + return null; + } + + if ((input.length() & 1) == 1) { + // Odd number of characters + throw new IllegalArgumentException(sm.getString("hexUtils.fromHex.oddDigits")); + } + + char[] inputChars = input.toCharArray(); + byte[] result = new byte[input.length() >> 1]; + for (int i = 0; i < result.length; i++) { + int upperNibble = getDec(inputChars[2*i]); + int lowerNibble = getDec(inputChars[2*i + 1]); + if (upperNibble < 0 || lowerNibble < 0) { + // Non hex character + throw new IllegalArgumentException(sm.getString("hexUtils.fromHex.nonHex")); + } + result[i] = (byte) ((upperNibble << 4) + lowerNibble); + } + return result; + } + + private static BaseEncryptionManager createEncryptionManager(String algorithm, + byte[] encryptionKey, String providerName) + throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { + if(null == encryptionKey) { + throw new IllegalStateException(sm.getString("encryptInterceptor.key.required")); + } + + String algorithmName; + String algorithmMode; + + // We need to break-apart the algorithm name e.g. AES/CBC/PKCS5Padding + // take just the algorithm part. + int pos = algorithm.indexOf('/'); + + if(pos >= 0) { + algorithmName = algorithm.substring(0, pos); + int pos2 = algorithm.indexOf('/', pos+1); + + if(pos2 >= 0) { + algorithmMode = algorithm.substring(pos + 1, pos2); + } else { + algorithmMode = "CBC"; + } + } else { + algorithmName = algorithm; + algorithmMode = "CBC"; + } + + if("GCM".equalsIgnoreCase(algorithmMode)) { + return new GCMEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName); + } else if("CBC".equalsIgnoreCase(algorithmMode) + || "OFB".equalsIgnoreCase(algorithmMode) + || "CFB".equalsIgnoreCase(algorithmMode)) { + return new BaseEncryptionManager(algorithm, + new SecretKeySpec(encryptionKey, algorithmName), + providerName); + } else { + throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported-mode", algorithmMode)); + } + } + + private static class BaseEncryptionManager { + /** + * The fully-specified algorithm e.g. AES/CBC/PKCS5Padding. + */ + private final String algorithm; + + /** + * The block size of the cipher. + */ + private final int blockSize; + + /** + * The cryptographic provider name. + */ + private final String providerName; + + /** + * The secret key to use for encryption and decryption operations. + */ + private final SecretKeySpec secretKey; + + /** + * A pool of Cipher objects. Ciphers are expensive to create, but not + * to re-initialize, so we use a pool of them which grows as necessary. + */ + private final ConcurrentLinkedQueue cipherPool; + + /** + * A pool of SecureRandom objects. Each encrypt operation requires access + * to a source of randomness. SecureRandom is thread-safe, but sharing a + * single instance will likely be a bottleneck. + */ + private final ConcurrentLinkedQueue randomPool; + + BaseEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName) + throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { + this.algorithm = algorithm; + this.providerName = providerName; + this.secretKey = secretKey; + + cipherPool = new ConcurrentLinkedQueue<>(); + Cipher cipher = createCipher(); + blockSize = cipher.getBlockSize(); + cipherPool.offer(cipher); + randomPool = new ConcurrentLinkedQueue<>(); + } + + public void shutdown() { + // Individual Cipher and SecureRandom objects need no explicit teardown + cipherPool.clear(); + randomPool.clear(); + } + + private String getAlgorithm() { + return algorithm; + } + + private SecretKeySpec getSecretKey() { + return secretKey; + } + + /** + * Gets the size, in bytes, of the initialization vector for the + * cipher being used. The IV size is often, but not always, the block + * size for the cipher. + * + * @return The size of the initialization vector for this algorithm. + */ + protected int getIVSize() { + return blockSize; + } + + private String getProviderName() { + return providerName; + } + + private Cipher createCipher() + throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { + String providerName = getProviderName(); + + if(null == providerName) { + return Cipher.getInstance(getAlgorithm()); + } else { + return Cipher.getInstance(getAlgorithm(), providerName); + } + } + + private Cipher getCipher() throws GeneralSecurityException { + Cipher cipher = cipherPool.poll(); + + if(null == cipher) { + cipher = createCipher(); + } + + return cipher; + } + + private void returnCipher(Cipher cipher) { + cipherPool.offer(cipher); + } + + private SecureRandom getRandom() { + SecureRandom random = randomPool.poll(); + + if(null == random) { + random = new SecureRandom(); + } + + return random; + } + + private void returnRandom(SecureRandom random) { + randomPool.offer(random); + } + + /** + * Encrypts the input bytes into two separate byte arrays: + * one for the random initialization vector (IV) used for this message, + * and the second one containing the actual encrypted payload. + * + * This method returns a pair of byte arrays instead of a single + * concatenated one to reduce the number of byte buffers created + * and copied during the whole operation -- including message re-building. + * + * @param bytes The data to encrypt. + * + * @return The IV in [0] and the encrypted data in [1]. + * + * @throws GeneralSecurityException If the input data cannot be encrypted. + */ + private byte[][] encrypt(byte[] bytes) throws GeneralSecurityException { + Cipher cipher = null; + + // Always use a random IV For cipher setup. + // The recipient doesn't need the (matching) IV because we will always + // pre-pad messages with the IV as a nonce. + byte[] iv = generateIVBytes(); + + try { + cipher = getCipher(); + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), generateIV(iv, 0, getIVSize())); + + // Prepend the IV to the beginning of the encrypted data + byte[][] data = new byte[2][]; + data[0] = iv; + data[1] = cipher.doFinal(bytes); + + return data; + } finally { + if(null != cipher) { + returnCipher(cipher); + } + } + } + + /** + * Decrypts the input bytes. + * + * @param bytes The data to decrypt. + * + * @return The decrypted data. + * + * @throws GeneralSecurityException If the input data cannot be decrypted. + */ + private byte[] decrypt(byte[] bytes) throws GeneralSecurityException { + Cipher cipher = null; + + int ivSize = getIVSize(); + AlgorithmParameterSpec IV = generateIV(bytes, 0, ivSize); + + try { + cipher = getCipher(); + + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), IV); + + // Decrypt remainder of the message. + return cipher.doFinal(bytes, ivSize, bytes.length - ivSize); + } finally { + if(null != cipher) { + returnCipher(cipher); + } + } + } + + protected byte[] generateIVBytes() { + byte[] ivBytes = new byte[getIVSize()]; + + SecureRandom random = null; + + try { + random = getRandom(); + + // Always use a random IV For cipher setup. + // The recipient doesn't need the (matching) IV because we will always + // pre-pad messages with the IV as a nonce. + random.nextBytes(ivBytes); + + return ivBytes; + } finally { + if(null != random) { + returnRandom(random); + } + } + } + + protected AlgorithmParameterSpec generateIV(byte[] ivBytes, int offset, int length) { + return new IvParameterSpec(ivBytes, offset, length); + } + } + + /** + * Implements an EncryptionManager for using GCM block cipher modes. + * + * GCM works a little differently than some of the other block cipher modes + * supported by EncryptInterceptor. First of all, it requires a different + * kind of AlgorithmParameterSpec object to be used, and second, it + * requires a slightly different initialization vector and something called + * an "authentication tag". + * + * The choice of IV length can be somewhat arbitrary, but there is consensus + * that 96-bit (12-byte) IVs for GCM are the best trade-off between security + * and performance. For other block cipher modes, IV length is the same as + * the block size. + * + * The "authentication tag" is a computed authentication value based upon + * the message and the encryption process. GCM defines these tags as the + * number of bits to use for the authentication tag, and it's clear that + * the highest number of bits supported 128-bit provide the best security. + */ + private static class GCMEncryptionManager extends BaseEncryptionManager + { + GCMEncryptionManager(String algorithm, SecretKeySpec secretKey, String providerName) + throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { + super(algorithm, secretKey, providerName); + } + + @Override + protected int getIVSize() { + return 12; // See class javadoc for explanation of this magic number (12) + } + + @Override + protected AlgorithmParameterSpec generateIV(byte[] bytes, int offset, int length) { + // See class javadoc for explanation of this magic number (128) + return new GCMParameterSpec(128, bytes, offset, length); + } + } + + static class ChannelConfigException + extends ChannelException + { + private static final long serialVersionUID = 1L; + + ChannelConfigException(String message) { + super(message); + } + } +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java new file mode 100644 index 0000000..04c3b4e --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptorMBean.java @@ -0,0 +1,31 @@ +/* + * 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.catalina.tribes.group.interceptors; + +public interface EncryptInterceptorMBean { + + // Config + int getOptionFlag(); + void setOptionFlag(int optionFlag); + + void setEncryptionAlgorithm(String algorithm); + String getEncryptionAlgorithm(); + void setEncryptionKey(byte[] key); + byte[] getEncryptionKey(); + void setProviderName(String provider); + String getProviderName(); +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java new file mode 100644 index 0000000..98c8353 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java @@ -0,0 +1,256 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Set; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * The fragmentation interceptor splits up large messages into smaller messages and assembles them on the other end. + * This is very useful when you don't want large messages hogging the sending sockets + * and smaller messages can make it through. + * + *
    Configuration Options
    + * FragmentationInterceptor.expire=<milliseconds> - how long do we keep the fragments in memory and wait for the rest to arrive default=60,000ms -> 60seconds + * This setting is useful to avoid OutOfMemoryErrors
    + * FragmentationInterceptor.maxSize=<max message size> - message size in bytes default=1024*100 (around a tenth of a MB)
    + */ +public class FragmentationInterceptor extends ChannelInterceptorBase implements FragmentationInterceptorMBean { + private static final Log log = LogFactory.getLog(FragmentationInterceptor.class); + protected static final StringManager sm = StringManager.getManager(FragmentationInterceptor.class); + + protected final HashMap fragpieces = new HashMap<>(); + private int maxSize = 1024*100; + private long expire = 1000 * 60; //one minute expiration + protected final boolean deepclone = true; + + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + int size = msg.getMessage().getLength(); + boolean frag = (size>maxSize) && okToProcess(msg.getOptions()); + if ( frag ) { + frag(destination, msg, payload); + } else { + msg.getMessage().append(frag); + super.sendMessage(destination, msg, payload); + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + boolean isFrag = XByteBuffer.toBoolean(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-1); + msg.getMessage().trim(1); + if ( isFrag ) { + defrag(msg); + } else { + super.messageReceived(msg); + } + } + + + public FragCollection getFragCollection(FragKey key, ChannelMessage msg) { + FragCollection coll = fragpieces.get(key); + if ( coll == null ) { + synchronized (fragpieces) { + coll = fragpieces.get(key); + if ( coll == null ) { + coll = new FragCollection(msg); + fragpieces.put(key, coll); + } + } + } + return coll; + } + + public void removeFragCollection(FragKey key) { + fragpieces.remove(key); + } + + public void defrag(ChannelMessage msg ) { + FragKey key = new FragKey(msg.getUniqueId()); + FragCollection coll = getFragCollection(key,msg); + coll.addMessage((ChannelMessage)msg.deepclone()); + + if ( coll.complete() ) { + removeFragCollection(key); + ChannelMessage complete = coll.assemble(); + super.messageReceived(complete); + + } + } + + public void frag(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + int size = msg.getMessage().getLength(); + + int count = ((size / maxSize )+(size%maxSize==0?0:1)); + ChannelMessage[] messages = new ChannelMessage[count]; + int remaining = size; + for ( int i=0; i set = fragpieces.keySet(); + Object[] keys = set.toArray(); + for (Object o : keys) { + FragKey key = (FragKey) o; + if (key != null && key.expired(getExpire())) { + removeFragCollection(key); + } + } + }catch ( Exception x ) { + if ( log.isErrorEnabled() ) { + log.error(sm.getString("fragmentationInterceptor.heartbeat.failed"),x); + } + } + super.heartbeat(); + } + + @Override + public int getMaxSize() { + return maxSize; + } + + @Override + public long getExpire() { + return expire; + } + + @Override + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } + + @Override + public void setExpire(long expire) { + this.expire = expire; + } + + public static class FragCollection { + private final long received = System.currentTimeMillis(); + private final ChannelMessage msg; + private final XByteBuffer[] frags; + public FragCollection(ChannelMessage msg) { + //get the total messages + int count = XByteBuffer.toInt(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-4); + frags = new XByteBuffer[count]; + this.msg = msg; + } + + public void addMessage(ChannelMessage msg) { + //remove the total messages + msg.getMessage().trim(4); + //get the msg nr + int nr = XByteBuffer.toInt(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-4); + //remove the msg nr + msg.getMessage().trim(4); + frags[nr] = msg.getMessage(); + + } + + public boolean complete() { + boolean result = true; + for ( int i=0; (iexpire; + } + } + + public static class FragKey { + private final byte[] uniqueId; + private final long received = System.currentTimeMillis(); + public FragKey(byte[] id ) { + this.uniqueId = id; + } + @Override + public int hashCode() { + return XByteBuffer.toInt(uniqueId,0); + } + + @Override + public boolean equals(Object o ) { + if ( o instanceof FragKey ) { + return Arrays.equals(uniqueId,((FragKey)o).uniqueId); + } else { + return false; + } + + } + + public boolean expired(long expire) { + return (System.currentTimeMillis()-received)>expire; + } + + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptorMBean.java new file mode 100644 index 0000000..6c47440 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptorMBean.java @@ -0,0 +1,29 @@ +/* + * 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.catalina.tribes.group.interceptors; + +public interface FragmentationInterceptorMBean { + + // Attributes + int getMaxSize(); + + long getExpire(); + + void setMaxSize(int maxSize); + + void setExpire(long expire); +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java new file mode 100644 index 0000000..1de0dc1 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java @@ -0,0 +1,296 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class GzipInterceptor extends ChannelInterceptorBase implements GzipInterceptorMBean { + + private static final Log log = LogFactory.getLog(GzipInterceptor.class); + protected static final StringManager sm = StringManager.getManager(GzipInterceptor.class); + + public static final int DEFAULT_BUFFER_SIZE = 2048; + public static final int DEFAULT_OPTION_COMPRESSION_ENABLE = 0x0100; + + private int compressionMinSize = 0; + private volatile boolean statsEnabled = false; + private int interval = 0; + + // Stats + private final AtomicInteger count = new AtomicInteger(); + private final AtomicInteger countCompressedTX = new AtomicInteger(); + private final AtomicInteger countUncompressedTX = new AtomicInteger(); + private final AtomicInteger countCompressedRX = new AtomicInteger(); + private final AtomicInteger countUncompressedRX = new AtomicInteger(); + private final AtomicLong sizeTX = new AtomicLong(); + private final AtomicLong compressedSizeTX = new AtomicLong(); + private final AtomicLong uncompressedSizeTX = new AtomicLong(); + private final AtomicLong sizeRX = new AtomicLong(); + private final AtomicLong compressedSizeRX = new AtomicLong(); + private final AtomicLong uncompressedSizeRX = new AtomicLong(); + + + public GzipInterceptor() { + setOptionFlag(DEFAULT_OPTION_COMPRESSION_ENABLE); + } + + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) + throws ChannelException { + try { + byte[] data = msg.getMessage().getBytes(); + if (statsEnabled) { + sizeTX.addAndGet(data.length); + } + + if (data.length > compressionMinSize) { + data = compress(data); + // Set the flag that indicates that the message is compressed + msg.setOptions(msg.getOptions() | getOptionFlag()); + if (statsEnabled) { + countCompressedTX.incrementAndGet(); + compressedSizeTX.addAndGet(data.length); + } + } else if (statsEnabled){ + countUncompressedTX.incrementAndGet(); + uncompressedSizeTX.addAndGet(data.length); + } + + msg.getMessage().trim(msg.getMessage().getLength()); + msg.getMessage().append(data,0,data.length); + super.sendMessage(destination, msg, payload); + + int currentCount = count.incrementAndGet(); + if (statsEnabled && interval > 0 && currentCount % interval == 0) { + report(); + } + } catch ( IOException x ) { + log.error(sm.getString("gzipInterceptor.compress.failed")); + throw new ChannelException(x); + } + } + + + @Override + public void messageReceived(ChannelMessage msg) { + try { + byte[] data = msg.getMessage().getBytes(); + if ((msg.getOptions() & getOptionFlag()) > 0) { + if (statsEnabled) { + countCompressedRX.incrementAndGet(); + compressedSizeRX.addAndGet(data.length); + } + // Message was compressed + data = decompress(data); + } else if (statsEnabled) { + countUncompressedRX.incrementAndGet(); + uncompressedSizeRX.addAndGet(data.length); + } + + if (statsEnabled) { + sizeRX.addAndGet(data.length); + } + + msg.getMessage().trim(msg.getMessage().getLength()); + msg.getMessage().append(data,0,data.length); + super.messageReceived(msg); + + int currentCount = count.incrementAndGet(); + if (statsEnabled && interval > 0 && currentCount % interval == 0) { + report(); + } + } catch ( IOException x ) { + log.error(sm.getString("gzipInterceptor.decompress.failed"),x); + } + } + + + public static byte[] compress(byte[] data) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + GZIPOutputStream gout = new GZIPOutputStream(bout); + gout.write(data); + gout.flush(); + gout.close(); + return bout.toByteArray(); + } + + + /** + * @param data Data to decompress + * @return Decompressed data + * @throws IOException Compression error + */ + public static byte[] decompress(byte[] data) throws IOException { + ByteArrayOutputStream bout = + new ByteArrayOutputStream(DEFAULT_BUFFER_SIZE); + ByteArrayInputStream bin = new ByteArrayInputStream(data); + GZIPInputStream gin = new GZIPInputStream(bin); + byte[] tmp = new byte[DEFAULT_BUFFER_SIZE]; + int length = gin.read(tmp); + while (length > -1) { + bout.write(tmp, 0, length); + length = gin.read(tmp); + } + return bout.toByteArray(); + } + + + @Override + public void report() { + log.info(sm.getString("gzipInterceptor.report", Integer.valueOf(getCount()), + Integer.valueOf(getCountCompressedTX()), Integer.valueOf(getCountUncompressedTX()), + Integer.valueOf(getCountCompressedRX()), Integer.valueOf(getCountUncompressedRX()), + Long.valueOf(getSizeTX()), Long.valueOf(getCompressedSizeTX()), + Long.valueOf(getUncompressedSizeTX()), + Long.valueOf(getSizeRX()), Long.valueOf(getCompressedSizeRX()), + Long.valueOf(getUncompressedSizeRX()))); + } + + + @Override + public int getCompressionMinSize() { + return compressionMinSize; + } + + + @Override + public void setCompressionMinSize(int compressionMinSize) { + this.compressionMinSize = compressionMinSize; + } + + + @Override + public boolean getStatsEnabled() { + return statsEnabled; + } + + + @Override + public void setStatsEnabled(boolean statsEnabled) { + this.statsEnabled = statsEnabled; + } + + + @Override + public int getInterval() { + return interval; + } + + + @Override + public void setInterval(int interval) { + this.interval = interval; + } + + + @Override + public int getCount() { + return count.get(); + } + + + @Override + public int getCountCompressedTX() { + return countCompressedTX.get(); + } + + + @Override + public int getCountUncompressedTX() { + return countUncompressedTX.get(); + } + + + @Override + public int getCountCompressedRX() { + return countCompressedRX.get(); + } + + + @Override + public int getCountUncompressedRX() { + return countUncompressedRX.get(); + } + + + @Override + public long getSizeTX() { + return sizeTX.get(); + } + + + @Override + public long getCompressedSizeTX() { + return compressedSizeTX.get(); + } + + + @Override + public long getUncompressedSizeTX() { + return uncompressedSizeTX.get(); + } + + + @Override + public long getSizeRX() { + return sizeRX.get(); + } + + + @Override + public long getCompressedSizeRX() { + return compressedSizeRX.get(); + } + + + @Override + public long getUncompressedSizeRX() { + return uncompressedSizeRX.get(); + } + + + @Override + public void reset() { + count.set(0); + countCompressedTX.set(0); + countUncompressedTX.set(0); + countCompressedRX.set(0); + countUncompressedRX.set(0); + sizeTX.set(0); + compressedSizeTX.set(0); + uncompressedSizeTX.set(0); + sizeRX.set(0); + compressedSizeRX.set(0); + uncompressedSizeRX.set(0); + } +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptorMBean.java new file mode 100644 index 0000000..07f30c0 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptorMBean.java @@ -0,0 +1,79 @@ +/* + * 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.catalina.tribes.group.interceptors; + +public interface GzipInterceptorMBean { + + // Config + int getOptionFlag(); + void setOptionFlag(int optionFlag); + + /** + * @return the minimum payload size for compression to be enabled. + */ + int getCompressionMinSize(); + /** + * Set the minimum payload size for compression to be enabled. A value of + * zero or less means compression will always be used. If not explicitly + * configured, a default of zero will be used. + * + * @param compressionMinSize The new minimum payload size + */ + void setCompressionMinSize(int compressionMinSize); + + /** + * @return {@code true} if the interceptor is configured to collect + * statistics, otherwise {@code false} + */ + boolean getStatsEnabled(); + /** + * Configure whether the interceptor collects statistics. + * + * @param statsEnabled {@code true} to enable statistics collections, + * otherwise {@code false} + */ + void setStatsEnabled(boolean statsEnabled); + + /** + * @return If statistics collection is enabled, the number of messages + * between statistics reports being written to the log. + */ + int getInterval(); + /** + * If statistics collection is enabled, set the number of messages between + * statistics reports being written to the log. A value of zero or less + * means no statistics reports are written. + * + * @param interval The new interval between reports + */ + void setInterval(int interval); + + // Stats + int getCount(); + int getCountCompressedTX(); + int getCountUncompressedTX(); + int getCountCompressedRX(); + int getCountUncompressedRX(); + long getSizeTX(); + long getCompressedSizeTX(); + long getUncompressedSizeTX(); + long getSizeRX(); + long getCompressedSizeRX(); + long getUncompressedSizeRX(); + void reset(); + void report(); +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties new file mode 100644 index 0000000..6564e4f --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties @@ -0,0 +1,105 @@ +# 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. + +domainFilterInterceptor.member.refused=Member [{0}] was refused to join cluster +domainFilterInterceptor.message.refused=Received message from cluster[{0}] was refused. + +encryptInterceptor.algorithm.required=Encryption algorithm is required, fully-specified e.g. AES/CBC/PKCS5Padding +encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptor does not support block cipher mode [{0}] +encryptInterceptor.decrypt.error.short-message=Failed to decrypt message: premature end-of-message +encryptInterceptor.decrypt.failed=Failed to decrypt message +encryptInterceptor.encrypt.failed=Failed to encrypt message +encryptInterceptor.init.failed=Failed to initialize EncryptInterceptor +encryptInterceptor.key.required=Encryption key is required +encryptInterceptor.tcpFailureDetector.ordering=EncryptInterceptor must be upstream of TcpFailureDetector. Please re-order EncryptInterceptor to be listed before TcpFailureDetector in your channel interceptor pipeline. + +fragmentationInterceptor.fragments.missing=Fragments are missing. +fragmentationInterceptor.heartbeat.failed=Unable to perform heartbeat clean up in the frag interceptor + +gzipInterceptor.compress.failed=Unable to compress byte contents +gzipInterceptor.decompress.failed=Unable to decompress byte contents +gzipInterceptor.report=GZip Interceptor Report[\n\ +\tTotal Messages: {0}\n\ +\tTx Messages Compressed: {1}\n\ +\tTx Messages Uncompressed: {2}\n\ +\tRx Messages Compressed: {3}\n\ +\tRx Messages Uncompressed: {4}\n\ +\tTotal Tx bytes: {5}\n\ +\tCompressed Tx bytes: {6}\n\ +\tUncompressed Tx bytes: {7}\n\ +\tTotal Rx bytes: {8}\n\ +\tCompressed Rx bytes: {9}\n\ +\tUncompressed Rx bytes: {10}\n\ +] + +messageDispatchInterceptor.AsyncMessage.failed=Error while processing async message. +messageDispatchInterceptor.completeMessage.failed=Unable to report back completed message. +messageDispatchInterceptor.errorMessage.failed=Unable to report back error message. +messageDispatchInterceptor.queue.full=Asynchronous queue is full, reached its limit of [{0}] bytes, current:[{1}] bytes. +messageDispatchInterceptor.unableAdd.queue=Unable to add the message to the async queue, queue bug? +messageDispatchInterceptor.warning.optionflag=Warning, you are overriding the asynchronous option flag, this will disable the Channel.SEND_OPTIONS_ASYNCHRONOUS that other apps might use. + +nonBlockingCoordinator.electionMessage.sendfailed=Unable to send election message to:[{0}] +nonBlockingCoordinator.heartbeat.failed=Unable to perform heartbeat. +nonBlockingCoordinator.heartbeat.inconsistency=Heartbeat found inconsistency, restart election +nonBlockingCoordinator.memberAdded.failed=Unable to start election when member was added. +nonBlockingCoordinator.memberAlive.failed=Unable to perform member alive check, assuming member down. +nonBlockingCoordinator.memberDisappeared.failed=Unable to start election when member was removed. +nonBlockingCoordinator.processCoordinationMessage.failed=Error processing coordination message. Could be fatal. +nonBlockingCoordinator.report=CoordinationEvent[type={0}\n\ +\tLocal: {1}\n\ +\tCoord: {2}\n\ +\tView: {3}\n\ +\tSuggested View: {4}\n\ +\tMembers: {5}\n\ +\tInfo: {6}\n\ +] + +orderInterceptor.messageAdded.sameCounter=Message added has the same counter, synchronization bug. Disable the order interceptor + +staticMembershipInterceptor.no.failureDetector=There is no TcpFailureDetector. Automatic detection of static members does not work properly. By defining the StaticMembershipInterceptor under the TcpFailureDetector, automatic detection of the static members will work. +staticMembershipInterceptor.no.pingInterceptor=There is no TcpPingInterceptor. The health check of static members does not work properly. By defining the TcpPingInterceptor, the health check of static members will work. +staticMembershipInterceptor.sendLocalMember.failed=Local member notification failed. +staticMembershipInterceptor.sendShutdown.failed=Shutdown notification failed. + +tcpFailureDetector.already.disappeared=Verification complete. Member already disappeared[{0}] +tcpFailureDetector.failureDetection.failed=Unable to perform failure detection check, assuming member down.[{0}] +tcpFailureDetector.heartbeat.failed=Unable to perform heartbeat on the TcpFailureDetector. +tcpFailureDetector.member.disappeared=Verification complete. Member disappeared[{0}] +tcpFailureDetector.memberDisappeared.verify=Received memberDisappeared[{0}] message. Will verify. +tcpFailureDetector.performBasicCheck.memberAdded=Member added, even though we weren''t notified:[{0}] +tcpFailureDetector.recievedPacket=Received a failure detector packet [{0}] +tcpFailureDetector.still.alive=Verification complete. Member still alive[{0}] +tcpFailureDetector.suspectMember.alive=Suspect member, confirmed alive.[{0}] +tcpFailureDetector.suspectMember.dead=Suspect member, confirmed dead.[{0}] + +tcpPingInterceptor.ping.failed=Unable to send TCP ping. +tcpPingInterceptor.pingFailed.pingThread=Unable to send ping from TCP ping thread. + +throughputInterceptor.report=ThroughputInterceptor Report[\n\ +\tTx Msg:{0} messages\n\ +\tSent:{1} MiB (total)\n\ +\tSent:{2} MiB (application)\n\ +\tTime:{3} seconds\n\ +\tTx Speed:{4} MiB/s (total)\n\ +\tTx Speed:{5} MiB/s (application)\n\ +\tError Msg:{6}\n\ +\tRx Msg:{7} messages\n\ +\tRx Speed:{8} MiB/s (since 1st msg)\n\ +\tReceived:{9} MiB]\n + +twoPhaseCommitInterceptor.expiredMessage=Removing expired message [{0}] +twoPhaseCommitInterceptor.heartbeat.failed=Unable to perform heartbeat on the TwoPhaseCommit interceptor. +twoPhaseCommitInterceptor.originalMessage.missing=Received a confirmation, but original message is missing. Id:[{0}] diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_cs.properties new file mode 100644 index 0000000..074dd42 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_cs.properties @@ -0,0 +1,59 @@ +# 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. + +encryptInterceptor.decrypt.error.short-message=DeÅ¡ifrování zprávy selhalo: pÅ™edÄasný konec zprávy + +gzipInterceptor.report=GZip Interceptor Report[\n\ +\tCelkový poÄet zpráv: {0}\n\ +\tTx zprávy - komprimované: {1}\n\ +\tTx zprávy - nekomprimované: {2}\n\ +\tRx zprávy - komprimované: {3}\n\ +\tRx zprávy - nekomprimované: {4}\n\ +\tCelkový poÄet Tx bytů: {5}\n\ +\tKomprimovano Tx bytů: {6}\n\ +\tNekomprimovano Tx bytů: {7}\n\ +\tCelkový poÄet Rx bytů: {8}\n\ +\tKomprimovano Rx bytů: {9}\n\ +\tNekomprimovano Rx bytů: {10}\n\ +] + +messageDispatchInterceptor.queue.full=Asynchronní fronta je plná, dosáhla svého limitu [{0}] bytů, aktuálnÄ›:[{1}] bytů. + +nonBlockingCoordinator.memberAlive.failed=Nelze provést kontrolu bÄ›hu Älena, Älen se považuje za nefunkÄní. +nonBlockingCoordinator.processCoordinationMessage.failed=Chyba pÅ™i zpracování koordinaÄní zprávy. Může být závažné. + +staticMembershipInterceptor.no.pingInterceptor=Není definován žádný TcpPingInterceptor. Kontrola stavu statických Älenů nefunguje správnÄ›. Po definování TcpPingInterceptor bude kontrola stavu funkÄní. +staticMembershipInterceptor.sendShutdown.failed=Odeslání události k ukonÄení selhalo. + +tcpFailureDetector.failureDetection.failed=NepodaÅ™ilo se provést kontrolu detekce selhání, Älen se považuje za nefunkÄní.[{0}] +tcpFailureDetector.heartbeat.failed=Nelze provést heartbeat na TcpFailureDetector. +tcpFailureDetector.still.alive=Verifikace dokonÄena. ÄŒlen [{0}] je stále živý. +tcpFailureDetector.suspectMember.alive=PodezÅ™elý Älen, živost potvrzena.[{0}] + +tcpPingInterceptor.ping.failed=Nelze poslat TCP ping. + +throughputInterceptor.report=ThroughputInterceptor Výpis[\n\ +\tOdchozí zprávy:{0} zpráv\n\ +\tOdesláno:{1} MiB (celkem)\n\ +\tOdesláno:{2} MiB (aplikace)\n\ +\tÄŒas:{3} sekund\n\ +\tOdchozí rychlost:{4} MiB/s (celkem)\n\ +\tOdchozí rychlost:{5} MiB/s (aplikace)\n\ +\tChybové zprávy:{6}\n\ +\tPříchozí zprávy:{7} zpráv\n\ +\tPříchozí rychlost:{8} MiB/s (od první zprávy)\n\ +\tPÅ™ijato:{9} MiB] + +twoPhaseCommitInterceptor.heartbeat.failed=Nelze provést heartbeat na TwoPhaseCommit interceptoru. diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_de.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_de.properties new file mode 100644 index 0000000..1ac61b5 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_de.properties @@ -0,0 +1,33 @@ +# 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. + +domainFilterInterceptor.member.refused=Mitglied [{0}] wurde nicht zum Cluster zugelassen + +encryptInterceptor.decrypt.error.short-message=Konnte die Nachricht nicht entschlüsseln: Vorzeitiges Ende der Nachricht +encryptInterceptor.decrypt.failed=Nachricht konnte nicht entschlüsselt werden + +messageDispatchInterceptor.queue.full=Asynchrone Warteschlange ist voll. Das Limit von [{0}] Bytes ist erreicht mit aktuell [{1}] Bytes. + +nonBlockingCoordinator.memberAlive.failed=Member Alive-Check kann nicht ausgeführt werden, down angenommen. +nonBlockingCoordinator.processCoordinationMessage.failed=Fehler beim Verarbeiten der Koordinationsnachricht. Könnte Fatal sein. + +staticMembershipInterceptor.sendShutdown.failed=Benachrichtigung über den Shutdown schlug fehl. + +tcpFailureDetector.failureDetection.failed=Ãœberprüng zur Fehlererkennung fehlgeschlagen, Mitglied [{0}] +tcpFailureDetector.still.alive=Verifikation abgeschlossen. Member sind immer noch am Leben [{0}] + +tcpPingInterceptor.ping.failed=Konnte kein TCP Ping senden. + +twoPhaseCommitInterceptor.heartbeat.failed=Kann den Heartbeat auf dem TwoPhaseCommit Interceptor nicht durchführen. diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_es.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_es.properties new file mode 100644 index 0000000..a528b18 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_es.properties @@ -0,0 +1,61 @@ +# 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. + +domainFilterInterceptor.message.refused=El mensaje [{0}] recibido del cluster fue rechazado + +encryptInterceptor.decrypt.error.short-message=Fallo al descifrar el mensaje: fin-de-mensaje prematuro + +gzipInterceptor.report=Reporte del Interceptor GZip[\n\ +\tTotal de Mensajes: {0}\n\ +\tTx Mensajes Comprimidos: {1}\n\ +\tTx Mensajes No Comprimidos: {2}\n\ +\tRx Comrimidos Mensajes: {3}\n\ +\tRx Mensajes No Comprimidos: {4}\n\ +\tTotal Tx bytes: {5}\n\ +\tTx bytes Comprimidos: {6}\n\ +\tTx bytes No Comprimidos: {7}\n\ +\tTotal Rx bytes: {8}\n\ +\tRx bytes Comprimidos: {9}\n\ +\tRx bytes No Comprimidos: {10}\n\ +] + +messageDispatchInterceptor.queue.full=La cola asincrónica esta llena, se alcanzó el limite de [{0}] bytes, actualmente:[{1}] bytes.\n + +nonBlockingCoordinator.memberAlive.failed=No se puede verificar si el miembro esta vivo, asumiendo que el miembro esta inactivo. +nonBlockingCoordinator.processCoordinationMessage.failed=Error procesando el mensaje de coordinación. Puede ser fatal.\n + +staticMembershipInterceptor.no.pingInterceptor=No existe TcpPingInterceptor. El verificador de estado de miembros estáticos no trabaja correctamente. Al definir el TcpPingInterceptor, el verificador de estado de miembros estáticos trabajará correctamente. +staticMembershipInterceptor.sendShutdown.failed=El aviso de apagado falló. + +tcpFailureDetector.failureDetection.failed=No se pudo realizar la verificación de detección de fallos, se asume que el membro esta abajo.[{0}] +tcpFailureDetector.heartbeat.failed=Imposible ejecutar heartbeat en el TcpFailureDetector. +tcpFailureDetector.member.disappeared=Verificación completada. Miembro desaparecido[{0}] +tcpFailureDetector.still.alive=Verificación completa. El miembro aun esta vivo [{0}] +tcpFailureDetector.suspectMember.alive=Se confima que esta vivo el miembro.[{0}]\n + +tcpPingInterceptor.ping.failed=Imposible enviar ping TCP + +throughputInterceptor.report=ThroughputInterceptor Reporte[\n\ +\tTx Msg:{0} mensajes\n\ +\tEnviados:{2} MiB (aplicación)\n\ +\tTiempo:{3} segundos\n\ +\tTx Speed:{4} MiB/s (total)\n\ +\tTx Speed::{5} MiB/s (aplicación)\n\ +\tMsg error:{6}\n\ +\tRx Msg:{7} mensajes\n\ +\tRx Speed:{8} MiB/s (desde 1er msg)\n\ +\tRecivido:{9} MiB] + +twoPhaseCommitInterceptor.heartbeat.failed=Incapáz de ejecutar heartbeat en el interceptor TwoPhaseCommit diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties new file mode 100644 index 0000000..b281a76 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties @@ -0,0 +1,105 @@ +# 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. + +domainFilterInterceptor.member.refused=Le membre [{0}] a été refusé dans le cluster +domainFilterInterceptor.message.refused=Le message reçu du cluster [{0}] a été refusé + +encryptInterceptor.algorithm.required=Un algorithme de cryptage est requis, avec une spécification complète telle que AES/CBC/PKCS5Padding +encryptInterceptor.algorithm.unsupported-mode=L''EncryptInterceptor ne supporte pas le mode de chiffrage de bloc [{0}] +encryptInterceptor.decrypt.error.short-message=Echec du décryptage du message : fin de message prématuré +encryptInterceptor.decrypt.failed=Echec de décryptage du message +encryptInterceptor.encrypt.failed=Erreur de cryptage du message +encryptInterceptor.init.failed=Echec de l'initalisation d'EncryptInterceptor +encryptInterceptor.key.required=Une clé de cryptage est requise +encryptInterceptor.tcpFailureDetector.ordering=EncryptInterceptor doit être en amont de TcpFailureDetector, l'EncryptInterceptor doit être repositionné pour être listé avant TcpFailureDetector dans le pipeline d'intercepteurs du canal + +fragmentationInterceptor.fragments.missing=Les fragments sont manquants +fragmentationInterceptor.heartbeat.failed=Impossible d'effectuer le nettoyage périodique de l'intercepteur de fragments + +gzipInterceptor.compress.failed=Impossible de compresser un contenu binaire +gzipInterceptor.decompress.failed=Impossible de décompresser le contenu des octets +gzipInterceptor.report=Rapport de l''intercepteur GZIP [\n\ +\ Total messages : {0}\n\ +\ Messages compressés envoyés : {1}\n\ +\ Messages non compressés envoyés : {2}\n\ +\ Messages compressés reçus : {3}\n\ +\ Messages non compressés reçus {4}\n\ +\ Octets (bytes) envoyés : {5}\n\ +\ Octets (bytes) compressés envoyés : {6}\n\ +\ Octets (bytes) non compressés envoyés : {7}\n\ +\ Octets (bytes) reçus : {8}\n\ +\ Octets (bytes) compressés envoyés : {9}\n\ +\ Octets (bytes) non compressés envoyés : {10}\n\ +] + +messageDispatchInterceptor.AsyncMessage.failed=Erreur lors du traitement du message asynchrone +messageDispatchInterceptor.completeMessage.failed=Impossible de renvoyer le message complet +messageDispatchInterceptor.errorMessage.failed=Impossible d'envoyer le message d'erreur +messageDispatchInterceptor.queue.full=La file d''attente asynchrone est pleine, ayant atteint sa limite de [{0}] octets. Actuellement : [{1}] octets. +messageDispatchInterceptor.unableAdd.queue=Impossible d'ajouter le message à la file asynchrone. Bogue de file ? +messageDispatchInterceptor.warning.optionflag=Attention, vous passez outre le drapeau d'option d'asynchronicité ("asynchronous option flag"), cela désactivera Channel.SEND_OPTIONS_ASYNCHRONOUS, que d'autres applications sont susceptibles d'utiliser. + +nonBlockingCoordinator.electionMessage.sendfailed=Impossible d''envoyer le message d''élection à : [{0}] +nonBlockingCoordinator.heartbeat.failed=Impossible d'effectuer le signal périodique +nonBlockingCoordinator.heartbeat.inconsistency=Le coordinateur à trouvé un état inconsistant, redémarrage de l'élection +nonBlockingCoordinator.memberAdded.failed=Impossible de démarrer une élection quand le membre a été ajouté +nonBlockingCoordinator.memberAlive.failed=Impossible d'effectuer le test de vie du membre, assume membre inactif. +nonBlockingCoordinator.memberDisappeared.failed=Impossible de démarrer une élection lorsqu'un membre a été enlevé +nonBlockingCoordinator.processCoordinationMessage.failed=Echec de traitement de message de coordination. Pourrait être fatal. +nonBlockingCoordinator.report=CoordinationEvent[type={0}\n\ +\tLocal : {1}\n\ +\tCoord : {2}\n\ +\tVue : {3}\n\ +\tVue Suggérée : {4}\n\ +\tMembres : {5}\n\ +\tInfo : {6}\n\ +] + +orderInterceptor.messageAdded.sameCounter=Le message ajouté a le même compteur, à cause d'un bug de synchronisation, l'intercepteur d'ordre doit être désactivé + +staticMembershipInterceptor.no.failureDetector=Il n'y a pas de détecteur TcpFailureDetector. La détection automatique de membres statiques ne fonctionne pas correctement. Par la définition d'un intercepteur StaticMembershipInterceptor sous le TcpFailureDetector, cette détection automatique fonctionnera. +staticMembershipInterceptor.no.pingInterceptor=Il n'y a pas de TcpPingInterceptor. Le test de bonne santé des membres statiques ne fonctionne pas correctement. En définissant le TcpPingInterceptor, le test de bonne santé des membres statiques fonctionnera. +staticMembershipInterceptor.sendLocalMember.failed=La notification du membre local a échouée +staticMembershipInterceptor.sendShutdown.failed=La notification d''arrêt a échoué. + +tcpFailureDetector.already.disappeared=La vérification est terminée, le membre avait déjà disparu [{0}] +tcpFailureDetector.failureDetection.failed=Impossible d''effectuer le test de détection de faute. Membre [{0}] supposé inactif. +tcpFailureDetector.heartbeat.failed=Incapable de faire une pulsation ("heatbeat") sur le TcpFailureDector +tcpFailureDetector.member.disappeared=La vérfication est complète, le membre a disparu [{0}] +tcpFailureDetector.memberDisappeared.verify=Reçu un message memberDisappeared[{0}], qui sera vérifié +tcpFailureDetector.performBasicCheck.memberAdded=Le membre a été ajouté bien qu''aucune notification n''ait été reçue : [{0}] +tcpFailureDetector.recievedPacket=Réception d''un paquet de détection d''échec [{0}] +tcpFailureDetector.still.alive=Vérification terminée. Le membre [{0}] vit toujours +tcpFailureDetector.suspectMember.alive=Membre suspect, confirmé vivant.[{0}] +tcpFailureDetector.suspectMember.dead=Un membre suspect a été confirmé mort [{0}] + +tcpPingInterceptor.ping.failed=Impossible d'envoyer un ping TCP. +tcpPingInterceptor.pingFailed.pingThread=Impossible d'envoyer un ping à partir du thread des ping TCP + +throughputInterceptor.report=Rapport de l''intercepteur du débit ("ThroughputInterceptor Report") [\n\ +\tMsg Transmis (Tx Msg) : {0} messages\n\ +\tEnvoyé (Sent) : {1} MiB (total)\n\ +\tEnvoyé (Sent) : {2} MiB (application)\n\ +\tDurée (Time) : {3} secondes\n\ +\tVitesse d''écriture (Tx Speed) : {4} MiB/s (total)\n\ +\tVitesse d''écriture (Tx Speed) : {5} MiB/s (application)\n\ +\tMsg d''erreur (Error Msg) : {6}\n\ +\tMsg Reçus (Rx Msg) : {7} messages\n\ +\tVitesse de Réception (Rx Speed) : {8} MiB/s (depuis le 1er message)\n\ +\tReçu : {9} MiB] + +twoPhaseCommitInterceptor.expiredMessage=Retrait du message expiré [{0}] +twoPhaseCommitInterceptor.heartbeat.failed=Impossible d'exécuter un battement de coeur (heartbeat) sur l'intercepteur (interceptor) "TwoPhaseCommit". +twoPhaseCommitInterceptor.originalMessage.missing=Reçue une confirmation mais le message d''origine manque, id : [{0}] diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties new file mode 100644 index 0000000..78e81a5 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties @@ -0,0 +1,105 @@ +# 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. + +domainFilterInterceptor.member.refused=メンãƒãƒ¼ã¯ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ [{0}] ã¸ã®å‚加を拒å¦ã•ã‚Œã¾ã—ãŸã€‚ +domainFilterInterceptor.message.refused=クラスター [{0}] ã‹ã‚‰å—ä¿¡ã—ãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã¯æ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ + +encryptInterceptor.algorithm.required=æš—å·åŒ–アルゴリズムãŒå¿…è¦ã§ã™ã€‚完全指定。 AES/CBC/PKCS5Padding +encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptorã¯ãƒ–ロック暗å·ãƒ¢ãƒ¼ãƒ‰ [{0}]をサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 +encryptInterceptor.decrypt.error.short-message=メッセージã®å¾©å·ã«å¤±æ•—: メッセージã®æœ«å°¾ãŒé€”切れã¦ã„ã¾ã™ +encryptInterceptor.decrypt.failed=メッセージã®å¾©å·ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +encryptInterceptor.encrypt.failed=メッセージを暗å·åŒ–ã§ãã¾ã›ã‚“。 +encryptInterceptor.init.failed=EncryptInterceptorã®åˆæœŸåŒ–ã«å¤±æ•—ã—ã¾ã—㟠+encryptInterceptor.key.required=æš—å·åŒ–キーãŒå¿…è¦ã§ã™ã€‚ +encryptInterceptor.tcpFailureDetector.ordering=EncryptInterceptorã¯TcpFailureDetectorã®ä¸Šæµã«ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 ãƒãƒ£ãƒãƒ«ã‚¤ãƒ³ã‚¿ãƒ¼ã‚»ãƒ—ターパイプラインã®TcpFailureDetectorã®å‰ã«ãƒªã‚¹ãƒˆã•ã‚Œã‚‹ã‚ˆã†ã«EncryptInterceptorã‚’å†è¨­å®šã—ã¦ãã ã•ã„。 + +fragmentationInterceptor.fragments.missing=フラグメントãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +fragmentationInterceptor.heartbeat.failed=fragmentationInterceptor ã§ãƒãƒ¼ãƒˆãƒ“ートクリーンアップを実行ã§ãã¾ã›ã‚“ + +gzipInterceptor.compress.failed=ãƒã‚¤ãƒˆãƒ‡ãƒ¼ã‚¿ã‚’圧縮ã§ãã¾ã›ã‚“ +gzipInterceptor.decompress.failed=圧縮ã•ã‚ŒãŸãƒã‚¤ãƒˆãƒ‡ãƒ¼ã‚¿ã‚’展開ã§ãã¾ã›ã‚“ +gzipInterceptor.report=GZip Interceptor Report[\n\ +\tåˆè¨ˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸: {0}\n\ +\t圧縮ã—ãŸé€ä¿¡ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸: {1}\n\ +\téžåœ§ç¸®ã®é€ä¿¡ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸: {2}\n\ +\t圧縮ã—ãŸå—信メッセージ: {3}\n\ +\téžåœ§ç¸®ã®å—信メッセージ: {4}\n\ +\tåˆè¨ˆé€ä¿¡ ãƒã‚¤ãƒˆ: {5}\n\ +\t圧縮ã•ã‚ŒãŸé€ä¿¡ãƒã‚¤ãƒˆ: {6}\n\ +\téžåœ§ç¸®ã®é€ä¿¡ãƒã‚¤ãƒˆ: {7}\n\ +\tåˆè¨ˆå—ä¿¡ãƒã‚¤ãƒˆ: {8}\n\ +\t圧縮ã—ãŸå—ä¿¡ãƒã‚¤ãƒˆ: {9}\n\ +\téžåœ§ç¸®ã®å—ä¿¡ãƒã‚¤ãƒˆ: {10}\n\ +] + +messageDispatchInterceptor.AsyncMessage.failed=éžåŒæœŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸å‡¦ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +messageDispatchInterceptor.completeMessage.failed=完了ã—ãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’報告ã§ãã¾ã›ã‚“。 +messageDispatchInterceptor.errorMessage.failed=エラーメッセージを返ã™ã“ã¨ãŒã§ãã¾ã›ã‚“。 +messageDispatchInterceptor.queue.full=éžåŒæœŸã‚­ãƒ¥ãƒ¼ãŒæº€æ¯ã§ã™ã€‚ç¾åœ¨ã¯ [{1}] ãƒã‚¤ãƒˆã§ä¸Šé™ã® [{0}] ãƒã‚¤ãƒˆã«é”ã—ã¦ã„ã¾ã™ã€‚ +messageDispatchInterceptor.unableAdd.queue=éžåŒæœŸã‚­ãƒ¥ãƒ¼ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’登録ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚キューã®ä¸å…·åˆã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。 +messageDispatchInterceptor.warning.optionflag=警告: éžåŒæœŸã‚ªãƒ—ションフラグを上書ãã—ãŸãŸã‚ã€ä»–ã®ã‚¢ãƒ—リケーションãŒä½¿ç”¨ã™ã‚‹å¯èƒ½æ€§ã®ã‚ã‚‹ Channel.SEND_OPTIONS_ASYNCHRONOUS ã¯ç„¡åŠ¹åŒ–ã•ã‚Œã¾ã™ã€‚ + +nonBlockingCoordinator.electionMessage.sendfailed=メンãƒãƒ¼ [{0}] ã«èª¿åœãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 +nonBlockingCoordinator.heartbeat.failed=ãƒãƒ¼ãƒˆãƒ“ートを実行ã§ãã¾ã›ã‚“。 +nonBlockingCoordinator.heartbeat.inconsistency=ãƒãƒ¼ãƒˆãƒ“ートãŒä¸ä¸€è‡´ã‚’発見ã—ã€ã‚¤ãƒ¬ã‚¯ã‚·ãƒ§ãƒ³ã‚’å†é–‹ã—ã¾ã™ã€‚ +nonBlockingCoordinator.memberAdded.failed=メンãƒãƒ¼ãŒè¿½åŠ ã•ã‚ŒãŸã¨ãã«ã‚¤ãƒ¬ã‚¯ã‚·ãƒ§ãƒ³ã‚’開始ã§ãã¾ã›ã‚“。 +nonBlockingCoordinator.memberAlive.failed=動作ãƒã‚§ãƒƒã‚¯ã‚’実行ã§ããªã„ãŸã‚ã€ãƒ¡ãƒ³ãƒãƒ¼ã¯åœæ­¢ã—ã¦ã„ã‚‹ã‚‚ã®ã¨ã—ã¦æ‰±ã„ã¾ã™ã€‚ +nonBlockingCoordinator.memberDisappeared.failed=メンãƒãƒ¼ãŒå‰Šé™¤ã•ã‚ŒãŸã¨ãã«ã‚¤ãƒ¬ã‚¯ã‚·ãƒ§ãƒ³ã‚’開始ã§ãã¾ã›ã‚“。 +nonBlockingCoordinator.processCoordinationMessage.failed=調åœãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ã€‚致命的ãªå•é¡ŒãŒç™ºç”Ÿã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +nonBlockingCoordinator.report=CoordinationEvent[type={0}\n\ +\tLocal: {1}\n\ +\tCoord: {2}\n\ +\tView: {3}\n\ +\tSuggested View: {4}\n\ +\tMembers: {5}\n\ +\tInfo: {6}\n\ +] + +orderInterceptor.messageAdded.sameCounter=åŒã˜ã‚«ã‚¦ãƒ³ã‚¿ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãŒè¿½åŠ ã•ã‚Œã¾ã—ãŸã€‚åŒæœŸãƒã‚°ãŒã‚ã‚Šã¾ã™ã€‚ Order インターセプタを無効ã«ã—ã¦ä¸‹ã•ã„。 + +staticMembershipInterceptor.no.failureDetector=TcpFailureDetector ãŒã‚ã‚Šã¾ã›ã‚“。é™çš„メンãƒãƒ¼ã®è‡ªå‹•æ¤œå‡ºæ©Ÿèƒ½ã¯æ­£å¸¸ã«å‹•ä½œã—ã¾ã›ã‚“。TcpFailureDetector é…下㫠StaticMembershipInterceptor を定義ã™ã‚Œã°ã€é™çš„メンãƒãƒ¼ã®è‡ªå‹•æ¤œå‡ºæ©Ÿèƒ½ãŒå‹•ä½œã™ã‚‹ã§ã—ょã†ã€‚ +staticMembershipInterceptor.no.pingInterceptor=TcpPingInterceptorãŒå­˜åœ¨ã—ãªã„ãŸã‚ã€é™çš„メンãƒãƒ¼ã®ãƒ˜ãƒ«ã‚¹ãƒã‚§ãƒƒã‚¯ã¯æ­£å¸¸ã«æ©Ÿèƒ½ã—ã¾ã›ã‚“。TcpPingInterceptorを定義ã™ã‚Œã°æ©Ÿèƒ½ã™ã‚‹ã§ã—ょã†ã€‚ +staticMembershipInterceptor.sendLocalMember.failed=ローカルメンãƒãƒ¼ã®é€šçŸ¥ã¯å¤±æ•—ã—ã¾ã—ãŸã€‚ +staticMembershipInterceptor.sendShutdown.failed=シャットダウン通知ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ + +tcpFailureDetector.already.disappeared=検証完了。メンãƒãƒ¼ã¯ã™ã§ã«é›¢è„±ã—ã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¾ã—㟠[{0}] +tcpFailureDetector.failureDetection.failed=故障検出ãƒã‚§ãƒƒã‚¯ãŒå®Ÿè¡Œã§ããªã„ãŸã‚ã€ãƒ¡ãƒ³ãƒãƒ¼ãŒåœæ­¢ã—ã¦ã„ã‚‹ã‚‚ã®ã¨ã—ã¦æ‰±ã„ã¾ã™ã€‚[{0}] +tcpFailureDetector.heartbeat.failed=TcpFailureDetector ã®ãƒãƒ¼ãƒˆãƒ“ートãƒã‚§ãƒƒã‚¯ãŒã§ãã¾ã›ã‚“。 +tcpFailureDetector.member.disappeared=メンãƒæ¤œè¨¼ãŒå®Œäº†ã—ã¾ã—ãŸã€‚メンãƒãƒ¼ãŒæ¶ˆãˆã¾ã—㟠[{0}] +tcpFailureDetector.memberDisappeared.verify=memberDisappeared[{0}]メッセージをå—ä¿¡ã—ã¾ã—ãŸã€‚メンãƒæ¤œè¨¼ã—ã¾ã™ã€‚ +tcpFailureDetector.performBasicCheck.memberAdded=ç§ãŸã¡ã«é€šçŸ¥ã•ã‚Œãªã‹ã£ãŸã«ã‚‚ã‹ã‹ã‚らãšã€ãƒ¡ãƒ³ãƒãƒ¼ãŒè¿½åŠ ã•ã‚Œã¾ã—ãŸï¼š[{0}] +tcpFailureDetector.recievedPacket=障害検出パケット [{0}] ã‚’å—ä¿¡ã—ã¾ã—㟠+tcpFailureDetector.still.alive=故障検出ãƒã‚§ãƒƒã‚¯ãŒå®Œäº†ã—ã¾ã—ãŸã€‚メンãƒãƒ¼ [{0}] ã¯æ­£å¸¸ã§ã™ã€‚ +tcpFailureDetector.suspectMember.alive=ç–‘ã‚ã—ã„クラスタメンãƒãƒ¼ã®ç”Ÿå­˜ã‚’確èªã—ã¾ã—ãŸã€‚[{0}] +tcpFailureDetector.suspectMember.dead=疑義メンãƒãŒæ­»äº¡ã—ãŸã“ã¨ãŒç¢ºèªã•ã‚Œã¾ã—ãŸã€‚[{0}] + +tcpPingInterceptor.ping.failed=TCP ã® ping ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 +tcpPingInterceptor.pingFailed.pingThread=TCP pingスレッドã‹ã‚‰pingã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 + +throughputInterceptor.report=ThroughputInterceptor Report[\n\ +\ é€ä¿¡ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ (Tx Msg):{0} messages\n\ +\ é€ä¿¡æ¸ˆã¿ (Sent):{1} MiB (total)\n\ +\ é€ä¿¡æ¸ˆã¿ (Sent):{2} MiB (application)\n\ +\ 時間 (Time):{3} seconds\n\ +\ é€ä¿¡é€Ÿåº¦ (Tx Speed):{4} MiB/s (total)\n\ +\ é€ä¿¡é€Ÿåº¦ (Tx Speed):{5} MiB/s (application)\n\ +\ エラーメッセージ (Error Msg):{6}\n\ +\ å—信メッセージ (Rx Msg):{7} messages\n\ +\ å—信速度 (Rx Speed):{8} MiB/s (since 1st msg)\n\ +\ å—信済㿠(Received):{9} MiB] + +twoPhaseCommitInterceptor.expiredMessage=期é™åˆ‡ã‚Œã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å–り除ãã¾ã—ãŸã€‚ [{0}] +twoPhaseCommitInterceptor.heartbeat.failed=TwoPhaseCommit インターセプターã®ãƒãƒ¼ãƒˆãƒ“ートãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ +twoPhaseCommitInterceptor.originalMessage.missing=確èªã‚’å—ä¿¡ã—ã¾ã—ãŸãŒã€å…ƒã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãŒã‚ã‚Šã¾ã›ã‚“。 Id:[{0}] diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties new file mode 100644 index 0000000..85f6685 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties @@ -0,0 +1,105 @@ +# 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. + +domainFilterInterceptor.member.refused=멤버 [{0}]ì´(ê°€) í´ëŸ¬ìŠ¤í„°ì— 참가하는 ê²ƒì´ ê±°ë¶€ë˜ì—ˆìŠµë‹ˆë‹¤. +domainFilterInterceptor.message.refused=í´ëŸ¬ìŠ¤í„° [{0}](으)로부터 ë°›ì€ ë©”ì‹œì§€ê°€ 거부ë˜ì—ˆìŠµë‹ˆë‹¤. + +encryptInterceptor.algorithm.required=암호화 ì•Œê³ ë¦¬ì¦˜ì„ ì™„ì „í•˜ê²Œ 지정해야 합니다. 예) AES/CBC/PKCS5Padding. +encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptorê°€ ë¸”ë¡ cipher 모드 [{0}]ì„(를) 지ì›í•˜ì§€ 않습니다. +encryptInterceptor.decrypt.error.short-message=메시지를 í•´ë…하지 못했습니다: 메시지가 너무 ì¼ì° ë났습니다 (premature end-of-message). +encryptInterceptor.decrypt.failed=메시지를 í•´ë…하지 못했습니다. +encryptInterceptor.encrypt.failed=메시지를 암호화하지 못했습니다. +encryptInterceptor.init.failed=EncryptInterceptor를 초기화하지 못했습니다. +encryptInterceptor.key.required=암호화 키가 필수ì ìž…니다. +encryptInterceptor.tcpFailureDetector.ordering=EncryptInterceptor는 반드시 TcpFailureDetector 보다 먼저 위치해야 합니다. ì±„ë„ ì¸í„°ì…‰í„° 파ì´í”„ë¼ì¸ ë‚´ì—ì„œ, EncryptInterceptorê°€ TcpFailureDetector 보다 먼저 위치하ë„ë¡ ì¡°ì •í•˜ì‹­ì‹œì˜¤. + +fragmentationInterceptor.fragments.missing=Fragmentë“¤ì´ ì—†ìŠµë‹ˆë‹¤. +fragmentationInterceptor.heartbeat.failed=FragmentationInterceptorì—ì„œ heartbeat를 clean up í•  수 없습니다. + +gzipInterceptor.compress.failed=ë°”ì´íŠ¸ 컨í…íŠ¸ë“¤ì„ ì••ì¶•í•  수 없습니다. +gzipInterceptor.decompress.failed=ë°”ì´íŠ¸ 컨í…íŠ¸ë“¤ì˜ ì••ì¶•ì„ í’€ 수 없습니다. +gzipInterceptor.report=GZip Interceptor Report[\n\ +\tTotal Messages: {0}\n\ +\tTx Messages Compressed: {1}\n\ +\tTx Messages Uncompressed: {2}\n\ +\tRx Messages Compressed: {3}\n\ +\tRx Messages Uncompressed: {4}\n\ +\tTotal Tx bytes: {5}\n\ +\tCompressed Tx bytes: {6}\n\ +\tUncompressed Tx bytes: {7}\n\ +\tTotal Rx bytes: {8}\n\ +\tCompressed Rx bytes: {9}\n\ +\tUncompressed Rx bytes: {10}\n\ +] + +messageDispatchInterceptor.AsyncMessage.failed=비ë™ê¸° 메시지를 처리하는 중 오류 ë°œìƒ +messageDispatchInterceptor.completeMessage.failed=ì™„ë£Œëœ ë©”ì‹œì§€ë¥¼ ë˜ëŒë ¤ ë³´ê³ í•  수 없습니다. +messageDispatchInterceptor.errorMessage.failed=오류 메시지를 ë˜ëŒë ¤ ë³´ê³ í•  수 없습니다. +messageDispatchInterceptor.queue.full=비ë™ê¸° íê°€ 꽉 차서 í•œê³„ê°’ì¸ [{0}] ë°”ì´íŠ¸ì— ë„달했습니다. 현재 ê°’: [{1}] ë°”ì´íŠ¸. +messageDispatchInterceptor.unableAdd.queue=비ë™ê¸° íì— ë©”ì‹œì§€ë¥¼ 추가할 수 없습니다. íì˜ ë²„ê·¸ì¼ê¹Œìš”? +messageDispatchInterceptor.warning.optionflag=경고: 귀하는 비ë™ê¸° 옵션 플래그를 오버ë¼ì´ë“œí•˜ê³  있는ë°, ì´ëŠ” 다른 애플리케ì´ì…˜ë“¤ì´ 사용할 ìˆ˜ë„ ìžˆëŠ” Channel.SEND_OPTIONS_ASYNCHRONOUS ì˜µì…˜ì„ ì‚¬ìš© 불능 ìƒíƒœë¡œ 만들 것입니다. + +nonBlockingCoordinator.electionMessage.sendfailed=Election 메시지를 [{0}]ì— ë³´ë‚¼ 수 없습니다. +nonBlockingCoordinator.heartbeat.failed=Heartbeat를 수행할 수 없습니다. +nonBlockingCoordinator.heartbeat.inconsistency=Heartbeatê°€ ì¼ê´€ë˜ì§€ ì•Šì€ ìƒíƒœë¡œ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. Electionì„ ë‹¤ì‹œ 시작합니다. +nonBlockingCoordinator.memberAdded.failed=멤버가 추가ë˜ì—ˆì„ ë•Œ, electionì„ ì‹œìž‘í•  수 없었습니다. +nonBlockingCoordinator.memberAlive.failed=멤버가 살아있는지 ì ê²€í•  수 없습니다. ì•„ë§ˆë„ í•´ë‹¹ 멤버가 ë‹¤ìš´ëœ ê²ƒ 같습니다. +nonBlockingCoordinator.memberDisappeared.failed=멤버가 제거ë˜ì—ˆì„ ë•Œ, electionì„ ì‹œìž‘í•  수 없었습니다. +nonBlockingCoordinator.processCoordinationMessage.failed=CoordinationMessage 처리 중 오류 ë°œìƒ. 치명ì ì¸ ì˜¤ë¥˜ì¼ ìˆ˜ 있습니다. +nonBlockingCoordinator.report=CoordinationEvent[type={0}\n\ +\tLocal: {1}\n\ +\tCoord: {2}\n\ +\tView: {3}\n\ +\tSuggested View: {4}\n\ +\tMembers: {5}\n\ +\tInfo: {6}\n\ +] + +orderInterceptor.messageAdded.sameCounter=ì¶”ê°€ëœ ë©”ì‹œì§€ê°€ ë™ì¼í•œ 카운터를 가지고 있습니다. ë™ê¸°í™” 결함입니다. OrderInterceptor를 사용불능 ìƒíƒœë¡œ 설정하십시오. + +staticMembershipInterceptor.no.failureDetector=TcpFailureDetectorê°€ 없습니다. ì •ì  ë©¤ë²„ë“¤ì— ëŒ€í•œ ìžë™ íƒì§€ê°€ ì •ìƒ ë™ìž‘하지 ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. TcpFailureDetector ì•„ëž˜ì— StaticMembershipInterceptor를 ì •ì˜í•˜ê²Œ ë˜ë©´, ì •ì  ë©¤ë²„ë“¤ì— ëŒ€í•œ ìžë™ íƒì§€ê°€ ì •ìƒ ë™ìž‘í•  것입니다. +staticMembershipInterceptor.no.pingInterceptor=TcpPingInterceptorê°€ 존재하지 않습니다. ì •ì  ë©¤ë²„ë“¤ì— ëŒ€í•œ heath check는 제대로 ë™ìž‘하지 ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. TcpPingInterceptor를 ì •ì˜í•¨ìœ¼ë¡œì¨, ì •ì  ë©¤ë²„ë“¤ì— ëŒ€í•œ health checkê°€ ì •ìƒ ë™ìž‘í•  것입니다. +staticMembershipInterceptor.sendLocalMember.failed=로컬 멤버 통지 실패 +staticMembershipInterceptor.sendShutdown.failed=ì‹œìŠ¤í…œì„ ì…§ë‹¤ìš´í•˜ê¸° 위한 통지가 실패했습니다. + +tcpFailureDetector.already.disappeared=ê²€ì¦ ì™„ë£Œ. 멤버가 ì´ë¯¸ 사ë¼ì¡ŒìŠµë‹ˆë‹¤: [{0}] +tcpFailureDetector.failureDetection.failed=ë©¤ë²„ì— ëŒ€í•œ 실패 íƒì§€ ì ê²€ì„ 수행할 수 없습니다. ì•„ë§ˆë„ í•´ë‹¹ 멤버 [{0}]ì´(ê°€) ë‹¤ìš´ëœ ê²ƒ 같습니다. +tcpFailureDetector.heartbeat.failed=TcpFailureDetectorì—ì„œ heartbeat ì ê²€ì„ 수행할 수 없습니다. +tcpFailureDetector.member.disappeared=ê²€ì¦ ì™„ë£Œ. 멤버가 사ë¼ì¡ŒìŠµë‹ˆë‹¤: [{0}] +tcpFailureDetector.memberDisappeared.verify=멤버 사ë¼ì§ 메시지를 받았습니다: [{0}]. ì´ë¥¼ ê²€ì¦í•  것입니다. +tcpFailureDetector.performBasicCheck.memberAdded=통지 받지는 못했지만, 멤버가 추가ë˜ì—ˆìŠµë‹ˆë‹¤: [{0}] +tcpFailureDetector.still.alive=ê²€ì¦ ì™„ë£Œ. 멤버가 ì•„ì§ ì‚´ì•„ 있습니다: [{0}] +tcpFailureDetector.suspectMember.alive=ì˜ì‹¬ 멤버 서버가 ì‚´ì•„ 있ìŒì„ 확ì¸í–ˆìŠµë‹ˆë‹¤. [{0}] +tcpFailureDetector.suspectMember.dead=ì˜ì‹¬ 멤버가 ë‹¤ìš´ëœ ê²ƒìœ¼ë¡œ 확ì¸ë¨: [{0}] + +tcpPingInterceptor.ping.failed=TCP pingì„ ë³´ë‚¼ 수 없습니다. +tcpPingInterceptor.pingFailed.pingThread=TCP ping 쓰레드로부터, pingì„ ì „ì†¡í•  수 없습니다. + +throughputInterceptor.report=ThroughputInterceptorì˜ ë³´ê³  [\n\ +\tTx Msg:{0} 메시지(들)\n\ +\tSent:{1} MiB (ì „ì²´)\n\ +\tSent:{2} MiB (애플리케ì´ì…˜)\n\ +\tTime:{3} ì´ˆ\n\ +\tTx Speed:{4} MiB/s (ì „ì²´)\n\ +\tTx Speed:{5} MiB/s (애플리케ì´ì…˜)\n\ +\tError Msg:{6}\n\ +\tRx Msg:{7} 메시지\n\ +\tRx Speed:{8} MiB/s (첫번째 메시지 ì´í›„ë¡œ)\n\ +\tReceived:{9} MiB]\n\ +\n + +twoPhaseCommitInterceptor.expiredMessage=ë§Œë£Œëœ ë©”ì‹œì§€ [{0}]ì„(를) 제거합니다. +twoPhaseCommitInterceptor.heartbeat.failed=해당 TwoPhaseCommit ì¸í„°ì…‰í„°ì—ì„œ heartbeat를 수행할 수 없습니다. +twoPhaseCommitInterceptor.originalMessage.missing=í™•ì¸ í”Œëž˜ê·¸ë¥¼ 받았지만, ì›ë³¸ 메시지가 없습니다. ID:[{0}] diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_pt_BR.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..121ef1d --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_pt_BR.properties @@ -0,0 +1,38 @@ +# 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. + +encryptInterceptor.decrypt.error.short-message=Falha ao decriptar mensagem: fim prematuro da mensagem + +nonBlockingCoordinator.memberAlive.failed=Impossível realizar verificação de membros ativos, assumindo membro está down + +staticMembershipInterceptor.no.pingInterceptor=Não há TcpPingInterceptor, portanto, a verificação de "saúde" dos membros estáticos não funcionam. Por favor, defina TcpPingInterceptor. +staticMembershipInterceptor.sendShutdown.failed=Notificação de desligamento falhou. + +tcpFailureDetector.failureDetection.failed=Impossível realizar detecção de falha, assumindo que membro [{0}] está down +tcpFailureDetector.still.alive=A verificação completa. O Membro esta ativo [{0}] + +tcpPingInterceptor.ping.failed=Imposível enviar ping TCP + +throughputInterceptor.report=ThroughputInterceptor Report[\n\ +\tMsg Tx:{0} mensagens\n\ +\tEnviado:{1} MiB (total)\n\ +\tEnviado:{2} MiB (aplicação)\n\ +\tTempo:{3} segundos\n\ +\tTaxa Tx:{4} MiB/s (total)\n\ +\tTaxa Tx:{5} MiB/s (aplicação)\n\ +\tMsgs erro:{6}\n\ +\tRx Msg:{7} mensagens\n\ +\tTaxa Rx:{8} MiB/s (desde a primeira mensagem)\n\ +\tRecebido:{9} MiB] diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ru.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ru.properties new file mode 100644 index 0000000..49fd7b1 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ru.properties @@ -0,0 +1,26 @@ +# 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. + +domainFilterInterceptor.member.refused=УчаÑтнику [{0}] было отказано в приÑоединении к клаÑтеру + +encryptInterceptor.decrypt.error.short-message=Ðевозможно раÑшифровать Ñообщение: Ñлишком мало Ñимволов + +nonBlockingCoordinator.memberAlive.failed=Ðевозможно проверить учаÑтника, Ñчитаем что упал + +staticMembershipInterceptor.sendShutdown.failed=Ðе удалоÑÑŒ Ñообщить об отключении. + +tcpFailureDetector.still.alive=Проверка завершена. УчаÑтник ещё жив [{0}] + +tcpPingInterceptor.ping.failed=Ðе возможно поÑлать TCP пинг. diff --git a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..f56971c --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties @@ -0,0 +1,104 @@ +# 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. + +domainFilterInterceptor.member.refused=æˆå‘˜è¢«æ‹’ç»åŠ å…¥é›†ç¾¤ cluster[{0}] +domainFilterInterceptor.message.refused=从集群[{0}]中接收的消æ¯è¢«æ‹’ç» + +encryptInterceptor.algorithm.required=加密算法是必需的,充分说明,例如AES / CBC / PKCS5Padding +encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptorä¸æ”¯æŒåˆ†ç»„密ç æ¨¡å¼[{0}] +encryptInterceptor.decrypt.error.short-message=解密消æ¯å¤±è´¥: 结尾消æ¯æå‰ç»“æŸ +encryptInterceptor.decrypt.failed=æ— æ³•è§£å¯†ä¿¡æ¯ +encryptInterceptor.encrypt.failed=æ— æ³•åŠ å¯†ä¿¡æ¯ +encryptInterceptor.init.failed=åˆå§‹åŒ–EncryptInterceptor失败 +encryptInterceptor.key.required=需è¦åŠ å¯†å¯†é’¥ +encryptInterceptor.tcpFailureDetector.ordering=加密拦截器必须ä½äºŽTCP故障检测器的上游。请é‡æ–°è®¢è´­åŠ å¯†æ‹¦æˆªå™¨ï¼Œå°†å…¶åˆ—在通é“拦截器管é“中的TCP故障检测器之å‰ã€‚ + +fragmentationInterceptor.fragments.missing=碎片丢失。 +fragmentationInterceptor.heartbeat.failed=无法在frag拦截器中执行心跳清除 + +gzipInterceptor.compress.failed=无法压缩字节内容 +gzipInterceptor.decompress.failed=无法解压缩字节内容 +gzipInterceptor.report=GZip拦截器报告[\n\ +\tåˆè®¡æ¶ˆæ¯: {0}\n\ +\tTx 压缩的消æ¯: {1}\n\ +\tTx 未压缩的消æ¯: {2}\n\ +\tRx 压缩的消æ¯: {3}\n\ +\tRx 未压缩的消æ¯: {4}\n\ +\tTx 总字节: {5}\n\ +\tTx 压缩的字节: {6}\n\ +\tTx 未压缩的字节: {7}\n\ +\tRx 总字节: {8}\n\ +\tRx 压缩的字节: {9}\n\ +\tRx 未压缩的字节: {10}\n\ +] + +messageDispatchInterceptor.AsyncMessage.failed=处ç†å¼‚步消æ¯æ—¶å‡ºé”™ã€‚ +messageDispatchInterceptor.completeMessage.failed=无法报告已完æˆçš„邮件。 +messageDispatchInterceptor.errorMessage.failed=æ— æ³•å›žä¼ é”™è¯¯ä¿¡æ¯ +messageDispatchInterceptor.queue.full=异步队列已满,达到 [{0}] 字节的é™åˆ¶ï¼Œå½“å‰ï¼š[{1}] 字节 +messageDispatchInterceptor.unableAdd.queue=无法将消æ¯æ·»åŠ åˆ°å¼‚步队列,队列 bug? +messageDispatchInterceptor.warning.optionflag=警告ï¼ä½ æ­£åœ¨è¦†ç›–异步选项标志,这将ç¦ç”¨å…¶å®ƒç¨‹åºå¯èƒ½ç”¨åˆ°çš„ Channel.SEND_OPTIONS_ASYNCHRONOUS。 + +nonBlockingCoordinator.electionMessage.sendfailed=无法将选择消æ¯å‘é€åˆ°ï¼š[{0}] +nonBlockingCoordinator.heartbeat.failed=无法执行心跳 +nonBlockingCoordinator.heartbeat.inconsistency=心跳å‘现ä¸ä¸€è‡´ï¼Œé‡æ–°å¯åŠ¨é€‰ä¸¾ +nonBlockingCoordinator.memberAdded.failed=添加æˆå‘˜åŽæ— æ³•å¼€å§‹é€‰ä¸¾ã€‚ +nonBlockingCoordinator.memberAlive.failed=无法执行æˆå‘˜æ´»åŠ¨æ£€æŸ¥ï¼ŒçŒœæµ‹æˆå‘˜ä¸‹çº¿ã€‚ +nonBlockingCoordinator.memberDisappeared.failed=当æˆå‘˜è¢«ç§»é™¤åŽæ— æ³•å¯åŠ¨é€‰ä¸¾ +nonBlockingCoordinator.processCoordinationMessage.failed=处ç†å调消æ¯æ—¶å‡ºé”™ã€‚ å¯èƒ½æ˜¯è‡´å‘½çš„。 +nonBlockingCoordinator.report=å调事件[类型={0}\n\ +本地:{1}\n\ +å标:{2}\n\ +视图:{3}\n\ +建议视图:{4}\n\ +æˆå‘˜ï¼š{5}\n\ +ä¿¡æ¯ï¼š{6}\n\ +] + +orderInterceptor.messageAdded.sameCounter=添加的消æ¯å…·æœ‰ç›¸åŒçš„计数器,åŒæ­¥é”™è¯¯ã€‚ç¦ç”¨è®¢å•æ‹¦æˆªç¨‹åº + +staticMembershipInterceptor.no.failureDetector=没有TcpFailureDetector。 自动检测é™æ€æˆå‘˜æ— æ³•æ­£å¸¸å·¥ä½œã€‚ 通过在TcpFailureDetector下定义StaticMembershipInterceptor,å¯ä»¥è‡ªåŠ¨æ£€æµ‹é™æ€æˆå‘˜ã€‚ +staticMembershipInterceptor.no.pingInterceptor=在没有TcpPingInterceptor的情况下,é™æ€æˆå‘˜çš„å¥åº·æ£€æŸ¥ä¸ä¼šæ­£å¸¸å·¥ä½œã€‚åªæœ‰å®šä¹‰äº†TcpPingInterceptor,æ‰èƒ½ä½¿å¥åº·æ£€æŸ¥æ­£å¸¸è¿›è¡Œã€‚ +staticMembershipInterceptor.sendLocalMember.failed=本地æˆå‘˜é€šçŸ¥å¤±è´¥ +staticMembershipInterceptor.sendShutdown.failed=关闭通知失败。 + +tcpFailureDetector.already.disappeared=验è¯å®Œæˆã€‚æˆå‘˜å·²æ¶ˆå¤±[{0}]。 +tcpFailureDetector.failureDetection.failed=无法进行失败监测,å‡å®šæˆå‘˜å®•æœºã€‚[{0}] +tcpFailureDetector.heartbeat.failed=TCP心跳检测器无法执行心跳 +tcpFailureDetector.member.disappeared=认è¯å®Œæˆã€‚æˆå‘˜æ¶ˆå¤±[{0}] +tcpFailureDetector.memberDisappeared.verify=(:收到的membermissed[{0}]消æ¯ã€‚将验è¯ã€‚ +tcpFailureDetector.performBasicCheck.memberAdded=æˆå‘˜å·²æ·»åŠ ï¼Œå³ä½¿æˆ‘们未收到通知:[{0}]。 +tcpFailureDetector.still.alive=验è¯å®Œæˆã€‚æˆå‘˜ [{0}] ä»ç„¶å­˜æ´» +tcpFailureDetector.suspectMember.alive=验è¯å¯ç–‘æˆå‘˜æœåŠ¡å™¨è¿˜æ´»ç€ã€‚[{0}] +tcpFailureDetector.suspectMember.dead=å¯ç–‘对象,确认已死。[{0}] + +tcpPingInterceptor.ping.failed=无法å‘é€ TCP ping +tcpPingInterceptor.pingFailed.pingThread=ä¸èƒ½ä»Žping 线程å‘é€ping + +throughputInterceptor.report=åžåé‡æ‹¦æˆªå™¨æŠ¥å‘Š[\n\ +\ 传输消æ¯: {0}消æ¯æ•°\n\ +\ å‘é€ï¼š{1} MiB (总共)\n\ +\ å‘é€ï¼š{2} MiB (应用)\n\ +\ 耗时:{3}秒\n\ +\ 传输速率:{4} MiB/s(总共)\n\ +\ 传输速率:{5} MiB/s(应用)\n\ +\ 错误消æ¯ï¼š{6}\n\ +\ 接收消æ¯ï¼š{7} 消æ¯æ•°\n\ +\ 接收速率:{8} MiB/s(从第一个消æ¯å¼€å§‹ï¼‰\n\ +\ 收到:{9} MiB]\n + +twoPhaseCommitInterceptor.expiredMessage=正在删除过期邮件[{0}] +twoPhaseCommitInterceptor.heartbeat.failed=无法在两阶段æ交拦截器上执行心跳。 +twoPhaseCommitInterceptor.originalMessage.missing=收到确认,但原始邮件丢失。Id:[{0}] diff --git a/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java new file mode 100644 index 0000000..23b4ad8 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java @@ -0,0 +1,333 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ErrorHandler; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.util.ExecutorFactory; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.catalina.tribes.util.TcclThreadFactory; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * The message dispatcher is a way to enable asynchronous communication + * through a channel. The dispatcher will look for the + * Channel.SEND_OPTIONS_ASYNCHRONOUS flag to be set, if it is, it + * will queue the message for delivery and immediately return to the sender. + */ +public class MessageDispatchInterceptor extends ChannelInterceptorBase + implements MessageDispatchInterceptorMBean { + + private static final Log log = LogFactory.getLog(MessageDispatchInterceptor.class); + protected static final StringManager sm = + StringManager.getManager(MessageDispatchInterceptor.class); + + protected long maxQueueSize = 1024*1024*64; //64 MiB + protected volatile boolean run = false; + protected boolean useDeepClone = true; + protected boolean alwaysSend = true; + + protected final AtomicLong currentSize = new AtomicLong(0); + protected ExecutorService executor = null; + protected int maxThreads = 10; + protected int maxSpareThreads = 2; + protected long keepAliveTime = 5000; + + + public MessageDispatchInterceptor() { + setOptionFlag(Channel.SEND_OPTIONS_ASYNCHRONOUS); + } + + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) + throws ChannelException { + boolean async = (msg.getOptions() & + Channel.SEND_OPTIONS_ASYNCHRONOUS) == Channel.SEND_OPTIONS_ASYNCHRONOUS; + if (async && run) { + if ((getCurrentSize()+msg.getMessage().getLength()) > maxQueueSize) { + if (alwaysSend) { + super.sendMessage(destination,msg,payload); + return; + } else { + throw new ChannelException(sm.getString("messageDispatchInterceptor.queue.full", + Long.toString(maxQueueSize), Long.toString(getCurrentSize()))); + } + } + //add to queue + if (useDeepClone) { + msg = (ChannelMessage)msg.deepclone(); + } + if (!addToQueue(msg, destination, payload)) { + throw new ChannelException( + sm.getString("messageDispatchInterceptor.unableAdd.queue")); + } + addAndGetCurrentSize(msg.getMessage().getLength()); + } else { + super.sendMessage(destination, msg, payload); + } + } + + + public boolean addToQueue(final ChannelMessage msg, final Member[] destination, + final InterceptorPayload payload) { + executor.execute(() -> sendAsyncData(msg, destination, payload)); + return true; + } + + + public void startQueue() { + if (run) { + return; + } + String channelName = ""; + if (getChannel().getName() != null) { + channelName = "[" + getChannel().getName() + "]"; + } + executor = ExecutorFactory.newThreadPool(maxSpareThreads, maxThreads, keepAliveTime, + TimeUnit.MILLISECONDS, + new TcclThreadFactory("MessageDispatchInterceptor.MessageDispatchThread" + channelName)); + run = true; + } + + + public void stopQueue() { + run = false; + executor.shutdownNow(); + setAndGetCurrentSize(0); + } + + + @Override + public void setOptionFlag(int flag) { + if ( flag != Channel.SEND_OPTIONS_ASYNCHRONOUS ) { + log.warn(sm.getString("messageDispatchInterceptor.warning.optionflag")); + } + super.setOptionFlag(flag); + } + + + public void setMaxQueueSize(long maxQueueSize) { + this.maxQueueSize = maxQueueSize; + } + + + public void setUseDeepClone(boolean useDeepClone) { + this.useDeepClone = useDeepClone; + } + + @Override + public long getMaxQueueSize() { + return maxQueueSize; + } + + + public boolean getUseDeepClone() { + return useDeepClone; + } + + @Override + public long getCurrentSize() { + return currentSize.get(); + } + + + public long addAndGetCurrentSize(long inc) { + return currentSize.addAndGet(inc); + } + + + public long setAndGetCurrentSize(long value) { + currentSize.set(value); + return value; + } + + @Override + public long getKeepAliveTime() { + return keepAliveTime; + } + + @Override + public int getMaxSpareThreads() { + return maxSpareThreads; + } + + @Override + public int getMaxThreads() { + return maxThreads; + } + + + public void setKeepAliveTime(long keepAliveTime) { + this.keepAliveTime = keepAliveTime; + } + + + public void setMaxSpareThreads(int maxSpareThreads) { + this.maxSpareThreads = maxSpareThreads; + } + + + public void setMaxThreads(int maxThreads) { + this.maxThreads = maxThreads; + } + + @Override + public boolean isAlwaysSend() { + return alwaysSend; + } + + + @Override + public void setAlwaysSend(boolean alwaysSend) { + this.alwaysSend = alwaysSend; + } + + + @Override + public void start(int svc) throws ChannelException { + //start the thread + if (!run ) { + synchronized (this) { + // only start with the sender + if ( !run && ((svc & Channel.SND_TX_SEQ)==Channel.SND_TX_SEQ) ) { + startQueue(); + } + } + } + super.start(svc); + } + + + @Override + public void stop(int svc) throws ChannelException { + //stop the thread + if (run) { + synchronized (this) { + if ( run && ((svc & Channel.SND_TX_SEQ)==Channel.SND_TX_SEQ)) { + stopQueue(); + } + } + } + + super.stop(svc); + } + + + protected void sendAsyncData(ChannelMessage msg, Member[] destination, + InterceptorPayload payload) { + ErrorHandler handler = null; + if (payload != null) { + handler = payload.getErrorHandler(); + } + try { + super.sendMessage(destination, msg, null); + try { + if (handler != null) { + handler.handleCompletion(new UniqueId(msg.getUniqueId())); + } + } catch ( Exception ex ) { + log.error(sm.getString("messageDispatchInterceptor.completeMessage.failed"),ex); + } + } catch ( Exception x ) { + ChannelException cx = null; + if (x instanceof ChannelException) { + cx = (ChannelException) x; + } else { + cx = new ChannelException(x); + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("messageDispatchInterceptor.AsyncMessage.failed"),x); + } + try { + if (handler != null) { + handler.handleError(cx, new UniqueId(msg.getUniqueId())); + } + } catch ( Exception ex ) { + log.error(sm.getString("messageDispatchInterceptor.errorMessage.failed"),ex); + } + } finally { + addAndGetCurrentSize(-msg.getMessage().getLength()); + } + } + + // ---------------------------------------------- stats of the thread pool + /** + * Return the current number of threads that are managed by the pool. + * @return the current number of threads that are managed by the pool + */ + @Override + public int getPoolSize() { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getPoolSize(); + } else { + return -1; + } + } + + /** + * Return the current number of threads that are in use. + * @return the current number of threads that are in use + */ + @Override + public int getActiveCount() { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getActiveCount(); + } else { + return -1; + } + } + + /** + * Return the total number of tasks that have ever been scheduled for execution by the pool. + * @return the total number of tasks that have ever been scheduled for execution by the pool + */ + @Override + public long getTaskCount() { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getTaskCount(); + } else { + return -1; + } + } + + /** + * Return the total number of tasks that have completed execution by the pool. + * @return the total number of tasks that have completed execution by the pool + */ + @Override + public long getCompletedTaskCount() { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getCompletedTaskCount(); + } else { + return -1; + } + } + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptorMBean.java new file mode 100644 index 0000000..be539b6 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptorMBean.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.tribes.group.interceptors; + +public interface MessageDispatchInterceptorMBean { + + int getOptionFlag(); + + boolean isAlwaysSend(); + + void setAlwaysSend(boolean alwaysSend); + + long getMaxQueueSize(); + + long getCurrentSize(); + + long getKeepAliveTime(); + + int getMaxSpareThreads(); + + int getMaxThreads(); + + // pool stats + int getPoolSize(); + + int getActiveCount(); + + long getTaskCount(); + + long getCompletedTaskCount(); + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java b/java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java new file mode 100644 index 0000000..dcaa626 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java @@ -0,0 +1,891 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.group.AbsoluteOrder; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.membership.Membership; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.catalina.tribes.util.UUIDGenerator; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + *

    Title: Auto merging leader election algorithm

    + * + *

    Description: Implementation of a simple coordinator algorithm that not only selects a coordinator, + * it also merges groups automatically when members are discovered that weren't part of the + *

    + *

    This algorithm is non blocking meaning it allows for transactions while the coordination phase is going on + *

    + *

    This implementation is based on a home brewed algorithm that uses the AbsoluteOrder of a membership + * to pass a token ring of the current membership.
    + * This is not the same as just using AbsoluteOrder! Consider the following scenario:
    + * Nodes, A,B,C,D,E on a network, in that priority. AbsoluteOrder will only work if all + * nodes are receiving pings from all the other nodes. + * meaning, that node{i} receives pings from node{all}-node{i}
    + * but the following could happen if a multicast problem occurs. + * A has members {B,C,D}
    + * B has members {A,C}
    + * C has members {D,E}
    + * D has members {A,B,C,E}
    + * E has members {A,C,D}
    + * Because the default Tribes membership implementation, relies on the multicast packets to + * arrive at all nodes correctly, there is nothing guaranteeing that it will.
    + *
    + * To best explain how this algorithm works, lets take the above example: + * For simplicity we assume that a send operation is O(1) for all nodes, although this algorithm will work + * where messages overlap, as they all depend on absolute order
    + * Scenario 1: A,B,C,D,E all come online at the same time + * Eval phase, A thinks of itself as leader, B thinks of A as leader, + * C thinks of itself as leader, D,E think of A as leader
    + * Token phase:
    + * (1) A sends out a message X{A-ldr, A-src, mbrs-A,B,C,D} to B where X is the id for the message(and the view)
    + * (1) C sends out a message Y{C-ldr, C-src, mbrs-C,D,E} to D where Y is the id for the message(and the view)
    + * (2) B receives X{A-ldr, A-src, mbrs-A,B,C,D}, sends X{A-ldr, A-src, mbrs-A,B,C,D} to C
    + * (2) D receives Y{C-ldr, C-src, mbrs-C,D,E} D is aware of A,B, sends Y{A-ldr, C-src, mbrs-A,B,C,D,E} to E
    + * (3) C receives X{A-ldr, A-src, mbrs-A,B,C,D}, sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to D
    + * (3) E receives Y{A-ldr, C-src, mbrs-A,B,C,D,E} sends Y{A-ldr, C-src, mbrs-A,B,C,D,E} to A
    + * (4) D receives X{A-ldr, A-src, mbrs-A,B,C,D,E} sends sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to A
    + * (4) A receives Y{A-ldr, C-src, mbrs-A,B,C,D,E}, holds the message, add E to its list of members
    + * (5) A receives X{A-ldr, A-src, mbrs-A,B,C,D,E}
    + * At this point, the state looks like
    + * A - {A-ldr, mbrs-A,B,C,D,E, id=X}
    + * B - {A-ldr, mbrs-A,B,C,D, id=X}
    + * C - {A-ldr, mbrs-A,B,C,D,E, id=X}
    + * D - {A-ldr, mbrs-A,B,C,D,E, id=X}
    + * E - {A-ldr, mbrs-A,B,C,D,E, id=Y}
    + *
    + * A message doesn't stop until it reaches its original sender, unless its dropped by a higher leader. + * As you can see, E still thinks the viewId=Y, which is not correct. But at this point we have + * arrived at the same membership and all nodes are informed of each other.
    + * To synchronize the rest we simply perform the following check at A when A receives X:
    + * Original X{A-ldr, A-src, mbrs-A,B,C,D} == Arrived X{A-ldr, A-src, mbrs-A,B,C,D,E}
    + * Since the condition is false, A, will resend the token, and A sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to B + * When A receives X again, the token is complete.
    + * Optionally, A can send a message X{A-ldr, A-src, mbrs-A,B,C,D,E confirmed} to A,B,C,D,E who then + * install and accept the view. + *

    + *

    + * Lets assume that C1 arrives, C1 has lower priority than C, but higher priority than D.
    + * Lets also assume that C1 sees the following view {B,D,E}
    + * C1 waits for a token to arrive. When the token arrives, the same scenario as above will happen.
    + * In the scenario where C1 sees {D,E} and A,B,C cannot see C1, no token will ever arrive.
    + * In this case, C1 sends a Z{C1-ldr, C1-src, mbrs-C1,D,E} to D
    + * D receives Z{C1-ldr, C1-src, mbrs-C1,D,E} and sends Z{A-ldr, C1-src, mbrs-A,B,C,C1,D,E} to E
    + * E receives Z{A-ldr, C1-src, mbrs-A,B,C,C1,D,E} and sends it to A
    + * A sends Z{A-ldr, A-src, mbrs-A,B,C,C1,D,E} to B and the chain continues until A receives the token again. + * At that time A optionally sends out Z{A-ldr, A-src, mbrs-A,B,C,C1,D,E, confirmed} to A,B,C,C1,D,E + *

    + *

    To ensure that the view gets implemented at all nodes at the same time, + * A will send out a VIEW_CONF message, this is the 'confirmed' message that is optional above. + *

    Ideally, the interceptor below this one would be the TcpFailureDetector to ensure correct memberships

    + * + *

    The example above, of course can be simplified with a finite statemachine:
    + * But I suck at writing state machines, my head gets all confused. One day I will document this algorithm though.
    + * Maybe I'll do a state diagram :) + *

    + *

    State Diagrams

    + * Initiate an election

    + * Receive an election message

    + */ +public class NonBlockingCoordinator extends ChannelInterceptorBase { + + private static final Log log = LogFactory.getLog(NonBlockingCoordinator.class); + protected static final StringManager sm = StringManager.getManager(NonBlockingCoordinator.class); + + /** + * header for a coordination message + */ + protected static final byte[] COORD_HEADER = new byte[] {-86, 38, -34, -29, -98, 90, 65, 63, -81, -122, -6, -110, 99, -54, 13, 63}; + /** + * Coordination request + */ + protected static final byte[] COORD_REQUEST = new byte[] {104, -95, -92, -42, 114, -36, 71, -19, -79, 20, 122, 101, -1, -48, -49, 30}; + /** + * Coordination confirmation, for blocking installations + */ + protected static final byte[] COORD_CONF = new byte[] {67, 88, 107, -86, 69, 23, 76, -70, -91, -23, -87, -25, -125, 86, 75, 20}; + + /** + * Alive message + */ + protected static final byte[] COORD_ALIVE = new byte[] {79, -121, -25, -15, -59, 5, 64, 94, -77, 113, -119, -88, 52, 114, -56, -46, + -18, 102, 10, 34, -127, -9, 71, 115, -70, 72, -101, 88, 72, -124, 127, 111, + 74, 76, -116, 50, 111, 103, 65, 3, -77, 51, -35, 0, 119, 117, 9, -26, + 119, 50, -75, -105, -102, 36, 79, 37, -68, -84, -123, 15, -22, -109, 106, -55}; + /** + * Time to wait for coordination timeout + */ + protected final long waitForCoordMsgTimeout = 15000; + /** + * Our current view + */ + protected volatile Membership view = null; + /** + * Out current viewId + */ + protected UniqueId viewId; + + /** + * Our nonblocking membership + */ + protected Membership membership = null; + + /** + * indicates that we are running an election + * and this is the one we are running + */ + protected UniqueId suggestedviewId; + protected volatile Membership suggestedView; + + protected volatile boolean started = false; + protected final int startsvc = 0xFFFF; + + protected final Object electionMutex = new Object(); + + protected final AtomicBoolean coordMsgReceived = new AtomicBoolean(false); + + public NonBlockingCoordinator() { + super(); + } + +//============================================================================================================ +// COORDINATION HANDLING +//============================================================================================================ + + public void startElection(boolean force) throws ChannelException { + synchronized (electionMutex) { + Member local = getLocalMember(false); + Member[] others = membership.getMembers(); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START_ELECT,this,"Election initiated")); + if ( others.length == 0 ) { + this.viewId = new UniqueId(UUIDGenerator.randomUUID(false)); + this.view = new Membership(local,AbsoluteOrder.comp, true); + this.handleViewConf(createElectionMsg(local,others,local), view); + return; //the only member, no need for an election + } + if ( suggestedviewId != null ) { + + if ( view != null && Arrays.diff(view,suggestedView,local).length == 0 && Arrays.diff(suggestedView,view,local).length == 0) { + suggestedviewId = null; + suggestedView = null; + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, running election matches view")); + } else { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, election running")); + } + return; //election already running, I'm not allowed to have two of them + } + if ( view != null && Arrays.diff(view,membership,local).length == 0 && Arrays.diff(membership,view,local).length == 0) { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, view matches membership")); + return; //already have this view installed + } + int prio = AbsoluteOrder.comp.compare(local,others[0]); + Member leader = ( prio < 0 )?local:others[0];//am I the leader in my view? + if ( local.equals(leader) || force ) { + CoordinationMessage msg = createElectionMsg(local, others, leader); + suggestedviewId = msg.getId(); + suggestedView = new Membership(local,AbsoluteOrder.comp,true); + Arrays.fill(suggestedView,msg.getMembers()); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_PROCESS_ELECT,this,"Election, sending request")); + sendElectionMsg(local,others[0],msg); + } else { + try { + coordMsgReceived.set(false); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_WAIT_FOR_MSG,this,"Election, waiting for request")); + electionMutex.wait(waitForCoordMsgTimeout); + } catch (InterruptedException x) { + Thread.currentThread().interrupt(); + } + String msg; + if (suggestedviewId == null && !coordMsgReceived.get()) { + if (Thread.interrupted()) { + msg = "Election abandoned, waiting interrupted."; + } else { + msg = "Election abandoned, waiting timed out."; + } + } else { + msg = "Election abandoned, received a message"; + } + fireInterceptorEvent(new CoordinationEvent( + CoordinationEvent.EVT_ELECT_ABANDONED, this, msg)); + } + } + } + + private CoordinationMessage createElectionMsg(Member local, Member[] others, Member leader) { + Membership m = new Membership(local,AbsoluteOrder.comp,true); + Arrays.fill(m,others); + Member[] mbrs = m.getMembers(); + m.reset(); + CoordinationMessage msg = new CoordinationMessage(leader, local, mbrs,new UniqueId(UUIDGenerator.randomUUID(true)), COORD_REQUEST); + return msg; + } + + protected void sendElectionMsg(Member local, Member next, CoordinationMessage msg) throws ChannelException { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_SEND_MSG,this,"Sending election message to("+next.getName()+")")); + super.sendMessage(new Member[] {next}, createData(msg, local), null); + } + + protected void sendElectionMsgToNextInline(Member local, CoordinationMessage msg) throws ChannelException { + int next = Arrays.nextIndex(local,msg.getMembers()); + int current = next; + msg.leader = msg.getMembers()[0]; + boolean sent = false; + while ( !sent && current >= 0 ) { + try { + sendElectionMsg(local, msg.getMembers()[current], msg); + sent = true; + }catch ( ChannelException x ) { + log.warn(sm.getString("nonBlockingCoordinator.electionMessage.sendfailed", msg.getMembers()[current])); + current = Arrays.nextIndex(msg.getMembers()[current],msg.getMembers()); + if ( current == next ) { + throw x; + } + } + } + } + + public ChannelData createData(CoordinationMessage msg, Member local) { + msg.write(); + ChannelData data = new ChannelData(true); + data.setAddress(local); + data.setMessage(msg.getBuffer()); + data.setOptions(Channel.SEND_OPTIONS_USE_ACK); + data.setTimestamp(System.currentTimeMillis()); + return data; + } + + protected boolean alive(Member mbr) { + return memberAlive(mbr, waitForCoordMsgTimeout); + } + + protected boolean memberAlive(Member mbr, long conTimeout) { + //could be a shutdown notification + if ( Arrays.equals(mbr.getCommand(),Member.SHUTDOWN_PAYLOAD) ) { + return false; + } + + try (Socket socket = new Socket()) { + InetAddress ia = InetAddress.getByAddress(mbr.getHost()); + InetSocketAddress addr = new InetSocketAddress(ia, mbr.getPort()); + socket.connect(addr, (int) conTimeout); + return true; + } catch (SocketTimeoutException | ConnectException x) { + //do nothing, we couldn't connect + } catch (Exception x) { + log.error(sm.getString("nonBlockingCoordinator.memberAlive.failed"),x); + } + return false; + } + + protected Membership mergeOnArrive(CoordinationMessage msg) { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_PRE_MERGE,this,"Pre merge")); + Member local = getLocalMember(false); + Membership merged = new Membership(local,AbsoluteOrder.comp,true); + Arrays.fill(merged,msg.getMembers()); + Arrays.fill(merged,getMembers()); + Member[] diff = Arrays.diff(merged,membership,local); + for (Member member : diff) { + if (!alive(member)) { + merged.removeMember(member); + } else { + memberAdded(member, false); + } + } + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_POST_MERGE,this,"Post merge")); + return merged; + } + + protected void processCoordMessage(CoordinationMessage msg) throws ChannelException { + if ( !coordMsgReceived.get() ) { + coordMsgReceived.set(true); + synchronized (electionMutex) { electionMutex.notifyAll();} + } + Membership merged = mergeOnArrive(msg); + if (isViewConf(msg)) { + handleViewConf(msg, merged); + } else { + handleToken(msg, merged); + } + } + + protected void handleToken(CoordinationMessage msg, Membership merged) throws ChannelException { + Member local = getLocalMember(false); + if ( local.equals(msg.getSource()) ) { + //my message msg.src=local + handleMyToken(local, msg, merged); + } else { + handleOtherToken(local, msg, merged); + } + } + + protected void handleMyToken(Member local, CoordinationMessage msg, Membership merged) throws ChannelException { + if ( local.equals(msg.getLeader()) ) { + //no leadership change + if ( Arrays.sameMembers(msg.getMembers(),merged.getMembers()) ) { + msg.type = COORD_CONF; + super.sendMessage(Arrays.remove(msg.getMembers(),local),createData(msg,local),null); + handleViewConf(msg, merged); + } else { + //membership change + suggestedView = new Membership(local,AbsoluteOrder.comp,true); + suggestedviewId = msg.getId(); + Arrays.fill(suggestedView,merged.getMembers()); + msg.view = merged.getMembers(); + sendElectionMsgToNextInline(local,msg); + } + } else { + //leadership change + suggestedView = null; + suggestedviewId = null; + msg.view = merged.getMembers(); + sendElectionMsgToNextInline(local,msg); + } + } + + protected void handleOtherToken(Member local, CoordinationMessage msg, Membership merged) throws ChannelException { + if ( local.equals(msg.getLeader()) ) { + //I am the new leader + //startElection(false); + } else { + msg.view = merged.getMembers(); + sendElectionMsgToNextInline(local,msg); + } + } + + protected void handleViewConf(CoordinationMessage msg, Membership merged) throws ChannelException { + if ( viewId != null && msg.getId().equals(viewId) ) + { + return;//we already have this view + } + view = new Membership(getLocalMember(false),AbsoluteOrder.comp,true); + Arrays.fill(view,msg.getMembers()); + viewId = msg.getId(); + + if ( viewId.equals(suggestedviewId) ) { + suggestedView = null; + suggestedviewId = null; + } + + if (suggestedView != null && AbsoluteOrder.comp.compare(suggestedView.getMembers()[0],merged.getMembers()[0])<0 ) { + suggestedView = null; + suggestedviewId = null; + } + + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_CONF_RX,this,"Accepted View")); + + if ( suggestedviewId == null && hasHigherPriority(merged.getMembers(),membership.getMembers()) ) { + startElection(false); + } + } + + protected boolean isViewConf(CoordinationMessage msg) { + return Arrays.contains(msg.getType(),0,COORD_CONF,0,COORD_CONF.length); + } + + protected boolean hasHigherPriority(Member[] complete, Member[] local) { + if ( local == null || local.length == 0 ) { + return false; + } + if ( complete == null || complete.length == 0 ) { + return true; + } + AbsoluteOrder.absoluteOrder(complete); + AbsoluteOrder.absoluteOrder(local); + return (AbsoluteOrder.comp.compare(complete[0],local[0]) > 0); + + } + + + /** + * Returns coordinator if one is available + * @return Member + */ + public Member getCoordinator() { + return (view != null && view.hasMembers()) ? view.getMembers()[0] : null; + } + + public Member[] getView() { + return (view != null && view.hasMembers()) ? view.getMembers() : new Member[0]; + } + + public UniqueId getViewId() { + return viewId; + } + + /** + * Block in/out messages while a election is going on + */ + protected void halt() { + + } + + /** + * Release lock for in/out messages election is completed + */ + protected void release() { + + } + + /** + * Wait for an election to end + */ + protected void waitForRelease() { + + } + + +//============================================================================================================ +// OVERRIDDEN METHODS FROM CHANNEL INTERCEPTOR BASE +//============================================================================================================ + @Override + public void start(int svc) throws ChannelException { + if (membership == null) { + setupMembership(); + } + if (started) { + return; + } + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START, this, "Before start")); + super.start(startsvc); + started = true; + if (view == null) { + view = new Membership(super.getLocalMember(true), AbsoluteOrder.comp, true); + } + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START, this, "After start")); + startElection(false); + } + + @Override + public void stop(int svc) throws ChannelException { + try { + halt(); + synchronized (electionMutex) { + if (!started) { + return; + } + started = false; + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_STOP, this, "Before stop")); + super.stop(startsvc); + this.view = null; + this.viewId = null; + this.suggestedView = null; + this.suggestedviewId = null; + this.membership.reset(); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_STOP, this, "After stop")); + } + }finally { + release(); + } + } + + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + waitForRelease(); + super.sendMessage(destination, msg, payload); + } + + @Override + public void messageReceived(ChannelMessage msg) { + if ( Arrays.contains(msg.getMessage().getBytesDirect(),0,COORD_ALIVE,0,COORD_ALIVE.length) ) { + //ignore message, its an alive message + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MSG_ARRIVE,this,"Alive Message")); + + } else if ( Arrays.contains(msg.getMessage().getBytesDirect(),0,COORD_HEADER,0,COORD_HEADER.length) ) { + try { + CoordinationMessage cmsg = new CoordinationMessage(msg.getMessage()); + Member[] cmbr = cmsg.getMembers(); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MSG_ARRIVE,this,"Coord Msg Arrived("+Arrays.toNameString(cmbr)+")")); + processCoordMessage(cmsg); + }catch ( ChannelException x ) { + log.error(sm.getString("nonBlockingCoordinator.processCoordinationMessage.failed"),x); + } + } else { + super.messageReceived(msg); + } + } + + @Override + public void memberAdded(Member member) { + memberAdded(member,true); + } + + public void memberAdded(Member member,boolean elect) { + if (membership == null) { + setupMembership(); + } + if (membership.memberAlive(member)) { + super.memberAdded(member); + } + try { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MBR_ADD,this,"Member add("+member.getName()+")")); + if (started && elect) { + startElection(false); + } + } catch (ChannelException x) { + log.error(sm.getString("nonBlockingCoordinator.memberAdded.failed"),x); + } + } + + @Override + public void memberDisappeared(Member member) { + membership.removeMember(member); + super.memberDisappeared(member); + try { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MBR_DEL,this,"Member remove("+member.getName()+")")); + if (started && (isCoordinator() || isHighest())) + { + startElection(true); //to do, if a member disappears, only the coordinator can start + } + } catch (ChannelException x) { + log.error(sm.getString("nonBlockingCoordinator.memberDisappeared.failed"),x); + } + } + + public boolean isHighest() { + Member local = getLocalMember(false); + if ( membership.getMembers().length == 0 ) { + return true; + } else { + return AbsoluteOrder.comp.compare(local,membership.getMembers()[0])<=0; + } + } + + public boolean isCoordinator() { + Member coord = getCoordinator(); + return coord != null && getLocalMember(false).equals(coord); + } + + @Override + public void heartbeat() { + try { + Member local = getLocalMember(false); + if ( view != null && (Arrays.diff(view,membership,local).length != 0 || Arrays.diff(membership,view,local).length != 0) ) { + if ( isHighest() ) { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START_ELECT, this, + sm.getString("nonBlockingCoordinator.heartbeat.inconsistency"))); + startElection(true); + } + } + } catch ( Exception x ){ + log.error(sm.getString("nonBlockingCoordinator.heartbeat.failed"),x); + } finally { + super.heartbeat(); + } + } + + /** + * has members + */ + @Override + public boolean hasMembers() { + + return membership.hasMembers(); + } + + /** + * Get all current cluster members + * @return all members or empty array + */ + @Override + public Member[] getMembers() { + + return membership.getMembers(); + } + + /** + * @param mbr Member + * @return Member + */ + @Override + public Member getMember(Member mbr) { + + return membership.getMember(mbr); + } + + /** + * Return the member that represents this node. + * + * @return Member + */ + @Override + public Member getLocalMember(boolean incAlive) { + Member local = super.getLocalMember(incAlive); + if ( view == null && (local != null)) { + setupMembership(); + } + return local; + } + + protected synchronized void setupMembership() { + if ( membership == null ) { + membership = new Membership(super.getLocalMember(true),AbsoluteOrder.comp,false); + } + } + + +//============================================================================================================ +// HELPER CLASSES FOR COORDINATION +//============================================================================================================ + + + public static class CoordinationMessage { + //X{A-ldr, A-src, mbrs-A,B,C,D} + protected final XByteBuffer buf; + protected Member leader; + protected Member source; + protected Member[] view; + protected UniqueId id; + protected byte[] type; + + public CoordinationMessage(XByteBuffer buf) { + this.buf = buf; + parse(); + } + + public CoordinationMessage(Member leader, + Member source, + Member[] view, + UniqueId id, + byte[] type) { + this.buf = new XByteBuffer(4096,false); + this.leader = leader; + this.source = source; + this.view = view; + this.id = id; + this.type = type; + this.write(); + } + + + public byte[] getHeader() { + return COORD_HEADER; + } + + public Member getLeader() { + if ( leader == null ) { + parse(); + } + return leader; + } + + public Member getSource() { + if ( source == null ) { + parse(); + } + return source; + } + + public UniqueId getId() { + if ( id == null ) { + parse(); + } + return id; + } + + public Member[] getMembers() { + if ( view == null ) { + parse(); + } + return view; + } + + public byte[] getType() { + if (type == null ) { + parse(); + } + return type; + } + + public XByteBuffer getBuffer() { + return this.buf; + } + + public void parse() { + //header + int offset = 16; + //leader + int ldrLen = XByteBuffer.toInt(buf.getBytesDirect(),offset); + offset += 4; + byte[] ldr = new byte[ldrLen]; + System.arraycopy(buf.getBytesDirect(),offset,ldr,0,ldrLen); + leader = MemberImpl.getMember(ldr); + offset += ldrLen; + //source + int srcLen = XByteBuffer.toInt(buf.getBytesDirect(),offset); + offset += 4; + byte[] src = new byte[srcLen]; + System.arraycopy(buf.getBytesDirect(),offset,src,0,srcLen); + source = MemberImpl.getMember(src); + offset += srcLen; + //view + int mbrCount = XByteBuffer.toInt(buf.getBytesDirect(),offset); + offset += 4; + view = new Member[mbrCount]; + for (int i=0; i + * There is no point in + * using this with the replicationMode="fastasynchqueue" as this mode guarantees ordering.
    + * If you are using the mode ack=false replicationMode=pooled, and have a lot of concurrent threads, + * this interceptor can really slow you down, as many messages will be completely out of order + * and the queue might become rather large. If this is the case, then you might want to set + * the value OrderInterceptor.maxQueue = 25 (meaning that we will never keep more than 25 messages in our queue) + *
    Configuration Options
    + * OrderInterceptor.expire=<milliseconds> - if a message arrives out of order, how long before we act on it default=3000ms
    + * OrderInterceptor.maxQueue=<max queue size> - how much can the queue grow to ensure ordering. + * This setting is useful to avoid OutOfMemoryErrorsdefault=Integer.MAX_VALUE
    + * OrderInterceptor.forwardExpired=<boolean> - this flag tells the interceptor what to + * do when a message has expired or the queue has grown larger than the maxQueue value. + * true means that the message is sent up the stack to the receiver that will receive and out of order message + * false means, forget the message and reset the message counter. default=true + */ +public class OrderInterceptor extends ChannelInterceptorBase { + protected static final StringManager sm = StringManager.getManager(OrderInterceptor.class); + private final Map outcounter = new HashMap<>(); + private final Map incounter = new HashMap<>(); + private final Map incoming = new HashMap<>(); + private long expire = 3000; + private boolean forwardExpired = true; + private int maxQueue = Integer.MAX_VALUE; + + final ReentrantReadWriteLock inLock = new ReentrantReadWriteLock(true); + final ReentrantReadWriteLock outLock= new ReentrantReadWriteLock(true); + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + if ( !okToProcess(msg.getOptions()) ) { + super.sendMessage(destination, msg, payload); + return; + } + ChannelException cx = null; + for (Member member : destination) { + try { + int nr = 0; + outLock.writeLock().lock(); + try { + nr = incCounter(member); + } finally { + outLock.writeLock().unlock(); + } + //reduce byte copy + msg.getMessage().append(nr); + try { + getNext().sendMessage(new Member[]{member}, msg, payload); + } finally { + msg.getMessage().trim(4); + } + } catch (ChannelException x) { + if (cx == null) { + cx = x; + } + cx.addFaultyMember(x.getFaultyMembers()); + } + }//for + if ( cx != null ) { + throw cx; + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + if ( !okToProcess(msg.getOptions()) ) { + super.messageReceived(msg); + return; + } + int msgnr = XByteBuffer.toInt(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-4); + msg.getMessage().trim(4); + MessageOrder order = new MessageOrder(msgnr,(ChannelMessage)msg.deepclone()); + inLock.writeLock().lock(); + try { + if ( processIncoming(order) ) { + processLeftOvers(msg.getAddress(),false); + } + } finally { + inLock.writeLock().unlock(); + } + } + protected void processLeftOvers(Member member, boolean force) { + MessageOrder tmp = incoming.get(member); + if ( force ) { + Counter cnt = getInCounter(member); + cnt.setCounter(Integer.MAX_VALUE); + } + if ( tmp!= null ) { + processIncoming(tmp); + } + } + /** + * @param order MessageOrder + * @return boolean - true if a message expired and was processed + */ + protected boolean processIncoming(MessageOrder order) { + boolean result = false; + Member member = order.getMessage().getAddress(); + Counter cnt = getInCounter(member); + + MessageOrder tmp = incoming.get(member); + if ( tmp != null ) { + order = MessageOrder.add(tmp,order); + } + + + while ( (order!=null) && (order.getMsgNr() <= cnt.getCounter()) ) { + //we are right on target. process orders + if ( order.getMsgNr() == cnt.getCounter() ) { + cnt.inc(); + } else if ( order.getMsgNr() > cnt.getCounter() ) { + cnt.setCounter(order.getMsgNr()); + } + super.messageReceived(order.getMessage()); + order.setMessage(null); + order = order.next; + } + MessageOrder head = order; + MessageOrder prev = null; + tmp = order; + //flag to empty out the queue when it larger than maxQueue + boolean empty = order!=null?order.getCount()>=maxQueue:false; + while ( tmp != null ) { + //process expired messages or empty out the queue + if ( tmp.isExpired(expire) || empty ) { + //reset the head + if ( tmp == head ) { + head = tmp.next; + } + cnt.setCounter(tmp.getMsgNr()+1); + if ( getForwardExpired() ) { + super.messageReceived(tmp.getMessage()); + } + tmp.setMessage(null); + tmp = tmp.next; + if ( prev != null ) { + prev.next = tmp; + } + result = true; + } else { + prev = tmp; + tmp = tmp.next; + } + } + if ( head == null ) { + incoming.remove(member); + } else { + incoming.put(member, head); + } + return result; + } + + @Override + public void memberAdded(Member member) { + //notify upwards + super.memberAdded(member); + } + + @Override + public void memberDisappeared(Member member) { + //reset counters - lock free + incounter.remove(member); + outcounter.remove(member); + //clear the remaining queue + processLeftOvers(member,true); + //notify upwards + super.memberDisappeared(member); + } + + protected int incCounter(Member mbr) { + Counter cnt = getOutCounter(mbr); + return cnt.inc(); + } + + protected Counter getInCounter(Member mbr) { + Counter cnt = incounter.get(mbr); + if ( cnt == null ) { + cnt = new Counter(); + cnt.inc(); //always start at 1 for incoming + incounter.put(mbr,cnt); + } + return cnt; + } + + protected Counter getOutCounter(Member mbr) { + Counter cnt = outcounter.get(mbr); + if ( cnt == null ) { + cnt = new Counter(); + outcounter.put(mbr,cnt); + } + return cnt; + } + + protected static class Counter { + private final AtomicInteger value = new AtomicInteger(0); + + public int getCounter() { + return value.get(); + } + + public void setCounter(int counter) { + this.value.set(counter); + } + + public int inc() { + return value.addAndGet(1); + } + } + + protected static class MessageOrder { + private final long received = System.currentTimeMillis(); + private MessageOrder next; + private final int msgNr; + private ChannelMessage msg = null; + public MessageOrder(int msgNr,ChannelMessage msg) { + this.msgNr = msgNr; + this.msg = msg; + } + + public boolean isExpired(long expireTime) { + return (System.currentTimeMillis()-received) > expireTime; + } + + public ChannelMessage getMessage() { + return msg; + } + + public void setMessage(ChannelMessage msg) { + this.msg = msg; + } + + public void setNext(MessageOrder order) { + this.next = order; + } + public MessageOrder getNext() { + return next; + } + + public int getCount() { + int counter = 1; + MessageOrder tmp = next; + while ( tmp != null ) { + counter++; + tmp = tmp.next; + } + return counter; + } + + @SuppressWarnings("null") // prev cannot be null + public static MessageOrder add(MessageOrder head, MessageOrder add) { + if ( head == null ) { + return add; + } + if ( add == null ) { + return head; + } + if ( head == add ) { + return add; + } + + if ( head.getMsgNr() > add.getMsgNr() ) { + add.next = head; + return add; + } + + MessageOrder iter = head; + MessageOrder prev = null; + while ( iter.getMsgNr() < add.getMsgNr() && (iter.next !=null ) ) { + prev = iter; + iter = iter.next; + } + if ( iter.getMsgNr() < add.getMsgNr() ) { + //add after + add.next = iter.next; + iter.next = add; + } else if (iter.getMsgNr() > add.getMsgNr()) { + //add before + prev.next = add; // prev cannot be null here, warning suppressed + add.next = iter; + + } else { + throw new ArithmeticException(sm.getString("orderInterceptor.messageAdded.sameCounter")); + } + + return head; + } + + public int getMsgNr() { + return msgNr; + } + + + } + + public void setExpire(long expire) { + this.expire = expire; + } + + public void setForwardExpired(boolean forwardExpired) { + this.forwardExpired = forwardExpired; + } + + public void setMaxQueue(int maxQueue) { + this.maxQueue = maxQueue; + } + + public long getExpire() { + return expire; + } + + public boolean getForwardExpired() { + return forwardExpired; + } + + public int getMaxQueue() { + return maxQueue; + } + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/SimpleCoordinator.java b/java/org/apache/catalina/tribes/group/interceptors/SimpleCoordinator.java new file mode 100644 index 0000000..ebdd6ae --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/SimpleCoordinator.java @@ -0,0 +1,118 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.AbsoluteOrder; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; + +/** + * A dinky coordinator, just uses a sorted version of the member array. + * + * @author rnewson + * + */ +public class SimpleCoordinator extends ChannelInterceptorBase { + + private Member[] view; + + private final AtomicBoolean membershipChanged = new AtomicBoolean(); + + private void membershipChanged() { + membershipChanged.set(true); + } + + @Override + public void memberAdded(final Member member) { + super.memberAdded(member); + membershipChanged(); + installViewWhenStable(); + } + + @Override + public void memberDisappeared(final Member member) { + super.memberDisappeared(member); + membershipChanged(); + installViewWhenStable(); + } + + /** + * Override to receive view changes. + * + * @param view The members array + */ + protected void viewChange(final Member[] view) { + } + + @Override + public void start(int svc) throws ChannelException { + super.start(svc); + installViewWhenStable(); + } + + private void installViewWhenStable() { + int stableCount = 0; + + while (stableCount < 10) { + if (membershipChanged.compareAndSet(true, false)) { + stableCount = 0; + } else { + stableCount++; + } + try { + TimeUnit.MILLISECONDS.sleep(250); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + final Member[] members = getMembers(); + final Member[] view = new Member[members.length+1]; + System.arraycopy(members, 0, view, 0, members.length); + view[members.length] = getLocalMember(false); + Arrays.sort(view, AbsoluteOrder.comp); + if (Arrays.equals(view, this.view)) { + return; + } + this.view = view; + viewChange(view); + } + + @Override + public void stop(int svc) throws ChannelException { + super.stop(svc); + } + + public Member[] getView() { + return view; + } + + public Member getCoordinator() { + return view == null ? null : view[0]; + } + + public boolean isCoordinator() { + return view == null ? false : getLocalMember(false).equals( + getCoordinator()); + } + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java new file mode 100644 index 0000000..be1a68f --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java @@ -0,0 +1,261 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.ScheduledExecutorService; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.AbsoluteOrder; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class StaticMembershipInterceptor extends ChannelInterceptorBase + implements StaticMembershipInterceptorMBean { + + private static final Log log = LogFactory.getLog(StaticMembershipInterceptor.class); + protected static final StringManager sm = + StringManager.getManager(StaticMembershipInterceptor.class); + + protected static final byte[] MEMBER_START = new byte[] { + 76, 111, 99, 97, 108, 32, 83, 116, 97, 116, 105, 99, 77, 101, 109, 98, 101, 114, 32, 78, + 111, 116, 105, 102, 105, 99, 97, 116, 105, 111, 110, 32, 68, 97, 116, 97}; + + protected static final byte[] MEMBER_STOP = new byte[] { + 76, 111, 99, 97, 108, 32, 83, 116, 97, 116, 105, 99, 77, 101, 109, 98, 101, 114, 32, 83, + 104, 117, 116, 100, 111, 119, 110, 32, 68, 97, 116, 97}; + + protected final ArrayList members = new ArrayList<>(); + protected Member localMember = null; + + public StaticMembershipInterceptor() { + super(); + } + + public void addStaticMember(Member member) { + synchronized (members) { + if (!members.contains(member)) { + members.add(member); + } + } + } + + public void removeStaticMember(Member member) { + synchronized (members) { + if (members.contains(member)) { + members.remove(member); + } + } + } + + public void setLocalMember(Member member) { + this.localMember = member; + localMember.setLocal(true); + } + + @Override + public void messageReceived(ChannelMessage msg) { + if (msg.getMessage().getLength() == MEMBER_START.length && + Arrays.equals(MEMBER_START, msg.getMessage().getBytes())) { + // receive member start + Member member = getMember(msg.getAddress()); + if (member != null) { + super.memberAdded(member); + } + } else if (msg.getMessage().getLength() == MEMBER_STOP.length && + Arrays.equals(MEMBER_STOP, msg.getMessage().getBytes())) { + // receive member shutdown + Member member = getMember(msg.getAddress()); + if (member != null) { + try { + member.setCommand(Member.SHUTDOWN_PAYLOAD); + super.memberDisappeared(member); + } finally { + member.setCommand(new byte[0]); + } + } + } else { + super.messageReceived(msg); + } + } + + /** + * has members + */ + @Override + public boolean hasMembers() { + return super.hasMembers() || (members.size()>0); + } + + /** + * Get all current cluster members + * @return all members or empty array + */ + @Override + public Member[] getMembers() { + if ( members.size() == 0 ) { + return super.getMembers(); + } else { + synchronized (members) { + Member[] others = super.getMembers(); + Member[] result = new Member[members.size() + others.length]; + for (int i = 0; i < others.length; i++) { + result[i] = others[i]; + } + for (int i = 0; i < members.size(); i++) { + result[i + others.length] = members.get(i); + } + AbsoluteOrder.absoluteOrder(result); + return result; + }//sync + }//end if + } + + /** + * @param mbr Member + * @return Member + */ + @Override + public Member getMember(Member mbr) { + if ( members.contains(mbr) ) { + return members.get(members.indexOf(mbr)); + } else { + return super.getMember(mbr); + } + } + + /** + * Return the member that represents this node. + * + * @return Member + */ + @Override + public Member getLocalMember(boolean incAlive) { + if (this.localMember != null ) { + return localMember; + } else { + return super.getLocalMember(incAlive); + } + } + + /** + * {@inheritDoc} + *

    + * Sends notifications upwards. + */ + @Override + public void start(int svc) throws ChannelException { + if ( (Channel.SND_RX_SEQ&svc)==Channel.SND_RX_SEQ ) { + super.start(Channel.SND_RX_SEQ); + } + if ( (Channel.SND_TX_SEQ&svc)==Channel.SND_TX_SEQ ) { + super.start(Channel.SND_TX_SEQ); + } + final ChannelInterceptorBase base = this; + ScheduledExecutorService executor = getChannel().getUtilityExecutor(); + for (final Member member : members) { + Runnable r = () -> { + base.memberAdded(member); + if (getfirstInterceptor().getMember(member) != null) { + sendLocalMember(new Member[]{member}); + } + }; + executor.execute(r); + } + super.start(svc & (~Channel.SND_RX_SEQ) & (~Channel.SND_TX_SEQ)); + + // check required interceptors + TcpFailureDetector failureDetector = null; + TcpPingInterceptor pingInterceptor = null; + ChannelInterceptor prev = getPrevious(); + while (prev != null) { + if (prev instanceof TcpFailureDetector ) { + failureDetector = (TcpFailureDetector) prev; + } + if (prev instanceof TcpPingInterceptor) { + pingInterceptor = (TcpPingInterceptor) prev; + } + prev = prev.getPrevious(); + } + if (failureDetector == null) { + log.warn(sm.getString("staticMembershipInterceptor.no.failureDetector")); + } + if (pingInterceptor == null) { + log.warn(sm.getString("staticMembershipInterceptor.no.pingInterceptor")); + } + } + + /** + * {@inheritDoc} + *

    + * Sends local member shutdown. + */ + @Override + public void stop(int svc) throws ChannelException { + // Sends local member shutdown. + Member[] members = getfirstInterceptor().getMembers(); + sendShutdown(members); + super.stop(svc); + } + + protected void sendLocalMember(Member[] members) { + try { + sendMemberMessage(members, MEMBER_START); + } catch (ChannelException cx) { + log.warn(sm.getString("staticMembershipInterceptor.sendLocalMember.failed"),cx); + } + } + + protected void sendShutdown(Member[] members) { + try { + sendMemberMessage(members, MEMBER_STOP); + } catch (ChannelException cx) { + log.warn(sm.getString("staticMembershipInterceptor.sendShutdown.failed"),cx); + } + } + + protected ChannelInterceptor getfirstInterceptor() { + ChannelInterceptor result = null; + ChannelInterceptor now = this; + do { + result = now; + now = now.getPrevious(); + } while (now.getPrevious() != null); + return result; + } + + protected void sendMemberMessage(Member[] members, byte[] message) throws ChannelException { + if ( members == null || members.length == 0 ) { + return; + } + ChannelData data = new ChannelData(true); + data.setAddress(getLocalMember(false)); + data.setTimestamp(System.currentTimeMillis()); + data.setOptions(getOptionFlag()); + data.setMessage(new XByteBuffer(message, false)); + super.sendMessage(members, data, null); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptorMBean.java new file mode 100644 index 0000000..b090237 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptorMBean.java @@ -0,0 +1,26 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import org.apache.catalina.tribes.Member; + +public interface StaticMembershipInterceptorMBean { + + int getOptionFlag(); + + Member getLocalMember(boolean incAlive); +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java new file mode 100644 index 0000000..dbfd3ac --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java @@ -0,0 +1,445 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NoRouteToHostException; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.Arrays; +import java.util.HashMap; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelException.FaultyMember; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.RemoteProcessException; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.membership.Membership; +import org.apache.catalina.tribes.membership.StaticMember; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * The TcpFailureDetector is a useful interceptor that adds reliability to the + * membership layer. + *

    + * If the network is busy, or the system is busy so that the membership receiver thread + * is not getting enough time to update its table, members can be "timed out" + * This failure detector will intercept the memberDisappeared message(unless its a true shutdown message) + * and connect to the member using TCP. + *

    + * The TcpFailureDetector works in two ways: + *

      + *
    1. It intercepts memberDisappeared events
    2. + *
    3. It catches send errors
    4. + *
    + */ +public class TcpFailureDetector extends ChannelInterceptorBase implements TcpFailureDetectorMBean { + + private static final Log log = LogFactory.getLog(TcpFailureDetector.class); + protected static final StringManager sm = StringManager.getManager(TcpFailureDetector.class); + + protected static final byte[] TCP_FAIL_DETECT = new byte[] { + 79, -89, 115, 72, 121, -126, 67, -55, -97, 111, -119, -128, -95, 91, 7, 20, + 125, -39, 82, 91, -21, -15, 67, -102, -73, 126, -66, -113, -127, 103, 30, -74, + 55, 21, -66, -121, 69, 126, 76, -88, -65, 10, 77, 19, 83, 56, 21, 50, + 85, -10, -108, -73, 58, -6, 64, 120, -111, 4, 125, -41, 114, -124, -64, -43}; + + protected long connectTimeout = 1000;//1 second default + + protected boolean performSendTest = true; + + protected boolean performReadTest = false; + + protected long readTestTimeout = 5000;//5 seconds + + protected Membership membership = null; + + protected final HashMap removeSuspects = new HashMap<>(); + + protected final HashMap addSuspects = new HashMap<>(); + + protected int removeSuspectsTimeout = 300; // 5 minutes + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + try { + super.sendMessage(destination, msg, payload); + }catch ( ChannelException cx ) { + FaultyMember[] mbrs = cx.getFaultyMembers(); + for (FaultyMember mbr : mbrs) { + if (mbr.getCause() != null && + (!(mbr.getCause() instanceof RemoteProcessException))) {//RemoteProcessException's are ok + this.memberDisappeared(mbr.getMember()); + }//end if + }//for + throw cx; + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + //catch incoming + boolean process = true; + if ( okToProcess(msg.getOptions()) ) { + //check to see if it is a testMessage, if so, process = false + process = ( (msg.getMessage().getLength() != TCP_FAIL_DETECT.length) || + (!Arrays.equals(TCP_FAIL_DETECT,msg.getMessage().getBytes()) ) ); + }//end if + + //ignore the message, it doesn't have the flag set + if ( process ) { + super.messageReceived(msg); + } else if ( log.isDebugEnabled() ) { + log.debug(sm.getString("tcpFailureDetector.recievedPacket", msg)); + } + }//messageReceived + + + @Override + public void memberAdded(Member member) { + if ( membership == null ) { + setupMembership(); + } + boolean notify = false; + synchronized (membership) { + if (removeSuspects.containsKey(member)) { + //previously marked suspect, system below picked up the member again + removeSuspects.remove(member); + } else if (membership.getMember(member) == null){ + //if we add it here, then add it upwards too + //check to see if it is alive + if (memberAlive(member)) { + membership.memberAlive(member); + addSuspects.remove(member); + notify = true; + } else { + if (member instanceof StaticMember) { + addSuspects.put(member, Long.valueOf(System.currentTimeMillis())); + } + } + } + } + if ( notify ) { + super.memberAdded(member); + } + } + + @Override + public void memberDisappeared(Member member) { + if ( membership == null ) { + setupMembership(); + } + boolean shutdown = Arrays.equals(member.getCommand(),Member.SHUTDOWN_PAYLOAD); + if (shutdown) { + synchronized (membership) { + if (!membership.contains(member)) { + return; + } + membership.removeMember(member); + removeSuspects.remove(member); + if (member instanceof StaticMember) { + addSuspects.put(member, Long.valueOf(System.currentTimeMillis())); + } + } + super.memberDisappeared(member); + } else { + boolean notify = false; + if(log.isInfoEnabled()) { + log.info(sm.getString("tcpFailureDetector.memberDisappeared.verify", member)); + } + synchronized (membership) { + if (!membership.contains(member)) { + if(log.isInfoEnabled()) { + log.info(sm.getString("tcpFailureDetector.already.disappeared", member)); + } + return; + } + //check to see if the member really is gone + if (!memberAlive(member)) { + //not correct, we need to maintain the map + membership.removeMember(member); + removeSuspects.remove(member); + if (member instanceof StaticMember) { + addSuspects.put(member, Long.valueOf(System.currentTimeMillis())); + } + notify = true; + } else { + //add the member as suspect + removeSuspects.put(member, Long.valueOf(System.currentTimeMillis())); + } + } + if ( notify ) { + if(log.isInfoEnabled()) { + log.info(sm.getString("tcpFailureDetector.member.disappeared", member)); + } + super.memberDisappeared(member); + } else { + if(log.isInfoEnabled()) { + log.info(sm.getString("tcpFailureDetector.still.alive", member)); + } + } + } + } + + @Override + public boolean hasMembers() { + if ( membership == null ) { + setupMembership(); + } + return membership.hasMembers(); + } + + @Override + public Member[] getMembers() { + if ( membership == null ) { + setupMembership(); + } + return membership.getMembers(); + } + + @Override + public Member getMember(Member mbr) { + if ( membership == null ) { + setupMembership(); + } + return membership.getMember(mbr); + } + + @Override + public Member getLocalMember(boolean incAlive) { + return super.getLocalMember(incAlive); + } + + @Override + public void heartbeat() { + super.heartbeat(); + checkMembers(false); + } + + @Override + public void checkMembers(boolean checkAll) { + try { + if (membership == null) { + setupMembership(); + } + synchronized (membership) { + if (!checkAll) { + performBasicCheck(); + } else { + performForcedCheck(); + } + } + } catch (Exception x) { + log.warn(sm.getString("tcpFailureDetector.heartbeat.failed"),x); + } + } + + protected void performForcedCheck() { + //update all alive times + Member[] members = super.getMembers(); + for (int i = 0; members != null && i < members.length; i++) { + if (memberAlive(members[i])) { + if (membership.memberAlive(members[i])) { + super.memberAdded(members[i]); + } + addSuspects.remove(members[i]); + } else { + if (membership.getMember(members[i])!=null) { + membership.removeMember(members[i]); + removeSuspects.remove(members[i]); + if (members[i] instanceof StaticMember) { + addSuspects.put(members[i], Long.valueOf(System.currentTimeMillis())); + } + super.memberDisappeared(members[i]); + } + } //end if + } //for + + } + + protected void performBasicCheck() { + //update all alive times + Member[] members = super.getMembers(); + for (int i = 0; members != null && i < members.length; i++) { + if (addSuspects.containsKey(members[i]) && membership.getMember(members[i]) == null) { + // avoid temporary adding member. + continue; + } + if (membership.memberAlive(members[i])) { + //we don't have this one in our membership, check to see if the member is alive + if (memberAlive(members[i])) { + log.warn(sm.getString("tcpFailureDetector.performBasicCheck.memberAdded", members[i])); + super.memberAdded(members[i]); + } else { + membership.removeMember(members[i]); + } //end if + } //end if + } //for + + //check suspect members if they are still alive, + //if not, simply issue the memberDisappeared message + Member[] keys = removeSuspects.keySet().toArray(new Member[0]); + for (Member m : keys) { + if (membership.getMember(m) != null && (!memberAlive(m))) { + membership.removeMember(m); + if (m instanceof StaticMember) { + addSuspects.put(m, Long.valueOf(System.currentTimeMillis())); + } + super.memberDisappeared(m); + removeSuspects.remove(m); + if (log.isInfoEnabled()) { + log.info(sm.getString("tcpFailureDetector.suspectMember.dead", m)); + } + } else { + if (removeSuspectsTimeout > 0) { + long timeNow = System.currentTimeMillis(); + int timeIdle = (int) ((timeNow - removeSuspects.get(m).longValue()) / 1000L); + if (timeIdle > removeSuspectsTimeout) { + removeSuspects.remove(m); // remove suspect member + } + } + } + } + + //check add suspects members if they are alive now, + //if they are, simply issue the memberAdded message + keys = addSuspects.keySet().toArray(new Member[0]); + for (Member m : keys) { + if (membership.getMember(m) == null && (memberAlive(m))) { + membership.memberAlive(m); + super.memberAdded(m); + addSuspects.remove(m); + if (log.isInfoEnabled()) { + log.info(sm.getString("tcpFailureDetector.suspectMember.alive", m)); + } + } //end if + } + } + + protected synchronized void setupMembership() { + if ( membership == null ) { + membership = new Membership(super.getLocalMember(true)); + } + + } + + protected boolean memberAlive(Member mbr) { + return memberAlive(mbr,TCP_FAIL_DETECT,performSendTest,performReadTest,readTestTimeout,connectTimeout,getOptionFlag()); + } + + protected boolean memberAlive(Member mbr, byte[] msgData, + boolean sendTest, boolean readTest, + long readTimeout, long conTimeout, + int optionFlag) { + //could be a shutdown notification + if ( Arrays.equals(mbr.getCommand(),Member.SHUTDOWN_PAYLOAD) ) { + return false; + } + + try (Socket socket = new Socket()) { + InetAddress ia = InetAddress.getByAddress(mbr.getHost()); + InetSocketAddress addr = new InetSocketAddress(ia, mbr.getPort()); + socket.setSoTimeout((int)readTimeout); + socket.connect(addr, (int) conTimeout); + if ( sendTest ) { + ChannelData data = new ChannelData(true); + data.setAddress(getLocalMember(false)); + data.setMessage(new XByteBuffer(msgData,false)); + data.setTimestamp(System.currentTimeMillis()); + int options = optionFlag | Channel.SEND_OPTIONS_BYTE_MESSAGE; + if ( readTest ) { + options = (options | Channel.SEND_OPTIONS_USE_ACK); + } else { + options = (options & (~Channel.SEND_OPTIONS_USE_ACK)); + } + data.setOptions(options); + byte[] message = XByteBuffer.createDataPackage(data); + socket.getOutputStream().write(message); + if ( readTest ) { + int length = socket.getInputStream().read(message); + return length > 0; + } + }//end if + return true; + } catch (SocketTimeoutException | ConnectException | NoRouteToHostException noop) { + //do nothing, we couldn't connect + } catch (Exception x) { + log.error(sm.getString("tcpFailureDetector.failureDetection.failed", mbr),x); + } + return false; + } + + @Override + public long getReadTestTimeout() { + return readTestTimeout; + } + + @Override + public boolean getPerformSendTest() { + return performSendTest; + } + + @Override + public boolean getPerformReadTest() { + return performReadTest; + } + + @Override + public long getConnectTimeout() { + return connectTimeout; + } + + @Override + public int getRemoveSuspectsTimeout() { + return removeSuspectsTimeout; + } + + @Override + public void setPerformReadTest(boolean performReadTest) { + this.performReadTest = performReadTest; + } + + @Override + public void setPerformSendTest(boolean performSendTest) { + this.performSendTest = performSendTest; + } + + @Override + public void setReadTestTimeout(long readTestTimeout) { + this.readTestTimeout = readTestTimeout; + } + + @Override + public void setConnectTimeout(long connectTimeout) { + this.connectTimeout = connectTimeout; + } + + @Override + public void setRemoveSuspectsTimeout(int removeSuspectsTimeout) { + this.removeSuspectsTimeout = removeSuspectsTimeout; + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetectorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetectorMBean.java new file mode 100644 index 0000000..a31b75e --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetectorMBean.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.tribes.group.interceptors; + +public interface TcpFailureDetectorMBean { + + int getOptionFlag(); + + // Attributes + long getConnectTimeout(); + + boolean getPerformSendTest(); + + boolean getPerformReadTest(); + + long getReadTestTimeout(); + + int getRemoveSuspectsTimeout(); + + void setPerformReadTest(boolean performReadTest); + + void setPerformSendTest(boolean performSendTest); + + void setReadTestTimeout(long readTestTimeout); + + void setConnectTimeout(long connectTimeout); + + void setRemoveSuspectsTimeout(int removeSuspectsTimeout); + + // Operations + void checkMembers(boolean checkAll); +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/TcpPingInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/TcpPingInterceptor.java new file mode 100644 index 0000000..06a21c5 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/TcpPingInterceptor.java @@ -0,0 +1,206 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Sends a ping to all members. + * Configure this interceptor with the TcpFailureDetector below it, + * and the TcpFailureDetector will act as the membership guide. + */ + +public class TcpPingInterceptor extends ChannelInterceptorBase implements TcpPingInterceptorMBean { + + private static final Log log = LogFactory.getLog(TcpPingInterceptor.class); + protected static final StringManager sm = StringManager.getManager(TcpPingInterceptor.class); + + protected static final byte[] TCP_PING_DATA = new byte[] { + 79, -89, 115, 72, 121, -33, 67, -55, -97, 111, -119, -128, -95, 91, 7, 20, + 125, -39, 82, 91, -21, -33, 67, -102, -73, 126, -66, -113, -127, 103, 30, -74, + 55, 21, -66, -121, 69, 33, 76, -88, -65, 10, 77, 19, 83, 56, 21, 50, + 85, -10, -108, -73, 58, -33, 33, 120, -111, 4, 125, -41, 114, -124, -64, -43}; + + protected long interval = 1000; //1 second + + protected boolean useThread = false; + protected boolean staticOnly = false; + protected volatile boolean running = true; + protected PingThread thread = null; + protected static final AtomicInteger cnt = new AtomicInteger(0); + + WeakReference failureDetector = null; + WeakReference staticMembers = null; + + @Override + public synchronized void start(int svc) throws ChannelException { + super.start(svc); + running = true; + if ( thread == null && useThread) { + thread = new PingThread(); + thread.setDaemon(true); + String channelName = ""; + if (getChannel().getName() != null) { + channelName = "[" + getChannel().getName() + "]"; + } + thread.setName("TcpPingInterceptor.PingThread" + channelName +"-"+cnt.addAndGet(1)); + thread.start(); + } + + //acquire the interceptors to invoke on send ping events + ChannelInterceptor next = getNext(); + while ( next != null ) { + if ( next instanceof TcpFailureDetector ) { + failureDetector = new WeakReference<>((TcpFailureDetector)next); + } + if ( next instanceof StaticMembershipInterceptor ) { + staticMembers = new WeakReference<>((StaticMembershipInterceptor)next); + } + next = next.getNext(); + } + + } + + @Override + public synchronized void stop(int svc) throws ChannelException { + running = false; + if (thread != null) { + thread.interrupt(); + thread = null; + } + super.stop(svc); + } + + @Override + public void heartbeat() { + super.heartbeat(); + if (!getUseThread()) { + sendPing(); + } + } + + @Override + public long getInterval() { + return interval; + } + + public void setInterval(long interval) { + this.interval = interval; + } + + public void setUseThread(boolean useThread) { + this.useThread = useThread; + } + + public void setStaticOnly(boolean staticOnly) { + this.staticOnly = staticOnly; + } + + @Override + public boolean getUseThread() { + return useThread; + } + + public boolean getStaticOnly() { + return staticOnly; + } + + protected void sendPing() { + TcpFailureDetector tcpFailureDetector = + failureDetector != null ? failureDetector.get() : null; + if (tcpFailureDetector != null) { + // We have a reference to the failure detector + // Piggy back on it + tcpFailureDetector.checkMembers(true); + } else { + StaticMembershipInterceptor smi = + staticOnly && staticMembers != null ? staticMembers.get() : null; + if (smi != null) { + sendPingMessage(smi.getMembers()); + } else { + sendPingMessage(getMembers()); + } + } + } + + protected void sendPingMessage(Member[] members) { + if ( members == null || members.length == 0 ) { + return; + } + ChannelData data = new ChannelData(true);//generates a unique Id + data.setAddress(getLocalMember(false)); + data.setTimestamp(System.currentTimeMillis()); + data.setOptions(getOptionFlag()); + data.setMessage(new XByteBuffer(TCP_PING_DATA, false)); + try { + super.sendMessage(members, data, null); + }catch (ChannelException x) { + log.warn(sm.getString("tcpPingInterceptor.ping.failed"),x); + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + //catch incoming + boolean process = true; + if ( okToProcess(msg.getOptions()) ) { + //check to see if it is a ping message, if so, process = false + process = ( (msg.getMessage().getLength() != TCP_PING_DATA.length) || + (!Arrays.equals(TCP_PING_DATA,msg.getMessage().getBytes()) ) ); + }//end if + + //ignore the message, it doesn't have the flag set + if ( process ) { + super.messageReceived(msg); + } else if ( log.isTraceEnabled() ) { + log.trace("Received a TCP ping packet:" + msg); + } + }//messageReceived + + protected class PingThread extends Thread { + @Override + public void run() { + while (running) { + try { + sleep(interval); + sendPing(); + }catch ( InterruptedException ix ) { + // Ignore. Probably triggered by a call to stop(). + // In the highly unlikely event it was a different trigger, + // simply ignore it and continue. + }catch ( Exception x ) { + log.warn(sm.getString("tcpPingInterceptor.pingFailed.pingThread"),x); + } + } + } + } + + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/TcpPingInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/TcpPingInterceptorMBean.java new file mode 100644 index 0000000..dac0515 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/TcpPingInterceptorMBean.java @@ -0,0 +1,27 @@ +/* + * 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.catalina.tribes.group.interceptors; + +public interface TcpPingInterceptorMBean { + + int getOptionFlag(); + + long getInterval(); + + boolean getUseThread(); + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java new file mode 100644 index 0000000..05410b8 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java @@ -0,0 +1,169 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.text.DecimalFormat; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class ThroughputInterceptor extends ChannelInterceptorBase + implements ThroughputInterceptorMBean { + + private static final Log log = LogFactory.getLog(ThroughputInterceptor.class); + protected static final StringManager sm = StringManager.getManager(ThroughputInterceptor.class); + + double mbTx = 0; + double mbAppTx = 0; + double mbRx = 0; + double timeTx = 0; + double lastCnt = 0; + final AtomicLong msgTxCnt = new AtomicLong(1); + final AtomicLong msgRxCnt = new AtomicLong(0); + final AtomicLong msgTxErr = new AtomicLong(0); + int interval = 10000; + final AtomicInteger access = new AtomicInteger(0); + long txStart = 0; + long rxStart = 0; + final DecimalFormat df = new DecimalFormat("#0.00"); + + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + if ( access.addAndGet(1) == 1 ) { + txStart = System.currentTimeMillis(); + } + long bytes = XByteBuffer.getDataPackageLength(((ChannelData)msg).getDataPackageLength()); + try { + super.sendMessage(destination, msg, payload); + }catch ( ChannelException x ) { + msgTxErr.addAndGet(1); + if ( access.get() == 1 ) { + access.addAndGet(-1); + } + throw x; + } + mbTx += (bytes*destination.length)/(1024d*1024d); + mbAppTx += bytes/(1024d*1024d); + if ( access.addAndGet(-1) == 0 ) { + long stop = System.currentTimeMillis(); + timeTx += (stop - txStart) / 1000d; + if ((msgTxCnt.get() / (double) interval) >= lastCnt) { + lastCnt++; + report(timeTx); + } + } + msgTxCnt.addAndGet(1); + } + + @Override + public void messageReceived(ChannelMessage msg) { + if ( rxStart == 0 ) { + rxStart = System.currentTimeMillis(); + } + long bytes = XByteBuffer.getDataPackageLength(((ChannelData)msg).getDataPackageLength()); + mbRx += bytes/(1024d*1024d); + msgRxCnt.addAndGet(1); + if ( msgRxCnt.get() % interval == 0 ) { + report(timeTx); + } + super.messageReceived(msg); + + } + + @Override + public void report(double timeTx) { + if ( log.isInfoEnabled() ) { + log.info(sm.getString("throughputInterceptor.report", + msgTxCnt, df.format(mbTx), df.format(mbAppTx), df.format(timeTx), + df.format(mbTx/timeTx), df.format(mbAppTx/timeTx), msgTxErr, msgRxCnt, + df.format(mbRx/((System.currentTimeMillis()-rxStart)/(double)1000)), + df.format(mbRx))); + } + } + + @Override + public void setInterval(int interval) { + this.interval = interval; + } + + @Override + public int getInterval() { + return interval; + } + + @Override + public double getLastCnt() { + return lastCnt; + } + + @Override + public double getMbAppTx() { + return mbAppTx; + } + + @Override + public double getMbRx() { + return mbRx; + } + + @Override + public double getMbTx() { + return mbTx; + } + + @Override + public AtomicLong getMsgRxCnt() { + return msgRxCnt; + } + + @Override + public AtomicLong getMsgTxCnt() { + return msgTxCnt; + } + + @Override + public AtomicLong getMsgTxErr() { + return msgTxErr; + } + + @Override + public long getRxStart() { + return rxStart; + } + + @Override + public double getTimeTx() { + return timeTx; + } + + @Override + public long getTxStart() { + return txStart; + } + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptorMBean.java b/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptorMBean.java new file mode 100644 index 0000000..e40b177 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptorMBean.java @@ -0,0 +1,54 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.concurrent.atomic.AtomicLong; + +public interface ThroughputInterceptorMBean { + + int getOptionFlag(); + + // Attributes + int getInterval(); + + void setInterval(int interval); + + // stats + double getLastCnt(); + + double getMbAppTx(); + + double getMbRx(); + + double getMbTx(); + + AtomicLong getMsgRxCnt(); + + AtomicLong getMsgTxCnt(); + + AtomicLong getMsgTxErr(); + + long getRxStart(); + + double getTimeTx(); + + long getTxStart(); + + // Operations + void report(double timeTx); + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java new file mode 100644 index 0000000..0b463fe --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java @@ -0,0 +1,150 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.catalina.tribes.util.UUIDGenerator; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class TwoPhaseCommitInterceptor extends ChannelInterceptorBase { + + private static final byte[] START_DATA = new byte[] {113, 1, -58, 2, -34, -60, 75, -78, -101, -12, 32, -29, 32, 111, -40, 4}; + private static final byte[] END_DATA = new byte[] {54, -13, 90, 110, 47, -31, 75, -24, -81, -29, 36, 52, -58, 77, -110, 56}; + private static final Log log = LogFactory.getLog(TwoPhaseCommitInterceptor.class); + protected static final StringManager sm = StringManager.getManager(TwoPhaseCommitInterceptor.class); + + protected final HashMap messages = new HashMap<>(); + protected long expire = 1000 * 60; //one minute expiration + protected boolean deepclone = true; + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws + ChannelException { + //todo, optimize, if destination.length==1, then we can do + //msg.setOptions(msg.getOptions() & (~getOptionFlag()) + //and just send one message + if (okToProcess(msg.getOptions()) ) { + super.sendMessage(destination, msg, null); + ChannelMessage confirmation = null; + if ( deepclone ) { + confirmation = (ChannelMessage)msg.deepclone(); + } else { + confirmation = (ChannelMessage)msg.clone(); + } + confirmation.getMessage().reset(); + UUIDGenerator.randomUUID(false,confirmation.getUniqueId(),0); + confirmation.getMessage().append(START_DATA,0,START_DATA.length); + confirmation.getMessage().append(msg.getUniqueId(),0,msg.getUniqueId().length); + confirmation.getMessage().append(END_DATA,0,END_DATA.length); + super.sendMessage(destination,confirmation,payload); + } else { + //turn off two phase commit + //this won't work if the interceptor has 0 as a flag + //since there is no flag to turn off + //msg.setOptions(msg.getOptions() & (~getOptionFlag())); + super.sendMessage(destination, msg, payload); + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + if (okToProcess(msg.getOptions())) { + if ( msg.getMessage().getLength() == (START_DATA.length+msg.getUniqueId().length+END_DATA.length) && + Arrays.contains(msg.getMessage().getBytesDirect(),0,START_DATA,0,START_DATA.length) && + Arrays.contains(msg.getMessage().getBytesDirect(),START_DATA.length+msg.getUniqueId().length,END_DATA,0,END_DATA.length) ) { + UniqueId id = new UniqueId(msg.getMessage().getBytesDirect(),START_DATA.length,msg.getUniqueId().length); + MapEntry original = messages.get(id); + if ( original != null ) { + super.messageReceived(original.msg); + messages.remove(id); + } else { + log.warn(sm.getString("twoPhaseCommitInterceptor.originalMessage.missing", Arrays.toString(id.getBytes()))); + } + } else { + UniqueId id = new UniqueId(msg.getUniqueId()); + MapEntry entry = new MapEntry((ChannelMessage)msg.deepclone(),id,System.currentTimeMillis()); + messages.put(id,entry); + } + } else { + super.messageReceived(msg); + } + } + + public boolean getDeepclone() { + return deepclone; + } + + public long getExpire() { + return expire; + } + + public void setDeepclone(boolean deepclone) { + this.deepclone = deepclone; + } + + public void setExpire(long expire) { + this.expire = expire; + } + + @Override + public void heartbeat() { + try { + long now = System.currentTimeMillis(); + @SuppressWarnings("unchecked") + Map.Entry[] entries = messages.entrySet().toArray(new Map.Entry[0]); + for (Map.Entry uniqueIdMapEntryEntry : entries) { + MapEntry entry = uniqueIdMapEntryEntry.getValue(); + if (entry.expired(now, expire)) { + log.info(sm.getString("twoPhaseCommitInterceptor.expiredMessage", entry.id)); + messages.remove(entry.id); + } + } + } catch ( Exception x ) { + log.warn(sm.getString("twoPhaseCommitInterceptor.heartbeat.failed"),x); + } finally { + super.heartbeat(); + } + } + + public static class MapEntry { + public final ChannelMessage msg; + public final UniqueId id; + public final long timestamp; + + public MapEntry(ChannelMessage msg, UniqueId id, long timestamp) { + this.msg = msg; + this.id = id; + this.timestamp = timestamp; + } + public boolean expired(long now, long expiration) { + return (now - timestamp ) > expiration; + } + + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/io/BufferPool.java b/java/org/apache/catalina/tribes/io/BufferPool.java new file mode 100644 index 0000000..29e8804 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/BufferPool.java @@ -0,0 +1,96 @@ +/* + * 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.catalina.tribes.io; + + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class BufferPool { + private static final Log log = LogFactory.getLog(BufferPool.class); + + public static final int DEFAULT_POOL_SIZE = + Integer.getInteger("org.apache.catalina.tribes.io.BufferPool.DEFAULT_POOL_SIZE", 100*1024*1024).intValue(); //100 MiB + + protected static final StringManager sm = StringManager.getManager(BufferPool.class); + + protected static volatile BufferPool instance = null; + + public static BufferPool getBufferPool() { + if (instance == null) { + synchronized (BufferPool.class) { + if (instance == null) { + BufferPool pool = new BufferPool(); + pool.setMaxSize(DEFAULT_POOL_SIZE); + log.info(sm.getString("bufferPool.created", + Integer.toString(DEFAULT_POOL_SIZE), + pool.getClass().getName())); + instance = pool; + } + } + } + return instance; + } + + private BufferPool() { + } + + public XByteBuffer getBuffer(int minSize, boolean discard) { + XByteBuffer buffer = queue.poll(); + if ( buffer != null ) { + size.addAndGet(-buffer.getCapacity()); + } + if ( buffer == null ) { + buffer = new XByteBuffer(minSize,discard); + } else if ( buffer.getCapacity() <= minSize ) { + buffer.expand(minSize); + } + buffer.setDiscard(discard); + buffer.reset(); + return buffer; + } + + public void returnBuffer(XByteBuffer buffer) { + if ( (size.get() + buffer.getCapacity()) <= maxSize ) { + size.addAndGet(buffer.getCapacity()); + queue.offer(buffer); + } + } + + public void clear() { + queue.clear(); + size.set(0); + } + + protected int maxSize; + protected final AtomicInteger size = new AtomicInteger(0); + protected final ConcurrentLinkedQueue queue = + new ConcurrentLinkedQueue<>(); + + public void setMaxSize(int bytes) { + this.maxSize = bytes; + } + + public int getMaxSize() { + return maxSize; + } + +} diff --git a/java/org/apache/catalina/tribes/io/ChannelData.java b/java/org/apache/catalina/tribes/io/ChannelData.java new file mode 100644 index 0000000..86c5250 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/ChannelData.java @@ -0,0 +1,380 @@ +/* + * 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.catalina.tribes.io; + +import java.sql.Timestamp; +import java.util.Arrays; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.util.UUIDGenerator; + +/** + * The ChannelData object is used to transfer a message through the + * channel interceptor stack and eventually out on a transport to be sent + * to another node. While the message is being processed by the different + * interceptors, the message data can be manipulated as each interceptor seems appropriate. + * @author Peter Rossbach + */ +public class ChannelData implements ChannelMessage { + private static final long serialVersionUID = 1L; + + public static final ChannelData[] EMPTY_DATA_ARRAY = new ChannelData[0]; + + public static volatile boolean USE_SECURE_RANDOM_FOR_UUID = false; + + /** + * The options this message was sent with + */ + private int options = 0 ; + /** + * The message data, stored in a dynamic buffer + */ + private XByteBuffer message ; + /** + * The timestamp that goes with this message + */ + private long timestamp ; + /** + * A unique message id + */ + private byte[] uniqueId ; + /** + * The source or reply-to address for this message + */ + private Member address; + + /** + * Creates an empty channel data with a new unique Id + * @see #ChannelData(boolean) + */ + public ChannelData() { + this(true); + } + + /** + * Create an empty channel data object + * @param generateUUID boolean - if true, a unique Id will be generated + */ + public ChannelData(boolean generateUUID) { + if ( generateUUID ) { + generateUUID(); + } + } + + + /** + * Creates a new channel data object with data + * @param uniqueId - unique message id + * @param message - message data + * @param timestamp - message timestamp + */ + public ChannelData(byte[] uniqueId, XByteBuffer message, long timestamp) { + this.uniqueId = uniqueId; + this.message = message; + this.timestamp = timestamp; + } + + /** + * @return Returns the message byte buffer + */ + @Override + public XByteBuffer getMessage() { + return message; + } + /** + * @param message The message to send. + */ + @Override + public void setMessage(XByteBuffer message) { + this.message = message; + } + /** + * @return Returns the timestamp. + */ + @Override + public long getTimestamp() { + return timestamp; + } + /** + * @param timestamp The timestamp to send + */ + @Override + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + /** + * @return Returns the uniqueId. + */ + @Override + public byte[] getUniqueId() { + return uniqueId; + } + /** + * @param uniqueId The uniqueId to send. + */ + public void setUniqueId(byte[] uniqueId) { + this.uniqueId = uniqueId; + } + /** + * @return returns the message options + * see org.apache.catalina.tribes.Channel#sendMessage(org.apache.catalina.tribes.Member[], java.io.Serializable, int) + * + */ + @Override + public int getOptions() { + return options; + } + /** + * Sets the message options. + * + * @param options the message options + */ + @Override + public void setOptions(int options) { + this.options = options; + } + + /** + * Returns the source or reply-to address + * @return Member + */ + @Override + public Member getAddress() { + return address; + } + + /** + * Sets the source or reply-to address + * @param address Member + */ + @Override + public void setAddress(Member address) { + this.address = address; + } + + /** + * Generates a UUID and invokes setUniqueId + */ + public void generateUUID() { + byte[] data = new byte[16]; + UUIDGenerator.randomUUID(USE_SECURE_RANDOM_FOR_UUID,data,0); + setUniqueId(data); + } + + public int getDataPackageLength() { + int length = + 4 + //options + 8 + //timestamp off=4 + 4 + //unique id length off=12 + uniqueId.length+ //id data off=12+uniqueId.length + 4 + //addr length off=12+uniqueId.length+4 + address.getDataLength()+ //member data off=12+uniqueId.length+4+add.length + 4 + //message length off=12+uniqueId.length+4+add.length+4 + message.getLength(); + return length; + + } + + /** + * Serializes the ChannelData object into a byte[] array + * @return byte[] + */ + public byte[] getDataPackage() { + int length = getDataPackageLength(); + byte[] data = new byte[length]; + int offset = 0; + return getDataPackage(data,offset); + } + + public byte[] getDataPackage(byte[] data, int offset) { + byte[] addr = address.getData(false); + XByteBuffer.toBytes(options,data,offset); + offset += 4; //options + XByteBuffer.toBytes(timestamp,data,offset); + offset += 8; //timestamp + XByteBuffer.toBytes(uniqueId.length,data,offset); + offset += 4; //uniqueId.length + System.arraycopy(uniqueId,0,data,offset,uniqueId.length); + offset += uniqueId.length; //uniqueId data + XByteBuffer.toBytes(addr.length,data,offset); + offset += 4; //addr.length + System.arraycopy(addr,0,data,offset,addr.length); + offset += addr.length; //addr data + XByteBuffer.toBytes(message.getLength(),data,offset); + offset += 4; //message.length + System.arraycopy(message.getBytesDirect(),0,data,offset,message.getLength()); + return data; + } + + /** + * Deserializes a ChannelData object from a byte array + * @param xbuf byte[] + * @return ChannelData + */ + public static ChannelData getDataFromPackage(XByteBuffer xbuf) { + ChannelData data = new ChannelData(false); + int offset = 0; + data.setOptions(XByteBuffer.toInt(xbuf.getBytesDirect(),offset)); + offset += 4; //options + data.setTimestamp(XByteBuffer.toLong(xbuf.getBytesDirect(),offset)); + offset += 8; //timestamp + data.uniqueId = new byte[XByteBuffer.toInt(xbuf.getBytesDirect(),offset)]; + offset += 4; //uniqueId length + System.arraycopy(xbuf.getBytesDirect(),offset,data.uniqueId,0,data.uniqueId.length); + offset += data.uniqueId.length; //uniqueId data + //byte[] addr = new byte[XByteBuffer.toInt(xbuf.getBytesDirect(),offset)]; + int addrlen = XByteBuffer.toInt(xbuf.getBytesDirect(),offset); + offset += 4; //addr length + //System.arraycopy(xbuf.getBytesDirect(),offset,addr,0,addr.length); + data.setAddress(MemberImpl.getMember(xbuf.getBytesDirect(),offset,addrlen)); + //offset += addr.length; //addr data + offset += addrlen; + int xsize = XByteBuffer.toInt(xbuf.getBytesDirect(),offset); + offset += 4; //xsize length + System.arraycopy(xbuf.getBytesDirect(),offset,xbuf.getBytesDirect(),0,xsize); + xbuf.setLength(xsize); + data.message = xbuf; + return data; + + } + + public static ChannelData getDataFromPackage(byte[] b) { + ChannelData data = new ChannelData(false); + int offset = 0; + data.setOptions(XByteBuffer.toInt(b,offset)); + offset += 4; //options + data.setTimestamp(XByteBuffer.toLong(b,offset)); + offset += 8; //timestamp + data.uniqueId = new byte[XByteBuffer.toInt(b,offset)]; + offset += 4; //uniqueId length + System.arraycopy(b,offset,data.uniqueId,0,data.uniqueId.length); + offset += data.uniqueId.length; //uniqueId data + byte[] addr = new byte[XByteBuffer.toInt(b,offset)]; + offset += 4; //addr length + System.arraycopy(b,offset,addr,0,addr.length); + data.setAddress(MemberImpl.getMember(addr)); + offset += addr.length; //addr data + int xsize = XByteBuffer.toInt(b,offset); + //data.message = new XByteBuffer(new byte[xsize],false); + data.message = BufferPool.getBufferPool().getBuffer(xsize,false); + offset += 4; //message length + System.arraycopy(b,offset,data.message.getBytesDirect(),0,xsize); + data.message.append(b,offset,xsize); + offset += xsize; //message data + return data; + } + + @Override + public int hashCode() { + return XByteBuffer.toInt(getUniqueId(),0); + } + + /** + * Compares to ChannelData objects, only compares on getUniqueId().equals(o.getUniqueId()) + * @param o Object + * @return boolean + */ + @Override + public boolean equals(Object o) { + if ( o instanceof ChannelData ) { + return Arrays.equals(getUniqueId(),((ChannelData)o).getUniqueId()); + } else { + return false; + } + } + + /** + * Create a shallow clone, only the data gets recreated + * @return ClusterData + */ + @Override + public ChannelData clone() { + ChannelData clone; + try { + clone = (ChannelData) super.clone(); + } catch (CloneNotSupportedException e) { + // Cannot happen + throw new AssertionError(); + } + if (this.message != null) { + clone.message = new XByteBuffer(this.message.getBytesDirect(),false); + } + return clone; + } + + /** + * Complete clone + * @return ClusterData + */ + @Override + public Object deepclone() { + byte[] d = this.getDataPackage(); + return getDataFromPackage(d); + } + + /** + * Utility method, returns true if the options flag indicates that an ack + * is to be sent after the message has been received and processed + * @param options int - the options for the message + * @return boolean + * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_USE_ACK + * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_SYNCHRONIZED_ACK + */ + public static boolean sendAckSync(int options) { + return ( (Channel.SEND_OPTIONS_USE_ACK & options) == Channel.SEND_OPTIONS_USE_ACK) && + ( (Channel.SEND_OPTIONS_SYNCHRONIZED_ACK & options) == Channel.SEND_OPTIONS_SYNCHRONIZED_ACK); + } + + + /** + * Utility method, returns true if the options flag indicates that an ack + * is to be sent after the message has been received but not yet processed + * @param options int - the options for the message + * @return boolean + * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_USE_ACK + * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_SYNCHRONIZED_ACK + */ + public static boolean sendAckAsync(int options) { + return ( (Channel.SEND_OPTIONS_USE_ACK & options) == Channel.SEND_OPTIONS_USE_ACK) && + ( (Channel.SEND_OPTIONS_SYNCHRONIZED_ACK & options) != Channel.SEND_OPTIONS_SYNCHRONIZED_ACK); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("ClusterData[src="); + buf.append(getAddress()).append("; id="); + buf.append(bToS(getUniqueId())).append("; sent="); + buf.append(new Timestamp(this.getTimestamp()).toString()).append(']'); + return buf.toString(); + } + + public static String bToS(byte[] data) { + StringBuilder buf = new StringBuilder(4*16); + buf.append('{'); + for (int i=0; data!=null && ibyte. + * @throws IOException if an I/O error occurs. In particular, an + * IOException may be thrown if the output stream has + * been closed. + */ + @Override + public void write(int b) throws IOException { + buffer.append((byte)b); + } + + public int size() { + return buffer.getLength(); + } + + public byte[] getArrayDirect() { + return buffer.getBytesDirect(); + } + + public byte[] getArray() { + return buffer.getBytes(); + } + + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/io/ListenCallback.java b/java/org/apache/catalina/tribes/io/ListenCallback.java new file mode 100644 index 0000000..66a1741 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/ListenCallback.java @@ -0,0 +1,39 @@ +/* + * 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.catalina.tribes.io; + +import org.apache.catalina.tribes.ChannelMessage; + + + +/** + * Internal interface, similar to the MessageListener but used + * at the IO base + * The listen callback interface is used by the replication system + * when data has been received. The interface does not care about + * objects and marshalling and just passes the bytes straight through. + */ +public interface ListenCallback +{ + /** + * This method is invoked on the callback object to notify it that new data has + * been received from one of the cluster nodes. + * @param data - the message bytes received from the cluster/replication system + */ + void messageDataReceived(ChannelMessage data); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/io/LocalStrings.properties b/java/org/apache/catalina/tribes/io/LocalStrings.properties new file mode 100644 index 0000000..534d0e4 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings.properties @@ -0,0 +1,27 @@ +# 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. + +bufferPool.created=Created a buffer pool with max size:[{0}] bytes of type: [{1}] + +objectReader.retrieveFailed.socketReceiverBufferSize=Unable to retrieve the socket receiver buffer size, setting to default [{0}] bytes. + +replicationStream.conflict=conflicting non-public interface class loaders + +xByteBuffer.discarded.invalidHeader=Discarded the package, invalid header +xByteBuffer.no.package=No package exists in XByteBuffer +xByteBuffer.size.larger.buffer=Size is larger than existing buffer. +xByteBuffer.unableCreate=Unable to create data package, buffer is too small. +xByteBuffer.unableTrim=Cannot trim more bytes than are available. length:[{0}] trim:[{1}] +xByteBuffer.wrong.class=Message has the wrong class. It should implement Serializable, instead it is:[{0}] diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/io/LocalStrings_cs.properties new file mode 100644 index 0000000..5891ce8 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_cs.properties @@ -0,0 +1,22 @@ +# 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. + +objectReader.retrieveFailed.socketReceiverBufferSize=Nelze získat velikost bufferu pro socket receiver, nastaveno na výchozí hodnotu [{0}] bytes. + +replicationStream.conflict=konflikt class loaderů neveÅ™ejného rozhraní + +xByteBuffer.no.package=Neexistuje package v XByteBuffer +xByteBuffer.unableCreate=Nelze vytvoÅ™ity datový balíÄek, buffer je příliÅ¡ malý. +xByteBuffer.wrong.class=Zpráva má Å¡patnou třídu. Třída má implementovat Serializable, míst toho je:[{0}] diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_de.properties b/java/org/apache/catalina/tribes/io/LocalStrings_de.properties new file mode 100644 index 0000000..7967e8f --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_de.properties @@ -0,0 +1,21 @@ +# 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. + +objectReader.retrieveFailed.socketReceiverBufferSize=Kann die Größe der Socket Puffer nicht auslesen. Setze sie auf den Standardwert von [{0}] Bytes. + +xByteBuffer.no.package=In XByteBuffer existiert kein Paket +xByteBuffer.size.larger.buffer=Größe liegt über dem bestehenden Buffer. +xByteBuffer.unableCreate=Kann Daten Paket nicht erzeugen, da der Puffer zu klein ist +xByteBuffer.wrong.class=Nachricht hat eine falsche Klasse. Sie sollte Serializable implementieren, sie implementiert aber: [{0}] diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_es.properties b/java/org/apache/catalina/tribes/io/LocalStrings_es.properties new file mode 100644 index 0000000..332cd08 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_es.properties @@ -0,0 +1,23 @@ +# 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. + +objectReader.retrieveFailed.socketReceiverBufferSize=Incapáz de recuperar el tamaño del buffer receptor, fijando el valor por defecto a [{0}] bytes.\n + +replicationStream.conflict=hay conflicto en la clase de los cargadores de la interfaz no pública + +xByteBuffer.no.package=No existe paquete en XByteBuffer +xByteBuffer.size.larger.buffer=El tamaño es mayor que el buffer +xByteBuffer.unableCreate=Imposible crear el paquete de datos, el buffer es demasiado pequeño. +xByteBuffer.wrong.class=El mensaje tiene la clase errónea. Debe implementar Serializable, pero en su lugar es:[{0}] diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/io/LocalStrings_fr.properties new file mode 100644 index 0000000..2450c0c --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_fr.properties @@ -0,0 +1,27 @@ +# 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. + +bufferPool.created=Création d''un pool de tampons de taille maximale : [{0}] octets de type : [{1}] + +objectReader.retrieveFailed.socketReceiverBufferSize=Incapacité de récupérer la taille du tampon du socket de réception, forcé à [{0}] octets + +replicationStream.conflict=Conflit entre des interfaces non-publics ayant des chargeurs de classe différents + +xByteBuffer.discarded.invalidHeader=L'en-tête est invalide donc le paquet a été abandonné +xByteBuffer.no.package=Il n'y a aucun package dans XByteBuffer +xByteBuffer.size.larger.buffer=La taille est plus grande que celle du buffer existant +xByteBuffer.unableCreate=Impossible de créer le package data, le tampon de mémoire est trop petit +xByteBuffer.unableTrim=Impossible d''élaguer plus d''octets que ce qui est disponible, longueur : [{0}] élagage : [{1}] +xByteBuffer.wrong.class=Message n''a pas la bonne classe. Cela doit implémenter Serializable au lieu de [{0}] diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/io/LocalStrings_ja.properties new file mode 100644 index 0000000..cd1fa28 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_ja.properties @@ -0,0 +1,27 @@ +# 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. + +bufferPool.created=最大サイズ [{1}] ã®ã‚¿ã‚¤ãƒ—ã® [{0}] ãƒã‚¤ãƒˆã®ãƒãƒƒãƒ•ã‚¡ãƒ—ールを作æˆã—ã¾ã—㟠+ +objectReader.retrieveFailed.socketReceiverBufferSize=TCP ソケットã®ãƒ¬ã‚·ãƒ¼ãƒãƒ¼ãƒãƒƒãƒ•ã‚¡ã‚µã‚¤ã‚ºã‚’å–å¾—ã§ããªã‹ã£ãŸãŸã‚既定値ã¨ã—㦠[{0}] byte を設定ã—ã¾ã™ã€‚ + +replicationStream.conflict=public ã§ãªã„インターフェイスã®ã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ãƒ¼ãŒè¤‡æ•°å­˜åœ¨ã—ã¾ã™ + +xByteBuffer.discarded.invalidHeader=ヘッダーãŒä¸æ­£ãªãŸã‚パッケージを破棄ã—ã¾ã™ã€‚ +xByteBuffer.no.package=XByteBuffer ã«ãƒ‘ッケージãŒã‚ã‚Šã¾ã›ã‚“。 +xByteBuffer.size.larger.buffer=ãƒãƒƒãƒ•ã‚¡é•·ã‚ˆã‚Šå¤§ããªå€¤ãŒã‚µã‚¤ã‚ºã¨ã—ã¦æŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +xByteBuffer.unableCreate=ãƒãƒƒãƒ•ã‚¡ãƒ¼ãŒå°ã•ã™ãŽã¦ãƒ‡ãƒ¼ã‚¿ãƒ‘ッケージを作æˆã§ãã¾ã›ã‚“。 +xByteBuffer.unableTrim=利用å¯èƒ½ãªãƒã‚¤ãƒˆæ•°ã‚ˆã‚Šå¤šãã®ãƒã‚¤ãƒˆã‚’トリムã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 é•·ã•ï¼š[{0}]トリム:[{1}] +xByteBuffer.wrong.class=メッセージã®ClassãŒé–“é•ã£ã¦ã„ã¾ã™ã€‚ãã‚Œã¯Serializableを実装ã™ã‚‹ã¯ãšã§ã™ãŒã€ä»£ã‚ã‚Šã« [{0}] ã§ã™ diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/io/LocalStrings_ko.properties new file mode 100644 index 0000000..88c2681 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_ko.properties @@ -0,0 +1,27 @@ +# 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. + +bufferPool.created=íƒ€ìž…ì´ [{1}](ì´)ê³  최대 í¬ê¸°ê°€ [{0}] ë°”ì´íŠ¸ë¥¼ 사용하여 ë²„í¼ í’€ì„ ìƒì„±í–ˆìŠµë‹ˆë‹¤. + +objectReader.retrieveFailed.socketReceiverBufferSize=소켓 receiverì˜ ë²„í¼ í¬ê¸°ë¥¼ ì•Œ 수 없습니다. 기본 ê°’ì¸ [{0}] ë°”ì´íŠ¸ë¡œ 설정합니다. + +replicationStream.conflict=해당 í´ëž˜ìŠ¤ì˜ í´ëž˜ìŠ¤ë¡œë”와, ê·¸ í´ëž˜ìŠ¤ì˜ 특정 non-public ì¸í„°íŽ˜ì´ìŠ¤ì˜ í´ëž˜ìŠ¤ë¡œë”ê°€ ì¼ì¹˜í•˜ì§€ ì•Šì•„ 충ëŒí•©ë‹ˆë‹¤. + +xByteBuffer.discarded.invalidHeader=유효하지 ì•Šì€ í—¤ë”ë¡œ ì¸í•´ 패키지를 í기했습니다. +xByteBuffer.no.package=XByteBufferì— ì–´ë–¤ íŒ¨í‚¤ì§€ë„ ì¡´ìž¬í•˜ì§€ 않습니다. +xByteBuffer.size.larger.buffer=í¬ê¸°ê°€ 기존 버í¼ì˜ 길ì´ë³´ë‹¤ í½ë‹ˆë‹¤. +xByteBuffer.unableCreate=ë°ì´í„° 패키지를 ìƒì„±í•  수 없습니다. 버í¼ê°€ 너무 작습니다. +xByteBuffer.unableTrim=가용한 ë²„í¼ í¬ê¸°ë³´ë‹¤ ë” ë§Žì€ ë°”ì´íŠ¸ë“¤ì„ 잘ë¼ë‚¼ 수는 없습니다. ë²„í¼ ê¸¸ì´:[{0}], 잘ë¼ë‚¼ 길ì´:[{1}] +xByteBuffer.wrong.class=메시지가 Serializable ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 구현하지 ì•Šì€ í´ëž˜ìŠ¤ìž…니다. í´ëž˜ìŠ¤ëŠ” [{0}]입니다. diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_pt_BR.properties b/java/org/apache/catalina/tribes/io/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..8544cc9 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_pt_BR.properties @@ -0,0 +1,18 @@ +# 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. + +replicationStream.conflict=conflitando com class loaders de interfaces não-públicas + +xByteBuffer.unableCreate=Impossível criar um pacote de dados, buffer é muito pequeno diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_ru.properties b/java/org/apache/catalina/tribes/io/LocalStrings_ru.properties new file mode 100644 index 0000000..08318ef --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +replicationStream.conflict=конфликтующие непубличные загрузчики клаÑÑов diff --git a/java/org/apache/catalina/tribes/io/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/io/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..ba016d3 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/LocalStrings_zh_CN.properties @@ -0,0 +1,27 @@ +# 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. + +bufferPool.created=已创建缓冲池,最大大å°ä¸ºï¼š[{0}]字节,类型为:[{1}] + +objectReader.retrieveFailed.socketReceiverBufferSize=无法检索套接字接收器缓冲区大å°ï¼Œè®¾ç½®ä¸ºé»˜è®¤[{0}]字节。 + +replicationStream.conflict=å’Œéžå…¬å¼€æŽ¥å£ç±»åŠ è½½å™¨å†²çª + +xByteBuffer.discarded.invalidHeader=丢弃了包,头无效。 +xByteBuffer.no.package=XByteBuffer中ä¸å­˜åœ¨æ•°æ®åŒ… +xByteBuffer.size.larger.buffer=大å°æ¯”现有缓冲区大。 +xByteBuffer.unableCreate=ä¸èƒ½åˆ›å»ºæ•°æ®åŒ…, buffer å¤ªå° +xByteBuffer.unableTrim=修剪的字节数ä¸èƒ½è¶…过å¯ç”¨å­—节数。长度:[{0}]修剪:[{1}] +xByteBuffer.wrong.class=消æ¯å¯¹åº”ç±»ä¸ç¬¦åˆè¦æ±‚。 它应该实现Serializable,而ä¸æ˜¯ï¼š[{0}]。 diff --git a/java/org/apache/catalina/tribes/io/ObjectReader.java b/java/org/apache/catalina/tribes/io/ObjectReader.java new file mode 100644 index 0000000..7851352 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/ObjectReader.java @@ -0,0 +1,173 @@ +/* + * 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.catalina.tribes.io; + +import java.io.IOException; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.transport.Constants; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + + +/** + * The object reader object is an object used in conjunction with + * java.nio TCP messages. This object stores the message bytes in a + * XByteBuffer until a full package has been received. + * This object uses an XByteBuffer which is an extendable object buffer that also allows + * for message encoding and decoding. + */ +public class ObjectReader { + + private static final Log log = LogFactory.getLog(ObjectReader.class); + protected static final StringManager sm = StringManager.getManager(ObjectReader.class); + + private XByteBuffer buffer; + + protected long lastAccess = System.currentTimeMillis(); + + protected boolean accessed = false; + private volatile boolean cancelled; + + public ObjectReader(int packetSize) { + this.buffer = new XByteBuffer(packetSize, true); + } + /** + * Creates an ObjectReader for a TCP NIO socket channel + * @param channel - the channel to be read. + */ + public ObjectReader(SocketChannel channel) { + this(channel.socket()); + } + + /** + * Creates an ObjectReader for a TCP socket + * @param socket Socket + */ + public ObjectReader(Socket socket) { + try{ + this.buffer = new XByteBuffer(socket.getReceiveBufferSize(), true); + }catch ( IOException x ) { + //unable to get buffer size + log.warn(sm.getString("objectReader.retrieveFailed.socketReceiverBufferSize", + Integer.toString(Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE))); + this.buffer = new XByteBuffer(Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE, true); + } + } + + public synchronized void access() { + this.accessed = true; + this.lastAccess = System.currentTimeMillis(); + } + + public synchronized void finish() { + this.accessed = false; + this.lastAccess = System.currentTimeMillis(); + } + + public synchronized boolean isAccessed() { + return this.accessed; + } + + /** + * Append new bytes to buffer. + * @see XByteBuffer#countPackages() + * @param data new transfer buffer + * @param len length in buffer + * @param count whether to return the count + * @return number of messages that was sent to callback (or -1 if count == false) + */ + public int append(ByteBuffer data, int len, boolean count) { + buffer.append(data,len); + int pkgCnt = -1; + if ( count ) { + pkgCnt = buffer.countPackages(); + } + return pkgCnt; + } + + public int append(byte[] data,int off,int len, boolean count) { + buffer.append(data,off,len); + int pkgCnt = -1; + if ( count ) { + pkgCnt = buffer.countPackages(); + } + return pkgCnt; + } + + /** + * Send buffer to cluster listener (callback). + * Is message complete receiver send message to callback? + * + * @see org.apache.catalina.tribes.transport.ReceiverBase#messageDataReceived(ChannelMessage) + * @see XByteBuffer#doesPackageExist() + * @see XByteBuffer#extractPackage(boolean) + * + * @return number of received packages/messages + */ + public ChannelMessage[] execute() { + int pkgCnt = buffer.countPackages(); + ChannelMessage[] result = new ChannelMessage[pkgCnt]; + for (int i=0; i0; + } + /** + * Returns the number of packages that the reader has read + * @return int + */ + public int count() { + return buffer.countPackages(); + } + + public void close() { + this.buffer = null; + } + + public synchronized long getLastAccess() { + return lastAccess; + } + + public boolean isCancelled() { + return cancelled; + } + + public synchronized void setLastAccess(long lastAccess) { + this.lastAccess = lastAccess; + } + + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + +} diff --git a/java/org/apache/catalina/tribes/io/ReplicationStream.java b/java/org/apache/catalina/tribes/io/ReplicationStream.java new file mode 100644 index 0000000..0cfeb65 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/ReplicationStream.java @@ -0,0 +1,178 @@ +/* + * 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.catalina.tribes.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; + +import org.apache.catalina.tribes.util.StringManager; + +/** + * Custom subclass of ObjectInputStream that loads from the + * class loader for this web application. This allows classes defined only + * with the web application to be found correctly. + * + * @author Craig R. McClanahan + * @author Bip Thelin + */ +public final class ReplicationStream extends ObjectInputStream { + + static final StringManager sm = StringManager.getManager(ReplicationStream.class); + + /** + * The class loader we will use to resolve classes. + */ + private ClassLoader[] classLoaders = null; + + /** + * Construct a new instance of CustomObjectInputStream + * + * @param stream The input stream we will read from + * @param classLoaders The class loader array used to instantiate objects + * + * @exception IOException if an input/output error occurs + */ + public ReplicationStream(InputStream stream, + ClassLoader[] classLoaders) + throws IOException { + + super(stream); + this.classLoaders = classLoaders; + } + + /** + * Load the local class equivalent of the specified stream class + * description, by using the class loader assigned to this Context. + * + * @param classDesc Class description from the input stream + * + * @exception ClassNotFoundException if this class cannot be found + * @exception IOException if an input/output error occurs + */ + @Override + public Class resolveClass(ObjectStreamClass classDesc) + throws ClassNotFoundException, IOException { + String name = classDesc.getName(); + try { + return resolveClass(name); + } catch (ClassNotFoundException e) { + return super.resolveClass(classDesc); + } + } + + public Class resolveClass(String name) throws ClassNotFoundException { + + boolean tryRepFirst = name.startsWith("org.apache.catalina.tribes"); + try { + if (tryRepFirst) { + return findReplicationClass(name); + } else { + return findExternalClass(name); + } + } catch (Exception x) { + if (tryRepFirst) { + return findExternalClass(name); + } else { + return findReplicationClass(name); + } + } + } + + /** + * ObjectInputStream.resolveProxyClass has some funky way of using + * the incorrect class loader to resolve proxy classes, let's do it our way instead + */ + @Override + protected Class resolveProxyClass(String[] interfaces) + throws IOException, ClassNotFoundException { + + ClassLoader latestLoader; + if (classLoaders.length > 0) { + latestLoader = classLoaders[0]; + } else { + latestLoader = null; + } + ClassLoader nonPublicLoader = null; + boolean hasNonPublicInterface = false; + + // define proxy in class loader of non-public interface(s), if any + Class[] classObjs = new Class[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + Class cl = this.resolveClass(interfaces[i]); + if (latestLoader==null) { + latestLoader = cl.getClassLoader(); + } + if ((cl.getModifiers() & Modifier.PUBLIC) == 0) { + if (hasNonPublicInterface) { + if (nonPublicLoader != cl.getClassLoader()) { + throw new IllegalAccessError( + sm.getString("replicationStream.conflict")); + } + } else { + nonPublicLoader = cl.getClassLoader(); + hasNonPublicInterface = true; + } + } + classObjs[i] = cl; + } + try { + // No way to avoid this at the moment + @SuppressWarnings("deprecation") + Class proxyClass = Proxy.getProxyClass(hasNonPublicInterface ? nonPublicLoader + : latestLoader, classObjs); + return proxyClass; + } catch (IllegalArgumentException e) { + throw new ClassNotFoundException(null, e); + } + } + + + public Class findReplicationClass(String name) + throws ClassNotFoundException { + Class clazz = Class.forName(name, false, getClass().getClassLoader()); + return clazz; + } + + public Class findExternalClass(String name) throws ClassNotFoundException { + ClassNotFoundException cnfe = null; + for (ClassLoader classLoader : classLoaders) { + try { + Class clazz = Class.forName(name, false, classLoader); + return clazz; + } catch (ClassNotFoundException x) { + cnfe = x; + } + } + if ( cnfe != null ) { + throw cnfe; + } else { + throw new ClassNotFoundException(name); + } + } + + @Override + public void close() throws IOException { + this.classLoaders = null; + super.close(); + } + + +} diff --git a/java/org/apache/catalina/tribes/io/XByteBuffer.java b/java/org/apache/catalina/tribes/io/XByteBuffer.java new file mode 100644 index 0000000..00bd3b4 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/XByteBuffer.java @@ -0,0 +1,617 @@ +/* + * 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.catalina.tribes.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * The XByteBuffer provides a dual functionality. + * One, it stores message bytes and automatically extends the byte buffer if needed.
    + * Two, it can encode and decode packages so that they can be defined and identified + * as they come in on a socket. + *
    + * THIS CLASS IS NOT THREAD SAFE
    + *
    + * Transfer package: + *
      + *
    • START_DATA- 7 bytes - FLT2002
    • + *
    • SIZE - 4 bytes - size of the data package
    • + *
    • DATA - should be as many bytes as the prev SIZE
    • + *
    • END_DATA - 7 bytes - TLF2003
    • + *
    + */ +public class XByteBuffer implements Serializable { + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(XByteBuffer.class); + protected static final StringManager sm = StringManager.getManager(XByteBuffer.class); + + /** + * This is a package header, 7 bytes (FLT2002) + */ + private static final byte[] START_DATA = {70,76,84,50,48,48,50}; + + /** + * This is the package footer, 7 bytes (TLF2003) + */ + private static final byte[] END_DATA = {84,76,70,50,48,48,51}; + + /** + * Variable to hold the data + */ + protected byte[] buf = null; + + /** + * Current length of data in the buffer + */ + protected int bufSize = 0; + + /** + * Flag for discarding invalid packages + * If this flag is set to true, and append(byte[],...) is called, + * the data added will be inspected, and if it doesn't start with + * START_DATA it will be thrown away. + * + */ + protected boolean discard = true; + + /** + * Constructs a new XByteBuffer.
    + * TODO use a pool of byte[] for performance + * @param size the initial size of the byte buffer + * @param discard Flag for discarding invalid packages + */ + public XByteBuffer(int size, boolean discard) { + buf = new byte[size]; + this.discard = discard; + } + + public XByteBuffer(byte[] data,boolean discard) { + this(data,data.length+128,discard); + } + + public XByteBuffer(byte[] data, int size,boolean discard) { + int length = Math.max(data.length,size); + buf = new byte[length]; + System.arraycopy(data,0,buf,0,data.length); + bufSize = data.length; + this.discard = discard; + } + + public int getLength() { + return bufSize; + } + + public void setLength(int size) { + if ( size > buf.length ) { + throw new ArrayIndexOutOfBoundsException(sm.getString("xByteBuffer.size.larger.buffer")); + } + bufSize = size; + } + + public void trim(int length) { + if ( (bufSize - length) < 0 ) { + throw new ArrayIndexOutOfBoundsException(sm.getString("xByteBuffer.unableTrim", + Integer.toString(bufSize), Integer.toString(length))); + } + bufSize -= length; + } + + public void reset() { + bufSize = 0; + } + + public byte[] getBytesDirect() { + return this.buf; + } + + /** + * @return the bytes in the buffer, in its exact length + */ + public byte[] getBytes() { + byte[] b = new byte[bufSize]; + System.arraycopy(buf,0,b,0,bufSize); + return b; + } + + /** + * Resets the buffer + */ + public void clear() { + bufSize = 0; + } + + /** + * Appends the data to the buffer. If the data is incorrectly formatted, ie, the data should always start with the + * header, false will be returned and the data will be discarded. + * @param b - bytes to be appended + * @param len - the number of bytes to append. + * @return true if the data was appended correctly. Returns false if the package is incorrect, ie missing header or something, or the length of data is 0 + */ + public boolean append(ByteBuffer b, int len) { + int newcount = bufSize + len; + if (newcount > buf.length) { + expand(newcount); + } + b.get(buf,bufSize,len); + + bufSize = newcount; + + if ( discard ) { + if (bufSize > START_DATA.length && (firstIndexOf(buf, 0, START_DATA) == -1)) { + bufSize = 0; + log.error(sm.getString("xByteBuffer.discarded.invalidHeader")); + return false; + } + } + return true; + + } + + public boolean append(byte i) { + int newcount = bufSize + 1; + if (newcount > buf.length) { + expand(newcount); + } + buf[bufSize] = i; + bufSize = newcount; + return true; + } + + + public boolean append(boolean i) { + int newcount = bufSize + 1; + if (newcount > buf.length) { + expand(newcount); + } + toBytes(i,buf,bufSize); + bufSize = newcount; + return true; + } + + public boolean append(long i) { + int newcount = bufSize + 8; + if (newcount > buf.length) { + expand(newcount); + } + toBytes(i,buf,bufSize); + bufSize = newcount; + return true; + } + + public boolean append(int i) { + int newcount = bufSize + 4; + if (newcount > buf.length) { + expand(newcount); + } + toBytes(i,buf,bufSize); + bufSize = newcount; + return true; + } + + public boolean append(byte[] b, int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return false; + } + + int newcount = bufSize + len; + if (newcount > buf.length) { + expand(newcount); + } + System.arraycopy(b, off, buf, bufSize, len); + bufSize = newcount; + + if ( discard ) { + if (bufSize > START_DATA.length && (firstIndexOf(buf, 0, START_DATA) == -1)) { + bufSize = 0; + log.error(sm.getString("xByteBuffer.discarded.invalidHeader")); + return false; + } + } + return true; + } + + public void expand(int newcount) { + //don't change the allocation strategy + byte newbuf[] = new byte[Math.max(buf.length << 1, newcount)]; + System.arraycopy(buf, 0, newbuf, 0, bufSize); + buf = newbuf; + } + + public int getCapacity() { + return buf.length; + } + + + /** + * Internal mechanism to make a check if a complete package exists + * within the buffer + * @return - true if a complete package (header,compress,size,data,footer) exists within the buffer + */ + public int countPackages() { + return countPackages(false); + } + + public int countPackages(boolean first) + { + int cnt = 0; + int pos = START_DATA.length; + int start = 0; + + while ( start < bufSize ) { + //first check start header + int index = firstIndexOf(buf,start,START_DATA); + //if the header (START_DATA) isn't the first thing or + //the buffer isn't even 14 bytes + if ( index != start || ((bufSize-start)<14) ) { + break; + } + //next 4 bytes are compress flag not needed for count packages + //then get the size 4 bytes + int size = toInt(buf, pos); + //now the total buffer has to be long enough to hold + //START_DATA.length+4+size+END_DATA.length + pos = start + START_DATA.length + 4 + size; + if ( (pos + END_DATA.length) > bufSize) { + break; + } + //and finally check the footer of the package END_DATA + int newpos = firstIndexOf(buf, pos, END_DATA); + //mismatch, there is no package + if (newpos != pos) { + break; + } + //increase the packet count + cnt++; + //reset the values + start = pos + END_DATA.length; + pos = start + START_DATA.length; + //we only want to verify that we have at least one package + if ( first ) { + break; + } + } + return cnt; + } + + /** + * Method to check if a package exists in this byte buffer. + * @return - true if a complete package (header,options,size,data,footer) exists within the buffer + */ + public boolean doesPackageExist() { + return (countPackages(true)>0); + } + + /** + * Extracts the message bytes from a package. + * If no package exists, a IllegalStateException will be thrown. + * @param clearFromBuffer - if true, the package will be removed from the byte buffer + * @return - returns the actual message bytes (header, compress,size and footer not included). + */ + public XByteBuffer extractDataPackage(boolean clearFromBuffer) { + int psize = countPackages(true); + if (psize == 0) { + throw new IllegalStateException(sm.getString("xByteBuffer.no.package")); + } + int size = toInt(buf, START_DATA.length); + XByteBuffer xbuf = BufferPool.getBufferPool().getBuffer(size,false); + xbuf.setLength(size); + System.arraycopy(buf, START_DATA.length + 4, xbuf.getBytesDirect(), 0, size); + if (clearFromBuffer) { + int totalsize = START_DATA.length + 4 + size + END_DATA.length; + bufSize = bufSize - totalsize; + System.arraycopy(buf, totalsize, buf, 0, bufSize); + } + return xbuf; + + } + + public ChannelData extractPackage(boolean clearFromBuffer) { + XByteBuffer xbuf = extractDataPackage(clearFromBuffer); + ChannelData cdata = ChannelData.getDataFromPackage(xbuf); + return cdata; + } + + /** + * Creates a complete data package + * @param cdata - the message data to be contained within the package + * @return - a full package (header,size,data,footer) + */ + public static byte[] createDataPackage(ChannelData cdata) { +// return createDataPackage(cdata.getDataPackage()); + //avoid one extra byte array creation + int dlength = cdata.getDataPackageLength(); + int length = getDataPackageLength(dlength); + byte[] data = new byte[length]; + int offset = 0; + System.arraycopy(START_DATA, 0, data, offset, START_DATA.length); + offset += START_DATA.length; + toBytes(dlength,data, START_DATA.length); + offset += 4; + cdata.getDataPackage(data,offset); + offset += dlength; + System.arraycopy(END_DATA, 0, data, offset, END_DATA.length); + offset += END_DATA.length; + return data; + } + + public static byte[] createDataPackage(byte[] data, int doff, int dlength, byte[] buffer, int bufoff) { + if ( (buffer.length-bufoff) > getDataPackageLength(dlength) ) { + throw new ArrayIndexOutOfBoundsException(sm.getString("xByteBuffer.unableCreate")); + } + System.arraycopy(START_DATA, 0, buffer, bufoff, START_DATA.length); + toBytes(data.length,buffer, bufoff+START_DATA.length); + System.arraycopy(data, doff, buffer, bufoff+START_DATA.length + 4, dlength); + System.arraycopy(END_DATA, 0, buffer, bufoff+START_DATA.length + 4 + data.length, END_DATA.length); + return buffer; + } + + + public static int getDataPackageLength(int datalength) { + int length = + START_DATA.length + //header length + 4 + //data length indicator + datalength + //actual data length + END_DATA.length; //footer length + return length; + + } + + public static byte[] createDataPackage(byte[] data) { + int length = getDataPackageLength(data.length); + byte[] result = new byte[length]; + return createDataPackage(data,0,data.length,result,0); + } + + +// public static void fillDataPackage(byte[] data, int doff, int dlength, XByteBuffer buf) { +// int pkglen = getDataPackageLength(dlength); +// if ( buf.getCapacity() < pkglen ) buf.expand(pkglen); +// createDataPackage(data,doff,dlength,buf.getBytesDirect(),buf.getLength()); +// } + + /** + * Convert four bytes to an int + * @param b - the byte array containing the four bytes + * @param off - the offset + * @return the integer value constructed from the four bytes + */ + public static int toInt(byte[] b,int off){ + return ( ( b[off+3]) & 0xFF) + + ( ( ( b[off+2]) & 0xFF) << 8) + + ( ( ( b[off+1]) & 0xFF) << 16) + + ( ( ( b[off+0]) & 0xFF) << 24); + } + + /** + * Convert eight bytes to a long + * @param b - the byte array containing the four bytes + * @param off - the offset + * @return the long value constructed from the eight bytes + */ + public static long toLong(byte[] b,int off){ + return ( ( (long) b[off+7]) & 0xFF) + + ( ( ( (long) b[off+6]) & 0xFF) << 8) + + ( ( ( (long) b[off+5]) & 0xFF) << 16) + + ( ( ( (long) b[off+4]) & 0xFF) << 24) + + ( ( ( (long) b[off+3]) & 0xFF) << 32) + + ( ( ( (long) b[off+2]) & 0xFF) << 40) + + ( ( ( (long) b[off+1]) & 0xFF) << 48) + + ( ( ( (long) b[off+0]) & 0xFF) << 56); + } + + + /** + * Converts a boolean and put it in a byte array. + * @param bool the integer + * @param data the byte buffer in which the boolean will be placed + * @param offset the offset in the byte array + * @return the byte array + */ + public static byte[] toBytes(boolean bool, byte[] data, int offset) { + data[offset] = (byte)(bool?1:0); + return data; + } + + /** + * Converts a byte array entry to boolean. + * @param b byte array + * @param offset within byte array + * @return true if byte array entry is non-zero, false otherwise + */ + public static boolean toBoolean(byte[] b, int offset) { + return b[offset] != 0; + } + + + /** + * Converts an integer to four bytes. + * @param n the integer + * @param b the byte buffer in which the integer will be placed + * @param offset the offset in the byte array + * @return four bytes in an array + */ + public static byte[] toBytes(int n, byte[] b, int offset) { + b[offset+3] = (byte) (n); + n >>>= 8; + b[offset+2] = (byte) (n); + n >>>= 8; + b[offset+1] = (byte) (n); + n >>>= 8; + b[offset+0] = (byte) (n); + return b; + } + + /** + * Converts a long to eight bytes. + * @param n the long + * @param b the byte buffer in which the integer will be placed + * @param offset the offset in the byte array + * @return eight bytes in an array + */ + public static byte[] toBytes(long n, byte[] b, int offset) { + b[offset+7] = (byte) (n); + n >>>= 8; + b[offset+6] = (byte) (n); + n >>>= 8; + b[offset+5] = (byte) (n); + n >>>= 8; + b[offset+4] = (byte) (n); + n >>>= 8; + b[offset+3] = (byte) (n); + n >>>= 8; + b[offset+2] = (byte) (n); + n >>>= 8; + b[offset+1] = (byte) (n); + n >>>= 8; + b[offset+0] = (byte) (n); + return b; + } + + /** + * Similar to a String.IndexOf, but uses pure bytes. + * @param src - the source bytes to be searched + * @param srcOff - offset on the source buffer + * @param find - the string to be found within src + * @return - the index of the first matching byte. -1 if the find array is not found + */ + public static int firstIndexOf(byte[] src, int srcOff, byte[] find){ + int result = -1; + if (find.length > src.length) { + return result; + } + if (find.length == 0 || src.length == 0) { + return result; + } + if (srcOff >= src.length ) { + throw new ArrayIndexOutOfBoundsException(); + } + boolean found = false; + int srclen = src.length; + int findlen = find.length; + byte first = find[0]; + int pos = srcOff; + while (!found) { + //find the first byte + while (pos < srclen){ + if (first == src[pos]) { + break; + } + pos++; + } + if (pos >= srclen) { + return -1; + } + + //we found the first character + //match the rest of the bytes - they have to match + if ( (srclen - pos) < findlen) { + return -1; + } + //assume it does exist + found = true; + for (int i = 1; ( (i < findlen) && found); i++) { + found = (find[i] == src[pos + i]); + } + if (found) { + result = pos; + } else if ( (srclen - pos) < findlen) { + return -1; //no more matches possible + } else { + pos++; + } + } + return result; + } + + + public static Serializable deserialize(byte[] data) + throws IOException, ClassNotFoundException, ClassCastException { + return deserialize(data,0,data.length); + } + + public static Serializable deserialize(byte[] data, int offset, int length) + throws IOException, ClassNotFoundException, ClassCastException { + return deserialize(data,offset,length,null); + } + + private static final AtomicInteger invokecount = new AtomicInteger(0); + + public static Serializable deserialize(byte[] data, int offset, int length, ClassLoader[] cls) + throws IOException, ClassNotFoundException, ClassCastException { + invokecount.addAndGet(1); + Object message = null; + if ( cls == null ) { + cls = new ClassLoader[0]; + } + if (data != null && length > 0) { + InputStream instream = new ByteArrayInputStream(data,offset,length); + ObjectInputStream stream = null; + stream = (cls.length>0)? new ReplicationStream(instream,cls):new ObjectInputStream(instream); + message = stream.readObject(); + instream.close(); + stream.close(); + } + if ( message == null ) { + return null; + } else if (message instanceof Serializable) { + return (Serializable) message; + } else { + throw new ClassCastException(sm.getString("xByteBuffer.wrong.class", message.getClass().getName())); + } + } + + /** + * Serializes a message into cluster data + * @param msg ClusterMessage + * @return serialized content as byte[] array + * @throws IOException Serialization error + */ + public static byte[] serialize(Serializable msg) throws IOException { + ByteArrayOutputStream outs = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(outs); + out.writeObject(msg); + out.flush(); + byte[] data = outs.toByteArray(); + return data; + } + + public void setDiscard(boolean discard) { + this.discard = discard; + } + + public boolean getDiscard() { + return discard; + } + +} diff --git a/java/org/apache/catalina/tribes/jmx/JmxRegistry.java b/java/org/apache/catalina/tribes/jmx/JmxRegistry.java new file mode 100644 index 0000000..a22904d --- /dev/null +++ b/java/org/apache/catalina/tribes/jmx/JmxRegistry.java @@ -0,0 +1,159 @@ +/* + * 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.catalina.tribes.jmx; + +import java.lang.management.ManagementFactory; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.JmxChannel; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class JmxRegistry { + + private static final Log log = LogFactory.getLog(JmxRegistry.class); + protected static final StringManager sm = StringManager.getManager(JmxRegistry.class); + private static ConcurrentHashMap registryCache = new ConcurrentHashMap<>(); + + private MBeanServer mbserver = ManagementFactory.getPlatformMBeanServer(); + private ObjectName baseOname = null; + + private JmxRegistry() { + } + + public static JmxRegistry getRegistry(Channel channel) { + if (channel == null || channel.getName() == null) { + return null; + } + JmxRegistry registry = registryCache.get(channel.getName()); + if (registry != null) { + return registry; + } + + if (!(channel instanceof JmxChannel)) { + return null; + } + JmxChannel jmxChannel = (JmxChannel) channel; + if (!jmxChannel.isJmxEnabled()) { + return null; + } + ObjectName baseOn = createBaseObjectName(jmxChannel.getJmxDomain(), + jmxChannel.getJmxPrefix(), channel.getName()); + if (baseOn == null) { + return null; + } + // create registry + registry = new JmxRegistry(); + registry.baseOname = baseOn; + // It doesn't matter if existing object gets over-written. This object + // holds minimal state and that state will be the same for all objects + // created for the same channel. + registryCache.put(channel.getName(), registry); + return registry; + } + + public static void removeRegistry(Channel channel, boolean clear) { + JmxRegistry registry = registryCache.get(channel.getName()); + if (registry == null) { + return; + } + if (clear) { + registry.clearMBeans(); + } + registryCache.remove(channel.getName()); + } + + private static ObjectName createBaseObjectName(String domain, String prefix, String name) { + if (domain == null) { + log.warn(sm.getString("jmxRegistry.no.domain")); + return null; + } + ObjectName on = null; + StringBuilder sb = new StringBuilder(domain); + sb.append(':'); + sb.append(prefix); + sb.append("type=Channel,channel="); + sb.append(name); + try { + on = new ObjectName(sb.toString()); + } catch (MalformedObjectNameException e) { + log.error(sm.getString("jmxRegistry.objectName.failed", sb.toString()), e); + } + return on; + } + + public ObjectName registerJmx(String keyprop, Object bean) { + if (mbserver == null) { + return null; + } + String oNameStr = baseOname.toString() + keyprop; + ObjectName oName = null; + try { + oName = new ObjectName(oNameStr); + if (mbserver.isRegistered(oName)) { + mbserver.unregisterMBean(oName); + } + mbserver.registerMBean(bean, oName); + } catch (NotCompliantMBeanException e) { + log.warn(sm.getString("jmxRegistry.registerJmx.notCompliant", bean), e); + return null; + } catch (MalformedObjectNameException e) { + log.error(sm.getString("jmxRegistry.objectName.failed", oNameStr), e); + return null; + } catch (Exception e) { + log.error(sm.getString("jmxRegistry.registerJmx.failed", bean, oNameStr), e); + return null; + } + return oName; + } + + public void unregisterJmx(ObjectName oname) { + if (oname ==null) { + return; + } + try { + mbserver.unregisterMBean(oname); + } catch (InstanceNotFoundException e) { + log.warn(sm.getString("jmxRegistry.unregisterJmx.notFound", oname), e); + } catch (Exception e) { + log.warn(sm.getString("jmxRegistry.unregisterJmx.failed", oname), e); + } + } + + private void clearMBeans() { + String query = baseOname.toString() + ",*"; + try { + ObjectName name = new ObjectName(query); + Set onames = mbserver.queryNames(name, null); + for (ObjectName objectName : onames) { + unregisterJmx(objectName); + } + } catch (MalformedObjectNameException e) { + log.error(sm.getString("jmxRegistry.objectName.failed", query), e); + } + } + +} diff --git a/java/org/apache/catalina/tribes/jmx/LocalStrings.properties b/java/org/apache/catalina/tribes/jmx/LocalStrings.properties new file mode 100644 index 0000000..1abab46 --- /dev/null +++ b/java/org/apache/catalina/tribes/jmx/LocalStrings.properties @@ -0,0 +1,21 @@ +# 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. + +jmxRegistry.no.domain=JMX domain is not specified +jmxRegistry.objectName.failed=The requested ObjectName [{0}] is not valid +jmxRegistry.registerJmx.failed=Failed to register object [{0}] with name [{1}] +jmxRegistry.registerJmx.notCompliant=The requested object[{0}] is not compliant with JMX specification +jmxRegistry.unregisterJmx.failed=Failed to unregister MBean with name [{0}] +jmxRegistry.unregisterJmx.notFound=The requested ObjectName [{0}] has not been registered in the MBeanServer diff --git a/java/org/apache/catalina/tribes/jmx/LocalStrings_es.properties b/java/org/apache/catalina/tribes/jmx/LocalStrings_es.properties new file mode 100644 index 0000000..2bba40d --- /dev/null +++ b/java/org/apache/catalina/tribes/jmx/LocalStrings_es.properties @@ -0,0 +1,16 @@ +# 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. + +jmxRegistry.registerJmx.failed=Fallo al registrar el objeto [{0}] con nombre [{1}]\n diff --git a/java/org/apache/catalina/tribes/jmx/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/jmx/LocalStrings_fr.properties new file mode 100644 index 0000000..58edc0e --- /dev/null +++ b/java/org/apache/catalina/tribes/jmx/LocalStrings_fr.properties @@ -0,0 +1,21 @@ +# 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. + +jmxRegistry.no.domain=Le nom de domaine JMX n'est pas spécifié +jmxRegistry.objectName.failed=L''ObjectName [{0}] demandé est invalide +jmxRegistry.registerJmx.failed=Echec de l''enregistrement de l''objet [{0}] avec le nom [{1}] +jmxRegistry.registerJmx.notCompliant=L''objet demandé [{0}] n''est pas compatible avec la spécification JMX +jmxRegistry.unregisterJmx.failed=Echec du désenregistrement du MBean nommé [{0}] +jmxRegistry.unregisterJmx.notFound=L''ObjectName [{0}] demandé n''a pas été enregistré dans le serveur JMX diff --git a/java/org/apache/catalina/tribes/jmx/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/jmx/LocalStrings_ja.properties new file mode 100644 index 0000000..8226ffd --- /dev/null +++ b/java/org/apache/catalina/tribes/jmx/LocalStrings_ja.properties @@ -0,0 +1,21 @@ +# 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. + +jmxRegistry.no.domain=JMX ドメインãŒæœªæŒ‡å®šã§ã™ã€‚ +jmxRegistry.objectName.failed=ä¸æ­£ãªã‚ªãƒ–ジェクトå [{0}] ã‚’è¦æ±‚ã•ã‚Œã¾ã—ãŸã€‚ +jmxRegistry.registerJmx.failed=オブジェクト [{0}] ã‚’åå‰ [{1}] ã§ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +jmxRegistry.registerJmx.notCompliant=è¦æ±‚ã•ã‚ŒãŸã‚ªãƒ–ジェクト [{0}] ã¯JMX仕様ã«æº–æ‹ ã—ã¦ã„ã¾ã›ã‚“ +jmxRegistry.unregisterJmx.failed=åå‰[{0}]ã®MBeanã®ç™»éŒ²ã‚’解除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +jmxRegistry.unregisterJmx.notFound=è¦æ±‚ã•ã‚ŒãŸObjectName [{0}]ã¯MBeanServerã«ç™»éŒ²ã•ã‚Œã¦ã„ã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/tribes/jmx/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/jmx/LocalStrings_ko.properties new file mode 100644 index 0000000..3ce14ce --- /dev/null +++ b/java/org/apache/catalina/tribes/jmx/LocalStrings_ko.properties @@ -0,0 +1,21 @@ +# 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. + +jmxRegistry.no.domain=JMX ë„ë©”ì¸ì´ 지정ë˜ì§€ 않았습니다. +jmxRegistry.objectName.failed=ìš”ì²­ëœ ObjectName [{0}]ì€(는) 유효하지 않습니다. +jmxRegistry.registerJmx.failed=[{1}](ì´)ë¼ëŠ” ì´ë¦„으로 ê°ì²´ [{0}]ì„(를) 등ë¡í•˜ì§€ 못했습니다. +jmxRegistry.registerJmx.notCompliant=ìš”ì²­ëœ ê°ì²´ [{0}]ì€(는) JMX 스펙과 호환ë˜ì§€ 않습니다. +jmxRegistry.unregisterJmx.failed=[{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ MBeanì— ëŒ€í•œ 등ë¡ì„ 제거하지 못했습니다. +jmxRegistry.unregisterJmx.notFound=ìš”ì²­ëœ ê°ì²´[{0}]는 MBeanServerì— ë“±ë¡ë˜ì§€ 않았습니다. diff --git a/java/org/apache/catalina/tribes/jmx/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/jmx/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..9e33f58 --- /dev/null +++ b/java/org/apache/catalina/tribes/jmx/LocalStrings_zh_CN.properties @@ -0,0 +1,21 @@ +# 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. + +jmxRegistry.no.domain=未指定JMX域 +jmxRegistry.objectName.failed=请求的ObjectName[{0}]无效 +jmxRegistry.registerJmx.failed=无法注册å称为 [{1}] 的对象 [{0}] +jmxRegistry.registerJmx.notCompliant=请求的对象[{0}]ä¸ç¬¦åˆJMX规范 +jmxRegistry.unregisterJmx.failed=无法注销å为[{0}]çš„MBean +jmxRegistry.unregisterJmx.notFound=ObjectName[{0}]还未注册到MBeanServer diff --git a/java/org/apache/catalina/tribes/membership/Constants.java b/java/org/apache/catalina/tribes/membership/Constants.java new file mode 100644 index 0000000..b0a9dda --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/Constants.java @@ -0,0 +1,34 @@ +/* + * 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.catalina.tribes.membership; + +import org.apache.catalina.tribes.util.Arrays; + +/** + * Manifest constants for the org.apache.catalina.tribes.membership + * package. + * + * @author Peter Rossbach + */ +public class Constants { + + public static final String Package = "org.apache.catalina.tribes.membership"; + public static void main(String[] args) throws Exception { + System.out.println(Arrays.toString("TRIBES-B".getBytes())); + System.out.println(Arrays.toString("TRIBES-E".getBytes())); + } +} diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings.properties b/java/org/apache/catalina/tribes/membership/LocalStrings.properties new file mode 100644 index 0000000..23fbe8d --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings.properties @@ -0,0 +1,71 @@ +# 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. + +McastService.domain=Unable to send domain update +McastService.parseSoTimeout=Unable to parse SoTimeout: [{0}] +McastService.parseTTL=Unable to parse TTL: [{0}] +McastService.payload=Unable to send payload update +McastService.stopFail=Unable to stop the mcast service, level: [{0}] + +mcastService.exceed.maxPacketSize=Packet length[{0}] exceeds max packet size of [{1}] bytes. +mcastService.missing.property=McastService:Required property [{0}] is missing. +mcastService.noStart=Multicast send is not started or enabled. + +mcastServiceImpl.bind=Attempting to bind the multicast socket to [{0}:{1}] +mcastServiceImpl.bind.failed=Binding to multicast address, failed. Binding to port only. +mcastServiceImpl.error.receiving=Error receiving mcast package. Sleeping 500ms +mcastServiceImpl.error.receivingNoSleep=Error receiving multicast package +mcastServiceImpl.invalid.startLevel=Invalid start level. Only acceptable levels are Channel.MBR_RX_SEQ and Channel.MBR_TX_SEQ +mcastServiceImpl.invalid.stopLevel=Invalid stop level. Only acceptable levels are Channel.MBR_RX_SEQ and Channel.MBR_TX_SEQ +mcastServiceImpl.invalidMemberPackage=Invalid member multicast package +mcastServiceImpl.memberAdd=Add member [{0}] +mcastServiceImpl.memberDisappeared.failed=Unable to process member disappeared message. +mcastServiceImpl.memberExpire=Expire member [{0}] +mcastServiceImpl.memberShutdown=Member [{0}] has shutdown +mcastServiceImpl.messageError=Unable to decode message +mcastServiceImpl.packet.tooLong=Multicast packet received was too long, dropping package:[{0}] +mcastServiceImpl.receive.running=McastService.receive already running. +mcastServiceImpl.recovery=Tribes membership running recovery thread. Multicasting is not functional. +mcastServiceImpl.recovery.failed=Recovery attempt number [{0}] failed. Trying again in [{1}] seconds. +mcastServiceImpl.recovery.startFailed=Recovery thread failed to start membership service. +mcastServiceImpl.recovery.stopFailed=Recovery thread failed to stop membership service. +mcastServiceImpl.recovery.successful=Membership recovery was successful. +mcastServiceImpl.send.failed=Unable to send mcast message. +mcastServiceImpl.send.running=McastService.send already running. +mcastServiceImpl.setInterface=Setting multihome multicast interface to:[{0}] +mcastServiceImpl.setSoTimeout=Setting cluster mcast soTimeout to [{0}] +mcastServiceImpl.setTTL=Setting cluster mcast TTL to [{0}] +mcastServiceImpl.unable.join=Unable to join multicast group, make sure your system has multicasting enabled. +mcastServiceImpl.unableReceive.broadcastMessage=Unable to receive broadcast message. +mcastServiceImpl.waitForMembers.done=Done sleeping, membership established, start level:[{0}] +mcastServiceImpl.waitForMembers.start=Sleeping for [{0}] milliseconds to establish cluster membership, start level:[{1}] + +memberImpl.invalid.package.begin=Invalid package, should start with:[{0}] +memberImpl.invalid.package.end=Invalid package, should end with:[{0}] +memberImpl.large.payload=Payload is too large for tribes to handle. +memberImpl.notEnough.bytes=Not enough bytes in member package. +memberImpl.package.small=Member package too small to validate. + +staticMember.invalid.uuidLength=UUID must be exactly 16 bytes, not:[{0}] + +staticMembershipProvider.heartbeat.failed=Unable to send StaticMembershipProvider.ping message. +staticMembershipProvider.leftOver.ignored=Message[{0}] is ignored. +staticMembershipProvider.pingThread.failed=Unable to send ping. +staticMembershipProvider.replyRequest.ignored=Message[{0}] is ignored. +staticMembershipProvider.startMembership.noReplies=Received 0 replies, probably a timeout. +staticMembershipProvider.stopMembership.sendFailed=Unable to send stop membership message. + +staticMembershipService.noLocalMember=There is no localMember in static member list. +staticMembershipService.stopFail=Unable to stop the static membership service, level: [{0}] diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_cs.properties new file mode 100644 index 0000000..96cb657 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_cs.properties @@ -0,0 +1,24 @@ +# 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. + +mcastServiceImpl.recovery=Tribes Älenství, běžící obnovovací vlákno, multicasting není funkÄní. +mcastServiceImpl.send.running=McastService.send již běží. +mcastServiceImpl.setSoTimeout=Nastavuji cluster mcast soTimeout na [{0}] +mcastServiceImpl.unableReceive.broadcastMessage=Nelze obdržet vysílanou zprávu. + +memberImpl.notEnough.bytes=Nedostatek bytů v balíku (package) + +staticMembershipProvider.leftOver.ignored=Zpráva [{0}] je ignorována. +staticMembershipProvider.stopMembership.sendFailed=Nelze odeslat zprávu o zastavení Älenství. diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_de.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_de.properties new file mode 100644 index 0000000..23349eb --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_de.properties @@ -0,0 +1,28 @@ +# 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. + +mcastServiceImpl.bind=Versuche Multicast Socket an [{0}:{1}] zu binden +mcastServiceImpl.recovery.failed=Wiederherstellungsversuch Nummer [{0}] ist fehlgeschlagen, versuche es in [{1}] Sekunden erneut +mcastServiceImpl.send.failed=Eine mcast-Nachricht kann nicht gesendet werden. +mcastServiceImpl.send.running=McastService.send läuft bereits +mcastServiceImpl.setSoTimeout=Setze Cluster mcast soTimeout auf [{0}] +mcastServiceImpl.setTTL=Setze Cluster mcast TTL auf [{0}] +mcastServiceImpl.unableReceive.broadcastMessage=Konnte Broadcast-Nachricht nicht empfangen. + +memberImpl.notEnough.bytes=Nicht genug bytes im Mitgliederpaket + +staticMembershipProvider.leftOver.ignored=Nachricht [{0}] wird ignoriert +staticMembershipProvider.pingThread.failed=Ping kann nicht gesendet werden. +staticMembershipProvider.stopMembership.sendFailed=Konnte Stoppe-Mitgliedschaft-Nachricht nicht senden. diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_es.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_es.properties new file mode 100644 index 0000000..26df51c --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_es.properties @@ -0,0 +1,27 @@ +# 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. + +mcastServiceImpl.bind=Tratando de atar el socket multicast a [{0}:{1}]\n +mcastServiceImpl.recovery=Membresía Tribes ejecutando hilo de recuperación, multicasting no esta operativo. +mcastServiceImpl.send.running=McastService.send aún esta corriendo. +mcastServiceImpl.setInterface=Fijando interfaz multicase multihme a:[{0}]\n +mcastServiceImpl.setSoTimeout=Fijando cluster mcast para tiempo de espera gotado en [{0}] +mcastServiceImpl.setTTL=Fijando cluster cluster mcast TTL a [{0}]\n +mcastServiceImpl.unableReceive.broadcastMessage=Incapaz de recibir el mensaje de broadcast + +memberImpl.notEnough.bytes=No hay suficientes bytes en el paquete miembro + +staticMembershipProvider.leftOver.ignored=Mensaje [{0}] ignorado.\n +staticMembershipProvider.stopMembership.sendFailed=Incapaz de enviar el mensaje de terminación de membresía diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_fr.properties new file mode 100644 index 0000000..b086a70 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_fr.properties @@ -0,0 +1,71 @@ +# 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. + +McastService.domain=Impossible d'envoyer la mise à jour du domaine +McastService.parseSoTimeout=Impossible de lire SoTimeout : [{0}] +McastService.parseTTL=Impossible d''analyser le TTL : [{0}] +McastService.payload=Impossible d'envoyer les données de mise à jour +McastService.stopFail=Impossible d''arrêter le service mcast, niveau [{0}] + +mcastService.exceed.maxPacketSize=La taille du paquet [{0}] excède la taille maximale qui est de [{1}] octets +mcastService.missing.property=McastService :La propriété obligatoire [{0}] manque. +mcastService.noStart=L'envoi multicast n'est pas démarré ou activé + +mcastServiceImpl.bind=Tentive d''associer le socket multicast à [{0} : {1}] +mcastServiceImpl.bind.failed=Echec de l'association à l’adresse multicast, association uniquement sur le port +mcastServiceImpl.error.receiving=Erreur en recevant un paquet multicast, attente de 500ms +mcastServiceImpl.error.receivingNoSleep=Erreur lors de la réception d'un paquet multicast +mcastServiceImpl.invalid.startLevel=Niveau de départ invalide. Les seuls niveaux acceptables sont Channel.MBR_RX_SEQ et Channel.MBR_TX_SEQ +mcastServiceImpl.invalid.stopLevel=Niveau de stop invalide, les seuls niveaux valides sont Channel.MBR_RX_SEQ et Channel.MBR_TX_SEQ +mcastServiceImpl.invalidMemberPackage=Membre invalide dans le paquet multicast +mcastServiceImpl.memberAdd=Ajout du membre [{0}] +mcastServiceImpl.memberDisappeared.failed=Impossible de traiter le message indiquant un membre disparu +mcastServiceImpl.memberExpire=Expiration du membre [{0}] +mcastServiceImpl.memberShutdown=Le membre [{0}] s''est arrêté +mcastServiceImpl.messageError=Impossible de décoder le message +mcastServiceImpl.packet.tooLong=Le paquet multicast reçu est trop long, il est abandonné : [{0}] +mcastServiceImpl.receive.running=McastService.receive est déjà en cours d'exécution +mcastServiceImpl.recovery=Le multicast est non fonctionnel, le registre de membres de Tribes exécute le processus de récupération +mcastServiceImpl.recovery.failed=La tentative de récupération numéro [{0}] échouée, nouvel essai dans [{1}] secondes +mcastServiceImpl.recovery.startFailed=Le thread de récupération n'a pas pu démarrer le registre de membres +mcastServiceImpl.recovery.stopFailed=Le thread de récupération n'a pu arrêter le registre de membres +mcastServiceImpl.recovery.successful=Succès de récupération du registre de membres +mcastServiceImpl.send.failed=Impossible d'envoyer le message mcast. +mcastServiceImpl.send.running=McastService.send est déjà en cours d'exécution. +mcastServiceImpl.setInterface=Définition de l''interface multicast multi réseaux comme [{0}] +mcastServiceImpl.setSoTimeout=Réglage du mcast soTimeout du cluster à [{0}] +mcastServiceImpl.setTTL=Le multicast TTL du cluster est fixé à [{0}] +mcastServiceImpl.unable.join=Incapable de rejoindre le le groupe de multidiffusion ("multicast"). Assurez-vous que la multidiffusion est activée sur votre système. +mcastServiceImpl.unableReceive.broadcastMessage=N'a pas pu recevoir de message général (broadcast) +mcastServiceImpl.waitForMembers.done=Fin de l''attente, le registre de membres est établi, démarrage de niveau : [{0}] +mcastServiceImpl.waitForMembers.start=Attente de [{0}] millisecondes pour établir le registre de membres du cluster, démarrage au niveau [{1}] + +memberImpl.invalid.package.begin=Le paquet est invalide, il devrait démarrer par [{0}] +memberImpl.invalid.package.end=le paquet est invalide, il devrait se terminer par : [{0}] +memberImpl.large.payload=Le contenu est trop gros pour être géré par Tribes +memberImpl.notEnough.bytes=Pas assez d'octets dans le paquet membre +memberImpl.package.small=Le paquet du membre est trop petit pour être validé + +staticMember.invalid.uuidLength=Un UUID doit faire exactement 16 octets et non [{0}] + +staticMembershipProvider.heartbeat.failed=Impossible d'envoyer le message StaticMembershipProvider.ping +staticMembershipProvider.leftOver.ignored=Le message [{0}] a été ignoré. +staticMembershipProvider.pingThread.failed=Impossible d'envoyer un ping +staticMembershipProvider.replyRequest.ignored=Le message [{0}] a été ignoré +staticMembershipProvider.startMembership.noReplies=Reçu zéro réponses, le délai d'attente a peut-être été dépassé +staticMembershipProvider.stopMembership.sendFailed=Impossible d'envoyer le message stop du registre de membres + +staticMembershipService.noLocalMember=Il n'y a pas de membre local dans la liste de membres statique +staticMembershipService.stopFail=Impossible d''arrêter le registre de membres statique, niveau : [{0}] diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_ja.properties new file mode 100644 index 0000000..2579a2a --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_ja.properties @@ -0,0 +1,71 @@ +# 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. + +McastService.domain=ドメイン更新ã®é€ä¿¡ãŒã§ãã¾ã›ã‚“ +McastService.parseSoTimeout=SoTimeoutを解æžã§ãã¾ã›ã‚“:[{0}] +McastService.parseTTL=TTL 値を解釈ã§ãã¾ã›ã‚“: [{0}] +McastService.payload=Payloadæ›´æ–°ã®é€ä¿¡ãŒã§ãã¾ã›ã‚“ +McastService.stopFail=マルãƒã‚­ãƒ£ã‚¹ãƒˆã‚µãƒ¼ãƒ“スをåœæ­¢ã§ãã¾ã›ã‚“。レベル:[{0}] + +mcastService.exceed.maxPacketSize=パケット長 [{0}] 㯠[{1}] ãƒã‚¤ãƒˆã®æœ€å¤§ãƒ‘ケットサイズを超ãˆã¦ã„ã¾ã™ã€‚ +mcastService.missing.property=McastService: 必須プロパティ [{0}] ãŒã‚ã‚Šã¾ã›ã‚“。 +mcastService.noStart=マルãƒã‚­ãƒ£ã‚¹ãƒˆé€ä¿¡ãŒé–‹å§‹ã¾ãŸã¯æœ‰åŠ¹ã«ãªã£ã¦ã„ã¾ã›ã‚“。 + +mcastServiceImpl.bind=マルãƒã‚­ãƒ£ã‚¹ãƒˆã‚½ã‚±ãƒƒãƒˆã® [{0}:{1}] ã¸ã®ãƒã‚¤ãƒ³ãƒ‰ã‚’試行ã—ã¾ã™ã€‚ +mcastServiceImpl.bind.failed=マルãƒã‚­ãƒ£ã‚¹ãƒˆã‚¢ãƒ‰ãƒ¬ã‚¹ã¸ã®ãƒã‚¤ãƒ³ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ãƒãƒ¼ãƒˆã®ã¿ã«ãƒã‚¤ãƒ³ãƒ‰ã—ã¾ã™ã€‚ +mcastServiceImpl.error.receiving=マルãƒã‚­ãƒ£ã‚¹ãƒˆãƒ‘ッケージをå—信中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚500msスリープã—ã¾ã™ã€‚ +mcastServiceImpl.error.receivingNoSleep=マルãƒã‚­ãƒ£ã‚¹ãƒˆ パッケージã®å—信エラー +mcastServiceImpl.invalid.startLevel=ä¸æ­£ãªé–‹å§‹ãƒ¬ãƒ™ãƒ«ã§ã™ã€‚å—ã‘付ã‘られるã®ã¯ Channel.MBR_RX_SEQ 㨠Channel.MBR_TX_SEQ ã§ã™ã€‚ +mcastServiceImpl.invalid.stopLevel=無効ãªåœæ­¢ãƒ¬ãƒ™ãƒ«ã§ã™ã€‚å—ã‘入れå¯èƒ½ãªãƒ¬ãƒ™ãƒ«ã¯Channel.MBR_RX_SEQã¨Channel.MBR_TX_SEQã®ã¿ã§ã™ã€‚ +mcastServiceImpl.invalidMemberPackage=無効ãªãƒ¡ãƒ³ãƒãƒžãƒ«ãƒã‚­ãƒ£ã‚¹ãƒˆãƒ‘ッケージ +mcastServiceImpl.memberAdd=メンム[{0}] を追加 +mcastServiceImpl.memberDisappeared.failed=メンãƒãƒ¼æ¶ˆå¤±ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’処ç†ã§ãã¾ã›ã‚“。 +mcastServiceImpl.memberExpire=Member [{0}]を期é™åˆ‡ã‚Œã«ã—ã¾ã™ +mcastServiceImpl.memberShutdown=Member [{0}] ã¯ã‚·ãƒ£ãƒƒãƒˆãƒ€ã‚¦ãƒ³ã—ã¾ã—㟠+mcastServiceImpl.messageError=メッセージをデコードã§ãã¾ã›ã‚“ +mcastServiceImpl.packet.tooLong=å—ä¿¡ã—ãŸãƒžãƒ«ãƒã‚­ãƒ£ã‚¹ãƒˆãƒ‘ケットãŒå¤§ãã™ãŽã‚‹ãŸã‚パッケージを破棄ã—ã¾ã™ã€‚å—ä¿¡ã—ãŸãƒ‘ケットサイズ㯠[{0}] ã§ã™ã€‚ +mcastServiceImpl.receive.running=McastService.receive ã¯æ—¢ã«å®Ÿè¡Œä¸­ã§ã™ã€‚ +mcastServiceImpl.recovery=リカãƒãƒªã‚¹ãƒ¬ãƒƒãƒ‰ãŒãƒ¡ãƒ³ãƒãƒ¼ã‚·ãƒƒãƒ—を復旧ã™ã‚‹ãŸã‚ã€ãƒžãƒ«ãƒã‚­ãƒ£ã‚¹ãƒˆã§ããªããªã‚Šã¾ã™ã€‚ +mcastServiceImpl.recovery.failed=リカãƒãƒªã®è©¦ã¿ [{0}] ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚[{1}] 秒後ã«å†è©¦è¡Œã—ã¾ã™ +mcastServiceImpl.recovery.startFailed=リカãƒãƒªãƒ¼ã‚¹ãƒ¬ãƒƒãƒ‰ã¯ãƒ¡ãƒ³ãƒãƒ¼ã‚·ãƒƒãƒ—サービスを開始ã§ãã¾ã›ã‚“。 +mcastServiceImpl.recovery.stopFailed=リカãƒãƒªãƒ¼ã‚¹ãƒ¬ãƒƒãƒ‰ã¯ãƒ¡ãƒ³ãƒãƒ¼ã‚·ãƒƒãƒ—サービスをåœæ­¢ã§ãã¾ã›ã‚“。 +mcastServiceImpl.recovery.successful=メンãƒãƒ¼ã‚·ãƒƒãƒ—ã®å›žå¾©ã«æˆåŠŸã—ã¾ã—ãŸã€‚ +mcastServiceImpl.send.failed=マルãƒã‚­ãƒ£ã‚¹ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 +mcastServiceImpl.send.running=McastService.sendã¯ã™ã§ã«å®Ÿè¡Œä¸­ã§ã™ã€‚ +mcastServiceImpl.setInterface=マルãƒãƒ›ãƒ¼ãƒ ãƒžãƒ«ãƒã‚­ãƒ£ã‚¹ãƒˆã‚¤ãƒ³ã‚¿ãƒ¼ãƒ•ã‚§ã‚¤ã‚¹ã‚’構æˆã—ã¾ã—ãŸ: [{0}] +mcastServiceImpl.setSoTimeout=クラスタ マルãƒã‚­ãƒ£ã‚¹ãƒˆ soTimeoutã‚’[{0}]ã«è¨­å®šã—ã¾ã™ +mcastServiceImpl.setTTL=クラスターã®ãƒžãƒ«ãƒã‚­ãƒ£ã‚¹ãƒˆ TTL ã‚’ [{0}] ã«è¨­å®šã—ã¾ã—ãŸã€‚ +mcastServiceImpl.unable.join=マルãƒã‚­ãƒ£ã‚¹ãƒˆã‚°ãƒ«ãƒ¼ãƒ—ã«å‚加ã§ãã¾ã›ã‚“ã€ã‚·ã‚¹ãƒ†ãƒ ã§ãƒžãƒ«ãƒã‚­ãƒ£ã‚¹ãƒˆãŒæœ‰åŠ¹ã«ãªã£ã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。 +mcastServiceImpl.unableReceive.broadcastMessage=ブロードキャストメッセージをå—ä¿¡ã§ãã¾ã›ã‚“。 +mcastServiceImpl.waitForMembers.done=sleep完了ã€ãƒ¡ãƒ³ãƒãƒ¼ã‚·ãƒƒãƒ—を確立ã€é–‹å§‹ãƒ¬ãƒ™ãƒ«ï¼š[{0}] +mcastServiceImpl.waitForMembers.start=クラスタメンãƒã‚·ãƒƒãƒ—を確立ã™ã‚‹ãŸã‚ã« [{0}] ミリ秒スリープã€é–‹å§‹ãƒ¬ãƒ™ãƒ«: [{1}] + +memberImpl.invalid.package.begin=パッケージãŒä¸æ­£ã§ã™ã€‚[{0}] ã‹ã‚‰é–‹å§‹ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +memberImpl.invalid.package.end=ä¸æ­£ãªãƒ‘ッケージã§ã™ã€‚[{0}] ã§çµ‚端ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +memberImpl.large.payload=ペイロードã¯tribes ãŒå‡¦ç†ã™ã‚‹ã«ã¯å¤§ãã™ãŽã¾ã™ã€‚ +memberImpl.notEnough.bytes=メンãƒãƒ¼ãƒ‘ッケージã®ãƒã‚¤ãƒˆé•·ãŒä¸è¶³ã—ã¦ã„ã¾ã™ã€‚ +memberImpl.package.small=メンãƒãƒ¼ãƒ‘ッケージãŒå°ã•ã™ãŽã¦æ¤œè¨¼ã§ãã¾ã›ã‚“。 + +staticMember.invalid.uuidLength=UUIDã¯æ­£ç¢ºã«16ãƒã‚¤ãƒˆã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。[{0}] + +staticMembershipProvider.heartbeat.failed=StaticMembershipProvider.pingメッセージをé€ä¿¡ã§ãã¾ã›ã‚“。 +staticMembershipProvider.leftOver.ignored=メッセージ [{0}] を無視ã—ã¾ã—ãŸã€‚ +staticMembershipProvider.pingThread.failed=Pingã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 +staticMembershipProvider.replyRequest.ignored=メッセージ [{0}] を無視ã—ã¾ã™ã€‚ +staticMembershipProvider.startMembership.noReplies=0回ã®è¿”ä¿¡ã‚’å—ã‘å–ã‚Šã¾ã—ãŸã€‚ãŠãらãタイムアウトã§ã™ã€‚ +staticMembershipProvider.stopMembership.sendFailed=メンãƒãƒ¼ã‚·ãƒƒãƒ—åœæ­¢ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。 + +staticMembershipService.noLocalMember=é™çš„メンãƒãƒªã‚¹ãƒˆã«ãƒ­ãƒ¼ã‚«ãƒ«ãƒ¡ãƒ³ãƒãŒå­˜åœ¨ã—ã¾ã›ã‚“。 +staticMembershipService.stopFail=Staticメンãƒãƒ¼ã‚·ãƒƒãƒ—サービスをåœæ­¢ã§ãã¾ã›ã‚“。レベル:[{0}] diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_ko.properties new file mode 100644 index 0000000..258ade1 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_ko.properties @@ -0,0 +1,65 @@ +# 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. + +McastService.domain=ë„ë©”ì¸ ë³€ê²½ 메시지를 보낼 수 없습니다. +McastService.parseSoTimeout=SoTimeoutì„ íŒŒì‹±í•  수 없습니다: [{0}] +McastService.parseTTL=TTLì„ íŒŒì‹±í•  수 없습니다: [{0}] +McastService.payload=Payload 변경 메시지를 보낼 수 없습니다. +McastService.stopFail=멀티ìºìŠ¤íŠ¸ 서비스를 중단시킬 수 없습니다. 레벨: [{0}] + +mcastService.exceed.maxPacketSize=패킷 ê¸¸ì´ [{0}]ì´(ê°€), 최대 패킷 í¬ê¸°ì¸ [{1}] ë°”ì´íŠ¸ë¥¼ 초과합니다. +mcastService.missing.property=McastService: 필수 프로í¼í‹° [{0}]ì´(ê°€) 없습니다. +mcastService.noStart=멀티ìºìŠ¤íŠ¸ ì „ì†¡ì´ ì‹œìž‘ë˜ì§€ 않았거나 사용가능 ìƒíƒœê°€ 아닙니다. + +mcastServiceImpl.bind=멀티ìºìŠ¤íŠ¸ ì†Œì¼“ì„ [{0}:{1}](으)ë¡œ ë°”ì¸ë”©í•˜ë ¤ ì‹œë„ ì¤‘ +mcastServiceImpl.bind.failed=멀티ìºìŠ¤íŠ¸ 주소로 ë°”ì¸ë”©í•˜ì§€ 못했습니다. í¬íŠ¸ë¡œë§Œ ë°”ì¸ë”©í•©ë‹ˆë‹¤. +mcastServiceImpl.error.receiving=멀티ìºìŠ¤íŠ¸ 패키지를 받는 중 오류 ë°œìƒ. 500 밀리초 ë™ì•ˆ ìž ì— ë“¤ê² ìŠµë‹ˆë‹¤. +mcastServiceImpl.invalid.startLevel=유효하지 ì•Šì€ ì‹œìž‘ 레벨. 허용ë˜ëŠ” ë ˆë²¨ë“¤ì€ Channel.MBR_RX_SEQ와 Channel.MBR_TX_SEQ ë¿ìž…니다. +mcastServiceImpl.invalid.stopLevel=유효하지 ì•Šì€ ì¤‘ì§€ 레벨. 허용 ë ˆë²¨ë“¤ì€ Channel.MBR_RX_SEQ와 Channel.MBR_TX_SEQ입니다. +mcastServiceImpl.memberDisappeared.failed=멤버 사ë¼ì§ 메시지를 처리할 수 없습니다. +mcastServiceImpl.packet.tooLong=ìˆ˜ì‹ ëœ ë©€í‹°ìºìŠ¤íŠ¸ íŒ¨í‚·ì´ ë„ˆë¬´ ê¹ë‹ˆë‹¤. 패키지를 무시합니다: [{0}] +mcastServiceImpl.receive.running=McastService.receiveê°€ ì´ë¯¸ 실행 중입니다. +mcastServiceImpl.recovery=Tribes ë©¤ë²„ì‹­ì„ ë³µêµ¬í•˜ë ¤ê³ , 별ë„ì˜ ì“°ë ˆë“œì—ì„œ 멀티ìºìŠ¤íŒ…ì„ ì‹œë„하고 있습니다. +mcastServiceImpl.recovery.failed=복구 ì‹œë„ê°€ [{0}]회 실패했습니다. [{1}] ì´ˆ í›„ì— ë‹¤ì‹œ ì‹œë„ í•˜ê² ìŠµë‹ˆë‹¤. +mcastServiceImpl.recovery.startFailed=복구 쓰레드가 멤버십 서비스를 시작하지 못했습니다. +mcastServiceImpl.recovery.stopFailed=복구 쓰레드가 멤버십 서비스를 중지시키지 못했습니다. +mcastServiceImpl.recovery.successful=멤버십 복구가 성공ì ì´ì—ˆìŠµë‹ˆë‹¤. +mcastServiceImpl.send.failed=멀티ìºìŠ¤íŠ¸ 메시지를 보낼 수 없습니다. +mcastServiceImpl.send.running=McastService.sendê°€ ì´ë¯¸ 실행 중입니다. +mcastServiceImpl.setInterface=멀티홈 멀티ìºìŠ¤íŠ¸ ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ [{0}]ì— ì„¤ì •í•©ë‹ˆë‹¤. +mcastServiceImpl.setSoTimeout=í´ëŸ¬ìŠ¤í„° 멀티ìºìŠ¤íŠ¸ì˜ soTimeoutì„ [{0}](으)ë¡œ 설정합니다. +mcastServiceImpl.setTTL=í´ëŸ¬ìŠ¤í„° 멀티ìºìŠ¤íŠ¸ TTLì„ [{0}](으)ë¡œ 설정합니다. +mcastServiceImpl.unable.join=멀티ìºìŠ¤íŠ¸ ê·¸ë£¹ì— ì°¸ê°€í•  수 없습니다. ê·€í•˜ì˜ ì‹œìŠ¤í…œì´ ë©€í‹°ìºìŠ¤íŒ…ì„ ì‚¬ìš© 가능하ë„ë¡ ì„¤ì •ë˜ì—ˆëŠ”지 확ì¸í•˜ì‹­ì‹œì˜¤. +mcastServiceImpl.unableReceive.broadcastMessage=브로드ìºìŠ¤íŠ¸ 메시지를 수신할 수 없습니다. +mcastServiceImpl.waitForMembers.done=잠들기가 ë났습니다. ë©¤ë²„ì‹­ì´ í™•ë¦½ë˜ì—ˆê³ , 시작 ë ˆë²¨ì€ [{0}]입니다. +mcastServiceImpl.waitForMembers.start=í´ëŸ¬ìŠ¤í„° ë©¤ë²„ì‹­ì„ í™•ë¦½í•˜ê¸° 위해, [{0}] 밀리초 ë™ì•ˆ ìž ì— ë“­ë‹ˆë‹¤. 시작 레벨: [{1}] + +memberImpl.invalid.package.begin=유효하지 ì•Šì€ íŒ¨í‚¤ì§€ìž…ë‹ˆë‹¤. [{0}](으)ë¡œ 시작해야 합니다. +memberImpl.invalid.package.end=유효하지 ì•Šì€ íŒ¨í‚¤ì§€. [{0}](으)ë¡œ ë나야 합니다. +memberImpl.large.payload=Payloadê°€ 너무 커서 tribeë“¤ì´ ì²˜ë¦¬í•  수 없습니다. +memberImpl.notEnough.bytes=멤버 ë°ì´í„° ë°”ì´íŠ¸ ë°°ì—´ì— ì¶©ë¶„í•œ ë°ì´í„°ê°€ 존재하지 않습니다. +memberImpl.package.small=멤버 패키지가 너무 ìž‘ì•„ì„œ, 유효한지 확ì¸í•  수 없습니다. + +staticMember.invalid.uuidLength=UUID는 정확히 16 ë°”ì´íŠ¸ì—¬ì•¼ë§Œ 합니다. [{0}]ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. + +staticMembershipProvider.heartbeat.failed=StaticMembershipProvider.ping 메시지를 보낼 수 없습니다. +staticMembershipProvider.leftOver.ignored=메시지 [{0}]ì€(는) 무시ë©ë‹ˆë‹¤. +staticMembershipProvider.pingThread.failed=Ping 메시지를 보낼 수 없습니다. +staticMembershipProvider.replyRequest.ignored=메시지 [{0}]ì€(는) 무시ë©ë‹ˆë‹¤. +staticMembershipProvider.startMembership.noReplies=ì‘ë‹µì„ í•˜ë‚˜ë„ ë°›ì§€ 못했습니다. ì•„ë§ˆë„ ì œí•œ 시간 ì´ˆê³¼ëœ ë“¯í•©ë‹ˆë‹¤. +staticMembershipProvider.stopMembership.sendFailed=멤버십 중지 메시지를 보낼 수 없습니다. + +staticMembershipService.noLocalMember=ì •ì  ë©¤ë²„ 리스트 ë‚´ì— ë¡œì»¬ 멤버가 í•˜ë‚˜ë„ ì—†ìŠµë‹ˆë‹¤. +staticMembershipService.stopFail=ì •ì  ë©¤ë²„ì‹­ 서비스를 중지시킬 수 없습니다. 레벨: [{0}] diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_pt_BR.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..1adb30e --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +mcastServiceImpl.send.running=McastService.send já está em execução. diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_ru.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_ru.properties new file mode 100644 index 0000000..1f331b8 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_ru.properties @@ -0,0 +1,18 @@ +# 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. + +mcastServiceImpl.send.running=McastService.send уже запущен + +staticMembershipProvider.leftOver.ignored=Сообщение[{0}] проигнорировано. diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/membership/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..6d3fc2e --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/LocalStrings_zh_CN.properties @@ -0,0 +1,65 @@ +# 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. + +McastService.domain=无法å‘é€åŸŸæ›´æ–° +McastService.parseSoTimeout=无法解æžSoTimeout:[{0}] +McastService.parseTTL=无法分æžTTL:[{0}] +McastService.payload=无法å‘é€è´Ÿè½½æ›´æ–° +McastService.stopFail=无法åœæ­¢mcastæœåŠ¡ï¼Œçº§åˆ«ï¼š[{0}] + +mcastService.exceed.maxPacketSize=æ•°æ®åŒ…长度[{0}]超过了最大数æ®åŒ…大å°[{1}]字节。 +mcastService.missing.property=McastService:缺少必需属性 [{0}]。 +mcastService.noStart=多路广播å‘é€æœªå¯åŠ¨æˆ–未å¯ç”¨ + +mcastServiceImpl.bind=å°è¯•å°†å¤šæ’­å¥—接字绑定到 [{0}:{1}] +mcastServiceImpl.bind.failed=绑定到多播地å€å¤±è´¥ã€‚仅绑定到端å£ã€‚ +mcastServiceImpl.error.receiving=接收mcast包时出错。ç¡çœ 500毫秒 +mcastServiceImpl.invalid.startLevel=无效的å¯åŠ¨çº§åˆ«ã€‚åªæŽ¥å—以下级别:Channel.MBR_RX_SEQ或 Channel.MBR_TX_SEQ +mcastServiceImpl.invalid.stopLevel=无效的åœæ­¢çº§åˆ«ã€‚åªæœ‰Channel.MBR_RX_SEQå’ŒChannel.MBR_TX_SEQ是å¯æŽ¥å—的级别 +mcastServiceImpl.memberDisappeared.failed=无法处ç†æˆå‘˜å·²æ¶ˆå¤±çš„消æ¯ã€‚ +mcastServiceImpl.packet.tooLong=收到的多播数æ®åŒ…太长,正在删除包:[{0}] +mcastServiceImpl.receive.running=McastService.receiveå·²ç»è¿è¡Œã€‚ +mcastServiceImpl.recovery=家æ—æˆå‘˜ï¼Œè¿è¡Œæ¢å¤çº¿ç¨‹ï¼Œå¹¿æ’­ä¸æ˜¯åŠŸèƒ½ã€‚ +mcastServiceImpl.recovery.failed=æ¢å¤å°è¯•æ¬¡æ•°[{0}]失败,请在[{1}]秒内é‡è¯• +mcastServiceImpl.recovery.startFailed=æ¢å¤çº¿ç¨‹æ— æ³•å¯åŠ¨æˆå‘˜èµ„æ ¼æœåŠ¡ã€‚ +mcastServiceImpl.recovery.stopFailed=æ¢å¤çº¿ç¨‹æœªèƒ½åœæ­¢æˆå‘˜æœåŠ¡ã€‚ +mcastServiceImpl.recovery.successful=æˆå‘˜èº«ä»½æ¢å¤æˆåŠŸã€‚ +mcastServiceImpl.send.failed=无法å‘é€å¤šæ’­ä¿¡æ¯ +mcastServiceImpl.send.running=McastService.sendå·²ç»è¿è¡Œ +mcastServiceImpl.setInterface=设置多宿主多播接å£ä¸ºï¼š[{0}] +mcastServiceImpl.setSoTimeout=设置集群多播超时时间:[{0}] +mcastServiceImpl.setTTL=设置集群多播TTL:[{0}] +mcastServiceImpl.unable.join=无法加入多播组,请确ä¿ä½ çš„系统已å¯ç”¨å¤šæ’­ã€‚ +mcastServiceImpl.unableReceive.broadcastMessage=无法接收广播消æ¯ã€‚ +mcastServiceImpl.waitForMembers.done=休眠完毕,æˆå‘˜å·²è¿žæŽ¥ï¼Œå¯åŠ¨ç­‰çº§ï¼š[{0}] +mcastServiceImpl.waitForMembers.start=休眠[{0}]毫秒åŽå¯åŠ¨è¿žæŽ¥é›†ç¾¤ï¼Œå¯åŠ¨ç™»è®°ï¼š[{1}] + +memberImpl.invalid.package.begin=无效的包,应以“[{0}]â€å¼€å¤´ã€‚ +memberImpl.invalid.package.end=无效的包,应以“[{0}]â€ç»“å°¾ +memberImpl.large.payload=è´Ÿè½½å¤ªå¤§ä»¥è‡³äºŽéš¾ä»¥å¤„ç† +memberImpl.notEnough.bytes=æˆå‘˜åŒ…中的字节ä¸å¤Ÿã€‚ +memberImpl.package.small=æˆå‘˜åŒ…太å°ä»¥è‡³äºŽä¸èƒ½æ ¡éªŒã€‚ + +staticMember.invalid.uuidLength=UUID必须正好是16个字节,而ä¸æ˜¯ï¼š[{0}] + +staticMembershipProvider.heartbeat.failed=无法å‘é€StaticMembershipProvider.ping消æ¯ã€‚ +staticMembershipProvider.leftOver.ignored=æ¶ˆæ¯ [{0}] 被忽略。 +staticMembershipProvider.pingThread.failed=无法å‘é€ping。 +staticMembershipProvider.replyRequest.ignored=忽略消æ¯[{0}]。 +staticMembershipProvider.startMembership.noReplies=0å“应,å¯èƒ½è¶…æ—¶ +staticMembershipProvider.stopMembership.sendFailed=无法å‘é€æˆå‘˜æ¶ˆæ¯ + +staticMembershipService.noLocalMember=é™æ€æˆå‘˜åˆ—表中没有本地æˆå‘˜ã€‚ +staticMembershipService.stopFail=无法åœæ­¢é™æ€æˆå‘˜èµ„æ ¼æœåŠ¡ï¼Œçº§åˆ«ï¼š[{0}] diff --git a/java/org/apache/catalina/tribes/membership/McastService.java b/java/org/apache/catalina/tribes/membership/McastService.java new file mode 100644 index 0000000..4c2c119 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/McastService.java @@ -0,0 +1,550 @@ +/* + * 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.catalina.tribes.membership; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.util.Properties; + +import javax.management.ObjectName; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipProvider; +import org.apache.catalina.tribes.MessageListener; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.jmx.JmxRegistry; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.catalina.tribes.util.UUIDGenerator; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A membership implementation using simple multicast. + * This is the representation of a multicast membership service. + * This class is responsible for maintaining a list of active cluster nodes in the cluster. + * If a node fails to send out a heartbeat, the node will be dismissed. + */ +public class McastService + extends MembershipServiceBase implements MessageListener, McastServiceMBean { + + private static final Log log = LogFactory.getLog(McastService.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + /** + * A handle to the actual low level implementation + */ + protected McastServiceImpl impl; + + /** + * A message listener delegate for broadcasts + */ + protected MessageListener msglistener; + /** + * The local member + */ + protected MemberImpl localMember ; + private int mcastSoTimeout; + private int mcastTTL; + + protected byte[] payload; + + protected byte[] domain; + + /** + * the ObjectName of this McastService. + */ + private ObjectName oname = null; + + /** + * Create a membership service. + */ + public McastService() { + //default values + setDefaults(this.properties); + } + + /** + * Sets the properties for the membership service. + * @param properties + *
    All are required
    + * 1. mcastPort - the port to listen to
    + * 2. mcastAddress - the mcast group address
    + * 4. bindAddress - the bind address if any - only one that can be null
    + * 5. memberDropTime - the time a member is gone before it is considered gone.
    + * 6. mcastFrequency - the frequency of sending messages
    + * 7. tcpListenPort - the port this member listens to
    + * 8. tcpListenHost - the bind address of this member
    + * @exception java.lang.IllegalArgumentException if a property is missing. + */ + @Override + public void setProperties(Properties properties) { + hasProperty(properties,"mcastPort"); + hasProperty(properties,"mcastAddress"); + hasProperty(properties,"memberDropTime"); + hasProperty(properties,"mcastFrequency"); + hasProperty(properties,"tcpListenPort"); + hasProperty(properties,"tcpListenHost"); + setDefaults(properties); + this.properties = properties; + } + + /** + * @return the local member name + */ + @Override + public String getLocalMemberName() { + return localMember.toString() ; + } + + @Override + public Member getLocalMember(boolean alive) { + if ( alive && localMember != null && impl != null) { + localMember.setMemberAliveTime(System.currentTimeMillis()-impl.getServiceStartTime()); + } + return localMember; + } + + @Override + public void setLocalMemberProperties(String listenHost, int listenPort, int securePort, int udpPort) { + properties.setProperty("tcpListenHost",listenHost); + properties.setProperty("tcpListenPort",String.valueOf(listenPort)); + properties.setProperty("udpListenPort",String.valueOf(udpPort)); + properties.setProperty("tcpSecurePort",String.valueOf(securePort)); + try { + if (localMember != null) { + localMember.setHostname(listenHost); + localMember.setPort(listenPort); + } else { + localMember = new MemberImpl(listenHost, listenPort, 0); + localMember.setUniqueId(UUIDGenerator.randomUUID(true)); + localMember.setPayload(getPayload()); + localMember.setDomain(getDomain()); + localMember.setLocal(true); + } + localMember.setSecurePort(securePort); + localMember.setUdpPort(udpPort); + localMember.getData(true, true); + }catch ( IOException x ) { + throw new IllegalArgumentException(x); + } + } + + public void setAddress(String addr) { + properties.setProperty("mcastAddress", addr); + } + + @Override + public String getAddress() { + return properties.getProperty("mcastAddress"); + } + + public void setMcastBindAddress(String bindaddr) { + setBind(bindaddr); + } + + public void setBind(String bindaddr) { + properties.setProperty("mcastBindAddress", bindaddr); + } + + @Override + public String getBind() { + return properties.getProperty("mcastBindAddress"); + } + + public void setPort(int port) { + properties.setProperty("mcastPort", String.valueOf(port)); + } + + public void setRecoveryCounter(int recoveryCounter) { + properties.setProperty("recoveryCounter", String.valueOf(recoveryCounter)); + } + + @Override + public int getRecoveryCounter(){ + String p = properties.getProperty("recoveryCounter"); + if(p != null){ + return Integer.parseInt(p); + } + return -1; + } + + public void setRecoveryEnabled(boolean recoveryEnabled) { + properties.setProperty("recoveryEnabled", String.valueOf(recoveryEnabled)); + } + + @Override + public boolean getRecoveryEnabled() { + String p = properties.getProperty("recoveryEnabled"); + if(p != null){ + return Boolean.parseBoolean(p); + } + return false; + } + + public void setRecoverySleepTime(long recoverySleepTime) { + properties.setProperty("recoverySleepTime", String.valueOf(recoverySleepTime)); + } + + @Override + public long getRecoverySleepTime(){ + String p = properties.getProperty("recoverySleepTime"); + if(p != null){ + return Long.parseLong(p); + } + return -1; + } + + public void setLocalLoopbackDisabled(boolean localLoopbackDisabled) { + properties.setProperty("localLoopbackDisabled",String.valueOf(localLoopbackDisabled)); + } + + @Override + public boolean getLocalLoopbackDisabled() { + String p = properties.getProperty("localLoopbackDisabled"); + if(p != null){ + return Boolean.parseBoolean(p); + } + return false; + } + + @Override + public int getPort() { + String p = properties.getProperty("mcastPort"); + return Integer.parseInt(p); + } + + public void setFrequency(long time) { + properties.setProperty("mcastFrequency", String.valueOf(time)); + } + + @Override + public long getFrequency() { + String p = properties.getProperty("mcastFrequency"); + return Long.parseLong(p); + } + + public void setMcastDropTime(long time) { + setDropTime(time); + } + public void setDropTime(long time) { + properties.setProperty("memberDropTime", String.valueOf(time)); + } + + @Override + public long getDropTime() { + String p = properties.getProperty("memberDropTime"); + return Long.parseLong(p); + } + + /** + * Check if a required property is available. + * @param properties The set of properties + * @param name The property to check for + */ + protected void hasProperty(Properties properties, String name){ + if ( properties.getProperty(name)==null) { + throw new IllegalArgumentException(sm.getString("mcastService.missing.property", name)); + } + } + + @Override + public void start(int level) throws Exception { + hasProperty(properties,"mcastPort"); + hasProperty(properties,"mcastAddress"); + hasProperty(properties,"memberDropTime"); + hasProperty(properties,"mcastFrequency"); + hasProperty(properties,"tcpListenPort"); + hasProperty(properties,"tcpListenHost"); + hasProperty(properties,"tcpSecurePort"); + hasProperty(properties,"udpListenPort"); + + + if ( impl != null ) { + impl.start(level); + return; + } + String host = getProperties().getProperty("tcpListenHost"); + int port = Integer.parseInt(getProperties().getProperty("tcpListenPort")); + int securePort = Integer.parseInt(getProperties().getProperty("tcpSecurePort")); + int udpPort = Integer.parseInt(getProperties().getProperty("udpListenPort")); + + if ( localMember == null ) { + localMember = new MemberImpl(host, port, 100); + localMember.setUniqueId(UUIDGenerator.randomUUID(true)); + localMember.setLocal(true); + } else { + localMember.setHostname(host); + localMember.setPort(port); + localMember.setMemberAliveTime(100); + } + localMember.setSecurePort(securePort); + localMember.setUdpPort(udpPort); + if ( this.payload != null ) { + localMember.setPayload(payload); + } + if ( this.domain != null ) { + localMember.setDomain(domain); + } + localMember.setServiceStartTime(System.currentTimeMillis()); + java.net.InetAddress bind = null; + if ( properties.getProperty("mcastBindAddress")!= null ) { + bind = java.net.InetAddress.getByName(properties.getProperty("mcastBindAddress")); + } + int ttl = -1; + int soTimeout = -1; + if ( properties.getProperty("mcastTTL") != null ) { + try { + ttl = Integer.parseInt(properties.getProperty("mcastTTL")); + } catch ( Exception x ) { + log.error(sm.getString("McastService.parseTTL", + properties.getProperty("mcastTTL")), x); + } + } + if ( properties.getProperty("mcastSoTimeout") != null ) { + try { + soTimeout = Integer.parseInt(properties.getProperty("mcastSoTimeout")); + } catch ( Exception x ) { + log.error(sm.getString("McastService.parseSoTimeout", + properties.getProperty("mcastSoTimeout")), x); + } + } + + impl = new McastServiceImpl(localMember,Long.parseLong(properties.getProperty("mcastFrequency")), + Long.parseLong(properties.getProperty("memberDropTime")), + Integer.parseInt(properties.getProperty("mcastPort")), + bind, + java.net.InetAddress.getByName(properties.getProperty("mcastAddress")), + ttl, + soTimeout, + this, + this, + Boolean.parseBoolean(properties.getProperty("localLoopbackDisabled"))); + impl.setMembershipService(this); + String value = properties.getProperty("recoveryEnabled"); + boolean recEnabled = Boolean.parseBoolean(value); + impl.setRecoveryEnabled(recEnabled); + int recCnt = Integer.parseInt(properties.getProperty("recoveryCounter")); + impl.setRecoveryCounter(recCnt); + long recSlpTime = Long.parseLong(properties.getProperty("recoverySleepTime")); + impl.setRecoverySleepTime(recSlpTime); + impl.setChannel(channel); + + impl.start(level); + // register jmx + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + this.oname = jmxRegistry.registerJmx(",component=Membership", this); + } + + } + + + /** + * Stop broadcasting and listening to membership pings + */ + @Override + public void stop(int svc) { + try { + if ( impl != null && impl.stop(svc) ) { + if (oname != null) { + JmxRegistry.getRegistry(channel).unregisterJmx(oname); + oname = null; + } + impl.setChannel(null); + impl = null; + channel = null; + } + } catch ( Exception x) { + log.error(sm.getString( + "McastService.stopFail", Integer.valueOf(svc)), x); + } + } + + public void setMessageListener(MessageListener listener) { + this.msglistener = listener; + } + + public void removeMessageListener() { + this.msglistener = null; + } + + @Override + public void messageReceived(ChannelMessage msg) { + if (msglistener!=null && msglistener.accept(msg)) { + msglistener.messageReceived(msg); + } + } + + @Override + public boolean accept(ChannelMessage msg) { + return true; + } + @Override + public void broadcast(ChannelMessage message) throws ChannelException { + if (impl==null || (impl.startLevel & Channel.MBR_TX_SEQ)!=Channel.MBR_TX_SEQ ) { + throw new ChannelException(sm.getString("mcastService.noStart")); + } + + byte[] data = XByteBuffer.createDataPackage((ChannelData)message); + if (data.length>McastServiceImpl.MAX_PACKET_SIZE) { + throw new ChannelException(sm.getString("mcastService.exceed.maxPacketSize", + Integer.toString(data.length) , + Integer.toString(McastServiceImpl.MAX_PACKET_SIZE))); + } + DatagramPacket packet = new DatagramPacket(data,0,data.length); + try { + impl.send(false, packet); + } catch (Exception x) { + throw new ChannelException(x); + } + } + + @Override + public int getSoTimeout() { + return mcastSoTimeout; + } + + public void setSoTimeout(int mcastSoTimeout) { + this.mcastSoTimeout = mcastSoTimeout; + properties.setProperty("mcastSoTimeout", String.valueOf(mcastSoTimeout)); + } + + @Override + public int getTtl() { + return mcastTTL; + } + + public byte[] getPayload() { + return payload; + } + + @Override + public byte[] getDomain() { + return domain; + } + + public void setTtl(int mcastTTL) { + this.mcastTTL = mcastTTL; + properties.setProperty("mcastTTL", String.valueOf(mcastTTL)); + } + + @Override + public void setPayload(byte[] payload) { + this.payload = payload; + if ( localMember != null ) { + localMember.setPayload(payload); + try { + if (impl != null) { + impl.send(false); + } + }catch ( Exception x ) { + log.error(sm.getString("McastService.payload"), x); + } + } + } + + @Override + public void setDomain(byte[] domain) { + this.domain = domain; + if ( localMember != null ) { + localMember.setDomain(domain); + try { + if (impl != null) { + impl.send(false); + } + }catch ( Exception x ) { + log.error(sm.getString("McastService.domain"), x); + } + } + } + + public void setDomain(String domain) { + if ( domain == null ) { + return; + } + if ( domain.startsWith("{") ) { + setDomain(Arrays.fromString(domain)); + } else { + setDomain(Arrays.convert(domain)); + } + } + + @Override + public MembershipProvider getMembershipProvider() { + return impl; + } + + protected void setDefaults(Properties properties) { + // default values + if (properties.getProperty("mcastPort") == null) { + properties.setProperty("mcastPort","45564"); + } + if (properties.getProperty("mcastAddress") == null) { + properties.setProperty("mcastAddress","228.0.0.4"); + } + if (properties.getProperty("memberDropTime") == null) { + properties.setProperty("memberDropTime","3000"); + } + if (properties.getProperty("mcastFrequency") == null) { + properties.setProperty("mcastFrequency","500"); + } + if (properties.getProperty("recoveryCounter") == null) { + properties.setProperty("recoveryCounter", "10"); + } + if (properties.getProperty("recoveryEnabled") == null) { + properties.setProperty("recoveryEnabled", "true"); + } + if (properties.getProperty("recoverySleepTime") == null) { + properties.setProperty("recoverySleepTime", "5000"); + } + if (properties.getProperty("localLoopbackDisabled") == null) { + properties.setProperty("localLoopbackDisabled", "false"); + } + } + + /** + * Simple test program + * @param args Command-line arguments + * @throws Exception If an error occurs + */ + public static void main(String args[]) throws Exception { + McastService service = new McastService(); + Properties p = new Properties(); + p.setProperty("mcastPort","5555"); + p.setProperty("mcastAddress","224.10.10.10"); + p.setProperty("mcastClusterDomain","catalina"); + p.setProperty("bindAddress","localhost"); + p.setProperty("memberDropTime","3000"); + p.setProperty("mcastFrequency","500"); + p.setProperty("tcpListenPort","4000"); + p.setProperty("tcpListenHost","127.0.0.1"); + p.setProperty("tcpSecurePort","4100"); + p.setProperty("udpListenPort","4200"); + service.setProperties(p); + service.start(); + Thread.sleep(60*1000*60); + } +} diff --git a/java/org/apache/catalina/tribes/membership/McastServiceImpl.java b/java/org/apache/catalina/tribes/membership/McastServiceImpl.java new file mode 100644 index 0000000..b2752a5 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/McastServiceImpl.java @@ -0,0 +1,746 @@ +/* + * 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.catalina.tribes.membership; + + +import java.io.IOException; +import java.net.BindException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.net.SocketTimeoutException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.MessageListener; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.util.JreCompat; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A membership implementation using simple multicast. + * This is the representation of a multicast membership service. + * This class is responsible for maintaining a list of active cluster nodes in the cluster. + * If a node fails to send out a heartbeat, the node will be dismissed. + * This is the low level implementation that handles the multicasting sockets. + * Need to fix this, could use java.nio and only need one thread to send and receive, or + * just use a timeout on the receive + */ +public class McastServiceImpl extends MembershipProviderBase { + + private static final Log log = LogFactory.getLog(McastService.class); + + protected static final int MAX_PACKET_SIZE = 65535; + + protected static final StringManager sm = StringManager.getManager(Constants.Package); + /** + * Internal flag used for the listen thread that listens to the multicasting socket. + */ + protected volatile boolean doRunSender = false; + protected volatile boolean doRunReceiver = false; + protected volatile int startLevel = 0; + /** + * Socket that we intend to listen to + */ + protected MulticastSocket socket; + /** + * The local member that we intend to broad cast over and over again + */ + protected final MemberImpl member; + /** + * The multicast address + */ + protected final InetAddress address; + /** + * The multicast port + */ + protected final int port; + /** + * The time it takes for a member to expire. + */ + protected final long timeToExpiration; + /** + * How often to we send out a broadcast saying we are alive, must be smaller than timeToExpiration + */ + protected final long sendFrequency; + /** + * Reuse the sendPacket, no need to create a new one every time + */ + protected DatagramPacket sendPacket; + /** + * Reuse the receivePacket, no need to create a new one every time + */ + protected DatagramPacket receivePacket; + + /** + * The actual listener, for callback when stuff goes down + */ + protected final MembershipListener service; + /** + * The actual listener for broadcast callbacks + */ + protected final MessageListener msgservice; + /** + * Thread to listen for pings + */ + protected ReceiverThread receiver; + /** + * Thread to send pings + */ + protected SenderThread sender; + + /** + * Time to live for the multicast packets that are being sent out + */ + protected final int mcastTTL; + /** + * Read timeout on the mcast socket + */ + protected int mcastSoTimeout = -1; + /** + * bind address + */ + protected final InetAddress mcastBindAddress; + + /** + * nr of times the system has to fail before a recovery is initiated + */ + protected int recoveryCounter = 10; + + /** + * The time the recovery thread sleeps between recovery attempts + */ + protected long recoverySleepTime = 5000; + + /** + * Add the ability to turn on/off recovery + */ + protected boolean recoveryEnabled = true; + + /** + * disable/enable local loopback message + */ + protected final boolean localLoopbackDisabled; + + private Channel channel; + + /** + * Create a new mcast service instance. + * @param member - the local member + * @param sendFrequency - the time (ms) in between pings sent out + * @param expireTime - the time (ms) for a member to expire + * @param port - the mcast port + * @param bind - the bind address (not sure this is used yet) + * @param mcastAddress - the mcast address + * @param ttl multicast ttl that will be set on the socket + * @param soTimeout Socket timeout + * @param service - the callback service + * @param msgservice Message listener + * @param localLoopbackDisabled - disable loopbackMode + * @throws IOException Init error + */ + public McastServiceImpl( + MemberImpl member, + long sendFrequency, + long expireTime, + int port, + InetAddress bind, + InetAddress mcastAddress, + int ttl, + int soTimeout, + MembershipListener service, + MessageListener msgservice, + boolean localLoopbackDisabled) + throws IOException { + this.member = member; + this.address = mcastAddress; + this.port = port; + this.mcastSoTimeout = soTimeout; + this.mcastTTL = ttl; + this.mcastBindAddress = bind; + this.timeToExpiration = expireTime; + this.service = service; + this.msgservice = msgservice; + this.sendFrequency = sendFrequency; + this.localLoopbackDisabled = localLoopbackDisabled; + init(); + } + + public void init() throws IOException { + setupSocket(); + sendPacket = new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE); + sendPacket.setAddress(address); + sendPacket.setPort(port); + receivePacket = new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE); + receivePacket.setAddress(address); + receivePacket.setPort(port); + member.setCommand(new byte[0]); + if ( membership == null ) { + membership = new Membership(member); + } + } + + protected void setupSocket() throws IOException { + if (mcastBindAddress != null) { + try { + log.info(sm.getString("mcastServiceImpl.bind", address, Integer.toString(port))); + socket = new MulticastSocket(new InetSocketAddress(address,port)); + } catch (BindException e) { + /* + * On some platforms (e.g. Linux) it is not possible to bind + * to the multicast address. In this case only bind to the + * port. + */ + log.info(sm.getString("mcastServiceImpl.bind.failed")); + socket = new MulticastSocket(port); + } + } else { + socket = new MulticastSocket(port); + } + // Hint if we want disable loop back(local machine) messages + JreCompat.getInstance().setSocketoptionIpMulticastLoop(socket, !localLoopbackDisabled); + if (mcastBindAddress != null) { + if(log.isInfoEnabled()) { + log.info(sm.getString("mcastServiceImpl.setInterface", mcastBindAddress)); + } + NetworkInterface networkInterface = NetworkInterface.getByInetAddress(mcastBindAddress); + socket.setNetworkInterface(networkInterface); + } //end if + //force a so timeout so that we don't block forever + if (mcastSoTimeout <= 0) { + mcastSoTimeout = (int)sendFrequency; + } + if (log.isInfoEnabled()) { + log.info(sm.getString("mcastServiceImpl.setSoTimeout", + Integer.toString(mcastSoTimeout))); + } + socket.setSoTimeout(mcastSoTimeout); + + if ( mcastTTL >= 0 ) { + if(log.isInfoEnabled()) { + log.info(sm.getString("mcastServiceImpl.setTTL", Integer.toString(mcastTTL))); + } + socket.setTimeToLive(mcastTTL); + } + } + + + /** + * Start the service + * @param level 1 starts the receiver, level 2 starts the sender + * @throws IOException if the service fails to start + * @throws IllegalStateException if the service is already started + */ + @Override + public synchronized void start(int level) throws IOException { + boolean valid = false; + if ( (level & Channel.MBR_RX_SEQ)==Channel.MBR_RX_SEQ ) { + if ( receiver != null ) { + throw new IllegalStateException(sm.getString("mcastServiceImpl.receive.running")); + } + try { + if ( sender == null ) { + socket.joinGroup(new InetSocketAddress(address, 0), null); + } + }catch (IOException iox) { + log.error(sm.getString("mcastServiceImpl.unable.join")); + throw iox; + } + doRunReceiver = true; + receiver = new ReceiverThread(); + receiver.setDaemon(true); + receiver.start(); + valid = true; + } + if ( (level & Channel.MBR_TX_SEQ)==Channel.MBR_TX_SEQ ) { + if ( sender != null ) { + throw new IllegalStateException(sm.getString("mcastServiceImpl.send.running")); + } + if ( receiver == null ) { + socket.joinGroup(new InetSocketAddress(address, 0), null); + } + //make sure at least one packet gets out there + send(false); + doRunSender = true; + sender = new SenderThread(sendFrequency); + sender.setDaemon(true); + sender.start(); + //we have started the receiver, but not yet waited for membership to establish + valid = true; + } + if (!valid) { + throw new IllegalArgumentException(sm.getString("mcastServiceImpl.invalid.startLevel")); + } + //pause, once or twice + waitForMembers(level); + startLevel = (startLevel | level); + } + + private void waitForMembers(int level) { + long memberwait = sendFrequency*2; + if(log.isInfoEnabled()) { + log.info(sm.getString("mcastServiceImpl.waitForMembers.start", + Long.toString(memberwait), Integer.toString(level))); + } + try {Thread.sleep(memberwait);}catch (InterruptedException ignore){} + if(log.isInfoEnabled()) { + log.info(sm.getString("mcastServiceImpl.waitForMembers.done", Integer.toString(level))); + } + } + + /** + * Stops the service. + * @param level Stop status + * @return true if the stop is complete + * @throws IOException if the service fails to disconnect from the sockets + */ + @Override + public synchronized boolean stop(int level) throws IOException { + boolean valid = false; + + if ( (level & Channel.MBR_RX_SEQ)==Channel.MBR_RX_SEQ ) { + valid = true; + doRunReceiver = false; + if ( receiver !=null ) { + receiver.interrupt(); + } + receiver = null; + } + if ( (level & Channel.MBR_TX_SEQ)==Channel.MBR_TX_SEQ ) { + valid = true; + doRunSender = false; + if ( sender != null ) { + sender.interrupt(); + } + sender = null; + } + + if (!valid) { + throw new IllegalArgumentException(sm.getString("mcastServiceImpl.invalid.stopLevel")); + } + startLevel = (startLevel & (~level)); + //we're shutting down, send a shutdown message and close the socket + if ( startLevel == 0 ) { + //send a stop message + member.setCommand(Member.SHUTDOWN_PAYLOAD); + send(false); + //leave mcast group + try { + socket.leaveGroup(new InetSocketAddress(address, 0), null); + } catch ( Exception ignore) { + // NO-OP + } + try { + socket.close(); + } catch (Exception ignore) { + // NO-OP + } + member.setServiceStartTime(-1); + } + return (startLevel == 0); + } + + /** + * Receive a datagram packet, locking wait + * @throws IOException Received failed + */ + public void receive() throws IOException { + boolean checkexpired = true; + try { + + socket.receive(receivePacket); + if(receivePacket.getLength() > MAX_PACKET_SIZE) { + log.error(sm.getString("mcastServiceImpl.packet.tooLong", + Integer.toString(receivePacket.getLength()))); + } else { + byte[] data = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), receivePacket.getOffset(), data, 0, data.length); + if (XByteBuffer.firstIndexOf(data,0,MemberImpl.TRIBES_MBR_BEGIN)==0) { + memberDataReceived(data); + } else { + memberBroadcastsReceived(data); + } + + } + } catch (SocketTimeoutException x ) { + //do nothing, this is normal, we don't want to block forever + //since the receive thread is the same thread + //that does membership expiration + } + if (checkexpired) { + checkExpired(); + } + } + + private void memberDataReceived(byte[] data) { + final Member m = MemberImpl.getMember(data); + if (log.isTraceEnabled()) { + log.trace("Mcast receive ping from member " + m); + } + Runnable t = null; + Thread currentThread = Thread.currentThread(); + if (Arrays.equals(m.getCommand(), Member.SHUTDOWN_PAYLOAD)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("mcastServiceImpl.memberShutdown", m)); + } + membership.removeMember(m); + t = () -> { + String name = currentThread.getName(); + try { + currentThread.setName("Membership-MemberDisappeared"); + service.memberDisappeared(m); + } finally { + currentThread.setName(name); + } + }; + } else if (membership.memberAlive(m)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("mcastServiceImpl.memberAdd", m)); + } + t = () -> { + String name = currentThread.getName(); + try { + currentThread.setName("Membership-MemberAdded"); + service.memberAdded(m); + } finally { + currentThread.setName(name); + } + }; + } + if (t != null) { + executor.execute(t); + } + } + + private void memberBroadcastsReceived(final byte[] b) { + if (log.isTraceEnabled()) { + log.trace("Mcast received broadcasts."); + } + XByteBuffer buffer = new XByteBuffer(b,true); + if (buffer.countPackages(true)>0) { + int count = buffer.countPackages(); + final ChannelData[] data = new ChannelData[count]; + for (int i=0; i { + Thread currentThread = Thread.currentThread(); + String name = currentThread.getName(); + try { + currentThread.setName("Membership-MemberAdded"); + for (ChannelData datum : data) { + try { + if (datum != null && !member.equals(datum.getAddress())) { + msgservice.messageReceived(datum); + } + } catch (Throwable t1) { + if (t1 instanceof ThreadDeath) { + throw (ThreadDeath) t1; + } + if (t1 instanceof VirtualMachineError) { + throw (VirtualMachineError) t1; + } + log.error(sm.getString("mcastServiceImpl.unableReceive.broadcastMessage"), t1); + } + } + } finally { + currentThread.setName(name); + } + }; + executor.execute(t); + } + } + + protected final Object expiredMutex = new Object(); + protected void checkExpired() { + synchronized (expiredMutex) { + Member[] expired = membership.expire(timeToExpiration); + for (final Member member : expired) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("mcastServiceImpl.memberExpire", member)); + } + try { + Runnable t = () -> { + Thread currentThread = Thread.currentThread(); + String name = currentThread.getName(); + try { + currentThread.setName("Membership-MemberExpired"); + service.memberDisappeared(member); + } finally { + currentThread.setName(name); + } + }; + executor.execute(t); + } catch (Exception x) { + log.error(sm.getString("mcastServiceImpl.memberDisappeared.failed"), x); + } + } + } + } + + /** + * Send a ping. + * @param checkexpired true to check for expiration + * @throws IOException Send error + */ + public void send(boolean checkexpired) throws IOException { + send(checkexpired,null); + } + + private final Object sendLock = new Object(); + + public void send(boolean checkexpired, DatagramPacket packet) throws IOException { + checkexpired = (checkexpired && (packet==null)); + //ignore if we haven't started the sender + //if ( (startLevel&Channel.MBR_TX_SEQ) != Channel.MBR_TX_SEQ ) return; + if (packet==null) { + member.inc(); + if(log.isTraceEnabled()) { + log.trace("Mcast send ping from member " + member); + } + byte[] data = member.getData(); + packet = new DatagramPacket(data,data.length); + } else if (log.isTraceEnabled()) { + log.trace("Sending message broadcast "+packet.getLength()+ " bytes from "+ member); + } + packet.setAddress(address); + packet.setPort(port); + //TODO this operation is not thread safe + synchronized (sendLock) { + socket.send(packet); + } + if ( checkexpired ) { + checkExpired(); + } + } + + public long getServiceStartTime() { + return (member!=null) ? member.getServiceStartTime() : -1l; + } + + public int getRecoveryCounter() { + return recoveryCounter; + } + + public boolean isRecoveryEnabled() { + return recoveryEnabled; + } + + public long getRecoverySleepTime() { + return recoverySleepTime; + } + + public Channel getChannel() { + return channel; + } + + public void setChannel(Channel channel) { + this.channel = channel; + } + + public class ReceiverThread extends Thread { + int errorCounter = 0; + public ReceiverThread() { + super(); + String channelName = ""; + if (channel.getName() != null) { + channelName = "[" + channel.getName() + "]"; + } + setName("Tribes-MembershipReceiver" + channelName); + } + @Override + public void run() { + while ( doRunReceiver ) { + try { + receive(); + errorCounter=0; + } catch ( ArrayIndexOutOfBoundsException ax ) { + //we can ignore this, as it means we have an invalid package + //but we will log it to debug + if ( log.isDebugEnabled() ) { + log.debug(sm.getString("mcastServiceImpl.invalidMemberPackage"), ax); + } + } catch ( Exception x ) { + if (errorCounter==0 && doRunReceiver) { + log.warn(sm.getString("mcastServiceImpl.error.receiving"),x); + } else if (log.isDebugEnabled()) { + if (doRunReceiver) { + log.debug(sm.getString("mcastServiceImpl.error.receiving"), x); + } else { + log.warn(sm.getString("mcastServiceImpl.error.receivingNoSleep"), x); + } + } + if (doRunReceiver) { + try { + sleep(500); + } catch (Exception ignore){ + // Ignore + } + if ( (++errorCounter)>=recoveryCounter ) { + errorCounter=0; + RecoveryThread.recover(McastServiceImpl.this); + } + } + } + } + } + }//class ReceiverThread + + public class SenderThread extends Thread { + final long time; + int errorCounter=0; + public SenderThread(long time) { + this.time = time; + String channelName = ""; + if (channel.getName() != null) { + channelName = "[" + channel.getName() + "]"; + } + setName("Tribes-MembershipSender" + channelName); + + } + @Override + public void run() { + while ( doRunSender ) { + try { + send(true); + errorCounter = 0; + } catch ( Exception x ) { + if (errorCounter==0) { + log.warn(sm.getString("mcastServiceImpl.send.failed"),x); + } else { + log.debug(sm.getString("mcastServiceImpl.send.failed"),x); + } + if ( (++errorCounter)>=recoveryCounter ) { + errorCounter=0; + RecoveryThread.recover(McastServiceImpl.this); + } + } + try { + sleep(time); + } catch (Exception ignore) { + // Ignore + } + } + } + }//class SenderThread + + protected static class RecoveryThread extends Thread { + + private static final AtomicBoolean running = new AtomicBoolean(false); + + public static synchronized void recover(McastServiceImpl parent) { + + if (!parent.isRecoveryEnabled()) { + return; + } + + if (!running.compareAndSet(false, true)) { + return; + } + + Thread t = new RecoveryThread(parent); + String channelName = ""; + if (parent.channel.getName() != null) { + channelName = "[" + parent.channel.getName() + "]"; + } + t.setName("Tribes-MembershipRecovery" + channelName); + t.setDaemon(true); + t.start(); + } + + + final McastServiceImpl parent; + public RecoveryThread(McastServiceImpl parent) { + this.parent = parent; + } + + public boolean stopService() { + try { + parent.stop(Channel.MBR_RX_SEQ | Channel.MBR_TX_SEQ); + return true; + } catch (Exception x) { + log.warn(sm.getString("mcastServiceImpl.recovery.stopFailed"), x); + return false; + } + } + public boolean startService() { + try { + parent.init(); + parent.start(Channel.MBR_RX_SEQ | Channel.MBR_TX_SEQ); + return true; + } catch (Exception x) { + log.warn(sm.getString("mcastServiceImpl.recovery.startFailed"), x); + return false; + } + } + @Override + public void run() { + boolean success = false; + int attempt = 0; + try { + while (!success) { + if(log.isInfoEnabled()) { + log.info(sm.getString("mcastServiceImpl.recovery")); + } + if (stopService() & startService()) { + success = true; + if(log.isInfoEnabled()) { + log.info(sm.getString("mcastServiceImpl.recovery.successful")); + } + } + try { + if (!success) { + if(log.isInfoEnabled()) { + log.info(sm.getString("mcastServiceImpl.recovery.failed", + Integer.toString(++attempt), + Long.toString(parent.recoverySleepTime))); + } + sleep(parent.recoverySleepTime); + } + }catch (InterruptedException ignore) { + } + } + }finally { + running.set(false); + } + } + } + + public void setRecoveryCounter(int recoveryCounter) { + this.recoveryCounter = recoveryCounter; + } + + public void setRecoveryEnabled(boolean recoveryEnabled) { + this.recoveryEnabled = recoveryEnabled; + } + + public void setRecoverySleepTime(long recoverySleepTime) { + this.recoverySleepTime = recoverySleepTime; + } +} diff --git a/java/org/apache/catalina/tribes/membership/McastServiceMBean.java b/java/org/apache/catalina/tribes/membership/McastServiceMBean.java new file mode 100644 index 0000000..9b76840 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/McastServiceMBean.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.tribes.membership; + +import java.util.Properties; + +import org.apache.catalina.tribes.Member; + +public interface McastServiceMBean { + + // Attributes + String getAddress(); + + int getPort(); + + long getFrequency(); + + long getDropTime(); + + String getBind(); + + int getTtl(); + + byte[] getDomain(); + + int getSoTimeout(); + + boolean getRecoveryEnabled(); + + int getRecoveryCounter(); + + long getRecoverySleepTime(); + + boolean getLocalLoopbackDisabled(); + + String getLocalMemberName(); + + // Operation + Properties getProperties(); + + boolean hasMembers(); + + String[] getMembersByName(); + + Member findMemberByName(String name); +} diff --git a/java/org/apache/catalina/tribes/membership/MemberImpl.java b/java/org/apache/catalina/tribes/membership/MemberImpl.java new file mode 100644 index 0000000..1f95405 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/MemberImpl.java @@ -0,0 +1,673 @@ +/* + * 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.catalina.tribes.membership; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.transport.SenderState; +import org.apache.catalina.tribes.util.StringManager; + +/** + * A membership implementation using simple multicast. + * This is the representation of a multicast member. + * Carries the host, and port of the this or other cluster nodes. + */ +public class MemberImpl implements Member, java.io.Externalizable { + + public static final transient byte[] TRIBES_MBR_BEGIN = new byte[] {84, 82, 73, 66, 69, 83, 45, 66, 1, 0}; + public static final transient byte[] TRIBES_MBR_END = new byte[] {84, 82, 73, 66, 69, 83, 45, 69, 1, 0}; + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + /** + * The listen host for this member + */ + protected volatile byte[] host = new byte[0]; + protected transient volatile String hostname; + /** + * The tcp listen port for this member + */ + protected volatile int port; + /** + * The udp listen port for this member + */ + protected volatile int udpPort = -1; + + /** + * The tcp/SSL listen port for this member + */ + protected volatile int securePort = -1; + + /** + * Counter for how many broadcast messages have been sent from this member + */ + protected AtomicInteger msgCount = new AtomicInteger(0); + + /** + * The number of milliseconds since this member was + * created, is kept track of using the start time + */ + protected volatile long memberAliveTime = 0; + + /** + * For the local member only + */ + protected transient long serviceStartTime; + + /** + * To avoid serialization over and over again, once the local dataPkg + * has been set, we use that to transmit data + */ + protected transient byte[] dataPkg = null; + + /** + * Unique session Id for this member + */ + protected volatile byte[] uniqueId = new byte[16]; + + /** + * Custom payload that an app framework can broadcast + * Also used to transport stop command. + */ + protected volatile byte[] payload = new byte[0]; + + /** + * Command, so that the custom payload doesn't have to be used + * This is for internal tribes use, such as SHUTDOWN_COMMAND + */ + protected volatile byte[] command = new byte[0]; + + /** + * Domain if we want to filter based on domain. + */ + protected volatile byte[] domain = new byte[0]; + + /** + * The flag indicating that this member is a local member. + */ + protected volatile boolean local = false; + + /** + * Empty constructor for serialization + */ + public MemberImpl() { + + } + + /** + * Construct a new member object. + * + * @param host - the tcp listen host + * @param port - the tcp listen port + * @param aliveTime - the number of milliseconds since this member was created + * + * @throws IOException If there is an error converting the host name to an + * IP address + */ + public MemberImpl(String host, + int port, + long aliveTime) throws IOException { + setHostname(host); + this.port = port; + this.memberAliveTime=aliveTime; + } + + public MemberImpl(String host, + int port, + long aliveTime, + byte[] payload) throws IOException { + this(host,port,aliveTime); + setPayload(payload); + } + + @Override + public boolean isReady() { + return SenderState.getSenderState(this).isReady(); + } + @Override + public boolean isSuspect() { + return SenderState.getSenderState(this).isSuspect(); + } + @Override + public boolean isFailing() { + return SenderState.getSenderState(this).isFailing(); + } + + /** + * Increment the message count. + */ + protected void inc() { + msgCount.incrementAndGet(); + } + + /** + * Create a data package to send over the wire representing this member. + * This is faster than serialization. + * @return - the bytes for this member deserialized + */ + public byte[] getData() { + return getData(true); + } + + + @Override + public byte[] getData(boolean getalive) { + return getData(getalive,false); + } + + + @Override + public synchronized int getDataLength() { + return TRIBES_MBR_BEGIN.length+ //start pkg + 4+ //data length + 8+ //alive time + 4+ //port + 4+ //secure port + 4+ //udp port + 1+ //host length + host.length+ //host + 4+ //command length + command.length+ //command + 4+ //domain length + domain.length+ //domain + 16+ //unique id + 4+ //payload length + payload.length+ //payload + TRIBES_MBR_END.length; //end pkg + } + + + @Override + public synchronized byte[] getData(boolean getalive, boolean reset) { + if (reset) { + dataPkg = null; + } + // Look in cache first + if (dataPkg != null) { + if (getalive) { + // You'd be surprised, but System.currentTimeMillis + // shows up on the profiler + long alive = System.currentTimeMillis() - getServiceStartTime(); + byte[] result = dataPkg.clone(); + XByteBuffer.toBytes(alive, result, TRIBES_MBR_BEGIN.length + 4); + dataPkg = result; + } + return dataPkg; + } + + //package looks like + //start package TRIBES_MBR_BEGIN.length + //package length - 4 bytes + //alive - 8 bytes + //port - 4 bytes + //secure port - 4 bytes + //udp port - 4 bytes + //host length - 1 byte + //host - hl bytes + //clen - 4 bytes + //command - clen bytes + //dlen - 4 bytes + //domain - dlen bytes + //uniqueId - 16 bytes + //payload length - 4 bytes + //payload plen bytes + //end package TRIBES_MBR_END.length + long alive=System.currentTimeMillis()-getServiceStartTime(); + byte[] data = new byte[getDataLength()]; + + int bodylength = (getDataLength() - TRIBES_MBR_BEGIN.length - TRIBES_MBR_END.length - 4); + + int pos = 0; + + //TRIBES_MBR_BEGIN + System.arraycopy(TRIBES_MBR_BEGIN,0,data,pos,TRIBES_MBR_BEGIN.length); + pos += TRIBES_MBR_BEGIN.length; + + //body length + XByteBuffer.toBytes(bodylength,data,pos); + pos += 4; + + //alive data + XByteBuffer.toBytes(alive,data,pos); + pos += 8; + //port + XByteBuffer.toBytes(port,data,pos); + pos += 4; + //secure port + XByteBuffer.toBytes(securePort,data,pos); + pos += 4; + //udp port + XByteBuffer.toBytes(udpPort,data,pos); + pos += 4; + //host length + data[pos++] = (byte) host.length; + //host + System.arraycopy(host,0,data,pos,host.length); + pos+=host.length; + //clen - 4 bytes + XByteBuffer.toBytes(command.length,data,pos); + pos+=4; + //command - clen bytes + System.arraycopy(command,0,data,pos,command.length); + pos+=command.length; + //dlen - 4 bytes + XByteBuffer.toBytes(domain.length,data,pos); + pos+=4; + //domain - dlen bytes + System.arraycopy(domain,0,data,pos,domain.length); + pos+=domain.length; + //unique Id + System.arraycopy(uniqueId,0,data,pos,uniqueId.length); + pos+=uniqueId.length; + //payload + XByteBuffer.toBytes(payload.length,data,pos); + pos+=4; + System.arraycopy(payload,0,data,pos,payload.length); + pos+=payload.length; + + //TRIBES_MBR_END + System.arraycopy(TRIBES_MBR_END,0,data,pos,TRIBES_MBR_END.length); + pos += TRIBES_MBR_END.length; + + //create local data + dataPkg = data; + return data; + } + /** + * Deserializes a member from data sent over the wire. + * + * @param data The bytes received + * @param member The member object to populate + * + * @return The populated member object. + */ + public static Member getMember(byte[] data, MemberImpl member) { + return getMember(data,0,data.length,member); + } + + public static Member getMember(byte[] data, int offset, int length, MemberImpl member) { + //package looks like + //start package TRIBES_MBR_BEGIN.length + //package length - 4 bytes + //alive - 8 bytes + //port - 4 bytes + //secure port - 4 bytes + //udp port - 4 bytes + //host length - 1 byte + //host - hl bytes + //clen - 4 bytes + //command - clen bytes + //dlen - 4 bytes + //domain - dlen bytes + //uniqueId - 16 bytes + //payload length - 4 bytes + //payload plen bytes + //end package TRIBES_MBR_END.length + + int pos = offset; + + if (XByteBuffer.firstIndexOf(data,offset,TRIBES_MBR_BEGIN)!=pos) { + throw new IllegalArgumentException(sm.getString("memberImpl.invalid.package.begin", org.apache.catalina.tribes.util.Arrays.toString(TRIBES_MBR_BEGIN))); + } + + if ( length < (TRIBES_MBR_BEGIN.length+4) ) { + throw new ArrayIndexOutOfBoundsException(sm.getString("memberImpl.package.small")); + } + + pos += TRIBES_MBR_BEGIN.length; + + int bodylength = XByteBuffer.toInt(data,pos); + pos += 4; + + if ( length < (bodylength+4+TRIBES_MBR_BEGIN.length+TRIBES_MBR_END.length) ) { + throw new ArrayIndexOutOfBoundsException(sm.getString("memberImpl.notEnough.bytes")); + } + + int endpos = pos+bodylength; + if (XByteBuffer.firstIndexOf(data,endpos,TRIBES_MBR_END)!=endpos) { + throw new IllegalArgumentException(sm.getString("memberImpl.invalid.package.end", org.apache.catalina.tribes.util.Arrays.toString(TRIBES_MBR_END))); + } + + + byte[] alived = new byte[8]; + System.arraycopy(data, pos, alived, 0, 8); + pos += 8; + byte[] portd = new byte[4]; + System.arraycopy(data, pos, portd, 0, 4); + pos += 4; + + byte[] sportd = new byte[4]; + System.arraycopy(data, pos, sportd, 0, 4); + pos += 4; + + byte[] uportd = new byte[4]; + System.arraycopy(data, pos, uportd, 0, 4); + pos += 4; + + + byte hl = data[pos++]; + byte[] addr = new byte[hl]; + System.arraycopy(data, pos, addr, 0, hl); + pos += hl; + + int cl = XByteBuffer.toInt(data, pos); + pos += 4; + + byte[] command = new byte[cl]; + System.arraycopy(data, pos, command, 0, command.length); + pos += command.length; + + int dl = XByteBuffer.toInt(data, pos); + pos += 4; + + byte[] domain = new byte[dl]; + System.arraycopy(data, pos, domain, 0, domain.length); + pos += domain.length; + + byte[] uniqueId = new byte[16]; + System.arraycopy(data, pos, uniqueId, 0, 16); + pos += 16; + + int pl = XByteBuffer.toInt(data, pos); + pos += 4; + + byte[] payload = new byte[pl]; + System.arraycopy(data, pos, payload, 0, payload.length); + pos += payload.length; + + synchronized (member) { + member.setHost(addr); + member.setPort(XByteBuffer.toInt(portd, 0)); + member.setSecurePort(XByteBuffer.toInt(sportd, 0)); + member.setUdpPort(XByteBuffer.toInt(uportd, 0)); + member.setMemberAliveTime(XByteBuffer.toLong(alived, 0)); + member.setUniqueId(uniqueId); + member.payload = payload; + member.domain = domain; + member.command = command; + + member.dataPkg = new byte[length]; + System.arraycopy(data, offset, member.dataPkg, 0, length); + } + + return member; + } + + public static Member getMember(byte[] data) { + return getMember(data,new MemberImpl()); + } + + public static Member getMember(byte[] data, int offset, int length) { + return getMember(data,offset,length,new MemberImpl()); + } + + /** + * Return the name of this object + * @return a unique name to the cluster + */ + @Override + public String getName() { + return "tcp://"+getHostname()+":"+getPort(); + } + + /** + * Return the listen port of this member + * @return - tcp listen port + */ + @Override + public int getPort() { + return this.port; + } + + /** + * Return the TCP listen host for this member + * @return IP address or host name + */ + @Override + public byte[] getHost() { + return host; + } + + public String getHostname() { + if (this.hostname != null) { + return hostname; + } else { + byte[] host = this.host; + this.hostname = org.apache.catalina.tribes.util.Arrays.toString(host, 0, host.length, true); + return this.hostname; + } + } + + public int getMsgCount() { + return msgCount.get(); + } + + /** + * Contains information on how long this member has been online. + * The result is the number of milli seconds this member has been + * broadcasting its membership to the cluster. + * @return nr of milliseconds since this member started. + */ + @Override + public long getMemberAliveTime() { + return memberAliveTime; + } + + public long getServiceStartTime() { + return serviceStartTime; + } + + @Override + public byte[] getUniqueId() { + return uniqueId; + } + + @Override + public byte[] getPayload() { + return payload; + } + + @Override + public byte[] getCommand() { + return command; + } + + @Override + public byte[] getDomain() { + return domain; + } + + @Override + public int getSecurePort() { + return securePort; + } + + @Override + public int getUdpPort() { + return udpPort; + } + + @Override + public void setMemberAliveTime(long time) { + memberAliveTime=time; + } + + + + /** + * String representation of this object + */ + @Override + public String toString() { + StringBuilder buf = new StringBuilder(getClass().getName()); + buf.append('['); + buf.append(getName()).append(','); + buf.append(getHostname()).append(','); + buf.append(port).append(", alive="); + buf.append(memberAliveTime).append(", "); + buf.append("securePort=").append(securePort).append(", "); + buf.append("UDP Port=").append(udpPort).append(", "); + buf.append("id=").append(bToS(this.uniqueId)).append(", "); + buf.append("payload=").append(bToS(this.payload,8)).append(", "); + buf.append("command=").append(bToS(this.command,8)).append(", "); + buf.append("domain=").append(bToS(this.domain,8)); + buf.append(']'); + return buf.toString(); + } + public static String bToS(byte[] data) { + return bToS(data,data.length); + } + public static String bToS(byte[] data, int max) { + StringBuilder buf = new StringBuilder(4*16); + buf.append('{'); + for (int i=0; data!=null && i oldPayloadLength) { + // It is possible that the max packet size will be exceeded + if ((newPayloadLength - oldPayloadLength + getData(false, false).length) > + McastServiceImpl.MAX_PACKET_SIZE) { + throw new IllegalArgumentException(sm.getString("memberImpl.large.payload")); + } + } + this.payload = payload != null ? payload : new byte[0]; + getData(true, true); + } + + @Override + public synchronized void setCommand(byte[] command) { + this.command = command!=null?command:new byte[0]; + getData(true,true); + } + + public synchronized void setDomain(byte[] domain) { + this.domain = domain!=null?domain:new byte[0]; + getData(true,true); + } + + public synchronized void setSecurePort(int securePort) { + this.securePort = securePort; + this.dataPkg = null; + } + + public synchronized void setUdpPort(int port) { + this.udpPort = port; + this.dataPkg = null; + } + + @Override + public boolean isLocal() { + return local; + } + + @Override + public void setLocal(boolean local) { + this.local = local; + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + int length = in.readInt(); + byte[] message = new byte[length]; + in.readFully(message); + getMember(message,this); + + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + byte[] data = this.getData(); + out.writeInt(data.length); + out.write(data); + } + +} diff --git a/java/org/apache/catalina/tribes/membership/Membership.java b/java/org/apache/catalina/tribes/membership/Membership.java new file mode 100644 index 0000000..9aaffaf --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/Membership.java @@ -0,0 +1,331 @@ +/* + * 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.catalina.tribes.membership; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; + +import org.apache.catalina.tribes.Member; + +/** + * A membership implementation using simple multicast. + * This is the representation of a multicast membership. + * This class is responsible for maintaining a list of active cluster nodes in the cluster. + * If a node fails to send out a heartbeat, the node will be dismissed. + * + * @author Peter Rossbach + */ +public class Membership implements Cloneable { + + protected static final Member[] EMPTY_MEMBERS = new Member[0]; + + // Non-final to support clone() + private Object membersLock = new Object(); + + /** + * The local member. + */ + protected final Member local; + + /** + * A map of all the members in the cluster. + */ + protected HashMap map = new HashMap<>(); // Guarded by membersLock + + /** + * A list of all the members in the cluster. + */ + protected volatile Member[] members = EMPTY_MEMBERS; // Guarded by membersLock + + /** + * Comparator for sorting members by alive time. + */ + protected final Comparator memberComparator; + + @Override + public Membership clone() { + synchronized (membersLock) { + Membership clone; + try { + clone = (Membership) super.clone(); + } catch (CloneNotSupportedException e) { + // Can't happen + throw new AssertionError(); + } + + // Standard clone() method will copy the map object. Replace that + // with a new map but with the same contents. + @SuppressWarnings("unchecked") + final HashMap tmpclone = (HashMap) map.clone(); + clone.map = tmpclone; + + // Standard clone() method will copy the array object. Replace that + // with a new array but with the same contents. + clone.members = members.clone(); + + // Standard clone() method will copy the lock object. Replace that + // with a new object. + clone.membersLock = new Object(); + return clone; + } + } + + /** + * Constructs a new membership + * @param local - has to be the name of the local member. Used to filter the local member from the cluster membership + * @param includeLocal - TBA + */ + public Membership(Member local, boolean includeLocal) { + this(local, Comparator.comparingLong(Member::getMemberAliveTime).reversed(), includeLocal); + } + + public Membership(Member local) { + this(local, false); + } + + public Membership(Member local, Comparator comp) { + this(local, comp, false); + } + + public Membership(Member local, Comparator comp, boolean includeLocal) { + this.local = local; + this.memberComparator = comp; + if (includeLocal) { + addMember(local); + } + } + + /** + * Reset the membership and start over fresh. i.e., delete all the members + * and wait for them to ping again and join this membership. + */ + public void reset() { + synchronized (membersLock) { + map.clear(); + members = EMPTY_MEMBERS ; + } + } + + /** + * Notify the membership that this member has announced itself. + * + * @param member - the member that just pinged us + * @return - true if this member is new to the cluster, false otherwise.
    + * - false if this member is the local member or updated. + */ + public boolean memberAlive(Member member) { + // Ignore ourselves + if (member.equals(local)) { + return false; + } + + boolean result = false; + synchronized (membersLock) { + MbrEntry entry = map.get(member); + if (entry == null) { + entry = addMember(member); + result = true; + } else { + // Update the member alive time + Member updateMember = entry.getMember(); + if (updateMember.getMemberAliveTime() != member.getMemberAliveTime()) { + // Update fields that can change + updateMember.setMemberAliveTime(member.getMemberAliveTime()); + updateMember.setPayload(member.getPayload()); + updateMember.setCommand(member.getCommand()); + // Re-order. Can't sort in place since a call to + // getMembers() may then receive an intermediate result. + Member[] newMembers = members.clone(); + Arrays.sort(newMembers, memberComparator); + members = newMembers; + } + } + entry.accessed(); + } + return result; + } + + /** + * Add a member to this component and sort array with memberComparator + * + * @param member The member to add + * + * @return The member entry created for this new member. + */ + public MbrEntry addMember(Member member) { + MbrEntry entry = new MbrEntry(member); + synchronized (membersLock) { + if (!map.containsKey(member) ) { + map.put(member, entry); + Member results[] = new Member[members.length + 1]; + System.arraycopy(members, 0, results, 0, members.length); + results[members.length] = member; + Arrays.sort(results, memberComparator); + members = results; + } + } + return entry; + } + + /** + * Remove a member from this component. + * + * @param member The member to remove + */ + public void removeMember(Member member) { + synchronized (membersLock) { + map.remove(member); + int n = -1; + for (int i = 0; i < members.length; i++) { + if (members[i] == member || members[i].equals(member)) { + n = i; + break; + } + } + if (n < 0) { + return; + } + Member results[] = new Member[members.length - 1]; + int j = 0; + for (int i = 0; i < members.length; i++) { + if (i != n) { + results[j++] = members[i]; + } + } + members = results; + } + } + + /** + * Runs a refresh cycle and returns a list of members that has expired. + * This also removes the members from the membership, in such a way that + * getMembers() = getMembers() - expire() + * @param maxtime - the max time a member can remain unannounced before it is considered dead. + * @return the list of expired members + */ + public Member[] expire(long maxtime) { + synchronized (membersLock) { + if (!hasMembers()) { + return EMPTY_MEMBERS; + } + + ArrayList list = null; + for (MbrEntry entry : map.values()) { + if (entry.hasExpired(maxtime)) { + if (list == null) { + // Only need a list when members are expired (smaller gc) + list = new ArrayList<>(); + } + list.add(entry.getMember()); + } + } + + if (list != null) { + Member[] result = list.toArray(new Member[0]); + for (Member member : result) { + removeMember(member); + } + return result; + } else { + return EMPTY_MEMBERS ; + } + } + } + + /** + * Returning that service has members or not. + * + * @return true if there are one or more members, otherwise + * false + */ + public boolean hasMembers() { + return members.length > 0; + } + + + public Member getMember(Member mbr) { + Member[] members = this.members; + if (members.length > 0) { + for (Member member : members) { + if (member.equals(mbr)) { + return member; + } + } + } + return null; + } + + public boolean contains(Member mbr) { + return getMember(mbr) != null; + } + + /** + * Returning a list of all the members in the membership. + * We not need a copy: add and remove generate new arrays. + * + * @return An array of the current members + */ + public Member[] getMembers() { + return members; + } + + + // --------------------------------------------- Inner Class + + /** + * Inner class that represents a member entry + */ + protected static class MbrEntry { + + protected final Member mbr; + protected long lastHeardFrom; + + public MbrEntry(Member mbr) { + this.mbr = mbr; + } + + /** + * Indicate that this member has been accessed. + */ + public void accessed(){ + lastHeardFrom = System.currentTimeMillis(); + } + + /** + * Obtain the member associated with this entry. + * + * @return The member for this entry. + */ + public Member getMember() { + return mbr; + } + + /** + * Check if this member has expired. + * + * @param maxtime The time threshold + * + * @return true if the member has expired, otherwise + * false + */ + public boolean hasExpired(long maxtime) { + return !mbr.isLocal() && (System.currentTimeMillis() - lastHeardFrom) > maxtime; + } + } +} diff --git a/java/org/apache/catalina/tribes/membership/MembershipProviderBase.java b/java/org/apache/catalina/tribes/membership/MembershipProviderBase.java new file mode 100644 index 0000000..8c3b93e --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/MembershipProviderBase.java @@ -0,0 +1,73 @@ +/* + * 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.catalina.tribes.membership; + +import java.util.Properties; +import java.util.concurrent.ScheduledExecutorService; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.MembershipProvider; +import org.apache.catalina.tribes.MembershipService; + +public abstract class MembershipProviderBase implements MembershipProvider { + + protected Membership membership; + protected MembershipListener membershipListener; + protected MembershipService service; + // The event notification executor + protected ScheduledExecutorService executor; + + @Override + public void init(Properties properties) throws Exception { + } + + @Override + public boolean hasMembers() { + if (membership == null ) { + return false; + } + return membership.hasMembers(); + } + + @Override + public Member getMember(Member mbr) { + if (membership.getMembers() == null) { + return null; + } + return membership.getMember(mbr); + } + + @Override + public Member[] getMembers() { + if (membership.getMembers() == null) { + return Membership.EMPTY_MEMBERS; + } + return membership.getMembers(); + } + + @Override + public void setMembershipListener(MembershipListener listener) { + this.membershipListener = listener; + } + + @Override + public void setMembershipService(MembershipService service) { + this.service = service; + executor = service.getChannel().getUtilityExecutor(); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/membership/MembershipServiceBase.java b/java/org/apache/catalina/tribes/membership/MembershipServiceBase.java new file mode 100644 index 0000000..b05b9cc --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/MembershipServiceBase.java @@ -0,0 +1,143 @@ +/* + * 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.catalina.tribes.membership; + +import java.util.Properties; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.MembershipService; + +public abstract class MembershipServiceBase implements MembershipService, MembershipListener { + + /** + * The implementation specific properties + */ + protected Properties properties = new Properties(); + protected volatile MembershipListener listener; + protected Channel channel; + + @Override + public void setProperties(Properties properties) { + this.properties = properties; + } + + @Override + public Properties getProperties() { + return properties; + } + + @Override + public boolean hasMembers() { + if (getMembershipProvider() == null ) { + return false; + } + return getMembershipProvider().hasMembers(); + } + + @Override + public Member getMember(Member mbr) { + if (getMembershipProvider() == null) { + return null; + } + return getMembershipProvider().getMember(mbr); + } + + @Override + public Member[] getMembers() { + if (getMembershipProvider() == null) { + return Membership.EMPTY_MEMBERS; + } + return getMembershipProvider().getMembers(); + } + + @Override + public String[] getMembersByName() { + Member[] currentMembers = getMembers(); + String [] membernames ; + if(currentMembers != null) { + membernames = new String[currentMembers.length]; + for (int i = 0; i < currentMembers.length; i++) { + membernames[i] = currentMembers[i].toString() ; + } + } else { + membernames = new String[0] ; + } + return membernames ; + } + + @Override + public Member findMemberByName(String name) { + Member[] currentMembers = getMembers(); + for (Member currentMember : currentMembers) { + if (name.equals(currentMember.toString())) { + return currentMember; + } + } + return null; + } + + @Override + public void setMembershipListener(MembershipListener listener) { + this.listener = listener; + } + + @Override + public void removeMembershipListener(){ + listener = null; + } + + @Override + public void memberAdded(Member member) { + MembershipListener listener = this.listener; + if (listener != null) { + listener.memberAdded(member); + } + } + + @Override + public void memberDisappeared(Member member) { + MembershipListener listener = this.listener; + if (listener != null) { + listener.memberDisappeared(member); + } + } + + @Override + public void broadcast(ChannelMessage message) throws ChannelException { + // no-op + } + + @Override + public Channel getChannel() { + return channel; + } + + @Override + public void setChannel(Channel channel) { + this.channel = channel; + } + + @Override + public void start() throws Exception { + start(MBR_RX); + start(MBR_TX); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/membership/StaticMember.java b/java/org/apache/catalina/tribes/membership/StaticMember.java new file mode 100644 index 0000000..39159f9 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/StaticMember.java @@ -0,0 +1,79 @@ +/* + * 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.catalina.tribes.membership; + +import java.io.IOException; + +import org.apache.catalina.tribes.util.Arrays; + +public class StaticMember extends MemberImpl { + public StaticMember() { + super(); + } + + public StaticMember(String host, int port, long aliveTime) throws IOException { + super(host, port, aliveTime); + } + + public StaticMember(String host, int port, long aliveTime, byte[] payload) throws IOException { + super(host, port, aliveTime, payload); + } + + /** + * @param host String, either in byte array string format, like {214,116,1,3} + * or as a regular hostname, 127.0.0.1 or tomcat01.mydomain.com + */ + public void setHost(String host) { + if ( host == null ) { + return; + } + if ( host.startsWith("{") ) { + setHost(Arrays.fromString(host)); + } else { + try { setHostname(host); }catch (IOException x) { throw new RuntimeException(x);} + } + + } + + /** + * @param domain String, either in byte array string format, like {214,116,1,3} + * or as a regular string value like 'mydomain'. The latter will be converted using ISO-8859-1 encoding + */ + public void setDomain(String domain) { + if ( domain == null ) { + return; + } + if ( domain.startsWith("{") ) { + setDomain(Arrays.fromString(domain)); + } else { + setDomain(Arrays.convert(domain)); + } + } + + /** + * @param id String, must be in byte array string format, like {214,116,1,3} and exactly 16 bytes long + */ + public void setUniqueId(String id) { + byte[] uuid = Arrays.fromString(id); + if ( uuid==null || uuid.length != 16 ) { + throw new RuntimeException(sm.getString("staticMember.invalid.uuidLength", id)); + } + setUniqueId(uuid); + } + + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/membership/StaticMembershipProvider.java b/java/org/apache/catalina/tribes/membership/StaticMembershipProvider.java new file mode 100644 index 0000000..a4406b9 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/StaticMembershipProvider.java @@ -0,0 +1,416 @@ +/* + * 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.catalina.tribes.membership; + +import java.io.Serializable; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelException.FaultyMember; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.Heartbeat; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.Response; +import org.apache.catalina.tribes.group.RpcCallback; +import org.apache.catalina.tribes.group.RpcChannel; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.catalina.tribes.util.ExceptionUtils; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class StaticMembershipProvider extends MembershipProviderBase implements RpcCallback, ChannelListener, Heartbeat { + + protected static final StringManager sm = StringManager.getManager(StaticMembershipProvider.class); + private static final Log log = LogFactory.getLog(StaticMembershipProvider.class); + + protected Channel channel; + protected RpcChannel rpcChannel; + private String membershipName = null; + private byte[] membershipId = null; + protected ArrayList staticMembers; + protected int sendOptions = Channel.SEND_OPTIONS_ASYNCHRONOUS; + protected long expirationTime = 5000; + protected int connectTimeout = 500; + protected long rpcTimeout = 3000; + protected int startLevel = 0; + // for ping thread + protected boolean useThread = false; + protected long pingInterval = 1000; + protected volatile boolean running = true; + protected PingThread thread = null; + + @Override + public void init(Properties properties) throws Exception { + String expirationTimeStr = properties.getProperty("expirationTime"); + this.expirationTime = Long.parseLong(expirationTimeStr); + String connectTimeoutStr = properties.getProperty("connectTimeout"); + this.connectTimeout = Integer.parseInt(connectTimeoutStr); + String rpcTimeouStr = properties.getProperty("rpcTimeout"); + this.rpcTimeout = Long.parseLong(rpcTimeouStr); + this.membershipName = properties.getProperty("membershipName"); + this.membershipId = membershipName.getBytes(StandardCharsets.ISO_8859_1); + membership = new Membership(service.getLocalMember(true)); + this.rpcChannel = new RpcChannel(this.membershipId, channel, this); + this.channel.addChannelListener(this); + String useThreadStr = properties.getProperty("useThread"); + this.useThread = Boolean.parseBoolean(useThreadStr); + String pingIntervalStr = properties.getProperty("pingInterval"); + this.pingInterval = Long.parseLong(pingIntervalStr); + } + + @Override + public void start(int level) throws Exception { + if (Channel.MBR_RX_SEQ==(level & Channel.MBR_RX_SEQ)) { + //no-op + } + if (Channel.MBR_TX_SEQ==(level & Channel.MBR_TX_SEQ)) { + //no-op + } + startLevel = (startLevel | level); + if (startLevel == (Channel.MBR_RX_SEQ | Channel.MBR_TX_SEQ)) { + startMembership(getAliveMembers(staticMembers.toArray(new Member[0]))); + running = true; + if ( thread == null && useThread) { + thread = new PingThread(); + thread.setDaemon(true); + thread.setName("StaticMembership.PingThread[" + this.channel.getName() +"]"); + thread.start(); + } + } + } + + @Override + public boolean stop(int level) throws Exception { + if (Channel.MBR_RX_SEQ==(level & Channel.MBR_RX_SEQ)) { + // no-op + } + if (Channel.MBR_TX_SEQ==(level & Channel.MBR_TX_SEQ)) { + // no-op + } + startLevel = (startLevel & (~level)); + if ( startLevel == 0 ) { + running = false; + if (thread != null) { + thread.interrupt(); + thread = null; + } + if (this.rpcChannel != null) { + this.rpcChannel.breakdown(); + } + if (this.channel != null) { + try { + stopMembership(this.getMembers()); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // Otherwise ignore + } + this.channel.removeChannelListener(this); + this.channel = null; + } + this.rpcChannel = null; + this.membership.reset(); + } + return (startLevel == 0); + } + + protected void startMembership(Member[] members) throws ChannelException { + if (members.length == 0) { + return; + } + MemberMessage msg = new MemberMessage(membershipId, MemberMessage.MSG_START, service.getLocalMember(true)); + Response[] resp = rpcChannel.send(members, msg, RpcChannel.ALL_REPLY, sendOptions, rpcTimeout); + if (resp.length > 0) { + for (Response response : resp) { + messageReceived(response.getMessage(), response.getSource()); + } + } else { + log.warn(sm.getString("staticMembershipProvider.startMembership.noReplies")); + } + } + + protected Member setupMember(Member mbr) { + // no-op + return mbr; + } + + protected void memberAdded(Member member) { + Member mbr = setupMember(member); + if(membership.memberAlive(mbr)) { + Runnable r = () -> { + Thread currentThread = Thread.currentThread(); + String name = currentThread.getName(); + try { + currentThread.setName("StaticMembership-memberAdded"); + membershipListener.memberAdded(mbr); + } finally { + currentThread.setName(name); + } + }; + executor.execute(r); + } + } + + protected void memberDisappeared(Member member) { + membership.removeMember(member); + Runnable r = () -> { + Thread currentThread = Thread.currentThread(); + String name = currentThread.getName(); + try { + currentThread.setName("StaticMembership-memberDisappeared"); + membershipListener.memberDisappeared(member); + } finally { + currentThread.setName(name); + } + }; + executor.execute(r); + } + + protected void memberAlive(Member member) { + if (!membership.contains(member)) { + memberAdded(member); + } + membership.memberAlive(member); + } + + protected void stopMembership(Member[] members) { + if (members.length == 0 ) { + return; + } + Member localmember = service.getLocalMember(false); + localmember.setCommand(Member.SHUTDOWN_PAYLOAD); + MemberMessage msg = new MemberMessage(membershipId, MemberMessage.MSG_STOP, localmember); + try { + channel.send(members, msg, sendOptions); + } catch (ChannelException e) { + log.error(sm.getString("staticMembershipProvider.stopMembership.sendFailed"), e); + } + } + + @Override + public void messageReceived(Serializable msg, Member sender) { + MemberMessage memMsg = (MemberMessage) msg; + Member member = memMsg.getMember(); + if (memMsg.getMsgtype() == MemberMessage.MSG_START) { + memberAdded(member); + } else if (memMsg.getMsgtype() == MemberMessage.MSG_STOP) { + memberDisappeared(member); + } else if (memMsg.getMsgtype() == MemberMessage.MSG_PING) { + memberAlive(member); + } + } + + @Override + public boolean accept(Serializable msg, Member sender) { + boolean result = false; + if (msg instanceof MemberMessage) { + result = Arrays.equals(this.membershipId, ((MemberMessage) msg).getMembershipId()); + } + return result; + } + + @Override + public Serializable replyRequest(Serializable msg, final Member sender) { + if (!(msg instanceof MemberMessage)) { + return null; + } + MemberMessage memMsg = (MemberMessage) msg; + if (memMsg.getMsgtype() == MemberMessage.MSG_START) { + messageReceived(memMsg, sender); + memMsg.setMember(service.getLocalMember(true)); + return memMsg; + } else if (memMsg.getMsgtype() == MemberMessage.MSG_PING) { + messageReceived(memMsg, sender); + memMsg.setMember(service.getLocalMember(true)); + return memMsg; + } else { + // other messages are ignored. + if (log.isInfoEnabled()) { + log.info(sm.getString("staticMembershipProvider.replyRequest.ignored", + memMsg.getTypeDesc())); + } + return null; + } + } + + @Override + public void leftOver(Serializable msg, Member sender) { + if (!(msg instanceof MemberMessage)) { + return; + } + MemberMessage memMsg = (MemberMessage) msg; + if (memMsg.getMsgtype() == MemberMessage.MSG_START) { + messageReceived(memMsg, sender); + } else if (memMsg.getMsgtype() == MemberMessage.MSG_PING) { + messageReceived(memMsg, sender); + } else { + // other messages are ignored. + if (log.isInfoEnabled()) { + log.info(sm.getString("staticMembershipProvider.leftOver.ignored", + memMsg.getTypeDesc())); + } + } + } + + @Override + public void heartbeat() { + try { + if (!useThread) { + ping(); + } + } catch (ChannelException e) { + log.warn(sm.getString("staticMembershipProvider.heartbeat.failed"), e); + } + } + + protected void ping() throws ChannelException { + // send ping + Member[] members = getAliveMembers(staticMembers.toArray(new Member[0])); + if (members.length > 0) { + try { + MemberMessage msg = new MemberMessage(membershipId, MemberMessage.MSG_PING, service.getLocalMember(true)); + Response[] resp = rpcChannel.send(members, msg, RpcChannel.ALL_REPLY, sendOptions, rpcTimeout); + for (Response response : resp) { + messageReceived(response.getMessage(), response.getSource()); + } + } catch (ChannelException ce) { + // Handle known failed members + FaultyMember[] faultyMembers = ce.getFaultyMembers(); + for (FaultyMember faultyMember : faultyMembers) { + memberDisappeared(faultyMember.getMember()); + } + throw ce; + } + } + // expire + checkExpired(); + } + + protected void checkExpired() { + Member[] expired = membership.expire(expirationTime); + for (Member member : expired) { + membershipListener.memberDisappeared(member); + } + } + + public void setChannel(Channel channel) { + this.channel = channel; + } + + public void setStaticMembers(ArrayList staticMembers) { + this.staticMembers = staticMembers; + } + + private Member[] getAliveMembers(Member[] members) { + List aliveMembers = new ArrayList<>(); + for (Member member : members) { + try (Socket socket = new Socket()) { + InetAddress ia = InetAddress.getByAddress(member.getHost()); + InetSocketAddress addr = new InetSocketAddress(ia, member.getPort()); + socket.connect(addr, connectTimeout); + aliveMembers.add(member); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // Otherwise ignore + } + } + return aliveMembers.toArray(new Member[0]); + } + + // ------------------------------------------------------------------------------ + // member message to send to and from other memberships + // ------------------------------------------------------------------------------ + public static class MemberMessage implements Serializable { + private static final long serialVersionUID = 1L; + public static final int MSG_START = 1; + public static final int MSG_STOP = 2; + public static final int MSG_PING = 3; + private final int msgtype; + private final byte[] membershipId; + private Member member; + + public MemberMessage(byte[] membershipId, int msgtype, Member member) { + this.membershipId = membershipId; + this.msgtype = msgtype; + this.member = member; + } + + public int getMsgtype() { + return msgtype; + } + + public byte[] getMembershipId() { + return membershipId; + } + + public Member getMember() { + return member; + } + + public void setMember(Member local) { + this.member = local; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("MemberMessage["); + buf.append("name="); + buf.append(new String(membershipId)); + buf.append("; type="); + buf.append(getTypeDesc()); + buf.append("; member="); + buf.append(member); + buf.append(']'); + return buf.toString(); + } + + protected String getTypeDesc() { + switch (msgtype) { + case MSG_START: + return "MSG_START"; + case MSG_STOP: + return "MSG_STOP"; + case MSG_PING: + return "MSG_PING"; + default: + return "UNKNOWN"; + } + } + } + + protected class PingThread extends Thread { + @Override + public void run() { + while (running) { + try { + sleep(pingInterval); + ping(); + }catch (InterruptedException ix) { + }catch (Exception x) { + log.warn(sm.getString("staticMembershipProvider.pingThread.failed"),x); + } + } + } + } +} diff --git a/java/org/apache/catalina/tribes/membership/StaticMembershipService.java b/java/org/apache/catalina/tribes/membership/StaticMembershipService.java new file mode 100644 index 0000000..b82a05b --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/StaticMembershipService.java @@ -0,0 +1,256 @@ +/* + * 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.catalina.tribes.membership; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Properties; + +import javax.management.ObjectName; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipProvider; +import org.apache.catalina.tribes.jmx.JmxRegistry; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class StaticMembershipService extends MembershipServiceBase + implements StaticMembershipServiceMBean { + + private static final Log log = LogFactory.getLog(StaticMembershipService.class); + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + protected final ArrayList staticMembers = new ArrayList<>(); + private StaticMember localMember; + private StaticMembershipProvider provider; + + /** + * the ObjectName of this MembershipService. + */ + private ObjectName oname = null; + + public StaticMembershipService() { + //default values + setDefaults(this.properties); + } + + @Override + public void start(int level) throws Exception { + if (provider != null) { + provider.start(level); + return; + } + localMember.setServiceStartTime(System.currentTimeMillis()); + localMember.setMemberAliveTime(100); + // build membership provider + if (provider == null) { + provider = buildMembershipProvider(); + } + provider.start(level); + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + this.oname = jmxRegistry.registerJmx(",component=Membership", this); + } + } + + protected StaticMembershipProvider buildMembershipProvider() throws Exception { + StaticMembershipProvider provider = new StaticMembershipProvider(); + provider.setChannel(channel); + provider.setMembershipListener(this); + provider.setMembershipService(this); + provider.setStaticMembers(staticMembers); + properties.setProperty("membershipName", getMembershipName()); + provider.init(properties); + return provider; + } + + @Override + public void stop(int level) { + try { + if (provider != null && provider.stop(level)) { + if (oname != null) { + JmxRegistry.getRegistry(channel).unregisterJmx(oname); + oname = null; + } + provider = null; + channel = null; + } + } catch (Exception e) { + log.error(sm.getString("staticMembershipService.stopFail", Integer.valueOf(level)), e); + } + } + + @Override + public Member getLocalMember(boolean incAliveTime) { + if ( incAliveTime && localMember != null) { + localMember.setMemberAliveTime(System.currentTimeMillis()-localMember.getServiceStartTime()); + } + return localMember; + } + + @Override + public void setLocalMemberProperties(String listenHost, int listenPort, + int securePort, int udpPort) { + properties.setProperty("tcpListenHost", listenHost); + properties.setProperty("tcpListenPort", String.valueOf(listenPort)); + try { + findLocalMember(); + localMember.setHostname(listenHost); + localMember.setPort(listenPort); + localMember.setSecurePort(securePort); + localMember.setUdpPort(udpPort); + localMember.getData(true, true); + } catch (IOException x) { + throw new IllegalArgumentException(x); + } + } + + @Override + public void setPayload(byte[] payload) { + // no-op + } + + @Override + public void setDomain(byte[] domain) { + // no-op + } + + @Override + public MembershipProvider getMembershipProvider() { + return provider; + } + + public ArrayList getStaticMembers() { + return staticMembers; + } + + public void addStaticMember(StaticMember member) { + staticMembers.add(member); + } + + public void removeStaticMember(StaticMember member) { + staticMembers.remove(member); + } + + public void setLocalMember(StaticMember member) { + this.localMember = member; + localMember.setLocal(true); + } + + @Override + public long getExpirationTime() { + String expirationTime = properties.getProperty("expirationTime"); + return Long.parseLong(expirationTime); + } + + public void setExpirationTime(long expirationTime) { + properties.setProperty("expirationTime", String.valueOf(expirationTime)); + } + + @Override + public int getConnectTimeout() { + String connectTimeout = properties.getProperty("connectTimeout"); + return Integer.parseInt(connectTimeout); + } + + public void setConnectTimeout(int connectTimeout) { + properties.setProperty("connectTimeout", String.valueOf(connectTimeout)); + } + + @Override + public long getRpcTimeout() { + String rpcTimeout = properties.getProperty("rpcTimeout"); + return Long.parseLong(rpcTimeout); + } + + public void setRpcTimeout(long rpcTimeout) { + properties.setProperty("rpcTimeout", String.valueOf(rpcTimeout)); + } + + @Override + public boolean getUseThread() { + String useThread = properties.getProperty("useThread"); + return Boolean.parseBoolean(useThread); + } + + public void setUseThread(boolean useThread) { + properties.setProperty("useThread", String.valueOf(useThread)); + } + + @Override + public long getPingInterval() { + String pingInterval = properties.getProperty("pingInterval"); + return Long.parseLong(pingInterval); + } + + public void setPingInterval(long pingInterval) { + properties.setProperty("pingInterval", String.valueOf(pingInterval)); + } + + @Override + public void setProperties(Properties properties) { + setDefaults(properties); + this.properties = properties; + } + + protected void setDefaults(Properties properties) { + // default values + if (properties.getProperty("expirationTime") == null) { + properties.setProperty("expirationTime","5000"); + } + if (properties.getProperty("connectTimeout") == null) { + properties.setProperty("connectTimeout","500"); + } + if (properties.getProperty("rpcTimeout") == null) { + properties.setProperty("rpcTimeout","3000"); + } + if (properties.getProperty("useThread") == null) { + properties.setProperty("useThread","false"); + } + if (properties.getProperty("pingInterval") == null) { + properties.setProperty("pingInterval","1000"); + } + } + + private String getMembershipName() { + return channel.getName()+"-"+"StaticMembership"; + } + + private void findLocalMember() throws IOException { + if (this.localMember != null) { + return; + } + String listenHost = properties.getProperty("tcpListenHost"); + String listenPort = properties.getProperty("tcpListenPort"); + + // find local member from static members + for (StaticMember staticMember : this.staticMembers) { + if (Arrays.equals(InetAddress.getByName(listenHost).getAddress(), staticMember.getHost()) + && Integer.parseInt(listenPort) == staticMember.getPort()) { + this.localMember = staticMember; + break; + } + } + if (this.localMember == null) { + throw new IllegalStateException(sm.getString("staticMembershipService.noLocalMember")); + } + staticMembers.remove(this.localMember); + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/membership/StaticMembershipServiceMBean.java b/java/org/apache/catalina/tribes/membership/StaticMembershipServiceMBean.java new file mode 100644 index 0000000..d6a2721 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/StaticMembershipServiceMBean.java @@ -0,0 +1,44 @@ +/* + * 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.catalina.tribes.membership; + +import java.util.Properties; + +import org.apache.catalina.tribes.Member; + +public interface StaticMembershipServiceMBean { + + // Attributes + long getExpirationTime(); + + int getConnectTimeout(); + + long getRpcTimeout(); + + boolean getUseThread(); + + long getPingInterval(); + + // Operation + Properties getProperties(); + + boolean hasMembers(); + + String[] getMembersByName(); + + Member findMemberByName(String name); +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java b/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java new file mode 100644 index 0000000..68785e3 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java @@ -0,0 +1,150 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLConnection; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public abstract class AbstractStreamProvider implements StreamProvider { + private static final Log log = LogFactory.getLog(AbstractStreamProvider.class); + protected static final StringManager sm = StringManager.getManager(AbstractStreamProvider.class); + + protected static final TrustManager[] INSECURE_TRUST_MANAGERS = new TrustManager[] { + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + + /** + * @return the socket factory, or null if not needed + */ + protected abstract SSLSocketFactory getSocketFactory(); + + /** + * Open URL connection to the specified URL. + * @param url the url + * @param headers the headers map + * @param connectTimeout connection timeout in ms + * @param readTimeout read timeout in ms + * @return the URL connection + * @throws IOException when an error occurs + */ + public URLConnection openConnection(String url, Map headers, int connectTimeout, int readTimeout) throws IOException { + if (log.isDebugEnabled()) { + log.debug(sm.getString("abstractStream.connection", + getClass().getSimpleName(), url, headers, Integer.toString(connectTimeout), Integer.toString(readTimeout))); + } + URLConnection connection; + try { + connection = new URI(url).toURL().openConnection(); + } catch (URISyntaxException | IllegalArgumentException e) { + // Not ideal but consistent with API + throw new IOException(e); + } + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + connection.addRequestProperty(entry.getKey(), entry.getValue()); + } + } + if (connectTimeout < 0 || readTimeout < 0) { + throw new IllegalArgumentException(sm.getString("abstractStream.invalidTimeout", + Integer.toString(connectTimeout), Integer.toString(readTimeout))); + } + connection.setConnectTimeout(connectTimeout); + connection.setReadTimeout(readTimeout); + return connection; + } + + @Override + public InputStream openStream(String url, Map headers, + int connectTimeout, int readTimeout) throws IOException { + URLConnection connection = openConnection(url, headers, connectTimeout, readTimeout); + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(getSocketFactory()); + if (log.isTraceEnabled()) { + log.trace(String.format("Using HttpsURLConnection with SSLSocketFactory [%s] for url [%s].", getSocketFactory(), url)); + } + } else { + if (log.isTraceEnabled()) { + log.trace(String.format("Using URLConnection for url [%s].", url)); + } + } + return connection.getInputStream(); + } + + protected static TrustManager[] configureCaCert(String caCertFile) throws Exception { + if (caCertFile != null) { + try (InputStream pemInputStream = new BufferedInputStream(new FileInputStream(caCertFile))) { + CertificateFactory certFactory = CertificateFactory.getInstance("X509"); + + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(null); + + Collection c = certFactory.generateCertificates(pemInputStream); + for (Certificate certificate : c) { + X509Certificate cert = (X509Certificate) certificate; + String alias = cert.getSubjectX500Principal().getName(); + trustStore.setCertificateEntry(alias, cert); + } + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + return trustManagerFactory.getTrustManagers(); + } catch (FileNotFoundException fnfe) { + log.error(sm.getString("abstractStream.fileNotFound", caCertFile)); + throw fnfe; + } catch (Exception e) { + log.error(sm.getString("abstractStream.trustManagerError", caCertFile)); + throw e; + } + } else { + log.warn(sm.getString("abstractStream.CACertUndefined")); + return INSECURE_TRUST_MANAGERS; + } + } +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/CertificateStreamProvider.java b/java/org/apache/catalina/tribes/membership/cloud/CertificateStreamProvider.java new file mode 100644 index 0000000..c5777d1 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/CertificateStreamProvider.java @@ -0,0 +1,82 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.jsse.PEMFile; + +public class CertificateStreamProvider extends AbstractStreamProvider { + + private static final Log log = LogFactory.getLog(CertificateStreamProvider.class); + + private final SSLSocketFactory factory; + + CertificateStreamProvider(String clientCertFile, String clientKeyFile, String clientKeyPassword, String clientKeyAlgo, String caCertFile) throws Exception { + char[] password = (clientKeyPassword != null) ? clientKeyPassword.toCharArray() : new char[0]; + KeyManager[] keyManagers = configureClientCert(clientCertFile, clientKeyFile, password, clientKeyAlgo); + TrustManager[] trustManagers = configureCaCert(caCertFile); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(keyManagers, trustManagers, null); + factory = context.getSocketFactory(); + } + + @Override + protected SSLSocketFactory getSocketFactory() { + return factory; + } + + private static KeyManager[] configureClientCert(String clientCertFile, String clientKeyFile, char[] clientKeyPassword, String clientKeyAlgo) throws Exception { + try (InputStream certInputStream = new FileInputStream(clientCertFile)) { + CertificateFactory certFactory = CertificateFactory.getInstance("X509"); + X509Certificate cert = (X509Certificate)certFactory.generateCertificate(certInputStream); + + PEMFile pemFile = new PEMFile(clientKeyFile, new String(clientKeyPassword), clientKeyAlgo); + PrivateKey privKey = pemFile.getPrivateKey(); + + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + + String alias = cert.getSubjectX500Principal().getName(); + keyStore.setKeyEntry(alias, privKey, clientKeyPassword, new Certificate[]{cert}); + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, clientKeyPassword); + + return keyManagerFactory.getKeyManagers(); + } catch (IOException e) { + log.error(sm.getString("certificateStream.clientCertError", clientCertFile, clientKeyFile)); + throw e; + } + } + +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipProvider.java b/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipProvider.java new file mode 100644 index 0000000..fe5e193 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipProvider.java @@ -0,0 +1,186 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.IOException; +import java.io.Serializable; +import java.net.InetAddress; +import java.security.AccessController; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedAction; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.Heartbeat; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.membership.Membership; +import org.apache.catalina.tribes.membership.MembershipProviderBase; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public abstract class CloudMembershipProvider extends MembershipProviderBase implements Heartbeat, ChannelListener { + private static final Log log = LogFactory.getLog(CloudMembershipProvider.class); + protected static final StringManager sm = StringManager.getManager(CloudMembershipProvider.class); + + protected static final String CUSTOM_ENV_PREFIX = "OPENSHIFT_KUBE_PING_"; + + protected String url; + protected StreamProvider streamProvider; + protected int connectionTimeout; + protected int readTimeout; + + protected Instant startTime; + protected MessageDigest md5; + + protected Map headers = new HashMap<>(); + + protected String localIp; + protected int port; + + protected long expirationTime = 5000; + + public CloudMembershipProvider() { + try { + md5 = MessageDigest.getInstance("md5"); + } catch (NoSuchAlgorithmException e) { + // Ignore + } + } + + /** + * Get value of environment variable. + * @param keys the environment variables + * @return the env variables values, or null if not found + */ + protected static String getEnv(String... keys) { + String val = null; + for (String key : keys) { + val = AccessController.doPrivileged((PrivilegedAction) () -> System.getenv(key)); + if (val != null) { + break; + } + } + return val; + } + + /** + * Get the Kubernetes namespace, or "tomcat" if the Kubernetes environment variable + * cannot be found (with a warning log about the missing namespace). + * @return the namespace + */ + protected String getNamespace() { + String namespace = getEnv(CUSTOM_ENV_PREFIX + "NAMESPACE", "KUBERNETES_NAMESPACE"); + if (namespace == null || namespace.length() == 0) { + log.warn(sm.getString("kubernetesMembershipProvider.noNamespace")); + namespace = "tomcat"; + } + return namespace; + } + + @Override + public void init(Properties properties) throws IOException { + startTime = Instant.now(); + + CloudMembershipService service = (CloudMembershipService) this.service; + connectionTimeout = service.getConnectTimeout(); + readTimeout = service.getReadTimeout(); + expirationTime = service.getExpirationTime(); + + localIp = InetAddress.getLocalHost().getHostAddress(); + port = Integer.parseInt(properties.getProperty("tcpListenPort")); + } + + @Override + public void start(int level) throws Exception { + if (membership == null) { + membership = new Membership(service.getLocalMember(true)); + } + service.getChannel().addChannelListener(this); + } + + @Override + public boolean stop(int level) throws Exception { + return true; + } + + @Override + public void heartbeat() { + Member[] announcedMembers = fetchMembers(); + // Add new members or refresh the members in the membership + for (Member member : announcedMembers) { + updateMember(member, true); + } + // Remove non refreshed members from the membership + Member[] expired = membership.expire(expirationTime); + for (Member member : expired) { + updateMember(member, false); + } + } + + /** + * Fetch current cluster members from the cloud orchestration. + * @return the member array + */ + protected abstract Member[] fetchMembers(); + + /** + * Add or remove specified member. + * @param member the member to add + * @param add true if the member is added, false otherwise + */ + protected void updateMember(Member member, boolean add) { + if (add && !membership.memberAlive(member)) { + return; + } + if (log.isDebugEnabled()) { + String message = add ? sm.getString("cloudMembershipProvider.add", member) + : sm.getString("cloudMembershipProvider.remove", member); + log.debug(message); + } + Runnable r = () -> { + Thread currentThread = Thread.currentThread(); + String name = currentThread.getName(); + try { + String threadName = add ? "CloudMembership-memberAdded" : "CloudMembership-memberDisappeared"; + currentThread.setName(threadName); + if (add) { + membershipListener.memberAdded(member); + } else { + membershipListener.memberDisappeared(member); + } + } finally { + currentThread.setName(name); + } + }; + executor.execute(r); + } + + @Override + public void messageReceived(Serializable msg, Member sender) { + } + + @Override + public boolean accept(Serializable msg, Member sender) { + return false; + } + +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipService.java b/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipService.java new file mode 100644 index 0000000..19c5e72 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipService.java @@ -0,0 +1,273 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.IOException; + +import javax.management.ObjectName; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipProvider; +import org.apache.catalina.tribes.jmx.JmxRegistry; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.membership.MembershipServiceBase; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A {@link org.apache.catalina.tribes.MembershipService} that uses Kubernetes API(default) or DNS to retrieve + * the members of a cluster.
    + *

    + * The default implementation of the MembershipProvider component is the {@link KubernetesMembershipProvider}. + * The MembershipProvider can be configured by the membershipProviderClassName property. + * Possible shortcuts are {@code kubernetes} and {@code dns}. For dns look at the {@link DNSMembershipProvider}. + *

    + *

    + * Configuration example + *

    + * + * {@code server.xml } + * + *
    + * {@code
    + * 
    + *           
    + *             
    + *           
    + *         
    + *         ...
    + *  }
    + *  
    + * + */ + +public class CloudMembershipService extends MembershipServiceBase + implements CloudMembershipServiceMBean { + + private static final Log log = LogFactory.getLog(CloudMembershipService.class); + protected static final StringManager sm = StringManager.getManager(CloudMembershipService.class); + + public static final String MEMBERSHIP_PROVIDER_CLASS_NAME = "membershipProviderClassName"; + private static final String KUBE = "kubernetes"; + private static final String DNS = "dns"; + private static final String KUBE_PROVIDER_CLASS = "org.apache.catalina.tribes.membership.cloud.KubernetesMembershipProvider"; + private static final String DNS_PROVIDER_CLASS = "org.apache.catalina.tribes.membership.cloud.DNSMembershipProvider"; + protected static final byte[] INITIAL_ID = new byte[16]; + + private MembershipProvider membershipProvider; + private MemberImpl localMember; + + private byte[] payload; + private byte[] domain; + + private ObjectName oname = null; + + /** + * Return a property. + * @param name the property name + * @return the property value + */ + public Object getProperty(String name) { + return properties.getProperty(name); + } + + /** + * Set a property. + * @param name the property name + * @param value the property value + * @return true if the property was successfully set + */ + public boolean setProperty(String name, String value) { + return (properties.setProperty(name, value) == null); + } + + /** + * Return the membership provider class. + * @return the classname + */ + public String getMembershipProviderClassName() { + return properties.getProperty(MEMBERSHIP_PROVIDER_CLASS_NAME); + } + + /** + * Set the membership provider class. + * @param membershipProviderClassName the class name + */ + public void setMembershipProviderClassName(String membershipProviderClassName) { + properties.setProperty(MEMBERSHIP_PROVIDER_CLASS_NAME, membershipProviderClassName); + } + + @Override + public void start(int level) throws Exception { + if ((level & MBR_RX) == 0) { + return; + } + + createOrUpdateLocalMember(); + localMember.setServiceStartTime(System.currentTimeMillis()); + localMember.setMemberAliveTime(100); + localMember.setPayload(payload); + localMember.setDomain(domain); + + if (membershipProvider == null) { + String provider = getMembershipProviderClassName(); + if (provider == null || KUBE.equals(provider)) { + provider = KUBE_PROVIDER_CLASS; + } else if (DNS.equals(provider)) { + provider = DNS_PROVIDER_CLASS; + } + if (log.isTraceEnabled()) { + log.trace("Using membershipProvider: " + provider); + } + membershipProvider = + (MembershipProvider) Class.forName(provider).getConstructor().newInstance(); + membershipProvider.setMembershipListener(this); + membershipProvider.setMembershipService(this); + membershipProvider.init(properties); + } + membershipProvider.start(level); + + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + oname = jmxRegistry.registerJmx(",component=Membership", this); + } + } + + @Override + public void stop(int level) { + try { + if (membershipProvider != null && membershipProvider.stop(level)) { + if (oname != null) { + JmxRegistry.getRegistry(channel).unregisterJmx(oname); + oname = null; + } + membershipProvider = null; + channel = null; + } + } catch (Exception e) { + log.error(sm.getString("cloudMembershipService.stopFail", Integer.valueOf(level)), e); + } + } + + @Override + public Member getLocalMember(boolean incAliveTime) { + if (incAliveTime && localMember != null) { + localMember.setMemberAliveTime(System.currentTimeMillis() - localMember.getServiceStartTime()); + } + return localMember; + } + + @Override + public void setLocalMemberProperties(String listenHost, int listenPort, int securePort, int udpPort) { + if (log.isTraceEnabled()) { + log.trace(String.format("setLocalMemberProperties(%s, %d, %d, %d)", listenHost, + Integer.valueOf(listenPort), Integer.valueOf(securePort), Integer.valueOf(udpPort))); + } + properties.setProperty("tcpListenHost", listenHost); + properties.setProperty("tcpListenPort", String.valueOf(listenPort)); + properties.setProperty("udpListenPort", String.valueOf(udpPort)); + properties.setProperty("tcpSecurePort", String.valueOf(securePort)); + + try { + createOrUpdateLocalMember(); + localMember.setPayload(payload); + localMember.setDomain(domain); + localMember.getData(true, true); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + private void createOrUpdateLocalMember() throws IOException { + String host = properties.getProperty("tcpListenHost"); + int port = Integer.parseInt(properties.getProperty("tcpListenPort")); + int securePort = Integer.parseInt(properties.getProperty("tcpSecurePort")); + int udpPort = Integer.parseInt(properties.getProperty("udpListenPort")); + + if (localMember == null) { + localMember = new MemberImpl(); + localMember.setUniqueId(INITIAL_ID); + localMember.setLocal(true); + } + localMember.setHostname(host); + localMember.setPort(port); + localMember.setSecurePort(securePort); + localMember.setUdpPort(udpPort); + localMember.getData(true, true); + } + + @Override + public void setPayload(byte[] payload) { + this.payload = payload; + if (localMember != null) { + localMember.setPayload(payload); + } + } + + @Override + public void setDomain(byte[] domain) { + this.domain = domain; + if (localMember != null) { + localMember.setDomain(domain); + } + } + + @Override + public MembershipProvider getMembershipProvider() { + return membershipProvider; + } + + public void setMembershipProvider(MembershipProvider memberProvider) { + this.membershipProvider = memberProvider; + } + + @Override + public int getConnectTimeout() { + return Integer.parseInt(properties.getProperty("connectTimeout", "1000")); + } + + public void setConnectTimeout(int connectTimeout) { + properties.setProperty("connectTimeout", String.valueOf(connectTimeout)); + } + + @Override + public int getReadTimeout() { + return Integer.parseInt(properties.getProperty("readTimeout", "1000")); + } + + public void setReadTimeout(int readTimeout) { + properties.setProperty("readTimeout", String.valueOf(readTimeout)); + } + + @Override + public long getExpirationTime() { + return Long.parseLong(properties.getProperty("expirationTime", "5000")); + } + + public void setExpirationTime(long expirationTime) { + properties.setProperty("expirationTime", String.valueOf(expirationTime)); + } +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipServiceMBean.java b/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipServiceMBean.java new file mode 100644 index 0000000..7817c55 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/CloudMembershipServiceMBean.java @@ -0,0 +1,40 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.util.Properties; + +import org.apache.catalina.tribes.Member; + +public interface CloudMembershipServiceMBean { + + // Attributes + int getConnectTimeout(); + + int getReadTimeout(); + + long getExpirationTime(); + + // Operation + Properties getProperties(); + + boolean hasMembers(); + + String[] getMembersByName(); + + Member findMemberByName(String name); +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/DNSMembershipProvider.java b/java/org/apache/catalina/tribes/membership/cloud/DNSMembershipProvider.java new file mode 100644 index 0000000..9822487 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/DNSMembershipProvider.java @@ -0,0 +1,195 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.IOException; +import java.io.Serializable; +import java.net.InetAddress; +import java.net.URLEncoder; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipService; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A {@link org.apache.catalina.tribes.MembershipProvider} that uses DNS to retrieve the members of a cluster.
    + * + *

    + * Configuration example for Kubernetes + *

    + * + * {@code server.xml } + * + *
    + * {@code
    + * 
    + *           
    + *             
    + *           
    + *         
    + *         ...
    + *  }
    + *  
    + * + * {@code dns-membership-service.yml } + * + *
    + * {@code
    + * apiVersion: v1
    + * kind: Service
    + * metadata:
    + *   annotations:
    + *     service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
    + *     description: "The service for tomcat cluster membership."
    + *   name: my-tomcat-app-membership
    + * spec:
    + *   clusterIP: None
    + *   ports:
    + *   - name: membership
    + *     port: 8888
    + *   selector:
    + *     app: my-tomcat-app
    + * }
    + * 
    + * + * Environment variable configuration
    + * + * {@code DNS_MEMBERSHIP_SERVICE_NAME=my-tomcat-app-membership } + */ + +public class DNSMembershipProvider extends CloudMembershipProvider { + private static final Log log = LogFactory.getLog(DNSMembershipProvider.class); + + private String dnsServiceName; + + @Override + public void start(int level) throws Exception { + if ((level & MembershipService.MBR_RX) == 0) { + return; + } + + super.start(level); + + // Set up Kubernetes API parameters + dnsServiceName = getEnv("DNS_MEMBERSHIP_SERVICE_NAME"); + if (dnsServiceName == null) { + dnsServiceName = getNamespace(); + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("cloudMembershipProvider.start", dnsServiceName)); + } + dnsServiceName = URLEncoder.encode(dnsServiceName, "UTF-8"); + + // Fetch initial members + heartbeat(); + } + + @Override + public boolean stop(int level) throws Exception { + return super.stop(level); + } + + @Override + protected Member[] fetchMembers() { + List members = new ArrayList<>(); + + InetAddress[] inetAddresses = null; + try { + inetAddresses = InetAddress.getAllByName(dnsServiceName); + } catch (UnknownHostException exception) { + log.warn(sm.getString("dnsMembershipProvider.dnsError", dnsServiceName), exception); + } + + if (inetAddresses != null) { + for (InetAddress inetAddress : inetAddresses) { + String ip = inetAddress.getHostAddress(); + byte[] id = md5.digest(ip.getBytes()); + // We found ourselves, ignore + if (ip.equals(localIp)) { + // Update the UID on initial lookup + Member localMember = service.getLocalMember(false); + if (localMember.getUniqueId() == CloudMembershipService.INITIAL_ID && localMember instanceof MemberImpl) { + ((MemberImpl) localMember).setUniqueId(id); + } + continue; + } + long aliveTime = -1; + MemberImpl member = null; + try { + member = new MemberImpl(ip, port, aliveTime); + } catch (IOException e) { + log.error(sm.getString("kubernetesMembershipProvider.memberError"), e); + continue; + } + member.setUniqueId(id); + members.add(member); + } + } + + return members.toArray(new Member[0]); + } + + @Override + public boolean accept(Serializable msg, Member sender) { + // Check if the sender is in the member list. + boolean found = false; + Member[] members = membership.getMembers(); + if (members != null) { + for (Member member : members) { + if (Arrays.equals(sender.getHost(), member.getHost()) + && sender.getPort() == member.getPort()) { + found = true; + break; + } + } + } + if (!found) { + MemberImpl member = new MemberImpl(); + member.setHost(sender.getHost()); + member.setPort(sender.getPort()); + byte[] host = sender.getHost(); + int i = 0; + StringBuilder buf = new StringBuilder(); + buf.append(host[i++] & 0xff); + for (; i < host.length; i++) { + buf.append('.').append(host[i] & 0xff); + } + + byte[] id = md5.digest(buf.toString().getBytes()); + member.setUniqueId(id); + member.setMemberAliveTime(-1); + updateMember(member, true); + } + return false; + } +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/InsecureStreamProvider.java b/java/org/apache/catalina/tribes/membership/cloud/InsecureStreamProvider.java new file mode 100644 index 0000000..f769245 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/InsecureStreamProvider.java @@ -0,0 +1,35 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +public class InsecureStreamProvider extends AbstractStreamProvider { + private final SSLSocketFactory factory; + + InsecureStreamProvider() throws Exception { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, INSECURE_TRUST_MANAGERS, null); + factory = context.getSocketFactory(); + } + + @Override + protected SSLSocketFactory getSocketFactory() { + return factory; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java b/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java new file mode 100644 index 0000000..6f0f4f2 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java @@ -0,0 +1,239 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipService; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.json.JSONParser; + +/** + * A {@link org.apache.catalina.tribes.MembershipProvider} that uses Kubernetes API to retrieve the members of a cluster.
    + * + */ + +public class KubernetesMembershipProvider extends CloudMembershipProvider { + private static final Log log = LogFactory.getLog(KubernetesMembershipProvider.class); + + @Override + public void start(int level) throws Exception { + if ((level & MembershipService.MBR_RX) == 0) { + return; + } + + super.start(level); + + // Set up Kubernetes API parameters + String namespace = getNamespace(); + + if (log.isDebugEnabled()) { + log.debug(sm.getString("cloudMembershipProvider.start", namespace)); + } + + String protocol = getEnv(CUSTOM_ENV_PREFIX + "MASTER_PROTOCOL", "KUBERNETES_MASTER_PROTOCOL"); + String masterHost = getEnv(CUSTOM_ENV_PREFIX + "MASTER_HOST", "KUBERNETES_SERVICE_HOST"); + String masterPort = getEnv(CUSTOM_ENV_PREFIX + "MASTER_PORT", "KUBERNETES_SERVICE_PORT"); + + String clientCertificateFile = getEnv(CUSTOM_ENV_PREFIX + "CLIENT_CERT_FILE", "KUBERNETES_CLIENT_CERTIFICATE_FILE"); + String caCertFile = getEnv(CUSTOM_ENV_PREFIX + "CA_CERT_FILE", "KUBERNETES_CA_CERTIFICATE_FILE"); + if (caCertFile == null) { + caCertFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; + } + + if (clientCertificateFile == null) { + if (protocol == null) { + protocol = "https"; + } + String saTokenFile = getEnv(CUSTOM_ENV_PREFIX + "SA_TOKEN_FILE", "SA_TOKEN_FILE"); + if (saTokenFile == null) { + saTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"; + } + try { + byte[] bytes = Files.readAllBytes(FileSystems.getDefault().getPath(saTokenFile)); + streamProvider = new TokenStreamProvider(new String(bytes, StandardCharsets.US_ASCII), caCertFile); + } catch (IOException e) { + log.error(sm.getString("kubernetesMembershipProvider.streamError"), e); + } + } else { + if (protocol == null) { + protocol = "http"; + } + String clientKeyFile = getEnv("KUBERNETES_CLIENT_KEY_FILE"); + if (clientKeyFile == null) { + log.error(sm.getString("kubernetesMembershipProvider.noKey")); + return; + } + String clientKeyPassword = getEnv("KUBERNETES_CLIENT_KEY_PASSWORD"); + String clientKeyAlgo = getEnv("KUBERNETES_CLIENT_KEY_ALGO"); + if (clientKeyAlgo == null) { + clientKeyAlgo = "RSA"; + } + streamProvider = new CertificateStreamProvider(clientCertificateFile, clientKeyFile, clientKeyPassword, clientKeyAlgo, caCertFile); + } + + String ver = getEnv(CUSTOM_ENV_PREFIX + "API_VERSION", "KUBERNETES_API_VERSION"); + if (ver == null) { + ver = "v1"; + } + + String labels = getEnv(CUSTOM_ENV_PREFIX + "LABELS", "KUBERNETES_LABELS"); + + namespace = URLEncoder.encode(namespace, "UTF-8"); + labels = labels == null ? null : URLEncoder.encode(labels, "UTF-8"); + + url = String.format("%s://%s:%s/api/%s/namespaces/%s/pods", protocol, masterHost, masterPort, ver, namespace); + if (labels != null && labels.length() > 0) { + url = url + "?labelSelector=" + labels; + } + + // Fetch initial members + heartbeat(); + } + + @Override + public boolean stop(int level) throws Exception { + try { + return super.stop(level); + } finally { + streamProvider = null; + } + } + + @Override + protected Member[] fetchMembers() { + if (streamProvider == null) { + return new Member[0]; + } + + List members = new ArrayList<>(); + + try (InputStream stream = streamProvider.openStream(url, headers, connectionTimeout, readTimeout); + InputStreamReader reader = new InputStreamReader(stream, "UTF-8")) { + parsePods(reader, members); + } catch (IOException e) { + log.error(sm.getString("kubernetesMembershipProvider.streamError"), e); + } + + return members.toArray(new Member[0]); + } + + @SuppressWarnings("unchecked") + protected void parsePods(Reader reader, List members) { + JSONParser parser = new JSONParser(reader); + try { + LinkedHashMap json = parser.object(); + Object itemsObject = json.get("items"); + if (!(itemsObject instanceof List)) { + log.error(sm.getString("kubernetesMembershipProvider.invalidPodsList", "no items")); + return; + } + List items = (List) itemsObject; + for (Object podObject : items) { + if (!(podObject instanceof LinkedHashMap)) { + log.warn(sm.getString("kubernetesMembershipProvider.invalidPod", "item")); + continue; + } + LinkedHashMap pod = (LinkedHashMap) podObject; + // If there is a "kind", check it is "Pod" + Object podKindObject = pod.get("kind"); + if (podKindObject != null && !"Pod".equals(podKindObject)) { + continue; + } + // "metadata" contains "name", "uid" and "creationTimestamp" + Object metadataObject = pod.get("metadata"); + if (!(metadataObject instanceof LinkedHashMap)) { + log.warn(sm.getString("kubernetesMembershipProvider.invalidPod", "metadata")); + continue; + } + LinkedHashMap metadata = (LinkedHashMap) metadataObject; + Object nameObject = metadata.get("name"); + if (nameObject == null) { + log.warn(sm.getString("kubernetesMembershipProvider.invalidPod", "name")); + continue; + } + Object objectUid = metadata.get("uid"); + Object creationTimestampObject = metadata.get("creationTimestamp"); + if (creationTimestampObject == null) { + log.warn(sm.getString("kubernetesMembershipProvider.invalidPod", "uid")); + continue; + } + // "status" contains "phase" (which must be "Running") and "podIP" + Object statusObject = pod.get("status"); + if (!(statusObject instanceof LinkedHashMap)) { + log.warn(sm.getString("kubernetesMembershipProvider.invalidPod", "status")); + continue; + } + LinkedHashMap status = (LinkedHashMap) statusObject; + if (!"Running".equals(status.get("phase"))) { + continue; + } + Object podIPObject = status.get("podIP"); + if (podIPObject == null) { + log.warn(sm.getString("kubernetesMembershipProvider.invalidPod", "podIP")); + continue; + } + String podIP = podIPObject.toString(); + String uid = (objectUid == null) ? podIP : objectUid.toString(); + + // We found ourselves, ignore + if (podIP.equals(localIp)) { + // Update the UID on initial lookup + Member localMember = service.getLocalMember(false); + if (localMember.getUniqueId() == CloudMembershipService.INITIAL_ID && localMember instanceof MemberImpl) { + byte[] id = md5.digest(uid.getBytes(StandardCharsets.US_ASCII)); + ((MemberImpl) localMember).setUniqueId(id); + } + continue; + } + + long aliveTime = Duration.between(Instant.parse(creationTimestampObject.toString()), startTime).toMillis(); + + MemberImpl member = null; + try { + member = new MemberImpl(podIP, port, aliveTime); + } catch (IOException e) { + // Shouldn't happen: + // an exception is thrown if hostname can't be resolved to IP, but we already provide an IP + log.error(sm.getString("kubernetesMembershipProvider.memberError"), e); + continue; + } + byte[] id = md5.digest(uid.getBytes(StandardCharsets.US_ASCII)); + member.setUniqueId(id); + members.add(member); + } + } catch (Exception e) { + log.error(sm.getString("kubernetesMembershipProvider.jsonError"), e); + } + } + +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties new file mode 100644 index 0000000..77bc9bb --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties @@ -0,0 +1,40 @@ +# 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. + +abstractStream.CACertUndefined=CA cert file undefined +abstractStream.connection=[{0}] opening connection: url [{1}], headers [{2}], connectTimeout [{3}], readTimeout [{4}] +abstractStream.fileNotFound=CA cert file [{0}] not found +abstractStream.invalidTimeout=Neither connectTimeout [{0}] nor readTimeout [{1}] can be less than 0 for URLConnection +abstractStream.trustManagerError=Could not create trust manager for [{0}] + +certificateStream.clientCertError=Could not create key manager for [{0}] ([{1}]) + +cloudMembershipProvider.add=Member [{0}] added +cloudMembershipProvider.remove=Member [{0}] disappeared +cloudMembershipProvider.start=Namespace [{0}] set; clustering enabled + +cloudMembershipService.stopFail=Unable to stop the cloud membership service, level: [{0}] + +dnsMembershipProvider.dnsError=Error getting hosts address list for namespace [{0}] + +kubernetesMembershipProvider.invalidPod=Pod is missing some required attributes: [{0}] +kubernetesMembershipProvider.invalidPodsList=Invalid pods list: [{0}] +kubernetesMembershipProvider.jsonError=JSON error +kubernetesMembershipProvider.memberError=Error creating member +kubernetesMembershipProvider.noKey=Client certificate key was not specified in the environment +kubernetesMembershipProvider.noNamespace=Namespace not set +kubernetesMembershipProvider.streamError=Failed to open stream + +tokenStream.failedConnection=Failed connection to [{0}] with token [{1}] diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_cs.properties new file mode 100644 index 0000000..0b2dd1e --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_cs.properties @@ -0,0 +1,18 @@ +# 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. + +abstractStream.trustManagerError=NepodaÅ™ilo se vytvoÅ™it trust manager pro [{0}] + +cloudMembershipService.stopFail=Nelze zastavit službu cloudového Älenství, úroveň: [{0}] diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_de.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_de.properties new file mode 100644 index 0000000..5bfd780 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_de.properties @@ -0,0 +1,23 @@ +# 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. + +abstractStream.fileNotFound=CA Zertifikats Datei [{0}] nicht gefunden +abstractStream.trustManagerError=Konnte keinen Trust Manager für [{0}] erzeugen. + +certificateStream.clientCertError=Es konnte kein Schlüssel-Manager für [{0}] ([{1}]) erzeugt werden + +dnsMembershipProvider.dnsError=Fehler beim Empfang der Host Adressenliste für den Namespace [{0}] + +kubernetesMembershipProvider.invalidPod=Pod fehlen benötigte Attribute: [{0}] diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_es.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_es.properties new file mode 100644 index 0000000..6c74193 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_es.properties @@ -0,0 +1,21 @@ +# 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. + +abstractStream.fileNotFound=No se encontró el archivo del certificado CA [{0}] +abstractStream.trustManagerError=No se pudo crear el administrador de confianza para [{0}] + +cloudMembershipService.stopFail=No se pudo detener el servicio de miembros estáticos, nivel: [{0}] + +kubernetesMembershipProvider.invalidPod=Algunos atributos requeridos faltan en el Pod: [{0}] diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_fr.properties new file mode 100644 index 0000000..caf4505 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_fr.properties @@ -0,0 +1,40 @@ +# 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. + +abstractStream.CACertUndefined=Le fichier certificat CA n'est pas défini +abstractStream.connection=[{0}] ouverture de la connection: url [{1}], en têtes [{2}], connectTimeout [{3}], readTimeout [{4}] +abstractStream.fileNotFound=Le fichier [{0}] définissant l''autorité des certificats (CA) n''a pas été trouvé +abstractStream.invalidTimeout=Ni connectTimeout [{0}] ni readTimeout [{1}] ne peuvent être inférieurs à zéro pour l''URLConnection +abstractStream.trustManagerError=Echec de création d''un gestionnaire de confiance (trust manager) pour [{0}] + +certificateStream.clientCertError=Impossible de créer le gestionnaire de clés pour [{0}] ([{1}]) + +cloudMembershipProvider.add=Le membre [{0}] a été ajouté +cloudMembershipProvider.remove=Le membre [{0}] a disparu +cloudMembershipProvider.start=L''espace de noms [{0}] a été défini, le cluster est activé + +cloudMembershipService.stopFail=Impossible d''arrêter le registre de membres statique, niveau : [{0}] + +dnsMembershipProvider.dnsError=Erreur en obtenant la liste des adresses des hôtes pour l''espace de noms [{0}] + +kubernetesMembershipProvider.invalidPod=Le pod manque des attributs nécessaires: [{0}] +kubernetesMembershipProvider.invalidPodsList=La liste de pods est invalide : [{0}] +kubernetesMembershipProvider.jsonError=Erreur JSON +kubernetesMembershipProvider.memberError=Erreur de création d'un membre +kubernetesMembershipProvider.noKey=La clé du certificat client n'a pas été spécifiée dans l'environnement +kubernetesMembershipProvider.noNamespace=L'espace de noms n'est pas paramétré +kubernetesMembershipProvider.streamError=Échec d'ouverture du flux + +tokenStream.failedConnection=Impossible de se connecter à [{0}] avec le token [{1}] diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_ja.properties new file mode 100644 index 0000000..83622e1 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_ja.properties @@ -0,0 +1,40 @@ +# 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. + +abstractStream.CACertUndefined=CA 証明書ファイルãŒæœªå®šç¾©ã§ã™ã€‚ +abstractStream.connection=[{0}] 開始接続: url [{1}]ã€hearders [{2}]ã€connectTimeout [{3}]ã€readTimeout [{4}] +abstractStream.fileNotFound=CA 証明書ファイル [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +abstractStream.invalidTimeout=URLConnection ã® connectTimeout [{0}] ã‚‚ readTimeout [{1}] ã‚‚ 0 未満ã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +abstractStream.trustManagerError=[{0}] ã®ãƒˆãƒ©ã‚¹ãƒˆãƒžãƒãƒ¼ã‚¸ãƒ£ãƒ¼ã‚’作æˆã§ãã¾ã›ã‚“ã§ã—㟠+ +certificateStream.clientCertError=[{0}]([{1}])ã®ã‚­ãƒ¼ãƒžãƒãƒ¼ã‚¸ãƒ£ãƒ¼ã‚’作æˆã§ãã¾ã›ã‚“ã§ã—㟠+ +cloudMembershipProvider.add=Member [{0}] ãŒè¿½åŠ ã•ã‚Œã¾ã—㟠+cloudMembershipProvider.remove=Member [{0}] ãŒé›¢è„±ã—ã¾ã—㟠+cloudMembershipProvider.start=ãƒãƒ¼ãƒ ã‚¹ãƒšãƒ¼ã‚¹ [{0}] ãŒè¨­å®šã•ã‚Œã¾ã—ãŸã€‚ クラスタリングãŒæœ‰åŠ¹ã§ã™ + +cloudMembershipService.stopFail=レベル [{0}] ã®ãƒ¡ãƒ³ãƒãƒ¼ã‚·ãƒƒãƒ—サービスをåœæ­¢ã§ãã¾ã›ã‚“ + +dnsMembershipProvider.dnsError=åå‰ç©ºé–“ [{0}] ã®ãƒ›ã‚¹ãƒˆã‚¢ãƒ‰ãƒ¬ã‚¹ãƒªã‚¹ãƒˆå–得中ã®ã‚¨ãƒ©ãƒ¼ + +kubernetesMembershipProvider.invalidPod=Pod ã«å¿…è¦ãªå±žæ€§ãŒã‚ã‚Šã¾ã›ã‚“。 +kubernetesMembershipProvider.invalidPodsList=ä¸æ­£ãª Pod リストã§ã™: [{0}] +kubernetesMembershipProvider.jsonError=JSONエラー +kubernetesMembershipProvider.memberError=メンãƒãƒ¼ä½œæˆä¸­ã®ã‚¨ãƒ©ãƒ¼ +kubernetesMembershipProvider.noKey=クライアント証明書キーãŒç’°å¢ƒå¤‰æ•°ã«æŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ã§ã—㟠+kubernetesMembershipProvider.noNamespace=Namespace ãŒæœªæŒ‡å®šã§ã™ã€‚ +kubernetesMembershipProvider.streamError=ストリームã®ã‚ªãƒ¼ãƒ—ンã«å¤±æ•—ã—ã¾ã—㟠+ +tokenStream.failedConnection=トークン[{1}]ã§[{0}]ã¸ã®æŽ¥ç¶šã«å¤±æ•—ã—ã¾ã—ãŸã€‚ diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_ko.properties new file mode 100644 index 0000000..473c018 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_ko.properties @@ -0,0 +1,33 @@ +# 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. + +abstractStream.CACertUndefined=CA ì¸ì¦ì„œ 파ì¼ì´ ì •ì˜ë˜ì§€ 않았습니다. +abstractStream.fileNotFound=CA ì¸ì¦ì„œ íŒŒì¼ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +abstractStream.trustManagerError=[{0}]ì„(를) 위한 Trust Manager를 ìƒì„±í•  수 없었습니다. + +certificateStream.clientCertError=[{0}]([{1}])ì„(를) 위한 키 매니저를 ìƒì„±í•  수 없습니다. + +cloudMembershipService.stopFail=ì •ì  ë©¤ë²„ì‹­ 서비스를 중단할 수 없습니다. 레벨: [{0}] + +dnsMembershipProvider.dnsError=네임스페ì´ìŠ¤ [{0}]ì„(를) 위한, í˜¸ìŠ¤íŠ¸ë“¤ì˜ ì£¼ì†Œ 목ë¡ì„ 얻는 중 오류 ë°œìƒ + +kubernetesMembershipProvider.invalidPod=Podì— ì¼ë¶€ 필수 ì†ì„±ë“¤ì´ 없습니다: [{0}] +kubernetesMembershipProvider.invalidPodsList=유효하지 ì•Šì€ podë“¤ì˜ ëª©ë¡: [{0}] +kubernetesMembershipProvider.jsonError=JSON 오류 +kubernetesMembershipProvider.memberError=멤버 ìƒì„± 중 오류 ë°œìƒ +kubernetesMembershipProvider.noNamespace=네임스페ì´ìŠ¤ê°€ 설정 ì•ˆë¨ +kubernetesMembershipProvider.streamError=ìŠ¤íŠ¸ë¦¼ì„ ì—´ì§€ 못했습니다. + +tokenStream.failedConnection=í† í° [{1}]ì„(를) 사용하여 [{0}]ì— ì—°ê²°ì„ ë§ºì§€ 못했습니다. diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_pt_BR.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..da45a3f --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_pt_BR.properties @@ -0,0 +1,18 @@ +# 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. + +abstractStream.trustManagerError=Impossível criar trust manager para [{0}] + +cloudMembershipService.stopFail=Impossível parar o serviço de membros estáticos, nível: [{0}] diff --git a/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..1ad6c1a --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/LocalStrings_zh_CN.properties @@ -0,0 +1,33 @@ +# 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. + +abstractStream.CACertUndefined=CAè¯ä¹¦æ–‡ä»¶æœªå®šä¹‰ +abstractStream.fileNotFound=未找到 CA è¯ä¹¦æ–‡ä»¶ [{0}] +abstractStream.trustManagerError=无法为[{0}]创建信任管ç†å™¨ + +certificateStream.clientCertError=无法为[{0}] [{1}] 创建key管ç†å™¨ + +cloudMembershipService.stopFail=无法åœæ­¢äº‘æˆå‘˜èµ„æ ¼æœåŠ¡ï¼Œçº§åˆ«ä¸ºï¼š[{0}] + +dnsMembershipProvider.dnsError=由于命å空间[{0}]å¯¼è‡´çš„å¤šä¸ªé”™è¯¯ä¸»æœºåœ°å€ + +kubernetesMembershipProvider.invalidPod=Pod丢失了一些必须的属性:[{0}]。 +kubernetesMembershipProvider.invalidPodsList=无效的播客列表:[{0}]。 +kubernetesMembershipProvider.jsonError=JSON错误 +kubernetesMembershipProvider.memberError=创建æˆå‘˜é”™è¯¯ +kubernetesMembershipProvider.noNamespace=命å空间未设置 +kubernetesMembershipProvider.streamError=无法打开æµã€‚ + +tokenStream.failedConnection=无法使用标记[{1}]连接到[{0}] diff --git a/java/org/apache/catalina/tribes/membership/cloud/StreamProvider.java b/java/org/apache/catalina/tribes/membership/cloud/StreamProvider.java new file mode 100644 index 0000000..7488864 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/StreamProvider.java @@ -0,0 +1,34 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +public interface StreamProvider { + /** + * Open stream to the specified URL. + * @param url the url + * @param headers the headers map + * @param connectTimeout connection timeout in ms + * @param readTimeout read timeout in ms + * @return the stream + * @throws IOException when an error occurs + */ + InputStream openStream(String url, Map headers, int connectTimeout, int readTimeout) throws IOException; +} diff --git a/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java b/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java new file mode 100644 index 0000000..b44ba1e --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +public class TokenStreamProvider extends AbstractStreamProvider { + + private String token; + private SSLSocketFactory factory; + + TokenStreamProvider(String token, String caCertFile) throws Exception { + this.token = token; + TrustManager[] trustManagers = configureCaCert(caCertFile); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustManagers, null); + this.factory = context.getSocketFactory(); + } + + @Override + protected SSLSocketFactory getSocketFactory() { + return factory; + } + + @Override + public InputStream openStream(String url, Map headers, int connectTimeout, int readTimeout) + throws IOException { + // Set token header + if (token != null) { + headers.put("Authorization", "Bearer " + token); + } + try { + return super.openStream(url, headers, connectTimeout, readTimeout); + } catch (IOException e) { + // Add debug information + throw new IOException(sm.getString("tokenStream.failedConnection", url, token), e); + } + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml b/java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml new file mode 100644 index 0000000..42af9db --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/tribes/package.html b/java/org/apache/catalina/tribes/package.html new file mode 100644 index 0000000..9ea57e7 --- /dev/null +++ b/java/org/apache/catalina/tribes/package.html @@ -0,0 +1,88 @@ + + +Apache Tribes - The Tomcat Cluster Communication Module + + +

    QuickStart

    +
    
    +            //create a channel
    +            Channel myChannel = new GroupChannel();
    +
    +            //create my listeners
    +            MyMessageListener msgListener = new MyMessageListener();
    +            MyMemberListener mbrListener = new MyMemberListener();
    +
    +            //attach the listeners to the channel
    +            myChannel.addMembershipListener(mbrListener);
    +            myChannel.addChannelListener(msgListener);
    +
    +            //start the channel
    +            myChannel.start(Channel.DEFAULT);
    +
    +            //create a message to be sent, message must implement java.io.Serializable
    +            //for performance reasons you probably want them to implement java.io.Externalizable
    +            Serializable myMsg = new MyMessage();
    +
    +            //retrieve my current members
    +            Member[] group = myChannel.getMembers();
    +
    +            //send the message
    +            channel.send(group,myMsg,Channel.SEND_OPTIONS_DEFAULT);
    +
    +    
    +

    Interfaces for the Application Developer

    +
      +
    1. org.apache.catalina.tribes.Channel + Main component to interact with to send messages +
    2. +
    3. org.apache.catalina.tribes.MembershipListener + Listen to membership changes +
    4. +
    5. org.apache.catalina.tribes.ChannelListener + Listen to data messages +
    6. +
    7. org.apache.catalina.tribes.Member + Identifies a node, implementation specific, default is org.apache.catalina.tribes.membership.MemberImpl +
    8. +
    +

    Interfaces for the Tribes Component Developer

    +
      +
    1. org.apache.catalina.tribes.Channel + Main component to that the application interacts with +
    2. +
    3. org.apache.catalina.tribes.ChannelReceiver + IO Component to receive messages over some network transport +
    4. +
    5. org.apache.catalina.tribes.ChannelSender + IO Component to send messages over some network transport +
    6. +
    7. org.apache.catalina.tribes.MembershipService + IO Component that handles membership discovery and +
    8. +
    9. org.apache.catalina.tribes.ChannelInterceptor + interceptors between the Channel and the IO layer +
    10. +
    11. org.apache.catalina.tribes.ChannelMessage + The message that is sent through the interceptor stack down to the IO layer +
    12. + +
    13. org.apache.catalina.tribes.Member + Identifies a node, implementation specific to the underlying IO logic +
    14. +
    + diff --git a/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java new file mode 100644 index 0000000..7499133 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java @@ -0,0 +1,1745 @@ +/* + * 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.catalina.tribes.tipis; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelException.FaultyMember; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.Heartbeat; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.group.Response; +import org.apache.catalina.tribes.group.RpcCallback; +import org.apache.catalina.tribes.group.RpcChannel; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * @param The type of Key + * @param The type of Value + */ +public abstract class AbstractReplicatedMap + implements Map, Serializable, RpcCallback, ChannelListener, + MembershipListener, Heartbeat { + + private static final long serialVersionUID = 1L; + + protected static final StringManager sm = StringManager.getManager(AbstractReplicatedMap.class); + + private final Log log = LogFactory.getLog(AbstractReplicatedMap.class); // must not be static + + /** + * The default initial capacity - MUST be a power of two. + */ + public static final int DEFAULT_INITIAL_CAPACITY = 16; + + /** + * The load factor used when none specified in constructor. + **/ + public static final float DEFAULT_LOAD_FACTOR = 0.75f; + + +//------------------------------------------------------------------------------ +// INSTANCE VARIABLES +//------------------------------------------------------------------------------ + protected final ConcurrentMap> innerMap; + + protected abstract int getStateMessageType(); + + protected abstract int getReplicateMessageType(); + + + /** + * Timeout for RPC messages, how long we will wait for a reply + */ + protected transient long rpcTimeout = 5000; + /** + * Reference to the channel for sending messages + */ + protected transient Channel channel; + /** + * The RpcChannel to send RPC messages through + */ + protected transient RpcChannel rpcChannel; + /** + * The Map context name makes this map unique, this + * allows us to have more than one map shared + * through one channel + */ + protected transient byte[] mapContextName; + /** + * Has the state been transferred + */ + protected transient boolean stateTransferred = false; + /** + * Simple lock object for transfers + */ + protected final transient Object stateMutex = new Object(); + /** + * A list of members in our map + */ + protected final transient HashMap mapMembers = new HashMap<>(); + /** + * Our default send options + */ + protected transient int channelSendOptions = Channel.SEND_OPTIONS_DEFAULT; + /** + * The owner of this map, ala a SessionManager for example + */ + protected transient MapOwner mapOwner; + /** + * External class loaders if serialization and deserialization is to be performed successfully. + */ + protected transient ClassLoader[] externalLoaders; + + /** + * The node we are currently backing up data to, this index will rotate + * on a round robin basis + */ + protected transient int currentNode = 0; + + /** + * Since the map keeps internal membership + * this is the timeout for a ping message to be responded to + * If a remote map doesn't respond within this timeframe, + * its considered dead. + */ + protected transient long accessTimeout = 5000; + + /** + * Readable string of the mapContextName value + */ + protected transient String mapname = ""; + + /** + * State of this map + */ + private transient volatile State state = State.NEW; + +//------------------------------------------------------------------------------ +// map owner interface +//------------------------------------------------------------------------------ + + public interface MapOwner { + void objectMadePrimary(Object key, Object value); + } + +//------------------------------------------------------------------------------ +// CONSTRUCTORS +//------------------------------------------------------------------------------ + + /** + * Creates a new map. + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param initialCapacity int - the size of this map, see HashMap + * @param loadFactor float - load factor, see HashMap + * @param channelSendOptions Send options + * @param cls - a list of classloaders to be used for deserialization of objects. + * @param terminate - Flag for whether to terminate this map that failed to start. + */ + public AbstractReplicatedMap(MapOwner owner, + Channel channel, + long timeout, + String mapContextName, + int initialCapacity, + float loadFactor, + int channelSendOptions, + ClassLoader[] cls, + boolean terminate) { + innerMap = new ConcurrentHashMap<>(initialCapacity, loadFactor, 15); + init(owner, channel, mapContextName, timeout, channelSendOptions, cls, terminate); + + } + + /** + * Helper methods, wraps a single member in an array + * @param m Member + * @return Member[] + */ + protected Member[] wrap(Member m) { + if ( m == null ) { + return new Member[0]; + } else { + return new Member[] {m}; + } + } + + /** + * Initializes the map by creating the RPC channel, registering itself as a channel listener + * This method is also responsible for initiating the state transfer + * @param owner Object + * @param channel Channel + * @param mapContextName String + * @param timeout long + * @param channelSendOptions int + * @param cls ClassLoader[] + * @param terminate - Flag for whether to terminate this map that failed to start. + */ + protected void init(MapOwner owner, Channel channel, String mapContextName, + long timeout, int channelSendOptions,ClassLoader[] cls, boolean terminate) { + long start = System.currentTimeMillis(); + if (log.isInfoEnabled()) { + log.info(sm.getString("abstractReplicatedMap.init.start", mapContextName)); + } + this.mapOwner = owner; + this.externalLoaders = cls; + this.channelSendOptions = channelSendOptions; + this.channel = channel; + this.rpcTimeout = timeout; + + this.mapname = mapContextName; + //unique context is more efficient if it is stored as bytes + this.mapContextName = mapContextName.getBytes(StandardCharsets.ISO_8859_1); + if ( log.isTraceEnabled() ) { + log.trace("Created Lazy Map with name:"+mapContextName+", bytes:"+Arrays.toString(this.mapContextName)); + } + + //create an rpc channel and add the map as a listener + this.rpcChannel = new RpcChannel(this.mapContextName, channel, this); + //add this map as a message listener + this.channel.addChannelListener(this); + //listen for membership notifications + this.channel.addMembershipListener(this); + + try { + //broadcast our map, this just notifies other members of our existence + broadcast(MapMessage.MSG_INIT, true); + //transfer state from another map + transferState(); + //state is transferred, we are ready for messaging + broadcast(MapMessage.MSG_START, true); + } catch (ChannelException x) { + log.warn(sm.getString("abstractReplicatedMap.unableSend.startMessage")); + if (terminate) { + breakdown(); + throw new RuntimeException(sm.getString("abstractReplicatedMap.unableStart"),x); + } + } + this.state = State.INITIALIZED; + long complete = System.currentTimeMillis() - start; + if (log.isInfoEnabled()) { + log.info(sm.getString("abstractReplicatedMap.init.completed", + mapContextName, Long.toString(complete))); + } + } + + + /** + * Sends a ping out to all the members in the cluster, not just map members + * that this map is alive. + * @param timeout long + * @throws ChannelException Send error + */ + protected void ping(long timeout) throws ChannelException { + MapMessage msg = new MapMessage(this.mapContextName, + MapMessage.MSG_PING, + false, + null, + null, + null, + channel.getLocalMember(false), + null); + if ( channel.getMembers().length > 0 ) { + try { + //send a ping, wait for all nodes to reply + Response[] resp = rpcChannel.send(channel.getMembers(), + msg, RpcChannel.ALL_REPLY, + (channelSendOptions), + (int) accessTimeout); + for (Response response : resp) { + MapMessage mapMsg = (MapMessage) response.getMessage(); + try { + mapMsg.deserialize(getExternalLoaders()); + Member member = response.getSource(); + State state = (State) mapMsg.getValue(); + if (state.isAvailable()) { + memberAlive(member); + } else if (state == State.STATETRANSFERRED) { + synchronized (mapMembers) { + if (log.isInfoEnabled()) { + log.info(sm.getString("abstractReplicatedMap.ping.stateTransferredMember", + member)); + } + if (mapMembers.containsKey(member)) { + mapMembers.put(member, Long.valueOf(System.currentTimeMillis())); + } + } + } else { + if (log.isInfoEnabled()) { + log.info(sm.getString("abstractReplicatedMap.mapMember.unavailable", + member)); + } + } + } catch (ClassNotFoundException | IOException e) { + log.error(sm.getString("abstractReplicatedMap.unable.deserialize.MapMessage"), e); + } + } + } catch (ChannelException ce) { + // Handle known failed members + FaultyMember[] faultyMembers = ce.getFaultyMembers(); + for (FaultyMember faultyMember : faultyMembers) { + memberDisappeared(faultyMember.getMember()); + } + throw ce; + } + } + //update our map of members, expire some if we didn't receive a ping back + synchronized (mapMembers) { + Member[] members = mapMembers.keySet().toArray(new Member[0]); + long now = System.currentTimeMillis(); + for (Member member : members) { + long access = mapMembers.get(member).longValue(); + if ( (now - access) > timeout ) { + log.warn(sm.getString("abstractReplicatedMap.ping.timeout", member, mapname)); + memberDisappeared(member); + } + } + }//synch + } + + /** + * We have received a member alive notification + * @param member Member + */ + protected void memberAlive(Member member) { + mapMemberAdded(member); + synchronized (mapMembers) { + mapMembers.put(member, Long.valueOf(System.currentTimeMillis())); + } + } + + /** + * Helper method to broadcast a message to all members in a channel + * @param msgtype int + * @param rpc boolean + * @throws ChannelException Send error + */ + protected void broadcast(int msgtype, boolean rpc) throws ChannelException { + Member[] members = channel.getMembers(); + // No destination. + if (members.length == 0 ) { + return; + } + //send out a map membership message, only wait for the first reply + MapMessage msg = new MapMessage(this.mapContextName, msgtype, + false, null, null, null, channel.getLocalMember(false), null); + if ( rpc) { + Response[] resp = rpcChannel.send(members, msg, + RpcChannel.FIRST_REPLY, (channelSendOptions), rpcTimeout); + if (resp.length > 0) { + for (Response response : resp) { + mapMemberAdded(response.getSource()); + messageReceived(response.getMessage(), response.getSource()); + } + } else { + log.warn(sm.getString("abstractReplicatedMap.broadcast.noReplies")); + } + } else { + channel.send(channel.getMembers(),msg,channelSendOptions); + } + } + + public void breakdown() { + this.state = State.DESTROYED; + if (this.rpcChannel != null) { + this.rpcChannel.breakdown(); + } + if (this.channel != null) { + try {broadcast(MapMessage.MSG_STOP,false); }catch ( Exception ignore){} + //cleanup + this.channel.removeChannelListener(this); + this.channel.removeMembershipListener(this); + } + this.rpcChannel = null; + this.channel = null; + synchronized (mapMembers) { + this.mapMembers.clear(); + } + innerMap.clear(); + this.stateTransferred = false; + this.externalLoaders = null; + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.mapContextName); + } + + @Override + public boolean equals(Object o) { + if ( !(o instanceof AbstractReplicatedMap)) { + return false; + } + if ( !(o.getClass().equals(this.getClass())) ) { + return false; + } + @SuppressWarnings("unchecked") + AbstractReplicatedMap other = (AbstractReplicatedMap)o; + return Arrays.equals(mapContextName,other.mapContextName); + } + +//------------------------------------------------------------------------------ +// GROUP COM INTERFACES +//------------------------------------------------------------------------------ + public Member[] getMapMembers(HashMap members) { + return members.keySet().toArray(new Member[0]); + } + public Member[] getMapMembers() { + synchronized (mapMembers) { + return getMapMembers(mapMembers); + } + } + + public Member[] getMapMembersExcl(Member[] exclude) { + if (exclude == null) { + return null; + } + synchronized (mapMembers) { + @SuppressWarnings("unchecked") // mapMembers has the correct type + HashMap list = (HashMap)mapMembers.clone(); + for (Member member : exclude) { + list.remove(member); + } + return getMapMembers(list); + } + } + + + /** + * Replicates any changes to the object since the last time + * The object has to be primary, ie, if the object is a proxy or a backup, it will not be replicated
    + * @param key The object to replicate + * @param complete - if set to true, the object is replicated to its backup + * if set to false, only objects that implement ReplicatedMapEntry and the isDirty() returns true will + * be replicated + */ + public void replicate(Object key, boolean complete) { + if ( log.isTraceEnabled() ) { + log.trace("Replicate invoked on key:"+key); + } + MapEntry entry = innerMap.get(key); + if ( entry == null ) { + return; + } + if ( !entry.isSerializable() ) { + return; + } + if (entry.isPrimary() && entry.getBackupNodes()!= null && entry.getBackupNodes().length > 0) { + //check to see if we need to replicate this object isDirty()||complete || isAccessReplicate() + ReplicatedMapEntry rentry = null; + if (entry.getValue() instanceof ReplicatedMapEntry) { + rentry = (ReplicatedMapEntry)entry.getValue(); + } + boolean isDirty = rentry != null && rentry.isDirty(); + boolean isAccess = rentry != null && rentry.isAccessReplicate(); + boolean repl = complete || isDirty || isAccess; + + if (!repl) { + if ( log.isTraceEnabled() ) { + log.trace("Not replicating:"+key+", no change made"); + } + + return; + } + //check to see if the message is diffable + MapMessage msg = null; + if (rentry != null && rentry.isDiffable() && (isDirty || complete)) { + rentry.lock(); + try { + //construct a diff message + msg = new MapMessage(mapContextName, getReplicateMessageType(), + true, (Serializable) entry.getKey(), null, + rentry.getDiff(), + entry.getPrimary(), + entry.getBackupNodes()); + rentry.resetDiff(); + } catch (IOException x) { + log.error(sm.getString("abstractReplicatedMap.unable.diffObject"), x); + } finally { + rentry.unlock(); + } + } + if (msg == null && complete) { + //construct a complete + msg = new MapMessage(mapContextName, getReplicateMessageType(), + false, (Serializable) entry.getKey(), + (Serializable) entry.getValue(), + null, entry.getPrimary(),entry.getBackupNodes()); + } + if (msg == null) { + //construct a access message + msg = new MapMessage(mapContextName, MapMessage.MSG_ACCESS, + false, (Serializable) entry.getKey(), null, null, entry.getPrimary(), + entry.getBackupNodes()); + } + try { + if ( channel!=null && entry.getBackupNodes()!= null && entry.getBackupNodes().length > 0 ) { + if (rentry != null) { + rentry.setLastTimeReplicated(System.currentTimeMillis()); + } + channel.send(entry.getBackupNodes(), msg, channelSendOptions); + } + } catch (ChannelException x) { + log.error(sm.getString("abstractReplicatedMap.unable.replicate"), x); + } + } //end if + + } + + /** + * This can be invoked by a periodic thread to replicate out any changes. + * For maps that don't store objects that implement ReplicatedMapEntry, this + * method should be used infrequently to avoid large amounts of data transfer + * @param complete boolean + */ + public void replicate(boolean complete) { + for (Entry> e : innerMap.entrySet()) { + replicate(e.getKey(), complete); + } + } + + public void transferState() { + try { + Member[] members = getMapMembers(); + Member backup = members.length > 0 ? members[0] : null; + if (backup != null) { + MapMessage msg = new MapMessage(mapContextName, getStateMessageType(), false, + null, null, null, null, null); + Response[] resp = rpcChannel.send(new Member[] {backup}, msg, RpcChannel.FIRST_REPLY, channelSendOptions, rpcTimeout); + if (resp.length > 0) { + synchronized (stateMutex) { + msg = (MapMessage) resp[0].getMessage(); + msg.deserialize(getExternalLoaders()); + ArrayList list = (ArrayList) msg.getValue(); + for (Object o : list) { + messageReceived((Serializable) o, resp[0].getSource()); + } //for + } + stateTransferred = true; + } else { + log.warn(sm.getString("abstractReplicatedMap.transferState.noReplies")); + } + } + } catch (ChannelException | ClassNotFoundException | IOException x) { + log.error(sm.getString("abstractReplicatedMap.unable.transferState"), x); + } + this.state = State.STATETRANSFERRED; + } + + /** + * @param msg Serializable + * @return Serializable - null if no reply should be sent + */ + @Override + public Serializable replyRequest(Serializable msg, final Member sender) { + if (! (msg instanceof MapMessage)) { + return null; + } + MapMessage mapmsg = (MapMessage) msg; + + //map init request + if (mapmsg.getMsgType() == MapMessage.MSG_INIT) { + mapmsg.setPrimary(channel.getLocalMember(false)); + return mapmsg; + } + + //map start request + if (mapmsg.getMsgType() == MapMessage.MSG_START) { + mapmsg.setPrimary(channel.getLocalMember(false)); + mapMemberAdded(sender); + return mapmsg; + } + + //backup request + if (mapmsg.getMsgType() == MapMessage.MSG_RETRIEVE_BACKUP) { + MapEntry entry = innerMap.get(mapmsg.getKey()); + if (entry == null || (!entry.isSerializable()) ) { + return null; + } + mapmsg.setValue( (Serializable) entry.getValue()); + return mapmsg; + } + + //state transfer request + if (mapmsg.getMsgType() == MapMessage.MSG_STATE || mapmsg.getMsgType() == MapMessage.MSG_STATE_COPY) { + synchronized (stateMutex) { //make sure we don't do two things at the same time + ArrayList list = new ArrayList<>(); + for (Entry> e : innerMap.entrySet()) { + MapEntry entry = innerMap.get(e.getKey()); + if ( entry != null && entry.isSerializable() ) { + boolean copy = (mapmsg.getMsgType() == MapMessage.MSG_STATE_COPY); + MapMessage me = new MapMessage(mapContextName, + copy?MapMessage.MSG_COPY:MapMessage.MSG_PROXY, + false, (Serializable) entry.getKey(), copy?(Serializable) entry.getValue():null, null, entry.getPrimary(),entry.getBackupNodes()); + list.add(me); + } + } + mapmsg.setValue(list); + return mapmsg; + + } //synchronized + } + + // ping + if (mapmsg.getMsgType() == MapMessage.MSG_PING) { + mapmsg.setValue(state); + mapmsg.setPrimary(channel.getLocalMember(false)); + return mapmsg; + } + + return null; + + } + + /** + * If the reply has already been sent to the requesting thread, + * the rpc callback can handle any data that comes in after the fact. + * @param msg Serializable + * @param sender Member + */ + @Override + public void leftOver(Serializable msg, Member sender) { + //left over membership messages + if (! (msg instanceof MapMessage)) { + return; + } + + MapMessage mapmsg = (MapMessage) msg; + try { + mapmsg.deserialize(getExternalLoaders()); + if (mapmsg.getMsgType() == MapMessage.MSG_START) { + mapMemberAdded(mapmsg.getPrimary()); + } else if (mapmsg.getMsgType() == MapMessage.MSG_INIT) { + memberAlive(mapmsg.getPrimary()); + } else if (mapmsg.getMsgType() == MapMessage.MSG_PING) { + Member member = mapmsg.getPrimary(); + if (log.isInfoEnabled()) { + log.info(sm.getString("abstractReplicatedMap.leftOver.pingMsg", member)); + } + State state = (State) mapmsg.getValue(); + if (state.isAvailable()) { + memberAlive(member); + } + } else { + // other messages are ignored. + if (log.isInfoEnabled()) { + log.info(sm.getString("abstractReplicatedMap.leftOver.ignored", + mapmsg.getTypeDesc())); + } + } + } catch (IOException | ClassNotFoundException x) { + log.error(sm.getString("abstractReplicatedMap.unable.deserialize.MapMessage"),x); + } + } + + @SuppressWarnings("unchecked") + @Override + public void messageReceived(Serializable msg, Member sender) { + if (! (msg instanceof MapMessage)) { + return; + } + + MapMessage mapmsg = (MapMessage) msg; + if ( log.isTraceEnabled() ) { + log.trace("Map["+mapname+"] received message:"+mapmsg); + } + + try { + mapmsg.deserialize(getExternalLoaders()); + } catch (IOException | ClassNotFoundException x) { + log.error(sm.getString("abstractReplicatedMap.unable.deserialize.MapMessage"), x); + return; + } + if ( log.isTraceEnabled() ) { + log.trace("Map message received from:"+sender.getName()+" msg:"+mapmsg); + } + if (mapmsg.getMsgType() == MapMessage.MSG_START) { + mapMemberAdded(mapmsg.getPrimary()); + } + + if (mapmsg.getMsgType() == MapMessage.MSG_STOP) { + memberDisappeared(mapmsg.getPrimary()); + } + + if (mapmsg.getMsgType() == MapMessage.MSG_PROXY) { + MapEntry entry = innerMap.get(mapmsg.getKey()); + if ( entry==null ) { + entry = new MapEntry<>((K) mapmsg.getKey(), (V) mapmsg.getValue()); + MapEntry old = innerMap.putIfAbsent(entry.getKey(), entry); + if (old != null) { + entry = old; + } + } + entry.setProxy(true); + entry.setBackup(false); + entry.setCopy(false); + entry.setBackupNodes(mapmsg.getBackupNodes()); + entry.setPrimary(mapmsg.getPrimary()); + } + + if (mapmsg.getMsgType() == MapMessage.MSG_REMOVE) { + innerMap.remove(mapmsg.getKey()); + } + + if (mapmsg.getMsgType() == MapMessage.MSG_BACKUP || mapmsg.getMsgType() == MapMessage.MSG_COPY) { + MapEntry entry = innerMap.get(mapmsg.getKey()); + if (entry == null) { + entry = new MapEntry<>((K) mapmsg.getKey(), (V) mapmsg.getValue()); + entry.setBackup(mapmsg.getMsgType() == MapMessage.MSG_BACKUP); + entry.setProxy(false); + entry.setCopy(mapmsg.getMsgType() == MapMessage.MSG_COPY); + entry.setBackupNodes(mapmsg.getBackupNodes()); + entry.setPrimary(mapmsg.getPrimary()); + if (mapmsg.getValue() instanceof ReplicatedMapEntry ) { + ((ReplicatedMapEntry)mapmsg.getValue()).setOwner(getMapOwner()); + } + } else { + entry.setBackup(mapmsg.getMsgType() == MapMessage.MSG_BACKUP); + entry.setProxy(false); + entry.setCopy(mapmsg.getMsgType() == MapMessage.MSG_COPY); + entry.setBackupNodes(mapmsg.getBackupNodes()); + entry.setPrimary(mapmsg.getPrimary()); + if (entry.getValue() instanceof ReplicatedMapEntry) { + ReplicatedMapEntry diff = (ReplicatedMapEntry) entry.getValue(); + if (mapmsg.isDiff()) { + diff.lock(); + try { + diff.applyDiff(mapmsg.getDiffValue(), 0, mapmsg.getDiffValue().length); + } catch (Exception x) { + log.error(sm.getString("abstractReplicatedMap.unableApply.diff", entry.getKey()), x); + } finally { + diff.unlock(); + } + } else { + if ( mapmsg.getValue()!=null ) { + if (mapmsg.getValue() instanceof ReplicatedMapEntry) { + ReplicatedMapEntry re = (ReplicatedMapEntry)mapmsg.getValue(); + re.setOwner(getMapOwner()); + entry.setValue((V) re); + } else { + entry.setValue((V) mapmsg.getValue()); + } + } else { + ((ReplicatedMapEntry)entry.getValue()).setOwner(getMapOwner()); + } + } //end if + } else if (mapmsg.getValue() instanceof ReplicatedMapEntry) { + ReplicatedMapEntry re = (ReplicatedMapEntry)mapmsg.getValue(); + re.setOwner(getMapOwner()); + entry.setValue((V) re); + } else { + if ( mapmsg.getValue()!=null ) { + entry.setValue((V) mapmsg.getValue()); + } + } //end if + } //end if + innerMap.put(entry.getKey(), entry); + } //end if + + if (mapmsg.getMsgType() == MapMessage.MSG_ACCESS) { + MapEntry entry = innerMap.get(mapmsg.getKey()); + if (entry != null) { + entry.setBackupNodes(mapmsg.getBackupNodes()); + entry.setPrimary(mapmsg.getPrimary()); + if (entry.getValue() instanceof ReplicatedMapEntry) { + ((ReplicatedMapEntry) entry.getValue()).accessEntry(); + } + } + } + + if (mapmsg.getMsgType() == MapMessage.MSG_NOTIFY_MAPMEMBER) { + MapEntry entry = innerMap.get(mapmsg.getKey()); + if (entry != null) { + entry.setBackupNodes(mapmsg.getBackupNodes()); + entry.setPrimary(mapmsg.getPrimary()); + if (entry.getValue() instanceof ReplicatedMapEntry) { + ((ReplicatedMapEntry) entry.getValue()).accessEntry(); + } + } + } + } + + @Override + public boolean accept(Serializable msg, Member sender) { + boolean result = false; + if (msg instanceof MapMessage) { + if ( log.isTraceEnabled() ) { + log.trace("Map["+mapname+"] accepting...."+msg); + } + result = Arrays.equals(mapContextName, ( (MapMessage) msg).getMapId()); + if ( log.isTraceEnabled() ) { + log.trace("Msg["+mapname+"] accepted["+result+"]...."+msg); + } + } + return result; + } + + public void mapMemberAdded(Member member) { + if ( member.equals(getChannel().getLocalMember(false)) ) { + return; + } + boolean memberAdded = false; + //select a backup node if we don't have one + Member mapMember = getChannel().getMember(member); + if (mapMember == null) { + log.warn(sm.getString("abstractReplicatedMap.mapMemberAdded.nullMember", member)); + return; + } + synchronized (mapMembers) { + if (!mapMembers.containsKey(mapMember) ) { + if (log.isInfoEnabled()) { + log.info(sm.getString("abstractReplicatedMap.mapMemberAdded.added", mapMember)); + } + mapMembers.put(mapMember, Long.valueOf(System.currentTimeMillis())); + memberAdded = true; + } + } + if ( memberAdded ) { + synchronized (stateMutex) { + for (Entry> e : innerMap.entrySet()) { + MapEntry entry = innerMap.get(e.getKey()); + if ( entry == null ) { + continue; + } + if (entry.isPrimary() && (entry.getBackupNodes() == null || entry.getBackupNodes().length == 0)) { + try { + Member[] backup = publishEntryInfo(entry.getKey(), entry.getValue()); + entry.setBackupNodes(backup); + entry.setPrimary(channel.getLocalMember(false)); + } catch (ChannelException x) { + log.error(sm.getString("abstractReplicatedMap.unableSelect.backup"), x); + } //catch + } //end if + } //while + } //synchronized + }//end if + } + + public boolean inSet(Member m, Member[] set) { + if ( set == null ) { + return false; + } + boolean result = false; + for (Member member : set) { + if (m.equals(member)) { + result = true; + break; + } + } + return result; + } + + public Member[] excludeFromSet(Member[] mbrs, Member[] set) { + List result = new ArrayList<>(); + for (Member member : set) { + boolean include = true; + for (Member mbr : mbrs) { + if (mbr.equals(member)) { + include = false; + break; + } + } + if (include) { + result.add(member); + } + } + return result.toArray(new Member[0]); + } + + @Override + public void memberAdded(Member member) { + //do nothing + } + + @Override + public void memberDisappeared(Member member) { + boolean removed = false; + synchronized (mapMembers) { + removed = (mapMembers.remove(member) != null ); + if (!removed) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("replicatedMap.member.disappeared.unknown", member)); + } + return; //the member was not part of our map. + } + } + if (log.isInfoEnabled()) { + log.info(sm.getString("replicatedMap.member.disappeared", member)); + } + long start = System.currentTimeMillis(); + Iterator>> i = innerMap.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry> e = i.next(); + MapEntry entry = innerMap.get(e.getKey()); + if (entry==null) { + continue; + } + if (entry.isPrimary() && inSet(member,entry.getBackupNodes())) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("abstractReplicatedMap.newBackup")); + } + try { + Member[] backup = publishEntryInfo(entry.getKey(), entry.getValue()); + entry.setBackupNodes(backup); + entry.setPrimary(channel.getLocalMember(false)); + } catch (ChannelException x) { + log.error(sm.getString("abstractReplicatedMap.unable.relocate", entry.getKey()), x); + } + } else if (member.equals(entry.getPrimary())) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("abstractReplicatedMap.primaryDisappeared")); + } + entry.setPrimary(null); + } //end if + + if ( entry.isProxy() && + entry.getPrimary() == null && + entry.getBackupNodes()!=null && + entry.getBackupNodes().length == 1 && + entry.getBackupNodes()[0].equals(member) ) { + //remove proxies that have no backup nor primaries + if (log.isDebugEnabled()) { + log.debug(sm.getString("abstractReplicatedMap.removeOrphan")); + } + i.remove(); + } else if ( entry.getPrimary() == null && + entry.isBackup() && + entry.getBackupNodes()!=null && + entry.getBackupNodes().length == 1 && + entry.getBackupNodes()[0].equals(channel.getLocalMember(false)) ) { + try { + if (log.isDebugEnabled()) { + log.debug(sm.getString("abstractReplicatedMap.newPrimary")); + } + entry.setPrimary(channel.getLocalMember(false)); + entry.setBackup(false); + entry.setProxy(false); + entry.setCopy(false); + Member[] backup = publishEntryInfo(entry.getKey(), entry.getValue()); + entry.setBackupNodes(backup); + if ( mapOwner!=null ) { + mapOwner.objectMadePrimary(entry.getKey(),entry.getValue()); + } + + } catch (ChannelException x) { + log.error(sm.getString("abstractReplicatedMap.unable.relocate", entry.getKey()), x); + } + } + + } //while + long complete = System.currentTimeMillis() - start; + if (log.isInfoEnabled()) { + log.info(sm.getString("abstractReplicatedMap.relocate.complete", + Long.toString(complete))); + } + } + + public int getNextBackupIndex() { + synchronized (mapMembers) { + int size = mapMembers.size(); + if (mapMembers.size() == 0) { + return -1; + } + int node = currentNode++; + if (node >= size) { + node = 0; + currentNode = 1; + } + return node; + } + } + public Member getNextBackupNode() { + Member[] members = getMapMembers(); + int node = getNextBackupIndex(); + if ( members.length == 0 || node==-1) { + return null; + } + if ( node >= members.length ) { + node = 0; + } + return members[node]; + } + + protected abstract Member[] publishEntryInfo(Object key, Object value) throws ChannelException; + + @Override + public void heartbeat() { + try { + if (this.state.isAvailable()) { + ping(accessTimeout); + } + }catch ( Exception x ) { + log.error(sm.getString("abstractReplicatedMap.heartbeat.failed"),x); + } + } + +//------------------------------------------------------------------------------ +// METHODS TO OVERRIDE +//------------------------------------------------------------------------------ + + /** + * Removes an object from this map, it will also remove it from + * + * @param key Object + * @return Object + */ + @Override + public V remove(Object key) { + return remove(key,true); + } + public V remove(Object key, boolean notify) { + MapEntry entry = innerMap.remove(key); + + try { + if (getMapMembers().length > 0 && notify) { + MapMessage msg = new MapMessage(getMapContextName(), MapMessage.MSG_REMOVE, false, (Serializable) key, null, null, null,null); + getChannel().send(getMapMembers(), msg, getChannelSendOptions()); + } + } catch ( ChannelException x ) { + log.error(sm.getString("abstractReplicatedMap.unable.remove"),x); + } + return entry!=null?entry.getValue():null; + } + + public MapEntry getInternal(Object key) { + return innerMap.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public V get(Object key) { + MapEntry entry = innerMap.get(key); + if (log.isTraceEnabled()) { + log.trace("Requesting id:"+key+" entry:"+entry); + } + if ( entry == null ) { + return null; + } + if ( !entry.isPrimary() ) { + //if the message is not primary, we need to retrieve the latest value + try { + Member[] backup = null; + MapMessage msg = null; + if (entry.isBackup()) { + //select a new backup node + backup = publishEntryInfo(key, entry.getValue()); + } else if ( entry.isProxy() ) { + //make sure we don't retrieve from ourselves + msg = new MapMessage(getMapContextName(), MapMessage.MSG_RETRIEVE_BACKUP, false, + (Serializable) key, null, null, null,null); + Response[] resp = getRpcChannel().send(entry.getBackupNodes(),msg, RpcChannel.FIRST_REPLY, getChannelSendOptions(), getRpcTimeout()); + if (resp == null || resp.length == 0 || resp[0].getMessage() == null) { + //no responses + log.warn(sm.getString("abstractReplicatedMap.unable.retrieve", key)); + return null; + } + msg = (MapMessage) resp[0].getMessage(); + msg.deserialize(getExternalLoaders()); + backup = entry.getBackupNodes(); + if ( msg.getValue()!=null ) { + entry.setValue((V) msg.getValue()); + } + + // notify member + msg = new MapMessage(getMapContextName(), MapMessage.MSG_NOTIFY_MAPMEMBER,false, + (Serializable)entry.getKey(), null, null, channel.getLocalMember(false), backup); + if ( backup != null && backup.length > 0) { + getChannel().send(backup, msg, getChannelSendOptions()); + } + + //invalidate the previous primary + msg = new MapMessage(getMapContextName(),MapMessage.MSG_PROXY,false,(Serializable)key,null,null,channel.getLocalMember(false),backup); + Member[] dest = getMapMembersExcl(backup); + if ( dest!=null && dest.length >0) { + getChannel().send(dest, msg, getChannelSendOptions()); + } + if (entry.getValue() instanceof ReplicatedMapEntry) { + ReplicatedMapEntry val = (ReplicatedMapEntry)entry.getValue(); + val.setOwner(getMapOwner()); + } + } else if ( entry.isCopy() ) { + backup = getMapMembers(); + if (backup.length > 0) { + msg = new MapMessage(getMapContextName(), MapMessage.MSG_NOTIFY_MAPMEMBER,false, + (Serializable)key,null,null,channel.getLocalMember(false),backup); + getChannel().send(backup, msg, getChannelSendOptions()); + } + } + entry.setPrimary(channel.getLocalMember(false)); + entry.setBackupNodes(backup); + entry.setBackup(false); + entry.setProxy(false); + entry.setCopy(false); + if ( getMapOwner()!=null ) { + getMapOwner().objectMadePrimary(key, entry.getValue()); + } + + } catch (RuntimeException | ChannelException | ClassNotFoundException | IOException x) { + log.error(sm.getString("abstractReplicatedMap.unable.get"), x); + return null; + } + } + if (log.isTraceEnabled()) { + log.trace("Requesting id:"+key+" result:"+entry.getValue()); + } + return entry.getValue(); + } + + + protected void printMap(String header) { + try { + System.out.println("\nDEBUG MAP:"+header); + System.out.println("Map[" + + new String(mapContextName, StandardCharsets.ISO_8859_1) + + ", Map Size:" + innerMap.size()); + Member[] mbrs = getMapMembers(); + for ( int i=0; iget(key) + * will make this entry primary for the group + * @param key Object + * @return boolean + */ + @Override + public boolean containsKey(Object key) { + return innerMap.containsKey(key); + } + + @Override + public V put(K key, V value) { + return put(key, value, true); + } + + public V put(K key, V value, boolean notify) { + MapEntry entry = new MapEntry<>(key, value); + entry.setBackup(false); + entry.setProxy(false); + entry.setCopy(false); + entry.setPrimary(channel.getLocalMember(false)); + + V old = null; + + //make sure that any old values get removed + if ( containsKey(key) ) { + old = remove(key); + } + try { + if ( notify ) { + Member[] backup = publishEntryInfo(key, value); + entry.setBackupNodes(backup); + } + } catch (ChannelException x) { + log.error(sm.getString("abstractReplicatedMap.unable.put"), x); + } + innerMap.put(key,entry); + return old; + } + + + /** + * Copies all values from one map to this instance + * @param m Map + */ + @Override + public void putAll(Map m) { + for (Entry value : m.entrySet()) { + @SuppressWarnings("unchecked") + Entry entry = (Entry) value; + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + clear(true); + } + + public void clear(boolean notify) { + if ( notify ) { + //only delete active keys + for (K k : keySet()) { + remove(k); + } + } else { + innerMap.clear(); + } + } + + @Override + public boolean containsValue(Object value) { + Objects.requireNonNull(value); + for (Entry> e : innerMap.entrySet()) { + MapEntry entry = innerMap.get(e.getKey()); + if (entry!=null && entry.isActive() && value.equals(entry.getValue())) { + return true; + } + } + return false; + } + + /** + * Returns the entire contents of the map + * Map.Entry.getValue() will return a LazyReplicatedMap.MapEntry object containing all the information + * about the object. + * @return Set + */ + public Set>> entrySetFull() { + return innerMap.entrySet(); + } + + public Set keySetFull() { + return innerMap.keySet(); + } + + public int sizeFull() { + return innerMap.size(); + } + + @Override + public Set> entrySet() { + LinkedHashSet> set = new LinkedHashSet<>(innerMap.size()); + for (Entry> e : innerMap.entrySet()) { + Object key = e.getKey(); + MapEntry entry = innerMap.get(key); + if ( entry != null && entry.isActive() ) { + set.add(entry); + } + } + return Collections.unmodifiableSet(set); + } + + @Override + public Set keySet() { + //todo implement + //should only return keys where this is active. + LinkedHashSet set = new LinkedHashSet<>(innerMap.size()); + for (Entry> e : innerMap.entrySet()) { + K key = e.getKey(); + MapEntry entry = innerMap.get(key); + if ( entry!=null && entry.isActive() ) { + set.add(key); + } + } + return Collections.unmodifiableSet(set); + + } + + + @Override + public int size() { + //todo, implement a counter variable instead + //only count active members in this node + int counter = 0; + Iterator>> it = innerMap.entrySet().iterator(); + while (it!=null && it.hasNext() ) { + Map.Entry e = it.next(); + if ( e != null ) { + MapEntry entry = innerMap.get(e.getKey()); + if (entry!=null && entry.isActive() && entry.getValue() != null) { + counter++; + } + } + } + return counter; + } + + @Override + public boolean isEmpty() { + return size()==0; + } + + @Override + public Collection values() { + List values = new ArrayList<>(); + for (Entry> e : innerMap.entrySet()) { + MapEntry entry = innerMap.get(e.getKey()); + if (entry!=null && entry.isActive() && entry.getValue()!=null) { + values.add(entry.getValue()); + } + } + return Collections.unmodifiableCollection(values); + } + + +//------------------------------------------------------------------------------ +// Map Entry class +//------------------------------------------------------------------------------ + public static class MapEntry implements Map.Entry { + private boolean backup; + private boolean proxy; + private boolean copy; + private Member[] backupNodes; + private Member primary; + private K key; + private V value; + + public MapEntry(K key, V value) { + setKey(key); + setValue(value); + + } + + public boolean isKeySerializable() { + return (key == null) || (key instanceof Serializable); + } + + public boolean isValueSerializable() { + return (value == null) || (value instanceof Serializable); + } + + public boolean isSerializable() { + return isKeySerializable() && isValueSerializable(); + } + + public boolean isBackup() { + return backup; + } + + public void setBackup(boolean backup) { + this.backup = backup; + } + + public boolean isProxy() { + return proxy; + } + + public boolean isPrimary() { + return (!proxy && !backup && !copy); + } + + public boolean isActive() { + return !proxy; + } + + public void setProxy(boolean proxy) { + this.proxy = proxy; + } + + public boolean isCopy() { + return copy; + } + + public void setCopy(boolean copy) { + this.copy = copy; + } + + public boolean isDiffable() { + return (value instanceof ReplicatedMapEntry) && + ((ReplicatedMapEntry)value).isDiffable(); + } + + public void setBackupNodes(Member[] nodes) { + this.backupNodes = nodes; + } + + public Member[] getBackupNodes() { + return backupNodes; + } + + public void setPrimary(Member m) { + primary = m; + } + + public Member getPrimary() { + return primary; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + V old = this.value; + this.value = value; + return old; + } + + @Override + public K getKey() { + return key; + } + + public K setKey(K key) { + K old = this.key; + this.key = key; + return old; + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public boolean equals(Object o) { + return key.equals(o); + } + + /** + * apply a diff, or an entire object + * @param data byte[] + * @param offset int + * @param length int + * @param diff boolean + * @throws IOException IO error + * @throws ClassNotFoundException Deserialization error + */ + @SuppressWarnings("unchecked") + public void apply(byte[] data, int offset, int length, boolean diff) throws IOException, ClassNotFoundException { + if (isDiffable() && diff) { + ReplicatedMapEntry rentry = (ReplicatedMapEntry) value; + rentry.lock(); + try { + rentry.applyDiff(data, offset, length); + } finally { + rentry.unlock(); + } + } else if (length == 0) { + value = null; + proxy = true; + } else { + value = (V) XByteBuffer.deserialize(data, offset, length); + } + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("MapEntry[key:"); + buf.append(getKey()).append("; "); + buf.append("value:").append(getValue()).append("; "); + buf.append("primary:").append(isPrimary()).append("; "); + buf.append("backup:").append(isBackup()).append("; "); + buf.append("proxy:").append(isProxy()).append(";]"); + return buf.toString(); + } + + } + +//------------------------------------------------------------------------------ +// map message to send to and from other maps +//------------------------------------------------------------------------------ + + public static class MapMessage implements Serializable, Cloneable { + private static final long serialVersionUID = 1L; + public static final int MSG_BACKUP = 1; + public static final int MSG_RETRIEVE_BACKUP = 2; + public static final int MSG_PROXY = 3; + public static final int MSG_REMOVE = 4; + public static final int MSG_STATE = 5; + public static final int MSG_START = 6; + public static final int MSG_STOP = 7; + public static final int MSG_INIT = 8; + public static final int MSG_COPY = 9; + public static final int MSG_STATE_COPY = 10; + public static final int MSG_ACCESS = 11; + public static final int MSG_NOTIFY_MAPMEMBER = 12; + public static final int MSG_PING = 13; + + private final byte[] mapId; + private final int msgtype; + private final boolean diff; + private transient Serializable key; + private transient Serializable value; + private byte[] valuedata; + private byte[] keydata; + private final byte[] diffvalue; + private final Member[] nodes; + private Member primary; + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("MapMessage[context="); + buf.append(new String(mapId)); + buf.append("; type="); + buf.append(getTypeDesc()); + buf.append("; key="); + buf.append(key); + buf.append("; value="); + buf.append(value); + buf.append(']'); + return buf.toString(); + } + + public String getTypeDesc() { + switch (msgtype) { + case MSG_BACKUP: return "MSG_BACKUP"; + case MSG_RETRIEVE_BACKUP: return "MSG_RETRIEVE_BACKUP"; + case MSG_PROXY: return "MSG_PROXY"; + case MSG_REMOVE: return "MSG_REMOVE"; + case MSG_STATE: return "MSG_STATE"; + case MSG_START: return "MSG_START"; + case MSG_STOP: return "MSG_STOP"; + case MSG_INIT: return "MSG_INIT"; + case MSG_STATE_COPY: return "MSG_STATE_COPY"; + case MSG_COPY: return "MSG_COPY"; + case MSG_ACCESS: return "MSG_ACCESS"; + case MSG_NOTIFY_MAPMEMBER: return "MSG_NOTIFY_MAPMEMBER"; + case MSG_PING: return "MSG_PING"; + default : return "UNKNOWN"; + } + } + + public MapMessage(byte[] mapId,int msgtype, boolean diff, + Serializable key, Serializable value, + byte[] diffvalue, Member primary, Member[] nodes) { + this.mapId = mapId; + this.msgtype = msgtype; + this.diff = diff; + this.key = key; + this.value = value; + this.diffvalue = diffvalue; + this.nodes = nodes; + this.primary = primary; + setValue(value); + setKey(key); + } + + public void deserialize(ClassLoader[] cls) throws IOException, ClassNotFoundException { + key(cls); + value(cls); + } + + public int getMsgType() { + return msgtype; + } + + public boolean isDiff() { + return diff; + } + + public Serializable getKey() { + try { + return key(null); + } catch ( Exception x ) { + throw new RuntimeException(sm.getString("mapMessage.deserialize.error.key"), x); + } + } + + public Serializable key(ClassLoader[] cls) throws IOException, ClassNotFoundException { + if ( key!=null ) { + return key; + } + if ( keydata == null || keydata.length == 0 ) { + return null; + } + key = XByteBuffer.deserialize(keydata,0,keydata.length,cls); + keydata = null; + return key; + } + + public byte[] getKeyData() { + return keydata; + } + + public Serializable getValue() { + try { + return value(null); + } catch ( Exception x ) { + throw new RuntimeException(sm.getString("mapMessage.deserialize.error.value"), x); + } + } + + public Serializable value(ClassLoader[] cls) throws IOException, ClassNotFoundException { + if ( value!=null ) { + return value; + } + if ( valuedata == null || valuedata.length == 0 ) { + return null; + } + value = XByteBuffer.deserialize(valuedata,0,valuedata.length,cls); + valuedata = null; + return value; + } + + public byte[] getValueData() { + return valuedata; + } + + public byte[] getDiffValue() { + return diffvalue; + } + + public Member[] getBackupNodes() { + return nodes; + } + + public Member getPrimary() { + return primary; + } + + private void setPrimary(Member m) { + primary = m; + } + + public byte[] getMapId() { + return mapId; + } + + public void setValue(Serializable value) { + try { + if ( value != null ) { + valuedata = XByteBuffer.serialize(value); + } + this.value = value; + }catch ( IOException x ) { + throw new RuntimeException(x); + } + } + + public void setKey(Serializable key) { + try { + if (key != null) { + keydata = XByteBuffer.serialize(key); + } + this.key = key; + } catch (IOException x) { + throw new RuntimeException(x); + } + } + + /** + * shallow clone + * @return Object + */ + @Override + public MapMessage clone() { + try { + return (MapMessage) super.clone(); + } catch (CloneNotSupportedException e) { + // Not possible + throw new AssertionError(); + } + } + } //MapMessage + + + public Channel getChannel() { + return channel; + } + + public byte[] getMapContextName() { + return mapContextName; + } + + public RpcChannel getRpcChannel() { + return rpcChannel; + } + + public long getRpcTimeout() { + return rpcTimeout; + } + + public Object getStateMutex() { + return stateMutex; + } + + public boolean isStateTransferred() { + return stateTransferred; + } + + public MapOwner getMapOwner() { + return mapOwner; + } + + public ClassLoader[] getExternalLoaders() { + return externalLoaders; + } + + public int getChannelSendOptions() { + return channelSendOptions; + } + + public long getAccessTimeout() { + return accessTimeout; + } + + public void setMapOwner(MapOwner mapOwner) { + this.mapOwner = mapOwner; + } + + public void setExternalLoaders(ClassLoader[] externalLoaders) { + this.externalLoaders = externalLoaders; + } + + public void setChannelSendOptions(int channelSendOptions) { + this.channelSendOptions = channelSendOptions; + } + + public void setAccessTimeout(long accessTimeout) { + this.accessTimeout = accessTimeout; + } + + private enum State { + NEW(false), + STATETRANSFERRED(false), + INITIALIZED(true), + DESTROYED(false); + + private final boolean available; + + State(boolean available) { + this.available = available; + } + + public boolean isAvailable() { + return available; + } + } +} diff --git a/java/org/apache/catalina/tribes/tipis/LazyReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/LazyReplicatedMap.java new file mode 100644 index 0000000..15f86ac --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LazyReplicatedMap.java @@ -0,0 +1,234 @@ +/* + * 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.catalina.tribes.tipis; + +import java.io.Serializable; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A smart implementation of a stateful replicated map. uses primary/secondary backup strategy. + * One node is always the primary and one node is always the backup. + * This map is synchronized across a cluster, and only has one backup member.
    + * A perfect usage for this map would be a session map for a session manager in a clustered environment.
    + * The only way to modify this list is to use the put, putAll, remove methods. + * entrySet, entrySetFull, keySet, keySetFull, returns all non modifiable sets.

    + * If objects (values) in the map change without invoking put() or remove() + * the data can be distributed using two different methods:
    + * replicate(boolean) and replicate(Object, boolean)
    + * These two methods are very important two understand. The map can work with two set of value objects:
    + * 1. Serializable - the entire object gets serialized each time it is replicated
    + * 2. ReplicatedMapEntry - this interface allows for a isDirty() flag and to replicate diffs if desired.
    + * Implementing the ReplicatedMapEntry interface allows you to decide what objects + * get replicated and how much data gets replicated each time.
    + * If you implement a smart AOP mechanism to detect changes in underlying objects, you can replicate + * only those changes by implementing the ReplicatedMapEntry interface, and return true when isDiffable() + * is invoked.

    + * + * This map implementation doesn't have a background thread running to replicate changes. + * If you do have changes without invoking put/remove then you need to invoke one of the following methods: + *
      + *
    • replicate(Object,boolean) - replicates only the object that belongs to the key
    • + *
    • replicate(boolean) - Scans the entire map for changes and replicates data
    • + *
    + * the boolean value in the replicate method used to decide + * whether to only replicate objects that implement the ReplicatedMapEntry interface + * or to replicate all objects. If an object doesn't implement the ReplicatedMapEntry interface + * each time the object gets replicated the entire object gets serialized, hence a call to replicate(true) + * will replicate all objects in this map that are using this node as primary. + * + *

    REMEMBER TO CALL breakdown() when you are done with the map to + * avoid memory leaks.

    + * TODO implement periodic sync/transfer thread + * + * @param The type of Key + * @param The type of Value + */ +public class LazyReplicatedMap extends AbstractReplicatedMap { + private static final long serialVersionUID = 1L; + // Lazy init to support serialization + private transient volatile Log log; + + +//------------------------------------------------------------------------------ +// CONSTRUCTORS / DESTRUCTORS +//------------------------------------------------------------------------------ + /** + * Creates a new map + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param initialCapacity int - the size of this map, see HashMap + * @param loadFactor float - load factor, see HashMap + * @param cls Class loaders + */ + public LazyReplicatedMap(MapOwner owner, Channel channel, long timeout, String mapContextName, int initialCapacity, float loadFactor, ClassLoader[] cls) { + super(owner,channel,timeout,mapContextName,initialCapacity,loadFactor, Channel.SEND_OPTIONS_DEFAULT,cls, true); + } + + /** + * Creates a new map + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param initialCapacity int - the size of this map, see HashMap + * @param cls Class loaders + */ + public LazyReplicatedMap(MapOwner owner, Channel channel, long timeout, String mapContextName, int initialCapacity, ClassLoader[] cls) { + super(owner, channel, timeout, mapContextName, initialCapacity, DEFAULT_LOAD_FACTOR, + Channel.SEND_OPTIONS_DEFAULT, cls, true); + } + + /** + * Creates a new map + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param cls Class loaders + */ + public LazyReplicatedMap(MapOwner owner, Channel channel, long timeout, String mapContextName, ClassLoader[] cls) { + super(owner, channel, timeout, mapContextName, DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, + Channel.SEND_OPTIONS_DEFAULT, cls, true); + } + + /** + * Creates a new map + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param cls Class loaders + * @param terminate boolean - Flag for whether to terminate this map that failed to start. + */ + public LazyReplicatedMap(MapOwner owner, Channel channel, long timeout, String mapContextName, ClassLoader[] cls, boolean terminate) { + super(owner, channel, timeout, mapContextName, DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, + Channel.SEND_OPTIONS_DEFAULT, cls, terminate); + } + + +//------------------------------------------------------------------------------ +// METHODS TO OVERRIDE +//------------------------------------------------------------------------------ + @Override + protected int getStateMessageType() { + return AbstractReplicatedMap.MapMessage.MSG_STATE; + } + + @Override + protected int getReplicateMessageType() { + return AbstractReplicatedMap.MapMessage.MSG_BACKUP; + } + + /** + * publish info about a map pair (key/value) to other nodes in the cluster + * @param key Object + * @param value Object + * @return Member - the backup node + * @throws ChannelException Cluster error + */ + @Override + protected Member[] publishEntryInfo(Object key, Object value) throws ChannelException { + Log log = getLog(); + if (! (key instanceof Serializable && value instanceof Serializable) ) { + return new Member[0]; + } + Member[] members = getMapMembers(); + int firstIdx = getNextBackupIndex(); + int nextIdx = firstIdx; + Member[] backup = new Member[0]; + + //there are no backups + if ( members.length == 0 || firstIdx == -1 ) { + return backup; + } + + boolean success = false; + do { + //select a backup node + Member next = members[nextIdx]; + + //increment for the next round of back up selection + nextIdx = nextIdx + 1; + if ( nextIdx >= members.length ) { + nextIdx = 0; + } + + if (next == null) { + continue; + } + MapMessage msg = null; + try { + Member[] tmpBackup = wrap(next); + //publish the backup data to one node + msg = new MapMessage(getMapContextName(), MapMessage.MSG_BACKUP, false, + (Serializable) key, (Serializable) value, null, channel.getLocalMember(false), tmpBackup); + if ( log.isTraceEnabled() ) { + log.trace("Publishing backup data:"+msg+" to: "+next.getName()); + } + UniqueId id = getChannel().send(tmpBackup, msg, getChannelSendOptions()); + if ( log.isTraceEnabled() ) { + log.trace("Data published:"+msg+" msg Id:"+id); + } + //we published out to a backup, mark the test success + success = true; + backup = tmpBackup; + }catch ( ChannelException x ) { + log.error(sm.getString("lazyReplicatedMap.unableReplicate.backup", key, next, x.getMessage()), x); + continue; + } + try { + //publish the data out to all nodes + Member[] proxies = excludeFromSet(backup, getMapMembers()); + if (success && proxies.length > 0 ) { + msg = new MapMessage(getMapContextName(), MapMessage.MSG_PROXY, false, + (Serializable) key, null, null, channel.getLocalMember(false),backup); + if ( log.isTraceEnabled() ) { + log.trace("Publishing proxy data:"+msg+" to: "+Arrays.toNameString(proxies)); + } + getChannel().send(proxies, msg, getChannelSendOptions()); + } + }catch ( ChannelException x ) { + //log the error, but proceed, this should only happen if a node went down, + //and if the node went down, then it can't receive the message, the others + //should still get it. + log.error(sm.getString("lazyReplicatedMap.unableReplicate.proxy", key, next, x.getMessage()), x); + } + } while ( !success && (firstIdx!=nextIdx)); + return backup; + } + + + private Log getLog() { + if (log == null) { + synchronized (this) { + if (log == null) { + log = LogFactory.getLog(LazyReplicatedMap.class); + } + } + } + return log; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings.properties new file mode 100644 index 0000000..b85b37c --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings.properties @@ -0,0 +1,57 @@ +# 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. + +abstractReplicatedMap.broadcast.noReplies=broadcast received 0 replies, probably a timeout. +abstractReplicatedMap.heartbeat.failed=Unable to send AbstractReplicatedMap.ping message +abstractReplicatedMap.init.completed=AbstractReplicatedMap[{0}] initialization was completed in [{1}] ms. +abstractReplicatedMap.init.start=Initializing AbstractReplicatedMap with context name:[{0}] +abstractReplicatedMap.leftOver.ignored=Message[{0}] is ignored. +abstractReplicatedMap.leftOver.pingMsg=PING message has been received beyond the timeout period. The map member[{0}] might have been removed from the map membership. +abstractReplicatedMap.mapMember.unavailable=Member[{0}] is not available yet. +abstractReplicatedMap.mapMemberAdded.added=Map member added:[{0}] +abstractReplicatedMap.mapMemberAdded.nullMember=Notified member is not registered in the membership:[{0}]. +abstractReplicatedMap.newBackup=Primary choosing a new backup +abstractReplicatedMap.newPrimary=Backup becoming primary +abstractReplicatedMap.ping.stateTransferredMember=Member[{0}] is state transferred but not available yet. +abstractReplicatedMap.ping.timeout=Member[{0}] in the Map[{1}] has timed-out in the ping processing. +abstractReplicatedMap.primaryDisappeared=Primary disappeared +abstractReplicatedMap.relocate.complete=Relocation of map entries was complete in [{0}] ms. +abstractReplicatedMap.removeOrphan=Removing orphaned proxy +abstractReplicatedMap.transferState.noReplies=Transfer state, 0 replies, probably a timeout. +abstractReplicatedMap.unable.deserialize.MapMessage=Unable to deserialize MapMessage. +abstractReplicatedMap.unable.diffObject=Unable to diff object. Will replicate the entire object instead. +abstractReplicatedMap.unable.get=Unable to replicate out data for an AbstractReplicatedMap.get operation +abstractReplicatedMap.unable.put=Unable to replicate out data for an AbstractReplicatedMap.put operation +abstractReplicatedMap.unable.relocate=Unable to relocate[{0}] to a new backup node +abstractReplicatedMap.unable.remove=Unable to replicate out data for an AbstractReplicatedMap.remove operation +abstractReplicatedMap.unable.replicate=Unable to replicate data. +abstractReplicatedMap.unable.retrieve=Unable to retrieve remote object for key:[{0}] +abstractReplicatedMap.unable.transferState=Unable to transfer AbstractReplicatedMap state. +abstractReplicatedMap.unableApply.diff=Unable to apply diff to key:[{0}] +abstractReplicatedMap.unableSelect.backup=Unable to select backup node. +abstractReplicatedMap.unableSend.startMessage=Unable to send map start message. +abstractReplicatedMap.unableStart=Unable to start replicated map. + +lazyReplicatedMap.unableReplicate.backup=Unable to replicate backup key:[{0}] to backup:[{1}]. Reason:[{2}] +lazyReplicatedMap.unableReplicate.proxy=Unable to replicate proxy key:[{0}] to backup:[{1}]. Reason:[{2}] + +mapMessage.deserialize.error.key=Failed to deserialize MapMessage key +mapMessage.deserialize.error.value=Failed to deserialize MapMessage value + +replicatedMap.member.disappeared=Member[{0}] disappeared. Related map entries will be relocated to the new node. +replicatedMap.member.disappeared.unknown=Member[{0}] disappeared, but was not present in the map. +replicatedMap.relocate.complete=Relocation of map entries was complete in [{0}] ms. +replicatedMap.unable.relocate=Unable to relocate[{0}] to a new backup node +replicatedMap.unableReplicate.completely=Unable to replicate backup key:[{0}]. Success nodes:[{1}]. Failed nodes:[{2}]. diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_cs.properties new file mode 100644 index 0000000..7a26156 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_cs.properties @@ -0,0 +1,23 @@ +# 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. + +abstractReplicatedMap.broadcast.noReplies=relace obdržela 0 odpovÄ›dí, pravdÄ›podobnÄ› timeout. +abstractReplicatedMap.leftOver.ignored=Zpráva [{0}] je ignorována. +abstractReplicatedMap.unable.get=Nelze replikovat výstupní data pro operaci AbstractReplicatedMap.get + +lazyReplicatedMap.unableReplicate.proxy=Nelze replikovat proxy klíÄ:[{0}] do zálohy:[{1}]. Důvod:[{2}] + +replicatedMap.relocate.complete=PÅ™emístÄ›ní mapy záznamů bylo dokonÄeno za [{0}] ms. +replicatedMap.unable.relocate=[{0}] nelze pÅ™emístit na nový záložní uzel diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_de.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_de.properties new file mode 100644 index 0000000..8e4325d --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_de.properties @@ -0,0 +1,20 @@ +# 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. + +abstractReplicatedMap.broadcast.noReplies=Broadcast hat keine Antworten erhalten; Vermutlich eine Zeitüberschreitung +abstractReplicatedMap.leftOver.ignored=Nachricht [{0}] wird ignoriert. + +replicatedMap.relocate.complete=Das Verschieben der Map Einträge ist fertig und hat [{0}] ms gedauert. +replicatedMap.unable.relocate=[{0}] kann nicht auch einen Backup-Node umgezogen werden diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_es.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_es.properties new file mode 100644 index 0000000..f77e9f2 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_es.properties @@ -0,0 +1,23 @@ +# 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. + +abstractReplicatedMap.broadcast.noReplies=Se recibieron 0 repuestas del broadcast, posiblemente un timeout +abstractReplicatedMap.leftOver.ignored=Mensaje [{0}] ignorado. +abstractReplicatedMap.unable.get=Imposilbe replicar datos fuera de la operación AbstractReplicatedMap.get + +lazyReplicatedMap.unableReplicate.proxy=Imposible replicar llave de proxy:[{0}] hacia el respaldo:[{1}]. Motivo:[{2}] + +replicatedMap.relocate.complete=La reubicación de las entradas de mapas fue completada en [{0}] ms. +replicatedMap.unable.relocate=No se pudo mover [{0}] a un nuevo nodo de respaldo diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_fr.properties new file mode 100644 index 0000000..2e2b340 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_fr.properties @@ -0,0 +1,57 @@ +# 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. + +abstractReplicatedMap.broadcast.noReplies=La diffusion (broadcast) n'a pas recu de réponse, probablement un dépassement du temps imparti +abstractReplicatedMap.heartbeat.failed=Impossible d'envoyer le message AbstractReplicatedMap.ping +abstractReplicatedMap.init.completed=L''initialisation de l''AbstractReplicatedMap[{0}] s''est terminé en [{1}] ms +abstractReplicatedMap.init.start=Initialisation de l''AbstractReplicatedMap avec le nom de contexte [{0}] +abstractReplicatedMap.leftOver.ignored=Le message [{0}] a été ignoré. +abstractReplicatedMap.leftOver.pingMsg=Le délai d''attente a été dépassé pour le message PING, le membre [{0}] a pu être enlevé de la structure du registre de membres +abstractReplicatedMap.mapMember.unavailable=Le membre [{0}] n''est pas encore disponible +abstractReplicatedMap.mapMemberAdded.added=Le membre de la structure répliquée a été ajouté : [{0}] +abstractReplicatedMap.mapMemberAdded.nullMember=Notifié que le membre n''est pas disponible dans le registre de membres : [{0}] +abstractReplicatedMap.newBackup=Le noeud primaire choisit une nouvelle sauvegarde +abstractReplicatedMap.newPrimary=Le noeud de sauvegarde devient le primaire +abstractReplicatedMap.ping.stateTransferredMember=L''état du membre [{0}] est transféré mais il n''est pas encore disponible +abstractReplicatedMap.ping.timeout=Le membre [{0}] dans la carte répliquée [{1}] a fait un timeout lors du traitement d''un ping +abstractReplicatedMap.primaryDisappeared=Le noeud primaire a disparu +abstractReplicatedMap.relocate.complete=La relocation des entrées de la structure répliquée a été finie en [{0}] ms +abstractReplicatedMap.removeOrphan=Retrait d'un proxy orphelin +abstractReplicatedMap.transferState.noReplies=État de transfert, 0 réponses, probablement un timeout +abstractReplicatedMap.unable.deserialize.MapMessage=Impossible de désérialiser MapMessage +abstractReplicatedMap.unable.diffObject=Impossible d'obtenir les différences de l'objet, il sera entièrement répliqué +abstractReplicatedMap.unable.get=Impossible de répliquer les données de sortie pour une opération AbstractReplicatedMap.get +abstractReplicatedMap.unable.put=Impossible de répliquer les données de sortie pour une opération AbstractReplicatedMap.put +abstractReplicatedMap.unable.relocate=Impossible de déplacer [{0}] sur un nouveau nÅ“ud de sauvegarde +abstractReplicatedMap.unable.remove=Impossible de répliquer les données de sortie pour une opération AbstractReplicatedMap.remove +abstractReplicatedMap.unable.replicate=Impossible de répliquer les données +abstractReplicatedMap.unable.retrieve=Impossible de récupérer les objets distants pour les clés : [{0}] +abstractReplicatedMap.unable.transferState=Impossible de transférer l'état de la AbstractReplicatedMap +abstractReplicatedMap.unableApply.diff=Impossible d''appliquer le diff à la clé : [{0}] +abstractReplicatedMap.unableSelect.backup=Impossible de choisir un nÅ“ud de sauvegarde +abstractReplicatedMap.unableSend.startMessage=Impossible d'envoyer le message de démarrage de la structure répliquée +abstractReplicatedMap.unableStart=Impossible de démarrer la structure répliquée + +lazyReplicatedMap.unableReplicate.backup=Impossible de répliquer la clé de sauvegarde : [{0}] +lazyReplicatedMap.unableReplicate.proxy=Impossible de copier la clé de proxy : [{0}] sur la sauvegarde (backup) : [{1}]. Raison : [{2}] + +mapMessage.deserialize.error.key=Erreur de désérialisation de la clé du MapMessage +mapMessage.deserialize.error.value=Erreur de désérialisation de la valeur du MapMessage + +replicatedMap.member.disappeared=Le membre [{0}] a disparu, les entrées correspondantes seront déplacées sur un nouveau nÅ“ud +replicatedMap.member.disappeared.unknown=Le membre [{0}] a disparu, mais il n''était pas présent dans la liste +replicatedMap.relocate.complete=La relocation des entrées de la structure répliquée a été accomplie en [{0}] ms +replicatedMap.unable.relocate=Impossible de déplacer [{0}] sur un nouveau noeud auxiliaire. +replicatedMap.unableReplicate.completely=Impossible de répliquer la clé de sauvegarde : [{0}], succès pour les nÅ“uds : [{1}], échec pour les nÅ“uds : [{2}] diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_ja.properties new file mode 100644 index 0000000..7c43b56 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_ja.properties @@ -0,0 +1,57 @@ +# 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. + +abstractReplicatedMap.broadcast.noReplies=æらãタイムアウトãŒç™ºç”Ÿã—ãŸãŸã‚ã€ãƒ–ロードキャストã®è¿”信㯠0 ã§ã—ãŸã€‚ +abstractReplicatedMap.heartbeat.failed=AbstractReplicatedMap.ping メッセージをé€ä¿¡ã§ãã¾ã›ã‚“ +abstractReplicatedMap.init.completed=AbstractReplicatedMap [{0}] ã®åˆæœŸåŒ–㯠[{1}] ミリ秒ã§å®Œäº†ã—ã¾ã—ãŸã€‚ +abstractReplicatedMap.init.start=コンテキストå:[{0}]を使用ã—ã¦AbstractReplicatedMapã‚’åˆæœŸåŒ–ã—ã¦ã„ã¾ã™ã€‚ +abstractReplicatedMap.leftOver.ignored=メッセージ [{0}] ã¯ç„¡è¦–ã—ã¾ã™ã€‚ +abstractReplicatedMap.leftOver.pingMsg=PINGメッセージãŒã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆæœŸé–“を超ãˆã¦å—ä¿¡ã•ã‚Œã¾ã—ãŸã€‚ マップメンãƒãƒ¼[{0}]ã¯ãƒžãƒƒãƒ—メンãƒãƒ¼ã‚·ãƒƒãƒ—ã‹ã‚‰å‰Šé™¤ã•ã‚Œã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +abstractReplicatedMap.mapMember.unavailable=メンãƒãƒ¼[{0}]ã¯ã¾ã åˆ©ç”¨ã§ãã¾ã›ã‚“。 +abstractReplicatedMap.mapMemberAdded.added=マップメンãƒãƒ¼ãŒè¿½åŠ ã•ã‚Œã¾ã—ãŸï¼š[{0}] +abstractReplicatedMap.mapMemberAdded.nullMember=通知ã•ã‚ŒãŸãƒ¡ãƒ³ãƒãƒ¼ã¯ãƒ¡ãƒ³ãƒãƒ¼ã‚·ãƒƒãƒ—ã«ç™»éŒ²ã•ã‚Œã¦ã„ã¾ã›ã‚“:[{0}] +abstractReplicatedMap.newBackup=Primary ãŒæ–°ã—ã„ backup ã‚’é¸ã‚“ã§ã„ã¾ã™ +abstractReplicatedMap.newPrimary=Backup ㌠primary ã«ãªã‚Šã¾ã—㟠+abstractReplicatedMap.ping.stateTransferredMember=メンãƒãƒ¼ [{0}] ã¯çŠ¶æ…‹ã‚’転é€ä¸­ã§åˆ©ç”¨å¯èƒ½ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +abstractReplicatedMap.ping.timeout=マップ [{1}] ã®ãƒ¡ãƒ³ãƒãƒ¼ [{0}] ãŒping処ç†ã§ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã—ãŸã€‚ +abstractReplicatedMap.primaryDisappeared=Primary ãŒæ¶ˆãˆã¾ã—㟠+abstractReplicatedMap.relocate.complete=マップè¦ç´ ã®å†é…置㯠[{0}] ミリ秒ã§å®Œäº†ã—ã¾ã—ãŸã€‚ +abstractReplicatedMap.removeOrphan=孤立ã—ãŸãƒ—ロキシを削除ã—ã¾ã—㟠+abstractReplicatedMap.transferState.noReplies=転é€çŠ¶æ…‹ã€0 レスãƒãƒ³ã‚¹ã€ãŠãらãタイムアウトã§ã™ã€‚ +abstractReplicatedMap.unable.deserialize.MapMessage=MapMessageã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºãŒã§ãã¾ã›ã‚“。 +abstractReplicatedMap.unable.diffObject=オブジェクトã®diffã‚’å–ã‚Œã¾ã›ã‚“。 代ã‚ã‚Šã«ã‚ªãƒ–ジェクト全体を複製ã—ã¾ã™ã€‚ +abstractReplicatedMap.unable.get=AbstractReplicatedMap.get オペレーションã§ãƒ‡ãƒ¼ã‚¿ã®è¤‡è£½ãŒã§ãã¾ã›ã‚“ +abstractReplicatedMap.unable.put=AbstractReplicatedMap.putオペレーションã®ãƒ‡ãƒ¼ã‚¿ã‚’レプリケートã§ãã¾ã›ã‚“ +abstractReplicatedMap.unable.relocate=[{0}] ã‚’æ–°ã—ã„ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—ノードã¸å†é…ç½®ã§ãã¾ã›ã‚“ +abstractReplicatedMap.unable.remove=AbstractReplicatedMap.remove オペレーションã®ãƒ‡ãƒ¼ã‚¿ã‚’レプリケートã§ãã¾ã›ã‚“ +abstractReplicatedMap.unable.replicate=データを複製ã§ãã¾ã›ã‚“。 +abstractReplicatedMap.unable.retrieve=キー:[{0}]ã®ãƒªãƒ¢ãƒ¼ãƒˆã‚ªãƒ–ジェクトをå–å¾—ã§ãã¾ã›ã‚“ +abstractReplicatedMap.unable.transferState=AbstractReplicatedMapã®çŠ¶æ…‹ã‚’転é€ã§ãã¾ã›ã‚“。 +abstractReplicatedMap.unableApply.diff=キー [{0}] ã®å·®åˆ†ã‚’計算ã§ãã¾ã›ã‚“ +abstractReplicatedMap.unableSelect.backup=ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—ノードをé¸æŠžã§ãã¾ã›ã‚“。 +abstractReplicatedMap.unableSend.startMessage=マップ開始メッセージをé€ä¿¡ã§ãã¾ã›ã‚“。 +abstractReplicatedMap.unableStart=レプリケーションマップã®èµ·å‹•ãŒã§ãã¾ã›ã‚“。 + +lazyReplicatedMap.unableReplicate.backup=[{2}] ã«ã‚ˆã‚Šã€ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—キー [{0}] をメンãƒãƒ¼ [{1}] ã¸è¤‡è£½ã§ãã¾ã›ã‚“。 +lazyReplicatedMap.unableReplicate.proxy=プロキシキーを [{0}] ã‹ã‚‰ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—ã® [{1}] ã¸è¤‡è£½ã§ãã¾ã›ã‚“。ç†ç”±:[{2}] + +mapMessage.deserialize.error.key=MapMessage キーã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +mapMessage.deserialize.error.value=MapMessageã®å€¤ã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºã«å¤±æ•—ã—ã¾ã—ãŸã€‚ + +replicatedMap.member.disappeared=メンãƒãƒ¼[{0}]ãŒæ¶ˆæ»…ã—ã¾ã—ãŸã€‚関連ã™ã‚‹ãƒžãƒƒãƒ—エントリã¯ã€æ–°ã—ã„ノードã«å†é…ç½®ã•ã‚Œã¾ã™ã€‚ +replicatedMap.member.disappeared.unknown=Member [{0}] ã¯æ¶ˆãˆã¾ã—ãŸãŒã€ãƒžãƒƒãƒ—ã«ã¯å­˜åœ¨ã—ã¾ã›ã‚“ã§ã—ãŸã€‚ +replicatedMap.relocate.complete=マップè¦ç´ ã®å†é…置㯠[{0}] ミリ秒ã§å®Œäº†ã—ã¾ã—ãŸã€‚ +replicatedMap.unable.relocate=[{0}]ã‚’æ–°ã—ã„ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—ノードã«å†é…ç½®ã§ãã¾ã›ã‚“ +replicatedMap.unableReplicate.completely=ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—キー[{0}]を複製ã§ãã¾ã›ã‚“。 æˆåŠŸãƒŽãƒ¼ãƒ‰ï¼š[{1}]。 障害ノード:[{2}]。 diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_ko.properties new file mode 100644 index 0000000..f430120 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_ko.properties @@ -0,0 +1,52 @@ +# 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. + +abstractReplicatedMap.broadcast.noReplies=broadcast 메소드가 0ê°œì˜ ì‘ë‹µì„ ë°›ì•˜ìŠµë‹ˆë‹¤. ì•„ë§ˆë„ ì œí•œ 시간 ì´ˆê³¼ì¸ ë“¯ 합니다. +abstractReplicatedMap.heartbeat.failed=AbstractReplicatedMap.ping 메시지를 보낼 수 없습니다. +abstractReplicatedMap.init.completed=AbstractReplicatedMap[{0}] 초기화가 [{1}] 밀리초 ë‚´ì— ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. +abstractReplicatedMap.init.start=컨í…스트 ì´ë¦„ [{0}]ì„(를) 사용하여 AbstractReplicatedMapì„ ì´ˆê¸°í™”í•©ë‹ˆë‹¤. +abstractReplicatedMap.leftOver.ignored=메시지 [{0}]ì€(는) 무시ë©ë‹ˆë‹¤. +abstractReplicatedMap.leftOver.pingMsg=PING 메시지가 제한 ì‹œê°„ì„ ì´ˆê³¼í•˜ì—¬ 수신ë˜ì—ˆìŠµë‹ˆë‹¤. 해당 map 멤버 [{0}]ì€(는), map 멤버십으로부터 ì´ë¯¸ 제거ë˜ì—ˆì„ 수 있습니다. +abstractReplicatedMap.mapMember.unavailable=멤버 [{0}]ì€(는) ì•„ì§ ê°€ìš©í•˜ì§€ 않습니다. +abstractReplicatedMap.mapMemberAdded.added=Map 멤버가 추가ë˜ì—ˆìŠµë‹ˆë‹¤:[{0}] +abstractReplicatedMap.mapMemberAdded.nullMember=í†µì§€ëœ ë©¤ë²„ëŠ” ë©¤ë²„ì‹­ì— ë“±ë¡ë˜ì–´ 있지 않습니다: [{0}] +abstractReplicatedMap.ping.stateTransferredMember=멤버 [{0}]ì´(ê°€), ìƒíƒœê°€ ì´ì „ëœ ìƒíƒœì´ì§€ë§Œ, ì•„ì§ ê°€ìš©í•˜ì§€ 않습니다. +abstractReplicatedMap.ping.timeout=Map [{1}] ë‚´ì˜ ë©¤ë²„ [{0}]ì€(는) ping 처리 ì¤‘ì— ì œí•œ 시간 초과ë˜ì—ˆìŠµë‹ˆë‹¤. +abstractReplicatedMap.relocate.complete=Map ì—”íŠ¸ë¦¬ë“¤ì„ ìž¬ìœ„ì¹˜ì‹œí‚¤ëŠ” ìž‘ì—…ì´ [{0}] ë°€ë¦¬ì´ˆì— ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. +abstractReplicatedMap.transferState.noReplies=transferStateì—ì„œ ì‘ë‹µì´ í•˜ë‚˜ë„ ì—†ì—ˆìŠµë‹ˆë‹¤. ì•„ë§ˆë„ ì œí•œ 시간 ì´ˆê³¼ëœ ë“¯í•©ë‹ˆë‹¤. +abstractReplicatedMap.unable.deserialize.MapMessage=MapMessage를 ì—­ì§ë ¬í™”í•  수 없습니다. +abstractReplicatedMap.unable.diffObject=ê°ì²´ì— 대한 diff를 ìƒì„±í•  수 없습니다. 대신 ì „ì²´ ê°ì²´ë¥¼ 복제하겠습니다. +abstractReplicatedMap.unable.get=AbstractReplicatedMap.get 오í¼ë ˆì´ì…˜ì—ì„œ, ë°ì´í„°ë¥¼ 복제하여 반환할 수 없습니다. +abstractReplicatedMap.unable.put=AbstractReplicatedMap.put 오í¼ë ˆì´ì…˜ì„ 위한 ë°ì´í„°ë¥¼ 외부로 복제할 수 없습니다. +abstractReplicatedMap.unable.relocate=[{0}]ì„(를) 새로운 백업 노드로 재위치시킬 수 없습니다. +abstractReplicatedMap.unable.remove=AbstractReplicatedMap.remove 오í¼ë ˆì´ì…˜ì—ì„œ, 복제를 위한 ë°ì´í„°ë¥¼ 전송할 수 없습니다. +abstractReplicatedMap.unable.replicate=ë°ì´í„°ë¥¼ 복제할 수 없습니다. +abstractReplicatedMap.unable.retrieve=키 [{0}]ì„(를) 위한 ì›ê²© ê°ì²´ë¥¼ 검색할 수 없습니다. +abstractReplicatedMap.unable.transferState=AbstractReplicatedMap ìƒíƒœë¥¼ ì „ì´ì‹œí‚¬ 수 없습니다. +abstractReplicatedMap.unableApply.diff=키 [{0}]ì— diff를 ì ìš©í•  수 없습니다. +abstractReplicatedMap.unableSelect.backup=백업 노드를 ì„ íƒí•  수 없습니다. +abstractReplicatedMap.unableSend.startMessage=Map 시작 메시지를 전송할 수 없습니다. +abstractReplicatedMap.unableStart=ReplicatedMapì„ ì‹œìž‘í•  수 없습니다. + +lazyReplicatedMap.unableReplicate.backup=백업 키 [{0}]ì„(를) 백업 [{1}]ì— ë³µì œí•  수 없습니다. 사유:[{2}] +lazyReplicatedMap.unableReplicate.proxy=프ë¡ì‹œ 키 [{0}]ì„(를) ë°±ì—…ì¸ [{1}](으)ë¡œ 복제할 수 없습니다. 사유: [{2}] + +mapMessage.deserialize.error.key=MapMessage.key()ì—ì„œ ì—­ì§ë ¬í™”ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. +mapMessage.deserialize.error.value=MapMessage ê°’ì„ ì—­ì§ë ¬í™”하지 못했습니다. + +replicatedMap.member.disappeared=멤버 [{0}]ì´(ê°€) 사ë¼ì¡ŒìŠµë‹ˆë‹¤. 관련 Mapì˜ ì—”íŠ¸ë¦¬ë“¤ì€ ìƒˆ 노드로 다시 위치시킬 것입니다. +replicatedMap.relocate.complete=Mapì˜ ì—”íŠ¸ë¦¬ë“¤ ëª¨ë‘ ë‹¤ì‹œ 위치시켰습니다. 소요시간: [{0}] 밀리초. +replicatedMap.unable.relocate=멤버 [{0}]ì— ìƒˆë¡œìš´ 백업 노드를 다시 지정할 수 없습니다. +replicatedMap.unableReplicate.completely=백업 키 [{0}]ì„(를) 복제할 수 없습니다. 성공 노드들: [{1}]. 실패 노드들: [{2}] diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_pt_BR.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..6cec62e --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_pt_BR.properties @@ -0,0 +1,21 @@ +# 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. + +abstractReplicatedMap.leftOver.ignored=Mensagem[{0}] é ignorada +abstractReplicatedMap.unable.get=Impossível replicar dados para uma operação AbstractReplicatedMap.get. + +lazyReplicatedMap.unableReplicate.proxy=Impossível replicar a chave de proxy [{0}] ao backup [{1}], pois: [{2}] + +replicatedMap.unable.relocate=Impossível realocar [{0}] para um novo nó de backup diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_ru.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_ru.properties new file mode 100644 index 0000000..bd15295 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +abstractReplicatedMap.leftOver.ignored=Сообщение[{0}] проигнорировано. diff --git a/java/org/apache/catalina/tribes/tipis/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/tipis/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..575ff86 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/LocalStrings_zh_CN.properties @@ -0,0 +1,52 @@ +# 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. + +abstractReplicatedMap.broadcast.noReplies=广播收到0回å¤ï¼Œå¯èƒ½æ˜¯è¶…时了。 +abstractReplicatedMap.heartbeat.failed=无法å‘é€AbstractReplicatedMap.pingæ¶ˆæ¯ +abstractReplicatedMap.init.completed=AbstractReplicatedMap[{0}]åˆå§‹åŒ–在[{1}]ms内完æˆã€‚ +abstractReplicatedMap.init.start=正在åˆå§‹åŒ–上下文å称为:[{0}]çš„AbstractReplicatedMap +abstractReplicatedMap.leftOver.ignored=消æ¯[{0}]被忽略 +abstractReplicatedMap.leftOver.pingMsg=PING消æ¯å·²è¶…过超时时间。映射æˆå‘˜[{0}]å¯èƒ½å·²ä»Žæ˜ å°„æˆå‘˜èº«ä»½ä¸­åˆ é™¤ã€‚ +abstractReplicatedMap.mapMember.unavailable=Member[{0}]还ä¸å¯ç”¨ +abstractReplicatedMap.mapMemberAdded.added=已添加映射æˆå‘˜ï¼š[{0}] +abstractReplicatedMap.mapMemberAdded.nullMember=通知的æˆå‘˜æœªæ³¨å†Œï¼š[{0}] +abstractReplicatedMap.ping.stateTransferredMember=æˆå‘˜[{0}]已状æ€è½¬ç§»ï¼Œä½†å°šä¸å¯ç”¨ã€‚ +abstractReplicatedMap.ping.timeout=映射[{1}]中的æˆå‘˜[{0}]在ping处ç†ä¸­è¶…时。 +abstractReplicatedMap.relocate.complete=已在[{0}]ms内完æˆæ˜ å°„项的é‡æ–°å®šä½ã€‚ +abstractReplicatedMap.transferState.noReplies=传输状æ€ï¼Œ0å“应,也许是超时。 +abstractReplicatedMap.unable.deserialize.MapMessage=无法ååºåˆ—化映射消æ¯ã€‚ +abstractReplicatedMap.unable.diffObject=无法区分对象。将å¤åˆ¶æ•´ä¸ªå¯¹è±¡ã€‚ +abstractReplicatedMap.unable.get=无法å¤åˆ¶AbstractReplicatedMap.getæ“ä½œçš„æ•°æ® +abstractReplicatedMap.unable.put=无法å¤åˆ¶AbstractReplicatedMap.Putæ“ä½œçš„æ•°æ® +abstractReplicatedMap.unable.relocate=无法将[{0}]é‡æ–°å®šä½åˆ°æ–°çš„备份节点 +abstractReplicatedMap.unable.remove=无法为AbstractReplicatedMap.removeæ“作å¤åˆ¶æ•°æ® +abstractReplicatedMap.unable.replicate=无法å¤åˆ¶æ•°æ®ã€‚ +abstractReplicatedMap.unable.retrieve=无法获å–远程对象,主键:[{0}] +abstractReplicatedMap.unable.transferState=无法传输AbstractReplicatedMapçŠ¶æ€ +abstractReplicatedMap.unableApply.diff=无法将diff应用于键:[{0}] +abstractReplicatedMap.unableSelect.backup=无法选择备用节点 +abstractReplicatedMap.unableSend.startMessage=无法å‘é€mapå¯åŠ¨æ¶ˆæ¯ã€‚ +abstractReplicatedMap.unableStart=无法å¯åŠ¨å¤åˆ¶Map + +lazyReplicatedMap.unableReplicate.backup=无法将备份密钥:[{0}]å¤åˆ¶åˆ°å¤‡ä»½ï¼š[{1}]。原因:[{2}] +lazyReplicatedMap.unableReplicate.proxy=ä¸èƒ½å¤åˆ¶proxy key:[{0}]到备份:[{1}]. 原因是:[{2}] + +mapMessage.deserialize.error.key=ååºåˆ—化MapMessage主键失败 +mapMessage.deserialize.error.value=MapMessage.valueçš„ååºåˆ—化误差 + +replicatedMap.member.disappeared=æˆå‘˜[{0}]消失,关è”的键值实体会é‡æ–°å…³è”到一个新的节点。 +replicatedMap.relocate.complete=map æ¡ç›®çš„é‡å®šä½åœ¨ [{0}] ms内完æˆã€‚ +replicatedMap.unable.relocate=ä¸èƒ½ä¸ºä¸€ä¸ªæ–°çš„备份节点é‡å¯å®šä½[{0}] +replicatedMap.unableReplicate.completely=无法å¤åˆ¶å¤‡ä»½ä¸»é”®ï¼š[{0}]。æˆåŠŸèŠ‚点:[{1}]。失败节点:[{2}] diff --git a/java/org/apache/catalina/tribes/tipis/ReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/ReplicatedMap.java new file mode 100644 index 0000000..05f28b1 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/ReplicatedMap.java @@ -0,0 +1,298 @@ +/* + * 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.catalina.tribes.tipis; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelException.FaultyMember; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.RemoteProcessException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * All-to-all replication for a hash map implementation. Each node in the cluster will carry an identical + * copy of the map.

    + * This map implementation doesn't have a background thread running to replicate changes. + * If you do have changes without invoking put/remove then you need to invoke one of the following methods: + *
      + *
    • replicate(Object,boolean) - replicates only the object that belongs to the key
    • + *
    • replicate(boolean) - Scans the entire map for changes and replicates data
    • + *
    + * the boolean value in the replicate method used to decide + * whether to only replicate objects that implement the ReplicatedMapEntry interface + * or to replicate all objects. If an object doesn't implement the ReplicatedMapEntry interface + * each time the object gets replicated the entire object gets serialized, hence a call to replicate(true) + * will replicate all objects in this map that are using this node as primary. + * + *

    REMEMBER TO CALL breakdown() + * when you are done with the map to avoid memory leaks.

    + * TODO implement periodic sync/transfer thread
    + * TODO memberDisappeared, should do nothing except change map membership + * by default it relocates the primary objects + * + * @param The type of Key + * @param The type of Value + */ +public class ReplicatedMap extends AbstractReplicatedMap { + + private static final long serialVersionUID = 1L; + + // Lazy init to support serialization + private transient volatile Log log; + + //-------------------------------------------------------------------------- + // CONSTRUCTORS / DESTRUCTORS + //-------------------------------------------------------------------------- + /** + * Creates a new map + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param initialCapacity int - the size of this map, see HashMap + * @param loadFactor float - load factor, see HashMap + * @param cls Class loaders + */ + public ReplicatedMap(MapOwner owner, Channel channel, long timeout, String mapContextName, int initialCapacity,float loadFactor, ClassLoader[] cls) { + super(owner,channel, timeout, mapContextName, initialCapacity, loadFactor, Channel.SEND_OPTIONS_DEFAULT, cls, true); + } + + /** + * Creates a new map + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param initialCapacity int - the size of this map, see HashMap + * @param cls Class loaders + */ + public ReplicatedMap(MapOwner owner, Channel channel, long timeout, String mapContextName, int initialCapacity, ClassLoader[] cls) { + super(owner,channel, timeout, mapContextName, initialCapacity, DEFAULT_LOAD_FACTOR, + Channel.SEND_OPTIONS_DEFAULT, cls, true); + } + + /** + * Creates a new map + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param cls Class loaders + */ + public ReplicatedMap(MapOwner owner, Channel channel, long timeout, String mapContextName, ClassLoader[] cls) { + super(owner, channel, timeout, mapContextName, DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, + Channel.SEND_OPTIONS_DEFAULT, cls, true); + } + + /** + * Creates a new map + * @param owner The map owner + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messages + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param cls Class loaders + * @param terminate boolean - Flag for whether to terminate this map that failed to start. + */ + public ReplicatedMap(MapOwner owner, Channel channel, long timeout, String mapContextName, ClassLoader[] cls, boolean terminate) { + super(owner, channel, timeout, mapContextName, DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, + Channel.SEND_OPTIONS_DEFAULT, cls, terminate); + } + +//------------------------------------------------------------------------------ +// METHODS TO OVERRIDE +//------------------------------------------------------------------------------ + @Override + protected int getStateMessageType() { + return AbstractReplicatedMap.MapMessage.MSG_STATE_COPY; + } + + @Override + protected int getReplicateMessageType() { + return AbstractReplicatedMap.MapMessage.MSG_COPY; + } + + /** + * publish info about a map pair (key/value) to other nodes in the cluster + * @param key Object + * @param value Object + * @return Member - the backup node + * @throws ChannelException Cluster error + */ + @Override + protected Member[] publishEntryInfo(Object key, Object value) throws ChannelException { + if (! (key instanceof Serializable && value instanceof Serializable) ) { + return new Member[0]; + } + //select a backup node + Member[] backup = getMapMembers(); + + if (backup == null || backup.length == 0) { + return null; + } + + try { + //publish the data out to all nodes + MapMessage msg = new MapMessage(getMapContextName(), MapMessage.MSG_COPY, false, + (Serializable) key, (Serializable) value, null,channel.getLocalMember(false), backup); + + getChannel().send(backup, msg, getChannelSendOptions()); + } catch (ChannelException e) { + FaultyMember[] faultyMembers = e.getFaultyMembers(); + if (faultyMembers.length == 0) { + throw e; + } + List faulty = new ArrayList<>(); + for (FaultyMember faultyMember : faultyMembers) { + if (!(faultyMember.getCause() instanceof RemoteProcessException)) { + faulty.add(faultyMember.getMember()); + } + } + Member[] realFaultyMembers = faulty.toArray(new Member[0]); + if (realFaultyMembers.length != 0) { + backup = excludeFromSet(realFaultyMembers, backup); + if (backup.length == 0) { + throw e; + } else { + if (getLog().isWarnEnabled()) { + getLog().warn(sm.getString("replicatedMap.unableReplicate.completely", key, + Arrays.toString(backup), Arrays.toString(realFaultyMembers)), e); + } + } + } + } + return backup; + } + + @Override + public void memberDisappeared(Member member) { + boolean removed = false; + Log log = getLog(); + synchronized (mapMembers) { + removed = (mapMembers.remove(member) != null ); + if (!removed) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("replicatedMap.member.disappeared.unknown", member)); + } + return; //the member was not part of our map. + } + } + if (log.isInfoEnabled()) { + log.info(sm.getString("replicatedMap.member.disappeared", member)); + } + long start = System.currentTimeMillis(); + for (Entry> e : innerMap.entrySet()) { + MapEntry entry = innerMap.get(e.getKey()); + if (entry==null) { + continue; + } + if (entry.isPrimary()) { + try { + Member[] backup = getMapMembers(); + if (backup.length > 0) { + MapMessage msg = new MapMessage(getMapContextName(), MapMessage.MSG_NOTIFY_MAPMEMBER,false, + (Serializable)entry.getKey(),null,null,channel.getLocalMember(false),backup); + getChannel().send(backup, msg, getChannelSendOptions()); + } + entry.setBackupNodes(backup); + entry.setPrimary(channel.getLocalMember(false)); + } catch (ChannelException x) { + log.error(sm.getString("replicatedMap.unable.relocate", entry.getKey()), x); + } + } else if (member.equals(entry.getPrimary())) { + entry.setPrimary(null); + } + + if ( entry.getPrimary() == null && + entry.isCopy() && + entry.getBackupNodes()!=null && + entry.getBackupNodes().length > 0 && + entry.getBackupNodes()[0].equals(channel.getLocalMember(false)) ) { + try { + entry.setPrimary(channel.getLocalMember(false)); + entry.setBackup(false); + entry.setProxy(false); + entry.setCopy(false); + Member[] backup = getMapMembers(); + if (backup.length > 0) { + MapMessage msg = new MapMessage(getMapContextName(), MapMessage.MSG_NOTIFY_MAPMEMBER,false, + (Serializable)entry.getKey(),null,null,channel.getLocalMember(false),backup); + getChannel().send(backup, msg, getChannelSendOptions()); + } + entry.setBackupNodes(backup); + if ( mapOwner!=null ) { + mapOwner.objectMadePrimary(entry.getKey(),entry.getValue()); + } + + } catch (ChannelException x) { + log.error(sm.getString("replicatedMap.unable.relocate", entry.getKey()), x); + } + } + + } //while + long complete = System.currentTimeMillis() - start; + if (log.isInfoEnabled()) { + log.info(sm.getString("replicatedMap.relocate.complete", + Long.toString(complete))); + } + } + + @Override + public void mapMemberAdded(Member member) { + if ( member.equals(getChannel().getLocalMember(false)) ) { + return; + } + boolean memberAdded = false; + synchronized (mapMembers) { + if (!mapMembers.containsKey(member) ) { + mapMembers.put(member, Long.valueOf(System.currentTimeMillis())); + memberAdded = true; + } + } + if ( memberAdded ) { + synchronized (stateMutex) { + Member[] backup = getMapMembers(); + for (Entry> e : innerMap.entrySet()) { + MapEntry entry = innerMap.get(e.getKey()); + if ( entry == null ) { + continue; + } + if (entry.isPrimary() && !inSet(member,entry.getBackupNodes())) { + entry.setBackupNodes(backup); + } + } + } + } + } + + + private Log getLog() { + if (log == null) { + synchronized (this) { + if (log == null) { + log = LogFactory.getLog(ReplicatedMap.class); + } + } + } + return log; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java b/java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java new file mode 100644 index 0000000..7b97aef --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java @@ -0,0 +1,138 @@ +/* + * 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.catalina.tribes.tipis; + +import java.io.IOException; +import java.io.Serializable; + +/** + * For smarter replication, an object can implement this interface to replicate diffs
    + * The replication logic will call the methods in the following order:
    + * + * 1. if ( entry.isDirty() )
    + * try { + * 2. entry.lock();
    + * 3. byte[] diff = entry.getDiff();
    + * 4. entry.reset();
    + * } finally {
    + * 5. entry.unlock();
    + * }
    + * }
    + *
    + *
    + *
    + * When the data is deserialized the logic is called in the following order
    + * + * 1. ReplicatedMapEntry entry = (ReplicatedMapEntry)objectIn.readObject();
    + * 2. if ( isBackup(entry)||isPrimary(entry) ) entry.setOwner(owner);
    + *
    + */ +public interface ReplicatedMapEntry extends Serializable { + + /** + * Has the object changed since last replication + * and is not in a locked state + * @return boolean + */ + boolean isDirty(); + + /** + * If this returns true, the map will extract the diff using getDiff() + * Otherwise it will serialize the entire object. + * @return boolean + */ + boolean isDiffable(); + + /** + * Returns a diff and sets the dirty map to false + * @return Serialized diff data + * @throws IOException IO error serializing + */ + byte[] getDiff() throws IOException; + + + /** + * Applies a diff to an existing object. + * @param diff Serialized diff data + * @param offset Array offset + * @param length Array length + * @throws IOException IO error deserializing + * @throws ClassNotFoundException Serialization error + */ + void applyDiff(byte[] diff, int offset, int length) throws IOException, ClassNotFoundException; + + /** + * Resets the current diff state and resets the dirty flag + */ + void resetDiff(); + + /** + * Lock during serialization + */ + void lock(); + + /** + * Unlock after serialization + */ + void unlock(); + + /** + * This method is called after the object has been + * created on a remote map. On this method, + * the object can initialize itself for any data that wasn't + * + * @param owner Object + */ + void setOwner(Object owner); + + /** + * For accuracy checking, a serialized attribute can contain a version number + * This number increases as modifications are made to the data. + * The replicated map can use this to ensure accuracy on a periodic basis + * @return long - the version number or -1 if the data is not versioned + */ + long getVersion(); + + /** + * Forces a certain version to a replicated map entry
    + * @param version long + */ + void setVersion(long version); + + /** + * @return the last replicate time. + */ + long getLastTimeReplicated(); + + /** + * Set the last replicate time. + * @param lastTimeReplicated New timestamp + */ + void setLastTimeReplicated(long lastTimeReplicated); + + /** + * If this returns true, to replicate that an object has been accessed + * @return boolean + */ + boolean isAccessReplicate(); + + /** + * Access to an existing object. + */ + void accessEntry(); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/AbstractRxTask.java b/java/org/apache/catalina/tribes/transport/AbstractRxTask.java new file mode 100644 index 0000000..2a95f7d --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/AbstractRxTask.java @@ -0,0 +1,70 @@ +/* + * 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.catalina.tribes.transport; + +import org.apache.catalina.tribes.io.ListenCallback; + +public abstract class AbstractRxTask implements Runnable +{ + + public static final int OPTION_DIRECT_BUFFER = ReceiverBase.OPTION_DIRECT_BUFFER; + + private ListenCallback callback; + private RxTaskPool pool; + private int options; + protected boolean useBufferPool = true; + + public AbstractRxTask(ListenCallback callback) { + this.callback = callback; + } + + public void setTaskPool(RxTaskPool pool) { + this.pool = pool; + } + + public void setOptions(int options) { + this.options = options; + } + + public void setCallback(ListenCallback callback) { + this.callback = callback; + } + + public RxTaskPool getTaskPool() { + return pool; + } + + public int getOptions() { + return options; + } + + public ListenCallback getCallback() { + return callback; + } + + public void close() { + // NO-OP + } + + public void setUseBufferPool(boolean usebufpool) { + useBufferPool = usebufpool; + } + + public boolean getUseBufferPool() { + return useBufferPool; + } +} diff --git a/java/org/apache/catalina/tribes/transport/AbstractSender.java b/java/org/apache/catalina/tribes/transport/AbstractSender.java new file mode 100644 index 0000000..b61a546 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/AbstractSender.java @@ -0,0 +1,355 @@ +/* + * 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.catalina.tribes.transport; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.apache.catalina.tribes.Member; + +public abstract class AbstractSender implements DataSender { + + private volatile boolean connected = false; + private int rxBufSize = Constants.DEFAULT_CLUSTER_ACK_BUFFER_SIZE; + private int txBufSize = Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE; + private int udpRxBufSize = Constants.DEFAULT_CLUSTER_ACK_BUFFER_SIZE; + private int udpTxBufSize = Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE; + private boolean directBuffer = false; + private int keepAliveCount = -1; + private int requestCount = 0; + private long connectTime; + private long keepAliveTime = -1; + private long timeout = 3000; + private Member destination; + private InetAddress address; + private int port; + private int maxRetryAttempts = 1;//1 resends + private int attempt; + private boolean tcpNoDelay = true; + private boolean soKeepAlive = false; + private boolean ooBInline = true; + private boolean soReuseAddress = true; + private boolean soLingerOn = false; + private int soLingerTime = 3; + private int soTrafficClass = 0x04 | 0x08 | 0x010; + private boolean throwOnFailedAck = true; + private boolean udpBased = false; + private int udpPort = -1; + + /** + * transfers sender properties from one sender to another + * @param from AbstractSender + * @param to AbstractSender + */ + public static void transferProperties(AbstractSender from, AbstractSender to) { + to.rxBufSize = from.rxBufSize; + to.txBufSize = from.txBufSize; + to.directBuffer = from.directBuffer; + to.keepAliveCount = from.keepAliveCount; + to.keepAliveTime = from.keepAliveTime; + to.timeout = from.timeout; + to.destination = from.destination; + to.address = from.address; + to.port = from.port; + to.maxRetryAttempts = from.maxRetryAttempts; + to.tcpNoDelay = from.tcpNoDelay; + to.soKeepAlive = from.soKeepAlive; + to.ooBInline = from.ooBInline; + to.soReuseAddress = from.soReuseAddress; + to.soLingerOn = from.soLingerOn; + to.soLingerTime = from.soLingerTime; + to.soTrafficClass = from.soTrafficClass; + to.throwOnFailedAck = from.throwOnFailedAck; + to.udpBased = from.udpBased; + to.udpPort = from.udpPort; + } + + + public AbstractSender() { + + } + + /** + * connect + * + * @throws IOException + * TODO Implement this org.apache.catalina.tribes.transport.DataSender method + */ + @Override + public abstract void connect() throws IOException; + + /** + * disconnect + * + * TODO Implement this org.apache.catalina.tribes.transport.DataSender method + */ + @Override + public abstract void disconnect(); + + /** + * keepalive + * + * @return boolean + * TODO Implement this org.apache.catalina.tribes.transport.DataSender method + */ + @Override + public boolean keepalive() { + boolean disconnect = false; + if (isUdpBased()) { + disconnect = true; //always disconnect UDP, TODO optimize the keepalive handling + } else if ( keepAliveCount >= 0 && requestCount>keepAliveCount ) { + disconnect = true; + } else if ( keepAliveTime >= 0 && (System.currentTimeMillis()-connectTime)>keepAliveTime ) { + disconnect = true; + } + if ( disconnect ) { + disconnect(); + } + return disconnect; + } + + protected void setConnected(boolean connected){ + this.connected = connected; + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public long getConnectTime() { + return connectTime; + } + + public Member getDestination() { + return destination; + } + + + public int getKeepAliveCount() { + return keepAliveCount; + } + + public long getKeepAliveTime() { + return keepAliveTime; + } + + @Override + public int getRequestCount() { + return requestCount; + } + + public int getRxBufSize() { + return rxBufSize; + } + + public long getTimeout() { + return timeout; + } + + public int getTxBufSize() { + return txBufSize; + } + + public InetAddress getAddress() { + return address; + } + + public int getPort() { + return port; + } + + public int getMaxRetryAttempts() { + return maxRetryAttempts; + } + + public void setDirectBuffer(boolean directBuffer) { + this.directBuffer = directBuffer; + } + + public boolean getDirectBuffer() { + return this.directBuffer; + } + + public int getAttempt() { + return attempt; + } + + public boolean getTcpNoDelay() { + return tcpNoDelay; + } + + public boolean getSoKeepAlive() { + return soKeepAlive; + } + + public boolean getOoBInline() { + return ooBInline; + } + + public boolean getSoReuseAddress() { + return soReuseAddress; + } + + public boolean getSoLingerOn() { + return soLingerOn; + } + + public int getSoLingerTime() { + return soLingerTime; + } + + public int getSoTrafficClass() { + return soTrafficClass; + } + + public boolean getThrowOnFailedAck() { + return throwOnFailedAck; + } + + @Override + public void setKeepAliveCount(int keepAliveCount) { + this.keepAliveCount = keepAliveCount; + } + + @Override + public void setKeepAliveTime(long keepAliveTime) { + this.keepAliveTime = keepAliveTime; + } + + public void setRequestCount(int requestCount) { + this.requestCount = requestCount; + } + + @Override + public void setRxBufSize(int rxBufSize) { + this.rxBufSize = rxBufSize; + } + + @Override + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + @Override + public void setTxBufSize(int txBufSize) { + this.txBufSize = txBufSize; + } + + public void setConnectTime(long connectTime) { + this.connectTime = connectTime; + } + + public void setMaxRetryAttempts(int maxRetryAttempts) { + this.maxRetryAttempts = maxRetryAttempts; + } + + public void setAttempt(int attempt) { + this.attempt = attempt; + } + + public void setTcpNoDelay(boolean tcpNoDelay) { + this.tcpNoDelay = tcpNoDelay; + } + + public void setSoKeepAlive(boolean soKeepAlive) { + this.soKeepAlive = soKeepAlive; + } + + public void setOoBInline(boolean ooBInline) { + this.ooBInline = ooBInline; + } + + public void setSoReuseAddress(boolean soReuseAddress) { + this.soReuseAddress = soReuseAddress; + } + + public void setSoLingerOn(boolean soLingerOn) { + this.soLingerOn = soLingerOn; + } + + public void setSoLingerTime(int soLingerTime) { + this.soLingerTime = soLingerTime; + } + + public void setSoTrafficClass(int soTrafficClass) { + this.soTrafficClass = soTrafficClass; + } + + public void setThrowOnFailedAck(boolean throwOnFailedAck) { + this.throwOnFailedAck = throwOnFailedAck; + } + + public void setDestination(Member destination) throws UnknownHostException { + this.destination = destination; + this.address = InetAddress.getByAddress(destination.getHost()); + this.port = destination.getPort(); + this.udpPort = destination.getUdpPort(); + + } + + public void setPort(int port) { + this.port = port; + } + + public void setAddress(InetAddress address) { + this.address = address; + } + + + public boolean isUdpBased() { + return udpBased; + } + + + public void setUdpBased(boolean udpBased) { + this.udpBased = udpBased; + } + + + public int getUdpPort() { + return udpPort; + } + + + public void setUdpPort(int udpPort) { + this.udpPort = udpPort; + } + + + public int getUdpRxBufSize() { + return udpRxBufSize; + } + + + public void setUdpRxBufSize(int udpRxBufSize) { + this.udpRxBufSize = udpRxBufSize; + } + + + public int getUdpTxBufSize() { + return udpTxBufSize; + } + + + public void setUdpTxBufSize(int udpTxBufSize) { + this.udpTxBufSize = udpTxBufSize; + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/Constants.java b/java/org/apache/catalina/tribes/transport/Constants.java new file mode 100644 index 0000000..07946d8 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/Constants.java @@ -0,0 +1,41 @@ +/* + * 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.catalina.tribes.transport; + +import org.apache.catalina.tribes.io.XByteBuffer; + +/** + * Manifest constants for the org.apache.catalina.tribes.transport + * package. + * @author Peter Rossbach + */ +public class Constants { + + public static final String Package = "org.apache.catalina.tribes.transport"; + + public static final int DEFAULT_CLUSTER_MSG_BUFFER_SIZE = 65536; + public static final int DEFAULT_CLUSTER_ACK_BUFFER_SIZE = 25188; + + /* + * Do not change any of these values! + */ + public static final byte[] ACK_DATA = new byte[] {6, 2, 3}; + public static final byte[] FAIL_ACK_DATA = new byte[] {11, 0, 5}; + public static final byte[] ACK_COMMAND = XByteBuffer.createDataPackage(ACK_DATA); + public static final byte[] FAIL_ACK_COMMAND = XByteBuffer.createDataPackage(FAIL_ACK_DATA); + +} diff --git a/java/org/apache/catalina/tribes/transport/DataSender.java b/java/org/apache/catalina/tribes/transport/DataSender.java new file mode 100644 index 0000000..b804989 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/DataSender.java @@ -0,0 +1,33 @@ +/* + * 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.catalina.tribes.transport; + +import java.io.IOException; + +public interface DataSender { + void connect() throws IOException; + void disconnect(); + boolean isConnected(); + void setRxBufSize(int size); + void setTxBufSize(int size); + boolean keepalive(); + void setTimeout(long timeout); + void setKeepAliveCount(int maxRequests); + void setKeepAliveTime(long keepAliveTimeInMs); + int getRequestCount(); + long getConnectTime(); +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings.properties b/java/org/apache/catalina/tribes/transport/LocalStrings.properties new file mode 100644 index 0000000..e2d6497 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings.properties @@ -0,0 +1,25 @@ +# 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. + +PooledSender.senderDisconnectFail=Failed to disconnect sender + +pooledSender.closed.queue=Queue is closed + +receiverBase.bind.failed=Failed bind replication listener on address:[{0}] +receiverBase.socket.bind=Receiver Server Socket bound to:[{0}] +receiverBase.start=Starting replication listener on address [{0}] +receiverBase.udp.bind=UDP Receiver Server Socket bound to:[{0}] +receiverBase.unable.bind=Unable to bind server socket to:[{0}] throwing error. +receiverBase.unable.bind.udp=Unable to bind UDP socket to:[{0}] throwing error. diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/transport/LocalStrings_cs.properties new file mode 100644 index 0000000..9f70547 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings_cs.properties @@ -0,0 +1,17 @@ +# 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. + +receiverBase.bind.failed=Selhalo pÅ™iÅ™azení replikaÄního listeneru na adresu:[{0}] +receiverBase.unable.bind=Nelze nastavit socket serveru na:[{0}] vyhazuje chybu. diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings_de.properties b/java/org/apache/catalina/tribes/transport/LocalStrings_de.properties new file mode 100644 index 0000000..acdfa6d --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +receiverBase.unable.bind=Kann Server Socket nicht an [{0}] binden. Werfe einen Fehler. diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings_es.properties b/java/org/apache/catalina/tribes/transport/LocalStrings_es.properties new file mode 100644 index 0000000..263a891 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings_es.properties @@ -0,0 +1,20 @@ +# 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. + +PooledSender.senderDisconnectFail=No pude desconectar al remitente + +receiverBase.bind.failed=Fallo al atachar el escuchador de replicación en la dirección: [{0}]\n +receiverBase.unable.bind=Imposible vincular el socket del servidor a:[{0}] lanzando error. +receiverBase.unable.bind.udp=Imposible atar el socket UDP a:[{0}] lanzando error.\n diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/transport/LocalStrings_fr.properties new file mode 100644 index 0000000..bb333ad --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings_fr.properties @@ -0,0 +1,25 @@ +# 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. + +PooledSender.senderDisconnectFail=Impossible de se déconnecter de l'envoyeur + +pooledSender.closed.queue=La queue est fermée + +receiverBase.bind.failed=Échec d''attachement en écoute de la réplication à l''adresse [{0}] +receiverBase.socket.bind=Socket de réception du serveur attaché à : [{0}] +receiverBase.start=Démarrage de l''écouteur de réplication sur l''adresse [{0}] +receiverBase.udp.bind=Le socket serveur receveur est associé avec [{0}] +receiverBase.unable.bind=Impossible de lier la socket serveur à : [{0}], cela renvoie une erreur. +receiverBase.unable.bind.udp=Impossible d''associer le socket UDP à [{0}], propagation de l''erreur diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/transport/LocalStrings_ja.properties new file mode 100644 index 0000000..cdf18c4 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings_ja.properties @@ -0,0 +1,25 @@ +# 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. + +PooledSender.senderDisconnectFail=Senderã®åˆ‡æ–­ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ + +pooledSender.closed.queue=キューã¯é–‰ã˜ã‚‰ã‚Œã¦ã„ã¾ã™ã€‚ + +receiverBase.bind.failed=アドレス [{0}] ã«ãƒ¬ãƒ—リケーションリスナーをæŸç¸›ã§ãã¾ã›ã‚“。 +receiverBase.socket.bind=Receiver ServerソケットãŒãƒã‚¤ãƒ³ãƒ‰ã•ã‚Œã¾ã—ãŸï¼š[{0}] +receiverBase.start=アドレス [{0}] ã§ãƒ¬ãƒ—リケーション リスナーを開始ã—ã¾ã™ +receiverBase.udp.bind=UDP å—信用ã®ã‚µãƒ¼ãƒãƒ¼ã‚½ã‚±ãƒƒãƒˆã‚’ [{0}] ã«ãƒã‚¤ãƒ³ãƒ‰ã—ã¾ã—ãŸã€‚ +receiverBase.unable.bind=[{0}] ã¸ã‚µãƒ¼ãƒãƒ¼ã‚½ã‚±ãƒƒãƒˆã‚’ãƒã‚¤ãƒ³ãƒ‰ã§ããªã‹ã£ãŸãŸã‚エラーをé€å‡ºã—ã¾ã—ãŸã€‚ +receiverBase.unable.bind.udp=UDP ソケットを [{0}] ã¸ãƒã‚¤ãƒ³ãƒ‰ã§ãã¾ã›ã‚“。エラーをé€å‡ºã—ã¾ã™ã€‚ diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/transport/LocalStrings_ko.properties new file mode 100644 index 0000000..122a7ea --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings_ko.properties @@ -0,0 +1,24 @@ +# 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. + +PooledSender.senderDisconnectFail=Senderì˜ ì—°ê²°ì„ ëŠì§€ 못했습니다. + +pooledSender.closed.queue=íê°€ 닫힌 ìƒíƒœìž…니다. + +receiverBase.bind.failed=복제 리스너를 어드레스 [{0}]ì— ë°”ì¸ë”©í•˜ì§€ 못했습니다. +receiverBase.socket.bind=Receiver 서버 ì†Œì¼“ì´ [{0}]ì— ë°”ì¸ë”© ë˜ì—ˆìŠµë‹ˆë‹¤. +receiverBase.udp.bind=UDP 수신 서버 ì†Œì¼“ì´ [{0}]ì— ë°”ì¸ë”© ë¨. +receiverBase.unable.bind=서버 ì†Œì¼“ì„ ë°”ì¸ë“œ í•  수 없습니다: [{0}]ì—ì„œ 오류 ë°œìƒ. +receiverBase.unable.bind.udp=오류 ë°œìƒìœ¼ë¡œ ì¸í•´ UDP ì†Œì¼“ì„ [{0}]ì— ë°”ì¸ë”©í•  수 없습니다. diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings_pt_BR.properties b/java/org/apache/catalina/tribes/transport/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..fdcc860 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +receiverBase.unable.bind=Impossível mapear socket do tipo servidor para:[{0}] lançando exceção diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/transport/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..bee9720 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings_zh_CN.properties @@ -0,0 +1,24 @@ +# 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. + +PooledSender.senderDisconnectFail=与 sender 断开连接失败 + +pooledSender.closed.queue=队列已关闭 + +receiverBase.bind.failed=对内容类型[{2}]返回[{3}]使用[{0}]匹é…[{1}] +receiverBase.socket.bind=æœåŠ¡å™¨å¥—接字接收器绑定到:[{0}] +receiverBase.udp.bind=绑定到的UDP接收器æœåŠ¡å™¨å¥—接字:[{0}] +receiverBase.unable.bind=无法绑定套接字端å£:[{0}],出现异常 +receiverBase.unable.bind.udp=无法将UDP套接字绑定到:[{0}]抛出错误。 diff --git a/java/org/apache/catalina/tribes/transport/MultiPointSender.java b/java/org/apache/catalina/tribes/transport/MultiPointSender.java new file mode 100644 index 0000000..bdf1347 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/MultiPointSender.java @@ -0,0 +1,32 @@ +/* + * 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.catalina.tribes.transport; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; + +/** + * @since 5.5.16 + */ +public interface MultiPointSender extends DataSender +{ + void sendMessage(Member[] destination, ChannelMessage data) throws ChannelException; + void setMaxRetryAttempts(int attempts); + void setDirectBuffer(boolean directBuf); + void add(Member member); + void remove(Member member); +} diff --git a/java/org/apache/catalina/tribes/transport/PooledSender.java b/java/org/apache/catalina/tribes/transport/PooledSender.java new file mode 100644 index 0000000..457230c --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/PooledSender.java @@ -0,0 +1,234 @@ +/* + * 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.catalina.tribes.transport; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public abstract class PooledSender extends AbstractSender implements MultiPointSender { + + private static final Log log = LogFactory.getLog(PooledSender.class); + protected static final StringManager sm = + StringManager.getManager(Constants.Package); + + private final SenderQueue queue; + private int poolSize = 25; + private long maxWait = 3000; + public PooledSender() { + queue = new SenderQueue(this,poolSize); + } + + public abstract DataSender getNewDataSender(); + + public DataSender getSender() { + return queue.getSender(getMaxWait()); + } + + public void returnSender(DataSender sender) { + sender.keepalive(); + queue.returnSender(sender); + } + + @Override + public synchronized void connect() throws IOException { + //do nothing, happens in the socket sender itself + queue.open(); + setConnected(true); + } + + @Override + public synchronized void disconnect() { + queue.close(); + setConnected(false); + } + + + public int getInPoolSize() { + return queue.getInPoolSize(); + } + + public int getInUsePoolSize() { + return queue.getInUsePoolSize(); + } + + + public void setPoolSize(int poolSize) { + this.poolSize = poolSize; + queue.setLimit(poolSize); + } + + public int getPoolSize() { + return poolSize; + } + + public long getMaxWait() { + return maxWait; + } + + public void setMaxWait(long maxWait) { + this.maxWait = maxWait; + } + + @Override + public boolean keepalive() { + //do nothing, the pool checks on every return + return (queue==null)?false:queue.checkIdleKeepAlive(); + } + + @Override + public void add(Member member) { + // no op, senders created upon demands + } + + @Override + public void remove(Member member) { + //no op for now, should not cancel out any keys + //can create serious sync issues + //all TCP connections are cleared out through keepalive + //and if remote node disappears + } + // ----------------------------------------------------- Inner Class + + private static class SenderQueue { + private int limit = 25; + + PooledSender parent = null; + + private List notinuse = null; + + private List inuse = null; + + private boolean isOpen = true; + + SenderQueue(PooledSender parent, int limit) { + this.limit = limit; + this.parent = parent; + notinuse = new ArrayList<>(); + inuse = new ArrayList<>(); + } + + /** + * @return Returns the limit. + */ + public int getLimit() { + return limit; + } + /** + * @param limit The limit to set. + */ + public void setLimit(int limit) { + this.limit = limit; + } + + public synchronized int getInUsePoolSize() { + return inuse.size(); + } + + public synchronized int getInPoolSize() { + return notinuse.size(); + } + + public synchronized boolean checkIdleKeepAlive() { + DataSender[] list = notinuse.toArray(new DataSender[0]); + boolean result = false; + for (DataSender dataSender : list) { + result = result | dataSender.keepalive(); + } + return result; + } + + public synchronized DataSender getSender(long timeout) { + long start = System.currentTimeMillis(); + while ( true ) { + if (!isOpen) { + throw new IllegalStateException(sm.getString("pooledSender.closed.queue")); + } + DataSender sender = null; + if (notinuse.size() == 0 && inuse.size() < limit) { + sender = parent.getNewDataSender(); + } else if (notinuse.size() > 0) { + sender = notinuse.remove(0); + } + if (sender != null) { + inuse.add(sender); + return sender; + }//end if + long delta = System.currentTimeMillis() - start; + if ( delta > timeout && timeout>0) { + return null; + } else { + try { + wait(Math.max(timeout - delta,1)); + }catch (InterruptedException x){} + }//end if + } + } + + public synchronized void returnSender(DataSender sender) { + if ( !isOpen) { + sender.disconnect(); + return; + } + //to do + inuse.remove(sender); + //just in case the limit has changed + if ( notinuse.size() < this.getLimit() ) { + notinuse.add(sender); + } else { + try { + sender.disconnect(); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString( + "PooledSender.senderDisconnectFail"), e); + } + } + } + notifyAll(); + } + + public synchronized void close() { + isOpen = false; + Object[] unused = notinuse.toArray(); + Object[] used = inuse.toArray(); + for (Object value : unused) { + DataSender sender = (DataSender) value; + sender.disconnect(); + }//for + for (Object o : used) { + DataSender sender = (DataSender) o; + sender.disconnect(); + }//for + notinuse.clear(); + inuse.clear(); + notifyAll(); + + + } + + public synchronized void open() { + isOpen = true; + notifyAll(); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/ReceiverBase.java b/java/org/apache/catalina/tribes/transport/ReceiverBase.java new file mode 100644 index 0000000..507865c --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/ReceiverBase.java @@ -0,0 +1,621 @@ +/* + * 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.catalina.tribes.transport; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.management.ObjectName; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ChannelReceiver; +import org.apache.catalina.tribes.MessageListener; +import org.apache.catalina.tribes.io.ListenCallback; +import org.apache.catalina.tribes.jmx.JmxRegistry; +import org.apache.catalina.tribes.util.ExecutorFactory; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public abstract class ReceiverBase implements ChannelReceiver, ListenCallback, RxTaskPool.TaskCreator { + + public static final int OPTION_DIRECT_BUFFER = 0x0004; + + private static final Log log = LogFactory.getLog(ReceiverBase.class); + + private static final Object bindLock = new Object(); + + protected static final StringManager sm = StringManager.getManager(Constants.Package); + + private MessageListener listener; + private String host = "auto"; + private InetAddress bind; + private int port = 4000; + private int udpPort = -1; + private int securePort = -1; + private int rxBufSize = Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE; + private int txBufSize = Constants.DEFAULT_CLUSTER_ACK_BUFFER_SIZE; + private int udpRxBufSize = Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE; + private int udpTxBufSize = Constants.DEFAULT_CLUSTER_ACK_BUFFER_SIZE; + + private volatile boolean listen = false; + private RxTaskPool pool; + private boolean direct = true; + private long tcpSelectorTimeout = 5000; + //how many times to search for an available socket + private int autoBind = 100; + private int maxThreads = 15; + private int minThreads = 6; + private int maxTasks = 100; + private int minTasks = 10; + private boolean tcpNoDelay = true; + private boolean soKeepAlive = false; + private boolean ooBInline = true; + private boolean soReuseAddress = true; + private boolean soLingerOn = true; + private int soLingerTime = 3; + private int soTrafficClass = 0x04 | 0x08 | 0x010; + private int timeout = 3000; //3 seconds + private boolean useBufferPool = true; + private boolean daemon = true; + private long maxIdleTime = 60000; + + private ExecutorService executor; + private Channel channel; + + /** + * the ObjectName of this Receiver. + */ + private ObjectName oname = null; + + public ReceiverBase() { + } + + @Override + public void start() throws IOException { + if ( executor == null ) { + //executor = new ThreadPoolExecutor(minThreads,maxThreads,60,TimeUnit.SECONDS,new LinkedBlockingQueue()); + String channelName = ""; + if (channel.getName() != null) { + channelName = "[" + channel.getName() + "]"; + } + TaskThreadFactory tf = new TaskThreadFactory("Tribes-Task-Receiver" + channelName + "-"); + executor = ExecutorFactory.newThreadPool(minThreads, maxThreads, maxIdleTime, TimeUnit.MILLISECONDS, tf); + } + // register jmx + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + this.oname = jmxRegistry.registerJmx(",component=Receiver", this); + } + } + + @Override + public void stop() { + if ( executor != null ) + { + executor.shutdownNow();//ignore left overs + } + executor = null; + if (oname != null) { + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + jmxRegistry.unregisterJmx(oname); + } + oname = null; + } + channel = null; + } + + /** + * getMessageListener + * + * @return MessageListener + */ + @Override + public MessageListener getMessageListener() { + return listener; + } + + /** + * @return The port + */ + @Override + public int getPort() { + return port; + } + + public int getRxBufSize() { + return rxBufSize; + } + + public int getTxBufSize() { + return txBufSize; + } + + /** + * setMessageListener + * + * @param listener MessageListener + */ + @Override + public void setMessageListener(MessageListener listener) { + this.listener = listener; + } + + public void setRxBufSize(int rxBufSize) { + this.rxBufSize = rxBufSize; + } + + public void setTxBufSize(int txBufSize) { + this.txBufSize = txBufSize; + } + + /** + * @return Returns the bind. + */ + public InetAddress getBind() { + if (bind == null) { + try { + if ("auto".equals(host)) { + host = InetAddress.getLocalHost().getHostAddress(); + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("receiverBase.start", host)); + } + bind = InetAddress.getByName(host); + } catch (IOException ioe) { + log.error(sm.getString("receiverBase.bind.failed", host), ioe); + } + } + return bind; + } + + /** + * Attempts to bind using the provided port and if that fails attempts to + * bind to each of the ports from portstart to (portstart + retries -1) + * until either there are no more ports or the bind is successful. The + * address to bind to is obtained via a call to {link {@link #getBind()}. + * @param socket The socket to bind + * @param portstart Starting port for bind attempts + * @param retries Number of times to attempt to bind (port incremented + * between attempts) + * @throws IOException Socket bind error + */ + protected void bind(ServerSocket socket, int portstart, int retries) throws IOException { + synchronized (bindLock) { + InetSocketAddress addr = null; + int port = portstart; + while (retries > 0) { + try { + addr = new InetSocketAddress(getBind(), port); + socket.bind(addr); + setPort(port); + log.info(sm.getString("receiverBase.socket.bind", addr)); + retries = 0; + } catch ( IOException x) { + retries--; + if ( retries <= 0 ) { + log.info(sm.getString("receiverBase.unable.bind", addr)); + throw x; + } + port++; + } + } + } + } + + /** + * Same as bind() except it does it for the UDP port + * @param socket The socket to bind + * @param portstart Starting port for bind attempts + * @param retries Number of times to attempt to bind (port incremented + * between attempts) + * @return int The retry count + * @throws IOException Socket bind error + */ + protected int bindUdp(DatagramSocket socket, int portstart, int retries) throws IOException { + InetSocketAddress addr = null; + while ( retries > 0 ) { + try { + addr = new InetSocketAddress(getBind(), portstart); + socket.bind(addr); + setUdpPort(portstart); + log.info(sm.getString("receiverBase.udp.bind", addr)); + return 0; + }catch ( IOException x) { + retries--; + if ( retries <= 0 ) { + log.info(sm.getString("receiverBase.unable.bind.udp", addr)); + throw x; + } + portstart++; + try { + Thread.sleep(25); + } catch (InterruptedException ti) { + Thread.currentThread().interrupt(); + } + retries = bindUdp(socket,portstart,retries); + } + } + return retries; + } + + + @Override + public void messageDataReceived(ChannelMessage data) { + if ( this.listener != null ) { + if ( listener.accept(data) ) { + listener.messageReceived(data); + } + } + } + + public int getWorkerThreadOptions() { + int options = 0; + if ( getDirect() ) { + options = options | OPTION_DIRECT_BUFFER; + } + return options; + } + + + /** + * @param bind The bind to set. + */ + public void setBind(InetAddress bind) { + this.bind = bind; + } + + public boolean getDirect() { + return direct; + } + + + + public void setDirect(boolean direct) { + this.direct = direct; + } + + + public String getAddress() { + getBind(); + return this.host; + } + + @Override + public String getHost() { + return getAddress(); + } + + public long getSelectorTimeout() { + return tcpSelectorTimeout; + } + + public boolean doListen() { + return listen; + } + + public MessageListener getListener() { + return listener; + } + + public RxTaskPool getTaskPool() { + return pool; + } + + public int getAutoBind() { + return autoBind; + } + + public int getMaxThreads() { + return maxThreads; + } + + public int getMinThreads() { + return minThreads; + } + + public boolean getTcpNoDelay() { + return tcpNoDelay; + } + + public boolean getSoKeepAlive() { + return soKeepAlive; + } + + public boolean getOoBInline() { + return ooBInline; + } + + + public boolean getSoLingerOn() { + return soLingerOn; + } + + public int getSoLingerTime() { + return soLingerTime; + } + + public boolean getSoReuseAddress() { + return soReuseAddress; + } + + public int getSoTrafficClass() { + return soTrafficClass; + } + + public int getTimeout() { + return timeout; + } + + public boolean getUseBufferPool() { + return useBufferPool; + } + + @Override + public int getSecurePort() { + return securePort; + } + + public int getMinTasks() { + return minTasks; + } + + public int getMaxTasks() { + return maxTasks; + } + + public ExecutorService getExecutor() { + return executor; + } + + public boolean isListening() { + return listen; + } + + public void setSelectorTimeout(long selTimeout) { + tcpSelectorTimeout = selTimeout; + } + + public void setListen(boolean doListen) { + this.listen = doListen; + } + + + public void setAddress(String host) { + this.host = host; + } + public void setHost(String host) { + setAddress(host); + } + + public void setListener(MessageListener listener) { + this.listener = listener; + } + + public void setPool(RxTaskPool pool) { + this.pool = pool; + } + + public void setPort(int port) { + this.port = port; + } + + public void setAutoBind(int autoBind) { + this.autoBind = autoBind; + if ( this.autoBind <= 0 ) { + this.autoBind = 1; + } + } + + public void setMaxThreads(int maxThreads) { + this.maxThreads = maxThreads; + } + + public void setMinThreads(int minThreads) { + this.minThreads = minThreads; + } + + public void setTcpNoDelay(boolean tcpNoDelay) { + this.tcpNoDelay = tcpNoDelay; + } + + public void setSoKeepAlive(boolean soKeepAlive) { + this.soKeepAlive = soKeepAlive; + } + + public void setOoBInline(boolean ooBInline) { + this.ooBInline = ooBInline; + } + + + public void setSoLingerOn(boolean soLingerOn) { + this.soLingerOn = soLingerOn; + } + + public void setSoLingerTime(int soLingerTime) { + this.soLingerTime = soLingerTime; + } + + public void setSoReuseAddress(boolean soReuseAddress) { + this.soReuseAddress = soReuseAddress; + } + + public void setSoTrafficClass(int soTrafficClass) { + this.soTrafficClass = soTrafficClass; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public void setUseBufferPool(boolean useBufferPool) { + this.useBufferPool = useBufferPool; + } + + public void setSecurePort(int securePort) { + this.securePort = securePort; + } + + public void setMinTasks(int minTasks) { + this.minTasks = minTasks; + } + + public void setMaxTasks(int maxTasks) { + this.maxTasks = maxTasks; + } + + public void setExecutor(ExecutorService executor) { + this.executor = executor; + } + + @Override + public void heartbeat() { + //empty operation + } + + @Override + public int getUdpPort() { + return udpPort; + } + + public void setUdpPort(int udpPort) { + this.udpPort = udpPort; + } + + public int getUdpRxBufSize() { + return udpRxBufSize; + } + + public void setUdpRxBufSize(int udpRxBufSize) { + this.udpRxBufSize = udpRxBufSize; + } + + public int getUdpTxBufSize() { + return udpTxBufSize; + } + + public void setUdpTxBufSize(int udpTxBufSize) { + this.udpTxBufSize = udpTxBufSize; + } + + @Override + public Channel getChannel() { + return channel; + } + + @Override + public void setChannel(Channel channel) { + this.channel = channel; + } + + // ---------------------------------------------- stats of the thread pool + /** + * Return the current number of threads that are managed by the pool. + * @return the current number of threads that are managed by the pool + */ + public int getPoolSize() { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getPoolSize(); + } else { + return -1; + } + } + + /** + * Return the current number of threads that are in use. + * @return the current number of threads that are in use + */ + public int getActiveCount() { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getActiveCount(); + } else { + return -1; + } + } + + /** + * Return the total number of tasks that have ever been scheduled for execution by the pool. + * @return the total number of tasks that have ever been scheduled for execution by the pool + */ + public long getTaskCount() { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getTaskCount(); + } else { + return -1; + } + } + + /** + * Return the total number of tasks that have completed execution by the pool. + * @return the total number of tasks that have completed execution by the pool + */ + public long getCompletedTaskCount() { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getCompletedTaskCount(); + } else { + return -1; + } + } + + // ---------------------------------------------- ThreadFactory Inner Class + class TaskThreadFactory implements ThreadFactory { + final ThreadGroup group; + final AtomicInteger threadNumber = new AtomicInteger(1); + final String namePrefix; + + TaskThreadFactory(String namePrefix) { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + this.namePrefix = namePrefix; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement()); + t.setDaemon(daemon); + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + } + + public boolean isDaemon() { + return daemon; + } + + public long getMaxIdleTime() { + return maxIdleTime; + } + + public void setDaemon(boolean daemon) { + this.daemon = daemon; + } + + public void setMaxIdleTime(long maxIdleTime) { + this.maxIdleTime = maxIdleTime; + } + +} diff --git a/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java b/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java new file mode 100644 index 0000000..7de04d9 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java @@ -0,0 +1,144 @@ +/* + * 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.catalina.tribes.transport; + +import java.io.IOException; + +import javax.management.ObjectName; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ChannelSender; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.jmx.JmxRegistry; +import org.apache.catalina.tribes.transport.nio.PooledParallelSender; + +/** + * Transmit message to other cluster members + * Actual senders are created based on the replicationMode + * type + */ +public class ReplicationTransmitter implements ChannelSender { + + private Channel channel; + + /** + * the ObjectName of this Sender. + */ + private ObjectName oname = null; + + public ReplicationTransmitter() { + } + + private MultiPointSender transport = new PooledParallelSender(); + + public MultiPointSender getTransport() { + return transport; + } + + public void setTransport(MultiPointSender transport) { + this.transport = transport; + } + + // ------------------------------------------------------------- public + + /** + * Send data to one member + * @see org.apache.catalina.tribes.ChannelSender#sendMessage(org.apache.catalina.tribes.ChannelMessage, org.apache.catalina.tribes.Member[]) + */ + @Override + public void sendMessage(ChannelMessage message, Member[] destination) throws ChannelException { + MultiPointSender sender = getTransport(); + sender.sendMessage(destination,message); + } + + + /** + * start the sender and register transmitter mbean + * + * @see org.apache.catalina.tribes.ChannelSender#start() + */ + @Override + public synchronized void start() throws IOException { + getTransport().connect(); + // register jmx + JmxRegistry jmxRegistry = JmxRegistry.getRegistry(channel); + if (jmxRegistry != null) { + this.oname = jmxRegistry.registerJmx(",component=Sender", transport); + } + } + + /** + * stop the sender and deregister mbeans (transmitter, senders) + * + * @see org.apache.catalina.tribes.ChannelSender#stop() + */ + @Override + public synchronized void stop() { + getTransport().disconnect(); + if (oname != null) { + JmxRegistry.getRegistry(channel).unregisterJmx(oname); + oname = null; + } + channel = null; + } + + /** + * Call transmitter to check for sender socket status + * + * @see org.apache.catalina.ha.tcp.SimpleTcpCluster#backgroundProcess() + */ + @Override + public void heartbeat() { + if (getTransport()!=null) { + getTransport().keepalive(); + } + } + + /** + * add new cluster member and create sender ( s. replicationMode) transfer + * current properties to sender + * + * @see org.apache.catalina.tribes.ChannelSender#add(org.apache.catalina.tribes.Member) + */ + @Override + public synchronized void add(Member member) { + getTransport().add(member); + } + + /** + * remove sender from transmitter. ( deregister mbean and disconnect sender ) + * + * @see org.apache.catalina.tribes.ChannelSender#remove(org.apache.catalina.tribes.Member) + */ + @Override + public synchronized void remove(Member member) { + getTransport().remove(member); + } + + @Override + public Channel getChannel() { + return channel; + } + + @Override + public void setChannel(Channel channel) { + this.channel = channel; + } + +} diff --git a/java/org/apache/catalina/tribes/transport/RxTaskPool.java b/java/org/apache/catalina/tribes/transport/RxTaskPool.java new file mode 100644 index 0000000..d17dde5 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/RxTaskPool.java @@ -0,0 +1,155 @@ +/* + * 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.catalina.tribes.transport; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * A very simple thread pool class. The pool size is set at + * construction time and remains fixed. Threads are cycled + * through a FIFO idle queue. + */ +public class RxTaskPool { + + final List idle = new ArrayList<>(); + final List used = new ArrayList<>(); + + final Object mutex = new Object(); + boolean running = true; + + private int maxTasks; + private int minTasks; + + private final TaskCreator creator; + + + public RxTaskPool (int maxTasks, int minTasks, TaskCreator creator) throws Exception { + // fill up the pool with worker threads + this.maxTasks = maxTasks; + this.minTasks = minTasks; + this.creator = creator; + } + + protected void configureTask(AbstractRxTask task) { + synchronized (task) { + task.setTaskPool(this); +// task.setName(task.getClass().getName() + "[" + inc() + "]"); +// task.setDaemon(true); +// task.setPriority(Thread.MAX_PRIORITY); +// task.start(); + } + } + + /** + * Find an idle worker thread, if any. Could return null. + * @return a worker + */ + public AbstractRxTask getRxTask() + { + AbstractRxTask worker = null; + synchronized (mutex) { + while ( worker == null && running ) { + if (idle.size() > 0) { + try { + worker = idle.remove(0); + } catch (java.util.NoSuchElementException x) { + //this means that there are no available workers + worker = null; + } + } else if ( used.size() < this.maxTasks && creator != null) { + worker = creator.createRxTask(); + configureTask(worker); + } else { + try { + mutex.wait(); + } catch (InterruptedException x) { + Thread.currentThread().interrupt(); + } + } + }//while + if ( worker != null ) { + used.add(worker); + } + } + return worker; + } + + public int available() { + synchronized (mutex) { + return idle.size(); + } + } + + /** + * Called by the worker thread to return itself to the + * idle pool. + * @param worker The worker + */ + public void returnWorker (AbstractRxTask worker) { + if ( running ) { + synchronized (mutex) { + used.remove(worker); + //if ( idle.size() < minThreads && !idle.contains(worker)) idle.add(worker); + if ( idle.size() < maxTasks && !idle.contains(worker)) { + idle.add(worker); //let max be the upper limit + } else { + worker.close(); + } + mutex.notifyAll(); + } + } else { + worker.close(); + } + } + + public int getMaxThreads() { + return maxTasks; + } + + public int getMinThreads() { + return minTasks; + } + + public void stop() { + running = false; + synchronized (mutex) { + Iterator i = idle.iterator(); + while ( i.hasNext() ) { + AbstractRxTask worker = i.next(); + returnWorker(worker); + i.remove(); + } + } + } + + public void setMaxTasks(int maxThreads) { + this.maxTasks = maxThreads; + } + + public void setMinTasks(int minThreads) { + this.minTasks = minThreads; + } + + public TaskCreator getTaskCreator() { + return this.creator; + } + + public interface TaskCreator { + AbstractRxTask createRxTask(); + } +} diff --git a/java/org/apache/catalina/tribes/transport/SenderState.java b/java/org/apache/catalina/tribes/transport/SenderState.java new file mode 100644 index 0000000..fedac53 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/SenderState.java @@ -0,0 +1,95 @@ +/* + * 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.catalina.tribes.transport; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.catalina.tribes.Member; + +public class SenderState { + + public static final int READY = 0; + public static final int SUSPECT = 1; + public static final int FAILING = 2; + + protected static final ConcurrentMap memberStates = new ConcurrentHashMap<>(); + + public static SenderState getSenderState(Member member) { + return getSenderState(member, true); + } + + public static SenderState getSenderState(Member member, boolean create) { + SenderState state = memberStates.get(member); + if (state == null && create) { + state = new SenderState(); + SenderState current = memberStates.putIfAbsent(member, state); + if (current != null) { + state = current; + } + } + return state; + } + + public static void removeSenderState(Member member) { + memberStates.remove(member); + } + + + // ----------------------------------------------------- Instance Variables + + private volatile int state = READY; + + // ----------------------------------------------------- Constructor + + + private SenderState() { + this(READY); + } + + private SenderState(int state) { + this.state = state; + } + + public boolean isSuspect() { + return (state == SUSPECT) || (state == FAILING); + } + + public void setSuspect() { + state = SUSPECT; + } + + public boolean isReady() { + return state == READY; + } + + public void setReady() { + state = READY; + } + + public boolean isFailing() { + return state == FAILING; + } + + public void setFailing() { + state = FAILING; + } + + + // ----------------------------------------------------- Public Properties + +} diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings.properties new file mode 100644 index 0000000..441b425 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings.properties @@ -0,0 +1,62 @@ +# 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. + +nioReceiver.alreadyStarted=ServerSocketChannel already started +nioReceiver.cleanup.fail=Unable to cleanup on selector close +nioReceiver.clientDisconnect=Replication client disconnected, error when polling key. Ignoring client. +nioReceiver.closeError=Close error +nioReceiver.eventsError=Error processing events +nioReceiver.noThread=No TcpReplicationThread available +nioReceiver.requestError=Unable to process request in NioReceiver +nioReceiver.run.fail=Unable to run replication listener +nioReceiver.start.fail=Unable to start cluster receiver +nioReceiver.stop.fail=Unable to close cluster receiver selector +nioReceiver.stop.threadRunning=The NioReceiver thread did not stop in a timely manner. Errors may be observed when the selector is closed. +nioReceiver.threadpool.fail=ThreadPool cannot be initialized. Listener not started. +nioReceiver.threadsExhausted=Channel key is registered, but has had no interest ops for the last [{0}] ms. (cancelled: [{1}]):[{2}] last access:[{3}] Possible cause: all threads used, perform thread dump + +nioReplicationTask.disconnect=Channel closed on the remote end, disconnecting +nioReplicationTask.error.register.key=Error registering key for read:[{0}] +nioReplicationTask.exception.drainChannel=Exception caught in TcpReplicationThread.drainChannel. +nioReplicationTask.process.clusterMsg.failed=Processing of cluster message failed. +nioReplicationTask.unable.ack=Unable to send ACK back through channel, channel disconnected?: [{0}] +nioReplicationTask.unable.drainChannel.ioe=IOException in replication worker, unable to drain channel. Probable cause: Keep alive socket closed[{0}]. + +nioSender.already.connected=NioSender is already in connected state. +nioSender.datagram.already.established=Datagram channel has already been established. Connection might be in progress. +nioSender.key.inValid=Key is not valid, it must have been cancelled. +nioSender.not.connected=NioSender is not connected, this should not occur. +nioSender.receive.failedAck=Received a failed ack:org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA +nioSender.sender.disconnected=Sender has been disconnected, can't process selection key. +nioSender.socketChannel.already.established=Socket channel has already been established. Connection might be in progress. +nioSender.unable.disconnect=Unable to disconnect NioSender. msg=[{0}] +nioSender.unable.receive.ack=Unable to receive an ack message. EOF on socket channel has been reached. +nioSender.unknown.state=Data is in unknown state. readyOps=[{0}] + +parallelNioSender.error.keepalive=Error during keepalive test for sender:[{0}] +parallelNioSender.operation.timedout=Operation has timed out([{0}] ms.). +parallelNioSender.selectorCloseFail=Failed to close selector +parallelNioSender.send.fail=Member send is failing for:[{0}] ; Setting to suspect. +parallelNioSender.send.fail.retrying=Member send is failing for:[{0}] ; Setting to suspect and retrying. +parallelNioSender.send.failed=Parallel NIO send failed. +parallelNioSender.sendFailed.attempt=Send failed, attempt:[{0}] max:[{1}] +parallelNioSender.sender.disconnected.notRetry=Not retrying send for:[{0}]; Sender is disconnected. +parallelNioSender.sender.disconnected.sendFailed=Send failed, and sender is disconnected. Not retrying. +parallelNioSender.unable.setup.NioSender=Unable to setup NioSender. + +pooledParallelSender.sender.disconnected=Sender not connected. +pooledParallelSender.unable.open=Unable to open NIO selector. +pooledParallelSender.unable.retrieveSender=Unable to retrieve a sender from the sender pool +pooledParallelSender.unable.retrieveSender.timeout=Unable to retrieve a data sender, time out([{0}] ms) error. diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_cs.properties new file mode 100644 index 0000000..f50f0b4 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_cs.properties @@ -0,0 +1,26 @@ +# 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. + +nioReceiver.start.fail=Nelze nastartovat příjimaÄ clusteru +nioReceiver.stop.fail=Nelze uzavřít voliÄ příjímaÄů clusteru +nioReceiver.threadpool.fail=ThreadPool nelze inicializovat. Listener není nastartován. + +nioReplicationTask.error.register.key=Chyba registrace klíÄe o Ätení:[{0}] + +nioSender.unknown.state=Data jsou v neznámém stavu. readyOps=[{0}] + +parallelNioSender.send.fail.retrying=Zaslání Älenům selhalo pro:[{0}] ; Opakování nastaveno. + +pooledParallelSender.sender.disconnected=Odesílatel není pÅ™ipojen. diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings_de.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_de.properties new file mode 100644 index 0000000..faef498 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_de.properties @@ -0,0 +1,24 @@ +# 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. + +nioReceiver.start.fail=Cluster-Empfänger konnte nicht gestartet werden +nioReceiver.stop.fail=Kann den Cluster Receiver Selektor nicht schliessen +nioReceiver.threadpool.fail=ThreadPool kann nicht initialisiert werden. Der Listener wird nicht gestartet. + +nioReplicationTask.process.clusterMsg.failed=Verarbeitung der Cluster Nachricht fehlgeschlagen. + +nioSender.unknown.state=Daten sind in unbekanntem Zustand. readyOps=[{0}] + +pooledParallelSender.sender.disconnected=Sender nicht verbunden diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings_es.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_es.properties new file mode 100644 index 0000000..cfd7201 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_es.properties @@ -0,0 +1,34 @@ +# 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. + +nioReceiver.cleanup.fail=Imposible hacer limpieza en el cierre del selector +nioReceiver.start.fail=Incapaz de iniciar el recividor del cluster +nioReceiver.stop.fail=No fue posible cerrar el selector de recepción del cluster +nioReceiver.threadpool.fail=ThreadPool no pudo ser inicializado. Escuchador no iniciado.\n + +nioReplicationTask.error.register.key=Error al registrar la llave para lectura:[{0}] +nioReplicationTask.process.clusterMsg.failed=Fallo al procesar el mensaje del cluster +nioReplicationTask.unable.ack=No se pudo devolver el ACK a travez del canal. Esta el canal desconectado?: [{0}]\n + +nioSender.not.connected=El NioSender no esta conectado, esto no deberia ocurrir.\n +nioSender.unknown.state=Los Datos estan en un estado desconocido. readyOps=[{0}] + +parallelNioSender.send.fail.retrying=El envió de miembro esta fallando por:[{0}] ; Fijándolo como sospechoso y reintentando. +parallelNioSender.send.failed=Fallo al enviar NIO paralelo. +parallelNioSender.sendFailed.attempt=Fallo de envío, intento:[{0}] máximo:[{1}]\n +parallelNioSender.sender.disconnected.sendFailed=Envio fallido, y el remitente esta desconctado. No intentando nuevamente.\n + +pooledParallelSender.sender.disconnected=El remitente no esta conectado +pooledParallelSender.unable.retrieveSender=Incapaz de recuperar el enviador desde el pool de enviadores diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_fr.properties new file mode 100644 index 0000000..0f7bf4e --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_fr.properties @@ -0,0 +1,62 @@ +# 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. + +nioReceiver.alreadyStarted=ServerSocketChannel est déjà démarré +nioReceiver.cleanup.fail=Impossible de nettoyer lors de la fermeture du sélecteur +nioReceiver.clientDisconnect=Le client de réplication est déconnecté, erreur lors du "polling" de clé. Le client est ignoré. +nioReceiver.closeError=Erreur de fermeture +nioReceiver.eventsError=Erreur lors du traitement des évènements +nioReceiver.noThread=Aucun TcpReplicationThread n'est disponible +nioReceiver.requestError=Impossible de traiter la requête dans NioReceiver +nioReceiver.run.fail=Impossible d'exécuter l'écouteur de réplication +nioReceiver.start.fail=Incapable de démarrer le récepteur de cluster +nioReceiver.stop.fail=Incapable de femer le sélecteur de récepteur de cluster ("cluster receiver selector") +nioReceiver.stop.threadRunning=Le thread NioReceiver ne s'est pas arrêté suffisamment rapidement, des erreurs peuvent se produire lorsque le sélecteur sera fermé +nioReceiver.threadpool.fail=Le ThreadPool n'a pas pu être initialisé. Le Listener n'a pas démarré. +nioReceiver.threadsExhausted=La clé du canal est enregistrée mais n''a pas reçue d''opérations qui l''intéressaient depuis [{0}] ms (annulé : [{1}]) : [{2}] dernier accès : [{3}] cause possible : tous les threads sont utilisés, effectuez un dump des threads + +nioReplicationTask.disconnect=Le canal a été fermé sur l'hôte distant, déconnection +nioReplicationTask.error.register.key=Erreur lors de l''enregistrement de la clé en lecture : [{0}] +nioReplicationTask.exception.drainChannel=Erreur rencontrée dans TcpReplicationThread.drainChannel +nioReplicationTask.process.clusterMsg.failed=Le traitement du message du cluster a échoué +nioReplicationTask.unable.ack=Impossible d''envoyer un ACK réponse par le canal, le canal peut avoir été déconnecté : [{0}] +nioReplicationTask.unable.drainChannel.ioe=IOException pendant le traitement de la réplication, impossible de drainer le canal, cause probable : le socket gardé actif a été fermé [{0}] + +nioSender.already.connected=NioSender est déjà dans l'état connecté +nioSender.datagram.already.established=Le canal de datagramme a déjà été établi, une connection peut être déjà en cours +nioSender.key.inValid=La clé est invalide, elle doit avoir été annulée +nioSender.not.connected=NioSender n'est pas connecté, cela ne devrait jamais arriver +nioSender.receive.failedAck=Réception d'un échec de confirmation : org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA +nioSender.sender.disconnected=L'envoyeur s'est déconnecté, impossible d'envoyer la clé de sélection +nioSender.socketChannel.already.established=Le canal du socket a déjà été établi, la connection est peut-être déjà en cours +nioSender.unable.disconnect=Impossible de déconnecter le NioSender, msg=[{0}] +nioSender.unable.receive.ack=Impossible de recevoir un message de confirmation, le canal a rencontré l'EOF +nioSender.unknown.state=Les données sont dans un état inconnu. readyOps=[{0}] + +parallelNioSender.error.keepalive=Erreur lors du test de temps d''attente pour l''expéditeur [{0}] +parallelNioSender.operation.timedout=L''opération a dépassé le temps imparti ([{0}] ms) +parallelNioSender.selectorCloseFail=Echec de la fermeture du sélecteur +parallelNioSender.send.fail=L''envoi d''un membre a échoué pour [{0}], le membre est considéré suspect +parallelNioSender.send.fail.retrying=L''envoi au membre[{0}] ne fonctionne pas ; Marqué comme suspect et nouvel essai. +parallelNioSender.send.failed=L'envoi NIO en parallèle a échoué +parallelNioSender.sendFailed.attempt=Echec de l''envoi, tentative : [{0}] maximum : [{1}] +parallelNioSender.sender.disconnected.notRetry=Pas de réessai d''envoi de [{0}], l''expéditeur s''est déconnecté +parallelNioSender.sender.disconnected.sendFailed=L'envoi a échoué et l'envoyeur est déconnecté, pas de nouvel essai +parallelNioSender.unable.setup.NioSender=Impossible d'installer un NioSender + +pooledParallelSender.sender.disconnected=Emetteur non connecté +pooledParallelSender.unable.open=Impossible d'ouvrir le sélecteur NIO +pooledParallelSender.unable.retrieveSender=Impossible d'obtenir un envoyeur depuis le pool +pooledParallelSender.unable.retrieveSender.timeout=Impossible d''obtenir un expéditeur de données, temps d''attente dépassé ([{0}] ms) diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ja.properties new file mode 100644 index 0000000..e115397 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ja.properties @@ -0,0 +1,62 @@ +# 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. + +nioReceiver.alreadyStarted=ServerSocketChannel ã¯æ—¢ã«é–‹å§‹ã—ã¦ã„ã¾ã™ã€‚ +nioReceiver.cleanup.fail=セレクターã®åˆ‡æ–­ã¨å¾Œå§‹æœ«ãŒå¤±æ•—ã—ã¾ã—㟠+nioReceiver.clientDisconnect=レプリケーションクライアントãŒåˆ‡æ–­ã•ã‚Œã¾ã—ãŸã€‚ãƒãƒ¼ãƒªãƒ³ã‚°ã‚­ãƒ¼ã§ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ クライアントを無視ã—ã¾ã™ã€‚ +nioReceiver.closeError=Close エラー +nioReceiver.eventsError=イベント処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +nioReceiver.noThread=使用å¯èƒ½ãª TcpReplicationThread ãŒã‚ã‚Šã¾ã›ã‚“ +nioReceiver.requestError=NioReceiverã§ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã§ãã¾ã›ã‚“ +nioReceiver.run.fail=レプリケーションリスナーを実行ã§ãã¾ã›ã‚“。 +nioReceiver.start.fail=クラスタレシーãƒã‚’èµ·å‹•ã§ãã¾ã›ã‚“ +nioReceiver.stop.fail=クラスターレシーãƒãƒ¼ã®ã‚»ãƒ¬ã‚¯ã‚¿ã‚’切断ã§ãã¾ã›ã‚“ +nioReceiver.stop.threadRunning=NioReceiver スレッドã¯æ™‚間内ã«åœæ­¢ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚Selectorを切断ã™ã‚‹ã¨ãã«ç•°å¸¸ã‚’検出ã—ãŸå¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +nioReceiver.threadpool.fail=ThreadPool ã‚’åˆæœŸåŒ–ã§ãã¾ã›ã‚“。リスナーを開始ã—ã¾ã›ã‚“ã§ã—ãŸã€‚ +nioReceiver.threadsExhausted=ãƒãƒ£ãƒãƒ«ã‚­ãƒ¼ã¯ç™»éŒ²ã•ã‚Œã¦ã„ã¾ã™ãŒã€æœ€å¾Œã® [{0}] ミリ秒間ã¯interest ops ãŒã‚ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚(キャンセル [{1}]):[{2}] 最終アクセス:[{3}] 考ãˆã‚‰ã‚Œã‚‹åŽŸå› : ã™ã¹ã¦ã®ã‚¹ãƒ¬ãƒƒãƒ‰ãŒä½¿ç”¨ã•ã‚Œã€ã‚¹ãƒ¬ãƒƒãƒ‰ãƒ€ãƒ³ãƒ—を実行ã—ã¾ã™ + +nioReplicationTask.disconnect=Channel ãŒãƒªãƒ¢ãƒ¼ãƒˆã‚¨ãƒ³ãƒ‰ã§é–‰ã˜ã‚‰ã‚Œã€åˆ‡æ–­ã•ã‚Œã¦ã„ã¾ã™ +nioReplicationTask.error.register.key=読ã¿å–り用ã®ã‚­ãƒ¼ã‚’登録中ã®ã‚¨ãƒ©ãƒ¼: [{0}] +nioReplicationTask.exception.drainChannel=TcpReplicationThread.drainChannelã§ä¾‹å¤–をキャッãƒã—ã¾ã—ãŸã€‚ +nioReplicationTask.process.clusterMsg.failed=クラスターメッセージを処ç†ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +nioReplicationTask.unable.ack=ãƒãƒ£ãƒ³ãƒãƒ«ã‹ã‚‰ ACK ã‚’é€ä¿¡ã§ãã¾ã›ã‚“。切断ã•ã‚Œã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚: [{0}] +nioReplicationTask.unable.drainChannel.ioe=レプリケーションワーカーã®IOExceptionã€ãƒãƒ£ãƒãƒ«ã‚’排除ã§ãã¾ã›ã‚“。考ãˆã‚‰ã‚Œã‚‹åŽŸå› ï¼šã‚­ãƒ¼ãƒ—アライブソケットãŒåˆ‡æ–­[{0}]。 + +nioSender.already.connected=NioSenderã¯ã™ã§ã«æŽ¥ç¶šã•ã‚ŒãŸçŠ¶æ…‹ã§ã™ã€‚ +nioSender.datagram.already.established=データグラムãƒãƒ£ãƒãƒ«ã¯ã™ã§ã«ç¢ºç«‹ã•ã‚Œã¦ã„ã¾ã™ã€‚コãƒã‚¯ã‚·ãƒ§ãƒ³ãŒé€²è¡Œä¸­ã§ã‚ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +nioSender.key.inValid=キーã¯ç„¡åŠ¹ã§ã™ã€‚キャンセルã•ã‚Œã¦ã„ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +nioSender.not.connected=NioSender ã¯æŽ¥ç¶šã—ã¦ã„ã¾ã›ã‚“。ã“ã‚Œã¯ç™ºç”Ÿã™ã‚‹ã¹ãã§ã¯ãªã„事象ã§ã™ã€‚ +nioSender.receive.failedAck=失敗ã—ãŸackã‚’å—ã‘å–ã‚Šã¾ã—ãŸï¼šorg.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA +nioSender.sender.disconnected=Sender ãŒåˆ‡æ–­ã•ã‚Œã¾ã—ãŸã€‚Selectキーを処ç†ã§ãã¾ã›ã‚“。 +nioSender.socketChannel.already.established=ソケットãƒãƒ£ãƒãƒ«ã¯ã™ã§ã«ç¢ºç«‹ã•ã‚Œã¦ã„ã¾ã™ã€‚ 接続ãŒé€²è¡Œä¸­ã§ã‚ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +nioSender.unable.disconnect=NioSenderを切断ã§ãã¾ã›ã‚“。msg = [{0}] +nioSender.unable.receive.ack=レスãƒãƒ³ã‚¹ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã§ãã¾ã›ã‚“。ソケットãƒãƒ£ãƒ³ãƒãƒ«ã¯çµ‚端ã«é”ã—ã¦ã„ã¾ã™ã€‚ +nioSender.unknown.state=ä¸æ˜ŽãªçŠ¶æ…‹ã§ã™ã€‚実行å¯èƒ½æ“作セット=[{0}] + +parallelNioSender.error.keepalive=Sender:[{0}]ã® keepalive テスト中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +parallelNioSender.operation.timedout=æ“作ãŒã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã—㟠([{0}] ミリ秒)。 +parallelNioSender.selectorCloseFail=Selector をクローズã§ãã¾ã›ã‚“ã§ã—㟠+parallelNioSender.send.fail=メンãƒãƒ¼ã®é€ä¿¡ã«å¤±æ•—ã—ã¾ã—ãŸï¼š[{0}]; Settingã«è¨­å®šã—ã¾ã™ã€‚ +parallelNioSender.send.fail.retrying=[{0}] ã¸ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é€ä¿¡ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚状態を SUSPECT ã¸å¤‰æ›´ã—ã¦å†é€ã—ã¾ã™ã€‚ +parallelNioSender.send.failed=パラレル NIO é€ä¿¡ã®å¤±æ•— +parallelNioSender.sendFailed.attempt=é€ä¿¡ã«å¤±æ•—ã—ã¾ã—ãŸï¼šè©¦è¡Œ[{0}] 最大:[{1}] +parallelNioSender.sender.disconnected.notRetry=å†è©¦è¡Œã—ã¦ã„ã¾ã›ã‚“:[{0}]; Sender ãŒåˆ‡æ–­ã•ã‚Œã¾ã—ãŸã€‚ +parallelNioSender.sender.disconnected.sendFailed=é€ä¿¡ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚é€ä¿¡å…ˆã¨ã®æŽ¥ç¶šã‚‚失ã‚ã‚Œã¦ã„ã¾ã™ã€‚å†é€ä¿¡ã‚’è¡Œã„ã¾ã›ã‚“。 +parallelNioSender.unable.setup.NioSender=NioSenderã®ã‚»ãƒƒãƒˆã‚¢ãƒƒãƒ—ãŒã§ãã¾ã›ã‚“。 + +pooledParallelSender.sender.disconnected=Sender 接続ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +pooledParallelSender.unable.open=NIO selectorをオープンã§ãã¾ã›ã‚“。 +pooledParallelSender.unable.retrieveSender=Senderプールã‹ã‚‰Senderã‚’å–å¾—ã§ãã¾ã›ã‚“ +pooledParallelSender.unable.retrieveSender.timeout=データSenderã‚’å–å¾—ã§ãã¾ã›ã‚“。タイムアウト ([{0}]ミリ秒) エラー。 diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ko.properties new file mode 100644 index 0000000..1268e35 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ko.properties @@ -0,0 +1,58 @@ +# 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. + +nioReceiver.alreadyStarted=ServerSocketChannelì´ ì´ë¯¸ 시작ë˜ì—ˆìŠµë‹ˆë‹¤. +nioReceiver.cleanup.fail=selector close ì‹œì— cleanupì„ í•  수 없습니다. +nioReceiver.clientDisconnect=복제 í´ë¼ì´ì–¸íŠ¸ê°€ ì—°ê²°ì´ ëŠê²¼ìŠµë‹ˆë‹¤. 키를 pollí•  ë•Œ, 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. 해당 í´ë¼ì´ì–¸íŠ¸ëŠ” 무시ë©ë‹ˆë‹¤. +nioReceiver.eventsError=ì´ë²¤íŠ¸ë“¤ì„ 처리하는 중 오류 ë°œìƒ +nioReceiver.requestError=NioReceiverì—ì„œ ìš”ì²­ì„ ì²˜ë¦¬í•  수 없습니다. +nioReceiver.run.fail=복제 리스너를 실행할 수 없습니다. +nioReceiver.start.fail=í´ëŸ¬ìŠ¤í„° receiver를 시작 í•  수 없습니다. +nioReceiver.stop.fail=í´ëŸ¬ìŠ¤í„° receiver selector를 ë‹«ì„ ìˆ˜ 없습니다. +nioReceiver.stop.threadRunning=NioReceiver 쓰레드가 ì ì ˆí•œ 시간 ë‚´ì— ì¤‘ì§€ë˜ì§€ 않았습니다. Selectorê°€ ë‹«íž ë•Œì— ì˜¤ë¥˜ë“¤ì´ ë°œê²¬ë  ìˆ˜ 있습니다. +nioReceiver.threadpool.fail=ì“°ë ˆë“œí’€ì„ ì´ˆê¸°í™”í•  수 없습니다. 리스너가 시작ë˜ì§€ 못했습니다. +nioReceiver.threadsExhausted=ì±„ë„ í‚¤ê°€ 등ë¡ë˜ì§€ë§Œ, 마지막 [{0}] 밀리초 ë™ì•ˆì— ì–´ë– í•œ interest 오í¼ë ˆì´ì…˜ë„ 없었습니다 (취소 여부: [{1}]). 키:[{2}], 최종 ì ‘ê·¼ 시간:[{3}], ìžˆì„ ë²•í•œ 사유: 모든 ì“°ë ˆë“œë“¤ì´ ì‚¬ìš© 중ì´ê³ , 쓰레드 ë¤í”„ 수행. + +nioReplicationTask.error.register.key=ì½ê¸°ë¥¼ 위한 키 ë“±ë¡ ì¤‘ 오류 ë°œìƒ: [{0}] +nioReplicationTask.exception.drainChannel=TcpReplicationThread.drainChannelì—ì„œ 예외 ë°œìƒ +nioReplicationTask.process.clusterMsg.failed=í´ëŸ¬ìŠ¤í„° 메시지 처리가 실패했습니다. +nioReplicationTask.unable.ack=채ë„ì„ í†µí•´ ACKì„ ë˜ëŒë ¤ 전송할 수 없습니다. 채ë„ì´ ë‹¨ì ˆë˜ì—ˆë‚˜ìš”?: [{0}] +nioReplicationTask.unable.drainChannel.ioe=복제 workerì—ì„œ IOExceptionì´ ë°œìƒí–ˆìœ¼ë©°, 채ë„ì„ ê¹¨ë—ì´ ë¹„ìš¸ 수 없습니다. 있ìŒì§í•œ 사유: Keep alive ì†Œì¼“ì´ ë‹«ížˆëŠ” 경우. [{0}] + +nioSender.already.connected=NioSenderê°€ ì´ë¯¸ ì—°ê²°ëœ ìƒíƒœì— 있습니다. +nioSender.datagram.already.established=ë°ì´í„°ê·¸ëž¨ 채ë„ì´ ì´ë¯¸ 확립ë˜ì–´ 있습니다. ì—°ê²°ì´ ì§„í–‰ ì¤‘ì¸ ìƒíƒœì¼ 수 있습니다. +nioSender.key.inValid=키가 유효하지 않습니다. ì´ëŠ” í•„ì‹œ ì´ë¯¸ 취소ë˜ì—ˆë˜ í‚¤ì¼ ê²ƒìž…ë‹ˆë‹¤. +nioSender.not.connected=NioSenderê°€ ì—°ê²°ë˜ì–´ 있지 않습니다. ì´ëŠ” ì¼ì–´ë‚˜ì„œëŠ” ì•ˆë  ì¼ìž…니다. +nioSender.receive.failedAck=실패한 ACK: org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA를 받았습니다. +nioSender.sender.disconnected=Senderê°€ ì—°ê²°ì´ ëŠì–´ì§„ ìƒíƒœìž…니다. SelectionKey를 처리할 수 없습니다. +nioSender.socketChannel.already.established=소켓 채ë„ì´ ì´ë¯¸ 확립ë˜ì–´ 있습니다. 혹시 ì—°ê²°ì´ ì§„í–‰ ì¤‘ì¼ ìˆ˜ 있습니다. +nioSender.unable.disconnect=NioSenderì˜ ì—°ê²°ì„ ëŠì„ 수 없습니다. 메시지=[{0}] +nioSender.unable.receive.ack=Ack 메시지를 ë°›ì„ ìˆ˜ 없습니다. 소켓 채ë„ì—ì„œ EOFì— ë„달했습니다. +nioSender.unknown.state=ë°ì´í„°ê°€ ì•Œ 수 없는 ìƒíƒœì— 있습니다. readyOps=[{0}] + +parallelNioSender.error.keepalive=Sender [{0}]ì„(를) 위해 keepalive 검사 중 오류 ë°œìƒ +parallelNioSender.operation.timedout=오í¼ë ˆì´ì…˜ì´ 제한 시간 초과ë˜ì—ˆìŠµë‹ˆë‹¤ (제한 시간: [{0}] 밀리초). +parallelNioSender.send.fail=ë©¤ë²„ì— ëŒ€í•œ ì „ì†¡ì´ ì‹¤íŒ¨í•˜ê³  있습니다: [{0}]; ì˜ì‹¬ 멤버로 설정합니다. +parallelNioSender.send.fail.retrying=멤버로 ì „ì†¡ì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤: [{0}]. ì˜ì‹¬ 멤버로 설정하고 다시 ì‹œë„합니다. +parallelNioSender.send.failed=병렬 NIO 전송 실패. +parallelNioSender.sendFailed.attempt=전송 실패, ì´ì‹œë„회수:[{0}] 최대시ë„회수:[{1}] +parallelNioSender.sender.disconnected.notRetry=[{0}]ì„(를) 위해 다시 전송 ì‹œë„를 하지 않습니다. Senderê°€ ì—°ê²°ì´ ëŠê²¼ìŠµë‹ˆë‹¤. +parallelNioSender.sender.disconnected.sendFailed=ì „ì†¡ì´ ì‹¤íŒ¨í–ˆìœ¼ë©° senderê°€ ì—°ê²°ì´ ëŠê²¼ìŠµë‹ˆë‹¤. 재시ë„는 하지 않습니다. +parallelNioSender.unable.setup.NioSender=NioSender를 ì…‹ì—…í•  수 없습니다. + +pooledParallelSender.sender.disconnected=Senderê°€ ì—°ê²°ë˜ì–´ 있지 않습니다. +pooledParallelSender.unable.open=NIO selector를 ì—´ 수 없습니다. +pooledParallelSender.unable.retrieveSender=Sender를 해당 sender 풀로부터 ì–»ì„ ìˆ˜ 없습니다. +pooledParallelSender.unable.retrieveSender.timeout=ë°ì´í„° sender를 조회할 수 없습니다. 제한 시간 초과 ([{0}] 밀리초) 오류. diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ru.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ru.properties new file mode 100644 index 0000000..d552e11 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_ru.properties @@ -0,0 +1,18 @@ +# 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. + +nioReplicationTask.error.register.key=Ошибка региÑтрации ключа Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ:[{0}] + +pooledParallelSender.sender.disconnected=Отправитель не подключен diff --git a/java/org/apache/catalina/tribes/transport/nio/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..b2f2974 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/LocalStrings_zh_CN.properties @@ -0,0 +1,58 @@ +# 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. + +nioReceiver.alreadyStarted=ServerSocketChannelå·²ç»è¢«å¯åŠ¨ +nioReceiver.cleanup.fail=无法清除关闭的选择器 +nioReceiver.clientDisconnect=å¤åˆ¶å®¢æˆ·ç«¯å·²æ–­å¼€è¿žæŽ¥ï¼Œè½®è¯¢å¯†é’¥æ—¶å‡ºé”™ã€‚忽略客户端。 +nioReceiver.eventsError=处ç†äº‹ä»¶æ—¶å‡ºé”™ +nioReceiver.requestError=NioReceiver无法处ç†è¯·æ±‚ +nioReceiver.run.fail=ä¸èƒ½å…许å¤åˆ¶ç›‘å¬å™¨ +nioReceiver.start.fail=无法å¯åŠ¨é›†ç¾¤æŽ¥æ”¶å™¨ +nioReceiver.stop.fail=无法关闭集群接收的选择器 +nioReceiver.stop.threadRunning=NioReceiver线程没有åŠæ—¶åœæ­¢ã€‚关闭选择器时å¯èƒ½ä¼šè§‚察到错误。 +nioReceiver.threadpool.fail=ThreadPool 无法åˆå§‹åŒ–。 监å¬å™¨æœªå¯åŠ¨ã€‚ +nioReceiver.threadsExhausted=通é“密钥已注册,但对上一次[{0}]ms没有兴趣的æ“作(已å–消:[{1}]):[{2}]最åŽä¸€æ¬¡è®¿é—®ï¼š[{3}]å¯èƒ½çš„原因:所有使用的线程,请执行线程转储 + +nioReplicationTask.error.register.key=错误的注册key被读å–:[{0}] +nioReplicationTask.exception.drainChannel=TcpReplicationThread.drainChannel中æ•èŽ·å¼‚常 +nioReplicationTask.process.clusterMsg.failed=处ç†é›†ç¾¤æ¶ˆæ¯å¤±è´¥ +nioReplicationTask.unable.ack=ä¸èƒ½é€šè¿‡channelå‘é€ack,channelå·²ç»æ–­å¼€?[{0}] +nioReplicationTask.unable.drainChannel.ioe=å¤åˆ¶å·¥ä½œå™¨ä¸­çš„IOException,无法耗尽通é“。å¯èƒ½çš„原因:ä¿æŒæ´»åŠ¨å¥—接字关闭[{0}]。 + +nioSender.already.connected=NioSenderå·²ç»å¤„äºŽè¿žæŽ¥çŠ¶æ€ +nioSender.datagram.already.established=æ•°æ®æŠ¥é€šé“å·²ç»å»ºç«‹ã€‚连接å¯èƒ½æ­£åœ¨è¿›è¡Œä¸­ã€‚ +nioSender.key.inValid=Key无效,它必须是已ç»è¢«å–消的。 +nioSender.not.connected=NioSender未连接,这是ä¸åº”该å‘生的。 +nioSender.receive.failedAck=收到一个失败的ack:org.apache.catalina.tribes.transport.Constants.FAIL_ack_DATA +nioSender.sender.disconnected=å‘件人已断开连接,无法处ç†é€‰æ‹©å¯†é’¥ã€‚ +nioSender.socketChannel.already.established=套接字通é“已建立。连接å¯èƒ½æ­£åœ¨è¿›è¡Œä¸­ +nioSender.unable.disconnect=无法断开NioSender,消æ¯=[{0}] +nioSender.unable.receive.ack=无法接收确认消æ¯ã€‚已到达套接字通é“上的EOF。 +nioSender.unknown.state=æ•°æ®å¤„于未知状æ€ã€‚readyOps = [{0}] + +parallelNioSender.error.keepalive=对å‘件人进行keepalive测试时出错:[{0}]。 +parallelNioSender.operation.timedout=æ“作已超时([{0}]ms)。 +parallelNioSender.send.fail=[{0}]çš„æˆå‘˜å‘é€å¤±è´¥ï¼›è®¾ç½®ä¸ºå¯ç–‘ +parallelNioSender.send.fail.retrying=æˆå‘˜å‘é€å¤±è´¥ï¼š[{0}]; 设置为怀疑并é‡è¯•ã€‚ +parallelNioSender.send.failed=并行的NIO.å‘é€å¤±è´¥ã€‚ +parallelNioSender.sendFailed.attempt=å‘é€å¤±è´¥ï¼Œå°è¯•: [{0}] 最大: [{1}] +parallelNioSender.sender.disconnected.notRetry=未é‡è¯•å‘é€ï¼š[{0}]ï¼›å‘件人已断开连接。 +parallelNioSender.sender.disconnected.sendFailed=å‘é€å¤±è´¥ä¸”sender已断开连接,ä¸å†é‡è¯•ã€‚ +parallelNioSender.unable.setup.NioSender=无法设置NioSender。 + +pooledParallelSender.sender.disconnected=sender 未连接。 +pooledParallelSender.unable.open=无法打开nio选择器。 +pooledParallelSender.unable.retrieveSender=无法从sender池中获å–一个sender +pooledParallelSender.unable.retrieveSender.timeout=无法检索数æ®å‘件人,超时([{0}]ms)错误。 diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java new file mode 100644 index 0000000..3d587c1 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java @@ -0,0 +1,487 @@ +/* + * 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.catalina.tribes.transport.nio; + +import java.io.IOException; +import java.net.ServerSocket; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedSelectorException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Deque; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.catalina.tribes.io.ObjectReader; +import org.apache.catalina.tribes.transport.AbstractRxTask; +import org.apache.catalina.tribes.transport.ReceiverBase; +import org.apache.catalina.tribes.transport.RxTaskPool; +import org.apache.catalina.tribes.util.ExceptionUtils; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class NioReceiver extends ReceiverBase implements Runnable, NioReceiverMBean { + + private static final Log log = LogFactory.getLog(NioReceiver.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(NioReceiver.class); + + private volatile boolean running = false; + + private AtomicReference selector = new AtomicReference<>(); + private ServerSocketChannel serverChannel = null; + private DatagramChannel datagramChannel = null; + + protected final Deque events = new ConcurrentLinkedDeque<>(); + + public NioReceiver() { + } + + @Override + public void stop() { + this.stopListening(); + super.stop(); + } + + /** + * Start cluster receiver. + * + * @throws IOException If the receiver fails to start + * + * @see org.apache.catalina.tribes.ChannelReceiver#start() + */ + @Override + public void start() throws IOException { + super.start(); + try { + setPool(new RxTaskPool(getMaxThreads(),getMinThreads(),this)); + } catch (Exception x) { + log.fatal(sm.getString("nioReceiver.threadpool.fail"), x); + if ( x instanceof IOException ) { + throw (IOException)x; + } else { + throw new IOException(x.getMessage()); + } + } + try { + getBind(); + bind(); + String channelName = ""; + if (getChannel().getName() != null) { + channelName = "[" + getChannel().getName() + "]"; + } + Thread t = new Thread(this, "NioReceiver" + channelName); + t.setDaemon(true); + t.start(); + } catch (Exception x) { + log.fatal(sm.getString("nioReceiver.start.fail"), x); + if ( x instanceof IOException ) { + throw (IOException)x; + } else { + throw new IOException(x.getMessage()); + } + } + } + + @Override + public AbstractRxTask createRxTask() { + NioReplicationTask thread = new NioReplicationTask(this,this); + thread.setUseBufferPool(this.getUseBufferPool()); + thread.setRxBufSize(getRxBufSize()); + thread.setOptions(getWorkerThreadOptions()); + return thread; + } + + + + protected void bind() throws IOException { + // allocate an unbound server socket channel + serverChannel = ServerSocketChannel.open(); + // Get the associated ServerSocket to bind it with + ServerSocket serverSocket = serverChannel.socket(); + // create a new Selector for use below + this.selector.set(Selector.open()); + // set the port the server channel will listen to + //serverSocket.bind(new InetSocketAddress(getBind(), getTcpListenPort())); + bind(serverSocket,getPort(),getAutoBind()); + // set non-blocking mode for the listening socket + serverChannel.configureBlocking(false); + // register the ServerSocketChannel with the Selector + serverChannel.register(this.selector.get(), SelectionKey.OP_ACCEPT); + + //set up the datagram channel + if (this.getUdpPort()>0) { + datagramChannel = DatagramChannel.open(); + configureDatagraChannel(); + //bind to the address to avoid security checks + bindUdp(datagramChannel.socket(),getUdpPort(),getAutoBind()); + } + } + + private void configureDatagraChannel() throws IOException { + datagramChannel.configureBlocking(false); + datagramChannel.socket().setSendBufferSize(getUdpTxBufSize()); + datagramChannel.socket().setReceiveBufferSize(getUdpRxBufSize()); + datagramChannel.socket().setReuseAddress(getSoReuseAddress()); + datagramChannel.socket().setSoTimeout(getTimeout()); + datagramChannel.socket().setTrafficClass(getSoTrafficClass()); + } + + public void addEvent(Runnable event) { + Selector selector = this.selector.get(); + if (selector != null) { + events.add(event); + if (log.isTraceEnabled()) { + log.trace("Adding event to selector:" + event); + } + if (isListening()) { + selector.wakeup(); + } + } + } + + public void events() { + if (events.isEmpty()) { + return; + } + Runnable r = null; + while ((r = events.pollFirst()) != null ) { + try { + if (log.isTraceEnabled()) { + log.trace("Processing event in selector:" + r); + } + r.run(); + } catch (Exception x) { + log.error(sm.getString("nioReceiver.eventsError"), x); + } + } + } + + public static void cancelledKey(SelectionKey key) { + ObjectReader reader = (ObjectReader)key.attachment(); + if ( reader != null ) { + reader.setCancelled(true); + reader.finish(); + } + key.cancel(); + key.attach(null); + if (key.channel() instanceof SocketChannel) { + try { ((SocketChannel)key.channel()).socket().close(); } catch (IOException e) { if (log.isDebugEnabled()) { + log.debug(sm.getString("nioReceiver.closeError"), e); + } } + } + if (key.channel() instanceof DatagramChannel) { + try { ((DatagramChannel)key.channel()).socket().close(); } catch (Exception e) { if (log.isDebugEnabled()) { + log.debug(sm.getString("nioReceiver.closeError"), e); + } } + } + try { key.channel().close(); } catch (IOException e) { if (log.isDebugEnabled()) { + log.debug(sm.getString("nioReceiver.closeError"), e); + } } + + } + protected long lastCheck = System.currentTimeMillis(); + protected void socketTimeouts() { + long now = System.currentTimeMillis(); + if ( (now-lastCheck) < getSelectorTimeout() ) { + return; + } + //timeout + Selector tmpsel = this.selector.get(); + Set keys = (isListening()&&tmpsel!=null)?tmpsel.keys():null; + if ( keys == null ) { + return; + } + for (SelectionKey key : keys) { + try { +// if (key.interestOps() == SelectionKey.OP_READ) { +// //only timeout sockets that we are waiting for a read from +// ObjectReader ka = (ObjectReader) key.attachment(); +// long delta = now - ka.getLastAccess(); +// if (delta > (long) getTimeout()) { +// cancelledKey(key); +// } +// } +// else + if ( key.interestOps() == 0 ) { + //check for keys that didn't make it in. + ObjectReader ka = (ObjectReader) key.attachment(); + if ( ka != null ) { + long delta = now - ka.getLastAccess(); + if (delta > getTimeout() && (!ka.isAccessed())) { + if (log.isWarnEnabled()) { + log.warn(sm.getString( + "nioReceiver.threadsExhausted", + Integer.valueOf(getTimeout()), + Boolean.valueOf(ka.isCancelled()), + key, + new java.sql.Timestamp(ka.getLastAccess()))); + } + ka.setLastAccess(now); + //key.interestOps(SelectionKey.OP_READ); + }//end if + } else { + cancelledKey(key); + }//end if + }//end if + }catch ( CancelledKeyException ckx ) { + cancelledKey(key); + } + } + lastCheck = System.currentTimeMillis(); + } + + + /** + * Get data from channel and store in byte array + * send it to cluster + * @throws IOException IO error + */ + protected void listen() throws Exception { + if (doListen()) { + log.warn(sm.getString("nioReceiver.alreadyStarted")); + return; + } + + setListen(true); + + // Avoid NPEs if selector is set to null on stop. + Selector selector = this.selector.get(); + + if (selector!=null && datagramChannel!=null) { + ObjectReader oreader = new ObjectReader(MAX_UDP_SIZE); //max size for a datagram packet + registerChannel(selector,datagramChannel,SelectionKey.OP_READ,oreader); + } + + while (doListen() && selector != null) { + // this may block for a long time, upon return the + // selected set contains keys of the ready channels + try { + events(); + socketTimeouts(); + int n = selector.select(getSelectorTimeout()); + if (n == 0) { + //there is a good chance that we got here + //because the TcpReplicationThread called + //selector wakeup(). + //if that happens, we must ensure that that + //thread has enough time to call interestOps +// synchronized (interestOpsMutex) { + //if we got the lock, means there are no + //keys trying to register for the + //interestOps method +// } + continue; // nothing to do + } + // get an iterator over the set of selected keys + Iterator it = selector.selectedKeys().iterator(); + // look at each key in the selected set + while (it!=null && it.hasNext()) { + SelectionKey key = it.next(); + // Is a new connection coming in? + if (key.isAcceptable()) { + ServerSocketChannel server = (ServerSocketChannel) key.channel(); + SocketChannel channel = server.accept(); + channel.socket().setReceiveBufferSize(getRxBufSize()); + channel.socket().setSendBufferSize(getTxBufSize()); + channel.socket().setTcpNoDelay(getTcpNoDelay()); + channel.socket().setKeepAlive(getSoKeepAlive()); + channel.socket().setOOBInline(getOoBInline()); + channel.socket().setReuseAddress(getSoReuseAddress()); + channel.socket().setSoLinger(getSoLingerOn(),getSoLingerTime()); + channel.socket().setSoTimeout(getTimeout()); + Object attach = new ObjectReader(channel); + registerChannel(selector, + channel, + SelectionKey.OP_READ, + attach); + } + // is there data to read on this channel? + if (key.isReadable()) { + readDataFromSocket(key); + } else { + key.interestOps(key.interestOps() & (~SelectionKey.OP_WRITE)); + } + + // remove key from selected set, it's been handled + it.remove(); + } + } catch (ClosedSelectorException cse) { + // ignore is normal at shutdown or stop listen socket + } catch (CancelledKeyException nx) { + log.warn(sm.getString("nioReceiver.clientDisconnect")); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("nioReceiver.requestError"), t); + } + + } + serverChannel.close(); + if (datagramChannel!=null) { + try { + datagramChannel.close(); + }catch (Exception iox) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("nioReceiver.closeError"), iox); + } + } + datagramChannel=null; + } + closeSelector(); + } + + + + /** + * Close Selector. + * + * @see org.apache.catalina.tribes.transport.ReceiverBase#stop() + */ + protected void stopListening() { + setListen(false); + Selector selector = this.selector.get(); + if (selector != null) { + try { + // Unlock the thread if it is blocked waiting for input + selector.wakeup(); + // Wait for the receiver thread to finish + int count = 0; + while (running && count < 50) { + Thread.sleep(100); + count ++; + } + if (running) { + log.warn(sm.getString("nioReceiver.stop.threadRunning")); + } + closeSelector(); + } catch (Exception x) { + log.error(sm.getString("nioReceiver.stop.fail"), x); + } finally { + this.selector.set(null); + } + } + } + + private void closeSelector() throws IOException { + Selector selector = this.selector.getAndSet(null); + if (selector == null) { + return; + } + try { + // look at each key in the selected set + for (SelectionKey key : selector.keys()) { + key.channel().close(); + key.attach(null); + key.cancel(); + } + } catch (IOException ignore){ + if (log.isWarnEnabled()) { + log.warn(sm.getString("nioReceiver.cleanup.fail"), ignore); + } + } catch (ClosedSelectorException ignore){ + // Ignore + } + try { + selector.selectNow(); + } catch (Throwable t){ + ExceptionUtils.handleThrowable(t); + // Ignore everything else + } + selector.close(); + } + + // ---------------------------------------------------------- + + /** + * Register the given channel with the given selector for + * the given operations of interest + * @param selector The selector to use + * @param channel The channel + * @param ops The operations to register + * @param attach Attachment object + * @throws Exception IO error with channel + */ + protected void registerChannel(Selector selector, + SelectableChannel channel, + int ops, + Object attach) throws Exception { + if (channel == null) + { + return; // could happen + } + // set the new channel non-blocking + channel.configureBlocking(false); + // register it with the selector + channel.register(selector, ops, attach); + } + + /** + * Start thread and listen + */ + @Override + public void run() { + running = true; + try { + listen(); + } catch (Exception x) { + log.error(sm.getString("nioReceiver.run.fail"), x); + } finally { + running = false; + } + } + + // ---------------------------------------------------------- + + /** + * Sample data handler method for a channel with data ready to read. + * @param key A SelectionKey object associated with a channel + * determined by the selector to be ready for reading. If the + * channel returns an EOF condition, it is closed here, which + * automatically invalidates the associated key. The selector + * will then de-register the channel on the next select call. + * @throws Exception IO error with channel + */ + protected void readDataFromSocket(SelectionKey key) throws Exception { + NioReplicationTask task = (NioReplicationTask) getTaskPool().getRxTask(); + if (task == null) { + // No threads/tasks available, do nothing, the selection + // loop will keep calling this method until a + // thread becomes available, the thread pool itself has a waiting mechanism + // so we will not wait here. + if (log.isDebugEnabled()) { + log.debug(sm.getString("nioReceiver.noThread")); + } + } else { + // invoking this wakes up the worker thread then returns + //add task to thread pool + task.serviceChannel(key); + getExecutor().execute(task); + } + } + + +} diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReceiverMBean.java b/java/org/apache/catalina/tribes/transport/nio/NioReceiverMBean.java new file mode 100644 index 0000000..acf7e7f --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/NioReceiverMBean.java @@ -0,0 +1,78 @@ +/* + * 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.catalina.tribes.transport.nio; + + +public interface NioReceiverMBean { + + // Receiver Attributes + String getAddress(); + + boolean getDirect(); + + int getPort(); + + int getAutoBind(); + + int getSecurePort(); + + int getUdpPort(); + + long getSelectorTimeout(); + + int getMaxThreads(); + + int getMinThreads(); + + long getMaxIdleTime(); + + boolean getOoBInline(); + + int getRxBufSize(); + + int getTxBufSize(); + + int getUdpRxBufSize(); + + int getUdpTxBufSize(); + + boolean getSoKeepAlive(); + + boolean getSoLingerOn(); + + int getSoLingerTime(); + + boolean getSoReuseAddress(); + + boolean getTcpNoDelay(); + + int getTimeout(); + + boolean getUseBufferPool(); + + boolean isListening(); + + // pool stats + int getPoolSize(); + + int getActiveCount(); + + long getTaskCount(); + + long getCompletedTaskCount(); + +} diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReplicationTask.java b/java/org/apache/catalina/tribes/transport/nio/NioReplicationTask.java new file mode 100644 index 0000000..f28648c --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/NioReplicationTask.java @@ -0,0 +1,362 @@ +/* + * 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.catalina.tribes.transport.nio; +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ChannelReceiver; +import org.apache.catalina.tribes.RemoteProcessException; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.io.BufferPool; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.ListenCallback; +import org.apache.catalina.tribes.io.ObjectReader; +import org.apache.catalina.tribes.transport.AbstractRxTask; +import org.apache.catalina.tribes.transport.Constants; +import org.apache.catalina.tribes.util.Logs; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A worker thread class which can drain channels and echo-back the input. Each + * instance is constructed with a reference to the owning thread pool object. + * When started, the thread loops forever waiting to be awakened to service the + * channel associated with a SelectionKey object. The worker is tasked by + * calling its serviceChannel() method with a SelectionKey object. The + * serviceChannel() method stores the key reference in the thread object then + * calls notify() to wake it up. When the channel has been drained, the worker + * thread returns itself to its parent pool. + */ +public class NioReplicationTask extends AbstractRxTask { + + private static final Log log = LogFactory.getLog(NioReplicationTask.class); + protected static final StringManager sm = StringManager.getManager(NioReplicationTask.class); + + private ByteBuffer buffer = null; + private SelectionKey key; + private int rxBufSize; + private final NioReceiver receiver; + + public NioReplicationTask (ListenCallback callback, NioReceiver receiver) { + super(callback); + this.receiver = receiver; + } + + // loop forever waiting for work to do + @Override + public synchronized void run() { + if ( buffer == null ) { + int size = getRxBufSize(); + if (key.channel() instanceof DatagramChannel) { + size = ChannelReceiver.MAX_UDP_SIZE; + } + if ( (getOptions() & OPTION_DIRECT_BUFFER) == OPTION_DIRECT_BUFFER) { + buffer = ByteBuffer.allocateDirect(size); + } else { + buffer = ByteBuffer.allocate(size); + } + } else { + buffer.clear(); + } + if (key == null) { + return; // just in case + } + if ( log.isTraceEnabled() ) { + log.trace("Servicing key:"+key); + } + + try { + ObjectReader reader = (ObjectReader)key.attachment(); + if ( reader == null ) { + if ( log.isTraceEnabled() ) { + log.trace("No object reader, cancelling:"+key); + } + cancelKey(key); + } else { + if ( log.isTraceEnabled() ) { + log.trace("Draining channel:"+key); + } + + drainChannel(key, reader); + } + } catch (Exception e) { + //this is common, since the sockets on the other + //end expire after a certain time. + if ( e instanceof CancelledKeyException ) { + //do nothing + } else if ( e instanceof IOException ) { + //don't spew out stack traces for IO exceptions unless debug is enabled. + if (log.isDebugEnabled()) { + log.debug(sm.getString("nioReplicationTask.unable.drainChannel.ioe", e.getMessage()), e); + } else { + log.warn (sm.getString("nioReplicationTask.unable.drainChannel.ioe", e.getMessage())); + } + } else if ( log.isErrorEnabled() ) { + //this is a real error, log it. + log.error(sm.getString("nioReplicationTask.exception.drainChannel"),e); + } + cancelKey(key); + } + key = null; + // done, ready for more, return to pool + getTaskPool().returnWorker (this); + } + + /** + * Called to initiate a unit of work by this worker thread + * on the provided SelectionKey object. This method is + * synchronized, as is the run() method, so only one key + * can be serviced at a given time. + * Before waking the worker thread, and before returning + * to the main selection loop, this key's interest set is + * updated to remove OP_READ. This will cause the selector + * to ignore read-readiness for this channel while the + * worker thread is servicing it. + * @param key The key to process + */ + public synchronized void serviceChannel (SelectionKey key) { + if ( log.isTraceEnabled() ) { + log.trace("About to service key:"+key); + } + ObjectReader reader = (ObjectReader)key.attachment(); + if ( reader != null ) { + reader.setLastAccess(System.currentTimeMillis()); + } + this.key = key; + key.interestOps (key.interestOps() & (~SelectionKey.OP_READ)); + key.interestOps (key.interestOps() & (~SelectionKey.OP_WRITE)); + } + + /** + * The actual code which drains the channel associated with + * the given key. This method assumes the key has been + * modified prior to invocation to turn off selection + * interest in OP_READ. When this method completes it + * re-enables OP_READ and calls wakeup() on the selector + * so the selector will resume watching this channel. + * @param key The key to process + * @param reader The reader + * @throws Exception IO error + */ + protected void drainChannel (final SelectionKey key, ObjectReader reader) throws Exception { + reader.access(); + ReadableByteChannel channel = (ReadableByteChannel) key.channel(); + int count=-1; + SocketAddress saddr = null; + + if (channel instanceof SocketChannel) { + // loop while data available, channel is non-blocking + while ((count = channel.read (buffer)) > 0) { + buffer.flip(); // make buffer readable + if ( buffer.hasArray() ) { + reader.append(buffer.array(),0,count,false); + } else { + reader.append(buffer,count,false); + } + buffer.clear(); // make buffer empty + //do we have at least one package? + if ( reader.hasPackage() ) { + break; + } + } + } else if (channel instanceof DatagramChannel) { + DatagramChannel dchannel = (DatagramChannel)channel; + saddr = dchannel.receive(buffer); + buffer.flip(); // make buffer readable + if ( buffer.hasArray() ) { + reader.append(buffer.array(),0,buffer.limit()-buffer.position(),false); + } else { + reader.append(buffer,buffer.limit()-buffer.position(),false); + } + buffer.clear(); // make buffer empty + //did we get a package + count = reader.hasPackage()?1:-1; + } + + int pkgcnt = reader.count(); + + if (count < 0 && pkgcnt == 0 ) { + //end of stream, and no more packages to process + remoteEof(key); + return; + } + + ChannelMessage[] msgs = pkgcnt == 0? ChannelData.EMPTY_DATA_ARRAY : reader.execute(); + + registerForRead(key,reader);//register to read new data, before we send it off to avoid dead locks + + for (ChannelMessage msg : msgs) { + /* + * Use send ack here if you want to ack the request to the remote + * server before completing the request + * This is considered an asynchronous request + */ + if (ChannelData.sendAckAsync(msg.getOptions())) { + sendAck(key, (WritableByteChannel) channel, Constants.ACK_COMMAND, saddr); + } + try { + if (Logs.MESSAGES.isTraceEnabled()) { + try { + Logs.MESSAGES.trace("NioReplicationThread - Received msg:" + new UniqueId(msg.getUniqueId()) + " at " + new java.sql.Timestamp(System.currentTimeMillis())); + } catch (Throwable t) { + } + } + //process the message + getCallback().messageDataReceived(msg); + /* + * Use send ack here if you want the request to complete on this + * server before sending the ack to the remote server + * This is considered a synchronized request + */ + if (ChannelData.sendAckSync(msg.getOptions())) { + sendAck(key, (WritableByteChannel) channel, Constants.ACK_COMMAND, saddr); + } + } catch (RemoteProcessException e) { + if (log.isDebugEnabled()) { + log.error(sm.getString("nioReplicationTask.process.clusterMsg.failed"), e); + } + if (ChannelData.sendAckSync(msg.getOptions())) { + sendAck(key, (WritableByteChannel) channel, Constants.FAIL_ACK_COMMAND, saddr); + } + } catch (Exception e) { + log.error(sm.getString("nioReplicationTask.process.clusterMsg.failed"), e); + if (ChannelData.sendAckSync(msg.getOptions())) { + sendAck(key, (WritableByteChannel) channel, Constants.FAIL_ACK_COMMAND, saddr); + } + } + if (getUseBufferPool()) { + BufferPool.getBufferPool().returnBuffer(msg.getMessage()); + msg.setMessage(null); + } + } + + if (count < 0) { + remoteEof(key); + } + } + + private void remoteEof(SelectionKey key) { + // close channel on EOF, invalidates the key + if ( log.isDebugEnabled() ) { + log.debug(sm.getString("nioReplicationTask.disconnect")); + } + cancelKey(key); + } + + protected void registerForRead(final SelectionKey key, ObjectReader reader) { + if ( log.isTraceEnabled() ) { + log.trace("Adding key for read event:"+key); + } + reader.finish(); + //register our OP_READ interest + Runnable r = () -> { + try { + if (key.isValid()) { + // resume interest in OP_READ, OP_WRITE + int resumeOps = key.interestOps() | SelectionKey.OP_READ; + key.interestOps(resumeOps); + if ( log.isTraceEnabled() ) { + log.trace("Registering key for read:"+key); + } + } + } catch (CancelledKeyException ckx ) { + NioReceiver.cancelledKey(key); + if ( log.isTraceEnabled() ) { + log.trace("CKX Cancelling key:"+key); + } + + } catch (Exception x) { + log.error(sm.getString("nioReplicationTask.error.register.key", key),x); + } + }; + receiver.addEvent(r); + } + + private void cancelKey(final SelectionKey key) { + if ( log.isTraceEnabled() ) { + log.trace("Adding key for cancel event:"+key); + } + + ObjectReader reader = (ObjectReader)key.attachment(); + if ( reader != null ) { + reader.setCancelled(true); + reader.finish(); + } + Runnable cx = () -> { + if ( log.isTraceEnabled() ) { + log.trace("Cancelling key:"+key); + } + + NioReceiver.cancelledKey(key); + }; + receiver.addEvent(cx); + } + + + /** + * Send a reply-acknowledgement (6,2,3), sends it doing a busy write, the ACK is so small + * that it should always go to the buffer. + * @param key The key to use + * @param channel The channel + * @param command The command to write + * @param udpaddr Target address + */ + protected void sendAck(SelectionKey key, WritableByteChannel channel, byte[] command, SocketAddress udpaddr) { + try { + + ByteBuffer buf = ByteBuffer.wrap(command); + int total = 0; + if (channel instanceof DatagramChannel) { + DatagramChannel dchannel = (DatagramChannel)channel; + //were using a shared channel, document says its thread safe + //TODO check optimization, one channel per thread? + while ( total < command.length ) { + total += dchannel.send(buf, udpaddr); + } + } else { + while ( total < command.length ) { + total += channel.write(buf); + } + } + if (log.isTraceEnabled()) { + log.trace("ACK sent to " + + ( (channel instanceof SocketChannel) ? + ((SocketChannel)channel).socket().getInetAddress() : + ((DatagramChannel)channel).socket().getInetAddress())); + } + } catch (IOException x) { + log.warn(sm.getString("nioReplicationTask.unable.ack", x.getMessage())); + } + } + + public void setRxBufSize(int rxBufSize) { + this.rxBufSize = rxBufSize; + } + + public int getRxBufSize() { + return rxBufSize; + } +} diff --git a/java/org/apache/catalina/tribes/transport/nio/NioSender.java b/java/org/apache/catalina/tribes/transport/nio/NioSender.java new file mode 100644 index 0000000..82e7149 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/NioSender.java @@ -0,0 +1,424 @@ +/* + * 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.catalina.tribes.transport.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Arrays; + +import org.apache.catalina.tribes.RemoteProcessException; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.transport.AbstractSender; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * This class is NOT thread safe and should never be used with more than one thread at a time + * + * This is a state machine, handled by the process method + * States are: + * - NOT_CONNECTED -> connect() -> CONNECTED + * - CONNECTED -> setMessage() -> READY TO WRITE + * - READY_TO_WRITE -> write() -> READY TO WRITE | READY TO READ + * - READY_TO_READ -> read() -> READY_TO_READ | TRANSFER_COMPLETE + * - TRANSFER_COMPLETE -> CONNECTED + */ +public class NioSender extends AbstractSender { + + private static final Log log = LogFactory.getLog(NioSender.class); + protected static final StringManager sm = StringManager.getManager(NioSender.class); + + + protected Selector selector; + protected SocketChannel socketChannel = null; + protected DatagramChannel dataChannel = null; + + /* + * STATE VARIABLES * + */ + protected ByteBuffer readbuf = null; + protected ByteBuffer writebuf = null; + protected volatile byte[] current = null; + protected final XByteBuffer ackbuf = new XByteBuffer(128,true); + protected int remaining = 0; + protected boolean complete; + + protected boolean connecting = false; + + public NioSender() { + super(); + + } + + /** + * State machine to send data. + * @param key The key to use + * @param waitForAck Wait for an ack + * @return true if the processing was successful + * @throws IOException An IO error occurred + */ + public boolean process(SelectionKey key, boolean waitForAck) throws IOException { + int ops = key.readyOps(); + key.interestOps(key.interestOps() & ~ops); + //in case disconnect has been called + if ((!isConnected()) && (!connecting)) { + throw new IOException(sm.getString("nioSender.sender.disconnected")); + } + if ( !key.isValid() ) { + throw new IOException(sm.getString("nioSender.key.inValid")); + } + if ( key.isConnectable() ) { + if ( socketChannel.finishConnect() ) { + completeConnect(); + if ( current != null ) { + key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); + } + return false; + } else { + //wait for the connection to finish + key.interestOps(key.interestOps() | SelectionKey.OP_CONNECT); + return false; + }//end if + } else if ( key.isWritable() ) { + boolean writecomplete = write(); + if ( writecomplete ) { + //we are completed, should we read an ack? + if ( waitForAck ) { + //register to read the ack + key.interestOps(key.interestOps() | SelectionKey.OP_READ); + } else { + //if not, we are ready, setMessage will reregister us for another write interest + //do a health check, we have no way of verify a disconnected + //socket since we don't register for OP_READ on waitForAck=false + read();//this causes overhead + setRequestCount(getRequestCount()+1); + return true; + } + } else { + //we are not complete, lets write some more + key.interestOps(key.interestOps()|SelectionKey.OP_WRITE); + }//end if + } else if ( key.isReadable() ) { + boolean readcomplete = read(); + if ( readcomplete ) { + setRequestCount(getRequestCount()+1); + return true; + } else { + key.interestOps(key.interestOps() | SelectionKey.OP_READ); + }//end if + } else { + //unknown state, should never happen + log.warn(sm.getString("nioSender.unknown.state", Integer.toString(ops))); + throw new IOException(sm.getString("nioSender.unknown.state", Integer.toString(ops))); + }//end if + return false; + } + + private void configureSocket() throws IOException { + if (socketChannel!=null) { + socketChannel.configureBlocking(false); + socketChannel.socket().setSendBufferSize(getTxBufSize()); + socketChannel.socket().setReceiveBufferSize(getRxBufSize()); + socketChannel.socket().setSoTimeout((int)getTimeout()); + socketChannel.socket().setSoLinger(getSoLingerOn(),getSoLingerOn()?getSoLingerTime():0); + socketChannel.socket().setTcpNoDelay(getTcpNoDelay()); + socketChannel.socket().setKeepAlive(getSoKeepAlive()); + socketChannel.socket().setReuseAddress(getSoReuseAddress()); + socketChannel.socket().setOOBInline(getOoBInline()); + socketChannel.socket().setSoLinger(getSoLingerOn(),getSoLingerTime()); + socketChannel.socket().setTrafficClass(getSoTrafficClass()); + } else if (dataChannel!=null) { + dataChannel.configureBlocking(false); + dataChannel.socket().setSendBufferSize(getUdpTxBufSize()); + dataChannel.socket().setReceiveBufferSize(getUdpRxBufSize()); + dataChannel.socket().setSoTimeout((int)getTimeout()); + dataChannel.socket().setReuseAddress(getSoReuseAddress()); + dataChannel.socket().setTrafficClass(getSoTrafficClass()); + } + } + + private void completeConnect() { + //we connected, register ourselves for writing + setConnected(true); + connecting = false; + setRequestCount(0); + setConnectTime(System.currentTimeMillis()); + } + + + + protected boolean read() throws IOException { + //if there is no message here, we are done + if ( current == null ) { + return true; + } + int read = isUdpBased()?dataChannel.read(readbuf) : socketChannel.read(readbuf); + //end of stream + if ( read == -1 ) { + throw new IOException(sm.getString("nioSender.unable.receive.ack")); + } else if ( read == 0 ) { + return false; + } + readbuf.flip(); + ackbuf.append(readbuf,read); + readbuf.clear(); + if (ackbuf.doesPackageExist() ) { + byte[] ackcmd = ackbuf.extractDataPackage(true).getBytes(); + boolean ack = Arrays.equals(ackcmd,org.apache.catalina.tribes.transport.Constants.ACK_DATA); + boolean fack = Arrays.equals(ackcmd,org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA); + if ( fack && getThrowOnFailedAck() ) { + throw new RemoteProcessException(sm.getString("nioSender.receive.failedAck")); + } + return ack || fack; + } else { + return false; + } + } + + + protected boolean write() throws IOException { + if ( (!isConnected()) || (this.socketChannel==null && this.dataChannel==null)) { + throw new IOException(sm.getString("nioSender.not.connected")); + } + if ( current != null ) { + if ( remaining > 0 ) { + //we have written everything, or we are starting a new package + //protect against buffer overwrite + int byteswritten = isUdpBased()?dataChannel.write(writebuf) : socketChannel.write(writebuf); + remaining -= byteswritten; + //if the entire message was written from the buffer + //reset the position counter + if ( remaining < 0 ) { + remaining = 0; + } + } + return (remaining==0); + } + //no message to send, we can consider that complete + return true; + } + + /** + * connect - blocking in this operation + * + * @throws IOException + * TODO Implement this org.apache.catalina.tribes.transport.IDataSender method + */ + @Override + public synchronized void connect() throws IOException { + if ( connecting || isConnected()) { + return; + } + connecting = true; + if ( isConnected() ) { + throw new IOException(sm.getString("nioSender.already.connected")); + } + if ( readbuf == null ) { + readbuf = getReadBuffer(); + } else { + readbuf.clear(); + } + if ( writebuf == null ) { + writebuf = getWriteBuffer(); + } else { + writebuf.clear(); + } + + if (isUdpBased()) { + InetSocketAddress daddr = new InetSocketAddress(getAddress(),getUdpPort()); + if ( dataChannel != null ) { + throw new IOException(sm.getString("nioSender.datagram.already.established")); + } + dataChannel = DatagramChannel.open(); + configureSocket(); + dataChannel.connect(daddr); + completeConnect(); + dataChannel.register(getSelector(),SelectionKey.OP_WRITE, this); + + } else { + InetSocketAddress addr = new InetSocketAddress(getAddress(),getPort()); + if ( socketChannel != null ) { + throw new IOException(sm.getString("nioSender.socketChannel.already.established")); + } + socketChannel = SocketChannel.open(); + configureSocket(); + if ( socketChannel.connect(addr) ) { + completeConnect(); + socketChannel.register(getSelector(), SelectionKey.OP_WRITE, this); + } else { + socketChannel.register(getSelector(), SelectionKey.OP_CONNECT, this); + } + } + } + + + /** + * disconnect + * + * TODO Implement this org.apache.catalina.tribes.transport.IDataSender method + */ + @Override + public void disconnect() { + try { + connecting = false; + setConnected(false); + if (socketChannel != null) { + try { + try { + socketChannel.socket().close(); + } catch (Exception x) { + // Ignore + } + //error free close, all the way + //try {socket.shutdownOutput();}catch ( Exception x){} + //try {socket.shutdownInput();}catch ( Exception x){} + //try {socket.close();}catch ( Exception x){} + try { + socketChannel.close(); + } catch (Exception x) { + // Ignore + } + } finally { + socketChannel = null; + } + } + if (dataChannel != null) { + try { + try { + dataChannel.socket().close(); + } catch (Exception x) { + // Ignore + } + //error free close, all the way + //try {socket.shutdownOutput();}catch ( Exception x){} + //try {socket.shutdownInput();}catch ( Exception x){} + //try {socket.close();}catch ( Exception x){} + try { + dataChannel.close(); + } catch (Exception x) { + // Ignore + } + } finally { + dataChannel = null; + } + } + } catch ( Exception x ) { + log.error(sm.getString("nioSender.unable.disconnect", x.getMessage())); + if ( log.isDebugEnabled() ) { + log.debug(sm.getString("nioSender.unable.disconnect", x.getMessage()),x); + } + } + } + + public void reset() { + if ( isConnected() && readbuf == null) { + readbuf = getReadBuffer(); + } + if ( readbuf != null ) { + readbuf.clear(); + } + if ( writebuf != null ) { + writebuf.clear(); + } + current = null; + ackbuf.clear(); + remaining = 0; + complete = false; + setAttempt(0); + setUdpBased(false); + } + + private ByteBuffer getReadBuffer() { + return getBuffer(getRxBufSize()); + } + + private ByteBuffer getWriteBuffer() { + return getBuffer(getTxBufSize()); + } + + private ByteBuffer getBuffer(int size) { + return getDirectBuffer()?ByteBuffer.allocateDirect(size):ByteBuffer.allocate(size); + } + + /** + * sendMessage + * + * @param data ChannelMessage + * @throws IOException + * TODO Implement this org.apache.catalina.tribes.transport.IDataSender method + */ + public void setMessage(byte[] data) throws IOException { + setMessage(data,0,data.length); + } + + public void setMessage(byte[] data,int offset, int length) throws IOException { + if (data != null) { + synchronized (this) { + current = data; + remaining = length; + ackbuf.clear(); + if (writebuf != null) { + writebuf.clear(); + } else { + writebuf = getBuffer(length); + } + if (writebuf.capacity() < length) { + writebuf = getBuffer(length); + } + + // TODO use ByteBuffer.wrap to avoid copying the data. + writebuf.put(data,offset,length); + writebuf.flip(); + if (isConnected()) { + if (isUdpBased()) { + dataChannel.register(getSelector(), SelectionKey.OP_WRITE, this); + } else { + socketChannel.register(getSelector(), SelectionKey.OP_WRITE, this); + } + } + } + } + } + + public byte[] getMessage() { + return current; + } + + + public boolean isComplete() { + return complete; + } + + public Selector getSelector() { + return selector; + } + + public void setSelector(Selector selector) { + this.selector = selector; + } + + + public void setComplete(boolean complete) { + this.complete = complete; + } +} diff --git a/java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java b/java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java new file mode 100644 index 0000000..f73347d --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java @@ -0,0 +1,436 @@ +/* + * 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.catalina.tribes.transport.nio; + +import java.io.IOException; +import java.lang.ref.Cleaner; +import java.net.UnknownHostException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.transport.AbstractSender; +import org.apache.catalina.tribes.transport.MultiPointSender; +import org.apache.catalina.tribes.transport.SenderState; +import org.apache.catalina.tribes.util.Logs; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class ParallelNioSender extends AbstractSender implements MultiPointSender { + + private static final Log log = LogFactory.getLog(ParallelNioSender.class); + protected static final StringManager sm = StringManager.getManager(ParallelNioSender.class); + + private static final Cleaner cleaner = Cleaner.create(); + + private final InternalState state; + + protected final long selectTimeout = 5000; //default 5 seconds, same as send timeout + + public ParallelNioSender() throws IOException { + state = new InternalState(Selector.open()); + cleaner.register(this, state); + setConnected(true); + } + + + @Override + public synchronized void sendMessage(Member[] destination, ChannelMessage msg) + throws ChannelException { + long start = System.currentTimeMillis(); + this.setUdpBased((msg.getOptions()&Channel.SEND_OPTIONS_UDP) == Channel.SEND_OPTIONS_UDP); + byte[] data = XByteBuffer.createDataPackage((ChannelData)msg); + NioSender[] senders = setupForSend(destination); + connect(senders); + setData(senders,data); + + int remaining = senders.length; + ChannelException cx = null; + try { + //loop until complete, an error happens, or we timeout + long delta = System.currentTimeMillis() - start; + boolean waitForAck = (Channel.SEND_OPTIONS_USE_ACK & + msg.getOptions()) == Channel.SEND_OPTIONS_USE_ACK; + while ( (remaining>0) && (delta 0 ) { + //timeout has occurred + ChannelException cxtimeout = new ChannelException(sm.getString( + "parallelNioSender.operation.timedout", Long.toString(getTimeout()))); + if (cx == null) { + cx = new ChannelException(sm.getString("parallelNioSender.operation.timedout", + Long.toString(getTimeout()))); + } + for (NioSender sender : senders) { + if (!sender.isComplete()) { + cx.addFaultyMember(sender.getDestination(), cxtimeout); + } + } + throw cx; + } else if ( cx != null ) { + //there was an error + throw cx; + } + } catch (Exception x ) { + try { + this.disconnect(); + } catch (Exception e) { + // Ignore + } + if ( x instanceof ChannelException ) { + throw (ChannelException)x; + } else { + throw new ChannelException(x); + } + } + + } + + private SendResult doLoop(long selectTimeOut, int maxAttempts, boolean waitForAck, ChannelMessage msg) + throws ChannelException { + SendResult result = new SendResult(); + int selectedKeys; + try { + selectedKeys = state.selector.select(selectTimeOut); + } catch (IOException ioe) { + throw new ChannelException(sm.getString("parallelNioSender.send.failed"), ioe); + } + + if (selectedKeys == 0) { + return result; + } + + Iterator it = state.selector.selectedKeys().iterator(); + while (it.hasNext()) { + SelectionKey sk = it.next(); + it.remove(); + int readyOps = sk.readyOps(); + sk.interestOps(sk.interestOps() & ~readyOps); + NioSender sender = (NioSender) sk.attachment(); + try { + if (sender.process(sk,waitForAck)) { + sender.setComplete(true); + result.complete(sender); + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("ParallelNioSender - Sent msg:" + + new UniqueId(msg.getUniqueId()) + " at " + + new java.sql.Timestamp(System.currentTimeMillis()) + " to " + + sender.getDestination().getName()); + } + SenderState.getSenderState(sender.getDestination()).setReady(); + }//end if + } catch (Exception x) { + if (log.isTraceEnabled()) { + log.trace("Error while processing send to " + sender.getDestination().getName(), + x); + } + SenderState state = SenderState.getSenderState(sender.getDestination()); + int attempt = sender.getAttempt()+1; + boolean retry = (attempt <= maxAttempts && maxAttempts>0); + synchronized (state) { + + //sk.cancel(); + if (state.isSuspect()) { + state.setFailing(); + } + if (state.isReady()) { + state.setSuspect(); + if ( retry ) { + log.warn(sm.getString("parallelNioSender.send.fail.retrying", sender.getDestination().getName())); + } else { + log.warn(sm.getString("parallelNioSender.send.fail", sender.getDestination().getName()), x); + } + } + } + if ( !isConnected() ) { + log.warn(sm.getString("parallelNioSender.sender.disconnected.notRetry", sender.getDestination().getName())); + ChannelException cx = new ChannelException(sm.getString("parallelNioSender.sender.disconnected.sendFailed"), x); + cx.addFaultyMember(sender.getDestination(),x); + result.failed(cx); + break; + } + + byte[] data = sender.getMessage(); + if (retry) { + try { + sender.disconnect(); + sender.connect(); + sender.setAttempt(attempt); + sender.setMessage(data); + } catch (Exception ignore){ + state.setFailing(); + } + } else { + ChannelException cx = new ChannelException( + sm.getString("parallelNioSender.sendFailed.attempt", + Integer.toString(sender.getAttempt()), + Integer.toString(maxAttempts)), x); + cx.addFaultyMember(sender.getDestination(),x); + result.failed(cx); + }//end if + } + } + return result; + } + + private static class SendResult { + private List completeSenders = new ArrayList<>(); + private ChannelException exception = null; + private void complete(NioSender sender) { + if (!completeSenders.contains(sender)) { + completeSenders.add(sender); + } + } + private int getCompleted() { + return completeSenders.size(); + } + private void failed(ChannelException cx){ + if (exception == null) { + exception = cx; + } + exception.addFaultyMember(cx.getFaultyMembers()); + } + + private ChannelException getFailed() { + return exception; + } + } + + private void connect(NioSender[] senders) throws ChannelException { + ChannelException x = null; + for (NioSender sender : senders) { + try { + sender.connect(); + } catch (IOException io) { + if (x == null) { + x = new ChannelException(io); + } + x.addFaultyMember(sender.getDestination(), io); + } + } + if ( x != null ) { + throw x; + } + } + + private void setData(NioSender[] senders, byte[] data) throws ChannelException { + ChannelException x = null; + for (NioSender sender : senders) { + try { + sender.setMessage(data); + } catch (IOException io) { + if (x == null) { + x = new ChannelException(io); + } + x.addFaultyMember(sender.getDestination(), io); + } + } + if ( x != null ) { + throw x; + } + } + + + private NioSender[] setupForSend(Member[] destination) throws ChannelException { + ChannelException cx = null; + NioSender[] result = new NioSender[destination.length]; + for ( int i=0; i> iter = state.nioSenders.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + try { + entry.getValue().disconnect(); + } catch (Exception e) { + if (x == null) { + x = new ChannelException(e); + } + x.addFaultyMember(entry.getKey(), e); + } + iter.remove(); + } + if (x != null) { + throw x; + } + } + + @Override + public void add(Member member) { + // NOOP + } + + @Override + public void remove(Member member) { + //disconnect senders + NioSender sender = state.nioSenders.remove(member); + if ( sender != null ) { + sender.disconnect(); + } + } + + + @Override + public synchronized void disconnect() { + setConnected(false); + try { + close(); + } catch (Exception x) { + // Ignore + } + } + + @Override + public synchronized boolean keepalive() { + boolean result = false; + for (Iterator> i = state.nioSenders.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = i.next(); + NioSender sender = entry.getValue(); + if ( sender.keepalive() ) { + //nioSenders.remove(entry.getKey()); + i.remove(); + result = true; + } else { + try { + sender.read(); + }catch ( IOException x ) { + sender.disconnect(); + sender.reset(); + //nioSenders.remove(entry.getKey()); + i.remove(); + result = true; + }catch ( Exception x ) { + log.warn(sm.getString("parallelNioSender.error.keepalive", sender),x); + } + } + } + //clean up any cancelled keys + if ( result ) { + try { state.selector.selectNow(); }catch (Exception e){/*Ignore*/} + } + return result; + } + + + private static class InternalState implements Runnable { + + private final Selector selector; + private final HashMap nioSenders = new HashMap<>(); + + private InternalState(Selector selector) { + this.selector = selector; + } + + @Override + public void run() { + Iterator iter = nioSenders.values().iterator(); + while (iter.hasNext()) { + NioSender nioSender = iter.next(); + try { + nioSender.disconnect(); + } catch (Exception e) { + // Ignore + } + iter.remove(); + } + try { + selector.close(); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("parallelNioSender.selectorCloseFail"), e); + } + } + } + } +} diff --git a/java/org/apache/catalina/tribes/transport/nio/PooledParallelSender.java b/java/org/apache/catalina/tribes/transport/nio/PooledParallelSender.java new file mode 100644 index 0000000..d344022 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/PooledParallelSender.java @@ -0,0 +1,71 @@ +/* + * 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.catalina.tribes.transport.nio; + +import java.io.IOException; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.transport.DataSender; +import org.apache.catalina.tribes.transport.PooledSender; +import org.apache.catalina.tribes.util.StringManager; + +public class PooledParallelSender extends PooledSender implements PooledParallelSenderMBean { + protected static final StringManager sm = StringManager.getManager(PooledParallelSender.class); + + @Override + public void sendMessage(Member[] destination, ChannelMessage message) throws ChannelException { + if (!isConnected()) { + throw new ChannelException(sm.getString("pooledParallelSender.sender.disconnected")); + } + ParallelNioSender sender = (ParallelNioSender)getSender(); + if (sender == null) { + ChannelException cx = new ChannelException(sm.getString( + "pooledParallelSender.unable.retrieveSender.timeout", + Long.toString(getMaxWait()))); + for (Member member : destination) { + cx.addFaultyMember(member, new NullPointerException(sm.getString("pooledParallelSender.unable.retrieveSender"))); + } + throw cx; + } else { + try { + if (!sender.isConnected()) { + sender.connect(); + } + sender.sendMessage(destination, message); + sender.keepalive(); + } catch (ChannelException x) { + sender.disconnect(); + throw x; + } finally { + returnSender(sender); + } + } + } + + @Override + public DataSender getNewDataSender() { + try { + ParallelNioSender sender = new ParallelNioSender(); + transferProperties(this,sender); + return sender; + } catch ( IOException x ) { + throw new RuntimeException(sm.getString("pooledParallelSender.unable.open"),x); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/nio/PooledParallelSenderMBean.java b/java/org/apache/catalina/tribes/transport/nio/PooledParallelSenderMBean.java new file mode 100644 index 0000000..cc78768 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/PooledParallelSenderMBean.java @@ -0,0 +1,68 @@ +/* + * 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.catalina.tribes.transport.nio; + +public interface PooledParallelSenderMBean { + + // Transport Attributes + int getRxBufSize(); + + int getTxBufSize(); + + int getUdpRxBufSize(); + + int getUdpTxBufSize(); + + boolean getDirectBuffer(); + + int getKeepAliveCount(); + + long getKeepAliveTime(); + + long getTimeout(); + + int getMaxRetryAttempts(); + + boolean getOoBInline(); + + boolean getSoKeepAlive(); + + boolean getSoLingerOn(); + + int getSoLingerTime(); + + boolean getSoReuseAddress(); + + int getSoTrafficClass(); + + boolean getTcpNoDelay(); + + boolean getThrowOnFailedAck(); + + // PooledSender Attributes + int getPoolSize(); + + long getMaxWait(); + + // Operation + boolean isConnected(); + + int getInPoolSize(); + + int getInUsePoolSize(); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/util/Arrays.java b/java/org/apache/catalina/tribes/util/Arrays.java new file mode 100644 index 0000000..9998f6e --- /dev/null +++ b/java/org/apache/catalina/tribes/util/Arrays.java @@ -0,0 +1,248 @@ +/* + * 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.catalina.tribes.util; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.group.AbsoluteOrder; +import org.apache.catalina.tribes.membership.Membership; + +public class Arrays { + protected static final StringManager sm = StringManager.getManager(Arrays.class); + + public static boolean contains(byte[] source, int srcoffset, byte[] key, int keyoffset, int length) { + if (srcoffset < 0 || srcoffset >= source.length) { + throw new ArrayIndexOutOfBoundsException(sm.getString("arrays.srcoffset.outOfBounds")); + } + if (keyoffset < 0 || keyoffset >= key.length) { + throw new ArrayIndexOutOfBoundsException(sm.getString("arrays.keyoffset.outOfBounds")); + } + if (length > (key.length - keyoffset)) { + throw new ArrayIndexOutOfBoundsException(sm.getString("arrays.length.outOfBounds")); + } + // we don't have enough data to validate it + if (length > (source.length - srcoffset)) { + return false; + } + boolean match = true; + int pos = keyoffset; + for (int i = srcoffset; match && i < length; i++) { + match = (source[i] == key[pos++]); + } + return match; + } + + public static String toString(byte[] data) { + return toString(data, 0, data != null ? data.length : 0); + } + + public static String toString(byte[] data, int offset, int length) { + return toString(data, offset, length, false); + } + + public static String toString(byte[] data, int offset, int length, boolean unsigned) { + StringBuilder buf = new StringBuilder("{"); + if (data != null && length > 0) { + int i = offset; + if (unsigned) { + buf.append(data[i++] & 0xff); + for (; i < length; i++) { + buf.append(", ").append(data[i] & 0xff); + } + } else { + buf.append(data[i++]); + for (; i < length; i++) { + buf.append(", ").append(data[i]); + } + } + } + buf.append('}'); + return buf.toString(); + } + + public static String toString(Object[] data) { + return toString(data, 0, data != null ? data.length : 0); + } + + public static String toString(Object[] data, int offset, int length) { + StringBuilder buf = new StringBuilder("{"); + if (data != null && length > 0) { + buf.append(data[offset++]); + for (int i = offset; i < length; i++) { + buf.append(", ").append(data[i]); + } + } + buf.append('}'); + return buf.toString(); + } + + public static String toNameString(Member[] data) { + return toNameString(data, 0, data != null ? data.length : 0); + } + + public static String toNameString(Member[] data, int offset, int length) { + StringBuilder buf = new StringBuilder("{"); + if (data != null && length > 0) { + buf.append(data[offset++].getName()); + for (int i = offset; i < length; i++) { + buf.append(", ").append(data[i].getName()); + } + } + buf.append('}'); + return buf.toString(); + } + + public static int add(int[] data) { + int result = 0; + for (int datum : data) { + result += datum; + } + return result; + } + + public static UniqueId getUniqudId(ChannelMessage msg) { + return new UniqueId(msg.getUniqueId()); + } + + public static UniqueId getUniqudId(byte[] data) { + return new UniqueId(data); + } + + public static boolean equals(byte[] o1, byte[] o2) { + return java.util.Arrays.equals(o1, o2); + } + + public static boolean equals(Object[] o1, Object[] o2) { + boolean result = o1.length == o2.length; + if (result) { + for (int i = 0; i < o1.length && result; i++) { + result = o1[i].equals(o2[i]); + } + } + return result; + } + + public static boolean sameMembers(Member[] m1, Member[] m2) { + AbsoluteOrder.absoluteOrder(m1); + AbsoluteOrder.absoluteOrder(m2); + return equals(m1, m2); + } + + public static Member[] merge(Member[] m1, Member[] m2) { + AbsoluteOrder.absoluteOrder(m1); + AbsoluteOrder.absoluteOrder(m2); + ArrayList list = new ArrayList<>(java.util.Arrays.asList(m1)); + for (Member member : m2) { + if (!list.contains(member)) { + list.add(member); + } + } + Member[] result = list.toArray(new Member[0]); + AbsoluteOrder.absoluteOrder(result); + return result; + } + + public static void fill(Membership mbrship, Member[] m) { + for (Member member : m) { + mbrship.addMember(member); + } + } + + public static Member[] diff(Membership complete, Membership local, Member ignore) { + List result = new ArrayList<>(); + Member[] comp = complete.getMembers(); + for (Member member : comp) { + if (ignore != null && ignore.equals(member)) { + continue; + } + if (local.getMember(member) == null) { + result.add(member); + } + } + return result.toArray(new Member[0]); + } + + public static Member[] remove(Member[] all, Member remove) { + return extract(all, new Member[] { remove }); + } + + public static Member[] extract(Member[] all, Member[] remove) { + List alist = java.util.Arrays.asList(all); + ArrayList list = new ArrayList<>(alist); + for (Member member : remove) { + list.remove(member); + } + return list.toArray(new Member[0]); + } + + public static int indexOf(Member member, Member[] members) { + int result = -1; + for (int i = 0; (result == -1) && (i < members.length); i++) { + if (member.equals(members[i])) { + result = i; + } + } + return result; + } + + public static int nextIndex(Member member, Member[] members) { + int idx = indexOf(member, members) + 1; + if (idx >= members.length) { + idx = ((members.length > 0) ? 0 : -1); + } + + return idx; + } + + public static int hashCode(byte a[]) { + if (a == null) { + return 0; + } + + int result = 1; + for (byte element : a) { + result = 31 * result + element; + } + return result; + } + + public static byte[] fromString(String value) { + if (value == null) { + return null; + } + if (!value.startsWith("{")) { + throw new RuntimeException(sm.getString("arrays.malformed.arrays")); + } + StringTokenizer t = new StringTokenizer(value, "{,}", false); + byte[] result = new byte[t.countTokens()]; + for (int i = 0; i < result.length; i++) { + result[i] = Byte.parseByte(t.nextToken()); + } + return result; + } + + + public static byte[] convert(String s) { + return s.getBytes(StandardCharsets.ISO_8859_1); + } +} diff --git a/java/org/apache/catalina/tribes/util/ExceptionUtils.java b/java/org/apache/catalina/tribes/util/ExceptionUtils.java new file mode 100644 index 0000000..c352e3c --- /dev/null +++ b/java/org/apache/catalina/tribes/util/ExceptionUtils.java @@ -0,0 +1,42 @@ +/* + * 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.catalina.tribes.util; + +/** + * Utilities for handling Throwables and Exceptions. + */ +public class ExceptionUtils { + + /** + * Checks whether the supplied Throwable is one that needs to be rethrown and swallows all others. + * + * @param t the Throwable to check + */ + public static void handleThrowable(Throwable t) { + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof StackOverflowError) { + // Swallow silently - it should be recoverable + return; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + // All other instances of Throwable will be silently swallowed + } +} diff --git a/java/org/apache/catalina/tribes/util/ExecutorFactory.java b/java/org/apache/catalina/tribes/util/ExecutorFactory.java new file mode 100644 index 0000000..2f2cdfd --- /dev/null +++ b/java/org/apache/catalina/tribes/util/ExecutorFactory.java @@ -0,0 +1,133 @@ +/* + * 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.catalina.tribes.util; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ExecutorFactory { + protected static final StringManager sm = StringManager.getManager(ExecutorFactory.class); + + public static ExecutorService newThreadPool(int minThreads, int maxThreads, long maxIdleTime, TimeUnit unit) { + TaskQueue taskqueue = new TaskQueue(); + ThreadPoolExecutor service = new TribesThreadPoolExecutor(minThreads, maxThreads, maxIdleTime, unit, taskqueue); + taskqueue.setParent(service); + return service; + } + + public static ExecutorService newThreadPool(int minThreads, int maxThreads, long maxIdleTime, TimeUnit unit, + ThreadFactory threadFactory) { + TaskQueue taskqueue = new TaskQueue(); + ThreadPoolExecutor service = new TribesThreadPoolExecutor(minThreads, maxThreads, maxIdleTime, unit, taskqueue, + threadFactory); + taskqueue.setParent(service); + return service; + } + + // ---------------------------------------------- TribesThreadPoolExecutor Inner Class + private static class TribesThreadPoolExecutor extends ThreadPoolExecutor { + TribesThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); + prestartAllCoreThreads(); + } + + TribesThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + prestartAllCoreThreads(); + } + + TribesThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, ThreadFactory threadFactory) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); + prestartAllCoreThreads(); + } + + TribesThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + prestartAllCoreThreads(); + } + + @Override + public void execute(Runnable command) { + try { + super.execute(command); + } catch (RejectedExecutionException rx) { + if (super.getQueue() instanceof TaskQueue) { + TaskQueue queue = (TaskQueue) super.getQueue(); + if (!queue.force(command)) { + throw new RejectedExecutionException(sm.getString("executorFactory.queue.full")); + } + } + } + } + } + + // ---------------------------------------------- TaskQueue Inner Class + private static class TaskQueue extends LinkedBlockingQueue { + private static final long serialVersionUID = 1L; + + transient ThreadPoolExecutor parent = null; + + TaskQueue() { + super(); + } + + public void setParent(ThreadPoolExecutor tp) { + parent = tp; + } + + public boolean force(Runnable o) { + if (parent != null && parent.isShutdown()) { + throw new RejectedExecutionException(sm.getString("executorFactory.not.running")); + } + // Forces the item onto the queue, to be used if the task is rejected + return super.offer(o); + } + + @Override + public boolean offer(Runnable o) { + // we can't do any checks + if (parent == null) { + return super.offer(o); + } + // we are maxed out on threads, simply queue the object + if (parent.getPoolSize() == parent.getMaximumPoolSize()) { + return super.offer(o); + } + // we have idle threads, just add it to the queue + // this is an approximation, so it could use some tuning + if (parent.getActiveCount() < (parent.getPoolSize())) { + return super.offer(o); + } + // if we have less threads than maximum force creation of a new thread + if (parent.getPoolSize() < parent.getMaximumPoolSize()) { + return false; + } + // if we reached here, we need to add it to the queue + return super.offer(o); + } + } +} diff --git a/java/org/apache/catalina/tribes/util/Jre14Compat.java b/java/org/apache/catalina/tribes/util/Jre14Compat.java new file mode 100644 index 0000000..ad155e6 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/Jre14Compat.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.tribes.util; + +import java.io.IOException; +import java.net.MulticastSocket; +import java.net.StandardSocketOptions; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class Jre14Compat extends JreCompat { + + private static final Log log = LogFactory.getLog(Jre14Compat.class); + private static final StringManager sm = StringManager.getManager(Jre14Compat.class); + + private static final boolean supported; + + static { + // Don't need any Java 19 specific classes (yet) so just test for one of + // the new ones for now. + Class c1 = null; + try { + c1 = Class.forName("java.io.Serial"); + } catch (ClassNotFoundException cnfe) { + // Must be pre-Java 16 + log.debug(sm.getString("jre14Compat.javaPre14"), cnfe); + } + + supported = (c1 != null); + } + + static boolean isSupported() { + return supported; + } + + + @Override + public void setSocketoptionIpMulticastLoop(MulticastSocket socket, boolean enabled) throws IOException { + /* + * Java < 14, a value of true means loopback is disabled. Java 14+ a value of true means loopback is enabled. + */ + socket.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, Boolean.valueOf(enabled)); + } + +} diff --git a/java/org/apache/catalina/tribes/util/JreCompat.java b/java/org/apache/catalina/tribes/util/JreCompat.java new file mode 100644 index 0000000..563675d --- /dev/null +++ b/java/org/apache/catalina/tribes/util/JreCompat.java @@ -0,0 +1,55 @@ +/* + * 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.catalina.tribes.util; + +import java.io.IOException; +import java.net.MulticastSocket; +import java.net.StandardSocketOptions; + +/** + * This is the base implementation class for JRE compatibility and provides an implementation based on Java 11. + * Sub-classes may extend this class and provide alternative implementations for later JRE versions + */ +public class JreCompat { + + private static final JreCompat instance; + + static { + // This is Tomcat 11.0.x with a minimum Java version of Java 11. + // Look for the highest supported JVM first + if (Jre14Compat.isSupported()) { + instance = new Jre14Compat(); + } else { + instance = new JreCompat(); + } + } + + + public static JreCompat getInstance() { + return instance; + } + + + // Java 11 implementations of Java 14 methods + + public void setSocketoptionIpMulticastLoop(MulticastSocket socket, boolean enabled) throws IOException { + /* + * Java < 14, a value of true means loopback is disabled. Java 14+ a value of true means loopback is enabled. + */ + socket.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, Boolean.valueOf(!enabled)); + } +} diff --git a/java/org/apache/catalina/tribes/util/LocalStrings.properties b/java/org/apache/catalina/tribes/util/LocalStrings.properties new file mode 100644 index 0000000..bbfb77a --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings.properties @@ -0,0 +1,27 @@ +# 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. + +arrays.keyoffset.outOfBounds=keyoffset is out of bounds. +arrays.length.outOfBounds=not enough data elements in the key, length is out of bounds. +arrays.malformed.arrays=byte arrays must be represented as {1,3,4,5,6} +arrays.srcoffset.outOfBounds=srcoffset is out of bounds. + +executorFactory.not.running=Executor not running, can't force a command into the queues +executorFactory.queue.full=Queue capacity is full. + +jre14Compat.javaPre14=Class not found so assuming code is running on a pre-Java 14 JVM + +uuidGenerator.createRandom=Creation of SecureRandom instance for UUID generation using [{0}] took [{1}] milliseconds. +uuidGenerator.unable.fit=Unable to fit [{0}] bytes into the array. length:[{1}] required length:[{2}] diff --git a/java/org/apache/catalina/tribes/util/LocalStrings_cs.properties b/java/org/apache/catalina/tribes/util/LocalStrings_cs.properties new file mode 100644 index 0000000..9a81c37 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings_cs.properties @@ -0,0 +1,18 @@ +# 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. + +arrays.keyoffset.outOfBounds=keyoffset je mimo rozsah. + +executorFactory.not.running=Executor neběží, nelze vnutit příkaz do front diff --git a/java/org/apache/catalina/tribes/util/LocalStrings_de.properties b/java/org/apache/catalina/tribes/util/LocalStrings_de.properties new file mode 100644 index 0000000..a13fdb9 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings_de.properties @@ -0,0 +1,18 @@ +# 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. + +arrays.keyoffset.outOfBounds=keyoffset ist Außerhalb der gültigen Grenzen + +executorFactory.not.running=Executor läuft nicht, kann Kommando nicht in der Queue ablegen diff --git a/java/org/apache/catalina/tribes/util/LocalStrings_es.properties b/java/org/apache/catalina/tribes/util/LocalStrings_es.properties new file mode 100644 index 0000000..cb86522 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings_es.properties @@ -0,0 +1,19 @@ +# 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. + +arrays.keyoffset.outOfBounds=keyoffset esta fura de los límites.\n +arrays.length.outOfBounds=no hay suficientes elementos de datos en la llave, la longitud es fuera de los límites + +executorFactory.not.running=Ejecutor no esta corriendo, no se puede forzar un comando dentro le as colas diff --git a/java/org/apache/catalina/tribes/util/LocalStrings_fr.properties b/java/org/apache/catalina/tribes/util/LocalStrings_fr.properties new file mode 100644 index 0000000..c44d5e2 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings_fr.properties @@ -0,0 +1,25 @@ +# 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. + +arrays.keyoffset.outOfBounds=Le décalage (offset) de la clé est en-dehors des limites. +arrays.length.outOfBounds=Pas assez de données dans la clé, la longueur dépasse les limites +arrays.malformed.arrays=les tableaux d'octets doivent être représentés tels que {1,3,4,5,6} +arrays.srcoffset.outOfBounds=srcoffset est hors limites + +executorFactory.not.running=L'Executor ne tourne pas, impossible de forcer une commande dans les files d'attente +executorFactory.queue.full=La file d'attente est complète + +uuidGenerator.createRandom=La création d''une instance de SecureRandom pour le génération des UUID en utilisant [{0}] a pris [{1}] millisecondes +uuidGenerator.unable.fit=Impossible de faire rentrer [{0}] octets dans le tableau, longueur : [{1}] longueur requise : [{2}] diff --git a/java/org/apache/catalina/tribes/util/LocalStrings_ja.properties b/java/org/apache/catalina/tribes/util/LocalStrings_ja.properties new file mode 100644 index 0000000..bbd5f18 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings_ja.properties @@ -0,0 +1,25 @@ +# 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. + +arrays.keyoffset.outOfBounds=keyoffset ãŒé ˜åŸŸå¤–を指ã—ã¦ã„ã¾ã™ã€‚ +arrays.length.outOfBounds=キーé…列ã®ãƒ‡ãƒ¼ã‚¿è¦ç´ ãŒä¸è¶³ã—ã¦ã„ã¾ã™ã€‚length ãŒå¢ƒç•Œã‚’超ãˆã¾ã—ãŸã€‚ +arrays.malformed.arrays=ãƒã‚¤ãƒˆé…列ã¯{1,3,4,5,6}ã®ã‚ˆã†ãªè¡¨ç¾ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +arrays.srcoffset.outOfBounds=srcoffsetãŒç¯„囲外ã§ã™ã€‚ + +executorFactory.not.running=エグゼキュータãŒå®Ÿè¡Œã•ã‚Œã¦ã„ãªã„ã®ã§ã€ã‚­ãƒ¥ãƒ¼ã«ã‚³ãƒžãƒ³ãƒ‰ã‚’強制ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +executorFactory.queue.full=キューã®å®¹é‡ãŒã„ã£ã±ã„ã§ã™ã€‚ + +uuidGenerator.createRandom=[{0}]を使用ã—ã¦UUID生æˆç”¨ã®SecureRandomインスタンスを作æˆã™ã‚‹ã¨ã€[{1}]ミリ秒ã‹ã‹ã‚Šã¾ã—ãŸã€‚ +uuidGenerator.unable.fit=[{0}] ãƒã‚¤ãƒˆã‚’é…列ã«åˆã‚ã›ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。ç¾åœ¨ã®ã‚µã‚¤ã‚ºã¯ [{1}] ã§å¿…è¦ãªã‚µã‚¤ã‚ºã¯ [{2}] ã§ã™ diff --git a/java/org/apache/catalina/tribes/util/LocalStrings_ko.properties b/java/org/apache/catalina/tribes/util/LocalStrings_ko.properties new file mode 100644 index 0000000..713fea3 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings_ko.properties @@ -0,0 +1,25 @@ +# 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. + +arrays.keyoffset.outOfBounds=keyoffsetì´ ë²”ìœ„ë¥¼ 초과합니다. +arrays.length.outOfBounds=키 ë‚´ì— ì¶©ë¶„í•œ ë°ì´í„° ì—˜ë¦¬ë¨¼íŠ¸ë“¤ì´ ì¡´ìž¬í•˜ì§€ 않습니다. 길ì´ê°€ 범위 ë°–ì— ìžˆìŠµë‹ˆë‹¤. +arrays.malformed.arrays=ë°”ì´íŠ¸ ë°°ì—´ì€ ë°˜ë“œì‹œ '{1,3,4,5,6}'ê³¼ ê°™ì´ í‘œí˜„ë˜ì–´ì•¼ 합니다. +arrays.srcoffset.outOfBounds=srcoffsetì´ ë²”ìœ„ 밖입니다. + +executorFactory.not.running=Executorê°€ 실행 ì¤‘ì´ ì•„ë‹™ë‹ˆë‹¤. ëª…ë ¹ì„ ê°•ì œë¡œ íì— ë„£ì„ ìˆ˜ 없습니다. +executorFactory.queue.full=íì˜ ìš©ëŸ‰ì´ ê½‰ 찼습니다. + +uuidGenerator.createRandom=[{0}]ì„(를) 사용하여, UUID ìƒì„±ì„ 위한 SecureRandom ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•˜ëŠ” ë°ì—, [{1}] 밀리초가 소요ë습니다. +uuidGenerator.unable.fit=[{0}] ë°”ì´íŠ¸ë¥¼ 해당 ë°°ì—´ì— ë§žì¶œ 수 없습니다. 길ì´:[{1}], 요구ë˜ëŠ” 길ì´:[{2}] diff --git a/java/org/apache/catalina/tribes/util/LocalStrings_pt_BR.properties b/java/org/apache/catalina/tribes/util/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..db4ad6e --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +executorFactory.not.running=Executor não está em execução, não é possível formar um comando nas filas diff --git a/java/org/apache/catalina/tribes/util/LocalStrings_zh_CN.properties b/java/org/apache/catalina/tribes/util/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..cd2af0f --- /dev/null +++ b/java/org/apache/catalina/tribes/util/LocalStrings_zh_CN.properties @@ -0,0 +1,25 @@ +# 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. + +arrays.keyoffset.outOfBounds=keyçš„å移.超出了界é™ã€‚ +arrays.length.outOfBounds=当å‰key下没有足够的元素,长度越界 +arrays.malformed.arrays=字节数组必须表示为{1,3,4,5,6} +arrays.srcoffset.outOfBounds=srcoffset超出界é™ã€‚ + +executorFactory.not.running=执行器没有è¿è¡Œï¼Œæ— æ³•å¼ºåˆ¶æŠŠå‘½ä»¤é€å…¥é˜Ÿåˆ— +executorFactory.queue.full=队列已满 + +uuidGenerator.createRandom=使用[{0}]创建用于UUID生æˆçš„SecureRandom实例花费了[{1}]毫秒。 +uuidGenerator.unable.fit=无法将[{0}]字节放入数组。长度:[{1}]所需长度:[{2}] diff --git a/java/org/apache/catalina/tribes/util/Logs.java b/java/org/apache/catalina/tribes/util/Logs.java new file mode 100644 index 0000000..1c1933b --- /dev/null +++ b/java/org/apache/catalina/tribes/util/Logs.java @@ -0,0 +1,27 @@ +/* + * 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.catalina.tribes.util; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Simple class that holds references to global loggers + */ +public class Logs { + public static final Log MESSAGES = LogFactory.getLog("org.apache.catalina.tribes.MESSAGES"); +} diff --git a/java/org/apache/catalina/tribes/util/StringManager.java b/java/org/apache/catalina/tribes/util/StringManager.java new file mode 100644 index 0000000..c309626 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/StringManager.java @@ -0,0 +1,262 @@ +/* + * 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.catalina.tribes.util; + +import java.text.MessageFormat; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + + +/** + * An internationalization / localization helper class which reduces the bother of handling ResourceBundles and takes + * care of the common cases of message formatting which otherwise require the creation of Object arrays and such. + *

    + * The StringManager operates on a package basis. One StringManager per package can be created and accessed via the + * getManager method call. + *

    + * The StringManager will look for a ResourceBundle named by the package name given plus the suffix of "LocalStrings". + * In practice, this means that the localized information will be contained in a LocalStrings.properties file located in + * the package directory of the classpath. + *

    + * Please see the documentation for java.util.ResourceBundle for more information. + * + * @author James Duncan Davidson [duncan@eng.sun.com] + * @author James Todd [gonzo@eng.sun.com] + * @author Mel Martinez [mmartinez@g1440.com] + * + * @see java.util.ResourceBundle + */ +public class StringManager { + + private static int LOCALE_CACHE_SIZE = 10; + + /** + * The ResourceBundle for this StringManager. + */ + private final ResourceBundle bundle; + private final Locale locale; + + + /** + * Creates a new StringManager for a given package. This is a private method and all access to it is arbitrated by + * the static getManager method call so that only one StringManager per package will be created. + * + * @param packageName Name of package to create StringManager for. + */ + private StringManager(String packageName, Locale locale) { + String bundleName = packageName + ".LocalStrings"; + ResourceBundle bnd = null; + try { + bnd = ResourceBundle.getBundle(bundleName, locale); + } catch (MissingResourceException ex) { + // Try from the current loader (that's the case for trusted apps) + // Should only be required if using a TC5 style classloader structure + // where common != shared != server + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl != null) { + try { + bnd = ResourceBundle.getBundle(bundleName, locale, cl); + } catch (MissingResourceException ex2) { + // Ignore + } + } + } + bundle = bnd; + // Get the actual locale, which may be different from the requested one + if (bundle != null) { + Locale bundleLocale = bundle.getLocale(); + if (bundleLocale.equals(Locale.ROOT)) { + this.locale = Locale.ENGLISH; + } else { + this.locale = bundleLocale; + } + } else { + this.locale = null; + } + } + + + /** + * Get a string from the underlying resource bundle or return null if the String is not found. + * + * @param key to desired resource String + * + * @return resource String matching key from underlying bundle or null if not found. + * + * @throws IllegalArgumentException if key is null + */ + public String getString(String key) { + if (key == null) { + String msg = "key may not have a null value"; + throw new IllegalArgumentException(msg); + } + + String str = null; + + try { + // Avoid NPE if bundle is null and treat it like an MRE + if (bundle != null) { + str = bundle.getString(key); + } + } catch (MissingResourceException mre) { + // bad: shouldn't mask an exception the following way: + // str = "[cannot find message associated with key '" + key + + // "' due to " + mre + "]"; + // because it hides the fact that the String was missing + // from the calling code. + // good: could just throw the exception (or wrap it in another) + // but that would probably cause much havoc on existing + // code. + // better: consistent with container pattern to + // simply return null. Calling code can then do + // a null check. + str = null; + } + + return str; + } + + + /** + * Get a string from the underlying resource bundle and format it with the given set of arguments. + * + * @param key The key for the required message + * @param args The values to insert into the message + * + * @return The requested string formatted with the provided arguments + */ + public String getString(final String key, final Object... args) { + String value = getString(key); + if (value == null) { + value = key; + } + + MessageFormat mf = new MessageFormat(value); + mf.setLocale(locale); + return mf.format(args, new StringBuffer(), null).toString(); + } + + + /** + * Identify the Locale this StringManager is associated with + * + * @return The Locale associated with this instance + */ + public Locale getLocale() { + return locale; + } + + + // -------------------------------------------------------------- + // STATIC SUPPORT METHODS + // -------------------------------------------------------------- + + private static final Map> managers = new HashMap<>(); + + + /** + * The StringManager will be returned for the package in which the class is located. If a manager for that package + * already exists, it will be reused, else a new StringManager will be created and returned. + * + * @param clazz The class for which to retrieve the StringManager + * + * @return The StringManager for the given class. + */ + public static final StringManager getManager(Class clazz) { + return getManager(clazz.getPackage().getName()); + } + + + /** + * If a manager for a package already exists, it will be reused, else a new StringManager will be created and + * returned. + * + * @param packageName The package name + * + * @return The StringManager for the given package. + */ + public static final StringManager getManager(String packageName) { + return getManager(packageName, Locale.getDefault()); + } + + + /** + * If a manager for a package/Locale combination already exists, it will be reused, else a new StringManager will be + * created and returned. + * + * @param packageName The package name + * @param locale The Locale + * + * @return The StringManager for a particular package and Locale + */ + public static final synchronized StringManager getManager(String packageName, Locale locale) { + + Map map = managers.get(packageName); + if (map == null) { + /* + * Don't want the HashMap size to exceed LOCALE_CACHE_SIZE. Expansion occurs when size() exceeds capacity. + * Therefore keep size at or below capacity. removeEldestEntry() executes after insertion therefore the test + * for removal needs to use one less than the maximum desired size. Note this is an LRU cache. + */ + map = new LinkedHashMap<>(LOCALE_CACHE_SIZE, 0.75f, true) { + private static final long serialVersionUID = 1L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > (LOCALE_CACHE_SIZE - 1)) { + return true; + } + return false; + } + }; + managers.put(packageName, map); + } + + StringManager mgr = map.get(locale); + if (mgr == null) { + mgr = new StringManager(packageName, locale); + map.put(locale, mgr); + } + return mgr; + } + + + /** + * Retrieve the StringManager for a list of Locales. The first StringManager found will be returned. + * + * @param packageName The package for which the StringManager is required + * @param requestedLocales the list of Locales + * + * @return the found StringManager or the default StringManager + */ + public static StringManager getManager(String packageName, Enumeration requestedLocales) { + while (requestedLocales.hasMoreElements()) { + Locale locale = requestedLocales.nextElement(); + StringManager result = getManager(packageName, locale); + if (result.getLocale().equals(locale)) { + return result; + } + } + // Return the default + return getManager(packageName); + } +} diff --git a/java/org/apache/catalina/tribes/util/TcclThreadFactory.java b/java/org/apache/catalina/tribes/util/TcclThreadFactory.java new file mode 100644 index 0000000..2a493ad --- /dev/null +++ b/java/org/apache/catalina/tribes/util/TcclThreadFactory.java @@ -0,0 +1,66 @@ +/* + * 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.catalina.tribes.util; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * ThreadFactory implementation that creates threads with the thread context class loader set to the class loader that + * loaded this factory. It is intended to be used when tasks may be passed to executors when the web application class + * loader is set as the thread context class loader, such as in async session replication. + */ +public class TcclThreadFactory implements ThreadFactory { + + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); + + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public TcclThreadFactory() { + this("pool-" + poolNumber.getAndIncrement() + "-thread-"); + } + + public TcclThreadFactory(String namePrefix) { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + this.namePrefix = namePrefix; + } + + @Override + public Thread newThread(Runnable r) { + final Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement()); + + if (IS_SECURITY_ENABLED) { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + t.setContextClassLoader(this.getClass().getClassLoader()); + return null; + } + }); + } else { + t.setContextClassLoader(this.getClass().getClassLoader()); + } + t.setDaemon(true); + return t; + } +} diff --git a/java/org/apache/catalina/tribes/util/UUIDGenerator.java b/java/org/apache/catalina/tribes/util/UUIDGenerator.java new file mode 100644 index 0000000..0358fbc --- /dev/null +++ b/java/org/apache/catalina/tribes/util/UUIDGenerator.java @@ -0,0 +1,94 @@ +/* + * 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.catalina.tribes.util; + +import java.security.SecureRandom; +import java.util.Random; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Smple generation of a UUID. + */ +public class UUIDGenerator { + private static final Log log = LogFactory.getLog(UUIDGenerator.class); + protected static final StringManager sm = StringManager.getManager("org.apache.catalina.tribes.util"); + + public static final int UUID_LENGTH = 16; + public static final int UUID_VERSION = 4; + public static final int BYTES_PER_INT = 4; + public static final int BITS_PER_BYTE = 8; + + protected static final SecureRandom secrand; + protected static final Random rand = new Random(); + + static { + long start = System.currentTimeMillis(); + secrand = new SecureRandom(); + // seed the generator + secrand.nextInt(); + long time = System.currentTimeMillis() - start; + if (time > 100) { + log.info(sm.getString("uuidGenerator.createRandom", secrand.getAlgorithm(), Long.valueOf(time))); + } + } + + public static byte[] randomUUID(boolean secure) { + byte[] result = new byte[UUID_LENGTH]; + return randomUUID(secure, result, 0); + } + + public static byte[] randomUUID(boolean secure, byte[] into, int offset) { + if ((offset + UUID_LENGTH) > into.length) { + throw new ArrayIndexOutOfBoundsException( + sm.getString("uuidGenerator.unable.fit", Integer.toString(UUID_LENGTH), + Integer.toString(into.length), Integer.toString(offset + UUID_LENGTH))); + } + Random r = (secure && (secrand != null)) ? secrand : rand; + nextBytes(into, offset, UUID_LENGTH, r); + into[6 + offset] &= 0x0F; + into[6 + offset] |= (UUID_VERSION << 4); + into[8 + offset] &= 0x3F; // 0011 1111 + into[8 + offset] |= 0x80; // 1000 0000 + return into; + } + + /** + * Same as java.util.Random.nextBytes except this one we don't have to allocate a new byte array + * + * @param into byte[] + * @param offset int + * @param length int + * @param r Random + */ + public static void nextBytes(byte[] into, int offset, int length, Random r) { + int numRequested = length; + int numGot = 0, rnd = 0; + while (true) { + for (int i = 0; i < BYTES_PER_INT; i++) { + if (numGot == numRequested) { + return; + } + rnd = (i == 0 ? r.nextInt() : rnd >> BITS_PER_BYTE); + into[offset + numGot] = (byte) rnd; + numGot++; + } + } + } + +} diff --git a/java/org/apache/catalina/users/AbstractGroup.java b/java/org/apache/catalina/users/AbstractGroup.java new file mode 100644 index 0000000..22f25f3 --- /dev/null +++ b/java/org/apache/catalina/users/AbstractGroup.java @@ -0,0 +1,168 @@ +/* + * 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.catalina.users; + + +import java.util.Iterator; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.catalina.UserDatabase; + + +/** + *

    Convenience base class for {@link Group} implementations.

    + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public abstract class AbstractGroup implements Group { + + + // ----------------------------------------------------- Instance Variables + + + /** + * The description of this group. + */ + protected String description = null; + + + /** + * The group name of this group. + */ + protected String groupname = null; + + + // ------------------------------------------------------------- Properties + + + /** + * Return the description of this group. + */ + @Override + public String getDescription() { + return this.description; + } + + + /** + * Set the description of this group. + * + * @param description The new description + */ + @Override + public void setDescription(String description) { + this.description = description; + } + + + /** + * Return the group name of this group, which must be unique + * within the scope of a {@link UserDatabase}. + */ + @Override + public String getGroupname() { + return this.groupname; + } + + + /** + * Set the group name of this group, which must be unique + * within the scope of a {@link UserDatabase}. + * + * @param groupname The new group name + */ + @Override + public void setGroupname(String groupname) { + this.groupname = groupname; + } + + + /** + * Return the set of {@link Role}s assigned specifically to this group. + */ + @Override + public abstract Iterator getRoles(); + + + /** + * Return the {@link UserDatabase} within which this Group is defined. + */ + @Override + public abstract UserDatabase getUserDatabase(); + + + /** + * Return an Iterator over the set of {@link org.apache.catalina.User}s that + * are members of this group. + */ + @Override + public abstract Iterator getUsers(); + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new {@link Role} to those assigned specifically to this group. + * + * @param role The new role + */ + @Override + public abstract void addRole(Role role); + + + /** + * Is this group specifically assigned the specified {@link Role}? + * + * @param role The role to check + */ + @Override + public abstract boolean isInRole(Role role); + + + /** + * Remove a {@link Role} from those assigned to this group. + * + * @param role The old role + */ + @Override + public abstract void removeRole(Role role); + + + /** + * Remove all {@link Role}s from those assigned to this group. + */ + @Override + public abstract void removeRoles(); + + + // ------------------------------------------------------ Principal Methods + + + /** + * Make the principal name the same as the group name. + */ + @Override + public String getName() { + return getGroupname(); + } + + +} diff --git a/java/org/apache/catalina/users/AbstractRole.java b/java/org/apache/catalina/users/AbstractRole.java new file mode 100644 index 0000000..1751ffc --- /dev/null +++ b/java/org/apache/catalina/users/AbstractRole.java @@ -0,0 +1,115 @@ +/* + * 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.catalina.users; + + +import org.apache.catalina.Role; +import org.apache.catalina.UserDatabase; + + +/** + *

    Convenience base class for {@link Role} implementations.

    + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public abstract class AbstractRole implements Role { + + + // ----------------------------------------------------- Instance Variables + + + /** + * The description of this Role. + */ + protected String description = null; + + + /** + * The role name of this Role. + */ + protected String rolename = null; + + + // ------------------------------------------------------------- Properties + + + /** + * Return the description of this role. + */ + @Override + public String getDescription() { + return this.description; + } + + + /** + * Set the description of this role. + * + * @param description The new description + */ + @Override + public void setDescription(String description) { + this.description = description; + } + + + /** + * Return the role name of this role, which must be unique + * within the scope of a {@link UserDatabase}. + */ + @Override + public String getRolename() { + return this.rolename; + } + + + /** + * Set the role name of this role, which must be unique + * within the scope of a {@link UserDatabase}. + * + * @param rolename The new role name + */ + @Override + public void setRolename(String rolename) { + this.rolename = rolename; + } + + + /** + * Return the {@link UserDatabase} within which this Role is defined. + */ + @Override + public abstract UserDatabase getUserDatabase(); + + + // --------------------------------------------------------- Public Methods + + + // ------------------------------------------------------ Principal Methods + + + /** + * Make the principal name the same as the role name. + */ + @Override + public String getName() { + return getRolename(); + } + + +} diff --git a/java/org/apache/catalina/users/AbstractUser.java b/java/org/apache/catalina/users/AbstractUser.java new file mode 100644 index 0000000..9c4034a --- /dev/null +++ b/java/org/apache/catalina/users/AbstractUser.java @@ -0,0 +1,225 @@ +/* + * 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.catalina.users; + + +import java.util.Iterator; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; + + +/** + *

    Convenience base class for {@link User} implementations.

    + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public abstract class AbstractUser implements User { + + + // ----------------------------------------------------- Instance Variables + + + /** + * The full name of this user. + */ + protected String fullName = null; + + + /** + * The logon password of this user. + */ + protected String password = null; + + + /** + * The logon username of this user. + */ + protected String username = null; + + + // ------------------------------------------------------------- Properties + + + /** + * Return the full name of this user. + */ + @Override + public String getFullName() { + return this.fullName; + } + + + /** + * Set the full name of this user. + * + * @param fullName The new full name + */ + @Override + public void setFullName(String fullName) { + this.fullName = fullName; + } + + + /** + * Return the set of {@link Group}s to which this user belongs. + */ + @Override + public abstract Iterator getGroups(); + + + /** + * Return the logon password of this user, optionally prefixed with the + * identifier of an encoding scheme surrounded by curly braces, such as + * {md5}xxxxx. + */ + @Override + public String getPassword() { + return this.password; + } + + + /** + * Set the logon password of this user, optionally prefixed with the + * identifier of an encoding scheme surrounded by curly braces, such as + * {md5}xxxxx. + * + * @param password The new logon password + */ + @Override + public void setPassword(String password) { + this.password = password; + } + + + /** + * Return the set of {@link Role}s assigned specifically to this user. + */ + @Override + public abstract Iterator getRoles(); + + + /** + * Return the logon username of this user, which must be unique + * within the scope of a {@link org.apache.catalina.UserDatabase}. + */ + @Override + public String getUsername() { + return this.username; + } + + + /** + * Set the logon username of this user, which must be unique within + * the scope of a {@link org.apache.catalina.UserDatabase}. + * + * @param username The new logon username + */ + @Override + public void setUsername(String username) { + this.username = username; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new {@link Group} to those this user belongs to. + * + * @param group The new group + */ + @Override + public abstract void addGroup(Group group); + + + /** + * Add a new {@link Role} to those assigned specifically to this user. + * + * @param role The new role + */ + @Override + public abstract void addRole(Role role); + + + /** + * Is this user in the specified {@link Group}? + * + * @param group The group to check + */ + @Override + public abstract boolean isInGroup(Group group); + + + /** + * Is this user specifically assigned the specified {@link Role}? This + * method does NOT check for roles inherited based on + * {@link Group} membership. + * + * @param role The role to check + */ + @Override + public abstract boolean isInRole(Role role); + + + /** + * Remove a {@link Group} from those this user belongs to. + * + * @param group The old group + */ + @Override + public abstract void removeGroup(Group group); + + + /** + * Remove all {@link Group}s from those this user belongs to. + */ + @Override + public abstract void removeGroups(); + + + /** + * Remove a {@link Role} from those assigned to this user. + * + * @param role The old role + */ + @Override + public abstract void removeRole(Role role); + + + /** + * Remove all {@link Role}s from those assigned to this user. + */ + @Override + public abstract void removeRoles(); + + + // ------------------------------------------------------ Principal Methods + + + /** + * Make the principal name the same as the group name. + */ + @Override + public String getName() { + return getUsername(); + } + + +} diff --git a/java/org/apache/catalina/users/Constants.java b/java/org/apache/catalina/users/Constants.java new file mode 100644 index 0000000..c79f517 --- /dev/null +++ b/java/org/apache/catalina/users/Constants.java @@ -0,0 +1,31 @@ +/* + * 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.catalina.users; + + +/** + * Manifest constants for this Java package. + * + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public final class Constants { + + public static final String Package = "org.apache.catalina.users"; + +} diff --git a/java/org/apache/catalina/users/DataSourceUserDatabase.java b/java/org/apache/catalina/users/DataSourceUserDatabase.java new file mode 100644 index 0000000..8ad9aa7 --- /dev/null +++ b/java/org/apache/catalina/users/DataSourceUserDatabase.java @@ -0,0 +1,1639 @@ +/* + * 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.catalina.users; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.sql.DataSource; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * UserDatabase backed by a data source. + */ +public class DataSourceUserDatabase extends SparseUserDatabase { + + private static final Log log = LogFactory.getLog(DataSourceUserDatabase.class); + private static final StringManager sm = StringManager.getManager(DataSourceUserDatabase.class); + + public DataSourceUserDatabase(DataSource dataSource, String id) { + this.dataSource = dataSource; + this.id = id; + } + + + /** + * DataSource to use. + */ + protected final DataSource dataSource; + + + /** + * The unique global identifier of this user database. + */ + protected final String id; + + protected final ConcurrentHashMap createdUsers = new ConcurrentHashMap<>(); + protected final ConcurrentHashMap modifiedUsers = new ConcurrentHashMap<>(); + protected final ConcurrentHashMap removedUsers = new ConcurrentHashMap<>(); + + protected final ConcurrentHashMap createdGroups = new ConcurrentHashMap<>(); + protected final ConcurrentHashMap modifiedGroups = new ConcurrentHashMap<>(); + protected final ConcurrentHashMap removedGroups = new ConcurrentHashMap<>(); + + protected final ConcurrentHashMap createdRoles = new ConcurrentHashMap<>(); + protected final ConcurrentHashMap modifiedRoles = new ConcurrentHashMap<>(); + protected final ConcurrentHashMap removedRoles = new ConcurrentHashMap<>(); + + + // ----------------------------------------------------- Instance Variables + + + /** + * The generated string for the all users PreparedStatement + */ + private String preparedAllUsers = null; + + + /** + * The generated string for the all groups PreparedStatement + */ + private String preparedAllGroups = null; + + + /** + * The generated string for the all roles PreparedStatement + */ + private String preparedAllRoles = null; + + + /** + * The generated string for the group PreparedStatement + */ + private String preparedGroup = null; + + + /** + * The generated string for the role PreparedStatement + */ + private String preparedRole = null; + + + /** + * The generated string for the roles PreparedStatement + */ + private String preparedUserRoles = null; + + + /** + * The generated string for the user PreparedStatement + */ + private String preparedUser = null; + + + /** + * The generated string for the groups PreparedStatement + */ + private String preparedUserGroups = null; + + + /** + * The generated string for the groups PreparedStatement + */ + private String preparedGroupRoles = null; + + + /** + * The name of the JNDI JDBC DataSource + */ + protected String dataSourceName = null; + + + /** + * The column in the user role table that names a role + */ + protected String roleNameCol = null; + + + /** + * The column in the role and group tables for the description + */ + protected String roleAndGroupDescriptionCol = null; + + + /** + * The column in the user group table that names a group + */ + protected String groupNameCol = null; + + + /** + * The column in the user table that holds the user's credentials + */ + protected String userCredCol = null; + + + /** + * The column in the user table that holds the user's full name + */ + protected String userFullNameCol = null; + + + /** + * The column in the user table that holds the user's name + */ + protected String userNameCol = null; + + + /** + * The table that holds the relation between users and roles + */ + protected String userRoleTable = null; + + + /** + * The table that holds the relation between users and groups + */ + protected String userGroupTable = null; + + + /** + * The table that holds the relation between groups and roles + */ + protected String groupRoleTable = null; + + + /** + * The table that holds user data. + */ + protected String userTable = null; + + + /** + * The table that holds user data. + */ + protected String groupTable = null; + + + /** + * The table that holds user data. + */ + protected String roleTable = null; + + + /** + * Last connection attempt. + */ + private volatile boolean connectionSuccess = true; + + + /** + * A flag, indicating if the user database is read only. + */ + protected boolean readonly = true; + + // The write lock on the database is assumed to include the write locks + // for groups, users and roles. + private final ReentrantReadWriteLock dbLock = new ReentrantReadWriteLock(); + private final Lock dbReadLock = dbLock.readLock(); + private final Lock dbWriteLock = dbLock.writeLock(); + + private final ReentrantReadWriteLock groupsLock = new ReentrantReadWriteLock(); + private final Lock groupsReadLock = groupsLock.readLock(); + private final Lock groupsWriteLock = groupsLock.writeLock(); + + private final ReentrantReadWriteLock usersLock = new ReentrantReadWriteLock(); + private final Lock usersReadLock = usersLock.readLock(); + private final Lock usersWriteLock = usersLock.writeLock(); + + private final ReentrantReadWriteLock rolesLock = new ReentrantReadWriteLock(); + private final Lock rolesReadLock = rolesLock.readLock(); + private final Lock rolesWriteLock = rolesLock.writeLock(); + + + // ------------------------------------------------------------- Properties + + /** + * @return the name of the JNDI JDBC DataSource. + */ + public String getDataSourceName() { + return dataSourceName; + } + + /** + * Set the name of the JNDI JDBC DataSource. + * + * @param dataSourceName the name of the JNDI JDBC DataSource + */ + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + /** + * @return the column in the user role table that names a role. + */ + public String getRoleNameCol() { + return roleNameCol; + } + + /** + * Set the column in the user role table that names a role. + * + * @param roleNameCol The column name + */ + public void setRoleNameCol( String roleNameCol ) { + this.roleNameCol = roleNameCol; + } + + /** + * @return the column in the user table that holds the user's credentials. + */ + public String getUserCredCol() { + return userCredCol; + } + + /** + * Set the column in the user table that holds the user's credentials. + * + * @param userCredCol The column name + */ + public void setUserCredCol( String userCredCol ) { + this.userCredCol = userCredCol; + } + + /** + * @return the column in the user table that holds the user's name. + */ + public String getUserNameCol() { + return userNameCol; + } + + /** + * Set the column in the user table that holds the user's name. + * + * @param userNameCol The column name + */ + public void setUserNameCol( String userNameCol ) { + this.userNameCol = userNameCol; + } + + /** + * @return the table that holds the relation between user's and roles. + */ + public String getUserRoleTable() { + return userRoleTable; + } + + /** + * Set the table that holds the relation between user's and roles. + * + * @param userRoleTable The table name + */ + public void setUserRoleTable( String userRoleTable ) { + this.userRoleTable = userRoleTable; + } + + /** + * @return the table that holds user data.. + */ + public String getUserTable() { + return userTable; + } + + /** + * Set the table that holds user data. + * + * @param userTable The table name + */ + public void setUserTable( String userTable ) { + this.userTable = userTable; + } + + + /** + * @return the roleAndGroupDescriptionCol + */ + public String getRoleAndGroupDescriptionCol() { + return this.roleAndGroupDescriptionCol; + } + + /** + * @param roleAndGroupDescriptionCol the roleAndGroupDescriptionCol to set + */ + public void setRoleAndGroupDescriptionCol(String roleAndGroupDescriptionCol) { + this.roleAndGroupDescriptionCol = roleAndGroupDescriptionCol; + } + + /** + * @return the groupNameCol + */ + public String getGroupNameCol() { + return this.groupNameCol; + } + + /** + * @param groupNameCol the groupNameCol to set + */ + public void setGroupNameCol(String groupNameCol) { + this.groupNameCol = groupNameCol; + } + + /** + * @return the userFullNameCol + */ + public String getUserFullNameCol() { + return this.userFullNameCol; + } + + /** + * @param userFullNameCol the userFullNameCol to set + */ + public void setUserFullNameCol(String userFullNameCol) { + this.userFullNameCol = userFullNameCol; + } + + /** + * @return the userGroupTable + */ + public String getUserGroupTable() { + return this.userGroupTable; + } + + /** + * @param userGroupTable the userGroupTable to set + */ + public void setUserGroupTable(String userGroupTable) { + this.userGroupTable = userGroupTable; + } + + /** + * @return the groupRoleTable + */ + public String getGroupRoleTable() { + return this.groupRoleTable; + } + + /** + * @param groupRoleTable the groupRoleTable to set + */ + public void setGroupRoleTable(String groupRoleTable) { + this.groupRoleTable = groupRoleTable; + } + + /** + * @return the groupTable + */ + public String getGroupTable() { + return this.groupTable; + } + + /** + * @param groupTable the groupTable to set + */ + public void setGroupTable(String groupTable) { + this.groupTable = groupTable; + } + + /** + * @return the roleTable + */ + public String getRoleTable() { + return this.roleTable; + } + + /** + * @param roleTable the roleTable to set + */ + public void setRoleTable(String roleTable) { + this.roleTable = roleTable; + } + + /** + * @return the readonly + */ + public boolean getReadonly() { + return this.readonly; + } + + /** + * @param readonly the readonly to set + */ + public void setReadonly(boolean readonly) { + this.readonly = readonly; + } + + @Override + public String getId() { + return id; + } + + @Override + public Iterator getGroups() { + dbReadLock.lock(); + try { + groupsReadLock.lock(); + try { + HashMap groups = new HashMap<>(); + groups.putAll(createdGroups); + groups.putAll(modifiedGroups); + + try (Connection dbConnection = openConnection()) { + if (dbConnection != null && preparedAllGroups != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllGroups)) { + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + String groupName = rs.getString(1); + if (groupName != null) { + if (!groups.containsKey(groupName) && !removedGroups.containsKey(groupName)) { + Group group = findGroupInternal(dbConnection, groupName); + if (group != null) { + groups.put(groupName, group); + } + } + } + } + } + } + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + return groups.values().iterator(); + } finally { + groupsReadLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public Iterator getRoles() { + dbReadLock.lock(); + try { + rolesReadLock.lock(); + try { + HashMap roles = new HashMap<>(); + roles.putAll(createdRoles); + roles.putAll(modifiedRoles); + + try (Connection dbConnection = openConnection()) { + if (dbConnection != null && preparedAllRoles != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllRoles)) { + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + String roleName = rs.getString(1); + if (roleName != null) { + if (!roles.containsKey(roleName) && !removedRoles.containsKey(roleName)) { + Role role = findRoleInternal(dbConnection, roleName); + if (role != null) { + roles.put(roleName, role); + } + } + } + } + } + } + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + return roles.values().iterator(); + } finally { + rolesReadLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public Iterator getUsers() { + dbReadLock.lock(); + try { + usersReadLock.lock(); + try { + HashMap users = new HashMap<>(); + users.putAll(createdUsers); + users.putAll(modifiedUsers); + + Connection dbConnection = openConnection(); + if (dbConnection != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllUsers)) { + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + String userName = rs.getString(1); + if (userName != null) { + if (!users.containsKey(userName) && !removedUsers.containsKey(userName)) { + User user = findUserInternal(dbConnection, userName); + if (user != null) { + users.put(userName, user); + } + } + } + } + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } finally { + closeConnection(dbConnection); + } + } + return users.values().iterator(); + } finally { + usersReadLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public void close() throws Exception { + } + + @Override + public Group createGroup(String groupname, String description) { + dbReadLock.lock(); + try { + groupsWriteLock.lock(); + try { + Group group = new GenericGroup<>(this, groupname, description, null); + createdGroups.put(groupname, group); + modifiedGroups.remove(groupname); + return group; + } finally { + groupsWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public Role createRole(String rolename, String description) { + dbReadLock.lock(); + try { + rolesWriteLock.lock(); + try { + Role role = new GenericRole<>(this, rolename, description); + createdRoles.put(rolename, role); + modifiedRoles.remove(rolename); + return role; + } finally { + rolesWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public User createUser(String username, String password, String fullName) { + dbReadLock.lock(); + try { + usersWriteLock.lock(); + try { + User user = new GenericUser<>(this, username, password, fullName, null, null); + createdUsers.put(username, user); + modifiedUsers.remove(username); + return user; + } finally { + usersWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public Group findGroup(String groupname) { + dbReadLock.lock(); + try { + groupsReadLock.lock(); + try { + // Check local changes first + Group group = createdGroups.get(groupname); + if (group != null) { + return group; + } + group = modifiedGroups.get(groupname); + if (group != null) { + return group; + } + group = removedGroups.get(groupname); + if (group != null) { + return null; + } + + if (isGroupStoreDefined()) { + Connection dbConnection = openConnection(); + if (dbConnection == null) { + return null; + } + try { + return findGroupInternal(dbConnection, groupname); + } finally { + closeConnection(dbConnection); + } + } else { + return null; + } + } finally { + groupsReadLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + public Group findGroupInternal(Connection dbConnection, String groupName) { + Group group = null; + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedGroup)) { + stmt.setString(1, groupName); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + if (rs.getString(1) != null) { + String description = (roleAndGroupDescriptionCol != null) ? rs.getString(2) : null; + ArrayList groupRoles = new ArrayList<>(); + if (groupName != null) { + groupName = groupName.trim(); + try (PreparedStatement stmt2 = dbConnection.prepareStatement(preparedGroupRoles)) { + stmt2.setString(1, groupName); + try (ResultSet rs2 = stmt2.executeQuery()) { + while (rs2.next()) { + String roleName = rs2.getString(1); + if (roleName != null) { + Role groupRole = findRoleInternal(dbConnection, roleName); + if (groupRole != null) { + groupRoles.add(groupRole); + } + } + } + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + group = new GenericGroup<>(this, groupName, description, groupRoles); + } + } + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + return group; + } + + @Override + public Role findRole(String rolename) { + dbReadLock.lock(); + try { + rolesReadLock.lock(); + try { + // Check local changes first + Role role = createdRoles.get(rolename); + if (role != null) { + return role; + } + role = modifiedRoles.get(rolename); + if (role != null) { + return role; + } + role = removedRoles.get(rolename); + if (role != null) { + return null; + } + + if (userRoleTable != null && roleNameCol != null) { + Connection dbConnection = openConnection(); + if (dbConnection == null) { + return null; + } + try { + return findRoleInternal(dbConnection, rolename); + } finally { + closeConnection(dbConnection); + } + } else { + return null; + } + } finally { + rolesReadLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + public Role findRoleInternal(Connection dbConnection, String roleName) { + Role role = null; + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedRole)) { + stmt.setString(1, roleName); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + if (rs.getString(1) != null) { + String description = (roleAndGroupDescriptionCol != null) ? rs.getString(2) : null; + role = new GenericRole<>(this, roleName, description); + } + } + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + return role; + } + + @Override + public User findUser(String username) { + dbReadLock.lock(); + try { + usersReadLock.lock(); + try { + // Check local changes first + User user = createdUsers.get(username); + if (user != null) { + return user; + } + user = modifiedUsers.get(username); + if (user != null) { + return user; + } + user = removedUsers.get(username); + if (user != null) { + return null; + } + + Connection dbConnection = openConnection(); + if (dbConnection == null) { + return null; + } + try { + return findUserInternal(dbConnection, username); + } finally { + closeConnection(dbConnection); + } + } finally { + usersReadLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + public User findUserInternal(Connection dbConnection, String userName) { + String dbCredentials = null; + String fullName = null; + + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedUser)) { + stmt.setString(1, userName); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + dbCredentials = rs.getString(1); + if (userFullNameCol != null) { + fullName = rs.getString(2); + } + } + + dbCredentials = (dbCredentials != null) ? dbCredentials.trim() : null; + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + + // Lookup groups + ArrayList groups = new ArrayList<>(); + if (isGroupStoreDefined()) { + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedUserGroups)) { + stmt.setString(1, userName); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + String groupName = rs.getString(1); + if (groupName != null) { + Group group = findGroupInternal(dbConnection, groupName); + if (group != null) { + groups.add(group); + } + } + } + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + + ArrayList roles = new ArrayList<>(); + if (userRoleTable != null && roleNameCol != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(preparedUserRoles)) { + stmt.setString(1, userName); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + String roleName = rs.getString(1); + if (roleName != null) { + Role role = findRoleInternal(dbConnection, roleName); + if (role != null) { + roles.add(role); + } + } + } + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + + User user = new GenericUser<>(this, userName, dbCredentials, fullName, groups, roles); + return user; + } + + @Override + public void modifiedGroup(Group group) { + dbReadLock.lock(); + try { + groupsWriteLock.lock(); + try { + String name = group.getName(); + if (!createdGroups.containsKey(name) && !removedGroups.containsKey(name)) { + modifiedGroups.put(name, group); + } + } finally { + groupsWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public void modifiedRole(Role role) { + dbReadLock.lock(); + try { + rolesWriteLock.lock(); + try { + String name = role.getName(); + if (!createdRoles.containsKey(name) && !removedRoles.containsKey(name)) { + modifiedRoles.put(name, role); + } + } finally { + rolesWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public void modifiedUser(User user) { + dbReadLock.lock(); + try { + usersWriteLock.lock(); + try { + String name = user.getName(); + if (!createdUsers.containsKey(name) && !removedUsers.containsKey(name)) { + modifiedUsers.put(name, user); + } + } finally { + usersWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public void open() throws Exception { + + if (log.isDebugEnabled()) { + // As there are lots of parameters to configure, log some debug to help out + log.debug(sm.getString("dataSourceUserDatabase.features", Boolean.toString(userRoleTable != null && roleNameCol != null), + Boolean.toString(isRoleStoreDefined()), Boolean.toString(isGroupStoreDefined()))); + } + + dbWriteLock.lock(); + try { + + StringBuilder temp = new StringBuilder("SELECT "); + temp.append(userCredCol); + if (userFullNameCol != null) { + temp.append(',').append(userFullNameCol); + } + temp.append(" FROM "); + temp.append(userTable); + temp.append(" WHERE "); + temp.append(userNameCol); + temp.append(" = ?"); + preparedUser = temp.toString(); + + temp = new StringBuilder("SELECT "); + temp.append(userNameCol); + temp.append(" FROM "); + temp.append(userTable); + preparedAllUsers = temp.toString(); + + temp = new StringBuilder("SELECT "); + temp.append(roleNameCol); + temp.append(" FROM "); + temp.append(userRoleTable); + temp.append(" WHERE "); + temp.append(userNameCol); + temp.append(" = ?"); + preparedUserRoles = temp.toString(); + + if (isGroupStoreDefined()) { + temp = new StringBuilder("SELECT "); + temp.append(groupNameCol); + temp.append(" FROM "); + temp.append(userGroupTable); + temp.append(" WHERE "); + temp.append(userNameCol); + temp.append(" = ?"); + preparedUserGroups = temp.toString(); + + temp = new StringBuilder("SELECT "); + temp.append(roleNameCol); + temp.append(" FROM "); + temp.append(groupRoleTable); + temp.append(" WHERE "); + temp.append(groupNameCol); + temp.append(" = ?"); + preparedGroupRoles = temp.toString(); + + temp = new StringBuilder("SELECT "); + temp.append(groupNameCol); + if (roleAndGroupDescriptionCol != null) { + temp.append(',').append(roleAndGroupDescriptionCol); + } + temp.append(" FROM "); + temp.append(groupTable); + temp.append(" WHERE "); + temp.append(groupNameCol); + temp.append(" = ?"); + preparedGroup = temp.toString(); + + temp = new StringBuilder("SELECT "); + temp.append(groupNameCol); + temp.append(" FROM "); + temp.append(groupTable); + preparedAllGroups = temp.toString(); + } + + if (isRoleStoreDefined()) { + // Create the role PreparedStatement string + temp = new StringBuilder("SELECT "); + temp.append(roleNameCol); + if (roleAndGroupDescriptionCol != null) { + temp.append(',').append(roleAndGroupDescriptionCol); + } + temp.append(" FROM "); + temp.append(roleTable); + temp.append(" WHERE "); + temp.append(roleNameCol); + temp.append(" = ?"); + preparedRole = temp.toString(); + + temp = new StringBuilder("SELECT "); + temp.append(roleNameCol); + temp.append(" FROM "); + temp.append(roleTable); + preparedAllRoles = temp.toString(); + } else if (userRoleTable != null && roleNameCol != null) { + // Validate roles existence from the user <-> roles table + temp = new StringBuilder("SELECT "); + temp.append(roleNameCol); + temp.append(" FROM "); + temp.append(userRoleTable); + temp.append(" WHERE "); + temp.append(roleNameCol); + temp.append(" = ?"); + preparedRole = temp.toString(); + } + + } finally { + dbWriteLock.unlock(); + } + + } + + @Override + public void removeGroup(Group group) { + dbReadLock.lock(); + try { + groupsWriteLock.lock(); + try { + String name = group.getName(); + createdGroups.remove(name); + modifiedGroups.remove(name); + removedGroups.put(name, group); + } finally { + groupsWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public void removeRole(Role role) { + dbReadLock.lock(); + try { + rolesWriteLock.lock(); + try { + String name = role.getName(); + createdRoles.remove(name); + modifiedRoles.remove(name); + removedRoles.put(name, role); + } finally { + rolesWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public void removeUser(User user) { + dbReadLock.lock(); + try { + usersWriteLock.lock(); + try { + String name = user.getName(); + createdUsers.remove(name); + modifiedUsers.remove(name); + removedUsers.put(name, user); + } finally { + usersWriteLock.unlock(); + } + } finally { + dbReadLock.unlock(); + } + } + + @Override + public void save() throws Exception { + if (readonly) { + return; + } + + Connection dbConnection = openConnection(); + if (dbConnection == null) { + return; + } + + dbWriteLock.lock(); + try { + try { + saveInternal(dbConnection); + } finally { + closeConnection(dbConnection); + } + } finally { + dbWriteLock.unlock(); + } + } + + protected void saveInternal(Connection dbConnection) { + + StringBuilder temp = null; + StringBuilder tempRelation = null; + StringBuilder tempRelationDelete = null; + + if (isRoleStoreDefined()) { + + // Removed roles + if (!removedRoles.isEmpty()) { + temp = new StringBuilder("DELETE FROM "); + temp.append(roleTable); + temp.append(" WHERE ").append(roleNameCol); + temp.append(" = ?"); + if (groupRoleTable != null) { + tempRelationDelete = new StringBuilder("DELETE FROM "); + tempRelationDelete.append(groupRoleTable); + tempRelationDelete.append(" WHERE "); + tempRelationDelete.append(roleNameCol); + tempRelationDelete.append(" = ?"); + } + StringBuilder tempRelationDelete2 = new StringBuilder("DELETE FROM "); + tempRelationDelete2.append(userRoleTable); + tempRelationDelete2.append(" WHERE "); + tempRelationDelete2.append(roleNameCol); + tempRelationDelete2.append(" = ?"); + for (Role role : removedRoles.values()) { + if (tempRelationDelete != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete.toString())) { + stmt.setString(1, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete2.toString())) { + stmt.setString(1, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + removedRoles.clear(); + } + + // Created roles + if (!createdRoles.isEmpty()) { + temp = new StringBuilder("INSERT INTO "); + temp.append(roleTable); + temp.append('(').append(roleNameCol); + if (roleAndGroupDescriptionCol != null) { + temp.append(',').append(roleAndGroupDescriptionCol); + } + temp.append(") VALUES (?"); + if (roleAndGroupDescriptionCol != null) { + temp.append(", ?"); + } + temp.append(')'); + for (Role role : createdRoles.values()) { + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, role.getRolename()); + if (roleAndGroupDescriptionCol != null) { + stmt.setString(2, role.getDescription()); + } + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + createdRoles.clear(); + } + + // Modified roles + if (!modifiedRoles.isEmpty() && roleAndGroupDescriptionCol != null) { + temp = new StringBuilder("UPDATE "); + temp.append(roleTable); + temp.append(" SET ").append(roleAndGroupDescriptionCol); + temp.append(" = ? WHERE ").append(roleNameCol); + temp.append(" = ?"); + for (Role role : modifiedRoles.values()) { + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, role.getDescription()); + stmt.setString(2, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + modifiedRoles.clear(); + } + + } else if (userRoleTable != null && roleNameCol != null) { + // Only remove role from users + tempRelationDelete = new StringBuilder("DELETE FROM "); + tempRelationDelete.append(userRoleTable); + tempRelationDelete.append(" WHERE "); + tempRelationDelete.append(roleNameCol); + tempRelationDelete.append(" = ?"); + for (Role role : removedRoles.values()) { + try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete.toString())) { + stmt.setString(1, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + removedRoles.clear(); + } + + if (isGroupStoreDefined()) { + + tempRelation = new StringBuilder("INSERT INTO "); + tempRelation.append(groupRoleTable); + tempRelation.append('(').append(groupNameCol).append(", "); + tempRelation.append(roleNameCol); + tempRelation.append(") VALUES (?, ?)"); + String groupRoleRelation = tempRelation.toString(); + // Always drop and recreate all group <-> role relations + tempRelationDelete = new StringBuilder("DELETE FROM "); + tempRelationDelete.append(groupRoleTable); + tempRelationDelete.append(" WHERE "); + tempRelationDelete.append(groupNameCol); + tempRelationDelete.append(" = ?"); + String groupRoleRelationDelete = tempRelationDelete.toString(); + + // Removed groups + if (!removedGroups.isEmpty()) { + temp = new StringBuilder("DELETE FROM "); + temp.append(groupTable); + temp.append(" WHERE ").append(groupNameCol); + temp.append(" = ?"); + StringBuilder tempRelationDelete2 = new StringBuilder("DELETE FROM "); + tempRelationDelete2.append(userGroupTable); + tempRelationDelete2.append(" WHERE "); + tempRelationDelete2.append(groupNameCol); + tempRelationDelete2.append(" = ?"); + for (Group group : removedGroups.values()) { + try (PreparedStatement stmt = dbConnection.prepareStatement(groupRoleRelationDelete)) { + stmt.setString(1, group.getGroupname()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete2.toString())) { + stmt.setString(1, group.getGroupname()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, group.getGroupname()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + removedGroups.clear(); + } + + // Created groups + if (!createdGroups.isEmpty()) { + temp = new StringBuilder("INSERT INTO "); + temp.append(groupTable); + temp.append('(').append(groupNameCol); + if (roleAndGroupDescriptionCol != null) { + temp.append(',').append(roleAndGroupDescriptionCol); + } + temp.append(") VALUES (?"); + if (roleAndGroupDescriptionCol != null) { + temp.append(", ?"); + } + temp.append(')'); + for (Group group : createdGroups.values()) { + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, group.getGroupname()); + if (roleAndGroupDescriptionCol != null) { + stmt.setString(2, group.getDescription()); + } + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + Iterator roles = group.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + try (PreparedStatement stmt = dbConnection.prepareStatement(groupRoleRelation)) { + stmt.setString(1, group.getGroupname()); + stmt.setString(2, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + } + createdGroups.clear(); + } + + // Modified groups + if (!modifiedGroups.isEmpty()) { + if (roleAndGroupDescriptionCol != null) { + temp = new StringBuilder("UPDATE "); + temp.append(groupTable); + temp.append(" SET ").append(roleAndGroupDescriptionCol); + temp.append(" = ? WHERE ").append(groupNameCol); + temp.append(" = ?"); + } + for (Group group : modifiedGroups.values()) { + if (temp != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, group.getDescription()); + stmt.setString(2, group.getGroupname()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + try (PreparedStatement stmt = dbConnection.prepareStatement(groupRoleRelationDelete)) { + stmt.setString(1, group.getGroupname()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + Iterator roles = group.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + try (PreparedStatement stmt = dbConnection.prepareStatement(groupRoleRelation)) { + stmt.setString(1, group.getGroupname()); + stmt.setString(2, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + } + modifiedGroups.clear(); + } + + } + + String userRoleRelation = null; + String userRoleRelationDelete = null; + if (userRoleTable != null && roleNameCol != null) { + tempRelation = new StringBuilder("INSERT INTO "); + tempRelation.append(userRoleTable); + tempRelation.append('(').append(userNameCol).append(", "); + tempRelation.append(roleNameCol); + tempRelation.append(") VALUES (?, ?)"); + userRoleRelation = tempRelation.toString(); + // Always drop and recreate all user <-> role relations + tempRelationDelete = new StringBuilder("DELETE FROM "); + tempRelationDelete.append(userRoleTable); + tempRelationDelete.append(" WHERE "); + tempRelationDelete.append(userNameCol); + tempRelationDelete.append(" = ?"); + userRoleRelationDelete = tempRelationDelete.toString(); + } + String userGroupRelation = null; + String userGroupRelationDelete = null; + if (isGroupStoreDefined()) { + tempRelation = new StringBuilder("INSERT INTO "); + tempRelation.append(userGroupTable); + tempRelation.append('(').append(userNameCol).append(", "); + tempRelation.append(groupNameCol); + tempRelation.append(") VALUES (?, ?)"); + userGroupRelation = tempRelation.toString(); + // Always drop and recreate all user <-> group relations + tempRelationDelete = new StringBuilder("DELETE FROM "); + tempRelationDelete.append(userGroupTable); + tempRelationDelete.append(" WHERE "); + tempRelationDelete.append(userNameCol); + tempRelationDelete.append(" = ?"); + userGroupRelationDelete = tempRelationDelete.toString(); + } + + // Removed users + if (!removedUsers.isEmpty()) { + temp = new StringBuilder("DELETE FROM "); + temp.append(userTable); + temp.append(" WHERE ").append(userNameCol); + temp.append(" = ?"); + for (User user : removedUsers.values()) { + if (userRoleRelationDelete != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(userRoleRelationDelete)) { + stmt.setString(1, user.getUsername()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + if (userGroupRelationDelete != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(userGroupRelationDelete)) { + stmt.setString(1, user.getUsername()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, user.getUsername()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + removedUsers.clear(); + } + + // Created users + if (!createdUsers.isEmpty()) { + temp = new StringBuilder("INSERT INTO "); + temp.append(userTable); + temp.append('(').append(userNameCol); + temp.append(", ").append(userCredCol); + if (userFullNameCol != null) { + temp.append(',').append(userFullNameCol); + } + temp.append(") VALUES (?, ?"); + if (userFullNameCol != null) { + temp.append(", ?"); + } + temp.append(')'); + for (User user : createdUsers.values()) { + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, user.getUsername()); + stmt.setString(2, user.getPassword()); + if (userFullNameCol != null) { + stmt.setString(3, user.getFullName()); + } + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + if (userRoleRelation != null) { + Iterator roles = user.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + try (PreparedStatement stmt = dbConnection.prepareStatement(userRoleRelation)) { + stmt.setString(1, user.getUsername()); + stmt.setString(2, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + } + if (userGroupRelation != null) { + Iterator groups = user.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + try (PreparedStatement stmt = dbConnection.prepareStatement(userGroupRelation)) { + stmt.setString(1, user.getUsername()); + stmt.setString(2, group.getGroupname()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + } + } + createdUsers.clear(); + } + + // Modified users + if (!modifiedUsers.isEmpty()) { + temp = new StringBuilder("UPDATE "); + temp.append(userTable); + temp.append(" SET ").append(userCredCol); + temp.append(" = ?"); + if (userFullNameCol != null) { + temp.append(", ").append(userFullNameCol).append(" = ?"); + } + temp.append(" WHERE ").append(userNameCol); + temp.append(" = ?"); + for (User user : modifiedUsers.values()) { + try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) { + stmt.setString(1, user.getPassword()); + if (userFullNameCol != null) { + stmt.setString(2, user.getFullName()); + stmt.setString(3, user.getUsername()); + } else { + stmt.setString(2, user.getUsername()); + } + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + if (userRoleRelationDelete != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(userRoleRelationDelete)) { + stmt.setString(1, user.getUsername()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + if (userGroupRelationDelete != null) { + try (PreparedStatement stmt = dbConnection.prepareStatement(userGroupRelationDelete)) { + stmt.setString(1, user.getUsername()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + if (userRoleRelation != null) { + Iterator roles = user.getRoles(); + while (roles.hasNext()) { + Role role = roles.next(); + try (PreparedStatement stmt = dbConnection.prepareStatement(userRoleRelation)) { + stmt.setString(1, user.getUsername()); + stmt.setString(2, role.getRolename()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + } + if (userGroupRelation != null) { + Iterator groups = user.getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + try (PreparedStatement stmt = dbConnection.prepareStatement(userGroupRelation)) { + stmt.setString(1, user.getUsername()); + stmt.setString(2, group.getGroupname()); + stmt.executeUpdate(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + } + } + } + modifiedGroups.clear(); + } + + } + + @Override + public boolean isAvailable() { + return connectionSuccess; + } + + /** + * Only use groups if the tables are fully defined. + * @return true when groups are used + */ + protected boolean isGroupStoreDefined() { + return groupTable != null && userGroupTable != null && groupNameCol != null + && groupRoleTable != null && isRoleStoreDefined(); + } + + + /** + * Only use roles if the tables are fully defined. + * @return true when roles are used + */ + protected boolean isRoleStoreDefined() { + return roleTable != null && userRoleTable != null && roleNameCol != null; + } + + + /** + * Open the specified database connection. + * + * @return Connection to the database + */ + protected Connection openConnection() { + if (dataSource == null) { + return null; + } + try { + Connection connection = dataSource.getConnection(); + connectionSuccess = true; + return connection; + } catch (Exception e) { + connectionSuccess = false; + // Log the problem for posterity + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + return null; + } + + /** + * Close the specified database connection. + * + * @param dbConnection The connection to be closed + */ + protected void closeConnection(Connection dbConnection) { + + // Do nothing if the database connection is already closed + if (dbConnection == null) { + return; + } + + // Commit if not auto committed + try { + if (!dbConnection.getAutoCommit()) { + dbConnection.commit(); + } + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + + // Close this database connection, and log any errors + try { + dbConnection.close(); + } catch (SQLException e) { + log.error(sm.getString("dataSourceUserDatabase.exception"), e); + } + + } + + +} diff --git a/java/org/apache/catalina/users/DataSourceUserDatabaseFactory.java b/java/org/apache/catalina/users/DataSourceUserDatabaseFactory.java new file mode 100644 index 0000000..1a69b7f --- /dev/null +++ b/java/org/apache/catalina/users/DataSourceUserDatabaseFactory.java @@ -0,0 +1,169 @@ +/* + * 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.catalina.users; + + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; +import javax.sql.DataSource; + + +/** + *

    JNDI object creation factory for DataSourceUserDatabase + * instances. This makes it convenient to configure a user database + * in the global JNDI resources associated with this Catalina instance, + * and then link to that resource for web applications that administer + * the contents of the user database.

    + * + *

    The DataSourceUserDatabase instance is configured based + * on the following parameter values:

    + *
      + *
    • dataSourceName - JNDI name of the DataSource, which + * must be located in the same Context environment as the UserDatabase
    • + *
    + * + * @author Craig R. McClanahan + */ +public class DataSourceUserDatabaseFactory implements ObjectFactory { + + + // --------------------------------------------------------- Public Methods + + + /** + *

    Create and return a new DataSourceUserDatabase instance + * that has been configured according to the properties of the + * specified Reference. If you instance can be created, + * return null instead.

    + * + * @param obj The possibly null object containing location or + * reference information that can be used in creating an object + * @param name The name of this object relative to nameCtx + * @param nameCtx The context relative to which the name + * parameter is specified, or null if name + * is relative to the default initial context + * @param environment The possibly null environment that is used in + * creating this object + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) + throws Exception { + + // We only know how to deal with javax.naming.References + // that specify a class name of "org.apache.catalina.UserDatabase" + if ((obj == null) || !(obj instanceof Reference)) { + return null; + } + Reference ref = (Reference) obj; + if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) { + return null; + } + + DataSource dataSource = null; + String dataSourceName = null; + RefAddr ra = null; + + ra = ref.get("dataSourceName"); + if (ra != null) { + dataSourceName = ra.getContent().toString(); + dataSource = (DataSource) nameCtx.lookup(dataSourceName); + } + + // Create and configure a DataSourceUserDatabase instance based on the + // RefAddr values associated with this Reference + DataSourceUserDatabase database = new DataSourceUserDatabase(dataSource, name.toString()); + database.setDataSourceName(dataSourceName); + + ra = ref.get("readonly"); + if (ra != null) { + database.setReadonly(Boolean.parseBoolean(ra.getContent().toString())); + } + + ra = ref.get("userTable"); + if (ra != null) { + database.setUserTable(ra.getContent().toString()); + } + + ra = ref.get("groupTable"); + if (ra != null) { + database.setGroupTable(ra.getContent().toString()); + } + + ra = ref.get("roleTable"); + if (ra != null) { + database.setRoleTable(ra.getContent().toString()); + } + + ra = ref.get("userRoleTable"); + if (ra != null) { + database.setUserRoleTable(ra.getContent().toString()); + } + + ra = ref.get("userGroupTable"); + if (ra != null) { + database.setUserGroupTable(ra.getContent().toString()); + } + + ra = ref.get("groupRoleTable"); + if (ra != null) { + database.setGroupRoleTable(ra.getContent().toString()); + } + + ra = ref.get("roleNameCol"); + if (ra != null) { + database.setRoleNameCol(ra.getContent().toString()); + } + + ra = ref.get("roleAndGroupDescriptionCol"); + if (ra != null) { + database.setRoleAndGroupDescriptionCol(ra.getContent().toString()); + } + + ra = ref.get("groupNameCol"); + if (ra != null) { + database.setGroupNameCol(ra.getContent().toString()); + } + + ra = ref.get("userCredCol"); + if (ra != null) { + database.setUserCredCol(ra.getContent().toString()); + } + + ra = ref.get("userFullNameCol"); + if (ra != null) { + database.setUserFullNameCol(ra.getContent().toString()); + } + + ra = ref.get("userNameCol"); + if (ra != null) { + database.setUserNameCol(ra.getContent().toString()); + } + + // Return the configured database instance + database.open(); + return database; + + } + + +} diff --git a/java/org/apache/catalina/users/GenericGroup.java b/java/org/apache/catalina/users/GenericGroup.java new file mode 100644 index 0000000..153f5e5 --- /dev/null +++ b/java/org/apache/catalina/users/GenericGroup.java @@ -0,0 +1,190 @@ +/* + * 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.catalina.users; + + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.catalina.UserDatabase; + + +/** + *

    Concrete implementation of {@link org.apache.catalina.Group} for a + * {@link UserDatabase}.

    + * + * @param The specific type of UserDase with which this group is associated + * + * @author Craig R. McClanahan + */ +public class GenericGroup extends AbstractGroup { + + + // ----------------------------------------------------------- Constructors + + + /** + * Package-private constructor used by the factory method in + * {@link UserDatabase}. + * + * @param database The {@link UserDatabase} that owns this group + * @param groupname Group name of this group + * @param description Description of this group + * @param roles The roles of this group + */ + GenericGroup(UD database, + String groupname, String description, List roles) { + + super(); + this.database = database; + this.groupname = groupname; + this.description = description; + if (roles != null) { + this.roles.addAll(roles); + } + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The {@link UserDatabase} that owns this group. + */ + protected final UD database; + + + /** + * The set of {@link Role}s associated with this group. + */ + protected final CopyOnWriteArrayList roles = new CopyOnWriteArrayList<>(); + + + // ------------------------------------------------------------- Properties + + + /** + * Return the set of {@link Role}s assigned specifically to this group. + */ + @Override + public Iterator getRoles() { + return roles.iterator(); + } + + + /** + * Return the {@link UserDatabase} within which this Group is defined. + */ + @Override + public UserDatabase getUserDatabase() { + return this.database; + } + + + /** + * Return the set of {@link org.apache.catalina.User}s that are members of this group. + */ + @Override + public Iterator getUsers() { + List results = new ArrayList<>(); + Iterator users = database.getUsers(); + while (users.hasNext()) { + User user = users.next(); + if (user.isInGroup(this)) { + results.add(user); + } + } + return results.iterator(); + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new {@link Role} to those assigned specifically to this group. + * + * @param role The new role + */ + @Override + public void addRole(Role role) { + if (roles.addIfAbsent(role)) { + database.modifiedGroup(this); + } + } + + + /** + * Is this group specifically assigned the specified {@link Role}? + * + * @param role The role to check + */ + @Override + public boolean isInRole(Role role) { + return roles.contains(role); + } + + + /** + * Remove a {@link Role} from those assigned to this group. + * + * @param role The old role + */ + @Override + public void removeRole(Role role) { + if (roles.remove(role)) { + database.modifiedGroup(this); + } + } + + + /** + * Remove all {@link Role}s from those assigned to this group. + */ + @Override + public void removeRoles() { + if (!roles.isEmpty()) { + roles.clear(); + database.modifiedGroup(this); + } + } + + + @Override + public boolean equals(Object obj) { + if (obj instanceof GenericGroup) { + GenericGroup group = (GenericGroup) obj; + return group.database == database && groupname.equals(group.getGroupname()); + } + return super.equals(obj); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((database == null) ? 0 : database.hashCode()); + result = prime * result + ((groupname == null) ? 0 : groupname.hashCode()); + return result; + } +} diff --git a/java/org/apache/catalina/users/GenericRole.java b/java/org/apache/catalina/users/GenericRole.java new file mode 100644 index 0000000..2103714 --- /dev/null +++ b/java/org/apache/catalina/users/GenericRole.java @@ -0,0 +1,109 @@ +/* + * 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.catalina.users; + + +import org.apache.catalina.UserDatabase; + + +/** + *

    Concrete implementation of {@link org.apache.catalina.Role} for a + * {@link UserDatabase}.

    + * + * @param The specific type of UserDase with which this role is associated + * + * @author Craig R. McClanahan + */ +public class GenericRole extends AbstractRole { + + + // ----------------------------------------------------------- Constructors + + + /** + * Package-private constructor used by the factory method in + * {@link UserDatabase}. + * + * @param database The {@link UserDatabase} that owns this role + * @param rolename Role name of this role + * @param description Description of this role + */ + GenericRole(UD database, + String rolename, String description) { + + super(); + this.database = database; + this.rolename = rolename; + this.description = description; + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The {@link UserDatabase} that owns this role. + */ + protected final UserDatabase database; + + + // ------------------------------------------------------------- Properties + + + /** + * Return the {@link UserDatabase} within which this role is defined. + */ + @Override + public UserDatabase getUserDatabase() { + return this.database; + } + + + @Override + public void setDescription(String description) { + database.modifiedRole(this); + super.setDescription(description); + } + + + @Override + public void setRolename(String rolename) { + database.modifiedRole(this); + super.setRolename(rolename); + } + + + @Override + public boolean equals(Object obj) { + if (obj instanceof GenericRole) { + GenericRole role = (GenericRole) obj; + return role.database == database && rolename.equals(role.getRolename()); + } + return super.equals(obj); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((database == null) ? 0 : database.hashCode()); + result = prime * result + ((rolename == null) ? 0 : rolename.hashCode()); + return result; + } +} diff --git a/java/org/apache/catalina/users/GenericUser.java b/java/org/apache/catalina/users/GenericUser.java new file mode 100644 index 0000000..a020acd --- /dev/null +++ b/java/org/apache/catalina/users/GenericUser.java @@ -0,0 +1,265 @@ +/* + * 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.catalina.users; + + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.UserDatabase; + +/** + *

    Concrete implementation of {@link org.apache.catalina.User} for a + * {@link UserDatabase}.

    + * + * @param The specific type of UserDase with which this role is associated + * + * @author Craig R. McClanahan + */ +public class GenericUser extends AbstractUser { + + + // ----------------------------------------------------------- Constructors + + + /** + * Package-private constructor used by the factory method in + * {@link UserDatabase}. + * + * @param database The {@link UserDatabase} that owns this user + * @param username Logon username of the new user + * @param password Logon password of the new user + * @param fullName Full name of the new user + * @param groups The groups of this user + * @param roles The roles of this user + */ + GenericUser(UD database, String username, + String password, String fullName, List groups, + List roles) { + + super(); + this.database = database; + this.username = username; + this.password = password; + this.fullName = fullName; + if (groups != null) { + this.groups.addAll(groups); + } + if (roles != null) { + this.roles.addAll(roles); + } + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The {@link UserDatabase} that owns this user. + */ + protected final UD database; + + + /** + * The set of {@link Group}s that this user is a member of. + */ + protected final CopyOnWriteArrayList groups = new CopyOnWriteArrayList<>(); + + + /** + * The set of {@link Role}s associated with this user. + */ + protected final CopyOnWriteArrayList roles = new CopyOnWriteArrayList<>(); + + + // ------------------------------------------------------------- Properties + + + /** + * Return the set of {@link Group}s to which this user belongs. + */ + @Override + public Iterator getGroups() { + return groups.iterator(); + } + + + /** + * Return the set of {@link Role}s assigned specifically to this user. + */ + @Override + public Iterator getRoles() { + return roles.iterator(); + } + + + /** + * Return the {@link UserDatabase} within which this User is defined. + */ + @Override + public UserDatabase getUserDatabase() { + return this.database; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new {@link Group} to those this user belongs to. + * + * @param group The new group + */ + @Override + public void addGroup(Group group) { + if (groups.addIfAbsent(group)) { + database.modifiedUser(this); + } + } + + + /** + * Add a new {@link Role} to those assigned specifically to this user. + * + * @param role The new role + */ + @Override + public void addRole(Role role) { + if (roles.addIfAbsent(role)) { + database.modifiedUser(this); + } + } + + + /** + * Is this user in the specified group? + * + * @param group The group to check + */ + @Override + public boolean isInGroup(Group group) { + return groups.contains(group); + } + + + /** + * Is this user specifically assigned the specified {@link Role}? This + * method does NOT check for roles inherited based on + * {@link Group} membership. + * + * @param role The role to check + */ + @Override + public boolean isInRole(Role role) { + return roles.contains(role); + } + + + /** + * Remove a {@link Group} from those this user belongs to. + * + * @param group The old group + */ + @Override + public void removeGroup(Group group) { + if (groups.remove(group)) { + database.modifiedUser(this); + } + } + + + /** + * Remove all {@link Group}s from those this user belongs to. + */ + @Override + public void removeGroups() { + if (!groups.isEmpty()) { + groups.clear(); + database.modifiedUser(this); + } + } + + + /** + * Remove a {@link Role} from those assigned to this user. + * + * @param role The old role + */ + @Override + public void removeRole(Role role) { + if (roles.remove(role)) { + database.modifiedUser(this); + } + } + + + /** + * Remove all {@link Role}s from those assigned to this user. + */ + @Override + public void removeRoles() { + if (!roles.isEmpty()) { + database.modifiedUser(this); + } + roles.clear(); + } + + + @Override + public void setFullName(String fullName) { + database.modifiedUser(this); + super.setFullName(fullName); + } + + + @Override + public void setPassword(String password) { + database.modifiedUser(this); + super.setPassword(password); + } + + + @Override + public void setUsername(String username) { + database.modifiedUser(this); + // Note: changing the user name is a problem ... + super.setUsername(username); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof GenericUser) { + GenericUser user = (GenericUser) obj; + return user.database == database && username.equals(user.getUsername()); + } + return super.equals(obj); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((database == null) ? 0 : database.hashCode()); + result = prime * result + ((username == null) ? 0 : username.hashCode()); + return result; + } +} diff --git a/java/org/apache/catalina/users/LocalStrings.properties b/java/org/apache/catalina/users/LocalStrings.properties new file mode 100644 index 0000000..56bea7c --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings.properties @@ -0,0 +1,33 @@ +# 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. + +dataSourceUserDatabase.exception=Exception accessing the database +dataSourceUserDatabase.features=DataSource UserDatabase features: User <-> Role [{0}], Roles [{1}], Groups [{2}] + +memoryUserDatabase.fileClose=Failed to close [{0}] +memoryUserDatabase.fileDelete=Failed to delete [{0}] +memoryUserDatabase.fileNotFound=The specified user database [{0}] could not be found +memoryUserDatabase.notPersistable=User database is not persistable - no write permissions on directory +memoryUserDatabase.nullGroup=Null or zero length group name specified. The group will be ignored. +memoryUserDatabase.nullRole=Null or zero length role name specified. The role will be ignored. +memoryUserDatabase.nullUser=Null or zero length user name specified. The user will be ignored. +memoryUserDatabase.readOnly=User database has been configured to be read only. Changes cannot be saved +memoryUserDatabase.reload=Reloading memory user database [{0}] from updated source [{1}] +memoryUserDatabase.reloadError=Error reloading memory user database [{0}] from updated source [{1}] +memoryUserDatabase.renameNew=Cannot rename new file to [{0}] +memoryUserDatabase.renameOld=Cannot rename original file to [{0}] +memoryUserDatabase.restoreOrig=Cannot restore [{0}] to original file +memoryUserDatabase.writeException=IOException writing to [{0}] +memoryUserDatabase.xmlFeatureEncoding=Exception configuring digester to permit java encoding names in XML files. Only IANA encoding names will be supported. diff --git a/java/org/apache/catalina/users/LocalStrings_cs.properties b/java/org/apache/catalina/users/LocalStrings_cs.properties new file mode 100644 index 0000000..8120027 --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings_cs.properties @@ -0,0 +1,16 @@ +# 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. + +memoryUserDatabase.nullRole=Uvedeno NULL nebo prázdné jméno role. Role bude ignorována. diff --git a/java/org/apache/catalina/users/LocalStrings_de.properties b/java/org/apache/catalina/users/LocalStrings_de.properties new file mode 100644 index 0000000..dfa43bb --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings_de.properties @@ -0,0 +1,17 @@ +# 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. + +memoryUserDatabase.fileNotFound=Die angegebene User-Database [{0}] konnte nicht gefunden werden +memoryUserDatabase.nullRole=Spezifizierter Rollenname ist null oder ist null Zeichen lang und wird daher ignoriert diff --git a/java/org/apache/catalina/users/LocalStrings_es.properties b/java/org/apache/catalina/users/LocalStrings_es.properties new file mode 100644 index 0000000..e9ab723 --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings_es.properties @@ -0,0 +1,26 @@ +# 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. + +memoryUserDatabase.fileNotFound=El usuario de base de datos especificado [{0}] no pudo ser encontrado +memoryUserDatabase.notPersistable=La base de datos de usuario no es persistible - no hay permisos de grabación sobre el directorio +memoryUserDatabase.nullGroup=Se ha especificado un nombre de grupo nulo o de tamaño cero. Se ignora el grupo. +memoryUserDatabase.nullRole=Se ha especificado un nombre rol nulo o de tamaño cero. Se ignora el rol. +memoryUserDatabase.nullUser=Se ha especificado un nombre de usuario nulo o de tamaño cero. Se ignora el usuario. +memoryUserDatabase.readOnly=User database has been configured to be read only. Changes cannot be saved +memoryUserDatabase.renameNew=Imposible de renombrar el archivo nuevo a [{0}] +memoryUserDatabase.renameOld=Imposible de renombrar el archivo original a [{0}] +memoryUserDatabase.restoreOrig=No se puede restablecer [{0}] al archivo original +memoryUserDatabase.writeException=IOException durante la escritura hacia [{0}] +memoryUserDatabase.xmlFeatureEncoding=Excepción al configurar el resumidor para permitir nombres codificados en java en los ficheros XML. Sólo se soportarán los nombres con codificación IANA. diff --git a/java/org/apache/catalina/users/LocalStrings_fr.properties b/java/org/apache/catalina/users/LocalStrings_fr.properties new file mode 100644 index 0000000..7fad94d --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings_fr.properties @@ -0,0 +1,33 @@ +# 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. + +dataSourceUserDatabase.exception=Exception lors de l'accès à la base de données +dataSourceUserDatabase.features=Fonctionnalités de la UserDatabase utilisant une DataSource: User <-> Rôle [{0}], Rôles [{1}], Groupes [{2}] + +memoryUserDatabase.fileClose=Echec de fermeture [{0}] +memoryUserDatabase.fileDelete=Impossible d''effacer [{0}] +memoryUserDatabase.fileNotFound=La base d''utilisateurs spécifiée [{0}] n''a pas été trouvée +memoryUserDatabase.notPersistable=La base de donnée des utilisateurs ne peut pas être persistée, il n'y a pas de permissions d'écriture sur le répertoire +memoryUserDatabase.nullGroup=Un nom de groupe nul ou vide a été spécifié, le groupe sera ignoré +memoryUserDatabase.nullRole=Le nom du rôle spécifié est nul ou a une taille de zéro. Le rôle sera ignoré. +memoryUserDatabase.nullUser=Le nom d'utilisateur est null ou a une longueur de zéro, il sera ignoré +memoryUserDatabase.readOnly=La base de donnée utilisateurs a été configurée en mode lecture seule, les modifications ne peuvent être sauvegardées +memoryUserDatabase.reload=Rechargement de la base de données des utilisateurs [{0}] à partir de la source mise à jour [{1}] +memoryUserDatabase.reloadError=Erreur de rechargement de la base de donnée utilisateurs [{0}] à partir de la source mise à jour [{1}] +memoryUserDatabase.renameNew=Impossible de renommer le nouveau fichier en [{0}] +memoryUserDatabase.renameOld=Impossible de renommer le fichier d''origine en [{0}] +memoryUserDatabase.restoreOrig=Impossible de restaurer [{0}] vers le fichier d''origine +memoryUserDatabase.writeException=IOException lors de l''écriture vers [{0}] +memoryUserDatabase.xmlFeatureEncoding=Exception lors de la configuration du Digester pour permettre des noms d'encodage Java dans les fichiers XML, seuls le noms IANA seront supportés diff --git a/java/org/apache/catalina/users/LocalStrings_ja.properties b/java/org/apache/catalina/users/LocalStrings_ja.properties new file mode 100644 index 0000000..b09a98f --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings_ja.properties @@ -0,0 +1,33 @@ +# 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. + +dataSourceUserDatabase.exception=データベースアクセスã§ä¾‹å¤–ãŒç™ºç”Ÿ +dataSourceUserDatabase.features=データソース UserDatabase ã®æ©Ÿèƒ½: User <-> Role [{0}]ã€Roles [{1}]ã€Group [{2}] + +memoryUserDatabase.fileClose=[{0}]ã®ã‚¯ãƒ­ãƒ¼ã‚ºã«å¤±æ•—ã—ã¾ã—㟠+memoryUserDatabase.fileDelete=[{0}]を削除ã§ãã¾ã›ã‚“ã§ã—㟠+memoryUserDatabase.fileNotFound=ユーザー情報データベースã¨ã—ã¦æŒ‡å®šã•ã‚ŒãŸ [{0}] ã¯å­˜åœ¨ã—ã¾ã›ã‚“。 +memoryUserDatabase.notPersistable=ユーザーデータベースã¯æ°¸ç¶šçš„ã§ã¯ã‚ã‚Šã¾ã›ã‚“ - ディレクトリã«å¯¾ã™ã‚‹æ›¸ãè¾¼ã¿æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“。 +memoryUserDatabase.nullGroup=Nullã¾ãŸã¯é•·ã•ã‚¼ãƒ­ã®ã‚°ãƒ«ãƒ¼ãƒ—åãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ グループã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +memoryUserDatabase.nullRole=NULLã¾ãŸã¯é•·ã•ã‚¼ãƒ­ã®ãƒ­ãƒ¼ãƒ«åãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ロールã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +memoryUserDatabase.nullUser=Nullã¾ãŸã¯é•·ã•ã‚¼ãƒ­ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ユーザーã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +memoryUserDatabase.readOnly=ユーザー情報データベースã¯èª­ã¿å–り専用ã«ãªã£ã¦ã„ã¾ã™ã€‚変更をä¿å­˜ã§ãã¾ã›ã‚“。 +memoryUserDatabase.reload=æ›´æ–°ã•ã‚ŒãŸã‚½ãƒ¼ã‚¹ [{1}] ã‹ã‚‰ãƒ¡ãƒ¢ãƒªãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ [{0}] ã‚’å†ãƒ­ãƒ¼ãƒ‰ã—ã¦ã„ã¾ã™ +memoryUserDatabase.reloadError=æ›´æ–°ã•ã‚ŒãŸã‚½ãƒ¼ã‚¹ [{1}] ã‹ã‚‰ãƒ¡ãƒ¢ãƒªãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ [{0}] ã‚’å†ãƒ­ãƒ¼ãƒ‰ä¸­ã®ã‚¨ãƒ©ãƒ¼ +memoryUserDatabase.renameNew=æ–°ã—ã„ファイルåã‚’ [{0}] ã«å¤‰æ›´ã§ãã¾ã›ã‚“ +memoryUserDatabase.renameOld=å…ƒã®ãƒ•ã‚¡ã‚¤ãƒ«åã‚’ [{0}] ã«å¤‰æ›´ã§ãã¾ã›ã‚“ +memoryUserDatabase.restoreOrig=[{0}]ã‚’å…ƒã®ãƒ•ã‚¡ã‚¤ãƒ«ã«å¾©å…ƒã§ãã¾ã›ã‚“ +memoryUserDatabase.writeException=[{0}] ã«æ›¸ãè¾¼ã¿ä¸­ã®å…¥å‡ºåŠ›ä¾‹å¤–ã§ã™ +memoryUserDatabase.xmlFeatureEncoding=XMLファイルã®Javaエンコーディングåを許å¯ã™ã‚‹ãŸã‚ã«digesterを設定ã™ã‚‹éš›ã®ä¾‹å¤–。 IANAã®ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‡ã‚£ãƒ³ã‚°åã®ã¿ãŒã‚µãƒãƒ¼ãƒˆã•ã‚Œã¾ã™ã€‚ diff --git a/java/org/apache/catalina/users/LocalStrings_ko.properties b/java/org/apache/catalina/users/LocalStrings_ko.properties new file mode 100644 index 0000000..d026164 --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings_ko.properties @@ -0,0 +1,32 @@ +# 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. + +dataSourceUserDatabase.exception=ë°ì´í„°ë² ì´ìŠ¤ ì ‘ê·¼ 과정ì—ì„œ 예외 ë°œìƒ + +memoryUserDatabase.fileClose=[{0}]ì„(를) 닫지 못했습니다. +memoryUserDatabase.fileDelete=[{0}]ì„(를) 삭제하지 못했습니다. +memoryUserDatabase.fileNotFound=ì§€ì •ëœ ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없었습니다. +memoryUserDatabase.notPersistable=ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ì— 저장할 수 없습니다 - ë””ë ‰í† ë¦¬ì— ì“°ê¸° ê¶Œí•œì´ ì—†ìŒ. +memoryUserDatabase.nullGroup=ë„ ë˜ëŠ” 길ì´ê°€ 0ì¸ ê·¸ë£¹ ì´ë¦„ì´ ì§€ì •ë˜ì—ˆìŠµë‹ˆë‹¤. 해당 ê·¸ë£¹ì€ ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +memoryUserDatabase.nullRole=ë„ì´ê±°ë‚˜ 길ì´ê°€ 0ì¸ ë¬¸ìžì—´ë¡œ, 역할 ì´ë¦„ì´ ì§€ì •ë˜ì—ˆìŠµë‹ˆë‹¤. 해당 ì—­í• ì€ ë¬´ì‹œë©ë‹ˆë‹¤. +memoryUserDatabase.nullUser=ì‚¬ìš©ìž ì´ë¦„ì— ë„ì´ê±°ë‚˜ 길ì´ê°€ 0ì¸ ë¬¸ìžì—´ì´ 지정ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ ì‚¬ìš©ìžëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +memoryUserDatabase.readOnly=ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ëŠ” ì½ê¸° 전용으로 설정ë˜ì–´ 있습니다. 변경 ì‚¬í•­ë“¤ì´ ì €ìž¥ë  ìˆ˜ 없습니다. +memoryUserDatabase.reload=ë³€ê²½ëœ ì†ŒìŠ¤ [{1}](으)로부터 메모리 ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ [{0}]ì„(를) 다시 로드합니다. +memoryUserDatabase.reloadError=메모리 ì‚¬ìš©ìž ë°ì´í„°ë² ì´ìŠ¤ [{0}]ì„(를), ë³€ê²½ëœ ì›ë³¸ [{1}](으)로부터 다시 로드하는 중 오류 ë°œìƒ +memoryUserDatabase.renameNew=새 파ì¼ì„ [{0}](으)ë¡œ ì´ë¦„ì„ ë³€ê²½í•  수 없습니다. +memoryUserDatabase.renameOld=ì›ë³¸ 파ì¼ì„ [{0}](으)ë¡œ ì´ë¦„ì„ ë³€ê²½í•  수 없습니다. +memoryUserDatabase.restoreOrig=[{0}] 파ì¼ì„ ì›ë³¸ 파ì¼ë¡œ 복구할 수 없습니다. +memoryUserDatabase.writeException=[{0}]ì— ë°ì´í„°ë¥¼ 쓰는 중 IOException ë°œìƒ +memoryUserDatabase.xmlFeatureEncoding=XML 파ì¼ë“¤ ë‚´ì—ì„œ ìžë°” ì¸ì½”딩 ì´ë¦„ë“¤ì„ í—ˆìš©í•˜ê¸° 위하여, Digester를 ì„¤ì •í•˜ë˜ ì¤‘ 예외 ë°œìƒ. ì˜¤ì§ IANA ì¸ì½”딩 ì´ë¦„들만 지ì›ë  것입니다. diff --git a/java/org/apache/catalina/users/LocalStrings_ru.properties b/java/org/apache/catalina/users/LocalStrings_ru.properties new file mode 100644 index 0000000..020ff41 --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +memoryUserDatabase.nullRole=Указано null или Ð¸Ð¼Ñ Ñ€Ð¾Ð»Ð¸ нулевой длины. Роль будет проигнорирована diff --git a/java/org/apache/catalina/users/LocalStrings_zh_CN.properties b/java/org/apache/catalina/users/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..8388a2e --- /dev/null +++ b/java/org/apache/catalina/users/LocalStrings_zh_CN.properties @@ -0,0 +1,32 @@ +# 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. + +dataSourceUserDatabase.exception=访问数æ®åº“异常 + +memoryUserDatabase.fileClose=关闭 [{0}] 失败 +memoryUserDatabase.fileDelete=无法删除 [{0}] +memoryUserDatabase.fileNotFound=指定用户数æ®åº“[{0}]未找到 +memoryUserDatabase.notPersistable=用户数æ®åº“ä¸å¯æŒä¹…化 - 对目录没有写入æƒé™ +memoryUserDatabase.nullGroup=指定的组å为空或零长度。将忽略组。 +memoryUserDatabase.nullRole=指定的角色å为Null或ç€é•¿åº¦ä¸º0。角色将被忽略, +memoryUserDatabase.nullUser=Null或者零长度的用户å称,忽略这个用户。 +memoryUserDatabase.readOnly=用户数æ®åº“已被设为åªè¯»ã€‚修改无法ä¿å­˜ +memoryUserDatabase.reload=从更新的字眼[{1}]é‡æ–°åŠ è½½å†…存用户数æ®åº“ +memoryUserDatabase.reloadError=从更新åŽçš„æº [{1}] é‡æ–°åŠ è½½å†…存用户数æ®åº“ [{0}] 时出错 +memoryUserDatabase.renameNew=无法将新文件é‡å‘½å为 [{0}] +memoryUserDatabase.renameOld=原文件无法改å为[{0}] +memoryUserDatabase.restoreOrig=无法往原始文件中ä¿å­˜[{0}] +memoryUserDatabase.writeException=å‘[{0}]写入IOException +memoryUserDatabase.xmlFeatureEncoding=é…ç½®digester以å…许在XML文件中使用javaç¼–ç å称时å‘生异常。åªæ”¯æŒIANAç¼–ç å称。 diff --git a/java/org/apache/catalina/users/MemoryGroup.java b/java/org/apache/catalina/users/MemoryGroup.java new file mode 100644 index 0000000..7f5d90e --- /dev/null +++ b/java/org/apache/catalina/users/MemoryGroup.java @@ -0,0 +1,71 @@ +/* + * 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.catalina.users; + + +import org.apache.catalina.UserDatabase; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.security.Escape; + +/** + *

    Concrete implementation of {@link org.apache.catalina.Group} for the + * {@link MemoryUserDatabase} implementation of {@link UserDatabase}.

    + * + * @author Craig R. McClanahan + * @since 4.1 + * @deprecated Use {@link GenericGroup} instead. + */ +@Deprecated +public class MemoryGroup extends GenericGroup { + + + /** + * Package-private constructor used by the factory method in + * {@link MemoryUserDatabase}. + * + * @param database The {@link MemoryUserDatabase} that owns this group + * @param groupname Group name of this group + * @param description Description of this group + */ + MemoryGroup(MemoryUserDatabase database, + String groupname, String description) { + super(database, groupname, description, null); + } + + + /** + *

    Return a String representation of this group in XML format.

    + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(" Escape.xml(x.getRolename()), rsb); + sb.append(rsb); + sb.append("\""); + sb.append("/>"); + return sb.toString(); + } +} diff --git a/java/org/apache/catalina/users/MemoryRole.java b/java/org/apache/catalina/users/MemoryRole.java new file mode 100644 index 0000000..08f6cec --- /dev/null +++ b/java/org/apache/catalina/users/MemoryRole.java @@ -0,0 +1,67 @@ +/* + * 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.catalina.users; + + +import org.apache.catalina.UserDatabase; +import org.apache.tomcat.util.security.Escape; + +/** + *

    Concrete implementation of {@link org.apache.catalina.Role} for the + * {@link MemoryUserDatabase} implementation of {@link UserDatabase}.

    + * + * @author Craig R. McClanahan + * @since 4.1 + * @deprecated Use {@link GenericRole} instead. + */ +@Deprecated +public class MemoryRole extends GenericRole { + + + /** + * Package-private constructor used by the factory method in + * {@link MemoryUserDatabase}. + * + * @param database The {@link MemoryUserDatabase} that owns this role + * @param rolename Role name of this role + * @param description Description of this role + */ + MemoryRole(MemoryUserDatabase database, + String rolename, String description) { + super(database, rolename, description); + } + + + /** + *

    Return a String representation of this role in XML format.

    + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(""); + return sb.toString(); + } + + +} diff --git a/java/org/apache/catalina/users/MemoryUser.java b/java/org/apache/catalina/users/MemoryUser.java new file mode 100644 index 0000000..4d241fe --- /dev/null +++ b/java/org/apache/catalina/users/MemoryUser.java @@ -0,0 +1,105 @@ +/* + * 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.catalina.users; + + +import org.apache.catalina.UserDatabase; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.security.Escape; + +/** + *

    Concrete implementation of {@link org.apache.catalina.User} for the + * {@link MemoryUserDatabase} implementation of {@link UserDatabase}.

    + * + * @author Craig R. McClanahan + * @since 4.1 + * @deprecated Use {@link GenericUser} instead. + */ +@Deprecated +public class MemoryUser extends GenericUser { + + + /** + * Package-private constructor used by the factory method in + * {@link MemoryUserDatabase}. + * + * @param database The {@link MemoryUserDatabase} that owns this user + * @param username Logon username of the new user + * @param password Logon password of the new user + * @param fullName Full name of the new user + */ + MemoryUser(MemoryUserDatabase database, String username, + String password, String fullName) { + super(database, username, password, fullName, null, null); + } + + + /** + *

    Return a String representation of this user in XML format.

    + * + *

    IMPLEMENTATION NOTE - For backwards compatibility, + * the reader that processes this entry will accept either + * username or name for the username + * property.

    + * @return the XML representation + */ + public String toXml() { + + StringBuilder sb = new StringBuilder(" Escape.xml(x.getGroupname()), sb); + sb.append("\""); + sb.append(" roles=\""); + StringUtils.join(roles, ',', (x) -> Escape.xml(x.getRolename()), sb); + sb.append("\""); + sb.append("/>"); + return sb.toString(); + } + + + /** + *

    Return a String representation of this user.

    + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("User username=\""); + sb.append(Escape.xml(username)); + sb.append("\""); + if (fullName != null) { + sb.append(", fullName=\""); + sb.append(Escape.xml(fullName)); + sb.append("\""); + } + sb.append(", groups=\""); + StringUtils.join(groups, ',', (x) -> Escape.xml(x.getGroupname()), sb); + sb.append("\""); + sb.append(", roles=\""); + StringUtils.join(roles, ',', (x) -> Escape.xml(x.getRolename()), sb); + sb.append("\""); + return sb.toString(); + } +} diff --git a/java/org/apache/catalina/users/MemoryUserDatabase.java b/java/org/apache/catalina/users/MemoryUserDatabase.java new file mode 100644 index 0000000..292ea3d --- /dev/null +++ b/java/org/apache/catalina/users/MemoryUserDatabase.java @@ -0,0 +1,925 @@ +/* + * 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.catalina.users; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.catalina.Globals; +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.catalina.UserDatabase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.digester.AbstractObjectCreationFactory; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; +import org.xml.sax.Attributes; + +/** + * Concrete implementation of {@link UserDatabase} that loads all defined users, + * groups, and roles into an in-memory data structure, and uses a specified XML + * file for its persistent storage. + *

    + * This class is thread-safe. + *

    + * This class does not enforce what, in an RDBMS, would be called referential + * integrity. Concurrent modifications may result in inconsistent data such as + * a User retaining a reference to a Role that has been removed from the + * database. + * + * @author Craig R. McClanahan + * @since 4.1 + */ +/* + * Implementation notes: + * + * Any operation that acts on a single element of the database (e.g. operations + * that create, read, update or delete a user, role or group) must first obtain + * the read lock. Operations that return iterators for users, roles or groups + * also fall into this category. + * + * Iterators must always be created from copies of the data to prevent possible + * corruption of the iterator due to the remove of all elements from the + * underlying Map that would occur during a subsequent re-loading of the + * database. + * + * Any operation that acts on multiple elements and expects the database to + * remain consistent during the operation (e.g. saving or loading the database) + * must first obtain the write lock. + */ +public class MemoryUserDatabase implements UserDatabase { + + private static final Log log = LogFactory.getLog(MemoryUserDatabase.class); + private static final StringManager sm = StringManager.getManager(MemoryUserDatabase.class); + + + // ----------------------------------------------------------- Constructors + + /** + * Create a new instance with default values. + */ + public MemoryUserDatabase() { + this(null); + } + + + /** + * Create a new instance with the specified values. + * + * @param id Unique global identifier of this user database + */ + public MemoryUserDatabase(String id) { + this.id = id; + } + + // ----------------------------------------------------- Instance Variables + + /** + * The set of {@link Group}s defined in this database, keyed by group name. + */ + protected final Map groups = new ConcurrentHashMap<>(); + + /** + * The unique global identifier of this user database. + */ + protected final String id; + + /** + * The relative (to catalina.base) or absolute pathname to the + * XML file in which we will save our persistent information. + */ + protected String pathname = "conf/tomcat-users.xml"; + + /** + * The relative or absolute pathname to the file in which our old + * information is stored while renaming is in progress. + */ + protected String pathnameOld = pathname + ".old"; + + /** + * The relative or absolute pathname of the file in which we write our new + * information prior to renaming. + */ + protected String pathnameNew = pathname + ".new"; + + /** + * A flag, indicating if the user database is read only. + */ + protected boolean readonly = true; + + /** + * The set of {@link Role}s defined in this database, keyed by role name. + */ + protected final Map roles = new ConcurrentHashMap<>(); + + /** + * The set of {@link User}s defined in this database, keyed by user name. + */ + protected final Map users = new ConcurrentHashMap<>(); + + private final ReentrantReadWriteLock dbLock = new ReentrantReadWriteLock(); + private final Lock readLock = dbLock.readLock(); + private final Lock writeLock = dbLock.writeLock(); + + private volatile long lastModified = 0; + private boolean watchSource = true; + + + // ------------------------------------------------------------- Properties + + /** + * @return the set of {@link Group}s defined in this user database. + */ + @Override + public Iterator getGroups() { + readLock.lock(); + try { + return new ArrayList<>(groups.values()).iterator(); + } finally { + readLock.unlock(); + } + } + + + /** + * @return the unique global identifier of this user database. + */ + @Override + public String getId() { + return this.id; + } + + + /** + * @return the relative or absolute pathname to the persistent storage file. + */ + public String getPathname() { + return this.pathname; + } + + + /** + * Set the relative or absolute pathname to the persistent storage file. + * + * @param pathname The new pathname + */ + public void setPathname(String pathname) { + this.pathname = pathname; + this.pathnameOld = pathname + ".old"; + this.pathnameNew = pathname + ".new"; + } + + + /** + * @return the readonly status of the user database + */ + public boolean getReadonly() { + return this.readonly; + } + + + /** + * Setting the readonly status of the user database + * + * @param readonly the new status + */ + public void setReadonly(boolean readonly) { + this.readonly = readonly; + } + + + public boolean getWatchSource() { + return watchSource; + } + + + + public void setWatchSource(boolean watchSource) { + this.watchSource = watchSource; + } + + + /** + * @return the set of {@link Role}s defined in this user database. + */ + @Override + public Iterator getRoles() { + readLock.lock(); + try { + return new ArrayList<>(roles.values()).iterator(); + } finally { + readLock.unlock(); + } + } + + + /** + * @return the set of {@link User}s defined in this user database. + */ + @Override + public Iterator getUsers() { + readLock.lock(); + try { + return new ArrayList<>(users.values()).iterator(); + } finally { + readLock.unlock(); + } + } + + + // --------------------------------------------------------- Public Methods + + /** + * Finalize access to this user database. + * + * @exception Exception if any exception is thrown during closing + */ + @Override + public void close() throws Exception { + + writeLock.lock(); + try { + save(); + users.clear(); + groups.clear(); + roles.clear(); + } finally { + writeLock.unlock(); + } + } + + + /** + * Create and return a new {@link Group} defined in this user database. + * + * @param groupname The group name of the new group (must be unique) + * @param description The description of this group + */ + @Override + public Group createGroup(String groupname, String description) { + if (groupname == null || groupname.length() == 0) { + String msg = sm.getString("memoryUserDatabase.nullGroup"); + log.warn(msg); + throw new IllegalArgumentException(msg); + } + + Group group = new GenericGroup<>(this, groupname, description, null); + readLock.lock(); + try { + groups.put(group.getGroupname(), group); + } finally { + readLock.unlock(); + } + return group; + } + + + /** + * Create and return a new {@link Role} defined in this user database. + * + * @param rolename The role name of the new group (must be unique) + * @param description The description of this group + */ + @Override + public Role createRole(String rolename, String description) { + if (rolename == null || rolename.length() == 0) { + String msg = sm.getString("memoryUserDatabase.nullRole"); + log.warn(msg); + throw new IllegalArgumentException(msg); + } + + Role role = new GenericRole<>(this, rolename, description); + readLock.lock(); + try { + roles.put(role.getRolename(), role); + } finally { + readLock.unlock(); + } + return role; + } + + + /** + * Create and return a new {@link User} defined in this user database. + * + * @param username The logon username of the new user (must be unique) + * @param password The logon password of the new user + * @param fullName The full name of the new user + */ + @Override + public User createUser(String username, String password, String fullName) { + + if (username == null || username.length() == 0) { + String msg = sm.getString("memoryUserDatabase.nullUser"); + log.warn(msg); + throw new IllegalArgumentException(msg); + } + + User user = new GenericUser<>(this, username, password, fullName, null, null); + readLock.lock(); + try { + users.put(user.getUsername(), user); + } finally { + readLock.unlock(); + } + return user; + } + + + /** + * Return the {@link Group} with the specified group name, if any; otherwise + * return null. + * + * @param groupname Name of the group to return + */ + @Override + public Group findGroup(String groupname) { + readLock.lock(); + try { + return groups.get(groupname); + } finally { + readLock.unlock(); + } + } + + + /** + * Return the {@link Role} with the specified role name, if any; otherwise + * return null. + * + * @param rolename Name of the role to return + */ + @Override + public Role findRole(String rolename) { + readLock.lock(); + try { + return roles.get(rolename); + } finally { + readLock.unlock(); + } + } + + + /** + * Return the {@link User} with the specified user name, if any; otherwise + * return null. + * + * @param username Name of the user to return + */ + @Override + public User findUser(String username) { + readLock.lock(); + try { + return users.get(username); + } finally { + readLock.unlock(); + } + } + + + /** + * Initialize access to this user database. + * + * @exception Exception if any exception is thrown during opening + */ + @Override + public void open() throws Exception { + writeLock.lock(); + try { + // Erase any previous groups and users + users.clear(); + groups.clear(); + roles.clear(); + + String pathName = getPathname(); + try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getResource(pathName)) { + lastModified = resource.getLastModified(); + + // Construct a digester to read the XML input file + Digester digester = new Digester(); + try { + digester.setFeature( + "http://apache.org/xml/features/allow-java-encodings", true); + } catch (Exception e) { + log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding"), e); + } + digester.addFactoryCreate("tomcat-users/group", + new MemoryGroupCreationFactory(this), true); + digester.addFactoryCreate("tomcat-users/role", + new MemoryRoleCreationFactory(this), true); + digester.addFactoryCreate("tomcat-users/user", + new MemoryUserCreationFactory(this), true); + + // Parse the XML input to load this database + digester.parse(resource.getInputStream()); + } catch (IOException ioe) { + log.error(sm.getString("memoryUserDatabase.fileNotFound", pathName)); + } catch (Exception e) { + // Fail safe on error + users.clear(); + groups.clear(); + roles.clear(); + throw e; + } + } finally { + writeLock.unlock(); + } + } + + + /** + * Remove the specified {@link Group} from this user database. + * + * @param group The group to be removed + */ + @Override + public void removeGroup(Group group) { + readLock.lock(); + try { + Iterator users = getUsers(); + while (users.hasNext()) { + User user = users.next(); + user.removeGroup(group); + } + groups.remove(group.getGroupname()); + } finally { + readLock.unlock(); + } + } + + + /** + * Remove the specified {@link Role} from this user database. + * + * @param role The role to be removed + */ + @Override + public void removeRole(Role role) { + readLock.lock(); + try { + Iterator groups = getGroups(); + while (groups.hasNext()) { + Group group = groups.next(); + group.removeRole(role); + } + Iterator users = getUsers(); + while (users.hasNext()) { + User user = users.next(); + user.removeRole(role); + } + roles.remove(role.getRolename()); + } finally { + readLock.unlock(); + } + } + + + /** + * Remove the specified {@link User} from this user database. + * + * @param user The user to be removed + */ + @Override + public void removeUser(User user) { + readLock.lock(); + try { + users.remove(user.getUsername()); + } finally { + readLock.unlock(); + } + } + + + /** + * Check for permissions to save this user database to persistent storage + * location. + * + * @return true if the database is writable + */ + public boolean isWritable() { + + File file = new File(pathname); + if (!file.isAbsolute()) { + file = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathname); + } + File dir = file.getParentFile(); + return dir.exists() && dir.isDirectory() && dir.canWrite(); + } + + + /** + * Save any updated information to the persistent storage location for this + * user database. + * + * @exception Exception if any exception is thrown during saving + */ + @Override + public void save() throws Exception { + + if (getReadonly()) { + log.error(sm.getString("memoryUserDatabase.readOnly")); + return; + } + + if (!isWritable()) { + log.warn(sm.getString("memoryUserDatabase.notPersistable")); + return; + } + + // Write out contents to a temporary file + File fileNew = new File(pathnameNew); + if (!fileNew.isAbsolute()) { + fileNew = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathnameNew); + } + + writeLock.lock(); + try { + try (FileOutputStream fos = new FileOutputStream(fileNew); + OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); + PrintWriter writer = new PrintWriter(osw)) { + + // Print the file prolog + writer.println(""); + writer.println(""); + + // Print entries for each defined role, group, and user + Iterator values = null; + values = getRoles(); + while(values.hasNext()) { + Role role = (Role)values.next(); + writer.print(" "); + } + values = getGroups(); + while (values.hasNext()) { + Group group = (Group)values.next(); + writer.print(" roles=group.getRoles(); roles.hasNext();) { + Role role = roles.next(); + writer.print(Escape.xml(role.getRolename())); + if(roles.hasNext()) { + writer.print(','); + } + } + writer.println("\"/>"); + } + + values = getUsers(); + while (values.hasNext()) { + User user = (User)values.next(); + writer.print(" groups=user.getGroups(); groups.hasNext();) { + Group group = groups.next(); + writer.print(Escape.xml(group.getGroupname())); + if(groups.hasNext()) { + writer.print(','); + } + } + writer.print("\" roles=\""); + for (Iterator roles=user.getRoles(); roles.hasNext();) { + Role role = roles.next(); + writer.print(Escape.xml(role.getRolename())); + if(roles.hasNext()) { + writer.print(','); + } + } + writer.print("\"/>"); + } + + // Print the file epilog + writer.println(""); + + // Check for errors that occurred while printing + if (writer.checkError()) { + throw new IOException(sm.getString("memoryUserDatabase.writeException", + fileNew.getAbsolutePath())); + } + } catch (IOException e) { + if (fileNew.exists() && !fileNew.delete()) { + log.warn(sm.getString("memoryUserDatabase.fileDelete", fileNew)); + } + throw e; + } + this.lastModified = fileNew.lastModified(); + } finally { + writeLock.unlock(); + } + + // Perform the required renames to permanently save this file + File fileOld = new File(pathnameOld); + if (!fileOld.isAbsolute()) { + fileOld = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathnameOld); + } + if (fileOld.exists() && !fileOld.delete()) { + throw new IOException(sm.getString("memoryUserDatabase.fileDelete", fileOld)); + } + File fileOrig = new File(pathname); + if (!fileOrig.isAbsolute()) { + fileOrig = new File(System.getProperty(Globals.CATALINA_BASE_PROP), pathname); + } + if (fileOrig.exists()) { + if (!fileOrig.renameTo(fileOld)) { + throw new IOException(sm.getString("memoryUserDatabase.renameOld", + fileOld.getAbsolutePath())); + } + } + if (!fileNew.renameTo(fileOrig)) { + if (fileOld.exists()) { + if (!fileOld.renameTo(fileOrig)) { + log.warn(sm.getString("memoryUserDatabase.restoreOrig", fileOld)); + } + } + throw new IOException(sm.getString("memoryUserDatabase.renameNew", + fileOrig.getAbsolutePath())); + } + if (fileOld.exists() && !fileOld.delete()) { + throw new IOException(sm.getString("memoryUserDatabase.fileDelete", fileOld)); + } + } + + + @Override + public void backgroundProcess() { + if (!watchSource) { + return; + } + + URI uri = ConfigFileLoader.getSource().getURI(getPathname()); + URLConnection uConn = null; + try { + URL url = uri.toURL(); + uConn = url.openConnection(); + + if (this.lastModified != uConn.getLastModified()) { + writeLock.lock(); + try { + long detectedLastModified = uConn.getLastModified(); + // Last modified as a resolution of 1s. Ensure that a write + // to the file is not in progress by ensuring that the last + // modified time is at least 2 seconds ago. + if (this.lastModified != detectedLastModified && + detectedLastModified + 2000 < System.currentTimeMillis()) { + log.info(sm.getString("memoryUserDatabase.reload", id, uri)); + open(); + } + } finally { + writeLock.unlock(); + } + } + } catch (Exception ioe) { + log.error(sm.getString("memoryUserDatabase.reloadError", id, uri), ioe); + } finally { + if (uConn != null) { + try { + // Can't close a uConn directly. Have to do it like this. + uConn.getInputStream().close(); + } catch (FileNotFoundException fnfe) { + // The file doesn't exist. + // This has been logged above. No need to log again. + // Set the last modified time to avoid repeated log messages + this.lastModified = 0; + } catch (IOException ioe) { + log.warn(sm.getString("memoryUserDatabase.fileClose", pathname), ioe); + } + } + } + } + + + /** + * Return a String representation of this UserDatabase. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MemoryUserDatabase[id="); + sb.append(this.id); + sb.append(",pathname="); + sb.append(pathname); + sb.append(",groupCount="); + sb.append(this.groups.size()); + sb.append(",roleCount="); + sb.append(this.roles.size()); + sb.append(",userCount="); + sb.append(this.users.size()); + sb.append(']'); + return sb.toString(); + } +} + + +/** + * Digester object creation factory for group instances. + */ +class MemoryGroupCreationFactory extends AbstractObjectCreationFactory { + + MemoryGroupCreationFactory(MemoryUserDatabase database) { + this.database = database; + } + + + @Override + public Object createObject(Attributes attributes) { + String groupname = attributes.getValue("groupname"); + if (groupname == null) { + groupname = attributes.getValue("name"); + } + String description = attributes.getValue("description"); + String roles = attributes.getValue("roles"); + Group group = database.findGroup(groupname); + if (group == null) { + group = database.createGroup(groupname, description); + } else { + if (group.getDescription() == null) { + group.setDescription(description); + } + } + if (roles != null) { + while (roles.length() > 0) { + String rolename = null; + int comma = roles.indexOf(','); + if (comma >= 0) { + rolename = roles.substring(0, comma).trim(); + roles = roles.substring(comma + 1); + } else { + rolename = roles.trim(); + roles = ""; + } + if (rolename.length() > 0) { + Role role = database.findRole(rolename); + if (role == null) { + role = database.createRole(rolename, null); + } + group.addRole(role); + } + } + } + return group; + } + + private final MemoryUserDatabase database; +} + + +/** + * Digester object creation factory for role instances. + */ +class MemoryRoleCreationFactory extends AbstractObjectCreationFactory { + + MemoryRoleCreationFactory(MemoryUserDatabase database) { + this.database = database; + } + + + @Override + public Object createObject(Attributes attributes) { + String rolename = attributes.getValue("rolename"); + if (rolename == null) { + rolename = attributes.getValue("name"); + } + String description = attributes.getValue("description"); + Role existingRole = database.findRole(rolename); + if (existingRole == null) { + return database.createRole(rolename, description); + } + if (existingRole.getDescription() == null) { + existingRole.setDescription(description); + } + return existingRole; + } + + private final MemoryUserDatabase database; +} + + +/** + * Digester object creation factory for user instances. + */ +class MemoryUserCreationFactory extends AbstractObjectCreationFactory { + + MemoryUserCreationFactory(MemoryUserDatabase database) { + this.database = database; + } + + + @Override + public Object createObject(Attributes attributes) { + String username = attributes.getValue("username"); + if (username == null) { + username = attributes.getValue("name"); + } + String password = attributes.getValue("password"); + String fullName = attributes.getValue("fullName"); + if (fullName == null) { + fullName = attributes.getValue("fullname"); + } + String groups = attributes.getValue("groups"); + String roles = attributes.getValue("roles"); + User user = database.createUser(username, password, fullName); + if (groups != null) { + while (groups.length() > 0) { + String groupname = null; + int comma = groups.indexOf(','); + if (comma >= 0) { + groupname = groups.substring(0, comma).trim(); + groups = groups.substring(comma + 1); + } else { + groupname = groups.trim(); + groups = ""; + } + if (groupname.length() > 0) { + Group group = database.findGroup(groupname); + if (group == null) { + group = database.createGroup(groupname, null); + } + user.addGroup(group); + } + } + } + if (roles != null) { + while (roles.length() > 0) { + String rolename = null; + int comma = roles.indexOf(','); + if (comma >= 0) { + rolename = roles.substring(0, comma).trim(); + roles = roles.substring(comma + 1); + } else { + rolename = roles.trim(); + roles = ""; + } + if (rolename.length() > 0) { + Role role = database.findRole(rolename); + if (role == null) { + role = database.createRole(rolename, null); + } + user.addRole(role); + } + } + } + return user; + } + + private final MemoryUserDatabase database; +} diff --git a/java/org/apache/catalina/users/MemoryUserDatabaseFactory.java b/java/org/apache/catalina/users/MemoryUserDatabaseFactory.java new file mode 100644 index 0000000..4e5bbee --- /dev/null +++ b/java/org/apache/catalina/users/MemoryUserDatabaseFactory.java @@ -0,0 +1,115 @@ +/* + * 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.catalina.users; + + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + + +/** + *

    JNDI object creation factory for MemoryUserDatabase + * instances. This makes it convenient to configure a user database + * in the global JNDI resources associated with this Catalina instance, + * and then link to that resource for web applications that administer + * the contents of the user database.

    + * + *

    The MemoryUserDatabase instance is configured based + * on the following parameter values:

    + *
      + *
    • pathname - Absolute or relative (to the directory + * path specified by the catalina.base system property) + * pathname to the XML file from which our user information is loaded, + * and to which it is stored. [conf/tomcat-users.xml]
    • + *
    + * + * @author Craig R. McClanahan + * @since 4.1 + */ +public class MemoryUserDatabaseFactory implements ObjectFactory { + + + // --------------------------------------------------------- Public Methods + + + /** + *

    Create and return a new MemoryUserDatabase instance + * that has been configured according to the properties of the + * specified Reference. If you instance can be created, + * return null instead.

    + * + * @param obj The possibly null object containing location or + * reference information that can be used in creating an object + * @param name The name of this object relative to nameCtx + * @param nameCtx The context relative to which the name + * parameter is specified, or null if name + * is relative to the default initial context + * @param environment The possibly null environment that is used in + * creating this object + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) + throws Exception { + + // We only know how to deal with javax.naming.References + // that specify a class name of "org.apache.catalina.UserDatabase" + if ((obj == null) || !(obj instanceof Reference)) { + return null; + } + Reference ref = (Reference) obj; + if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) { + return null; + } + + // Create and configure a MemoryUserDatabase instance based on the + // RefAddr values associated with this Reference + MemoryUserDatabase database = new MemoryUserDatabase(name.toString()); + RefAddr ra = null; + + ra = ref.get("pathname"); + if (ra != null) { + database.setPathname(ra.getContent().toString()); + } + + ra = ref.get("readonly"); + if (ra != null) { + database.setReadonly(Boolean.parseBoolean(ra.getContent().toString())); + } + + ra = ref.get("watchSource"); + if (ra != null) { + database.setWatchSource(Boolean.parseBoolean(ra.getContent().toString())); + } + + // Return the configured database instance + database.open(); + // Don't try something we know won't work + if (!database.getReadonly()) { + database.save(); + } + return database; + + } + + +} diff --git a/java/org/apache/catalina/users/SparseUserDatabase.java b/java/org/apache/catalina/users/SparseUserDatabase.java new file mode 100644 index 0000000..c0bc423 --- /dev/null +++ b/java/org/apache/catalina/users/SparseUserDatabase.java @@ -0,0 +1,29 @@ +/* + * 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.catalina.users; + +import org.apache.catalina.UserDatabase; + +public abstract class SparseUserDatabase implements UserDatabase { + + @Override + public boolean isSparse() { + return true; + } + + +} diff --git a/java/org/apache/catalina/users/mbeans-descriptors.xml b/java/org/apache/catalina/users/mbeans-descriptors.xml new file mode 100644 index 0000000..d0dd524 --- /dev/null +++ b/java/org/apache/catalina/users/mbeans-descriptors.xml @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/util/CharsetMapper.java b/java/org/apache/catalina/util/CharsetMapper.java new file mode 100644 index 0000000..c04d1c5 --- /dev/null +++ b/java/org/apache/catalina/util/CharsetMapper.java @@ -0,0 +1,135 @@ +/* + * 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.catalina.util; + + +import java.io.InputStream; +import java.util.Locale; +import java.util.Properties; + +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.compat.JreCompat; + + + +/** + * Utility class that attempts to map from a Locale to the corresponding + * character set to be used for interpreting input text (or generating + * output text) when the Content-Type header does not include one. You + * can customize the behavior of this class by modifying the mapping data + * it loads, or by subclassing it (to change the algorithm) and then using + * your own version for a particular web application. + * + * @author Craig R. McClanahan + */ +public class CharsetMapper { + + + // ---------------------------------------------------- Manifest Constants + + + /** + * Default properties resource name. + */ + public static final String DEFAULT_RESOURCE = + "/org/apache/catalina/util/CharsetMapperDefault.properties"; + + + // ---------------------------------------------------------- Constructors + + + /** + * Construct a new CharsetMapper using the default properties resource. + */ + public CharsetMapper() { + this(DEFAULT_RESOURCE); + } + + + /** + * Construct a new CharsetMapper using the specified properties resource. + * + * @param name Name of a properties resource to be loaded + * + * @exception IllegalArgumentException if the specified properties + * resource could not be loaded for any reason. + */ + public CharsetMapper(String name) { + if (JreCompat.isGraalAvailable()) { + map.put("en", "ISO-8859-1"); + } else { + try (InputStream stream = this.getClass().getResourceAsStream(name)) { + map.load(stream); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + throw new IllegalArgumentException(t); + } + } + } + + + // ---------------------------------------------------- Instance Variables + + + /** + * The mapping properties that have been initialized from the specified or + * default properties resource. + */ + private Properties map = new Properties(); + + + // ------------------------------------------------------- Public Methods + + + /** + * Calculate the name of a character set to be assumed, given the specified + * Locale and the absence of a character set specified as part of the + * content type header. + * + * @param locale The locale for which to calculate a character set + * @return the charset name + */ + public String getCharset(Locale locale) { + // Match full language_country_variant first, then language_country, + // then language only + String charset = map.getProperty(locale.toString()); + if (charset == null) { + charset = map.getProperty(locale.getLanguage() + "_" + + locale.getCountry()); + if (charset == null) { + charset = map.getProperty(locale.getLanguage()); + } + } + return charset; + } + + + /** + * The deployment descriptor can have a + * locale-encoding-mapping-list element which describes the + * webapp's desired mapping from locale to charset. This method + * gets called when processing the web.xml file for a context + * + * @param locale The locale for a character set + * @param charset The charset to be associated with the locale + */ + public void addCharsetMappingFromDeploymentDescriptor(String locale, String charset) { + map.put(locale, charset); + } + + +} diff --git a/java/org/apache/catalina/util/CharsetMapperDefault.properties b/java/org/apache/catalina/util/CharsetMapperDefault.properties new file mode 100644 index 0000000..6f8bf49 --- /dev/null +++ b/java/org/apache/catalina/util/CharsetMapperDefault.properties @@ -0,0 +1,17 @@ +# 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. + +en=ISO-8859-1 +fr=ISO-8859-1 diff --git a/java/org/apache/catalina/util/ContextName.java b/java/org/apache/catalina/util/ContextName.java new file mode 100644 index 0000000..f426d41 --- /dev/null +++ b/java/org/apache/catalina/util/ContextName.java @@ -0,0 +1,201 @@ +/* + * 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.catalina.util; + +import java.util.Locale; + +/** + * Utility class to manage context names so there is one place where the + * conversions between baseName, path and version take place. + */ +public final class ContextName { + public static final String ROOT_NAME = "ROOT"; + private static final String VERSION_MARKER = "##"; + private static final char FWD_SLASH_REPLACEMENT = '#'; + + private final String baseName; + private final String path; + private final String version; + private final String name; + + + /** + * Creates an instance from a context name, display name, base name, + * directory name, WAR name or context.xml name. + * + * @param name The name to use as the basis for this object + * @param stripFileExtension If a .war or .xml file extension is present + * at the end of the provided name should it be + * removed? + */ + public ContextName(String name, boolean stripFileExtension) { + + String tmp1 = name; + + // Convert Context names and display names to base names + + // Strip off any leading "/" + if (tmp1.startsWith("/")) { + tmp1 = tmp1.substring(1); + } + + // Replace any remaining / + tmp1 = tmp1.replace('/', FWD_SLASH_REPLACEMENT); + + // Insert the ROOT name if required + if (tmp1.startsWith(VERSION_MARKER) || tmp1.isEmpty()) { + tmp1 = ROOT_NAME + tmp1; + } + + // Remove any file extensions + if (stripFileExtension && + (tmp1.toLowerCase(Locale.ENGLISH).endsWith(".war") || + tmp1.toLowerCase(Locale.ENGLISH).endsWith(".xml"))) { + tmp1 = tmp1.substring(0, tmp1.length() -4); + } + + baseName = tmp1; + + String tmp2; + // Extract version number + int versionIndex = baseName.indexOf(VERSION_MARKER); + if (versionIndex > -1) { + version = baseName.substring(versionIndex + 2); + tmp2 = baseName.substring(0, versionIndex); + } else { + version = ""; + tmp2 = baseName; + } + + if (ROOT_NAME.equals(tmp2)) { + path = ""; + } else { + path = "/" + tmp2.replace(FWD_SLASH_REPLACEMENT, '/'); + } + + if (versionIndex > -1) { + this.name = path + VERSION_MARKER + version; + } else { + this.name = path; + } + } + + /** + * Construct an instance from a path and version. + * + * @param path Context path to use + * @param version Context version to use + */ + public ContextName(String path, String version) { + // Path should never be null, '/' or '/ROOT' + if (path == null || "/".equals(path) || "/ROOT".equals(path)) { + this.path = ""; + } else { + this.path = path; + } + + // Version should never be null + if (version == null) { + this.version = ""; + } else { + this.version = version; + } + + // Name is path + version + if (this.version.isEmpty()) { + name = this.path; + } else { + name = this.path + VERSION_MARKER + this.version; + } + + // Base name is converted path + version + StringBuilder tmp = new StringBuilder(); + if (this.path.isEmpty()) { + tmp.append(ROOT_NAME); + } else { + tmp.append(this.path.substring(1).replace('/', + FWD_SLASH_REPLACEMENT)); + } + if (!this.version.isEmpty()) { + tmp.append(VERSION_MARKER); + tmp.append(this.version); + } + this.baseName = tmp.toString(); + } + + public String getBaseName() { + return baseName; + } + + public String getPath() { + return path; + } + + public String getVersion() { + return version; + } + + public String getName() { + return name; + } + + public String getDisplayName() { + StringBuilder tmp = new StringBuilder(); + if ("".equals(path)) { + tmp.append('/'); + } else { + tmp.append(path); + } + + if (!version.isEmpty()) { + tmp.append(VERSION_MARKER); + tmp.append(version); + } + + return tmp.toString(); + } + + @Override + public String toString() { + return getDisplayName(); + } + + + /** + * Extract the final component of the given path which is assumed to be a + * base name and generate a {@link ContextName} from that base name. + * + * @param path The path that ends in a base name + * + * @return the {@link ContextName} generated from the given base name + */ + public static ContextName extractFromPath(String path) { + // Convert '\' to '/' + path = path.replace("\\", "/"); + // Remove trailing '/'. Use while just in case a value ends in /// + while (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + int lastSegment = path.lastIndexOf('/'); + if (lastSegment > 0) { + path = path.substring(lastSegment + 1); + } + + return new ContextName(path, true); + } +} diff --git a/java/org/apache/catalina/util/CustomObjectInputStream.java b/java/org/apache/catalina/util/CustomObjectInputStream.java new file mode 100644 index 0000000..c6f41cd --- /dev/null +++ b/java/org/apache/catalina/util/CustomObjectInputStream.java @@ -0,0 +1,195 @@ +/* + * 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.catalina.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; +import java.lang.reflect.Proxy; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.res.StringManager; + +/** + * Custom subclass of ObjectInputStream that loads from the + * class loader for this web application. This allows classes defined only + * with the web application to be found correctly. + * + * @author Craig R. McClanahan + * @author Bip Thelin + */ +public final class CustomObjectInputStream extends ObjectInputStream { + + private static final StringManager sm = StringManager.getManager(CustomObjectInputStream.class); + + private static final WeakHashMap> reportedClassCache = + new WeakHashMap<>(); + + /** + * The class loader we will use to resolve classes. + */ + private final ClassLoader classLoader; + private final Set reportedClasses; + private final Log log; + + private final Pattern allowedClassNamePattern; + private final String allowedClassNameFilter; + private final boolean warnOnFailure; + + + /** + * Construct a new instance of CustomObjectInputStream without any filtering + * of deserialized classes. + * + * @param stream The input stream we will read from + * @param classLoader The class loader used to instantiate objects + * + * @exception IOException if an input/output error occurs + */ + public CustomObjectInputStream(InputStream stream, ClassLoader classLoader) throws IOException { + this(stream, classLoader, null, null, false); + } + + + /** + * Construct a new instance of CustomObjectInputStream with filtering of + * deserialized classes. + * + * @param stream The input stream we will read from + * @param classLoader The class loader used to instantiate objects + * @param log The logger to use to report any issues. It may only be null if + * the filterMode does not require logging + * @param allowedClassNamePattern The regular expression to use to filter + * deserialized classes. The fully qualified + * class name must match this pattern for + * deserialization to be allowed if filtering + * is enabled. + * @param warnOnFailure Should any failures be logged? + * + * @exception IOException if an input/output error occurs + */ + public CustomObjectInputStream(InputStream stream, ClassLoader classLoader, + Log log, Pattern allowedClassNamePattern, boolean warnOnFailure) + throws IOException { + super(stream); + if (log == null && allowedClassNamePattern != null && warnOnFailure) { + throw new IllegalArgumentException( + sm.getString("customObjectInputStream.logRequired")); + } + this.classLoader = classLoader; + this.log = log; + this.allowedClassNamePattern = allowedClassNamePattern; + if (allowedClassNamePattern == null) { + this.allowedClassNameFilter = null; + } else { + this.allowedClassNameFilter = allowedClassNamePattern.toString(); + } + this.warnOnFailure = warnOnFailure; + + Set reportedClasses; + synchronized (reportedClassCache) { + reportedClasses = reportedClassCache.get(classLoader); + } + if (reportedClasses == null) { + reportedClasses = ConcurrentHashMap.newKeySet(); + Set original; + synchronized (reportedClassCache) { + original = reportedClassCache.putIfAbsent(classLoader, reportedClasses); + } + if (original != null) { + // Concurrent attempts to create the new Set. Make sure all + // threads use the first successfully added Set. + reportedClasses = original; + } + } + this.reportedClasses = reportedClasses; + } + + + /** + * Load the local class equivalent of the specified stream class + * description, by using the class loader assigned to this Context. + * + * @param classDesc Class description from the input stream + * + * @exception ClassNotFoundException if this class cannot be found + * @exception IOException if an input/output error occurs + */ + @Override + public Class resolveClass(ObjectStreamClass classDesc) + throws ClassNotFoundException, IOException { + + String name = classDesc.getName(); + if (allowedClassNamePattern != null) { + boolean allowed = allowedClassNamePattern.matcher(name).matches(); + if (!allowed) { + boolean doLog = warnOnFailure && reportedClasses.add(name); + String msg = sm.getString("customObjectInputStream.nomatch", name, allowedClassNameFilter); + if (doLog) { + log.warn(msg); + } else if (log.isDebugEnabled()) { + log.debug(msg); + } + throw new InvalidClassException(msg); + } + } + + try { + return Class.forName(name, false, classLoader); + } catch (ClassNotFoundException e) { + try { + // Try also the superclass because of primitive types + return super.resolveClass(classDesc); + } catch (ClassNotFoundException e2) { + // Rethrow original exception, as it can have more information + // about why the class was not found. BZ 48007 + throw e; + } + } + } + + + /** + * Return a proxy class that implements the interfaces named in a proxy + * class descriptor. Do this using the class loader assigned to this + * Context. + */ + @Override + protected Class resolveProxyClass(String[] interfaces) + throws IOException, ClassNotFoundException { + + Class[] cinterfaces = new Class[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + cinterfaces[i] = classLoader.loadClass(interfaces[i]); + } + + try { + // No way to avoid this at the moment + @SuppressWarnings("deprecation") + Class proxyClass = Proxy.getProxyClass(classLoader, cinterfaces); + return proxyClass; + } catch (IllegalArgumentException e) { + throw new ClassNotFoundException(null, e); + } + } +} diff --git a/java/org/apache/catalina/util/DOMWriter.java b/java/org/apache/catalina/util/DOMWriter.java new file mode 100644 index 0000000..96f6567 --- /dev/null +++ b/java/org/apache/catalina/util/DOMWriter.java @@ -0,0 +1,165 @@ +/* + * 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.catalina.util; + +import java.io.PrintWriter; +import java.io.Writer; + +import org.apache.tomcat.util.security.Escape; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * A DOM writer optimised for use by WebDAV. + */ +public class DOMWriter { + + private final PrintWriter out; + + + public DOMWriter(Writer writer) { + out = new PrintWriter(writer); + } + + + /** + * Prints the specified node, recursively. + * @param node The node to output + */ + public void print(Node node) { + + // is there anything to do? + if (node == null) { + return; + } + + int type = node.getNodeType(); + switch (type) { + // print document + case Node.DOCUMENT_NODE: + print(((Document) node).getDocumentElement()); + out.flush(); + break; + + // print element with attributes + case Node.ELEMENT_NODE: + out.print('<'); + out.print(node.getLocalName()); + Attr attrs[] = sortAttributes(node.getAttributes()); + for (Attr attr : attrs) { + out.print(' '); + out.print(attr.getLocalName()); + + out.print("=\""); + out.print(Escape.xml("", true, attr.getNodeValue())); + out.print('"'); + } + out.print('>'); + printChildren(node); + break; + + // handle entity reference nodes + case Node.ENTITY_REFERENCE_NODE: + printChildren(node); + break; + + // print cdata sections + case Node.CDATA_SECTION_NODE: + out.print(Escape.xml("", true, node.getNodeValue())); + break; + + // print text + case Node.TEXT_NODE: + out.print(Escape.xml("", true, node.getNodeValue())); + break; + + // print processing instruction + case Node.PROCESSING_INSTRUCTION_NODE: + out.print(" 0) { + out.print(' '); + out.print(data); + } + out.print("?>"); + break; + } + + if (type == Node.ELEMENT_NODE) { + out.print("'); + } + + out.flush(); + + } // print(Node) + + + private void printChildren(Node node) { + NodeList children = node.getChildNodes(); + if (children != null) { + int len = children.getLength(); + for (int i = 0; i < len; i++) { + print(children.item(i)); + } + } + } + + + /** + * Returns a sorted list of attributes. + * @param attrs The map to sort + * @return a sorted attribute array + */ + private Attr[] sortAttributes(NamedNodeMap attrs) { + if (attrs == null) { + return new Attr[0]; + } + + int len = attrs.getLength(); + Attr array[] = new Attr[len]; + for (int i = 0; i < len; i++) { + array[i] = (Attr) attrs.item(i); + } + for (int i = 0; i < len - 1; i++) { + String name = null; + name = array[i].getLocalName(); + int index = i; + for (int j = i + 1; j < len; j++) { + String curName = null; + curName = array[j].getLocalName(); + if (curName.compareTo(name) < 0) { + name = curName; + index = j; + } + } + if (index != i) { + Attr temp = array[i]; + array[i] = array[index]; + array[index] = temp; + } + } + + return array; + } +} diff --git a/java/org/apache/catalina/util/ErrorPageSupport.java b/java/org/apache/catalina/util/ErrorPageSupport.java new file mode 100644 index 0000000..93012be --- /dev/null +++ b/java/org/apache/catalina/util/ErrorPageSupport.java @@ -0,0 +1,104 @@ +/* + * 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.catalina.util; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.tomcat.util.descriptor.web.ErrorPage; + +/** + * Provides support for tracking per exception type and per HTTP status code + * error pages. + */ +public class ErrorPageSupport { + + // Fully qualified class name to error page + private Map exceptionPages = new ConcurrentHashMap<>(); + + // HTTP status code to error page + private Map statusPages = new ConcurrentHashMap<>(); + + + public void add(ErrorPage errorPage) { + String exceptionType = errorPage.getExceptionType(); + if (exceptionType == null) { + statusPages.put(Integer.valueOf(errorPage.getErrorCode()), errorPage); + } else { + exceptionPages.put(exceptionType, errorPage); + } + } + + + public void remove(ErrorPage errorPage) { + String exceptionType = errorPage.getExceptionType(); + if (exceptionType == null) { + statusPages.remove(Integer.valueOf(errorPage.getErrorCode()), errorPage); + } else { + exceptionPages.remove(exceptionType, errorPage); + } + } + + + public ErrorPage find(int statusCode) { + return statusPages.get(Integer.valueOf(statusCode)); + } + + + /** + * Find the ErrorPage, if any, for the named exception type. + * + * @param exceptionType The fully qualified class name of the exception type + * + * @return The ErrorPage for the named exception type, or {@code null} if + * none is configured + */ + public ErrorPage find(String exceptionType) { + return exceptionPages.get(exceptionType); + } + + + public ErrorPage find(Throwable exceptionType) { + if (exceptionType == null) { + return null; + } + Class clazz = exceptionType.getClass(); + String name = clazz.getName(); + while (!Object.class.equals(clazz)) { + ErrorPage errorPage = exceptionPages.get(name); + if (errorPage != null) { + return errorPage; + } + clazz = clazz.getSuperclass(); + if (clazz == null) { + break; + } + name = clazz.getName(); + } + return null; + } + + + public ErrorPage[] findAll() { + Set errorPages = new HashSet<>(); + errorPages.addAll(exceptionPages.values()); + errorPages.addAll(statusPages.values()); + return errorPages.toArray(new ErrorPage[0]); + } +} diff --git a/java/org/apache/catalina/util/IOTools.java b/java/org/apache/catalina/util/IOTools.java new file mode 100644 index 0000000..7b5507d --- /dev/null +++ b/java/org/apache/catalina/util/IOTools.java @@ -0,0 +1,110 @@ +/* + * 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.catalina.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + + +/** + * Contains commonly needed I/O-related methods + * + * @author Dan Sandberg + */ +public class IOTools { + protected static final int DEFAULT_BUFFER_SIZE=4*1024; //4k + + private IOTools() { + //Ensure non-instantiability + } + + /** + * Read input from reader and write it to writer until there is no more + * input from reader. + * + * @param reader the reader to read from. + * @param writer the writer to write to. + * @param buf the char array to use as a buffer + * @throws IOException IO error + */ + public static void flow( Reader reader, Writer writer, char[] buf ) + throws IOException { + int numRead; + while ( (numRead = reader.read(buf) ) >= 0) { + writer.write(buf, 0, numRead); + } + } + + /** + * Read input from reader and write it to writer until there is no more + * input from reader. + * + * @param reader the reader to read from. + * @param writer the writer to write to. + * @throws IOException IO error + * @see #flow( Reader, Writer, char[] ) + */ + public static void flow( Reader reader, Writer writer ) + throws IOException { + char[] buf = new char[DEFAULT_BUFFER_SIZE]; + flow( reader, writer, buf ); + } + + + /** + * Read input from input stream and write it to output stream until there is + * no more input from input stream using a new buffer of the default size + * (4 KiB). + * + * @param is input stream the input stream to read from. + * @param os output stream the output stream to write to. + * + * @throws IOException If an I/O error occurs during the copy + */ + public static void flow(InputStream is, OutputStream os) throws IOException { + byte[] buf = new byte[DEFAULT_BUFFER_SIZE]; + int numRead; + while ( (numRead = is.read(buf) ) >= 0) { + if (os != null) { + os.write(buf, 0, numRead); + } + } + } + + + /** + * Read until EOF or the buffer is filled. + * + * @param is The source to read from + * @param buf The buffer to write to + * + * @return The number of bytes read + * + * @throws IOException If an I/O error occurs during the read + */ + public static int readFully(InputStream is, byte[] buf) throws IOException { + int bytesRead = 0; + int read; + while (bytesRead < buf.length && ((read = is.read(buf, bytesRead, buf.length - bytesRead)) >= 0)) { + bytesRead += read; + } + return bytesRead; + } +} diff --git a/java/org/apache/catalina/util/Introspection.java b/java/org/apache/catalina/util/Introspection.java new file mode 100644 index 0000000..5c9bc2b --- /dev/null +++ b/java/org/apache/catalina/util/Introspection.java @@ -0,0 +1,180 @@ +/* + * 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.catalina.util; + +import java.beans.Introspector; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Provides introspection utilities that either require knowledge of Tomcat + * internals or are solely used by Tomcat internals. + */ +public class Introspection { + + private static final StringManager sm = + StringManager.getManager("org.apache.catalina.util"); + + + /** + * Extract the Java Bean property name from the setter name. + * + * Note: This method assumes that the method name has already been checked + * for correctness. + * @param setter The setter method + * @return the bean property name + */ + public static String getPropertyName(Method setter) { + return Introspector.decapitalize(setter.getName().substring(3)); + } + + + /** + * Determines if a method has a valid name and signature for a Java Bean + * setter. + * + * @param method The method to test + * + * @return true if the method does have a valid name and + * signature, else false + */ + public static boolean isValidSetter(Method method) { + if (method.getName().startsWith("set") + && method.getName().length() > 3 + && method.getParameterTypes().length == 1 + && method.getReturnType().getName().equals("void")) { + return true; + } + return false; + } + + /** + * Determines if a method is a valid lifecycle callback method. + * + * @param method + * The method to test + * + * @return true if the method is a valid lifecycle callback + * method, else false + */ + public static boolean isValidLifecycleCallback(Method method) { + if (method.getParameterTypes().length != 0 + || Modifier.isStatic(method.getModifiers()) + || method.getExceptionTypes().length > 0 + || !method.getReturnType().getName().equals("void")) { + return false; + } + return true; + } + + /** + * Obtain the declared fields for a class taking account of any security + * manager that may be configured. + * @param clazz The class to introspect + * @return the class fields as an array + */ + public static Field[] getDeclaredFields(final Class clazz) { + Field[] fields = null; + if (Globals.IS_SECURITY_ENABLED) { + fields = AccessController.doPrivileged((PrivilegedAction) clazz::getDeclaredFields); + } else { + fields = clazz.getDeclaredFields(); + } + return fields; + } + + + /** + * Obtain the declared methods for a class taking account of any security + * manager that may be configured. + * @param clazz The class to introspect + * @return the class methods as an array + */ + public static Method[] getDeclaredMethods(final Class clazz) { + Method[] methods = null; + if (Globals.IS_SECURITY_ENABLED) { + methods = AccessController.doPrivileged((PrivilegedAction) clazz::getDeclaredMethods); + } else { + methods = clazz.getDeclaredMethods(); + } + return methods; + } + + + /** + * Attempt to load a class using the given Container's class loader. If the + * class cannot be loaded, a debug level log message will be written to the + * Container's log and null will be returned. + * @param context The class loader of this context will be used to attempt + * to load the class + * @param className The class name + * @return the loaded class or null if loading failed + */ + public static Class loadClass(Context context, String className) { + ClassLoader cl = context.getLoader().getClassLoader(); + Log log = context.getLogger(); + Class clazz = null; + try { + clazz = cl.loadClass(className); + } catch (ClassNotFoundException | NoClassDefFoundError | ClassFormatError e) { + log.debug(sm.getString("introspection.classLoadFailed", className), e); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.debug(sm.getString("introspection.classLoadFailed", className), t); + } + return clazz; + } + + /** + * Converts the primitive type to its corresponding wrapper. + * + * @param clazz + * Class that will be evaluated + * @return if the parameter is a primitive type returns its wrapper; + * otherwise returns the same class + */ + public static Class convertPrimitiveType(Class clazz) { + if (clazz.equals(char.class)) { + return Character.class; + } else if (clazz.equals(int.class)) { + return Integer.class; + } else if (clazz.equals(boolean.class)) { + return Boolean.class; + } else if (clazz.equals(double.class)) { + return Double.class; + } else if (clazz.equals(byte.class)) { + return Byte.class; + } else if (clazz.equals(short.class)) { + return Short.class; + } else if (clazz.equals(long.class)) { + return Long.class; + } else if (clazz.equals(float.class)) { + return Float.class; + } else { + return clazz; + } + } +} diff --git a/java/org/apache/catalina/util/LifecycleBase.java b/java/org/apache/catalina/util/LifecycleBase.java new file mode 100644 index 0000000..f25af03 --- /dev/null +++ b/java/org/apache/catalina/util/LifecycleBase.java @@ -0,0 +1,426 @@ +/* + * 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.catalina.util; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Base implementation of the {@link Lifecycle} interface that implements the + * state transition rules for {@link Lifecycle#start()} and + * {@link Lifecycle#stop()} + */ +public abstract class LifecycleBase implements Lifecycle { + + private static final Log log = LogFactory.getLog(LifecycleBase.class); + + private static final StringManager sm = StringManager.getManager(LifecycleBase.class); + + + /** + * The list of registered LifecycleListeners for event notifications. + */ + private final List lifecycleListeners = new CopyOnWriteArrayList<>(); + + + /** + * The current state of the source component. + */ + private volatile LifecycleState state = LifecycleState.NEW; + + + private boolean throwOnFailure = true; + + + /** + * Will a {@link LifecycleException} thrown by a sub-class during + * {@link #initInternal()}, {@link #startInternal()}, + * {@link #stopInternal()} or {@link #destroyInternal()} be re-thrown for + * the caller to handle or will it be logged instead? + * + * @return {@code true} if the exception will be re-thrown, otherwise + * {@code false} + */ + public boolean getThrowOnFailure() { + return throwOnFailure; + } + + + /** + * Configure if a {@link LifecycleException} thrown by a sub-class during + * {@link #initInternal()}, {@link #startInternal()}, + * {@link #stopInternal()} or {@link #destroyInternal()} will be re-thrown + * for the caller to handle or if it will be logged instead. + * + * @param throwOnFailure {@code true} if the exception should be re-thrown, + * otherwise {@code false} + */ + public void setThrowOnFailure(boolean throwOnFailure) { + this.throwOnFailure = throwOnFailure; + } + + + @Override + public void addLifecycleListener(LifecycleListener listener) { + lifecycleListeners.add(listener); + } + + + @Override + public LifecycleListener[] findLifecycleListeners() { + return lifecycleListeners.toArray(new LifecycleListener[0]); + } + + + @Override + public void removeLifecycleListener(LifecycleListener listener) { + lifecycleListeners.remove(listener); + } + + + /** + * Allow sub classes to fire {@link Lifecycle} events. + * + * @param type Event type + * @param data Data associated with event. + */ + protected void fireLifecycleEvent(String type, Object data) { + LifecycleEvent event = new LifecycleEvent(this, type, data); + for (LifecycleListener listener : lifecycleListeners) { + listener.lifecycleEvent(event); + } + } + + + @Override + public final synchronized void init() throws LifecycleException { + if (!state.equals(LifecycleState.NEW)) { + invalidTransition(BEFORE_INIT_EVENT); + } + + try { + setStateInternal(LifecycleState.INITIALIZING, null, false); + initInternal(); + setStateInternal(LifecycleState.INITIALIZED, null, false); + } catch (Throwable t) { + handleSubClassException(t, "lifecycleBase.initFail", toString()); + } + } + + + /** + * Sub-classes implement this method to perform any instance initialisation + * required. + * + * @throws LifecycleException If the initialisation fails + */ + protected abstract void initInternal() throws LifecycleException; + + + @Override + public final synchronized void start() throws LifecycleException { + + if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) || + LifecycleState.STARTED.equals(state)) { + + if (log.isDebugEnabled()) { + Exception e = new LifecycleException(); + log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e); + } else if (log.isInfoEnabled()) { + log.info(sm.getString("lifecycleBase.alreadyStarted", toString())); + } + + return; + } + + if (state.equals(LifecycleState.NEW)) { + init(); + } else if (state.equals(LifecycleState.FAILED)) { + stop(); + } else if (!state.equals(LifecycleState.INITIALIZED) && + !state.equals(LifecycleState.STOPPED)) { + invalidTransition(BEFORE_START_EVENT); + } + + try { + setStateInternal(LifecycleState.STARTING_PREP, null, false); + startInternal(); + if (state.equals(LifecycleState.FAILED)) { + // This is a 'controlled' failure. The component put itself into the + // FAILED state so call stop() to complete the clean-up. + stop(); + } else if (!state.equals(LifecycleState.STARTING)) { + // Shouldn't be necessary but acts as a check that sub-classes are + // doing what they are supposed to. + invalidTransition(AFTER_START_EVENT); + } else { + setStateInternal(LifecycleState.STARTED, null, false); + } + } catch (Throwable t) { + // This is an 'uncontrolled' failure so put the component into the + // FAILED state and throw an exception. + handleSubClassException(t, "lifecycleBase.startFail", toString()); + } + } + + + /** + * Sub-classes must ensure that the state is changed to + * {@link LifecycleState#STARTING} during the execution of this method. + * Changing state will trigger the {@link Lifecycle#START_EVENT} event. + * + * If a component fails to start it may either throw a + * {@link LifecycleException} which will cause it's parent to fail to start + * or it can place itself in the error state in which case {@link #stop()} + * will be called on the failed component but the parent component will + * continue to start normally. + * + * @throws LifecycleException Start error occurred + */ + protected abstract void startInternal() throws LifecycleException; + + + @Override + public final synchronized void stop() throws LifecycleException { + + if (LifecycleState.STOPPING_PREP.equals(state) || LifecycleState.STOPPING.equals(state) || + LifecycleState.STOPPED.equals(state)) { + + if (log.isDebugEnabled()) { + Exception e = new LifecycleException(); + log.debug(sm.getString("lifecycleBase.alreadyStopped", toString()), e); + } else if (log.isInfoEnabled()) { + log.info(sm.getString("lifecycleBase.alreadyStopped", toString())); + } + + return; + } + + if (state.equals(LifecycleState.NEW)) { + state = LifecycleState.STOPPED; + return; + } + + if (!state.equals(LifecycleState.STARTED) && !state.equals(LifecycleState.FAILED)) { + invalidTransition(BEFORE_STOP_EVENT); + } + + try { + if (state.equals(LifecycleState.FAILED)) { + // Don't transition to STOPPING_PREP as that would briefly mark the + // component as available but do ensure the BEFORE_STOP_EVENT is + // fired + fireLifecycleEvent(BEFORE_STOP_EVENT, null); + } else { + setStateInternal(LifecycleState.STOPPING_PREP, null, false); + } + + stopInternal(); + + // Shouldn't be necessary but acts as a check that sub-classes are + // doing what they are supposed to. + if (!state.equals(LifecycleState.STOPPING) && !state.equals(LifecycleState.FAILED)) { + invalidTransition(AFTER_STOP_EVENT); + } + + setStateInternal(LifecycleState.STOPPED, null, false); + } catch (Throwable t) { + handleSubClassException(t, "lifecycleBase.stopFail", toString()); + } finally { + if (this instanceof Lifecycle.SingleUse) { + // Complete stop process first + setStateInternal(LifecycleState.STOPPED, null, false); + destroy(); + } + } + } + + + /** + * Sub-classes must ensure that the state is changed to + * {@link LifecycleState#STOPPING} during the execution of this method. + * Changing state will trigger the {@link Lifecycle#STOP_EVENT} event. + * + * @throws LifecycleException Stop error occurred + */ + protected abstract void stopInternal() throws LifecycleException; + + + @Override + public final synchronized void destroy() throws LifecycleException { + if (LifecycleState.FAILED.equals(state)) { + try { + // Triggers clean-up + stop(); + } catch (LifecycleException e) { + // Just log. Still want to destroy. + log.error(sm.getString("lifecycleBase.destroyStopFail", toString()), e); + } + } + + if (LifecycleState.DESTROYING.equals(state) || LifecycleState.DESTROYED.equals(state)) { + if (log.isDebugEnabled()) { + Exception e = new LifecycleException(); + log.debug(sm.getString("lifecycleBase.alreadyDestroyed", toString()), e); + } else if (log.isInfoEnabled() && !(this instanceof Lifecycle.SingleUse)) { + // Rather than have every component that might need to call + // destroy() check for SingleUse, don't log an info message if + // multiple calls are made to destroy() + log.info(sm.getString("lifecycleBase.alreadyDestroyed", toString())); + } + + return; + } + + if (!state.equals(LifecycleState.STOPPED) && !state.equals(LifecycleState.FAILED) && + !state.equals(LifecycleState.NEW) && !state.equals(LifecycleState.INITIALIZED)) { + invalidTransition(BEFORE_DESTROY_EVENT); + } + + try { + setStateInternal(LifecycleState.DESTROYING, null, false); + destroyInternal(); + setStateInternal(LifecycleState.DESTROYED, null, false); + } catch (Throwable t) { + handleSubClassException(t, "lifecycleBase.destroyFail", toString()); + } + } + + + /** + * Sub-classes implement this method to perform any instance destruction + * required. + * + * @throws LifecycleException If the destruction fails + */ + protected abstract void destroyInternal() throws LifecycleException; + + + @Override + public LifecycleState getState() { + return state; + } + + + @Override + public String getStateName() { + return getState().toString(); + } + + + /** + * Provides a mechanism for sub-classes to update the component state. + * Calling this method will automatically fire any associated + * {@link Lifecycle} event. It will also check that any attempted state + * transition is valid for a sub-class. + * + * @param state The new state for this component + * @throws LifecycleException when attempting to set an invalid state + */ + protected synchronized void setState(LifecycleState state) throws LifecycleException { + setStateInternal(state, null, true); + } + + + /** + * Provides a mechanism for sub-classes to update the component state. + * Calling this method will automatically fire any associated + * {@link Lifecycle} event. It will also check that any attempted state + * transition is valid for a sub-class. + * + * @param state The new state for this component + * @param data The data to pass to the associated {@link Lifecycle} event + * @throws LifecycleException when attempting to set an invalid state + */ + protected synchronized void setState(LifecycleState state, Object data) + throws LifecycleException { + setStateInternal(state, data, true); + } + + + private synchronized void setStateInternal(LifecycleState state, Object data, boolean check) + throws LifecycleException { + + if (log.isDebugEnabled()) { + log.debug(sm.getString("lifecycleBase.setState", this, state)); + } + + if (check) { + // Must have been triggered by one of the abstract methods (assume + // code in this class is correct) + // null is never a valid state + if (state == null) { + invalidTransition("null"); + // Unreachable code - here to stop eclipse complaining about + // a possible NPE further down the method + return; + } + + // Any method can transition to failed + // startInternal() permits STARTING_PREP to STARTING + // stopInternal() permits STOPPING_PREP to STOPPING and FAILED to + // STOPPING + if (!(state == LifecycleState.FAILED || + (this.state == LifecycleState.STARTING_PREP && + state == LifecycleState.STARTING) || + (this.state == LifecycleState.STOPPING_PREP && + state == LifecycleState.STOPPING) || + (this.state == LifecycleState.FAILED && + state == LifecycleState.STOPPING))) { + // No other transition permitted + invalidTransition(state.name()); + } + } + + this.state = state; + String lifecycleEvent = state.getLifecycleEvent(); + if (lifecycleEvent != null) { + fireLifecycleEvent(lifecycleEvent, data); + } + } + + + private void invalidTransition(String type) throws LifecycleException { + String msg = sm.getString("lifecycleBase.invalidTransition", type, toString(), state); + throw new LifecycleException(msg); + } + + + private void handleSubClassException(Throwable t, String key, Object... args) throws LifecycleException { + setStateInternal(LifecycleState.FAILED, null, false); + ExceptionUtils.handleThrowable(t); + String msg = sm.getString(key, args); + if (getThrowOnFailure()) { + if (!(t instanceof LifecycleException)) { + t = new LifecycleException(msg, t); + } + throw (LifecycleException) t; + } else { + log.error(msg, t); + } + } +} diff --git a/java/org/apache/catalina/util/LifecycleMBeanBase.java b/java/org/apache/catalina/util/LifecycleMBeanBase.java new file mode 100644 index 0000000..37217af --- /dev/null +++ b/java/org/apache/catalina/util/LifecycleMBeanBase.java @@ -0,0 +1,236 @@ +/* + * 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.catalina.util; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.catalina.Globals; +import org.apache.catalina.JmxEnabled; +import org.apache.catalina.LifecycleException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +public abstract class LifecycleMBeanBase extends LifecycleBase + implements JmxEnabled { + + private static final Log log = LogFactory.getLog(LifecycleMBeanBase.class); + + private static final StringManager sm = + StringManager.getManager("org.apache.catalina.util"); + + + /* Cache components of the MBean registration. */ + private String domain = null; + private ObjectName oname = null; + + /** + * Sub-classes wishing to perform additional initialization should override + * this method, ensuring that super.initInternal() is the first call in the + * overriding method. + */ + @Override + protected void initInternal() throws LifecycleException { + // If oname is not null then registration has already happened via + // preRegister(). + if (oname == null) { + oname = register(this, getObjectNameKeyProperties()); + } + } + + + /** + * Sub-classes wishing to perform additional clean-up should override this + * method, ensuring that super.destroyInternal() is the last call in the + * overriding method. + */ + @Override + protected void destroyInternal() throws LifecycleException { + unregister(oname); + } + + + /** + * Specify the domain under which this component should be registered. Used + * with components that cannot (easily) navigate the component hierarchy to + * determine the correct domain to use. + */ + @Override + public final void setDomain(String domain) { + this.domain = domain; + } + + + /** + * Obtain the domain under which this component will be / has been + * registered. + */ + @Override + public final String getDomain() { + if (domain == null) { + domain = getDomainInternal(); + } + + if (domain == null) { + domain = Globals.DEFAULT_MBEAN_DOMAIN; + } + + return domain; + } + + + /** + * Method implemented by sub-classes to identify the domain in which MBeans + * should be registered. + * + * @return The name of the domain to use to register MBeans. + */ + protected abstract String getDomainInternal(); + + + /** + * Obtain the name under which this component has been registered with JMX. + */ + @Override + public final ObjectName getObjectName() { + return oname; + } + + + /** + * Allow sub-classes to specify the key properties component of the + * {@link ObjectName} that will be used to register this component. + * + * @return The string representation of the key properties component of the + * desired {@link ObjectName} + */ + protected abstract String getObjectNameKeyProperties(); + + + /** + * Utility method to enable sub-classes to easily register additional + * components that don't implement {@link JmxEnabled} with an MBean server. + *
    + * Note: This method should only be used once {@link #initInternal()} has + * been called and before {@link #destroyInternal()} has been called. + * + * @param obj The object the register + * @param objectNameKeyProperties The key properties component of the + * object name to use to register the + * object + * + * @return The name used to register the object + */ + protected final ObjectName register(Object obj, + String objectNameKeyProperties) { + + // Construct an object name with the right domain + StringBuilder name = new StringBuilder(getDomain()); + name.append(':'); + name.append(objectNameKeyProperties); + + ObjectName on = null; + + try { + on = new ObjectName(name.toString()); + Registry.getRegistry(null, null).registerComponent(obj, on, null); + } catch (Exception e) { + log.warn(sm.getString("lifecycleMBeanBase.registerFail", obj, name), e); + } + + return on; + } + + + /** + * Utility method to enable sub-classes to easily unregister additional + * components that don't implement {@link JmxEnabled} with an MBean server. + *
    + * Note: This method should only be used once {@link #initInternal()} has + * been called and before {@link #destroyInternal()} has been called. + * + * @param objectNameKeyProperties The key properties component of the + * object name to use to unregister the + * object + */ + protected final void unregister(String objectNameKeyProperties) { + // Construct an object name with the right domain + StringBuilder name = new StringBuilder(getDomain()); + name.append(':'); + name.append(objectNameKeyProperties); + Registry.getRegistry(null, null).unregisterComponent(name.toString()); + } + + + /** + * Utility method to enable sub-classes to easily unregister additional + * components that don't implement {@link JmxEnabled} with an MBean server. + *
    + * Note: This method should only be used once {@link #initInternal()} has + * been called and before {@link #destroyInternal()} has been called. + * + * @param on The name of the component to unregister + */ + protected final void unregister(ObjectName on) { + Registry.getRegistry(null, null).unregisterComponent(on); + } + + + /** + * Not used - NOOP. + */ + @Override + public final void postDeregister() { + // NOOP + } + + + /** + * Not used - NOOP. + */ + @Override + public final void postRegister(Boolean registrationDone) { + // NOOP + } + + + /** + * Not used - NOOP. + */ + @Override + public final void preDeregister() throws Exception { + // NOOP + } + + + /** + * Allows the object to be registered with an alternative + * {@link MBeanServer} and/or {@link ObjectName}. + */ + @Override + public final ObjectName preRegister(MBeanServer server, ObjectName name) + throws Exception { + + this.oname = name; + this.domain = name.getDomain().intern(); + + return oname; + } + +} diff --git a/java/org/apache/catalina/util/LocalStrings.properties b/java/org/apache/catalina/util/LocalStrings.properties new file mode 100644 index 0000000..e7a3343 --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings.properties @@ -0,0 +1,52 @@ +# 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. + +SecurityUtil.doAsPrivilege=An exception occurs when running the PrivilegedExceptionAction block. + +customObjectInputStream.logRequired=A valid logger is required for class name filtering with logging +customObjectInputStream.nomatch=The class [{0}] did not match the regular expression [{1}] for classes allowed to be deserialized + +introspection.classLoadFailed=Failed to load class [{0}] + +lifecycleBase.alreadyDestroyed=The destroy() method was called on component [{0}] after destroy() had already been called. The second call will be ignored. +lifecycleBase.alreadyStarted=The start() method was called on component [{0}] after start() had already been called. The second call will be ignored. +lifecycleBase.alreadyStopped=The stop() method was called on component [{0}] after stop() had already been called. The second call will be ignored. +lifecycleBase.destroyFail=Failed to destroy component [{0}] +lifecycleBase.destroyStopFail=Called stop() on failed component [{0}] to trigger clean-up but it failed too +lifecycleBase.initFail=Failed to initialize component [{0}] +lifecycleBase.invalidTransition=An invalid Lifecycle transition was attempted ([{0}]) for component [{1}] in state [{2}] +lifecycleBase.setState=Setting state for [{0}] to [{1}] +lifecycleBase.startFail=Failed to start component [{0}] +lifecycleBase.stopFail=Failed to stop component [{0}] + +lifecycleMBeanBase.registerFail=Failed to register object [{0}] with name [{1}] during component initialisation + +netmask.cidrNegative=The CIDR [{0}] is negative +netmask.cidrNotNumeric=The CIDR [{0}] is not numeric +netmask.cidrTooBig=The CIDR [{0}] is greater than the address length [{1}] +netmask.invalidAddress=The address [{0}] is not valid +netmask.invalidPort=The port part in the pattern [{0}] is not valid + +parameterMap.locked=No modifications are allowed to a locked ParameterMap + +resourceSet.locked=No modifications are allowed to a locked ResourceSet + +sessionIdGeneratorBase.createRandom=Creation of SecureRandom instance for session ID generation using [{0}] took [{1}] milliseconds. +sessionIdGeneratorBase.noSHA1PRNG=The default SHA1PRNG algorithm for SecureRandom is not supported by this JVM. Using the platform default. +sessionIdGeneratorBase.random=Exception initializing random number generator of class [{0}]. Falling back to java.secure.SecureRandom +sessionIdGeneratorBase.randomAlgorithm=Exception initializing random number generator using algorithm [{0}] +sessionIdGeneratorBase.randomProvider=Exception initializing random number generator using provider [{0}] + +timebucket.maintenance.error=Error processing periodic maintenance diff --git a/java/org/apache/catalina/util/LocalStrings_cs.properties b/java/org/apache/catalina/util/LocalStrings_cs.properties new file mode 100644 index 0000000..9dced58 --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_cs.properties @@ -0,0 +1,21 @@ +# 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. + +customObjectInputStream.logRequired=Je vyžadován platný logger pro filtrování třídy s logováním +customObjectInputStream.nomatch=Třída [{0}] neodpovídá regulárnímu výrazu [{1}] s definicí tříd povolených k deserializaci + +introspection.classLoadFailed=NaÄtení třídy [{0}] selhalo + +netmask.cidrNegative=CIDR [{0}] je negativní diff --git a/java/org/apache/catalina/util/LocalStrings_de.properties b/java/org/apache/catalina/util/LocalStrings_de.properties new file mode 100644 index 0000000..18d7386 --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_de.properties @@ -0,0 +1,27 @@ +# 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. + +customObjectInputStream.logRequired=Ein gültiger Logger ist für Klassennamenfilterung mit Loggen nötig +customObjectInputStream.nomatch=Die Klasse [{0}] wird nicht mit dem regulären Ausdruck [{1}] gefunden, der erlaubte deserialisierte Klassen bestimmt. + +introspection.classLoadFailed=Konnte Klasse [{0}] nicht laden + +lifecycleBase.initFail=Konnte Komponente [{0}] nicht initialisieren +lifecycleBase.setState=Setze Status für [{0}] auf [{1}] + +netmask.cidrNegative=Die CIDR [{0}] ist negativ +netmask.invalidAddress=Die Adresse [{0}] ist nicht gültig + +sessionIdGeneratorBase.randomProvider=Während der Zufallsgenerator mit Hilfe des Providers [{0}] initialisiert wurde, trat eine Exception auf diff --git a/java/org/apache/catalina/util/LocalStrings_es.properties b/java/org/apache/catalina/util/LocalStrings_es.properties new file mode 100644 index 0000000..288b73f --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_es.properties @@ -0,0 +1,31 @@ +# 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. + +SecurityUtil.doAsPrivilege=Una excepción se ha producido durante la ejecución del bloque PrivilegedExceptionAction. + +customObjectInputStream.logRequired=Se requiere un logeador váliso para el nombre de la clase filtrado con logeo +customObjectInputStream.nomatch=La clase [{0}] no concuerda con la expresión regular [{1}] para clases que tienen permitido el deserializamiento + +introspection.classLoadFailed=Fallo al cargar la clase [{0}] + +lifecycleBase.initFail=Fallo al iniciar el componente [{0}] + +netmask.cidrNegative=El CIDR [{0}] es negativo + +parameterMap.locked=No se permiten modificaciones en un ParameterMap bloqueado + +resourceSet.locked=No se permiten modificaciones en un ResourceSet bloqueado + +sessionIdGeneratorBase.random=Excepción inicializando generador de números aleatorios de clase [{0}] diff --git a/java/org/apache/catalina/util/LocalStrings_fr.properties b/java/org/apache/catalina/util/LocalStrings_fr.properties new file mode 100644 index 0000000..0b3b860 --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_fr.properties @@ -0,0 +1,52 @@ +# 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. + +SecurityUtil.doAsPrivilege=Une exception s'est produite lors de l'execution du bloc PrivilegedExceptionAction. + +customObjectInputStream.logRequired=Un enregistreur ("logger") valide est requis pour filtrer par nom de classe +customObjectInputStream.nomatch=La classe [{0}] n''est pas acceptée par l''expression régulière [{1}] qui autorise la désérialisation + +introspection.classLoadFailed=Echec du chargement de la classe [{0}] + +lifecycleBase.alreadyDestroyed=La méthode destroy() a été appelée sur le composant [{0}] après que destroy() ait déjà été appelé, le deuxième appel sera ignoré +lifecycleBase.alreadyStarted=La méthode start() a été appelée sur le composant [{0}] après que start() ait déjà été appelé, le deuxième appel sera ignoré +lifecycleBase.alreadyStopped=La méthode stop() a été appelée sur le composant [{0}] après que stop() ait déjà été appelé, le deuxième appel sera ignoré +lifecycleBase.destroyFail=Echec de la destruction du composant [{0}] +lifecycleBase.destroyStopFail=L''appel de stop() sur le composant en échec [{0}] pour causer un nettoyage a échoué +lifecycleBase.initFail=Echec d''initialisation du composant [{0}] +lifecycleBase.invalidTransition=Un transition de Lifecycle invalide a été tentée ([{0}]) pour le composant [{1}] dans l''état [{2}] +lifecycleBase.setState=Fixe l''état pour [{0}] à [{1}] +lifecycleBase.startFail=Echec de démarrage du composant [{0}] +lifecycleBase.stopFail=Echec de l''arrêt du composant [{0}] + +lifecycleMBeanBase.registerFail=Echec de l''enregistrement de l''objet [{0}] avec le nom [{1}] pendant l''initialisation du composant + +netmask.cidrNegative=Le CIDR [{0}] est négatif +netmask.cidrNotNumeric=Le CIDR [{0}] n''est pas un nombre +netmask.cidrTooBig=Le CIDR [{0}] est plus grand que la longueur de l''adresse [{1}] +netmask.invalidAddress=L''adresse [{0}] est invalide +netmask.invalidPort=La portion concernant le port dans le modèle [{0}] est invalide + +parameterMap.locked=Aucune modification n'est autorisée sur un ParameterMap verrouillé + +resourceSet.locked=Aucune modification n'est autorisée sur un ResourceSet verrouillé + +sessionIdGeneratorBase.createRandom=La création de l''instance de SecureRandom pour le générateur d''id de session en utilisant [{0}] a pris [{1}] millisecondes +sessionIdGeneratorBase.noSHA1PRNG=L'algorithme SHA1PRNG par défaut de SecureRandom n'est pas supporté par cette JVM, le défaut pour la plateforme sera utilisé +sessionIdGeneratorBase.random=Exception durant l''initialisation de la classe du générateur de nombre aléatoire [{0}] +sessionIdGeneratorBase.randomAlgorithm=Erreur lors de l''initialisation du générateur de nombres aléatoires en utilisant l''algorithme [{0}] +sessionIdGeneratorBase.randomProvider=Exception lors de l''initialisation du générateur de nombres aléatoires utilisant le fournisseur [{0}] + +timebucket.maintenance.error=Erreur lors du traitement de la maintenance périodique diff --git a/java/org/apache/catalina/util/LocalStrings_ja.properties b/java/org/apache/catalina/util/LocalStrings_ja.properties new file mode 100644 index 0000000..f44e1c9 --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_ja.properties @@ -0,0 +1,52 @@ +# 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. + +SecurityUtil.doAsPrivilege=PrivilegedExceptionActionブロックを実行中ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ + +customObjectInputStream.logRequired=ロギングã«ã‚ˆã‚‹ã‚¯ãƒ©ã‚¹åã®ãƒ•ã‚£ãƒ«ã‚¿ãƒªãƒ³ã‚°ã«ã¯æœ‰åŠ¹ãªãƒ­ã‚¬ãƒ¼ãŒå¿…è¦ã§ã™ +customObjectInputStream.nomatch=クラス [{0}] ã¯ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºãŒè¨±å¯ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹åã®æ­£è¦è¡¨ç¾ [{1}] ã«ãƒžãƒƒãƒã—ã¾ã›ã‚“ + +introspection.classLoadFailed=クラス [{0}] ã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ + +lifecycleBase.alreadyDestroyed=destroy()メソッドãŒæ—¢ã«å‘¼ã³å‡ºã•ã‚ŒãŸå¾Œã§ã€ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ [{0}] ã«å¯¾ã—ã¦å‘¼ã³å‡ºã•ã‚Œã¾ã—ãŸã€‚2番目ã®å‘¼ã³å‡ºã—ã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +lifecycleBase.alreadyStarted=以å‰ã« start() を呼ã³å‡ºã—ãŸã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ [{0}] ã® start() を呼ã³å‡ºã—ã¾ã—ãŸã€‚二度目ã®å‘¼ã³å‡ºã—ã¯ç„¡è¦–ã—ã¾ã™ã€‚ +lifecycleBase.alreadyStopped=以å‰ã« stop() を呼ã³å‡ºã—ãŸã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ [{0}] ã® stop() を呼ã³å‡ºã—ã¾ã—ãŸã€‚二度目ã®å‘¼ã³å‡ºã—ã¯ç„¡è¦–ã—ã¾ã™ã€‚ +lifecycleBase.destroyFail=コンãƒãƒ¼ãƒãƒ³ãƒˆ [{0}] を破棄ã§ãã¾ã›ã‚“。 +lifecycleBase.destroyStopFail=失敗ã—ãŸã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ [{0}] ã«å¯¾ã—ã¦stop()を呼ã³å‡ºã—ã¦ã‚¯ãƒªãƒ¼ãƒ³ã‚¢ãƒƒãƒ—をトリガーã—ã¾ã—ãŸãŒã€å¤±æ•—ã—ã¾ã—㟠+lifecycleBase.initFail=コンãƒãƒ¼ãƒãƒ³ãƒˆ [{0}] ã®åˆæœŸåŒ–ã«å¤±æ•—ã—ã¾ã—㟠+lifecycleBase.invalidTransition=状態 [{2}] ã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ [{1}] ã®ç„¡åŠ¹ãªãƒ©ã‚¤ãƒ•ã‚µã‚¤ã‚¯ãƒ«é·ç§»ãŒè©¦ã¿ã‚‰ã‚Œã¾ã—㟠([{0}]) +lifecycleBase.setState=[{0}]ã®çŠ¶æ…‹ã‚’[{1}]ã«è¨­å®šã—ã¾ã™ +lifecycleBase.startFail=コンãƒãƒ¼ãƒãƒ³ãƒˆ [{0}] ã®é–‹å§‹ã«å¤±æ•—ã—ã¾ã—㟠+lifecycleBase.stopFail=コンãƒãƒ¼ãƒãƒ³ãƒˆ [{0}] ã‚’åœæ­¢ã§ãã¾ã›ã‚“。 + +lifecycleMBeanBase.registerFail=コンãƒãƒ¼ãƒãƒ³ãƒˆã®åˆæœŸåŒ–時ã«ã‚ªãƒ–ジェクト [{0}] ã‚’åå‰ [{1}] ã§ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ + +netmask.cidrNegative=CIDR [{0}] ã«è² ã®å€¤ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ +netmask.cidrNotNumeric=CIDR [{0}]ã¯æ•°å€¤ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +netmask.cidrTooBig=CIDR [{0}] ã¯ã‚¢ãƒ‰ãƒ¬ã‚¹ç¯„囲 [{1}] を越ãˆã¦ã„ã¾ã™ã€‚ +netmask.invalidAddress=アドレス [{0}] ã¯ç„¡åŠ¹ã§ã™ +netmask.invalidPort=パターン[{0}]ã®ãƒãƒ¼ãƒˆéƒ¨åˆ†ãŒç„¡åŠ¹ã§ã™ + +parameterMap.locked=ロックã•ã‚ŒãŸParameterMapã¯å¤‰æ›´ãŒè¨±ã•ã‚Œã¾ã›ã‚“ + +resourceSet.locked=ロックã•ã‚ŒãŸResourceSetã¯å¤‰æ›´ãŒè¨±ã•ã‚Œã¾ã›ã‚“ + +sessionIdGeneratorBase.createRandom=セッション ID を生æˆã™ã‚‹ãŸã‚ã® SecureRandom インスタンスã®ä½œæˆã« [{1}] ミリ秒ã‹ã‹ã‚Šã¾ã—ãŸã€‚アルゴリズム㯠[{0}] ã§ã™ã€‚ +sessionIdGeneratorBase.noSHA1PRNG=SecureRandom ã®æ—¢å®šã® SHA1PRNG アルゴリズムã¯ã€ã“ã® JVM ã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 プラットフォームã®æ—¢å®šå€¤ã‚’使用ã—ã¾ã™ã€‚ +sessionIdGeneratorBase.random=クラス [{0}] ã®ä¹±æ•°ç™ºç”Ÿå™¨ã®åˆæœŸåŒ–ã®ä¾‹å¤–ã§ã™ +sessionIdGeneratorBase.randomAlgorithm=アルゴリズム [{0}] を使用ã—ã¦ä¹±æ•°ã‚¸ã‚§ãƒãƒ¬ãƒ¼ã‚¿ã‚’åˆæœŸåŒ–ã™ã‚‹éš›ã®ä¾‹å¤– +sessionIdGeneratorBase.randomProvider=プロãƒã‚¤ãƒ€ [{0}] を使用ã—ã¦ä¹±æ•°ã‚¸ã‚§ãƒãƒ¬ãƒ¼ã‚¿ã‚’åˆæœŸåŒ–中ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—㟠+ +timebucket.maintenance.error=定期メンテナンスã®å‡¦ç†ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠diff --git a/java/org/apache/catalina/util/LocalStrings_ko.properties b/java/org/apache/catalina/util/LocalStrings_ko.properties new file mode 100644 index 0000000..b50f4e7 --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_ko.properties @@ -0,0 +1,50 @@ +# 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. + +SecurityUtil.doAsPrivilege=PrivilegedExceptionAction 블ë¡ì„ 실행하는 ë™ì•ˆ 예외가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. + +customObjectInputStream.logRequired=로그를 남기면서 í´ëž˜ìŠ¤ ì´ë¦„ì„ í•„í„°ë§ í•˜ê¸° 위해서는 유효한 Loggerê°€ 필요합니다. +customObjectInputStream.nomatch=í´ëž˜ìŠ¤ [{0}]ì€(는), ì—­ì§ë ¬í™” ë˜ë„ë¡ í—ˆìš©ëœ í´ëž˜ìŠ¤ë“¤ì˜ ì •ê·œì‹ íŒ¨í„´ [{1}]ê³¼(와) 부합ë˜ì§€ 않습니다. + +introspection.classLoadFailed=í´ëž˜ìŠ¤ [{0}]ì„(를) 로드하지 못했습니다. + +lifecycleBase.alreadyDestroyed=ì´ë¯¸ destroy() 메소드가 호출ë˜ì—ˆë˜ 구성요소 [{0}]ì—ì„œ, 다시 destroy()ê°€ 호출ë˜ì—ˆìŠµë‹ˆë‹¤. ë‘번째 í˜¸ì¶œì€ ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +lifecycleBase.alreadyStarted=start()ê°€ ì´ë¯¸ í˜¸ì¶œëœ í›„ì—, 구성요소 [{0}]ì— start() 메소드가 호출ë˜ì—ˆìŠµë‹ˆë‹¤. ë‘번째 í˜¸ì¶œì€ ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +lifecycleBase.alreadyStopped=stop()ì´ ì´ë¯¸ í˜¸ì¶œëœ í›„ì—, 구성요소 [{0}]ì— ëŒ€í•´ stop() 메소드가 호출ë˜ì—ˆìŠµë‹ˆë‹¤. ë‘번째 í˜¸ì¶œì€ ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +lifecycleBase.destroyFail=구성요소 [{0}]ì„(를) 소멸시키지 못했습니다. +lifecycleBase.destroyStopFail=실패한 구성요소 [{0}]ì— ëŒ€í•´, clean-upì„ ê°œì‹œí•˜ê¸° 위해 stop()ì„ í˜¸ì¶œí–ˆìœ¼ë‚˜, ì´ í˜¸ì¶œ ë˜í•œ 실패했습니다. +lifecycleBase.initFail=구성요소 [{0}]ì„(를) 초기화하지 못했습니다. +lifecycleBase.invalidTransition=ìƒíƒœ [{2}]ì— ìžˆëŠ” 구성 요소 [{1}]ì— ëŒ€í•´, 유효하지 ì•Šì€ Lifecycle ì „í™˜ì´ ì‹œë„ë˜ì—ˆìŠµë‹ˆë‹¤ ([{0}]). +lifecycleBase.setState=[{0}]ì„(를) 위한 ìƒíƒœë¥¼ [{1}](으)ë¡œ 설정합니다. +lifecycleBase.startFail=구성요소 [{0}]ì„(를) 시작하지 못했습니다. +lifecycleBase.stopFail=구성요소 [{0}]ì„(를) 중지시키지 못했습니다. + +lifecycleMBeanBase.registerFail=구성요소 초기화 중, [{1}](ì´)ë¼ëŠ” ì´ë¦„ì„ ê°€ì§„ ê°ì²´ [{0}]ì„(를) 등ë¡í•˜ì§€ 못했습니다. + +netmask.cidrNegative=CIDR [{0}]ì´(ê°€) ìŒìˆ˜ìž…니다. +netmask.cidrNotNumeric=CIDR [{0}]ì´(ê°€) 숫ìžê°€ 아닙니다. +netmask.cidrTooBig=CIDR [{0}]ì´(ê°€) 주소 ê¸¸ì´ [{1}]보다 í½ë‹ˆë‹¤. +netmask.invalidAddress=주소 [{0}]ì€(는) 유효하지 않습니다. +netmask.invalidPort=패턴 [{0}] ë‚´ì˜ í¬íŠ¸ ë¶€ë¶„ì´ ìœ íš¨í•˜ì§€ 않습니다. + +parameterMap.locked=잠금 ìƒíƒœì¸ ParameterMapì— ëŒ€í•œ ë³€ê²½ì´ í—ˆìš©ë˜ì§€ 않습니다. + +resourceSet.locked=잠금 ìƒíƒœì¸ ResourceSetì— ëŒ€í•œ ë³€ê²½ì€ í—ˆìš©ë˜ì§€ 않습니다. + +sessionIdGeneratorBase.createRandom=[{0}] ì•Œê³ ë¦¬ì¦˜ì„ ì‚¬ìš©í•˜ì—¬, 세션 ID를 ìƒì„±í•˜ê¸° 위한 SecureRandom ê°ì²´ë¥¼ ìƒì„±í•˜ëŠ”ë°, [{1}] 밀리초가 소요ë습니다. +sessionIdGeneratorBase.noSHA1PRNG=현 JVMì´ SecureRandomì„ ìœ„í•œ 기본 SHA1PRNG ì•Œê³ ë¦¬ì¦˜ì„ ì§€ì›í•˜ì§€ 않습니다. í”Œëž«í¼ ê¸°ë³¸ ì„¤ì •ì„ ì‚¬ìš©í•©ë‹ˆë‹¤. +sessionIdGeneratorBase.random=í´ëž˜ìŠ¤ [{0}]ì˜ ë‚œìˆ˜ ë°œìƒê¸°ë¥¼ 초기화하는 중 예외 ë°œìƒ. java.secure.SecureRandom으로 대체합니다. +sessionIdGeneratorBase.randomAlgorithm=알고리즘 [{0}]ì„(를) 사용하여 난수 ë°œìƒê¸°ë¥¼ 초기화하는 중 오류 ë°œìƒ +sessionIdGeneratorBase.randomProvider=Provider [{0}]ì„(를) 사용하여, 난수 ë°œìƒê¸°ë¥¼ 초기화하는 중 예외 ë°œìƒ diff --git a/java/org/apache/catalina/util/LocalStrings_pt_BR.properties b/java/org/apache/catalina/util/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..aa8cad4 --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +customObjectInputStream.nomatch=A classe [{0}] não combina com a expressão regular [{1}] para classes permitidas para deserialização diff --git a/java/org/apache/catalina/util/LocalStrings_ru.properties b/java/org/apache/catalina/util/LocalStrings_ru.properties new file mode 100644 index 0000000..6520ab8 --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_ru.properties @@ -0,0 +1,20 @@ +# 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. + +introspection.classLoadFailed=Ðе возможно загрузить клаÑÑ [{0}] + +lifecycleBase.initFail=Ошибка инициализации компонента [{0}] + +netmask.cidrNegative=CIDR [{0}] отрицателен\n diff --git a/java/org/apache/catalina/util/LocalStrings_zh_CN.properties b/java/org/apache/catalina/util/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..861269c --- /dev/null +++ b/java/org/apache/catalina/util/LocalStrings_zh_CN.properties @@ -0,0 +1,50 @@ +# 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. + +SecurityUtil.doAsPrivilege=è¿è¡ŒPrivilegedExceptionActionå—æ—¶å‘生异常。 + +customObjectInputStream.logRequired=使用日志记录进行类å过滤需è¦ä¸€ä¸ªæœ‰æ•ˆçš„日志记录器 +customObjectInputStream.nomatch=因为类å…许被ååºåˆ—化,类[{0}]未能匹é…常规的表达å¼[{1}] + +introspection.classLoadFailed=加载 class [{0}] 失败 + +lifecycleBase.alreadyDestroyed=在调用destroy()之åŽï¼Œåœ¨ç»„件[{0}]上调用了destroy()方法。第二个调用将被忽略。 +lifecycleBase.alreadyStarted=在调用start()之åŽï¼Œåœ¨ç»„件[{0}]上调用start()方法。第二个调用将被忽略。 +lifecycleBase.alreadyStopped=在调用stop()之åŽï¼Œå¯¹ç»„件[{0}]调用了stop()方法。第二个调用将被忽略。 +lifecycleBase.destroyFail=未能销æ¯ç»„件[{0}] +lifecycleBase.destroyStopFail=在失败组件[{0}]上调用Stop()以触å‘清ç†ï¼Œä½†ä¹Ÿå¤±è´¥äº† +lifecycleBase.initFail=åˆå§‹åŒ–组件[{0}]失败。 +lifecycleBase.invalidTransition=无效的生命周期转å˜è¢«å°è¯• [{0}]) 组件 [{1}] çŠ¶æ€ [{2}] +lifecycleBase.setState=设置状æ€ä»Ž[{0}]到[{1}] +lifecycleBase.startFail=无法å¯åŠ¨ç»„件[{0}] +lifecycleBase.stopFail=无法åœæ­¢ç»„件[{0}] + +lifecycleMBeanBase.registerFail=在组件åˆå§‹åŒ–期间,无法注册å为[{1}]的对象[{0}] + +netmask.cidrNegative=CIDR [{0}]为负数。 +netmask.cidrNotNumeric=CIDR[{0}]ä¸æ˜¯æ•°å­— +netmask.cidrTooBig=CIDR[{0}]大于地å€é•¿åº¦[{1}] +netmask.invalidAddress=åœ°å€ [{0}] 无效 +netmask.invalidPort=正则[{0}]中的端å£éƒ¨åˆ†æ˜¯æ— æ•ˆçš„ + +parameterMap.locked=ä¸å…许修改é”定的å‚数映射 + +resourceSet.locked=ä¸å…许修改é”定的资æºé›† + +sessionIdGeneratorBase.createRandom=使用[{0}]创建会è¯ID生æˆçš„SecureRandom实例花费了[{1}]毫秒。 +sessionIdGeneratorBase.noSHA1PRNG=æ­¤ JVM ä¸æ”¯æŒ SecureRandom 的默认 SHA1PRNG 算法。使用平å°é»˜è®¤å€¼ã€‚\n +sessionIdGeneratorBase.random=åˆå§‹åŒ–ç±»[{0}]çš„éšæœºæ•°ç”Ÿæˆå™¨æ—¶å‘生异常。回到java.secure.SecureRandom +sessionIdGeneratorBase.randomAlgorithm=使用算法[{0}]åˆå§‹åŒ–éšæœºæ•°ç”Ÿæˆå™¨æ—¶å‘生异常 +sessionIdGeneratorBase.randomProvider=使用程åºæ供的åˆå§‹åŒ–éšæœºæ•°ç”Ÿæˆå™¨å¼‚常[{0}] diff --git a/java/org/apache/catalina/util/NetMask.java b/java/org/apache/catalina/util/NetMask.java new file mode 100644 index 0000000..a41cda1 --- /dev/null +++ b/java/org/apache/catalina/util/NetMask.java @@ -0,0 +1,309 @@ +/* + * 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.catalina.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.apache.tomcat.util.res.StringManager; + +/** + * A class representing a CIDR netmask. + *

    + * The constructor takes a string as an argument which represents a netmask, as per the CIDR notation -- whether this + * netmask be IPv4 or IPv6. It then extracts the network address (before the /) and the CIDR prefix (after the /), and + * tells through the #matches() method whether a candidate {@link InetAddress} object fits in the recorded range. + *

    + *

    + * As byte arrays as returned by InetAddress.getByName() are always in network byte order, finding a match + * is therefore as simple as testing whether the n first bits (where n is the CIDR) are the same in both byte arrays + * (the one of the network address and the one of the candidate address). We do that by first doing byte comparisons, + * then testing the last bits if any (that is, if the remainder of the integer division of the CIDR by 8 is not 0). + *

    + *

    + * As a bonus, if no '/' is found in the input, it is assumed that an exact address match is required. + *

    + */ +public final class NetMask { + + private static final StringManager sm = StringManager.getManager(NetMask.class); + + /** + * The argument to the constructor, used for .toString() + */ + private final String expression; + + /** + * The byte array representing the address extracted from the expression + */ + private final byte[] netaddr; + + /** + * The number of bytes to test for equality (CIDR / 8) + */ + private final int nrBytes; + + /** + * The right shift to apply to the last byte if CIDR % 8 is not 0; if it is 0, this variable is set to 0 + */ + private final int lastByteShift; + + /** + * Should we use the port pattern when matching + */ + private final boolean foundPort; + + /** + * The regular expression used to test for the server port (optional). + */ + private final Pattern portPattern; + + + /** + * Constructor + * + * @param input the CIDR netmask + * + * @throws IllegalArgumentException if the netmask is not correct (invalid address specification, malformed CIDR + * prefix, etc) + */ + public NetMask(final String input) { + + expression = input; + + final int portIdx = input.indexOf(';'); + final String nonPortPart; + + if (portIdx == -1) { + foundPort = false; + nonPortPart = input; + portPattern = null; + } else { + foundPort = true; + nonPortPart = input.substring(0, portIdx); + try { + portPattern = Pattern.compile(input.substring(portIdx + 1)); + } catch (PatternSyntaxException e) { + /* + * In case of error never match any non-empty port given + */ + throw new IllegalArgumentException(sm.getString("netmask.invalidPort", input), e); + } + } + + final int idx = nonPortPart.indexOf('/'); + + /* + * Handle the "IP only" case first + */ + if (idx == -1) { + try { + netaddr = InetAddress.getByName(nonPortPart).getAddress(); + } catch (UnknownHostException e) { + throw new IllegalArgumentException(sm.getString("netmask.invalidAddress", nonPortPart)); + } + nrBytes = netaddr.length; + lastByteShift = 0; + return; + } + + /* + * OK, we do have a netmask specified, so let's extract both the address and the CIDR. + */ + + final String addressPart = nonPortPart.substring(0, idx), cidrPart = nonPortPart.substring(idx + 1); + + try { + /* + * The address first... + */ + netaddr = InetAddress.getByName(addressPart).getAddress(); + } catch (UnknownHostException e) { + throw new IllegalArgumentException(sm.getString("netmask.invalidAddress", addressPart)); + } + + final int addrlen = netaddr.length * 8; + final int cidr; + + try { + /* + * And then the CIDR. + */ + cidr = Integer.parseInt(cidrPart); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(sm.getString("netmask.cidrNotNumeric", cidrPart)); + } + + /* + * We don't want a negative CIDR, nor do we want a CIDR which is greater than the address length (consider + * 0.0.0.0/33, or ::/129) + */ + if (cidr < 0) { + throw new IllegalArgumentException(sm.getString("netmask.cidrNegative", cidrPart)); + } + if (cidr > addrlen) { + throw new IllegalArgumentException(sm.getString("netmask.cidrTooBig", cidrPart, Integer.valueOf(addrlen))); + } + + nrBytes = cidr / 8; + + /* + * These last two lines could be shortened to: + * + * lastByteShift = (8 - (cidr % 8)) & 7; + * + * But... It's not worth it. In fact, explaining why it could work would be too long to be worth the trouble, so + * let's do it the simple way... + */ + + final int remainder = cidr % 8; + + lastByteShift = (remainder == 0) ? 0 : 8 - remainder; + } + + + /** + * Test if a given address and port matches this netmask. + * + * @param addr The {@link java.net.InetAddress} to test + * @param port The port to test + * + * @return true on match, false otherwise + */ + public boolean matches(final InetAddress addr, int port) { + if (!foundPort) { + return false; + } + final String portString = Integer.toString(port); + if (!portPattern.matcher(portString).matches()) { + return false; + } + return matches(addr, true); + } + + + /** + * Test if a given address matches this netmask. + * + * @param addr The {@link java.net.InetAddress} to test + * + * @return true on match, false otherwise + */ + public boolean matches(final InetAddress addr) { + return matches(addr, false); + } + + + /** + * Test if a given address matches this netmask. + * + * @param addr The {@link java.net.InetAddress} to test + * @param checkedPort Indicates, whether we already checked the port + * + * @return true on match, false otherwise + */ + public boolean matches(final InetAddress addr, boolean checkedPort) { + if (!checkedPort && foundPort) { + return false; + } + final byte[] candidate = addr.getAddress(); + + /* + * OK, remember that a CIDR prefix tells the number of BITS which should be equal between this NetMask's + * recorded address (netaddr) and the candidate address. One byte is 8 bits, no matter what, and IP addresses, + * whether they be IPv4 or IPv6, are big endian, aka MSB, Most Significant Byte (first). + * + * We therefore need to get the byte array of the candidate address, compare as many bytes of the candidate + * address with the recorded address as the CIDR prefix tells us to (that is, CIDR / 8), and then deal with the + * remaining bits -- if any. + * + * But prior to that, a simple test can be done: we deal with IP addresses here, which means IPv4 and IPv6. IPv4 + * addresses are encoded on 4 bytes, IPv6 addresses are encoded on 16 bytes. If the candidate address length is + * different than this NetMask's address, we don't have a match. + */ + if (candidate.length != netaddr.length) { + return false; + } + + + /* + * Now do the byte-compare. The constructor has recorded the number of bytes to compare in nrBytes, use that. If + * any of the byte we have to compare is different than what we expect, we don't have a match. + * + * If, on the opposite, after this loop, all bytes have been deemed equal, then the loop variable i will point + * to the byte right after that -- which we will need... + */ + int i = 0; + for (; i < nrBytes; i++) { + if (netaddr[i] != candidate[i]) { + return false; + } + } + + /* + * ... if there are bits left to test. There aren't any if lastByteShift is set to 0. + */ + if (lastByteShift == 0) { + return true; + } + + /* + * If it is not 0, however, we must test for the relevant bits in the next byte (whatever is in the bytes after + * that doesn't matter). We do it this way (remember that lastByteShift contains the amount of bits we should + * _right_ shift the last byte): + * + * - grab both bytes at index i, both from the netmask address and the candidate address; - xor them both. + * + * After the xor, it means that all the remaining bits of the CIDR should be set to 0... + */ + final int lastByte = netaddr[i] ^ candidate[i]; + + /* + * ... Which means that right shifting by lastByteShift should be 0. + */ + return lastByte >> lastByteShift == 0; + } + + + @Override + public String toString() { + return expression; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NetMask other = (NetMask) o; + return nrBytes == other.nrBytes && lastByteShift == other.lastByteShift && + Arrays.equals(netaddr, other.netaddr); + } + + @Override + public int hashCode() { + int result = 31 * Arrays.hashCode(netaddr) + lastByteShift; + return result; + } + +} diff --git a/java/org/apache/catalina/util/NetMaskSet.java b/java/org/apache/catalina/util/NetMaskSet.java new file mode 100644 index 0000000..6365dc8 --- /dev/null +++ b/java/org/apache/catalina/util/NetMaskSet.java @@ -0,0 +1,162 @@ +/* + * 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.catalina.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.tomcat.util.buf.StringUtils; + + +/** + * This class maintains a Set of NetMask objects and allows to check if a given IP address is matched by any of the + * NetMasks, making it easy to create Allow and Deny lists of CIDR networks and hosts. + */ +public class NetMaskSet { + + private final Set netmasks = new HashSet<>(); + + /** + * Tests if the provided InetAddress matches any of the {@link NetMask}s in the set. + * + * @param inetAddress An InetAddress to check + * + * @return {@code true} if the passed inetAddress is matched by any of the {@link NetMask}s in the set + */ + public boolean contains(InetAddress inetAddress) { + + for (NetMask nm : netmasks) { + if (nm.matches(inetAddress)) { + return true; + } + } + + return false; + } + + /** + * Tests if the provided IP address matches any of the {@link NetMask}s in the set. + * + * @param ipAddress an IP address to check + * + * @return {@code true} if the passed IP address is matched by any of the {@link NetMask}s in the set + * + * @throws UnknownHostException if the passed input is not a valid IP address + */ + public boolean contains(String ipAddress) throws UnknownHostException { + + InetAddress inetAddress = InetAddress.getByName(ipAddress); + return this.contains(inetAddress); + } + + /** + * Adds a NetMask object to the set if the set does not contain it + * + * @param netmask The NetMask to add + * + * @return true if the object was added + */ + public boolean add(NetMask netmask) { + return netmasks.add(netmask); + } + + /** + * Creates a NetMask object from the input string and adds it to the set. + * + * @param input The string from which to construct the NetMask + * + * @return true if the object was added + * + * @throws IllegalArgumentException if the input is not a valid CIDR format. + */ + public boolean add(String input) { + NetMask netmask = new NetMask(input); + return netmasks.add(netmask); + } + + /** + * removes all entries from the set + */ + public void clear() { + netmasks.clear(); + } + + /** + * Tests if the set is empty. + * + * @return {@code true} if the set is empty, otherwise {@code false} + */ + public boolean isEmpty() { + return netmasks.isEmpty(); + } + + /** + * Adds a {@link NetMask} list from a string input containing a comma-separated list of (hopefully valid) + * {@link NetMask}s. + * + * @param input The input string + * + * @return a list of processing error messages (empty when no errors) + */ + public List addAll(String input) { + + if (input == null || input.isEmpty()) { + return Collections.emptyList(); + } + + List errMessages = new ArrayList<>(); + + for (String s : StringUtils.splitCommaSeparated(input)) { + try { + this.add(s); + } catch (IllegalArgumentException e) { + errMessages.add(s + ": " + e.getMessage()); + } + } + + return Collections.unmodifiableList(errMessages); + } + + /** + * Provides a string representation of this NetMaskSet. The format of the String is not guaranteed to remain fixed. + * + * @return a comma separated list of the NetMasks in this set + */ + @Override + public String toString() { + + String result = netmasks.toString(); + + // remove open and close brackets if exist + if (result.startsWith("[")) { + result = result.substring(1); + } + + if (result.endsWith("]")) { + result = result.substring(0, result.length() - 1); + } + + return result; + } + +} diff --git a/java/org/apache/catalina/util/ParameterMap.java b/java/org/apache/catalina/util/ParameterMap.java new file mode 100644 index 0000000..5e09295 --- /dev/null +++ b/java/org/apache/catalina/util/ParameterMap.java @@ -0,0 +1,256 @@ +/* + * 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.catalina.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Implementation of java.util.Map that includes a + * locked property. This class can be used to safely expose + * Catalina internal parameter map objects to user classes without having + * to clone them in order to avoid modifications. When first created, a + * ParameterMap instance is not locked. + * + * @param The type of Key + * @param The type of Value + * + * @author Craig R. McClanahan + */ +public final class ParameterMap implements Map, Serializable { + + private static final long serialVersionUID = 2L; + + private final Map delegatedMap; + + private final Map unmodifiableDelegatedMap; + + + /** + * Construct a new, empty map with the default initial capacity and + * load factor. + */ + public ParameterMap() { + delegatedMap = new LinkedHashMap<>(); + unmodifiableDelegatedMap = Collections.unmodifiableMap(delegatedMap); + } + + + /** + * Construct a new, empty map with the specified initial capacity and + * default load factor. + * + * @param initialCapacity The initial capacity of this map + */ + public ParameterMap(int initialCapacity) { + delegatedMap = new LinkedHashMap<>(initialCapacity); + unmodifiableDelegatedMap = Collections.unmodifiableMap(delegatedMap); + } + + + /** + * Construct a new, empty map with the specified initial capacity and + * load factor. + * + * @param initialCapacity The initial capacity of this map + * @param loadFactor The load factor of this map + */ + public ParameterMap(int initialCapacity, float loadFactor) { + delegatedMap = new LinkedHashMap<>(initialCapacity, loadFactor); + unmodifiableDelegatedMap = Collections.unmodifiableMap(delegatedMap); + } + + + /** + * Construct a new map with the same mappings as the given map. + * + * @param map Map whose contents are duplicated in the new map + */ + public ParameterMap(Map map) { + delegatedMap = new LinkedHashMap<>(map); + unmodifiableDelegatedMap = Collections.unmodifiableMap(delegatedMap); + } + + + /** + * The current lock state of this parameter map. + */ + private boolean locked = false; + + + /** + * @return the locked state of this parameter map. + */ + public boolean isLocked() { + return locked; + } + + + /** + * Set the locked state of this parameter map. + * + * @param locked The new locked state + */ + public void setLocked(boolean locked) { + this.locked = locked; + } + + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager("org.apache.catalina.util"); + + + /** + * {@inheritDoc} + * + * @exception IllegalStateException if this map is currently locked + */ + @Override + public void clear() { + checkLocked(); + delegatedMap.clear(); + } + + + /** + * {@inheritDoc} + * + * @exception IllegalStateException if this map is currently locked + */ + @Override + public V put(K key, V value) { + checkLocked(); + return delegatedMap.put(key, value); + } + + + /** + * {@inheritDoc} + * + * @exception IllegalStateException if this map is currently locked + */ + @Override + public void putAll(Map map) { + checkLocked(); + delegatedMap.putAll(map); + } + + + /** + * {@inheritDoc} + * + * @exception IllegalStateException if this map is currently locked + */ + @Override + public V remove(Object key) { + checkLocked(); + return delegatedMap.remove(key); + } + + + private void checkLocked() { + if (locked) { + throw new IllegalStateException(sm.getString("parameterMap.locked")); + } + } + + + @Override + public int size() { + return delegatedMap.size(); + } + + + @Override + public boolean isEmpty() { + return delegatedMap.isEmpty(); + } + + + @Override + public boolean containsKey(Object key) { + return delegatedMap.containsKey(key); + } + + + @Override + public boolean containsValue(Object value) { + return delegatedMap.containsValue(value); + } + + + @Override + public V get(Object key) { + return delegatedMap.get(key); + } + + + /** + * {@inheritDoc} + *

    + * Returns an unmodifiable {@link Set} view of the keys + * contained in this map if it is locked. + */ + @Override + public Set keySet() { + if (locked) { + return unmodifiableDelegatedMap.keySet(); + } + + return delegatedMap.keySet(); + } + + + /** + * {@inheritDoc} + *

    + * Returns an unmodifiable {@link Collection} view of the + * values contained in this map if it is locked. + */ + @Override + public Collection values() { + if (locked) { + return unmodifiableDelegatedMap.values(); + } + + return delegatedMap.values(); + } + + + /** + * {@inheritDoc} + *

    + * Returns an unmodifiable {@link Set} view of the mappings + * contained in this map if it is locked. + */ + @Override + public Set> entrySet() { + if (locked) { + return unmodifiableDelegatedMap.entrySet(); + } + + return delegatedMap.entrySet(); + } +} diff --git a/java/org/apache/catalina/util/RequestUtil.java b/java/org/apache/catalina/util/RequestUtil.java new file mode 100644 index 0000000..dfd9ab7 --- /dev/null +++ b/java/org/apache/catalina/util/RequestUtil.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.util; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * General purpose request parsing and encoding utility methods. + * + * @author Craig R. McClanahan + * @author Tim Tye + */ +public final class RequestUtil { + + /** + * Build an appropriate return value for + * {@link HttpServletRequest#getRequestURL()} based on the provided + * request object. Note that this will also work for instances of + * {@link jakarta.servlet.http.HttpServletRequestWrapper}. + * + * @param request The request object for which the URL should be built + * + * @return The request URL for the given request object + */ + public static StringBuffer getRequestURL(HttpServletRequest request) { + StringBuffer url = new StringBuffer(); + String scheme = request.getScheme(); + int port = request.getServerPort(); + if (port < 0) { + // Work around java.net.URL bug + port = 80; + } + + url.append(scheme); + url.append("://"); + url.append(request.getServerName()); + if ((scheme.equals("http") && (port != 80)) + || (scheme.equals("https") && (port != 443))) { + url.append(':'); + url.append(port); + } + url.append(request.getRequestURI()); + + return url; + } +} diff --git a/java/org/apache/catalina/util/ResourceSet.java b/java/org/apache/catalina/util/ResourceSet.java new file mode 100644 index 0000000..9e4dd7d --- /dev/null +++ b/java/org/apache/catalina/util/ResourceSet.java @@ -0,0 +1,182 @@ +/* + * 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.catalina.util; + + +import java.util.Collection; +import java.util.HashSet; + +import org.apache.tomcat.util.res.StringManager; + + +/** + * Extended implementation of HashSet that includes a + * locked property. This class can be used to safely expose + * resource path sets to user classes without having to clone them in order + * to avoid modifications. When first created, a ResourceMap + * is not locked. + * + * @param The type of elements in the Set + * + * @author Craig R. McClanahan + */ +public final class ResourceSet extends HashSet { + + private static final long serialVersionUID = 1L; + + // ----------------------------------------------------------- Constructors + /** + * Construct a new, empty set with the default initial capacity and + * load factor. + */ + public ResourceSet() { + + super(); + + } + + + /** + * Construct a new, empty set with the specified initial capacity and + * default load factor. + * + * @param initialCapacity The initial capacity of this set + */ + public ResourceSet(int initialCapacity) { + + super(initialCapacity); + + } + + + /** + * Construct a new, empty set with the specified initial capacity and + * load factor. + * + * @param initialCapacity The initial capacity of this set + * @param loadFactor The load factor of this set + */ + public ResourceSet(int initialCapacity, float loadFactor) { + + super(initialCapacity, loadFactor); + + } + + + /** + * Construct a new set with the same contents as the existing collection. + * + * @param coll The collection whose contents we should copy + */ + public ResourceSet(Collection coll) { + + super(coll); + + } + + + // ------------------------------------------------------------- Properties + + + /** + * The current lock state of this parameter map. + */ + private boolean locked = false; + + + /** + * @return the locked state of this parameter map. + */ + public boolean isLocked() { + return this.locked; + } + + + /** + * Set the locked state of this parameter map. + * + * @param locked The new locked state + */ + public void setLocked(boolean locked) { + this.locked = locked; + } + + + /** + * The string manager for this package. + */ + private static final StringManager sm = + StringManager.getManager("org.apache.catalina.util"); + + + // --------------------------------------------------------- Public Methods + + + /** + * Add the specified element to this set if it is not already present. + * Return true if the element was added. + * + * @param o The object to be added + * + * @exception IllegalStateException if this ResourceSet is locked + */ + @Override + public boolean add(T o) { + if (locked) { + throw new IllegalStateException + (sm.getString("resourceSet.locked")); + } + return super.add(o); + } + + + /** + * Remove all of the elements from this set. + * + * @exception IllegalStateException if this ResourceSet is locked + */ + @Override + public void clear() { + + if (locked) { + throw new IllegalStateException + (sm.getString("resourceSet.locked")); + } + super.clear(); + + } + + + /** + * Remove the given element from this set if it is present. + * Return true if the element was removed. + * + * @param o The object to be removed + * + * @exception IllegalStateException if this ResourceSet is locked + */ + @Override + public boolean remove(Object o) { + if (locked) { + throw new IllegalStateException + (sm.getString("resourceSet.locked")); + } + return super.remove(o); + } + + +} diff --git a/java/org/apache/catalina/util/ServerInfo.java b/java/org/apache/catalina/util/ServerInfo.java new file mode 100644 index 0000000..5ef6c54 --- /dev/null +++ b/java/org/apache/catalina/util/ServerInfo.java @@ -0,0 +1,143 @@ +/* + * 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.catalina.util; + + +import java.io.InputStream; +import java.util.Properties; + +import org.apache.tomcat.util.ExceptionUtils; + + +/** + * Simple utility module to make it easy to plug in the server identifier + * when integrating Tomcat. + * + * @author Craig R. McClanahan + */ +public class ServerInfo { + + + // ------------------------------------------------------- Static Variables + + + /** + * The server information String with which we identify ourselves. + */ + private static final String serverInfo; + + /** + * The server built String. + */ + private static final String serverBuilt; + + /** + * The server built String, in ISO-8604 date format. + */ + private static final String serverBuiltIso; + + /** + * The server's version number String. + */ + private static final String serverNumber; + + static { + + String info = null; + String built = null; + String builtIso = null; + String number = null; + + Properties props = new Properties(); + try (InputStream is = ServerInfo.class.getResourceAsStream + ("/org/apache/catalina/util/ServerInfo.properties")) { + props.load(is); + info = props.getProperty("server.info"); + built = props.getProperty("server.built"); + builtIso = props.getProperty("server.built.iso"); + number = props.getProperty("server.number"); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + if (info == null || info.equals("Apache Tomcat/@VERSION@")) { + info = "Apache Tomcat/10.1.x-dev"; + } + if (built == null || built.equals("@VERSION_BUILT@")) { + built = "unknown"; + } + if (builtIso == null || builtIso.equals("@VERSION_BUILT_ISO@")) { + builtIso = "unknown"; + } + if (number == null || number.equals("@VERSION_NUMBER@")) { + number = "10.1.x"; + } + + serverInfo = info; + serverBuilt = built; + serverBuiltIso = builtIso; + serverNumber = number; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * @return the server identification for this version of Tomcat. + */ + public static String getServerInfo() { + return serverInfo; + } + + /** + * @return the server built time for this version of Tomcat. + */ + public static String getServerBuilt() { + return serverBuilt; + } + + /** + * @return the server built date for this version of Tomcat in ISO-8601 date format. + */ + public static String getServerBuiltISO() { + return serverBuiltIso; + } + + /** + * @return the server's version number. + */ + public static String getServerNumber() { + return serverNumber; + } + + public static void main(String args[]) { + System.out.println("Server version: " + getServerInfo()); + System.out.println("Server built: " + getServerBuilt()); + System.out.println("Server number: " + getServerNumber()); + System.out.println("OS Name: " + + System.getProperty("os.name")); + System.out.println("OS Version: " + + System.getProperty("os.version")); + System.out.println("Architecture: " + + System.getProperty("os.arch")); + System.out.println("JVM Version: " + + System.getProperty("java.runtime.version")); + System.out.println("JVM Vendor: " + + System.getProperty("java.vm.vendor")); + } + +} diff --git a/java/org/apache/catalina/util/ServerInfo.properties b/java/org/apache/catalina/util/ServerInfo.properties new file mode 100644 index 0000000..3aa34f4 --- /dev/null +++ b/java/org/apache/catalina/util/ServerInfo.properties @@ -0,0 +1,19 @@ +# 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. + +server.info=Apache Tomcat/@VERSION@ +server.number=@VERSION_NUMBER@ +server.built=@VERSION_BUILT@ +server.built.iso=@VERSION_BUILT_ISO@ diff --git a/java/org/apache/catalina/util/SessionConfig.java b/java/org/apache/catalina/util/SessionConfig.java new file mode 100644 index 0000000..7720adc --- /dev/null +++ b/java/org/apache/catalina/util/SessionConfig.java @@ -0,0 +1,111 @@ +/* + * 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.catalina.util; + +import jakarta.servlet.SessionCookieConfig; + +import org.apache.catalina.Context; + +public class SessionConfig { + + private static final String DEFAULT_SESSION_COOKIE_NAME = "JSESSIONID"; + private static final String DEFAULT_SESSION_PARAMETER_NAME = "jsessionid"; + + /** + * Determine the name to use for the session cookie for the provided + * context. + * @param context The context + * @return the cookie name for the context + */ + public static String getSessionCookieName(Context context) { + return getConfiguredSessionCookieName(context, DEFAULT_SESSION_COOKIE_NAME); + } + + /** + * Determine the name to use for the session path parameter for the provided + * context. + * @param context The context + * @return the parameter name for the session + */ + public static String getSessionUriParamName(Context context) { + return getConfiguredSessionCookieName(context, DEFAULT_SESSION_PARAMETER_NAME); + } + + + private static String getConfiguredSessionCookieName(Context context, String defaultName) { + // Priority is: + // 1. Cookie name defined in context + // 2. Cookie name configured for app + // 3. Default defined by spec + if (context != null) { + String cookieName = context.getSessionCookieName(); + if (cookieName != null && cookieName.length() > 0) { + return cookieName; + } + + SessionCookieConfig scc = context.getServletContext().getSessionCookieConfig(); + cookieName = scc.getName(); + if (cookieName != null && cookieName.length() > 0) { + return cookieName; + } + } + return defaultName; + } + + + /** + * Determine the value to use for the session cookie path for the provided + * context. + * + * @param context The context + * @return the parameter name for the session + */ + public static String getSessionCookiePath(Context context) { + + SessionCookieConfig scc = context.getServletContext().getSessionCookieConfig(); + + String contextPath = context.getSessionCookiePath(); + if (contextPath == null || contextPath.length() == 0) { + contextPath = scc.getPath(); + } + if (contextPath == null || contextPath.length() == 0) { + contextPath = context.getEncodedPath(); + } + if (context.getSessionCookiePathUsesTrailingSlash()) { + // Handle special case of ROOT context where cookies require a path of + // '/' but the servlet spec uses an empty string + // Also ensure the cookies for a context with a path of /foo don't get + // sent for requests with a path of /foobar + if (!contextPath.endsWith("/")) { + contextPath = contextPath + "/"; + } + } else { + // Only handle special case of ROOT context where cookies require a + // path of '/' but the servlet spec uses an empty string + if (contextPath.length() == 0) { + contextPath = "/"; + } + } + + return contextPath; + } + + + private SessionConfig() { + // Utility class. Hide default constructor. + } +} diff --git a/java/org/apache/catalina/util/SessionIdGeneratorBase.java b/java/org/apache/catalina/util/SessionIdGeneratorBase.java new file mode 100644 index 0000000..b2af0f3 --- /dev/null +++ b/java/org/apache/catalina/util/SessionIdGeneratorBase.java @@ -0,0 +1,327 @@ +/* + * 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.catalina.util; + +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.SessionIdGenerator; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public abstract class SessionIdGeneratorBase extends LifecycleBase implements SessionIdGenerator { + + private final Log log = LogFactory.getLog(SessionIdGeneratorBase.class); // must not be static + + private static final StringManager sm = StringManager.getManager("org.apache.catalina.util"); + + public static final String DEFAULT_SECURE_RANDOM_ALGORITHM; + + static { + /* + * The default is normally SHA1PRNG. This was chosen because a) it is + * quick and b) it available by default in all JREs. However, it may not + * be available in some configurations such as those that use a FIPS + * certified provider. In those cases, use the platform default. + */ + Set algorithmNames = Security.getAlgorithms("SecureRandom"); + if (algorithmNames.contains("SHA1PRNG")) { + DEFAULT_SECURE_RANDOM_ALGORITHM = "SHA1PRNG"; + } else { + // Empty string - This will trigger the use of the platform default. + DEFAULT_SECURE_RANDOM_ALGORITHM = ""; + Log log = LogFactory.getLog(SessionIdGeneratorBase.class); + log.warn(sm.getString("sessionIdGeneratorBase.noSHA1PRNG")); + } + } + + /** + * Queue of random number generator objects to be used when creating session + * identifiers. If the queue is empty when a random number generator is + * required, a new random number generator object is created. This is + * designed this way since random number generators use a sync to make them + * thread-safe and the sync makes using a single object slow(er). + */ + private final Queue randoms = new ConcurrentLinkedQueue<>(); + + private String secureRandomClass = null; + + private String secureRandomAlgorithm = DEFAULT_SECURE_RANDOM_ALGORITHM; + + private String secureRandomProvider = null; + + + /** Node identifier when in a cluster. Defaults to the empty string. */ + private String jvmRoute = ""; + + + /** Number of bytes in a session ID. Defaults to 16. */ + private int sessionIdLength = 16; + + + /** + * Get the class name of the {@link SecureRandom} implementation used to + * generate session IDs. + * + * @return The fully qualified class name. {@code null} indicates that the + * JRE provided {@link SecureRandom} implementation will be used + */ + public String getSecureRandomClass() { + return secureRandomClass; + } + + + /** + * Specify a non-default {@link SecureRandom} implementation to use. The + * implementation must be self-seeding and have a zero-argument constructor. + * If not specified, an instance of {@link SecureRandom} will be generated. + * + * @param secureRandomClass The fully-qualified class name + */ + public void setSecureRandomClass(String secureRandomClass) { + this.secureRandomClass = secureRandomClass; + } + + + /** + * Get the name of the algorithm used to create the {@link SecureRandom} + * instances which generate new session IDs. + * + * @return The name of the algorithm. {@code null} or the empty string means + * that platform default will be used + */ + public String getSecureRandomAlgorithm() { + return secureRandomAlgorithm; + } + + + /** + * Specify a non-default algorithm to use to create instances of + * {@link SecureRandom} which are used to generate session IDs. If no + * algorithm is specified, SHA1PRNG will be used. If SHA1PRNG is not + * available, the platform default will be used. To use the platform default + * (which may be SHA1PRNG), specify {@code null} or the empty string. If an + * invalid algorithm and/or provider is specified the {@link SecureRandom} + * instances will be created using the defaults for this + * {@link SessionIdGenerator} implementation. If that fails, the + * {@link SecureRandom} instances will be created using platform defaults. + * + * @param secureRandomAlgorithm The name of the algorithm + */ + public void setSecureRandomAlgorithm(String secureRandomAlgorithm) { + this.secureRandomAlgorithm = secureRandomAlgorithm; + } + + + /** + * Get the name of the provider used to create the {@link SecureRandom} + * instances which generate new session IDs. + * + * @return The name of the provider. {@code null} or the empty string means + * that platform default will be used + */ + public String getSecureRandomProvider() { + return secureRandomProvider; + } + + + /** + * Specify a non-default provider to use to create instances of + * {@link SecureRandom} which are used to generate session IDs. If no + * provider is specified, the platform default is used. To use the platform + * default specify {@code null} or the empty string. If an invalid algorithm + * and/or provider is specified the {@link SecureRandom} instances will be + * created using the defaults for this {@link SessionIdGenerator} + * implementation. If that fails, the {@link SecureRandom} instances will be + * created using platform defaults. + * + * @param secureRandomProvider The name of the provider + */ + public void setSecureRandomProvider(String secureRandomProvider) { + this.secureRandomProvider = secureRandomProvider; + } + + + /** + * Return the node identifier associated with this node which will be + * included in the generated session ID. + */ + @Override + public String getJvmRoute() { + return jvmRoute; + } + + + /** + * Specify the node identifier associated with this node which will be + * included in the generated session ID. + * + * @param jvmRoute The node identifier + */ + @Override + public void setJvmRoute(String jvmRoute) { + this.jvmRoute = jvmRoute; + } + + + /** + * Return the number of bytes for a session ID + */ + @Override + public int getSessionIdLength() { + return sessionIdLength; + } + + + /** + * Specify the number of bytes for a session ID + * + * @param sessionIdLength Number of bytes + */ + @Override + public void setSessionIdLength(int sessionIdLength) { + this.sessionIdLength = sessionIdLength; + } + + + /** + * Generate and return a new session identifier. + */ + @Override + public String generateSessionId() { + return generateSessionId(jvmRoute); + } + + + protected void getRandomBytes(byte bytes[]) { + + SecureRandom random = randoms.poll(); + if (random == null) { + random = createSecureRandom(); + } + random.nextBytes(bytes); + randoms.add(random); + } + + + /** + * Create a new random number generator instance we should use for + * generating session identifiers. + */ + private SecureRandom createSecureRandom() { + + SecureRandom result = null; + + long t1 = System.currentTimeMillis(); + if (secureRandomClass != null) { + try { + // Construct and seed a new random number generator + Class clazz = Class.forName(secureRandomClass); + result = (SecureRandom) clazz.getConstructor().newInstance(); + } catch (Exception e) { + log.error(sm.getString("sessionIdGeneratorBase.random", + secureRandomClass), e); + } + } + + boolean error = false; + if (result == null) { + // No secureRandomClass or creation failed. Use SecureRandom. + try { + if (secureRandomProvider != null && + secureRandomProvider.length() > 0) { + result = SecureRandom.getInstance(secureRandomAlgorithm, + secureRandomProvider); + } else if (secureRandomAlgorithm != null && + secureRandomAlgorithm.length() > 0) { + result = SecureRandom.getInstance(secureRandomAlgorithm); + } + } catch (NoSuchAlgorithmException e) { + error = true; + log.error(sm.getString("sessionIdGeneratorBase.randomAlgorithm", + secureRandomAlgorithm), e); + } catch (NoSuchProviderException e) { + error = true; + log.error(sm.getString("sessionIdGeneratorBase.randomProvider", + secureRandomProvider), e); + } + } + + // In theory, DEFAULT_SECURE_RANDOM_ALGORITHM should always work but + // with custom providers that might not be the case. + if (result == null && error && !DEFAULT_SECURE_RANDOM_ALGORITHM.equals(secureRandomAlgorithm)) { + // Invalid provider / algorithm - use the default + try { + result = SecureRandom.getInstance(DEFAULT_SECURE_RANDOM_ALGORITHM); + } catch (NoSuchAlgorithmException e) { + log.error(sm.getString("sessionIdGeneratorBase.randomAlgorithm", + secureRandomAlgorithm), e); + } + } + + if (result == null) { + // Nothing works - use platform default + result = new SecureRandom(); + } + + // Force seeding to take place + result.nextInt(); + + long t2 = System.currentTimeMillis(); + if ((t2 - t1) > 100) { + log.warn(sm.getString("sessionIdGeneratorBase.createRandom", + result.getAlgorithm(), Long.valueOf(t2 - t1))); + } + return result; + } + + + @Override + protected void initInternal() throws LifecycleException { + // NO-OP + } + + + @Override + protected void startInternal() throws LifecycleException { + // Ensure SecureRandom has been initialised + generateSessionId(); + + setState(LifecycleState.STARTING); + } + + + @Override + protected void stopInternal() throws LifecycleException { + setState(LifecycleState.STOPPING); + randoms.clear(); + } + + + @Override + protected void destroyInternal() throws LifecycleException { + // NO-OP + } +} diff --git a/java/org/apache/catalina/util/StandardSessionIdGenerator.java b/java/org/apache/catalina/util/StandardSessionIdGenerator.java new file mode 100644 index 0000000..ea6ea65 --- /dev/null +++ b/java/org/apache/catalina/util/StandardSessionIdGenerator.java @@ -0,0 +1,65 @@ +/* + * 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.catalina.util; + +public class StandardSessionIdGenerator extends SessionIdGeneratorBase { + + @Override + public String generateSessionId(String route) { + + byte random[] = new byte[16]; + int sessionIdLength = getSessionIdLength(); + + // Render the result as a String of hexadecimal digits + // Start with enough space for sessionIdLength and medium route size + StringBuilder buffer = new StringBuilder(2 * sessionIdLength + 20); + + int resultLenBytes = 0; + + while (resultLenBytes < sessionIdLength) { + getRandomBytes(random); + for (int j = 0; + j < random.length && resultLenBytes < sessionIdLength; + j++) { + byte b1 = (byte) ((random[j] & 0xf0) >> 4); + byte b2 = (byte) (random[j] & 0x0f); + if (b1 < 10) { + buffer.append((char) ('0' + b1)); + } else { + buffer.append((char) ('A' + (b1 - 10))); + } + if (b2 < 10) { + buffer.append((char) ('0' + b2)); + } else { + buffer.append((char) ('A' + (b2 - 10))); + } + resultLenBytes++; + } + } + + if (route != null && route.length() > 0) { + buffer.append('.').append(route); + } else { + String jvmRoute = getJvmRoute(); + if (jvmRoute != null && jvmRoute.length() > 0) { + buffer.append('.').append(jvmRoute); + } + } + + return buffer.toString(); + } +} diff --git a/java/org/apache/catalina/util/Strftime.java b/java/org/apache/catalina/util/Strftime.java new file mode 100644 index 0000000..e54d2a6 --- /dev/null +++ b/java/org/apache/catalina/util/Strftime.java @@ -0,0 +1,252 @@ +/* + * 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.catalina.util; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Properties; +import java.util.TimeZone; + +/** + * Converts dates to strings using the same format specifiers as strftime + * + * Note: This does not mimic strftime perfectly. Certain strftime commands, + * are not supported, and will convert as if they were literals. + * + * Certain complicated commands, like those dealing with the week of the year + * probably don't have exactly the same behavior as strftime. + * + * These limitations are due to use SimpleDateTime. If the conversion was done + * manually, all these limitations could be eliminated. + * + * The interface looks like a subset of DateFormat. Maybe someday someone will make this class + * extend DateFormat. + * + * @author Bip Thelin + * @author Dan Sandberg + */ +public class Strftime { + protected static final Properties translate; + protected final SimpleDateFormat simpleDateFormat; + + /* + * Initialize our pattern translation + */ + static { + translate = new Properties(); + translate.put("a","EEE"); + translate.put("A","EEEE"); + translate.put("b","MMM"); + translate.put("B","MMMM"); + translate.put("c","EEE MMM d HH:mm:ss yyyy"); + + //There's no way to specify the century in SimpleDateFormat. We don't want to hard-code + //20 since this could be wrong for the pre-2000 files. + //translate.put("C", "20"); + translate.put("d","dd"); + translate.put("D","MM/dd/yy"); + translate.put("e","dd"); //will show as '03' instead of ' 3' + translate.put("F","yyyy-MM-dd"); + translate.put("g","yy"); + translate.put("G","yyyy"); + translate.put("H","HH"); + translate.put("h","MMM"); + translate.put("I","hh"); + translate.put("j","DDD"); + translate.put("k","HH"); //will show as '07' instead of ' 7' + translate.put("l","hh"); //will show as '07' instead of ' 7' + translate.put("m","MM"); + translate.put("M","mm"); + translate.put("n","\n"); + translate.put("p","a"); + translate.put("P","a"); //will show as pm instead of PM + translate.put("r","hh:mm:ss a"); + translate.put("R","HH:mm"); + //There's no way to specify this with SimpleDateFormat + //translate.put("s","seconds since epoch"); + translate.put("S","ss"); + translate.put("t","\t"); + translate.put("T","HH:mm:ss"); + //There's no way to specify this with SimpleDateFormat + //translate.put("u","day of week ( 1-7 )"); + + //There's no way to specify this with SimpleDateFormat + //translate.put("U","week in year with first Sunday as first day..."); + + translate.put("V","ww"); //I'm not sure this is always exactly the same + + //There's no way to specify this with SimpleDateFormat + //translate.put("W","week in year with first Monday as first day..."); + + //There's no way to specify this with SimpleDateFormat + //translate.put("w","E"); + translate.put("X","HH:mm:ss"); + translate.put("x","MM/dd/yy"); + translate.put("y","yy"); + translate.put("Y","yyyy"); + translate.put("Z","z"); + translate.put("z","Z"); + translate.put("%","%"); + } + + + /** + * Create an instance of this date formatting class + * + * @param origFormat the strftime-style formatting string + * @param locale the locale to use for locale-specific conversions + */ + public Strftime( String origFormat, Locale locale ) { + String convertedFormat = convertDateFormat( origFormat ); + simpleDateFormat = new SimpleDateFormat( convertedFormat, locale ); + } + + /** + * Format the date according to the strftime-style string given in the constructor. + * + * @param date the date to format + * @return the formatted date + */ + public String format( Date date ) { + return simpleDateFormat.format( date ); + } + + /** + * Get the timezone used for formatting conversions + * + * @return the timezone + */ + public TimeZone getTimeZone() { + return simpleDateFormat.getTimeZone(); + } + + /** + * Change the timezone used to format dates + * + * @param timeZone The new time zone + * @see SimpleDateFormat#setTimeZone + */ + public void setTimeZone( TimeZone timeZone ) { + simpleDateFormat.setTimeZone( timeZone ); + } + + /** + * Search the provided pattern and get the C standard + * Date/Time formatting rules and convert them to the + * Java equivalent. + * + * @param pattern The pattern to search + * @return The modified pattern + */ + protected String convertDateFormat( String pattern ) { + boolean inside = false; + boolean mark = false; + boolean modifiedCommand = false; + + StringBuilder buf = new StringBuilder(); + + for(int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + + if ( c=='%' && !mark ) { + mark=true; + } else { + if ( mark ) { + if ( modifiedCommand ) { + //don't do anything--we just wanted to skip a char + modifiedCommand = false; + mark = false; + } else { + inside = translateCommand( buf, pattern, i, inside ); + //It's a modifier code + if ( c=='O' || c=='E' ) { + modifiedCommand = true; + } else { + mark=false; + } + } + } else { + if ( !inside && c != ' ' ) { + //We start a literal, which we need to quote + buf.append('\''); + inside = true; + } + + buf.append(c); + } + } + } + + if ( buf.length() > 0 ) { + char lastChar = buf.charAt( buf.length() - 1 ); + + if( lastChar!='\'' && inside ) { + buf.append('\''); + } + } + return buf.toString(); + } + + protected String quote( String str, boolean insideQuotes ) { + String retVal = str; + if ( !insideQuotes ) { + retVal = '\'' + retVal + '\''; + } + return retVal; + } + + /** + * Try to get the Java Date/Time formatting associated with + * the C standard provided. + * + * @param buf The buffer + * @param pattern The date/time pattern + * @param index The char index + * @param oldInside Flag value + * @return True if new is inside buffer + */ + protected boolean translateCommand( StringBuilder buf, String pattern, int index, boolean oldInside ) { + char firstChar = pattern.charAt( index ); + boolean newInside = oldInside; + + //O and E are modifiers, they mean to present an alternative representation of the next char + //we just handle the next char as if the O or E wasn't there + if ( firstChar == 'O' || firstChar == 'E' ) { + if ( index + 1 < pattern.length() ) { + newInside = translateCommand( buf, pattern, index + 1, oldInside ); + } else { + buf.append( quote("%" + firstChar, oldInside ) ); + } + } else { + String command = translate.getProperty( String.valueOf( firstChar ) ); + + //If we don't find a format, treat it as a literal--That's what apache does + if ( command == null ) { + buf.append( quote( "%" + firstChar, oldInside ) ); + } else { + //If we were inside quotes, close the quotes + if ( oldInside ) { + buf.append( '\'' ); + } + buf.append( command ); + newInside = false; + } + } + return newInside; + } +} diff --git a/java/org/apache/catalina/util/StringUtil.java b/java/org/apache/catalina/util/StringUtil.java new file mode 100644 index 0000000..41062af --- /dev/null +++ b/java/org/apache/catalina/util/StringUtil.java @@ -0,0 +1,43 @@ +/* + * 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.catalina.util; + +import java.util.regex.Pattern; + +public class StringUtil { + /** + * {@link Pattern} for a comma delimited string that support whitespace characters + */ + private static final Pattern commaSeparatedValuesPattern = Pattern.compile("\\s*,\\s*"); + + /** + * Splits a comma-separated string into an array of String values. + * + * Whitespace around the commas is removed. + * + * Null or empty values will return a zero-element array. + * + * @param s The string to split by commas. + * + * @return An array of String values. + */ + public static String[] splitCommaSeparated(String s) { + return (s == null || s.length() == 0) ? new String[0] : + commaSeparatedValuesPattern.split(s); + + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/util/TLSUtil.java b/java/org/apache/catalina/util/TLSUtil.java new file mode 100644 index 0000000..7f895dd --- /dev/null +++ b/java/org/apache/catalina/util/TLSUtil.java @@ -0,0 +1,50 @@ +/* + * 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.catalina.util; + +import org.apache.catalina.Globals; +import org.apache.tomcat.util.net.SSLSupport; + +public class TLSUtil { + + /** + * Determines if the named request attribute is used to pass information + * about the TLS configuration of the connection to the application. Both + * the standard request attributes defined by the Servlet specification and + * Tomcat specific attributes are supported. + * + * @param name The attribute name to test + * + * @return {@code true} if the attribute is used to pass TLS configuration + * information, otherwise {@code false} + */ + public static boolean isTLSRequestAttribute(String name) { + switch (name) { + case Globals.CERTIFICATES_ATTR: + case Globals.CIPHER_SUITE_ATTR: + case Globals.KEY_SIZE_ATTR: + case Globals.SSL_SESSION_ID_ATTR: + case Globals.SSL_SESSION_MGR_ATTR: + case SSLSupport.PROTOCOL_VERSION_KEY: + case SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY: + case SSLSupport.REQUESTED_CIPHERS_KEY: + return true; + default: + return false; + } + } +} diff --git a/java/org/apache/catalina/util/TimeBucketCounter.java b/java/org/apache/catalina/util/TimeBucketCounter.java new file mode 100644 index 0000000..e106e24 --- /dev/null +++ b/java/org/apache/catalina/util/TimeBucketCounter.java @@ -0,0 +1,221 @@ +/* + * 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.catalina.util; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * This class maintains a thread safe hash map that has timestamp-based buckets followed by a string for a key, and a + * counter for a value. each time the increment() method is called it adds the key if it does not exist, increments its + * value and returns it. a maintenance thread cleans up keys that are prefixed by previous timestamp buckets. + */ +public class TimeBucketCounter { + + private static final Log log = LogFactory.getLog(TimeBucketCounter.class); + private static final StringManager sm = StringManager.getManager(TimeBucketCounter.class); + + /** + * Map to hold the buckets + */ + private final ConcurrentHashMap map = new ConcurrentHashMap<>(); + + /** + * Milliseconds bucket size as a Power of 2 for bit shift math, e.g. 16 for 65_536ms which is about 1:05 minute + */ + private final int numBits; + + /** + * Ratio of actual duration to config duration + */ + private final double ratio; + + /** + * The future allowing control of the background processor. + */ + private ScheduledFuture maintenanceFuture; + private ScheduledFuture monitorFuture; + private final ScheduledExecutorService executorService; + private final long sleeptime; + + /** + * Creates a new TimeBucketCounter with the specified lifetime. + * + * @param bucketDuration duration in seconds, e.g. for 1 minute pass 60 + * @param executorService the executor service which will be used to run the maintenance + */ + public TimeBucketCounter(int bucketDuration, ScheduledExecutorService executorService) { + + this.executorService = executorService; + + int durationMillis = bucketDuration * 1000; + + int bits = 0; + int pof2 = nextPowerOf2(durationMillis); + int bitCheck = pof2; + while (bitCheck > 1) { + bitCheck = pof2 >> ++bits; + } + + this.numBits = bits; + + this.ratio = ratioToPowerOf2(durationMillis); + + int cleanupsPerBucketDuration = (durationMillis >= 60_000) ? 6 : 3; + sleeptime = durationMillis / cleanupsPerBucketDuration; + + // Start our thread + if (sleeptime > 0) { + monitorFuture = executorService + .scheduleWithFixedDelay(new MaintenanceMonitor(), 0, 60, TimeUnit.SECONDS); + } + } + + /** + * Increments the counter for the passed identifier in the current time bucket and returns the new value. + * + * @param identifier an identifier for which we want to maintain count, e.g. IP Address + * + * @return the count within the current time bucket + */ + public final int increment(String identifier) { + String key = getCurrentBucketPrefix() + "-" + identifier; + AtomicInteger ai = map.computeIfAbsent(key, v -> new AtomicInteger()); + return ai.incrementAndGet(); + } + + /** + * Calculates the current time bucket prefix by shifting bits for fast division, e.g. shift 16 bits is the same as + * dividing by 65,536 which is about 1:05m. + * + * @return The current bucket prefix. + */ + public final int getCurrentBucketPrefix() { + return (int) (System.currentTimeMillis() >> this.numBits); + } + + public int getNumBits() { + return numBits; + } + + /** + * The actual duration may differ from the configured duration because it is set to the next power of 2 value in + * order to perform very fast bit shift arithmetic. + * + * @return the actual bucket duration in milliseconds + */ + public int getActualDuration() { + return (int) Math.pow(2, getNumBits()); + } + + /** + * Returns the ratio between the configured duration param and the actual duration which will be set to the next + * power of 2. We then multiply the configured requests param by the same ratio in order to compensate for the added + * time, if any. + * + * @return the ratio, e.g. 1.092 if the actual duration is 65_536 for the configured duration of 60_000 + */ + public double getRatio() { + return ratio; + } + + /** + * Returns the ratio to the next power of 2 so that we can adjust the value. + */ + static double ratioToPowerOf2(int value) { + double nextPO2 = nextPowerOf2(value); + return Math.round((1000 * nextPO2 / value)) / 1000d; + } + + /** + * Returns the next power of 2 given a value, e.g. 256 for 250, or 1024, for 1000. + */ + static int nextPowerOf2(int value) { + int valueOfHighestBit = Integer.highestOneBit(value); + if (valueOfHighestBit == value) { + return value; + } + + return valueOfHighestBit << 1; + } + + /** + * When we want to test a full bucket duration we need to sleep until the next bucket starts. + * + * @return the number of milliseconds until the next bucket + */ + public long getMillisUntilNextBucket() { + long millis = System.currentTimeMillis(); + long nextTimeBucketMillis = ((millis + (long) Math.pow(2, numBits)) >> numBits) << numBits; + long delta = nextTimeBucketMillis - millis; + return delta; + } + + /** + * Sets isRunning to false to terminate the maintenance thread. + */ + public void destroy() { + // Stop our thread + if (monitorFuture != null) { + monitorFuture.cancel(true); + monitorFuture = null; + } + if (maintenanceFuture != null) { + maintenanceFuture.cancel(true); + maintenanceFuture = null; + } + } + + private class Maintenance implements Runnable { + @Override + public void run() { + String currentBucketPrefix = String.valueOf(getCurrentBucketPrefix()); + ConcurrentHashMap.KeySetView keys = map.keySet(); + // remove obsolete keys + keys.removeIf(k -> !k.startsWith(currentBucketPrefix)); + } + } + + private class MaintenanceMonitor implements Runnable { + @Override + public void run() { + if (sleeptime > 0 && + (maintenanceFuture == null || maintenanceFuture.isDone())) { + if (maintenanceFuture != null && maintenanceFuture.isDone()) { + // There was an error executing the scheduled task, get it and log it + try { + maintenanceFuture.get(); + } catch (InterruptedException | ExecutionException e) { + log.error(sm.getString("timebucket.maintenance.error"), e); + } + } + maintenanceFuture = executorService.scheduleWithFixedDelay(new Maintenance(), sleeptime, sleeptime, + TimeUnit.MILLISECONDS); + } + } + } + +} diff --git a/java/org/apache/catalina/util/ToStringUtil.java b/java/org/apache/catalina/util/ToStringUtil.java new file mode 100644 index 0000000..d1602bf --- /dev/null +++ b/java/org/apache/catalina/util/ToStringUtil.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.util; + +import org.apache.catalina.Contained; +import org.apache.catalina.Container; +import org.apache.catalina.Manager; + +/** + * Utility class used to help generate return values for calls to + * {@link Object#toString()}. + */ +public class ToStringUtil { + + private ToStringUtil() { + // Utility class. Hide default constructor + } + + + public static final String toString(Contained contained) { + return toString(contained, contained.getContainer()); + } + + + public static final String toString(Object obj, Container container) { + return containedToString(obj, container, "Container"); + } + + + public static final String toString(Object obj, Manager manager) { + return containedToString(obj, manager, "Manager"); + } + + + private static String containedToString(Object contained, Object container, + String containerTypeName) { + StringBuilder sb = new StringBuilder(contained.getClass().getSimpleName()); + sb.append('['); + if (container == null) { + sb.append(containerTypeName); + sb.append(" is null"); + } else { + sb.append(container.toString()); + } + sb.append(']'); + return sb.toString(); + } +} diff --git a/java/org/apache/catalina/util/TomcatCSS.java b/java/org/apache/catalina/util/TomcatCSS.java new file mode 100644 index 0000000..21ffbcb --- /dev/null +++ b/java/org/apache/catalina/util/TomcatCSS.java @@ -0,0 +1,32 @@ +/* + * 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.catalina.util; + + +public class TomcatCSS { + + public static final String TOMCAT_CSS = + "body {font-family:Tahoma,Arial,sans-serif;} " + + "h1, h2, h3, b {color:white;background-color:#525D76;} " + + "h1 {font-size:22px;} " + + "h2 {font-size:16px;} " + + "h3 {font-size:14px;} " + + "p {font-size:12px;} " + + "a {color:black;} " + + ".line {height:1px;background-color:#525D76;border:none;}"; + +} diff --git a/java/org/apache/catalina/util/URLEncoder.java b/java/org/apache/catalina/util/URLEncoder.java new file mode 100644 index 0000000..2ac1d66 --- /dev/null +++ b/java/org/apache/catalina/util/URLEncoder.java @@ -0,0 +1,193 @@ +/* + * 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.catalina.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.util.BitSet; + +/** + * This class is very similar to the java.net.URLEncoder class. + * + * Unfortunately, with java.net.URLEncoder there is no way to specify to the + * java.net.URLEncoder which characters should NOT be encoded. + * + * This code was moved from DefaultServlet.java + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public final class URLEncoder implements Cloneable { + + private static final char[] hexadecimal = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + public static final URLEncoder DEFAULT = new URLEncoder(); + public static final URLEncoder QUERY = new URLEncoder(); + + static { + /* + * Encoder for URI paths, so from the spec: + * + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + // ALPHA and DIGIT are always treated as safe characters + // Add the remaining unreserved characters + DEFAULT.addSafeCharacter('-'); + DEFAULT.addSafeCharacter('.'); + DEFAULT.addSafeCharacter('_'); + DEFAULT.addSafeCharacter('~'); + // Add the sub-delims + DEFAULT.addSafeCharacter('!'); + DEFAULT.addSafeCharacter('$'); + DEFAULT.addSafeCharacter('&'); + DEFAULT.addSafeCharacter('\''); + DEFAULT.addSafeCharacter('('); + DEFAULT.addSafeCharacter(')'); + DEFAULT.addSafeCharacter('*'); + DEFAULT.addSafeCharacter('+'); + DEFAULT.addSafeCharacter(','); + DEFAULT.addSafeCharacter(';'); + DEFAULT.addSafeCharacter('='); + // Add the remaining literals + DEFAULT.addSafeCharacter(':'); + DEFAULT.addSafeCharacter('@'); + // Add '/' so it isn't encoded when we encode a path + DEFAULT.addSafeCharacter('/'); + + /* + * Encoder for query strings + * https://www.w3.org/TR/html5/forms.html#application/x-www-form-urlencoded-encoding-algorithm + * 0x20 ' ' -> '+' + * 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A as-is + * '*', '-', '.', '0' to '9', 'A' to 'Z', '_', 'a' to 'z' + * Also '=' and '&' are not encoded + * Everything else %nn encoded + */ + // Special encoding for space + QUERY.setEncodeSpaceAsPlus(true); + // Alpha and digit are safe by default + // Add the other permitted characters + QUERY.addSafeCharacter('*'); + QUERY.addSafeCharacter('-'); + QUERY.addSafeCharacter('.'); + QUERY.addSafeCharacter('_'); + QUERY.addSafeCharacter('='); + QUERY.addSafeCharacter('&'); + } + + //Array containing the safe characters set. + private final BitSet safeCharacters; + + private boolean encodeSpaceAsPlus = false; + + + public URLEncoder() { + this(new BitSet(256)); + + for (char i = 'a'; i <= 'z'; i++) { + addSafeCharacter(i); + } + for (char i = 'A'; i <= 'Z'; i++) { + addSafeCharacter(i); + } + for (char i = '0'; i <= '9'; i++) { + addSafeCharacter(i); + } + } + + + private URLEncoder(BitSet safeCharacters) { + this.safeCharacters = safeCharacters; + } + + + public void addSafeCharacter(char c) { + safeCharacters.set(c); + } + + + public void removeSafeCharacter(char c) { + safeCharacters.clear(c); + } + + + public void setEncodeSpaceAsPlus(boolean encodeSpaceAsPlus) { + this.encodeSpaceAsPlus = encodeSpaceAsPlus; + } + + + /** + * URL encodes the provided path using the given character set. + * + * @param path The path to encode + * @param charset The character set to use to convert the path to bytes + * + * @return The encoded path + */ + public String encode(String path, Charset charset) { + + int maxBytesPerChar = 10; + StringBuilder rewrittenPath = new StringBuilder(path.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + OutputStreamWriter writer = new OutputStreamWriter(buf, charset); + + for (int i = 0; i < path.length(); i++) { + int c = path.charAt(i); + if (safeCharacters.get(c)) { + rewrittenPath.append((char)c); + } else if (encodeSpaceAsPlus && c == ' ') { + rewrittenPath.append('+'); + } else { + // convert to external encoding before hex conversion + try { + writer.write((char)c); + writer.flush(); + } catch(IOException e) { + buf.reset(); + continue; + } + byte[] ba = buf.toByteArray(); + for (byte toEncode : ba) { + // Converting each byte in the buffer + rewrittenPath.append('%'); + int low = toEncode & 0x0f; + int high = (toEncode & 0xf0) >> 4; + rewrittenPath.append(hexadecimal[high]); + rewrittenPath.append(hexadecimal[low]); + } + buf.reset(); + } + } + return rewrittenPath.toString(); + } + + + @Override + public Object clone() { + URLEncoder result = new URLEncoder((BitSet) safeCharacters.clone()); + result.setEncodeSpaceAsPlus(encodeSpaceAsPlus); + return result; + } +} diff --git a/java/org/apache/catalina/util/XMLWriter.java b/java/org/apache/catalina/util/XMLWriter.java new file mode 100644 index 0000000..143c305 --- /dev/null +++ b/java/org/apache/catalina/util/XMLWriter.java @@ -0,0 +1,249 @@ +/* + * 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.catalina.util; + +import java.io.IOException; +import java.io.Writer; + +import org.apache.tomcat.util.security.Escape; + +/** + * XMLWriter helper class. + */ +public class XMLWriter { + + // -------------------------------------------------------------- Constants + + /** + * Opening tag. + */ + public static final int OPENING = 0; + + + /** + * Closing tag. + */ + public static final int CLOSING = 1; + + + /** + * Element with no content. + */ + public static final int NO_CONTENT = 2; + + + // ----------------------------------------------------- Instance Variables + + /** + * Buffer. + */ + protected StringBuilder buffer = new StringBuilder(); + + + /** + * Writer. + */ + protected final Writer writer; + + + protected boolean lastWriteWasOpen; + + + // ----------------------------------------------------------- Constructors + + /** + * New XML writer utility that will store its data in an internal buffer. + */ + public XMLWriter() { + this(null); + } + + + /** + * New XML writer utility that will store its data in an internal buffer + * and can write it to the specified writer. + *

    + * See {@link #sendData()} + * + * @param writer The writer to use + */ + public XMLWriter(Writer writer) { + this.writer = writer; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Retrieve generated XML. + * + * @return String containing the generated XML + */ + @Override + public String toString() { + return buffer.toString(); + } + + + /** + * Write property to the XML. + * + * @param namespace Namespace + * @param name Property name + * @param value Property value + */ + public void writeProperty(String namespace, String name, String value) { + writeElement(namespace, name, OPENING); + buffer.append(value); + writeElement(namespace, name, CLOSING); + } + + + /** + * Write an element. + * + * @param name Element name + * @param namespace Namespace abbreviation + * @param type Element type + */ + public void writeElement(String namespace, String name, int type) { + writeElement(namespace, null, name, type); + } + + + /** + * Write an element. + * + * @param namespace Namespace abbreviation + * @param namespaceInfo Namespace info + * @param name Element name + * @param type Element type + */ + public void writeElement(String namespace, String namespaceInfo, + String name, int type) { + if ((namespace != null) && (namespace.length() > 0)) { + switch (type) { + case OPENING: + if (lastWriteWasOpen) { + buffer.append('\n'); + } + if (namespaceInfo != null) { + buffer.append("<" + namespace + ":" + name + " xmlns:" + + namespace + "=\"" + + namespaceInfo + "\">"); + } else { + buffer.append("<" + namespace + ":" + name + ">"); + } + lastWriteWasOpen = true; + break; + case CLOSING: + buffer.append("\n"); + lastWriteWasOpen = false; + break; + case NO_CONTENT: + default: + if (lastWriteWasOpen) { + buffer.append('\n'); + } + if (namespaceInfo != null) { + buffer.append("<" + namespace + ":" + name + " xmlns:" + + namespace + "=\"" + + namespaceInfo + "\"/>\n"); + } else { + buffer.append("<" + namespace + ":" + name + "/>\n"); + } + lastWriteWasOpen = false; + break; + } + } else { + switch (type) { + case OPENING: + if (lastWriteWasOpen) { + buffer.append('\n'); + } + buffer.append("<" + name + ">"); + lastWriteWasOpen = true; + break; + case CLOSING: + buffer.append("\n"); + lastWriteWasOpen = false; + break; + case NO_CONTENT: + default: + if (lastWriteWasOpen) { + buffer.append('\n'); + } + buffer.append("<" + name + "/>\n"); + lastWriteWasOpen = false; + break; + } + } + } + + + /** + * Write text. + * + * @param text Text to append + */ + public void writeText(String text) { + buffer.append(Escape.xml(text)); + } + + + /** + * Write raw XML data. + * + * @param raw Raw XML to append + */ + public void writeRaw(String raw) { + buffer.append(raw); + } + + + /** + * Write data. + * + * @param data Data to append + */ + public void writeData(String data) { + buffer.append(""); + } + + + /** + * Write XML Header. + */ + public void writeXMLHeader() { + buffer.append("\n"); + } + + + /** + * Send data and reinitializes buffer, if a writer has been specified. + * @throws IOException Error writing XML data + */ + public void sendData() + throws IOException { + if (writer != null) { + writer.write(buffer.toString()); + buffer = new StringBuilder(); + } + } + + +} diff --git a/java/org/apache/catalina/valves/AbstractAccessLogValve.java b/java/org/apache/catalina/valves/AbstractAccessLogValve.java new file mode 100644 index 0000000..b1d77e9 --- /dev/null +++ b/java/org/apache/catalina/valves/AbstractAccessLogValve.java @@ -0,0 +1,1908 @@ +/* + * 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.catalina.valves; + + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.net.InetAddress; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Globals; +import org.apache.catalina.Session; +import org.apache.catalina.connector.ClientAbortException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.TLSUtil; +import org.apache.coyote.ActionCode; +import org.apache.coyote.RequestInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.net.IPv6Utils; + + +/** + *

    + * Abstract implementation of the Valve interface that generates a web server access log with the detailed line + * contents matching a configurable pattern. The syntax of the available patterns is similar to that supported by the + * Apache HTTP Server mod_log_config module. + *

    + *

    + * Patterns for the logged message may include constant text or any of the following replacement strings, for which the + * corresponding information from the specified Response is substituted: + *

    + *
      + *
    • %a - Remote IP address + *
    • %A - Local IP address + *
    • %b - Bytes sent, excluding HTTP headers, or '-' if no bytes were sent + *
    • %B - Bytes sent, excluding HTTP headers + *
    • %h - Remote host name (or IP address if enableLookups for the connector is + * false) + *
    • %H - Request protocol + *
    • %l - Remote logical username from identd (always returns '-') + *
    • %m - Request method + *
    • %p - Local port + *
    • %q - Query string (prepended with a '?' if it exists, otherwise an empty string + *
    • %r - First line of the request + *
    • %s - HTTP status code of the response + *
    • %S - User session ID + *
    • %t - Date and time, in Common Log Format format + *
    • %u - Remote user that was authenticated + *
    • %U - Requested URL path + *
    • %v - Local server name + *
    • %D - Time taken to process the request, in microseconds + *
    • %T - Time taken to process the request, in seconds + *
    • %F - Time taken to commit the response, in milliseconds + *
    • %I - current Request thread name (can compare later with stacktraces) + *
    • %X - Connection status when response is completed: + *
        + *
      • X = Connection aborted before the response completed.
      • + *
      • + = Connection may be kept alive after the response is sent.
      • + *
      • - = Connection will be closed after the response is sent.
      • + *
      + *
    + *

    + * In addition, the caller can specify one of the following aliases for commonly utilized patterns: + *

    + *
      + *
    • common - %h %l %u %t "%r" %s %b + *
    • combined - %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" + *
    + *

    + * There is also support to write information from the cookie, incoming header, the Session or something else in the + * ServletRequest.
    + * It is modeled after the Apache HTTP Server log configuration syntax: + *

    + *
      + *
    • %{xxx}i for incoming headers + *
    • %{xxx}o for outgoing response headers + *
    • %{xxx}c for a specific cookie + *
    • %{xxx}r xxx is an attribute in the ServletRequest + *
    • %{xxx}s xxx is an attribute in the HttpSession + *
    • %{xxx}t xxx is an enhanced SimpleDateFormat pattern (see Configuration Reference document for + * details on supported time patterns) + *
    • %{xxx}T xxx is the unit for the time taken to process the request (see Configuration Reference + * document for details on supported units) + *
    + *

    + * Conditional logging is also supported. This can be done with the conditionUnless and + * conditionIf properties. If the value returned from ServletRequest.getAttribute(conditionUnless) yields a + * non-null value, the logging will be skipped. If the value returned from ServletRequest.getAttribute(conditionIf) + * yields the null value, the logging will be skipped. The condition attribute is synonym for + * conditionUnless and is provided for backwards compatibility. + *

    + *

    + * For extended attributes coming from a getAttribute() call, it is you responsibility to ensure there are no newline or + * control characters. + *

    + * + * @author Craig R. McClanahan + * @author Jason Brittain + * @author Remy Maucherat + * @author Takayuki Kaneko + * @author Peter Rossbach + */ +public abstract class AbstractAccessLogValve extends ValveBase implements AccessLog { + + private static final Log log = LogFactory.getLog(AbstractAccessLogValve.class); + + /** + * The list of our time format types. + */ + private enum FormatType { + CLF, + SEC, + MSEC, + MSEC_FRAC, + SDF + } + + /** + * The list of our port types. + */ + private enum PortType { + LOCAL, + REMOTE + } + + /** + * The list of our ip address types. + */ + private enum RemoteAddressType { + REMOTE, + PEER + } + + // ------------------------------------------------------ Constructor + public AbstractAccessLogValve() { + super(true); + } + + // ----------------------------------------------------- Instance Variables + + + /** + * enabled this component + */ + protected boolean enabled = true; + + /** + * Use IPv6 canonical representation format as defined by RFC 5952. + */ + private boolean ipv6Canonical = false; + + /** + * The pattern used to format our access log lines. + */ + protected String pattern = null; + + /** + * The size of our global date format cache + */ + private static final int globalCacheSize = 300; + + /** + * The size of our thread local date format cache + */ + private static final int localCacheSize = 60; + + /** + *

    + * Cache structure for formatted timestamps based on seconds. + *

    + *

    + * The cache consists of entries for a consecutive range of seconds. The length of the range is configurable. It is + * implemented based on a cyclic buffer. New entries shift the range. + *

    + *

    + * There is one cache for the CLF format (the access log standard format) and a HashMap of caches for additional + * formats used by SimpleDateFormat. + *

    + *

    + * Although the cache supports specifying a locale when retrieving a formatted timestamp, each format will always + * use the locale given when the format was first used. New locales can only be used for new formats. The CLF format + * will always be formatted using the locale en_US. + *

    + *

    + * The cache is not threadsafe. It can be used without synchronization via thread local instances, or with + * synchronization as a global cache. + *

    + *

    + * The cache can be created with a parent cache to build a cache hierarchy. Access to the parent cache is + * threadsafe. + *

    + *

    + * This class uses a small thread local first level cache and a bigger synchronized global second level cache. + *

    + */ + protected static class DateFormatCache { + + protected class Cache { + + /* CLF log format */ + private static final String cLFFormat = "dd/MMM/yyyy:HH:mm:ss Z"; + + /* Second used to retrieve CLF format in most recent invocation */ + private long previousSeconds = Long.MIN_VALUE; + /* Value of CLF format retrieved in most recent invocation */ + private String previousFormat = ""; + + /* First second contained in cache */ + private long first = Long.MIN_VALUE; + /* Last second contained in cache */ + private long last = Long.MIN_VALUE; + /* Index of "first" in the cyclic cache */ + private int offset = 0; + /* Helper object to be able to call SimpleDateFormat.format(). */ + private final Date currentDate = new Date(); + + protected final String cache[]; + private SimpleDateFormat formatter; + private boolean isCLF = false; + + private Cache parent = null; + + private Cache(Cache parent) { + this(null, parent); + } + + private Cache(String format, Cache parent) { + this(format, null, parent); + } + + private Cache(String format, Locale loc, Cache parent) { + cache = new String[cacheSize]; + for (int i = 0; i < cacheSize; i++) { + cache[i] = null; + } + if (loc == null) { + loc = cacheDefaultLocale; + } + if (format == null) { + isCLF = true; + format = cLFFormat; + formatter = new SimpleDateFormat(format, Locale.US); + } else { + formatter = new SimpleDateFormat(format, loc); + } + formatter.setTimeZone(TimeZone.getDefault()); + this.parent = parent; + } + + private String getFormatInternal(long time) { + + long seconds = time / 1000; + + /* + * First step: if we have seen this timestamp during the previous call, and we need CLF, return the + * previous value. + */ + if (seconds == previousSeconds) { + return previousFormat; + } + + /* Second step: Try to locate in cache */ + previousSeconds = seconds; + int index = (offset + (int) (seconds - first)) % cacheSize; + if (index < 0) { + index += cacheSize; + } + if (seconds >= first && seconds <= last) { + if (cache[index] != null) { + /* Found, so remember for next call and return. */ + previousFormat = cache[index]; + return previousFormat; + } + + /* Third step: not found in cache, adjust cache and add item */ + } else if (seconds >= last + cacheSize || seconds <= first - cacheSize) { + first = seconds; + last = first + cacheSize - 1; + index = 0; + offset = 0; + for (int i = 1; i < cacheSize; i++) { + cache[i] = null; + } + } else if (seconds > last) { + for (int i = 1; i < seconds - last; i++) { + cache[(index + cacheSize - i) % cacheSize] = null; + } + first = seconds - (cacheSize - 1); + last = seconds; + offset = (index + 1) % cacheSize; + } else if (seconds < first) { + for (int i = 1; i < first - seconds; i++) { + cache[(index + i) % cacheSize] = null; + } + first = seconds; + last = seconds + (cacheSize - 1); + offset = index; + } + + /* + * Last step: format new timestamp either using parent cache or locally. + */ + if (parent != null) { + synchronized (parent) { + previousFormat = parent.getFormatInternal(time); + } + } else { + currentDate.setTime(time); + previousFormat = formatter.format(currentDate); + if (isCLF) { + StringBuilder current = new StringBuilder(32); + current.append('['); + current.append(previousFormat); + current.append(']'); + previousFormat = current.toString(); + } + } + cache[index] = previousFormat; + return previousFormat; + } + } + + /* Number of cached entries */ + private int cacheSize = 0; + + private final Locale cacheDefaultLocale; + private final DateFormatCache parent; + protected final Cache cLFCache; + private final Map formatCache = new HashMap<>(); + + protected DateFormatCache(int size, Locale loc, DateFormatCache parent) { + cacheSize = size; + cacheDefaultLocale = loc; + this.parent = parent; + Cache parentCache = null; + if (parent != null) { + synchronized (parent) { + parentCache = parent.getCache(null, null); + } + } + cLFCache = new Cache(parentCache); + } + + private Cache getCache(String format, Locale loc) { + Cache cache; + if (format == null) { + cache = cLFCache; + } else { + cache = formatCache.get(format); + if (cache == null) { + Cache parentCache = null; + if (parent != null) { + synchronized (parent) { + parentCache = parent.getCache(format, loc); + } + } + cache = new Cache(format, loc, parentCache); + formatCache.put(format, cache); + } + } + return cache; + } + + public String getFormat(long time) { + return cLFCache.getFormatInternal(time); + } + + public String getFormat(String format, Locale loc, long time) { + return getCache(format, loc).getFormatInternal(time); + } + } + + /** + * Global date format cache. + */ + private static final DateFormatCache globalDateCache = new DateFormatCache(globalCacheSize, Locale.getDefault(), + null); + + /** + * Thread local date format cache. + */ + private static final ThreadLocal localDateCache = ThreadLocal + .withInitial(() -> new DateFormatCache(localCacheSize, Locale.getDefault(), globalDateCache)); + + + /** + * The system time when we last updated the Date that this valve uses for log lines. + */ + private static final ThreadLocal localDate = ThreadLocal.withInitial(Date::new); + + /** + * Are we doing conditional logging. default null. It is the value of conditionUnless property. + */ + protected String condition = null; + + /** + * Are we doing conditional logging. default null. It is the value of conditionIf property. + */ + protected String conditionIf = null; + + /** + * Name of locale used to format timestamps in log entries and in log file name suffix. + */ + protected String localeName = Locale.getDefault().toString(); + + + /** + * Locale used to format timestamps in log entries and in log file name suffix. + */ + protected Locale locale = Locale.getDefault(); + + /** + * Array of AccessLogElement, they will be used to make log message. + */ + protected AccessLogElement[] logElements = null; + + /** + * Array of elements where the value needs to be cached at the start of the request. + */ + protected CachedElement[] cachedElements = null; + + /** + * Should this valve use request attributes for IP address, hostname, protocol and port used for the request. + * Default is false. + * + * @see #setRequestAttributesEnabled(boolean) + */ + protected boolean requestAttributesEnabled = false; + + /** + * Buffer pool used for log message generation. Pool used to reduce garbage generation. + */ + private SynchronizedStack charArrayWriters = new SynchronizedStack<>(); + + /** + * Log message buffers are usually recycled and re-used. To prevent excessive memory usage, if a buffer grows beyond + * this size it will be discarded. The default is 256 characters. This should be set to larger than the typical + * access log message size. + */ + private int maxLogMessageBufferSize = 256; + + /** + * Does the configured log pattern include a known TLS attribute? + */ + private boolean tlsAttributeRequired = false; + + + // ------------------------------------------------------------- Properties + + public int getMaxLogMessageBufferSize() { + return maxLogMessageBufferSize; + } + + + public void setMaxLogMessageBufferSize(int maxLogMessageBufferSize) { + this.maxLogMessageBufferSize = maxLogMessageBufferSize; + } + + + public boolean getIpv6Canonical() { + return ipv6Canonical; + } + + + public void setIpv6Canonical(boolean ipv6Canonical) { + this.ipv6Canonical = ipv6Canonical; + } + + + /** + * {@inheritDoc} Default is false. + */ + @Override + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + this.requestAttributesEnabled = requestAttributesEnabled; + } + + + @Override + public boolean getRequestAttributesEnabled() { + return requestAttributesEnabled; + } + + /** + * @return the enabled flag. + */ + public boolean getEnabled() { + return enabled; + } + + /** + * @param enabled The enabled to set. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * @return the format pattern. + */ + public String getPattern() { + return this.pattern; + } + + + /** + * Set the format pattern, first translating any recognized alias. + * + * @param pattern The new pattern + */ + public void setPattern(String pattern) { + if (pattern == null) { + this.pattern = ""; + } else if (pattern.equals(Constants.AccessLog.COMMON_ALIAS)) { + this.pattern = Constants.AccessLog.COMMON_PATTERN; + } else if (pattern.equals(Constants.AccessLog.COMBINED_ALIAS)) { + this.pattern = Constants.AccessLog.COMBINED_PATTERN; + } else { + this.pattern = pattern; + } + logElements = createLogElements(); + if (logElements != null) { + cachedElements = createCachedElements(logElements); + } + } + + /** + * Return whether the attribute name to look for when performing conditional logging. If null, every request is + * logged. + * + * @return the attribute name + */ + public String getCondition() { + return condition; + } + + + /** + * Set the ServletRequest.attribute to look for to perform conditional logging. Set to null to log everything. + * + * @param condition Set to null to log everything + */ + public void setCondition(String condition) { + this.condition = condition; + } + + + /** + * Return whether the attribute name to look for when performing conditional logging. If null, every request is + * logged. + * + * @return the attribute name + */ + public String getConditionUnless() { + return getCondition(); + } + + + /** + * Set the ServletRequest.attribute to look for to perform conditional logging. Set to null to log everything. + * + * @param condition Set to null to log everything + */ + public void setConditionUnless(String condition) { + setCondition(condition); + } + + /** + * Return whether the attribute name to look for when performing conditional logging. If null, every request is + * logged. + * + * @return the attribute name + */ + public String getConditionIf() { + return conditionIf; + } + + + /** + * Set the ServletRequest.attribute to look for to perform conditional logging. Set to null to log everything. + * + * @param condition Set to null to log everything + */ + public void setConditionIf(String condition) { + this.conditionIf = condition; + } + + /** + * Return the locale used to format timestamps in log entries and in log file name suffix. + * + * @return the locale + */ + public String getLocale() { + return localeName; + } + + + /** + * Set the locale used to format timestamps in log entries and in log file name suffix. Changing the locale is only + * supported as long as the AccessLogValve has not logged anything. Changing the locale later can lead to + * inconsistent formatting. + * + * @param localeName The locale to use. + */ + public void setLocale(String localeName) { + this.localeName = localeName; + locale = findLocale(localeName, locale); + } + + // --------------------------------------------------------- Public Methods + + /** + * Log a message summarizing the specified request and response, according to the format specified by the + * pattern property. + * + * @param request Request being processed + * @param response Response being processed + * + * @exception IOException if an input/output error has occurred + * @exception ServletException if a servlet error has occurred + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + if (tlsAttributeRequired) { + // The log pattern uses TLS attributes. Ensure these are populated + // before the request is processed because with NIO2 it is possible + // for the connection to be closed (and the TLS info lost) before + // the access log requests the TLS info. Requesting it now causes it + // to be cached in the request. + request.getAttribute(Globals.CERTIFICATES_ATTR); + } + if (cachedElements != null) { + for (CachedElement element : cachedElements) { + element.cache(request); + } + } + getNext().invoke(request, response); + } + + + @Override + public void log(Request request, Response response, long time) { + if (!getState().isAvailable() || !getEnabled() || logElements == null || + condition != null && null != request.getRequest().getAttribute(condition) || + conditionIf != null && null == request.getRequest().getAttribute(conditionIf)) { + return; + } + + // Date for access log should be the beginning of the request + Date date = getDate(request.getCoyoteRequest().getStartTime()); + + CharArrayWriter result = charArrayWriters.pop(); + if (result == null) { + result = new CharArrayWriter(128); + } + + for (AccessLogElement logElement : logElements) { + logElement.addElement(result, date, request, response, time); + } + + log(result); + + if (result.size() <= maxLogMessageBufferSize) { + result.reset(); + charArrayWriters.push(result); + } + } + + // -------------------------------------------------------- Protected Methods + + /** + * Log the specified message. + * + * @param message Message to be logged. This object will be recycled by the calling method. + */ + protected abstract void log(CharArrayWriter message); + + // -------------------------------------------------------- Private Methods + + /** + * This method returns a Date object that is accurate to within one second. If a thread calls this method to get a + * Date and it's been less than 1 second since a new Date was created, this method simply gives out the same Date + * again so that the system doesn't spend time creating Date objects unnecessarily. + * + * @param systime The time + * + * @return the date object + */ + private static Date getDate(long systime) { + Date date = localDate.get(); + date.setTime(systime); + return date; + } + + + /** + * Find a locale by name. + * + * @param name The locale name + * @param fallback Fallback locale if the name is not found + * + * @return the locale object + */ + protected static Locale findLocale(String name, Locale fallback) { + if (name == null || name.isEmpty()) { + return Locale.getDefault(); + } else { + for (Locale l : Locale.getAvailableLocales()) { + if (name.equals(l.toString())) { + return l; + } + } + } + log.error(sm.getString("accessLogValve.invalidLocale", name)); + return fallback; + } + + + /** + * AccessLogElement writes the partial message into the buffer. + */ + protected interface AccessLogElement { + void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time); + } + + /** + * Marks an AccessLogElement as needing to be have the value cached at the start of the request rather than just + * recorded at the end as the source data for the element may not be available at the end of the request. This + * typically occurs for remote network information, such as ports, IP addresses etc. when the connection is closed + * unexpectedly. These elements take advantage of these values being cached elsewhere on first request and do not + * cache the value in the element since the elements are state-less. + */ + protected interface CachedElement { + void cache(Request request); + } + + /** + * write thread name - %I + */ + protected static class ThreadNameElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + RequestInfo info = request.getCoyoteRequest().getRequestProcessor(); + if (info != null) { + buf.append(info.getWorkerThreadName()); + } else { + buf.append('-'); + } + } + } + + /** + * write local IP address - %A + */ + protected static class LocalAddrElement implements AccessLogElement { + + private final String localAddrValue; + + public LocalAddrElement(boolean ipv6Canonical) { + String init; + try { + init = InetAddress.getLocalHost().getHostAddress(); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + init = "127.0.0.1"; + } + + if (ipv6Canonical) { + localAddrValue = IPv6Utils.canonize(init); + } else { + localAddrValue = init; + } + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(localAddrValue); + } + } + + /** + * write remote IP address - %a + */ + protected class RemoteAddrElement implements AccessLogElement, CachedElement { + /** + * Type of address to log + */ + private static final String remoteAddress = "remote"; + private static final String peerAddress = "peer"; + + private final RemoteAddressType remoteAddressType; + + public RemoteAddrElement() { + remoteAddressType = RemoteAddressType.REMOTE; + } + + public RemoteAddrElement(String type) { + switch (type) { + case remoteAddress: + remoteAddressType = RemoteAddressType.REMOTE; + break; + case peerAddress: + remoteAddressType = RemoteAddressType.PEER; + break; + default: + log.error(sm.getString("accessLogValve.invalidRemoteAddressType", type)); + remoteAddressType = RemoteAddressType.REMOTE; + break; + } + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + String value = null; + if (remoteAddressType == RemoteAddressType.PEER) { + value = request.getPeerAddr(); + } else { + if (requestAttributesEnabled) { + Object addr = request.getAttribute(REMOTE_ADDR_ATTRIBUTE); + if (addr == null) { + value = request.getRemoteAddr(); + } else { + value = addr.toString(); + } + } else { + value = request.getRemoteAddr(); + } + } + + if (ipv6Canonical) { + value = IPv6Utils.canonize(value); + } + buf.append(value); + } + + @Override + public void cache(Request request) { + if (!requestAttributesEnabled) { + if (remoteAddressType == RemoteAddressType.PEER) { + request.getPeerAddr(); + } else { + request.getRemoteAddr(); + } + } + } + } + + /** + * write remote host name - %h + */ + protected class HostElement implements AccessLogElement, CachedElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + String value = null; + if (requestAttributesEnabled) { + Object host = request.getAttribute(REMOTE_HOST_ATTRIBUTE); + if (host != null) { + value = host.toString(); + } + } + if (value == null || value.length() == 0) { + value = request.getRemoteHost(); + } + if (value == null || value.length() == 0) { + value = "-"; + } + + if (ipv6Canonical) { + value = IPv6Utils.canonize(value); + } + buf.append(value); + } + + @Override + public void cache(Request request) { + if (!requestAttributesEnabled) { + request.getRemoteHost(); + } + } + } + + /** + * write remote logical username from identd (always returns '-') - %l + */ + protected static class LogicalUserNameElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append('-'); + } + } + + /** + * write request protocol - %H + */ + protected class ProtocolElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (requestAttributesEnabled) { + Object proto = request.getAttribute(PROTOCOL_ATTRIBUTE); + if (proto == null) { + buf.append(request.getProtocol()); + } else { + buf.append(proto.toString()); + } + } else { + buf.append(request.getProtocol()); + } + } + } + + /** + * write remote user that was authenticated (if any), else '-' - %u + */ + protected static class UserElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (request != null) { + String value = request.getRemoteUser(); + if (value != null) { + escapeAndAppend(value, buf); + } else { + buf.append('-'); + } + } else { + buf.append('-'); + } + } + } + + /** + * write date and time, in configurable format (default CLF) - %t or %{format}t + */ + protected class DateAndTimeElement implements AccessLogElement { + + /** + * Format prefix specifying request start time + */ + private static final String requestStartPrefix = "begin"; + + /** + * Format prefix specifying response end time + */ + private static final String responseEndPrefix = "end"; + + /** + * Separator between optional prefix and rest of format + */ + private static final String prefixSeparator = ":"; + + /** + * Special format for seconds since epoch + */ + private static final String secFormat = "sec"; + + /** + * Special format for milliseconds since epoch + */ + private static final String msecFormat = "msec"; + + /** + * Special format for millisecond part of timestamp + */ + private static final String msecFractionFormat = "msec_frac"; + + /** + * The patterns we use to replace "S" and "SSS" millisecond formatting of SimpleDateFormat by our own handling + */ + private static final String msecPattern = "{#}"; + private static final String tripleMsecPattern = msecPattern + msecPattern + msecPattern; + + /* Our format description string, null if CLF */ + private final String format; + /* Does the format string contain characters we need to escape */ + private final boolean needsEscaping; + /* Whether to use begin of request or end of response as the timestamp */ + private final boolean usesBegin; + /* The format type */ + private final FormatType type; + /* Whether we need to postprocess by adding milliseconds */ + private boolean usesMsecs = false; + + protected DateAndTimeElement() { + this(null); + } + + /** + * Replace the millisecond formatting character 'S' by some dummy characters in order to make the resulting + * formatted time stamps cacheable. We replace the dummy chars later with the actual milliseconds because that's + * relatively cheap. + */ + private String tidyFormat(String format) { + boolean escape = false; + StringBuilder result = new StringBuilder(); + int len = format.length(); + char x; + for (int i = 0; i < len; i++) { + x = format.charAt(i); + if (escape || x != 'S') { + result.append(x); + } else { + result.append(msecPattern); + usesMsecs = true; + } + if (x == '\'') { + escape = !escape; + } + } + return result.toString(); + } + + protected DateAndTimeElement(String sdf) { + String format = sdf; + boolean needsEscaping = false; + if (sdf != null) { + CharArrayWriter writer = new CharArrayWriter(); + escapeAndAppend(sdf, writer); + String escaped = writer.toString(); + if (!escaped.equals(sdf)) { + needsEscaping = true; + } + } + this.needsEscaping = needsEscaping; + boolean usesBegin = false; + FormatType type = FormatType.CLF; + + if (format != null) { + if (format.equals(requestStartPrefix)) { + usesBegin = true; + format = ""; + } else if (format.startsWith(requestStartPrefix + prefixSeparator)) { + usesBegin = true; + format = format.substring(6); + } else if (format.equals(responseEndPrefix)) { + usesBegin = false; + format = ""; + } else if (format.startsWith(responseEndPrefix + prefixSeparator)) { + usesBegin = false; + format = format.substring(4); + } + if (format.length() == 0) { + type = FormatType.CLF; + } else if (format.equals(secFormat)) { + type = FormatType.SEC; + } else if (format.equals(msecFormat)) { + type = FormatType.MSEC; + } else if (format.equals(msecFractionFormat)) { + type = FormatType.MSEC_FRAC; + } else { + type = FormatType.SDF; + format = tidyFormat(format); + } + } + this.format = format; + this.usesBegin = usesBegin; + this.type = type; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + long timestamp = date.getTime(); + long frac; + if (!usesBegin) { + timestamp += TimeUnit.NANOSECONDS.toMillis(time); + } + /* + * Implementation note: This is deliberately not implemented using switch. If a switch is used the compiler + * (at least the Oracle one) will use a synthetic class to implement the switch. The problem is that this + * class needs to be pre-loaded when using a SecurityManager and the name of that class will depend on any + * anonymous inner classes and any other synthetic classes. As such the name is not constant and keeping the + * pre-loading up to date as the name changes is error prone. + */ + if (type == FormatType.CLF) { + buf.append(localDateCache.get().getFormat(timestamp)); + } else if (type == FormatType.SEC) { + buf.append(Long.toString(timestamp / 1000)); + } else if (type == FormatType.MSEC) { + buf.append(Long.toString(timestamp)); + } else if (type == FormatType.MSEC_FRAC) { + frac = timestamp % 1000; + if (frac < 100) { + if (frac < 10) { + buf.append('0'); + buf.append('0'); + } else { + buf.append('0'); + } + } + buf.append(Long.toString(frac)); + } else { + // FormatType.SDF + String temp = localDateCache.get().getFormat(format, locale, timestamp); + if (usesMsecs) { + frac = timestamp % 1000; + StringBuilder tripleMsec = new StringBuilder(4); + if (frac < 100) { + if (frac < 10) { + tripleMsec.append('0'); + tripleMsec.append('0'); + } else { + tripleMsec.append('0'); + } + } + tripleMsec.append(frac); + temp = temp.replace(tripleMsecPattern, tripleMsec); + temp = temp.replace(msecPattern, Long.toString(frac)); + } + if (needsEscaping) { + escapeAndAppend(temp, buf); + } else { + buf.append(temp); + } + } + } + } + + /** + * write first line of the request (method and request URI) - %r + */ + protected static class RequestElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (request != null) { + String method = request.getMethod(); + if (method == null) { + // No method means no request line + buf.append('-'); + } else { + buf.append(request.getMethod()); + buf.append(' '); + buf.append(request.getRequestURI()); + if (request.getQueryString() != null) { + buf.append('?'); + buf.append(request.getQueryString()); + } + buf.append(' '); + buf.append(request.getProtocol()); + } + } else { + buf.append('-'); + } + } + } + + /** + * write HTTP status code of the response - %s + */ + protected static class HttpStatusCodeElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (response != null) { + // This approach is used to reduce GC from toString conversion + int status = response.getStatus(); + if (100 <= status && status < 1000) { + buf.append((char) ('0' + (status / 100))).append((char) ('0' + ((status / 10) % 10))) + .append((char) ('0' + (status % 10))); + } else { + buf.append(Integer.toString(status)); + } + } else { + buf.append('-'); + } + } + } + + /** + * write local or remote port for request connection - %p and %{xxx}p + */ + protected class PortElement implements AccessLogElement, CachedElement { + + /** + * Type of port to log + */ + private static final String localPort = "local"; + private static final String remotePort = "remote"; + + private final PortType portType; + + public PortElement() { + portType = PortType.LOCAL; + } + + public PortElement(String type) { + switch (type) { + case remotePort: + portType = PortType.REMOTE; + break; + case localPort: + portType = PortType.LOCAL; + break; + default: + log.error(sm.getString("accessLogValve.invalidPortType", type)); + portType = PortType.LOCAL; + break; + } + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (requestAttributesEnabled && portType == PortType.LOCAL) { + Object port = request.getAttribute(SERVER_PORT_ATTRIBUTE); + if (port == null) { + buf.append(Integer.toString(request.getServerPort())); + } else { + buf.append(port.toString()); + } + } else { + if (portType == PortType.LOCAL) { + buf.append(Integer.toString(request.getServerPort())); + } else { + buf.append(Integer.toString(request.getRemotePort())); + } + } + } + + @Override + public void cache(Request request) { + if (portType == PortType.REMOTE) { + request.getRemotePort(); + } + } + } + + /** + * write bytes sent, excluding HTTP headers - %b, %B + */ + protected static class ByteSentElement implements AccessLogElement { + private final boolean conversion; + + /** + * @param conversion true to write '-' instead of 0 - %b. + */ + public ByteSentElement(boolean conversion) { + this.conversion = conversion; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + // Don't need to flush since trigger for log message is after the + // response has been committed + long length = response.getBytesWritten(false); + if (length <= 0) { + // Protect against nulls and unexpected types as these values + // may be set by untrusted applications + Object start = request.getAttribute(Globals.SENDFILE_FILE_START_ATTR); + if (start instanceof Long) { + Object end = request.getAttribute(Globals.SENDFILE_FILE_END_ATTR); + if (end instanceof Long) { + length = ((Long) end).longValue() - ((Long) start).longValue(); + } + } + } + if (length <= 0 && conversion) { + buf.append('-'); + } else { + buf.append(Long.toString(length)); + } + } + } + + /** + * write request method (GET, POST, etc.) - %m + */ + protected static class MethodElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (request != null) { + buf.append(request.getMethod()); + } + } + } + + /** + * write time taken to process the request - %D, %T + */ + protected static class ElapsedTimeElement implements AccessLogElement { + private final boolean micros; + private final boolean millis; + + /** + * @param micros true, write time in microseconds - %D + * @param millis true, write time in milliseconds, if both arguments are false, write + * time in seconds - %T + */ + public ElapsedTimeElement(boolean micros, boolean millis) { + this.micros = micros; + this.millis = millis; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (micros) { + buf.append(Long.toString(TimeUnit.NANOSECONDS.toMicros(time))); + } else if (millis) { + buf.append(Long.toString(TimeUnit.NANOSECONDS.toMillis(time))); + } else { + // second + buf.append(Long.toString(TimeUnit.NANOSECONDS.toSeconds(time))); + } + } + } + + /** + * write time until first byte is written (commit time) in millis - %F + */ + protected static class FirstByteTimeElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + long commitTime = response.getCoyoteResponse().getCommitTimeNanos(); + if (commitTime == -1) { + buf.append('-'); + } else { + long delta = commitTime - request.getCoyoteRequest().getStartTimeNanos(); + buf.append(Long.toString(TimeUnit.NANOSECONDS.toMillis(delta))); + } + } + } + + /** + * write Query string (prepended with a '?' if it exists) - %q + */ + protected static class QueryElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + String query = null; + if (request != null) { + query = request.getQueryString(); + } + if (query != null) { + buf.append('?'); + buf.append(query); + } + } + } + + /** + * write user session ID - %S + */ + protected static class SessionIdElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (request == null) { + buf.append('-'); + } else { + Session session = request.getSessionInternal(false); + if (session == null) { + buf.append('-'); + } else { + buf.append(session.getIdInternal()); + } + } + } + } + + /** + * write requested URL path - %U + */ + protected static class RequestURIElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (request != null) { + buf.append(request.getRequestURI()); + } else { + buf.append('-'); + } + } + } + + /** + * write local server name - %v + */ + protected class LocalServerNameElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + String value = null; + if (requestAttributesEnabled) { + Object serverName = request.getAttribute(SERVER_NAME_ATTRIBUTE); + if (serverName != null) { + value = serverName.toString(); + } + } + if (value == null || value.length() == 0) { + value = request.getServerName(); + } + if (value == null || value.length() == 0) { + value = "-"; + } + + if (ipv6Canonical) { + value = IPv6Utils.canonize(value); + } + buf.append(value); + } + } + + /** + * write any string + */ + protected static class StringElement implements AccessLogElement { + private final String str; + + public StringElement(String str) { + this.str = str; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(str); + } + } + + /** + * write incoming headers - %{xxx}i + */ + protected static class HeaderElement implements AccessLogElement { + private final String header; + + public HeaderElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + Enumeration iter = request.getHeaders(header); + if (iter.hasMoreElements()) { + escapeAndAppend(iter.nextElement(), buf); + while (iter.hasMoreElements()) { + buf.append(','); + escapeAndAppend(iter.nextElement(), buf); + } + return; + } + buf.append('-'); + } + } + + /** + * write a specific cookie - %{xxx}c + */ + protected static class CookieElement implements AccessLogElement { + private final String cookieNameToLog; + + public CookieElement(String cookieNameToLog) { + this.cookieNameToLog = cookieNameToLog; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + StringBuilder value = new StringBuilder(); + boolean first = true; + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookieNameToLog.equals(cookie.getName())) { + if (first) { + first = false; + } else { + value.append(','); + } + value.append(cookie.getValue()); + } + } + } + if (value.length() == 0) { + buf.append('-'); + } else { + escapeAndAppend(value.toString(), buf); + } + } + } + + /** + * write a specific response header - %{xxx}o + */ + protected static class ResponseHeaderElement implements AccessLogElement { + private final String header; + + public ResponseHeaderElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (null != response) { + Iterator iter = response.getHeaders(header).iterator(); + if (iter.hasNext()) { + escapeAndAppend(iter.next(), buf); + while (iter.hasNext()) { + buf.append(','); + escapeAndAppend(iter.next(), buf); + } + return; + } + } + buf.append('-'); + } + } + + /** + * write an attribute in the ServletRequest - %{xxx}r + */ + protected static class RequestAttributeElement implements AccessLogElement { + private final String attribute; + + public RequestAttributeElement(String attribute) { + this.attribute = attribute; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + Object value = null; + if (request != null) { + value = request.getAttribute(attribute); + } else { + value = "??"; + } + if (value != null) { + if (value instanceof String) { + escapeAndAppend((String) value, buf); + } else { + escapeAndAppend(value.toString(), buf); + } + } else { + buf.append('-'); + } + } + } + + /** + * write an attribute in the HttpSession - %{xxx}s + */ + protected static class SessionAttributeElement implements AccessLogElement { + private final String attribute; + + public SessionAttributeElement(String attribute) { + this.attribute = attribute; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + Object value = null; + if (null != request) { + HttpSession sess = request.getSession(false); + if (null != sess) { + value = sess.getAttribute(attribute); + } + } else { + value = "??"; + } + if (value != null) { + if (value instanceof String) { + escapeAndAppend((String) value, buf); + } else { + escapeAndAppend(value.toString(), buf); + } + } else { + buf.append('-'); + } + } + } + + /** + * Write connection status when response is completed - %X + */ + protected static class ConnectionStatusElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (response != null && request != null) { + boolean statusFound = false; + + // Check whether connection IO is in "not allowed" state + AtomicBoolean isIoAllowed = new AtomicBoolean(false); + request.getCoyoteRequest().action(ActionCode.IS_IO_ALLOWED, isIoAllowed); + if (!isIoAllowed.get()) { + buf.append('X'); + statusFound = true; + } else { + // Check for connection aborted cond + if (response.isError()) { + Throwable ex = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + if (ex instanceof ClientAbortException) { + buf.append('X'); + statusFound = true; + } + } + } + + // If status is not found yet, cont to check whether connection is keep-alive or close + if (!statusFound) { + String connStatus = response.getHeader(org.apache.coyote.http11.Constants.CONNECTION); + if (org.apache.coyote.http11.Constants.CLOSE.equalsIgnoreCase(connStatus)) { + buf.append('-'); + } else { + buf.append('+'); + } + } + } else { + // Unknown connection status + buf.append('?'); + } + } + } + + /** + * Parse pattern string and create the array of AccessLogElement. + * + * @return the log elements array + */ + protected AccessLogElement[] createLogElements() { + List list = new ArrayList<>(); + boolean replace = false; + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (replace) { + /* + * For code that processes {, the behavior will be ... if I do not encounter a closing } - then I ignore + * the { + */ + if ('{' == ch) { + StringBuilder name = new StringBuilder(); + int j = i + 1; + for (; j < pattern.length() && '}' != pattern.charAt(j); j++) { + name.append(pattern.charAt(j)); + } + if (j + 1 < pattern.length()) { + /* the +1 was to account for } which we increment now */ + j++; + list.add(createAccessLogElement(name.toString(), pattern.charAt(j))); + i = j; /* Since we walked more than one character */ + } else { + // D'oh - end of string - pretend we never did this + // and do processing the "old way" + list.add(createAccessLogElement(ch)); + } + } else { + list.add(createAccessLogElement(ch)); + } + replace = false; + } else if (ch == '%') { + replace = true; + list.add(new StringElement(buf.toString())); + buf = new StringBuilder(); + } else { + buf.append(ch); + } + } + if (buf.length() > 0) { + list.add(new StringElement(buf.toString())); + } + return list.toArray(new AccessLogElement[0]); + } + + + private CachedElement[] createCachedElements(AccessLogElement[] elements) { + List list = new ArrayList<>(); + for (AccessLogElement element : elements) { + if (element instanceof CachedElement) { + list.add((CachedElement) element); + } + } + return list.toArray(new CachedElement[0]); + } + + + /** + * Create an AccessLogElement implementation which needs an element name. + * + * @param name Header name + * @param pattern char in the log pattern + * + * @return the log element + */ + protected AccessLogElement createAccessLogElement(String name, char pattern) { + switch (pattern) { + case 'i': + return new HeaderElement(name); + case 'c': + return new CookieElement(name); + case 'o': + return new ResponseHeaderElement(name); + case 'a': + return new RemoteAddrElement(name); + case 'p': + return new PortElement(name); + case 'r': + if (TLSUtil.isTLSRequestAttribute(name)) { + tlsAttributeRequired = true; + } + return new RequestAttributeElement(name); + case 's': + return new SessionAttributeElement(name); + case 't': + return new DateAndTimeElement(name); + case 'T': + // ms for milliseconds, us for microseconds, and s for seconds + if ("ms".equals(name)) { + return new ElapsedTimeElement(false, true); + } else if ("us".equals(name)) { + return new ElapsedTimeElement(true, false); + } else { + return new ElapsedTimeElement(false, false); + } + default: + return new StringElement("???"); + } + } + + /** + * Create an AccessLogElement implementation. + * + * @param pattern char in the log pattern + * + * @return the log element + */ + protected AccessLogElement createAccessLogElement(char pattern) { + switch (pattern) { + case 'a': + return new RemoteAddrElement(); + case 'A': + return new LocalAddrElement(ipv6Canonical); + case 'b': + return new ByteSentElement(true); + case 'B': + return new ByteSentElement(false); + case 'D': + return new ElapsedTimeElement(true, false); + case 'F': + return new FirstByteTimeElement(); + case 'h': + return new HostElement(); + case 'H': + return new ProtocolElement(); + case 'l': + return new LogicalUserNameElement(); + case 'm': + return new MethodElement(); + case 'p': + return new PortElement(); + case 'q': + return new QueryElement(); + case 'r': + return new RequestElement(); + case 's': + return new HttpStatusCodeElement(); + case 'S': + return new SessionIdElement(); + case 't': + return new DateAndTimeElement(); + case 'T': + return new ElapsedTimeElement(false, false); + case 'u': + return new UserElement(); + case 'U': + return new RequestURIElement(); + case 'v': + return new LocalServerNameElement(); + case 'I': + return new ThreadNameElement(); + case 'X': + return new ConnectionStatusElement(); + default: + return new StringElement("???" + pattern + "???"); + } + } + + + /* + * This method is intended to mimic the escaping performed by httpd and mod_log_config. mod_log_config escapes more + * elements than indicated by the documentation. See: + * https://github.com/apache/httpd/blob/trunk/modules/loggers/mod_log_config.c + * + * The following escaped elements are not supported by Tomcat: - %C cookie value (see %{}c below) - %e environment + * variable - %f filename - %l remote logname (always logs "-") - %n note - %R handler - %ti trailer request header + * - %to trailer response header - %V server name per UseCanonicalName setting + * + * The following escaped elements are not escaped in Tomcat because values that would require escaping are rejected + * before they reach the AccessLogValve: - %h remote host - %H request protocol - %m request method - %q query + * string - %r request line - %U request URI - %v canonical server name + * + * The following escaped elements are supported by Tomcat: - %{}i request header - %{}o response header - %u remote + * user + * + * The following additional Tomcat elements are escaped for consistency: - %{}c cookie value - %{}r request + * attribute - %{}s session attribute + * + * giving a total of 6 elements that are escaped in Tomcat. + * + * Quoting from the httpd docs: "...non-printable and other special characters in %r, %i and %o are escaped using + * \xhh sequences, where hh stands for the hexadecimal representation of the raw byte. Exceptions from this rule are + * " and \, which are escaped by prepending a backslash, and all whitespace characters, which are written in their + * C-style notation (\n, \t, etc)." + * + * Reviewing the httpd code, characters with the high bit set are escaped. The httpd is assuming a single byte + * encoding which may not be true for Tomcat so Tomcat uses the Java \\uXXXX encoding. + */ + protected static void escapeAndAppend(String input, CharArrayWriter dest) { + if (input == null || input.isEmpty()) { + dest.append('-'); + return; + } + + int len = input.length(); + // As long as we don't encounter chars that need escaping, + // we only remember start and length of that string part. + // "next" is the start of the string part containing these chars, + // "current - 1" is its end. So writing from "next" with length + // "current - next" writes that part. + // We write that part whenever we find a character to escape and the + // unchanged and unwritten string part is not empty. + int next = 0; + char c; + for (int current = 0; current < len; current++) { + c = input.charAt(current); + // Fast path + if (c >= 32 && c < 127) { + // special case " and \ + switch (c) { + case '\\': // dec 92 + // Write unchanged string parts + if (current > next) { + dest.write(input, next, current - next); + } + next = current + 1; + dest.append("\\\\"); + break; + case '\"': // dec 34 + // Write unchanged string parts + if (current > next) { + dest.write(input, next, current - next); + } + next = current + 1; + dest.append("\\\""); + break; + // Don't output individual unchanged chars, + // write the sub string only when the first char to encode + // is encountered plus at the end. + default: + } + // Control (1-31), delete (127) or above 127 + } else { + // Write unchanged string parts + if (current > next) { + dest.write(input, next, current - next); + } + next = current + 1; + switch (c) { + // Standard escapes for some control chars + case '\f': // dec 12 + dest.append("\\f"); + break; + case '\n': // dec 10 + dest.append("\\n"); + break; + case '\r': // dec 13 + dest.append("\\r"); + break; + case '\t': // dec 09 + dest.append("\\t"); + break; + // Unicode escape \\uXXXX + default: + dest.append("\\u"); + dest.append(HexUtils.toHexString(c)); + } + } + } + // Write remaining unchanged string parts + if (len > next) { + dest.write(input, next, len - next); + } + } +} diff --git a/java/org/apache/catalina/valves/AccessLogValve.java b/java/org/apache/catalina/valves/AccessLogValve.java new file mode 100644 index 0000000..63d3527 --- /dev/null +++ b/java/org/apache/catalina/valves/AccessLogValve.java @@ -0,0 +1,682 @@ +/* + * 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.catalina.valves; + + +import java.io.BufferedWriter; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.catalina.LifecycleException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.B2CConverter; + + +/** + * This is a concrete implementation of {@link AbstractAccessLogValve} that outputs the access log to a file. The + * features of this implementation include: + *
      + *
    • Automatic date-based rollover of log files
    • + *
    • Optional log file rotation
    • + *
    + *

    + * For UNIX users, another field called checkExists is also available. If set to true, the log file's + * existence will be checked before each logging. This way an external log rotator can move the file somewhere and + * Tomcat will start with a new file. + *

    + *

    + * For JMX junkies, a public method called rotate has been made available to allow you to tell this + * instance to move the existing log file to somewhere else and start writing a new log file. + *

    + */ +public class AccessLogValve extends AbstractAccessLogValve { + + private static final Log log = LogFactory.getLog(AccessLogValve.class); + + // ------------------------------------------------------ Constructor + public AccessLogValve() { + super(); + } + + // ----------------------------------------------------- Instance Variables + + + /** + * The as-of date for the currently open log file, or a zero-length string if there is no open log file. + */ + private volatile String dateStamp = ""; + + + /** + * The directory in which log files are created. + */ + private String directory = "logs"; + + /** + * The prefix that is added to log file filenames. + */ + protected volatile String prefix = "access_log"; + + + /** + * Should we rotate our log file? Default is true (like old behavior) + */ + protected boolean rotatable = true; + + /** + * Should we defer inclusion of the date stamp in the file name until rotate time? Default is false. + */ + protected boolean renameOnRotate = false; + + + /** + * Buffered logging. + */ + private boolean buffered = true; + + + /** + * The suffix that is added to log file filenames. + */ + protected volatile String suffix = ""; + + + /** + * The PrintWriter to which we are currently logging, if any. + */ + protected PrintWriter writer = null; + + + /** + * A date formatter to format a Date using the format given by fileDateFormat. + */ + protected SimpleDateFormat fileDateFormatter = null; + + + /** + * The current log file we are writing to. Helpful when checkExists is true. + */ + protected File currentLogFile = null; + + /** + * Instant when the log daily rotation was last checked. + */ + private volatile long rotationLastChecked = 0L; + + /** + * Do we check for log file existence? Helpful if an external agent renames the log file so we can automagically + * recreate it. + */ + private boolean checkExists = false; + + /** + * Date format to place in log file name. + */ + protected String fileDateFormat = ".yyyy-MM-dd"; + + /** + * Character set used by the log file. If it is null, UTF-8 will be used. An empty string will be + * treated as null when this property is assigned. + */ + protected volatile String encoding = null; + + /** + * The number of days to retain the access log files before they are removed. + */ + private int maxDays = -1; + private volatile boolean checkForOldLogs = false; + + // ------------------------------------------------------------- Properties + + + public int getMaxDays() { + return maxDays; + } + + + public void setMaxDays(int maxDays) { + this.maxDays = maxDays; + } + + + /** + * @return the directory in which we create log files. + */ + public String getDirectory() { + return directory; + } + + + /** + * Set the directory in which we create log files. + * + * @param directory The new log file directory + */ + public void setDirectory(String directory) { + this.directory = directory; + } + + /** + * Check for file existence before logging. + * + * @return true if file existence is checked first + */ + public boolean isCheckExists() { + + return checkExists; + + } + + + /** + * Set whether to check for log file existence before logging. + * + * @param checkExists true meaning to check for file existence. + */ + public void setCheckExists(boolean checkExists) { + + this.checkExists = checkExists; + + } + + + /** + * @return the log file prefix. + */ + public String getPrefix() { + return prefix; + } + + + /** + * Set the log file prefix. + * + * @param prefix The new log file prefix + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + + /** + * Should we rotate the access log. + * + * @return true if the access log should be rotated + */ + public boolean isRotatable() { + return rotatable; + } + + + /** + * Configure whether the access log should be rotated. + * + * @param rotatable true if the log should be rotated + */ + public void setRotatable(boolean rotatable) { + this.rotatable = rotatable; + } + + + /** + * Should we defer inclusion of the date stamp in the file name until rotate time. + * + * @return true if the logs file names are time stamped only when they are rotated + */ + public boolean isRenameOnRotate() { + return renameOnRotate; + } + + + /** + * Set the value if we should defer inclusion of the date stamp in the file name until rotate time + * + * @param renameOnRotate true if defer inclusion of date stamp + */ + public void setRenameOnRotate(boolean renameOnRotate) { + this.renameOnRotate = renameOnRotate; + } + + + /** + * Is the logging buffered. Usually buffering can increase performance. + * + * @return true if the logging uses a buffer + */ + public boolean isBuffered() { + return buffered; + } + + + /** + * Set the value if the logging should be buffered + * + * @param buffered true if buffered. + */ + public void setBuffered(boolean buffered) { + this.buffered = buffered; + } + + + /** + * @return the log file suffix. + */ + public String getSuffix() { + return suffix; + } + + + /** + * Set the log file suffix. + * + * @param suffix The new log file suffix + */ + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + /** + * @return the date format date based log rotation. + */ + public String getFileDateFormat() { + return fileDateFormat; + } + + + /** + * Set the date format date based log rotation. + * + * @param fileDateFormat The format for the file timestamp + */ + public void setFileDateFormat(String fileDateFormat) { + String newFormat; + if (fileDateFormat == null) { + newFormat = ""; + } else { + newFormat = fileDateFormat; + } + this.fileDateFormat = newFormat; + + synchronized (this) { + fileDateFormatter = new SimpleDateFormat(newFormat, Locale.US); + fileDateFormatter.setTimeZone(TimeZone.getDefault()); + } + } + + /** + * Return the character set name that is used to write the log file. + * + * @return Character set name, or null if the default character set is used. + */ + public String getEncoding() { + return encoding; + } + + /** + * Set the character set that is used to write the log file. + * + * @param encoding The name of the character set. + */ + public void setEncoding(String encoding) { + if (encoding != null && encoding.length() > 0) { + this.encoding = encoding; + } else { + this.encoding = null; + } + } + + // --------------------------------------------------------- Public Methods + + /** + * Execute a periodic task, such as reloading, etc. This method will be invoked inside the classloading context of + * this container. Unexpected throwables will be caught and logged. + */ + @Override + public synchronized void backgroundProcess() { + if (getState().isAvailable() && getEnabled() && writer != null && buffered) { + writer.flush(); + } + + int maxDays = this.maxDays; + String prefix = this.prefix; + String suffix = this.suffix; + + if (rotatable && checkForOldLogs && maxDays > 0) { + long deleteIfLastModifiedBefore = System.currentTimeMillis() - (maxDays * 24L * 60 * 60 * 1000); + File dir = getDirectoryFile(); + if (dir.isDirectory()) { + String[] oldAccessLogs = dir.list(); + + if (oldAccessLogs != null) { + for (String oldAccessLog : oldAccessLogs) { + boolean match = false; + + if (prefix != null && prefix.length() > 0) { + if (!oldAccessLog.startsWith(prefix)) { + continue; + } + match = true; + } + + if (suffix != null && suffix.length() > 0) { + if (!oldAccessLog.endsWith(suffix)) { + continue; + } + match = true; + } + + if (match) { + File file = new File(dir, oldAccessLog); + if (file.isFile() && file.lastModified() < deleteIfLastModifiedBefore) { + if (!file.delete()) { + log.warn(sm.getString("accessLogValve.deleteFail", file.getAbsolutePath())); + } + } + } + } + } + } + checkForOldLogs = false; + } + } + + /** + * Rotate the log file if necessary. + */ + public void rotate() { + if (rotatable) { + // Only do a logfile switch check once a second, max. + long systime = System.currentTimeMillis(); + if ((systime - rotationLastChecked) > 1000) { + synchronized (this) { + if ((systime - rotationLastChecked) > 1000) { + rotationLastChecked = systime; + + String tsDate; + // Check for a change of date + tsDate = fileDateFormatter.format(new Date(systime)); + + // If the date has changed, switch log files + if (!dateStamp.equals(tsDate)) { + close(true); + dateStamp = tsDate; + open(); + } + } + } + } + } + } + + /** + * Rename the existing log file to something else. Then open the old log file name up once again. Intended to be + * called by a JMX agent. + * + * @param newFileName The file name to move the log file entry to + * + * @return true if a file was rotated with no error + */ + public synchronized boolean rotate(String newFileName) { + + if (currentLogFile != null) { + File holder = currentLogFile; + close(false); + try { + holder.renameTo(new File(newFileName)); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + log.error(sm.getString("accessLogValve.rotateFail"), e); + } + + /* Make sure date is correct */ + dateStamp = fileDateFormatter.format(new Date(System.currentTimeMillis())); + + open(); + return true; + } else { + return false; + } + + } + + // -------------------------------------------------------- Private Methods + + + private File getDirectoryFile() { + File dir = new File(directory); + if (!dir.isAbsolute()) { + dir = new File(getContainer().getCatalinaBase(), directory); + } + return dir; + } + + + /** + * Create a File object based on the current log file name. Directories are created as needed but the underlying + * file is not created or opened. + * + * @param useDateStamp include the timestamp in the file name. + * + * @return the log file object + */ + private File getLogFile(boolean useDateStamp) { + // Create the directory if necessary + File dir = getDirectoryFile(); + if (!dir.mkdirs() && !dir.isDirectory()) { + log.error(sm.getString("accessLogValve.openDirFail", dir)); + } + + // Calculate the current log file name + File pathname; + if (useDateStamp) { + pathname = new File(dir.getAbsoluteFile(), prefix + dateStamp + suffix); + } else { + pathname = new File(dir.getAbsoluteFile(), prefix + suffix); + } + File parent = pathname.getParentFile(); + if (!parent.mkdirs() && !parent.isDirectory()) { + log.error(sm.getString("accessLogValve.openDirFail", parent)); + } + return pathname; + } + + /** + * Move a current but rotated log file back to the unrotated one. Needed if date stamp inclusion is deferred to + * rotation time. + */ + private void restore() { + File newLogFile = getLogFile(false); + File rotatedLogFile = getLogFile(true); + if (rotatedLogFile.exists() && !newLogFile.exists() && !rotatedLogFile.equals(newLogFile)) { + try { + if (!rotatedLogFile.renameTo(newLogFile)) { + log.error(sm.getString("accessLogValve.renameFail", rotatedLogFile, newLogFile)); + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + log.error(sm.getString("accessLogValve.renameFail", rotatedLogFile, newLogFile), e); + } + } + } + + + /** + * Close the currently open log file (if any) + * + * @param rename Rename file to final name after closing + */ + private synchronized void close(boolean rename) { + if (writer == null) { + return; + } + writer.flush(); + writer.close(); + if (rename && renameOnRotate) { + File newLogFile = getLogFile(true); + if (!newLogFile.exists()) { + try { + if (!currentLogFile.renameTo(newLogFile)) { + log.error(sm.getString("accessLogValve.renameFail", currentLogFile, newLogFile)); + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + log.error(sm.getString("accessLogValve.renameFail", currentLogFile, newLogFile), e); + } + } else { + log.error(sm.getString("accessLogValve.alreadyExists", currentLogFile, newLogFile)); + } + } + writer = null; + dateStamp = ""; + currentLogFile = null; + } + + + /** + * Log the specified message to the log file, switching files if the date has changed since the previous log call. + * + * @param message Message to be logged + */ + @Override + public void log(CharArrayWriter message) { + + rotate(); + + /* In case something external rotated the file instead */ + if (checkExists) { + synchronized (this) { + if (currentLogFile != null && !currentLogFile.exists()) { + try { + close(false); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + log.info(sm.getString("accessLogValve.closeFail"), e); + } + + /* Make sure date is correct */ + dateStamp = fileDateFormatter.format(new Date(System.currentTimeMillis())); + + open(); + } + } + } + + // Log this message + try { + message.write(System.lineSeparator()); + synchronized (this) { + if (writer != null) { + message.writeTo(writer); + if (!buffered) { + writer.flush(); + } + } + } + } catch (IOException ioe) { + log.warn(sm.getString("accessLogValve.writeFail", message.toString()), ioe); + } + } + + + /** + * Open the new log file for the date specified by dateStamp. + */ + protected synchronized void open() { + // Open the current log file + // If no rotate - no need for dateStamp in fileName + File pathname = getLogFile(rotatable && !renameOnRotate); + + Charset charset = null; + if (encoding != null) { + try { + charset = B2CConverter.getCharset(encoding); + } catch (UnsupportedEncodingException ex) { + log.error(sm.getString("accessLogValve.unsupportedEncoding", encoding), ex); + } + } + if (charset == null) { + charset = StandardCharsets.UTF_8; + } + + try { + writer = new PrintWriter( + new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pathname, true), charset), 128000), + false); + + currentLogFile = pathname; + } catch (IOException e) { + writer = null; + currentLogFile = null; + log.error(sm.getString("accessLogValve.openFail", pathname, System.getProperty("user.name")), e); + } + // Rotating a log file will always trigger a new file to be opened so + // when a new file is opened, check to see if any old files need to be + // removed. + checkForOldLogs = true; + } + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + + // Initialize the Date formatters + String format = getFileDateFormat(); + fileDateFormatter = new SimpleDateFormat(format, Locale.US); + fileDateFormatter.setTimeZone(TimeZone.getDefault()); + dateStamp = fileDateFormatter.format(new Date(System.currentTimeMillis())); + if (rotatable && renameOnRotate) { + restore(); + } + open(); + + super.startInternal(); + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + + super.stopInternal(); + close(false); + } +} diff --git a/java/org/apache/catalina/valves/Constants.java b/java/org/apache/catalina/valves/Constants.java new file mode 100644 index 0000000..c19561f --- /dev/null +++ b/java/org/apache/catalina/valves/Constants.java @@ -0,0 +1,38 @@ +/* + * 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.catalina.valves; + + +/** + * Manifest constants for the org.apache.catalina.valves package. + * + * @author Craig R. McClanahan + */ + +public final class Constants { + + public static final String Package = "org.apache.catalina.valves"; + + // Constants for the AccessLogValve class + public static final class AccessLog { + public static final String COMMON_ALIAS = "common"; + public static final String COMMON_PATTERN = "%h %l %u %t \"%r\" %s %b"; + public static final String COMBINED_ALIAS = "combined"; + public static final String COMBINED_PATTERN = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\""; + } + +} diff --git a/java/org/apache/catalina/valves/CrawlerSessionManagerValve.java b/java/org/apache/catalina/valves/CrawlerSessionManagerValve.java new file mode 100644 index 0000000..21f0328 --- /dev/null +++ b/java/org/apache/catalina/valves/CrawlerSessionManagerValve.java @@ -0,0 +1,290 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Enumeration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionBindingEvent; +import jakarta.servlet.http.HttpSessionBindingListener; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Web crawlers can trigger the creation of many thousands of sessions as they crawl a site which may result in + * significant memory consumption. This Valve ensures that crawlers are associated with a single session - just like + * normal users - regardless of whether or not they provide a session token with their requests. + */ +public class CrawlerSessionManagerValve extends ValveBase { + + private static final Log log = LogFactory.getLog(CrawlerSessionManagerValve.class); + + private final Map clientIdSessionId = new ConcurrentHashMap<>(); + private final Map sessionIdClientId = new ConcurrentHashMap<>(); + + private String crawlerUserAgents = ".*[bB]ot.*|.*Yahoo! Slurp.*|.*Feedfetcher-Google.*"; + private Pattern uaPattern = null; + + private String crawlerIps = null; + private Pattern ipPattern = null; + + private int sessionInactiveInterval = 60; + + private boolean isHostAware = true; + + private boolean isContextAware = true; + + + /** + * Specifies a default constructor so async support can be configured. + */ + public CrawlerSessionManagerValve() { + super(true); + } + + + /** + * Specify the regular expression (using {@link Pattern}) that will be used to identify crawlers based in the + * User-Agent header provided. The default is ".*GoogleBot.*|.*bingbot.*|.*Yahoo! Slurp.*" + * + * @param crawlerUserAgents The regular expression using {@link Pattern} + */ + public void setCrawlerUserAgents(String crawlerUserAgents) { + this.crawlerUserAgents = crawlerUserAgents; + if (crawlerUserAgents == null || crawlerUserAgents.length() == 0) { + uaPattern = null; + } else { + uaPattern = Pattern.compile(crawlerUserAgents); + } + } + + /** + * @see #setCrawlerUserAgents(String) + * + * @return The current regular expression being used to match user agents. + */ + public String getCrawlerUserAgents() { + return crawlerUserAgents; + } + + + /** + * Specify the regular expression (using {@link Pattern}) that will be used to identify crawlers based on their IP + * address. The default is no crawler IPs. + * + * @param crawlerIps The regular expression using {@link Pattern} + */ + public void setCrawlerIps(String crawlerIps) { + this.crawlerIps = crawlerIps; + if (crawlerIps == null || crawlerIps.length() == 0) { + ipPattern = null; + } else { + ipPattern = Pattern.compile(crawlerIps); + } + } + + /** + * @see #setCrawlerIps(String) + * + * @return The current regular expression being used to match IP addresses. + */ + public String getCrawlerIps() { + return crawlerIps; + } + + + /** + * Specify the session timeout (in seconds) for a crawler's session. This is typically lower than that for a user + * session. The default is 60 seconds. + * + * @param sessionInactiveInterval The new timeout for crawler sessions + */ + public void setSessionInactiveInterval(int sessionInactiveInterval) { + this.sessionInactiveInterval = sessionInactiveInterval; + } + + /** + * @see #setSessionInactiveInterval(int) + * + * @return The current timeout in seconds + */ + public int getSessionInactiveInterval() { + return sessionInactiveInterval; + } + + + public Map getClientIpSessionId() { + return clientIdSessionId; + } + + + public boolean isHostAware() { + return isHostAware; + } + + + public void setHostAware(boolean isHostAware) { + this.isHostAware = isHostAware; + } + + + public boolean isContextAware() { + return isContextAware; + } + + + public void setContextAware(boolean isContextAware) { + this.isContextAware = isContextAware; + } + + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + + uaPattern = Pattern.compile(crawlerUserAgents); + } + + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + boolean isBot = false; + String sessionId = null; + String clientIp = request.getRemoteAddr(); + String clientIdentifier = getClientIdentifier(request.getHost(), request.getContext(), clientIp); + + if (log.isTraceEnabled()) { + log.trace(request.hashCode() + ": ClientIdentifier=" + clientIdentifier + ", RequestedSessionId=" + + request.getRequestedSessionId()); + } + + // If the incoming request has a valid session ID, no action is required + if (request.getSession(false) == null) { + + // Is this a crawler - check the UA headers + Enumeration uaHeaders = request.getHeaders("user-agent"); + String uaHeader = null; + if (uaHeaders.hasMoreElements()) { + uaHeader = uaHeaders.nextElement(); + } + + // If more than one UA header - assume not a bot + if (uaHeader != null && !uaHeaders.hasMoreElements()) { + + if (log.isTraceEnabled()) { + log.trace(request.hashCode() + ": UserAgent=" + uaHeader); + } + + if (uaPattern.matcher(uaHeader).matches()) { + isBot = true; + + if (log.isTraceEnabled()) { + log.trace(request.hashCode() + ": Bot found. UserAgent=" + uaHeader); + } + } + } + + if (ipPattern != null && ipPattern.matcher(clientIp).matches()) { + isBot = true; + + if (log.isTraceEnabled()) { + log.trace(request.hashCode() + ": Bot found. IP=" + clientIp); + } + } + + // If this is a bot, is the session ID known? + if (isBot) { + sessionId = clientIdSessionId.get(clientIdentifier); + if (sessionId != null) { + request.setRequestedSessionId(sessionId); + if (log.isTraceEnabled()) { + log.trace(request.hashCode() + ": SessionID=" + sessionId); + } + } + } + } + + getNext().invoke(request, response); + + if (isBot) { + if (sessionId == null) { + // Has bot just created a session, if so make a note of it + HttpSession s = request.getSession(false); + if (s != null) { + clientIdSessionId.put(clientIdentifier, s.getId()); + sessionIdClientId.put(s.getId(), clientIdentifier); + // #valueUnbound() will be called on session expiration + s.setAttribute(this.getClass().getName(), + new CrawlerHttpSessionBindingListener(clientIdSessionId, clientIdentifier)); + s.setMaxInactiveInterval(sessionInactiveInterval); + + if (log.isTraceEnabled()) { + log.trace(request.hashCode() + ": New bot session. SessionID=" + s.getId()); + } + } + } else { + if (log.isTraceEnabled()) { + log.trace(request.hashCode() + ": Bot session accessed. SessionID=" + sessionId); + } + } + } + } + + + private String getClientIdentifier(Host host, Context context, String clientIp) { + StringBuilder result = new StringBuilder(clientIp); + if (isHostAware) { + result.append('-').append(host.getName()); + } + if (isContextAware && context != null) { + result.append(context.getName()); + } + return result.toString(); + } + + private static class CrawlerHttpSessionBindingListener implements HttpSessionBindingListener, Serializable { + private static final long serialVersionUID = 1L; + + private final transient Map clientIdSessionId; + private final transient String clientIdentifier; + + private CrawlerHttpSessionBindingListener(Map clientIdSessionId, String clientIdentifier) { + this.clientIdSessionId = clientIdSessionId; + this.clientIdentifier = clientIdentifier; + } + + @Override + public void valueUnbound(HttpSessionBindingEvent event) { + if (clientIdentifier != null && clientIdSessionId != null) { + clientIdSessionId.remove(clientIdentifier, event.getSession().getId()); + } + } + } +} diff --git a/java/org/apache/catalina/valves/ErrorReportValve.java b/java/org/apache/catalina/valves/ErrorReportValve.java new file mode 100644 index 0000000..7832b4f --- /dev/null +++ b/java/org/apache/catalina/valves/ErrorReportValve.java @@ -0,0 +1,472 @@ +/* + * 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.catalina.valves; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Scanner; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.ErrorPageSupport; +import org.apache.catalina.util.IOTools; +import org.apache.catalina.util.ServerInfo; +import org.apache.catalina.util.TomcatCSS; +import org.apache.coyote.ActionCode; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; + +/** + *

    + * Implementation of a Valve that outputs HTML error pages. + *

    + *

    + * This Valve should be attached at the Host level, although it will work if attached to a Context. + *

    + *

    + * HTML code from the Cocoon 2 project. + *

    + * + * @author Remy Maucherat + * @author Craig R. McClanahan + * @author Nicola Ken Barozzi Aisa + * @author Stefano Mazzocchi + * @author Yoav Shapira + */ +public class ErrorReportValve extends ValveBase { + + private boolean showReport = true; + + private boolean showServerInfo = true; + + private final ErrorPageSupport errorPageSupport = new ErrorPageSupport(); + + + // ------------------------------------------------------ Constructor + + public ErrorReportValve() { + super(true); + } + + + // --------------------------------------------------------- Public Methods + + /** + * Invoke the next Valve in the sequence. When the invoke returns, check the response state. If the status code is + * greater than or equal to 400 or an uncaught exception was thrown then the error handling will be triggered. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + // Perform the request + getNext().invoke(request, response); + + if (response.isCommitted()) { + if (response.setErrorReported()) { + // Error wasn't previously reported but we can't write an error + // page because the response has already been committed. + + // See if IO is allowed + AtomicBoolean ioAllowed = new AtomicBoolean(true); + response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, ioAllowed); + + if (ioAllowed.get()) { + // I/O is currently still allowed. Flush any data that is + // still to be written to the client. + try { + response.flushBuffer(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + // Now close immediately to signal to the client that + // something went wrong + response.getCoyoteResponse().action(ActionCode.CLOSE_NOW, + request.getAttribute(RequestDispatcher.ERROR_EXCEPTION)); + } + } + return; + } + + Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + + // If an async request is in progress and is not going to end once this + // container thread finishes, do not process any error page here. + if (request.isAsync() && !request.isAsyncCompleting()) { + return; + } + + if (throwable != null && !response.isError()) { + // Make sure that the necessary methods have been called on the + // response. (It is possible a component may just have set the + // Throwable. Tomcat won't do that but other components might.) + // These are safe to call at this point as we know that the response + // has not been committed. + response.reset(); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + // One way or another, response.sendError() will have been called before + // execution reaches this point and suspended the response. Need to + // reverse that so this valve can write to the response. + response.setSuspended(false); + + try { + report(request, response, throwable); + } catch (Throwable tt) { + ExceptionUtils.handleThrowable(tt); + } + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Return the error page associated with the specified status and exception. + * + * @param statusCode the status code + * @param throwable the exception + * + * @return the associated error page + */ + protected ErrorPage findErrorPage(int statusCode, Throwable throwable) { + ErrorPage errorPage = null; + if (throwable != null) { + errorPage = errorPageSupport.find(throwable); + } + if (errorPage == null) { + errorPage = errorPageSupport.find(statusCode); + } + if (errorPage == null) { + // Default error page + errorPage = errorPageSupport.find(0); + } + return errorPage; + } + + + /** + * Prints out an error report. + * + * @param request The request being processed + * @param response The response being generated + * @param throwable The exception that occurred (which possibly wraps a root cause exception + */ + protected void report(Request request, Response response, Throwable throwable) { + + int statusCode = response.getStatus(); + + // Do nothing on a 1xx, 2xx and 3xx status + // Do nothing if anything has been written already + // Do nothing if the response hasn't been explicitly marked as in error + // and that error has not been reported. + if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) { + return; + } + + // If an error has occurred that prevents further I/O, don't waste time + // producing an error report that will never be read + AtomicBoolean result = new AtomicBoolean(false); + response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result); + if (!result.get()) { + return; + } + + ErrorPage errorPage = findErrorPage(statusCode, throwable); + + if (errorPage != null) { + if (sendErrorPage(errorPage.getLocation(), response)) { + // If the page was sent successfully, don't write the standard + // error page. + return; + } + } + + String message = Escape.htmlElementContent(response.getMessage()); + if (message == null) { + if (throwable != null) { + String exceptionMessage = throwable.getMessage(); + if (exceptionMessage != null && exceptionMessage.length() > 0) { + try (Scanner scanner = new Scanner(exceptionMessage)) { + message = Escape.htmlElementContent(scanner.nextLine()); + } + } + } + if (message == null) { + message = ""; + } + } + + // Do nothing if there is no reason phrase for the specified status code and + // no error message provided + String reason = null; + String description = null; + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + response.setLocale(smClient.getLocale()); + try { + reason = smClient.getString("http." + statusCode + ".reason"); + description = smClient.getString("http." + statusCode + ".desc"); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + if (reason == null || description == null) { + if (message.isEmpty()) { + return; + } else { + reason = smClient.getString("errorReportValve.unknownReason"); + description = smClient.getString("errorReportValve.noDescription"); + } + } + + StringBuilder sb = new StringBuilder(); + + sb.append(""); + sb.append(""); + sb.append(""); + sb.append(smClient.getString("errorReportValve.statusHeader", String.valueOf(statusCode), reason)); + sb.append(""); + sb.append(""); + sb.append(""); + sb.append("

    "); + sb.append(smClient.getString("errorReportValve.statusHeader", String.valueOf(statusCode), reason)) + .append("

    "); + if (isShowReport()) { + sb.append("
    "); + sb.append("

    "); + sb.append(smClient.getString("errorReportValve.type")); + sb.append(" "); + if (throwable != null) { + sb.append(smClient.getString("errorReportValve.exceptionReport")); + } else { + sb.append(smClient.getString("errorReportValve.statusReport")); + } + sb.append("

    "); + if (!message.isEmpty()) { + sb.append("

    "); + sb.append(smClient.getString("errorReportValve.message")); + sb.append(" "); + sb.append(message).append("

    "); + } + sb.append("

    "); + sb.append(smClient.getString("errorReportValve.description")); + sb.append(" "); + sb.append(description); + sb.append("

    "); + if (throwable != null) { + String stackTrace = getPartialServletStackTrace(throwable); + sb.append("

    "); + sb.append(smClient.getString("errorReportValve.exception")); + sb.append("

    ");
    +                sb.append(Escape.htmlElementContent(stackTrace));
    +                sb.append("
    "); + + int loops = 0; + Throwable rootCause = throwable.getCause(); + while (rootCause != null && (loops < 10)) { + stackTrace = getPartialServletStackTrace(rootCause); + sb.append("

    "); + sb.append(smClient.getString("errorReportValve.rootCause")); + sb.append("

    ");
    +                    sb.append(Escape.htmlElementContent(stackTrace));
    +                    sb.append("
    "); + // In case root cause is somehow heavily nested + rootCause = rootCause.getCause(); + loops++; + } + + sb.append("

    "); + sb.append(smClient.getString("errorReportValve.note")); + sb.append(" "); + sb.append(smClient.getString("errorReportValve.rootCauseInLogs")); + sb.append("

    "); + + } + sb.append("
    "); + } + if (isShowServerInfo()) { + sb.append("

    ").append(ServerInfo.getServerInfo()).append("

    "); + } + sb.append(""); + + try { + try { + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (container.getLogger().isDebugEnabled()) { + container.getLogger().debug(sm.getString("errorReportValve.contentTypeFail"), t); + } + } + Writer writer = response.getReporter(); + if (writer != null) { + // If writer is null, it's an indication that the response has + // been hard committed already, which should never happen + writer.write(sb.toString()); + response.finishResponse(); + } + } catch (IOException | IllegalStateException e) { + // Ignore + } + + } + + + /** + * Print out a partial servlet stack trace (truncating at the last occurrence of jakarta.servlet.). + * + * @param t The stack trace to process + * + * @return the stack trace relative to the application layer + */ + protected String getPartialServletStackTrace(Throwable t) { + StringBuilder trace = new StringBuilder(); + trace.append(t.toString()).append(System.lineSeparator()); + StackTraceElement[] elements = t.getStackTrace(); + int pos = elements.length; + for (int i = elements.length - 1; i >= 0; i--) { + if ((elements[i].getClassName().startsWith("org.apache.catalina.core.ApplicationFilterChain")) && + (elements[i].getMethodName().equals("internalDoFilter"))) { + pos = i; + break; + } + } + for (int i = 0; i < pos; i++) { + if (!(elements[i].getClassName().startsWith("org.apache.catalina.core."))) { + trace.append('\t').append(elements[i].toString()).append(System.lineSeparator()); + } + } + return trace.toString(); + } + + + private boolean sendErrorPage(String location, Response response) { + File file = new File(location); + if (!file.isAbsolute()) { + file = new File(getContainer().getCatalinaBase(), location); + } + if (!file.isFile() || !file.canRead()) { + getContainer().getLogger().warn(sm.getString("errorReportValve.errorPageNotFound", location)); + return false; + } + + // Hard coded for now. Consider making this optional. At Valve level or + // page level? + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + try (OutputStream os = response.getOutputStream(); InputStream is = new FileInputStream(file);) { + IOTools.flow(is, os); + } catch (IOException e) { + getContainer().getLogger().warn(sm.getString("errorReportValve.errorPageIOException", location), e); + return false; + } + + return true; + } + + + /** + * Enables/Disables full error reports + * + * @param showReport true to show full error data + */ + public void setShowReport(boolean showReport) { + this.showReport = showReport; + } + + public boolean isShowReport() { + return showReport; + } + + /** + * Enables/Disables server info on error pages + * + * @param showServerInfo true to show server info + */ + public void setShowServerInfo(boolean showServerInfo) { + this.showServerInfo = showServerInfo; + } + + public boolean isShowServerInfo() { + return showServerInfo; + } + + + public boolean setProperty(String name, String value) { + if (name.startsWith("errorCode.")) { + int code = Integer.parseInt(name.substring(10)); + ErrorPage ep = new ErrorPage(); + ep.setErrorCode(code); + ep.setLocation(value); + errorPageSupport.add(ep); + return true; + } else if (name.startsWith("exceptionType.")) { + String className = name.substring(14); + ErrorPage ep = new ErrorPage(); + ep.setExceptionType(className); + ep.setLocation(value); + errorPageSupport.add(ep); + return true; + } + return false; + } + + public String getProperty(String name) { + String result; + if (name.startsWith("errorCode.")) { + int code = Integer.parseInt(name.substring(10)); + ErrorPage ep = errorPageSupport.find(code); + if (ep == null) { + result = null; + } else { + result = ep.getLocation(); + } + } else if (name.startsWith("exceptionType.")) { + String className = name.substring(14); + ErrorPage ep = errorPageSupport.find(className); + if (ep == null) { + result = null; + } else { + result = ep.getLocation(); + } + } else { + result = null; + } + return result; + } +} diff --git a/java/org/apache/catalina/valves/ExtendedAccessLogValve.java b/java/org/apache/catalina/valves/ExtendedAccessLogValve.java new file mode 100644 index 0000000..c75e41d --- /dev/null +++ b/java/org/apache/catalina/valves/ExtendedAccessLogValve.java @@ -0,0 +1,805 @@ +/* + * 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.catalina.valves; + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.StringReader; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.ServerInfo; +import org.apache.catalina.util.URLEncoder; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; + +/** + * An implementation of the W3c Extended Log File Format. See http://www.w3.org/TR/WD-logfile.html for more information + * about the format. The following fields are supported: + *
      + *
    • c-dns: Client hostname (or ip address if enableLookups for the connector is false)
    • + *
    • c-ip: Client ip address
    • + *
    • bytes: bytes served
    • + *
    • cs-method: request method
    • + *
    • cs-uri: The full uri requested
    • + *
    • cs-uri-query: The query string
    • + *
    • cs-uri-stem: The uri without query string
    • + *
    • date: The date in yyyy-mm-dd format for GMT
    • + *
    • s-dns: The server dns entry
    • + *
    • s-ip: The server ip address
    • + *
    • cs(XXX): The value of header XXX from client to server
    • + *
    • sc(XXX): The value of header XXX from server to client
    • + *
    • sc-status: The status code
    • + *
    • time: Time the request was served
    • + *
    • time-taken: Time (in seconds) taken to serve the request
    • + *
    • x-threadname: Current request thread name (can compare later with stacktraces)
    • + *
    • x-A(XXX): Pull XXX attribute from the servlet context
    • + *
    • x-C(XXX): Pull the cookie(s) of the name XXX
    • + *
    • x-O(XXX): Pull the all response header values XXX
    • + *
    • x-R(XXX): Pull XXX attribute from the servlet request
    • + *
    • x-S(XXX): Pull XXX attribute from the session
    • + *
    • x-P(...): Call request.getParameter(...) and URLencode it. Helpful to capture certain POST + * parameters.
    • + *
    • For any of the x-H(...) the following method will be called from the HttpServletRequest object
    • + *
    • x-H(authType): getAuthType
    • + *
    • x-H(characterEncoding): getCharacterEncoding
    • + *
    • x-H(contentLength): getContentLength
    • + *
    • x-H(locale): getLocale
    • + *
    • x-H(protocol): getProtocol
    • + *
    • x-H(remoteUser): getRemoteUser
    • + *
    • x-H(requestedSessionId): getRequestedSessionId
    • + *
    • x-H(requestedSessionIdFromCookie): isRequestedSessionIdFromCookie
    • + *
    • x-H(requestedSessionIdValid): isRequestedSessionIdValid
    • + *
    • x-H(scheme): getScheme
    • + *
    • x-H(secure): isSecure
    • + *
    + *

    + * Log rotation can be on or off. This is dictated by the rotatable property. + *

    + *

    + * For UNIX users, another field called checkExists is also available. If set to true, the log file's + * existence will be checked before each logging. This way an external log rotator can move the file somewhere and + * Tomcat will start with a new file. + *

    + *

    + * For JMX junkies, a public method called rotate has been made available to allow you to tell this + * instance to move the existing log file to somewhere else and start writing a new log file. + *

    + *

    + * Conditional logging is also supported. This can be done with the condition property. If the value + * returned from ServletRequest.getAttribute(condition) yields a non-null value, the logging will be skipped. + *

    + *

    + * For extended attributes coming from a getAttribute() call, it is you responsibility to ensure there are no newline or + * control characters. + *

    + * + * @author Peter Rossbach + */ +public class ExtendedAccessLogValve extends AccessLogValve { + + private static final Log log = LogFactory.getLog(ExtendedAccessLogValve.class); + + // -------------------------------------------------------- Private Methods + + /** + * Wrap the incoming value with double quotes (") and escape any double quotes appearing in the value using two + * double quotes (""). + * + * @param value - The value to wrap + * + * @return '-' if null. Otherwise, toString() will be called on the object and the value will be wrapped in quotes + * and any quotes will be escaped with 2 sets of quotes. + */ + static String wrap(Object value) { + String svalue; + // Does the value contain a " ? If so must encode it + if (value == null || "-".equals(value)) { + return "-"; + } + + try { + svalue = value.toString(); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + /* Log error */ + return "-"; + } + + /* Wrap all values in double quotes. */ + StringBuilder buffer = new StringBuilder(svalue.length() + 2); + buffer.append('\"'); + int i = 0; + while (i < svalue.length()) { + int j = svalue.indexOf('\"', i); + if (j == -1) { + buffer.append(svalue.substring(i)); + i = svalue.length(); + } else { + buffer.append(svalue.substring(i, j + 1)); + buffer.append('"'); + i = j + 1; + } + } + + buffer.append('\"'); + return buffer.toString(); + } + + /** + * Open the new log file for the date specified by dateStamp. + */ + @Override + protected synchronized void open() { + super.open(); + if (currentLogFile.length() == 0) { + writer.println("#Fields: " + pattern); + writer.println("#Version: 2.0"); + writer.println("#Software: " + ServerInfo.getServerInfo()); + } + } + + + // ------------------------------------------------------ Lifecycle Methods + + + protected static class DateElement implements AccessLogElement { + // Milli-seconds in 24 hours + private static final long INTERVAL = (1000 * 60 * 60 * 24); + + private static final ThreadLocal currentDate = ThreadLocal + .withInitial(() -> new ElementTimestampStruct("yyyy-MM-dd")); + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + ElementTimestampStruct eds = currentDate.get(); + long millis = eds.currentTimestamp.getTime(); + if (date.getTime() > (millis + INTERVAL - 1) || date.getTime() < millis) { + eds.currentTimestamp.setTime(date.getTime() - (date.getTime() % INTERVAL)); + eds.currentTimestampString = eds.currentTimestampFormat.format(eds.currentTimestamp); + } + buf.append(eds.currentTimestampString); + } + } + + protected static class TimeElement implements AccessLogElement { + // Milli-seconds in a second + private static final long INTERVAL = 1000; + + private static final ThreadLocal currentTime = ThreadLocal + .withInitial(() -> new ElementTimestampStruct("HH:mm:ss")); + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + ElementTimestampStruct eds = currentTime.get(); + long millis = eds.currentTimestamp.getTime(); + if (date.getTime() > (millis + INTERVAL - 1) || date.getTime() < millis) { + eds.currentTimestamp.setTime(date.getTime() - (date.getTime() % INTERVAL)); + eds.currentTimestampString = eds.currentTimestampFormat.format(eds.currentTimestamp); + } + buf.append(eds.currentTimestampString); + } + } + + protected static class RequestHeaderElement implements AccessLogElement { + private final String header; + + public RequestHeaderElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getHeader(header))); + } + } + + protected static class ResponseHeaderElement implements AccessLogElement { + private final String header; + + public ResponseHeaderElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(response.getHeader(header))); + } + } + + protected static class ServletContextElement implements AccessLogElement { + private final String attribute; + + public ServletContextElement(String attribute) { + this.attribute = attribute; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getContext().getServletContext().getAttribute(attribute))); + } + } + + protected static class CookieElement implements AccessLogElement { + private final String name; + + public CookieElement(String name) { + this.name = name; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + StringBuilder value = new StringBuilder(); + boolean first = true; + Cookie[] c = request.getCookies(); + for (int i = 0; c != null && i < c.length; i++) { + if (name.equals(c[i].getName())) { + if (first) { + first = false; + } else { + value.append(','); + } + value.append(c[i].getValue()); + } + } + if (value.length() == 0) { + buf.append('-'); + } else { + buf.append(wrap(value.toString())); + } + } + } + + /** + * write a specific response header - x-O(xxx) + */ + protected static class ResponseAllHeaderElement implements AccessLogElement { + private final String header; + + public ResponseAllHeaderElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + if (null != response) { + Iterator iter = response.getHeaders(header).iterator(); + if (iter.hasNext()) { + StringBuilder buffer = new StringBuilder(); + boolean first = true; + while (iter.hasNext()) { + if (first) { + first = false; + } else { + buffer.append(','); + } + buffer.append(iter.next()); + } + buf.append(wrap(buffer.toString())); + } + return; + } + buf.append('-'); + } + } + + protected static class RequestAttributeElement implements AccessLogElement { + private final String attribute; + + public RequestAttributeElement(String attribute) { + this.attribute = attribute; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getAttribute(attribute))); + } + } + + protected static class SessionAttributeElement implements AccessLogElement { + private final String attribute; + + public SessionAttributeElement(String attribute) { + this.attribute = attribute; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + HttpSession session = null; + if (request != null) { + session = request.getSession(false); + if (session != null) { + buf.append(wrap(session.getAttribute(attribute))); + } + } + } + } + + protected static class RequestParameterElement implements AccessLogElement { + private final String parameter; + + public RequestParameterElement(String parameter) { + this.parameter = parameter; + } + + /** + * urlEncode the given string. If null or empty, return null. + */ + private String urlEncode(String value) { + if (null == value || value.length() == 0) { + return null; + } + return URLEncoder.QUERY.encode(value, StandardCharsets.UTF_8); + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(urlEncode(request.getParameter(parameter)))); + } + } + + protected static class PatternTokenizer { + private final StringReader sr; + private StringBuilder buf = new StringBuilder(); + private boolean ended = false; + private boolean subToken; + private boolean parameter; + + public PatternTokenizer(String str) { + sr = new StringReader(str); + } + + public boolean hasSubToken() { + return subToken; + } + + public boolean hasParameter() { + return parameter; + } + + public String getToken() throws IOException { + if (ended) { + return null; + } + + String result = null; + subToken = false; + parameter = false; + + int c = sr.read(); + while (c != -1) { + switch (c) { + case ' ': + result = buf.toString(); + buf.setLength(0); + buf.append((char) c); + return result; + case '-': + result = buf.toString(); + buf.setLength(0); + subToken = true; + return result; + case '(': + result = buf.toString(); + buf.setLength(0); + parameter = true; + return result; + case ')': + throw new IOException(sm.getString("patternTokenizer.unexpectedParenthesis")); + default: + buf.append((char) c); + } + c = sr.read(); + } + ended = true; + if (buf.length() != 0) { + return buf.toString(); + } else { + return null; + } + } + + public String getParameter() throws IOException { + String result; + if (!parameter) { + return null; + } + parameter = false; + int c = sr.read(); + while (c != -1) { + if (c == ')') { + result = buf.toString(); + buf = new StringBuilder(); + return result; + } + buf.append((char) c); + c = sr.read(); + } + return null; + } + + public String getWhiteSpaces() throws IOException { + if (isEnded()) { + return ""; + } + StringBuilder whiteSpaces = new StringBuilder(); + if (buf.length() > 0) { + whiteSpaces.append(buf); + buf = new StringBuilder(); + } + int c = sr.read(); + while (Character.isWhitespace((char) c)) { + whiteSpaces.append((char) c); + c = sr.read(); + } + if (c == -1) { + ended = true; + } else { + buf.append((char) c); + } + return whiteSpaces.toString(); + } + + public boolean isEnded() { + return ended; + } + + public String getRemains() throws IOException { + StringBuilder remains = new StringBuilder(); + for (int c = sr.read(); c != -1; c = sr.read()) { + remains.append((char) c); + } + return remains.toString(); + } + + } + + @Override + protected AccessLogElement[] createLogElements() { + if (log.isTraceEnabled()) { + log.trace("decodePattern, pattern =" + pattern); + } + List list = new ArrayList<>(); + + PatternTokenizer tokenizer = new PatternTokenizer(pattern); + try { + + // Ignore leading whitespace. + tokenizer.getWhiteSpaces(); + + if (tokenizer.isEnded()) { + log.info(sm.getString("extendedAccessLogValve.emptyPattern")); + return null; + } + + String token = tokenizer.getToken(); + while (token != null) { + if (log.isTraceEnabled()) { + log.trace("token = " + token); + } + AccessLogElement element = getLogElement(token, tokenizer); + if (element == null) { + break; + } + list.add(element); + String whiteSpaces = tokenizer.getWhiteSpaces(); + if (whiteSpaces.length() > 0) { + list.add(new StringElement(whiteSpaces)); + } + if (tokenizer.isEnded()) { + break; + } + token = tokenizer.getToken(); + } + if (log.isTraceEnabled()) { + log.trace("finished decoding with element size of: " + list.size()); + } + return list.toArray(new AccessLogElement[0]); + } catch (IOException e) { + log.error(sm.getString("extendedAccessLogValve.patternParseError", pattern), e); + return null; + } + } + + protected AccessLogElement getLogElement(String token, PatternTokenizer tokenizer) throws IOException { + if ("date".equals(token)) { + return new DateElement(); + } else if ("time".equals(token)) { + if (tokenizer.hasSubToken()) { + String nextToken = tokenizer.getToken(); + if ("taken".equals(nextToken)) { + return new ElapsedTimeElement(false, false); + } + } else { + return new TimeElement(); + } + } else if ("bytes".equals(token)) { + return new ByteSentElement(true); + } else if ("cached".equals(token)) { + /* I don't know how to evaluate this! */ + return new StringElement("-"); + } else if ("c".equals(token)) { + String nextToken = tokenizer.getToken(); + if ("ip".equals(nextToken)) { + return new RemoteAddrElement(); + } else if ("dns".equals(nextToken)) { + return new HostElement(); + } + } else if ("s".equals(token)) { + String nextToken = tokenizer.getToken(); + if ("ip".equals(nextToken)) { + return new LocalAddrElement(getIpv6Canonical()); + } else if ("dns".equals(nextToken)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, + long time) { + String value; + try { + value = InetAddress.getLocalHost().getHostName(); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + value = "localhost"; + } + buf.append(value); + } + }; + } + } else if ("cs".equals(token)) { + return getClientToServerElement(tokenizer); + } else if ("sc".equals(token)) { + return getServerToClientElement(tokenizer); + } else if ("sr".equals(token) || "rs".equals(token)) { + return getProxyElement(tokenizer); + } else if ("x".equals(token)) { + return getXParameterElement(tokenizer); + } + log.error(sm.getString("extendedAccessLogValve.decodeError", token)); + return null; + } + + protected AccessLogElement getClientToServerElement(PatternTokenizer tokenizer) throws IOException { + if (tokenizer.hasSubToken()) { + String token = tokenizer.getToken(); + if ("method".equals(token)) { + return new MethodElement(); + } else if ("uri".equals(token)) { + if (tokenizer.hasSubToken()) { + token = tokenizer.getToken(); + if ("stem".equals(token)) { + return new RequestURIElement(); + } else if ("query".equals(token)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, + long time) { + String query = request.getQueryString(); + if (query != null) { + buf.append(query); + } else { + buf.append('-'); + } + } + }; + } + } else { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, + long time) { + String query = request.getQueryString(); + if (query == null) { + buf.append(request.getRequestURI()); + } else { + buf.append(request.getRequestURI()); + buf.append('?'); + buf.append(request.getQueryString()); + } + } + }; + } + } + } else if (tokenizer.hasParameter()) { + String parameter = tokenizer.getParameter(); + if (parameter == null) { + log.error(sm.getString("extendedAccessLogValve.noClosing")); + return null; + } + return new RequestHeaderElement(parameter); + } + log.error(sm.getString("extendedAccessLogValve.decodeError", tokenizer.getRemains())); + return null; + } + + protected AccessLogElement getServerToClientElement(PatternTokenizer tokenizer) throws IOException { + if (tokenizer.hasSubToken()) { + String token = tokenizer.getToken(); + if ("status".equals(token)) { + return new HttpStatusCodeElement(); + } else if ("comment".equals(token)) { + return new StringElement("?"); + } + } else if (tokenizer.hasParameter()) { + String parameter = tokenizer.getParameter(); + if (parameter == null) { + log.error(sm.getString("extendedAccessLogValve.noClosing")); + return null; + } + return new ResponseHeaderElement(parameter); + } + log.error(sm.getString("extendedAccessLogValve.decodeError", tokenizer.getRemains())); + return null; + } + + protected AccessLogElement getProxyElement(PatternTokenizer tokenizer) throws IOException { + String token = null; + if (tokenizer.hasSubToken()) { + tokenizer.getToken(); + return new StringElement("-"); + } else if (tokenizer.hasParameter()) { + tokenizer.getParameter(); + return new StringElement("-"); + } + log.error(sm.getString("extendedAccessLogValve.decodeError", token)); + return null; + } + + protected AccessLogElement getXParameterElement(PatternTokenizer tokenizer) throws IOException { + if (!tokenizer.hasSubToken()) { + log.error(sm.getString("extendedAccessLogValve.badXParam")); + return null; + } + String token = tokenizer.getToken(); + if ("threadname".equals(token)) { + return new ThreadNameElement(); + } + + if (!tokenizer.hasParameter()) { + log.error(sm.getString("extendedAccessLogValve.badXParam")); + return null; + } + String parameter = tokenizer.getParameter(); + if (parameter == null) { + log.error(sm.getString("extendedAccessLogValve.noClosing")); + return null; + } + if ("A".equals(token)) { + return new ServletContextElement(parameter); + } else if ("C".equals(token)) { + return new CookieElement(parameter); + } else if ("R".equals(token)) { + return new RequestAttributeElement(parameter); + } else if ("S".equals(token)) { + return new SessionAttributeElement(parameter); + } else if ("H".equals(token)) { + return getServletRequestElement(parameter); + } else if ("P".equals(token)) { + return new RequestParameterElement(parameter); + } else if ("O".equals(token)) { + return new ResponseAllHeaderElement(parameter); + } + log.error(sm.getString("extendedAccessLogValve.badXParamValue", token)); + return null; + } + + protected AccessLogElement getServletRequestElement(String parameter) { + if ("authType".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getAuthType())); + } + }; + } else if ("remoteUser".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getRemoteUser())); + } + }; + } else if ("requestedSessionId".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getRequestedSessionId())); + } + }; + } else if ("requestedSessionIdFromCookie".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap("" + request.isRequestedSessionIdFromCookie())); + } + }; + } else if ("requestedSessionIdValid".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap("" + request.isRequestedSessionIdValid())); + } + }; + } else if ("contentLength".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap("" + request.getContentLengthLong())); + } + }; + } else if ("characterEncoding".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getCharacterEncoding())); + } + }; + } else if ("locale".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getLocale())); + } + }; + } else if ("protocol".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap(request.getProtocol())); + } + }; + } else if ("scheme".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(request.getScheme()); + } + }; + } else if ("secure".equals(parameter)) { + return new AccessLogElement() { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append(wrap("" + request.isSecure())); + } + }; + } + log.error(sm.getString("extendedAccessLogValve.badXParamValue", parameter)); + return null; + } + + private static class ElementTimestampStruct { + private final Date currentTimestamp = new Date(0); + private final SimpleDateFormat currentTimestampFormat; + private String currentTimestampString; + + ElementTimestampStruct(String format) { + currentTimestampFormat = new SimpleDateFormat(format, Locale.US); + currentTimestampFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + } + } +} diff --git a/java/org/apache/catalina/valves/HealthCheckValve.java b/java/org/apache/catalina/valves/HealthCheckValve.java new file mode 100644 index 0000000..d411f82 --- /dev/null +++ b/java/org/apache/catalina/valves/HealthCheckValve.java @@ -0,0 +1,109 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.LifecycleBase; +import org.apache.tomcat.util.buf.MessageBytes; + + +/** + * Simple Valve that responds to cloud orchestrators health checks. + */ +public class HealthCheckValve extends ValveBase { + + private static final String UP = "{\n" + " \"status\": \"UP\",\n" + " \"checks\": []\n" + "}"; + + private static final String DOWN = "{\n" + " \"status\": \"DOWN\",\n" + " \"checks\": []\n" + "}"; + + private String path = "/health"; + + /** + * Will be set to true if the valve is associated with a context. + */ + protected boolean context = false; + + /** + * Check if all child containers are available. + */ + protected boolean checkContainersAvailable = true; + + public HealthCheckValve() { + super(true); + } + + public final String getPath() { + return path; + } + + public final void setPath(String path) { + this.path = path; + } + + public boolean getCheckContainersAvailable() { + return this.checkContainersAvailable; + } + + public void setCheckContainersAvailable(boolean checkContainersAvailable) { + this.checkContainersAvailable = checkContainersAvailable; + } + + @Override + protected void startInternal() throws LifecycleException { + super.startInternal(); + context = (getContainer() instanceof Context); + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + MessageBytes urlMB = context ? request.getRequestPathMB() : request.getDecodedRequestURIMB(); + if (urlMB.equals(path)) { + response.setContentType("application/json"); + if (!checkContainersAvailable || isAvailable(getContainer())) { + response.getOutputStream().print(UP); + } else { + response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + response.getOutputStream().print(DOWN); + } + } else { + getNext().invoke(request, response); + } + } + + protected boolean isAvailable(Container container) { + for (Container child : container.findChildren()) { + if (!isAvailable(child)) { + return false; + } + } + if (container instanceof LifecycleBase) { + return ((LifecycleBase) container).getState().isAvailable(); + } else { + return true; + } + } + +} diff --git a/java/org/apache/catalina/valves/JDBCAccessLogValve.java b/java/org/apache/catalina/valves/JDBCAccessLogValve.java new file mode 100644 index 0000000..c93874a --- /dev/null +++ b/java/org/apache/catalina/valves/JDBCAccessLogValve.java @@ -0,0 +1,657 @@ +/* + * 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.catalina.valves; + + +import java.io.IOException; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Properties; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.tomcat.util.ExceptionUtils; + +/** + *

    + * This Tomcat extension logs server access directly to a database, and can be used instead of the regular file-based + * access log implemented in AccessLogValve. To use, copy into the server/classes directory of the Tomcat installation + * and configure in server.xml as: + *

    + * + *
    + *      <Valve className="org.apache.catalina.valves.JDBCAccessLogValve"
    + *          driverName="your_jdbc_driver"
    + *          connectionURL="your_jdbc_url"
    + *          pattern="combined" resolveHosts="false"
    + *      />
    + * 
    + *

    + * Many parameters can be configured, such as the database connection (with driverName and + * connectionURL), the table name (tableName) and the field names (corresponding to the + * get/set method names). The same options as AccessLogValve are supported, such as resolveHosts and + * pattern ("common" or "combined" only). + *

    + *

    + * When Tomcat is started, a database connection is created and used for all the log activity. When Tomcat is shutdown, + * the database connection is closed. This logger can be used at the level of the Engine context (being shared by all + * the defined hosts) or the Host context (one instance of the logger per host, possibly using different databases). + *

    + *

    + * The database table can be created with the following command: + *

    + * + *
    + * CREATE TABLE access (
    + * id INT UNSIGNED AUTO_INCREMENT NOT NULL,
    + * remoteHost CHAR(15) NOT NULL,
    + * userName CHAR(15),
    + * timestamp TIMESTAMP NOT NULL,
    + * virtualHost VARCHAR(64) NOT NULL,
    + * method VARCHAR(8) NOT NULL,
    + * query VARCHAR(255) NOT NULL,
    + * status SMALLINT UNSIGNED NOT NULL,
    + * bytes INT UNSIGNED NOT NULL,
    + * referer VARCHAR(128),
    + * userAgent VARCHAR(128),
    + * PRIMARY KEY (id),
    + * INDEX (timestamp),
    + * INDEX (remoteHost),
    + * INDEX (virtualHost),
    + * INDEX (query),
    + * INDEX (userAgent)
    + * );
    + * 
    + *

    + * Set JDBCAccessLogValve attribute useLongContentLength="true" as you have more then 4GB outputs. Please, use long SQL + * datatype at access.bytes attribute. The datatype of bytes at oracle is number and other databases use bytes + * BIGINT NOT NULL. + *

    + *

    + * If the table is created as above, its name and the field names don't need to be defined. + *

    + *

    + * If the request method is "common", only these fields are used: + * remoteHost, user, timeStamp, query, status, bytes + *

    + *

    + * TO DO: provide option for excluding logging of certain MIME types. + *

    + * + * @author Andre de Jesus + * @author Peter Rossbach + */ + +public final class JDBCAccessLogValve extends ValveBase implements AccessLog { + + // ----------------------------------------------------------- Constructors + + + /** + * Class constructor. Initializes the fields with the default values. The defaults are: + * + *
    +     * driverName = null;
    +     * connectionURL = null;
    +     * tableName = "access";
    +     * remoteHostField = "remoteHost";
    +     * userField = "userName";
    +     * timestampField = "timestamp";
    +     * virtualHostField = "virtualHost";
    +     * methodField = "method";
    +     * queryField = "query";
    +     * statusField = "status";
    +     * bytesField = "bytes";
    +     * refererField = "referer";
    +     * userAgentField = "userAgent";
    +     * pattern = "common";
    +     * resolveHosts = false;
    +     * 
    + */ + public JDBCAccessLogValve() { + super(true); + driverName = null; + connectionURL = null; + tableName = "access"; + remoteHostField = "remoteHost"; + userField = "userName"; + timestampField = "timestamp"; + virtualHostField = "virtualHost"; + methodField = "method"; + queryField = "query"; + statusField = "status"; + bytesField = "bytes"; + refererField = "referer"; + userAgentField = "userAgent"; + pattern = "common"; + resolveHosts = false; + conn = null; + ps = null; + currentTimeMillis = new java.util.Date().getTime(); + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Use long contentLength as you have more 4 GB output. + * + * @since 6.0.15 + */ + boolean useLongContentLength = false; + + /** + * The connection username to use when trying to connect to the database. + */ + String connectionName = null; + + + /** + * The connection URL to use when trying to connect to the database. + */ + String connectionPassword = null; + + /** + * Instance of the JDBC Driver class we use as a connection factory. + */ + Driver driver = null; + + + private String driverName; + private String connectionURL; + private String tableName; + private String remoteHostField; + private String userField; + private String timestampField; + private String virtualHostField; + private String methodField; + private String queryField; + private String statusField; + private String bytesField; + private String refererField; + private String userAgentField; + private String pattern; + private boolean resolveHosts; + + + private Connection conn; + private PreparedStatement ps; + + + private long currentTimeMillis; + + /** + * Should this valve set request attributes for IP address, hostname, protocol and port used for the request. + * Default is true. + * + * @see #setRequestAttributesEnabled(boolean) + */ + boolean requestAttributesEnabled = true; + + + // ------------------------------------------------------------- Properties + + /** + * {@inheritDoc} Default is true. + */ + @Override + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + this.requestAttributesEnabled = requestAttributesEnabled; + } + + @Override + public boolean getRequestAttributesEnabled() { + return requestAttributesEnabled; + } + + /** + * @return the username to use to connect to the database. + */ + public String getConnectionName() { + return connectionName; + } + + /** + * Set the username to use to connect to the database. + * + * @param connectionName Username + */ + public void setConnectionName(String connectionName) { + this.connectionName = connectionName; + } + + /** + * Sets the database driver name. + * + * @param driverName The complete name of the database driver class. + */ + public void setDriverName(String driverName) { + this.driverName = driverName; + } + + /** + * @return the password to use to connect to the database. + */ + public String getConnectionPassword() { + return connectionPassword; + } + + /** + * Set the password to use to connect to the database. + * + * @param connectionPassword User password + */ + public void setConnectionPassword(String connectionPassword) { + this.connectionPassword = connectionPassword; + } + + /** + * Sets the JDBC URL for the database where the log is stored. + * + * @param connectionURL The JDBC URL of the database. + */ + public void setConnectionURL(String connectionURL) { + this.connectionURL = connectionURL; + } + + + /** + * Sets the name of the table where the logs are stored. + * + * @param tableName The name of the table. + */ + public void setTableName(String tableName) { + this.tableName = tableName; + } + + + /** + * Sets the name of the field containing the remote host. + * + * @param remoteHostField The name of the remote host field. + */ + public void setRemoteHostField(String remoteHostField) { + this.remoteHostField = remoteHostField; + } + + + /** + * Sets the name of the field containing the remote user name. + * + * @param userField The name of the remote user field. + */ + public void setUserField(String userField) { + this.userField = userField; + } + + + /** + * Sets the name of the field containing the server-determined timestamp. + * + * @param timestampField The name of the server-determined timestamp field. + */ + public void setTimestampField(String timestampField) { + this.timestampField = timestampField; + } + + + /** + * Sets the name of the field containing the virtual host information (this is in fact the server name). + * + * @param virtualHostField The name of the virtual host field. + */ + public void setVirtualHostField(String virtualHostField) { + this.virtualHostField = virtualHostField; + } + + + /** + * Sets the name of the field containing the HTTP request method. + * + * @param methodField The name of the HTTP request method field. + */ + public void setMethodField(String methodField) { + this.methodField = methodField; + } + + + /** + * Sets the name of the field containing the URL part of the HTTP query. + * + * @param queryField The name of the field containing the URL part of the HTTP query. + */ + public void setQueryField(String queryField) { + this.queryField = queryField; + } + + + /** + * Sets the name of the field containing the HTTP response status code. + * + * @param statusField The name of the HTTP response status code field. + */ + public void setStatusField(String statusField) { + this.statusField = statusField; + } + + + /** + * Sets the name of the field containing the number of bytes returned. + * + * @param bytesField The name of the returned bytes field. + */ + public void setBytesField(String bytesField) { + this.bytesField = bytesField; + } + + + /** + * Sets the name of the field containing the referer. + * + * @param refererField The referer field name. + */ + public void setRefererField(String refererField) { + this.refererField = refererField; + } + + + /** + * Sets the name of the field containing the user agent. + * + * @param userAgentField The name of the user agent field. + */ + public void setUserAgentField(String userAgentField) { + this.userAgentField = userAgentField; + } + + + /** + * Sets the logging pattern. The patterns supported correspond to the file-based "common" and "combined". These are + * translated into the use of tables containing either set of fields. + *

    + * TO DO: more flexible field choices. + *

    + * + * @param pattern The name of the logging pattern. + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + + /** + * Determines whether IP host name resolution is done. + * + * @param resolveHosts "true" or "false", if host IP resolution is desired or not. + */ + public void setResolveHosts(String resolveHosts) { + this.resolveHosts = Boolean.parseBoolean(resolveHosts); + } + + /** + * @return true if content length should be considered a long rather than an int, defaults to + * false + */ + public boolean getUseLongContentLength() { + return this.useLongContentLength; + } + + /** + * @param useLongContentLength the useLongContentLength to set + */ + public void setUseLongContentLength(boolean useLongContentLength) { + this.useLongContentLength = useLongContentLength; + } + + // --------------------------------------------------------- Public Methods + + + /** + * This method is invoked by Tomcat on each query. + * + * @param request The Request object. + * @param response The Response object. + * + * @exception IOException Should not be thrown. + * @exception ServletException Database SQLException is wrapped in a ServletException. + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + getNext().invoke(request, response); + } + + + @Override + public void log(Request request, Response response, long time) { + if (!getState().isAvailable()) { + return; + } + + final String EMPTY = ""; + + String remoteHost; + if (resolveHosts) { + if (requestAttributesEnabled) { + Object host = request.getAttribute(REMOTE_HOST_ATTRIBUTE); + if (host == null) { + remoteHost = request.getRemoteHost(); + } else { + remoteHost = (String) host; + } + } else { + remoteHost = request.getRemoteHost(); + } + } else { + if (requestAttributesEnabled) { + Object addr = request.getAttribute(REMOTE_ADDR_ATTRIBUTE); + if (addr == null) { + remoteHost = request.getRemoteAddr(); + } else { + remoteHost = (String) addr; + } + } else { + remoteHost = request.getRemoteAddr(); + } + } + String user = request.getRemoteUser(); + String query = request.getRequestURI(); + + long bytes = response.getBytesWritten(true); + if (bytes < 0) { + bytes = 0; + } + int status = response.getStatus(); + String virtualHost = EMPTY; + String method = EMPTY; + String referer = EMPTY; + String userAgent = EMPTY; + String logPattern = pattern; + if (logPattern.equals("combined")) { + virtualHost = request.getServerName(); + method = request.getMethod(); + referer = request.getHeader("referer"); + userAgent = request.getHeader("user-agent"); + } + synchronized (this) { + int numberOfTries = 2; + while (numberOfTries > 0) { + try { + open(); + + ps.setString(1, remoteHost); + ps.setString(2, user); + ps.setTimestamp(3, new Timestamp(getCurrentTimeMillis())); + ps.setString(4, query); + ps.setInt(5, status); + + if (useLongContentLength) { + ps.setLong(6, bytes); + } else { + if (bytes > Integer.MAX_VALUE) { + bytes = -1; + } + ps.setInt(6, (int) bytes); + } + if (logPattern.equals("combined")) { + ps.setString(7, virtualHost); + ps.setString(8, method); + ps.setString(9, referer); + ps.setString(10, userAgent); + } + ps.executeUpdate(); + return; + } catch (SQLException e) { + // Log the problem for posterity + container.getLogger().error(sm.getString("jdbcAccessLogValve.exception"), e); + + // Close the connection so that it gets reopened next time + if (conn != null) { + close(); + } + } + numberOfTries--; + } + } + + } + + + /** + * Open (if necessary) and return a database connection for use by this AccessLogValve. + * + * @exception SQLException if a database error occurs + */ + protected void open() throws SQLException { + + // Do nothing if there is a database connection already open + if (conn != null) { + return; + } + + // Instantiate our database driver if necessary + if (driver == null) { + try { + Class clazz = Class.forName(driverName); + driver = (Driver) clazz.getConstructor().newInstance(); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + throw new SQLException(e.getMessage(), e); + } + } + + // Open a new connection + Properties props = new Properties(); + if (connectionName != null) { + props.put("user", connectionName); + } + if (connectionPassword != null) { + props.put("password", connectionPassword); + } + conn = driver.connect(connectionURL, props); + conn.setAutoCommit(true); + String logPattern = pattern; + if (logPattern.equals("common")) { + ps = conn.prepareStatement( + "INSERT INTO " + tableName + " (" + remoteHostField + ", " + userField + ", " + timestampField + + ", " + queryField + ", " + statusField + ", " + bytesField + ") VALUES(?, ?, ?, ?, ?, ?)"); + } else if (logPattern.equals("combined")) { + ps = conn.prepareStatement("INSERT INTO " + tableName + " (" + remoteHostField + ", " + userField + ", " + + timestampField + ", " + queryField + ", " + statusField + ", " + bytesField + ", " + + virtualHostField + ", " + methodField + ", " + refererField + ", " + userAgentField + + ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + } + } + + /** + * Close the specified database connection. + */ + protected void close() { + + // Do nothing if the database connection is already closed + if (conn == null) { + return; + } + + // Close our prepared statements (if any) + try { + ps.close(); + } catch (Throwable f) { + ExceptionUtils.handleThrowable(f); + } + this.ps = null; + + + // Close this database connection, and log any errors + try { + conn.close(); + } catch (SQLException e) { + container.getLogger().error(sm.getString("jdbcAccessLogValve.close"), e); // Just log it here + } finally { + this.conn = null; + } + + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + try { + open(); + } catch (SQLException e) { + throw new LifecycleException(e); + } + super.startInternal(); + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + close(); + } + + + public long getCurrentTimeMillis() { + long systime = System.currentTimeMillis(); + if ((systime - currentTimeMillis) > 1000) { + currentTimeMillis = new java.util.Date(systime).getTime(); + } + return currentTimeMillis; + } + +} diff --git a/java/org/apache/catalina/valves/JsonAccessLogValve.java b/java/org/apache/catalina/valves/JsonAccessLogValve.java new file mode 100644 index 0000000..61ca3d0 --- /dev/null +++ b/java/org/apache/catalina/valves/JsonAccessLogValve.java @@ -0,0 +1,271 @@ +/* + * 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.catalina.valves; + +import java.io.CharArrayWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.tomcat.util.json.JSONFilter; + +/** + * Access log valve derivative that rewrites entries as JSON. + * Important note: the attribute names are not final + * Patterns are mapped to attributes as followed: + *
      + *
    • a: remoteAddr
    • + *
    • A: localAddr
    • + *
    • b: size (byteSent: size)
    • + *
    • B: byteSentNC
    • + *
    • D: elapsedTime
    • + *
    • F: firstByteTime
    • + *
    • h: host
    • + *
    • H: protocol
    • + *
    • l: logicalUserName
    • + *
    • m: method
    • + *
    • p: port
    • + *
    • q: query
    • + *
    • r: request
    • + *
    • s: statusCode
    • + *
    • S: sessionId
    • + *
    • t: time (dateTime: time)
    • + *
    • T: elapsedTimeS
    • + *
    • u: user
    • + *
    • U: path (requestURI: path)
    • + *
    • v: localServerName
    • + *
    • I: threadName
    • + *
    • X: connectionStatus
    • + *
    • %{xxx}a: remoteAddress-xxx
    • + *
    • %{xxx}p: port-xxx
    • + *
    • %{xxx}t: time-xxx
    • + *
    • %{xxx}c: cookies
    • + *
    • %{xxx}i: requestHeaders
    • + *
    • %{xxx}o: responseHeaders
    • + *
    • %{xxx}r: requestAttributes
    • + *
    • %{xxx}s: sessionAttributes
    • + *
    + * The attribute list is based on + * https://github.com/fluent/fluentd/blob/master/lib/fluent/plugin/parser_apache2.rb#L72 + */ +public class JsonAccessLogValve extends AccessLogValve { + + private static final Map PATTERNS; + static { + Map pattern2AttributeName = new HashMap<>(); + pattern2AttributeName.put(Character.valueOf('a'), "remoteAddr"); + pattern2AttributeName.put(Character.valueOf('A'), "localAddr"); + pattern2AttributeName.put(Character.valueOf('b'), "size"); + pattern2AttributeName.put(Character.valueOf('B'), "byteSentNC"); + pattern2AttributeName.put(Character.valueOf('D'), "elapsedTime"); + pattern2AttributeName.put(Character.valueOf('F'), "firstByteTime"); + pattern2AttributeName.put(Character.valueOf('h'), "host"); + pattern2AttributeName.put(Character.valueOf('H'), "protocol"); + pattern2AttributeName.put(Character.valueOf('I'), "threadName"); + pattern2AttributeName.put(Character.valueOf('l'), "logicalUserName"); + pattern2AttributeName.put(Character.valueOf('m'), "method"); + pattern2AttributeName.put(Character.valueOf('p'), "port"); + pattern2AttributeName.put(Character.valueOf('q'), "query"); + pattern2AttributeName.put(Character.valueOf('r'), "request"); + pattern2AttributeName.put(Character.valueOf('s'), "statusCode"); + pattern2AttributeName.put(Character.valueOf('S'), "sessionId"); + pattern2AttributeName.put(Character.valueOf('t'), "time"); + pattern2AttributeName.put(Character.valueOf('T'), "elapsedTimeS"); + pattern2AttributeName.put(Character.valueOf('u'), "user"); + pattern2AttributeName.put(Character.valueOf('U'), "path"); + pattern2AttributeName.put(Character.valueOf('v'), "localServerName"); + pattern2AttributeName.put(Character.valueOf('X'), "connectionStatus"); + PATTERNS = Collections.unmodifiableMap(pattern2AttributeName); + } + + private static final Map SUB_OBJECT_PATTERNS; + static { + Map pattern2AttributeName = new HashMap<>(); + pattern2AttributeName.put(Character.valueOf('c'), "cookies"); + pattern2AttributeName.put(Character.valueOf('i'), "requestHeaders"); + pattern2AttributeName.put(Character.valueOf('o'), "responseHeaders"); + pattern2AttributeName.put(Character.valueOf('r'), "requestAttributes"); + pattern2AttributeName.put(Character.valueOf('s'), "sessionAttributes"); + SUB_OBJECT_PATTERNS = Collections.unmodifiableMap(pattern2AttributeName); + } + + /** + * write any char + */ + protected static class CharElement implements AccessLogElement { + private final char ch; + + public CharElement(char ch) { + this.ch = ch; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.write(ch); + } + } + + private boolean addSubkeyedItems(ListIterator iterator, List elements, String patternAttribute) { + if (! elements.isEmpty()) { + iterator.add(new StringElement("\"" + patternAttribute + "\": {")); + for (JsonWrappedElement element: elements) { + iterator.add(element); + iterator.add(new CharElement(',')); + } + iterator.previous(); + iterator.remove(); + iterator.add(new StringElement("},")); + return true; + } + return false; + } + + @Override + protected AccessLogElement[] createLogElements() { + Map> subTypeLists = new HashMap<>(); + for (Character pattern: SUB_OBJECT_PATTERNS.keySet()) { + subTypeLists.put(pattern, new ArrayList<>()); + } + boolean hasSub = false; + List logElements = new ArrayList<>(Arrays.asList(super.createLogElements())); + ListIterator lit = logElements.listIterator(); + lit.add(new CharElement('{')); + while (lit.hasNext()) { + AccessLogElement logElement = lit.next(); + // remove all other elements, like StringElements + if (!(logElement instanceof JsonWrappedElement)) { + lit.remove(); + continue; + } + // Remove items which should be written as + // Json objects and add them later in correct order + JsonWrappedElement wrappedLogElement = (JsonWrappedElement)logElement; + AccessLogElement ale = wrappedLogElement.getDelegate(); + if (ale instanceof HeaderElement) { + subTypeLists.get(Character.valueOf('i')).add(wrappedLogElement); + lit.remove(); + } else if (ale instanceof ResponseHeaderElement) { + subTypeLists.get(Character.valueOf('o')).add(wrappedLogElement); + lit.remove(); + } else if (ale instanceof RequestAttributeElement) { + subTypeLists.get(Character.valueOf('r')).add(wrappedLogElement); + lit.remove(); + } else if (ale instanceof SessionAttributeElement) { + subTypeLists.get(Character.valueOf('s')).add(wrappedLogElement); + lit.remove(); + } else if (ale instanceof CookieElement) { + subTypeLists.get(Character.valueOf('c')).add(wrappedLogElement); + lit.remove(); + } else { + // Keep the simple items and add separator + lit.add(new CharElement(',')); + } + } + // Add back the items that are output as Json objects + for (Character pattern: SUB_OBJECT_PATTERNS.keySet()) { + if (addSubkeyedItems(lit, subTypeLists.get(pattern), SUB_OBJECT_PATTERNS.get(pattern))) { + hasSub = true; + } + } + // remove last comma (or possibly "},") + lit.previous(); + lit.remove(); + // Last item was a sub object, close it + if (hasSub) { + lit.add(new StringElement("}}")); + } else { + lit.add(new CharElement('}')); + } + return logElements.toArray(new AccessLogElement[0]); + } + + @Override + protected AccessLogElement createAccessLogElement(String name, char pattern) { + AccessLogElement ale = super.createAccessLogElement(name, pattern); + return new JsonWrappedElement(pattern, name, true, ale); + } + + @Override + protected AccessLogElement createAccessLogElement(char pattern) { + AccessLogElement ale = super.createAccessLogElement(pattern); + return new JsonWrappedElement(pattern, true, ale); + } + + private static class JsonWrappedElement implements AccessLogElement, CachedElement { + + private CharSequence attributeName; + private boolean quoteValue; + private AccessLogElement delegate; + + private CharSequence escapeJsonString(CharSequence nonEscaped) { + return JSONFilter.escape(nonEscaped); + } + + JsonWrappedElement(char pattern, String key, boolean quoteValue, AccessLogElement delegate) { + this.quoteValue = quoteValue; + this.delegate = delegate; + String patternAttribute = PATTERNS.get(Character.valueOf(pattern)); + if (patternAttribute == null) { + patternAttribute = "other-" + Character.toString(pattern); + } + if (key != null && ! "".equals(key)) { + if (SUB_OBJECT_PATTERNS.containsKey(Character.valueOf(pattern))) { + this.attributeName = escapeJsonString(key); + } else { + this.attributeName = escapeJsonString(patternAttribute + "-" + key); + } + } else { + this.attributeName = escapeJsonString(patternAttribute); + } + } + + JsonWrappedElement(char pattern, boolean quoteValue, AccessLogElement delegate) { + this(pattern, null, quoteValue, delegate); + } + + public AccessLogElement getDelegate() { + return delegate; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + buf.append('"').append(attributeName).append('"').append(':'); + if (quoteValue) { + buf.append('"'); + } + delegate.addElement(buf, date, request, response, time); + if (quoteValue) { + buf.append('"'); + } + } + + @Override + public void cache(Request request) { + if (delegate instanceof CachedElement) { + ((CachedElement) delegate).cache(request); + } + } + } + +} diff --git a/java/org/apache/catalina/valves/JsonErrorReportValve.java b/java/org/apache/catalina/valves/JsonErrorReportValve.java new file mode 100644 index 0000000..c073783 --- /dev/null +++ b/java/org/apache/catalina/valves/JsonErrorReportValve.java @@ -0,0 +1,109 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.io.Writer; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.coyote.ActionCode; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.json.JSONFilter; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * Implementation of a Valve that outputs error jsons. + *

    + *

    + * This Valve should be attached at the Host level, although it will work if attached to a Context. + *

    + */ +public class JsonErrorReportValve extends ErrorReportValve { + + public JsonErrorReportValve() { + super(); + } + + @Override + protected void report(Request request, Response response, Throwable throwable) { + + int statusCode = response.getStatus(); + + // Do nothing on a 1xx, 2xx and 3xx status + // Do nothing if anything has been written already + // Do nothing if the response hasn't been explicitly marked as in error + // and that error has not been reported. + if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) { + return; + } + + // If an error has occurred that prevents further I/O, don't waste time + // producing an error report that will never be read + AtomicBoolean result = new AtomicBoolean(false); + response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result); + if (!result.get()) { + return; + } + + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + response.setLocale(smClient.getLocale()); + String type = null; + if (throwable != null) { + type = smClient.getString("errorReportValve.exceptionReport"); + } else { + type = smClient.getString("errorReportValve.statusReport"); + } + String message = response.getMessage(); + if (message == null && throwable != null) { + message = throwable.getMessage(); + } + String description = null; + description = smClient.getString("http." + statusCode + ".desc"); + if (description == null) { + if (message == null || message.isEmpty()) { + return; + } else { + description = smClient.getString("errorReportValve.noDescription"); + } + } + String jsonReport = "{\n" + " \"type\": \"" + JSONFilter.escape(type) + "\",\n" + " \"message\": \"" + + JSONFilter.escape(message) + "\",\n" + " \"description\": \"" + JSONFilter.escape(description) + + "\"\n" + "}"; + try { + try { + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (container.getLogger().isDebugEnabled()) { + container.getLogger().debug(sm.getString("errorReportValve.contentTypeFail"), t); + } + } + Writer writer = response.getReporter(); + if (writer != null) { + writer.write(jsonReport); + response.finishResponse(); + return; + } + } catch (IOException | IllegalStateException e) { + // Ignore + } + } +} diff --git a/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java b/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java new file mode 100644 index 0000000..09214f2 --- /dev/null +++ b/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java @@ -0,0 +1,232 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.SessionConfig; + +/** + *

    + * A Valve to detect situations where a load-balanced node receiving a request has been deactivated by the load balancer + * (JK_LB_ACTIVATION=DIS) and the incoming request has no valid session. + *

    + *

    + * In these cases, the user's session cookie should be removed if it exists, any ";jsessionid" parameter should be + * removed from the request URI, and the client should be redirected to the same URI. This will cause the load-balanced + * to re-balance the client to another server. + *

    + *

    + * All this work is required because when the activation state of a node is DISABLED, the load-balancer will still send + * requests to the node if they appear to have a session on that node. Since mod_jk doesn't actually know whether the + * session id is valid, it will send the request blindly to the disabled node, which makes it take much longer to drain + * the node than strictly necessary. + *

    + *

    + * For testing purposes, a special cookie can be configured and used by a client to ignore the normal behavior of this + * Valve and allow a client to get a new session on a DISABLED node. See {@link #setIgnoreCookieName} and + * {@link #setIgnoreCookieValue} to configure those values. + *

    + *

    + * This Valve should be installed earlier in the Valve pipeline than any authentication valves, as the redirection + * should take place before an authentication valve would save a request to a protected resource. + *

    + * + * @see Load balancer + * documentation + */ +public class LoadBalancerDrainingValve extends ValveBase { + + /** + * The request attribute key where the load-balancer's activation state can be found. + */ + public static final String ATTRIBUTE_KEY_JK_LB_ACTIVATION = "JK_LB_ACTIVATION"; + + /** + * The HTTP response code that will be used to redirect the request back to the load-balancer for re-balancing. + * Defaults to 307 (TEMPORARY_REDIRECT). HTTP status code 305 (USE_PROXY) might be an option, here. too. + */ + private int _redirectStatusCode = HttpServletResponse.SC_TEMPORARY_REDIRECT; + + /** + * The name of the cookie which can be set to ignore the "draining" action of this Filter. This will allow a client + * to contact the server without being re-balanced to another server. The expected cookie value can be set in the + * {@link #_ignoreCookieValue}. The cookie name and value must match to avoid being re-balanced. + */ + private String _ignoreCookieName; + + /** + * The value of the cookie which can be set to ignore the "draining" action of this Filter. This will allow a client + * to contact the server without being re-balanced to another server. The expected cookie name can be set in the + * {@link #_ignoreCookieName}. The cookie name and value must match to avoid being re-balanced. + */ + private String _ignoreCookieValue; + + public LoadBalancerDrainingValve() { + super(true); // Supports async + } + + // + // Configuration parameters + // + + /** + * Sets the HTTP response code that will be used to redirect the request back to the load-balancer for re-balancing. + * Defaults to 307 (TEMPORARY_REDIRECT). + * + * @param code The code to use for the redirect + */ + public void setRedirectStatusCode(int code) { + _redirectStatusCode = code; + } + + /** + * Gets the name of the cookie that can be used to override the re-balancing behavior of this Valve when the current + * node is in the DISABLED activation state. + * + * @return The cookie name used to ignore normal processing rules. + * + * @see #setIgnoreCookieValue + */ + public String getIgnoreCookieName() { + return _ignoreCookieName; + } + + /** + * Sets the name of the cookie that can be used to override the re-balancing behavior of this Valve when the current + * node is in the DISABLED activation state. There is no default value for this setting: the ability to override the + * re-balancing behavior of this Valve is disabled by default. + * + * @param cookieName The cookie name to use to ignore normal processing rules. + * + * @see #getIgnoreCookieValue + */ + public void setIgnoreCookieName(String cookieName) { + _ignoreCookieName = cookieName; + } + + /** + * Gets the expected value of the cookie that can be used to override the re-balancing behavior of this Valve when + * the current node is in the DISABLED activation state. + * + * @return The cookie value used to ignore normal processing rules. + * + * @see #setIgnoreCookieValue + */ + public String getIgnoreCookieValue() { + return _ignoreCookieValue; + } + + /** + * Sets the expected value of the cookie that can be used to override the re-balancing behavior of this Valve when + * the current node is in the DISABLED activation state. The "ignore" cookie's value must be exactly equal to + * this value in order to allow the client to override the re-balancing behavior. + * + * @param cookieValue The cookie value to use to ignore normal processing rules. + * + * @see #getIgnoreCookieValue + */ + public void setIgnoreCookieValue(String cookieValue) { + _ignoreCookieValue = cookieValue; + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + if ("DIS".equals(request.getAttribute(ATTRIBUTE_KEY_JK_LB_ACTIVATION)) && + !request.isRequestedSessionIdValid()) { + + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("loadBalancerDrainingValve.draining")); + } + + boolean ignoreRebalance = false; + Cookie sessionCookie = null; + + final Cookie[] cookies = request.getCookies(); + + final String sessionCookieName = SessionConfig.getSessionCookieName(request.getContext()); + + if (null != cookies) { + for (Cookie cookie : cookies) { + final String cookieName = cookie.getName(); + if (containerLog.isTraceEnabled()) { + containerLog.trace("Checking cookie " + cookieName + "=" + cookie.getValue()); + } + + if (sessionCookieName.equals(cookieName) && + request.getRequestedSessionId().equals(cookie.getValue())) { + sessionCookie = cookie; + } else if (null != _ignoreCookieName && _ignoreCookieName.equals(cookieName) && + null != _ignoreCookieValue && _ignoreCookieValue.equals(cookie.getValue())) { + // The client presenting a valid ignore-cookie value? + ignoreRebalance = true; + } + } + } + + if (ignoreRebalance) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("loadBalancerDrainingValve.skip", _ignoreCookieName)); + } + + getNext().invoke(request, response); + + return; + } + + // Kill any session cookie that was found + // TODO: Consider implications of SSO cookies + if (null != sessionCookie) { + sessionCookie.setPath(SessionConfig.getSessionCookiePath(request.getContext())); + sessionCookie.setMaxAge(0); // Delete + sessionCookie.setValue(""); // Purge the cookie's value + // Replicate logic used to set secure attribute for session cookies + SessionCookieConfig sessionCookieConfig = request.getContext().getServletContext() + .getSessionCookieConfig(); + sessionCookie.setSecure(request.isSecure() || sessionCookieConfig.isSecure()); + response.addCookie(sessionCookie); + } + + // Re-write the URI if it contains a ;jsessionid parameter + String uri = request.getRequestURI(); + String sessionURIParamName = SessionConfig.getSessionUriParamName(request.getContext()); + if (uri.contains(";" + sessionURIParamName + "=")) { + uri = uri.replaceFirst(";" + sessionURIParamName + "=[^&?]*", ""); + } + + String queryString = request.getQueryString(); + + if (null != queryString) { + uri = uri + "?" + queryString; + } + + // NOTE: Do not call response.encodeRedirectURL or the bad + // sessionid will be restored + response.setHeader("Location", uri); + response.setStatus(_redirectStatusCode); + } else { + getNext().invoke(request, response); + } + } +} diff --git a/java/org/apache/catalina/valves/LocalStrings.properties b/java/org/apache/catalina/valves/LocalStrings.properties new file mode 100644 index 0000000..c91f3c7 --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings.properties @@ -0,0 +1,165 @@ +# 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. + +accessLogValve.alreadyExists=Failed to rename access log from [{0}] to [{1}], file already exists. +accessLogValve.closeFail=Failed to close access log file +accessLogValve.deleteFail=Failed to delete old access log [{0}] +accessLogValve.invalidLocale=Failed to set locale to [{0}] +accessLogValve.invalidPortType=Invalid port type [{0}], using server (local) port +accessLogValve.invalidRemoteAddressType=Invalid remote address type [{0}], using remote (non-peer) address +accessLogValve.openDirFail=Failed to create directory [{0}] for access logs +accessLogValve.openFail=Failed to open access log file [{0}] Note: running as user [{1}] +accessLogValve.renameFail=Failed to rename access log from [{0}] to [{1}] +accessLogValve.rotateFail=Failed to rotate access log +accessLogValve.unsupportedEncoding=Failed to set encoding to [{0}], will use the system default character set. +accessLogValve.writeFail=Failed to write log message [{0}] + +errorReportValve.contentTypeFail=Failure to set the content-type of response +errorReportValve.description=Description +errorReportValve.errorPageIOException=Unable to display error page at [{0}] due to an exception +errorReportValve.errorPageNotFound=Unable to find a static error page at [{0}] +errorReportValve.exception=Exception +errorReportValve.exceptionReport=Exception Report +errorReportValve.message=Message +errorReportValve.noDescription=No description available +errorReportValve.note=Note +errorReportValve.rootCause=Root Cause +errorReportValve.rootCauseInLogs=The full stack trace of the root cause is available in the server logs. +errorReportValve.statusHeader=HTTP Status {0} – {1} +errorReportValve.statusReport=Status Report +errorReportValve.type=Type +errorReportValve.unknownReason=Unknown Reason + +extendedAccessLogValve.badXParam=Invalid x parameter format, needs to be 'x-#(...) +extendedAccessLogValve.badXParamValue=Invalid x parameter value for Servlet request [{0}] +extendedAccessLogValve.decodeError=Unable to decode the rest of chars starting with [{0}] +extendedAccessLogValve.emptyPattern=Pattern was just empty or whitespace +extendedAccessLogValve.noClosing=No closing ) found for in decode +extendedAccessLogValve.patternParseError=Error parsing pattern [{0}] + +http.400.desc=The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). +http.400.reason=Bad Request +http.401.desc=The request has not been applied to the target resource because it lacks valid authentication credentials for that resource. +http.401.reason=Unauthorized +http.402.desc=This status code is reserved for future use. +http.402.reason=Payment Required +http.403.desc=The server understood the request but refuses to authorize it. +http.403.reason=Forbidden +http.404.desc=The origin server did not find a current representation for the target resource or is not willing to disclose that one exists. +http.404.reason=Not Found +http.405.desc=The method received in the request-line is known by the origin server but not supported by the target resource. +http.405.reason=Method Not Allowed +http.406.desc=The target resource does not have a current representation that would be acceptable to the user agent, according to the proactive negotiation header fields received in the request, and the server is unwilling to supply a default representation. +http.406.reason=Not Acceptable +http.407.desc=This status code is similar to 401 (Unauthorized), but it indicates that the client needs to authenticate itself in order to use a proxy. +http.407.reason=Proxy Authentication Required +http.408.desc=The server did not receive a complete request message within the time that it was prepared to wait. +http.408.reason=Request Timeout +http.409.desc=The request could not be completed due to a conflict with the current state of the target resource. +http.409.reason=Conflict +http.410.desc=Access to the target resource is no longer available at the origin server and that this condition is likely to be permanent. +http.410.reason=Gone +http.411.desc=The server refuses to accept the request without a defined Content-Length. +http.411.reason=Length Required +http.412.desc=One or more conditions given in the request header fields evaluated to false when tested on the server. +http.412.reason=Precondition Failed +http.413.desc=The server is refusing to process a request because the request payload is larger than the server is willing or able to process. +http.413.reason=Payload Too Large +http.414.desc=The server is refusing to service the request because the request-target is longer than the server is willing to interpret. +http.414.reason=URI Too Long +http.415.desc=The origin server is refusing to service the request because the payload is in a format not supported by this method on the target resource. +http.415.reason=Unsupported Media Type +http.416.desc=None of the ranges in the request's Range header field overlap the current extent of the selected resource or that the set of ranges requested has been rejected due to invalid ranges or an excessive request of small or overlapping ranges. +http.416.reason=Range Not Satisfiable +http.417.desc=The expectation given in the request's Expect header field could not be met by at least one of the inbound servers. +http.417.reason=Expectation Failed +http.421.desc=The request was directed at a server that is not able to produce a response. +http.421.reason=Misdirected Request +http.422.desc=The server understands the content type of the request entity, and the syntax of the request entity is correct but was unable to process the contained instructions. +http.422.reason=Unprocessable Entity +http.423.desc=The source or destination resource of a method is locked. +http.423.reason=Locked +http.424.desc=The method could not be performed on the resource because the requested action depended on another action and that action failed. +http.424.reason=Failed Dependency +http.426.desc=the server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol. +http.426.reason=Upgrade Required +http.428.desc=The origin server requires the request to be conditional. +http.428.reason=Precondition Required +http.429.desc=The user has sent too many requests in a given amount of time ("rate limiting"). +http.429.reason=Too Many Requests +http.431.desc=The server is unwilling to process the request because its header fields are too large. +http.431.reason=Request Header Fields Too Large +http.451.desc=The server refused this request for legal reasons. +http.451.reason=Unavailable For Legal Reasons +http.500.desc=The server encountered an unexpected condition that prevented it from fulfilling the request. +http.500.reason=Internal Server Error +http.501.desc=The server does not support the functionality required to fulfill the request. +http.501.reason=Not Implemented +http.502.desc=The server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request. +http.502.reason=Bad Gateway +http.503.desc=The server is currently unable to handle the request due to a temporary overload or scheduled maintenance, which will likely be alleviated after some delay. +http.503.reason=Service Unavailable +http.504.desc=The server, while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request. +http.504.reason=Gateway Timeout +http.505.desc=The server does not support, or refuses to support, the major version of HTTP that was used in the request message. +http.505.reason=HTTP Version Not Supported +http.506.desc=The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process. +http.506.reason=Variant Also Negotiates +http.507.desc=The method could not be performed on the resource because the server is unable to store the representation needed to successfully complete the request. +http.507.reason=Insufficient Storage +http.508.desc=The server terminated an operation because it encountered an infinite loop while processing a request with "Depth: infinity". +http.508.reason=Loop Detected +http.510.desc=The policy for accessing the resource has not been met in the request +http.510.reason=Not Extended +http.511.desc=The client needs to authenticate to gain network access. +http.511.reason=Network Authentication Required + +jdbcAccessLogValve.close=Failed to close database +jdbcAccessLogValve.exception=Exception performing insert access entry + +loadBalancerDrainingValve.draining=Load-balancer is in DISABLED state, draining this node +loadBalancerDrainingValve.skip=Client is presenting a valid [{0}] cookie, re-balancing is being skipped + +patternTokenizer.unexpectedParenthesis=Unexpected ')' in pattern + +persistentValve.acquireFailed=The request for [{0}] did not obtain the per session Semaphore as no permit was available +persistentValve.acquireInterrupted=The request for [{0}] did not obtain the per session Semaphore as it was interrupted while waiting for a permit +persistentValve.filter.failure=Unable to compile filter=[{0}] +persistentValve.requestIgnore=The request for [{0}] was ignored by this Valve as it matches the configured filter +persistentValve.requestProcess=The request for [{0}] will be processed by this Valve as it does not match the configured filter +persistentValve.sessionLoadFail=Loading session [{0}] from the store failed + +proxyErrorReportValve.error=Proxy error to [{0}] + +remoteCidrValve.invalid=Invalid configuration provided for [{0}]. See previous messages for details. +remoteCidrValve.noPort=Request does not contain a valid server port. Request denied. +remoteCidrValve.noRemoteIp=Client does not have an IP address. Request denied. +remoteCidrValve.unexpectedPort=Request contains server port, although connector configuration attribute addConnectorPort is false. Request denied. + +remoteIpValve.invalidHostHeader=Invalid value [{0}] found for Host in HTTP header [{1}] +remoteIpValve.invalidHostWithPort=Host value [{0}] in HTTP header [{1}] included a port number which will be ignored +remoteIpValve.invalidPortHeader=Invalid value [{0}] found for port in HTTP header [{1}] +remoteIpValve.invalidRemoteAddress=Unable to determine the remote host because the reported remote address [{0}] is not valid + +requestFilterValve.configInvalid=One or more invalid configuration settings were provided for the Remote[Addr|Host]Valve which prevented the Valve and its parent containers from starting +requestFilterValve.deny=Denied request for [{0}] based on property [{1}] + +sslValve.certError=Failed to process certificate string [{0}] to create a java.security.cert.X509Certificate object +sslValve.invalidProvider=The SSL provider specified on the connector associated with this request of [{0}] is invalid. The certificate data could not be processed. + +stuckThreadDetectionValve.interrupted=Thread interrupted after the request is finished, ignoring +stuckThreadDetectionValve.notifyStuckThreadCompleted=Thread [{0}] (id=[{3}]) was previously reported to be stuck but has completed. It was active for approximately [{1}] milliseconds.{2,choice,0#|0< There is/are still [{2}] thread(s) that are monitored by this Valve and may be stuck.} +stuckThreadDetectionValve.notifyStuckThreadDetected=Thread [{0}] (id=[{6}]) has been active for [{1}] milliseconds (since [{2}]) to serve the same request for [{4}] and may be stuck (configured threshold for this StuckThreadDetectionValve is [{5}] seconds). There is/are [{3}] thread(s) in total that are monitored by this Valve and may be stuck. +stuckThreadDetectionValve.notifyStuckThreadInterrupted=Thread [{0}] (id=[{5}]) has been interrupted because it was active for [{1}] milliseconds (since [{2}]) to serve the same request for [{3}] and was probably stuck (configured interruption threshold for this StuckThreadDetectionValve is [{4}] seconds). diff --git a/java/org/apache/catalina/valves/LocalStrings_cs.properties b/java/org/apache/catalina/valves/LocalStrings_cs.properties new file mode 100644 index 0000000..975b1af --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_cs.properties @@ -0,0 +1,37 @@ +# 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. + +accessLogValve.invalidLocale=Nastavení locale na [{0}] selhalo +accessLogValve.openFail=Chyba otevÅ™ení přístupového logovacího souboru [{0}] + +errorReportValve.description=Popis +errorReportValve.rootCauseInLogs=Celý stack trace zdrojové chyby je dostupný v server logu. + +http.401.reason=Neautorizováno +http.403.desc=Server rozumí dotazu, není vÅ¡ak autorizován. +http.404.reason=Nenalezeno +http.405.desc=Metoda pÅ™ijatá v dotazu je známá původnímu serveru, ale není podporována cílovým zdrojem. +http.407.reason=Vyžadována autentizace proxy +http.412.reason=PÅ™edpoklad selhal +http.415.reason=Nepodporovaný Media Type +http.421.desc=Dotaz byl pÅ™esmÄ›rován na server, který nemůže vytvoÅ™it odpovÄ›Ä. +http.423.desc=Zdrojový nebo cílový zdroj metody je zamknut. +http.423.reason=Zamknuto +http.426.reason=Vyžadován upgrade +http.428.desc=Původní server požaduje dotaz jako podmínÄ›ný. +http.429.reason=PříliÅ¡ mnoho požadavků +http.431.reason=Pole hlaviÄky dotazu jsou příliÅ¡ velká +http.502.desc=Server (v roli gateway nebo proxy) obdržel neplatnou odpovÄ›Ä od příchozího serveru, který kontaktoval pÅ™i pokusu zpracovat požadavek. +http.511.desc=Pro získání přístupu k síti se musí klient autentizovat. diff --git a/java/org/apache/catalina/valves/LocalStrings_de.properties b/java/org/apache/catalina/valves/LocalStrings_de.properties new file mode 100644 index 0000000..9bbddda --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_de.properties @@ -0,0 +1,48 @@ +# 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. + +accessLogValve.invalidLocale=Konnte Locale nicht auf [{0}] setzen +accessLogValve.openFail=Konnte Access Logfile [{0}] nicht öffnen +accessLogValve.rotateFail=Rotieren des Zugriffslogs ist fehlgeschlagen + +errorReportValve.description=Beschreibung +errorReportValve.note=Hinweis +errorReportValve.rootCauseInLogs=Der komplette Stacktrace der Ursache ist in den Server logs zu finden +errorReportValve.unknownReason=Unbekannter Grund + +http.401.reason=Unautorisiert +http.402.desc=Dieser Status-Code ist für eine zukünftige Nutzung reserviert +http.403.desc=Der Server hat die Anfrage verstanden, verbietet aber eine Autorisierung. +http.404.reason=nicht gefunden +http.406.reason=Nicht annehmbar +http.407.reason=Authentisierung für Proxy benötigt +http.411.reason=Länge benötigt +http.412.reason=Vorbedingung nicht erfüllt +http.415.reason=Nicht unterstützter Media-Type +http.421.desc=Die Anfrage wurde an einen Server gestellt der keine Antwort erzeugen konnte. +http.423.desc=Die Quell- oder Zielressource einer Methode ist gesperrt. +http.423.reason=Gesperrt +http.426.reason=Upgrade nötig +http.429.reason=Zu viele Anfragen +http.431.reason=Request-Header-Feld zu groß +http.504.reason=Gateway-Zeitüberschreitung +http.505.reason=HTTP Version nicht unterstützt +http.507.reason=Nicht genügend Speicherplatz +http.510.reason=Nicht erweitert +http.511.desc=Um Netzwerk Zugriff zu erlangen muss sich der Client authentifizieren. + +remoteCidrValve.noRemoteIp=Client verfügt über keine IP Adresse. Zugriff verweigert. + +remoteIpValve.invalidPortHeader=Ungültiger Wert [{0}] für Port im HTTP Header [{1}] gefunden diff --git a/java/org/apache/catalina/valves/LocalStrings_es.properties b/java/org/apache/catalina/valves/LocalStrings_es.properties new file mode 100644 index 0000000..93b988f --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_es.properties @@ -0,0 +1,85 @@ +# 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. + +accessLogValve.closeFail=No pude cerrar fichero de historial +accessLogValve.invalidLocale=Fallo al fijar locales a [{0}]\n +accessLogValve.openDirFail=No pude crear directorio [{0}] para historiales de acceso +accessLogValve.openFail=Fallo al abrir el archivo access log [{0}]\n +accessLogValve.rotateFail=No se pudo rotar el historial de acceso + +errorReportValve.description=Descripción +errorReportValve.exception=excepción +errorReportValve.exceptionReport=Informe de Excepción +errorReportValve.message=mensaje +errorReportValve.note=nota +errorReportValve.rootCause=causa raíz +errorReportValve.rootCauseInLogs=La traza completa de la causa de este error se encuentra en los archivos de registro del servidor. +errorReportValve.statusHeader=Estado HTTP {0} – {1} +errorReportValve.statusReport=Informe de estado +errorReportValve.type=Tipo + +http.400.desc=El requerimiento enviado por el cliente era sintácticamente incorrecto. +http.401.desc=Este requerimiento requiere autenticación HTTP. +http.401.reason=No autorizado +http.402.desc=Este código de estado está reservado para uso futuro. +http.403.desc=El acceso al recurso especificado ha sido prohibido. +http.404.desc=El recurso requerido no está disponible. +http.404.reason=No encontrado +http.405.desc=El método HTTP especificado no está permitido para el recurso requerido. +http.406.desc=El recurso identificado por este requerimiento sólo es capaz de generar respuestas con características no aceptables con arreglo a las cabeceras "accept" de requerimiento. +http.407.desc=El cliente debe de ser primero autenticado en el apoderado. +http.407.reason=Se requiere autenticación de proxy +http.408.desc=El cliente no produjo un requerimiento dentro del tiempo en que el servidor estaba preparado esperando. +http.409.desc=El requerimiento no pudo ser completado debido a un conflicto con el estado actual del recurso. +http.410.desc=El recurso requerido ya no está disponible y no se conoce dirección de reenvío. +http.411.desc=Este requerimiento no puede ser manejado sin un tamaño definido de contenido. +http.412.desc=Una precondición especificada ha fallado para este requerimiento. +http.412.reason=La precon +http.413.desc=La entidad de requerimiento es mayor de lo que el servidor quiere o puede procesar. +http.414.desc=El servidor rechazó este requerimiento porque la URI requerida era demasiado larga. +http.415.desc=El servidor rechazó este requerimiento porque la entidad requerida se encuentra en un formato no soportado por el recurso requerido para el método requerido. +http.415.reason=Tipo de medio no soportado +http.416.desc=El rango de byte requerido no puede ser satisfecho. +http.417.desc=Lo que se espera dado por la cabecera "Expect" de requerimiento no pudo ser completado. +http.421.desc=La solicitud ha sido dirigida a un servidor que no fue capaz de producir una respuesta +http.422.desc=El servidor entendió el tipo de contenido y la sintáxis del requerimiento pero no pudo procesar las instrucciones contenidas. +http.423.desc=La fuente o recurso de destino de un método está bloqueada. +http.423.reason=Bloqueado +http.426.reason=Se requiere actualización +http.428.desc=El servidor de origen requiere que la petición sea condicional +http.429.reason=Demasiadas peticiones +http.431.reason=Los campos de cabecera solicitados son muy largos +http.500.desc=El servidor encontró un error interno que hizo que no pudiera rellenar este requerimiento. +http.501.desc=El servidor no soporta la funcionalidad necesaria para rellenar este requerimiento. +http.502.desc=Este servidor recibió una respuesta inválida desde un servidor que consultó cuando actuaba como apoderado o pasarela. +http.503.desc=El servicio requerido no está disponible en este momento. +http.504.desc=El servidor recibió un Tiempo Agotado desde un servidor superior cuando actuaba como pasarela o apoderado. +http.505.desc=El servidor no soporta la versión de protocolo HTTP requerida. +http.505.reason=Versión HTTP no soportada +http.507.desc=El recurso no tiene espacio suficiente para registrar el estado del recurso tras la ejecución de este método. +http.507.reason=El storage no es suficiente +http.511.desc=El cliente se tiene que autenticar para tener accesso a la red + +jdbcAccessLogValve.exception=Excepción realizando entrada de acceso a inserción + +remoteIpValve.invalidPortHeader=Valor inválido [{0}] hallado para el puerto en cabecera HTTP [{1}] + +requestFilterValve.configInvalid=Uno o más parámetros de configuración inválidos fueron proveídos para Remote[Addr|Host]Valve lo cual impide que el Valve y sus contenedores padres puedan iniciar + +sslValve.certError=No pude procesar cadena de certificado [{0}] para crear un objeto java.security.cert.X509Certificate +sslValve.invalidProvider=El proveedor de SSL especificado en el conecto asociado con este requerimiento de [{0}] ies inválido. No se pueden procesar los datos del certificado. + +stuckThreadDetectionValve.notifyStuckThreadCompleted=El hilo [{0}] (id=[{3}]), que previamente se reportó como atascado, se ha completado. Estuvo activo por aproximadamente [{1}] milisegundos. {2, choice,0#|0< Hay aún [{2}] hilo(s) que son monitorizados por esta Válvula y pueden estar atascados.} +stuckThreadDetectionValve.notifyStuckThreadDetected=El hilo [{0}] (id=[{6}]) ha estado activo durante [{1}] miilisegundos (desde [{2}]) para servir el mismo requerimiento para [{4}] y puede estar atascado (el umbral configurado para este StuckThreadDetectionValve es de [{5}] segundos). Hay [{3}] hilo(s) en total que son monitorizados por esta Válvula y pueden estar atascados. diff --git a/java/org/apache/catalina/valves/LocalStrings_fr.properties b/java/org/apache/catalina/valves/LocalStrings_fr.properties new file mode 100644 index 0000000..13a874c --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_fr.properties @@ -0,0 +1,165 @@ +# 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. + +accessLogValve.alreadyExists=Échec de renommage du journal d''accès de [{0}] en [{1}], le fichier existe déjà. +accessLogValve.closeFail=Échec de fermeture du fichier de journal d'accès +accessLogValve.deleteFail=Impossible d''effacer l''ancien journal d''accès [{0}] +accessLogValve.invalidLocale=Impossible de définir les paramètres régionaux sur [{0}] +accessLogValve.invalidPortType=Type de port [{0}] invalide, utilisation du port (local) de serveur +accessLogValve.invalidRemoteAddressType=Le type [{0}] d''adresse distante est invalide, l''adresse distante classique sera utilisée et non celle du pair +accessLogValve.openDirFail=Echec de création du répertoire [{0}] pour les journaux d''accès +accessLogValve.openFail=Echec à l''ouverture du journal d''accès [{0}] +accessLogValve.renameFail=Échec de renommage du journal d''accès de [{0}] en [{1}] +accessLogValve.rotateFail=Échec de rotation des journaux d'accès +accessLogValve.unsupportedEncoding=Impossible de changer l''encodage en [{0}], le jeu de caractères par défaut du système sera utilisé +accessLogValve.writeFail=Impossible d''écrire le message de log [{0}] + +errorReportValve.contentTypeFail=Echec pour définir le content-type de la réponse +errorReportValve.description=description +errorReportValve.errorPageIOException=Impossible d''afficher la page d''erreur à [{0}] à cause d''une exception +errorReportValve.errorPageNotFound=Impossible de trouver une erreur page statique à [{0}] +errorReportValve.exception=exception +errorReportValve.exceptionReport=Rapport d'exception +errorReportValve.message=message +errorReportValve.noDescription=Pas de description disponible +errorReportValve.note=note +errorReportValve.rootCause=cause mère +errorReportValve.rootCauseInLogs=La trace complète de la cause mère de cette erreur est disponible dans les fichiers journaux de ce serveur. +errorReportValve.statusHeader=État HTTP {0} – {1} +errorReportValve.statusReport=Rapport d'état +errorReportValve.type=Type +errorReportValve.unknownReason=Raison inconnue. + +extendedAccessLogValve.badXParam=Le format du paramètre étendu est invalide, il doit être de la forme 'x-#(...)' +extendedAccessLogValve.badXParamValue=La valeur du paramètre étendu est invalide pour la requête de Servlet [{0}] +extendedAccessLogValve.decodeError=Impossible de décoder les caractères restants à partir de [{0}] +extendedAccessLogValve.emptyPattern=Le modèle est vide +extendedAccessLogValve.noClosing=Une parenthèse de fermeture n'a pas été trouvée lors du décodage +extendedAccessLogValve.patternParseError=Erreur lors de l''analyse du modèle [{0}] + +http.400.desc=La requête envoyée par le client était syntaxiquement incorrecte. +http.400.reason=Requête invalide +http.401.desc=La requête n'a pas été appliquée car elle n'a pas de crédits d'authentication valides pour la resource cible +http.401.reason=Non autorisé +http.402.desc=Un paiement est demandé pour accéder à cette ressource. +http.402.reason=Paiement requis +http.403.desc=L'accès à la ressource demandée a été interdit. +http.403.reason=Interdit +http.404.desc=La ressource demandée n'est pas disponible. +http.404.reason=Non trouvé +http.405.desc=La méthode HTTP spécifiée n'est pas autorisée pour la ressource demandée. +http.405.reason=Méthode non autorisée +http.406.desc=La ressource identifiée par cette requête n'est capable de générer des réponses qu'avec des caractéristiques incompatible avec la directive "accept" présente dans l'entête de requête. +http.406.reason=Inacceptable +http.407.desc=Le client doit d'abord s'authentifier auprès du relais. +http.407.reason=Authentification Proxy est requise +http.408.desc=Le client n'a pas produit de requête pendant le temps d'attente du serveur. +http.408.reason=Timeout de la requête +http.409.desc=La requête ne peut être finalisée suite à un conflit lié à l'état de la ressource. +http.409.reason=Conflit +http.410.desc=La ressource demandée n'est pas disponible, et aucune adresse de rebond (forwarding) n'est connue. +http.410.reason=Disparu +http.411.desc=La requête ne peut être traitée sans définition d'une taille de contenu (Content-Length). +http.411.reason=Une longueur est requise +http.412.desc=Une condition préalable demandée n'est pas satisfaite pour cette requête. +http.412.reason=Erreur dans la pré-condition +http.413.desc=L'entité de requête est plus importante que ce que le serveur veut ou peut traiter. +http.413.reason=Les données sont trop grandes +http.414.desc=Le serveur a refusé cette requête car l'URI de requête est trop longue. +http.414.reason=L'URI est trop longue +http.415.desc=Le serveur a refusé cette requête car l'entité de requête est dans un format non supporté par la ressource demandée avec la méthode spécifiée. +http.415.reason=Type de média non supporté +http.416.desc=La plage d'octets demandée (byte range) ne peut être satisfaite. +http.416.reason=Plage non réalisable +http.417.desc=L'attente indiquée dans la directive "Expect" de l'entête de requête ne peut être satisfaite. +http.417.reason=L'expectation a échouée +http.421.desc=La requête a été dirigée vers un serveur qui est incapable de produire une réponse. +http.421.reason=Requête mal dirigée +http.422.desc=Le serveur a compris le type de contenu (content type) ainsi que la syntaxe de la requête mais a été incapable de traiter les instructions contenues. +http.422.reason=Impossible de traiter cette entité +http.423.desc=La ressource source ou destination de la méthode est verrouillée. +http.423.reason=Verrouillé +http.424.desc=La méthode n'a pas pu être exécutée sur la ressource parce qu'elle dépendait d'une autre action qui a échoué. +http.424.reason=Echec de dépendence +http.426.desc=Le serveur a refusé de traiter cette requête en utilisant le protocole actuel mais pourrait le faire si le client en utilise un autre +http.426.reason=Mise à jour du protocole requise +http.428.desc=Le serveur d'origine exige que la requête soit conditionnelle +http.428.reason=Précondition requise +http.429.desc=L'utilisateur a effectué une nombre de requêtes trop élevé dans un laps de temps trop court (limitation de fréquence) +http.429.reason=Trop de requêtes +http.431.desc=Le serveur refuse de traiter la requête parce que ses champs d'en-tête sont trop gros +http.431.reason=Les champs d'en-tête de la requête sont trop gros +http.451.desc=Le serveur a refusé cette requête pour des raisons légales +http.451.reason=Indisponible pour des raisons légales +http.500.desc=Le serveur a rencontré une erreur interne qui l'a empêché de satisfaire la requête. +http.500.reason=Erreur interne du serveur +http.501.desc=Le serveur ne supporte pas la fonctionnalité demandée pour satisfaire cette requête. +http.501.reason=Non implémentée +http.502.desc=Le serveur a reçu une réponse invalide d'un serveur qu'il consultait en tant que relais ou passerelle. +http.502.reason=Mauvaise passerelle +http.503.desc=Le service demandé n'est pas disponible actuellement. +http.503.reason=Service indisponible +http.504.desc=Le serveur a reçu un dépassement de délai (timeout) d'un serveur amont qu'il consultait en tant que relais ou passerelle. +http.504.reason=Timeout de la passerelle +http.505.desc=Le serveur ne supporte pas la version demandée du protocole HTTP. +http.505.reason=Version HTTP non supportée +http.506.desc=Le serveur a rencontré une erreur de configuration interne : la variante choisie de la ressource est configurée pour mener elle-même la négociation de contenu de manière transparente, et n'est donc pas le bon endroit pour la négociation elle-même +http.506.reason=506 Variant Also Negotiates (RFC 2295) (référence circulaire) +http.507.desc=L'espace disponible est insuffisant pour enregistrer l'état de la ressource après exécution de cette méthode. +http.507.reason=Stockage insuffisant +http.508.desc=Le serveur a mis fin à une opération car il a rencontré une boucle infinie en traitant une requête avec "Depth : infinity" +http.508.reason=Boucle détectée +http.510.desc=La requête ne correspond pas à la politique d'accès pour cette ressource +http.510.reason=Non étendu +http.511.desc=Le client doit s'authentifier pour accéder au réseau. +http.511.reason=L’authentification du réseau est nécessaire + +jdbcAccessLogValve.close=Echec de fermeture de la base de donnée +jdbcAccessLogValve.exception=Exception en insérant l'entrée de l'accès + +loadBalancerDrainingValve.draining=Le balanceur de charge est dans l'étât DISABLED, le changement de répartition de la charge est sauté +loadBalancerDrainingValve.skip=Le client a envoyé un cookie [{0}] valide, le le changement de répartition de la charge est sauté + +patternTokenizer.unexpectedParenthesis=')' inattendu dans le modèle + +persistentValve.acquireFailed=La requête pour [{0}] n''a pas obtenu le sémaphore associé à la session car aucun permis n''était disponible +persistentValve.acquireInterrupted=La requête pour [{0}] n''a pas obtenu le sémaphore associé à la session car elle a été interrompue au cours de l''attente d''un permis +persistentValve.filter.failure=Impossible de compiler le filtre=[{0}] +persistentValve.requestIgnore=La requête pour [{0}] a été ignorée par cette Valve car elle correspond au filtre configuré +persistentValve.requestProcess=La requête pour [{0}] va être traitée par cette Valve car elle ne correspond pas au filtre configuré +persistentValve.sessionLoadFail=Le chargement de la session [{0}] depuis le stockage a échoué + +proxyErrorReportValve.error=Erreur de proxy vers [{0}] + +remoteCidrValve.invalid=La configuration fournie pour [{0}] est invalide, voir les précédents messages pour plus de détails +remoteCidrValve.noPort=La requête est rejetée car elle ne contient pas un port du serveur valide +remoteCidrValve.noRemoteIp=Le client n'a pas d'adresse IP, requête interdite +remoteCidrValve.unexpectedPort=La requête est rejetée car elle contient un port du serveur, alors que la configuration du connecteur addConnectorPort est false + +remoteIpValve.invalidHostHeader=La valeur invalide [{0}] a été trouvée pour le Host dans l''en-tête HTTP [{1}] +remoteIpValve.invalidHostWithPort=La valeur de Host [{0}] dans l''en-tête HTTP [{1}] contenait un numéro de port qui sera ingnoré +remoteIpValve.invalidPortHeader=La valeur de port [{0}] trouvée dans l''en-tête HTTP [{1}] est invalide +remoteIpValve.invalidRemoteAddress=Impossible de déterminer l''hôte distant car l''adresse distante [{0}] est invalide + +requestFilterValve.configInvalid=Un ou plusieurs paramètres de configuration spécifiés pour ce Remote[Addr|Host]Valve ont empêché la Valve et le conteneur parent de démarrer +requestFilterValve.deny=Refus de la requête pour [{0}] basé sur la propriété [{1}] + +sslValve.certError=Impossible de traiter le certificat [{0}] pour créer un objet java.security.cert.X509Certificate +sslValve.invalidProvider=Le fournisseur SSL spécifié pour le connecteur associé avec cette requête de [{0}] est invalide, le certificat n''a pas pu être traité + +stuckThreadDetectionValve.interrupted=Le fil d'exécution a été interrompu après la fin de la requête, cela sera ignoré +stuckThreadDetectionValve.notifyStuckThreadCompleted=Le Thread [{0}] (id=[{3}]) qui a été préalablement rapporté comme étant bloqué s''est terminé, il a été actif pendant approximativement [{1}] millisecondes, il y a [{2}] thread(s) au total qui sont surveillés par cette valve et qui pourraient être bloqués +stuckThreadDetectionValve.notifyStuckThreadDetected=Le Thread [{0}] (id=[{6}]) a été actif depuis [{1}] millisecondes (depuis [{2}]) pour traiter la même requête pour [{4}] et pourrait être bloqué (le seuil configurable est de [{5}] secondes pour cette StuckThreadDetectionValve), il y a [{3}] thread(s) au total qui sont surveillés par cette valve et qui pourraient être bloqués +stuckThreadDetectionValve.notifyStuckThreadInterrupted=Le Thread [{0}] (id=[{5}]) a été interrompu car il a été actif depuis [{1}] millisecondes (depuis [{2}]) pour traiter la même requête pour [{3}] et était probablement bloqué (le seuil configurable est de [{4}] secondes pour cette StuckThreadDetectionValve) diff --git a/java/org/apache/catalina/valves/LocalStrings_ja.properties b/java/org/apache/catalina/valves/LocalStrings_ja.properties new file mode 100644 index 0000000..77d3cbd --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_ja.properties @@ -0,0 +1,166 @@ +# 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. + +accessLogValve.alreadyExists=[{0}]ã‹ã‚‰[{1}]ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒ­ã‚°ã®åå‰ã®å¤‰æ›´ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ファイルã¯ã™ã§ã«å­˜åœ¨ã—ã¦ã„ã¾ã™ã€‚ +accessLogValve.closeFail=アクセスログã®ã‚¯ãƒ­ãƒ¼ã‚ºã«å¤±æ•—ã—ã¾ã—㟠+accessLogValve.deleteFail=å¤ã„アクセスログ [{0}] を削除ã§ãã¾ã›ã‚“ã§ã—㟠+accessLogValve.invalidLocale=[{0}] をロケールã«è¨­å®šã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +accessLogValve.invalidPortType=ä¸æ­£ãªãƒãƒ¼ãƒˆç¨®é¡ž [{0}] ã®ä»£ã‚ã‚Šã«ã‚µãƒ¼ãƒãƒ¼ã®ãƒ­ãƒ¼ã‚«ãƒ«ãƒãƒ¼ãƒˆã‚’使用ã—ã¾ã™ã€‚ +accessLogValve.invalidRemoteAddressType=リモート (éžãƒ”ã‚¢) アドレスを使用ã—ã¦ã„る無効ãªãƒªãƒ¢ãƒ¼ãƒˆã‚¢ãƒ‰ãƒ¬ã‚¹ã‚¿ã‚¤ãƒ— [{0}] +accessLogValve.openDirFail=アクセスログã®ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª[{0}]ã®ä½œæˆã«å¤±æ•—ã—ã¾ã—㟠+accessLogValve.openFail=アクセスログファイル [{0}] ã‚’é–‹ã‘ã¾ã›ã‚“。 +accessLogValve.renameFail=[{0}]ã‹ã‚‰[{1}]ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒ­ã‚°ã®åå‰ã®å¤‰æ›´ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +accessLogValve.rotateFail=アクセスログã®ãƒ­ãƒ¼ãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã«å¤±æ•—ã—ã¾ã—㟠+accessLogValve.unsupportedEncoding=文字エンコーディング㫠[{0}] を指定ã§ãã¾ã›ã‚“。システムã®æ—¢å®šå€¤ã‚’使用ã—ã¾ã™ã€‚ +accessLogValve.writeFail=ログメッセージ [{0}] ã®æ›¸ãè¾¼ã¿ã«å¤±æ•—ã—ã¾ã—㟠+ +errorReportValve.contentTypeFail=レスãƒãƒ³ã‚¹ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„タイプ設定ã«å¤±æ•—ã—ã¾ã—㟠+errorReportValve.description=説明 +errorReportValve.errorPageIOException=例外ã®ãŸã‚ [{0}] ã«ã‚¨ãƒ©ãƒ¼ãƒšãƒ¼ã‚¸ã‚’表示ã§ãã¾ã›ã‚“ +errorReportValve.errorPageNotFound=[{0}]ã«é™çš„エラーページãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +errorReportValve.exception=例外 +errorReportValve.exceptionReport=例外報告 +errorReportValve.message=メッセージ +errorReportValve.noDescription=説明ã¯ã‚ã‚Šã¾ã›ã‚“ +errorReportValve.note=æ³¨æ„ +errorReportValve.rootCause=根本原因 +errorReportValve.rootCauseInLogs=原因ã®ã™ã¹ã¦ã®ã‚¹ã‚¿ãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹ã¯ã€ã‚µãƒ¼ãƒã®ãƒ­ã‚°ã«è¨˜éŒ²ã•ã‚Œã¦ã„ã¾ã™ã€‚ +errorReportValve.statusHeader=HTTPステータス {0} – {1} +errorReportValve.statusReport=ステータスレãƒãƒ¼ãƒˆ +errorReportValve.type=タイプ +errorReportValve.unknownReason=未知ã®ç†ç”± + +extendedAccessLogValve.badXParam=無効ãªxパラメータフォーマットã§ã™ã€‚ 'x-#(...)ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +extendedAccessLogValve.badXParamValue=サーブレットリクエスト [{0}] ã®ç„¡åŠ¹ãªxパラメータ値 +extendedAccessLogValve.decodeError=[{0}]ã§å§‹ã¾ã‚‹æ®‹ã‚Šã®æ–‡å­—をデコードã§ãã¾ã›ã‚“ +extendedAccessLogValve.emptyPattern=パターン文字列ãŒç©ºã§ã™ã€‚ã‚‚ã—ãã¯ç©ºç™½ã ã‘ã§æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ã€‚ +extendedAccessLogValve.noClosing=終了)ãŒãƒ‡ã‚³ãƒ¼ãƒ‰ã§è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +extendedAccessLogValve.patternParseError=パターン文字列 [{0}] を解æžã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ + +http.400.desc=サーãƒã¯ã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã‚¨ãƒ©ãƒ¼ï¼ˆä¾‹ãˆã°ã€ç„¡åŠ¹ãªãƒªã‚¯ã‚¨ã‚¹ãƒˆæ§‹æ–‡ã€ç„¡åŠ¹ãªãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ•ãƒ¬ãƒ¼ãƒŸãƒ³ã‚°ã€ã¾ãŸã¯ç„¡åŠ¹ãªãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ«ãƒ¼ãƒ†ã‚£ãƒ³ã‚°ï¼‰ã§ã‚ã‚‹ã¨è€ƒãˆã‚‰ã‚Œã‚‹ãŸã‚ã«ã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã§ããªã„ã€ã¾ãŸã¯å‡¦ç†ã—ã¾ã›ã‚“。 +http.400.reason=Bad Request +http.401.desc=リクエストã«ã¯å¯¾è±¡ãƒªã‚½ãƒ¼ã‚¹ã®æœ‰åŠ¹ãªèªè¨¼è³‡æ ¼ãŒãªã„ãŸã‚ã€é©ç”¨ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +http.401.reason=Unauthorized +http.402.desc=ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã‚³ãƒ¼ãƒ‰ã¯ã€å°†æ¥ã®ä½¿ç”¨ã®ãŸã‚ã«äºˆç´„ã•ã‚Œã¦ã„ã¾ã™ +http.402.reason=Payment Required +http.403.desc=サーãƒãƒ¼ã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®èªè¨¼ã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚ +http.403.reason=Forbidden +http.404.desc=オリジンサーãƒãƒ¼ã¯ã€ã‚¿ãƒ¼ã‚²ãƒƒãƒˆãƒªã‚½ãƒ¼ã‚¹ã®ç¾åœ¨ã®è¡¨ç¾ã‚’見ã¤ã‘られãªã‹ã£ãŸã‹ã€ã¾ãŸã¯ãã‚ŒãŒå­˜åœ¨ã™ã‚‹ã“ã¨ã‚’開示ã™ã‚‹ã¤ã‚‚ã‚Šã¯ã‚ã‚Šã¾ã›ã‚“。 +http.404.reason=見ã¤ã‹ã‚Šã¾ã›ã‚“ +http.405.desc=リクエストラインã§å—ä¿¡ã•ã‚ŒãŸãƒ¡ã‚½ãƒƒãƒ‰ã¯ã€ã‚ªãƒªã‚¸ãƒ³ã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦èªè­˜ã•ã‚Œã¾ã™ãŒã€ã‚¿ãƒ¼ã‚²ãƒƒãƒˆãƒªã‚½ãƒ¼ã‚¹ã«ã‚ˆã£ã¦ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +http.405.reason=Method Not Allowed +http.406.desc=ターゲット リソースã¯ã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆã§å—ã‘å–ã£ãŸãƒ—ロアクティブãªãƒã‚´ã‚·ã‚¨ãƒ¼ã‚·ãƒ§ãƒ³ ヘッダ フィールドã«å¾“ã£ã¦ã€ãƒ¦ãƒ¼ã‚¶ エージェントãŒå—ã‘入れられるç¾åœ¨ã®è¡¨ç¾ã‚’æŒãŸãšã€ã‚µãƒ¼ãƒã¯æ—¢å®šã®è¡¨ç¾ã‚’æä¾›ã™ã‚‹ã“ã¨ã‚’望ã¾ã—ãã‚ã‚Šã¾ã›ã‚“。 +http.406.reason=Not Acceptable +http.407.desc=ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã‚³ãƒ¼ãƒ‰ã¯401(Unauthorized)ã«ä¼¼ã¦ã„ã¾ã™ãŒã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆãŒãƒ—ロキシを使用ã™ã‚‹ãŸã‚ã«è‡ªèº«ã‚’èªè¨¼ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ã“ã¨ã‚’示ã—ã¾ã™ã€‚ +http.407.reason=プロキシèªè¨¼ãŒå¿…è¦ã§ã™ +http.408.desc=サーãƒãƒ¼ã¯ã€å¾…機用ã«æº–å‚™ã•ã‚ŒãŸæ™‚間内ã«å®Œå…¨ãªãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ä¿¡ã—ã¾ã›ã‚“ã§ã—ãŸã€‚ +http.408.reason=リクエストタイムアウト +http.409.desc=ターゲットリソースã®ç¾åœ¨ã®çŠ¶æ…‹ã¨ã®ç«¶åˆã®ãŸã‚ã«ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’完了ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +http.409.reason=Conflict +http.410.desc=オリジンサーãƒãƒ¼ã§ã‚¿ãƒ¼ã‚²ãƒƒãƒˆãƒªã‚½ãƒ¼ã‚¹ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãŒåˆ©ç”¨ã§ããªããªã‚Šã€ã“ã®çŠ¶æ…‹ãŒæ°¸ç¶šçš„ã«ãªã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +http.410.reason=Gone +http.411.desc=サーãƒãƒ¼ã¯ã€å®šç¾©ã•ã‚ŒãŸContent-Lengthãªã—ã§ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’å—ã‘入れるã“ã¨ã‚’æ‹’å¦ã—ã¾ã™ã€‚ +http.411.reason=Length Required +http.412.desc=リクエストヘッダーフィールドã«æŒ‡å®šã•ã‚ŒãŸ1ã¤ä»¥ä¸Šã®æ¡ä»¶ãŒã€ã‚µãƒ¼ãƒãƒ¼ä¸Šã§ãƒ†ã‚¹ãƒˆã•ã‚ŒãŸã¨ãã«falseã«è©•ä¾¡ã•ã‚Œã¾ã—ãŸã€‚ +http.412.reason=å‰ææ¡ä»¶å¤±æ•— +http.413.desc=リクエストペイロードãŒã‚µãƒ¼ãƒãƒ¼ãŒå‡¦ç†ã§ãã‚‹ã€ã¾ãŸã¯å‡¦ç†ã§ãるよりも大ãã„ãŸã‚ã€ã‚µãƒ¼ãƒãƒ¼ã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å‡¦ç†ã‚’æ‹’å¦ã—ã¦ã„ã¾ã™ã€‚ +http.413.reason=Payload Too Large +http.414.desc=リクエストã®å¯¾è±¡ãŒã‚µãƒ¼ãƒãƒ¼ãŒè§£é‡ˆã—よã†ã¨ã™ã‚‹ã‚ˆã‚Šã‚‚é•·ã„ãŸã‚ã€ã‚µãƒ¼ãƒãƒ¼ã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®ã‚µãƒ¼ãƒ“スを拒å¦ã—ã¦ã„ã¾ã™ã€‚ +http.414.reason=URI Too Long +http.415.desc=ペイロードãŒã‚¿ãƒ¼ã‚²ãƒƒãƒˆãƒªã‚½ãƒ¼ã‚¹ä¸Šã®ã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ã§ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„å½¢å¼ã§ã‚ã‚‹ãŸã‚ã€ã‚ªãƒªã‚¸ãƒ³ã‚µãƒ¼ãƒãƒ¼ã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã™ã‚‹ã“ã¨ã‚’æ‹’å¦ã—ã¦ã„ã¾ã™ã€‚ +http.415.reason=未対応ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚¿ã‚¤ãƒ—ã§ã™ +http.416.desc=リクエストã®Rangeヘッダーフィールドã®ç¯„囲ã®ã„ãšã‚Œã‚‚ã€é¸æŠžã•ã‚ŒãŸãƒªã‚½ãƒ¼ã‚¹ã®ç¾åœ¨ã®ã‚¨ã‚¯ã‚¹ãƒ†ãƒ³ãƒˆã¨é‡è¤‡ã—ãªã„ã‹ã€ç„¡åŠ¹ãªç¯„囲ã¾ãŸã¯å°ã•ã™ãŽã‚‹ç¯„囲ã¾ãŸã¯é‡è¤‡ã™ã‚‹ç¯„囲ã®éŽå‰°ãªãƒªã‚¯ã‚¨ã‚¹ãƒˆã®ãŸã‚ã«ãƒªã‚¯ã‚¨ã‚¹ãƒˆã•ã‚ŒãŸç¯„囲ã®é›†åˆãŒæ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ +http.416.reason=Range Not Satisfiable +http.417.desc=リクエストã®Expectヘッダーフィールドã§æŒ‡å®šã•ã‚ŒãŸæœŸå¾…値ãŒã€å°‘ãªãã¨ã‚‚1ã¤ã®ã‚¤ãƒ³ãƒã‚¦ãƒ³ãƒ‰ã‚µãƒ¼ãƒãƒ¼ã§æº€ãŸã•ã‚Œã¦ã„ãªã„å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +http.417.reason=Expectation Failed +http.421.desc=リクエストã¯ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚’生æˆã§ããªã„サーãƒãƒ¼ã«å‘ã‘られã¾ã—ãŸã€‚ +http.421.reason=Misdirected Request +http.422.desc=サーãƒãƒ¼ã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„タイプをç†è§£ã—ã¦ãŠã‚Šã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ã®æ§‹æ–‡ã¯æ­£ã—ã„ã‚‚ã®ã®ã€å«ã¾ã‚Œã¦ã„る命令を処ç†ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +http.422.reason=Unprocessable Entity +http.423.desc=メソッド呼ã³å‡ºã—ã®ä¾é ¼å…ƒãƒªã‚½ãƒ¼ã‚¹ã€ã‚ã‚‹ã„ã¯ä¾é ¼å…ˆãƒªã‚½ãƒ¼ã‚¹ã¯ãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã¾ã™ã€‚ +http.423.reason=ロックã•ã‚Œã¦ã„ã¾ã™ +http.424.desc=è¦æ±‚ã•ã‚ŒãŸã‚¢ã‚¯ã‚·ãƒ§ãƒ³ãŒåˆ¥ã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«ä¾å­˜ã—ã€ãã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ãŒå¤±æ•—ã—ãŸãŸã‚ã€ã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ã¯ãƒªã‚½ãƒ¼ã‚¹ä¸Šã§å®Ÿè¡Œã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +http.424.reason=Failed Dependency +http.426.desc=サーãƒãƒ¼ã¯ç¾åœ¨ã®ãƒ—ロトコルを使用ã—ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’実行ã™ã‚‹ã“ã¨ã‚’æ‹’å¦ã—ã¾ã™ãŒã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆãŒåˆ¥ã®ãƒ—ロトコルã«ã‚¢ãƒƒãƒ—グレードã—ãŸå¾Œã«ãã®è¦æ±‚を実行ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +http.426.reason=アップグレードãŒå¿…è¦ã§ã™ +http.428.desc=オリジンサーãƒãƒ¼ã¯ã€è¦æ±‚ãŒæ¡ä»¶ä»˜ãã§ã‚ã‚‹ã“ã¨ã‚’è¦æ±‚ã—ã¾ã™ã€‚ +http.428.reason=Precondition Required +http.429.desc=ユーザーãŒæŒ‡å®šã—ãŸæ™‚間内ã«å¤šãã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ä¿¡ã—ã¾ã—ãŸï¼ˆãƒ¬ãƒ¼ãƒˆåˆ¶é™ï¼‰ã€‚ +http.429.reason=大é‡ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒç™ºç”Ÿã—ã¦ã„ã¾ã™ +http.431.desc=ヘッダーフィールドãŒå¤§ãã™ãŽã‚‹ãŸã‚ã€ã‚µãƒ¼ãƒãƒ¼ã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ã™ã™ã‚“ã§å‡¦ç†ã—ã¾ã›ã‚“。 +http.431.reason=リクエストヘッダフィールドãŒå¤§ãéŽãŽã¾ã™ +http.451.desc=サーãƒãƒ¼ã¯ã“ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’法的ç†ç”±ã§æ‹’å¦ã—ã¾ã—ãŸã€‚ +http.451.reason=Unavailable For Legal Reasons +http.500.desc=サーãƒãƒ¼ã¯äºˆæœŸã—ãªã„æ¡ä»¶ã«é­é‡ã—ã¾ã—ãŸã€‚ãã‚Œã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å®Ÿè¡Œã‚’妨ã’ã¾ã™ã€‚ +http.500.reason=Internal Server Error +http.501.desc=サーãƒãƒ¼ã¯ã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’実行ã™ã‚‹ãŸã‚ã«å¿…è¦ãªæ©Ÿèƒ½ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 +http.501.reason=Not Implemented +http.502.desc=ゲートウェイã‚ã‚‹ã„ã¯ãƒ—ロキシサーãƒãƒ¼ã‹ã‚‰ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’試ã¿ãŸå†…部サーãƒãƒ¼ã‹ã‚‰ç„¡åŠ¹ãªãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚’å—ä¿¡ã—ã¾ã—ãŸã€‚ +http.502.reason=Bad Gateway +http.503.desc=サーãƒãƒ¼ã¯ã€ä¸€æ™‚çš„ãªéŽè² è·ã¾ãŸã¯å®šæœŸä¿å®ˆã®ãŸã‚ã«ç¾åœ¨ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã§ãã¾ã›ã‚“。é…ã‚Œã¦ç·©å’Œã•ã‚Œã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +http.503.reason=Service Unavailable +http.504.desc=ゲートウェイã¾ãŸã¯ãƒ—ロキシã¨ã—ã¦æ©Ÿèƒ½ã—ã¦ã„るサーãƒãƒ¼ã¯ã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’完了ã™ã‚‹ãŸã‚ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹å¿…è¦ã®ã‚る上æµã®ã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰ã‚¿ã‚¤ãƒ ãƒªãƒ¼ãªãƒ¬ã‚¹ãƒãƒ³ã‚¹ã‚’å—ä¿¡ã—ã¾ã›ã‚“ã§ã—ãŸã€‚ +http.504.reason=ゲートウェイタイムアウト +http.505.desc=サーãƒãƒ¼ã¯ã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã§ä½¿ç”¨ã•ã‚ŒãŸãƒ¡ã‚¸ãƒ£ãƒ¼ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®HTTPをサãƒãƒ¼ãƒˆã—ã¦ã„ãªã„ã‹ã€ã¾ãŸã¯ã‚µãƒãƒ¼ãƒˆã‚’æ‹’å¦ã—ã¦ã„ã¾ã™ã€‚ +http.505.reason=サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„HTTPãƒãƒ¼ã‚¸ãƒ§ãƒ³ +http.506.desc=サーãƒãƒ¼ã«ã¯å†…部構æˆã‚¨ãƒ©ãƒ¼ãŒã‚ã‚Šã¾ã™ã€‚é¸æŠžã•ã‚ŒãŸå¯å¤‰ãƒªã‚½ãƒ¼ã‚¹ã¯é€éŽçš„ãªã‚³ãƒ³ãƒ†ãƒ³ãƒ„ãƒã‚´ã‚·ã‚¨ãƒ¼ã‚·ãƒ§ãƒ³è‡ªä½“ã«é–¢ä¸Žã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€ãƒã‚´ã‚·ã‚¨ãƒ¼ã‚·ãƒ§ãƒ³ãƒ—ロセスã®é©åˆ‡ãªã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +http.506.reason=Variant Also Negotiates +http.507.desc=サーãƒãƒ¼ãŒãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’正常ã«å®Œäº†ã™ã‚‹ã®ã«å¿…è¦ãªè¡¨ç¾ã‚’ä¿ç®¡ã§ããªã„ãŸã‚ã€ã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ã‚’リソースã«å¯¾ã—ã¦å®Ÿè¡Œã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +http.507.reason=ストレージã«å……分ãªç©ºã容é‡ãŒã‚ã‚Šã¾ã›ã‚“ +http.508.desc=サーãƒãƒ¼ã¯ã€"Depth:infinity"ã§ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã—ã¦ã„ã‚‹é–“ã«ç„¡é™ãƒ«ãƒ¼ãƒ—を検出ã—ãŸãŸã‚ã€æ“作を終了ã—ã¾ã—ãŸã€‚ +http.508.reason=Loop Detected +http.510.desc=リクエストã«ãƒªã‚½ãƒ¼ã‚¹ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ãŸã‚ã®ãƒãƒªã‚·ãƒ¼ãŒæº€ãŸã•ã‚Œã¦ã„ã¾ã›ã‚“。 +http.510.reason=Not Extended +http.511.desc=クライアントã¯ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚¢ã‚¯ã‚»ã‚¹ã‚’å–å¾—ã™ã‚‹ãŸã‚ã«èªè¨¼ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +http.511.reason=Network Authentication Required + +jdbcAccessLogValve.close=データベースã®ã‚¯ãƒ­ãƒ¼ã‚ºã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +jdbcAccessLogValve.exception=アクセスエントリã®æŒ¿å…¥ã‚’実行中ã®ä¾‹å¤–ã§ã™ + +loadBalancerDrainingValve.draining=ロードãƒãƒ©ãƒ³ã‚µã¯ DISABLED 状態ã«ã‚ã‚Šã€ã“ã®ãƒŽãƒ¼ãƒ‰ã‚’使用ã—ã¦ã„ã¾ã›ã‚“ +loadBalancerDrainingValve.skip=クライアントã¯æœ‰åŠ¹ãª [{0}] Cookie ã‚’æ示ã—ã¦ã„ã‚‹ãŸã‚ã€ãƒªãƒãƒ©ãƒ³ã‚¹ã¯ã‚¹ã‚­ãƒƒãƒ—ã•ã‚Œã¦ã„ã¾ã™ + +patternTokenizer.unexpectedParenthesis=パターンã«äºˆæœŸã—ãªã„ ')' ãŒã‚ã‚Šã¾ã™ + +persistentValve.acquireFailed=リクエスト [{0}] ã¯ã€åˆ©ç”¨å¯èƒ½ãªè¨±å¯ãŒãªã‹ã£ãŸãŸã‚ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ã”ã¨ã®ã‚»ãƒžãƒ•ã‚©ã‚’å–å¾—ã§ãã¾ã›ã‚“ã§ã—㟠+persistentValve.acquireInterrupted=リクエスト [{0}] ã¯ã€è¨±å¯ã®å¾…機中ã«ä¸­æ–­ã•ã‚ŒãŸãŸã‚ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ã”ã¨ã®ã‚»ãƒžãƒ•ã‚©ã‚’å–å¾—ã§ãã¾ã›ã‚“ã§ã—㟠+persistentValve.filter.failure=filter = [{0}]をコンパイルã§ãã¾ã›ã‚“ +persistentValve.requestIgnore=リクエスト [{0}] ã¯ã€æ§‹æˆã•ã‚ŒãŸãƒ•ã‚£ãƒ«ã‚¿ãƒ¼ã¨ä¸€è‡´ã™ã‚‹ãŸã‚ã€ã“ã® Valve ã«ã‚ˆã£ã¦ç„¡è¦–ã•ã‚Œã¾ã—㟠+persistentValve.requestProcess=リクエスト [{0}] ã¯ã€æ§‹æˆã•ã‚ŒãŸãƒ•ã‚£ãƒ«ã‚¿ãƒ¼ã¨ä¸€è‡´ã—ãªã„ãŸã‚ã€ã“ã® Valve ã«ã‚ˆã£ã¦å‡¦ç†ã•ã‚Œã¾ã™ +persistentValve.sessionLoadFail=ストアã‹ã‚‰ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ [{0}] ã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—㟠+ +proxyErrorReportValve.error=[{0}] ã«å¯¾ã™ã‚‹ãƒ—ロキシ エラー + +remoteCidrValve.invalid="[{0}]" ã«ä¸æ­£ãªå€¤ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚詳細ã¯å‰ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å‚ç…§ã—ã¦ãã ã•ã„。 +remoteCidrValve.noPort=リクエストã«æœ‰åŠ¹ãªã‚µãƒ¼ãƒãƒ¼ãƒãƒ¼ãƒˆãŒå«ã¾ã‚Œã¦ã„ã¾ã›ã‚“。 リクエストã¯æ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ +remoteCidrValve.noRemoteIp=クライアント㮠IP アドレスをå–å¾—ã§ãã¾ã›ã‚“。リクエストを拒å¦ã—ã¾ã™ã€‚ +remoteCidrValve.unexpectedPort=Connector コンフィグレーション属性 addConnectorPort 㯠false ã§ã™ãŒã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ã¯ã‚µãƒ¼ãƒãƒ¼ ãƒãƒ¼ãƒˆãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ リクエストã¯æ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ + +remoteIpValve.invalidHostHeader=HTTP ヘッダ [{1}] 中㮠Host ã«ç„¡åŠ¹ãªå€¤ [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã—㟠+remoteIpValve.invalidHostWithPort=HTTP ヘッダ [{1}] 中㮠Host ã®å€¤ [{0}] ã¯ãƒãƒ¼ãƒˆç•ªå·ã‚’å«ã‚“ã§ã„ã¾ã™ãŒç„¡è¦–ã•ã‚Œã¾ã™ +remoteIpValve.invalidPortHeader=HTTP ヘッダー [{1}] ã«ç„¡åŠ¹ãªãƒãƒ¼ãƒˆç•ªå· [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +remoteIpValve.invalidRemoteAddress=報告ã•ã‚ŒãŸãƒªãƒ¢ãƒ¼ãƒˆã‚¢ãƒ‰ãƒ¬ã‚¹ [{0}] ãŒç„¡åŠ¹ã§ã‚ã‚‹ãŸã‚ã€ãƒªãƒ¢ãƒ¼ãƒˆãƒ›ã‚¹ãƒˆã‚’特定ã§ãã¾ã›ã‚“ + +requestFilterValve.configInvalid=Valveã¨ãã®è¦ªã‚³ãƒ³ãƒ†ãƒŠã®èµ·å‹•ã‚’妨ã’ãŸRemote [Addr | Host] Valveã«1ã¤ä»¥ä¸Šã®ç„¡åŠ¹ãªæ§‹æˆè¨­å®šãŒæä¾›ã•ã‚Œã¾ã—ãŸã€‚ +requestFilterValve.deny=プロパティ [{1}] ã«ã‚ˆã‚Š [{0}] ã¸ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚ + +sslValve.certError=java.security.cert.X509Certificateオブジェクトを生æˆã™ã‚‹ãŸã‚ã®è¨¼æ˜Žæ›¸æ–‡å­—列[{0}]ã®å‡¦ç†ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +sslValve.invalidProvider=リクエスト [{0}] ã«é–¢é€£ä»˜ã‘られãŸã‚³ãƒã‚¯ã‚¿ãƒ¼ã«æŒ‡å®šã•ã‚ŒãŸ SSL プロãƒã‚¤ãƒ€ãƒ¼ã¯ç„¡åŠ¹ã§ã™ã€‚証明書データを処ç†ã§ãã¾ã›ã‚“。 + +stuckThreadDetectionValve.interrupted=リクエスト終了後ã«ã‚¹ãƒ¬ãƒƒãƒ‰ãŒä¸­æ–­ã•ã‚Œã€ç„¡è¦–ã•ã‚Œã¾ã—㟠+stuckThreadDetectionValve.notifyStuckThreadCompleted=スレッド [{0}] (id = [{3}]) ã¯ä»¥å‰ã«ã‚¹ã‚¿ãƒƒã‚¯ã•ã‚Œã¦ã„ã‚‹ã¨å ±å‘Šã•ã‚Œã¾ã—ãŸãŒå®Œäº†ã—ã¾ã—ãŸã€‚ãã‚Œã¯ãŠã‚ˆã [{1}] ミリ秒ã®é–“アクティブã ã£ãŸã€‚\n\ +\ {2,choice,0#|0< ã“ã®ãƒãƒ«ãƒ–ã«ã‚ˆã£ã¦ç›£è¦–ã•ã‚Œã¦ã„るスレッド [{2}] ã¯æ®‹ã£ã¦ã„ã¾ã™ãŒã€ã‚¹ã‚¿ãƒƒã‚¯ã•ã‚Œã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚} +stuckThreadDetectionValve.notifyStuckThreadDetected=スレッド [{0}] (ID = [{6}]) 㯠[{1}] ミリ秒 ([{2}] 以é™) ã‹ã‚‰ [{4}] ã«å¯¾ã™ã‚‹åŒã˜ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’処ç†ã™ã‚‹ãŸã‚ã«ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã§ã‚ã‚Šã€ã‚¹ã‚¿ãƒƒã‚¯ã•ã‚Œã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ (ã“ã®StuckThreadDetectionValveã®è¨­å®šã•ã‚ŒãŸã—ãã„値 (threshold) 㯠[{5}] 秒ã§ã™)。ã“ã®Valveã«ã‚ˆã£ã¦ç›£è¦–ã•ã‚Œã¦ã„るスレッドã¯åˆè¨ˆã§ [{3}] 個ã‚ã‚Šã€ã‚¹ã‚¿ãƒƒã‚¯ã•ã‚Œã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +stuckThreadDetectionValve.notifyStuckThreadInterrupted=スレッド [{0}] (id=[{5}]) ã«å‰²ã‚Šè¾¼ã¿ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚[{3}] ã«å¯¾ã™ã‚‹ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å‡¦ç†æ™‚間㌠[{1}] ミリ秒 ([{2}] ã‹ã‚‰é–‹å§‹) を超éŽã—ãŸãŸã‚処ç†ãŒé€²ã¾ãªããªã£ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚StuckThreadDetectionValve ã«ã¯å‰²ã‚Šè¾¼ã¿ãŒç™ºç”Ÿã™ã‚‹ã¾ã§ã®æ™‚é–“ [{4}] 秒ãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ diff --git a/java/org/apache/catalina/valves/LocalStrings_ko.properties b/java/org/apache/catalina/valves/LocalStrings_ko.properties new file mode 100644 index 0000000..55502ce --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_ko.properties @@ -0,0 +1,152 @@ +# 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. + +accessLogValve.alreadyExists=ì ‘ê·¼ 로그 파ì¼ì„ [{0}]ì—ì„œ [{1}](으)ë¡œ ì´ë¦„ì„ ë³€ê²½í•˜ì§€ 못했습니다. 파ì¼ì´ ì´ë¯¸ 존재합니다. +accessLogValve.closeFail=ì ‘ê·¼ 로그 파ì¼ì„ 닫지 못했습니다. +accessLogValve.deleteFail=ì´ì „ ì ‘ê·¼ 로그 íŒŒì¼ [{0}]ì„(를) 삭제하지 못했습니다. +accessLogValve.invalidLocale=로케ì¼ì„ [{0}](으)ë¡œ 설정하지 못했습니다. +accessLogValve.invalidPortType=유효하지 ì•Šì€ í¬íŠ¸ 타입 [{0}]. 서버 (로컬) í¬íŠ¸ë¥¼ 사용합니다. +accessLogValve.invalidRemoteAddressType=유효하지 ì•Šì€ ì›ê²© 주소 타입 [{0}]. Peerê°€ ì•„ë‹Œ ì›ê²© 주소로 간주합니다. +accessLogValve.openDirFail=ì ‘ê·¼ 로그 파ì¼(들)ì„ ìœ„í•œ 디렉토리 [{0}]ì„(를) ìƒì„±í•˜ì§€ 못했습니다. +accessLogValve.openFail=ì ‘ê·¼ 로그 íŒŒì¼ [{0}]ì„(를) 열지 못했습니다. +accessLogValve.renameFail=ì ‘ê·¼ 로그 파ì¼ì„ [{0}]ì—ì„œ [{1}](으)ë¡œ ì´ë¦„ì„ ë³€ê²½í•˜ì§€ 못했습니다. +accessLogValve.rotateFail=ì ‘ê·¼ 로그를 순환시키지 못했습니다. +accessLogValve.unsupportedEncoding=ì¸ì½”ë”©ì„ [{0}](으)ë¡œ 설정하지 못했습니다. 시스템 기본 문ìžì…‹ì„ 사용할 것입니다. +accessLogValve.writeFail=ë‹¤ìŒ ë¡œê·¸ 메시지를 쓰지 못했습니다: [{0}] + +errorReportValve.description=설명 +errorReportValve.errorPageIOException=예외 ë°œìƒìœ¼ë¡œ ì¸í•˜ì—¬ [{0}]ì— ìœ„ì¹˜í•œ 오류 페ì´ì§€ë¥¼ 표시할 수 없습니다. +errorReportValve.errorPageNotFound=[{0}]ì— ìœ„ì¹˜í•œ ì •ì  ì˜¤ë¥˜ 페ì´ì§€ë¥¼ ì°¾ì„ ìˆ˜ 없습니다. +errorReportValve.exception=예외 +errorReportValve.exceptionReport=예외 ë³´ê³  +errorReportValve.message=메시지 +errorReportValve.noDescription=ì„¤ëª…ì´ ì—†ìŠµë‹ˆë‹¤. +errorReportValve.note=비고 +errorReportValve.rootCause=근본 ì›ì¸ (root cause) +errorReportValve.rootCauseInLogs=근본 ì›ì¸(root cause)ì˜ í’€ ìŠ¤íƒ íŠ¸ë ˆì´ìŠ¤ë¥¼, 서버 로그들ì—ì„œ 확ì¸í•  수 있습니다. +errorReportValve.statusHeader=HTTP ìƒíƒœ {0} – {1} +errorReportValve.statusReport=ìƒíƒœ ë³´ê³  +errorReportValve.type=타입 +errorReportValve.unknownReason=ì•Œ 수 없는 사유 + +extendedAccessLogValve.badXParam=유효하지 ì•Šì€ x 파ë¼ë¯¸í„° í¬ë§·. í¬ë§·ì€ 'x-#(...) ì´ì–´ì•¼ 합니다. +extendedAccessLogValve.badXParamValue=서블릿 ìš”ì²­ì„ ìœ„í•´ 유효하지 ì•Šì€ x 파ë¼ë¯¸í„° ê°’: [{0}] +extendedAccessLogValve.decodeError=[{0}](으)ë¡œ 시작하는 문ìžë“¤ì˜ 나머지 ë¶€ë¶„ì„ ë””ì½”ë“œí•  수 없습니다. +extendedAccessLogValve.emptyPattern=íŒ¨í„´ì´ ê·¸ì € 빈 문ìžì—´ì´ì—ˆê±°ë‚˜, 공백 문ìžë¡œë§Œ 채워진 문ìžì—´ì´ì—ˆìŠµë‹ˆë‹¤. +extendedAccessLogValve.noClosing=ë””ì½”ë“œëœ ì ‘ê·¼ 로그 í–‰ì—ì„œ 닫는 중괄호, '')'', ê°€ 없습니다. +extendedAccessLogValve.patternParseError=패턴 [{0}]ì„(를) 파싱하는 중 오류 ë°œìƒ + +http.400.desc=í´ë¼ì´ì–¸íŠ¸ 오류로서 ì¸ì§€ëœ ì–´ë–¤ 문제로 ì¸í•˜ì—¬, 서버가 해당 ìš”ì²­ì„ ì²˜ë¦¬í•  수 없거나, 처리하지 ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. (예: ìž˜ëª»ëœ ìš”ì²­ 문법, 유효하지 ì•Šì€ ìš”ì²­ 메시지 framing, ë˜ëŠ” 신뢰할 수 없는 요청 ë¼ìš°íŒ…). +http.400.reason=ìž˜ëª»ëœ ìš”ì²­ +http.401.desc=ëŒ€ìƒ ë¦¬ì†ŒìŠ¤ì— ì ‘ê·¼í•˜ê¸° 위한 유효한 ì¸ì¦ credentialsê°€ 없기 때문ì—, ìš”ì²­ì— ì ìš©ë˜ì§€ 않았습니다. +http.401.reason=ì¸ê°€ ì•ˆë¨ +http.402.desc=ì´ ìƒíƒœ 코드는 ë¯¸ëž˜ì˜ ì‚¬ìš©ì„ ìœ„í•´ 예약ë˜ì–´ 있습니다. +http.402.reason=ì§€ë¶ˆì´ ìš”êµ¬ë¨ +http.403.desc=서버가 ìš”ì²­ì„ ì´í•´í–ˆìœ¼ë‚˜ 승ì¸ì„ 거부합니다. +http.403.reason=ê¸ˆì§€ë¨ +http.404.desc=Origin 서버가 ëŒ€ìƒ ë¦¬ì†ŒìŠ¤ë¥¼ 위한 í˜„ìž¬ì˜ representationì„ ì°¾ì§€ 못했거나, ê·¸ê²ƒì´ ì¡´ìž¬í•˜ëŠ”ì§€ë¥¼ ë°ížˆë ¤ 하지 않습니다. +http.404.reason=ì°¾ì„ ìˆ˜ ì—†ìŒ +http.405.desc=요청 í–‰ì— í¬í•¨ëœ 해당 메소드는, origin ì„œë²„ì— ì˜í•´ ì¸ì§€ë˜ì—ˆìœ¼ë‚˜, ëŒ€ìƒ ë¦¬ì†ŒìŠ¤ì— ì˜í•´ 지ì›ë˜ì§€ 않습니다. +http.405.reason=허용ë˜ì§€ 않는 메소드 +http.406.desc=요청으로부터 ë°›ì€ proactive negotiation í—¤ë”ì— ë”°ë¥´ë©´, ëŒ€ìƒ ë¦¬ì†ŒìŠ¤ëŠ” 해당 user agentê°€ 받아들ì¼ë§Œí•œ í˜„ìž¬ì˜ representationì´ ì—†ê³ , 서버 ë˜í•œ 기본 representationì„ ì œê³µí•˜ì§€ 않으려 합니다. +http.406.reason=ë°›ì•„ë“¤ì¼ ìˆ˜ ì—†ìŒ +http.407.desc=ì´ ìƒíƒœ 코드는 401 (ì¸ì¦ 안ë¨)ê³¼ 유사하나, ì´ëŠ” í´ë¼ì´ì–¸íŠ¸ê°€ 프ë¡ì‹œë¥¼ 사용하기 위하여 스스로를 ì¸ì¦í•  필요가 있ìŒì„ 알려ì¤ë‹ˆë‹¤. +http.407.reason=프ë¡ì‹œ ì¸ì¦ì´ ìš”êµ¬ë¨ +http.408.desc=대기하ë„ë¡ ì¤€ë¹„ëœ ì‹œê°„ ì´ë‚´ì—, 서버가 완전한 요청 메시지를 수신하지 못했습니다. +http.408.reason=요청 제한 시간 초과 +http.409.desc=ëŒ€ìƒ ë¦¬ì†ŒìŠ¤ì˜ í˜„ìž¬ ìƒíƒœì™€ì˜ ì¶©ëŒ ë•Œë¬¸ì—, ìš”ì²­ì´ ì™„ë£Œë  ìˆ˜ 없었습니다. +http.409.reason=충ëŒë¨ +http.410.desc=ëŒ€ìƒ ë¦¬ì†ŒìŠ¤ì— ëŒ€í•œ ì ‘ê·¼ì´ í•´ë‹¹ origin 서버ì—ì„œ ë”ì´ìƒ 가용하지 않으며, ì´ëŸ¬í•œ ì¡°ê±´ì€ ì•„ë§ˆë„ ì˜êµ¬ì ì¼ 것으로 보입니다. +http.410.reason=사ë¼ì¡ŒìŒ +http.411.desc=Content-Lengthê°€ ì •ì˜ë˜ì§€ ì•Šì€ ìš”ì²­ì„, 서버가 받아들ì´ê¸°ë¥¼ 거부했습니다. +http.411.reason=Lengthê°€ ìš”êµ¬ë¨ +http.412.desc=서버ì—ì„œ ê²€ì‚¬ë  ë•Œ, 요청 í—¤ë” í•„ë“œë“¤ ë‚´ì— ì£¼ì–´ì§„ 하나 ì´ìƒì˜ ì¡°ê±´(들)ì´, falseë¡œ í‰ê°€ë˜ì—ˆìŠµë‹ˆë‹¤. +http.412.reason=사전 ì¡°ê±´ 충족 실패 +http.413.desc=ìš”ì²­ì˜ payloadê°€ 서버가 처리하려 하거나 처리할 수 있는 것 보다 í¬ê¸° 때문ì—, 서버가 요청 처리를 거부합니다. +http.413.reason=Payloadê°€ 너무 í½ë‹ˆë‹¤. +http.414.desc=서버가 처리할 수 있는 것보다 request-targetì´ ë” ê¸¸ê¸° 때문ì—, ìš”ì²­ì— ëŒ€í•œ 서비스를 거부합니다. +http.414.reason=URIê°€ 너무 ê¹ë‹ˆë‹¤. +http.415.desc=Payloadê°€ ëŒ€ìƒ ë¦¬ì†ŒìŠ¤ì— ëŒ€í•œ ì´ ë©”ì†Œë“œì— ì˜í•´ 지ì›ë˜ì§€ 않는 í¬ë§·ì´ê¸° 때문ì—, Origin 서버가 ìš”ì²­ì„ ì„œë¹„ìŠ¤í•˜ê¸°ë¥¼ 거부합니다. +http.415.reason=지ì›ë˜ì§€ 않는 Media Type +http.416.desc=ìš”ì²­ì˜ Range í—¤ë” í•„ë“œ ë‚´ì˜ ë²”ìœ„ë“¤ 중 ì–´ëŠ ê²ƒë„, ì„ íƒëœ ë¦¬ì†ŒìŠ¤ì˜ í˜„ìž¬ 범위와 겹치지 않거나, ìš”ì²­ëœ ë²”ìœ„ë“¤ì˜ ì§‘í•©ì´ ìœ íš¨í•˜ì§€ ì•Šì€ ë²”ìœ„ë“¤, ë˜ëŠ” ê³¼ë„하게 작거나 겹치는 범위들ì´ê¸° ë•Œë¬¸ì— ê±°ì ˆë˜ì—ˆìŠµë‹ˆë‹¤. +http.416.reason=ì¶©ì¡±ë  ìˆ˜ 없는 범위 +http.417.desc=ìš”ì²­ì˜ Expect í—¤ë” í•„ë“œì— ì£¼ì–´ì§„ expectationì´, ì ì–´ë„ 하나 ì´ìƒì˜ inbound ì„œë²„ë“¤ì— ì˜í•´ ì¶©ì¡±ë  ìˆ˜ 없었습니다. +http.417.reason=Expectation Failed +http.421.desc=ìš”ì²­ì´ ì‘ë‹µì„ ìƒì„±í•  수 없는 서버로 전달ë˜ì—ˆìŠµë‹ˆë‹¤. +http.421.reason=잘못 ì•ˆë‚´ëœ ìš”ì²­ +http.422.desc=서버가 요청 ì—”í‹°í‹°ì˜ Content-Typeì„ ì´í•´í•˜ê³ , 요청 ì—”í‹°í‹°ì˜ ë¬¸ë²•ì´ ì˜¬ë°”ë¥´ê²Œ ë˜ì–´ 있지만, í¬í•¨ëœ instructionë“¤ì„ ì²˜ë¦¬í•  수 없었습니다. +http.422.reason=처리할 수 없는 엔티티 +http.423.desc=ë©”ì†Œë“œì˜ ì›ë³¸ ë˜ëŠ” ëŒ€ìƒ ë¦¬ì†ŒìŠ¤ê°€ 잠금 ìƒíƒœìž…니다. +http.423.reason=ìž ê²¨ì§ +http.424.desc=ìš”ì²­ëœ ì•¡ì…˜ì´ ì´ë¯¸ 실패한 ë˜ ë‹¤ë¥¸ ì•¡ì…˜ì— ì˜ì¡´í•˜ê³  있었기 때문ì—, 해당 ë¦¬ì†ŒìŠ¤ì— ëŒ€í•´ ì´ ë©”ì†Œë“œë¥¼ 수행할 수 없습니다. +http.424.reason=실패한 ì˜ì¡´ì  요청 +http.426.desc=서버가 í˜„ìž¬ì˜ í”„ë¡œí† ì½œì„ ì‚¬ìš©í•˜ì—¬ ìš”ì²­ì„ ì²˜ë¦¬í•˜ê¸°ë¥¼ 거부했지만, í´ë¼ì´ì–¸íŠ¸ê°€ 다른 프로토콜로 업그레ì´ë“œí•œ í›„ì— ì²˜ë¦¬í•˜ë ¤ í•  ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. +http.426.reason=업그레ì´ë“œê°€ ìš”êµ¬ë¨ +http.428.desc=Origin 서버는 ìš”ì²­ì´ ì‚¬ì „ ì¡°ê±´ì ì´ê¸°ë¥¼ 요구합니다 (예: If-Match와 ê°™ì€ í—¤ë”). +http.428.reason=ì‚¬ì „ì¡°ê±´ì´ í•„ìˆ˜ì ìž…니다. +http.429.desc=사용ìžê°€ 주어진 시간 ë™ì•ˆ 너무 ë§Žì€ ìš”ì²­ì„ ë³´ëƒˆìŠµë‹ˆë‹¤. ("rate limiting") +http.429.reason=너무 ë§Žì€ ìš”ì²­ë“¤ +http.431.desc=요청 ë‚´ì˜ í—¤ë” í•„ë“œë“¤ì´ ë„ˆë¬´ 커서 서버가 처리하려 하지 않습니다. +http.431.reason=ìš”ì²­ì˜ í—¤ë” í•„ë“œë“¤ì´ ë„ˆë¬´ í¼ +http.451.desc=서버가 법ì ì¸ 사유들로 ì´ ìš”ì²­ì„ ê±°ë¶€í–ˆìŠµë‹ˆë‹¤. +http.451.reason=법ì ì¸ 사유들로 ì¸í•˜ì—¬ 가용하지 ì•ŠìŒ +http.500.desc=서버가, 해당 ìš”ì²­ì„ ì¶©ì¡±ì‹œí‚¤ì§€ 못하게 하는 예기치 ì•Šì€ ì¡°ê±´ì„ ë§žë‹¥ëœ¨ë ¸ìŠµë‹ˆë‹¤. +http.500.reason=내부 서버 오류 +http.501.desc=서버가 ì´ ìš”ì²­ì„ ì¶©ì¡±ì‹œí‚¤ëŠ”ë° í•„ìš”í•œ 필수ì ì¸ ê¸°ëŠ¥ì„ ì§€ì›í•˜ì§€ 않습니다. +http.501.reason=구현ë˜ì§€ ì•ŠìŒ +http.502.desc=서버가 게ì´íŠ¸ì›¨ì´ ë˜ëŠ” 프ë¡ì‹œë¡œì„œ ë™ìž‘하면서 ìš”ì²­ì„ ì²˜ë¦¬í•˜ë ¤ ì‹œë„하는 ë™ì•ˆ, inbound 서버로부터 유효하지 ì•Šì€ ì‘ë‹µì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +http.502.reason=ìž˜ëª»ëœ ê²Œì´íŠ¸ì›¨ì´ +http.503.desc=ì¼ì‹œì ì¸ 서버 부하 ë˜ëŠ” ì˜ˆì •ëœ ìœ ì§€ë³´ìˆ˜ 작업으로 ì¸í•˜ì—¬, 서버가 현재 ìš”ì²­ì„ ì²˜ë¦¬í•  수 없습니다. ìž ì‹œ ì§€ì—°ëœ ë’¤ì— ìƒí™©ì´ 나아질 것으로 보입니다. +http.503.reason=서비스가 가용하지 ì•ŠìŒ +http.504.desc=서버가 게ì´íŠ¸ì›¨ì´ ë˜ëŠ” 프ë¡ì‹œë¡œ ë™ìž‘하는 ë™ì•ˆ, ìš”ì²­ì„ ì²˜ë¦¬ 완료하기 위해 접근해야 하는 ìƒìœ„ 서버로부터, 필요한 ì‘ë‹µì„ ì ì ˆí•œ 시간 ë‚´ì— ë°›ì§€ 못했습니다. +http.504.reason=게ì´íŠ¸ì›¨ì´ 제한 시간 초과 +http.505.desc=서버가 요청 메시지ì—ì„œ ì‚¬ìš©ëœ HTTPì˜ major ë²„ì „ì„ ì§€ì›í•˜ì§€ 않거나, ë˜ëŠ” 지ì›í•˜ê¸°ë¥¼ 거부합니다. +http.505.reason=HTTP ë²„ì „ì´ ì§€ì›ë˜ì§€ ì•ŠìŒ +http.506.desc=ì„œë²„ì— ë‚´ë¶€ 설정 오류가 있습니다: ì„ íƒëœ 변형(variant) 리소스가, 투명한 컨í…트 êµì„­(negotiation) ê·¸ ìžì²´ì— 관여하ë„ë¡ ì„¤ì •ë˜ì–´ 있는ë°, 그로 ì¸í•˜ì—¬ êµì„­ í”„ë¡œì„¸ìŠ¤ì— ì ì ˆí•œ 엔드í¬ì¸íŠ¸ê°€ 아닙니다. +http.506.reason=Variant Also Negotiates +http.507.desc=서버가 요청 처리를 성공ì ìœ¼ë¡œ 완료하기 위해 필요한 representationì„ ì €ìž¥í•  수 없기 때문ì—, 해당 메소드가 해당 ë¦¬ì†ŒìŠ¤ì— ëŒ€í•´ ì²˜ë¦¬ë  ìˆ˜ 없었습니다. +http.507.reason=충분하지 ì•Šì€ ì €ìž¥ 공간 +http.508.desc=서버가 "Depth: infinity"를 가진 ìš”ì²­ì„ ì²˜ë¦¬í•˜ëŠ” ë„중, 무한 루프를 맞닥뜨리는 ë°”ëžŒì— ì˜¤í¼ë ˆì´ì…˜ì„ 종료시켰습니다. +http.508.reason=루프가 íƒì§€ë¨ +http.510.desc=요청ì´, ë¦¬ì†ŒìŠ¤ì— ì ‘ê·¼í•˜ê¸° 위한 policy를 충족시키지 않습니다. +http.510.reason=확장 ì•ˆë¨ +http.511.desc=í´ë¼ì´ì–¸íŠ¸ê°€ 네트워í¬ì— 접근하기 위해서는 ì¸ì¦ì„ 해야 합니다. +http.511.reason=ë„¤íŠ¸ì›Œí¬ ì¸ì¦ì´ 필요함 + +jdbcAccessLogValve.close=ë°ì´í„°ë² ì´ìŠ¤ë¥¼ 닫지 못했습니다. +jdbcAccessLogValve.exception=ì ‘ê·¼ 엔트리를 추가하는 중 예외 ë°œìƒ + +persistentValve.filter.failure=필터를 컴파ì¼í•  수 없습니다: [{0}] + +remoteCidrValve.invalid=[{0}]ì„(를) 위해 유효하지 ì•Šì€ ì„¤ì •ì´ ì œê³µë˜ì—ˆìŠµë‹ˆë‹¤. ìƒì„¸ 정보를 보시려면 ì´ì „ ë©”ì‹œì§€ë“¤ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤. +remoteCidrValve.noPort=유효한 서버 í¬íŠ¸ë¥¼ í¬í•¨í•˜ì§€ ì•Šì€ í•´ë‹¹ ìš”ì²­ì€ ê±°ë¶€ë©ë‹ˆë‹¤. +remoteCidrValve.noRemoteIp=í´ë¼ì´ì–¸íŠ¸ê°€ IP 주소를 가지고 있지 않습니다. ìš”ì²­ì€ ê±°ì ˆë˜ì—ˆìŠµë‹ˆë‹¤. +remoteCidrValve.unexpectedPort=Connector 설정 ì†ì„±ì¸ addConnectorPortê°€ falseì¸ë°ë„ 불구하고, ìš”ì²­ì´ ì„œë²„ í¬íŠ¸ë¥¼ í¬í•¨í•˜ê³  있어서, 해당 ìš”ì²­ì€ ê±°ë¶€ë©ë‹ˆë‹¤. + +remoteIpValve.invalidHostHeader=HTTP í—¤ë” [{1}] ë‚´ì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤: [{0}] +remoteIpValve.invalidHostWithPort=HTTP í—¤ë” [{1}] ë‚´ì˜ í˜¸ìŠ¤íŠ¸ ê°’ [{0}]ì´(ê°€) í¬íŠ¸ 번호를 í¬í•¨í–ˆëŠ”ë°, ì´ëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +remoteIpValve.invalidPortHeader=HTTP í—¤ë” [{1}] ë‚´ì— ìœ íš¨í•˜ì§€ ì•Šì€ í¬íŠ¸ 번호 값입니다: [{0}] +remoteIpValve.invalidRemoteAddress=ë³´ê³ ëœ ì›ê²© 주소 [{0}](ì´)ê°€ 유효하지 ì•Šì•„ì„œ ì›ê²© 호스트를 ì‹ë³„í•  수 없습니다. + +requestFilterValve.configInvalid=Remote[Addr|Host]Valve를 위해 하나 ì´ìƒì˜ 유효하지 ì•Šì€ ì„¤ì •ì´ ì œê³µë˜ì—ˆëŠ”ë°, ì´ëŠ” 해당 Valve와 부모 컨테ì´ë„ˆë“¤ì´ 시작ë˜ì§€ 못하게 했습니다. +requestFilterValve.deny=프로í¼í‹° [{1}]ì— ê¸°ë°˜í•˜ì—¬, [{0}]ì„(를) 위한 ìš”ì²­ì„ ê±°ì ˆí•©ë‹ˆë‹¤. + +sslValve.certError=java.security.cert.X509Certificate ê°ì²´ë¥¼ ìƒì„±í•˜ê¸° 위한 ì¸ì¦ì„œ 문ìžì—´ [{0}]ì„(를) 처리하지 못했습니다. +sslValve.invalidProvider=[{0}]ì˜ ì´ ìš”ì²­ê³¼ ì—°ê´€ëœ Connectorì— ì§€ì •ëœ SSL provider는 유효하지 않습니다. 해당 ì¸ì¦ì„œ ë°ì´í„°ê°€ ì²˜ë¦¬ë  ìˆ˜ 없었습니다. + +stuckThreadDetectionValve.notifyStuckThreadCompleted=쓰레드 [{0}] (ID=[{3}])ì´(ê°€) ì´ì „ì— stuck ìƒíƒœë¡œ ë³´ê³ ëœ ë°” 있으나 ì´ì œ 완료ë˜ì—ˆìŠµë‹ˆë‹¤. 해당 쓰레드는 대략 [{1}] 밀리초 ë™ì•ˆ 활성화ë˜ì–´ 있었습니다. {2,choice,0#|0< ì´ Valveì— ì˜í•´ 모니터ë§ë˜ëŠ” ì“°ë ˆë“œë“¤ì´ ì—¬ì „ížˆ [{2}]개가 있고, ê·¸ê²ƒë“¤ì€ ì–´ì©Œë©´ stuck ìƒíƒœì— ìžˆì„ ìˆ˜ 있습니다.} +stuckThreadDetectionValve.notifyStuckThreadDetected=쓰레드 [{0}] (id=[{6}])ì´(ê°€), [{4}]ì„(를) 위한 ë™ì¼í•œ ìš”ì²­ì„ ì²˜ë¦¬í•˜ê¸° 위해, ([{2}] ì´í›„) [{1}] 밀리초 ë™ì•ˆ 활성화ë˜ì–´ 있었으며, 해당 쓰레드가 stuckëœ ìƒíƒœì— ìžˆì„ ìˆ˜ 있습니다.\n\ +(ì´ StuckThreadDetectionValve를 위한 stuck ìƒíƒœ 진입 기준ì ì€ [{5}] 초입니다.) ì´ Valveì— ì˜í•´ 모니터ë˜ëŠ” ì „ì²´ 쓰레드들 중 [{3}] ê°œì˜ ì“°ë ˆë“œê°€ stuck ìƒíƒœì¼ 수 있습니다. +stuckThreadDetectionValve.notifyStuckThreadInterrupted=쓰레드 [{0}](id=[{5}])ì´(ê°€), [{1}] 밀리초 ë™ì•ˆ ë™ì¼ ìš”ì²­ì„ ì²˜ë¦¬í•˜ê¸° 위해 ([{2}] ì´í›„ë¡œ) [{3}] ë™ì•ˆ 활성화ë˜ì–´ 있었으나, í•„ì‹œ stuck ìƒíƒœì— ìžˆì„ ë²•í•œ 쓰레드ì´ê¸° ë•Œë¬¸ì— ì¤‘ë‹¨ë˜ì—ˆìŠµë‹ˆë‹¤. (ì´ StuckThreadDetectionValve를 위한 중단 한계치는 [{4}] 초로 설정ë˜ì–´ 있습니다.) diff --git a/java/org/apache/catalina/valves/LocalStrings_pt_BR.properties b/java/org/apache/catalina/valves/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..9044f5a --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_pt_BR.properties @@ -0,0 +1,22 @@ +# 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. + +errorReportValve.rootCauseInLogs=A pilha de erros completa da causa principal está disponível nos logs do servidor. + +http.404.reason=Não Encontrado +http.407.reason=Exige-se autenticação ao proxy. +http.415.reason=Media Type não suportado +http.426.reason=Atualização Requerida +http.502.desc=O servidor recebeu uma resposta inválida enquanto atuava como gateway ou proxy diff --git a/java/org/apache/catalina/valves/LocalStrings_ru.properties b/java/org/apache/catalina/valves/LocalStrings_ru.properties new file mode 100644 index 0000000..a49d4d0 --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_ru.properties @@ -0,0 +1,53 @@ +# 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. + +http.401.reason=Ðе авторизовано +http.402.reason=ТребуетÑÑ Ð¾Ð¿Ð»Ð°Ñ‚Ð° +http.403.desc=Сервер получил запроÑ, но отказалÑÑ ÐµÐ³Ð¾ авторизовать. +http.403.reason=ДоÑтуп запрещен +http.404.reason=Ðе найдено +http.405.reason=ÐедопуÑтимый метод +http.406.reason=Ðеприемлемый Ð·Ð°Ð¿Ñ€Ð¾Ñ +http.407.desc=Этот код ÑтатуÑа похож на 401 (Ðе авторизовано), но он означает, что клиенту требуетÑÑ Ð¿Ñ€Ð¾Ð¹Ñ‚Ð¸ аутентификацию, чтобы иÑпользовать прокÑи-Ñервер. +http.407.reason=ТребуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Ð½Ð° прокÑи-Ñервере +http.408.reason=Ð’Ñ€ÐµÐ¼Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа иÑтекло +http.409.reason=Конфликт +http.410.reason=ÐедоÑтупно навÑегда +http.411.desc=Сервер отказываетÑÑ Ð¿Ñ€Ð¸Ð½Ð¸Ð¼Ð°Ñ‚ÑŒ Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð±ÐµÐ· определенного заголовка Content-Length. +http.411.reason=Ðеобходимо указать длину +http.412.reason=Сбой при обработке предварительного уÑÐ»Ð¾Ð²Ð¸Ñ +http.414.desc=Сервер отказываетÑÑ Ð¾Ð±Ñлуживать запроÑ, потому что запрашиваеvsq URI длиннее, чем Ñервер может интерпретировать. +http.414.reason=ÐедопуÑÑ‚Ð¸Ð¼Ð°Ñ Ð´Ð»Ð¸Ð½Ð° URI запроÑа +http.415.reason=Ðеподдерживаемый тип медиа +http.423.reason=Заблокирован +http.426.reason=ТребуетÑÑ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ðµ +http.429.reason=Слишком много запроÑов +http.431.reason=ÐŸÐ¾Ð»Ñ Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²ÐºÐ° запроÑа Ñлишком велики +http.451.reason=ÐедоÑтупно по юридичеÑким причинам +http.500.reason=ВнутреннÑÑ Ð¾ÑˆÐ¸Ð±ÐºÐ° Ñервера +http.501.desc=Сервер не поддерживает функционал необходимый Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа. +http.501.reason=Ðе реализовано +http.502.reason=Ошибка шлюза +http.503.reason=Ð¡ÐµÑ€Ð²Ð¸Ñ Ð½Ðµ доÑтупен +http.504.reason=Ð’Ñ€ÐµÐ¼Ñ Ð¿Ñ€Ð¾Ñ…Ð¾Ð¶Ð´ÐµÐ½Ð¸Ñ Ñ‡ÐµÑ€ÐµÐ· шлюз иÑтекло +http.505.reason=ВерÑÐ¸Ñ HTTP не поддерживаетÑÑ +http.507.reason=ÐедоÑтаточно меÑта +http.508.reason=Обнаружен цикл +http.510.desc=Ð’ запроÑе не была Ñоблюдена политика доÑтупа к реÑурÑу +http.510.reason=ОтÑутÑтвуют раÑÑˆÐ¸Ñ€ÐµÐ½Ð¸Ñ +http.511.desc=Клиенты должны пройти аутентификацию Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð¾Ñтупа к Ñети. +http.511.reason=ТребуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ + +remoteIpValve.invalidPortHeader=Обнаружено некорректное значение [{0}] Ð´Ð»Ñ Ð¿Ð¾Ñ€Ñ‚Ð° в HTTP заголовке [{1}] diff --git a/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties b/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..0e28c08 --- /dev/null +++ b/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties @@ -0,0 +1,151 @@ +# 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. + +accessLogValve.alreadyExists=无法将访问日志从[{0}]é‡å‘½å为[{1}],文件已存在 +accessLogValve.closeFail=关闭访问日志文件失败 +accessLogValve.deleteFail=未能删除旧的访问日志[{0}] +accessLogValve.invalidLocale=无法将区域设置设为 [{0}] +accessLogValve.invalidPortType=端å£ç±»åž‹ [{0}] 无效,使用æœåŠ¡å™¨ï¼ˆæœ¬åœ°ï¼‰ç«¯å£ +accessLogValve.invalidRemoteAddressType=远程地å€ç±»åž‹[{0}]无效,使用远程(éžå¯¹ç­‰)åœ°å€ +accessLogValve.openDirFail=无法为访问日志创建目录[{0}]。 +accessLogValve.openFail=无法打开访问日志文件[{0}]。 +accessLogValve.renameFail=无法将访问日志从[{0}]é‡å‘½å为[{1}]。 +accessLogValve.rotateFail=失败的循环切割访问日志. +accessLogValve.unsupportedEncoding=未能将编ç è®¾ç½®ä¸º[{0}],将使用系统默认字符集。 +accessLogValve.writeFail=无法写入日志消æ¯[{0}] + +errorReportValve.description=æè¿° +errorReportValve.errorPageIOException=由于出现异常,无法在[{0}]处显示错误页 +errorReportValve.errorPageNotFound=在[{0}]无法找到é™æ€é”™è¯¯é¡µé¢ +errorReportValve.exception=例外情况 +errorReportValve.exceptionReport=异常报告 +errorReportValve.message=æ¶ˆæ¯ +errorReportValve.noDescription=没有å¯ç”¨çš„æè¿° +errorReportValve.note=):æ³¨æ„ +errorReportValve.rootCause=根本原因。 +errorReportValve.rootCauseInLogs=主è¦é—®é¢˜çš„全部 stack ä¿¡æ¯å¯ä»¥åœ¨ server logs 里查看 +errorReportValve.statusHeader=HTTPçŠ¶æ€ {0} - {1} +errorReportValve.statusReport=状æ€æŠ¥å‘Š +errorReportValve.type=类型 +errorReportValve.unknownReason=未知的原因 + +extendedAccessLogValve.badXParam=无效的xå‚æ•°æ ¼å¼ï¼Œéœ€è¦æ˜¯'x-#(…) +extendedAccessLogValve.badXParamValue=Servlet请求[{0}]çš„xå‚数值无效 +extendedAccessLogValve.decodeError=无法解ç ä»¥[{0}]开头的其余字符 +extendedAccessLogValve.emptyPattern=模å¼åªæ˜¯ç©ºçš„或空白的 +extendedAccessLogValve.noClosing=未关闭)在解ç ä¸­æ‰¾åˆ° +extendedAccessLogValve.patternParseError=分æžæ¨¡å¼[{0}]时出错 + +http.400.desc=由于被认为是客户端对错误(例如:畸形的请求语法ã€æ— æ•ˆçš„请求信æ¯å¸§æˆ–者虚拟的请求路由),æœåŠ¡å™¨æ— æ³•æˆ–ä¸ä¼šå¤„ç†å½“å‰è¯·æ±‚。 +http.400.reason=错误的请求 +http.401.desc=因为当å‰è¯·æ±‚缺少对目标资æºå¯¹æœ‰æ•ˆçš„认è¯ä¿¡æ¯ï¼Œæ‰€ä»¥å®ƒä¸ä¼šå®žæ–½ã€‚ +http.401.reason=未ç»æŽˆæƒçš„ +http.402.desc=这个状æ€ç æ—¶ä¸ºæœªæ¥ä½¿ç”¨é¢„留的. +http.402.reason=需è¦æ”¯ä»˜ +http.403.desc=æœåŠ¡å™¨ç†è§£è¯¥è¯·æ±‚但拒ç»æŽˆæƒã€‚ +http.403.reason=被ç¦æ­¢ +http.404.desc=æºæœåŠ¡å™¨æœªèƒ½æ‰¾åˆ°ç›®æ ‡èµ„æºçš„表示或者是ä¸æ„¿å…¬å¼€ä¸€ä¸ªå·²ç»å­˜åœ¨çš„资æºè¡¨ç¤ºã€‚ +http.404.reason=未找到 +http.405.desc=请求行中接收的方法由æºæœåŠ¡å™¨çŸ¥é“,但目标资æºä¸æ”¯æŒ +http.405.reason=方法ä¸å…许 +http.406.desc=æ ¹æ®è¯·æ±‚中接收到的主动å商头字段,目标资æºæ²¡æœ‰ç”¨æˆ·ä»£ç†å¯ä»¥æŽ¥å—的当å‰è¡¨ç¤ºï¼Œè€Œä¸”æœåŠ¡å™¨ä¸æ„¿æ„æ供缺çœè¡¨ç¤ºã€‚ +http.406.reason=ä¸å¯æŽ¥æ”¶ +http.407.desc=状æ€ç å’Œ401(未授æƒçš„)类似,但是表示客户端为了使用代ç†éœ€è¦å¯¹å®ƒè‡ªèº«è¿›è¡Œè®¤è¯ã€‚ +http.407.reason=代ç†éœ€è¦è®¤è¯ +http.408.desc=在预分é…的等待时间内,æœåŠ¡å™¨æœªæ”¶åˆ°å®Œæ•´çš„请求信æ¯ã€‚ +http.408.reason=请求超时 +http.409.desc=由于和目标资æºå¯¹å½“å‰çŠ¶æ€å‘生冲çªï¼Œæ‰€ä»¥è¯·æ±‚无法完æˆã€‚ +http.409.reason=å†²çª +http.410.desc=原始æœåŠ¡å™¨ä¸Šä¸å†å¯ä»¥è®¿é—®ç›®æ ‡èµ„æºï¼Œå¹¶ä¸”æ­¤æ¡ä»¶å¯èƒ½æ˜¯æ°¸ä¹…性的。 +http.410.reason=跑了。 +http.411.desc=æœåŠ¡å™¨æ‹’ç»æŽ¥å—没有定义内容长度的请求。 +http.411.reason=所需长度 +http.412.desc=在æœåŠ¡å™¨ä¸Šæµ‹è¯•æ—¶ï¼Œè¯·æ±‚头字段中给出的一个或多个æ¡ä»¶è¢«è¯„估为false。 +http.412.reason=å‰ç½®æ¡ä»¶å¤±è´¥ +http.413.desc=æœåŠ¡å™¨æ‹’ç»å¤„ç†è¯·æ±‚,因为请求负载大于æœåŠ¡å™¨æ„¿æ„或能够处ç†çš„è´Ÿè½½ +http.413.reason=有效载è·è¿‡å¤§ +http.414.desc=æœåŠ¡å™¨æ‹’ç»ä¸ºè¯·æ±‚æä¾›æœåŠ¡ï¼Œå› ä¸ºè¯·æ±‚目标比æœåŠ¡å™¨æ„¿æ„解释的è¦é•¿ã€‚ +http.414.reason=URI太长 +http.415.desc=æºæœåŠ¡å™¨æ‹’ç»æœåŠ¡è¯·æ±‚,因为有效负载的格å¼åœ¨ç›®æ ‡èµ„æºä¸Šæ­¤æ–¹æ³•ä¸æ”¯æŒã€‚ +http.415.reason=ä¸æ”¯æŒçš„媒体类型 +http.416.desc=(:请求的范围头字段中的任何范围都没有与选定资æºçš„当å‰èŒƒå›´é‡å ï¼Œæˆ–者请求的范围集由于无效范围或å°èŒƒå›´æˆ–é‡å èŒƒå›´çš„过度请求而被拒ç»ã€‚ +http.416.reason=范围ä¸æ»¡è¶³ +http.417.desc=(:至少有一个入站æœåŠ¡å™¨æ— æ³•æ»¡è¶³è¯·æ±‚çš„Expect头字段中给定的期望。 +http.417.reason=期望的失败 +http.421.desc=请求被定å‘到一å°æ— æ³•å“应的æœåŠ¡å™¨ +http.421.reason=错误的请求。 +http.422.desc=æœåŠ¡å™¨äº†è§£è¯·æ±‚实体的内容类型,请求实体的语法正确,但无法处ç†åŒ…å«çš„指令。 +http.422.reason=ä¸å¯å¤„ç†å®žä½“ +http.423.desc=æºæˆ–目标资æºçš„æ–¹æ³•è¢«é” +http.423.reason=å·²é”定 +http.424.desc=这个方法ä¸èƒ½åœ¨è¿™ä¸ªèµ„æºä¸Šæ‰§è¡Œï¼Œå› ä¸ºè¯·æ±‚æ“作ä¾èµ–å¦ä¸€ä¸ªæ“作,但是å¦ä¸€ä¸ªæ“作失败了。 +http.424.reason=失败的ä¾èµ–项 +http.426.desc=æœåŠ¡å™¨æ‹’ç»ä½¿ç”¨å½“å‰å议执行请求,但å¯èƒ½æ„¿æ„在客户端å‡çº§åˆ°å…¶ä»–åè®®åŽæ‰§è¡Œã€‚ +http.426.reason=需è¦å‡çº§ +http.428.desc=原始æœåŠ¡å™¨è¦æ±‚请求是有æ¡ä»¶çš„。 +http.428.reason=è¦æ±‚先决æ¡ä»¶ +http.429.desc=用户在给定的时间内å‘é€äº†å¤ªå¤šè¯·æ±‚(“速率é™åˆ¶â€ï¼‰ +http.429.reason=请求过多 +http.431.desc=æœåŠ¡å™¨ä¸æ„¿æ„处ç†è¯¥è¯·æ±‚,因为它的头字段太大。 +http.431.reason=请求头的字段太大 +http.451.desc=æœåŠ¡å™¨å‡ºäºŽæ³•å¾‹åŽŸå› æ‹’ç»äº†æ­¤è¯·æ±‚。 +http.451.reason=因法律原因无法获得。 +http.500.desc=æœåŠ¡å™¨é‡åˆ°ä¸€ä¸ªæ„外的情况,阻止它完æˆè¯·æ±‚。 +http.500.reason=内部æœåŠ¡å™¨é”™è¯¯ +http.501.desc=æœåŠ¡å™¨ä¸æ”¯æŒå®Œæˆè¯·æ±‚所需的功能。 +http.501.reason=未实现 +http.502.desc=æœåŠ¡å™¨åœ¨å……当网关或代ç†æ—¶, 在å°è¯•å®Œæˆè¯·æ±‚æ—¶, 从它访问的入站æœåŠ¡å™¨æ”¶åˆ°æ— æ•ˆå“应。 +http.502.reason=å网关 +http.503.desc=由于临时过载或计划维护,æœåŠ¡å™¨å½“å‰æ— æ³•å¤„ç†è¯·æ±‚,这å¯èƒ½ä¼šåœ¨ä¸€äº›å»¶è¿ŸåŽå¾—到缓解。 +http.503.reason=æœåŠ¡ä¸å¯ç”¨ã€‚ +http.504.desc=æœåŠ¡å™¨åœ¨å……当网关或代ç†æ—¶ï¼Œæ²¡æœ‰ä»Žä¸Šæ¸¸æœåŠ¡å™¨æŽ¥æ”¶åˆ°å®Œæˆè¯·æ±‚所需访问的åŠæ—¶å“应。 +http.504.reason=网关超时 +http.505.desc=æœåŠ¡å™¨ä¸æ”¯æŒæˆ–æ‹’ç»æ”¯æŒè¯·æ±‚消æ¯ä¸­ä½¿ç”¨çš„主è¦HTTP版本。 +http.505.reason=HTTP 版本ä¸æ”¯æŒ +http.506.desc=æœåŠ¡å™¨å†…部é…置错误:选å–çš„å˜ä½“资æºé…置为自身去处ç†é€æ˜Žçš„内容å商,因此在å商进程中ä¸æ˜¯ä¸€ä¸ªåˆé€‚的终点。 +http.506.reason=å˜ä½“也å商 +http.507.desc=无法对资æºæ‰§è¡Œè¯¥æ–¹æ³•ï¼Œå› ä¸ºæœåŠ¡å™¨æ— æ³•å­˜å‚¨æˆåŠŸå®Œæˆè¯·æ±‚所需的表示。 +http.507.reason=存储空间.ä¸è¶³ +http.508.desc=æœåŠ¡å™¨ç»ˆæ­¢äº†ä¸€ä¸ªæ“作,因为它在处ç†â€œæ·±åº¦ï¼šæ— é™â€è¯·æ±‚æ—¶é‡åˆ°æ— é™å¾ªçŽ¯ +http.508.reason=监测到循环回路 +http.510.desc=请求中未满足访问资æºçš„ç­–ç•¥ +http.510.reason=没有.扩展 +http.511.desc=客户端需è¦è¿›è¡Œèº«ä»½éªŒè¯æ‰èƒ½èŽ·å¾—网络访问æƒé™ã€‚ +http.511.reason=需è¦ç½‘ç»œèº«ä»½éªŒè¯ + +jdbcAccessLogValve.close=无法关闭数æ®åº“。 +jdbcAccessLogValve.exception=执行æ’入访问项时å‘生异常 + +persistentValve.filter.failure=无法编译filter=[{0}] + +remoteCidrValve.invalid=为[{0}]æ供的é…置无效。有关详细信æ¯ï¼Œè¯·å‚阅以å‰çš„æ¶ˆæ¯ +remoteCidrValve.noPort=请求ä¸å«æœ‰åˆæ³•çš„æœåŠ¡å™¨ç«¯å£å·ã€‚请求被拒ç»ã€‚ +remoteCidrValve.noRemoteIp=客户端没有IP地å€ã€‚请求被拒ç»ã€‚ +remoteCidrValve.unexpectedPort=请求包å«æœåŠ¡å™¨ç«¯å£,但连接器é…置属性addConnectorPort为false.请求被拒ç». + +remoteIpValve.invalidHostHeader=在HTTP请求头[{1}]中å‘现Host的无效值[{0}] +remoteIpValve.invalidHostWithPort=HTTP头[{1}]中的主机值[{0}]包å«å°†è¢«å¿½ç•¥çš„端å£å· +remoteIpValve.invalidPortHeader=HTTP标头[{1}]中的端å£æ‰¾åˆ°çš„值[{0}]无效 +remoteIpValve.invalidRemoteAddress=无法确定远程主机,因为å‘布的远程地å€[{0}]是éžæ³•çš„ + +requestFilterValve.configInvalid=为Remote [Addr | Host]阀门æ供了一个或多个无效é…置设置,阻止ValveåŠå…¶çˆ¶å®¹å™¨å¯åŠ¨ +requestFilterValve.deny=æ ¹æ®[{1}]é…置拒ç»[{0}]的请求 + +sslValve.certError=无法处ç†è¯ä¹¦å­—符串[{0}]以创建java.security.cert.X509Certificate对象 +sslValve.invalidProvider=与此[{0}]请求关è”的连接器上指定的SSLæ供程åºæ— æ•ˆã€‚ 无法处ç†è¯ä¹¦æ•°æ®ã€‚ + +stuckThreadDetectionValve.notifyStuckThreadCompleted=线程[{0}](id=[{3}])之å‰æŠ¥å‘Šä¸ºå¡ä½ï¼Œä½†æ˜¯å·²ç»å®Œæˆã€‚它活跃了大概[{1}]毫秒。{2,选择,0#|0< ä»æœ‰[{2}]个被Valve监控的线程å¯èƒ½å¡ä½} +stuckThreadDetectionValve.notifyStuckThreadDetected=线程[{0}](id=[{6}])已处于活动状æ€[{1}]毫秒(自[{2}]起),以便为[{4}]æ供相åŒçš„请求,并且å¯èƒ½è¢«å¡ä½ï¼ˆæ­¤StuckThreadDetectionValveçš„é…置阈值为[{5}]秒)。总共有[{3}]个线程å—此阀监视,å¯èƒ½è¢«å¡ä½ã€‚ +stuckThreadDetectionValve.notifyStuckThreadInterrupted=线程[{0}](id=[{5}])已被中断,因为它在[{1}]毫秒(自[{2}]起)内处于活动状æ€ï¼Œä»¥ä¾¿ä¸º[{3}]æ供相åŒçš„请求,并且å¯èƒ½è¢«å¡ä½ï¼ˆæ­¤StuckThreadDetectionValveçš„é…置中断阈值为[{4}]秒)。 diff --git a/java/org/apache/catalina/valves/PersistentValve.java b/java/org/apache/catalina/valves/PersistentValve.java new file mode 100644 index 0000000..f2f0df2 --- /dev/null +++ b/java/org/apache/catalina/valves/PersistentValve.java @@ -0,0 +1,457 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Globals; +import org.apache.catalina.Host; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.Store; +import org.apache.catalina.StoreManager; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; + +/** + * Valve that implements per-request session persistence. It is intended to be used with non-sticky load-balancers and a + * PersistentManager. The Valve works by loading the session from the Store at the start of the request, the request + * then updates the session as required and the Valve saves the session to the Store at the end of the request. + *

    + * To avoid conflicts and/or errors when updating the session store, each session must only be accessed by no more than + * one concurrent request. The {@code filter} field can be used to define requests (e.g. those for static resources) + * that do not need access to the session and can Requests for resources that do not need to access the session and can + * bypass the session load/save functionality provided by this Valve. + *

    + * The Valve uses a per session {@code Semaphore} to ensure that each session is accessed by no more than one request at + * a time within a single Tomcat instance. The behaviour if multiple requests try to access the session concurrently can + * be controlled by the {@code semaphoreFairness}, {@code semaphoreBlockOnAcquire} and {@code + * semaphoreAcquireUninterruptibly} fields. If a request fails to obtain the Semaphore, the response is generated by the + * {@link #onSemaphoreNotAcquired(Request, Response)} method which, by default, returns a {@code 429} status code. + *

    + * The per session Semaphores only provide limited protection against concurrent requests within a single Tomcat + * instance. If multiple requests access the same session concurrently across different Tomcat instances, update + * conflicts and/or session data loss and/or errors are very likely. + *

    + * USAGE CONSTRAINTS: + *

      + *
    • This Valve must only be used with a PersistentManager
    • + *
    • The client must ensure that no more than one concurrent request accesses a session at any time across all Tomcat + * instances
    • + *
    + */ +public class PersistentValve extends ValveBase { + + // Saves a couple of calls to getClassLoader() on every request. Under high + // load these calls took just long enough to appear as a hot spot (although + // a very minor one) in a profiler. + private static final ClassLoader MY_CLASSLOADER = PersistentValve.class.getClassLoader(); + + private volatile boolean clBindRequired; + + protected Pattern filter = null; + + private ConcurrentMap sessionToSemaphoreMap = new ConcurrentHashMap<>(); + + private boolean semaphoreFairness = true; + + private boolean semaphoreBlockOnAcquire = true; + + private boolean semaphoreAcquireUninterruptibly = true; + + + public PersistentValve() { + super(true); + } + + + @Override + public void setContainer(Container container) { + super.setContainer(container); + if (container instanceof Engine || container instanceof Host) { + clBindRequired = true; + } else { + clBindRequired = false; + } + } + + + /** + * Select the appropriate child Context to process this request, based on the specified request URI. If no matching + * Context can be found, return an appropriate HTTP error. + * + * @param request Request to be processed + * @param response Response to be produced + * + * @exception IOException if an input/output error occurred + * @exception ServletException if a servlet error occurred + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + // request without session + if (isRequestWithoutSession(request.getDecodedRequestURI())) { + if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("persistentValve.requestIgnore", request.getDecodedRequestURI())); + } + getNext().invoke(request, response); + return; + } else if (containerLog.isTraceEnabled()) { + containerLog.trace(sm.getString("persistentValve.requestProcess", request.getDecodedRequestURI())); + } + + // Select the Context to be used for this Request + Context context = request.getContext(); + if (context == null) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, sm.getString("standardHost.noContext")); + return; + } + + String sessionId = request.getRequestedSessionId(); + UsageCountingSemaphore semaphore = null; + boolean mustReleaseSemaphore = true; + + try { + // Acquire the per session semaphore + if (sessionId != null) { + semaphore = sessionToSemaphoreMap.compute(sessionId, + (k, v) -> v == null ? new UsageCountingSemaphore(semaphoreFairness) : v.incrementUsageCount()); + if (semaphoreBlockOnAcquire) { + if (semaphoreAcquireUninterruptibly) { + semaphore.acquireUninterruptibly(); + } else { + try { + semaphore.acquire(); + } catch (InterruptedException e) { + mustReleaseSemaphore = false; + onSemaphoreNotAcquired(request, response); + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("persistentValve.acquireInterrupted", request.getDecodedRequestURI())); + } + return; + } + } + } else { + if (!semaphore.tryAcquire()) { + onSemaphoreNotAcquired(request, response); + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("persistentValve.acquireFailed", request.getDecodedRequestURI())); + } + return; + } + } + } + + // Update the session last access time for our session (if any) + Manager manager = context.getManager(); + if (sessionId != null && manager instanceof StoreManager) { + Store store = ((StoreManager) manager).getStore(); + if (store != null) { + Session session = null; + try { + session = store.load(sessionId); + } catch (Exception e) { + containerLog.error(sm.getString("persistentValve.sessionLoadFail", sessionId)); + } + if (session != null) { + if (!session.isValid() || isSessionStale(session, System.currentTimeMillis())) { + if (containerLog.isTraceEnabled()) { + containerLog.trace("session swapped in is invalid or expired"); + } + session.expire(); + store.remove(sessionId); + } else { + session.setManager(manager); + // session.setId(sessionId); Only if new ??? + manager.add(session); + // ((StandardSession)session).activate(); + session.access(); + session.endAccess(); + } + } + } + } + if (containerLog.isTraceEnabled()) { + containerLog.trace("sessionId: " + sessionId); + } + + // Ask the next valve to process the request. + getNext().invoke(request, response); + + // If still processing async, don't try to store the session + if (!request.isAsync()) { + // Read the sessionid after the response. + // HttpSession hsess = hreq.getSession(false); + Session hsess; + try { + hsess = request.getSessionInternal(false); + } catch (Exception ex) { + hsess = null; + } + String newsessionId = null; + if (hsess != null) { + newsessionId = hsess.getIdInternal(); + } + + if (containerLog.isTraceEnabled()) { + containerLog.trace("newsessionId: " + newsessionId); + } + if (newsessionId != null) { + try { + bind(context); + + /* store the session and remove it from the manager */ + if (manager instanceof StoreManager) { + Session session = manager.findSession(newsessionId); + Store store = ((StoreManager) manager).getStore(); + boolean stored = false; + if (session != null) { + if (store != null && session.isValid() && + !isSessionStale(session, System.currentTimeMillis())) { + store.save(session); + ((StoreManager) manager).removeSuper(session); + session.recycle(); + stored = true; + } + } + if (!stored) { + if (containerLog.isTraceEnabled()) { + containerLog + .trace("newsessionId store: " + store + " session: " + session + + " valid: " + + (session == null ? "N/A" : Boolean.toString(session.isValid())) + + " stale: " + isSessionStale(session, System.currentTimeMillis())); + } + } + } else { + if (containerLog.isTraceEnabled()) { + containerLog.trace("newsessionId Manager: " + manager); + } + } + } finally { + unbind(context); + } + } + } + } finally { + if (semaphore != null) { + if (mustReleaseSemaphore) { + semaphore.release(); + } + sessionToSemaphoreMap.computeIfPresent(sessionId, + (k, v) -> v.decrementAndGetUsageCount() == 0 ? null : v); + } + } + } + + + /** + * Handle the case where a semaphore cannot be obtained. The default behaviour is to return a 429 (too many + * requests) status code. + * + * @param request The request that will not be processed + * @param response The response that will be used for this request + * + * @throws IOException If an I/O error occurs while working with the request or response + */ + protected void onSemaphoreNotAcquired(Request request, Response response) throws IOException { + response.sendError(429); + } + + + /** + * Indicate whether the session has been idle for longer than its expiration date as of the supplied time. + * + * @param session The session to check + * @param timeNow The current time to check for + * + * @return true if the session is past its expiration + */ + protected boolean isSessionStale(Session session, long timeNow) { + + if (session != null) { + int maxInactiveInterval = session.getMaxInactiveInterval(); + if (maxInactiveInterval > 0) { + int timeIdle = (int) (session.getIdleTimeInternal() / 1000L); + if (timeIdle >= maxInactiveInterval) { + return true; + } + } + } + + return false; + } + + + private void bind(Context context) { + if (clBindRequired) { + context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); + } + } + + + private void unbind(Context context) { + if (clBindRequired) { + context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER); + } + } + + protected boolean isRequestWithoutSession(String uri) { + Pattern f = filter; + return f != null && f.matcher(uri).matches(); + } + + public String getFilter() { + if (filter == null) { + return null; + } + return filter.toString(); + } + + public void setFilter(String filter) { + if (filter == null || filter.length() == 0) { + this.filter = null; + } else { + try { + this.filter = Pattern.compile(filter); + } catch (PatternSyntaxException pse) { + container.getLogger().error(sm.getString("persistentValve.filter.failure", filter), pse); + } + } + } + + + /** + * If multiple threads attempt to acquire the same per session Semaphore, will permits be granted in the same order + * they were requested? + * + * @return {@code true} if fairness is enabled, otherwise {@code false} + */ + public boolean isSemaphoreFairness() { + return semaphoreFairness; + } + + + /** + * Configure whether the per session Semaphores will handle granting of permits in the same order they were + * requested if multiple threads attempt to acquire the same Semaphore. + * + * @param semaphoreFairness {@code true} if permits should be granted in the same order they are requested, + * otherwise {@code false} + */ + public void setSemaphoreFairness(boolean semaphoreFairness) { + this.semaphoreFairness = semaphoreFairness; + } + + + /** + * If a thread attempts to acquire the per session Semaphore while it is being used by another request, should the + * thread block to wait for the Semaphore or should the request be rejected? + * + * @return {@code true} if the thread should block, otherwise {@code false} to reject the concurrent request + */ + public boolean isSemaphoreBlockOnAcquire() { + return semaphoreBlockOnAcquire; + } + + + /** + * Configure whether a thread should block and wait for the per session Semaphore or reject the request if the + * Semaphore is being used by another request. + * + * @param semaphoreBlockOnAcquire {@code true} to block, otherwise {@code false} + */ + public void setSemaphoreBlockOnAcquire(boolean semaphoreBlockOnAcquire) { + this.semaphoreBlockOnAcquire = semaphoreBlockOnAcquire; + } + + + /** + * If a thread is blocking to acquire a per session Semaphore, can that thread be interrupted? + * + * @return {@code true} if the thread can not be interrupted, otherwise {@code false}. + */ + public boolean isSemaphoreAcquireUninterruptibly() { + return semaphoreAcquireUninterruptibly; + } + + + /** + * Configure whether a thread blocking to acquire a per session Semaphore can be interrupted. + * + * @param semaphoreAcquireUninterruptibly {@code true} if the thread can not be interrupted, otherwise + * {@code false}. + */ + public void setSemaphoreAcquireUninterruptibly(boolean semaphoreAcquireUninterruptibly) { + this.semaphoreAcquireUninterruptibly = semaphoreAcquireUninterruptibly; + } + + + /* + * The PersistentValve uses a per session semaphore to ensure that only one request accesses a session at a time. To + * limit the size of the session ID to Semaphore map, the Semaphores are created when required and destroyed (made + * eligible for GC) as soon as they are not required. Tracking usage in a thread-safe way requires a usage counter + * that does not block. The Semaphore's internal tracking can't be used because the only way to increment usage is + * via the acquire methods and they block. Therefore, this class was created which uses a separate AtomicLong long + * to track usage. + */ + private static class UsageCountingSemaphore { + private final AtomicLong usageCount = new AtomicLong(1); + private final Semaphore semaphore; + + private UsageCountingSemaphore(boolean fairness) { + semaphore = new Semaphore(1, fairness); + } + + private UsageCountingSemaphore incrementUsageCount() { + usageCount.incrementAndGet(); + return this; + } + + private long decrementAndGetUsageCount() { + return usageCount.decrementAndGet(); + } + + private void acquire() throws InterruptedException { + semaphore.acquire(); + } + + private void acquireUninterruptibly() { + semaphore.acquireUninterruptibly(); + } + + private boolean tryAcquire() { + return semaphore.tryAcquire(); + } + + private void release() { + semaphore.release(); + } + } +} diff --git a/java/org/apache/catalina/valves/ProxyErrorReportValve.java b/java/org/apache/catalina/valves/ProxyErrorReportValve.java new file mode 100644 index 0000000..d6f7243 --- /dev/null +++ b/java/org/apache/catalina/valves/ProxyErrorReportValve.java @@ -0,0 +1,226 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.IOTools; +import org.apache.coyote.ActionCode; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * Implementation of a Valve that proxies or redirects error reporting to other urls. + *

    + *

    + * This Valve should be attached at the Host level, although it will work if attached to a Context. + *

    + */ +public class ProxyErrorReportValve extends ErrorReportValve { + private static final Log log = LogFactory.getLog(ProxyErrorReportValve.class); + + /** + * Use a redirect or proxy the response to the specified location. Default to redirect. + */ + protected boolean useRedirect = true; + + /** + * @return the useRedirect + */ + public boolean getUseRedirect() { + return this.useRedirect; + } + + /** + * @param useRedirect the useRedirect to set + */ + public void setUseRedirect(boolean useRedirect) { + this.useRedirect = useRedirect; + } + + /** + * Use a properties file for the URLs. + */ + protected boolean usePropertiesFile = false; + + /** + * @return the usePropertiesFile + */ + public boolean getUsePropertiesFile() { + return this.usePropertiesFile; + } + + /** + * @param usePropertiesFile the usePropertiesFile to set + */ + public void setUsePropertiesFile(boolean usePropertiesFile) { + this.usePropertiesFile = usePropertiesFile; + } + + private String getRedirectUrl(Response response) { + ResourceBundle resourceBundle = ResourceBundle.getBundle(this.getClass().getSimpleName(), response.getLocale()); + String redirectUrl = null; + try { + redirectUrl = resourceBundle.getString(Integer.toString(response.getStatus())); + } catch (MissingResourceException e) { + // Ignore + } + if (redirectUrl == null) { + try { + redirectUrl = resourceBundle.getString(Integer.toString(0)); + } catch (MissingResourceException ex) { + // Ignore + } + } + return redirectUrl; + } + + @Override + protected void report(Request request, Response response, Throwable throwable) { + + int statusCode = response.getStatus(); + + // Do nothing on a 1xx, 2xx and 3xx status + // Do nothing if anything has been written already + // Do nothing if the response hasn't been explicitly marked as in error + // and that error has not been reported. + if (statusCode < 400 || response.getContentWritten() > 0) { + return; + } + + // If an error has occurred that prevents further I/O, don't waste time + // producing an error report that will never be read + AtomicBoolean result = new AtomicBoolean(false); + response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result); + if (!result.get()) { + return; + } + + String urlString = null; + if (usePropertiesFile) { + urlString = getRedirectUrl(response); + } else { + ErrorPage errorPage = findErrorPage(statusCode, throwable); + if (errorPage != null) { + urlString = errorPage.getLocation(); + } + } + if (urlString == null) { + super.report(request, response, throwable); + return; + } + + // No need to delegate anymore + if (!response.setErrorReported()) { + return; + } + + StringBuilder stringBuilder = new StringBuilder(urlString); + if (urlString.indexOf("?") > -1) { + stringBuilder.append('&'); + } else { + stringBuilder.append('?'); + } + stringBuilder.append("requestUri="); + stringBuilder.append(URLEncoder.encode(request.getDecodedRequestURI(), request.getConnector().getURICharset())); + stringBuilder.append("&statusCode="); + stringBuilder.append(URLEncoder.encode(String.valueOf(statusCode), StandardCharsets.UTF_8)); + + String reason = null; + String description = null; + StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales()); + response.setLocale(smClient.getLocale()); + try { + reason = smClient.getString("http." + statusCode + ".reason"); + description = smClient.getString("http." + statusCode + ".desc"); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + if (reason == null || description == null) { + reason = smClient.getString("errorReportValve.unknownReason"); + description = smClient.getString("errorReportValve.noDescription"); + } + stringBuilder.append("&statusDescription="); + stringBuilder.append(URLEncoder.encode(description, StandardCharsets.UTF_8)); + stringBuilder.append("&statusReason="); + stringBuilder.append(URLEncoder.encode(reason, StandardCharsets.UTF_8)); + + String message = response.getMessage(); + if (message != null) { + stringBuilder.append("&message="); + stringBuilder.append(URLEncoder.encode(message, StandardCharsets.UTF_8)); + } + if (throwable != null) { + stringBuilder.append("&throwable="); + stringBuilder.append(URLEncoder.encode(throwable.toString(), StandardCharsets.UTF_8)); + } + + urlString = stringBuilder.toString(); + if (useRedirect) { + if (log.isTraceEnabled()) { + log.trace("Redirecting error reporting to " + urlString); + } + try { + response.sendRedirect(urlString); + } catch (IOException e) { + // Ignore + } + } else { + if (log.isTraceEnabled()) { + log.trace("Proxying error reporting to " + urlString); + } + HttpURLConnection httpURLConnection = null; + try { + URL url = (new URI(urlString)).toURL(); + httpURLConnection = (HttpURLConnection) url.openConnection(); + httpURLConnection.connect(); + response.setContentType(httpURLConnection.getContentType()); + response.setContentLength(httpURLConnection.getContentLength()); + OutputStream outputStream = response.getOutputStream(); + InputStream inputStream = url.openStream(); + IOTools.flow(inputStream, outputStream); + } catch (URISyntaxException | IOException | IllegalArgumentException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("proxyErrorReportValve.error", urlString), e); + } + // Ignore + } finally { + if (httpURLConnection != null) { + httpURLConnection.disconnect(); + } + } + } + } +} + diff --git a/java/org/apache/catalina/valves/RemoteAddrValve.java b/java/org/apache/catalina/valves/RemoteAddrValve.java new file mode 100644 index 0000000..d48fed2 --- /dev/null +++ b/java/org/apache/catalina/valves/RemoteAddrValve.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.valves; + + +import java.io.IOException; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + * Concrete implementation of RequestFilterValve that filters based on the string representation of the + * remote client's IP address optionally combined with the server connector port number. + * + * @author Craig R. McClanahan + */ +public final class RemoteAddrValve extends RequestFilterValve { + + private static final Log log = LogFactory.getLog(RemoteAddrValve.class); + + + // --------------------------------------------------------- Public Methods + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + String property; + if (getUsePeerAddress()) { + property = request.getPeerAddr(); + } else { + property = request.getRequest().getRemoteAddr(); + } + if (getAddConnectorPort()) { + property = property + ";" + request.getConnector().getPortWithOffset(); + } + process(property, request, response); + } + + + @Override + protected Log getLog() { + return log; + } +} diff --git a/java/org/apache/catalina/valves/RemoteCIDRValve.java b/java/org/apache/catalina/valves/RemoteCIDRValve.java new file mode 100644 index 0000000..7b9638b --- /dev/null +++ b/java/org/apache/catalina/valves/RemoteCIDRValve.java @@ -0,0 +1,251 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.NetMask; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.StringUtils; + +public final class RemoteCIDRValve extends RequestFilterValve { + + /** + * Our logger + */ + private static final Log log = LogFactory.getLog(RemoteCIDRValve.class); + + /** + * The list of allowed {@link NetMask}s + */ + private final List allow = new ArrayList<>(); + + /** + * The list of denied {@link NetMask}s + */ + private final List deny = new ArrayList<>(); + + + public RemoteCIDRValve() { + } + + + /** + * Return a string representation of the {@link NetMask} list in #allow. + * + * @return the #allow list as a string, without the leading '[' and trailing ']' + */ + @Override + public String getAllow() { + return allow.toString().replace("[", "").replace("]", ""); + } + + + /** + * Fill the #allow list with the list of netmasks provided as an argument, if any. Calls #fillFromInput. + * + * @param input The list of netmasks, as a comma separated string + * + * @throws IllegalArgumentException One or more netmasks are invalid + */ + @Override + public void setAllow(final String input) { + final List messages = fillFromInput(input, allow); + + if (messages.isEmpty()) { + return; + } + + allowValid = false; + for (final String message : messages) { + log.error(message); + } + + throw new IllegalArgumentException(sm.getString("remoteCidrValve.invalid", "allow")); + } + + + /** + * Return a string representation of the {@link NetMask} list in #deny. + * + * @return the #deny list as a string, without the leading '[' and trailing ']' + */ + @Override + public String getDeny() { + return deny.toString().replace("[", "").replace("]", ""); + } + + + /** + * Fill the #deny list with the list of netmasks provided as an argument, if any. Calls #fillFromInput. + * + * @param input The list of netmasks, as a comma separated string + * + * @throws IllegalArgumentException One or more netmasks are invalid + */ + @Override + public void setDeny(final String input) { + final List messages = fillFromInput(input, deny); + + if (messages.isEmpty()) { + return; + } + + denyValid = false; + for (final String message : messages) { + log.error(message); + } + + throw new IllegalArgumentException(sm.getString("remoteCidrValve.invalid", "deny")); + } + + + @Override + public void invoke(final Request request, final Response response) throws IOException, ServletException { + String property; + if (getUsePeerAddress()) { + property = request.getPeerAddr(); + } else { + property = request.getRequest().getRemoteAddr(); + } + if (getAddConnectorPort()) { + property = property + ";" + request.getConnector().getPortWithOffset(); + } + process(property, request, response); + } + + @Override + public boolean isAllowed(final String property) { + + final int portIdx = property.indexOf(';'); + final int port; + final String nonPortPart; + + if (portIdx == -1) { + if (getAddConnectorPort()) { + log.error(sm.getString("remoteCidrValve.noPort")); + return false; + } + port = -1; + nonPortPart = property; + } else { + if (!getAddConnectorPort()) { + log.error(sm.getString("remoteCidrValve.unexpectedPort")); + return false; + } + nonPortPart = property.substring(0, portIdx); + try { + port = Integer.parseInt(property.substring(portIdx + 1)); + } catch (NumberFormatException e) { + // This should be in the 'could never happen' category but handle it + // to be safe. + log.error(sm.getString("remoteCidrValve.noPort"), e); + return false; + } + } + + final InetAddress addr; + try { + addr = InetAddress.getByName(nonPortPart); + } catch (UnknownHostException e) { + // This should be in the 'could never happen' category but handle it + // to be safe. + log.error(sm.getString("remoteCidrValve.noRemoteIp"), e); + return false; + } + + for (final NetMask nm : deny) { + if (getAddConnectorPort()) { + if (nm.matches(addr, port)) { + return false; + } + } else { + if (nm.matches(addr)) { + return false; + } + } + } + + for (final NetMask nm : allow) { + if (getAddConnectorPort()) { + if (nm.matches(addr, port)) { + return true; + } + } else { + if (nm.matches(addr)) { + return true; + } + } + } + + // Allow if deny is specified but allow isn't + if (!deny.isEmpty() && allow.isEmpty()) { + return true; + } + + // Deny this request + return false; + } + + + @Override + protected Log getLog() { + return log; + } + + + /** + * Fill a {@link NetMask} list from a string input containing a comma-separated list of (hopefully valid) + * {@link NetMask}s. + * + * @param input The input string + * @param target The list to fill + * + * @return a string list of processing errors (empty when no errors) + */ + + private List fillFromInput(final String input, final List target) { + target.clear(); + if (input == null || input.isEmpty()) { + return Collections.emptyList(); + } + + final List messages = new ArrayList<>(); + NetMask nm; + + for (final String s : StringUtils.splitCommaSeparated(input)) { + try { + nm = new NetMask(s); + target.add(nm); + } catch (IllegalArgumentException e) { + messages.add(s + ": " + e.getMessage()); + } + } + + return Collections.unmodifiableList(messages); + } +} diff --git a/java/org/apache/catalina/valves/RemoteHostValve.java b/java/org/apache/catalina/valves/RemoteHostValve.java new file mode 100644 index 0000000..58d28a1 --- /dev/null +++ b/java/org/apache/catalina/valves/RemoteHostValve.java @@ -0,0 +1,57 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Concrete implementation of RequestFilterValve that filters based on the remote client's host name + * optionally combined with the server connector port number. + * + * @author Craig R. McClanahan + */ +public final class RemoteHostValve extends RequestFilterValve { + + private static final Log log = LogFactory.getLog(RemoteHostValve.class); + + + // --------------------------------------------------------- Public Methods + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + String property; + if (getAddConnectorPort()) { + property = request.getRequest().getRemoteHost() + ";" + request.getConnector().getPortWithOffset(); + } else { + property = request.getRequest().getRemoteHost(); + } + process(property, request, response); + } + + + @Override + protected Log getLog() { + return log; + } +} diff --git a/java/org/apache/catalina/valves/RemoteIpValve.java b/java/org/apache/catalina/valves/RemoteIpValve.java new file mode 100644 index 0000000..857a050 --- /dev/null +++ b/java/org/apache/catalina/valves/RemoteIpValve.java @@ -0,0 +1,952 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Enumeration; +import java.util.regex.Pattern; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Globals; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.Host; + +/** + *

    + * Tomcat port of mod_remoteip, this valve + * replaces the apparent client remote IP address and hostname for the request with the IP address list presented by a + * proxy or a load balancer via a request headers (e.g. "X-Forwarded-For"). + *

    + *

    + * Another feature of this valve is to replace the apparent scheme (http/https) and server port with the scheme + * presented by a proxy or a load balancer via a request header (e.g. "X-Forwarded-Proto"). + *

    + *

    + * This valve proceeds as follows: + *

    + *

    + * If the incoming request.getRemoteAddr() matches the valve's list of internal or trusted proxies: + *

    + *
      + *
    • Loop on the comma delimited list of IPs and hostnames passed by the preceding load balancer or proxy in the given + * request's Http header named $remoteIpHeader (default value x-forwarded-for). Values are + * processed in right-to-left order.
    • + *
    • For each ip/host of the list: + *
        + *
      • if it matches the internal proxies list, the ip/host is swallowed
      • + *
      • if it matches the trusted proxies list, the ip/host is added to the created proxies header
      • + *
      • otherwise, the ip/host is declared to be the remote ip and looping is stopped.
      • + *
      + *
    • + *
    • If the request http header named $protocolHeader (default value X-Forwarded-Proto) + * consists only of forwards that match protocolHeaderHttpsValue configuration parameter (default + * https) then request.isSecure = true, request.scheme = https and + * request.serverPort = 443. Note that 443 can be overwritten with the $httpsServerPort + * configuration parameter.
    • + *
    • Mark the request with the attribute {@link Globals#REQUEST_FORWARDED_ATTRIBUTE} and value {@code Boolean.TRUE} to + * indicate that this request has been forwarded by one or more proxies.
    • + *
    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Configuration parameters
    RemoteIpValve propertyDescriptionEquivalent mod_remoteip directiveFormatDefault Value
    remoteIpHeaderName of the Http Header read by this valve that holds the list of traversed IP addresses starting from the + * requesting clientRemoteIPHeaderCompliant http header namex-forwarded-for
    internalProxiesRegular expression that matches the IP addresses of internal proxies. If they appear in the + * remoteIpHeader value, they will be trusted and will not appear in the proxiesHeader + * valueRemoteIPInternalProxyRegular expression (in the syntax supported by {@link java.util.regex.Pattern java.util.regex})10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}| + * 169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}| + * 100\.6[4-9]{1}\.\d{1,3}\.\d{1,3}|100\.[7-9]{1}\d{1}\.\d{1,3}\.\d{1,3}| + * 100\.1[0-1]{1}\d{1}\.\d{1,3}\.\d{1,3}|100\.12[0-7]{1}\.\d{1,3}\.\d{1,3}| + * 172\.1[6-9]{1}\.\d{1,3}\.\d{1,3}|172\.2[0-9]{1}\.\d{1,3}\.\d{1,3}| 172\.3[0-1]{1}\.\d{1,3}\.\d{1,3}| + * 0:0:0:0:0:0:0:1|::1
    + * By default, 10/8, 192.168/16, 169.254/16, 127/8, 100.64/10, 172.16/12, and ::1 are allowed.
    proxiesHeaderName of the http header created by this valve to hold the list of proxies that have been processed in the + * incoming remoteIpHeaderproxiesHeaderCompliant http header namex-forwarded-by
    trustedProxiesRegular expression that matches the IP addresses of trusted proxies. If they appear in the + * remoteIpHeader value, they will be trusted and will appear in the proxiesHeader valueRemoteIPTrustedProxyRegular expression (in the syntax supported by {@link java.util.regex.Pattern java.util.regex}) 
    protocolHeaderName of the http header read by this valve that holds the flag that this requestN/ACompliant http header name like X-Forwarded-Proto, X-Forwarded-Ssl or + * Front-End-HttpsX-Forwarded-Proto
    protocolHeaderHttpsValueValue of the protocolHeader to indicate that it is an Https requestN/AString like https or ONhttps
    httpServerPortValue returned by {@link jakarta.servlet.ServletRequest#getServerPort()} when the protocolHeader + * indicates http protocolN/Ainteger80
    httpsServerPortValue returned by {@link jakarta.servlet.ServletRequest#getServerPort()} when the protocolHeader + * indicates https protocolN/Ainteger443
    + *

    + * This Valve may be attached to any Container, depending on the granularity of the filtering you wish to perform. + *

    + *

    + * Regular expression vs. IP address blocks: mod_remoteip allows to use address blocks + * (e.g. 192.168/16) to configure RemoteIPInternalProxy and RemoteIPTrustedProxy + * ; as Tomcat doesn't have a library similar to apr_ipsubnet_test, + * RemoteIpValve uses regular expression to configure internalProxies and + * trustedProxies in the same fashion as {@link RequestFilterValve} does. + *

    + *
    + *

    + * Sample with internal proxies + *

    + *

    + * RemoteIpValve configuration: + *

    + * + * <Valve + * className="org.apache.catalina.valves.RemoteIpValve" + * internalProxies="192\.168\.0\.10|192\.168\.0\.11" + * remoteIpHeader="x-forwarded-for" + * proxiesHeader="x-forwarded-by" + * protocolHeader="x-forwarded-proto" + * /> + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Request Values
    propertyValue Before RemoteIpValveValue After RemoteIpValve
    request.remoteAddr192.168.0.10140.211.11.130
    request.header['x-forwarded-for']140.211.11.130, 192.168.0.10null
    request.header['x-forwarded-by']nullnull
    request.header['x-forwarded-proto']httpshttps
    request.schemehttphttps
    request.securefalsetrue
    request.serverPort80443
    + *

    + * Note : x-forwarded-by header is null because only internal proxies as been traversed by the request. + * x-forwarded-by is null because all the proxies are trusted or internal. + *

    + *
    + *

    + * Sample with trusted proxies + *

    + *

    + * RemoteIpValve configuration: + *

    + * + * <Valve + * className="org.apache.catalina.valves.RemoteIpValve" + * internalProxies="192\.168\.0\.10|192\.168\.0\.11" + * remoteIpHeader="x-forwarded-for" + * proxiesHeader="x-forwarded-by" + * trustedProxies="proxy1|proxy2" + * /> + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Request Values
    propertyValue Before RemoteIpValveValue After RemoteIpValve
    request.remoteAddr192.168.0.10140.211.11.130
    request.header['x-forwarded-for']140.211.11.130, proxy1, proxy2null
    request.header['x-forwarded-by']nullproxy1, proxy2
    + *

    + * Note : proxy1 and proxy2 are both trusted proxies that come in x-forwarded-for + * header, they both are migrated in x-forwarded-by header. x-forwarded-by is null because all + * the proxies are trusted or internal. + *

    + *
    + *

    + * Sample with internal and trusted proxies + *

    + *

    + * RemoteIpValve configuration: + *

    + * + * <Valve + * className="org.apache.catalina.valves.RemoteIpValve" + * internalProxies="192\.168\.0\.10|192\.168\.0\.11" + * remoteIpHeader="x-forwarded-for" + * proxiesHeader="x-forwarded-by" + * trustedProxies="proxy1|proxy2" + * /> + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Request Values
    propertyValue Before RemoteIpValveValue After RemoteIpValve
    request.remoteAddr192.168.0.10140.211.11.130
    request.header['x-forwarded-for']140.211.11.130, proxy1, proxy2, 192.168.0.10null
    request.header['x-forwarded-by']nullproxy1, proxy2
    + *

    + * Note : proxy1 and proxy2 are both trusted proxies that come in x-forwarded-for + * header, they both are migrated in x-forwarded-by header. As 192.168.0.10 is an internal + * proxy, it does not appear in x-forwarded-by. x-forwarded-by is null because all the proxies + * are trusted or internal. + *

    + *
    + *

    + * Sample with an untrusted proxy + *

    + *

    + * RemoteIpValve configuration: + *

    + * + * <Valve + * className="org.apache.catalina.valves.RemoteIpValve" + * internalProxies="192\.168\.0\.10|192\.168\.0\.11" + * remoteIpHeader="x-forwarded-for" + * proxiesHeader="x-forwarded-by" + * trustedProxies="proxy1|proxy2" + * /> + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Request Values
    propertyValue Before RemoteIpValveValue After RemoteIpValve
    request.remoteAddr192.168.0.10untrusted-proxy
    request.header['x-forwarded-for']140.211.11.130, untrusted-proxy, proxy1140.211.11.130
    request.header['x-forwarded-by']nullproxy1
    + *

    + * Note : x-forwarded-by holds the trusted proxy proxy1. x-forwarded-by holds + * 140.211.11.130 because untrusted-proxy is not trusted and thus, we cannot trust that + * untrusted-proxy is the actual remote ip. request.remoteAddr is untrusted-proxy + * that is an IP verified by proxy1. + *

    + */ +public class RemoteIpValve extends ValveBase { + /** + * Logger + */ + private static final Log log = LogFactory.getLog(RemoteIpValve.class); + + /** + * Convert a given comma delimited String into an array of String + * + * @param commaDelimitedStrings The string to convert + * + * @return array of String (non code}) + * + * @deprecated Unused. Will be removed in Tomcat 11. + */ + @Deprecated + protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) { + return StringUtils.splitCommaSeparated(commaDelimitedStrings); + } + + private String hostHeader = null; + + private boolean changeLocalName = false; + + /** + * @see #setHttpServerPort(int) + */ + private int httpServerPort = 80; + + /** + * @see #setHttpsServerPort(int) + */ + private int httpsServerPort = 443; + + private String portHeader = null; + + private boolean changeLocalPort = false; + + /** + * @see #setInternalProxies(String) + */ + private Pattern internalProxies = Pattern + .compile("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" + + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + + "100\\.6[4-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + "100\\.[7-9]{1}\\d{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "100\\.1[0-1]{1}\\d{1}\\.\\d{1,3}\\.\\d{1,3}|" + "100\\.12[0-7]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" + + "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|" + "0:0:0:0:0:0:0:1|::1"); + + /** + * @see #setProtocolHeader(String) + */ + private String protocolHeader = "X-Forwarded-Proto"; + + /** + * @see #setProtocolHeaderHttpsValue(String) + */ + private String protocolHeaderHttpsValue = "https"; + + /** + * @see #setProxiesHeader(String) + */ + private String proxiesHeader = "X-Forwarded-By"; + + /** + * @see #setRemoteIpHeader(String) + */ + private String remoteIpHeader = "X-Forwarded-For"; + + /** + * @see #setRequestAttributesEnabled(boolean) + */ + private boolean requestAttributesEnabled = true; + + /** + * @see RemoteIpValve#setTrustedProxies(String) + */ + private Pattern trustedProxies = null; + + + /** + * Default constructor that ensures {@link ValveBase#ValveBase(boolean)} is called with true. + */ + public RemoteIpValve() { + // Async requests are supported with this valve + super(true); + } + + /** + * Obtain the name of the HTTP header used to override the value returned by {@link Request#getServerName()} and + * (optionally depending on {link {@link #isChangeLocalName()} {@link Request#getLocalName()}. + * + * @return The HTTP header name + */ + public String getHostHeader() { + return hostHeader; + } + + /** + * Set the name of the HTTP header used to override the value returned by {@link Request#getServerName()} and + * (optionally depending on {link {@link #isChangeLocalName()} {@link Request#getLocalName()}. + * + * @param hostHeader The HTTP header name + */ + public void setHostHeader(String hostHeader) { + this.hostHeader = hostHeader; + } + + public boolean isChangeLocalName() { + return changeLocalName; + } + + public void setChangeLocalName(boolean changeLocalName) { + this.changeLocalName = changeLocalName; + } + + public int getHttpServerPort() { + return httpServerPort; + } + + public int getHttpsServerPort() { + return httpsServerPort; + } + + /** + * Obtain the name of the HTTP header used to override the value returned by {@link Request#getServerPort()} and + * (optionally depending on {link {@link #isChangeLocalPort()} {@link Request#getLocalPort()}. + * + * @return The HTTP header name + */ + public String getPortHeader() { + return portHeader; + } + + /** + * Set the name of the HTTP header used to override the value returned by {@link Request#getServerPort()} and + * (optionally depending on {link {@link #isChangeLocalPort()} {@link Request#getLocalPort()}. + * + * @param portHeader The HTTP header name + */ + public void setPortHeader(String portHeader) { + this.portHeader = portHeader; + } + + public boolean isChangeLocalPort() { + return changeLocalPort; + } + + public void setChangeLocalPort(boolean changeLocalPort) { + this.changeLocalPort = changeLocalPort; + } + + /** + * @see #setInternalProxies(String) + * + * @return Regular expression that defines the internal proxies + */ + public String getInternalProxies() { + if (internalProxies == null) { + return null; + } + return internalProxies.toString(); + } + + /** + * @see #setProtocolHeader(String) + * + * @return the protocol header (e.g. "X-Forwarded-Proto") + */ + public String getProtocolHeader() { + return protocolHeader; + } + + /** + * @see RemoteIpValve#setProtocolHeaderHttpsValue(String) + * + * @return the value of the protocol header for incoming https request (e.g. "https") + */ + public String getProtocolHeaderHttpsValue() { + return protocolHeaderHttpsValue; + } + + /** + * @see #setProxiesHeader(String) + * + * @return the proxies header name (e.g. "X-Forwarded-By") + */ + public String getProxiesHeader() { + return proxiesHeader; + } + + /** + * @see #setRemoteIpHeader(String) + * + * @return the remote IP header name (e.g. "X-Forwarded-For") + */ + public String getRemoteIpHeader() { + return remoteIpHeader; + } + + /** + * @see #setRequestAttributesEnabled(boolean) + * + * @return true if the attributes will be logged, otherwise false + */ + public boolean getRequestAttributesEnabled() { + return requestAttributesEnabled; + } + + /** + * @see #setTrustedProxies(String) + * + * @return Regular expression that defines the trusted proxies + */ + public String getTrustedProxies() { + if (trustedProxies == null) { + return null; + } + return trustedProxies.toString(); + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + final String originalRemoteAddr = request.getRemoteAddr(); + final String originalRemoteHost = request.getRemoteHost(); + final String originalScheme = request.getScheme(); + final boolean originalSecure = request.isSecure(); + final String originalServerName = request.getServerName(); + final String originalLocalName = isChangeLocalName() ? request.getLocalName() : null; + final int originalServerPort = request.getServerPort(); + final int originalLocalPort = request.getLocalPort(); + final String originalProxiesHeader = request.getHeader(proxiesHeader); + final String originalRemoteIpHeader = request.getHeader(remoteIpHeader); + boolean isInternal = internalProxies != null && internalProxies.matcher(originalRemoteAddr).matches(); + + if (isInternal || (trustedProxies != null && trustedProxies.matcher(originalRemoteAddr).matches())) { + String remoteIp = null; + Deque proxiesHeaderValue = new ArrayDeque<>(); + StringBuilder concatRemoteIpHeaderValue = new StringBuilder(); + + for (Enumeration e = request.getHeaders(remoteIpHeader); e.hasMoreElements();) { + if (concatRemoteIpHeaderValue.length() > 0) { + concatRemoteIpHeaderValue.append(", "); + } + + concatRemoteIpHeaderValue.append(e.nextElement()); + } + + String[] remoteIpHeaderValue = StringUtils.splitCommaSeparated(concatRemoteIpHeaderValue.toString()); + int idx; + if (!isInternal) { + proxiesHeaderValue.addFirst(originalRemoteAddr); + } + // loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain + for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) { + String currentRemoteIp = remoteIpHeaderValue[idx]; + remoteIp = currentRemoteIp; + if (internalProxies != null && internalProxies.matcher(currentRemoteIp).matches()) { + // do nothing, internalProxies IPs are not appended to the + } else if (trustedProxies != null && trustedProxies.matcher(currentRemoteIp).matches()) { + proxiesHeaderValue.addFirst(currentRemoteIp); + } else { + idx--; // decrement idx because break statement doesn't do it + break; + } + } + // continue to loop on remoteIpHeaderValue to build the new value of the remoteIpHeader + Deque newRemoteIpHeaderValue = new ArrayDeque<>(); + for (; idx >= 0; idx--) { + String currentRemoteIp = remoteIpHeaderValue[idx]; + newRemoteIpHeaderValue.addFirst(currentRemoteIp); + } + if (remoteIp != null) { + + request.setRemoteAddr(remoteIp); + if (request.getConnector().getEnableLookups()) { + // This isn't a lazy lookup but that would be a little more + // invasive - mainly in Request.getRemoteHost() - and if + // enableLookups is true it seems reasonable that the + // hotsname will be required so look it up here. + try { + InetAddress inetAddress = InetAddress.getByName(remoteIp); + // We know we need a DNS look up so use getCanonicalHostName() + request.setRemoteHost(inetAddress.getCanonicalHostName()); + } catch (UnknownHostException e) { + log.debug(sm.getString("remoteIpValve.invalidRemoteAddress", remoteIp), e); + request.setRemoteHost(remoteIp); + } + } else { + request.setRemoteHost(remoteIp); + } + + if (proxiesHeaderValue.size() == 0) { + request.getCoyoteRequest().getMimeHeaders().removeHeader(proxiesHeader); + } else { + String commaDelimitedListOfProxies = StringUtils.join(proxiesHeaderValue); + request.getCoyoteRequest().getMimeHeaders().setValue(proxiesHeader) + .setString(commaDelimitedListOfProxies); + } + if (newRemoteIpHeaderValue.size() == 0) { + request.getCoyoteRequest().getMimeHeaders().removeHeader(remoteIpHeader); + } else { + String commaDelimitedRemoteIpHeaderValue = StringUtils.join(newRemoteIpHeaderValue); + request.getCoyoteRequest().getMimeHeaders().setValue(remoteIpHeader) + .setString(commaDelimitedRemoteIpHeaderValue); + } + } + + if (protocolHeader != null) { + String protocolHeaderValue = request.getHeader(protocolHeader); + if (protocolHeaderValue == null) { + // Don't modify the secure, scheme and serverPort attributes + // of the request + } else if (isForwardedProtoHeaderValueSecure(protocolHeaderValue)) { + request.setSecure(true); + request.getCoyoteRequest().scheme().setString("https"); + setPorts(request, httpsServerPort); + } else { + request.setSecure(false); + request.getCoyoteRequest().scheme().setString("http"); + setPorts(request, httpServerPort); + } + } + + if (hostHeader != null) { + String hostHeaderValue = request.getHeader(hostHeader); + if (hostHeaderValue != null) { + try { + int portIndex = Host.parse(hostHeaderValue); + if (portIndex > -1) { + log.debug(sm.getString("remoteIpValve.invalidHostWithPort", hostHeaderValue, hostHeader)); + hostHeaderValue = hostHeaderValue.substring(0, portIndex); + } + + request.getCoyoteRequest().serverName().setString(hostHeaderValue); + if (isChangeLocalName()) { + request.getCoyoteRequest().localName().setString(hostHeaderValue); + } + + } catch (IllegalArgumentException iae) { + log.debug(sm.getString("remoteIpValve.invalidHostHeader", hostHeaderValue, hostHeader)); + } + } + } + + request.setAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE, Boolean.TRUE); + + if (log.isTraceEnabled()) { + log.trace("Incoming request " + request.getRequestURI() + " with originalRemoteAddr [" + + originalRemoteAddr + "], originalRemoteHost=[" + originalRemoteHost + "], originalSecure=[" + + originalSecure + "], originalScheme=[" + originalScheme + "], originalServerName=[" + + originalServerName + "], originalServerPort=[" + originalServerPort + + "] will be seen as newRemoteAddr=[" + request.getRemoteAddr() + "], newRemoteHost=[" + + request.getRemoteHost() + "], newSecure=[" + request.isSecure() + "], newScheme=[" + + request.getScheme() + "], newServerName=[" + request.getServerName() + "], newServerPort=[" + + request.getServerPort() + "]"); + } + } else { + if (log.isTraceEnabled()) { + log.trace("Skip RemoteIpValve for request " + request.getRequestURI() + " with originalRemoteAddr '" + + request.getRemoteAddr() + "'"); + } + } + if (requestAttributesEnabled) { + request.setAttribute(AccessLog.REMOTE_ADDR_ATTRIBUTE, request.getRemoteAddr()); + request.setAttribute(Globals.REMOTE_ADDR_ATTRIBUTE, request.getRemoteAddr()); + request.setAttribute(AccessLog.REMOTE_HOST_ATTRIBUTE, request.getRemoteHost()); + request.setAttribute(AccessLog.PROTOCOL_ATTRIBUTE, request.getProtocol()); + request.setAttribute(AccessLog.SERVER_NAME_ATTRIBUTE, request.getServerName()); + request.setAttribute(AccessLog.SERVER_PORT_ATTRIBUTE, Integer.valueOf(request.getServerPort())); + } + try { + getNext().invoke(request, response); + } finally { + if (!request.isAsync()) { + request.setRemoteAddr(originalRemoteAddr); + request.setRemoteHost(originalRemoteHost); + request.setSecure(originalSecure); + request.getCoyoteRequest().scheme().setString(originalScheme); + request.getCoyoteRequest().serverName().setString(originalServerName); + if (isChangeLocalName()) { + request.getCoyoteRequest().localName().setString(originalLocalName); + } + request.setServerPort(originalServerPort); + request.setLocalPort(originalLocalPort); + + MimeHeaders headers = request.getCoyoteRequest().getMimeHeaders(); + if (originalProxiesHeader == null || originalProxiesHeader.length() == 0) { + headers.removeHeader(proxiesHeader); + } else { + headers.setValue(proxiesHeader).setString(originalProxiesHeader); + } + + if (originalRemoteIpHeader == null || originalRemoteIpHeader.length() == 0) { + headers.removeHeader(remoteIpHeader); + } else { + headers.setValue(remoteIpHeader).setString(originalRemoteIpHeader); + } + } + } + } + + /* + * Considers the value to be secure if it exclusively holds forwards for {@link #protocolHeaderHttpsValue}. + */ + private boolean isForwardedProtoHeaderValueSecure(String protocolHeaderValue) { + if (!protocolHeaderValue.contains(",")) { + return protocolHeaderHttpsValue.equalsIgnoreCase(protocolHeaderValue); + } + String[] forwardedProtocols = StringUtils.splitCommaSeparated(protocolHeaderValue); + if (forwardedProtocols.length == 0) { + return false; + } + for (String forwardedProtocol : forwardedProtocols) { + if (!protocolHeaderHttpsValue.equalsIgnoreCase(forwardedProtocol)) { + return false; + } + } + return true; + } + + private void setPorts(Request request, int defaultPort) { + int port = defaultPort; + if (portHeader != null) { + String portHeaderValue = request.getHeader(portHeader); + if (portHeaderValue != null) { + try { + port = Integer.parseInt(portHeaderValue); + } catch (NumberFormatException nfe) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("remoteIpValve.invalidPortHeader", portHeaderValue, portHeader), nfe); + } + } + } + } + request.setServerPort(port); + if (changeLocalPort) { + request.setLocalPort(port); + } + } + + /** + *

    + * Server Port value if the {@link #protocolHeader} is not null and does not indicate HTTP + *

    + *

    + * Default value : 80 + *

    + * + * @param httpServerPort The server port + */ + public void setHttpServerPort(int httpServerPort) { + this.httpServerPort = httpServerPort; + } + + /** + *

    + * Server Port value if the {@link #protocolHeader} indicates HTTPS + *

    + *

    + * Default value : 443 + *

    + * + * @param httpsServerPort The server port + */ + public void setHttpsServerPort(int httpsServerPort) { + this.httpsServerPort = httpsServerPort; + } + + /** + *

    + * Regular expression that defines the internal proxies. + *

    + *

    + * Default value : + * 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254.\d{1,3}.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1 + *

    + * + * @param internalProxies The proxy regular expression + */ + public void setInternalProxies(String internalProxies) { + if (internalProxies == null || internalProxies.length() == 0) { + this.internalProxies = null; + } else { + this.internalProxies = Pattern.compile(internalProxies); + } + } + + /** + *

    + * Header that holds the incoming protocol, usually named X-Forwarded-Proto. If null, + * request.scheme and request.secure will not be modified. + *

    + *

    + * Default value : X-Forwarded-Proto + *

    + * + * @param protocolHeader The header name + */ + public void setProtocolHeader(String protocolHeader) { + this.protocolHeader = protocolHeader; + } + + /** + *

    + * Case insensitive value of the protocol header to indicate that the incoming http request uses SSL. + *

    + *

    + * Default value : https + *

    + * + * @param protocolHeaderHttpsValue The header name + */ + public void setProtocolHeaderHttpsValue(String protocolHeaderHttpsValue) { + this.protocolHeaderHttpsValue = protocolHeaderHttpsValue; + } + + /** + *

    + * The proxiesHeader directive specifies a header into which mod_remoteip will collect a list of all of the + * intermediate client IP addresses trusted to resolve the actual remote IP. Note that intermediate + * RemoteIPTrustedProxy addresses are recorded in this header, while any intermediate RemoteIPInternalProxy + * addresses are discarded. + *

    + *

    + * Name of the http header that holds the list of trusted proxies that has been traversed by the http request. + *

    + *

    + * The value of this header can be comma delimited. + *

    + *

    + * Default value : X-Forwarded-By + *

    + * + * @param proxiesHeader The header name + */ + public void setProxiesHeader(String proxiesHeader) { + this.proxiesHeader = proxiesHeader; + } + + /** + *

    + * Name of the http header from which the remote ip is extracted. + *

    + *

    + * The value of this header can be comma delimited. + *

    + *

    + * Default value : X-Forwarded-For + *

    + * + * @param remoteIpHeader The header name + */ + public void setRemoteIpHeader(String remoteIpHeader) { + this.remoteIpHeader = remoteIpHeader; + } + + /** + * Should this valve set request attributes for IP address, Hostname, protocol and port used for the request? This + * are typically used in conjunction with the {@link AccessLog} which will otherwise log the original values. + * Default is true. The attributes set are: + *
      + *
    • org.apache.catalina.AccessLog.RemoteAddr
    • + *
    • org.apache.catalina.AccessLog.RemoteHost
    • + *
    • org.apache.catalina.AccessLog.Protocol
    • + *
    • org.apache.catalina.AccessLog.ServerPort
    • + *
    • org.apache.tomcat.remoteAddr
    • + *
    + * + * @param requestAttributesEnabled true causes the attributes to be set, false disables + * the setting of the attributes. + */ + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + this.requestAttributesEnabled = requestAttributesEnabled; + } + + /** + *

    + * Regular expression defining proxies that are trusted when they appear in the {@link #remoteIpHeader} header. + *

    + *

    + * Default value : empty list, no external proxy is trusted. + *

    + * + * @param trustedProxies The regular expression + */ + public void setTrustedProxies(String trustedProxies) { + if (trustedProxies == null || trustedProxies.length() == 0) { + this.trustedProxies = null; + } else { + this.trustedProxies = Pattern.compile(trustedProxies); + } + } +} diff --git a/java/org/apache/catalina/valves/RequestFilterValve.java b/java/org/apache/catalina/valves/RequestFilterValve.java new file mode 100644 index 0000000..b42f833 --- /dev/null +++ b/java/org/apache/catalina/valves/RequestFilterValve.java @@ -0,0 +1,430 @@ +/* + * 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.catalina.valves; + + +import java.io.IOException; +import java.util.regex.Pattern; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; + +/** + * Implementation of a Valve that performs filtering based on comparing the appropriate request property (selected based + * on which subclass you choose to configure into your Container's pipeline) against the regular expressions configured + * for this Valve. + *

    + * This valve is configured by setting the allow and/or deny properties to a regular + * expressions (in the syntax supported by {@link Pattern}) to which the appropriate request property will be compared. + * Evaluation proceeds as follows: + *

      + *
    • The subclass extracts the request property to be filtered, and calls the common process() method. + *
    • If there is a deny expression configured, the property will be compared to the expression. If a match is found, + * this request will be rejected with a "Forbidden" HTTP response.
    • + *
    • If there is a allow expression configured, the property will be compared to each such expression. If a match is + * found, this request will be allowed to pass through to the next Valve in the current pipeline.
    • + *
    • If a deny expression was specified but no allow expression, allow this request to pass through (because none of + * the deny expressions matched it). + *
    • The request will be rejected with a "Forbidden" HTTP response.
    • + *
    + *

    + * As an option the valve can generate an invalid authenticate header instead of denying the request. This + * can be combined with the context attribute preemptiveAuthentication="true" and an authenticator to force + * authentication instead of denial. + *

    + * This Valve may be attached to any Container, depending on the granularity of the filtering you wish to perform. + * + * @author Craig R. McClanahan + */ +public abstract class RequestFilterValve extends ValveBase { + + // ------------------------------------------------------ Constructor + public RequestFilterValve() { + super(true); + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The regular expression used to test for allowed requests. + */ + protected volatile Pattern allow = null; + + + /** + * The current allow configuration value that may or may not compile into a valid {@link Pattern}. + */ + protected volatile String allowValue = null; + + + /** + * Helper variable to catch configuration errors. It is true by default, but becomes false + * if there was an attempt to assign an invalid value to the allow pattern. + */ + protected volatile boolean allowValid = true; + + + /** + * The regular expression used to test for denied requests. + */ + protected volatile Pattern deny = null; + + + /** + * The current deny configuration value that may or may not compile into a valid {@link Pattern}. + */ + protected volatile String denyValue = null; + + + /** + * Helper variable to catch configuration errors. It is true by default, but becomes false + * if there was an attempt to assign an invalid value to the deny pattern. + */ + protected volatile boolean denyValid = true; + + + /** + * The HTTP response status code that is used when rejecting denied request. It is 403 by default, but may be + * changed to be 404. + */ + protected int denyStatus = HttpServletResponse.SC_FORBIDDEN; + + /** + *

    + * If invalidAuthenticationWhenDeny is true and the context has preemptiveAuthentication + * set, set an invalid authorization header to trigger basic auth instead of denying the request.. + */ + private boolean invalidAuthenticationWhenDeny = false; + + /** + * Flag deciding whether we add the server connector port to the property compared in the filtering method. The port + * will be appended using a ";" as a separator. + */ + private volatile boolean addConnectorPort = false; + + /** + * Flag deciding whether we use the connection peer address or the remote address. This makes a dfifference when + * using AJP or the RemoteIpValve. + */ + private volatile boolean usePeerAddress = false; + + // ------------------------------------------------------------- Properties + + + /** + * Return the regular expression used to test for allowed requests for this Valve, if any; otherwise, return + * null. + * + * @return the regular expression + */ + public String getAllow() { + return allowValue; + } + + + /** + * Set the regular expression used to test for allowed requests for this Valve, if any. + * + * @param allow The new allow expression + */ + public void setAllow(String allow) { + if (allow == null || allow.length() == 0) { + this.allow = null; + allowValue = null; + allowValid = true; + } else { + boolean success = false; + try { + allowValue = allow; + this.allow = Pattern.compile(allow); + success = true; + } finally { + allowValid = success; + } + } + } + + + /** + * Return the regular expression used to test for denied requests for this Valve, if any; otherwise, return + * null. + * + * @return the regular expression + */ + public String getDeny() { + return denyValue; + } + + + /** + * Set the regular expression used to test for denied requests for this Valve, if any. + * + * @param deny The new deny expression + */ + public void setDeny(String deny) { + if (deny == null || deny.length() == 0) { + this.deny = null; + denyValue = null; + denyValid = true; + } else { + boolean success = false; + try { + denyValue = deny; + this.deny = Pattern.compile(deny); + success = true; + } finally { + denyValid = success; + } + } + } + + + /** + * Returns {@code false} if the last change to the {@code allow} pattern did not apply successfully. E.g. if the + * pattern is syntactically invalid. + * + * @return false if the current pattern is invalid + */ + public final boolean isAllowValid() { + return allowValid; + } + + + /** + * Returns {@code false} if the last change to the {@code deny} pattern did not apply successfully. E.g. if the + * pattern is syntactically invalid. + * + * @return false if the current pattern is invalid + */ + public final boolean isDenyValid() { + return denyValid; + } + + + /** + * @return response status code that is used to reject denied request. + */ + public int getDenyStatus() { + return denyStatus; + } + + + /** + * Set response status code that is used to reject denied request. + * + * @param denyStatus The status code + */ + public void setDenyStatus(int denyStatus) { + this.denyStatus = denyStatus; + } + + + /** + * @return true if a deny is handled by setting an invalid auth header. + */ + public boolean getInvalidAuthenticationWhenDeny() { + return invalidAuthenticationWhenDeny; + } + + + /** + * Set invalidAuthenticationWhenDeny property. + * + * @param value true to handle a deny by setting an invalid auth header + */ + public void setInvalidAuthenticationWhenDeny(boolean value) { + invalidAuthenticationWhenDeny = value; + } + + + /** + * Get the flag deciding whether we add the server connector port to the property compared in the filtering method. + * The port will be appended using a ";" as a separator. + * + * @return true to add the connector port + */ + public boolean getAddConnectorPort() { + return addConnectorPort; + } + + + /** + * Set the flag deciding whether we add the server connector port to the property compared in the filtering method. + * The port will be appended using a ";" as a separator. + * + * @param addConnectorPort The new flag + */ + public void setAddConnectorPort(boolean addConnectorPort) { + this.addConnectorPort = addConnectorPort; + } + + + /** + * Get the flag deciding whether we use the connection peer address or the remote address. This makes a dfifference + * when using AJP or the RemoteIpValve. + * + * @return true if we use the connection peer address + */ + public boolean getUsePeerAddress() { + return usePeerAddress; + } + + + /** + * Set the flag deciding whether we use the connection peer address or the remote address. This makes a dfifference + * when using AJP or the RemoteIpValve. + * + * @param usePeerAddress The new flag + */ + public void setUsePeerAddress(boolean usePeerAddress) { + this.usePeerAddress = usePeerAddress; + } + + // --------------------------------------------------------- Public Methods + + /** + * Extract the desired request property, and pass it (along with the specified request and response objects) to the + * protected process() method to perform the actual filtering. This method must be implemented by a + * concrete subclass. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public abstract void invoke(Request request, Response response) throws IOException, ServletException; + + + // ------------------------------------------------------ Protected Methods + + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + if (!allowValid || !denyValid) { + throw new LifecycleException(sm.getString("requestFilterValve.configInvalid")); + } + } + + + @Override + protected void startInternal() throws LifecycleException { + if (!allowValid || !denyValid) { + throw new LifecycleException(sm.getString("requestFilterValve.configInvalid")); + } + super.startInternal(); + } + + + /** + * Perform the filtering that has been configured for this Valve, matching against the specified request property. + * + * @param property The request property on which to filter + * @param request The servlet request to be processed + * @param response The servlet response to be processed + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + protected void process(String property, Request request, Response response) throws IOException, ServletException { + + if (isAllowed(property)) { + getNext().invoke(request, response); + return; + } + + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("requestFilterValve.deny", request.getRequestURI(), property)); + } + + // Deny this request + denyRequest(request, response); + } + + + protected abstract Log getLog(); + + + /** + * Reject the request that was denied by this valve. + *

    + * If invalidAuthenticationWhenDeny is true and the context has preemptiveAuthentication + * set, set an invalid authorization header to trigger basic auth. + * + * @param request The servlet request to be processed + * @param response The servlet response to be processed + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + protected void denyRequest(Request request, Response response) throws IOException, ServletException { + if (invalidAuthenticationWhenDeny) { + Context context = request.getContext(); + if (context != null && context.getPreemptiveAuthentication()) { + if (request.getCoyoteRequest().getMimeHeaders().getValue("authorization") == null) { + request.getCoyoteRequest().getMimeHeaders().addValue("authorization").setString("invalid"); + } + getNext().invoke(request, response); + return; + } + } + response.sendError(denyStatus); + } + + + /** + * Perform the test implemented by this Valve, matching against the specified request property value. This method is + * public so that it can be called through JMX, e.g. to test whether certain IP address is allowed or denied by the + * valve configuration. + * + * @param property The request property value on which to filter + * + * @return true if the request is allowed + */ + public boolean isAllowed(String property) { + // Use local copies for thread safety + Pattern deny = this.deny; + Pattern allow = this.allow; + + // Check the deny patterns, if any + if (deny != null && deny.matcher(property).matches()) { + return false; + } + + // Check the allow patterns, if any + if (allow != null && allow.matcher(property).matches()) { + return true; + } + + // Allow if denies specified but not allows + if (deny != null && allow == null) { + return true; + } + + // Deny this request + return false; + } +} diff --git a/java/org/apache/catalina/valves/SSLValve.java b/java/org/apache/catalina/valves/SSLValve.java new file mode 100644 index 0000000..2daeb5e --- /dev/null +++ b/java/org/apache/catalina/valves/SSLValve.java @@ -0,0 +1,195 @@ +/* + * 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.catalina.valves; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.Globals; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.UDecoder; + +/** + * When using mod_proxy_http, the client SSL information is not included in the protocol (unlike mod_jk and + * mod_proxy_ajp). To make the client SSL information available to Tomcat, some additional configuration is required. In + * httpd, mod_headers is used to add the SSL information as HTTP headers. In Tomcat, this valve is used to read the + * information from the HTTP headers and insert it into the request. + *

    + * Note: Ensure that the headers are always set by httpd for all requests to prevent a client spoofing SSL + * information by sending fake headers. + *

    + * In httpd.conf add the following: + * + *

    + * <IfModule ssl_module>
    + *   RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s"
    + *   RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s"
    + *   RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s"
    + *   RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s"
    + * </IfModule>
    + * 
    + * + * In server.xml, configure this valve under the Engine element in server.xml: + * + *
    + * <Engine ...>
    + *   <Valve className="org.apache.catalina.valves.SSLValve" />
    + *   <Host ... />
    + * </Engine>
    + * 
    + */ +public class SSLValve extends ValveBase { + + private static final Log log = LogFactory.getLog(SSLValve.class); + + private String sslClientCertHeader = "ssl_client_cert"; + private String sslClientEscapedCertHeader = "ssl_client_escaped_cert"; + private String sslCipherHeader = "ssl_cipher"; + private String sslSessionIdHeader = "ssl_session_id"; + private String sslCipherUserKeySizeHeader = "ssl_cipher_usekeysize"; + + // ------------------------------------------------------ Constructor + public SSLValve() { + super(true); + } + + + public String getSslClientCertHeader() { + return sslClientCertHeader; + } + + public void setSslClientCertHeader(String sslClientCertHeader) { + this.sslClientCertHeader = sslClientCertHeader; + } + + public String getSslClientEscapedCertHeader() { + return sslClientEscapedCertHeader; + } + + public void setSslClientEscapedCertHeader(String sslClientEscapedCertHeader) { + this.sslClientEscapedCertHeader = sslClientEscapedCertHeader; + } + + public String getSslCipherHeader() { + return sslCipherHeader; + } + + public void setSslCipherHeader(String sslCipherHeader) { + this.sslCipherHeader = sslCipherHeader; + } + + public String getSslSessionIdHeader() { + return sslSessionIdHeader; + } + + public void setSslSessionIdHeader(String sslSessionIdHeader) { + this.sslSessionIdHeader = sslSessionIdHeader; + } + + public String getSslCipherUserKeySizeHeader() { + return sslCipherUserKeySizeHeader; + } + + public void setSslCipherUserKeySizeHeader(String sslCipherUserKeySizeHeader) { + this.sslCipherUserKeySizeHeader = sslCipherUserKeySizeHeader; + } + + + public String mygetHeader(Request request, String header) { + String strcert0 = request.getHeader(header); + if (strcert0 == null) { + return null; + } + /* mod_header writes "(null)" when the ssl variable is no filled */ + if ("(null)".equals(strcert0)) { + return null; + } + return strcert0; + } + + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + /* + * Known behaviours of reverse proxies that are handled by the processing below: - mod_header converts the '\n' + * into ' ' - nginx converts the '\n' into multiple ' ' - nginx ssl_client_escaped_cert uses "uri component" + * escaping, keeping only ALPHA, DIGIT, "-", ".", "_", "~" + * + * The code assumes that the trimmed header value starts with '-----BEGIN CERTIFICATE-----' and ends with + * '-----END CERTIFICATE-----'. + * + * Note: As long as the BEGIN marker and the rest of the content are on separate lines, the CertificateFactory + * is tolerant of any additional whitespace. + */ + String headerValue; + String headerEscapedValue = mygetHeader(request, sslClientEscapedCertHeader); + if (headerEscapedValue != null) { + headerValue = UDecoder.URLDecode(headerEscapedValue, null); + } else { + headerValue = mygetHeader(request, sslClientCertHeader); + } + if (headerValue != null) { + headerValue = headerValue.trim(); + if (headerValue.length() > 27) { + String body = headerValue.substring(27); + String header = "-----BEGIN CERTIFICATE-----\n"; + String strcerts = header.concat(body); + ByteArrayInputStream bais = new ByteArrayInputStream(strcerts.getBytes(StandardCharsets.ISO_8859_1)); + X509Certificate jsseCerts[] = null; + String providerName = (String) request.getConnector().getProperty("clientCertProvider"); + try { + CertificateFactory cf; + if (providerName == null) { + cf = CertificateFactory.getInstance("X.509"); + } else { + cf = CertificateFactory.getInstance("X.509", providerName); + } + X509Certificate cert = (X509Certificate) cf.generateCertificate(bais); + jsseCerts = new X509Certificate[1]; + jsseCerts[0] = cert; + } catch (java.security.cert.CertificateException e) { + log.warn(sm.getString("sslValve.certError", strcerts), e); + } catch (NoSuchProviderException e) { + log.error(sm.getString("sslValve.invalidProvider", providerName), e); + } + request.setAttribute(Globals.CERTIFICATES_ATTR, jsseCerts); + } + } + headerValue = mygetHeader(request, sslCipherHeader); + if (headerValue != null) { + request.setAttribute(Globals.CIPHER_SUITE_ATTR, headerValue); + } + headerValue = mygetHeader(request, sslSessionIdHeader); + if (headerValue != null) { + request.setAttribute(Globals.SSL_SESSION_ID_ATTR, headerValue); + } + headerValue = mygetHeader(request, sslCipherUserKeySizeHeader); + if (headerValue != null) { + request.setAttribute(Globals.KEY_SIZE_ATTR, Integer.valueOf(headerValue)); + } + getNext().invoke(request, response); + } +} diff --git a/java/org/apache/catalina/valves/SemaphoreValve.java b/java/org/apache/catalina/valves/SemaphoreValve.java new file mode 100644 index 0000000..6b4b583 --- /dev/null +++ b/java/org/apache/catalina/valves/SemaphoreValve.java @@ -0,0 +1,237 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.util.concurrent.Semaphore; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; + + +/** + *

    + * Implementation of a Valve that limits concurrency. + *

    + *

    + * This Valve may be attached to any Container, depending on the granularity of the concurrency control you wish to + * perform. Note that internally, some async requests may require multiple serial requests to complete what - to the + * user - appears as a single request. + *

    + * + * @author Remy Maucherat + */ +public class SemaphoreValve extends ValveBase { + + // ------------------------------------------------------ Constructor + public SemaphoreValve() { + super(true); + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Semaphore. + */ + protected Semaphore semaphore = null; + + + // ------------------------------------------------------------- Properties + + + /** + * Concurrency level of the semaphore. + */ + protected int concurrency = 10; + + public int getConcurrency() { + return concurrency; + } + + public void setConcurrency(int concurrency) { + this.concurrency = concurrency; + } + + + /** + * Fairness of the semaphore. + */ + protected boolean fairness = false; + + public boolean getFairness() { + return fairness; + } + + public void setFairness(boolean fairness) { + this.fairness = fairness; + } + + + /** + * Block until a permit is available. + */ + protected boolean block = true; + + public boolean getBlock() { + return block; + } + + public void setBlock(boolean block) { + this.block = block; + } + + + /** + * Block interruptibly until a permit is available. + */ + protected boolean interruptible = false; + + public boolean getInterruptible() { + return interruptible; + } + + public void setInterruptible(boolean interruptible) { + this.interruptible = interruptible; + } + + + /** + * High concurrency status. This status code is returned as an + * error if concurrency is too high. + */ + protected int highConcurrencyStatus = -1; + + public int getHighConcurrencyStatus() { + return this.highConcurrencyStatus; + } + + public void setHighConcurrencyStatus(int highConcurrencyStatus) { + this.highConcurrencyStatus = highConcurrencyStatus; + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + semaphore = new Semaphore(concurrency, fairness); + super.startInternal(); + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + semaphore = null; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Do concurrency control on the request using the semaphore. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + if (controlConcurrency(request, response)) { + boolean shouldRelease = true; + try { + if (block) { + if (interruptible) { + try { + semaphore.acquire(); + } catch (InterruptedException e) { + shouldRelease = false; + permitDenied(request, response); + return; + } + } else { + semaphore.acquireUninterruptibly(); + } + } else { + if (!semaphore.tryAcquire()) { + shouldRelease = false; + permitDenied(request, response); + return; + } + } + getNext().invoke(request, response); + } finally { + if (shouldRelease) { + semaphore.release(); + } + } + } else { + getNext().invoke(request, response); + } + + } + + + /** + * Subclass friendly method to add conditions. + * + * @param request The Servlet request + * @param response The Servlet response + * + * @return true if the concurrency control should occur on this request + */ + public boolean controlConcurrency(Request request, Response response) { + return true; + } + + + /** + * Subclass friendly method to add error handling when a permit isn't granted. + * + * @param request The Servlet request + * @param response The Servlet response + * + * @throws IOException Error writing output + * @throws ServletException Other error + */ + public void permitDenied(Request request, Response response) throws IOException, ServletException { + if (highConcurrencyStatus > 0) { + response.sendError(highConcurrencyStatus); + } + } + + +} diff --git a/java/org/apache/catalina/valves/StuckThreadDetectionValve.java b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java new file mode 100644 index 0000000..a523029 --- /dev/null +++ b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java @@ -0,0 +1,396 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * This valve allows to detect requests that take a long time to process, which might indicate that the thread that is + * processing it is stuck. + */ +public class StuckThreadDetectionValve extends ValveBase { + + /** + * Logger + */ + private static final Log log = LogFactory.getLog(StuckThreadDetectionValve.class); + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(Constants.Package); + + /** + * Keeps count of the number of stuck threads detected + */ + private final AtomicInteger stuckCount = new AtomicInteger(0); + + /** + * Keeps count of the number of stuck threads that have been interrupted + */ + private AtomicLong interruptedThreadsCount = new AtomicLong(); + + /** + * In seconds. Default 600 (10 minutes). + */ + private int threshold = 600; + + /** + * In seconds. Default is -1 to disable interruption. + */ + private int interruptThreadThreshold; + + /** + * The only references we keep to actual running Thread objects are in this Map (which is automatically cleaned in + * invoke()s finally clause). That way, Threads can be GC'ed, even though the Valve still thinks they are stuck + * (caused by a long monitor interval) + */ + private final Map activeThreads = new ConcurrentHashMap<>(); + + private final Queue completedStuckThreadsQueue = new ConcurrentLinkedQueue<>(); + + /** + * Specifies the threshold (in seconds) used when checking for stuck threads. If <=0, the detection is disabled. + * The default is 600 seconds. + * + * @param threshold The new threshold in seconds + */ + public void setThreshold(int threshold) { + this.threshold = threshold; + } + + /** + * @see #setThreshold(int) + * + * @return The current threshold in seconds + */ + public int getThreshold() { + return threshold; + } + + + public int getInterruptThreadThreshold() { + return interruptThreadThreshold; + } + + /** + * Specifies the threshold (in seconds) before stuck threads are interrupted. If <=0, the interruption is + * disabled. The default is -1. If >=0, the value must actually be >= threshold. + * + * @param interruptThreadThreshold The new thread interruption threshold in seconds + */ + public void setInterruptThreadThreshold(int interruptThreadThreshold) { + this.interruptThreadThreshold = interruptThreadThreshold; + } + + /** + * Required to enable async support. + */ + public StuckThreadDetectionValve() { + super(true); + } + + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + + if (log.isTraceEnabled()) { + log.trace("Monitoring stuck threads with threshold = " + threshold + " sec"); + } + } + + private void notifyStuckThreadDetected(MonitoredThread monitoredThread, long activeTime, int numStuckThreads) { + if (log.isWarnEnabled()) { + String msg = sm.getString("stuckThreadDetectionValve.notifyStuckThreadDetected", + monitoredThread.getThread().getName(), Long.valueOf(activeTime), monitoredThread.getStartTime(), + Integer.valueOf(numStuckThreads), monitoredThread.getRequestUri(), Integer.valueOf(threshold), + String.valueOf(monitoredThread.getThread().getId())); + // msg += "\n" + getStackTraceAsString(trace); + Throwable th = new Throwable(); + th.setStackTrace(monitoredThread.getThread().getStackTrace()); + log.warn(msg, th); + } + } + + private void notifyStuckThreadCompleted(CompletedStuckThread thread, int numStuckThreads) { + if (log.isWarnEnabled()) { + String msg = sm.getString("stuckThreadDetectionValve.notifyStuckThreadCompleted", thread.getName(), + Long.valueOf(thread.getTotalActiveTime()), Integer.valueOf(numStuckThreads), + String.valueOf(thread.getId())); + // Since the "stuck thread notification" is warn, this should also + // be warn + log.warn(msg); + } + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + if (threshold <= 0) { + // short-circuit if not monitoring stuck threads + getNext().invoke(request, response); + return; + } + + // Save the thread/runnable + // Keeping a reference to the thread object here does not prevent + // GC'ing, as the reference is removed from the Map in the finally clause + + Thread currentThread = Thread.currentThread(); + Long key = Long.valueOf(currentThread.getId()); + StringBuffer requestUrl = request.getRequestURL(); + if (request.getQueryString() != null) { + requestUrl.append('?'); + requestUrl.append(request.getQueryString()); + } + MonitoredThread monitoredThread = new MonitoredThread(currentThread, requestUrl.toString(), + interruptThreadThreshold > 0); + activeThreads.put(key, monitoredThread); + + try { + getNext().invoke(request, response); + } finally { + activeThreads.remove(key); + if (monitoredThread.markAsDone() == MonitoredThreadState.STUCK) { + if (monitoredThread.wasInterrupted()) { + interruptedThreadsCount.incrementAndGet(); + } + completedStuckThreadsQueue.add( + new CompletedStuckThread(monitoredThread.getThread(), monitoredThread.getActiveTimeInMillis())); + } + } + } + + @Override + public void backgroundProcess() { + super.backgroundProcess(); + + long thresholdInMillis = threshold * 1000L; + + // Check monitored threads, being careful that the request might have + // completed by the time we examine it + for (MonitoredThread monitoredThread : activeThreads.values()) { + long activeTime = monitoredThread.getActiveTimeInMillis(); + + if (activeTime >= thresholdInMillis && monitoredThread.markAsStuckIfStillRunning()) { + int numStuckThreads = stuckCount.incrementAndGet(); + notifyStuckThreadDetected(monitoredThread, activeTime, numStuckThreads); + } + if (interruptThreadThreshold > 0 && activeTime >= interruptThreadThreshold * 1000L) { + monitoredThread.interruptIfStuck(interruptThreadThreshold); + } + } + // Check if any threads previously reported as stuck, have finished. + for (CompletedStuckThread completedStuckThread = completedStuckThreadsQueue + .poll(); completedStuckThread != null; completedStuckThread = completedStuckThreadsQueue.poll()) { + + int numStuckThreads = stuckCount.decrementAndGet(); + notifyStuckThreadCompleted(completedStuckThread, numStuckThreads); + } + } + + public int getStuckThreadCount() { + return stuckCount.get(); + } + + public long[] getStuckThreadIds() { + List idList = new ArrayList<>(); + for (MonitoredThread monitoredThread : activeThreads.values()) { + if (monitoredThread.isMarkedAsStuck()) { + idList.add(Long.valueOf(monitoredThread.getThread().getId())); + } + } + + long[] result = new long[idList.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = idList.get(i).longValue(); + } + return result; + } + + public String[] getStuckThreadNames() { + List nameList = new ArrayList<>(); + for (MonitoredThread monitoredThread : activeThreads.values()) { + if (monitoredThread.isMarkedAsStuck()) { + nameList.add(monitoredThread.getThread().getName()); + } + } + return nameList.toArray(new String[0]); + } + + public long getInterruptedThreadsCount() { + return interruptedThreadsCount.get(); + } + + + private static class MonitoredThread { + + /** + * Reference to the thread to get a stack trace from background task + */ + private final Thread thread; + private final String requestUri; + private final long start; + private final AtomicInteger state = new AtomicInteger(MonitoredThreadState.RUNNING.ordinal()); + /** + * Semaphore to synchronize the stuck thread with the background-process thread. It's not used if the + * interruption feature is not active. + */ + private final Semaphore interruptionSemaphore; + /** + * Set to true after the thread is interrupted. No need to make it volatile since it is accessed right after + * acquiring the semaphore. + */ + private boolean interrupted; + + MonitoredThread(Thread thread, String requestUri, boolean interruptible) { + this.thread = thread; + this.requestUri = requestUri; + this.start = System.currentTimeMillis(); + if (interruptible) { + interruptionSemaphore = new Semaphore(1); + } else { + interruptionSemaphore = null; + } + } + + public Thread getThread() { + return this.thread; + } + + public String getRequestUri() { + return requestUri; + } + + public long getActiveTimeInMillis() { + return System.currentTimeMillis() - start; + } + + public Date getStartTime() { + return new Date(start); + } + + public boolean markAsStuckIfStillRunning() { + return this.state.compareAndSet(MonitoredThreadState.RUNNING.ordinal(), + MonitoredThreadState.STUCK.ordinal()); + } + + public MonitoredThreadState markAsDone() { + int val = this.state.getAndSet(MonitoredThreadState.DONE.ordinal()); + MonitoredThreadState threadState = MonitoredThreadState.values()[val]; + + if (threadState == MonitoredThreadState.STUCK && interruptionSemaphore != null) { + try { + // use the semaphore to synchronize with the background thread + // which might try to interrupt this current thread. + // Otherwise, the current thread might be interrupted after + // going out from here, maybe already serving a new request + this.interruptionSemaphore.acquire(); + } catch (InterruptedException e) { + log.debug(sm.getString("stuckThreadDetectionValve.interrupted"), e); + } + // no need to release the semaphore, it will be GCed + } + // else the request went through before being marked as stuck, no need + // to sync against the semaphore + return threadState; + } + + boolean isMarkedAsStuck() { + return this.state.get() == MonitoredThreadState.STUCK.ordinal(); + } + + public boolean interruptIfStuck(long interruptThreadThreshold) { + if (!isMarkedAsStuck() || interruptionSemaphore == null || !this.interruptionSemaphore.tryAcquire()) { + // if the semaphore is already acquired, it means that the + // request thread got unstuck before we interrupted it + return false; + } + try { + if (log.isWarnEnabled()) { + String msg = sm.getString("stuckThreadDetectionValve.notifyStuckThreadInterrupted", + this.getThread().getName(), Long.valueOf(getActiveTimeInMillis()), this.getStartTime(), + this.getRequestUri(), Long.valueOf(interruptThreadThreshold), + String.valueOf(this.getThread().getId())); + Throwable th = new Throwable(); + th.setStackTrace(this.getThread().getStackTrace()); + log.warn(msg, th); + } + this.thread.interrupt(); + } finally { + this.interrupted = true; + this.interruptionSemaphore.release(); + } + return true; + } + + public boolean wasInterrupted() { + return interrupted; + } + } + + private static class CompletedStuckThread { + + private final String threadName; + private final long threadId; + private final long totalActiveTime; + + CompletedStuckThread(Thread thread, long totalActiveTime) { + this.threadName = thread.getName(); + this.threadId = thread.getId(); + this.totalActiveTime = totalActiveTime; + } + + public String getName() { + return this.threadName; + } + + public long getId() { + return this.threadId; + } + + public long getTotalActiveTime() { + return this.totalActiveTime; + } + } + + private enum MonitoredThreadState { + RUNNING, + STUCK, + DONE + } +} diff --git a/java/org/apache/catalina/valves/ValveBase.java b/java/org/apache/catalina/valves/ValveBase.java new file mode 100644 index 0000000..8d69925 --- /dev/null +++ b/java/org/apache/catalina/valves/ValveBase.java @@ -0,0 +1,227 @@ +/* + * 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.catalina.valves; + +import org.apache.catalina.Contained; +import org.apache.catalina.Container; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Valve; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.catalina.util.ToStringUtil; +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.res.StringManager; + +/** + * Convenience base class for implementations of the Valve interface. A subclass MUST implement + * an invoke() method to provide the required functionality, and MAY implement the + * Lifecycle interface to provide configuration management and lifecycle support. + * + * @author Craig R. McClanahan + */ +public abstract class ValveBase extends LifecycleMBeanBase implements Contained, Valve { + + protected static final StringManager sm = StringManager.getManager(ValveBase.class); + + + // ------------------------------------------------------ Constructor + + public ValveBase() { + this(false); + } + + + public ValveBase(boolean asyncSupported) { + this.asyncSupported = asyncSupported; + } + + + // ------------------------------------------------------ Instance Variables + + /** + * Does this valve support Servlet 3+ async requests? + */ + protected boolean asyncSupported; + + + /** + * The Container whose pipeline this Valve is a component of. + */ + protected Container container = null; + + + /** + * Container log + */ + protected Log containerLog = null; + + + /** + * The next Valve in the pipeline this Valve is a component of. + */ + protected Valve next = null; + + + // -------------------------------------------------------------- Properties + + @Override + public Container getContainer() { + return container; + } + + + @Override + public void setContainer(Container container) { + this.container = container; + } + + + @Override + public boolean isAsyncSupported() { + return asyncSupported; + } + + + public void setAsyncSupported(boolean asyncSupported) { + this.asyncSupported = asyncSupported; + } + + + @Override + public Valve getNext() { + return next; + } + + + @Override + public void setNext(Valve valve) { + this.next = valve; + } + + + // ---------------------------------------------------------- Public Methods + + /** + * {@inheritDoc} + *

    + * The default implementation is NO-OP. + */ + @Override + public void backgroundProcess() { + // NOOP by default + } + + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + containerLog = getContainer().getLogger(); + } + + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void startInternal() throws LifecycleException { + setState(LifecycleState.STARTING); + } + + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException if this component detects a fatal error that prevents this component from being + * used + */ + @Override + protected void stopInternal() throws LifecycleException { + setState(LifecycleState.STOPPING); + } + + + @Override + public String toString() { + return ToStringUtil.toString(this); + } + + + // -------------------- JMX and Registration -------------------- + + @Override + public String getObjectNameKeyProperties() { + StringBuilder name = new StringBuilder("type=Valve"); + + Container container = getContainer(); + + name.append(container.getMBeanKeyProperties()); + + int seq = 0; + + // Pipeline may not be present in unit testing + Pipeline p = container.getPipeline(); + if (p != null) { + for (Valve valve : p.getValves()) { + // Skip null valves + if (valve == null) { + continue; + } + // Only compare valves in pipeline until we find this valve + if (valve == this) { + break; + } + if (valve.getClass() == this.getClass()) { + // Duplicate valve earlier in pipeline + // increment sequence number + seq++; + } + } + } + + if (seq > 0) { + name.append(",seq="); + name.append(seq); + } + + String className = this.getClass().getName(); + int period = className.lastIndexOf('.'); + if (period >= 0) { + className = className.substring(period + 1); + } + name.append(",name="); + name.append(className); + + return name.toString(); + } + + + @Override + public String getDomainInternal() { + Container c = getContainer(); + if (c == null) { + return null; + } else { + return c.getDomain(); + } + } +} diff --git a/java/org/apache/catalina/valves/mbeans-descriptors.xml b/java/org/apache/catalina/valves/mbeans-descriptors.xml new file mode 100644 index 0000000..6290778 --- /dev/null +++ b/java/org/apache/catalina/valves/mbeans-descriptors.xml @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/valves/package.html b/java/org/apache/catalina/valves/package.html new file mode 100644 index 0000000..7643476 --- /dev/null +++ b/java/org/apache/catalina/valves/package.html @@ -0,0 +1,28 @@ + + + +

    This package contains a variety of small Valve implementations that do +not warrant being packaged separately. In addition, there is a convenience +base class (ValveBase) that supports the usual mechanisms for +including custom Valves into the corresponding Pipeline.

    + +

    Other packages that include Valves include +org.apache.tomcat.logger and +org.apache.tomcat.security.

    + + diff --git a/java/org/apache/catalina/valves/rewrite/InternalRewriteMap.java b/java/org/apache/catalina/valves/rewrite/InternalRewriteMap.java new file mode 100644 index 0000000..b556e4c --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/InternalRewriteMap.java @@ -0,0 +1,122 @@ +/* + * 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.catalina.valves.rewrite; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import org.apache.catalina.util.URLEncoder; +import org.apache.tomcat.util.buf.UDecoder; + +public class InternalRewriteMap { + + public static RewriteMap toMap(String name) { + if ("toupper".equals(name)) { + return new UpperCase(); + } else if ("tolower".equals(name)) { + return new LowerCase(); + } else if ("escape".equals(name)) { + return new Escape(); + } else if ("unescape".equals(name)) { + return new Unescape(); + } else { + return null; + } + } + + public static class LowerCase implements RewriteMap { + + private Locale locale = Locale.getDefault(); + + @Override + public String setParameters(String params) { + this.locale = Locale.forLanguageTag(params); + return null; + } + + @Override + public String lookup(String key) { + if (key != null) { + return key.toLowerCase(locale); + } + return null; + } + + } + + public static class UpperCase implements RewriteMap { + + private Locale locale = Locale.getDefault(); + + @Override + public String setParameters(String params) { + this.locale = Locale.forLanguageTag(params); + return null; + } + + @Override + public String lookup(String key) { + if (key != null) { + return key.toUpperCase(locale); + } + return null; + } + + } + + public static class Escape implements RewriteMap { + + private Charset charset = StandardCharsets.UTF_8; + + @Override + public String setParameters(String params) { + this.charset = Charset.forName(params); + return null; + } + + @Override + public String lookup(String key) { + if (key != null) { + return URLEncoder.DEFAULT.encode(key, charset); + } + return null; + } + + } + + public static class Unescape implements RewriteMap { + + private Charset charset = StandardCharsets.UTF_8; + + @Override + public String setParameters(String params) { + this.charset = Charset.forName(params); + return null; + } + + @Override + public String lookup(String key) { + if (key != null) { + return UDecoder.URLDecode(key, charset); + } + return null; + } + + } + +} diff --git a/java/org/apache/catalina/valves/rewrite/LocalStrings.properties b/java/org/apache/catalina/valves/rewrite/LocalStrings.properties new file mode 100644 index 0000000..ffb6009 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/LocalStrings.properties @@ -0,0 +1,33 @@ +# 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. + +quotedStringTokenizer.tokenizeError=Error tokenizing text [{0}] after position [{1}] from mode [{2}] + +rewriteMap.tooManyParameters=Too many parameters for this map +rewriteMap.txtInvalidLine=Invalid line [{0}] in text file [{1}] +rewriteMap.txtReadError=Error reading text file [{0}] + +rewriteValve.closeError=Error closing configuration +rewriteValve.invalidFlags=Invalid flag in [{0}] flags [{1}] +rewriteValve.invalidLine=Invalid line [{0}] +rewriteValve.invalidMapClassName=Invalid map class name [{0}] +rewriteValve.noConfiguration=No configuration resource found [{0}] +rewriteValve.readConfiguration=Read configuration from [/WEB-INF/{0}] +rewriteValve.readError=Error reading configuration + +substitution.badType=Bad type [{0}] in substitution [{1}] +substitution.invalid=Invalid substitution [{0}] +substitution.missingDigit=Substitution [{0}] is missing digit or curly brace +substitution.noMap=Map [{0}] not found in substitution [{1}] diff --git a/java/org/apache/catalina/valves/rewrite/LocalStrings_fr.properties b/java/org/apache/catalina/valves/rewrite/LocalStrings_fr.properties new file mode 100644 index 0000000..171fcd7 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/LocalStrings_fr.properties @@ -0,0 +1,33 @@ +# 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. + +quotedStringTokenizer.tokenizeError=Erreur de découpage du texte [{0}] après la position [{1}] en utilisant le mode [{2}] + +rewriteMap.tooManyParameters=Cette map ne supporte pas plusieurs paramètres +rewriteMap.txtInvalidLine=Ligne invalide [{0}] dans le fichier texte [{1}] +rewriteMap.txtReadError=Erreur en lisant le fichier texte [{0}] + +rewriteValve.closeError=Erreur lors de la fermeture de la configuration +rewriteValve.invalidFlags=Indicateur invalide dans [{0}] indicateurs [{1}] +rewriteValve.invalidLine=Ligne invalide [{0}] +rewriteValve.invalidMapClassName=Le nom de la classe [{0}] de la structure est invalide +rewriteValve.noConfiguration=Aucune ressource de configuration trouvée à [{0}] +rewriteValve.readConfiguration=Lecture de la configuration depuis [/WEB-INF/{0}] +rewriteValve.readError=Erreur lors de la lecture de la configuration + +substitution.badType=Mauvais type [{0}] dans la substitution [{1}] +substitution.invalid=Substitution invalide [{0}] +substitution.missingDigit=La substitution [{0}] manque un chiffre ou une accolade +substitution.noMap=La carte [{0}] n''a pas été trouvée dans la substitution [{1}] diff --git a/java/org/apache/catalina/valves/rewrite/LocalStrings_ja.properties b/java/org/apache/catalina/valves/rewrite/LocalStrings_ja.properties new file mode 100644 index 0000000..c749ca3 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/LocalStrings_ja.properties @@ -0,0 +1,33 @@ +# 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. + +quotedStringTokenizer.tokenizeError=モード [{2}] ã®ä½ç½® [{1}] ã®å¾Œã®ãƒ†ã‚­ã‚¹ãƒˆ [{0}] ã®ãƒˆãƒ¼ã‚¯ãƒ³åŒ–中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+ +rewriteMap.tooManyParameters=ã“ã®ãƒžãƒƒãƒ—ã®ãƒ‘ラメータãŒå¤šã™ãŽã¾ã™ +rewriteMap.txtInvalidLine=テキスト ファイル [{1}] ã®è¡Œ [{0}] ãŒç„¡åŠ¹ã§ã™ +rewriteMap.txtReadError=テキスト ファイル [{0}] ã®èª­ã¿å–り中ã®ã‚¨ãƒ©ãƒ¼ + +rewriteValve.closeError=構æˆã‚¯ãƒ­ãƒ¼ã‚ºä¸­ã®ã‚¨ãƒ©ãƒ¼ +rewriteValve.invalidFlags=[{0}]ã«ç„¡åŠ¹ãªãƒ•ãƒ©ã‚° [{1}]ãŒã‚ã‚Šã¾ã™ã€‚ +rewriteValve.invalidLine=無効ãªè¡Œ[{0}] +rewriteValve.invalidMapClassName=Mapクラスå[{0}]ãŒç„¡åŠ¹ã§ã™ã€‚ +rewriteValve.noConfiguration=設定リソースãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ [{0}] +rewriteValve.readConfiguration=[/WEB-INF/{0}] ã‹ã‚‰æ§‹æˆã‚’読ã¿å–ã‚Šã¾ã™ +rewriteValve.readError=構æˆã®èª­ã¿å–りエラー + +substitution.badType=ç½®æ› [{1}] 内ã®ä¸æ­£ãªã‚¿ã‚¤ãƒ— [{0}] +substitution.invalid=無効ãªç½®æ› [{0}] +substitution.missingDigit=ç½®æ› [{0}] ã«æ•°å­—ã¾ãŸã¯ä¸­æ‹¬å¼§ãŒã‚ã‚Šã¾ã›ã‚“ +substitution.noMap=マップ [{0}] ãŒç½®æ› [{1}] ã«è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ diff --git a/java/org/apache/catalina/valves/rewrite/LocalStrings_ko.properties b/java/org/apache/catalina/valves/rewrite/LocalStrings_ko.properties new file mode 100644 index 0000000..7d3d3e0 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/LocalStrings_ko.properties @@ -0,0 +1,24 @@ +# 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. + +quotedStringTokenizer.tokenizeError=토í°ìœ¼ë¡œ 파싱하는 중 오류 ë°œìƒ (문ìžì—´: [{0}], 위치: [{1}], 현재 모드: [{2}]) + +rewriteMap.tooManyParameters=RewriteMap ë””ë ‰í‹°ë¸Œì— ë„ˆë¬´ ë§Žì€ íŒŒë¼ë¯¸í„°ë“¤ì´ 설정ë˜ì—ˆìŠµë‹ˆë‹¤. + +rewriteValve.closeError=설정 ë¦¬ì†ŒìŠ¤ì˜ ìŠ¤íŠ¸ë¦¼ì„ ë‹«ëŠ” 중 오류 ë°œìƒ +rewriteValve.invalidFlags=[{0}] ë‚´ì— ìœ íš¨í•˜ì§€ ì•Šì€ í”Œëž˜ê·¸ìž…ë‹ˆë‹¤ (플래그들: [{1}]). +rewriteValve.invalidLine=유효하지 ì•Šì€ í–‰ [{0}] +rewriteValve.invalidMapClassName=유효하지 ì•Šì€ Map í´ëž˜ìŠ¤ ì´ë¦„: [{0}] +rewriteValve.readError=ì„¤ì •ì„ ì½ëŠ” ë„중 오류 ë°œìƒ diff --git a/java/org/apache/catalina/valves/rewrite/LocalStrings_zh_CN.properties b/java/org/apache/catalina/valves/rewrite/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..f704400 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/LocalStrings_zh_CN.properties @@ -0,0 +1,26 @@ +# 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. + +quotedStringTokenizer.tokenizeError=模å¼[{2}]中[{1}]ä½ç½®å¤„的文本[{0}]标记错误 + +rewriteMap.tooManyParameters=对当å‰mapæ¥è¯´å‚数太多 +rewriteMap.txtInvalidLine=文本文件中[{1}],[{0}]为无效行 +rewriteMap.txtReadError=读å–文本文件[{0}]出错 + +rewriteValve.closeError=关闭é…置时出错 +rewriteValve.invalidFlags=[{0}]标志[{1}]中的标志无效 +rewriteValve.invalidLine=无效行[{0}] +rewriteValve.invalidMapClassName=无效的映射类å[{0}] +rewriteValve.readError=读å–é…置时å‘生异常 diff --git a/java/org/apache/catalina/valves/rewrite/QuotedStringTokenizer.java b/java/org/apache/catalina/valves/rewrite/QuotedStringTokenizer.java new file mode 100644 index 0000000..a6204fb --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/QuotedStringTokenizer.java @@ -0,0 +1,139 @@ +/* + * 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.catalina.valves.rewrite; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.tomcat.util.res.StringManager; + +public class QuotedStringTokenizer { + + protected static final StringManager sm = StringManager.getManager(QuotedStringTokenizer.class); + + private Iterator tokenIterator; + private int tokenCount; + private int returnedTokens = 0; + + enum WordMode { + SPACES, QUOTED, ESCAPED, SIMPLE, COMMENT + } + + public QuotedStringTokenizer(String text) { + List tokens; + if (text != null) { + tokens = tokenizeText(text); + } else { + tokens = Collections.emptyList(); + } + this.tokenCount = tokens.size(); + this.tokenIterator = tokens.iterator(); + } + + private List tokenizeText(String inputText) { + List tokens = new ArrayList<>(); + int pos = 0; + int length = inputText.length(); + WordMode currentMode = WordMode.SPACES; + StringBuilder currentToken = new StringBuilder(); + while (pos < length) { + char currentChar = inputText.charAt(pos); + switch (currentMode) { + case SPACES: + currentMode = handleSpaces(currentToken, currentChar); + break; + case QUOTED: + currentMode = handleQuoted(tokens, currentToken, currentChar); + break; + case ESCAPED: + currentToken.append(currentChar); + currentMode = WordMode.QUOTED; + break; + case SIMPLE: + currentMode = handleSimple(tokens, currentToken, currentChar); + break; + case COMMENT: + if (currentChar == '\r' || currentChar == '\n') { + currentMode = WordMode.SPACES; + } + break; + default: + throw new IllegalStateException(sm.getString("quotedStringTokenizer.tokenizeError", + inputText, Integer.valueOf(pos), currentMode)); + } + pos++; + } + String possibleLastToken = currentToken.toString(); + if (!possibleLastToken.isEmpty()) { + tokens.add(possibleLastToken); + } + return tokens; + } + + private WordMode handleSimple(List tokens, StringBuilder currentToken, char currentChar) { + if (Character.isWhitespace(currentChar)) { + tokens.add(currentToken.toString()); + currentToken.setLength(0); + return WordMode.SPACES; + } else { + currentToken.append(currentChar); + } + return WordMode.SIMPLE; + } + + private WordMode handleQuoted(List tokens, StringBuilder currentToken, char currentChar) { + if (currentChar == '"') { + tokens.add(currentToken.toString()); + currentToken.setLength(0); + return WordMode.SPACES; + } else if (currentChar == '\\') { + return WordMode.ESCAPED; + } else { + currentToken.append(currentChar); + } + return WordMode.QUOTED; + } + + private WordMode handleSpaces(StringBuilder currentToken, char currentChar) { + if (!Character.isWhitespace(currentChar)) { + if (currentChar == '"') { + return WordMode.QUOTED; + } else if (currentChar == '#') { + return WordMode.COMMENT; + } else { + currentToken.append(currentChar); + return WordMode.SIMPLE; + } + } + return WordMode.SPACES; + } + + public boolean hasMoreTokens() { + return tokenIterator.hasNext(); + } + + public String nextToken() { + returnedTokens++; + return tokenIterator.next(); + } + + public int countTokens() { + return tokenCount - returnedTokens; + } +} diff --git a/java/org/apache/catalina/valves/rewrite/RandomizedTextRewriteMap.java b/java/org/apache/catalina/valves/rewrite/RandomizedTextRewriteMap.java new file mode 100644 index 0000000..37a2433 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/RandomizedTextRewriteMap.java @@ -0,0 +1,94 @@ +/* + * 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.catalina.valves.rewrite; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource.Resource; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implement a map for the txt: and rnd: mod_rewrite capabilities. + */ +public class RandomizedTextRewriteMap implements RewriteMap{ + + protected static final StringManager sm = StringManager.getManager(RandomizedTextRewriteMap.class); + + private static final Random random = new Random(); + private final Map map = new HashMap<>(); + + /** + * Create a map from a text file according to the mod_rewrite syntax. + * @param txtFilePath the text file path + * @param useRandom if the map should produce random results + */ + public RandomizedTextRewriteMap(String txtFilePath, boolean useRandom) { + String line; + try (Resource txtResource = ConfigFileLoader.getSource().getResource(txtFilePath); + BufferedReader reader = new BufferedReader(new InputStreamReader(txtResource.getInputStream()))) { + while ((line = reader.readLine()) != null) { + if (line.startsWith("#") || line.isEmpty()) { + //Ignore comment or empty lines + continue; + } + String[] keyValuePair = line.split(" ", 2); + if (keyValuePair.length > 1) { + String key = keyValuePair[0]; + String value = keyValuePair[1]; + String[] possibleValues = null; + if (useRandom && value.contains("|")) { + possibleValues = value.split("\\|"); + } else { + possibleValues = new String[1]; + possibleValues[0] = value; + } + map.put(key, possibleValues); + } else { + throw new IllegalArgumentException(sm.getString("rewriteMap.txtInvalidLine", line, txtFilePath)); + } + } + } catch (IOException e) { + throw new IllegalArgumentException(sm.getString("rewriteMap.txtReadError", txtFilePath), e); + } + } + + @Override + public String setParameters(String params) { + throw new IllegalArgumentException( + StringManager.getManager(RewriteMap.class).getString("rewriteMap.tooManyParameters")); + } + + @Override + public String lookup(String key) { + String[] possibleValues = map.get(key); + if (possibleValues != null) { + if (possibleValues.length > 1) { + return possibleValues[random.nextInt(possibleValues.length)]; + } else { + return possibleValues[0]; + } + } + return null; + } +} diff --git a/java/org/apache/catalina/valves/rewrite/Resolver.java b/java/org/apache/catalina/valves/rewrite/Resolver.java new file mode 100644 index 0000000..2da76d5 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/Resolver.java @@ -0,0 +1,39 @@ +/* + * 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.catalina.valves.rewrite; + +import java.nio.charset.Charset; + +/** + * Resolver abstract class. + */ +public abstract class Resolver { + + public abstract String resolve(String key); + + public String resolveEnv(String key) { + return System.getProperty(key); + } + + public abstract String resolveSsl(String key); + + public abstract String resolveHttp(String key); + + public abstract boolean resolveResource(int type, String name); + + public abstract Charset getUriCharset(); +} diff --git a/java/org/apache/catalina/valves/rewrite/ResolverImpl.java b/java/org/apache/catalina/valves/rewrite/ResolverImpl.java new file mode 100644 index 0000000..c7d9295 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/ResolverImpl.java @@ -0,0 +1,368 @@ +/* + * 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.catalina.valves.rewrite; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.TimeUnit; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.connector.Request; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.jsse.PEMFile; +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.net.openssl.ciphers.EncryptionLevel; +import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser; + +public class ResolverImpl extends Resolver { + + protected Request request = null; + + public ResolverImpl(Request request) { + this.request = request; + } + + /** + * The following are not implemented: + * - SERVER_ADMIN + * - API_VERSION + * - IS_SUBREQ + */ + @Override + public String resolve(String key) { + if (key.equals("HTTP_USER_AGENT")) { + return request.getHeader("user-agent"); + } else if (key.equals("HTTP_REFERER")) { + return request.getHeader("referer"); + } else if (key.equals("HTTP_COOKIE")) { + return request.getHeader("cookie"); + } else if (key.equals("HTTP_FORWARDED")) { + return request.getHeader("forwarded"); + } else if (key.equals("HTTP_HOST")) { + // Don't look directly at the host header to handle: + // - Host name in HTTP/1.1 request line + // - HTTP/0.9 & HTTP/1.0 requests + // - HTTP/2 :authority pseudo header + return request.getServerName(); + } else if (key.equals("HTTP_PROXY_CONNECTION")) { + return request.getHeader("proxy-connection"); + } else if (key.equals("HTTP_ACCEPT")) { + return request.getHeader("accept"); + } else if (key.equals("REMOTE_ADDR")) { + return request.getRemoteAddr(); + } else if (key.equals("REMOTE_HOST")) { + return request.getRemoteHost(); + } else if (key.equals("REMOTE_PORT")) { + return String.valueOf(request.getRemotePort()); + } else if (key.equals("REMOTE_USER")) { + return request.getRemoteUser(); + } else if (key.equals("REMOTE_IDENT")) { + return request.getRemoteUser(); + } else if (key.equals("REQUEST_METHOD")) { + return request.getMethod(); + } else if (key.equals("SCRIPT_FILENAME")) { + return request.getServletContext().getRealPath(request.getServletPath()); + } else if (key.equals("REQUEST_PATH")) { + return request.getRequestPathMB().toString(); + } else if (key.equals("CONTEXT_PATH")) { + return request.getContextPath(); + } else if (key.equals("SERVLET_PATH")) { + return emptyStringIfNull(request.getServletPath()); + } else if (key.equals("PATH_INFO")) { + return emptyStringIfNull(request.getPathInfo()); + } else if (key.equals("QUERY_STRING")) { + return emptyStringIfNull(request.getQueryString()); + } else if (key.equals("AUTH_TYPE")) { + return request.getAuthType(); + } else if (key.equals("DOCUMENT_ROOT")) { + return request.getServletContext().getRealPath("/"); + } else if (key.equals("SERVER_NAME")) { + return request.getLocalName(); + } else if (key.equals("SERVER_ADDR")) { + return request.getLocalAddr(); + } else if (key.equals("SERVER_PORT")) { + return String.valueOf(request.getLocalPort()); + } else if (key.equals("SERVER_PROTOCOL")) { + return request.getProtocol(); + } else if (key.equals("SERVER_SOFTWARE")) { + return "tomcat"; + } else if (key.equals("THE_REQUEST")) { + return request.getMethod() + " " + request.getRequestURI() + + " " + request.getProtocol(); + } else if (key.equals("REQUEST_URI")) { + return request.getRequestURI(); + } else if (key.equals("REQUEST_FILENAME")) { + return request.getPathTranslated(); + } else if (key.equals("HTTPS")) { + return request.isSecure() ? "on" : "off"; + } else if (key.equals("TIME_YEAR")) { + return String.valueOf(Calendar.getInstance().get(Calendar.YEAR)); + } else if (key.equals("TIME_MON")) { + return String.valueOf(Calendar.getInstance().get(Calendar.MONTH)); + } else if (key.equals("TIME_DAY")) { + return String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_MONTH)); + } else if (key.equals("TIME_HOUR")) { + return String.valueOf(Calendar.getInstance().get(Calendar.HOUR_OF_DAY)); + } else if (key.equals("TIME_MIN")) { + return String.valueOf(Calendar.getInstance().get(Calendar.MINUTE)); + } else if (key.equals("TIME_SEC")) { + return String.valueOf(Calendar.getInstance().get(Calendar.SECOND)); + } else if (key.equals("TIME_WDAY")) { + return String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK)); + } else if (key.equals("TIME")) { + return FastHttpDateFormat.getCurrentDate(); + } + return null; + } + + @Override + public String resolveEnv(String key) { + Object result = request.getAttribute(key); + return (result != null) ? result.toString() : System.getProperty(key); + } + + @Override + public String resolveSsl(String key) { + SSLSupport sslSupport = (SSLSupport) request.getAttribute(SSLSupport.SESSION_MGR); + try { + // SSL_SRP_USER: no planned support for SRP + // SSL_SRP_USERINFO: no planned support for SRP + if (key.equals("HTTPS")) { + return String.valueOf(sslSupport != null); + } else if (key.equals("SSL_PROTOCOL")) { + return sslSupport.getProtocol(); + } else if (key.equals("SSL_SESSION_ID")) { + return sslSupport.getSessionId(); + } else if (key.equals("SSL_SESSION_RESUMED")) { + // FIXME session resumption state, not available anywhere + } else if (key.equals("SSL_SECURE_RENEG")) { + // FIXME available from SSLHostConfig + } else if (key.equals("SSL_COMPRESS_METHOD")) { + // FIXME available from SSLHostConfig + } else if (key.equals("SSL_TLS_SNI")) { + // FIXME from handshake SNI processing + } else if (key.equals("SSL_CIPHER")) { + return sslSupport.getCipherSuite(); + } else if (key.equals("SSL_CIPHER_EXPORT")) { + String cipherSuite = sslSupport.getCipherSuite(); + if (cipherSuite != null) { + Set cipherList = OpenSSLCipherConfigurationParser.parse(cipherSuite); + if (cipherList.size() == 1) { + Cipher cipher = cipherList.iterator().next(); + if (cipher.getLevel().equals(EncryptionLevel.EXP40) + || cipher.getLevel().equals(EncryptionLevel.EXP56)) { + return "true"; + } else { + return "false"; + } + } + } + } else if (key.equals("SSL_CIPHER_ALGKEYSIZE")) { + String cipherSuite = sslSupport.getCipherSuite(); + if (cipherSuite != null) { + Set cipherList = OpenSSLCipherConfigurationParser.parse(cipherSuite); + if (cipherList.size() == 1) { + Cipher cipher = cipherList.iterator().next(); + return String.valueOf(cipher.getAlg_bits()); + } + } + } else if (key.equals("SSL_CIPHER_USEKEYSIZE")) { + Integer keySize = sslSupport.getKeySize(); + return (keySize == null) ? null : sslSupport.getKeySize().toString(); + } else if (key.startsWith("SSL_CLIENT_")) { + X509Certificate[] certificates = sslSupport.getPeerCertificateChain(); + if (certificates != null && certificates.length > 0) { + key = key.substring("SSL_CLIENT_".length()); + String result = resolveSslCertificates(key, certificates); + if (result != null) { + return result; + } else if (key.startsWith("SAN_OTHER_msUPN_")) { + // Type otherName, which is 0 + key = key.substring("SAN_OTHER_msUPN_".length()); + // FIXME OID from resolveAlternateName + } else if (key.equals("CERT_RFC4523_CEA")) { + // FIXME return certificate[0] format CertificateExactAssertion in RFC4523 + } else if (key.equals("VERIFY")) { + // FIXME return verification state, not available anywhere + } + } + } else if (key.startsWith("SSL_SERVER_")) { + X509Certificate[] certificates = sslSupport.getLocalCertificateChain(); + if (certificates != null && certificates.length > 0) { + key = key.substring("SSL_SERVER_".length()); + String result = resolveSslCertificates(key, certificates); + if (result != null) { + return result; + } else if (key.startsWith("SAN_OTHER_dnsSRV_")) { + // Type otherName, which is 0 + key = key.substring("SAN_OTHER_dnsSRV_".length()); + // FIXME OID from resolveAlternateName + } + } + } + } catch (IOException e) { + // TLS access error + } + return null; + } + + private String resolveSslCertificates(String key, X509Certificate[] certificates) { + if (key.equals("M_VERSION")) { + return String.valueOf(certificates[0].getVersion()); + } else if (key.equals("M_SERIAL")) { + return certificates[0].getSerialNumber().toString(); + } else if (key.equals("S_DN")) { + return certificates[0].getSubjectX500Principal().toString(); + } else if (key.startsWith("S_DN_")) { + key = key.substring("S_DN_".length()); + return resolveComponent(certificates[0].getSubjectX500Principal().getName(), key); + } else if (key.startsWith("SAN_Email_")) { + // Type rfc822Name, which is 1 + key = key.substring("SAN_Email_".length()); + return resolveAlternateName(certificates[0], 1, Integer.parseInt(key)); + } else if (key.startsWith("SAN_DNS_")) { + // Type dNSName, which is 2 + key = key.substring("SAN_DNS_".length()); + return resolveAlternateName(certificates[0], 2, Integer.parseInt(key)); + } else if (key.equals("I_DN")) { + return certificates[0].getIssuerX500Principal().getName(); + } else if (key.startsWith("I_DN_")) { + key = key.substring("I_DN_".length()); + return resolveComponent(certificates[0].getIssuerX500Principal().toString(), key); + } else if (key.equals("V_START")) { + return String.valueOf(certificates[0].getNotBefore().getTime()); + } else if (key.equals("V_END")) { + return String.valueOf(certificates[0].getNotAfter().getTime()); + } else if (key.equals("V_REMAIN")) { + long remain = certificates[0].getNotAfter().getTime() - System.currentTimeMillis(); + if (remain < 0) { + remain = 0L; + } + // Return remaining days + return String.valueOf(TimeUnit.MILLISECONDS.toDays(remain)); + } else if (key.equals("A_SIG")) { + return certificates[0].getSigAlgName(); + } else if (key.equals("A_KEY")) { + return certificates[0].getPublicKey().getAlgorithm(); + } else if (key.equals("CERT")) { + try { + return PEMFile.toPEM(certificates[0]); + } catch (CertificateEncodingException e) { + // Ignore + } + } else if (key.startsWith("CERT_CHAIN_")) { + key = key.substring("CERT_CHAIN_".length()); + try { + return PEMFile.toPEM(certificates[Integer.parseInt(key)]); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException + | CertificateEncodingException e) { + // Ignore + } + } + return null; + } + + private String resolveComponent(String fullDN, String component) { + HashMap components = new HashMap<>(); + StringTokenizer tokenizer = new StringTokenizer(fullDN, ","); + while (tokenizer.hasMoreElements()) { + String token = tokenizer.nextToken().trim(); + int pos = token.indexOf('='); + if (pos > 0 && (pos + 1) < token.length()) { + components.put(token.substring(0, pos), token.substring(pos + 1)); + } + } + return components.get(component); + } + + private String resolveAlternateName(X509Certificate certificate, int type, int n) { + try { + Collection> alternateNames = certificate.getSubjectAlternativeNames(); + if (alternateNames != null) { + List elements = new ArrayList<>(); + for (List alternateName : alternateNames) { + Integer alternateNameType = (Integer) alternateName.get(0); + if (alternateNameType.intValue() == type) { + elements.add(String.valueOf(alternateName.get(1))); + } + } + if (elements.size() > n) { + return elements.get(n); + } + } + } catch (NumberFormatException | ArrayIndexOutOfBoundsException + | CertificateParsingException e) { + // Ignore + } + return null; + } + + @Override + public String resolveHttp(String key) { + String header = request.getHeader(key); + if (header == null) { + return ""; + } else { + return header; + } + } + + @Override + public boolean resolveResource(int type, String name) { + WebResourceRoot resources = request.getContext().getResources(); + WebResource resource = resources.getResource(name); + if (!resource.exists()) { + return false; + } else { + switch (type) { + case 0: + return resource.isDirectory(); + case 1: + return resource.isFile(); + case 2: + return resource.isFile() && resource.getContentLength() > 0; + default: + return false; + } + } + } + + private static String emptyStringIfNull(String value) { + if (value == null) { + return ""; + } else { + return value; + } + } + + @Override + public Charset getUriCharset() { + return request.getConnector().getURICharset(); + } +} diff --git a/java/org/apache/catalina/valves/rewrite/RewriteCond.java b/java/org/apache/catalina/valves/rewrite/RewriteCond.java new file mode 100644 index 0000000..90bdc52 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/RewriteCond.java @@ -0,0 +1,239 @@ +/* + * 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.catalina.valves.rewrite; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RewriteCond { + + public abstract static class Condition { + public abstract boolean evaluate(String value, Resolver resolver); + } + + public static class PatternCondition extends Condition { + public Pattern pattern; + private ThreadLocal matcher = new ThreadLocal<>(); + + @Override + public boolean evaluate(String value, Resolver resolver) { + Matcher m = pattern.matcher(value); + if (m.matches()) { + matcher.set(m); + return true; + } else { + return false; + } + } + + public Matcher getMatcher() { + return matcher.get(); + } + } + + public static class LexicalCondition extends Condition { + /** + * -1: < + * 0: = + * 1: > + */ + public int type = 0; + public String condition; + + @Override + public boolean evaluate(String value, Resolver resolver) { + int result = value.compareTo(condition); + switch (type) { + case -1: + return (result < 0); + case 0: + return (result == 0); + case 1: + return (result > 0); + default: + return false; + } + + } + } + + public static class ResourceCondition extends Condition { + /** + * 0: -d (is directory ?) + * 1: -f (is regular file ?) + * 2: -s (is regular file with size ?) + */ + public int type = 0; + + @Override + public boolean evaluate(String value, Resolver resolver) { + return resolver.resolveResource(type, value); + } + } + + protected String testString = null; + protected String condPattern = null; + protected String flagsString = null; + + public String getCondPattern() { + return condPattern; + } + + public void setCondPattern(String condPattern) { + this.condPattern = condPattern; + } + + public String getTestString() { + return testString; + } + + public void setTestString(String testString) { + this.testString = testString; + } + + public final String getFlagsString() { + return flagsString; + } + + public final void setFlagsString(String flagsString) { + this.flagsString = flagsString; + } + + public void parse(Map maps) { + test = new Substitution(); + test.setSub(testString); + test.parse(maps); + if (condPattern.startsWith("!")) { + positive = false; + condPattern = condPattern.substring(1); + } + if (condPattern.startsWith("<")) { + LexicalCondition ncondition = new LexicalCondition(); + ncondition.type = -1; + ncondition.condition = condPattern.substring(1); + this.condition = ncondition; + } else if (condPattern.startsWith(">")) { + LexicalCondition ncondition = new LexicalCondition(); + ncondition.type = 1; + ncondition.condition = condPattern.substring(1); + this.condition = ncondition; + } else if (condPattern.startsWith("=")) { + LexicalCondition ncondition = new LexicalCondition(); + ncondition.type = 0; + ncondition.condition = condPattern.substring(1); + this.condition = ncondition; + } else if (condPattern.equals("-d")) { + ResourceCondition ncondition = new ResourceCondition(); + ncondition.type = 0; + this.condition = ncondition; + } else if (condPattern.equals("-f")) { + ResourceCondition ncondition = new ResourceCondition(); + ncondition.type = 1; + this.condition = ncondition; + } else if (condPattern.equals("-s")) { + ResourceCondition ncondition = new ResourceCondition(); + ncondition.type = 2; + this.condition = ncondition; + } else { + PatternCondition ncondition = new PatternCondition(); + int flags = Pattern.DOTALL; + if (isNocase()) { + flags |= Pattern.CASE_INSENSITIVE; + } + ncondition.pattern = Pattern.compile(condPattern, flags); + this.condition = ncondition; + } + } + + public Matcher getMatcher() { + if (condition instanceof PatternCondition) { + return ((PatternCondition) condition).getMatcher(); + } + return null; + } + + /** + * String representation. + */ + @Override + public String toString() { + return "RewriteCond " + testString + " " + condPattern + + ((flagsString != null) ? (" " + flagsString) : ""); + } + + + protected boolean positive = true; + + protected Substitution test = null; + + protected Condition condition = null; + + /** + * This makes the test case-insensitive, i.e., there is no difference between + * 'A-Z' and 'a-z' both in the expanded TestString and the CondPattern. This + * flag is effective only for comparisons between TestString and CondPattern. + * It has no effect on filesystem and subrequest checks. + */ + public boolean nocase = false; + + /** + * Use this to combine rule conditions with a local OR instead of the implicit AND. + */ + public boolean ornext = false; + + /** + * Evaluate the condition based on the context + * + * @param rule corresponding matched rule + * @param cond last matched condition + * @param resolver Property resolver + * @return true if the condition matches + */ + public boolean evaluate(Matcher rule, Matcher cond, Resolver resolver) { + String value = test.evaluate(rule, cond, resolver); + if (positive) { + return condition.evaluate(value, resolver); + } else { + return !condition.evaluate(value, resolver); + } + } + + public boolean isNocase() { + return nocase; + } + + public void setNocase(boolean nocase) { + this.nocase = nocase; + } + + public boolean isOrnext() { + return ornext; + } + + public void setOrnext(boolean ornext) { + this.ornext = ornext; + } + + public boolean isPositive() { + return positive; + } + + public void setPositive(boolean positive) { + this.positive = positive; + } +} diff --git a/java/org/apache/catalina/valves/rewrite/RewriteMap.java b/java/org/apache/catalina/valves/rewrite/RewriteMap.java new file mode 100644 index 0000000..6077c27 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/RewriteMap.java @@ -0,0 +1,76 @@ +/* + * 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.catalina.valves.rewrite; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Interface for user defined lookup/replacement logic that can be defined in + * a {@code rewrite.config} file by a {@code RewriteMap} directive. Such a map + * can then be used by a {@code RewriteRule} defined in the same file. + *

    + * An example {@code rewrite.config} file could look like: + *

    + * RewriteMap uc example.UpperCaseMap
    + *
    + * RewriteRule ^/(.*)$ ${uc:$1}
    + * 
    + * + * One parameter can be optionally appended to the {@code RewriteMap} directive. + * This could be used – for example – to specify a name of a file, that + * contains a lookup table used by the implementation of the map. + */ +public interface RewriteMap { + + /** + * Optional parameter that can be defined through the {@code RewriteMap} + * directive in the {@code rewrite.config} file. + * + * @param params the optional parameter + * @return value is currently ignored + */ + String setParameters(String params); + + /** + * Optional parameters that can be defined through the {@code RewriteMap} + * directive in the {@code rewrite.config} file. + *

    + * This method will be called, if there are more than one parameters defined. + * + * @param params the optional parameters + */ + default void setParameters(String... params) { + if (params == null) { + return; + } + if (params.length > 1) { + throw new IllegalArgumentException( + StringManager.getManager(RewriteMap.class).getString("rewriteMap.tooManyParameters")); + } + setParameters(params[0]); + } + + /** + * Maps a key to a replacement value.
    + * The method is free to return {@code null} to indicate, that the default + * value from the {@code RewriteRule} directive should be used. + * + * @param key used by the actual implementation to generate a mapped value + * @return mapped value or {@code null} + */ + String lookup(String key); +} diff --git a/java/org/apache/catalina/valves/rewrite/RewriteRule.java b/java/org/apache/catalina/valves/rewrite/RewriteRule.java new file mode 100644 index 0000000..269eeda --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/RewriteRule.java @@ -0,0 +1,577 @@ +/* + * 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.catalina.valves.rewrite; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RewriteRule { + + protected RewriteCond[] conditions = new RewriteCond[0]; + + protected ThreadLocal pattern = new ThreadLocal<>(); + protected Substitution substitution = null; + + protected String patternString = null; + protected String substitutionString = null; + protected String flagsString = null; + protected boolean positive = true; + + public void parse(Map maps) { + // Parse the substitution + if (!"-".equals(substitutionString)) { + substitution = new Substitution(); + substitution.setSub(substitutionString); + substitution.parse(maps); + substitution.setEscapeBackReferences(isEscapeBackReferences()); + } + // Parse the pattern + if (patternString.startsWith("!")) { + positive = false; + patternString = patternString.substring(1); + } + int flags = Pattern.DOTALL; + if (isNocase()) { + flags |= Pattern.CASE_INSENSITIVE; + } + Pattern.compile(patternString, flags); + // Parse conditions + for (RewriteCond condition : conditions) { + condition.parse(maps); + } + // Parse flag which have substitution values + if (isEnv()) { + for (String s : envValue) { + Substitution newEnvSubstitution = new Substitution(); + newEnvSubstitution.setSub(s); + newEnvSubstitution.parse(maps); + envSubstitution.add(newEnvSubstitution); + envResult.add(new ThreadLocal<>()); + } + } + if (isCookie()) { + cookieSubstitution = new Substitution(); + cookieSubstitution.setSub(cookieValue); + cookieSubstitution.parse(maps); + } + } + + public void addCondition(RewriteCond condition) { + RewriteCond[] conditions = Arrays.copyOf(this.conditions, this.conditions.length + 1); + conditions[this.conditions.length] = condition; + this.conditions = conditions; + } + + /** + * Evaluate the rule based on the context + * @param url The char sequence + * @param resolver Property resolver + * @return null if no rewrite took place + */ + public CharSequence evaluate(CharSequence url, Resolver resolver) { + Pattern pattern = this.pattern.get(); + if (pattern == null) { + // Parse the pattern + int flags = Pattern.DOTALL; + if (isNocase()) { + flags |= Pattern.CASE_INSENSITIVE; + } + pattern = Pattern.compile(patternString, flags); + this.pattern.set(pattern); + } + Matcher matcher = pattern.matcher(url); + // Use XOR + if (positive ^ matcher.matches()) { + // Evaluation done + return null; + } + // Evaluate conditions + boolean done = false; + boolean rewrite = true; + Matcher lastMatcher = null; + int pos = 0; + while (!done) { + if (pos < conditions.length) { + rewrite = conditions[pos].evaluate(matcher, lastMatcher, resolver); + if (rewrite) { + Matcher lastMatcher2 = conditions[pos].getMatcher(); + if (lastMatcher2 != null) { + lastMatcher = lastMatcher2; + } + while (pos < conditions.length && conditions[pos].isOrnext()) { + pos++; + } + } else if (!conditions[pos].isOrnext()) { + done = true; + } + pos++; + } else { + done = true; + } + } + // Use the substitution to rewrite the url + if (rewrite) { + if (isEnv()) { + for (int i = 0; i < envSubstitution.size(); i++) { + envResult.get(i).set(envSubstitution.get(i).evaluate(matcher, lastMatcher, resolver)); + } + } + if (isCookie()) { + cookieResult.set(cookieSubstitution.evaluate(matcher, lastMatcher, resolver)); + } + if (substitution != null) { + return substitution.evaluate(matcher, lastMatcher, resolver); + } else { + return url; + } + } else { + return null; + } + } + + + /** + * String representation. + */ + @Override + public String toString() { + return "RewriteRule " + patternString + " " + substitutionString + + ((flagsString != null) ? (" " + flagsString) : ""); + } + + + private boolean escapeBackReferences = false; + + /** + * This flag chains the current rule with the next rule (which itself + * can be chained with the following rule, etc.). This has the following + * effect: if a rule matches, then processing continues as usual, i.e., + * the flag has no effect. If the rule does not match, then all following + * chained rules are skipped. For instance, use it to remove the ".www" + * part inside a per-directory rule set when you let an external redirect + * happen (where the ".www" part should not to occur!). + */ + protected boolean chain = false; + + /** + * This sets a cookie on the client's browser. The cookie's name is + * specified by NAME and the value is VAL. The domain field is the domain + * of the cookie, such as '.apache.org',the optional lifetime + * is the lifetime of the cookie in minutes, and the optional path is the + * path of the cookie + */ + protected boolean cookie = false; + protected String cookieName = null; + protected String cookieValue = null; + protected String cookieDomain = null; + protected int cookieLifetime = -1; + protected String cookiePath = null; + protected boolean cookieSecure = false; + protected boolean cookieHttpOnly = false; + protected Substitution cookieSubstitution = null; + protected ThreadLocal cookieResult = new ThreadLocal<>(); + + /** + * This forces a request attribute named VAR to be set to the value VAL, + * where VAL can contain regexp back references $N and %N which will be + * expanded. Multiple env flags are allowed. + */ + protected boolean env = false; + protected ArrayList envName = new ArrayList<>(); + protected ArrayList envValue = new ArrayList<>(); + protected ArrayList envSubstitution = new ArrayList<>(); + protected ArrayList> envResult = new ArrayList<>(); + + /** + * This forces the current URL to be forbidden, i.e., it immediately sends + * back an HTTP response of 403 (FORBIDDEN). Use this flag in conjunction + * with appropriate RewriteConds to conditionally block some URLs. + */ + protected boolean forbidden = false; + + /** + * This forces the current URL to be gone, i.e., it immediately sends + * back an HTTP response of 410 (GONE). Use this flag to mark pages which + * no longer exist as gone. + */ + protected boolean gone = false; + + /** + * Host. This means this rule and its associated conditions will apply to + * host, allowing host rewriting (ex: redirecting internally *.foo.com to + * bar.foo.com). + */ + protected boolean host = false; + + /** + * Stop the rewriting process here and don't apply any more rewriting + * rules. This corresponds to the Perl last command or the break command + * from the C language. Use this flag to prevent the currently rewritten + * URL from being rewritten further by following rules. For example, use + * it to rewrite the root-path URL ('/') to a real one, e.g., '/e/www/'. + */ + protected boolean last = false; + + /** + * Re-run the rewriting process (starting again with the first rewriting + * rule). Here the URL to match is again not the original URL but the URL + * from the last rewriting rule. This corresponds to the Perl next + * command or the continue command from the C language. Use this flag to + * restart the rewriting process, i.e., to immediately go to the top of + * the loop. But be careful not to create an infinite loop! + */ + protected boolean next = false; + + /** + * This makes the Pattern case-insensitive, i.e., there is no difference + * between 'A-Z' and 'a-z' when Pattern is matched against the current + * URL. + */ + protected boolean nocase = false; + + /** + * This flag keeps mod_rewrite from applying the usual URI escaping rules + * to the result of a rewrite. Ordinarily, special characters (such as + * '%', '$', ';', and so on) will be escaped into their hexcode + * equivalents ('%25', '%24', and '%3B', respectively); this flag + * prevents this from being done. This allows percent symbols to appear + * in the output, as in + * RewriteRule /foo/(.*) /bar?arg=P1\%3d$1 [R,NE] + * which would turn '/foo/zed' into a safe request for '/bar?arg=P1=zed'. + */ + protected boolean noescape = false; + + /** + * This flag forces the rewriting engine to skip a rewriting rule if the + * current request is an internal sub-request. For instance, sub-requests + * occur internally in Apache when mod_include tries to find out + * information about possible directory default files (index.xxx). On + * sub-requests it is not always useful and even sometimes causes a + * failure to if the complete set of rules are applied. Use this flag to + * exclude some rules. Use the following rule for your decision: whenever + * you prefix some URLs with CGI-scripts to force them to be processed by + * the CGI-script, the chance is high that you will run into problems (or + * even overhead) on sub-requests. In these cases, use this flag. + */ + protected boolean nosubreq = false; + + /* + * Note: No proxy + */ + + /* + * Note: No passthrough + */ + + /** + * This flag forces the rewriting engine to append a query string part in + * the substitution string to the existing one instead of replacing it. + * Use this when you want to add more data to the query string via + * a rewrite rule. + */ + protected boolean qsappend = false; + + /** + * When the requested URI contains a query string, and the target URI does + * not, the default behavior of RewriteRule is to copy that query string + * to the target URI. Using the [QSD] flag causes the query string + * to be discarded. + * Using [QSD] and [QSA] together will result in [QSD] taking precedence. + */ + protected boolean qsdiscard = false; + + /** + * Prefix Substitution with http://thishost[:thisport]/ (which makes the + * new URL a URI) to force an external redirection. If no code is given + * an HTTP response of 302 (FOUND, previously MOVED TEMPORARILY) is used. + * If you want to use other response codes in the range 300-399 just + * specify them as a number or use one of the following symbolic names: + * temp (default), permanent, seeother. Use it for rules which should + * canonicalize the URL and give it back to the client, e.g., translate + * "/~" into "/u/" or always append a slash to /u/user, etc. Note: + * When you use this flag, make sure that the substitution field is a + * valid URL! If not, you are redirecting to an invalid location! + * And remember that this flag itself only prefixes the URL with + * http://thishost[:thisport]/, rewriting continues. Usually you also + * want to stop and do the redirection immediately. To stop the + * rewriting you also have to provide the 'L' flag. + */ + protected boolean redirect = false; + protected int redirectCode = 0; + + /** + * This flag forces the rewriting engine to skip the next num rules in + * sequence when the current rule matches. Use this to make pseudo + * if-then-else constructs: The last rule of the then-clause becomes + * skip=N where N is the number of rules in the else-clause. + * (This is not the same as the 'chain|C' flag!) + */ + protected int skip = 0; + + /** + * Force the MIME-type of the target file to be MIME-type. For instance, + * this can be used to setup the content-type based on some conditions. + * For example, the following snippet allows .php files to be displayed + * by mod_php if they are called with the .phps extension: + * RewriteRule ^(.+\.php)s$ $1 [T=application/x-httpd-php-source] + */ + protected boolean type = false; + protected String typeValue = null; + + /** + * Allows skipping the next valve in the Catalina pipeline. + */ + protected boolean valveSkip = false; + + public boolean isEscapeBackReferences() { + return escapeBackReferences; + } + public void setEscapeBackReferences(boolean escapeBackReferences) { + this.escapeBackReferences = escapeBackReferences; + } + public boolean isChain() { + return chain; + } + public void setChain(boolean chain) { + this.chain = chain; + } + public RewriteCond[] getConditions() { + return conditions; + } + public void setConditions(RewriteCond[] conditions) { + this.conditions = conditions; + } + public boolean isCookie() { + return cookie; + } + public void setCookie(boolean cookie) { + this.cookie = cookie; + } + public String getCookieName() { + return cookieName; + } + public void setCookieName(String cookieName) { + this.cookieName = cookieName; + } + public String getCookieValue() { + return cookieValue; + } + public void setCookieValue(String cookieValue) { + this.cookieValue = cookieValue; + } + public String getCookieResult() { + return cookieResult.get(); + } + public boolean isEnv() { + return env; + } + public int getEnvSize() { + return envName.size(); + } + public void setEnv(boolean env) { + this.env = env; + } + public String getEnvName(int i) { + return envName.get(i); + } + public void addEnvName(String envName) { + this.envName.add(envName); + } + public String getEnvValue(int i) { + return envValue.get(i); + } + public void addEnvValue(String envValue) { + this.envValue.add(envValue); + } + public String getEnvResult(int i) { + return envResult.get(i).get(); + } + public boolean isForbidden() { + return forbidden; + } + public void setForbidden(boolean forbidden) { + this.forbidden = forbidden; + } + public boolean isGone() { + return gone; + } + public void setGone(boolean gone) { + this.gone = gone; + } + public boolean isLast() { + return last; + } + public void setLast(boolean last) { + this.last = last; + } + public boolean isNext() { + return next; + } + public void setNext(boolean next) { + this.next = next; + } + public boolean isNocase() { + return nocase; + } + public void setNocase(boolean nocase) { + this.nocase = nocase; + } + public boolean isNoescape() { + return noescape; + } + public void setNoescape(boolean noescape) { + this.noescape = noescape; + } + public boolean isNosubreq() { + return nosubreq; + } + public void setNosubreq(boolean nosubreq) { + this.nosubreq = nosubreq; + } + public boolean isQsappend() { + return qsappend; + } + public void setQsappend(boolean qsappend) { + this.qsappend = qsappend; + } + public final boolean isQsdiscard() { + return qsdiscard; + } + public final void setQsdiscard(boolean qsdiscard) { + this.qsdiscard = qsdiscard; + } + public boolean isRedirect() { + return redirect; + } + public void setRedirect(boolean redirect) { + this.redirect = redirect; + } + public int getRedirectCode() { + return redirectCode; + } + public void setRedirectCode(int redirectCode) { + this.redirectCode = redirectCode; + } + public int getSkip() { + return skip; + } + public void setSkip(int skip) { + this.skip = skip; + } + public Substitution getSubstitution() { + return substitution; + } + public void setSubstitution(Substitution substitution) { + this.substitution = substitution; + } + public boolean isType() { + return type; + } + public void setType(boolean type) { + this.type = type; + } + public String getTypeValue() { + return typeValue; + } + public void setTypeValue(String typeValue) { + this.typeValue = typeValue; + } + + public String getPatternString() { + return patternString; + } + + public void setPatternString(String patternString) { + this.patternString = patternString; + } + + public String getSubstitutionString() { + return substitutionString; + } + + public void setSubstitutionString(String substitutionString) { + this.substitutionString = substitutionString; + } + + public final String getFlagsString() { + return flagsString; + } + + public final void setFlagsString(String flagsString) { + this.flagsString = flagsString; + } + + public boolean isHost() { + return host; + } + + public void setHost(boolean host) { + this.host = host; + } + + public String getCookieDomain() { + return cookieDomain; + } + + public void setCookieDomain(String cookieDomain) { + this.cookieDomain = cookieDomain; + } + + public int getCookieLifetime() { + return cookieLifetime; + } + + public void setCookieLifetime(int cookieLifetime) { + this.cookieLifetime = cookieLifetime; + } + + public String getCookiePath() { + return cookiePath; + } + + public void setCookiePath(String cookiePath) { + this.cookiePath = cookiePath; + } + + public boolean isCookieSecure() { + return cookieSecure; + } + + public void setCookieSecure(boolean cookieSecure) { + this.cookieSecure = cookieSecure; + } + + public boolean isCookieHttpOnly() { + return cookieHttpOnly; + } + + public void setCookieHttpOnly(boolean cookieHttpOnly) { + this.cookieHttpOnly = cookieHttpOnly; + } + + public boolean isValveSkip() { + return this.valveSkip; + } + + public void setValveSkip(boolean valveSkip) { + this.valveSkip = valveSkip; + } + +} diff --git a/java/org/apache/catalina/valves/rewrite/RewriteValve.java b/java/org/apache/catalina/valves/rewrite/RewriteValve.java new file mode 100644 index 0000000..d152106 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/RewriteValve.java @@ -0,0 +1,818 @@ +/* + * 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.catalina.valves.rewrite; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.URLEncoder; +import org.apache.catalina.valves.ValveBase; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.CharChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource; +import org.apache.tomcat.util.http.RequestUtil; + +/** + * Note: Extra caution should be used when adding a Rewrite Rule. When + * specifying a regex to match for in a Rewrite Rule, certain regex could allow + * an attacker to DoS your server, as Java's regex parsing is vulnerable to + * "catastrophic backtracking" (also known as "Regular expression Denial of + * Service", or ReDoS). There are some open source tools to help detect + * vulnerable regex, though in general it is a hard problem. A good defence is + * to use a regex debugger on your desired regex, and read more on the subject + * of catastrophic backtracking. + * + * @see OWASP + * ReDoS + */ +public class RewriteValve extends ValveBase { + + /** + * The rewrite rules that the valve will use. + */ + protected RewriteRule[] rules = null; + + + /** + * If rewriting occurs, the whole request will be processed again. + */ + protected ThreadLocal invoked = new ThreadLocal<>(); + + + /** + * Relative path to the configuration file. + * Note: If the valve's container is a context, this will be relative to + * /WEB-INF/. + */ + protected String resourcePath = "rewrite.config"; + + + /** + * Will be set to true if the valve is associated with a context. + */ + protected boolean context = false; + + + /** + * enabled this component + */ + protected boolean enabled = true; + + /** + * Maps to be used by the rules. + */ + protected Map maps = new ConcurrentHashMap<>(); + + + /** + * Maps configuration. + */ + protected ArrayList mapsConfiguration = new ArrayList<>(); + + + public RewriteValve() { + super(true); + } + + + public boolean getEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + containerLog = LogFactory.getLog(getContainer().getLogName() + ".rewrite"); + } + + + @Override + protected void startInternal() throws LifecycleException { + + super.startInternal(); + + InputStream is = null; + + // Process configuration file for this valve + if (getContainer() instanceof Context) { + context = true; + String webInfResourcePath = "/WEB-INF/" + resourcePath; + is = ((Context) getContainer()).getServletContext() + .getResourceAsStream(webInfResourcePath); + if (containerLog.isDebugEnabled()) { + if (is == null) { + containerLog.debug(sm.getString("rewriteValve.noConfiguration", webInfResourcePath)); + } else { + containerLog.debug(sm.getString("rewriteValve.readConfiguration", webInfResourcePath)); + } + } + } else { + String resourceName = Container.getConfigPath(getContainer(), resourcePath); + try { + ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getResource(resourceName); + is = resource.getInputStream(); + } catch (IOException e) { + if (containerLog.isDebugEnabled()) { + containerLog.debug(sm.getString("rewriteValve.noConfiguration", resourceName), e); + } + } + } + + if (is == null) { + // Will use management operations to configure the valve dynamically + return; + } + + try (InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); + BufferedReader reader = new BufferedReader(isr)) { + parse(reader); + } catch (IOException ioe) { + containerLog.error(sm.getString("rewriteValve.closeError"), ioe); + } finally { + try { + is.close(); + } catch (IOException e) { + containerLog.error(sm.getString("rewriteValve.closeError"), e); + } + } + + } + + public void setConfiguration(String configuration) + throws Exception { + if (containerLog == null) { + containerLog = LogFactory.getLog(getContainer().getLogName() + ".rewrite"); + } + maps.clear(); + parse(new BufferedReader(new StringReader(configuration))); + } + + public String getConfiguration() { + StringBuilder buffer = new StringBuilder(); + for (String mapConfiguration : mapsConfiguration) { + buffer.append(mapConfiguration).append("\r\n"); + } + if (mapsConfiguration.size() > 0) { + buffer.append("\r\n"); + } + for (RewriteRule rule : rules) { + for (int j = 0; j < rule.getConditions().length; j++) { + buffer.append(rule.getConditions()[j].toString()).append("\r\n"); + } + buffer.append(rule.toString()).append("\r\n").append("\r\n"); + } + return buffer.toString(); + } + + protected void parse(BufferedReader reader) throws LifecycleException { + List rules = new ArrayList<>(); + List conditions = new ArrayList<>(); + while (true) { + try { + String line = reader.readLine(); + if (line == null) { + break; + } + Object result = parse(line); + if (result instanceof RewriteRule) { + RewriteRule rule = (RewriteRule) result; + if (containerLog.isTraceEnabled()) { + containerLog.trace("Add rule with pattern " + rule.getPatternString() + + " and substitution " + rule.getSubstitutionString()); + } + for (int i = (conditions.size() - 1); i > 0; i--) { + if (conditions.get(i - 1).isOrnext()) { + conditions.get(i).setOrnext(true); + } + } + for (RewriteCond condition : conditions) { + if (containerLog.isTraceEnabled()) { + RewriteCond cond = condition; + containerLog.trace("Add condition " + cond.getCondPattern() + + " test " + cond.getTestString() + " to rule with pattern " + + rule.getPatternString() + " and substitution " + + rule.getSubstitutionString() + (cond.isOrnext() ? " [OR]" : "") + + (cond.isNocase() ? " [NC]" : "")); + } + rule.addCondition(condition); + } + conditions.clear(); + rules.add(rule); + } else if (result instanceof RewriteCond) { + conditions.add((RewriteCond) result); + } else if (result instanceof Object[]) { + String mapName = (String) ((Object[]) result)[0]; + RewriteMap map = (RewriteMap) ((Object[]) result)[1]; + maps.put(mapName, map); + // Keep the original configuration line as it is not possible to get + // the parameters back without an API change + mapsConfiguration.add(line); + if (map instanceof Lifecycle) { + ((Lifecycle) map).start(); + } + } + } catch (IOException e) { + containerLog.error(sm.getString("rewriteValve.readError"), e); + } + } + this.rules = rules.toArray(new RewriteRule[0]); + + // Finish parsing the rules + for (RewriteRule rule : this.rules) { + rule.parse(maps); + } + } + + @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + for (RewriteMap map : maps.values()) { + if (map instanceof Lifecycle) { + ((Lifecycle) map).stop(); + } + } + maps.clear(); + rules = null; + } + + + @Override + public void invoke(Request request, Response response) + throws IOException, ServletException { + + if (!getEnabled() || rules == null || rules.length == 0) { + getNext().invoke(request, response); + return; + } + + if (Boolean.TRUE.equals(invoked.get())) { + try { + getNext().invoke(request, response); + } finally { + invoked.set(null); + } + return; + } + + try { + + Resolver resolver = new ResolverImpl(request); + + invoked.set(Boolean.TRUE); + + // As long as MB isn't a char sequence or affiliated, this has to be + // converted to a string + Charset uriCharset = request.getConnector().getURICharset(); + String originalQueryStringEncoded = request.getQueryString(); + MessageBytes urlMB = + context ? request.getRequestPathMB() : request.getDecodedRequestURIMB(); + urlMB.toChars(); + CharSequence urlDecoded = urlMB.getCharChunk(); + CharSequence host = request.getServerName(); + boolean rewritten = false; + boolean done = false; + boolean qsa = false; + boolean qsd = false; + boolean valveSkip = false; + for (int i = 0; i < rules.length; i++) { + RewriteRule rule = rules[i]; + CharSequence test = (rule.isHost()) ? host : urlDecoded; + CharSequence newtest = rule.evaluate(test, resolver); + if (newtest != null && !test.toString().equals(newtest.toString())) { + if (containerLog.isTraceEnabled()) { + containerLog.trace("Rewrote " + test + " as " + newtest + + " with rule pattern " + rule.getPatternString()); + } + if (rule.isHost()) { + host = newtest; + } else { + urlDecoded = newtest; + } + rewritten = true; + } + + // Check QSA before the final reply + if (!qsa && newtest != null && rule.isQsappend()) { + qsa = true; + } + + if (!qsd && newtest != null && rule.isQsdiscard()) { + qsd = true; + } + + if (!valveSkip && newtest != null && rule.isValveSkip()) { + valveSkip = true; + } + + // Final reply + + // - forbidden + if (rule.isForbidden() && newtest != null) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + done = true; + break; + } + // - gone + if (rule.isGone() && newtest != null) { + response.sendError(HttpServletResponse.SC_GONE); + done = true; + break; + } + + // - redirect (code) + if (rule.isRedirect() && newtest != null) { + // Append the query string to the url if there is one and it + // hasn't been rewritten + String urlStringDecoded = urlDecoded.toString(); + int index = urlStringDecoded.indexOf('?'); + String rewrittenQueryStringDecoded; + if (index == -1) { + rewrittenQueryStringDecoded = null; + } else { + rewrittenQueryStringDecoded = urlStringDecoded.substring(index + 1); + urlStringDecoded = urlStringDecoded.substring(0, index); + } + + StringBuilder urlStringEncoded = + new StringBuilder(URLEncoder.DEFAULT.encode(urlStringDecoded, uriCharset)); + if (!qsd && originalQueryStringEncoded != null + && originalQueryStringEncoded.length() > 0) { + if (rewrittenQueryStringDecoded == null) { + urlStringEncoded.append('?'); + urlStringEncoded.append(originalQueryStringEncoded); + } else { + if (qsa) { + // if qsa is specified append the query + urlStringEncoded.append('?'); + urlStringEncoded.append(URLEncoder.QUERY.encode( + rewrittenQueryStringDecoded, uriCharset)); + urlStringEncoded.append('&'); + urlStringEncoded.append(originalQueryStringEncoded); + } else if (index == urlStringEncoded.length() - 1) { + // if the ? is the last character delete it, its only purpose was to + // prevent the rewrite module from appending the query string + urlStringEncoded.deleteCharAt(index); + } else { + urlStringEncoded.append('?'); + urlStringEncoded.append(URLEncoder.QUERY.encode( + rewrittenQueryStringDecoded, uriCharset)); + } + } + } else if (rewrittenQueryStringDecoded != null) { + urlStringEncoded.append('?'); + urlStringEncoded.append( + URLEncoder.QUERY.encode(rewrittenQueryStringDecoded, uriCharset)); + } + + // Insert the context if + // 1. this valve is associated with a context + // 2. the url starts with a leading slash + // 3. the url isn't absolute + if (context && urlStringEncoded.charAt(0) == '/' && + !UriUtil.hasScheme(urlStringEncoded)) { + urlStringEncoded.insert(0, request.getContext().getEncodedPath()); + } + if (rule.isNoescape()) { + response.sendRedirect( + UDecoder.URLDecode(urlStringEncoded.toString(), uriCharset)); + } else { + response.sendRedirect(urlStringEncoded.toString()); + } + response.setStatus(rule.getRedirectCode()); + done = true; + break; + } + + // Reply modification + + // - cookie + if (rule.isCookie() && newtest != null) { + Cookie cookie = new Cookie(rule.getCookieName(), + rule.getCookieResult()); + cookie.setDomain(rule.getCookieDomain()); + cookie.setMaxAge(rule.getCookieLifetime()); + cookie.setPath(rule.getCookiePath()); + cookie.setSecure(rule.isCookieSecure()); + cookie.setHttpOnly(rule.isCookieHttpOnly()); + response.addCookie(cookie); + } + // - env (note: this sets a request attribute) + if (rule.isEnv() && newtest != null) { + for (int j = 0; j < rule.getEnvSize(); j++) { + request.setAttribute(rule.getEnvName(j), rule.getEnvResult(j)); + } + } + // - content type (note: this will not force the content type, use a filter + // to do that) + if (rule.isType() && newtest != null) { + response.setContentType(rule.getTypeValue()); + } + + // Control flow processing + + // - chain (skip remaining chained rules if this one does not match) + if (rule.isChain() && newtest == null) { + for (int j = i; j < rules.length; j++) { + if (!rules[j].isChain()) { + i = j; + break; + } + } + continue; + } + // - last (stop rewriting here) + if (rule.isLast() && newtest != null) { + break; + } + // - next (redo again) + if (rule.isNext() && newtest != null) { + i = 0; + continue; + } + // - skip (n rules) + if (newtest != null) { + i += rule.getSkip(); + } + + } + + if (rewritten) { + if (!done) { + // See if we need to replace the query string + String urlStringDecoded = urlDecoded.toString(); + String queryStringDecoded = null; + int queryIndex = urlStringDecoded.indexOf('?'); + if (queryIndex != -1) { + queryStringDecoded = urlStringDecoded.substring(queryIndex+1); + urlStringDecoded = urlStringDecoded.substring(0, queryIndex); + } + // Save the current context path before re-writing starts + String contextPath = null; + if (context) { + contextPath = request.getContextPath(); + } + // Populated the encoded (i.e. undecoded) requestURI + request.getCoyoteRequest().requestURI().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + CharChunk chunk = request.getCoyoteRequest().requestURI().getCharChunk(); + if (context) { + // This is neither decoded nor normalized + chunk.append(contextPath); + } + chunk.append(URLEncoder.DEFAULT.encode(urlStringDecoded, uriCharset)); + // Decoded and normalized URI + // Rewriting may have denormalized the URL + urlStringDecoded = RequestUtil.normalize(urlStringDecoded); + request.getCoyoteRequest().decodedURI().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().decodedURI().getCharChunk(); + if (context) { + // This is decoded and normalized + chunk.append(request.getServletContext().getContextPath()); + } + chunk.append(urlStringDecoded); + // Set the new Query if there is one + if (queryStringDecoded != null) { + request.getCoyoteRequest().queryString().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().queryString().getCharChunk(); + chunk.append(URLEncoder.QUERY.encode(queryStringDecoded, uriCharset)); + if (qsa && originalQueryStringEncoded != null && + originalQueryStringEncoded.length() > 0) { + chunk.append('&'); + chunk.append(originalQueryStringEncoded); + } + } + // Set the new host if it changed + if (!host.equals(request.getServerName())) { + request.getCoyoteRequest().serverName().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().serverName().getCharChunk(); + chunk.append(host.toString()); + } + request.getMappingData().recycle(); + // Reinvoke the whole request recursively + Connector connector = request.getConnector(); + try { + if (!connector.getProtocolHandler().getAdapter().prepare( + request.getCoyoteRequest(), response.getCoyoteResponse())) { + return; + } + } catch (Exception e) { + // This doesn't actually happen in the Catalina adapter implementation + } + Pipeline pipeline = connector.getService().getContainer().getPipeline(); + request.setAsyncSupported(pipeline.isAsyncSupported()); + pipeline.getFirst().invoke(request, response); + } + } else { + Valve next = getNext(); + if (valveSkip) { + next = next.getNext(); + if (next == null) { + // Ignore and invoke the next valve normally + next = getNext(); + } + } + next.invoke(request, response); + } + + } finally { + invoked.set(null); + } + + } + + + /** + * This factory method will parse a line formed like: + * + * Example: + * RewriteCond %{REMOTE_HOST} ^host1.* [OR] + * + * @param line A line from the rewrite configuration + * @return The condition, rule or map resulting from parsing the line + */ + public static Object parse(String line) { + QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(line); + if (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (token.equals("RewriteCond")) { + // RewriteCond TestString CondPattern [Flags] + RewriteCond condition = new RewriteCond(); + if (tokenizer.countTokens() < 2) { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidLine", line)); + } + condition.setTestString(tokenizer.nextToken()); + condition.setCondPattern(tokenizer.nextToken()); + if (tokenizer.hasMoreTokens()) { + String flags = tokenizer.nextToken(); + condition.setFlagsString(flags); + if (flags.startsWith("[") && flags.endsWith("]")) { + flags = flags.substring(1, flags.length() - 1); + } + StringTokenizer flagsTokenizer = new StringTokenizer(flags, ","); + while (flagsTokenizer.hasMoreElements()) { + parseCondFlag(line, condition, flagsTokenizer.nextToken()); + } + } + return condition; + } else if (token.equals("RewriteRule")) { + // RewriteRule Pattern Substitution [Flags] + RewriteRule rule = new RewriteRule(); + if (tokenizer.countTokens() < 2) { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidLine", line)); + } + rule.setPatternString(tokenizer.nextToken()); + rule.setSubstitutionString(tokenizer.nextToken()); + if (tokenizer.hasMoreTokens()) { + String flags = tokenizer.nextToken(); + rule.setFlagsString(flags); + if (flags.startsWith("[") && flags.endsWith("]")) { + flags = flags.substring(1, flags.length() - 1); + } + StringTokenizer flagsTokenizer = new StringTokenizer(flags, ","); + while (flagsTokenizer.hasMoreElements()) { + parseRuleFlag(line, rule, flagsTokenizer.nextToken()); + } + } + return rule; + } else if (token.equals("RewriteMap")) { + // RewriteMap name rewriteMapClassName whateverOptionalParameterInWhateverFormat + if (tokenizer.countTokens() < 2) { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidLine", line)); + } + String name = tokenizer.nextToken(); + String rewriteMapClassName = tokenizer.nextToken(); + RewriteMap map = null; + if (rewriteMapClassName.startsWith("int:")) { + map = InternalRewriteMap.toMap(rewriteMapClassName.substring("int:".length())); + } else if (rewriteMapClassName.startsWith("txt:")) { + map = new RandomizedTextRewriteMap(rewriteMapClassName.substring("txt:".length()), false); + } else if (rewriteMapClassName.startsWith("rnd:")) { + map = new RandomizedTextRewriteMap(rewriteMapClassName.substring("rnd:".length()), true); + } else if (rewriteMapClassName.startsWith("prg:")) { + // https://httpd.apache.org/docs/2.4/rewrite/rewritemap.html#prg + // Not worth implementing further since this is a simpler CGI + // piping stdin/stdout from an external native process + // Instead assume a class and use the RewriteMap interface + rewriteMapClassName = rewriteMapClassName.substring("prg:".length()); + } else if (rewriteMapClassName.startsWith("dbm:")) { + // FIXME: https://httpd.apache.org/docs/2.4/rewrite/rewritemap.html#dbm + // Probably too specific to HTTP Server to implement + } else if (rewriteMapClassName.startsWith("dbd:") || rewriteMapClassName.startsWith("fastdbd:")) { + // FIXME: https://httpd.apache.org/docs/2.4/rewrite/rewritemap.html#dbd + } + if (map == null) { + try { + map = (RewriteMap) (Class.forName( + rewriteMapClassName).getConstructor().newInstance()); + } catch (Exception e) { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidMapClassName", line)); + } + } + if (tokenizer.hasMoreTokens()) { + if (tokenizer.countTokens() == 1) { + map.setParameters(tokenizer.nextToken()); + } else { + List params = new ArrayList<>(); + while (tokenizer.hasMoreTokens()) { + params.add(tokenizer.nextToken()); + } + map.setParameters(params.toArray(new String[0])); + } + } + return new Object[] { name, map }; + } else if (token.startsWith("#")) { + // it's a comment, ignore it + } else { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidLine", line)); + } + } + return null; + } + + + /** + * Parser for RewriteCond flags. + * @param line The configuration line being parsed + * @param condition The current condition + * @param flag The flag + */ + protected static void parseCondFlag(String line, RewriteCond condition, String flag) { + if (flag.equals("NC") || flag.equals("nocase")) { + condition.setNocase(true); + } else if (flag.equals("OR") || flag.equals("ornext")) { + condition.setOrnext(true); + } else { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidFlags", line, flag)); + } + } + + + /** + * Parser for RewriteRule flags. + * @param line The configuration line being parsed + * @param rule The current rule + * @param flag The flag + */ + protected static void parseRuleFlag(String line, RewriteRule rule, String flag) { + if (flag.equals("B")) { + rule.setEscapeBackReferences(true); + } else if (flag.equals("chain") || flag.equals("C")) { + rule.setChain(true); + } else if (flag.startsWith("cookie=") || flag.startsWith("CO=")) { + rule.setCookie(true); + if (flag.startsWith("cookie")) { + flag = flag.substring("cookie=".length()); + } else if (flag.startsWith("CO=")) { + flag = flag.substring("CO=".length()); + } + StringTokenizer tokenizer = new StringTokenizer(flag, ":"); + if (tokenizer.countTokens() < 2) { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidFlags", line, flag)); + } + rule.setCookieName(tokenizer.nextToken()); + rule.setCookieValue(tokenizer.nextToken()); + if (tokenizer.hasMoreTokens()) { + rule.setCookieDomain(tokenizer.nextToken()); + } + if (tokenizer.hasMoreTokens()) { + try { + rule.setCookieLifetime(Integer.parseInt(tokenizer.nextToken())); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidFlags", line, flag), e); + } + } + if (tokenizer.hasMoreTokens()) { + rule.setCookiePath(tokenizer.nextToken()); + } + if (tokenizer.hasMoreTokens()) { + rule.setCookieSecure(Boolean.parseBoolean(tokenizer.nextToken())); + } + if (tokenizer.hasMoreTokens()) { + rule.setCookieHttpOnly(Boolean.parseBoolean(tokenizer.nextToken())); + } + } else if (flag.startsWith("env=") || flag.startsWith("E=")) { + rule.setEnv(true); + if (flag.startsWith("env=")) { + flag = flag.substring("env=".length()); + } else if (flag.startsWith("E=")) { + flag = flag.substring("E=".length()); + } + int pos = flag.indexOf(':'); + if (pos == -1 || (pos + 1) == flag.length()) { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidFlags", line, flag)); + } + rule.addEnvName(flag.substring(0, pos)); + rule.addEnvValue(flag.substring(pos + 1)); + } else if (flag.startsWith("forbidden") || flag.startsWith("F")) { + rule.setForbidden(true); + } else if (flag.startsWith("gone") || flag.startsWith("G")) { + rule.setGone(true); + } else if (flag.startsWith("host") || flag.startsWith("H")) { + rule.setHost(true); + } else if (flag.startsWith("last") || flag.startsWith("L")) { + rule.setLast(true); + } else if (flag.startsWith("nocase") || flag.startsWith("NC")) { + rule.setNocase(true); + } else if (flag.startsWith("noescape") || flag.startsWith("NE")) { + rule.setNoescape(true); + } else if (flag.startsWith("next") || flag.startsWith("N")) { + rule.setNext(true); + // Note: Proxy is not supported as Tomcat does not have proxy + // capabilities + } else if (flag.startsWith("qsappend") || flag.startsWith("QSA")) { + rule.setQsappend(true); + } else if (flag.startsWith("qsdiscard") || flag.startsWith("QSD")) { + rule.setQsdiscard(true); + } else if (flag.startsWith("redirect") || flag.startsWith("R")) { + rule.setRedirect(true); + int redirectCode = HttpServletResponse.SC_FOUND; + if (flag.startsWith("redirect=") || flag.startsWith("R=")) { + if (flag.startsWith("redirect=")) { + flag = flag.substring("redirect=".length()); + } else if (flag.startsWith("R=")) { + flag = flag.substring("R=".length()); + } + switch(flag) { + case "temp": + redirectCode = HttpServletResponse.SC_FOUND; + break; + case "permanent": + redirectCode = HttpServletResponse.SC_MOVED_PERMANENTLY; + break; + case "seeother": + redirectCode = HttpServletResponse.SC_SEE_OTHER; + break; + default: + redirectCode = Integer.parseInt(flag); + break; + } + } + rule.setRedirectCode(redirectCode); + } else if (flag.startsWith("skip") || flag.startsWith("S")) { + if (flag.startsWith("skip=")) { + flag = flag.substring("skip=".length()); + } else if (flag.startsWith("S=")) { + flag = flag.substring("S=".length()); + } + rule.setSkip(Integer.parseInt(flag)); + } else if (flag.startsWith("type") || flag.startsWith("T")) { + if (flag.startsWith("type=")) { + flag = flag.substring("type=".length()); + } else if (flag.startsWith("T=")) { + flag = flag.substring("T=".length()); + } + rule.setType(true); + rule.setTypeValue(flag); + } else if (flag.startsWith("valveSkip") || flag.startsWith("VS")) { + rule.setValveSkip(true); + } else { + throw new IllegalArgumentException(sm.getString("rewriteValve.invalidFlags", line, flag)); + } + } +} diff --git a/java/org/apache/catalina/valves/rewrite/Substitution.java b/java/org/apache/catalina/valves/rewrite/Substitution.java new file mode 100644 index 0000000..b7e3915 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/Substitution.java @@ -0,0 +1,348 @@ +/* + * 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.catalina.valves.rewrite; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; + +import org.apache.catalina.util.URLEncoder; +import org.apache.tomcat.util.res.StringManager; + +public class Substitution { + + protected static final StringManager sm = StringManager.getManager(Substitution.class); + + public abstract static class SubstitutionElement { + public abstract String evaluate(Matcher rule, Matcher cond, Resolver resolver); + } + + public static class StaticElement extends SubstitutionElement { + public String value; + + @Override + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + return value; + } + + } + + public class RewriteRuleBackReferenceElement extends SubstitutionElement { + public int n; + @Override + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + String result = rule.group(n); + if (result == null) { + result = ""; + } + if (escapeBackReferences) { + // Note: This should be consistent with the way httpd behaves. + // We might want to consider providing a dedicated decoder + // with an option to add additional safe characters to + // provide users with more flexibility + return URLEncoder.DEFAULT.encode(result, resolver.getUriCharset()); + } else { + return result; + } + } + } + + public static class RewriteCondBackReferenceElement extends SubstitutionElement { + public int n; + @Override + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + return (cond.group(n) == null ? "" : cond.group(n)); + } + } + + public static class ServerVariableElement extends SubstitutionElement { + public String key; + @Override + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + return resolver.resolve(key); + } + } + + public static class ServerVariableEnvElement extends SubstitutionElement { + public String key; + @Override + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + return resolver.resolveEnv(key); + } + } + + public static class ServerVariableSslElement extends SubstitutionElement { + public String key; + @Override + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + return resolver.resolveSsl(key); + } + } + + public static class ServerVariableHttpElement extends SubstitutionElement { + public String key; + @Override + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + return resolver.resolveHttp(key); + } + } + + public class MapElement extends SubstitutionElement { + public RewriteMap map = null; + public SubstitutionElement[] defaultValue = null; + public SubstitutionElement[] key = null; + @Override + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + String result = map.lookup(evaluateSubstitution(key, rule, cond, resolver)); + if (result == null && defaultValue != null) { + result = evaluateSubstitution(defaultValue, rule, cond, resolver); + } + return result; + } + } + + protected SubstitutionElement[] elements = null; + + protected String sub = null; + public String getSub() { return sub; } + public void setSub(String sub) { this.sub = sub; } + + private boolean escapeBackReferences; + void setEscapeBackReferences(boolean escapeBackReferences) { + this.escapeBackReferences = escapeBackReferences; + } + + public void parse(Map maps) { + this.elements = parseSubstitution(sub, maps); + } + + private SubstitutionElement[] parseSubstitution(String sub, Map maps) { + + List elements = new ArrayList<>(); + int pos = 0; + int percentPos = 0; + int dollarPos = 0; + int backslashPos = 0; + + while (pos < sub.length()) { + percentPos = sub.indexOf('%', pos); + dollarPos = sub.indexOf('$', pos); + backslashPos = sub.indexOf('\\', pos); + if (percentPos == -1 && dollarPos == -1 && backslashPos == -1) { + // Static text + StaticElement newElement = new StaticElement(); + newElement.value = sub.substring(pos); + pos = sub.length(); + elements.add(newElement); + } else if (isFirstPos(backslashPos, dollarPos, percentPos)) { + if (backslashPos + 1 == sub.length()) { + throw new IllegalArgumentException(sm.getString("substitution.invalid", sub)); + } + StaticElement newElement = new StaticElement(); + newElement.value = sub.substring(pos, backslashPos) + sub.substring(backslashPos + 1, backslashPos + 2); + pos = backslashPos + 2; + elements.add(newElement); + } else if (isFirstPos(dollarPos, percentPos)) { + // $: back reference to rule or map lookup + if (dollarPos + 1 == sub.length()) { + throw new IllegalArgumentException(sm.getString("substitution.invalid", sub)); + } + if (pos < dollarPos) { + // Static text + StaticElement newElement = new StaticElement(); + newElement.value = sub.substring(pos, dollarPos); + elements.add(newElement); + } + if (Character.isDigit(sub.charAt(dollarPos + 1))) { + // $: back reference to rule + RewriteRuleBackReferenceElement newElement = new RewriteRuleBackReferenceElement(); + newElement.n = Character.digit(sub.charAt(dollarPos + 1), 10); + pos = dollarPos + 2; + elements.add(newElement); + } else if (sub.charAt(dollarPos + 1) == '{') { + // $: map lookup as ${mapname:key|default} + MapElement newElement = new MapElement(); + int open = sub.indexOf('{', dollarPos); + int colon = findMatchingColonOrBar(true, sub, open); + int def = findMatchingColonOrBar(false, sub, open); + int close = findMatchingBrace(sub, open); + if (!(-1 < open && open < colon && colon < close)) { + throw new IllegalArgumentException(sm.getString("substitution.invalid", sub)); + } + newElement.map = maps.get(sub.substring(open + 1, colon)); + if (newElement.map == null) { + throw new IllegalArgumentException(sm.getString("substitution.noMap", sub.substring(open + 1, colon), sub)); + } + String key = null; + String defaultValue = null; + if (def > -1) { + if (!(colon < def && def < close)) { + throw new IllegalArgumentException(sm.getString("substitution.invalid", sub)); + } + key = sub.substring(colon + 1, def); + defaultValue = sub.substring(def + 1, close); + } else { + key = sub.substring(colon + 1, close); + } + newElement.key = parseSubstitution(key, maps); + if (defaultValue != null) { + newElement.defaultValue = parseSubstitution(defaultValue, maps); + } + pos = close + 1; + elements.add(newElement); + } else { + throw new IllegalArgumentException(sm.getString("substitution.missingDigit", sub)); + } + } else { + // %: back reference to condition or server variable + if (percentPos + 1 == sub.length()) { + throw new IllegalArgumentException(sm.getString("substitution.invalid", sub)); + } + if (pos < percentPos) { + // Static text + StaticElement newElement = new StaticElement(); + newElement.value = sub.substring(pos, percentPos); + elements.add(newElement); + } + if (Character.isDigit(sub.charAt(percentPos + 1))) { + // %: back reference to condition + RewriteCondBackReferenceElement newElement = new RewriteCondBackReferenceElement(); + newElement.n = Character.digit(sub.charAt(percentPos + 1), 10); + pos = percentPos + 2; + elements.add(newElement); + } else if (sub.charAt(percentPos + 1) == '{') { + // %: server variable as %{variable} + SubstitutionElement newElement = null; + int open = sub.indexOf('{', percentPos); + int colon = findMatchingColonOrBar(true, sub, open); + int close = findMatchingBrace(sub, open); + if (!(-1 < open && open < close)) { + throw new IllegalArgumentException(sm.getString("substitution.invalid", sub)); + } + if (colon > -1 && open < colon && colon < close) { + String type = sub.substring(open + 1, colon); + if (type.equals("ENV")) { + newElement = new ServerVariableEnvElement(); + ((ServerVariableEnvElement) newElement).key = sub.substring(colon + 1, close); + } else if (type.equals("SSL")) { + newElement = new ServerVariableSslElement(); + ((ServerVariableSslElement) newElement).key = sub.substring(colon + 1, close); + } else if (type.equals("HTTP")) { + newElement = new ServerVariableHttpElement(); + ((ServerVariableHttpElement) newElement).key = sub.substring(colon + 1, close); + } else { + throw new IllegalArgumentException(sm.getString("substitution.badType", type, sub)); + } + } else { + newElement = new ServerVariableElement(); + ((ServerVariableElement) newElement).key = sub.substring(open + 1, close); + } + pos = close + 1; + elements.add(newElement); + } else { + throw new IllegalArgumentException(sm.getString("substitution.missingDigit", sub)); + } + } + } + + return elements.toArray(new SubstitutionElement[0]); + + } + + private static int findMatchingBrace(String sub, int start) { + int nesting = 1; + for (int i = start + 1; i < sub.length(); i++) { + char c = sub.charAt(i); + if (c == '{') { + char previousChar = sub.charAt(i-1); + if (previousChar == '$' || previousChar == '%') { + nesting++; + } + } else if (c == '}') { + nesting--; + if (nesting == 0) { + return i; + } + } + } + return -1; + } + + private static int findMatchingColonOrBar(boolean colon, String sub, int start) { + int nesting = 0; + for (int i = start + 1; i < sub.length(); i++) { + char c = sub.charAt(i); + if (c == '{') { + char previousChar = sub.charAt(i-1); + if (previousChar == '$' || previousChar == '%') { + nesting++; + } + } else if (c == '}') { + nesting--; + } else if (colon ? c == ':' : c =='|') { + if (nesting == 0) { + return i; + } + } + } + return -1; + } + + /** + * Evaluate the substitution based on the context. + * @param rule corresponding matched rule + * @param cond last matched condition + * @param resolver The property resolver + * @return The substitution result + */ + public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { + return evaluateSubstitution(elements, rule, cond, resolver); + } + + private String evaluateSubstitution(SubstitutionElement[] elements, Matcher rule, Matcher cond, Resolver resolver) { + StringBuilder buf = new StringBuilder(); + for (SubstitutionElement element : elements) { + buf.append(element.evaluate(rule, cond, resolver)); + } + return buf.toString(); + } + + /** + * Checks whether the first int is non negative and smaller than any non negative other int + * given with {@code others}. + * + * @param testPos + * integer to test against + * @param others + * list of integers that are paired against {@code testPos}. Any + * negative integer will be ignored. + * @return {@code true} if {@code testPos} is not negative and is less then any given other + * integer, {@code false} otherwise + */ + private boolean isFirstPos(int testPos, int... others) { + if (testPos < 0) { + return false; + } + for (int other : others) { + if (other >= 0 && other < testPos) { + return false; + } + } + return true; + } +} diff --git a/java/org/apache/catalina/valves/rewrite/mbeans-descriptors.xml b/java/org/apache/catalina/valves/rewrite/mbeans-descriptors.xml new file mode 100644 index 0000000..970a6d1 --- /dev/null +++ b/java/org/apache/catalina/valves/rewrite/mbeans-descriptors.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/webresources/AbstractArchiveResource.java b/java/org/apache/catalina/webresources/AbstractArchiveResource.java new file mode 100644 index 0000000..d4f2e1c --- /dev/null +++ b/java/org/apache/catalina/webresources/AbstractArchiveResource.java @@ -0,0 +1,316 @@ +/* + * 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.catalina.webresources; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.cert.Certificate; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +import org.apache.catalina.util.URLEncoder; + +public abstract class AbstractArchiveResource extends AbstractResource { + + private final AbstractArchiveResourceSet archiveResourceSet; + private final String baseUrl; + private final JarEntry resource; + private final String codeBaseUrl; + private final String name; + private boolean readCerts = false; + private Certificate[] certificates; + + protected AbstractArchiveResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, String baseUrl, + JarEntry jarEntry, String codeBaseUrl) { + super(archiveResourceSet.getRoot(), webAppPath); + this.archiveResourceSet = archiveResourceSet; + this.baseUrl = baseUrl; + this.resource = jarEntry; + this.codeBaseUrl = codeBaseUrl; + + String resourceName = resource.getName(); + if (resourceName.charAt(resourceName.length() - 1) == '/') { + resourceName = resourceName.substring(0, resourceName.length() - 1); + } + String internalPath = archiveResourceSet.getInternalPath(); + if (internalPath.length() > 0 && resourceName.equals(internalPath.subSequence(1, internalPath.length()))) { + name = ""; + } else { + int index = resourceName.lastIndexOf('/'); + if (index == -1) { + name = resourceName; + } else { + name = resourceName.substring(index + 1); + } + } + } + + protected AbstractArchiveResourceSet getArchiveResourceSet() { + return archiveResourceSet; + } + + protected final String getBase() { + return archiveResourceSet.getBase(); + } + + protected final String getBaseUrl() { + return baseUrl; + } + + protected final JarEntry getResource() { + return resource; + } + + @Override + public long getLastModified() { + return resource.getTime(); + } + + @Override + public boolean exists() { + return true; + } + + @Override + public boolean isVirtual() { + return false; + } + + @Override + public boolean isDirectory() { + return resource.isDirectory(); + } + + @Override + public boolean isFile() { + return !resource.isDirectory(); + } + + @Override + public boolean delete() { + return false; + } + + @Override + public String getName() { + return name; + } + + @Override + public long getContentLength() { + if (isDirectory()) { + return -1; + } + return resource.getSize(); + } + + @Override + public String getCanonicalPath() { + return null; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public long getCreation() { + return resource.getTime(); + } + + @Override + public URL getURL() { + String url = baseUrl + URLEncoder.DEFAULT.encode(resource.getName(), StandardCharsets.UTF_8); + try { + return new URI(url).toURL(); + } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("fileResource.getUrlFail", url), e); + } + return null; + } + } + + @Override + public URL getCodeBase() { + try { + return new URI(codeBaseUrl).toURL(); + } catch (MalformedURLException | URISyntaxException e) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("fileResource.getUrlFail", codeBaseUrl), e); + } + return null; + } + } + + @Override + public final byte[] getContent() { + long len = getContentLength(); + + if (len > Integer.MAX_VALUE) { + // Can't create an array that big + throw new ArrayIndexOutOfBoundsException( + sm.getString("abstractResource.getContentTooLarge", getWebappPath(), Long.valueOf(len))); + } + + if (len < 0) { + // Content is not applicable here (e.g. is a directory) + return null; + } + + int size = (int) len; + byte[] result = new byte[size]; + + int pos = 0; + try (JarInputStreamWrapper jisw = getJarInputStreamWrapper()) { + if (jisw == null) { + // An error occurred, don't return corrupted content + return null; + } + while (pos < size) { + int n = jisw.read(result, pos, size - pos); + if (n < 0) { + break; + } + pos += n; + } + // Once the stream has been read, read the certs + certificates = jisw.getCertificates(); + readCerts = true; + } catch (IOException ioe) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractResource.getContentFail", getWebappPath()), ioe); + } + // Don't return corrupted content + return null; + } + + return result; + } + + + @Override + public Certificate[] getCertificates() { + if (!readCerts) { + // TODO - get content first + throw new IllegalStateException(); + } + return certificates; + } + + @Override + public Manifest getManifest() { + return archiveResourceSet.getManifest(); + } + + @Override + protected final InputStream doGetInputStream() { + if (isDirectory()) { + return null; + } + return getJarInputStreamWrapper(); + } + + protected abstract JarInputStreamWrapper getJarInputStreamWrapper(); + + /** + * This wrapper assumes that the InputStream was created from a JarFile obtained from a call to + * getArchiveResourceSet().openJarFile(). If this is not the case then the usage counting in + * AbstractArchiveResourceSet will break and the JarFile may be unexpectedly closed. + */ + protected class JarInputStreamWrapper extends InputStream { + + private final JarEntry jarEntry; + private final InputStream is; + private final AtomicBoolean closed = new AtomicBoolean(false); + + + public JarInputStreamWrapper(JarEntry jarEntry, InputStream is) { + this.jarEntry = jarEntry; + this.is = is; + } + + + @Override + public int read() throws IOException { + return is.read(); + } + + + @Override + public int read(byte[] b) throws IOException { + return is.read(b); + } + + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return is.read(b, off, len); + } + + + @Override + public long skip(long n) throws IOException { + return is.skip(n); + } + + + @Override + public int available() throws IOException { + return is.available(); + } + + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false, true)) { + // Must only call this once else the usage counting will break + archiveResourceSet.closeJarFile(); + } + is.close(); + } + + + @Override + public synchronized void mark(int readlimit) { + is.mark(readlimit); + } + + + @Override + public synchronized void reset() throws IOException { + is.reset(); + } + + + @Override + public boolean markSupported() { + return is.markSupported(); + } + + public Certificate[] getCertificates() { + return jarEntry.getCertificates(); + } + } +} diff --git a/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java b/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java new file mode 100644 index 0000000..51d9d24 --- /dev/null +++ b/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java @@ -0,0 +1,335 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipFile; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.util.ResourceSet; + +public abstract class AbstractArchiveResourceSet extends AbstractResourceSet { + + private URL baseUrl; + private String baseUrlString; + private JarFile archive = null; + protected Map archiveEntries = null; + protected final Object archiveLock = new Object(); + private long archiveUseCount = 0; + private JarContents jarContents; + private boolean retainBloomFilterForArchives = false; + + protected final void setBaseUrl(URL baseUrl) { + this.baseUrl = baseUrl; + if (baseUrl == null) { + this.baseUrlString = null; + } else { + this.baseUrlString = baseUrl.toString(); + } + } + + @Override + public final URL getBaseUrl() { + return baseUrl; + } + + protected final String getBaseUrlString() { + return baseUrlString; + } + + + @Override + public final String[] list(String path) { + checkPath(path); + String webAppMount = getWebAppMount(); + + ArrayList result = new ArrayList<>(); + if (path.startsWith(webAppMount)) { + String pathInJar = getInternalPath() + path.substring(webAppMount.length()); + // Always strip off the leading '/' to get the JAR path + if (pathInJar.length() > 0 && pathInJar.charAt(0) == '/') { + pathInJar = pathInJar.substring(1); + } + for (String name : getArchiveEntries(false).keySet()) { + if (name.length() > pathInJar.length() && name.startsWith(pathInJar)) { + if (name.charAt(name.length() - 1) == '/') { + name = name.substring(pathInJar.length(), name.length() - 1); + } else { + name = name.substring(pathInJar.length()); + } + if (name.length() == 0) { + continue; + } + if (name.charAt(0) == '/') { + name = name.substring(1); + } + if (name.length() > 0 && name.lastIndexOf('/') == -1) { + result.add(name); + } + } + } + } else { + if (!path.endsWith("/")) { + path = path + "/"; + } + if (webAppMount.startsWith(path)) { + int i = webAppMount.indexOf('/', path.length()); + if (i == -1) { + return new String[] { webAppMount.substring(path.length()) }; + } else { + return new String[] { webAppMount.substring(path.length(), i) }; + } + } + } + return result.toArray(new String[0]); + } + + @Override + public final Set listWebAppPaths(String path) { + checkPath(path); + String webAppMount = getWebAppMount(); + + ResourceSet result = new ResourceSet<>(); + if (path.startsWith(webAppMount)) { + String pathInJar = getInternalPath() + path.substring(webAppMount.length()); + // Always strip off the leading '/' to get the JAR path and make + // sure it ends in '/' + if (pathInJar.length() > 0) { + if (pathInJar.charAt(pathInJar.length() - 1) != '/') { + pathInJar = pathInJar.substring(1) + '/'; + } + if (pathInJar.charAt(0) == '/') { + pathInJar = pathInJar.substring(1); + } + } + + for (String name : getArchiveEntries(false).keySet()) { + if (name.length() > pathInJar.length() && name.startsWith(pathInJar)) { + int nextSlash = name.indexOf('/', pathInJar.length()); + if (nextSlash != -1 && nextSlash != name.length() - 1) { + name = name.substring(0, nextSlash + 1); + } + result.add(webAppMount + '/' + name.substring(getInternalPath().length())); + } + } + } else { + if (!path.endsWith("/")) { + path = path + "/"; + } + if (webAppMount.startsWith(path)) { + int i = webAppMount.indexOf('/', path.length()); + if (i == -1) { + result.add(webAppMount + "/"); + } else { + result.add(webAppMount.substring(0, i + 1)); + } + } + } + result.setLocked(true); + return result; + } + + + /** + * Obtain the map of entries in the archive. May return null in which case {@link #getArchiveEntry(String)} should + * be used. + * + * @param single Is this request being make to support a single lookup? If false, a map will always be returned. If + * true, implementations may use this as a hint in determining the optimum way to respond. + * + * @return The archives entries mapped to their names or null if {@link #getArchiveEntry(String)} should be used. + */ + protected abstract Map getArchiveEntries(boolean single); + + + /** + * Obtain a single entry from the archive. For performance reasons, {@link #getArchiveEntries(boolean)} should + * always be called first and the archive entry looked up in the map if one is returned. Only if that call returns + * null should this method be used. + * + * @param pathInArchive The path in the archive of the entry required + * + * @return The specified archive entry or null if it does not exist + */ + protected abstract JarEntry getArchiveEntry(String pathInArchive); + + @Override + public final boolean mkdir(String path) { + checkPath(path); + + return false; + } + + @Override + public final boolean write(String path, InputStream is, boolean overwrite) { + checkPath(path); + + if (is == null) { + throw new NullPointerException(sm.getString("dirResourceSet.writeNpe")); + } + + return false; + } + + @Override + public final WebResource getResource(String path) { + checkPath(path); + String webAppMount = getWebAppMount(); + WebResourceRoot root = getRoot(); + + /* + * If jarContents reports that this resource definitely does not contain the path, we can end this method and + * move on to the next jar. + */ + if (jarContents != null && !jarContents.mightContainResource(path, webAppMount)) { + return new EmptyResource(root, path); + } + + /* + * Implementation notes + * + * The path parameter passed into this method always starts with '/'. + * + * The path parameter passed into this method may or may not end with a '/'. JarFile.getEntry() will return a + * matching directory entry whether or not the name ends in a '/'. However, if the entry is requested without + * the '/' subsequent calls to JarEntry.isDirectory() will return false. + * + * Paths in JARs never start with '/'. Leading '/' need to be removed before any JarFile.getEntry() call. + */ + + // If the JAR has been mounted below the web application root, return + // an empty resource for requests outside of the mount point. + + if (path.startsWith(webAppMount)) { + String pathInJar = getInternalPath() + path.substring(webAppMount.length()); + // Always strip off the leading '/' to get the JAR path + if (pathInJar.length() > 0 && pathInJar.charAt(0) == '/') { + pathInJar = pathInJar.substring(1); + } + if (pathInJar.equals("")) { + // Special case + // This is a directory resource so the path must end with / + if (!path.endsWith("/")) { + path = path + "/"; + } + return new JarResourceRoot(root, new File(getBase()), baseUrlString, path); + } else { + JarEntry jarEntry = null; + if (isMultiRelease()) { + // Calls JarFile.getJarEntry() which is multi-release aware + jarEntry = getArchiveEntry(pathInJar); + } else { + Map jarEntries = getArchiveEntries(true); + if (!(pathInJar.charAt(pathInJar.length() - 1) == '/')) { + if (jarEntries == null) { + jarEntry = getArchiveEntry(pathInJar + '/'); + } else { + jarEntry = jarEntries.get(pathInJar + '/'); + } + if (jarEntry != null) { + path = path + '/'; + } + } + if (jarEntry == null) { + if (jarEntries == null) { + jarEntry = getArchiveEntry(pathInJar); + } else { + jarEntry = jarEntries.get(pathInJar); + } + } + } + if (jarEntry == null) { + return new EmptyResource(root, path); + } else { + return createArchiveResource(jarEntry, path, getManifest()); + } + } + } else { + return new EmptyResource(root, path); + } + } + + protected abstract boolean isMultiRelease(); + + protected abstract WebResource createArchiveResource(JarEntry jarEntry, String webAppPath, Manifest manifest); + + @Override + public final boolean isReadOnly() { + return true; + } + + @Override + public void setReadOnly(boolean readOnly) { + if (readOnly) { + // This is the hard-coded default - ignore the call + return; + } + + throw new IllegalArgumentException(sm.getString("abstractArchiveResourceSet.setReadOnlyFalse")); + } + + @SuppressWarnings("deprecation") + protected JarFile openJarFile() throws IOException { + synchronized (archiveLock) { + if (archive == null) { + archive = new JarFile(new File(getBase()), true, ZipFile.OPEN_READ, Runtime.version()); + WebResourceRoot root = getRoot(); + if (root.getArchiveIndexStrategyEnum().getUsesBloom() || + root.getContext() != null && root.getContext().getUseBloomFilterForArchives()) { + jarContents = new JarContents(archive); + retainBloomFilterForArchives = root.getArchiveIndexStrategyEnum().getRetain(); + } + } + archiveUseCount++; + return archive; + } + } + + protected void closeJarFile() { + synchronized (archiveLock) { + archiveUseCount--; + } + } + + @Override + public void gc() { + synchronized (archiveLock) { + if (archive != null && archiveUseCount == 0) { + try { + archive.close(); + } catch (IOException e) { + // Log at least WARN + } + archive = null; + archiveEntries = null; + if (!retainBloomFilterForArchives) { + jarContents = null; + } + } + } + } +} diff --git a/java/org/apache/catalina/webresources/AbstractFileResourceSet.java b/java/org/apache/catalina/webresources/AbstractFileResourceSet.java new file mode 100644 index 0000000..7a81b94 --- /dev/null +++ b/java/org/apache/catalina/webresources/AbstractFileResourceSet.java @@ -0,0 +1,250 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.catalina.LifecycleException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.compat.JrePlatform; +import org.apache.tomcat.util.http.RequestUtil; + +public abstract class AbstractFileResourceSet extends AbstractResourceSet { + + private static final Log log = LogFactory.getLog(AbstractFileResourceSet.class); + + protected static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private File fileBase; + private String absoluteBase; + private String canonicalBase; + private boolean readOnly = false; + + protected AbstractFileResourceSet(String internalPath) { + setInternalPath(internalPath); + } + + protected final File getFileBase() { + return fileBase; + } + + @Override + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + @Override + public boolean isReadOnly() { + return readOnly; + } + + protected final File file(String name, boolean mustExist) { + + if (name.equals("/")) { + name = ""; + } + File file = new File(fileBase, name); + + // If the requested names ends in '/', the Java File API will return a + // matching file if one exists. This isn't what we want as it is not + // consistent with the Servlet spec rules for request mapping. + if (name.endsWith("/") && file.isFile()) { + return null; + } + + // If the file/dir must exist but the identified file/dir can't be read + // then signal that the resource was not found + if (mustExist && !file.canRead()) { + return null; + } + + // If allow linking is enabled, files are not limited to being located + // under the fileBase so all further checks are disabled. + if (getRoot().getAllowLinking()) { + return file; + } + + // Additional Windows specific checks to handle known problems with + // File.getCanonicalPath() + if (JrePlatform.IS_WINDOWS && isInvalidWindowsFilename(name)) { + return null; + } + + // Check that this file is located under the WebResourceSet's base + String canPath = null; + try { + canPath = file.getCanonicalPath(); + } catch (IOException e) { + // Ignore + } + if (canPath == null || !canPath.startsWith(canonicalBase)) { + return null; + } + + // Ensure that the file is not outside the fileBase. This should not be + // possible for standard requests (the request is normalized early in + // the request processing) but might be possible for some access via the + // Servlet API (RequestDispatcher, HTTP/2 push etc.) therefore these + // checks are retained as an additional safety measure + // absoluteBase has been normalized so absPath needs to be normalized as + // well. + String absPath = normalize(file.getAbsolutePath()); + if (absPath == null || absoluteBase.length() > absPath.length()) { + return null; + } + + // Remove the fileBase location from the start of the paths since that + // was not part of the requested path and the remaining check only + // applies to the request path + absPath = absPath.substring(absoluteBase.length()); + canPath = canPath.substring(canonicalBase.length()); + + // The remaining request path must start with '/' if it has non-zero length + if (canPath.length() > 0 && canPath.charAt(0) != File.separatorChar) { + return null; + } + + // Case sensitivity check + // The normalized requested path should be an exact match the equivalent + // canonical path. If it is not, possible reasons include: + // - case differences on case insensitive file systems + // - Windows removing a trailing ' ' or '.' from the file name + // + // In all cases, a mismatch here results in the resource not being + // found + // + // absPath is normalized so canPath needs to be normalized as well + // Can't normalize canPath earlier as canonicalBase is not normalized + if (canPath.length() > 0) { + canPath = normalize(canPath); + } + if (!canPath.equals(absPath)) { + if (!canPath.equalsIgnoreCase(absPath)) { + // Typically means symlinks are in use but being ignored. Given + // the symlink was likely created for a reason, log a warning + // that it was ignored. + logIgnoredSymlink(getRoot().getContext().getName(), absPath, canPath); + } + return null; + } + + return file; + } + + + protected void logIgnoredSymlink(String contextPath, String absPath, String canPath) { + String msg = sm.getString("abstractFileResourceSet.canonicalfileCheckFailed", contextPath, absPath, canPath); + // Log issues with configuration files at a higher level + if (absPath.startsWith("/META-INF/") || absPath.startsWith("/WEB-INF/")) { + log.error(msg); + } else { + log.warn(msg); + } + } + + private boolean isInvalidWindowsFilename(String name) { + final int len = name.length(); + if (len == 0) { + return false; + } + // This consistently ~10 times faster than the equivalent regular + // expression irrespective of input length. + for (int i = 0; i < len; i++) { + char c = name.charAt(i); + if (c == '\"' || c == '<' || c == '>' || c == ':') { + // These characters are disallowed in Windows file names and + // there are known problems for file names with these characters + // when using File#getCanonicalPath(). + // Note: There are additional characters that are disallowed in + // Windows file names but these are not known to cause + // problems when using File#getCanonicalPath(). + return true; + } + } + // Windows does not allow file names to end in ' ' unless specific low + // level APIs are used to create the files that bypass various checks. + // File names that end in ' ' are known to cause problems when using + // File#getCanonicalPath(). + if (name.charAt(len - 1) == ' ') { + return true; + } + return false; + } + + + /** + * Return a context-relative path, beginning with a "/", that represents the canonical version of the specified path + * after ".." and "." elements are resolved out. If the specified path attempts to go outside the boundaries of the + * current context (i.e. too many ".." path elements are present), return null instead. + * + * @param path Path to be normalized + */ + private String normalize(String path) { + return RequestUtil.normalize(path, File.separatorChar == '\\'); + } + + @Override + public URL getBaseUrl() { + try { + return getFileBase().toURI().toURL(); + } catch (MalformedURLException e) { + return null; + } + } + + /** + * {@inheritDoc} + *

    + * This is a NO-OP by default for File based resource sets. + */ + @Override + public void gc() { + // NO-OP + } + + + // -------------------------------------------------------- Lifecycle methods + + @Override + protected void initInternal() throws LifecycleException { + fileBase = new File(getBase(), getInternalPath()); + checkType(fileBase); + + this.absoluteBase = normalize(fileBase.getAbsolutePath()); + + try { + this.canonicalBase = fileBase.getCanonicalPath(); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + + // Need to handle mapping of the file system root as a special case + if ("/".equals(this.absoluteBase)) { + this.absoluteBase = ""; + } + if ("/".equals(this.canonicalBase)) { + this.canonicalBase = ""; + } + } + + + protected abstract void checkType(File file); +} diff --git a/java/org/apache/catalina/webresources/AbstractResource.java b/java/org/apache/catalina/webresources/AbstractResource.java new file mode 100644 index 0000000..384c327 --- /dev/null +++ b/java/org/apache/catalina/webresources/AbstractResource.java @@ -0,0 +1,105 @@ +/* + * 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.catalina.webresources; + +import java.io.InputStream; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.res.StringManager; + +public abstract class AbstractResource implements WebResource { + + protected static final StringManager sm = StringManager.getManager(AbstractResource.class); + + private final WebResourceRoot root; + private final String webAppPath; + + private String mimeType = null; + private volatile String weakETag; + + + protected AbstractResource(WebResourceRoot root, String webAppPath) { + this.root = root; + this.webAppPath = webAppPath; + } + + + @Override + public final WebResourceRoot getWebResourceRoot() { + return root; + } + + + @Override + public final String getWebappPath() { + return webAppPath; + } + + + @Override + public final String getLastModifiedHttp() { + return FastHttpDateFormat.formatDate(getLastModified()); + } + + + @Override + public final String getETag() { + if (weakETag == null) { + synchronized (this) { + if (weakETag == null) { + long contentLength = getContentLength(); + long lastModified = getLastModified(); + if ((contentLength >= 0) || (lastModified >= 0)) { + weakETag = "W/\"" + contentLength + "-" + lastModified + "\""; + } + } + } + } + return weakETag; + } + + @Override + public final void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + + @Override + public final String getMimeType() { + return mimeType; + } + + + @Override + public final InputStream getInputStream() { + InputStream is = doGetInputStream(); + + if (is == null || !root.getTrackLockedFiles()) { + return is; + } + + return new TrackedInputStream(root, getName(), is); + } + + protected abstract InputStream doGetInputStream(); + + + protected abstract Log getLog(); +} diff --git a/java/org/apache/catalina/webresources/AbstractResourceSet.java b/java/org/apache/catalina/webresources/AbstractResourceSet.java new file mode 100644 index 0000000..f139339 --- /dev/null +++ b/java/org/apache/catalina/webresources/AbstractResourceSet.java @@ -0,0 +1,138 @@ +/* + * 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.catalina.webresources; + +import java.util.jar.Manifest; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.util.LifecycleBase; +import org.apache.tomcat.util.res.StringManager; + +public abstract class AbstractResourceSet extends LifecycleBase implements WebResourceSet { + + private WebResourceRoot root; + private String base; + private String internalPath = ""; + private String webAppMount; + private boolean classLoaderOnly; + private boolean staticOnly; + private Manifest manifest; + + + protected static final StringManager sm = StringManager.getManager(AbstractResourceSet.class); + + + protected final void checkPath(String path) { + if (path == null || path.length() == 0 || path.charAt(0) != '/') { + throw new IllegalArgumentException(sm.getString("abstractResourceSet.checkPath", path)); + } + } + + @Override + public final void setRoot(WebResourceRoot root) { + this.root = root; + } + + protected final WebResourceRoot getRoot() { + return root; + } + + + protected final String getInternalPath() { + return internalPath; + } + + public final void setInternalPath(String internalPath) { + checkPath(internalPath); + // Optimise internal processing + if (internalPath.equals("/")) { + this.internalPath = ""; + } else { + this.internalPath = internalPath; + } + } + + public final void setWebAppMount(String webAppMount) { + checkPath(webAppMount); + // Optimise internal processing + if (webAppMount.equals("/")) { + this.webAppMount = ""; + } else { + this.webAppMount = webAppMount; + } + } + + protected final String getWebAppMount() { + return webAppMount; + } + + public final void setBase(String base) { + this.base = base; + } + + protected final String getBase() { + return base; + } + + @Override + public boolean getClassLoaderOnly() { + return classLoaderOnly; + } + + @Override + public void setClassLoaderOnly(boolean classLoaderOnly) { + this.classLoaderOnly = classLoaderOnly; + } + + @Override + public boolean getStaticOnly() { + return staticOnly; + } + + @Override + public void setStaticOnly(boolean staticOnly) { + this.staticOnly = staticOnly; + } + + protected final void setManifest(Manifest manifest) { + this.manifest = manifest; + } + + protected final Manifest getManifest() { + return manifest; + } + + + // -------------------------------------------------------- Lifecycle methods + @Override + protected final void startInternal() throws LifecycleException { + setState(LifecycleState.STARTING); + } + + @Override + protected final void stopInternal() throws LifecycleException { + setState(LifecycleState.STOPPING); + } + + @Override + protected final void destroyInternal() throws LifecycleException { + gc(); + } +} diff --git a/java/org/apache/catalina/webresources/AbstractSingleArchiveResource.java b/java/org/apache/catalina/webresources/AbstractSingleArchiveResource.java new file mode 100644 index 0000000..cd22d28 --- /dev/null +++ b/java/org/apache/catalina/webresources/AbstractSingleArchiveResource.java @@ -0,0 +1,52 @@ +/* + * 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.catalina.webresources; + +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public abstract class AbstractSingleArchiveResource extends AbstractArchiveResource { + + protected AbstractSingleArchiveResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, + String baseUrl, JarEntry jarEntry, String codeBaseUrl) { + super(archiveResourceSet, webAppPath, baseUrl, jarEntry, codeBaseUrl); + } + + + @Override + protected JarInputStreamWrapper getJarInputStreamWrapper() { + JarFile jarFile = null; + try { + jarFile = getArchiveResourceSet().openJarFile(); + // Need to create a new JarEntry so the certificates can be read + JarEntry jarEntry = jarFile.getJarEntry(getResource().getName()); + InputStream is = jarFile.getInputStream(jarEntry); + return new JarInputStreamWrapper(jarEntry, is); + } catch (IOException e) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("jarResource.getInputStreamFail", getResource().getName(), getBaseUrl()), + e); + } + if (jarFile != null) { + getArchiveResourceSet().closeJarFile(); + } + return null; + } + } +} diff --git a/java/org/apache/catalina/webresources/AbstractSingleArchiveResourceSet.java b/java/org/apache/catalina/webresources/AbstractSingleArchiveResourceSet.java new file mode 100644 index 0000000..363e32e --- /dev/null +++ b/java/org/apache/catalina/webresources/AbstractSingleArchiveResourceSet.java @@ -0,0 +1,150 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipFile; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResourceRoot; +import org.apache.tomcat.util.buf.UriUtil; + +/** + * Base class for a {@link org.apache.catalina.WebResourceSet} based on a single, rather than nested, archive. + */ +public abstract class AbstractSingleArchiveResourceSet extends AbstractArchiveResourceSet { + + private volatile Boolean multiRelease; + + /** + * A no argument constructor is required for this to work with the digester. + */ + public AbstractSingleArchiveResourceSet() { + } + + + public AbstractSingleArchiveResourceSet(WebResourceRoot root, String webAppMount, String base, String internalPath) + throws IllegalArgumentException { + setRoot(root); + setWebAppMount(webAppMount); + setBase(base); + setInternalPath(internalPath); + + if (getRoot().getState().isAvailable()) { + try { + start(); + } catch (LifecycleException e) { + throw new IllegalStateException(e); + } + } + } + + + @Override + protected Map getArchiveEntries(boolean single) { + synchronized (archiveLock) { + if (archiveEntries == null && !single) { + JarFile jarFile = null; + archiveEntries = new HashMap<>(); + try { + jarFile = openJarFile(); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + archiveEntries.put(entry.getName(), entry); + } + } catch (IOException ioe) { + // Should never happen + archiveEntries = null; + throw new IllegalStateException(ioe); + } finally { + if (jarFile != null) { + closeJarFile(); + } + } + } + return archiveEntries; + } + } + + + @Override + protected JarEntry getArchiveEntry(String pathInArchive) { + JarFile jarFile = null; + try { + jarFile = openJarFile(); + return jarFile.getJarEntry(pathInArchive); + } catch (IOException ioe) { + // Should never happen + throw new IllegalStateException(ioe); + } finally { + if (jarFile != null) { + closeJarFile(); + } + } + } + + + @Override + protected boolean isMultiRelease() { + if (multiRelease == null) { + synchronized (archiveLock) { + if (multiRelease == null) { + JarFile jarFile = null; + try { + jarFile = openJarFile(); + multiRelease = Boolean.valueOf(jarFile.isMultiRelease()); + } catch (IOException ioe) { + // Should never happen + throw new IllegalStateException(ioe); + } finally { + if (jarFile != null) { + closeJarFile(); + } + } + } + } + } + + return multiRelease.booleanValue(); + } + + + // -------------------------------------------------------- Lifecycle methods + @Override + protected void initInternal() throws LifecycleException { + + try (JarFile jarFile = new JarFile(new File(getBase()), true, ZipFile.OPEN_READ, Runtime.version())) { + setManifest(jarFile.getManifest()); + } catch (IOException ioe) { + throw new IllegalArgumentException(ioe); + } + + try { + setBaseUrl(UriUtil.buildJarSafeUrl(new File(getBase()))); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/java/org/apache/catalina/webresources/Cache.java b/java/org/apache/catalina/webresources/Cache.java new file mode 100644 index 0000000..467a43b --- /dev/null +++ b/java/org/apache/catalina/webresources/Cache.java @@ -0,0 +1,340 @@ +/* + * 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.catalina.webresources; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot.CacheStrategy; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class Cache { + + private static final Log log = LogFactory.getLog(Cache.class); + protected static final StringManager sm = StringManager.getManager(Cache.class); + + private static final long TARGET_FREE_PERCENT_GET = 5; + private static final long TARGET_FREE_PERCENT_BACKGROUND = 10; + + // objectMaxSize must be < maxSize/20 + private static final int OBJECT_MAX_SIZE_FACTOR = 20; + + private final StandardRoot root; + private final AtomicLong size = new AtomicLong(0); + + private long ttl = 5000; + private long maxSize = 10 * 1024 * 1024; + private int objectMaxSize = (int) maxSize / OBJECT_MAX_SIZE_FACTOR; + private CacheStrategy cacheStrategy; + + private LongAdder lookupCount = new LongAdder(); + private LongAdder hitCount = new LongAdder(); + + private final ConcurrentMap resourceCache = new ConcurrentHashMap<>(); + + public Cache(StandardRoot root) { + this.root = root; + } + + protected WebResource getResource(String path, boolean useClassLoaderResources) { + + if (noCache(path)) { + return root.getResourceInternal(path, useClassLoaderResources); + } + + CacheStrategy strategy = getCacheStrategy(); + if (strategy != null) { + if (strategy.noCache(path)) { + return root.getResourceInternal(path, useClassLoaderResources); + } + } + + lookupCount.increment(); + + CachedResource cacheEntry = resourceCache.get(path); + + if (cacheEntry != null && !cacheEntry.validateResource(useClassLoaderResources)) { + removeCacheEntry(path); + cacheEntry = null; + } + + if (cacheEntry == null) { + // Local copy to ensure consistency + int objectMaxSizeBytes = getObjectMaxSizeBytes(); + CachedResource newCacheEntry = new CachedResource(this, root, path, getTtl(), objectMaxSizeBytes, + useClassLoaderResources); + + // Concurrent callers will end up with the same CachedResource + // instance + cacheEntry = resourceCache.putIfAbsent(path, newCacheEntry); + + if (cacheEntry == null) { + // newCacheEntry was inserted into the cache - validate it + cacheEntry = newCacheEntry; + cacheEntry.validateResource(useClassLoaderResources); + + // Even if the resource content larger than objectMaxSizeBytes + // there is still benefit in caching the resource metadata + + long delta = cacheEntry.getSize(); + size.addAndGet(delta); + + if (size.get() > maxSize) { + // Process resources unordered for speed. Trades cache + // efficiency (younger entries may be evicted before older + // ones) for speed since this is on the critical path for + // request processing + long targetSize = maxSize * (100 - TARGET_FREE_PERCENT_GET) / 100; + long newSize = evict(targetSize, resourceCache.values().iterator()); + if (newSize > maxSize) { + // Unable to create sufficient space for this resource + // Remove it from the cache + removeCacheEntry(path); + log.warn(sm.getString("cache.addFail", path, root.getContext().getName())); + } + } + } else { + // Another thread added the entry to the cache + if (cacheEntry.usesClassLoaderResources() != useClassLoaderResources) { + // Race condition adding cache entries with the same path + // but differing values for useClassLoaderResources. + // Cache only supports one entry per path with one value of + // useClassLoaderResources. + // Let the other thread "win" and add the resource to the + // cache. This thread will receive a cacheEntry instance + // that isn't added to the cache. + // There are assumptions here. They are: + // - refactoring the Cache to use a combined key of + // path+useClassLoaderResources adds unnecessary + // complexity + // - the race condition is rare (over the lifetime of an + // application) + // - it would be rare for an application to need to cache a + // resource for both values of useClassLoaderResources + cacheEntry = newCacheEntry; + } + // Make sure it is validated + cacheEntry.validateResource(useClassLoaderResources); + } + } else { + hitCount.increment(); + } + + return cacheEntry; + } + + protected WebResource[] getResources(String path, boolean useClassLoaderResources) { + lookupCount.increment(); + + // Don't call noCache(path) since the class loader only caches + // individual resources. Therefore, always cache collections here + + CachedResource cacheEntry = resourceCache.get(path); + + if (cacheEntry != null && !cacheEntry.validateResources(useClassLoaderResources)) { + removeCacheEntry(path); + cacheEntry = null; + } + + if (cacheEntry == null) { + // Local copy to ensure consistency + int objectMaxSizeBytes = getObjectMaxSizeBytes(); + CachedResource newCacheEntry = new CachedResource(this, root, path, getTtl(), objectMaxSizeBytes, + useClassLoaderResources); + + // Concurrent callers will end up with the same CachedResource + // instance + cacheEntry = resourceCache.putIfAbsent(path, newCacheEntry); + + if (cacheEntry == null) { + // newCacheEntry was inserted into the cache - validate it + cacheEntry = newCacheEntry; + cacheEntry.validateResources(useClassLoaderResources); + + // Content will not be cached but we still need metadata size + long delta = cacheEntry.getSize(); + size.addAndGet(delta); + + if (size.get() > maxSize) { + // Process resources unordered for speed. Trades cache + // efficiency (younger entries may be evicted before older + // ones) for speed since this is on the critical path for + // request processing + long targetSize = maxSize * (100 - TARGET_FREE_PERCENT_GET) / 100; + long newSize = evict(targetSize, resourceCache.values().iterator()); + if (newSize > maxSize) { + // Unable to create sufficient space for this resource + // Remove it from the cache + removeCacheEntry(path); + log.warn(sm.getString("cache.addFail", path)); + } + } + } else { + // Another thread added the entry to the cache + // Make sure it is validated + cacheEntry.validateResources(useClassLoaderResources); + } + } else { + hitCount.increment(); + } + + return cacheEntry.getWebResources(); + } + + protected void backgroundProcess() { + // Create an ordered set of all cached resources with the least recently + // used first. This is a background process so we can afford to take the + // time to order the elements first + TreeSet orderedResources = new TreeSet<>( + Comparator.comparingLong(CachedResource::getNextCheck)); + orderedResources.addAll(resourceCache.values()); + + Iterator iter = orderedResources.iterator(); + + long targetSize = maxSize * (100 - TARGET_FREE_PERCENT_BACKGROUND) / 100; + long newSize = evict(targetSize, iter); + + if (newSize > targetSize) { + log.info(sm.getString("cache.backgroundEvictFail", Long.valueOf(TARGET_FREE_PERCENT_BACKGROUND), + root.getContext().getName(), Long.valueOf(newSize / 1024))); + } + } + + private boolean noCache(String path) { + // Don't cache classes. The class loader handles this. + // Don't cache JARs. The ResourceSet handles this. + if ((path.endsWith(".class") && (path.startsWith("/WEB-INF/classes/") || path.startsWith("/WEB-INF/lib/"))) || + (path.startsWith("/WEB-INF/lib/") && path.endsWith(".jar"))) { + return true; + } + return false; + } + + private long evict(long targetSize, Iterator iter) { + + long now = System.currentTimeMillis(); + + long newSize = size.get(); + + while (newSize > targetSize && iter.hasNext()) { + CachedResource resource = iter.next(); + + // Don't expire anything that has been checked within the TTL + if (resource.getNextCheck() > now) { + continue; + } + + // Remove the entry from the cache + removeCacheEntry(resource.getWebappPath()); + + newSize = size.get(); + } + + return newSize; + } + + void removeCacheEntry(String path) { + // With concurrent calls for the same path, the entry is only removed + // once and the cache size is only updated (if required) once. + CachedResource cachedResource = resourceCache.remove(path); + if (cachedResource != null) { + long delta = cachedResource.getSize(); + size.addAndGet(-delta); + } + } + + public CacheStrategy getCacheStrategy() { + return cacheStrategy; + } + + public void setCacheStrategy(CacheStrategy cacheStrategy) { + this.cacheStrategy = cacheStrategy; + } + + public long getTtl() { + return ttl; + } + + public void setTtl(long ttl) { + this.ttl = ttl; + } + + public long getMaxSize() { + // Internally bytes, externally kilobytes + return maxSize / 1024; + } + + public void setMaxSize(long maxSize) { + // Internally bytes, externally kilobytes + this.maxSize = maxSize * 1024; + } + + public long getLookupCount() { + return lookupCount.sum(); + } + + public long getHitCount() { + return hitCount.sum(); + } + + public void setObjectMaxSize(int objectMaxSize) { + if (objectMaxSize * 1024L > Integer.MAX_VALUE) { + log.warn(sm.getString("cache.objectMaxSizeTooBigBytes", Integer.valueOf(objectMaxSize))); + this.objectMaxSize = Integer.MAX_VALUE; + } + // Internally bytes, externally kilobytes + this.objectMaxSize = objectMaxSize * 1024; + } + + public int getObjectMaxSize() { + // Internally bytes, externally kilobytes + return objectMaxSize / 1024; + } + + public int getObjectMaxSizeBytes() { + return objectMaxSize; + } + + void enforceObjectMaxSizeLimit() { + long limit = maxSize / OBJECT_MAX_SIZE_FACTOR; + if (limit > Integer.MAX_VALUE) { + return; + } + if (objectMaxSize > limit) { + log.warn(sm.getString("cache.objectMaxSizeTooBig", Integer.valueOf(objectMaxSize / 1024), + Integer.valueOf((int) limit / 1024))); + objectMaxSize = (int) limit; + } + } + + public void clear() { + resourceCache.clear(); + size.set(0); + } + + public long getSize() { + return size.get() / 1024; + } +} diff --git a/java/org/apache/catalina/webresources/CachedResource.java b/java/org/apache/catalina/webresources/CachedResource.java new file mode 100644 index 0000000..f82b368 --- /dev/null +++ b/java/org/apache/catalina/webresources/CachedResource.java @@ -0,0 +1,627 @@ +/* + * 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.catalina.webresources; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.nio.charset.Charset; +import java.security.Permission; +import java.security.cert.Certificate; +import java.text.Collator; +import java.util.Arrays; +import java.util.Locale; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * This class is designed to wrap a 'raw' WebResource and providing caching for expensive operations. Inexpensive + * operations may be passed through to the underlying resource. + */ +public class CachedResource implements WebResource { + + private static final Log log = LogFactory.getLog(CachedResource.class); + private static final StringManager sm = StringManager.getManager(CachedResource.class); + + // Estimate (on high side to be safe) of average size excluding content + // based on profiler data. + private static final long CACHE_ENTRY_SIZE = 500; + + private final Cache cache; + private final StandardRoot root; + private final String webAppPath; + private final long ttl; + private final int objectMaxSizeBytes; + private final boolean usesClassLoaderResources; + + private volatile WebResource webResource; + private volatile WebResource[] webResources; + private volatile long nextCheck; + + private volatile Long cachedLastModified = null; + private volatile String cachedLastModifiedHttp = null; + private volatile byte[] cachedContent = null; + private volatile Boolean cachedIsFile = null; + private volatile Boolean cachedIsDirectory = null; + private volatile Boolean cachedExists = null; + private volatile Boolean cachedIsVirtual = null; + private volatile Long cachedContentLength = null; + + + public CachedResource(Cache cache, StandardRoot root, String path, long ttl, int objectMaxSizeBytes, + boolean usesClassLoaderResources) { + this.cache = cache; + this.root = root; + this.webAppPath = path; + this.ttl = ttl; + this.objectMaxSizeBytes = objectMaxSizeBytes; + this.usesClassLoaderResources = usesClassLoaderResources; + } + + protected boolean validateResource(boolean useClassLoaderResources) { + // It is possible that some resources will only be visible for a given + // value of useClassLoaderResources. Therefore, if the lookup is made + // with a different value of useClassLoaderResources than was used when + // creating the cache entry, invalidate the entry. This should have + // minimal performance impact as it would be unusual for a resource to + // be looked up both as a static resource and as a class loader + // resource. + if (usesClassLoaderResources != useClassLoaderResources) { + return false; + } + + long now = System.currentTimeMillis(); + + if (webResource == null) { + synchronized (this) { + if (webResource == null) { + webResource = root.getResourceInternal(webAppPath, useClassLoaderResources); + getLastModified(); + getContentLength(); + nextCheck = ttl + now; + // exists() is a relatively expensive check for a file so + // use the fact that we know if it exists at this point + if (webResource instanceof EmptyResource) { + cachedExists = Boolean.FALSE; + } else { + cachedExists = Boolean.TRUE; + } + return true; + } + } + } + + if (now < nextCheck) { + return true; + } + + // Assume resources inside WARs will not change + if (!root.isPackedWarFile()) { + WebResource webResourceInternal = root.getResourceInternal(webAppPath, useClassLoaderResources); + if (!webResource.exists() && webResourceInternal.exists()) { + return false; + } + + // If modified date or length change - resource has changed / been + // removed etc. + if (webResource.getLastModified() != getLastModified() || + webResource.getContentLength() != getContentLength()) { + return false; + } + + // Has a resource been inserted / removed in a different resource set + if (webResource.getLastModified() != webResourceInternal.getLastModified() || + webResource.getContentLength() != webResourceInternal.getContentLength()) { + return false; + } + } + + nextCheck = ttl + now; + return true; + } + + protected boolean validateResources(boolean useClassLoaderResources) { + long now = System.currentTimeMillis(); + + if (webResources == null) { + synchronized (this) { + if (webResources == null) { + webResources = root.getResourcesInternal(webAppPath, useClassLoaderResources); + nextCheck = ttl + now; + return true; + } + } + } + + if (now < nextCheck) { + return true; + } + + // Assume resources inside WARs will not change + if (root.isPackedWarFile()) { + nextCheck = ttl + now; + return true; + } else { + // At this point, always expire the entry and re-populating it is + // likely to be as expensive as validating it. + return false; + } + } + + protected long getNextCheck() { + return nextCheck; + } + + @Override + public long getLastModified() { + if (cachedLastModified == null) { + cachedLastModified = Long.valueOf(webResource.getLastModified()); + } + return cachedLastModified.longValue(); + } + + @Override + public String getLastModifiedHttp() { + if (cachedLastModifiedHttp == null) { + cachedLastModifiedHttp = webResource.getLastModifiedHttp(); + } + return cachedLastModifiedHttp; + } + + @Override + public boolean exists() { + if (cachedExists == null) { + cachedExists = Boolean.valueOf(webResource.exists()); + } + return cachedExists.booleanValue(); + } + + @Override + public boolean isVirtual() { + if (cachedIsVirtual == null) { + cachedIsVirtual = Boolean.valueOf(webResource.isVirtual()); + } + return cachedIsVirtual.booleanValue(); + } + + @Override + public boolean isDirectory() { + if (cachedIsDirectory == null) { + cachedIsDirectory = Boolean.valueOf(webResource.isDirectory()); + } + return cachedIsDirectory.booleanValue(); + } + + @Override + public boolean isFile() { + if (cachedIsFile == null) { + cachedIsFile = Boolean.valueOf(webResource.isFile()); + } + return cachedIsFile.booleanValue(); + } + + @Override + public boolean delete() { + boolean deleteResult = webResource.delete(); + if (deleteResult) { + cache.removeCacheEntry(webAppPath); + } + return deleteResult; + } + + @Override + public String getName() { + return webResource.getName(); + } + + @Override + public long getContentLength() { + if (cachedContentLength == null) { + long result = 0; + if (webResource != null) { + result = webResource.getContentLength(); + cachedContentLength = Long.valueOf(result); + } + return result; + } + return cachedContentLength.longValue(); + } + + @Override + public String getCanonicalPath() { + return webResource.getCanonicalPath(); + } + + @Override + public boolean canRead() { + return webResource.canRead(); + } + + @Override + public String getWebappPath() { + return webAppPath; + } + + @Override + public String getETag() { + return webResource.getETag(); + } + + @Override + public void setMimeType(String mimeType) { + webResource.setMimeType(mimeType); + } + + @Override + public String getMimeType() { + return webResource.getMimeType(); + } + + @Override + public InputStream getInputStream() { + byte[] content = getContent(); + if (content == null) { + // Can't cache InputStreams + return webResource.getInputStream(); + } + return new ByteArrayInputStream(content); + } + + @Override + public byte[] getContent() { + if (cachedContent == null) { + if (getContentLength() > objectMaxSizeBytes) { + return null; + } + cachedContent = webResource.getContent(); + } + return cachedContent; + } + + @Override + public long getCreation() { + return webResource.getCreation(); + } + + @Override + public URL getURL() { + /* + * We don't want applications using this URL to access the resource directly as that could lead to inconsistent + * results when the resource is updated on the file system but the cache entry has not yet expired. We saw this, + * for example, in JSP compilation. + * + * - last modified time was obtained via ServletContext.getResource("path").openConnection().getLastModified() + * + * - JSP content was obtained via ServletContext.getResourceAsStream("path") + * + * The result was that the JSP modification was detected but the JSP content was read from the cache so the + * non-updated JSP page was used to generate the .java and .class file + * + * One option to resolve this issue is to use a custom URL scheme for resource URLs. This would allow us, via + * registration of a URLStreamHandlerFactory, to control how the resources are accessed and ensure that all + * access go via the cache. We took this approach for war: URLs so we can use jar:war:file: URLs to reference + * resources in unpacked WAR files. However, because URL.setURLStreamHandlerFactory() may only be called once, + * this can cause problems when using other libraries that also want to use a custom URL scheme. + * + * The approach below allows us to insert a custom URLStreamHandler without registering a custom protocol. The + * only limitation (compared to registering a custom protocol) is that if the application constructs the same + * URL from a String, they will access the resource directly and not via the cache. + */ + URL resourceURL = webResource.getURL(); + if (resourceURL == null) { + return null; + } + try { + CachedResourceURLStreamHandler handler = new CachedResourceURLStreamHandler(resourceURL, root, webAppPath, + usesClassLoaderResources); + URL result = new URL(null, resourceURL.toExternalForm(), handler); + handler.setCacheURL(result); + return result; + } catch (MalformedURLException e) { + log.error(sm.getString("cachedResource.invalidURL", resourceURL.toExternalForm()), e); + return null; + } + } + + @Override + public URL getCodeBase() { + return webResource.getCodeBase(); + } + + @Override + public Certificate[] getCertificates() { + return webResource.getCertificates(); + } + + @Override + public Manifest getManifest() { + return webResource.getManifest(); + } + + @Override + public WebResourceRoot getWebResourceRoot() { + return webResource.getWebResourceRoot(); + } + + WebResource getWebResource() { + return webResource; + } + + WebResource[] getWebResources() { + return webResources; + } + + boolean usesClassLoaderResources() { + return usesClassLoaderResources; + } + + + // Assume that the cache entry will always include the content unless the + // resource content is larger than objectMaxSizeBytes. This isn't always the + // case but it makes tracking the current cache size easier. + long getSize() { + long result = CACHE_ENTRY_SIZE; + // Longer paths use a noticeable amount of memory so account for this in + // the cache size. The fixed component of a String instance's memory + // usage is accounted for in the 500 bytes above. + result += getWebappPath().length() * 2; + if (getContentLength() <= objectMaxSizeBytes) { + result += getContentLength(); + } + return result; + } + + + /* + * Mimics the behaviour of FileURLConnection.getInputStream for a directory. Deliberately uses default locale. + */ + private static InputStream buildInputStream(String[] files) { + Arrays.sort(files, Collator.getInstance(Locale.getDefault())); + StringBuilder result = new StringBuilder(); + for (String file : files) { + result.append(file); + // Every entry is followed by \n including the last + result.append('\n'); + } + return new ByteArrayInputStream(result.toString().getBytes(Charset.defaultCharset())); + } + + + /** + * URLStreamHandler to handle a URL for a cached resource, delegating reads to the Cache. + *

      + *
    • delegates reads to the Cache, to ensure consistent invalidation behavior
    • + *
    • delegates hashCode()/ equals() behavior to the underlying Resource URL. (Equinox/ OSGi compatibility)
    • + *
    • detects the case where a new relative URL is created from the wrapped URL, inheriting its handler; in this + * case reverts to default behavior
    • + *
    + */ + private static class CachedResourceURLStreamHandler extends URLStreamHandler { + + private final URL resourceURL; + private final StandardRoot root; + private final String webAppPath; + private final boolean usesClassLoaderResources; + + private URL cacheURL = null; + + CachedResourceURLStreamHandler(URL resourceURL, StandardRoot root, String webAppPath, + boolean usesClassLoaderResources) { + this.resourceURL = resourceURL; + this.root = root; + this.webAppPath = webAppPath; + this.usesClassLoaderResources = usesClassLoaderResources; + } + + protected void setCacheURL(URL cacheURL) { + this.cacheURL = cacheURL; + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + // This deliberately uses ==. If u isn't the URL object this + // URLStreamHandler was constructed for we do not want to use this + // URLStreamHandler to create a connection. + if (cacheURL != null && u == cacheURL) { + if ("jar".equals(cacheURL.getProtocol())) { + return new CachedResourceJarURLConnection(resourceURL, root, webAppPath, usesClassLoaderResources); + } else { + return new CachedResourceURLConnection(resourceURL, root, webAppPath, usesClassLoaderResources); + } + } else { + // This stream handler has been inherited by a URL that was constructed from a cache URL. + // We need to break that link. + URI constructedURI; + try { + constructedURI = new URI(u.toExternalForm()); + } catch (URISyntaxException e) { + // Not ideal but consistent with API + throw new IOException(e); + } + URL constructedURL = constructedURI.toURL(); + return constructedURL.openConnection(); + } + } + + /** + * {@inheritDoc} + *

    + * We don't know what the requirements are for equals for the wrapped resourceURL so if u1 is the cacheURL, + * delegate to the resourceURL and it's handler. Otherwise, use the default implementation from + * URLStreamHandler. + */ + @Override + protected boolean equals(URL u1, URL u2) { + // Deliberate use of == + if (cacheURL == u1) { + return resourceURL.equals(u2); + } + // Not the cacheURL. This stream handler has been inherited by a URL that was constructed from a cache URL. + // Use the default implementation from URLStreamHandler. + return super.equals(u1, u2); + } + + /** + * {@inheritDoc} + *

    + * We don't know what the requirements are for hashcode for the wrapped resourceURL so if u1 is the cacheURL, + * delegate to the resourceURL and it's handler. Otherwise, use the default implementation from + * URLStreamHandler. + */ + @Override + protected int hashCode(URL u) { + // Deliberate use of == + if (cacheURL == u) { + return resourceURL.hashCode(); + } + // Not the cacheURL. This stream handler has been inherited by a URL that was constructed from a cache URL. + // Use the default implementation from URLStreamHandler. + return super.hashCode(u); + } + } + + + /* + * Keep this in sync with CachedResourceJarURLConnection. + */ + private static class CachedResourceURLConnection extends URLConnection { + + private final StandardRoot root; + private final String webAppPath; + private final boolean usesClassLoaderResources; + private final URL resourceURL; + + protected CachedResourceURLConnection(URL resourceURL, StandardRoot root, String webAppPath, + boolean usesClassLoaderResources) { + super(resourceURL); + this.root = root; + this.webAppPath = webAppPath; + this.usesClassLoaderResources = usesClassLoaderResources; + this.resourceURL = resourceURL; + } + + @Override + public void connect() throws IOException { + // NO-OP + } + + @Override + public InputStream getInputStream() throws IOException { + WebResource resource = getResource(); + if (resource.isDirectory()) { + return buildInputStream(resource.getWebResourceRoot().list(webAppPath)); + } else { + return getResource().getInputStream(); + } + } + + @Override + public Permission getPermission() throws IOException { + // Doesn't trigger a call to connect for file:// URLs + return resourceURL.openConnection().getPermission(); + } + + @Override + public long getLastModified() { + return getResource().getLastModified(); + } + + @Override + public long getContentLengthLong() { + return getResource().getContentLength(); + } + + private WebResource getResource() { + return root.getResource(webAppPath, false, usesClassLoaderResources); + } + } + + + /* + * Keep this in sync with CachedResourceURLConnection. + */ + private static class CachedResourceJarURLConnection extends JarURLConnection { + + private final StandardRoot root; + private final String webAppPath; + private final boolean usesClassLoaderResources; + private final URL resourceURL; + + protected CachedResourceJarURLConnection(URL resourceURL, StandardRoot root, String webAppPath, + boolean usesClassLoaderResources) throws IOException { + super(resourceURL); + this.root = root; + this.webAppPath = webAppPath; + this.usesClassLoaderResources = usesClassLoaderResources; + this.resourceURL = resourceURL; + } + + @Override + public void connect() throws IOException { + // NO-OP + } + + @Override + public InputStream getInputStream() throws IOException { + WebResource resource = getResource(); + if (resource.isDirectory()) { + return buildInputStream(resource.getWebResourceRoot().list(webAppPath)); + } else { + return getResource().getInputStream(); + } + } + + @Override + public Permission getPermission() throws IOException { + // Doesn't trigger a call to connect for jar:// URLs + return resourceURL.openConnection().getPermission(); + } + + @Override + public long getLastModified() { + return getResource().getLastModified(); + } + + @Override + public long getContentLengthLong() { + return getResource().getContentLength(); + } + + private WebResource getResource() { + return root.getResource(webAppPath, false, usesClassLoaderResources); + } + + @Override + public JarFile getJarFile() throws IOException { + return ((JarURLConnection) resourceURL.openConnection()).getJarFile(); + } + + } +} diff --git a/java/org/apache/catalina/webresources/ClasspathURLStreamHandler.java b/java/org/apache/catalina/webresources/ClasspathURLStreamHandler.java new file mode 100644 index 0000000..daab855 --- /dev/null +++ b/java/org/apache/catalina/webresources/ClasspathURLStreamHandler.java @@ -0,0 +1,49 @@ +/* + * 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.catalina.webresources; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import org.apache.tomcat.util.res.StringManager; + +public class ClasspathURLStreamHandler extends URLStreamHandler { + + private static final StringManager sm = StringManager.getManager(ClasspathURLStreamHandler.class); + + + @Override + protected URLConnection openConnection(URL u) throws IOException { + String path = u.getPath(); + + // Thread context class loader first + URL classpathUrl = Thread.currentThread().getContextClassLoader().getResource(path); + if (classpathUrl == null) { + // This class's class loader if no joy with the tccl + classpathUrl = ClasspathURLStreamHandler.class.getResource(path); + } + + if (classpathUrl == null) { + throw new FileNotFoundException(sm.getString("classpathUrlStreamHandler.notFound", u)); + } + + return classpathUrl.openConnection(); + } +} diff --git a/java/org/apache/catalina/webresources/DirResourceSet.java b/java/org/apache/catalina/webresources/DirResourceSet.java new file mode 100644 index 0000000..a221c3f --- /dev/null +++ b/java/org/apache/catalina/webresources/DirResourceSet.java @@ -0,0 +1,302 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Set; +import java.util.jar.Manifest; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceRoot.ResourceSetType; +import org.apache.catalina.util.ResourceSet; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Represents a {@link org.apache.catalina.WebResourceSet} based on a directory. + */ +public class DirResourceSet extends AbstractFileResourceSet { + + private static final Log log = LogFactory.getLog(DirResourceSet.class); + + /** + * A no argument constructor is required for this to work with the digester. + */ + public DirResourceSet() { + super("/"); + } + + /** + * Creates a new {@link org.apache.catalina.WebResourceSet} based on a directory. + * + * @param root The {@link WebResourceRoot} this new {@link org.apache.catalina.WebResourceSet} will be added + * to. + * @param webAppMount The path within the web application at which this {@link org.apache.catalina.WebResourceSet} + * will be mounted. For example, to add a directory of JARs to a web application, the + * directory would be mounted at "/WEB-INF/lib/" + * @param base The absolute path to the directory on the file system from which the resources will be + * served. + * @param internalPath The path within this new {@link org.apache.catalina.WebResourceSet} where resources will be + * served from. + */ + public DirResourceSet(WebResourceRoot root, String webAppMount, String base, String internalPath) { + super(internalPath); + setRoot(root); + setWebAppMount(webAppMount); + setBase(base); + + if (root.getContext().getAddWebinfClassesResources()) { + File f = new File(base, internalPath); + f = new File(f, "/WEB-INF/classes/META-INF/resources"); + + if (f.isDirectory()) { + root.createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", f.getAbsolutePath(), null, "/"); + } + } + + if (getRoot().getState().isAvailable()) { + try { + start(); + } catch (LifecycleException e) { + throw new IllegalStateException(e); + } + } + } + + + @Override + public WebResource getResource(String path) { + checkPath(path); + String webAppMount = getWebAppMount(); + WebResourceRoot root = getRoot(); + if (path.startsWith(webAppMount)) { + File f = file(path.substring(webAppMount.length()), false); + if (f == null) { + return new EmptyResource(root, path); + } + if (!f.exists()) { + return new EmptyResource(root, path, f); + } + if (f.isDirectory() && path.charAt(path.length() - 1) != '/') { + path = path + '/'; + } + return new FileResource(root, path, f, isReadOnly(), getManifest()); + } else { + return new EmptyResource(root, path); + } + } + + @Override + public String[] list(String path) { + checkPath(path); + String webAppMount = getWebAppMount(); + if (path.startsWith(webAppMount)) { + File f = file(path.substring(webAppMount.length()), true); + if (f == null) { + return EMPTY_STRING_ARRAY; + } + String[] result = f.list(); + if (result == null) { + return EMPTY_STRING_ARRAY; + } else { + return result; + } + } else { + if (!path.endsWith("/")) { + path = path + "/"; + } + if (webAppMount.startsWith(path)) { + int i = webAppMount.indexOf('/', path.length()); + if (i == -1) { + return new String[] { webAppMount.substring(path.length()) }; + } else { + return new String[] { webAppMount.substring(path.length(), i) }; + } + } + return EMPTY_STRING_ARRAY; + } + } + + @Override + public Set listWebAppPaths(String path) { + checkPath(path); + String webAppMount = getWebAppMount(); + ResourceSet result = new ResourceSet<>(); + if (path.startsWith(webAppMount)) { + File f = file(path.substring(webAppMount.length()), true); + if (f != null) { + File[] list = f.listFiles(); + if (list != null) { + for (File entry : list) { + // f has already been validated so the following checks + // can be much simpler than those in file() + if (!getRoot().getAllowLinking()) { + // allow linking is disabled so need to check for + // symlinks + boolean symlink = true; + String absPath = null; + String canPath = null; + try { + // We know that 'f' must be valid since it will + // have been checked in the call to file() + // above. Therefore strip off the path of the + // path that was contributed by 'f' and check + // that what is left does not contain a symlink. + absPath = entry.getAbsolutePath().substring(f.getAbsolutePath().length()); + String entryCanPath = entry.getCanonicalPath(); + String fCanPath = f.getCanonicalPath(); + if (entryCanPath.length() >= fCanPath.length()) { + canPath = entryCanPath.substring(fCanPath.length()); + if (absPath.equals(canPath)) { + symlink = false; + } + } + } catch (IOException ioe) { + // Ignore the exception. Assume we have a symlink. + canPath = "Unknown"; + } + if (symlink) { + logIgnoredSymlink(getRoot().getContext().getName(), absPath, canPath); + continue; + } + } + StringBuilder sb = new StringBuilder(path); + if (path.charAt(path.length() - 1) != '/') { + sb.append('/'); + } + sb.append(entry.getName()); + if (entry.isDirectory()) { + sb.append('/'); + } + result.add(sb.toString()); + } + } + } + } else { + if (!path.endsWith("/")) { + path = path + "/"; + } + if (webAppMount.startsWith(path)) { + int i = webAppMount.indexOf('/', path.length()); + if (i == -1) { + result.add(webAppMount + "/"); + } else { + result.add(webAppMount.substring(0, i + 1)); + } + } + } + result.setLocked(true); + return result; + } + + @Override + public boolean mkdir(String path) { + checkPath(path); + if (isReadOnly()) { + return false; + } + String webAppMount = getWebAppMount(); + if (path.startsWith(webAppMount)) { + File f = file(path.substring(webAppMount.length()), false); + if (f == null) { + return false; + } + return f.mkdir(); + } else { + return false; + } + } + + @Override + public boolean write(String path, InputStream is, boolean overwrite) { + checkPath(path); + + if (is == null) { + throw new NullPointerException(sm.getString("dirResourceSet.writeNpe")); + } + + if (isReadOnly()) { + return false; + } + + // write() is meant to create a file so ensure that the path doesn't + // end in '/' + if (path.endsWith("/")) { + return false; + } + + File dest = null; + String webAppMount = getWebAppMount(); + if (path.startsWith(webAppMount)) { + dest = file(path.substring(webAppMount.length()), false); + if (dest == null) { + return false; + } + } else { + return false; + } + + if (dest.exists() && !overwrite) { + return false; + } + + try { + if (overwrite) { + Files.copy(is, dest.toPath(), StandardCopyOption.REPLACE_EXISTING); + } else { + Files.copy(is, dest.toPath()); + } + } catch (IOException ioe) { + return false; + } + + return true; + } + + @Override + protected void checkType(File file) { + if (file.isDirectory() == false) { + throw new IllegalArgumentException( + sm.getString("dirResourceSet.notDirectory", getBase(), File.separator, getInternalPath())); + } + } + + // -------------------------------------------------------- Lifecycle methods + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + // Is this an exploded web application? + if (getWebAppMount().equals("")) { + // Look for a manifest + File mf = file("META-INF/MANIFEST.MF", true); + if (mf != null && mf.isFile()) { + try (FileInputStream fis = new FileInputStream(mf)) { + setManifest(new Manifest(fis)); + } catch (IOException e) { + log.warn(sm.getString("dirResourceSet.manifestFail", mf.getAbsolutePath()), e); + } + } + } + } +} diff --git a/java/org/apache/catalina/webresources/EmptyResource.java b/java/org/apache/catalina/webresources/EmptyResource.java new file mode 100644 index 0000000..7b35876 --- /dev/null +++ b/java/org/apache/catalina/webresources/EmptyResource.java @@ -0,0 +1,172 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.cert.Certificate; +import java.util.jar.Manifest; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; + +public class EmptyResource implements WebResource { + + private final WebResourceRoot root; + private final String webAppPath; + private final File file; + + public EmptyResource(WebResourceRoot root, String webAppPath) { + this(root, webAppPath, null); + } + + public EmptyResource(WebResourceRoot root, String webAppPath, File file) { + this.root = root; + this.webAppPath = webAppPath; + this.file = file; + } + + @Override + public long getLastModified() { + return 0; + } + + @Override + public String getLastModifiedHttp() { + return null; + } + + @Override + public boolean exists() { + return false; + } + + @Override + public boolean isVirtual() { + return false; + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public boolean isFile() { + return false; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public String getName() { + int index = webAppPath.lastIndexOf('/'); + if (index == -1) { + return webAppPath; + } else { + return webAppPath.substring(index + 1); + } + } + + @Override + public long getContentLength() { + return -1; + } + + @Override + public String getCanonicalPath() { + if (file == null) { + return null; + } else { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + return null; + } + } + } + + @Override + public boolean canRead() { + return false; + } + + @Override + public String getWebappPath() { + return webAppPath; + } + + @Override + public String getETag() { + return null; + } + + @Override + public void setMimeType(String mimeType) { + // NOOP + } + + @Override + public String getMimeType() { + return null; + } + + @Override + public InputStream getInputStream() { + return null; + } + + @Override + public byte[] getContent() { + return null; + } + + @Override + public long getCreation() { + return 0; + } + + @Override + public URL getURL() { + return null; + } + + @Override + public URL getCodeBase() { + return null; + } + + @Override + public Certificate[] getCertificates() { + return null; + } + + @Override + public Manifest getManifest() { + return null; + } + + @Override + public WebResourceRoot getWebResourceRoot() { + return root; + } +} diff --git a/java/org/apache/catalina/webresources/EmptyResourceSet.java b/java/org/apache/catalina/webresources/EmptyResourceSet.java new file mode 100644 index 0000000..c408feb --- /dev/null +++ b/java/org/apache/catalina/webresources/EmptyResourceSet.java @@ -0,0 +1,177 @@ +/* + * 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.catalina.webresources; + +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.Set; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.util.LifecycleBase; + +/** + * A {@link WebResourceSet} implementation that is not backed by a file system and behaves as if it has no resources + * available. This is primarily used in embedded mode when the web application is configured entirely programmatically + * and does not use any static resources from the file system. + */ +public class EmptyResourceSet extends LifecycleBase implements WebResourceSet { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private WebResourceRoot root; + private boolean classLoaderOnly; + private boolean staticOnly; + + public EmptyResourceSet(WebResourceRoot root) { + this.root = root; + } + + /** + * {@inheritDoc} + *

    + * This implementation always returns an {@link EmptyResource}. + */ + @Override + public WebResource getResource(String path) { + return new EmptyResource(root, path); + } + + /** + * {@inheritDoc} + *

    + * This implementation always returns an empty array. + */ + @Override + public String[] list(String path) { + return EMPTY_STRING_ARRAY; + } + + /** + * {@inheritDoc} + *

    + * This implementation always returns an empty set. + */ + @Override + public Set listWebAppPaths(String path) { + return Collections.emptySet(); + } + + /** + * {@inheritDoc} + *

    + * This implementation always returns false. + */ + @Override + public boolean mkdir(String path) { + return false; + } + + /** + * {@inheritDoc} + *

    + * This implementation always returns false. + */ + @Override + public boolean write(String path, InputStream is, boolean overwrite) { + return false; + } + + @Override + public void setRoot(WebResourceRoot root) { + this.root = root; + } + + @Override + public boolean getClassLoaderOnly() { + return classLoaderOnly; + } + + @Override + public void setClassLoaderOnly(boolean classLoaderOnly) { + this.classLoaderOnly = classLoaderOnly; + } + + @Override + public boolean getStaticOnly() { + return staticOnly; + } + + @Override + public void setStaticOnly(boolean staticOnly) { + this.staticOnly = staticOnly; + } + + /** + * {@inheritDoc} + *

    + * This implementation always returns null. + */ + @Override + public URL getBaseUrl() { + return null; + } + + /** + * {@inheritDoc} + *

    + * Calls to this method will be ignored as this implementation always read only. + */ + @Override + public void setReadOnly(boolean readOnly) { + + } + + /** + * {@inheritDoc} + *

    + * This implementation always returns true. + */ + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public void gc() { + // NO-OP + } + + @Override + protected void initInternal() throws LifecycleException { + // NO-OP + } + + @Override + protected void startInternal() throws LifecycleException { + setState(LifecycleState.STARTING); + } + + @Override + protected void stopInternal() throws LifecycleException { + setState(LifecycleState.STOPPING); + } + + @Override + protected void destroyInternal() throws LifecycleException { + // NO-OP + } +} diff --git a/java/org/apache/catalina/webresources/ExtractingRoot.java b/java/org/apache/catalina/webresources/ExtractingRoot.java new file mode 100644 index 0000000..fe97186 --- /dev/null +++ b/java/org/apache/catalina/webresources/ExtractingRoot.java @@ -0,0 +1,103 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import jakarta.servlet.ServletContext; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.util.IOTools; +import org.apache.tomcat.util.res.StringManager; + +/** + * If the main resources are packaged as a WAR file then any JARs will be extracted to the work directory and used from + * there. + */ +public class ExtractingRoot extends StandardRoot { + + private static final StringManager sm = StringManager.getManager(ExtractingRoot.class); + + private static final String APPLICATION_JARS_DIR = "application-jars"; + + @Override + protected void processWebInfLib() throws LifecycleException { + + // Don't extract JAR files unless the application is deployed as a + // packed WAR file. + if (!super.isPackedWarFile()) { + super.processWebInfLib(); + return; + } + + File expansionTarget = getExpansionTarget(); + if (!expansionTarget.isDirectory()) { + if (!expansionTarget.mkdirs()) { + throw new LifecycleException(sm.getString("extractingRoot.targetFailed", expansionTarget)); + } + } + + WebResource[] possibleJars = listResources("/WEB-INF/lib", false); + + for (WebResource possibleJar : possibleJars) { + if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) { + try { + File dest = new File(expansionTarget, possibleJar.getName()); + dest = dest.getCanonicalFile(); + try (InputStream sourceStream = possibleJar.getInputStream(); + OutputStream destStream = new FileOutputStream(dest)) { + IOTools.flow(sourceStream, destStream); + } + + createWebResourceSet(ResourceSetType.CLASSES_JAR, "/WEB-INF/classes", dest.toURI().toURL(), "/"); + } catch (IOException ioe) { + throw new LifecycleException(sm.getString("extractingRoot.jarFailed", possibleJar.getName()), ioe); + } + } + } + } + + private File getExpansionTarget() { + File tmpDir = (File) getContext().getServletContext().getAttribute(ServletContext.TEMPDIR); + File expansionTarget = new File(tmpDir, APPLICATION_JARS_DIR); + return expansionTarget; + } + + + @Override + protected boolean isPackedWarFile() { + return false; + } + + + @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + + if (super.isPackedWarFile()) { + // Remove the extracted JARs from the work directory + File expansionTarget = getExpansionTarget(); + ExpandWar.delete(expansionTarget); + } + } +} diff --git a/java/org/apache/catalina/webresources/FileResource.java b/java/org/apache/catalina/webresources/FileResource.java new file mode 100644 index 0000000..32ce5bf --- /dev/null +++ b/java/org/apache/catalina/webresources/FileResource.java @@ -0,0 +1,288 @@ +/* + * 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.catalina.webresources; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.cert.Certificate; +import java.util.jar.Manifest; + +import org.apache.catalina.WebResourceRoot; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Represents a single resource (file or directory) that is located on a file system. + */ +public class FileResource extends AbstractResource { + + private static final Log log = LogFactory.getLog(FileResource.class); + + private static final boolean PROPERTIES_NEED_CONVERT; + static { + boolean isEBCDIC = false; + try { + String encoding = Charset.defaultCharset().displayName(); + if (encoding.contains("EBCDIC")) { + isEBCDIC = true; + } + } catch (SecurityException e) { + // Ignore + } + PROPERTIES_NEED_CONVERT = isEBCDIC; + } + + + private final File resource; + private final String name; + private final boolean readOnly; + private final Manifest manifest; + private final boolean needConvert; + + public FileResource(WebResourceRoot root, String webAppPath, File resource, boolean readOnly, Manifest manifest) { + super(root, webAppPath); + this.resource = resource; + + if (webAppPath.charAt(webAppPath.length() - 1) == '/') { + String realName = resource.getName() + '/'; + if (webAppPath.endsWith(realName)) { + name = resource.getName(); + } else { + // This is the root directory of a mounted ResourceSet + // Need to return the mounted name, not the real name + int endOfName = webAppPath.length() - 1; + name = webAppPath.substring(webAppPath.lastIndexOf('/', endOfName - 1) + 1, endOfName); + } + } else { + // Must be a file + name = resource.getName(); + } + + this.readOnly = readOnly; + this.manifest = manifest; + this.needConvert = PROPERTIES_NEED_CONVERT && name.endsWith(".properties"); + } + + @Override + public long getLastModified() { + return resource.lastModified(); + } + + @Override + public boolean exists() { + return resource.exists(); + } + + @Override + public boolean isVirtual() { + return false; + } + + @Override + public boolean isDirectory() { + return resource.isDirectory(); + } + + @Override + public boolean isFile() { + return resource.isFile(); + } + + @Override + public boolean delete() { + if (readOnly) { + return false; + } + return resource.delete(); + } + + @Override + public String getName() { + return name; + } + + @Override + public long getContentLength() { + return getContentLengthInternal(needConvert); + } + + private long getContentLengthInternal(boolean convert) { + if (convert) { + byte[] content = getContent(); + if (content == null) { + return -1; + } else { + return content.length; + } + } + + if (isDirectory()) { + return -1; + } + + return resource.length(); + } + + @Override + public String getCanonicalPath() { + try { + return resource.getCanonicalPath(); + } catch (IOException ioe) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("fileResource.getCanonicalPathFail", resource.getPath()), ioe); + } + return null; + } + } + + @Override + public boolean canRead() { + return resource.canRead(); + } + + @Override + protected InputStream doGetInputStream() { + if (needConvert) { + byte[] content = getContent(); + if (content == null) { + return null; + } else { + return new ByteArrayInputStream(content); + } + } + try { + return new FileInputStream(resource); + } catch (FileNotFoundException fnfe) { + // Race condition (file has been deleted) - not an error + return null; + } + } + + @Override + public final byte[] getContent() { + // Use internal version to avoid loop when needConvert is true + long len = getContentLengthInternal(false); + + if (len > Integer.MAX_VALUE) { + // Can't create an array that big + throw new ArrayIndexOutOfBoundsException( + sm.getString("abstractResource.getContentTooLarge", getWebappPath(), Long.valueOf(len))); + } + + if (len < 0) { + // Content is not applicable here (e.g. is a directory) + return null; + } + + int size = (int) len; + byte[] result = new byte[size]; + + int pos = 0; + try (InputStream is = new FileInputStream(resource)) { + while (pos < size) { + int n = is.read(result, pos, size - pos); + if (n < 0) { + break; + } + pos += n; + } + } catch (IOException ioe) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractResource.getContentFail", getWebappPath()), ioe); + } + return null; + } + + if (needConvert) { + // Workaround for certain files on platforms that use + // EBCDIC encoding, when they are read through FileInputStream. + // See commit message of rev.303915 for original details + // https://svn.apache.org/viewvc?view=revision&revision=303915 + String str = new String(result); + try { + result = str.getBytes(StandardCharsets.UTF_8); + } catch (Exception e) { + result = null; + } + } + return result; + } + + + @Override + public long getCreation() { + try { + BasicFileAttributes attrs = Files.readAttributes(resource.toPath(), BasicFileAttributes.class); + return attrs.creationTime().toMillis(); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("fileResource.getCreationFail", resource.getPath()), e); + } + return 0; + } + } + + @Override + public URL getURL() { + if (resource.exists()) { + try { + return resource.toURI().toURL(); + } catch (MalformedURLException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("fileResource.getUrlFail", resource.getPath()), e); + } + return null; + } + } else { + return null; + } + } + + @Override + public URL getCodeBase() { + if (getWebappPath().startsWith("/WEB-INF/classes/") && name.endsWith(".class")) { + return getWebResourceRoot().getResource("/WEB-INF/classes/").getURL(); + } else { + return getURL(); + } + } + + @Override + public Certificate[] getCertificates() { + return null; + } + + @Override + public Manifest getManifest() { + return manifest; + } + + @Override + protected Log getLog() { + return log; + } +} diff --git a/java/org/apache/catalina/webresources/FileResourceSet.java b/java/org/apache/catalina/webresources/FileResourceSet.java new file mode 100644 index 0000000..035e2f7 --- /dev/null +++ b/java/org/apache/catalina/webresources/FileResourceSet.java @@ -0,0 +1,168 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.InputStream; +import java.util.Set; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.util.ResourceSet; + +/** + * Represents a {@link org.apache.catalina.WebResourceSet} based on a single file. + */ +public class FileResourceSet extends AbstractFileResourceSet { + + /** + * A no argument constructor is required for this to work with the digester. + */ + public FileResourceSet() { + super("/"); + } + + /** + * Creates a new {@link org.apache.catalina.WebResourceSet} based on a file. + * + * @param root The {@link WebResourceRoot} this new {@link org.apache.catalina.WebResourceSet} will be added + * to. + * @param webAppMount The path within the web application at which this {@link org.apache.catalina.WebResourceSet} + * will be mounted. For example, to add a directory of JARs to a web application, the + * directory would be mounted at "WEB-INF/lib/" + * @param base The absolute path to the file on the file system from which the resource will be served. + * @param internalPath The path within this new {@link org.apache.catalina.WebResourceSet} where resources will be + * served from. + */ + public FileResourceSet(WebResourceRoot root, String webAppMount, String base, String internalPath) { + super(internalPath); + setRoot(root); + setWebAppMount(webAppMount); + setBase(base); + + if (getRoot().getState().isAvailable()) { + try { + start(); + } catch (LifecycleException e) { + throw new IllegalStateException(e); + } + } + } + + + @Override + public WebResource getResource(String path) { + checkPath(path); + + String webAppMount = getWebAppMount(); + WebResourceRoot root = getRoot(); + if (path.equals(webAppMount)) { + File f = file("", true); + if (f == null) { + return new EmptyResource(root, path); + } + return new FileResource(root, path, f, isReadOnly(), null); + } + + if (path.charAt(path.length() - 1) != '/') { + path = path + '/'; + } + + if (webAppMount.startsWith(path)) { + String name = path.substring(0, path.length() - 1); + name = name.substring(name.lastIndexOf('/') + 1); + if (name.length() > 0) { + return new VirtualResource(root, path, name); + } + } + return new EmptyResource(root, path); + } + + @Override + public String[] list(String path) { + checkPath(path); + + if (path.charAt(path.length() - 1) != '/') { + path = path + '/'; + } + String webAppMount = getWebAppMount(); + + if (webAppMount.startsWith(path)) { + webAppMount = webAppMount.substring(path.length()); + if (webAppMount.equals(getFileBase().getName())) { + return new String[] { getFileBase().getName() }; + } else { + // Virtual directory + int i = webAppMount.indexOf('/'); + if (i > 0) { + return new String[] { webAppMount.substring(0, i) }; + } + } + } + + return EMPTY_STRING_ARRAY; + } + + @Override + public Set listWebAppPaths(String path) { + checkPath(path); + + ResourceSet result = new ResourceSet<>(); + + if (path.charAt(path.length() - 1) != '/') { + path = path + '/'; + } + String webAppMount = getWebAppMount(); + + if (webAppMount.startsWith(path)) { + webAppMount = webAppMount.substring(path.length()); + if (webAppMount.equals(getFileBase().getName())) { + result.add(path + getFileBase().getName()); + } else { + // Virtual directory + int i = webAppMount.indexOf('/'); + if (i > 0) { + result.add(path + webAppMount.substring(0, i + 1)); + } + } + } + + result.setLocked(true); + return result; + } + + @Override + public boolean mkdir(String path) { + checkPath(path); + return false; + } + + @Override + public boolean write(String path, InputStream is, boolean overwrite) { + checkPath(path); + return false; + } + + @Override + protected void checkType(File file) { + if (file.isFile() == false) { + throw new IllegalArgumentException( + sm.getString("fileResourceSet.notFile", getBase(), File.separator, getInternalPath())); + } + } +} diff --git a/java/org/apache/catalina/webresources/JarContents.java b/java/org/apache/catalina/webresources/JarContents.java new file mode 100644 index 0000000..e5a5d9d --- /dev/null +++ b/java/org/apache/catalina/webresources/JarContents.java @@ -0,0 +1,140 @@ +/* + * 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.catalina.webresources; + +import java.util.BitSet; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * This class represents the contents of a jar by determining whether a given resource might be in the cache, + * based on a bloom filter. This is not a general-purpose bloom filter because it contains logic to strip out characters + * from the beginning of the key. The hash methods are simple but good enough for this purpose. + */ +public final class JarContents { + private final BitSet bits1; + private final BitSet bits2; + /** + * Constant used by a typical hashing method. + */ + private static final int HASH_PRIME_1 = 31; + + /** + * Constant used by a typical hashing method. + */ + private static final int HASH_PRIME_2 = 17; + + /** + * Size of the fixed-length bit table. Larger reduces false positives, smaller saves memory. + */ + private static final int TABLE_SIZE = 2048; + + /** + * Parses the passed-in jar and populates the bit array. + * + * @param jar the JAR file + */ + public JarContents(JarFile jar) { + Enumeration entries = jar.entries(); + bits1 = new BitSet(TABLE_SIZE); + bits2 = new BitSet(TABLE_SIZE); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + int startPos = 0; + + // If the path starts with a slash, that's not useful information. + // Skipping it increases the significance of our key by + // removing an insignificant character. + boolean precedingSlash = name.charAt(0) == '/'; + if (precedingSlash) { + startPos = 1; + } + + // Find the correct table slot + int pathHash1 = hashcode(name, startPos, HASH_PRIME_1); + int pathHash2 = hashcode(name, startPos, HASH_PRIME_2); + + bits1.set(pathHash1 % TABLE_SIZE); + bits2.set(pathHash2 % TABLE_SIZE); + + // While directory entry names always end in "/", application code + // may look them up without the trailing "/". Add this second form. + if (entry.isDirectory()) { + pathHash1 = hashcode(name, startPos, name.length() - 1, HASH_PRIME_1); + pathHash2 = hashcode(name, startPos, name.length() - 1, HASH_PRIME_2); + + bits1.set(pathHash1 % TABLE_SIZE); + bits2.set(pathHash2 % TABLE_SIZE); + } + } + } + + /** + * Simple hashcode of a portion of the string. Typically we would use substring, but memory and runtime speed are + * critical. + * + * @param content Wrapping String. + * @param startPos First character in the range. + * + * @return hashcode of the range. + */ + private int hashcode(String content, int startPos, int hashPrime) { + return hashcode(content, startPos, content.length(), hashPrime); + } + + private int hashcode(String content, int startPos, int endPos, int hashPrime) { + int h = hashPrime / 2; + for (int i = startPos; i < endPos; i++) { + h = hashPrime * h + content.charAt(i); + } + + if (h < 0) { + h = h * -1; + } + return h; + } + + + /** + * Method that identifies whether a given path MIGHT be in this jar. Uses the Bloom filter mechanism. + * + * @param path Requested path. Sometimes starts with "/WEB-INF/classes". + * @param webappRoot The value of the webapp location, which can be stripped from the path. Typically is + * "/WEB-INF/classes". + * + * @return Whether the prefix of the path is known to be in this jar. + */ + public boolean mightContainResource(String path, String webappRoot) { + int startPos = 0; + if (path.startsWith(webappRoot)) { + startPos = webappRoot.length(); + } + + if (path.charAt(startPos) == '/') { + // ignore leading slash + startPos++; + } + + // calculate the hash lazily and return a boolean value for this path + return (bits1.get(hashcode(path, startPos, HASH_PRIME_1) % TABLE_SIZE) && + bits2.get(hashcode(path, startPos, HASH_PRIME_2) % TABLE_SIZE)); + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/webresources/JarResource.java b/java/org/apache/catalina/webresources/JarResource.java new file mode 100644 index 0000000..5619930 --- /dev/null +++ b/java/org/apache/catalina/webresources/JarResource.java @@ -0,0 +1,42 @@ +/* + * 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.catalina.webresources; + +import java.util.jar.JarEntry; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Represents a single resource (file or directory) that is located within a JAR. + */ +public class JarResource extends AbstractSingleArchiveResource { + + private static final Log log = LogFactory.getLog(JarResource.class); + + + public JarResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, String baseUrl, + JarEntry jarEntry) { + super(archiveResourceSet, webAppPath, "jar:" + baseUrl + "!/", jarEntry, baseUrl); + } + + + @Override + protected Log getLog() { + return log; + } +} diff --git a/java/org/apache/catalina/webresources/JarResourceRoot.java b/java/org/apache/catalina/webresources/JarResourceRoot.java new file mode 100644 index 0000000..cdd6e1b --- /dev/null +++ b/java/org/apache/catalina/webresources/JarResourceRoot.java @@ -0,0 +1,162 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.cert.Certificate; +import java.util.jar.Manifest; + +import org.apache.catalina.WebResourceRoot; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class JarResourceRoot extends AbstractResource { + + private static final Log log = LogFactory.getLog(JarResourceRoot.class); + + private final File base; + private final String baseUrl; + private final String name; + + public JarResourceRoot(WebResourceRoot root, File base, String baseUrl, String webAppPath) { + super(root, webAppPath); + // Validate the webAppPath before going any further + if (!webAppPath.endsWith("/")) { + throw new IllegalArgumentException(sm.getString("jarResourceRoot.invalidWebAppPath", webAppPath)); + } + this.base = base; + this.baseUrl = "jar:" + baseUrl; + // Extract the name from the webAppPath + // Strip the trailing '/' character + String resourceName = webAppPath.substring(0, webAppPath.length() - 1); + int i = resourceName.lastIndexOf('/'); + if (i > -1) { + resourceName = resourceName.substring(i + 1); + } + name = resourceName; + } + + @Override + public long getLastModified() { + return base.lastModified(); + } + + @Override + public boolean exists() { + return true; + } + + @Override + public boolean isVirtual() { + return false; + } + + @Override + public boolean isDirectory() { + return true; + } + + @Override + public boolean isFile() { + return false; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public String getName() { + return name; + } + + @Override + public long getContentLength() { + return -1; + } + + @Override + public String getCanonicalPath() { + return null; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + protected InputStream doGetInputStream() { + return null; + } + + @Override + public byte[] getContent() { + return null; + } + + @Override + public long getCreation() { + return base.lastModified(); + } + + @Override + public URL getURL() { + String url = baseUrl + "!/"; + try { + return new URI(url).toURL(); + } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("fileResource.getUrlFail", url), e); + } + return null; + } + } + + @Override + public URL getCodeBase() { + try { + return new URI(baseUrl).toURL(); + } catch (MalformedURLException | URISyntaxException e) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("fileResource.getUrlFail", baseUrl), e); + } + return null; + } + } + + @Override + protected Log getLog() { + return log; + } + + @Override + public Certificate[] getCertificates() { + return null; + } + + @Override + public Manifest getManifest() { + return null; + } +} diff --git a/java/org/apache/catalina/webresources/JarResourceSet.java b/java/org/apache/catalina/webresources/JarResourceSet.java new file mode 100644 index 0000000..c6e8a09 --- /dev/null +++ b/java/org/apache/catalina/webresources/JarResourceSet.java @@ -0,0 +1,61 @@ +/* + * 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.catalina.webresources; + +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; + +/** + * Represents a {@link org.apache.catalina.WebResourceSet} based on a JAR file. + */ +public class JarResourceSet extends AbstractSingleArchiveResourceSet { + + /** + * A no argument constructor is required for this to work with the digester. + */ + public JarResourceSet() { + } + + + /** + * Creates a new {@link org.apache.catalina.WebResourceSet} based on a JAR file. + * + * @param root The {@link WebResourceRoot} this new {@link org.apache.catalina.WebResourceSet} will be added + * to. + * @param webAppMount The path within the web application at which this {@link org.apache.catalina.WebResourceSet} + * will be mounted. + * @param base The absolute path to the JAR file on the file system from which the resources will be served. + * @param internalPath The path within this new {@link org.apache.catalina.WebResourceSet} where resources will be + * served from. E.g. for a resource JAR, this would be "META-INF/resources" + * + * @throws IllegalArgumentException if the webAppMount or internalPath is not valid (valid paths must start with + * '/') + */ + public JarResourceSet(WebResourceRoot root, String webAppMount, String base, String internalPath) + throws IllegalArgumentException { + super(root, webAppMount, base, internalPath); + } + + + @Override + protected WebResource createArchiveResource(JarEntry jarEntry, String webAppPath, Manifest manifest) { + return new JarResource(this, webAppPath, getBaseUrlString(), jarEntry); + } +} diff --git a/java/org/apache/catalina/webresources/JarWarResource.java b/java/org/apache/catalina/webresources/JarWarResource.java new file mode 100644 index 0000000..dcefd22 --- /dev/null +++ b/java/org/apache/catalina/webresources/JarWarResource.java @@ -0,0 +1,94 @@ +/* + * 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.catalina.webresources; + +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.UriUtil; + +/** + * Represents a single resource (file or directory) that is located within a JAR that in turn is located in a WAR file. + */ +public class JarWarResource extends AbstractArchiveResource { + + private static final Log log = LogFactory.getLog(JarWarResource.class); + + private final String archivePath; + + public JarWarResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, String baseUrl, + JarEntry jarEntry, String archivePath) { + + super(archiveResourceSet, webAppPath, "jar:war:" + baseUrl + UriUtil.getWarSeparator() + archivePath + "!/", + jarEntry, "war:" + baseUrl + UriUtil.getWarSeparator() + archivePath); + this.archivePath = archivePath; + } + + @Override + protected JarInputStreamWrapper getJarInputStreamWrapper() { + JarFile warFile = null; + JarInputStream jarIs = null; + JarEntry entry = null; + try { + warFile = getArchiveResourceSet().openJarFile(); + JarEntry jarFileInWar = warFile.getJarEntry(archivePath); + InputStream isInWar = warFile.getInputStream(jarFileInWar); + + jarIs = new JarInputStream(isInWar); + entry = jarIs.getNextJarEntry(); + while (entry != null && !entry.getName().equals(getResource().getName())) { + entry = jarIs.getNextJarEntry(); + } + + if (entry == null) { + return null; + } + + return new JarInputStreamWrapper(entry, jarIs); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jarResource.getInputStreamFail", getResource().getName(), getBaseUrl()), e); + } + // Ensure jarIs is closed if there is an exception + entry = null; + return null; + } finally { + if (entry == null) { + if (jarIs != null) { + try { + jarIs.close(); + } catch (IOException ioe) { + // Ignore + } + } + if (warFile != null) { + getArchiveResourceSet().closeJarFile(); + } + } + } + } + + @Override + protected Log getLog() { + return log; + } +} diff --git a/java/org/apache/catalina/webresources/JarWarResourceSet.java b/java/org/apache/catalina/webresources/JarWarResourceSet.java new file mode 100644 index 0000000..68bef6d --- /dev/null +++ b/java/org/apache/catalina/webresources/JarWarResourceSet.java @@ -0,0 +1,263 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.tomcat.util.buf.UriUtil; + +/** + * Represents a {@link org.apache.catalina.WebResourceSet} based on a JAR file that is nested inside a packed WAR file. + * This is only intended for internal use within Tomcat and therefore cannot be created via configuration. + */ +public class JarWarResourceSet extends AbstractArchiveResourceSet { + + private final String archivePath; + + /** + * Creates a new {@link org.apache.catalina.WebResourceSet} based on a JAR file that is nested inside a WAR. + * + * @param root The {@link WebResourceRoot} this new {@link org.apache.catalina.WebResourceSet} will be added + * to. + * @param webAppMount The path within the web application at which this {@link org.apache.catalina.WebResourceSet} + * will be mounted. + * @param base The absolute path to the WAR file on the file system in which the JAR is located. + * @param archivePath The path within the WAR file where the JAR file is located. + * @param internalPath The path within this new {@link org.apache.catalina.WebResourceSet} where resources will be + * served from. E.g. for a resource JAR, this would be "META-INF/resources" + * + * @throws IllegalArgumentException if the webAppMount or internalPath is not valid (valid paths must start with + * '/') + */ + public JarWarResourceSet(WebResourceRoot root, String webAppMount, String base, String archivePath, + String internalPath) throws IllegalArgumentException { + setRoot(root); + setWebAppMount(webAppMount); + setBase(base); + this.archivePath = archivePath; + setInternalPath(internalPath); + + if (getRoot().getState().isAvailable()) { + try { + start(); + } catch (LifecycleException e) { + throw new IllegalStateException(e); + } + } + } + + @Override + protected WebResource createArchiveResource(JarEntry jarEntry, String webAppPath, Manifest manifest) { + return new JarWarResource(this, webAppPath, getBaseUrlString(), jarEntry, archivePath); + } + + + /** + * {@inheritDoc} + *

    + * JarWar can't optimise for a single resource so the Map is always returned. + */ + @Override + protected Map getArchiveEntries(boolean single) { + synchronized (archiveLock) { + if (archiveEntries == null) { + JarFile warFile = null; + InputStream jarFileIs = null; + archiveEntries = new HashMap<>(); + boolean multiRelease = false; + try { + warFile = openJarFile(); + JarEntry jarFileInWar = warFile.getJarEntry(archivePath); + jarFileIs = warFile.getInputStream(jarFileInWar); + + try (TomcatJarInputStream jarIs = new TomcatJarInputStream(jarFileIs)) { + JarEntry entry = jarIs.getNextJarEntry(); + while (entry != null) { + archiveEntries.put(entry.getName(), entry); + entry = jarIs.getNextJarEntry(); + } + Manifest m = jarIs.getManifest(); + setManifest(m); + if (m != null) { + String value = m.getMainAttributes().getValue("Multi-Release"); + if (value != null) { + multiRelease = Boolean.parseBoolean(value); + } + } + // Hack to work-around JarInputStream swallowing these + // entries. TomcatJarInputStream is used above which + // extends JarInputStream and the method that creates + // the entries over-ridden so we can a) tell if the + // entries are present and b) cache them so we can + // access them here. + entry = jarIs.getMetaInfEntry(); + if (entry != null) { + archiveEntries.put(entry.getName(), entry); + } + entry = jarIs.getManifestEntry(); + if (entry != null) { + archiveEntries.put(entry.getName(), entry); + } + } + if (multiRelease) { + processArchivesEntriesForMultiRelease(); + } + } catch (IOException ioe) { + // Should never happen + archiveEntries = null; + throw new IllegalStateException(ioe); + } finally { + if (warFile != null) { + closeJarFile(); + } + if (jarFileIs != null) { + try { + jarFileIs.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + return archiveEntries; + } + } + + + protected void processArchivesEntriesForMultiRelease() { + + int targetVersion = Runtime.version().feature(); + + Map versionedEntries = new HashMap<>(); + Iterator> iter = archiveEntries.entrySet().iterator(); + while (iter.hasNext()) { + Entry entry = iter.next(); + String name = entry.getKey(); + if (name.startsWith("META-INF/versions/")) { + // Remove the multi-release version + iter.remove(); + + // Get the base name and version for this versioned entry + int i = name.indexOf('/', 18); + if (i > 0) { + String baseName = name.substring(i + 1); + int version = Integer.parseInt(name.substring(18, i)); + + // Ignore any entries targeting for a later version than + // the target for this runtime + if (version <= targetVersion) { + VersionedJarEntry versionedJarEntry = versionedEntries.get(baseName); + if (versionedJarEntry == null) { + // No versioned entry found for this name. Create + // one. + versionedEntries.put(baseName, new VersionedJarEntry(version, entry.getValue())); + } else { + // Ignore any entry for which we have already found + // a later version + if (version > versionedJarEntry.getVersion()) { + // Replace the entry targeted at an earlier + // version + versionedEntries.put(baseName, new VersionedJarEntry(version, entry.getValue())); + } + } + } + } + } + } + + for (Entry versionedJarEntry : versionedEntries.entrySet()) { + archiveEntries.put(versionedJarEntry.getKey(), versionedJarEntry.getValue().getJarEntry()); + } + } + + + /** + * {@inheritDoc} + *

    + * Should never be called since {@link #getArchiveEntries(boolean)} always returns a Map. + */ + @Override + protected JarEntry getArchiveEntry(String pathInArchive) { + throw new IllegalStateException(sm.getString("jarWarResourceSet.codingError")); + } + + + @Override + protected boolean isMultiRelease() { + // This always returns false otherwise the superclass will call + // #getArchiveEntry(String) + return false; + } + + + // -------------------------------------------------------- Lifecycle methods + @Override + protected void initInternal() throws LifecycleException { + + try (JarFile warFile = new JarFile(getBase())) { + JarEntry jarFileInWar = warFile.getJarEntry(archivePath); + InputStream jarFileIs = warFile.getInputStream(jarFileInWar); + + try (JarInputStream jarIs = new JarInputStream(jarFileIs)) { + setManifest(jarIs.getManifest()); + } + } catch (IOException ioe) { + throw new IllegalArgumentException(ioe); + } + + try { + setBaseUrl(UriUtil.buildJarSafeUrl(new File(getBase()))); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + + private static final class VersionedJarEntry { + private final int version; + private final JarEntry jarEntry; + + VersionedJarEntry(int version, JarEntry jarEntry) { + this.version = version; + this.jarEntry = jarEntry; + } + + + public int getVersion() { + return version; + } + + + public JarEntry getJarEntry() { + return jarEntry; + } + } +} diff --git a/java/org/apache/catalina/webresources/LocalStrings.properties b/java/org/apache/catalina/webresources/LocalStrings.properties new file mode 100644 index 0000000..0b8472f --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings.properties @@ -0,0 +1,61 @@ +# 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. + +abstractArchiveResourceSet.setReadOnlyFalse=Archive based WebResourceSets such as those based on JARs are hard-coded to be read-only and may not be configured to be read-write + +abstractFileResourceSet.canonicalfileCheckFailed=Resource for web application [{0}] at path [{1}] was not loaded as the canonical path [{2}] did not match. Use of symlinks is one possible cause. + +abstractResource.getContentFail=Unable to return [{0}] as a byte array +abstractResource.getContentTooLarge=Unable to return [{0}] as a byte array since the resource is [{1}] bytes in size which is larger than the maximum size of a byte array + +abstractResourceSet.checkPath=The requested path [{0}] is not valid. It must begin with "/". + +cache.addFail=Unable to add the resource at [{0}] to the cache for web application [{1}] because there was insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache +cache.backgroundEvictFail=The background cache eviction process was unable to free [{0}] percent of the cache for Context [{1}] - consider increasing the maximum size of the cache. After eviction approximately [{2}] KiB of data remained in the cache. +cache.objectMaxSizeTooBig=The value of [{0}] KiB for objectMaxSize is larger than the limit of maxSize/20 so has been reduced to [{1}] KiB +cache.objectMaxSizeTooBigBytes=The value specified for the maximum object size to cache [{0}] KiB is greater than Integer.MAX_VALUE bytes which is the maximum size that can be cached. The limit will be set to Integer.MAX_VALUE bytes. + +cachedResource.invalidURL=Unable to create an instance of CachedResourceURLStreamHandler because the URL [{0}] is malformed + +classpathUrlStreamHandler.notFound=Unable to load the resource [{0}] using the thread context class loader or the current class''s class loader + +dirResourceSet.manifestFail=Failed to read manifest from [{0}] +dirResourceSet.notDirectory=The directory specified by base and internal path [{0}]{1}[{2}] does not exist. +dirResourceSet.writeNpe=The input stream may not be null + +extractingRoot.jarFailed=Failed to extract the JAR file [{0}] +extractingRoot.targetFailed=Failed to create the directory [{0}] for extracted JAR files + +fileResource.getCanonicalPathFail=Unable to determine the canonical path for the resource [{0}] +fileResource.getCreationFail=Unable to determine the creation time for the resource [{0}] +fileResource.getUrlFail=Unable to determine a URL for the resource [{0}] + +fileResourceSet.notFile=The file specified by base and internal path [{0}]{1}[{2}] does not exist. + +jarResource.getInputStreamFail=Unable to obtain an InputStream for the resource [{0}] located in the JAR [{1}] + +jarResourceRoot.invalidWebAppPath=This resource always refers to a directory so the supplied webAppPath must end with / but the provided webAppPath was [{0}] + +jarWarResourceSet.codingError=Coding error + +standardRoot.checkStateNotStarted=The resources may not be accessed if they are not currently started +standardRoot.createInvalidFile=Unable to create WebResourceSet from [{0}] +standardRoot.createUnknownType=Unable to create WebResourceSet of unknown type [{0}] +standardRoot.invalidPath=The resource path [{0}] is not valid +standardRoot.invalidPathNormal=The resource path [{0}] has been normalized to [{1}] which is not valid +standardRoot.lockedFile=The web application [{0}] failed to close the file [{1}] opened via the following stack trace +standardRoot.noContext=A Context has not been configured for this WebResourceRoot +standardRoot.startInvalidMain=The main resource set specified [{0}] is not a directory or war file, or is not readable (it does not exist or permissions to access it are missing) +standardRoot.unsupportedProtocol=The URL protocol [{0}] is not supported by this web resources implementation diff --git a/java/org/apache/catalina/webresources/LocalStrings_cs.properties b/java/org/apache/catalina/webresources/LocalStrings_cs.properties new file mode 100644 index 0000000..a342292 --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_cs.properties @@ -0,0 +1,20 @@ +# 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. + +cache.addFail=Nelze pÅ™idat zdroj [{0}] do cache webové aplikace [{1}], protože není dostatek volného místa po uvolnÄ›ní proÅ¡lých cache záznamů - zvažte zvýšení maximální velikosti cache + +extractingRoot.targetFailed=Selhalo vytvoÅ™ení adresáře [{0}] pro rozbalené JAR soubory + +standardRoot.createUnknownType=Nelze vytvoÅ™it WebResourceSet neznámého typu [{0}] diff --git a/java/org/apache/catalina/webresources/LocalStrings_de.properties b/java/org/apache/catalina/webresources/LocalStrings_de.properties new file mode 100644 index 0000000..25d6dbd --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +extractingRoot.targetFailed=Konnte Verzeichnis [{0}] zum entpacken einer JAR-Datei nicht anlegen diff --git a/java/org/apache/catalina/webresources/LocalStrings_es.properties b/java/org/apache/catalina/webresources/LocalStrings_es.properties new file mode 100644 index 0000000..323028d --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_es.properties @@ -0,0 +1,22 @@ +# 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. + +cache.addFail=Imposible adicionar recursos a [{0}] de la cache para la applicación web [{1}] porque no hay suficiente espacio libre luego de eliminar los datos expirados de la caché - considere incrementar el tamaño máximo de la chaché + +dirResourceSet.notDirectory=El directorio especificado por la base y el camino interno [{0}]{1}[{2}] no existe.\n + +extractingRoot.targetFailed=Fallo al crear directorio [{0}] para los archivos JAR extraidos + +standardRoot.createUnknownType=Imposible crear WebResourceSet de tipo desconocido [{0}]\n diff --git a/java/org/apache/catalina/webresources/LocalStrings_fr.properties b/java/org/apache/catalina/webresources/LocalStrings_fr.properties new file mode 100644 index 0000000..0bea5d9 --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_fr.properties @@ -0,0 +1,61 @@ +# 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. + +abstractArchiveResourceSet.setReadOnlyFalse=Les archives basées sur WebResourceSets telles que celles des JARs sont fixées comme étant en lecture seule et ne peuvent être configurées en lecture écriture + +abstractFileResourceSet.canonicalfileCheckFailed=La ressource de l''application web [{0}] du chemin [{1}] n''a pas été chargée car le chemin canonique [{2}] ne correspond pas; l''utilisation de liens symboliques peut être une cause possible + +abstractResource.getContentFail=Impossible de retourner [{0}] en tant que tableau d''octets +abstractResource.getContentTooLarge=Impossible de retourner [{0}] comme tableau d''octets car la ressource a une taille de [{1}] octets qui est supérieure à la taille maximale d''un tableau d''octets + +abstractResourceSet.checkPath=Le chemin demandé [{0}] n''est pas valide, il doit commencer par ''/'' + +cache.addFail=Incapable d''ajouter la ressource située [{0}] au cache de l''application web [{1}] parce qu''il n''y avait pas assez d''espace libre disponible après l''éviction des entrées de cache expirées - envisagez d''augmenter la taille maximale du cache +cache.backgroundEvictFail=Le processus d''arrière plan d''éviction du cache n''a pas pu nettoyer [{0}] pourcents du cache pour le contexte [{1}], il faudrait augmenter la taille maximale du cache ; après l''éviction, approximativement [{2}] KiB de données restaient dans le cache +cache.objectMaxSizeTooBig=La valeur [{0}] KiB pour l''objectMaxSize est plus grade que la limite de maxSize/20 son elle a été réduite à [{1}] KiB\n +cache.objectMaxSizeTooBigBytes=La valeur de taille d''objet maximale pouvant être mis en cache de [{0}] KiB est supérieure à Integer.MAX_VALUE qui est le maximum, la limite a donc été fixée à Integer.MAX_VALUE octets + +cachedResource.invalidURL=La création d''une instance de CachedResourceURLStreamHandler a échouée car l''URL [{0}] est malformée + +classpathUrlStreamHandler.notFound=Impossible de charger la ressource [{0}] en utilisant le chargeur de classe de contexte du thread ou celui de la classe actuelle + +dirResourceSet.manifestFail=Impossible de lire le manifeste depuis [{0}] +dirResourceSet.notDirectory=Le répertoire qui a été spécifié pour la base et le chemin interne [{0}]{1}[{2}] n''existe pas +dirResourceSet.writeNpe=Le flux d'entrée ne peut pas être null + +extractingRoot.jarFailed=Echec de l’extraction du fichier JAR [{0}] +extractingRoot.targetFailed=Echec de la création du répertoire [{0}] pour l''extraction des fichiers contenus dans le JAR + +fileResource.getCanonicalPathFail=Impossible de déterminer le chemin canonique pour la ressource [{0}] +fileResource.getCreationFail=Impossible de déterminer la date de création de la ressource [{0}] +fileResource.getUrlFail=Impossible de déterminer l''URL pour la ressource [{0}] + +fileResourceSet.notFile=Le fichier spécifié par ses chemins de base et internes [{0}]{1}[{2}] n''existe pas + +jarResource.getInputStreamFail=Impossible d''obtenir une InputStream pour la ressource [{0}] située dans le JAR [{1}] + +jarResourceRoot.invalidWebAppPath=Cette ressource se réfère toujours à un répertoire donc le webAppPath fourni doit se terminer avec ''/'' mais il était [{0}] + +jarWarResourceSet.codingError=Erreur de programmation + +standardRoot.checkStateNotStarted=Les ressources ne peuvent pas être accédées tant qu'elles ne sont pas démarrées +standardRoot.createInvalidFile=Impossible de créer WebResourceSet à partir de [{0}] +standardRoot.createUnknownType=Impossible de créer un WebResourceSet pour le type inconnu [{0}] +standardRoot.invalidPath=Le chemin de ressources [{0}] est invalide +standardRoot.invalidPathNormal=Le chemin de ressource [{0}] a été normalisé en [{1}] ce qui est invalide +standardRoot.lockedFile=L''application web [{0}] n''a pas fermé le fichier [{1}] ouvert à partir de la trace +standardRoot.noContext=Un contexte n'a pas été configuré pour ce WebResourceRoot +standardRoot.startInvalidMain=L''ensemble de ressources principal [{0}] n''est pas un répertoire, un fichier war, ou n''est pas lisible (il n''existe pas ou les permissions pour y accéder manquent) +standardRoot.unsupportedProtocol=Le protocole [{0}] de l''URL n''est pas supporté par cette implémentation des ressources web diff --git a/java/org/apache/catalina/webresources/LocalStrings_ja.properties b/java/org/apache/catalina/webresources/LocalStrings_ja.properties new file mode 100644 index 0000000..7a66dba --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_ja.properties @@ -0,0 +1,61 @@ +# 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. + +abstractArchiveResourceSet.setReadOnlyFalse=JAR ã«åŸºã¥ã WebResourceSet ãªã©ã®ã‚¢ãƒ¼ã‚«ã‚¤ãƒ– ベース㮠WebResourceSet ã¯ã€èª­ã¿å–り専用ã«ãƒãƒ¼ãƒ‰ã‚³ãƒ¼ãƒ‰ã•ã‚Œã¦ãŠã‚Šã€èª­ã¿å–ã‚Š/書ãè¾¼ã¿å¯èƒ½ã«æ§‹æˆã•ã‚Œã¦ã„ãªã„å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ + +abstractFileResourceSet.canonicalfileCheckFailed=æ­£è¦ãƒ‘ス [{2}] ãŒä¸€è‡´ã—ãªã‹ã£ãŸãŸã‚ã€ãƒ‘ス [{1}] ã®Webアプリケーション [{0}] ã®ãƒªã‚½ãƒ¼ã‚¹ãŒèª­ã¿è¾¼ã¾ã‚Œã¾ã›ã‚“ã§ã—ãŸã€‚ シンボリックリンクã®ä½¿ç”¨ã¯ã€è€ƒãˆã‚‰ã‚Œã‚‹åŽŸå› ã®1ã¤ã§ã™ã€‚ + +abstractResource.getContentFail=[{0}]ã‚’ãƒã‚¤ãƒˆé…列ã¨ã—ã¦è¿”ã™ã“ã¨ãŒã§ãã¾ã›ã‚“。 +abstractResource.getContentTooLarge=リソースãŒãƒã‚¤ãƒˆé…列ã®æœ€å¤§ã‚µã‚¤ã‚ºã‚ˆã‚Šã‚‚大ãã„サイズ㮠[{1}] ãƒã‚¤ãƒˆã§ã‚ã‚‹ãŸã‚ã€[{0}] ã‚’ãƒã‚¤ãƒˆé…列ã¨ã—ã¦è¿”ã™ã“ã¨ãŒã§ãã¾ã›ã‚“ + +abstractResourceSet.checkPath=リクエストパス [{0}] ãŒç„¡åŠ¹ã§ã™ã€‚"/"ã§å§‹ã¾ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ + +cache.addFail=有効期é™åˆ‡ã‚Œã®é …目を破棄ã—ã¦ã‚‚利用å¯èƒ½ãªé ˜åŸŸãŒä¸è¶³ã™ã‚‹ãŸã‚ã€Web アプリケーション [{1}] ã®ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã«ãƒªã‚½ãƒ¼ã‚¹ [{0}] を追加ã§ãã¾ã›ã‚“。最大キャッシュサイズã®å¢—加を検討ã—ã¦ãã ã•ã„。 +cache.backgroundEvictFail=コンテキスト [{1}] ã®ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã‚­ãƒ£ãƒƒã‚·ãƒ¥å‰Šé™¤å‡¦ç†ã¯å…¨ä½“ã® [{0}] % を解放ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚キャッシュサイズã®æœ€å¤§å€¤ã®å¢—加を検討ã—ã¦ãã ã•ã„。ç¾åœ¨ã¯ç´„ [{2}] KiB ã®ãƒ‡ãƒ¼ã‚¿ãŒã‚­ãƒ£ãƒƒã‚·ãƒ¥ã«æ®‹å­˜ã—ã¦ã„ã¾ã™ã€‚ +cache.objectMaxSizeTooBig=objectMaxSizeã® [{0}] KiBã®å€¤ãŒmaxSize / 20ã®åˆ¶é™ã‚ˆã‚Šå¤§ãã„ãŸã‚ã€[{1}] KiBã«æ¸›å°‘ã—ã¾ã—㟠+cache.objectMaxSizeTooBigBytes=キャッシュå¯èƒ½ãªã‚ªãƒ–ジェクトサイズã®æœ€å¤§å€¤ã«æŒ‡å®šã•ã‚ŒãŸ [{0}] KiB 㯠Integer.MAX_VALUE ãƒã‚¤ãƒˆã‚’越ãˆã¦ã„ã¾ã™ã€‚最大値㫠Integer.MAX_VALUE を設定ã—ã¾ã™ã€‚ + +cachedResource.invalidURL=URL [{0}] ã¯ä¸æ­£ã§ã™ã€‚CachedResourceURLStreamHandler インスタンスを生æˆã§ãã¾ã›ã‚“ + +classpathUrlStreamHandler.notFound=スレッドコンテキストクラスローダーã€ã‚ã‚‹ã„ã¯ã€ç¾åœ¨ã®ã‚¯ãƒ©ã‚¹ã®ã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ãƒ¼ã§ãƒªã‚½ãƒ¼ã‚¹ [{0}] を読ã¿è¾¼ã¿ã§ãã¾ã›ã‚“ + +dirResourceSet.manifestFail=[{0}]ã‹ã‚‰ãƒžãƒ‹ãƒ•ã‚§ã‚¹ãƒˆã‚’読ã¿è¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸã€‚ +dirResourceSet.notDirectory=ベースパスã¨å†…部パスã§æŒ‡å®šã—㟠[{0}][{1}][{2}] ã«ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªãŒã‚ã‚Šã¾ã›ã‚“。 +dirResourceSet.writeNpe=入力ストリームã«ã¯ null を指定ã§ãã¾ã›ã‚“。 + +extractingRoot.jarFailed=JARファイル[{0}]ã®æŠ½å‡ºã«å¤±æ•—ã—ã¾ã—㟠+extractingRoot.targetFailed=JAR ファイルを展開ã™ã‚‹ãŸã‚ã®ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª [{0}] を作æˆã§ãã¾ã›ã‚“。 + +fileResource.getCanonicalPathFail=リソース [{0}] ã®æ­£è¦åŒ–パスをå–å¾—ã§ãã¾ã›ã‚“ +fileResource.getCreationFail=リソース [{0}] ã®ä½œæˆæ™‚間を特定ã§ãã¾ã›ã‚“ +fileResource.getUrlFail=リソース [{0}] ã® URL ã‚’å–å¾—ã§ãã¾ã›ã‚“。 + +fileResourceSet.notFile=基本パスãŠã‚ˆã³å†…部パスã§æŒ‡å®šã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ« [{0}]{1}[{2}] ãŒã‚ã‚Šã¾ã›ã‚“。 + +jarResource.getInputStreamFail=JAR ファイル [{1}] ã®ãƒªã‚½ãƒ¼ã‚¹ [{0}] ã®å…¥åŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’å–å¾—ã§ãã¾ã›ã‚“ + +jarResourceRoot.invalidWebAppPath=ã“ã®ãƒªã‚½ãƒ¼ã‚¹ã¯å¸¸ã«ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’å‚ç…§ã™ã‚‹ãŸã‚ã€æŒ‡å®šã•ã‚ŒãŸwebAppPathã¯/ã§çµ‚了ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ãŒã€æŒ‡å®šã•ã‚ŒãŸwebAppPath㯠[{0}] ã§ã™ + +jarWarResourceSet.codingError=コーディングエラー + +standardRoot.checkStateNotStarted=リソースã¯ã€ç¾åœ¨èµ·å‹•ã•ã‚Œã¦ã„ãªã„å ´åˆã¯ã‚¢ã‚¯ã‚»ã‚¹ã§ããªã„å ´åˆãŒã‚ã‚Šã¾ã™ +standardRoot.createInvalidFile=[{0}]ã‹ã‚‰WebResourceSetを作æˆã§ãã¾ã›ã‚“ +standardRoot.createUnknownType=未知ã®ã‚¯ãƒ©ã‚¹ [{0}] ã® WebResourceSet を作æˆã§ãã¾ã›ã‚“ +standardRoot.invalidPath=無効ãªãƒªã‚½ãƒ¼ã‚¹ãƒ‘ス [{0}] +standardRoot.invalidPathNormal=リソースパス [{0}] ã¯æœ‰åŠ¹ã§ã¯ãªã„ [{1}] ã«æ­£è¦åŒ–ã•ã‚Œã¦ã„ã¾ã™ã€‚ +standardRoot.lockedFile=Webアプリケーション[{0}]ã¯ã€æ¬¡ã®ã‚¹ã‚¿ãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹ã«ã‚ˆã£ã¦é–‹ã‹ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«[{1}]ã‚’é–‰ã˜ã‚‹ã“ã¨ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +standardRoot.noContext=ã“ã® WebResourceRoot ã«ã¯Context ãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +standardRoot.startInvalidMain=指定ã•ã‚ŒãŸãƒ¡ã‚¤ãƒ³ãƒªã‚½ãƒ¼ã‚¹ã‚»ãƒƒãƒˆ [{0}] ã¯ã€ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã¾ãŸã¯ war ファイルã§ã¯ãªã„ã‹ã€èª­ã¿å–ã‚Šå¯èƒ½ã§ã¯ã‚ã‚Šã¾ã›ã‚“ (存在ã—ãªã„ã‹ã€ã‚¢ã‚¯ã‚»ã‚¹è¨±å¯ãŒã‚ã‚Šã¾ã›ã‚“) +standardRoot.unsupportedProtocol=URLプロトコル[{0}]ã¯ã“ã®Webリソース実装ã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 diff --git a/java/org/apache/catalina/webresources/LocalStrings_ko.properties b/java/org/apache/catalina/webresources/LocalStrings_ko.properties new file mode 100644 index 0000000..eb2a113 --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_ko.properties @@ -0,0 +1,61 @@ +# 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. + +abstractArchiveResourceSet.setReadOnlyFalse=JAR 파ì¼ë“¤ì— 기반한 것들과 ê°™ì€ ì•„ì¹´ì´ë¸Œ 기반 WebResourceSet들ì€, ì½ê¸° 전용으로 하드코드ë˜ì–´ 있으며, ì½ê¸° ë° ì“°ê¸° 용으로 ì„¤ì •ë  ìˆ˜ 없습니다. + +abstractFileResourceSet.canonicalfileCheckFailed=경로 [{1}]ì— ìœ„ì¹˜í•œ 웹 애플리케ì´ì…˜ [{0}]ì„(를) 위한 리소스가, 표준 경로 [{2}]와(ê³¼) ì¼ì¹˜í•˜ì§€ ì•Šì•„ì„œ, 로드ë˜ì§€ 못했습니다. 심벌릭 ë§í¬ ì‚¬ìš©ì´ ì›ì¸ 중 í•˜ë‚˜ì¼ ìˆ˜ 있습니다. + +abstractResource.getContentFail=[{0}]ì„(를) ë°”ì´íŠ¸ ë°°ì—´ë¡œ 반환할 수 없습니다. +abstractResource.getContentTooLarge=ë¦¬ì†ŒìŠ¤ì˜ í¬ê¸°ê°€ [{1}] ë°”ì´íŠ¸ë¡œì„œ, ì´ëŠ” ë°”ì´íŠ¸ ë°°ì—´ 최대 í¬ê¸°ë³´ë‹¤ í¬ê¸° 때문ì—, [{0}]ì„(를) ë°”ì´íŠ¸ 배열로서 반환할 수 없습니다. + +abstractResourceSet.checkPath=ìš”ì²­ëœ ê²½ë¡œ [{0}]ì€(는) 유효하지 않습니다. 반드시 "/"ë¡œ 시작해야 합니다. + +cache.addFail=[{0}]ì— ìœ„ì¹˜í•œ 리소스를 웹 애플리케ì´ì…˜ [{1}]ì„(를) 위한 ìºì‹œì— 추가할 수 없습니다. 왜ëƒí•˜ë©´ ë§Œë£Œëœ ìºì‹œ ì—”íŠ¸ë¦¬ë“¤ì„ ì—†ì• ë²„ë¦° ì´í›„ì—ë„ ì—¬ìœ  ê³µê°„ì´ ì¶©ë¶„í•˜ì§€ 않기 때문입니다. ìºì‹œì˜ 최대 í¬ê¸°ë¥¼ ì¦ê°€ì‹œí‚¤ëŠ” ê²ƒì„ ê³ ë ¤í•´ 보십시오. +cache.backgroundEvictFail=백그ë¼ìš´ë“œ ìºì‹œ 퇴거 (cache eviction) 프로세스가, 컨í…스트 [{1}]ì„(를) 위한 ìºì‹œì˜ [{0}] í¼ì„¼íŠ¸ë¥¼ 해제시킬 수 없었습니다. ìºì‹œì˜ 최대 í¬ê¸°ë¥¼ ì¦ê°€ì‹œí‚¬ ê²ƒì„ ê³ ë ¤í•´ 보십시오. ìºì‹œ 퇴거 ìž‘ì—… ì´í›„, 대략 [{2}] KiBì˜ ë°ì´í„°ê°€ ìºì‹œì— 남아 있습니다. +cache.objectMaxSizeTooBig=objectMaxSize를 위한 ê°’ [{0}] KiBì´, maxSize/20ì¸ ìµœëŒ€í•œê³„ê°’ 보다 커서, [{1}] KiBë¡œ 줄여졌습니다. +cache.objectMaxSizeTooBigBytes=[{0}] KiB를 ìºì‹œí•˜ê¸° 위해, 최대 ê°ì²´ í¬ê¸°ë¡œì„œ ì§€ì •ëœ ê°’ì´ Integer.MAX_VALUE ë°”ì´íŠ¸ë³´ë‹¤ í°ë°, Integer.MAX_VALUE는 ìºì‹œë  수 있는 최대 í¬ê¸°ìž…니다. 한계 ê°’ì„ Integer.MAX_VALUE ë°”ì´íŠ¸ë¡œ 설정하겠습니다. + +cachedResource.invalidURL=URL [{0}]ì´(ê°€) 유효하지 않기 ë•Œë¬¸ì— CachedResourceURLStreamHandler ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•  수 없습니다. + +classpathUrlStreamHandler.notFound=쓰레드 컨í…스트 í´ëž˜ìŠ¤ë¡œë” ë˜ëŠ” 현재 í´ëž˜ìŠ¤ì˜ í´ëž˜ìŠ¤ë¡œë”를 사용하여, 리소스 [{0}]ì„(를) 로드할 수 없습니다. + +dirResourceSet.manifestFail=[{0}](으)로부터 manifest를 ì½ì§€ 못했습니다. +dirResourceSet.notDirectory=base와 internal path [{0}]{1}[{2}](으)ë¡œ ì§€ì •ëœ ë””ë ‰í† ë¦¬ê°€ 존재하지 않습니다. +dirResourceSet.writeNpe=ìž…ë ¥ ìŠ¤íŠ¸ë¦¼ì´ ë„ì¼ ìˆ˜ëŠ” 없습니다. + +extractingRoot.jarFailed=JAR íŒŒì¼ [{0}]ì„(를) 추출하지 못했습니다. +extractingRoot.targetFailed=JAR 파ì¼ë“¤ì˜ ì••ì¶•ì„ í’€ê¸° 위한 디렉토리 [{0}]ì„(를) ìƒì„±í•  수 없습니다. + +fileResource.getCanonicalPathFail=리소스 [{0}]ì— ëŒ€í•œ canonical 경로를 ê²°ì •í•  수 없습니다. +fileResource.getCreationFail=리소스 [{0}]ì˜ ìƒì„± ì‹œê°„ì„ ê²°ì •í•  수 없습니다. +fileResource.getUrlFail=리소스 [{0}]ì„(를) 위한 URLì„ ê²°ì •í•  수 없습니다. + +fileResourceSet.notFile=base와 내부 경로 [{0}]{1}[{2}]ì— ì˜í•´ ì§€ì •ëœ íŒŒì¼ì´ 존재하지 않습니다. + +jarResource.getInputStreamFail=JAR [{1}] ë‚´ì˜ ë¦¬ì†ŒìŠ¤ [{0}]ì„(를) 위한 InputStreamì„ ì–»ì„ ìˆ˜ 없습니다. + +jarResourceRoot.invalidWebAppPath=ì´ ë¦¬ì†ŒìŠ¤ëŠ” 언제나 디렉토리를 가리켜서, ì œê³µëœ webAppPathê°€ 반드시 ''/'' ë¡œ ë나야 하지만, ì œê³µëœ webAppPath는 [{0}]ì´ì—ˆìŠµë‹ˆë‹¤. + +jarWarResourceSet.codingError=코딩 오류 + +standardRoot.checkStateNotStarted=현재 시작ë˜ì–´ 있는 ìƒíƒœê°€ 아니ë¼ë©´, ë¦¬ì†ŒìŠ¤ë“¤ì€ ì ‘ê·¼ë  ìˆ˜ 없습니다. +standardRoot.createInvalidFile=[{0}](으)로부터 WebResourceSetì„ ìƒì„±í•  수 없습니다. +standardRoot.createUnknownType=ì•Œ 수 없는 타입 [{0}]ì˜ WebResourceSetì„ ìƒì„±í•  수 없습니다. +standardRoot.invalidPath=리소스 경로 [{0}]ì€(는) 유효하지 않습니다. +standardRoot.invalidPathNormal=리소스 경로 [{0}]ì´(ê°€) [{1}](으)ë¡œ 정규화ë˜ì–´ 있는ë°, ì´ëŠ” 유효하지 않습니다. +standardRoot.lockedFile=웹 애플리케ì´ì…˜ [{0}]ì´(ê°€) íŒŒì¼ [{1}]ì„(를) 닫지 못했습니다. 해당 파ì¼ì€ 다ìŒê³¼ ê°™ì€ ìŠ¤íƒ íŠ¸ë ˆì´ìŠ¤ ë‚´ì—ì„œ 열렸었습니다. +standardRoot.noContext=컨í…스트가 ì´ WebResourceRoot를 위해 설정ë˜ì§€ 않았습니다. +standardRoot.startInvalidMain=ì§€ì •ëœ ì£¼ìš” 리소스셋 [{0}]ì€(는) 유효하지 않습니다. +standardRoot.unsupportedProtocol=URL 프로토콜 [{0}]ì€(는), ì´ ì›¹ 리소스 êµ¬í˜„ì— ì˜í•´ 지ì›ë˜ì§€ 않습니다. diff --git a/java/org/apache/catalina/webresources/LocalStrings_pt_BR.properties b/java/org/apache/catalina/webresources/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..6e84bee --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +jarWarResourceSet.codingError=Erro de codificação diff --git a/java/org/apache/catalina/webresources/LocalStrings_ru.properties b/java/org/apache/catalina/webresources/LocalStrings_ru.properties new file mode 100644 index 0000000..190dd83 --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_ru.properties @@ -0,0 +1,18 @@ +# 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. + +extractingRoot.targetFailed=Ошибка ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð´Ð¸Ñ€ÐµÐºÑ‚Ð¾Ñ€Ð¸Ð¸ [{0}] Ð´Ð»Ñ Ñ€Ð°Ñпакованных JAR файлов + +standardRoot.createUnknownType=Ðевозможно Ñоздать WebResourceSet неизвеÑтного типа [{0}] diff --git a/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties b/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..f1aaabd --- /dev/null +++ b/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties @@ -0,0 +1,61 @@ +# 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. + +abstractArchiveResourceSet.setReadOnlyFalse=基于存档的WebResourceSets 如基于jarçš„WebResourceSets 硬编ç ä¸ºåªè¯»ï¼Œå¹¶ä¸”ä¸èƒ½é…置为读写 + +abstractFileResourceSet.canonicalfileCheckFailed=路径[{1}]处的web应用程åº[{0}]的资æºæœªåŠ è½½,因为规范链接[{2}]ä¸åŒ¹é….使用符å·é“¾æŽ¥æ˜¯ä¸€ä¸ªå¯èƒ½çš„原因. + +abstractResource.getContentFail=无法把[{0}]作为byte数组返回 +abstractResource.getContentTooLarge=无法返回[{0}]作为字节数组,因为资æºçš„大å°[[1]]ä¸ªå­—èŠ‚å¤§äºŽå­—èŠ‚æ•°ç»„çš„æœ€å¤§å¤§å° + +abstractResourceSet.checkPath=请求的路径[{0}]无效。必须以“/â€å¼€å¤´ã€‚ + +cache.addFail=无法将ä½äºŽ[{0}]的资æºæ·»åŠ åˆ°Web应用程åº[{1}]的缓存中,因为在清除过期缓存æ¡ç›®åŽå¯ç”¨ç©ºé—´ä»ä¸è¶³ - 请考虑增加缓存的最大空间。 +cache.backgroundEvictFail=åŽå°ç¼“存收回进程无法释放上下文[{1}]的缓存的[{0}]%-请考虑增加缓存的最大大å°ã€‚在é€å‡ºä¹‹åŽï¼Œç¼“存中大约ä¿ç•™äº†[{2}] KiBçš„æ•°æ®ã€‚ +cache.objectMaxSizeTooBig=objectMaxSize的值[{0}] KiB大于maxSize/20çš„é™åˆ¶ï¼Œå› æ­¤å·²ç¼©å‡ä¸º[{1}] KiB +cache.objectMaxSizeTooBigBytes=为è¦ç¼“存的最大对象大å°[{0}] KiB指定的值大于Integer.MAX_VALUE字节,åŽè€…是å¯ä»¥ç¼“存的最大大å°ã€‚该é™åˆ¶å°†è®¾ç½®ä¸ºInteger.MAX_VALUE字节。 + +cachedResource.invalidURL=无法创建CachedResourceURLStreamHandler实例,因为URL[{0}]畸形 + +classpathUrlStreamHandler.notFound=无法使用线程上下文类加载器或当å‰ç±»çš„类加载器加载资æº[{0}] + +dirResourceSet.manifestFail=从[{0}]读å–manifest失败 +dirResourceSet.notDirectory=基本和内部路径[{0}] {1} [{2}]指定的目录ä¸å­˜åœ¨ã€‚ +dirResourceSet.writeNpe=输入æµä¸èƒ½ä¸ºç©º + +extractingRoot.jarFailed=解压JAR文件[{0}]失败 +extractingRoot.targetFailed=无法为æå–çš„ JAR 文件创建目录 [{0}] + +fileResource.getCanonicalPathFail=ä¸èƒ½åˆ¤æ–­èµ„æºçš„标准路径[{0}] +fileResource.getCreationFail=æ— æ³•ç¡®å®šèµ„æº [{0}] 的创建时间 +fileResource.getUrlFail=ä¸èƒ½å†³å®šä¸€ä¸ªurl 为资æº[{0}] + +fileResourceSet.notFile=由基路径和内部路径[{0}]{1}[{2}]指定的文件ä¸å­˜åœ¨ã€‚ + +jarResource.getInputStreamFail=无法获å–JAR[{1}]中的资æºæ–‡ä»¶[{0}]的一个InputStream + +jarResourceRoot.invalidWebAppPath=此资æºæ€»æ˜¯å¼•ç”¨ä¸€ä¸ªç›®å½•ï¼Œå› æ­¤æ供的webAppPath必须以/结尾,但æ供的webAppPath是[{0}] + +jarWarResourceSet.codingError=ç¼–ç é”™è¯¯ + +standardRoot.checkStateNotStarted=如果当å‰æœªå¯åŠ¨èµ„æºï¼Œåˆ™å¯èƒ½æ— æ³•è®¿é—®è¿™äº›èµ„æº +standardRoot.createInvalidFile=无法从[{0}]创建WebResourceSet。 +standardRoot.createUnknownType=无法为未知类型[{0}]创建WebResourceSet。 +standardRoot.invalidPath=资æºè·¯å¾„[{0}]无效 +standardRoot.invalidPathNormal=资æºè·¯å¾„[{0}]已规范化为无效的[{1}] +standardRoot.lockedFile=Web应用程åº[{0}]无法关闭通过以下堆栈跟踪打开的文件[{1}] +standardRoot.noContext=尚未为WebResourceRooté…置上下文 +standardRoot.startInvalidMain=指定的主资æºé›† [{0}] 无效 +standardRoot.unsupportedProtocol=æ­¤web资æºå®žçŽ°ä¸æ”¯æŒURLåè®®[{0}] diff --git a/java/org/apache/catalina/webresources/StandardRoot.java b/java/org/apache/catalina/webresources/StandardRoot.java new file mode 100644 index 0000000..2cb98e2 --- /dev/null +++ b/java/org/apache/catalina/webresources/StandardRoot.java @@ -0,0 +1,860 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.ObjectName; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.TrackedWebResource; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.util.LifecycleMBeanBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.http.RequestUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + *

    + * Provides the resources implementation for a web application. The {@link org.apache.catalina.Lifecycle} of this class + * should be aligned with that of the associated {@link Context}. + *

    + *

    + * This implementation assumes that the base attribute supplied to + * {@link StandardRoot#createWebResourceSet( org.apache.catalina.WebResourceRoot.ResourceSetType, String, String, String, String)} + * represents the absolute path to a file. + *

    + */ +public class StandardRoot extends LifecycleMBeanBase implements WebResourceRoot { + + private static final Log log = LogFactory.getLog(StandardRoot.class); + protected static final StringManager sm = StringManager.getManager(StandardRoot.class); + + private Context context; + private boolean allowLinking = false; + private final List preResources = new ArrayList<>(); + private WebResourceSet main; + private final List classResources = new ArrayList<>(); + private final List jarResources = new ArrayList<>(); + private final List postResources = new ArrayList<>(); + + private final Cache cache = new Cache(this); + private boolean cachingAllowed = true; + private ObjectName cacheJmxName = null; + + private boolean trackLockedFiles = false; + private final Set trackedResources = ConcurrentHashMap.newKeySet(); + + private ArchiveIndexStrategy archiveIndexStrategy = ArchiveIndexStrategy.SIMPLE; + + // Constructs to make iteration over all WebResourceSets simpler + private final List mainResources = new ArrayList<>(); + private final List> allResources = new ArrayList<>(); + { + allResources.add(preResources); + allResources.add(mainResources); + allResources.add(classResources); + allResources.add(jarResources); + allResources.add(postResources); + } + + + /** + * Creates a new standard implementation of {@link WebResourceRoot}. A no argument constructor is required for this + * to work with the digester. {@link #setContext(Context)} must be called before this component is initialized. + */ + public StandardRoot() { + // NO-OP + } + + public StandardRoot(Context context) { + this.context = context; + } + + @Override + public String[] list(String path) { + return list(path, true); + } + + private String[] list(String path, boolean validate) { + if (validate) { + path = validate(path); + } + + // Set because we don't want duplicates + // LinkedHashSet to retain the order. It is the order of the + // WebResourceSet that matters but it is simpler to retain the order + // over all of the JARs. + HashSet result = new LinkedHashSet<>(); + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + if (!webResourceSet.getClassLoaderOnly()) { + String[] entries = webResourceSet.list(path); + result.addAll(Arrays.asList(entries)); + } + } + } + return result.toArray(new String[0]); + } + + + @Override + public Set listWebAppPaths(String path) { + path = validate(path); + + // Set because we don't want duplicates + Set result = new HashSet<>(); + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + if (!webResourceSet.getClassLoaderOnly()) { + result.addAll(webResourceSet.listWebAppPaths(path)); + } + } + } + if (result.size() == 0) { + return null; + } + return result; + } + + @Override + public boolean mkdir(String path) { + path = validate(path); + + if (preResourceExists(path)) { + return false; + } + + boolean mkdirResult = main.mkdir(path); + + if (mkdirResult && isCachingAllowed()) { + // Remove the entry from the cache so the new directory is visible + cache.removeCacheEntry(path); + } + return mkdirResult; + } + + @Override + public boolean write(String path, InputStream is, boolean overwrite) { + path = validate(path); + + if (!overwrite && preResourceExists(path)) { + return false; + } + + boolean writeResult = main.write(path, is, overwrite); + + if (writeResult && isCachingAllowed()) { + // Remove the entry from the cache so the new resource is visible + cache.removeCacheEntry(path); + } + + return writeResult; + } + + private boolean preResourceExists(String path) { + for (WebResourceSet webResourceSet : preResources) { + WebResource webResource = webResourceSet.getResource(path); + if (webResource.exists()) { + return true; + } + } + return false; + } + + @Override + public WebResource getResource(String path) { + return getResource(path, true, false); + } + + protected WebResource getResource(String path, boolean validate, boolean useClassLoaderResources) { + if (validate) { + path = validate(path); + } + + if (isCachingAllowed()) { + return cache.getResource(path, useClassLoaderResources); + } else { + return getResourceInternal(path, useClassLoaderResources); + } + } + + + @Override + public WebResource getClassLoaderResource(String path) { + return getResource("/WEB-INF/classes" + path, true, true); + } + + + @Override + public WebResource[] getClassLoaderResources(String path) { + return getResources("/WEB-INF/classes" + path, true); + } + + + /** + * Ensures that this object is in a valid state to serve resources, checks that the path is a String that starts + * with '/' and checks that the path can be normalized without stepping outside of the root. + * + * @param path The path to validate + * + * @return the normalized path + */ + private String validate(String path) { + if (!getState().isAvailable()) { + throw new IllegalStateException(sm.getString("standardRoot.checkStateNotStarted")); + } + + if (path == null || path.length() == 0 || !path.startsWith("/")) { + throw new IllegalArgumentException(sm.getString("standardRoot.invalidPath", path)); + } + + String result; + if (File.separatorChar == '\\') { + // On Windows '\\' is a separator so in case a Windows style + // separator has managed to make it into the path, replace it. + result = RequestUtil.normalize(path, true); + } else { + // On UNIX and similar systems, '\\' is a valid file name so do not + // convert it to '/' + result = RequestUtil.normalize(path, false); + } + if (result == null || result.length() == 0 || !result.startsWith("/")) { + throw new IllegalArgumentException(sm.getString("standardRoot.invalidPathNormal", path, result)); + } + + return result; + } + + protected final WebResource getResourceInternal(String path, boolean useClassLoaderResources) { + WebResource result = null; + WebResource virtual = null; + WebResource mainEmpty = null; + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() || + useClassLoaderResources && !webResourceSet.getStaticOnly()) { + result = webResourceSet.getResource(path); + if (result.exists()) { + return result; + } + if (virtual == null) { + if (result.isVirtual()) { + virtual = result; + } else if (main.equals(webResourceSet)) { + mainEmpty = result; + } + } + } + } + } + + // Use the first virtual result if no real result was found + if (virtual != null) { + return virtual; + } + + // Default is empty resource in main resources + return mainEmpty; + } + + @Override + public WebResource[] getResources(String path) { + return getResources(path, false); + } + + private WebResource[] getResources(String path, boolean useClassLoaderResources) { + path = validate(path); + + if (isCachingAllowed()) { + return cache.getResources(path, useClassLoaderResources); + } else { + return getResourcesInternal(path, useClassLoaderResources); + } + } + + protected WebResource[] getResourcesInternal(String path, boolean useClassLoaderResources) { + List result = new ArrayList<>(); + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + if (useClassLoaderResources || !webResourceSet.getClassLoaderOnly()) { + WebResource webResource = webResourceSet.getResource(path); + if (webResource.exists()) { + result.add(webResource); + } + } + } + } + + if (result.size() == 0) { + result.add(main.getResource(path)); + } + + return result.toArray(new WebResource[0]); + } + + @Override + public WebResource[] listResources(String path) { + return listResources(path, true); + } + + protected WebResource[] listResources(String path, boolean validate) { + if (validate) { + path = validate(path); + } + + String[] resources = list(path, false); + WebResource[] result = new WebResource[resources.length]; + for (int i = 0; i < resources.length; i++) { + if (path.charAt(path.length() - 1) == '/') { + result[i] = getResource(path + resources[i], false, false); + } else { + result[i] = getResource(path + '/' + resources[i], false, false); + } + } + return result; + } + + // TODO: Should the createWebResourceSet() methods be removed to some + // utility class for file system based resource sets? + + @Override + public void createWebResourceSet(ResourceSetType type, String webAppMount, URL url, String internalPath) { + BaseLocation baseLocation = new BaseLocation(url); + createWebResourceSet(type, webAppMount, baseLocation.getBasePath(), baseLocation.getArchivePath(), + internalPath); + } + + @Override + public void createWebResourceSet(ResourceSetType type, String webAppMount, String base, String archivePath, + String internalPath) { + List resourceList; + WebResourceSet resourceSet; + + switch (type) { + case PRE: + resourceList = preResources; + break; + case CLASSES_JAR: + resourceList = classResources; + break; + case RESOURCE_JAR: + resourceList = jarResources; + break; + case POST: + resourceList = postResources; + break; + default: + throw new IllegalArgumentException(sm.getString("standardRoot.createUnknownType", type)); + } + + // This implementation assumes that the base for all resources will be a + // file. + File file = new File(base); + + if (file.isFile()) { + if (archivePath != null) { + // Must be a JAR nested inside a WAR if archivePath is non-null + resourceSet = new JarWarResourceSet(this, webAppMount, base, archivePath, internalPath); + } else if (file.getName().toLowerCase(Locale.ENGLISH).endsWith(".jar")) { + resourceSet = new JarResourceSet(this, webAppMount, base, internalPath); + } else { + resourceSet = new FileResourceSet(this, webAppMount, base, internalPath); + } + } else if (file.isDirectory()) { + resourceSet = new DirResourceSet(this, webAppMount, base, internalPath); + } else { + throw new IllegalArgumentException(sm.getString("standardRoot.createInvalidFile", file)); + } + + if (type.equals(ResourceSetType.CLASSES_JAR)) { + resourceSet.setClassLoaderOnly(true); + } else if (type.equals(ResourceSetType.RESOURCE_JAR)) { + resourceSet.setStaticOnly(true); + } + + resourceList.add(resourceSet); + } + + @Override + public void addPreResources(WebResourceSet webResourceSet) { + webResourceSet.setRoot(this); + preResources.add(webResourceSet); + } + + @Override + public WebResourceSet[] getPreResources() { + return preResources.toArray(new WebResourceSet[0]); + } + + @Override + public void addJarResources(WebResourceSet webResourceSet) { + webResourceSet.setRoot(this); + jarResources.add(webResourceSet); + } + + @Override + public WebResourceSet[] getJarResources() { + return jarResources.toArray(new WebResourceSet[0]); + } + + @Override + public void addPostResources(WebResourceSet webResourceSet) { + webResourceSet.setRoot(this); + postResources.add(webResourceSet); + } + + @Override + public WebResourceSet[] getPostResources() { + return postResources.toArray(new WebResourceSet[0]); + } + + protected WebResourceSet[] getClassResources() { + return classResources.toArray(new WebResourceSet[0]); + } + + protected void addClassResources(WebResourceSet webResourceSet) { + webResourceSet.setRoot(this); + classResources.add(webResourceSet); + } + + @Override + public void setAllowLinking(boolean allowLinking) { + if (this.allowLinking != allowLinking && cachingAllowed) { + // If allow linking changes, invalidate the cache. + cache.clear(); + } + this.allowLinking = allowLinking; + } + + @Override + public boolean getAllowLinking() { + return allowLinking; + } + + @Override + public void setCachingAllowed(boolean cachingAllowed) { + this.cachingAllowed = cachingAllowed; + if (!cachingAllowed) { + cache.clear(); + } + } + + @Override + public boolean isCachingAllowed() { + return cachingAllowed; + } + + + @Override + public CacheStrategy getCacheStrategy() { + return cache.getCacheStrategy(); + } + + @Override + public void setCacheStrategy(CacheStrategy strategy) { + cache.setCacheStrategy(strategy); + } + + @Override + public long getCacheTtl() { + return cache.getTtl(); + } + + @Override + public void setCacheTtl(long cacheTtl) { + cache.setTtl(cacheTtl); + } + + @Override + public long getCacheMaxSize() { + return cache.getMaxSize(); + } + + @Override + public void setCacheMaxSize(long cacheMaxSize) { + cache.setMaxSize(cacheMaxSize); + } + + @Override + public void setCacheObjectMaxSize(int cacheObjectMaxSize) { + cache.setObjectMaxSize(cacheObjectMaxSize); + // Don't enforce the limit when not running as attributes may get set in + // any order. + if (getState().isAvailable()) { + cache.enforceObjectMaxSizeLimit(); + } + } + + @Override + public int getCacheObjectMaxSize() { + return cache.getObjectMaxSize(); + } + + @Override + public void setTrackLockedFiles(boolean trackLockedFiles) { + this.trackLockedFiles = trackLockedFiles; + if (!trackLockedFiles) { + trackedResources.clear(); + } + } + + @Override + public boolean getTrackLockedFiles() { + return trackLockedFiles; + } + + @Override + public void setArchiveIndexStrategy(String archiveIndexStrategy) { + this.archiveIndexStrategy = ArchiveIndexStrategy.valueOf(archiveIndexStrategy.toUpperCase(Locale.ENGLISH)); + } + + @Override + public String getArchiveIndexStrategy() { + return this.archiveIndexStrategy.name(); + } + + @Override + public ArchiveIndexStrategy getArchiveIndexStrategyEnum() { + return this.archiveIndexStrategy; + } + + public List getTrackedResources() { + List result = new ArrayList<>(trackedResources.size()); + for (TrackedWebResource resource : trackedResources) { + result.add(resource.toString()); + } + return result; + } + + @Override + public Context getContext() { + return context; + } + + @Override + public void setContext(Context context) { + this.context = context; + } + + /** + * Class loader resources are handled by treating JARs in WEB-INF/lib as resource JARs (without the internal + * META-INF/resources/ prefix) mounted at WEB-INF/classes (rather than the web app root). This enables reuse of the + * resource handling plumbing. These resources are marked as class loader only so they are only used in the methods + * that are explicitly defined to return class loader resources. This prevents calls to + * getResource("/WEB-INF/classes") returning from one or more of the JAR files. + * + * @throws LifecycleException If an error occurs that should stop the web application from starting + */ + protected void processWebInfLib() throws LifecycleException { + WebResource[] possibleJars = listResources("/WEB-INF/lib", false); + + for (WebResource possibleJar : possibleJars) { + if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) { + createWebResourceSet(ResourceSetType.CLASSES_JAR, "/WEB-INF/classes", possibleJar.getURL(), "/"); + } + } + } + + /** + * For unit testing. + * + * @param main The main resources + */ + protected final void setMainResources(WebResourceSet main) { + this.main = main; + mainResources.clear(); + mainResources.add(main); + } + + + @Override + public void backgroundProcess() { + cache.backgroundProcess(); + gc(); + } + + + @Override + public void gc() { + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + webResourceSet.gc(); + } + } + } + + @Override + public void registerTrackedResource(TrackedWebResource trackedResource) { + trackedResources.add(trackedResource); + } + + + @Override + public void deregisterTrackedResource(TrackedWebResource trackedResource) { + trackedResources.remove(trackedResource); + } + + + @Override + public List getBaseUrls() { + List result = new ArrayList<>(); + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + if (!webResourceSet.getClassLoaderOnly()) { + URL url = webResourceSet.getBaseUrl(); + if (url != null) { + result.add(url); + } + } + } + } + return result; + } + + + /* + * Returns true if and only if all the resources for this web application are provided via a packed WAR file. It is + * used to optimise cache validation in this case on the basis that the WAR file will not change. + */ + protected boolean isPackedWarFile() { + return main instanceof WarResourceSet && preResources.isEmpty() && postResources.isEmpty(); + } + + + // ----------------------------------------------------------- JMX Lifecycle + @Override + protected String getDomainInternal() { + return context.getDomain(); + } + + @Override + protected String getObjectNameKeyProperties() { + StringBuilder keyProperties = new StringBuilder("type=WebResourceRoot"); + keyProperties.append(context.getMBeanKeyProperties()); + + return keyProperties.toString(); + } + + // --------------------------------------------------------------- Lifecycle + + @Override + protected void initInternal() throws LifecycleException { + super.initInternal(); + + if (context == null) { + throw new IllegalStateException(sm.getString("standardRoot.noContext")); + } + + cacheJmxName = register(cache, getObjectNameKeyProperties() + ",name=Cache"); + + registerURLStreamHandlerFactory(); + + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + webResourceSet.init(); + } + } + } + + protected void registerURLStreamHandlerFactory() { + if (!JreCompat.isGraalAvailable()) { + // Ensure support for jar:war:file:/ URLs will be available (required + // for resource JARs in packed WAR files). + TomcatURLStreamHandlerFactory.register(); + } + } + + @Override + protected void startInternal() throws LifecycleException { + mainResources.clear(); + + main = createMainResourceSet(); + + mainResources.add(main); + + for (List list : allResources) { + // Skip class resources since they are started below + if (list != classResources) { + for (WebResourceSet webResourceSet : list) { + webResourceSet.start(); + } + } + } + + // This has to be called after the other resources have been started + // else it won't find all the matching resources + processWebInfLib(); + // Need to start the newly found resources + for (WebResourceSet classResource : classResources) { + classResource.start(); + } + + cache.enforceObjectMaxSizeLimit(); + + setState(LifecycleState.STARTING); + } + + protected WebResourceSet createMainResourceSet() { + String docBase = context.getDocBase(); + + WebResourceSet mainResourceSet; + if (docBase == null) { + mainResourceSet = new EmptyResourceSet(this); + } else { + File f = new File(docBase); + if (!f.isAbsolute()) { + f = new File(((Host) context.getParent()).getAppBaseFile(), f.getPath()); + } + if (f.isDirectory()) { + mainResourceSet = new DirResourceSet(this, "/", f.getAbsolutePath(), "/"); + } else if (f.isFile() && docBase.endsWith(".war")) { + mainResourceSet = new WarResourceSet(this, "/", f.getAbsolutePath()); + } else { + throw new IllegalArgumentException(sm.getString("standardRoot.startInvalidMain", f.getAbsolutePath())); + } + } + + return mainResourceSet; + } + + @Override + protected void stopInternal() throws LifecycleException { + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + webResourceSet.stop(); + } + } + + if (main != null) { + main.destroy(); + } + mainResources.clear(); + + for (WebResourceSet webResourceSet : jarResources) { + webResourceSet.destroy(); + } + jarResources.clear(); + + for (WebResourceSet webResourceSet : classResources) { + webResourceSet.destroy(); + } + classResources.clear(); + + for (TrackedWebResource trackedResource : trackedResources) { + log.error(sm.getString("standardRoot.lockedFile", context.getName(), trackedResource.getName()), + trackedResource.getCreatedBy()); + try { + trackedResource.close(); + } catch (IOException e) { + // Ignore + } + } + cache.clear(); + + setState(LifecycleState.STOPPING); + } + + @Override + protected void destroyInternal() throws LifecycleException { + for (List list : allResources) { + for (WebResourceSet webResourceSet : list) { + webResourceSet.destroy(); + } + } + + unregister(cacheJmxName); + + super.destroyInternal(); + } + + + // Unit tests need to access this class + static class BaseLocation { + + private final String basePath; + private final String archivePath; + + BaseLocation(URL url) { + File f = null; + + if ("jar".equals(url.getProtocol()) || "war".equals(url.getProtocol())) { + String jarUrl = url.toString(); + int endOfFileUrl = -1; + if ("jar".equals(url.getProtocol())) { + endOfFileUrl = jarUrl.indexOf("!/"); + } else { + endOfFileUrl = jarUrl.indexOf(UriUtil.getWarSeparator()); + } + String fileUrl = jarUrl.substring(4, endOfFileUrl); + try { + f = new File(new URI(fileUrl)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + int startOfArchivePath = endOfFileUrl + 2; + if (jarUrl.length() > startOfArchivePath) { + archivePath = jarUrl.substring(startOfArchivePath); + } else { + archivePath = null; + } + } else if ("file".equals(url.getProtocol())) { + try { + f = new File(url.toURI()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + archivePath = null; + } else { + throw new IllegalArgumentException(sm.getString("standardRoot.unsupportedProtocol", url.getProtocol())); + } + + basePath = f.getAbsolutePath(); + } + + + String getBasePath() { + return basePath; + } + + + String getArchivePath() { + return archivePath; + } + } +} diff --git a/java/org/apache/catalina/webresources/TomcatJarInputStream.java b/java/org/apache/catalina/webresources/TomcatJarInputStream.java new file mode 100644 index 0000000..a05add0 --- /dev/null +++ b/java/org/apache/catalina/webresources/TomcatJarInputStream.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.webresources; + +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.zip.ZipEntry; + +/** + * The purpose of this sub-class is to obtain references to the JarEntry objects for META-INF/ and META-INF/MANIFEST.MF + * that are otherwise swallowed by the JarInputStream implementation. + */ +public class TomcatJarInputStream extends JarInputStream { + + private JarEntry metaInfEntry; + private JarEntry manifestEntry; + + + TomcatJarInputStream(InputStream in) throws IOException { + super(in); + } + + + @Override + protected ZipEntry createZipEntry(String name) { + ZipEntry ze = super.createZipEntry(name); + if (metaInfEntry == null && "META-INF/".equals(name)) { + metaInfEntry = (JarEntry) ze; + } else if (manifestEntry == null && "META-INF/MANIFESR.MF".equals(name)) { + manifestEntry = (JarEntry) ze; + } + return ze; + } + + + JarEntry getMetaInfEntry() { + return metaInfEntry; + } + + + JarEntry getManifestEntry() { + return manifestEntry; + } +} diff --git a/java/org/apache/catalina/webresources/TomcatURLStreamHandlerFactory.java b/java/org/apache/catalina/webresources/TomcatURLStreamHandlerFactory.java new file mode 100644 index 0000000..02a0842 --- /dev/null +++ b/java/org/apache/catalina/webresources/TomcatURLStreamHandlerFactory.java @@ -0,0 +1,164 @@ +/* + * 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.catalina.webresources; + +import java.net.URL; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.catalina.webresources.war.Handler; + +public class TomcatURLStreamHandlerFactory implements URLStreamHandlerFactory { + + private static final String WAR_PROTOCOL = "war"; + private static final String CLASSPATH_PROTOCOL = "classpath"; + + // Singleton instance + private static volatile TomcatURLStreamHandlerFactory instance = null; + + /** + * Obtain a reference to the singleton instance. It is recommended that callers check the value of + * {@link #isRegistered()} before using the returned instance. + * + * @return A reference to the singleton instance + */ + public static TomcatURLStreamHandlerFactory getInstance() { + getInstanceInternal(true); + return instance; + } + + + private static TomcatURLStreamHandlerFactory getInstanceInternal(boolean register) { + // Double checked locking. OK because instance is volatile. + if (instance == null) { + synchronized (TomcatURLStreamHandlerFactory.class) { + if (instance == null) { + instance = new TomcatURLStreamHandlerFactory(register); + } + } + } + return instance; + } + + + private final boolean registered; + + // List of factories for application defined stream handler factories. + private final List userFactories = new CopyOnWriteArrayList<>(); + + /** + * Register this factory with the JVM. May be called more than once. The implementation ensures that registration + * only occurs once. + * + * @return true if the factory is already registered with the JVM or was successfully registered as a + * result of this call. false if the factory was disabled prior to this call. + */ + public static boolean register() { + return getInstanceInternal(true).isRegistered(); + } + + + /** + * Prevent this this factory from registering with the JVM. May be called more than once. + * + * @return true if the factory is already disabled or was successfully disabled as a result of this + * call. false if the factory was already registered prior to this call. + */ + public static boolean disable() { + return !getInstanceInternal(false).isRegistered(); + } + + + /** + * Release references to any user provided factories that have been loaded using the provided class loader. Called + * during web application stop to prevent memory leaks. + * + * @param classLoader The class loader to release + */ + public static void release(ClassLoader classLoader) { + if (instance == null) { + return; + } + List factories = instance.userFactories; + for (URLStreamHandlerFactory factory : factories) { + ClassLoader factoryLoader = factory.getClass().getClassLoader(); + while (factoryLoader != null) { + if (classLoader.equals(factoryLoader)) { + // Implementation note: userFactories is a + // CopyOnWriteArrayList, so items are removed with + // List.remove() instead of usual Iterator.remove() + factories.remove(factory); + break; + } + factoryLoader = factoryLoader.getParent(); + } + } + } + + + private TomcatURLStreamHandlerFactory(boolean register) { + // Hide default constructor + // Singleton pattern to ensure there is only one instance of this + // factory + this.registered = register; + if (register) { + URL.setURLStreamHandlerFactory(this); + } + } + + + public boolean isRegistered() { + return registered; + } + + + /** + * Since the JVM only allows a single call to {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)} and + * Tomcat needs to register a handler, provide a mechanism to allow applications to register their own handlers. + * + * @param factory The user provided factory to add to the factories Tomcat has already registered + */ + public void addUserFactory(URLStreamHandlerFactory factory) { + userFactories.add(factory); + } + + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + + // Tomcat's handler always takes priority so applications can't override + // it. + if (WAR_PROTOCOL.equals(protocol)) { + return new Handler(); + } else if (CLASSPATH_PROTOCOL.equals(protocol)) { + return new ClasspathURLStreamHandler(); + } + + // Application handlers + for (URLStreamHandlerFactory factory : userFactories) { + URLStreamHandler handler = factory.createURLStreamHandler(protocol); + if (handler != null) { + return handler; + } + } + + // Unknown protocol + return null; + } +} diff --git a/java/org/apache/catalina/webresources/TrackedInputStream.java b/java/org/apache/catalina/webresources/TrackedInputStream.java new file mode 100644 index 0000000..dc461b3 --- /dev/null +++ b/java/org/apache/catalina/webresources/TrackedInputStream.java @@ -0,0 +1,112 @@ +/* + * 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.catalina.webresources; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.apache.catalina.TrackedWebResource; +import org.apache.catalina.WebResourceRoot; + +class TrackedInputStream extends InputStream implements TrackedWebResource { + + private final WebResourceRoot root; + private final String name; + private final InputStream is; + private final Exception creation; + + TrackedInputStream(WebResourceRoot root, String name, InputStream is) { + this.root = root; + this.name = name; + this.is = is; + this.creation = new Exception(); + + root.registerTrackedResource(this); + } + + @Override + public int read() throws IOException { + return is.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return is.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return is.read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return is.skip(n); + } + + @Override + public int available() throws IOException { + return is.available(); + } + + @Override + public void close() throws IOException { + root.deregisterTrackedResource(this); + is.close(); + } + + @Override + public synchronized void mark(int readlimit) { + is.mark(readlimit); + } + + @Override + public synchronized void reset() throws IOException { + is.reset(); + } + + @Override + public boolean markSupported() { + return is.markSupported(); + } + + @Override + public String getName() { + return name; + } + + @Override + public Exception getCreatedBy() { + return creation; + } + + @Override + public String toString() { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + sw.append('['); + sw.append(name); + sw.append(']'); + sw.append(System.lineSeparator()); + creation.printStackTrace(pw); + pw.flush(); + + return sw.toString(); + } +} diff --git a/java/org/apache/catalina/webresources/VirtualResource.java b/java/org/apache/catalina/webresources/VirtualResource.java new file mode 100644 index 0000000..24a33e1 --- /dev/null +++ b/java/org/apache/catalina/webresources/VirtualResource.java @@ -0,0 +1,44 @@ +/* + * 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.catalina.webresources; + +import org.apache.catalina.WebResourceRoot; + +public class VirtualResource extends EmptyResource { + + private final String name; + + public VirtualResource(WebResourceRoot root, String webAppPath, String name) { + super(root, webAppPath); + this.name = name; + } + + @Override + public boolean isVirtual() { + return true; + } + + @Override + public boolean isDirectory() { + return true; + } + + @Override + public String getName() { + return name; + } +} diff --git a/java/org/apache/catalina/webresources/WarResource.java b/java/org/apache/catalina/webresources/WarResource.java new file mode 100644 index 0000000..4672a39 --- /dev/null +++ b/java/org/apache/catalina/webresources/WarResource.java @@ -0,0 +1,43 @@ +/* + * 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.catalina.webresources; + +import java.util.jar.JarEntry; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.UriUtil; + +/** + * Represents a single resource (file or directory) that is located within a WAR. + */ +public class WarResource extends AbstractSingleArchiveResource { + + private static final Log log = LogFactory.getLog(WarResource.class); + + + public WarResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, String baseUrl, + JarEntry jarEntry) { + super(archiveResourceSet, webAppPath, "war:" + baseUrl + UriUtil.getWarSeparator(), jarEntry, baseUrl); + } + + + @Override + protected Log getLog() { + return log; + } +} diff --git a/java/org/apache/catalina/webresources/WarResourceSet.java b/java/org/apache/catalina/webresources/WarResourceSet.java new file mode 100644 index 0000000..c3d76d4 --- /dev/null +++ b/java/org/apache/catalina/webresources/WarResourceSet.java @@ -0,0 +1,57 @@ +/* + * 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.catalina.webresources; + +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; + +/** + * Represents a {@link org.apache.catalina.WebResourceSet} based on a WAR file. + */ +public class WarResourceSet extends AbstractSingleArchiveResourceSet { + + /** + * A no argument constructor is required for this to work with the digester. + */ + public WarResourceSet() { + } + + + /** + * Creates a new {@link org.apache.catalina.WebResourceSet} based on a WAR file. + * + * @param root The {@link WebResourceRoot} this new {@link org.apache.catalina.WebResourceSet} will be added + * to. + * @param webAppMount The path within the web application at which this {@link org.apache.catalina.WebResourceSet} + * will be mounted. + * @param base The absolute path to the WAR file on the file system from which the resources will be served. + * + * @throws IllegalArgumentException if the webAppMount is not valid (valid paths must start with '/') + */ + public WarResourceSet(WebResourceRoot root, String webAppMount, String base) throws IllegalArgumentException { + super(root, webAppMount, base, "/"); + } + + + @Override + protected WebResource createArchiveResource(JarEntry jarEntry, String webAppPath, Manifest manifest) { + return new WarResource(this, webAppPath, getBaseUrlString(), jarEntry); + } +} diff --git a/java/org/apache/catalina/webresources/mbeans-descriptors.xml b/java/org/apache/catalina/webresources/mbeans-descriptors.xml new file mode 100644 index 0000000..d38104e --- /dev/null +++ b/java/org/apache/catalina/webresources/mbeans-descriptors.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/webresources/war/Handler.java b/java/org/apache/catalina/webresources/war/Handler.java new file mode 100644 index 0000000..d2a09d1 --- /dev/null +++ b/java/org/apache/catalina/webresources/war/Handler.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.webresources.war; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +public class Handler extends URLStreamHandler { + + @Override + protected URLConnection openConnection(URL u) throws IOException { + return new WarURLConnection(u); + } + + @Override + protected void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, String path, + String query, String ref) { + if (path.startsWith("file:") && !path.startsWith("file:/")) { + // Work around a problem with the URLs in the security policy file. + // On Windows, the use of ${catalina.[home|base]} in the policy file + // results in codebase URLs of the form file:C:/... when they should + // be file:/C:/... + // For file: and jar: URLs, the JRE compensates for this. It does not + // compensate for this for war:file:... URLs. Therefore, we do that + // here + path = "file:/" + path.substring(5); + } + super.setURL(u, protocol, host, port, authority, userInfo, path, query, ref); + } +} diff --git a/java/org/apache/catalina/webresources/war/WarURLConnection.java b/java/org/apache/catalina/webresources/war/WarURLConnection.java new file mode 100644 index 0000000..8c1628e --- /dev/null +++ b/java/org/apache/catalina/webresources/war/WarURLConnection.java @@ -0,0 +1,79 @@ +/* + * 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.catalina.webresources.war; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.security.Permission; + +import org.apache.tomcat.util.buf.UriUtil; + + +public class WarURLConnection extends URLConnection { + + private final URLConnection wrappedJarUrlConnection; + private boolean connected; + + protected WarURLConnection(URL url) throws IOException { + super(url); + URL innerJarUrl = UriUtil.warToJar(url); + wrappedJarUrlConnection = innerJarUrl.openConnection(); + } + + + @Override + public void connect() throws IOException { + if (!connected) { + wrappedJarUrlConnection.connect(); + connected = true; + } + } + + + @Override + public InputStream getInputStream() throws IOException { + connect(); + return wrappedJarUrlConnection.getInputStream(); + } + + + @Override + public Permission getPermission() throws IOException { + return wrappedJarUrlConnection.getPermission(); + } + + + @Override + public long getLastModified() { + return wrappedJarUrlConnection.getLastModified(); + } + + + @Override + public int getContentLength() { + return wrappedJarUrlConnection.getContentLength(); + } + + + @Override + public long getContentLengthLong() { + return wrappedJarUrlConnection.getContentLengthLong(); + } + +} diff --git a/java/org/apache/coyote/AbstractProcessor.java b/java/org/apache/coyote/AbstractProcessor.java new file mode 100644 index 0000000..c61be06 --- /dev/null +++ b/java/org/apache/coyote/AbstractProcessor.java @@ -0,0 +1,1020 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConnection; + +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.parser.Host; +import org.apache.tomcat.util.log.UserDataHelper; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.DispatchType; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +/** + * Provides functionality and attributes common to all supported protocols (currently HTTP and AJP) for processing a + * single request/response. + */ +public abstract class AbstractProcessor extends AbstractProcessorLight implements ActionHook { + + private static final StringManager sm = StringManager.getManager(AbstractProcessor.class); + + // Used to avoid useless B2C conversion on the host name. + private char[] hostNameC = new char[0]; + + protected final Adapter adapter; + protected final AsyncStateMachine asyncStateMachine; + private volatile long asyncTimeout = -1; + /* + * Tracks the current async generation when a timeout is dispatched. In the time it takes for a container thread to + * be allocated and the timeout processing to start, it is possible that the application completes this generation + * of async processing and starts a new one. If the timeout is then processed against the new generation, response + * mix-up can occur. This field is used to ensure that any timeout event processed is for the current async + * generation. This prevents the response mix-up. + */ + private volatile long asyncTimeoutGeneration = 0; + protected final Request request; + protected final Response response; + protected volatile SocketWrapperBase socketWrapper = null; + protected volatile SSLSupport sslSupport; + + + /** + * Error state for the request/response currently being processed. + */ + private ErrorState errorState = ErrorState.NONE; + + protected final UserDataHelper userDataHelper; + + public AbstractProcessor(Adapter adapter) { + this(adapter, new Request(), new Response()); + } + + + protected AbstractProcessor(Adapter adapter, Request coyoteRequest, Response coyoteResponse) { + this.adapter = adapter; + asyncStateMachine = new AsyncStateMachine(this); + request = coyoteRequest; + response = coyoteResponse; + response.setHook(this); + request.setResponse(response); + request.setHook(this); + userDataHelper = new UserDataHelper(getLog()); + } + + + /** + * Update the current error state to the new error state if the new error state is more severe than the current + * error state. + * + * @param errorState The error status details + * @param t The error which occurred + */ + @SuppressWarnings("deprecation") + protected void setErrorState(ErrorState errorState, Throwable t) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractProcessor.setErrorState", errorState), t); + } + // Use the return value to avoid processing more than one async error + // in a single async cycle. + response.setError(); + boolean blockIo = this.errorState.isIoAllowed() && !errorState.isIoAllowed(); + this.errorState = this.errorState.getMostSevere(errorState); + // Don't change the status code for IOException since that is almost + // certainly a client disconnect in which case it is preferable to keep + // the original status code http://markmail.org/message/4cxpwmxhtgnrwh7n + if (response.getStatus() < 400 && !(t instanceof IOException)) { + response.setStatus(500); + } + if (t != null) { + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + } + if (blockIo && isAsync()) { + if (asyncStateMachine.asyncError()) { + processSocketEvent(SocketEvent.ERROR, true); + } + } + } + + + protected ErrorState getErrorState() { + return errorState; + } + + + @Override + public Request getRequest() { + return request; + } + + + /** + * Get the associated adapter. + * + * @return the associated adapter + */ + public Adapter getAdapter() { + return adapter; + } + + + /** + * Set the socket wrapper being used. + * + * @param socketWrapper The socket wrapper + */ + protected void setSocketWrapper(SocketWrapperBase socketWrapper) { + this.socketWrapper = socketWrapper; + } + + + /** + * @return the socket wrapper being used. + */ + protected final SocketWrapperBase getSocketWrapper() { + return socketWrapper; + } + + + @Override + public final void setSslSupport(SSLSupport sslSupport) { + this.sslSupport = sslSupport; + } + + + /** + * Provides a mechanism to trigger processing on a container thread. + * + * @param runnable The task representing the processing that needs to take place on a container thread + */ + protected void execute(Runnable runnable) { + SocketWrapperBase socketWrapper = this.socketWrapper; + if (socketWrapper == null) { + throw new RejectedExecutionException(sm.getString("abstractProcessor.noExecute")); + } else { + socketWrapper.execute(runnable); + } + } + + + @Override + public boolean isAsync() { + return asyncStateMachine.isAsync(); + } + + + @Override + public SocketState asyncPostProcess() { + return asyncStateMachine.asyncPostProcess(); + } + + + @Override + public final SocketState dispatch(SocketEvent status) throws IOException { + + if (status == SocketEvent.OPEN_WRITE && response.getWriteListener() != null) { + asyncStateMachine.asyncOperation(); + try { + if (flushBufferedWrite()) { + return SocketState.LONG; + } + } catch (IOException ioe) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractProcessor.asyncFail"), ioe); + } + status = SocketEvent.ERROR; + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe); + } + } else if (status == SocketEvent.OPEN_READ && request.getReadListener() != null) { + dispatchNonBlockingRead(); + } else if (status == SocketEvent.ERROR) { + // An I/O error occurred on a non-container thread. This includes: + // - read/write timeouts fired by the Poller in NIO + // - completion handler failures in NIO2 + + if (request.getAttribute(RequestDispatcher.ERROR_EXCEPTION) == null) { + // Because the error did not occur on a container thread the + // request's error attribute has not been set. If an exception + // is available from the socketWrapper, use it to set the + // request's error attribute here so it is visible to the error + // handling. + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, socketWrapper.getError()); + } + + if (request.getReadListener() != null || response.getWriteListener() != null) { + // The error occurred during non-blocking I/O. Set the correct + // state else the error handling will trigger an ISE. + asyncStateMachine.asyncOperation(); + } + } + + RequestInfo rp = request.getRequestProcessor(); + try { + rp.setStage(Constants.STAGE_SERVICE); + if (!getAdapter().asyncDispatch(request, response, status)) { + setErrorState(ErrorState.CLOSE_NOW, null); + } + } catch (InterruptedIOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + setErrorState(ErrorState.CLOSE_NOW, t); + getLog().error(sm.getString("http11processor.request.process"), t); + } + + rp.setStage(Constants.STAGE_ENDED); + + SocketState state; + + if (getErrorState().isError()) { + request.updateCounters(); + state = SocketState.CLOSED; + } else if (isAsync()) { + state = SocketState.LONG; + } else { + request.updateCounters(); + state = dispatchEndRequest(); + } + + if (getLog().isTraceEnabled()) { + getLog().trace("Socket: [" + socketWrapper + "], Status in: [" + status + "], State out: [" + state + "]"); + } + + return state; + } + + + protected void parseHost(MessageBytes valueMB) { + if (valueMB == null || valueMB.isNull()) { + populateHost(); + populatePort(); + return; + } else if (valueMB.getLength() == 0) { + // Empty Host header so set sever name to empty string + request.serverName().setString(""); + populatePort(); + return; + } + + ByteChunk valueBC = valueMB.getByteChunk(); + byte[] valueB = valueBC.getBytes(); + int valueL = valueBC.getLength(); + int valueS = valueBC.getStart(); + if (hostNameC.length < valueL) { + hostNameC = new char[valueL]; + } + + try { + // Validates the host name + int colonPos = Host.parse(valueMB); + + // Extract the port information first, if any + if (colonPos != -1) { + int port = 0; + for (int i = colonPos + 1; i < valueL; i++) { + char c = (char) valueB[i + valueS]; + if (c < '0' || c > '9') { + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, null); + return; + } + port = port * 10 + c - '0'; + } + request.setServerPort(port); + + // Only need to copy the host name up to the : + valueL = colonPos; + } + + // Extract the host name + for (int i = 0; i < valueL; i++) { + hostNameC[i] = (char) valueB[i + valueS]; + } + request.serverName().setChars(hostNameC, 0, valueL); + + } catch (IllegalArgumentException e) { + // IllegalArgumentException indicates that the host name is invalid + UserDataHelper.Mode logMode = userDataHelper.getNextMode(); + if (logMode != null) { + String message = sm.getString("abstractProcessor.hostInvalid", valueMB.toString()); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("abstractProcessor.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + getLog().info(message, e); + break; + case DEBUG: + getLog().debug(message, e); + } + } + + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, e); + } + } + + + /** + * Called when a host header is not present in the request (e.g. HTTP/1.0). It populates the server name with + * appropriate information. The source is expected to vary by protocol. + *

    + * The default implementation is a NO-OP. + */ + protected void populateHost() { + // NO-OP + } + + + /** + * Called when a host header is not present or is empty in the request (e.g. HTTP/1.0). It populates the server port + * with appropriate information. The source is expected to vary by protocol. + *

    + * The default implementation is a NO-OP. + */ + protected void populatePort() { + // NO-OP + } + + + @Override + public final void action(ActionCode actionCode, Object param) { + switch (actionCode) { + // 'Normal' servlet support + case COMMIT: { + if (!response.isCommitted()) { + try { + // Validate and write response headers + prepareResponse(); + } catch (IOException e) { + handleIOException(e); + } + } + break; + } + case CLOSE: { + action(ActionCode.COMMIT, null); + try { + finishResponse(); + } catch (IOException e) { + handleIOException(e); + } + break; + } + case ACK: { + ack((ContinueResponseTiming) param); + break; + } + case CLIENT_FLUSH: { + action(ActionCode.COMMIT, null); + try { + flush(); + } catch (IOException e) { + handleIOException(e); + response.setErrorException(e); + } + break; + } + case AVAILABLE: { + request.setAvailable(available(Boolean.TRUE.equals(param))); + break; + } + case REQ_SET_BODY_REPLAY: { + ByteChunk body = (ByteChunk) param; + setRequestBody(body); + break; + } + + // Error handling + case IS_ERROR: { + ((AtomicBoolean) param).set(getErrorState().isError()); + break; + } + case IS_IO_ALLOWED: { + ((AtomicBoolean) param).set(getErrorState().isIoAllowed()); + break; + } + case CLOSE_NOW: { + // Prevent further writes to the response + setSwallowResponse(); + if (param instanceof Throwable) { + setErrorState(ErrorState.CLOSE_NOW, (Throwable) param); + } else { + setErrorState(ErrorState.CLOSE_NOW, null); + } + break; + } + case DISABLE_SWALLOW_INPUT: { + // Cancelled upload or similar. + // No point reading the remainder of the request. + disableSwallowRequest(); + // This is an error state. Make sure it is marked as such. + setErrorState(ErrorState.CLOSE_CLEAN, null); + break; + } + + // Request attribute support + case REQ_HOST_ADDR_ATTRIBUTE: { + if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) { + request.remoteAddr().setString(socketWrapper.getRemoteAddr()); + } + break; + } + case REQ_PEER_ADDR_ATTRIBUTE: { + if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) { + request.peerAddr().setString(socketWrapper.getRemoteAddr()); + } + break; + } + case REQ_HOST_ATTRIBUTE: { + populateRequestAttributeRemoteHost(); + break; + } + case REQ_LOCALPORT_ATTRIBUTE: { + if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) { + request.setLocalPort(socketWrapper.getLocalPort()); + } + break; + } + case REQ_LOCAL_ADDR_ATTRIBUTE: { + if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) { + request.localAddr().setString(socketWrapper.getLocalAddr()); + } + break; + } + case REQ_LOCAL_NAME_ATTRIBUTE: { + if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) { + request.localName().setString(socketWrapper.getLocalName()); + } + break; + } + case REQ_REMOTEPORT_ATTRIBUTE: { + if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) { + request.setRemotePort(socketWrapper.getRemotePort()); + } + break; + } + + // SSL request attribute support + case REQ_SSL_ATTRIBUTE: { + populateSslRequestAttributes(); + break; + } + case REQ_SSL_CERTIFICATE: { + try { + sslReHandShake(); + } catch (IOException ioe) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); + } + break; + } + + // Servlet 3.0 asynchronous support + case ASYNC_START: { + asyncStateMachine.asyncStart((AsyncContextCallback) param); + break; + } + case ASYNC_COMPLETE: { + clearDispatches(); + if (asyncStateMachine.asyncComplete()) { + processSocketEvent(SocketEvent.OPEN_READ, true); + } + break; + } + case ASYNC_DISPATCH: { + if (asyncStateMachine.asyncDispatch()) { + processSocketEvent(SocketEvent.OPEN_READ, true); + } + break; + } + case ASYNC_DISPATCHED: { + asyncStateMachine.asyncDispatched(); + break; + } + case ASYNC_ERROR: { + asyncStateMachine.asyncError(); + break; + } + case ASYNC_IS_ASYNC: { + ((AtomicBoolean) param).set(asyncStateMachine.isAsync()); + break; + } + case ASYNC_IS_COMPLETING: { + ((AtomicBoolean) param).set(asyncStateMachine.isCompleting()); + break; + } + case ASYNC_IS_DISPATCHING: { + ((AtomicBoolean) param).set(asyncStateMachine.isAsyncDispatching()); + break; + } + case ASYNC_IS_ERROR: { + ((AtomicBoolean) param).set(asyncStateMachine.isAsyncError()); + break; + } + case ASYNC_IS_STARTED: { + ((AtomicBoolean) param).set(asyncStateMachine.isAsyncStarted()); + break; + } + case ASYNC_IS_TIMINGOUT: { + ((AtomicBoolean) param).set(asyncStateMachine.isAsyncTimingOut()); + break; + } + case ASYNC_RUN: { + asyncStateMachine.asyncRun((Runnable) param); + break; + } + case ASYNC_SETTIMEOUT: { + if (param == null) { + return; + } + long timeout = ((Long) param).longValue(); + setAsyncTimeout(timeout); + break; + } + case ASYNC_TIMEOUT: { + AtomicBoolean result = (AtomicBoolean) param; + result.set(asyncStateMachine.asyncTimeout()); + break; + } + case ASYNC_POST_PROCESS: { + asyncStateMachine.asyncPostProcess(); + break; + } + + // Servlet 3.1 non-blocking I/O + case REQUEST_BODY_FULLY_READ: { + AtomicBoolean result = (AtomicBoolean) param; + result.set(isRequestBodyFullyRead()); + break; + } + case NB_READ_INTEREST: { + AtomicBoolean isReady = (AtomicBoolean) param; + isReady.set(isReadyForRead()); + break; + } + case NB_WRITE_INTEREST: { + AtomicBoolean isReady = (AtomicBoolean) param; + isReady.set(isReadyForWrite()); + break; + } + case DISPATCH_READ: { + addDispatch(DispatchType.NON_BLOCKING_READ); + break; + } + case DISPATCH_WRITE: { + addDispatch(DispatchType.NON_BLOCKING_WRITE); + break; + } + case DISPATCH_EXECUTE: { + executeDispatches(); + break; + } + + // Servlet 3.1 HTTP Upgrade + case UPGRADE: { + doHttpUpgrade((UpgradeToken) param); + break; + } + + // Servlet 4.0 Push requests + case IS_PUSH_SUPPORTED: { + AtomicBoolean result = (AtomicBoolean) param; + result.set(isPushSupported()); + break; + } + case PUSH_REQUEST: { + doPush((Request) param); + break; + } + + // Servlet 4.0 Trailers + case IS_TRAILER_FIELDS_READY: { + AtomicBoolean result = (AtomicBoolean) param; + result.set(isTrailerFieldsReady()); + break; + } + case IS_TRAILER_FIELDS_SUPPORTED: { + AtomicBoolean result = (AtomicBoolean) param; + result.set(isTrailerFieldsSupported()); + break; + } + + // Identifiers + case PROTOCOL_REQUEST_ID: { + @SuppressWarnings("unchecked") + AtomicReference result = (AtomicReference) param; + result.set(getProtocolRequestId()); + break; + } + case SERVLET_CONNECTION: { + @SuppressWarnings("unchecked") + AtomicReference result = (AtomicReference) param; + result.set(getServletConnection()); + break; + } + } + } + + + private void handleIOException(IOException ioe) { + if (ioe instanceof CloseNowException) { + // Close the channel but keep the connection open + setErrorState(ErrorState.CLOSE_NOW, ioe); + } else { + // Close the connection and all channels within that connection + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); + } + } + + + /** + * Perform any necessary processing for a non-blocking read before dispatching to the adapter. + */ + protected void dispatchNonBlockingRead() { + asyncStateMachine.asyncOperation(); + } + + + /** + * {@inheritDoc} + *

    + * Sub-classes of this base class represent a single request/response pair. The timeout to be processed is, + * therefore, the Servlet asynchronous processing timeout. + */ + @Override + public void timeoutAsync(long now) { + if (now < 0) { + doTimeoutAsync(); + } else { + long asyncTimeout = getAsyncTimeout(); + if (asyncTimeout > 0) { + long asyncStart = asyncStateMachine.getLastAsyncStart(); + if ((now - asyncStart) > asyncTimeout) { + doTimeoutAsync(); + } + } else if (!asyncStateMachine.isAvailable()) { + // Timeout the async process if the associated web application + // is no longer running. + doTimeoutAsync(); + } + } + } + + + private void doTimeoutAsync() { + // Avoid multiple timeouts + setAsyncTimeout(-1); + asyncTimeoutGeneration = asyncStateMachine.getCurrentGeneration(); + processSocketEvent(SocketEvent.TIMEOUT, true); + } + + + @Override + public boolean checkAsyncTimeoutGeneration() { + return asyncTimeoutGeneration == asyncStateMachine.getCurrentGeneration(); + } + + + public void setAsyncTimeout(long timeout) { + asyncTimeout = timeout; + } + + + public long getAsyncTimeout() { + return asyncTimeout; + } + + + @Override + public void recycle() { + errorState = ErrorState.NONE; + asyncStateMachine.recycle(); + } + + + protected abstract void prepareResponse() throws IOException; + + + protected abstract void finishResponse() throws IOException; + + + protected abstract void ack(ContinueResponseTiming continueResponseTiming); + + + protected abstract void flush() throws IOException; + + + protected abstract int available(boolean doRead); + + + protected abstract void setRequestBody(ByteChunk body); + + + protected abstract void setSwallowResponse(); + + + protected abstract void disableSwallowRequest(); + + + /** + * Processors that populate request attributes directly (e.g. AJP) should over-ride this method and return + * {@code false}. + * + * @return {@code true} if the SocketWrapper should be used to populate the request attributes, otherwise + * {@code false}. + */ + protected boolean getPopulateRequestAttributesFromSocket() { + return true; + } + + + /** + * Populate the remote host request attribute. Processors (e.g. AJP) that populate this from an alternative source + * should override this method. + */ + protected void populateRequestAttributeRemoteHost() { + if (getPopulateRequestAttributesFromSocket() && socketWrapper != null) { + request.remoteHost().setString(socketWrapper.getRemoteHost()); + } + } + + + /** + * Populate the TLS related request attributes from the {@link SSLSupport} instance associated with this processor. + * Protocols that populate TLS attributes from a different source (e.g. AJP) should override this method. + */ + protected void populateSslRequestAttributes() { + try { + if (sslSupport != null) { + Object sslO = sslSupport.getCipherSuite(); + if (sslO != null) { + request.setAttribute(SSLSupport.CIPHER_SUITE_KEY, sslO); + } + sslO = sslSupport.getPeerCertificateChain(); + if (sslO != null) { + request.setAttribute(SSLSupport.CERTIFICATE_KEY, sslO); + } + sslO = sslSupport.getKeySize(); + if (sslO != null) { + request.setAttribute(SSLSupport.KEY_SIZE_KEY, sslO); + } + sslO = sslSupport.getSessionId(); + if (sslO != null) { + request.setAttribute(SSLSupport.SESSION_ID_KEY, sslO); + } + sslO = sslSupport.getProtocol(); + if (sslO != null) { + request.setAttribute(SSLSupport.PROTOCOL_VERSION_KEY, sslO); + } + sslO = sslSupport.getRequestedProtocols(); + if (sslO != null) { + request.setAttribute(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY, sslO); + } + sslO = sslSupport.getRequestedCiphers(); + if (sslO != null) { + request.setAttribute(SSLSupport.REQUESTED_CIPHERS_KEY, sslO); + } + request.setAttribute(SSLSupport.SESSION_MGR, sslSupport); + } + } catch (Exception e) { + getLog().warn(sm.getString("abstractProcessor.socket.ssl"), e); + } + } + + + /** + * Processors that can perform a TLS re-handshake (e.g. HTTP/1.1) should override this method and implement the + * re-handshake. + * + * @throws IOException If authentication is required then there will be I/O with the client and this exception will + * be thrown if that goes wrong + */ + protected void sslReHandShake() throws IOException { + // NO-OP + } + + + protected void processSocketEvent(SocketEvent event, boolean dispatch) { + SocketWrapperBase socketWrapper = getSocketWrapper(); + if (socketWrapper != null) { + socketWrapper.processSocket(event, dispatch); + } + } + + + protected boolean isReadyForRead() { + if (available(true) > 0) { + return true; + } + + if (!isRequestBodyFullyRead()) { + registerReadInterest(); + } + + return false; + } + + + protected abstract boolean isRequestBodyFullyRead(); + + + protected abstract void registerReadInterest(); + + + protected abstract boolean isReadyForWrite(); + + + protected void executeDispatches() { + SocketWrapperBase socketWrapper = getSocketWrapper(); + Iterator dispatches = getIteratorAndClearDispatches(); + if (socketWrapper != null) { + Lock lock = socketWrapper.getLock(); + lock.lock(); + try { + /* + * This method is called when non-blocking IO is initiated by defining a read and/or write listener in a + * non-container thread. It is called once the non-container thread completes so that the first calls to + * onWritePossible() and/or onDataAvailable() as appropriate are made by the container. + * + * Processing the dispatches requires (TODO confirm applies without APR) that the socket has been added + * to the waitingRequests queue. This may not have occurred by the time that the non-container thread + * completes triggering the call to this method. Therefore, the coded syncs on the SocketWrapper as the + * container thread that initiated this non-container thread holds a lock on the SocketWrapper. The + * container thread will add the socket to the waitingRequests queue before releasing the lock on the + * socketWrapper. Therefore, by obtaining the lock on socketWrapper before processing the dispatches, we + * can be sure that the socket has been added to the waitingRequests queue. + */ + while (dispatches != null && dispatches.hasNext()) { + DispatchType dispatchType = dispatches.next(); + socketWrapper.processSocket(dispatchType.getSocketStatus(), false); + } + } finally { + lock.unlock(); + } + } + } + + + /** + * {@inheritDoc} Processors that implement HTTP upgrade must override this method and provide the necessary token. + */ + @Override + public UpgradeToken getUpgradeToken() { + // Should never reach this code but in case we do... + throw new IllegalStateException(sm.getString("abstractProcessor.httpupgrade.notsupported")); + } + + + /** + * Process an HTTP upgrade. Processors that support HTTP upgrade should override this method and process the + * provided token. + * + * @param upgradeToken Contains all the information necessary for the Processor to process the upgrade + * + * @throws UnsupportedOperationException if the protocol does not support HTTP upgrade + */ + protected void doHttpUpgrade(UpgradeToken upgradeToken) { + // Should never happen + throw new UnsupportedOperationException(sm.getString("abstractProcessor.httpupgrade.notsupported")); + } + + + /** + * {@inheritDoc} Processors that implement HTTP upgrade must override this method. + */ + @Override + public ByteBuffer getLeftoverInput() { + // Should never reach this code but in case we do... + throw new IllegalStateException(sm.getString("abstractProcessor.httpupgrade.notsupported")); + } + + + /** + * {@inheritDoc} Processors that implement HTTP upgrade must override this method. + */ + @Override + public boolean isUpgrade() { + return false; + } + + + /** + * Protocols that support push should override this method and return {@code + * true}. + * + * @return {@code true} if push is supported by this processor, otherwise {@code false}. + */ + protected boolean isPushSupported() { + return false; + } + + + /** + * Process a push. Processors that support push should override this method and process the provided token. + * + * @param pushTarget Contains all the information necessary for the Processor to process the push request + * + * @throws UnsupportedOperationException if the protocol does not support push + */ + protected void doPush(Request pushTarget) { + throw new UnsupportedOperationException(sm.getString("abstractProcessor.pushrequest.notsupported")); + } + + + protected abstract boolean isTrailerFieldsReady(); + + + /** + * Protocols that support trailer fields should override this method and return {@code true}. + * + * @return {@code true} if trailer fields are supported by this processor, otherwise {@code false}. + */ + protected boolean isTrailerFieldsSupported() { + return false; + } + + + /** + * Protocols that provide per HTTP request IDs (e.g. Stream ID for HTTP/2) should override this method and return + * the appropriate ID. + * + * @return The ID associated with this request or the empty string if no such ID is defined + */ + protected Object getProtocolRequestId() { + return null; + } + + + /** + * Protocols must override this method and return an appropriate ServletConnection instance + * + * @return the ServletConnection instance associated with the current request. + */ + protected abstract ServletConnection getServletConnection(); + + + /** + * Flush any pending writes. Used during non-blocking writes to flush any remaining data from a previous incomplete + * write. + * + * @return true if data remains to be flushed at the end of method + * + * @throws IOException If an I/O error occurs while attempting to flush the data + */ + protected abstract boolean flushBufferedWrite() throws IOException; + + + /** + * Perform any necessary clean-up processing if the dispatch resulted in the completion of processing for the + * current request. + * + * @return The state to return for the socket once the clean-up for the current request has completed + * + * @throws IOException If an I/O error occurs while attempting to end the request + */ + protected abstract SocketState dispatchEndRequest() throws IOException; + + + @SuppressWarnings("deprecation") + @Override + protected final void logAccess(SocketWrapperBase socketWrapper) throws IOException { + // Set the socket wrapper so the access log can read the socket related + // information (e.g. client IP) + setSocketWrapper(socketWrapper); + // Setup the minimal request information + request.setStartTimeNanos(System.nanoTime()); + // Setup the minimal response information + response.setStatus(400); + response.setError(); + getAdapter().log(request, response, 0); + } +} diff --git a/java/org/apache/coyote/AbstractProcessorLight.java b/java/org/apache/coyote/AbstractProcessorLight.java new file mode 100644 index 0000000..8948f14 --- /dev/null +++ b/java/org/apache/coyote/AbstractProcessorLight.java @@ -0,0 +1,191 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.DispatchType; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; + +/** + * This is a light-weight abstract processor implementation that is intended as a basis for all Processor + * implementations from the light-weight upgrade processors to the HTTP/AJP processors. + */ +public abstract class AbstractProcessorLight implements Processor { + + private Set dispatches = new CopyOnWriteArraySet<>(); + + + @Override + public SocketState process(SocketWrapperBase socketWrapper, SocketEvent status) throws IOException { + + SocketState state = SocketState.CLOSED; + Iterator dispatches = null; + do { + if (dispatches != null) { + DispatchType nextDispatch = dispatches.next(); + if (getLog().isTraceEnabled()) { + getLog().trace("Processing dispatch type: [" + nextDispatch + "]"); + } + state = dispatch(nextDispatch.getSocketStatus()); + if (!dispatches.hasNext()) { + state = checkForPipelinedData(state, socketWrapper); + } + } else if (status == SocketEvent.DISCONNECT) { + // Do nothing here, just wait for it to get recycled + } else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) { + state = dispatch(status); + state = checkForPipelinedData(state, socketWrapper); + } else if (status == SocketEvent.OPEN_WRITE) { + // Extra write event likely after async, ignore + state = SocketState.LONG; + } else if (status == SocketEvent.OPEN_READ) { + state = service(socketWrapper); + } else if (status == SocketEvent.CONNECT_FAIL) { + logAccess(socketWrapper); + } else { + // Default to closing the socket if the SocketEvent passed in + // is not consistent with the current state of the Processor + state = SocketState.CLOSED; + } + + if (getLog().isTraceEnabled()) { + getLog().trace( + "Socket: [" + socketWrapper + "], Status in: [" + status + "], State out: [" + state + "]"); + } + + /* + * If state is already CLOSED don't call asyncPostProcess() as that will likely change the the state to some + * other value causing processing to continue when it should cease. The AsyncStateMachine will be recycled + * as part of the Processor clean-up on CLOSED so it doesn't matter what state it is left in at this point. + */ + if (isAsync() && state != SocketState.CLOSED) { + state = asyncPostProcess(); + if (getLog().isTraceEnabled()) { + getLog().trace( + "Socket: [" + socketWrapper + "], State after async post processing: [" + state + "]"); + } + } + + if (dispatches == null || !dispatches.hasNext()) { + // Only returns non-null iterator if there are + // dispatches to process. + dispatches = getIteratorAndClearDispatches(); + } + } while (state == SocketState.ASYNC_END || dispatches != null && state != SocketState.CLOSED); + + return state; + } + + + private SocketState checkForPipelinedData(SocketState inState, SocketWrapperBase socketWrapper) + throws IOException { + if (inState == SocketState.OPEN) { + // There may be pipe-lined data to read. If the data isn't + // processed now, execution will exit this loop and call + // release() which will recycle the processor (and input + // buffer) deleting any pipe-lined data. To avoid this, + // process it now. + return service(socketWrapper); + } else { + return inState; + } + } + + + public void addDispatch(DispatchType dispatchType) { + synchronized (dispatches) { + dispatches.add(dispatchType); + } + } + + + public Iterator getIteratorAndClearDispatches() { + // Note: Logic in AbstractProtocol depends on this method only returning + // a non-null value if the iterator is non-empty. i.e. it should never + // return an empty iterator. + Iterator result; + synchronized (dispatches) { + // Synchronized as the generation of the iterator and the clearing + // of dispatches needs to be an atomic operation. + result = dispatches.iterator(); + if (result.hasNext()) { + dispatches.clear(); + } else { + result = null; + } + } + return result; + } + + + protected void clearDispatches() { + synchronized (dispatches) { + dispatches.clear(); + } + } + + + /** + * Add an entry to the access log for a failed connection attempt. + * + * @param socketWrapper The connection to process + * + * @throws IOException If an I/O error occurs during the processing of the request + */ + protected void logAccess(SocketWrapperBase socketWrapper) throws IOException { + // NO-OP by default + } + + + /** + * Service a 'standard' HTTP request. This method is called for both new requests and for requests that have + * partially read the HTTP request line or HTTP headers. Once the headers have been fully read this method is not + * called again until there is a new HTTP request to process. Note that the request type may change during + * processing which may result in one or more calls to {@link #dispatch(SocketEvent)}. Requests may be pipe-lined. + * + * @param socketWrapper The connection to process + * + * @return The state the caller should put the socket in when this method returns + * + * @throws IOException If an I/O error occurs during the processing of the request + */ + protected abstract SocketState service(SocketWrapperBase socketWrapper) throws IOException; + + /** + * Process an in-progress request that is not longer in standard HTTP mode. Uses currently include Servlet 3.0 Async + * and HTTP upgrade connections. Further uses may be added in the future. These will typically start as HTTP + * requests. + * + * @param status The event to process + * + * @return The state the caller should put the socket in when this method returns + * + * @throws IOException If an I/O error occurs during the processing of the request + */ + protected abstract SocketState dispatch(SocketEvent status) throws IOException; + + protected abstract SocketState asyncPostProcess(); + + protected abstract Log getLog(); +} diff --git a/java/org/apache/coyote/AbstractProtocol.java b/java/org/apache/coyote/AbstractProtocol.java new file mode 100644 index 0000000..3a05b01 --- /dev/null +++ b/java/org/apache/coyote/AbstractProtocol.java @@ -0,0 +1,1234 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanRegistration; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.WebConnection; + +import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; +import org.apache.juli.logging.Log; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.net.AbstractEndpoint; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +public abstract class AbstractProtocol implements ProtocolHandler, MBeanRegistration { + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(AbstractProtocol.class); + + + /** + * Counter used to generate unique JMX names for connectors using automatic port binding. + */ + private static final AtomicInteger nameCounter = new AtomicInteger(0); + + + /** + * Unique ID for this connector. Only used if the connector is configured to use a random port as the port will + * change if stop(), start() is called. + */ + private int nameIndex = 0; + + + /** + * Endpoint that provides low-level network I/O - must be matched to the ProtocolHandler implementation + * (ProtocolHandler using NIO, requires NIO Endpoint etc.). + */ + private final AbstractEndpoint endpoint; + + + private Handler handler; + + + private final Set waitingProcessors = ConcurrentHashMap.newKeySet(); + + /** + * Controller for the timeout scheduling. + */ + private ScheduledFuture timeoutFuture = null; + private ScheduledFuture monitorFuture; + + public AbstractProtocol(AbstractEndpoint endpoint) { + this.endpoint = endpoint; + ConnectionHandler cHandler = new ConnectionHandler<>(this); + getEndpoint().setHandler(cHandler); + setHandler(cHandler); + setConnectionLinger(Constants.DEFAULT_CONNECTION_LINGER); + setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY); + } + + + // ----------------------------------------------- Generic property handling + + /** + * Generic property setter used by the digester. Other code should not need to use this. The digester will only use + * this method if it can't find a more specific setter. That means the property belongs to the Endpoint, the + * ServerSocketFactory or some other lower level component. This method ensures that it is visible to both. + * + * @param name The name of the property to set + * @param value The value, in string form, to set for the property + * + * @return true if the property was set successfully, otherwise false + */ + public boolean setProperty(String name, String value) { + return endpoint.setProperty(name, value); + } + + + /** + * Generic property getter used by the digester. Other code should not need to use this. + * + * @param name The name of the property to get + * + * @return The value of the property converted to a string + */ + public String getProperty(String name) { + return endpoint.getProperty(name); + } + + + // ------------------------------- Properties managed by the ProtocolHandler + + /** + * Name of MBean for the Global Request Processor. + */ + protected ObjectName rgOname = null; + + public ObjectName getGlobalRequestProcessorMBeanName() { + return rgOname; + } + + /** + * The adapter provides the link between the ProtocolHandler and the connector. + */ + protected Adapter adapter; + + @Override + public void setAdapter(Adapter adapter) { + this.adapter = adapter; + } + + @Override + public Adapter getAdapter() { + return adapter; + } + + + /** + * The maximum number of idle processors that will be retained in the cache and re-used with a subsequent request. + * The default is 200. A value of -1 means unlimited. In the unlimited case, the theoretical maximum number of + * cached Processor objects is {@link #getMaxConnections()} although it will usually be closer to + * {@link #getMaxThreads()}. + */ + protected int processorCache = 200; + + public int getProcessorCache() { + return this.processorCache; + } + + public void setProcessorCache(int processorCache) { + this.processorCache = processorCache; + } + + + private String clientCertProvider = null; + + /** + * When client certificate information is presented in a form other than instances of + * {@link java.security.cert.X509Certificate} it needs to be converted before it can be used and this property + * controls which JSSE provider is used to perform the conversion. For example it is used with the AJP connectors + * and with the {@link org.apache.catalina.valves.SSLValve}. If not specified, the default provider will be used. + * + * @return The name of the JSSE provider to use + */ + public String getClientCertProvider() { + return clientCertProvider; + } + + public void setClientCertProvider(String s) { + this.clientCertProvider = s; + } + + + private int maxHeaderCount = 100; + + public int getMaxHeaderCount() { + return maxHeaderCount; + } + + public void setMaxHeaderCount(int maxHeaderCount) { + this.maxHeaderCount = maxHeaderCount; + } + + + @Override + public boolean isSendfileSupported() { + return endpoint.getUseSendfile(); + } + + + @Override + public String getId() { + return endpoint.getId(); + } + + + // ---------------------- Properties that are passed through to the EndPoint + + @Override + public Executor getExecutor() { + return endpoint.getExecutor(); + } + + @Override + public void setExecutor(Executor executor) { + endpoint.setExecutor(executor); + } + + + @Override + public ScheduledExecutorService getUtilityExecutor() { + return endpoint.getUtilityExecutor(); + } + + @Override + public void setUtilityExecutor(ScheduledExecutorService utilityExecutor) { + endpoint.setUtilityExecutor(utilityExecutor); + } + + + public int getMaxThreads() { + return endpoint.getMaxThreads(); + } + + public void setMaxThreads(int maxThreads) { + endpoint.setMaxThreads(maxThreads); + } + + public int getMaxConnections() { + return endpoint.getMaxConnections(); + } + + public void setMaxConnections(int maxConnections) { + endpoint.setMaxConnections(maxConnections); + } + + + public int getMinSpareThreads() { + return endpoint.getMinSpareThreads(); + } + + public void setMinSpareThreads(int minSpareThreads) { + endpoint.setMinSpareThreads(minSpareThreads); + } + + + public int getThreadPriority() { + return endpoint.getThreadPriority(); + } + + public void setThreadPriority(int threadPriority) { + endpoint.setThreadPriority(threadPriority); + } + + + public int getAcceptCount() { + return endpoint.getAcceptCount(); + } + + public void setAcceptCount(int acceptCount) { + endpoint.setAcceptCount(acceptCount); + } + + + public boolean getTcpNoDelay() { + return endpoint.getTcpNoDelay(); + } + + public void setTcpNoDelay(boolean tcpNoDelay) { + endpoint.setTcpNoDelay(tcpNoDelay); + } + + + public int getConnectionLinger() { + return endpoint.getConnectionLinger(); + } + + public void setConnectionLinger(int connectionLinger) { + endpoint.setConnectionLinger(connectionLinger); + } + + + /** + * The time Tomcat will wait for a subsequent request before closing the connection. The default is + * {@link #getConnectionTimeout()}. + * + * @return The timeout in milliseconds + */ + public int getKeepAliveTimeout() { + return endpoint.getKeepAliveTimeout(); + } + + public void setKeepAliveTimeout(int keepAliveTimeout) { + endpoint.setKeepAliveTimeout(keepAliveTimeout); + } + + public InetAddress getAddress() { + return endpoint.getAddress(); + } + + public void setAddress(InetAddress ia) { + endpoint.setAddress(ia); + } + + + public int getPort() { + return endpoint.getPort(); + } + + public void setPort(int port) { + endpoint.setPort(port); + } + + + public int getPortOffset() { + return endpoint.getPortOffset(); + } + + public void setPortOffset(int portOffset) { + endpoint.setPortOffset(portOffset); + } + + + public int getPortWithOffset() { + return endpoint.getPortWithOffset(); + } + + + public int getLocalPort() { + return endpoint.getLocalPort(); + } + + /* + * When Tomcat expects data from the client, this is the time Tomcat will wait for that data to arrive before + * closing the connection. + */ + public int getConnectionTimeout() { + return endpoint.getConnectionTimeout(); + } + + public void setConnectionTimeout(int timeout) { + endpoint.setConnectionTimeout(timeout); + } + + public long getConnectionCount() { + return endpoint.getConnectionCount(); + } + + public void setAcceptorThreadPriority(int threadPriority) { + endpoint.setAcceptorThreadPriority(threadPriority); + } + + public int getAcceptorThreadPriority() { + return endpoint.getAcceptorThreadPriority(); + } + + + // ---------------------------------------------------------- Public methods + + public synchronized int getNameIndex() { + if (nameIndex == 0) { + nameIndex = nameCounter.incrementAndGet(); + } + + return nameIndex; + } + + + /** + * The name will be prefix-address-port if address is non-null and prefix-port if the address is null. + * + * @return A name for this protocol instance that is appropriately quoted for use in an ObjectName. + */ + public String getName() { + return ObjectName.quote(getNameInternal()); + } + + + private String getNameInternal() { + StringBuilder name = new StringBuilder(getNamePrefix()); + name.append('-'); + String id = getId(); + if (id != null) { + name.append(id); + } else { + if (getAddress() != null) { + name.append(getAddress().getHostAddress()); + name.append('-'); + } + int port = getPortWithOffset(); + if (port == 0) { + // Auto binding is in use. Check if port is known + name.append("auto-"); + name.append(getNameIndex()); + port = getLocalPort(); + if (port != -1) { + name.append('-'); + name.append(port); + } + } else { + name.append(port); + } + } + return name.toString(); + } + + + public void addWaitingProcessor(Processor processor) { + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("abstractProtocol.waitingProcessor.add", processor)); + } + waitingProcessors.add(processor); + } + + + public void removeWaitingProcessor(Processor processor) { + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("abstractProtocol.waitingProcessor.remove", processor)); + } + waitingProcessors.remove(processor); + } + + + /* + * Primarily for debugging and testing. Could be exposed via JMX if considered useful. + */ + public int getWaitingProcessorCount() { + return waitingProcessors.size(); + } + + + // ----------------------------------------------- Accessors for sub-classes + + protected AbstractEndpoint getEndpoint() { + return endpoint; + } + + + protected Handler getHandler() { + return handler; + } + + protected void setHandler(Handler handler) { + this.handler = handler; + } + + + // -------------------------------------------------------- Abstract methods + + /** + * Concrete implementations need to provide access to their logger to be used by the abstract classes. + * + * @return the logger + */ + protected abstract Log getLog(); + + + /** + * Obtain the prefix to be used when construction a name for this protocol handler. The name will be + * prefix-address-port. + * + * @return the prefix + */ + protected abstract String getNamePrefix(); + + + /** + * Obtain the name of the protocol, (Http, Ajp, etc.). Used with JMX. + * + * @return the protocol name + */ + protected abstract String getProtocolName(); + + + /** + * Find a suitable handler for the protocol negotiated at the network layer. + * + * @param name The name of the requested negotiated protocol. + * + * @return The instance where {@link UpgradeProtocol#getAlpnName()} matches the requested protocol + */ + protected abstract UpgradeProtocol getNegotiatedProtocol(String name); + + + /** + * Find a suitable handler for the protocol upgraded name specified. This is used for direct connection protocol + * selection. + * + * @param name The name of the requested negotiated protocol. + * + * @return The instance where {@link UpgradeProtocol#getAlpnName()} matches the requested protocol + */ + protected abstract UpgradeProtocol getUpgradeProtocol(String name); + + + /** + * Create and configure a new Processor instance for the current protocol implementation. + * + * @return A fully configured Processor instance that is ready to use + */ + protected abstract Processor createProcessor(); + + + protected abstract Processor createUpgradeProcessor(SocketWrapperBase socket, UpgradeToken upgradeToken); + + + // ----------------------------------------------------- JMX related methods + + protected String domain; + protected ObjectName oname; + protected MBeanServer mserver; + + public ObjectName getObjectName() { + return oname; + } + + public String getDomain() { + return domain; + } + + @Override + public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { + oname = name; + mserver = server; + domain = name.getDomain(); + return name; + } + + @Override + public void postRegister(Boolean registrationDone) { + // NOOP + } + + @Override + public void preDeregister() throws Exception { + // NOOP + } + + @Override + public void postDeregister() { + // NOOP + } + + private ObjectName createObjectName() throws MalformedObjectNameException { + // Use the same domain as the connector + domain = getAdapter().getDomain(); + + if (domain == null) { + return null; + } + + StringBuilder name = new StringBuilder(getDomain()); + name.append(":type=ProtocolHandler,port="); + int port = getPortWithOffset(); + if (port > 0) { + name.append(port); + } else { + name.append("auto-"); + name.append(getNameIndex()); + } + InetAddress address = getAddress(); + if (address != null) { + name.append(",address="); + name.append(ObjectName.quote(address.getHostAddress())); + } + return new ObjectName(name.toString()); + } + + + // ------------------------------------------------------- Lifecycle methods + + /* + * NOTE: There is no maintenance of state or checking for valid transitions within this class. It is expected that + * the connector will maintain state and prevent invalid state transitions. + */ + + @Override + public void init() throws Exception { + if (getLog().isInfoEnabled()) { + getLog().info(sm.getString("abstractProtocolHandler.init", getName())); + logPortOffset(); + } + + if (oname == null) { + // Component not pre-registered so register it + oname = createObjectName(); + if (oname != null) { + Registry.getRegistry(null, null).registerComponent(this, oname, null); + } + } + + if (this.domain != null) { + ObjectName rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName()); + this.rgOname = rgOname; + Registry.getRegistry(null, null).registerComponent(getHandler().getGlobal(), rgOname, null); + } + + String endpointName = getName(); + endpoint.setName(endpointName.substring(1, endpointName.length() - 1)); + endpoint.setDomain(domain); + + endpoint.init(); + } + + + @Override + public void start() throws Exception { + if (getLog().isInfoEnabled()) { + getLog().info(sm.getString("abstractProtocolHandler.start", getName())); + logPortOffset(); + } + + endpoint.start(); + monitorFuture = getUtilityExecutor().scheduleWithFixedDelay(() -> { + startAsyncTimeout(); + }, 0, 60, TimeUnit.SECONDS); + } + + + /** + * Note: The name of this method originated with the Servlet 3.0 asynchronous processing but evolved over time to + * represent a timeout that is triggered independently of the socket read/write timeouts. + */ + protected void startAsyncTimeout() { + if (timeoutFuture == null || timeoutFuture.isDone()) { + if (timeoutFuture != null && timeoutFuture.isDone()) { + // There was an error executing the scheduled task, get it and log it + try { + timeoutFuture.get(); + } catch (InterruptedException | ExecutionException e) { + getLog().error(sm.getString("abstractProtocolHandler.asyncTimeoutError"), e); + } + } + timeoutFuture = getUtilityExecutor().scheduleAtFixedRate(() -> { + long now = System.currentTimeMillis(); + for (Processor processor : waitingProcessors) { + processor.timeoutAsync(now); + } + }, 1, 1, TimeUnit.SECONDS); + } + } + + protected void stopAsyncTimeout() { + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + timeoutFuture = null; + } + } + + @Override + public void pause() throws Exception { + if (getLog().isInfoEnabled()) { + getLog().info(sm.getString("abstractProtocolHandler.pause", getName())); + } + + endpoint.pause(); + } + + + public boolean isPaused() { + return endpoint.isPaused(); + } + + + @Override + public void resume() throws Exception { + if (getLog().isInfoEnabled()) { + getLog().info(sm.getString("abstractProtocolHandler.resume", getName())); + } + + endpoint.resume(); + } + + + @Override + public void stop() throws Exception { + if (getLog().isInfoEnabled()) { + getLog().info(sm.getString("abstractProtocolHandler.stop", getName())); + logPortOffset(); + } + + if (monitorFuture != null) { + monitorFuture.cancel(true); + monitorFuture = null; + } + stopAsyncTimeout(); + // Timeout any waiting processor + for (Processor processor : waitingProcessors) { + processor.timeoutAsync(-1); + } + + endpoint.stop(); + } + + + @Override + public void destroy() throws Exception { + if (getLog().isInfoEnabled()) { + getLog().info(sm.getString("abstractProtocolHandler.destroy", getName())); + logPortOffset(); + } + + try { + endpoint.destroy(); + } finally { + if (oname != null) { + if (mserver == null) { + Registry.getRegistry(null, null).unregisterComponent(oname); + } else { + // Possibly registered with a different MBeanServer + try { + mserver.unregisterMBean(oname); + } catch (MBeanRegistrationException | InstanceNotFoundException e) { + getLog().info(sm.getString("abstractProtocol.mbeanDeregistrationFailed", oname, mserver)); + } + } + } + + ObjectName rgOname = getGlobalRequestProcessorMBeanName(); + if (rgOname != null) { + Registry.getRegistry(null, null).unregisterComponent(rgOname); + } + } + } + + + @Override + public void closeServerSocketGraceful() { + endpoint.closeServerSocketGraceful(); + } + + + @Override + public long awaitConnectionsClose(long waitMillis) { + getLog().info(sm.getString("abstractProtocol.closeConnectionsAwait", Long.valueOf(waitMillis), getName())); + return endpoint.awaitConnectionsClose(waitMillis); + } + + + private void logPortOffset() { + if (getPort() != getPortWithOffset()) { + getLog().info(sm.getString("abstractProtocolHandler.portOffset", getName(), String.valueOf(getPort()), + String.valueOf(getPortOffset()))); + } + } + + + // ------------------------------------------- Connection handler base class + + protected static class ConnectionHandler implements AbstractEndpoint.Handler { + + private final AbstractProtocol proto; + private final RequestGroupInfo global = new RequestGroupInfo(); + private final AtomicLong registerCount = new AtomicLong(0); + private final RecycledProcessors recycledProcessors = new RecycledProcessors(this); + + public ConnectionHandler(AbstractProtocol proto) { + this.proto = proto; + } + + protected AbstractProtocol getProtocol() { + return proto; + } + + protected Log getLog() { + return getProtocol().getLog(); + } + + @Override + public Object getGlobal() { + return global; + } + + @Override + public void recycle() { + recycledProcessors.clear(); + } + + + @Override + public SocketState process(SocketWrapperBase wrapper, SocketEvent status) { + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("abstractConnectionHandler.process", wrapper.getSocket(), status)); + } + if (wrapper == null) { + // Nothing to do. Socket has been closed. + return SocketState.CLOSED; + } + + S socket = wrapper.getSocket(); + + // We take complete ownership of the Processor inside of this method to ensure + // no other thread can release it while we're using it. Whatever processor is + // held by this variable will be associated with the SocketWrapper before this + // method returns. + Processor processor = (Processor) wrapper.takeCurrentProcessor(); + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("abstractConnectionHandler.connectionsGet", processor, socket)); + } + + // Timeouts are calculated on a dedicated thread and then + // dispatched. Because of delays in the dispatch process, the + // timeout may no longer be required. Check here and avoid + // unnecessary processing. + if (SocketEvent.TIMEOUT == status && (processor == null || !processor.isAsync() && !processor.isUpgrade() || + processor.isAsync() && !processor.checkAsyncTimeoutGeneration())) { + // This is effectively a NO-OP + return SocketState.OPEN; + } + + if (processor != null) { + // Make sure an async timeout doesn't fire + getProtocol().removeWaitingProcessor(processor); + } else if (status == SocketEvent.DISCONNECT || status == SocketEvent.ERROR) { + // Nothing to do. Endpoint requested a close and there is no + // longer a processor associated with this socket. + return SocketState.CLOSED; + } + + try { + if (processor == null) { + String negotiatedProtocol = wrapper.getNegotiatedProtocol(); + // OpenSSL typically returns null whereas JSSE typically + // returns "" when no protocol is negotiated + if (negotiatedProtocol != null && negotiatedProtocol.length() > 0) { + UpgradeProtocol upgradeProtocol = getProtocol().getNegotiatedProtocol(negotiatedProtocol); + if (upgradeProtocol != null) { + processor = upgradeProtocol.getProcessor(wrapper, getProtocol().getAdapter()); + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("abstractConnectionHandler.processorCreate", processor)); + } + } else if (negotiatedProtocol.equals("http/1.1")) { + // Explicitly negotiated the default protocol. + // Obtain a processor below. + } else { + // TODO: + // OpenSSL 1.0.2's ALPN callback doesn't support + // failing the handshake with an error if no + // protocol can be negotiated. Therefore, we need to + // fail the connection here. Once this is fixed, + // replace the code below with the commented out + // block. + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("abstractConnectionHandler.negotiatedProcessor.fail", + negotiatedProtocol)); + } + return SocketState.CLOSED; + /* + * To replace the code above once OpenSSL 1.1.0 is used. // Failed to create processor. This + * is a bug. throw new IllegalStateException(sm.getString( + * "abstractConnectionHandler.negotiatedProcessor.fail", negotiatedProtocol)); + */ + } + } + } + if (processor == null) { + processor = recycledProcessors.pop(); + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("abstractConnectionHandler.processorPop", processor)); + } + } + if (processor == null) { + processor = getProtocol().createProcessor(); + register(processor); + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("abstractConnectionHandler.processorCreate", processor)); + } + } + + processor.setSslSupport(wrapper.getSslSupport()); + + SocketState state = SocketState.CLOSED; + do { + state = processor.process(wrapper, status); + + if (state == SocketState.UPGRADING) { + // Get the HTTP upgrade handler + UpgradeToken upgradeToken = processor.getUpgradeToken(); + // Restore leftover input to the wrapper so the upgrade + // processor can process it. + ByteBuffer leftOverInput = processor.getLeftoverInput(); + wrapper.unRead(leftOverInput); + if (upgradeToken == null) { + // Assume direct HTTP/2 connection + UpgradeProtocol upgradeProtocol = getProtocol().getUpgradeProtocol("h2c"); + if (upgradeProtocol != null) { + // Release the Http11 processor to be re-used + release(processor); + // Create the upgrade processor + processor = upgradeProtocol.getProcessor(wrapper, getProtocol().getAdapter()); + } else { + if (getLog().isDebugEnabled()) { + getLog().debug( + sm.getString("abstractConnectionHandler.negotiatedProcessor.fail", "h2c")); + } + // Exit loop and trigger appropriate clean-up + state = SocketState.CLOSED; + } + } else { + HttpUpgradeHandler httpUpgradeHandler = upgradeToken.getHttpUpgradeHandler(); + // Release the Http11 processor to be re-used + release(processor); + // Create the upgrade processor + processor = getProtocol().createUpgradeProcessor(wrapper, upgradeToken); + if (getLog().isTraceEnabled()) { + getLog().trace( + sm.getString("abstractConnectionHandler.upgradeCreate", processor, wrapper)); + } + // Initialise the upgrade handler (which may trigger + // some IO using the new protocol which is why the lines + // above are necessary) + // This cast should be safe. If it fails the error + // handling for the surrounding try/catch will deal with + // it. + if (upgradeToken.getInstanceManager() == null) { + httpUpgradeHandler.init((WebConnection) processor); + } else { + ClassLoader oldCL = upgradeToken.getContextBind().bind(false, null); + try { + httpUpgradeHandler.init((WebConnection) processor); + } finally { + upgradeToken.getContextBind().unbind(false, oldCL); + } + } + if (httpUpgradeHandler instanceof InternalHttpUpgradeHandler) { + if (((InternalHttpUpgradeHandler) httpUpgradeHandler).hasAsyncIO()) { + // The handler will initiate all further I/O + state = SocketState.ASYNC_IO; + } + } + } + } + } while (state == SocketState.UPGRADING); + + if (state == SocketState.LONG) { + // In the middle of processing a request/response. Keep the + // socket associated with the processor. Exact requirements + // depend on type of long poll + longPoll(wrapper, processor); + if (processor.isAsync()) { + getProtocol().addWaitingProcessor(processor); + } + } else if (state == SocketState.OPEN) { + // In keep-alive but between requests. OK to recycle + // processor. Continue to poll for the next request. + release(processor); + processor = null; + wrapper.registerReadInterest(); + } else if (state == SocketState.SENDFILE) { + // Sendfile in progress. If it fails, the socket will be + // closed. If it works, the socket either be added to the + // poller (or equivalent) to await more data or processed + // if there are any pipe-lined requests remaining. + } else if (state == SocketState.UPGRADED) { + // Don't add sockets back to the poller if this was a + // non-blocking write otherwise the poller may trigger + // multiple read events which may lead to thread starvation + // in the connector. The write() method will add this socket + // to the poller if necessary. + if (status != SocketEvent.OPEN_WRITE) { + longPoll(wrapper, processor); + getProtocol().addWaitingProcessor(processor); + } + } else if (state == SocketState.ASYNC_IO) { + // Don't add sockets back to the poller. + // The handler will initiate all further I/O + if (status != SocketEvent.OPEN_WRITE) { + getProtocol().addWaitingProcessor(processor); + } + } else if (state == SocketState.SUSPENDED) { + // Don't add sockets back to the poller. + // The resumeProcessing() method will add this socket + // to the poller. + } else { + // Connection closed. OK to recycle the processor. + // Processors handling upgrades require additional clean-up + // before release. + if (processor != null && processor.isUpgrade()) { + UpgradeToken upgradeToken = processor.getUpgradeToken(); + HttpUpgradeHandler httpUpgradeHandler = upgradeToken.getHttpUpgradeHandler(); + InstanceManager instanceManager = upgradeToken.getInstanceManager(); + if (instanceManager == null) { + httpUpgradeHandler.destroy(); + } else { + ClassLoader oldCL = upgradeToken.getContextBind().bind(false, null); + try { + httpUpgradeHandler.destroy(); + } finally { + try { + instanceManager.destroyInstance(httpUpgradeHandler); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + getLog().error(sm.getString("abstractConnectionHandler.error"), e); + } + upgradeToken.getContextBind().unbind(false, oldCL); + } + } + } + + release(processor); + processor = null; + } + + if (processor != null) { + wrapper.setCurrentProcessor(processor); + } + return state; + } catch (SocketException e) { + // SocketExceptions are normal + getLog().debug(sm.getString("abstractConnectionHandler.socketexception.debug"), e); + } catch (IOException e) { + // IOExceptions are normal + getLog().debug(sm.getString("abstractConnectionHandler.ioexception.debug"), e); + } catch (ProtocolException e) { + // Protocol exceptions normally mean the client sent invalid or + // incomplete data. + getLog().debug(sm.getString("abstractConnectionHandler.protocolexception.debug"), e); + } + // Future developers: if you discover any other + // rare-but-nonfatal exceptions, catch them here, and log as + // above. + catch (OutOfMemoryError oome) { + // Try and handle this here to give Tomcat a chance to close the + // connection and prevent clients waiting until they time out. + // Worst case, it isn't recoverable and the attempt at logging + // will trigger another OOME. + getLog().error(sm.getString("abstractConnectionHandler.oome"), oome); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + // any other exception or error is odd. Here we log it + // with "ERROR" level, so it will show up even on + // less-than-verbose logs. + getLog().error(sm.getString("abstractConnectionHandler.error"), e); + } + + // Make sure socket/processor is removed from the list of current + // connections + release(processor); + return SocketState.CLOSED; + } + + + protected void longPoll(SocketWrapperBase socket, Processor processor) { + if (!processor.isAsync()) { + // This is currently only used with HTTP + // Either: + // - this is an upgraded connection + // - the request line/headers have not been completely + // read + socket.registerReadInterest(); + } + } + + + /** + * Expected to be used by the handler once the processor is no longer required. Care must be taken to ensure + * that this method is only called once per processor, after the request processing has completed. + * + * @param processor Processor being released (that was associated with the socket) + */ + private void release(Processor processor) { + if (processor != null) { + processor.recycle(); + if (processor.isUpgrade()) { + // While UpgradeProcessor instances should not normally be + // present in waitingProcessors there are various scenarios + // where this can happen. E.g.: + // - when AsyncIO is used + // - WebSocket I/O error on non-container thread + // Err on the side of caution and always try and remove any + // UpgradeProcessor instances from waitingProcessors + getProtocol().removeWaitingProcessor(processor); + } else { + // After recycling, only instances of UpgradeProcessorBase + // will return true for isUpgrade(). + // Instances of UpgradeProcessorBase should not be added to + // recycledProcessors since that pool is only for AJP or + // HTTP processors + recycledProcessors.push(processor); + if (getLog().isTraceEnabled()) { + getLog().trace("Pushed Processor [" + processor + "]"); + } + } + } + } + + + /** + * Expected to be used by the Endpoint to release resources on socket close, errors etc. + */ + @Override + public void release(SocketWrapperBase socketWrapper) { + Processor processor = (Processor) socketWrapper.takeCurrentProcessor(); + release(processor); + } + + + protected void register(Processor processor) { + if (getProtocol().getDomain() != null) { + synchronized (this) { + try { + long count = registerCount.incrementAndGet(); + RequestInfo rp = processor.getRequest().getRequestProcessor(); + rp.setGlobalProcessor(global); + ObjectName rpName = new ObjectName( + getProtocol().getDomain() + ":type=RequestProcessor,worker=" + getProtocol().getName() + + ",name=" + getProtocol().getProtocolName() + "Request" + count); + if (getLog().isTraceEnabled()) { + getLog().trace("Register [" + processor + "] as [" + rpName + "]"); + } + Registry.getRegistry(null, null).registerComponent(rp, rpName, null); + rp.setRpName(rpName); + } catch (Exception e) { + getLog().warn(sm.getString("abstractProtocol.processorRegisterError"), e); + } + } + } + } + + protected void unregister(Processor processor) { + if (getProtocol().getDomain() != null) { + synchronized (this) { + try { + Request r = processor.getRequest(); + if (r == null) { + // Probably an UpgradeProcessor + return; + } + RequestInfo rp = r.getRequestProcessor(); + rp.setGlobalProcessor(null); + ObjectName rpName = rp.getRpName(); + if (getLog().isTraceEnabled()) { + getLog().trace("Unregister [" + rpName + "]"); + } + Registry.getRegistry(null, null).unregisterComponent(rpName); + rp.setRpName(null); + } catch (Exception e) { + getLog().warn(sm.getString("abstractProtocol.processorUnregisterError"), e); + } + } + } + } + + @Override + public final void pause() { + /* + * Inform all the processors associated with current connections that the endpoint is being paused. Most + * won't care. Those processing multiplexed streams may wish to take action. For example, HTTP/2 may wish to + * stop accepting new streams. + * + * Note that even if the endpoint is resumed, there is (currently) no API to inform the Processors of this. + */ + for (SocketWrapperBase wrapper : proto.getEndpoint().getConnections()) { + Processor processor = (Processor) wrapper.getCurrentProcessor(); + if (processor != null) { + processor.pause(); + } + } + } + } + + protected static class RecycledProcessors extends SynchronizedStack { + + private final transient ConnectionHandler handler; + protected final AtomicInteger size = new AtomicInteger(0); + + public RecycledProcessors(ConnectionHandler handler) { + this.handler = handler; + } + + @SuppressWarnings("sync-override") // Size may exceed cache size a bit + @Override + public boolean push(Processor processor) { + int cacheSize = handler.getProtocol().getProcessorCache(); + boolean offer = cacheSize == -1 ? true : size.get() < cacheSize; + // avoid over growing our cache or add after we have stopped + boolean result = false; + if (offer) { + result = super.push(processor); + if (result) { + size.incrementAndGet(); + } + } + if (!result) { + handler.unregister(processor); + } + return result; + } + + @SuppressWarnings("sync-override") // OK if size is too big briefly + @Override + public Processor pop() { + Processor result = super.pop(); + if (result != null) { + size.decrementAndGet(); + } + return result; + } + + @Override + public synchronized void clear() { + Processor next = pop(); + while (next != null) { + handler.unregister(next); + next = pop(); + } + super.clear(); + size.set(0); + } + } + +} diff --git a/java/org/apache/coyote/ActionCode.java b/java/org/apache/coyote/ActionCode.java new file mode 100644 index 0000000..308d3b3 --- /dev/null +++ b/java/org/apache/coyote/ActionCode.java @@ -0,0 +1,267 @@ +/* + * 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.coyote; + +/** + * ActionCodes represent callbacks from the servlet container to the coyote connector. Actions are implemented by + * ProtocolHandler, using the ActionHook interface. + * + * @see ProtocolHandler + * @see ActionHook + * + * @author Remy Maucherat + */ +public enum ActionCode { + ACK, + CLOSE, + COMMIT, + + /** + * A serious error occurred from which it is not possible to recover safely. Further attempts to write to the + * response should be ignored and the connection needs to be closed as soon as possible. This can also be used to + * forcibly close a connection if an error occurs after the response has been committed. + */ + CLOSE_NOW, + + /** + * A flush() operation originated by the client ( i.e. a flush() on the servlet output stream or writer, called by a + * servlet ). Argument is the Response. + */ + CLIENT_FLUSH, + + /** + * Has the processor been placed into the error state? Note that the response may not have an appropriate error code + * set. + */ + IS_ERROR, + + /** + * The processor may have been placed into an error state and some error states do not permit any further I/O. Is + * I/O currently allowed? + */ + IS_IO_ALLOWED, + + /** + * Hook called if swallowing request input should be disabled. Example: Cancel a large file upload. + */ + DISABLE_SWALLOW_INPUT, + + /** + * Callback for lazy evaluation - extract the remote host name and address. + */ + REQ_HOST_ATTRIBUTE, + + /** + * Callback for lazy evaluation - extract the remote host address. + */ + REQ_HOST_ADDR_ATTRIBUTE, + + /** + * Callback for lazy evaluation - extract the connection peer address. + */ + REQ_PEER_ADDR_ATTRIBUTE, + + /** + * Callback for lazy evaluation - extract the SSL-related attributes including the client certificate if present. + */ + REQ_SSL_ATTRIBUTE, + + /** + * Force a TLS re-handshake and make the resulting client certificate (if any) available as a request attribute. + */ + REQ_SSL_CERTIFICATE, + + /** + * Callback for lazy evaluation - socket remote port. + */ + REQ_REMOTEPORT_ATTRIBUTE, + + /** + * Callback for lazy evaluation - socket local port. + */ + REQ_LOCALPORT_ATTRIBUTE, + + /** + * Callback for lazy evaluation - local address. + */ + REQ_LOCAL_ADDR_ATTRIBUTE, + + /** + * Callback for lazy evaluation - local address. + */ + REQ_LOCAL_NAME_ATTRIBUTE, + + /** + * Callback for setting FORM auth body replay + */ + REQ_SET_BODY_REPLAY, + + /** + * Callback for getting the amount of available bytes. + */ + AVAILABLE, + + /** + * Callback for an async request. + */ + ASYNC_START, + + /** + * Callback for an async call to {@link jakarta.servlet.AsyncContext#dispatch()}. + */ + ASYNC_DISPATCH, + + /** + * Callback to indicate the the actual dispatch has started and that the async state needs change. + */ + ASYNC_DISPATCHED, + + /** + * Callback for an async call to {@link jakarta.servlet.AsyncContext#start(Runnable)}. + */ + ASYNC_RUN, + + /** + * Callback for an async call to {@link jakarta.servlet.AsyncContext#complete()}. + */ + ASYNC_COMPLETE, + + /** + * Callback to trigger the processing of an async timeout. + */ + ASYNC_TIMEOUT, + + /** + * Callback to trigger the error processing. + */ + ASYNC_ERROR, + + /** + * Callback for an async call to {@link jakarta.servlet.AsyncContext#setTimeout(long)} + */ + ASYNC_SETTIMEOUT, + + /** + * Callback to determine if async processing is in progress. + */ + ASYNC_IS_ASYNC, + + /** + * Callback to determine if async dispatch is in progress. + */ + ASYNC_IS_STARTED, + + /** + * Call back to determine if async complete is in progress. + */ + ASYNC_IS_COMPLETING, + + /** + * Callback to determine if async dispatch is in progress. + */ + ASYNC_IS_DISPATCHING, + + /** + * Callback to determine if async is timing out. + */ + ASYNC_IS_TIMINGOUT, + + /** + * Callback to determine if async is in error. + */ + ASYNC_IS_ERROR, + + /** + * Callback to trigger post processing. Typically only used during error handling to trigger essential processing + * that otherwise would be skipped. + */ + ASYNC_POST_PROCESS, + + /** + * Callback to trigger the HTTP upgrade process. + */ + UPGRADE, + + /** + * Indicator that Servlet is interested in being notified when data is available to be read. + */ + NB_READ_INTEREST, + + /** + * Used with non-blocking writes to determine if a write is currently allowed (sets passed parameter to + * true) or not (sets passed parameter to false). If a write is not allowed then callback + * will be triggered at some future point when write becomes possible again. + */ + NB_WRITE_INTEREST, + + /** + * Indicates if the request body has been fully read. + */ + REQUEST_BODY_FULLY_READ, + + /** + * Indicates that the container needs to trigger a call to onDataAvailable() for the registered non-blocking read + * listener. + */ + DISPATCH_READ, + + /** + * Indicates that the container needs to trigger a call to onWritePossible() for the registered non-blocking write + * listener. + */ + DISPATCH_WRITE, + + /** + * Execute any non-blocking dispatches that have been registered via {@link #DISPATCH_READ} or + * {@link #DISPATCH_WRITE}. Typically required when the non-blocking listeners are configured on a thread where the + * processing wasn't triggered by a read or write event on the socket. + */ + DISPATCH_EXECUTE, + + /** + * Is server push supported and allowed for the current request? + */ + IS_PUSH_SUPPORTED, + + /** + * Push a request on behalf of the client of the current request. + */ + PUSH_REQUEST, + + /** + * Are the request trailer fields ready to be read? Note that this returns true if it is known that request trailer + * fields are not supported so an empty collection of trailers can then be read. + */ + IS_TRAILER_FIELDS_READY, + + /** + * Are HTTP trailer fields supported for the current response? Note that once an HTTP/1.1 response has been + * committed, it will no longer support trailer fields. + */ + IS_TRAILER_FIELDS_SUPPORTED, + + /** + * Obtain the request identifier for this request as defined by the protocol in use. Note that some protocols do not + * define such an identifier. E.g. this will be Stream ID for HTTP/2. + */ + PROTOCOL_REQUEST_ID, + + /** + * Obtain the servlet connection instance for the network connection supporting the current request. + */ + SERVLET_CONNECTION +} diff --git a/java/org/apache/coyote/ActionHook.java b/java/org/apache/coyote/ActionHook.java new file mode 100644 index 0000000..7b54c17 --- /dev/null +++ b/java/org/apache/coyote/ActionHook.java @@ -0,0 +1,37 @@ +/* + * 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.coyote; + + +/** + * Action hook. Actions represent the callback mechanism used by coyote servlet containers to request operations on the + * coyote connectors. Some standard actions are defined in ActionCode, however custom actions are permitted. The param + * object can be used to pass and return information related with the action. This interface is typically implemented by + * ProtocolHandlers, and the param is usually a Request or Response object. + * + * @author Remy Maucherat + */ +public interface ActionHook { + + /** + * Send an action to the connector. + * + * @param actionCode Type of the action + * @param param Action parameter + */ + void action(ActionCode actionCode, Object param); +} diff --git a/java/org/apache/coyote/Adapter.java b/java/org/apache/coyote/Adapter.java new file mode 100644 index 0000000..f85b1f2 --- /dev/null +++ b/java/org/apache/coyote/Adapter.java @@ -0,0 +1,82 @@ +/* + * 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.coyote; + +import org.apache.tomcat.util.net.SocketEvent; + +/** + * Adapter. This represents the entry point in a coyote-based servlet container. + * + * @author Remy Maucherat + * + * @see ProtocolHandler + */ +public interface Adapter { + + /** + * Call the service method, and notify all listeners + * + * @param req The request object + * @param res The response object + * + * @exception Exception if an error happens during handling of the request. Common errors are: + *

      + *
    • IOException if an input/output error occurs and we are processing an included + * servlet (otherwise it is swallowed and handled by the top level error handler mechanism) + *
    • ServletException if a servlet throws an exception and we are processing an included + * servlet (otherwise it is swallowed and handled by the top level error handler mechanism) + *
    + * Tomcat should be able to handle and log any other exception ( including runtime + * exceptions ) + */ + void service(Request req, Response res) throws Exception; + + /** + * Prepare the given request/response for processing. This method requires that the request object has been + * populated with the information available from the HTTP headers. + * + * @param req The request object + * @param res The response object + * + * @return true if processing can continue, otherwise false in which case an appropriate + * error will have been set on the response + * + * @throws Exception If the processing fails unexpectedly + */ + boolean prepare(Request req, Response res) throws Exception; + + boolean asyncDispatch(Request req, Response res, SocketEvent status) throws Exception; + + void log(Request req, Response res, long time); + + /** + * Assert that request and response have been recycled. If they have not then log a warning and force a recycle. + * This method is called as a safety check when a processor is being recycled and may be returned to a pool for + * reuse. + * + * @param req Request + * @param res Response + */ + void checkRecycled(Request req, Response res); + + /** + * Provide the name of the domain to use to register MBeans for components associated with the connector. + * + * @return The MBean domain name + */ + String getDomain(); +} diff --git a/java/org/apache/coyote/AsyncContextCallback.java b/java/org/apache/coyote/AsyncContextCallback.java new file mode 100644 index 0000000..8d359c6 --- /dev/null +++ b/java/org/apache/coyote/AsyncContextCallback.java @@ -0,0 +1,47 @@ +/* + * 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.coyote; + +/** + * Provides a mechanism for the Coyote connectors to communicate with the {@link jakarta.servlet.AsyncContext}. It is + * implemented in this manner so that the org.apache.coyote package does not have a dependency on the + * org.apache.catalina package. + */ +public interface AsyncContextCallback { + void fireOnComplete(); + + /** + * Reports if the web application associated with this async request is available. + * + * @return {@code true} if the associated web application is available, otherwise {@code false} + */ + boolean isAvailable(); + + /** + * Used to notify the Context that async processing has started. Specifically, for the counting of in-progress async + * requests to work correctly, this must be called exactly once every time the {@link AsyncStateMachine} transitions + * from DISPATCHED to any other state. + */ + void incrementInProgressAsyncCount(); + + /** + * Used to notify the Context that async processing has ended. Specifically, for the counting of in-progress async + * requests to work correctly, this must be called exactly once every time the {@link AsyncStateMachine} transitions + * to DISPATCHED from any other state. + */ + void decrementInProgressAsyncCount(); +} diff --git a/java/org/apache/coyote/AsyncStateMachine.java b/java/org/apache/coyote/AsyncStateMachine.java new file mode 100644 index 0000000..465971a --- /dev/null +++ b/java/org/apache/coyote/AsyncStateMachine.java @@ -0,0 +1,529 @@ +/* + * 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.coyote; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.PrivilegedGetTccl; +import org.apache.tomcat.util.security.PrivilegedSetTccl; + +/** + * Manages the state transitions for async requests. + * + *
    + * The internal states that are used are:
    + * DISPATCHED       - Standard request. Not in Async mode.
    + * STARTING         - ServletRequest.startAsync() has been called from
    + *                    Servlet.service() but service() has not exited.
    + * STARTED          - ServletRequest.startAsync() has been called from
    + *                    Servlet.service() and service() has exited.
    + * READ_WRITE_OP    - Performing an asynchronous read or write.
    + * MUST_COMPLETE    - ServletRequest.startAsync() followed by complete() have
    + *                    been called during a single Servlet.service() method. The
    + *                    complete() will be processed as soon as Servlet.service()
    + *                    exits.
    + * COMPLETE_PENDING - ServletRequest.startAsync() has been called from
    + *                    Servlet.service() but, before service() exited, complete()
    + *                    was called from another thread. The complete() will
    + *                    be processed as soon as Servlet.service() exits.
    + * COMPLETING       - The call to complete() was made once the request was in
    + *                    the STARTED state.
    + * TIMING_OUT       - The async request has timed out and is waiting for a call
    + *                    to complete() or dispatch(). If that isn't made, the error
    + *                    state will be entered.
    + * MUST_DISPATCH    - ServletRequest.startAsync() followed by dispatch() have
    + *                    been called during a single Servlet.service() method. The
    + *                    dispatch() will be processed as soon as Servlet.service()
    + *                    exits.
    + * DISPATCH_PENDING - ServletRequest.startAsync() has been called from
    + *                    Servlet.service() but, before service() exited, dispatch()
    + *                    was called from another thread. The dispatch() will
    + *                    be processed as soon as Servlet.service() exits.
    + * DISPATCHING      - The dispatch is being processed.
    + * MUST_ERROR       - ServletRequest.startAsync() has been called from
    + *                    Servlet.service() but, before service() exited, an I/O
    + *                    error occurred on another thread. The container will
    + *                    perform the necessary error handling when
    + *                    Servlet.service() exits.
    + * ERROR            - Something went wrong.
    + *
    + *
    + * The valid state transitions are:
    + *
    + *                  post()                                        dispatched()
    + *    |-------»------------------»---------|    |-------«-----------------------«-----|
    + *    |                                    |    |                                     |
    + *    |                                    |    |        post()                       |
    + *    |               post()              \|/  \|/       dispatched()                 |
    + *    |           |-----»----------------»DISPATCHED«-------------«-------------|     |
    + *    |           |                          | /|\ |                            |     |
    + *    |           |              startAsync()|  |--|timeout()                   |     |
    + *    ^           |                          |                                  |     |
    + *    |           |        complete()        |                  dispatch()      ^     |
    + *    |           |   |--«---------------«-- | ---«--MUST_ERROR--»-----|        |     |
    + *    |           |   |                      |         /|\             |        |     |
    + *    |           ^   |                      |          |              |        |     |
    + *    |           |   |                      |    /-----|error()       |        |     |
    + *    |           |   |                      |   /                     |        ^     |
    + *    |           |  \|/  ST-complete()     \|/ /   ST-dispatch()     \|/       |     |
    + *    |    MUST_COMPLETE«--------«--------STARTING--------»---------»MUST_DISPATCH    |
    + *    |                                    / | \                                      |
    + *    |                                   /  |  \                                     |
    + *    |                    OT-complete() /   |   \    OT-dispatch()                   |
    + *    |   COMPLETE_PENDING«------«------/    |    \-------»---------»DISPATCH_PENDING |
    + *    |        |      /|\                    |                       /|\ |            |
    + *    |        |       |                     |                        |  |post()      |
    + *    |        |       |OT-complete()        |           OT-dispatch()|  |            |
    + *    |        |       |---------«-------«---|---«--\                 |  |            |
    + *    |        |                             |       \                |  |            |
    + *    |        |         /-------«-------«-- | --«---READ_WRITE--»----|  |            |
    + *    |        |        / ST-complete()      |        /  /|\  \          |            |
    + *    |        |       /                     | post()/   /     \         |            |
    + *    |        |      /                      |      /   /       \        |            |
    + *    |        |     /                       |     /   /         \       |            |
    + *    |        |    /                        |    /   /           \      |            |
    + *    |        |   /                         |   |   /             \     |            |
    + *    |        |  /                          |   |  /  ST-dispatch()\    |            |
    + *    |        |  |                          |   | |                 \   |            |
    + *    |  post()|  |  timeout()         post()|   | |asyncOperation()  \  |  timeout() |
    + *    |        |  |  |--|                    |   | |                  |  |    |--|    |
    + *    |       \|/\|/\|/ |     complete()    \|/ \|/|   dispatch()    \|/\|/  \|/ |    |
    + *    |--«-----COMPLETING«--------«----------STARTED--------»---------»DISPATCHING----|
    + *            /|\  /|\                       | /|\ |                       /|\ /|\
    + *             |    |                        |  |--|                        |   |
    + *             |    |               timeout()|  post()                      |   |
    + *             |    |                        |                              |   |
    + *             |    |       complete()      \|/         dispatch()          |   |
    + *             |    |------------«-------TIMING_OUT--------»----------------|   |
    + *             |                                                                |
    + *             |            complete()                     dispatch()           |
    + *             |---------------«-----------ERROR--------------»-----------------|
    + *
    + *
    + * Notes: * For clarity, the transitions to ERROR which are valid from every state apart from
    + *          STARTING are not shown.
    + *        * All transitions may happen on either the Servlet.service() thread (ST) or on any
    + *          other thread (OT) unless explicitly marked.
    + * 
    + */ +class AsyncStateMachine { + + private static final Log log = LogFactory.getLog(AsyncStateMachine.class); + private static final StringManager sm = StringManager.getManager(AsyncStateMachine.class); + + private enum AsyncState { + DISPATCHED(false, false, false, false), + STARTING(true, true, false, false), + STARTED(true, true, false, false), + MUST_COMPLETE(true, true, true, false), + COMPLETE_PENDING(true, true, false, false), + COMPLETING(true, false, true, false), + TIMING_OUT(true, true, false, false), + MUST_DISPATCH(true, true, false, true), + DISPATCH_PENDING(true, true, false, false), + DISPATCHING(true, false, false, true), + READ_WRITE_OP(true, true, false, false), + MUST_ERROR(true, true, false, false), + ERROR(true, true, false, false); + + private final boolean isAsync; + private final boolean isStarted; + private final boolean isCompleting; + private final boolean isDispatching; + + AsyncState(boolean isAsync, boolean isStarted, boolean isCompleting, boolean isDispatching) { + this.isAsync = isAsync; + this.isStarted = isStarted; + this.isCompleting = isCompleting; + this.isDispatching = isDispatching; + } + + boolean isAsync() { + return isAsync; + } + + boolean isStarted() { + return isStarted; + } + + boolean isDispatching() { + return isDispatching; + } + + boolean isCompleting() { + return isCompleting; + } + } + + + private volatile AsyncState state = AsyncState.DISPATCHED; + private volatile long lastAsyncStart = 0; + /* + * Tracks the current generation of async processing for this state machine. The generation is incremented every + * time async processing is started. The primary purpose of this is to enable Tomcat to detect and prevent attempts + * to process an event for a previous generation with the current generation as processing such an event usually + * ends badly: e.g. CVE-2018-8037. + */ + private final AtomicLong generation = new AtomicLong(0); + /* + * Error processing should only be triggered once per async generation. This field tracks whether the async + * processing has entered the error state during this async cycle. + * + * Guarded by this + */ + private boolean hasProcessedError = false; + + // Need this to fire listener on complete + private AsyncContextCallback asyncCtxt = null; + private final AbstractProcessor processor; + + + AsyncStateMachine(AbstractProcessor processor) { + this.processor = processor; + } + + + boolean isAsync() { + return state.isAsync(); + } + + boolean isAsyncDispatching() { + return state.isDispatching(); + } + + boolean isAsyncStarted() { + return state.isStarted(); + } + + boolean isAsyncTimingOut() { + return state == AsyncState.TIMING_OUT; + } + + boolean isAsyncError() { + return state == AsyncState.ERROR; + } + + boolean isCompleting() { + return state.isCompleting(); + } + + /** + * Obtain the time that this connection last transitioned to async processing. + * + * @return The time (as returned by {@link System#currentTimeMillis()}) that this connection last transitioned to + * async + */ + long getLastAsyncStart() { + return lastAsyncStart; + } + + long getCurrentGeneration() { + return generation.get(); + } + + synchronized void asyncStart(AsyncContextCallback asyncCtxt) { + if (state == AsyncState.DISPATCHED) { + generation.incrementAndGet(); + updateState(AsyncState.STARTING); + // Note: In this instance, caller is responsible for calling + // asyncCtxt.incrementInProgressAsyncCount() as that allows simpler + // error handling. + this.asyncCtxt = asyncCtxt; + lastAsyncStart = System.currentTimeMillis(); + } else { + throw new IllegalStateException(sm.getString("asyncStateMachine.invalidAsyncState", "asyncStart()", state)); + } + } + + synchronized void asyncOperation() { + if (state == AsyncState.STARTED) { + updateState(AsyncState.READ_WRITE_OP); + } else { + throw new IllegalStateException( + sm.getString("asyncStateMachine.invalidAsyncState", "asyncOperation()", state)); + } + } + + /* + * Async has been processed. Whether or not to enter a long poll depends on current state. For example, as per + * SRV.2.3.3.3 can now process calls to complete() or dispatch(). + */ + synchronized SocketState asyncPostProcess() { + if (state == AsyncState.COMPLETE_PENDING) { + clearNonBlockingListeners(); + updateState(AsyncState.COMPLETING); + return SocketState.ASYNC_END; + } else if (state == AsyncState.DISPATCH_PENDING) { + clearNonBlockingListeners(); + updateState(AsyncState.DISPATCHING); + return SocketState.ASYNC_END; + } else if (state == AsyncState.STARTING || state == AsyncState.READ_WRITE_OP) { + updateState(AsyncState.STARTED); + return SocketState.LONG; + } else if (state == AsyncState.MUST_COMPLETE || state == AsyncState.COMPLETING) { + asyncCtxt.fireOnComplete(); + updateState(AsyncState.DISPATCHED); + asyncCtxt.decrementInProgressAsyncCount(); + return SocketState.ASYNC_END; + } else if (state == AsyncState.MUST_DISPATCH) { + updateState(AsyncState.DISPATCHING); + return SocketState.ASYNC_END; + } else if (state == AsyncState.DISPATCHING) { + asyncCtxt.fireOnComplete(); + updateState(AsyncState.DISPATCHED); + asyncCtxt.decrementInProgressAsyncCount(); + return SocketState.ASYNC_END; + } else if (state == AsyncState.STARTED) { + // This can occur if an async listener does a dispatch to an async + // servlet during onTimeout + return SocketState.LONG; + } else { + throw new IllegalStateException( + sm.getString("asyncStateMachine.invalidAsyncState", "asyncPostProcess()", state)); + } + } + + + synchronized boolean asyncComplete() { + Request request = processor.getRequest(); + if ((request == null || !request.isRequestThread()) && + (state == AsyncState.STARTING || state == AsyncState.READ_WRITE_OP)) { + updateState(AsyncState.COMPLETE_PENDING); + return false; + } + + clearNonBlockingListeners(); + boolean triggerDispatch = false; + if (state == AsyncState.STARTING || state == AsyncState.MUST_ERROR) { + // Processing is on a container thread so no need to transfer + // processing to a new container thread + updateState(AsyncState.MUST_COMPLETE); + } else if (state == AsyncState.STARTED) { + updateState(AsyncState.COMPLETING); + // A dispatch to a container thread is always required. + // If on a non-container thread, need to get back onto a container + // thread to complete the processing. + // If on a container thread the current request/response are not the + // request/response associated with the AsyncContext so need a new + // container thread to process the different request/response. + triggerDispatch = true; + } else if (state == AsyncState.READ_WRITE_OP || state == AsyncState.TIMING_OUT || state == AsyncState.ERROR) { + // Read/write operations can happen on or off a container thread but + // while in this state the call to listener that triggers the + // read/write will be in progress on a container thread. + // Processing of timeouts and errors can happen on or off a + // container thread (on is much more likely) but while in this state + // the call that triggers the timeout will be in progress on a + // container thread. + // The socket will be added to the poller when the container thread + // exits the AbstractConnectionHandler.process() method so don't do + // a dispatch here which would add it to the poller a second time. + updateState(AsyncState.COMPLETING); + } else { + throw new IllegalStateException( + sm.getString("asyncStateMachine.invalidAsyncState", "asyncComplete()", state)); + } + return triggerDispatch; + } + + + synchronized boolean asyncTimeout() { + if (state == AsyncState.STARTED) { + updateState(AsyncState.TIMING_OUT); + return true; + } else if (state == AsyncState.COMPLETING || state == AsyncState.DISPATCHING || + state == AsyncState.DISPATCHED) { + // NOOP - App called complete() or dispatch() between the the + // timeout firing and execution reaching this point + return false; + } else { + throw new IllegalStateException( + sm.getString("asyncStateMachine.invalidAsyncState", "asyncTimeout()", state)); + } + } + + + synchronized boolean asyncDispatch() { + Request request = processor.getRequest(); + if ((request == null || !request.isRequestThread()) && + (state == AsyncState.STARTING || state == AsyncState.READ_WRITE_OP)) { + updateState(AsyncState.DISPATCH_PENDING); + return false; + } + + clearNonBlockingListeners(); + boolean triggerDispatch = false; + if (state == AsyncState.STARTING || state == AsyncState.MUST_ERROR) { + // Processing is on a container thread so no need to transfer + // processing to a new container thread + updateState(AsyncState.MUST_DISPATCH); + } else if (state == AsyncState.STARTED) { + updateState(AsyncState.DISPATCHING); + // A dispatch to a container thread is always required. + // If on a non-container thread, need to get back onto a container + // thread to complete the processing. + // If on a container thread the current request/response are not the + // request/response associated with the AsyncContext so need a new + // container thread to process the different request/response. + triggerDispatch = true; + } else if (state == AsyncState.READ_WRITE_OP || state == AsyncState.TIMING_OUT || state == AsyncState.ERROR) { + // Read/write operations can happen on or off a container thread but + // while in this state the call to listener that triggers the + // read/write will be in progress on a container thread. + // Processing of timeouts and errors can happen on or off a + // container thread (on is much more likely) but while in this state + // the call that triggers the timeout will be in progress on a + // container thread. + // The socket will be added to the poller when the container thread + // exits the AbstractConnectionHandler.process() method so don't do + // a dispatch here which would add it to the poller a second time. + updateState(AsyncState.DISPATCHING); + } else { + throw new IllegalStateException( + sm.getString("asyncStateMachine.invalidAsyncState", "asyncDispatch()", state)); + } + return triggerDispatch; + } + + + synchronized void asyncDispatched() { + if (state == AsyncState.DISPATCHING || state == AsyncState.MUST_DISPATCH) { + updateState(AsyncState.DISPATCHED); + asyncCtxt.decrementInProgressAsyncCount(); + } else { + throw new IllegalStateException( + sm.getString("asyncStateMachine.invalidAsyncState", "asyncDispatched()", state)); + } + } + + + synchronized boolean asyncError() { + Request request = processor.getRequest(); + boolean containerThread = (request != null && request.isRequestThread()); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("asyncStateMachine.asyncError.start")); + } + + clearNonBlockingListeners(); + if (state == AsyncState.STARTING) { + updateState(AsyncState.MUST_ERROR); + } else { + if (hasProcessedError) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("asyncStateMachine.asyncError.skip")); + } + return false; + } + hasProcessedError = true; + if (state == AsyncState.DISPATCHED) { + // Async error handling has moved processing back into an async + // state. Need to increment in progress count as it will decrement + // when the async state is exited again. + asyncCtxt.incrementInProgressAsyncCount(); + updateState(AsyncState.ERROR); + } else { + updateState(AsyncState.ERROR); + } + } + + // Return true for non-container threads to trigger a dispatch + return !containerThread; + } + + + synchronized void asyncRun(Runnable runnable) { + if (state == AsyncState.STARTING || state == AsyncState.STARTED || state == AsyncState.READ_WRITE_OP) { + // Execute the runnable using a container thread from the + // Connector's thread pool. Use a wrapper to prevent a memory leak + ClassLoader oldCL; + Thread currentThread = Thread.currentThread(); + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedAction pa = new PrivilegedGetTccl(currentThread); + oldCL = AccessController.doPrivileged(pa); + } else { + oldCL = currentThread.getContextClassLoader(); + } + try { + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedAction pa = new PrivilegedSetTccl(currentThread, this.getClass().getClassLoader()); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(this.getClass().getClassLoader()); + } + + processor.execute(runnable); + } finally { + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedAction pa = new PrivilegedSetTccl(currentThread, oldCL); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(oldCL); + } + } + } else { + throw new IllegalStateException(sm.getString("asyncStateMachine.invalidAsyncState", "asyncRun()", state)); + } + } + + + synchronized boolean isAvailable() { + if (asyncCtxt == null) { + // Async processing has probably been completed in another thread. + // Trigger a timeout to make sure the Processor is cleaned up. + return false; + } + return asyncCtxt.isAvailable(); + } + + + synchronized void recycle() { + // Use lastAsyncStart to determine if this instance has been used since + // it was last recycled. If it hasn't there is no need to recycle again + // which saves the relatively expensive call to notifyAll() + if (lastAsyncStart == 0) { + return; + } + // Ensure in case of error that any non-container threads that have been + // paused are unpaused. + notifyAll(); + asyncCtxt = null; + state = AsyncState.DISPATCHED; + lastAsyncStart = 0; + hasProcessedError = false; + } + + + private void clearNonBlockingListeners() { + processor.getRequest().listener = null; + processor.getRequest().getResponse().listener = null; + } + + + private synchronized void updateState(AsyncState newState) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("asyncStateMachine.stateChange", state, newState)); + } + state = newState; + } +} diff --git a/java/org/apache/coyote/BadRequestException.java b/java/org/apache/coyote/BadRequestException.java new file mode 100644 index 0000000..d02f1be --- /dev/null +++ b/java/org/apache/coyote/BadRequestException.java @@ -0,0 +1,68 @@ +/* + * 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.coyote; + +import java.io.IOException; + +/** + * Extend IOException to identify it as being caused by a bad request from a remote client. + */ +public class BadRequestException extends IOException { + + private static final long serialVersionUID = 1L; + + + // ------------------------------------------------------------ Constructors + + /** + * Construct a new BadRequestException with no other information. + */ + public BadRequestException() { + super(); + } + + + /** + * Construct a new BadRequestException for the specified message. + * + * @param message Message describing this exception + */ + public BadRequestException(String message) { + super(message); + } + + + /** + * Construct a new BadRequestException for the specified throwable. + * + * @param throwable Throwable that caused this exception + */ + public BadRequestException(Throwable throwable) { + super(throwable); + } + + + /** + * Construct a new BadRequestException for the specified message and throwable. + * + * @param message Message describing this exception + * @param throwable Throwable that caused this exception + */ + public BadRequestException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/java/org/apache/coyote/CloseNowException.java b/java/org/apache/coyote/CloseNowException.java new file mode 100644 index 0000000..a8bb0ae --- /dev/null +++ b/java/org/apache/coyote/CloseNowException.java @@ -0,0 +1,50 @@ +/* + * 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.coyote; + +import java.io.IOException; + +/** + * This exception is thrown to signal to the Tomcat internals that an error has occurred that requires the connection to + * be closed. For multiplexed protocols such as HTTP/2, this means the channel must be closed but the connection can + * continue. For non-multiplexed protocols, the connection must be closed. It corresponds to + * {@link ErrorState#CLOSE_NOW}. + */ +public class CloseNowException extends IOException { + + private static final long serialVersionUID = 1L; + + + public CloseNowException() { + super(); + } + + + public CloseNowException(String message, Throwable cause) { + super(message, cause); + } + + + public CloseNowException(String message) { + super(message); + } + + + public CloseNowException(Throwable cause) { + super(cause); + } +} diff --git a/java/org/apache/coyote/CompressionConfig.java b/java/org/apache/coyote/CompressionConfig.java new file mode 100644 index 0000000..b4bd64c --- /dev/null +++ b/java/org/apache/coyote/CompressionConfig.java @@ -0,0 +1,313 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.regex.Pattern; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.ResponseUtil; +import org.apache.tomcat.util.http.parser.AcceptEncoding; +import org.apache.tomcat.util.http.parser.TokenList; +import org.apache.tomcat.util.res.StringManager; + +public class CompressionConfig { + + private static final Log log = LogFactory.getLog(CompressionConfig.class); + private static final StringManager sm = StringManager.getManager(CompressionConfig.class); + + private int compressionLevel = 0; + private Pattern noCompressionUserAgents = null; + private String compressibleMimeType = "text/html,text/xml,text/plain,text/css," + + "text/javascript,application/javascript,application/json,application/xml"; + private String[] compressibleMimeTypes = null; + private int compressionMinSize = 2048; + + + /** + * Set compression level. + * + * @param compression One of on, force, off or the minimum compression size + * in bytes which implies on + */ + public void setCompression(String compression) { + if (compression.equals("on")) { + this.compressionLevel = 1; + } else if (compression.equals("force")) { + this.compressionLevel = 2; + } else if (compression.equals("off")) { + this.compressionLevel = 0; + } else { + try { + // Try to parse compression as an int, which would give the + // minimum compression size + setCompressionMinSize(Integer.parseInt(compression)); + this.compressionLevel = 1; + } catch (Exception e) { + this.compressionLevel = 0; + } + } + } + + + /** + * Return compression level. + * + * @return The current compression level in string form (off/on/force) + */ + public String getCompression() { + switch (compressionLevel) { + case 0: + return "off"; + case 1: + return "on"; + case 2: + return "force"; + } + return "off"; + } + + + public int getCompressionLevel() { + return compressionLevel; + } + + + /** + * Obtain the String form of the regular expression that defines the user agents to not use gzip with. + * + * @return The regular expression as a String + */ + public String getNoCompressionUserAgents() { + if (noCompressionUserAgents == null) { + return null; + } else { + return noCompressionUserAgents.toString(); + } + } + + + public Pattern getNoCompressionUserAgentsPattern() { + return noCompressionUserAgents; + } + + + /** + * Set no compression user agent pattern. Regular expression as supported by {@link Pattern}. e.g.: + * gorilla|desesplorer|tigrus. + * + * @param noCompressionUserAgents The regular expression for user agent strings for which compression should not be + * applied + */ + public void setNoCompressionUserAgents(String noCompressionUserAgents) { + if (noCompressionUserAgents == null || noCompressionUserAgents.length() == 0) { + this.noCompressionUserAgents = null; + } else { + this.noCompressionUserAgents = Pattern.compile(noCompressionUserAgents); + } + } + + + public String getCompressibleMimeType() { + return compressibleMimeType; + } + + + public void setCompressibleMimeType(String valueS) { + compressibleMimeType = valueS; + compressibleMimeTypes = null; + } + + + public String[] getCompressibleMimeTypes() { + String[] result = compressibleMimeTypes; + if (result != null) { + return result; + } + List values = new ArrayList<>(); + StringTokenizer tokens = new StringTokenizer(compressibleMimeType, ","); + while (tokens.hasMoreTokens()) { + String token = tokens.nextToken().trim(); + if (token.length() > 0) { + values.add(token); + } + } + result = values.toArray(new String[0]); + compressibleMimeTypes = result; + return result; + } + + + public int getCompressionMinSize() { + return compressionMinSize; + } + + + /** + * Set Minimum size to trigger compression. + * + * @param compressionMinSize The minimum content length required for compression in bytes + */ + public void setCompressionMinSize(int compressionMinSize) { + this.compressionMinSize = compressionMinSize; + } + + + /** + * Determines if compression should be enabled for the given response and if it is, sets any necessary headers to + * mark it as such. + * + * @param request The request that triggered the response + * @param response The response to consider compressing + * + * @return {@code true} if compression was enabled for the given response, otherwise {@code false} + */ + public boolean useCompression(Request request, Response response) { + // Check if compression is enabled + if (compressionLevel == 0) { + return false; + } + + MimeHeaders responseHeaders = response.getMimeHeaders(); + + // Check if content is not already compressed + MessageBytes contentEncodingMB = responseHeaders.getValue("Content-Encoding"); + if (contentEncodingMB != null) { + // Content-Encoding values are ordered but order is not important + // for this check so use a Set rather than a List + Set tokens = new HashSet<>(); + try { + TokenList.parseTokenList(responseHeaders.values("Content-Encoding"), tokens); + } catch (IOException e) { + // Because we are using StringReader, any exception here is a + // Tomcat bug. + log.warn(sm.getString("compressionConfig.ContentEncodingParseFail"), e); + return false; + } + if (tokens.contains("gzip") || tokens.contains("br")) { + return false; + } + } + + // If force mode, the length and MIME type checks are skipped + if (compressionLevel != 2) { + // Check if the response is of sufficient length to trigger the compression + long contentLength = response.getContentLengthLong(); + if (contentLength != -1 && contentLength < compressionMinSize) { + return false; + } + + // Check for compatible MIME-TYPE + String[] compressibleMimeTypes = getCompressibleMimeTypes(); + if (compressibleMimeTypes != null && + !startsWithStringArray(compressibleMimeTypes, response.getContentType())) { + return false; + } + } + + // Check if the resource has a strong ETag + String eTag = responseHeaders.getHeader("ETag"); + if (eTag != null && !eTag.trim().startsWith("W/")) { + // Has an ETag that doesn't start with "W/..." so it must be a + // strong ETag + return false; + } + + // If processing reaches this far, the response might be compressed. + // Therefore, set the Vary header to keep proxies happy + ResponseUtil.addVaryFieldName(responseHeaders, "accept-encoding"); + + // Check if user-agent supports gzip encoding + // Only interested in whether gzip encoding is supported. Other + // encodings and weights can be ignored. + Enumeration headerValues = request.getMimeHeaders().values("accept-encoding"); + boolean foundGzip = false; + while (!foundGzip && headerValues.hasMoreElements()) { + List acceptEncodings = null; + try { + acceptEncodings = AcceptEncoding.parse(new StringReader(headerValues.nextElement())); + } catch (IOException ioe) { + // If there is a problem reading the header, disable compression + return false; + } + + for (AcceptEncoding acceptEncoding : acceptEncodings) { + if ("gzip".equalsIgnoreCase(acceptEncoding.getEncoding())) { + foundGzip = true; + break; + } + } + } + + if (!foundGzip) { + return false; + } + + // If force mode, the browser checks are skipped + if (compressionLevel != 2) { + // Check for incompatible Browser + Pattern noCompressionUserAgents = this.noCompressionUserAgents; + if (noCompressionUserAgents != null) { + MessageBytes userAgentValueMB = request.getMimeHeaders().getValue("user-agent"); + if (userAgentValueMB != null) { + String userAgentValue = userAgentValueMB.toString(); + if (noCompressionUserAgents.matcher(userAgentValue).matches()) { + return false; + } + } + } + } + + // All checks have passed. Compression is enabled. + + // Compressed content length is unknown so mark it as such. + response.setContentLength(-1); + // Configure the content encoding for compressed content + responseHeaders.setValue("Content-Encoding").setString("gzip"); + + return true; + } + + + /** + * Checks if any entry in the string array starts with the specified value + * + * @param sArray the StringArray + * @param value string + */ + private static boolean startsWithStringArray(String sArray[], String value) { + if (value == null) { + return false; + } + for (String s : sArray) { + if (value.startsWith(s)) { + return true; + } + } + return false; + } +} diff --git a/java/org/apache/coyote/Constants.java b/java/org/apache/coyote/Constants.java new file mode 100644 index 0000000..7e4074c --- /dev/null +++ b/java/org/apache/coyote/Constants.java @@ -0,0 +1,98 @@ +/* + * 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.coyote; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Constants. + * + * @author Remy Maucherat + */ +public final class Constants { + + public static final Charset DEFAULT_URI_CHARSET = StandardCharsets.UTF_8; + public static final Charset DEFAULT_BODY_CHARSET = StandardCharsets.ISO_8859_1; + + public static final int MAX_NOTES = 32; + + + // Request states + public static final int STAGE_NEW = 0; + public static final int STAGE_PARSE = 1; + public static final int STAGE_PREPARE = 2; + public static final int STAGE_SERVICE = 3; + public static final int STAGE_ENDINPUT = 4; + public static final int STAGE_ENDOUTPUT = 5; + public static final int STAGE_KEEPALIVE = 6; + public static final int STAGE_ENDED = 7; + + // Default protocol settings + public static final int DEFAULT_CONNECTION_LINGER = -1; + public static final boolean DEFAULT_TCP_NO_DELAY = true; + + /** + * Has security been turned on? + */ + public static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); + + + /** + * The request attribute that is set to the value of {@code Boolean.TRUE} if connector processing this request + * supports use of sendfile. + */ + public static final String SENDFILE_SUPPORTED_ATTR = "org.apache.tomcat.sendfile.support"; + + + /** + * The request attribute that can be used by a servlet to pass to the connector the name of the file that is to be + * served by sendfile. The value should be {@code String} that is {@code File.getCanonicalPath()} of the file to be + * served. + */ + public static final String SENDFILE_FILENAME_ATTR = "org.apache.tomcat.sendfile.filename"; + + + /** + * The request attribute that can be used by a servlet to pass to the connector the start offset of the part of a + * file that is to be served by sendfile. The value should be {@code java.lang.Long}. To serve complete file the + * value should be {@code Long.valueOf(0)}. + */ + public static final String SENDFILE_FILE_START_ATTR = "org.apache.tomcat.sendfile.start"; + + + /** + * The request attribute that can be used by a servlet to pass to the connector the end offset (not including) of + * the part of a file that is to be served by sendfile. The value should be {@code java.lang.Long}. To serve + * complete file the value should be equal to the length of the file. + */ + public static final String SENDFILE_FILE_END_ATTR = "org.apache.tomcat.sendfile.end"; + + + /** + * The request attribute set by the RemoteIpFilter, RemoteIpValve (and may be set by other similar components) that + * identifies for the connector the remote IP address claimed to be associated with this request when a request is + * received via one or more proxies. It is typically provided via the X-Forwarded-For HTTP header. + */ + public static final String REMOTE_ADDR_ATTRIBUTE = "org.apache.tomcat.remoteAddr"; + + /** + * The request attribute set by the RemoteIpFilter, RemoteIpValve (and may be set by other similar components) that + * identifies for the connector the connection peer IP address. + */ + public static final String PEER_ADDR_ATTRIBUTE = "org.apache.tomcat.peerAddr"; +} diff --git a/java/org/apache/coyote/ContinueResponseTiming.java b/java/org/apache/coyote/ContinueResponseTiming.java new file mode 100644 index 0000000..20ec0ae --- /dev/null +++ b/java/org/apache/coyote/ContinueResponseTiming.java @@ -0,0 +1,78 @@ +/* + * 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.coyote; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Defines timing options for responding to requests that contain a '100-continue' expectations. + */ +public enum ContinueResponseTiming { + + /** + * Tomcat will automatically send the 100 intermediate response before sending the request to the servlet. + */ + IMMEDIATELY("immediately"), + + /** + * Send the 100 intermediate response only when the servlet attempts to read the request's body by either: + *
      + *
    • calling read on the InputStream returned by HttpServletRequest.getInputStream
    • + *
    • calling read on the BufferedReader returned by HttpServletRequest.getReader
    • + *
    + * This allows the servlet to process the request headers and possibly respond before reading the request body. + */ + ON_REQUEST_BODY_READ("onRead"), + + + /** + * Internal use only. Used to indicate that the 100 intermediate response should be sent if possible regardless of + * the current configuration. + */ + ALWAYS("always"); + + + private static final StringManager sm = StringManager.getManager(ContinueResponseTiming.class); + + public static ContinueResponseTiming fromString(String value) { + /* + * Do this for two reasons: - Not all of the Enum values are intended to be used in configuration - the naming + * convention for Enum constants and configuration values - is not consistent + */ + if (IMMEDIATELY.toString().equalsIgnoreCase(value)) { + return IMMEDIATELY; + } else if (ON_REQUEST_BODY_READ.toString().equalsIgnoreCase(value)) { + return ON_REQUEST_BODY_READ; + } else { + throw new IllegalArgumentException(sm.getString("continueResponseTiming.invalid", value)); + } + } + + + private final String configValue; + + + ContinueResponseTiming(String configValue) { + this.configValue = configValue; + } + + + @Override + public String toString() { + return configValue; + } +} diff --git a/java/org/apache/coyote/ErrorState.java b/java/org/apache/coyote/ErrorState.java new file mode 100644 index 0000000..9724e65 --- /dev/null +++ b/java/org/apache/coyote/ErrorState.java @@ -0,0 +1,86 @@ +/* + * 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.coyote; + +public enum ErrorState { + + /** + * Not in an error state. + */ + NONE(false, 0, true, true), + + /** + * The current request/response is in an error state and while it is safe to complete the current response it is not + * safe to continue to use the existing connection which must be closed once the response has been completed. For + * multiplexed protocols, the channel must be closed when the current request/response completes but the connection + * may continue. + */ + CLOSE_CLEAN(true, 1, true, true), + + /** + * The current request/response is in an error state and it is not safe to continue to use them. For multiplexed + * protocols (such as HTTP/2) the stream/channel must be closed immediately but the connection may continue. For + * non-multiplexed protocols (AJP, HTTP/1.x) the current connection must be closed. + */ + CLOSE_NOW(true, 2, false, true), + + /** + * An error has been detected that impacts the underlying network connection. It is not safe to continue using the + * network connection which must be closed immediately. For multiplexed protocols (such as HTTP/2) this impacts all + * multiplexed channels. + */ + CLOSE_CONNECTION_NOW(true, 3, false, false); + + private final boolean error; + private final int severity; + private final boolean ioAllowed; + private final boolean connectionIoAllowed; + + ErrorState(boolean error, int severity, boolean ioAllowed, boolean connectionIoAllowed) { + this.error = error; + this.severity = severity; + this.ioAllowed = ioAllowed; + this.connectionIoAllowed = connectionIoAllowed; + } + + public boolean isError() { + return error; + } + + /** + * Compare this ErrorState with the provided ErrorState and return the most severe. + * + * @param input The error state to compare to this one + * + * @return The most severe error state from the the provided error state and this one + */ + public ErrorState getMostSevere(ErrorState input) { + if (input.severity > this.severity) { + return input; + } else { + return this; + } + } + + public boolean isIoAllowed() { + return ioAllowed; + } + + public boolean isConnectionIoAllowed() { + return connectionIoAllowed; + } +} diff --git a/java/org/apache/coyote/InputBuffer.java b/java/org/apache/coyote/InputBuffer.java new file mode 100644 index 0000000..d9c27ff --- /dev/null +++ b/java/org/apache/coyote/InputBuffer.java @@ -0,0 +1,50 @@ +/* + * 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.coyote; + +import java.io.IOException; + +import org.apache.tomcat.util.net.ApplicationBufferHandler; + +/** + * This class is only for internal use in the protocol implementation. All reading from Tomcat (or adapter) should be + * done using Request.doRead(). + */ +public interface InputBuffer { + + /** + * Read from the input stream into the ByteBuffer provided by the ApplicationBufferHandler. IMPORTANT: the current + * model assumes that the protocol will 'own' the ByteBuffer and return a pointer to it. + * + * @param handler ApplicationBufferHandler that provides the buffer to read data into. + * + * @return The number of bytes that have been added to the buffer or -1 for end of stream + * + * @throws IOException If an I/O error occurs reading from the input stream + */ + int doRead(ApplicationBufferHandler handler) throws IOException; + + + /** + * Obtain an estimate of the number of bytes that can be read without blocking. Typically, this will be the number + * of available bytes known to be buffered. + * + * @return The number of bytes that can be read without blocking + */ + int available(); + +} diff --git a/java/org/apache/coyote/LocalStrings.properties b/java/org/apache/coyote/LocalStrings.properties new file mode 100644 index 0000000..cffa960 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings.properties @@ -0,0 +1,73 @@ +# 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. + +abstractConnectionHandler.connectionsGet=Found processor [{0}] for socket [{1}] +abstractConnectionHandler.error=Error reading request, ignored +abstractConnectionHandler.ioexception.debug=IOExceptions are normal, ignored +abstractConnectionHandler.negotiatedProcessor.fail=Failed to create Processor for negotiated protocol [{0}] +abstractConnectionHandler.oome=Failed to complete processing of a request +abstractConnectionHandler.process=Processing socket [{0}] with status [{1}] +abstractConnectionHandler.processorCreate=Created new processor [{0}] +abstractConnectionHandler.processorPop=Popped processor [{0}] from cache +abstractConnectionHandler.protocolexception.debug=ProtocolExceptions are normal, ignored +abstractConnectionHandler.socketexception.debug=SocketExceptions are normal, ignored +abstractConnectionHandler.upgradeCreate=Created upgrade processor [{0}] for socket wrapper [{1}] + +abstractProcessor.asyncFail=Unable to write async data +abstractProcessor.fallToDebug=\n\ +\ Note: further occurrences of request parsing errors will be logged at DEBUG level. +abstractProcessor.hostInvalid=The host [{0}] is not valid +abstractProcessor.httpupgrade.notsupported=HTTP upgrade is not supported by this protocol +abstractProcessor.noExecute=Unable to transfer processing to a container thread because this Processor is not currently associated with a SocketWrapper +abstractProcessor.pushrequest.notsupported=Server push requests are not supported by this protocol +abstractProcessor.setErrorState=Error state [{0}] reported while processing request +abstractProcessor.socket.ssl=Exception getting SSL attributes + +abstractProtocol.closeConnectionsAwait=Waiting [{0}] milliseconds for existing connections to [{1}] to complete and close. +abstractProtocol.mbeanDeregistrationFailed=Failed to deregister MBean named [{0}] from MBean server [{1}] +abstractProtocol.processorRegisterError=Error registering request processor +abstractProtocol.processorUnregisterError=Error unregistering request processor +abstractProtocol.waitingProcessor.add=Adding processor [{0}] to waiting processors +abstractProtocol.waitingProcessor.remove=Removed processor [{0}] from waiting processors + +abstractProtocolHandler.asyncTimeoutError=Error processing async timeouts +abstractProtocolHandler.destroy=Destroying ProtocolHandler [{0}] +abstractProtocolHandler.init=Initializing ProtocolHandler [{0}] +abstractProtocolHandler.pause=Pausing ProtocolHandler [{0}] +abstractProtocolHandler.portOffset=ProtocolHandler [{0}] is configured with a base port of [{1}] and a port offset of [{2}] +abstractProtocolHandler.resume=Resuming ProtocolHandler [{0}] +abstractProtocolHandler.setAttribute=Set attribute [{0}] with value [{1}] +abstractProtocolHandler.start=Starting ProtocolHandler [{0}] +abstractProtocolHandler.stop=Stopping ProtocolHandler [{0}] + +asyncStateMachine.asyncError.skip=Ignoring call to asyncError() as it has already been called since async processing started +asyncStateMachine.asyncError.start=Starting to process call to asyncError() +asyncStateMachine.invalidAsyncState=Calling [{0}] is not valid for a request with Async state [{1}] +asyncStateMachine.stateChange=Changing async state from [{0}] to [{1}] + +compressionConfig.ContentEncodingParseFail=Failed to parse Content-Encoding header when checking to see if compression was already in use + +continueResponseTiming.invalid=The value [{0}] is not a valid configuration option for continueResponseTiming + +request.notAsync=It is only valid to switch to non-blocking IO within async processing or HTTP upgrade processing +request.nullReadListener=The listener passed to setReadListener() may not be null +request.readListenerSet=The non-blocking read listener has already been set + +response.encoding.invalid=The encoding [{0}] is not recognised by the JRE +response.noTrailers.notSupported=A trailer fields supplier may not be set for this response. Either the underlying protocol does not support trailer fields or the protocol requires that the supplier is set before the response is committed +response.notAsync=It is only valid to switch to non-blocking IO within async processing or HTTP upgrade processing +response.notNonBlocking=It is invalid to call isReady() when the response has not been put into non-blocking mode +response.nullWriteListener=The listener passed to setWriteListener() may not be null +response.writeListenerSet=The non-blocking write listener has already been set diff --git a/java/org/apache/coyote/LocalStrings_cs.properties b/java/org/apache/coyote/LocalStrings_cs.properties new file mode 100644 index 0000000..e485896 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_cs.properties @@ -0,0 +1,27 @@ +# 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. + +abstractConnectionHandler.processorPop=Získaný zpracovatel [{0}] z cache + +abstractProcessor.fallToDebug=\n\ +\ Poznámka: další výskyty chyb ze zpracování dotazu budou zalogovány na úrovni DEBUG. +abstractProcessor.hostInvalid=Host [{0}] je neplatný +abstractProcessor.httpupgrade.notsupported=HTTP upgrade není podporován tímto protokolem + +abstractProtocolHandler.init=Inicializace ProtocolHandler [{0}] + +asyncStateMachine.invalidAsyncState=Volání [{0}] není platné pro dotaz s Async stavem [{1}] + +response.writeListenerSet=Listener pro neblokující zápis již byl nastaven diff --git a/java/org/apache/coyote/LocalStrings_de.properties b/java/org/apache/coyote/LocalStrings_de.properties new file mode 100644 index 0000000..5f37d72 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_de.properties @@ -0,0 +1,29 @@ +# 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. + +abstractConnectionHandler.oome=Verarbeitung der Anfrage fehlgeschlagen +abstractConnectionHandler.processorPop=Prozessor [{0}] wurde aus dem Cache genommen. +abstractConnectionHandler.socketexception.debug=SocketExceptions sind normal, werden ignoriert + +abstractProcessor.fallToDebug=\n\ +\ Info: Weitere Vorkommen von Fehlern beim Parsen der Anfragen werden mit DEBUG Level ausgegeben +abstractProcessor.hostInvalid=Der Host [{0}] ist nicht gültig. +abstractProcessor.httpupgrade.notsupported=HTTP-Upgrade wird von diesem Protokol nicht unterstützt + +abstractProtocolHandler.init=Initialisiere ProtocolHandler[{0}] + +asyncStateMachine.invalidAsyncState=Der Aufruf von [{0}] ist nicht erlaubt, während der Request im Async-Status [{1}] ist + +response.writeListenerSet=Der Nicht-blockierende Schreib-Listener wurde bereits gesetzt diff --git a/java/org/apache/coyote/LocalStrings_es.properties b/java/org/apache/coyote/LocalStrings_es.properties new file mode 100644 index 0000000..c2fce07 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_es.properties @@ -0,0 +1,29 @@ +# 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. + +abstractConnectionHandler.error=Error leyendo requerimiento, ignorado +abstractConnectionHandler.processorPop=Sacando procesador [{0}] de la cache + +abstractProcessor.fallToDebug=\n\ +\ Nota: futuras ocurrencias de la solicitud the parseo de errores será logueada con nivel DEBUG. +abstractProcessor.hostInvalid=El host [{0}] no es válido\n +abstractProcessor.httpupgrade.notsupported=La actualización HTTP no esta soportada por esta protocol +abstractProcessor.socket.ssl=Excepción obteniendo atributos SSL + +abstractProtocolHandler.init=Inicializando el manejador de protocolo [{0}]\n + +asyncStateMachine.invalidAsyncState=Llamando [{0}] no es una solicitud válida en el estado [{1}]\n + +response.writeListenerSet=El escuchador de escritura no bloqueable ya ha sido establecido diff --git a/java/org/apache/coyote/LocalStrings_fr.properties b/java/org/apache/coyote/LocalStrings_fr.properties new file mode 100644 index 0000000..dbb80f6 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_fr.properties @@ -0,0 +1,73 @@ +# 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. + +abstractConnectionHandler.connectionsGet=Trouvé le processeur [{0}] pour le socket [{1}] +abstractConnectionHandler.error=Erreur de lecture de la requête, ignorée +abstractConnectionHandler.ioexception.debug=Les IOException sont normales et sont ignorées +abstractConnectionHandler.negotiatedProcessor.fail=Impossible de créer un processeur pour le protocole négocié [{0}] +abstractConnectionHandler.oome=Echec de la fin de traitement d'une requête +abstractConnectionHandler.process=Traitement du socket [{0}] avec le statut [{1}] +abstractConnectionHandler.processorCreate=Création d''un nouveau processeur [{0}] +abstractConnectionHandler.processorPop=Retrait du processeur [{0}] du cache +abstractConnectionHandler.protocolexception.debug=Les ProtocolExceptions sont normales et sont ignorées +abstractConnectionHandler.socketexception.debug=Les SocketException sont normales et sont ignorées +abstractConnectionHandler.upgradeCreate=Création du processeur pour l''upgrade [{0}] du wrapper du socket [{1}] + +abstractProcessor.asyncFail=Impossible d'écrire les données asynchrones +abstractProcessor.fallToDebug=\n\ +\ Note : les occurrences suivantes d'erreurs d'analyse de requête seront enregistrées au niveau DEBUG. +abstractProcessor.hostInvalid=L''hôte [{0}] n''est pas valide +abstractProcessor.httpupgrade.notsupported=La promotion (upgrade) HTTP n'est pas supporté par ce protocole +abstractProcessor.noExecute=Impossible de transférer l'exécution à un thread du conteneur parce que ce processeur n'est pas associé à un SocketWrapper +abstractProcessor.pushrequest.notsupported=Le requêtes push du serveur ne sont pas supportées par ce protocole +abstractProcessor.setErrorState=Etat d''erreur [{0}] lors du traitement de la requête +abstractProcessor.socket.ssl=Exception lors de l'obtention des attributs SSL + +abstractProtocol.closeConnectionsAwait=Attente de [{0}] millisecondes pour que les connections en cours vers [{1}] soient complètes et fermées +abstractProtocol.mbeanDeregistrationFailed=Erreur lors du désenregistrement du mbean [{0}] dans le serveur [{1}] +abstractProtocol.processorRegisterError=Erreur lors de l'enregistrement du processeur de requêtes +abstractProtocol.processorUnregisterError=Erreur lors du désenregistrement du processeur de requêtes +abstractProtocol.waitingProcessor.add=Ajout du processeur [{0}] au processeurs en attente +abstractProtocol.waitingProcessor.remove=Retrait du processeur [{0}] des processeurs en attente + +abstractProtocolHandler.asyncTimeoutError=Erreur de traitement du délai d'attente maximum en mode asynchrone +abstractProtocolHandler.destroy=Destruction du gestionnaire de protocole [{0}] +abstractProtocolHandler.init=Initialisation du gestionnaire de protocole [{0}] +abstractProtocolHandler.pause=Le gestionnaire de protocole [{0}] est mis en pause +abstractProtocolHandler.portOffset=Le gestionnaire de protocole [{0}] est configuré avec un port de base [{1}] et un offset de port [{2}] +abstractProtocolHandler.resume=Reprise du gestionnaire de protocole [{0}] +abstractProtocolHandler.setAttribute=Fixe l''attribut [{0}] avec la valeur [{1}] +abstractProtocolHandler.start=Démarrage du gestionnaire de protocole [{0}] +abstractProtocolHandler.stop=Arrêt du gestionnaire de protocole [{0}] + +asyncStateMachine.asyncError.skip=L'appel à asyncError() est ignoré car il a déjà été appelé depuis que le traitement asynchrone a commencé +asyncStateMachine.asyncError.start=Début du traitement de l'appel à asyncError() +asyncStateMachine.invalidAsyncState=L''appel à [{0}] n''est pas valide pour une requête dans l''état Async [{1}] +asyncStateMachine.stateChange=Changement de l''état async de [{0}] à [{1}] + +compressionConfig.ContentEncodingParseFail=Echec du traitement de l'en-tête Content-Encoding en vérifiant si la compression était déjà utilisée + +continueResponseTiming.invalid=La valeur [{0}] n''est pas valide pour continueResponseTiming + +request.notAsync=Il n'est possible de passer en mode d'entrée-sorties non bloquantes que lors de traitements asynchrones ou après mise à niveau depuis HTTP +request.nullReadListener=L'écouteur passé à setReadListener() ne peut pas être null +request.readListenerSet=L'écouteur des lectures non bloquantes a déjà été défini + +response.encoding.invalid=L''encodage [{0}] n''est pas reconnu par le JRE +response.noTrailers.notSupported=Un fournisseur d'en-tête de fin ne peut pas être mis sur cette réponse, soit le protocole ne supporte pas ces en-têtes, soit le protocole requiert que le fournisseur soit fourni avant le début de l'envoi de la réponse +response.notAsync=Il n'est possible de passer en mode d'entrée-sorties non bloquantes que lors de traitements asynchrones ou après mise à niveau depuis HTTP +response.notNonBlocking=Il n'est pas permis d'appeler isReady() quand la réponse n'a pas été mise en mode non-bloquant +response.nullWriteListener=L'écouteur passé à setWriteListener() ne peut pas être null +response.writeListenerSet=La cible de notifications d''écriture ("write listener") non-bloquante a déjà été définie diff --git a/java/org/apache/coyote/LocalStrings_ja.properties b/java/org/apache/coyote/LocalStrings_ja.properties new file mode 100644 index 0000000..22275d7 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_ja.properties @@ -0,0 +1,73 @@ +# 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. + +abstractConnectionHandler.connectionsGet=ソケット [{1}] ã®ãƒ—ロセッサ [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã—㟠+abstractConnectionHandler.error=リクエストã®èª­ã¿å–り中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚無視ã—ã¾ã™ã€‚ +abstractConnectionHandler.ioexception.debug=IOExceptionã¯æ­£å¸¸ã§ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +abstractConnectionHandler.negotiatedProcessor.fail=ãƒã‚´ã‚·ã‚¨ãƒ¼ãƒˆã•ã‚ŒãŸãƒ—ロトコル[{0}]ã®ãƒ—ロセッサã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +abstractConnectionHandler.oome=リクエスト処ç†ã®å®Œäº†ã«å¤±æ•—ã—ã¾ã—㟠+abstractConnectionHandler.process=ステータス [{1}] ã®ã‚½ã‚±ãƒƒãƒˆ [{0}] を処ç†ã—ã¦ã„ã¾ã™ +abstractConnectionHandler.processorCreate=æ–°ã—ã„プロセッサ [{0}] を生æˆã—ã¾ã—㟠+abstractConnectionHandler.processorPop=キャッシュã‹ã‚‰ãƒ—ロセッサー [{0}] ã‚’å–å¾—ã—ã¾ã—ãŸã€‚ +abstractConnectionHandler.protocolexception.debug=ProtocolExceptionsã¯æ­£å¸¸ã§ã™ã€‚無視ã—ã¾ã™ã€‚ +abstractConnectionHandler.socketexception.debug=SocketExceptionsã¯æ­£å¸¸ã§ã™ã€‚無視ã—ã¾ã™ã€‚ +abstractConnectionHandler.upgradeCreate=SocketWrapper [{1}]ã®ã‚¢ãƒƒãƒ—グレードプロセッサ[{0}]ãŒä½œæˆã•ã‚Œã¾ã—ãŸã€‚ + +abstractProcessor.asyncFail=éžåŒæœŸãƒ‡ãƒ¼ã‚¿ã‚’書ã込むã“ã¨ãŒã§ãã¾ã›ã‚“ +abstractProcessor.fallToDebug=\n\ +\ 注: 以é™ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆæ§‹æ–‡è§£æžã‚¨ãƒ©ãƒ¼ã®ç™ºç”Ÿã¯DEBUGレベルã§ãƒ­ã‚°ã«å‡ºåŠ›ã•ã‚Œã¾ã™ã€‚ +abstractProcessor.hostInvalid=ホストå [{0}] ã¯ç„¡åŠ¹ã§ã™ã€‚ +abstractProcessor.httpupgrade.notsupported=ã“ã®ãƒ—ロトコル㯠HTTP アップグレードã«å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“。 +abstractProcessor.noExecute=ã“ã®ãƒ—ロセッサãŒç¾åœ¨ SocketWrapper ã«é–¢é€£ä»˜ã‘られã¦ã„ãªã„ãŸã‚ã€ã‚³ãƒ³ãƒ†ãƒŠã‚¹ãƒ¬ãƒƒãƒ‰ã«å‡¦ç†ã‚’転é€ã§ãã¾ã›ã‚“ +abstractProcessor.pushrequest.notsupported=ã“ã®ãƒ—ロトコルã¯ã‚µãƒ¼ãƒãƒ¼ãƒ—ッシュã®è¦æ±‚ã«å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“。 +abstractProcessor.setErrorState=リクエストã®å‡¦ç†ä¸­ã«ã‚¨ãƒ©ãƒ¼çŠ¶æ…‹[{0}]ãŒå ±å‘Šã•ã‚Œã¾ã—㟠+abstractProcessor.socket.ssl=SSL属性å–得時ã®ä¾‹å¤– + +abstractProtocol.closeConnectionsAwait=[{1}]ã¸ã®æ—¢å­˜ã®æŽ¥ç¶šãŒå®Œäº†ã—ã¦é–‰ã˜ã‚‹ã®ã‚’[{0}]ミリ秒待機ã—ã¾ã™ã€‚ +abstractProtocol.mbeanDeregistrationFailed=MBeanサーãƒãƒ¼[{1}]ã‹ã‚‰[{0}]ã¨ã„ã†åå‰ã®MBeanã®ç™»éŒ²ã‚’解除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +abstractProtocol.processorRegisterError=リクエストプロセッサ登録中ã®ã‚¨ãƒ©ãƒ¼ +abstractProtocol.processorUnregisterError=リクエストプロセッサ登録解除中ã®ã‚¨ãƒ©ãƒ¼ +abstractProtocol.waitingProcessor.add=待機中ã®ãƒ—ロセッサ㫠[{0}] を追加ã—ã¾ã—㟠+abstractProtocol.waitingProcessor.remove=待機中ã®ãƒ—ロセッサã‹ã‚‰ [{0}] を除去ã—ã¾ã—㟠+ +abstractProtocolHandler.asyncTimeoutError=éžåŒæœŸã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆå‡¦ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +abstractProtocolHandler.destroy=ProtocolHandler [{0}] を破棄ã—ã¾ã™ã€‚ +abstractProtocolHandler.init=プロトコルãƒãƒ³ãƒ‰ãƒ© [{0}] ã‚’åˆæœŸåŒ–ã—ã¾ã™ã€‚ +abstractProtocolHandler.pause=ProtocolHandler [{0}] を一時åœæ­¢ã—ã¾ã™ã€‚ +abstractProtocolHandler.portOffset=ProtocolHandler [{0}] ã¯ã€ãƒ™ãƒ¼ã‚¹ãƒãƒ¼ãƒˆ [{1}] ã¨ãƒãƒ¼ãƒˆã‚ªãƒ•ã‚»ãƒƒãƒˆ [{2}] ã§æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ +abstractProtocolHandler.resume=プロトコルãƒãƒ³ãƒ‰ãƒ©ãƒ¼ [{0}] ã‚’å†é–‹ã—ã¾ã™ã€‚ +abstractProtocolHandler.setAttribute=属性[{0}]ã«å€¤[{1}]を設定ã™ã‚‹ +abstractProtocolHandler.start=プロトコルãƒãƒ³ãƒ‰ãƒ©ãƒ¼ [{0}] を開始ã—ã¾ã—ãŸã€‚ +abstractProtocolHandler.stop=ProtocolHandler [{0}]ã®åœæ­¢ä¸­ + +asyncStateMachine.asyncError.skip=asyncError() ã¯éžåŒæœŸå‡¦ç†ã®é–‹å§‹å¾Œã«ã™ã§ã«å‘¼ã³å‡ºã•ã‚Œã¦ã„ã‚‹ãŸã‚無視ã—ã¾ã™ +asyncStateMachine.asyncError.start=asyncError() ã®å‡¦ç†ã‚’開始ã—ã¾ã™ +asyncStateMachine.invalidAsyncState=éžåŒæœŸçŠ¶æ…‹ [{1}] ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾ã—㦠[{0}] を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“ +asyncStateMachine.stateChange=éžåŒæœŸçŠ¶æ…‹ã‚’[{0}]ã‹ã‚‰[{1}]ã«å¤‰æ›´ã—ã¾ã™ + +compressionConfig.ContentEncodingParseFail=圧縮ãŒä½¿ç”¨æ¸ˆã¿ã‹ç¢ºèªã™ã‚‹éš›ã«ã€Content-Encoding ヘッダã®è§£æžã«å¤±æ•—ã—ã¾ã—㟠+ +continueResponseTiming.invalid=値[{0}]ã¯continueResponseTimingã®æœ‰åŠ¹ãªæ§‹æˆã‚ªãƒ—ションã§ã¯ã‚ã‚Šã¾ã›ã‚“ + +request.notAsync=éžåŒæœŸå‡¦ç†ã¾ãŸã¯HTTPアップグレード処ç†å†…ã®ãƒŽãƒ³ãƒ–ロッキングIOã«åˆ‡ã‚Šæ›¿ãˆã‚‹ã“ã¨ã¯æœ‰åŠ¹ã§ã™ã€‚ +request.nullReadListener=setReadListener() ã«ã¯ null を指定ã§ãã¾ã›ã‚“。 +request.readListenerSet=ノンブロッキングリードリスナーã¯æ—¢ã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ + +response.encoding.invalid=JRE ã¯æ–‡å­—エンコーディング [{0}] ã‚’èªè­˜ã—ã¾ã›ã‚“。 +response.noTrailers.notSupported=ã“ã®ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã«trailer フィールドサプライヤを設定ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 下ä½ã®ãƒ—ロトコルãŒtrailer フィールドをサãƒãƒ¼ãƒˆã—ã¦ã„ãªã„ã‹ã€ã¾ãŸã¯ãƒ—ロトコルãŒã€ãƒ¬ã‚¹ãƒãƒ³ã‚¹ãŒã‚³ãƒŸãƒƒãƒˆã•ã‚Œã‚‹å‰ã«ã‚µãƒ—ライヤãŒè¨­å®šã•ã‚Œã¦ã„ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +response.notAsync=éžåŒæœŸå‡¦ç†ã€ã‚ã‚‹ã„ã¯ã€HTTP アップグレード処ç†ã®é€”中ã§ã®ã¿ãƒŽãƒ³ãƒ–ロッキング IO ã¸åˆ‡ã‚Šæ›¿ãˆã‚‹ã“ã¨ãŒã§ãã¾ã™ +response.notNonBlocking=ノンブロッキングモードã«ã—ãªã‹ã£ãŸãƒ¬ã‚¹ãƒãƒ³ã‚¹ã® isReady() を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +response.nullWriteListener=setWriteListener() ã«ã¯ null を指定ã§ãã¾ã›ã‚“。 +response.writeListenerSet=ノンブロッキング書ãè¾¼ã¿ãƒªã‚¹ãƒŠãƒ¼ãŒè¨­å®šæ¸ˆã¿ã§ã™ diff --git a/java/org/apache/coyote/LocalStrings_ko.properties b/java/org/apache/coyote/LocalStrings_ko.properties new file mode 100644 index 0000000..6847030 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_ko.properties @@ -0,0 +1,70 @@ +# 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. + +abstractConnectionHandler.connectionsGet=소켓 [{1}]ì„(를) 위한 프로세서 [{0}]ì„(를) 발견했습니다. +abstractConnectionHandler.error=ìš”ì²­ì„ ì½ëŠ” 중 오류 ë°œìƒ. 무시합니다. +abstractConnectionHandler.ioexception.debug=IOExceptionë“¤ì€ ì •ìƒì ì´ë¯€ë¡œ, 무시합니다. +abstractConnectionHandler.negotiatedProcessor.fail=Negotiateëœ í”„ë¡œí† ì½œ [{0}]ì„(를) 위한 프로세서를 ìƒì„±í•˜ì§€ 못했습니다. +abstractConnectionHandler.oome=요청 처리를 완료하지 못했습니다. +abstractConnectionHandler.process=ìƒíƒœê°€ [{1}]ì¸ ì†Œì¼“ [{0}]ì„(를) 처리합니다. +abstractConnectionHandler.processorCreate=ìƒì„±ëœ 새 프로세서 [{0}] +abstractConnectionHandler.processorPop=ìºì‹œë¡œë¶€í„° 프로세서 [{0}]ì„(를) 추출했습니다. +abstractConnectionHandler.protocolexception.debug=ProtocolExceptionë“¤ì€ ì •ìƒì ì´ë¯€ë¡œ, 무시합니다. +abstractConnectionHandler.socketexception.debug=SocketExceptionë“¤ì€ ì •ìƒì ì¸ ìƒíƒœì´ë¯€ë¡œ 무시ë˜ì—ˆìŠµë‹ˆë‹¤. +abstractConnectionHandler.upgradeCreate=소켓 wrapper [{1}]ì„(를) 위한 업그레ì´ë“œ 프로세서 [{0}]ì„(를) ìƒì„±í–ˆìŠµë‹ˆë‹¤. + +abstractProcessor.fallToDebug=\n\ +\ 비고: ìš”ì²­ì— ëŒ€í•œ 파싱 ì˜¤ë¥˜ë“¤ì´ ë” ë°œìƒí•˜ëŠ” 경우 DEBUG 레벨 로그로 기ë¡ë  것입니다. +abstractProcessor.hostInvalid=호스트 [{0}]ì€(는) 유효하지 않습니다. +abstractProcessor.httpupgrade.notsupported=HTTP 업그레ì´ë“œëŠ” ì´ í”„ë¡œí† ì½œì— ì˜í•´ 지ì›ë˜ì§€ 않습니다. +abstractProcessor.noExecute=ì´ í”„ë¡œì„¸ì„œê°€ 현재 SocketWrapper와 ì—°ê´€ë˜ì–´ 있지 않기 때문ì—, 처리 ìž‘ì—…ì„ ì»¨í…Œì´ë„ˆ 쓰레드로 ì´ê´€í•  수 없습니다. +abstractProcessor.pushrequest.notsupported=ì´ í”„ë¡œí† ì½œì€ ì„œë²„ push ìš”ì²­ë“¤ì„ ì§€ì›í•˜ì§€ 않습니다. +abstractProcessor.setErrorState=요청 처리 중 오류 ìƒíƒœ [{0}]ì´(ê°€) ë³´ê³ ë¨. +abstractProcessor.socket.ssl=SSL ì†ì„±ë“¤ì„ 얻으려는 중 예외 ë°œìƒ + +abstractProtocol.closeConnectionsAwait=[{1}] í”„ë¡œí† ì½œì— ì—°ê²°ëœ ê¸°ì¡´ ì—°ê²°ë“¤ì´ ì™„ë£Œë˜ê³  닫히기까지 [{0}] 밀리초 ë™ì•ˆ 대기합니다. +abstractProtocol.mbeanDeregistrationFailed=MBean 서버 [{1}](으)로부터, [{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ MBeanì˜ ë“±ë¡ì„ 제거하지 못했습니다. +abstractProtocol.processorRegisterError=RequestProcessor 구성요소를 등ë¡í•˜ëŠ” 중 오류 ë°œìƒ +abstractProtocol.processorUnregisterError=RequestProcessor 구성요소를 ë“±ë¡ í•´ì œí•˜ëŠ” 중 오류 ë°œìƒ +abstractProtocol.waitingProcessor.add=대기 í”„ë¡œì„¸ì„œì— ì¶”ê°€ëœ í”„ë¡œì„¸ì„œ [{0}] +abstractProtocol.waitingProcessor.remove=대기 프로세서ì—ì„œ ì œê±°ëœ í”„ë¡œì„¸ì„œ [{0}] + +abstractProtocolHandler.asyncTimeoutError=비ë™ê¸° 제한 시간 초과를 처리하는 ë™ì•ˆ 오류 ë°œìƒ +abstractProtocolHandler.destroy=프로토콜 핸들러 [{0}]ì„(를) 소멸시킵니다. +abstractProtocolHandler.init=프로토콜 핸들러 [{0}]ì„(를) 초기화합니다. +abstractProtocolHandler.pause=프로토콜 핸들러 [{0}]ì„(를) ì¼ì‹œ 정지 중 +abstractProtocolHandler.portOffset=프로토콜 핸들러 [{0}]ì´(ê°€), base port [{1}], 그리고 port offset [{2}](으)ë¡œ 설정ë©ë‹ˆë‹¤. +abstractProtocolHandler.resume=프로토콜 핸들러 [{0}]ì„(를) 재개합니다. +abstractProtocolHandler.setAttribute=ì†ì„± [{0}]ì— ê°’ [{1}]ì„(를) 설정 +abstractProtocolHandler.start=프로토콜 핸들러 [{0}]ì„(를) 시작합니다. +abstractProtocolHandler.stop=프로토콜 핸들러 [{0}]ì„(를) 중지시킵니다. + +asyncStateMachine.invalidAsyncState=비ë™ê¸° ìƒíƒœê°€ [{1}]ì¸ ìš”ì²­ì— ëŒ€í•˜ì—¬, [{0}]ì„(를) 호출하는 ê²ƒì€ ìœ íš¨í•˜ì§€ 않습니다. +asyncStateMachine.stateChange=비ë™ê¸° ìƒíƒœë¥¼ [{0}]ì—ì„œ [{1}](으)ë¡œ 변경합니다. + +compressionConfig.ContentEncodingParseFail=ì••ì¶•ì´ ì´ë¯¸ 사용ë˜ëŠ”지 여부를 ì ê²€í•˜ëŠ” 중, Content-Encoding í—¤ë”를 파싱하지 못했습니다. + +continueResponseTiming.invalid=ê°’ [{0}]ì€(는) continueResponseTimingì„ ìœ„í•œ 유효한 설정 ì˜µì…˜ì´ ì•„ë‹™ë‹ˆë‹¤. + +request.notAsync=ì˜¤ì§ ë¹„ë™ê¸° 처리 ë˜ëŠ” HTTP 업그레ì´ë“œ 처리 ì‹œì—만, Non-blocking IOë¡œì˜ ì „í™˜ì´ ìœ íš¨í•©ë‹ˆë‹¤. +request.nullReadListener=setReadListener()ì— ì „ë‹¬ëœ ë¦¬ìŠ¤ë„ˆëŠ” ë„ì¼ ìˆ˜ 없습니다. +request.readListenerSet=Non-blocking ì½ê¸° 리스너가 ì´ë¯¸ 설정ë˜ì–´ 있습니다. + +response.encoding.invalid=ì¸ì½”딩 [{0}]ì€(는) JREì— ì˜í•´ ì¸ì‹ë˜ì§€ 않습니다. +response.noTrailers.notSupported=ì´ ì‘ë‹µì„ ìœ„í•´, Trailer fields supplierê°€ ì„¤ì •ë  ìˆ˜ 없습니다. 기반 í”„ë¡œí† ì½œì´ trailer fieldë“¤ì„ ì§€ì›í•˜ì§€ 않거나, ë˜ëŠ” í”„ë¡œí† ì½œì´ í•´ë‹¹ ì‘ë‹µì´ ì»¤ë°‹ë˜ê¸° ì „ì— Trailer fields supplierê°€ 설정ë˜ê¸°ë¥¼ 요구합니다. +response.notAsync=ì˜¤ì§ ë¹„ë™ê¸° 처리 중 ë˜ëŠ” HTTP 업그레ì´ë“œ 처리 ì¤‘ì¼ ë•Œì—만, non-blocking IOë¡œ 전환하려는 ê²ƒì´ ìœ íš¨í•©ë‹ˆë‹¤. +response.notNonBlocking=ì‘ë‹µì´ non-blocking 모드 ë‚´ì— ìžˆì§€ ì•Šì„ ë•Œ, isReady()를 호출하는 ê²ƒì€ ìœ íš¨í•˜ì§€ 않습니다. +response.nullWriteListener=setWriteListener()ì— ì „ë‹¬ë˜ëŠ” 리스너가 ë„ì´ì´ì„œëŠ” 안ë©ë‹ˆë‹¤. +response.writeListenerSet=Non-blocking 쓰기 리스너가 ì´ë¯¸ 설정ë˜ì—ˆìŠµë‹ˆë‹¤. diff --git a/java/org/apache/coyote/LocalStrings_pt_BR.properties b/java/org/apache/coyote/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..8635f36 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +response.writeListenerSet=O listener de escrita não bloqueante já foi configurado diff --git a/java/org/apache/coyote/LocalStrings_ru.properties b/java/org/apache/coyote/LocalStrings_ru.properties new file mode 100644 index 0000000..31874ff --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_ru.properties @@ -0,0 +1,20 @@ +# 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. + +abstractConnectionHandler.processorPop=процеÑÑор [{0}] вынут из кÑша + +abstractProcessor.httpupgrade.notsupported=Обновление HTTP не поддерживаетÑÑ Ð´Ð°Ð½Ð½Ñ‹Ð¼ протоколом + +abstractProtocolHandler.init=Ð˜Ð½Ð¸Ñ†Ð¸Ð°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ ProtocolHandler [{0}] diff --git a/java/org/apache/coyote/LocalStrings_zh_CN.properties b/java/org/apache/coyote/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..ddcff70 --- /dev/null +++ b/java/org/apache/coyote/LocalStrings_zh_CN.properties @@ -0,0 +1,70 @@ +# 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. + +abstractConnectionHandler.connectionsGet=为套接字[{1}]找到一个处ç†å™¨[{0}] +abstractConnectionHandler.error=读å–请求时出错,忽略 +abstractConnectionHandler.ioexception.debug=正常的 IOException,忽略 +abstractConnectionHandler.negotiatedProcessor.fail=无法为å商åè®®[{0}]创建处ç†å™¨ +abstractConnectionHandler.oome=无法完æˆè¯·æ±‚çš„å¤„ç† +abstractConnectionHandler.process=正在处ç†çŠ¶æ€ä¸º[{1}]的套接字[{0}] +abstractConnectionHandler.processorCreate=创建新处ç†å™¨[{0}] +abstractConnectionHandler.processorPop=从缓存中弹出的处ç†å™¨[{0}] +abstractConnectionHandler.protocolexception.debug=ProtocolExceptions是正常的,被忽略 +abstractConnectionHandler.socketexception.debug=(:SocketException是正常的,忽略 +abstractConnectionHandler.upgradeCreate=为套接字包装程åº[{1}]创建了å‡çº§å¤„ç†å™¨[{0}] + +abstractProcessor.fallToDebug=\n\ +\ 注æ„:更多的请求解æžé”™è¯¯å°†ä»¥DEBUG级别日志进行记录。 +abstractProcessor.hostInvalid=[{0}] 是无效主机 +abstractProcessor.httpupgrade.notsupported=æ­¤åè®®ä¸æ”¯æŒHTTPå‡çº§ï¼ˆupgrade)。 +abstractProcessor.noExecute=无法将处ç†ä¼ è¾“到容器线程,因为此处ç†å™¨å½“å‰æœªä¸ŽSocketWrapperå…³è” +abstractProcessor.pushrequest.notsupported=æ­¤åè®®ä¸æ”¯æŒæœåŠ¡å™¨æŽ¨é€è¯·æ±‚ +abstractProcessor.setErrorState=正在处ç†è¯·æ±‚时出现错误状æ€[{0}] +abstractProcessor.socket.ssl=获å–SSL属性异常 + +abstractProtocol.closeConnectionsAwait=等待[{0}]毫秒,等待到[{1}]的现有连接完æˆå¹¶å…³é—­ã€‚ +abstractProtocol.mbeanDeregistrationFailed=无法从MBeanæœåŠ¡å™¨[{1}]中注销å为[{0}]çš„MBean +abstractProtocol.processorRegisterError=注册请求处ç†å™¨é”™è¯¯ +abstractProtocol.processorUnregisterError=注销请求处ç†å™¨é”™è¯¯ +abstractProtocol.waitingProcessor.add=添加处ç†å™¨[{0}]到等待队列 +abstractProtocol.waitingProcessor.remove=将从等待的处ç†å™¨ä¸­ç§»é™¤å¤„ç†å™¨[{0}] + +abstractProtocolHandler.asyncTimeoutError=错误的处ç†å¼‚步超时 +abstractProtocolHandler.destroy=正在摧æ¯å议处ç†å™¨ [{0}] +abstractProtocolHandler.init=åˆå§‹åŒ–å议处ç†å™¨ [{0}] +abstractProtocolHandler.pause=æš‚åœProtocolHandler[{0}] +abstractProtocolHandler.portOffset=ProtocolHandler[{0}]的基本端å£ä¸º[{1}],端å£å移é‡ä¸º[{2}] +abstractProtocolHandler.resume=正在æ¢å¤ProtocolHandler[{0}] +abstractProtocolHandler.setAttribute=使用值[{1}]设置属性[{0}] +abstractProtocolHandler.start=开始å议处ç†å¥æŸ„[{0}] +abstractProtocolHandler.stop=正在åœæ­¢ProtocolHandler [{0}] + +asyncStateMachine.invalidAsyncState=调用[{0}]对于具有异步状æ€[{1}]的请求无效 +asyncStateMachine.stateChange=正在将异步状æ€ä»Ž[{0}]更改为[{1}] + +compressionConfig.ContentEncodingParseFail=检查压缩是å¦å·²ç»åœ¨ä½¿ç”¨æ—¶ï¼Œè§£æžContent-Encoding头失败 + +continueResponseTiming.invalid=对于continueResponseTiming,值[{0}]ä¸æ˜¯æœ‰æ•ˆçš„é…置项 + +request.notAsync=åªæœ‰åœ¨å¼‚步处ç†æˆ–HTTPå‡çº§å¤„ç†ä¸­åˆ‡æ¢åˆ°éžé˜»å¡žIOæ‰æœ‰æ•ˆ +request.nullReadListener=传递给setReadListener()的侦å¬å™¨ä¸èƒ½ä¸ºç©º +request.readListenerSet=已设置éžé˜»å¡žè¯»å–侦å¬å™¨ + +response.encoding.invalid=JRE无法识别编ç [{0}] +response.noTrailers.notSupported=ä¸èƒ½ä¸ºæ­¤å“应设置拖车字段供应商。底层åè®®ä¸æ”¯æŒå°¾éƒ¨å­—段,或者åè®®è¦æ±‚在æ交å“应之å‰è®¾ç½®ä¾›åº”商 +response.notAsync=åªæœ‰åœ¨å¼‚步处ç†æˆ–HTTPå‡çº§å¤„ç†ä¸­åˆ‡æ¢åˆ°éžé˜»å¡žIOæ‰æœ‰æ•ˆ +response.notNonBlocking=当å“应尚未进入éžé˜»å¡žæ¨¡å¼æ—¶ï¼Œè°ƒç”¨ isReady() 无效 +response.nullWriteListener=传递给setWriteListener()的侦å¬å™¨ä¸èƒ½ä¸ºç©º +response.writeListenerSet=éžé˜»å¡žçš„写入监å¬å™¨å·²ç»è¢«è®¾ç½®. diff --git a/java/org/apache/coyote/OutputBuffer.java b/java/org/apache/coyote/OutputBuffer.java new file mode 100644 index 0000000..f4ba643 --- /dev/null +++ b/java/org/apache/coyote/OutputBuffer.java @@ -0,0 +1,48 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Output buffer. This class is used internally by the protocol implementation. All writes from higher level code should + * happen via Response.doWrite(). + * + * @author Remy Maucherat + */ +public interface OutputBuffer { + + /** + * Write the given data to the response. The caller owns the chunks. + * + * @param chunk data to write + * + * @return The number of bytes written which may be less than available in the input chunk + * + * @throws IOException an underlying I/O error occurred + */ + int doWrite(ByteBuffer chunk) throws IOException; + + + /** + * Bytes written to the underlying socket. This includes the effects of chunking, compression, etc. + * + * @return Bytes written for the current request + */ + long getBytesWritten(); +} diff --git a/java/org/apache/coyote/Processor.java b/java/org/apache/coyote/Processor.java new file mode 100644 index 0000000..ad41bc4 --- /dev/null +++ b/java/org/apache/coyote/Processor.java @@ -0,0 +1,114 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; + +/** + * Common interface for processors of all protocols. + */ +public interface Processor { + + /** + * Process a connection. This is called whenever an event occurs (e.g. more data arrives) that allows processing to + * continue for a connection that is not currently being processed. + * + * @param socketWrapper The connection to process + * @param status The status of the connection that triggered this additional processing + * + * @return The state the caller should put the socket in when this method returns + * + * @throws IOException If an I/O error occurs during the processing of the request + */ + SocketState process(SocketWrapperBase socketWrapper, SocketEvent status) throws IOException; + + /** + * Generate an upgrade token. + * + * @return An upgrade token encapsulating the information required to process the upgrade request + * + * @throws IllegalStateException if this is called on a Processor that does not support upgrading + */ + UpgradeToken getUpgradeToken(); + + /** + * @return {@code true} if the Processor is currently processing an upgrade request, otherwise {@code false} + */ + boolean isUpgrade(); + + boolean isAsync(); + + /** + * Check this processor to see if the timeout has expired and process a timeout if that is that case. + *

    + * Note: The name of this method originated with the Servlet 3.0 asynchronous processing but evolved over time to + * represent a timeout that is triggered independently of the socket read/write timeouts. + * + * @param now The time (as returned by {@link System#currentTimeMillis()} to use as the current time to determine + * whether the timeout has expired. If negative, the timeout will always be treated as ifq it has + * expired. + */ + void timeoutAsync(long now); + + /** + * @return The request associated with this processor. + */ + Request getRequest(); + + /** + * Recycle the processor, ready for the next request which may be on the same connection or a different connection. + */ + void recycle(); + + /** + * Set the SSL information for this HTTP connection. + * + * @param sslSupport The SSL support object to use for this connection + */ + void setSslSupport(SSLSupport sslSupport); + + /** + * Allows retrieving additional input during the upgrade process. + * + * @return leftover bytes + * + * @throws IllegalStateException if this is called on a Processor that does not support upgrading + */ + ByteBuffer getLeftoverInput(); + + /** + * Informs the processor that the underlying I/O layer has stopped accepting new connections. This is primarily + * intended to enable processors that use multiplexed connections to prevent further 'streams' being added to an + * existing multiplexed connection. + */ + void pause(); + + /** + * Check to see if the async generation (each cycle of async increments the generation of the AsyncStateMachine) is + * the same as the generation when the most recent async timeout was triggered. This is intended to be used to avoid + * unnecessary processing. + * + * @return {@code true} If the async generation has not changed since the async timeout was triggered + */ + boolean checkAsyncTimeoutGeneration(); +} diff --git a/java/org/apache/coyote/ProtocolException.java b/java/org/apache/coyote/ProtocolException.java new file mode 100644 index 0000000..9762e07 --- /dev/null +++ b/java/org/apache/coyote/ProtocolException.java @@ -0,0 +1,41 @@ +/* + * 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.coyote; + +/** + * Used when we need to indicate failure but the (Servlet) API doesn't declare any appropriate exceptions. + */ +public class ProtocolException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ProtocolException() { + super(); + } + + public ProtocolException(String message, Throwable cause) { + super(message, cause); + } + + public ProtocolException(String message) { + super(message); + } + + public ProtocolException(Throwable cause) { + super(cause); + } +} diff --git a/java/org/apache/coyote/ProtocolHandler.java b/java/org/apache/coyote/ProtocolHandler.java new file mode 100644 index 0000000..a92473c --- /dev/null +++ b/java/org/apache/coyote/ProtocolHandler.java @@ -0,0 +1,257 @@ +/* + * 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.coyote; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; + +import org.apache.tomcat.util.net.SSLHostConfig; + +/** + * Abstract the protocol implementation, including threading, etc. This is the main interface to be implemented by a + * coyote protocol. Adapter is the main interface to be implemented by a coyote servlet container. + * + * @author Remy Maucherat + * @author Costin Manolache + * + * @see Adapter + */ +public interface ProtocolHandler { + + /** + * Return the adapter associated with the protocol handler. + * + * @return the adapter + */ + Adapter getAdapter(); + + + /** + * The adapter, used to call the connector. + * + * @param adapter The adapter to associate + */ + void setAdapter(Adapter adapter); + + + /** + * The executor, provide access to the underlying thread pool. + * + * @return The executor used to process requests + */ + Executor getExecutor(); + + + /** + * Set the optional executor that will be used by the connector. + * + * @param executor the executor + */ + void setExecutor(Executor executor); + + + /** + * Get the utility executor that should be used by the protocol handler. + * + * @return the executor + */ + ScheduledExecutorService getUtilityExecutor(); + + + /** + * Set the utility executor that should be used by the protocol handler. + * + * @param utilityExecutor the executor + */ + void setUtilityExecutor(ScheduledExecutorService utilityExecutor); + + + /** + * Initialise the protocol. + * + * @throws Exception If the protocol handler fails to initialise + */ + void init() throws Exception; + + + /** + * Start the protocol. + * + * @throws Exception If the protocol handler fails to start + */ + void start() throws Exception; + + + /** + * Pause the protocol (optional). + * + * @throws Exception If the protocol handler fails to pause + */ + void pause() throws Exception; + + + /** + * Resume the protocol (optional). + * + * @throws Exception If the protocol handler fails to resume + */ + void resume() throws Exception; + + + /** + * Stop the protocol. + * + * @throws Exception If the protocol handler fails to stop + */ + void stop() throws Exception; + + + /** + * Destroy the protocol (optional). + * + * @throws Exception If the protocol handler fails to destroy + */ + void destroy() throws Exception; + + + /** + * Close the server socket (to prevent further connections) if the server socket was bound on {@link #start()} + * (rather than on {@link #init()} but do not perform any further shutdown. + */ + void closeServerSocketGraceful(); + + + /** + * Wait for the client connections to the server to close gracefully. The method will return when all of the client + * connections have closed or the method has been waiting for {@code waitTimeMillis}. + * + * @param waitMillis The maximum time to wait in milliseconds for the client connections to close. + * + * @return The wait time, if any remaining when the method returned + */ + long awaitConnectionsClose(long waitMillis); + + + /** + * Does this ProtocolHandler support sendfile? + * + * @return true if this Protocol Handler supports sendfile, otherwise false + */ + boolean isSendfileSupported(); + + + /** + * Add a new SSL configuration for a virtual host. + * + * @param sslHostConfig the configuration + */ + void addSslHostConfig(SSLHostConfig sslHostConfig); + + + /** + * Add a new SSL configuration for a virtual host. + * + * @param sslHostConfig the configuration + * @param replace If {@code true} replacement of an existing configuration is permitted, otherwise any such + * attempted replacement will trigger an exception + * + * @throws IllegalArgumentException If the host name is not valid or if a configuration has already been provided + * for that host and replacement is not allowed + */ + void addSslHostConfig(SSLHostConfig sslHostConfig, boolean replace); + + + /** + * Find all configured SSL virtual host configurations which will be used by SNI. + * + * @return the configurations + */ + SSLHostConfig[] findSslHostConfigs(); + + + /** + * Add a new protocol for used by HTTP/1.1 upgrade or ALPN. + * + * @param upgradeProtocol the protocol + */ + void addUpgradeProtocol(UpgradeProtocol upgradeProtocol); + + + /** + * Return all configured upgrade protocols. + * + * @return the protocols + */ + UpgradeProtocol[] findUpgradeProtocols(); + + + /** + * Some protocols, like AJP, have a packet length that shouldn't be exceeded, and this can be used to adjust the + * buffering used by the application layer. + * + * @return the desired buffer size, or -1 if not relevant + */ + default int getDesiredBufferSize() { + return -1; + } + + + /** + * The default behavior is to identify connectors uniquely with address and port. However, certain connectors are + * not using that and need some other identifier, which then can be used as a replacement. + * + * @return the id + */ + default String getId() { + return null; + } + + + /** + * Create a new ProtocolHandler for the given protocol. + * + * @param protocol the protocol + * + * @return the newly instantiated protocol handler + * + * @throws ClassNotFoundException Specified protocol was not found + * @throws InstantiationException Specified protocol could not be instantiated + * @throws IllegalAccessException Exception occurred + * @throws IllegalArgumentException Exception occurred + * @throws InvocationTargetException Exception occurred + * @throws NoSuchMethodException Exception occurred + * @throws SecurityException Exception occurred + */ + static ProtocolHandler create(String protocol) + throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException, SecurityException { + if (protocol == null || "HTTP/1.1".equals(protocol) || + org.apache.coyote.http11.Http11NioProtocol.class.getName().equals(protocol)) { + return new org.apache.coyote.http11.Http11NioProtocol(); + } else if ("AJP/1.3".equals(protocol) || + org.apache.coyote.ajp.AjpNioProtocol.class.getName().equals(protocol)) { + return new org.apache.coyote.ajp.AjpNioProtocol(); + } else { + // Instantiate protocol handler + Class clazz = Class.forName(protocol); + return (ProtocolHandler) clazz.getConstructor().newInstance(); + } + } + + +} diff --git a/java/org/apache/coyote/Request.java b/java/org/apache/coyote/Request.java new file mode 100644 index 0000000..5059b87 --- /dev/null +++ b/java/org/apache/coyote/Request.java @@ -0,0 +1,864 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletConnection; + +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.Parameters; +import org.apache.tomcat.util.http.ServerCookies; +import org.apache.tomcat.util.http.parser.MediaType; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.res.StringManager; + +/** + * This is a low-level, efficient representation of a server request. Most fields are GC-free, expensive operations are + * delayed until the user code needs the information. Processing is delegated to modules, using a hook mechanism. This + * class is not intended for user code - it is used internally by tomcat for processing the request in the most + * efficient way. Users ( servlets ) can access the information using a facade, which provides the high-level view of + * the request. Tomcat defines a number of attributes: + *

      + *
    • "org.apache.tomcat.request" - allows access to the low-level request object in trusted applications + *
    + * + * @author James Duncan Davidson [duncan@eng.sun.com] + * @author James Todd [gonzo@eng.sun.com] + * @author Jason Hunter [jch@eng.sun.com] + * @author Harish Prabandham + * @author Alex Cruikshank [alex@epitonic.com] + * @author Hans Bergsten [hans@gefionsoftware.com] + * @author Costin Manolache + * @author Remy Maucherat + */ +public final class Request { + + private static final StringManager sm = StringManager.getManager(Request.class); + + // Expected maximum typical number of cookies per request. + private static final int INITIAL_COOKIE_SIZE = 4; + + /* + * At 100,000 requests a second there are enough IDs here for ~3,000,000 years before it overflows (and then we have + * another 3,000,000 years before it gets back to zero). + * + * Local testing shows that 5, 10, 50, 500 or 1000 threads can obtain 60,000,000+ IDs a second from a single + * AtomicLong. That is about about 17ns per request. It does not appear that the introduction of this counter will + * cause a bottleneck for request processing. + */ + private static final AtomicLong requestIdGenerator = new AtomicLong(0); + + // ----------------------------------------------------------- Constructors + + public Request() { + parameters.setQuery(queryMB); + parameters.setURLDecoder(urlDecoder); + } + + + // ----------------------------------------------------- Instance Variables + + private int serverPort = -1; + private final MessageBytes serverNameMB = MessageBytes.newInstance(); + + private int remotePort; + private int localPort; + + private final MessageBytes schemeMB = MessageBytes.newInstance(); + + private final MessageBytes methodMB = MessageBytes.newInstance(); + private final MessageBytes uriMB = MessageBytes.newInstance(); + private final MessageBytes decodedUriMB = MessageBytes.newInstance(); + private final MessageBytes queryMB = MessageBytes.newInstance(); + private final MessageBytes protoMB = MessageBytes.newInstance(); + + private volatile String requestId = Long.toString(requestIdGenerator.getAndIncrement()); + + // remote address/host + private final MessageBytes remoteAddrMB = MessageBytes.newInstance(); + private final MessageBytes peerAddrMB = MessageBytes.newInstance(); + private final MessageBytes localNameMB = MessageBytes.newInstance(); + private final MessageBytes remoteHostMB = MessageBytes.newInstance(); + private final MessageBytes localAddrMB = MessageBytes.newInstance(); + + private final MimeHeaders headers = new MimeHeaders(); + private final Map trailerFields = new HashMap<>(); + + /** + * Path parameters + */ + private final Map pathParameters = new HashMap<>(); + + /** + * Notes. + */ + private final Object notes[] = new Object[Constants.MAX_NOTES]; + + + /** + * Associated input buffer. + */ + private InputBuffer inputBuffer = null; + + + /** + * URL decoder. + */ + private final UDecoder urlDecoder = new UDecoder(); + + + /** + * HTTP specific fields. (remove them ?) + */ + private long contentLength = -1; + private MessageBytes contentTypeMB = null; + private Charset charset = null; + // Retain the original, user specified character encoding so it can be + // returned even if it is invalid + private String characterEncoding = null; + + /** + * Is there an expectation ? + */ + private boolean expectation = false; + + private final ServerCookies serverCookies = new ServerCookies(INITIAL_COOKIE_SIZE); + private final Parameters parameters = new Parameters(); + + private final MessageBytes remoteUser = MessageBytes.newInstance(); + private boolean remoteUserNeedsAuthorization = false; + private final MessageBytes authType = MessageBytes.newInstance(); + private final HashMap attributes = new HashMap<>(); + + private Response response; + private volatile ActionHook hook; + + private long bytesRead = 0; + // Time of the request - useful to avoid repeated calls to System.currentTime + private long startTimeNanos = -1; + private long threadId = 0; + private int available = 0; + + private final RequestInfo reqProcessorMX = new RequestInfo(this); + + private boolean sendfile = true; + + /** + * Holds request body reading error exception. + */ + private Exception errorException = null; + + /* + * State for non-blocking output is maintained here as it is the one point easily reachable from the + * CoyoteInputStream and the CoyoteAdapter which both need access to state. + */ + volatile ReadListener listener; + // Ensures listener is only fired after a call is isReady() + private boolean fireListener = false; + // Tracks read registration to prevent duplicate registrations + private boolean registeredForRead = false; + // Lock used to manage concurrent access to above flags + private final Object nonBlockingStateLock = new Object(); + + public ReadListener getReadListener() { + return listener; + } + + public void setReadListener(ReadListener listener) { + if (listener == null) { + throw new NullPointerException(sm.getString("request.nullReadListener")); + } + if (getReadListener() != null) { + throw new IllegalStateException(sm.getString("request.readListenerSet")); + } + // Note: This class is not used for HTTP upgrade so only need to test + // for async + AtomicBoolean result = new AtomicBoolean(false); + action(ActionCode.ASYNC_IS_ASYNC, result); + if (!result.get()) { + throw new IllegalStateException(sm.getString("request.notAsync")); + } + + this.listener = listener; + + // The container is responsible for the first call to + // listener.onDataAvailable(). If isReady() returns true, the container + // needs to call listener.onDataAvailable() from a new thread. If + // isReady() returns false, the socket will be registered for read and + // the container will call listener.onDataAvailable() once data arrives. + // Must call isFinished() first as a call to isReady() if the request + // has been finished will register the socket for read interest and that + // is not required. + if (!isFinished() && isReady()) { + synchronized (nonBlockingStateLock) { + // Ensure we don't get multiple read registrations + registeredForRead = true; + // Need to set the fireListener flag otherwise when the + // container tries to trigger onDataAvailable, nothing will + // happen + fireListener = true; + } + action(ActionCode.DISPATCH_READ, null); + if (!isRequestThread()) { + // Not on a container thread so need to execute the dispatch + action(ActionCode.DISPATCH_EXECUTE, null); + } + } + } + + public boolean isReady() { + // Assume read is not possible + boolean ready = false; + synchronized (nonBlockingStateLock) { + if (registeredForRead) { + fireListener = true; + return false; + } + ready = checkRegisterForRead(); + fireListener = !ready; + } + return ready; + } + + private boolean checkRegisterForRead() { + AtomicBoolean ready = new AtomicBoolean(false); + synchronized (nonBlockingStateLock) { + if (!registeredForRead) { + action(ActionCode.NB_READ_INTEREST, ready); + registeredForRead = !ready.get(); + } + } + return ready.get(); + } + + public void onDataAvailable() throws IOException { + boolean fire = false; + synchronized (nonBlockingStateLock) { + registeredForRead = false; + if (fireListener) { + fireListener = false; + fire = true; + } + } + if (fire) { + listener.onDataAvailable(); + } + } + + + private final AtomicBoolean allDataReadEventSent = new AtomicBoolean(false); + + public boolean sendAllDataReadEvent() { + return allDataReadEventSent.compareAndSet(false, true); + } + + + // ------------------------------------------------------------- Properties + + public MimeHeaders getMimeHeaders() { + return headers; + } + + + public boolean isTrailerFieldsReady() { + AtomicBoolean result = new AtomicBoolean(false); + action(ActionCode.IS_TRAILER_FIELDS_READY, result); + return result.get(); + } + + + public Map getTrailerFields() { + return trailerFields; + } + + + public UDecoder getURLDecoder() { + return urlDecoder; + } + + + // -------------------- Request data -------------------- + + public MessageBytes scheme() { + return schemeMB; + } + + public MessageBytes method() { + return methodMB; + } + + public MessageBytes requestURI() { + return uriMB; + } + + public MessageBytes decodedURI() { + return decodedUriMB; + } + + public MessageBytes queryString() { + return queryMB; + } + + public MessageBytes protocol() { + return protoMB; + } + + /** + * Get the "virtual host", derived from the Host: header associated with this request. + * + * @return The buffer holding the server name, if any. Use isNull() to check if there is no value set. + */ + public MessageBytes serverName() { + return serverNameMB; + } + + public int getServerPort() { + return serverPort; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public MessageBytes remoteAddr() { + return remoteAddrMB; + } + + public MessageBytes peerAddr() { + return peerAddrMB; + } + + public MessageBytes remoteHost() { + return remoteHostMB; + } + + public MessageBytes localName() { + return localNameMB; + } + + public MessageBytes localAddr() { + return localAddrMB; + } + + public int getRemotePort() { + return remotePort; + } + + public void setRemotePort(int port) { + this.remotePort = port; + } + + public int getLocalPort() { + return localPort; + } + + public void setLocalPort(int port) { + this.localPort = port; + } + + + // -------------------- encoding/type -------------------- + + /** + * Get the character encoding used for this request. + * + * @return The value set via {@link #setCharset(Charset)} or if no call has been made to that method try to obtain + * if from the content type. + */ + public String getCharacterEncoding() { + if (characterEncoding == null) { + characterEncoding = getCharsetFromContentType(getContentType()); + } + + return characterEncoding; + } + + + /** + * Get the character encoding used for this request. + * + * @return The value set via {@link #setCharset(Charset)} or if no call has been made to that method try to obtain + * if from the content type. + * + * @throws UnsupportedEncodingException If the user agent has specified an invalid character encoding + */ + public Charset getCharset() throws UnsupportedEncodingException { + if (charset == null) { + getCharacterEncoding(); + if (characterEncoding != null) { + charset = B2CConverter.getCharset(characterEncoding); + } + } + + return charset; + } + + + public void setCharset(Charset charset) { + this.charset = charset; + this.characterEncoding = charset.name(); + } + + + public void setContentLength(long len) { + this.contentLength = len; + } + + + public int getContentLength() { + long length = getContentLengthLong(); + + if (length < Integer.MAX_VALUE) { + return (int) length; + } + return -1; + } + + public long getContentLengthLong() { + if (contentLength > -1) { + return contentLength; + } + + MessageBytes clB = headers.getUniqueValue("content-length"); + contentLength = (clB == null || clB.isNull()) ? -1 : clB.getLong(); + + return contentLength; + } + + public String getContentType() { + contentType(); + if (contentTypeMB == null || contentTypeMB.isNull()) { + return null; + } + return contentTypeMB.toStringType(); + } + + + public void setContentType(String type) { + contentTypeMB.setString(type); + } + + + public MessageBytes contentType() { + if (contentTypeMB == null) { + contentTypeMB = headers.getValue("content-type"); + } + return contentTypeMB; + } + + + public void setContentType(MessageBytes mb) { + contentTypeMB = mb; + } + + + public String getHeader(String name) { + return headers.getHeader(name); + } + + + public void setExpectation(boolean expectation) { + this.expectation = expectation; + } + + + public boolean hasExpectation() { + return expectation; + } + + + // -------------------- Associated response -------------------- + + public Response getResponse() { + return response; + } + + public void setResponse(Response response) { + this.response = response; + response.setRequest(this); + } + + protected void setHook(ActionHook hook) { + this.hook = hook; + } + + public void action(ActionCode actionCode, Object param) { + if (hook != null) { + if (param == null) { + hook.action(actionCode, this); + } else { + hook.action(actionCode, param); + } + } + } + + + // -------------------- Cookies -------------------- + + public ServerCookies getCookies() { + return serverCookies; + } + + + // -------------------- Parameters -------------------- + + public Parameters getParameters() { + return parameters; + } + + + public void addPathParameter(String name, String value) { + pathParameters.put(name, value); + } + + public String getPathParameter(String name) { + return pathParameters.get(name); + } + + + // -------------------- Other attributes -------------------- + // We can use notes for most - need to discuss what is of general interest + + public void setAttribute(String name, Object o) { + attributes.put(name, o); + } + + public HashMap getAttributes() { + return attributes; + } + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public MessageBytes getRemoteUser() { + return remoteUser; + } + + public boolean getRemoteUserNeedsAuthorization() { + return remoteUserNeedsAuthorization; + } + + public void setRemoteUserNeedsAuthorization(boolean remoteUserNeedsAuthorization) { + this.remoteUserNeedsAuthorization = remoteUserNeedsAuthorization; + } + + public MessageBytes getAuthType() { + return authType; + } + + public int getAvailable() { + return available; + } + + public void setAvailable(int available) { + this.available = available; + } + + public boolean getSendfile() { + return sendfile; + } + + public void setSendfile(boolean sendfile) { + this.sendfile = sendfile; + } + + public boolean isFinished() { + AtomicBoolean result = new AtomicBoolean(false); + action(ActionCode.REQUEST_BODY_FULLY_READ, result); + return result.get(); + } + + public boolean getSupportsRelativeRedirects() { + if (protocol().equals("") || protocol().equals("HTTP/1.0")) { + return false; + } + return true; + } + + + // -------------------- Input Buffer -------------------- + + public InputBuffer getInputBuffer() { + return inputBuffer; + } + + + public void setInputBuffer(InputBuffer inputBuffer) { + this.inputBuffer = inputBuffer; + } + + + /** + * Read data from the input buffer and put it into ApplicationBufferHandler. The buffer is owned by the protocol + * implementation - it will be reused on the next read. The Adapter must either process the data in place or copy it + * to a separate buffer if it needs to hold it. In most cases this is done during byte->char conversions or via + * InputStream. Unlike InputStream, this interface allows the app to process data in place, without copy. + * + * @param handler The destination to which to copy the data + * + * @return The number of bytes copied + * + * @throws IOException If an I/O error occurs during the copy + */ + public int doRead(ApplicationBufferHandler handler) throws IOException { + if (getBytesRead() == 0 && !response.isCommitted()) { + action(ActionCode.ACK, ContinueResponseTiming.ON_REQUEST_BODY_READ); + } + + int n = inputBuffer.doRead(handler); + if (n > 0) { + bytesRead += n; + } + return n; + } + + + // -------------------- Error tracking -------------------- + + /** + * Set the error Exception that occurred during the writing of the response processing. + * + * @param ex The exception that occurred + */ + public void setErrorException(Exception ex) { + errorException = ex; + } + + + /** + * Get the Exception that occurred during the writing of the response. + * + * @return The exception that occurred + */ + public Exception getErrorException() { + return errorException; + } + + + public boolean isExceptionPresent() { + return errorException != null; + } + + + // -------------------- debug -------------------- + + public String getRequestId() { + return requestId; + } + + + public String getProtocolRequestId() { + AtomicReference ref = new AtomicReference<>(); + hook.action(ActionCode.PROTOCOL_REQUEST_ID, ref); + return ref.get(); + } + + + public ServletConnection getServletConnection() { + AtomicReference ref = new AtomicReference<>(); + hook.action(ActionCode.SERVLET_CONNECTION, ref); + return ref.get(); + } + + + @Override + public String toString() { + return "R( " + requestURI().toString() + ")"; + } + + public long getStartTime() { + return System.currentTimeMillis() - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos); + } + + /** + * @param startTime time + * + * @deprecated This setter will be removed in Tomcat 11 + */ + @Deprecated + public void setStartTime(long startTime) { + } + + public long getStartTimeNanos() { + return startTimeNanos; + } + + public void setStartTimeNanos(long startTimeNanos) { + this.startTimeNanos = startTimeNanos; + } + + public long getThreadId() { + return threadId; + } + + public void clearRequestThread() { + threadId = 0; + } + + public void setRequestThread() { + Thread t = Thread.currentThread(); + threadId = t.getId(); + getRequestProcessor().setWorkerThreadName(t.getName()); + } + + public boolean isRequestThread() { + return Thread.currentThread().getId() == threadId; + } + + // -------------------- Per-Request "notes" -------------------- + + + /** + * Used to store private data. Thread data could be used instead - but if you have the req, getting/setting a note + * is just an array access, may be faster than ThreadLocal for very frequent operations. Example use: Catalina + * CoyoteAdapter: ADAPTER_NOTES = 1 - stores the HttpServletRequest object ( req/res) To avoid conflicts, note in + * the range 0 - 8 are reserved for the servlet container ( catalina connector, etc ), and values in 9 - 16 for + * connector use. 17-31 range is not allocated or used. + * + * @param pos Index to use to store the note + * @param value The value to store at that index + */ + public void setNote(int pos, Object value) { + notes[pos] = value; + } + + + public Object getNote(int pos) { + return notes[pos]; + } + + + // -------------------- Recycling -------------------- + + + public void recycle() { + bytesRead = 0; + + contentLength = -1; + contentTypeMB = null; + charset = null; + characterEncoding = null; + expectation = false; + headers.recycle(); + trailerFields.clear(); + serverNameMB.recycle(); + serverPort = -1; + localAddrMB.recycle(); + localNameMB.recycle(); + localPort = -1; + peerAddrMB.recycle(); + remoteAddrMB.recycle(); + remoteHostMB.recycle(); + remotePort = -1; + available = 0; + sendfile = true; + + // There may be multiple calls to recycle but only the first should + // trigger a change in the request ID until a new request has been + // started. Use startTimeNanos to detect when a request has started so a + // subsequent call to recycle() will trigger a change in the request ID. + if (startTimeNanos != -1) { + requestId = Long.toHexString(requestIdGenerator.getAndIncrement()); + } + + serverCookies.recycle(); + parameters.recycle(); + pathParameters.clear(); + + uriMB.recycle(); + decodedUriMB.recycle(); + queryMB.recycle(); + methodMB.recycle(); + protoMB.recycle(); + + schemeMB.recycle(); + + remoteUser.recycle(); + remoteUserNeedsAuthorization = false; + authType.recycle(); + attributes.clear(); + + errorException = null; + + listener = null; + synchronized (nonBlockingStateLock) { + fireListener = false; + registeredForRead = false; + } + allDataReadEventSent.set(false); + + startTimeNanos = -1; + threadId = 0; + } + + // -------------------- Info -------------------- + public void updateCounters() { + reqProcessorMX.updateCounters(); + } + + public RequestInfo getRequestProcessor() { + return reqProcessorMX; + } + + public long getBytesRead() { + return bytesRead; + } + + public boolean isProcessing() { + return reqProcessorMX.getStage() == Constants.STAGE_SERVICE; + } + + /** + * Parse the character encoding from the specified content type header. If the content type is null, or there is no + * explicit character encoding, null is returned. + * + * @param contentType a content type header + */ + private static String getCharsetFromContentType(String contentType) { + + if (contentType == null) { + return null; + } + + MediaType mediaType = null; + try { + mediaType = MediaType.parseMediaType(new StringReader(contentType)); + } catch (IOException e) { + // Ignore - null test below handles this + } + if (mediaType != null) { + return mediaType.getCharset(); + } + + return null; + } +} diff --git a/java/org/apache/coyote/RequestGroupInfo.java b/java/org/apache/coyote/RequestGroupInfo.java new file mode 100644 index 0000000..843ad0d --- /dev/null +++ b/java/org/apache/coyote/RequestGroupInfo.java @@ -0,0 +1,156 @@ +/* + * 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.coyote; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.tomcat.util.modeler.BaseModelMBean; + +/** + * Only as a JMX artifact, to aggregate the data collected from each RequestProcessor thread. + */ +public class RequestGroupInfo extends BaseModelMBean { + private final List processors = new ArrayList<>(); + private long deadMaxTime = 0; + private long deadProcessingTime = 0; + private int deadRequestCount = 0; + private int deadErrorCount = 0; + private long deadBytesReceived = 0; + private long deadBytesSent = 0; + + public synchronized void addRequestProcessor(RequestInfo rp) { + processors.add(rp); + } + + public synchronized void removeRequestProcessor(RequestInfo rp) { + if (rp != null) { + if (deadMaxTime < rp.getMaxTime()) { + deadMaxTime = rp.getMaxTime(); + } + deadProcessingTime += rp.getProcessingTime(); + deadRequestCount += rp.getRequestCount(); + deadErrorCount += rp.getErrorCount(); + deadBytesReceived += rp.getBytesReceived(); + deadBytesSent += rp.getBytesSent(); + + processors.remove(rp); + } + } + + public synchronized long getMaxTime() { + long maxTime = deadMaxTime; + for (RequestInfo rp : processors) { + if (maxTime < rp.getMaxTime()) { + maxTime = rp.getMaxTime(); + } + } + return maxTime; + } + + // Used to reset the times + public synchronized void setMaxTime(long maxTime) { + deadMaxTime = maxTime; + for (RequestInfo rp : processors) { + rp.setMaxTime(maxTime); + } + } + + public synchronized long getProcessingTime() { + long time = deadProcessingTime; + for (RequestInfo rp : processors) { + time += rp.getProcessingTime(); + } + return time; + } + + public synchronized void setProcessingTime(long totalTime) { + deadProcessingTime = totalTime; + for (RequestInfo rp : processors) { + rp.setProcessingTime(totalTime); + } + } + + public synchronized int getRequestCount() { + int requestCount = deadRequestCount; + for (RequestInfo rp : processors) { + requestCount += rp.getRequestCount(); + } + return requestCount; + } + + public synchronized void setRequestCount(int requestCount) { + deadRequestCount = requestCount; + for (RequestInfo rp : processors) { + rp.setRequestCount(requestCount); + } + } + + public synchronized int getErrorCount() { + int requestCount = deadErrorCount; + for (RequestInfo rp : processors) { + requestCount += rp.getErrorCount(); + } + return requestCount; + } + + public synchronized void setErrorCount(int errorCount) { + deadErrorCount = errorCount; + for (RequestInfo rp : processors) { + rp.setErrorCount(errorCount); + } + } + + public synchronized long getBytesReceived() { + long bytes = deadBytesReceived; + for (RequestInfo rp : processors) { + bytes += rp.getBytesReceived(); + } + return bytes; + } + + public synchronized void setBytesReceived(long bytesReceived) { + deadBytesReceived = bytesReceived; + for (RequestInfo rp : processors) { + rp.setBytesReceived(bytesReceived); + } + } + + public synchronized long getBytesSent() { + long bytes = deadBytesSent; + for (RequestInfo rp : processors) { + bytes += rp.getBytesSent(); + } + return bytes; + } + + public synchronized void setBytesSent(long bytesSent) { + deadBytesSent = bytesSent; + for (RequestInfo rp : processors) { + rp.setBytesSent(bytesSent); + } + } + + public void resetCounters() { + this.setBytesReceived(0); + this.setBytesSent(0); + this.setRequestCount(0); + this.setProcessingTime(0); + this.setMaxTime(0); + this.setErrorCount(0); + } +} diff --git a/java/org/apache/coyote/RequestInfo.java b/java/org/apache/coyote/RequestInfo.java new file mode 100644 index 0000000..d1fffd3 --- /dev/null +++ b/java/org/apache/coyote/RequestInfo.java @@ -0,0 +1,264 @@ +/* + * 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.coyote; + +import java.util.concurrent.TimeUnit; + +import javax.management.ObjectName; + + +/** + * Structure holding the Request and Response objects. It also holds statistical information about request processing + * and provide management information about the requests being processed. Each thread uses a Request/Response pair that + * is recycled on each request. This object provides a place to collect global low-level statistics - without having to + * deal with synchronization ( since each thread will have it's own RequestProcessorMX ). + * + * @author Costin Manolache + */ +public class RequestInfo { + private RequestGroupInfo global = null; + + // ----------------------------------------------------------- Constructors + + public RequestInfo(Request req) { + this.req = req; + } + + public RequestGroupInfo getGlobalProcessor() { + return global; + } + + public void setGlobalProcessor(RequestGroupInfo global) { + if (global != null) { + this.global = global; + global.addRequestProcessor(this); + } else { + if (this.global != null) { + this.global.removeRequestProcessor(this); + this.global = null; + } + } + } + + + // ----------------------------------------------------- Instance Variables + private final Request req; + private int stage = Constants.STAGE_NEW; + private String workerThreadName; + private ObjectName rpName; + + // -------------------- Information about the current request ----------- + // This is useful for long-running requests only + + public String getMethod() { + return req.method().toString(); + } + + public String getCurrentUri() { + return req.requestURI().toString(); + } + + public String getCurrentQueryString() { + return req.queryString().toString(); + } + + public String getProtocol() { + return req.protocol().toString(); + } + + public String getVirtualHost() { + return req.serverName().toString(); + } + + public int getServerPort() { + return req.getServerPort(); + } + + public String getRemoteAddr() { + req.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, null); + return req.remoteAddr().toString(); + } + + public String getPeerAddr() { + req.action(ActionCode.REQ_PEER_ADDR_ATTRIBUTE, null); + return req.peerAddr().toString(); + } + + /** + * Obtain the remote address for this connection as reported by an intermediate proxy (if any). + * + * @return The remote address for the this connection + */ + public String getRemoteAddrForwarded() { + String remoteAddrProxy = (String) req.getAttribute(Constants.REMOTE_ADDR_ATTRIBUTE); + if (remoteAddrProxy == null) { + return getRemoteAddr(); + } + return remoteAddrProxy; + } + + public int getContentLength() { + return req.getContentLength(); + } + + public long getRequestBytesReceived() { + return req.getBytesRead(); + } + + public long getRequestBytesSent() { + return req.getResponse().getContentWritten(); + } + + public long getRequestProcessingTime() { + // Not perfect, but good enough to avoid returning strange values due to + // concurrent updates. + long startTime = req.getStartTimeNanos(); + if (getStage() == Constants.STAGE_ENDED || startTime < 0) { + return 0; + } else { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); + } + } + + // -------------------- Statistical data -------------------- + // Collected at the end of each request. + private long bytesSent; + private long bytesReceived; + + // Total time = divide by requestCount to get average. + private long processingTime; + // The longest response time for a request + private long maxTime; + // URI of the request that took maxTime + private String maxRequestUri; + + private int requestCount; + // number of response codes >= 400 + private int errorCount; + + // the time of the last request + private long lastRequestProcessingTime = 0; + + + /** + * Called by the processor before recycling the request. It'll collect statistic information. + */ + void updateCounters() { + bytesReceived += req.getBytesRead(); + bytesSent += req.getResponse().getContentWritten(); + + requestCount++; + if (req.getResponse().getStatus() >= 400) { + errorCount++; + } + long time = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - req.getStartTimeNanos()); + this.lastRequestProcessingTime = time; + processingTime += time; + if (maxTime < time) { + maxTime = time; + maxRequestUri = req.requestURI().toString(); + } + } + + public int getStage() { + return stage; + } + + public void setStage(int stage) { + this.stage = stage; + } + + public long getBytesSent() { + return bytesSent; + } + + public void setBytesSent(long bytesSent) { + this.bytesSent = bytesSent; + } + + public long getBytesReceived() { + return bytesReceived; + } + + public void setBytesReceived(long bytesReceived) { + this.bytesReceived = bytesReceived; + } + + public long getProcessingTime() { + return processingTime; + } + + public void setProcessingTime(long processingTime) { + this.processingTime = processingTime; + } + + public long getMaxTime() { + return maxTime; + } + + public void setMaxTime(long maxTime) { + this.maxTime = maxTime; + } + + public String getMaxRequestUri() { + return maxRequestUri; + } + + public void setMaxRequestUri(String maxRequestUri) { + this.maxRequestUri = maxRequestUri; + } + + public int getRequestCount() { + return requestCount; + } + + public void setRequestCount(int requestCount) { + this.requestCount = requestCount; + } + + public int getErrorCount() { + return errorCount; + } + + public void setErrorCount(int errorCount) { + this.errorCount = errorCount; + } + + public String getWorkerThreadName() { + return workerThreadName; + } + + public ObjectName getRpName() { + return rpName; + } + + public long getLastRequestProcessingTime() { + return lastRequestProcessingTime; + } + + public void setWorkerThreadName(String workerThreadName) { + this.workerThreadName = workerThreadName; + } + + public void setRpName(ObjectName rpName) { + this.rpName = rpName; + } + + public void setLastRequestProcessingTime(long lastRequestProcessingTime) { + this.lastRequestProcessingTime = lastRequestProcessingTime; + } +} diff --git a/java/org/apache/coyote/Response.java b/java/org/apache/coyote/Response.java new file mode 100644 index 0000000..b1d88ee --- /dev/null +++ b/java/org/apache/coyote/Response.java @@ -0,0 +1,781 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import jakarta.servlet.WriteListener; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.MediaType; +import org.apache.tomcat.util.res.StringManager; + +/** + * Response object. + * + * @author James Duncan Davidson [duncan@eng.sun.com] + * @author Jason Hunter [jch@eng.sun.com] + * @author James Todd [gonzo@eng.sun.com] + * @author Harish Prabandham + * @author Hans Bergsten [hans@gefionsoftware.com] + * @author Remy Maucherat + */ +public final class Response { + + private static final StringManager sm = StringManager.getManager(Response.class); + private static final Log log = LogFactory.getLog(Response.class); + + + // ----------------------------------------------------- Class Variables + + /** + * Default locale as mandated by the spec. + */ + private static final Locale DEFAULT_LOCALE = Locale.getDefault(); + + + // ----------------------------------------------------- Instance Variables + + /** + * Status code. + */ + int status = 200; + + + /** + * Status message. + */ + String message = null; + + + /** + * Response headers. + */ + final MimeHeaders headers = new MimeHeaders(); + + + private Supplier> trailerFieldsSupplier = null; + + /** + * Associated output buffer. + */ + OutputBuffer outputBuffer; + + + /** + * Notes. + */ + final Object notes[] = new Object[Constants.MAX_NOTES]; + + + /** + * Committed flag. + */ + volatile boolean committed = false; + + + /** + * Action hook. + */ + volatile ActionHook hook; + + + /** + * HTTP specific fields. + */ + String contentType = null; + String contentLanguage = null; + Charset charset = null; + // Retain the original name used to set the charset so exactly that name is + // used in the ContentType header. Some (arguably non-specification + // compliant) user agents are very particular + String characterEncoding = null; + long contentLength = -1; + private Locale locale = DEFAULT_LOCALE; + + // General information + private long contentWritten = 0; + private long commitTimeNanos = -1; + + /** + * Holds response writing error exception. + */ + private Exception errorException = null; + + /** + * With the introduction of async processing and the possibility of non-container threads calling sendError() + * tracking the current error state and ensuring that the correct error page is called becomes more complicated. + * This state attribute helps by tracking the current error state and informing callers that attempt to change state + * if the change was successful or if another thread got there first. + * + *
    +     * The state machine is very simple:
    +     *
    +     * 0 - NONE
    +     * 1 - NOT_REPORTED
    +     * 2 - REPORTED
    +     *
    +     *
    +     *   -->---->-- >NONE
    +     *   |   |        |
    +     *   |   |        | setError()
    +     *   ^   ^        |
    +     *   |   |       \|/
    +     *   |   |-<-NOT_REPORTED
    +     *   |            |
    +     *   ^            | report()
    +     *   |            |
    +     *   |           \|/
    +     *   |----<----REPORTED
    +     * 
    + */ + private final AtomicInteger errorState = new AtomicInteger(0); + + Request req; + + + // ------------------------------------------------------------- Properties + + public Request getRequest() { + return req; + } + + public void setRequest(Request req) { + this.req = req; + } + + + public void setOutputBuffer(OutputBuffer outputBuffer) { + this.outputBuffer = outputBuffer; + } + + + public MimeHeaders getMimeHeaders() { + return headers; + } + + + protected void setHook(ActionHook hook) { + this.hook = hook; + } + + + // -------------------- Per-Response "notes" -------------------- + + public void setNote(int pos, Object value) { + notes[pos] = value; + } + + + public Object getNote(int pos) { + return notes[pos]; + } + + + // -------------------- Actions -------------------- + + public void action(ActionCode actionCode, Object param) { + if (hook != null) { + if (param == null) { + hook.action(actionCode, this); + } else { + hook.action(actionCode, param); + } + } + } + + + // -------------------- State -------------------- + + public int getStatus() { + return status; + } + + + /** + * Set the response status. + * + * @param status The status value to set + */ + public void setStatus(int status) { + this.status = status; + } + + + /** + * Get the status message. + * + * @return The message associated with the current status + */ + public String getMessage() { + return message; + } + + + /** + * Set the status message. + * + * @param message The status message to set + */ + public void setMessage(String message) { + this.message = message; + } + + + public boolean isCommitted() { + return committed; + } + + + public void setCommitted(boolean v) { + if (v && !this.committed) { + this.commitTimeNanos = System.nanoTime(); + } + this.committed = v; + } + + /** + * Return the time the response was committed (based on System.currentTimeMillis). + * + * @return the time the response was committed + */ + public long getCommitTime() { + return System.currentTimeMillis() - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - commitTimeNanos); + } + + /** + * Return the time the response was committed (based on System.nanoTime). + * + * @return the time the response was committed + */ + public long getCommitTimeNanos() { + return commitTimeNanos; + } + + // -----------------Error State -------------------- + + /** + * Set the error Exception that occurred during the writing of the response processing. + * + * @param ex The exception that occurred + */ + public void setErrorException(Exception ex) { + if (errorException == null) { + errorException = ex; + } + } + + + /** + * Get the Exception that occurred during the writing of the response. + * + * @return The exception that occurred + */ + public Exception getErrorException() { + return errorException; + } + + + public boolean isExceptionPresent() { + return errorException != null; + } + + + /** + * Set the error flag. + * + * @return false if the error flag was already set + * + * @deprecated This method will be changed to return void in Tomcat 11 onwards + */ + @Deprecated + public boolean setError() { + return errorState.compareAndSet(0, 1); + } + + + /** + * Error flag accessor. + * + * @return true if the response has encountered an error + */ + public boolean isError() { + return errorState.get() > 0; + } + + + public boolean isErrorReportRequired() { + return errorState.get() == 1; + } + + + public boolean setErrorReported() { + return errorState.compareAndSet(1, 2); + } + + + // -------------------- Methods -------------------- + + public void reset() throws IllegalStateException { + + if (committed) { + throw new IllegalStateException(); + } + + recycle(); + } + + + // -------------------- Headers -------------------- + /** + * Does the response contain the given header.
    + * Warning: This method always returns false for Content-Type and Content-Length. + * + * @param name The name of the header of interest + * + * @return {@code true} if the response contains the header. + */ + public boolean containsHeader(String name) { + return headers.getHeader(name) != null; + } + + + public void setHeader(String name, String value) { + char cc = name.charAt(0); + if (cc == 'C' || cc == 'c') { + if (checkSpecialHeader(name, value)) { + return; + } + } + headers.setValue(name).setString(value); + } + + + public void addHeader(String name, String value) { + addHeader(name, value, null); + } + + + public void addHeader(String name, String value, Charset charset) { + char cc = name.charAt(0); + if (cc == 'C' || cc == 'c') { + if (checkSpecialHeader(name, value)) { + return; + } + } + MessageBytes mb = headers.addValue(name); + if (charset != null) { + mb.setCharset(charset); + } + mb.setString(value); + } + + + public void setTrailerFields(Supplier> supplier) { + AtomicBoolean trailerFieldsSupported = new AtomicBoolean(false); + action(ActionCode.IS_TRAILER_FIELDS_SUPPORTED, trailerFieldsSupported); + if (!trailerFieldsSupported.get()) { + throw new IllegalStateException(sm.getString("response.noTrailers.notSupported")); + } + + this.trailerFieldsSupplier = supplier; + } + + + public Supplier> getTrailerFields() { + return trailerFieldsSupplier; + } + + + /** + * Set internal fields for special header names. Called from set/addHeader. Return true if the header is special, no + * need to set the header. + */ + private boolean checkSpecialHeader(String name, String value) { + // XXX Eliminate redundant fields !!! + // ( both header and in special fields ) + if (name.equalsIgnoreCase("Content-Type")) { + setContentType(value); + return true; + } + if (name.equalsIgnoreCase("Content-Length")) { + try { + long cL = Long.parseLong(value); + setContentLength(cL); + return true; + } catch (NumberFormatException ex) { + // Do nothing - the spec doesn't have any "throws" + // and the user might know what they're doing + return false; + } + } + return false; + } + + + /** + * Signal that we're done with the headers, and body will follow. Any implementation needs to notify ContextManager, + * to allow interceptors to fix headers. + */ + public void sendHeaders() { + action(ActionCode.COMMIT, this); + setCommitted(true); + } + + + // -------------------- I18N -------------------- + + + public Locale getLocale() { + return locale; + } + + /** + * Called explicitly by user to set the Content-Language and the default encoding. + * + * @param locale The locale to use for this response + */ + public void setLocale(Locale locale) { + + if (locale == null) { + this.locale = null; + this.contentLanguage = null; + return; + } + + // Save the locale for use by getLocale() + this.locale = locale; + + // Set the contentLanguage for header output + contentLanguage = locale.toLanguageTag(); + } + + /** + * Return the content language. + * + * @return The language code for the language currently associated with this response + */ + public String getContentLanguage() { + return contentLanguage; + } + + + /** + * Overrides the character encoding used in the body of the response. This method must be called prior to writing + * output using getWriter(). + * + * @param characterEncoding The name of character encoding. + * + * @throws UnsupportedEncodingException If the specified name is not recognised + */ + public void setCharacterEncoding(String characterEncoding) throws UnsupportedEncodingException { + if (isCommitted()) { + return; + } + if (characterEncoding == null) { + this.charset = null; + this.characterEncoding = null; + return; + } + + this.characterEncoding = characterEncoding; + this.charset = B2CConverter.getCharset(characterEncoding); + } + + + public Charset getCharset() { + return charset; + } + + + /** + * @return The name of the current encoding + */ + public String getCharacterEncoding() { + return characterEncoding; + } + + + /** + * Sets the content type. This method must preserve any response charset that may already have been set via a call + * to response.setContentType(), response.setLocale(), or response.setCharacterEncoding(). + * + * @param type the content type + */ + public void setContentType(String type) { + + if (type == null) { + this.contentType = null; + return; + } + + MediaType m = null; + try { + m = MediaType.parseMediaType(new StringReader(type)); + } catch (IOException e) { + // Ignore - null test below handles this + } + if (m == null) { + // Invalid - Assume no charset and just pass through whatever + // the user provided. + this.contentType = type; + return; + } + + this.contentType = m.toStringNoCharset(); + + String charsetValue = m.getCharset(); + + if (charsetValue == null) { + // No charset and we know value is valid as parser was successful + // Pass-through user provided value in case user-agent is buggy and + // requires specific format + this.contentType = type; + } else { + // There is a charset so have to rebuild content-type without it + this.contentType = m.toStringNoCharset(); + charsetValue = charsetValue.trim(); + if (charsetValue.length() > 0) { + try { + charset = B2CConverter.getCharset(charsetValue); + } catch (UnsupportedEncodingException e) { + log.warn(sm.getString("response.encoding.invalid", charsetValue), e); + } + } + } + } + + public void setContentTypeNoCharset(String type) { + this.contentType = type; + } + + public String getContentType() { + + String ret = contentType; + + if (ret != null && charset != null) { + ret = ret + ";charset=" + characterEncoding; + } + + return ret; + } + + public void setContentLength(long contentLength) { + this.contentLength = contentLength; + } + + public int getContentLength() { + long length = getContentLengthLong(); + + if (length < Integer.MAX_VALUE) { + return (int) length; + } + return -1; + } + + public long getContentLengthLong() { + return contentLength; + } + + + /** + * Write a chunk of bytes. + * + * @param chunk The ByteBuffer to write + * + * @throws IOException If an I/O error occurs during the write + */ + public void doWrite(ByteBuffer chunk) throws IOException { + int len = chunk.remaining(); + outputBuffer.doWrite(chunk); + contentWritten += len - chunk.remaining(); + } + + // -------------------- + + public void recycle() { + + contentType = null; + contentLanguage = null; + locale = DEFAULT_LOCALE; + charset = null; + characterEncoding = null; + contentLength = -1; + status = 200; + message = null; + committed = false; + commitTimeNanos = -1; + errorException = null; + errorState.set(0); + headers.recycle(); + trailerFieldsSupplier = null; + // Servlet 3.1 non-blocking write listener + listener = null; + synchronized (nonBlockingStateLock) { + fireListener = false; + registeredForWrite = false; + } + + // update counters + contentWritten = 0; + } + + /** + * Bytes written by application - i.e. before compression, chunking, etc. + * + * @return The total number of bytes written to the response by the application. This will not be the number of + * bytes written to the network which may be more or less than this value. + */ + public long getContentWritten() { + return contentWritten; + } + + /** + * Bytes written to socket - i.e. after compression, chunking, etc. + * + * @param flush Should any remaining bytes be flushed before returning the total? If {@code false} bytes remaining + * in the buffer will not be included in the returned value + * + * @return The total number of bytes written to the socket for this response + */ + public long getBytesWritten(boolean flush) { + if (flush) { + action(ActionCode.CLIENT_FLUSH, this); + } + return outputBuffer.getBytesWritten(); + } + + /* + * State for non-blocking output is maintained here as it is the one point easily reachable from the + * CoyoteOutputStream and the Processor which both need access to state. + */ + volatile WriteListener listener; + // Ensures listener is only fired after a call is isReady() + private boolean fireListener = false; + // Tracks write registration to prevent duplicate registrations + private boolean registeredForWrite = false; + // Lock used to manage concurrent access to above flags + private final Object nonBlockingStateLock = new Object(); + + public WriteListener getWriteListener() { + return listener; + } + + public void setWriteListener(WriteListener listener) { + if (listener == null) { + throw new NullPointerException(sm.getString("response.nullWriteListener")); + } + if (getWriteListener() != null) { + throw new IllegalStateException(sm.getString("response.writeListenerSet")); + } + // Note: This class is not used for HTTP upgrade so only need to test + // for async + AtomicBoolean result = new AtomicBoolean(false); + action(ActionCode.ASYNC_IS_ASYNC, result); + if (!result.get()) { + throw new IllegalStateException(sm.getString("response.notAsync")); + } + + this.listener = listener; + + // The container is responsible for the first call to + // listener.onWritePossible(). If isReady() returns true, the container + // needs to call listener.onWritePossible() from a new thread. If + // isReady() returns false, the socket will be registered for write and + // the container will call listener.onWritePossible() once data can be + // written. + if (isReady()) { + synchronized (nonBlockingStateLock) { + // Ensure we don't get multiple write registrations if + // ServletOutputStream.isReady() returns false during a call to + // onDataAvailable() + registeredForWrite = true; + // Need to set the fireListener flag otherwise when the + // container tries to trigger onWritePossible, nothing will + // happen + fireListener = true; + } + action(ActionCode.DISPATCH_WRITE, null); + if (!req.isRequestThread()) { + // Not on a container thread so need to execute the dispatch + action(ActionCode.DISPATCH_EXECUTE, null); + } + } + } + + public boolean isReady() { + if (listener == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("response.notNonBlocking")); + } + return false; + } + // Assume write is not possible + boolean ready = false; + synchronized (nonBlockingStateLock) { + if (registeredForWrite) { + fireListener = true; + return false; + } + ready = checkRegisterForWrite(); + fireListener = !ready; + } + return ready; + } + + public boolean checkRegisterForWrite() { + AtomicBoolean ready = new AtomicBoolean(false); + synchronized (nonBlockingStateLock) { + if (!registeredForWrite) { + action(ActionCode.NB_WRITE_INTEREST, ready); + registeredForWrite = !ready.get(); + } + } + return ready.get(); + } + + public void onWritePossible() throws IOException { + // Any buffered data left over from a previous non-blocking write is + // written in the Processor so if this point is reached the app is able + // to write data. + boolean fire = false; + synchronized (nonBlockingStateLock) { + registeredForWrite = false; + if (fireListener) { + fireListener = false; + fire = true; + } + } + if (fire) { + listener.onWritePossible(); + } + } +} diff --git a/java/org/apache/coyote/UpgradeProtocol.java b/java/org/apache/coyote/UpgradeProtocol.java new file mode 100644 index 0000000..96db49e --- /dev/null +++ b/java/org/apache/coyote/UpgradeProtocol.java @@ -0,0 +1,98 @@ +/* + * 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.coyote; + +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; +import org.apache.tomcat.util.net.SocketWrapperBase; + +public interface UpgradeProtocol { + + /** + * @param isSSLEnabled Is this for a connector that is configured to support TLS. Some protocols (e.g. HTTP/2) only + * support HTTP upgrade over non-secure connections. + * + * @return The name that clients will use to request an upgrade to this protocol via an HTTP/1.1 upgrade request or + * null if upgrade via an HTTP/1.1 upgrade request is not supported. + */ + String getHttpUpgradeName(boolean isSSLEnabled); + + /** + * @return The byte sequence as listed in the IANA registry for this protocol or null if upgrade via + * ALPN is not supported. + */ + byte[] getAlpnIdentifier(); + + /** + * @return The name of the protocol as listed in the IANA registry if and only if {@link #getAlpnIdentifier()} + * returns the UTF-8 encoding of this name. If {@link #getAlpnIdentifier()} returns some other byte + * sequence, then this method returns the empty string. If upgrade via ALPN is not supported then + * null is returned. + */ + /* + * Implementation note: If Tomcat ever supports ALPN for a protocol where the identifier is not the UTF-8 encoding + * of the name then some refactoring is going to be required. + * + * Implementation note: Tomcat assumes that the UTF-8 encoding of this name will not exceed 255 bytes. Tomcat's + * behaviour if longer names are used is undefined. + */ + String getAlpnName(); + + /** + * @param socketWrapper The socketWrapper for the connection that requires a processor + * @param adapter The Adapter instance that provides access to the standard Engine/Host/Context/Wrapper + * processing chain + * + * @return A processor instance for processing a connection using this protocol. + */ + Processor getProcessor(SocketWrapperBase socketWrapper, Adapter adapter); + + + /** + * @param socketWrapper The socket + * @param adapter The Adapter to use to configure the new upgrade handler + * @param request A copy (may be incomplete) of the request that triggered the upgrade + * + * @return An instance of the HTTP upgrade handler for this protocol + */ + InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase socketWrapper, Adapter adapter, + Request request); + + + /** + * Allows the implementation to examine the request and accept or reject it based on what it finds. + * + * @param request The request that included an upgrade header for this protocol + * + * @return true if the request is accepted, otherwise false + */ + boolean accept(Request request); + + + /** + * Configure the HTTP/1.1 protocol that this UpgradeProcotol is nested under. Connections passed to this + * UpgradeProtocol via HTTP upgrade will have been initially handled by this HTTP/1.1 protocol implementation. + *

    + * The default implementation is a NO-OP. + * + * @param protocol The HTTP/1.1 protocol implementation that will initially handle any connections passed to this + * UpgradeProtocol via the HTTP upgrade mechanism + */ + default void setHttp11Protocol(AbstractHttp11Protocol protocol) { + // NO-OP + } +} diff --git a/java/org/apache/coyote/UpgradeToken.java b/java/org/apache/coyote/UpgradeToken.java new file mode 100644 index 0000000..b1985a5 --- /dev/null +++ b/java/org/apache/coyote/UpgradeToken.java @@ -0,0 +1,57 @@ +/* + * 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.coyote; + +import jakarta.servlet.http.HttpUpgradeHandler; + +import org.apache.tomcat.ContextBind; +import org.apache.tomcat.InstanceManager; + +/** + * Token used during the upgrade process. + */ +public final class UpgradeToken { + + private final ContextBind contextBind; + private final HttpUpgradeHandler httpUpgradeHandler; + private final InstanceManager instanceManager; + private final String protocol; + + public UpgradeToken(HttpUpgradeHandler httpUpgradeHandler, ContextBind contextBind, InstanceManager instanceManager, + String protocol) { + this.contextBind = contextBind; + this.httpUpgradeHandler = httpUpgradeHandler; + this.instanceManager = instanceManager; + this.protocol = protocol; + } + + public ContextBind getContextBind() { + return contextBind; + } + + public HttpUpgradeHandler getHttpUpgradeHandler() { + return httpUpgradeHandler; + } + + public InstanceManager getInstanceManager() { + return instanceManager; + } + + public String getProtocol() { + return protocol; + } +} diff --git a/java/org/apache/coyote/ajp/AbstractAjpProtocol.java b/java/org/apache/coyote/ajp/AbstractAjpProtocol.java new file mode 100644 index 0000000..8241a7b --- /dev/null +++ b/java/org/apache/coyote/ajp/AbstractAjpProtocol.java @@ -0,0 +1,282 @@ +/* + * 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.coyote.ajp; + +import java.net.InetAddress; +import java.util.regex.Pattern; + +import org.apache.coyote.AbstractProtocol; +import org.apache.coyote.Processor; +import org.apache.coyote.UpgradeProtocol; +import org.apache.coyote.UpgradeToken; +import org.apache.tomcat.util.net.AbstractEndpoint; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +/** + * The is the base implementation for the AJP protocol handlers. Implementations typically extend this base class rather + * than implement {@link org.apache.coyote.ProtocolHandler}. All of the implementations that ship with Tomcat are + * implemented this way. + * + * @param The type of socket used by the implementation + */ +public abstract class AbstractAjpProtocol extends AbstractProtocol { + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(AbstractAjpProtocol.class); + + + public AbstractAjpProtocol(AbstractEndpoint endpoint) { + super(endpoint); + setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT); + // AJP does not use Send File + getEndpoint().setUseSendfile(false); + // AJP listens on loopback by default + getEndpoint().setAddress(InetAddress.getLoopbackAddress()); + } + + + @Override + protected String getProtocolName() { + return "Ajp"; + } + + + /** + * {@inheritDoc} Overridden to make getter accessible to other classes in this package. + */ + @Override + protected AbstractEndpoint getEndpoint() { + return super.getEndpoint(); + } + + + /** + * {@inheritDoc} AJP does not support protocol negotiation so this always returns null. + */ + @Override + protected UpgradeProtocol getNegotiatedProtocol(String name) { + return null; + } + + + /** + * {@inheritDoc} AJP does not support protocol upgrade so this always returns null. + */ + @Override + protected UpgradeProtocol getUpgradeProtocol(String name) { + return null; + } + + // ------------------------------------------------- AJP specific properties + // ------------------------------------------ managed in the ProtocolHandler + + private boolean ajpFlush = true; + + public boolean getAjpFlush() { + return ajpFlush; + } + + /** + * Configure whether to aend an AJP flush packet when flushing. A flush packet is a zero byte AJP13 SEND_BODY_CHUNK + * packet. mod_jk and mod_proxy_ajp interpret this as a request to flush data to the client. AJP always does flush + * at the and of the response, so if it is not important, that the packets get streamed up to the client, do not use + * extra flush packets. For compatibility and to stay on the safe side, flush packets are enabled by default. + * + * @param ajpFlush The new flush setting + */ + public void setAjpFlush(boolean ajpFlush) { + this.ajpFlush = ajpFlush; + } + + + private boolean tomcatAuthentication = true; + + /** + * Should authentication be done in the native web server layer, or in the Servlet container ? + * + * @return {@code true} if authentication should be performed by Tomcat, otherwise {@code false} + */ + public boolean getTomcatAuthentication() { + return tomcatAuthentication; + } + + public void setTomcatAuthentication(boolean tomcatAuthentication) { + this.tomcatAuthentication = tomcatAuthentication; + } + + + private boolean tomcatAuthorization = false; + + /** + * Should authentication be done in the native web server layer and authorization in the Servlet container? + * + * @return {@code true} if authorization should be performed by Tomcat, otherwise {@code false} + */ + public boolean getTomcatAuthorization() { + return tomcatAuthorization; + } + + public void setTomcatAuthorization(boolean tomcatAuthorization) { + this.tomcatAuthorization = tomcatAuthorization; + } + + + private String secret = null; + + /** + * Set the secret that must be included with every request. + * + * @param secret The required secret + */ + public void setSecret(String secret) { + this.secret = secret; + } + + protected String getSecret() { + return secret; + } + + /** + * Set the required secret that must be included with every request. + * + * @param requiredSecret The required secret + * + * @deprecated Replaced by {@link #setSecret(String)}. Will be removed in Tomcat 11 onwards + */ + @Deprecated + public void setRequiredSecret(String requiredSecret) { + setSecret(requiredSecret); + } + + /** + * @return The current secret + * + * @deprecated Replaced by {@link #getSecret()}. Will be removed in Tomcat 11 onwards + */ + @Deprecated + protected String getRequiredSecret() { + return getSecret(); + } + + + private boolean secretRequired = true; + + public void setSecretRequired(boolean secretRequired) { + this.secretRequired = secretRequired; + } + + public boolean getSecretRequired() { + return secretRequired; + } + + + private Pattern allowedRequestAttributesPattern; + + public void setAllowedRequestAttributesPattern(String allowedRequestAttributesPattern) { + this.allowedRequestAttributesPattern = Pattern.compile(allowedRequestAttributesPattern); + } + + public String getAllowedRequestAttributesPattern() { + return allowedRequestAttributesPattern.pattern(); + } + + protected Pattern getAllowedRequestAttributesPatternInternal() { + return allowedRequestAttributesPattern; + } + + + /** + * AJP packet size. + */ + private int packetSize = Constants.MAX_PACKET_SIZE; + + public int getPacketSize() { + return packetSize; + } + + public void setPacketSize(int packetSize) { + this.packetSize = Math.max(packetSize, Constants.MAX_PACKET_SIZE); + } + + + @Override + public int getDesiredBufferSize() { + return getPacketSize() - Constants.SEND_HEAD_LEN; + } + + + // --------------------------------------------- SSL is not supported in AJP + + @Override + public void addSslHostConfig(SSLHostConfig sslHostConfig) { + getLog().warn(sm.getString("ajpprotocol.noSSL", sslHostConfig.getHostName())); + } + + + @Override + public void addSslHostConfig(SSLHostConfig sslHostConfig, boolean replace) { + getLog().warn(sm.getString("ajpprotocol.noSSL", sslHostConfig.getHostName())); + } + + + @Override + public SSLHostConfig[] findSslHostConfigs() { + return new SSLHostConfig[0]; + } + + + @Override + public void addUpgradeProtocol(UpgradeProtocol upgradeProtocol) { + getLog().warn(sm.getString("ajpprotocol.noUpgrade", upgradeProtocol.getClass().getName())); + } + + + @Override + public UpgradeProtocol[] findUpgradeProtocols() { + return new UpgradeProtocol[0]; + } + + + @Override + protected Processor createProcessor() { + AjpProcessor processor = new AjpProcessor(this, getAdapter()); + return processor; + } + + + @Override + protected Processor createUpgradeProcessor(SocketWrapperBase socket, UpgradeToken upgradeToken) { + throw new IllegalStateException(sm.getString("ajpprotocol.noUpgradeHandler", + upgradeToken.getHttpUpgradeHandler().getClass().getName())); + } + + + @Override + public void start() throws Exception { + if (getSecretRequired()) { + String secret = getSecret(); + if (secret == null || secret.length() == 0) { + throw new IllegalArgumentException(sm.getString("ajpprotocol.noSecret")); + } + } + super.start(); + } +} diff --git a/java/org/apache/coyote/ajp/AjpMessage.java b/java/org/apache/coyote/ajp/AjpMessage.java new file mode 100644 index 0000000..c20f4a2 --- /dev/null +++ b/java/org/apache/coyote/ajp/AjpMessage.java @@ -0,0 +1,406 @@ +/* + * 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.coyote.ajp; + +import java.nio.ByteBuffer; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.res.StringManager; + +/** + * A single packet for communication between the web server and the container. Designed to be reused many times with no + * creation of garbage. Understands the format of data types for these packets. Can be used (somewhat confusingly) for + * both incoming and outgoing packets. + * + * @author Henri Gomez + * @author Dan Milstein + * @author Keith Wannamaker + * @author Kevin Seguin + * @author Costin Manolache + */ +public class AjpMessage { + + + private static final Log log = LogFactory.getLog(AjpMessage.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(AjpMessage.class); + + + // ------------------------------------------------------------ Constructor + + + public AjpMessage(int packetSize) { + buf = new byte[packetSize]; + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Fixed size buffer. + */ + protected final byte buf[]; + + + /** + * The current read or write position in the buffer. + */ + protected int pos; + + + /** + * This actually means different things depending on whether the packet is read or write. For read, it's the length + * of the payload (excluding the header). For write, it's the length of the packet as a whole (counting the header). + * Oh, well. + */ + protected int len; + + + // --------------------------------------------------------- Public Methods + + + /** + * Prepare this packet for accumulating a message from the container to the web server. Set the write position to + * just after the header (but leave the length unwritten, because it is as yet unknown). + */ + public void reset() { + len = 4; + pos = 4; + } + + + /** + * For a packet to be sent to the web server, finish the process of accumulating data and write the length of the + * data payload into the header. + */ + public void end() { + len = pos; + int dLen = len - 4; + + buf[0] = (byte) 0x41; + buf[1] = (byte) 0x42; + buf[2] = (byte) ((dLen >>> 8) & 0xFF); + buf[3] = (byte) (dLen & 0xFF); + } + + + /** + * Return the underlying byte buffer. + * + * @return The buffer + */ + public byte[] getBuffer() { + return buf; + } + + + /** + * Return the current message length. + * + * @return For read, it's the length of the payload (excluding the header). For write, it's the length of the packet + * as a whole (counting the header). + */ + public int getLen() { + return len; + } + + + /** + * Add a short integer (2 bytes) to the message. + * + * @param val The integer to append + */ + public void appendInt(int val) { + buf[pos++] = (byte) ((val >>> 8) & 0xFF); + buf[pos++] = (byte) (val & 0xFF); + } + + + /** + * Append a byte (1 byte) to the message. + * + * @param val The byte value to append + */ + public void appendByte(int val) { + buf[pos++] = (byte) val; + } + + + /** + * Write a MessageBytes out at the current write position. A null MessageBytes is encoded as a string with length 0. + * + * @param mb The data to write + */ + public void appendBytes(MessageBytes mb) { + if (mb == null) { + log.error(sm.getString("ajpmessage.null"), new NullPointerException()); + appendInt(0); + appendByte(0); + return; + } + if (mb.getType() != MessageBytes.T_BYTES) { + mb.toBytes(); + ByteChunk bc = mb.getByteChunk(); + // Need to filter out CTLs excluding TAB. ISO-8859-1 and UTF-8 + // values will be OK. Strings using other encodings may be + // corrupted. + byte[] buffer = bc.getBuffer(); + for (int i = bc.getOffset(); i < bc.getLength(); i++) { + // byte values are signed i.e. -128 to 127 + // The values are used unsigned. 0 to 31 are CTLs so they are + // filtered (apart from TAB which is 9). 127 is a control (DEL). + // The values 128 to 255 are all OK. Converting those to signed + // gives -128 to -1. + if ((buffer[i] > -1 && buffer[i] <= 31 && buffer[i] != 9) || buffer[i] == 127) { + buffer[i] = ' '; + } + } + } + appendByteChunk(mb.getByteChunk()); + } + + + /** + * Write a ByteChunk out at the current write position. A null ByteChunk is encoded as a string with length 0. + * + * @param bc The data to write + */ + public void appendByteChunk(ByteChunk bc) { + if (bc == null) { + log.error(sm.getString("ajpmessage.null"), new NullPointerException()); + appendInt(0); + appendByte(0); + return; + } + appendBytes(bc.getBytes(), bc.getStart(), bc.getLength()); + } + + + /** + * Copy a chunk of bytes into the packet, starting at the current write position. The chunk of bytes is encoded with + * the length in two bytes first, then the data itself, and finally a terminating \0 (which is not included + * in the encoded length). + * + * @param b The array from which to copy bytes. + * @param off The offset into the array at which to start copying + * @param numBytes The number of bytes to copy. + */ + public void appendBytes(byte[] b, int off, int numBytes) { + if (checkOverflow(numBytes)) { + return; + } + appendInt(numBytes); + System.arraycopy(b, off, buf, pos, numBytes); + pos += numBytes; + appendByte(0); + } + + + /** + * Copy a chunk of bytes into the packet, starting at the current write position. The chunk of bytes is encoded with + * the length in two bytes first, then the data itself, and finally a terminating \0 (which is not included + * in the encoded length). + * + * @param b The ByteBuffer from which to copy bytes. + */ + public void appendBytes(ByteBuffer b) { + int numBytes = b.remaining(); + if (checkOverflow(numBytes)) { + return; + } + appendInt(numBytes); + b.get(buf, pos, numBytes); + pos += numBytes; + appendByte(0); + } + + + private boolean checkOverflow(int numBytes) { + if (pos + numBytes + 3 > buf.length) { + log.error(sm.getString("ajpmessage.overflow", "" + numBytes, "" + pos), + new ArrayIndexOutOfBoundsException()); + if (log.isDebugEnabled()) { + dump("Overflow/coBytes"); + } + return true; + } + return false; + } + + + /** + * Read an integer from packet, and advance the read position past it. Integers are encoded as two unsigned bytes + * with the high-order byte first, and, as far as I can tell, in little-endian order within each byte. + * + * @return The integer value read from the message + */ + public int getInt() { + int b1 = buf[pos++] & 0xFF; + int b2 = buf[pos++] & 0xFF; + validatePos(pos); + return (b1 << 8) + b2; + } + + + public int peekInt() { + validatePos(pos + 2); + int b1 = buf[pos] & 0xFF; + int b2 = buf[pos + 1] & 0xFF; + return (b1 << 8) + b2; + } + + + public byte getByte() { + byte res = buf[pos++]; + validatePos(pos); + return res; + } + + + public void getBytes(MessageBytes mb) { + doGetBytes(mb, true); + } + + public void getBodyBytes(MessageBytes mb) { + doGetBytes(mb, false); + } + + private void doGetBytes(MessageBytes mb, boolean terminated) { + int length = getInt(); + if (length == 0xFFFF || length == -1) { + mb.recycle(); + return; + } + if (terminated) { + validatePos(pos + length + 1); + } else { + validatePos(pos + length); + } + mb.setBytes(buf, pos, length); + mb.getCharChunk().recycle(); // not valid anymore + pos += length; + if (terminated) { + pos++; // Skip the terminating \0 + } + } + + + /** + * Read a 32 bits integer from packet, and advance the read position past it. Integers are encoded as four unsigned + * bytes with the high-order byte first, and, as far as I can tell, in little-endian order within each byte. + * + * @return The long value read from the message + */ + public int getLongInt() { + int b1 = buf[pos++] & 0xFF; // No swap, Java order + b1 <<= 8; + b1 |= (buf[pos++] & 0xFF); + b1 <<= 8; + b1 |= (buf[pos++] & 0xFF); + b1 <<= 8; + b1 |= (buf[pos++] & 0xFF); + validatePos(pos); + return b1; + } + + + public int processHeader(boolean toContainer) { + pos = 0; + int mark = getInt(); + len = getInt(); + // Verify message signature + if ((toContainer && mark != 0x1234) || (!toContainer && mark != 0x4142)) { + log.error(sm.getString("ajpmessage.invalid", "" + mark)); + if (log.isTraceEnabled()) { + dump("In"); + } + return -1; + } + if (log.isTraceEnabled()) { + log.trace("Received " + len + " " + buf[0]); + } + return len; + } + + + private void dump(String prefix) { + if (log.isTraceEnabled()) { + log.trace(prefix + ": " + HexUtils.toHexString(buf) + " " + pos + "/" + (len + 4)); + } + int max = pos; + if (len + 4 > pos) { + max = len + 4; + } + if (max > 1000) { + max = 1000; + } + if (log.isTraceEnabled()) { + for (int j = 0; j < max; j += 16) { + log.trace(hexLine(buf, j, len)); + } + } + } + + + private void validatePos(int posToTest) { + if (posToTest > len + 4) { + // Trying to read data beyond the end of the AJP message + throw new ArrayIndexOutOfBoundsException(sm.getString("ajpMessage.invalidPos", Integer.valueOf(posToTest))); + } + } + // ------------------------------------------------------ Protected Methods + + + protected static String hexLine(byte buf[], int start, int len) { + StringBuilder sb = new StringBuilder(); + for (int i = start; i < start + 16; i++) { + if (i < len + 4) { + sb.append(hex(buf[i])).append(' '); + } else { + sb.append(" "); + } + } + sb.append(" | "); + for (int i = start; i < start + 16 && i < len + 4; i++) { + if (!Character.isISOControl((char) buf[i])) { + sb.append(Character.valueOf((char) buf[i])); + } else { + sb.append('.'); + } + } + return sb.toString(); + } + + + protected static String hex(int x) { + String h = Integer.toHexString(x); + if (h.length() == 1) { + h = "0" + h; + } + return h.substring(h.length() - 2); + } + + +} diff --git a/java/org/apache/coyote/ajp/AjpNio2Protocol.java b/java/org/apache/coyote/ajp/AjpNio2Protocol.java new file mode 100644 index 0000000..857d6ac --- /dev/null +++ b/java/org/apache/coyote/ajp/AjpNio2Protocol.java @@ -0,0 +1,51 @@ +/* + * 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.coyote.ajp; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.Nio2Channel; +import org.apache.tomcat.util.net.Nio2Endpoint; + + +/** + * This the NIO2 based protocol handler implementation for AJP. + */ +public class AjpNio2Protocol extends AbstractAjpProtocol { + + private static final Log log = LogFactory.getLog(AjpNio2Protocol.class); + + @Override + protected Log getLog() { + return log; + } + + + // ------------------------------------------------------------ Constructor + + public AjpNio2Protocol() { + super(new Nio2Endpoint()); + } + + + // ----------------------------------------------------- JMX related methods + + @Override + protected String getNamePrefix() { + return "ajp-nio2"; + } +} diff --git a/java/org/apache/coyote/ajp/AjpNioProtocol.java b/java/org/apache/coyote/ajp/AjpNioProtocol.java new file mode 100644 index 0000000..0b6cce7 --- /dev/null +++ b/java/org/apache/coyote/ajp/AjpNioProtocol.java @@ -0,0 +1,50 @@ +/* + * 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.coyote.ajp; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.NioChannel; +import org.apache.tomcat.util.net.NioEndpoint; + +/** + * This the NIO based protocol handler implementation for AJP. + */ +public class AjpNioProtocol extends AbstractAjpProtocol { + + private static final Log log = LogFactory.getLog(AjpNioProtocol.class); + + @Override + protected Log getLog() { + return log; + } + + + // ------------------------------------------------------------ Constructor + + public AjpNioProtocol() { + super(new NioEndpoint()); + } + + + // ----------------------------------------------------- JMX related methods + + @Override + protected String getNamePrefix() { + return "ajp-nio"; + } +} diff --git a/java/org/apache/coyote/ajp/AjpProcessor.java b/java/org/apache/coyote/ajp/AjpProcessor.java new file mode 100644 index 0000000..d13e326 --- /dev/null +++ b/java/org/apache/coyote/ajp/AjpProcessor.java @@ -0,0 +1,1341 @@ +/* + * 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.coyote.ajp; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import jakarta.servlet.ServletConnection; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.coyote.AbstractProcessor; +import org.apache.coyote.ActionCode; +import org.apache.coyote.Adapter; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.coyote.ErrorState; +import org.apache.coyote.InputBuffer; +import org.apache.coyote.OutputBuffer; +import org.apache.coyote.RequestInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +/** + * AJP Processor implementation. + */ +public class AjpProcessor extends AbstractProcessor { + + private static final Log log = LogFactory.getLog(AjpProcessor.class); + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(AjpProcessor.class); + + + /** + * End message array. + */ + private static final byte[] endMessageArray; + private static final byte[] endAndCloseMessageArray; + + + /** + * Flush message array. + */ + private static final byte[] flushMessageArray; + + + /** + * Pong message array. + */ + private static final byte[] pongMessageArray; + + + private static final Map jakartaAttributeMapping; + private static final Set iisTlsAttributes; + + + static { + // Allocate the end message array + AjpMessage endMessage = new AjpMessage(16); + endMessage.reset(); + endMessage.appendByte(Constants.JK_AJP13_END_RESPONSE); + endMessage.appendByte(1); + endMessage.end(); + endMessageArray = new byte[endMessage.getLen()]; + System.arraycopy(endMessage.getBuffer(), 0, endMessageArray, 0, endMessage.getLen()); + + // Allocate the end and close message array + AjpMessage endAndCloseMessage = new AjpMessage(16); + endAndCloseMessage.reset(); + endAndCloseMessage.appendByte(Constants.JK_AJP13_END_RESPONSE); + endAndCloseMessage.appendByte(0); + endAndCloseMessage.end(); + endAndCloseMessageArray = new byte[endAndCloseMessage.getLen()]; + System.arraycopy(endAndCloseMessage.getBuffer(), 0, endAndCloseMessageArray, 0, endAndCloseMessage.getLen()); + + // Allocate the flush message array + AjpMessage flushMessage = new AjpMessage(16); + flushMessage.reset(); + flushMessage.appendByte(Constants.JK_AJP13_SEND_BODY_CHUNK); + flushMessage.appendInt(0); + flushMessage.appendByte(0); + flushMessage.end(); + flushMessageArray = new byte[flushMessage.getLen()]; + System.arraycopy(flushMessage.getBuffer(), 0, flushMessageArray, 0, flushMessage.getLen()); + + // Allocate the pong message array + AjpMessage pongMessage = new AjpMessage(16); + pongMessage.reset(); + pongMessage.appendByte(Constants.JK_AJP13_CPONG_REPLY); + pongMessage.end(); + pongMessageArray = new byte[pongMessage.getLen()]; + System.arraycopy(pongMessage.getBuffer(), 0, pongMessageArray, 0, pongMessage.getLen()); + + // Build Map of Java Servlet to Jakarta Servlet attribute names + Map m = new HashMap<>(); + m.put("jakarta.servlet.request.cipher_suite", "jakarta.servlet.request.cipher_suite"); + m.put("jakarta.servlet.request.key_size", "jakarta.servlet.request.key_size"); + m.put("jakarta.servlet.request.ssl_session", "jakarta.servlet.request.ssl_session"); + m.put("jakarta.servlet.request.X509Certificate", "jakarta.servlet.request.X509Certificate"); + m.put("javax.servlet.request.cipher_suite", "jakarta.servlet.request.cipher_suite"); + m.put("javax.servlet.request.key_size", "jakarta.servlet.request.key_size"); + m.put("javax.servlet.request.ssl_session", "jakarta.servlet.request.ssl_session"); + m.put("javax.servlet.request.X509Certificate", "jakarta.servlet.request.X509Certificate"); + jakartaAttributeMapping = Collections.unmodifiableMap(m); + + Set s = new HashSet<>(); + s.add("CERT_ISSUER"); + s.add("CERT_SUBJECT"); + s.add("CERT_COOKIE"); + s.add("HTTPS_SERVER_SUBJECT"); + s.add("CERT_FLAGS"); + s.add("HTTPS_SECRETKEYSIZE"); + s.add("CERT_SERIALNUMBER"); + s.add("HTTPS_SERVER_ISSUER"); + s.add("HTTPS_KEYSIZE"); + iisTlsAttributes = Collections.unmodifiableSet(s); + } + + + // ----------------------------------------------------- Instance Variables + + private final AbstractAjpProtocol protocol; + + + /** + * GetBody message array. Not static like the other message arrays since the message varies with packetSize and that + * can vary per connector. + */ + private final byte[] getBodyMessageArray; + + + /** + * AJP packet size. + */ + private final int outputMaxChunkSize; + + /** + * Header message. Note that this header is merely the one used during the processing of the first message of a + * "request", so it might not be a request header. It will stay unchanged during the processing of the whole + * request. + */ + private final AjpMessage requestHeaderMessage; + + + /** + * Message used for response composition. + */ + private final AjpMessage responseMessage; + + + /** + * Location of next write of the response message (used with non-blocking writes when the message may not be written + * in a single write). A value of -1 indicates that no message has been written to the buffer. + */ + private int responseMsgPos = -1; + + + /** + * Body message. + */ + private final AjpMessage bodyMessage; + + + /** + * Body message. + */ + private final MessageBytes bodyBytes = MessageBytes.newInstance(); + + + /** + * Temp message bytes used for processing. + */ + private final MessageBytes tmpMB = MessageBytes.newInstance(); + + + /** + * Byte chunk for certs. + */ + private final MessageBytes certificates = MessageBytes.newInstance(); + + + /** + * End of stream flag. + */ + private boolean endOfStream = false; + + + /** + * Request body empty flag. + */ + private boolean empty = true; + + + /** + * First read. + */ + private boolean first = true; + + + /** + * Indicates that a 'get body chunk' message has been sent but the body chunk has not yet been received. + */ + private boolean waitingForBodyMessage = false; + + + /** + * Replay read. + */ + private boolean replay = false; + + + /** + * Should any response body be swallowed and not sent to the client. + */ + private boolean swallowResponse = false; + + + /** + * Finished response. + */ + private boolean responseFinished = false; + + + /** + * Bytes written to client for the current request. + */ + private long bytesWritten = 0; + + + // ------------------------------------------------------------ Constructor + + public AjpProcessor(AbstractAjpProtocol protocol, Adapter adapter) { + super(adapter); + this.protocol = protocol; + + int packetSize = protocol.getPacketSize(); + // Calculate maximum chunk size as packetSize may have been changed from + // the default (Constants.MAX_PACKET_SIZE) + this.outputMaxChunkSize = packetSize - Constants.SEND_HEAD_LEN; + + request.setInputBuffer(new SocketInputBuffer()); + + requestHeaderMessage = new AjpMessage(packetSize); + responseMessage = new AjpMessage(packetSize); + bodyMessage = new AjpMessage(packetSize); + + // Set the getBody message buffer + AjpMessage getBodyMessage = new AjpMessage(16); + getBodyMessage.reset(); + getBodyMessage.appendByte(Constants.JK_AJP13_GET_BODY_CHUNK); + // Adjust read size if packetSize != default (Constants.MAX_PACKET_SIZE) + getBodyMessage.appendInt(Constants.MAX_READ_SIZE + packetSize - Constants.MAX_PACKET_SIZE); + getBodyMessage.end(); + getBodyMessageArray = new byte[getBodyMessage.getLen()]; + System.arraycopy(getBodyMessage.getBuffer(), 0, getBodyMessageArray, 0, getBodyMessage.getLen()); + + response.setOutputBuffer(new SocketOutputBuffer()); + } + + + // --------------------------------------------------------- Public Methods + + @Override + protected boolean flushBufferedWrite() throws IOException { + if (hasDataToWrite()) { + socketWrapper.flush(false); + if (hasDataToWrite()) { + // There is data to write but go via Response to + // maintain a consistent view of non-blocking state + response.checkRegisterForWrite(); + return true; + } + } + return false; + } + + + @Override + protected void dispatchNonBlockingRead() { + if (available(true) > 0) { + super.dispatchNonBlockingRead(); + } + } + + + @Override + protected SocketState dispatchEndRequest() { + // Set keep alive timeout for next request + socketWrapper.setReadTimeout(protocol.getKeepAliveTimeout()); + recycle(); + if (protocol.isPaused()) { + return SocketState.CLOSED; + } else { + return SocketState.OPEN; + } + } + + + @Override + public SocketState service(SocketWrapperBase socket) throws IOException { + + RequestInfo rp = request.getRequestProcessor(); + rp.setStage(org.apache.coyote.Constants.STAGE_PARSE); + + // Setting up the socket + this.socketWrapper = socket; + + boolean cping = false; + // Expected to block on the first read as there should be at least one + // AJP message to read. + boolean firstRead = true; + + while (!getErrorState().isError() && !protocol.isPaused()) { + // Parsing the request header + try { + // Get first message of the request + if (!readMessage(requestHeaderMessage, firstRead)) { + break; + } + firstRead = false; + + // Processing the request so make sure the connection rather + // than keep-alive timeout is used + socketWrapper.setReadTimeout(protocol.getConnectionTimeout()); + + // Check message type, process right away and break if + // not regular request processing + int type = requestHeaderMessage.getByte(); + if (type == Constants.JK_AJP13_CPING_REQUEST) { + if (protocol.isPaused()) { + recycle(); + break; + } + cping = true; + try { + socketWrapper.write(true, pongMessageArray, 0, pongMessageArray.length); + socketWrapper.flush(true); + } catch (IOException e) { + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("ajpprocessor.pongFail"), e); + } + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } + recycle(); + continue; + } else if (type != Constants.JK_AJP13_FORWARD_REQUEST) { + // Unexpected packet type. Unread body packets should have + // been swallowed in finish(). + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString("ajpprocessor.unexpectedMessage", Integer.toString(type))); + } + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, null); + break; + } + request.setStartTimeNanos(System.nanoTime()); + } catch (IOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + break; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + getLog().debug(sm.getString("ajpprocessor.header.error"), t); + // 400 - Bad Request + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, t); + } + + if (getErrorState().isIoAllowed()) { + // Setting up filters, and parse some request headers + rp.setStage(org.apache.coyote.Constants.STAGE_PREPARE); + try { + prepareRequest(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + getLog().debug(sm.getString("ajpprocessor.request.prepare"), t); + // 500 - Internal Server Error + response.setStatus(500); + setErrorState(ErrorState.CLOSE_CLEAN, t); + } + } + + if (getErrorState().isIoAllowed() && !cping && protocol.isPaused()) { + // 503 - Service unavailable + response.setStatus(503); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + cping = false; + + // Process the request in the adapter + if (getErrorState().isIoAllowed()) { + try { + rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE); + getAdapter().service(request, response); + } catch (InterruptedIOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + getLog().error(sm.getString("ajpprocessor.request.process"), t); + // 500 - Internal Server Error + response.setStatus(500); + setErrorState(ErrorState.CLOSE_CLEAN, t); + getAdapter().log(request, response, 0); + } + } + + if (isAsync() && !getErrorState().isError()) { + break; + } + + // Finish the response if not done yet + if (!responseFinished && getErrorState().isIoAllowed()) { + try { + action(ActionCode.COMMIT, null); + finishResponse(); + } catch (IOException ioe) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + setErrorState(ErrorState.CLOSE_NOW, t); + } + } + + // If there was an error, make sure the request is counted as + // and error, and update the statistics counter + if (getErrorState().isError()) { + response.setStatus(500); + } + request.updateCounters(); + + rp.setStage(org.apache.coyote.Constants.STAGE_KEEPALIVE); + + // Set keep alive timeout for next request + socketWrapper.setReadTimeout(protocol.getKeepAliveTimeout()); + + recycle(); + } + + rp.setStage(org.apache.coyote.Constants.STAGE_ENDED); + + if (getErrorState().isError() || protocol.isPaused()) { + return SocketState.CLOSED; + } else { + if (isAsync()) { + return SocketState.LONG; + } else { + return SocketState.OPEN; + } + } + } + + + @Override + public void recycle() { + getAdapter().checkRecycled(request, response); + super.recycle(); + request.recycle(); + response.recycle(); + first = true; + endOfStream = false; + waitingForBodyMessage = false; + empty = true; + replay = false; + responseFinished = false; + certificates.recycle(); + swallowResponse = false; + bytesWritten = 0; + } + + + @Override + public void pause() { + // NOOP for AJP + } + + + // ------------------------------------------------------ Protected Methods + + // Methods used by SocketInputBuffer + /** + * Read an AJP body message. Used to read both the 'special' packet in ajp13 and to receive the data after we send a + * GET_BODY packet. + * + * @param block If there is no data available to read when this method is called, should this call block until data + * becomes available? + * + * @return true if at least one body byte was read, otherwise false + */ + private boolean receive(boolean block) throws IOException { + + bodyMessage.reset(); + + if (!readMessage(bodyMessage, block)) { + return false; + } + + waitingForBodyMessage = false; + + // No data received. + if (bodyMessage.getLen() == 0) { + // just the header + return false; + } + int blen = bodyMessage.peekInt(); + if (blen == 0) { + return false; + } + + bodyMessage.getBodyBytes(bodyBytes); + empty = false; + return true; + } + + + /** + * Read an AJP message. + * + * @param message The message to populate + * @param block If there is no data available to read when this method is called, should this call block until + * data becomes available? + * + * @return true if the message has been read, false if no data was read + * + * @throws IOException any other failure, including incomplete reads + */ + private boolean readMessage(AjpMessage message, boolean block) throws IOException { + + byte[] buf = message.getBuffer(); + + if (!read(buf, 0, Constants.H_SIZE, block)) { + return false; + } + + int messageLength = message.processHeader(true); + if (messageLength < 0) { + // Invalid AJP header signature + throw new IOException(sm.getString("ajpmessage.invalidLength", Integer.valueOf(messageLength))); + } else if (messageLength == 0) { + // Zero length message. + return true; + } else { + if (messageLength > message.getBuffer().length) { + // Message too long for the buffer + // Need to trigger a 400 response + String msg = sm.getString("ajpprocessor.header.tooLong", Integer.valueOf(messageLength), + Integer.valueOf(buf.length)); + log.error(msg); + throw new IllegalArgumentException(msg); + } + read(buf, Constants.H_SIZE, messageLength, true); + return true; + } + } + + + /** + * Get more request body data from the web server and store it in the internal buffer. + * + * @param block true if this is blocking IO + * + * @return true if there is more data, false if not. + * + * @throws IOException An IO error occurred + */ + protected boolean refillReadBuffer(boolean block) throws IOException { + // When using replay (e.g. after FORM auth) all the data to read has + // been buffered so there is no opportunity to refill the buffer. + if (replay) { + endOfStream = true; // we've read everything there is + } + if (endOfStream) { + return false; + } + + if (first) { + first = false; + long contentLength = request.getContentLengthLong(); + // - When content length > 0, AJP sends the first body message + // automatically. + // - When content length == 0, AJP does not send a body message. + // - When content length is unknown, AJP does not send the first + // body message automatically. + if (contentLength > 0) { + waitingForBodyMessage = true; + } else if (contentLength == 0) { + endOfStream = true; + return false; + } + } + + // Request more data immediately + if (!waitingForBodyMessage) { + socketWrapper.write(true, getBodyMessageArray, 0, getBodyMessageArray.length); + socketWrapper.flush(true); + waitingForBodyMessage = true; + } + + boolean moreData = receive(block); + if (!moreData && !waitingForBodyMessage) { + endOfStream = true; + } + return moreData; + } + + + /** + * After reading the request headers, we have to setup the request filters. + */ + private void prepareRequest() { + + // Translate the HTTP method code to a String. + byte methodCode = requestHeaderMessage.getByte(); + if (methodCode != Constants.SC_M_JK_STORED) { + String methodName = Constants.getMethodForCode(methodCode - 1); + request.method().setString(methodName); + } + + requestHeaderMessage.getBytes(request.protocol()); + requestHeaderMessage.getBytes(request.requestURI()); + + requestHeaderMessage.getBytes(request.remoteAddr()); + requestHeaderMessage.getBytes(request.remoteHost()); + requestHeaderMessage.getBytes(request.localName()); + request.setLocalPort(requestHeaderMessage.getInt()); + + if (socketWrapper != null) { + request.peerAddr().setString(socketWrapper.getRemoteAddr()); + } + + boolean isSSL = requestHeaderMessage.getByte() != 0; + if (isSSL) { + request.scheme().setString("https"); + } + + // Decode headers + MimeHeaders headers = request.getMimeHeaders(); + + // Set this every time in case limit has been changed via JMX + headers.setLimit(protocol.getMaxHeaderCount()); + + boolean contentLengthSet = false; + int hCount = requestHeaderMessage.getInt(); + for (int i = 0; i < hCount; i++) { + String hName = null; + + // Header names are encoded as either an integer code starting + // with 0xA0, or as a normal string (in which case the first + // two bytes are the length). + int isc = requestHeaderMessage.peekInt(); + int hId = isc & 0xFF; + + MessageBytes vMB = null; + isc &= 0xFF00; + if (0xA000 == isc) { + requestHeaderMessage.getInt(); // To advance the read position + hName = Constants.getHeaderForCode(hId - 1); + vMB = headers.addValue(hName); + } else { + // reset hId -- if the header currently being read + // happens to be 7 or 8 bytes long, the code below + // will think it's the content-type header or the + // content-length header - SC_REQ_CONTENT_TYPE=7, + // SC_REQ_CONTENT_LENGTH=8 - leading to unexpected + // behaviour. see bug 5861 for more information. + hId = -1; + requestHeaderMessage.getBytes(tmpMB); + ByteChunk bc = tmpMB.getByteChunk(); + vMB = headers.addValue(bc.getBuffer(), bc.getStart(), bc.getLength()); + } + + requestHeaderMessage.getBytes(vMB); + + if (hId == Constants.SC_REQ_CONTENT_LENGTH || (hId == -1 && tmpMB.equalsIgnoreCase("Content-Length"))) { + long cl = vMB.getLong(); + if (contentLengthSet) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } else { + contentLengthSet = true; + // Set the content-length header for the request + request.setContentLength(cl); + } + } else if (hId == Constants.SC_REQ_CONTENT_TYPE || (hId == -1 && tmpMB.equalsIgnoreCase("Content-Type"))) { + // just read the content-type header, so set it + ByteChunk bchunk = vMB.getByteChunk(); + request.contentType().setBytes(bchunk.getBytes(), bchunk.getOffset(), bchunk.getLength()); + } + } + + // Decode extra attributes + String secret = protocol.getSecret(); + boolean secretPresentInRequest = false; + byte attributeCode; + while ((attributeCode = requestHeaderMessage.getByte()) != Constants.SC_A_ARE_DONE) { + + switch (attributeCode) { + + case Constants.SC_A_REQ_ATTRIBUTE: + requestHeaderMessage.getBytes(tmpMB); + String n = tmpMB.toString(); + requestHeaderMessage.getBytes(tmpMB); + String v = tmpMB.toString(); + /* + * AJP13 misses to forward the local IP address and the remote port. Allow the AJP connector to add + * this info via private request attributes. We will accept the forwarded data and remove it from + * the public list of request attributes. + */ + if (n.equals(Constants.SC_A_REQ_LOCAL_ADDR)) { + request.localAddr().setString(v); + } else if (n.equals(Constants.SC_A_REQ_REMOTE_PORT)) { + try { + request.setRemotePort(Integer.parseInt(v)); + } catch (NumberFormatException nfe) { + // Ignore invalid value + } + } else if (n.equals(Constants.SC_A_SSL_PROTOCOL)) { + request.setAttribute(SSLSupport.PROTOCOL_VERSION_KEY, v); + } else if (n.equals("JK_LB_ACTIVATION")) { + request.setAttribute(n, v); + } else if (jakartaAttributeMapping.containsKey(n)) { + // AJP uses the Java Servlet attribute names. + // Need to convert these to Jakarta Servlet. + request.setAttribute(jakartaAttributeMapping.get(n), v); + } else if (iisTlsAttributes.contains(n)) { + // Allow IIS TLS attributes + request.setAttribute(n, v); + } else { + // All 'known' attributes will be processed by the previous + // blocks. Any remaining attribute is an 'arbitrary' one. + Pattern pattern = protocol.getAllowedRequestAttributesPatternInternal(); + if (pattern != null && pattern.matcher(n).matches()) { + request.setAttribute(n, v); + } else { + log.warn(sm.getString("ajpprocessor.unknownAttribute", n)); + response.setStatus(403); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + } + break; + + case Constants.SC_A_CONTEXT: + requestHeaderMessage.getBytes(tmpMB); + // nothing + break; + + case Constants.SC_A_SERVLET_PATH: + requestHeaderMessage.getBytes(tmpMB); + // nothing + break; + + case Constants.SC_A_REMOTE_USER: + boolean tomcatAuthorization = protocol.getTomcatAuthorization(); + if (tomcatAuthorization || !protocol.getTomcatAuthentication()) { + // Implies tomcatAuthentication == false + requestHeaderMessage.getBytes(request.getRemoteUser()); + request.setRemoteUserNeedsAuthorization(tomcatAuthorization); + } else { + // Ignore user information from reverse proxy + requestHeaderMessage.getBytes(tmpMB); + } + break; + + case Constants.SC_A_AUTH_TYPE: + if (protocol.getTomcatAuthorization() || !protocol.getTomcatAuthentication()) { + // Implies tomcatAuthentication == false + requestHeaderMessage.getBytes(request.getAuthType()); + } else { + // Ignore user information from reverse proxy + requestHeaderMessage.getBytes(tmpMB); + } + break; + + case Constants.SC_A_QUERY_STRING: + requestHeaderMessage.getBytes(request.queryString()); + break; + + case Constants.SC_A_JVM_ROUTE: + requestHeaderMessage.getBytes(tmpMB); + // nothing + break; + + case Constants.SC_A_SSL_CERT: + // SSL certificate extraction is lazy, moved to JkCoyoteHandler + requestHeaderMessage.getBytes(certificates); + break; + + case Constants.SC_A_SSL_CIPHER: + requestHeaderMessage.getBytes(tmpMB); + request.setAttribute(SSLSupport.CIPHER_SUITE_KEY, tmpMB.toString()); + break; + + case Constants.SC_A_SSL_SESSION: + requestHeaderMessage.getBytes(tmpMB); + request.setAttribute(SSLSupport.SESSION_ID_KEY, tmpMB.toString()); + break; + + case Constants.SC_A_SSL_KEY_SIZE: + request.setAttribute(SSLSupport.KEY_SIZE_KEY, Integer.valueOf(requestHeaderMessage.getInt())); + break; + + case Constants.SC_A_STORED_METHOD: + requestHeaderMessage.getBytes(request.method()); + break; + + case Constants.SC_A_SECRET: + requestHeaderMessage.getBytes(tmpMB); + if (secret != null && secret.length() > 0) { + secretPresentInRequest = true; + if (!tmpMB.equals(secret)) { + response.setStatus(403); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + } + break; + + default: + // Ignore unknown attribute for backward compatibility + break; + + } + + } + + // Check if secret was submitted if required + if (secret != null && secret.length() > 0 && !secretPresentInRequest) { + response.setStatus(403); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + + // Check for a full URI (including protocol://host:port/) + ByteChunk uriBC = request.requestURI().getByteChunk(); + if (uriBC.startsWithIgnoreCase("http", 0)) { + + int pos = uriBC.indexOf("://", 0, 3, 4); + int uriBCStart = uriBC.getStart(); + int slashPos = -1; + if (pos != -1) { + byte[] uriB = uriBC.getBytes(); + slashPos = uriBC.indexOf('/', pos + 3); + if (slashPos == -1) { + slashPos = uriBC.getLength(); + // Set URI as "/" + request.requestURI().setBytes(uriB, uriBCStart + pos + 1, 1); + } else { + request.requestURI().setBytes(uriB, uriBCStart + slashPos, uriBC.getLength() - slashPos); + } + MessageBytes hostMB = headers.setValue("host"); + hostMB.setBytes(uriB, uriBCStart + pos + 3, slashPos - pos - 3); + } + + } + + MessageBytes valueMB = request.getMimeHeaders().getValue("host"); + parseHost(valueMB); + + if (!getErrorState().isIoAllowed()) { + getAdapter().log(request, response, 0); + } + } + + + /** + * {@inheritDoc} + *

    + * This implementation populates the server name from the local name provided by the AJP message. + */ + @Override + protected void populateHost() { + try { + request.serverName().duplicate(request.localName()); + } catch (IOException e) { + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, e); + } + } + + + /** + * {@inheritDoc} + *

    + * This implementation populates the server port from the local port provided by the AJP message. + */ + @Override + protected void populatePort() { + // No host information (HTTP/1.0) + request.setServerPort(request.getLocalPort()); + } + + + /** + * When committing the response, we have to validate the set of headers, as well as setup the response filters. + */ + @Override + protected final void prepareResponse() throws IOException { + + response.setCommitted(true); + + // Responses with certain status codes and/or methods are not permitted to include a response body. + int statusCode = response.getStatus(); + if (statusCode < 200 || statusCode == 204 || statusCode == 205 || statusCode == 304 || + request.method().equals("HEAD")) { + // No entity body + swallowResponse = true; + } + + // Prepare special headers + MimeHeaders headers = response.getMimeHeaders(); + String contentType = response.getContentType(); + if (contentType != null) { + headers.setValue("Content-Type").setString(contentType); + } + String contentLanguage = response.getContentLanguage(); + if (contentLanguage != null) { + headers.setValue("Content-Language").setString(contentLanguage); + } + long contentLength = response.getContentLengthLong(); + if (contentLength >= 0) { + headers.setValue("Content-Length").setLong(contentLength); + } + + tmpMB.recycle(); + responseMsgPos = -1; + + int numHeaders = headers.size(); + boolean needAjpMessageHeader = true; + while (needAjpMessageHeader) { + // Write AJP message header + responseMessage.reset(); + responseMessage.appendByte(Constants.JK_AJP13_SEND_HEADERS); + + // Write HTTP response line + responseMessage.appendInt(statusCode); + // Reason phrase is optional but mod_jk + httpd 2.x fails with a null + // reason phrase - bug 45026 + tmpMB.setString(Integer.toString(response.getStatus())); + responseMessage.appendBytes(tmpMB); + + // Start headers + responseMessage.appendInt(numHeaders); + + needAjpMessageHeader = false; + + for (int i = 0; i < numHeaders; i++) { + try { + // Write headers + MessageBytes hN = headers.getName(i); + int hC = Constants.getResponseAjpIndex(hN.toString()); + if (hC > 0) { + responseMessage.appendInt(hC); + } else { + responseMessage.appendBytes(hN); + } + MessageBytes hV = headers.getValue(i); + responseMessage.appendBytes(hV); + } catch (IllegalArgumentException iae) { + // Log the problematic header + log.warn(sm.getString("ajpprocessor.response.invalidHeader", headers.getName(i), headers.getValue(i)), + iae); + // Remove the problematic header + headers.removeHeader(i); + numHeaders--; + // Restart writing of AJP message + needAjpMessageHeader = true; + break; + } + } + } + + // Write to buffer + responseMessage.end(); + socketWrapper.write(true, responseMessage.getBuffer(), 0, responseMessage.getLen()); + socketWrapper.flush(true); + } + + + /** + * Callback to write data from the buffer. + */ + @Override + protected final void flush() throws IOException { + // Calling code should ensure that there is no data in the buffers for + // non-blocking writes. + // TODO Validate the assertion above + if (!responseFinished) { + if (protocol.getAjpFlush()) { + // Send the flush message + socketWrapper.write(true, flushMessageArray, 0, flushMessageArray.length); + } + socketWrapper.flush(true); + } + } + + + /** + * Finish AJP response. + */ + @Override + protected final void finishResponse() throws IOException { + if (responseFinished) { + return; + } + + responseFinished = true; + + // Swallow the unread body packet if present + if (waitingForBodyMessage || first && request.getContentLengthLong() > 0) { + refillReadBuffer(true); + } + + // Add the end message + if (getErrorState().isError()) { + socketWrapper.write(true, endAndCloseMessageArray, 0, endAndCloseMessageArray.length); + } else { + socketWrapper.write(true, endMessageArray, 0, endMessageArray.length); + } + socketWrapper.flush(true); + } + + + @Override + protected final void ack(ContinueResponseTiming continueResponseTiming) { + // NO-OP for AJP + } + + + @Override + protected final int available(boolean doRead) { + if (endOfStream) { + return 0; + } + if (empty && doRead) { + try { + refillReadBuffer(false); + } catch (IOException timeout) { + // Not ideal. This will indicate that data is available + // which should trigger a read which in turn will trigger + // another IOException and that one can be thrown. + return 1; + } + } + if (empty) { + return 0; + } else { + return request.getInputBuffer().available(); + } + } + + + @Override + protected final void setRequestBody(ByteChunk body) { + int length = body.getLength(); + bodyBytes.setBytes(body.getBytes(), body.getStart(), length); + request.setContentLength(length); + first = false; + empty = false; + replay = true; + endOfStream = false; + } + + + @Override + protected final void setSwallowResponse() { + swallowResponse = true; + } + + + @Override + protected final void disableSwallowRequest() { + /* + * NO-OP With AJP, Tomcat controls when the client sends request body data. At most there will be a single + * packet to read and that will be handled in finishResponse(). + */ + } + + + @Override + protected final boolean getPopulateRequestAttributesFromSocket() { + // NO-OPs the attribute requests since they are pre-populated when + // parsing the first AJP message. + return false; + } + + + @Override + protected final void populateRequestAttributeRemoteHost() { + // Get remote host name using a DNS resolution + if (request.remoteHost().isNull()) { + try { + request.remoteHost().setString(InetAddress.getByName(request.remoteAddr().toString()).getHostName()); + } catch (IOException iex) { + // Ignore + } + } + } + + + @Override + protected final void populateSslRequestAttributes() { + if (!certificates.isNull()) { + List jsseCerts = new ArrayList<>(); + ByteChunk certData = certificates.getByteChunk(); + ByteArrayInputStream bais = + new ByteArrayInputStream(certData.getBytes(), certData.getStart(), certData.getLength()); + // Fill the elements. + try { + CertificateFactory cf; + String clientCertProvider = protocol.getClientCertProvider(); + if (clientCertProvider == null) { + cf = CertificateFactory.getInstance("X.509"); + } else { + cf = CertificateFactory.getInstance("X.509", clientCertProvider); + } + while (bais.available() > 0) { + X509Certificate cert = (X509Certificate) cf.generateCertificate(bais); + jsseCerts.add(cert); + } + } catch (CertificateException | NoSuchProviderException e) { + getLog().error(sm.getString("ajpprocessor.certs.fail"), e); + return; + } + request.setAttribute(SSLSupport.CERTIFICATE_KEY, jsseCerts.toArray(new X509Certificate[0])); + } + } + + + @Override + protected final boolean isRequestBodyFullyRead() { + return endOfStream; + } + + + @Override + protected final void registerReadInterest() { + socketWrapper.registerReadInterest(); + } + + + @Override + protected final boolean isReadyForWrite() { + return responseMsgPos == -1 && socketWrapper.isReadyForWrite(); + } + + + @Override + protected boolean isTrailerFieldsReady() { + // AJP does not support trailers so return true so app can request the + // trailers and find out that there are none. + return true; + } + + + /** + * Read at least the specified amount of bytes, and place them in the input buffer. Note that if any data is + * available to read then this method will always block until at least the specified number of bytes have been read. + * + * @param buf Buffer to read data into + * @param pos Start position + * @param n The minimum number of bytes to read + * @param block If there is no data available to read when this method is called, should this call block until data + * becomes available? + * + * @return true if the requested number of bytes were read else false + * + * @throws IOException If an I/O error occurs during the read + */ + private boolean read(byte[] buf, int pos, int n, boolean block) throws IOException { + int read = socketWrapper.read(block, buf, pos, n); + if (read > 0 && read < n) { + int left = n - read; + int start = pos + read; + while (left > 0) { + read = socketWrapper.read(true, buf, start, left); + if (read == -1) { + throw new EOFException(); + } + left = left - read; + start = start + read; + } + } else if (read == -1) { + throw new EOFException(); + } + + return read > 0; + } + + + private void writeData(ByteBuffer chunk) throws IOException { + boolean blocking = (response.getWriteListener() == null); + + int len = chunk.remaining(); + int off = 0; + + // Write this chunk + while (len > 0) { + int thisTime = Math.min(len, outputMaxChunkSize); + + responseMessage.reset(); + responseMessage.appendByte(Constants.JK_AJP13_SEND_BODY_CHUNK); + chunk.limit(chunk.position() + thisTime); + responseMessage.appendBytes(chunk); + responseMessage.end(); + socketWrapper.write(blocking, responseMessage.getBuffer(), 0, responseMessage.getLen()); + socketWrapper.flush(blocking); + + len -= thisTime; + off += thisTime; + } + + bytesWritten += off; + } + + + private boolean hasDataToWrite() { + return responseMsgPos != -1 || socketWrapper.hasDataToWrite(); + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + protected ServletConnection getServletConnection() { + return socketWrapper.getServletConnection("ajp", ""); + } + + + // ------------------------------------- InputStreamInputBuffer Inner Class + + /** + * This class is an input buffer which will read its data from an input stream. + */ + protected class SocketInputBuffer implements InputBuffer { + + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + + if (endOfStream) { + return -1; + } + if (empty) { + if (!refillReadBuffer(true)) { + return -1; + } + } + ByteChunk bc = bodyBytes.getByteChunk(); + handler.setByteBuffer(ByteBuffer.wrap(bc.getBuffer(), bc.getStart(), bc.getLength())); + empty = true; + return handler.getByteBuffer().remaining(); + } + + @Override + public int available() { + if (empty) { + return 0; + } else { + return bodyBytes.getByteChunk().getLength(); + } + } + } + + + // ----------------------------------- OutputStreamOutputBuffer Inner Class + + /** + * This class is an output buffer which will write data to an output stream. + */ + protected class SocketOutputBuffer implements OutputBuffer { + + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + + if (!response.isCommitted()) { + // Validate and write response headers + try { + prepareResponse(); + } catch (IOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } + } + + int len = 0; + if (!swallowResponse) { + try { + len = chunk.remaining(); + writeData(chunk); + len -= chunk.remaining(); + } catch (IOException ioe) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); + // Re-throw + throw ioe; + } + } + return len; + } + + @Override + public long getBytesWritten() { + return bytesWritten; + } + } +} diff --git a/java/org/apache/coyote/ajp/Constants.java b/java/org/apache/coyote/ajp/Constants.java new file mode 100644 index 0000000..f124d8e --- /dev/null +++ b/java/org/apache/coyote/ajp/Constants.java @@ -0,0 +1,192 @@ +/* + * 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.coyote.ajp; + +import java.util.HashMap; +import java.util.Map; + +/** + * Constants. + * + * @author Remy Maucherat + */ +public final class Constants { + + public static final int DEFAULT_CONNECTION_TIMEOUT = -1; + + // Prefix codes for message types from server to container + public static final byte JK_AJP13_FORWARD_REQUEST = 2; + public static final byte JK_AJP13_SHUTDOWN = 7; // XXX Unused + public static final byte JK_AJP13_PING_REQUEST = 8; // XXX Unused + public static final byte JK_AJP13_CPING_REQUEST = 10; + + // Prefix codes for message types from container to server + public static final byte JK_AJP13_SEND_BODY_CHUNK = 3; + public static final byte JK_AJP13_SEND_HEADERS = 4; + public static final byte JK_AJP13_END_RESPONSE = 5; + public static final byte JK_AJP13_GET_BODY_CHUNK = 6; + public static final byte JK_AJP13_CPONG_REPLY = 9; + + // Integer codes for common response header strings + public static final int SC_RESP_CONTENT_TYPE = 0xA001; + public static final int SC_RESP_CONTENT_LANGUAGE = 0xA002; + public static final int SC_RESP_CONTENT_LENGTH = 0xA003; + public static final int SC_RESP_DATE = 0xA004; + public static final int SC_RESP_LAST_MODIFIED = 0xA005; + public static final int SC_RESP_LOCATION = 0xA006; + public static final int SC_RESP_SET_COOKIE = 0xA007; + public static final int SC_RESP_SET_COOKIE2 = 0xA008; + public static final int SC_RESP_SERVLET_ENGINE = 0xA009; + public static final int SC_RESP_STATUS = 0xA00A; + public static final int SC_RESP_WWW_AUTHENTICATE = 0xA00B; + public static final int SC_RESP_AJP13_MAX = 11; + + // Integer codes for common (optional) request attribute names + public static final byte SC_A_CONTEXT = 1; // XXX Unused + public static final byte SC_A_SERVLET_PATH = 2; // XXX Unused + public static final byte SC_A_REMOTE_USER = 3; + public static final byte SC_A_AUTH_TYPE = 4; + public static final byte SC_A_QUERY_STRING = 5; + public static final byte SC_A_JVM_ROUTE = 6; + public static final byte SC_A_SSL_CERT = 7; + public static final byte SC_A_SSL_CIPHER = 8; + public static final byte SC_A_SSL_SESSION = 9; + public static final byte SC_A_SSL_KEY_SIZE = 11; + public static final byte SC_A_SECRET = 12; + public static final byte SC_A_STORED_METHOD = 13; + + // Used for attributes which are not in the list above + public static final byte SC_A_REQ_ATTRIBUTE = 10; + + /** + * AJP private request attributes + */ + public static final String SC_A_REQ_LOCAL_ADDR = "AJP_LOCAL_ADDR"; + public static final String SC_A_REQ_REMOTE_PORT = "AJP_REMOTE_PORT"; + public static final String SC_A_SSL_PROTOCOL = "AJP_SSL_PROTOCOL"; + + // Terminates list of attributes + public static final byte SC_A_ARE_DONE = (byte) 0xFF; + + /** + * Default maximum total byte size for an AJP packet + */ + public static final int MAX_PACKET_SIZE = 8192; + /** + * Size of basic packet header + */ + public static final int H_SIZE = 4; + + /** + * Size of the header metadata + */ + public static final int READ_HEAD_LEN = 6; + public static final int SEND_HEAD_LEN = 8; + + /** + * Default maximum size of data that can be sent in one packet + */ + public static final int MAX_READ_SIZE = MAX_PACKET_SIZE - READ_HEAD_LEN; + public static final int MAX_SEND_SIZE = MAX_PACKET_SIZE - SEND_HEAD_LEN; + + // Translates integer codes to names of HTTP methods + private static final String[] methodTransArray = { "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", + "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "ACL", "REPORT", "VERSION-CONTROL", + "CHECKIN", "CHECKOUT", "UNCHECKOUT", "SEARCH", "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", + "BASELINE-CONTROL", "MKACTIVITY" }; + + /** + * Converts an AJP coded HTTP method to the method name. + * + * @param code the coded value + * + * @return the string value of the method + */ + public static String getMethodForCode(final int code) { + return methodTransArray[code]; + } + + public static final int SC_M_JK_STORED = (byte) 0xFF; + + // id's for common request headers + public static final int SC_REQ_ACCEPT = 1; + public static final int SC_REQ_ACCEPT_CHARSET = 2; + public static final int SC_REQ_ACCEPT_ENCODING = 3; + public static final int SC_REQ_ACCEPT_LANGUAGE = 4; + public static final int SC_REQ_AUTHORIZATION = 5; + public static final int SC_REQ_CONNECTION = 6; + public static final int SC_REQ_CONTENT_TYPE = 7; + public static final int SC_REQ_CONTENT_LENGTH = 8; + public static final int SC_REQ_COOKIE = 9; + public static final int SC_REQ_COOKIE2 = 10; + public static final int SC_REQ_HOST = 11; + public static final int SC_REQ_PRAGMA = 12; + public static final int SC_REQ_REFERER = 13; + public static final int SC_REQ_USER_AGENT = 14; + + // Translates integer codes to request header names + private static final String[] headerTransArray = { "accept", "accept-charset", "accept-encoding", "accept-language", + "authorization", "connection", "content-type", "content-length", "cookie", "cookie2", "host", "pragma", + "referer", "user-agent" }; + + /** + * Converts an AJP coded HTTP request header to the header name. + * + * @param code the coded value + * + * @return the string value of the header name + */ + public static String getHeaderForCode(final int code) { + return headerTransArray[code]; + } + + // Translates integer codes to response header names + private static final String[] responseTransArray = { "Content-Type", "Content-Language", "Content-Length", "Date", + "Last-Modified", "Location", "Set-Cookie", "Set-Cookie2", "Servlet-Engine", "Status", "WWW-Authenticate" }; + + /** + * Converts an AJP coded response header name to the HTTP response header name. + * + * @param code the coded value + * + * @return the string value of the header + */ + public static String getResponseHeaderForCode(final int code) { + return responseTransArray[code]; + } + + private static final Map responseTransMap = new HashMap<>(20); + + static { + try { + for (int i = 0; i < SC_RESP_AJP13_MAX; i++) { + responseTransMap.put(getResponseHeaderForCode(i), Integer.valueOf(0xA001 + i)); + } + } catch (Exception e) { + // Do nothing + } + } + + public static int getResponseAjpIndex(String header) { + Integer i = responseTransMap.get(header); + if (i == null) { + return 0; + } else { + return i.intValue(); + } + } +} diff --git a/java/org/apache/coyote/ajp/LocalStrings.properties b/java/org/apache/coyote/ajp/LocalStrings.properties new file mode 100644 index 0000000..be8a46c --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings.properties @@ -0,0 +1,37 @@ +# 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. + +ajpMessage.invalidPos=Requested read of bytes at position [{0}] which is beyond the end of the AJP message + +ajpmessage.invalid=Invalid message received with signature [{0}] +ajpmessage.invalidLength=Invalid message received with length [{0}] +ajpmessage.null=Cannot append null value +ajpmessage.overflow=Overflow error for buffer adding [{0}] bytes at position [{1}] + +ajpprocessor.certs.fail=Certificate conversion failed +ajpprocessor.header.error=Header message parsing failed +ajpprocessor.header.tooLong=Header message of length [{0}] received but the packetSize is only [{1}] +ajpprocessor.pongFail=Pong message failed +ajpprocessor.readtimeout=Timeout attempting to read data from the socket +ajpprocessor.request.prepare=Error preparing request +ajpprocessor.request.process=Error processing request +ajpprocessor.response.invalidHeader=The HTTP response header [{0}] with value [{1}] has been removed from the response because it is invalid +ajpprocessor.unexpectedMessage=Unexpected message type [{0}] +ajpprocessor.unknownAttribute=Rejecting request due to unknown request attribute [{0}] received from reverse proxy + +ajpprotocol.noSSL=SSL is not supported with AJP. The SSL host configuration for [{0}] was ignored +ajpprotocol.noSecret=The AJP Connector is configured with secretRequired="true" but the secret attribute is either null or "". This combination is not valid. +ajpprotocol.noUpgrade=Upgrade is not supported with AJP. The UpgradeProtocol configuration for [{0}] was ignored +ajpprotocol.noUpgradeHandler=Upgrade is not supported with AJP. The HttpUpgradeHandler [{0}] can not be processed diff --git a/java/org/apache/coyote/ajp/LocalStrings_cs.properties b/java/org/apache/coyote/ajp/LocalStrings_cs.properties new file mode 100644 index 0000000..42a7350 --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_cs.properties @@ -0,0 +1,18 @@ +# 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. + +ajpmessage.overflow=Chyba pÅ™eteÄení pro buffer pÅ™idávající [{0}] bytů na pozici [{1}] + +ajpprotocol.noUpgrade=Upgrade není s AJP podporován. Konfigurace UpgradeProtocol pro [{0}] byla ignorována diff --git a/java/org/apache/coyote/ajp/LocalStrings_de.properties b/java/org/apache/coyote/ajp/LocalStrings_de.properties new file mode 100644 index 0000000..eb01020 --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_de.properties @@ -0,0 +1,20 @@ +# 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. + +ajpmessage.overflow=Ãœberlauffehler im Puffer beim Hinzufügen von [{0}] Bytes an Position [{1}] + +ajpprocessor.header.error=Fehler beim Parsen der Header-Nachricht + +ajpprotocol.noUpgrade=Upgrade wird von AJP nicht unterstützt. Die Konfiguration UpgradeProtocol für [{0}] wurde ignoriert diff --git a/java/org/apache/coyote/ajp/LocalStrings_es.properties b/java/org/apache/coyote/ajp/LocalStrings_es.properties new file mode 100644 index 0000000..947ebc2 --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_es.properties @@ -0,0 +1,25 @@ +# 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. + +ajpmessage.invalid=Mensaje inválido recibido con firma [{0}] +ajpmessage.null=No puedo añadir valor nulo +ajpmessage.overflow=Error de desbordamiento en búfer al añadir [{0}] bytes en posición [{1}] + +ajpprocessor.certs.fail=Fallo en conversión de Certificado +ajpprocessor.header.error=Fallo en análisis de mensaje de cabecera +ajpprocessor.request.prepare=Error preparando requerimiento +ajpprocessor.request.process=Error procesando requerimiento + +ajpprotocol.noUpgrade=Actualización (upgrade) no esta soportada para AJP. Se ha ignorado la configuración UpgradeProtocol para [{0}] diff --git a/java/org/apache/coyote/ajp/LocalStrings_fr.properties b/java/org/apache/coyote/ajp/LocalStrings_fr.properties new file mode 100644 index 0000000..151428c --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_fr.properties @@ -0,0 +1,37 @@ +# 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. + +ajpMessage.invalidPos=Une lecture d''octets à la position [{0}] a été demandée ce qui est au-delà de la fin du message AJP + +ajpmessage.invalid=Message invalide reçu avec la signature [{0}] +ajpmessage.invalidLength=Message invalide reçu avec une longueur [{0}] +ajpmessage.null=Impossible d'ajouter une valeur nulle. +ajpmessage.overflow=Débordement du tampon en ajoutant [{0}] octets à la position [{1}] + +ajpprocessor.certs.fail=La conversion du certificat a échouée +ajpprocessor.header.error=Erreur de traitement du message d'en-tête +ajpprocessor.header.tooLong=Un en-tête de message de taille [{0}] a été reçu mais la packtSize est de seulement [{1}] +ajpprocessor.pongFail=Le message de réponse au ping a échoué +ajpprocessor.readtimeout=Timeout lors de la lecture de données sur le socket +ajpprocessor.request.prepare=Erreur lors de la préparation de la requête +ajpprocessor.request.process=Erreur de traitement de la requête +ajpprocessor.response.invalidHeader=L''en-tête de réponse HTTP [{0}] avec la valeur [{1}] a été retiré de la réponse car il est invalide +ajpprocessor.unexpectedMessage=Message de type inattendu [{0}] +ajpprocessor.unknownAttribute=La requête est rejetée à cause de l''attribut de requête [{0}] inconnu reçu du reverse proxy + +ajpprotocol.noSSL=SSL n''est pas supporté par AJP, la configuration de l''hôte SSL pour [{0}] a été ignorée +ajpprotocol.noSecret=Le connecteur AJP est configuré avec secretRequired="true" mais l'attribut secret est soit null soit "", cette combinaison n'est pas valide +ajpprotocol.noUpgrade=L''upgrade n''est pas supporté par AJP. La configuration UpgradeProtocol pour [{0}] a été ignorée +ajpprotocol.noUpgradeHandler=AJP ne supporte pas la mise à niveau (upgrade) de HTTP/1.1, le HttpUpgradeHandler [{0}] ne peut pas être utilisé diff --git a/java/org/apache/coyote/ajp/LocalStrings_ja.properties b/java/org/apache/coyote/ajp/LocalStrings_ja.properties new file mode 100644 index 0000000..fca931c --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_ja.properties @@ -0,0 +1,37 @@ +# 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. + +ajpMessage.invalidPos=AJP メッセージã®çµ‚端より先ã®ä½ç½® [{0}] ã®ãƒã‚¤ãƒˆèª­ã¿å–ã‚Šã‚’è¦æ±‚ã•ã‚Œã¾ã—ãŸã€‚ + +ajpmessage.invalid=ç½²å[{0}]ã§ç„¡åŠ¹ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãŒå—ä¿¡ã•ã‚Œã¾ã—ãŸã€‚ +ajpmessage.invalidLength=é•·ã•[{0}]ã®ç„¡åŠ¹ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãŒå—ä¿¡ã•ã‚Œã¾ã—㟠+ajpmessage.null=null 値ã¯è¿½åŠ ã§ãã¾ã›ã‚“。 +ajpmessage.overflow=ãƒãƒƒãƒ•ã‚¡ãƒ¼ã®ä½ç½® [{1}] 㸠[{0}] ãƒã‚¤ãƒˆã®ãƒ‡ãƒ¼ã‚¿ã‚’追加ã—よã†ã¨ã—ã¦ã€ã‚ªãƒ¼ãƒãƒ¼ãƒ•ãƒ­ãƒ¼ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ + +ajpprocessor.certs.fail=証明書変æ›ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +ajpprocessor.header.error=ヘッダーメッセージã®è§£æžã«å¤±æ•—ã—ã¾ã—㟠+ajpprocessor.header.tooLong=å—ä¿¡ã—ãŸãƒ˜ãƒƒãƒ€ãƒ¼ã«æŒ‡å®šã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é•·ã¯ [{0}] ã§ã™ãŒpacketSize 㯠[{1}] ã—ã‹ã‚ã‚Šã¾ã›ã‚“。 +ajpprocessor.pongFail=Pong メッセージãŒå¤±æ•—ã—ã¾ã—㟠+ajpprocessor.readtimeout=ソケットã‹ã‚‰ã®ãƒ‡ãƒ¼ã‚¿ã®èª­ã¿å–りをタイムアウトã«ã—ã¾ã™ã€‚ +ajpprocessor.request.prepare=リクエスト準備中エラー +ajpprocessor.request.process=リクエスト処ç†ã‚¨ãƒ©ãƒ¼ +ajpprocessor.response.invalidHeader=値ã¨ã—㦠[{1}] ã‚’æŒã¤HTTPレスãƒãƒ³ã‚¹ãƒ˜ãƒƒãƒ€ [{0}] ã¯ç„¡åŠ¹ã§ã‚ã‚‹ãŸã‚ã€å¿œç­”ã‹ã‚‰å‰Šé™¤ã•ã‚Œã¾ã—㟠+ajpprocessor.unexpectedMessage=予期ã—ãªã„メッセージ タイプ [{0}] +ajpprocessor.unknownAttribute=リãƒãƒ¼ã‚¹ãƒ—ロキシã‹ã‚‰å—ä¿¡ã—ãŸä¸æ˜Žãªãƒªã‚¯ã‚¨ã‚¹ãƒˆå±žæ€§[{0}]ã«ã‚ˆã‚‹ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®æ‹’å¦ + +ajpprotocol.noSSL=AJP 㯠SSL ã«å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“。SSL ãƒ›ã‚¹ãƒˆæ§‹æˆ [{0}] を無視ã—ã¾ã™ã€‚ +ajpprotocol.noSecret=AJP コãƒã‚¯ã‚¿ã¯ secretRequired="true" ã¨ã—ã¦æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ãŒã€secret 属性㯠null ã¾ãŸã¯ç©ºæ–‡å­—列ãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ã“ã®çµ„ã¿åˆã‚ã›ã¯æœ‰åŠ¹ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +ajpprotocol.noUpgrade=AJP ã¯ãƒ—ロトコルアップグレードã«å¯¾å¿œã—ã¦ã„ãªã„ãŸã‚ã€[{0}] ã®è¨­å®šã‚’無視ã—ã¾ã—㟠+ajpprotocol.noUpgradeHandler=アップグレードã¯AJPã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。HttpUpgradeHandler [{0}]を処ç†ã§ãã¾ã›ã‚“。 diff --git a/java/org/apache/coyote/ajp/LocalStrings_ko.properties b/java/org/apache/coyote/ajp/LocalStrings_ko.properties new file mode 100644 index 0000000..4cfc255 --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_ko.properties @@ -0,0 +1,34 @@ +# 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. + +ajpMessage.invalidPos=위치 [{0}](으)로부터 ë°”ì´íŠ¸ë“¤ì„ ì½ê¸°ë¥¼ 요청 받았는ë°, ì´ëŠ” 해당 AJP ë©”ì‹œì§€ì˜ ë 위치를 넘어섰습니다. + +ajpmessage.invalid=서명 [{0}]와(ê³¼) 함께, 유효하지 ì•Šì€ ë©”ì‹œì§€ë¥¼ 받았습니다. +ajpmessage.invalidLength=길ì´ê°€ [{0}]ì¸ ìœ íš¨í•˜ì§€ ì•Šì€ ë©”ì‹œì§€ë¥¼ 받았습니다. +ajpmessage.null=ë„ ê°’ì„ ì¶”ê°€í•  수 없습니다. +ajpmessage.overflow=버í¼ì— [{0}] ë°”ì´íŠ¸ë“¤ì„ 위치 [{1}]ì— ì¶”ê°€í•˜ëŠ” ë™ì•ˆ 오버플로우 오류 ë°œìƒ + +ajpprocessor.certs.fail=ì¸ì¦ì„œë¥¼ 변환시키지 못했습니다. +ajpprocessor.header.error=í—¤ë” ë©”ì‹œì§€ë¥¼ 파싱하지 못했습니다. +ajpprocessor.header.tooLong=길ì´ê°€ [{0}]ì¸ í—¤ë” ë©”ì‹œì§€ë¥¼ 받았지만, 패킷 í¬ê¸°ëŠ” 단지 [{1}]입니다. +ajpprocessor.readtimeout=소켓으로부터 ë°ì´í„°ë¥¼ ì½ìœ¼ë ¤ëŠ” ì‹œë„ê°€ 제한 시간 초과ë˜ì—ˆìŠµë‹ˆë‹¤. +ajpprocessor.request.prepare=ìš”ì²­ì„ ì¤€ë¹„í•˜ëŠ” 중 오류 ë°œìƒ +ajpprocessor.request.process=요청 처리 중 오류 ë°œìƒ +ajpprocessor.unknownAttribute=리버스 프ë¡ì‹œ 서버로부터 ì•Œ 수 없는 요청 ì†ì„± [{0}]ì´(ê°€) 접수ë˜ì–´ 요청 처리를 거부합니다. + +ajpprotocol.noSSL=AJP와 함께 SSLì€ ì§€ì›ë˜ì§€ 않습니다. [{0}]ì„(를) 위한 SSL 호스트 ì„¤ì •ì€ ë¬´ì‹œë˜ì—ˆìŠµë‹ˆë‹¤. +ajpprotocol.noSecret=AJP ì—°ê²°ìžëŠ” secretRequired="true"ë¡œ 구성ë˜ì—ˆìœ¼ë‚˜ 보안 ì†ì„±ì´ ë„ ë˜ëŠ” ""입니다. ì´ ì¡°í•©ì€ ìœ íš¨í•˜ì§€ 않습니다. +ajpprotocol.noUpgrade=AJPì—ì„œ 프로토콜 업그레ì´ë“œëŠ” 지ì›ë˜ì§€ 않습니다. [{0}]ì„(를) 위한 UpgradeProtocol ì„¤ì •ì€ ë¬´ì‹œë©ë‹ˆë‹¤. +ajpprotocol.noUpgradeHandler=AJP를 사용할 ë•Œ, 업그레ì´ë“œëŠ” 지ì›ë˜ì§€ 않습니다. HttpUpgradeHandler [{0}]ì€(는) ì²˜ë¦¬ë  ìˆ˜ 없습니다. diff --git a/java/org/apache/coyote/ajp/LocalStrings_pt_BR.properties b/java/org/apache/coyote/ajp/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..b48197e --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +ajpprotocol.noUpgrade=Atualização não é suportada com AJP. A configuração UpgradeProtocol para [{0}] foi ignorada diff --git a/java/org/apache/coyote/ajp/LocalStrings_ru.properties b/java/org/apache/coyote/ajp/LocalStrings_ru.properties new file mode 100644 index 0000000..0ddda12 --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +ajpmessage.overflow=Ошибка Ð¿ÐµÑ€ÐµÐ¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð±ÑƒÑ„ÐµÑ€Ð° при добавлении [{0}] байтов в позиции [{1}] diff --git a/java/org/apache/coyote/ajp/LocalStrings_zh_CN.properties b/java/org/apache/coyote/ajp/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..ff95917 --- /dev/null +++ b/java/org/apache/coyote/ajp/LocalStrings_zh_CN.properties @@ -0,0 +1,35 @@ +# 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. + +ajpMessage.invalidPos=请求读å–的字节ä½äºŽä½ç½®[{0}],该ä½ç½®è¶…出了AJP消æ¯çš„结尾 + +ajpmessage.invalid=收到无效的带有签å[{0}]çš„æ¶ˆæ¯ +ajpmessage.invalidLength=接收到长度为[{0}]的无效消æ¯ã€‚ +ajpmessage.null=ä¸èƒ½èµ‹ç©ºå€¼ +ajpmessage.overflow=在缓冲区[{1}]ä½ç½®æ·»åŠ [{0}]字节时å‘生溢出错误 + +ajpprocessor.certs.fail=):è¯ä¹¦è½¬æ¢å¤±è´¥ +ajpprocessor.header.error=头部信æ¯è§£æžå¤±è´¥ +ajpprocessor.header.tooLong=已收到长度为[{0}]的头消æ¯ï¼Œä½†packetSize仅为[{1}] +ajpprocessor.readtimeout=从Socket读å–æ•°æ®è¶…æ—¶ +ajpprocessor.request.prepare=准备请求错误 +ajpprocessor.request.process=处ç†è¯·æ±‚错误 +ajpprocessor.response.invalidHeader=å“应头:[{0}] 值:[{1}]无效,已从å“应中移除 +ajpprocessor.unknownAttribute=由于请求属性[{0}]接收自åå‘代ç†ï¼Œè¯·æ±‚è¢«æ‹’ç» + +ajpprotocol.noSSL=AJPä¸æ”¯æŒSSL。[{0}]çš„SSL主机é…置被忽略 +ajpprotocol.noSecret=AJP连接器é…ç½®secretRequired="true",但是属性secret确实空或者空字符串,这样的组åˆæ˜¯æ— æ•ˆçš„。 +ajpprotocol.noUpgrade=AJP ä¸æ”¯æŒå‡çº§ã€‚[{0}] çš„å‡çº§åè®®é…置被忽略。 +ajpprotocol.noUpgradeHandler=AJPä¸æ”¯æŒå‡çº§ã€‚ HttpUpgradeHandler [{0}]æ— æ³•å¤„ç† diff --git a/java/org/apache/coyote/http11/AbstractHttp11JsseProtocol.java b/java/org/apache/coyote/http11/AbstractHttp11JsseProtocol.java new file mode 100644 index 0000000..2ea6597 --- /dev/null +++ b/java/org/apache/coyote/http11/AbstractHttp11JsseProtocol.java @@ -0,0 +1,63 @@ +/* + * 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.coyote.http11; + +import org.apache.tomcat.util.net.AbstractJsseEndpoint; +import org.apache.tomcat.util.net.openssl.OpenSSLImplementation; + +public abstract class AbstractHttp11JsseProtocol extends AbstractHttp11Protocol { + + public AbstractHttp11JsseProtocol(AbstractJsseEndpoint endpoint) { + super(endpoint); + } + + + @Override + protected AbstractJsseEndpoint getEndpoint() { + // Over-ridden to add cast + return (AbstractJsseEndpoint) super.getEndpoint(); + } + + + protected String getSslImplementationShortName() { + if (OpenSSLImplementation.class.getName().equals(getSslImplementationName())) { + return "openssl"; + } + if (getSslImplementationName() != null && + getSslImplementationName().endsWith(".panama.OpenSSLImplementation")) { + return "opensslffm"; + } + return "jsse"; + } + + public String getSslImplementationName() { + return getEndpoint().getSslImplementationName(); + } + + public void setSslImplementationName(String s) { + getEndpoint().setSslImplementationName(s); + } + + + public int getSniParseLimit() { + return getEndpoint().getSniParseLimit(); + } + + public void setSniParseLimit(int sniParseLimit) { + getEndpoint().setSniParseLimit(sniParseLimit); + } +} diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java new file mode 100644 index 0000000..e6fa586 --- /dev/null +++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java @@ -0,0 +1,802 @@ +/* + * 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.coyote.http11; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpUpgradeHandler; + +import org.apache.coyote.AbstractProtocol; +import org.apache.coyote.CompressionConfig; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.coyote.Processor; +import org.apache.coyote.Request; +import org.apache.coyote.Response; +import org.apache.coyote.UpgradeProtocol; +import org.apache.coyote.UpgradeToken; +import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; +import org.apache.coyote.http11.upgrade.UpgradeGroupInfo; +import org.apache.coyote.http11.upgrade.UpgradeProcessorExternal; +import org.apache.coyote.http11.upgrade.UpgradeProcessorInternal; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.modeler.Util; +import org.apache.tomcat.util.net.AbstractEndpoint; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +public abstract class AbstractHttp11Protocol extends AbstractProtocol { + + protected static final StringManager sm = StringManager.getManager(AbstractHttp11Protocol.class); + + private final CompressionConfig compressionConfig = new CompressionConfig(); + + + public AbstractHttp11Protocol(AbstractEndpoint endpoint) { + super(endpoint); + setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT); + } + + + @Override + public void init() throws Exception { + // Upgrade protocols have to be configured first since the endpoint + // init (triggered via super.init() below) uses this list to configure + // the list of ALPN protocols to advertise + for (UpgradeProtocol upgradeProtocol : upgradeProtocols) { + configureUpgradeProtocol(upgradeProtocol); + } + + super.init(); + + // Set the Http11Protocol (i.e. this) for any upgrade protocols once + // this has completed initialisation as the upgrade protocols may expect this + // to be initialised when the call is made + for (UpgradeProtocol upgradeProtocol : upgradeProtocols) { + upgradeProtocol.setHttp11Protocol(this); + } + } + + + @Override + public void destroy() throws Exception { + // There may be upgrade protocols with their own MBeans. These need to + // be de-registered. + ObjectName rgOname = getGlobalRequestProcessorMBeanName(); + if (rgOname != null) { + Registry registry = Registry.getRegistry(null, null); + ObjectName query = new ObjectName(rgOname.getCanonicalName() + ",Upgrade=*"); + Set upgrades = registry.getMBeanServer().queryMBeans(query, null); + for (ObjectInstance upgrade : upgrades) { + registry.unregisterComponent(upgrade.getObjectName()); + } + } + + super.destroy(); + } + + + @Override + protected String getProtocolName() { + return "Http"; + } + + + /** + * {@inheritDoc} + *

    + * Over-ridden here to make the method visible to nested classes. + */ + @Override + protected AbstractEndpoint getEndpoint() { + return super.getEndpoint(); + } + + + // ------------------------------------------------ HTTP specific properties + // ------------------------------------------ managed in the ProtocolHandler + + private ContinueResponseTiming continueResponseTiming = ContinueResponseTiming.IMMEDIATELY; + + public String getContinueResponseTiming() { + return continueResponseTiming.toString(); + } + + public void setContinueResponseTiming(String continueResponseTiming) { + this.continueResponseTiming = ContinueResponseTiming.fromString(continueResponseTiming); + } + + public ContinueResponseTiming getContinueResponseTimingInternal() { + return continueResponseTiming; + } + + + private boolean useKeepAliveResponseHeader = true; + + public boolean getUseKeepAliveResponseHeader() { + return useKeepAliveResponseHeader; + } + + public void setUseKeepAliveResponseHeader(boolean useKeepAliveResponseHeader) { + this.useKeepAliveResponseHeader = useKeepAliveResponseHeader; + } + + + private String relaxedPathChars = null; + + public String getRelaxedPathChars() { + return relaxedPathChars; + } + + public void setRelaxedPathChars(String relaxedPathChars) { + this.relaxedPathChars = relaxedPathChars; + } + + + private String relaxedQueryChars = null; + + public String getRelaxedQueryChars() { + return relaxedQueryChars; + } + + public void setRelaxedQueryChars(String relaxedQueryChars) { + this.relaxedQueryChars = relaxedQueryChars; + } + + + private boolean allowHostHeaderMismatch = false; + + /** + * Will Tomcat accept an HTTP 1.1 request where the host header does not agree with the host specified (if any) in + * the request line? + * + * @return {@code true} if Tomcat will allow such requests, otherwise {@code false} + * + * @deprecated This will removed in Tomcat 11 onwards where {@code allowHostHeaderMismatch} will be hard-coded to + * {@code false}. + */ + @Deprecated + public boolean getAllowHostHeaderMismatch() { + return allowHostHeaderMismatch; + } + + /** + * Will Tomcat accept an HTTP 1.1 request where the host header does not agree with the host specified (if any) in + * the request line? + * + * @param allowHostHeaderMismatch {@code true} to allow such requests, {@code false} to reject them with a 400 + * + * @deprecated This will removed in Tomcat 11 onwards where {@code allowHostHeaderMismatch} will be hard-coded to + * {@code false}. + */ + @Deprecated + public void setAllowHostHeaderMismatch(boolean allowHostHeaderMismatch) { + this.allowHostHeaderMismatch = allowHostHeaderMismatch; + } + + + private boolean rejectIllegalHeader = true; + + /** + * If an HTTP request is received that contains an illegal header name or value (e.g. the header name is not a + * token) will the request be rejected (with a 400 response) or will the illegal header be ignored? + * + * @return {@code true} if the request will be rejected or {@code false} if the header will be ignored + * + * @deprecated This will removed in Tomcat 11 onwards where {@code allowHostHeaderMismatch} will be hard-coded to + * {@code true}. + */ + @Deprecated + public boolean getRejectIllegalHeader() { + return rejectIllegalHeader; + } + + /** + * If an HTTP request is received that contains an illegal header name or value (e.g. the header name is not a + * token) should the request be rejected (with a 400 response) or should the illegal header be ignored? + * + * @param rejectIllegalHeader {@code true} to reject requests with illegal header names or values, {@code false} to + * ignore the header + * + * @deprecated This will removed in Tomcat 11 onwards where {@code allowHostHeaderMismatch} will be hard-coded to + * {@code true}. + */ + @Deprecated + public void setRejectIllegalHeader(boolean rejectIllegalHeader) { + this.rejectIllegalHeader = rejectIllegalHeader; + } + + + private int maxSavePostSize = 4 * 1024; + + /** + * Return the maximum size of the post which will be saved during FORM or CLIENT-CERT authentication. + * + * @return The size in bytes + */ + public int getMaxSavePostSize() { + return maxSavePostSize; + } + + /** + * Set the maximum size of a POST which will be buffered during FORM or CLIENT-CERT authentication. When a POST is + * received where the security constraints require a client certificate, the POST body needs to be buffered while an + * SSL handshake takes place to obtain the certificate. A similar buffering is required during FORM auth. + * + * @param maxSavePostSize The maximum size POST body to buffer in bytes + */ + public void setMaxSavePostSize(int maxSavePostSize) { + this.maxSavePostSize = maxSavePostSize; + } + + + /** + * Maximum size of the HTTP message header. + */ + private int maxHttpHeaderSize = 8 * 1024; + + public int getMaxHttpHeaderSize() { + return maxHttpHeaderSize; + } + + public void setMaxHttpHeaderSize(int valueI) { + maxHttpHeaderSize = valueI; + } + + + /** + * Maximum size of the HTTP request message header. + */ + private int maxHttpRequestHeaderSize = -1; + + public int getMaxHttpRequestHeaderSize() { + return maxHttpRequestHeaderSize == -1 ? getMaxHttpHeaderSize() : maxHttpRequestHeaderSize; + } + + public void setMaxHttpRequestHeaderSize(int valueI) { + maxHttpRequestHeaderSize = valueI; + } + + + /** + * Maximum size of the HTTP response message header. + */ + private int maxHttpResponseHeaderSize = -1; + + public int getMaxHttpResponseHeaderSize() { + return maxHttpResponseHeaderSize == -1 ? getMaxHttpHeaderSize() : maxHttpResponseHeaderSize; + } + + public void setMaxHttpResponseHeaderSize(int valueI) { + maxHttpResponseHeaderSize = valueI; + } + + + private int connectionUploadTimeout = 300000; + + /** + * Specifies a different (usually longer) connection timeout during data upload. Default is 5 minutes as in Apache + * HTTPD server. + * + * @return The timeout in milliseconds + */ + public int getConnectionUploadTimeout() { + return connectionUploadTimeout; + } + + /** + * Set the upload timeout. + * + * @param timeout Upload timeout in milliseconds + */ + public void setConnectionUploadTimeout(int timeout) { + connectionUploadTimeout = timeout; + } + + + private boolean disableUploadTimeout = true; + + /** + * Get the flag that controls upload time-outs. If true, the connectionUploadTimeout will be ignored and the regular + * socket timeout will be used for the full duration of the connection. + * + * @return {@code true} if the separate upload timeout is disabled + */ + public boolean getDisableUploadTimeout() { + return disableUploadTimeout; + } + + /** + * Set the flag to control whether a separate connection timeout is used during upload of a request body. + * + * @param isDisabled {@code true} if the separate upload timeout should be disabled + */ + public void setDisableUploadTimeout(boolean isDisabled) { + disableUploadTimeout = isDisabled; + } + + + public void setCompression(String compression) { + compressionConfig.setCompression(compression); + } + + public String getCompression() { + return compressionConfig.getCompression(); + } + + protected int getCompressionLevel() { + return compressionConfig.getCompressionLevel(); + } + + + public String getNoCompressionUserAgents() { + return compressionConfig.getNoCompressionUserAgents(); + } + + protected Pattern getNoCompressionUserAgentsPattern() { + return compressionConfig.getNoCompressionUserAgentsPattern(); + } + + public void setNoCompressionUserAgents(String noCompressionUserAgents) { + compressionConfig.setNoCompressionUserAgents(noCompressionUserAgents); + } + + + public String getCompressibleMimeType() { + return compressionConfig.getCompressibleMimeType(); + } + + public void setCompressibleMimeType(String valueS) { + compressionConfig.setCompressibleMimeType(valueS); + } + + public String[] getCompressibleMimeTypes() { + return compressionConfig.getCompressibleMimeTypes(); + } + + + public int getCompressionMinSize() { + return compressionConfig.getCompressionMinSize(); + } + + public void setCompressionMinSize(int compressionMinSize) { + compressionConfig.setCompressionMinSize(compressionMinSize); + } + + + public boolean useCompression(Request request, Response response) { + return compressionConfig.useCompression(request, response); + } + + + private Pattern restrictedUserAgents = null; + + /** + * Get the string form of the regular expression that defines the User agents which should be restricted to HTTP/1.0 + * support. + * + * @return The regular expression as a String + */ + public String getRestrictedUserAgents() { + if (restrictedUserAgents == null) { + return null; + } else { + return restrictedUserAgents.toString(); + } + } + + protected Pattern getRestrictedUserAgentsPattern() { + return restrictedUserAgents; + } + + /** + * Set restricted user agent list (which will downgrade the connector to HTTP/1.0 mode). Regular expression as + * supported by {@link Pattern}. + * + * @param restrictedUserAgents The regular expression as supported by {@link Pattern} for the user agents e.g. + * "gorilla|desesplorer|tigrus" + */ + public void setRestrictedUserAgents(String restrictedUserAgents) { + if (restrictedUserAgents == null || restrictedUserAgents.length() == 0) { + this.restrictedUserAgents = null; + } else { + this.restrictedUserAgents = Pattern.compile(restrictedUserAgents); + } + } + + + private String server; + + public String getServer() { + return server; + } + + /** + * Set the server header name. + * + * @param server The new value to use for the server header + */ + public void setServer(String server) { + this.server = server; + } + + + private boolean serverRemoveAppProvidedValues = false; + + /** + * Should application provider values for the HTTP Server header be removed. Note that if {@link #server} is set, + * any application provided value will be over-ridden. + * + * @return {@code true} if application provided values should be removed, otherwise {@code false} + */ + public boolean getServerRemoveAppProvidedValues() { + return serverRemoveAppProvidedValues; + } + + public void setServerRemoveAppProvidedValues(boolean serverRemoveAppProvidedValues) { + this.serverRemoveAppProvidedValues = serverRemoveAppProvidedValues; + } + + + /** + * Maximum size of trailing headers in bytes + */ + private int maxTrailerSize = 8192; + + public int getMaxTrailerSize() { + return maxTrailerSize; + } + + public void setMaxTrailerSize(int maxTrailerSize) { + this.maxTrailerSize = maxTrailerSize; + } + + + /** + * Maximum size of extension information in chunked encoding + */ + private int maxExtensionSize = 8192; + + public int getMaxExtensionSize() { + return maxExtensionSize; + } + + public void setMaxExtensionSize(int maxExtensionSize) { + this.maxExtensionSize = maxExtensionSize; + } + + + /** + * Maximum amount of request body to swallow. + */ + private int maxSwallowSize = 2 * 1024 * 1024; + + public int getMaxSwallowSize() { + return maxSwallowSize; + } + + public void setMaxSwallowSize(int maxSwallowSize) { + this.maxSwallowSize = maxSwallowSize; + } + + + /** + * This field indicates if the protocol is treated as if it is secure. This normally means https is being used but + * can be used to fake https e.g behind a reverse proxy. + */ + private boolean secure; + + public boolean getSecure() { + return secure; + } + + public void setSecure(boolean b) { + secure = b; + } + + + /** + * The names of headers that are allowed to be sent via a trailer when using chunked encoding. They are stored in + * lower case. + */ + private Set allowedTrailerHeaders = ConcurrentHashMap.newKeySet(); + + public void setAllowedTrailerHeaders(String commaSeparatedHeaders) { + // Jump through some hoops so we don't end up with an empty set while + // doing updates. + Set toRemove = new HashSet<>(allowedTrailerHeaders); + if (commaSeparatedHeaders != null) { + String[] headers = commaSeparatedHeaders.split(","); + for (String header : headers) { + String trimmedHeader = header.trim().toLowerCase(Locale.ENGLISH); + if (toRemove.contains(trimmedHeader)) { + toRemove.remove(trimmedHeader); + } else { + allowedTrailerHeaders.add(trimmedHeader); + } + } + allowedTrailerHeaders.removeAll(toRemove); + } + } + + protected Set getAllowedTrailerHeadersInternal() { + return allowedTrailerHeaders; + } + + public boolean isTrailerHeaderAllowed(String headerName) { + return allowedTrailerHeaders.contains(headerName); + } + + public String getAllowedTrailerHeaders() { + // Chances of a change during execution of this line are small enough + // that a sync is unnecessary. + List copy = new ArrayList<>(allowedTrailerHeaders); + return StringUtils.join(copy); + } + + public void addAllowedTrailerHeader(String header) { + if (header != null) { + allowedTrailerHeaders.add(header.trim().toLowerCase(Locale.ENGLISH)); + } + } + + public void removeAllowedTrailerHeader(String header) { + if (header != null) { + allowedTrailerHeaders.remove(header.trim().toLowerCase(Locale.ENGLISH)); + } + } + + + /** + * The upgrade protocol instances configured. + */ + private final List upgradeProtocols = new ArrayList<>(); + + @Override + public void addUpgradeProtocol(UpgradeProtocol upgradeProtocol) { + upgradeProtocols.add(upgradeProtocol); + } + + @Override + public UpgradeProtocol[] findUpgradeProtocols() { + return upgradeProtocols.toArray(new UpgradeProtocol[0]); + } + + + /** + * The protocols that are available via internal Tomcat support for access via HTTP upgrade. + */ + private final Map httpUpgradeProtocols = new HashMap<>(); + /** + * The protocols that are available via internal Tomcat support for access via ALPN negotiation. + */ + private final Map negotiatedProtocols = new HashMap<>(); + + private void configureUpgradeProtocol(UpgradeProtocol upgradeProtocol) { + // HTTP Upgrade + String httpUpgradeName = upgradeProtocol.getHttpUpgradeName(getEndpoint().isSSLEnabled()); + boolean httpUpgradeConfigured = false; + if (httpUpgradeName != null && httpUpgradeName.length() > 0) { + httpUpgradeProtocols.put(httpUpgradeName, upgradeProtocol); + httpUpgradeConfigured = true; + getLog().info(sm.getString("abstractHttp11Protocol.httpUpgradeConfigured", getName(), httpUpgradeName)); + } + + + // ALPN + String alpnName = upgradeProtocol.getAlpnName(); + if (alpnName != null && alpnName.length() > 0) { + // ALPN is only available with TLS + if (getEndpoint().isSSLEnabled()) { + negotiatedProtocols.put(alpnName, upgradeProtocol); + getEndpoint().addNegotiatedProtocol(alpnName); + getLog().info(sm.getString("abstractHttp11Protocol.alpnConfigured", getName(), alpnName)); + } else { + if (!httpUpgradeConfigured) { + // ALPN is not supported by this connector and the upgrade + // protocol implementation does not support standard HTTP + // upgrade so there is no way available to enable support + // for this protocol. + getLog().error(sm.getString("abstractHttp11Protocol.alpnWithNoAlpn", + upgradeProtocol.getClass().getName(), alpnName, getName())); + } + } + } + } + + @Override + public UpgradeProtocol getNegotiatedProtocol(String negotiatedName) { + return negotiatedProtocols.get(negotiatedName); + } + + @Override + public UpgradeProtocol getUpgradeProtocol(String upgradedName) { + return httpUpgradeProtocols.get(upgradedName); + } + + + /** + * Map of upgrade protocol name to {@link UpgradeGroupInfo} instance. + *

    + * HTTP upgrades via {@link HttpServletRequest#upgrade(Class)} do not have to depend on an {@code UpgradeProtocol}. + * To enable basic statistics to be made available for these protocols, a map of protocol name to + * {@link UpgradeGroupInfo} instances is maintained here. + */ + private final Map upgradeProtocolGroupInfos = new ConcurrentHashMap<>(); + + public UpgradeGroupInfo getUpgradeGroupInfo(String upgradeProtocol) { + if (upgradeProtocol == null) { + return null; + } + UpgradeGroupInfo result = upgradeProtocolGroupInfos.get(upgradeProtocol); + if (result == null) { + // Protecting against multiple JMX registration, not modification + // of the Map. + synchronized (upgradeProtocolGroupInfos) { + result = upgradeProtocolGroupInfos.get(upgradeProtocol); + if (result == null) { + result = new UpgradeGroupInfo(); + upgradeProtocolGroupInfos.put(upgradeProtocol, result); + ObjectName oname = getONameForUpgrade(upgradeProtocol); + if (oname != null) { + try { + Registry.getRegistry(null, null).registerComponent(result, oname, null); + } catch (Exception e) { + getLog().warn(sm.getString("abstractHttp11Protocol.upgradeJmxRegistrationFail"), e); + result = null; + } + } + } + } + } + return result; + } + + + public ObjectName getONameForUpgrade(String upgradeProtocol) { + ObjectName oname = null; + ObjectName parentRgOname = getGlobalRequestProcessorMBeanName(); + if (parentRgOname != null) { + StringBuilder name = new StringBuilder(parentRgOname.getCanonicalName()); + name.append(",Upgrade="); + if (Util.objectNameValueNeedsQuote(upgradeProtocol)) { + name.append(ObjectName.quote(upgradeProtocol)); + } else { + name.append(upgradeProtocol); + } + try { + oname = new ObjectName(name.toString()); + } catch (Exception e) { + getLog().warn(sm.getString("abstractHttp11Protocol.upgradeJmxNameFail"), e); + } + } + return oname; + } + + + // ------------------------------------------------ HTTP specific properties + // ------------------------------------------ passed through to the EndPoint + + public boolean isSSLEnabled() { + return getEndpoint().isSSLEnabled(); + } + + public void setSSLEnabled(boolean SSLEnabled) { + getEndpoint().setSSLEnabled(SSLEnabled); + } + + + public boolean getUseSendfile() { + return getEndpoint().getUseSendfile(); + } + + public void setUseSendfile(boolean useSendfile) { + getEndpoint().setUseSendfile(useSendfile); + } + + + /** + * @return The maximum number of requests which can be performed over a keep-alive connection. The default is the + * same as for Apache HTTP Server (100). + */ + public int getMaxKeepAliveRequests() { + return getEndpoint().getMaxKeepAliveRequests(); + } + + /** + * Set the maximum number of Keep-Alive requests to allow. This is to safeguard from DoS attacks. Setting to a + * negative value disables the limit. + * + * @param mkar The new maximum number of Keep-Alive requests allowed + */ + public void setMaxKeepAliveRequests(int mkar) { + getEndpoint().setMaxKeepAliveRequests(mkar); + } + + + // ----------------------------------------------- HTTPS specific properties + // ------------------------------------------ passed through to the EndPoint + + public String getDefaultSSLHostConfigName() { + return getEndpoint().getDefaultSSLHostConfigName(); + } + + public void setDefaultSSLHostConfigName(String defaultSSLHostConfigName) { + getEndpoint().setDefaultSSLHostConfigName(defaultSSLHostConfigName); + } + + + @Override + public void addSslHostConfig(SSLHostConfig sslHostConfig) { + getEndpoint().addSslHostConfig(sslHostConfig); + } + + + @Override + public void addSslHostConfig(SSLHostConfig sslHostConfig, boolean replace) { + getEndpoint().addSslHostConfig(sslHostConfig, replace); + } + + + @Override + public SSLHostConfig[] findSslHostConfigs() { + return getEndpoint().findSslHostConfigs(); + } + + + public void reloadSslHostConfigs() { + getEndpoint().reloadSslHostConfigs(); + } + + + public void reloadSslHostConfig(String hostName) { + getEndpoint().reloadSslHostConfig(hostName); + } + + + // ------------------------------------------------------------- Common code + + @Override + protected Processor createProcessor() { + Http11Processor processor = new Http11Processor(this, adapter); + return processor; + } + + + @Override + protected Processor createUpgradeProcessor(SocketWrapperBase socket, UpgradeToken upgradeToken) { + HttpUpgradeHandler httpUpgradeHandler = upgradeToken.getHttpUpgradeHandler(); + if (httpUpgradeHandler instanceof InternalHttpUpgradeHandler) { + return new UpgradeProcessorInternal(socket, upgradeToken, getUpgradeGroupInfo(upgradeToken.getProtocol())); + } else { + return new UpgradeProcessorExternal(socket, upgradeToken, getUpgradeGroupInfo(upgradeToken.getProtocol())); + } + } +} diff --git a/java/org/apache/coyote/http11/Constants.java b/java/org/apache/coyote/http11/Constants.java new file mode 100644 index 0000000..700834c --- /dev/null +++ b/java/org/apache/coyote/http11/Constants.java @@ -0,0 +1,157 @@ +/* + * 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.coyote.http11; + +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Constants. + * + * @author Remy Maucherat + */ +public final class Constants { + + public static final int DEFAULT_CONNECTION_TIMEOUT = 60000; + + + /** + * CRLF. + */ + public static final String CRLF = "\r\n"; + + + /** + * CR. + */ + public static final byte CR = (byte) '\r'; + + + /** + * LF. + */ + public static final byte LF = (byte) '\n'; + + + /** + * SP. + */ + public static final byte SP = (byte) ' '; + + + /** + * HT. + */ + public static final byte HT = (byte) '\t'; + + + /** + * COLON. + */ + public static final byte COLON = (byte) ':'; + + + /** + * SEMI_COLON. + */ + public static final byte SEMI_COLON = (byte) ';'; + + + /** + * 'A'. + */ + public static final byte A = (byte) 'A'; + + + /** + * 'a'. + */ + public static final byte a = (byte) 'a'; + + + /** + * 'Z'. + */ + public static final byte Z = (byte) 'Z'; + + + /** + * '?'. + */ + public static final byte QUESTION = (byte) '?'; + + + /** + * Lower case offset. + */ + public static final byte LC_OFFSET = A - a; + + + /* Various constant "strings" */ + public static final String CONNECTION = "Connection"; + public static final String CLOSE = "close"; + public static final String KEEP_ALIVE_HEADER_VALUE_TOKEN = "keep-alive"; + public static final String CHUNKED = "chunked"; + public static final byte[] ACK_BYTES = ByteChunk.convertToBytes("HTTP/1.1 100 " + CRLF + CRLF); + public static final String TRANSFERENCODING = "Transfer-Encoding"; + public static final String KEEP_ALIVE_HEADER_NAME = "Keep-Alive"; + public static final byte[] _200_BYTES = ByteChunk.convertToBytes("200"); + public static final byte[] _400_BYTES = ByteChunk.convertToBytes("400"); + public static final byte[] _404_BYTES = ByteChunk.convertToBytes("404"); + + + /** + * Identity filters (input and output). + */ + public static final int IDENTITY_FILTER = 0; + + + /** + * Chunked filters (input and output). + */ + public static final int CHUNKED_FILTER = 1; + + + /** + * Void filters (input and output). + */ + public static final int VOID_FILTER = 2; + + + /** + * GZIP filter (output). + */ + public static final int GZIP_FILTER = 3; + + + /** + * Buffered filter (input) + */ + public static final int BUFFERED_FILTER = 3; + + + /** + * HTTP/1.0. + */ + public static final String HTTP_10 = "HTTP/1.0"; + + + /** + * HTTP/1.1. + */ + public static final String HTTP_11 = "HTTP/1.1"; + public static final byte[] HTTP_11_BYTES = ByteChunk.convertToBytes(HTTP_11); +} diff --git a/java/org/apache/coyote/http11/HeadersTooLargeException.java b/java/org/apache/coyote/http11/HeadersTooLargeException.java new file mode 100644 index 0000000..49001d2 --- /dev/null +++ b/java/org/apache/coyote/http11/HeadersTooLargeException.java @@ -0,0 +1,41 @@ +/* + * 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.coyote.http11; + +/** + * Exception used to mark the specific error condition of the HTTP headers exceeding the maximum permitted size. + */ +public class HeadersTooLargeException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + public HeadersTooLargeException() { + super(); + } + + public HeadersTooLargeException(String message, Throwable cause) { + super(message, cause); + } + + public HeadersTooLargeException(String s) { + super(s); + } + + public HeadersTooLargeException(Throwable cause) { + super(cause); + } +} diff --git a/java/org/apache/coyote/http11/Http11InputBuffer.java b/java/org/apache/coyote/http11/Http11InputBuffer.java new file mode 100644 index 0000000..9390c3b --- /dev/null +++ b/java/org/apache/coyote/http11/Http11InputBuffer.java @@ -0,0 +1,1237 @@ +/* + * 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.coyote.http11; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.apache.coyote.CloseNowException; +import org.apache.coyote.InputBuffer; +import org.apache.coyote.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.HeaderUtil; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.HttpParser; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +/** + * InputBuffer for HTTP that provides request header parsing as well as transfer encoding. + */ +public class Http11InputBuffer implements InputBuffer, ApplicationBufferHandler { + + // -------------------------------------------------------------- Constants + + private static final Log log = LogFactory.getLog(Http11InputBuffer.class); + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(Http11InputBuffer.class); + + + private static final byte[] CLIENT_PREFACE_START = + "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1); + + /** + * Associated Coyote request. + */ + private final Request request; + + + /** + * Headers of the associated request. + */ + private final MimeHeaders headers; + + + private final boolean rejectIllegalHeader; + + /** + * State. + */ + private volatile boolean parsingHeader; + + + /** + * Swallow input ? (in the case of an expectation) + */ + private boolean swallowInput; + + + /** + * The read buffer. + */ + private ByteBuffer byteBuffer; + + + /** + * Pos of the end of the header in the buffer, which is also the start of the body. + */ + private int end; + + + /** + * Wrapper that provides access to the underlying socket. + */ + private SocketWrapperBase wrapper; + + + /** + * Underlying input buffer. + */ + private InputBuffer inputStreamInputBuffer; + + + /** + * Filter library. Note: Filter[Constants.CHUNKED_FILTER] is always the "chunked" filter. + */ + private InputFilter[] filterLibrary; + + + /** + * Active filters (in order). + */ + private InputFilter[] activeFilters; + + + /** + * Index of the last active filter. + */ + private int lastActiveFilter; + + + /** + * Parsing state - used for non blocking parsing so that when more data arrives, we can pick up where we left off. + */ + private byte prevChr = 0; + private byte chr = 0; + private volatile boolean parsingRequestLine; + private int parsingRequestLinePhase = 0; + private boolean parsingRequestLineEol = false; + private int parsingRequestLineStart = 0; + private int parsingRequestLineQPos = -1; + private HeaderParsePosition headerParsePos; + private final HeaderParseData headerData = new HeaderParseData(); + private final HttpParser httpParser; + + /** + * Maximum allowed size of the HTTP request line plus headers plus any leading blank lines. + */ + private final int headerBufferSize; + + /** + * Known size of the NioChannel read buffer. + */ + private int socketReadBufferSize; + + + // ----------------------------------------------------------- Constructors + + public Http11InputBuffer(Request request, int headerBufferSize, boolean rejectIllegalHeader, + HttpParser httpParser) { + + this.request = request; + headers = request.getMimeHeaders(); + + this.headerBufferSize = headerBufferSize; + this.rejectIllegalHeader = rejectIllegalHeader; + this.httpParser = httpParser; + + filterLibrary = new InputFilter[0]; + activeFilters = new InputFilter[0]; + lastActiveFilter = -1; + + parsingHeader = true; + parsingRequestLine = true; + parsingRequestLinePhase = 0; + parsingRequestLineEol = false; + parsingRequestLineStart = 0; + parsingRequestLineQPos = -1; + headerParsePos = HeaderParsePosition.HEADER_START; + swallowInput = true; + + inputStreamInputBuffer = new SocketInputBuffer(); + } + + + // ------------------------------------------------------------- Properties + + /** + * Add an input filter to the filter library. + * + * @throws NullPointerException if the supplied filter is null + */ + void addFilter(InputFilter filter) { + + if (filter == null) { + throw new NullPointerException(sm.getString("iib.filter.npe")); + } + + InputFilter[] newFilterLibrary = Arrays.copyOf(filterLibrary, filterLibrary.length + 1); + newFilterLibrary[filterLibrary.length] = filter; + filterLibrary = newFilterLibrary; + + activeFilters = new InputFilter[filterLibrary.length]; + } + + + /** + * Get filters. + */ + InputFilter[] getFilters() { + return filterLibrary; + } + + + /** + * Add an input filter to the filter library. + */ + void addActiveFilter(InputFilter filter) { + + if (lastActiveFilter == -1) { + filter.setBuffer(inputStreamInputBuffer); + } else { + for (int i = 0; i <= lastActiveFilter; i++) { + if (activeFilters[i] == filter) { + return; + } + } + filter.setBuffer(activeFilters[lastActiveFilter]); + } + + activeFilters[++lastActiveFilter] = filter; + + filter.setRequest(request); + } + + + /** + * Set the swallow input flag. + */ + void setSwallowInput(boolean swallowInput) { + this.swallowInput = swallowInput; + } + + + // ---------------------------------------------------- InputBuffer Methods + + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + if (lastActiveFilter == -1) { + return inputStreamInputBuffer.doRead(handler); + } else { + return activeFilters[lastActiveFilter].doRead(handler); + } + } + + + // ------------------------------------------------------- Protected Methods + + /** + * Recycle the input buffer. This should be called when closing the connection. + */ + void recycle() { + wrapper = null; + request.recycle(); + + for (int i = 0; i <= lastActiveFilter; i++) { + activeFilters[i].recycle(); + } + + byteBuffer.limit(0).position(0); + lastActiveFilter = -1; + swallowInput = true; + + chr = 0; + prevChr = 0; + headerParsePos = HeaderParsePosition.HEADER_START; + parsingRequestLinePhase = 0; + parsingRequestLineEol = false; + parsingRequestLineStart = 0; + parsingRequestLineQPos = -1; + headerData.recycle(); + // Recycled last because they are volatile + // All variables visible to this thread are guaranteed to be visible to + // any other thread once that thread reads the same volatile. The first + // action when parsing input data is to read one of these volatiles. + parsingRequestLine = true; + parsingHeader = true; + } + + + /** + * End processing of current HTTP request. Note: All bytes of the current request should have been already consumed. + * This method only resets all the pointers so that we are ready to parse the next HTTP request. + */ + void nextRequest() { + request.recycle(); + + if (byteBuffer.position() > 0) { + if (byteBuffer.remaining() > 0) { + // Copy leftover bytes to the beginning of the buffer + byteBuffer.compact(); + byteBuffer.flip(); + } else { + // Reset position and limit to 0 + byteBuffer.position(0).limit(0); + } + } + + // Recycle filters + for (int i = 0; i <= lastActiveFilter; i++) { + activeFilters[i].recycle(); + } + + // Reset pointers + lastActiveFilter = -1; + parsingHeader = true; + swallowInput = true; + + headerParsePos = HeaderParsePosition.HEADER_START; + parsingRequestLine = true; + parsingRequestLinePhase = 0; + parsingRequestLineEol = false; + parsingRequestLineStart = 0; + parsingRequestLineQPos = -1; + headerData.recycle(); + } + + + /** + * Read the request line. This function is meant to be used during the HTTP request header parsing. Do NOT attempt + * to read the request body using it. + * + * @throws IOException If an exception occurs during the underlying socket read operations, or if the given buffer + * is not big enough to accommodate the whole line. + * + * @return true if data is properly fed; false if no data is available immediately and thread should be freed + */ + boolean parseRequestLine(boolean keptAlive, int connectionTimeout, int keepAliveTimeout) throws IOException { + + // check state + if (!parsingRequestLine) { + return true; + } + // + // Skipping blank lines + // + if (parsingRequestLinePhase < 2) { + do { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (keptAlive) { + // Haven't read any request data yet so use the keep-alive + // timeout. + wrapper.setReadTimeout(keepAliveTimeout); + } + if (!fill(false)) { + // A read is pending, so no longer in initial state + parsingRequestLinePhase = 1; + return false; + } + // At least one byte of the request has been received. + // Switch to the socket timeout. + wrapper.setReadTimeout(connectionTimeout); + } + if (!keptAlive && byteBuffer.position() == 0 && byteBuffer.limit() >= CLIENT_PREFACE_START.length) { + boolean prefaceMatch = true; + for (int i = 0; i < CLIENT_PREFACE_START.length && prefaceMatch; i++) { + if (CLIENT_PREFACE_START[i] != byteBuffer.get(i)) { + prefaceMatch = false; + } + } + if (prefaceMatch) { + // HTTP/2 preface matched + parsingRequestLinePhase = -1; + return false; + } + } + // Set the start time once we start reading data (even if it is + // just skipping blank lines) + if (request.getStartTimeNanos() < 0) { + request.setStartTimeNanos(System.nanoTime()); + } + chr = byteBuffer.get(); + } while (chr == Constants.CR || chr == Constants.LF); + byteBuffer.position(byteBuffer.position() - 1); + + parsingRequestLineStart = byteBuffer.position(); + parsingRequestLinePhase = 2; + } + if (parsingRequestLinePhase == 2) { + // + // Reading the method name + // Method name is a token + // + boolean space = false; + while (!space) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) { + return false; + } + } + // Spec says method name is a token followed by a single SP but + // also be tolerant of multiple SP and/or HT. + int pos = byteBuffer.position(); + chr = byteBuffer.get(); + if (chr == Constants.SP || chr == Constants.HT) { + space = true; + request.method().setBytes(byteBuffer.array(), parsingRequestLineStart, + pos - parsingRequestLineStart); + } else if (!HttpParser.isToken(chr)) { + // Avoid unknown protocol triggering an additional error + request.protocol().setString(Constants.HTTP_11); + String invalidMethodValue = parseInvalid(parsingRequestLineStart, byteBuffer); + throw new IllegalArgumentException(sm.getString("iib.invalidmethod", invalidMethodValue)); + } + } + parsingRequestLinePhase = 3; + } + if (parsingRequestLinePhase == 3) { + // Spec says single SP but also be tolerant of multiple SP and/or HT + boolean space = true; + while (space) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) { + return false; + } + } + chr = byteBuffer.get(); + if (chr != Constants.SP && chr != Constants.HT) { + space = false; + byteBuffer.position(byteBuffer.position() - 1); + } + } + parsingRequestLineStart = byteBuffer.position(); + parsingRequestLinePhase = 4; + } + if (parsingRequestLinePhase == 4) { + // Mark the current buffer position + + int end = 0; + // + // Reading the URI + // + boolean space = false; + while (!space) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) { + return false; + } + } + int pos = byteBuffer.position(); + prevChr = chr; + chr = byteBuffer.get(); + if (prevChr == Constants.CR && chr != Constants.LF) { + // CR not followed by LF so not an HTTP/0.9 request and + // therefore invalid. Trigger error handling. + // Avoid unknown protocol triggering an additional error + request.protocol().setString(Constants.HTTP_11); + String invalidRequestTarget = parseInvalid(parsingRequestLineStart, byteBuffer); + throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget", invalidRequestTarget)); + } + if (chr == Constants.SP || chr == Constants.HT) { + space = true; + end = pos; + } else if (chr == Constants.CR) { + // HTTP/0.9 style request. CR is optional. LF is not. + } else if (chr == Constants.LF) { + // HTTP/0.9 style request + // Stop this processing loop + space = true; + // Set blank protocol (indicates HTTP/0.9) + request.protocol().setString(""); + // Skip the protocol processing + parsingRequestLinePhase = 7; + if (prevChr == Constants.CR) { + end = pos - 1; + } else { + end = pos; + } + } else if (chr == Constants.QUESTION && parsingRequestLineQPos == -1) { + parsingRequestLineQPos = pos; + } else if (parsingRequestLineQPos != -1 && !httpParser.isQueryRelaxed(chr)) { + // Avoid unknown protocol triggering an additional error + request.protocol().setString(Constants.HTTP_11); + // %nn decoding will be checked at the point of decoding + String invalidRequestTarget = parseInvalid(parsingRequestLineStart, byteBuffer); + throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget", invalidRequestTarget)); + } else if (httpParser.isNotRequestTargetRelaxed(chr)) { + // Avoid unknown protocol triggering an additional error + request.protocol().setString(Constants.HTTP_11); + // This is a general check that aims to catch problems early + // Detailed checking of each part of the request target will + // happen in Http11Processor#prepareRequest() + String invalidRequestTarget = parseInvalid(parsingRequestLineStart, byteBuffer); + throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget", invalidRequestTarget)); + } + } + if (parsingRequestLineQPos >= 0) { + request.queryString().setBytes(byteBuffer.array(), parsingRequestLineQPos + 1, + end - parsingRequestLineQPos - 1); + request.requestURI().setBytes(byteBuffer.array(), parsingRequestLineStart, + parsingRequestLineQPos - parsingRequestLineStart); + } else { + request.requestURI().setBytes(byteBuffer.array(), parsingRequestLineStart, + end - parsingRequestLineStart); + } + // HTTP/0.9 processing jumps to stage 7. + // Don't want to overwrite that here. + if (parsingRequestLinePhase == 4) { + parsingRequestLinePhase = 5; + } + } + if (parsingRequestLinePhase == 5) { + // Spec says single SP but also be tolerant of multiple and/or HT + boolean space = true; + while (space) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) { + return false; + } + } + byte chr = byteBuffer.get(); + if (chr != Constants.SP && chr != Constants.HT) { + space = false; + byteBuffer.position(byteBuffer.position() - 1); + } + } + parsingRequestLineStart = byteBuffer.position(); + parsingRequestLinePhase = 6; + + // Mark the current buffer position + end = 0; + } + if (parsingRequestLinePhase == 6) { + // + // Reading the protocol + // Protocol is always "HTTP/" DIGIT "." DIGIT + // + while (!parsingRequestLineEol) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) { + return false; + } + } + + int pos = byteBuffer.position(); + prevChr = chr; + chr = byteBuffer.get(); + if (chr == Constants.CR) { + // Possible end of request line. Need LF next else invalid. + } else if (prevChr == Constants.CR && chr == Constants.LF) { + // CRLF is the standard line terminator + end = pos - 1; + parsingRequestLineEol = true; + } else if (chr == Constants.LF) { + // LF is an optional line terminator + end = pos; + parsingRequestLineEol = true; + } else if (prevChr == Constants.CR || !HttpParser.isHttpProtocol(chr)) { + String invalidProtocol = parseInvalid(parsingRequestLineStart, byteBuffer); + throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol", invalidProtocol)); + } + } + + if (end - parsingRequestLineStart > 0) { + request.protocol().setBytes(byteBuffer.array(), parsingRequestLineStart, end - parsingRequestLineStart); + parsingRequestLinePhase = 7; + } + // If no protocol is found, the ISE below will be triggered. + } + if (parsingRequestLinePhase == 7) { + // Parsing is complete. Return and clean-up. + parsingRequestLine = false; + parsingRequestLinePhase = 0; + parsingRequestLineEol = false; + parsingRequestLineStart = 0; + return true; + } + throw new IllegalStateException(sm.getString("iib.invalidPhase", Integer.valueOf(parsingRequestLinePhase))); + } + + + /** + * Parse the HTTP headers. + */ + boolean parseHeaders() throws IOException { + if (!parsingHeader) { + throw new IllegalStateException(sm.getString("iib.parseheaders.ise.error")); + } + + HeaderParseStatus status = HeaderParseStatus.HAVE_MORE_HEADERS; + + do { + status = parseHeader(); + // Checking that + // (1) Headers plus request line size does not exceed its limit + // (2) There are enough bytes to avoid expanding the buffer when + // reading body + // Technically, (2) is technical limitation, (1) is logical + // limitation to enforce the meaning of headerBufferSize + // From the way how buf is allocated and how blank lines are being + // read, it should be enough to check (1) only. + if (byteBuffer.position() > headerBufferSize || + byteBuffer.capacity() - byteBuffer.position() < socketReadBufferSize) { + throw new IllegalArgumentException(sm.getString("iib.requestheadertoolarge.error")); + } + } while (status == HeaderParseStatus.HAVE_MORE_HEADERS); + if (status == HeaderParseStatus.DONE) { + parsingHeader = false; + end = byteBuffer.position(); + return true; + } else { + return false; + } + } + + + int getParsingRequestLinePhase() { + return parsingRequestLinePhase; + } + + + private String parseInvalid(int startPos, ByteBuffer buffer) { + // Look for the next space + byte b = 0; + while (buffer.hasRemaining() && b != 0x20) { + b = buffer.get(); + } + String result = HeaderUtil.toPrintableString(buffer.array(), buffer.arrayOffset() + startPos, + buffer.position() - startPos); + if (b != 0x20) { + // Ran out of buffer rather than found a space + result = result + "..."; + } + return result; + } + + + /** + * End request (consumes leftover bytes). + * + * @throws IOException an underlying I/O error occurred + */ + void endRequest() throws IOException { + + if (swallowInput && (lastActiveFilter != -1)) { + int extraBytes = (int) activeFilters[lastActiveFilter].end(); + byteBuffer.position(byteBuffer.position() - extraBytes); + } + } + + + @Override + public int available() { + return available(false); + } + + + /** + * Available bytes in the buffers for the current request. Note that when requests are pipelined, the data in + * byteBuffer may relate to the next request rather than this one. + */ + int available(boolean read) { + int available; + + if (lastActiveFilter == -1) { + available = inputStreamInputBuffer.available(); + } else { + available = activeFilters[lastActiveFilter].available(); + } + + // Only try a non-blocking read if: + // - there is no data in the filters + // - the caller requested a read + // - there is no data in byteBuffer + // - the socket wrapper indicates a read is allowed + // + // Notes: 1. When pipelined requests are being used available may be + // zero even when byteBuffer has data. This is because the data + // in byteBuffer is for the next request. We don't want to + // attempt a read in this case. + // 2. wrapper.hasDataToRead() is present to handle the NIO2 case + try { + if (available == 0 && read && !byteBuffer.hasRemaining() && wrapper.hasDataToRead()) { + fill(false); + available = byteBuffer.remaining(); + } + } catch (IOException ioe) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("iib.available.readFail"), ioe); + } + // Not ideal. This will indicate that data is available which should + // trigger a read which in turn will trigger another IOException and + // that one can be thrown. + available = 1; + } + return available; + } + + + /** + * Has all of the request body been read? There are subtle differences between this and available() > 0 primarily + * because of having to handle faking non-blocking reads with the blocking IO connector. + */ + boolean isFinished() { + // The active filters have the definitive information on whether or not + // the current request body has been read. Note that byteBuffer may + // contain pipelined data so is not a good indicator. + if (lastActiveFilter >= 0) { + return activeFilters[lastActiveFilter].isFinished(); + } else { + // No filters. Assume request is not finished. EOF will signal end of + // request. + return false; + } + } + + ByteBuffer getLeftover() { + int available = byteBuffer.remaining(); + if (available > 0) { + return ByteBuffer.wrap(byteBuffer.array(), byteBuffer.position(), available); + } else { + return null; + } + } + + + boolean isChunking() { + for (int i = 0; i < lastActiveFilter; i++) { + if (activeFilters[i] == filterLibrary[Constants.CHUNKED_FILTER]) { + return true; + } + } + return false; + } + + + void init(SocketWrapperBase socketWrapper) { + + wrapper = socketWrapper; + wrapper.setAppReadBufHandler(this); + + int bufLength = headerBufferSize + wrapper.getSocketBufferHandler().getReadBuffer().capacity(); + if (byteBuffer == null || byteBuffer.capacity() < bufLength) { + byteBuffer = ByteBuffer.allocate(bufLength); + byteBuffer.position(0).limit(0); + } + } + + + // --------------------------------------------------------- Private Methods + + /** + * Attempts to read some data into the input buffer. + * + * @return true if more data was added to the input buffer otherwise false + */ + private boolean fill(boolean block) throws IOException { + + if (log.isTraceEnabled()) { + log.trace("Before fill(): parsingHeader: [" + parsingHeader + "], parsingRequestLine: [" + + parsingRequestLine + "], parsingRequestLinePhase: [" + parsingRequestLinePhase + + "], parsingRequestLineStart: [" + parsingRequestLineStart + "], byteBuffer.position(): [" + + byteBuffer.position() + "], byteBuffer.limit(): [" + byteBuffer.limit() + "], end: [" + end + "]"); + } + + if (parsingHeader) { + if (byteBuffer.limit() >= headerBufferSize) { + if (parsingRequestLine) { + // Avoid unknown protocol triggering an additional error + request.protocol().setString(Constants.HTTP_11); + } + throw new IllegalArgumentException(sm.getString("iib.requestheadertoolarge.error")); + } + } else { + byteBuffer.limit(end).position(end); + } + + int nRead = -1; + int mark = byteBuffer.position(); + try { + if (byteBuffer.position() < byteBuffer.limit()) { + byteBuffer.position(byteBuffer.limit()); + } + byteBuffer.limit(byteBuffer.capacity()); + SocketWrapperBase socketWrapper = this.wrapper; + if (socketWrapper != null) { + nRead = socketWrapper.read(block, byteBuffer); + } else { + throw new CloseNowException(sm.getString("iib.eof.error")); + } + } finally { + // Ensure that the buffer limit and position are returned to a + // consistent "ready for read" state if an error occurs during in + // the above code block. + // Some error conditions can result in the position being reset to + // zero which also invalidates the mark. + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65677 + if (byteBuffer.position() >= mark) { + // // Position and mark are consistent. Assume a read (possibly + // of zero bytes) has occurred. + byteBuffer.limit(byteBuffer.position()); + byteBuffer.position(mark); + } else { + // Position and mark are inconsistent. Set position and limit to + // zero so effectively no data is reported as read. + byteBuffer.position(0); + byteBuffer.limit(0); + } + } + + if (log.isTraceEnabled()) { + log.trace("Received [" + new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.remaining(), + StandardCharsets.ISO_8859_1) + "]"); + } + + if (nRead > 0) { + return true; + } else if (nRead == -1) { + throw new EOFException(sm.getString("iib.eof.error")); + } else { + return false; + } + + } + + + /** + * Parse an HTTP header. + * + * @return One of {@link HeaderParseStatus#NEED_MORE_DATA}, {@link HeaderParseStatus#HAVE_MORE_HEADERS} or + * {@link HeaderParseStatus#DONE}. + */ + private HeaderParseStatus parseHeader() throws IOException { + + /* + * Implementation note: Any changes to this method probably need to be echoed in + * ChunkedInputFilter.parseHeader(). Why not use a common implementation? In short, this code uses non-blocking + * reads whereas ChunkedInputFilter using blocking reads. The code is just different enough that a common + * implementation wasn't viewed as practical. + */ + while (headerParsePos == HeaderParsePosition.HEADER_START) { + + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) { + return HeaderParseStatus.NEED_MORE_DATA; + } + } + + prevChr = chr; + chr = byteBuffer.get(); + + if (chr == Constants.CR && prevChr != Constants.CR) { + // Possible start of CRLF - process the next byte. + } else if (chr == Constants.LF) { + // CRLF or LF is an acceptable line terminator + return HeaderParseStatus.DONE; + } else { + if (prevChr == Constants.CR) { + // Must have read two bytes (first was CR, second was not LF) + byteBuffer.position(byteBuffer.position() - 2); + } else { + // Must have only read one byte + byteBuffer.position(byteBuffer.position() - 1); + } + break; + } + } + + if (headerParsePos == HeaderParsePosition.HEADER_START) { + // Mark the current buffer position + headerData.start = byteBuffer.position(); + headerData.lineStart = headerData.start; + headerParsePos = HeaderParsePosition.HEADER_NAME; + } + + // + // Reading the header name + // Header name is always US-ASCII + // + + while (headerParsePos == HeaderParsePosition.HEADER_NAME) { + + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) { // parse header + return HeaderParseStatus.NEED_MORE_DATA; + } + } + + int pos = byteBuffer.position(); + chr = byteBuffer.get(); + if (chr == Constants.COLON) { + if (headerData.start == pos) { + // Zero length header name - not valid. + // skipLine() will handle the error + return skipLine(false); + } + headerParsePos = HeaderParsePosition.HEADER_VALUE_START; + headerData.headerValue = headers.addValue(byteBuffer.array(), headerData.start, pos - headerData.start); + pos = byteBuffer.position(); + // Mark the current buffer position + headerData.start = pos; + headerData.realPos = pos; + headerData.lastSignificantChar = pos; + break; + } else if (!HttpParser.isToken(chr)) { + // Non-token characters are illegal in header names + // Parsing continues so the error can be reported in context + headerData.lastSignificantChar = pos; + byteBuffer.position(byteBuffer.position() - 1); + // skipLine() will handle the error + return skipLine(false); + } + + // chr is next byte of header name. Convert to lowercase. + if (chr >= Constants.A && chr <= Constants.Z) { + byteBuffer.put(pos, (byte) (chr - Constants.LC_OFFSET)); + } + } + + // Skip the line and ignore the header + if (headerParsePos == HeaderParsePosition.HEADER_SKIPLINE) { + return skipLine(false); + } + + // + // Reading the header value (which can be spanned over multiple lines) + // + + while (headerParsePos == HeaderParsePosition.HEADER_VALUE_START || + headerParsePos == HeaderParsePosition.HEADER_VALUE || + headerParsePos == HeaderParsePosition.HEADER_MULTI_LINE) { + + if (headerParsePos == HeaderParsePosition.HEADER_VALUE_START) { + // Skipping spaces + while (true) { + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) {// parse header + // HEADER_VALUE_START + return HeaderParseStatus.NEED_MORE_DATA; + } + } + + chr = byteBuffer.get(); + if (chr != Constants.SP && chr != Constants.HT) { + headerParsePos = HeaderParsePosition.HEADER_VALUE; + byteBuffer.position(byteBuffer.position() - 1); + // Avoids prevChr = chr at start of header value + // parsing which causes problems when chr is CR + // (in the case of an empty header value) + chr = 0; + break; + } + } + } + if (headerParsePos == HeaderParsePosition.HEADER_VALUE) { + + // Reading bytes until the end of the line + boolean eol = false; + while (!eol) { + + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) {// parse header + // HEADER_VALUE + return HeaderParseStatus.NEED_MORE_DATA; + } + } + + prevChr = chr; + chr = byteBuffer.get(); + if (chr == Constants.CR && prevChr != Constants.CR) { + // CR is only permitted at the start of a CRLF sequence. + // Possible start of CRLF - process the next byte. + } else if (chr == Constants.LF) { + // CRLF or LF is an acceptable line terminator + eol = true; + } else if (prevChr == Constants.CR) { + // Invalid value - also need to delete header + return skipLine(true); + } else if (HttpParser.isControl(chr) && chr != Constants.HT) { + // Invalid value - also need to delete header + return skipLine(true); + } else if (chr == Constants.SP || chr == Constants.HT) { + byteBuffer.put(headerData.realPos, chr); + headerData.realPos++; + } else { + byteBuffer.put(headerData.realPos, chr); + headerData.realPos++; + headerData.lastSignificantChar = headerData.realPos; + } + } + + // Ignore whitespaces at the end of the line + headerData.realPos = headerData.lastSignificantChar; + + // Checking the first character of the new line. If the character + // is a LWS, then it's a multiline header + headerParsePos = HeaderParsePosition.HEADER_MULTI_LINE; + } + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) {// parse header + // HEADER_MULTI_LINE + return HeaderParseStatus.NEED_MORE_DATA; + } + } + + byte peek = byteBuffer.get(byteBuffer.position()); + if (headerParsePos == HeaderParsePosition.HEADER_MULTI_LINE) { + if (peek != Constants.SP && peek != Constants.HT) { + headerParsePos = HeaderParsePosition.HEADER_START; + break; + } else { + // Copying one extra space in the buffer (since there must + // be at least one space inserted between the lines) + byteBuffer.put(headerData.realPos, peek); + headerData.realPos++; + headerParsePos = HeaderParsePosition.HEADER_VALUE_START; + } + } + } + // Set the header value + headerData.headerValue.setBytes(byteBuffer.array(), headerData.start, + headerData.lastSignificantChar - headerData.start); + headerData.recycle(); + return HeaderParseStatus.HAVE_MORE_HEADERS; + } + + + private HeaderParseStatus skipLine(boolean deleteHeader) throws IOException { + boolean rejectThisHeader = rejectIllegalHeader; + // Check if rejectIllegalHeader is disabled and needs to be overridden + // for this header. The header name is required to determine if this + // override is required. The header name is only available once the + // header has been created. If the header has been created then + // deleteHeader will be true. + if (!rejectThisHeader && deleteHeader) { + if (headers.getName(headers.size() - 1).equalsIgnoreCase("content-length")) { + // Malformed content-length headers must always be rejected + // RFC 9112, section 6.3, bullet 5. + rejectThisHeader = true; + } else { + // Only need to delete the header if the request isn't going to + // be rejected (it will be the most recent one) + headers.removeHeader(headers.size() - 1); + } + } + + // Parse the rest of the invalid header so we can construct a useful + // exception and/or debug message. + headerParsePos = HeaderParsePosition.HEADER_SKIPLINE; + boolean eol = false; + + // Reading bytes until the end of the line + while (!eol) { + + // Read new bytes if needed + if (byteBuffer.position() >= byteBuffer.limit()) { + if (!fill(false)) { + return HeaderParseStatus.NEED_MORE_DATA; + } + } + + int pos = byteBuffer.position(); + prevChr = chr; + chr = byteBuffer.get(); + if (chr == Constants.CR) { + // Skip + } else if (chr == Constants.LF) { + // CRLF or LF is an acceptable line terminator + eol = true; + } else { + headerData.lastSignificantChar = pos; + } + } + if (rejectThisHeader || log.isDebugEnabled()) { + if (rejectThisHeader) { + throw new IllegalArgumentException( + sm.getString("iib.invalidheader.reject", HeaderUtil.toPrintableString(byteBuffer.array(), + headerData.lineStart, headerData.lastSignificantChar - headerData.lineStart + 1))); + } + log.debug(sm.getString("iib.invalidheader", HeaderUtil.toPrintableString(byteBuffer.array(), + headerData.lineStart, headerData.lastSignificantChar - headerData.lineStart + 1))); + } + + headerParsePos = HeaderParsePosition.HEADER_START; + return HeaderParseStatus.HAVE_MORE_HEADERS; + } + + + // ----------------------------------------------------------- Inner classes + + private enum HeaderParseStatus { + DONE, + HAVE_MORE_HEADERS, + NEED_MORE_DATA + } + + + private enum HeaderParsePosition { + /** + * Start of a new header. A CRLF here means that there are no more headers. Any other character starts a header + * name. + */ + HEADER_START, + /** + * Reading a header name. All characters of header are HTTP_TOKEN_CHAR. Header name is followed by ':'. No + * whitespace is allowed.
    + * Any non-HTTP_TOKEN_CHAR (this includes any whitespace) encountered before ':' will result in the whole line + * being ignored. + */ + HEADER_NAME, + /** + * Skipping whitespace before text of header value starts, either on the first line of header value (just after + * ':') or on subsequent lines when it is known that subsequent line starts with SP or HT. + */ + HEADER_VALUE_START, + /** + * Reading the header value. We are inside the value. Either on the first line or on any subsequent line. We + * come into this state from HEADER_VALUE_START after the first non-SP/non-HT byte is encountered on the line. + */ + HEADER_VALUE, + /** + * Before reading a new line of a header. Once the next byte is peeked, the state changes without advancing our + * position. The state becomes either HEADER_VALUE_START (if that first byte is SP or HT), or HEADER_START + * (otherwise). + */ + HEADER_MULTI_LINE, + /** + * Reading all bytes until the next CRLF. The line is being ignored. + */ + HEADER_SKIPLINE + } + + + private static class HeaderParseData { + /** + * The first character of the header line. + */ + int lineStart = 0; + /** + * When parsing header name: first character of the header.
    + * When skipping broken header line: first character of the header.
    + * When parsing header value: first character after ':'. + */ + int start = 0; + /** + * When parsing header name: not used (stays as 0).
    + * When skipping broken header line: not used (stays as 0).
    + * When parsing header value: starts as the first character after ':'. Then is increased as far as more bytes of + * the header are harvested. Bytes from buf[pos] are copied to buf[realPos]. Thus the string from [start] to + * [realPos-1] is the prepared value of the header, with whitespaces removed as needed.
    + */ + int realPos = 0; + /** + * When parsing header name: not used (stays as 0).
    + * When skipping broken header line: last non-CR/non-LF character.
    + * When parsing header value: position after the last not-LWS character.
    + */ + int lastSignificantChar = 0; + /** + * MB that will store the value of the header. It is null while parsing header name and is created after the + * name has been parsed. + */ + MessageBytes headerValue = null; + + public void recycle() { + lineStart = 0; + start = 0; + realPos = 0; + lastSignificantChar = 0; + headerValue = null; + } + } + + + // ------------------------------------- InputStreamInputBuffer Inner Class + + /** + * This class is an input buffer which will read its data from an input stream. + */ + private class SocketInputBuffer implements InputBuffer { + + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + + if (byteBuffer.position() >= byteBuffer.limit()) { + // The application is reading the HTTP request body + boolean block = (request.getReadListener() == null); + if (!fill(block)) { + if (block) { + return -1; + } else { + return 0; + } + } + } + + int length = byteBuffer.remaining(); + handler.setByteBuffer(byteBuffer.duplicate()); + byteBuffer.position(byteBuffer.limit()); + + return length; + } + + @Override + public int available() { + return byteBuffer.remaining(); + } + } + + + @Override + public void setByteBuffer(ByteBuffer buffer) { + byteBuffer = buffer; + } + + + @Override + public ByteBuffer getByteBuffer() { + return byteBuffer; + } + + + @Override + public void expand(int size) { + if (byteBuffer.capacity() >= size) { + byteBuffer.limit(size); + } + ByteBuffer temp = ByteBuffer.allocate(size); + temp.put(byteBuffer); + byteBuffer = temp; + byteBuffer.mark(); + temp = null; + } +} diff --git a/java/org/apache/coyote/http11/Http11Nio2Protocol.java b/java/org/apache/coyote/http11/Http11Nio2Protocol.java new file mode 100644 index 0000000..2d0bbfa --- /dev/null +++ b/java/org/apache/coyote/http11/Http11Nio2Protocol.java @@ -0,0 +1,59 @@ +/* + * 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.coyote.http11; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.Nio2Channel; +import org.apache.tomcat.util.net.Nio2Endpoint; + + +/** + * HTTP/1.1 protocol implementation using NIO2. + */ +public class Http11Nio2Protocol extends AbstractHttp11JsseProtocol { + + private static final Log log = LogFactory.getLog(Http11Nio2Protocol.class); + + + public Http11Nio2Protocol() { + this(new Nio2Endpoint()); + } + + + public Http11Nio2Protocol(Nio2Endpoint endpoint) { + super(endpoint); + } + + + @Override + protected Log getLog() { + return log; + } + + + // ----------------------------------------------------- JMX related methods + + @Override + protected String getNamePrefix() { + if (isSSLEnabled()) { + return "https-" + getSslImplementationShortName() + "-nio2"; + } else { + return "http-nio2"; + } + } +} diff --git a/java/org/apache/coyote/http11/Http11NioProtocol.java b/java/org/apache/coyote/http11/Http11NioProtocol.java new file mode 100644 index 0000000..98b5ccd --- /dev/null +++ b/java/org/apache/coyote/http11/Http11NioProtocol.java @@ -0,0 +1,78 @@ +/* + * 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.coyote.http11; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.NioChannel; +import org.apache.tomcat.util.net.NioEndpoint; + + +/** + * HTTP/1.1 protocol implementation using NIO. + */ +public class Http11NioProtocol extends AbstractHttp11JsseProtocol { + + private static final Log log = LogFactory.getLog(Http11NioProtocol.class); + + + public Http11NioProtocol() { + this(new NioEndpoint()); + } + + + public Http11NioProtocol(NioEndpoint endpoint) { + super(endpoint); + } + + + @Override + protected Log getLog() { + return log; + } + + + // -------------------- Pool setup -------------------- + + public void setSelectorTimeout(long timeout) { + ((NioEndpoint) getEndpoint()).setSelectorTimeout(timeout); + } + + public long getSelectorTimeout() { + return ((NioEndpoint) getEndpoint()).getSelectorTimeout(); + } + + public void setPollerThreadPriority(int threadPriority) { + ((NioEndpoint) getEndpoint()).setPollerThreadPriority(threadPriority); + } + + public int getPollerThreadPriority() { + return ((NioEndpoint) getEndpoint()).getPollerThreadPriority(); + } + + + // ----------------------------------------------------- JMX related methods + + @Override + protected String getNamePrefix() { + if (isSSLEnabled()) { + return "https-" + getSslImplementationShortName() + "-nio"; + } else { + return "http-nio"; + } + } +} diff --git a/java/org/apache/coyote/http11/Http11OutputBuffer.java b/java/org/apache/coyote/http11/Http11OutputBuffer.java new file mode 100644 index 0000000..0b9bf29 --- /dev/null +++ b/java/org/apache/coyote/http11/Http11OutputBuffer.java @@ -0,0 +1,569 @@ +/* + * 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.coyote.http11; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.apache.coyote.ActionCode; +import org.apache.coyote.CloseNowException; +import org.apache.coyote.Response; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +/** + * Provides buffering for the HTTP headers (allowing responses to be reset before they have been committed) and the link + * to the Socket for writing the headers (once committed) and the response body. Note that buffering of the response + * body happens at a higher level. + */ +public class Http11OutputBuffer implements HttpOutputBuffer { + + // -------------------------------------------------------------- Variables + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(Http11OutputBuffer.class); + + + // ----------------------------------------------------- Instance Variables + + /** + * Associated Coyote response. + */ + protected final Response response; + + + private volatile boolean ackSent = false; + + + /** + * Finished flag. + */ + protected boolean responseFinished; + + + /** + * The buffer used for header composition. + */ + protected final ByteBuffer headerBuffer; + + + /** + * Filter library for processing the response body. + */ + protected OutputFilter[] filterLibrary; + + + /** + * Active filters for the current request. + */ + protected OutputFilter[] activeFilters; + + + /** + * Index of the last active filter. + */ + protected int lastActiveFilter; + + + /** + * Underlying output buffer. + */ + protected HttpOutputBuffer outputStreamOutputBuffer; + + + /** + * Wrapper for socket where data will be written to. + */ + protected SocketWrapperBase socketWrapper; + + + /** + * Bytes written to client for the current request + */ + protected long byteCount = 0; + + + protected Http11OutputBuffer(Response response, int headerBufferSize) { + + this.response = response; + + headerBuffer = ByteBuffer.allocate(headerBufferSize); + + filterLibrary = new OutputFilter[0]; + activeFilters = new OutputFilter[0]; + lastActiveFilter = -1; + + responseFinished = false; + + outputStreamOutputBuffer = new SocketOutputBuffer(); + } + + + // ------------------------------------------------------------- Properties + + /** + * Add an output filter to the filter library. Note that calling this method resets the currently active filters to + * none. + * + * @param filter The filter to add + */ + public void addFilter(OutputFilter filter) { + + OutputFilter[] newFilterLibrary = Arrays.copyOf(filterLibrary, filterLibrary.length + 1); + newFilterLibrary[filterLibrary.length] = filter; + filterLibrary = newFilterLibrary; + + activeFilters = new OutputFilter[filterLibrary.length]; + } + + + /** + * Get filters. + * + * @return The current filter library containing all possible filters + */ + public OutputFilter[] getFilters() { + return filterLibrary; + } + + + /** + * Add an output filter to the active filters for the current response. + *

    + * The filter does not have to be present in {@link #getFilters()}. + *

    + * A filter can only be added to a response once. If the filter has already been added to this response then this + * method will be a NO-OP. + * + * @param filter The filter to add + */ + public void addActiveFilter(OutputFilter filter) { + + if (lastActiveFilter == -1) { + filter.setBuffer(outputStreamOutputBuffer); + } else { + for (int i = 0; i <= lastActiveFilter; i++) { + if (activeFilters[i] == filter) { + return; + } + } + filter.setBuffer(activeFilters[lastActiveFilter]); + } + + activeFilters[++lastActiveFilter] = filter; + + filter.setResponse(response); + } + + + // --------------------------------------------------- OutputBuffer Methods + + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + + if (!response.isCommitted()) { + // Send the connector a request for commit. The connector should + // then validate the headers, send them (using sendHeaders) and + // set the filters accordingly. + response.action(ActionCode.COMMIT, null); + } + + if (lastActiveFilter == -1) { + return outputStreamOutputBuffer.doWrite(chunk); + } else { + return activeFilters[lastActiveFilter].doWrite(chunk); + } + } + + + @Override + public long getBytesWritten() { + if (lastActiveFilter == -1) { + return outputStreamOutputBuffer.getBytesWritten(); + } else { + return activeFilters[lastActiveFilter].getBytesWritten(); + } + } + + + // ----------------------------------------------- HttpOutputBuffer Methods + + /** + * Flush the response. + * + * @throws IOException an underlying I/O error occurred + */ + @Override + public void flush() throws IOException { + if (lastActiveFilter == -1) { + outputStreamOutputBuffer.flush(); + } else { + activeFilters[lastActiveFilter].flush(); + } + } + + + @Override + public void end() throws IOException { + if (responseFinished) { + return; + } + + if (lastActiveFilter == -1) { + outputStreamOutputBuffer.end(); + } else { + activeFilters[lastActiveFilter].end(); + } + + responseFinished = true; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Reset the header buffer if an error occurs during the writing of the headers so the error response can be + * written. + */ + void resetHeaderBuffer() { + headerBuffer.position(0).limit(headerBuffer.capacity()); + } + + + /** + * Recycle the output buffer. This should be called when closing the connection. + */ + public void recycle() { + nextRequest(); + socketWrapper = null; + } + + + /** + * End processing of current HTTP request. Note: All bytes of the current request should have been already consumed. + * This method only resets all the pointers so that we are ready to parse the next HTTP request. + */ + public void nextRequest() { + // Recycle filters + for (int i = 0; i <= lastActiveFilter; i++) { + activeFilters[i].recycle(); + } + // Recycle response object + response.recycle(); + // Reset pointers + headerBuffer.position(0).limit(headerBuffer.capacity()); + lastActiveFilter = -1; + ackSent = false; + responseFinished = false; + byteCount = 0; + } + + + public void init(SocketWrapperBase socketWrapper) { + this.socketWrapper = socketWrapper; + } + + + public void sendAck() throws IOException { + // It possible that the protocol configuration is changed between the + // request being received and the first read of the body. That could led + // to multiple calls to this method so ensure the ACK is only sent once. + if (!response.isCommitted() && !ackSent) { + ackSent = true; + socketWrapper.write(isBlocking(), Constants.ACK_BYTES, 0, Constants.ACK_BYTES.length); + if (flushBuffer(true)) { + throw new IOException(sm.getString("iob.failedwrite.ack")); + } + } + } + + + /** + * Commit the response. + * + * @throws IOException an underlying I/O error occurred + */ + protected void commit() throws IOException { + response.setCommitted(true); + + if (headerBuffer.position() > 0) { + // Sending the response header buffer + headerBuffer.flip(); + try { + SocketWrapperBase socketWrapper = this.socketWrapper; + if (socketWrapper != null) { + socketWrapper.write(isBlocking(), headerBuffer); + } else { + throw new CloseNowException(sm.getString("iob.failedwrite")); + } + } finally { + headerBuffer.position(0).limit(headerBuffer.capacity()); + } + } + } + + + /** + * Send the response status line. + */ + public void sendStatus() { + // Write protocol name + write(Constants.HTTP_11_BYTES); + headerBuffer.put(Constants.SP); + + // Write status code + int status = response.getStatus(); + switch (status) { + case 200: + write(Constants._200_BYTES); + break; + case 400: + write(Constants._400_BYTES); + break; + case 404: + write(Constants._404_BYTES); + break; + default: + write(status); + } + + headerBuffer.put(Constants.SP); + + // The reason phrase is optional but the space before it is not. Skip + // sending the reason phrase. Clients should ignore it (RFC 7230) and it + // just wastes bytes. + + headerBuffer.put(Constants.CR).put(Constants.LF); + } + + + /** + * Send a header. + * + * @param name Header name + * @param value Header value + */ + public void sendHeader(MessageBytes name, MessageBytes value) { + write(name); + headerBuffer.put(Constants.COLON).put(Constants.SP); + write(value); + headerBuffer.put(Constants.CR).put(Constants.LF); + } + + + /** + * End the header block. + */ + public void endHeaders() { + headerBuffer.put(Constants.CR).put(Constants.LF); + } + + + /** + * This method will write the contents of the specified message bytes buffer to the output stream, without + * filtering. This method is meant to be used to write the response header. + * + * @param mb data to be written + */ + private void write(MessageBytes mb) { + if (mb.getType() != MessageBytes.T_BYTES) { + mb.toBytes(); + ByteChunk bc = mb.getByteChunk(); + // Need to filter out CTLs excluding TAB. ISO-8859-1 and UTF-8 + // values will be OK. Strings using other encodings may be + // corrupted. + byte[] buffer = bc.getBuffer(); + for (int i = bc.getOffset(); i < bc.getLength(); i++) { + // byte values are signed i.e. -128 to 127 + // The values are used unsigned. 0 to 31 are CTLs so they are + // filtered (apart from TAB which is 9). 127 is a control (DEL). + // The values 128 to 255 are all OK. Converting those to signed + // gives -128 to -1. + if ((buffer[i] > -1 && buffer[i] <= 31 && buffer[i] != 9) || buffer[i] == 127) { + buffer[i] = ' '; + } + } + } + write(mb.getByteChunk()); + } + + + /** + * This method will write the contents of the specified byte chunk to the output stream, without filtering. This + * method is meant to be used to write the response header. + * + * @param bc data to be written + */ + private void write(ByteChunk bc) { + // Writing the byte chunk to the output buffer + int length = bc.getLength(); + checkLengthBeforeWrite(length); + headerBuffer.put(bc.getBytes(), bc.getStart(), length); + } + + + /** + * This method will write the contents of the specified byte buffer to the output stream, without filtering. This + * method is meant to be used to write the response header. + * + * @param b data to be written + */ + public void write(byte[] b) { + checkLengthBeforeWrite(b.length); + + // Writing the byte chunk to the output buffer + headerBuffer.put(b); + } + + + /** + * This method will write the specified integer to the output stream. This method is meant to be used to write the + * response header. + * + * @param value data to be written + */ + private void write(int value) { + // From the Tomcat 3.3 HTTP/1.0 connector + String s = Integer.toString(value); + int len = s.length(); + checkLengthBeforeWrite(len); + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + headerBuffer.put((byte) c); + } + } + + + /** + * Checks to see if there is enough space in the buffer to write the requested number of bytes. + */ + private void checkLengthBeforeWrite(int length) { + // "+ 4": BZ 57509. Reserve space for CR/LF/COLON/SP characters that + // are put directly into the buffer following this write operation. + if (headerBuffer.position() + length + 4 > headerBuffer.capacity()) { + throw new HeadersTooLargeException(sm.getString("iob.responseheadertoolarge.error")); + } + } + + + // ------------------------------------------------------ Non-blocking writes + + /** + * Writes any remaining buffered data. + * + * @param block Should this method block until the buffer is empty + * + * @return true if data remains in the buffer (which can only happen in non-blocking mode) else + * false. + * + * @throws IOException Error writing data + */ + protected boolean flushBuffer(boolean block) throws IOException { + return socketWrapper.flush(block); + } + + + /** + * Is standard Servlet blocking IO being used for output? + * + * @return true if this is blocking IO + */ + protected final boolean isBlocking() { + return response.getWriteListener() == null; + } + + + protected final boolean isReady() { + boolean result = !hasDataToWrite(); + if (!result) { + socketWrapper.registerWriteInterest(); + } + return result; + } + + + public boolean hasDataToWrite() { + return socketWrapper.hasDataToWrite(); + } + + + public void registerWriteInterest() { + socketWrapper.registerWriteInterest(); + } + + + boolean isChunking() { + for (int i = 0; i < lastActiveFilter; i++) { + if (activeFilters[i] == filterLibrary[Constants.CHUNKED_FILTER]) { + return true; + } + } + return false; + } + + + // ------------------------------------------ SocketOutputBuffer Inner Class + + /** + * This class is an output buffer which will write data to a socket. + */ + protected class SocketOutputBuffer implements HttpOutputBuffer { + + /** + * Write chunk. + */ + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + try { + int len = chunk.remaining(); + SocketWrapperBase socketWrapper = Http11OutputBuffer.this.socketWrapper; + if (socketWrapper != null) { + socketWrapper.write(isBlocking(), chunk); + } else { + throw new CloseNowException(sm.getString("iob.failedwrite")); + } + len -= chunk.remaining(); + byteCount += len; + return len; + } catch (IOException ioe) { + response.action(ActionCode.CLOSE_NOW, ioe); + // Re-throw + throw ioe; + } + } + + @Override + public long getBytesWritten() { + return byteCount; + } + + @Override + public void end() throws IOException { + socketWrapper.flush(true); + } + + @Override + public void flush() throws IOException { + socketWrapper.flush(isBlocking()); + } + } +} diff --git a/java/org/apache/coyote/http11/Http11Processor.java b/java/org/apache/coyote/http11/Http11Processor.java new file mode 100644 index 0000000..87aa0da --- /dev/null +++ b/java/org/apache/coyote/http11/Http11Processor.java @@ -0,0 +1,1435 @@ +/* + * 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.coyote.http11; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import jakarta.servlet.ServletConnection; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.coyote.AbstractProcessor; +import org.apache.coyote.ActionCode; +import org.apache.coyote.Adapter; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.coyote.ErrorState; +import org.apache.coyote.Request; +import org.apache.coyote.RequestInfo; +import org.apache.coyote.UpgradeProtocol; +import org.apache.coyote.UpgradeToken; +import org.apache.coyote.http11.filters.BufferedInputFilter; +import org.apache.coyote.http11.filters.ChunkedInputFilter; +import org.apache.coyote.http11.filters.ChunkedOutputFilter; +import org.apache.coyote.http11.filters.GzipOutputFilter; +import org.apache.coyote.http11.filters.IdentityInputFilter; +import org.apache.coyote.http11.filters.IdentityOutputFilter; +import org.apache.coyote.http11.filters.SavedRequestInputFilter; +import org.apache.coyote.http11.filters.VoidInputFilter; +import org.apache.coyote.http11.filters.VoidOutputFilter; +import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; +import org.apache.coyote.http11.upgrade.UpgradeApplicationBufferHandler; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.HttpParser; +import org.apache.tomcat.util.http.parser.TokenList; +import org.apache.tomcat.util.log.UserDataHelper; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SendfileDataBase; +import org.apache.tomcat.util.net.SendfileKeepAliveState; +import org.apache.tomcat.util.net.SendfileState; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +public class Http11Processor extends AbstractProcessor { + + private static final Log log = LogFactory.getLog(Http11Processor.class); + + /** + * The string manager for this package. + */ + private static final StringManager sm = StringManager.getManager(Http11Processor.class); + + + private final AbstractHttp11Protocol protocol; + + + /** + * Input. + */ + private final Http11InputBuffer inputBuffer; + + + /** + * Output. + */ + private final Http11OutputBuffer outputBuffer; + + + private final HttpParser httpParser; + + + /** + * Tracks how many internal filters are in the filter library so they are skipped when looking for pluggable + * filters. + */ + private int pluggableFilterIndex = Integer.MAX_VALUE; + + + /** + * Keep-alive. + */ + private volatile boolean keepAlive = true; + + + /** + * Flag used to indicate that the socket should be kept open (e.g. for keep alive or send file). + */ + private volatile boolean openSocket = false; + + + /** + * Flag that indicates if the request headers have been completely read. + */ + private volatile boolean readComplete = true; + + /** + * HTTP/1.1 flag. + */ + private boolean http11 = true; + + + /** + * HTTP/0.9 flag. + */ + private boolean http09 = false; + + + /** + * Content delimiter for the request (if false, the connection will be closed at the end of the request). + */ + private boolean contentDelimitation = true; + + + /** + * Instance of the new protocol to use after the HTTP connection has been upgraded. + */ + private UpgradeToken upgradeToken = null; + + + /** + * Sendfile data. + */ + private SendfileDataBase sendfileData = null; + + + @SuppressWarnings("deprecation") + public Http11Processor(AbstractHttp11Protocol protocol, Adapter adapter) { + super(adapter); + this.protocol = protocol; + + httpParser = new HttpParser(protocol.getRelaxedPathChars(), protocol.getRelaxedQueryChars()); + + inputBuffer = new Http11InputBuffer(request, protocol.getMaxHttpRequestHeaderSize(), + protocol.getRejectIllegalHeader(), httpParser); + request.setInputBuffer(inputBuffer); + + outputBuffer = new Http11OutputBuffer(response, protocol.getMaxHttpResponseHeaderSize()); + response.setOutputBuffer(outputBuffer); + + // Create and add the identity filters. + inputBuffer.addFilter(new IdentityInputFilter(protocol.getMaxSwallowSize())); + outputBuffer.addFilter(new IdentityOutputFilter()); + + // Create and add the chunked filters. + inputBuffer.addFilter( + new ChunkedInputFilter(protocol.getMaxTrailerSize(), protocol.getAllowedTrailerHeadersInternal(), + protocol.getMaxExtensionSize(), protocol.getMaxSwallowSize())); + outputBuffer.addFilter(new ChunkedOutputFilter()); + + // Create and add the void filters. + inputBuffer.addFilter(new VoidInputFilter()); + outputBuffer.addFilter(new VoidOutputFilter()); + + // Create and add buffered input filter + inputBuffer.addFilter(new BufferedInputFilter(protocol.getMaxSwallowSize())); + + // Create and add the gzip filters. + // inputBuffer.addFilter(new GzipInputFilter()); + outputBuffer.addFilter(new GzipOutputFilter()); + + pluggableFilterIndex = inputBuffer.getFilters().length; + } + + + /** + * Determine if we must drop the connection because of the HTTP status code. Use the same list of codes as + * Apache/httpd. + */ + private static boolean statusDropsConnection(int status) { + return status == 400 /* SC_BAD_REQUEST */ || status == 408 /* SC_REQUEST_TIMEOUT */ || + status == 411 /* SC_LENGTH_REQUIRED */ || status == 413 /* SC_REQUEST_ENTITY_TOO_LARGE */ || + status == 414 /* SC_REQUEST_URI_TOO_LONG */ || status == 500 /* SC_INTERNAL_SERVER_ERROR */ || + status == 503 /* SC_SERVICE_UNAVAILABLE */ || status == 501 /* SC_NOT_IMPLEMENTED */; + } + + + /** + * Add an input filter to the current request. If the encoding is not supported, a 501 response will be returned to + * the client. + */ + private void addInputFilter(InputFilter[] inputFilters, String encodingName) { + if (contentDelimitation) { + // Chunked has already been specified and it must be the final + // encoding. + // 400 - Bad request + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, null); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.prepare") + + " Transfer encoding lists chunked before [" + encodingName + "]"); + } + return; + } + + // Parsing trims and converts to lower case. + if (encodingName.equals("chunked")) { + inputBuffer.addActiveFilter(inputFilters[Constants.CHUNKED_FILTER]); + contentDelimitation = true; + } else { + for (int i = pluggableFilterIndex; i < inputFilters.length; i++) { + if (inputFilters[i].getEncodingName().toString().equals(encodingName)) { + inputBuffer.addActiveFilter(inputFilters[i]); + return; + } + } + // Unsupported transfer encoding + // 501 - Unimplemented + response.setStatus(501); + setErrorState(ErrorState.CLOSE_CLEAN, null); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.prepare") + " Unsupported transfer encoding [" + + encodingName + "]"); + } + } + } + + + @Override + public SocketState service(SocketWrapperBase socketWrapper) throws IOException { + RequestInfo rp = request.getRequestProcessor(); + rp.setStage(org.apache.coyote.Constants.STAGE_PARSE); + + // Setting up the I/O + setSocketWrapper(socketWrapper); + + // Flags + keepAlive = true; + openSocket = false; + readComplete = true; + boolean keptAlive = false; + SendfileState sendfileState = SendfileState.DONE; + + while (!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null && + sendfileState == SendfileState.DONE && !protocol.isPaused()) { + + // Parsing the request header + try { + if (!inputBuffer.parseRequestLine(keptAlive, protocol.getConnectionTimeout(), + protocol.getKeepAliveTimeout())) { + if (inputBuffer.getParsingRequestLinePhase() == -1) { + return SocketState.UPGRADING; + } else if (handleIncompleteRequestLineRead()) { + break; + } + } + + // Process the Protocol component of the request line + // Need to know if this is an HTTP 0.9 request before trying to + // parse headers. + prepareRequestProtocol(); + + if (protocol.isPaused()) { + // 503 - Service unavailable + response.setStatus(503); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } else { + keptAlive = true; + // Set this every time in case limit has been changed via JMX + request.getMimeHeaders().setLimit(protocol.getMaxHeaderCount()); + // Don't parse headers for HTTP/0.9 + if (!http09 && !inputBuffer.parseHeaders()) { + // We've read part of the request, don't recycle it + // instead associate it with the socket + openSocket = true; + readComplete = false; + break; + } + if (!protocol.getDisableUploadTimeout()) { + socketWrapper.setReadTimeout(protocol.getConnectionUploadTimeout()); + } + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.header.parse"), e); + } + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + break; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + UserDataHelper.Mode logMode = userDataHelper.getNextMode(); + if (logMode != null) { + String message = sm.getString("http11processor.header.parse"); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("http11processor.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + log.info(message, t); + break; + case DEBUG: + log.debug(message, t); + } + } + // 400 - Bad Request + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, t); + } + + // Has an upgrade been requested? + if (isConnectionToken(request.getMimeHeaders(), "upgrade")) { + // Check the protocol + String requestedProtocol = request.getHeader("Upgrade"); + + UpgradeProtocol upgradeProtocol = protocol.getUpgradeProtocol(requestedProtocol); + if (upgradeProtocol != null) { + if (upgradeProtocol.accept(request)) { + // Create clone of request for upgraded protocol + Request upgradeRequest = null; + try { + upgradeRequest = cloneRequest(request); + } catch (ByteChunk.BufferOverflowException ioe) { + response.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } catch (IOException ioe) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + setErrorState(ErrorState.CLOSE_CLEAN, ioe); + } + + if (upgradeRequest != null) { + // Complete the HTTP/1.1 upgrade process + response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); + response.setHeader("Connection", "Upgrade"); + response.setHeader("Upgrade", requestedProtocol); + action(ActionCode.CLOSE, null); + getAdapter().log(request, response, 0); + + // Continue processing using new protocol + InternalHttpUpgradeHandler upgradeHandler = upgradeProtocol + .getInternalUpgradeHandler(socketWrapper, getAdapter(), upgradeRequest); + UpgradeToken upgradeToken = new UpgradeToken(upgradeHandler, null, null, requestedProtocol); + action(ActionCode.UPGRADE, upgradeToken); + return SocketState.UPGRADING; + } + } + } + } + + if (getErrorState().isIoAllowed()) { + // Setting up filters, and parse some request headers + rp.setStage(org.apache.coyote.Constants.STAGE_PREPARE); + try { + prepareRequest(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.prepare"), t); + } + // 500 - Internal Server Error + response.setStatus(500); + setErrorState(ErrorState.CLOSE_CLEAN, t); + } + } + + int maxKeepAliveRequests = protocol.getMaxKeepAliveRequests(); + if (maxKeepAliveRequests == 1) { + keepAlive = false; + } else if (maxKeepAliveRequests > 0 && socketWrapper.decrementKeepAlive() <= 0) { + keepAlive = false; + } + + // Process the request in the adapter + if (getErrorState().isIoAllowed()) { + try { + rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE); + getAdapter().service(request, response); + // Handle when the response was committed before a serious + // error occurred. Throwing a ServletException should both + // set the status to 500 and set the errorException. + // If we fail here, then the response is likely already + // committed, so we can't try and set headers. + if (keepAlive && !getErrorState().isError() && !isAsync() && + statusDropsConnection(response.getStatus())) { + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + } catch (InterruptedIOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } catch (HeadersTooLargeException e) { + log.error(sm.getString("http11processor.request.process"), e); + // The response should not have been committed but check it + // anyway to be safe + if (response.isCommitted()) { + setErrorState(ErrorState.CLOSE_NOW, e); + } else { + response.reset(); + response.setStatus(500); + setErrorState(ErrorState.CLOSE_CLEAN, e); + response.setHeader("Connection", "close"); // TODO: Remove + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("http11processor.request.process"), t); + // 500 - Internal Server Error + response.setStatus(500); + setErrorState(ErrorState.CLOSE_CLEAN, t); + getAdapter().log(request, response, 0); + } + } + + // Finish the handling of the request + rp.setStage(org.apache.coyote.Constants.STAGE_ENDINPUT); + if (!isAsync()) { + // If this is an async request then the request ends when it has + // been completed. The AsyncContext is responsible for calling + // endRequest() in that case. + endRequest(); + } + rp.setStage(org.apache.coyote.Constants.STAGE_ENDOUTPUT); + + // If there was an error, make sure the request is counted as + // and error, and update the statistics counter + if (getErrorState().isError()) { + response.setStatus(500); + } + + if (!isAsync() || getErrorState().isError()) { + request.updateCounters(); + if (getErrorState().isIoAllowed()) { + inputBuffer.nextRequest(); + outputBuffer.nextRequest(); + } + } + + if (!protocol.getDisableUploadTimeout()) { + int connectionTimeout = protocol.getConnectionTimeout(); + if (connectionTimeout > 0) { + socketWrapper.setReadTimeout(connectionTimeout); + } else { + socketWrapper.setReadTimeout(0); + } + } + + rp.setStage(org.apache.coyote.Constants.STAGE_KEEPALIVE); + + sendfileState = processSendfile(socketWrapper); + } + + rp.setStage(org.apache.coyote.Constants.STAGE_ENDED); + + if (getErrorState().isError() || (protocol.isPaused() && !isAsync())) { + return SocketState.CLOSED; + } else if (isAsync()) { + return SocketState.LONG; + } else if (isUpgrade()) { + return SocketState.UPGRADING; + } else { + if (sendfileState == SendfileState.PENDING) { + return SocketState.SENDFILE; + } else { + if (openSocket) { + if (readComplete) { + return SocketState.OPEN; + } else { + return SocketState.LONG; + } + } else { + return SocketState.CLOSED; + } + } + } + } + + + @Override + protected final void setSocketWrapper(SocketWrapperBase socketWrapper) { + super.setSocketWrapper(socketWrapper); + inputBuffer.init(socketWrapper); + outputBuffer.init(socketWrapper); + } + + + private Request cloneRequest(Request source) throws IOException { + Request dest = new Request(); + + // Transfer the minimal information required for the copy of the Request + // that is passed to the HTTP upgrade process + dest.decodedURI().duplicate(source.decodedURI()); + dest.method().duplicate(source.method()); + dest.getMimeHeaders().duplicate(source.getMimeHeaders()); + dest.requestURI().duplicate(source.requestURI()); + dest.queryString().duplicate(source.queryString()); + + // Preparation for reading the request body + MimeHeaders headers = source.getMimeHeaders(); + prepareExpectation(headers); + prepareInputFilters(headers); + ack(ContinueResponseTiming.ALWAYS); + + // Need to read and buffer the request body, if any. RFC 7230 requires + // that the request is fully read before the upgrade takes place. + ByteChunk body = new ByteChunk(); + int maxSavePostSize = protocol.getMaxSavePostSize(); + if (maxSavePostSize != 0) { + body.setLimit(maxSavePostSize); + ApplicationBufferHandler buffer = new UpgradeApplicationBufferHandler(); + + while (source.getInputBuffer().doRead(buffer) >= 0) { + body.append(buffer.getByteBuffer()); + } + } + + // Make the buffered request body available to the upgraded protocol. + SavedRequestInputFilter srif = new SavedRequestInputFilter(body); + dest.setInputBuffer(srif); + + return dest; + } + + + private boolean handleIncompleteRequestLineRead() { + // Haven't finished reading the request so keep the socket + // open + openSocket = true; + // Check to see if we have read any of the request line yet + if (inputBuffer.getParsingRequestLinePhase() > 1) { + // Started to read request line. + if (protocol.isPaused()) { + // Partially processed the request so need to respond + response.setStatus(503); + setErrorState(ErrorState.CLOSE_CLEAN, null); + return false; + } else { + // Need to keep processor associated with socket + readComplete = false; + } + } + return true; + } + + + private void checkExpectationAndResponseStatus() { + if (request.hasExpectation() && !isRequestBodyFullyRead() && + (response.getStatus() < 200 || response.getStatus() > 299)) { + // Client sent Expect: 100-continue but received a + // non-2xx final response. Disable keep-alive (if enabled) + // to ensure that the connection is closed. Some clients may + // still send the body, some may send the next request. + // No way to differentiate, so close the connection to + // force the client to send the next request. + inputBuffer.setSwallowInput(false); + keepAlive = false; + } + } + + + private void checkMaxSwallowSize() { + // Parse content-length header + long contentLength = -1; + try { + contentLength = request.getContentLengthLong(); + } catch (Exception e) { + // Ignore, an error here is already processed in prepareRequest + // but is done again since the content length is still -1 + } + if (contentLength > 0 && protocol.getMaxSwallowSize() > -1 && + (contentLength - request.getBytesRead() > protocol.getMaxSwallowSize())) { + // There is more data to swallow than Tomcat will accept so the + // connection is going to be closed. Disable keep-alive which will + // trigger adding the "Connection: close" header if not already + // present. + keepAlive = false; + } + } + + + private void prepareRequestProtocol() { + + MessageBytes protocolMB = request.protocol(); + if (protocolMB.equals(Constants.HTTP_11)) { + http09 = false; + http11 = true; + protocolMB.setString(Constants.HTTP_11); + } else if (protocolMB.equals(Constants.HTTP_10)) { + http09 = false; + http11 = false; + keepAlive = false; + protocolMB.setString(Constants.HTTP_10); + } else if (protocolMB.equals("")) { + // HTTP/0.9 + http09 = true; + http11 = false; + keepAlive = false; + } else { + // Unsupported protocol + http09 = false; + http11 = false; + // Send 505; Unsupported HTTP version + response.setStatus(505); + setErrorState(ErrorState.CLOSE_CLEAN, null); + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.request.prepare") + " Unsupported HTTP version \"" + + protocolMB + "\""); + } + } + } + + + /** + * After reading the request headers, we have to setup the request filters. + */ + @SuppressWarnings("deprecation") + private void prepareRequest() throws IOException { + + if (protocol.isSSLEnabled()) { + request.scheme().setString("https"); + } + + MimeHeaders headers = request.getMimeHeaders(); + + // Check connection header + MessageBytes connectionValueMB = headers.getValue(Constants.CONNECTION); + if (connectionValueMB != null && !connectionValueMB.isNull()) { + Set tokens = new HashSet<>(); + TokenList.parseTokenList(headers.values(Constants.CONNECTION), tokens); + if (tokens.contains(Constants.CLOSE)) { + keepAlive = false; + } else if (tokens.contains(Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN)) { + keepAlive = true; + } + } + + if (http11) { + prepareExpectation(headers); + } + + // Check user-agent header + Pattern restrictedUserAgents = protocol.getRestrictedUserAgentsPattern(); + if (restrictedUserAgents != null && (http11 || keepAlive)) { + MessageBytes userAgentValueMB = headers.getValue("user-agent"); + // Check in the restricted list, and adjust the http11 + // and keepAlive flags accordingly + if (userAgentValueMB != null && !userAgentValueMB.isNull()) { + String userAgentValue = userAgentValueMB.toString(); + if (restrictedUserAgents.matcher(userAgentValue).matches()) { + http11 = false; + keepAlive = false; + } + } + } + + + // Check host header + MessageBytes hostValueMB = null; + try { + hostValueMB = headers.getUniqueValue("host"); + } catch (IllegalArgumentException iae) { + // Multiple Host headers are not permitted + badRequest("http11processor.request.multipleHosts"); + } + if (http11 && hostValueMB == null) { + badRequest("http11processor.request.noHostHeader"); + } + + // Check for an absolute-URI less the query string which has already + // been removed during the parsing of the request line + ByteChunk uriBC = request.requestURI().getByteChunk(); + byte[] uriB = uriBC.getBytes(); + if (uriBC.startsWithIgnoreCase("http", 0)) { + int pos = 4; + // Check for https + if (uriBC.startsWithIgnoreCase("s", pos)) { + pos++; + } + // Next 3 characters must be "://" + if (uriBC.startsWith("://", pos)) { + pos += 3; + int uriBCStart = uriBC.getStart(); + + // '/' does not appear in the authority so use the first + // instance to split the authority and the path segments + int slashPos = uriBC.indexOf('/', pos); + // '@' in the authority delimits the userinfo + int atPos = uriBC.indexOf('@', pos); + if (slashPos > -1 && atPos > slashPos) { + // First '@' is in the path segments so no userinfo + atPos = -1; + } + + if (slashPos == -1) { + slashPos = uriBC.getLength(); + // Set URI as "/". Use 6 as it will always be a '/'. + // 01234567 + // http:// + // https:// + request.requestURI().setBytes(uriB, uriBCStart + 6, 1); + } else { + request.requestURI().setBytes(uriB, uriBCStart + slashPos, uriBC.getLength() - slashPos); + } + + // Skip any user info + if (atPos != -1) { + // Validate the userinfo + for (; pos < atPos; pos++) { + byte c = uriB[uriBCStart + pos]; + if (!HttpParser.isUserInfo(c)) { + // Strictly there needs to be a check for valid %nn + // encoding here but skip it since it will never be + // decoded because the userinfo is ignored + badRequest("http11processor.request.invalidUserInfo"); + break; + } + } + // Skip the '@' + pos = atPos + 1; + } + + if (http11) { + // Missing host header is illegal but handled above + if (hostValueMB != null) { + // Any host in the request line must be consistent with + // the Host header + if (!hostValueMB.getByteChunk().equalsIgnoreCase(uriB, uriBCStart + pos, slashPos - pos)) { + if (protocol.getAllowHostHeaderMismatch()) { + // The requirements of RFC 2616 are being + // applied. If the host header and the request + // line do not agree, the request line takes + // precedence + hostValueMB = headers.setValue("host"); + hostValueMB.setBytes(uriB, uriBCStart + pos, slashPos - pos); + } else { + // The requirements of RFC 7230 are being + // applied. If the host header and the request + // line do not agree, trigger a 400 response. + badRequest("http11processor.request.inconsistentHosts"); + } + } + } + } else { + // Not HTTP/1.1 - no Host header so generate one since + // Tomcat internals assume it is set + try { + hostValueMB = headers.setValue("host"); + hostValueMB.setBytes(uriB, uriBCStart + pos, slashPos - pos); + } catch (IllegalStateException e) { + // Edge case + // If the request has too many headers it won't be + // possible to create the host header. Ignore this as + // processing won't reach the point where the Tomcat + // internals expect there to be a host header. + } + } + } else { + badRequest("http11processor.request.invalidScheme"); + } + } + + // Validate the characters in the URI. %nn decoding will be checked at + // the point of decoding. + for (int i = uriBC.getStart(); i < uriBC.getEnd(); i++) { + if (!httpParser.isAbsolutePathRelaxed(uriB[i])) { + badRequest("http11processor.request.invalidUri"); + break; + } + } + + // Input filter setup + prepareInputFilters(headers); + + // Validate host name and extract port if present + parseHost(hostValueMB); + + if (!getErrorState().isIoAllowed()) { + getAdapter().log(request, response, 0); + } + } + + + private void prepareExpectation(MimeHeaders headers) { + MessageBytes expectMB = headers.getValue("expect"); + if (expectMB != null && !expectMB.isNull()) { + if (expectMB.toString().trim().equalsIgnoreCase("100-continue")) { + request.setExpectation(true); + } else { + response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + } + } + + private void prepareInputFilters(MimeHeaders headers) throws IOException { + + contentDelimitation = false; + + InputFilter[] inputFilters = inputBuffer.getFilters(); + + // Parse transfer-encoding header + // HTTP specs say an HTTP 1.1 server should accept any recognised + // HTTP 1.x header from a 1.x client unless the specs says otherwise. + if (!http09) { + MessageBytes transferEncodingValueMB = headers.getValue("transfer-encoding"); + if (transferEncodingValueMB != null) { + List encodingNames = new ArrayList<>(); + if (TokenList.parseTokenList(headers.values("transfer-encoding"), encodingNames)) { + for (String encodingName : encodingNames) { + addInputFilter(inputFilters, encodingName); + } + } else { + // Invalid transfer encoding + badRequest("http11processor.request.invalidTransferEncoding"); + } + } + } + + // Parse content-length header + long contentLength = -1; + try { + contentLength = request.getContentLengthLong(); + } catch (NumberFormatException e) { + badRequest("http11processor.request.nonNumericContentLength"); + } catch (IllegalArgumentException e) { + badRequest("http11processor.request.multipleContentLength"); + } + if (contentLength >= 0) { + if (contentDelimitation) { + // contentDelimitation being true at this point indicates that + // chunked encoding is being used but chunked encoding should + // not be used with a content length. RFC 2616, section 4.4, + // bullet 3 states Content-Length must be ignored in this case - + // so remove it. + headers.removeHeader("content-length"); + request.setContentLength(-1); + keepAlive = false; + } else { + inputBuffer.addActiveFilter(inputFilters[Constants.IDENTITY_FILTER]); + contentDelimitation = true; + } + } + + if (!contentDelimitation) { + // If there's no content length + // (broken HTTP/1.0 or HTTP/1.1), assume + // the client is not broken and didn't send a body + inputBuffer.addActiveFilter(inputFilters[Constants.VOID_FILTER]); + contentDelimitation = true; + } + } + + + private void badRequest(String errorKey) { + response.setStatus(400); + setErrorState(ErrorState.CLOSE_CLEAN, null); + if (log.isDebugEnabled()) { + log.debug(sm.getString(errorKey)); + } + } + + + /** + * When committing the response, we have to validate the set of headers, as well as setup the response filters. + */ + @Override + protected final void prepareResponse() throws IOException { + + boolean entityBody = true; + contentDelimitation = false; + + OutputFilter[] outputFilters = outputBuffer.getFilters(); + + if (http09 == true) { + // HTTP/0.9 + outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); + outputBuffer.commit(); + return; + } + + int statusCode = response.getStatus(); + if (statusCode < 200 || statusCode == 204 || statusCode == 205 || statusCode == 304) { + // No entity body + outputBuffer.addActiveFilter(outputFilters[Constants.VOID_FILTER]); + entityBody = false; + contentDelimitation = true; + if (statusCode == 205) { + // RFC 7231 requires the server to explicitly signal an empty + // response in this case + response.setContentLength(0); + } else { + response.setContentLength(-1); + } + } + + if (request.method().equals("HEAD")) { + // No entity body + outputBuffer.addActiveFilter(outputFilters[Constants.VOID_FILTER]); + contentDelimitation = true; + } + + // Sendfile support + if (protocol.getUseSendfile()) { + prepareSendfile(outputFilters); + } + + // Check for compression + boolean useCompression = false; + if (entityBody && sendfileData == null) { + useCompression = protocol.useCompression(request, response); + } + + MimeHeaders headers = response.getMimeHeaders(); + // A SC_NO_CONTENT response may include entity headers + if (entityBody || statusCode == HttpServletResponse.SC_NO_CONTENT) { + String contentType = response.getContentType(); + if (contentType != null) { + headers.setValue("Content-Type").setString(contentType); + } + String contentLanguage = response.getContentLanguage(); + if (contentLanguage != null) { + headers.setValue("Content-Language").setString(contentLanguage); + } + } + + long contentLength = response.getContentLengthLong(); + boolean connectionClosePresent = isConnectionToken(headers, Constants.CLOSE); + if (http11 && response.getTrailerFields() != null) { + // If trailer fields are set, always use chunking + outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]); + contentDelimitation = true; + headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); + } else if (contentLength != -1) { + headers.setValue("Content-Length").setLong(contentLength); + outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); + contentDelimitation = true; + } else { + // If the response code supports an entity body and we're on + // HTTP 1.1 then we chunk unless we have a Connection: close header + if (http11 && entityBody && !connectionClosePresent) { + outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]); + contentDelimitation = true; + headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); + } else { + outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); + } + } + + if (useCompression) { + outputBuffer.addActiveFilter(outputFilters[Constants.GZIP_FILTER]); + } + + // Add date header unless application has already set one (e.g. in a + // Caching Filter) + if (headers.getValue("Date") == null) { + headers.addValue("Date").setString(FastHttpDateFormat.getCurrentDate()); + } + + // Although using transfer-encoding for gzip would be doable and was + // the original intent (which means the compression would be from an + // endpoint to the next, so only for the current transmission), it + // has been found that using content-encoding (which is end to end + // compression) is more efficient and more reliable. + + if ((entityBody) && (!contentDelimitation) || connectionClosePresent) { + // Disable keep-alive if: + // - there is a response body but way for the client to determine + // the content length information; or + // - there is a "connection: close" header present + // This will cause the "connection: close" header to be added if it + // is not already present. + keepAlive = false; + } + + // This may disable keep-alive so check before working out the Connection header + checkExpectationAndResponseStatus(); + + // This may disable keep-alive if there is more body to swallow + // than the configuration allows + checkMaxSwallowSize(); + + // If we know that the request is bad this early, add the + // Connection: close header. + if (keepAlive && statusDropsConnection(statusCode)) { + keepAlive = false; + } + if (!keepAlive) { + // Avoid adding the close header twice + if (!connectionClosePresent) { + headers.addValue(Constants.CONNECTION).setString(Constants.CLOSE); + } + } else if (!getErrorState().isError()) { + if (!http11) { + headers.addValue(Constants.CONNECTION).setString(Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN); + } + + if (protocol.getUseKeepAliveResponseHeader()) { + boolean connectionKeepAlivePresent = isConnectionToken(request.getMimeHeaders(), + Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN); + + if (connectionKeepAlivePresent) { + int keepAliveTimeout = protocol.getKeepAliveTimeout(); + + if (keepAliveTimeout > 0) { + String value = "timeout=" + keepAliveTimeout / 1000L; + headers.setValue(Constants.KEEP_ALIVE_HEADER_NAME).setString(value); + + if (http11) { + // Append if there is already a Connection header, + // else create the header + MessageBytes connectionHeaderValue = headers.getValue(Constants.CONNECTION); + if (connectionHeaderValue == null) { + headers.addValue(Constants.CONNECTION) + .setString(Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN); + } else { + connectionHeaderValue.setString(connectionHeaderValue.getString() + ", " + + Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN); + } + } + } + } + } + } + + // Add server header + String server = protocol.getServer(); + if (server == null) { + if (protocol.getServerRemoveAppProvidedValues()) { + headers.removeHeader("server"); + } + } else { + // server always overrides anything the app might set + headers.setValue("Server").setString(server); + } + + // Build the response header + try { + outputBuffer.sendStatus(); + + int size = headers.size(); + for (int i = 0; i < size; i++) { + try { + outputBuffer.sendHeader(headers.getName(i), headers.getValue(i)); + } catch (IllegalArgumentException iae) { + // Log the problematic header + log.warn(sm.getString("http11processor.response.invalidHeader", headers.getName(i), + headers.getValue(i)), iae); + // Remove the problematic header + headers.removeHeader(i); + size--; + // Header buffer is corrupted. Reset it and start again. + outputBuffer.resetHeaderBuffer(); + i = 0; + outputBuffer.sendStatus(); + } + } + outputBuffer.endHeaders(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // If something goes wrong, reset the header buffer so the error + // response can be written instead. + outputBuffer.resetHeaderBuffer(); + throw t; + } + + outputBuffer.commit(); + } + + private static boolean isConnectionToken(MimeHeaders headers, String token) throws IOException { + MessageBytes connection = headers.getValue(Constants.CONNECTION); + if (connection == null) { + return false; + } + + Set tokens = new HashSet<>(); + TokenList.parseTokenList(headers.values(Constants.CONNECTION), tokens); + return tokens.contains(token); + } + + + private void prepareSendfile(OutputFilter[] outputFilters) { + String fileName = (String) request.getAttribute(org.apache.coyote.Constants.SENDFILE_FILENAME_ATTR); + if (fileName == null) { + sendfileData = null; + } else { + // No entity body sent here + outputBuffer.addActiveFilter(outputFilters[Constants.VOID_FILTER]); + contentDelimitation = true; + long pos = ((Long) request.getAttribute(org.apache.coyote.Constants.SENDFILE_FILE_START_ATTR)).longValue(); + long end = ((Long) request.getAttribute(org.apache.coyote.Constants.SENDFILE_FILE_END_ATTR)).longValue(); + sendfileData = socketWrapper.createSendfileData(fileName, pos, end - pos); + } + } + + + /* + * Note: populateHost() is not over-ridden. request.serverName() will be set to return the default host name by the + * Mapper. + */ + + + /** + * {@inheritDoc} + *

    + * This implementation provides the server port from the local port. + */ + @Override + protected void populatePort() { + // Ensure the local port field is populated before using it. + request.action(ActionCode.REQ_LOCALPORT_ATTRIBUTE, request); + request.setServerPort(request.getLocalPort()); + } + + + @Override + protected boolean flushBufferedWrite() throws IOException { + if (outputBuffer.hasDataToWrite()) { + if (outputBuffer.flushBuffer(false)) { + // The buffer wasn't fully flushed so re-register the + // socket for write. Note this does not go via the + // Response since the write registration state at + // that level should remain unchanged. Once the buffer + // has been emptied then the code below will call + // Adaptor.asyncDispatch() which will enable the + // Response to respond to this event. + outputBuffer.registerWriteInterest(); + return true; + } + } + return false; + } + + + @Override + protected SocketState dispatchEndRequest() { + if (!keepAlive || protocol.isPaused()) { + return SocketState.CLOSED; + } else { + endRequest(); + inputBuffer.nextRequest(); + outputBuffer.nextRequest(); + if (socketWrapper.isReadPending()) { + return SocketState.LONG; + } else { + return SocketState.OPEN; + } + } + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + protected ServletConnection getServletConnection() { + return socketWrapper.getServletConnection("http/1.1", ""); + } + + + /* + * No more input will be passed to the application. Remaining input will be swallowed or the connection dropped + * depending on the error and expectation status. + */ + private void endRequest() { + if (getErrorState().isError()) { + // If we know we are closing the connection, don't drain + // input. This way uploading a 100GB file doesn't tie up the + // thread if the servlet has rejected it. + inputBuffer.setSwallowInput(false); + } else { + // Need to check this again here in case the response was + // committed before the error that requires the connection + // to be closed occurred. + checkExpectationAndResponseStatus(); + } + + // Finish the handling of the request + if (getErrorState().isIoAllowed()) { + try { + inputBuffer.endRequest(); + } catch (IOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // 500 - Internal Server Error + // Can't add a 500 to the access log since that has already been + // written in the Adapter.service method. + response.setStatus(500); + setErrorState(ErrorState.CLOSE_NOW, t); + log.error(sm.getString("http11processor.request.finish"), t); + } + } + if (getErrorState().isIoAllowed()) { + try { + action(ActionCode.COMMIT, null); + outputBuffer.end(); + } catch (IOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + setErrorState(ErrorState.CLOSE_NOW, t); + log.error(sm.getString("http11processor.response.finish"), t); + } + } + } + + + @Override + protected final void finishResponse() throws IOException { + outputBuffer.end(); + } + + + @Override + protected final void ack(ContinueResponseTiming continueResponseTiming) { + // Only try and send the ACK for ALWAYS or if the timing of the request + // to send the ACK matches the current configuration. + if (continueResponseTiming == ContinueResponseTiming.ALWAYS || + continueResponseTiming == protocol.getContinueResponseTimingInternal()) { + // Acknowledge request + // Send a 100 status back if it makes sense (response not committed + // yet, and client specified an expectation for 100-continue) + if (!response.isCommitted() && request.hasExpectation()) { + try { + outputBuffer.sendAck(); + } catch (IOException e) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); + } + } + } + } + + + @Override + protected final void flush() throws IOException { + outputBuffer.flush(); + } + + + @Override + protected final int available(boolean doRead) { + return inputBuffer.available(doRead); + } + + + @Override + protected final void setRequestBody(ByteChunk body) { + InputFilter savedBody = new SavedRequestInputFilter(body); + Http11InputBuffer internalBuffer = (Http11InputBuffer) request.getInputBuffer(); + internalBuffer.addActiveFilter(savedBody); + } + + + @Override + protected final void setSwallowResponse() { + outputBuffer.responseFinished = true; + } + + + @Override + protected final void disableSwallowRequest() { + inputBuffer.setSwallowInput(false); + } + + + @Override + protected final void sslReHandShake() throws IOException { + if (sslSupport != null) { + // Consume and buffer the request body, so that it does not + // interfere with the client's handshake messages + InputFilter[] inputFilters = inputBuffer.getFilters(); + ((BufferedInputFilter) inputFilters[Constants.BUFFERED_FILTER]).setLimit(protocol.getMaxSavePostSize()); + inputBuffer.addActiveFilter(inputFilters[Constants.BUFFERED_FILTER]); + + /* + * Outside the try/catch because we want I/O errors during renegotiation to be thrown for the caller to + * handle since they will be fatal to the connection. + */ + socketWrapper.doClientAuth(sslSupport); + try { + /* + * Errors processing the cert chain do not affect the client connection so they can be logged and + * swallowed here. + */ + Object sslO = sslSupport.getPeerCertificateChain(); + if (sslO != null) { + request.setAttribute(SSLSupport.CERTIFICATE_KEY, sslO); + } + } catch (IOException ioe) { + log.warn(sm.getString("http11processor.socket.ssl"), ioe); + } + } + } + + + @Override + protected final boolean isRequestBodyFullyRead() { + return inputBuffer.isFinished(); + } + + + @Override + protected final void registerReadInterest() { + socketWrapper.registerReadInterest(); + } + + + @Override + protected final boolean isReadyForWrite() { + return outputBuffer.isReady(); + } + + + @Override + public UpgradeToken getUpgradeToken() { + return upgradeToken; + } + + + @Override + protected final void doHttpUpgrade(UpgradeToken upgradeToken) { + this.upgradeToken = upgradeToken; + // Stop further HTTP output + outputBuffer.responseFinished = true; + } + + + @Override + public ByteBuffer getLeftoverInput() { + return inputBuffer.getLeftover(); + } + + + @Override + public boolean isUpgrade() { + return upgradeToken != null; + } + + + @Override + protected boolean isTrailerFieldsReady() { + if (inputBuffer.isChunking()) { + return inputBuffer.isFinished(); + } else { + return true; + } + } + + + @Override + protected boolean isTrailerFieldsSupported() { + // Request must be HTTP/1.1 to support trailer fields + if (!http11) { + return false; + } + + // If the response is not yet committed, chunked encoding can be used + // and the trailer fields sent + if (!response.isCommitted()) { + return true; + } + + // Response has been committed - need to see if chunked is being used + return outputBuffer.isChunking(); + } + + + /** + * Trigger sendfile processing if required. + * + * @return The state of send file processing + */ + private SendfileState processSendfile(SocketWrapperBase socketWrapper) { + openSocket = keepAlive; + // Done is equivalent to sendfile not being used + SendfileState result = SendfileState.DONE; + // Do sendfile as needed: add socket to sendfile and end + if (sendfileData != null && !getErrorState().isError()) { + if (keepAlive) { + if (available(false) == 0) { + sendfileData.keepAliveState = SendfileKeepAliveState.OPEN; + } else { + sendfileData.keepAliveState = SendfileKeepAliveState.PIPELINED; + } + } else { + sendfileData.keepAliveState = SendfileKeepAliveState.NONE; + } + result = socketWrapper.processSendfile(sendfileData); + switch (result) { + case ERROR: + // Write failed + if (log.isDebugEnabled()) { + log.debug(sm.getString("http11processor.sendfile.error")); + } + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, null); + //$FALL-THROUGH$ + default: + sendfileData = null; + } + } + return result; + } + + + @Override + public final void recycle() { + getAdapter().checkRecycled(request, response); + super.recycle(); + inputBuffer.recycle(); + outputBuffer.recycle(); + upgradeToken = null; + socketWrapper = null; + sendfileData = null; + sslSupport = null; + } + + + @Override + public void pause() { + // NOOP for HTTP + } +} diff --git a/java/org/apache/coyote/http11/HttpOutputBuffer.java b/java/org/apache/coyote/http11/HttpOutputBuffer.java new file mode 100644 index 0000000..1a66ad7 --- /dev/null +++ b/java/org/apache/coyote/http11/HttpOutputBuffer.java @@ -0,0 +1,39 @@ +/* + * 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.coyote.http11; + +import java.io.IOException; + +import org.apache.coyote.OutputBuffer; + +public interface HttpOutputBuffer extends OutputBuffer { + + /** + * Finish writing the current response. It is acceptable to write extra bytes using + * {@link #doWrite(java.nio.ByteBuffer)} during the execution of this method. + * + * @throws IOException If an I/O error occurs while writing to the client + */ + void end() throws IOException; + + /** + * Flushes any unwritten data to the client. + * + * @throws IOException If an I/O error occurs while flushing + */ + void flush() throws IOException; +} diff --git a/java/org/apache/coyote/http11/InputFilter.java b/java/org/apache/coyote/http11/InputFilter.java new file mode 100644 index 0000000..a68ee7a --- /dev/null +++ b/java/org/apache/coyote/http11/InputFilter.java @@ -0,0 +1,81 @@ +/* + * 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.coyote.http11; + +import java.io.IOException; + +import org.apache.coyote.InputBuffer; +import org.apache.coyote.Request; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Input filter interface. + * + * @author Remy Maucherat + */ +public interface InputFilter extends InputBuffer { + + /** + * Some filters need additional parameters from the request. + * + * @param request The request to be associated with this filter + */ + void setRequest(Request request); + + + /** + * Make the filter ready to process the next request. + */ + void recycle(); + + + /** + * Get the name of the encoding handled by this filter. + * + * @return The encoding name as a byte chunk to facilitate comparison with the value read from the HTTP headers + * which will also be a ByteChunk + */ + ByteChunk getEncodingName(); + + + /** + * Set the next buffer in the filter pipeline. + * + * @param buffer The next buffer + */ + void setBuffer(InputBuffer buffer); + + + /** + * End the current request. + * + * @return 0 is the expected return value. A positive value indicates that too many bytes were read. This method is + * allowed to use buffer.doRead to consume extra bytes. The result of this method can't be negative (if + * an error happens, an IOException should be thrown instead). + * + * @throws IOException If an error happens + */ + long end() throws IOException; + + + /** + * Has the request body been read fully? + * + * @return {@code true} if the request body has been fully read, otherwise {@code false} + */ + boolean isFinished(); +} diff --git a/java/org/apache/coyote/http11/LocalStrings.properties b/java/org/apache/coyote/http11/LocalStrings.properties new file mode 100644 index 0000000..9e4d2ec --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings.properties @@ -0,0 +1,58 @@ +# 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. + +abstractHttp11Protocol.alpnConfigured=The [{0}] connector has been configured to support negotiation to [{1}] via ALPN +abstractHttp11Protocol.alpnWithNoAlpn=The upgrade handler [{0}] for [{1}] only supports upgrade via ALPN but has been configured for the [{2}] connector that does not support ALPN. +abstractHttp11Protocol.httpUpgradeConfigured=The [{0}] connector has been configured to support HTTP upgrade to [{1}] +abstractHttp11Protocol.upgradeJmxNameFail=Failed to create ObjectName with which to register upgrade protocol in JMX +abstractHttp11Protocol.upgradeJmxRegistrationFail=Failed to register upgrade protocol in JMX + +http11processor.fallToDebug=\n\ +\ Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level. +http11processor.header.parse=Error parsing HTTP request header +http11processor.request.finish=Error finishing request +http11processor.request.inconsistentHosts=The host specified in the request line is not consistent with the host header +http11processor.request.invalidScheme=The HTTP request contained an absolute URI with an invalid scheme +http11processor.request.invalidTransferEncoding=The HTTP request contained an invalid Transfer-Encoding header +http11processor.request.invalidUri=The HTTP request contained an invalid URI +http11processor.request.invalidUserInfo=The HTTP request contained an absolute URI with an invalid userinfo +http11processor.request.multipleContentLength=The request contained multiple content-length headers +http11processor.request.multipleHosts=The request contained multiple host headers +http11processor.request.noHostHeader=The HTTP/1.1 request did not provide a host header +http11processor.request.nonNumericContentLength=The request contained a content-length header with a non-numeric value +http11processor.request.prepare=Error preparing request +http11processor.request.process=Error processing request +http11processor.response.finish=Error finishing response +http11processor.response.invalidHeader=The HTTP response header [{0}] with value [{1}] has been removed from the response because it is invalid +http11processor.sendfile.error=Error sending data using sendfile. May be caused by invalid request attributes for start/end points +http11processor.socket.info=Exception getting socket information + +iib.available.readFail=A non-blocking read failed while attempting to determine if data was available +iib.eof.error=Unexpected EOF read on the socket +iib.failedread.apr=Read failed with APR/native error code [{0}] +iib.filter.npe=You may not add a null filter +iib.invalidHttpProtocol=Invalid character found in the HTTP protocol [{0}] +iib.invalidPhase=Invalid request line parse phase [{0}] +iib.invalidRequestTarget=Invalid character found in the request target [{0}]. The valid characters are defined in RFC 7230 and RFC 3986 +iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 7230 and has been ignored. +iib.invalidheader.reject=The HTTP header line [{0}] does not conform to RFC 7230. The request has been rejected. +iib.invalidmethod=Invalid character found in method name [{0}]. HTTP method names must be tokens +iib.parseheaders.ise.error=Unexpected state: headers already parsed. Buffer not recycled? +iib.readtimeout=Timeout attempting to read data from the socket +iib.requestheadertoolarge.error=Request header is too large + +iob.failedwrite=Failed write +iob.failedwrite.ack=Failed to send HTTP 100 continue response +iob.responseheadertoolarge.error=An attempt was made to write more data to the response headers than there was room available in the buffer. Increase maxHttpHeaderSize on the connector or write less data into the response headers. diff --git a/java/org/apache/coyote/http11/LocalStrings_cs.properties b/java/org/apache/coyote/http11/LocalStrings_cs.properties new file mode 100644 index 0000000..64271a7 --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_cs.properties @@ -0,0 +1,22 @@ +# 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. + +http11processor.fallToDebug=\n\ +\ Poznámka: další výskyty chyb z naÄítání HTTP dotazu budou zalogovány na úrovni DEBUG. +http11processor.request.prepare=Chyba pÅ™i přípravÄ› dotazu +http11processor.request.process=Chyba zpracování požadavku +http11processor.socket.info=Výjimka pÅ™i získávání informací o socketu + +iib.invalidmethod=Ve jménu metody [{0}] byl nalezen neplatný znak. Názvy HTTP metod musí být tokeny diff --git a/java/org/apache/coyote/http11/LocalStrings_de.properties b/java/org/apache/coyote/http11/LocalStrings_de.properties new file mode 100644 index 0000000..e308809 --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_de.properties @@ -0,0 +1,22 @@ +# 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. + +http11processor.request.prepare=Fehler beim Vorbereiten der Anfrage +http11processor.request.process=Fehler bei der Verarbeitung der Anfrage +http11processor.socket.info=Ausnahme beim Lesen der Informationen zum Socket + +iib.invalidmethod=Ungültiges Zeichen im Methodennamen [{0}] gefunden. HTTP Methodennamen müssen Token sein + +iob.failedwrite=Fehlgeschlagener Schreibvorgang diff --git a/java/org/apache/coyote/http11/LocalStrings_es.properties b/java/org/apache/coyote/http11/LocalStrings_es.properties new file mode 100644 index 0000000..9114044 --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_es.properties @@ -0,0 +1,30 @@ +# 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. + +http11processor.header.parse=Error analizando cabecera de requerimiento HTTP +http11processor.request.finish=Error acabando requerimiento +http11processor.request.invalidUri=El pedido HTTP contenía una URI inválida +http11processor.request.prepare=Error preparando solicitud +http11processor.request.process=Error procesando requerimiento +http11processor.response.finish=Error acabando respuesta +http11processor.socket.info=Excepción obteniendo información de conector + +iib.eof.error=Inesperado Fin De Archivo (EOF) leído en el enchufe (socket) +iib.invalidmethod=Se encontró un carácter inválido en el nombre del método. Los nombres de métodos HTTP deben ser tokens +iib.parseheaders.ise.error=Estado inesperado: las cabeceras ya fueron parseadas. No se ha reciclado el buffer?? +iib.requestheadertoolarge.error=La cabecera del requerimiento es demasido grande + +iob.failedwrite=Fallo de escritura +iob.responseheadertoolarge.error=Un intento para escribir más datos en las cabeceras de respuesta escribir fue hecho, sin embargo no hay más espacio disponible en el bufer. Aumente el parámetro maxHttpHeaderSize en el conector o escribe menos datos en las respuestas de las cabeceras.\n diff --git a/java/org/apache/coyote/http11/LocalStrings_fr.properties b/java/org/apache/coyote/http11/LocalStrings_fr.properties new file mode 100644 index 0000000..1c419a6 --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_fr.properties @@ -0,0 +1,58 @@ +# 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. + +abstractHttp11Protocol.alpnConfigured=Le connecteur [{0}] a été configuré pour supporter la négociation de [{1}] avec ALPN +abstractHttp11Protocol.alpnWithNoAlpn=Le gestionnaire de mise à niveau [{0}] pour [{1}] ne supporte qu''une mise à niveau via ALPN mais il a été configuré pour le connecteur [{2}] qui ne supporte pas ALPN +abstractHttp11Protocol.httpUpgradeConfigured=Le connecteur [{0}] a été configuré pour supporter la mise à niveau de HTTP vers [{1}] +abstractHttp11Protocol.upgradeJmxNameFail=Impossible de créer l'ObjectName pour l'enregistrement JMX du protocole d'upgrade +abstractHttp11Protocol.upgradeJmxRegistrationFail=Impossible d'enregistrer le protocole d'upgrade dans JMX + +http11processor.fallToDebug=\n\ +\ Note : les occurrences suivantes d'erreurs d'analyse de requête HTTP seront enregistrées au niveau DEBUG. +http11processor.header.parse=Erreur lors de l'analyse d'un en-tête de requête HTTP +http11processor.request.finish=Erreur en terminant la requête +http11processor.request.inconsistentHosts=L'hôte spécifié dans la ligne de requête ne correspond pas à celui de l'en-tête hôte +http11processor.request.invalidScheme=La requête HTTP contenait une URi absolue avec un schéma invalide +http11processor.request.invalidTransferEncoding=La requête HTTP contenait un en-tête Trasfer-Encoding invalide +http11processor.request.invalidUri=La requête HTTP contenait un URI non valide +http11processor.request.invalidUserInfo=La requête HTTP contenait un URI absolu avec un composant "userinfo" invalide +http11processor.request.multipleContentLength=La requête contenait plusieurs en-têtes content-length +http11processor.request.multipleHosts=La requête contenait plusieurs en-têtes hôtes +http11processor.request.noHostHeader=La requ6ete HTTP/1.1 ne contient pas d'en-tête host +http11processor.request.nonNumericContentLength=La requête contenait un en-tête content-length avec une valeur non numérique +http11processor.request.prepare=Echec de préparation de la requête +http11processor.request.process=Erreur de traitement de la requête +http11processor.response.finish=Erreur en finissant la réponse +http11processor.response.invalidHeader=L''en-tête de réponse HTTP [{0}] avec la valeur [{1}] a été enlevé de la réponse car il est invalide +http11processor.sendfile.error=Erreur d'envoi des données avec sendfile, cela peut être causé par des attributs de démarrage ou de fin incorrects dans la requête +http11processor.socket.info=Exception pendant la requête d'information sur le socket. + +iib.available.readFail=Une lecture non-bloquante a échoué lors de la détermination préalable de données disponibles +iib.eof.error=Fin de flux (EOF) inattendue à la lecture sur la socket +iib.failedread.apr=Echec de lecteur avec le code d''erreur APR [{0}] +iib.filter.npe=Impossible d'ajouter un filtre null +iib.invalidHttpProtocol=Un caractère invalide a été trouvé dans le protocole HTTP +iib.invalidPhase=Etape invalide de traitement [{0}] de la ligne de requête +iib.invalidRequestTarget=Un caractère invalide a été trouvé dans la cible de la requête, les caractères valides sont définis dans RFC 7230 et RFC 3986 +iib.invalidheader=La ligne d''en-être HTTP [{0}] n''est pas conforme à la RFC 7230 et a été ignorée +iib.invalidheader.reject=La ligne d''en-tête HTTP [{0}] ne respecte pas la RFC 7230. La requête a été rejetée. +iib.invalidmethod=Caractère invalide trouvé dans le nom de méthode. Les noms HTTP doivent être des "token". +iib.parseheaders.ise.error=Etat inattendu, les en-êtres ont déjà été traités, il est possible que le buffer n'ait pas été recyclé +iib.readtimeout=Délai d'attente dépassé en lisant des données du socket +iib.requestheadertoolarge.error=L'entête de requête est trop important + +iob.failedwrite=Echec d'écriture +iob.failedwrite.ack=Echec d'envoi de la réponse HTTP 100 Continue +iob.responseheadertoolarge.error=Essai d'écriture de plus de données dans les en-t^tes de réponse, qu'il n'y a de place disponible dans le tampon. Augmentez maxHttpHeaderSize pour le connecteur, ou écrivez moins de données dans les en-têtes de réponse. diff --git a/java/org/apache/coyote/http11/LocalStrings_ja.properties b/java/org/apache/coyote/http11/LocalStrings_ja.properties new file mode 100644 index 0000000..1b29ac7 --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_ja.properties @@ -0,0 +1,58 @@ +# 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. + +abstractHttp11Protocol.alpnConfigured=[{0}] コãƒã‚¯ã‚¿ã¯ã€ALPN経由㧠[{1}] ã¸ã®ãƒã‚´ã‚·ã‚¨ãƒ¼ã‚·ãƒ§ãƒ³ã‚’サãƒãƒ¼ãƒˆã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ +abstractHttp11Protocol.alpnWithNoAlpn=[{1}] ã®ã‚¢ãƒƒãƒ—グレードãƒãƒ³ãƒ‰ãƒ© [{0}] ã¯ã€ALPNã«ã‚ˆã‚‹ã‚¢ãƒƒãƒ—グレードã®ã¿ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã™ãŒã€ALPNをサãƒãƒ¼ãƒˆã—ã¦ã„ãªã„ [{2}] コãƒã‚¯ã‚¿ç”¨ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ã€‚ +abstractHttp11Protocol.httpUpgradeConfigured=コãƒã‚¯ã‚¿ [{0}] ã¯ã€[{1}] ã¸ã®HTTPアップグレードをサãƒãƒ¼ãƒˆã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ +abstractHttp11Protocol.upgradeJmxNameFail=JMXã«ã‚¢ãƒƒãƒ—グレードプロトコルを登録ã™ã‚‹ãŸã‚ã®ObjectNameã®ä½œæˆã«å¤±æ•—ã—ã¾ã—㟠+abstractHttp11Protocol.upgradeJmxRegistrationFail=JMXã«ã‚¢ãƒƒãƒ—グレードプロトコルを登録ã§ãã¾ã›ã‚“ã§ã—㟠+ +http11processor.fallToDebug=\n\ +\ 注: 以é™ã®HTTPリクエスト構文解æžã‚¨ãƒ©ãƒ¼ã®ç™ºç”Ÿã¯DEBUGレベルã§ãƒ­ã‚°ã«å‡ºåŠ›ã•ã‚Œã¾ã™ã€‚ +http11processor.header.parse=HTTP リクエストヘッダーを解æžä¸­ã®ã‚¨ãƒ©ãƒ¼ +http11processor.request.finish=リクエスト終了処ç†ã‚¨ãƒ©ãƒ¼ +http11processor.request.inconsistentHosts=リクエスト行ã«æŒ‡å®šã•ã‚ŒãŸãƒ›ã‚¹ãƒˆãŒ Host ヘッダーã®å€¤ã¨çŸ›ç›¾ã—ã¦ã„ã¾ã™ã€‚ +http11processor.request.invalidScheme=HTTP リクエストã«ç„¡åŠ¹ãªã‚¹ã‚­ãƒ¼ãƒžã‚’指定ã—ãŸå®Œå…¨ URI ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +http11processor.request.invalidTransferEncoding=HTTP リクエストã«ç„¡åŠ¹ãª Transfer-Encoding ヘッダãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +http11processor.request.invalidUri=HTTPリクエストã«ç„¡åŠ¹ãªURIãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +http11processor.request.invalidUserInfo=HTTP リクエストã«ä¸æ­£ãª userinfo ã‚’å«ã‚€çµ¶å¯¾ URI ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +http11processor.request.multipleContentLength=リクエストã«è¤‡æ•°ã® content-length ヘッダãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +http11processor.request.multipleHosts=リクエストã«ã¯è¤‡æ•°ã® host ヘッダーãŒå«ã¾ã‚Œã¦ã„ã¾ã—ãŸã€‚ +http11processor.request.noHostHeader=HTTP/1.1 リクエスト㧠host ヘッダーãŒæä¾›ã•ã‚Œã¾ã›ã‚“ã§ã—㟠+http11processor.request.nonNumericContentLength=リクエスト㮠content-length ヘッダã«æ•°å€¤ã§ãªã„値ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +http11processor.request.prepare=リクエスト準備中ã®ã‚¨ãƒ©ãƒ¼ +http11processor.request.process=リクエスト処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +http11processor.response.finish=レスãƒãƒ³ã‚¹çµ‚了処ç†ã®ã‚¨ãƒ©ãƒ¼ +http11processor.response.invalidHeader=値 [{1}] ã‚’æŒã¤HTTPレスãƒãƒ³ã‚¹ãƒ˜ãƒƒãƒ€ [{0}] ã¯ã€ç„¡åŠ¹ã§ã‚ã‚‹ãŸã‚レスãƒãƒ³ã‚¹ã‹ã‚‰å‰Šé™¤ã•ã‚Œã¾ã—㟠+http11processor.sendfile.error=sendfileを使ã£ã¦ãƒ‡ãƒ¼ã‚¿ã‚’é€ä¿¡ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã“ã‚Œã¯é–‹å§‹ç‚¹ã¾ãŸã¯çµ‚了点ã®ç„¡åŠ¹ãªãƒªã‚¯ã‚¨ã‚¹ãƒˆå±žæ€§ã«ã‚ˆã£ã¦å¼•ãèµ·ã“ã•ã‚Œã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +http11processor.socket.info=ソケット情報をå–å¾—ã™ã‚‹éš›ã®ä¾‹å¤– + +iib.available.readFail=利用ã§ãるデータãŒã‚ã‚‹ã‹ç¢ºã‹ã‚ã¦ã„る途中ã§ãƒŽãƒ³ãƒ–ロッキング読ã¿è¾¼ã¿ãŒå¤±æ•—ã—ã¾ã—㟠+iib.eof.error=ソケットã‹ã‚‰äºˆæœŸã—ãªã„EOFを読ã¿è¾¼ã¿ã¾ã—㟠+iib.failedread.apr=APR/nativeエラーコード [{0}] ã§èª­ã¿å–ã‚ŠãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ +iib.filter.npe=Nullフィルタを追加ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +iib.invalidHttpProtocol=HTTPプロトコルã§ç„¡åŠ¹ãªæ–‡å­—ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚ +iib.invalidPhase=リクエスト行ã®è§£æžãƒ•ã‚§ãƒ¼ã‚º [{0}] ã¯ç„¡åŠ¹ã§ã™ +iib.invalidRequestTarget=リクエストã®å®›å…ˆ [{0}] ã«ç„¡åŠ¹ãªæ–‡å­—ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚利用å¯èƒ½ãªæ–‡å­—㯠RFC 7230 ãŠã‚ˆã³ RFC 3986 ã«å®šç¾©ã•ã‚Œã¦ã„ã¾ã™ã€‚ +iib.invalidheader=HTTP ヘッダー行 [{0}]㯠RFC 7230 ã«é©åˆã—ãªã„ãŸã‚無視ã—ã¾ã™ã€‚ +iib.invalidheader.reject=HTTP ヘッダー㮠[{0}] 行目㯠RFC 7230 ã«æº–æ‹ ã—ã¦ã„ã¾ã›ã‚“。リクエストã¯æ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ +iib.invalidmethod=HTTP メソッドå [{0}] ã«ç„¡åŠ¹ãªæ–‡å­—ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚HTTP メソッドåã¯æ±ºã‚られãŸãƒˆãƒ¼ã‚¯ãƒ³ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“ +iib.parseheaders.ise.error=予期ã—ãªã„状態:ヘッダãŒã™ã§ã«è§£æžã•ã‚Œã¦ã„ã¾ã™ã€‚ãƒãƒƒãƒ•ã‚¡ãŒæœªå›žåŽã§ã™ã‹ï¼Ÿ +iib.readtimeout=ソケットã‹ã‚‰ãƒ‡ãƒ¼ã‚¿ã‚’読ã¿å–ã‚ã†ã¨ã—ã¦ã„ã‚‹éš›ã®ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆ +iib.requestheadertoolarge.error=リクエストヘッダãŒé•·ã™ãŽã¾ã™ + +iob.failedwrite=書ãè¾¼ã¿ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ +iob.failedwrite.ack=HTTP 100 continue レスãƒãƒ³ã‚¹ã®é€ä¿¡ã«å¤±æ•—ã—ã¾ã—㟠+iob.responseheadertoolarge.error=レスãƒãƒ³ã‚¹ãƒ˜ãƒƒãƒ€ã«ãƒãƒƒãƒ•ã‚¡ãƒ¼é ˜åŸŸã‚ˆã‚Šå¤§ããªãƒ‡ãƒ¼ã‚¿ã®æ›¸ãè¾¼ã¿ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚Connector ã® maxHttpHeaderSize を増やã™ã‹ã€ãƒ¬ã‚¹ãƒãƒ³ã‚¹ãƒ˜ãƒƒãƒ€ã‚ˆã‚Šã‚‚データをå°ã•ãã—ã¦ä¸‹ã•ã„。 diff --git a/java/org/apache/coyote/http11/LocalStrings_ko.properties b/java/org/apache/coyote/http11/LocalStrings_ko.properties new file mode 100644 index 0000000..aa601d7 --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_ko.properties @@ -0,0 +1,57 @@ +# 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. + +abstractHttp11Protocol.alpnConfigured=[{0}] connector는 ALPNì„ í†µí•´ [{1}](으)ë¡œ negotiationì„ ì§€ì›í•˜ë„ë¡ ì„¤ì •ë˜ì—ˆìŠµë‹ˆë‹¤. +abstractHttp11Protocol.alpnWithNoAlpn=[{1}]ì„(를) 위한 업그레ì´ë“œ 핸들러 [{0}]ì€(는) ì˜¤ì§ ALPNì„ í†µí•œ 업그레ì´ë“œë§Œ 지ì›í•©ë‹ˆë‹¤ë§Œ, 해당 업그레ì´ë“œ 핸들러가 ALPNì„ ì§€ì›í•˜ì§€ 않는 [{2}] Connector를 위해 설정ë˜ì–´ 있습니다. +abstractHttp11Protocol.httpUpgradeConfigured=[{0}] connectorê°€, [{1}](으)ë¡œ HTTP 업그레ì´ë“œë¥¼ 지ì›í•˜ë„ë¡ ì„¤ì •ë˜ì–´ 있습니다. +abstractHttp11Protocol.upgradeJmxNameFail=업그레ì´ë“œ í”„ë¡œí† ì½œì„ JMXì— ë“±ë¡í•˜ê¸° 위해 사용할 ObjectNameì„ ìƒì„±í•˜ì§€ 못했습니다. +abstractHttp11Protocol.upgradeJmxRegistrationFail=업그레ì´ë“œ í”„ë¡œí† ì½œì„ JMXì— ë“±ë¡í•˜ì§€ 못했습니다. + +http11processor.fallToDebug=\n\ +\ 비고: HTTP 요청 파싱 ì˜¤ë¥˜ë“¤ì´ ë” ë°œìƒí•˜ëŠ” 경우 DEBUG 레벨 로그로 기ë¡ë  것입니다. +http11processor.header.parse=HTTP 요청 í—¤ë”를 파싱하는 중 오류 ë°œìƒ +http11processor.request.finish=ìš”ì²­ì„ ì™„ë£Œí•˜ëŠ” 중 오류 ë°œìƒ +http11processor.request.inconsistentHosts=요청 í–‰(request line)ì— ì§€ì •ëœ í˜¸ìŠ¤íŠ¸ê°€, 호스트 í—¤ë”와 ì¼ê´€ë˜ì§€ 않습니다. +http11processor.request.invalidScheme=HTTP ìš”ì²­ì´ ìœ íš¨í•˜ì§€ ì•Šì€ ìŠ¤í‚´ì„ ê°€ì§„ 절대 URI를 í¬í•¨í–ˆìŠµë‹ˆë‹¤. +http11processor.request.invalidTransferEncoding=HTTP ìš”ì²­ì´ ìœ íš¨í•˜ì§€ ì•Šì€ Transfer-Encoding header를 í¬í•¨í•©ë‹ˆë‹¤ +http11processor.request.invalidUri==HTTP ìš”ì²­ì´ ìœ íš¨í•˜ì§€ ì•Šì€ URI를 í¬í•¨í–ˆìŠµë‹ˆë‹¤. +http11processor.request.invalidUserInfo=HTTP 요청ì´, 유효하지 ì•Šì€ userinfo를 가진 절대 URI를 í¬í•¨í–ˆìŠµë‹ˆë‹¤. +http11processor.request.multipleContentLength=해당 ìš”ì²­ì´ ë³µìˆ˜ ê°œì˜ Content-Length í—¤ë”ë“¤ì„ í¬í•¨í–ˆìŠµë‹ˆë‹¤. +http11processor.request.multipleHosts=ìš”ì²­ì´ ì—¬ëŸ¬ ê°œì˜ í˜¸ìŠ¤íŠ¸ í—¤ë”ë“¤ì„ í¬í•¨í–ˆìŠµë‹ˆë‹¤. +http11processor.request.noHostHeader=HTTP/1.1 ìš”ì²­ì´ í˜¸ìŠ¤íŠ¸ í—¤ë”를 제공하지 않았습니다. +http11processor.request.nonNumericContentLength=해당 ìš”ì²­ì´ ìˆ«ìžê°€ ì•„ë‹Œ Content-Length í—¤ë” ê°’ì„ í¬í•¨í–ˆìŠµë‹ˆë‹¤. +http11processor.request.prepare=ìš”ì²­ì„ ì¤€ë¹„í•˜ëŠ” 중 오류 ë°œìƒ +http11processor.request.process=요청 처리 중 오류 ë°œìƒ +http11processor.response.finish=ì‘ë‹µì„ ì™„ë£Œí•˜ëŠ” 중 오류 ë°œìƒ +http11processor.response.invalidHeader=ê°’ì´ [{1}]ì¸ HTTP ì‘답 í—¤ë” [{0}](ì´)ê°€ 유효하지 ì•Šì€ ê°’ì´ë¯€ë¡œ ì‘답ì—ì„œ 제거ë˜ì—ˆìŠµë‹ˆë‹¤. +http11processor.sendfile.error=sendfileì„ ì‚¬ìš©í•˜ì—¬ ë°ì´í„°ë¥¼ 보내는 중 오류 ë°œìƒ. 시작 지ì ê³¼ 종료 지ì ì„ 위한 요청 ì†ì„±ë“¤ì´ 유효하지 ì•Šì•„ ë°œìƒí–ˆì„ 수 있습니다. +http11processor.socket.info=ì†Œì¼“ì— ëŒ€í•œ 정보를 얻는 중 예외 ë°œìƒ + +iib.available.readFail=ë°ì´í„°ê°€ 가용한지 결정하려 ì‹œë„하는 ë™ì•ˆ, non-blocking ì½ê¸°ê°€ 실패했습니다. +iib.eof.error=소켓ì—ì„œ 예기치 ì•Šì€ EOF를 ì½ì—ˆìŠµë‹ˆë‹¤. +iib.failedread.apr=APR/native 오류 코드 [{0}]와(ê³¼) 함께, ì½ê¸°ê°€ 실패했습니다. +iib.filter.npe=ë„ì¸ í•„í„°ë¥¼ 추가할 수 없습니다. +iib.invalidHttpProtocol=HTTP 프로토콜ì—ì„œ 유효하지 ì•Šì€ ë¬¸ìžê°€ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. +iib.invalidPhase=파싱 êµ­ë©´ [{0}]ì—ì„œ, 유효하지 ì•Šì€ HTTP 요청 ë¼ì¸ 오류 +iib.invalidRequestTarget=요청 타겟ì—ì„œ 유효하지 ì•Šì€ ë¬¸ìžê°€ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. 유효한 문ìžë“¤ì€ RFC 7230ê³¼ RFC 3986ì— ì •ì˜ë˜ì–´ 있습니다. +iib.invalidheader=HTTP í—¤ë” í–‰ [{0}]ì´(ê°€) RFC 7230ì„ ì¤€ìˆ˜í•˜ì§€ ì•Šì•„, 무시ë˜ì—ˆìŠµë‹ˆë‹¤. +iib.invalidmethod=메소드 ì´ë¦„ì— ìœ íš¨í•˜ì§€ ì•Šì€ ë¬¸ìžê°€ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. HTTP 메소드 ì´ë¦„ì€ ìœ íš¨í•œ 토í°ì´ì–´ì•¼ 합니다. +iib.parseheaders.ise.error=예기치 ì•Šì€ ìƒíƒœ: í—¤ë”ë“¤ì´ ì´ë¯¸ 파싱ë˜ì—ˆìŠµë‹ˆë‹¤. 버í¼ê°€ 참조 í•´ì œë˜ì§€ 않았었나요? +iib.readtimeout=소켓으로부터 ë°ì´í„°ë¥¼ ì½ìœ¼ë ¤ ì‹œë„하는 중 제한 시간 초과 +iib.requestheadertoolarge.error=요청 í—¤ë”ê°€ 너무 í½ë‹ˆë‹¤. + +iob.failedwrite=쓰기 실패 +iob.failedwrite.ack=HTTP 100 continue ì‘ë‹µì„ ë³´ë‚´ì§€ 못했습니다. +iob.responseheadertoolarge.error=ì‘답 í—¤ë”ë“¤ì— ê°€ìš©í•œ ë²„í¼ ê³µê°„ì„ ì´ˆê³¼í•˜ëŠ” ë°ì´í„°ë¥¼ 쓰려는 ì‹œë„ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. 해당 Connectorì˜ maxHttpHeaderSize를 ì¦ê°€ì‹œí‚¤ê±°ë‚˜, ì‘답 í—¤ë”ë“¤ì— ë³´ë‹¤ ì ì€ ë°ì´í„°ë¥¼ ì“°ë„ë¡ í•˜ì‹­ì‹œì˜¤. diff --git a/java/org/apache/coyote/http11/LocalStrings_pt_BR.properties b/java/org/apache/coyote/http11/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..fe920b3 --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_pt_BR.properties @@ -0,0 +1,19 @@ +# 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. + +http11processor.request.process=Erro ao processar requisição +http11processor.socket.info=Exceção ao coletar informação do socket + +iib.invalidmethod=Encontrado caracter inválido no nome do método. Nomes dos métodos HTTP precisam ser tokens diff --git a/java/org/apache/coyote/http11/LocalStrings_ru.properties b/java/org/apache/coyote/http11/LocalStrings_ru.properties new file mode 100644 index 0000000..afb79f4 --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_ru.properties @@ -0,0 +1,22 @@ +# 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. + +http11processor.request.invalidUri=HTTP Ð·Ð°Ð¿Ñ€Ð¾Ñ Ñодержал некорректный URI +http11processor.request.prepare=Ошибка при подготовке запроÑа +http11processor.request.process=Ошибка при обработке запроÑа +http11processor.socket.info=Ошибка при получении информации о Ñокете + +iob.failedwrite=Ошибка запиÑи +iob.responseheadertoolarge.error=Была Ñовершена попытка запиÑи большего количеÑтва данных в заголовки ответа чем имелоÑÑŒ меÑта в буфере. Увеличьте maxHttpHeaderSize Ð´Ð»Ñ ÐºÐ¾Ð½Ð½ÐµÐºÑ‚Ð¾Ñ€Ð° или запиÑывайте меньше данных в заголовки ответа. diff --git a/java/org/apache/coyote/http11/LocalStrings_zh_CN.properties b/java/org/apache/coyote/http11/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..176a16f --- /dev/null +++ b/java/org/apache/coyote/http11/LocalStrings_zh_CN.properties @@ -0,0 +1,57 @@ +# 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. + +abstractHttp11Protocol.alpnConfigured=[{0}]连接器已é…置为支æŒé€šè¿‡ALPN与[{1}]å商。 +abstractHttp11Protocol.alpnWithNoAlpn=[{1}]çš„å‡çº§å¤„ç†ç¨‹åº[{0}]仅支æŒé€šè¿‡ALPNå‡çº§ï¼Œä½†å·²ä¸ºä¸æ”¯æŒALPNçš„[{2}]连接器é…ç½® +abstractHttp11Protocol.httpUpgradeConfigured=[{0}]连接器已é…置为支æŒHTTPå‡çº§åˆ°[{1}] +abstractHttp11Protocol.upgradeJmxNameFail=无法在JMX中创建用于注册å‡çº§å议的ObjectName +abstractHttp11Protocol.upgradeJmxRegistrationFail=JMXå‡çº§å议注册失败 + +http11processor.fallToDebug=\n\ +\ 注æ„:HTTP请求解æžé”™è¯¯çš„进一步å‘生将记录在DEBUG级别。 +http11processor.header.parse=è§£æž HTTP 请求 header 错误 +http11processor.request.finish=完æˆè¯·æ±‚时出错 +http11processor.request.inconsistentHosts=请求行中指定的主机与主机头ä¸ä¸€è‡´ã€‚ +http11processor.request.invalidScheme=HTTP请求包å«å…·æœ‰æ— æ•ˆæ–¹æ¡ˆçš„ç»å¯¹URL +http11processor.request.invalidTransferEncoding=HTTP请求包å«äº†ä¸€ä¸ªæ— æ•ˆçš„Transfer-Encoding头 +http11processor.request.invalidUri=HTTP请求包å«ä¸€ä¸ªæ— æ•ˆçš„URI +http11processor.request.invalidUserInfo=HTTP 请求包å«å¸¦æœ‰æ— æ•ˆ userinfo çš„ç»å¯¹ URI +http11processor.request.multipleContentLength=请求包å«äº†å¤šä¸ªcontent-length请求头å‚æ•° +http11processor.request.multipleHosts=请求包å«å¤šä¸ªä¸»æœºå¤´ +http11processor.request.noHostHeader=http/1.1请求没有æ供主机头 +http11processor.request.nonNumericContentLength=请求包å«ä¸€ä¸ªå…·æœ‰éžæ•°å­—值的内容长度头 +http11processor.request.prepare=准备请求时出错 +http11processor.request.process=错误的处ç†è¯·æ±‚ +http11processor.response.finish=错误完æˆç›¸åº” +http11processor.response.invalidHeader=HTTPå“应头信æ¯[{0}] 值[{1}],由于无效已从å“应中移除 +http11processor.sendfile.error=使用sendfileå‘é€æ•°æ®æ—¶å‡ºé”™ã€‚å¯èƒ½æ˜¯ç”±äºŽèµ·ç‚¹/终点的请求属性无效引起的 +http11processor.socket.info=获å–socketä¿¡æ¯å¼‚常 + +iib.available.readFail=å°è¯•ç¡®å®šæ•°æ®æ˜¯å¦å¯ç”¨æ—¶ï¼Œéžé˜»å¡žè¯»å–失败 +iib.eof.error=套接字读å–到æ„外的EOF +iib.failedread.apr=读å–失败,APR/本机错误代ç ä¸º[{0}] +iib.filter.npe=ä½ ä¸èƒ½æ·»åŠ ç©ºè¿‡æ»¤å™¨(null) +iib.invalidHttpProtocol=在HTTPå议中å‘现无效字符[{0}] +iib.invalidPhase=无效的请求行解æžé˜¶æ®µ[{0}] +iib.invalidRequestTarget=在请求目标中找到无效字符[{0}]。有效字符在RFC 7230å’ŒRFC 3986中定义 +iib.invalidheader=HTTP headerè¡Œ [{0}] ä¸ç¬¦åˆRFC 7230并且已被忽略。 +iib.invalidmethod=在方法å称[{0}]中å‘现无效的字符串, HTTP 方法å必须是有效的符å·. +iib.parseheaders.ise.error=æ„外状æ€ï¼šå·²è§£æžæ ‡å¤´ã€‚ 缓冲池ä¸å›žæ”¶ï¼Ÿ +iib.readtimeout=从套接字读å–æ•°æ®è¶…æ—¶ +iib.requestheadertoolarge.error=请求头太大 + +iob.failedwrite=写入.失败 +iob.failedwrite.ack=无法å‘é€HTTP 100继续å“应 +iob.responseheadertoolarge.error=å°è¯•å°†æ›´å¤šæ•°æ®å†™å…¥å“应标头,而ä¸æ˜¯ç¼“冲区中有å¯ç”¨ç©ºé—´ã€‚ 增加连接器上的maxHttpHeaderSize或将更少的数æ®å†™å…¥å“应头。 diff --git a/java/org/apache/coyote/http11/OutputFilter.java b/java/org/apache/coyote/http11/OutputFilter.java new file mode 100644 index 0000000..040ec3b --- /dev/null +++ b/java/org/apache/coyote/http11/OutputFilter.java @@ -0,0 +1,49 @@ +/* + * 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.coyote.http11; + +import org.apache.coyote.Response; + +/** + * Output filter. + * + * @author Remy Maucherat + */ +public interface OutputFilter extends HttpOutputBuffer { + + /** + * Some filters need additional parameters from the response. All the necessary reading can occur in that method, as + * this method is called after the response header processing is complete. + * + * @param response The response to associate with this OutputFilter + */ + void setResponse(Response response); + + + /** + * Make the filter ready to process the next request. + */ + void recycle(); + + + /** + * Set the next buffer in the filter pipeline. + * + * @param buffer The next buffer instance + */ + void setBuffer(HttpOutputBuffer buffer); +} diff --git a/java/org/apache/coyote/http11/filters/BufferedInputFilter.java b/java/org/apache/coyote/http11/filters/BufferedInputFilter.java new file mode 100644 index 0000000..8727f4d --- /dev/null +++ b/java/org/apache/coyote/http11/filters/BufferedInputFilter.java @@ -0,0 +1,188 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.apache.coyote.InputBuffer; +import org.apache.coyote.Request; +import org.apache.coyote.http11.InputFilter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.res.StringManager; + +/** + * Input filter responsible for reading and buffering the request body, so that + * it does not interfere with client SSL handshake messages. + */ +public class BufferedInputFilter implements InputFilter, ApplicationBufferHandler { + + private static final StringManager sm = StringManager.getManager(BufferedInputFilter.class); + + private static final String ENCODING_NAME = "buffered"; + private static final ByteChunk ENCODING = new ByteChunk(); + + + static { + ENCODING.setBytes(ENCODING_NAME.getBytes(StandardCharsets.ISO_8859_1), 0, ENCODING_NAME.length()); + } + + + // Use ByteChunk since it correctly handles the special buffer size of -1 + // for maxSavePostSize. + private ByteChunk buffered; + private ByteBuffer tempRead; + private InputBuffer buffer; + private boolean hasRead = false; + + private final int maxSwallowSize; + + + public BufferedInputFilter(int maxSwallowSize) { + this.maxSwallowSize = maxSwallowSize; + } + + // --------------------------------------------------------- Public Methods + + + /** + * Set the buffering limit. This should be reset every time the buffer is + * used. + * + * @param limit The maximum number of bytes that will be buffered + */ + public void setLimit(int limit) { + if (buffered == null) { + buffered = new ByteChunk(); + buffered.setLimit(limit); + } + } + + + // ---------------------------------------------------- InputBuffer Methods + + + /** + * Reads the request body and buffers it. + */ + @Override + public void setRequest(Request request) { + // save off the Request body + try { + if (buffered.getLimit() == 0) { + // Special case - ignore (swallow) body. Do so within a limit. + long swallowed = 0; + int read = 0; + while ((read = buffer.doRead(this)) >= 0) { + swallowed += read; + if (maxSwallowSize > -1 && swallowed > maxSwallowSize) { + // No need for i18n - this isn't going to get logged + throw new IOException(sm.getString("bufferedInputFilter.maxSwallowSize")); + } + } + } else { + while (buffer.doRead(this) >= 0) { + buffered.append(tempRead); + tempRead = null; + } + } + } catch(IOException | BufferOverflowException ioe) { + // No need for i18n - this isn't going to get logged anywhere + throw new IllegalStateException(sm.getString("bufferedInputFilter.bodySize", ioe.getMessage())); + } + } + + /** + * Fills the given ByteBuffer with the buffered request body. + */ + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + if (isFinished()) { + return -1; + } + + handler.setByteBuffer(ByteBuffer.wrap(buffered.getBuffer(), buffered.getStart(), buffered.getLength())); + hasRead = true; + return buffered.getLength(); + } + + @Override + public void setBuffer(InputBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void recycle() { + if (buffered != null) { + if (buffered.getBuffer() != null && buffered.getBuffer().length > 65536) { + buffered = null; + } else { + buffered.recycle(); + } + } + hasRead = false; + buffer = null; + } + + @Override + public ByteChunk getEncodingName() { + return ENCODING; + } + + @Override + public long end() throws IOException { + return 0; + } + + @Override + public int available() { + int available = buffered.getLength(); + if (available == 0) { + // No data buffered here. Try the next filter in the chain. + return buffer.available(); + } else { + return available; + } + } + + + @Override + public boolean isFinished() { + return hasRead || buffered.getLength() <= 0; + } + + + @Override + public void setByteBuffer(ByteBuffer buffer) { + tempRead = buffer; + } + + + @Override + public ByteBuffer getByteBuffer() { + return tempRead; + } + + + @Override + public void expand(int size) { + // no-op + } +} diff --git a/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java b/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java new file mode 100644 index 0000000..13dcb7d --- /dev/null +++ b/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java @@ -0,0 +1,659 @@ +/* + * 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.coyote.http11.filters; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.coyote.BadRequestException; +import org.apache.coyote.InputBuffer; +import org.apache.coyote.Request; +import org.apache.coyote.http11.Constants; +import org.apache.coyote.http11.InputFilter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.http.parser.HttpParser; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.res.StringManager; + +/** + * Chunked input filter. Parses chunked data according to + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
    + * + * @author Remy Maucherat + */ +public class ChunkedInputFilter implements InputFilter, ApplicationBufferHandler { + + private static final StringManager sm = StringManager.getManager(ChunkedInputFilter.class); + + + // -------------------------------------------------------------- Constants + + protected static final String ENCODING_NAME = "chunked"; + protected static final ByteChunk ENCODING = new ByteChunk(); + + + // ----------------------------------------------------- Static Initializer + + static { + ENCODING.setBytes(ENCODING_NAME.getBytes(StandardCharsets.ISO_8859_1), + 0, ENCODING_NAME.length()); + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Next buffer in the pipeline. + */ + protected InputBuffer buffer; + + + /** + * Number of bytes remaining in the current chunk. + */ + protected int remaining = 0; + + + /** + * Byte chunk used to read bytes. + */ + protected ByteBuffer readChunk; + + + /** + * Flag set to true when the end chunk has been read. + */ + protected boolean endChunk = false; + + + /** + * Byte chunk used to store trailing headers. + */ + protected final ByteChunk trailingHeaders = new ByteChunk(); + + + /** + * Flag set to true if the next call to doRead() must parse a CRLF pair + * before doing anything else. + */ + protected boolean needCRLFParse = false; + + + /** + * Request being parsed. + */ + private Request request; + + + /** + * Limit for extension size. + */ + private final long maxExtensionSize; + + + /** + * Limit for trailer size. + */ + private final int maxTrailerSize; + + + /** + * Size of extensions processed for this request. + */ + private long extensionSize; + + + private final int maxSwallowSize; + + + /** + * Flag that indicates if an error has occurred. + */ + private boolean error; + + + private final Set allowedTrailerHeaders; + + // ----------------------------------------------------------- Constructors + + public ChunkedInputFilter(int maxTrailerSize, Set allowedTrailerHeaders, + int maxExtensionSize, int maxSwallowSize) { + this.trailingHeaders.setLimit(maxTrailerSize); + this.allowedTrailerHeaders = allowedTrailerHeaders; + this.maxExtensionSize = maxExtensionSize; + this.maxTrailerSize = maxTrailerSize; + this.maxSwallowSize = maxSwallowSize; + } + + + // ---------------------------------------------------- InputBuffer Methods + + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + if (endChunk) { + return -1; + } + + checkError(); + + if(needCRLFParse) { + needCRLFParse = false; + parseCRLF(false); + } + + if (remaining <= 0) { + if (!parseChunkHeader()) { + throwBadRequestException(sm.getString("chunkedInputFilter.invalidHeader")); + } + if (endChunk) { + parseEndChunk(); + return -1; + } + } + + int result = 0; + + if (readChunk == null || readChunk.position() >= readChunk.limit()) { + if (readBytes() < 0) { + throwEOFException(sm.getString("chunkedInputFilter.eos")); + } + } + + if (remaining > readChunk.remaining()) { + result = readChunk.remaining(); + remaining = remaining - result; + if (readChunk != handler.getByteBuffer()) { + handler.setByteBuffer(readChunk.duplicate()); + } + readChunk.position(readChunk.limit()); + } else { + result = remaining; + if (readChunk != handler.getByteBuffer()) { + handler.setByteBuffer(readChunk.duplicate()); + handler.getByteBuffer().limit(readChunk.position() + remaining); + } + readChunk.position(readChunk.position() + remaining); + remaining = 0; + //we need a CRLF + if ((readChunk.position() + 1) >= readChunk.limit()) { + //if we call parseCRLF we overrun the buffer here + //so we defer it to the next call BZ 11117 + needCRLFParse = true; + } else { + parseCRLF(false); //parse the CRLF immediately + } + } + + return result; + } + + + // ---------------------------------------------------- InputFilter Methods + + /** + * Read the content length from the request. + */ + @Override + public void setRequest(Request request) { + this.request = request; + } + + + /** + * End the current request. + */ + @Override + public long end() throws IOException { + long swallowed = 0; + int read = 0; + // Consume extra bytes : parse the stream until the end chunk is found + while ((read = doRead(this)) >= 0) { + swallowed += read; + if (maxSwallowSize > -1 && swallowed > maxSwallowSize) { + throwBadRequestException(sm.getString("inputFilter.maxSwallow")); + } + } + + // Return the number of extra bytes which were consumed + return readChunk.remaining(); + } + + + /** + * Amount of bytes still available in a buffer. + */ + @Override + public int available() { + int available = 0; + if (readChunk != null) { + available = readChunk.remaining(); + } + if (available == 0) { + // No data buffered here. Try the next filter in the chain. + return buffer.available(); + } else { + return available; + } + } + + + /** + * Set the next buffer in the filter pipeline. + */ + @Override + public void setBuffer(InputBuffer buffer) { + this.buffer = buffer; + } + + + /** + * Make the filter ready to process the next request. + */ + @Override + public void recycle() { + remaining = 0; + if (readChunk != null) { + readChunk.position(0).limit(0); + } + endChunk = false; + needCRLFParse = false; + trailingHeaders.recycle(); + trailingHeaders.setLimit(maxTrailerSize); + extensionSize = 0; + error = false; + } + + + /** + * Return the name of the associated encoding; Here, the value is + * "identity". + */ + @Override + public ByteChunk getEncodingName() { + return ENCODING; + } + + + @Override + public boolean isFinished() { + return endChunk; + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Read bytes from the previous buffer. + * @return The byte count which has been read + * @throws IOException Read error + */ + protected int readBytes() throws IOException { + return buffer.doRead(this); + } + + + /** + * Parse the header of a chunk. + * A chunk header can look like one of the following:
    + * A10CRLF
    + * F23;chunk-extension to be ignoredCRLF + * + *

    + * The letters before CRLF or ';' (whatever comes first) must be valid hex + * digits. We should not parse F23IAMGONNAMESSTHISUP34CRLF as a valid + * header according to the spec. + * @return true if the chunk header has been + * successfully parsed + * @throws IOException Read error + */ + protected boolean parseChunkHeader() throws IOException { + + int result = 0; + boolean eol = false; + int readDigit = 0; + boolean extension = false; + + while (!eol) { + + if (readChunk == null || readChunk.position() >= readChunk.limit()) { + if (readBytes() <= 0) { + return false; + } + } + + byte chr = readChunk.get(readChunk.position()); + if (chr == Constants.CR || chr == Constants.LF) { + parseCRLF(false); + eol = true; + } else if (chr == Constants.SEMI_COLON && !extension) { + // First semi-colon marks the start of the extension. Further + // semi-colons may appear to separate multiple chunk-extensions. + // These need to be processed as part of parsing the extensions. + extension = true; + extensionSize++; + } else if (!extension) { + //don't read data after the trailer + int charValue = HexUtils.getDec(chr); + if (charValue != -1 && readDigit < 8) { + readDigit++; + result = (result << 4) | charValue; + } else { + //we shouldn't allow invalid, non hex characters + //in the chunked header + return false; + } + } else { + // Extension 'parsing' + // Note that the chunk-extension is neither parsed nor + // validated. Currently it is simply ignored. + extensionSize++; + if (maxExtensionSize > -1 && extensionSize > maxExtensionSize) { + throwBadRequestException(sm.getString("chunkedInputFilter.maxExtension")); + } + } + + // Parsing the CRLF increments pos + if (!eol) { + readChunk.position(readChunk.position() + 1); + } + } + + if (readDigit == 0 || result < 0) { + return false; + } + + if (result == 0) { + endChunk = true; + } + + remaining = result; + return true; + } + + + /** + * Parse CRLF at end of chunk. + * + * @param tolerant Should tolerant parsing (LF and CRLF) be used? This + * is recommended (RFC2616, section 19.3) for message + * headers. + * @throws IOException An error occurred parsing CRLF + */ + protected void parseCRLF(boolean tolerant) throws IOException { + + boolean eol = false; + boolean crfound = false; + + while (!eol) { + if (readChunk == null || readChunk.position() >= readChunk.limit()) { + if (readBytes() <= 0) { + throwBadRequestException(sm.getString("chunkedInputFilter.invalidCrlfNoData")); + } + } + + byte chr = readChunk.get(readChunk.position()); + if (chr == Constants.CR) { + if (crfound) { + throwBadRequestException(sm.getString("chunkedInputFilter.invalidCrlfCRCR")); + } + crfound = true; + } else if (chr == Constants.LF) { + if (!tolerant && !crfound) { + throwBadRequestException(sm.getString("chunkedInputFilter.invalidCrlfNoCR")); + } + eol = true; + } else { + throwBadRequestException(sm.getString("chunkedInputFilter.invalidCrlf")); + } + + readChunk.position(readChunk.position() + 1); + } + } + + + /** + * Parse end chunk data. + * @throws IOException Error propagation + */ + protected void parseEndChunk() throws IOException { + // Handle optional trailer headers + while (parseHeader()) { + // Loop until we run out of headers + } + } + + + private boolean parseHeader() throws IOException { + + /* + * Implementation note: Any changes to this method probably need to be echoed in + * Http11InputBuffer.parseHeader(). Why not use a common implementation? In short, this code uses blocking + * reads whereas Http11InputBuffer using non-blocking reads. The code is just different enough that a common + * implementation wasn't viewed as practical. + */ + + Map headers = request.getTrailerFields(); + + byte chr = 0; + + // Read new bytes if needed + if (readChunk == null || readChunk.position() >= readChunk.limit()) { + if (readBytes() <0) { + throwEOFException(sm.getString("chunkedInputFilter.eosTrailer")); + } + } + + // readBytes() above will set readChunk unless it returns a value < 0 + chr = readChunk.get(readChunk.position()); + + // CRLF terminates the request + if (chr == Constants.CR || chr == Constants.LF) { + parseCRLF(false); + return false; + } + + // Mark the current buffer position + int startPos = trailingHeaders.getEnd(); + + // + // Reading the header name + // Header name is always US-ASCII + // + + boolean colon = false; + while (!colon) { + + // Read new bytes if needed + if (readChunk == null || readChunk.position() >= readChunk.limit()) { + if (readBytes() <0) { + throwEOFException(sm.getString("chunkedInputFilter.eosTrailer")); + } + } + + // readBytes() above will set readChunk unless it returns a value < 0 + chr = readChunk.get(readChunk.position()); + if (chr >= Constants.A && chr <= Constants.Z) { + chr = (byte) (chr - Constants.LC_OFFSET); + } + + if (chr == Constants.COLON) { + colon = true; + } else if (!HttpParser.isToken(chr)) { + // Non-token characters are illegal in header names + throwBadRequestException(sm.getString("chunkedInputFilter.invalidTrailerHeaderName")); + } else if (trailingHeaders.getEnd() >= trailingHeaders.getLimit()) { + throwBadRequestException(sm.getString("chunkedInputFilter.maxTrailer")); + } else { + trailingHeaders.append(chr); + } + + readChunk.position(readChunk.position() + 1); + + } + int colonPos = trailingHeaders.getEnd(); + + // + // Reading the header value (which can be spanned over multiple lines) + // + + boolean eol = false; + boolean validLine = true; + int lastSignificantChar = 0; + + while (validLine) { + + boolean space = true; + + // Skipping spaces + while (space) { + + // Read new bytes if needed + if (readChunk == null || readChunk.position() >= readChunk.limit()) { + if (readBytes() <0) { + throwEOFException(sm.getString("chunkedInputFilter.eosTrailer")); + } + } + + chr = readChunk.get(readChunk.position()); + if (chr == Constants.SP || chr == Constants.HT) { + readChunk.position(readChunk.position() + 1); + // If we swallow whitespace, make sure it counts towards the + // limit placed on trailing header size + int newlimit = trailingHeaders.getLimit() -1; + if (trailingHeaders.getEnd() > newlimit) { + throwBadRequestException(sm.getString("chunkedInputFilter.maxTrailer")); + } + trailingHeaders.setLimit(newlimit); + } else { + space = false; + } + + } + + // Reading bytes until the end of the line + while (!eol) { + + // Read new bytes if needed + if (readChunk == null || readChunk.position() >= readChunk.limit()) { + if (readBytes() <0) { + throwEOFException(sm.getString("chunkedInputFilter.eosTrailer")); + } + } + + chr = readChunk.get(readChunk.position()); + if (chr == Constants.CR || chr == Constants.LF) { + parseCRLF(true); + eol = true; + } else if (HttpParser.isControl(chr) && chr != Constants.HT) { + throw new IOException(sm.getString("chunkedInputFilter.invalidTrailerHeaderValue")); + } else if (trailingHeaders.getEnd() >= trailingHeaders.getLimit()) { + throwBadRequestException(sm.getString("chunkedInputFilter.maxTrailer")); + } else if (chr == Constants.SP || chr == Constants.HT) { + trailingHeaders.append(chr); + } else { + trailingHeaders.append(chr); + lastSignificantChar = trailingHeaders.getEnd(); + } + + if (!eol) { + readChunk.position(readChunk.position() + 1); + } + } + + // Checking the first character of the new line. If the character + // is a LWS, then it's a multiline header + + // Read new bytes if needed + if (readChunk == null || readChunk.position() >= readChunk.limit()) { + if (readBytes() <0) { + throwEOFException(sm.getString("chunkedInputFilter.eosTrailer")); + } + } + + chr = readChunk.get(readChunk.position()); + if (chr != Constants.SP && chr != Constants.HT) { + validLine = false; + } else if (trailingHeaders.getEnd() >= trailingHeaders.getLimit()) { + throwBadRequestException(sm.getString("chunkedInputFilter.maxTrailer")); + } else { + eol = false; + // Copying one extra space in the buffer (since there must + // be at least one space inserted between the lines) + trailingHeaders.append(chr); + } + + } + + String headerName = new String(trailingHeaders.getBytes(), startPos, + colonPos - startPos, StandardCharsets.ISO_8859_1); + + headerName = headerName.toLowerCase(Locale.ENGLISH); + + if (allowedTrailerHeaders.contains(headerName)) { + + String value = new String(trailingHeaders.getBytes(), colonPos, + lastSignificantChar - colonPos, StandardCharsets.ISO_8859_1); + + headers.put(headerName, value); + } + + return true; + } + + + private void throwBadRequestException(String msg) throws IOException { + error = true; + throw new BadRequestException(msg); + } + + + private void throwEOFException(String msg) throws IOException { + error = true; + throw new EOFException(msg); + } + + + private void checkError() throws IOException { + if (error) { + throw new IOException(sm.getString("chunkedInputFilter.error")); + } + } + + + @Override + public void setByteBuffer(ByteBuffer buffer) { + readChunk = buffer; + } + + + @Override + public ByteBuffer getByteBuffer() { + return readChunk; + } + + + @Override + public void expand(int size) { + // no-op + } +} diff --git a/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java b/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java new file mode 100644 index 0000000..5879bbf --- /dev/null +++ b/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java @@ -0,0 +1,209 @@ +/* + * 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.coyote.http11.filters; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.apache.coyote.Response; +import org.apache.coyote.http11.HttpOutputBuffer; +import org.apache.coyote.http11.OutputFilter; +import org.apache.tomcat.util.buf.HexUtils; + +/** + * Chunked output filter. + * + * @author Remy Maucherat + */ +public class ChunkedOutputFilter implements OutputFilter { + + private static final byte[] LAST_CHUNK_BYTES = {(byte) '0', (byte) '\r', (byte) '\n'}; + private static final byte[] CRLF_BYTES = {(byte) '\r', (byte) '\n'}; + private static final byte[] END_CHUNK_BYTES = + {(byte) '0', (byte) '\r', (byte) '\n', (byte) '\r', (byte) '\n'}; + + private static final Set disallowedTrailerFieldNames = new HashSet<>(); + + static { + // Always add these in lower case + disallowedTrailerFieldNames.add("age"); + disallowedTrailerFieldNames.add("cache-control"); + disallowedTrailerFieldNames.add("content-length"); + disallowedTrailerFieldNames.add("content-encoding"); + disallowedTrailerFieldNames.add("content-range"); + disallowedTrailerFieldNames.add("content-type"); + disallowedTrailerFieldNames.add("date"); + disallowedTrailerFieldNames.add("expires"); + disallowedTrailerFieldNames.add("location"); + disallowedTrailerFieldNames.add("retry-after"); + disallowedTrailerFieldNames.add("trailer"); + disallowedTrailerFieldNames.add("transfer-encoding"); + disallowedTrailerFieldNames.add("vary"); + disallowedTrailerFieldNames.add("warning"); + } + + /** + * Next buffer in the pipeline. + */ + protected HttpOutputBuffer buffer; + + + /** + * Chunk header. + */ + protected final ByteBuffer chunkHeader = ByteBuffer.allocate(10); + + + protected final ByteBuffer lastChunk = ByteBuffer.wrap(LAST_CHUNK_BYTES); + protected final ByteBuffer crlfChunk = ByteBuffer.wrap(CRLF_BYTES); + /** + * End chunk. + */ + protected final ByteBuffer endChunk = ByteBuffer.wrap(END_CHUNK_BYTES); + + + private Response response; + + + public ChunkedOutputFilter() { + chunkHeader.put(8, (byte) '\r'); + chunkHeader.put(9, (byte) '\n'); + } + + + // --------------------------------------------------- OutputBuffer Methods + + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + + int result = chunk.remaining(); + + if (result <= 0) { + return 0; + } + + int pos = calculateChunkHeader(result); + + chunkHeader.position(pos).limit(10); + buffer.doWrite(chunkHeader); + + buffer.doWrite(chunk); + + chunkHeader.position(8).limit(10); + buffer.doWrite(chunkHeader); + + return result; + } + + + private int calculateChunkHeader(int len) { + // Calculate chunk header + int pos = 8; + int current = len; + while (current > 0) { + int digit = current % 16; + current = current / 16; + chunkHeader.put(--pos, HexUtils.getHex(digit)); + } + return pos; + } + + + @Override + public long getBytesWritten() { + return buffer.getBytesWritten(); + } + + + // --------------------------------------------------- OutputFilter Methods + + @Override + public void setResponse(Response response) { + this.response = response; + } + + + @Override + public void setBuffer(HttpOutputBuffer buffer) { + this.buffer = buffer; + } + + + @Override + public void flush() throws IOException { + // No data buffered in this filter. Flush next buffer. + buffer.flush(); + } + + + @Override + public void end() throws IOException { + + Supplier> trailerFieldsSupplier = response.getTrailerFields(); + Map trailerFields = null; + + if (trailerFieldsSupplier != null) { + trailerFields = trailerFieldsSupplier.get(); + } + + if (trailerFields == null) { + // Write end chunk + buffer.doWrite(endChunk); + endChunk.position(0).limit(endChunk.capacity()); + } else { + buffer.doWrite(lastChunk); + lastChunk.position(0).limit(lastChunk.capacity()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + + try (OutputStreamWriter osw = new OutputStreamWriter(baos, StandardCharsets.ISO_8859_1)) { + for (Map.Entry trailerField : trailerFields.entrySet()) { + // Ignore disallowed headers + if (disallowedTrailerFieldNames.contains( + trailerField.getKey().toLowerCase(Locale.ENGLISH))) { + continue; + } + osw.write(trailerField.getKey()); + osw.write(':'); + osw.write(' '); + osw.write(trailerField.getValue()); + osw.write("\r\n"); + } + } + + buffer.doWrite(ByteBuffer.wrap(baos.toByteArray())); + + buffer.doWrite(crlfChunk); + crlfChunk.position(0).limit(crlfChunk.capacity()); + } + buffer.end(); + } + + + @Override + public void recycle() { + response = null; + } +} diff --git a/java/org/apache/coyote/http11/filters/GzipOutputFilter.java b/java/org/apache/coyote/http11/filters/GzipOutputFilter.java new file mode 100644 index 0000000..14c68db --- /dev/null +++ b/java/org/apache/coyote/http11/filters/GzipOutputFilter.java @@ -0,0 +1,170 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.zip.GZIPOutputStream; + +import org.apache.coyote.Response; +import org.apache.coyote.http11.HttpOutputBuffer; +import org.apache.coyote.http11.OutputFilter; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Gzip output filter. + * + * @author Remy Maucherat + */ +public class GzipOutputFilter implements OutputFilter { + + protected static final Log log = LogFactory.getLog(GzipOutputFilter.class); + private static final StringManager sm = StringManager.getManager(GzipOutputFilter.class); + + + // ----------------------------------------------------- Instance Variables + + /** + * Next buffer in the pipeline. + */ + protected HttpOutputBuffer buffer; + + + /** + * Compression output stream. + */ + protected GZIPOutputStream compressionStream = null; + + + /** + * Fake internal output stream. + */ + protected final OutputStream fakeOutputStream = new FakeOutputStream(); + + + // --------------------------------------------------- OutputBuffer Methods + + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + if (compressionStream == null) { + compressionStream = new GZIPOutputStream(fakeOutputStream, true); + } + int len = chunk.remaining(); + if (chunk.hasArray()) { + compressionStream.write(chunk.array(), chunk.arrayOffset() + chunk.position(), len); + chunk.position(chunk.position() + len); + } else { + byte[] bytes = new byte[len]; + chunk.get(bytes); + compressionStream.write(bytes, 0, len); + } + return len; + } + + + @Override + public long getBytesWritten() { + return buffer.getBytesWritten(); + } + + + // --------------------------------------------------- OutputFilter Methods + + /** + * Added to allow flushing to happen for the gzip'ed outputstream + */ + @Override + public void flush() throws IOException { + if (compressionStream != null) { + try { + if (log.isTraceEnabled()) { + log.trace("Flushing the compression stream!"); + } + compressionStream.flush(); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("gzipOutputFilter.flushFail"), e); + } + } + } + buffer.flush(); + } + + + @Override + public void setResponse(Response response) { + // NOOP: No need for parameters from response in this filter + } + + + @Override + public void setBuffer(HttpOutputBuffer buffer) { + this.buffer = buffer; + } + + + @Override + public void end() throws IOException { + if (compressionStream == null) { + compressionStream = new GZIPOutputStream(fakeOutputStream, true); + } + compressionStream.finish(); + compressionStream.close(); + buffer.end(); + } + + + /** + * Make the filter ready to process the next request. + */ + @Override + public void recycle() { + // Set compression stream to null + compressionStream = null; + } + + + // ------------------------------------------- FakeOutputStream Inner Class + + + protected class FakeOutputStream + extends OutputStream { + protected final ByteBuffer outputChunk = ByteBuffer.allocate(1); + @Override + public void write(int b) + throws IOException { + // Shouldn't get used for good performance, but is needed for + // compatibility with Sun JDK 1.4.0 + outputChunk.put(0, (byte) (b & 0xff)); + buffer.doWrite(outputChunk); + } + @Override + public void write(byte[] b, int off, int len) + throws IOException { + buffer.doWrite(ByteBuffer.wrap(b, off, len)); + } + @Override + public void flush() throws IOException {/*NOOP*/} + @Override + public void close() throws IOException {/*NOOP*/} + } + + +} diff --git a/java/org/apache/coyote/http11/filters/IdentityInputFilter.java b/java/org/apache/coyote/http11/filters/IdentityInputFilter.java new file mode 100644 index 0000000..f89cee5 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/IdentityInputFilter.java @@ -0,0 +1,232 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.apache.coyote.InputBuffer; +import org.apache.coyote.Request; +import org.apache.coyote.http11.InputFilter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.res.StringManager; + +/** + * Identity input filter. + * + * @author Remy Maucherat + */ +public class IdentityInputFilter implements InputFilter, ApplicationBufferHandler { + + private static final StringManager sm = StringManager.getManager(IdentityInputFilter.class); + + + // -------------------------------------------------------------- Constants + + protected static final String ENCODING_NAME = "identity"; + protected static final ByteChunk ENCODING = new ByteChunk(); + + + // ----------------------------------------------------- Static Initializer + + static { + ENCODING.setBytes(ENCODING_NAME.getBytes(StandardCharsets.ISO_8859_1), + 0, ENCODING_NAME.length()); + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Content length. + */ + protected long contentLength = -1; + + + /** + * Remaining bytes. + */ + protected long remaining = 0; + + + /** + * Next buffer in the pipeline. + */ + protected InputBuffer buffer; + + + /** + * ByteBuffer used to read leftover bytes. + */ + protected ByteBuffer tempRead; + + + private final int maxSwallowSize; + + + public IdentityInputFilter(int maxSwallowSize) { + this.maxSwallowSize = maxSwallowSize; + } + + + // ---------------------------------------------------- InputBuffer Methods + + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + + int result = -1; + + if (contentLength >= 0) { + if (remaining > 0) { + int nRead = buffer.doRead(handler); + if (nRead > remaining) { + // The chunk is longer than the number of bytes remaining + // in the body; changing the chunk length to the number + // of bytes remaining + handler.getByteBuffer().limit(handler.getByteBuffer().position() + (int) remaining); + result = (int) remaining; + } else { + result = nRead; + } + if (nRead > 0) { + remaining = remaining - nRead; + } + } else { + // No more bytes left to be read : return -1 and clear the + // buffer + if (handler.getByteBuffer() != null) { + handler.getByteBuffer().position(0).limit(0); + } + result = -1; + } + } + + return result; + + } + + + // ---------------------------------------------------- InputFilter Methods + + + /** + * Read the content length from the request. + */ + @Override + public void setRequest(Request request) { + contentLength = request.getContentLengthLong(); + remaining = contentLength; + } + + + @Override + public long end() throws IOException { + + final boolean maxSwallowSizeExceeded = (maxSwallowSize > -1 && remaining > maxSwallowSize); + long swallowed = 0; + + // Consume extra bytes. + while (remaining > 0) { + + int nread = buffer.doRead(this); + tempRead = null; + if (nread > 0 ) { + swallowed += nread; + remaining = remaining - nread; + if (maxSwallowSizeExceeded && swallowed > maxSwallowSize) { + // Note: We do not fail early so the client has a chance to + // read the response before the connection is closed. See: + // https://httpd.apache.org/docs/2.0/misc/fin_wait_2.html#appendix + throw new IOException(sm.getString("inputFilter.maxSwallow")); + } + } else { // errors are handled higher up. + remaining = 0; + } + } + + // If too many bytes were read, return the amount. + return -remaining; + + } + + + /** + * Amount of bytes still available in a buffer. + */ + @Override + public int available() { + // No data buffered here. Try the next filter in the chain. + return buffer.available(); + } + + + /** + * Set the next buffer in the filter pipeline. + */ + @Override + public void setBuffer(InputBuffer buffer) { + this.buffer = buffer; + } + + + /** + * Make the filter ready to process the next request. + */ + @Override + public void recycle() { + contentLength = -1; + remaining = 0; + } + + + /** + * Return the name of the associated encoding; Here, the value is + * "identity". + */ + @Override + public ByteChunk getEncodingName() { + return ENCODING; + } + + + @Override + public boolean isFinished() { + // Only finished if a content length is defined and there is no data + // remaining + return contentLength > -1 && remaining <= 0; + } + + + @Override + public void setByteBuffer(ByteBuffer buffer) { + tempRead = buffer; + } + + + @Override + public ByteBuffer getByteBuffer() { + return tempRead; + } + + + @Override + public void expand(int size) { + // no-op + } +} diff --git a/java/org/apache/coyote/http11/filters/IdentityOutputFilter.java b/java/org/apache/coyote/http11/filters/IdentityOutputFilter.java new file mode 100644 index 0000000..8c44d85 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/IdentityOutputFilter.java @@ -0,0 +1,132 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.coyote.Response; +import org.apache.coyote.http11.HttpOutputBuffer; +import org.apache.coyote.http11.OutputFilter; + +/** + * Identity output filter. + * + * @author Remy Maucherat + */ +public class IdentityOutputFilter implements OutputFilter { + + // ----------------------------------------------------- Instance Variables + + /** + * Content length. + */ + protected long contentLength = -1; + + + /** + * Remaining bytes. + */ + protected long remaining = 0; + + + /** + * Next buffer in the pipeline. + */ + protected HttpOutputBuffer buffer; + + + // --------------------------------------------------- OutputBuffer Methods + + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + + int result = -1; + + if (contentLength >= 0) { + if (remaining > 0) { + result = chunk.remaining(); + if (result > remaining) { + // The chunk is longer than the number of bytes remaining + // in the body; changing the chunk length to the number + // of bytes remaining + chunk.limit(chunk.position() + (int) remaining); + result = (int) remaining; + remaining = 0; + } else { + remaining = remaining - result; + } + buffer.doWrite(chunk); + } else { + // No more bytes left to be written : return -1 and clear the + // buffer + chunk.position(0); + chunk.limit(0); + result = -1; + } + } else { + // If no content length was set, just write the bytes + result = chunk.remaining(); + buffer.doWrite(chunk); + result -= chunk.remaining(); + } + + return result; + + } + + + @Override + public long getBytesWritten() { + return buffer.getBytesWritten(); + } + + + // --------------------------------------------------- OutputFilter Methods + + @Override + public void setResponse(Response response) { + contentLength = response.getContentLengthLong(); + remaining = contentLength; + } + + + @Override + public void setBuffer(HttpOutputBuffer buffer) { + this.buffer = buffer; + } + + + @Override + public void flush() throws IOException { + // No data buffered in this filter. Flush next buffer. + buffer.flush(); + } + + + @Override + public void end() throws IOException { + buffer.end(); + } + + + @Override + public void recycle() { + contentLength = -1; + remaining = 0; + } +} diff --git a/java/org/apache/coyote/http11/filters/LocalStrings.properties b/java/org/apache/coyote/http11/filters/LocalStrings.properties new file mode 100644 index 0000000..3650316 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings.properties @@ -0,0 +1,34 @@ +# 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. + +bufferedInputFilter.bodySize=Request body too large for buffer: [{0}] +bufferedInputFilter.maxSwallowSize=Ignored body exceeded maxSwallowSize + +chunkedInputFilter.eos=Unexpected end of stream while reading request body +chunkedInputFilter.eosTrailer=Unexpected end of stream while reading trailer headers +chunkedInputFilter.error=No data available due to previous error +chunkedInputFilter.invalidCrlf=Invalid end of line sequence (character other than CR or LF found) +chunkedInputFilter.invalidCrlfCRCR=Invalid end of line sequence (CRCR) +chunkedInputFilter.invalidCrlfNoCR=Invalid end of line sequence (No CR before LF) +chunkedInputFilter.invalidCrlfNoData=Invalid end of line sequence (no data available to read) +chunkedInputFilter.invalidHeader=Invalid chunk header +chunkedInputFilter.invalidTrailerHeaderName=Invalid trailer header name (non-token character in name) +chunkedInputFilter.invalidTrailerHeaderValue=Invalid trailer header value (control character in value) +chunkedInputFilter.maxExtension=maxExtensionSize exceeded +chunkedInputFilter.maxTrailer=maxTrailerSize exceeded + +gzipOutputFilter.flushFail=Ignored exception while flushing gzip filter + +inputFilter.maxSwallow=maxSwallowSize exceeded diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_cs.properties b/java/org/apache/coyote/http11/filters/LocalStrings_cs.properties new file mode 100644 index 0000000..527bf20 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_cs.properties @@ -0,0 +1,18 @@ +# 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. + +chunkedInputFilter.eos=NeoÄekuvaný konec streamu pÅ™i Ätení tÄ›la dotazu +chunkedInputFilter.eosTrailer=NeoÄekávaný konec toku dat bÄ›hem Ätení úvodních hlaviÄek +chunkedInputFilter.invalidCrlfNoCR=Neplatný konec řádku (není CR pÅ™ed LF) diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_de.properties b/java/org/apache/coyote/http11/filters/LocalStrings_de.properties new file mode 100644 index 0000000..006fb2e --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_de.properties @@ -0,0 +1,18 @@ +# 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. + +chunkedInputFilter.eos=Unerwartetes Ende des Streams beim Lesen des Request Bodys. +chunkedInputFilter.eosTrailer=Unerwartetes Ende des Eingabestroms während die Trailer-Header gelesen wurden +chunkedInputFilter.invalidCrlfNoCR=Falsche Buchstabensequenz für Zeilenende (richtig wäre ) diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_es.properties b/java/org/apache/coyote/http11/filters/LocalStrings_es.properties new file mode 100644 index 0000000..0bd142c --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_es.properties @@ -0,0 +1,18 @@ +# 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. + +chunkedInputFilter.eos=Fin del flujo inesperado mientras se leía la petición del cuerpo +chunkedInputFilter.eosTrailer=Fin inesperado de stream mintras se leían las cabeceras finales +chunkedInputFilter.invalidCrlfNoCR=Fin de linea incorrecto (CR no debe estar antes de LF) diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_fr.properties b/java/org/apache/coyote/http11/filters/LocalStrings_fr.properties new file mode 100644 index 0000000..fbf0298 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_fr.properties @@ -0,0 +1,34 @@ +# 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. + +bufferedInputFilter.bodySize=Le corps de la requête est trop grand pour le tampon: [{0}] +bufferedInputFilter.maxSwallowSize=Le corps de la requête qui dépassé maxSwallowSize a été ingoré + +chunkedInputFilter.eos=Fin du flux inattendue durant la lecture du corps de la requête +chunkedInputFilter.eosTrailer=Fin inattendue de flux lors de la lecture des en-têtes de fin (trailer headers) +chunkedInputFilter.error=Aucune donnée disponible suite à l'erreur précédente +chunkedInputFilter.invalidCrlf=Séquence de fin de ligne invalide, un caractère autre que CR ou LF a été trouvé +chunkedInputFilter.invalidCrlfCRCR=Séquence de fin de ligne invalide, CR CR +chunkedInputFilter.invalidCrlfNoCR=Séquence de fin de ligne incorrecte (manque CR devant LF) +chunkedInputFilter.invalidCrlfNoData=Séquence de fin de ligne invalide (aucune donnée disponible en lecture) +chunkedInputFilter.invalidHeader=En-tête de morceau (chunk) invalide +chunkedInputFilter.invalidTrailerHeaderName=Le nom de l'en-tête de fin est invalide (un caractère non autorisé dans le nom) +chunkedInputFilter.invalidTrailerHeaderValue=La valeur de l'en-tête de fin est invalide (un caractère de contrôle dans la valeur) +chunkedInputFilter.maxExtension=maxExtensionSize a été dépassé +chunkedInputFilter.maxTrailer=maxTrailerSize a été dépassé + +gzipOutputFilter.flushFail=L'erreur lors de l'envoi des données du filtre gzip est ignorée + +inputFilter.maxSwallow=maxSwallowSize a été dépassé diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_ja.properties b/java/org/apache/coyote/http11/filters/LocalStrings_ja.properties new file mode 100644 index 0000000..28c94a3 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_ja.properties @@ -0,0 +1,34 @@ +# 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. + +bufferedInputFilter.bodySize=リクエスト本文ãŒãƒãƒƒãƒ•ã‚¡ã«å¯¾ã—ã¦å¤§ãã™ãŽã¾ã™: [{0}] +bufferedInputFilter.maxSwallowSize=無視ã•ã‚ŒãŸæœ¬æ–‡ãŒ maxSwallowSize を超ãˆã¾ã—㟠+ +chunkedInputFilter.eos=リクエストボディã®èª­ã¿å–り中ã«äºˆæœŸã›ã¬ã‚¹ãƒˆãƒªãƒ¼ãƒ ã®çµ‚端ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚ +chunkedInputFilter.eosTrailer=トレーラーヘッダーã®èª­ã¿è¾¼ã¿ä¸­ã«çªç„¶ã‚¹ãƒˆãƒªãƒ¼ãƒ ãŒçµ‚了ã—ã¾ã—ãŸã€‚ +chunkedInputFilter.error=以å‰ã®ã‚¨ãƒ©ãƒ¼ã®ãŸã‚利用ã§ãるデータãŒã‚ã‚Šã¾ã›ã‚“。 +chunkedInputFilter.invalidCrlf=無効ãªEOLシーケンス(CRã¾ãŸã¯LF以外ã®æ–‡å­—ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸï¼‰ +chunkedInputFilter.invalidCrlfCRCR=無効ãªEOLシーケンス(CRCR) +chunkedInputFilter.invalidCrlfNoCR=無効ãªæ”¹è¡Œã‚³ãƒ¼ãƒ‰ã§ã™ (LFã®å‰ã«CRãŒã‚ã‚Šã¾ã›ã‚“) +chunkedInputFilter.invalidCrlfNoData=無効ãªEOLé †åºï¼ˆèª­ã¿è¾¼ã¿å¯èƒ½ãªãƒ‡ãƒ¼ã‚¿ãŒã‚ã‚Šã¾ã›ã‚“) +chunkedInputFilter.invalidHeader=無効ãªãƒãƒ£ãƒ³ã‚¯ãƒ˜ãƒƒãƒ€ãƒ¼ã§ã™ã€‚ +chunkedInputFilter.invalidTrailerHeaderName=無効㪠trailer header å(ヘッダåã«éžãƒˆãƒ¼ã‚¯ãƒ³æ–‡å­—ã‚’å«ã‚€ï¼‰ +chunkedInputFilter.invalidTrailerHeaderValue=無効㪠trailer header 値(値ã«åˆ¶å¾¡æ–‡å­—ã‚’å«ã‚€ï¼‰ +chunkedInputFilter.maxExtension=maxExtensionSizeを超éŽã—ã¾ã—㟠+chunkedInputFilter.maxTrailer=maxTrailerSize を超éŽã—ã¦ã„ã¾ã™ã€‚ + +gzipOutputFilter.flushFail=gzip フィルターã®ãƒ•ãƒ©ãƒƒã‚·ãƒ¥ä¸­ã«ä¾‹å¤–ãŒç„¡è¦–ã•ã‚Œã¾ã—㟠+ +inputFilter.maxSwallow=maxShallowSize を超ãˆã¾ã—ãŸã€‚ diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_ko.properties b/java/org/apache/coyote/http11/filters/LocalStrings_ko.properties new file mode 100644 index 0000000..4557c08 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_ko.properties @@ -0,0 +1,27 @@ +# 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. + +chunkedInputFilter.eos=ìš”ì²­ì˜ body를 ì½ëŠ” ë™ì•ˆ 예기치 ì•Šì€ ìŠ¤íŠ¸ë¦¼ì˜ ë +chunkedInputFilter.eosTrailer=Trailer í—¤ë”ë“¤ì„ ì½ëŠ” 중 예기치 ì•Šì€ ìŠ¤íŠ¸ë¦¼ì˜ ë +chunkedInputFilter.error=ì´ì „ 오류로 ì¸í•˜ì—¬ ë°ì´í„°ê°€ 가용하지 않습니다. +chunkedInputFilter.invalidCrlf=유효하지 ì•Šì€ í–‰ì˜ ë 시퀀스입니다. (CR ë˜ëŠ” LFê°€ ì•„ë‹Œ 다른 문ìžê°€ 발견ë¨) +chunkedInputFilter.invalidCrlfCRCR=유효하지 ì•Šì€ í–‰ì˜ ë 시퀀스 (CRCR) +chunkedInputFilter.invalidCrlfNoCR=유효하지 ì•Šì€ ë¼ì¸ ë 시퀀스 (CR ë°”ì´íŠ¸ê°€ LF ë°”ì´íŠ¸ ì „ì— ì¡´ìž¬í•˜ì§€ ì•ŠìŒ) +chunkedInputFilter.invalidCrlfNoData=유효하지 ì•Šì€ í–‰ì˜ ë 시퀀스 (ë” ì´ìƒ ì½ì„ ë°ì´í„°ê°€ ì—†ìŒ) +chunkedInputFilter.invalidHeader=유효하지 ì•Šì€ chunk í—¤ë” +chunkedInputFilter.maxExtension=maxExtensionSize ê°’ì„ ì´ˆê³¼í–ˆìŠµë‹ˆë‹¤. +chunkedInputFilter.maxTrailer=maxTrailerSize ê°’ì„ ì´ˆê³¼í–ˆìŠµë‹ˆë‹¤. + +inputFilter.maxSwallow=maxSwallowSize ê°’ì„ ì´ˆê³¼í–ˆìŠµë‹ˆë‹¤. diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_pt_BR.properties b/java/org/apache/coyote/http11/filters/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..07d18c8 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +chunkedInputFilter.invalidCrlfNoCR=Sequência de fim de linh inválida (sem CR antes do LF) diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_ru.properties b/java/org/apache/coyote/http11/filters/LocalStrings_ru.properties new file mode 100644 index 0000000..9e258ed --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_ru.properties @@ -0,0 +1,17 @@ +# 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. + +chunkedInputFilter.eos=Ðеожиданный конец потока при чтении тела запроÑа +chunkedInputFilter.invalidCrlfNoCR=ÐедопуÑÑ‚Ð¸Ð¼Ð°Ñ Ð¿Ð¾ÑледовательноÑÑ‚ÑŒ конца Ñтроки ( нет возврата каретки до перехода на новую Ñтроку) diff --git a/java/org/apache/coyote/http11/filters/LocalStrings_zh_CN.properties b/java/org/apache/coyote/http11/filters/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..11bed70 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/LocalStrings_zh_CN.properties @@ -0,0 +1,27 @@ +# 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. + +chunkedInputFilter.eos=读å–请求主体时æµçš„æ„å¤–ç»“æŸ +chunkedInputFilter.eosTrailer=è¯»å– trailer 头时出现æ„外的æµç»“æŸ +chunkedInputFilter.error=没有数æ®å¯ç”¨ç”±äºŽå…ˆå‰çš„错误 +chunkedInputFilter.invalidCrlf=行尾åºåˆ—无效(找到除CR或LF以外的字符) +chunkedInputFilter.invalidCrlfCRCR=无效的结æŸçš„è¡Œåºåˆ—(CRCR) +chunkedInputFilter.invalidCrlfNoCR=无效的行尾结æŸç¬¦(æ¢è¡Œå‰ç¼ºå°‘回车) +chunkedInputFilter.invalidCrlfNoData=无效的行尾åºåˆ—(没有å¯è¯»å–çš„æ•°æ®ï¼‰ +chunkedInputFilter.invalidHeader=æ— æ•ˆçš„å— +chunkedInputFilter.maxExtension=超过最大扩展数 +chunkedInputFilter.maxTrailer=超过最大数 + +inputFilter.maxSwallow=最大åžå’½æ•°æ®å¤§å°è¶…出异常 diff --git a/java/org/apache/coyote/http11/filters/SavedRequestInputFilter.java b/java/org/apache/coyote/http11/filters/SavedRequestInputFilter.java new file mode 100644 index 0000000..d9a8c15 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/SavedRequestInputFilter.java @@ -0,0 +1,112 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.coyote.InputBuffer; +import org.apache.coyote.http11.InputFilter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.ApplicationBufferHandler; + +/** + * Input filter responsible for replaying the request body when restoring the + * saved request after FORM authentication. + */ +public class SavedRequestInputFilter implements InputFilter { + + /** + * The original request body. + */ + protected ByteChunk input = null; + + /** + * Create a new SavedRequestInputFilter. + * + * @param input The saved request body to be replayed. + */ + public SavedRequestInputFilter(ByteChunk input) { + this.input = input; + } + + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + if(input.getOffset()>= input.getEnd()) { + return -1; + } + + ByteBuffer byteBuffer = handler.getByteBuffer(); + byteBuffer.position(byteBuffer.limit()).limit(byteBuffer.capacity()); + input.subtract(byteBuffer); + + return byteBuffer.remaining(); + } + + /** + * Set the content length on the request. + */ + @Override + public void setRequest(org.apache.coyote.Request request) { + request.setContentLength(input.getLength()); + } + + /** + * Make the filter ready to process the next request. + */ + @Override + public void recycle() { + input = null; + } + + /** + * Return the name of the associated encoding; here, the value is null. + */ + @Override + public ByteChunk getEncodingName() { + return null; + } + + /** + * Set the next buffer in the filter pipeline (has no effect). + */ + @Override + public void setBuffer(InputBuffer buffer) { + // NOOP since this filter will be providing the request body + } + + /** + * Amount of bytes still available in a buffer. + */ + @Override + public int available() { + return input.getLength(); + } + + /** + * End the current request (has no effect). + */ + @Override + public long end() throws IOException { + return 0; + } + + @Override + public boolean isFinished() { + return input.getOffset() >= input.getEnd(); + } +} diff --git a/java/org/apache/coyote/http11/filters/VoidInputFilter.java b/java/org/apache/coyote/http11/filters/VoidInputFilter.java new file mode 100644 index 0000000..0784d7b --- /dev/null +++ b/java/org/apache/coyote/http11/filters/VoidInputFilter.java @@ -0,0 +1,123 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.coyote.InputBuffer; +import org.apache.coyote.Request; +import org.apache.coyote.http11.InputFilter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.ApplicationBufferHandler; + +/** + * Void input filter, which returns -1 when attempting a read. Used with a GET, + * HEAD, or a similar request. + * + * @author Remy Maucherat + */ +public class VoidInputFilter implements InputFilter { + + + // -------------------------------------------------------------- Constants + + protected static final String ENCODING_NAME = "void"; + protected static final ByteChunk ENCODING = new ByteChunk(); + + + // ----------------------------------------------------- Static Initializer + + static { + ENCODING.setBytes(ENCODING_NAME.getBytes(StandardCharsets.ISO_8859_1), + 0, ENCODING_NAME.length()); + } + + + // ---------------------------------------------------- InputBuffer Methods + + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + return -1; + } + + + // ---------------------------------------------------- InputFilter Methods + + /** + * Set the associated request. + */ + @Override + public void setRequest(Request request) { + // NOOP: Request isn't used so ignore it + } + + + /** + * Set the next buffer in the filter pipeline. + */ + @Override + public void setBuffer(InputBuffer buffer) { + // NOOP: No body to read + } + + + /** + * Make the filter ready to process the next request. + */ + @Override + public void recycle() { + // NOOP + } + + + /** + * Return the name of the associated encoding; Here, the value is + * "void". + */ + @Override + public ByteChunk getEncodingName() { + return ENCODING; + } + + + /** + * End the current request. It is acceptable to write extra bytes using + * buffer.doWrite during the execution of this method. + * + * @return Should return 0 unless the filter does some content length + * delimitation, in which case the number is the amount of extra bytes or + * missing bytes, which would indicate an error. + * Note: It is recommended that extra bytes be swallowed by the filter. + */ + @Override + public long end() throws IOException { + return 0; + } + + + @Override + public int available() { + return 0; + } + + + @Override + public boolean isFinished() { + return true; + } +} diff --git a/java/org/apache/coyote/http11/filters/VoidOutputFilter.java b/java/org/apache/coyote/http11/filters/VoidOutputFilter.java new file mode 100644 index 0000000..edf38f9 --- /dev/null +++ b/java/org/apache/coyote/http11/filters/VoidOutputFilter.java @@ -0,0 +1,81 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.coyote.Response; +import org.apache.coyote.http11.HttpOutputBuffer; +import org.apache.coyote.http11.OutputFilter; + +/** + * Void output filter, which silently swallows bytes written. Used with a 204 + * status (no content) or a HEAD request. + * + * @author Remy Maucherat + */ +public class VoidOutputFilter implements OutputFilter { + + private HttpOutputBuffer buffer = null; + + + // --------------------------------------------------- OutputBuffer Methods + + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + return chunk.remaining(); + } + + + @Override + public long getBytesWritten() { + return 0; + } + + + // --------------------------------------------------- OutputFilter Methods + + @Override + public void setResponse(Response response) { + // NOOP: No need for parameters from response in this filter + } + + + @Override + public void setBuffer(HttpOutputBuffer buffer) { + this.buffer = buffer; + } + + + @Override + public void flush() throws IOException { + this.buffer.flush(); + } + + + @Override + public void recycle() { + buffer = null; + } + + + @Override + public void end() throws IOException { + buffer.end(); + } +} diff --git a/java/org/apache/coyote/http11/upgrade/InternalHttpUpgradeHandler.java b/java/org/apache/coyote/http11/upgrade/InternalHttpUpgradeHandler.java new file mode 100644 index 0000000..3151d95 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/InternalHttpUpgradeHandler.java @@ -0,0 +1,50 @@ +/* + * 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.coyote.http11.upgrade; + +import jakarta.servlet.http.HttpUpgradeHandler; + +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; + + +/** + * This Tomcat specific interface is implemented by handlers that require direct + * access to Tomcat's I/O layer rather than going through the Servlet API. + */ +public interface InternalHttpUpgradeHandler extends HttpUpgradeHandler { + + SocketState upgradeDispatch(SocketEvent status); + + void timeoutAsync(long now); + + void setSocketWrapper(SocketWrapperBase wrapper); + + void setSslSupport(SSLSupport sslSupport); + + void pause(); + + default boolean hasAsyncIO() { + return false; + } + + default UpgradeInfo getUpgradeInfo() { + return null; + } +} \ No newline at end of file diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings.properties new file mode 100644 index 0000000..2f0516f --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings.properties @@ -0,0 +1,36 @@ +# 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. + +upgrade.sis.errorCloseFail=Failed to close InputStream cleanly after a previous error +upgrade.sis.isFinished.ise=It is illegal to call isFinished() when the ServletInputStream is not in non-blocking mode (i.e. setReadListener() must be called first) +upgrade.sis.isReady.ise=It is illegal to call isReady() when the ServletInputStream is not in non-blocking mode (i.e. setReadListener() must be called first) +upgrade.sis.onErrorFail=onError processing for the registered ReadListener triggered this further error which was swallowed +upgrade.sis.read.closed=The InputStream has been closed +upgrade.sis.read.ise=It is illegal to call any of the read() methods in non-blocking mode without first checking that there is data available by calling isReady() +upgrade.sis.readListener.null=It is illegal to pass null to setReadListener() +upgrade.sis.readListener.set=It is illegal to call setReadListener() more than once for the same upgraded connection +upgrade.sos.canWrite.ise=It is illegal to call canWrite() when the ServletOutputStream is not in non-blocking mode (i.e. setWriteListener() must be called first) +upgrade.sos.errorCloseFail=Failed to close OutputStream cleanly after a previous error +upgrade.sos.onErrorFail=onError processing for the registered WriteListener triggered this further error which was swallowed +upgrade.sos.write.closed=The OutputStream has been closed +upgrade.sos.write.ise=It is illegal to call any of the write() methods in non-blocking mode without first checking that there is space available by calling isReady() +upgrade.sos.writeListener.null=It is illegal to pass null to setWriteListener() +upgrade.sos.writeListener.set=It is illegal to call setWriteListener() more than once for the same upgraded connection + +upgradeProcessor.isCloseFail=Failed to close input stream associated with upgraded connection +upgradeProcessor.osCloseFail=Failed to close output stream associated with upgraded connection +upgradeProcessor.requiredClose=Closing upgraded connection due to closeRequired state of streams: Input [{0}], Output [{1}] +upgradeProcessor.stop=Closing upgraded connection as incoming socket status was STOP +upgradeProcessor.unexpectedState=Closing upgraded connection unexpectedly as incoming socket status was [{0}] diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings_cs.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings_cs.properties new file mode 100644 index 0000000..2d9a7dc --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings_cs.properties @@ -0,0 +1,20 @@ +# 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. + +upgrade.sis.read.closed=InputStream byl uzavÅ™en +upgrade.sis.readListener.set=Volání setReadListener() více než jednou je neplatné pro stejné aktualilzované spojené +upgrade.sos.writeListener.null=PÅ™edat null do setWriteListener() je neplatné + +upgradeProcessor.unexpectedState=NeoÄekávané uzavírání upgraded spojení z důvodu příchozího stavu [{0}] socketu diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings_de.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings_de.properties new file mode 100644 index 0000000..59d9123 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings_de.properties @@ -0,0 +1,18 @@ +# 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. + +upgrade.sis.read.closed=Der InputStream wurde geschlossen. +upgrade.sis.readListener.set=setReadListener() darf nicht mehr als einmal für eine Upgraded-Verbindung aufgerufen werden +upgrade.sos.writeListener.null=setWriteListener() darf nicht mit null aufgerufen werden diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings_es.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings_es.properties new file mode 100644 index 0000000..7450df6 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings_es.properties @@ -0,0 +1,20 @@ +# 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. + +upgrade.sis.read.closed=El InputStream ha sido cerrado. +upgrade.sis.readListener.set=Es ilegal llamar a setReadListener() más de una vez para la misma conexión actualizada\n +upgrade.sos.writeListener.null=Es ilegal pasar valor nulo a setWriteListener()\n + +upgradeProcessor.unexpectedState=Cerrando la conexión actualizada inesperadamente debido al que el estatus del socket fue [{0}]\n diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings_fr.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings_fr.properties new file mode 100644 index 0000000..be05453 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings_fr.properties @@ -0,0 +1,36 @@ +# 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. + +upgrade.sis.errorCloseFail=Impossible de fermer l'InputStream proprement après une précédente erreur +upgrade.sis.isFinished.ise=Il est illégal d'appeler isFinished() quand le ServletInputStream n'est pas en mode non bloquant, c'est à dire que setReadListener() doit d'abord être appelé +upgrade.sis.isReady.ise=il est illégal d'appeler isReady() quand le ServletInputStream n'est pas en mode non bloquant, c'est à dire que setReadListener() doit d'abord être appelé +upgrade.sis.onErrorFail=Le traitement de onError pour le ReadListener configuré a causé cette erreur qui a été avalée +upgrade.sis.read.closed=Le flux d'entrée (InputStream) a été fermé +upgrade.sis.read.ise=Il est interdit d'appeler une des méthodes read() en mode non bloquant avant de d'abord vérifier qu'il y a des données disponibles en utilisant isReady() +upgrade.sis.readListener.null=Il est illégal de passer un argument null à setReadListener() +upgrade.sis.readListener.set=Il est interdit d'appeler setReadListener() plus d'une fois pour une même connection upgradée +upgrade.sos.canWrite.ise=il est illégal d'appeler canWrite() quand le ServletOutputStream n'est pas en mode non bloquant, c'est à dire que setWriteListener() doit d'abord être appelé +upgrade.sos.errorCloseFail=Impossible de fermer l'OutputStream proprement après une précédente erreur +upgrade.sos.onErrorFail=Le traitement de onError pour le WriteListener configuré a causé cette erreur qui a été avalée +upgrade.sos.write.closed=L'OutputSteam a été fermée +upgrade.sos.write.ise=Il est interdit d'appeler une des méthodes write() en mode non bloquant avant de d'abord vérifier qu'il y a de la place disponible en utilisant isReady() +upgrade.sos.writeListener.null=Il est illégal de passer un argument null à setWriteListener() +upgrade.sos.writeListener.set=Il est interdit d'appeler setWriteListener() plus d'une fois pour une même connection upgradée + +upgradeProcessor.isCloseFail=Impossible de fermer l'InputStream associée avec la connection upgradée +upgradeProcessor.osCloseFail=Impossible de fermer l'OutputStream associée avec la connection upgradée +upgradeProcessor.requiredClose=Fermeture de la connection upgradée à cause de l''état du closeRequired des flux : Entrée [{0}] Sortie [{1}] +upgradeProcessor.stop=Fermeture de la connection upgradée car l'état du socket est STOP +upgradeProcessor.unexpectedState=Fermeture inattendue de la connection upgradée alors que le statut du socket en lecture est [{0}] diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings_ja.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings_ja.properties new file mode 100644 index 0000000..acf2bd7 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings_ja.properties @@ -0,0 +1,36 @@ +# 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. + +upgrade.sis.errorCloseFail=ç›´å‰ã®ã‚¨ãƒ©ãƒ¼ã®å¾Œã«å®Œå…¨ã«InputStreamã‚’é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +upgrade.sis.isFinished.ise=ServletInputStreamãŒéžãƒ–ロッキングモードã§ãªã„å ´åˆï¼ˆã¤ã¾ã‚Šã€æœ€åˆã«setReadListener()を呼ã³å‡ºã•ãªã‘ã‚Œã°ãªã‚‰ãªã„å ´åˆï¼‰isFinished()を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +upgrade.sis.isReady.ise=ServletInputStreamãŒéžãƒ–ロッキングモードã§ãªã„å ´åˆï¼ˆã¤ã¾ã‚Šã€æœ€åˆã«setReadListener()を呼ã³å‡ºã•ãªã‘ã‚Œã°ãªã‚‰ãªã„å ´åˆï¼‰isReady()を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +upgrade.sis.onErrorFail=onErrorã¯ç™»éŒ²ã•ã‚ŒãŸReadListenerãŒå¼•ãèµ·ã“ã—ãŸã•ã‚‰ãªã‚‹ã‚¨ãƒ©ãƒ¼ã‚’飲ã¿è¾¼ã‚“ã§å‡¦ç†ã—ã¦ã„ã¾ã™ +upgrade.sis.read.closed=InputStream ã¯ã™ã§ã«åˆ‡æ–­ã•ã‚Œã¦ã„ã¾ã™ã€‚ +upgrade.sis.read.ise=isReady()を呼ã³å‡ºã™ã“ã¨ã«ã‚ˆã£ã¦åˆ©ç”¨å¯èƒ½ãªãƒ‡ãƒ¼ã‚¿ãŒã‚ã‚‹ã“ã¨ã‚’最åˆã«ãƒã‚§ãƒƒã‚¯ã™ã‚‹ã“ã¨ãªãã€éžãƒ–ロックモードã§read()メソッドを呼ã³å‡ºã™ã“ã¨ã¯ä¸æ­£ã§ã™ã€‚ +upgrade.sis.readListener.null=setReadListener() ã«ã¯ null を指定ã§ãã¾ã›ã‚“。 +upgrade.sis.readListener.set=アップグレードã—ãŸã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã«ä½•åº¦ã‚‚ setReadListener() を呼ã³å‡ºã™ã®ã¯ä¸æ­£ãªæ“作ã§ã™ã€‚ +upgrade.sos.canWrite.ise=ServletOutputStreamãŒéžãƒ–ロッキングモードã§ãªã„å ´åˆï¼ˆã¤ã¾ã‚Šã€setWriteListener()を最åˆã«å‘¼ã³å‡ºã•ãªã‘ã‚Œã°ãªã‚‰ãªã„å ´åˆï¼‰ã€canWrite()を呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +upgrade.sos.errorCloseFail=以å‰ã®ã‚¨ãƒ©ãƒ¼ã®å¾Œã«OutputStreamã‚’ãã‚Œã„ã«é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +upgrade.sos.onErrorFail=onErrorã¯ç™»éŒ²ã•ã‚ŒãŸWriteListenerãŒå¼•ãèµ·ã“ã—ãŸã•ã‚‰ãªã‚‹ã‚¨ãƒ©ãƒ¼ã‚’飲ã¿è¾¼ã‚“ã§å‡¦ç†ã—ã¦ã„ã¾ã™ +upgrade.sos.write.closed=OutputStreamã¯ã‚¯ãƒ­ãƒ¼ã‚ºã•ã‚Œã¾ã—㟠+upgrade.sos.write.ise=ノンブロッキングモードã§ã¯åˆã‚ã« isReady() を呼ã³å‡ºã—ã¦åˆ©ç”¨å¯èƒ½ãªé ˜åŸŸãŒã‚ã‚‹ã“ã¨ã‚’確ã‹ã‚ãªã‘ã‚Œã°ã€ã‚らゆる write() メソッドã®å‘¼ã³å‡ºã—ã¯ä¸æ­£ã«ãªã‚Šã¾ã™ã€‚ +upgrade.sos.writeListener.null=setWriteListener() ã« null を渡ã™ã®ã¯ä¸æ­£ãªæ“作ã§ã™ã€‚ +upgrade.sos.writeListener.set=åŒã˜ã‚¢ãƒƒãƒ—グレードã•ã‚ŒãŸã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã«å¯¾ã—ã¦setWriteListener()を複数回呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 + +upgradeProcessor.isCloseFail=アップグレードã•ã‚ŒãŸã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã«é–¢é€£ä»˜ã‘られãŸå…¥åŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +upgradeProcessor.osCloseFail=アップグレードã•ã‚ŒãŸæŽ¥ç¶šã«é–¢é€£ä»˜ã‘られãŸå‡ºåŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—㟠+upgradeProcessor.requiredClose=ストリームãŒcloseRequired状態ã«ãªã£ãŸãŸã‚ã€ã‚¢ãƒƒãƒ—グレードã•ã‚ŒãŸæŽ¥ç¶šã‚’クローズã—ã¦ã„ã¾ã™ï¼šå…¥åŠ›[{0}]ã€å‡ºåŠ›[{1}] +upgradeProcessor.stop=ç€ä¿¡ã‚½ã‚±ãƒƒãƒˆã®çŠ¶æ…‹ãŒSTOPã ã£ãŸã®ã§ã‚¢ãƒƒãƒ—グレードã•ã‚ŒãŸã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã‚’é–‰ã˜ã¦ã„ã¾ã™ã€‚ +upgradeProcessor.unexpectedState=ソケットã®çŠ¶æ…‹ã¯ [{0}] ã§ã—ãŸãŒã‚¢ãƒƒãƒ—グレードã—ãŸã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã¯äºˆæœŸã›ã¬ç†ç”±ã§åˆ‡æ–­ã—ã¾ã—㟠diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings_ko.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings_ko.properties new file mode 100644 index 0000000..043b197 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings_ko.properties @@ -0,0 +1,36 @@ +# 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. + +upgrade.sis.errorCloseFail=ì´ì „ 오류 ë°œìƒ ì´í›„, InputStreamì„ ê¹¨ë—하게 닫지 못했습니다. +upgrade.sis.isFinished.ise=ServletInputStreamì´ non-blocking 모드 ì•ˆì— ìžˆì§€ ì•Šì„ ë•Œ, isFinished()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (즉, setReadListener()ê°€ 먼저 호출ë˜ì–´ì•¼ë§Œ 합니다.) +upgrade.sis.isReady.ise=ServletInputStreamì´ non-blocking 모드 ì•ˆì— ìžˆì§€ ì•Šì„ ë•Œ, isReady()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (즉 setReadListener()ê°€ 반드시 먼저 호출ë˜ì–´ì•¼ 합니다.) +upgrade.sis.onErrorFail=등ë¡ëœ ReadListener를 위한 onError 처리가 ë”ë§Žì€ ì˜¤ë¥˜ë¥¼ 유발시켰습니다. ì´ ì¶”ê°€ 오류는 별ë„ë¡œ 표출ë˜ì§€ 않습니다. +upgrade.sis.read.closed=InputStreamì€ ì´ë¯¸ 닫혀 있습니다. +upgrade.sis.read.ise=Non-blocking 모드ì—서는, 먼저 isReady()를 호출하여 가용한 ë°ì´í„°ê°€ 있는지 여부를 ì ê²€í•˜ì§€ ì•Šì€ ìƒíƒœì—ì„œ, ì–´ë–¤ ì¢…ë¥˜ì˜ read() 메소드ë¼ë„ 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +upgrade.sis.readListener.null=setReadListener()ì— ë„ì„ ë„˜ê¸°ëŠ” ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +upgrade.sis.readListener.set=업그레ì´ë“œëœ ë™ì¼í•œ ì—°ê²°ì„ ìœ„í•´, setReadListener()를 ë‘ ë²ˆ ì´ìƒ 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +upgrade.sos.canWrite.ise=ServletOutputStreamì´ non-blocking 모드 ì•ˆì— ìžˆì§€ ì•Šì„ ë•Œ, canWrite()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (즉 setWriteListener()ê°€ 반드시 먼저 호출ë˜ì–´ì•¼ 합니다.) +upgrade.sos.errorCloseFail=ì´ì „ 오류 ë°œìƒ ì´í›„, OutputStreamì„ ê¹¨ë—하게 닫지 못했습니다. +upgrade.sos.onErrorFail=등ë¡ëœ WriteListener를 위한 onError 처리가 ì´ ì˜¤ë¥˜ë¥¼ ë” ìœ ë°œì‹œì¼°ìŠµë‹ˆë‹¤. ì´ ì¶”ê°€ 오류는 별ë„ë¡œ 표출ë˜ì§€ 않습니다. +upgrade.sos.write.closed=OutputStreamì´ ì´ë¯¸ 닫혀 있습니다. +upgrade.sos.write.ise=Non-blocking 모드ì—서는, 먼저 isReady()를 호출하여 ê³µê°„ì´ ë‚¨ì•„ìžˆëŠ”ì§€ ì ê²€í•˜ì§€ ì•Šê³ , ì–´ë–¤ ì¢…ë¥˜ì˜ write() ë©”ì†Œë“œë“¤ì„ í˜¸ì¶œí•˜ëŠ” ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +upgrade.sos.writeListener.null=setWriteListener() 호출 ì‹œ, ë„ì„ ë„˜ê¸°ëŠ” ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +upgrade.sos.writeListener.set=ë™ì¼í•œ 업그레ì´ë“œëœ ì—°ê²°ì— ëŒ€í•˜ì—¬, setWriteListener()를 ë‘번 ì´ìƒ 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. + +upgradeProcessor.isCloseFail=업그레ì´ë“œëœ ì—°ê²°ê³¼ ì—°ê´€ëœ ìž…ë ¥ ìŠ¤íŠ¸ë¦¼ì„ ë‹«ì§€ 못했습니다. +upgradeProcessor.osCloseFail=업그레ì´ë“œëœ ì—°ê²°ê³¼ ì—°ê´€ëœ ì¶œë ¥ ìŠ¤íŠ¸ë¦¼ì„ ë‹«ì§€ 못했습니다. +upgradeProcessor.requiredClose=ìŠ¤íŠ¸ë¦¼ë“¤ì˜ closeRequired ìƒíƒœë¡œ ì¸í•˜ì—¬, 업그레ì´ë“œëœ ì—°ê²°ì„ ë‹«ìŠµë‹ˆë‹¤: ìž…ë ¥ [{0}], 출력 [{1}] +upgradeProcessor.stop=Incoming ì†Œì¼“ì˜ ìƒíƒœê°€ STOPìž„ì— ë”°ë¼, 업그레ì´ë“œëœ ì—°ê²°ì„ ë‹«ìŠµë‹ˆë‹¤. +upgradeProcessor.unexpectedState=Incoming ì†Œì¼“ì˜ ìƒíƒœê°€ [{0}](ì´)ë¼ì„œ, 업그레ì´ë“œëœ ì—°ê²°ì„ ì˜ˆê¸°ì¹˜ 않게 종료합니다. diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings_ru.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings_ru.properties new file mode 100644 index 0000000..3db96af --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings_ru.properties @@ -0,0 +1,18 @@ +# 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. + +upgrade.sis.read.closed=InputStream был закрыт +upgrade.sis.readListener.set=Ðекорректно вызывать setReadListener() более одного раза Ð´Ð»Ñ Ð¾Ð´Ð½Ð¾Ð³Ð¾ и того же обновленного ÑÐ¾ÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ +upgrade.sos.writeListener.null=ЗапрещаетÑÑ Ð¿ÐµÑ€ÐµÐ´Ð°Ð²Ð°Ñ‚ÑŒ null в setWriteListener() diff --git a/java/org/apache/coyote/http11/upgrade/LocalStrings_zh_CN.properties b/java/org/apache/coyote/http11/upgrade/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..358d8e4 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/LocalStrings_zh_CN.properties @@ -0,0 +1,36 @@ +# 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. + +upgrade.sis.errorCloseFail=在上一个错误之åŽæ— æ³•å¹²å‡€åœ°å…³é—­InputStream +upgrade.sis.isFinished.ise=当 ServletInputStream ä¸å¤„于éžé˜»å¡žæ¨¡å¼æ—¶è°ƒç”¨ isFinished() 是éžæ³•çš„(å³å¿…须首先调用 setReadListener()) +upgrade.sis.isReady.ise=当ServletInputStream未处于éžé˜»å¡žæ¨¡å¼æ—¶ï¼Œè°ƒç”¨isReady()是éžæ³•çš„(å³å¿…须首先调用setReadListener()) +upgrade.sis.onErrorFail=对注册的readlistener的错误处ç†è§¦å‘了这个进一步的错误,该错误被åžå…¥ +upgrade.sis.read.closed=InputStream 已被关闭 +upgrade.sis.read.ise=在éžé˜»å¡žæ¨¡å¼ä¸‹è°ƒç”¨ä»»ä½•read()方法而ä¸é¦–先通过调用isready()检查是å¦æœ‰å¯ç”¨çš„æ•°æ®æ˜¯éžæ³•çš„ +upgrade.sis.readListener.null=å‘setReadListener()传递null是éžæ³•çš„ +upgrade.sis.readListener.set=在åŒä¸€ä¸ªupgraded连接上调用多次setReadListener()函数是éžæ³•çš„ +upgrade.sos.canWrite.ise=当ServletOutputStream未处于éžé˜»å¡žæ¨¡å¼æ—¶ï¼Œè°ƒç”¨canWrite()是éžæ³•çš„(å³å¿…须首先调用setWriteListener) +upgrade.sos.errorCloseFail=在上一个错误之åŽæ— æ³•å¹²å‡€åœ°å…³é—­OutputStream +upgrade.sos.onErrorFail=对注册的WriteListener 的错误处ç†è§¦å‘了这个进一步的错误,该错误被åžå…¥ +upgrade.sos.write.closed=输出æµå·²è¢«å…³é—­ +upgrade.sos.write.ise=在éžé˜»å¡žæ¨¡å¼ä¸‹è°ƒç”¨ä»»ä½•å†™()方法都是éžæ³•çš„,而无需首先检查是å¦æœ‰å¯ç”¨çš„空间,åªéœ€è°ƒç”¨isreadi() +upgrade.sos.writeListener.null=对setWriteListener()传递null是éžæ³•çš„ +upgrade.sos.writeListener.set=对于åŒä¸€ä¸ªå‡çº§çš„连接,多次调用setWriteListener()是éžæ³•çš„ + +upgradeProcessor.isCloseFail=无法关闭与å‡çº§è¿žæŽ¥å…³è”çš„è¾“å…¥æµ +upgradeProcessor.osCloseFail=无法关闭与å‡çº§è¿žæŽ¥å…³è”的输出æµã€‚ +upgradeProcessor.requiredClose=由于æµçš„closeRequired状æ€ï¼Œå› æ­¤å…³é—­å‡çº§çš„连接:输入[{0}],输出[{1}] +upgradeProcessor.stop=正在关闭å‡çº§çš„连接,因为传入的套接字状æ€ä¸ºâ€œåœæ­¢â€ã€‚ +upgradeProcessor.unexpectedState=因传入套接字状æ€ä¸º[{0}]而æ„外关闭å‡çº§è¿žæŽ¥ diff --git a/java/org/apache/coyote/http11/upgrade/UpgradeApplicationBufferHandler.java b/java/org/apache/coyote/http11/upgrade/UpgradeApplicationBufferHandler.java new file mode 100644 index 0000000..b551aab --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/UpgradeApplicationBufferHandler.java @@ -0,0 +1,45 @@ +/* + * 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.coyote.http11.upgrade; + +import java.nio.ByteBuffer; + +import org.apache.tomcat.util.net.ApplicationBufferHandler; + +/** + * Trivial implementation of {@link ApplicationBufferHandler} to support saving + * of HTTP request bodies during an HTTP/1.1 upgrade. + */ +public class UpgradeApplicationBufferHandler implements ApplicationBufferHandler { + + private ByteBuffer byteBuffer; + + @Override + public void setByteBuffer(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + } + + @Override + public ByteBuffer getByteBuffer() { + return byteBuffer; + } + + @Override + public void expand(int size) { + // NO-OP + } +} diff --git a/java/org/apache/coyote/http11/upgrade/UpgradeGroupInfo.java b/java/org/apache/coyote/http11/upgrade/UpgradeGroupInfo.java new file mode 100644 index 0000000..638d0d2 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/UpgradeGroupInfo.java @@ -0,0 +1,133 @@ +/* + * 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.coyote.http11.upgrade; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; + +import org.apache.tomcat.util.modeler.BaseModelMBean; + +/** + * This aggregates the data collected from each UpgradeInfo instance. + */ +public class UpgradeGroupInfo extends BaseModelMBean { + + private final Set upgradeInfos = (new ConcurrentHashMap()).keySet(Boolean.TRUE); + + private LongAdder deadBytesReceived = new LongAdder(); + private LongAdder deadBytesSent = new LongAdder(); + private LongAdder deadMsgsReceived = new LongAdder(); + private LongAdder deadMsgsSent = new LongAdder(); + + + public void addUpgradeInfo(UpgradeInfo ui) { + upgradeInfos.add(ui); + } + + + public void removeUpgradeInfo(UpgradeInfo ui) { + if (ui != null) { + deadBytesReceived.add(ui.getBytesReceived()); + deadBytesSent.add(ui.getBytesSent()); + deadMsgsReceived.add(ui.getMsgsReceived()); + deadMsgsSent.add(ui.getMsgsSent()); + + upgradeInfos.remove(ui); + } + } + + + public long getBytesReceived() { + long bytes = deadBytesReceived.longValue(); + for (UpgradeInfo ui : upgradeInfos) { + bytes += ui.getBytesReceived(); + } + return bytes; + } + public void setBytesReceived(long bytesReceived) { + deadBytesReceived.reset(); + if (bytesReceived != 0) { + deadBytesReceived.add(bytesReceived); + } + for (UpgradeInfo ui : upgradeInfos) { + ui.setBytesReceived(bytesReceived); + } + } + + + public long getBytesSent() { + long bytes = deadBytesSent.longValue(); + for (UpgradeInfo ui : upgradeInfos) { + bytes += ui.getBytesSent(); + } + return bytes; + } + public void setBytesSent(long bytesSent) { + deadBytesSent.reset(); + if (bytesSent != 0) { + deadBytesSent.add(bytesSent); + } + for (UpgradeInfo ui : upgradeInfos) { + ui.setBytesSent(bytesSent); + } + } + + + public long getMsgsReceived() { + long msgs = deadMsgsReceived.longValue(); + for (UpgradeInfo ui : upgradeInfos) { + msgs += ui.getMsgsReceived(); + } + return msgs; + } + public void setMsgsReceived(long msgsReceived) { + deadMsgsReceived.reset(); + if (msgsReceived != 0) { + deadMsgsReceived.add(msgsReceived); + } + for (UpgradeInfo ui : upgradeInfos) { + ui.setMsgsReceived(msgsReceived); + } + } + + + public long getMsgsSent() { + long msgs = deadMsgsSent.longValue(); + for (UpgradeInfo ui : upgradeInfos) { + msgs += ui.getMsgsSent(); + } + return msgs; + } + public void setMsgsSent(long msgsSent) { + deadMsgsSent.reset(); + if (msgsSent != 0) { + deadMsgsSent.add(msgsSent); + } + for (UpgradeInfo ui : upgradeInfos) { + ui.setMsgsSent(msgsSent); + } + } + + + public void resetCounters() { + setBytesReceived(0); + setBytesSent(0); + setMsgsReceived(0); + setMsgsSent(0); + } +} diff --git a/java/org/apache/coyote/http11/upgrade/UpgradeInfo.java b/java/org/apache/coyote/http11/upgrade/UpgradeInfo.java new file mode 100644 index 0000000..eb3313c --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/UpgradeInfo.java @@ -0,0 +1,96 @@ +/* + * 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.coyote.http11.upgrade; + +/** + * Structure to hold statistical information about connections that have been + * established using the HTTP/1.1 upgrade mechanism. Bytes sent/received will + * always be populated. Messages sent/received will be populated if that makes + * sense for the protocol and the information is exposed by the protocol + * implementation. + */ +public class UpgradeInfo { + + private UpgradeGroupInfo groupInfo = null; + private volatile long bytesSent = 0; + private volatile long bytesReceived = 0; + private volatile long msgsSent = 0; + private volatile long msgsReceived = 0; + + + + public UpgradeGroupInfo getGlobalProcessor() { + return groupInfo; + } + + + public void setGroupInfo(UpgradeGroupInfo groupInfo) { + if (groupInfo == null) { + if (this.groupInfo != null) { + this.groupInfo.removeUpgradeInfo(this); + this.groupInfo = null; + } + } else { + this.groupInfo = groupInfo; + groupInfo.addUpgradeInfo(this); + } + } + + + public long getBytesSent() { + return bytesSent; + } + public void setBytesSent(long bytesSent) { + this.bytesSent = bytesSent; + } + public void addBytesSent(long bytesSent) { + this.bytesSent += bytesSent; + } + + + public long getBytesReceived() { + return bytesReceived; + } + public void setBytesReceived(long bytesReceived) { + this.bytesReceived = bytesReceived; + } + public void addBytesReceived(long bytesReceived) { + this.bytesReceived += bytesReceived; + } + + + public long getMsgsSent() { + return msgsSent; + } + public void setMsgsSent(long msgsSent) { + this.msgsSent = msgsSent; + } + public void addMsgsSent(long msgsSent) { + this.msgsSent += msgsSent; + } + + + public long getMsgsReceived() { + return msgsReceived; + } + public void setMsgsReceived(long msgsReceived) { + this.msgsReceived = msgsReceived; + } + public void addMsgsReceived(long msgsReceived) { + this.msgsReceived += msgsReceived; + } +} diff --git a/java/org/apache/coyote/http11/upgrade/UpgradeProcessorBase.java b/java/org/apache/coyote/http11/upgrade/UpgradeProcessorBase.java new file mode 100644 index 0000000..01923d7 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/UpgradeProcessorBase.java @@ -0,0 +1,105 @@ +/* + * 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.coyote.http11.upgrade; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jakarta.servlet.http.WebConnection; + +import org.apache.coyote.AbstractProcessorLight; +import org.apache.coyote.Request; +import org.apache.coyote.UpgradeToken; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SocketWrapperBase; + +public abstract class UpgradeProcessorBase extends AbstractProcessorLight implements WebConnection { + + protected static final int INFINITE_TIMEOUT = -1; + + private final UpgradeToken upgradeToken; + + public UpgradeProcessorBase(UpgradeToken upgradeToken) { + this.upgradeToken = upgradeToken; + } + + + // ------------------------------------------- Implemented Processor methods + + @Override + public final boolean isUpgrade() { + return true; + } + + + @Override + public UpgradeToken getUpgradeToken() { + return upgradeToken; + } + + + @Override + public final void recycle() { + // Currently a NO-OP as upgrade processors are not recycled. + } + + + // ---------------------------- Processor methods that are NO-OP for upgrade + + @Override + public final SocketState service(SocketWrapperBase socketWrapper) throws IOException { + return null; + } + + + @Override + public final SocketState asyncPostProcess() { + return null; + } + + + @Override + public final boolean isAsync() { + return false; + } + + + @Override + public final Request getRequest() { + return null; + } + + + @Override + public ByteBuffer getLeftoverInput() { + return null; + } + + + @Override + public boolean checkAsyncTimeoutGeneration() { + return false; + } + + + // ----------------- Processor methods that are NO-OP by default for upgrade + + @Override + public void timeoutAsync(long now) { + // NO-OP + } +} diff --git a/java/org/apache/coyote/http11/upgrade/UpgradeProcessorExternal.java b/java/org/apache/coyote/http11/upgrade/UpgradeProcessorExternal.java new file mode 100644 index 0000000..f2cdf95 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/UpgradeProcessorExternal.java @@ -0,0 +1,145 @@ +/* + * 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.coyote.http11.upgrade; + +import java.io.IOException; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; + +import org.apache.coyote.UpgradeToken; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +public class UpgradeProcessorExternal extends UpgradeProcessorBase { + + private static final Log log = LogFactory.getLog(UpgradeProcessorExternal.class); + private static final StringManager sm = StringManager.getManager(UpgradeProcessorExternal.class); + + private final UpgradeServletInputStream upgradeServletInputStream; + private final UpgradeServletOutputStream upgradeServletOutputStream; + private final UpgradeInfo upgradeInfo; + + public UpgradeProcessorExternal(SocketWrapperBase wrapper, UpgradeToken upgradeToken, + UpgradeGroupInfo upgradeGroupInfo) { + super(upgradeToken); + this.upgradeInfo = new UpgradeInfo(); + if (upgradeGroupInfo != null) { + upgradeGroupInfo.addUpgradeInfo(upgradeInfo); + } + this.upgradeServletInputStream = new UpgradeServletInputStream(this, wrapper, upgradeInfo); + this.upgradeServletOutputStream = new UpgradeServletOutputStream(this, wrapper, upgradeInfo); + + /* + * Leave timeouts in the hands of the upgraded protocol. + */ + wrapper.setReadTimeout(INFINITE_TIMEOUT); + wrapper.setWriteTimeout(INFINITE_TIMEOUT); + } + + + @Override + protected Log getLog() { + return log; + } + + + // --------------------------------------------------- AutoCloseable methods + + @Override + public void close() throws Exception { + upgradeServletInputStream.close(); + upgradeServletOutputStream.close(); + // Triggers update of stats from UpgradeInfo to UpgradeGroupInfo + upgradeInfo.setGroupInfo(null); + } + + + // --------------------------------------------------- WebConnection methods + + @Override + public ServletInputStream getInputStream() throws IOException { + return upgradeServletInputStream; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + return upgradeServletOutputStream; + } + + + // ------------------------------------------- Implemented Processor methods + + @Override + public final SocketState dispatch(SocketEvent status) { + if (status == SocketEvent.OPEN_READ) { + upgradeServletInputStream.onDataAvailable(); + } else if (status == SocketEvent.OPEN_WRITE) { + upgradeServletOutputStream.onWritePossible(); + } else if (status == SocketEvent.STOP) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeProcessor.stop")); + } + try { + upgradeServletInputStream.close(); + } catch (IOException ioe) { + log.debug(sm.getString("upgradeProcessor.isCloseFail", ioe)); + } + try { + upgradeServletOutputStream.close(); + } catch (IOException ioe) { + log.debug(sm.getString("upgradeProcessor.osCloseFail", ioe)); + } + return SocketState.CLOSED; + } else { + // Unexpected state + if (log.isDebugEnabled()) { + log.debug(sm.getString("upgradeProcessor.unexpectedState")); + } + return SocketState.CLOSED; + } + if (upgradeServletInputStream.isClosed() && + upgradeServletOutputStream.isClosed()) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeProcessor.requiredClose", + Boolean.valueOf(upgradeServletInputStream.isClosed()), + Boolean.valueOf(upgradeServletOutputStream.isClosed()))); + } + return SocketState.CLOSED; + } + return SocketState.UPGRADED; + } + + + // ----------------------------------------- Unimplemented Processor methods + + @Override + public final void setSslSupport(SSLSupport sslSupport) { + // NO-OP + } + + + @Override + public void pause() { + // NOOP + } +} diff --git a/java/org/apache/coyote/http11/upgrade/UpgradeProcessorInternal.java b/java/org/apache/coyote/http11/upgrade/UpgradeProcessorInternal.java new file mode 100644 index 0000000..99c8508 --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/UpgradeProcessorInternal.java @@ -0,0 +1,116 @@ +/* + * 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.coyote.http11.upgrade; + +import java.io.IOException; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; + +import org.apache.coyote.UpgradeToken; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; + +public class UpgradeProcessorInternal extends UpgradeProcessorBase { + + private static final Log log = LogFactory.getLog(UpgradeProcessorInternal.class); + + private final InternalHttpUpgradeHandler internalHttpUpgradeHandler; + + public UpgradeProcessorInternal(SocketWrapperBase wrapper, UpgradeToken upgradeToken, + UpgradeGroupInfo upgradeGroupInfo) { + super(upgradeToken); + this.internalHttpUpgradeHandler = (InternalHttpUpgradeHandler) upgradeToken.getHttpUpgradeHandler(); + /* + * Leave timeouts in the hands of the upgraded protocol. + */ + wrapper.setReadTimeout(INFINITE_TIMEOUT); + wrapper.setWriteTimeout(INFINITE_TIMEOUT); + + internalHttpUpgradeHandler.setSocketWrapper(wrapper); + + // HTTP/2 uses RequestInfo objects so does not provide upgradeInfo + UpgradeInfo upgradeInfo = internalHttpUpgradeHandler.getUpgradeInfo(); + if (upgradeInfo != null && upgradeGroupInfo != null) { + upgradeInfo.setGroupInfo(upgradeGroupInfo); + } + } + + + @Override + public SocketState dispatch(SocketEvent status) { + return internalHttpUpgradeHandler.upgradeDispatch(status); + } + + + @Override + public final void setSslSupport(SSLSupport sslSupport) { + internalHttpUpgradeHandler.setSslSupport(sslSupport); + } + + + @Override + public void pause() { + internalHttpUpgradeHandler.pause(); + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + public void timeoutAsync(long now) { + internalHttpUpgradeHandler.timeoutAsync(now); + } + + + public boolean hasAsyncIO() { + return internalHttpUpgradeHandler.hasAsyncIO(); + } + + + // --------------------------------------------------- AutoCloseable methods + + @Override + public void close() throws Exception { + UpgradeInfo upgradeInfo = internalHttpUpgradeHandler.getUpgradeInfo(); + if (upgradeInfo != null) { + upgradeInfo.setGroupInfo(null); + } + internalHttpUpgradeHandler.destroy(); + } + + + // --------------------------------------------------- WebConnection methods + + @Override + public ServletInputStream getInputStream() throws IOException { + return null; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + return null; + } +} diff --git a/java/org/apache/coyote/http11/upgrade/UpgradeServletInputStream.java b/java/org/apache/coyote/http11/upgrade/UpgradeServletInputStream.java new file mode 100644 index 0000000..643657c --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/UpgradeServletInputStream.java @@ -0,0 +1,270 @@ +/* + * 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.coyote.http11.upgrade; + +import java.io.IOException; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; + +import org.apache.coyote.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.net.DispatchType; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +public class UpgradeServletInputStream extends ServletInputStream { + + private static final Log log = LogFactory.getLog(UpgradeServletInputStream.class); + private static final StringManager sm = + StringManager.getManager(UpgradeServletInputStream.class); + + private final UpgradeProcessorBase processor; + private final SocketWrapperBase socketWrapper; + private final UpgradeInfo upgradeInfo; + + private volatile boolean closed = false; + private volatile boolean eof = false; + // Start in blocking-mode + private volatile Boolean ready = Boolean.TRUE; + private volatile ReadListener listener = null; + + + public UpgradeServletInputStream(UpgradeProcessorBase processor, SocketWrapperBase socketWrapper, + UpgradeInfo upgradeInfo) { + this.processor = processor; + this.socketWrapper = socketWrapper; + this.upgradeInfo = upgradeInfo; + } + + + @Override + public final boolean isFinished() { + if (listener == null) { + throw new IllegalStateException( + sm.getString("upgrade.sis.isFinished.ise")); + } + return eof; + } + + + @Override + public final boolean isReady() { + if (listener == null) { + throw new IllegalStateException( + sm.getString("upgrade.sis.isReady.ise")); + } + + if (eof || closed) { + return false; + } + + // If we already know the current state, return it. + if (ready != null) { + return ready.booleanValue(); + } + + try { + ready = Boolean.valueOf(socketWrapper.isReadyForRead()); + } catch (IOException e) { + onError(e); + } + return ready.booleanValue(); + } + + + @Override + public final void setReadListener(ReadListener listener) { + if (listener == null) { + throw new IllegalArgumentException( + sm.getString("upgrade.sis.readListener.null")); + } + if (this.listener != null) { + throw new IllegalArgumentException( + sm.getString("upgrade.sis.readListener.set")); + } + if (closed) { + throw new IllegalStateException(sm.getString("upgrade.sis.read.closed")); + } + + this.listener = listener; + + // Container is responsible for first call to onDataAvailable(). + Request request = processor.getRequest(); + if (request != null && request.isRequestThread()) { + processor.addDispatch(DispatchType.NON_BLOCKING_READ); + } else { + socketWrapper.registerReadInterest(); + } + + // Switching to non-blocking. Don't know if data is available. + ready = null; + } + + + @Override + public final int read() throws IOException { + preReadChecks(); + + return readInternal(); + } + + + @Override + public final int readLine(byte[] b, int off, int len) throws IOException { + preReadChecks(); + + if (len <= 0) { + return 0; + } + int count = 0, c; + + while ((c = readInternal()) != -1) { + b[off++] = (byte) c; + count++; + if (c == '\n' || count == len) { + break; + } + } + + if (count > 0) { + upgradeInfo.addBytesReceived(count); + return count; + } else { + return -1; + } + } + + + @Override + public final int read(byte[] b, int off, int len) throws IOException { + preReadChecks(); + + try { + int result = socketWrapper.read(listener == null, b, off, len); + if (result == -1) { + eof = true; + } else { + upgradeInfo.addBytesReceived(result); + } + return result; + } catch (IOException ioe) { + close(); + throw ioe; + } + } + + + + @Override + public void close() throws IOException { + eof = true; + closed = true; + } + + + private void preReadChecks() { + if (listener != null && (ready == null || !ready.booleanValue())) { + throw new IllegalStateException(sm.getString("upgrade.sis.read.ise")); + } + if (closed) { + throw new IllegalStateException(sm.getString("upgrade.sis.read.closed")); + } + // No longer know if data is available + ready = null; + } + + + private int readInternal() throws IOException { + // Single byte reads for non-blocking need special handling so all + // single byte reads run through this method. + byte[] b = new byte[1]; + int result; + try { + result = socketWrapper.read(listener == null, b, 0, 1); + } catch (IOException ioe) { + close(); + throw ioe; + } + if (result == 0) { + return -1; + } else if (result == -1) { + eof = true; + return -1; + } else { + upgradeInfo.addBytesReceived(1); + return b[0] & 0xFF; + } + } + + + final void onDataAvailable() { + try { + if (listener == null || !socketWrapper.isReadyForRead()) { + return; + } + } catch (IOException e) { + onError(e); + } + ready = Boolean.TRUE; + ClassLoader oldCL = processor.getUpgradeToken().getContextBind().bind(false, null); + try { + if (!eof) { + listener.onDataAvailable(); + } + if (eof) { + listener.onAllDataRead(); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + onError(t); + } finally { + processor.getUpgradeToken().getContextBind().unbind(false, oldCL); + } + } + + + private void onError(Throwable t) { + if (listener == null) { + return; + } + ClassLoader oldCL = processor.getUpgradeToken().getContextBind().bind(false, null); + try { + listener.onError(t); + } catch (Throwable t2) { + ExceptionUtils.handleThrowable(t2); + log.warn(sm.getString("upgrade.sis.onErrorFail"), t2); + } finally { + processor.getUpgradeToken().getContextBind().unbind(false, oldCL); + } + try { + close(); + } catch (IOException ioe) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("upgrade.sis.errorCloseFail"), ioe); + } + } + ready = Boolean.FALSE; + } + + + final boolean isClosed() { + return closed; + } +} diff --git a/java/org/apache/coyote/http11/upgrade/UpgradeServletOutputStream.java b/java/org/apache/coyote/http11/upgrade/UpgradeServletOutputStream.java new file mode 100644 index 0000000..fe8595a --- /dev/null +++ b/java/org/apache/coyote/http11/upgrade/UpgradeServletOutputStream.java @@ -0,0 +1,286 @@ +/* + * 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.coyote.http11.upgrade; + +import java.io.IOException; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; + +import org.apache.coyote.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.net.DispatchType; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +public class UpgradeServletOutputStream extends ServletOutputStream { + + private static final Log log = LogFactory.getLog(UpgradeServletOutputStream.class); + private static final StringManager sm = + StringManager.getManager(UpgradeServletOutputStream.class); + + private final UpgradeProcessorBase processor; + private final SocketWrapperBase socketWrapper; + private final UpgradeInfo upgradeInfo; + + // Used to ensure that isReady() and onWritePossible() have a consistent + // view of buffer and registered. + private final Object registeredLock = new Object(); + + // Used to ensure that only one thread writes to the socket at a time and + // that buffer is consistently updated with any unwritten data after the + // write. Note it is not necessary to hold this lock when checking if buffer + // contains data but, depending on how the result is used, some form of + // synchronization may be required (see fireListenerLock for an example). + private final Object writeLock = new Object(); + + private volatile boolean flushing = false; + + private volatile boolean closed = false; + + // Start in blocking-mode + private volatile WriteListener listener = null; + + // Guarded by registeredLock + private boolean registered = false; + + + + public UpgradeServletOutputStream(UpgradeProcessorBase processor, SocketWrapperBase socketWrapper, + UpgradeInfo upgradeInfo) { + this.processor = processor; + this.socketWrapper = socketWrapper; + this.upgradeInfo = upgradeInfo; + } + + + @Override + public final boolean isReady() { + if (listener == null) { + throw new IllegalStateException( + sm.getString("upgrade.sos.canWrite.ise")); + } + if (closed) { + return false; + } + + // Make sure isReady() and onWritePossible() have a consistent view of + // fireListener when determining if the listener should fire + synchronized (registeredLock) { + if (flushing) { + // Since flushing is true the socket must already be registered + // for write and multiple registrations will cause problems. + registered = true; + return false; + } else if (registered){ + // The socket is already registered for write and multiple + // registrations will cause problems. + return false; + } else { + boolean result = socketWrapper.isReadyForWrite(); + registered = !result; + return result; + } + } + } + + + @Override + public final void setWriteListener(WriteListener listener) { + if (listener == null) { + throw new IllegalArgumentException( + sm.getString("upgrade.sos.writeListener.null")); + } + if (this.listener != null) { + throw new IllegalArgumentException( + sm.getString("upgrade.sos.writeListener.set")); + } + if (closed) { + throw new IllegalStateException(sm.getString("upgrade.sos.write.closed")); + } + this.listener = listener; + // Container is responsible for first call to onWritePossible(). + synchronized (registeredLock) { + registered = true; + // Container is responsible for first call to onDataAvailable(). + Request request = processor.getRequest(); + if (request != null && request.isRequestThread()) { + processor.addDispatch(DispatchType.NON_BLOCKING_WRITE); + } else { + socketWrapper.registerWriteInterest(); + } + } + + } + + + final boolean isClosed() { + return closed; + } + + + @Override + public void write(int b) throws IOException { + synchronized (writeLock) { + preWriteChecks(); + writeInternal(new byte[] { (byte) b }, 0, 1); + } + } + + + @Override + public void write(byte[] b, int off, int len) throws IOException { + synchronized (writeLock) { + preWriteChecks(); + writeInternal(b, off, len); + } + } + + + @Override + public void flush() throws IOException { + preWriteChecks(); + flushInternal(listener == null, true); + } + + + private void flushInternal(boolean block, boolean updateFlushing) throws IOException { + try { + synchronized (writeLock) { + if (updateFlushing) { + flushing = socketWrapper.flush(block); + if (flushing) { + socketWrapper.registerWriteInterest(); + } + } else { + socketWrapper.flush(block); + } + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + onError(t); + if (t instanceof IOException) { + throw (IOException) t; + } else { + throw new IOException(t); + } + } + } + + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + flushInternal(listener == null, false); + } + + + private void preWriteChecks() { + if (listener != null && !socketWrapper.canWrite()) { + throw new IllegalStateException(sm.getString("upgrade.sos.write.ise")); + } + if (closed) { + throw new IllegalStateException(sm.getString("upgrade.sos.write.closed")); + } + } + + + /** + * Must hold writeLock to call this method. + */ + private void writeInternal(byte[] b, int off, int len) throws IOException { + if (listener == null) { + // Simple case - blocking IO + socketWrapper.write(true, b, off, len); + } else { + socketWrapper.write(false, b, off, len); + } + upgradeInfo.addBytesSent(len); + } + + + final void onWritePossible() { + try { + if (flushing) { + flushInternal(false, true); + if (flushing) { + return; + } + } else { + // This may fill the write buffer in which case the + // isReadyForWrite() call below will re-register the socket for + // write + flushInternal(false, false); + } + } catch (IOException ioe) { + onError(ioe); + return; + } + + // Make sure isReady() and onWritePossible() have a consistent view + // of buffer and fireListener when determining if the listener + // should fire + boolean fire = false; + synchronized (registeredLock) { + if (socketWrapper.isReadyForWrite()) { + registered = false; + fire = true; + } else { + registered = true; + } + } + + if (fire) { + ClassLoader oldCL = processor.getUpgradeToken().getContextBind().bind(false, null); + try { + listener.onWritePossible(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + onError(t); + } finally { + processor.getUpgradeToken().getContextBind().unbind(false, oldCL); + } + } + } + + + private void onError(Throwable t) { + if (listener == null) { + return; + } + ClassLoader oldCL = processor.getUpgradeToken().getContextBind().bind(false, null); + try { + listener.onError(t); + } catch (Throwable t2) { + ExceptionUtils.handleThrowable(t2); + log.warn(sm.getString("upgrade.sos.onErrorFail"), t2); + } finally { + processor.getUpgradeToken().getContextBind().unbind(false, oldCL); + } + try { + close(); + } catch (IOException ioe) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("upgrade.sos.errorCloseFail"), ioe); + } + } + } +} diff --git a/java/org/apache/coyote/http2/AbstractNonZeroStream.java b/java/org/apache/coyote/http2/AbstractNonZeroStream.java new file mode 100644 index 0000000..aae614f --- /dev/null +++ b/java/org/apache/coyote/http2/AbstractNonZeroStream.java @@ -0,0 +1,63 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +/** + * Base class for all streams other than stream 0, the connection. Primarily provides functionality shared between full + * Stream and RecycledStream. + */ +abstract class AbstractNonZeroStream extends AbstractStream { + + protected static final ByteBuffer ZERO_LENGTH_BYTEBUFFER = ByteBuffer.allocate(0); + + protected final StreamStateMachine state; + + + AbstractNonZeroStream(String connectionId, Integer identifier) { + super(identifier); + this.state = new StreamStateMachine(connectionId, getIdAsString()); + } + + + AbstractNonZeroStream(Integer identifier, StreamStateMachine state) { + super(identifier); + this.state = state; + } + + + final boolean isClosedFinal() { + return state.isClosedFinal(); + } + + + final void checkState(FrameType frameType) throws Http2Exception { + state.checkFrameType(frameType); + } + + + /** + * Obtain the ByteBuffer to store DATA frame payload data for this stream that has been received from the client. + * + * @return {@code null} if the DATA frame payload can be swallowed, or a ByteBuffer with at least enough space + * remaining for the current flow control window for stream data from the client. + */ + abstract ByteBuffer getInputByteBuffer(); + + abstract void receivedData(int payloadSize) throws Http2Exception; +} diff --git a/java/org/apache/coyote/http2/AbstractStream.java b/java/org/apache/coyote/http2/AbstractStream.java new file mode 100644 index 0000000..367e764 --- /dev/null +++ b/java/org/apache/coyote/http2/AbstractStream.java @@ -0,0 +1,165 @@ +/* + * 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.coyote.http2; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Base class for all streams including the connection (referred to as Stream 0) and is used primarily when managing + * prioritization. + */ +abstract class AbstractStream { + + private static final Log log = LogFactory.getLog(AbstractStream.class); + private static final StringManager sm = StringManager.getManager(AbstractStream.class); + + private final Integer identifier; + private final String idAsString; + + private long windowSize = ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE; + protected final Lock windowAllocationLock = new ReentrantLock(); + protected final Condition windowAllocationAvailable = windowAllocationLock.newCondition(); + + private volatile int connectionAllocationRequested = 0; + private volatile int connectionAllocationMade = 0; + + + AbstractStream(Integer identifier) { + this.identifier = identifier; + this.idAsString = identifier.toString(); + } + + + final Integer getIdentifier() { + return identifier; + } + + + final String getIdAsString() { + return idAsString; + } + + + final int getIdAsInt() { + return identifier.intValue(); + } + + + final void setWindowSize(long windowSize) { + windowAllocationLock.lock(); + try { + this.windowSize = windowSize; + } finally { + windowAllocationLock.unlock(); + } + } + + + final long getWindowSize() { + windowAllocationLock.lock(); + try { + return windowSize; + } finally { + windowAllocationLock.unlock(); + } + } + + + /** + * Increment window size. + * + * @param increment The amount by which the window size should be increased + * + * @throws Http2Exception If the window size is now higher than the maximum allowed + */ + void incrementWindowSize(int increment) throws Http2Exception { + windowAllocationLock.lock(); + try { + // No need for overflow protection here. + // Increment can't be more than Integer.MAX_VALUE and once windowSize + // goes beyond 2^31-1 an error is triggered. + windowSize += increment; + + if (log.isTraceEnabled()) { + log.trace(sm.getString("abstractStream.windowSizeInc", getConnectionId(), getIdAsString(), + Integer.toString(increment), Long.toString(windowSize))); + } + + if (windowSize > ConnectionSettingsBase.MAX_WINDOW_SIZE) { + String msg = sm.getString("abstractStream.windowSizeTooBig", getConnectionId(), identifier, + Integer.toString(increment), Long.toString(windowSize)); + if (identifier.intValue() == 0) { + throw new ConnectionException(msg, Http2Error.FLOW_CONTROL_ERROR); + } else { + throw new StreamException(msg, Http2Error.FLOW_CONTROL_ERROR, identifier.intValue()); + } + } + } finally { + windowAllocationLock.unlock(); + } + } + + + final void decrementWindowSize(int decrement) { + windowAllocationLock.lock(); + try { + // No need for overflow protection here. Decrement can never be larger + // the Integer.MAX_VALUE and once windowSize goes negative no further + // decrements are permitted + windowSize -= decrement; + if (log.isTraceEnabled()) { + log.trace(sm.getString("abstractStream.windowSizeDec", getConnectionId(), getIdAsString(), + Integer.toString(decrement), Long.toString(windowSize))); + } + } finally { + windowAllocationLock.unlock(); + } + } + + + final int getConnectionAllocationRequested() { + return connectionAllocationRequested; + } + + + final void setConnectionAllocationRequested(int connectionAllocationRequested) { + log.trace(sm.getString("abstractStream.setConnectionAllocationRequested", getConnectionId(), getIdAsString(), + Integer.toString(this.connectionAllocationRequested), Integer.toString(connectionAllocationRequested))); + this.connectionAllocationRequested = connectionAllocationRequested; + } + + + final int getConnectionAllocationMade() { + return connectionAllocationMade; + } + + + final void setConnectionAllocationMade(int connectionAllocationMade) { + log.trace(sm.getString("abstractStream.setConnectionAllocationMade", getConnectionId(), getIdAsString(), + Integer.toString(this.connectionAllocationMade), Integer.toString(connectionAllocationMade))); + this.connectionAllocationMade = connectionAllocationMade; + } + + + abstract String getConnectionId(); +} diff --git a/java/org/apache/coyote/http2/ByteUtil.java b/java/org/apache/coyote/http2/ByteUtil.java new file mode 100644 index 0000000..e8904c4 --- /dev/null +++ b/java/org/apache/coyote/http2/ByteUtil.java @@ -0,0 +1,107 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +/** + * Utility class for extracting values from byte arrays. + */ +class ByteUtil { + + private ByteUtil() { + // Hide default constructor + } + + + static boolean isBit7Set(byte input) { + return (input & 0x80) != 0; + } + + + static int get31Bits(byte[] input, int firstByte) { + return ((input[firstByte] & 0x7F) << 24) + ((input[firstByte + 1] & 0xFF) << 16) + + ((input[firstByte + 2] & 0xFF) << 8) + (input[firstByte + 3] & 0xFF); + } + + + static int get31Bits(ByteBuffer input, int firstByte) { + return ((input.get(firstByte) & 0x7F) << 24) + ((input.get(firstByte + 1) & 0xFF) << 16) + + ((input.get(firstByte + 2) & 0xFF) << 8) + (input.get(firstByte + 3) & 0xFF); + } + + + static void set31Bits(byte[] output, int firstByte, int value) { + output[firstByte] = (byte) ((value & 0x7F000000) >> 24); + output[firstByte + 1] = (byte) ((value & 0xFF0000) >> 16); + output[firstByte + 2] = (byte) ((value & 0xFF00) >> 8); + output[firstByte + 3] = (byte) (value & 0xFF); + } + + + static int getOneByte(byte[] input, int pos) { + return (input[pos] & 0xFF); + } + + + static int getOneByte(ByteBuffer input, int pos) { + return (input.get(pos) & 0xFF); + } + + + static int getTwoBytes(byte[] input, int firstByte) { + return ((input[firstByte] & 0xFF) << 8) + (input[firstByte + 1] & 0xFF); + } + + + static int getThreeBytes(byte[] input, int firstByte) { + return ((input[firstByte] & 0xFF) << 16) + ((input[firstByte + 1] & 0xFF) << 8) + (input[firstByte + 2] & 0xFF); + } + + + static int getThreeBytes(ByteBuffer input, int firstByte) { + return ((input.get(firstByte) & 0xFF) << 16) + ((input.get(firstByte + 1) & 0xFF) << 8) + + (input.get(firstByte + 2) & 0xFF); + } + + + static void setTwoBytes(byte[] output, int firstByte, int value) { + output[firstByte] = (byte) ((value & 0xFF00) >> 8); + output[firstByte + 1] = (byte) (value & 0xFF); + } + + + static void setThreeBytes(byte[] output, int firstByte, int value) { + output[firstByte] = (byte) ((value & 0xFF0000) >> 16); + output[firstByte + 1] = (byte) ((value & 0xFF00) >> 8); + output[firstByte + 2] = (byte) (value & 0xFF); + } + + + static long getFourBytes(byte[] input, int firstByte) { + return ((long) (input[firstByte] & 0xFF) << 24) + ((input[firstByte + 1] & 0xFF) << 16) + + ((input[firstByte + 2] & 0xFF) << 8) + (input[firstByte + 3] & 0xFF); + } + + + static void setFourBytes(byte[] output, int firstByte, long value) { + output[firstByte] = (byte) ((value & 0xFF000000) >> 24); + output[firstByte + 1] = (byte) ((value & 0xFF0000) >> 16); + output[firstByte + 2] = (byte) ((value & 0xFF00) >> 8); + output[firstByte + 3] = (byte) (value & 0xFF); + } +} diff --git a/java/org/apache/coyote/http2/ConnectionException.java b/java/org/apache/coyote/http2/ConnectionException.java new file mode 100644 index 0000000..6f48a31 --- /dev/null +++ b/java/org/apache/coyote/http2/ConnectionException.java @@ -0,0 +1,34 @@ +/* + * 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.coyote.http2; + +/** + * Thrown when an HTTP/2 connection error occurs. + */ +class ConnectionException extends Http2Exception { + + private static final long serialVersionUID = 1L; + + ConnectionException(String msg, Http2Error error) { + super(msg, error); + } + + + ConnectionException(String msg, Http2Error error, Throwable cause) { + super(msg, error, cause); + } +} diff --git a/java/org/apache/coyote/http2/ConnectionSettingsBase.java b/java/org/apache/coyote/http2/ConnectionSettingsBase.java new file mode 100644 index 0000000..64d5cf4 --- /dev/null +++ b/java/org/apache/coyote/http2/ConnectionSettingsBase.java @@ -0,0 +1,233 @@ +/* + * 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.coyote.http2; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +abstract class ConnectionSettingsBase { + + private final Log log = LogFactory.getLog(ConnectionSettingsBase.class); // must not be static + private final StringManager sm = StringManager.getManager(ConnectionSettingsBase.class); + + private final String connectionId; + + // Limits + static final int MAX_WINDOW_SIZE = (1 << 31) - 1; + static final int MIN_MAX_FRAME_SIZE = 1 << 14; + static final int MAX_MAX_FRAME_SIZE = (1 << 24) - 1; + static final long UNLIMITED = ((long) 1 << 32); // Use the maximum possible + static final int MAX_HEADER_TABLE_SIZE = 1 << 16; + + // Defaults (defined by the specification) + static final int DEFAULT_HEADER_TABLE_SIZE = Hpack.DEFAULT_TABLE_SIZE; + static final boolean DEFAULT_ENABLE_PUSH = true; + static final long DEFAULT_MAX_CONCURRENT_STREAMS = UNLIMITED; + static final int DEFAULT_INITIAL_WINDOW_SIZE = (1 << 16) - 1; + static final int DEFAULT_MAX_FRAME_SIZE = MIN_MAX_FRAME_SIZE; + static final long DEFAULT_MAX_HEADER_LIST_SIZE = 1 << 15; + + // Defaults (defined by Tomcat) + static final long DEFAULT_NO_RFC7540_PRIORITIES = 1; + + Map current = new ConcurrentHashMap<>(); + Map pending = new ConcurrentHashMap<>(); + + + ConnectionSettingsBase(String connectionId) { + this.connectionId = connectionId; + // Set up the defaults + current.put(Setting.HEADER_TABLE_SIZE, Long.valueOf(DEFAULT_HEADER_TABLE_SIZE)); + current.put(Setting.ENABLE_PUSH, Long.valueOf(DEFAULT_ENABLE_PUSH ? 1 : 0)); + current.put(Setting.MAX_CONCURRENT_STREAMS, Long.valueOf(DEFAULT_MAX_CONCURRENT_STREAMS)); + current.put(Setting.INITIAL_WINDOW_SIZE, Long.valueOf(DEFAULT_INITIAL_WINDOW_SIZE)); + current.put(Setting.MAX_FRAME_SIZE, Long.valueOf(DEFAULT_MAX_FRAME_SIZE)); + current.put(Setting.MAX_HEADER_LIST_SIZE, Long.valueOf(DEFAULT_MAX_HEADER_LIST_SIZE)); + current.put(Setting.NO_RFC7540_PRIORITIES, Long.valueOf(DEFAULT_NO_RFC7540_PRIORITIES)); + } + + + final void set(Setting setting, long value) throws T { + if (log.isTraceEnabled()) { + log.trace(sm.getString("connectionSettings.debug", connectionId, getEndpointName(), setting, + Long.toString(value))); + } + + switch (setting) { + case HEADER_TABLE_SIZE: + validateHeaderTableSize(value); + break; + case ENABLE_PUSH: + validateEnablePush(value); + break; + case MAX_CONCURRENT_STREAMS: + // No further validation required + break; + case INITIAL_WINDOW_SIZE: + validateInitialWindowSize(value); + break; + case MAX_FRAME_SIZE: + validateMaxFrameSize(value); + break; + case MAX_HEADER_LIST_SIZE: + // No further validation required + break; + case NO_RFC7540_PRIORITIES: + validateNoRfc7540Priorities(value); + break; + case UNKNOWN: + // Unrecognised. Ignore it. + return; + } + + set(setting, Long.valueOf(value)); + } + + + synchronized void set(Setting setting, Long value) { + current.put(setting, value); + } + + + final int getHeaderTableSize() { + return getMinInt(Setting.HEADER_TABLE_SIZE); + } + + + final boolean getEnablePush() { + long result = getMin(Setting.ENABLE_PUSH); + return result != 0; + } + + + final long getMaxConcurrentStreams() { + return getMax(Setting.MAX_CONCURRENT_STREAMS); + } + + + final int getInitialWindowSize() { + return getMaxInt(Setting.INITIAL_WINDOW_SIZE); + } + + + final int getMaxFrameSize() { + return getMaxInt(Setting.MAX_FRAME_SIZE); + } + + + final long getMaxHeaderListSize() { + return getMax(Setting.MAX_HEADER_LIST_SIZE); + } + + + private synchronized long getMin(Setting setting) { + Long pendingValue = pending.get(setting); + long currentValue = current.get(setting).longValue(); + if (pendingValue == null) { + return currentValue; + } else { + return Long.min(pendingValue.longValue(), currentValue); + } + } + + + private synchronized int getMinInt(Setting setting) { + long result = getMin(setting); + if (result > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) result; + } + } + + + private synchronized long getMax(Setting setting) { + Long pendingValue = pending.get(setting); + long currentValue = current.get(setting).longValue(); + if (pendingValue == null) { + return currentValue; + } else { + return Long.max(pendingValue.longValue(), currentValue); + } + } + + + private synchronized int getMaxInt(Setting setting) { + long result = getMax(setting); + if (result > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) result; + } + } + + + private void validateHeaderTableSize(long headerTableSize) throws T { + if (headerTableSize > MAX_HEADER_TABLE_SIZE) { + String msg = sm.getString("connectionSettings.headerTableSizeLimit", connectionId, + Long.toString(headerTableSize)); + throwException(msg, Http2Error.PROTOCOL_ERROR); + } + } + + + private void validateEnablePush(long enablePush) throws T { + // Can't be less than zero since the result of the byte->long conversion + // will never be negative + if (enablePush > 1) { + String msg = sm.getString("connectionSettings.enablePushInvalid", connectionId, Long.toString(enablePush)); + throwException(msg, Http2Error.PROTOCOL_ERROR); + } + } + + + private void validateInitialWindowSize(long initialWindowSize) throws T { + if (initialWindowSize > MAX_WINDOW_SIZE) { + String msg = sm.getString("connectionSettings.windowSizeTooBig", connectionId, + Long.toString(initialWindowSize), Long.toString(MAX_WINDOW_SIZE)); + throwException(msg, Http2Error.FLOW_CONTROL_ERROR); + } + } + + + private void validateMaxFrameSize(long maxFrameSize) throws T { + if (maxFrameSize < MIN_MAX_FRAME_SIZE || maxFrameSize > MAX_MAX_FRAME_SIZE) { + String msg = + sm.getString("connectionSettings.maxFrameSizeInvalid", connectionId, Long.toString(maxFrameSize), + Integer.toString(MIN_MAX_FRAME_SIZE), Integer.toString(MAX_MAX_FRAME_SIZE)); + throwException(msg, Http2Error.PROTOCOL_ERROR); + } + } + + + private void validateNoRfc7540Priorities(long noRfc7540Priorities) throws T { + if (noRfc7540Priorities < 0 || noRfc7540Priorities > 1) { + String msg = sm.getString("connectionSettings.noRfc7540PrioritiesInvalid", connectionId, + Long.toString(noRfc7540Priorities)); + throwException(msg, Http2Error.PROTOCOL_ERROR); + } + } + + + abstract void throwException(String msg, Http2Error error) throws T; + + abstract String getEndpointName(); +} diff --git a/java/org/apache/coyote/http2/ConnectionSettingsLocal.java b/java/org/apache/coyote/http2/ConnectionSettingsLocal.java new file mode 100644 index 0000000..372be80 --- /dev/null +++ b/java/org/apache/coyote/http2/ConnectionSettingsLocal.java @@ -0,0 +1,105 @@ +/* + * 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.coyote.http2; + +import java.util.Map; + +/** + * Represents the local connection settings i.e. the settings the client is expected to use when communicating with the + * server. There will be a delay between calling a setter and the setting taking effect at the client. When a setter is + * called, the new value is added to the set of pending settings. Once the ACK is received, the new value is moved to + * the current settings. While waiting for the ACK, the getters will return the most lenient / generous / relaxed of the + * current setting and the pending setting. This class does not validate the values passed to the setters. If an invalid + * value is used the client will respond (almost certainly by closing the connection) as defined in the HTTP/2 + * specification. + */ +class ConnectionSettingsLocal extends ConnectionSettingsBase { + + private static final String ENDPOINT_NAME = "Local(client->server)"; + + private boolean sendInProgress = false; + + + ConnectionSettingsLocal(String connectionId) { + super(connectionId); + } + + + @Override + final synchronized void set(Setting setting, Long value) { + checkSend(); + if (current.get(setting).longValue() == value.longValue()) { + pending.remove(setting); + } else { + pending.put(setting, value); + } + } + + + final synchronized byte[] getSettingsFrameForPending() { + checkSend(); + int payloadSize = pending.size() * 6; + byte[] result = new byte[9 + payloadSize]; + + ByteUtil.setThreeBytes(result, 0, payloadSize); + result[3] = FrameType.SETTINGS.getIdByte(); + // No flags + // Stream is zero + // Payload + int pos = 9; + for (Map.Entry setting : pending.entrySet()) { + ByteUtil.setTwoBytes(result, pos, setting.getKey().getId()); + pos += 2; + ByteUtil.setFourBytes(result, pos, setting.getValue().longValue()); + pos += 4; + } + sendInProgress = true; + return result; + } + + + final synchronized boolean ack() { + if (sendInProgress) { + sendInProgress = false; + current.putAll(pending); + pending.clear(); + return true; + } else { + return false; + } + } + + + private void checkSend() { + if (sendInProgress) { + // Coding error. No need for i18n + throw new IllegalStateException(); + } + } + + + @Override + final void throwException(String msg, Http2Error error) throws IllegalArgumentException { + throw new IllegalArgumentException(msg); + } + + + @Override + final String getEndpointName() { + return ENDPOINT_NAME; + } +} diff --git a/java/org/apache/coyote/http2/ConnectionSettingsRemote.java b/java/org/apache/coyote/http2/ConnectionSettingsRemote.java new file mode 100644 index 0000000..6b3c0ab --- /dev/null +++ b/java/org/apache/coyote/http2/ConnectionSettingsRemote.java @@ -0,0 +1,41 @@ +/* + * 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.coyote.http2; + +/** + * Represents the remote connection settings: i.e. the settings the server must use when communicating with the client. + */ +class ConnectionSettingsRemote extends ConnectionSettingsBase { + + private static final String ENDPOINT_NAME = "Remote(server->client)"; + + ConnectionSettingsRemote(String connectionId) { + super(connectionId); + } + + + @Override + final void throwException(String msg, Http2Error error) throws ConnectionException { + throw new ConnectionException(msg, error); + } + + + @Override + final String getEndpointName() { + return ENDPOINT_NAME; + } +} diff --git a/java/org/apache/coyote/http2/Constants.java b/java/org/apache/coyote/http2/Constants.java new file mode 100644 index 0000000..5575c76 --- /dev/null +++ b/java/org/apache/coyote/http2/Constants.java @@ -0,0 +1,44 @@ +/* + * 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.coyote.http2; + +public class Constants { + + /** + * RFC 7540 prioritisation default weight. + * + * @deprecated Unused. Will be removed in Tomcat 11 onwards. + */ + @Deprecated + public static final int DEFAULT_WEIGHT = 16; + + // Parsing + static final int DEFAULT_HEADER_READ_BUFFER_SIZE = 1024; + + // Header frame size + // TODO: Is 1k the optimal value? + static final int DEFAULT_HEADERS_FRAME_SIZE = 1024; + // TODO: Is 64 too big? Just the status header with compression + static final int DEFAULT_HEADERS_ACK_FRAME_SIZE = 64; + + // Limits + static final int DEFAULT_MAX_COOKIE_COUNT = 200; + static final int DEFAULT_MAX_HEADER_COUNT = 100; + static final int DEFAULT_MAX_HEADER_SIZE = 8 * 1024; + static final int DEFAULT_MAX_TRAILER_COUNT = 100; + static final int DEFAULT_MAX_TRAILER_SIZE = 8 * 1024; +} diff --git a/java/org/apache/coyote/http2/Flags.java b/java/org/apache/coyote/http2/Flags.java new file mode 100644 index 0000000..d7ff181 --- /dev/null +++ b/java/org/apache/coyote/http2/Flags.java @@ -0,0 +1,49 @@ +/* + * 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.coyote.http2; + +class Flags { + + private Flags() { + // Utility class. Hide default constructor + } + + + static boolean isEndOfStream(int flags) { + return (flags & 0x01) != 0; + } + + + static boolean isAck(int flags) { + return (flags & 0x01) != 0; + } + + + static boolean isEndOfHeaders(int flags) { + return (flags & 0x04) != 0; + } + + + static boolean hasPadding(int flags) { + return (flags & 0x08) != 0; + } + + + static boolean hasPriority(int flags) { + return (flags & 0x20) != 0; + } +} diff --git a/java/org/apache/coyote/http2/FrameType.java b/java/org/apache/coyote/http2/FrameType.java new file mode 100644 index 0000000..da0614c --- /dev/null +++ b/java/org/apache/coyote/http2/FrameType.java @@ -0,0 +1,117 @@ +/* + * 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.coyote.http2; + +import java.util.function.IntPredicate; + +import org.apache.tomcat.util.res.StringManager; + +enum FrameType { + // @formatter:off + DATA ( 0, false, true, null, false), + HEADERS ( 1, false, true, null, true), + PRIORITY ( 2, false, true, (x) -> x == 5, false), + RST ( 3, false, true, (x) -> x == 4, false), + SETTINGS ( 4, true, false, (x) -> x % 6 == 0, true), + PUSH_PROMISE ( 5, false, true, (x) -> x >= 4, true), + PING ( 6, true, false, (x) -> x == 8, false), + GOAWAY ( 7, true, false, (x) -> x >= 8, false), + WINDOW_UPDATE ( 8, true, true, (x) -> x == 4, true), + CONTINUATION ( 9, false, true, null, true), + PRIORITY_UPDATE ( 16, true, false, (x) -> x >= 4, true), + UNKNOWN (256, true, true, null, false); + // @formatter:on + + private static final StringManager sm = StringManager.getManager(FrameType.class); + + private final int id; + private final boolean streamZero; + private final boolean streamNonZero; + private final IntPredicate payloadSizeValidator; + private final boolean payloadErrorFatal; + + + FrameType(int id, boolean streamZero, boolean streamNonZero, IntPredicate payloadSizeValidator, + boolean payloadErrorFatal) { + this.id = id; + this.streamZero = streamZero; + this.streamNonZero = streamNonZero; + this.payloadSizeValidator = payloadSizeValidator; + this.payloadErrorFatal = payloadErrorFatal; + } + + + int getId() { + return id; + } + + + byte getIdByte() { + return (byte) id; + } + + + void check(int streamId, int payloadSize) throws Http2Exception { + // Is FrameType valid for the given stream? + if (streamId == 0 && !streamZero || streamId != 0 && !streamNonZero) { + throw new ConnectionException(sm.getString("frameType.checkStream", this), Http2Error.PROTOCOL_ERROR); + } + + // Is the payload size valid for the given FrameType + if (payloadSizeValidator != null && !payloadSizeValidator.test(payloadSize)) { + if (payloadErrorFatal || streamId == 0) { + throw new ConnectionException( + sm.getString("frameType.checkPayloadSize", Integer.toString(payloadSize), this), + Http2Error.FRAME_SIZE_ERROR); + } else { + throw new StreamException( + sm.getString("frameType.checkPayloadSize", Integer.toString(payloadSize), this), + Http2Error.FRAME_SIZE_ERROR, streamId); + } + } + } + + + static FrameType valueOf(int i) { + switch (i) { + case 0: + return DATA; + case 1: + return HEADERS; + case 2: + return PRIORITY; + case 3: + return RST; + case 4: + return SETTINGS; + case 5: + return PUSH_PROMISE; + case 6: + return PING; + case 7: + return GOAWAY; + case 8: + return WINDOW_UPDATE; + case 9: + return CONTINUATION; + case 16: + return PRIORITY_UPDATE; + default: + return UNKNOWN; + } + } +} diff --git a/java/org/apache/coyote/http2/HPackHuffman.java b/java/org/apache/coyote/http2/HPackHuffman.java new file mode 100644 index 0000000..e5ce482 --- /dev/null +++ b/java/org/apache/coyote/http2/HPackHuffman.java @@ -0,0 +1,585 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.apache.tomcat.util.res.StringManager; + +public class HPackHuffman { + + protected static final StringManager sm = StringManager.getManager(HPackHuffman.class); + + private static final HuffmanCode[] HUFFMAN_CODES; + + /** + * array based tree representation of a huffman code. + *

    + * the high two bytes corresponds to the tree node if the bit is set, and the low two bytes for if it is clear if + * the high bit is set it is a terminal node, otherwise it contains the next node position. + */ + private static final int[] DECODING_TABLE; + + private static final int LOW_TERMINAL_BIT = (0b10000000) << 8; + private static final int HIGH_TERMINAL_BIT = (0b10000000) << 24; + private static final int LOW_MASK = 0b0111111111111111; + + + static { + + HuffmanCode[] codes = new HuffmanCode[257]; + + codes[0] = new HuffmanCode(0x1ff8, 13); + codes[1] = new HuffmanCode(0x7fffd8, 23); + codes[2] = new HuffmanCode(0xfffffe2, 28); + codes[3] = new HuffmanCode(0xfffffe3, 28); + codes[4] = new HuffmanCode(0xfffffe4, 28); + codes[5] = new HuffmanCode(0xfffffe5, 28); + codes[6] = new HuffmanCode(0xfffffe6, 28); + codes[7] = new HuffmanCode(0xfffffe7, 28); + codes[8] = new HuffmanCode(0xfffffe8, 28); + codes[9] = new HuffmanCode(0xffffea, 24); + codes[10] = new HuffmanCode(0x3ffffffc, 30); + codes[11] = new HuffmanCode(0xfffffe9, 28); + codes[12] = new HuffmanCode(0xfffffea, 28); + codes[13] = new HuffmanCode(0x3ffffffd, 30); + codes[14] = new HuffmanCode(0xfffffeb, 28); + codes[15] = new HuffmanCode(0xfffffec, 28); + codes[16] = new HuffmanCode(0xfffffed, 28); + codes[17] = new HuffmanCode(0xfffffee, 28); + codes[18] = new HuffmanCode(0xfffffef, 28); + codes[19] = new HuffmanCode(0xffffff0, 28); + codes[20] = new HuffmanCode(0xffffff1, 28); + codes[21] = new HuffmanCode(0xffffff2, 28); + codes[22] = new HuffmanCode(0x3ffffffe, 30); + codes[23] = new HuffmanCode(0xffffff3, 28); + codes[24] = new HuffmanCode(0xffffff4, 28); + codes[25] = new HuffmanCode(0xffffff5, 28); + codes[26] = new HuffmanCode(0xffffff6, 28); + codes[27] = new HuffmanCode(0xffffff7, 28); + codes[28] = new HuffmanCode(0xffffff8, 28); + codes[29] = new HuffmanCode(0xffffff9, 28); + codes[30] = new HuffmanCode(0xffffffa, 28); + codes[31] = new HuffmanCode(0xffffffb, 28); + codes[32] = new HuffmanCode(0x14, 6); + codes[33] = new HuffmanCode(0x3f8, 10); + codes[34] = new HuffmanCode(0x3f9, 10); + codes[35] = new HuffmanCode(0xffa, 12); + codes[36] = new HuffmanCode(0x1ff9, 13); + codes[37] = new HuffmanCode(0x15, 6); + codes[38] = new HuffmanCode(0xf8, 8); + codes[39] = new HuffmanCode(0x7fa, 11); + codes[40] = new HuffmanCode(0x3fa, 10); + codes[41] = new HuffmanCode(0x3fb, 10); + codes[42] = new HuffmanCode(0xf9, 8); + codes[43] = new HuffmanCode(0x7fb, 11); + codes[44] = new HuffmanCode(0xfa, 8); + codes[45] = new HuffmanCode(0x16, 6); + codes[46] = new HuffmanCode(0x17, 6); + codes[47] = new HuffmanCode(0x18, 6); + codes[48] = new HuffmanCode(0x0, 5); + codes[49] = new HuffmanCode(0x1, 5); + codes[50] = new HuffmanCode(0x2, 5); + codes[51] = new HuffmanCode(0x19, 6); + codes[52] = new HuffmanCode(0x1a, 6); + codes[53] = new HuffmanCode(0x1b, 6); + codes[54] = new HuffmanCode(0x1c, 6); + codes[55] = new HuffmanCode(0x1d, 6); + codes[56] = new HuffmanCode(0x1e, 6); + codes[57] = new HuffmanCode(0x1f, 6); + codes[58] = new HuffmanCode(0x5c, 7); + codes[59] = new HuffmanCode(0xfb, 8); + codes[60] = new HuffmanCode(0x7ffc, 15); + codes[61] = new HuffmanCode(0x20, 6); + codes[62] = new HuffmanCode(0xffb, 12); + codes[63] = new HuffmanCode(0x3fc, 10); + codes[64] = new HuffmanCode(0x1ffa, 13); + codes[65] = new HuffmanCode(0x21, 6); + codes[66] = new HuffmanCode(0x5d, 7); + codes[67] = new HuffmanCode(0x5e, 7); + codes[68] = new HuffmanCode(0x5f, 7); + codes[69] = new HuffmanCode(0x60, 7); + codes[70] = new HuffmanCode(0x61, 7); + codes[71] = new HuffmanCode(0x62, 7); + codes[72] = new HuffmanCode(0x63, 7); + codes[73] = new HuffmanCode(0x64, 7); + codes[74] = new HuffmanCode(0x65, 7); + codes[75] = new HuffmanCode(0x66, 7); + codes[76] = new HuffmanCode(0x67, 7); + codes[77] = new HuffmanCode(0x68, 7); + codes[78] = new HuffmanCode(0x69, 7); + codes[79] = new HuffmanCode(0x6a, 7); + codes[80] = new HuffmanCode(0x6b, 7); + codes[81] = new HuffmanCode(0x6c, 7); + codes[82] = new HuffmanCode(0x6d, 7); + codes[83] = new HuffmanCode(0x6e, 7); + codes[84] = new HuffmanCode(0x6f, 7); + codes[85] = new HuffmanCode(0x70, 7); + codes[86] = new HuffmanCode(0x71, 7); + codes[87] = new HuffmanCode(0x72, 7); + codes[88] = new HuffmanCode(0xfc, 8); + codes[89] = new HuffmanCode(0x73, 7); + codes[90] = new HuffmanCode(0xfd, 8); + codes[91] = new HuffmanCode(0x1ffb, 13); + codes[92] = new HuffmanCode(0x7fff0, 19); + codes[93] = new HuffmanCode(0x1ffc, 13); + codes[94] = new HuffmanCode(0x3ffc, 14); + codes[95] = new HuffmanCode(0x22, 6); + codes[96] = new HuffmanCode(0x7ffd, 15); + codes[97] = new HuffmanCode(0x3, 5); + codes[98] = new HuffmanCode(0x23, 6); + codes[99] = new HuffmanCode(0x4, 5); + codes[100] = new HuffmanCode(0x24, 6); + codes[101] = new HuffmanCode(0x5, 5); + codes[102] = new HuffmanCode(0x25, 6); + codes[103] = new HuffmanCode(0x26, 6); + codes[104] = new HuffmanCode(0x27, 6); + codes[105] = new HuffmanCode(0x6, 5); + codes[106] = new HuffmanCode(0x74, 7); + codes[107] = new HuffmanCode(0x75, 7); + codes[108] = new HuffmanCode(0x28, 6); + codes[109] = new HuffmanCode(0x29, 6); + codes[110] = new HuffmanCode(0x2a, 6); + codes[111] = new HuffmanCode(0x7, 5); + codes[112] = new HuffmanCode(0x2b, 6); + codes[113] = new HuffmanCode(0x76, 7); + codes[114] = new HuffmanCode(0x2c, 6); + codes[115] = new HuffmanCode(0x8, 5); + codes[116] = new HuffmanCode(0x9, 5); + codes[117] = new HuffmanCode(0x2d, 6); + codes[118] = new HuffmanCode(0x77, 7); + codes[119] = new HuffmanCode(0x78, 7); + codes[120] = new HuffmanCode(0x79, 7); + codes[121] = new HuffmanCode(0x7a, 7); + codes[122] = new HuffmanCode(0x7b, 7); + codes[123] = new HuffmanCode(0x7ffe, 15); + codes[124] = new HuffmanCode(0x7fc, 11); + codes[125] = new HuffmanCode(0x3ffd, 14); + codes[126] = new HuffmanCode(0x1ffd, 13); + codes[127] = new HuffmanCode(0xffffffc, 28); + codes[128] = new HuffmanCode(0xfffe6, 20); + codes[129] = new HuffmanCode(0x3fffd2, 22); + codes[130] = new HuffmanCode(0xfffe7, 20); + codes[131] = new HuffmanCode(0xfffe8, 20); + codes[132] = new HuffmanCode(0x3fffd3, 22); + codes[133] = new HuffmanCode(0x3fffd4, 22); + codes[134] = new HuffmanCode(0x3fffd5, 22); + codes[135] = new HuffmanCode(0x7fffd9, 23); + codes[136] = new HuffmanCode(0x3fffd6, 22); + codes[137] = new HuffmanCode(0x7fffda, 23); + codes[138] = new HuffmanCode(0x7fffdb, 23); + codes[139] = new HuffmanCode(0x7fffdc, 23); + codes[140] = new HuffmanCode(0x7fffdd, 23); + codes[141] = new HuffmanCode(0x7fffde, 23); + codes[142] = new HuffmanCode(0xffffeb, 24); + codes[143] = new HuffmanCode(0x7fffdf, 23); + codes[144] = new HuffmanCode(0xffffec, 24); + codes[145] = new HuffmanCode(0xffffed, 24); + codes[146] = new HuffmanCode(0x3fffd7, 22); + codes[147] = new HuffmanCode(0x7fffe0, 23); + codes[148] = new HuffmanCode(0xffffee, 24); + codes[149] = new HuffmanCode(0x7fffe1, 23); + codes[150] = new HuffmanCode(0x7fffe2, 23); + codes[151] = new HuffmanCode(0x7fffe3, 23); + codes[152] = new HuffmanCode(0x7fffe4, 23); + codes[153] = new HuffmanCode(0x1fffdc, 21); + codes[154] = new HuffmanCode(0x3fffd8, 22); + codes[155] = new HuffmanCode(0x7fffe5, 23); + codes[156] = new HuffmanCode(0x3fffd9, 22); + codes[157] = new HuffmanCode(0x7fffe6, 23); + codes[158] = new HuffmanCode(0x7fffe7, 23); + codes[159] = new HuffmanCode(0xffffef, 24); + codes[160] = new HuffmanCode(0x3fffda, 22); + codes[161] = new HuffmanCode(0x1fffdd, 21); + codes[162] = new HuffmanCode(0xfffe9, 20); + codes[163] = new HuffmanCode(0x3fffdb, 22); + codes[164] = new HuffmanCode(0x3fffdc, 22); + codes[165] = new HuffmanCode(0x7fffe8, 23); + codes[166] = new HuffmanCode(0x7fffe9, 23); + codes[167] = new HuffmanCode(0x1fffde, 21); + codes[168] = new HuffmanCode(0x7fffea, 23); + codes[169] = new HuffmanCode(0x3fffdd, 22); + codes[170] = new HuffmanCode(0x3fffde, 22); + codes[171] = new HuffmanCode(0xfffff0, 24); + codes[172] = new HuffmanCode(0x1fffdf, 21); + codes[173] = new HuffmanCode(0x3fffdf, 22); + codes[174] = new HuffmanCode(0x7fffeb, 23); + codes[175] = new HuffmanCode(0x7fffec, 23); + codes[176] = new HuffmanCode(0x1fffe0, 21); + codes[177] = new HuffmanCode(0x1fffe1, 21); + codes[178] = new HuffmanCode(0x3fffe0, 22); + codes[179] = new HuffmanCode(0x1fffe2, 21); + codes[180] = new HuffmanCode(0x7fffed, 23); + codes[181] = new HuffmanCode(0x3fffe1, 22); + codes[182] = new HuffmanCode(0x7fffee, 23); + codes[183] = new HuffmanCode(0x7fffef, 23); + codes[184] = new HuffmanCode(0xfffea, 20); + codes[185] = new HuffmanCode(0x3fffe2, 22); + codes[186] = new HuffmanCode(0x3fffe3, 22); + codes[187] = new HuffmanCode(0x3fffe4, 22); + codes[188] = new HuffmanCode(0x7ffff0, 23); + codes[189] = new HuffmanCode(0x3fffe5, 22); + codes[190] = new HuffmanCode(0x3fffe6, 22); + codes[191] = new HuffmanCode(0x7ffff1, 23); + codes[192] = new HuffmanCode(0x3ffffe0, 26); + codes[193] = new HuffmanCode(0x3ffffe1, 26); + codes[194] = new HuffmanCode(0xfffeb, 20); + codes[195] = new HuffmanCode(0x7fff1, 19); + codes[196] = new HuffmanCode(0x3fffe7, 22); + codes[197] = new HuffmanCode(0x7ffff2, 23); + codes[198] = new HuffmanCode(0x3fffe8, 22); + codes[199] = new HuffmanCode(0x1ffffec, 25); + codes[200] = new HuffmanCode(0x3ffffe2, 26); + codes[201] = new HuffmanCode(0x3ffffe3, 26); + codes[202] = new HuffmanCode(0x3ffffe4, 26); + codes[203] = new HuffmanCode(0x7ffffde, 27); + codes[204] = new HuffmanCode(0x7ffffdf, 27); + codes[205] = new HuffmanCode(0x3ffffe5, 26); + codes[206] = new HuffmanCode(0xfffff1, 24); + codes[207] = new HuffmanCode(0x1ffffed, 25); + codes[208] = new HuffmanCode(0x7fff2, 19); + codes[209] = new HuffmanCode(0x1fffe3, 21); + codes[210] = new HuffmanCode(0x3ffffe6, 26); + codes[211] = new HuffmanCode(0x7ffffe0, 27); + codes[212] = new HuffmanCode(0x7ffffe1, 27); + codes[213] = new HuffmanCode(0x3ffffe7, 26); + codes[214] = new HuffmanCode(0x7ffffe2, 27); + codes[215] = new HuffmanCode(0xfffff2, 24); + codes[216] = new HuffmanCode(0x1fffe4, 21); + codes[217] = new HuffmanCode(0x1fffe5, 21); + codes[218] = new HuffmanCode(0x3ffffe8, 26); + codes[219] = new HuffmanCode(0x3ffffe9, 26); + codes[220] = new HuffmanCode(0xffffffd, 28); + codes[221] = new HuffmanCode(0x7ffffe3, 27); + codes[222] = new HuffmanCode(0x7ffffe4, 27); + codes[223] = new HuffmanCode(0x7ffffe5, 27); + codes[224] = new HuffmanCode(0xfffec, 20); + codes[225] = new HuffmanCode(0xfffff3, 24); + codes[226] = new HuffmanCode(0xfffed, 20); + codes[227] = new HuffmanCode(0x1fffe6, 21); + codes[228] = new HuffmanCode(0x3fffe9, 22); + codes[229] = new HuffmanCode(0x1fffe7, 21); + codes[230] = new HuffmanCode(0x1fffe8, 21); + codes[231] = new HuffmanCode(0x7ffff3, 23); + codes[232] = new HuffmanCode(0x3fffea, 22); + codes[233] = new HuffmanCode(0x3fffeb, 22); + codes[234] = new HuffmanCode(0x1ffffee, 25); + codes[235] = new HuffmanCode(0x1ffffef, 25); + codes[236] = new HuffmanCode(0xfffff4, 24); + codes[237] = new HuffmanCode(0xfffff5, 24); + codes[238] = new HuffmanCode(0x3ffffea, 26); + codes[239] = new HuffmanCode(0x7ffff4, 23); + codes[240] = new HuffmanCode(0x3ffffeb, 26); + codes[241] = new HuffmanCode(0x7ffffe6, 27); + codes[242] = new HuffmanCode(0x3ffffec, 26); + codes[243] = new HuffmanCode(0x3ffffed, 26); + codes[244] = new HuffmanCode(0x7ffffe7, 27); + codes[245] = new HuffmanCode(0x7ffffe8, 27); + codes[246] = new HuffmanCode(0x7ffffe9, 27); + codes[247] = new HuffmanCode(0x7ffffea, 27); + codes[248] = new HuffmanCode(0x7ffffeb, 27); + codes[249] = new HuffmanCode(0xffffffe, 28); + codes[250] = new HuffmanCode(0x7ffffec, 27); + codes[251] = new HuffmanCode(0x7ffffed, 27); + codes[252] = new HuffmanCode(0x7ffffee, 27); + codes[253] = new HuffmanCode(0x7ffffef, 27); + codes[254] = new HuffmanCode(0x7fffff0, 27); + codes[255] = new HuffmanCode(0x3ffffee, 26); + codes[256] = new HuffmanCode(0x3fffffff, 30); + HUFFMAN_CODES = codes; + + // lengths determined by experimentation, just set it to something large then see how large it actually ends up + int[] codingTree = new int[256]; + // the current position in the tree + int pos = 0; + int allocated = 1; // the next position to allocate to + // map of the current state at a given position + // only used while building the tree + HuffmanCode[] currentCode = new HuffmanCode[256]; + currentCode[0] = new HuffmanCode(0, 0); + + final Set allCodes = new HashSet<>(Arrays.asList(HUFFMAN_CODES)); + + while (!allCodes.isEmpty()) { + int length = currentCode[pos].length; + int code = currentCode[pos].value; + + int newLength = length + 1; + HuffmanCode high = new HuffmanCode(code << 1 | 1, newLength); + HuffmanCode low = new HuffmanCode(code << 1, newLength); + int newVal = 0; + boolean highTerminal = allCodes.remove(high); + if (highTerminal) { + // bah, linear search + int i = 0; + for (i = 0; i < codes.length; ++i) { + if (codes[i].equals(high)) { + break; + } + } + newVal = LOW_TERMINAL_BIT | i; + } else { + int highPos = allocated++; + currentCode[highPos] = high; + newVal = highPos; + } + newVal <<= 16; + boolean lowTerminal = allCodes.remove(low); + if (lowTerminal) { + // bah, linear search + int i = 0; + for (i = 0; i < codes.length; ++i) { + if (codes[i].equals(low)) { + break; + } + } + newVal |= LOW_TERMINAL_BIT | i; + } else { + int lowPos = allocated++; + currentCode[lowPos] = low; + newVal |= lowPos; + } + codingTree[pos] = newVal; + pos++; + } + DECODING_TABLE = codingTree; + } + + /** + * Decodes a huffman encoded string into the target StringBuilder. There must be enough space left in the buffer for + * this method to succeed. + * + * @param data The byte buffer + * @param length The length of data from the buffer to decode + * @param target The target for the decompressed data + * + * @throws HpackException If the Huffman encoded value in HPACK headers did not end with EOS padding + */ + public static void decode(ByteBuffer data, int length, StringBuilder target) throws HpackException { + assert data.remaining() >= length; + int treePos = 0; + boolean eosBits = true; + int eosBitCount = 0; + for (int i = 0; i < length; ++i) { + byte b = data.get(); + int bitPos = 7; + while (bitPos >= 0) { + int val = DECODING_TABLE[treePos]; + if (((1 << bitPos) & b) == 0) { + // bit not set, we want the lower part of the tree + if ((val & LOW_TERMINAL_BIT) == 0) { + treePos = val & LOW_MASK; + eosBits = false; + eosBitCount = 0; + } else { + target.append((char) (val & LOW_MASK)); + treePos = 0; + eosBits = true; + } + } else { + if (eosBits) { + eosBitCount++; + } + // bit not set, we want the lower part of the tree + if ((val & HIGH_TERMINAL_BIT) == 0) { + treePos = (val >> 16) & LOW_MASK; + } else { + if (eosBitCount != 0) { + // This must be the EOS symbol which MUST be treated + // as an error + throw new HpackException(sm.getString("hpackhuffman.stringLiteralEOS")); + } + target.append((char) ((val >> 16) & LOW_MASK)); + treePos = 0; + eosBits = true; + } + } + bitPos--; + } + } + if (eosBitCount > 7) { + throw new HpackException(sm.getString("hpackhuffman.stringLiteralTooMuchPadding")); + } + if (!eosBits) { + throw new HpackException(sm.getString("hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS")); + } + } + + + /** + * Encodes the given string into the buffer. If there is not enough space in the buffer, or the encoded version is + * bigger than the original it will return false and not modify the buffers position. + * + * @param buffer The buffer to encode into + * @param toEncode The string to encode + * @param forceLowercase If the string should be encoded in lower case + * + * @return true if encoding succeeded + */ + public static boolean encode(ByteBuffer buffer, String toEncode, boolean forceLowercase) { + if (buffer.remaining() <= toEncode.length()) { + return false; + } + int start = buffer.position(); + // this sucks, but we need to put the length first + // and we don't really have any option but to calculate it in advance to make sure we have left enough room + // so we end up iterating twice + int length = 0; + for (int i = 0; i < toEncode.length(); ++i) { + char c = toEncode.charAt(i); + if (c > 255) { + throw new IllegalArgumentException( + sm.getString("hpack.invalidCharacter", Character.toString(c), Integer.valueOf(c))); + } + if (forceLowercase) { + c = Hpack.toLower(c); + } + HuffmanCode code = HUFFMAN_CODES[c]; + length += code.length; + } + int byteLength = length / 8 + (length % 8 == 0 ? 0 : 1); + + buffer.put((byte) (1 << 7)); + Hpack.encodeInteger(buffer, byteLength, 7); + + + int bytePos = 0; + byte currentBufferByte = 0; + for (int i = 0; i < toEncode.length(); ++i) { + char c = toEncode.charAt(i); + if (forceLowercase) { + c = Hpack.toLower(c); + } + HuffmanCode code = HUFFMAN_CODES[c]; + if (code.length + bytePos <= 8) { + // it fits in the current byte + currentBufferByte |= ((code.value & 0xFF) << 8 - (code.length + bytePos)); + bytePos += code.length; + } else { + // it does not fit, it may need up to 4 bytes + int val = code.value; + int rem = code.length; + while (rem > 0) { + if (!buffer.hasRemaining()) { + buffer.position(start); + return false; + } + int remainingInByte = 8 - bytePos; + if (rem > remainingInByte) { + currentBufferByte |= (val >> (rem - remainingInByte)); + } else { + currentBufferByte |= (val << (remainingInByte - rem)); + } + if (rem > remainingInByte) { + buffer.put(currentBufferByte); + currentBufferByte = 0; + bytePos = 0; + } else { + bytePos = rem; + } + rem -= remainingInByte; + } + } + if (bytePos == 8) { + if (!buffer.hasRemaining()) { + buffer.position(start); + return false; + } + buffer.put(currentBufferByte); + currentBufferByte = 0; + bytePos = 0; + } + if (buffer.position() - start > toEncode.length()) { + // the encoded version is longer than the original + // just return false + buffer.position(start); + return false; + } + } + if (bytePos > 0) { + // add the EOS bytes if we have not finished on a single byte + if (!buffer.hasRemaining()) { + buffer.position(start); + return false; + } + buffer.put((byte) (currentBufferByte | ((0xFF) >> bytePos))); + } + return true; + } + + protected static class HuffmanCode { + /** + * The value of the least significant bits of the code + */ + int value; + /** + * length of the code, in bits + */ + int length; + + public HuffmanCode(int value, int length) { + this.value = value; + this.length = length; + } + + public int getValue() { + return value; + } + + public int getLength() { + return length; + } + + @Override + public boolean equals(Object o) { + + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HuffmanCode that = (HuffmanCode) o; + + if (length != that.length) { + return false; + } + if (value != that.value) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = value; + result = 31 * result + length; + return result; + } + + @Override + public String toString() { + return "HuffmanCode{" + "value=" + value + ", length=" + length + '}'; + } + } +} diff --git a/java/org/apache/coyote/http2/HeaderSink.java b/java/org/apache/coyote/http2/HeaderSink.java new file mode 100644 index 0000000..791fe57 --- /dev/null +++ b/java/org/apache/coyote/http2/HeaderSink.java @@ -0,0 +1,43 @@ +/* + * 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.coyote.http2; + +import org.apache.coyote.http2.HpackDecoder.HeaderEmitter; + +/** + * Purpose of this class is to silently swallow any headers. It is used once the connection close process has started if + * headers for new streams are received. + */ +class HeaderSink implements HeaderEmitter { + + @Override + public void emitHeader(String name, String value) { + // NO-OP + } + + @Override + public void validateHeaders() throws StreamException { + // NO-OP + } + + @Override + public void setHeaderException(StreamException streamException) { + // NO-OP + // The connection is already closing so no need to process additional + // errors + } +} diff --git a/java/org/apache/coyote/http2/Hpack.java b/java/org/apache/coyote/http2/Hpack.java new file mode 100644 index 0000000..b0ec92e --- /dev/null +++ b/java/org/apache/coyote/http2/Hpack.java @@ -0,0 +1,216 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.apache.tomcat.util.res.StringManager; + +final class Hpack { + + private static final StringManager sm = StringManager.getManager(Hpack.class); + + private static final byte LOWER_DIFF = 'a' - 'A'; + static final int DEFAULT_TABLE_SIZE = 4096; + private static final int MAX_INTEGER_OCTETS = 8; // not sure what a good value for this is, but the spec says we + // need to provide an upper bound + + /** + * table that contains powers of two, used as both bitmask and to quickly calculate 2^n + */ + private static final int[] PREFIX_TABLE; + + + static final HeaderField[] STATIC_TABLE; + static final int STATIC_TABLE_LENGTH; + + static { + PREFIX_TABLE = new int[32]; + for (int i = 0; i < 32; ++i) { + int n = 0; + for (int j = 0; j < i; ++j) { + n = n << 1; + n |= 1; + } + PREFIX_TABLE[i] = n; + } + + HeaderField[] fields = new HeaderField[62]; + // note that zero is not used + fields[1] = new HeaderField(":authority", null); + fields[2] = new HeaderField(":method", "GET"); + fields[3] = new HeaderField(":method", "POST"); + fields[4] = new HeaderField(":path", "/"); + fields[5] = new HeaderField(":path", "/index.html"); + fields[6] = new HeaderField(":scheme", "http"); + fields[7] = new HeaderField(":scheme", "https"); + fields[8] = new HeaderField(":status", "200"); + fields[9] = new HeaderField(":status", "204"); + fields[10] = new HeaderField(":status", "206"); + fields[11] = new HeaderField(":status", "304"); + fields[12] = new HeaderField(":status", "400"); + fields[13] = new HeaderField(":status", "404"); + fields[14] = new HeaderField(":status", "500"); + fields[15] = new HeaderField("accept-charset", null); + fields[16] = new HeaderField("accept-encoding", "gzip, deflate"); + fields[17] = new HeaderField("accept-language", null); + fields[18] = new HeaderField("accept-ranges", null); + fields[19] = new HeaderField("accept", null); + fields[20] = new HeaderField("access-control-allow-origin", null); + fields[21] = new HeaderField("age", null); + fields[22] = new HeaderField("allow", null); + fields[23] = new HeaderField("authorization", null); + fields[24] = new HeaderField("cache-control", null); + fields[25] = new HeaderField("content-disposition", null); + fields[26] = new HeaderField("content-encoding", null); + fields[27] = new HeaderField("content-language", null); + fields[28] = new HeaderField("content-length", null); + fields[29] = new HeaderField("content-location", null); + fields[30] = new HeaderField("content-range", null); + fields[31] = new HeaderField("content-type", null); + fields[32] = new HeaderField("cookie", null); + fields[33] = new HeaderField("date", null); + fields[34] = new HeaderField("etag", null); + fields[35] = new HeaderField("expect", null); + fields[36] = new HeaderField("expires", null); + fields[37] = new HeaderField("from", null); + fields[38] = new HeaderField("host", null); + fields[39] = new HeaderField("if-match", null); + fields[40] = new HeaderField("if-modified-since", null); + fields[41] = new HeaderField("if-none-match", null); + fields[42] = new HeaderField("if-range", null); + fields[43] = new HeaderField("if-unmodified-since", null); + fields[44] = new HeaderField("last-modified", null); + fields[45] = new HeaderField("link", null); + fields[46] = new HeaderField("location", null); + fields[47] = new HeaderField("max-forwards", null); + fields[48] = new HeaderField("proxy-authenticate", null); + fields[49] = new HeaderField("proxy-authorization", null); + fields[50] = new HeaderField("range", null); + fields[51] = new HeaderField("referer", null); + fields[52] = new HeaderField("refresh", null); + fields[53] = new HeaderField("retry-after", null); + fields[54] = new HeaderField("server", null); + fields[55] = new HeaderField("set-cookie", null); + fields[56] = new HeaderField("strict-transport-security", null); + fields[57] = new HeaderField("transfer-encoding", null); + fields[58] = new HeaderField("user-agent", null); + fields[59] = new HeaderField("vary", null); + fields[60] = new HeaderField("via", null); + fields[61] = new HeaderField("www-authenticate", null); + STATIC_TABLE = fields; + STATIC_TABLE_LENGTH = STATIC_TABLE.length - 1; + } + + static class HeaderField { + final String name; + final String value; + final int size; + + HeaderField(String name, String value) { + this.name = name; + this.value = value; + if (value != null) { + this.size = 32 + name.length() + value.length(); + } else { + this.size = -1; + } + } + } + + /** + * Decodes an integer in the HPACK prefix format. If the return value is -1 it means that there was not enough data + * in the buffer to complete the decoding sequence. + *

    + * If this method returns -1 then the source buffer will not have been modified. + * + * @param source The buffer that contains the integer + * @param n The encoding prefix length + * + * @return The encoded integer, or -1 if there was not enough data + */ + static int decodeInteger(ByteBuffer source, int n) throws HpackException { + if (source.remaining() == 0) { + return -1; + } + int count = 1; + int sp = source.position(); + int mask = PREFIX_TABLE[n]; + + int i = mask & source.get(); + int b; + if (i < PREFIX_TABLE[n]) { + return i; + } else { + int m = 0; + do { + if (count++ > MAX_INTEGER_OCTETS) { + throw new HpackException( + sm.getString("hpack.integerEncodedOverTooManyOctets", Integer.valueOf(MAX_INTEGER_OCTETS))); + } + if (source.remaining() == 0) { + // we have run out of data + // reset + source.position(sp); + return -1; + } + b = source.get(); + i = i + (b & 127) * (PREFIX_TABLE[m] + 1); + m += 7; + } while ((b & 128) == 128); + } + return i; + } + + /** + * Encodes an integer in the HPACK prefix format. + *

    + * This method assumes that the buffer has already had the first 8-n bits filled. As such it will modify the last + * byte that is already present in the buffer, and potentially add more if required + * + * @param source The buffer that contains the integer + * @param value The integer to encode + * @param n The encoding prefix length + */ + static void encodeInteger(ByteBuffer source, int value, int n) { + int twoNminus1 = PREFIX_TABLE[n]; + int pos = source.position() - 1; + if (value < twoNminus1) { + source.put(pos, (byte) (source.get(pos) | value)); + } else { + source.put(pos, (byte) (source.get(pos) | twoNminus1)); + value = value - twoNminus1; + while (value >= 128) { + source.put((byte) (value % 128 + 128)); + value = value / 128; + } + source.put((byte) value); + } + } + + + static char toLower(char c) { + if (c >= 'A' && c <= 'Z') { + return (char) (c + LOWER_DIFF); + } + return c; + } + + private Hpack() { + } + +} diff --git a/java/org/apache/coyote/http2/HpackDecoder.java b/java/org/apache/coyote/http2/HpackDecoder.java new file mode 100644 index 0000000..1d04406 --- /dev/null +++ b/java/org/apache/coyote/http2/HpackDecoder.java @@ -0,0 +1,490 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * A decoder for HPACK. + */ +public class HpackDecoder { + + private static final Log log = LogFactory.getLog(HpackDecoder.class); + private static final StringManager sm = StringManager.getManager(HpackDecoder.class); + + private static final int DEFAULT_RING_BUFFER_SIZE = 10; + + /** + * The object that receives the headers that are emitted from this decoder + */ + private HeaderEmitter headerEmitter; + + /** + * The header table + */ + private Hpack.HeaderField[] headerTable; + + /** + * The current HEAD position of the header table. We use a ring buffer type construct as it would be silly to + * actually shuffle the items around in the array. + */ + private int firstSlotPosition = 0; + + /** + * The current table size by index (aka the number of index positions that are filled up) + */ + private int filledTableSlots = 0; + + /** + * the current calculates memory size, as per the HPACK algorithm + */ + private int currentMemorySize = 0; + + /** + * The maximum allowed memory size set by the container. + */ + private int maxMemorySizeHard; + /** + * The maximum memory size currently in use. May be less than the hard limit. + */ + private int maxMemorySizeSoft; + + private int maxHeaderCount = Constants.DEFAULT_MAX_HEADER_COUNT; + private int maxHeaderSize = Constants.DEFAULT_MAX_HEADER_SIZE; + + private volatile int headerCount = 0; + private volatile boolean countedCookie; + private volatile int headerSize = 0; + + HpackDecoder(int maxMemorySize) { + this.maxMemorySizeHard = maxMemorySize; + this.maxMemorySizeSoft = maxMemorySize; + headerTable = new Hpack.HeaderField[DEFAULT_RING_BUFFER_SIZE]; + } + + HpackDecoder() { + this(Hpack.DEFAULT_TABLE_SIZE); + } + + /** + * Decodes the provided frame data. If this method leaves data in the buffer then this buffer should be compacted so + * this data is preserved, unless there is no more data in which case this should be considered a protocol error. + * + * @param buffer The buffer + * + * @throws HpackException If the packed data is not valid + */ + void decode(ByteBuffer buffer) throws HpackException { + while (buffer.hasRemaining()) { + int originalPos = buffer.position(); + byte b = buffer.get(); + if ((b & 0b10000000) != 0) { + // if the first bit is set it is an indexed header field + buffer.position(buffer.position() - 1); // unget the byte + int index = Hpack.decodeInteger(buffer, 7); // prefix is 7 + if (index == -1) { + buffer.position(originalPos); + return; + } else if (index == 0) { + throw new HpackException(sm.getString("hpackdecoder.zeroNotValidHeaderTableIndex")); + } + handleIndex(index); + } else if ((b & 0b01000000) != 0) { + // Literal Header Field with Incremental Indexing + String headerName = readHeaderName(buffer, 6); + if (headerName == null) { + buffer.position(originalPos); + return; + } + String headerValue = readHpackString(buffer); + if (headerValue == null) { + buffer.position(originalPos); + return; + } + emitHeader(headerName, headerValue); + addEntryToHeaderTable(new Hpack.HeaderField(headerName, headerValue)); + } else if ((b & 0b11110000) == 0) { + // Literal Header Field without Indexing + String headerName = readHeaderName(buffer, 4); + if (headerName == null) { + buffer.position(originalPos); + return; + } + String headerValue = readHpackString(buffer); + if (headerValue == null) { + buffer.position(originalPos); + return; + } + emitHeader(headerName, headerValue); + } else if ((b & 0b11110000) == 0b00010000) { + // Literal Header Field never indexed + String headerName = readHeaderName(buffer, 4); + if (headerName == null) { + buffer.position(originalPos); + return; + } + String headerValue = readHpackString(buffer); + if (headerValue == null) { + buffer.position(originalPos); + return; + } + emitHeader(headerName, headerValue); + } else if ((b & 0b11100000) == 0b00100000) { + // context update max table size change + if (!handleMaxMemorySizeChange(buffer, originalPos)) { + return; + } + } else { + throw new RuntimeException(sm.getString("hpackdecoder.notImplemented")); + } + } + } + + private boolean handleMaxMemorySizeChange(ByteBuffer buffer, int originalPos) throws HpackException { + if (headerCount != 0) { + throw new HpackException(sm.getString("hpackdecoder.tableSizeUpdateNotAtStart")); + } + buffer.position(buffer.position() - 1); // unget the byte + int size = Hpack.decodeInteger(buffer, 5); + if (size == -1) { + buffer.position(originalPos); + return false; + } + if (size > maxMemorySizeHard) { + throw new HpackException(sm.getString("hpackdecoder.maxMemorySizeExceeded", Integer.valueOf(size), + Integer.valueOf(maxMemorySizeHard))); + } + maxMemorySizeSoft = size; + if (currentMemorySize > maxMemorySizeSoft) { + int newTableSlots = filledTableSlots; + int tableLength = headerTable.length; + int newSize = currentMemorySize; + while (newSize > maxMemorySizeSoft) { + int clearIndex = firstSlotPosition; + firstSlotPosition++; + if (firstSlotPosition == tableLength) { + firstSlotPosition = 0; + } + Hpack.HeaderField oldData = headerTable[clearIndex]; + headerTable[clearIndex] = null; + newSize -= oldData.size; + newTableSlots--; + } + this.filledTableSlots = newTableSlots; + currentMemorySize = newSize; + } + return true; + } + + private String readHeaderName(ByteBuffer buffer, int prefixLength) throws HpackException { + buffer.position(buffer.position() - 1); // unget the byte + int index = Hpack.decodeInteger(buffer, prefixLength); + if (index == -1) { + return null; + } else if (index != 0) { + return handleIndexedHeaderName(index); + } else { + return readHpackString(buffer); + } + } + + private String readHpackString(ByteBuffer buffer) throws HpackException { + if (!buffer.hasRemaining()) { + return null; + } + byte data = buffer.get(buffer.position()); + + int length = Hpack.decodeInteger(buffer, 7); + if (buffer.remaining() < length || length == -1) { + return null; + } + boolean huffman = (data & 0b10000000) != 0; + if (huffman) { + return readHuffmanString(length, buffer); + } + StringBuilder stringBuilder = new StringBuilder(length); + for (int i = 0; i < length; ++i) { + stringBuilder.append((char) buffer.get()); + } + return stringBuilder.toString(); + } + + private String readHuffmanString(int length, ByteBuffer buffer) throws HpackException { + StringBuilder stringBuilder = new StringBuilder(length); + HPackHuffman.decode(buffer, length, stringBuilder); + return stringBuilder.toString(); + } + + private String handleIndexedHeaderName(int index) throws HpackException { + if (index <= Hpack.STATIC_TABLE_LENGTH) { + return Hpack.STATIC_TABLE[index].name; + } else { + // index is 1 based + if (index > Hpack.STATIC_TABLE_LENGTH + filledTableSlots) { + throw new HpackException(sm.getString("hpackdecoder.headerTableIndexInvalid", Integer.valueOf(index), + Integer.valueOf(Hpack.STATIC_TABLE_LENGTH), Integer.valueOf(filledTableSlots))); + } + int adjustedIndex = getRealIndex(index - Hpack.STATIC_TABLE_LENGTH); + Hpack.HeaderField res = headerTable[adjustedIndex]; + if (res == null) { + throw new HpackException(sm.getString("hpackdecoder.nullHeader", Integer.valueOf(index))); + } + return res.name; + } + } + + /** + * Handle an indexed header representation + * + * @param index The index + * + * @throws HpackException If an error occurs processing the given index + */ + private void handleIndex(int index) throws HpackException { + if (index <= Hpack.STATIC_TABLE_LENGTH) { + addStaticTableEntry(index); + } else { + int adjustedIndex = getRealIndex(index - Hpack.STATIC_TABLE_LENGTH); + if (log.isTraceEnabled()) { + log.trace(sm.getString("hpackdecoder.useDynamic", Integer.valueOf(adjustedIndex))); + } + Hpack.HeaderField headerField = headerTable[adjustedIndex]; + emitHeader(headerField.name, headerField.value); + } + } + + /** + * because we use a ring buffer type construct, and don't actually shuffle items in the array, we need to figure out + * the real index to use. + *

    + * package private for unit tests + * + * @param index The index from the hpack + * + * @return the real index into the array + */ + int getRealIndex(int index) throws HpackException { + // the index is one based, but our table is zero based, hence -1 + // also because of our ring buffer setup the indexes are reversed + // index = 1 is at position firstSlotPosition + filledSlots + int realIndex = (firstSlotPosition + (filledTableSlots - index)) % headerTable.length; + if (realIndex < 0) { + throw new HpackException(sm.getString("hpackdecoder.headerTableIndexInvalid", Integer.valueOf(index), + Integer.valueOf(Hpack.STATIC_TABLE_LENGTH), Integer.valueOf(filledTableSlots))); + } + return realIndex; + } + + private void addStaticTableEntry(int index) throws HpackException { + // adds an entry from the static table. + if (log.isTraceEnabled()) { + log.trace(sm.getString("hpackdecoder.useStatic", Integer.valueOf(index))); + } + Hpack.HeaderField entry = Hpack.STATIC_TABLE[index]; + emitHeader(entry.name, (entry.value == null) ? "" : entry.value); + } + + private void addEntryToHeaderTable(Hpack.HeaderField entry) { + if (entry.size > maxMemorySizeSoft) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("hpackdecoder.clearDynamic")); + } + // it is to big to fit, so we just completely clear the table. + while (filledTableSlots > 0) { + headerTable[firstSlotPosition] = null; + firstSlotPosition++; + if (firstSlotPosition == headerTable.length) { + firstSlotPosition = 0; + } + filledTableSlots--; + } + currentMemorySize = 0; + return; + } + resizeIfRequired(); + int newTableSlots = filledTableSlots + 1; + int tableLength = headerTable.length; + int index = (firstSlotPosition + filledTableSlots) % tableLength; + if (log.isTraceEnabled()) { + log.trace(sm.getString("hpackdecoder.addDynamic", Integer.valueOf(index), entry.name, entry.value)); + } + headerTable[index] = entry; + int newSize = currentMemorySize + entry.size; + while (newSize > maxMemorySizeSoft) { + int clearIndex = firstSlotPosition; + firstSlotPosition++; + if (firstSlotPosition == tableLength) { + firstSlotPosition = 0; + } + Hpack.HeaderField oldData = headerTable[clearIndex]; + headerTable[clearIndex] = null; + newSize -= oldData.size; + newTableSlots--; + } + this.filledTableSlots = newTableSlots; + currentMemorySize = newSize; + } + + private void resizeIfRequired() { + if (filledTableSlots == headerTable.length) { + Hpack.HeaderField[] newArray = new Hpack.HeaderField[headerTable.length + 10]; // we only grow slowly + for (int i = 0; i < headerTable.length; ++i) { + newArray[i] = headerTable[(firstSlotPosition + i) % headerTable.length]; + } + firstSlotPosition = 0; + headerTable = newArray; + } + } + + + /** + * Interface implemented by the intended recipient of the headers. + */ + interface HeaderEmitter { + /** + * Pass a single header to the recipient. + * + * @param name Header name + * @param value Header value + * + * @throws HpackException If a header is received that is not compliant with the HTTP/2 specification + */ + void emitHeader(String name, String value) throws HpackException; + + /** + * Inform the recipient of the headers that a stream error needs to be triggered using the given message when + * {@link #validateHeaders()} is called. This is used when the Parser becomes aware of an error that is not + * visible to the recipient. + * + * @param streamException The exception to use when resetting the stream + */ + void setHeaderException(StreamException streamException); + + /** + * Are the headers pass to the recipient so far valid? The decoder needs to process all the headers to maintain + * state even if there is a problem. In addition, it is easy for the the intended recipient to track if the + * complete set of headers is valid since to do that state needs to be maintained between the parsing of the + * initial headers and the parsing of any trailer headers. The recipient is the best place to maintain that + * state. + * + * @throws StreamException If the headers received to date are not valid + */ + void validateHeaders() throws StreamException; + } + + + HeaderEmitter getHeaderEmitter() { + return headerEmitter; + } + + + void setHeaderEmitter(HeaderEmitter headerEmitter) { + this.headerEmitter = headerEmitter; + // Reset limit tracking + headerCount = 0; + countedCookie = false; + headerSize = 0; + } + + + void setMaxHeaderCount(int maxHeaderCount) { + this.maxHeaderCount = maxHeaderCount; + } + + + void setMaxHeaderSize(int maxHeaderSize) { + this.maxHeaderSize = maxHeaderSize; + } + + + private void emitHeader(String name, String value) throws HpackException { + // Header names are forced to lower case + if ("cookie".equals(name)) { + // Only count the cookie header once since HTTP/2 splits it into + // multiple headers to aid compression + if (!countedCookie) { + headerCount++; + countedCookie = true; + } + } else { + headerCount++; + } + // Overhead will vary. The main concern is that lots of small headers + // trigger the limiting mechanism correctly. Therefore, use an overhead + // estimate of 3 which is the worst case for small headers. + int inc = 3 + name.length() + value.length(); + headerSize += inc; + if (!isHeaderCountExceeded() && !isHeaderSizeExceeded(0)) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("hpackdecoder.emitHeader", name, value)); + } + headerEmitter.emitHeader(name, value); + } + } + + + boolean isHeaderCountExceeded() { + if (maxHeaderCount < 0) { + return false; + } + return headerCount > maxHeaderCount; + } + + + boolean isHeaderSizeExceeded(int unreadSize) { + if (maxHeaderSize < 0) { + return false; + } + return (headerSize + unreadSize) > maxHeaderSize; + } + + + boolean isHeaderSwallowSizeExceeded(int unreadSize) { + if (maxHeaderSize < 0) { + return false; + } + // Swallow the same again before closing the connection. + return (headerSize + unreadSize) > (2 * maxHeaderSize); + } + + + // package private fields for unit tests + + int getFirstSlotPosition() { + return firstSlotPosition; + } + + Hpack.HeaderField[] getHeaderTable() { + return headerTable; + } + + int getFilledTableSlots() { + return filledTableSlots; + } + + int getCurrentMemorySize() { + return currentMemorySize; + } + + int getMaxMemorySizeSoft() { + return maxMemorySizeSoft; + } +} diff --git a/java/org/apache/coyote/http2/HpackEncoder.java b/java/org/apache/coyote/http2/HpackEncoder.java new file mode 100644 index 0000000..391423e --- /dev/null +++ b/java/org/apache/coyote/http2/HpackEncoder.java @@ -0,0 +1,402 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.res.StringManager; + +/** + * Encoder for HPACK frames. + */ +class HpackEncoder { + + private static final Log log = LogFactory.getLog(HpackEncoder.class); + private static final StringManager sm = StringManager.getManager(HpackEncoder.class); + + private static final HpackHeaderFunction DEFAULT_HEADER_FUNCTION = new HpackHeaderFunction() { + @Override + public boolean shouldUseIndexing(String headerName, String value) { + // content length and date change all the time + // no need to index them, or they will churn the table + switch (headerName) { + case "content-length": + case "date": + return false; + default: + return true; + } + } + + @Override + public boolean shouldUseHuffman(String header, String value) { + return value.length() > 5; // TODO: figure out a good value for this + } + + @Override + public boolean shouldUseHuffman(String header) { + return header.length() > 5; // TODO: figure out a good value for this + } + + + }; + + private int headersIterator = -1; + private boolean firstPass = true; + + private MimeHeaders currentHeaders; + + private int entryPositionCounter; + + private int newMaxHeaderSize = -1; // if the max header size has been changed + private int minNewMaxHeaderSize = -1; // records the smallest value of newMaxHeaderSize, as per section 4.1 + + private static final Map ENCODING_STATIC_TABLE; + + private final Deque evictionQueue = new ArrayDeque<>(); + private final Map> dynamicTable = new HashMap<>(); // TODO: use a custom data structure to + // reduce allocations + + static { + Map map = new HashMap<>(); + for (int i = 1; i < Hpack.STATIC_TABLE.length; ++i) { + Hpack.HeaderField m = Hpack.STATIC_TABLE[i]; + TableEntry[] existing = map.get(m.name); + if (existing == null) { + map.put(m.name, new TableEntry[] { new TableEntry(m.name, m.value, i) }); + } else { + TableEntry[] newEntry = new TableEntry[existing.length + 1]; + System.arraycopy(existing, 0, newEntry, 0, existing.length); + newEntry[existing.length] = new TableEntry(m.name, m.value, i); + map.put(m.name, newEntry); + } + } + ENCODING_STATIC_TABLE = Collections.unmodifiableMap(map); + } + + /** + * The maximum table size + */ + private int maxTableSize = Hpack.DEFAULT_TABLE_SIZE; + + /** + * The current table size + */ + private int currentTableSize; + + private final HpackHeaderFunction hpackHeaderFunction; + + HpackEncoder() { + this.hpackHeaderFunction = DEFAULT_HEADER_FUNCTION; + } + + /** + * Encodes the headers into a buffer. + * + * @param headers The headers to encode + * @param target The buffer to which to write the encoded headers + * + * @return The state of the encoding process + */ + State encode(MimeHeaders headers, ByteBuffer target) { + int it = headersIterator; + if (headersIterator == -1) { + handleTableSizeChange(target); + // new headers map + it = 0; + currentHeaders = headers; + } else { + if (headers != currentHeaders) { + throw new IllegalStateException(); + } + } + while (it < currentHeaders.size()) { + // FIXME: Review lowercase policy + String headerName = headers.getName(it).toString().toLowerCase(Locale.US); + boolean skip = false; + if (firstPass) { + if (headerName.charAt(0) != ':') { + skip = true; + } + } else { + if (headerName.charAt(0) == ':') { + skip = true; + } + } + if (!skip) { + String val = headers.getValue(it).toString(); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("hpackEncoder.encodeHeader", headerName, val)); + } + TableEntry tableEntry = findInTable(headerName, val); + + // We use 11 to make sure we have enough room for the + // variable length integers + int required = 11 + headerName.length() + 1 + val.length(); + + if (target.remaining() < required) { + this.headersIterator = it; + return State.UNDERFLOW; + } + // Only index if it will fit + boolean canIndex = hpackHeaderFunction.shouldUseIndexing(headerName, val) && + (headerName.length() + val.length() + 32) < maxTableSize; + if (tableEntry == null && canIndex) { + // add the entry to the dynamic table + target.put((byte) (1 << 6)); + writeHuffmanEncodableName(target, headerName); + writeHuffmanEncodableValue(target, headerName, val); + addToDynamicTable(headerName, val); + } else if (tableEntry == null) { + // literal never indexed + target.put((byte) (1 << 4)); + writeHuffmanEncodableName(target, headerName); + writeHuffmanEncodableValue(target, headerName, val); + } else { + // so we know something is already in the table + if (val.equals(tableEntry.value)) { + // the whole thing is in the table + target.put((byte) (1 << 7)); + Hpack.encodeInteger(target, tableEntry.getPosition(), 7); + } else { + if (canIndex) { + // add the entry to the dynamic table + target.put((byte) (1 << 6)); + Hpack.encodeInteger(target, tableEntry.getPosition(), 6); + writeHuffmanEncodableValue(target, headerName, val); + addToDynamicTable(headerName, val); + + } else { + target.put((byte) (1 << 4)); + Hpack.encodeInteger(target, tableEntry.getPosition(), 4); + writeHuffmanEncodableValue(target, headerName, val); + } + } + } + + } + if (++it == currentHeaders.size() && firstPass) { + firstPass = false; + it = 0; + } + } + headersIterator = -1; + firstPass = true; + return State.COMPLETE; + } + + private void writeHuffmanEncodableName(ByteBuffer target, String headerName) { + if (hpackHeaderFunction.shouldUseHuffman(headerName)) { + if (HPackHuffman.encode(target, headerName, true)) { + return; + } + } + target.put((byte) 0); // to use encodeInteger we need to place the first byte in the buffer. + Hpack.encodeInteger(target, headerName.length(), 7); + for (int j = 0; j < headerName.length(); ++j) { + target.put((byte) Hpack.toLower(headerName.charAt(j))); + } + + } + + private void writeHuffmanEncodableValue(ByteBuffer target, String headerName, String val) { + if (hpackHeaderFunction.shouldUseHuffman(headerName, val)) { + if (!HPackHuffman.encode(target, val, false)) { + writeValueString(target, val); + } + } else { + writeValueString(target, val); + } + } + + private void writeValueString(ByteBuffer target, String val) { + target.put((byte) 0); // to use encodeInteger we need to place the first byte in the buffer. + Hpack.encodeInteger(target, val.length(), 7); + for (int j = 0; j < val.length(); ++j) { + target.put((byte) val.charAt(j)); + } + } + + private void addToDynamicTable(String headerName, String val) { + int pos = entryPositionCounter++; + DynamicTableEntry d = new DynamicTableEntry(headerName, val, -pos); + dynamicTable.computeIfAbsent(headerName, k -> new ArrayList<>(1)).add(d); + evictionQueue.add(d); + currentTableSize += d.getSize(); + runEvictionIfRequired(); + if (entryPositionCounter == Integer.MAX_VALUE) { + // prevent rollover + preventPositionRollover(); + } + + } + + + private void preventPositionRollover() { + // if the position counter is about to roll over we iterate all the table entries + // and set their position to their actual position + for (List tableEntries : dynamicTable.values()) { + for (TableEntry t : tableEntries) { + t.position = t.getPosition(); + } + } + entryPositionCounter = 0; + } + + private void runEvictionIfRequired() { + + while (currentTableSize > maxTableSize) { + TableEntry next = evictionQueue.poll(); + if (next == null) { + return; + } + currentTableSize -= next.size; + List list = dynamicTable.get(next.name); + list.remove(next); + if (list.isEmpty()) { + dynamicTable.remove(next.name); + } + } + } + + private TableEntry findInTable(String headerName, String value) { + TableEntry[] staticTable = ENCODING_STATIC_TABLE.get(headerName); + if (staticTable != null) { + for (TableEntry st : staticTable) { + if (st.value != null && st.value.equals(value)) { // todo: some form of lookup? + return st; + } + } + } + List dynamic = dynamicTable.get(headerName); + if (dynamic != null) { + for (TableEntry st : dynamic) { + if (st.value.equals(value)) { // todo: some form of lookup? + return st; + } + } + } + if (staticTable != null) { + return staticTable[0]; + } + return null; + } + + public void setMaxTableSize(int newSize) { + this.newMaxHeaderSize = newSize; + if (minNewMaxHeaderSize == -1) { + minNewMaxHeaderSize = newSize; + } else { + minNewMaxHeaderSize = Math.min(newSize, minNewMaxHeaderSize); + } + } + + private void handleTableSizeChange(ByteBuffer target) { + if (newMaxHeaderSize == -1) { + return; + } + if (minNewMaxHeaderSize != newMaxHeaderSize) { + target.put((byte) (1 << 5)); + Hpack.encodeInteger(target, minNewMaxHeaderSize, 5); + } + target.put((byte) (1 << 5)); + Hpack.encodeInteger(target, newMaxHeaderSize, 5); + maxTableSize = newMaxHeaderSize; + runEvictionIfRequired(); + newMaxHeaderSize = -1; + minNewMaxHeaderSize = -1; + } + + enum State { + COMPLETE, + UNDERFLOW, + + } + + private static class TableEntry { + private final String name; + private final String value; + private final int size; + private int position; + + private TableEntry(String name, String value, int position) { + this.name = name; + this.value = value; + this.position = position; + if (value != null) { + this.size = 32 + name.length() + value.length(); + } else { + this.size = -1; + } + } + + int getPosition() { + return position; + } + + int getSize() { + return size; + } + } + + private class DynamicTableEntry extends TableEntry { + + private DynamicTableEntry(String name, String value, int position) { + super(name, value, position); + } + + @Override + int getPosition() { + return super.getPosition() + entryPositionCounter + Hpack.STATIC_TABLE_LENGTH; + } + } + + private interface HpackHeaderFunction { + boolean shouldUseIndexing(String header, String value); + + /** + * Returns true if huffman encoding should be used on the header value + * + * @param header The header name + * @param value The header value to be encoded + * + * @return true if the value should be encoded + */ + boolean shouldUseHuffman(String header, String value); + + /** + * Returns true if huffman encoding should be used on the header name + * + * @param header The header name to be encoded + * + * @return true if the value should be encoded + */ + boolean shouldUseHuffman(String header); + } +} diff --git a/java/org/apache/coyote/http2/HpackException.java b/java/org/apache/coyote/http2/HpackException.java new file mode 100644 index 0000000..40629af --- /dev/null +++ b/java/org/apache/coyote/http2/HpackException.java @@ -0,0 +1,33 @@ +/* + * 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.coyote.http2; + +/** + * Exception that is thrown when the HPACK compress context is broken. In this case the connection must be closed. + */ +class HpackException extends Exception { + + private static final long serialVersionUID = 1L; + + HpackException(String message) { + super(message); + } + + HpackException() { + super(); + } +} diff --git a/java/org/apache/coyote/http2/Http2AsyncParser.java b/java/org/apache/coyote/http2/Http2AsyncParser.java new file mode 100644 index 0000000..e1c8170 --- /dev/null +++ b/java/org/apache/coyote/http2/Http2AsyncParser.java @@ -0,0 +1,343 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.http.WebConnection; + +import org.apache.coyote.ProtocolException; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.net.SocketWrapperBase.BlockingMode; +import org.apache.tomcat.util.net.SocketWrapperBase.CompletionCheck; +import org.apache.tomcat.util.net.SocketWrapperBase.CompletionHandlerCall; +import org.apache.tomcat.util.net.SocketWrapperBase.CompletionState; + +class Http2AsyncParser extends Http2Parser { + + private final SocketWrapperBase socketWrapper; + private final Http2AsyncUpgradeHandler upgradeHandler; + private volatile Throwable error = null; + + Http2AsyncParser(String connectionId, Input input, Output output, SocketWrapperBase socketWrapper, + Http2AsyncUpgradeHandler upgradeHandler) { + super(connectionId, input, output); + this.socketWrapper = socketWrapper; + socketWrapper.getSocketBufferHandler().expand(input.getMaxFrameSize()); + this.upgradeHandler = upgradeHandler; + } + + + @Override + void readConnectionPreface(WebConnection webConnection, Stream stream) throws Http2Exception { + byte[] prefaceData = new byte[CLIENT_PREFACE_START.length]; + ByteBuffer preface = ByteBuffer.wrap(prefaceData); + ByteBuffer header = ByteBuffer.allocate(9); + ByteBuffer framePayload = ByteBuffer.allocate(input.getMaxFrameSize()); + PrefaceCompletionHandler handler = + new PrefaceCompletionHandler(webConnection, stream, prefaceData, preface, header, framePayload); + socketWrapper.read(BlockingMode.NON_BLOCK, socketWrapper.getReadTimeout(), TimeUnit.MILLISECONDS, null, handler, + handler, preface, header, framePayload); + } + + + private class PrefaceCompletionHandler extends FrameCompletionHandler { + + private final WebConnection webConnection; + private final Stream stream; + private final byte[] prefaceData; + + private volatile boolean prefaceValidated = false; + + private PrefaceCompletionHandler(WebConnection webConnection, Stream stream, byte[] prefaceData, + ByteBuffer... buffers) { + super(FrameType.SETTINGS, buffers); + this.webConnection = webConnection; + this.stream = stream; + this.prefaceData = prefaceData; + } + + @Override + public CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers, int offset, int length) { + if (offset != 0 || length != 3) { + try { + throw new IllegalArgumentException(sm.getString("http2Parser.invalidBuffers")); + } catch (IllegalArgumentException e) { + error = e; + return CompletionHandlerCall.DONE; + } + } + if (!prefaceValidated) { + if (buffers[0].hasRemaining()) { + // The preface must be fully read before being validated + return CompletionHandlerCall.CONTINUE; + } + // Validate preface content + for (int i = 0; i < CLIENT_PREFACE_START.length; i++) { + if (CLIENT_PREFACE_START[i] != prefaceData[i]) { + error = new ProtocolException(sm.getString("http2Parser.preface.invalid")); + return CompletionHandlerCall.DONE; + } + } + prefaceValidated = true; + } + return validate(state, buffers[1], buffers[2]); + } + + @Override + public void completed(Long result, Void attachment) { + if (streamException || error == null) { + ByteBuffer payload = buffers[2]; + payload.flip(); + try { + if (streamException) { + swallowPayload(streamId, frameTypeId, payloadSize, false, payload); + } else { + readSettingsFrame(flags, payloadSize, payload); + } + } catch (RuntimeException | IOException | Http2Exception e) { + error = e; + } + // Any extra frame is not processed yet, so put back any leftover data + if (payload.hasRemaining()) { + socketWrapper.unRead(payload); + } + // Finish processing the connection + upgradeHandler.processConnectionCallback(webConnection, stream); + } else { + upgradeHandler + .closeConnection(new ConnectionException(error.getMessage(), Http2Error.PROTOCOL_ERROR, error)); + } + // Continue reading frames + upgradeHandler.upgradeDispatch(SocketEvent.OPEN_READ); + } + } + + @Override + protected boolean readFrame(boolean block, FrameType expected) throws IOException, Http2Exception { + handleAsyncException(); + ByteBuffer header = ByteBuffer.allocate(9); + ByteBuffer framePayload = ByteBuffer.allocate(input.getMaxFrameSize()); + FrameCompletionHandler handler = new FrameCompletionHandler(expected, header, framePayload); + CompletionState state = socketWrapper.read(block ? BlockingMode.BLOCK : BlockingMode.NON_BLOCK, + block ? socketWrapper.getReadTimeout() : 0, TimeUnit.MILLISECONDS, null, handler, handler, header, + framePayload); + if (state == CompletionState.ERROR || state == CompletionState.INLINE) { + handleAsyncException(); + return true; + } else { + return false; + } + } + + private void handleAsyncException() throws IOException, Http2Exception { + if (error != null) { + Throwable error = this.error; + this.error = null; + if (error instanceof Http2Exception) { + throw (Http2Exception) error; + } else if (error instanceof IOException) { + throw (IOException) error; + } else if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } else { + throw new RuntimeException(error); + } + } + } + + private class FrameCompletionHandler implements CompletionCheck, CompletionHandler { + + private final FrameType expected; + protected final ByteBuffer[] buffers; + + private volatile boolean parsedFrameHeader = false; + private volatile boolean validated = false; + private volatile CompletionState state = null; + protected volatile int payloadSize; + protected volatile int frameTypeId; + protected volatile FrameType frameType; + protected volatile int flags; + protected volatile int streamId; + protected volatile boolean streamException = false; + + private FrameCompletionHandler(FrameType expected, ByteBuffer... buffers) { + this.expected = expected; + this.buffers = buffers; + } + + @Override + public CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers, int offset, int length) { + if (offset != 0 || length != 2) { + try { + throw new IllegalArgumentException(sm.getString("http2Parser.invalidBuffers")); + } catch (IllegalArgumentException e) { + error = e; + return CompletionHandlerCall.DONE; + } + } + return validate(state, buffers[0], buffers[1]); + } + + protected CompletionHandlerCall validate(CompletionState state, ByteBuffer frameHeaderBuffer, + ByteBuffer payload) { + if (!parsedFrameHeader) { + // The first buffer should be 9 bytes long + if (frameHeaderBuffer.position() < 9) { + return CompletionHandlerCall.CONTINUE; + } + parsedFrameHeader = true; + payloadSize = ByteUtil.getThreeBytes(frameHeaderBuffer, 0); + frameTypeId = ByteUtil.getOneByte(frameHeaderBuffer, 3); + frameType = FrameType.valueOf(frameTypeId); + flags = ByteUtil.getOneByte(frameHeaderBuffer, 4); + streamId = ByteUtil.get31Bits(frameHeaderBuffer, 5); + } + this.state = state; + + if (!validated) { + validated = true; + try { + validateFrame(expected, frameType, streamId, flags, payloadSize); + } catch (StreamException e) { + error = e; + streamException = true; + } catch (Http2Exception e) { + error = e; + // The problem will be handled later, consider the frame read is done + return CompletionHandlerCall.DONE; + } + } + + if (payload.position() < payloadSize) { + return CompletionHandlerCall.CONTINUE; + } + + return CompletionHandlerCall.DONE; + } + + @Override + public void completed(Long result, Void attachment) { + if (streamException || error == null) { + ByteBuffer payload = buffers[1]; + payload.flip(); + try { + boolean continueParsing; + do { + continueParsing = false; + if (streamException) { + swallowPayload(streamId, frameTypeId, payloadSize, false, payload); + } else { + switch (frameType) { + case DATA: + readDataFrame(streamId, flags, payloadSize, payload); + break; + case HEADERS: + readHeadersFrame(streamId, flags, payloadSize, payload); + break; + case PRIORITY: + readPriorityFrame(streamId, payload); + break; + case RST: + readRstFrame(streamId, payload); + break; + case SETTINGS: + readSettingsFrame(flags, payloadSize, payload); + break; + case PUSH_PROMISE: + readPushPromiseFrame(streamId, flags, payloadSize, payload); + break; + case PING: + readPingFrame(flags, payload); + break; + case GOAWAY: + readGoawayFrame(payloadSize, payload); + break; + case WINDOW_UPDATE: + readWindowUpdateFrame(streamId, payload); + break; + case CONTINUATION: + readContinuationFrame(streamId, flags, payloadSize, payload); + break; + case PRIORITY_UPDATE: + readPriorityUpdateFrame(payloadSize, payload); + break; + case UNKNOWN: + readUnknownFrame(streamId, frameTypeId, flags, payloadSize, payload); + } + } + if (!upgradeHandler.isOverheadLimitExceeded()) { + // See if there is a new 9 byte header and continue parsing if possible + if (payload.remaining() >= 9) { + int position = payload.position(); + payloadSize = ByteUtil.getThreeBytes(payload, position); + frameTypeId = ByteUtil.getOneByte(payload, position + 3); + frameType = FrameType.valueOf(frameTypeId); + flags = ByteUtil.getOneByte(payload, position + 4); + streamId = ByteUtil.get31Bits(payload, position + 5); + streamException = false; + if (payload.remaining() - 9 >= payloadSize) { + continueParsing = true; + // Now go over frame header + payload.position(payload.position() + 9); + try { + validateFrame(null, frameType, streamId, flags, payloadSize); + } catch (StreamException e) { + error = e; + streamException = true; + } catch (Http2Exception e) { + error = e; + continueParsing = false; + } + } + } + } + } while (continueParsing); + } catch (RuntimeException | IOException | Http2Exception e) { + error = e; + } finally { + if (payload.hasRemaining()) { + socketWrapper.unRead(payload); + } + } + } + if (state == CompletionState.DONE) { + // The call was not completed inline, so must start reading new frames + // or process the stream exception + upgradeHandler.upgradeDispatch(SocketEvent.OPEN_READ); + } + } + + @Override + public void failed(Throwable e, Void attachment) { + // Always a fatal IO error + error = e; + if (log.isDebugEnabled()) { + log.debug(sm.getString("http2Parser.error", connectionId, Integer.valueOf(streamId), frameType), e); + } + if (state == null || state == CompletionState.DONE) { + upgradeHandler.upgradeDispatch(SocketEvent.ERROR); + } + } + + } + +} diff --git a/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java b/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java new file mode 100644 index 0000000..2d873b0 --- /dev/null +++ b/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java @@ -0,0 +1,558 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import jakarta.servlet.http.WebConnection; + +import org.apache.coyote.Adapter; +import org.apache.coyote.ProtocolException; +import org.apache.coyote.Request; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.net.SendfileState; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.net.SocketWrapperBase.BlockingMode; +import org.apache.tomcat.util.net.SocketWrapperBase.CompletionState; + +public class Http2AsyncUpgradeHandler extends Http2UpgradeHandler { + + private static final ByteBuffer[] BYTEBUFFER_ARRAY = new ByteBuffer[0]; + // Ensures headers are generated and then written for one thread at a time. + // Because of the compression used, headers need to be written to the + // network in the same order they are generated. + private final Lock headerWriteLock = new ReentrantLock(); + // Ensures thread triggers the stream reset is the first to send a RST frame + private final Lock sendResetLock = new ReentrantLock(); + private final AtomicReference error = new AtomicReference<>(); + private final AtomicReference applicationIOE = new AtomicReference<>(); + + public Http2AsyncUpgradeHandler(Http2Protocol protocol, Adapter adapter, Request coyoteRequest, + SocketWrapperBase socketWrapper) { + super(protocol, adapter, coyoteRequest, socketWrapper); + } + + private final CompletionHandler errorCompletion = new CompletionHandler<>() { + @Override + public void completed(Long result, Void attachment) { + } + + @Override + public void failed(Throwable t, Void attachment) { + error.set(t); + } + }; + private final CompletionHandler applicationErrorCompletion = new CompletionHandler<>() { + @Override + public void completed(Long result, Void attachment) { + } + + @Override + public void failed(Throwable t, Void attachment) { + if (t instanceof IOException) { + applicationIOE.set((IOException) t); + } + error.set(t); + } + }; + + @Override + protected Http2Parser getParser(String connectionId) { + return new Http2AsyncParser(connectionId, this, this, socketWrapper, this); + } + + + @Override + protected PingManager getPingManager() { + return new AsyncPingManager(); + } + + + @Override + public boolean hasAsyncIO() { + return true; + } + + + @Override + protected void processConnection(WebConnection webConnection, Stream stream) { + // The end of the processing will instead be an async callback + } + + void processConnectionCallback(WebConnection webConnection, Stream stream) { + super.processConnection(webConnection, stream); + } + + + @Override + protected void writeSettings() { + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, + ByteBuffer.wrap(localSettings.getSettingsFrameForPending()), + ByteBuffer.wrap(createWindowUpdateForSettings())); + Throwable err = error.get(); + if (err != null) { + String msg = sm.getString("upgradeHandler.sendPrefaceFail", connectionId); + if (log.isDebugEnabled()) { + log.debug(msg); + } + throw new ProtocolException(msg, err); + } + } + + + @Override + void sendStreamReset(StreamStateMachine state, StreamException se) throws IOException { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.rst.debug", connectionId, Integer.toString(se.getStreamId()), + se.getError(), se.getMessage())); + } + // Write a RST frame + byte[] rstFrame = new byte[13]; + // Length + ByteUtil.setThreeBytes(rstFrame, 0, 4); + // Type + rstFrame[3] = FrameType.RST.getIdByte(); + // No flags + // Stream ID + ByteUtil.set31Bits(rstFrame, 5, se.getStreamId()); + // Payload + ByteUtil.setFourBytes(rstFrame, 9, se.getError().getCode()); + + // Need to update state atomically with the sending of the RST + // frame else other threads currently working with this stream + // may see the state change and send a RST frame before the RST + // frame triggered by this thread. If that happens the client + // may see out of order RST frames which may hard to follow if + // the client is unaware the RST frames may be received out of + // order. + sendResetLock.lock(); + try { + if (state != null) { + boolean active = state.isActive(); + state.sendReset(); + if (active) { + decrementActiveRemoteStreamCount(); + } + } + + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, ByteBuffer.wrap(rstFrame)); + } finally { + sendResetLock.unlock(); + } + handleAsyncException(); + } + + + @Override + protected void writeGoAwayFrame(int maxStreamId, long errorCode, byte[] debugMsg) throws IOException { + byte[] fixedPayload = new byte[8]; + ByteUtil.set31Bits(fixedPayload, 0, maxStreamId); + ByteUtil.setFourBytes(fixedPayload, 4, errorCode); + int len = 8; + if (debugMsg != null) { + len += debugMsg.length; + } + byte[] payloadLength = new byte[3]; + ByteUtil.setThreeBytes(payloadLength, 0, len); + if (debugMsg != null) { + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, ByteBuffer.wrap(payloadLength), + ByteBuffer.wrap(GOAWAY), ByteBuffer.wrap(fixedPayload), ByteBuffer.wrap(debugMsg)); + } else { + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, ByteBuffer.wrap(payloadLength), + ByteBuffer.wrap(GOAWAY), ByteBuffer.wrap(fixedPayload)); + } + handleAsyncException(); + } + + + @Override + void writeHeaders(Stream stream, int pushedStreamId, MimeHeaders mimeHeaders, boolean endOfStream, int payloadSize) + throws IOException { + headerWriteLock.lock(); + try { + AsyncHeaderFrameBuffers headerFrameBuffers = (AsyncHeaderFrameBuffers) doWriteHeaders(stream, + pushedStreamId, mimeHeaders, endOfStream, payloadSize); + if (headerFrameBuffers != null) { + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, applicationErrorCompletion, + headerFrameBuffers.bufs.toArray(BYTEBUFFER_ARRAY)); + handleAsyncException(); + } + } finally { + headerWriteLock.unlock(); + } + if (endOfStream) { + sentEndOfStream(stream); + } + } + + + @Override + protected HeaderFrameBuffers getHeaderFrameBuffers(int initialPayloadSize) { + return new AsyncHeaderFrameBuffers(initialPayloadSize); + } + + + @Override + void writeBody(Stream stream, ByteBuffer data, int len, boolean finished) throws IOException { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.writeBody", connectionId, stream.getIdAsString(), + Integer.toString(len), Boolean.valueOf(finished))); + } + + reduceOverheadCount(FrameType.DATA); + + // Need to check this now since sending end of stream will change this. + boolean writable = stream.canWrite(); + byte[] header = new byte[9]; + ByteUtil.setThreeBytes(header, 0, len); + header[3] = FrameType.DATA.getIdByte(); + if (finished) { + header[4] = FLAG_END_OF_STREAM; + sentEndOfStream(stream); + } + if (writable) { + ByteUtil.set31Bits(header, 5, stream.getIdAsInt()); + int orgLimit = data.limit(); + data.limit(data.position() + len); + socketWrapper.write(BlockingMode.BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, applicationErrorCompletion, ByteBuffer.wrap(header), data); + data.limit(orgLimit); + handleAsyncException(); + } + } + + + @Override + void writeWindowUpdate(AbstractNonZeroStream stream, int increment, boolean applicationInitiated) + throws IOException { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.windowUpdateConnection", getConnectionId(), + Integer.valueOf(increment))); + } + // Build window update frame for stream 0 + byte[] frame = new byte[13]; + ByteUtil.setThreeBytes(frame, 0, 4); + frame[3] = FrameType.WINDOW_UPDATE.getIdByte(); + ByteUtil.set31Bits(frame, 9, increment); + boolean neetToWriteConnectionUpdate = true; + // No need to send update from closed stream + if (stream instanceof Stream && ((Stream) stream).canWrite()) { + int streamIncrement = ((Stream) stream).getWindowUpdateSizeToWrite(increment); + if (streamIncrement > 0) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.windowUpdateStream", getConnectionId(), getIdAsString(), + Integer.valueOf(streamIncrement))); + } + byte[] frame2 = new byte[13]; + ByteUtil.setThreeBytes(frame2, 0, 4); + frame2[3] = FrameType.WINDOW_UPDATE.getIdByte(); + ByteUtil.set31Bits(frame2, 9, streamIncrement); + ByteUtil.set31Bits(frame2, 5, stream.getIdAsInt()); + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, ByteBuffer.wrap(frame), + ByteBuffer.wrap(frame2)); + neetToWriteConnectionUpdate = false; + } + } + if (neetToWriteConnectionUpdate) { + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, ByteBuffer.wrap(frame)); + } + handleAsyncException(); + } + + + @Override + public void settingsEnd(boolean ack) throws IOException { + if (ack) { + if (!localSettings.ack()) { + // Ack was unexpected + log.warn(sm.getString("upgradeHandler.unexpectedAck", connectionId, getIdAsString())); + } + } else { + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, ByteBuffer.wrap(SETTINGS_ACK)); + } + handleAsyncException(); + } + + + private void handleAsyncException() throws IOException { + IOException ioe = applicationIOE.getAndSet(null); + if (ioe != null) { + handleAppInitiatedIOException(ioe); + } else { + Throwable err = this.error.getAndSet(null); + if (err != null) { + if (err instanceof IOException) { + throw (IOException) err; + } else { + throw new IOException(err); + } + } + } + } + + @Override + protected SendfileState processSendfile(SendfileData sendfile) { + if (sendfile != null) { + try { + try (FileChannel channel = FileChannel.open(sendfile.path, StandardOpenOption.READ)) { + sendfile.mappedBuffer = channel.map(MapMode.READ_ONLY, sendfile.pos, sendfile.end - sendfile.pos); + } + // Reserve as much as possible right away + int reservation = (sendfile.end - sendfile.pos > Integer.MAX_VALUE) ? Integer.MAX_VALUE : + (int) (sendfile.end - sendfile.pos); + sendfile.streamReservation = sendfile.stream.reserveWindowSize(reservation, true); + sendfile.connectionReservation = reserveWindowSize(sendfile.stream, sendfile.streamReservation, true); + } catch (IOException e) { + return SendfileState.ERROR; + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.sendfile.reservation", connectionId, + sendfile.stream.getIdAsString(), Integer.valueOf(sendfile.connectionReservation), + Integer.valueOf(sendfile.streamReservation))); + } + + // connectionReservation will always be smaller than or the same as + // streamReservation + int frameSize = Integer.min(getMaxFrameSize(), sendfile.connectionReservation); + boolean finished = + (frameSize == sendfile.left) && sendfile.stream.getCoyoteResponse().getTrailerFields() == null; + + // Need to check this now since sending end of stream will change this. + boolean writable = sendfile.stream.canWrite(); + byte[] header = new byte[9]; + ByteUtil.setThreeBytes(header, 0, frameSize); + header[3] = FrameType.DATA.getIdByte(); + if (finished) { + header[4] = FLAG_END_OF_STREAM; + sentEndOfStream(sendfile.stream); + } + if (writable) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.writeBody", connectionId, sendfile.stream.getIdAsString(), + Integer.toString(frameSize), Boolean.valueOf(finished))); + } + ByteUtil.set31Bits(header, 5, sendfile.stream.getIdAsInt()); + sendfile.mappedBuffer.limit(sendfile.mappedBuffer.position() + frameSize); + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, + sendfile, SocketWrapperBase.COMPLETE_WRITE_WITH_COMPLETION, new SendfileCompletionHandler(), + ByteBuffer.wrap(header), sendfile.mappedBuffer); + try { + handleAsyncException(); + } catch (IOException e) { + return SendfileState.ERROR; + } + } + return SendfileState.PENDING; + } else { + return SendfileState.DONE; + } + } + + protected class SendfileCompletionHandler implements CompletionHandler { + @Override + public void completed(Long nBytes, SendfileData sendfile) { + CompletionState completionState = null; + long bytesWritten = nBytes.longValue() - 9; + + /* + * Loop for in-line writes only. Avoids a possible stack-overflow of chained completion handlers with a long + * series of in-line writes. + */ + do { + sendfile.left -= bytesWritten; + if (sendfile.left == 0) { + try { + sendfile.stream.getOutputBuffer().end(); + } catch (IOException e) { + failed(e, sendfile); + } + return; + } + sendfile.streamReservation -= bytesWritten; + sendfile.connectionReservation -= bytesWritten; + sendfile.pos += bytesWritten; + try { + if (sendfile.connectionReservation == 0) { + if (sendfile.streamReservation == 0) { + int reservation = (sendfile.end - sendfile.pos > Integer.MAX_VALUE) ? Integer.MAX_VALUE : + (int) (sendfile.end - sendfile.pos); + sendfile.streamReservation = sendfile.stream.reserveWindowSize(reservation, true); + } + sendfile.connectionReservation = + reserveWindowSize(sendfile.stream, sendfile.streamReservation, true); + } + } catch (IOException e) { + failed(e, sendfile); + return; + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.sendfile.reservation", connectionId, + sendfile.stream.getIdAsString(), Integer.valueOf(sendfile.connectionReservation), + Integer.valueOf(sendfile.streamReservation))); + } + + // connectionReservation will always be smaller than or the same as + // streamReservation + int frameSize = Integer.min(getMaxFrameSize(), sendfile.connectionReservation); + boolean finished = + (frameSize == sendfile.left) && sendfile.stream.getCoyoteResponse().getTrailerFields() == null; + + // Need to check this now since sending end of stream will change this. + boolean writable = sendfile.stream.canWrite(); + byte[] header = new byte[9]; + ByteUtil.setThreeBytes(header, 0, frameSize); + header[3] = FrameType.DATA.getIdByte(); + if (finished) { + header[4] = FLAG_END_OF_STREAM; + sentEndOfStream(sendfile.stream); + } + if (writable) { + if (log.isTraceEnabled()) { + log.trace( + sm.getString("upgradeHandler.writeBody", connectionId, sendfile.stream.getIdAsString(), + Integer.toString(frameSize), Boolean.valueOf(finished))); + } + ByteUtil.set31Bits(header, 5, sendfile.stream.getIdAsInt()); + sendfile.mappedBuffer.limit(sendfile.mappedBuffer.position() + frameSize); + // Note: Completion handler not called in the write + // completes in-line. The wrote will continue via the + // surrounding loop. + completionState = socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), + TimeUnit.MILLISECONDS, sendfile, SocketWrapperBase.COMPLETE_WRITE, this, + ByteBuffer.wrap(header), sendfile.mappedBuffer); + try { + handleAsyncException(); + } catch (IOException e) { + failed(e, sendfile); + return; + } + } + // Update bytesWritten for start of next loop iteration + bytesWritten = frameSize; + } while (completionState == CompletionState.INLINE); + } + + @Override + public void failed(Throwable t, SendfileData sendfile) { + applicationErrorCompletion.failed(t, null); + } + } + + protected class AsyncPingManager extends PingManager { + @Override + public void sendPing(boolean force) throws IOException { + if (initiateDisabled) { + return; + } + long now = System.nanoTime(); + if (force || now - lastPingNanoTime > pingIntervalNano) { + lastPingNanoTime = now; + byte[] payload = new byte[8]; + int sentSequence = ++sequence; + PingRecord pingRecord = new PingRecord(sentSequence, now); + inflightPings.add(pingRecord); + ByteUtil.set31Bits(payload, 4, sentSequence); + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, ByteBuffer.wrap(PING), + ByteBuffer.wrap(payload)); + handleAsyncException(); + } + } + + @Override + public void receivePing(byte[] payload, boolean ack) throws IOException { + if (ack) { + super.receivePing(payload, ack); + } else { + // Client originated ping. Echo it back. + socketWrapper.write(BlockingMode.SEMI_BLOCK, protocol.getWriteTimeout(), TimeUnit.MILLISECONDS, null, + SocketWrapperBase.COMPLETE_WRITE, errorCompletion, ByteBuffer.wrap(PING_ACK), + ByteBuffer.wrap(payload)); + handleAsyncException(); + } + } + + } + + + private static class AsyncHeaderFrameBuffers implements HeaderFrameBuffers { + + int payloadSize; + + private byte[] header; + private ByteBuffer payload; + + private final List bufs = new ArrayList<>(); + + AsyncHeaderFrameBuffers(int initialPayloadSize) { + this.payloadSize = initialPayloadSize; + } + + @Override + public void startFrame() { + header = new byte[9]; + payload = ByteBuffer.allocate(payloadSize); + } + + @Override + public void endFrame() throws IOException { + bufs.add(ByteBuffer.wrap(header)); + bufs.add(payload); + } + + @Override + public void endHeaders() throws IOException { + } + + @Override + public byte[] getHeader() { + return header; + } + + @Override + public ByteBuffer getPayload() { + return payload; + } + + @Override + public void expandPayload() { + payloadSize = payloadSize * 2; + payload = ByteBuffer.allocate(payloadSize); + } + } +} diff --git a/java/org/apache/coyote/http2/Http2Error.java b/java/org/apache/coyote/http2/Http2Error.java new file mode 100644 index 0000000..0a0eae4 --- /dev/null +++ b/java/org/apache/coyote/http2/Http2Error.java @@ -0,0 +1,54 @@ +/* + * 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.coyote.http2; + +enum Http2Error { + // @formatter:off + NO_ERROR (0x00), + PROTOCOL_ERROR (0x01), + INTERNAL_ERROR (0x02), + FLOW_CONTROL_ERROR (0x03), + SETTINGS_TIMEOUT (0x04), + STREAM_CLOSED (0x05), + FRAME_SIZE_ERROR (0x06), + REFUSED_STREAM (0x07), + CANCEL (0x08), + COMPRESSION_ERROR (0x09), + CONNECT_ERROR (0x0a), + ENHANCE_YOUR_CALM (0x0b), + INADEQUATE_SECURITY (0x0c), + HTTP_1_1_REQUIRED (0x0d); + // @formatter:on + + private final long code; + + Http2Error(long code) { + this.code = code; + } + + + long getCode() { + return code; + } + + + byte[] getCodeBytes() { + byte[] codeByte = new byte[4]; + ByteUtil.setFourBytes(codeByte, 0, code); + return codeByte; + } +} diff --git a/java/org/apache/coyote/http2/Http2Exception.java b/java/org/apache/coyote/http2/Http2Exception.java new file mode 100644 index 0000000..583af37 --- /dev/null +++ b/java/org/apache/coyote/http2/Http2Exception.java @@ -0,0 +1,41 @@ +/* + * 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.coyote.http2; + +abstract class Http2Exception extends Exception { + + private static final long serialVersionUID = 1L; + + private final Http2Error error; + + + Http2Exception(String msg, Http2Error error) { + super(msg); + this.error = error; + } + + + Http2Exception(String msg, Http2Error error, Throwable cause) { + super(msg, cause); + this.error = error; + } + + + Http2Error getError() { + return error; + } +} diff --git a/java/org/apache/coyote/http2/Http2OutputBuffer.java b/java/org/apache/coyote/http2/Http2OutputBuffer.java new file mode 100644 index 0000000..1de4569 --- /dev/null +++ b/java/org/apache/coyote/http2/Http2OutputBuffer.java @@ -0,0 +1,77 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.coyote.Response; +import org.apache.coyote.http11.HttpOutputBuffer; +import org.apache.coyote.http11.OutputFilter; +import org.apache.coyote.http2.Stream.StreamOutputBuffer; + +public class Http2OutputBuffer implements HttpOutputBuffer { + + private final Response coyoteResponse; + private HttpOutputBuffer next; + + + /** + * Add a filter at the start of the existing processing chain. Subsequent calls to the {@link HttpOutputBuffer} + * methods of this object will be passed to the filter. If appropriate, the filter will then call the same method on + * the next HttpOutputBuffer in the chain until the call reaches the StreamOutputBuffer. + * + * @param filter The filter to add to the start of the processing chain + */ + public void addFilter(OutputFilter filter) { + filter.setBuffer(next); + next = filter; + } + + + public Http2OutputBuffer(Response coyoteResponse, StreamOutputBuffer streamOutputBuffer) { + this.coyoteResponse = coyoteResponse; + this.next = streamOutputBuffer; + } + + + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + if (!coyoteResponse.isCommitted()) { + coyoteResponse.sendHeaders(); + } + return next.doWrite(chunk); + } + + + @Override + public long getBytesWritten() { + return next.getBytesWritten(); + } + + + @Override + public void end() throws IOException { + next.end(); + } + + + @Override + public void flush() throws IOException { + next.flush(); + } +} diff --git a/java/org/apache/coyote/http2/Http2Parser.java b/java/org/apache/coyote/http2/Http2Parser.java new file mode 100644 index 0000000..b9f95e3 --- /dev/null +++ b/java/org/apache/coyote/http2/Http2Parser.java @@ -0,0 +1,833 @@ +/* + * 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.coyote.http2; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.http.WebConnection; + +import org.apache.coyote.ProtocolException; +import org.apache.coyote.http2.HpackDecoder.HeaderEmitter; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteBufferUtils; +import org.apache.tomcat.util.http.parser.Priority; +import org.apache.tomcat.util.res.StringManager; + +class Http2Parser { + + protected static final Log log = LogFactory.getLog(Http2Parser.class); + protected static final StringManager sm = StringManager.getManager(Http2Parser.class); + + static final byte[] CLIENT_PREFACE_START = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1); + + protected final String connectionId; + protected final Input input; + private final Output output; + private final byte[] frameHeaderBuffer = new byte[9]; + + private volatile HpackDecoder hpackDecoder; + private volatile ByteBuffer headerReadBuffer = ByteBuffer.allocate(Constants.DEFAULT_HEADER_READ_BUFFER_SIZE); + private volatile int headersCurrentStream = -1; + private volatile boolean headersEndStream = false; + + Http2Parser(String connectionId, Input input, Output output) { + this.connectionId = connectionId; + this.input = input; + this.output = output; + } + + + /** + * Read and process a single frame. Once the start of a frame is read, the remainder will be read using blocking IO. + * + * @param block Should this method block until a frame is available if no frame is available immediately? + * + * @return true if a frame was read otherwise false + * + * @throws IOException If an IO error occurs while trying to read a frame + * + * @deprecated Unused. Will be removed in Tomcat 11 onwards. + */ + @Deprecated + boolean readFrame(boolean block) throws Http2Exception, IOException { + return readFrame(block, null); + } + + + /** + * Read and process a single frame. The initial read is non-blocking to determine if a frame is present. Once the + * start of a frame is read, the remainder will be read using blocking IO. + * + * @return true if a frame was read otherwise false + * + * @throws IOException If an IO error occurs while trying to read a frame + */ + boolean readFrame() throws Http2Exception, IOException { + return readFrame(false, null); + } + + + protected boolean readFrame(boolean block, FrameType expected) throws IOException, Http2Exception { + + if (!input.fill(block, frameHeaderBuffer)) { + return false; + } + + int payloadSize = ByteUtil.getThreeBytes(frameHeaderBuffer, 0); + int frameTypeId = ByteUtil.getOneByte(frameHeaderBuffer, 3); + FrameType frameType = FrameType.valueOf(frameTypeId); + int flags = ByteUtil.getOneByte(frameHeaderBuffer, 4); + int streamId = ByteUtil.get31Bits(frameHeaderBuffer, 5); + + try { + validateFrame(expected, frameType, streamId, flags, payloadSize); + } catch (StreamException se) { + swallowPayload(streamId, frameTypeId, payloadSize, false, null); + throw se; + } + + switch (frameType) { + case DATA: + readDataFrame(streamId, flags, payloadSize, null); + break; + case HEADERS: + readHeadersFrame(streamId, flags, payloadSize, null); + break; + case PRIORITY: + readPriorityFrame(streamId, null); + break; + case RST: + readRstFrame(streamId, null); + break; + case SETTINGS: + readSettingsFrame(flags, payloadSize, null); + break; + case PUSH_PROMISE: + readPushPromiseFrame(streamId, flags, payloadSize, null); + break; + case PING: + readPingFrame(flags, null); + break; + case GOAWAY: + readGoawayFrame(payloadSize, null); + break; + case WINDOW_UPDATE: + readWindowUpdateFrame(streamId, null); + break; + case CONTINUATION: + readContinuationFrame(streamId, flags, payloadSize, null); + break; + case PRIORITY_UPDATE: + readPriorityUpdateFrame(payloadSize, null); + break; + case UNKNOWN: + readUnknownFrame(streamId, frameTypeId, flags, payloadSize, null); + } + + return true; + } + + protected void readDataFrame(int streamId, int flags, int payloadSize, ByteBuffer buffer) + throws Http2Exception, IOException { + // Process the Stream + int padLength = 0; + + boolean endOfStream = Flags.isEndOfStream(flags); + + int dataLength; + if (Flags.hasPadding(flags)) { + if (buffer == null) { + byte[] b = new byte[1]; + input.fill(true, b); + padLength = b[0] & 0xFF; + } else { + padLength = buffer.get() & 0xFF; + } + + if (padLength >= payloadSize) { + throw new ConnectionException( + sm.getString("http2Parser.processFrame.tooMuchPadding", connectionId, + Integer.toString(streamId), Integer.toString(padLength), Integer.toString(payloadSize)), + Http2Error.PROTOCOL_ERROR); + } + // +1 is for the padding length byte we just read above + dataLength = payloadSize - (padLength + 1); + } else { + dataLength = payloadSize; + } + + if (log.isTraceEnabled()) { + String padding; + if (Flags.hasPadding(flags)) { + padding = Integer.toString(padLength); + } else { + padding = "none"; + } + log.trace(sm.getString("http2Parser.processFrameData.lengths", connectionId, Integer.toString(streamId), + Integer.toString(dataLength), padding)); + } + + ByteBuffer dest = output.startRequestBodyFrame(streamId, payloadSize, endOfStream); + if (dest == null) { + swallowPayload(streamId, FrameType.DATA.getId(), dataLength, false, buffer); + // Process padding before sending any notifications in case padding + // is invalid. + if (Flags.hasPadding(flags)) { + swallowPayload(streamId, FrameType.DATA.getId(), padLength, true, buffer); + } + if (endOfStream) { + output.receivedEndOfStream(streamId); + } + } else { + synchronized (dest) { + if (dest.remaining() < payloadSize) { + // Client has sent more data than permitted by Window size + swallowPayload(streamId, FrameType.DATA.getId(), dataLength, false, buffer); + if (Flags.hasPadding(flags)) { + swallowPayload(streamId, FrameType.DATA.getId(), padLength, true, buffer); + } + throw new StreamException(sm.getString("http2Parser.processFrameData.window", connectionId), + Http2Error.FLOW_CONTROL_ERROR, streamId); + } + if (buffer == null) { + input.fill(true, dest, dataLength); + } else { + int oldLimit = buffer.limit(); + buffer.limit(buffer.position() + dataLength); + dest.put(buffer); + buffer.limit(oldLimit); + } + // Process padding before sending any notifications in case + // padding is invalid. + if (Flags.hasPadding(flags)) { + swallowPayload(streamId, FrameType.DATA.getId(), padLength, true, buffer); + } + if (endOfStream) { + output.receivedEndOfStream(streamId); + } + output.endRequestBodyFrame(streamId, dataLength); + } + } + } + + + protected void readHeadersFrame(int streamId, int flags, int payloadSize, ByteBuffer buffer) + throws Http2Exception, IOException { + + headersEndStream = Flags.isEndOfStream(flags); + + if (hpackDecoder == null) { + hpackDecoder = output.getHpackDecoder(); + } + try { + hpackDecoder.setHeaderEmitter(output.headersStart(streamId, headersEndStream)); + } catch (StreamException se) { + swallowPayload(streamId, FrameType.HEADERS.getId(), payloadSize, false, buffer); + throw se; + } + + int padLength = 0; + boolean padding = Flags.hasPadding(flags); + boolean priority = Flags.hasPriority(flags); + int optionalLen = 0; + if (padding) { + optionalLen = 1; + } + if (priority) { + optionalLen += 5; + } + if (optionalLen > 0) { + byte[] optional = new byte[optionalLen]; + if (buffer == null) { + input.fill(true, optional); + } else { + buffer.get(optional); + } + if (padding) { + padLength = ByteUtil.getOneByte(optional, 0); + if (padLength >= payloadSize) { + throw new ConnectionException(sm.getString("http2Parser.processFrame.tooMuchPadding", connectionId, + Integer.toString(streamId), Integer.toString(padLength), Integer.toString(payloadSize)), + Http2Error.PROTOCOL_ERROR); + } + } + + // Ignore RFC 7450 priority data if present + + payloadSize -= optionalLen; + payloadSize -= padLength; + } + + readHeaderPayload(streamId, payloadSize, buffer); + + swallowPayload(streamId, FrameType.HEADERS.getId(), padLength, true, buffer); + + // Validate the headers so far + hpackDecoder.getHeaderEmitter().validateHeaders(); + + if (Flags.isEndOfHeaders(flags)) { + onHeadersComplete(streamId); + } else { + headersCurrentStream = streamId; + } + } + + + protected void readPriorityFrame(int streamId, ByteBuffer buffer) throws IOException { + // RFC 7450 priority frames are ignored. Still need to treat as overhead. + try { + swallowPayload(streamId, FrameType.PRIORITY.getId(), 5, false, buffer); + } catch (ConnectionException e) { + // Will never happen because swallowPayload() is called with isPadding set + // to false + } + output.increaseOverheadCount(FrameType.PRIORITY); + } + + + protected void readRstFrame(int streamId, ByteBuffer buffer) throws Http2Exception, IOException { + byte[] payload = new byte[4]; + if (buffer == null) { + input.fill(true, payload); + } else { + buffer.get(payload); + } + + long errorCode = ByteUtil.getFourBytes(payload, 0); + output.reset(streamId, errorCode); + headersCurrentStream = -1; + headersEndStream = false; + } + + + protected void readSettingsFrame(int flags, int payloadSize, ByteBuffer buffer) throws Http2Exception, IOException { + boolean ack = Flags.isAck(flags); + if (payloadSize > 0 && ack) { + throw new ConnectionException(sm.getString("http2Parser.processFrameSettings.ackWithNonZeroPayload"), + Http2Error.FRAME_SIZE_ERROR); + } + + if (payloadSize == 0 && !ack) { + // Ensure empty SETTINGS frame increments the overhead count + output.setting(null, 0); + } else { + // Process the settings + byte[] setting = new byte[6]; + for (int i = 0; i < payloadSize / 6; i++) { + if (buffer == null) { + input.fill(true, setting); + } else { + buffer.get(setting); + } + int id = ByteUtil.getTwoBytes(setting, 0); + long value = ByteUtil.getFourBytes(setting, 2); + Setting key = Setting.valueOf(id); + if (key == Setting.UNKNOWN) { + log.warn(sm.getString("connectionSettings.unknown", connectionId, Integer.toString(id), + Long.toString(value))); + } + output.setting(key, value); + } + } + output.settingsEnd(ack); + } + + + /** + * This default server side implementation always throws an exception. If re-used for client side parsing, this + * method should be overridden with an appropriate implementation. + * + * @param streamId The pushed stream + * @param flags The flags set in the frame header + * @param payloadSize The size of the payload in bytes + * @param buffer The payload, if available + * + * @throws Http2Exception Always + * @throws IOException May be thrown by sub-classes that parse this frame + */ + protected void readPushPromiseFrame(int streamId, int flags, int payloadSize, ByteBuffer buffer) + throws Http2Exception, IOException { + throw new ConnectionException( + sm.getString("http2Parser.processFramePushPromise", connectionId, Integer.valueOf(streamId)), + Http2Error.PROTOCOL_ERROR); + } + + + protected void readPingFrame(int flags, ByteBuffer buffer) throws IOException { + // Read the payload + byte[] payload = new byte[8]; + if (buffer == null) { + input.fill(true, payload); + } else { + buffer.get(payload); + } + output.pingReceive(payload, Flags.isAck(flags)); + } + + + protected void readGoawayFrame(int payloadSize, ByteBuffer buffer) throws IOException { + byte[] payload = new byte[payloadSize]; + if (buffer == null) { + input.fill(true, payload); + } else { + buffer.get(payload); + } + + int lastStreamId = ByteUtil.get31Bits(payload, 0); + long errorCode = ByteUtil.getFourBytes(payload, 4); + String debugData = null; + if (payloadSize > 8) { + debugData = new String(payload, 8, payloadSize - 8, StandardCharsets.UTF_8); + } + output.goaway(lastStreamId, errorCode, debugData); + } + + + protected void readWindowUpdateFrame(int streamId, ByteBuffer buffer) throws Http2Exception, IOException { + byte[] payload = new byte[4]; + if (buffer == null) { + input.fill(true, payload); + } else { + buffer.get(payload); + } + int windowSizeIncrement = ByteUtil.get31Bits(payload, 0); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("http2Parser.processFrameWindowUpdate.debug", connectionId, + Integer.toString(streamId), Integer.toString(windowSizeIncrement))); + } + + // Validate the data + if (windowSizeIncrement == 0) { + if (streamId == 0) { + throw new ConnectionException(sm.getString("http2Parser.processFrameWindowUpdate.invalidIncrement", + connectionId, Integer.toString(streamId)), Http2Error.PROTOCOL_ERROR); + } else { + throw new StreamException(sm.getString("http2Parser.processFrameWindowUpdate.invalidIncrement", + connectionId, Integer.toString(streamId)), Http2Error.PROTOCOL_ERROR, streamId); + } + } + + output.incrementWindowSize(streamId, windowSizeIncrement); + } + + + protected void readContinuationFrame(int streamId, int flags, int payloadSize, ByteBuffer buffer) + throws Http2Exception, IOException { + if (headersCurrentStream == -1) { + // No headers to continue + throw new ConnectionException(sm.getString("http2Parser.processFrameContinuation.notExpected", connectionId, + Integer.toString(streamId)), Http2Error.PROTOCOL_ERROR); + } + + boolean endOfHeaders = Flags.isEndOfHeaders(flags); + + // Used to detect abusive clients sending large numbers of small + // continuation frames + output.headersContinue(payloadSize, endOfHeaders); + + readHeaderPayload(streamId, payloadSize, buffer); + + // Validate the headers so far + hpackDecoder.getHeaderEmitter().validateHeaders(); + + if (endOfHeaders) { + headersCurrentStream = -1; + onHeadersComplete(streamId); + } + } + + + protected void readPriorityUpdateFrame(int payloadSize, ByteBuffer buffer) throws Http2Exception, IOException { + // Identify prioritized stream ID + byte[] payload = new byte[payloadSize]; + if (buffer == null) { + input.fill(true, payload); + } else { + buffer.get(payload); + } + + int prioritizedStreamID = ByteUtil.get31Bits(payload, 0); + + if (prioritizedStreamID == 0) { + throw new ConnectionException(sm.getString("http2Parser.processFramePriorityUpdate.streamZero"), + Http2Error.PROTOCOL_ERROR); + } + + ByteArrayInputStream bais = new ByteArrayInputStream(payload, 4, payloadSize - 4); + Reader r = new BufferedReader(new InputStreamReader(bais, StandardCharsets.US_ASCII)); + Priority p = Priority.parsePriority(r); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("http2Parser.processFramePriorityUpdate.debug", connectionId, + Integer.toString(prioritizedStreamID), Integer.toString(p.getUrgency()), + Boolean.valueOf(p.getIncremental()))); + } + + output.priorityUpdate(prioritizedStreamID, p); + } + + + protected void readHeaderPayload(int streamId, int payloadSize, ByteBuffer buffer) + throws Http2Exception, IOException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("http2Parser.processFrameHeaders.payload", connectionId, Integer.valueOf(streamId), + Integer.valueOf(payloadSize))); + } + + int remaining = payloadSize; + + while (remaining > 0) { + if (headerReadBuffer.remaining() == 0) { + // Buffer needs expansion + int newSize; + if (headerReadBuffer.capacity() < payloadSize) { + // First step, expand to the current payload. That should + // cover most cases. + newSize = payloadSize; + } else { + // Header must be spread over multiple frames. Keep doubling + // buffer size until the header can be read. + newSize = headerReadBuffer.capacity() * 2; + } + headerReadBuffer = ByteBufferUtils.expand(headerReadBuffer, newSize); + } + int toRead = Math.min(headerReadBuffer.remaining(), remaining); + // headerReadBuffer in write mode + if (buffer == null) { + input.fill(true, headerReadBuffer, toRead); + } else { + int oldLimit = buffer.limit(); + buffer.limit(buffer.position() + toRead); + headerReadBuffer.put(buffer); + buffer.limit(oldLimit); + } + // switch to read mode + headerReadBuffer.flip(); + try { + hpackDecoder.decode(headerReadBuffer); + } catch (HpackException hpe) { + throw new ConnectionException(sm.getString("http2Parser.processFrameHeaders.decodingFailed"), + Http2Error.COMPRESSION_ERROR, hpe); + } + + // switches to write mode + headerReadBuffer.compact(); + remaining -= toRead; + + if (hpackDecoder.isHeaderCountExceeded()) { + StreamException headerException = new StreamException( + sm.getString("http2Parser.headerLimitCount", connectionId, Integer.valueOf(streamId)), + Http2Error.ENHANCE_YOUR_CALM, streamId); + hpackDecoder.getHeaderEmitter().setHeaderException(headerException); + } + + if (hpackDecoder.isHeaderSizeExceeded(headerReadBuffer.position())) { + StreamException headerException = new StreamException( + sm.getString("http2Parser.headerLimitSize", connectionId, Integer.valueOf(streamId)), + Http2Error.ENHANCE_YOUR_CALM, streamId); + hpackDecoder.getHeaderEmitter().setHeaderException(headerException); + } + + if (hpackDecoder.isHeaderSwallowSizeExceeded(headerReadBuffer.position())) { + throw new ConnectionException( + sm.getString("http2Parser.headerLimitSize", connectionId, Integer.valueOf(streamId)), + Http2Error.ENHANCE_YOUR_CALM); + } + } + } + + + protected void readUnknownFrame(int streamId, int frameTypeId, int flags, int payloadSize, ByteBuffer buffer) + throws IOException { + try { + swallowPayload(streamId, frameTypeId, payloadSize, false, buffer); + } catch (ConnectionException e) { + // Will never happen because swallowPayload() is called with isPadding set + // to false + } finally { + output.onSwallowedUnknownFrame(streamId, frameTypeId, flags, payloadSize); + } + } + + + /** + * Swallow some or all of the bytes from the payload of an HTTP/2 frame. + * + * @param streamId Stream being swallowed + * @param frameTypeId Type of HTTP/2 frame for which the bytes will be swallowed + * @param len Number of bytes to swallow + * @param isPadding Are the bytes to be swallowed padding bytes? + * @param byteBuffer Used with {@link Http2AsyncParser} to access the data that has already been read + * + * @throws IOException If an I/O error occurs reading additional bytes into the input buffer. + * @throws ConnectionException If the swallowed bytes are expected to have a value of zero but do not + */ + protected void swallowPayload(int streamId, int frameTypeId, int len, boolean isPadding, ByteBuffer byteBuffer) + throws IOException, ConnectionException { + if (log.isTraceEnabled()) { + log.trace(sm.getString("http2Parser.swallow.debug", connectionId, Integer.toString(streamId), + Integer.toString(len))); + } + try { + if (len == 0) { + return; + } + if (!isPadding && byteBuffer != null) { + byteBuffer.position(byteBuffer.position() + len); + } else { + int read = 0; + byte[] buffer = new byte[1024]; + while (read < len) { + int thisTime = Math.min(buffer.length, len - read); + if (byteBuffer == null) { + input.fill(true, buffer, 0, thisTime); + } else { + byteBuffer.get(buffer, 0, thisTime); + } + if (isPadding) { + // Validate the padding is zero since receiving non-zero padding + // is a strong indication of either a faulty client or a server + // side bug. + for (int i = 0; i < thisTime; i++) { + if (buffer[i] != 0) { + throw new ConnectionException(sm.getString("http2Parser.nonZeroPadding", connectionId, + Integer.toString(streamId)), Http2Error.PROTOCOL_ERROR); + } + } + } + read += thisTime; + } + } + } finally { + if (FrameType.DATA.getIdByte() == frameTypeId) { + if (isPadding) { + // Need to add 1 for the padding length bytes that was also + // part of the payload. + len += 1; + } + if (len > 0) { + output.onSwallowedDataFramePayload(streamId, len); + } + } + } + } + + + protected void onHeadersComplete(int streamId) throws Http2Exception { + // Any left over data is a compression error + if (headerReadBuffer.position() > 0) { + throw new ConnectionException(sm.getString("http2Parser.processFrameHeaders.decodingDataLeft"), + Http2Error.COMPRESSION_ERROR); + } + + synchronized (output) { + output.headersEnd(streamId, headersEndStream); + + if (headersEndStream) { + headersEndStream = false; + } + } + + // Reset size for new request if the buffer was previously expanded + if (headerReadBuffer.capacity() > Constants.DEFAULT_HEADER_READ_BUFFER_SIZE) { + headerReadBuffer = ByteBuffer.allocate(Constants.DEFAULT_HEADER_READ_BUFFER_SIZE); + } + } + + + /* + * Implementation note: Validation applicable to all incoming frames should be implemented here. Frame type specific + * validation should be performed in the appropriate readXxxFrame() method. For validation applicable to some but + * not all frame types, use your judgement. + */ + protected void validateFrame(FrameType expected, FrameType frameType, int streamId, int flags, int payloadSize) + throws Http2Exception { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("http2Parser.processFrame", connectionId, Integer.toString(streamId), frameType, + Integer.toString(flags), Integer.toString(payloadSize))); + } + + if (expected != null && frameType != expected) { + throw new StreamException(sm.getString("http2Parser.processFrame.unexpectedType", expected, frameType), + Http2Error.PROTOCOL_ERROR, streamId); + } + + int maxFrameSize = input.getMaxFrameSize(); + if (payloadSize > maxFrameSize) { + throw new ConnectionException(sm.getString("http2Parser.payloadTooBig", Integer.toString(payloadSize), + Integer.toString(maxFrameSize)), Http2Error.FRAME_SIZE_ERROR); + } + + if (headersCurrentStream != -1) { + if (headersCurrentStream != streamId) { + throw new ConnectionException( + sm.getString("http2Parser.headers.wrongStream", connectionId, + Integer.toString(headersCurrentStream), Integer.toString(streamId)), + Http2Error.COMPRESSION_ERROR); + } + if (frameType == FrameType.RST) { + // NO-OP: RST is OK here + } else if (frameType != FrameType.CONTINUATION) { + throw new ConnectionException(sm.getString("http2Parser.headers.wrongFrameType", connectionId, + Integer.toString(headersCurrentStream), frameType), Http2Error.COMPRESSION_ERROR); + } + } + + frameType.check(streamId, payloadSize); + } + + + /** + * Read and validate the connection preface from input using blocking IO. + * + * @param webConnection The connection + * @param stream The current stream + */ + void readConnectionPreface(WebConnection webConnection, Stream stream) throws Http2Exception { + byte[] data = new byte[CLIENT_PREFACE_START.length]; + try { + input.fill(true, data); + + for (int i = 0; i < CLIENT_PREFACE_START.length; i++) { + if (CLIENT_PREFACE_START[i] != data[i]) { + throw new ProtocolException(sm.getString("http2Parser.preface.invalid")); + } + } + + // Must always be followed by a settings frame + readFrame(true, FrameType.SETTINGS); + } catch (IOException ioe) { + throw new ProtocolException(sm.getString("http2Parser.preface.io"), ioe); + } + } + + + /** + * Interface that must be implemented by the source of data for the parser. + */ + interface Input { + + /** + * Fill the given array with data unless non-blocking is requested and no data is available. If any data is + * available then the buffer will be filled using blocking I/O. + * + * @param block Should the first read into the provided buffer be a blocking read or not. + * @param data Buffer to fill + * @param offset Position in buffer to start writing + * @param length Number of bytes to read + * + * @return true if the buffer was filled otherwise false + * + * @throws IOException If an I/O occurred while obtaining data with which to fill the buffer + */ + boolean fill(boolean block, byte[] data, int offset, int length) throws IOException; + + default boolean fill(boolean block, byte[] data) throws IOException { + return fill(block, data, 0, data.length); + } + + default boolean fill(boolean block, ByteBuffer data, int len) throws IOException { + boolean result = fill(block, data.array(), data.arrayOffset() + data.position(), len); + if (result) { + data.position(data.position() + len); + } + return result; + } + + int getMaxFrameSize(); + } + + + /** + * Interface that must be implemented to receive notifications from the parser as it processes incoming frames. + */ + interface Output { + + HpackDecoder getHpackDecoder(); + + // Data frames + ByteBuffer startRequestBodyFrame(int streamId, int payloadSize, boolean endOfStream) throws Http2Exception; + + void endRequestBodyFrame(int streamId, int dataLength) throws Http2Exception, IOException; + + void receivedEndOfStream(int streamId) throws ConnectionException; + + /** + * Notification triggered when the parser swallows some or all of a DATA frame payload without writing it to the + * ByteBuffer returned by {@link #startRequestBodyFrame(int, int, boolean)}. + * + * @param streamId The stream on which the payload that has been swallowed was received + * @param swallowedDataBytesCount The number of bytes that the parser swallowed. + * + * @throws ConnectionException If an error fatal to the HTTP/2 connection occurs while swallowing the payload + * @throws IOException If an I/O occurred while swallowing the payload + */ + void onSwallowedDataFramePayload(int streamId, int swallowedDataBytesCount) + throws ConnectionException, IOException; + + // Header frames + HeaderEmitter headersStart(int streamId, boolean headersEndStream) throws Http2Exception, IOException; + + void headersContinue(int payloadSize, boolean endOfHeaders); + + void headersEnd(int streamId, boolean endOfStream) throws Http2Exception; + + // Reset frames + void reset(int streamId, long errorCode) throws Http2Exception; + + // Settings frames + void setting(Setting setting, long value) throws ConnectionException; + + void settingsEnd(boolean ack) throws IOException; + + // Ping frames + void pingReceive(byte[] payload, boolean ack) throws IOException; + + // Goaway + void goaway(int lastStreamId, long errorCode, String debugData); + + // Window size + void incrementWindowSize(int streamId, int increment) throws Http2Exception; + + // Priority update + void priorityUpdate(int prioritizedStreamID, Priority p) throws Http2Exception; + + /** + * Notification triggered when the parser swallows the payload of an unknown frame. + * + * @param streamId The stream on which the swallowed frame was received + * @param frameTypeId The (unrecognised) type of swallowed frame + * @param flags The flags set in the header of the swallowed frame + * @param size The payload size of the swallowed frame + * + * @throws IOException If an I/O occurred while swallowing the unknown frame + */ + void onSwallowedUnknownFrame(int streamId, int frameTypeId, int flags, int size) throws IOException; + + void increaseOverheadCount(FrameType frameType); + } +} diff --git a/java/org/apache/coyote/http2/Http2Protocol.java b/java/org/apache/coyote/http2/Http2Protocol.java new file mode 100644 index 0000000..8287af0 --- /dev/null +++ b/java/org/apache/coyote/http2/Http2Protocol.java @@ -0,0 +1,393 @@ +/* + * 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.coyote.http2; + +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; + +import javax.management.ObjectName; + +import org.apache.coyote.Adapter; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.coyote.Processor; +import org.apache.coyote.Request; +import org.apache.coyote.RequestGroupInfo; +import org.apache.coyote.Response; +import org.apache.coyote.UpgradeProtocol; +import org.apache.coyote.UpgradeToken; +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; +import org.apache.coyote.http11.upgrade.UpgradeProcessorInternal; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +public class Http2Protocol implements UpgradeProtocol { + + private static final Log log = LogFactory.getLog(Http2Protocol.class); + private static final StringManager sm = StringManager.getManager(Http2Protocol.class); + + static final long DEFAULT_READ_TIMEOUT = 5000; + static final long DEFAULT_WRITE_TIMEOUT = 5000; + static final long DEFAULT_KEEP_ALIVE_TIMEOUT = 20000; + static final long DEFAULT_STREAM_READ_TIMEOUT = 20000; + static final long DEFAULT_STREAM_WRITE_TIMEOUT = 20000; + // The HTTP/2 specification recommends a minimum default of 100 + static final long DEFAULT_MAX_CONCURRENT_STREAMS = 100; + // Maximum amount of streams which can be concurrently executed over + // a single connection + static final int DEFAULT_MAX_CONCURRENT_STREAM_EXECUTION = 20; + // Default factor used when adjusting overhead count for overhead frames + static final int DEFAULT_OVERHEAD_COUNT_FACTOR = 10; + // Default factor used when adjusting overhead count for reset frames + static final int DEFAULT_OVERHEAD_RESET_FACTOR = 50; + // Not currently configurable. This makes the practical limit for + // overheadCountFactor to be ~20. The exact limit will vary with traffic + // patterns. + static final int DEFAULT_OVERHEAD_REDUCTION_FACTOR = -20; + static final int DEFAULT_OVERHEAD_CONTINUATION_THRESHOLD = 1024; + static final int DEFAULT_OVERHEAD_DATA_THRESHOLD = 1024; + static final int DEFAULT_OVERHEAD_WINDOW_UPDATE_THRESHOLD = 1024; + + private static final String HTTP_UPGRADE_NAME = "h2c"; + private static final String ALPN_NAME = "h2"; + private static final byte[] ALPN_IDENTIFIER = ALPN_NAME.getBytes(StandardCharsets.UTF_8); + + // All timeouts in milliseconds + // These are the socket level timeouts + private long readTimeout = DEFAULT_READ_TIMEOUT; + private long writeTimeout = DEFAULT_WRITE_TIMEOUT; + private long keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; + // These are the stream level timeouts + private long streamReadTimeout = DEFAULT_STREAM_READ_TIMEOUT; + private long streamWriteTimeout = DEFAULT_STREAM_WRITE_TIMEOUT; + + private long maxConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS; + private int maxConcurrentStreamExecution = DEFAULT_MAX_CONCURRENT_STREAM_EXECUTION; + // To advertise a different default to the client specify it here but DO NOT + // change the default defined in ConnectionSettingsBase. + private int initialWindowSize = ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE; + // Limits + private int maxHeaderCount = Constants.DEFAULT_MAX_HEADER_COUNT; + private int maxTrailerCount = Constants.DEFAULT_MAX_TRAILER_COUNT; + private int overheadCountFactor = DEFAULT_OVERHEAD_COUNT_FACTOR; + private int overheadResetFactor = DEFAULT_OVERHEAD_RESET_FACTOR; + private int overheadContinuationThreshold = DEFAULT_OVERHEAD_CONTINUATION_THRESHOLD; + private int overheadDataThreshold = DEFAULT_OVERHEAD_DATA_THRESHOLD; + private int overheadWindowUpdateThreshold = DEFAULT_OVERHEAD_WINDOW_UPDATE_THRESHOLD; + + private boolean initiatePingDisabled = false; + private boolean useSendfile = true; + // Reference to HTTP/1.1 protocol that this instance is configured under + private AbstractHttp11Protocol http11Protocol = null; + + private RequestGroupInfo global = new RequestGroupInfo(); + + @Override + public String getHttpUpgradeName(boolean isSSLEnabled) { + if (isSSLEnabled) { + return null; + } else { + return HTTP_UPGRADE_NAME; + } + } + + @Override + public byte[] getAlpnIdentifier() { + return ALPN_IDENTIFIER; + } + + @Override + public String getAlpnName() { + return ALPN_NAME; + } + + @Override + public Processor getProcessor(SocketWrapperBase socketWrapper, Adapter adapter) { + String upgradeProtocol = getUpgradeProtocolName(); + UpgradeProcessorInternal processor = new UpgradeProcessorInternal(socketWrapper, + new UpgradeToken(getInternalUpgradeHandler(socketWrapper, adapter, null), null, null, upgradeProtocol), + null); + return processor; + } + + + @Override + public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase socketWrapper, Adapter adapter, + Request coyoteRequest) { + return socketWrapper.hasAsyncIO() ? new Http2AsyncUpgradeHandler(this, adapter, coyoteRequest, socketWrapper) : + new Http2UpgradeHandler(this, adapter, coyoteRequest, socketWrapper); + } + + + @Override + public boolean accept(Request request) { + // Should only be one HTTP2-Settings header + Enumeration settings = request.getMimeHeaders().values("HTTP2-Settings"); + int count = 0; + while (settings.hasMoreElements()) { + count++; + settings.nextElement(); + } + if (count != 1) { + return false; + } + + Enumeration connection = request.getMimeHeaders().values("Connection"); + boolean found = false; + while (connection.hasMoreElements() && !found) { + found = connection.nextElement().contains("HTTP2-Settings"); + } + return found; + } + + + public long getReadTimeout() { + return readTimeout; + } + + + public void setReadTimeout(long readTimeout) { + this.readTimeout = readTimeout; + } + + + public long getWriteTimeout() { + return writeTimeout; + } + + + public void setWriteTimeout(long writeTimeout) { + this.writeTimeout = writeTimeout; + } + + + public long getKeepAliveTimeout() { + return keepAliveTimeout; + } + + + public void setKeepAliveTimeout(long keepAliveTimeout) { + this.keepAliveTimeout = keepAliveTimeout; + } + + + public long getStreamReadTimeout() { + return streamReadTimeout; + } + + + public void setStreamReadTimeout(long streamReadTimeout) { + this.streamReadTimeout = streamReadTimeout; + } + + + public long getStreamWriteTimeout() { + return streamWriteTimeout; + } + + + public void setStreamWriteTimeout(long streamWriteTimeout) { + this.streamWriteTimeout = streamWriteTimeout; + } + + + public long getMaxConcurrentStreams() { + return maxConcurrentStreams; + } + + + public void setMaxConcurrentStreams(long maxConcurrentStreams) { + this.maxConcurrentStreams = maxConcurrentStreams; + } + + + public int getMaxConcurrentStreamExecution() { + return maxConcurrentStreamExecution; + } + + + public void setMaxConcurrentStreamExecution(int maxConcurrentStreamExecution) { + this.maxConcurrentStreamExecution = maxConcurrentStreamExecution; + } + + + public int getInitialWindowSize() { + return initialWindowSize; + } + + + public void setInitialWindowSize(int initialWindowSize) { + this.initialWindowSize = initialWindowSize; + } + + + public boolean getUseSendfile() { + return useSendfile; + } + + + public void setUseSendfile(boolean useSendfile) { + this.useSendfile = useSendfile; + } + + + boolean isTrailerHeaderAllowed(String headerName) { + return http11Protocol.isTrailerHeaderAllowed(headerName); + } + + + public void setMaxHeaderCount(int maxHeaderCount) { + this.maxHeaderCount = maxHeaderCount; + } + + + public int getMaxHeaderCount() { + return maxHeaderCount; + } + + + public int getMaxHeaderSize() { + return http11Protocol.getMaxHttpRequestHeaderSize(); + } + + + public void setMaxTrailerCount(int maxTrailerCount) { + this.maxTrailerCount = maxTrailerCount; + } + + + public int getMaxTrailerCount() { + return maxTrailerCount; + } + + + public int getMaxTrailerSize() { + return http11Protocol.getMaxTrailerSize(); + } + + + public int getOverheadCountFactor() { + return overheadCountFactor; + } + + + public void setOverheadCountFactor(int overheadCountFactor) { + this.overheadCountFactor = overheadCountFactor; + } + + + public int getOverheadResetFactor() { + return overheadResetFactor; + } + + + public void setOverheadResetFactor(int overheadResetFactor) { + if (overheadResetFactor < 0) { + this.overheadResetFactor = 0; + } else { + this.overheadResetFactor = overheadResetFactor; + } + } + + + public int getOverheadContinuationThreshold() { + return overheadContinuationThreshold; + } + + + public void setOverheadContinuationThreshold(int overheadContinuationThreshold) { + this.overheadContinuationThreshold = overheadContinuationThreshold; + } + + + public int getOverheadDataThreshold() { + return overheadDataThreshold; + } + + + public void setOverheadDataThreshold(int overheadDataThreshold) { + this.overheadDataThreshold = overheadDataThreshold; + } + + + public int getOverheadWindowUpdateThreshold() { + return overheadWindowUpdateThreshold; + } + + + public void setOverheadWindowUpdateThreshold(int overheadWindowUpdateThreshold) { + this.overheadWindowUpdateThreshold = overheadWindowUpdateThreshold; + } + + + public void setInitiatePingDisabled(boolean initiatePingDisabled) { + this.initiatePingDisabled = initiatePingDisabled; + } + + + public boolean getInitiatePingDisabled() { + return initiatePingDisabled; + } + + + public boolean useCompression(Request request, Response response) { + return http11Protocol.useCompression(request, response); + } + + + public ContinueResponseTiming getContinueResponseTimingInternal() { + return http11Protocol.getContinueResponseTimingInternal(); + } + + + public AbstractHttp11Protocol getHttp11Protocol() { + return this.http11Protocol; + } + + + @Override + public void setHttp11Protocol(AbstractHttp11Protocol http11Protocol) { + this.http11Protocol = http11Protocol; + + try { + ObjectName oname = this.http11Protocol.getONameForUpgrade(getUpgradeProtocolName()); + // This can be null when running the testsuite + if (oname != null) { + Registry.getRegistry(null, null).registerComponent(global, oname, null); + } + } catch (Exception e) { + log.warn(sm.getString("http2Protocol.jmxRegistration.fail"), e); + } + } + + + public String getUpgradeProtocolName() { + if (http11Protocol.isSSLEnabled()) { + return ALPN_NAME; + } else { + return HTTP_UPGRADE_NAME; + } + } + + + public RequestGroupInfo getGlobal() { + return global; + } +} diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java b/java/org/apache/coyote/http2/Http2UpgradeHandler.java new file mode 100644 index 0000000..eac7b21 --- /dev/null +++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java @@ -0,0 +1,2096 @@ +/* + * 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.coyote.http2; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; + +import jakarta.servlet.ServletConnection; +import jakarta.servlet.http.WebConnection; + +import org.apache.coyote.Adapter; +import org.apache.coyote.ProtocolException; +import org.apache.coyote.Request; +import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; +import org.apache.coyote.http2.HpackDecoder.HeaderEmitter; +import org.apache.coyote.http2.HpackEncoder.State; +import org.apache.coyote.http2.Http2Parser.Input; +import org.apache.coyote.http2.Http2Parser.Output; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.Priority; +import org.apache.tomcat.util.log.UserDataHelper; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SendfileState; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +/** + * This represents an HTTP/2 connection from a client to Tomcat. It is designed on the basis that there will never be + * more than one thread performing I/O at a time.
    + * For reading, this implementation is blocking within frames and non-blocking between frames.
    + * Note: + *

      + *
    • You will need to nest an <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" /> element + * inside a TLS enabled Connector element in server.xml to enable HTTP/2 support.
    • + *
    + */ +class Http2UpgradeHandler extends AbstractStream implements InternalHttpUpgradeHandler, Input, Output { + + protected static final Log log = LogFactory.getLog(Http2UpgradeHandler.class); + protected static final StringManager sm = StringManager.getManager(Http2UpgradeHandler.class); + + private static final Integer STREAM_ID_ZERO = Integer.valueOf(0); + + protected static final int FLAG_END_OF_STREAM = 1; + protected static final int FLAG_END_OF_HEADERS = 4; + + protected static final byte[] PING = { 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00 }; + protected static final byte[] PING_ACK = { 0x00, 0x00, 0x08, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00 }; + + protected static final byte[] SETTINGS_ACK = { 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00 }; + + protected static final byte[] GOAWAY = { 0x07, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + private static final String HTTP2_SETTINGS_HEADER = "HTTP2-Settings"; + + protected static final HeaderSink HEADER_SINK = new HeaderSink(); + + protected final String connectionId; + + protected final Http2Protocol protocol; + private final Adapter adapter; + protected final SocketWrapperBase socketWrapper; + private volatile SSLSupport sslSupport; + + private volatile Http2Parser parser; + + // Simple state machine (sequence of states) + private AtomicReference connectionState = new AtomicReference<>(ConnectionState.NEW); + private volatile long pausedNanoTime = Long.MAX_VALUE; + + /** + * Remote settings are settings defined by the client and sent to Tomcat that Tomcat must use when communicating + * with the client. + */ + private final ConnectionSettingsRemote remoteSettings; + /** + * Local settings are settings defined by Tomcat and sent to the client that the client must use when communicating + * with Tomcat. + */ + protected final ConnectionSettingsLocal localSettings; + + private HpackDecoder hpackDecoder; + private HpackEncoder hpackEncoder; + + private final ConcurrentNavigableMap streams = new ConcurrentSkipListMap<>(); + protected final AtomicInteger activeRemoteStreamCount = new AtomicInteger(0); + // Start at -1 so the 'add 2' logic in closeIdleStreams() works + private volatile int maxActiveRemoteStreamId = -1; + private volatile int maxProcessedStreamId; + private final AtomicInteger nextLocalStreamId = new AtomicInteger(2); + private final PingManager pingManager = getPingManager(); + private volatile int newStreamsSinceLastPrune = 0; + private final Set backLogStreams = new HashSet<>(); + private long backLogSize = 0; + // The time at which the connection will timeout unless data arrives before + // then. -1 means no timeout. + private volatile long connectionTimeout = -1; + + // Stream concurrency control + private AtomicInteger streamConcurrency = null; + private Queue queuedRunnable = null; + + // Track 'overhead' frames vs 'request/response' frames + private final AtomicLong overheadCount; + private volatile int lastNonFinalDataPayload; + private volatile int lastWindowUpdate; + + protected final UserDataHelper userDataHelper = new UserDataHelper(log); + + + Http2UpgradeHandler(Http2Protocol protocol, Adapter adapter, Request coyoteRequest, + SocketWrapperBase socketWrapper) { + super(STREAM_ID_ZERO); + this.protocol = protocol; + this.adapter = adapter; + this.socketWrapper = socketWrapper; + + // Defaults to -10 * the count factor. + // i.e. when the connection opens, 10 'overhead' frames in a row will + // cause the connection to be closed. + // Over time the count should be a slowly decreasing negative number. + // Therefore, the longer a connection is 'well-behaved', the greater + // tolerance it will have for a period of 'bad' behaviour. + overheadCount = new AtomicLong(-10 * protocol.getOverheadCountFactor()); + + lastNonFinalDataPayload = protocol.getOverheadDataThreshold() * 2; + lastWindowUpdate = protocol.getOverheadWindowUpdateThreshold() * 2; + + connectionId = getServletConnection().getConnectionId(); + + remoteSettings = new ConnectionSettingsRemote(connectionId); + localSettings = new ConnectionSettingsLocal(connectionId); + + localSettings.set(Setting.MAX_CONCURRENT_STREAMS, protocol.getMaxConcurrentStreams()); + localSettings.set(Setting.INITIAL_WINDOW_SIZE, protocol.getInitialWindowSize()); + + pingManager.initiateDisabled = protocol.getInitiatePingDisabled(); + + // Initial HTTP request becomes stream 1. + if (coyoteRequest != null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.upgrade", connectionId)); + } + Integer key = Integer.valueOf(1); + Stream stream = new Stream(key, this, coyoteRequest); + streams.put(key, stream); + maxActiveRemoteStreamId = 1; + activeRemoteStreamCount.set(1); + maxProcessedStreamId = 1; + } + } + + + protected PingManager getPingManager() { + return new PingManager(); + } + + + @Override + public void init(WebConnection webConnection) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.init", connectionId, connectionState.get())); + } + + if (!connectionState.compareAndSet(ConnectionState.NEW, ConnectionState.CONNECTED)) { + return; + } + + // Init concurrency control if needed + if (protocol.getMaxConcurrentStreamExecution() < localSettings.getMaxConcurrentStreams()) { + streamConcurrency = new AtomicInteger(0); + queuedRunnable = new ConcurrentLinkedQueue<>(); + } + + parser = getParser(connectionId); + + Stream stream = null; + + socketWrapper.setReadTimeout(protocol.getReadTimeout()); + socketWrapper.setWriteTimeout(protocol.getWriteTimeout()); + + if (webConnection != null) { + // HTTP/2 started via HTTP upgrade. + // The initial HTTP/1.1 request is available as Stream 1. + + try { + // Process the initial settings frame + stream = getStream(1, true); + String base64Settings = stream.getCoyoteRequest().getHeader(HTTP2_SETTINGS_HEADER); + byte[] settings = Base64.decodeBase64URLSafe(base64Settings); + + // Settings are only valid on stream 0 + FrameType.SETTINGS.check(0, settings.length); + + for (int i = 0; i < settings.length % 6; i++) { + int id = ByteUtil.getTwoBytes(settings, i * 6); + long value = ByteUtil.getFourBytes(settings, (i * 6) + 2); + Setting key = Setting.valueOf(id); + if (key == Setting.UNKNOWN) { + log.warn(sm.getString("connectionSettings.unknown", connectionId, Integer.toString(id), + Long.toString(value))); + } + remoteSettings.set(key, value); + } + } catch (Http2Exception e) { + throw new ProtocolException(sm.getString("upgradeHandler.upgrade.fail", connectionId)); + } + } + + // Send the initial settings frame + writeSettings(); + + // Make sure the client has sent a valid connection preface before we + // send the response to the original request over HTTP/2. + try { + parser.readConnectionPreface(webConnection, stream); + } catch (Http2Exception e) { + String msg = sm.getString("upgradeHandler.invalidPreface", connectionId); + if (log.isDebugEnabled()) { + log.debug(msg, e); + } + throw new ProtocolException(msg); + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.prefaceReceived", connectionId)); + } + + // Allow streams and connection to determine timeouts + socketWrapper.setReadTimeout(-1); + socketWrapper.setWriteTimeout(-1); + + processConnection(webConnection, stream); + } + + protected void processConnection(WebConnection webConnection, Stream stream) { + // Send a ping to get an idea of round trip time as early as possible + try { + pingManager.sendPing(true); + } catch (IOException ioe) { + throw new ProtocolException(sm.getString("upgradeHandler.pingFailed", connectionId), ioe); + } + + if (webConnection != null) { + processStreamOnContainerThread(stream); + } + } + + protected Http2Parser getParser(String connectionId) { + return new Http2Parser(connectionId, this, this); + } + + + protected void processStreamOnContainerThread(Stream stream) { + StreamProcessor streamProcessor = new StreamProcessor(this, stream, adapter, socketWrapper); + streamProcessor.setSslSupport(sslSupport); + processStreamOnContainerThread(streamProcessor, SocketEvent.OPEN_READ); + } + + + protected void decrementActiveRemoteStreamCount() { + setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet()); + } + + + void processStreamOnContainerThread(StreamProcessor streamProcessor, SocketEvent event) { + StreamRunnable streamRunnable = new StreamRunnable(streamProcessor, event); + if (streamConcurrency == null) { + socketWrapper.execute(streamRunnable); + } else { + if (getStreamConcurrency() < protocol.getMaxConcurrentStreamExecution()) { + increaseStreamConcurrency(); + socketWrapper.execute(streamRunnable); + } else { + queuedRunnable.offer(streamRunnable); + } + } + } + + + @Override + public void setSocketWrapper(SocketWrapperBase wrapper) { + // NO-OP. It is passed via the constructor + } + + + @Override + public void setSslSupport(SSLSupport sslSupport) { + this.sslSupport = sslSupport; + } + + + @Override + public SocketState upgradeDispatch(SocketEvent status) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.upgradeDispatch.entry", connectionId, status)); + } + + // WebConnection is not used so passing null here is fine + // Might not be necessary. init() will handle that. + init(null); + + SocketState result = SocketState.CLOSED; + + try { + switch (status) { + case OPEN_READ: + socketWrapper.getLock().lock(); + try { + if (socketWrapper.canWrite()) { + // Only send a ping if there is no other data waiting to be sent. + // Ping manager will ensure they aren't sent too frequently. + pingManager.sendPing(false); + } + } finally { + socketWrapper.getLock().unlock(); + } + try { + // Disable the connection timeout while frames are processed + setConnectionTimeout(-1); + while (true) { + try { + if (!parser.readFrame()) { + break; + } + } catch (StreamException se) { + // Log the Stream error but not necessarily all of + // them + UserDataHelper.Mode logMode = userDataHelper.getNextMode(); + if (logMode != null) { + String message = sm.getString("upgradeHandler.stream.error", connectionId, + Integer.toString(se.getStreamId())); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("upgradeHandler.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + log.info(message, se); + break; + case DEBUG: + log.debug(message, se); + } + } + // Stream errors are not fatal to the connection so + // continue reading frames + Stream stream = getStream(se.getStreamId(), false); + if (stream == null) { + sendStreamReset(null, se); + } else { + stream.close(se); + } + } finally { + if (isOverheadLimitExceeded()) { + throw new ConnectionException( + sm.getString("upgradeHandler.tooMuchOverhead", connectionId), + Http2Error.ENHANCE_YOUR_CALM); + } + } + } + + // Need to know the correct timeout before starting the read + // but that may not be known at this time if one or more + // requests are currently being processed so don't set a + // timeout for the socket... + socketWrapper.setReadTimeout(-1); + + // ...set a timeout on the connection + setConnectionTimeoutForStreamCount(activeRemoteStreamCount.get()); + + } catch (Http2Exception ce) { + // Really ConnectionException + if (log.isDebugEnabled()) { + log.debug(sm.getString("upgradeHandler.connectionError"), ce); + } + closeConnection(ce); + break; + } + + if (connectionState.get() != ConnectionState.CLOSED) { + if (socketWrapper.hasAsyncIO()) { + result = SocketState.ASYNC_IO; + } else { + result = SocketState.UPGRADED; + } + } + break; + + case OPEN_WRITE: + processWrites(); + if (socketWrapper.hasAsyncIO()) { + result = SocketState.ASYNC_IO; + } else { + result = SocketState.UPGRADED; + } + break; + + case TIMEOUT: + closeConnection(null); + break; + + case DISCONNECT: + case ERROR: + case STOP: + case CONNECT_FAIL: + close(); + break; + } + } catch (IOException ioe) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("upgradeHandler.ioerror", connectionId), ioe); + } + close(); + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.upgradeDispatch.exit", connectionId, result)); + } + return result; + } + + + /* + * Sets the connection timeout based on the current number of active streams. + */ + protected void setConnectionTimeoutForStreamCount(int streamCount) { + if (streamCount == 0) { + // No streams currently active. Use the keep-alive + // timeout for the connection. + long keepAliveTimeout = protocol.getKeepAliveTimeout(); + if (keepAliveTimeout == -1) { + setConnectionTimeout(-1); + } else { + setConnectionTimeout(System.currentTimeMillis() + keepAliveTimeout); + } + } else { + // Streams currently active. Individual streams have + // timeouts so keep the connection open. + setConnectionTimeout(-1); + } + } + + + private void setConnectionTimeout(long connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + + @Override + public void timeoutAsync(long now) { + long connectionTimeout = this.connectionTimeout; + if (now == -1 || connectionTimeout > -1 && now > connectionTimeout) { + // Have to dispatch as this will be executed from a non-container + // thread. + socketWrapper.processSocket(SocketEvent.TIMEOUT, true); + } + } + + + ConnectionSettingsRemote getRemoteSettings() { + return remoteSettings; + } + + + ConnectionSettingsLocal getLocalSettings() { + return localSettings; + } + + + Http2Protocol getProtocol() { + return protocol; + } + + + @Override + public void pause() { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.pause.entry", connectionId)); + } + + if (connectionState.compareAndSet(ConnectionState.CONNECTED, ConnectionState.PAUSING)) { + pausedNanoTime = System.nanoTime(); + + try { + writeGoAwayFrame((1 << 31) - 1, Http2Error.NO_ERROR.getCode(), null); + } catch (IOException ioe) { + // This is fatal for the connection. Ignore it here. There will be + // further attempts at I/O in upgradeDispatch() and it can better + // handle the IO errors. + } + } + } + + + @Override + public void destroy() { + // NO-OP + } + + + void checkPauseState() throws IOException { + if (connectionState.get() == ConnectionState.PAUSING) { + if (pausedNanoTime + pingManager.getRoundTripTimeNano() < System.nanoTime()) { + connectionState.compareAndSet(ConnectionState.PAUSING, ConnectionState.PAUSED); + writeGoAwayFrame(maxProcessedStreamId, Http2Error.NO_ERROR.getCode(), null); + } + } + } + + + private int increaseStreamConcurrency() { + return streamConcurrency.incrementAndGet(); + } + + private int decreaseStreamConcurrency() { + return streamConcurrency.decrementAndGet(); + } + + private int getStreamConcurrency() { + return streamConcurrency.get(); + } + + void executeQueuedStream() { + if (streamConcurrency == null) { + return; + } + decreaseStreamConcurrency(); + if (getStreamConcurrency() < protocol.getMaxConcurrentStreamExecution()) { + StreamRunnable streamRunnable = queuedRunnable.poll(); + if (streamRunnable != null) { + increaseStreamConcurrency(); + socketWrapper.execute(streamRunnable); + } + } + } + + + void sendStreamReset(StreamStateMachine state, StreamException se) throws IOException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.rst.debug", connectionId, Integer.toString(se.getStreamId()), + se.getError(), se.getMessage())); + } + + // Write a RST frame + byte[] rstFrame = new byte[13]; + // Length + ByteUtil.setThreeBytes(rstFrame, 0, 4); + // Type + rstFrame[3] = FrameType.RST.getIdByte(); + // No flags + // Stream ID + ByteUtil.set31Bits(rstFrame, 5, se.getStreamId()); + // Payload + ByteUtil.setFourBytes(rstFrame, 9, se.getError().getCode()); + + // Need to update state atomically with the sending of the RST + // frame else other threads currently working with this stream + // may see the state change and send a RST frame before the RST + // frame triggered by this thread. If that happens the client + // may see out of order RST frames which may hard to follow if + // the client is unaware the RST frames may be received out of + // order. + socketWrapper.getLock().lock(); + try { + if (state != null) { + boolean active = state.isActive(); + state.sendReset(); + if (active) { + decrementActiveRemoteStreamCount(); + } + } + socketWrapper.write(true, rstFrame, 0, rstFrame.length); + socketWrapper.flush(true); + } finally { + socketWrapper.getLock().unlock(); + } + } + + + void closeConnection(Http2Exception ce) { + long code; + byte[] msg; + if (ce == null) { + code = Http2Error.NO_ERROR.getCode(); + msg = null; + } else { + code = ce.getError().getCode(); + msg = ce.getMessage().getBytes(StandardCharsets.UTF_8); + } + try { + writeGoAwayFrame(maxProcessedStreamId, code, msg); + } catch (IOException ioe) { + // Ignore. GOAWAY is sent on a best efforts basis and the original + // error has already been logged. + } + close(); + } + + + /** + * Write the initial settings frame and any necessary supporting frames. If the initial settings increase the + * initial window size, it will also be necessary to send a WINDOW_UPDATE frame to increase the size of the flow + * control window for the connection (stream 0). + */ + protected void writeSettings() { + // Send the initial settings frame + try { + byte[] settings = localSettings.getSettingsFrameForPending(); + socketWrapper.write(true, settings, 0, settings.length); + byte[] windowUpdateFrame = createWindowUpdateForSettings(); + if (windowUpdateFrame.length > 0) { + socketWrapper.write(true, windowUpdateFrame, 0, windowUpdateFrame.length); + } + socketWrapper.flush(true); + } catch (IOException ioe) { + String msg = sm.getString("upgradeHandler.sendPrefaceFail", connectionId); + if (log.isDebugEnabled()) { + log.debug(msg); + } + throw new ProtocolException(msg, ioe); + } + } + + + /** + * @return The WINDOW_UPDATE frame if one is required or an empty array if no WINDOW_UPDATE is required. + */ + protected byte[] createWindowUpdateForSettings() { + // Build a WINDOW_UPDATE frame if one is required. If not, create an + // empty byte array. + byte[] windowUpdateFrame; + int increment = protocol.getInitialWindowSize() - ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE; + if (increment > 0) { + // Build window update frame for stream 0 + windowUpdateFrame = new byte[13]; + ByteUtil.setThreeBytes(windowUpdateFrame, 0, 4); + windowUpdateFrame[3] = FrameType.WINDOW_UPDATE.getIdByte(); + ByteUtil.set31Bits(windowUpdateFrame, 9, increment); + } else { + windowUpdateFrame = new byte[0]; + } + + return windowUpdateFrame; + } + + + protected void writeGoAwayFrame(int maxStreamId, long errorCode, byte[] debugMsg) throws IOException { + byte[] fixedPayload = new byte[8]; + ByteUtil.set31Bits(fixedPayload, 0, maxStreamId); + ByteUtil.setFourBytes(fixedPayload, 4, errorCode); + int len = 8; + if (debugMsg != null) { + len += debugMsg.length; + } + byte[] payloadLength = new byte[3]; + ByteUtil.setThreeBytes(payloadLength, 0, len); + + Lock lock = socketWrapper.getLock(); + lock.lock(); + try { + socketWrapper.write(true, payloadLength, 0, payloadLength.length); + socketWrapper.write(true, GOAWAY, 0, GOAWAY.length); + socketWrapper.write(true, fixedPayload, 0, 8); + if (debugMsg != null) { + socketWrapper.write(true, debugMsg, 0, debugMsg.length); + } + socketWrapper.flush(true); + } finally { + lock.unlock(); + } + } + + void writeHeaders(Stream stream, int pushedStreamId, MimeHeaders mimeHeaders, boolean endOfStream, int payloadSize) + throws IOException { + // This ensures the Stream processing thread has control of the socket. + Lock lock = socketWrapper.getLock(); + lock.lock(); + try { + doWriteHeaders(stream, pushedStreamId, mimeHeaders, endOfStream, payloadSize); + } finally { + lock.unlock(); + } + stream.sentHeaders(); + if (endOfStream) { + sentEndOfStream(stream); + } + } + + + /* + * Separate method to allow Http2AsyncUpgradeHandler to call this code without synchronizing on socketWrapper since + * it doesn't need to. + */ + protected HeaderFrameBuffers doWriteHeaders(Stream stream, int pushedStreamId, MimeHeaders mimeHeaders, + boolean endOfStream, int payloadSize) throws IOException { + + if (log.isTraceEnabled()) { + if (pushedStreamId == 0) { + log.trace(sm.getString("upgradeHandler.writeHeaders", connectionId, stream.getIdAsString(), + Boolean.valueOf(endOfStream))); + } else { + log.trace(sm.getString("upgradeHandler.writePushHeaders", connectionId, stream.getIdAsString(), + Integer.valueOf(pushedStreamId), Boolean.valueOf(endOfStream))); + } + } + + if (!stream.canWrite()) { + return null; + } + + HeaderFrameBuffers headerFrameBuffers = getHeaderFrameBuffers(payloadSize); + + byte[] pushedStreamIdBytes = null; + if (pushedStreamId > 0) { + pushedStreamIdBytes = new byte[4]; + ByteUtil.set31Bits(pushedStreamIdBytes, 0, pushedStreamId); + } + + boolean first = true; + State state = null; + + while (state != State.COMPLETE) { + headerFrameBuffers.startFrame(); + if (first && pushedStreamIdBytes != null) { + headerFrameBuffers.getPayload().put(pushedStreamIdBytes); + } + state = getHpackEncoder().encode(mimeHeaders, headerFrameBuffers.getPayload()); + headerFrameBuffers.getPayload().flip(); + if (state == State.COMPLETE || headerFrameBuffers.getPayload().limit() > 0) { + ByteUtil.setThreeBytes(headerFrameBuffers.getHeader(), 0, headerFrameBuffers.getPayload().limit()); + if (first) { + first = false; + if (pushedStreamIdBytes == null) { + headerFrameBuffers.getHeader()[3] = FrameType.HEADERS.getIdByte(); + } else { + headerFrameBuffers.getHeader()[3] = FrameType.PUSH_PROMISE.getIdByte(); + } + if (endOfStream) { + headerFrameBuffers.getHeader()[4] = FLAG_END_OF_STREAM; + } + } else { + headerFrameBuffers.getHeader()[3] = FrameType.CONTINUATION.getIdByte(); + } + if (state == State.COMPLETE) { + headerFrameBuffers.getHeader()[4] += FLAG_END_OF_HEADERS; + } + if (log.isTraceEnabled()) { + log.trace(headerFrameBuffers.getPayload().limit() + " bytes"); + } + ByteUtil.set31Bits(headerFrameBuffers.getHeader(), 5, stream.getIdAsInt()); + headerFrameBuffers.endFrame(); + } else if (state == State.UNDERFLOW) { + headerFrameBuffers.expandPayload(); + } + } + headerFrameBuffers.endHeaders(); + return headerFrameBuffers; + } + + protected HeaderFrameBuffers getHeaderFrameBuffers(int initialPayloadSize) { + return new DefaultHeaderFrameBuffers(initialPayloadSize); + } + + + protected HpackEncoder getHpackEncoder() { + if (hpackEncoder == null) { + hpackEncoder = new HpackEncoder(); + } + // Ensure latest agreed table size is used + hpackEncoder.setMaxTableSize(remoteSettings.getHeaderTableSize()); + return hpackEncoder; + } + + + void writeBody(Stream stream, ByteBuffer data, int len, boolean finished) throws IOException { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.writeBody", connectionId, stream.getIdAsString(), + Integer.toString(len), Boolean.valueOf(finished))); + } + + reduceOverheadCount(FrameType.DATA); + + // Need to check this now since sending end of stream will change this. + boolean writable = stream.canWrite(); + byte[] header = new byte[9]; + ByteUtil.setThreeBytes(header, 0, len); + header[3] = FrameType.DATA.getIdByte(); + if (finished) { + header[4] = FLAG_END_OF_STREAM; + sentEndOfStream(stream); + } + if (writable) { + ByteUtil.set31Bits(header, 5, stream.getIdAsInt()); + socketWrapper.getLock().lock(); + try { + socketWrapper.write(true, header, 0, header.length); + int orgLimit = data.limit(); + data.limit(data.position() + len); + socketWrapper.write(true, data); + data.limit(orgLimit); + socketWrapper.flush(true); + } catch (IOException ioe) { + handleAppInitiatedIOException(ioe); + } finally { + socketWrapper.getLock().unlock(); + } + } + } + + + protected void sentEndOfStream(Stream stream) { + stream.sentEndOfStream(); + if (!stream.isActive()) { + decrementActiveRemoteStreamCount(); + } + } + + + /* + * Handles an I/O error on the socket underlying the HTTP/2 connection when it is triggered by application code + * (usually reading the request or writing the response). Such I/O errors are fatal so the connection is closed. The + * exception is re-thrown to make the client code aware of the problem. + * + * Note: We can not rely on this exception reaching the socket processor since the application code may swallow it. + */ + protected void handleAppInitiatedIOException(IOException ioe) throws IOException { + close(); + throw ioe; + } + + + /* + * Needs to know if this was application initiated since that affects the error handling. + */ + void writeWindowUpdate(AbstractNonZeroStream stream, int increment, boolean applicationInitiated) + throws IOException { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.windowUpdateConnection", getConnectionId(), + Integer.valueOf(increment))); + } + socketWrapper.getLock().lock(); + try { + // Build window update frame for stream 0 + byte[] frame = new byte[13]; + ByteUtil.setThreeBytes(frame, 0, 4); + frame[3] = FrameType.WINDOW_UPDATE.getIdByte(); + ByteUtil.set31Bits(frame, 9, increment); + socketWrapper.write(true, frame, 0, frame.length); + boolean needFlush = true; + // No need to send update from closed stream + if (stream instanceof Stream && ((Stream) stream).canWrite()) { + int streamIncrement = ((Stream) stream).getWindowUpdateSizeToWrite(increment); + if (streamIncrement > 0) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.windowUpdateStream", getConnectionId(), getIdAsString(), + Integer.valueOf(streamIncrement))); + } + // Re-use buffer as connection update has already been written + ByteUtil.set31Bits(frame, 5, stream.getIdAsInt()); + ByteUtil.set31Bits(frame, 9, streamIncrement); + try { + socketWrapper.write(true, frame, 0, frame.length); + socketWrapper.flush(true); + needFlush = false; + } catch (IOException ioe) { + if (applicationInitiated) { + handleAppInitiatedIOException(ioe); + } else { + throw ioe; + } + } + } + } + if (needFlush) { + socketWrapper.flush(true); + } + } finally { + socketWrapper.getLock().unlock(); + } + } + + + protected void processWrites() throws IOException { + Lock lock = socketWrapper.getLock(); + lock.lock(); + try { + if (socketWrapper.flush(false)) { + socketWrapper.registerWriteInterest(); + } else { + // Only send a ping if there is no other data waiting to be sent. + // Ping manager will ensure they aren't sent too frequently. + pingManager.sendPing(false); + } + } finally { + lock.unlock(); + } + } + + + /* + * Requesting an allocation from the connection window for the specified stream. + */ + int reserveWindowSize(Stream stream, int reservation, boolean block) throws IOException { + /* + * Need to be holding the stream lock so releaseBacklog() can't notify this thread until after this thread + * enters wait(). + */ + int allocation = 0; + stream.windowAllocationLock.lock(); + try { + windowAllocationLock.lock(); + try { + if (!stream.canWrite()) { + stream.doStreamCancel( + sm.getString("upgradeHandler.stream.notWritable", stream.getConnectionId(), + stream.getIdAsString(), stream.state.getCurrentStateName()), + Http2Error.STREAM_CLOSED); + } + long windowSize = getWindowSize(); + if (stream.getConnectionAllocationMade() > 0) { + // The stream is/was in the backlog and has been granted an allocation - use it. + allocation = stream.getConnectionAllocationMade(); + stream.setConnectionAllocationMade(0); + } else if (windowSize < 1) { + /* + * The connection window has no capacity. If the stream has not been granted an allocation, and the + * stream was not already added to the backlog due to an partial reservation (see next else if + * block) add it to the backlog so it can obtain an allocation when capacity is available. + */ + if (stream.getConnectionAllocationMade() == 0 && stream.getConnectionAllocationRequested() == 0) { + stream.setConnectionAllocationRequested(reservation); + backLogSize += reservation; + backLogStreams.add(stream); + } + } else if (windowSize < reservation) { + /* + * The connection window has some capacity but not enough to fill this reservation. Allocate what + * capacity is available and add the stream to the backlog so it can obtain a further allocation + * when capacity is available. + */ + allocation = (int) windowSize; + decrementWindowSize(allocation); + int reservationRemaining = reservation - allocation; + stream.setConnectionAllocationRequested(reservationRemaining); + backLogSize += reservationRemaining; + backLogStreams.add(stream); + + } else { + // The connection window has sufficient capacity for this reservation. Allocate the full amount. + allocation = reservation; + decrementWindowSize(allocation); + } + } finally { + windowAllocationLock.unlock(); + } + if (allocation == 0) { + if (block) { + try { + // Connection level window is empty. Although this + // request is for a stream, use the connection + // timeout + long writeTimeout = protocol.getWriteTimeout(); + stream.waitForConnectionAllocation(writeTimeout); + // Has this stream been granted an allocation + if (stream.getConnectionAllocationMade() == 0) { + String msg; + Http2Error error; + if (stream.isActive()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("upgradeHandler.noAllocation", connectionId, + stream.getIdAsString())); + } + // No allocation + // Close the connection. Do this first since + // closing the stream will raise an exception. + close(); + msg = sm.getString("stream.writeTimeout"); + error = Http2Error.ENHANCE_YOUR_CALM; + } else { + msg = sm.getString("stream.clientCancel"); + error = Http2Error.STREAM_CLOSED; + } + // Close the stream + // This thread is in application code so need + // to signal to the application that the + // stream is closing + stream.doStreamCancel(msg, error); + } else { + allocation = stream.getConnectionAllocationMade(); + stream.setConnectionAllocationMade(0); + } + } catch (InterruptedException e) { + throw new IOException(sm.getString("upgradeHandler.windowSizeReservationInterrupted", + connectionId, stream.getIdAsString(), Integer.toString(reservation)), e); + } + } else { + stream.waitForConnectionAllocationNonBlocking(); + return 0; + } + } + } finally { + stream.windowAllocationLock.unlock(); + } + return allocation; + } + + + @Override + protected void incrementWindowSize(int increment) throws Http2Exception { + Set streamsToNotify = null; + + windowAllocationLock.lock(); + try { + long windowSize = getWindowSize(); + if (windowSize < 1 && windowSize + increment > 0) { + // Connection window is exhausted. Assume there will be streams + // to notify. The overhead is minimal if there are none. + streamsToNotify = releaseBackLog((int) (windowSize + increment)); + } else { + super.incrementWindowSize(increment); + } + } finally { + windowAllocationLock.unlock(); + } + + if (streamsToNotify != null) { + for (AbstractStream stream : streamsToNotify) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.releaseBacklog", connectionId, stream.getIdAsString())); + } + // There is never any O/P on stream zero but it is included in + // the backlog as it simplifies the code. Skip it if it appears + // here. + if (this == stream) { + continue; + } + ((Stream) stream).notifyConnection(); + } + } + } + + + /** + * Process send file (if supported) for the given stream. The appropriate request attributes should be set before + * calling this method. + * + * @param sendfileData The stream and associated data to process + * + * @return The result of the send file processing + */ + protected SendfileState processSendfile(SendfileData sendfileData) { + return SendfileState.DONE; + } + + + private Set releaseBackLog(int increment) throws Http2Exception { + windowAllocationLock.lock(); + try { + Set result = new HashSet<>(); + if (backLogSize < increment) { + // Can clear the whole backlog + for (AbstractStream stream : backLogStreams) { + if (stream.getConnectionAllocationRequested() > 0) { + stream.setConnectionAllocationMade(stream.getConnectionAllocationRequested()); + stream.setConnectionAllocationRequested(0); + result.add(stream); + } + } + // Cast is safe due to test above + int remaining = increment - (int) backLogSize; + backLogSize = 0; + super.incrementWindowSize(remaining); + + backLogStreams.clear(); + } else { + // Can't clear the whole backlog. + // Need streams in priority order + Set orderedStreams = new ConcurrentSkipListSet<>(Comparator.comparingInt(Stream::getUrgency) + .thenComparing(Stream::getIncremental).thenComparing(Stream::getIdAsInt)); + orderedStreams.addAll(backLogStreams); + + // Iteration 1. Need to work out how much we can clear. + long urgencyWhereAllocationIsExhausted = 0; + long requestedAllocationForIncrementalStreams = 0; + int remaining = increment; + Iterator orderedStreamsIterator = orderedStreams.iterator(); + while (orderedStreamsIterator.hasNext()) { + Stream s = orderedStreamsIterator.next(); + if (urgencyWhereAllocationIsExhausted < s.getUrgency()) { + if (remaining < 1) { + break; + } + requestedAllocationForIncrementalStreams = 0; + } + urgencyWhereAllocationIsExhausted = s.getUrgency(); + if (s.getIncremental()) { + requestedAllocationForIncrementalStreams += s.getConnectionAllocationRequested(); + remaining -= s.getConnectionAllocationRequested(); + } else { + remaining -= s.getConnectionAllocationRequested(); + if (remaining < 1) { + break; + } + } + } + + // Iteration 2. Allocate. + // Reset for second iteration + remaining = increment; + orderedStreamsIterator = orderedStreams.iterator(); + while (orderedStreamsIterator.hasNext()) { + Stream s = orderedStreamsIterator.next(); + if (s.getUrgency() < urgencyWhereAllocationIsExhausted) { + // Can fully allocate + remaining = allocate(s, remaining); + result.add(s); + orderedStreamsIterator.remove(); + backLogStreams.remove(s); + } else if (requestedAllocationForIncrementalStreams == 0) { + // Allocation ran out in non-incremental streams so fully + // allocate in iterator order until allocation is exhausted + remaining = allocate(s, remaining); + result.add(s); + if (s.getConnectionAllocationRequested() == 0) { + // Fully allocated + orderedStreamsIterator.remove(); + backLogStreams.remove(s); + } + if (remaining < 1) { + break; + } + } else { + // Allocation ran out in incremental streams. Distribute + // remaining allocation between the incremental streams at + // this urgency level. + if (s.getUrgency() != urgencyWhereAllocationIsExhausted) { + break; + } + + int share = (int) (s.getConnectionAllocationRequested() * remaining / + requestedAllocationForIncrementalStreams); + if (share == 0) { + share = 1; + } + allocate(s, share); + result.add(s); + if (s.getConnectionAllocationRequested() == 0) { + // Fully allocated (unlikely but possible due to + // rounding if only a few bytes required). + orderedStreamsIterator.remove(); + backLogStreams.remove(s); + } + } + } + } + return result; + } finally { + windowAllocationLock.unlock(); + } + } + + + private int allocate(AbstractStream stream, int allocation) { + windowAllocationLock.lock(); + try { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.allocate.debug", getConnectionId(), stream.getIdAsString(), + Integer.toString(allocation))); + } + + int leftToAllocate = allocation; + + if (stream.getConnectionAllocationRequested() > 0) { + int allocatedThisTime; + if (allocation >= stream.getConnectionAllocationRequested()) { + allocatedThisTime = stream.getConnectionAllocationRequested(); + } else { + allocatedThisTime = allocation; + } + stream.setConnectionAllocationRequested(stream.getConnectionAllocationRequested() - allocatedThisTime); + stream.setConnectionAllocationMade(stream.getConnectionAllocationMade() + allocatedThisTime); + leftToAllocate = leftToAllocate - allocatedThisTime; + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.allocate.left", getConnectionId(), stream.getIdAsString(), + Integer.toString(leftToAllocate))); + } + + return leftToAllocate; + } finally { + windowAllocationLock.unlock(); + } + } + + + private Stream getStream(int streamId) { + Integer key = Integer.valueOf(streamId); + AbstractStream result = streams.get(key); + if (result instanceof Stream) { + return (Stream) result; + } + return null; + } + + + private Stream getStream(int streamId, boolean unknownIsError) throws ConnectionException { + Stream result = getStream(streamId); + if (result == null && unknownIsError) { + // Stream has been closed and removed from the map + throw new ConnectionException(sm.getString("upgradeHandler.stream.closed", Integer.toString(streamId)), + Http2Error.PROTOCOL_ERROR); + } + return result; + } + + + private AbstractNonZeroStream getAbstractNonZeroStream(int streamId) { + Integer key = Integer.valueOf(streamId); + return streams.get(key); + } + + + private AbstractNonZeroStream getAbstractNonZeroStream(int streamId, boolean unknownIsError) + throws ConnectionException { + AbstractNonZeroStream result = getAbstractNonZeroStream(streamId); + if (result == null && unknownIsError) { + // Stream has been closed and removed from the map + throw new ConnectionException(sm.getString("upgradeHandler.stream.closed", Integer.toString(streamId)), + Http2Error.PROTOCOL_ERROR); + } + return result; + } + + + private Stream createRemoteStream(int streamId) throws ConnectionException { + Integer key = Integer.valueOf(streamId); + + if (streamId % 2 != 1) { + throw new ConnectionException(sm.getString("upgradeHandler.stream.even", key), Http2Error.PROTOCOL_ERROR); + } + + pruneClosedStreams(streamId); + + Stream result = new Stream(key, this); + streams.put(key, result); + return result; + } + + + private Stream createLocalStream(Request request) { + int streamId = nextLocalStreamId.getAndAdd(2); + + Integer key = Integer.valueOf(streamId); + + Stream result = new Stream(key, this, request); + streams.put(key, result); + return result; + } + + + private void close() { + ConnectionState previous = connectionState.getAndSet(ConnectionState.CLOSED); + if (previous == ConnectionState.CLOSED) { + // Already closed + return; + } + + for (AbstractNonZeroStream stream : streams.values()) { + if (stream instanceof Stream) { + // The connection is closing. Close the associated streams as no + // longer required (also notifies any threads waiting for allocations). + ((Stream) stream).receiveReset(Http2Error.CANCEL.getCode()); + } + } + try { + socketWrapper.close(); + } catch (Exception e) { + log.debug(sm.getString("upgradeHandler.socketCloseFailed"), e); + } + } + + + private void pruneClosedStreams(int streamId) { + // Only prune every 10 new streams + if (newStreamsSinceLastPrune < 9) { + // Not atomic. Increments may be lost. Not a problem. + newStreamsSinceLastPrune++; + return; + } + // Reset counter + newStreamsSinceLastPrune = 0; + + // RFC 7540, 5.3.4 endpoints should maintain state for at least the + // maximum number of concurrent streams. + long max = localSettings.getMaxConcurrentStreams(); + + // Ideally need to retain information for a "significant" amount of time + // after sending END_STREAM (RFC 7540, page 20) so we detect potential + // connection error. 5x seems reasonable. The client will have had + // plenty of opportunity to process the END_STREAM if another 5x max + // concurrent streams have been processed. + max = max * 5; + if (max > Integer.MAX_VALUE) { + max = Integer.MAX_VALUE; + } + + final int size = streams.size(); + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.pruneStart", connectionId, Long.toString(max), + Integer.toString(size))); + } + + int toClose = size - (int) max; + + // Need to try and prune some streams. Prune streams starting with the + // oldest. Pruning stops as soon as enough streams have been pruned. + // Iterator is in key order. + for (AbstractNonZeroStream stream : streams.values()) { + if (toClose < 1) { + return; + } + if (stream instanceof Stream && ((Stream) stream).isActive()) { + continue; + } + streams.remove(stream.getIdentifier()); + toClose--; + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.pruned", connectionId, stream.getIdAsString())); + } + + } + + if (toClose > 0) { + log.warn(sm.getString("upgradeHandler.pruneIncomplete", connectionId, Integer.toString(streamId), + Integer.toString(toClose))); + } + } + + + void push(Request request, Stream associatedStream) throws IOException { + if (localSettings.getMaxConcurrentStreams() < activeRemoteStreamCount.incrementAndGet()) { + // If there are too many open streams, simply ignore the push + // request. + setConnectionTimeoutForStreamCount(activeRemoteStreamCount.decrementAndGet()); + return; + } + + Stream pushStream; + + /* + * Uses SocketWrapper lock since PUSH_PROMISE frames have to be sent in order. Once the stream has been created + * we need to ensure that the PUSH_PROMISE is sent before the next stream is created for a PUSH_PROMISE. + */ + Lock lock = socketWrapper.getLock(); + lock.lock(); + try { + pushStream = createLocalStream(request); + writeHeaders(associatedStream, pushStream.getIdAsInt(), request.getMimeHeaders(), false, + Constants.DEFAULT_HEADERS_FRAME_SIZE); + } finally { + lock.unlock(); + } + + pushStream.sentPushPromise(); + + processStreamOnContainerThread(pushStream); + } + + + @Override + protected final String getConnectionId() { + return connectionId; + } + + + void reduceOverheadCount(FrameType frameType) { + // A non-overhead frame reduces the overhead count by + // Http2Protocol.DEFAULT_OVERHEAD_REDUCTION_FACTOR. A simple browser + // request is likely to have one non-overhead frame (HEADERS) and one + // overhead frame (REPRIORITISE). With the default settings the overhead + // count will reduce by 10 for each simple request. + // Requests and responses with bodies will create additional + // non-overhead frames, further reducing the overhead count. + updateOverheadCount(frameType, Http2Protocol.DEFAULT_OVERHEAD_REDUCTION_FACTOR); + } + + + @Override + public void increaseOverheadCount(FrameType frameType) { + // An overhead frame increases the overhead count by + // overheadCountFactor. By default, this means an overhead frame + // increases the overhead count by 10. A simple browser request is + // likely to have one non-overhead frame (HEADERS) and one overhead + // frame (REPRIORITISE). With the default settings the overhead count + // will reduce by 10 for each simple request. + updateOverheadCount(frameType, getProtocol().getOverheadCountFactor()); + } + + + private void increaseOverheadCount(FrameType frameType, int increment) { + // Overhead frames that indicate inefficient (and potentially malicious) + // use of small frames trigger an increase that is inversely + // proportional to size. The default threshold for all three potential + // areas for abuse (HEADERS, DATA, WINDOW_UPDATE) is 1024 bytes. Frames + // with sizes smaller than this will trigger an increase of + // threshold/size. + // DATA and WINDOW_UPDATE take an average over the last two non-final + // frames to allow for client buffering schemes that can result in some + // small DATA payloads. + updateOverheadCount(frameType, increment); + } + + + private void updateOverheadCount(FrameType frameType, int increment) { + long newOverheadCount = overheadCount.addAndGet(increment); + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.overheadChange", connectionId, getIdAsString(), frameType.name(), + Long.valueOf(newOverheadCount))); + } + } + + + boolean isOverheadLimitExceeded() { + return overheadCount.get() > 0; + } + + + // ----------------------------------------------- Http2Parser.Input methods + + @Override + public boolean fill(boolean block, byte[] data, int offset, int length) throws IOException { + int len = length; + int pos = offset; + boolean nextReadBlock = block; + int thisRead = 0; + + while (len > 0) { + // Blocking reads use the protocol level read timeout. Non-blocking + // reads do not timeout. The intention is that once a frame has + // started to be read, the read timeout applies until it is + // completely read. + if (nextReadBlock) { + socketWrapper.setReadTimeout(protocol.getReadTimeout()); + } else { + socketWrapper.setReadTimeout(-1); + } + thisRead = socketWrapper.read(nextReadBlock, data, pos, len); + if (thisRead == 0) { + if (nextReadBlock) { + // Should never happen + throw new IllegalStateException(); + } else { + return false; + } + } else if (thisRead == -1) { + if (connectionState.get().isNewStreamAllowed()) { + throw new EOFException(); + } else { + return false; + } + } else { + pos += thisRead; + len -= thisRead; + nextReadBlock = true; + } + } + + return true; + } + + + @Override + public int getMaxFrameSize() { + return localSettings.getMaxFrameSize(); + } + + + // ---------------------------------------------- Http2Parser.Output methods + + @Override + public HpackDecoder getHpackDecoder() { + if (hpackDecoder == null) { + hpackDecoder = new HpackDecoder(localSettings.getHeaderTableSize()); + } + return hpackDecoder; + } + + + @Override + public ByteBuffer startRequestBodyFrame(int streamId, int payloadSize, boolean endOfStream) throws Http2Exception { + // DATA frames reduce the overhead count ... + reduceOverheadCount(FrameType.DATA); + + // .. but lots of small payloads are inefficient so that will increase + // the overhead count unless it is the final DATA frame where small + // payloads are expected. + + // See also https://bz.apache.org/bugzilla/show_bug.cgi?id=63690 + // The buffering behaviour of some clients means that small data frames + // are much more frequent (roughly 1 in 20) than expected. Use an + // average over two frames to avoid false positives. + if (!endOfStream) { + int overheadThreshold = protocol.getOverheadDataThreshold(); + int average = (lastNonFinalDataPayload >> 1) + (payloadSize >> 1); + lastNonFinalDataPayload = payloadSize; + // Avoid division by zero + if (average == 0) { + average = 1; + } + if (average < overheadThreshold) { + increaseOverheadCount(FrameType.DATA, overheadThreshold / average); + } + } + + AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(streamId, true); + abstractNonZeroStream.checkState(FrameType.DATA); + abstractNonZeroStream.receivedData(payloadSize); + ByteBuffer result = abstractNonZeroStream.getInputByteBuffer(); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.startRequestBodyFrame.result", getConnectionId(), + abstractNonZeroStream.getIdAsString(), result)); + } + + return result; + } + + + @Override + public void endRequestBodyFrame(int streamId, int dataLength) throws Http2Exception, IOException { + AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(streamId, true); + if (abstractNonZeroStream instanceof Stream) { + ((Stream) abstractNonZeroStream).getInputBuffer().onDataAvailable(); + } else { + // The Stream was recycled between the call in Http2Parser to + // startRequestBodyFrame() and the synchronized block that contains + // the call to this method. This means the bytes read will have been + // written to the original stream and, effectively, swallowed. + // Therefore, need to notify that those bytes were swallowed here. + if (dataLength>0) { + onSwallowedDataFramePayload(streamId, dataLength); + } + } + } + + + @Override + public void onSwallowedDataFramePayload(int streamId, int swallowedDataBytesCount) throws IOException { + AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(streamId); + writeWindowUpdate(abstractNonZeroStream, swallowedDataBytesCount, false); + } + + + @Override + public HeaderEmitter headersStart(int streamId, boolean headersEndStream) throws Http2Exception, IOException { + + // Check the pause state before processing headers since the pause state + // determines if a new stream is created or if this stream is ignored. + checkPauseState(); + + if (connectionState.get().isNewStreamAllowed()) { + Stream stream = getStream(streamId, false); + if (stream == null) { + stream = createRemoteStream(streamId); + } + if (streamId < maxActiveRemoteStreamId) { + throw new ConnectionException(sm.getString("upgradeHandler.stream.old", Integer.valueOf(streamId), + Integer.valueOf(maxActiveRemoteStreamId)), Http2Error.PROTOCOL_ERROR); + } + stream.checkState(FrameType.HEADERS); + stream.receivedStartOfHeaders(headersEndStream); + closeIdleStreams(streamId); + return stream; + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.noNewStreams", connectionId, Integer.toString(streamId))); + } + reduceOverheadCount(FrameType.HEADERS); + // Stateless so a static can be used to save on GC + return HEADER_SINK; + } + } + + + private void closeIdleStreams(int newMaxActiveRemoteStreamId) { + final ConcurrentNavigableMap subMap = streams.subMap( + Integer.valueOf(maxActiveRemoteStreamId), false, Integer.valueOf(newMaxActiveRemoteStreamId), false); + for (AbstractNonZeroStream stream : subMap.values()) { + if (stream instanceof Stream) { + ((Stream) stream).closeIfIdle(); + } + } + maxActiveRemoteStreamId = newMaxActiveRemoteStreamId; + } + + + /** + * Unused - NO-OP. + * + * @param streamId Unused + * @param parentStreamId Unused + * @param exclusive Unused + * @param weight Unused + * @throws Http2Exception Never thrown + * + * @deprecated Unused. Will be removed in Tomcat 11 onwards. + */ + @Deprecated + public void reprioritise(int streamId, int parentStreamId, boolean exclusive, int weight) throws Http2Exception { + // NO-OP + } + + + @Override + public void headersContinue(int payloadSize, boolean endOfHeaders) { + // Generally, continuation frames don't impact the overhead count but if + // they are small and the frame isn't the final header frame then that + // is indicative of an abusive client + if (!endOfHeaders) { + int overheadThreshold = getProtocol().getOverheadContinuationThreshold(); + if (payloadSize < overheadThreshold) { + if (payloadSize == 0) { + // Avoid division by zero + increaseOverheadCount(FrameType.HEADERS, overheadThreshold); + } else { + increaseOverheadCount(FrameType.HEADERS, overheadThreshold / payloadSize); + } + } + } + } + + + @Override + public void headersEnd(int streamId, boolean endOfStream) throws Http2Exception { + AbstractNonZeroStream abstractNonZeroStream = + getAbstractNonZeroStream(streamId, connectionState.get().isNewStreamAllowed()); + if (abstractNonZeroStream instanceof Stream) { + boolean processStream = false; + setMaxProcessedStream(streamId); + Stream stream = (Stream) abstractNonZeroStream; + if (stream.isActive()) { + if (stream.receivedEndOfHeaders()) { + + if (localSettings.getMaxConcurrentStreams() < activeRemoteStreamCount.incrementAndGet()) { + decrementActiveRemoteStreamCount(); + // Ignoring maxConcurrentStreams increases the overhead count + increaseOverheadCount(FrameType.HEADERS); + throw new StreamException( + sm.getString("upgradeHandler.tooManyRemoteStreams", + Long.toString(localSettings.getMaxConcurrentStreams())), + Http2Error.REFUSED_STREAM, streamId); + } + // Valid new stream reduces the overhead count + reduceOverheadCount(FrameType.HEADERS); + + processStream = true; + } + } + /* + * Need to process end of stream before calling processStreamOnContainerThread to avoid a race condition + * where the container thread finishes before end of stream is processed, thinks the request hasn't been + * fully read so issues a RST with error code 0 (NO_ERROR) to tell the client not to send the request body, + * if any. This breaks tests and generates unnecessary RST messages for standard clients. + */ + if (endOfStream) { + receivedEndOfStream(stream); + } + if (processStream) { + processStreamOnContainerThread(stream); + } + } + } + + + @Override + public void receivedEndOfStream(int streamId) throws ConnectionException { + AbstractNonZeroStream abstractNonZeroStream = + getAbstractNonZeroStream(streamId, connectionState.get().isNewStreamAllowed()); + if (abstractNonZeroStream instanceof Stream) { + Stream stream = (Stream) abstractNonZeroStream; + receivedEndOfStream(stream); + } + } + + + private void receivedEndOfStream(Stream stream) throws ConnectionException { + stream.receivedEndOfStream(); + if (!stream.isActive()) { + decrementActiveRemoteStreamCount(); + } + } + + + private void setMaxProcessedStream(int streamId) { + if (maxProcessedStreamId < streamId) { + maxProcessedStreamId = streamId; + } + } + + + @Override + public void reset(int streamId, long errorCode) throws Http2Exception { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.reset.receive", getConnectionId(), Integer.toString(streamId), + Long.toString(errorCode))); + } + increaseOverheadCount(FrameType.RST, getProtocol().getOverheadResetFactor()); + AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(streamId, true); + abstractNonZeroStream.checkState(FrameType.RST); + if (abstractNonZeroStream instanceof Stream) { + Stream stream = (Stream) abstractNonZeroStream; + boolean active = stream.isActive(); + stream.receiveReset(errorCode); + if (active) { + decrementActiveRemoteStreamCount(); + } + } + } + + + @Override + public void setting(Setting setting, long value) throws ConnectionException { + + increaseOverheadCount(FrameType.SETTINGS); + + // Possible with empty settings frame + if (setting == null) { + return; + } + + // Special handling required + if (setting == Setting.INITIAL_WINDOW_SIZE) { + long oldValue = remoteSettings.getInitialWindowSize(); + // Do this first in case new value is invalid + remoteSettings.set(setting, value); + int diff = (int) (value - oldValue); + for (AbstractNonZeroStream stream : streams.values()) { + try { + stream.incrementWindowSize(diff); + } catch (Http2Exception h2e) { + ((Stream) stream).close(new StreamException( + sm.getString("upgradeHandler.windowSizeTooBig", connectionId, stream.getIdAsString()), + h2e.getError(), stream.getIdAsInt())); + } + } + } else if (setting == Setting.NO_RFC7540_PRIORITIES) { + // This should not be changed after the initial setting + if (value != ConnectionSettingsBase.DEFAULT_NO_RFC7540_PRIORITIES) { + throw new ConnectionException(sm.getString("upgradeHandler.enableRfc7450Priorities", connectionId), + Http2Error.PROTOCOL_ERROR); + } + } else { + remoteSettings.set(setting, value); + } + } + + + @Override + public void settingsEnd(boolean ack) throws IOException { + if (ack) { + if (!localSettings.ack()) { + // Ack was unexpected + log.warn(sm.getString("upgradeHandler.unexpectedAck", connectionId, getIdAsString())); + } + } else { + socketWrapper.getLock().lock(); + try { + socketWrapper.write(true, SETTINGS_ACK, 0, SETTINGS_ACK.length); + socketWrapper.flush(true); + } finally { + socketWrapper.getLock().unlock(); + } + } + } + + + @Override + public void pingReceive(byte[] payload, boolean ack) throws IOException { + if (!ack) { + increaseOverheadCount(FrameType.PING); + } + pingManager.receivePing(payload, ack); + } + + + @Override + public void goaway(int lastStreamId, long errorCode, String debugData) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("upgradeHandler.goaway.debug", connectionId, Integer.toString(lastStreamId), + Long.toHexString(errorCode), debugData)); + } + close(); + } + + + @Override + public void incrementWindowSize(int streamId, int increment) throws Http2Exception { + // See also https://bz.apache.org/bugzilla/show_bug.cgi?id=63690 + // The buffering behaviour of some clients means that small data frames + // are much more frequent (roughly 1 in 20) than expected. Some clients + // issue a Window update for every DATA frame so a similar pattern may + // be observed. Use an average over two frames to avoid false positives. + + int average = (lastWindowUpdate >> 1) + (increment >> 1); + int overheadThreshold = protocol.getOverheadWindowUpdateThreshold(); + lastWindowUpdate = increment; + // Avoid division by zero + if (average == 0) { + average = 1; + } + + if (streamId == 0) { + // Check for small increments which are inefficient + if (average < overheadThreshold) { + // The smaller the increment, the larger the overhead + increaseOverheadCount(FrameType.WINDOW_UPDATE, overheadThreshold / average); + } + + incrementWindowSize(increment); + } else { + AbstractNonZeroStream stream = getAbstractNonZeroStream(streamId, true); + + // Check for small increments which are inefficient + if (average < overheadThreshold) { + // For Streams, client might only release the minimum so check + // against current demand + if (increment < stream.getConnectionAllocationRequested()) { + // The smaller the increment, the larger the overhead + increaseOverheadCount(FrameType.WINDOW_UPDATE, overheadThreshold / average); + } + } + + stream.checkState(FrameType.WINDOW_UPDATE); + stream.incrementWindowSize(increment); + } + } + + + @Override + public void priorityUpdate(int prioritizedStreamID, Priority p) throws Http2Exception { + increaseOverheadCount(FrameType.PRIORITY_UPDATE); + AbstractNonZeroStream abstractNonZeroStream = getAbstractNonZeroStream(prioritizedStreamID, true); + if (abstractNonZeroStream instanceof Stream) { + Stream stream = (Stream) abstractNonZeroStream; + stream.setUrgency(p.getUrgency()); + stream.setIncremental(p.getIncremental()); + } + } + + + @Override + public void onSwallowedUnknownFrame(int streamId, int frameTypeId, int flags, int size) throws IOException { + // NO-OP. + } + + + void replaceStream(AbstractNonZeroStream original, AbstractNonZeroStream replacement) { + AbstractNonZeroStream current = streams.get(original.getIdentifier()); + // Only replace the stream if it currently uses the full implementation. + if (current instanceof Stream) { + streams.put(original.getIdentifier(), replacement); + } + } + + + public ServletConnection getServletConnection() { + if (socketWrapper.getSslSupport() == null) { + return socketWrapper.getServletConnection("h2c", ""); + } else { + return socketWrapper.getServletConnection("h2", ""); + } + } + + protected class PingManager { + + protected boolean initiateDisabled = false; + + // 10 seconds + protected final long pingIntervalNano = 10000000000L; + + protected int sequence = 0; + protected long lastPingNanoTime = Long.MIN_VALUE; + + protected Queue inflightPings = new ConcurrentLinkedQueue<>(); + protected Queue roundTripTimes = new ConcurrentLinkedQueue<>(); + + /** + * Check to see if a ping was sent recently and, if not, send one. + * + * @param force Send a ping, even if one was sent recently + * + * @throws IOException If an I/O issue prevents the ping from being sent + */ + public void sendPing(boolean force) throws IOException { + if (initiateDisabled) { + return; + } + long now = System.nanoTime(); + if (force || now - lastPingNanoTime > pingIntervalNano) { + lastPingNanoTime = now; + byte[] payload = new byte[8]; + socketWrapper.getLock().lock(); + try { + int sentSequence = ++sequence; + PingRecord pingRecord = new PingRecord(sentSequence, now); + inflightPings.add(pingRecord); + ByteUtil.set31Bits(payload, 4, sentSequence); + socketWrapper.write(true, PING, 0, PING.length); + socketWrapper.write(true, payload, 0, payload.length); + socketWrapper.flush(true); + } finally { + socketWrapper.getLock().unlock(); + } + } + } + + public void receivePing(byte[] payload, boolean ack) throws IOException { + if (ack) { + // Extract the sequence from the payload + int receivedSequence = ByteUtil.get31Bits(payload, 4); + PingRecord pingRecord = inflightPings.poll(); + while (pingRecord != null && pingRecord.getSequence() < receivedSequence) { + pingRecord = inflightPings.poll(); + } + if (pingRecord == null) { + // Unexpected ACK. Log it. + } else { + long roundTripTime = System.nanoTime() - pingRecord.getSentNanoTime(); + roundTripTimes.add(Long.valueOf(roundTripTime)); + while (roundTripTimes.size() > 3) { + // Ignore the returned value as we just want to reduce + // the queue to 3 entries to use for the rolling average. + roundTripTimes.poll(); + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("pingManager.roundTripTime", connectionId, Long.valueOf(roundTripTime))); + } + } + + } else { + // Client originated ping. Echo it back. + socketWrapper.getLock().lock(); + try { + socketWrapper.write(true, PING_ACK, 0, PING_ACK.length); + socketWrapper.write(true, payload, 0, payload.length); + socketWrapper.flush(true); + } finally { + socketWrapper.getLock().unlock(); + } + } + } + + public long getRoundTripTimeNano() { + return (long) roundTripTimes.stream().mapToLong(Long::longValue).average().orElse(0); + } + } + + + protected static class PingRecord { + + private final int sequence; + private final long sentNanoTime; + + public PingRecord(int sequence, long sentNanoTime) { + this.sequence = sequence; + this.sentNanoTime = sentNanoTime; + } + + public int getSequence() { + return sequence; + } + + public long getSentNanoTime() { + return sentNanoTime; + } + } + + + private enum ConnectionState { + + NEW(true), + CONNECTED(true), + PAUSING(true), + PAUSED(false), + CLOSED(false); + + private final boolean newStreamsAllowed; + + ConnectionState(boolean newStreamsAllowed) { + this.newStreamsAllowed = newStreamsAllowed; + } + + public boolean isNewStreamAllowed() { + return newStreamsAllowed; + } + } + + + protected interface HeaderFrameBuffers { + void startFrame(); + + void endFrame() throws IOException; + + void endHeaders() throws IOException; + + byte[] getHeader(); + + ByteBuffer getPayload(); + + void expandPayload(); + } + + + private class DefaultHeaderFrameBuffers implements HeaderFrameBuffers { + + private final byte[] header; + private ByteBuffer payload; + + DefaultHeaderFrameBuffers(int initialPayloadSize) { + header = new byte[9]; + payload = ByteBuffer.allocate(initialPayloadSize); + } + + @Override + public void startFrame() { + // NO-OP + } + + + @Override + public void endFrame() throws IOException { + try { + socketWrapper.write(true, header, 0, header.length); + socketWrapper.write(true, payload); + socketWrapper.flush(true); + } catch (IOException ioe) { + handleAppInitiatedIOException(ioe); + } + payload.clear(); + } + + @Override + public void endHeaders() { + // NO-OP + } + + @Override + public byte[] getHeader() { + return header; + } + + @Override + public ByteBuffer getPayload() { + return payload; + } + + @Override + public void expandPayload() { + payload = ByteBuffer.allocate(payload.capacity() * 2); + } + } +} diff --git a/java/org/apache/coyote/http2/LocalStrings.properties b/java/org/apache/coyote/http2/LocalStrings.properties new file mode 100644 index 0000000..5872e2c --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings.properties @@ -0,0 +1,184 @@ +# 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. + +abstractStream.setConnectionAllocationMade=Connection [{0}], Stream [{1}], connection allocation made changed from [{2}] to [{3}] +abstractStream.setConnectionAllocationRequested=Connection [{0}], Stream [{1}], connection allocation requested changed from [{2}] to [{3}] +abstractStream.windowSizeDec=Connection [{0}], Stream [{1}], reduce flow control window by [{2}] to [{3}] +abstractStream.windowSizeInc=Connection [{0}], Stream [{1}], increase flow control window by [{2}] to [{3}] +abstractStream.windowSizeTooBig=Connection [{0}], Stream [{1}], increased window size by [{2}] to [{3}] which exceeded permitted maximum + +connectionPrefaceParser.eos=Unexpected end of stream while reading opening client preface byte sequence. Only [{0}] bytes read. +connectionPrefaceParser.mismatch=An unexpected byte sequence was received at the start of the client preface [{0}] + +connectionSettings.debug=Connection [{0}], Endpoint [{1}], Parameter type [{2}] set to [{3}] +connectionSettings.enablePushInvalid=Connection [{0}], The requested value for enable push [{1}] is not one of the permitted values (zero or one) +connectionSettings.headerTableSizeLimit=Connection [{0}], Attempted to set a header table size of [{1}] but the limit is 16k +connectionSettings.maxFrameSizeInvalid=Connection [{0}], The requested maximum frame size of [{1}] is outside the permitted range of [{2}] to [{3}] +connectionSettings.noRfc7540PrioritiesInvalid=Connection [{0}], The requested no RFC 7540 priorities setting [{1}] was not zero or one +connectionSettings.unknown=Connection [{0}], An unknown setting with identifier [{1}] and value [{2}] was ignored +connectionSettings.windowSizeTooBig=Connection [{0}], The requested window size of [{1}] is bigger than the maximum permitted value of [{2}] + +frameType.checkPayloadSize=Payload size of [{0}] is not valid for frame type [{1}] +frameType.checkStream=Invalid frame type [{0}] + +hpack.integerEncodedOverTooManyOctets=HPACK variable length integer encoded over too many octets, max is [{0}] +hpack.invalidCharacter=The Unicode character [{0}] at code point [{1}] cannot be encoded as it is outside the permitted range of 0 to 255. + +hpackEncoder.encodeHeader=Encoding header [{0}] with value [{1}] + +hpackdecoder.addDynamic=Adding header to index [{0}] of dynamic table with name [{1}] and value [{2}] +hpackdecoder.clearDynamic=Emptying dynamic table +hpackdecoder.emitHeader=Emitting header with name [{0}] and value [{1}] +hpackdecoder.headerTableIndexInvalid=The header table index [{0}] is not valid as there are [{1}] static entries and [{2}] dynamic entries +hpackdecoder.maxMemorySizeExceeded=The header table size [{0}] exceeds the maximum size [{1}] +hpackdecoder.notImplemented=Not yet implemented +hpackdecoder.nullHeader=Null header at index [{0}] +hpackdecoder.tableSizeUpdateNotAtStart=Any table size update must be sent at the start of a header block +hpackdecoder.useDynamic=Using header from index [{0}] of dynamic table +hpackdecoder.useStatic=Using header from index [{0}] of static table +hpackdecoder.zeroNotValidHeaderTableIndex=Zero is not a valid header table index + +hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS=Huffman encoded value in HPACK headers did not end with EOS padding +hpackhuffman.stringLiteralEOS=Huffman encoded value in HPACK headers contained the EOS symbol +hpackhuffman.stringLiteralTooMuchPadding=More than 7 bits of EOS padding were provided at the end of an Huffman encoded string literal + +http2Parser.error=Connection [{0}], Stream [{1}], Frame type [{2}], Error +http2Parser.headerLimitCount=Connection [{0}], Stream [{1}], Too many headers +http2Parser.headerLimitSize=Connection [{0}], Stream [{1}], Total header size too big +http2Parser.headers.wrongFrameType=Connection [{0}], headers in progress for stream [{1}] but a frame of type [{2}] was received +http2Parser.headers.wrongStream=Connection [{0}], headers in progress for stream [{1}] but a frame for stream [{2}] was received +http2Parser.invalidBuffers=Reading should be done with two buffers +http2Parser.nonZeroPadding=Connection [{0}], Stream [{1}], Non-zero padding received +http2Parser.payloadTooBig=The payload is [{0}] bytes long but the maximum frame size is [{1}] +http2Parser.preface.invalid=Invalid connection preface presented +http2Parser.preface.io=Unable to read connection preface +http2Parser.processFrame=Connection [{0}], Stream [{1}], Frame type [{2}], Flags [{3}], Payload size [{4}] +http2Parser.processFrame.tooMuchPadding=Connection [{0}], Stream [{1}], The padding length [{2}] was too big for the payload [{3}] +http2Parser.processFrame.unexpectedType=Expected frame type [{0}] but received frame type [{1}] +http2Parser.processFrameContinuation.notExpected=Connection [{0}], Continuation frame received for stream [{1}] when no headers were in progress +http2Parser.processFrameData.lengths=Connection [{0}], Stream [{1}], Data length [{2}], Padding length [{3}] +http2Parser.processFrameData.window=Connection [{0}], Client sent more data than stream window allowed +http2Parser.processFrameHeaders.decodingDataLeft=Data left over after HPACK decoding - it should have been consumed +http2Parser.processFrameHeaders.decodingFailed=There was an error during the HPACK decoding of HTTP headers +http2Parser.processFrameHeaders.payload=Connection [{0}], Stream [{1}], Processing headers payload of size [{2}] +http2Parser.processFramePriorityUpdate.debug=Connection [{0}], Stream [{1}], Urgency [{2}], Incremental [{3}] +http2Parser.processFramePriorityUpdate.streamZero=Connection [{0}], Priority update frame received to prioritize stream zero +http2Parser.processFramePushPromise=Connection [{0}], Stream [{1}], Push promise frames should not be sent by the client +http2Parser.processFrameSettings.ackWithNonZeroPayload=Settings frame received with the ACK flag set and payload present +http2Parser.processFrameWindowUpdate.debug=Connection [{0}], Stream [{1}], Window size increment [{2}] +http2Parser.processFrameWindowUpdate.invalidIncrement=Connection [{0}], Stream [{1}], Window update frame received with an invalid increment size of [0]. +http2Parser.swallow.debug=Connection [{0}], Stream [{1}], Swallowed [{2}] bytes + +http2Protocol.jmxRegistration.fail=JMX registration for the HTTP/2 protocol failed + +pingManager.roundTripTime=Connection [{0}] Round trip time measured as [{1}]ns + +stream.clientCancel=Client reset the stream before the response was complete +stream.closed=Connection [{0}], Stream [{1}], Unable to write to stream once it has been closed +stream.header.case=Connection [{0}], Stream [{1}], HTTP header name [{2}] must be in lower case +stream.header.connection=Connection [{0}], Stream [{1}], HTTP header [{2}] is not permitted in an HTTP/2 request +stream.header.contentLength=Connection [{0}], Stream [{1}], The content length header value [{2}] does not agree with the size of the data received [{3}] +stream.header.debug=Connection [{0}], Stream [{1}], HTTP header [{2}], Value [{3}] +stream.header.duplicate=Connection [{0}], Stream [{1}], received multiple [{2}] headers +stream.header.empty=Connection [{0}], Stream [{1}], Invalid empty header name +stream.header.invalid=Connection [{0}], Stream [{1}], The header [{2}] contained invalid value [{3}] +stream.header.noPath=Connection [{0}], Stream [{1}], The [:path] pseudo header was empty +stream.header.required=Connection [{0}], Stream [{1}], One or more required headers was missing +stream.header.te=Connection [{0}], Stream [{1}], HTTP header [te] is not permitted to have the value [{2}] in an HTTP/2 request +stream.header.unexpectedPseudoHeader=Connection [{0}], Stream [{1}], Pseudo header [{2}] received after a regular header +stream.header.unknownPseudoHeader=Connection [{0}], Stream [{1}], Unknown pseudo header [{2}] received +stream.host.inconsistent=Connection [{0}], Stream [{1}], The header host header [{2}] is inconsistent with previously provided values for host [{3}] and/or port [{4}] +stream.inputBuffer.copy=Copying [{0}] bytes from inBuffer to outBuffer +stream.inputBuffer.dispatch=Data added to inBuffer when read interest is registered. Triggering a read dispatch +stream.inputBuffer.empty=The Stream input buffer is empty. Waiting for more data +stream.inputBuffer.readTimeout=Timeout waiting to read data from client +stream.inputBuffer.reset=Stream reset +stream.inputBuffer.signal=Data added to inBuffer when read thread is waiting. Signalling that thread to continue +stream.inputBuffer.swallowUnread=Swallowing [{0}] bytes previously read into input stream buffer +stream.notWritable=Connection [{0}], Stream [{1}], This stream is not writable +stream.outputBuffer.flush.debug=Connection [{0}], Stream [{1}], flushing output with buffer at position [{2}], writeInProgress [{3}] and closed [{4}] +stream.recycle=Connection [{0}], Stream [{1}] has been recycled +stream.reset.fail=Connection [{0}], Stream [{1}], Failed to reset stream +stream.reset.receive=Connection [{0}], Stream [{1}], Reset received due to [{2}] +stream.reset.send=Connection [{0}], Stream [{1}], Reset sent due to [{2}] +stream.trailerHeader.noEndOfStream=Connection [{0}], Stream [{1}], The trailer headers did not include the end of stream flag +stream.writeTimeout=Timeout waiting for client to increase flow control window to permit stream data to be written + +streamProcessor.cancel=Connection [{0}], Stream [{1}], The remaining request body is not required. +streamProcessor.error.connection=Connection [{0}], Stream [{1}], An error occurred during processing that was fatal to the connection +streamProcessor.error.stream=Connection [{0}], Stream [{1}], An error occurred during processing that was fatal to the stream +streamProcessor.flushBufferedWrite.entry=Connection [{0}], Stream [{1}], Flushing buffered writes +streamProcessor.service.error=Error during request processing + +streamStateMachine.debug.change=Connection [{0}], Stream [{1}], State changed from [{2}] to [{3}] +streamStateMachine.invalidFrame=Connection [{0}], Stream [{1}], State [{2}], Frame type [{3}] + +upgradeHandler.allocate.debug=Connection [{0}], Stream [{1}], allocated [{2}] bytes +upgradeHandler.allocate.left=Connection [{0}], Stream [{1}], [{2}] bytes unallocated - trying to allocate to children +upgradeHandler.connectionError=Connection error +upgradeHandler.enableRfc7450Priorities=Connection [{0}], RFC 7450 priorities may not be enabled after being disabled in the initial connection settings frame (see RFC 9218) +upgradeHandler.fallToDebug=\n\ +\ Note: further occurrences of HTTP/2 stream errors will be logged at DEBUG level. +upgradeHandler.goaway.debug=Connection [{0}], Goaway, Last stream [{1}], Error code [{2}], Debug data [{3}] +upgradeHandler.init=Connection [{0}], State [{1}] +upgradeHandler.invalidPreface=Connection [{0}], Invalid connection preface +upgradeHandler.ioerror=Connection [{0}] +upgradeHandler.noAllocation=Connection [{0}], Stream [{1}], Timeout waiting for allocation +upgradeHandler.noNewStreams=Connection [{0}], Stream [{1}], Stream ignored as no new streams are permitted on this connection +upgradeHandler.overheadChange=Connection [{0}], Stream [{1}], Frame type [{2}] resulted in new overhead count of [{3}] +upgradeHandler.pause.entry=Connection [{0}] Pausing +upgradeHandler.pingFailed=Connection [{0}] Failed to send ping to client +upgradeHandler.prefaceReceived=Connection [{0}], Connection preface received from client +upgradeHandler.pruneIncomplete=Connection [{0}], Stream [{1}], Failed to fully prune the connection because there are [{2}] too many active streams +upgradeHandler.pruneStart=Connection [{0}] Starting pruning of old streams. Limit is [{1}] and there are currently [{2}] streams. +upgradeHandler.pruned=Connection [{0}] Pruned completed stream [{1}] +upgradeHandler.releaseBacklog=Connection [{0}], Stream [{1}] released from backlog +upgradeHandler.reset.receive=Connection [{0}], Stream [{1}], Reset received due to [{2}] +upgradeHandler.rst.debug=Connection [{0}], Stream [{1}], Error [{2}], Message [{3}], RST (closing stream) +upgradeHandler.sendPrefaceFail=Connection [{0}], Failed to send preface to client +upgradeHandler.sendfile.reservation=Connection [{0}], Stream [{1}], Connection reservation [{2}], Stream reservation [{3}] prior to sendfile write +upgradeHandler.socketCloseFailed=Error closing socket +upgradeHandler.startRequestBodyFrame.result=Connection [{0}], Stream [{1}] startRequestBodyFrame returned [{2}] +upgradeHandler.stream.closed=Stream [{0}] has been closed for some time +upgradeHandler.stream.error=Connection [{0}], Stream [{1}] Closed due to error +upgradeHandler.stream.even=A new remote stream ID of [{0}] was requested but all remote streams must use odd identifiers +upgradeHandler.stream.notWritable=Connection [{0}], Stream [{1}], This stream is in state [{2}] and is not writable +upgradeHandler.stream.old=A new remote stream ID of [{0}] was requested but the most recent stream was [{1}] +upgradeHandler.tooManyRemoteStreams=The client attempted to use more than [{0}] active streams +upgradeHandler.tooMuchOverhead=Connection [{0}], Too much overhead so the connection will be closed +upgradeHandler.unexpectedAck=Connection [{0}], Stream [{1}], A settings acknowledgement was received when not expected +upgradeHandler.upgrade=Connection [{0}], HTTP/1.1 upgrade to stream [1] +upgradeHandler.upgrade.fail=Connection [{0}], HTTP/1.1 upgrade failed +upgradeHandler.upgradeDispatch.entry=Entry, Connection [{0}], SocketStatus [{1}] +upgradeHandler.upgradeDispatch.exit=Exit, Connection [{0}], SocketState [{1}] +upgradeHandler.windowSizeReservationInterrupted=Connection [{0}], Stream [{1}], reservation for [{2}] bytes +upgradeHandler.windowSizeTooBig=Connection [{0}], Stream [{1}], Window size too big +upgradeHandler.windowUpdateConnection=Connection [{0}], Sent window update to client increasing window by [{1}] bytes +upgradeHandler.windowUpdateStream=Connection [{0}], Stream [{1}], Sent window update to client increasing window by [{2}] bytes +upgradeHandler.writeBody=Connection [{0}], Stream [{1}], Data length [{2}], EndOfStream [{3}] +upgradeHandler.writeHeaders=Connection [{0}], Stream [{1}], Writing the headers, EndOfStream [{2}] +upgradeHandler.writePushHeaders=Connection [{0}], Stream [{1}], Pushed stream [{2}], EndOfStream [{3}] + +windowAllocationManager.dispatched=Connection [{0}], Stream [{1}], Dispatched +windowAllocationManager.notified=Connection [{0}], Stream [{1}], Notified +windowAllocationManager.notify=Connection [{0}], Stream [{1}], Waiting type [{2}], Notify type [{3}] +windowAllocationManager.waitFor.connection=Connection [{0}], Stream [{1}], Waiting for [{2}] bytes from connection flow control window (blocking) with timeout [{3}] +windowAllocationManager.waitFor.ise=Connection [{0}], Stream [{1}], Already waiting +windowAllocationManager.waitFor.stream=Connection [{0}], Stream [{1}], Waiting for Stream flow control window (blocking) with timeout [{2}] +windowAllocationManager.waitForNonBlocking.connection=Connection [{0}], Stream [{1}], Waiting for Connection flow control window (non-blocking) +windowAllocationManager.waitForNonBlocking.stream=Connection [{0}], Stream [{1}], Waiting for Stream flow control window (non-blocking) + +writeStateMachine.endWrite.ise=It is illegal to specify [{0}] for the new state once a write has completed +writeStateMachine.ise=It is illegal to call [{0}()] in state [{1}] diff --git a/java/org/apache/coyote/http2/LocalStrings_cs.properties b/java/org/apache/coyote/http2/LocalStrings_cs.properties new file mode 100644 index 0000000..b07e990 --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_cs.properties @@ -0,0 +1,39 @@ +# 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. + +abstractStream.windowSizeInc=Spojení [{0}], Stream [{1}], zvyÅ¡te tok kontrolního okna z [{2}] na [{3}] + +connectionPrefaceParser.mismatch=NeoÄekávaná sekvence bytů byla pÅ™ijata pÅ™i zahájení klientem [{0}] + +connectionSettings.debug=Spojení [{0}], typ parametru [{1}] nastaven na [{2}] +connectionSettings.headerTableSizeLimit=Spojení [{0}], Pokus o nastavení velikosti hlaviÄky tabulky na [{1}], limit je vÅ¡ak 16k +connectionSettings.maxFrameSizeInvalid=Spojení [{0}], požadovaná maximnální velikost rámce [{1}] je mimo povolený rozsah od [{2}] do [{3}] + +hpack.invalidCharacter=Unicode znak [{0}] na kódové znaÄce [{1}] nemůže být zakódován, protože je mimo povolený rozsah 0 až 255. + +http2Parser.headers.wrongStream=Spojení [{0}] obsahuje hlaviÄky pro stream [{1}], ale byl obdržen rámec pro stream [{2}] +http2Parser.processFrameData.window=Spojení [{0}], klient poslal více dat než stream povouje +http2Parser.processFrameHeaders.decodingDataLeft=Po dekódování HPACK zůstala data, která mÄ›la být zkonzumována +http2Parser.processFramePushPromise=Connection [{0}], Stream [{1}], Rámec pro Push nemá být zaslán klientem + +upgradeHandler.pingFailed=Spojení [{0}] pro odeslání příkazu ping na klienta selhalo +upgradeHandler.prefaceReceived=Spojení [{0}], přírava spojení pÅ™ijata od klienta +upgradeHandler.pruneIncomplete=Plné omezení spojení [{0}] selhalo, neboÅ¥ streamy byly aktivní / použité v prioritním stromu. Existuje [{2}] příliÅ¡ mnoho streamů +upgradeHandler.rst.debug=Spojení [{0}], Stream [{1}], Chyba [{2}], Zpráva [{3}], RST (zavírání streamu) +upgradeHandler.sendPrefaceFail=Spojení [{0}], selhalo odeslánízahájení klientovi +upgradeHandler.socketCloseFailed=Chyba zavírání socketu +upgradeHandler.stream.even=Bylo vyžádáno nové vzdálené stream ID [{0}], ale vÅ¡echny vzdálené streamy musí používat streams must obdobné identifikátory +upgradeHandler.upgrade=Spojení [{0}], HTTP/1.1 upgrade na stream [1] +upgradeHandler.writeHeaders=Spojení [{0}], Stream [{1}] diff --git a/java/org/apache/coyote/http2/LocalStrings_de.properties b/java/org/apache/coyote/http2/LocalStrings_de.properties new file mode 100644 index 0000000..7491438 --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_de.properties @@ -0,0 +1,43 @@ +# 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. + +abstractStream.windowSizeInc=Verbindung [{0}], Stream [{1}], erhöhe Flow Contrrol Window um [{2}] auf [{3}] + +connectionPrefaceParser.mismatch=Es wurde eine unerwartete Byte Sequenz beim Start der Client Preface Phase [{0}] empfangen + +connectionSettings.debug=Verbindung [{0}], Parameter typ [{1}] gesetzt auf [{2}] +connectionSettings.headerTableSizeLimit=Verbindung [{0}], versuchte die Kopfzeilentabellengröße auf [{1}] zu setzen aber die Grenze liegt bei 16k +connectionSettings.maxFrameSizeInvalid=Verbindung [{0}], die angeforderte maximale Framegröße von [{1}] ist außerhalb des erlaubten Bereichs von [{2}] bis [{3}] + +hpack.invalidCharacter=Das Unicode Zeichen [{0}] an Code Punkt [{1}] kann nicht kodiert werden, da es außerhalb des erlaubten Bereiches von 0 bis 255 ist. + +hpackdecoder.maxMemorySizeExceeded=Die header table Größe [{0}] überschreitet die maximale Größe von [{1}] +hpackdecoder.nullHeader=Null header bei Index [{0}] + +http2Parser.headerLimitSize=Verbindung [{0}], Stream [{1}], Gesamt-Header-Größe zu groß +http2Parser.processFrameData.window=Verbindung [{0}], Client hat mehr Daten gesendet als das Stream-Fenster zulässt +http2Parser.processFrameHeaders.decodingDataLeft=Nach der HPACK-Dekodierung sind noch Daten übrig - die hätten verarbeitet sein sollen + +stream.header.unknownPseudoHeader=Verbindung [{0}], Stream [{1}], Unbekannten Pseudo-Header [{2}] empfangen + +streamProcessor.service.error=Fehler bei der Anfrageverarbeitung + +upgradeHandler.ioerror=Verbindung [{0}] +upgradeHandler.pingFailed=Verbindung [{0}] – Das Senden eines ''ping'' zum Klienten schlug fehl. +upgradeHandler.socketCloseFailed=Fehler beim Schließen des Sockets. +upgradeHandler.upgrade=Verbindung [{0}], HTTP/1.1 Upgrade auf Stream [1] +upgradeHandler.upgrade.fail=Verbindung [{0}], HTTP/1.1 upgrade fehlgeschlagen +upgradeHandler.windowSizeTooBig=Verbindung [{0}], Stream [{1}], Fenster-Größe zu groß +upgradeHandler.writeHeaders=Verbindung [{0}], Stream [{1}], schreibe Header, EndOfStream [{2}] diff --git a/java/org/apache/coyote/http2/LocalStrings_es.properties b/java/org/apache/coyote/http2/LocalStrings_es.properties new file mode 100644 index 0000000..513511c --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_es.properties @@ -0,0 +1,57 @@ +# 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. + +abstractStream.windowSizeInc=Conexión [{0}], Flujo [{1}], aumente el control de flujo de la ventana en [{2}] para [{3}]\n + +connectionPrefaceParser.mismatch=Una sequencia de byte no esperada fue recibida al inicio del prefacio de cliente [{0}] + +connectionSettings.debug=Conexión [{0}], Parámetro tipo [{1}] fijado a [{2}] +connectionSettings.headerTableSizeLimit=La conexión [{0}], intentó fijar un tamaño de cabecera de [{1}] pero el límite es 16k\n +connectionSettings.maxFrameSizeInvalid=Conexión [{0}], El tamaño de cuadro máximo solicitado de [{1}] esta fuera del rango permitido de [{2}] hasta [{3}]\n +connectionSettings.unknown=Conexión [{0}], Un parámetro desconocido con identificador [{1}] y valor [{2}] fue ignorado\n + +hpack.invalidCharacter=El carácter Unicode [{0}] en el punto del código [{1}] no puede ser codificado al estar fuera del rango permitido de 0 a 255. + +http2Parser.headerLimitSize=Conexión [{0}], Flujo [{1}], Tamaño total de la cabecera my grade +http2Parser.headers.wrongStream=La conexión [{0}], tiene cabeceras en progreso para stream [{1}] pero un frame para stream [{2}] fue recibido +http2Parser.preface.invalid=Se presentó un prefacio de conexión inválido +http2Parser.processFrameData.window=Conexión [{0}], El cliente mandó más datos que los permitidos por el flujo de ventana. +http2Parser.processFrameHeaders.decodingDataLeft=Datos sobrantes luego de decodificar HPACK - Estos datos se deberían haber consumido +http2Parser.processFramePushPromise=Conexión [{0}], Flujo [{1}], Push promise frames no deben ser enviadas por el cliente + +stream.closed=Conexión [{0}], Flujo [{1}], Imposible escribir en el flujo una vez que ha sido crear +stream.header.debug=Conexión [{0}], Flujo [{1}], cabecera HTTP [{2}], Valor [{3}]\n +stream.header.noPath=Conexión [{0}], Flujo [{1}], El [:path] de la seudo cabecera estaba vacía +stream.header.unknownPseudoHeader=Conexión [{0}], Flujo [{1}], Se recibió una Pseudo cabecera desconocida [{2}] +stream.inputBuffer.reset=Reinicio de flujo +stream.inputBuffer.signal=Se adicionaron datos al inBuffer cuando el hilo esta esperando. Señalizando al hilo que a continuar + +streamProcessor.error.connection=Conexión [{0}], Flujo [{1}], Ha ocurrido un error el procesamiento que fue fatal para la conexión + +streamStateMachine.debug.change=Conexión [{0}], Flujo [{1}], Estado cambió de [{2}] a [{3}] + +upgradeHandler.allocate.left=Conexión [{0}], Flujo [{1}], [{2}] bytes no asignados - tratando de asignar en el hijo +upgradeHandler.ioerror=Conexión [{0}] +upgradeHandler.pingFailed=Conexión [{0}] falló al hacer ping al cliente +upgradeHandler.prefaceReceived=Conexión [{0}], Pre face de conexión recibida del cliente\n +upgradeHandler.pruneIncomplete=La conexión [{0}] Falló al podar completamente la conexión porque existen flujos activos / usados en el árbol de priorida. Existen [{2}] muchos flujos +upgradeHandler.rst.debug=Conexión [{0}], Flujo [{1}], Error [{2}], Mensaje [{3}], RST (cerrando flujo) +upgradeHandler.sendPrefaceFail=La conexión [{0}], Falló al enviar el prefacio al cliente\n +upgradeHandler.socketCloseFailed=Error cerrando el socket +upgradeHandler.stream.even=Un nuevo flujo remoto con ID [{0}] fue solicitado, pero todos los flujos remotos deben usar identificadores raros +upgradeHandler.upgrade=La conexión [{0}], HTTP/1.1 se actualizó al flujo [1]\n +upgradeHandler.upgradeDispatch.entry=Entrada, Conexión [{0}], SocketStatus [{1}]\n +upgradeHandler.upgradeDispatch.exit=Salida, Conexión [{0}], Estado de Socket [{1}] +upgradeHandler.writeHeaders=Conexión [{0}], Flujo [{1}] diff --git a/java/org/apache/coyote/http2/LocalStrings_fr.properties b/java/org/apache/coyote/http2/LocalStrings_fr.properties new file mode 100644 index 0000000..e510bbb --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_fr.properties @@ -0,0 +1,184 @@ +# 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. + +abstractStream.setConnectionAllocationMade=Connection [{0}], Stream [{1}], l''allocation pour la connection effectuée a changé de [{2}] vers [{3}] +abstractStream.setConnectionAllocationRequested=Connection [{0}], Stream [{1}], l''allocation pour la connection demandée a changé de [{2}] vers [{3}] +abstractStream.windowSizeDec=Connection [{0}], Flux [{1}], réduction de la fenêtre de contrôle de flux de [{2}] à [{3}] +abstractStream.windowSizeInc=Connection [{0}], Stream [{1}], augmentez la taille de la fenêtre de contrôle de flux de [{2}] à [{3}] +abstractStream.windowSizeTooBig=Connection [{0}], Flux [{1}], L''augmentation de la taille de la fenêtre de [{2}] à [{3}] a excédé le maximum autorisé + +connectionPrefaceParser.eos=Fin de flux inattendue lors de la lecture de la préface du client, seuls [{0}] octets ont été lus +connectionPrefaceParser.mismatch=Une séquence inattendue d''octets a été recue au début de la préface client [{0}] + +connectionSettings.debug=Connection [{0}], Paramètre type [{1}] mis à [{2}] +connectionSettings.enablePushInvalid=Connection [{0}], La valeur demandée pour activer le push [{1}] n''est pas une de celles permises (zéro ou un) +connectionSettings.headerTableSizeLimit=La Connection [{0}] a essayé de configurer une taille de [{1}] pour la table des en-têtes (headers), mais la limite est 16k +connectionSettings.maxFrameSizeInvalid=Connection [{0}], la taille maximum de trame demandée [{1}] est en-dehors des limites permises [{2}] - [{3}] +connectionSettings.noRfc7540PrioritiesInvalid=Connection [{0}], Le paramètre "no RFC 7540 priorities" [{1}] n''était pas zéro ou un +connectionSettings.unknown=Connection [{0}], Un paramètre inconnu avec l''identifiant [{1}] et la valeur [{2}] a été ignoré +connectionSettings.windowSizeTooBig=Connection [{0}], La taille de fenêtre demandée [{1}] est plus grande que la valeur maximale autorisée [{2}] + +frameType.checkPayloadSize=La taille de données [{0}] n''est pas valide pour une trame de type [{1}] +frameType.checkStream=Type de trame invalide [{0}] + +hpack.integerEncodedOverTooManyOctets=Un entier de taille variable de HPACK a été encodé sur trop d''octets, le maximum est de [{0}] +hpack.invalidCharacter=Le caractère Unicode [{0}] ayant le code point [{1}] ne peut être encodé, parce qu''il est en-dehors de l''éventail permis 0-255. + +hpackEncoder.encodeHeader=Encodage de l''en-tête [{0}] avec la valeur [{1}] + +hpackdecoder.addDynamic=Ajout de l''en-tête à l''index [{0}] de la table dynamique avec le nom [{1}] et la valeur [{2}] +hpackdecoder.clearDynamic=Vidage de la table dynamique +hpackdecoder.emitHeader=Envoi de l''en-tête avec le nom [{0}] et la valeur [{1}] +hpackdecoder.headerTableIndexInvalid=L''index [{0}] dans la table des en-têtes n''est pas valide car il y a [{1}] en-têtes statiques et [{2}] en-têtes dynamiques +hpackdecoder.maxMemorySizeExceeded=La taille de la table des en-têtes [{0}] dépasse la taille maximale [{1}] +hpackdecoder.notImplemented=Pas encore implémenté +hpackdecoder.nullHeader=L''en-tête à l''index [{0}] est nul +hpackdecoder.tableSizeUpdateNotAtStart=Toute mise à jour de la taille de la table doit être faite avant le début d'un bloc d'en-têtes +hpackdecoder.useDynamic=Utilisation de l''en-tête de l''index [{0}] de la table dynamique +hpackdecoder.useStatic=Utilisation de l''en-tête de l''index [{0}] de la table statique +hpackdecoder.zeroNotValidHeaderTableIndex=Zéro n'est pas un index valide dans la table des en-têtes + +hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS=La valeur encodée en Huffman dans les en-têtes HPACK n'avait pas de données tampon d'EOS +hpackhuffman.stringLiteralEOS=La valeur encodée en Huffman dans les en-têtes HPACK contenait le symbole d'EOS +hpackhuffman.stringLiteralTooMuchPadding=Plus de 7 bits de données tampon de fin de flux ont été fournis à la fin d'une chaîne encodée avec Huffman + +http2Parser.error=Connection [{0}], Flux [{1}], Type de trame [{2}], Erreur +http2Parser.headerLimitCount=Connection [{0}], Slux [{1}], Trop d''en-têtes +http2Parser.headerLimitSize=Connection [{0}], Flux [{1}], La taille totale des en-têtes est trop grosse +http2Parser.headers.wrongFrameType=Connection [{0}], Le traitement des en-têtes est en cours pour le flux [{1}] mais une trame de type [{2}] a été reçue +http2Parser.headers.wrongStream=Connection [{0}], en têtes en cours pour le flux [{1}] mais une trame du flux [{2}] a été reçue +http2Parser.invalidBuffers=La lecture doit être faite avec deux buffers +http2Parser.nonZeroPadding=Connection [{0}], Stream [{1}], rembourrage (padding) non-zéro recu +http2Parser.payloadTooBig=La taille des données est de [{0}] octets mais la taille maximale de la trame est de [{1}] +http2Parser.preface.invalid=Une préface de connection invalide a été reçue +http2Parser.preface.io=Impossible de lire la préface de la connection +http2Parser.processFrame=Connection [{0}], Flux [{1}], Type de trame [{2}], Drapeaux [{3}], Taille des données [{4}] +http2Parser.processFrame.tooMuchPadding=Connection [{0}], Flux [{1}], La taille [{2}] des données tampon est trop grosse pour la taille de données [{3}] +http2Parser.processFrame.unexpectedType=Attendu une trame de type [{0}] mais reçu une trame de type [{1}] +http2Parser.processFrameContinuation.notExpected=Connection [{0}], La trame de continuation a été reçue pour le flux [{1}] alors qu''aucun trainement d''en-têtes n''était en cours +http2Parser.processFrameData.lengths=Connection [{0}], Flux [{1}], Taille des données, [{2}], Taille des données tampon [{3}] +http2Parser.processFrameData.window=Connection [{0}], le client a envoyé plus de données que la "stream window" ne le permet +http2Parser.processFrameHeaders.decodingDataLeft=Des données restent après le décodage de HPACK, elles auraient dû être consommées +http2Parser.processFrameHeaders.decodingFailed=Une erreur de décodage HPACK des en-têtes HTTP s'est produite +http2Parser.processFrameHeaders.payload=Connection [{0}], Flux [{1}], Traitement des en-têtes avec une taille de données de [{2}] +http2Parser.processFramePriorityUpdate.debug=Connection [{0}], Stream [{1}], Urgency [{2}], Incremental [{3}] +http2Parser.processFramePriorityUpdate.streamZero=Connection [{0}], La trame de mise à jour de priorité a été recue pour le flux zéro +http2Parser.processFramePushPromise=Connexion [{0}], Flux (Stream) [{1}], les trames de promesse d''envoi ("Push promise frames") ne doivent pas être envoyées par le client. +http2Parser.processFrameSettings.ackWithNonZeroPayload=La trame de paramètres a été reçue avec un indicateur ACK activé et des données présentes +http2Parser.processFrameWindowUpdate.debug=Connection [{0}], Flux [{1}], Incrémentation de [{2}] de la taille de fenêtre +http2Parser.processFrameWindowUpdate.invalidIncrement=La trame de mise à jour de la fenêtre a été reçue avec un incrément invalide [0] +http2Parser.swallow.debug=Connection [{0}], Flux [{1}], Avalé [{2}] octets + +http2Protocol.jmxRegistration.fail=L'enregistrement dans JMX du protocole HTTP/2 a échoué + +pingManager.roundTripTime=Connection [{0}] Le temps d''aller retour est de [{1}]ns + +stream.clientCancel=Le client a réinitialisé la stream avant que la réponse ne soit complète +stream.closed=Connection [{0}], Flux [{1}], Impossible d''écrire sur un flux après sa fermeture +stream.header.case=Connection [{0}], Flux [{1}], Le nom d''en-tête HTTP [{2}] doit être en miniscules +stream.header.connection=Connection [{0}], Flux [{1}], L''en-tête HTTP [{2}] n''est pas autorisé dans une requête HTTP/2 +stream.header.contentLength=Connection [{0}], Flux [{1}], La valeur de l''en-tête content-length [{2}] ne correspond pas à la taille des données reçue [{3}] +stream.header.debug=Connection [{0}], Flux [{1}], en-tête HTTP [{2}], valeur [{3}] +stream.header.duplicate=Connection [{0}], Flux [{1}], Reçu plusieurs en-têtes [{2}] +stream.header.empty=Connection [{0}], Flux [{1}], Le nom d''en-tête nul est invalide +stream.header.invalid=Connection [{0}], Flux [{1}], L''en-tête[{2}] contenait la valeur invalide [{3}] +stream.header.noPath=Connection [{0}], flux [{1}], Le [:path] pseudo en-tête est vide +stream.header.required=Connection [{0}], Flux [{1}], Un ou plusieurs en-têtes nécessaires sont manquants +stream.header.te=Connection [{0}], Flux [{1}], L''en-tête HTTP [te] n''est pas autorisé avec la valeur [{2}] dans une requête HTTP/2 +stream.header.unexpectedPseudoHeader=Connection [{0}], Flux [{1}], Le pseudo en-tête [{2}] a été reçu après un en-tête normal +stream.header.unknownPseudoHeader=Connection [{0}], Flux [{1}], Un pseudo en-tête inconnu [{2}] a été reçu +stream.host.inconsistent=Connection [{0}], Stream [{1}], L''en tête hôte [{2}] est inconsistant avec les valeurs fournies précédemment pour l''hôte [{3}] et/ou le port [{4}] +stream.inputBuffer.copy=Copide de [{0}] octets depuis inBuffer vers outBuffer +stream.inputBuffer.dispatch=Des données on été ajoutées dans inBuffer alors que la lecture est surveillée, envoi d'un évènement de lecture +stream.inputBuffer.empty=Le tampon d'entrée du flux est vide, attente de données +stream.inputBuffer.readTimeout=Délai d'attente maximum dépassé pendant la lecture des données du client +stream.inputBuffer.reset=Flux réinitialisé +stream.inputBuffer.signal=Des données ont été ajoutées dans inBuffer alors que le thread de lecture attend, cela lui sera signalé +stream.inputBuffer.swallowUnread=[{0}] bytes qui ont été auparavant lu dans le tampon d''entrée ont été avalés +stream.notWritable=Connection [{0}], Flux [{1}], Impossible d''écrire sur ce flux +stream.outputBuffer.flush.debug=Connection [{0}], Flux [{1}], envoi des données mises en tampon depuis la position [{2}], writeInProgress [{3}] et closed [{4}] +stream.recycle=Connection [{0}], Stream [{1}] a été recyclée +stream.reset.fail=Connection [{0}], Flux [{1}], Echec de réinitialisation du flux +stream.reset.receive=Connection [{0}], Flux [{1}], Réinitialisation reçue à cause de [{2}] +stream.reset.send=Connection [{0}], Flux [{1}], Réinitialisation envoyée à cause de [{2}] +stream.trailerHeader.noEndOfStream=Connection [{0}], Flux [{1}], Les en-têtes de fin n''incluent pas l''indicateur de fin de flux +stream.writeTimeout=Temps d'attente maximum du client dépassé pour augmenter la fenêtre de contrôle de flux pour permettre l'écriture de données + +streamProcessor.cancel=Connection [{0}], Flux [{1}], Le reste du corps de la requête n''est pas nécessaire +streamProcessor.error.connection=Connection [{0}], Stream [{1}], Une erreur s''est produite dans le traitement, fatale pour la connection +streamProcessor.error.stream=Connection [{0}], Flux [{1}], Une erreur d''est produite durant le traitement qui a été fatale au flux +streamProcessor.flushBufferedWrite.entry=Connection [{0}], Flux [{1}], Envoi des écritures mises en tampon +streamProcessor.service.error=Erreur durant le traitement de la requête + +streamStateMachine.debug.change=Connection [{0}], Flux [{1}], L’état a changé de [{2}] vers [{3}] +streamStateMachine.invalidFrame=Connection [{0}], Flux [{1}], Etat [{2}], Type de trame [{3}] + +upgradeHandler.allocate.debug=Connection [{0}], Flux [{1}], [{2}] octets alloués +upgradeHandler.allocate.left=Connection [{0}], Flux [{1}], [{2}] octets désalloués, essai d''allocation aux enfants +upgradeHandler.connectionError=Erreur de la connection +upgradeHandler.enableRfc7450Priorities=Connection [{0}], les priorités RFC 7450 ne doivent pas être activées après avoir été désactivées dans la trame initiale des paramètres de connection (voir la RFC 9218) +upgradeHandler.fallToDebug=\n\ +\ Note: les occurrences suivantes d'erreurs de stream HTTP/2 seront enregistrées au niveau DEBUG. +upgradeHandler.goaway.debug=Connection [{0}], Goaway, Dernier flux [{1}], Code d''erreur [{2}], Données de débogage [{3}] +upgradeHandler.init=Connection [{0}], Etat [{1}] +upgradeHandler.invalidPreface=Connection [{0}], Préface de connection invalide +upgradeHandler.ioerror=Connection [{0}] +upgradeHandler.noAllocation=Connection [{0}], Flux [{1}], Temps d''attente maximum dépassé lors de l''allocation +upgradeHandler.noNewStreams=Connection [{0}], Flux [{1}], Flux ignoré car aucun nouveau flux n''est autorisé sur cette connection +upgradeHandler.overheadChange=Connection [{0}], Stream [{1}], Frame type [{2}] donne un nouveau temps ajouté de [{3}] +upgradeHandler.pause.entry=Connection [{0}] mise en pause +upgradeHandler.pingFailed=La connection [{0}] n''a pas réussi à envoyer un ping au client +upgradeHandler.prefaceReceived=Connection [{0}], préface de la connection recue du client +upgradeHandler.pruneIncomplete=Connexion [{0}], Flux [{1}], Erreur lors de l''élimination complète de la connexion parce que des flux sont encore actifs / utilisés dans l''arbre de priorité, il y a [{2}] flux en trop +upgradeHandler.pruneStart=Connection [{0}] Début de l''élimination des anciens flux, la limite est de [{1}] et il y a actuellement [{2}] flux +upgradeHandler.pruned=Connection [{0}] Elimination du flux terminé [{1}] +upgradeHandler.releaseBacklog=Connection [{0}], Flux [{1}] enlevée de la file d''attente +upgradeHandler.reset.receive=Connection [{0}], Stream [{1}], Reset a été reçu à cause de [{2}] +upgradeHandler.rst.debug=Connexion [{0}], Flux [{1}], Erreur [{2}], Message [{3}], RST (fermeture du flux) +upgradeHandler.sendPrefaceFail=Connexion [{0}], échec d''envoi de la préface au client +upgradeHandler.sendfile.reservation=Connection [{0}], Stream [{1}], Connection reservation [{2}], Stream reservation [{3}] avant l''écriture avec sendfile +upgradeHandler.socketCloseFailed=Echec de la fermeture du socket +upgradeHandler.startRequestBodyFrame.result=Connection [{0}], Stream [{1}] startRequestBodyFrame a renvoyé [{2}] +upgradeHandler.stream.closed=Le flux [{0}] a déjà été fermé auparavant +upgradeHandler.stream.error=Connection [{0}], Stream [{1}] Fermé à cause d''une erreur +upgradeHandler.stream.even=Un nouvel ID de flux distant (remote stream) [{0}] a été requis, mais tous les flux distants doivent utiliser ID impairs +upgradeHandler.stream.notWritable=Connection [{0}], Flux [{1}], Impossible d''écrire sur ce flux +upgradeHandler.stream.old=Un nouveau flux distant avec l''ID [{0}] a été demandé mais le flux le plus récent est [{1}] +upgradeHandler.tooManyRemoteStreams=Le client a essayé d''utiliser plus de [{0}] flux actifs +upgradeHandler.tooMuchOverhead=Connection [{0}], Le traitement est trop coûteux donc la connection sera fermée +upgradeHandler.unexpectedAck=Connection [{0}], Flux [{1}], Une notification de réception de paramètres a été reçue alors qu''aucune n''était attendue +upgradeHandler.upgrade=Connexion [{0}], HTTP/1.1 transformée en flux [1] +upgradeHandler.upgrade.fail=Connection [{0}], Echec de l''upgrade de HTTP/1.1 +upgradeHandler.upgradeDispatch.entry=Entrée, Connection [{0}], SocketStatus [{1}] +upgradeHandler.upgradeDispatch.exit=Sortie, Connection [{0}], SocketState [{1}] +upgradeHandler.windowSizeReservationInterrupted=Connection [{0}], Flux [{1}], réservé [{2}] octets +upgradeHandler.windowSizeTooBig=Connection [{0}], Flux [{1}], La taille de la fenêtre est trop grosse +upgradeHandler.windowUpdateConnection=Connection [{0}], envoi de la mise à jour de la fenêtre augmentant celle ci de [{1}] octets +upgradeHandler.windowUpdateStream=Connection [{0}], Stream [{1}], envoi de la mise à jour de la fenêtre augmentant celle ci de [{2}] octets +upgradeHandler.writeBody=Connection [{0}], Flux [{1}], Taille des données [{2}] +upgradeHandler.writeHeaders=Connection [{0}], Stream [{1}] +upgradeHandler.writePushHeaders=Connection [{0}], Flux [{1}], Flux de push [{2}], EndOfStream [{3}] + +windowAllocationManager.dispatched=Connection [{0}], Flux [{1}], Envoyé +windowAllocationManager.notified=Connection [{0}], Flux [{1}], Notifié +windowAllocationManager.notify=Connection [{0}], Flux [{1}], Attente de type [{2}], Notification de type [{3}] +windowAllocationManager.waitFor.connection=Connection [{0}], Flux [{1}], Attente d''une fenêtre de contrôle de flux de Connection (bloquante) avec une délai maximum d''attente de [{2}] +windowAllocationManager.waitFor.ise=Connection [{0}], Flux [{1}], Déjà en train d''attendre +windowAllocationManager.waitFor.stream=Connection [{0}], Flux [{1}], Attente d''une fenêtre de contrôle de flux de Flux (bloquante) avec une délai maximum d''attente de [{2}] +windowAllocationManager.waitForNonBlocking.connection=Connection [{0}], Flux [{1}], Attente d''une fenêtre de contrôle de flux de Connection (non bloquante) +windowAllocationManager.waitForNonBlocking.stream=Connection [{0}], Flux [{1}], Attente d''une fenêtre de contrôle de flux de Flux (non bloquante) + +writeStateMachine.endWrite.ise=il est illégal de spécifier [{0}] pour le nouvel état dès lors qu''une écriture s''est terminée +writeStateMachine.ise=Il est illégal d''appeler [{0}()] dans l''état [{1}] diff --git a/java/org/apache/coyote/http2/LocalStrings_ja.properties b/java/org/apache/coyote/http2/LocalStrings_ja.properties new file mode 100644 index 0000000..fa98e53 --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_ja.properties @@ -0,0 +1,184 @@ +# 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. + +abstractStream.setConnectionAllocationMade=接続 [{0}] ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}] ã€ä½œæˆã•ã‚ŒãŸæŽ¥ç¶šå‰²ã‚Šå½“ã¦ãŒ [{2}] ã‹ã‚‰ [{3}] ã«å¤‰æ›´ã•ã‚Œã¾ã—㟠+abstractStream.setConnectionAllocationRequested=接続 [{0}] ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}] ã€è¦æ±‚ã•ã‚ŒãŸæŽ¥ç¶šå‰²ã‚Šå½“ã¦ãŒ [{2}] ã‹ã‚‰ [{3}] ã«å¤‰æ›´ã•ã‚Œã¾ã—㟠+abstractStream.windowSizeDec=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ•ãƒ­ãƒ¼åˆ¶å¾¡ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã‚’ [{2}] ãšã¤ [{3}] ã«ç¸®å° +abstractStream.windowSizeInc=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ•ãƒ­ãƒ¼åˆ¶å¾¡ã‚¦ã‚¤ãƒ³ãƒ‰ã‚¦ã‚’ [{2}] 増やã—㦠[{3}] ã«ã—ã¾ã™ +abstractStream.windowSizeTooBig=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã‚µã‚¤ã‚ºãŒ [{2}] 増加ã—㦠[{3}] ã«ãªã£ãŸãŸã‚許容最大値を超ãˆã¾ã—㟠+ +connectionPrefaceParser.eos=オープニングクライアントPrefaceã®ãƒã‚¤ãƒˆã‚·ãƒ¼ã‚±ãƒ³ã‚¹ã‚’読ã¿å–ã£ã¦ã„ã‚‹ã¨ãã«äºˆæœŸã—ãªã„ストリームã®çµ‚ã‚ã‚ŠãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ [{0}]ãƒã‚¤ãƒˆã ã‘ãŒèª­ã¿è¾¼ã¾ã‚Œã¾ã™ã€‚ +connectionPrefaceParser.mismatch=クライアントプリフェイス [{0}] ã®å…ˆé ­ã«æœªçŸ¥ã®ãƒã‚¤ãƒˆåˆ—ã‚’å—ä¿¡ã—ã¾ã—㟠+ +connectionSettings.debug=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ãƒ‘ラメータ [{1}] ã« [{2}] を設定ã—ã¾ã—ãŸã€‚ +connectionSettings.enablePushInvalid=コãƒã‚¯ã‚·ãƒ§ãƒ³[{0}]ã€æœ‰åŠ¹ãƒ—ッシュ[{1}]ã«ãƒªã‚¯ã‚¨ã‚¹ãƒˆã•ã‚ŒãŸå€¤ãŒè¨±å®¹å€¤ï¼ˆ0ã¾ãŸã¯1)ã®ã„ãšã‚Œã§ã‚‚ã‚ã‚Šã¾ã›ã‚“。 +connectionSettings.headerTableSizeLimit=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ãƒ˜ãƒƒãƒ€ãƒ¼ãƒ†ãƒ¼ãƒ–ルサイズ㫠[{1}] を指定ã•ã‚Œã¾ã—ãŸãŒä¸Šé™ã¯ 16k ã§ã™ã€‚ +connectionSettings.maxFrameSizeInvalid=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€è¦æ±‚ã•ã‚ŒãŸæœ€å¤§ãƒ•ãƒ¬ãƒ¼ãƒ ã‚µã‚¤ã‚º [{1}] ã¯å¯èƒ½ãªç¯„囲㮠[{2}] ã‹ã‚‰ [{3}] を超ãˆã¦ã„ã¾ã™ã€‚ +connectionSettings.noRfc7540PrioritiesInvalid=接続 [{0}] ã§ã¯ã€è¦æ±‚ã•ã‚ŒãŸ RFC 7540 優先度設定 [{1}] ㌠0 ã§ã‚‚ 1 ã§ã‚‚ã‚ã‚Šã¾ã›ã‚“ã§ã—㟠+connectionSettings.unknown=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€æœªçŸ¥ã®è¨­å®šå [{1}] ã®å€¤ [{2}] を無視ã—ã¾ã—ãŸã€‚ +connectionSettings.windowSizeTooBig=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€è¦æ±‚ã•ã‚ŒãŸã‚¦ã‚¤ãƒ³ãƒ‰ã‚¦ã‚µã‚¤ã‚º [{1}] ã¯ä¸Šé™å€¤ [{2}] を越ãˆã¦ã„ã¾ã™ã€‚ + +frameType.checkPayloadSize=[{0}] ã®ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰ã‚µã‚¤ã‚ºãŒãƒ•ãƒ¬ãƒ¼ãƒ ã‚¿ã‚¤ãƒ— [{1}] ã«ç„¡åŠ¹ã§ã™ +frameType.checkStream=無効ãªãƒ•ãƒ¬ãƒ¼ãƒ ã‚¿ã‚¤ãƒ— [{0}] + +hpack.integerEncodedOverTooManyOctets=エンコードã•ã‚ŒãŸHPACKå¯å¤‰é•·æ•´æ•°ã¯å¤šãã®ã‚ªã‚¯ãƒ†ãƒƒãƒˆã‚’超éŽã€‚最大値㯠[{0}] +hpack.invalidCharacter=コードãƒã‚¤ãƒ³ãƒˆ [{1}] ã®ãƒ¦ãƒ‹ã‚³ãƒ¼ãƒ‰æ–‡å­— [{0}] ã¯æœ‰åŠ¹ç¯„囲 0 ã‹ã‚‰ 255 ã®ç¯„囲外ã®ãŸã‚ã€ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã§ãã¾ã›ã‚“。 + +hpackEncoder.encodeHeader=ヘッダー[{0}]を値[{1}]ã§ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã—ã¦ã„ã¾ã™ + +hpackdecoder.addDynamic=動的テーブルã®ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ [{0}] ã«ã€åå‰ [{1}] ã¨å€¤ [{2}] ã®ãƒ˜ãƒƒãƒ€ãƒ¼ã‚’追加ã—ã¾ã™ +hpackdecoder.clearDynamic=動的テーブルを空ã«ã—ã¾ã™ +hpackdecoder.emitHeader=åå‰ãŒ [{0}] ã§å€¤ãŒ [{1}] ã®ãƒ˜ãƒƒãƒ€ãƒ¼ã‚’出力ã—ã¾ã™ã€‚ +hpackdecoder.headerTableIndexInvalid=[{1}] é™çš„エントリ㨠[{2}] 動的エントリãŒå­˜åœ¨ã™ã‚‹ãŸã‚ã€ãƒ˜ãƒƒãƒ€ãƒ¼ãƒ†ãƒ¼ãƒ–ルインデックス [{0}] ã¯ç„¡åŠ¹ã§ã™ +hpackdecoder.maxMemorySizeExceeded=ヘッダテーブルサイズ [{0}] ã¯æœ€å¤§ã‚µã‚¤ã‚º [{1}] を超ãˆã¦ã„ã¾ã™ +hpackdecoder.notImplemented=ã¾ã å®Ÿè£…ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +hpackdecoder.nullHeader=インデックス [{0}] ã®ãƒ˜ãƒƒãƒ€ã¯ Null ã§ã™ +hpackdecoder.tableSizeUpdateNotAtStart=ã™ã¹ã¦ã®ãƒ†ãƒ¼ãƒ–ルサイズã®æ›´æ–°ã¯ãƒ˜ãƒƒãƒ€ãƒ¼ãƒ–ロックã®å…ˆé ­ã«é€ä¿¡ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +hpackdecoder.useDynamic=動的テーブルã®ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ [{0}] ã‹ã‚‰ãƒ˜ãƒƒãƒ€ãƒ¼ã‚’使用ã—ã¾ã™ +hpackdecoder.useStatic=é™çš„テーブルã®ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ [{0}] ã‹ã‚‰ãƒ˜ãƒƒãƒ€ãƒ¼ã‚’使用ã—ã¾ã™ +hpackdecoder.zeroNotValidHeaderTableIndex=ゼロã¯æœ‰åŠ¹ãªãƒ˜ãƒƒãƒ€ãƒ¼ãƒ†ãƒ¼ãƒ–ルインデックスã§ã¯ã‚ã‚Šã¾ã›ã‚“。 + +hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS=HPACK ヘッダーã®ãƒãƒ•ãƒžãƒ³ç¬¦å·åŒ–ã—ãŸå€¤ã¯ EOS パディングã§çµ‚了ã—ã¦ã„ã¾ã›ã‚“。 +hpackhuffman.stringLiteralEOS=HPACK ヘッダ中ã®ãƒãƒ•ãƒžãƒ³ç¬¦å·åŒ–値㫠EOS 記å·ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +hpackhuffman.stringLiteralTooMuchPadding=Huffman 符å·åŒ–ã•ã‚ŒãŸæ–‡å­—列リテラルã®çµ‚ã‚ã‚Šã«ã€7ビット以上ã®EOSパディングãŒæä¾›ã•ã‚Œã¾ã—ãŸã€‚ + +http2Parser.error=接続 [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ•ãƒ¬ãƒ¼ãƒ ã‚¿ã‚¤ãƒ— [{2}]ã€ã‚¨ãƒ©ãƒ¼ +http2Parser.headerLimitCount=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ˜ãƒƒãƒ€ãƒ¼ãŒå¤šã™ãŽã¾ã™ +http2Parser.headerLimitSize=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€åˆè¨ˆãƒ˜ãƒƒãƒ€ãƒ¼ã‚µã‚¤ã‚ºãŒå¤§ãã™ãŽã¾ã™ +http2Parser.headers.wrongFrameType=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}] ã®é€²è¡Œä¸­ã®ãƒ˜ãƒƒãƒ€ãƒ¼ã€ã—ã‹ã—タイプ [{2}] ã®ãƒ•ãƒ¬ãƒ¼ãƒ ãŒå—ä¿¡ã•ã‚Œã¾ã—㟠+http2Parser.headers.wrongStream=接続 [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}] ã®ãƒ˜ãƒƒãƒ€å‡¦ç†ä¸­ã«ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{2}] ã®ãƒ•ãƒ¬ãƒ¼ãƒ ãŒå—ä¿¡ã•ã‚Œã¾ã—㟠+http2Parser.invalidBuffers=読ã¿è¾¼ã¿ã¯2ã¤ã®ãƒãƒƒãƒ•ã‚¡ã§è¡Œã†å¿…è¦ãŒã‚ã‚Šã¾ã™ +http2Parser.nonZeroPadding=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€éžã‚¼ãƒ­ã®ãƒ‘ディングをå—ä¿¡ã—ã¾ã—㟠+http2Parser.payloadTooBig=ペイロードã®é•·ã•ã¯ [{0}] ãƒã‚¤ãƒˆã§ã™ãŒã€æœ€å¤§ãƒ•ãƒ¬ãƒ¼ãƒ ã‚µã‚¤ã‚ºã¯ [{1}] ã§ã™ +http2Parser.preface.invalid=無効ãªã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ preface ãŒæ示ã•ã‚Œã¾ã—㟠+http2Parser.preface.io=コãƒã‚¯ã‚·ãƒ§ãƒ³ preface を読むã“ã¨ãŒã§ãã¾ã›ã‚“ +http2Parser.processFrame=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ•ãƒ¬ãƒ¼ãƒ ã‚¿ã‚¤ãƒ— [{2}]ã€ãƒ•ãƒ©ã‚° [{3}]ã€ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰ã‚µã‚¤ã‚º [{4}] +http2Parser.processFrame.tooMuchPadding=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰ [{3}] ã«å¯¾ã—ã¦ãƒ‘ディング長 [{2}] ã¯å¤§ãã™ãŽã¾ã™ã€‚ +http2Parser.processFrame.unexpectedType=予想ã•ã‚Œã‚‹ãƒ•ãƒ¬ãƒ¼ãƒ ã‚¿ã‚¤ãƒ— [{0}]ã€ã—ã‹ã—å—ä¿¡ã•ã‚ŒãŸãƒ•ãƒ¬ãƒ¼ãƒ ã‚¿ã‚¤ãƒ— [{1}] +http2Parser.processFrameContinuation.notExpected=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€é€²è¡Œä¸­ã®ãƒ˜ãƒƒãƒ€ãƒ¼ãŒãªã„ã¨ãã«ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}] ã®ContinuationフレームãŒå—ä¿¡ã•ã‚Œã¾ã—㟠+http2Parser.processFrameData.lengths=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ‡ãƒ¼ã‚¿é•·ã€[{2}]ã€ãƒ‘ディング長 [{3}] +http2Parser.processFrameData.window=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã¯ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚¦ã‚¤ãƒ³ãƒ‰ã‚¦ã‚µã‚¤ã‚ºã‚ˆã‚Šå¤§ããªãƒ‡ãƒ¼ã‚¿ã‚’é€ä¿¡ã—ã¾ã—㟠+http2Parser.processFrameHeaders.decodingDataLeft=HPAC をデコードã—ãŸã®ã«ãƒ‡ãƒ¼ã‚¿ãŒæ®‹ã£ã¦ã„ã¾ã™ã€‚ã™ã¹ã¦ä½¿ç”¨ã™ã‚‹ã¹ãã§ã™ +http2Parser.processFrameHeaders.decodingFailed=HTTP ヘッダー㮠HPACK 復å·åŒ–中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +http2Parser.processFrameHeaders.payload=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚µã‚¤ã‚º [{2}] ã®ãƒ˜ãƒƒãƒ€ãƒ¼ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰ã‚’処ç†ä¸­ +http2Parser.processFramePriorityUpdate.debug=接続 [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ç·Šæ€¥åº¦ [{2}]ã€å¢—分 [{3}] +http2Parser.processFramePriorityUpdate.streamZero=接続 [{0}] ã¯ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  0 を優先ã™ã‚‹ãŸã‚ã®å„ªå…ˆæ›´æ–°ãƒ•ãƒ¬ãƒ¼ãƒ ã‚’å—ä¿¡ã—ã¾ã—㟠+http2Parser.processFramePushPromise=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã‹ã‚‰ PUSH_PROMISE フレームをé€ä¿¡ã™ã‚‹ã¹ãã§ã¯ã‚ã‚Šã¾ã›ã‚“ +http2Parser.processFrameSettings.ackWithNonZeroPayload=ACKフラグãŒã‚»ãƒƒãƒˆã•ã‚Œã€ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰ãŒå­˜åœ¨ã™ã‚‹çŠ¶æ…‹ã§å—ä¿¡ã•ã‚ŒãŸSettingsフレーム +http2Parser.processFrameWindowUpdate.debug=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¦ã‚¤ãƒ³ãƒ‰ã‚¦ã‚µã‚¤ã‚ºã‚’ [{2}] ã«æ‹¡å¤§ã—ã¾ã™ã€‚ +http2Parser.processFrameWindowUpdate.invalidIncrement=無効ãªå¢—分サイズ [0] ã§å—ä¿¡ã•ã‚ŒãŸWindow Updateフレーム +http2Parser.swallow.debug=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€é£²ã¿è¾¼ã¾ã‚ŒãŸ [{2}] ãƒã‚¤ãƒˆ + +http2Protocol.jmxRegistration.fail=HTTP/2プロトコルã®JMX登録ã«å¤±æ•—ã—ã¾ã—㟠+ +pingManager.roundTripTime=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}] ã®å¾€å¾©æ™‚間㯠[{1}] ns ã§ã—ãŸã€‚ + +stream.clientCancel=レスãƒãƒ³ã‚¹ãŒå®Œäº†ã™ã‚‹å‰ã«ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆãŒã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’リセットã—ã¾ã—㟠+stream.closed=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€åˆ‡æ–­ã—ãŸã‚¹ãƒˆãƒªãƒ¼ãƒ ã«ã¯æ›¸ãè¾¼ã¿ã§ãã¾ã›ã‚“ +stream.header.case=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€HTTP ヘッダーå [{2}] ã¯å°æ–‡å­—ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +stream.header.connection=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€HTTP/2 ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ã¯ HTTP ヘッダー [{2}] を指定ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +stream.header.contentLength=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€content length ヘッダーã®å€¤ [{2}] ã¨å—ä¿¡ã—ãŸãƒ‡ãƒ¼ã‚¿é•· [{3}] ã¯ä¸€è‡´ã—ã¾ã›ã‚“。 +stream.header.debug=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€HTTP ヘッダー [{2}]ã€å€¤ã¯ [{3}] +stream.header.duplicate=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ˜ãƒƒãƒ€ãƒ¼ [{2}] を複数å—ä¿¡ã—ã¾ã—ãŸã€‚ +stream.header.empty=接続 [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ç„¡åŠ¹ãªç©ºãƒ˜ãƒƒãƒ€åã§ã™ +stream.header.invalid=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ˜ãƒƒãƒ€ãƒ¼ [{2}] ã«ä¸æ­£ãªå€¤ [{3}] ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ +stream.header.noPath=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ç–‘似ヘッダー [:path] ãŒç©ºã§ã™ã€‚ +stream.header.required=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ï¼‘ã¤ä»¥ä¸Šã®å¿…須ヘッダãŒã‚ã‚Šã¾ã›ã‚“。 +stream.header.te=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€HTTP/2 ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã§ã¯ HTTP ヘッダー [te] ã®å€¤ã« [{2}] を指定ã§ãã¾ã›ã‚“。 +stream.header.unexpectedPseudoHeader=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€é€šå¸¸ã®ãƒ˜ãƒƒãƒ€ãƒ¼ã®å¾Œã«ç–‘似ヘッダー [{2}] ã‚’å—ä¿¡ã—ã¾ã—ãŸã€‚ +stream.header.unknownPseudoHeader=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€æœªçŸ¥ã®ç–‘似ヘッダー [{2}] ã‚’å—ä¿¡ã—ã¾ã—ãŸã€‚ +stream.host.inconsistent=Connection [{0}]ã€Stream [{1}]ã§ã€ãƒ›ã‚¹ãƒˆãƒ˜ãƒƒãƒ€ãƒ¼ [{2}] ã¯ã€ä»¥å‰æä¾›ã•ã‚ŒãŸãƒ›ã‚¹ãƒˆ [{3}] ãŠã‚ˆã³/ã¾ãŸã¯ãƒãƒ¼ãƒˆ [{4}] ã®å€¤ã¨çŸ›ç›¾ã—ã¦ã„ã¾ã™ +stream.inputBuffer.copy=入力ãƒãƒƒãƒ•ã‚¡ãƒ¼ã‹ã‚‰å‡ºåŠ›ãƒãƒƒãƒ•ã‚¡ãƒ¼ã¸ã‚³ãƒ”ーã—ãŸã®ã¯ [{0}] ãƒã‚¤ãƒˆã§ã™ã€‚ +stream.inputBuffer.dispatch=read interest ãŒç™»éŒ²ã•ã‚Œã‚‹ã¨ã€inBufferã«ãƒ‡ãƒ¼ã‚¿ãŒè¿½åŠ ã•ã‚Œã¾ã™ã€‚読ã¿å–りディスパッãƒã‚’トリガã—ã¾ã™ã€‚ +stream.inputBuffer.empty=ストリーム入力ãƒãƒƒãƒ•ã‚¡ãŒç©ºã§ã™ã€‚ より多ãã®ãƒ‡ãƒ¼ã‚¿ã‚’å¾…ã£ã¦ã„ã¾ã™ã€‚ +stream.inputBuffer.readTimeout=クライアントã‹ã‚‰ãƒ‡ãƒ¼ã‚¿ã‚’読ã¿å–る待機中ã®ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆ +stream.inputBuffer.reset=ストリームリセット +stream.inputBuffer.signal=読ã¿è¾¼ã¿ã‚¹ãƒ¬ãƒƒãƒ‰ãŒå¾…æ©Ÿã—ã¦ã„ã‚‹é–“ã« inBuffer ã¸ãƒ‡ãƒ¼ã‚¿ãŒè¿½åŠ ã•ã‚Œã¾ã—ãŸã€‚スレッドã¸å‡¦ç†ã®å†é–‹ã‚’通知ã—ã™ã€‚ +stream.inputBuffer.swallowUnread=以å‰ã«å…¥åŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ãƒãƒƒãƒ•ã‚¡ã«èª­ã¿è¾¼ã¾ã‚ŒãŸ [{0}] ãƒã‚¤ãƒˆã‚’飲ã¿è¾¼ã¾ã™ +stream.notWritable=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã“ã®ã‚¹ãƒˆãƒªãƒ¼ãƒ ã«ã¯æ›¸ãè¾¼ã¿ã§ãã¾ã›ã‚“。 +stream.outputBuffer.flush.debug=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒãƒƒãƒ•ã‚¡ãƒã‚¸ã‚·ãƒ§ãƒ³ [{2}]ã§å‡ºåŠ›ã‚’フラッシュã€writeInProgress [{3}]ã€ã‚¯ãƒ­ãƒ¼ã‚º [{4}] +stream.recycle=Connection[{0}]ã€Stream[{1}]ã¯ãƒªã‚µã‚¤ã‚¯ãƒ«ã•ã‚Œã¾ã—㟠+stream.reset.fail=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’リセットã§ãã¾ã›ã‚“。 +stream.reset.receive=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€[{2}] ã®ãŸã‚ã«å—ä¿¡ã•ã‚ŒãŸãƒªã‚»ãƒƒãƒˆ +stream.reset.send=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€[{2}] ãŒåŽŸå› ã§ RESET ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚ +stream.trailerHeader.noEndOfStream=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€trailer ヘッダーã«ã‚¹ãƒˆãƒªãƒ¼ãƒ çµ‚了フラグãŒå«ã¾ã‚Œã¦ã„ã¾ã›ã‚“ +stream.writeTimeout=クライアントãŒã‚¹ãƒˆãƒªãƒ¼ãƒ ãƒ‡ãƒ¼ã‚¿ã®æ›¸ãè¾¼ã¿ã‚’許å¯ã™ã‚‹ãŸã‚ã«ãƒ•ãƒ­ãƒ¼åˆ¶å¾¡ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã‚’増やã™ã®ã‚’å¾…ã¤ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆ + +streamProcessor.cancel=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€æ®‹ã‚Šã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒœãƒ‡ã‚£ã¯å¿…è¦ã¨ã•ã‚Œã¾ã›ã‚“ +streamProcessor.error.connection=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã«è‡´å‘½çš„ãªã‚¨ãƒ©ãƒ¼ãŒå‡¦ç†ä¸­ã«ç™ºç”Ÿã—ã¾ã—㟠+streamProcessor.error.stream=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€å‡¦ç†ä¸­ã«ã‚¹ãƒˆãƒªãƒ¼ãƒ ã«è‡´å‘½çš„ãªã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+streamProcessor.flushBufferedWrite.entry=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€æ›¸ãè¾¼ã¿ç”¨ãƒãƒƒãƒ•ã‚¡ã‚’フラッシュã—ã¾ã™ã€‚ +streamProcessor.service.error=リクエスト処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ + +streamStateMachine.debug.change=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€çŠ¶æ…‹ã‚’ [{2}] ã‹ã‚‰ [{3}] ã¸å¤‰æ›´ã—ã¾ã—ãŸã€‚ +streamStateMachine.invalidFrame=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€çŠ¶æ…‹ [{2}]ã€ãƒ•ãƒ¬ãƒ¼ãƒ ç¨®é¡ž [{3}] + +upgradeHandler.allocate.debug=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€å‰²ã‚Šå½“ã¦ã‚‰ã‚ŒãŸ [{2}] ãƒã‚¤ãƒˆ +upgradeHandler.allocate.left=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€[{2}] ãƒã‚¤ãƒˆãŒæœªå‰²ã‚Šå½“㦠- å­ã¸ã®å‰²ã‚Šå½“ã¦ã‚’試ã¿ã¦ã„ã¾ã™ +upgradeHandler.connectionError=接続エラー +upgradeHandler.enableRfc7450Priorities=接続 [{0}] ã¯ã€RFC 7450 優先順ä½ãŒåˆæœŸæŽ¥ç¶šè¨­å®šãƒ•ãƒ¬ãƒ¼ãƒ ã§ç„¡åŠ¹ã«ã•ã‚ŒãŸå¾Œã«æœ‰åŠ¹ã«ãªã‚‰ãªã„å ´åˆãŒã‚ã‚Šã¾ã™ (RFC 9218 ã‚’å‚ç…§) +upgradeHandler.fallToDebug=\n\ +\ 注: HTTP/2 ストリームã®ã‚¨ãƒ©ãƒ¼ãŒã•ã‚‰ã«ç™ºç”Ÿã™ã‚‹ã¨ã€DEBUG レベルã§ãƒ­ã‚°ã«è¨˜éŒ²ã•ã‚Œã¾ã™ã€‚ +upgradeHandler.goaway.debug=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€Goawayã€æœ€çµ‚ストリーム [{1}]ã€ã‚¨ãƒ©ãƒ¼ã‚³ãƒ¼ãƒ‰ [{2}]ã€ãƒ‡ãƒãƒƒã‚°ãƒ‡ãƒ¼ã‚¿ [{3}] +upgradeHandler.init=コãƒã‚¯ã‚·ãƒ§ãƒ³[{0}]ã€çŠ¶æ…‹[{1}] +upgradeHandler.invalidPreface=コãƒã‚¯ã‚·ãƒ§ãƒ³[{0}]ã€ç„¡åŠ¹ãªConnection Preface +upgradeHandler.ioerror=コãƒã‚¯ã‚·ãƒ§ãƒ³[{0}] +upgradeHandler.noAllocation=接続 [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€å‰²ã‚Šå½“ã¦ã®å¾…æ©ŸãŒã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã—㟠+upgradeHandler.noNewStreams=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã“ã®ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã«ã¯æ–°ã—ã„ストリームを作æˆã§ããªã„ãŸã‚ストリームを無視ã—ã¾ã™ã€‚ +upgradeHandler.overheadChange=接続 [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ•ãƒ¬ãƒ¼ãƒ ã‚¿ã‚¤ãƒ— [{2}] ã«ã‚ˆã‚Šã€æ–°ã—ã„オーãƒãƒ¼ãƒ˜ãƒƒãƒ‰ã‚«ã‚¦ãƒ³ãƒˆ [{3}] ãŒç™ºç”Ÿã—ã¾ã—㟠+upgradeHandler.pause.entry=コãƒã‚¯ã‚·ãƒ§ãƒ³[{0}] 一時åœæ­¢ä¸­ +upgradeHandler.pingFailed=コãƒã‚¯ã‚·ãƒ§ãƒ³ ID [{0}] ã¯ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã¸ã® ping é€ä¿¡ã«å¤±æ•—ã—ã¾ã—㟠+upgradeHandler.prefaceReceived=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã‹ã‚‰ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ãƒ—リフェイスをå—ä¿¡ã—ã¾ã—ãŸã€‚ +upgradeHandler.pruneIncomplete=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã‚’削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚アクティブãªã‚¹ãƒˆãƒªãƒ¼ãƒ æ•° [{2}] ã¯å¤šã™ãŽã¾ã™ã€‚ +upgradeHandler.pruneStart=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}] å¤ã„ストリームã®ãƒ—ルーニングを開始ã—ã¾ã™ã€‚ 上é™ã¯ [{1}] ã§ã€ç¾åœ¨ [{2}] ストリームãŒã‚ã‚Šã¾ã™ã€‚ +upgradeHandler.pruned=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€å®Œäº†ã—ãŸã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}] ã¯å‰Šé™¤ã—ã¾ã™ã€‚ +upgradeHandler.releaseBacklog=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}] ã¯ãƒãƒƒã‚¯ãƒ­ã‚°ã‹ã‚‰è§£æ”¾ã•ã‚Œã¾ã—㟠+upgradeHandler.reset.receive=Connection[{0}]ã€Stream[{1}]ã€[{2}]ã®ãŸã‚ã«ãƒªã‚»ãƒƒãƒˆã‚’å—ä¿¡ã—ã¾ã—㟠+upgradeHandler.rst.debug=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¨ãƒ©ãƒ¼ [{2}]ã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ [{3}]ã€RST (ストリームを切断ã—ã¾ã™) +upgradeHandler.sendPrefaceFail=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã«ãƒ—リフェイスをé€ä¿¡ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +upgradeHandler.sendfile.reservation=sendfileã«ã‚ˆã‚‹writeã®å‰ã® コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€æŽ¥ç¶šäºˆç´„ [{2}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ äºˆç´„ [{3}] +upgradeHandler.socketCloseFailed=ソケットクローズ中ã®ã‚¨ãƒ©ãƒ¼ +upgradeHandler.startRequestBodyFrame.result=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}] startRequestBodyFrame㌠[{2}] ã‚’è¿”ã—ã¾ã—㟠+upgradeHandler.stream.closed=ストリーム [{0}] ãŒã—ã°ã‚‰ãé–‰ã˜ã‚‰ã‚Œã¦ã„ã¾ã—㟠+upgradeHandler.stream.error=Connection [{0}]ã€Stream [{1}] ã¯ã‚¨ãƒ©ãƒ¼ã®ãŸã‚ã«ã‚¯ãƒ­ãƒ¼ã‚ºã—ã¾ã—㟠+upgradeHandler.stream.even=æ–°ã—ã„リモートストリーム ID [{0}] ã‚’è¦æ±‚ã•ã‚Œã¾ã—ãŸãŒãƒªãƒ¢ãƒ¼ãƒˆã‚¹ãƒˆãƒªãƒ¼ãƒ ã® ID ã¯å¥‡æ•°ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +upgradeHandler.stream.notWritable=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã“ã®ã‚¹ãƒˆãƒªãƒ¼ãƒ ã¯æ›¸ãè¾¼ã¿å¯èƒ½ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +upgradeHandler.stream.old=æ–°ã—ã„リモートストリーム ID [{0}] ã‚’è¦æ±‚ã•ã‚Œã¾ã—ãŸãŒã€æœ€æ–°ã®ã‚¹ãƒˆãƒªãƒ¼ãƒ ã¯ [{1}] ã§ã™ã€‚ +upgradeHandler.tooManyRemoteStreams=クライアント㯠[{0}] 以上ã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’使用ã—よã†ã¨ã—ã¾ã—㟠+upgradeHandler.tooMuchOverhead=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚ªãƒ¼ãƒãƒ¼ãƒ˜ãƒƒãƒ‰ãŒå¤šã™ãŽã‚‹ãŸã‚ã€æŽ¥ç¶šãŒé–‰ã˜ã‚‰ã‚Œã¾ã™ã€‚ +upgradeHandler.unexpectedAck=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€äºˆæœŸã—ãªã„ã¨ãã«settings ackã‚’å—ä¿¡ã—ã¾ã—㟠+upgradeHandler.upgrade=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [1] ã¸ã®HTTP/1.1 upgrade +upgradeHandler.upgrade.fail=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€HTTP/1.1 ã®ã‚¢ãƒƒãƒ—グレードã«å¤±æ•—ã—ã¾ã—㟠+upgradeHandler.upgradeDispatch.entry=エントリã€ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³[{0}]ã€ã‚½ã‚±ãƒƒãƒˆçŠ¶æ…‹ [{1}] +upgradeHandler.upgradeDispatch.exit=終了ã€ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³[{0}]ã€ã‚½ã‚±ãƒƒãƒˆçŠ¶æ…‹[{1}] +upgradeHandler.windowSizeReservationInterrupted=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€[{2}] ãƒã‚¤ãƒˆã®äºˆç´„ +upgradeHandler.windowSizeTooBig=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã‚µã‚¤ã‚ºãŒå¤§ãã™ãŽã¾ã™ +upgradeHandler.windowUpdateConnection=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã®æ›´æ–°ã‚’クライアントã«é€ä¿¡ã—ã€ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã‚’[{1}]ãƒã‚¤ãƒˆå¢—ã‚„ã—ã¾ã™ +upgradeHandler.windowUpdateStream=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã®æ›´æ–°ã‚’クライアントã«é€ä¿¡ã—ã€ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã‚’ [{2}] ãƒã‚¤ãƒˆå¢—ã‚„ã—ã¾ã™ +upgradeHandler.writeBody=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ‡ãƒ¼ã‚¿é•· [{2}] +upgradeHandler.writeHeaders=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}], ストリーム [{1}] +upgradeHandler.writePushHeaders=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ—ッシュã•ã‚ŒãŸã‚¹ãƒˆãƒªãƒ¼ãƒ  [{2}]ã€EndOfStream [{3}] + +windowAllocationManager.dispatched=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ãƒ‡ã‚£ã‚¹ãƒ‘ッãƒã•ã‚Œã¾ã—㟠+windowAllocationManager.notified=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€é€šçŸ¥ã•ã‚Œã¾ã—㟠+windowAllocationManager.notify=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€å¾…機タイプ [{2}]ã€é€šçŸ¥ã‚¿ã‚¤ãƒ— [{3}] +windowAllocationManager.waitFor.connection=接続 [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€æŽ¥ç¶šãƒ•ãƒ­ãƒ¼åˆ¶å¾¡ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ (blocking) を待機中ã§ã™ (タイムアウト [{2}]) +windowAllocationManager.waitFor.ise=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã™ã§ã«å¾…機中ã§ã™ +windowAllocationManager.waitFor.stream=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ ãƒ•ãƒ­ãƒ¼åˆ¶å¾¡ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ (blocking) をタイムアウト [{2}]ã§å¾…機中ã§ã™ +windowAllocationManager.waitForNonBlocking.connection=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€æŽ¥ç¶šãƒ•ãƒ­ãƒ¼åˆ¶å¾¡ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ (non-blocking) を待機中ã§ã™ +windowAllocationManager.waitForNonBlocking.stream=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ  [{1}]ã€ã‚¹ãƒˆãƒªãƒ¼ãƒ ãƒ•ãƒ­ãƒ¼åˆ¶å¾¡ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ (non-blocking) を待機中ã§ã™ + +writeStateMachine.endWrite.ise=書ãè¾¼ã¿ãŒå®Œäº†ã—ãŸã‚‰æ–°ã—ã„状態㫠[{0}] を指定ã™ã‚‹ã®ã¯ä¸æ­£ã§ã™ +writeStateMachine.ise=状態 [{1}] ã® [{0}()] を呼ã³å‡ºã™ã“ã¨ã¯ä¸æ­£ã§ã™ diff --git a/java/org/apache/coyote/http2/LocalStrings_ko.properties b/java/org/apache/coyote/http2/LocalStrings_ko.properties new file mode 100644 index 0000000..5893a35 --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_ko.properties @@ -0,0 +1,180 @@ +# 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. + +abstractStream.setConnectionAllocationMade=ì—°ê²° [{0}], 스트림 [{1}], ì—°ê²° í• ë‹¹ì´ [{2}]ì—ì„œ [{3}](으)ë¡œ 변경ë˜ì—ˆìŠµë‹ˆë‹¤. +abstractStream.setConnectionAllocationRequested=ì—°ê²° [{0}], 스트림 [{1}], ìš”ì²­ëœ ì—°ê²° í• ë‹¹ì´ [{2}]ì—ì„œ [{3}](으)ë¡œ 변경ë˜ì—ˆìŠµë‹ˆë‹¤. +abstractStream.windowSizeDec=ì—°ê²° [{0}], 스트림 [{1}], flow control 윈ë„우를 [{2}] ë§Œí¼ ì¤„ì—¬ [{3}]ì— ì´ë¥´ê²Œ 합니다. +abstractStream.windowSizeInc=ì—°ê²° [{0}], 스트림 [{1}]: Flow control 윈ë„우를 [{2}] ë§Œí¼ ì¦ê°€ì‹œì¼œ 윈ë„ìš° í¬ê¸°ê°€ [{3}]ì´(ê°€) ë˜ë„ë¡ í•©ë‹ˆë‹¤. +abstractStream.windowSizeTooBig=ì—°ê²° [{0}], 스트림 [{1}], 윈ë„ìš° í¬ê¸°ë¥¼ [{2}] ë§Œí¼ ì¦ê°€ì‹œì¼œ [{3}](으)ë¡œ 만들었으나, ì´ëŠ” í—ˆìš©ëœ ìµœëŒ€ê°’ì„ ì´ˆê³¼í–ˆìŠµë‹ˆë‹¤. + +connectionPrefaceParser.eos=개시ë˜ëŠ” í´ë¼ì´ì–¸íŠ¸ preface ë°”ì´íŠ¸ 시퀀스를 ì½ëŠ” ë™ì•ˆ, 예기치 ì•Šì€ ìŠ¤íŠ¸ë¦¼ì˜ ë. 단지 [{0}] ë°”ì´íŠ¸ë§Œì„ ì½ìŒ. +connectionPrefaceParser.mismatch=해당 client preface [{0}]ì˜ ì‹œìž‘ 부분ì—ì„œ 예기치 ì•Šì€ ë°”ì´íŠ¸ 시퀀스를 받았습니다. + +connectionSettings.debug=ì—°ê²° [{0}]: 파ë¼ë¯¸í„° 타입 [{1}]ì„(를) [{2}](으)ë¡œ 설정함. +connectionSettings.enablePushInvalid=ì—°ê²° [{0}], enablePush를 위해 ìš”ì²­ëœ ê°’ [{1}]ì€(는), í—ˆìš©ëœ ê°’ë“¤(0 ë˜ëŠ” 1) ì¤‘ì˜ í•˜ë‚˜ê°€ 아닙니다. +connectionSettings.headerTableSizeLimit=ì—°ê²° [{0}]: í—¤ë” í…Œì´ë¸” í¬ê¸°ë¡œ [{1}]ì„(를) 설정하려 ì‹œë„했으나, í•œê³„ê°’ì€ 16k입니다. +connectionSettings.maxFrameSizeInvalid=ì—°ê²° [{0}]: [{1}]ì˜ ìš”ì²­ëœ ìµœëŒ€ 프레임 í¬ê¸°ê°€ í—ˆìš©ëœ ë²”ìœ„([{2}] - [{3}])ì˜ ë°”ê¹¥ì— ì¡´ìž¬í•©ë‹ˆë‹¤. +connectionSettings.unknown=ì—°ê²° [{0}]: ì‹ë³„ìžê°€ [{1}](ì´)ê³  ê°’ì´ [{2}]ì¸ ì•Œ 수 없는 ì„¤ì •ì´ ë¬´ì‹œë˜ì—ˆìŠµë‹ˆë‹¤. +connectionSettings.windowSizeTooBig=ì—°ê²° [{0}]: ìš”ì²­ëœ ìœˆë„ìš° í¬ê¸° [{1}]ì´(ê°€) 최대 허용치 [{2}] 보다 í½ë‹ˆë‹¤. + +frameType.checkPayloadSize=Payloadì˜ í¬ê¸° [{0}]ì€(는) 프레임 타입 [{1}]ì„(를) 위해 유효하지 않습니다. +frameType.checkStream=유효하지 ì•Šì€ í”„ë ˆìž„ 타입 [{0}] + +hpack.integerEncodedOverTooManyOctets=HPACK 가변 ê¸¸ì´ ì •ìˆ˜ê°€ 너무 ë§Žì€ ì˜¥í…Ÿ(octet)들로 ì¸ì½”딩ë˜ì–´ 있습니다. 최대 길ì´ëŠ” [{0}]입니다. +hpack.invalidCharacter=code point [{1}]ì— ìœ„ì¹˜í•œ 유니코드 ë¬¸ìž [{0}]ì€(는), 0ì—ì„œ 255ê¹Œì§€ì˜ í—ˆìš© 범위 ë°”ê¹¥ì— ìžˆìœ¼ë¯€ë¡œ ì¸ì½”ë”©ë  ìˆ˜ 없습니다. + +hpackEncoder.encodeHeader=ì¸ì½”딩 í—¤ë” [{0}]와(ê³¼) ê·¸ì˜ ê°’ [{1}] + +hpackdecoder.addDynamic=ë™ì  í…Œì´ë¸”ì˜ ì¸ë±ìŠ¤ [{0}]ì— ì´ë¦„ì´ [{1}]ì´ê³  ê°’ì´ [{2}]ì¸ í—¤ë”를 추가합니다. +hpackdecoder.clearDynamic=ë™ì  í…Œì´ë¸”ì„ ë¹„ì›ë‹ˆë‹¤. +hpackdecoder.emitHeader=ì´ë¦„ì´ [{1}]ì´ê³  ê°’ì´ [{2}]ì¸ í—¤ë”를 내보냅니다. +hpackdecoder.headerTableIndexInvalid=[{1}]ê°œì˜ ì •ì  ì—”íŠ¸ë¦¬ë“¤ê³¼ [{2}]ê°œì˜ ë™ì  ì—”íŠ¸ë¦¬ë“¤ì´ ì¡´ìž¬í•˜ê¸°ì—, í—¤ë” í…Œì´ë¸” ì¸ë±ìŠ¤ [{0}]ì€(는) 유효하지 않습니다. +hpackdecoder.maxMemorySizeExceeded=í—¤ë” í…Œì´ë¸” í¬ê¸° [{1}]ì´(ê°€) 최대 í¬ê¸° [{1}]ì„(를) 초과합니다. +hpackdecoder.notImplemented=ì•„ì§ êµ¬í˜„ ì•ˆë¨ +hpackdecoder.nullHeader=ì¸ë±ìŠ¤ê°€ [{0}]ì¸ ìœ„ì¹˜ì— ë„ í—¤ë”ê°€ 존재합니다. +hpackdecoder.tableSizeUpdateNotAtStart=í…Œì´ë¸” í¬ê¸° 변경ì€, 반드시 í—¤ë” ë¸”ë¡ì˜ 시작 ì‹œì— ì „ì†¡ë˜ì–´ì•¼ë§Œ 합니다. +hpackdecoder.useDynamic=ë™ì  í…Œì´ë¸”ì˜ ì¸ë±ìŠ¤ [{0}](으)로부터 í—¤ë”를 사용합니다. +hpackdecoder.useStatic=ì •ì  í…Œì´ë¸”ì˜ ì¸ë±ìŠ¤ [{0}](으)로부터 í—¤ë”를 사용합니다. +hpackdecoder.zeroNotValidHeaderTableIndex=0ì€ ìœ íš¨í•œ í—¤ë” í…Œì´ë¸” ì¸ë±ìŠ¤ê°€ 아닙니다. + +hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS=HPACK í—¤ë”들 ë‚´ì˜ Huffman 알고리즘으로 ì¸ì½”ë”©ëœ ê°’ì´, EOS padding으로 ë나지 않았습니다. +hpackhuffman.stringLiteralEOS=HPACK í—¤ë”들 ë‚´ì˜ Huffman 알고리즘으로 ì¸ì½”ë”©ëœ ê°’ì´, EOS 부호를 í¬í•¨í–ˆìŠµë‹ˆë‹¤. +hpackhuffman.stringLiteralTooMuchPadding=Huffman 알고리즘으로 ì¸ì½”ë”©ëœ ë¬¸ìžì—´ì˜ ëì— 7 비트를 초과한 EOS padding입니다. + +http2Parser.error=ì—°ê²° [{0}], 스트림 [{1}], 프레임 타입 [{2}], 오류 ë°œìƒ +http2Parser.headerLimitCount=ì—°ê²° [{0}], 스트림 [{1}], 너무 ë§Žì€ í—¤ë”ë“¤ì´ ìžˆìŒ +http2Parser.headerLimitSize=ì—°ê²° [{0}], 스트림 [{1}], ì „ì²´ í—¤ë” í¬ê¸°ê°€ 너무 í½ë‹ˆë‹¤. +http2Parser.headers.wrongFrameType=ì—°ê²° [{0}], 스트림 [{1}]ì„(를) 위한 í—¤ë”ë“¤ì´ ì§„í–‰ì¤‘ì´ì§€ë§Œ, 타입 [{2}]ì˜ í”„ë ˆìž„ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +http2Parser.headers.wrongStream=ì—°ê²° [{0}]: 스트림 [{1}]ì˜ í—¤ë”ë“¤ì„ ì²˜ë¦¬í•˜ëŠ” 과정ì—ì„œ, 스트림 [{2}]ì˜ í”„ë ˆìž„ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +http2Parser.invalidBuffers=ì½ê¸°ëŠ” ë‘ ê°œì˜ ë²„í¼ë“¤ê³¼ 함께 ì´ë£¨ì–´ì ¸ì•¼ 합니다. +http2Parser.nonZeroPadding=ì—°ê²° [{0}], 스트림 [{1}], 0ì´ ì•„ë‹Œ paddingì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +http2Parser.payloadTooBig=Payloadì˜ ê¸¸ì´ê°€ [{0}]ë°”ì´íŠ¸ì´ì§€ë§Œ, 최대 프레임 í¬ê¸°ëŠ” [{1}]입니다. +http2Parser.preface.invalid=유효하지 ì•Šì€ ì—°ê²° preface ì´(ê°€) 제공ë˜ì—ˆìŠµë‹ˆë‹¤. +http2Parser.preface.io=ì—°ê²° preface를 ì½ì„ 수 없습니다. +http2Parser.processFrame=ì—°ê²° [{0}], 스트림 [{1}], 프레임 타입 [{2}], 플래그들 [{3}], Payload í¬ê¸° [{4}] +http2Parser.processFrame.tooMuchPadding=ì—°ê²° [{0}], 스트림 [{1}], padding ê¸¸ì´ [{2}]ì€(는) payload [{3}]ì„(를) 위해 너무 í½ë‹ˆë‹¤. +http2Parser.processFrame.unexpectedType=프레임 타입 [{0}]ì´(ê°€) 요구ë˜ì—ˆìœ¼ë‚˜, 프레임 타입 [{1}]ì„(를) 받았습니다. +http2Parser.processFrameContinuation.notExpected=ì—°ê²° [{0}]: í—¤ë”ë“¤ì´ ì•„ë¬´ ê²ƒë„ ì§„í–‰ë˜ì§€ ì•Šì€ ìƒíƒœì—ì„œ, 스트림 [{1}]ì„(를) 위한 Continuation í”„ë ˆìž„ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +http2Parser.processFrameData.lengths=ì—°ê²° [{0}], 스트림 [{1}], ë°ì´í„° 길ì´, [{2}], Padding ê¸¸ì´ [{3}] +http2Parser.processFrameData.window=ì—°ê²° [{0}]: í´ë¼ì´ì–¸íŠ¸ê°€ 스트림 윈ë„ìš°ê°€ 허용하는 ë°ì´í„° í¬ê¸°ë³´ë‹¤ ë” ë§Žì€ ë°ì´í„°ë¥¼ 전송했습니다. +http2Parser.processFrameHeaders.decodingDataLeft=HPACK 디코딩 후 남아있는 ë°ì´í„° - 반드시 소비ë˜ì—ˆì–´ì•¼ 합니다. +http2Parser.processFrameHeaders.decodingFailed=HTTP í—¤ë”ë“¤ì˜ HPACK 디코딩 과정ì—ì„œ 오류가 있었습니다. +http2Parser.processFrameHeaders.payload=ì—°ê²° [{0}], 스트림 [{1}], í¬ê¸°ê°€ [{2}]ì¸ í—¤ë”ë“¤ì˜ payload를 처리합니다. +http2Parser.processFramePushPromise=ì—°ê²° [{0}], 스트림 [{1}], Push promise í”„ë ˆìž„ë“¤ì´ í´ë¼ì´ì–¸íŠ¸ì— ì˜í•´ 전송ë˜ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +http2Parser.processFrameSettings.ackWithNonZeroPayload=ACK 플래그가 설정ë˜ê³  payloadê°€ 존재하는, Settings í”„ë ˆìž„ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +http2Parser.processFrameWindowUpdate.debug=ì—°ê²° [{0}], 스트림 [{1}], 윈ë„ìš° í¬ê¸°ë¥¼ [{2}] ë§Œí¼ ì¦ê°€ 시킵니다. +http2Parser.processFrameWindowUpdate.invalidIncrement=유효하지 ì•Šì€ ì¦ë¶„ í¬ê¸°ì¸ [0]와(ê³¼) 함께, 윈ë„ìš° 변경 í”„ë ˆìž„ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +http2Parser.swallow.debug=ì—°ê²° [{0}], 스트림 [{1}], [{2}] ë°”ì´íŠ¸ë¥¼ 처리하지 ì•Šê³  건너뛰었습니다. + +http2Protocol.jmxRegistration.fail=HTTP/2 í”„ë¡œí† ì½œì„ JMXì— ë“±ë¡í•˜ì§€ 못했습니다. + +pingManager.roundTripTime=ì—°ê²° [{0}]: ë¼ìš´ë“œ 트립 ì‹œê°„ì´ [{1}] 나노초(ns)ë¡œ 측정ë˜ì—ˆìŠµë‹ˆë‹¤. + +stream.clientCancel=ì‘ë‹µì´ ì™„ë£Œë˜ê¸° ì „ì— í´ë¼ì´ì–¸íŠ¸ê°€ ìŠ¤íŠ¸ë¦¼ì„ ë¦¬ì…‹í–ˆìŠµë‹ˆë‹¤. +stream.closed=ì—°ê²° [{0}], 스트림 [{1}], 한번 닫힌 ìŠ¤íŠ¸ë¦¼ì— ì“°ê¸°ë¥¼ í•  수 없습니다. +stream.header.case=ì—°ê²° [{0}], 스트림 [{1}], HTTP í—¤ë” ì´ë¦„ [{2}]ì€(는) 반드시 소문ìžì—¬ì•¼ 합니다. +stream.header.connection=ì—°ê²° [{0}], 스트림 [{1}], HTTP í—¤ë” [{2}]ì€ HTTP/2 요청ì—ì„œ 허용ë˜ì§€ 않습니다. +stream.header.contentLength=ì—°ê²° [{0}], 스트림 [{1}], 해당 Content-Length í—¤ë” ê°’ [{2}]ì€(는) ìˆ˜ì‹ ëœ ë°ì´í„°ì˜ í¬ê¸° [{3}]와(ê³¼) ì¼ì¹˜í•˜ì§€ 않습니다. +stream.header.debug=ì—°ê²° [{0}], 스트림 [{1}], HTTP í—¤ë”: [{2}], ê°’: [{3}] +stream.header.duplicate=ì—°ê²° [{0}], 스트림 [{1}], 여러 ê°œì˜ [{2}] í—¤ë”ë“¤ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +stream.header.empty=ì—°ê²° [{0}], 스트림 [{1}], í—¤ë” ì´ë¦„ì´ ë¹ˆ 문ìžì—´ì´ì–´ì„œ 유효하지 않습니다. +stream.header.invalid=ì—°ê²° [{0}], 스트림 [{1}], í—¤ë” [{2}]ì´(ê°€) 유효하지 ì•Šì€ ê°’ì„ í¬í•¨í–ˆìŠµë‹ˆë‹¤: [{3}] +stream.header.noPath=ì—°ê²° [{0}], 스트림 [{1}], [:path] ê°€ìƒ í—¤ë”ê°€ 비어 있었습니다. +stream.header.required=ì—°ê²° [{0}], 스트림 [{1}], 하나 ì´ìƒì˜ 필수 í—¤ë”ë“¤ì´ ì—†ìŠµë‹ˆë‹¤. +stream.header.te=ì—°ê²° [{0}], 스트림 [{1}], HTTP/2 요청ì—ì„œ, HTTP í—¤ë” [te]ì´(ê°€) ê°’ [{2}]ì„(를) 갖는 ê²ƒì€ í—ˆìš©ë˜ì§€ 않습니다. +stream.header.unexpectedPseudoHeader=ì—°ê²° [{0}], 스트림 [{1}], ì •ê·œ í—¤ë” ë‹¤ìŒì— ê°€ìƒ í—¤ë” [{2}]ì„(를) 받았습니다. +stream.header.unknownPseudoHeader=ì—°ê²° [{0}], 스트림 [{1}], ì•Œ 수 없는 ê°€ìƒ í—¤ë” [{2}]ì„(를) 받았습니다. +stream.host.inconsistent=ì—°ê²° [{0}], 스트림 [{1}], 호스트 í—¤ë” ê°’ [{2}](ì´)ê°€ ì´ì „ì— ì œê³µë˜ì—ˆë˜ 호스트 [{3}], í¬íŠ¸ë²ˆí˜¸ [{4}] ê°’ë“¤ì— ë¹„êµí•˜ì—¬ ì¼ê´€ëœ ê°’ì´ ì•„ë‹™ë‹ˆë‹¤. +stream.inputBuffer.copy=[{0}] ë°”ì´íŠ¸ë¥¼ inBufferì—ì„œ outBufferë¡œ 복사합니다. +stream.inputBuffer.dispatch=readInterestê°€ 등ë¡ë  ë•Œ, ë°ì´í„°ê°€ inBufferì— ì¶”ê°€ë˜ì—ˆìŠµë‹ˆë‹¤. ì½ê¸° 디스패치를 개시합니다. +stream.inputBuffer.empty=ìŠ¤íŠ¸ë¦¼ì˜ ìž…ë ¥ 버í¼ê°€ 비어 있습니다. ë” ë§Žì€ ë°ì´í„°ë¥¼ 기다립니다. +stream.inputBuffer.readTimeout=í´ë¼ì´ì–¸íŠ¸ë¡œë¶€í„° ë°ì´í„°ë¥¼ ì½ê¸°ë¥¼ ì¼ì • 시간 ë™ì•ˆ 기다리는 중입니다. +stream.inputBuffer.reset=ìŠ¤íŠ¸ë¦¼ì´ ìž¬ì„¤ì •(reset)ë˜ì—ˆìŠµë‹ˆë‹¤. +stream.inputBuffer.signal=ì½ê¸° 쓰레드가 대기하는 ë™ì•ˆ inBufferì— ë°ì´í„°ê°€ 추가ë˜ì—ˆìŠµë‹ˆë‹¤. 해당 쓰레드가 ì½ê¸°ë¥¼ 계ì†í•˜ë„ë¡ ì‹œê·¸ë„ì„ ë³´ëƒ…ë‹ˆë‹¤. +stream.inputBuffer.swallowUnread=ì´ì „ì— ì½ì–´ ìž…ë ¥ 스트림 버í¼ì— 넣어진 [{0}] ë°”ì´íŠ¸ë“¤ì„ 무시합니다. +stream.notWritable=ì—°ê²° [{0}], 스트림 [{1}], ì´ ìŠ¤íŠ¸ë¦¼ì€ ì“°ê¸° 가능하지 않습니다. +stream.outputBuffer.flush.debug=ì—°ê²° [{0}], 스트림 [{1}], 위치 [{2}]ì˜ ë²„í¼ë¥¼ 출력으로 배출합니다. 쓰기 진행 중 여부: [{3}],닫힘 여부: [{4}] +stream.recycle=ì—°ê²° [{0}], 스트림 [{1}]ì´(ê°€) 참조 í•´ì œë˜ì—ˆìŠµë‹ˆë‹¤. +stream.reset.fail=ì—°ê²° [{0}], 스트림 [{1}], ìŠ¤íŠ¸ë¦¼ì„ ìž¬ì„¤ì •(reset)하지 못했습니다. +stream.reset.receive=ì—°ê²° [{0}], 스트림 [{1}], [{2}](으)ë¡œ ì¸í•´ 재설정(reset)ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +stream.reset.send=ì—°ê²° [{0}], 스트림 [{1}], [{2}](으)ë¡œ ì¸í•˜ì—¬ 재설정(reset)ì´ ì „ì†¡ë˜ì—ˆìŒ. +stream.trailerHeader.noEndOfStream=ì—°ê²° [{0}], 스트림 [{1}], Trailer í—¤ë”ë“¤ì´ ìŠ¤íŠ¸ë¦¼ì˜ ë 플래그를 í¬í•¨í•˜ì§€ 않았습니다. +stream.writeTimeout=스트림 ë°ì´í„°ê°€ 쓰여지ë„ë¡ í—ˆìš©í•˜ê¸° 위한 í름 제어 (flow control) 윈ë„우를, í´ë¼ì´ì–¸íŠ¸ê°€ ì¦ê°€ì‹œí‚¤ê¸°ë¥¼ ì¼ì • 시간 ë™ì•ˆ 기다리는 중입니다. + +streamProcessor.cancel=ì—°ê²° [{0}], 스트림 [{1}], ìš”ì²­ì˜ bodyê°€ 완전히 ì½ížˆì§€ ì•Šê³  남아 있어, ë” ì´ìƒ ë°ì´í„°ëŠ” 불필요합니다. +streamProcessor.error.connection=ì—°ê²° [{0}], 스트림 [{1}]: 처리 중 해당 ì—°ê²°ì— ì‹¬ê°í•œ 오류 ë°œìƒ +streamProcessor.error.stream=ì—°ê²° [{0}], 스트림 [{1}], 처리 중 ìŠ¤íŠ¸ë¦¼ì— ì¹˜ëª…ì ì¸ 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. +streamProcessor.flushBufferedWrite.entry=ì—°ê²° [{0}], 스트림 [{1}], 버í¼ì— 쓰여진 ë°ì´í„°ë¥¼ 배출합니다. +streamProcessor.service.error=요청 처리 중 오류 ë°œìƒ + +streamStateMachine.debug.change=ì—°ê²° [{0}], 스트림 [{1}], ìƒíƒœê°€ [{2}]ì—ì„œ [{3}](으)ë¡œ 변경ë¨. +streamStateMachine.invalidFrame=ì—°ê²° [{0}], 스트림 [{1}], ìƒíƒœ [{2}], 프레임 타입 [{3}] + +upgradeHandler.allocate.debug=ì—°ê²° [{0}], 스트림 [{1}], [{2}] ë°”ì´íŠ¸ë¥¼ 할당함. +upgradeHandler.allocate.left=ì—°ê²° [{0}], 스트림 [{1}], [{2}] ë°”ì´íŠ¸ë“¤ì´ 할당 í•´ì œë˜ì—ˆìŠµë‹ˆë‹¤ - ìžì‹ë“¤ì— 할당하려 ì‹œë„합니다. +upgradeHandler.connectionError=ì—°ê²° 오류 +upgradeHandler.fallToDebug=\n\ +\ 주ì˜: 추가로 ë°œìƒí•˜ëŠ” HTTP/2 스트림 ì˜¤ë¥˜ë“¤ì€ ë””ë²„ê·¸ ìˆ˜ì¤€ì˜ ë¡œê·¸ë¡œ 기ë¡ë  것입니다. +upgradeHandler.goaway.debug=ì—°ê²° [{0}], Goaway, 마지막 스트림 [{1}], 오류 코드 [{2}], 디버그 ë°ì´í„° [{3}] +upgradeHandler.init=ì—°ê²° [{0}], ìƒíƒœ [{1}] +upgradeHandler.invalidPreface=ì—°ê²° [{0}]: 유효하지 ì•Šì€ ì—°ê²° preface +upgradeHandler.ioerror=ì—°ê²° [{0}] +upgradeHandler.noAllocation=ì—°ê²° [{0}], 스트림 [{1}], ì—°ê²° í• ë‹¹ì„ ìœ„í•´ 대기하는 중 제한 시간 초과 ë˜ì—ˆìŠµë‹ˆë‹¤. +upgradeHandler.noNewStreams=ì—°ê²° [{0}], 스트림 [{1}], ì´ ì—°ê²°ì—는 새로운 ìŠ¤íŠ¸ë¦¼ë“¤ì´ í—ˆìš©ë˜ì§€ 않기ì—, ìŠ¤íŠ¸ë¦¼ì´ ë¬´ì‹œë˜ì—ˆìŠµë‹ˆë‹¤. +upgradeHandler.overheadChange=ì—°ê²° [{0}], 스트림 [{1}], 프레임 타입 [{2}](ì´)ê°€ 새로운 오버헤드 프레임 수 [{3}](ì„)를 야기했습니다. +upgradeHandler.pause.entry=ì—°ê²° [{0}]ì´(ê°€) ì¼ì‹œ 정지 중 +upgradeHandler.pingFailed=ì—°ê²° [{0}]: í´ë¼ì´ì–¸íŠ¸ì— ping 메시지를 보내지 못했습니다. +upgradeHandler.prefaceReceived=ì—°ê²° [{0}]: ì—°ê²° preface를 í´ë¼ì´ì–¸íŠ¸ë¡œë¶€í„° 받았습니다. +upgradeHandler.pruneIncomplete=ì—°ê²° [{0}]: ìŠ¤íŠ¸ë¦¼ë“¤ì´ Priority treeì—ì„œ 활성화ë˜ì–´ 있거나 사용ë˜ê³  있기 때문ì—, 해당 ì—°ê²°ì„ ì™„ì „ížˆ 제거하지 못했습니다. 너무 ë§Žì€ ìŠ¤íŠ¸ë¦¼ë“¤ì´ ì¡´ìž¬í•©ë‹ˆë‹¤: [{2}]. +upgradeHandler.pruneStart=ì—°ê²° [{0}]: ì´ì „ ìŠ¤íŠ¸ë¦¼ë“¤ì— ëŒ€í•œ 가지치기를 시작합니다. í•œê³„ê°’ì€ [{1}] ì´ê³ , 현재 [{2}]ê°œì˜ ìŠ¤íŠ¸ë¦¼ë“¤ì´ ì¡´ìž¬í•©ë‹ˆë‹¤. +upgradeHandler.pruned=ì—°ê²° [{0}]ì´(ê°€) ì™„ë£Œëœ ìŠ¤íŠ¸ë¦¼ [{1}]ì„(를) 제거했습니다. +upgradeHandler.releaseBacklog=ì—°ê²° [{0}], 스트림 [{1}]ì´(ê°€) 백로그로부터 í•´ì œë˜ì—ˆìŠµë‹ˆë‹¤. +upgradeHandler.reset.receive=ì—°ê²° [{0}], 스트림 [{1}], [{2}](으)ë¡œ ì¸í•´ ë¦¬ì…‹ì„ ìˆ˜ì‹ í–ˆìŠµë‹ˆë‹¤. +upgradeHandler.rst.debug=ì—°ê²° [{0}], 스트림 [{1}], 오류 [{2}], 메시지 [{3}], RST (ìŠ¤íŠ¸ë¦¼ì„ ë‹«ìŠµë‹ˆë‹¤) +upgradeHandler.sendPrefaceFail=ì—°ê²° [{0}]: í´ë¼ì´ì–¸íŠ¸ì— preface를 전송하지 못했습니다. +upgradeHandler.sendfile.reservation=sendfile 쓰기 ì§ì „입니다. ì—°ê²° [{0}], 스트림 [{1}], ì—°ê²° 예약 [{2}], ì—°ê²° 예약 [{3}] +upgradeHandler.socketCloseFailed=ì†Œì¼“ì„ ë‹«ëŠ” 중 오류 ë°œìƒ +upgradeHandler.startRequestBodyFrame.result=ì—°ê²° [{0}], 스트림 [{1}]: startRequestBodyFrame()ì´ [{2}]ì„(를) 반환함. +upgradeHandler.stream.closed=스트림 [{0}]ì´(ê°€) 얼마 ë™ì•ˆ ì´ë¯¸ 닫혀 있었습니다. +upgradeHandler.stream.error=ì—°ê²° [{0}], 스트림 [{1}] (ì´)ê°€ 오류로 닫혔습니다. +upgradeHandler.stream.even=[{0}]ì˜ ìƒˆë¡œìš´ ì›ê²© 스트림 IDê°€ 요청ë˜ì—ˆìœ¼ë‚˜, 모든 ì›ê²© ìŠ¤íŠ¸ë¦¼ì€ ë°˜ë“œì‹œ í™€ìˆ˜ì˜ ID를 사용해야 합니다. +upgradeHandler.stream.notWritable=ì—°ê²° [{0}], 스트림 [{1}], ì´ ìŠ¤íŠ¸ë¦¼ì€ ì“°ê¸° 가능하지 않습니다. +upgradeHandler.stream.old=새로운 ì›ê²© 스트림 ID [{0}]ì´(ê°€) 요청ë˜ì—ˆì§€ë§Œ, 가장 ìµœê·¼ì˜ ìŠ¤íŠ¸ë¦¼ì€ [{1}]ì´ì—ˆìŠµë‹ˆë‹¤. +upgradeHandler.tooManyRemoteStreams=í´ë¼ì´ì–¸íŠ¸ê°€, í™œì„±í™”ëœ ìŠ¤íŠ¸ë¦¼ë“¤ì„ [{0}]개를 초과하여 사용하려 ì‹œë„했습니다. +upgradeHandler.tooMuchOverhead=ì—°ê²° [{0}]: 너무 ë§Žì€ ì˜¤ë²„í—¤ë“œë¡œ ì¸í•˜ì—¬ ì—°ê²°ì´ ë‹«íž ê²ƒìž…ë‹ˆë‹¤. +upgradeHandler.unexpectedAck=ì—°ê²° [{0}], 스트림 [{1}], 예기치 ì•Šì€ ìƒí™©ì—ì„œ settings acknowledgement를 받았습니다. +upgradeHandler.upgrade=ì—°ê²° [{0}]: HTTP/1.1ì´ ìŠ¤íŠ¸ë¦¼ [1](으)ë¡œ 업그레ì´ë“œë©ë‹ˆë‹¤. +upgradeHandler.upgrade.fail=ì—°ê²° [{0}], HTTP/1.1 업그레ì´ë“œ 실패 +upgradeHandler.upgradeDispatch.entry=엔트리, ì—°ê²° [{0}], SocketStatus [{1}] +upgradeHandler.upgradeDispatch.exit=Exit, ì—°ê²° [{0}], SocketState [{1}] +upgradeHandler.windowSizeReservationInterrupted=ì—°ê²° [{0}], 스트림 [{1}], 예비하려 í•œ ë°”ì´íŠ¸ í¬ê¸°: [{2}] +upgradeHandler.windowSizeTooBig=ì—°ê²° [{0}], 스트림 [{1}], 윈ë„ìš° í¬ê¸°ê°€ 너무 í½ë‹ˆë‹¤. +upgradeHandler.windowUpdateConnection=ì—°ê²° [{0}], 윈ë„ìš° í¬ê¸°ë¥¼ [{1}] ë°”ì´íŠ¸ ë§Œí¼ ëŠ˜ë¦¬ë©°, í´ë¼ì´ì–¸íŠ¸ì—게 윈ë„ìš° 변경 í”„ë ˆìž„ì„ ì „ì†¡í–ˆìŠµë‹ˆë‹¤. +upgradeHandler.windowUpdateStream=ì—°ê²° [{0}], 스트림 [{1}], 윈ë„ìš° í¬ê¸°ë¥¼ [{2}] ë°”ì´íŠ¸ ë§Œí¼ ëŠ˜ë¦¬ë©°, í´ë¼ì´ì–¸íŠ¸ì—게 윈ë„ìš° 변경 í”„ë ˆìž„ì„ ì „ì†¡í–ˆìŠµë‹ˆë‹¤. +upgradeHandler.writeBody=ì—°ê²° [{0}], 스트림 [{1}], ë°ì´í„° ê¸¸ì´ [{2}] +upgradeHandler.writeHeaders=ì—°ê²° [{0}], 스트림 [{1}] +upgradeHandler.writePushHeaders=ì—°ê²° [{0}], 스트림 [{1}], Pushëœ ìŠ¤íŠ¸ë¦¼ [{2}], EndOfStream [{3}] + +windowAllocationManager.dispatched=ì—°ê²° [{0}], 스트림 [{1}]ì— ë””ìŠ¤íŒ¨ì¹˜ë©ë‹ˆë‹¤. +windowAllocationManager.notified=ì—°ê²° [{0}], 스트림 [{1}]ì— í†µì§€ë©ë‹ˆë‹¤. +windowAllocationManager.notify=ì—°ê²° [{0}], 스트림 [{1}], 대기 타입 [{2}], 통지 타입 [{3}] +windowAllocationManager.waitFor.connection=ì—°ê²° [{0}], 스트림 [{1}], 제한 시간 [{2}] ë‚´ì—ì„œ, ì—°ê²° í름 제어 윈ë„ìš°(blocking)를 대기합니다. +windowAllocationManager.waitFor.ise=ì—°ê²° [{0}], 스트림 [{1}], ì´ë¯¸ 대기 중입니다. +windowAllocationManager.waitFor.stream=ì—°ê²° [{0}], 스트림 [{1}], 제한 시간 [{2}] ë‚´ì—ì„œ, 스트림 í름 제어 윈ë„ìš°(blocking)를 대기합니다. +windowAllocationManager.waitForNonBlocking.connection=ì—°ê²° [{0}], 스트림 [{1}], ì—°ê²° í름 제어 윈ë„ìš°(non-blocking)를 대기합니다. +windowAllocationManager.waitForNonBlocking.stream=ì—°ê²° [{0}], 스트림 [{1}], 스트림 í름 제어 윈ë„ìš°(non-blocking)를 대기합니다. + +writeStateMachine.endWrite.ise=쓰기가 한번 완료ë˜ê³  나면, 새로운 ìƒíƒœë¥¼ 위해 [{0}]ì„(를) 지정하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +writeStateMachine.ise=[{1}]ì¸ ìƒíƒœì—ì„œ [{0}()]ì„(를) 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. diff --git a/java/org/apache/coyote/http2/LocalStrings_pt_BR.properties b/java/org/apache/coyote/http2/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..dcf1879 --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_pt_BR.properties @@ -0,0 +1,20 @@ +# 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. + +hpack.invalidCharacter=Caracter Unicode [{0}] no code point [{1}] não pode ser codificado porque está fora do limite (de 0 a 255) + +http2Parser.headers.wrongStream=Conexão [{0}], cabeçalhos em progresso para fluxo [{1}] mas um pacote para fluxo [{2}] foi recebido + +upgradeHandler.pingFailed=Conexão [{0}] falhou ao enviar ping para cliente diff --git a/java/org/apache/coyote/http2/LocalStrings_ru.properties b/java/org/apache/coyote/http2/LocalStrings_ru.properties new file mode 100644 index 0000000..6e505ea --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_ru.properties @@ -0,0 +1,20 @@ +# 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. + +http2Parser.processFrameData.window=Соединение [{0}], Клиент поÑлал больше данных чем разрешено окном потока + +upgradeHandler.ioerror=Соединение [{0}] +upgradeHandler.pingFailed=Соединение [{0}], ошибка при передаче ping''а клиенту +upgradeHandler.socketCloseFailed=Ошибка при закрытии Ñокета diff --git a/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties b/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..41f4306 --- /dev/null +++ b/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties @@ -0,0 +1,179 @@ +# 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. + +abstractStream.setConnectionAllocationMade=连接 [{0}]ï¼Œæµ [{1}],连接分é…从 [{2}] 更改为 [{3}] +abstractStream.setConnectionAllocationRequested=连接 [{0}]ï¼Œæµ [{1}],请求的连接分é…从 [{2}] 更改为 [{3}] +abstractStream.windowSizeDec=连接[{0}],æµ[{1}],将æµæŽ§åˆ¶çª—å£å‡å°‘[{2}]到[{3}] +abstractStream.windowSizeInc=连接 [{0}], æµ [{1}], 增加æµé‡æŽ§åˆ¶çª—å£[{2}] 到 [{3}] +abstractStream.windowSizeTooBig=连接[{0}],æµ[{1}],窗å£å¤§å°ä»Ž[{2}]增加到[{3}],超过了å…许的最大值 + +connectionPrefaceParser.eos=读å–打开客户端åºå­—节åºåˆ—时出现æ„外的æµç»“尾。åªè¯»å–了[{0}]个字节。 +connectionPrefaceParser.mismatch=请求了新的远程æµID[{0}],但所有远程æµéƒ½å¿…须使用奇数标识符。 + +connectionSettings.debug=连接[{0}],å‚数类型[{1}]设置为[{2}] +connectionSettings.enablePushInvalid=连接[{0}],请求的enable push[{1}]值ä¸æ˜¯å…许的值之一(零或一) +connectionSettings.headerTableSizeLimit=连接 [{0}],å°è¯•å°† header 表大å°è®¾ç½®ä¸º [{1}],但é™åˆ¶ä¸º 16k +connectionSettings.maxFrameSizeInvalid=连接[{0}],请求的最大帧大å°[{1}]在[{2}]到[{3}]çš„å…许范围之外 +connectionSettings.unknown=连接[{0}],标识为[{1}]和值为[{2}]的未知设置被忽略 +connectionSettings.windowSizeTooBig=连接[{0}],请求窗å£å¤§å°[{0}],大于最大å…许的值[{1}] + +frameType.checkPayloadSize=对帧类型[{1}]æ¥è¯´ï¼Œè´Ÿè½½[{0}]是无效的 +frameType.checkStream=无效的帧类型[{0}] + +hpack.integerEncodedOverTooManyOctets=HPACK å¯å˜é•¿åº¦æ•´æ•°ç¼–ç è¿‡å¤šçš„å…«ä½å­—节,最大值为[{0}] +hpack.invalidCharacter=代ç ç‚¹[{1}]处的Unicode字符[{0}]无法编ç ï¼Œå› ä¸ºå®ƒè¶…出了å…许的0到255范围。 + +hpackEncoder.encodeHeader=ç¼–ç å¤´[{0}],值为[{1}] + +hpackdecoder.addDynamic=正在将头添加到å为[{1}]和值[{2}]动æ€è¡¨çš„索引[{0}] +hpackdecoder.clearDynamic=清空动æ€è¡¨ +hpackdecoder.emitHeader=正在å‘出å为[{0}]值为[{1}]的头 +hpackdecoder.headerTableIndexInvalid=头部表索引[{0}]无效,因为有[{1}]个é™æ€å®žä¾‹å’Œ[{2}]个动æ€å®žä¾‹ +hpackdecoder.maxMemorySizeExceeded=头表大å°[{0}]超过了最大大å°[{1}] +hpackdecoder.notImplemented=尚未实施 +hpackdecoder.nullHeader=索引[{0}]处的头为空 +hpackdecoder.tableSizeUpdateNotAtStart=任何表大å°çš„更新都必须在头å—开始时å‘é€ã€‚ +hpackdecoder.useDynamic=从动æ€è¡¨çš„索引[{0}]使用头 +hpackdecoder.useStatic=从é™æ€è¡¨çš„索引[{0}]使用头 +hpackdecoder.zeroNotValidHeaderTableIndex=零ä¸æ˜¯æœ‰æ•ˆçš„头表索引 + +hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS=HPACK头中的Huffmanç¼–ç å€¼æ²¡æœ‰ä»¥EOSå¡«å……ç»“æŸ +hpackhuffman.stringLiteralEOS=包å«EOS符å·çš„HPACK头中的Huffmanç¼–ç å€¼ +hpackhuffman.stringLiteralTooMuchPadding=超过7个bitsçš„EOSå¡«å……æ供了在一个éœå¤«æ›¼ç¼–ç å­—ç¬¦ä¸²çš„ç»“æŸ + +http2Parser.error=Connection [{0}],Stream [{1}], 框架类型 [{2}], 错误 +http2Parser.headerLimitCount=连接[{0}],æµ[{1}],标题太多 +http2Parser.headerLimitSize=连接[{0}],Stream[{1}],总的头信æ¯å°ºå¯¸å¤ªå¤§ +http2Parser.headers.wrongFrameType=连接[{0}],正在处ç†æµ[{1}]的头,但收到了类型为[{2}]的帧 +http2Parser.headers.wrongStream=连接[{0}], 头部信æ¯å¯¹äºŽæµ[{1}]正在进行但对于æµ[{2}]的一帧已ç»æ”¶åˆ°äº†ã€‚ +http2Parser.invalidBuffers=åº”ä½¿ç”¨ä¸¤ä¸ªç¼“å†²åŒºè¿›è¡Œè¯»å– +http2Parser.nonZeroPadding=连接[{0}],æµ[{1}],éžé›¶å¡«å…… +http2Parser.payloadTooBig=有效负载是[{0}]字节长,但最大帧大å°æ˜¯[{1}]。 +http2Parser.preface.invalid=出现无效连接 +http2Parser.preface.io=无法读å–连接å‰è¨€ +http2Parser.processFrame=连接[{0}]ã€æµ[{1}]ã€å¸§ç±»åž‹[{2}]ã€æ ‡å¿—[{3}]ã€è´Ÿè½½å¤§å°[{4}] +http2Parser.processFrame.tooMuchPadding=连接[{0}],æµ[{1}],填充长度[{2}]对于负载[{3}]太大 +http2Parser.processFrame.unexpectedType=需è¦å¸§ç±»åž‹[{0}],但收到帧类型[{1}] +http2Parser.processFrameContinuation.notExpected=连接[{0}],当没有头正在进行时,为æµ[{1}]接收到连续帧。 +http2Parser.processFrameData.lengths=连接[{0}],æµ[{1}],数æ®é•¿åº¦ï¼Œ[{2}],填充长度[{3}] +http2Parser.processFrameData.window=连接[{0}],客户端å‘é€çš„æ•°æ®æ¯”æµçª—å£å…许的多 +http2Parser.processFrameHeaders.decodingDataLeft=æ•°æ®åœ¨HPACK解ç åŽä¾ç„¶ä¿ç•™ - 它本应该被消费掉 +http2Parser.processFrameHeaders.decodingFailed=对HTTP头进行HPACK解ç æ—¶å‡ºé”™ +http2Parser.processFrameHeaders.payload=连接:[{0}],æµï¼š[{1}],正在处ç†[{1}]大å°çš„头文件负载 +http2Parser.processFramePushPromise=请求了新的远程æµID[{0}],但所有远程æµéƒ½å¿…须使用奇数标识符 +http2Parser.processFrameSettings.ackWithNonZeroPayload=接收到带有ACK标志设置和有效负载的设置帧 +http2Parser.processFrameWindowUpdate.debug=连接[{0}],æµ[{1}],窗å£å¤§å°å¢žé‡[{2}] +http2Parser.processFrameWindowUpdate.invalidIncrement=接收到的窗å£æ›´æ–°å¸§å…·æœ‰æ— æ•ˆçš„增é‡å¤§å°[0] +http2Parser.swallow.debug=连接:[{0}],æµï¼š[{1}],åžä¸‹[{2}]字节 + +http2Protocol.jmxRegistration.fail=HTTP/2å议注册JMX失败 + +pingManager.roundTripTime=连接[{0}]往返时间测é‡ä¸º[{1}]ns + +stream.clientCancel=客户端在å“应完æˆå‰é‡ç½®äº†æ•°æ®æµ +stream.closed=连接[{0}],æµ[{1}]ï¼Œä¸€æ—¦å…³é—­å°±æ— æ³•å†™å…¥æµ +stream.header.case=连接[{0}],æµ[{1}],HTTP标头å称[{2}]å¿…é¡»å°å†™ +stream.header.connection=HTTP/2请求中ä¸å…许连接[{0}]ã€æµ[{1}]ã€HTTP头[{2}] +stream.header.contentLength=连接[{0}],æµ[{1}],内容长度头值[{2}]与接收的数æ®å¤§å°[{3}]ä¸ä¸€è‡´ +stream.header.debug=连接[{0}],æµ[{1}],HTTP标头[{2}],值[{3}] +stream.header.duplicate=连接[{0}],æµ[{1}],收到多个[{2}]头 +stream.header.empty=连接[{0}],æµ[{1}],无效的空头å称 +stream.header.invalid=连接[{0}],æµ[{1}],头[{2}]包å«æ— æ•ˆå€¼[{3}] +stream.header.noPath=连接[{0}],æµ[{1}],[:path]伪标头为空 +stream.header.required=连接 [{0}], æµ [{1}], 缺少一个或多个必è¦çš„头文件 +stream.header.te=连接[{0}]ã€æµ[{1}]ã€HTTP头[te]在HTTP/2请求中ä¸å…许有值[{2}] +stream.header.unexpectedPseudoHeader=连接[{0}],æµ[{1}],伪头[{2}]在常规头之åŽæŽ¥æ”¶ +stream.header.unknownPseudoHeader=收到连接[{0}],æµ[{1}],未知伪标头[{2}] +stream.host.inconsistent=连接 [{0}]ï¼Œæµ [{1}],主机头 [{2}] 与之å‰ä¸ºä¸»æœº [{3}] ç«¯å£ [{4}] æ供的值ä¸ä¸€è‡´ +stream.inputBuffer.copy=正在将[{0}]字节从inBufferå¤åˆ¶åˆ°outBuffer +stream.inputBuffer.dispatch=注册读å–兴趣时将数æ®æ·»åŠ åˆ°inBuffer中。触å‘读å–分派 +stream.inputBuffer.empty=æµè¾“å…¥ç¼“å†²åŒºä¸ºç©ºã€‚ç­‰å¾…æ›´å¤šæ•°æ® +stream.inputBuffer.readTimeout=等待从客户端读å–æ•°æ®è¶…æ—¶ +stream.inputBuffer.reset=æµ.é‡ç½® +stream.inputBuffer.signal=读线程在等待时,数æ®è¢«æ·»åŠ åˆ°inBuffer中。 å‘ä¿¡å·é€šçŸ¥è¯¥çº¿ç¨‹ç»§ç»­ +stream.inputBuffer.swallowUnread=å…ˆå‰è¯»å–到输入æµç¼“冲区åžå…¥äº†[{0}]个字节 +stream.notWritable=连接[{0}],æµ[{1}],此æµä¸å¯å†™ +stream.outputBuffer.flush.debug=连接[{0}],æµ[{1}],用缓冲区在ä½ç½®[{2}]刷新输出,writeInProgress[{3}]并关闭了[{4}] +stream.recycle=连接[{0}],æµ[{1}]将被回收 +stream.reset.fail=连接[{0}],æµ[{1}],é‡ç½®æµå¤±è´¥ +stream.reset.receive=连接[{0}],æµ[{1}],由于[{2}]而收到é‡ç½® +stream.reset.send=连接[{0}],æµ[{1}],由于[{2}]å°†é‡ç½®å‘é€ +stream.trailerHeader.noEndOfStream=连接[{0}],æµ[{1}],尾部标头ä¸åŒ…括æµç»“æŸæ ‡å¿— +stream.writeTimeout=等待客户端增加æµæŽ§åˆ¶çª—å£ä»¥å…许写入æµæ•°æ®çš„超时 + +streamProcessor.cancel=连接到[{0}],Stream [{1}], +streamProcessor.error.connection=连接[{0}],Stream[{0}],处ç†ä¸­å‘生错误,对连接æ¥è¯´æ˜¯è‡´å‘½çš„。 +streamProcessor.error.stream=连接[{0}],æµ[{1}],处ç†è¿‡ç¨‹ä¸­å‘生对æµè‡´å‘½çš„错误 +streamProcessor.flushBufferedWrite.entry=连接[{0}],æµ[{1}],正在刷新缓冲写入 +streamProcessor.service.error=请求处ç†æœŸé—´å‡ºé”™ + +streamStateMachine.debug.change=连接[{0}],æµ[{1}],状æ€ä»Ž[{2}]更改为[{3}] +streamStateMachine.invalidFrame=连接[{0}]ã€æµ[{1}]ã€çŠ¶æ€[{2}]ã€å¸§ç±»åž‹[{3}] + +upgradeHandler.allocate.debug=连接[{0}],æµ[{1}],已分é…[{2}]字节 +upgradeHandler.allocate.left=连接[{0}],æµ[{1}],[{2}]å­—èŠ‚æœªåˆ†é… - å°è¯•åˆ†é…ç»™å­é¡¹ +upgradeHandler.connectionError=连接错误 +upgradeHandler.fallToDebug=注æ„:往åŽå‡ºçŽ° HTTP/2 æµçš„错误将以 DEBUG 日志级别输出。 +upgradeHandler.goaway.debug=连接[{0}],离开,最åŽçš„æµ[{1}],错误ç [{2}],调试数æ®[{3}] +upgradeHandler.init=连接[{0}],状æ€[{1}] +upgradeHandler.invalidPreface=连接[{0}],连接å‰è¨€æ— æ•ˆ +upgradeHandler.ioerror=连接[{0}] +upgradeHandler.noAllocation=连接[{0}],æµ[{1}],等待分é…超时 +upgradeHandler.noNewStreams=连接[{0}],æµ[{1}],忽略æµï¼Œå› ä¸ºæ­¤è¿žæŽ¥ä¸Šä¸å…è®¸æœ‰æ–°çš„æµ +upgradeHandler.overheadChange=连接[{0}]ã€æµ[{1}]ã€å¸§ç±»åž‹[{2}]产生新的系统开销数[{3}] +upgradeHandler.pause.entry=连接[{0}]æ­£åœ¨æš‚åœ +upgradeHandler.pingFailed=连接[{0}]对客户端å‘é€ping失败. +upgradeHandler.prefaceReceived=连接[{0}],从客户端收到连接准备。 +upgradeHandler.pruneIncomplete=连接[{0}],æµ[{1}],无法完全修剪连接,因为有[{2}]个活动æµå¤ªå¤š +upgradeHandler.pruneStart=连接[{0}]正在开始修剪旧æµã€‚é™åˆ¶ä¸º[{1}],当å‰æœ‰[{2}]个æµã€‚ +upgradeHandler.pruned=连接[{0}]已修剪完æˆçš„æµ[{1}] +upgradeHandler.releaseBacklog=连接[{0}],æµ[{1}]已从待办事项列表中释放 +upgradeHandler.reset.receive=连接[{0}],æµ[{1}],由于[{2}]而é‡ç½® +upgradeHandler.rst.debug=连接[{0}],æµ[{1}],错误[{2}],消æ¯[{3}],RST(关闭æµï¼‰ +upgradeHandler.sendPrefaceFail=连接[{0}],给客户端å‘é€å‰è¨€å¤±è´¥ +upgradeHandler.sendfile.reservation=在å‘é€æ–‡ä»¶å†™å…¥ä¹‹å‰çš„连接 [{0}]ã€æµ [{1}]ã€è¿žæŽ¥é¢„ç•™ [{2}]ã€æµé¢„ç•™ [{3}] +upgradeHandler.socketCloseFailed=关闭 socket 错误 +upgradeHandler.startRequestBodyFrame.result=连接[{0}],æµ[{1}]startRequestBodyFrame返回[{2}] +upgradeHandler.stream.closed=æµ[{0}]å·²ç»å…³é—­äº†ä¸€æ®µæ—¶é—´ +upgradeHandler.stream.error=连接[{0}],æµ[{1}]由于错误被关闭 +upgradeHandler.stream.even=请求了新的远程æµID[{0}],但所有远程æµéƒ½å¿…须使用奇数标识符 +upgradeHandler.stream.notWritable=连接[{0}],æµ[{1}],此æµä¸å¯å†™ã€‚ +upgradeHandler.stream.old=请求了新的远程æµID [{0}],但最近的æµæ˜¯[{1}] +upgradeHandler.tooManyRemoteStreams=客户端试图使用超过[{0}]个活动æµã€‚ +upgradeHandler.tooMuchOverhead=连接[{0}],开销过大,连接将关闭 +upgradeHandler.unexpectedAck=连接[{0}],æµ[{1}],收到一个éžé¢„期的设置确认 +upgradeHandler.upgrade=连接[{0}], HTTP/1.1 å‡çº§åˆ°æµ[1] +upgradeHandler.upgrade.fail=):连接[{0}],http/1.1å‡çº§å¤±è´¥ +upgradeHandler.upgradeDispatch.entry=æ¡ç›®ï¼Œè¿žæŽ¥[{0}],SocketStatus [{1}] +upgradeHandler.upgradeDispatch.exit=退出,连接[{0}], SocketState[{1}] +upgradeHandler.windowSizeReservationInterrupted=连接[{0}],æµ[{1}],ä¿ç•™[{2}]字节 +upgradeHandler.windowSizeTooBig=连接[{0}],æµ[{1}],窗å£å¤ªå¤§ +upgradeHandler.windowUpdateConnection=连接[{0}],å‘客户端å‘é€äº†çª—å£æ›´æ–°,将窗å£å¢žåŠ [{1}]字节 +upgradeHandler.windowUpdateStream=连接[{0}],æµ[{1}]å‘客户端å‘é€äº†çª—å£æ›´æ–°,将窗å£å¢žåŠ [{2}]字节 +upgradeHandler.writeBody=连接 [{0}],æ•°æ®æµ[{1}], æ•°æ®é•¿åº¦[{2}] +upgradeHandler.writeHeaders=连接[{0}],æµ[{1}],正在写入头信æ¯ï¼ŒEndOfStream[{2}] +upgradeHandler.writePushHeaders=连接[{0}]ã€æµ[{1}]ã€æŽ¨é€æµ[{2}]ã€EndOfStream[{3}] + +windowAllocationManager.dispatched=连接[{0}],æµ[{1}],已调度 +windowAllocationManager.notified=连接[{0}],æµ[{1}],已通知 +windowAllocationManager.notify=连接[{0}], æµ[{1}], 等待类型[{2}], 通知类型[{3}] +windowAllocationManager.waitFor.connection=连接[{0}],æµ[{1}],等待连接æµæŽ§åˆ¶çª—å£ï¼ˆé˜»å¡žï¼‰ï¼Œè¶…时为[{2}] +windowAllocationManager.waitFor.ise=连接[{0}], æµ[{1}], å·²ç»å‡†å¤‡å¥½ +windowAllocationManager.waitFor.stream=连接[{0}],æµ[{1}],等待æµæŽ§åˆ¶çª—å£ï¼ˆé˜»å¡žï¼‰ï¼Œè¶…时为[{2}] +windowAllocationManager.waitForNonBlocking.connection=连接[{0}],æµ[{1}],正在等待连接æµæŽ§åˆ¶çª—å£ï¼ˆéžé˜»å¡žï¼‰ +windowAllocationManager.waitForNonBlocking.stream=连接[{0}],æµ[{1}],正在等待æµæŽ§åˆ¶çª—å£ï¼ˆéžé˜»å¡žï¼‰ + +writeStateMachine.endWrite.ise=写入完æˆåŽï¼Œä¸ºæ–°çŠ¶æ€æŒ‡å®š[{0}]是éžæ³•çš„ +writeStateMachine.ise=处于 [{1}] 状æ€æ—¶è°ƒç”¨ [{0}()] 方法是éžæ³•çš„ diff --git a/java/org/apache/coyote/http2/RecycledStream.java b/java/org/apache/coyote/http2/RecycledStream.java new file mode 100644 index 0000000..c4c180a --- /dev/null +++ b/java/org/apache/coyote/http2/RecycledStream.java @@ -0,0 +1,69 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +/** + * Represents a closed stream in the priority tree. Used in preference to the full {@link Stream} as has much lower + * memory usage. + */ +class RecycledStream extends AbstractNonZeroStream { + + private final String connectionId; + private int remainingFlowControlWindow; + + RecycledStream(String connectionId, Integer identifier, StreamStateMachine state, int remainingFlowControlWindow) { + super(identifier, state); + this.connectionId = connectionId; + this.remainingFlowControlWindow = remainingFlowControlWindow; + } + + + @Override + String getConnectionId() { + return connectionId; + } + + + @Override + void incrementWindowSize(int increment) throws Http2Exception { + // NO-OP + } + + + @Override + void receivedData(int payloadSize) throws ConnectionException { + remainingFlowControlWindow -= payloadSize; + } + + + /** + * {@inheritDoc} + *

    + * This implementation will return an zero length ByteBuffer to trigger a flow control error if more DATA frame + * payload than the remaining flow control window is received for this recycled stream. + */ + @Override + ByteBuffer getInputByteBuffer() { + if (remainingFlowControlWindow < 0) { + return ZERO_LENGTH_BYTEBUFFER; + } else { + return null; + } + } +} diff --git a/java/org/apache/coyote/http2/SendfileData.java b/java/org/apache/coyote/http2/SendfileData.java new file mode 100644 index 0000000..63f178d --- /dev/null +++ b/java/org/apache/coyote/http2/SendfileData.java @@ -0,0 +1,33 @@ +/* + * 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.coyote.http2; + +import java.nio.MappedByteBuffer; +import java.nio.file.Path; + +class SendfileData { + Path path; + Stream stream; + // Note: a mapped buffer is a special construct with an underlying file + // that doesn't need to be closed + MappedByteBuffer mappedBuffer; + long left; + int streamReservation; + int connectionReservation; + long pos; + long end; +} \ No newline at end of file diff --git a/java/org/apache/coyote/http2/Setting.java b/java/org/apache/coyote/http2/Setting.java new file mode 100644 index 0000000..bf70202 --- /dev/null +++ b/java/org/apache/coyote/http2/Setting.java @@ -0,0 +1,72 @@ +/* + * 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.coyote.http2; + +enum Setting { + HEADER_TABLE_SIZE(1), + ENABLE_PUSH(2), + MAX_CONCURRENT_STREAMS(3), + INITIAL_WINDOW_SIZE(4), + MAX_FRAME_SIZE(5), + MAX_HEADER_LIST_SIZE(6), + NO_RFC7540_PRIORITIES(9), + UNKNOWN(Integer.MAX_VALUE); + + private final int id; + + Setting(int id) { + this.id = id; + } + + final int getId() { + return id; + } + + @Override + public final String toString() { + return Integer.toString(id); + } + + static Setting valueOf(int i) { + switch (i) { + case 1: { + return HEADER_TABLE_SIZE; + } + case 2: { + return ENABLE_PUSH; + } + case 3: { + return MAX_CONCURRENT_STREAMS; + } + case 4: { + return INITIAL_WINDOW_SIZE; + } + case 5: { + return MAX_FRAME_SIZE; + } + case 6: { + return MAX_HEADER_LIST_SIZE; + } + case 9: { + return NO_RFC7540_PRIORITIES; + } + default: { + return UNKNOWN; + } + } + } +} diff --git a/java/org/apache/coyote/http2/Stream.java b/java/org/apache/coyote/http2/Stream.java new file mode 100644 index 0000000..400e9a6 --- /dev/null +++ b/java/org/apache/coyote/http2/Stream.java @@ -0,0 +1,1503 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import org.apache.coyote.ActionCode; +import org.apache.coyote.CloseNowException; +import org.apache.coyote.InputBuffer; +import org.apache.coyote.Request; +import org.apache.coyote.Response; +import org.apache.coyote.http11.HttpOutputBuffer; +import org.apache.coyote.http11.OutputFilter; +import org.apache.coyote.http11.filters.SavedRequestInputFilter; +import org.apache.coyote.http11.filters.VoidOutputFilter; +import org.apache.coyote.http2.HpackDecoder.HeaderEmitter; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.Host; +import org.apache.tomcat.util.http.parser.Priority; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.net.WriteBuffer; +import org.apache.tomcat.util.res.StringManager; + +class Stream extends AbstractNonZeroStream implements HeaderEmitter { + + private static final Log log = LogFactory.getLog(Stream.class); + private static final StringManager sm = StringManager.getManager(Stream.class); + + private static final int HEADER_STATE_START = 0; + private static final int HEADER_STATE_PSEUDO = 1; + private static final int HEADER_STATE_REGULAR = 2; + private static final int HEADER_STATE_TRAILER = 3; + + private static final MimeHeaders ACK_HEADERS; + + private static final Integer HTTP_UPGRADE_STREAM = Integer.valueOf(1); + + private static final Set HTTP_CONNECTION_SPECIFIC_HEADERS = new HashSet<>(); + + static { + Response response = new Response(); + response.setStatus(100); + StreamProcessor.prepareHeaders(null, response, true, null, null); + ACK_HEADERS = response.getMimeHeaders(); + + HTTP_CONNECTION_SPECIFIC_HEADERS.add("connection"); + HTTP_CONNECTION_SPECIFIC_HEADERS.add("proxy-connection"); + HTTP_CONNECTION_SPECIFIC_HEADERS.add("keep-alive"); + HTTP_CONNECTION_SPECIFIC_HEADERS.add("transfer-encoding"); + HTTP_CONNECTION_SPECIFIC_HEADERS.add("upgrade"); + } + + private volatile long contentLengthReceived = 0; + + private final Http2UpgradeHandler handler; + private final WindowAllocationManager allocationManager = new WindowAllocationManager(this); + private final Request coyoteRequest; + private final Response coyoteResponse = new Response(); + private final StreamInputBuffer inputBuffer; + private final StreamOutputBuffer streamOutputBuffer = new StreamOutputBuffer(); + private final Http2OutputBuffer http2OutputBuffer = new Http2OutputBuffer(coyoteResponse, streamOutputBuffer); + + // State machine would be too much overhead + private int headerState = HEADER_STATE_START; + private StreamException headerException = null; + + private volatile StringBuilder cookieHeader = null; + private volatile boolean hostHeaderSeen = false; + + private Object pendingWindowUpdateForStreamLock = new Object(); + private int pendingWindowUpdateForStream = 0; + + private volatile int urgency = Priority.DEFAULT_URGENCY; + private volatile boolean incremental = Priority.DEFAULT_INCREMENTAL; + + + Stream(Integer identifier, Http2UpgradeHandler handler) { + this(identifier, handler, null); + } + + + @SuppressWarnings("deprecation") + Stream(Integer identifier, Http2UpgradeHandler handler, Request coyoteRequest) { + super(handler.getConnectionId(), identifier); + this.handler = handler; + setWindowSize(handler.getRemoteSettings().getInitialWindowSize()); + if (coyoteRequest == null) { + // HTTP/2 new request + this.coyoteRequest = new Request(); + this.inputBuffer = new StandardStreamInputBuffer(); + this.coyoteRequest.setInputBuffer(inputBuffer); + } else { + // HTTP/2 Push or HTTP/1.1 upgrade + this.coyoteRequest = coyoteRequest; + this.inputBuffer = + new SavedRequestStreamInputBuffer((SavedRequestInputFilter) coyoteRequest.getInputBuffer()); + // Headers have been read by this point + state.receivedStartOfHeaders(); + if (HTTP_UPGRADE_STREAM.equals(identifier)) { + // Populate coyoteRequest from headers (HTTP/1.1 only) + try { + prepareRequest(); + } catch (IllegalArgumentException iae) { + // Something in the headers is invalid + // Set correct return status + coyoteResponse.setStatus(400); + // Set error flag. This triggers error processing rather than + // the normal mapping + coyoteResponse.setError(); + } + } + // Request body, if any, has been read and buffered + state.receivedEndOfStream(); + } + this.coyoteRequest.setSendfile(handler.hasAsyncIO() && handler.getProtocol().getUseSendfile()); + this.coyoteResponse.setOutputBuffer(http2OutputBuffer); + this.coyoteRequest.setResponse(coyoteResponse); + this.coyoteRequest.protocol().setString("HTTP/2.0"); + if (this.coyoteRequest.getStartTimeNanos() < 0) { + this.coyoteRequest.setStartTimeNanos(System.nanoTime()); + } + } + + + private void prepareRequest() { + if (coyoteRequest.scheme().isNull()) { + if (handler.getProtocol().getHttp11Protocol().isSSLEnabled()) { + coyoteRequest.scheme().setString("https"); + } else { + coyoteRequest.scheme().setString("http"); + } + } + MessageBytes hostValueMB = coyoteRequest.getMimeHeaders().getUniqueValue("host"); + if (hostValueMB == null) { + throw new IllegalArgumentException(); + } + // This processing expects bytes. Server push will have used a String + // so trigger a conversion if required. + hostValueMB.toBytes(); + ByteChunk valueBC = hostValueMB.getByteChunk(); + byte[] valueB = valueBC.getBytes(); + int valueL = valueBC.getLength(); + int valueS = valueBC.getStart(); + + int colonPos = Host.parse(hostValueMB); + if (colonPos != -1) { + int port = 0; + for (int i = colonPos + 1; i < valueL; i++) { + char c = (char) valueB[i + valueS]; + if (c < '0' || c > '9') { + throw new IllegalArgumentException(); + } + port = port * 10 + c - '0'; + } + coyoteRequest.setServerPort(port); + + // Only need to copy the host name up to the : + valueL = colonPos; + } + + // Extract the host name + char[] hostNameC = new char[valueL]; + for (int i = 0; i < valueL; i++) { + hostNameC[i] = (char) valueB[i + valueS]; + } + coyoteRequest.serverName().setChars(hostNameC, 0, valueL); + } + + + final void receiveReset(long errorCode) { + if (log.isTraceEnabled()) { + log.trace( + sm.getString("stream.reset.receive", getConnectionId(), getIdAsString(), Long.toString(errorCode))); + } + // Set the new state first since read and write both check this + state.receivedReset(); + // Reads wait internally so need to call a method to break the wait() + if (inputBuffer != null) { + inputBuffer.receiveReset(); + } + cancelAllocationRequests(); + } + + + final void cancelAllocationRequests() { + allocationManager.notifyAny(); + } + + + @Override + final void incrementWindowSize(int windowSizeIncrement) throws Http2Exception { + windowAllocationLock.lock(); + try { + // If this is zero then any thread that has been trying to write for + // this stream will be waiting. Notify that thread it can continue. Use + // notify all even though only one thread is waiting to be on the safe + // side. + boolean notify = getWindowSize() < 1; + super.incrementWindowSize(windowSizeIncrement); + if (notify && getWindowSize() > 0) { + allocationManager.notifyStream(); + } + } finally { + windowAllocationLock.unlock(); + } + } + + + final int reserveWindowSize(int reservation, boolean block) throws IOException { + windowAllocationLock.lock(); + try { + long windowSize = getWindowSize(); + while (windowSize < 1) { + if (!canWrite()) { + throw new CloseNowException(sm.getString("stream.notWritable", getConnectionId(), getIdAsString())); + } + if (block) { + try { + long writeTimeout = handler.getProtocol().getStreamWriteTimeout(); + allocationManager.waitForStream(writeTimeout); + windowSize = getWindowSize(); + if (windowSize == 0) { + doStreamCancel(sm.getString("stream.writeTimeout"), Http2Error.ENHANCE_YOUR_CALM); + } + } catch (InterruptedException e) { + // Possible shutdown / rst or similar. Use an IOException to + // signal to the client that further I/O isn't possible for this + // Stream. + throw new IOException(e); + } + } else { + allocationManager.waitForStreamNonBlocking(); + return 0; + } + } + int allocation; + if (windowSize < reservation) { + allocation = (int) windowSize; + } else { + allocation = reservation; + } + decrementWindowSize(allocation); + return allocation; + } finally { + windowAllocationLock.unlock(); + } + } + + + @SuppressWarnings("deprecation") + void doStreamCancel(String msg, Http2Error error) throws CloseNowException { + StreamException se = new StreamException(msg, error, getIdAsInt()); + // Prevent the application making further writes + streamOutputBuffer.closed = true; + // Prevent Tomcat's error handling trying to write + coyoteResponse.setError(); + coyoteResponse.setErrorReported(); + // Trigger a reset once control returns to Tomcat + streamOutputBuffer.reset = se; + throw new CloseNowException(msg, se); + } + + + void waitForConnectionAllocation(long timeout) throws InterruptedException { + allocationManager.waitForConnection(timeout); + } + + + void waitForConnectionAllocationNonBlocking() { + allocationManager.waitForConnectionNonBlocking(); + } + + + void notifyConnection() { + allocationManager.notifyConnection(); + } + + + @Override + public final void emitHeader(String name, String value) throws HpackException { + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.header.debug", getConnectionId(), getIdAsString(), name, value)); + } + + // Header names must be lower case + if (!name.toLowerCase(Locale.US).equals(name)) { + throw new HpackException(sm.getString("stream.header.case", getConnectionId(), getIdAsString(), name)); + } + + if (HTTP_CONNECTION_SPECIFIC_HEADERS.contains(name)) { + throw new HpackException( + sm.getString("stream.header.connection", getConnectionId(), getIdAsString(), name)); + } + + if ("te".equals(name)) { + if (!"trailers".equals(value)) { + throw new HpackException(sm.getString("stream.header.te", getConnectionId(), getIdAsString(), value)); + } + } + + if (headerException != null) { + // Don't bother processing the header since the stream is going to + // be reset anyway + return; + } + + if (name.length() == 0) { + throw new HpackException(sm.getString("stream.header.empty", getConnectionId(), getIdAsString())); + } + + boolean pseudoHeader = name.charAt(0) == ':'; + + if (pseudoHeader && headerState != HEADER_STATE_PSEUDO) { + headerException = new StreamException( + sm.getString("stream.header.unexpectedPseudoHeader", getConnectionId(), getIdAsString(), name), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); + // No need for further processing. The stream will be reset. + return; + } + + if (headerState == HEADER_STATE_PSEUDO && !pseudoHeader) { + headerState = HEADER_STATE_REGULAR; + } + + switch (name) { + case ":method": { + if (coyoteRequest.method().isNull()) { + coyoteRequest.method().setString(value); + if ("HEAD".equals(value)) { + configureVoidOutputFilter(); + } + } else { + throw new HpackException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":method")); + } + break; + } + case ":scheme": { + if (coyoteRequest.scheme().isNull()) { + coyoteRequest.scheme().setString(value); + } else { + throw new HpackException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":scheme")); + } + break; + } + case ":path": { + if (!coyoteRequest.requestURI().isNull()) { + throw new HpackException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":path")); + } + if (value.length() == 0) { + throw new HpackException(sm.getString("stream.header.noPath", getConnectionId(), getIdAsString())); + } + int queryStart = value.indexOf('?'); + String uri; + if (queryStart == -1) { + uri = value; + } else { + uri = value.substring(0, queryStart); + String query = value.substring(queryStart + 1); + coyoteRequest.queryString().setString(query); + } + // Bug 61120. Set the URI as bytes rather than String so: + // - any path parameters are correctly processed + // - the normalization security checks are performed that prevent + // directory traversal attacks + byte[] uriBytes = uri.getBytes(StandardCharsets.ISO_8859_1); + coyoteRequest.requestURI().setBytes(uriBytes, 0, uriBytes.length); + break; + } + case ":authority": { + if (coyoteRequest.serverName().isNull()) { + parseAuthority(value, false); + } else { + throw new HpackException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":authority")); + } + break; + } + case "cookie": { + // Cookie headers need to be concatenated into a single header + // See RFC 7540 8.1.2.5 + if (cookieHeader == null) { + cookieHeader = new StringBuilder(); + } else { + cookieHeader.append("; "); + } + cookieHeader.append(value); + break; + } + case "host": { + if (coyoteRequest.serverName().isNull()) { + // No :authority header. This is first host header. Use it. + hostHeaderSeen = true; + parseAuthority(value, true); + } else if (!hostHeaderSeen) { + // First host header - must be consistent with :authority + hostHeaderSeen = true; + compareAuthority(value); + } else { + // Multiple hosts headers - illegal + throw new HpackException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), "host")); + } + break; + } + case "priority": { + try { + Priority p = Priority.parsePriority(new StringReader(value)); + setUrgency(p.getUrgency()); + setIncremental(p.getIncremental()); + } catch (IOException ioe) { + // Not possible with StringReader + } + break; + } + default: { + if (headerState == HEADER_STATE_TRAILER && !handler.getProtocol().isTrailerHeaderAllowed(name)) { + break; + } + if ("expect".equals(name) && "100-continue".equals(value)) { + coyoteRequest.setExpectation(true); + } + if (pseudoHeader) { + headerException = new StreamException( + sm.getString("stream.header.unknownPseudoHeader", getConnectionId(), getIdAsString(), name), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); + } + + if (headerState == HEADER_STATE_TRAILER) { + // HTTP/2 headers are already always lower case + coyoteRequest.getTrailerFields().put(name, value); + } else { + coyoteRequest.getMimeHeaders().addValue(name).setString(value); + } + } + } + } + + + void configureVoidOutputFilter() { + addOutputFilter(new VoidOutputFilter()); + // Prevent further writes by the application + streamOutputBuffer.closed = true; + } + + private void parseAuthority(String value, boolean host) throws HpackException { + int i; + try { + i = Host.parse(value); + } catch (IllegalArgumentException iae) { + // Host value invalid + throw new HpackException(sm.getString("stream.header.invalid", getConnectionId(), getIdAsString(), + host ? "host" : ":authority", value)); + } + if (i > -1) { + coyoteRequest.serverName().setString(value.substring(0, i)); + coyoteRequest.setServerPort(Integer.parseInt(value.substring(i + 1))); + } else { + coyoteRequest.serverName().setString(value); + } + } + + + private void compareAuthority(String value) throws HpackException { + int i; + try { + i = Host.parse(value); + } catch (IllegalArgumentException iae) { + // Host value invalid + throw new HpackException( + sm.getString("stream.header.invalid", getConnectionId(), getIdAsString(), "host", value)); + } + if (i == -1 && (!value.equals(coyoteRequest.serverName().getString()) || coyoteRequest.getServerPort() != -1) || + i > -1 && ((!value.substring(0, i).equals(coyoteRequest.serverName().getString()) || + Integer.parseInt(value.substring(i + 1)) != coyoteRequest.getServerPort()))) { + // Host value inconsistent + throw new HpackException(sm.getString("stream.host.inconsistent", getConnectionId(), getIdAsString(), value, + coyoteRequest.serverName().getString(), Integer.toString(coyoteRequest.getServerPort()))); + } + + } + + + @Override + public void setHeaderException(StreamException streamException) { + if (headerException == null) { + headerException = streamException; + } + } + + + @Override + public void validateHeaders() throws StreamException { + if (headerException == null) { + return; + } + handler.getHpackDecoder().setHeaderEmitter(Http2UpgradeHandler.HEADER_SINK); + throw headerException; + } + + + final boolean receivedEndOfHeaders() throws ConnectionException { + if (coyoteRequest.method().isNull() || coyoteRequest.scheme().isNull() || + !coyoteRequest.method().equals("CONNECT") && coyoteRequest.requestURI().isNull()) { + throw new ConnectionException(sm.getString("stream.header.required", getConnectionId(), getIdAsString()), + Http2Error.PROTOCOL_ERROR); + } + // Cookie headers need to be concatenated into a single header + // See RFC 7540 8.1.2.5 + // Can only do this once the headers are fully received + if (cookieHeader != null) { + coyoteRequest.getMimeHeaders().addValue("cookie").setString(cookieHeader.toString()); + } + return headerState == HEADER_STATE_REGULAR || headerState == HEADER_STATE_PSEUDO; + } + + + final void writeHeaders() throws IOException { + boolean endOfStream = streamOutputBuffer.hasNoBody() && coyoteResponse.getTrailerFields() == null; + handler.writeHeaders(this, 0, coyoteResponse.getMimeHeaders(), endOfStream, + Constants.DEFAULT_HEADERS_FRAME_SIZE); + } + + + final void addOutputFilter(OutputFilter filter) { + http2OutputBuffer.addFilter(filter); + } + + + final void writeTrailers() throws IOException { + Supplier> supplier = coyoteResponse.getTrailerFields(); + if (supplier == null) { + // No supplier was set, end of stream will already have been sent + return; + } + + // We can re-use the MimeHeaders from the response since they have + // already been processed by the encoder at this point + MimeHeaders mimeHeaders = coyoteResponse.getMimeHeaders(); + mimeHeaders.recycle(); + + Map headerMap = supplier.get(); + if (headerMap == null) { + headerMap = Collections.emptyMap(); + } + + // Copy the contents of the Map to the MimeHeaders + // TODO: Is there benefit in refactoring this? Is MimeHeaders too + // heavyweight? Can we reduce the copy/conversions? + for (Map.Entry headerEntry : headerMap.entrySet()) { + MessageBytes mb = mimeHeaders.addValue(headerEntry.getKey()); + mb.setString(headerEntry.getValue()); + } + + handler.writeHeaders(this, 0, mimeHeaders, true, Constants.DEFAULT_HEADERS_FRAME_SIZE); + } + + + final void writeAck() throws IOException { + handler.writeHeaders(this, 0, ACK_HEADERS, false, Constants.DEFAULT_HEADERS_ACK_FRAME_SIZE); + } + + + @Override + final String getConnectionId() { + return handler.getConnectionId(); + } + + + final Request getCoyoteRequest() { + return coyoteRequest; + } + + + final Response getCoyoteResponse() { + return coyoteResponse; + } + + + @Override + final ByteBuffer getInputByteBuffer() { + if (inputBuffer == null) { + // This must either be a push or an HTTP upgrade. Either way there + // should not be a request body so return a zero length ByteBuffer + // to trigger a flow control error. + return ZERO_LENGTH_BYTEBUFFER; + } + return inputBuffer.getInBuffer(); + } + + + final void receivedStartOfHeaders(boolean headersEndStream) throws Http2Exception { + if (headerState == HEADER_STATE_START) { + headerState = HEADER_STATE_PSEUDO; + handler.getHpackDecoder().setMaxHeaderCount(handler.getProtocol().getMaxHeaderCount()); + handler.getHpackDecoder().setMaxHeaderSize(handler.getProtocol().getMaxHeaderSize()); + } else if (headerState == HEADER_STATE_PSEUDO || headerState == HEADER_STATE_REGULAR) { + // Trailer headers MUST include the end of stream flag + if (headersEndStream) { + headerState = HEADER_STATE_TRAILER; + handler.getHpackDecoder().setMaxHeaderCount(handler.getProtocol().getMaxTrailerCount()); + handler.getHpackDecoder().setMaxHeaderSize(handler.getProtocol().getMaxTrailerSize()); + } else { + throw new ConnectionException( + sm.getString("stream.trailerHeader.noEndOfStream", getConnectionId(), getIdAsString()), + Http2Error.PROTOCOL_ERROR); + } + } + // Parser will catch attempt to send a headers frame after the stream + // has closed. + state.receivedStartOfHeaders(); + } + + + @Override + final void receivedData(int payloadSize) throws Http2Exception { + contentLengthReceived += payloadSize; + long contentLengthHeader = coyoteRequest.getContentLengthLong(); + if (contentLengthHeader > -1 && contentLengthReceived > contentLengthHeader) { + throw new ConnectionException( + sm.getString("stream.header.contentLength", getConnectionId(), getIdAsString(), + Long.valueOf(contentLengthHeader), Long.valueOf(contentLengthReceived)), + Http2Error.PROTOCOL_ERROR); + } + } + + + final void receivedEndOfStream() throws ConnectionException { + if (isContentLengthInconsistent()) { + throw new ConnectionException( + sm.getString("stream.header.contentLength", getConnectionId(), getIdAsString(), + Long.valueOf(coyoteRequest.getContentLengthLong()), Long.valueOf(contentLengthReceived)), + Http2Error.PROTOCOL_ERROR); + } + state.receivedEndOfStream(); + if (inputBuffer != null) { + inputBuffer.notifyEof(); + } + } + + + final boolean isContentLengthInconsistent() { + long contentLengthHeader = coyoteRequest.getContentLengthLong(); + if (contentLengthHeader > -1 && contentLengthReceived != contentLengthHeader) { + return true; + } + return false; + } + + + final void sentHeaders() { + state.sentHeaders(); + } + + + final void sentEndOfStream() { + streamOutputBuffer.endOfStreamSent = true; + state.sentEndOfStream(); + } + + + final boolean isReadyForWrite() { + return streamOutputBuffer.isReady(); + } + + + final boolean flush(boolean block) throws IOException { + return streamOutputBuffer.flush(block); + } + + + final StreamInputBuffer getInputBuffer() { + return inputBuffer; + } + + + final HttpOutputBuffer getOutputBuffer() { + return http2OutputBuffer; + } + + + final void sentPushPromise() { + state.sentPushPromise(); + } + + + final boolean isActive() { + return state.isActive(); + } + + + final boolean canWrite() { + return state.canWrite(); + } + + + final void closeIfIdle() { + state.closeIfIdle(); + } + + + final boolean isInputFinished() { + return !state.isFrameTypePermitted(FrameType.DATA); + } + + + final void close(Http2Exception http2Exception) { + if (http2Exception instanceof StreamException) { + try { + StreamException se = (StreamException) http2Exception; + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.reset.send", getConnectionId(), getIdAsString(), se.getError())); + } + + // Need to update state atomically with the sending of the RST + // frame else other threads currently working with this stream + // may see the state change and send a RST frame before the RST + // frame triggered by this thread. If that happens the client + // may see out of order RST frames which may hard to follow if + // the client is unaware the RST frames may be received out of + // order. + handler.sendStreamReset(state, se); + + cancelAllocationRequests(); + if (inputBuffer != null) { + inputBuffer.swallowUnread(); + } + } catch (IOException ioe) { + ConnectionException ce = + new ConnectionException(sm.getString("stream.reset.fail", getConnectionId(), getIdAsString()), + Http2Error.PROTOCOL_ERROR, ioe); + handler.closeConnection(ce); + } + } else { + handler.closeConnection(http2Exception); + } + recycle(); + } + + + /* + * This method is called recycle for consistency with the rest of the Tomcat code base. Currently, it calls the + * handler to replace this stream with an implementation that uses less memory. It does not fully recycle the Stream + * ready for re-use since Stream objects are not re-used. This is useful because Stream instances are retained for a + * period after the Stream closes. + */ + final void recycle() { + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.recycle", getConnectionId(), getIdAsString())); + } + int remaining; + // May be null if stream was closed before any DATA frames were processed. + ByteBuffer inputByteBuffer = getInputByteBuffer(); + if (inputByteBuffer == null) { + remaining = 0; + } else { + remaining = inputByteBuffer.remaining(); + } + handler.replaceStream(this, new RecycledStream(getConnectionId(), getIdentifier(), state, remaining)); + } + + + final boolean isPushSupported() { + return handler.getRemoteSettings().getEnablePush(); + } + + + final void push(Request request) throws IOException { + // Can only push when supported and from a peer initiated stream + if (!isPushSupported() || getIdAsInt() % 2 == 0) { + return; + } + // Set the special HTTP/2 headers + request.getMimeHeaders().addValue(":method").duplicate(request.method()); + request.getMimeHeaders().addValue(":scheme").duplicate(request.scheme()); + StringBuilder path = new StringBuilder(request.requestURI().toString()); + if (!request.queryString().isNull()) { + path.append('?'); + path.append(request.queryString().toString()); + } + request.getMimeHeaders().addValue(":path").setString(path.toString()); + + // Authority needs to include the port only if a non-standard port is + // being used. + if (!(request.scheme().equals("http") && request.getServerPort() == 80) && + !(request.scheme().equals("https") && request.getServerPort() == 443)) { + request.getMimeHeaders().addValue(":authority") + .setString(request.serverName().getString() + ":" + request.getServerPort()); + } else { + request.getMimeHeaders().addValue(":authority").duplicate(request.serverName()); + } + + push(handler, request, this); + } + + + boolean isTrailerFieldsReady() { + // Once EndOfStream has been received, canRead will be false + return !state.canRead(); + } + + + boolean isTrailerFieldsSupported() { + return !streamOutputBuffer.endOfStreamSent; + } + + + StreamException getResetException() { + return streamOutputBuffer.reset; + } + + + int getWindowUpdateSizeToWrite(int increment) { + int result; + int threshold = handler.getProtocol().getOverheadWindowUpdateThreshold(); + synchronized (pendingWindowUpdateForStreamLock) { + if (increment > threshold) { + result = increment + pendingWindowUpdateForStream; + pendingWindowUpdateForStream = 0; + } else { + pendingWindowUpdateForStream += increment; + if (pendingWindowUpdateForStream > threshold) { + result = pendingWindowUpdateForStream; + pendingWindowUpdateForStream = 0; + } else { + result = 0; + } + } + } + return result; + } + + + public int getUrgency() { + return urgency; + } + + + public void setUrgency(int urgency) { + this.urgency = urgency; + } + + + public boolean getIncremental() { + return incremental; + } + + + public void setIncremental(boolean incremental) { + this.incremental = incremental; + } + + + private static void push(final Http2UpgradeHandler handler, final Request request, final Stream stream) + throws IOException { + if (org.apache.coyote.Constants.IS_SECURITY_ENABLED) { + try { + AccessController.doPrivileged(new PrivilegedPush(handler, request, stream)); + } catch (PrivilegedActionException ex) { + Exception e = ex.getException(); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new IOException(ex); + } + } + + } else { + handler.push(request, stream); + } + } + + + private static class PrivilegedPush implements PrivilegedExceptionAction { + + private final Http2UpgradeHandler handler; + private final Request request; + private final Stream stream; + + PrivilegedPush(Http2UpgradeHandler handler, Request request, Stream stream) { + this.handler = handler; + this.request = request; + this.stream = stream; + } + + @Override + public Void run() throws IOException { + handler.push(request, stream); + return null; + } + } + + + class StreamOutputBuffer implements HttpOutputBuffer, WriteBuffer.Sink { + + private final Lock writeLock = new ReentrantLock(); + private final ByteBuffer buffer = ByteBuffer.allocate(8 * 1024); + private final WriteBuffer writeBuffer = new WriteBuffer(32 * 1024); + // Flag that indicates that data was left over on a previous + // non-blocking write. Once set, this flag stays set until all the data + // has been written. + private boolean dataLeft; + private volatile long written = 0; + private int streamReservation = 0; + private volatile boolean closed = false; + private volatile StreamException reset = null; + private volatile boolean endOfStreamSent = false; + + /* + * The write methods share a common lock to ensure that only one thread at a time is able to access the buffer. + * Without this protection, a client that performed concurrent writes could corrupt the buffer. + */ + + @Override + public final int doWrite(ByteBuffer chunk) throws IOException { + writeLock.lock(); + try { + if (closed) { + throw new IOException(sm.getString("stream.closed", getConnectionId(), getIdAsString())); + } + // chunk is always fully written + int result = chunk.remaining(); + if (writeBuffer.isEmpty()) { + int chunkLimit = chunk.limit(); + while (chunk.remaining() > 0) { + int thisTime = Math.min(buffer.remaining(), chunk.remaining()); + chunk.limit(chunk.position() + thisTime); + buffer.put(chunk); + chunk.limit(chunkLimit); + if (chunk.remaining() > 0 && !buffer.hasRemaining()) { + // Only flush if we have more data to write and the buffer + // is full + if (flush(true, coyoteResponse.getWriteListener() == null)) { + writeBuffer.add(chunk); + dataLeft = true; + break; + } + } + } + } else { + writeBuffer.add(chunk); + } + written += result; + return result; + } finally { + writeLock.unlock(); + } + } + + final boolean flush(boolean block) throws IOException { + writeLock.lock(); + try { + /* + * Need to ensure that there is exactly one call to flush even when there is no data to write. Too few calls + * (i.e. zero) and the end of stream message is not sent for a completed asynchronous write. Too many calls + * and the end of stream message is sent too soon and trailer headers are not sent. + */ + boolean dataInBuffer = buffer.position() > 0; + boolean flushed = false; + + if (dataInBuffer) { + dataInBuffer = flush(false, block); + flushed = true; + } + + if (dataInBuffer) { + dataLeft = true; + } else { + if (writeBuffer.isEmpty()) { + // Both buffer and writeBuffer are empty. + if (flushed) { + dataLeft = false; + } else { + dataLeft = flush(false, block); + } + } else { + dataLeft = writeBuffer.write(this, block); + } + } + + return dataLeft; + } finally { + writeLock.unlock(); + } + } + + private boolean flush(boolean writeInProgress, boolean block) throws IOException { + writeLock.lock(); + try { + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.outputBuffer.flush.debug", getConnectionId(), getIdAsString(), + Integer.toString(buffer.position()), Boolean.toString(writeInProgress), + Boolean.toString(closed))); + } + if (buffer.position() == 0) { + if (closed && !endOfStreamSent) { + // Handling this special case here is simpler than trying + // to modify the following code to handle it. + handler.writeBody(Stream.this, buffer, 0, coyoteResponse.getTrailerFields() == null); + } + // Buffer is empty. Nothing to do. + return false; + } + buffer.flip(); + int left = buffer.remaining(); + while (left > 0) { + if (streamReservation == 0) { + streamReservation = reserveWindowSize(left, block); + if (streamReservation == 0) { + // Must be non-blocking. + // Note: Can't add to the writeBuffer here as the write + // may originate from the writeBuffer. + buffer.compact(); + return true; + } + } + while (streamReservation > 0) { + int connectionReservation = handler.reserveWindowSize(Stream.this, streamReservation, block); + if (connectionReservation == 0) { + // Must be non-blocking. + // Note: Can't add to the writeBuffer here as the write + // may originate from the writeBuffer. + buffer.compact(); + return true; + } + // Do the write + handler.writeBody(Stream.this, buffer, connectionReservation, !writeInProgress && closed && + left == connectionReservation && coyoteResponse.getTrailerFields() == null); + streamReservation -= connectionReservation; + left -= connectionReservation; + } + } + buffer.clear(); + return false; + } finally { + writeLock.unlock(); + } + } + + final boolean isReady() { + writeLock.lock(); + try { + // Bug 63682 + // Only want to return false if the window size is zero AND we are + // already waiting for an allocation. + if (getWindowSize() > 0 && allocationManager.isWaitingForStream() || + handler.getWindowSize() > 0 && allocationManager.isWaitingForConnection() || dataLeft) { + return false; + } else { + return true; + } + } finally { + writeLock.unlock(); + } + } + + @Override + public final long getBytesWritten() { + return written; + } + + @Override + public final void end() throws IOException { + if (reset != null) { + throw new CloseNowException(reset); + } + if (!closed) { + closed = true; + flush(true); + writeTrailers(); + } + } + + /** + * @return true if it is certain that the associated response has no body. + */ + final boolean hasNoBody() { + return ((written == 0) && closed); + } + + @Override + public void flush() throws IOException { + /* + * This method should only be called during blocking I/O. All the Servlet API calls that end up here are + * illegal during non-blocking I/O. Servlet 5.4. However, the wording Servlet specification states that the + * behaviour is undefined so we do the best we can which is to perform a flush using blocking I/O or + * non-blocking I/O based depending which is currently in use. + */ + flush(getCoyoteResponse().getWriteListener() == null); + } + + @Override + public boolean writeFromBuffer(ByteBuffer src, boolean blocking) throws IOException { + writeLock.lock(); + try { + int chunkLimit = src.limit(); + while (src.remaining() > 0) { + int thisTime = Math.min(buffer.remaining(), src.remaining()); + src.limit(src.position() + thisTime); + buffer.put(src); + src.limit(chunkLimit); + if (flush(false, blocking)) { + return true; + } + } + return false; + } finally { + writeLock.unlock(); + } + } + } + + + abstract class StreamInputBuffer implements InputBuffer { + + abstract void receiveReset(); + + abstract void swallowUnread() throws IOException; + + abstract void notifyEof(); + + abstract ByteBuffer getInBuffer(); + + abstract void onDataAvailable() throws IOException; + + abstract boolean isReadyForRead(); + + abstract boolean isRequestBodyFullyRead(); + + abstract void insertReplayedBody(ByteChunk body); + } + + + class StandardStreamInputBuffer extends StreamInputBuffer { + + private final Lock readStateLock = new ReentrantLock(); + /* + * Two buffers are required to avoid various multi-threading issues. These issues arise from the fact that the + * Stream (or the Request/Response) used by the application is processed in one thread but the connection is + * processed in another. Therefore it is possible that a request body frame could be received before the + * application is ready to read it. If it isn't buffered, processing of the connection (and hence all streams) + * would block until the application read the data. Hence the incoming data has to be buffered. If only one + * buffer was used then it could become corrupted if the connection thread is trying to add to it at the same + * time as the application is read it. While it should be possible to avoid this corruption by careful use of + * the buffer it would still require the same copies as using two buffers and the behaviour would be less clear. + * + * The buffers are created lazily because they quickly add up to a lot of memory and most requests do not have + * bodies. + */ + // This buffer is used to populate the ByteChunk passed in to the read + // method + private byte[] outBuffer; + // This buffer is the destination for incoming data. It is normally is + // 'write mode'. + private volatile ByteBuffer inBuffer; + private volatile boolean readInterest; + private volatile boolean closed; + private boolean resetReceived; + + @SuppressWarnings("deprecation") + @Override + public final int doRead(ApplicationBufferHandler applicationBufferHandler) throws IOException { + + ensureBuffersExist(); + + int written = -1; + + // It is still possible that the stream has been closed and inBuffer + // set to null between the call to ensureBuffersExist() above and + // the sync below. The checks just before and just inside the sync + // ensure we don't get any NPEs reported. + ByteBuffer tmpInBuffer = inBuffer; + if (tmpInBuffer == null) { + return -1; + } + // Ensure that only one thread accesses inBuffer at a time + synchronized (tmpInBuffer) { + if (inBuffer == null) { + return -1; + } + boolean canRead = false; + while (inBuffer.position() == 0 && (canRead = isActive() && !isInputFinished())) { + // Need to block until some data is written + try { + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.inputBuffer.empty")); + } + + long readTimeout = handler.getProtocol().getStreamReadTimeout(); + if (readTimeout < 0) { + inBuffer.wait(); + } else { + inBuffer.wait(readTimeout); + } + + if (resetReceived) { + throw new IOException(sm.getString("stream.inputBuffer.reset")); + } + + if (inBuffer.position() == 0 && isActive() && !isInputFinished()) { + String msg = sm.getString("stream.inputBuffer.readTimeout"); + StreamException se = new StreamException(msg, Http2Error.ENHANCE_YOUR_CALM, getIdAsInt()); + // Trigger a reset once control returns to Tomcat + coyoteResponse.setError(); + streamOutputBuffer.reset = se; + throw new CloseNowException(msg, se); + } + } catch (InterruptedException e) { + // Possible shutdown / rst or similar. Use an + // IOException to signal to the client that further I/O + // isn't possible for this Stream. + throw new IOException(e); + } + } + + if (inBuffer.position() > 0) { + // Data is available in the inBuffer. Copy it to the + // outBuffer. + inBuffer.flip(); + written = inBuffer.remaining(); + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.inputBuffer.copy", Integer.toString(written))); + } + inBuffer.get(outBuffer, 0, written); + inBuffer.clear(); + } else if (!canRead) { + return -1; + } else { + // Should never happen + throw new IllegalStateException(); + } + } + + applicationBufferHandler.setByteBuffer(ByteBuffer.wrap(outBuffer, 0, written)); + + // Increment client-side flow control windows by the number of bytes + // read + handler.writeWindowUpdate(Stream.this, written, true); + + return written; + } + + + @Override + final boolean isReadyForRead() { + ensureBuffersExist(); + + readStateLock.lock(); + try { + if (available() > 0) { + return true; + } + + if (!isRequestBodyFullyRead()) { + readInterest = true; + } + + return false; + } finally { + readStateLock.unlock(); + } + } + + @Override + final boolean isRequestBodyFullyRead() { + readStateLock.lock(); + try { + return (inBuffer == null || inBuffer.position() == 0) && isInputFinished(); + } finally { + readStateLock.unlock(); + } + } + + + @Override + public final int available() { + readStateLock.lock(); + try { + if (inBuffer == null) { + return 0; + } + return inBuffer.position(); + } finally { + readStateLock.unlock(); + } + } + + + /* + * Called after placing some data in the inBuffer. + */ + @Override + final void onDataAvailable() throws IOException { + readStateLock.lock(); + try { + if (closed) { + swallowUnread(); + } else if (readInterest) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.inputBuffer.dispatch")); + } + readInterest = false; + coyoteRequest.action(ActionCode.DISPATCH_READ, null); + // Always need to dispatch since this thread is processing + // the incoming connection and streams are processed on their + // own. + coyoteRequest.action(ActionCode.DISPATCH_EXECUTE, null); + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.inputBuffer.signal")); + } + synchronized (inBuffer) { + inBuffer.notifyAll(); + } + } + } finally { + readStateLock.unlock(); + } + } + + + @Override + final ByteBuffer getInBuffer() { + ensureBuffersExist(); + return inBuffer; + } + + + @Override + final void insertReplayedBody(ByteChunk body) { + readStateLock.lock(); + try { + inBuffer = ByteBuffer.wrap(body.getBytes(), body.getOffset(), body.getLength()); + } finally { + readStateLock.unlock(); + } + } + + + private void ensureBuffersExist() { + if (inBuffer == null && !closed) { + // The client must obey Tomcat's window size when sending so + // this is the initial window size set by Tomcat that the client + // uses (i.e. the local setting is required here). + int size = handler.getLocalSettings().getInitialWindowSize(); + readStateLock.lock(); + try { + if (inBuffer == null && !closed) { + inBuffer = ByteBuffer.allocate(size); + outBuffer = new byte[size]; + } + } finally { + readStateLock.unlock(); + } + } + } + + + @Override + final void receiveReset() { + if (inBuffer != null) { + synchronized (inBuffer) { + resetReceived = true; + inBuffer.notifyAll(); + } + } + } + + @Override + final void notifyEof() { + if (inBuffer != null) { + synchronized (inBuffer) { + inBuffer.notifyAll(); + } + } + } + + @Override + final void swallowUnread() throws IOException { + readStateLock.lock(); + try { + closed = true; + } finally { + readStateLock.unlock(); + } + if (inBuffer != null) { + int unreadByteCount = 0; + synchronized (inBuffer) { + unreadByteCount = inBuffer.position(); + if (log.isTraceEnabled()) { + log.trace(sm.getString("stream.inputBuffer.swallowUnread", Integer.valueOf(unreadByteCount))); + } + if (unreadByteCount > 0) { + inBuffer.position(0); + inBuffer.limit(inBuffer.limit() - unreadByteCount); + } + } + // Do this outside of the sync because: + // - it doesn't need to be inside the sync + // - if inside the sync it can trigger a deadlock + // https://markmail.org/message/vbglzkvj6wxlhh3p + if (unreadByteCount > 0) { + handler.onSwallowedDataFramePayload(getIdAsInt(), unreadByteCount); + } + } + } + } + + + class SavedRequestStreamInputBuffer extends StreamInputBuffer { + + private final SavedRequestInputFilter inputFilter; + + SavedRequestStreamInputBuffer(SavedRequestInputFilter inputFilter) { + this.inputFilter = inputFilter; + } + + + @Override + public int doRead(ApplicationBufferHandler handler) throws IOException { + return inputFilter.doRead(handler); + } + + @Override + public int available() { + return inputFilter.available(); + } + + @Override + void receiveReset() { + // NO-OP + } + + @Override + void swallowUnread() throws IOException { + // NO-OP + } + + @Override + void notifyEof() { + // NO-OP + } + + @Override + ByteBuffer getInBuffer() { + return null; + } + + @Override + void onDataAvailable() throws IOException { + // NO-OP + } + + @Override + boolean isReadyForRead() { + return true; + } + + @Override + boolean isRequestBodyFullyRead() { + return inputFilter.isFinished(); + } + + @Override + void insertReplayedBody(ByteChunk body) { + // NO-OP + } + } +} diff --git a/java/org/apache/coyote/http2/StreamException.java b/java/org/apache/coyote/http2/StreamException.java new file mode 100644 index 0000000..ec35b3c --- /dev/null +++ b/java/org/apache/coyote/http2/StreamException.java @@ -0,0 +1,37 @@ +/* + * 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.coyote.http2; + +/** + * Thrown when an HTTP/2 stream error occurs. + */ +class StreamException extends Http2Exception { + + private static final long serialVersionUID = 1L; + + private final int streamId; + + StreamException(String msg, Http2Error error, int streamId) { + super(msg, error); + this.streamId = streamId; + } + + + int getStreamId() { + return streamId; + } +} diff --git a/java/org/apache/coyote/http2/StreamProcessor.java b/java/org/apache/coyote/http2/StreamProcessor.java new file mode 100644 index 0000000..43865c1 --- /dev/null +++ b/java/org/apache/coyote/http2/StreamProcessor.java @@ -0,0 +1,556 @@ +/* + * 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.coyote.http2; + +import java.io.File; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import jakarta.servlet.ServletConnection; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.coyote.AbstractProcessor; +import org.apache.coyote.ActionCode; +import org.apache.coyote.Adapter; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.coyote.ErrorState; +import org.apache.coyote.Request; +import org.apache.coyote.RequestGroupInfo; +import org.apache.coyote.Response; +import org.apache.coyote.http11.filters.GzipOutputFilter; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.HttpParser; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.DispatchType; +import org.apache.tomcat.util.net.SendfileState; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; + +class StreamProcessor extends AbstractProcessor { + + private static final Log log = LogFactory.getLog(StreamProcessor.class); + private static final StringManager sm = StringManager.getManager(StreamProcessor.class); + + private static final Set H2_PSEUDO_HEADERS_REQUEST = new HashSet<>(); + + private final Lock processLock = new ReentrantLock(); + private final Http2UpgradeHandler handler; + private final Stream stream; + private SendfileData sendfileData = null; + private SendfileState sendfileState = null; + + static { + H2_PSEUDO_HEADERS_REQUEST.add(":method"); + H2_PSEUDO_HEADERS_REQUEST.add(":scheme"); + H2_PSEUDO_HEADERS_REQUEST.add(":authority"); + H2_PSEUDO_HEADERS_REQUEST.add(":path"); + } + + StreamProcessor(Http2UpgradeHandler handler, Stream stream, Adapter adapter, SocketWrapperBase socketWrapper) { + super(adapter, stream.getCoyoteRequest(), stream.getCoyoteResponse()); + this.handler = handler; + this.stream = stream; + setSocketWrapper(socketWrapper); + } + + + final void process(SocketEvent event) { + try { + // Note: The regular processor uses the socketWrapper lock, but using that here triggers a deadlock + processLock.lock(); + try { + // HTTP/2 equivalent of AbstractConnectionHandler#process() without the + // socket <-> processor mapping + SocketState state = SocketState.CLOSED; + try { + state = process(socketWrapper, event); + + if (state == SocketState.LONG) { + handler.getProtocol().getHttp11Protocol().addWaitingProcessor(this); + } else if (state == SocketState.CLOSED) { + handler.getProtocol().getHttp11Protocol().removeWaitingProcessor(this); + if (!stream.isInputFinished() && getErrorState().isIoAllowed()) { + // The request has been processed but the request body has not been + // fully read. This typically occurs when Tomcat rejects an upload + // of some form (e.g. PUT or POST). Need to tell the client not to + // send any more data on this stream (reset). + StreamException se = + new StreamException(sm.getString("streamProcessor.cancel", stream.getConnectionId(), + stream.getIdAsString()), Http2Error.NO_ERROR, stream.getIdAsInt()); + stream.close(se); + } else if (!getErrorState().isConnectionIoAllowed()) { + ConnectionException ce = new ConnectionException( + sm.getString("streamProcessor.error.connection", stream.getConnectionId(), + stream.getIdAsString()), + Http2Error.INTERNAL_ERROR); + stream.close(ce); + } else if (!getErrorState().isIoAllowed()) { + StreamException se = stream.getResetException(); + if (se == null) { + se = new StreamException( + sm.getString("streamProcessor.error.stream", stream.getConnectionId(), + stream.getIdAsString()), + Http2Error.INTERNAL_ERROR, stream.getIdAsInt()); + } + stream.close(se); + } else { + if (!stream.isActive()) { + // stream.close() will call recycle so only need it here + stream.recycle(); + } + } + } + } catch (Exception e) { + String msg = sm.getString("streamProcessor.error.connection", stream.getConnectionId(), + stream.getIdAsString()); + if (log.isDebugEnabled()) { + log.debug(msg, e); + } + ConnectionException ce = new ConnectionException(msg, Http2Error.INTERNAL_ERROR, e); + stream.close(ce); + state = SocketState.CLOSED; + } finally { + if (state == SocketState.CLOSED) { + recycle(); + } + } + } finally { + processLock.unlock(); + } + } finally { + handler.executeQueuedStream(); + } + } + + + @Override + protected final void prepareResponse() throws IOException { + response.setCommitted(true); + if (handler.hasAsyncIO() && handler.getProtocol().getUseSendfile()) { + prepareSendfile(); + } + prepareHeaders(request, response, sendfileData == null, handler.getProtocol(), stream); + stream.writeHeaders(); + } + + + private void prepareSendfile() { + String fileName = + (String) stream.getCoyoteRequest().getAttribute(org.apache.coyote.Constants.SENDFILE_FILENAME_ATTR); + if (fileName != null) { + sendfileData = new SendfileData(); + sendfileData.path = new File(fileName).toPath(); + sendfileData.pos = ((Long) stream.getCoyoteRequest() + .getAttribute(org.apache.coyote.Constants.SENDFILE_FILE_START_ATTR)).longValue(); + sendfileData.end = + ((Long) stream.getCoyoteRequest().getAttribute(org.apache.coyote.Constants.SENDFILE_FILE_END_ATTR)) + .longValue(); + sendfileData.left = sendfileData.end - sendfileData.pos; + sendfileData.stream = stream; + } + } + + + // Static so it can be used by Stream to build the MimeHeaders required for + // an ACK. For that use case coyoteRequest, protocol and stream will be null. + static void prepareHeaders(Request coyoteRequest, Response coyoteResponse, boolean noSendfile, + Http2Protocol protocol, Stream stream) { + MimeHeaders headers = coyoteResponse.getMimeHeaders(); + int statusCode = coyoteResponse.getStatus(); + + // Add the pseudo header for status + headers.addValue(":status").setString(Integer.toString(statusCode)); + + + // Compression can't be used with sendfile + // Need to check for compression (and set headers appropriately) before + // adding headers below + if (noSendfile && protocol != null && protocol.useCompression(coyoteRequest, coyoteResponse)) { + // Enable compression. Headers will have been set. Need to configure + // output filter at this point. + stream.addOutputFilter(new GzipOutputFilter()); + } + + // Check to see if a response body is present + if (!(statusCode < 200 || statusCode == 204 || statusCode == 205 || statusCode == 304)) { + String contentType = coyoteResponse.getContentType(); + if (contentType != null) { + headers.setValue("content-type").setString(contentType); + } + String contentLanguage = coyoteResponse.getContentLanguage(); + if (contentLanguage != null) { + headers.setValue("content-language").setString(contentLanguage); + } + // Add a content-length header if a content length has been set unless + // the application has already added one + long contentLength = coyoteResponse.getContentLengthLong(); + if (contentLength != -1 && headers.getValue("content-length") == null) { + headers.addValue("content-length").setLong(contentLength); + } + } else { + // Disable response body + if (stream != null) { + stream.configureVoidOutputFilter(); + } + if (statusCode == 205) { + // RFC 7231 requires the server to explicitly signal an empty + // response in this case + coyoteResponse.setContentLength(0); + } else { + coyoteResponse.setContentLength(-1); + } + } + + // Add date header unless it is an informational response or the + // application has already set one + if (statusCode >= 200 && headers.getValue("date") == null) { + headers.addValue("date").setString(FastHttpDateFormat.getCurrentDate()); + } + } + + + @Override + protected final void finishResponse() throws IOException { + sendfileState = handler.processSendfile(sendfileData); + if (!(sendfileState == SendfileState.PENDING)) { + stream.getOutputBuffer().end(); + } + } + + + @Override + protected final void ack(ContinueResponseTiming continueResponseTiming) { + // Only try and send the ACK for ALWAYS or if the timing of the request + // to send the ACK matches the current configuration. + if (continueResponseTiming == ContinueResponseTiming.ALWAYS || + continueResponseTiming == handler.getProtocol().getContinueResponseTimingInternal()) { + if (!response.isCommitted() && request.hasExpectation()) { + try { + stream.writeAck(); + } catch (IOException ioe) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); + } + } + } + } + + + @Override + protected final void flush() throws IOException { + stream.getOutputBuffer().flush(); + } + + + @Override + protected final int available(boolean doRead) { + return stream.getInputBuffer().available(); + } + + + @Override + protected final void setRequestBody(ByteChunk body) { + stream.getInputBuffer().insertReplayedBody(body); + try { + stream.receivedEndOfStream(); + } catch (ConnectionException e) { + // Exception will not be thrown in this case + } + } + + + @Override + protected final void setSwallowResponse() { + // NO-OP + } + + + @Override + protected final void disableSwallowRequest() { + // NO-OP + // HTTP/2 has to swallow any input received to ensure that the flow + // control windows are correctly tracked. + } + + + @Override + protected void processSocketEvent(SocketEvent event, boolean dispatch) { + if (dispatch) { + handler.processStreamOnContainerThread(this, event); + } else { + this.process(event); + } + } + + + @Override + protected final boolean isReadyForRead() { + return stream.getInputBuffer().isReadyForRead(); + } + + + @Override + protected final boolean isRequestBodyFullyRead() { + return stream.getInputBuffer().isRequestBodyFullyRead(); + } + + + @Override + protected final void registerReadInterest() { + // Should never be called for StreamProcessor as isReadyForRead() is + // overridden + throw new UnsupportedOperationException(); + } + + + @Override + protected final boolean isReadyForWrite() { + return stream.isReadyForWrite(); + } + + + @Override + protected final void executeDispatches() { + Iterator dispatches = getIteratorAndClearDispatches(); + /* + * Compare with superclass that uses SocketWrapper A sync is not necessary here as the window sizes are updated + * with syncs before the dispatches are executed and it is the window size updates that need to be complete + * before the dispatch executes. + */ + while (dispatches != null && dispatches.hasNext()) { + DispatchType dispatchType = dispatches.next(); + /* + * Dispatch on new thread. Firstly, this avoids a deadlock on the SocketWrapper as Streams being processed + * by container threads lock the SocketProcessor before they lock the SocketWrapper which is the opposite + * order to container threads processing via Http2UpgrageHandler. Secondly, this code executes after a + * Window update has released one or more Streams. By dispatching each Stream to a dedicated thread, those + * Streams may progress concurrently. + */ + processSocketEvent(dispatchType.getSocketStatus(), true); + } + } + + + @Override + protected final boolean isPushSupported() { + return stream.isPushSupported(); + } + + + @Override + protected final void doPush(Request pushTarget) { + try { + stream.push(pushTarget); + } catch (IOException ioe) { + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); + response.setErrorException(ioe); + } + } + + + @Override + protected boolean isTrailerFieldsReady() { + return stream.isTrailerFieldsReady(); + } + + + @Override + protected boolean isTrailerFieldsSupported() { + return stream.isTrailerFieldsSupported(); + } + + + @Override + protected String getProtocolRequestId() { + return stream.getIdAsString(); + } + + + @Override + public final void recycle() { + // StreamProcessor instances are not re-used. + + // Calling removeRequestProcessor even though the RequestProcesser was + // never added will add the values from the RequestProcessor to the + // running total for the GlobalRequestProcessor + RequestGroupInfo global = handler.getProtocol().getGlobal(); + if (global != null) { + global.removeRequestProcessor(request.getRequestProcessor()); + } + + // Clear fields that can be cleared to aid GC and trigger NPEs if this + // is reused + setSocketWrapper(null); + } + + + @Override + protected final Log getLog() { + return log; + } + + + @Override + protected ServletConnection getServletConnection() { + return handler.getServletConnection(); + } + + + @Override + public final void pause() { + // NO-OP. Handled by the Http2UpgradeHandler + } + + + @Override + public final SocketState service(SocketWrapperBase socket) throws IOException { + try { + if (validateRequest()) { + adapter.service(request, response); + } else { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + adapter.log(request, response, 0); + setErrorState(ErrorState.CLOSE_CLEAN, null); + } + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("streamProcessor.service.error"), e); + } + response.setStatus(500); + setErrorState(ErrorState.CLOSE_NOW, e); + } + + if (sendfileState == SendfileState.PENDING) { + return SocketState.SENDFILE; + } else if (getErrorState().isError()) { + action(ActionCode.CLOSE, null); + request.updateCounters(); + return SocketState.CLOSED; + } else if (isAsync()) { + return SocketState.LONG; + } else { + action(ActionCode.CLOSE, null); + request.updateCounters(); + return SocketState.CLOSED; + } + } + + + /* + * In HTTP/1.1 some aspects of the request are validated as the request is parsed and the request rejected + * immediately with a 400 response. These checks are performed in Http11InputBuffer. Because, in Tomcat's HTTP/2 + * implementation, incoming frames are processed on one thread while the corresponding request/response is processed + * on a separate thread, rejecting invalid requests is more involved. + * + * One approach would be to validate the request during parsing, note any validation errors and then generate a 400 + * response once processing moves to the separate request/response thread. This would require refactoring to track + * the validation errors. + * + * A second approach, and the one currently adopted, is to perform the validation shortly after processing of the + * received request passes to the separate thread and to generate a 400 response if validation fails. + * + * The checks performed below are based on the checks in Http11InputBuffer. + */ + private boolean validateRequest() { + HttpParser httpParser = new HttpParser(handler.getProtocol().getHttp11Protocol().getRelaxedPathChars(), + handler.getProtocol().getHttp11Protocol().getRelaxedQueryChars()); + + // Method name must be a token + String method = request.method().toString(); + if (!HttpParser.isToken(method)) { + return false; + } + + // Scheme must adhere to RFC 3986 + String scheme = request.scheme().toString(); + if (!HttpParser.isScheme(scheme)) { + return false; + } + + // Invalid character in request target + // (other checks such as valid %nn happen later) + ByteChunk bc = request.requestURI().getByteChunk(); + for (int i = bc.getStart(); i < bc.getEnd(); i++) { + if (httpParser.isNotRequestTargetRelaxed(bc.getBuffer()[i])) { + return false; + } + } + + // Ensure the query string doesn't contain invalid characters. + // (other checks such as valid %nn happen later) + String qs = request.queryString().toString(); + if (qs != null) { + for (char c : qs.toCharArray()) { + if (!httpParser.isQueryRelaxed(c)) { + return false; + } + } + } + + // HTTP header names must be tokens. + // Stream#emitHeader() checks that all the pseudo headers appear first. + MimeHeaders headers = request.getMimeHeaders(); + Enumeration names = headers.names(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if (!H2_PSEUDO_HEADERS_REQUEST.contains(name) && !HttpParser.isToken(name)) { + return false; + } + } + + return true; + } + + + @Override + protected final boolean flushBufferedWrite() throws IOException { + if (log.isTraceEnabled()) { + log.trace(sm.getString("streamProcessor.flushBufferedWrite.entry", stream.getConnectionId(), + stream.getIdAsString())); + } + if (stream.flush(false)) { + // The buffer wasn't fully flushed so re-register the + // stream for write. Note this does not go via the + // Response since the write registration state at + // that level should remain unchanged. Once the buffer + // has been emptied then the code below will call + // dispatch() which will enable the + // Response to respond to this event. + if (stream.isReadyForWrite()) { + // Unexpected + throw new IllegalStateException(); + } + return true; + } + return false; + } + + + @Override + protected final SocketState dispatchEndRequest() throws IOException { + return SocketState.CLOSED; + } +} diff --git a/java/org/apache/coyote/http2/StreamRunnable.java b/java/org/apache/coyote/http2/StreamRunnable.java new file mode 100644 index 0000000..51967d1 --- /dev/null +++ b/java/org/apache/coyote/http2/StreamRunnable.java @@ -0,0 +1,37 @@ +/* + * 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.coyote.http2; + +import org.apache.tomcat.util.net.SocketEvent; + +class StreamRunnable implements Runnable { + + private final StreamProcessor processor; + private final SocketEvent event; + + + StreamRunnable(StreamProcessor processor, SocketEvent event) { + this.processor = processor; + this.event = event; + } + + + @Override + public void run() { + processor.process(event); + } +} diff --git a/java/org/apache/coyote/http2/StreamStateMachine.java b/java/org/apache/coyote/http2/StreamStateMachine.java new file mode 100644 index 0000000..b94c89c --- /dev/null +++ b/java/org/apache/coyote/http2/StreamStateMachine.java @@ -0,0 +1,252 @@ +/* + * 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.coyote.http2; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * See state diagram in RFC 7540.
    + * The following additions are supported by this state machine: + *

      + *
    • differentiate between closed (normal) and closed caused by reset
    • + *
    + */ +class StreamStateMachine { + + private static final Log log = LogFactory.getLog(StreamStateMachine.class); + private static final StringManager sm = StringManager.getManager(StreamStateMachine.class); + + private final String connectionId; + private final String streamId; + + private State state; + + + StreamStateMachine(String connectionId, String streamId) { + this.connectionId = connectionId; + this.streamId = streamId; + stateChange(null, State.IDLE); + } + + + final synchronized void sentPushPromise() { + stateChange(State.IDLE, State.RESERVED_LOCAL); + } + + + final synchronized void sentHeaders() { + // No change if currently OPEN + stateChange(State.RESERVED_LOCAL, State.HALF_CLOSED_REMOTE); + } + + + final synchronized void receivedStartOfHeaders() { + stateChange(State.IDLE, State.OPEN); + stateChange(State.RESERVED_REMOTE, State.HALF_CLOSED_LOCAL); + } + + + final synchronized void sentEndOfStream() { + stateChange(State.OPEN, State.HALF_CLOSED_LOCAL); + stateChange(State.HALF_CLOSED_REMOTE, State.CLOSED_TX); + } + + + final synchronized void receivedEndOfStream() { + stateChange(State.OPEN, State.HALF_CLOSED_REMOTE); + stateChange(State.HALF_CLOSED_LOCAL, State.CLOSED_RX); + } + + + /** + * Marks the stream as reset. This method will not change the stream state if: + *
      + *
    • The stream is already reset
    • + *
    • The stream is already closed
    • + *
    + * + * @throws IllegalStateException If the stream is in a state that does not permit resets + */ + public synchronized void sendReset() { + if (state == State.IDLE) { + throw new IllegalStateException( + sm.getString("streamStateMachine.debug.change", connectionId, streamId, state)); + } + if (state.canReset()) { + stateChange(state, State.CLOSED_RST_TX); + } + } + + + final synchronized void receivedReset() { + stateChange(state, State.CLOSED_RST_RX); + } + + + private void stateChange(State oldState, State newState) { + if (state == oldState) { + state = newState; + if (log.isTraceEnabled()) { + log.trace(sm.getString("streamStateMachine.debug.change", connectionId, streamId, oldState, newState)); + } + } + } + + + final synchronized void checkFrameType(FrameType frameType) throws Http2Exception { + // No state change. Checks that receiving the frame type is valid for + // the current state of this stream. + if (!isFrameTypePermitted(frameType)) { + if (state.connectionErrorForInvalidFrame) { + throw new ConnectionException( + sm.getString("streamStateMachine.invalidFrame", connectionId, streamId, state, frameType), + state.errorCodeForInvalidFrame); + } else { + throw new StreamException( + sm.getString("streamStateMachine.invalidFrame", connectionId, streamId, state, frameType), + state.errorCodeForInvalidFrame, Integer.parseInt(streamId)); + } + } + } + + + final synchronized boolean isFrameTypePermitted(FrameType frameType) { + return state.isFrameTypePermitted(frameType); + } + + + final synchronized boolean isActive() { + return state.isActive(); + } + + + final synchronized boolean canRead() { + return state.canRead(); + } + + + final synchronized boolean canWrite() { + return state.canWrite(); + } + + + final synchronized boolean isClosedFinal() { + return state == State.CLOSED_FINAL; + } + + final synchronized void closeIfIdle() { + stateChange(State.IDLE, State.CLOSED_FINAL); + } + + final synchronized String getCurrentStateName() { + return state.name(); + } + + private enum State { + // @formatter:off + IDLE (false, false, false, true, + Http2Error.PROTOCOL_ERROR, FrameType.HEADERS, + FrameType.PRIORITY), + OPEN (true, true, true, true, + Http2Error.PROTOCOL_ERROR, FrameType.DATA, + FrameType.HEADERS, + FrameType.PRIORITY, + FrameType.RST, + FrameType.PUSH_PROMISE, + FrameType.WINDOW_UPDATE), + RESERVED_LOCAL (false, false, true, true, + Http2Error.PROTOCOL_ERROR, FrameType.PRIORITY, + FrameType.RST, + FrameType.WINDOW_UPDATE), + RESERVED_REMOTE (false, true, true, true, + Http2Error.PROTOCOL_ERROR, FrameType.HEADERS, + FrameType.PRIORITY, + FrameType.RST), + HALF_CLOSED_LOCAL (true, false, true, true, + Http2Error.PROTOCOL_ERROR, FrameType.DATA, + FrameType.HEADERS, + FrameType.PRIORITY, + FrameType.RST, + FrameType.PUSH_PROMISE, + FrameType.WINDOW_UPDATE), + HALF_CLOSED_REMOTE (false, true, true, true, + Http2Error.STREAM_CLOSED, FrameType.PRIORITY, + FrameType.RST, + FrameType.WINDOW_UPDATE), + CLOSED_RX (false, false, false, true, + Http2Error.STREAM_CLOSED, FrameType.PRIORITY), + CLOSED_TX (false, false, false, true, + Http2Error.STREAM_CLOSED, FrameType.PRIORITY, + FrameType.RST, + FrameType.WINDOW_UPDATE), + CLOSED_RST_RX (false, false, false, false, + Http2Error.STREAM_CLOSED, FrameType.PRIORITY), + CLOSED_RST_TX (false, false, false, false, + Http2Error.STREAM_CLOSED, FrameType.DATA, + FrameType.HEADERS, + FrameType.PRIORITY, + FrameType.RST, + FrameType.PUSH_PROMISE, + FrameType.WINDOW_UPDATE), + CLOSED_FINAL (false, false, false, true, + Http2Error.PROTOCOL_ERROR, FrameType.PRIORITY); + // @formatter:on + + private final boolean canRead; + private final boolean canWrite; + private final boolean canReset; + private final boolean connectionErrorForInvalidFrame; + private final Http2Error errorCodeForInvalidFrame; + private final Set frameTypesPermitted; + + State(boolean canRead, boolean canWrite, boolean canReset, boolean connectionErrorForInvalidFrame, + Http2Error errorCode, FrameType... frameTypes) { + this.canRead = canRead; + this.canWrite = canWrite; + this.canReset = canReset; + this.connectionErrorForInvalidFrame = connectionErrorForInvalidFrame; + this.errorCodeForInvalidFrame = errorCode; + frameTypesPermitted = new HashSet<>(Arrays.asList(frameTypes)); + } + + public boolean isActive() { + return canWrite || canRead; + } + + public boolean canRead() { + return canRead; + } + + public boolean canWrite() { + return canWrite; + } + + public boolean canReset() { + return canReset; + } + + public boolean isFrameTypePermitted(FrameType frameType) { + return frameTypesPermitted.contains(frameType); + } + } +} diff --git a/java/org/apache/coyote/http2/WindowAllocationManager.java b/java/org/apache/coyote/http2/WindowAllocationManager.java new file mode 100644 index 0000000..811fe18 --- /dev/null +++ b/java/org/apache/coyote/http2/WindowAllocationManager.java @@ -0,0 +1,241 @@ +/* + * 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.coyote.http2; + +import java.util.concurrent.TimeUnit; + +import org.apache.coyote.ActionCode; +import org.apache.coyote.Response; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Tracks whether the stream is waiting for an allocation to the stream flow control window, to the connection flow + * control window or not waiting for an allocation and only issues allocation notifications when the stream is known to + * be waiting for the notification. + *

    + * It is possible for a stream to be waiting for a connection allocation when a stream allocation is made. Therefore + * this class tracks the type of allocation that the stream is waiting for to ensure that notifications are correctly + * triggered. + *

    + * With the implementation at the time of writing, it is not possible for a stream to receive an unexpected connection + * notification as these are only issues to streams in the backlog and a stream must be waiting for a connection + * allocation in order to be placed on the backlog. However, as a precaution, this class protects against unexpected + * connection notifications. + *

    + * It is important for asynchronous processing not to notify unless a notification is expected else a dispatch will be + * performed unnecessarily which may lead to unexpected results. + *

    + * A previous implementation used separate locks for the stream and connection notifications. However, correct handling + * of allocation waiting requires holding the stream lock when making the decision to wait. Therefore both allocations + * need to wait on the Stream. + */ +class WindowAllocationManager { + + private static final Log log = LogFactory.getLog(WindowAllocationManager.class); + private static final StringManager sm = StringManager.getManager(WindowAllocationManager.class); + + private static final int NONE = 0; + private static final int STREAM = 1; + private static final int CONNECTION = 2; + + private final Stream stream; + + private int waitingFor = NONE; + + WindowAllocationManager(Stream stream) { + this.stream = stream; + } + + void waitForStream(long timeout) throws InterruptedException { + if (log.isDebugEnabled()) { + log.debug(sm.getString("windowAllocationManager.waitFor.stream", stream.getConnectionId(), + stream.getIdAsString(), Long.toString(timeout))); + } + + waitFor(STREAM, timeout); + } + + + void waitForConnection(long timeout) throws InterruptedException { + if (log.isDebugEnabled()) { + log.debug(sm.getString("windowAllocationManager.waitFor.connection", stream.getConnectionId(), + stream.getIdAsString(), Integer.toString(stream.getConnectionAllocationRequested()), + Long.toString(timeout))); + } + + waitFor(CONNECTION, timeout); + } + + + void waitForStreamNonBlocking() { + if (log.isDebugEnabled()) { + log.debug(sm.getString("windowAllocationManager.waitForNonBlocking.stream", stream.getConnectionId(), + stream.getIdAsString())); + } + + waitForNonBlocking(STREAM); + } + + + void waitForConnectionNonBlocking() { + if (log.isDebugEnabled()) { + log.debug(sm.getString("windowAllocationManager.waitForNonBlocking.connection", stream.getConnectionId(), + stream.getIdAsString())); + } + + waitForNonBlocking(CONNECTION); + } + + + void notifyStream() { + notify(STREAM); + } + + + void notifyConnection() { + notify(CONNECTION); + } + + + void notifyAny() { + notify(STREAM | CONNECTION); + } + + + boolean isWaitingForStream() { + return isWaitingFor(STREAM); + } + + + boolean isWaitingForConnection() { + return isWaitingFor(CONNECTION); + } + + + private boolean isWaitingFor(int waitTarget) { + stream.windowAllocationLock.lock(); + try { + return (waitingFor & waitTarget) > 0; + } finally { + stream.windowAllocationLock.unlock(); + } + } + + + private void waitFor(int waitTarget, final long timeout) throws InterruptedException { + stream.windowAllocationLock.lock(); + try { + if (waitingFor != NONE) { + throw new IllegalStateException(sm.getString("windowAllocationManager.waitFor.ise", + stream.getConnectionId(), stream.getIdAsString())); + } + + waitingFor = waitTarget; + long startNanos = -1; + + // Loop to handle spurious wake-ups + do { + if (timeout < 0) { + stream.windowAllocationAvailable.await(); + } else { + long timeoutRemaining; + if (startNanos == -1) { + startNanos = System.nanoTime(); + timeoutRemaining = timeout; + } else { + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); + if (elapsedMillis == 0) { + elapsedMillis = 1; + } + timeoutRemaining = timeout - elapsedMillis; + if (timeoutRemaining <= 0) { + return; + } + } + stream.windowAllocationAvailable.await(timeoutRemaining, TimeUnit.MILLISECONDS); + } + } while (waitingFor != NONE); + } finally { + stream.windowAllocationLock.unlock(); + } + } + + + private void waitForNonBlocking(int waitTarget) { + stream.windowAllocationLock.lock(); + try { + if (waitingFor == NONE) { + waitingFor = waitTarget; + } else if (waitingFor == waitTarget) { + // NO-OP + // Non-blocking post-processing may attempt to flush + } else { + throw new IllegalStateException(sm.getString("windowAllocationManager.waitFor.ise", + stream.getConnectionId(), stream.getIdAsString())); + } + } finally { + stream.windowAllocationLock.unlock(); + } + } + + + private void notify(int notifyTarget) { + + stream.windowAllocationLock.lock(); + try { + if (log.isDebugEnabled()) { + log.debug(sm.getString("windowAllocationManager.notify", stream.getConnectionId(), + stream.getIdAsString(), Integer.toString(waitingFor), Integer.toString(notifyTarget))); + } + + if ((notifyTarget & waitingFor) > NONE) { + // Reset this here so multiple notifies (possible with a + // backlog containing multiple streams and small window updates) + // are handled correctly (only the first should trigger a call + // to stream.notify(). Additional notify() calls may trigger + // unexpected timeouts. + waitingFor = NONE; + Response response = stream.getCoyoteResponse(); + if (response != null) { + if (response.getWriteListener() == null) { + // Blocking, so use notify to release StreamOutputBuffer + if (log.isDebugEnabled()) { + log.debug(sm.getString("windowAllocationManager.notified", stream.getConnectionId(), + stream.getIdAsString())); + } + stream.windowAllocationAvailable.signal(); + } else { + // Non-blocking so dispatch + if (log.isDebugEnabled()) { + log.debug(sm.getString("windowAllocationManager.dispatched", stream.getConnectionId(), + stream.getIdAsString())); + } + response.action(ActionCode.DISPATCH_WRITE, null); + // Need to explicitly execute dispatches on the StreamProcessor + // as this thread is being processed by an UpgradeProcessor + // which won't see this dispatch + response.action(ActionCode.DISPATCH_EXECUTE, null); + } + } + } + } finally { + stream.windowAllocationLock.unlock(); + } + } +} diff --git a/java/org/apache/coyote/mbeans-descriptors.xml b/java/org/apache/coyote/mbeans-descriptors.xml new file mode 100644 index 0000000..e23b15b --- /dev/null +++ b/java/org/apache/coyote/mbeans-descriptors.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/org/apache/el/ExpressionFactoryImpl.java b/java/org/apache/el/ExpressionFactoryImpl.java new file mode 100644 index 0000000..82f5cf1 --- /dev/null +++ b/java/org/apache/el/ExpressionFactoryImpl.java @@ -0,0 +1,83 @@ +/* + * 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.el; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.el.ExpressionFactory; +import jakarta.el.MethodExpression; +import jakarta.el.ValueExpression; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.ExpressionBuilder; +import org.apache.el.stream.StreamELResolverImpl; +import org.apache.el.util.ExceptionUtils; +import org.apache.el.util.MessageFactory; + + +/** + * @see jakarta.el.ExpressionFactory + * + * @author Jacob Hookom [jacob@hookom.net] + */ +@aQute.bnd.annotation.spi.ServiceProvider(value=ExpressionFactory.class) +public class ExpressionFactoryImpl extends ExpressionFactory { + + static { + ExceptionUtils.preload(); + } + + @Override + public T coerceToType(Object obj, Class type) { + return ELSupport.coerceToType(null, obj, type); + } + + @Override + public MethodExpression createMethodExpression(ELContext context, + String expression, Class expectedReturnType, + Class[] expectedParamTypes) { + ExpressionBuilder builder = new ExpressionBuilder(expression, context); + return builder.createMethodExpression(expectedReturnType, + expectedParamTypes); + } + + @Override + public ValueExpression createValueExpression(ELContext context, + String expression, Class expectedType) { + if (expectedType == null) { + throw new NullPointerException(MessageFactory + .get("error.value.expectedType")); + } + ExpressionBuilder builder = new ExpressionBuilder(expression, context); + return builder.createValueExpression(expectedType); + } + + @Override + public ValueExpression createValueExpression(Object instance, + Class expectedType) { + if (expectedType == null) { + throw new NullPointerException(MessageFactory + .get("error.value.expectedType")); + } + return new ValueExpressionLiteral(instance, expectedType); + } + + @Override + public ELResolver getStreamELResolver() { + return new StreamELResolverImpl(); + } +} diff --git a/java/org/apache/el/LocalStrings.properties b/java/org/apache/el/LocalStrings.properties new file mode 100644 index 0000000..57e0d01 --- /dev/null +++ b/java/org/apache/el/LocalStrings.properties @@ -0,0 +1,56 @@ +# 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. + +elSupport.coerce.nonAbstract=Unable to coerce a LambdaExpression to the functional interface [{0}] because the method [{1}] is not abstract + +error.cannotSetVariables=Cannot set variables on factory +error.compare=Cannot compare [{0}] to [{1}] +error.context.null=ELContext was null +error.convert=Cannot convert [{0}] of type [{1}] to [{2}] +error.fnMapper.method=Function [{0}] not found +error.fnMapper.null=Expression uses functions, but no FunctionMapper was provided +error.fnMapper.paramcount=Function [{0}] specifies [{1}] params, but [{2}] were declared +error.function=Problems calling function [{0}] +error.function.tooManyMethodParameterSets=There are multiple sets of parameters specified for function [{0}] +error.identifier.noMethod=Identity [{0}] was null and was unable to invoke +error.identifier.notMethodExpression=Identity [{0}] does not reference a method expression instance, returned type [{1}] +error.identifier.notjava=The identifier [{0}] is not a valid Java identifier as required by section 1.19 of the EL specification (Identifier ::= Java language identifier). This check can be disabled by setting the system property org.apache.el.parser.SKIP_IDENTIFIER_CHECK to true. +error.invalidMethodExpression=Not a valid method expression [{0}] +error.invoke.tooFewParams=The method [{0}] was called with [{1}] parameter(s) when it expected at least [{2}] +error.invoke.wrongParams=The method [{0}] was called with [{1}] parameter(s) when it expected [{2}] +error.lambda.tooManyMethodParameterSets=There are more sets of method parameters specified than there are nested lambda expressions +error.lambda.wrongNestedState=Nested state may only be set once +error.method=Not a valid MethodExpression : [{0}] +error.method.ambiguous=Unable to find unambiguous method: {0}.{1}({2}) +error.method.notfound=Method not found: {0}.{1}({2}) +error.method.nullParms=Parameter types cannot be null +error.mixed=Expression cannot contain both ''#{...}'' and ''${...}'' : [{0}] +error.noFunctionMapperTarget=FunctionMapper target cannot be null +error.noVariableMapperTarget=VariableMapper target cannot be null +error.null=Expression cannot be null +error.nullLocalName=Local name cannot be null +error.nullMethod=Method cannot be null +error.parseFail=Failed to parse the expression [{0}] +error.resolver.unhandled=ELResolver did not handle type: [{0}] with property of [{1}] +error.resolver.unhandled.null=ELResolver cannot handle a null base Object with identifier [{0}] +error.syntax.set=Illegal Syntax for Set Operation +error.unreachable.base=Target Unreachable, identifier [{0}] resolved to null +error.unreachable.property=Target Unreachable, [{0}] returned null +error.value.expectedType=Expected type cannot be null +error.value.literal.write=ValueExpression is a literal and not writable: [{0}] + +stream.compare.notComparable=Stream elements must implement Comparable +stream.optional.empty=It is illegal to call get() on an empty optional +stream.optional.paramNotLambda=The parameter for the method [{0}] should be a lambda expression diff --git a/java/org/apache/el/LocalStrings_es.properties b/java/org/apache/el/LocalStrings_es.properties new file mode 100644 index 0000000..a66aefd --- /dev/null +++ b/java/org/apache/el/LocalStrings_es.properties @@ -0,0 +1,37 @@ +# 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. + +error.compare=No puedo comparar [{0}] con [{1}] +error.context.null=ELContext era nulo +error.convert=No puedo convertir [{0}] desde tipo [{1}] a [{2}] +error.fnMapper.method=Función [{0}] no hallada +error.fnMapper.null=La expresión usa funciones, pero no se ha suministrado FunctionMapper +error.fnMapper.paramcount=La función [{0}] especifica [{1}] parémetros, pero [{2}] fueron declarados +error.function=Problemas llamando a función [{0}] +error.identifier.notjava=El identificador [{0}] no es un identificado Java válido según se requiere en la sección 1.9 de la especificación EL (Identificador ::= identificador de lenguaje Java). Este chequeo se puede desactivar poniendo la propiedad del sistema org.apache.el.parser.SKIP_IDENTIFIER_CHECK a verdad (true). +error.method=No es una MethodExpression válida: [{0}] +error.method.ambiguous=No pude hallar método ambiguo: {0}.{1}({2}) +error.method.notfound=Método no hallado: {0}.{1}({2}) +error.method.nullParms=Los tipos de parámetro no pueden ser nulo +error.mixed=La expresión no puede contenera la vez ''#{..}'' y ''${..}'' : [{0}] +error.noVariableMapperTarget=VariableMapper no puede apuntar a un valor nulo +error.null=La expresión no puede ser nula +error.resolver.unhandled=ELResolver no manejó el tipo: [{0}] con propiedad de [{1}] +error.resolver.unhandled.null=ELResolver no puede manejar un Objeto base nulo con identificador de [{0}] +error.syntax.set=Sitáxis ilegal para Operación de Poner Valor +error.unreachable.base=Objetivo inalcanzable, identificador [{0}] resuelto a nulo +error.unreachable.property=Objetivo inalcanzable, [{0}] devolvió nulo +error.value.expectedType=El tipo esperado no puede ser nulo +error.value.literal.write=ValueExpression es un literal y no un grabable: [{0}] diff --git a/java/org/apache/el/LocalStrings_fr.properties b/java/org/apache/el/LocalStrings_fr.properties new file mode 100644 index 0000000..919641d --- /dev/null +++ b/java/org/apache/el/LocalStrings_fr.properties @@ -0,0 +1,56 @@ +# 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. + +elSupport.coerce.nonAbstract=Impossible de contraindre une LambdaExpression vers l''interface fonctionnelle [{0}] car la méthode [{1}] n''est pas abstraite + +error.cannotSetVariables=Impossible de fixer la valeur des variables de la fabrique +error.compare=Impossible de comparer [{0}] à [{1}] +error.context.null=ELContext est null +error.convert=Impossible de convertir [{0}] de type [{1}] en [{2}] +error.fnMapper.method=La fonction [{0}] n''a pas été trouvée +error.fnMapper.null=L'expression utilise des fonctions, mais aucun FunctionMapper n'a été fourni +error.fnMapper.paramcount=La fonction [{0}] spécifie [{1}] paramètres mais [{2}] ont été déclarés +error.function=Erreurs lors de l''appel de la fonction [{0}] +error.function.tooManyMethodParameterSets=Plusieurs ensembles de paramètres ont été spécifiés pour la fonction [{0}] +error.identifier.noMethod=L''identité [{0}] était null et n''a pu faire l''invocation +error.identifier.notMethodExpression=L''identité [{0}] ne référence pas une instance d''une expression d''une méthode, et a retourné le type [{1}] +error.identifier.notjava=L''identifiant [{0}] n''est pas un identifiant Java valide comme requis par la section 1.19 de la spécification de l''EL (identifiant ::= identifiant de langage Java), ce test peut être désactivé en définissant la propriété système org.apache.el.parser.SKIP_IDENTIFIER_CHECK à true +error.invalidMethodExpression=[{0}] n''est pas une expression de méthode valide +error.invoke.tooFewParams=La méthode [{0}] a été appelée avec [{1}] paramètre(s) alors qu''elle en attendait au moins [{2}] +error.invoke.wrongParams=La méthode [{0}] a été appelée avec [{1}] paramètre(s) alors qu''elle en attendait [{2}] +error.lambda.tooManyMethodParameterSets=Plus d'ensembles de paramètres de méthodes que d'expression lambdas incluses ont été spécifiés +error.lambda.wrongNestedState=L'état inclus ne peut être défini qu'une seule fois +error.method=Pas une MethodExpression valide: [{0}] +error.method.ambiguous=Impossible de trouver une méthode non ambiguë: {0}.{1}({2}) +error.method.notfound=La méthode n''a pas été trouvée: {0}.{1}({2}) +error.method.nullParms=Les types des paramètres ne peuvent pas être null +error.mixed=L''expression ne peut pas contenir à la fois ''#{...}'' et ''${...}'' : [{0}] +error.noFunctionMapperTarget=La cible du FunctionMapper ne peut pas être null +error.noVariableMapperTarget=La cible du VariableMapper ne peut pas être null +error.null=L'expression ne peut pas être null +error.nullLocalName=Le nom local ne peut pas être null +error.nullMethod=La méthode ne peut pas être null +error.parseFail=Impossible de traiter l''expression [{0}] +error.resolver.unhandled=L''ELResolver n''a pas géré le type: [{0}] avec la propriété de [{1}] +error.resolver.unhandled.null=L''ELResolver ne peut pas gérer un objet de base null avec l''identifiant [{0}] +error.syntax.set=Syntaxe invalide pour l'opération Set +error.unreachable.base=La cible est inaccessible, l''identifiant [{0}] est résolu en null +error.unreachable.property=La cible est inaccessible, [{0}] a renvoyé null +error.value.expectedType=Le type attendu ne peut pas être null +error.value.literal.write=La ValueExpression est un littéral et ne peut pas être écrite: [{0}] + +stream.compare.notComparable=Les éléments du flux doivent implémenter Comparable +stream.optional.empty=Il est illégal d'appeler get() sur un optionnel vide +stream.optional.paramNotLambda=Le paramètre de la méthode [{0}] devrait être une expression lambda diff --git a/java/org/apache/el/LocalStrings_ja.properties b/java/org/apache/el/LocalStrings_ja.properties new file mode 100644 index 0000000..ce87e79 --- /dev/null +++ b/java/org/apache/el/LocalStrings_ja.properties @@ -0,0 +1,56 @@ +# 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. + +elSupport.coerce.nonAbstract=メソッド [{1}] ãŒabstractã§ã¯ãªã„ãŸã‚ã€LambdaExpressionã‚’functional インターフェイス [{0}] ã«å¼·åˆ¶å¤‰æ›ã§ãã¾ã›ã‚“ + +error.cannotSetVariables=ファクトリã§å¤‰æ•°ã‚’設定ã§ãã¾ã›ã‚“ +error.compare=[{0}] 㨠[{1}] を比較ã§ãã¾ã›ã‚“ +error.context.null=ELContextãŒnullã§ã—㟠+error.convert=åž‹ [{1}] ã® [{0}] ã‚’ [{2}] ã«å¤‰æ›ã§ãã¾ã›ã‚“ +error.fnMapper.method=Function[{0}]ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +error.fnMapper.null=å¼ã¯é–¢æ•°ã‚’使用ã—ã¾ã™ãŒã€FunctionMapperãŒæä¾›ã•ã‚Œã¦ã„ã¾ã›ã‚“ +error.fnMapper.paramcount=関数 [{0}] 㯠[{1}] パラメータを指定ã—ã¦ã„ã¾ã™ãŒã€[{2}] ãŒå®£è¨€ã•ã‚Œã¦ã„ã¾ã™ +error.function=関数[{0}]ã®å‘¼ã³å‡ºã—ã§å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—㟠+error.function.tooManyMethodParameterSets=関数[{0}]ã«æŒ‡å®šã•ã‚ŒãŸãƒ‘ラメーターã®ã‚»ãƒƒãƒˆãŒè¤‡æ•°ã‚ã‚Šã¾ã™ +error.identifier.noMethod=識別å­[{0}]ãŒnullã§ã‚ã‚Šã€å‘¼ã³å‡ºã™ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—㟠+error.identifier.notMethodExpression=è­˜åˆ¥å­ [{0}] ã¯ãƒ¡ã‚½ãƒƒãƒ‰å¼ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’å‚ç…§ã—ã¦ã„ã¾ã›ã‚“。返å´ã•ã‚ŒãŸåž‹ã¯ [{1}] ã§ã™ +error.identifier.notjava=識別å­[{0}]ã¯ã€EL仕様ã®ã‚»ã‚¯ã‚·ãƒ§ãƒ³1.19ã§è¦æ±‚ã•ã‚Œã¦ã„る有効ãªJava識別å­ã§ã¯ã‚ã‚Šã¾ã›ã‚“(識別å­::= Java言語識別å­ï¼‰ã€‚ ã“ã®ãƒã‚§ãƒƒã‚¯ã¯ã€ã‚·ã‚¹ãƒ†ãƒ ãƒ—ロパティorg.apache.el.parser.SKIP_IDENTIFIER_CHECKã‚’trueã«è¨­å®šã™ã‚‹ã“ã¨ã§ç„¡åŠ¹ã«ã§ãã¾ã™ã€‚ +error.invalidMethodExpression=有効ãªãƒ¡ã‚½ãƒƒãƒ‰å¼ã§ã¯ã‚ã‚Šã¾ã›ã‚“[{0}] +error.invoke.tooFewParams=メソッド [{0}] ã¯ã€å°‘ãªãã¨ã‚‚ [{2}] 個ã®ãƒ‘ラメータãŒå¿…è¦ãªã¨ãã«ã€[{1}] 個ã®ãƒ‘ラメーターを指定ã—ã¦å‘¼ã³å‡ºã•ã‚Œã¾ã—ãŸ\n +error.invoke.wrongParams=メソッド [{0}] ã¯ã€[{2}] 個ã®ãƒ‘ラメータを期待ã—ã¦ã„ãŸã¨ãã«ã€[{1}] 個ã®ãƒ‘ラメーターを指定ã—ã¦å‘¼ã³å‡ºã•ã‚Œã¾ã—㟠+error.lambda.tooManyMethodParameterSets=ãƒã‚¹ãƒˆã•ã‚ŒãŸãƒ©ãƒ ãƒ€å¼ã‚ˆã‚Šã‚‚多ãã®ãƒ¡ã‚½ãƒƒãƒ‰ãƒ‘ラメーターã®ã‚»ãƒƒãƒˆãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ +error.lambda.wrongNestedState=ãƒã‚¹ãƒˆã•ã‚ŒãŸçŠ¶æ…‹ã¯ä¸€åº¦ã ã‘設定ã§ãã¾ã™ +error.method=MethodExpression [{0}] ã¯æœ‰åŠ¹ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +error.method.ambiguous=明確ãªãƒ¡ã‚½ãƒƒãƒ‰ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“: {0}.{1}({2}) +error.method.notfound=メソッドãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“: {0}.{1}({2}) +error.method.nullParms=パラメータタイプã¯nullã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +error.mixed=å¼ã«ã¯ ''#{...}'' 㨠''${...}'' ã®ä¸¡æ–¹ã‚’å«ã‚ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“: [{0}] +error.noFunctionMapperTarget=FunctionMapperã¯nullã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +error.noVariableMapperTarget=VariableMapperã¯nullã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +error.null=å¼ã¯nullã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +error.nullLocalName=Functionã®åå‰ã¯nullã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +error.nullMethod=Methodã¯nullã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +error.parseFail=å¼ [{0}] ã®è§£æžã«å¤±æ•—ã—ã¾ã—㟠+error.resolver.unhandled=ELResolver 㯠プロパティ [{1}] ã‚’æŒã¤åž‹ [{0}] を処ç†ã—ã¾ã›ã‚“ã§ã—㟠+error.resolver.unhandled.null=ELResolverã¯è­˜åˆ¥å­ [{0}] ã‚’æŒã¤nullベースã®ã‚ªãƒ–ジェクトを処ç†ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +error.syntax.set=Set æ“作ã®æ§‹æ–‡ãŒç„¡åŠ¹ã§ã™ +error.unreachable.base=ターゲットã«åˆ°é”ã§ãã¾ã›ã‚“。識別å­[{0}]ã¯nullã«è§£æ±ºã•ã‚Œã¾ã—㟠+error.unreachable.property=ターゲットã«åˆ°é”ã§ãã¾ã›ã‚“。[{0}]ã¯nullã‚’è¿”ã—ã¾ã—㟠+error.value.expectedType=期待ã•ã‚Œã‚‹åž‹ã‚’nullã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +error.value.literal.write=ValueExpressionã¯ãƒªãƒ†ãƒ©ãƒ«ã§ã‚ã‚Šã€æ›¸ãè¾¼ã¿å¯èƒ½ã§ã¯ã‚ã‚Šã¾ã›ã‚“: [{0}] + +stream.compare.notComparable=Streamè¦ç´ ã¯Comparableを実装ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“ +stream.optional.empty=空ã®Optionalã§get()を呼ã³å‡ºã™ã“ã¨ã¯é•æ³•ã§ã™ +stream.optional.paramNotLambda=メソッド [{0}] ã®ãƒ‘ラメータã¯ãƒ©ãƒ ãƒ€å¼ã§ã‚ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ diff --git a/java/org/apache/el/LocalStrings_ko.properties b/java/org/apache/el/LocalStrings_ko.properties new file mode 100644 index 0000000..d339518 --- /dev/null +++ b/java/org/apache/el/LocalStrings_ko.properties @@ -0,0 +1,56 @@ +# 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. + +elSupport.coerce.nonAbstract=메소드 [{1}](ì´)ê°€ ì¶”ìƒ ë©”ì†Œë“œê°€ 아니기 때문ì—, 람다 표현ì‹ì„ 함수 ì¸í„°íŽ˜ì´ìŠ¤ [{0}](으)ë¡œ ê°•ì œ 변환할 수 없습니다. + +error.cannotSetVariables=íŒ©í† ë¦¬ì— ë³€ìˆ˜ë“¤ì„ ì„¤ì •í•  수 없습니다. +error.compare=[{0}](ì„)를 [{1}](와)ê³¼ 비êµí•  수 없습니다. +error.context.null=ELContextê°€ ë„입니다. +error.convert=타입 [{1}]ì¸ [{0}](ì„)를 [{2}](으)ë¡œ 변환할 수 없습니다. +error.fnMapper.method=함수 [{0}](ì„)를 ì°¾ì„ ìˆ˜ 없습니다. +error.fnMapper.null=표현ì‹ì´ í•¨ìˆ˜ë“¤ì„ ì‚¬ìš©í•˜ê³  있지만, FunctionMapperê°€ 제공ë˜ì§€ 않았습니다. +error.fnMapper.paramcount=함수 [{0}](ì´)ê°€ [{1}] ê°œì˜ íŒŒë¼ë¯¸í„°ë“¤ì„ 지정하고 있지만, [{2}] 개만 ì„ ì–¸ë˜ì–´ 있습니다. +error.function=함수 [{0}] 호출 중 문제 ë°œìƒ +error.function.tooManyMethodParameterSets=함수 [{0}]ì— ì§€ì •ëœ íŒŒë¼ë¯¸í„°ë“¤ì— 대해 복수 ê°œì˜ ì§‘í•©ë“¤ì´ ì¡´ìž¬í•©ë‹ˆë‹¤. +error.identifier.noMethod=Identity [{0}](ì´)ê°€ ë„ì´ì–´ì„œ 호출할 수 없습니다. +error.identifier.notMethodExpression=Identity [{0}](ì´)ê°€ MethodExpression ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 구현하고 있지 ì•Šì€ íƒ€ìž… [{1}]입니다. +error.identifier.notjava=ì‹ë³„ìž [{0}](ì€)는 EL 스펙 1.19ì—ì„œ 요구하는 유효한 ìžë°” ì‹ë³„ìžê°€ 아닙니다 (ì‹ë³„ìž ::= ìžë°” 언어 ì‹ë³„ìž). ì´ ì‹ë³„ìž ê²€ì‚¬ëŠ” 시스템 프로í¼í‹° org.apache.el.parser.SKIP_IDENTIFIER_CHECK를 trueë¡œ 설정하면 비활성화할 수 있습니다. +error.invalidMethodExpression=유효한 메소드 표현ì‹ì´ 아닙니다: [{0}] +error.invoke.tooFewParams=메소드 [{0}](ì€)는 최소 [{2}] ê°œì˜ íŒŒë¼ë¯¸í„°ë“¤ì„ 요구하는ë°, [{1}] ê°œì˜ íŒŒë¼ë¯¸í„°ë“¤ê³¼ 함께 호출ë˜ì—ˆìŠµë‹ˆë‹¤. +error.invoke.wrongParams=메소드 [{0}](ì€)는 [{2}] ê°œì˜ íŒŒë¼ë¯¸í„°ë“¤ì„ 요구하는ë°, [{1}] ê°œì˜ íŒŒë¼ë¯¸í„°ë“¤ê³¼ 함께 호출ë˜ì—ˆìŠµë‹ˆë‹¤. +error.lambda.tooManyMethodParameterSets=ë‚´ìž¬ëœ ëžŒë‹¤ 표현ì‹ì— ì§€ì •ëœ íŒŒë¼ë¯¸í„°ë“¤ì˜ 개수보다 ë§Žì€ ë©”ì†Œë“œ 파ë¼ë¯¸í„°ì˜ ì§‘í•©ë“¤ì´ ì¡´ìž¬í•©ë‹ˆë‹¤. +error.lambda.wrongNestedState=NestedState는 ì˜¤ì§ í•œë²ˆë§Œ ì„¤ì •ë  ìˆ˜ 있습니다. +error.method=유효하지 ì•Šì€ ë©”ì†Œë“œ 표현ì‹: [{0}] +error.method.ambiguous=모호하지 않게 메소드를 ì°¾ì„ ìˆ˜ 없습니다: {0}.{1}({2}) +error.method.notfound=메소드를 ì°¾ì„ ìˆ˜ 없습니다: {0}.{1}({2}) +error.method.nullParms=파ë¼ë¯¸í„° íƒ€ìž…ë“¤ì€ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +error.mixed=표현ì‹ì€ ''#{...}''와 ''${...}'', 둘 모ë‘를 í¬í•¨í•  수 없습니다: [{0}] +error.noFunctionMapperTarget=FunctionMapper 대ìƒì€ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +error.noVariableMapperTarget=VariableMapper 대ìƒì€ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +error.null=표현ì‹ì€ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +error.nullLocalName=localNameì´ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +error.nullMethod=메소드는 ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +error.parseFail=í‘œí˜„ì‹ [{0}]ì„(를) 파싱하지 못했습니다. +error.resolver.unhandled=ELResolverê°€ 프로í¼í‹° [{1}](ì„)를 가진 타입 [{0}](ì„)를 처리하지 못했습니다. +error.resolver.unhandled.null=ELResolverê°€ ì‹ë³„ìž [{0}]ì¸ ë„ ê¸°ë°˜ ê°ì²´ë¥¼ 처리할 수 없습니다. +error.syntax.set=ê°’ 설정 오í¼ë ˆì´ì…˜ì€ 허용ë˜ì§€ 않습니다. +error.unreachable.base=ëŒ€ìƒ ê°ì²´ ì ‘ê·¼ 불가, ì‹ë³„ìž [{0}](ì´)ê°€ ë„입니다. +error.unreachable.property=ëŒ€ìƒ ê°ì²´ ì ‘ê·¼ 불가, [{0}](ì´)ê°€ ë„ì„ ë°˜í™˜í–ˆìŠµë‹ˆë‹¤. +error.value.expectedType=기대ë˜ëŠ” íƒ€ìž…ì€ ë„ì¼ ìˆ˜ 없습니다. +error.value.literal.write=ValueExpressionì€ ë¦¬í„°ëŸ´ì´ì§€, 쓰기 가능하지 않습니다: [{0}] + +stream.compare.notComparable=스트림 ìš”ì†Œë“¤ì€ ë°˜ë“œì‹œ Comparable ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 구현해야 합니다. +stream.optional.empty=빈 Optionalì— ëŒ€í•´ get()ì„ í˜¸ì¶œí•˜ëŠ” ê²ƒì€ í—ˆìš©ë˜ì§€ 않습니다. +stream.optional.paramNotLambda=메소드 [{0}]ì˜ íŒŒë¼ë¯¸í„°ëŠ” 람다 표현ì‹ì´ì–´ì•¼ 합니다. diff --git a/java/org/apache/el/LocalStrings_zh_CN.properties b/java/org/apache/el/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..da20b96 --- /dev/null +++ b/java/org/apache/el/LocalStrings_zh_CN.properties @@ -0,0 +1,56 @@ +# 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. + +elSupport.coerce.nonAbstract=无法将LambdaExpression强制到函数接å£[{0}],因为方法[{1}]ä¸æ˜¯æŠ½è±¡çš„ + +error.cannotSetVariables=ä¸èƒ½åœ¨å·¥åŽ‚上设置å˜é‡ +error.compare=无法将[{0}]与[{1}]进行比较 +error.context.null=ELConect为空 +error.convert=无法将类型为[{1}]çš„[{0}]转æ¢ä¸º[{2}] +error.fnMapper.method=找ä¸åˆ°å‡½æ•°[{0}] +error.fnMapper.null=表达å¼ä½¿ç”¨å‡½æ•°ï¼Œä½†æœªæä¾›FunctionMapper +error.fnMapper.paramcount=函数[{0}]指定了[{1}]å‚数,但声明了[{2}] +error.function=调用函数[{0}]时出现问题 +error.function.tooManyMethodParameterSets=为函数[{0}]指定了多组å‚æ•° +error.identifier.noMethod=标识[{0}]为空,无法调用 +error.identifier.notMethodExpression=Identity [{0}]未引用方法表达å¼å®žä¾‹ï¼Œè¿”回类型[{1}] +error.identifier.notjava=标识符[{0}]ä¸æ˜¯EL规范第1.19节è¦æ±‚的有效Java标识符(标识符::=Java语言标识符)。å¯ä»¥é€šè¿‡å°†ç³»ç»Ÿå±žæ€§org.apache.el.parser.SKIP_IDENTIFIER_CHECK设置为trueæ¥ç¦ç”¨æ­¤æ£€æŸ¥ã€‚ +error.invalidMethodExpression=ä¸æ˜¯æœ‰æ•ˆçš„方法表达å¼[{0}] +error.invoke.tooFewParams=方法[{0}]在调用时入å‚为 [{1}],预期入å‚至少为 [{2}] +error.invoke.wrongParams=方法[{0}]在调用时入å‚为 [{1}],预期入å‚为 [{2}] +error.lambda.tooManyMethodParameterSets=指定的方法å‚数集多于嵌套lambda表达å¼çš„é›†åˆ +error.lambda.wrongNestedState=嵌套状æ€åªèƒ½è®¾ç½®ä¸€æ¬¡ +error.method=无效的方法表达å¼ï¼š[{0}] +error.method.ambiguous=找ä¸åˆ°æ˜Žç¡®çš„方法:{0}。{1}({2}) +error.method.notfound=找ä¸åˆ°æ–¹æ³•ï¼š{0}。{1}({2}) +error.method.nullParms=å‚数类型ä¸èƒ½ä¸ºnull +error.mixed=表达å¼ä¸èƒ½åŒæ—¶åŒ…å« ''#{...}'' and ''${...}'' : [{0}] +error.noFunctionMapperTarget=FunctionMapper目标ä¸èƒ½ä¸ºç©º +error.noVariableMapperTarget=VariableMapper目标ä¸èƒ½ä¸ºç©º +error.null=表达å¼ä¸èƒ½ä¸ºnull +error.nullLocalName=本地å称ä¸èƒ½ä¸ºnull +error.nullMethod=方法ä¸èƒ½ä¸ºnull +error.parseFail=解æžè¡¨è¾¾å¼[{0}]失败 +error.resolver.unhandled=ELResolver未处ç†å±žæ€§ä¸º[{1}]的类型:[{0}] +error.resolver.unhandled.null=ELResolver无法处ç†æ ‡è¯†ç¬¦ä¸º[{0}]的空基对象 +error.syntax.set=Setæ“作的语法éžæ³• +error.unreachable.base=目标ä¸å¯è¾¾ï¼Œæ ‡è¯†ç¬¦[{0}]解æžä¸ºnull +error.unreachable.property=目标ä¸å¯è¾¾ï¼Œ[{0}]返回null +error.value.expectedType=预期的类型ä¸èƒ½ä¸ºnull +error.value.literal.write=ValueExpression是文字,ä¸å¯å†™ï¼š[{0}] + +stream.compare.notComparable=Stream中的元素必须实现了ComparableæŽ¥å£ +stream.optional.empty=对空å¯é€‰è°ƒç”¨get()是éžæ³•çš„ +stream.optional.paramNotLambda=方法[{0}]çš„å‚数应为lambdaè¡¨è¾¾å¼ diff --git a/java/org/apache/el/MethodExpressionImpl.java b/java/org/apache/el/MethodExpressionImpl.java new file mode 100644 index 0000000..539bdca --- /dev/null +++ b/java/org/apache/el/MethodExpressionImpl.java @@ -0,0 +1,329 @@ +/* + * 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.el; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.FunctionMapper; +import jakarta.el.MethodExpression; +import jakarta.el.MethodInfo; +import jakarta.el.MethodNotFoundException; +import jakarta.el.MethodReference; +import jakarta.el.PropertyNotFoundException; +import jakarta.el.VariableMapper; + +import org.apache.el.lang.EvaluationContext; +import org.apache.el.lang.ExpressionBuilder; +import org.apache.el.parser.Node; +import org.apache.el.util.ReflectionUtil; + + +/** + * An Expression that refers to a method on an object. + * + *

    + * The {@link jakarta.el.ExpressionFactory#createMethodExpression} method + * can be used to parse an expression string and return a concrete instance + * of MethodExpression that encapsulates the parsed expression. + * The {@link FunctionMapper} is used at parse time, not evaluation time, + * so one is not needed to evaluate an expression using this class. + * However, the {@link ELContext} is needed at evaluation time.

    + * + *

    The {@link #getMethodInfo} and {@link #invoke} methods will evaluate the + * expression each time they are called. The {@link jakarta.el.ELResolver} in the + * ELContext is used to resolve the top-level variables and to + * determine the behavior of the . and [] + * operators. For any of the two methods, the + * {@link jakarta.el.ELResolver#getValue} method is used to resolve all properties + * up to but excluding the last one. This provides the base object + * on which the method appears. If the base object is null, a + * NullPointerException must be thrown. At the last resolution, + * the final property is then coerced to a String, + * which provides the name of the method to be found. A method matching the + * name and expected parameters provided at parse time is found and it is + * either queried or invoked (depending on the method called on this + * MethodExpression).

    + * + *

    See the notes about comparison, serialization and immutability in + * the {@link jakarta.el.Expression} javadocs. + * + * @see jakarta.el.ELResolver + * @see jakarta.el.Expression + * @see jakarta.el.ExpressionFactory + * @see jakarta.el.MethodExpression + * + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class MethodExpressionImpl extends MethodExpression implements + Externalizable { + + private Class expectedType; + + private String expr; + + private FunctionMapper fnMapper; + + private VariableMapper varMapper; + + private transient Node node; + + private Class[] paramTypes; + + public MethodExpressionImpl() { + super(); + } + + public MethodExpressionImpl(String expr, Node node, + FunctionMapper fnMapper, VariableMapper varMapper, + Class expectedType, Class[] paramTypes) { + super(); + this.expr = expr; + this.node = node; + this.fnMapper = fnMapper; + this.varMapper = varMapper; + this.expectedType = expectedType; + this.paramTypes = paramTypes; + } + + /** + * Determines whether the specified object is equal to this + * Expression. + * + *

    + * The result is true if and only if the argument is not + * null, is an Expression object that is the + * of the same type (ValueExpression or + * MethodExpression), and has an identical parsed + * representation. + *

    + * + *

    + * Note that two expressions can be equal if their expression Strings are + * different. For example, ${fn1:foo()} and + * ${fn2:foo()} are equal if their corresponding + * FunctionMappers mapped fn1:foo and + * fn2:foo to the same method. + *

    + * + * @param obj + * the Object to test for equality. + * @return true if obj equals this + * Expression; false otherwise. + * @see java.util.Hashtable + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + return (obj instanceof MethodExpressionImpl && obj.hashCode() == this + .hashCode()); + } + + /** + * Returns the original String used to create this Expression, + * unmodified. + * + *

    + * This is used for debugging purposes but also for the purposes of + * comparison (e.g. to ensure the expression in a configuration file has not + * changed). + *

    + * + *

    + * This method does not provide sufficient information to re-create an + * expression. Two different expressions can have exactly the same + * expression string but different function mappings. Serialization should + * be used to save and restore the state of an Expression. + *

    + * + * @return The original expression String. + * + * @see jakarta.el.Expression#getExpressionString() + */ + @Override + public String getExpressionString() { + return this.expr; + } + + /** + * Evaluates the expression relative to the provided context, and returns + * information about the actual referenced method. + * + * @param context + * The context of this evaluation + * @return an instance of MethodInfo containing information + * about the method the expression evaluated to. + * @throws NullPointerException + * if context is null or the base object is + * null on the last resolution. + * @throws PropertyNotFoundException + * if one of the property resolutions failed because a specified + * variable or property does not exist or is not readable. + * @throws MethodNotFoundException + * if no suitable method can be found. + * @throws ELException + * if an exception was thrown while performing property or + * variable resolution. The thrown exception must be included as + * the cause property of this exception, if available. + * @see jakarta.el.MethodExpression#getMethodInfo(jakarta.el.ELContext) + */ + @Override + public MethodInfo getMethodInfo(ELContext context) + throws PropertyNotFoundException, MethodNotFoundException, + ELException { + Node n = this.getNode(); + EvaluationContext ctx = new EvaluationContext(context, this.fnMapper, + this.varMapper); + ctx.notifyBeforeEvaluation(getExpressionString()); + MethodInfo result = n.getMethodInfo(ctx, this.paramTypes); + ctx.notifyAfterEvaluation(getExpressionString()); + return result; + } + + private Node getNode() throws ELException { + if (this.node == null) { + this.node = ExpressionBuilder.createNode(this.expr); + } + return this.node; + } + + /** + * Returns the hash code for this Expression. + * + *

    + * See the note in the {@link #equals} method on how two expressions can be + * equal if their expression Strings are different. Recall that if two + * objects are equal according to the equals(Object) method, + * then calling the hashCode method on each of the two + * objects must produce the same integer result. Implementations must take + * special note and implement hashCode correctly. + *

    + * + * @return The hash code for this Expression. + * @see #equals + * @see java.util.Hashtable + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return this.expr.hashCode(); + } + + /** + * Evaluates the expression relative to the provided context, invokes the + * method that was found using the supplied parameters, and returns the + * result of the method invocation. + * + * @param context + * The context of this evaluation. + * @param params + * The parameters to pass to the method, or null + * if no parameters. + * @return the result of the method invocation (null if the + * method has a void return type). + * @throws NullPointerException + * if context is null or the base object is + * null on the last resolution. + * @throws PropertyNotFoundException + * if one of the property resolutions failed because a specified + * variable or property does not exist or is not readable. + * @throws MethodNotFoundException + * if no suitable method can be found. + * @throws ELException + * if an exception was thrown while performing property or + * variable resolution. The thrown exception must be included as + * the cause property of this exception, if available. If the + * exception thrown is an InvocationTargetException, + * extract its cause and pass it to the + * ELException constructor. + * @see jakarta.el.MethodExpression#invoke(jakarta.el.ELContext, + * java.lang.Object[]) + */ + @Override + public Object invoke(ELContext context, Object[] params) + throws PropertyNotFoundException, MethodNotFoundException, + ELException { + EvaluationContext ctx = new EvaluationContext(context, this.fnMapper, + this.varMapper); + ctx.notifyBeforeEvaluation(getExpressionString()); + Object result = this.getNode().invoke(ctx, this.paramTypes, params); + ctx.notifyAfterEvaluation(getExpressionString()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.io.Externalizable#readExternal(java.io.ObjectInput) + */ + @Override + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.expr = in.readUTF(); + String type = in.readUTF(); + if (!type.isEmpty()) { + this.expectedType = ReflectionUtil.forName(type); + } + this.paramTypes = ReflectionUtil.toTypeArray(((String[]) in + .readObject())); + this.fnMapper = (FunctionMapper) in.readObject(); + this.varMapper = (VariableMapper) in.readObject(); + } + + /* + * (non-Javadoc) + * + * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput) + */ + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(this.expr); + out.writeUTF((this.expectedType != null) ? this.expectedType.getName() + : ""); + out.writeObject(ReflectionUtil.toTypeNameArray(this.paramTypes)); + out.writeObject(this.fnMapper); + out.writeObject(this.varMapper); + } + + @Override + public boolean isLiteralText() { + return false; + } + + + /** + * @since EL 3.0 + */ + @Override + public boolean isParametersProvided() { + return this.getNode().isParametersProvided(); + } + + + @Override + public MethodReference getMethodReference(ELContext context) { + EvaluationContext ctx = new EvaluationContext(context, this.fnMapper, this.varMapper); + ctx.notifyBeforeEvaluation(getExpressionString()); + MethodReference methodReference = this.getNode().getMethodReference(ctx); + ctx.notifyAfterEvaluation(getExpressionString()); + return methodReference; + } +} diff --git a/java/org/apache/el/MethodExpressionLiteral.java b/java/org/apache/el/MethodExpressionLiteral.java new file mode 100644 index 0000000..9fd0046 --- /dev/null +++ b/java/org/apache/el/MethodExpressionLiteral.java @@ -0,0 +1,134 @@ +/* + * 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.el; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.lang.annotation.Annotation; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.MethodExpression; +import jakarta.el.MethodInfo; +import jakarta.el.MethodReference; + +import org.apache.el.util.MessageFactory; +import org.apache.el.util.ReflectionUtil; + + +public class MethodExpressionLiteral extends MethodExpression implements Externalizable { + + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + private Class expectedType; + + private String expr; + + private Class[] paramTypes; + + public MethodExpressionLiteral() { + // do nothing + } + + public MethodExpressionLiteral(String expr, Class expectedType, + Class[] paramTypes) { + this.expr = expr; + this.expectedType = expectedType; + this.paramTypes = paramTypes; + } + + @Override + public MethodInfo getMethodInfo(ELContext context) throws ELException { + context.notifyBeforeEvaluation(getExpressionString()); + MethodInfo result = getMethodInfoInternal(); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } + + + private MethodInfo getMethodInfoInternal() throws ELException { + return new MethodInfo(this.expr, this.expectedType, this.paramTypes); + } + + @Override + public Object invoke(ELContext context, Object[] params) throws ELException { + context.notifyBeforeEvaluation(getExpressionString()); + Object result; + if (this.expectedType != null) { + result = context.convertToType(this.expr, this.expectedType); + } else { + result = this.expr; + } + context.notifyAfterEvaluation(getExpressionString()); + return result; + } + + + @Override + public MethodReference getMethodReference(ELContext context) { + if (context == null) { + throw new NullPointerException(MessageFactory.get("error.context.null")); + } + context.notifyBeforeEvaluation(getExpressionString()); + MethodReference result = + new MethodReference(null, getMethodInfoInternal(), EMPTY_ANNOTATION_ARRAY, EMPTY_OBJECT_ARRAY); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } + + @Override + public String getExpressionString() { + return this.expr; + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof MethodExpressionLiteral && this.hashCode() == obj.hashCode()); + } + + @Override + public int hashCode() { + return this.expr.hashCode(); + } + + @Override + public boolean isLiteralText() { + return true; + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + this.expr = in.readUTF(); + String type = in.readUTF(); + if (!type.isEmpty()) { + this.expectedType = ReflectionUtil.forName(type); + } + this.paramTypes = ReflectionUtil.toTypeArray(((String[]) in + .readObject())); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(this.expr); + out.writeUTF((this.expectedType != null) ? this.expectedType.getName() + : ""); + out.writeObject(ReflectionUtil.toTypeNameArray(this.paramTypes)); + } +} diff --git a/java/org/apache/el/ValueExpressionImpl.java b/java/org/apache/el/ValueExpressionImpl.java new file mode 100644 index 0000000..686be04 --- /dev/null +++ b/java/org/apache/el/ValueExpressionImpl.java @@ -0,0 +1,293 @@ +/* + * 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.el; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.FunctionMapper; +import jakarta.el.PropertyNotFoundException; +import jakarta.el.PropertyNotWritableException; +import jakarta.el.ValueExpression; +import jakarta.el.ValueReference; +import jakarta.el.VariableMapper; + +import org.apache.el.lang.EvaluationContext; +import org.apache.el.lang.ExpressionBuilder; +import org.apache.el.parser.AstLiteralExpression; +import org.apache.el.parser.Node; +import org.apache.el.util.ReflectionUtil; + + +/** + * An Expression that can get or set a value. + * + *

    + * In previous incarnations of this API, expressions could only be read. + * ValueExpression objects can now be used both to retrieve a + * value and to set a value. Expressions that can have a value set on them are + * referred to as l-value expressions. Those that cannot are referred to as + * r-value expressions. Not all r-value expressions can be used as l-value + * expressions (e.g. "${1+1}" or + * "${firstName} ${lastName}"). See the EL Specification for + * details. Expressions that cannot be used as l-values must always return + * true from isReadOnly(). + *

    + * + *

    + * The {@link jakarta.el.ExpressionFactory#createValueExpression} method + * can be used to parse an expression string and return a concrete instance + * of ValueExpression that encapsulates the parsed expression. + * The {@link FunctionMapper} is used at parse time, not evaluation time, + * so one is not needed to evaluate an expression using this class. + * However, the {@link ELContext} is needed at evaluation time.

    + * + *

    The {@link #getValue}, {@link #setValue}, {@link #isReadOnly} and + * {@link #getType} methods will evaluate the expression each time they are + * called. The {@link jakarta.el.ELResolver} in the ELContext is used + * to resolve the top-level variables and to determine the behavior of the + * . and [] operators. For any of the four methods, + * the {@link jakarta.el.ELResolver#getValue} method is used to resolve all + * properties up to but excluding the last one. This provides the + * base object. At the last resolution, the + * ValueExpression will call the corresponding + * {@link jakarta.el.ELResolver#getValue}, {@link jakarta.el.ELResolver#setValue}, + * {@link jakarta.el.ELResolver#isReadOnly} or {@link jakarta.el.ELResolver#getType} + * method, depending on which was called on the ValueExpression. + *

    + * + *

    See the notes about comparison, serialization and immutability in + * the {@link jakarta.el.Expression} javadocs. + * + * @see jakarta.el.ELResolver + * @see jakarta.el.Expression + * @see jakarta.el.ExpressionFactory + * @see jakarta.el.ValueExpression + * + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class ValueExpressionImpl extends ValueExpression implements + Externalizable { + + private Class expectedType; + + private String expr; + + private FunctionMapper fnMapper; + + private VariableMapper varMapper; + + private transient Node node; + + public ValueExpressionImpl() { + super(); + } + + public ValueExpressionImpl(String expr, Node node, FunctionMapper fnMapper, + VariableMapper varMapper, Class expectedType) { + this.expr = expr; + this.node = node; + this.fnMapper = fnMapper; + this.varMapper = varMapper; + this.expectedType = expectedType; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ValueExpressionImpl)) { + return false; + } + if (obj.hashCode() != this.hashCode()) { + return false; + } + + return this.getNode().equals(((ValueExpressionImpl) obj).getNode()); + } + + /* + * (non-Javadoc) + * + * @see jakarta.el.ValueExpression#getExpectedType() + */ + @Override + public Class getExpectedType() { + return this.expectedType; + } + + /** + * Returns the type the result of the expression will be coerced to after + * evaluation. + * + * @return the expectedType passed to the + * ExpressionFactory.createValueExpression method + * that created this ValueExpression. + * + * @see jakarta.el.Expression#getExpressionString() + */ + @Override + public String getExpressionString() { + return this.expr; + } + + private Node getNode() throws ELException { + if (this.node == null) { + this.node = ExpressionBuilder.createNode(this.expr); + } + return this.node; + } + + /* + * (non-Javadoc) + * + * @see jakarta.el.ValueExpression#getType(jakarta.el.ELContext) + */ + @Override + public Class getType(ELContext context) throws PropertyNotFoundException, + ELException { + EvaluationContext ctx = new EvaluationContext(context, this.fnMapper, + this.varMapper); + context.notifyBeforeEvaluation(getExpressionString()); + Class result = this.getNode().getType(ctx); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } + + /* + * (non-Javadoc) + * + * @see jakarta.el.ValueExpression#getValue(jakarta.el.ELContext) + */ + @SuppressWarnings("unchecked") + @Override + public T getValue(ELContext context) throws PropertyNotFoundException, + ELException { + EvaluationContext ctx = new EvaluationContext(context, this.fnMapper, + this.varMapper); + context.notifyBeforeEvaluation(getExpressionString()); + Object value = this.getNode().getValue(ctx); + if (this.expectedType != null) { + value = context.convertToType(value, this.expectedType); + } + context.notifyAfterEvaluation(getExpressionString()); + return (T) value; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return this.getNode().hashCode(); + } + + /* + * (non-Javadoc) + * + * @see jakarta.el.ValueExpression#isLiteralText() + */ + @Override + public boolean isLiteralText() { + try { + return this.getNode() instanceof AstLiteralExpression; + } catch (ELException ele) { + return false; + } + } + + /* + * (non-Javadoc) + * + * @see jakarta.el.ValueExpression#isReadOnly(jakarta.el.ELContext) + */ + @Override + public boolean isReadOnly(ELContext context) + throws PropertyNotFoundException, ELException { + EvaluationContext ctx = new EvaluationContext(context, this.fnMapper, + this.varMapper); + context.notifyBeforeEvaluation(getExpressionString()); + boolean result = this.getNode().isReadOnly(ctx); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } + + @Override + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.expr = in.readUTF(); + String type = in.readUTF(); + if (!type.isEmpty()) { + this.expectedType = ReflectionUtil.forName(type); + } + this.fnMapper = (FunctionMapper) in.readObject(); + this.varMapper = (VariableMapper) in.readObject(); + } + + /* + * (non-Javadoc) + * + * @see jakarta.el.ValueExpression#setValue(jakarta.el.ELContext, + * java.lang.Object) + */ + @Override + public void setValue(ELContext context, Object value) + throws PropertyNotFoundException, PropertyNotWritableException, + ELException { + EvaluationContext ctx = new EvaluationContext(context, this.fnMapper, + this.varMapper); + context.notifyBeforeEvaluation(getExpressionString()); + this.getNode().setValue(ctx, value); + context.notifyAfterEvaluation(getExpressionString()); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(this.expr); + out.writeUTF((this.expectedType != null) ? this.expectedType.getName() + : ""); + out.writeObject(this.fnMapper); + out.writeObject(this.varMapper); + } + + @Override + public String toString() { + return "ValueExpression["+this.expr+"]"; + } + + /** + * @since EL 2.2 + */ + @Override + public ValueReference getValueReference(ELContext context) { + EvaluationContext ctx = new EvaluationContext(context, this.fnMapper, + this.varMapper); + context.notifyBeforeEvaluation(getExpressionString()); + ValueReference result = this.getNode().getValueReference(ctx); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } +} diff --git a/java/org/apache/el/ValueExpressionLiteral.java b/java/org/apache/el/ValueExpressionLiteral.java new file mode 100644 index 0000000..2ecfe8d --- /dev/null +++ b/java/org/apache/el/ValueExpressionLiteral.java @@ -0,0 +1,137 @@ +/* + * 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.el; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import jakarta.el.ELContext; +import jakarta.el.PropertyNotWritableException; +import jakarta.el.ValueExpression; + +import org.apache.el.util.MessageFactory; +import org.apache.el.util.ReflectionUtil; + + +public final class ValueExpressionLiteral extends ValueExpression implements + Externalizable { + + private static final long serialVersionUID = 1L; + + private Object value; + private String valueString; + + private Class expectedType; + + public ValueExpressionLiteral() { + super(); + } + + public ValueExpressionLiteral(Object value, Class expectedType) { + this.value = value; + this.expectedType = expectedType; + } + + @SuppressWarnings("unchecked") + @Override + public T getValue(ELContext context) { + context.notifyBeforeEvaluation(getExpressionString()); + Object result; + if (this.expectedType != null) { + result = context.convertToType(this.value, this.expectedType); + } else { + result = this.value; + } + context.notifyAfterEvaluation(getExpressionString()); + return (T) result; + } + + @Override + public void setValue(ELContext context, Object value) { + context.notifyBeforeEvaluation(getExpressionString()); + throw new PropertyNotWritableException(MessageFactory.get( + "error.value.literal.write", this.value)); + } + + @Override + public boolean isReadOnly(ELContext context) { + context.notifyBeforeEvaluation(getExpressionString()); + context.notifyAfterEvaluation(getExpressionString()); + return true; + } + + @Override + public Class getType(ELContext context) { + context.notifyBeforeEvaluation(getExpressionString()); + Class result = (this.value != null) ? this.value.getClass() : null; + context.notifyAfterEvaluation(getExpressionString()); + return result; + } + + @Override + public Class getExpectedType() { + return this.expectedType; + } + + @Override + public String getExpressionString() { + if (this.valueString == null) { + this.valueString = (this.value != null) ? this.value.toString() : null; + } + return this.valueString; + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof ValueExpressionLiteral && this + .equals((ValueExpressionLiteral) obj)); + } + + public boolean equals(ValueExpressionLiteral ve) { + return (ve != null && (this.value != null && ve.value != null && (this.value == ve.value || this.value + .equals(ve.value)))); + } + + @Override + public int hashCode() { + return (this.value != null) ? this.value.hashCode() : 0; + } + + @Override + public boolean isLiteralText() { + return true; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(this.value); + out.writeUTF((this.expectedType != null) ? this.expectedType.getName() + : ""); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.value = in.readObject(); + String type = in.readUTF(); + if (!type.isEmpty()) { + this.expectedType = ReflectionUtil.forName(type); + } + } +} diff --git a/java/org/apache/el/lang/ELArithmetic.java b/java/org/apache/el/lang/ELArithmetic.java new file mode 100644 index 0000000..de7daca --- /dev/null +++ b/java/org/apache/el/lang/ELArithmetic.java @@ -0,0 +1,405 @@ +/* + * 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.el.lang; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; + +import jakarta.el.ELException; + +import org.apache.el.util.MessageFactory; + + +/** + * A helper class of Arithmetic defined by the EL Specification + * @author Jacob Hookom [jacob@hookom.net] + */ +public abstract class ELArithmetic { + + public static final class BigDecimalDelegate extends ELArithmetic { + + @Override + protected Number add(Number num0, Number num1) { + return ((BigDecimal) num0).add((BigDecimal) num1); + } + + @Override + protected Number coerce(Number num) { + if (num instanceof BigDecimal) { + return num; + } + if (num instanceof BigInteger) { + return new BigDecimal((BigInteger) num); + } + return new BigDecimal(num.doubleValue()); + } + + @Override + protected Number coerce(String str) { + return new BigDecimal(str); + } + + @Override + protected Number divide(Number num0, Number num1) { + return ((BigDecimal) num0).divide((BigDecimal) num1, + RoundingMode.HALF_UP); + } + + @Override + protected Number subtract(Number num0, Number num1) { + return ((BigDecimal) num0).subtract((BigDecimal) num1); + } + + @Override + protected Number mod(Number num0, Number num1) { + return Double.valueOf(num0.doubleValue() % num1.doubleValue()); + } + + @Override + protected Number multiply(Number num0, Number num1) { + return ((BigDecimal) num0).multiply((BigDecimal) num1); + } + + @Override + public boolean matches(Object obj0, Object obj1) { + return (obj0 instanceof BigDecimal || obj1 instanceof BigDecimal); + } + } + + public static final class BigIntegerDelegate extends ELArithmetic { + + @Override + protected Number add(Number num0, Number num1) { + return ((BigInteger) num0).add((BigInteger) num1); + } + + @Override + protected Number coerce(Number num) { + if (num instanceof BigInteger) { + return num; + } + return new BigInteger(num.toString()); + } + + @Override + protected Number coerce(String str) { + return new BigInteger(str); + } + + @Override + protected Number divide(Number num0, Number num1) { + return (new BigDecimal((BigInteger) num0)).divide(new BigDecimal((BigInteger) num1), RoundingMode.HALF_UP); + } + + @Override + protected Number multiply(Number num0, Number num1) { + return ((BigInteger) num0).multiply((BigInteger) num1); + } + + @Override + protected Number mod(Number num0, Number num1) { + return ((BigInteger) num0).remainder((BigInteger) num1); + } + + @Override + protected Number subtract(Number num0, Number num1) { + return ((BigInteger) num0).subtract((BigInteger) num1); + } + + @Override + public boolean matches(Object obj0, Object obj1) { + return (obj0 instanceof BigInteger || obj1 instanceof BigInteger); + } + } + + public static final class DoubleDelegate extends ELArithmetic { + + @Override + protected Number add(Number num0, Number num1) { + // could only be one of these + if (num0 instanceof BigDecimal) { + return ((BigDecimal) num0).add(new BigDecimal(num1.doubleValue())); + } else if (num1 instanceof BigDecimal) { + return ((new BigDecimal(num0.doubleValue()).add((BigDecimal) num1))); + } + return Double.valueOf(num0.doubleValue() + num1.doubleValue()); + } + + @Override + protected Number coerce(Number num) { + if (num instanceof Double) { + return num; + } + if (num instanceof BigInteger) { + return new BigDecimal((BigInteger) num); + } + return Double.valueOf(num.doubleValue()); + } + + @Override + protected Number coerce(String str) { + return Double.valueOf(str); + } + + @Override + protected Number divide(Number num0, Number num1) { + return Double.valueOf(num0.doubleValue() / num1.doubleValue()); + } + + @Override + protected Number mod(Number num0, Number num1) { + return Double.valueOf(num0.doubleValue() % num1.doubleValue()); + } + + @Override + protected Number subtract(Number num0, Number num1) { + // could only be one of these + if (num0 instanceof BigDecimal) { + return ((BigDecimal) num0).subtract(new BigDecimal(num1.doubleValue())); + } else if (num1 instanceof BigDecimal) { + return ((new BigDecimal(num0.doubleValue()).subtract((BigDecimal) num1))); + } + return Double.valueOf(num0.doubleValue() - num1.doubleValue()); + } + + @Override + protected Number multiply(Number num0, Number num1) { + // could only be one of these + if (num0 instanceof BigDecimal) { + return ((BigDecimal) num0).multiply(new BigDecimal(num1.doubleValue())); + } else if (num1 instanceof BigDecimal) { + return ((new BigDecimal(num0.doubleValue()).multiply((BigDecimal) num1))); + } + return Double.valueOf(num0.doubleValue() * num1.doubleValue()); + } + + @Override + public boolean matches(Object obj0, Object obj1) { + return (obj0 instanceof Double + || obj1 instanceof Double + || obj0 instanceof Float + || obj1 instanceof Float + || (obj0 instanceof String && ELSupport + .isStringFloat((String) obj0)) || (obj1 instanceof String && ELSupport + .isStringFloat((String) obj1))); + } + } + + public static final class LongDelegate extends ELArithmetic { + + @Override + protected Number add(Number num0, Number num1) { + return Long.valueOf(num0.longValue() + num1.longValue()); + } + + @Override + protected Number coerce(Number num) { + if (num instanceof Long) { + return num; + } + return Long.valueOf(num.longValue()); + } + + @Override + protected Number coerce(String str) { + return Long.valueOf(str); + } + + @Override + protected Number divide(Number num0, Number num1) { + return Long.valueOf(num0.longValue() / num1.longValue()); + } + + @Override + protected Number mod(Number num0, Number num1) { + return Long.valueOf(num0.longValue() % num1.longValue()); + } + + @Override + protected Number subtract(Number num0, Number num1) { + return Long.valueOf(num0.longValue() - num1.longValue()); + } + + @Override + protected Number multiply(Number num0, Number num1) { + return Long.valueOf(num0.longValue() * num1.longValue()); + } + + @Override + public boolean matches(Object obj0, Object obj1) { + return (obj0 instanceof Long || obj1 instanceof Long); + } + } + + public static final BigDecimalDelegate BIGDECIMAL = new BigDecimalDelegate(); + + public static final BigIntegerDelegate BIGINTEGER = new BigIntegerDelegate(); + + public static final DoubleDelegate DOUBLE = new DoubleDelegate(); + + public static final LongDelegate LONG = new LongDelegate(); + + private static final Long ZERO = Long.valueOf(0); + + public static final Number add(final Object obj0, final Object obj1) { + final ELArithmetic delegate = findDelegate(obj0, obj1); + if (delegate == null) { + return Long.valueOf(0); + } + + Number num0 = delegate.coerce(obj0); + Number num1 = delegate.coerce(obj1); + + return delegate.add(num0, num1); + } + + public static final Number mod(final Object obj0, final Object obj1) { + if (obj0 == null && obj1 == null) { + return Long.valueOf(0); + } + + final ELArithmetic delegate; + if (BIGDECIMAL.matches(obj0, obj1)) { + delegate = DOUBLE; + } else if (DOUBLE.matches(obj0, obj1)) { + delegate = DOUBLE; + } else if (BIGINTEGER.matches(obj0, obj1)) { + delegate = BIGINTEGER; + } else { + delegate = LONG; + } + + Number num0 = delegate.coerce(obj0); + Number num1 = delegate.coerce(obj1); + + return delegate.mod(num0, num1); + } + + public static final Number subtract(final Object obj0, final Object obj1) { + final ELArithmetic delegate = findDelegate(obj0, obj1); + if (delegate == null) { + return Long.valueOf(0); + } + + Number num0 = delegate.coerce(obj0); + Number num1 = delegate.coerce(obj1); + + return delegate.subtract(num0, num1); + } + + public static final Number divide(final Object obj0, final Object obj1) { + if (obj0 == null && obj1 == null) { + return ZERO; + } + + final ELArithmetic delegate; + if (BIGDECIMAL.matches(obj0, obj1)) { + delegate = BIGDECIMAL; + } else if (BIGINTEGER.matches(obj0, obj1)) { + delegate = BIGDECIMAL; + } else { + delegate = DOUBLE; + } + + Number num0 = delegate.coerce(obj0); + Number num1 = delegate.coerce(obj1); + + return delegate.divide(num0, num1); + } + + public static final Number multiply(final Object obj0, final Object obj1) { + final ELArithmetic delegate = findDelegate(obj0, obj1); + if (delegate == null) { + return Long.valueOf(0); + } + + Number num0 = delegate.coerce(obj0); + Number num1 = delegate.coerce(obj1); + + return delegate.multiply(num0, num1); + } + + private static ELArithmetic findDelegate(final Object obj0, final Object obj1) { + if (obj0 == null && obj1 == null) { + return null; + } + + if (BIGDECIMAL.matches(obj0, obj1)) { + return BIGDECIMAL; + } else if (DOUBLE.matches(obj0, obj1)) { + if (BIGINTEGER.matches(obj0, obj1)) { + return BIGDECIMAL; + } else { + return DOUBLE; + } + } else if (BIGINTEGER.matches(obj0, obj1)) { + return BIGINTEGER; + } else { + return LONG; + } + } + + public static final boolean isNumber(final Object obj) { + return (obj != null && isNumberType(obj.getClass())); + } + + public static final boolean isNumberType(final Class type) { + return type == Long.TYPE || type == Double.TYPE || + type == Byte.TYPE || type == Short.TYPE || + type == Integer.TYPE || type == Float.TYPE || + Number.class.isAssignableFrom(type); + } + + protected ELArithmetic() { + super(); + } + + protected abstract Number add(Number num0, Number num1); + + protected abstract Number multiply(Number num0, Number num1); + + protected abstract Number subtract(Number num0, Number num1); + + protected abstract Number mod(Number num0, Number num1); + + protected abstract Number coerce(Number num); + + protected final Number coerce(final Object obj) { + + if (isNumber(obj)) { + return coerce((Number) obj); + } + if (obj == null || "".equals(obj)) { + return coerce(ZERO); + } + if (obj instanceof String) { + return coerce((String) obj); + } + if (obj instanceof Character) { + return coerce(Short.valueOf((short) ((Character) obj).charValue())); + } + + throw new ELException(MessageFactory.get("error.convert", obj, obj.getClass(), "Number")); + } + + protected abstract Number coerce(String str); + + protected abstract Number divide(Number num0, Number num1); + + protected abstract boolean matches(Object obj0, Object obj1); +} diff --git a/java/org/apache/el/lang/ELSupport.java b/java/org/apache/el/lang/ELSupport.java new file mode 100644 index 0000000..aafb057 --- /dev/null +++ b/java/org/apache/el/lang/ELSupport.java @@ -0,0 +1,766 @@ +/* + * 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.el.lang; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorManager; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.LambdaExpression; + +import org.apache.el.util.MessageFactory; + + +/** + * A helper class that implements the EL Specification + * + * @author Jacob Hookom [jacob@hookom.net] + */ +public class ELSupport { + + private static final Long ZERO = Long.valueOf(0L); + + protected static final boolean COERCE_TO_ZERO; + + static { + String coerceToZeroStr; + if (System.getSecurityManager() != null) { + coerceToZeroStr = AccessController.doPrivileged( + (PrivilegedAction) () -> System.getProperty( + "org.apache.el.parser.COERCE_TO_ZERO", "false") + ); + } else { + coerceToZeroStr = System.getProperty( + "org.apache.el.parser.COERCE_TO_ZERO", "false"); + } + COERCE_TO_ZERO = Boolean.parseBoolean(coerceToZeroStr); + } + + + /** + * Compare two objects, after coercing to the same type if appropriate. + * + * If the objects are identical, or they are equal according to + * {@link #equals(ELContext, Object, Object)} then return 0. + * + * If either object is a BigDecimal, then coerce both to BigDecimal first. + * Similarly for Double(Float), BigInteger, and Long(Integer, Char, Short, Byte). + * + * Otherwise, check that the first object is an instance of Comparable, and compare + * against the second object. If that is null, return 1, otherwise + * return the result of comparing against the second object. + * + * Similarly, if the second object is Comparable, if the first is null, return -1, + * else return the result of comparing against the first object. + * + * A null object is considered as: + *

      + *
    • ZERO when compared with Numbers
    • + *
    • the empty string for String compares
    • + *
    • Otherwise null is considered to be lower than anything else.
    • + *
    + * + * @param ctx the context in which this comparison is taking place + * @param obj0 first object + * @param obj1 second object + * @return -1, 0, or 1 if this object is less than, equal to, or greater than val. + * @throws ELException if neither object is Comparable + * @throws ClassCastException if the objects are not mutually comparable + */ + public static final int compare(final ELContext ctx, final Object obj0, final Object obj1) + throws ELException { + if (obj0 == obj1 || equals(ctx, obj0, obj1)) { + return 0; + } + if (isBigDecimalOp(obj0, obj1)) { + BigDecimal bd0 = (BigDecimal) coerceToNumber(ctx, obj0, BigDecimal.class); + BigDecimal bd1 = (BigDecimal) coerceToNumber(ctx, obj1, BigDecimal.class); + return bd0.compareTo(bd1); + } + if (isDoubleOp(obj0, obj1)) { + Double d0 = (Double) coerceToNumber(ctx, obj0, Double.class); + Double d1 = (Double) coerceToNumber(ctx, obj1, Double.class); + return d0.compareTo(d1); + } + if (isBigIntegerOp(obj0, obj1)) { + BigInteger bi0 = (BigInteger) coerceToNumber(ctx, obj0, BigInteger.class); + BigInteger bi1 = (BigInteger) coerceToNumber(ctx, obj1, BigInteger.class); + return bi0.compareTo(bi1); + } + if (isLongOp(obj0, obj1)) { + Long l0 = (Long) coerceToNumber(ctx, obj0, Long.class); + Long l1 = (Long) coerceToNumber(ctx, obj1, Long.class); + return l0.compareTo(l1); + } + if (obj0 instanceof String || obj1 instanceof String) { + return coerceToString(ctx, obj0).compareTo(coerceToString(ctx, obj1)); + } + if (obj0 instanceof Comparable) { + @SuppressWarnings("unchecked") // checked above + final Comparable comparable = (Comparable) obj0; + return (obj1 != null) ? comparable.compareTo(obj1) : 1; + } + if (obj1 instanceof Comparable) { + @SuppressWarnings("unchecked") // checked above + final Comparable comparable = (Comparable) obj1; + return (obj0 != null) ? -comparable.compareTo(obj0) : -1; + } + throw new ELException(MessageFactory.get("error.compare", obj0, obj1)); + } + + /** + * Compare two objects for equality, after coercing to the same type if appropriate. + * + * If the objects are identical (including both null) return true. + * If either object is null, return false. + * If either object is Boolean, coerce both to Boolean and check equality. + * Similarly for Enum, String, BigDecimal, Double(Float), Long(Integer, Short, Byte, Character) + * Otherwise default to using Object.equals(). + * + * @param ctx the context in which this equality test is taking place + * @param obj0 the first object + * @param obj1 the second object + * @return true if the objects are equal + * @throws ELException if one of the coercion fails + */ + public static final boolean equals(final ELContext ctx, final Object obj0, final Object obj1) + throws ELException { + if (obj0 == obj1) { + return true; + } else if (obj0 == null || obj1 == null) { + return false; + } else if (isBigDecimalOp(obj0, obj1)) { + BigDecimal bd0 = (BigDecimal) coerceToNumber(ctx, obj0, BigDecimal.class); + BigDecimal bd1 = (BigDecimal) coerceToNumber(ctx, obj1, BigDecimal.class); + return bd0.equals(bd1); + } else if (isDoubleOp(obj0, obj1)) { + Double d0 = (Double) coerceToNumber(ctx, obj0, Double.class); + Double d1 = (Double) coerceToNumber(ctx, obj1, Double.class); + return d0.equals(d1); + } else if (isBigIntegerOp(obj0, obj1)) { + BigInteger bi0 = (BigInteger) coerceToNumber(ctx, obj0, BigInteger.class); + BigInteger bi1 = (BigInteger) coerceToNumber(ctx, obj1, BigInteger.class); + return bi0.equals(bi1); + } else if (isLongOp(obj0, obj1)) { + Long l0 = (Long) coerceToNumber(ctx, obj0, Long.class); + Long l1 = (Long) coerceToNumber(ctx, obj1, Long.class); + return l0.equals(l1); + } else if (obj0 instanceof Boolean || obj1 instanceof Boolean) { + return coerceToBoolean(ctx, obj0, false).equals(coerceToBoolean(ctx, obj1, false)); + } else if (obj0.getClass().isEnum()) { + return obj0.equals(coerceToEnum(ctx, obj1, obj0.getClass())); + } else if (obj1.getClass().isEnum()) { + return obj1.equals(coerceToEnum(ctx, obj0, obj1.getClass())); + } else if (obj0 instanceof String || obj1 instanceof String) { + int lexCompare = coerceToString(ctx, obj0).compareTo(coerceToString(ctx, obj1)); + return (lexCompare == 0) ? true : false; + } else { + return obj0.equals(obj1); + } + } + + // Going to have to have some casts /raw types somewhere so doing it here + // keeps them all in one place. There might be a neater / better solution + // but I couldn't find it + @SuppressWarnings("unchecked") + public static final Enum coerceToEnum(final ELContext ctx, final Object obj, + @SuppressWarnings("rawtypes") Class type) { + + if (ctx != null) { + boolean originalIsPropertyResolved = ctx.isPropertyResolved(); + try { + Object result = ctx.getELResolver().convertToType(ctx, obj, type); + if (ctx.isPropertyResolved()) { + return (Enum) result; + } + } finally { + ctx.setPropertyResolved(originalIsPropertyResolved); + } + } + + if (obj == null || "".equals(obj)) { + return null; + } + if (type.isAssignableFrom(obj.getClass())) { + return (Enum) obj; + } + + if (!(obj instanceof String)) { + throw new ELException(MessageFactory.get("error.convert", + obj, obj.getClass(), type)); + } + + Enum result; + try { + result = Enum.valueOf(type, (String) obj); + } catch (IllegalArgumentException iae) { + throw new ELException(MessageFactory.get("error.convert", + obj, obj.getClass(), type)); + } + return result; + } + + /** + * Convert an object to Boolean. + * Null and empty string are false. + * @param ctx the context in which this conversion is taking place + * @param obj the object to convert + * @param primitive is the target a primitive in which case coercion to null + * is not permitted + * @return the Boolean value of the object + * @throws ELException if object is not Boolean or String + */ + public static final Boolean coerceToBoolean(final ELContext ctx, final Object obj, + boolean primitive) throws ELException { + + if (ctx != null) { + boolean originalIsPropertyResolved = ctx.isPropertyResolved(); + try { + Object result = ctx.getELResolver().convertToType(ctx, obj, Boolean.class); + if (ctx.isPropertyResolved()) { + return (Boolean) result; + } + } finally { + ctx.setPropertyResolved(originalIsPropertyResolved); + } + } + + if (!COERCE_TO_ZERO && !primitive) { + if (obj == null) { + return null; + } + } + + if (obj == null || "".equals(obj)) { + return Boolean.FALSE; + } + if (obj instanceof Boolean) { + return (Boolean) obj; + } + if (obj instanceof String) { + return Boolean.valueOf((String) obj); + } + + throw new ELException(MessageFactory.get("error.convert", + obj, obj.getClass(), Boolean.class)); + } + + private static Character coerceToCharacter(final ELContext ctx, final Object obj) + throws ELException { + + if (ctx != null) { + boolean originalIsPropertyResolved = ctx.isPropertyResolved(); + try { + Object result = ctx.getELResolver().convertToType(ctx, obj, Character.class); + if (ctx.isPropertyResolved()) { + return (Character) result; + } + } finally { + ctx.setPropertyResolved(originalIsPropertyResolved); + } + } + + if (obj == null || "".equals(obj)) { + return Character.valueOf((char) 0); + } + if (obj instanceof String) { + return Character.valueOf(((String) obj).charAt(0)); + } + if (ELArithmetic.isNumber(obj)) { + return Character.valueOf((char) ((Number) obj).shortValue()); + } + Class objType = obj.getClass(); + if (obj instanceof Character) { + return (Character) obj; + } + + throw new ELException(MessageFactory.get("error.convert", + obj, objType, Character.class)); + } + + protected static final Number coerceToNumber(final Number number, + final Class type) throws ELException { + if (Long.TYPE == type || Long.class.equals(type)) { + return Long.valueOf(number.longValue()); + } + if (Double.TYPE == type || Double.class.equals(type)) { + return Double.valueOf(number.doubleValue()); + } + if (Integer.TYPE == type || Integer.class.equals(type)) { + return Integer.valueOf(number.intValue()); + } + if (BigInteger.class.equals(type)) { + if (number instanceof BigDecimal) { + return ((BigDecimal) number).toBigInteger(); + } + if (number instanceof BigInteger) { + return number; + } + return BigInteger.valueOf(number.longValue()); + } + if (BigDecimal.class.equals(type)) { + if (number instanceof BigDecimal) { + return number; + } + if (number instanceof BigInteger) { + return new BigDecimal((BigInteger) number); + } + return new BigDecimal(number.doubleValue()); + } + if (Byte.TYPE == type || Byte.class.equals(type)) { + return Byte.valueOf(number.byteValue()); + } + if (Short.TYPE == type || Short.class.equals(type)) { + return Short.valueOf(number.shortValue()); + } + if (Float.TYPE == type || Float.class.equals(type)) { + return Float.valueOf(number.floatValue()); + } + if (Number.class.equals(type)) { + return number; + } + + throw new ELException(MessageFactory.get("error.convert", + number, number.getClass(), type)); + } + + public static final Number coerceToNumber(final ELContext ctx, final Object obj, + final Class type) throws ELException { + + if (ctx != null) { + boolean originalIsPropertyResolved = ctx.isPropertyResolved(); + try { + Object result = ctx.getELResolver().convertToType(ctx, obj, type); + if (ctx.isPropertyResolved()) { + return (Number) result; + } + } finally { + ctx.setPropertyResolved(originalIsPropertyResolved); + } + } + + if (!COERCE_TO_ZERO) { + if (obj == null && !type.isPrimitive()) { + return null; + } + } + + if (obj == null || "".equals(obj)) { + return coerceToNumber(ZERO, type); + } + if (obj instanceof String) { + return coerceToNumber((String) obj, type); + } + if (ELArithmetic.isNumber(obj)) { + return coerceToNumber((Number) obj, type); + } + + if (obj instanceof Character) { + return coerceToNumber(Short.valueOf((short) ((Character) obj) + .charValue()), type); + } + + throw new ELException(MessageFactory.get("error.convert", + obj, obj.getClass(), type)); + } + + protected static final Number coerceToNumber(final String val, + final Class type) throws ELException { + if (Long.TYPE == type || Long.class.equals(type)) { + try { + return Long.valueOf(val); + } catch (NumberFormatException nfe) { + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + } + if (Integer.TYPE == type || Integer.class.equals(type)) { + try { + return Integer.valueOf(val); + } catch (NumberFormatException nfe) { + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + } + if (Double.TYPE == type || Double.class.equals(type)) { + try { + return Double.valueOf(val); + } catch (NumberFormatException nfe) { + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + } + if (BigInteger.class.equals(type)) { + try { + return new BigInteger(val); + } catch (NumberFormatException nfe) { + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + } + if (BigDecimal.class.equals(type)) { + try { + return new BigDecimal(val); + } catch (NumberFormatException nfe) { + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + } + if (Byte.TYPE == type || Byte.class.equals(type)) { + try { + return Byte.valueOf(val); + } catch (NumberFormatException nfe) { + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + } + if (Short.TYPE == type || Short.class.equals(type)) { + try { + return Short.valueOf(val); + } catch (NumberFormatException nfe) { + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + } + if (Float.TYPE == type || Float.class.equals(type)) { + try { + return Float.valueOf(val); + } catch (NumberFormatException nfe) { + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + } + + throw new ELException(MessageFactory.get("error.convert", + val, String.class, type)); + } + + /** + * Coerce an object to a string. + * @param ctx the context in which this conversion is taking place + * @param obj the object to convert + * @return the String value of the object + */ + public static final String coerceToString(final ELContext ctx, final Object obj) { + + if (ctx != null) { + boolean originalIsPropertyResolved = ctx.isPropertyResolved(); + try { + Object result = ctx.getELResolver().convertToType(ctx, obj, String.class); + if (ctx.isPropertyResolved()) { + return (String) result; + } + } finally { + ctx.setPropertyResolved(originalIsPropertyResolved); + } + } + + if (obj == null) { + return ""; + } else if (obj instanceof String) { + return (String) obj; + } else if (obj instanceof Enum) { + return ((Enum) obj).name(); + } else { + return obj.toString(); + } + } + + public static final T coerceToType(final ELContext ctx, final Object obj, + final Class type) throws ELException { + + if (ctx != null) { + boolean originalIsPropertyResolved = ctx.isPropertyResolved(); + try { + T result = ctx.getELResolver().convertToType(ctx, obj, type); + if (ctx.isPropertyResolved()) { + return result; + } + } finally { + ctx.setPropertyResolved(originalIsPropertyResolved); + } + } + + if (type == null || Object.class.equals(type) || + (obj != null && type.isAssignableFrom(obj.getClass()))) { + @SuppressWarnings("unchecked") + T result = (T) obj; + return result; + } + + if (!COERCE_TO_ZERO) { + if (obj == null && !type.isPrimitive() && + !String.class.isAssignableFrom(type)) { + return null; + } + } + + if (String.class.equals(type)) { + @SuppressWarnings("unchecked") + T result = (T) coerceToString(ctx, obj); + return result; + } + if (ELArithmetic.isNumberType(type)) { + @SuppressWarnings("unchecked") + T result = (T) coerceToNumber(ctx, obj, type); + return result; + } + if (Character.class.equals(type) || Character.TYPE == type) { + @SuppressWarnings("unchecked") + T result = (T) coerceToCharacter(ctx, obj); + return result; + } + if (Boolean.class.equals(type) || Boolean.TYPE == type) { + @SuppressWarnings("unchecked") + T result = (T) coerceToBoolean(ctx, obj, Boolean.TYPE == type); + return result; + } + if (type.isEnum()) { + @SuppressWarnings("unchecked") + T result = (T) coerceToEnum(ctx, obj, type); + return result; + } + + // new to spec + if (obj == null) { + return null; + } + if (obj instanceof String) { + String str = (String) obj; + PropertyEditor editor = PropertyEditorManager.findEditor(type); + if (editor == null) { + if (str.isEmpty()) { + return null; + } + throw new ELException(MessageFactory.get("error.convert", obj, + obj.getClass(), type)); + } else { + try { + editor.setAsText(str); + @SuppressWarnings("unchecked") + T result = (T) editor.getValue(); + return result; + } catch (RuntimeException e) { + if (str.isEmpty()) { + return null; + } + throw new ELException(MessageFactory.get("error.convert", + obj, obj.getClass(), type), e); + } + } + } + + // Handle special case because the syntax for the empty set is the same + // for an empty map. The parser will always parse {} as an empty set. + if (obj instanceof Set && type == Map.class && + ((Set) obj).isEmpty()) { + @SuppressWarnings("unchecked") + T result = (T) Collections.EMPTY_MAP; + return result; + } + + // Handle arrays + if (type.isArray() && obj.getClass().isArray()) { + @SuppressWarnings("unchecked") + T result = (T) coerceToArray(ctx, obj, type); + return result; + } + + if (obj instanceof LambdaExpression && isFunctionalInterface(type)) { + T result = coerceToFunctionalInterface(ctx, (LambdaExpression) obj, type); + return result; + } + + throw new ELException(MessageFactory.get("error.convert", + obj, obj.getClass(), type)); + } + + private static Object coerceToArray(final ELContext ctx, final Object obj, + final Class type) { + // Note: Nested arrays will result in nested calls to this method. + + // Note: Calling method has checked the obj is an array. + + int size = Array.getLength(obj); + // Cast the input object to an array (calling method has checked it is + // an array) + // Get the target type for the array elements + Class componentType = type.getComponentType(); + // Create a new array of the correct type + Object result = Array.newInstance(componentType, size); + // Coerce each element in turn. + for (int i = 0; i < size; i++) { + Array.set(result, i, coerceToType(ctx, Array.get(obj, i), componentType)); + } + + return result; + } + + + private static T coerceToFunctionalInterface(final ELContext ctx, final LambdaExpression lambdaExpression, + final Class type) { + Supplier proxy = () -> { + // Create a dynamic proxy for the functional interface + @SuppressWarnings("unchecked") + T result = (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, + (Object obj, Method method, Object[] args) -> { + // Functional interfaces have a single, abstract method + if (!Modifier.isAbstract(method.getModifiers())) { + throw new ELException(MessageFactory.get("elSupport.coerce.nonAbstract", type, method)); + } + if (ctx == null) { + return lambdaExpression.invoke(args); + } else { + return lambdaExpression.invoke(ctx, args); + } + }); + return result; + }; + if (System.getSecurityManager() != null) { + return AccessController.doPrivileged((PrivilegedAction) proxy::get); + } else { + return proxy.get(); + } + } + + + public static final boolean isBigDecimalOp(final Object obj0, + final Object obj1) { + return (obj0 instanceof BigDecimal || obj1 instanceof BigDecimal); + } + + public static final boolean isBigIntegerOp(final Object obj0, + final Object obj1) { + return (obj0 instanceof BigInteger || obj1 instanceof BigInteger); + } + + public static final boolean isDoubleOp(final Object obj0, final Object obj1) { + return (obj0 instanceof Double + || obj1 instanceof Double + || obj0 instanceof Float + || obj1 instanceof Float); + } + + public static final boolean isLongOp(final Object obj0, final Object obj1) { + return (obj0 instanceof Long + || obj1 instanceof Long + || obj0 instanceof Integer + || obj1 instanceof Integer + || obj0 instanceof Character + || obj1 instanceof Character + || obj0 instanceof Short + || obj1 instanceof Short + || obj0 instanceof Byte + || obj1 instanceof Byte); + } + + public static final boolean isStringFloat(final String str) { + int len = str.length(); + if (len > 1) { + for (int i = 0; i < len; i++) { + switch (str.charAt(i)) { + case 'E': + return true; + case 'e': + return true; + case '.': + return true; + } + } + } + return false; + } + + + /* + * Copied to jakarta.el.ELContext - keep in sync + */ + static boolean isFunctionalInterface(Class type) { + + if (!type.isInterface()) { + return false; + } + + boolean foundAbstractMethod = false; + Method[] methods = type.getMethods(); + for (Method method : methods) { + if (Modifier.isAbstract(method.getModifiers())) { + // Abstract methods that override one of the public methods + // of Object don't count + if (overridesObjectMethod(method)) { + continue; + } + if (foundAbstractMethod) { + // Found more than one + return false; + } else { + foundAbstractMethod = true; + } + } + } + return foundAbstractMethod; + } + + + /* + * Copied to jakarta.el.ELContext - keep in sync + */ + private static boolean overridesObjectMethod(Method method) { + // There are three methods that can be overridden + if ("equals".equals(method.getName())) { + if (method.getReturnType().equals(boolean.class)) { + if (method.getParameterCount() == 1) { + if (method.getParameterTypes()[0].equals(Object.class)) { + return true; + } + } + } + } else if ("hashCode".equals(method.getName())) { + if (method.getReturnType().equals(int.class)) { + if (method.getParameterCount() == 0) { + return true; + } + } + } else if ("toString".equals(method.getName())) { + if (method.getReturnType().equals(String.class)) { + if (method.getParameterCount() == 0) { + return true; + } + } + } + + return false; + } + + + private ELSupport() { + // Uility class - hide default constructor; + } +} diff --git a/java/org/apache/el/lang/EvaluationContext.java b/java/org/apache/el/lang/EvaluationContext.java new file mode 100644 index 0000000..1c354b2 --- /dev/null +++ b/java/org/apache/el/lang/EvaluationContext.java @@ -0,0 +1,188 @@ +/* + * 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.el.lang; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.el.EvaluationListener; +import jakarta.el.FunctionMapper; +import jakarta.el.ImportHandler; +import jakarta.el.VariableMapper; + +import org.apache.el.util.MessageFactory; + +public final class EvaluationContext extends ELContext { + + private final ELContext elContext; + + private final FunctionMapper fnMapper; + + private final VariableMapper varMapper; + + private LambdaExpressionNestedState lambdaExpressionNestedState; + + public EvaluationContext(ELContext elContext, FunctionMapper fnMapper, + VariableMapper varMapper) { + this.elContext = elContext; + this.fnMapper = fnMapper; + this.varMapper = varMapper; + } + + public ELContext getELContext() { + return elContext; + } + + @Override + public FunctionMapper getFunctionMapper() { + return fnMapper; + } + + @Override + public VariableMapper getVariableMapper() { + return varMapper; + } + + @Override + public Object getContext(Class key) { + return elContext.getContext(key); + } + + @Override + public ELResolver getELResolver() { + return elContext.getELResolver(); + } + + @Override + public boolean isPropertyResolved() { + return elContext.isPropertyResolved(); + } + + @Override + public void putContext(Class key, Object contextObject) { + elContext.putContext(key, contextObject); + } + + @Override + public void setPropertyResolved(boolean resolved) { + elContext.setPropertyResolved(resolved); + } + + @Override + public Locale getLocale() { + return elContext.getLocale(); + } + + @Override + public void setLocale(Locale locale) { + elContext.setLocale(locale); + } + + @Override + public void setPropertyResolved(Object base, Object property) { + elContext.setPropertyResolved(base, property); + } + + @Override + public ImportHandler getImportHandler() { + return elContext.getImportHandler(); + } + + @Override + public void addEvaluationListener(EvaluationListener listener) { + elContext.addEvaluationListener(listener); + } + + @Override + public List getEvaluationListeners() { + return elContext.getEvaluationListeners(); + } + + @Override + public void notifyBeforeEvaluation(String expression) { + elContext.notifyBeforeEvaluation(expression); + } + + @Override + public void notifyAfterEvaluation(String expression) { + elContext.notifyAfterEvaluation(expression); + } + + @Override + public void notifyPropertyResolved(Object base, Object property) { + elContext.notifyPropertyResolved(base, property); + } + + @Override + public boolean isLambdaArgument(String name) { + return elContext.isLambdaArgument(name); + } + + @Override + public Object getLambdaArgument(String name) { + return elContext.getLambdaArgument(name); + } + + @Override + public void enterLambdaScope(Map arguments) { + elContext.enterLambdaScope(arguments); + } + + @Override + public void exitLambdaScope() { + elContext.exitLambdaScope(); + } + + @Override + public T convertToType(Object obj, Class type) { + return elContext.convertToType(obj, type); + } + + + public LambdaExpressionNestedState getLambdaExpressionNestedState() { + // State is stored in the EvaluationContext instance associated with the + // outermost lambda expression of a set of nested expressions. + + if (lambdaExpressionNestedState != null) { + // This instance is storing state so it must be associated with the + // outermost lambda expression. + return lambdaExpressionNestedState; + } + + // Check to see if the associated lambda expression is nested as state + // will be stored in the EvaluationContext associated with the outermost + // lambda expression. + if (elContext instanceof EvaluationContext) { + return ((EvaluationContext) elContext).getLambdaExpressionNestedState(); + } + + return null; + } + + + public void setLambdaExpressionNestedState(LambdaExpressionNestedState lambdaExpressionNestedState) { + if (this.lambdaExpressionNestedState != null) { + // Should never happen + throw new IllegalStateException(MessageFactory.get("error.lambda.wrongNestedState")); + } + + this.lambdaExpressionNestedState = lambdaExpressionNestedState; + } +} diff --git a/java/org/apache/el/lang/ExpressionBuilder.java b/java/org/apache/el/lang/ExpressionBuilder.java new file mode 100644 index 0000000..980f01d --- /dev/null +++ b/java/org/apache/el/lang/ExpressionBuilder.java @@ -0,0 +1,333 @@ +/* + * 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.el.lang; + +import java.io.StringReader; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.FunctionMapper; +import jakarta.el.MethodExpression; +import jakarta.el.ValueExpression; +import jakarta.el.VariableMapper; + +import org.apache.el.MethodExpressionImpl; +import org.apache.el.MethodExpressionLiteral; +import org.apache.el.ValueExpressionImpl; +import org.apache.el.parser.AstDeferredExpression; +import org.apache.el.parser.AstDynamicExpression; +import org.apache.el.parser.AstFunction; +import org.apache.el.parser.AstIdentifier; +import org.apache.el.parser.AstLiteralExpression; +import org.apache.el.parser.AstValue; +import org.apache.el.parser.ELParser; +import org.apache.el.parser.Node; +import org.apache.el.parser.NodeVisitor; +import org.apache.el.util.ConcurrentCache; +import org.apache.el.util.ExceptionUtils; +import org.apache.el.util.MessageFactory; + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class ExpressionBuilder implements NodeVisitor { + + private static final SynchronizedStack parserCache = new SynchronizedStack<>(); + + private static final int CACHE_SIZE; + private static final String CACHE_SIZE_PROP = + "org.apache.el.ExpressionBuilder.CACHE_SIZE"; + + static { + String cacheSizeStr; + if (System.getSecurityManager() == null) { + cacheSizeStr = System.getProperty(CACHE_SIZE_PROP, "5000"); + } else { + cacheSizeStr = AccessController.doPrivileged( + (PrivilegedAction) () -> System.getProperty(CACHE_SIZE_PROP, "5000")); + } + CACHE_SIZE = Integer.parseInt(cacheSizeStr); + } + + private static final ConcurrentCache expressionCache = + new ConcurrentCache<>(CACHE_SIZE); + + private FunctionMapper fnMapper; + + private VariableMapper varMapper; + + private final String expression; + + public ExpressionBuilder(String expression, ELContext ctx) + throws ELException { + this.expression = expression; + + FunctionMapper ctxFn = ctx.getFunctionMapper(); + VariableMapper ctxVar = ctx.getVariableMapper(); + + if (ctxFn != null) { + this.fnMapper = new FunctionMapperFactory(ctxFn); + } + if (ctxVar != null) { + this.varMapper = new VariableMapperFactory(ctxVar); + } + } + + public static Node createNode(String expr) throws ELException { + Node n = createNodeInternal(expr); + return n; + } + + private static Node createNodeInternal(String expr) + throws ELException { + if (expr == null) { + throw new ELException(MessageFactory.get("error.null")); + } + + Node n = expressionCache.get(expr); + if (n == null) { + ELParser parser = parserCache.pop(); + try { + if (parser == null) { + parser = new ELParser(new StringReader(expr)); + } else { + parser.ReInit(new StringReader(expr)); + } + n = parser.CompositeExpression(); + + // validate composite expression + int numChildren = n.jjtGetNumChildren(); + if (numChildren == 1) { + n = n.jjtGetChild(0); + } else { + Class type = null; + Node child = null; + for (int i = 0; i < numChildren; i++) { + child = n.jjtGetChild(i); + if (child instanceof AstLiteralExpression) { + continue; + } + if (type == null) { + type = child.getClass(); + } else { + if (!type.equals(child.getClass())) { + throw new ELException(MessageFactory.get( + "error.mixed", expr)); + } + } + } + } + + if (n instanceof AstDeferredExpression + || n instanceof AstDynamicExpression) { + n = n.jjtGetChild(0); + } + expressionCache.put(expr, n); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + throw new ELException( + MessageFactory.get("error.parseFail", expr), t); + } finally { + if (parser != null) { + parserCache.push(parser); + } + } + } + return n; + } + + private void prepare(Node node) throws ELException { + try { + node.accept(this); + } catch (Exception e) { + if (e instanceof ELException) { + throw (ELException) e; + } else { + throw (new ELException(e)); + } + } + if (this.fnMapper instanceof FunctionMapperFactory) { + this.fnMapper = ((FunctionMapperFactory) this.fnMapper).create(); + } + if (this.varMapper instanceof VariableMapperFactory) { + this.varMapper = ((VariableMapperFactory) this.varMapper).create(); + } + } + + private Node build() throws ELException { + Node n = createNodeInternal(this.expression); + this.prepare(n); + if (n instanceof AstDeferredExpression + || n instanceof AstDynamicExpression) { + n = n.jjtGetChild(0); + } + return n; + } + + /* + * (non-Javadoc) + * + * @see com.sun.el.parser.NodeVisitor#visit(com.sun.el.parser.Node) + */ + @Override + public void visit(Node node) throws ELException { + if (node instanceof AstFunction) { + + AstFunction funcNode = (AstFunction) node; + + Method m = null; + + if (this.fnMapper != null) { + m = fnMapper.resolveFunction(funcNode.getPrefix(), funcNode + .getLocalName()); + } + + // References to variables that refer to lambda expressions will be + // parsed as functions. This is handled at runtime but at this point + // need to treat it as a variable rather than a function. + if (m == null && this.varMapper != null && + funcNode.getPrefix().length() == 0) { + this.varMapper.resolveVariable(funcNode.getLocalName()); + return; + } + + if (this.fnMapper == null) { + throw new ELException(MessageFactory.get("error.fnMapper.null")); + } + + if (m == null) { + throw new ELException(MessageFactory.get( + "error.fnMapper.method", funcNode.getOutputName())); + } + + int methodParameterCount = m.getParameterTypes().length; + // AstFunction->MethodParameters->Parameters() + int inputParameterCount = node.jjtGetChild(0).jjtGetNumChildren(); + if (m.isVarArgs() && inputParameterCount < methodParameterCount - 1 || + !m.isVarArgs() && inputParameterCount != methodParameterCount) { + throw new ELException(MessageFactory.get( + "error.fnMapper.paramcount", funcNode.getOutputName(), + "" + methodParameterCount, "" + node.jjtGetChild(0).jjtGetNumChildren())); + } + } else if (node instanceof AstIdentifier && this.varMapper != null) { + String variable = node.getImage(); + + // simply capture it + this.varMapper.resolveVariable(variable); + } + } + + public ValueExpression createValueExpression(Class expectedType) + throws ELException { + Node n = this.build(); + return new ValueExpressionImpl(this.expression, n, this.fnMapper, + this.varMapper, expectedType); + } + + public MethodExpression createMethodExpression(Class expectedReturnType, + Class[] expectedParamTypes) throws ELException { + Node n = this.build(); + if (!n.isParametersProvided() && expectedParamTypes == null) { + throw new NullPointerException(MessageFactory + .get("error.method.nullParms")); + } + if (n instanceof AstValue || n instanceof AstIdentifier) { + return new MethodExpressionImpl(expression, n, this.fnMapper, + this.varMapper, expectedReturnType, expectedParamTypes); + } else if (n instanceof AstLiteralExpression) { + return new MethodExpressionLiteral(expression, expectedReturnType, + expectedParamTypes); + } else { + throw new ELException(MessageFactory.get("error.invalidMethodExpression", expression)); + } + } + + /* + * Copied from org.apache.tomcat.util.collections.SynchronizedStack since + * we don't want the EL implementation to depend on the JAR where that + * class resides. + */ + private static class SynchronizedStack { + + public static final int DEFAULT_SIZE = 128; + private static final int DEFAULT_LIMIT = -1; + + private int size; + private final int limit; + + /* + * Points to the next available object in the stack + */ + private int index = -1; + + private Object[] stack; + + + SynchronizedStack() { + this(DEFAULT_SIZE, DEFAULT_LIMIT); + } + + SynchronizedStack(int size, int limit) { + this.size = size; + this.limit = limit; + stack = new Object[size]; + } + + + public synchronized boolean push(T obj) { + index++; + if (index == size) { + if (limit == -1 || size < limit) { + expand(); + } else { + index--; + return false; + } + } + stack[index] = obj; + return true; + } + + @SuppressWarnings("unchecked") + public synchronized T pop() { + if (index == -1) { + return null; + } + T result = (T) stack[index]; + stack[index--] = null; + return result; + } + + private void expand() { + int newSize = size * 2; + if (limit != -1 && newSize > limit) { + newSize = limit; + } + Object[] newStack = new Object[newSize]; + System.arraycopy(stack, 0, newStack, 0, size); + // This is the only point where garbage is created by throwing away the + // old array. Note it is only the array, not the contents, that becomes + // garbage. + stack = newStack; + size = newSize; + } + } + +} diff --git a/java/org/apache/el/lang/FunctionMapperFactory.java b/java/org/apache/el/lang/FunctionMapperFactory.java new file mode 100644 index 0000000..2d68e3a --- /dev/null +++ b/java/org/apache/el/lang/FunctionMapperFactory.java @@ -0,0 +1,70 @@ +/* + * 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.el.lang; + +import java.lang.reflect.Method; + +import jakarta.el.FunctionMapper; + +import org.apache.el.util.MessageFactory; + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public class FunctionMapperFactory extends FunctionMapper { + + protected FunctionMapperImpl memento = null; + protected final FunctionMapper target; + + public FunctionMapperFactory(FunctionMapper mapper) { + if (mapper == null) { + throw new NullPointerException(MessageFactory.get("error.noFunctionMapperTarget")); + } + this.target = mapper; + } + + + /* (non-Javadoc) + * @see jakarta.el.FunctionMapper#resolveFunction(String, String) + */ + @Override + public Method resolveFunction(String prefix, String localName) { + if (this.memento == null) { + this.memento = new FunctionMapperImpl(); + } + Method m = this.target.resolveFunction(prefix, localName); + if (m != null) { + this.memento.mapFunction(prefix, localName, m); + } + return m; + } + + + @Override + public void mapFunction(String prefix, String localName, Method method) { + if (this.memento == null) { + this.memento = new FunctionMapperImpl(); + } + memento.mapFunction(prefix, localName, method); + } + + + public FunctionMapper create() { + return this.memento; + } + +} diff --git a/java/org/apache/el/lang/FunctionMapperImpl.java b/java/org/apache/el/lang/FunctionMapperImpl.java new file mode 100644 index 0000000..07a02d8 --- /dev/null +++ b/java/org/apache/el/lang/FunctionMapperImpl.java @@ -0,0 +1,187 @@ +/* + * 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.el.lang; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import jakarta.el.FunctionMapper; + +import org.apache.el.util.MessageFactory; +import org.apache.el.util.ReflectionUtil; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public class FunctionMapperImpl extends FunctionMapper implements + Externalizable { + + private static final long serialVersionUID = 1L; + + protected ConcurrentMap functions = new ConcurrentHashMap<>(); + + /* + * (non-Javadoc) + * + * @see jakarta.el.FunctionMapper#resolveFunction(String, String) + */ + @Override + public Method resolveFunction(String prefix, String localName) { + Function f = this.functions.get(prefix + ":" + localName); + if (f == null) { + return null; + } + return f.getMethod(); + } + + @Override + public void mapFunction(String prefix, String localName, Method m) { + String key = prefix + ":" + localName; + if (m == null) { + functions.remove(key); + } else { + Function f = new Function(prefix, localName, m); + functions.put(key, f); + } + } + + /* + * (non-Javadoc) + * + * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput) + */ + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(this.functions); + } + + /* + * (non-Javadoc) + * + * @see java.io.Externalizable#readExternal(java.io.ObjectInput) + */ + @SuppressWarnings("unchecked") + @Override + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.functions = (ConcurrentMap) in.readObject(); + } + + public static class Function implements Externalizable { + + protected transient Method m; + protected String owner; + protected String name; + protected String[] types; + protected String prefix; + protected String localName; + + public Function(String prefix, String localName, Method m) { + if (localName == null) { + throw new NullPointerException(MessageFactory.get("error.nullLocalName")); + } + if (m == null) { + throw new NullPointerException(MessageFactory.get("error.nullMethod")); + } + this.prefix = prefix; + this.localName = localName; + this.m = m; + } + + public Function() { + // for serialization + } + + /* + * (non-Javadoc) + * + * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput) + */ + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF((this.prefix != null) ? this.prefix : ""); + out.writeUTF(this.localName); + if (this.owner != null && this.name != null && this.types != null) { + out.writeUTF(this.owner); + out.writeUTF(this.name); + out.writeObject(this.types); + } else { + out.writeUTF(this.m.getDeclaringClass().getName()); + out.writeUTF(this.m.getName()); + out.writeObject(ReflectionUtil.toTypeNameArray(this.m.getParameterTypes())); + } + } + + /* + * (non-Javadoc) + * + * @see java.io.Externalizable#readExternal(java.io.ObjectInput) + */ + @Override + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + + this.prefix = in.readUTF(); + if (this.prefix.isEmpty()) { + this.prefix = null; + } + this.localName = in.readUTF(); + this.owner = in.readUTF(); + this.name = in.readUTF(); + this.types = (String[]) in.readObject(); + } + + public Method getMethod() { + if (this.m == null) { + try { + Class t = ReflectionUtil.forName(this.owner); + Class[] p = ReflectionUtil.toTypeArray(this.types); + this.m = t.getMethod(this.name, p); + } catch (Exception e) { + // Ignore: this results in ELException after further resolution + } + } + return this.m; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Function) { + return this.hashCode() == obj.hashCode(); + } + return false; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return (this.prefix + this.localName).hashCode(); + } + } + +} diff --git a/java/org/apache/el/lang/LambdaExpressionNestedState.java b/java/org/apache/el/lang/LambdaExpressionNestedState.java new file mode 100644 index 0000000..f4a4d47 --- /dev/null +++ b/java/org/apache/el/lang/LambdaExpressionNestedState.java @@ -0,0 +1,51 @@ +/* + * 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.el.lang; + +/** + * Stores the state required for correct evaluation of lambda expressions. + * Lambda expressions may be nested. Correct evaluation requires knowledge not + * just of the current lambda expression, but also of any nested and nesting + * expressions. + *

    + * The sets of nodes for parsed expressions are cached and, as a result, a set + * of nodes may be being used by multiple concurrent threads. This means any + * state relating to evaluation cannot be stored in the nodes. State is + * therefore stored in the {@link EvaluationContext} which is created, used for + * a single evaluation and then discarded. + */ +public final class LambdaExpressionNestedState { + + private int nestingCount = 0; + private boolean hasFormalParameters = false; + + public void incrementNestingCount() { + nestingCount++; + } + + public int getNestingCount() { + return nestingCount; + } + + public void setHasFormalParameters() { + hasFormalParameters = true; + } + + public boolean getHasFormalParameters() { + return hasFormalParameters; + } +} diff --git a/java/org/apache/el/lang/VariableMapperFactory.java b/java/org/apache/el/lang/VariableMapperFactory.java new file mode 100644 index 0000000..1a9409e --- /dev/null +++ b/java/org/apache/el/lang/VariableMapperFactory.java @@ -0,0 +1,56 @@ +/* + * 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.el.lang; + +import jakarta.el.ValueExpression; +import jakarta.el.VariableMapper; + +import org.apache.el.util.MessageFactory; + +public class VariableMapperFactory extends VariableMapper { + + private final VariableMapper target; + private VariableMapper momento; + + public VariableMapperFactory(VariableMapper target) { + if (target == null) { + throw new NullPointerException(MessageFactory.get("error.noVariableMapperTarget")); + } + this.target = target; + } + + public VariableMapper create() { + return this.momento; + } + + @Override + public ValueExpression resolveVariable(String variable) { + ValueExpression expr = this.target.resolveVariable(variable); + if (expr != null) { + if (this.momento == null) { + this.momento = new VariableMapperImpl(); + } + this.momento.setVariable(variable, expr); + } + return expr; + } + + @Override + public ValueExpression setVariable(String variable, ValueExpression expression) { + throw new UnsupportedOperationException(MessageFactory.get("error.cannotSetVariables")); + } +} diff --git a/java/org/apache/el/lang/VariableMapperImpl.java b/java/org/apache/el/lang/VariableMapperImpl.java new file mode 100644 index 0000000..01dbb4a --- /dev/null +++ b/java/org/apache/el/lang/VariableMapperImpl.java @@ -0,0 +1,65 @@ +/* + * 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.el.lang; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.HashMap; +import java.util.Map; + +import jakarta.el.ValueExpression; +import jakarta.el.VariableMapper; + +public class VariableMapperImpl extends VariableMapper implements Externalizable { + + private static final long serialVersionUID = 1L; + + private Map vars = new HashMap<>(); + + public VariableMapperImpl() { + super(); + } + + @Override + public ValueExpression resolveVariable(String variable) { + return this.vars.get(variable); + } + + @Override + public ValueExpression setVariable(String variable, + ValueExpression expression) { + if (expression == null) { + return vars.remove(variable); + } else { + return vars.put(variable, expression); + } + } + + @SuppressWarnings("unchecked") + @Override + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.vars = (Map) in.readObject(); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(this.vars); + } +} diff --git a/java/org/apache/el/parser/ArithmeticNode.java b/java/org/apache/el/parser/ArithmeticNode.java new file mode 100644 index 0000000..2a665d1 --- /dev/null +++ b/java/org/apache/el/parser/ArithmeticNode.java @@ -0,0 +1,37 @@ +/* + * 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.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public abstract class ArithmeticNode extends SimpleNode { + + public ArithmeticNode(int i) { + super(i); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return Number.class; + } +} diff --git a/java/org/apache/el/parser/AstAnd.java b/java/org/apache/el/parser/AstAnd.java new file mode 100644 index 0000000..85d268e --- /dev/null +++ b/java/org/apache/el/parser/AstAnd.java @@ -0,0 +1,46 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstAnd.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstAnd extends BooleanNode { + public AstAnd(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj = children[0].getValue(ctx); + Boolean b = ELSupport.coerceToBoolean(ctx, obj, true); + if (!b.booleanValue()) { + return b; + } + obj = children[1].getValue(ctx); + b = ELSupport.coerceToBoolean(ctx, obj, true); + return b; + } +} diff --git a/java/org/apache/el/parser/AstAssign.java b/java/org/apache/el/parser/AstAssign.java new file mode 100644 index 0000000..066625f --- /dev/null +++ b/java/org/apache/el/parser/AstAssign.java @@ -0,0 +1,50 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstAssign.java Version 4.3 */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + +public class AstAssign extends SimpleNode { + + public AstAssign(int id) { + super(id); + } + + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + Object value = children[1].getValue(ctx); + + children[0].setValue(ctx, value); + + return value; + } + + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + Object value = children[1].getValue(ctx); + + children[0].setValue(ctx, value); + + return children[1].getType(ctx); + } +} +/* JavaCC - OriginalChecksum=151e58546054b618e758d7dc172cc7b5 (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstBracketSuffix.java b/java/org/apache/el/parser/AstBracketSuffix.java new file mode 100644 index 0000000..16f2e01 --- /dev/null +++ b/java/org/apache/el/parser/AstBracketSuffix.java @@ -0,0 +1,38 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstBracketSuffix.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstBracketSuffix extends SimpleNode { + public AstBracketSuffix(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return this.children[0].getValue(ctx); + } +} diff --git a/java/org/apache/el/parser/AstChoice.java b/java/org/apache/el/parser/AstChoice.java new file mode 100644 index 0000000..173ea22 --- /dev/null +++ b/java/org/apache/el/parser/AstChoice.java @@ -0,0 +1,48 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstChoice.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstChoice extends SimpleNode { + public AstChoice(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + Object val = this.getValue(ctx); + return (val != null) ? val.getClass() : null; + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Boolean b0 = ELSupport.coerceToBoolean(ctx, obj0, true); + return this.children[((b0.booleanValue() ? 1 : 2))].getValue(ctx); + } +} diff --git a/java/org/apache/el/parser/AstCompositeExpression.java b/java/org/apache/el/parser/AstCompositeExpression.java new file mode 100644 index 0000000..e893cd4 --- /dev/null +++ b/java/org/apache/el/parser/AstCompositeExpression.java @@ -0,0 +1,56 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstCompositeExpression.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstCompositeExpression extends SimpleNode { + + public AstCompositeExpression(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return String.class; + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + StringBuilder sb = new StringBuilder(16); + Object obj = null; + if (this.children != null) { + for (Node child : this.children) { + obj = child.getValue(ctx); + if (obj != null) { + sb.append(ELSupport.coerceToString(ctx, obj)); + } + } + } + return sb.toString(); + } +} diff --git a/java/org/apache/el/parser/AstConcatenation.java b/java/org/apache/el/parser/AstConcatenation.java new file mode 100644 index 0000000..ae83893 --- /dev/null +++ b/java/org/apache/el/parser/AstConcatenation.java @@ -0,0 +1,46 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstConcatenation.java Version 4.3 */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + +public class AstConcatenation extends SimpleNode { + + public AstConcatenation(int id) { + super(id); + } + + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + // Coerce the two child nodes to string and then concatenate + String s1 = ELSupport.coerceToString(ctx, children[0].getValue(ctx)); + String s2 = ELSupport.coerceToString(ctx, children[1].getValue(ctx)); + return s1 + s2; + } + + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + return String.class; + } +} +/* JavaCC - OriginalChecksum=a95de353974c2c05fa5c7d695a1d50fd (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstDeferredExpression.java b/java/org/apache/el/parser/AstDeferredExpression.java new file mode 100644 index 0000000..302b6fa --- /dev/null +++ b/java/org/apache/el/parser/AstDeferredExpression.java @@ -0,0 +1,56 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstDeferredExpression.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstDeferredExpression extends SimpleNode { + public AstDeferredExpression(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return this.children[0].getType(ctx); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return this.children[0].getValue(ctx); + } + + @Override + public boolean isReadOnly(EvaluationContext ctx) + throws ELException { + return this.children[0].isReadOnly(ctx); + } + + @Override + public void setValue(EvaluationContext ctx, Object value) + throws ELException { + this.children[0].setValue(ctx, value); + } +} diff --git a/java/org/apache/el/parser/AstDiv.java b/java/org/apache/el/parser/AstDiv.java new file mode 100644 index 0000000..fd723d9 --- /dev/null +++ b/java/org/apache/el/parser/AstDiv.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstDiv.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELArithmetic; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstDiv extends ArithmeticNode { + public AstDiv(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + return ELArithmetic.divide(obj0, obj1); + } +} diff --git a/java/org/apache/el/parser/AstDotSuffix.java b/java/org/apache/el/parser/AstDotSuffix.java new file mode 100644 index 0000000..2e02ee8 --- /dev/null +++ b/java/org/apache/el/parser/AstDotSuffix.java @@ -0,0 +1,49 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstDotSuffix.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; +import org.apache.el.util.MessageFactory; +import org.apache.el.util.Validation; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstDotSuffix extends SimpleNode { + public AstDotSuffix(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return this.image; + } + + @Override + public void setImage(String image) { + if (!Validation.isIdentifier(image)) { + throw new ELException(MessageFactory.get("error.identifier.notjava", + image)); + } + this.image = image; + } +} diff --git a/java/org/apache/el/parser/AstDynamicExpression.java b/java/org/apache/el/parser/AstDynamicExpression.java new file mode 100644 index 0000000..413880b --- /dev/null +++ b/java/org/apache/el/parser/AstDynamicExpression.java @@ -0,0 +1,56 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstDynamicExpression.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstDynamicExpression extends SimpleNode { + public AstDynamicExpression(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return this.children[0].getType(ctx); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return this.children[0].getValue(ctx); + } + + @Override + public boolean isReadOnly(EvaluationContext ctx) + throws ELException { + return this.children[0].isReadOnly(ctx); + } + + @Override + public void setValue(EvaluationContext ctx, Object value) + throws ELException { + this.children[0].setValue(ctx, value); + } +} diff --git a/java/org/apache/el/parser/AstEmpty.java b/java/org/apache/el/parser/AstEmpty.java new file mode 100644 index 0000000..323995b --- /dev/null +++ b/java/org/apache/el/parser/AstEmpty.java @@ -0,0 +1,59 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstEmpty.java */ +package org.apache.el.parser; + +import java.util.Collection; +import java.util.Map; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstEmpty extends SimpleNode { + public AstEmpty(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return Boolean.class; + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj = this.children[0].getValue(ctx); + if (obj == null) { + return Boolean.TRUE; + } else if (obj instanceof String) { + return Boolean.valueOf(((String) obj).length() == 0); + } else if (obj instanceof Object[]) { + return Boolean.valueOf(((Object[]) obj).length == 0); + } else if (obj instanceof Collection) { + return Boolean.valueOf(((Collection) obj).isEmpty()); + } else if (obj instanceof Map) { + return Boolean.valueOf(((Map) obj).isEmpty()); + } + return Boolean.FALSE; + } +} diff --git a/java/org/apache/el/parser/AstEqual.java b/java/org/apache/el/parser/AstEqual.java new file mode 100644 index 0000000..4a9b53e --- /dev/null +++ b/java/org/apache/el/parser/AstEqual.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstEqual.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstEqual extends BooleanNode { + public AstEqual(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + return Boolean.valueOf(ELSupport.equals(ctx, obj0, obj1)); + } +} diff --git a/java/org/apache/el/parser/AstFalse.java b/java/org/apache/el/parser/AstFalse.java new file mode 100644 index 0000000..54ae49d --- /dev/null +++ b/java/org/apache/el/parser/AstFalse.java @@ -0,0 +1,38 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstFalse.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstFalse extends BooleanNode { + public AstFalse(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return Boolean.FALSE; + } +} diff --git a/java/org/apache/el/parser/AstFloatingPoint.java b/java/org/apache/el/parser/AstFloatingPoint.java new file mode 100644 index 0000000..7715edc --- /dev/null +++ b/java/org/apache/el/parser/AstFloatingPoint.java @@ -0,0 +1,67 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstFloatingPoint.java */ +package org.apache.el.parser; + +import java.math.BigDecimal; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstFloatingPoint extends SimpleNode { + public AstFloatingPoint(int id) { + super(id); + } + + private volatile Number number; + + public Number getFloatingPoint() { + // The parser should ensure the format of the string to be parsed. + if (this.number == null) { + try { + Double d = Double.valueOf(this.image); + if (d.isInfinite() || d.isNaN()) { + this.number = new BigDecimal(this.image); + } else { + this.number = d; + } + } catch (NumberFormatException e) { + // Catch NumberFormatException here just in case the parser + // provides invalid input. + throw new ELException(e); + } + } + return this.number; + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return this.getFloatingPoint(); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return this.getFloatingPoint().getClass(); + } +} diff --git a/java/org/apache/el/parser/AstFunction.java b/java/org/apache/el/parser/AstFunction.java new file mode 100644 index 0000000..bac8db1 --- /dev/null +++ b/java/org/apache/el/parser/AstFunction.java @@ -0,0 +1,225 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstFunction.java */ +package org.apache.el.parser; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import jakarta.el.ELClass; +import jakarta.el.ELException; +import jakarta.el.FunctionMapper; +import jakarta.el.LambdaExpression; +import jakarta.el.ValueExpression; +import jakarta.el.VariableMapper; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; +import org.apache.el.util.MessageFactory; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstFunction extends SimpleNode { + + protected String localName = ""; + + protected String prefix = ""; + + public AstFunction(int id) { + super(id); + } + + public String getLocalName() { + return localName; + } + + public String getOutputName() { + if (this.prefix == null) { + return this.localName; + } else { + return this.prefix + ":" + this.localName; + } + } + + public String getPrefix() { + return prefix; + } + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + + FunctionMapper fnMapper = ctx.getFunctionMapper(); + + // quickly validate again for this request + if (fnMapper == null) { + throw new ELException(MessageFactory.get("error.fnMapper.null")); + } + Method m = fnMapper.resolveFunction(this.prefix, this.localName); + if (m == null) { + throw new ELException(MessageFactory.get("error.fnMapper.method", this.getOutputName())); + } + return m.getReturnType(); + } + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + + FunctionMapper fnMapper = ctx.getFunctionMapper(); + + // quickly validate again for this request + if (fnMapper == null) { + throw new ELException(MessageFactory.get("error.fnMapper.null")); + } + Method m = fnMapper.resolveFunction(this.prefix, this.localName); + + if (m == null && this.prefix.length() == 0) { + // TODO: Do we need to think about precedence of the various ways + // a lambda expression may be obtained from something that + // the parser thinks is a function? + Object obj = null; + if (ctx.isLambdaArgument(this.localName)) { + obj = ctx.getLambdaArgument(this.localName); + } + if (obj == null) { + VariableMapper varMapper = ctx.getVariableMapper(); + if (varMapper != null) { + obj = varMapper.resolveVariable(this.localName); + if (obj instanceof ValueExpression) { + // See if this returns a LambdaExpression + obj = ((ValueExpression) obj).getValue(ctx); + } + } + } + if (obj == null) { + obj = ctx.getELResolver().getValue(ctx, null, this.localName); + } + if (obj instanceof LambdaExpression) { + // Build arguments + int i = 0; + while (obj instanceof LambdaExpression && i < jjtGetNumChildren()) { + Node args = jjtGetChild(i); + obj = ((LambdaExpression) obj).invoke(((AstMethodParameters) args).getParameters(ctx)); + i++; + } + if (i < jjtGetNumChildren()) { + // Haven't consumed all the sets of parameters therefore + // there were too many sets of parameters + throw new ELException(MessageFactory.get("error.lambda.tooManyMethodParameterSets")); + } + return obj; + } + + // Call to a constructor or a static method + obj = ctx.getImportHandler().resolveClass(this.localName); + if (obj != null) { + return ctx.getELResolver().invoke(ctx, new ELClass((Class) obj), "", null, + ((AstMethodParameters) this.children[0]).getParameters(ctx)); + } + obj = ctx.getImportHandler().resolveStatic(this.localName); + if (obj != null) { + return ctx.getELResolver().invoke(ctx, new ELClass((Class) obj), this.localName, null, + ((AstMethodParameters) this.children[0]).getParameters(ctx)); + } + } + + if (m == null) { + throw new ELException(MessageFactory.get("error.fnMapper.method", this.getOutputName())); + } + + // Not a lambda expression so must be a function. Check there is just a + // single set of method parameters + if (this.jjtGetNumChildren() != 1) { + throw new ELException(MessageFactory.get("error.function.tooManyMethodParameterSets", getOutputName())); + } + + Node parameters = jjtGetChild(0); + Class[] paramTypes = m.getParameterTypes(); + Object[] params = null; + Object result = null; + int inputParameterCount = parameters.jjtGetNumChildren(); + int methodParameterCount = paramTypes.length; + if (inputParameterCount == 0 && methodParameterCount == 1 && m.isVarArgs()) { + params = new Object[] { null }; + } else if (inputParameterCount > 0) { + params = new Object[methodParameterCount]; + try { + for (int i = 0; i < methodParameterCount; i++) { + if (m.isVarArgs() && i == methodParameterCount - 1) { + if (inputParameterCount < methodParameterCount) { + params[i] = new Object[] { null }; + } else if (inputParameterCount == methodParameterCount && isArray(parameters.jjtGetChild(i).getValue(ctx))) { + params[i] = parameters.jjtGetChild(i).getValue(ctx); + } else { + Object[] varargs = new Object[inputParameterCount - methodParameterCount + 1]; + Class target = paramTypes[i].getComponentType(); + for (int j = i; j < inputParameterCount; j++) { + varargs[j - i] = parameters.jjtGetChild(j).getValue(ctx); + varargs[j - i] = ELSupport.coerceToType(ctx, varargs[j - i], target); + } + params[i] = varargs; + } + } else { + params[i] = parameters.jjtGetChild(i).getValue(ctx); + } + params[i] = ELSupport.coerceToType(ctx, params[i], paramTypes[i]); + } + } catch (ELException ele) { + throw new ELException(MessageFactory.get("error.function", this.getOutputName()), ele); + } + } + try { + result = m.invoke(null, params); + } catch (IllegalAccessException iae) { + throw new ELException(MessageFactory.get("error.function", this.getOutputName()), iae); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause(); + if (cause instanceof ThreadDeath) { + throw (ThreadDeath) cause; + } + if (cause instanceof VirtualMachineError) { + throw (VirtualMachineError) cause; + } + throw new ELException(MessageFactory.get("error.function", this.getOutputName()), cause); + } + return result; + } + + + private boolean isArray(Object obj) { + if (obj == null) { + return false; + } + return obj.getClass().isArray(); + } + + + public void setLocalName(String localName) { + this.localName = localName; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + + @Override + public String toString() { + return ELParserTreeConstants.jjtNodeName[id] + "[" + this.getOutputName() + "]"; + } +} diff --git a/java/org/apache/el/parser/AstGreaterThan.java b/java/org/apache/el/parser/AstGreaterThan.java new file mode 100644 index 0000000..953947c --- /dev/null +++ b/java/org/apache/el/parser/AstGreaterThan.java @@ -0,0 +1,47 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstGreaterThan.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstGreaterThan extends BooleanNode { + public AstGreaterThan(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + if (obj0 == null) { + return Boolean.FALSE; + } + Object obj1 = this.children[1].getValue(ctx); + if (obj1 == null) { + return Boolean.FALSE; + } + return (ELSupport.compare(ctx, obj0, obj1) > 0) ? Boolean.TRUE : Boolean.FALSE; + } +} diff --git a/java/org/apache/el/parser/AstGreaterThanEqual.java b/java/org/apache/el/parser/AstGreaterThanEqual.java new file mode 100644 index 0000000..3095546 --- /dev/null +++ b/java/org/apache/el/parser/AstGreaterThanEqual.java @@ -0,0 +1,47 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstGreaterThanEqual.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstGreaterThanEqual extends BooleanNode { + public AstGreaterThanEqual(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + if (obj0 == obj1) { + return Boolean.TRUE; + } + if (obj0 == null || obj1 == null) { + return Boolean.FALSE; + } + return (ELSupport.compare(ctx, obj0, obj1) >= 0) ? Boolean.TRUE : Boolean.FALSE; + } +} diff --git a/java/org/apache/el/parser/AstIdentifier.java b/java/org/apache/el/parser/AstIdentifier.java new file mode 100644 index 0000000..4565536 --- /dev/null +++ b/java/org/apache/el/parser/AstIdentifier.java @@ -0,0 +1,238 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstIdentifier.java */ +package org.apache.el.parser; + +import jakarta.el.ELClass; +import jakarta.el.ELException; +import jakarta.el.MethodExpression; +import jakarta.el.MethodInfo; +import jakarta.el.MethodNotFoundException; +import jakarta.el.MethodReference; +import jakarta.el.PropertyNotFoundException; +import jakarta.el.ValueExpression; +import jakarta.el.ValueReference; +import jakarta.el.VariableMapper; + +import org.apache.el.lang.EvaluationContext; +import org.apache.el.util.MessageFactory; +import org.apache.el.util.Validation; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstIdentifier extends SimpleNode { + public AstIdentifier(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + VariableMapper varMapper = ctx.getVariableMapper(); + if (varMapper != null) { + ValueExpression expr = varMapper.resolveVariable(this.image); + if (expr != null) { + return expr.getType(ctx.getELContext()); + } + } + ctx.setPropertyResolved(false); + Class result = ctx.getELResolver().getType(ctx, null, this.image); + if (!ctx.isPropertyResolved()) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.resolver.unhandled.null", this.image)); + } + return result; + } + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + // Lambda parameters + if (ctx.isLambdaArgument(this.image)) { + return ctx.getLambdaArgument(this.image); + } + + // Variable mapper + VariableMapper varMapper = ctx.getVariableMapper(); + if (varMapper != null) { + ValueExpression expr = varMapper.resolveVariable(this.image); + if (expr != null) { + return expr.getValue(ctx.getELContext()); + } + } + + // EL Resolvers + ctx.setPropertyResolved(false); + Object result; + /* Putting the Boolean into the ELContext is part of a performance + * optimisation for ScopedAttributeELResolver. When looking up "foo", + * the resolver can't differentiate between ${ foo } and ${ foo.bar }. + * This is important because the expensive class lookup only needs to + * be performed in the later case. This flag tells the resolver if the + * lookup can be skipped. + */ + if (parent instanceof AstValue) { + ctx.putContext(this.getClass(), Boolean.FALSE); + } else { + ctx.putContext(this.getClass(), Boolean.TRUE); + } + try { + result = ctx.getELResolver().getValue(ctx, null, this.image); + } finally { + // Always reset the flag to false so the optimisation is not applied + // inappropriately + ctx.putContext(this.getClass(), Boolean.FALSE); + } + + if (ctx.isPropertyResolved()) { + return result; + } + + // Import + result = ctx.getImportHandler().resolveClass(this.image); + if (result != null) { + return new ELClass((Class) result); + } + result = ctx.getImportHandler().resolveStatic(this.image); + if (result != null) { + try { + return ((Class) result).getField(this.image).get(null); + } catch (IllegalArgumentException | IllegalAccessException + | NoSuchFieldException | SecurityException e) { + throw new ELException(e); + } + } + + throw new PropertyNotFoundException(MessageFactory.get( + "error.resolver.unhandled.null", this.image)); + } + + @Override + public boolean isReadOnly(EvaluationContext ctx) throws ELException { + VariableMapper varMapper = ctx.getVariableMapper(); + if (varMapper != null) { + ValueExpression expr = varMapper.resolveVariable(this.image); + if (expr != null) { + return expr.isReadOnly(ctx.getELContext()); + } + } + ctx.setPropertyResolved(false); + boolean result = ctx.getELResolver().isReadOnly(ctx, null, this.image); + if (!ctx.isPropertyResolved()) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.resolver.unhandled.null", this.image)); + } + return result; + } + + @Override + public void setValue(EvaluationContext ctx, Object value) + throws ELException { + VariableMapper varMapper = ctx.getVariableMapper(); + if (varMapper != null) { + ValueExpression expr = varMapper.resolveVariable(this.image); + if (expr != null) { + expr.setValue(ctx.getELContext(), value); + return; + } + } + ctx.setPropertyResolved(false); + ctx.getELResolver().setValue(ctx, null, this.image, value); + if (!ctx.isPropertyResolved()) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.resolver.unhandled.null", this.image)); + } + } + + @Override + public Object invoke(EvaluationContext ctx, Class[] paramTypes, + Object[] paramValues) throws ELException { + return this.getMethodExpression(ctx).invoke(ctx.getELContext(), paramValues); + } + + + @Override + public MethodInfo getMethodInfo(EvaluationContext ctx, + Class[] paramTypes) throws ELException { + return this.getMethodExpression(ctx).getMethodInfo(ctx.getELContext()); + } + + @Override + public MethodReference getMethodReference(EvaluationContext ctx) { + return this.getMethodExpression(ctx).getMethodReference(ctx.getELContext()); + } + + @Override + public void setImage(String image) { + if (!Validation.isIdentifier(image)) { + throw new ELException(MessageFactory.get("error.identifier.notjava", + image)); + } + this.image = image; + } + + + @Override + public ValueReference getValueReference(EvaluationContext ctx) { + VariableMapper varMapper = ctx.getVariableMapper(); + + if (varMapper == null) { + return null; + } + + ValueExpression expr = varMapper.resolveVariable(this.image); + + if (expr == null) { + return null; + } + + return expr.getValueReference(ctx); + } + + + private MethodExpression getMethodExpression(EvaluationContext ctx) + throws ELException { + Object obj = null; + + // case A: ValueExpression exists, getValue which must + // be a MethodExpression + VariableMapper varMapper = ctx.getVariableMapper(); + ValueExpression ve = null; + if (varMapper != null) { + ve = varMapper.resolveVariable(this.image); + if (ve != null) { + obj = ve.getValue(ctx); + } + } + + // case B: evaluate the identity against the ELResolver, again, must be + // a MethodExpression to be able to invoke + if (ve == null) { + ctx.setPropertyResolved(false); + obj = ctx.getELResolver().getValue(ctx, null, this.image); + } + + // finally provide helpful hints + if (obj instanceof MethodExpression) { + return (MethodExpression) obj; + } else if (obj == null) { + throw new MethodNotFoundException(MessageFactory.get("error.identifier.noMethod", this.image)); + } else { + throw new ELException(MessageFactory.get("error.identifier.notMethodExpression", this.image, obj.getClass().getName())); + } + } +} diff --git a/java/org/apache/el/parser/AstInteger.java b/java/org/apache/el/parser/AstInteger.java new file mode 100644 index 0000000..f30c131 --- /dev/null +++ b/java/org/apache/el/parser/AstInteger.java @@ -0,0 +1,68 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstInteger.java */ +package org.apache.el.parser; + +import java.math.BigInteger; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstInteger extends SimpleNode { + public AstInteger(int id) { + super(id); + } + + private volatile Number number; + + protected Number getInteger() { + // The parser should ensure the format of the string to be parsed + if (this.number == null) { + try { + try { + this.number = Long.valueOf(this.image); + } catch (NumberFormatException ignore) { + // Too large for Long. Try BigInteger. + this.number = new BigInteger(this.image); + } + } catch (ArithmeticException | NumberFormatException e) { + // Too big for BigInteger. + // Catch NumberFormatException as well here just in case the + // parser provides invalid input. + throw new ELException(e); + } + } + return number; + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return this.getInteger().getClass(); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return this.getInteger(); + } +} diff --git a/java/org/apache/el/parser/AstLambdaExpression.java b/java/org/apache/el/parser/AstLambdaExpression.java new file mode 100644 index 0000000..6bf58d6 --- /dev/null +++ b/java/org/apache/el/parser/AstLambdaExpression.java @@ -0,0 +1,151 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstLambdaExpression.java Version 4.3 */ +package org.apache.el.parser; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.el.ELException; +import jakarta.el.LambdaExpression; + +import org.apache.el.ValueExpressionImpl; +import org.apache.el.lang.EvaluationContext; +import org.apache.el.lang.LambdaExpressionNestedState; +import org.apache.el.util.MessageFactory; + +public class AstLambdaExpression extends SimpleNode { + + public AstLambdaExpression(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + + // Correct evaluation requires knowledge of the whole set of nested + // expressions, not just the current expression + LambdaExpressionNestedState state = ctx.getLambdaExpressionNestedState(); + if (state == null) { + // This must be an outer lambda expression. Create and populate the + // state. + state = new LambdaExpressionNestedState(); + populateNestedState(state); + ctx.setLambdaExpressionNestedState(state); + } + + // Check that there are not more sets of parameters than there are + // nested expressions. + int methodParameterSetCount = jjtGetNumChildren() - 2; + if (methodParameterSetCount > state.getNestingCount()) { + throw new ELException(MessageFactory.get( + "error.lambda.tooManyMethodParameterSets")); + } + + // First child is always parameters even if there aren't any + AstLambdaParameters formalParametersNode = + (AstLambdaParameters) children[0]; + Node[] formalParamNodes = formalParametersNode.children; + + // Second child is a value expression + ValueExpressionImpl ve = new ValueExpressionImpl("", children[1], + ctx.getFunctionMapper(), ctx.getVariableMapper(), null); + + // Build a LambdaExpression + List formalParameters = new ArrayList<>(); + if (formalParamNodes != null) { + for (Node formalParamNode : formalParamNodes) { + formalParameters.add(formalParamNode.getImage()); + } + } + LambdaExpression le = new LambdaExpression(formalParameters, ve); + le.setELContext(ctx); + + if (jjtGetNumChildren() == 2) { + // No method parameters + // Can only invoke the expression if none of the lambda expressions + // in the nesting declare parameters + if (state.getHasFormalParameters()) { + return le; + } else { + return le.invoke(ctx, (Object[]) null); + } + } + + /* + * This is a (possibly nested) lambda expression with one or more sets + * of parameters provided. + * + * If there are more nested expressions than sets of parameters this may + * return a LambdaExpression. + * + * If there are more sets of parameters than nested expressions an + * ELException will have been thrown by the check at the start of this + * method. + */ + + // Always have to invoke the outer-most expression + int methodParameterIndex = 2; + Object result = le.invoke(((AstMethodParameters) + children[methodParameterIndex]).getParameters(ctx)); + methodParameterIndex++; + + while (result instanceof LambdaExpression && + methodParameterIndex < jjtGetNumChildren()) { + result = ((LambdaExpression) result).invoke(((AstMethodParameters) + children[methodParameterIndex]).getParameters(ctx)); + methodParameterIndex++; + } + + return result; + } + + + private void populateNestedState(LambdaExpressionNestedState lambdaExpressionNestedState) { + // Increment the nesting count for the current expression + lambdaExpressionNestedState.incrementNestingCount(); + + if (jjtGetNumChildren() > 1) { + Node firstChild = jjtGetChild(0); + if (firstChild instanceof AstLambdaParameters) { + if (firstChild.jjtGetNumChildren() > 0) { + lambdaExpressionNestedState.setHasFormalParameters(); + } + } else { + // Can't be a lambda expression + return; + } + Node secondChild = jjtGetChild(1); + if (secondChild instanceof AstLambdaExpression) { + ((AstLambdaExpression) secondChild).populateNestedState(lambdaExpressionNestedState); + } + } + } + + + @Override + public String toString() { + // Purely for debug purposes. May not be complete or correct. Certainly + // is not efficient. Be sure not to call this from 'real' code. + StringBuilder result = new StringBuilder(); + for (Node n : children) { + result.append(n.toString()); + } + return result.toString(); + } +} +/* JavaCC - OriginalChecksum=071159eff10c8e15ec612c765ae4480a (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstLambdaParameters.java b/java/org/apache/el/parser/AstLambdaParameters.java new file mode 100644 index 0000000..f3198e1 --- /dev/null +++ b/java/org/apache/el/parser/AstLambdaParameters.java @@ -0,0 +1,43 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstLambdaParameters.java Version 4.3 */ +package org.apache.el.parser; + +public class AstLambdaParameters extends SimpleNode { + + public AstLambdaParameters(int id) { + super(id); + } + + @Override + public String toString() { + // Purely for debug purposes. May not be complete or correct. Certainly + // is not efficient. Be sure not to call this from 'real' code. + StringBuilder result = new StringBuilder(); + result.append('('); + if (children != null) { + for (Node n : children) { + result.append(n.toString()); + result.append(','); + } + } + result.append(")->"); + return result.toString(); + } + +} +/* JavaCC - OriginalChecksum=a8c1609257dac59e41c43d6ed91072c6 (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstLessThan.java b/java/org/apache/el/parser/AstLessThan.java new file mode 100644 index 0000000..f7882dd --- /dev/null +++ b/java/org/apache/el/parser/AstLessThan.java @@ -0,0 +1,47 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstLessThan.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstLessThan extends BooleanNode { + public AstLessThan(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + if (obj0 == null) { + return Boolean.FALSE; + } + Object obj1 = this.children[1].getValue(ctx); + if (obj1 == null) { + return Boolean.FALSE; + } + return (ELSupport.compare(ctx, obj0, obj1) < 0) ? Boolean.TRUE : Boolean.FALSE; + } +} diff --git a/java/org/apache/el/parser/AstLessThanEqual.java b/java/org/apache/el/parser/AstLessThanEqual.java new file mode 100644 index 0000000..2baadb6 --- /dev/null +++ b/java/org/apache/el/parser/AstLessThanEqual.java @@ -0,0 +1,47 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstLessThanEqual.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstLessThanEqual extends BooleanNode { + public AstLessThanEqual(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + if (obj0 == obj1) { + return Boolean.TRUE; + } + if (obj0 == null || obj1 == null) { + return Boolean.FALSE; + } + return (ELSupport.compare(ctx, obj0, obj1) <= 0) ? Boolean.TRUE : Boolean.FALSE; + } +} diff --git a/java/org/apache/el/parser/AstListData.java b/java/org/apache/el/parser/AstListData.java new file mode 100644 index 0000000..eb88917 --- /dev/null +++ b/java/org/apache/el/parser/AstListData.java @@ -0,0 +1,50 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstListData.java Version 4.3 */ +package org.apache.el.parser; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + +public class AstListData extends SimpleNode { + public AstListData(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + List result = new ArrayList<>(); + + if (children != null) { + for (Node child : children) { + result.add(child.getValue(ctx)); + } + } + + return result; + } + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + return List.class; + } +} +/* JavaCC - OriginalChecksum=7f2694086a9ba64558ee39d1cd719db1 (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstLiteralExpression.java b/java/org/apache/el/parser/AstLiteralExpression.java new file mode 100644 index 0000000..7fe65c1 --- /dev/null +++ b/java/org/apache/el/parser/AstLiteralExpression.java @@ -0,0 +1,65 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstLiteralExpression.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstLiteralExpression extends SimpleNode { + public AstLiteralExpression(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + return String.class; + } + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + return this.image; + } + + @Override + public void setImage(String image) { + if (image.indexOf('\\') == -1) { + this.image = image; + return; + } + int size = image.length(); + StringBuilder buf = new StringBuilder(size); + for (int i = 0; i < size; i++) { + char c = image.charAt(i); + if (c == '\\' && i + 2 < size) { + char c1 = image.charAt(i + 1); + char c2 = image.charAt(i + 2); + if ((c1 == '#' || c1 == '$') && c2 == '{') { + c = c1; + i++; + } + } + buf.append(c); + } + this.image = buf.toString(); + } +} diff --git a/java/org/apache/el/parser/AstMapData.java b/java/org/apache/el/parser/AstMapData.java new file mode 100644 index 0000000..ce7245b --- /dev/null +++ b/java/org/apache/el/parser/AstMapData.java @@ -0,0 +1,56 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstMapData.java Version 4.3 */ +package org.apache.el.parser; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + +public class AstMapData extends SimpleNode { + + public AstMapData(int id) { + super(id); + } + + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + Map result = new HashMap<>(); + + if (children != null) { + for (Node child : children) { + AstMapEntry mapEntry = (AstMapEntry) child; + Object key = mapEntry.children[0].getValue(ctx); + Object value = mapEntry.children[1].getValue(ctx); + result.put(key, value); + } + } + + return result; + } + + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + return Map.class; + } +} +/* JavaCC - OriginalChecksum=a68b5c6f0a0708f478fdf8c0e6e1263e (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstMapEntry.java b/java/org/apache/el/parser/AstMapEntry.java new file mode 100644 index 0000000..5a897dc --- /dev/null +++ b/java/org/apache/el/parser/AstMapEntry.java @@ -0,0 +1,26 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstMapEntry.java Version 4.3 */ +package org.apache.el.parser; + +public +class AstMapEntry extends SimpleNode { + public AstMapEntry(int id) { + super(id); + } +} +/* JavaCC - OriginalChecksum=6a7910e58a583371769800554113a8d3 (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstMethodParameters.java b/java/org/apache/el/parser/AstMethodParameters.java new file mode 100644 index 0000000..11a2ac2 --- /dev/null +++ b/java/org/apache/el/parser/AstMethodParameters.java @@ -0,0 +1,53 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstDotSuffix.java */ +package org.apache.el.parser; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.el.lang.EvaluationContext; + +public final class AstMethodParameters extends SimpleNode { + public AstMethodParameters(int id) { + super(id); + } + + public Object[] getParameters(EvaluationContext ctx) { + List params = new ArrayList<>(); + for (int i = 0; i < this.jjtGetNumChildren(); i++) { + params.add(this.jjtGetChild(i).getValue(ctx)); + } + return params.toArray(new Object[0]); + } + + @Override + public String toString() { + // Purely for debug purposes. May not be complete or correct. Certainly + // is not efficient. Be sure not to call this from 'real' code. + StringBuilder result = new StringBuilder(); + result.append('('); + if (children != null) { + for (Node n : children) { + result.append(n.toString()); + result.append(','); + } + } + result.append(')'); + return result.toString(); + } +} diff --git a/java/org/apache/el/parser/AstMinus.java b/java/org/apache/el/parser/AstMinus.java new file mode 100644 index 0000000..c31f936 --- /dev/null +++ b/java/org/apache/el/parser/AstMinus.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstMinus.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELArithmetic; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstMinus extends ArithmeticNode { + public AstMinus(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + return ELArithmetic.subtract(obj0, obj1); + } +} diff --git a/java/org/apache/el/parser/AstMod.java b/java/org/apache/el/parser/AstMod.java new file mode 100644 index 0000000..ee2f626 --- /dev/null +++ b/java/org/apache/el/parser/AstMod.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstMod.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELArithmetic; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstMod extends ArithmeticNode { + public AstMod(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + return ELArithmetic.mod(obj0, obj1); + } +} diff --git a/java/org/apache/el/parser/AstMult.java b/java/org/apache/el/parser/AstMult.java new file mode 100644 index 0000000..2b5f860 --- /dev/null +++ b/java/org/apache/el/parser/AstMult.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstMult.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELArithmetic; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstMult extends ArithmeticNode { + public AstMult(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + return ELArithmetic.multiply(obj0, obj1); + } +} diff --git a/java/org/apache/el/parser/AstNegative.java b/java/org/apache/el/parser/AstNegative.java new file mode 100644 index 0000000..c2a540c --- /dev/null +++ b/java/org/apache/el/parser/AstNegative.java @@ -0,0 +1,84 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstNegative.java */ +package org.apache.el.parser; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstNegative extends SimpleNode { + public AstNegative(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return Number.class; + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj = this.children[0].getValue(ctx); + + if (obj == null) { + return Long.valueOf(0); + } + if (obj instanceof BigDecimal) { + return ((BigDecimal) obj).negate(); + } + if (obj instanceof BigInteger) { + return ((BigInteger) obj).negate(); + } + if (obj instanceof String) { + if (ELSupport.isStringFloat((String) obj)) { + return Double.valueOf(-Double.parseDouble((String) obj)); + } + return Long.valueOf(-Long.parseLong((String) obj)); + } + if (obj instanceof Long) { + return Long.valueOf(-((Long) obj).longValue()); + } + if (obj instanceof Double) { + return Double.valueOf(-((Double) obj).doubleValue()); + } + if (obj instanceof Integer) { + return Integer.valueOf(-((Integer) obj).intValue()); + } + if (obj instanceof Float) { + return Float.valueOf(-((Float) obj).floatValue()); + } + if (obj instanceof Short) { + return Short.valueOf((short) -((Short) obj).shortValue()); + } + if (obj instanceof Byte) { + return Byte.valueOf((byte) -((Byte) obj).byteValue()); + } + Long num = (Long) ELSupport.coerceToNumber(ctx, obj, Long.class); + return Long.valueOf(-num.longValue()); + } +} diff --git a/java/org/apache/el/parser/AstNot.java b/java/org/apache/el/parser/AstNot.java new file mode 100644 index 0000000..1a16180 --- /dev/null +++ b/java/org/apache/el/parser/AstNot.java @@ -0,0 +1,47 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstNot.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstNot extends SimpleNode { + public AstNot(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return Boolean.class; + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj = this.children[0].getValue(ctx); + Boolean b = ELSupport.coerceToBoolean(ctx, obj, true); + return Boolean.valueOf(!b.booleanValue()); + } +} diff --git a/java/org/apache/el/parser/AstNotEqual.java b/java/org/apache/el/parser/AstNotEqual.java new file mode 100644 index 0000000..5f19904 --- /dev/null +++ b/java/org/apache/el/parser/AstNotEqual.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstNotEqual.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstNotEqual extends BooleanNode { + public AstNotEqual(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + return Boolean.valueOf(!ELSupport.equals(ctx, obj0, obj1)); + } +} diff --git a/java/org/apache/el/parser/AstNull.java b/java/org/apache/el/parser/AstNull.java new file mode 100644 index 0000000..b9b28d9 --- /dev/null +++ b/java/org/apache/el/parser/AstNull.java @@ -0,0 +1,44 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstNull.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstNull extends SimpleNode { + public AstNull(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return null; + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return null; + } +} diff --git a/java/org/apache/el/parser/AstOr.java b/java/org/apache/el/parser/AstOr.java new file mode 100644 index 0000000..a16a04c --- /dev/null +++ b/java/org/apache/el/parser/AstOr.java @@ -0,0 +1,46 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstOr.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstOr extends BooleanNode { + public AstOr(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj = this.children[0].getValue(ctx); + Boolean b = ELSupport.coerceToBoolean(ctx, obj, true); + if (b.booleanValue()) { + return b; + } + obj = this.children[1].getValue(ctx); + b = ELSupport.coerceToBoolean(ctx, obj, true); + return b; + } +} diff --git a/java/org/apache/el/parser/AstPlus.java b/java/org/apache/el/parser/AstPlus.java new file mode 100644 index 0000000..06510fd --- /dev/null +++ b/java/org/apache/el/parser/AstPlus.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstPlus.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.ELArithmetic; +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstPlus extends ArithmeticNode { + public AstPlus(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + Object obj0 = this.children[0].getValue(ctx); + Object obj1 = this.children[1].getValue(ctx); + return ELArithmetic.add(obj0, obj1); + } +} diff --git a/java/org/apache/el/parser/AstSemicolon.java b/java/org/apache/el/parser/AstSemicolon.java new file mode 100644 index 0000000..fb72c77 --- /dev/null +++ b/java/org/apache/el/parser/AstSemicolon.java @@ -0,0 +1,49 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstSemicolon.java Version 4.3 */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + +public class AstSemicolon extends SimpleNode { + + public AstSemicolon(int id) { + super(id); + } + + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + + // Evaluate and throw away + children[0].getValue(ctx); + + return children[1].getValue(ctx); + } + + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + // Evaluate and throw away + children[0].getType(ctx); + + return children[1].getType(ctx); + } +} +/* JavaCC - OriginalChecksum=ce956594ca572a4e452fe4f084a03099 (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstSetData.java b/java/org/apache/el/parser/AstSetData.java new file mode 100644 index 0000000..e53e7ed --- /dev/null +++ b/java/org/apache/el/parser/AstSetData.java @@ -0,0 +1,51 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstSetData.java Version 4.3 */ +package org.apache.el.parser; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + +public class AstSetData extends SimpleNode { + + public AstSetData(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + Set result = new HashSet<>(); + + if (children != null) { + for (Node child : children) { + result.add(child.getValue(ctx)); + } + } + + return result; + } + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + return Set.class; + } +} +/* JavaCC - OriginalChecksum=e1dc4e2011eee313491decfa9e0152fe (do not edit this line) */ diff --git a/java/org/apache/el/parser/AstString.java b/java/org/apache/el/parser/AstString.java new file mode 100644 index 0000000..f701a17 --- /dev/null +++ b/java/org/apache/el/parser/AstString.java @@ -0,0 +1,75 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstString.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstString extends SimpleNode { + public AstString(int id) { + super(id); + } + + private volatile String string; + + public String getString() { + if (this.string == null) { + this.string = this.image.substring(1, this.image.length() - 1); + } + return this.string; + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return String.class; + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return this.getString(); + } + + @Override + public void setImage(String image) { + if (image.indexOf('\\') == -1) { + this.image = image; + return; + } + int size = image.length(); + StringBuilder buf = new StringBuilder(size); + for (int i = 0; i < size; i++) { + char c = image.charAt(i); + if (c == '\\' && i + 1 < size) { + char c1 = image.charAt(i + 1); + if (c1 == '\\' || c1 == '"' || c1 == '\'') { + c = c1; + i++; + } + } + buf.append(c); + } + this.image = buf.toString(); + } +} diff --git a/java/org/apache/el/parser/AstTrue.java b/java/org/apache/el/parser/AstTrue.java new file mode 100644 index 0000000..70543d9 --- /dev/null +++ b/java/org/apache/el/parser/AstTrue.java @@ -0,0 +1,38 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstTrue.java */ +package org.apache.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstTrue extends BooleanNode { + public AstTrue(int id) { + super(id); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + return Boolean.TRUE; + } +} diff --git a/java/org/apache/el/parser/AstValue.java b/java/org/apache/el/parser/AstValue.java new file mode 100644 index 0000000..2bd5205 --- /dev/null +++ b/java/org/apache/el/parser/AstValue.java @@ -0,0 +1,392 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. AstValue.java */ +package org.apache.el.parser; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import jakarta.el.ELException; +import jakarta.el.ELResolver; +import jakarta.el.LambdaExpression; +import jakarta.el.MethodInfo; +import jakarta.el.MethodReference; +import jakarta.el.PropertyNotFoundException; +import jakarta.el.ValueReference; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; +import org.apache.el.stream.Optional; +import org.apache.el.util.MessageFactory; +import org.apache.el.util.ReflectionUtil; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class AstValue extends SimpleNode { + + private static final Object[] EMPTY_ARRAY = new Object[0]; + + protected static class Target { + protected Object base; + + protected Object property; + } + + public AstValue(int id) { + super(id); + } + + @Override + public Class getType(EvaluationContext ctx) throws ELException { + Target t = getTarget(ctx); + ctx.setPropertyResolved(false); + Class result = ctx.getELResolver().getType(ctx, t.base, t.property); + if (!ctx.isPropertyResolved()) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.resolver.unhandled", t.base, t.property)); + } + return result; + } + + private Target getTarget(EvaluationContext ctx) throws ELException { + // evaluate expr-a to value-a + Object base = this.children[0].getValue(ctx); + + // if our base is null (we know there are more properties to evaluate) + if (base == null) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.unreachable.base", this.children[0].getImage())); + } + + // set up our start/end + Object property = null; + int propCount = this.jjtGetNumChildren(); + + int i = 1; + // Evaluate any properties or methods before our target + ELResolver resolver = ctx.getELResolver(); + while (i < propCount) { + if (i + 2 < propCount && + this.children[i + 1] instanceof AstMethodParameters) { + // Method call not at end of expression + base = resolver.invoke(ctx, base, + this.children[i].getValue(ctx), null, + ((AstMethodParameters) + this.children[i + 1]).getParameters(ctx)); + i += 2; + } else if (i + 2 == propCount && + this.children[i + 1] instanceof AstMethodParameters) { + // Method call at end of expression + ctx.setPropertyResolved(false); + property = this.children[i].getValue(ctx); + i += 2; + + if (property == null) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.unreachable.property", property)); + } + } else if (i + 1 < propCount) { + // Object with property not at end of expression + property = this.children[i].getValue(ctx); + ctx.setPropertyResolved(false); + base = resolver.getValue(ctx, base, property); + i++; + + } else { + // Object with property at end of expression + ctx.setPropertyResolved(false); + property = this.children[i].getValue(ctx); + i++; + + if (property == null) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.unreachable.property", property)); + } + } + if (base == null) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.unreachable.property", property)); + } + } + + Target t = new Target(); + t.base = base; + t.property = property; + return t; + } + + @Override + public Object getValue(EvaluationContext ctx) throws ELException { + Object base = this.children[0].getValue(ctx); + int propCount = this.jjtGetNumChildren(); + int i = 1; + Object suffix = null; + ELResolver resolver = ctx.getELResolver(); + while (base != null && i < propCount) { + suffix = this.children[i].getValue(ctx); + if (i + 1 < propCount && + (this.children[i+1] instanceof AstMethodParameters)) { + AstMethodParameters mps = + (AstMethodParameters) this.children[i+1]; + if (base instanceof Optional && "orElseGet".equals(suffix) && + mps.jjtGetNumChildren() == 1) { + Node paramFoOptional = mps.jjtGetChild(0); + if (!(paramFoOptional instanceof AstLambdaExpression || + paramFoOptional instanceof LambdaExpression)) { + throw new ELException(MessageFactory.get( + "stream.optional.paramNotLambda", suffix)); + } + } + // This is a method + Object[] paramValues = mps.getParameters(ctx); + base = resolver.invoke(ctx, base, suffix, + getTypesFromValues(paramValues), paramValues); + i+=2; + } else { + // This is a property + if (suffix == null) { + return null; + } + + ctx.setPropertyResolved(false); + base = resolver.getValue(ctx, base, suffix); + i++; + } + } + if (!ctx.isPropertyResolved()) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.resolver.unhandled", base, suffix)); + } + return base; + } + + @Override + public boolean isReadOnly(EvaluationContext ctx) throws ELException { + Target t = getTarget(ctx); + ctx.setPropertyResolved(false); + boolean result = + ctx.getELResolver().isReadOnly(ctx, t.base, t.property); + if (!ctx.isPropertyResolved()) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.resolver.unhandled", t.base, t.property)); + } + return result; + } + + @Override + public void setValue(EvaluationContext ctx, Object value) + throws ELException { + Target t = getTarget(ctx); + ctx.setPropertyResolved(false); + ELResolver resolver = ctx.getELResolver(); + + // coerce to the expected type + Class targetClass = resolver.getType(ctx, t.base, t.property); + resolver.setValue(ctx, t.base, t.property, + ELSupport.coerceToType(ctx, value, targetClass)); + if (!ctx.isPropertyResolved()) { + throw new PropertyNotFoundException(MessageFactory.get( + "error.resolver.unhandled", t.base, t.property)); + } + } + + @Override + // Interface el.parser.Node uses raw types (and is auto-generated) + public MethodInfo getMethodInfo(EvaluationContext ctx, + @SuppressWarnings("rawtypes") Class[] paramTypes) + throws ELException { + Target t = getTarget(ctx); + Class[] types = null; + if (isParametersProvided()) { + Object[] values = ((AstMethodParameters) this.jjtGetChild( + this.jjtGetNumChildren() - 1)).getParameters(ctx); + types = getTypesFromValues(values); + } else { + types = paramTypes; + } + Method m = ReflectionUtil.getMethod(ctx, t.base, t.property, types, null); + return new MethodInfo(m.getName(), m.getReturnType(), m.getParameterTypes()); + } + + @Override + // Interface el.parser.Node uses a raw type (and is auto-generated) + public Object invoke(EvaluationContext ctx, + @SuppressWarnings("rawtypes") Class[] paramTypes, + Object[] paramValues) throws ELException { + + Target t = getTarget(ctx); + Method m = null; + Object[] values = null; + Class[] types = null; + if (isParametersProvided()) { + values = ((AstMethodParameters) this.jjtGetChild( + this.jjtGetNumChildren() - 1)).getParameters(ctx); + types = getTypesFromValues(values); + } else { + values = paramValues; + types = paramTypes; + } + m = ReflectionUtil.getMethod(ctx, t.base, t.property, types, values); + + // Handle varArgs and any coercion required + values = convertArgs(ctx, values, m); + + Object result = null; + try { + result = m.invoke(t.base, values); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new ELException(e); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause(); + if (cause instanceof ThreadDeath) { + throw (ThreadDeath) cause; + } + if (cause instanceof VirtualMachineError) { + throw (VirtualMachineError) cause; + } + throw new ELException(cause); + } + return result; + } + + @Override + public MethodReference getMethodReference(EvaluationContext ctx) { + Target t = getTarget(ctx); + Method m = null; + Object[] values = null; + Class[] types = null; + if (isParametersProvided()) { + values = ((AstMethodParameters) this.jjtGetChild( + this.jjtGetNumChildren() - 1)).getParameters(ctx); + types = getTypesFromValues(values); + } + m = ReflectionUtil.getMethod(ctx, t.base, t.property, types, values); + + // Handle varArgs and any coercion required + values = convertArgs(ctx, values, m); + + return new MethodReference(t.base, getMethodInfo(ctx, types), m.getAnnotations(), values); + } + + private Object[] convertArgs(EvaluationContext ctx, Object[] src, Method m) { + Class[] types = m.getParameterTypes(); + if (types.length == 0) { + // Treated as if parameters have been provided so src is ignored + return EMPTY_ARRAY; + } + + int paramCount = types.length; + + if (m.isVarArgs() && paramCount > 1 && (src == null || paramCount > src.length) || + !m.isVarArgs() && (paramCount > 0 && src == null || + src != null && src.length != paramCount)) { + String srcCount = null; + if (src != null) { + srcCount = Integer.toString(src.length); + } + String msg; + if (m.isVarArgs()) { + msg = MessageFactory.get("error.invoke.tooFewParams", + m.getName(), srcCount, Integer.toString(paramCount)); + } else { + msg = MessageFactory.get("error.invoke.wrongParams", + m.getName(), srcCount, Integer.toString(paramCount)); + } + throw new IllegalArgumentException(msg); + } + + if (src == null) { + // Must be a varargs method with a single parameter. + // Use a new array every time since the called code could modify the + // contents of the array + return new Object[1]; + } + + Object[] dest = new Object[paramCount]; + + for (int i = 0; i < paramCount - 1; i++) { + dest[i] = ELSupport.coerceToType(ctx, src[i], types[i]); + } + + if (m.isVarArgs()) { + Class varArgType = m.getParameterTypes()[paramCount - 1].getComponentType(); + Object[] varArgs = + (Object[]) Array.newInstance(varArgType, src.length - (paramCount - 1)); + for (int i = 0; i < src.length - (paramCount - 1); i ++) { + varArgs[i] = ELSupport.coerceToType(ctx, src[paramCount - 1 + i], varArgType); + } + dest[paramCount - 1] = varArgs; + } else { + dest[paramCount - 1] = ELSupport.coerceToType( + ctx, src[paramCount - 1], types[paramCount - 1]); + } + + return dest; + } + + private Class[] getTypesFromValues(Object[] values) { + if (values == null) { + return null; + } + + Class result[] = new Class[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + result[i] = null; + } else { + result[i] = values[i].getClass(); + } + } + return result; + } + + + /** + * @since EL 2.2 + */ + @Override + public ValueReference getValueReference(EvaluationContext ctx) { + // Check this is a reference to a base and a property + if (this.children.length > 2 && + this.jjtGetChild(2) instanceof AstMethodParameters) { + // This is a method call + return null; + } + Target t = getTarget(ctx); + return new ValueReference(t.base, t.property); + } + + + /** + * @since EL 2.2 + */ + @Override + public boolean isParametersProvided() { + // Assumption is that method parameters, if present, will be the last + // child + int len = children.length; + if (len > 2) { + if (this.jjtGetChild(len - 1) instanceof AstMethodParameters) { + return true; + } + } + return false; + } +} diff --git a/java/org/apache/el/parser/BooleanNode.java b/java/org/apache/el/parser/BooleanNode.java new file mode 100644 index 0000000..3cd042a --- /dev/null +++ b/java/org/apache/el/parser/BooleanNode.java @@ -0,0 +1,37 @@ +/* + * 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.el.parser; + +import jakarta.el.ELException; + +import org.apache.el.lang.EvaluationContext; + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public abstract class BooleanNode extends SimpleNode { + + public BooleanNode(int i) { + super(i); + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + return Boolean.class; + } +} diff --git a/java/org/apache/el/parser/ELParser.html b/java/org/apache/el/parser/ELParser.html new file mode 100644 index 0000000..24a312a --- /dev/null +++ b/java/org/apache/el/parser/ELParser.html @@ -0,0 +1,223 @@ + + + + +BNF for ELParser.jj + + +

    BNF for ELParser.jj

    +

    NON-TERMINALS

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CompositeExpression::=( DeferredExpression | DynamicExpression | LiteralExpression )* <EOF>
    LiteralExpression::=<LITERAL_EXPRESSION>
    DeferredExpression::=<START_DEFERRED_EXPRESSION> Expression <END_EXPRESSION>
    DynamicExpression::=<START_DYNAMIC_EXPRESSION> Expression <END_EXPRESSION>
    Expression::=Choice
    Choice::=Or ( <QUESTIONMARK> Choice <COLON> Choice )*
    Or::=And ( ( <OR0> | <OR1> ) And )*
    And::=Equality ( ( <AND0> | <AND1> ) Equality )*
    Equality::=Compare ( ( ( <EQ0> | <EQ1> ) Compare ) | ( ( <NE0> | <NE1> ) Compare ) )*
    Compare::=Math ( ( ( <LT0> | <LT1> ) Math ) | ( ( <GT0> | <GT1> ) Math ) | ( ( <LE0> | <LE1> ) Math ) | ( ( <GE0> | <GE1> ) Math ) )*
    Math::=Multiplication ( ( <PLUS> Multiplication ) | ( <MINUS> Multiplication ) )*
    Multiplication::=Unary ( ( <MULT> Unary ) | ( <DIV> Unary ) | ( ( <MOD0> | <MOD1> ) Unary ) )*
    Unary::=<MINUS> Unary
    |( <NOT0> | <NOT1> ) Unary
    |<EMPTY> Unary
    |Value
    Value::=( ValuePrefix ( ValueSuffix )* )
    ValuePrefix::=Literal
    |NonLiteral
    ValueSuffix::=DotSuffix
    |BracketSuffix
    DotSuffix::=<DOT> <IDENTIFIER>
    BracketSuffix::=<LBRACK> Expression <RBRACK>
    NonLiteral::=<LPAREN> Expression <RPAREN>
    |Function
    |Identifier
    Identifier::=<IDENTIFIER>
    Function::=<IDENTIFIER> ( <FUNCTIONSUFFIX> )? <LPAREN> ( Expression ( <COMMA> Expression )* )? <RPAREN>
    Literal::=Boolean
    |FloatingPoint
    |Integer
    |String
    |Null
    Boolean::=<TRUE>
    |<FALSE>
    FloatingPoint::=<FLOATING_POINT_LITERAL>
    Integer::=<INTEGER_LITERAL>
    String::=<STRING_LITERAL>
    Null::=<NULL>
    + + diff --git a/java/org/apache/el/parser/ELParser.java b/java/org/apache/el/parser/ELParser.java new file mode 100644 index 0000000..7a50b6b --- /dev/null +++ b/java/org/apache/el/parser/ELParser.java @@ -0,0 +1,3962 @@ +/* ELParser.java */ +/* Generated By:JJTree&JavaCC: Do not edit this line. ELParser.java */ +package org.apache.el.parser; +import java.io.StringReader; +import jakarta.el.ELException; +@SuppressWarnings("all") // Ignore warnings in generated code +public class ELParser/*@bgen(jjtree)*/implements ELParserTreeConstants, ELParserConstants {/*@bgen(jjtree)*/ + protected JJTELParserState jjtree = new JJTELParserState(); + public static Node parse(String ref) throws ELException { + try { + return new ELParser(new StringReader(ref)).CompositeExpression(); + } catch (ParseException pe) { + throw new ELException(pe.getMessage()); + } + } + +/* + * CompositeExpression + * Allow most flexible parsing, restrict by examining + * type of returned node + */ + final public AstCompositeExpression CompositeExpression() throws ParseException {/*@bgen(jjtree) CompositeExpression */ + AstCompositeExpression jjtn000 = new AstCompositeExpression(JJTCOMPOSITEEXPRESSION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LITERAL_EXPRESSION: + case START_DYNAMIC_EXPRESSION: + case START_DEFERRED_EXPRESSION:{ + ; + break; + } + default: + jj_la1[0] = jj_gen; + break label_1; + } + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case START_DEFERRED_EXPRESSION:{ + DeferredExpression(); + break; + } + case START_DYNAMIC_EXPRESSION:{ + DynamicExpression(); + break; + } + case LITERAL_EXPRESSION:{ + LiteralExpression(); + break; + } + default: + jj_la1[1] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + jj_consume_token(0); +jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; +{if ("" != null) { + return jjtn000; +}} + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + throw new Error("Missing return statement in function"); +} + +/* + * LiteralExpression + * Non-EL Expression blocks + */ + final public void LiteralExpression() throws ParseException {/*@bgen(jjtree) LiteralExpression */ + AstLiteralExpression jjtn000 = new AstLiteralExpression(JJTLITERALEXPRESSION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + try { + t = jj_consume_token(LITERAL_EXPRESSION); +jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; +jjtn000.setImage(t.image); + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * DeferredExpression + * #{...} Expressions + */ + final public void DeferredExpression() throws ParseException {/*@bgen(jjtree) DeferredExpression */ + AstDeferredExpression jjtn000 = new AstDeferredExpression(JJTDEFERREDEXPRESSION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(START_DEFERRED_EXPRESSION); + Expression(); + jj_consume_token(RBRACE); + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * DynamicExpression + * ${...} Expressions + */ + final public void DynamicExpression() throws ParseException {/*@bgen(jjtree) DynamicExpression */ + AstDynamicExpression jjtn000 = new AstDynamicExpression(JJTDYNAMICEXPRESSION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(START_DYNAMIC_EXPRESSION); + Expression(); + jj_consume_token(RBRACE); + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Expression + * EL Expression Language Root + */ + final public void Expression() throws ParseException { + Semicolon(); +} + +/* + * Semicolon + */ + final public void Semicolon() throws ParseException { + Assignment(); + label_2: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case SEMICOLON:{ + ; + break; + } + default: + jj_la1[2] = jj_gen; + break label_2; + } + jj_consume_token(SEMICOLON); +AstSemicolon jjtn001 = new AstSemicolon(JJTSEMICOLON); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Assignment(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } +} + +/* + * Assignment + */ + final public void Assignment() throws ParseException { + if (jj_2_2(4)) { + LambdaExpression(); + } else { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case START_SET_OR_MAP: + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL: + case LPAREN: + case LBRACK: + case NOT0: + case NOT1: + case EMPTY: + case MINUS: + case IDENTIFIER:{ + Choice(); + label_3: + while (true) { + if (jj_2_1(2)) { + ; + } else { + break label_3; + } + jj_consume_token(ASSIGN); +AstAssign jjtn001 = new AstAssign(JJTASSIGN); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Assignment(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } + break; + } + default: + jj_la1[3] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } +} + +/* + * Lambda expression + */ + final public void LambdaExpression() throws ParseException {/*@bgen(jjtree) LambdaExpression */ + AstLambdaExpression jjtn000 = new AstLambdaExpression(JJTLAMBDAEXPRESSION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + LambdaParameters(); + jj_consume_token(ARROW); + if (jj_2_3(3)) { + LambdaExpression(); + } else { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case START_SET_OR_MAP: + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL: + case LPAREN: + case LBRACK: + case NOT0: + case NOT1: + case EMPTY: + case MINUS: + case IDENTIFIER:{ + Choice(); + break; + } + default: + jj_la1[4] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Lambda parameters + */ + final public void LambdaParameters() throws ParseException {/*@bgen(jjtree) LambdaParameters */ + AstLambdaParameters jjtn000 = new AstLambdaParameters(JJTLAMBDAPARAMETERS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case IDENTIFIER:{ + Identifier(); + break; + } + case LPAREN:{ + jj_consume_token(LPAREN); + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case IDENTIFIER:{ + Identifier(); + label_4: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case COMMA:{ + ; + break; + } + default: + jj_la1[5] = jj_gen; + break label_4; + } + jj_consume_token(COMMA); + Identifier(); + } + break; + } + default: + jj_la1[6] = jj_gen; + ; + } + jj_consume_token(RPAREN); + break; + } + default: + jj_la1[7] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Possible invocation of lambda expression. Invocations must be bracketed but + * being bracketed does not mean it is an invocation. + */ + final public void LambdaExpressionOrInvocation() throws ParseException {/*@bgen(jjtree) LambdaExpression */ + AstLambdaExpression jjtn000 = new AstLambdaExpression(JJTLAMBDAEXPRESSION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(LPAREN); + LambdaParameters(); + jj_consume_token(ARROW); + if (jj_2_4(3)) { + LambdaExpression(); + } else { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case START_SET_OR_MAP: + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL: + case LPAREN: + case LBRACK: + case NOT0: + case NOT1: + case EMPTY: + case MINUS: + case IDENTIFIER:{ + Choice(); + break; + } + default: + jj_la1[8] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + jj_consume_token(RPAREN); + label_5: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LPAREN:{ + ; + break; + } + default: + jj_la1[9] = jj_gen; + break label_5; + } + MethodParameters(); + } + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Choice + * For Choice markup a ? b : c, then Or + */ + final public void Choice() throws ParseException { + Or(); + label_6: + while (true) { + if (jj_2_5(3)) { + ; + } else { + break label_6; + } + jj_consume_token(QUESTIONMARK); + Choice(); + jj_consume_token(COLON); +AstChoice jjtn001 = new AstChoice(JJTCHOICE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Choice(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 3); + } + } + } +} + +/* + * Or + * For 'or' '||', then And + */ + final public void Or() throws ParseException { + And(); + label_7: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case OR0: + case OR1:{ + ; + break; + } + default: + jj_la1[10] = jj_gen; + break label_7; + } + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case OR0:{ + jj_consume_token(OR0); + break; + } + case OR1:{ + jj_consume_token(OR1); + break; + } + default: + jj_la1[11] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstOr jjtn001 = new AstOr(JJTOR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + And(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } +} + +/* + * And + * For 'and' '&&', then Equality + */ + final public void And() throws ParseException { + Equality(); + label_8: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case AND0: + case AND1:{ + ; + break; + } + default: + jj_la1[12] = jj_gen; + break label_8; + } + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case AND0:{ + jj_consume_token(AND0); + break; + } + case AND1:{ + jj_consume_token(AND1); + break; + } + default: + jj_la1[13] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstAnd jjtn001 = new AstAnd(JJTAND); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Equality(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } +} + +/* + * Equality + * For '==' 'eq' '!=' 'ne', then Compare + */ + final public void Equality() throws ParseException { + Compare(); + label_9: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case EQ0: + case EQ1: + case NE0: + case NE1:{ + ; + break; + } + default: + jj_la1[14] = jj_gen; + break label_9; + } + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case EQ0: + case EQ1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case EQ0:{ + jj_consume_token(EQ0); + break; + } + case EQ1:{ + jj_consume_token(EQ1); + break; + } + default: + jj_la1[15] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstEqual jjtn001 = new AstEqual(JJTEQUAL); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Compare(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + break; + } + case NE0: + case NE1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case NE0:{ + jj_consume_token(NE0); + break; + } + case NE1:{ + jj_consume_token(NE1); + break; + } + default: + jj_la1[16] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstNotEqual jjtn002 = new AstNotEqual(JJTNOTEQUAL); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + Compare(); + } catch (Throwable jjte002) { +if (jjtc002) { + jjtree.clearNodeScope(jjtn002); + jjtc002 = false; + } else { + jjtree.popNode(); + } + if (jjte002 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte002; + }} + } + if (jjte002 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte002; + }} + } + {if (true) { + throw (Error)jjte002; + }} + } finally { +if (jjtc002) { + jjtree.closeNodeScope(jjtn002, 2); + } + } + break; + } + default: + jj_la1[17] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } +} + +/* + * Compare + * For a bunch of them, then += + */ + final public void Compare() throws ParseException { + Concatenation(); + label_10: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case GT0: + case GT1: + case LT0: + case LT1: + case GE0: + case GE1: + case LE0: + case LE1:{ + ; + break; + } + default: + jj_la1[18] = jj_gen; + break label_10; + } + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LT0: + case LT1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LT0:{ + jj_consume_token(LT0); + break; + } + case LT1:{ + jj_consume_token(LT1); + break; + } + default: + jj_la1[19] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstLessThan jjtn001 = new AstLessThan(JJTLESSTHAN); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Concatenation(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + break; + } + case GT0: + case GT1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case GT0:{ + jj_consume_token(GT0); + break; + } + case GT1:{ + jj_consume_token(GT1); + break; + } + default: + jj_la1[20] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstGreaterThan jjtn002 = new AstGreaterThan(JJTGREATERTHAN); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + Concatenation(); + } catch (Throwable jjte002) { +if (jjtc002) { + jjtree.clearNodeScope(jjtn002); + jjtc002 = false; + } else { + jjtree.popNode(); + } + if (jjte002 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte002; + }} + } + if (jjte002 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte002; + }} + } + {if (true) { + throw (Error)jjte002; + }} + } finally { +if (jjtc002) { + jjtree.closeNodeScope(jjtn002, 2); + } + } + break; + } + case LE0: + case LE1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LE0:{ + jj_consume_token(LE0); + break; + } + case LE1:{ + jj_consume_token(LE1); + break; + } + default: + jj_la1[21] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstLessThanEqual jjtn003 = new AstLessThanEqual(JJTLESSTHANEQUAL); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + Concatenation(); + } catch (Throwable jjte003) { +if (jjtc003) { + jjtree.clearNodeScope(jjtn003); + jjtc003 = false; + } else { + jjtree.popNode(); + } + if (jjte003 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte003; + }} + } + if (jjte003 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte003; + }} + } + {if (true) { + throw (Error)jjte003; + }} + } finally { +if (jjtc003) { + jjtree.closeNodeScope(jjtn003, 2); + } + } + break; + } + case GE0: + case GE1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case GE0:{ + jj_consume_token(GE0); + break; + } + case GE1:{ + jj_consume_token(GE1); + break; + } + default: + jj_la1[22] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstGreaterThanEqual jjtn004 = new AstGreaterThanEqual(JJTGREATERTHANEQUAL); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + Concatenation(); + } catch (Throwable jjte004) { +if (jjtc004) { + jjtree.clearNodeScope(jjtn004); + jjtc004 = false; + } else { + jjtree.popNode(); + } + if (jjte004 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte004; + }} + } + if (jjte004 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte004; + }} + } + {if (true) { + throw (Error)jjte004; + }} + } finally { +if (jjtc004) { + jjtree.closeNodeScope(jjtn004, 2); + } + } + break; + } + default: + jj_la1[23] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } +} + +/* + * Concatenation + * For +=, then Math + * + */ + final public void Concatenation() throws ParseException { + Math(); + label_11: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case CONCAT:{ + ; + break; + } + default: + jj_la1[24] = jj_gen; + break label_11; + } + jj_consume_token(CONCAT); +AstConcatenation jjtn001 = new AstConcatenation(JJTCONCATENATION); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Math(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } +} + +/* + * Math + * For '+' '-', then Multiplication + */ + final public void Math() throws ParseException { + Multiplication(); + label_12: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case PLUS: + case MINUS:{ + ; + break; + } + default: + jj_la1[25] = jj_gen; + break label_12; + } + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case PLUS:{ + jj_consume_token(PLUS); +AstPlus jjtn001 = new AstPlus(JJTPLUS); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Multiplication(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + break; + } + case MINUS:{ + jj_consume_token(MINUS); +AstMinus jjtn002 = new AstMinus(JJTMINUS); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + Multiplication(); + } catch (Throwable jjte002) { +if (jjtc002) { + jjtree.clearNodeScope(jjtn002); + jjtc002 = false; + } else { + jjtree.popNode(); + } + if (jjte002 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte002; + }} + } + if (jjte002 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte002; + }} + } + {if (true) { + throw (Error)jjte002; + }} + } finally { +if (jjtc002) { + jjtree.closeNodeScope(jjtn002, 2); + } + } + break; + } + default: + jj_la1[26] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } +} + +/* + * Multiplication + * For a bunch of them, then Unary + */ + final public void Multiplication() throws ParseException { + Unary(); + label_13: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case MULT: + case DIV0: + case DIV1: + case MOD0: + case MOD1:{ + ; + break; + } + default: + jj_la1[27] = jj_gen; + break label_13; + } + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case MULT:{ + jj_consume_token(MULT); +AstMult jjtn001 = new AstMult(JJTMULT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Unary(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + break; + } + case DIV0: + case DIV1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case DIV0:{ + jj_consume_token(DIV0); + break; + } + case DIV1:{ + jj_consume_token(DIV1); + break; + } + default: + jj_la1[28] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstDiv jjtn002 = new AstDiv(JJTDIV); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + Unary(); + } catch (Throwable jjte002) { +if (jjtc002) { + jjtree.clearNodeScope(jjtn002); + jjtc002 = false; + } else { + jjtree.popNode(); + } + if (jjte002 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte002; + }} + } + if (jjte002 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte002; + }} + } + {if (true) { + throw (Error)jjte002; + }} + } finally { +if (jjtc002) { + jjtree.closeNodeScope(jjtn002, 2); + } + } + break; + } + case MOD0: + case MOD1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case MOD0:{ + jj_consume_token(MOD0); + break; + } + case MOD1:{ + jj_consume_token(MOD1); + break; + } + default: + jj_la1[29] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstMod jjtn003 = new AstMod(JJTMOD); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + Unary(); + } catch (Throwable jjte003) { +if (jjtc003) { + jjtree.clearNodeScope(jjtn003); + jjtc003 = false; + } else { + jjtree.popNode(); + } + if (jjte003 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte003; + }} + } + if (jjte003 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte003; + }} + } + {if (true) { + throw (Error)jjte003; + }} + } finally { +if (jjtc003) { + jjtree.closeNodeScope(jjtn003, 2); + } + } + break; + } + default: + jj_la1[30] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } +} + +/* + * Unary + * For '-' '!' 'not' 'empty', then Value + */ + final public void Unary() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case MINUS:{ + jj_consume_token(MINUS); +AstNegative jjtn001 = new AstNegative(JJTNEGATIVE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + Unary(); + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + } + case NOT0: + case NOT1:{ + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case NOT0:{ + jj_consume_token(NOT0); + break; + } + case NOT1:{ + jj_consume_token(NOT1); + break; + } + default: + jj_la1[31] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +AstNot jjtn002 = new AstNot(JJTNOT); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + Unary(); + } catch (Throwable jjte002) { +if (jjtc002) { + jjtree.clearNodeScope(jjtn002); + jjtc002 = false; + } else { + jjtree.popNode(); + } + if (jjte002 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte002; + }} + } + if (jjte002 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte002; + }} + } + {if (true) { + throw (Error)jjte002; + }} + } finally { +if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + } + case EMPTY:{ + jj_consume_token(EMPTY); +AstEmpty jjtn003 = new AstEmpty(JJTEMPTY); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + Unary(); + } catch (Throwable jjte003) { +if (jjtc003) { + jjtree.clearNodeScope(jjtn003); + jjtc003 = false; + } else { + jjtree.popNode(); + } + if (jjte003 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte003; + }} + } + if (jjte003 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte003; + }} + } + {if (true) { + throw (Error)jjte003; + }} + } finally { +if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + } + case START_SET_OR_MAP: + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL: + case LPAREN: + case LBRACK: + case IDENTIFIER:{ + Value(); + break; + } + default: + jj_la1[32] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +} + +/* + * Value + * Defines Prefix plus zero or more Suffixes + */ + final public void Value() throws ParseException { +AstValue jjtn001 = new AstValue(JJTVALUE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + ValuePrefix(); + label_14: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case DOT: + case LBRACK:{ + ; + break; + } + default: + jj_la1[33] = jj_gen; + break label_14; + } + ValueSuffix(); + } + } catch (Throwable jjte001) { +if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte001; + }} + } + if (jjte001 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte001; + }} + } + {if (true) { + throw (Error)jjte001; + }} + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, jjtree.nodeArity() > 1); + } + } +} + +/* + * ValuePrefix + * For Literals, Variables, and Functions + */ + final public void ValuePrefix() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL:{ + Literal(); + break; + } + case START_SET_OR_MAP: + case LPAREN: + case LBRACK: + case IDENTIFIER:{ + NonLiteral(); + break; + } + default: + jj_la1[34] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +} + +/* + * ValueSuffix + * Either dot or bracket notation + */ + final public void ValueSuffix() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case DOT:{ + DotSuffix(); + break; + } + case LBRACK:{ + BracketSuffix(); + break; + } + default: + jj_la1[35] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LPAREN:{ + MethodParameters(); + break; + } + default: + jj_la1[36] = jj_gen; + ; + } +} + +/* + * DotSuffix + * Dot Property + */ + final public void DotSuffix() throws ParseException {/*@bgen(jjtree) DotSuffix */ + AstDotSuffix jjtn000 = new AstDotSuffix(JJTDOTSUFFIX); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + try { + jj_consume_token(DOT); + t = jj_consume_token(IDENTIFIER); +jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; +jjtn000.setImage(t.image); + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * BracketSuffix + * Sub Expression Suffix + */ + final public void BracketSuffix() throws ParseException {/*@bgen(jjtree) BracketSuffix */ + AstBracketSuffix jjtn000 = new AstBracketSuffix(JJTBRACKETSUFFIX); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(LBRACK); + Expression(); + jj_consume_token(RBRACK); + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * MethodParameters + */ + final public void MethodParameters() throws ParseException {/*@bgen(jjtree) MethodParameters */ + AstMethodParameters jjtn000 = new AstMethodParameters(JJTMETHODPARAMETERS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(LPAREN); + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case START_SET_OR_MAP: + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL: + case LPAREN: + case LBRACK: + case NOT0: + case NOT1: + case EMPTY: + case MINUS: + case IDENTIFIER:{ + Expression(); + label_15: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case COMMA:{ + ; + break; + } + default: + jj_la1[37] = jj_gen; + break label_15; + } + jj_consume_token(COMMA); + Expression(); + } + break; + } + default: + jj_la1[38] = jj_gen; + ; + } + jj_consume_token(RPAREN); + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * NonLiteral + * For Grouped Operations, Identifiers, and Functions + */ + final public void NonLiteral() throws ParseException { + if (jj_2_6(5)) { + LambdaExpressionOrInvocation(); + } else { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LPAREN:{ + jj_consume_token(LPAREN); + Expression(); + jj_consume_token(RPAREN); + break; + } + default: + jj_la1[39] = jj_gen; + if (jj_2_7(2147483647)) { + Function(); + } else { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case IDENTIFIER:{ + Identifier(); + break; + } + default: + jj_la1[40] = jj_gen; + if (jj_2_8(5)) { + SetData(); + } else { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LBRACK:{ + ListData(); + break; + } + case START_SET_OR_MAP:{ + MapData(); + break; + } + default: + jj_la1[41] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } + } + } + } +} + +/* + * Note that both an empty Set and an empty Map are represented by {}. The + * parser will always parse {} as an empty Set and special handling is required + * to convert it to an empty Map when appropriate. + */ + final public void SetData() throws ParseException {/*@bgen(jjtree) SetData */ + AstSetData jjtn000 = new AstSetData(JJTSETDATA); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(START_SET_OR_MAP); + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case START_SET_OR_MAP: + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL: + case LPAREN: + case LBRACK: + case NOT0: + case NOT1: + case EMPTY: + case MINUS: + case IDENTIFIER:{ + Expression(); + label_16: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case COMMA:{ + ; + break; + } + default: + jj_la1[42] = jj_gen; + break label_16; + } + jj_consume_token(COMMA); + Expression(); + } + break; + } + default: + jj_la1[43] = jj_gen; + ; + } + jj_consume_token(RBRACE); + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + + final public void ListData() throws ParseException {/*@bgen(jjtree) ListData */ + AstListData jjtn000 = new AstListData(JJTLISTDATA); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(LBRACK); + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case START_SET_OR_MAP: + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL: + case LPAREN: + case LBRACK: + case NOT0: + case NOT1: + case EMPTY: + case MINUS: + case IDENTIFIER:{ + Expression(); + label_17: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case COMMA:{ + ; + break; + } + default: + jj_la1[44] = jj_gen; + break label_17; + } + jj_consume_token(COMMA); + Expression(); + } + break; + } + default: + jj_la1[45] = jj_gen; + ; + } + jj_consume_token(RBRACK); + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Note that both an empty Set and an empty Map are represented by {}. The + * parser will always parse {} as an empty Set and special handling is required + * to convert it to an empty Map when appropriate. + */ + final public void MapData() throws ParseException {/*@bgen(jjtree) MapData */ + AstMapData jjtn000 = new AstMapData(JJTMAPDATA); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(START_SET_OR_MAP); + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case START_SET_OR_MAP: + case INTEGER_LITERAL: + case FLOATING_POINT_LITERAL: + case STRING_LITERAL: + case TRUE: + case FALSE: + case NULL: + case LPAREN: + case LBRACK: + case NOT0: + case NOT1: + case EMPTY: + case MINUS: + case IDENTIFIER:{ + MapEntry(); + label_18: + while (true) { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case COMMA:{ + ; + break; + } + default: + jj_la1[46] = jj_gen; + break label_18; + } + jj_consume_token(COMMA); + MapEntry(); + } + break; + } + default: + jj_la1[47] = jj_gen; + ; + } + jj_consume_token(RBRACE); + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + + final public void MapEntry() throws ParseException {/*@bgen(jjtree) MapEntry */ + AstMapEntry jjtn000 = new AstMapEntry(JJTMAPENTRY); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + Expression(); + jj_consume_token(COLON); + Expression(); + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Identifier + * Java Language Identifier + */ + final public void Identifier() throws ParseException {/*@bgen(jjtree) Identifier */ + AstIdentifier jjtn000 = new AstIdentifier(JJTIDENTIFIER); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + try { + t = jj_consume_token(IDENTIFIER); +jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; +jjtn000.setImage(t.image); + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Function + * Namespace:Name(a,b,c) + */ + final public void Function() throws ParseException {/*@bgen(jjtree) Function */ + AstFunction jjtn000 = new AstFunction(JJTFUNCTION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t0 = null; + Token t1 = null; + try { + t0 = jj_consume_token(IDENTIFIER); + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case COLON:{ + jj_consume_token(COLON); + t1 = jj_consume_token(IDENTIFIER); + break; + } + default: + jj_la1[48] = jj_gen; + ; + } +if (t1 != null) { + jjtn000.setPrefix(t0.image); + jjtn000.setLocalName(t1.image); + } else { + jjtn000.setLocalName(t0.image); + } + label_19: + while (true) { + MethodParameters(); + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case LPAREN:{ + ; + break; + } + default: + jj_la1[49] = jj_gen; + break label_19; + } + } + } catch (Throwable jjte000) { +if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) { + throw (RuntimeException)jjte000; + }} + } + if (jjte000 instanceof ParseException) { + {if (true) { + throw (ParseException)jjte000; + }} + } + {if (true) { + throw (Error)jjte000; + }} + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Literal + * Reserved Keywords + */ + final public void Literal() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case TRUE: + case FALSE:{ + Boolean(); + break; + } + case FLOATING_POINT_LITERAL:{ + FloatingPoint(); + break; + } + case INTEGER_LITERAL:{ + Integer(); + break; + } + case STRING_LITERAL:{ + String(); + break; + } + case NULL:{ + Null(); + break; + } + default: + jj_la1[50] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +} + +/* + * Boolean + * For 'true' 'false' + */ + final public void Boolean() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) { + case TRUE:{ +AstTrue jjtn001 = new AstTrue(JJTTRUE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jj_consume_token(TRUE); + } finally { +if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + } + case FALSE:{ +AstFalse jjtn002 = new AstFalse(JJTFALSE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jj_consume_token(FALSE); + } finally { +if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + } + default: + jj_la1[51] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +} + +/* + * FloatingPoint + * For Decimal and Floating Point Literals + */ + final public void FloatingPoint() throws ParseException {/*@bgen(jjtree) FloatingPoint */ + AstFloatingPoint jjtn000 = new AstFloatingPoint(JJTFLOATINGPOINT); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + try { + t = jj_consume_token(FLOATING_POINT_LITERAL); +jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; +jjtn000.setImage(t.image); + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Integer + * For Simple Numeric Literals + */ + final public void Integer() throws ParseException {/*@bgen(jjtree) Integer */ + AstInteger jjtn000 = new AstInteger(JJTINTEGER); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + try { + t = jj_consume_token(INTEGER_LITERAL); +jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; +jjtn000.setImage(t.image); + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * String + * For Quoted Literals + */ + final public void String() throws ParseException {/*@bgen(jjtree) String */ + AstString jjtn000 = new AstString(JJTSTRING); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + try { + t = jj_consume_token(STRING_LITERAL); +jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; +jjtn000.setImage(t.image); + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + +/* + * Null + * For 'null' + */ + final public void Null() throws ParseException {/*@bgen(jjtree) Null */ + AstNull jjtn000 = new AstNull(JJTNULL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(NULL); + } finally { +if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } +} + + private boolean jj_2_1(int xla) + { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return (!jj_3_1()); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(0, xla); } + } + + private boolean jj_2_2(int xla) + { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return (!jj_3_2()); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(1, xla); } + } + + private boolean jj_2_3(int xla) + { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return (!jj_3_3()); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(2, xla); } + } + + private boolean jj_2_4(int xla) + { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return (!jj_3_4()); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(3, xla); } + } + + private boolean jj_2_5(int xla) + { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return (!jj_3_5()); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(4, xla); } + } + + private boolean jj_2_6(int xla) + { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return (!jj_3_6()); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(5, xla); } + } + + private boolean jj_2_7(int xla) + { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return (!jj_3_7()); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(6, xla); } + } + + private boolean jj_2_8(int xla) + { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return (!jj_3_8()); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(7, xla); } + } + + private boolean jj_3R_And_173_17_41() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(39)) { + jj_scanpos = xsp; + if (jj_scan_token(40)) { + return true; + } + } + if (jj_3R_Equality_182_5_40()) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaExpressionOrInvocation_144_45_30() + { + if (jj_3R_Choice_155_5_22()) { + return true; + } + return false; + } + + private boolean jj_3R_Equality_182_5_40() + { + if (jj_3R_Compare_196_5_44()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_Equality_184_9_45()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_ListData_350_26_109() + { + if (jj_scan_token(COMMA)) { + return true; + } + if (jj_3R_Expression_99_5_36()) { + return true; + } + return false; + } + + private boolean jj_3R_MapEntry_368_5_107() + { + if (jj_3R_Expression_99_5_36()) { + return true; + } + if (jj_scan_token(COLON)) { + return true; + } + if (jj_3R_Expression_99_5_36()) { + return true; + } + return false; + } + + private boolean jj_3R_MapData_362_11_105() + { + if (jj_3R_MapEntry_368_5_107()) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaParameters_132_46_43() + { + if (jj_scan_token(COMMA)) { + return true; + } + if (jj_3R_Identifier_377_5_38()) { + return true; + } + return false; + } + + private boolean jj_3R_And_173_5_34() + { + if (jj_3R_Equality_182_5_40()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_And_173_17_41()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_SetData_343_26_37() + { + if (jj_scan_token(COMMA)) { + return true; + } + if (jj_3R_Expression_99_5_36()) { + return true; + } + return false; + } + + private boolean jj_3R_Or_164_12_35() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(41)) { + jj_scanpos = xsp; + if (jj_scan_token(42)) { + return true; + } + } + if (jj_3R_And_173_5_34()) { + return true; + } + return false; + } + + private boolean jj_3R_MapData_361_5_99() + { + if (jj_scan_token(START_SET_OR_MAP)) { + return true; + } + Token xsp; + xsp = jj_scanpos; + if (jj_3R_MapData_362_11_105()) { + jj_scanpos = xsp; + } + if (jj_scan_token(RBRACE)) { + return true; + } + return false; + } + + private boolean jj_3R_ListData_350_11_104() + { + if (jj_3R_Expression_99_5_36()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_ListData_350_26_109()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_Or_164_5_29() + { + if (jj_3R_And_173_5_34()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_Or_164_12_35()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3_5() + { + if (jj_scan_token(QUESTIONMARK)) { + return true; + } + if (jj_3R_Choice_155_5_22()) { + return true; + } + if (jj_scan_token(COLON)) { + return true; + } + return false; + } + + private boolean jj_3R_ListData_349_5_98() + { + if (jj_scan_token(LBRACK)) { + return true; + } + Token xsp; + xsp = jj_scanpos; + if (jj_3R_ListData_350_11_104()) { + jj_scanpos = xsp; + } + if (jj_scan_token(RBRACK)) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaParameters_132_31_39() + { + if (jj_3R_Identifier_377_5_38()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_LambdaParameters_132_46_43()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_SetData_343_11_31() + { + if (jj_3R_Expression_99_5_36()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_SetData_343_26_37()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_Choice_155_5_22() + { + if (jj_3R_Or_164_5_29()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3_5()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3_3() + { + if (jj_3R_LambdaExpression_124_5_21()) { + return true; + } + return false; + } + + private boolean jj_3R_MethodParameters_317_31_111() + { + if (jj_scan_token(COMMA)) { + return true; + } + return false; + } + + private boolean jj_3R_SetData_342_5_25() + { + if (jj_scan_token(START_SET_OR_MAP)) { + return true; + } + Token xsp; + xsp = jj_scanpos; + if (jj_3R_SetData_343_11_31()) { + jj_scanpos = xsp; + } + if (jj_scan_token(RBRACE)) { + return true; + } + return false; + } + + private boolean jj_3_4() + { + if (jj_3R_LambdaExpression_124_5_21()) { + return true; + } + return false; + } + + private boolean jj_3R_null_328_18_24() + { + if (jj_scan_token(IDENTIFIER)) { + return true; + } + if (jj_scan_token(COLON)) { + return true; + } + return false; + } + + private boolean jj_3_7() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_null_328_18_24()) { + jj_scanpos = xsp; + } + if (jj_scan_token(IDENTIFIER)) { + return true; + } + if (jj_scan_token(LPAREN)) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaParameters_132_20_33() + { + if (jj_scan_token(LPAREN)) { + return true; + } + Token xsp; + xsp = jj_scanpos; + if (jj_3R_LambdaParameters_132_31_39()) { + jj_scanpos = xsp; + } + if (jj_scan_token(RPAREN)) { + return true; + } + return false; + } + + private boolean jj_3R_NonLiteral_332_7_89() + { + if (jj_3R_MapData_361_5_99()) { + return true; + } + return false; + } + + private boolean jj_3R_NonLiteral_331_7_88() + { + if (jj_3R_ListData_349_5_98()) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaExpressionOrInvocation_141_5_23() + { + if (jj_scan_token(LPAREN)) { + return true; + } + if (jj_3R_LambdaParameters_132_5_27()) { + return true; + } + if (jj_scan_token(ARROW)) { + return true; + } + Token xsp; + xsp = jj_scanpos; + if (jj_3_4()) { + jj_scanpos = xsp; + if (jj_3R_LambdaExpressionOrInvocation_144_45_30()) { + return true; + } + } + if (jj_scan_token(RPAREN)) { + return true; + } + return false; + } + + private boolean jj_3_8() + { + if (jj_3R_SetData_342_5_25()) { + return true; + } + return false; + } + + private boolean jj_3R_NonLiteral_329_7_87() + { + if (jj_3R_Identifier_377_5_38()) { + return true; + } + return false; + } + + private boolean jj_3R_NonLiteral_328_7_86() + { + if (jj_3R_Function_390_5_97()) { + return true; + } + return false; + } + + private boolean jj_3R_NonLiteral_327_7_85() + { + if (jj_scan_token(LPAREN)) { + return true; + } + if (jj_3R_Expression_99_5_36()) { + return true; + } + if (jj_scan_token(RPAREN)) { + return true; + } + return false; + } + + private boolean jj_3R_MethodParameters_317_16_110() + { + if (jj_3R_Expression_99_5_36()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_MethodParameters_317_31_111()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_ValueSuffix_291_41_108() + { + if (jj_3R_MethodParameters_317_5_106()) { + return true; + } + return false; + } + + private boolean jj_3R_NonLiteral_326_5_77() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3_6()) { + jj_scanpos = xsp; + if (jj_3R_NonLiteral_327_7_85()) { + jj_scanpos = xsp; + if (jj_3R_NonLiteral_328_7_86()) { + jj_scanpos = xsp; + if (jj_3R_NonLiteral_329_7_87()) { + jj_scanpos = xsp; + if (jj_3_8()) { + jj_scanpos = xsp; + if (jj_3R_NonLiteral_331_7_88()) { + jj_scanpos = xsp; + if (jj_3R_NonLiteral_332_7_89()) { + return true; + } + } + } + } + } + } + } + return false; + } + + private boolean jj_3_6() + { + if (jj_3R_LambdaExpressionOrInvocation_141_5_23()) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaParameters_132_5_32() + { + if (jj_3R_Identifier_377_5_38()) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaParameters_132_5_27() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_LambdaParameters_132_5_32()) { + jj_scanpos = xsp; + if (jj_3R_LambdaParameters_132_20_33()) { + return true; + } + } + return false; + } + + private boolean jj_3_1() + { + if (jj_scan_token(ASSIGN)) { + return true; + } + if (jj_3R_Assignment_115_5_20()) { + return true; + } + return false; + } + + private boolean jj_3R_MethodParameters_317_5_106() + { + if (jj_scan_token(LPAREN)) { + return true; + } + Token xsp; + xsp = jj_scanpos; + if (jj_3R_MethodParameters_317_16_110()) { + jj_scanpos = xsp; + } + if (jj_scan_token(RPAREN)) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaExpression_124_5_21() + { + if (jj_3R_LambdaParameters_132_5_27()) { + return true; + } + if (jj_scan_token(ARROW)) { + return true; + } + Token xsp; + xsp = jj_scanpos; + if (jj_3_3()) { + jj_scanpos = xsp; + if (jj_3R_LambdaExpression_124_68_28()) { + return true; + } + } + return false; + } + + private boolean jj_3R_Semicolon_107_20_46() + { + if (jj_scan_token(SEMICOLON)) { + return true; + } + if (jj_3R_Assignment_115_5_20()) { + return true; + } + return false; + } + + private boolean jj_3R_BracketSuffix_309_5_91() + { + if (jj_scan_token(LBRACK)) { + return true; + } + if (jj_3R_Expression_99_5_36()) { + return true; + } + if (jj_scan_token(RBRACK)) { + return true; + } + return false; + } + + private boolean jj_3R_ValueSuffix_291_21_79() + { + if (jj_3R_BracketSuffix_309_5_91()) { + return true; + } + return false; + } + + private boolean jj_3R_Assignment_116_5_26() + { + if (jj_3R_Choice_155_5_22()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3_1()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3_2() + { + if (jj_3R_LambdaExpression_124_5_21()) { + return true; + } + return false; + } + + private boolean jj_3R_Assignment_115_5_20() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3_2()) { + jj_scanpos = xsp; + if (jj_3R_Assignment_116_5_26()) { + return true; + } + } + return false; + } + + private boolean jj_3R_DotSuffix_300_5_90() + { + if (jj_scan_token(DOT)) { + return true; + } + if (jj_scan_token(IDENTIFIER)) { + return true; + } + return false; + } + + private boolean jj_3R_Semicolon_107_5_42() + { + if (jj_3R_Assignment_115_5_20()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_Semicolon_107_20_46()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_ValueSuffix_291_7_78() + { + if (jj_3R_DotSuffix_300_5_90()) { + return true; + } + return false; + } + + private boolean jj_3R_ValueSuffix_291_5_75() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_ValueSuffix_291_7_78()) { + jj_scanpos = xsp; + if (jj_3R_ValueSuffix_291_21_79()) { + return true; + } + } + xsp = jj_scanpos; + if (jj_3R_ValueSuffix_291_41_108()) { + jj_scanpos = xsp; + } + return false; + } + + private boolean jj_3R_Expression_99_5_36() + { + if (jj_3R_Semicolon_107_5_42()) { + return true; + } + return false; + } + + private boolean jj_3R_Value_272_21_72() + { + if (jj_3R_ValueSuffix_291_5_75()) { + return true; + } + return false; + } + + private boolean jj_3R_ValuePrefix_282_7_74() + { + if (jj_3R_NonLiteral_326_5_77()) { + return true; + } + return false; + } + + private boolean jj_3R_ValuePrefix_281_5_71() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_ValuePrefix_281_5_73()) { + jj_scanpos = xsp; + if (jj_3R_ValuePrefix_282_7_74()) { + return true; + } + } + return false; + } + + private boolean jj_3R_ValuePrefix_281_5_73() + { + if (jj_3R_Literal_408_5_76()) { + return true; + } + return false; + } + + private boolean jj_3R_Value_272_5_70() + { + if (jj_3R_ValuePrefix_281_5_71()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_Value_272_21_72()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_Null_458_5_96() + { + if (jj_scan_token(NULL)) { + return true; + } + return false; + } + + private boolean jj_3R_Unary_263_9_66() + { + if (jj_3R_Value_272_5_70()) { + return true; + } + return false; + } + + private boolean jj_3R_Unary_261_9_65() + { + if (jj_scan_token(EMPTY)) { + return true; + } + if (jj_3R_Unary_257_9_59()) { + return true; + } + return false; + } + + private boolean jj_3R_Unary_259_9_64() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(37)) { + jj_scanpos = xsp; + if (jj_scan_token(38)) { + return true; + } + } + if (jj_3R_Unary_257_9_59()) { + return true; + } + return false; + } + + private boolean jj_3R_Unary_257_9_59() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_Unary_257_9_63()) { + jj_scanpos = xsp; + if (jj_3R_Unary_259_9_64()) { + jj_scanpos = xsp; + if (jj_3R_Unary_261_9_65()) { + jj_scanpos = xsp; + if (jj_3R_Unary_263_9_66()) { + return true; + } + } + } + } + return false; + } + + private boolean jj_3R_Unary_257_9_63() + { + if (jj_scan_token(MINUS)) { + return true; + } + if (jj_3R_Unary_257_9_59()) { + return true; + } + return false; + } + + private boolean jj_3R_String_449_5_95() + { + if (jj_scan_token(STRING_LITERAL)) { + return true; + } + return false; + } + + private boolean jj_3R_Multiplication_247_9_69() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(51)) { + jj_scanpos = xsp; + if (jj_scan_token(52)) { + return true; + } + } + if (jj_3R_Unary_257_9_59()) { + return true; + } + return false; + } + + private boolean jj_3R_Integer_440_5_94() + { + if (jj_scan_token(INTEGER_LITERAL)) { + return true; + } + return false; + } + + private boolean jj_3R_Multiplication_245_9_68() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(49)) { + jj_scanpos = xsp; + if (jj_scan_token(50)) { + return true; + } + } + if (jj_3R_Unary_257_9_59()) { + return true; + } + return false; + } + + private boolean jj_3R_Multiplication_243_9_60() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_Multiplication_243_9_67()) { + jj_scanpos = xsp; + if (jj_3R_Multiplication_245_9_68()) { + jj_scanpos = xsp; + if (jj_3R_Multiplication_247_9_69()) { + return true; + } + } + } + return false; + } + + private boolean jj_3R_Multiplication_243_9_67() + { + if (jj_scan_token(MULT)) { + return true; + } + if (jj_3R_Unary_257_9_59()) { + return true; + } + return false; + } + + private boolean jj_3R_Multiplication_241_5_57() + { + if (jj_3R_Unary_257_9_59()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_Multiplication_243_9_60()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_FloatingPoint_431_5_93() + { + if (jj_scan_token(FLOATING_POINT_LITERAL)) { + return true; + } + return false; + } + + private boolean jj_3R_Math_231_9_62() + { + if (jj_scan_token(MINUS)) { + return true; + } + if (jj_3R_Multiplication_241_5_57()) { + return true; + } + return false; + } + + private boolean jj_3R_Boolean_423_7_101() + { + if (jj_scan_token(FALSE)) { + return true; + } + return false; + } + + private boolean jj_3R_Math_229_9_58() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_Math_229_9_61()) { + jj_scanpos = xsp; + if (jj_3R_Math_231_9_62()) { + return true; + } + } + return false; + } + + private boolean jj_3R_Math_229_9_61() + { + if (jj_scan_token(PLUS)) { + return true; + } + if (jj_3R_Multiplication_241_5_57()) { + return true; + } + return false; + } + + private boolean jj_3R_Boolean_421_5_100() + { + if (jj_scan_token(TRUE)) { + return true; + } + return false; + } + + private boolean jj_3R_Boolean_421_5_92() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_Boolean_421_5_100()) { + jj_scanpos = xsp; + if (jj_3R_Boolean_423_7_101()) { + return true; + } + } + return false; + } + + private boolean jj_3R_Math_227_5_51() + { + if (jj_3R_Multiplication_241_5_57()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_Math_229_9_58()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_Literal_412_7_84() + { + if (jj_3R_Null_458_5_96()) { + return true; + } + return false; + } + + private boolean jj_3R_Literal_411_7_83() + { + if (jj_3R_String_449_5_95()) { + return true; + } + return false; + } + + private boolean jj_3R_Concatenation_217_10_52() + { + if (jj_scan_token(CONCAT)) { + return true; + } + if (jj_3R_Math_227_5_51()) { + return true; + } + return false; + } + + private boolean jj_3R_Literal_410_7_82() + { + if (jj_3R_Integer_440_5_94()) { + return true; + } + return false; + } + + private boolean jj_3R_Literal_409_7_81() + { + if (jj_3R_FloatingPoint_431_5_93()) { + return true; + } + return false; + } + + private boolean jj_3R_Function_390_24_102() + { + if (jj_scan_token(COLON)) { + return true; + } + if (jj_scan_token(IDENTIFIER)) { + return true; + } + return false; + } + + private boolean jj_3R_Literal_408_5_80() + { + if (jj_3R_Boolean_421_5_92()) { + return true; + } + return false; + } + + private boolean jj_3R_Literal_408_5_76() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_Literal_408_5_80()) { + jj_scanpos = xsp; + if (jj_3R_Literal_409_7_81()) { + jj_scanpos = xsp; + if (jj_3R_Literal_410_7_82()) { + jj_scanpos = xsp; + if (jj_3R_Literal_411_7_83()) { + jj_scanpos = xsp; + if (jj_3R_Literal_412_7_84()) { + return true; + } + } + } + } + } + return false; + } + + private boolean jj_3R_Concatenation_215_6_47() + { + if (jj_3R_Math_227_5_51()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_Concatenation_217_10_52()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_Function_399_7_103() + { + if (jj_3R_MethodParameters_317_5_106()) { + return true; + } + return false; + } + + private boolean jj_3R_Compare_204_9_56() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(29)) { + jj_scanpos = xsp; + if (jj_scan_token(30)) { + return true; + } + } + if (jj_3R_Concatenation_215_6_47()) { + return true; + } + return false; + } + + private boolean jj_3R_Compare_202_9_55() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(31)) { + jj_scanpos = xsp; + if (jj_scan_token(32)) { + return true; + } + } + if (jj_3R_Concatenation_215_6_47()) { + return true; + } + return false; + } + + private boolean jj_3R_Compare_200_9_54() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(25)) { + jj_scanpos = xsp; + if (jj_scan_token(26)) { + return true; + } + } + if (jj_3R_Concatenation_215_6_47()) { + return true; + } + return false; + } + + private boolean jj_3R_Compare_198_9_48() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_Compare_198_9_53()) { + jj_scanpos = xsp; + if (jj_3R_Compare_200_9_54()) { + jj_scanpos = xsp; + if (jj_3R_Compare_202_9_55()) { + jj_scanpos = xsp; + if (jj_3R_Compare_204_9_56()) { + return true; + } + } + } + } + return false; + } + + private boolean jj_3R_Compare_198_9_53() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(27)) { + jj_scanpos = xsp; + if (jj_scan_token(28)) { + return true; + } + } + if (jj_3R_Concatenation_215_6_47()) { + return true; + } + return false; + } + + private boolean jj_3R_Function_390_5_97() + { + if (jj_scan_token(IDENTIFIER)) { + return true; + } + Token xsp; + xsp = jj_scanpos; + if (jj_3R_Function_390_24_102()) { + jj_scanpos = xsp; + } + if (jj_3R_Function_399_7_103()) { + return true; + } + while (true) { + xsp = jj_scanpos; + if (jj_3R_Function_399_7_103()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_Compare_196_5_44() + { + if (jj_3R_Concatenation_215_6_47()) { + return true; + } + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_Compare_198_9_48()) { jj_scanpos = xsp; break; } + } + return false; + } + + private boolean jj_3R_Equality_186_9_50() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(35)) { + jj_scanpos = xsp; + if (jj_scan_token(36)) { + return true; + } + } + if (jj_3R_Compare_196_5_44()) { + return true; + } + return false; + } + + private boolean jj_3R_Equality_184_9_45() + { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_Equality_184_9_49()) { + jj_scanpos = xsp; + if (jj_3R_Equality_186_9_50()) { + return true; + } + } + return false; + } + + private boolean jj_3R_Equality_184_9_49() + { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(33)) { + jj_scanpos = xsp; + if (jj_scan_token(34)) { + return true; + } + } + if (jj_3R_Compare_196_5_44()) { + return true; + } + return false; + } + + private boolean jj_3R_LambdaExpression_124_68_28() + { + if (jj_3R_Choice_155_5_22()) { + return true; + } + return false; + } + + private boolean jj_3R_Identifier_377_5_38() + { + if (jj_scan_token(IDENTIFIER)) { + return true; + } + return false; + } + + /** Generated Token Manager. */ + public ELParserTokenManager token_source; + SimpleCharStream jj_input_stream; + /** Current token. */ + public Token token; + /** Next token. */ + public Token jj_nt; + private int jj_ntk; + private Token jj_scanpos, jj_lastpos; + private int jj_la; + private int jj_gen; + final private int[] jj_la1 = new int[52]; + static private int[] jj_la1_0; + static private int[] jj_la1_1; + static { + jj_la1_init_0(); + jj_la1_init_1(); + } + private static void jj_la1_init_0() { + jj_la1_0 = new int[] {0xe,0xe,0x800000,0x15ed00,0x15ed00,0x1000000,0x0,0x40000,0x15ed00,0x40000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xfe000000,0x18000000,0x6000000,0x80000000,0x60000000,0xfe000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x15ed00,0x120000,0x15ed00,0x120000,0x40000,0x1000000,0x15ed00,0x40000,0x0,0x100100,0x1000000,0x15ed00,0x1000000,0x15ed00,0x1000000,0x15ed00,0x400000,0x40000,0x1ec00,0xc000,}; + } + private static void jj_la1_init_1() { + jj_la1_1 = new int[] {0x0,0x0,0x0,0x1008860,0x1008860,0x0,0x1000000,0x1000000,0x1008860,0x0,0x600,0x600,0x180,0x180,0x1e,0x6,0x18,0x1e,0x1,0x0,0x0,0x1,0x0,0x1,0x200000,0xc000,0xc000,0x1e2000,0x60000,0x180000,0x1e2000,0x60,0x1008860,0x0,0x1000000,0x0,0x0,0x0,0x1008860,0x0,0x1000000,0x0,0x0,0x1008860,0x0,0x1008860,0x0,0x1008860,0x0,0x0,0x0,0x0,}; + } + final private JJCalls[] jj_2_rtns = new JJCalls[8]; + private boolean jj_rescan = false; + private int jj_gc = 0; + + /** Constructor with InputStream. */ + public ELParser(java.io.InputStream stream) { + this(stream, null); + } + /** Constructor with InputStream and supplied encoding */ + public ELParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new ELParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 52; i++) { + jj_la1[i] = -1; + } + for (int i = 0; i < jj_2_rtns.length; i++) { + jj_2_rtns[i] = new JJCalls(); + } + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + /** Reinitialise. */ + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 52; i++) { + jj_la1[i] = -1; + } + for (int i = 0; i < jj_2_rtns.length; i++) { + jj_2_rtns[i] = new JJCalls(); + } + } + + /** Constructor. */ + public ELParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new ELParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 52; i++) { + jj_la1[i] = -1; + } + for (int i = 0; i < jj_2_rtns.length; i++) { + jj_2_rtns[i] = new JJCalls(); + } + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader stream) { + if (jj_input_stream == null) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + } else { + jj_input_stream.ReInit(stream, 1, 1); + } + if (token_source == null) { + token_source = new ELParserTokenManager(jj_input_stream); + } + + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 52; i++) { + jj_la1[i] = -1; + } + for (int i = 0; i < jj_2_rtns.length; i++) { + jj_2_rtns[i] = new JJCalls(); + } + } + + /** Constructor with generated Token Manager. */ + public ELParser(ELParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 52; i++) { + jj_la1[i] = -1; + } + for (int i = 0; i < jj_2_rtns.length; i++) { + jj_2_rtns[i] = new JJCalls(); + } + } + + /** Reinitialise. */ + public void ReInit(ELParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 52; i++) { + jj_la1[i] = -1; + } + for (int i = 0; i < jj_2_rtns.length; i++) { + jj_2_rtns[i] = new JJCalls(); + } + } + + private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) { + token = token.next; + } else { + token = token.next = token_source.getNextToken(); + } + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + if (++jj_gc > 100) { + jj_gc = 0; + for (int i = 0; i < jj_2_rtns.length; i++) { + JJCalls c = jj_2_rtns[i]; + while (c != null) { + if (c.gen < jj_gen) { + c.first = null; + } + c = c.next; + } + } + } + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + @SuppressWarnings("serial") + static private final class LookaheadSuccess extends java.lang.Error { + @Override + public Throwable fillInStackTrace() { + return this; + } + } + static private final LookaheadSuccess jj_ls = new LookaheadSuccess(); + private boolean jj_scan_token(int kind) { + if (jj_scanpos == jj_lastpos) { + jj_la--; + if (jj_scanpos.next == null) { + jj_lastpos = jj_scanpos = jj_scanpos.next = token_source.getNextToken(); + } else { + jj_lastpos = jj_scanpos = jj_scanpos.next; + } + } else { + jj_scanpos = jj_scanpos.next; + } + if (jj_rescan) { + int i = 0; Token tok = token; + while (tok != null && tok != jj_scanpos) { i++; tok = tok.next; } + if (tok != null) { + jj_add_error_token(kind, i); + } + } + if (jj_scanpos.kind != kind) { + return true; + } + if (jj_la == 0 && jj_scanpos == jj_lastpos) { + throw jj_ls; + } + return false; + } + + +/** Get the next Token. */ + final public Token getNextToken() { + if (token.next != null) { + token = token.next; + } else { + token = token.next = token_source.getNextToken(); + } + jj_ntk = -1; + jj_gen++; + return token; + } + +/** Get the specific Token. */ + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) { + t = t.next; + } else { + t = t.next = token_source.getNextToken(); + } + } + return t; + } + + private int jj_ntk_f() { + if ((jj_nt=token.next) == null) { + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + } else { + return (jj_ntk = jj_nt.kind); + } + } + + private java.util.List jj_expentries = new java.util.ArrayList(); + private int[] jj_expentry; + private int jj_kind = -1; + private int[] jj_lasttokens = new int[100]; + private int jj_endpos; + + private void jj_add_error_token(int kind, int pos) { + if (pos >= 100) { + return; + } + + if (pos == jj_endpos + 1) { + jj_lasttokens[jj_endpos++] = kind; + } else if (jj_endpos != 0) { + jj_expentry = new int[jj_endpos]; + + for (int i = 0; i < jj_endpos; i++) { + jj_expentry[i] = jj_lasttokens[i]; + } + + for (int[] oldentry : jj_expentries) { + if (oldentry.length == jj_expentry.length) { + boolean isMatched = true; + + for (int i = 0; i < jj_expentry.length; i++) { + if (oldentry[i] != jj_expentry[i]) { + isMatched = false; + break; + } + + } + if (isMatched) { + jj_expentries.add(jj_expentry); + break; + } + } + } + + if (pos != 0) { + jj_lasttokens[(jj_endpos = pos) - 1] = kind; + } + } + } + + /** Generate ParseException. */ + public ParseException generateParseException() { + jj_expentries.clear(); + boolean[] la1tokens = new boolean[62]; + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 52; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1< jj_gen) { + jj_la = p.arg; jj_lastpos = jj_scanpos = p.first; + switch (i) { + case 0: jj_3_1(); break; + case 1: jj_3_2(); break; + case 2: jj_3_3(); break; + case 3: jj_3_4(); break; + case 4: jj_3_5(); break; + case 5: jj_3_6(); break; + case 6: jj_3_7(); break; + case 7: jj_3_8(); break; + } + } + p = p.next; + } while (p != null); + + } catch(LookaheadSuccess ls) { } + } + jj_rescan = false; + } + + private void jj_save(int index, int xla) { + JJCalls p = jj_2_rtns[index]; + while (p.gen > jj_gen) { + if (p.next == null) { p = p.next = new JJCalls(); break; } + p = p.next; + } + + p.gen = jj_gen + xla - jj_la; + p.first = token; + p.arg = xla; + } + + static final class JJCalls { + int gen; + Token first; + int arg; + JJCalls next; + } + +} diff --git a/java/org/apache/el/parser/ELParser.jjt b/java/org/apache/el/parser/ELParser.jjt new file mode 100644 index 0000000..ce9c7fe --- /dev/null +++ b/java/org/apache/el/parser/ELParser.jjt @@ -0,0 +1,587 @@ +/* + * 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. + */ + +/* + Author: Jacob Hookom + Email: jacob at hookom.net +*/ + +/* == Option Declaration == */ +options +{ + STATIC=false; + NODE_PREFIX="Ast"; + VISITOR_EXCEPTION="jakarta.el.ELException"; + VISITOR=false; + MULTI=true; + NODE_DEFAULT_VOID=true; + JAVA_UNICODE_ESCAPE=false; + UNICODE_INPUT=true; + BUILD_NODE_FILES=true; +} + +/* == Parser Declaration == */ +PARSER_BEGIN( ELParser ) +package org.apache.el.parser; +import java.io.StringReader; +import jakarta.el.ELException; +public class ELParser { + + public static Node parse(String ref) throws ELException { + try { + return new ELParser(new StringReader(ref)).CompositeExpression(); + } catch (ParseException pe) { + throw new ELException(pe.getMessage()); + } + } +} +PARSER_END( ELParser ) + +/* + * CompositeExpression + * Allow most flexible parsing, restrict by examining + * type of returned node + */ +AstCompositeExpression CompositeExpression() #CompositeExpression : {} +{ + (DeferredExpression() | + DynamicExpression() | + LiteralExpression())* { return jjtThis; } +} + +/* + * LiteralExpression + * Non-EL Expression blocks + */ +void LiteralExpression() #LiteralExpression : { Token t = null; } +{ + t= { jjtThis.setImage(t.image); } +} + +/* + * DeferredExpression + * #{...} Expressions + */ +void DeferredExpression() #DeferredExpression : {} +{ + Expression() +} + +/* + * DynamicExpression + * ${...} Expressions + */ +void DynamicExpression() #DynamicExpression : {} +{ + Expression() +} + +/* + * Expression + * EL Expression Language Root + */ +void Expression() : {} +{ + Semicolon() +} + +/* + * Semicolon + */ +void Semicolon() : {} +{ + Assignment() ( Assignment() #Semicolon(2) )* +} + +/* + * Assignment + */ +void Assignment() : {} +{ + LOOKAHEAD(4) LambdaExpression() | + Choice() ( LOOKAHEAD(2) Assignment() #Assign(2) )* +} + +/* + * Lambda expression + */ +void LambdaExpression() #LambdaExpression : {} +{ + LambdaParameters() ( LOOKAHEAD(3) LambdaExpression() | Choice() ) +} + +/* + * Lambda parameters + */ +void LambdaParameters() #LambdaParameters : {} +{ + Identifier() | ( Identifier() ( Identifier() )* )? +} + +/* + * Possible invocation of lambda expression. Invocations must be bracketed but + * being bracketed does not mean it is an invocation. + */ +void LambdaExpressionOrInvocation() #LambdaExpression : {} +{ + + LambdaParameters() + + ( LOOKAHEAD(3) LambdaExpression() | Choice() ) + + ( MethodParameters() )* +} + +/* + * Choice + * For Choice markup a ? b : c, then Or + */ +void Choice() : {} +{ + Or() (LOOKAHEAD(3) Choice() Choice() #Choice(3))* +} + +/* + * Or + * For 'or' '||', then And + */ +void Or() : {} +{ + And() ((|) And() #Or(2))* +} + +/* + * And + * For 'and' '&&', then Equality + */ +void And() : {} +{ + Equality() ((|) Equality() #And(2))* +} + +/* + * Equality + * For '==' 'eq' '!=' 'ne', then Compare + */ +void Equality() : {} +{ + Compare() + ( + ((|) Compare() #Equal(2)) + | + ((|) Compare() #NotEqual(2)) + )* +} + +/* + * Compare + * For a bunch of them, then += + */ +void Compare() : {} +{ + Concatenation() + ( + ((|) Concatenation() #LessThan(2)) + | + ((|) Concatenation() #GreaterThan(2)) + | + ((|) Concatenation() #LessThanEqual(2)) + | + ((|) Concatenation() #GreaterThanEqual(2)) + )* +} + +/* + * Concatenation + * For +=, then Math + * + */ + void Concatenation() : {} + { + Math() + ( + Math() #Concatenation(2) + )* + } + +/* + * Math + * For '+' '-', then Multiplication + */ +void Math() : {} +{ + Multiplication() + ( + ( Multiplication() #Plus(2)) + | + ( Multiplication() #Minus(2)) + )* +} + +/* + * Multiplication + * For a bunch of them, then Unary + */ +void Multiplication() : {} +{ + Unary() + ( + ( Unary() #Mult(2)) + | + ((|) Unary() #Div(2)) + | + ((|) Unary() #Mod(2)) + )* +} + +/* + * Unary + * For '-' '!' 'not' 'empty', then Value + */ +void Unary() : {} +{ + Unary() #Negative + | + (|) Unary() #Not + | + Unary() #Empty + | + Value() +} + +/* + * Value + * Defines Prefix plus zero or more Suffixes + */ +void Value() : {} +{ + (ValuePrefix() (ValueSuffix())*) #Value(>1) +} + +/* + * ValuePrefix + * For Literals, Variables, and Functions + */ +void ValuePrefix() : {} +{ + Literal() + | NonLiteral() +} + +/* + * ValueSuffix + * Either dot or bracket notation + */ +void ValueSuffix() : {} +{ + ( DotSuffix() | BracketSuffix() ) ( MethodParameters())? +} + +/* + * DotSuffix + * Dot Property + */ +void DotSuffix() #DotSuffix : { Token t = null; } +{ + t= { jjtThis.setImage(t.image); } +} + +/* + * BracketSuffix + * Sub Expression Suffix + */ +void BracketSuffix() #BracketSuffix : {} +{ + Expression() +} + +/* + * MethodParameters + */ +void MethodParameters() #MethodParameters : {} +{ + ( Expression() ( Expression())* )? +} + +/* + * NonLiteral + * For Grouped Operations, Identifiers, and Functions + */ +void NonLiteral() : {} +{ + LOOKAHEAD(5) LambdaExpressionOrInvocation() + | Expression() + | LOOKAHEAD(( )? ) Function() + | Identifier() + | LOOKAHEAD(5)SetData() + | ListData() + | MapData() +} + +/* + * Note that both an empty Set and an empty Map are represented by {}. The + * parser will always parse {} as an empty Set and special handling is required + * to convert it to an empty Map when appropriate. + */ +void SetData() #SetData: {} +{ + + ( Expression() ( Expression() )* )? + +} + +void ListData() #ListData: {} +{ + + ( Expression() ( Expression() )* )? + +} + +/* + * Note that both an empty Set and an empty Map are represented by {}. The + * parser will always parse {} as an empty Set and special handling is required + * to convert it to an empty Map when appropriate. + */ +void MapData() #MapData: {} +{ + + ( MapEntry() ( MapEntry() )* )? + +} + +void MapEntry() #MapEntry: {} +{ + Expression() Expression() +} + +/* + * Identifier + * Java Language Identifier + */ +void Identifier() #Identifier : { Token t = null; } +{ + t= { jjtThis.setImage(t.image); } +} + +/* + * Function + * Namespace:Name(a,b,c) + */ +void Function() #Function : +{ + Token t0 = null; + Token t1 = null; +} +{ + t0= ( t1= )? + { + if (t1 != null) { + jjtThis.setPrefix(t0.image); + jjtThis.setLocalName(t1.image); + } else { + jjtThis.setLocalName(t0.image); + } + } + ( MethodParameters() )+ +} + +/* + * Literal + * Reserved Keywords + */ +void Literal() : {} +{ + Boolean() + | FloatingPoint() + | Integer() + | String() + | Null() +} + +/* + * Boolean + * For 'true' 'false' + */ +void Boolean() : {} +{ + #True + | #False +} + +/* + * FloatingPoint + * For Decimal and Floating Point Literals + */ +void FloatingPoint() #FloatingPoint : { Token t = null; } +{ + t= { jjtThis.setImage(t.image); } +} + +/* + * Integer + * For Simple Numeric Literals + */ +void Integer() #Integer : { Token t = null; } +{ + t= { jjtThis.setImage(t.image); } +} + +/* + * String + * For Quoted Literals + */ +void String() #String : { Token t = null; } +{ + t= { jjtThis.setImage(t.image); } +} + +/* + * Null + * For 'null' + */ +void Null() #Null : {} +{ + +} + + +/* ========================================================================== */ +TOKEN_MGR_DECLS: +{ +java.util.Deque deque = new java.util.ArrayDeque(); +} + TOKEN : +{ + /* + * The following definition uses + rather than * in two places to prevent + * LITERAL_EXPRESSION matching the empty string that could result in the + * Parser entering an infinite loop. + */ + < LITERAL_EXPRESSION: + ( (~["$", "#", "\\"])* "\\" (["$", "#"])? + | (~["$", "#"])* (["$", "#"] ~["{", "$", "#", "\\"]) + | (~["$", "#"])+ + )+ + | "$" + | "#" + > +| + < START_DYNAMIC_EXPRESSION: "${" > {deque.push(DEFAULT);}: IN_EXPRESSION +| + < START_DEFERRED_EXPRESSION: "#{" > {deque.push(DEFAULT);}: IN_EXPRESSION +} + + SKIP : { " " | "\t" | "\n" | "\r" } + + TOKEN : +{ + < START_SET_OR_MAP : "{" > {deque.push(curLexState);}: IN_SET_OR_MAP +| < RBRACE: "}" > {SwitchTo(deque.pop());} +| < INTEGER_LITERAL: ["0"-"9"] (["0"-"9"])* > +| < FLOATING_POINT_LITERAL: (["0"-"9"])+ "." (["0"-"9"])* ()? + | "." (["0"-"9"])+ ()? + | (["0"-"9"])+ + > +| < #EXPONENT: ["e","E"] (["+","-"])? (["0"-"9"])+ > +| < STRING_LITERAL: ("\"" ((~["\"","\\"]) + | ("\\" ( ["\\","\"","\'"] )))* "\"") + | ("\'" ((~["\'","\\"]) + | ("\\" ( ["\\","\"","\'"] )))* "\'") + > +| < TRUE : "true" > +| < FALSE : "false" > +| < NULL : "null" > +| < DOT : "." > +| < LPAREN : "(" > +| < RPAREN : ")" > +| < LBRACK : "[" > +| < RBRACK : "]" > +| < COLON : ":" > +| < SEMICOLON : ";" > +| < COMMA : "," > +| < GT0 : ">" > +| < GT1 : "gt" > +| < LT0 : "<" > +| < LT1 : "lt" > +| < GE0 : ">=" > +| < GE1 : "ge" > +| < LE0 : "<=" > +| < LE1 : "le" > +| < EQ0 : "==" > +| < EQ1 : "eq" > +| < NE0 : "!=" > +| < NE1 : "ne" > +| < NOT0 : "!" > +| < NOT1 : "not" > +| < AND0 : "&&" > +| < AND1 : "and" > +| < OR0 : "||" > +| < OR1 : "or" > +| < EMPTY : "empty" > +| < INSTANCEOF : "instanceof" > +| < MULT : "*" > +| < PLUS : "+" > +| < MINUS : "-" > +| < QUESTIONMARK : "?" > +| < DIV0 : "/" > +| < DIV1 : "div" > +| < MOD0 : "%" > +| < MOD1 : "mod" > +| < CONCAT : "+=" > +| < ASSIGN : "=" > +| < ARROW : "->" > +| < IDENTIFIER : (|) (|)* > +| < FUNCTIONSUFFIX : () > +| < #IMPL_OBJ_START: "#" > +| < #LETTER: + [ + "\u0024", + "\u0041"-"\u005a", + "\u005f", + "\u0061"-"\u007a", + "\u00c0"-"\u00d6", + "\u00d8"-"\u00f6", + "\u00f8"-"\u00ff", + "\u0100"-"\u1fff", + "\u3040"-"\u318f", + "\u3300"-"\u337f", + "\u3400"-"\u3d2d", + "\u4e00"-"\u9fff", + "\uf900"-"\ufaff" + ] + > +| < #DIGIT: + [ + "\u0030"-"\u0039", + "\u0660"-"\u0669", + "\u06f0"-"\u06f9", + "\u0966"-"\u096f", + "\u09e6"-"\u09ef", + "\u0a66"-"\u0a6f", + "\u0ae6"-"\u0aef", + "\u0b66"-"\u0b6f", + "\u0be7"-"\u0bef", + "\u0c66"-"\u0c6f", + "\u0ce6"-"\u0cef", + "\u0d66"-"\u0d6f", + "\u0e50"-"\u0e59", + "\u0ed0"-"\u0ed9", + "\u1040"-"\u1049" + ] + > +| < ILLEGAL_CHARACTER: (~[]) > +} diff --git a/java/org/apache/el/parser/ELParserConstants.java b/java/org/apache/el/parser/ELParserConstants.java new file mode 100644 index 0000000..906d150 --- /dev/null +++ b/java/org/apache/el/parser/ELParserConstants.java @@ -0,0 +1,201 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. ELParserConstants.java */ +package org.apache.el.parser; + + +/** + * Token literal values and constants. + * Generated by org.javacc.parser.OtherFilesGen#start() + */ +public interface ELParserConstants { + + /** End of File. */ + int EOF = 0; + /** RegularExpression Id. */ + int LITERAL_EXPRESSION = 1; + /** RegularExpression Id. */ + int START_DYNAMIC_EXPRESSION = 2; + /** RegularExpression Id. */ + int START_DEFERRED_EXPRESSION = 3; + /** RegularExpression Id. */ + int START_SET_OR_MAP = 8; + /** RegularExpression Id. */ + int RBRACE = 9; + /** RegularExpression Id. */ + int INTEGER_LITERAL = 10; + /** RegularExpression Id. */ + int FLOATING_POINT_LITERAL = 11; + /** RegularExpression Id. */ + int EXPONENT = 12; + /** RegularExpression Id. */ + int STRING_LITERAL = 13; + /** RegularExpression Id. */ + int TRUE = 14; + /** RegularExpression Id. */ + int FALSE = 15; + /** RegularExpression Id. */ + int NULL = 16; + /** RegularExpression Id. */ + int DOT = 17; + /** RegularExpression Id. */ + int LPAREN = 18; + /** RegularExpression Id. */ + int RPAREN = 19; + /** RegularExpression Id. */ + int LBRACK = 20; + /** RegularExpression Id. */ + int RBRACK = 21; + /** RegularExpression Id. */ + int COLON = 22; + /** RegularExpression Id. */ + int SEMICOLON = 23; + /** RegularExpression Id. */ + int COMMA = 24; + /** RegularExpression Id. */ + int GT0 = 25; + /** RegularExpression Id. */ + int GT1 = 26; + /** RegularExpression Id. */ + int LT0 = 27; + /** RegularExpression Id. */ + int LT1 = 28; + /** RegularExpression Id. */ + int GE0 = 29; + /** RegularExpression Id. */ + int GE1 = 30; + /** RegularExpression Id. */ + int LE0 = 31; + /** RegularExpression Id. */ + int LE1 = 32; + /** RegularExpression Id. */ + int EQ0 = 33; + /** RegularExpression Id. */ + int EQ1 = 34; + /** RegularExpression Id. */ + int NE0 = 35; + /** RegularExpression Id. */ + int NE1 = 36; + /** RegularExpression Id. */ + int NOT0 = 37; + /** RegularExpression Id. */ + int NOT1 = 38; + /** RegularExpression Id. */ + int AND0 = 39; + /** RegularExpression Id. */ + int AND1 = 40; + /** RegularExpression Id. */ + int OR0 = 41; + /** RegularExpression Id. */ + int OR1 = 42; + /** RegularExpression Id. */ + int EMPTY = 43; + /** RegularExpression Id. */ + int INSTANCEOF = 44; + /** RegularExpression Id. */ + int MULT = 45; + /** RegularExpression Id. */ + int PLUS = 46; + /** RegularExpression Id. */ + int MINUS = 47; + /** RegularExpression Id. */ + int QUESTIONMARK = 48; + /** RegularExpression Id. */ + int DIV0 = 49; + /** RegularExpression Id. */ + int DIV1 = 50; + /** RegularExpression Id. */ + int MOD0 = 51; + /** RegularExpression Id. */ + int MOD1 = 52; + /** RegularExpression Id. */ + int CONCAT = 53; + /** RegularExpression Id. */ + int ASSIGN = 54; + /** RegularExpression Id. */ + int ARROW = 55; + /** RegularExpression Id. */ + int IDENTIFIER = 56; + /** RegularExpression Id. */ + int FUNCTIONSUFFIX = 57; + /** RegularExpression Id. */ + int IMPL_OBJ_START = 58; + /** RegularExpression Id. */ + int LETTER = 59; + /** RegularExpression Id. */ + int DIGIT = 60; + /** RegularExpression Id. */ + int ILLEGAL_CHARACTER = 61; + + /** Lexical state. */ + int DEFAULT = 0; + /** Lexical state. */ + int IN_EXPRESSION = 1; + /** Lexical state. */ + int IN_SET_OR_MAP = 2; + + /** Literal token values. */ + String[] tokenImage = { + "", + "", + "\"${\"", + "\"#{\"", + "\" \"", + "\"\\t\"", + "\"\\n\"", + "\"\\r\"", + "\"{\"", + "\"}\"", + "", + "", + "", + "", + "\"true\"", + "\"false\"", + "\"null\"", + "\".\"", + "\"(\"", + "\")\"", + "\"[\"", + "\"]\"", + "\":\"", + "\";\"", + "\",\"", + "\">\"", + "\"gt\"", + "\"<\"", + "\"lt\"", + "\">=\"", + "\"ge\"", + "\"<=\"", + "\"le\"", + "\"==\"", + "\"eq\"", + "\"!=\"", + "\"ne\"", + "\"!\"", + "\"not\"", + "\"&&\"", + "\"and\"", + "\"||\"", + "\"or\"", + "\"empty\"", + "\"instanceof\"", + "\"*\"", + "\"+\"", + "\"-\"", + "\"?\"", + "\"/\"", + "\"div\"", + "\"%\"", + "\"mod\"", + "\"+=\"", + "\"=\"", + "\"->\"", + "", + "", + "\"#\"", + "", + "", + "", + }; + +} diff --git a/java/org/apache/el/parser/ELParserTokenManager.java b/java/org/apache/el/parser/ELParserTokenManager.java new file mode 100644 index 0000000..4c4279d --- /dev/null +++ b/java/org/apache/el/parser/ELParserTokenManager.java @@ -0,0 +1,2339 @@ +/* ELParserTokenManager.java */ +/* Generated By:JJTree&JavaCC: Do not edit this line. ELParserTokenManager.java */ +package org.apache.el.parser; + +/** Token Manager. */ +@SuppressWarnings("all") // Ignore warnings in generated code +public class ELParserTokenManager implements ELParserConstants { +java.util.Deque deque = new java.util.ArrayDeque(); + + /** Debug output. */ + public java.io.PrintStream debugStream = System.out; + /** Set debug output. */ + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0){ + switch (pos) + { + case 0: + if ((active0 & 0xcL) != 0L) + { + jjmatchedKind = 1; + return 5; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0){ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private int jjMoveStringLiteralDfa0_0(){ + switch(curChar) + { + case 35: + return jjMoveStringLiteralDfa1_0(0x8L); + case 36: + return jjMoveStringLiteralDfa1_0(0x4L); + default : + return jjMoveNfa_0(7, 0); + } +} +private int jjMoveStringLiteralDfa1_0(long active0){ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(0, active0); + return 1; + } + switch(curChar) + { + case 123: + if ((active0 & 0x4L) != 0L) { + return jjStopAtPos(1, 2); + } else if ((active0 & 0x8L) != 0L) { + return jjStopAtPos(1, 3); + } + break; + default : + break; + } + return jjStartNfa_0(0, active0); +} +static final long[] jjbitVec0 = { + 0xfffffffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec2 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private int jjMoveNfa_0(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 8; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) { + ReInitRounds(); + } + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 7: + if ((0xffffffe7ffffffffL & l) != 0L) + { + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(0, 4); } + } + else if ((0x1800000000L & l) != 0L) + { + if (kind > 1) { + kind = 1; + } + { jjCheckNAdd(5); } + } + if ((0xffffffe7ffffffffL & l) != 0L) + { jjCheckNAddTwoStates(0, 1); } + break; + case 0: + if ((0xffffffe7ffffffffL & l) != 0L) + { jjCheckNAddTwoStates(0, 1); } + break; + case 2: + if ((0xffffffe7ffffffffL & l) == 0L) { + break; + } + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(0, 4); } + break; + case 3: + if ((0xffffffe7ffffffffL & l) != 0L) + { jjCheckNAddTwoStates(3, 4); } + break; + case 4: + if ((0x1800000000L & l) != 0L) + { jjCheckNAdd(5); } + break; + case 5: + if ((0xffffffe7ffffffffL & l) == 0L) { + break; + } + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(5, 8); } + break; + case 6: + if ((0x1800000000L & l) == 0L) { + break; + } + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(9, 13); } + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 7: + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(0, 4); } + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddTwoStates(0, 1); } + else if (curChar == 92) + { + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(14, 17); } + } + break; + case 0: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddTwoStates(0, 1); } + break; + case 1: + if (curChar != 92) { + break; + } + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(14, 17); } + break; + case 2: + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(0, 4); } + break; + case 3: + { jjCheckNAddTwoStates(3, 4); } + break; + case 5: + if ((0xf7ffffffefffffffL & l) == 0L) { + break; + } + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(5, 8); } + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 7: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjCheckNAddTwoStates(0, 1); } + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(0, 4); } + } + break; + case 0: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjCheckNAddTwoStates(0, 1); } + break; + case 2: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) { + break; + } + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(0, 4); } + break; + case 3: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjCheckNAddTwoStates(3, 4); } + break; + case 5: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) { + break; + } + if (kind > 1) { + kind = 1; + } + { jjCheckNAddStates(5, 8); } + break; + default : if (i1 == 0 || l1 == 0 || i2 == 0 || l2 == 0) { + break; + } else { + break; + } + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 8 - (jjnewStateCnt = startsAt))) { + return curPos; + } + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0){ + switch (pos) + { + case 0: + if ((active0 & 0x20000L) != 0L) { + return 1; + } + if ((active0 & 0x141d555401c000L) != 0L) + { + jjmatchedKind = 56; + return 30; + } + return -1; + case 1: + if ((active0 & 0x41554000000L) != 0L) { + return 30; + } + if ((active0 & 0x1419400001c000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 1; + return 30; + } + return -1; + case 2: + if ((active0 & 0x14014000000000L) != 0L) { + return 30; + } + if ((active0 & 0x18000001c000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 2; + return 30; + } + return -1; + case 3: + if ((active0 & 0x14000L) != 0L) { + return 30; + } + if ((active0 & 0x180000008000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 3; + return 30; + } + return -1; + case 4: + if ((active0 & 0x80000008000L) != 0L) { + return 30; + } + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 4; + return 30; + } + return -1; + case 5: + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 5; + return 30; + } + return -1; + case 6: + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 6; + return 30; + } + return -1; + case 7: + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 7; + return 30; + } + return -1; + case 8: + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 8; + return 30; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0){ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private int jjMoveStringLiteralDfa0_2(){ + switch(curChar) + { + case 33: + jjmatchedKind = 37; + return jjMoveStringLiteralDfa1_2(0x800000000L); + case 37: + return jjStopAtPos(0, 51); + case 38: + return jjMoveStringLiteralDfa1_2(0x8000000000L); + case 40: + return jjStopAtPos(0, 18); + case 41: + return jjStopAtPos(0, 19); + case 42: + return jjStopAtPos(0, 45); + case 43: + jjmatchedKind = 46; + return jjMoveStringLiteralDfa1_2(0x20000000000000L); + case 44: + return jjStopAtPos(0, 24); + case 45: + jjmatchedKind = 47; + return jjMoveStringLiteralDfa1_2(0x80000000000000L); + case 46: + return jjStartNfaWithStates_2(0, 17, 1); + case 47: + return jjStopAtPos(0, 49); + case 58: + return jjStopAtPos(0, 22); + case 59: + return jjStopAtPos(0, 23); + case 60: + jjmatchedKind = 27; + return jjMoveStringLiteralDfa1_2(0x80000000L); + case 61: + jjmatchedKind = 54; + return jjMoveStringLiteralDfa1_2(0x200000000L); + case 62: + jjmatchedKind = 25; + return jjMoveStringLiteralDfa1_2(0x20000000L); + case 63: + return jjStopAtPos(0, 48); + case 91: + return jjStopAtPos(0, 20); + case 93: + return jjStopAtPos(0, 21); + case 97: + return jjMoveStringLiteralDfa1_2(0x10000000000L); + case 100: + return jjMoveStringLiteralDfa1_2(0x4000000000000L); + case 101: + return jjMoveStringLiteralDfa1_2(0x80400000000L); + case 102: + return jjMoveStringLiteralDfa1_2(0x8000L); + case 103: + return jjMoveStringLiteralDfa1_2(0x44000000L); + case 105: + return jjMoveStringLiteralDfa1_2(0x100000000000L); + case 108: + return jjMoveStringLiteralDfa1_2(0x110000000L); + case 109: + return jjMoveStringLiteralDfa1_2(0x10000000000000L); + case 110: + return jjMoveStringLiteralDfa1_2(0x5000010000L); + case 111: + return jjMoveStringLiteralDfa1_2(0x40000000000L); + case 116: + return jjMoveStringLiteralDfa1_2(0x4000L); + case 123: + return jjStopAtPos(0, 8); + case 124: + return jjMoveStringLiteralDfa1_2(0x20000000000L); + case 125: + return jjStopAtPos(0, 9); + default : + return jjMoveNfa_2(0, 0); + } +} +private int jjMoveStringLiteralDfa1_2(long active0){ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(0, active0); + return 1; + } + switch(curChar) + { + case 38: + if ((active0 & 0x8000000000L) != 0L) { + return jjStopAtPos(1, 39); + } + break; + case 61: + if ((active0 & 0x20000000L) != 0L) { + return jjStopAtPos(1, 29); + } else if ((active0 & 0x80000000L) != 0L) { + return jjStopAtPos(1, 31); + } else if ((active0 & 0x200000000L) != 0L) { + return jjStopAtPos(1, 33); + } else if ((active0 & 0x800000000L) != 0L) { + return jjStopAtPos(1, 35); + } else if ((active0 & 0x20000000000000L) != 0L) { + return jjStopAtPos(1, 53); + } + break; + case 62: + if ((active0 & 0x80000000000000L) != 0L) { + return jjStopAtPos(1, 55); + } + break; + case 97: + return jjMoveStringLiteralDfa2_2(active0, 0x8000L); + case 101: + if ((active0 & 0x40000000L) != 0L) { + return jjStartNfaWithStates_2(1, 30, 30); + } else if ((active0 & 0x100000000L) != 0L) { + return jjStartNfaWithStates_2(1, 32, 30); + } else if ((active0 & 0x1000000000L) != 0L) { + return jjStartNfaWithStates_2(1, 36, 30); + } + break; + case 105: + return jjMoveStringLiteralDfa2_2(active0, 0x4000000000000L); + case 109: + return jjMoveStringLiteralDfa2_2(active0, 0x80000000000L); + case 110: + return jjMoveStringLiteralDfa2_2(active0, 0x110000000000L); + case 111: + return jjMoveStringLiteralDfa2_2(active0, 0x10004000000000L); + case 113: + if ((active0 & 0x400000000L) != 0L) { + return jjStartNfaWithStates_2(1, 34, 30); + } + break; + case 114: + if ((active0 & 0x40000000000L) != 0L) { + return jjStartNfaWithStates_2(1, 42, 30); + } + return jjMoveStringLiteralDfa2_2(active0, 0x4000L); + case 116: + if ((active0 & 0x4000000L) != 0L) { + return jjStartNfaWithStates_2(1, 26, 30); + } else if ((active0 & 0x10000000L) != 0L) { + return jjStartNfaWithStates_2(1, 28, 30); + } + break; + case 117: + return jjMoveStringLiteralDfa2_2(active0, 0x10000L); + case 124: + if ((active0 & 0x20000000000L) != 0L) { + return jjStopAtPos(1, 41); + } + break; + default : + break; + } + return jjStartNfa_2(0, active0); +} +private int jjMoveStringLiteralDfa2_2(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_2(0, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(1, active0); + return 2; + } + switch(curChar) + { + case 100: + if ((active0 & 0x10000000000L) != 0L) { + return jjStartNfaWithStates_2(2, 40, 30); + } else if ((active0 & 0x10000000000000L) != 0L) { + return jjStartNfaWithStates_2(2, 52, 30); + } + break; + case 108: + return jjMoveStringLiteralDfa3_2(active0, 0x18000L); + case 112: + return jjMoveStringLiteralDfa3_2(active0, 0x80000000000L); + case 115: + return jjMoveStringLiteralDfa3_2(active0, 0x100000000000L); + case 116: + if ((active0 & 0x4000000000L) != 0L) { + return jjStartNfaWithStates_2(2, 38, 30); + } + break; + case 117: + return jjMoveStringLiteralDfa3_2(active0, 0x4000L); + case 118: + if ((active0 & 0x4000000000000L) != 0L) { + return jjStartNfaWithStates_2(2, 50, 30); + } + break; + default : + break; + } + return jjStartNfa_2(1, active0); +} +private int jjMoveStringLiteralDfa3_2(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_2(1, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(2, active0); + return 3; + } + switch(curChar) + { + case 101: + if ((active0 & 0x4000L) != 0L) { + return jjStartNfaWithStates_2(3, 14, 30); + } + break; + case 108: + if ((active0 & 0x10000L) != 0L) { + return jjStartNfaWithStates_2(3, 16, 30); + } + break; + case 115: + return jjMoveStringLiteralDfa4_2(active0, 0x8000L); + case 116: + return jjMoveStringLiteralDfa4_2(active0, 0x180000000000L); + default : + break; + } + return jjStartNfa_2(2, active0); +} +private int jjMoveStringLiteralDfa4_2(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_2(2, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(3, active0); + return 4; + } + switch(curChar) + { + case 97: + return jjMoveStringLiteralDfa5_2(active0, 0x100000000000L); + case 101: + if ((active0 & 0x8000L) != 0L) { + return jjStartNfaWithStates_2(4, 15, 30); + } + break; + case 121: + if ((active0 & 0x80000000000L) != 0L) { + return jjStartNfaWithStates_2(4, 43, 30); + } + break; + default : + break; + } + return jjStartNfa_2(3, active0); +} +private int jjMoveStringLiteralDfa5_2(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_2(3, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(4, active0); + return 5; + } + switch(curChar) + { + case 110: + return jjMoveStringLiteralDfa6_2(active0, 0x100000000000L); + default : + break; + } + return jjStartNfa_2(4, active0); +} +private int jjMoveStringLiteralDfa6_2(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_2(4, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(5, active0); + return 6; + } + switch(curChar) + { + case 99: + return jjMoveStringLiteralDfa7_2(active0, 0x100000000000L); + default : + break; + } + return jjStartNfa_2(5, active0); +} +private int jjMoveStringLiteralDfa7_2(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_2(5, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(6, active0); + return 7; + } + switch(curChar) + { + case 101: + return jjMoveStringLiteralDfa8_2(active0, 0x100000000000L); + default : + break; + } + return jjStartNfa_2(6, active0); +} +private int jjMoveStringLiteralDfa8_2(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_2(6, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(7, active0); + return 8; + } + switch(curChar) + { + case 111: + return jjMoveStringLiteralDfa9_2(active0, 0x100000000000L); + default : + break; + } + return jjStartNfa_2(7, active0); +} +private int jjMoveStringLiteralDfa9_2(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_2(7, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(8, active0); + return 9; + } + switch(curChar) + { + case 102: + if ((active0 & 0x100000000000L) != 0L) { + return jjStartNfaWithStates_2(9, 44, 30); + } + break; + default : + break; + } + return jjStartNfa_2(8, active0); +} +private int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +static final long[] jjbitVec3 = { + 0x1ff00000fffffffeL, 0xffffffffffffc000L, 0xffffffffL, 0x600000000000000L +}; +static final long[] jjbitVec4 = { + 0x0L, 0x0L, 0x0L, 0xff7fffffff7fffffL +}; +static final long[] jjbitVec5 = { + 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec6 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffL, 0x0L +}; +static final long[] jjbitVec7 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0x0L, 0x0L +}; +static final long[] jjbitVec8 = { + 0x3fffffffffffL, 0x0L, 0x0L, 0x0L +}; +private int jjMoveNfa_2(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 30; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) { + ReInitRounds(); + } + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 10) { + kind = 10; + } + { jjCheckNAddStates(18, 22); } + } + else if ((0x1800000000L & l) != 0L) + { + if (kind > 56) { + kind = 56; + } + { jjCheckNAddTwoStates(28, 29); } + } + else if (curChar == 39) + { jjCheckNAddStates(23, 25); } + else if (curChar == 34) + { jjCheckNAddStates(26, 28); } + else if (curChar == 46) + { jjCheckNAdd(1); } + break; + case 30: + if ((0x3ff001000000000L & l) != 0L) + { + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + } + if ((0x3ff001000000000L & l) != 0L) + { + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + } + break; + case 1: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAddTwoStates(1, 2); } + break; + case 3: + if ((0x280000000000L & l) != 0L) + { jjCheckNAdd(4); } + break; + case 4: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAdd(4); } + break; + case 5: + if (curChar == 34) + { jjCheckNAddStates(26, 28); } + break; + case 6: + if ((0xfffffffbffffffffL & l) != 0L) + { jjCheckNAddStates(26, 28); } + break; + case 8: + if ((0x8400000000L & l) != 0L) + { jjCheckNAddStates(26, 28); } + break; + case 9: + if (curChar == 34 && kind > 13) { + kind = 13; + } + break; + case 10: + if (curChar == 39) + { jjCheckNAddStates(23, 25); } + break; + case 11: + if ((0xffffff7fffffffffL & l) != 0L) + { jjCheckNAddStates(23, 25); } + break; + case 13: + if ((0x8400000000L & l) != 0L) + { jjCheckNAddStates(23, 25); } + break; + case 14: + if (curChar == 39 && kind > 13) { + kind = 13; + } + break; + case 15: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 10) { + kind = 10; + } + { jjCheckNAddStates(18, 22); } + break; + case 16: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 10) { + kind = 10; + } + { jjCheckNAdd(16); } + break; + case 17: + if ((0x3ff000000000000L & l) != 0L) + { jjCheckNAddTwoStates(17, 18); } + break; + case 18: + if (curChar != 46) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAddTwoStates(19, 20); } + break; + case 19: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAddTwoStates(19, 20); } + break; + case 21: + if ((0x280000000000L & l) != 0L) + { jjCheckNAdd(22); } + break; + case 22: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAdd(22); } + break; + case 23: + if ((0x3ff000000000000L & l) != 0L) + { jjCheckNAddTwoStates(23, 24); } + break; + case 25: + if ((0x280000000000L & l) != 0L) + { jjCheckNAdd(26); } + break; + case 26: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAdd(26); } + break; + case 27: + if ((0x1800000000L & l) == 0L) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAddTwoStates(28, 29); } + break; + case 28: + if ((0x3ff001000000000L & l) == 0L) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + break; + case 29: + if ((0x3ff001000000000L & l) == 0L) { + break; + } + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x7fffffe87fffffeL & l) == 0L) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAddTwoStates(28, 29); } + break; + case 30: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + } + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + } + break; + case 2: + if ((0x2000000020L & l) != 0L) + { jjAddStates(29, 30); } + break; + case 6: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddStates(26, 28); } + break; + case 7: + if (curChar == 92) { + jjstateSet[jjnewStateCnt++] = 8; + } + break; + case 8: + if (curChar == 92) + { jjCheckNAddStates(26, 28); } + break; + case 11: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddStates(23, 25); } + break; + case 12: + if (curChar == 92) { + jjstateSet[jjnewStateCnt++] = 13; + } + break; + case 13: + if (curChar == 92) + { jjCheckNAddStates(23, 25); } + break; + case 20: + if ((0x2000000020L & l) != 0L) + { jjAddStates(31, 32); } + break; + case 24: + if ((0x2000000020L & l) != 0L) + { jjAddStates(33, 34); } + break; + case 28: + if ((0x7fffffe87fffffeL & l) == 0L) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + break; + case 29: + if ((0x7fffffe87fffffeL & l) == 0L) { + break; + } + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAddTwoStates(28, 29); } + break; + case 30: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + } + break; + case 6: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjAddStates(26, 28); } + break; + case 11: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjAddStates(23, 25); } + break; + case 28: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + break; + case 29: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) { + break; + } + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + break; + default : if (i1 == 0 || l1 == 0 || i2 == 0 || l2 == 0) { + break; + } else { + break; + } + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 30 - (jjnewStateCnt = startsAt))) { + return curPos; + } + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0){ + switch (pos) + { + case 0: + if ((active0 & 0x20000L) != 0L) { + return 1; + } + if ((active0 & 0x141d555401c000L) != 0L) + { + jjmatchedKind = 56; + return 30; + } + return -1; + case 1: + if ((active0 & 0x41554000000L) != 0L) { + return 30; + } + if ((active0 & 0x1419400001c000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 1; + return 30; + } + return -1; + case 2: + if ((active0 & 0x14014000000000L) != 0L) { + return 30; + } + if ((active0 & 0x18000001c000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 2; + return 30; + } + return -1; + case 3: + if ((active0 & 0x14000L) != 0L) { + return 30; + } + if ((active0 & 0x180000008000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 3; + return 30; + } + return -1; + case 4: + if ((active0 & 0x80000008000L) != 0L) { + return 30; + } + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 4; + return 30; + } + return -1; + case 5: + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 5; + return 30; + } + return -1; + case 6: + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 6; + return 30; + } + return -1; + case 7: + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 7; + return 30; + } + return -1; + case 8: + if ((active0 & 0x100000000000L) != 0L) + { + jjmatchedKind = 56; + jjmatchedPos = 8; + return 30; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0){ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private int jjMoveStringLiteralDfa0_1(){ + switch(curChar) + { + case 33: + jjmatchedKind = 37; + return jjMoveStringLiteralDfa1_1(0x800000000L); + case 37: + return jjStopAtPos(0, 51); + case 38: + return jjMoveStringLiteralDfa1_1(0x8000000000L); + case 40: + return jjStopAtPos(0, 18); + case 41: + return jjStopAtPos(0, 19); + case 42: + return jjStopAtPos(0, 45); + case 43: + jjmatchedKind = 46; + return jjMoveStringLiteralDfa1_1(0x20000000000000L); + case 44: + return jjStopAtPos(0, 24); + case 45: + jjmatchedKind = 47; + return jjMoveStringLiteralDfa1_1(0x80000000000000L); + case 46: + return jjStartNfaWithStates_1(0, 17, 1); + case 47: + return jjStopAtPos(0, 49); + case 58: + return jjStopAtPos(0, 22); + case 59: + return jjStopAtPos(0, 23); + case 60: + jjmatchedKind = 27; + return jjMoveStringLiteralDfa1_1(0x80000000L); + case 61: + jjmatchedKind = 54; + return jjMoveStringLiteralDfa1_1(0x200000000L); + case 62: + jjmatchedKind = 25; + return jjMoveStringLiteralDfa1_1(0x20000000L); + case 63: + return jjStopAtPos(0, 48); + case 91: + return jjStopAtPos(0, 20); + case 93: + return jjStopAtPos(0, 21); + case 97: + return jjMoveStringLiteralDfa1_1(0x10000000000L); + case 100: + return jjMoveStringLiteralDfa1_1(0x4000000000000L); + case 101: + return jjMoveStringLiteralDfa1_1(0x80400000000L); + case 102: + return jjMoveStringLiteralDfa1_1(0x8000L); + case 103: + return jjMoveStringLiteralDfa1_1(0x44000000L); + case 105: + return jjMoveStringLiteralDfa1_1(0x100000000000L); + case 108: + return jjMoveStringLiteralDfa1_1(0x110000000L); + case 109: + return jjMoveStringLiteralDfa1_1(0x10000000000000L); + case 110: + return jjMoveStringLiteralDfa1_1(0x5000010000L); + case 111: + return jjMoveStringLiteralDfa1_1(0x40000000000L); + case 116: + return jjMoveStringLiteralDfa1_1(0x4000L); + case 123: + return jjStopAtPos(0, 8); + case 124: + return jjMoveStringLiteralDfa1_1(0x20000000000L); + case 125: + return jjStopAtPos(0, 9); + default : + return jjMoveNfa_1(0, 0); + } +} +private int jjMoveStringLiteralDfa1_1(long active0){ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(0, active0); + return 1; + } + switch(curChar) + { + case 38: + if ((active0 & 0x8000000000L) != 0L) { + return jjStopAtPos(1, 39); + } + break; + case 61: + if ((active0 & 0x20000000L) != 0L) { + return jjStopAtPos(1, 29); + } else if ((active0 & 0x80000000L) != 0L) { + return jjStopAtPos(1, 31); + } else if ((active0 & 0x200000000L) != 0L) { + return jjStopAtPos(1, 33); + } else if ((active0 & 0x800000000L) != 0L) { + return jjStopAtPos(1, 35); + } else if ((active0 & 0x20000000000000L) != 0L) { + return jjStopAtPos(1, 53); + } + break; + case 62: + if ((active0 & 0x80000000000000L) != 0L) { + return jjStopAtPos(1, 55); + } + break; + case 97: + return jjMoveStringLiteralDfa2_1(active0, 0x8000L); + case 101: + if ((active0 & 0x40000000L) != 0L) { + return jjStartNfaWithStates_1(1, 30, 30); + } else if ((active0 & 0x100000000L) != 0L) { + return jjStartNfaWithStates_1(1, 32, 30); + } else if ((active0 & 0x1000000000L) != 0L) { + return jjStartNfaWithStates_1(1, 36, 30); + } + break; + case 105: + return jjMoveStringLiteralDfa2_1(active0, 0x4000000000000L); + case 109: + return jjMoveStringLiteralDfa2_1(active0, 0x80000000000L); + case 110: + return jjMoveStringLiteralDfa2_1(active0, 0x110000000000L); + case 111: + return jjMoveStringLiteralDfa2_1(active0, 0x10004000000000L); + case 113: + if ((active0 & 0x400000000L) != 0L) { + return jjStartNfaWithStates_1(1, 34, 30); + } + break; + case 114: + if ((active0 & 0x40000000000L) != 0L) { + return jjStartNfaWithStates_1(1, 42, 30); + } + return jjMoveStringLiteralDfa2_1(active0, 0x4000L); + case 116: + if ((active0 & 0x4000000L) != 0L) { + return jjStartNfaWithStates_1(1, 26, 30); + } else if ((active0 & 0x10000000L) != 0L) { + return jjStartNfaWithStates_1(1, 28, 30); + } + break; + case 117: + return jjMoveStringLiteralDfa2_1(active0, 0x10000L); + case 124: + if ((active0 & 0x20000000000L) != 0L) { + return jjStopAtPos(1, 41); + } + break; + default : + break; + } + return jjStartNfa_1(0, active0); +} +private int jjMoveStringLiteralDfa2_1(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_1(0, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(1, active0); + return 2; + } + switch(curChar) + { + case 100: + if ((active0 & 0x10000000000L) != 0L) { + return jjStartNfaWithStates_1(2, 40, 30); + } else if ((active0 & 0x10000000000000L) != 0L) { + return jjStartNfaWithStates_1(2, 52, 30); + } + break; + case 108: + return jjMoveStringLiteralDfa3_1(active0, 0x18000L); + case 112: + return jjMoveStringLiteralDfa3_1(active0, 0x80000000000L); + case 115: + return jjMoveStringLiteralDfa3_1(active0, 0x100000000000L); + case 116: + if ((active0 & 0x4000000000L) != 0L) { + return jjStartNfaWithStates_1(2, 38, 30); + } + break; + case 117: + return jjMoveStringLiteralDfa3_1(active0, 0x4000L); + case 118: + if ((active0 & 0x4000000000000L) != 0L) { + return jjStartNfaWithStates_1(2, 50, 30); + } + break; + default : + break; + } + return jjStartNfa_1(1, active0); +} +private int jjMoveStringLiteralDfa3_1(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_1(1, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(2, active0); + return 3; + } + switch(curChar) + { + case 101: + if ((active0 & 0x4000L) != 0L) { + return jjStartNfaWithStates_1(3, 14, 30); + } + break; + case 108: + if ((active0 & 0x10000L) != 0L) { + return jjStartNfaWithStates_1(3, 16, 30); + } + break; + case 115: + return jjMoveStringLiteralDfa4_1(active0, 0x8000L); + case 116: + return jjMoveStringLiteralDfa4_1(active0, 0x180000000000L); + default : + break; + } + return jjStartNfa_1(2, active0); +} +private int jjMoveStringLiteralDfa4_1(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_1(2, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(3, active0); + return 4; + } + switch(curChar) + { + case 97: + return jjMoveStringLiteralDfa5_1(active0, 0x100000000000L); + case 101: + if ((active0 & 0x8000L) != 0L) { + return jjStartNfaWithStates_1(4, 15, 30); + } + break; + case 121: + if ((active0 & 0x80000000000L) != 0L) { + return jjStartNfaWithStates_1(4, 43, 30); + } + break; + default : + break; + } + return jjStartNfa_1(3, active0); +} +private int jjMoveStringLiteralDfa5_1(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_1(3, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(4, active0); + return 5; + } + switch(curChar) + { + case 110: + return jjMoveStringLiteralDfa6_1(active0, 0x100000000000L); + default : + break; + } + return jjStartNfa_1(4, active0); +} +private int jjMoveStringLiteralDfa6_1(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_1(4, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(5, active0); + return 6; + } + switch(curChar) + { + case 99: + return jjMoveStringLiteralDfa7_1(active0, 0x100000000000L); + default : + break; + } + return jjStartNfa_1(5, active0); +} +private int jjMoveStringLiteralDfa7_1(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_1(5, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(6, active0); + return 7; + } + switch(curChar) + { + case 101: + return jjMoveStringLiteralDfa8_1(active0, 0x100000000000L); + default : + break; + } + return jjStartNfa_1(6, active0); +} +private int jjMoveStringLiteralDfa8_1(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_1(6, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(7, active0); + return 8; + } + switch(curChar) + { + case 111: + return jjMoveStringLiteralDfa9_1(active0, 0x100000000000L); + default : + break; + } + return jjStartNfa_1(7, active0); +} +private int jjMoveStringLiteralDfa9_1(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_1(7, old0); +} + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(8, active0); + return 9; + } + switch(curChar) + { + case 102: + if ((active0 & 0x100000000000L) != 0L) { + return jjStartNfaWithStates_1(9, 44, 30); + } + break; + default : + break; + } + return jjStartNfa_1(8, active0); +} +private int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private int jjMoveNfa_1(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 30; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) { + ReInitRounds(); + } + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 10) { + kind = 10; + } + { jjCheckNAddStates(18, 22); } + } + else if ((0x1800000000L & l) != 0L) + { + if (kind > 56) { + kind = 56; + } + { jjCheckNAddTwoStates(28, 29); } + } + else if (curChar == 39) + { jjCheckNAddStates(23, 25); } + else if (curChar == 34) + { jjCheckNAddStates(26, 28); } + else if (curChar == 46) + { jjCheckNAdd(1); } + break; + case 30: + if ((0x3ff001000000000L & l) != 0L) + { + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + } + if ((0x3ff001000000000L & l) != 0L) + { + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + } + break; + case 1: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAddTwoStates(1, 2); } + break; + case 3: + if ((0x280000000000L & l) != 0L) + { jjCheckNAdd(4); } + break; + case 4: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAdd(4); } + break; + case 5: + if (curChar == 34) + { jjCheckNAddStates(26, 28); } + break; + case 6: + if ((0xfffffffbffffffffL & l) != 0L) + { jjCheckNAddStates(26, 28); } + break; + case 8: + if ((0x8400000000L & l) != 0L) + { jjCheckNAddStates(26, 28); } + break; + case 9: + if (curChar == 34 && kind > 13) { + kind = 13; + } + break; + case 10: + if (curChar == 39) + { jjCheckNAddStates(23, 25); } + break; + case 11: + if ((0xffffff7fffffffffL & l) != 0L) + { jjCheckNAddStates(23, 25); } + break; + case 13: + if ((0x8400000000L & l) != 0L) + { jjCheckNAddStates(23, 25); } + break; + case 14: + if (curChar == 39 && kind > 13) { + kind = 13; + } + break; + case 15: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 10) { + kind = 10; + } + { jjCheckNAddStates(18, 22); } + break; + case 16: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 10) { + kind = 10; + } + { jjCheckNAdd(16); } + break; + case 17: + if ((0x3ff000000000000L & l) != 0L) + { jjCheckNAddTwoStates(17, 18); } + break; + case 18: + if (curChar != 46) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAddTwoStates(19, 20); } + break; + case 19: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAddTwoStates(19, 20); } + break; + case 21: + if ((0x280000000000L & l) != 0L) + { jjCheckNAdd(22); } + break; + case 22: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAdd(22); } + break; + case 23: + if ((0x3ff000000000000L & l) != 0L) + { jjCheckNAddTwoStates(23, 24); } + break; + case 25: + if ((0x280000000000L & l) != 0L) + { jjCheckNAdd(26); } + break; + case 26: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 11) { + kind = 11; + } + { jjCheckNAdd(26); } + break; + case 27: + if ((0x1800000000L & l) == 0L) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAddTwoStates(28, 29); } + break; + case 28: + if ((0x3ff001000000000L & l) == 0L) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + break; + case 29: + if ((0x3ff001000000000L & l) == 0L) { + break; + } + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x7fffffe87fffffeL & l) == 0L) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAddTwoStates(28, 29); } + break; + case 30: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + } + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + } + break; + case 2: + if ((0x2000000020L & l) != 0L) + { jjAddStates(29, 30); } + break; + case 6: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddStates(26, 28); } + break; + case 7: + if (curChar == 92) { + jjstateSet[jjnewStateCnt++] = 8; + } + break; + case 8: + if (curChar == 92) + { jjCheckNAddStates(26, 28); } + break; + case 11: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddStates(23, 25); } + break; + case 12: + if (curChar == 92) { + jjstateSet[jjnewStateCnt++] = 13; + } + break; + case 13: + if (curChar == 92) + { jjCheckNAddStates(23, 25); } + break; + case 20: + if ((0x2000000020L & l) != 0L) + { jjAddStates(31, 32); } + break; + case 24: + if ((0x2000000020L & l) != 0L) + { jjAddStates(33, 34); } + break; + case 28: + if ((0x7fffffe87fffffeL & l) == 0L) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + break; + case 29: + if ((0x7fffffe87fffffeL & l) == 0L) { + break; + } + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAddTwoStates(28, 29); } + break; + case 30: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + } + break; + case 6: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjAddStates(26, 28); } + break; + case 11: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjAddStates(23, 25); } + break; + case 28: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) { + break; + } + if (kind > 56) { + kind = 56; + } + { jjCheckNAdd(28); } + break; + case 29: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) { + break; + } + if (kind > 57) { + kind = 57; + } + { jjCheckNAdd(29); } + break; + default : if (i1 == 0 || l1 == 0 || i2 == 0 || l2 == 0) { + break; + } else { + break; + } + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 30 - (jjnewStateCnt = startsAt))) { + return curPos; + } + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} + +/** Token literal values. */ +public static final String[] jjstrLiteralImages = { +"", null, "\44\173", "\43\173", null, null, null, null, "\173", "\175", null, +null, null, null, "\164\162\165\145", "\146\141\154\163\145", "\156\165\154\154", +"\56", "\50", "\51", "\133", "\135", "\72", "\73", "\54", "\76", "\147\164", "\74", +"\154\164", "\76\75", "\147\145", "\74\75", "\154\145", "\75\75", "\145\161", "\41\75", +"\156\145", "\41", "\156\157\164", "\46\46", "\141\156\144", "\174\174", "\157\162", +"\145\155\160\164\171", "\151\156\163\164\141\156\143\145\157\146", "\52", "\53", "\55", "\77", "\57", +"\144\151\166", "\45", "\155\157\144", "\53\75", "\75", "\55\76", null, null, null, null, null, +null, }; +protected Token jjFillToken() +{ + final Token t; + final String curTokenImage; + final int beginLine; + final int endLine; + final int beginColumn; + final int endColumn; + String im = jjstrLiteralImages[jjmatchedKind]; + curTokenImage = (im == null) ? input_stream.GetImage() : im; + beginLine = input_stream.getBeginLine(); + beginColumn = input_stream.getBeginColumn(); + endLine = input_stream.getEndLine(); + endColumn = input_stream.getEndColumn(); + t = Token.newToken(jjmatchedKind, curTokenImage); + + t.beginLine = beginLine; + t.endLine = endLine; + t.beginColumn = beginColumn; + t.endColumn = endColumn; + + return t; +} +static final int[] jjnextStates = { + 0, 1, 3, 4, 2, 0, 1, 4, 2, 0, 1, 4, 5, 2, 0, 1, + 2, 6, 16, 17, 18, 23, 24, 11, 12, 14, 6, 7, 9, 3, 4, 21, + 22, 25, 26, +}; +private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec2[i2] & l2) != 0L); + default : + if ((jjbitVec0[i1] & l1) != 0L) { + return true; + } + return false; + } +} +private static final boolean jjCanMove_1(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec4[i2] & l2) != 0L); + case 48: + return ((jjbitVec5[i2] & l2) != 0L); + case 49: + return ((jjbitVec6[i2] & l2) != 0L); + case 51: + return ((jjbitVec7[i2] & l2) != 0L); + case 61: + return ((jjbitVec8[i2] & l2) != 0L); + default : + if ((jjbitVec3[i1] & l1) != 0L) { + return true; + } + return false; + } +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +/** Get the next Token. */ +public Token getNextToken() +{ + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(Exception e) + { + jjmatchedKind = 0; + jjmatchedPos = -1; + matchedToken = jjFillToken(); + return matchedToken; + } + image = jjimage; + image.setLength(0); + jjimageLen = 0; + + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + try { input_stream.backup(0); + while (curChar <= 32 && (0x100002600L & (1L << curChar)) != 0L) { + curChar = input_stream.BeginToken(); + } + } + catch (java.io.IOException e1) { continue EOFLoop; } + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + if (jjmatchedPos == 0 && jjmatchedKind > 61) + { + jjmatchedKind = 61; + } + break; + case 2: + try { input_stream.backup(0); + while (curChar <= 32 && (0x100002600L & (1L << curChar)) != 0L) { + curChar = input_stream.BeginToken(); + } + } + catch (java.io.IOException e1) { continue EOFLoop; } + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + if (jjmatchedPos == 0 && jjmatchedKind > 61) + { + jjmatchedKind = 61; + } + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) { + input_stream.backup(curPos - jjmatchedPos - 1); + } + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + TokenLexicalActions(matchedToken); + if (jjnewLexState[jjmatchedKind] != -1) { + curLexState = jjnewLexState[jjmatchedKind]; + } + return matchedToken; + } + else + { + if (jjnewLexState[jjmatchedKind] != -1) { + curLexState = jjnewLexState[jjmatchedKind]; + } + continue EOFLoop; + } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } else { + error_column++; + } + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } +} + +void SkipLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + default : + break; + } +} +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + default : + break; + } +} +void TokenLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + case 2 : + image.append(jjstrLiteralImages[2]); + lengthOfMatch = jjstrLiteralImages[2].length(); + deque.push(DEFAULT); + break; + case 3 : + image.append(jjstrLiteralImages[3]); + lengthOfMatch = jjstrLiteralImages[3].length(); + deque.push(DEFAULT); + break; + case 8 : + image.append(jjstrLiteralImages[8]); + lengthOfMatch = jjstrLiteralImages[8].length(); + deque.push(curLexState); + break; + case 9 : + image.append(jjstrLiteralImages[9]); + lengthOfMatch = jjstrLiteralImages[9].length(); + SwitchTo(deque.pop()); + break; + default : + break; + } +} +private void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} + +private void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} + + /** Constructor. */ + public ELParserTokenManager(SimpleCharStream stream){ + + if (SimpleCharStream.staticFlag) { + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + } + + input_stream = stream; + } + + /** Constructor. */ + public ELParserTokenManager (SimpleCharStream stream, int lexState){ + ReInit(stream); + SwitchTo(lexState); + } + + /** Reinitialise parser. */ + + public void ReInit(SimpleCharStream stream) + { + + + jjmatchedPos = + jjnewStateCnt = + 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); + } + + private void ReInitRounds() + { + int i; + jjround = 0x80000001; + for (i = 30; i-- > 0;) { + jjrounds[i] = 0x80000000; + } + } + + /** Reinitialise parser. */ + public void ReInit(SimpleCharStream stream, int lexState) + + { + ReInit(stream); + SwitchTo(lexState); + } + + /** Switch to specified lex state. */ + public void SwitchTo(int lexState) + { + if (lexState >= 3 || lexState < 0) { + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + } else { + curLexState = lexState; + } + } + + +/** Lexer state names. */ +public static final String[] lexStateNames = { + "DEFAULT", + "IN_EXPRESSION", + "IN_SET_OR_MAP", +}; + +/** Lex State array. */ +public static final int[] jjnewLexState = { + -1, -1, 1, 1, -1, -1, -1, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; +static final long[] jjtoToken = { + 0x23ffffffffffef0fL, +}; +static final long[] jjtoSkip = { + 0xf0L, +}; +static final long[] jjtoSpecial = { + 0x0L, +}; +static final long[] jjtoMore = { + 0x0L, +}; + protected SimpleCharStream input_stream; + + private final int[] jjrounds = new int[30]; + private final int[] jjstateSet = new int[2 * 30]; + private final StringBuilder jjimage = new StringBuilder(); + private StringBuilder image = jjimage; + private int jjimageLen; + private int lengthOfMatch; + protected int curChar; +} diff --git a/java/org/apache/el/parser/ELParserTreeConstants.java b/java/org/apache/el/parser/ELParserTreeConstants.java new file mode 100644 index 0000000..c442896 --- /dev/null +++ b/java/org/apache/el/parser/ELParserTreeConstants.java @@ -0,0 +1,97 @@ +/* Generated By:JavaCC: Do not edit this line. ELParserTreeConstants.java Version 7.0.10 */ +package org.apache.el.parser; + +public interface ELParserTreeConstants +{ + int JJTCOMPOSITEEXPRESSION = 0; + int JJTLITERALEXPRESSION = 1; + int JJTDEFERREDEXPRESSION = 2; + int JJTDYNAMICEXPRESSION = 3; + int JJTVOID = 4; + int JJTSEMICOLON = 5; + int JJTASSIGN = 6; + int JJTLAMBDAEXPRESSION = 7; + int JJTLAMBDAPARAMETERS = 8; + int JJTCHOICE = 9; + int JJTOR = 10; + int JJTAND = 11; + int JJTEQUAL = 12; + int JJTNOTEQUAL = 13; + int JJTLESSTHAN = 14; + int JJTGREATERTHAN = 15; + int JJTLESSTHANEQUAL = 16; + int JJTGREATERTHANEQUAL = 17; + int JJTCONCATENATION = 18; + int JJTPLUS = 19; + int JJTMINUS = 20; + int JJTMULT = 21; + int JJTDIV = 22; + int JJTMOD = 23; + int JJTNEGATIVE = 24; + int JJTNOT = 25; + int JJTEMPTY = 26; + int JJTVALUE = 27; + int JJTDOTSUFFIX = 28; + int JJTBRACKETSUFFIX = 29; + int JJTMETHODPARAMETERS = 30; + int JJTSETDATA = 31; + int JJTLISTDATA = 32; + int JJTMAPDATA = 33; + int JJTMAPENTRY = 34; + int JJTIDENTIFIER = 35; + int JJTFUNCTION = 36; + int JJTTRUE = 37; + int JJTFALSE = 38; + int JJTFLOATINGPOINT = 39; + int JJTINTEGER = 40; + int JJTSTRING = 41; + int JJTNULL = 42; + + + String[] jjtNodeName = { + "CompositeExpression", + "LiteralExpression", + "DeferredExpression", + "DynamicExpression", + "void", + "Semicolon", + "Assign", + "LambdaExpression", + "LambdaParameters", + "Choice", + "Or", + "And", + "Equal", + "NotEqual", + "LessThan", + "GreaterThan", + "LessThanEqual", + "GreaterThanEqual", + "Concatenation", + "Plus", + "Minus", + "Mult", + "Div", + "Mod", + "Negative", + "Not", + "Empty", + "Value", + "DotSuffix", + "BracketSuffix", + "MethodParameters", + "SetData", + "ListData", + "MapData", + "MapEntry", + "Identifier", + "Function", + "True", + "False", + "FloatingPoint", + "Integer", + "String", + "Null", + }; +} +/* JavaCC - OriginalChecksum=4a0b3deb38bd21ee4df7ed00585cdbf6 (do not edit this line) */ diff --git a/java/org/apache/el/parser/JJTELParserState.java b/java/org/apache/el/parser/JJTELParserState.java new file mode 100644 index 0000000..8077c72 --- /dev/null +++ b/java/org/apache/el/parser/JJTELParserState.java @@ -0,0 +1,124 @@ +/* Generated By:JavaCC: Do not edit this line. JJTELParserState.java Version 7.0.10 */ +package org.apache.el.parser; + +@SuppressWarnings("all") // Ignore warnings in generated code +public class JJTELParserState { + private java.util.List nodes; + private java.util.List marks; + + private int sp; // number of nodes on stack + private int mk; // current mark + private boolean node_created; + + public JJTELParserState() { + nodes = new java.util.ArrayList(); + marks = new java.util.ArrayList(); + sp = 0; + mk = 0; + } + + /* Determines whether the current node was actually closed and + pushed. This should only be called in the final user action of a + node scope. */ + public boolean nodeCreated() { + return node_created; + } + + /* Call this to reinitialize the node stack. It is called + automatically by the parser's ReInit() method. */ + public void reset() { + nodes.clear(); + marks.clear(); + sp = 0; + mk = 0; + } + + /* Returns the root node of the AST. It only makes sense to call + this after a successful parse. */ + public Node rootNode() { + return nodes.get(0); + } + + /* Pushes a node on to the stack. */ + public void pushNode(Node n) { + nodes.add(n); + ++sp; + } + + /* Returns the node on the top of the stack, and remove it from the + stack. */ + public Node popNode() { + if (--sp < mk) { + mk = marks.remove(marks.size()-1); + } + return nodes.remove(nodes.size()-1); + } + + /* Returns the node currently on the top of the stack. */ + public Node peekNode() { + return nodes.get(nodes.size()-1); + } + + /* Returns the number of children on the stack in the current node + scope. */ + public int nodeArity() { + return sp - mk; + } + + + public void clearNodeScope(Node n) { + while (sp > mk) { + popNode(); + } + mk = marks.remove(marks.size()-1); + } + + + public void openNodeScope(Node n) { + marks.add(mk); + mk = sp; + n.jjtOpen(); + } + + + /* A definite node is constructed from a specified number of + children. That number of nodes are popped from the stack and + made the children of the definite node. Then the definite node + is pushed on to the stack. */ + public void closeNodeScope(Node n, int num) { + mk = marks.remove(marks.size()-1); + while (num-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, num); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } + + + /* A conditional node is constructed if its condition is true. All + the nodes that have been pushed since the node was opened are + made children of the conditional node, which is then pushed + on to the stack. If the condition is false the node is not + constructed and they are left on the stack. */ + public void closeNodeScope(Node n, boolean condition) { + if (condition) { + int a = nodeArity(); + mk = marks.remove(marks.size()-1); + while (a-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, a); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } else { + mk = marks.remove(marks.size()-1); + node_created = false; + } + } +} +/* JavaCC - OriginalChecksum=5f71fbb7f7a6d37c4c91e15a8243ab32 (do not edit this line) */ diff --git a/java/org/apache/el/parser/Node.java b/java/org/apache/el/parser/Node.java new file mode 100644 index 0000000..d7c559b --- /dev/null +++ b/java/org/apache/el/parser/Node.java @@ -0,0 +1,89 @@ +/* Generated By:JJTree: Do not edit this line. Node.java */ + +/* + * 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.el.parser; + +import jakarta.el.ELException; +import jakarta.el.MethodInfo; +import jakarta.el.MethodReference; +import jakarta.el.ValueReference; + +import org.apache.el.lang.EvaluationContext; + + +/* All AST nodes must implement this interface. It provides basic + machinery for constructing the parent and child relationships + between nodes. */ + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +@SuppressWarnings("all") // Ignore warnings in generated code +public interface Node { + + /** This method is called after the node has been made the current + node. It indicates that child nodes can now be added to it. */ + void jjtOpen(); + + /** This method is called after all the child nodes have been + added. */ + void jjtClose(); + + /** This pair of methods are used to inform the node of its + parent. */ + void jjtSetParent(Node n); + Node jjtGetParent(); + + /** This method tells the node to add its argument to the node's + list of children. */ + void jjtAddChild(Node n, int i); + + /** This method returns a child node. The children are numbered + from zero, left to right. */ + Node jjtGetChild(int i); + + /** Return the number of children the node has. */ + int jjtGetNumChildren(); + + String getImage(); + + Object getValue(EvaluationContext ctx) throws ELException; + void setValue(EvaluationContext ctx, Object value) throws ELException; + Class getType(EvaluationContext ctx) throws ELException; + boolean isReadOnly(EvaluationContext ctx) throws ELException; + void accept(NodeVisitor visitor) throws Exception; + MethodInfo getMethodInfo(EvaluationContext ctx, Class[] paramTypes) + throws ELException; + Object invoke(EvaluationContext ctx, Class[] paramTypes, + Object[] paramValues) throws ELException; + + /** + * @since EL 2.2 + */ + ValueReference getValueReference(EvaluationContext ctx); + + /** + * @since EL 2.2 + */ + boolean isParametersProvided(); + + /** + * @since EL 5.0 + */ + MethodReference getMethodReference(EvaluationContext ctx); +} diff --git a/java/org/apache/el/parser/NodeVisitor.java b/java/org/apache/el/parser/NodeVisitor.java new file mode 100644 index 0000000..52504ea --- /dev/null +++ b/java/org/apache/el/parser/NodeVisitor.java @@ -0,0 +1,24 @@ +/* + * 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.el.parser; + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public interface NodeVisitor { + void visit(Node node) throws Exception; +} diff --git a/java/org/apache/el/parser/ParseException.java b/java/org/apache/el/parser/ParseException.java new file mode 100644 index 0000000..8ee8fa0 --- /dev/null +++ b/java/org/apache/el/parser/ParseException.java @@ -0,0 +1,198 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 7.0 */ +/* JavaCCOptions:KEEP_LINE_COLUMN=true */ +package org.apache.el.parser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +@SuppressWarnings("all") // Ignore warnings in generated code +public class ParseException extends Exception { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * The end of line string for this machine. + */ + protected static String EOL = System.getProperty("line.separator", "\n"); + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(initialise(currentTokenVal, expectedTokenSequencesVal, tokenImageVal)); + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + } + + /** Constructor with message. */ + public ParseException(String message) { + super(message); + } + + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * following this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * It uses "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser) the correct error message + * gets displayed. + */ + private static String initialise(Token currentToken, + int[][] expectedTokenSequences, + String[] tokenImage) { + + StringBuilder expected = new StringBuilder(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(' '); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(EOL).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) { + retval += " "; + } + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += " " + tokenImage[tok.kind]; + retval += " \""; + retval += add_escapes(tok.image); + retval += " \""; + tok = tok.next; + } + if (currentToken.next != null) { + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + } + retval += "." + EOL; + + + if (expectedTokenSequences.length == 0) { + // Nothing to add here + } else { + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + EOL + " "; + } else { + retval += "Was expecting one of:" + EOL + " "; + } + retval += expected.toString(); + } + + return retval; + } + + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + static String add_escapes(String str) { + StringBuilder retval = new StringBuilder(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4)); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} +/* JavaCC - OriginalChecksum=7b4787d8879bd4e4e79738c1d4a275eb (do not edit this line) */ diff --git a/java/org/apache/el/parser/SimpleCharStream.java b/java/org/apache/el/parser/SimpleCharStream.java new file mode 100644 index 0000000..7307fe0 --- /dev/null +++ b/java/org/apache/el/parser/SimpleCharStream.java @@ -0,0 +1,483 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 7.0 */ +/* JavaCCOptions:STATIC=false,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +package org.apache.el.parser; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +@SuppressWarnings("all") // Ignore warnings in generated code +public class SimpleCharStream +{ +/** Whether parser is static. */ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; +/** Position in buffer. */ + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 1; + protected boolean trackLineColumn = true; + + public void setTabSize(int i) { tabSize = i; } + public int getTabSize() { return tabSize; } + + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) { + bufpos = maxNextCharInd = 0; + } else { + ExpandBuff(false); + } + } + else if (available > tokenBegin) { + available = bufsize; + } else if ((tokenBegin - available) < 2048) { + ExpandBuff(true); + } else { + available = tokenBegin; + } + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } else { + maxNextCharInd += i; + } + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) { + tokenBegin = bufpos; + } + throw e; + } + } + +/** Start. */ + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } else { + line += (column = 1); + } + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + +/** Read a character. */ + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) { + bufpos = 0; + } + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) { + FillBuff(); + } + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return c; + } + + @Deprecated + /** + * @deprecated + * @see #getEndColumn + */ + + public int getColumn() { + return bufcolumn[bufpos]; + } + + @Deprecated + /** + * @deprecated + * @see #getEndLine + */ + + public int getLine() { + return bufline[bufpos]; + } + + /** Get token end column number. */ + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + /** Get token end line number. */ + public int getEndLine() { + return bufline[bufpos]; + } + + /** Get token beginning column number. */ + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + /** Get token beginning line number. */ + public int getBeginLine() { + return bufline[tokenBegin]; + } + +/** Backup a number of characters. */ + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) { + bufpos += bufsize; + } + } + + /** Constructor. */ + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + /** Constructor. */ + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + /** Constructor. */ + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + /** Get token literal value. */ + public String GetImage() + { + if (bufpos >= tokenBegin) { + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + } else { + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + } + + /** Get the suffix. */ + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) { + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + } else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + /** Reset buffer when finished. */ + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) { + bufline[j] = newLine++; + } else { + bufline[j] = newLine; + } + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + boolean getTrackLineColumn() { return trackLineColumn; } + void setTrackLineColumn(boolean tlc) { trackLineColumn = tlc; } +} +/* JavaCC - OriginalChecksum=1f3491e3824e8fdde2ac2b6c2dc76fe6 (do not edit this line) */ diff --git a/java/org/apache/el/parser/SimpleNode.java b/java/org/apache/el/parser/SimpleNode.java new file mode 100644 index 0000000..cb8b2ad --- /dev/null +++ b/java/org/apache/el/parser/SimpleNode.java @@ -0,0 +1,222 @@ +/* + * 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. + */ +/* Generated By:JJTree: Do not edit this line. SimpleNode.java */ +package org.apache.el.parser; + +import java.util.Arrays; + +import jakarta.el.ELException; +import jakarta.el.MethodInfo; +import jakarta.el.MethodReference; +import jakarta.el.PropertyNotWritableException; +import jakarta.el.ValueReference; + +import org.apache.el.lang.EvaluationContext; +import org.apache.el.util.MessageFactory; + + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public abstract class SimpleNode implements Node { + + /* + * Uses SimpleNode rather than Node for performance. + * + * See https://bz.apache.org/bugzilla/show_bug.cgi?id=68068 + */ + protected SimpleNode parent; + + protected SimpleNode[] children; + + protected final int id; + + protected String image; + + public SimpleNode(int i) { + id = i; + } + + @Override + public void jjtOpen() { + // NOOP by default + } + + @Override + public void jjtClose() { + // NOOP by default + } + + @Override + public void jjtSetParent(Node n) { + parent = (SimpleNode) n; + } + + @Override + public Node jjtGetParent() { + return parent; + } + + @Override + public void jjtAddChild(Node n, int i) { + if (children == null) { + children = new SimpleNode[i + 1]; + } else if (i >= children.length) { + SimpleNode c[] = new SimpleNode[i + 1]; + System.arraycopy(children, 0, c, 0, children.length); + children = c; + } + children[i] = (SimpleNode) n; + } + + @Override + public Node jjtGetChild(int i) { + return children[i]; + } + + @Override + public int jjtGetNumChildren() { + return (children == null) ? 0 : children.length; + } + + /* + * You can override these two methods in subclasses of SimpleNode to + * customize the way the node appears when the tree is dumped. If your + * output uses more than one line you should override toString(String), + * otherwise overriding toString() is probably all you need to do. + */ + + @Override + public String toString() { + if (this.image != null) { + return ELParserTreeConstants.jjtNodeName[id] + "[" + this.image + + "]"; + } + return ELParserTreeConstants.jjtNodeName[id]; + } + + @Override + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + @Override + public Class getType(EvaluationContext ctx) + throws ELException { + throw new UnsupportedOperationException(); + } + + @Override + public Object getValue(EvaluationContext ctx) + throws ELException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isReadOnly(EvaluationContext ctx) + throws ELException { + return true; + } + + @Override + public void setValue(EvaluationContext ctx, Object value) + throws ELException { + throw new PropertyNotWritableException(MessageFactory.get("error.syntax.set")); + } + + @Override + public void accept(NodeVisitor visitor) throws Exception { + visitor.visit(this); + if (this.children != null && this.children.length > 0) { + for (Node child : this.children) { + child.accept(visitor); + } + } + } + + @Override + public Object invoke(EvaluationContext ctx, Class[] paramTypes, + Object[] paramValues) throws ELException { + throw new UnsupportedOperationException(); + } + + @Override + public MethodInfo getMethodInfo(EvaluationContext ctx, + Class[] paramTypes) throws ELException { + throw new UnsupportedOperationException(); + } + + @Override + public MethodReference getMethodReference(EvaluationContext ctx) { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(children); + result = prime * result + id; + result = prime * result + ((image == null) ? 0 : image.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof SimpleNode)) { + return false; + } + SimpleNode other = (SimpleNode) obj; + if (id != other.id) { + return false; + } + if (image == null) { + if (other.image != null) { + return false; + } + } else if (!image.equals(other.image)) { + return false; + } + if (!Arrays.equals(children, other.children)) { + return false; + } + return true; + } + + /** + * @since EL 2.2 + */ + @Override + public ValueReference getValueReference(EvaluationContext ctx) { + return null; + } + + /** + * @since EL 2.2 + */ + @Override + public boolean isParametersProvided() { + return false; + } +} diff --git a/java/org/apache/el/parser/Token.java b/java/org/apache/el/parser/Token.java new file mode 100644 index 0000000..55f34a7 --- /dev/null +++ b/java/org/apache/el/parser/Token.java @@ -0,0 +1,133 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 7.0 */ +/* JavaCCOptions:TOKEN_EXTENDS=,KEEP_LINE_COLUMN=true,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +package org.apache.el.parser; + +/** + * Describes the input token stream. + */ + +@SuppressWarnings("all") // Ignore warnings in generated code +public class Token implements java.io.Serializable { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** The line number of the first character of this Token. */ + public int beginLine; + /** The column number of the first character of this Token. */ + public int beginColumn; + /** The line number of the last character of this Token. */ + public int endLine; + /** The column number of the last character of this Token. */ + public int endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * An optional attribute value of the Token. + * Tokens which are not used as syntactic sugar will often contain + * meaningful values that will be used later on by the compiler or + * interpreter. This attribute value is often different from the image. + * Any subclass of Token that actually wants to return a non-null value can + * override this method as appropriate. + */ + public Object getValue() { + return null; + } + + /** + * No-argument constructor + */ + public Token() {} + + /** + * Constructs a new token for the specified Image. + */ + public Token(int kind) + { + this(kind, null); + } + + /** + * Constructs a new token for the specified Image and Kind. + */ + public Token(int kind, String image) + { + this.kind = kind; + this.image = image; + } + + /** + * Returns the image. + */ + @Override + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simply add something like : + * + * case MyParserConstants.ID : return new IDToken(ofKind, image); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use sit in your lexical actions. + */ + public static Token newToken(int ofKind, String image) + { + switch(ofKind) + { + default : return new Token(ofKind, image); + } + } + + public static Token newToken(int ofKind) + { + return newToken(ofKind, null); + } + +} +/* JavaCC - OriginalChecksum=4467a1ea6179d025edd92279d5f99003 (do not edit this line) */ diff --git a/java/org/apache/el/parser/TokenMgrError.java b/java/org/apache/el/parser/TokenMgrError.java new file mode 100644 index 0000000..3d3b565 --- /dev/null +++ b/java/org/apache/el/parser/TokenMgrError.java @@ -0,0 +1,148 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 7.0 */ +/* JavaCCOptions: */ +package org.apache.el.parser; + +/** Token Manager Error. */ +@SuppressWarnings("all") // Ignore warnings in generated code +public class TokenMgrError extends Error +{ + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occurred. + */ + public static final int LEXICAL_ERROR = 0; + + /** + * An attempt was made to create a second instance of a static token manager. + */ + public static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + public static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + public static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their escaped (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuilder retval = new StringBuilder(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4)); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexical error + * curLexState : lexical state in which this error occurred + * errorLine : line number when the error occurred + * errorColumn : column number when the error occurred + * errorAfter : prefix that was seen before this error occurred + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalErr(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar) { + char curChar1 = (char)curChar; + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar1)) + "\"") + " (" + curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + @Override + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + /** No arg constructor. */ + public TokenMgrError() { + } + + /** Constructor with message and reason. */ + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + /** Full Constructor. */ + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar, int reason) { + this(LexicalErr(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} +/* JavaCC - OriginalChecksum=66bf707ee7f559454e7fb080d48291e1 (do not edit this line) */ diff --git a/java/org/apache/el/stream/Optional.java b/java/org/apache/el/stream/Optional.java new file mode 100644 index 0000000..19478f9 --- /dev/null +++ b/java/org/apache/el/stream/Optional.java @@ -0,0 +1,76 @@ +/* + * 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.el.stream; + +import jakarta.el.ELException; +import jakarta.el.LambdaExpression; + +import org.apache.el.util.MessageFactory; + +public class Optional { + + private final Object obj; + + static final Optional EMPTY = new Optional(null); + + Optional(Object obj) { + this.obj = obj; + } + + + public Object get() throws ELException { + if (obj == null) { + throw new ELException(MessageFactory.get("stream.optional.empty")); + } else { + return obj; + } + } + + + public void ifPresent(LambdaExpression le) { + if (obj != null) { + le.invoke(obj); + } + } + + + public Object orElse(Object other) { + if (obj == null) { + return other; + } else { + return obj; + } + } + + + public Object orElseGet(Object le) { + if (obj == null) { + // EL 3.0 specification says parameter is LambdaExpression but it + // may already have been evaluated. If that is the case, the + // original parameter will have been checked to ensure it was a + // LambdaExpression before it was evaluated. + + if (le instanceof LambdaExpression) { + return ((LambdaExpression) le).invoke((Object[]) null); + } else { + return le; + } + } else { + return obj; + } + } +} diff --git a/java/org/apache/el/stream/Stream.java b/java/org/apache/el/stream/Stream.java new file mode 100644 index 0000000..4f9241e --- /dev/null +++ b/java/org/apache/el/stream/Stream.java @@ -0,0 +1,522 @@ +/* + * 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.el.stream; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +import jakarta.el.ELException; +import jakarta.el.LambdaExpression; + +import org.apache.el.lang.ELArithmetic; +import org.apache.el.lang.ELSupport; +import org.apache.el.util.MessageFactory; + +public class Stream { + + private final Iterator iterator; + + + public Stream(Iterator iterator) { + this.iterator = iterator; + } + + + public Stream filter(final LambdaExpression le) { + Iterator downStream = new OpIterator() { + @Override + protected void findNext() { + while (iterator.hasNext()) { + Object obj = iterator.next(); + if (ELSupport.coerceToBoolean(null, le.invoke(obj), + true).booleanValue()) { + next = obj; + foundNext = true; + break; + } + } + } + }; + return new Stream(downStream); + } + + + public Stream map(final LambdaExpression le) { + Iterator downStream = new OpIterator() { + @Override + protected void findNext() { + if (iterator.hasNext()) { + Object obj = iterator.next(); + next = le.invoke(obj); + foundNext = true; + } + } + }; + return new Stream(downStream); + } + + + public Stream flatMap(final LambdaExpression le) { + Iterator downStream = new OpIterator() { + + private Iterator inner; + + @Override + protected void findNext() { + while (iterator.hasNext() || + (inner != null && inner.hasNext())) { + if (inner == null || !inner.hasNext()) { + inner = ((Stream) le.invoke(iterator.next())).iterator; + } + + if (inner.hasNext()) { + next = inner.next(); + foundNext = true; + break; + } + } + } + }; + return new Stream(downStream); + } + + + public Stream distinct() { + Iterator downStream = new OpIterator() { + + private Set values = new HashSet<>(); + + @Override + protected void findNext() { + while (iterator.hasNext()) { + Object obj = iterator.next(); + if (values.add(obj)) { + next = obj; + foundNext = true; + break; + } + } + } + }; + return new Stream(downStream); + } + + + public Stream sorted() { + Iterator downStream = new OpIterator() { + + private Iterator sorted = null; + + @Override + protected void findNext() { + if (sorted == null) { + sort(); + } + if (sorted.hasNext()) { + next = sorted.next(); + foundNext = true; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void sort() { + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + Collections.sort(list); + sorted = list.iterator(); + } + }; + return new Stream(downStream); + } + + + public Stream sorted(final LambdaExpression le) { + Iterator downStream = new OpIterator() { + + private Iterator sorted = null; + + @Override + protected void findNext() { + if (sorted == null) { + sort(le); + } + if (sorted.hasNext()) { + next = sorted.next(); + foundNext = true; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void sort(LambdaExpression le) { + List list = new ArrayList<>(); + Comparator c = new LambdaExpressionComparator(le); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + list.sort(c); + sorted = list.iterator(); + } + }; + return new Stream(downStream); + } + + + public Object forEach(final LambdaExpression le) { + while (iterator.hasNext()) { + le.invoke(iterator.next()); + } + return null; + } + + + public Stream peek(final LambdaExpression le) { + Iterator downStream = new OpIterator() { + @Override + protected void findNext() { + if (iterator.hasNext()) { + Object obj = iterator.next(); + le.invoke(obj); + next = obj; + foundNext = true; + } + } + }; + return new Stream(downStream); + } + + + public Iterator iterator() { + return iterator; + } + + + public Stream limit(final Number count) { + return substream(Integer.valueOf(0), count); + } + + + public Stream substream(final Number start) { + return substream(start, Integer.valueOf(Integer.MAX_VALUE)); + } + + public Stream substream(final Number start, final Number end) { + + Iterator downStream = new OpIterator() { + + private final int startPos = start.intValue(); + private final int endPos = end.intValue(); + private int itemCount = 0; + + @Override + protected void findNext() { + while (itemCount < startPos && iterator.hasNext()) { + iterator.next(); + itemCount++; + } + if (itemCount < endPos && iterator.hasNext()) { + itemCount++; + next = iterator.next(); + foundNext = true; + } + } + }; + return new Stream(downStream); + } + + + public List toList() { + List result = new ArrayList<>(); + while (iterator.hasNext()) { + result.add(iterator.next()); + } + return result; + } + + + public Object[] toArray() { + List result = new ArrayList<>(); + while (iterator.hasNext()) { + result.add(iterator.next()); + } + return result.toArray(new Object[0]); + } + + + public Optional reduce(LambdaExpression le) { + Object seed = null; + + if (iterator.hasNext()) { + seed = iterator.next(); + } + + if (seed == null) { + return Optional.EMPTY; + } else { + return new Optional(reduce(seed, le)); + } + } + + + public Object reduce(Object seed, LambdaExpression le) { + Object result = seed; + + while (iterator.hasNext()) { + result = le.invoke(result, iterator.next()); + } + + return result; + } + + + public Optional max() { + return compare(true); + } + + + public Optional max(LambdaExpression le) { + return compare(true, le); + } + + + public Optional min() { + return compare(false); + } + + + public Optional min(LambdaExpression le) { + return compare(false, le); + } + + + public Optional average() { + long count = 0; + Number sum = Long.valueOf(0); + + while (iterator.hasNext()) { + count++; + sum = ELArithmetic.add(sum, iterator.next()); + } + + if (count == 0) { + return Optional.EMPTY; + } else { + return new Optional(ELArithmetic.divide(sum, Long.valueOf(count))); + } + } + + + public Number sum() { + Number sum = Long.valueOf(0); + + while (iterator.hasNext()) { + sum = ELArithmetic.add(sum, iterator.next()); + } + + return sum; + } + + + public Long count() { + long count = 0; + + while (iterator.hasNext()) { + iterator.next(); + count ++; + } + + return Long.valueOf(count); + } + + + public Optional anyMatch(LambdaExpression le) { + if (!iterator.hasNext()) { + return Optional.EMPTY; + } + + Boolean match = Boolean.FALSE; + + while (!match.booleanValue() && iterator.hasNext()) { + match = (Boolean) le.invoke(iterator.next()); + } + + return new Optional(match); + } + + + public Optional allMatch(LambdaExpression le) { + if (!iterator.hasNext()) { + return Optional.EMPTY; + } + + Boolean match = Boolean.TRUE; + + while (match.booleanValue() && iterator.hasNext()) { + match = (Boolean) le.invoke(iterator.next()); + } + + return new Optional(match); + } + + + public Optional noneMatch(LambdaExpression le) { + if (!iterator.hasNext()) { + return Optional.EMPTY; + } + + Boolean match = Boolean.FALSE; + + while (!match.booleanValue() && iterator.hasNext()) { + match = (Boolean) le.invoke(iterator.next()); + } + + return new Optional(Boolean.valueOf(!match.booleanValue())); + } + + + public Optional findFirst() { + if (iterator.hasNext()) { + return new Optional(iterator.next()); + } else { + return Optional.EMPTY; + } + } + + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Optional compare(boolean isMax) { + Comparable result = null; + + if (iterator.hasNext()) { + Object obj = iterator.next(); + if ((obj instanceof Comparable)) { + result = (Comparable) obj; + } else { + throw new ELException( + MessageFactory.get("stream.compare.notComparable")); + } + } + + while (iterator.hasNext()) { + Object obj = iterator.next(); + if ((obj instanceof Comparable)) { + if (isMax && ((Comparable) obj).compareTo(result) > 0) { + result = (Comparable) obj; + } else if (!isMax && ((Comparable) obj).compareTo(result) < 0) { + result = (Comparable) obj; + } + } else { + throw new ELException( + MessageFactory.get("stream.compare.notComparable")); + } + } + + if (result == null) { + return Optional.EMPTY; + } else { + return new Optional(result); + } + } + + + private Optional compare(boolean isMax, LambdaExpression le) { + Object result = null; + + if (iterator.hasNext()) { + Object obj = iterator.next(); + result = obj; + } + + while (iterator.hasNext()) { + Object obj = iterator.next(); + if (isMax && ELSupport.coerceToNumber(null, le.invoke(obj, result), + Integer.class).intValue() > 0) { + result = obj; + } else if (!isMax && ELSupport.coerceToNumber(null, le.invoke(obj, result), + Integer.class).intValue() < 0) { + result = obj; + } + } + + if (result == null) { + return Optional.EMPTY; + } else { + return new Optional(result); + } + } + + + private static class LambdaExpressionComparator + implements Comparator { + + private final LambdaExpression le; + + LambdaExpressionComparator(LambdaExpression le) { + this.le = le; + } + + @Override + public int compare(Object o1, Object o2) { + return ELSupport.coerceToNumber( + null, le.invoke(o1, o2), Integer.class).intValue(); + } + } + + + private abstract static class OpIterator implements Iterator { + protected boolean foundNext = false; + protected Object next; + + @Override + public boolean hasNext() { + if (foundNext) { + return true; + } + findNext(); + return foundNext; + } + + @Override + public Object next() { + if (foundNext) { + foundNext = false; + return next; + } + findNext(); + if (foundNext) { + foundNext = false; + return next; + } else { + throw new NoSuchElementException(); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + protected abstract void findNext(); + } +} diff --git a/java/org/apache/el/stream/StreamELResolverImpl.java b/java/org/apache/el/stream/StreamELResolverImpl.java new file mode 100644 index 0000000..46bff5b --- /dev/null +++ b/java/org/apache/el/stream/StreamELResolverImpl.java @@ -0,0 +1,114 @@ +/* + * 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.el.stream; + +import java.beans.FeatureDescriptor; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; + +public class StreamELResolverImpl extends ELResolver { + + @Override + public Object getValue(ELContext context, Object base, Object property) { + return null; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, + Object value) { + // NO-OP + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + return false; + } + + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + @Override + public Iterator getFeatureDescriptors(ELContext context, + Object base) { + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return null; + } + + @Override + public Object invoke(ELContext context, Object base, Object method, + Class[] paramTypes, Object[] params) { + + if ("stream".equals(method) && params.length == 0) { + if (base.getClass().isArray()) { + context.setPropertyResolved(true); + return new Stream(new ArrayIterator(base)); + } else if (base instanceof Collection) { + context.setPropertyResolved(true); + @SuppressWarnings("unchecked") + Collection collection = (Collection) base; + return new Stream(collection.iterator()); + } + } + + // Not for handling by this resolver + return null; + } + + + private static class ArrayIterator implements Iterator { + + private final Object base; + private final int size; + private int index = 0; + + ArrayIterator(Object base) { + this.base = base; + size = Array.getLength(base); + } + + @Override + public boolean hasNext() { + return size > index; + } + + @Override + public Object next() { + try { + return Array.get(base, index++); + } catch (ArrayIndexOutOfBoundsException e) { + throw new NoSuchElementException(); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/java/org/apache/el/util/ConcurrentCache.java b/java/org/apache/el/util/ConcurrentCache.java new file mode 100644 index 0000000..1be17ab --- /dev/null +++ b/java/org/apache/el/util/ConcurrentCache.java @@ -0,0 +1,59 @@ +/* + * 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.el.util; + +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +public final class ConcurrentCache { + + private final int size; + + private final Map eden; + + private final Map longterm; + + public ConcurrentCache(int size) { + this.size = size; + this.eden = new ConcurrentHashMap<>(size); + this.longterm = new WeakHashMap<>(size); + } + + public V get(K k) { + V v = this.eden.get(k); + if (v == null) { + synchronized (longterm) { + v = this.longterm.get(k); + } + if (v != null) { + this.eden.put(k, v); + } + } + return v; + } + + public void put(K k, V v) { + if (this.eden.size() >= size) { + synchronized (longterm) { + this.longterm.putAll(this.eden); + } + this.eden.clear(); + } + this.eden.put(k, v); + } +} diff --git a/java/org/apache/el/util/ExceptionUtils.java b/java/org/apache/el/util/ExceptionUtils.java new file mode 100644 index 0000000..d8a76ff --- /dev/null +++ b/java/org/apache/el/util/ExceptionUtils.java @@ -0,0 +1,56 @@ +/* + * 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.el.util; + +/** + * Utilities for handling Throwables and Exceptions. + */ +/* + * Copied from o.a.t.u.ExceptionUtils + */ +public class ExceptionUtils { + + /** + * Checks whether the supplied Throwable is one that needs to be + * rethrown and swallows all others. + * @param t the Throwable to check + */ + public static void handleThrowable(Throwable t) { + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof StackOverflowError) { + // Swallow silently - it should be recoverable + return; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + // All other instances of Throwable will be silently swallowed + } + + + /** + * NO-OP method provided to enable simple pre-loading of this class. Since + * the class is used extensively in error handling, it is prudent to + * pre-load it to avoid any failure to load this class masking the true + * problem during error handling. + */ + public static void preload() { + // NO-OP + } +} diff --git a/java/org/apache/el/util/MessageFactory.java b/java/org/apache/el/util/MessageFactory.java new file mode 100644 index 0000000..2cc9362 --- /dev/null +++ b/java/org/apache/el/util/MessageFactory.java @@ -0,0 +1,80 @@ +/* + * 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.el.util; + +import java.text.Format; +import java.text.MessageFormat; +import java.text.NumberFormat; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +/** + * @author Jacob Hookom [jacob@hookom.net] + */ +public final class MessageFactory { + + private static final ResourceBundle DEFAULT_BUNDLE = ResourceBundle.getBundle("org.apache.el.LocalStrings"); + + private static final MessageFactory DEFAULT_MESSAGE_FACTORY = new MessageFactory(DEFAULT_BUNDLE); + + public static String get(final String key) { + return DEFAULT_MESSAGE_FACTORY.getInternal(key); + } + + public static String get(final String key, final Object... args) { + return DEFAULT_MESSAGE_FACTORY.getInternal(key, args); + } + + private final ResourceBundle bundle; + + public MessageFactory(ResourceBundle bundle) { + this.bundle = bundle; + } + + protected String getInternal(final String key) { + try { + return bundle.getString(key); + } catch (MissingResourceException e) { + return key; + } + } + + protected String getInternal(final String key, final Object... args) { + String value = getInternal(key); + + MessageFormat mf = new MessageFormat(value); + Format[] formats = null; + + // Unless an argument has been explicitly configured to use a number + // format, convert all Number arguments to String else MessageFormat may + // try to format them in unexpected ways. + if (args != null) { + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof Number) { + if (formats == null) { + formats = mf.getFormatsByArgumentIndex(); + } + if (i < formats.length && !(formats[i] instanceof NumberFormat)) { + args[i] = args[i].toString(); + } + } + } + } + + return mf.format(args, new StringBuffer(), null).toString(); + } +} diff --git a/java/org/apache/el/util/ReflectionUtil.java b/java/org/apache/el/util/ReflectionUtil.java new file mode 100644 index 0000000..226fc2a --- /dev/null +++ b/java/org/apache/el/util/ReflectionUtil.java @@ -0,0 +1,617 @@ +/* + * 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.el.util; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import jakarta.el.ELException; +import jakarta.el.MethodNotFoundException; + +import org.apache.el.lang.ELSupport; +import org.apache.el.lang.EvaluationContext; + + +/** + * Utilities for Managing Serialization and Reflection + * + * @author Jacob Hookom [jacob@hookom.net] + */ +public class ReflectionUtil { + + protected static final String[] PRIMITIVE_NAMES = new String[] { "boolean", + "byte", "char", "double", "float", "int", "long", "short", "void" }; + + protected static final Class[] PRIMITIVES = new Class[] { boolean.class, + byte.class, char.class, double.class, float.class, int.class, + long.class, short.class, Void.TYPE }; + + private ReflectionUtil() { + super(); + } + + public static Class forName(String name) throws ClassNotFoundException { + if (null == name || name.isEmpty()) { + return null; + } + Class c = forNamePrimitive(name); + if (c == null) { + if (name.endsWith("[]")) { + String nc = name.substring(0, name.length() - 2); + c = Class.forName(nc, true, getContextClassLoader()); + c = Array.newInstance(c, 0).getClass(); + } else { + c = Class.forName(name, true, getContextClassLoader()); + } + } + return c; + } + + protected static Class forNamePrimitive(String name) { + if (name.length() <= 8) { + int p = Arrays.binarySearch(PRIMITIVE_NAMES, name); + if (p >= 0) { + return PRIMITIVES[p]; + } + } + return null; + } + + /** + * Converts an array of Class names to Class types. + * @param s The array of class names + * @return An array of Class instance where the element at index i in the + * result is an instance of the class with the name at index i in + * the input + * @throws ClassNotFoundException If a class of a given name cannot be found + */ + public static Class[] toTypeArray(String[] s) throws ClassNotFoundException { + if (s == null) { + return null; + } + Class[] c = new Class[s.length]; + for (int i = 0; i < s.length; i++) { + c[i] = forName(s[i]); + } + return c; + } + + /** + * Converts an array of Class types to Class names. + * @param c The array of class instances + * @return An array of Class names where the element at index i in the + * result is the name of the class instance at index i in the input + */ + public static String[] toTypeNameArray(Class[] c) { + if (c == null) { + return null; + } + String[] s = new String[c.length]; + for (int i = 0; i < c.length; i++) { + s[i] = c[i].getName(); + } + return s; + } + + /** + * Returns a method based on the criteria. + * @param ctx the context in which the expression is being evaluated + * @param base the object that owns the method + * @param property the name of the method + * @param paramTypes the parameter types to use + * @param paramValues the parameter values + * @return the method specified + * @throws MethodNotFoundException If a method cannot be found that matches + * the given criteria + */ + /* + * This class duplicates code in jakarta.el.Util. When making changes keep + * the code in sync. + */ + @SuppressWarnings("null") + public static Method getMethod(EvaluationContext ctx, Object base, Object property, + Class[] paramTypes, Object[] paramValues) + throws MethodNotFoundException { + + if (base == null || property == null) { + throw new MethodNotFoundException(MessageFactory.get( + "error.method.notfound", base, property, + paramString(paramTypes))); + } + + String methodName = (property instanceof String) ? (String) property + : property.toString(); + + int paramCount; + if (paramTypes == null) { + paramCount = 0; + } else { + paramCount = paramTypes.length; + } + + Method[] methods = base.getClass().getMethods(); + Map candidates = new HashMap<>(); + + for (Method m : methods) { + if (!m.getName().equals(methodName)) { + // Method name doesn't match + continue; + } + + Class[] mParamTypes = m.getParameterTypes(); + int mParamCount = mParamTypes.length; + + // Check the number of parameters + // Multiple tests to improve readability + if (!m.isVarArgs() && paramCount != mParamCount) { + // Method has wrong number of parameters + continue; + } + if (m.isVarArgs() && paramCount < mParamCount -1) { + // Method has wrong number of parameters + continue; + } + if (m.isVarArgs() && paramCount == mParamCount && paramValues != null && + paramValues.length > paramCount && !paramTypes[mParamCount -1].isArray()) { + // Method arguments don't match + continue; + } + if (m.isVarArgs() && paramCount > mParamCount && paramValues != null && + paramValues.length != paramCount) { + // Might match a different varargs method + continue; + } + if (!m.isVarArgs() && paramValues != null && paramCount != paramValues.length) { + // Might match a different varargs method + continue; + } + + // Check the parameters match + int exactMatch = 0; + int assignableMatch = 0; + int coercibleMatch = 0; + int varArgsMatch = 0; + boolean noMatch = false; + for (int i = 0; i < mParamCount; i++) { + // Can't be null + if (m.isVarArgs() && i == (mParamCount - 1)) { + if (i == paramCount || (paramValues != null && paramValues.length == i)) { + // Var args defined but nothing is passed as varargs + // Use MAX_VALUE so this matches only if nothing else does + varArgsMatch = Integer.MAX_VALUE; + break; + } + Class varType = mParamTypes[i].getComponentType(); + for (int j = i; j < paramCount; j++) { + if (isAssignableFrom(paramTypes[j], varType)) { + assignableMatch++; + varArgsMatch++; + } else { + if (paramValues == null) { + noMatch = true; + break; + } else { + if (isCoercibleFrom(ctx, paramValues[j], varType)) { + coercibleMatch++; + varArgsMatch++; + } else { + noMatch = true; + break; + } + } + } + // Don't treat a varArgs match as an exact match, it can + // lead to a varArgs method matching when the result + // should be ambiguous + } + } else { + if (mParamTypes[i].equals(paramTypes[i])) { + exactMatch++; + } else if (paramTypes[i] != null && isAssignableFrom(paramTypes[i], mParamTypes[i])) { + assignableMatch++; + } else { + if (paramValues == null) { + noMatch = true; + break; + } else { + if (isCoercibleFrom(ctx, paramValues[i], mParamTypes[i])) { + coercibleMatch++; + } else { + noMatch = true; + break; + } + } + } + } + } + if (noMatch) { + continue; + } + + // If a method is found where every parameter matches exactly, + // and no vars args are present, return it + if (exactMatch == paramCount && varArgsMatch == 0) { + Method result = getMethod(base.getClass(), base, m); + if (result == null) { + throw new MethodNotFoundException(MessageFactory.get( + "error.method.notfound", base, property, + paramString(paramTypes))); + } + return result; + } + + candidates.put(m, new MatchResult( + m.isVarArgs(), exactMatch, assignableMatch, coercibleMatch, varArgsMatch, m.isBridge())); + } + + // Look for the method that has the highest number of parameters where + // the type matches exactly + MatchResult bestMatch = new MatchResult(true, 0, 0, 0, 0, true); + Method match = null; + boolean multiple = false; + for (Map.Entry entry : candidates.entrySet()) { + int cmp = entry.getValue().compareTo(bestMatch); + if (cmp > 0 || match == null) { + bestMatch = entry.getValue(); + match = entry.getKey(); + multiple = false; + } else if (cmp == 0) { + multiple = true; + } + } + if (multiple) { + if (bestMatch.getExactCount() == paramCount - 1) { + // Only one parameter is not an exact match - try using the + // super class + match = resolveAmbiguousMethod(candidates.keySet(), paramTypes); + } else { + match = null; + } + + if (match == null) { + // If multiple methods have the same matching number of parameters + // the match is ambiguous so throw an exception + throw new MethodNotFoundException(MessageFactory.get( + "error.method.ambiguous", base, property, + paramString(paramTypes))); + } + } + + // Handle case where no match at all was found + if (match == null) { + throw new MethodNotFoundException(MessageFactory.get( + "error.method.notfound", base, property, + paramString(paramTypes))); + } + + Method result = getMethod(base.getClass(), base, match); + if (result == null) { + throw new MethodNotFoundException(MessageFactory.get( + "error.method.notfound", base, property, + paramString(paramTypes))); + } + return result; + } + + /* + * This class duplicates code in jakarta.el.Util. When making changes keep + * the code in sync. + */ + private static Method resolveAmbiguousMethod(Set candidates, + Class[] paramTypes) { + // Identify which parameter isn't an exact match + Method m = candidates.iterator().next(); + + int nonMatchIndex = 0; + Class nonMatchClass = null; + + for (int i = 0; i < paramTypes.length; i++) { + if (m.getParameterTypes()[i] != paramTypes[i]) { + nonMatchIndex = i; + nonMatchClass = paramTypes[i]; + break; + } + } + + if (nonMatchClass == null) { + // Null will always be ambiguous + return null; + } + + for (Method c : candidates) { + if (c.getParameterTypes()[nonMatchIndex] == + paramTypes[nonMatchIndex]) { + // Methods have different non-matching parameters + // Result is ambiguous + return null; + } + } + + // Can't be null + Class superClass = nonMatchClass.getSuperclass(); + while (superClass != null) { + for (Method c : candidates) { + if (c.getParameterTypes()[nonMatchIndex].equals(superClass)) { + // Found a match + return c; + } + } + superClass = superClass.getSuperclass(); + } + + // Treat instances of Number as a special case + Method match = null; + if (Number.class.isAssignableFrom(nonMatchClass)) { + for (Method c : candidates) { + Class candidateType = c.getParameterTypes()[nonMatchIndex]; + if (Number.class.isAssignableFrom(candidateType) || + candidateType.isPrimitive()) { + if (match == null) { + match = c; + } else { + // Match still ambiguous + match = null; + break; + } + } + } + } + + return match; + } + + + /* + * This class duplicates code in jakarta.el.Util. When making changes keep + * the code in sync. + */ + private static boolean isAssignableFrom(Class src, Class target) { + // src will always be an object + // Short-cut. null is always assignable to an object and in EL null + // can always be coerced to a valid value for a primitive + if (src == null) { + return true; + } + + Class targetClass; + if (target.isPrimitive()) { + if (target == Boolean.TYPE) { + targetClass = Boolean.class; + } else if (target == Character.TYPE) { + targetClass = Character.class; + } else if (target == Byte.TYPE) { + targetClass = Byte.class; + } else if (target == Short.TYPE) { + targetClass = Short.class; + } else if (target == Integer.TYPE) { + targetClass = Integer.class; + } else if (target == Long.TYPE) { + targetClass = Long.class; + } else if (target == Float.TYPE) { + targetClass = Float.class; + } else { + targetClass = Double.class; + } + } else { + targetClass = target; + } + return targetClass.isAssignableFrom(src); + } + + + /* + * This class duplicates code in jakarta.el.Util. When making changes keep + * the code in sync. + */ + private static boolean isCoercibleFrom(EvaluationContext ctx, Object src, Class target) { + // TODO: This isn't pretty but it works. Significant refactoring would + // be required to avoid the exception. + try { + ELSupport.coerceToType(ctx, src, target); + } catch (ELException e) { + return false; + } + return true; + } + + + /* + * This class duplicates code in jakarta.el.Util. When making changes keep + * the code in sync. + */ + private static Method getMethod(Class type, Object base, Method m) { + if (m == null || + (Modifier.isPublic(type.getModifiers()) && + (Modifier.isStatic(m.getModifiers()) && m.canAccess(null) || m.canAccess(base)))) { + return m; + } + Class[] interfaces = type.getInterfaces(); + Method mp = null; + for (Class iface : interfaces) { + try { + mp = iface.getMethod(m.getName(), m.getParameterTypes()); + mp = getMethod(mp.getDeclaringClass(), base, mp); + if (mp != null) { + return mp; + } + } catch (NoSuchMethodException e) { + // Ignore + } + } + Class sup = type.getSuperclass(); + if (sup != null) { + try { + mp = sup.getMethod(m.getName(), m.getParameterTypes()); + mp = getMethod(mp.getDeclaringClass(), base, mp); + if (mp != null) { + return mp; + } + } catch (NoSuchMethodException e) { + // Ignore + } + } + return null; + } + + + private static String paramString(Class[] types) { + if (types != null) { + StringBuilder sb = new StringBuilder(); + for (Class type : types) { + if (type == null) { + sb.append("null, "); + } else { + sb.append(type.getName()).append(", "); + } + } + if (sb.length() > 2) { + sb.setLength(sb.length() - 2); + } + return sb.toString(); + } + return null; + } + + + private static ClassLoader getContextClassLoader() { + ClassLoader tccl; + if (System.getSecurityManager() != null) { + PrivilegedAction pa = new PrivilegedGetTccl(); + tccl = AccessController.doPrivileged(pa); + } else { + tccl = Thread.currentThread().getContextClassLoader(); + } + + return tccl; + } + + + private static class PrivilegedGetTccl implements PrivilegedAction { + @Override + public ClassLoader run() { + return Thread.currentThread().getContextClassLoader(); + } + } + + + /* + * This class duplicates code in jakarta.el.Util. When making changes keep + * the code in sync. + */ + private static class MatchResult implements Comparable { + + private final boolean varArgs; + private final int exactCount; + private final int assignableCount; + private final int coercibleCount; + private final int varArgsCount; + private final boolean bridge; + + MatchResult(boolean varArgs, int exactCount, int assignableCount, int coercibleCount, int varArgsCount, + boolean bridge) { + this.varArgs = varArgs; + this.exactCount = exactCount; + this.assignableCount = assignableCount; + this.coercibleCount = coercibleCount; + this.varArgsCount = varArgsCount; + this.bridge = bridge; + } + + public boolean isVarArgs() { + return varArgs; + } + + public int getExactCount() { + return exactCount; + } + + public int getAssignableCount() { + return assignableCount; + } + + public int getCoercible() { + return coercibleCount; + } + + public int getVarArgsCount() { + return varArgsCount; + } + + public boolean isBridge() { + return bridge; + } + + @Override + public int compareTo(MatchResult o) { + // Non-varArgs always beats varArgs + int cmp = Boolean.compare(o.isVarArgs(), this.isVarArgs()); + if (cmp == 0) { + cmp = Integer.compare(this.getExactCount(), o.getExactCount()); + if (cmp == 0) { + cmp = Integer.compare(this.getAssignableCount(), o.getAssignableCount()); + if (cmp == 0) { + cmp = Integer.compare(this.getCoercible(), o.getCoercible()); + if (cmp == 0) { + // Fewer var args matches are better + cmp = Integer.compare(o.getVarArgsCount(), this.getVarArgsCount()); + if (cmp == 0) { + // The nature of bridge methods is such that it actually + // doesn't matter which one we pick as long as we pick + // one. That said, pick the 'right' one (the non-bridge + // one) anyway. + cmp = Boolean.compare(o.isBridge(), this.isBridge()); + } + } + } + } + } + return cmp; + } + + @Override + public boolean equals(Object o) { + return o == this || (null != o && + this.getClass().equals(o.getClass()) && + ((MatchResult)o).getExactCount() == this.getExactCount() && + ((MatchResult)o).getAssignableCount() == this.getAssignableCount() && + ((MatchResult)o).getCoercible() == this.getCoercible() && + ((MatchResult)o).getVarArgsCount() == this.getVarArgsCount() && + ((MatchResult)o).isVarArgs() == this.isVarArgs() && + ((MatchResult)o).isBridge() == this.isBridge()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + assignableCount; + result = prime * result + (bridge ? 1231 : 1237); + result = prime * result + coercibleCount; + result = prime * result + exactCount; + result = prime * result + (varArgs ? 1231 : 1237); + result = prime * result + varArgsCount; + return result; + } + } +} diff --git a/java/org/apache/el/util/Validation.java b/java/org/apache/el/util/Validation.java new file mode 100644 index 0000000..a27e75f --- /dev/null +++ b/java/org/apache/el/util/Validation.java @@ -0,0 +1,108 @@ +/* + * 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.el.util; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +public class Validation { + + // Java keywords, boolean literals & the null literal in alphabetical order + private static final String invalidIdentifiers[] = { "abstract", "assert", + "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", + "false", "final", "finally", "float", "for", "goto", "if", "implements", + "import", "instanceof", "int", "interface", "long", "native", "new", + "null", "package", "private", "protected", "public", "return", "short", + "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "true", "try", "void", "volatile", + "while" }; + + private static final boolean IS_SECURITY_ENABLED = + (System.getSecurityManager() != null); + + private static final boolean SKIP_IDENTIFIER_CHECK; + + static { + String skipIdentifierCheckStr; + if (IS_SECURITY_ENABLED) { + skipIdentifierCheckStr = AccessController.doPrivileged( + (PrivilegedAction) () -> System.getProperty("org.apache.el.parser.SKIP_IDENTIFIER_CHECK", "false")); + } else { + skipIdentifierCheckStr = System.getProperty( + "org.apache.el.parser.SKIP_IDENTIFIER_CHECK", "false"); + } + SKIP_IDENTIFIER_CHECK = Boolean.parseBoolean(skipIdentifierCheckStr); + } + + + private Validation() { + // Utility class. Hide default constructor + } + + /** + * Test whether a string is a Java identifier. Note that the behaviour of + * this method depend on the system property + * {@code org.apache.el.parser.SKIP_IDENTIFIER_CHECK} + * + * @param key The string to test + * + * @return {@code true} if the provided String should be treated as a Java + * identifier, otherwise false + */ + public static boolean isIdentifier(String key) { + + if (SKIP_IDENTIFIER_CHECK) { + return true; + } + + // Should not be the case but check to be sure + if (key == null || key.length() == 0) { + return false; + } + + // Check the list of known invalid values + int i = 0; + int j = invalidIdentifiers.length; + while (i < j) { + int k = (i + j) >>> 1; // Avoid overflow + int result = invalidIdentifiers[k].compareTo(key); + if (result == 0) { + return false; + } + if (result < 0) { + i = k + 1; + } else { + j = k; + } + } + + // Check the start character that has more restrictions + if (!Character.isJavaIdentifierStart(key.charAt(0))) { + return false; + } + + // Check each remaining character used is permitted + for (int idx = 1; idx < key.length(); idx++) { + if (!Character.isJavaIdentifierPart(key.charAt(idx))) { + return false; + } + } + + return true; + } +} diff --git a/java/org/apache/jasper/Constants.java b/java/org/apache/jasper/Constants.java new file mode 100644 index 0000000..ec17cea --- /dev/null +++ b/java/org/apache/jasper/Constants.java @@ -0,0 +1,101 @@ +/* + * 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.jasper; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Some constants and other global data that are used by the compiler and the runtime. + * + * @author Anil K. Vijendran + * @author Harish Prabandham + * @author Shawn Bayern + * @author Mark Roth + */ +public class Constants { + + public static final String SPEC_VERSION = "3.1"; + + /** + * These classes/packages are automatically imported by the + * generated code. + */ + private static final String[] PRIVATE_STANDARD_IMPORTS = { + "jakarta.servlet.*", + "jakarta.servlet.http.*", + "jakarta.servlet.jsp.*" + }; + public static final List STANDARD_IMPORTS = + Collections.unmodifiableList(Arrays.asList(PRIVATE_STANDARD_IMPORTS)); + + /** + * Default size of the JSP buffer. + */ + public static final int DEFAULT_BUFFER_SIZE = 8 * 1024; + + /** + * Default size for the tag buffers. + */ + public static final int DEFAULT_TAG_BUFFER_SIZE = 512; + + /** + * Default tag handler pool size. + */ + public static final int MAX_POOL_SIZE = 5; + + /** + * Has security been turned on? + */ + public static final boolean IS_SECURITY_ENABLED = + (System.getSecurityManager() != null); + + /** + * Name of the system property containing + * the tomcat product installation path + */ + public static final String CATALINA_HOME_PROP = "catalina.home"; + + + /** + * Name of the ServletContext init-param that determines if the XML parsers + * used for *.tld files will be validating or not. + *

    + * This must be kept in sync with org.apache.catalina.Globals + */ + public static final String XML_VALIDATION_TLD_INIT_PARAM = + "org.apache.jasper.XML_VALIDATE_TLD"; + + /** + * Name of the ServletContext init-param that determines if the XML parsers + * will block the resolution of external entities. + *

    + * This must be kept in sync with org.apache.catalina.Globals + */ + public static final String XML_BLOCK_EXTERNAL_INIT_PARAM = + "org.apache.jasper.XML_BLOCK_EXTERNAL"; + + /** + * Name of the ServletContext init-param that determines the JSP + * factory pool size. Set the value to a positive integer to enable it. + * The default value is 8 per thread. + */ + public static final String JSP_FACTORY_POOL_SIZE_INIT_PARAM = + "org.apache.jasper.runtime.JspFactoryImpl.POOL_SIZE"; + +} diff --git a/java/org/apache/jasper/EmbeddedServletOptions.java b/java/org/apache/jasper/EmbeddedServletOptions.java new file mode 100644 index 0000000..a0a88de --- /dev/null +++ b/java/org/apache/jasper/EmbeddedServletOptions.java @@ -0,0 +1,968 @@ +/* + * 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.jasper; + +import java.io.File; +import java.util.Enumeration; +import java.util.Map; +import java.util.Properties; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; + +import org.apache.jasper.compiler.JspConfig; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.compiler.TagPluginManager; +import org.apache.jasper.compiler.TldCache; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A class to hold all init parameters specific to the JSP engine. + * + * @author Anil K. Vijendran + * @author Hans Bergsten + * @author Pierre Delisle + */ +public final class EmbeddedServletOptions implements Options { + + // Logger + private final Log log = LogFactory.getLog(EmbeddedServletOptions.class); // must not be static + + private Properties settings = new Properties(); + + /** + * Is Jasper being used in development mode? + */ + private boolean development = true; + + /** + * Should Ant fork its java compiles of JSP pages. + */ + public boolean fork = true; + + /** + * Do you want to keep the generated Java files around? + */ + private boolean keepGenerated = true; + + /** + * How should template text that consists entirely of whitespace be handled? + */ + private TrimSpacesOption trimSpaces = TrimSpacesOption.FALSE; + + /** + * Determines whether tag handler pooling is enabled. + */ + private boolean isPoolingEnabled = true; + + /** + * Do you want support for "mapped" files? This will generate + * servlet that has a print statement per line of the JSP file. + * This seems like a really nice feature to have for debugging. + */ + private boolean mappedFile = true; + + /** + * Do we want to include debugging information in the class file? + */ + private boolean classDebugInfo = true; + + /** + * Background compile thread check interval in seconds. + */ + private int checkInterval = 0; + + /** + * Is the generation of SMAP info for JSR45 debugging suppressed? + */ + private boolean isSmapSuppressed = false; + + /** + * Should SMAP info for JSR45 debugging be dumped to a file? + */ + private boolean isSmapDumped = false; + + /** + * Are Text strings to be generated as char arrays? + */ + private boolean genStringAsCharArray = false; + + private boolean errorOnUseBeanInvalidClassAttribute = true; + + /** + * I want to see my generated servlets. Which directory are they + * in? + */ + private File scratchDir; + + /** + * What classpath should I use while compiling generated servlets? + */ + private String classpath = null; + + /** + * Compiler to use. + */ + private String compiler = null; + + /** + * Compiler target VM. + */ + private String compilerTargetVM = "11"; + + /** + * The compiler source VM. + */ + private String compilerSourceVM = "11"; + + /** + * The compiler class name. + */ + private String compilerClassName = null; + + /** + * Cache for the TLD URIs, resource paths and parsed files. + */ + private TldCache tldCache = null; + + /** + * Jsp config information + */ + private JspConfig jspConfig = null; + + /** + * TagPluginManager + */ + private TagPluginManager tagPluginManager = null; + + /** + * Java platform encoding to generate the JSP + * page servlet. + */ + private String javaEncoding = "UTF-8"; + + /** + * Modification test interval. + */ + private int modificationTestInterval = 4; + + /** + * Is re-compilation attempted immediately after a failure? + */ + private boolean recompileOnFail = false; + + /** + * Is generation of X-Powered-By response header enabled/disabled? + */ + private boolean xpoweredBy; + + /** + * Should we include a source fragment in exception messages, which could be displayed + * to the developer ? + */ + private boolean displaySourceFragment = true; + + + /** + * The maximum number of loaded jsps per web-application. If there are more + * jsps loaded, they will be unloaded. + */ + private int maxLoadedJsps = -1; + + /** + * The idle time in seconds after which a JSP is unloaded. + * If unset or less or equal than 0, no jsps are unloaded. + */ + private int jspIdleTimeout = -1; + + /** + * Should JSP.1.6 be applied strictly to attributes defined using scriptlet + * expressions? + */ + private boolean strictQuoteEscaping = true; + + /** + * When EL is used in JSP attribute values, should the rules for quoting of + * attributes described in JSP.1.6 be applied to the expression? + */ + private boolean quoteAttributeEL = true; + + private String variableForExpressionFactory = "_el_expressionfactory"; + + private String variableForInstanceManager = "_jsp_instancemanager"; + + private boolean poolTagsWithExtends = false; + + private boolean strictGetProperty = true; + + private boolean strictWhitespace = true; + + private String jspServletBase = "org.apache.jasper.runtime.HttpJspBase"; + + private String serviceMethodName = "_jspService"; + + private String servletClasspathAttribute = "org.apache.catalina.jsp_classpath"; + + private String jspPrecompilationQueryParameter = "jsp_precompile"; + + private String generatedJspPackageName = "org.apache.jsp"; + + private String generatedTagFilePackageName = "org.apache.jsp.tag"; + + private String tempVariableNamePrefix = "_jspx_temp"; + + private boolean useInstanceManagerForTags = false; + + public String getProperty(String name ) { + return settings.getProperty( name ); + } + + public void setProperty(String name, String value ) { + if (name != null && value != null){ + settings.setProperty( name, value ); + } + } + + public void setQuoteAttributeEL(boolean b) { + this.quoteAttributeEL = b; + } + + @Override + public boolean getQuoteAttributeEL() { + return quoteAttributeEL; + } + + /** + * Are we keeping generated code around? + */ + @Override + public boolean getKeepGenerated() { + return keepGenerated; + } + + @Override + public TrimSpacesOption getTrimSpaces() { + return trimSpaces; + } + + @Override + public boolean isPoolingEnabled() { + return isPoolingEnabled; + } + + /** + * Are we supporting HTML mapped servlets? + */ + @Override + public boolean getMappedFile() { + return mappedFile; + } + + /** + * Should class files be compiled with debug information? + */ + @Override + public boolean getClassDebugInfo() { + return classDebugInfo; + } + + /** + * Background JSP compile thread check interval + */ + @Override + public int getCheckInterval() { + return checkInterval; + } + + /** + * Modification test interval. + */ + @Override + public int getModificationTestInterval() { + return modificationTestInterval; + } + + /** + * Re-compile on failure. + */ + @Override + public boolean getRecompileOnFail() { + return recompileOnFail; + } + + /** + * Is Jasper being used in development mode? + */ + @Override + public boolean getDevelopment() { + return development; + } + + /** + * Is the generation of SMAP info for JSR45 debugging suppressed? + */ + @Override + public boolean isSmapSuppressed() { + return isSmapSuppressed; + } + + /** + * Should SMAP info for JSR45 debugging be dumped to a file? + */ + @Override + public boolean isSmapDumped() { + return isSmapDumped; + } + + /** + * Are Text strings to be generated as char arrays? + */ + @Override + public boolean genStringAsCharArray() { + return this.genStringAsCharArray; + } + + /** + * What is my scratch dir? + */ + @Override + public File getScratchDir() { + return scratchDir; + } + + /** + * What classpath should I use while compiling the servlets + * generated from JSP files? + */ + @Override + public String getClassPath() { + return classpath; + } + + /** + * Is generation of X-Powered-By response header enabled/disabled? + */ + @Override + public boolean isXpoweredBy() { + return xpoweredBy; + } + + /** + * Compiler to use. + */ + @Override + public String getCompiler() { + return compiler; + } + + /** + * @see Options#getCompilerTargetVM + */ + @Override + public String getCompilerTargetVM() { + return compilerTargetVM; + } + + /** + * @see Options#getCompilerSourceVM + */ + @Override + public String getCompilerSourceVM() { + return compilerSourceVM; + } + + /** + * Java compiler class to use. + */ + @Override + public String getCompilerClassName() { + return compilerClassName; + } + + @Override + public boolean getErrorOnUseBeanInvalidClassAttribute() { + return errorOnUseBeanInvalidClassAttribute; + } + + public void setErrorOnUseBeanInvalidClassAttribute(boolean b) { + errorOnUseBeanInvalidClassAttribute = b; + } + + @Override + public TldCache getTldCache() { + return tldCache; + } + + public void setTldCache(TldCache tldCache) { + this.tldCache = tldCache; + } + + @Override + public String getJavaEncoding() { + return javaEncoding; + } + + @Override + public boolean getFork() { + return fork; + } + + @Override + public JspConfig getJspConfig() { + return jspConfig; + } + + @Override + public TagPluginManager getTagPluginManager() { + return tagPluginManager; + } + + @Override + public boolean isCaching() { + return false; + } + + @Override + public Map getCache() { + return null; + } + + /** + * Should we include a source fragment in exception messages, which could be displayed + * to the developer ? + */ + @Override + public boolean getDisplaySourceFragment() { + return displaySourceFragment; + } + + /** + * Should jsps be unloaded if to many are loaded? + * If set to a value greater than 0 eviction of jsps is started. Default: -1 + */ + @Override + public int getMaxLoadedJsps() { + return maxLoadedJsps; + } + + /** + * Should any jsps be unloaded when being idle for this time in seconds? + * If set to a value greater than 0 eviction of jsps is started. Default: -1 + */ + @Override + public int getJspIdleTimeout() { + return jspIdleTimeout; + } + + @Override + public boolean getStrictQuoteEscaping() { + return strictQuoteEscaping; + } + + @Override + public String getVariableForExpressionFactory() { + return variableForExpressionFactory; + } + + @Override + public String getVariableForInstanceManager() { + return variableForInstanceManager; + } + + @Override + public boolean getPoolTagsWithExtends() { + return poolTagsWithExtends; + } + + @Override + public boolean getStrictGetProperty() { + return strictGetProperty; + } + + @Override + public boolean getStrictWhitespace() { + return strictWhitespace; + } + + @Override + public String getJspServletBase() { + return jspServletBase; + } + + @Override + public String getServiceMethodName() { + return serviceMethodName; + } + + @Override + public String getServletClasspathAttribute() { + return servletClasspathAttribute; + } + + @Override + public String getJspPrecompilationQueryParameter() { + return jspPrecompilationQueryParameter; + } + + @Override + public String getGeneratedJspPackageName() { + return generatedJspPackageName; + } + + @Override + public String getGeneratedTagFilePackageName() { + return generatedTagFilePackageName; + } + + @Override + public String getTempVariableNamePrefix() { + return tempVariableNamePrefix; + } + + @Override + public boolean getUseInstanceManagerForTags() { + return useInstanceManagerForTags; + } + + /** + * Create an EmbeddedServletOptions object using data available from + * ServletConfig and ServletContext. + * @param config The Servlet config + * @param context The Servlet context + */ + public EmbeddedServletOptions(ServletConfig config, ServletContext context) { + + Enumeration enumeration=config.getInitParameterNames(); + while( enumeration.hasMoreElements() ) { + String k=enumeration.nextElement(); + String v=config.getInitParameter( k ); + setProperty( k, v); + } + + String keepgen = config.getInitParameter("keepgenerated"); + if (keepgen != null) { + if (keepgen.equalsIgnoreCase("true")) { + this.keepGenerated = true; + } else if (keepgen.equalsIgnoreCase("false")) { + this.keepGenerated = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.keepgen")); + } + } + } + + + String trimsp = config.getInitParameter("trimSpaces"); + if (trimsp != null) { + try { + trimSpaces = TrimSpacesOption.valueOf(trimsp.toUpperCase()); + } catch (IllegalArgumentException iae) { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.trimspaces"), iae); + } + } + } + + this.isPoolingEnabled = true; + String poolingEnabledParam = config.getInitParameter("enablePooling"); + if (poolingEnabledParam != null + && !poolingEnabledParam.equalsIgnoreCase("true")) { + if (poolingEnabledParam.equalsIgnoreCase("false")) { + this.isPoolingEnabled = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.enablePooling")); + } + } + } + + String mapFile = config.getInitParameter("mappedfile"); + if (mapFile != null) { + if (mapFile.equalsIgnoreCase("true")) { + this.mappedFile = true; + } else if (mapFile.equalsIgnoreCase("false")) { + this.mappedFile = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.mappedFile")); + } + } + } + + String debugInfo = config.getInitParameter("classdebuginfo"); + if (debugInfo != null) { + if (debugInfo.equalsIgnoreCase("true")) { + this.classDebugInfo = true; + } else if (debugInfo.equalsIgnoreCase("false")) { + this.classDebugInfo = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.classDebugInfo")); + } + } + } + + String checkInterval = config.getInitParameter("checkInterval"); + if (checkInterval != null) { + try { + this.checkInterval = Integer.parseInt(checkInterval); + } catch(NumberFormatException ex) { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.checkInterval")); + } + } + } + + String modificationTestInterval = config.getInitParameter("modificationTestInterval"); + if (modificationTestInterval != null) { + try { + this.modificationTestInterval = Integer.parseInt(modificationTestInterval); + } catch(NumberFormatException ex) { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.modificationTestInterval")); + } + } + } + + String recompileOnFail = config.getInitParameter("recompileOnFail"); + if (recompileOnFail != null) { + if (recompileOnFail.equalsIgnoreCase("true")) { + this.recompileOnFail = true; + } else if (recompileOnFail.equalsIgnoreCase("false")) { + this.recompileOnFail = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.recompileOnFail")); + } + } + } + String development = config.getInitParameter("development"); + if (development != null) { + if (development.equalsIgnoreCase("true")) { + this.development = true; + } else if (development.equalsIgnoreCase("false")) { + this.development = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.development")); + } + } + } + + String suppressSmap = config.getInitParameter("suppressSmap"); + if (suppressSmap != null) { + if (suppressSmap.equalsIgnoreCase("true")) { + isSmapSuppressed = true; + } else if (suppressSmap.equalsIgnoreCase("false")) { + isSmapSuppressed = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.suppressSmap")); + } + } + } + + String dumpSmap = config.getInitParameter("dumpSmap"); + if (dumpSmap != null) { + if (dumpSmap.equalsIgnoreCase("true")) { + isSmapDumped = true; + } else if (dumpSmap.equalsIgnoreCase("false")) { + isSmapDumped = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.dumpSmap")); + } + } + } + + String genCharArray = config.getInitParameter("genStringAsCharArray"); + if (genCharArray != null) { + if (genCharArray.equalsIgnoreCase("true")) { + genStringAsCharArray = true; + } else if (genCharArray.equalsIgnoreCase("false")) { + genStringAsCharArray = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.genchararray")); + } + } + } + + String errBeanClass = config.getInitParameter("errorOnUseBeanInvalidClassAttribute"); + if (errBeanClass != null) { + if (errBeanClass.equalsIgnoreCase("true")) { + errorOnUseBeanInvalidClassAttribute = true; + } else if (errBeanClass.equalsIgnoreCase("false")) { + errorOnUseBeanInvalidClassAttribute = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.errBean")); + } + } + } + + String classpath = config.getInitParameter("classpath"); + if (classpath != null) { + this.classpath = classpath; + } + + /* + * scratchdir + */ + String dir = config.getInitParameter("scratchdir"); + if (dir != null && Constants.IS_SECURITY_ENABLED) { + log.info(Localizer.getMessage("jsp.info.ignoreSetting", "scratchdir", dir)); + dir = null; + } + if (dir != null) { + scratchDir = new File(dir); + } else { + scratchDir = (File) context.getAttribute(ServletContext.TEMPDIR); + } + if (scratchDir == null) { + log.fatal(Localizer.getMessage("jsp.error.no.scratch.dir")); + return; + } + + if (!(scratchDir.exists() && scratchDir.canRead() && + scratchDir.canWrite() && scratchDir.isDirectory())) { + log.fatal(Localizer.getMessage("jsp.error.bad.scratch.dir", + scratchDir.getAbsolutePath())); + } + + this.compiler = config.getInitParameter("compiler"); + + String compilerTargetVM = config.getInitParameter("compilerTargetVM"); + if(compilerTargetVM != null) { + this.compilerTargetVM = compilerTargetVM; + } + + String compilerSourceVM = config.getInitParameter("compilerSourceVM"); + if(compilerSourceVM != null) { + this.compilerSourceVM = compilerSourceVM; + } + + String javaEncoding = config.getInitParameter("javaEncoding"); + if (javaEncoding != null) { + this.javaEncoding = javaEncoding; + } + + String compilerClassName = config.getInitParameter("compilerClassName"); + if (compilerClassName != null) { + this.compilerClassName = compilerClassName; + } + + String fork = config.getInitParameter("fork"); + if (fork != null) { + if (fork.equalsIgnoreCase("true")) { + this.fork = true; + } else if (fork.equalsIgnoreCase("false")) { + this.fork = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.fork")); + } + } + } + + String xpoweredBy = config.getInitParameter("xpoweredBy"); + if (xpoweredBy != null) { + if (xpoweredBy.equalsIgnoreCase("true")) { + this.xpoweredBy = true; + } else if (xpoweredBy.equalsIgnoreCase("false")) { + this.xpoweredBy = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.xpoweredBy")); + } + } + } + + String displaySourceFragment = config.getInitParameter("displaySourceFragment"); + if (displaySourceFragment != null) { + if (displaySourceFragment.equalsIgnoreCase("true")) { + this.displaySourceFragment = true; + } else if (displaySourceFragment.equalsIgnoreCase("false")) { + this.displaySourceFragment = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.displaySourceFragment")); + } + } + } + + String maxLoadedJsps = config.getInitParameter("maxLoadedJsps"); + if (maxLoadedJsps != null) { + try { + this.maxLoadedJsps = Integer.parseInt(maxLoadedJsps); + } catch(NumberFormatException ex) { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.maxLoadedJsps", ""+this.maxLoadedJsps)); + } + } + } + + String jspIdleTimeout = config.getInitParameter("jspIdleTimeout"); + if (jspIdleTimeout != null) { + try { + this.jspIdleTimeout = Integer.parseInt(jspIdleTimeout); + } catch(NumberFormatException ex) { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.jspIdleTimeout", ""+this.jspIdleTimeout)); + } + } + } + + String strictQuoteEscaping = config.getInitParameter("strictQuoteEscaping"); + if (strictQuoteEscaping != null) { + if (strictQuoteEscaping.equalsIgnoreCase("true")) { + this.strictQuoteEscaping = true; + } else if (strictQuoteEscaping.equalsIgnoreCase("false")) { + this.strictQuoteEscaping = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.strictQuoteEscaping")); + } + } + } + + String quoteAttributeEL = config.getInitParameter("quoteAttributeEL"); + if (quoteAttributeEL != null) { + if (quoteAttributeEL.equalsIgnoreCase("true")) { + this.quoteAttributeEL = true; + } else if (quoteAttributeEL.equalsIgnoreCase("false")) { + this.quoteAttributeEL = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.quoteAttributeEL")); + } + } + } + + String variableForExpressionFactory = config.getInitParameter("variableForExpressionFactory"); + if (variableForExpressionFactory != null) { + this.variableForExpressionFactory = variableForExpressionFactory; + } + + String variableForInstanceManager = config.getInitParameter("variableForInstanceManager"); + if (variableForInstanceManager != null) { + this.variableForInstanceManager = variableForInstanceManager; + } + + String poolTagsWithExtends = config.getInitParameter("poolTagsWithExtends"); + if (poolTagsWithExtends != null) { + if (poolTagsWithExtends.equalsIgnoreCase("true")) { + this.poolTagsWithExtends = true; + } else if (poolTagsWithExtends.equalsIgnoreCase("false")) { + this.poolTagsWithExtends = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.poolTagsWithExtends")); + } + } + } + + String strictGetProperty = config.getInitParameter("strictGetProperty"); + if (strictGetProperty != null) { + if (strictGetProperty.equalsIgnoreCase("true")) { + this.strictGetProperty = true; + } else if (strictGetProperty.equalsIgnoreCase("false")) { + this.strictGetProperty = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.strictGetProperty")); + } + } + } + + String strictWhitespace = config.getInitParameter("strictWhitespace"); + if (strictWhitespace != null) { + if (strictWhitespace.equalsIgnoreCase("true")) { + this.strictWhitespace = true; + } else if (strictWhitespace.equalsIgnoreCase("false")) { + this.strictWhitespace = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.strictWhitespace")); + } + } + } + + String jspServletBase = config.getInitParameter("jspServletBase"); + if (jspServletBase != null) { + this.jspServletBase = jspServletBase; + } + + String serviceMethodName = config.getInitParameter("serviceMethodName"); + if (serviceMethodName != null) { + this.serviceMethodName = serviceMethodName; + } + + String servletClasspathAttribute = config.getInitParameter("servletClasspathAttribute"); + if (servletClasspathAttribute != null) { + this.servletClasspathAttribute = servletClasspathAttribute; + } + + String jspPrecompilationQueryParameter = config.getInitParameter("jspPrecompilationQueryParameter"); + if (jspPrecompilationQueryParameter != null) { + this.jspPrecompilationQueryParameter = jspPrecompilationQueryParameter; + } + + String generatedJspPackageName = config.getInitParameter("generatedJspPackageName"); + if (generatedJspPackageName != null) { + this.generatedJspPackageName = generatedJspPackageName; + } + + String generatedTagFilePackageName = config.getInitParameter("generatedTagFilePackageName"); + if (generatedTagFilePackageName != null) { + this.generatedTagFilePackageName = generatedTagFilePackageName; + } + + String tempVariableNamePrefix = config.getInitParameter("tempVariableNamePrefix"); + if (tempVariableNamePrefix != null) { + this.tempVariableNamePrefix = tempVariableNamePrefix; + } + + String useInstanceManagerForTags = config.getInitParameter("useInstanceManagerForTags"); + if (useInstanceManagerForTags != null) { + if (useInstanceManagerForTags.equalsIgnoreCase("true")) { + this.useInstanceManagerForTags = true; + } else if (useInstanceManagerForTags.equalsIgnoreCase("false")) { + this.useInstanceManagerForTags = false; + } else { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jsp.warning.useInstanceManagerForTags")); + } + } + } + + // Setup the global Tag Libraries location cache for this + // web-application. + tldCache = TldCache.getInstance(context); + + // Setup the jsp config info for this web app. + jspConfig = new JspConfig(context); + + // Create a Tag plugin instance + tagPluginManager = new TagPluginManager(context); + } + +} + diff --git a/java/org/apache/jasper/JasperException.java b/java/org/apache/jasper/JasperException.java new file mode 100644 index 0000000..46e0867 --- /dev/null +++ b/java/org/apache/jasper/JasperException.java @@ -0,0 +1,50 @@ +/* + * 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.jasper; + +/** + * Base class for all exceptions generated by the JSP engine. Makes it + * convenient to catch just this at the top-level. + * + * @author Anil K. Vijendran + */ +public class JasperException extends jakarta.servlet.ServletException { + + private static final long serialVersionUID = 1L; + + public JasperException(String reason) { + super(reason); + } + + /** + * Creates a JasperException with the embedded exception and the reason for + * throwing a JasperException. + * @param reason The exception message + * @param exception The root cause + */ + public JasperException(String reason, Throwable exception) { + super(reason, exception); + } + + /** + * Creates a JasperException with the embedded exception. + * @param exception The root cause + */ + public JasperException(Throwable exception) { + super(exception); + } +} diff --git a/java/org/apache/jasper/JspC.java b/java/org/apache/jasper/JspC.java new file mode 100644 index 0000000..d20a4f1 --- /dev/null +++ b/java/org/apache/jasper/JspC.java @@ -0,0 +1,1797 @@ +/* + * 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.jasper; + +import java.io.BufferedReader; +import java.io.CharArrayWriter; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import jakarta.servlet.jsp.JspFactory; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; + +import org.apache.jasper.compiler.Compiler; +import org.apache.jasper.compiler.JspConfig; +import org.apache.jasper.compiler.JspRuntimeContext; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.compiler.TagPluginManager; +import org.apache.jasper.compiler.TldCache; +import org.apache.jasper.runtime.JspFactoryImpl; +import org.apache.jasper.servlet.JspCServletContext; +import org.apache.jasper.servlet.TldScanner; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tools.ant.AntClassLoader; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.util.FileUtils; +import org.xml.sax.SAXException; + +/** + * Shell for the jspc compiler. Handles all options associated with the + * command line and creates compilation contexts which it then compiles + * according to the specified options. + * + * This version can process files from a _single_ webapp at once, i.e. + * a single docbase can be specified. + * + * It can be used as an Ant task using: + *

    + *   <taskdef classname="org.apache.jasper.JspC" name="jasper" >
    + *      <classpath>
    + *          <pathelement location="${java.home}/../lib/tools.jar"/>
    + *          <fileset dir="${ENV.CATALINA_HOME}/lib">
    + *              <include name="*.jar"/>
    + *          </fileset>
    + *          <path refid="myjars"/>
    + *       </classpath>
    + *  </taskdef>
    + *
    + *  <jasper verbose="0"
    + *           package="my.package"
    + *           uriroot="${webapps.dir}/${webapp.name}"
    + *           webXmlFragment="${build.dir}/generated_web.xml"
    + *           outputDir="${webapp.dir}/${webapp.name}/WEB-INF/src/my/package" />
    + * 
    + * + * @author Danno Ferrin + * @author Pierre Delisle + * @author Costin Manolache + * @author Yoav Shapira + */ +public class JspC extends Task implements Options { + + static { + // the Validator uses this to access the EL ExpressionFactory + JspFactory.setDefaultFactory(new JspFactoryImpl()); + } + + // Logger + private static final Log log = LogFactory.getLog(JspC.class); + + protected static final String SWITCH_VERBOSE = "-v"; + protected static final String SWITCH_HELP = "-help"; + protected static final String SWITCH_OUTPUT_DIR = "-d"; + protected static final String SWITCH_PACKAGE_NAME = "-p"; + protected static final String SWITCH_CACHE = "-cache"; + protected static final String SWITCH_CLASS_NAME = "-c"; + protected static final String SWITCH_FULL_STOP = "--"; + protected static final String SWITCH_COMPILE = "-compile"; + protected static final String SWITCH_FAIL_FAST = "-failFast"; + protected static final String SWITCH_SOURCE = "-source"; + protected static final String SWITCH_TARGET = "-target"; + protected static final String SWITCH_URI_BASE = "-uribase"; + protected static final String SWITCH_URI_ROOT = "-uriroot"; + protected static final String SWITCH_FILE_WEBAPP = "-webapp"; + protected static final String SWITCH_WEBAPP_INC = "-webinc"; + protected static final String SWITCH_WEBAPP_FRG = "-webfrg"; + protected static final String SWITCH_WEBAPP_XML = "-webxml"; + protected static final String SWITCH_WEBAPP_XML_ENCODING = "-webxmlencoding"; + protected static final String SWITCH_ADD_WEBAPP_XML_MAPPINGS = "-addwebxmlmappings"; + protected static final String SWITCH_MAPPED = "-mapped"; + protected static final String SWITCH_XPOWERED_BY = "-xpoweredBy"; + protected static final String SWITCH_TRIM_SPACES = "-trimSpaces"; + protected static final String SWITCH_CLASSPATH = "-classpath"; + protected static final String SWITCH_DIE = "-die"; + protected static final String SWITCH_POOLING = "-poolingEnabled"; + protected static final String SWITCH_ENCODING = "-javaEncoding"; + protected static final String SWITCH_SMAP = "-smap"; + protected static final String SWITCH_DUMP_SMAP = "-dumpsmap"; + protected static final String SWITCH_VALIDATE_TLD = "-validateTld"; + protected static final String SWITCH_VALIDATE_XML = "-validateXml"; + protected static final String SWITCH_NO_BLOCK_EXTERNAL = "-no-blockExternal"; + protected static final String SWITCH_NO_STRICT_QUOTE_ESCAPING = "-no-strictQuoteEscaping"; + protected static final String SWITCH_QUOTE_ATTRIBUTE_EL = "-quoteAttributeEL"; + protected static final String SWITCH_NO_QUOTE_ATTRIBUTE_EL = "-no-quoteAttributeEL"; + protected static final String SWITCH_THREAD_COUNT = "-threadCount"; + protected static final String SHOW_SUCCESS ="-s"; + protected static final String LIST_ERRORS = "-l"; + protected static final int INC_WEBXML = 10; + protected static final int FRG_WEBXML = 15; + protected static final int ALL_WEBXML = 20; + protected static final int DEFAULT_DIE_LEVEL = 1; + protected static final int NO_DIE_LEVEL = 0; + protected static final Set insertBefore = new HashSet<>(); + + static { + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + insertBefore.add(""); + } + + protected String classPath = null; + protected ClassLoader loader = null; + protected TrimSpacesOption trimSpaces = TrimSpacesOption.FALSE; + protected boolean genStringAsCharArray = false; + protected boolean validateTld; + protected boolean validateXml; + protected boolean blockExternal = true; + protected boolean strictQuoteEscaping = true; + protected boolean quoteAttributeEL = true; + protected boolean xpoweredBy; + protected boolean mappedFile = false; + protected boolean poolingEnabled = true; + protected File scratchDir; + + protected String targetPackage; + protected String targetClassName; + protected String uriBase; + protected String uriRoot; + protected int dieLevel; + protected boolean helpNeeded = false; + protected boolean compile = false; + protected boolean failFast = false; + protected boolean smapSuppressed = true; + protected boolean smapDumped = false; + protected boolean caching = true; + protected final Map cache = new HashMap<>(); + + protected String compiler = null; + + protected String compilerTargetVM = "11"; + protected String compilerSourceVM = "11"; + + protected boolean classDebugInfo = true; + + /** + * Throw an exception if there's a compilation error, or swallow it. + * Default is true to preserve old behavior. + */ + protected boolean failOnError = true; + + /** + * Should a separate process be forked to perform the compilation? + */ + private boolean fork = false; + + /** + * The file extensions to be handled as JSP files. + * Default list is .jsp and .jspx. + */ + protected List extensions; + + /** + * The pages. + */ + protected final List pages = new ArrayList<>(); + + /** + * Needs better documentation, this data member does. + * True by default. + */ + protected boolean errorOnUseBeanInvalidClassAttribute = true; + + /** + * The java file encoding. Default + * is UTF-8. Added per bugzilla 19622. + */ + protected String javaEncoding = "UTF-8"; + + /** The number of threads to use; default is one per core */ + protected int threadCount = Runtime.getRuntime().availableProcessors(); + + // Generation of web.xml fragments + protected String webxmlFile; + protected int webxmlLevel; + protected String webxmlEncoding = "UTF-8"; + protected boolean addWebXmlMappings = false; + + protected Writer mapout; + protected CharArrayWriter servletout; + protected CharArrayWriter mappingout; + + /** + * The servlet context. + */ + protected JspCServletContext context; + + /** + * The runtime context. + * Maintain a dummy JspRuntimeContext for compiling tag files. + */ + protected JspRuntimeContext rctxt; + + /** + * Cache for the TLD locations + */ + protected TldCache tldCache = null; + + protected JspConfig jspConfig = null; + protected TagPluginManager tagPluginManager = null; + + protected TldScanner scanner = null; + + protected boolean verbose = false; + protected boolean listErrors = false; + protected boolean showSuccess = false; + protected int argPos; + protected boolean fullstop = false; + protected String args[]; + + public static void main(String arg[]) { + if (arg.length == 0) { + System.out.println(Localizer.getMessage("jspc.usage")); + } else { + JspC jspc = new JspC(); + try { + jspc.setArgs(arg); + if (jspc.helpNeeded) { + System.out.println(Localizer.getMessage("jspc.usage")); + } else { + jspc.execute(); + } + } catch (JasperException | BuildException e) { + System.err.println(e); + if (jspc.dieLevel != NO_DIE_LEVEL) { + System.exit(jspc.dieLevel); + } + } + } + } + + /** + * Apply command-line arguments. + * @param arg The arguments + * @throws JasperException JSPC error + */ + public void setArgs(String[] arg) throws JasperException { + args = arg; + String tok; + + dieLevel = NO_DIE_LEVEL; + + while ((tok = nextArg()) != null) { + if (tok.equals(SWITCH_VERBOSE)) { + verbose = true; + showSuccess = true; + listErrors = true; + } else if (tok.equals(SWITCH_OUTPUT_DIR)) { + tok = nextArg(); + setOutputDir( tok ); + } else if (tok.equals(SWITCH_PACKAGE_NAME)) { + targetPackage = nextArg(); + } else if (tok.equals(SWITCH_COMPILE)) { + compile=true; + } else if (tok.equals(SWITCH_FAIL_FAST)) { + failFast = true; + } else if (tok.equals(SWITCH_CLASS_NAME)) { + targetClassName = nextArg(); + } else if (tok.equals(SWITCH_URI_BASE)) { + uriBase=nextArg(); + } else if (tok.equals(SWITCH_URI_ROOT)) { + setUriroot( nextArg()); + } else if (tok.equals(SWITCH_FILE_WEBAPP)) { + setUriroot( nextArg()); + } else if ( tok.equals( SHOW_SUCCESS ) ) { + showSuccess = true; + } else if ( tok.equals( LIST_ERRORS ) ) { + listErrors = true; + } else if (tok.equals(SWITCH_WEBAPP_INC)) { + webxmlFile = nextArg(); + if (webxmlFile != null) { + webxmlLevel = INC_WEBXML; + } + } else if (tok.equals(SWITCH_WEBAPP_FRG)) { + webxmlFile = nextArg(); + if (webxmlFile != null) { + webxmlLevel = FRG_WEBXML; + } + } else if (tok.equals(SWITCH_WEBAPP_XML)) { + webxmlFile = nextArg(); + if (webxmlFile != null) { + webxmlLevel = ALL_WEBXML; + } + } else if (tok.equals(SWITCH_WEBAPP_XML_ENCODING)) { + setWebXmlEncoding(nextArg()); + } else if (tok.equals(SWITCH_ADD_WEBAPP_XML_MAPPINGS)) { + setAddWebXmlMappings(true); + } else if (tok.equals(SWITCH_MAPPED)) { + mappedFile = true; + } else if (tok.equals(SWITCH_XPOWERED_BY)) { + xpoweredBy = true; + } else if (tok.equals(SWITCH_TRIM_SPACES)) { + tok = nextArg(); + if (TrimSpacesOption.SINGLE.toString().equalsIgnoreCase(tok)) { + setTrimSpaces(TrimSpacesOption.SINGLE); + } else { + setTrimSpaces(TrimSpacesOption.TRUE); + argPos--; + } + } else if (tok.equals(SWITCH_CACHE)) { + tok = nextArg(); + if ("false".equals(tok)) { + caching = false; + } else { + caching = true; + } + } else if (tok.equals(SWITCH_CLASSPATH)) { + setClassPath(nextArg()); + } else if (tok.startsWith(SWITCH_DIE)) { + try { + dieLevel = Integer.parseInt( + tok.substring(SWITCH_DIE.length())); + } catch (NumberFormatException nfe) { + dieLevel = DEFAULT_DIE_LEVEL; + } + } else if (tok.equals(SWITCH_HELP)) { + helpNeeded = true; + } else if (tok.equals(SWITCH_POOLING)) { + tok = nextArg(); + if ("false".equals(tok)) { + poolingEnabled = false; + } else { + poolingEnabled = true; + } + } else if (tok.equals(SWITCH_ENCODING)) { + setJavaEncoding(nextArg()); + } else if (tok.equals(SWITCH_SOURCE)) { + setCompilerSourceVM(nextArg()); + } else if (tok.equals(SWITCH_TARGET)) { + setCompilerTargetVM(nextArg()); + } else if (tok.equals(SWITCH_SMAP)) { + smapSuppressed = false; + } else if (tok.equals(SWITCH_DUMP_SMAP)) { + smapDumped = true; + } else if (tok.equals(SWITCH_VALIDATE_TLD)) { + setValidateTld(true); + } else if (tok.equals(SWITCH_VALIDATE_XML)) { + setValidateXml(true); + } else if (tok.equals(SWITCH_NO_BLOCK_EXTERNAL)) { + setBlockExternal(false); + } else if (tok.equals(SWITCH_NO_STRICT_QUOTE_ESCAPING)) { + setStrictQuoteEscaping(false); + } else if (tok.equals(SWITCH_QUOTE_ATTRIBUTE_EL)) { + setQuoteAttributeEL(true); + } else if (tok.equals(SWITCH_NO_QUOTE_ATTRIBUTE_EL)) { + setQuoteAttributeEL(false); + } else if (tok.equals(SWITCH_THREAD_COUNT)) { + setThreadCount(nextArg()); + } else { + if (tok.startsWith("-")) { + throw new JasperException(Localizer.getMessage("jspc.error.unknownOption", tok)); + } + if (!fullstop) { + argPos--; + } + // Start treating the rest as JSP Pages + break; + } + } + + // Add all extra arguments to the list of files + while( true ) { + String file = nextFile(); + if( file==null ) { + break; + } + pages.add( file ); + } + } + + /** + * In JspC this always returns true. + * {@inheritDoc} + */ + @Override + public boolean getKeepGenerated() { + // isn't this why we are running jspc? + return true; + } + + @Override + public TrimSpacesOption getTrimSpaces() { + return trimSpaces; + } + + public void setTrimSpaces(TrimSpacesOption trimSpaces) { + this.trimSpaces = trimSpaces; + } + + /** + * Sets the option to control handling of template text that consists + * entirely of whitespace. + * + * @param ts New value + */ + public void setTrimSpaces(String ts) { + this.trimSpaces = TrimSpacesOption.valueOf(ts); + } + + /* + * Backwards compatibility with 8.5.x + */ + public void setTrimSpaces(boolean trimSpaces) { + if (trimSpaces) { + setTrimSpaces(TrimSpacesOption.TRUE); + } else { + setTrimSpaces(TrimSpacesOption.FALSE); + } + } + + @Override + public boolean isPoolingEnabled() { + return poolingEnabled; + } + + /** + * Sets the option to enable the tag handler pooling. + * @param poolingEnabled New value + */ + public void setPoolingEnabled(boolean poolingEnabled) { + this.poolingEnabled = poolingEnabled; + } + + @Override + public boolean isXpoweredBy() { + return xpoweredBy; + } + + /** + * Sets the option to enable generation of X-Powered-By response header. + * @param xpoweredBy New value + */ + public void setXpoweredBy(boolean xpoweredBy) { + this.xpoweredBy = xpoweredBy; + } + + /** + * In JspC this always returns true. + * {@inheritDoc} + */ + @Override + public boolean getDisplaySourceFragment() { + return true; + } + + @Override + public int getMaxLoadedJsps() { + return -1; + } + + @Override + public int getJspIdleTimeout() { + return -1; + } + + @Override + public boolean getErrorOnUseBeanInvalidClassAttribute() { + return errorOnUseBeanInvalidClassAttribute; + } + + /** + * Sets the option to issue a compilation error if the class attribute + * specified in useBean action is invalid. + * @param b New value + */ + public void setErrorOnUseBeanInvalidClassAttribute(boolean b) { + errorOnUseBeanInvalidClassAttribute = b; + } + + @Override + public boolean getMappedFile() { + return mappedFile; + } + + public void setMappedFile(boolean b) { + mappedFile = b; + } + + /** + * Sets the option to include debug information in compiled class. + * @param b New value + */ + public void setClassDebugInfo( boolean b ) { + classDebugInfo=b; + } + + @Override + public boolean getClassDebugInfo() { + // compile with debug info + return classDebugInfo; + } + + @Override + public boolean isCaching() { + return caching; + } + + /** + * Sets the option to enable caching. + * @param caching New value + * + * @see Options#isCaching() + */ + public void setCaching(boolean caching) { + this.caching = caching; + } + + @Override + public Map getCache() { + return cache; + } + + /** + * In JspC this always returns 0. + * {@inheritDoc} + */ + @Override + public int getCheckInterval() { + return 0; + } + + /** + * In JspC this always returns 0. + * {@inheritDoc} + */ + @Override + public int getModificationTestInterval() { + return 0; + } + + + /** + * In JspC this always returns false. + * {@inheritDoc} + */ + @Override + public boolean getRecompileOnFail() { + return false; + } + + + /** + * In JspC this always returns false. + * {@inheritDoc} + */ + @Override + public boolean getDevelopment() { + return false; + } + + @Override + public boolean isSmapSuppressed() { + return smapSuppressed; + } + + /** + * Sets smapSuppressed flag. + * @param smapSuppressed New value + */ + public void setSmapSuppressed(boolean smapSuppressed) { + this.smapSuppressed = smapSuppressed; + } + + @Override + public boolean isSmapDumped() { + return smapDumped; + } + + /** + * Sets smapDumped flag. + * @param smapDumped New value + * + * @see Options#isSmapDumped() + */ + public void setSmapDumped(boolean smapDumped) { + this.smapDumped = smapDumped; + } + + + /** + * Determines whether text strings are to be generated as char arrays, + * which improves performance in some cases. + * + * @param genStringAsCharArray true if text strings are to be generated as + * char arrays, false otherwise + */ + public void setGenStringAsCharArray(boolean genStringAsCharArray) { + this.genStringAsCharArray = genStringAsCharArray; + } + + @Override + public boolean genStringAsCharArray() { + return genStringAsCharArray; + } + + @Override + public File getScratchDir() { + return scratchDir; + } + + @Override + public String getCompiler() { + return compiler; + } + + /** + * Sets the option to determine what compiler to use. + * @param c New value + * + * @see Options#getCompiler() + */ + public void setCompiler(String c) { + compiler=c; + } + + @Override + public String getCompilerClassName() { + return null; + } + + @Override + public String getCompilerTargetVM() { + return compilerTargetVM; + } + + /** + * Sets the compiler target VM. + * @param vm New value + * + * @see Options#getCompilerTargetVM() + */ + public void setCompilerTargetVM(String vm) { + compilerTargetVM = vm; + } + + @Override + public String getCompilerSourceVM() { + return compilerSourceVM; + } + + /** + * Sets the compiler source VM. + * @param vm New value + * + * @see Options#getCompilerSourceVM() + */ + public void setCompilerSourceVM(String vm) { + compilerSourceVM = vm; + } + + @Override + public TldCache getTldCache() { + return tldCache; + } + + /** + * Returns the encoding to use for + * java files. The default is UTF-8. + * + * @return String The encoding + */ + @Override + public String getJavaEncoding() { + return javaEncoding; + } + + /** + * Sets the encoding to use for + * java files. + * + * @param encodingName The name, e.g. "UTF-8" + */ + public void setJavaEncoding(String encodingName) { + javaEncoding = encodingName; + } + + @Override + public boolean getFork() { + return fork; + } + + public void setFork(boolean fork) { + this.fork = fork; + } + + @Override + public String getClassPath() { + if( classPath != null ) { + return classPath; + } + return System.getProperty("java.class.path"); + } + + /** + * Sets the classpath used while compiling the servlets generated from JSP + * files + * @param s New value + */ + public void setClassPath(String s) { + classPath=s; + } + + /** + * Returns the list of file extensions + * that are treated as JSP files. + * + * @return The list of extensions + */ + public List getExtensions() { + return extensions; + } + + /** + * Adds the given file extension to the + * list of extensions handled as JSP files. + * + * @param extension The extension to add, e.g. "myjsp" + */ + protected void addExtension(final String extension) { + if(extension != null) { + if(extensions == null) { + extensions = new ArrayList<>(); + } + + extensions.add(extension); + } + } + + /** + * Base dir for the webapp. Used to generate class names and resolve + * includes. + * @param s New value + */ + public void setUriroot( String s ) { + if (s == null) { + uriRoot = null; + return; + } + try { + uriRoot = resolveFile(s).getCanonicalPath(); + } catch( Exception ex ) { + uriRoot = s; + } + } + + /** + * Parses comma-separated list of JSP files to be processed. If the argument + * is null, nothing is done. + * + *

    Each file is interpreted relative to uriroot, unless it is absolute, + * in which case it must start with uriroot.

    + * + * @param jspFiles Comma-separated list of JSP files to be processed + */ + public void setJspFiles(final String jspFiles) { + if(jspFiles == null) { + return; + } + + StringTokenizer tok = new StringTokenizer(jspFiles, ","); + while (tok.hasMoreTokens()) { + pages.add(tok.nextToken()); + } + } + + /** + * Sets the compile flag. + * + * @param b Flag value + */ + public void setCompile( final boolean b ) { + compile = b; + } + + /** + * Sets the verbosity level. The actual number doesn't + * matter: if it's greater than zero, the verbose flag will + * be true. + * + * @param level Positive means verbose + */ + public void setVerbose( final int level ) { + if (level > 0) { + verbose = true; + showSuccess = true; + listErrors = true; + } + } + + public void setValidateTld( boolean b ) { + this.validateTld = b; + } + + public boolean isValidateTld() { + return validateTld; + } + + public void setValidateXml( boolean b ) { + this.validateXml = b; + } + + public boolean isValidateXml() { + return validateXml; + } + + public void setBlockExternal( boolean b ) { + this.blockExternal = b; + } + + public boolean isBlockExternal() { + return blockExternal; + } + + public void setStrictQuoteEscaping( boolean b ) { + this.strictQuoteEscaping = b; + } + + @Override + public boolean getStrictQuoteEscaping() { + return strictQuoteEscaping; + } + + public void setQuoteAttributeEL(boolean b) { + quoteAttributeEL = b; + } + + @Override + public boolean getQuoteAttributeEL() { + return quoteAttributeEL; + } + + public int getThreadCount() { + return threadCount; + } + + public void setThreadCount(String threadCount) { + if (threadCount == null) { + return; + } + int newThreadCount; + try { + if (threadCount.endsWith("C")) { + double factor = Double.parseDouble(threadCount.substring(0, threadCount.length() - 1)); + newThreadCount = (int) (factor * Runtime.getRuntime().availableProcessors()); + } else { + newThreadCount = Integer.parseInt(threadCount); + } + } catch (NumberFormatException e) { + throw new BuildException(Localizer.getMessage("jspc.error.parseThreadCount", threadCount)); + } + if (newThreadCount < 1) { + throw new BuildException(Localizer.getMessage( + "jspc.error.minThreadCount", Integer.valueOf(newThreadCount))); + } + this.threadCount = newThreadCount; + } + + public void setListErrors( boolean b ) { + listErrors = b; + } + + public void setOutputDir( String s ) { + if( s!= null ) { + scratchDir = resolveFile(s).getAbsoluteFile(); + } else { + scratchDir=null; + } + } + + /** + * Sets the package name to be used for the generated servlet classes. + * @param p New value + */ + public void setPackage( String p ) { + targetPackage=p; + } + + /** + * Class name of the generated file ( without package ). + * Can only be used if a single file is converted. + * XXX Do we need this feature ? + * @param p New value + */ + public void setClassName( String p ) { + targetClassName=p; + } + + /** + * File where we generate configuration with the class definitions to be + * included in a web.xml file. + * @param s New value + */ + public void setWebXmlInclude( String s ) { + webxmlFile=resolveFile(s).getAbsolutePath(); + webxmlLevel=INC_WEBXML; + } + + /** + * File where we generate a complete web-fragment.xml with the class + * definitions. + * @param s New value + */ + public void setWebFragmentXml( String s ) { + webxmlFile=resolveFile(s).getAbsolutePath(); + webxmlLevel=FRG_WEBXML; + } + + /** + * File where we generate a complete web.xml with the class definitions. + * @param s New value + */ + public void setWebXml( String s ) { + webxmlFile=resolveFile(s).getAbsolutePath(); + webxmlLevel=ALL_WEBXML; + } + + /** + * Sets the encoding to be used to read and write web.xml files. + * + *

    + * If not specified, defaults to UTF-8. + *

    + * + * @param encoding + * Encoding, e.g. "UTF-8". + */ + public void setWebXmlEncoding(String encoding) { + webxmlEncoding = encoding; + } + + /** + * Sets the option to merge generated web.xml fragment into the + * WEB-INF/web.xml file of the web application that we were processing. + * + * @param b + * true to merge the fragment into the existing + * web.xml file of the processed web application + * ({uriroot}/WEB-INF/web.xml), false to keep the + * generated web.xml fragment + */ + public void setAddWebXmlMappings(boolean b) { + addWebXmlMappings = b; + } + + /** + * Sets the option that throws an exception in case of a compilation error. + * @param b New value + */ + public void setFailOnError(final boolean b) { + failOnError = b; + } + + /** + * @return true if an exception will be thrown + * in case of a compilation error. + */ + public boolean getFailOnError() { + return failOnError; + } + + @Override + public JspConfig getJspConfig() { + return jspConfig; + } + + @Override + public TagPluginManager getTagPluginManager() { + return tagPluginManager; + } + + + /** + * {@inheritDoc} + *

    + * Hard-coded to {@code false} for pre-compiled code to enable repeatable + * builds. + */ + @Override + public boolean getGeneratedJavaAddTimestamp() { + return false; + } + + + /** + * Adds servlet declaration and mapping for the JSP page servlet to the + * generated web.xml fragment. + * + * @param file + * Context-relative path to the JSP file, e.g. + * /index.jsp + * @param clctxt + * Compilation context of the servlet + * @throws IOException An IO error occurred + */ + public void generateWebMapping( String file, JspCompilationContext clctxt ) + throws IOException + { + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jspc.generatingMapping", file, clctxt)); + } + + String className = clctxt.getServletClassName(); + String packageName = clctxt.getServletPackageName(); + + String thisServletName; + if (packageName.isEmpty()) { + thisServletName = className; + } else { + thisServletName = packageName + '.' + className; + } + + if (servletout != null) { + synchronized(servletout) { + servletout.write("\n \n "); + servletout.write(thisServletName); + servletout.write("\n "); + servletout.write(thisServletName); + servletout.write("\n \n"); + } + } + if (mappingout != null) { + synchronized(mappingout) { + mappingout.write("\n \n "); + mappingout.write(thisServletName); + mappingout.write("\n "); + mappingout.write(file.replace('\\', '/')); + mappingout.write("\n \n"); + } + } + } + + /** + * Include the generated web.xml inside the webapp's web.xml. + * @throws IOException An IO error occurred + */ + protected void mergeIntoWebXml() throws IOException { + + File webappBase = new File(uriRoot); + File webXml = new File(webappBase, "WEB-INF/web.xml"); + File webXml2 = new File(webappBase, "WEB-INF/web2.xml"); + String insertStartMarker = + Localizer.getMessage("jspc.webinc.insertStart"); + String insertEndMarker = + Localizer.getMessage("jspc.webinc.insertEnd"); + + try (BufferedReader reader = new BufferedReader(openWebxmlReader(webXml)); + BufferedReader fragmentReader = + new BufferedReader(openWebxmlReader(new File(webxmlFile))); + PrintWriter writer = new PrintWriter(openWebxmlWriter(webXml2))) { + + // Insert the and declarations + boolean inserted = false; + int current = reader.read(); + while (current > -1) { + if (current == '<') { + String element = getElement(reader); + if (!inserted && insertBefore.contains(element)) { + // Insert generated content here + writer.println(insertStartMarker); + while (true) { + String line = fragmentReader.readLine(); + if (line == null) { + writer.println(); + break; + } + writer.println(line); + } + writer.println(insertEndMarker); + writer.println(); + writer.write(element); + inserted = true; + } else if (element.equals(insertStartMarker)) { + // Skip the previous auto-generated content + while (true) { + current = reader.read(); + if (current < 0) { + throw new EOFException(); + } + if (current == '<') { + element = getElement(reader); + if (element.equals(insertEndMarker)) { + break; + } + } + } + current = reader.read(); + while (current == '\n' || current == '\r') { + current = reader.read(); + } + continue; + } else { + writer.write(element); + } + } else { + writer.write(current); + } + current = reader.read(); + } + } + + try (FileInputStream fis = new FileInputStream(webXml2); + FileOutputStream fos = new FileOutputStream(webXml)) { + + byte buf[] = new byte[512]; + while (true) { + int n = fis.read(buf); + if (n < 0) { + break; + } + fos.write(buf, 0, n); + } + } + + if(!webXml2.delete() && log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jspc.delete.fail", + webXml2.toString())); + } + + if (!(new File(webxmlFile)).delete() && log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jspc.delete.fail", webxmlFile)); + } + + } + + /* + * Assumes valid xml + */ + private String getElement(Reader reader) throws IOException { + StringBuilder result = new StringBuilder(); + result.append('<'); + + boolean done = false; + + while (!done) { + int current = reader.read(); + while (current != '>') { + if (current < 0) { + throw new EOFException(); + } + result.append((char) current); + current = reader.read(); + } + result.append((char) current); + + int len = result.length(); + if (len > 4 && result.substring(0, 4).equals("")) { + done = true; + } + } else { + done = true; + } + } + + + return result.toString(); + } + + protected void processFile(String file) throws JasperException { + + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jspc.processing", file)); + } + + ClassLoader originalClassLoader = null; + Thread currentThread = Thread.currentThread(); + + try { + // set up a scratch/output dir if none is provided + if (scratchDir == null) { + String temp = System.getProperty("java.io.tmpdir"); + if (temp == null) { + temp = ""; + } + scratchDir = new File(temp).getAbsoluteFile(); + } + + String jspUri=file.replace('\\','/'); + JspCompilationContext clctxt = new JspCompilationContext + ( jspUri, this, context, null, rctxt ); + + /* Override the defaults */ + if ((targetClassName != null) && (targetClassName.length() > 0)) { + clctxt.setServletClassName(targetClassName); + targetClassName = null; + } + if (targetPackage != null) { + clctxt.setBasePackageName(targetPackage); + } + + originalClassLoader = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(loader); + + clctxt.setClassLoader(loader); + clctxt.setClassPath(classPath); + + Compiler clc = clctxt.createCompiler(); + + // If compile is set, generate both .java and .class, if + // .jsp file is newer than .class file; + // Otherwise only generate .java, if .jsp file is newer than + // the .java file + if( clc.isOutDated(compile) ) { + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jspc.outdated", jspUri)); + } + + clc.compile(compile, true); + } + + // Generate mapping + generateWebMapping( file, clctxt ); + if ( showSuccess ) { + log.info(Localizer.getMessage("jspc.built", file)); + } + + } catch (JasperException je) { + Throwable rootCause = je; + while (rootCause instanceof JasperException + && ((JasperException) rootCause).getRootCause() != null) { + rootCause = ((JasperException) rootCause).getRootCause(); + } + if (rootCause != je) { + log.error(Localizer.getMessage("jspc.error.generalException", + file), + rootCause); + } + throw je; + } catch (Exception e) { + if ((e instanceof FileNotFoundException) && log.isWarnEnabled()) { + log.warn(Localizer.getMessage("jspc.error.fileDoesNotExist", + e.getMessage())); + } + throw new JasperException(e); + } finally { + if (originalClassLoader != null) { + currentThread.setContextClassLoader(originalClassLoader); + } + } + } + + + /** + * Locate all jsp files in the webapp. Used if no explicit jsps are + * specified. Scan is performed via the ServletContext and will include any + * JSPs located in resource JARs. + */ + public void scanFiles() { + // Make sure default extensions are always included + if ((getExtensions() == null) || (getExtensions().size() < 2)) { + addExtension("jsp"); + addExtension("jspx"); + } + + scanFilesInternal("/"); + } + + + private void scanFilesInternal(String input) { + Set paths = context.getResourcePaths(input); + for (String path : paths) { + if (path.endsWith("/")) { + scanFilesInternal(path); + } else if (jspConfig.isJspPage(path)) { + pages.add(path); + } else { + String ext = path.substring(path.lastIndexOf('.') + 1); + if (extensions.contains(ext)) { + pages.add(path); + } + } + } + } + + + /** + * Executes the compilation. + */ + @Override + public void execute() { + if(log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jspc.start", Integer.toString(pages.size()))); + } + + try { + if (uriRoot == null) { + if (pages.size() == 0) { + throw new JasperException(Localizer.getMessage("jsp.error.jspc.missingTarget")); + } + String firstJsp = pages.get(0); + File firstJspF = new File(firstJsp); + if (!firstJspF.exists()) { + throw new JasperException(Localizer.getMessage( + "jspc.error.fileDoesNotExist", firstJsp)); + } + locateUriRoot(firstJspF); + } + + if (uriRoot == null) { + throw new JasperException(Localizer.getMessage("jsp.error.jspc.no_uriroot")); + } + + File uriRootF = new File(uriRoot); + if (!uriRootF.isDirectory()) { + throw new JasperException(Localizer.getMessage("jsp.error.jspc.uriroot_not_dir")); + } + + if (loader == null) { + loader = initClassLoader(); + } + if (context == null) { + initServletContext(loader); + } + + // No explicit pages, we'll process all .jsp in the webapp + if (pages.size() == 0) { + scanFiles(); + } else { + // Ensure pages are all relative to the uriRoot. + // Those that are not will trigger an error later. The error + // could be detected earlier but isn't to support the use case + // when failFast is not used. + for (int i = 0; i < pages.size(); i++) { + String nextjsp = pages.get(i); + + File fjsp = new File(nextjsp); + if (!fjsp.isAbsolute()) { + fjsp = new File(uriRootF, nextjsp); + } + if (!fjsp.exists()) { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage( + "jspc.error.fileDoesNotExist", fjsp.toString())); + } + continue; + } + String s = fjsp.getAbsolutePath(); + if (s.startsWith(uriRoot)) { + nextjsp = s.substring(uriRoot.length()); + } + if (nextjsp.startsWith("." + File.separatorChar)) { + nextjsp = nextjsp.substring(2); + } + pages.set(i, nextjsp); + } + } + + initWebXml(); + + int errorCount = 0; + long start = System.currentTimeMillis(); + + ExecutorService threadPool = Executors.newFixedThreadPool(threadCount); + ExecutorCompletionService service = new ExecutorCompletionService<>(threadPool); + try { + int pageCount = pages.size(); + for (String nextjsp : pages) { + service.submit(new ProcessFile(nextjsp)); + } + JasperException reportableError = null; + for (int i = 0; i < pageCount; i++) { + try { + service.take().get(); + } catch (ExecutionException e) { + if (failFast) { + // Generation is not interruptible so any tasks that + // have started will complete. + List notExecuted = threadPool.shutdownNow(); + i += notExecuted.size(); + Throwable t = e.getCause(); + if (t instanceof JasperException) { + reportableError = (JasperException) t; + } else { + reportableError = new JasperException(t); + } + } else { + errorCount++; + log.error(Localizer.getMessage("jspc.error.compilation"), e); + } + } catch (InterruptedException e) { + // Ignore + } + } + if (reportableError != null) { + throw reportableError; + } + } finally { + threadPool.shutdown(); + } + + long time = System.currentTimeMillis() - start; + String msg = Localizer.getMessage("jspc.generation.result", + Integer.toString(errorCount), Long.toString(time)); + if (failOnError && errorCount > 0) { + System.out.println(Localizer.getMessage( + "jspc.errorCount", Integer.valueOf(errorCount))); + throw new BuildException(msg); + } else { + log.info(msg); + } + + completeWebXml(); + + if (addWebXmlMappings) { + mergeIntoWebXml(); + } + + } catch (IOException ioe) { + throw new BuildException(ioe); + + } catch (JasperException je) { + if (failOnError) { + throw new BuildException(je); + } + } finally { + if (loader != null) { + LogFactory.release(loader); + } + } + } + + // ==================== protected utility methods ==================== + + protected String nextArg() { + if ((argPos >= args.length) + || (fullstop = SWITCH_FULL_STOP.equals(args[argPos]))) { + return null; + } else { + return args[argPos++]; + } + } + + protected String nextFile() { + if (fullstop) { + argPos++; + } + if (argPos >= args.length) { + return null; + } else { + return args[argPos++]; + } + } + + protected void initWebXml() throws JasperException { + try { + if (webxmlLevel >= INC_WEBXML) { + mapout = openWebxmlWriter(new File(webxmlFile)); + servletout = new CharArrayWriter(); + mappingout = new CharArrayWriter(); + } else { + mapout = null; + servletout = null; + mappingout = null; + } + if (webxmlLevel >= ALL_WEBXML) { + mapout.write(Localizer.getMessage("jspc.webxml.header", webxmlEncoding)); + mapout.flush(); + } else if (webxmlLevel >= FRG_WEBXML) { + mapout.write(Localizer.getMessage("jspc.webfrg.header", webxmlEncoding)); + mapout.flush(); + } else if ((webxmlLevel>= INC_WEBXML) && !addWebXmlMappings) { + mapout.write(Localizer.getMessage("jspc.webinc.header")); + mapout.flush(); + } + } catch (IOException ioe) { + mapout = null; + servletout = null; + mappingout = null; + throw new JasperException(ioe); + } + } + + protected void completeWebXml() { + if (mapout != null) { + try { + servletout.writeTo(mapout); + mappingout.writeTo(mapout); + if (webxmlLevel >= ALL_WEBXML) { + mapout.write(Localizer.getMessage("jspc.webxml.footer")); + } else if (webxmlLevel >= FRG_WEBXML) { + mapout.write(Localizer.getMessage("jspc.webfrg.footer")); + } else if ((webxmlLevel >= INC_WEBXML) && !addWebXmlMappings) { + mapout.write(Localizer.getMessage("jspc.webinc.footer")); + } + mapout.close(); + } catch (IOException ioe) { + // nothing to do if it fails since we are done with it + } + } + } + + + protected void initTldScanner(JspCServletContext context, ClassLoader classLoader) { + if (scanner != null) { + return; + } + + scanner = newTldScanner(context, true, isValidateTld(), isBlockExternal()); + scanner.setClassLoader(classLoader); + } + + + protected TldScanner newTldScanner(JspCServletContext context, boolean namespaceAware, + boolean validate, boolean blockExternal) { + return new TldScanner(context, namespaceAware, validate, blockExternal); + } + + + protected void initServletContext(ClassLoader classLoader) + throws IOException, JasperException { + // TODO: should we use the Ant Project's log? + PrintWriter log = new PrintWriter(System.out); + URL resourceBase = new File(uriRoot).getCanonicalFile().toURI().toURL(); + + context = new JspCServletContext(log, resourceBase, classLoader, + isValidateXml(), isBlockExternal()); + if (isValidateTld()) { + context.setInitParameter(Constants.XML_VALIDATION_TLD_INIT_PARAM, "true"); + } + + + initTldScanner(context, classLoader); + + try { + scanner.scan(); + } catch (SAXException e) { + throw new JasperException(e); + } + tldCache = new TldCache(context, scanner.getUriTldResourcePathMap(), + scanner.getTldResourcePathTaglibXmlMap()); + context.setAttribute(TldCache.SERVLET_CONTEXT_ATTRIBUTE_NAME, tldCache); + rctxt = new JspRuntimeContext(context, this); + jspConfig = new JspConfig(context); + tagPluginManager = new TagPluginManager(context); + } + + /** + * Initializes the classloader as/if needed for the given + * compilation context. + * @return the classloader that will be used + * @throws IOException If an error occurs + */ + protected ClassLoader initClassLoader() throws IOException { + + classPath = getClassPath(); + + ClassLoader jspcLoader = getClass().getClassLoader(); + if (jspcLoader instanceof AntClassLoader) { + classPath += File.pathSeparator + + ((AntClassLoader) jspcLoader).getClasspath(); + } + + // Turn the classPath into URLs + List urls = new ArrayList<>(); + StringTokenizer tokenizer = new StringTokenizer(classPath, + File.pathSeparator); + while (tokenizer.hasMoreTokens()) { + String path = tokenizer.nextToken(); + try { + File libFile = new File(path); + urls.add(libFile.toURI().toURL()); + } catch (IOException ioe) { + // Failing a toCanonicalPath on a file that + // exists() should be a JVM regression test, + // therefore we have permission to freak uot + throw new RuntimeException(ioe.toString()); + } + } + + File webappBase = new File(uriRoot); + if (webappBase.exists()) { + File classes = new File(webappBase, "/WEB-INF/classes"); + try { + if (classes.exists()) { + classPath = classPath + File.pathSeparator + + classes.getCanonicalPath(); + urls.add(classes.getCanonicalFile().toURI().toURL()); + } + } catch (IOException ioe) { + // failing a toCanonicalPath on a file that + // exists() should be a JVM regression test, + // therefore we have permission to freak out + throw new RuntimeException(ioe.toString()); + } + File webinfLib = new File(webappBase, "/WEB-INF/lib"); + if (webinfLib.exists() && webinfLib.isDirectory()) { + String[] libs = webinfLib.list(); + if (libs != null) { + for (String lib : libs) { + if (lib.length() < 5) { + continue; + } + String ext = lib.substring(lib.length() - 4); + if (!".jar".equalsIgnoreCase(ext)) { + if (".tld".equalsIgnoreCase(ext)) { + log.warn(Localizer.getMessage("jspc.warning.tldInWebInfLib")); + } + continue; + } + try { + File libFile = new File(webinfLib, lib); + classPath = classPath + File.pathSeparator + libFile.getAbsolutePath(); + urls.add(libFile.getAbsoluteFile().toURI().toURL()); + } catch (IOException ioe) { + // failing a toCanonicalPath on a file that + // exists() should be a JVM regression test, + // therefore we have permission to freak out + throw new RuntimeException(ioe.toString()); + } + } + } + } + } + + URL[] urlsA = urls.toArray(new URL[0]); + loader = new URLClassLoader(urlsA, this.getClass().getClassLoader()); + return loader; + } + + /** + * Find the WEB-INF dir by looking up in the directory tree. + * This is used if no explicit docbase is set, but only files. + * + * @param f The path from which it will start looking + */ + protected void locateUriRoot( File f ) { + String tUriBase = uriBase; + if (tUriBase == null) { + tUriBase = "/"; + } + try { + if (f.exists()) { + f = new File(f.getAbsolutePath()); + while (true) { + File g = new File(f, "WEB-INF"); + if (g.exists() && g.isDirectory()) { + uriRoot = f.getCanonicalPath(); + uriBase = tUriBase; + if (log.isInfoEnabled()) { + log.info(Localizer.getMessage( + "jspc.implicit.uriRoot", + uriRoot)); + } + break; + } + if (f.exists() && f.isDirectory()) { + tUriBase = "/" + f.getName() + "/" + tUriBase; + } + + String fParent = f.getParent(); + if (fParent == null) { + break; + } else { + f = new File(fParent); + } + + // If there is no acceptable candidate, uriRoot will + // remain null. + } + + if (uriRoot != null) { + File froot = new File(uriRoot); + uriRoot = froot.getCanonicalPath(); + } + } + } catch (IOException ioe) { + // Missing uriRoot will be handled in the caller. + } + } + + /** + * Resolves the relative or absolute pathname correctly + * in both Ant and command-line situations. If Ant launched + * us, we should use the basedir of the current project + * to resolve relative paths. + * + * See Bugzilla 35571. + * + * @param s The file + * @return The file resolved + */ + protected File resolveFile(final String s) { + if(getProject() == null) { + // Note FileUtils.getFileUtils replaces FileUtils.newFileUtils in Ant 1.6.3 + return FileUtils.getFileUtils().resolveFile(null, s); + } else { + return FileUtils.getFileUtils().resolveFile(getProject().getBaseDir(), s); + } + } + + private Reader openWebxmlReader(File file) throws IOException { + FileInputStream fis = new FileInputStream(file); + try { + return webxmlEncoding != null ? new InputStreamReader(fis, + webxmlEncoding) : new InputStreamReader(fis); + } catch (IOException ex) { + fis.close(); + throw ex; + } + } + + private Writer openWebxmlWriter(File file) throws IOException { + FileOutputStream fos = new FileOutputStream(file); + try { + return webxmlEncoding != null ? new OutputStreamWriter(fos, + webxmlEncoding) : new OutputStreamWriter(fos); + } catch (IOException ex) { + fos.close(); + throw ex; + } + } + + + private class ProcessFile implements Callable { + private final String file; + + private ProcessFile(String file) { + this.file = file; + } + + @Override + public Void call() throws Exception { + processFile(file); + return null; + } + } +} diff --git a/java/org/apache/jasper/JspCompilationContext.java b/java/org/apache/jasper/JspCompilationContext.java new file mode 100644 index 0000000..9876c56 --- /dev/null +++ b/java/org/apache/jasper/JspCompilationContext.java @@ -0,0 +1,769 @@ +/* + * 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.jasper; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.util.Set; +import java.util.jar.JarEntry; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.jsp.tagext.TagInfo; + +import org.apache.jasper.compiler.Compiler; +import org.apache.jasper.compiler.JspRuntimeContext; +import org.apache.jasper.compiler.JspUtil; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.compiler.ServletWriter; +import org.apache.jasper.servlet.JasperLoader; +import org.apache.jasper.servlet.JspServletWrapper; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; + +/** + * A place holder for various things that are used through out the JSP + * engine. This is a per-request/per-context data structure. Some of + * the instance variables are set at different points. + * + * Most of the path-related stuff is here - mangling names, versions, dirs, + * loading resources and dealing with uris. + * + * @author Anil K. Vijendran + * @author Harish Prabandham + * @author Pierre Delisle + * @author Costin Manolache + * @author Kin-man Chung + */ +public class JspCompilationContext { + + private final Log log = LogFactory.getLog(JspCompilationContext.class); // must not be static + + private String className; + private final String jspUri; + private String basePackageName; + private String derivedPackageName; + private String servletJavaFileName; + private String javaPath; + private String classFileName; + private ServletWriter writer; + private final Options options; + private final JspServletWrapper jsw; + private Compiler jspCompiler; + private String classPath; + + private final String baseURI; + private String outputDir; + private final ServletContext context; + private ClassLoader loader; + + private final JspRuntimeContext rctxt; + + private volatile boolean removed = false; + + // volatile so changes are visible when multiple threads request a JSP file + // that has been modified + private volatile URLClassLoader jspLoader; + private URL baseUrl; + private Class servletClass; + + private final boolean isTagFile; + private boolean protoTypeMode; + private TagInfo tagInfo; + private Jar tagJar; + + // jspURI _must_ be relative to the context + public JspCompilationContext(String jspUri, Options options, + ServletContext context, JspServletWrapper jsw, + JspRuntimeContext rctxt) { + this(jspUri, null, options, context, jsw, rctxt, null, false); + } + + public JspCompilationContext(String tagfile, TagInfo tagInfo, + Options options, ServletContext context, JspServletWrapper jsw, + JspRuntimeContext rctxt, Jar tagJar) { + this(tagfile, tagInfo, options, context, jsw, rctxt, tagJar, true); + } + + private JspCompilationContext(String jspUri, TagInfo tagInfo, + Options options, ServletContext context, JspServletWrapper jsw, + JspRuntimeContext rctxt, Jar tagJar, boolean isTagFile) { + + this.jspUri = canonicalURI(jspUri); + this.options = options; + this.jsw = jsw; + this.context = context; + + String baseURI = jspUri.substring(0, jspUri.lastIndexOf('/') + 1); + // hack fix for resolveRelativeURI + if (baseURI.isEmpty()) { + baseURI = "/"; + } else if (baseURI.charAt(0) != '/') { + // strip the base slash since it will be combined with the + // uriBase to generate a file + baseURI = "/" + baseURI; + } + if (baseURI.charAt(baseURI.length() - 1) != '/') { + baseURI += '/'; + } + this.baseURI = baseURI; + + this.rctxt = rctxt; + this.basePackageName = options.getGeneratedJspPackageName(); + + this.tagInfo = tagInfo; + this.tagJar = tagJar; + this.isTagFile = isTagFile; + } + + + /* ==================== Methods to override ==================== */ + + // ---------- Class path and loader ---------- + + /** + * @return the classpath that is passed off to the Java compiler. + */ + public String getClassPath() { + if( classPath != null ) { + return classPath; + } + return rctxt.getClassPath(); + } + + /** + * The classpath that is passed off to the Java compiler. + * @param classPath The class path to use + */ + public void setClassPath(String classPath) { + this.classPath = classPath; + } + + /** + * What class loader to use for loading classes while compiling + * this JSP? + * @return the class loader used to load all compiled classes + */ + public ClassLoader getClassLoader() { + if( loader != null ) { + return loader; + } + return rctxt.getParentClassLoader(); + } + + public void setClassLoader(ClassLoader loader) { + this.loader = loader; + } + + public ClassLoader getJspLoader() { + if( jspLoader == null ) { + jspLoader = new JasperLoader(new URL[] {baseUrl}, getClassLoader(), + basePackageName, rctxt.getPermissionCollection()); + } + return jspLoader; + } + + public void clearJspLoader() { + jspLoader = null; + } + + + // ---------- Input/Output ---------- + + /** + * The output directory to generate code into. The output directory + * is make up of the scratch directory, which is provide in Options, + * plus the directory derived from the package name. + * @return the output directory in which the generated sources are placed + */ + public String getOutputDir() { + if (outputDir == null) { + createOutputDir(); + } + + return outputDir; + } + + /** + * Create a "Compiler" object based on some init param data. This + * is not done yet. Right now we're just hardcoding the actual + * compilers that are created. + * @return the Java compiler wrapper + */ + public Compiler createCompiler() { + if (jspCompiler != null ) { + return jspCompiler; + } + jspCompiler = null; + if (options.getCompilerClassName() != null) { + jspCompiler = createCompiler(options.getCompilerClassName()); + } else { + if (options.getCompiler() == null) { + jspCompiler = createCompiler("org.apache.jasper.compiler.JDTCompiler"); + if (jspCompiler == null) { + jspCompiler = createCompiler("org.apache.jasper.compiler.AntCompiler"); + } + } else { + jspCompiler = createCompiler("org.apache.jasper.compiler.AntCompiler"); + if (jspCompiler == null) { + jspCompiler = createCompiler("org.apache.jasper.compiler.JDTCompiler"); + } + } + } + if (jspCompiler == null) { + throw new IllegalStateException(Localizer.getMessage("jsp.error.compiler.config", + options.getCompilerClassName(), options.getCompiler())); + } + jspCompiler.init(this, jsw); + return jspCompiler; + } + + protected Compiler createCompiler(String className) { + Compiler compiler = null; + try { + compiler = (Compiler) Class.forName(className).getConstructor().newInstance(); + } catch (NoClassDefFoundError | ClassNotFoundException e) { + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jsp.error.compiler"), e); + } + } catch (ReflectiveOperationException e) { + log.warn(Localizer.getMessage("jsp.error.compiler"), e); + } + return compiler; + } + + public Compiler getCompiler() { + return jspCompiler; + } + + // ---------- Access resources in the webapp ---------- + + /** + * Get the full value of a URI relative to this compilations context + * uses current file as the base. + * @param uri The relative URI + * @return absolute URI + */ + public String resolveRelativeUri(String uri) { + // sometimes we get uri's massaged from File(String), so check for + // a root directory separator char + if (uri.startsWith("/") || uri.startsWith(File.separator)) { + return uri; + } else { + return baseURI + uri; + } + } + + /** + * Gets a resource as a stream, relative to the meanings of this + * context's implementation. + * @param res the resource to look for + * @return a null if the resource cannot be found or represented + * as an InputStream. + */ + public java.io.InputStream getResourceAsStream(String res) { + return context.getResourceAsStream(canonicalURI(res)); + } + + + public URL getResource(String res) throws MalformedURLException { + return context.getResource(canonicalURI(res)); + } + + + public Set getResourcePaths(String path) { + return context.getResourcePaths(canonicalURI(path)); + } + + /** + * Gets the actual path of a URI relative to the context of + * the compilation. + * @param path The webapp path + * @return the corresponding path in the filesystem + */ + public String getRealPath(String path) { + if (context != null) { + return context.getRealPath(path); + } + return path; + } + + /** + * Returns the JAR file in which the tag file for which this + * JspCompilationContext was created is packaged, or null if this + * JspCompilationContext does not correspond to a tag file, or if the + * corresponding tag file is not packaged in a JAR. + * @return a JAR file + */ + public Jar getTagFileJar() { + return this.tagJar; + } + + public void setTagFileJar(Jar tagJar) { + this.tagJar = tagJar; + } + + /* ==================== Common implementation ==================== */ + + /** + * Just the class name (does not include package name) of the + * generated class. + * @return the class name + */ + public String getServletClassName() { + + if (className != null) { + return className; + } + + if (isTagFile) { + className = tagInfo.getTagClassName(); + int lastIndex = className.lastIndexOf('.'); + if (lastIndex != -1) { + className = className.substring(lastIndex + 1); + } + } else { + int iSep = jspUri.lastIndexOf('/') + 1; + className = JspUtil.makeJavaIdentifier(jspUri.substring(iSep)); + } + return className; + } + + public void setServletClassName(String className) { + this.className = className; + } + + /** + * Path of the JSP URI. Note that this is not a file name. This is + * the context rooted URI of the JSP file. + * @return the path to the JSP + */ + public String getJspFile() { + return jspUri; + } + + + public Long getLastModified(String resource) { + return getLastModified(resource, tagJar); + } + + + public Long getLastModified(String resource, Jar tagJar) { + long result = -1; + URLConnection uc = null; + try { + if (tagJar != null) { + if (resource.startsWith("/")) { + resource = resource.substring(1); + } + result = tagJar.getLastModified(resource); + } else { + URL jspUrl = getResource(resource); + if (jspUrl == null) { + incrementRemoved(); + return Long.valueOf(result); + } + uc = jspUrl.openConnection(); + if (uc instanceof JarURLConnection) { + JarEntry jarEntry = ((JarURLConnection) uc).getJarEntry(); + if (jarEntry != null) { + result = jarEntry.getTime(); + } else { + result = uc.getLastModified(); + } + } else { + result = uc.getLastModified(); + } + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage( + "jsp.error.lastModified", getJspFile()), e); + } + result = -1; + } finally { + if (uc != null) { + try { + uc.getInputStream().close(); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage( + "jsp.error.lastModified", getJspFile()), e); + } + result = -1; + } + } + } + return Long.valueOf(result); + } + + public boolean isTagFile() { + return isTagFile; + } + + public TagInfo getTagInfo() { + return tagInfo; + } + + public void setTagInfo(TagInfo tagi) { + tagInfo = tagi; + } + + /** + * @return true if we are compiling a tag file + * in prototype mode. + * ie we only generate codes with class for the tag handler with empty + * method bodies. + */ + public boolean isPrototypeMode() { + return protoTypeMode; + } + + public void setPrototypeMode(boolean pm) { + protoTypeMode = pm; + } + + /** + * Package name for the generated class is made up of the base package + * name, which is user settable, and the derived package name. The + * derived package name directly mirrors the file hierarchy of the JSP page. + * @return the package name + */ + public String getServletPackageName() { + if (isTagFile()) { + String className = tagInfo.getTagClassName(); + int lastIndex = className.lastIndexOf('.'); + String packageName = ""; + if (lastIndex != -1) { + packageName = className.substring(0, lastIndex); + } + return packageName; + } else { + String dPackageName = getDerivedPackageName(); + if (dPackageName.length() == 0) { + return basePackageName; + } + return basePackageName + '.' + getDerivedPackageName(); + } + } + + protected String getDerivedPackageName() { + if (derivedPackageName == null) { + int iSep = jspUri.lastIndexOf('/'); + derivedPackageName = (iSep > 0) ? + JspUtil.makeJavaPackage(jspUri.substring(1,iSep)) : ""; + } + return derivedPackageName; + } + + /** + * @return The base package name into which all servlet and associated code + * is generated + */ + public String getBasePackageName() { + return basePackageName; + } + + /** + * The package name into which the servlet class is generated. + * @param basePackageName The package name to use + */ + public void setBasePackageName(String basePackageName) { + this.basePackageName = basePackageName; + } + + /** + * @return Full path name of the Java file into which the servlet is being + * generated. + */ + public String getServletJavaFileName() { + if (servletJavaFileName == null) { + servletJavaFileName = getOutputDir() + getServletClassName() + ".java"; + } + return servletJavaFileName; + } + + /** + * @return the Options object for this context. + */ + public Options getOptions() { + return options; + } + + public ServletContext getServletContext() { + return context; + } + + public JspRuntimeContext getRuntimeContext() { + return rctxt; + } + + /** + * @return the path of the Java file relative to the work directory. + */ + public String getJavaPath() { + + if (javaPath != null) { + return javaPath; + } + + if (isTagFile()) { + String tagName = tagInfo.getTagClassName(); + javaPath = tagName.replace('.', '/') + ".java"; + } else { + javaPath = getServletPackageName().replace('.', '/') + '/' + + getServletClassName() + ".java"; + } + return javaPath; + } + + public String getClassFileName() { + if (classFileName == null) { + classFileName = getOutputDir() + getServletClassName() + ".class"; + } + return classFileName; + } + + /** + * @return the writer that is used to write the generated Servlet source. + */ + public ServletWriter getWriter() { + return writer; + } + + public void setWriter(ServletWriter writer) { + this.writer = writer; + } + + /** + * Gets the 'location' of the TLD associated with the given taglib 'uri'. + * @param uri The taglib URI + * @return An array of two Strings: The first element denotes the real + * path to the TLD. If the path to the TLD points to a jar file, then the + * second element denotes the name of the TLD entry in the jar file. + * Returns null if the given uri is not associated with any tag library + * 'exposed' in the web application. + */ + public TldResourcePath getTldResourcePath(String uri) { + return getOptions().getTldCache().getTldResourcePath(uri); + } + + /** + * @return true if generated code is kept. + */ + public boolean keepGenerated() { + return getOptions().getKeepGenerated(); + } + + // ==================== Removal ==================== + + public void incrementRemoved() { + if (removed == false && rctxt != null) { + rctxt.removeWrapper(jspUri); + } + removed = true; + } + + public boolean isRemoved() { + return removed; + } + + // ==================== Compile and reload ==================== + + public void compile() throws JasperException, FileNotFoundException { + createCompiler(); + if (jspCompiler.isOutDated()) { + if (isRemoved()) { + throw new FileNotFoundException(jspUri); + } + try { + jspCompiler.removeGeneratedFiles(); + jspLoader = null; + jspCompiler.compile(); + jsw.setReload(true); + jsw.setCompilationException(null); + } catch (JasperException ex) { + // Cache compilation exception + jsw.setCompilationException(ex); + if (options.getDevelopment() && options.getRecompileOnFail()) { + // Force a recompilation attempt on next access + jsw.setLastModificationTest(-1); + } + throw ex; + } catch (FileNotFoundException fnfe) { + // Re-throw to let caller handle this - will result in a 404 + throw fnfe; + } catch (Exception ex) { + JasperException je = new JasperException( + Localizer.getMessage("jsp.error.unable.compile"), + ex); + // Cache compilation exception + jsw.setCompilationException(je); + throw je; + } + } + } + + // ==================== Manipulating the class ==================== + + public Class load() throws JasperException { + try { + getJspLoader(); + + String name = getFQCN(); + servletClass = jspLoader.loadClass(name); + } catch (ClassNotFoundException cex) { + throw new JasperException(Localizer.getMessage("jsp.error.unable.load"), + cex); + } catch (Exception ex) { + throw new JasperException(Localizer.getMessage("jsp.error.unable.compile"), + ex); + } + removed = false; + return servletClass; + } + + public String getFQCN() { + String name; + if (isTagFile()) { + name = tagInfo.getTagClassName(); + } else { + name = getServletPackageName() + "." + getServletClassName(); + } + return name; + } + + // ==================== protected methods ==================== + + private static final Object outputDirLock = new Object(); + + public void checkOutputDir() { + if (outputDir != null) { + if (!(new File(outputDir)).exists()) { + makeOutputDir(); + } + } else { + createOutputDir(); + } + } + + protected boolean makeOutputDir() { + synchronized(outputDirLock) { + File outDirFile = new File(outputDir); + return (outDirFile.mkdirs() || outDirFile.isDirectory()); + } + } + + protected void createOutputDir() { + String path = null; + if (isTagFile()) { + String tagName = tagInfo.getTagClassName(); + path = tagName.replace('.', File.separatorChar); + path = path.substring(0, path.lastIndexOf(File.separatorChar)); + } else { + path = getServletPackageName().replace('.',File.separatorChar); + } + + // Append servlet or tag handler path to scratch dir + try { + File base = options.getScratchDir(); + baseUrl = base.toURI().toURL(); + outputDir = base.getAbsolutePath() + File.separator + path + File.separator; + if (!makeOutputDir()) { + log.error(Localizer.getMessage("jsp.error.outputfolder.detail", outputDir)); + throw new IllegalStateException(Localizer.getMessage("jsp.error.outputfolder")); + } + } catch (MalformedURLException e) { + throw new IllegalStateException(Localizer.getMessage("jsp.error.outputfolder"), e); + } + } + + protected static final boolean isPathSeparator(char c) { + return (c == '/' || c == '\\'); + } + + protected static final String canonicalURI(String s) { + if (s == null) { + return null; + } + StringBuilder result = new StringBuilder(); + final int len = s.length(); + int pos = 0; + while (pos < len) { + char c = s.charAt(pos); + if ( isPathSeparator(c) ) { + /* + * multiple path separators. + * 'foo///bar' -> 'foo/bar' + */ + while (pos+1 < len && isPathSeparator(s.charAt(pos+1))) { + ++pos; + } + + if (pos+1 < len && s.charAt(pos+1) == '.') { + /* + * a single dot at the end of the path - we are done. + */ + if (pos+2 >= len) { + break; + } + + switch (s.charAt(pos+2)) { + /* + * self directory in path + * foo/./bar -> foo/bar + */ + case '/': + case '\\': + pos += 2; + continue; + + /* + * two dots in a path: go back one hierarchy. + * foo/bar/../baz -> foo/baz + */ + case '.': + // only if we have exactly _two_ dots. + if (pos+3 < len && isPathSeparator(s.charAt(pos+3))) { + pos += 3; + int separatorPos = result.length()-1; + while (separatorPos >= 0 && + ! isPathSeparator(result + .charAt(separatorPos))) { + --separatorPos; + } + if (separatorPos >= 0) { + result.setLength(separatorPos); + } + continue; + } + } + } + } + result.append(c); + ++pos; + } + return result.toString(); + } +} diff --git a/java/org/apache/jasper/Options.java b/java/org/apache/jasper/Options.java new file mode 100644 index 0000000..6bccdeb --- /dev/null +++ b/java/org/apache/jasper/Options.java @@ -0,0 +1,371 @@ +/* + * 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.jasper; + +import java.io.File; +import java.util.Map; + +import jakarta.servlet.jsp.tagext.TagLibraryInfo; + +import org.apache.jasper.compiler.JspConfig; +import org.apache.jasper.compiler.TagPluginManager; +import org.apache.jasper.compiler.TldCache; + +/** + * A class to hold all init parameters specific to the JSP engine. + * + * @author Anil K. Vijendran + * @author Hans Bergsten + * @author Pierre Delisle + */ +public interface Options { + + /** + * Returns true if Jasper issues a compilation error instead of a runtime + * Instantiation error if the class attribute specified in useBean action + * is invalid. + * @return true to get an error + */ + boolean getErrorOnUseBeanInvalidClassAttribute(); + + /** + * @return true to keep the generated source + */ + boolean getKeepGenerated(); + + /** + * @return true if tag handler pooling is enabled, + * false otherwise. + */ + boolean isPoolingEnabled(); + + /** + * @return true if HTML mapped Servlets are supported. + */ + boolean getMappedFile(); + + /** + * @return true if debug information in included + * in compiled classes. + */ + boolean getClassDebugInfo(); + + /** + * @return background compile thread check interval in seconds + */ + int getCheckInterval(); + + /** + * Main development flag, which enables detailed error reports with + * sources, as well automatic recompilation of JSPs and tag files. + * This setting should usually be false when running + * in production. + * @return true if Jasper is in development mode + */ + boolean getDevelopment(); + + /** + * @return true to include a source fragment in exception + * messages. + */ + boolean getDisplaySourceFragment(); + + /** + * @return true to suppress generation of SMAP info for + * JSR45 debugging. + */ + boolean isSmapSuppressed(); + + /** + * This setting is ignored if suppressSmap() is true. + * @return true to write SMAP info for JSR45 debugging to a + * file. + */ + boolean isSmapDumped(); + + /** + * @return {@link TrimSpacesOption#TRUE} to remove template text that + * consists only of whitespace from the output completely, + * {@link TrimSpacesOption#SINGLE} to replace such template text + * with a single space, {@link TrimSpacesOption#FALSE} to leave + * such template text unchanged or {@link TrimSpacesOption#EXTENDED} + * to remove template text that consists only of whitespace and to + * replace any sequence of whitespace and new lines within template + * text with a single new line. + */ + TrimSpacesOption getTrimSpaces(); + + /** + * @return the work folder + */ + File getScratchDir(); + + /** + * @return the classpath used to compile generated Servlets + */ + String getClassPath(); + + /** + * Compiler to use. + * + *

    + * If null (the default), the java compiler from Eclipse JDT + * project, bundled with Tomcat, will be used. Otherwise, the + * javac task from Apache Ant will be used to call an external + * java compiler and the value of this option will be passed to it. See + * Apache Ant documentation for the possible values. + * @return the compiler name + */ + String getCompiler(); + + /** + * @return the compiler target VM, e.g. 1.8. + */ + String getCompilerTargetVM(); + + /** + * @return the compiler source VM, e.g. 1.8. + */ + String getCompilerSourceVM(); + + /** + * @return Jasper Java compiler class to use. + */ + String getCompilerClassName(); + + /** + * The cache that maps URIs, resource paths and parsed TLD files for the + * various tag libraries 'exposed' by the web application. + * A tag library is 'exposed' either explicitly in + * web.xml or implicitly via the uri tag in the TLD + * of a taglib deployed in a jar file (WEB-INF/lib). + * + * @return the instance of the TldLocationsCache + * for the web-application. + */ + TldCache getTldCache(); + + /** + * @return Java platform encoding to generate the JSP page servlet. + */ + String getJavaEncoding(); + + /** + * The boolean flag to tell Ant whether to fork JSP page compilations. + * + *

    + * Is used only when Jasper uses an external java compiler (wrapped through + * a javac Apache Ant task). + * @return true to fork a process during compilation + */ + boolean getFork(); + + /** + * @return JSP configuration information specified in web.xml. + */ + JspConfig getJspConfig(); + + /** + * @return true to generate a X-Powered-By response header. + */ + boolean isXpoweredBy(); + + /** + * @return a Tag Plugin Manager + */ + TagPluginManager getTagPluginManager(); + + /** + * Indicates whether text strings are to be generated as char arrays. + * + * @return true if text strings are to be generated as char + * arrays, false otherwise + */ + boolean genStringAsCharArray(); + + /** + * @return modification test interval. + */ + int getModificationTestInterval(); + + + /** + * @return true if re-compile will occur on a failure. + */ + boolean getRecompileOnFail(); + + /** + * @return true is caching is enabled + * (used for precompilation). + */ + boolean isCaching(); + + /** + * The web-application wide cache for the TagLibraryInfo tag library + * descriptors, used if {@link #isCaching()} returns true. + * + *

    + * Using this cache avoids the cost of repeating the parsing of a tag + * library descriptor XML file (performed by TagLibraryInfoImpl.parseTLD). + *

    + * + * @return the Map(String uri, TagLibraryInfo tld) instance. + */ + Map getCache(); + + /** + * The maximum number of loaded jsps per web-application. If there are more + * jsps loaded, they will be unloaded. If unset or less than 0, no jsps + * are unloaded. + * @return The JSP count + */ + int getMaxLoadedJsps(); + + /** + * @return the idle time in seconds after which a JSP is unloaded. + * If unset or less or equal than 0, no jsps are unloaded. + */ + int getJspIdleTimeout(); + + /** + * @return {@code true} if the quote escaping required by section JSP.1.6 of + * the JSP specification should be applied to scriplet expression. + */ + boolean getStrictQuoteEscaping(); + + /** + * @return {@code true} if EL expressions used within attributes should have + * the quoting rules in JSP.1.6 applied to the expression. + */ + boolean getQuoteAttributeEL(); + + /** + * @return the name of the variable that will be used in the generated + * JSP code for the expression factory + */ + default String getVariableForExpressionFactory() { + return "_el_expressionfactory"; + } + + /** + * @return the name of the variable that will be used in the generated + * JSP code for the instance manager + */ + default String getVariableForInstanceManager() { + return "_jsp_instancemanager"; + } + + /** + * @return {@code true} if tag pooling is disabled with page that uses + * extends. + */ + default boolean getPoolTagsWithExtends() { + return false; + } + + /** + * @return {@code true} if the requirement to have the object + * used in jsp:getProperty action to be previously "introduced" + * to the JSP processor (see JSP.5.3) is enforced. + */ + default boolean getStrictGetProperty() { + return true; + } + + /** + * @return {@code true} if the strict white space rules are + * applied. + */ + default boolean getStrictWhitespace() { + return true; + } + + /** + * @return the default base class for generated JSP Servlets + */ + default String getJspServletBase() { + return "org.apache.jasper.runtime.HttpJspBase"; + } + + /** + * _jspService is the name of the method that is called by + * HttpJspBase.service(). This is where most of the code generated + * from JSPs go. + * @return the method name + */ + default String getServiceMethodName() { + return "_jspService"; + } + + /** + * @return ServletContext attribute for classpath. This is tomcat specific. + * Other servlet engines may choose to support this attribute if they + * want to have this JSP engine running on them. + */ + default String getServletClasspathAttribute() { + return "org.apache.catalina.jsp_classpath"; + } + + /** + * @return The query parameter that causes the JSP engine to just + * pregenerated the servlet but not invoke it. + */ + default String getJspPrecompilationQueryParameter() { + return "jsp_precompile"; + } + + /** + * @return The default package name for compiled jsp pages. + */ + default String getGeneratedJspPackageName() { + return "org.apache.jsp"; + } + + /** + * @return The default package name for tag handlers generated from tag files. + */ + default String getGeneratedTagFilePackageName() { + return "org.apache.jsp.tag"; + } + + /** + * @return Prefix to use for generated temporary variable names + */ + default String getTempVariableNamePrefix() { + return "_jspx_temp"; + } + + /** + * @return {@code true} if the container instance manager will be used + * to create the bean instances + */ + default boolean getUseInstanceManagerForTags() { + return false; + } + + + /** + * Should the container include the time the file was generated in the + * comments at the start of a Java file generated from a JSP or tag. + * Defaults to {@code true}. + * + * @return {@code true} to include the timestamp, otherwise don't include it + */ + default boolean getGeneratedJavaAddTimestamp() { + return true; + } +} diff --git a/java/org/apache/jasper/TrimSpacesOption.java b/java/org/apache/jasper/TrimSpacesOption.java new file mode 100644 index 0000000..2890aa3 --- /dev/null +++ b/java/org/apache/jasper/TrimSpacesOption.java @@ -0,0 +1,24 @@ +/* + * 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.jasper; + +public enum TrimSpacesOption { + FALSE, + TRUE, + SINGLE, + EXTENDED +} diff --git a/java/org/apache/jasper/compiler/AntCompiler.java b/java/org/apache/jasper/compiler/AntCompiler.java new file mode 100644 index 0000000..632954e --- /dev/null +++ b/java/org/apache/jasper/compiler/AntCompiler.java @@ -0,0 +1,493 @@ +/* + * 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.jasper.compiler; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.jasper.Constants; +import org.apache.jasper.JasperException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DefaultLogger; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.taskdefs.Javac; +import org.apache.tools.ant.types.Path; +import org.apache.tools.ant.types.PatternSet; + +/** + * Main JSP compiler class. This class uses Ant for compiling. + * + * @author Anil K. Vijendran + * @author Mandar Raje + * @author Pierre Delisle + * @author Kin-man Chung + * @author Remy Maucherat + * @author Mark Roth + */ +public class AntCompiler extends Compiler { + + private final Log log = LogFactory.getLog(AntCompiler.class); // must not be static + + protected static final Object javacLock = new Object(); + + static { + System.setErr(new SystemLogHandler(System.err)); + } + + // ----------------------------------------------------- Instance Variables + + protected Project project = null; + protected JasperAntLogger logger; + + // ------------------------------------------------------------ Constructor + + // Lazy eval - if we don't need to compile we probably don't need the project + protected Project getProject() { + + if (project != null) { + return project; + } + + // Initializing project + project = new Project(); + logger = new JasperAntLogger(); + logger.setOutputPrintStream(System.out); + logger.setErrorPrintStream(System.err); + logger.setMessageOutputLevel(Project.MSG_INFO); + project.addBuildListener( logger); + if (System.getProperty(Constants.CATALINA_HOME_PROP) != null) { + project.setBasedir(System.getProperty(Constants.CATALINA_HOME_PROP)); + } + + if( options.getCompiler() != null ) { + if( log.isTraceEnabled() ) { + log.trace("Compiler " + options.getCompiler() ); + } + project.setProperty("build.compiler", options.getCompiler() ); + } + project.init(); + return project; + } + + public static class JasperAntLogger extends DefaultLogger { + + protected final StringBuilder reportBuf = new StringBuilder(); + + @Override + protected void printMessage(final String message, + final PrintStream stream, + final int priority) { + } + + @Override + protected void log(String message) { + reportBuf.append(message); + reportBuf.append(System.lineSeparator()); + } + + protected String getReport() { + String report = reportBuf.toString(); + reportBuf.setLength(0); + return report; + } + } + + // --------------------------------------------------------- Public Methods + + + /** + * Compile the servlet from .java file to .class file + */ + @Override + protected void generateClass(Map smaps) + throws FileNotFoundException, JasperException, Exception { + + long t1 = 0; + if (log.isDebugEnabled()) { + t1 = System.currentTimeMillis(); + } + + String javaEncoding = ctxt.getOptions().getJavaEncoding(); + String javaFileName = ctxt.getServletJavaFileName(); + String classpath = ctxt.getClassPath(); + + StringBuilder errorReport = new StringBuilder(); + + StringBuilder info=new StringBuilder(); + info.append("Compile: javaFileName=" + javaFileName + "\n" ); + info.append(" classpath=" + classpath + "\n" ); + + // Start capturing the System.err output for this thread + SystemLogHandler.setThread(); + + // Initializing javac task + getProject(); + Javac javac = (Javac) project.createTask("javac"); + + // Initializing classpath + Path path = new Path(project); + path.setPath(System.getProperty("java.class.path")); + info.append(" cp=" + System.getProperty("java.class.path") + "\n"); + StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator); + while (tokenizer.hasMoreElements()) { + String pathElement = tokenizer.nextToken(); + File repository = new File(pathElement); + path.setLocation(repository); + info.append(" cp=" + repository + "\n"); + } + + if (log.isTraceEnabled()) { + log.trace( "Using classpath: " + System.getProperty("java.class.path") + + File.pathSeparator + classpath); + } + + // Initializing sourcepath + Path srcPath = new Path(project); + srcPath.setLocation(options.getScratchDir()); + + info.append(" work dir=" + options.getScratchDir() + "\n"); + + // Initialize and set java extensions + String exts = System.getProperty("java.ext.dirs"); + if (exts != null) { + Path extdirs = new Path(project); + extdirs.setPath(exts); + javac.setExtdirs(extdirs); + info.append(" extension dir=" + exts + "\n"); + } + + // Configure the compiler object + javac.setEncoding(javaEncoding); + javac.setClasspath(path); + javac.setDebug(ctxt.getOptions().getClassDebugInfo()); + javac.setSrcdir(srcPath); + javac.setTempdir(options.getScratchDir()); + javac.setFork(ctxt.getOptions().getFork()); + info.append(" srcDir=" + srcPath + "\n" ); + + // Set the Java compiler to use + if (options.getCompiler() != null) { + javac.setCompiler(options.getCompiler()); + info.append(" compiler=" + options.getCompiler() + "\n"); + } + + if (options.getCompilerTargetVM() != null) { + javac.setTarget(options.getCompilerTargetVM()); + info.append(" compilerTargetVM=" + options.getCompilerTargetVM() + "\n"); + } + + if (options.getCompilerSourceVM() != null) { + javac.setSource(options.getCompilerSourceVM()); + info.append(" compilerSourceVM=" + options.getCompilerSourceVM() + "\n"); + } + + // Build includes path + PatternSet.NameEntry includes = javac.createInclude(); + + includes.setName(ctxt.getJavaPath()); + info.append(" include="+ ctxt.getJavaPath() + "\n" ); + + BuildException be = null; + + try { + if (ctxt.getOptions().getFork()) { + javac.execute(); + } else { + synchronized(javacLock) { + javac.execute(); + } + } + } catch (BuildException e) { + be = e; + log.error(Localizer.getMessage("jsp.error.javac"), e); + log.error(Localizer.getMessage("jsp.error.javac.env") + info.toString()); + } + + errorReport.append(logger.getReport()); + + // Stop capturing the System.err output for this thread + String errorCapture = SystemLogHandler.unsetThread(); + if (errorCapture != null) { + errorReport.append(System.lineSeparator()); + errorReport.append(errorCapture); + } + + if (!ctxt.keepGenerated()) { + File javaFile = new File(javaFileName); + if (!javaFile.delete()) { + throw new JasperException(Localizer.getMessage( + "jsp.warning.compiler.javafile.delete.fail", javaFile)); + } + } + + if (be != null) { + String errorReportString = errorReport.toString(); + log.error(Localizer.getMessage("jsp.error.compilation", javaFileName, errorReportString)); + JavacErrorDetail[] javacErrors = ErrorDispatcher.parseJavacErrors( + errorReportString, javaFileName, pageNodes); + if (javacErrors != null) { + errDispatcher.javacError(javacErrors); + } else { + errDispatcher.javacError(errorReportString, be); + } + } + + if( log.isDebugEnabled() ) { + long t2 = System.currentTimeMillis(); + log.debug(Localizer.getMessage("jsp.compiled", ctxt.getServletJavaFileName(), Long.valueOf(t2 - t1))); + } + + logger = null; + project = null; + + if (ctxt.isPrototypeMode()) { + return; + } + + // JSR45 Support + if (!options.isSmapSuppressed()) { + SmapUtil.installSmap(smaps); + } + } + + + protected static class SystemLogHandler extends PrintStream { + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct the handler to capture the output of the given steam. + * @param wrapped The wrapped stream + */ + public SystemLogHandler(PrintStream wrapped) { + super(wrapped); + this.wrapped = wrapped; + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Wrapped PrintStream. + */ + protected final PrintStream wrapped; + + + /** + * Thread <-> PrintStream associations. + */ + protected static final ThreadLocal streams = + new ThreadLocal<>(); + + + /** + * Thread <-> ByteArrayOutputStream associations. + */ + protected static final ThreadLocal data = + new ThreadLocal<>(); + + + // --------------------------------------------------------- Public Methods + + /** + * Start capturing thread's output. + */ + public static void setThread() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + data.set(baos); + streams.set(new PrintStream(baos)); + } + + + /** + * Stop capturing thread's output and return captured data as a String. + * @return the captured output + */ + public static String unsetThread() { + ByteArrayOutputStream baos = data.get(); + if (baos == null) { + return null; + } + streams.set(null); + data.set(null); + return baos.toString(); + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Find PrintStream to which the output must be written to. + * @return the current stream + */ + protected PrintStream findStream() { + PrintStream ps = streams.get(); + if (ps == null) { + ps = wrapped; + } + return ps; + } + + + // ---------------------------------------------------- PrintStream Methods + + + @Override + public void flush() { + findStream().flush(); + } + + @Override + public void close() { + findStream().close(); + } + + @Override + public boolean checkError() { + return findStream().checkError(); + } + + @Override + protected void setError() { + //findStream().setError(); + } + + @Override + public void write(int b) { + findStream().write(b); + } + + @Override + public void write(byte[] b) + throws IOException { + findStream().write(b); + } + + @Override + public void write(byte[] buf, int off, int len) { + findStream().write(buf, off, len); + } + + @Override + public void print(boolean b) { + findStream().print(b); + } + + @Override + public void print(char c) { + findStream().print(c); + } + + @Override + public void print(int i) { + findStream().print(i); + } + + @Override + public void print(long l) { + findStream().print(l); + } + + @Override + public void print(float f) { + findStream().print(f); + } + + @Override + public void print(double d) { + findStream().print(d); + } + + @Override + public void print(char[] s) { + findStream().print(s); + } + + @Override + public void print(String s) { + findStream().print(s); + } + + @Override + public void print(Object obj) { + findStream().print(obj); + } + + @Override + public void println() { + findStream().println(); + } + + @Override + public void println(boolean x) { + findStream().println(x); + } + + @Override + public void println(char x) { + findStream().println(x); + } + + @Override + public void println(int x) { + findStream().println(x); + } + + @Override + public void println(long x) { + findStream().println(x); + } + + @Override + public void println(float x) { + findStream().println(x); + } + + @Override + public void println(double x) { + findStream().println(x); + } + + @Override + public void println(char[] x) { + findStream().println(x); + } + + @Override + public void println(String x) { + findStream().println(x); + } + + @Override + public void println(Object x) { + findStream().println(x); + } + + } + +} diff --git a/java/org/apache/jasper/compiler/AttributeParser.java b/java/org/apache/jasper/compiler/AttributeParser.java new file mode 100644 index 0000000..bcb8bc3 --- /dev/null +++ b/java/org/apache/jasper/compiler/AttributeParser.java @@ -0,0 +1,333 @@ +/* + * 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.jasper.compiler; + +/** + * Converts a JSP attribute value into the unquoted equivalent. The attribute + * may contain EL expressions, in which case care needs to be taken to avoid any + * ambiguities. For example, consider the attribute values "${1+1}" and + * "\${1+1}". After unquoting, both appear as "${1+1}" but the first should + * evaluate to "2" and the second to "${1+1}". Literal \, $ and # need special + * treatment to ensure there is no ambiguity. The JSP attribute unquoting + * covers \\, \", \', \$, \#, %\>, <\%, &apos; and &quot; + */ +public class AttributeParser { + + /** + * Parses the provided input String as a JSP attribute and returns an + * unquoted value. + * + * @param input The input. + * @param quote The quote character for the attribute or 0 for + * scripting expressions. + * @param isELIgnored Is expression language being ignored on the page + * where the JSP attribute is defined. + * @param isDeferredSyntaxAllowedAsLiteral + * Are deferred expressions treated as literals? + * @param strict Should the rules of JSP.1.6 for escaping of quotes + * be strictly applied? + * @param quoteAttributeEL Should the rules of JSP.1.6 for escaping in + * attributes be applied to EL in attribute values? + * @return An unquoted JSP attribute that, if it contains + * expression language can be safely passed to the EL + * processor without fear of ambiguity. + */ + public static String getUnquoted(String input, char quote, + boolean isELIgnored, boolean isDeferredSyntaxAllowedAsLiteral, + boolean strict, boolean quoteAttributeEL) { + return new AttributeParser(input, quote, isELIgnored, + isDeferredSyntaxAllowedAsLiteral, strict, quoteAttributeEL).getUnquoted(); + } + + /* The quoted input string. */ + private final String input; + + /* The quote used for the attribute - null for scripting expressions. */ + private final char quote; + + /* Is expression language being ignored - affects unquoting. \$ and \# are + * treated as literals rather than quoted values. */ + private final boolean isELIgnored; + + /* Are deferred expression treated as literals */ + private final boolean isDeferredSyntaxAllowedAsLiteral; + + /* If a quote appears that matches quote, must it always be escaped? See + * JSP.1.6. + */ + private final boolean strict; + + private final boolean quoteAttributeEL; + + /* The type ($ or #) of expression. Literals have a type of null. */ + private final char type; + + /* The length of the quoted input string. */ + private final int size; + + /* Tracks the current position of the parser in the input String. */ + private int i = 0; + + /* Indicates if the last character returned by nextChar() was escaped. */ + private boolean lastChEscaped = false; + + /* The unquoted result. */ + private final StringBuilder result; + + + private AttributeParser(String input, char quote, + boolean isELIgnored, boolean isDeferredSyntaxAllowedAsLiteral, + boolean strict, boolean quoteAttributeEL) { + this.input = input; + this.quote = quote; + this.isELIgnored = isELIgnored; + this.isDeferredSyntaxAllowedAsLiteral = + isDeferredSyntaxAllowedAsLiteral; + this.strict = strict; + this.quoteAttributeEL = quoteAttributeEL; + this.type = getType(input); + this.size = input.length(); + result = new StringBuilder(size); + } + + /* + * Work through input looking for literals and expressions until the input + * has all been read. + */ + private String getUnquoted() { + while (i < size) { + parseLiteral(); + parseEL(); + } + return result.toString(); + } + + /* + * This method gets the next unquoted character and looks for + * - literals that need to be converted for EL processing + * \ -> type{'\\'} + * $ -> type{'$'} + * # -> type{'#'} + * - start of EL + * ${ + * #{ + * Note all the examples above *do not* include the escaping required to use + * the values in Java code. + */ + private void parseLiteral() { + boolean foundEL = false; + while (i < size && !foundEL) { + char ch = nextChar(); + if (!isELIgnored && ch == '\\') { + if (type == 0) { + result.append("\\"); + } else { + result.append(type); + result.append("{'\\\\'}"); + } + } else if (!isELIgnored && ch == '$' && lastChEscaped){ + if (type == 0) { + result.append("\\$"); + } else { + result.append(type); + result.append("{'$'}"); + } + } else if (!isELIgnored && ch == '#' && lastChEscaped){ + // Note if isDeferredSyntaxAllowedAsLiteral==true, \# will + // not be treated as an escape + if (type == 0) { + result.append("\\#"); + } else { + result.append(type); + result.append("{'#'}"); + } + } else if (ch == type){ + if (i < size) { + char next = input.charAt(i); + if (next == '{') { + foundEL = true; + // Move back to start of EL + i--; + } else { + result.append(ch); + } + } else { + result.append(ch); + } + } else { + result.append(ch); + } + } + } + + /* + * Once inside EL, no need to unquote or convert anything. The EL is + * terminated by '}'. The only other valid location for '}' is inside a + * StringLiteral. The literals are delimited by '\'' or '\"'. The only other + * valid location for '\'' or '\"' is also inside a StringLiteral. A quote + * character inside a StringLiteral must be escaped if the same quote + * character is used to delimit the StringLiteral. + */ + private void parseEL() { + boolean endEL = false; + boolean insideLiteral = false; + char literalQuote = 0; + while (i < size && !endEL) { + char ch; + if (quoteAttributeEL) { + ch = nextChar(); + } else { + ch = input.charAt(i++); + } + if (ch == '\'' || ch == '\"') { + if (insideLiteral) { + if (literalQuote == ch) { + insideLiteral = false; + } + } else { + insideLiteral = true; + literalQuote = ch; + } + result.append(ch); + } else if (ch == '\\') { + result.append(ch); + if (insideLiteral && size < i) { + if (quoteAttributeEL) { + ch = nextChar(); + } else { + ch = input.charAt(i++); + } + result.append(ch); + } + } else if (ch == '}') { + if (!insideLiteral) { + endEL = true; + } + result.append(ch); + } else { + result.append(ch); + } + } + } + + /* + * Returns the next unquoted character and sets the lastChEscaped flag to + * indicate if it was quoted/escaped or not. + * ' is always unquoted to ' + * " is always unquoted to " + * \" is always unquoted to " + * \' is always unquoted to ' + * \\ is always unquoted to \ + * \$ is unquoted to $ if EL is not being ignored + * \# is unquoted to # if EL is not being ignored + * <\% is always unquoted to <% + * %\> is always unquoted to %> + */ + private char nextChar() { + lastChEscaped = false; + char ch = input.charAt(i); + + if (ch == '&') { + if (i + 5 < size && input.charAt(i + 1) == 'a' && + input.charAt(i + 2) == 'p' && input.charAt(i + 3) == 'o' && + input.charAt(i + 4) == 's' && input.charAt(i + 5) == ';') { + ch = '\''; + i += 6; + } else if (i + 5 < size && input.charAt(i + 1) == 'q' && + input.charAt(i + 2) == 'u' && input.charAt(i + 3) == 'o' && + input.charAt(i + 4) == 't' && input.charAt(i + 5) == ';') { + ch = '\"'; + i += 6; + } else { + ++i; + } + } else if (ch == '\\' && i + 1 < size) { + ch = input.charAt(i + 1); + if (ch == '\\' || ch == '\"' || ch == '\'' || + (!isELIgnored && + (ch == '$' || + (!isDeferredSyntaxAllowedAsLiteral && + ch == '#')))) { + i += 2; + lastChEscaped = true; + } else { + ch = '\\'; + ++i; + } + } else if (ch == '<' && (i + 2 < size) && input.charAt(i + 1) == '\\' && + input.charAt(i + 2) == '%') { + // Note this is a hack since nextChar only returns a single char + // It is safe since <% does not require special treatment for EL + // or for literals + result.append('<'); + i+=3; + return '%'; + } else if (ch == '%' && i + 2 < size && input.charAt(i + 1) == '\\' && + input.charAt(i + 2) == '>') { + // Note this is a hack since nextChar only returns a single char + // It is safe since %> does not require special treatment for EL + // or for literals + result.append('%'); + i+=3; + return '>'; + } else if (ch == quote && strict) { + String msg = Localizer.getMessage("jsp.error.attribute.noescape", + input, ""+ quote); + throw new IllegalArgumentException(msg); + } else { + ++i; + } + + return ch; + } + + /* + * Determines the type of expression by looking for the first unquoted ${ + * or #{. + */ + private char getType(String value) { + if (value == null) { + return 0; + } + + if (isELIgnored) { + return 0; + } + + int j = 0; + int len = value.length(); + char current; + + while (j < len) { + current = value.charAt(j); + if (current == '\\') { + // Escape character - skip a character + j++; + } else if (current == '#' && !isDeferredSyntaxAllowedAsLiteral) { + if (j < (len -1) && value.charAt(j + 1) == '{') { + return '#'; + } + } else if (current == '$') { + if (j < (len - 1) && value.charAt(j + 1) == '{') { + return '$'; + } + } + j++; + } + return 0; + } +} diff --git a/java/org/apache/jasper/compiler/BeanRepository.java b/java/org/apache/jasper/compiler/BeanRepository.java new file mode 100644 index 0000000..1a28fdd --- /dev/null +++ b/java/org/apache/jasper/compiler/BeanRepository.java @@ -0,0 +1,75 @@ +/* + * 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.jasper.compiler; + + +import java.util.HashMap; + +import org.apache.jasper.JasperException; + +/** + * Repository of {page, request, session, application}-scoped beans + * + * @author Mandar Raje + * @author Remy Maucherat + */ +public class BeanRepository { + + private final HashMap beanTypes; + private final ClassLoader loader; + private final ErrorDispatcher errDispatcher; + + /** + * Constructor. + * @param loader The class loader + * @param err The error dispatcher that will be used to report errors + */ + public BeanRepository(ClassLoader loader, ErrorDispatcher err) { + this.loader = loader; + this.errDispatcher = err; + beanTypes = new HashMap<>(); + } + + public void addBean(Node.UseBean n, String s, String type, String scope) + throws JasperException { + + if (!(scope == null || scope.equals("page") || scope.equals("request") + || scope.equals("session") || scope.equals("application"))) { + errDispatcher.jspError(n, "jsp.error.usebean.badScope"); + } + + beanTypes.put(s, type); + } + + public Class getBeanType(String bean) + throws JasperException { + Class clazz = null; + try { + clazz = loader.loadClass(beanTypes.get(bean)); + } catch (ClassNotFoundException ex) { + throw new JasperException (ex); + } + return clazz; + } + + public boolean checkVariable(String bean) { + return beanTypes.containsKey(bean); + } + +} + + diff --git a/java/org/apache/jasper/compiler/Collector.java b/java/org/apache/jasper/compiler/Collector.java new file mode 100644 index 0000000..9ba669a --- /dev/null +++ b/java/org/apache/jasper/compiler/Collector.java @@ -0,0 +1,217 @@ +/* + * 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.jasper.compiler; + +import org.apache.jasper.JasperException; + +/** + * Collect info about the page and nodes, and make them available through + * the PageInfo object. + * + * @author Kin-man Chung + * @author Mark Roth + */ + +class Collector { + + /** + * A visitor for collecting information on the page and the body of + * the custom tags. + */ + private static class CollectVisitor extends Node.Visitor { + + private boolean scriptingElementSeen = false; + private boolean usebeanSeen = false; + private boolean includeActionSeen = false; + private boolean paramActionSeen = false; + private boolean setPropertySeen = false; + private boolean hasScriptingVars = false; + + @Override + public void visit(Node.ParamAction n) throws JasperException { + if (n.getValue().isExpression()) { + scriptingElementSeen = true; + } + paramActionSeen = true; + } + + @Override + public void visit(Node.IncludeAction n) throws JasperException { + if (n.getPage().isExpression()) { + scriptingElementSeen = true; + } + includeActionSeen = true; + visitBody(n); + } + + @Override + public void visit(Node.ForwardAction n) throws JasperException { + if (n.getPage().isExpression()) { + scriptingElementSeen = true; + } + visitBody(n); + } + + @Override + public void visit(Node.SetProperty n) throws JasperException { + if (n.getValue() != null && n.getValue().isExpression()) { + scriptingElementSeen = true; + } + setPropertySeen = true; + } + + @Override + public void visit(Node.UseBean n) throws JasperException { + if (n.getBeanName() != null && n.getBeanName().isExpression()) { + scriptingElementSeen = true; + } + usebeanSeen = true; + visitBody(n); + } + + @Override + public void visit(Node.PlugIn n) throws JasperException { + if (n.getHeight() != null && n.getHeight().isExpression()) { + scriptingElementSeen = true; + } + if (n.getWidth() != null && n.getWidth().isExpression()) { + scriptingElementSeen = true; + } + visitBody(n); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + // Check to see what kinds of element we see as child elements + checkSeen( n.getChildInfo(), n ); + } + + /** + * Check all child nodes for various elements and update the given + * ChildInfo object accordingly. Visits body in the process. + */ + private void checkSeen( Node.ChildInfo ci, Node n ) + throws JasperException + { + // save values collected so far + boolean scriptingElementSeenSave = scriptingElementSeen; + scriptingElementSeen = false; + boolean usebeanSeenSave = usebeanSeen; + usebeanSeen = false; + boolean includeActionSeenSave = includeActionSeen; + includeActionSeen = false; + boolean paramActionSeenSave = paramActionSeen; + paramActionSeen = false; + boolean setPropertySeenSave = setPropertySeen; + setPropertySeen = false; + boolean hasScriptingVarsSave = hasScriptingVars; + hasScriptingVars = false; + + // Scan attribute list for expressions + if( n instanceof Node.CustomTag ) { + Node.CustomTag ct = (Node.CustomTag)n; + Node.JspAttribute[] attrs = ct.getJspAttributes(); + for (int i = 0; attrs != null && i < attrs.length; i++) { + if (attrs[i].isExpression()) { + scriptingElementSeen = true; + break; + } + } + } + + visitBody(n); + + if( (n instanceof Node.CustomTag) && !hasScriptingVars) { + Node.CustomTag ct = (Node.CustomTag)n; + hasScriptingVars = ct.getVariableInfos().length > 0 || + ct.getTagVariableInfos().length > 0; + } + + // Record if the tag element and its body contains any scriptlet. + ci.setScriptless(! scriptingElementSeen); + ci.setHasUseBean(usebeanSeen); + ci.setHasIncludeAction(includeActionSeen); + ci.setHasParamAction(paramActionSeen); + ci.setHasSetProperty(setPropertySeen); + ci.setHasScriptingVars(hasScriptingVars); + + // Propagate value of scriptingElementSeen up. + scriptingElementSeen = scriptingElementSeen || scriptingElementSeenSave; + usebeanSeen = usebeanSeen || usebeanSeenSave; + setPropertySeen = setPropertySeen || setPropertySeenSave; + includeActionSeen = includeActionSeen || includeActionSeenSave; + paramActionSeen = paramActionSeen || paramActionSeenSave; + hasScriptingVars = hasScriptingVars || hasScriptingVarsSave; + } + + @Override + public void visit(Node.JspElement n) throws JasperException { + if (n.getNameAttribute().isExpression()) { + scriptingElementSeen = true; + } + + Node.JspAttribute[] attrs = n.getJspAttributes(); + for (Node.JspAttribute attr : attrs) { + if (attr.isExpression()) { + scriptingElementSeen = true; + break; + } + } + visitBody(n); + } + + @Override + public void visit(Node.JspBody n) throws JasperException { + checkSeen( n.getChildInfo(), n ); + } + + @Override + public void visit(Node.NamedAttribute n) throws JasperException { + checkSeen( n.getChildInfo(), n ); + } + + @Override + public void visit(Node.Declaration n) throws JasperException { + scriptingElementSeen = true; + } + + @Override + public void visit(Node.Expression n) throws JasperException { + scriptingElementSeen = true; + } + + @Override + public void visit(Node.Scriptlet n) throws JasperException { + scriptingElementSeen = true; + } + + private void updatePageInfo(PageInfo pageInfo) { + pageInfo.setScriptless(! scriptingElementSeen); + } + } + + + public static void collect(Compiler compiler, Node.Nodes page) + throws JasperException { + + CollectVisitor collectVisitor = new CollectVisitor(); + page.visit(collectVisitor); + collectVisitor.updatePageInfo(compiler.getPageInfo()); + + } +} + diff --git a/java/org/apache/jasper/compiler/Compiler.java b/java/org/apache/jasper/compiler/Compiler.java new file mode 100644 index 0000000..7317501 --- /dev/null +++ b/java/org/apache/jasper/compiler/Compiler.java @@ -0,0 +1,623 @@ +/* + * 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.jasper.compiler; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.net.JarURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.Options; +import org.apache.jasper.TrimSpacesOption; +import org.apache.jasper.servlet.JspServletWrapper; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.scan.JarFactory; + +/** + * Main JSP compiler class. This class uses Ant for compiling. + * + * @author Anil K. Vijendran + * @author Mandar Raje + * @author Pierre Delisle + * @author Kin-man Chung + * @author Remy Maucherat + * @author Mark Roth + */ +public abstract class Compiler { + + private final Log log = LogFactory.getLog(Compiler.class); // must not be static + + // ----------------------------------------------------- Instance Variables + + protected JspCompilationContext ctxt; + + protected ErrorDispatcher errDispatcher; + + protected PageInfo pageInfo; + + protected JspServletWrapper jsw; + + protected TagFileProcessor tfp; + + protected Options options; + + protected Node.Nodes pageNodes; + + + // ------------------------------------------------------------ Constructor + + public void init(JspCompilationContext ctxt, JspServletWrapper jsw) { + this.jsw = jsw; + this.ctxt = ctxt; + this.options = ctxt.getOptions(); + } + + + // --------------------------------------------------------- Public Methods + + public SmapStratum getSmap(String className) { + + Map smaps = ctxt.getRuntimeContext().getSmaps(); + SmapStratum smap = smaps.get(className); + + if (smap == null && !options.isSmapSuppressed()) { + // Tomcat was restarted so cached SMAP has been lost. However, it + // was written to the class file so it can be recovered. + smap = SmapUtil.loadSmap(className, ctxt.getJspLoader()); + if (smap != null) { + smaps.put(className, smap); + } + } + + return smap; + } + + + /** + * Compile the jsp file into equivalent servlet in .java file + * + * @return A map of class names to JSR 045 source maps + * + * @throws Exception Error generating Java source + */ + protected Map generateJava() throws Exception { + + long t1, t2, t3, t4; + + t1 = t2 = t3 = t4 = 0; + + if (log.isDebugEnabled()) { + t1 = System.currentTimeMillis(); + } + + // Setup page info area + pageInfo = new PageInfo(new BeanRepository(ctxt.getClassLoader(), + errDispatcher), ctxt); + + JspConfig jspConfig = options.getJspConfig(); + JspConfig.JspProperty jspProperty = jspConfig.findJspProperty(ctxt + .getJspFile()); + + /* + * If the current uri is matched by a pattern specified in a + * jsp-property-group in web.xml, initialize pageInfo with those + * properties. + */ + if (jspProperty.isELIgnored() != null) { + pageInfo.setELIgnored(JspUtil.booleanValue(jspProperty + .isELIgnored())); + } + if (jspProperty.getErrorOnELNotFound() != null) { + pageInfo.setErrorOnELNotFound(JspUtil.booleanValue(jspProperty + .getErrorOnELNotFound())); + } + if (jspProperty.isScriptingInvalid() != null) { + pageInfo.setScriptingInvalid(JspUtil.booleanValue(jspProperty + .isScriptingInvalid())); + } + if (jspProperty.getIncludePrelude() != null) { + pageInfo.setIncludePrelude(jspProperty.getIncludePrelude()); + } + if (jspProperty.getIncludeCoda() != null) { + pageInfo.setIncludeCoda(jspProperty.getIncludeCoda()); + } + if (jspProperty.isDeferedSyntaxAllowedAsLiteral() != null) { + pageInfo.setDeferredSyntaxAllowedAsLiteral(JspUtil.booleanValue(jspProperty + .isDeferedSyntaxAllowedAsLiteral())); + } + if (jspProperty.isTrimDirectiveWhitespaces() != null) { + pageInfo.setTrimDirectiveWhitespaces(JspUtil.booleanValue(jspProperty + .isTrimDirectiveWhitespaces())); + } + // Default ContentType processing is deferred until after the page has + // been parsed + if (jspProperty.getBuffer() != null) { + pageInfo.setBufferValue(jspProperty.getBuffer(), null, + errDispatcher); + } + if (jspProperty.isErrorOnUndeclaredNamespace() != null) { + pageInfo.setErrorOnUndeclaredNamespace( + JspUtil.booleanValue( + jspProperty.isErrorOnUndeclaredNamespace())); + } + if (ctxt.isTagFile()) { + try { + double libraryVersion = Double.parseDouble(ctxt.getTagInfo() + .getTagLibrary().getRequiredVersion()); + if (libraryVersion < 2.0) { + pageInfo.setIsELIgnored("true", null, errDispatcher, true); + } + if (libraryVersion < 2.1) { + pageInfo.setDeferredSyntaxAllowedAsLiteral("true", null, + errDispatcher, true); + } + } catch (NumberFormatException ex) { + errDispatcher.jspError(ex); + } + } + + ctxt.checkOutputDir(); + String javaFileName = ctxt.getServletJavaFileName(); + + try { + /* + * The setting of isELIgnored changes the behaviour of the parser + * in subtle ways. To add to the 'fun', isELIgnored can be set in + * any file that forms part of the translation unit so setting it + * in a file included towards the end of the translation unit can + * change how the parser should have behaved when parsing content + * up to the point where isELIgnored was set. Arghh! + * Previous attempts to hack around this have only provided partial + * solutions. We now use two passes to parse the translation unit. + * The first just parses the directives and the second parses the + * whole translation unit once we know how isELIgnored has been set. + * TODO There are some possible optimisations of this process. + */ + // Parse the file + ParserController parserCtl = new ParserController(ctxt, this); + + // Pass 1 - the directives + Node.Nodes directives = + parserCtl.parseDirectives(ctxt.getJspFile()); + Validator.validateDirectives(this, directives); + + // Pass 2 - the whole translation unit + pageNodes = parserCtl.parse(ctxt.getJspFile()); + + // Leave this until now since it can only be set once - bug 49726 + if (pageInfo.getContentType() == null && + jspProperty.getDefaultContentType() != null) { + pageInfo.setContentType(jspProperty.getDefaultContentType()); + } + + if (ctxt.isPrototypeMode()) { + // generate prototype .java file for the tag file + try (ServletWriter writer = setupContextWriter(javaFileName)) { + Generator.generate(writer, this, pageNodes); + return null; + } + } + + // Validate and process attributes - don't re-validate the + // directives we validated in pass 1 + Validator.validateExDirectives(this, pageNodes); + + if (log.isDebugEnabled()) { + t2 = System.currentTimeMillis(); + } + + // Collect page info + Collector.collect(this, pageNodes); + + // Compile (if necessary) and load the tag files referenced in + // this compilation unit. + tfp = new TagFileProcessor(); + tfp.loadTagFiles(this, pageNodes); + + if (log.isDebugEnabled()) { + t3 = System.currentTimeMillis(); + } + + // Determine which custom tag needs to declare which scripting vars + ScriptingVariabler.set(pageNodes, errDispatcher); + + // Optimizations by Tag Plugins + TagPluginManager tagPluginManager = options.getTagPluginManager(); + tagPluginManager.apply(pageNodes, errDispatcher, pageInfo); + + // Optimization: concatenate contiguous template texts. + TextOptimizer.concatenate(this, pageNodes); + + // Generate static function mapper codes. + ELFunctionMapper.map(pageNodes); + + // generate servlet .java file + try (ServletWriter writer = setupContextWriter(javaFileName)) { + Generator.generate(writer, this, pageNodes); + } + + // The writer is only used during the compile, dereference + // it in the JspCompilationContext when done to allow it + // to be GC'd and save memory. + ctxt.setWriter(null); + + if (log.isTraceEnabled()) { + t4 = System.currentTimeMillis(); + log.trace("Generated " + javaFileName + " total=" + (t4 - t1) + + " generate=" + (t4 - t3) + " validate=" + (t2 - t1)); + } + + } catch (RuntimeException e) { + // Remove the generated .java file + File file = new File(javaFileName); + if (file.exists()) { + if (!file.delete()) { + log.warn(Localizer.getMessage( + "jsp.warning.compiler.javafile.delete.fail", + file.getAbsolutePath())); + } + } + throw e; + } + + Map smaps = null; + + // JSR45 Support + if (!options.isSmapSuppressed()) { + smaps = SmapUtil.generateSmap(ctxt, pageNodes); + // Add them to the web application wide cache for future lookup in + // error handling etc. + ctxt.getRuntimeContext().getSmaps().putAll(smaps); + } + + // If any proto type .java and .class files was generated, + // the prototype .java may have been replaced by the current + // compilation (if the tag file is self referencing), but the + // .class file need to be removed, to make sure that javac would + // generate .class again from the new .java file just generated. + tfp.removeProtoTypeFiles(ctxt.getClassFileName()); + + return smaps; + } + + private ServletWriter setupContextWriter(String javaFileName) + throws FileNotFoundException, JasperException { + ServletWriter writer; + // Setup the ServletWriter + String javaEncoding = ctxt.getOptions().getJavaEncoding(); + OutputStreamWriter osw = null; + + try { + osw = new OutputStreamWriter( + new FileOutputStream(javaFileName), javaEncoding); + } catch (UnsupportedEncodingException ex) { + errDispatcher.jspError("jsp.error.needAlternateJavaEncoding", + javaEncoding); + } + + if (ctxt.getOptions().getTrimSpaces().equals(TrimSpacesOption.EXTENDED)) { + writer = new NewlineReductionServletWriter(new PrintWriter(osw)); + } else { + writer = new ServletWriter(new PrintWriter(osw)); + } + + ctxt.setWriter(writer); + return writer; + } + + /** + * Servlet compilation. This compiles the generated sources into + * Servlets. + * + * @param smaps The source maps for the class(es) generated from the source + * file + * + * @throws FileNotFoundException Source files not found + * @throws JasperException Compilation error + * @throws Exception Some other error + */ + protected abstract void generateClass(Map smaps) + throws FileNotFoundException, JasperException, Exception; + + /** + * Compile the jsp file from the current engine context. + * @throws FileNotFoundException Source files not found + * @throws JasperException Compilation error + * @throws Exception Some other error + */ + public void compile() throws FileNotFoundException, JasperException, + Exception { + compile(true); + } + + /** + * Compile the jsp file from the current engine context. As an side- effect, + * tag files that are referenced by this page are also compiled. + * + * @param compileClass + * If true, generate both .java and .class file If false, + * generate only .java file + * @throws FileNotFoundException Source files not found + * @throws JasperException Compilation error + * @throws Exception Some other error + */ + public void compile(boolean compileClass) throws FileNotFoundException, + JasperException, Exception { + compile(compileClass, false); + } + + /** + * Compile the jsp file from the current engine context. As an side- effect, + * tag files that are referenced by this page are also compiled. + * + * @param compileClass + * If true, generate both .java and .class file If false, + * generate only .java file + * @param jspcMode + * true if invoked from JspC, false otherwise + * @throws FileNotFoundException Source files not found + * @throws JasperException Compilation error + * @throws Exception Some other error + */ + public void compile(boolean compileClass, boolean jspcMode) + throws FileNotFoundException, JasperException, Exception { + if (errDispatcher == null) { + this.errDispatcher = new ErrorDispatcher(jspcMode); + } + + try { + final Long jspLastModified = ctxt.getLastModified(ctxt.getJspFile()); + Map smaps = generateJava(); + File javaFile = new File(ctxt.getServletJavaFileName()); + if (!javaFile.setLastModified(jspLastModified.longValue())) { + throw new JasperException(Localizer.getMessage("jsp.error.setLastModified", javaFile)); + } + if (compileClass) { + generateClass(smaps); + // Fix for bugzilla 41606 + // Set JspServletWrapper.servletClassLastModifiedTime after successful compile + File targetFile = new File(ctxt.getClassFileName()); + if (targetFile.exists()) { + if (!targetFile.setLastModified(jspLastModified.longValue())) { + throw new JasperException( + Localizer.getMessage("jsp.error.setLastModified", targetFile)); + } + if (jsw != null) { + jsw.setServletClassLastModifiedTime( + jspLastModified.longValue()); + } + } + } + } finally { + if (tfp != null && ctxt.isPrototypeMode()) { + tfp.removeProtoTypeFiles(null); + } + // Make sure these object which are only used during the + // generation and compilation of the JSP page get + // dereferenced so that they can be GC'd and reduce the + // memory footprint. + tfp = null; + errDispatcher = null; + pageInfo = null; + pageNodes = null; + + if (ctxt.getWriter() != null) { + ctxt.getWriter().close(); + ctxt.setWriter(null); + } + } + } + + /** + * This is a protected method intended to be overridden by subclasses of + * Compiler. This is used by the compile method to do all the compilation. + * @return true if the source generation and compilation + * should occur + */ + public boolean isOutDated() { + return isOutDated(true); + } + + /** + * Determine if a compilation is necessary by checking the time stamp of the + * JSP page with that of the corresponding .class or .java file. If the page + * has dependencies, the check is also extended to its dependents, and so + * on. This method can by overridden by a subclasses of Compiler. + * + * @param checkClass + * If true, check against .class file, if false, check against + * .java file. + * @return true if the source generation and compilation + * should occur + */ + public boolean isOutDated(boolean checkClass) { + + if (jsw != null + && (ctxt.getOptions().getModificationTestInterval() > 0)) { + + if (jsw.getLastModificationTest() + + (ctxt.getOptions().getModificationTestInterval() * 1000) > System + .currentTimeMillis()) { + return false; + } + jsw.setLastModificationTest(System.currentTimeMillis()); + } + + // Test the target file first. Unless there is an error checking the + // last modified time of the source (unlikely) the target is going to + // have to be checked anyway. If the target doesn't exist (likely during + // startup) this saves an unnecessary check of the source. + File targetFile; + if (checkClass) { + targetFile = new File(ctxt.getClassFileName()); + } else { + targetFile = new File(ctxt.getServletJavaFileName()); + } + if (!targetFile.exists()) { + return true; + } + long targetLastModified = targetFile.lastModified(); + if (checkClass && jsw != null) { + jsw.setServletClassLastModifiedTime(targetLastModified); + } + + Long jspRealLastModified = ctxt.getLastModified(ctxt.getJspFile()); + if (jspRealLastModified.longValue() < 0) { + // Something went wrong - assume modification + return true; + } + + if (targetLastModified != jspRealLastModified.longValue()) { + if (log.isTraceEnabled()) { + log.trace("Compiler: outdated: " + targetFile + " " + + targetLastModified); + } + return true; + } + + // determine if source dependent files (e.g. includes using include + // directives) have been changed. + if (jsw == null) { + return false; + } + + Map depends = jsw.getDependants(); + if (depends == null) { + return false; + } + + for (Entry include : depends.entrySet()) { + try { + String key = include.getKey(); + URL includeUrl; + long includeLastModified = 0; + if (key.startsWith("jar:jar:")) { + // Assume we constructed this correctly + int entryStart = key.lastIndexOf("!/"); + String entry = key.substring(entryStart + 2); + try (Jar jar = JarFactory.newInstance(new URI(key.substring(4, entryStart)).toURL())) { + includeLastModified = jar.getLastModified(entry); + } + } else { + if (key.startsWith("jar:") || key.startsWith("file:")) { + includeUrl = new URI(key).toURL(); + } else { + includeUrl = ctxt.getResource(include.getKey()); + } + if (includeUrl == null) { + return true; + } + URLConnection iuc = includeUrl.openConnection(); + if (iuc instanceof JarURLConnection) { + includeLastModified = + ((JarURLConnection) iuc).getJarEntry().getTime(); + } else { + includeLastModified = iuc.getLastModified(); + } + iuc.getInputStream().close(); + } + + if (includeLastModified != include.getValue().longValue()) { + return true; + } + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jsp.error.compiler.missingResource"), e); + } + return true; + } + } + + return false; + + } + + /** + * @return the error dispatcher. + */ + public ErrorDispatcher getErrorDispatcher() { + return errDispatcher; + } + + /** + * @return the info about the page under compilation + */ + public PageInfo getPageInfo() { + return pageInfo; + } + + public JspCompilationContext getCompilationContext() { + return ctxt; + } + + /** + * Remove generated files + */ + public void removeGeneratedFiles() { + removeGeneratedClassFiles(); + + try { + File javaFile = new File(ctxt.getServletJavaFileName()); + if (log.isTraceEnabled()) { + log.trace("Deleting " + javaFile); + } + if (javaFile.exists()) { + if (!javaFile.delete()) { + log.warn(Localizer.getMessage( + "jsp.warning.compiler.javafile.delete.fail", + javaFile.getAbsolutePath())); + } + } + } catch (Exception e) { + // Remove as much as possible, log possible exceptions + log.warn(Localizer.getMessage("jsp.warning.compiler.classfile.delete.fail.unknown"), + e); + } + } + + public void removeGeneratedClassFiles() { + try { + File classFile = new File(ctxt.getClassFileName()); + if (log.isTraceEnabled()) { + log.trace("Deleting " + classFile); + } + if (classFile.exists()) { + if (!classFile.delete()) { + log.warn(Localizer.getMessage( + "jsp.warning.compiler.classfile.delete.fail", + classFile.getAbsolutePath())); + } + } + } catch (Exception e) { + // Remove as much as possible, log possible exceptions + log.warn(Localizer.getMessage("jsp.warning.compiler.classfile.delete.fail.unknown"), + e); + } + } +} diff --git a/java/org/apache/jasper/compiler/DefaultErrorHandler.java b/java/org/apache/jasper/compiler/DefaultErrorHandler.java new file mode 100644 index 0000000..d1d6819 --- /dev/null +++ b/java/org/apache/jasper/compiler/DefaultErrorHandler.java @@ -0,0 +1,119 @@ +/* + * 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.jasper.compiler; + +import org.apache.jasper.JasperException; + +/** + * Default implementation of ErrorHandler interface. + * + * @author Jan Luehe + */ +class DefaultErrorHandler implements ErrorHandler { + + /* + * Processes the given JSP parse error. + * + * @param fname Name of the JSP file in which the parse error occurred + * @param line Parse error line number + * @param column Parse error column number + * @param errMsg Parse error message + * @param exception Parse exception + */ + @Override + public void jspError(String fname, int line, int column, String errMsg, + Exception ex) throws JasperException { + throw new JasperException(fname + " (" + + Localizer.getMessage("jsp.error.location", + Integer.toString(line), Integer.toString(column)) + + ") " + errMsg, ex); + } + + /* + * Processes the given JSP parse error. + * + * @param errMsg Parse error message + * @param exception Parse exception + */ + @Override + public void jspError(String errMsg, Exception ex) throws JasperException { + throw new JasperException(errMsg, ex); + } + + /* + * Processes the given javac compilation errors. + * + * @param details Array of JavacErrorDetail instances corresponding to the + * compilation errors + */ + @Override + public void javacError(JavacErrorDetail[] details) throws JasperException { + + if (details == null) { + return; + } + + Object[] args = null; + StringBuilder buf = new StringBuilder(); + + for (JavacErrorDetail detail : details) { + if (detail.getJspBeginLineNumber() >= 0) { + args = new Object[]{ + Integer.valueOf(detail.getJspBeginLineNumber()), + detail.getJspFileName()}; + buf.append(System.lineSeparator()); + buf.append(System.lineSeparator()); + buf.append(Localizer.getMessage("jsp.error.single.line.number", + args)); + buf.append(System.lineSeparator()); + buf.append(detail.getErrorMessage()); + buf.append(System.lineSeparator()); + buf.append(detail.getJspExtract()); + } else { + args = new Object[]{ + Integer.valueOf(detail.getJavaLineNumber()), + detail.getJavaFileName()}; + buf.append(System.lineSeparator()); + buf.append(System.lineSeparator()); + buf.append(Localizer.getMessage("jsp.error.java.line.number", + args)); + buf.append(System.lineSeparator()); + buf.append(detail.getErrorMessage()); + } + } + buf.append(System.lineSeparator()); + buf.append(System.lineSeparator()); + buf.append("Stacktrace:"); + throw new JasperException( + Localizer.getMessage("jsp.error.unable.compile") + ": " + buf); + } + + /** + * Processes the given javac error report and exception. + * + * @param errorReport Compilation error report + * @param exception Compilation exception + */ + @Override + public void javacError(String errorReport, Exception exception) + throws JasperException { + + throw new JasperException( + Localizer.getMessage("jsp.error.unable.compile"), exception); + } + +} diff --git a/java/org/apache/jasper/compiler/ELFunctionMapper.java b/java/org/apache/jasper/compiler/ELFunctionMapper.java new file mode 100644 index 0000000..dd97df3 --- /dev/null +++ b/java/org/apache/jasper/compiler/ELFunctionMapper.java @@ -0,0 +1,331 @@ +/* + * 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.jasper.compiler; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.jsp.tagext.FunctionInfo; + +import org.apache.jasper.Constants; +import org.apache.jasper.JasperException; +import org.apache.tomcat.util.security.PrivilegedGetTccl; + +/** + * This class generates functions mappers for the EL expressions in the page. + * Instead of a global mapper, a mapper is used for each call to EL + * evaluator, thus avoiding the prefix overlapping and redefinition + * issues. + * + * @author Kin-man Chung + */ + +public class ELFunctionMapper { + private int currFunc = 0; + private StringBuilder ds; // Contains codes to initialize the functions mappers. + private StringBuilder ss; // Contains declarations of the functions mappers. + + /** + * Creates the functions mappers for all EL expressions in the JSP page. + * + * @param page The current compilation unit. + * @throws JasperException EL error + */ + public static void map(Node.Nodes page) + throws JasperException { + + ELFunctionMapper map = new ELFunctionMapper(); + map.ds = new StringBuilder(); + map.ss = new StringBuilder(); + + page.visit(map.new ELFunctionVisitor()); + + // Append the declarations to the root node + String ds = map.ds.toString(); + if (ds.length() > 0) { + Node root = page.getRoot(); + @SuppressWarnings("unused") + Node unused = new Node.Declaration(map.ss.toString(), null, root); + unused = new Node.Declaration( + "static {\n" + ds + "}\n", null, root); + } + } + + /** + * A visitor for the page. The places where EL is allowed are scanned + * for functions, and if found functions mappers are created. + */ + private class ELFunctionVisitor extends Node.Visitor { + + /** + * Use a global name map to facilitate reuse of function maps. + * The key used is prefix:function:uri. + */ + private final Map gMap = new HashMap<>(); + + @Override + public void visit(Node.ParamAction n) throws JasperException { + doMap(n.getValue()); + visitBody(n); + } + + @Override + public void visit(Node.IncludeAction n) throws JasperException { + doMap(n.getPage()); + visitBody(n); + } + + @Override + public void visit(Node.ForwardAction n) throws JasperException { + doMap(n.getPage()); + visitBody(n); + } + + @Override + public void visit(Node.SetProperty n) throws JasperException { + doMap(n.getValue()); + visitBody(n); + } + + @Override + public void visit(Node.UseBean n) throws JasperException { + doMap(n.getBeanName()); + visitBody(n); + } + + @Override + public void visit(Node.PlugIn n) throws JasperException { + doMap(n.getHeight()); + doMap(n.getWidth()); + visitBody(n); + } + + @Override + public void visit(Node.JspElement n) throws JasperException { + + Node.JspAttribute[] attrs = n.getJspAttributes(); + for (int i = 0; attrs != null && i < attrs.length; i++) { + doMap(attrs[i]); + } + doMap(n.getNameAttribute()); + visitBody(n); + } + + @Override + public void visit(Node.UninterpretedTag n) throws JasperException { + + Node.JspAttribute[] attrs = n.getJspAttributes(); + for (int i = 0; attrs != null && i < attrs.length; i++) { + doMap(attrs[i]); + } + visitBody(n); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + Node.JspAttribute[] attrs = n.getJspAttributes(); + for (int i = 0; attrs != null && i < attrs.length; i++) { + doMap(attrs[i]); + } + visitBody(n); + } + + @Override + public void visit(Node.ELExpression n) throws JasperException { + doMap(n.getEL()); + } + + private void doMap(Node.JspAttribute attr) + throws JasperException { + if (attr != null) { + doMap(attr.getEL()); + } + } + + /** + * Creates function mappers, if needed, from ELNodes + */ + private void doMap(ELNode.Nodes el) + throws JasperException { + + // Only care about functions in ELNode's + class Fvisitor extends ELNode.Visitor { + private final List funcs = new ArrayList<>(); + private final Set keySet = new HashSet<>(); + @Override + public void visit(ELNode.Function n) throws JasperException { + String key = n.getPrefix() + ":" + n.getName(); + if (keySet.add(key)) { + funcs.add(n); + } + } + } + + if (el == null) { + return; + } + + // First locate all unique functions in this EL + Fvisitor fv = new Fvisitor(); + el.visit(fv); + List functions = fv.funcs; + + if (functions.size() == 0) { + return; + } + + // Reuse a previous map if possible + String decName = matchMap(functions); + if (decName != null) { + el.setMapName(decName); + return; + } + + // Generate declaration for the map statically + decName = getMapName(); + ss.append("private static org.apache.jasper.runtime.ProtectedFunctionMapper " + decName + ";\n"); + + ds.append(" " + decName + "= "); + ds.append("org.apache.jasper.runtime.ProtectedFunctionMapper"); + + // Special case if there is only one function in the map + String funcMethod = null; + if (functions.size() == 1) { + funcMethod = ".getMapForFunction"; + } else { + ds.append(".getInstance();\n"); + funcMethod = " " + decName + ".mapFunction"; + } + + // Setup arguments for either getMapForFunction or mapFunction + for (ELNode.Function f : functions) { + FunctionInfo funcInfo = f.getFunctionInfo(); + String fnQName = f.getPrefix() + ":" + f.getName(); + if (funcInfo == null) { + // Added via Lambda or ImportHandler. EL will expect a + // function mapper even if one isn't used so just pass null + ds.append(funcMethod + "(null, null, null, null);\n"); + } else { + ds.append(funcMethod + "(\"" + fnQName + "\", " + + getCanonicalName(funcInfo.getFunctionClass()) + + ".class, " + '\"' + f.getMethodName() + "\", " + + "new Class[] {"); + String params[] = f.getParameters(); + for (int k = 0; k < params.length; k++) { + if (k != 0) { + ds.append(", "); + } + int iArray = params[k].indexOf('['); + if (iArray < 0) { + ds.append(params[k] + ".class"); + } else { + String baseType = params[k].substring(0, iArray); + ds.append("java.lang.reflect.Array.newInstance("); + ds.append(baseType); + ds.append(".class,"); + + // Count the number of array dimension + int aCount = 0; + for (int jj = iArray; jj < params[k].length(); jj++ ) { + if (params[k].charAt(jj) == '[') { + aCount++; + } + } + if (aCount == 1) { + ds.append("0).getClass()"); + } else { + ds.append("new int[" + aCount + "]).getClass()"); + } + } + } + ds.append("});\n"); + } + // Put the current name in the global function map + gMap.put(fnQName + ':' + f.getUri(), decName); + } + el.setMapName(decName); + } + + /** + * Find the name of the function mapper for an EL. Reuse a + * previously generated one if possible. + * @param functions A List of ELNode.Function instances that + * represents the functions in an EL + * @return A previous generated function mapper name that can be used + * by this EL; null if none found. + */ + private String matchMap(List functions) { + + String mapName = null; + for (ELNode.Function f : functions) { + String temName = gMap.get(f.getPrefix() + ':' + f.getName() + + ':' + f.getUri()); + if (temName == null) { + return null; + } + if (mapName == null) { + mapName = temName; + } else if (!temName.equals(mapName)) { + // If not all in the previous match, then no match. + return null; + } + } + return mapName; + } + + /* + * @return A unique name for a function mapper. + */ + private String getMapName() { + return "_jspx_fnmap_" + currFunc++; + } + + /** + * Convert a binary class name into a canonical one that can be used + * when generating Java source code. + * + * @param className Binary class name + * @return Canonical equivalent + */ + private String getCanonicalName(String className) throws JasperException { + Class clazz; + + ClassLoader tccl; + Thread currentThread = Thread.currentThread(); + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedAction pa = new PrivilegedGetTccl(currentThread); + tccl = AccessController.doPrivileged(pa); + } else { + tccl = currentThread.getContextClassLoader(); + } + + try { + clazz = Class.forName(className, false, tccl); + } catch (ClassNotFoundException e) { + throw new JasperException(e); + } + return clazz.getCanonicalName(); + } + } +} + diff --git a/java/org/apache/jasper/compiler/ELInterpreter.java b/java/org/apache/jasper/compiler/ELInterpreter.java new file mode 100644 index 0000000..73b0ffb --- /dev/null +++ b/java/org/apache/jasper/compiler/ELInterpreter.java @@ -0,0 +1,46 @@ +/* + * 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.jasper.compiler; + +import org.apache.jasper.JspCompilationContext; + +/** + * Defines the interface for the expression language interpreter. This allows + * users to provide custom EL interpreter implementations that can optimise + * EL processing for an application by , for example, performing code generation + * for simple expressions. + */ +public interface ELInterpreter { + + /** + * Returns the string representing the code that will be inserted into the + * servlet generated for JSP. The default implementation creates a call to + * {@link org.apache.jasper.runtime.PageContextImpl#proprietaryEvaluate( + * String, Class, jakarta.servlet.jsp.PageContext, + * org.apache.jasper.runtime.ProtectedFunctionMapper)} but other + * implementations may produce more optimised code. + * @param context The compilation context + * @param isTagFile true if in a tag file rather than a JSP + * @param expression a String containing zero or more "${}" expressions + * @param expectedType the expected type of the interpreted result + * @param fnmapvar Variable pointing to a function map. + * @return a String representing a call to the EL interpreter. + */ + String interpreterCall(JspCompilationContext context, + boolean isTagFile, String expression, + Class expectedType, String fnmapvar); +} diff --git a/java/org/apache/jasper/compiler/ELInterpreterFactory.java b/java/org/apache/jasper/compiler/ELInterpreterFactory.java new file mode 100644 index 0000000..efadcab --- /dev/null +++ b/java/org/apache/jasper/compiler/ELInterpreterFactory.java @@ -0,0 +1,107 @@ +/* + * 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.jasper.compiler; + +import jakarta.servlet.ServletContext; + +import org.apache.jasper.JspCompilationContext; + +/** + * Provides {@link ELInterpreter} instances for JSP compilation. + * + * The search order is as follows: + *
      + *
    1. ELInterpreter instance or implementation class name provided as a + * ServletContext attribute
    2. + *
    3. Implementation class named in a ServletContext initialisation parameter + *
    4. + *
    5. Default implementation
    6. + *
    + */ +public class ELInterpreterFactory { + + public static final String EL_INTERPRETER_CLASS_NAME = + ELInterpreter.class.getName(); + + private static final ELInterpreter DEFAULT_INSTANCE = + new DefaultELInterpreter(); + + + /** + * Obtain the correct EL Interpreter for the given web application. + * @param context The Servlet context + * @return the EL interpreter + * @throws Exception If an error occurs creating the interpreter + */ + public static ELInterpreter getELInterpreter(ServletContext context) + throws Exception { + + ELInterpreter result = null; + + // Search for an implementation + // 1. ServletContext attribute (set by application or cached by a + // previous call to this method). + Object attribute = context.getAttribute(EL_INTERPRETER_CLASS_NAME); + if (attribute instanceof ELInterpreter) { + return (ELInterpreter) attribute; + } else if (attribute instanceof String) { + result = createInstance(context, (String) attribute); + } + + // 2. ServletContext init parameter + if (result == null) { + String className = + context.getInitParameter(EL_INTERPRETER_CLASS_NAME); + if (className != null) { + result = createInstance(context, className); + } + } + + // 3. Default + if (result == null) { + result = DEFAULT_INSTANCE; + } + + // Cache the result for next time + context.setAttribute(EL_INTERPRETER_CLASS_NAME, result); + return result; + } + + + private static ELInterpreter createInstance(ServletContext context, + String className) throws Exception { + return (ELInterpreter) context.getClassLoader().loadClass( + className).getConstructor().newInstance(); + } + + + private ELInterpreterFactory() { + // Utility class. Hide default constructor. + } + + + public static class DefaultELInterpreter implements ELInterpreter { + + @Override + public String interpreterCall(JspCompilationContext context, + boolean isTagFile, String expression, + Class expectedType, String fnmapvar) { + return JspUtil.interpreterCall(isTagFile, expression, expectedType, + fnmapvar); + } + } +} diff --git a/java/org/apache/jasper/compiler/ELNode.java b/java/org/apache/jasper/compiler/ELNode.java new file mode 100644 index 0000000..0c99ce3 --- /dev/null +++ b/java/org/apache/jasper/compiler/ELNode.java @@ -0,0 +1,269 @@ +/* + * 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.jasper.compiler; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import jakarta.servlet.jsp.tagext.FunctionInfo; + +import org.apache.jasper.JasperException; + +/** + * This class defines internal representation for an EL Expression + * + * It currently only defines functions. It can be expanded to define + * all the components of an EL expression, if need to. + * + * @author Kin-man Chung + */ + +abstract class ELNode { + + public abstract void accept(Visitor v) throws JasperException; + + + /** + * Represents an EL expression: anything in ${ and }. + */ + public static class Root extends ELNode { + + private final ELNode.Nodes expr; + private final char type; + + Root(ELNode.Nodes expr, char type) { + this.expr = expr; + this.type = type; + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public ELNode.Nodes getExpression() { + return expr; + } + + public char getType() { + return type; + } + } + + /** + * Represents text outside of EL expression. + */ + public static class Text extends ELNode { + + private final String text; + + Text(String text) { + this.text = text; + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public String getText() { + return text; + } + } + + /** + * Represents anything in EL expression, other than functions, including + * function arguments etc + */ + public static class ELText extends ELNode { + + private final String text; + + ELText(String text) { + this.text = text; + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public String getText() { + return text; + } + } + + /** + * Represents a function + * Currently only include the prefix and function name, but not its + * arguments. + */ + public static class Function extends ELNode { + + private final String prefix; + private final String name; + private final String originalText; + private String uri; + private FunctionInfo functionInfo; + private String methodName; + private String[] parameters; + + Function(String prefix, String name, String originalText) { + this.prefix = prefix; + this.name = name; + this.originalText = originalText; + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public String getPrefix() { + return prefix; + } + + public String getName() { + return name; + } + + public String getOriginalText() { + return originalText; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getUri() { + return uri; + } + + public void setFunctionInfo(FunctionInfo f) { + this.functionInfo = f; + } + + public FunctionInfo getFunctionInfo() { + return functionInfo; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public String getMethodName() { + return methodName; + } + + public void setParameters(String[] parameters) { + this.parameters = parameters; + } + + public String[] getParameters() { + return parameters; + } + } + + /** + * An ordered list of ELNode. + */ + public static class Nodes { + + /* Name used for creating a map for the functions in this + EL expression, for communication to Generator. + */ + private String mapName = null; // The function map associated this EL + private final List list; + + Nodes() { + list = new ArrayList<>(); + } + + public void add(ELNode en) { + list.add(en); + } + + /** + * Visit the nodes in the list with the supplied visitor. + * + * @param v The visitor used + * + * @throws JasperException if an error occurs while visiting a node + */ + public void visit(Visitor v) throws JasperException { + for (ELNode n : list) { + n.accept(v); + } + } + + public Iterator iterator() { + return list.iterator(); + } + + public boolean isEmpty() { + return list.size() == 0; + } + + /** + * @return true if the expression contains a ${...} + */ + public boolean containsEL() { + for (ELNode n : list) { + if (n instanceof Root) { + return true; + } + } + return false; + } + + public void setMapName(String name) { + this.mapName = name; + } + + public String getMapName() { + return mapName; + } + + } + + /* + * A visitor class for traversing ELNodes + */ + public static class Visitor { + + public void visit(Root n) throws JasperException { + n.getExpression().visit(this); + } + + @SuppressWarnings("unused") + public void visit(Function n) throws JasperException { + // NOOP by default + } + + @SuppressWarnings("unused") + public void visit(Text n) throws JasperException { + // NOOP by default + } + + @SuppressWarnings("unused") + public void visit(ELText n) throws JasperException { + // NOOP by default + } + } +} + diff --git a/java/org/apache/jasper/compiler/ELParser.java b/java/org/apache/jasper/compiler/ELParser.java new file mode 100644 index 0000000..8f7ad93 --- /dev/null +++ b/java/org/apache/jasper/compiler/ELParser.java @@ -0,0 +1,589 @@ +/* + * 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.jasper.compiler; + +import org.apache.jasper.JasperException; +import org.apache.jasper.compiler.ELNode.ELText; +import org.apache.jasper.compiler.ELNode.Function; +import org.apache.jasper.compiler.ELNode.Root; +import org.apache.jasper.compiler.ELNode.Text; + +/** + * This class implements a parser for EL expressions. + * + * It takes strings of the form xxx${..}yyy${..}zzz etc, and turn it into a + * ELNode.Nodes. + * + * Currently, it only handles text outside ${..} and functions in ${ ..}. + * + * @author Kin-man Chung + */ + +public class ELParser { + + private Token curToken; // current token + private Token prevToken; // previous token + private String whiteSpace = ""; + + private final ELNode.Nodes expr; + + private ELNode.Nodes ELexpr; + + private int index; // Current index of the expression + + private final String expression; // The EL expression + + private char type; + + private final boolean isDeferredSyntaxAllowedAsLiteral; + + private static final String reservedWords[] = { "and", "div", "empty", + "eq", "false", "ge", "gt", "instanceof", "le", "lt", "mod", "ne", + "not", "null", "or", "true" }; + + public ELParser(String expression, boolean isDeferredSyntaxAllowedAsLiteral) { + index = 0; + this.expression = expression; + this.isDeferredSyntaxAllowedAsLiteral = isDeferredSyntaxAllowedAsLiteral; + expr = new ELNode.Nodes(); + } + + /** + * Parse an EL expression + * + * @param expression + * The input expression string of the form Char* ('${' Char* + * '}')* Char* + * @param isDeferredSyntaxAllowedAsLiteral + * Are deferred expressions treated as literals? + * @return Parsed EL expression in ELNode.Nodes + */ + public static ELNode.Nodes parse(String expression, + boolean isDeferredSyntaxAllowedAsLiteral) { + ELParser parser = new ELParser(expression, + isDeferredSyntaxAllowedAsLiteral); + while (parser.hasNextChar()) { + String text = parser.skipUntilEL(); + if (text.length() > 0) { + parser.expr.add(new ELNode.Text(text)); + } + ELNode.Nodes elexpr = parser.parseEL(); + if (!elexpr.isEmpty()) { + parser.expr.add(new ELNode.Root(elexpr, parser.type)); + } + } + return parser.expr; + } + + /** + * Parse an EL expression string '${...}'. Currently only separates the EL + * into functions and everything else. + * + * @return An ELNode.Nodes representing the EL expression + * + * Note: This cannot be refactored to use the standard EL implementation as + * the EL API does not provide the level of access required to the + * parsed expression. + */ + private ELNode.Nodes parseEL() { + + StringBuilder buf = new StringBuilder(); + ELexpr = new ELNode.Nodes(); + curToken = null; + prevToken = null; + int openBraces = 0; + while (hasNext()) { + curToken = nextToken(); + if (curToken instanceof Char) { + if (curToken.toChar() == '}') { + openBraces--; + if (openBraces < 0) { + break; + } + } else if (curToken.toChar() == '{') { + openBraces++; + } + buf.append(curToken.toString()); + } else { + // Output whatever is in buffer + if (buf.length() > 0) { + ELexpr.add(new ELNode.ELText(buf.toString())); + buf.setLength(0); + } + if (!parseFunction()) { + ELexpr.add(new ELNode.ELText(curToken.toString())); + } + } + } + if (curToken != null) { + buf.append(curToken.getWhiteSpace()); + } + if (buf.length() > 0) { + ELexpr.add(new ELNode.ELText(buf.toString())); + } + + return ELexpr; + } + + /** + * Parse for a function FunctionInvocation ::= (identifier ':')? identifier + * '(' (Expression (,Expression)*)? ')' Note: currently we don't parse + * arguments + */ + private boolean parseFunction() { + if (!(curToken instanceof Id) || isELReserved(curToken.toTrimmedString()) || + prevToken instanceof Char && prevToken.toChar() == '.') { + return false; + } + String s1 = null; // Function prefix + String s2 = curToken.toTrimmedString(); // Function name + int start = index - curToken.toString().length(); + Token original = curToken; + if (hasNext()) { + int mark = getIndex() - whiteSpace.length(); + curToken = nextToken(); + if (curToken.toChar() == ':') { + if (hasNext()) { + Token t2 = nextToken(); + if (t2 instanceof Id) { + s1 = s2; + s2 = t2.toTrimmedString(); + if (hasNext()) { + curToken = nextToken(); + } + } + } + } + if (curToken.toChar() == '(') { + ELexpr.add(new ELNode.Function(s1, s2, expression.substring(start, index - 1))); + return true; + } + curToken = original; + setIndex(mark); + } + return false; + } + + /** + * Test if an id is a reserved word in EL + */ + private boolean isELReserved(String id) { + int i = 0; + int j = reservedWords.length; + while (i < j) { + int k = (i + j) >>> 1; + int result = reservedWords[k].compareTo(id); + if (result == 0) { + return true; + } + if (result < 0) { + i = k + 1; + } else { + j = k; + } + } + return false; + } + + /** + * Skip until an EL expression ('${' || '#{') is reached, allowing escape + * sequences '\$' and '\#'. + * + * @return The text string up to the EL expression + */ + private String skipUntilEL() { + StringBuilder buf = new StringBuilder(); + while (hasNextChar()) { + char ch = nextChar(); + if (ch == '\\') { + // Is this the start of a "\$" or "\#" escape sequence? + char p0 = peek(0); + if (p0 == '$' || (p0 == '#' && !isDeferredSyntaxAllowedAsLiteral)) { + buf.append(nextChar()); + } else { + buf.append(ch); + } + } else if ((ch == '$' || (ch == '#' && !isDeferredSyntaxAllowedAsLiteral)) && + peek(0) == '{') { + this.type = ch; + nextChar(); + break; + } else { + buf.append(ch); + } + } + return buf.toString(); + } + + + /** + * Escape '$' and '#', inverting the unescaping performed in + * {@link #skipUntilEL()} but only for ${ and #{ sequences since escaping + * for $ and # is optional. + * + * @param input Non-EL input to be escaped + * @param isDeferredSyntaxAllowedAsLiteral Flag that indicates if deferred + * syntax (#{) is allowed as a literal.\ + * + * @return The escaped version of the input + */ + static String escapeLiteralExpression(String input, + boolean isDeferredSyntaxAllowedAsLiteral) { + int len = input.length(); + int lastAppend = 0; + StringBuilder output = null; + for (int i = 0; i < len; i++) { + char ch = input.charAt(i); + if (ch =='$' || (!isDeferredSyntaxAllowedAsLiteral && ch == '#')) { + if (i + 1 < len && input.charAt(i + 1) == '{') { + if (output == null) { + output = new StringBuilder(len + 20); + } + output.append(input.substring(lastAppend, i)); + lastAppend = i + 1; + output.append('\\'); + output.append(ch); + } + } + } + if (output == null) { + return input; + } else { + output.append(input.substring(lastAppend, len)); + return output.toString(); + } + } + + + /** + * Escape '\\', '\'' and '\"', inverting the unescaping performed in + * {@link #skipUntilEL()}. + * + * @param input Non-EL input to be escaped + * + * @return The escaped version of the input + */ + private static String escapeELText(String input) { + int len = input.length(); + char quote = 0; + int lastAppend = 0; + int start = 0; + int end = len; + + // Look to see if the value is quoted + String trimmed = input.trim(); + int trimmedLen = trimmed.length(); + if (trimmedLen > 1) { + // Might be quoted + quote = trimmed.charAt(0); + if (quote == '\'' || quote == '\"') { + if (trimmed.charAt(trimmedLen - 1) != quote) { + throw new IllegalArgumentException(Localizer.getMessage( + "org.apache.jasper.compiler.ELParser.invalidQuotesForStringLiteral", + input)); + } + start = input.indexOf(quote) + 1; + end = start + trimmedLen - 2; + } else { + quote = 0; + } + } + + StringBuilder output = null; + for (int i = start; i < end; i++) { + char ch = input.charAt(i); + if (ch == '\\' || ch == quote) { + if (output == null) { + output = new StringBuilder(len + 20); + } + output.append(input.substring(lastAppend, i)); + lastAppend = i + 1; + output.append('\\'); + output.append(ch); + } + } + if (output == null) { + return input; + } else { + output.append(input.substring(lastAppend, len)); + return output.toString(); + } + } + + + /* + * @return true if there is something left in EL expression buffer other + * than white spaces. + */ + private boolean hasNext() { + skipSpaces(); + return hasNextChar(); + } + + private String getAndResetWhiteSpace() { + String result = whiteSpace; + whiteSpace = ""; + return result; + } + + /* + * Implementation note: This method assumes that it is always preceded by a + * call to hasNext() in order for whitespace handling to be correct. + * + * @return The next token in the EL expression buffer. + */ + private Token nextToken() { + prevToken = curToken; + if (hasNextChar()) { + char ch = nextChar(); + if (Character.isJavaIdentifierStart(ch)) { + int start = index - 1; + while (index < expression.length() && + Character.isJavaIdentifierPart( + ch = expression.charAt(index))) { + nextChar(); + } + return new Id(getAndResetWhiteSpace(), expression.substring(start, index)); + } + + if (ch == '\'' || ch == '"') { + return parseQuotedChars(ch); + } else { + // For now... + return new Char(getAndResetWhiteSpace(), ch); + } + } + return null; + } + + /* + * Parse a string in single or double quotes, allowing for escape sequences + * '\\', '\"' and "\'" + */ + private Token parseQuotedChars(char quote) { + StringBuilder buf = new StringBuilder(); + buf.append(quote); + while (hasNextChar()) { + char ch = nextChar(); + if (ch == '\\') { + ch = nextChar(); + if (ch == '\\' || ch == '\'' || ch == '\"') { + buf.append(ch); + } else { + throw new IllegalArgumentException(Localizer.getMessage( + "org.apache.jasper.compiler.ELParser.invalidQuoting", + expression)); + } + } else if (ch == quote) { + buf.append(ch); + break; + } else { + buf.append(ch); + } + } + return new QuotedString(getAndResetWhiteSpace(), buf.toString()); + } + + /* + * A collection of low level parse methods dealing with character in the EL + * expression buffer. + */ + + private void skipSpaces() { + int start = index; + while (hasNextChar()) { + char c = expression.charAt(index); + if (c > ' ') { + break; + } + index++; + } + whiteSpace = expression.substring(start, index); + } + + private boolean hasNextChar() { + return index < expression.length(); + } + + private char nextChar() { + if (index >= expression.length()) { + return (char) -1; + } + return expression.charAt(index++); + } + + private char peek(int advance) { + int target = index + advance; + if (target >= expression.length()) { + return (char) -1; + } + return expression.charAt(target); + } + + private int getIndex() { + return index; + } + + private void setIndex(int i) { + index = i; + } + + /* + * Represents a token in EL expression string + */ + private static class Token { + + protected final String whiteSpace; + + Token(String whiteSpace) { + this.whiteSpace = whiteSpace; + } + + char toChar() { + return 0; + } + + @Override + public String toString() { + return whiteSpace; + } + + String toTrimmedString() { + return ""; + } + + String getWhiteSpace() { + return whiteSpace; + } + } + + /* + * Represents an ID token in EL + */ + private static class Id extends Token { + String id; + + Id(String whiteSpace, String id) { + super(whiteSpace); + this.id = id; + } + + @Override + public String toString() { + return whiteSpace + id; + } + + @Override + String toTrimmedString() { + return id; + } + } + + /* + * Represents a character token in EL + */ + private static class Char extends Token { + + private char ch; + + Char(String whiteSpace, char ch) { + super(whiteSpace); + this.ch = ch; + } + + @Override + char toChar() { + return ch; + } + + @Override + public String toString() { + return whiteSpace + ch; + } + + @Override + String toTrimmedString() { + return "" + ch; + } + } + + /* + * Represents a quoted (single or double) string token in EL + */ + private static class QuotedString extends Token { + + private String value; + + QuotedString(String whiteSpace, String v) { + super(whiteSpace); + this.value = v; + } + + @Override + public String toString() { + return whiteSpace + value; + } + + @Override + String toTrimmedString() { + return value; + } + } + + public char getType() { + return type; + } + + + static class TextBuilder extends ELNode.Visitor { + + protected final boolean isDeferredSyntaxAllowedAsLiteral; + protected final StringBuilder output = new StringBuilder(); + + protected TextBuilder(boolean isDeferredSyntaxAllowedAsLiteral) { + this.isDeferredSyntaxAllowedAsLiteral = isDeferredSyntaxAllowedAsLiteral; + } + + public String getText() { + return output.toString(); + } + + @Override + public void visit(Root n) throws JasperException { + output.append(n.getType()); + output.append('{'); + n.getExpression().visit(this); + output.append('}'); + } + + @Override + public void visit(Function n) throws JasperException { + output.append(escapeLiteralExpression(n.getOriginalText(), isDeferredSyntaxAllowedAsLiteral)); + output.append('('); + } + + @Override + public void visit(Text n) throws JasperException { + output.append(escapeLiteralExpression(n.getText(),isDeferredSyntaxAllowedAsLiteral)); + } + + @Override + public void visit(ELText n) throws JasperException { + output.append(escapeELText(n.getText())); + } + } +} diff --git a/java/org/apache/jasper/compiler/EncodingDetector.java b/java/org/apache/jasper/compiler/EncodingDetector.java new file mode 100644 index 0000000..fb7795c --- /dev/null +++ b/java/org/apache/jasper/compiler/EncodingDetector.java @@ -0,0 +1,218 @@ +/* + * 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.jasper.compiler; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +/* + * The BoM detection is derived from: + * https://svn.us.apache.org/viewvc/tomcat/trunk/java/org/apache/jasper/xmlparser/XMLEncodingDetector.java?annotate=1742248 + * + * The prolog is always at least as specific as the BOM therefore any encoding + * specified in the prolog should take priority over the BOM. + */ +class EncodingDetector { + + private static final XMLInputFactory XML_INPUT_FACTORY; + static { + ClassLoader oldCl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(EncodingDetector.class.getClassLoader()); + XML_INPUT_FACTORY = XMLInputFactory.newFactory(); + } finally { + if (oldCl != null) { + Thread.currentThread().setContextClassLoader(oldCl); + } + } + } + + private final String encoding; + private final int skip; + private final boolean encodingSpecifiedInProlog; + + + EncodingDetector(BufferedInputStream bis) throws IOException { + // Buffer is 1k. BOM is only 4 bytes. + bis.mark(4); + + BomResult bomResult = processBom(bis); + + // Reset the stream back to the start to allow the XML prolog detection + // to work. Skip any BoM we discovered. + bis.reset(); + for (int i = 0; i < bomResult.skip; i++) { + bis.read(); + } + + String prologEncoding = getPrologEncoding(bis); + if (prologEncoding == null) { + encodingSpecifiedInProlog = false; + encoding = bomResult.encoding; + } else { + encodingSpecifiedInProlog = true; + encoding = prologEncoding; + } + skip = bomResult.skip; + } + + + String getEncoding() { + return encoding; + } + + + int getSkip() { + return skip; + } + + + boolean isEncodingSpecifiedInProlog() { + return encodingSpecifiedInProlog; + } + + + private String getPrologEncoding(InputStream stream) { + String encoding = null; + try { + XMLStreamReader xmlStreamReader = XML_INPUT_FACTORY.createXMLStreamReader(stream); + encoding = xmlStreamReader.getCharacterEncodingScheme(); + } catch (XMLStreamException e) { + // Ignore + } + return encoding; + } + + + private BomResult processBom(InputStream stream) { + // Read first four bytes (or as many are available) and determine + // encoding + try { + final byte[] b4 = new byte[4]; + int count = 0; + int singleByteRead; + while (count < 4) { + singleByteRead = stream.read(); + if (singleByteRead == -1) { + break; + } + b4[count] = (byte) singleByteRead; + count++; + } + + return parseBom(b4, count); + } catch (IOException ioe) { + // Failed. + return new BomResult("UTF-8", 0); + } + } + + + private BomResult parseBom(byte[] b4, int count) { + + if (count < 2) { + return new BomResult("UTF-8", 0); + } + + // UTF-16, with BOM + int b0 = b4[0] & 0xFF; + int b1 = b4[1] & 0xFF; + if (b0 == 0xFE && b1 == 0xFF) { + // UTF-16, big-endian + return new BomResult("UTF-16BE", 2); + } + if (b0 == 0xFF && b1 == 0xFE) { + // UTF-16, little-endian + return new BomResult("UTF-16LE", 2); + } + + // default to UTF-8 if we don't have enough bytes to make a + // good determination of the encoding + if (count < 3) { + return new BomResult("UTF-8", 0); + } + + // UTF-8 with a BOM + int b2 = b4[2] & 0xFF; + if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) { + return new BomResult("UTF-8", 3); + } + + // default to UTF-8 if we don't have enough bytes to make a + // good determination of the encoding + if (count < 4) { + return new BomResult("UTF-8", 0); + } + + // Other encodings. No BOM. Try and ID encoding. + int b3 = b4[3] & 0xFF; + if (b0 == 0x00 && b1 == 0x00 && b2 == 0x00 && b3 == 0x3C) { + // UCS-4, big endian (1234) + return new BomResult("ISO-10646-UCS-4", 0); + } + if (b0 == 0x3C && b1 == 0x00 && b2 == 0x00 && b3 == 0x00) { + // UCS-4, little endian (4321) + return new BomResult("ISO-10646-UCS-4", 0); + } + if (b0 == 0x00 && b1 == 0x00 && b2 == 0x3C && b3 == 0x00) { + // UCS-4, unusual octet order (2143) + // REVISIT: What should this be? + return new BomResult("ISO-10646-UCS-4", 0); + } + if (b0 == 0x00 && b1 == 0x3C && b2 == 0x00 && b3 == 0x00) { + // UCS-4, unusual octet order (3412) + // REVISIT: What should this be? + return new BomResult("ISO-10646-UCS-4", 0); + } + if (b0 == 0x00 && b1 == 0x3C && b2 == 0x00 && b3 == 0x3F) { + // UTF-16, big-endian, no BOM + // (or could turn out to be UCS-2... + // REVISIT: What should this be? + return new BomResult("UTF-16BE", 0); + } + if (b0 == 0x3C && b1 == 0x00 && b2 == 0x3F && b3 == 0x00) { + // UTF-16, little-endian, no BOM + // (or could turn out to be UCS-2... + return new BomResult("UTF-16LE", 0); + } + if (b0 == 0x4C && b1 == 0x6F && b2 == 0xA7 && b3 == 0x94) { + // EBCDIC + // a la xerces1, return CP037 instead of EBCDIC here + return new BomResult("CP037", 0); + } + + // default encoding + return new BomResult("UTF-8", 0); + } + + + private static class BomResult { + + public final String encoding; + public final int skip; + + BomResult(String encoding, int skip) { + this.encoding = encoding; + this.skip = skip; + } + } +} diff --git a/java/org/apache/jasper/compiler/ErrorDispatcher.java b/java/org/apache/jasper/compiler/ErrorDispatcher.java new file mode 100644 index 0000000..6d3abfd --- /dev/null +++ b/java/org/apache/jasper/compiler/ErrorDispatcher.java @@ -0,0 +1,514 @@ +/* + * 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.jasper.compiler; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.xml.sax.SAXException; + +/** + * Class responsible for dispatching JSP parse and javac compilation errors + * to the configured error handler. + * + * This class is also responsible for localizing any error codes before they + * are passed on to the configured error handler. + * + * In the case of a Java compilation error, the compiler error message is + * parsed into an array of JavacErrorDetail instances, which is passed on to + * the configured error handler. + * + * @author Jan Luehe + * @author Kin-man Chung + */ +public class ErrorDispatcher { + + /** + * Custom error handler + */ + private final ErrorHandler errHandler; + + /** + * Indicates whether the compilation was initiated by JspServlet or JspC + */ + private final boolean jspcMode; + + + /** + * Constructor. + * + * @param jspcMode true if compilation has been initiated by JspC, false + * otherwise + */ + public ErrorDispatcher(boolean jspcMode) { + // XXX check web.xml for custom error handler + errHandler = new DefaultErrorHandler(); + this.jspcMode = jspcMode; + } + + /** + * Dispatches the given JSP parse error to the configured error handler. + * + * The given error code is localized. If it is not found in the + * resource bundle for localized error messages, it is used as the error + * message. + * + * @param errCode Error code + * @param args Arguments for parametric replacement + * @throws JasperException An error occurred + */ + public void jspError(String errCode, String... args) throws JasperException { + dispatch(null, errCode, args, null); + } + + /** + * Dispatches the given JSP parse error to the configured error handler. + * + * The given error code is localized. If it is not found in the + * resource bundle for localized error messages, it is used as the error + * message. + * + * @param where Error location + * @param errCode Error code + * @param args Arguments for parametric replacement + * @throws JasperException An error occurred + */ + public void jspError(Mark where, String errCode, String... args) + throws JasperException { + dispatch(where, errCode, args, null); + } + + /** + * Dispatches the given JSP parse error to the configured error handler. + * + * The given error code is localized. If it is not found in the + * resource bundle for localized error messages, it is used as the error + * message. + * + * @param n Node that caused the error + * @param errCode Error code + * @param args Arguments for parametric replacement + * @throws JasperException An error occurred + */ + public void jspError(Node n, String errCode, String... args) + throws JasperException { + dispatch(n.getStart(), errCode, args, null); + } + + /** + * Dispatches the given parsing exception to the configured error handler. + * + * @param e Parsing exception + * @throws JasperException An error occurred + */ + public void jspError(Exception e) throws JasperException { + dispatch(null, null, null, e); + } + + /** + * Dispatches the given JSP parse error to the configured error handler. + * + * The given error code is localized. If it is not found in the + * resource bundle for localized error messages, it is used as the error + * message. + * + * @param errCode Error code + * @param args Arguments for parametric replacement + * @param e Parsing exception + * @throws JasperException An error occurred + */ + public void jspError(Exception e, String errCode, String... args) + throws JasperException { + dispatch(null, errCode, args, e); + } + + /** + * Dispatches the given JSP parse error to the configured error handler. + * + * The given error code is localized. If it is not found in the + * resource bundle for localized error messages, it is used as the error + * message. + * + * @param where Error location + * @param e Parsing exception + * @param errCode Error code + * @param args Arguments for parametric replacement + * @throws JasperException An error occurred + */ + public void jspError(Mark where, Exception e, String errCode, String... args) + throws JasperException { + dispatch(where, errCode, args, e); + } + + /** + * Dispatches the given JSP parse error to the configured error handler. + * + * The given error code is localized. If it is not found in the + * resource bundle for localized error messages, it is used as the error + * message. + * + * @param n Node that caused the error + * @param e Parsing exception + * @param errCode Error code + * @param args Arguments for parametric replacement + * @throws JasperException An error occurred + */ + public void jspError(Node n, Exception e, String errCode, String... args) + throws JasperException { + dispatch(n.getStart(), errCode, args, e); + } + + /** + * Parses the given error message into an array of javac compilation error + * messages (one per javac compilation error line number). + * + * @param errMsg Error message + * @param fname Name of Java source file whose compilation failed + * @param page Node representation of JSP page from which the Java source + * file was generated + * + * @return Array of javac compilation errors, or null if the given error + * message does not contain any compilation error line numbers + * @throws JasperException An error occurred + * @throws IOException IO error which usually should not occur + */ + public static JavacErrorDetail[] parseJavacErrors(String errMsg, + String fname, + Node.Nodes page) + throws JasperException, IOException { + + return parseJavacMessage(errMsg, fname, page); + } + + /** + * Dispatches the given javac compilation errors to the configured error + * handler. + * + * @param javacErrors Array of javac compilation errors + * @throws JasperException An error occurred + */ + public void javacError(JavacErrorDetail[] javacErrors) + throws JasperException { + + errHandler.javacError(javacErrors); + } + + + /** + * Dispatches the given compilation error report and exception to the + * configured error handler. + * + * @param errorReport Compilation error report + * @param e Compilation exception + * @throws JasperException An error occurred + */ + public void javacError(String errorReport, Exception e) + throws JasperException { + + errHandler.javacError(errorReport, e); + } + + + //********************************************************************* + // Private utility methods + + /** + * Dispatches the given JSP parse error to the configured error handler. + * + * The given error code is localized. If it is not found in the + * resource bundle for localized error messages, it is used as the error + * message. + * + * @param where Error location + * @param errCode Error code + * @param args Arguments for parametric replacement + * @param e Parsing exception + * @throws JasperException An error occurred + */ + private void dispatch(Mark where, String errCode, Object[] args, + Exception e) throws JasperException { + String file = null; + String errMsg = null; + int line = -1; + int column = -1; + boolean hasLocation = false; + + // Localize + if (errCode != null) { + errMsg = Localizer.getMessage(errCode, args); + } else if (e != null) { + // give a hint about what's wrong + errMsg = e.getMessage(); + } + + // Get error location + if (where != null) { + if (jspcMode) { + // Get the full URL of the resource that caused the error + try { + URL url = where.getURL(); + if (url != null) { + file = url.toString(); + } else { + // Fallback to using context-relative path + file = where.getFile(); + } + } catch (MalformedURLException me) { + // Fallback to using context-relative path + file = where.getFile(); + } + } else { + // Get the context-relative resource path, so as to not + // disclose any local filesystem details + file = where.getFile(); + } + line = where.getLineNumber(); + column = where.getColumnNumber(); + hasLocation = true; + } + + // Get nested exception + Exception nestedEx = e; + if ((e instanceof SAXException) + && (((SAXException) e).getException() != null)) { + nestedEx = ((SAXException) e).getException(); + } + + if (hasLocation) { + errHandler.jspError(file, line, column, errMsg, nestedEx); + } else { + errHandler.jspError(errMsg, nestedEx); + } + } + + /** + * Parses the given Java compilation error message, which may contain one + * or more compilation errors, into an array of JavacErrorDetail instances. + * + * Each JavacErrorDetail instance contains the information about a single + * compilation error. + * + * @param errMsg Compilation error message that was generated by the + * javac compiler + * @param fname Name of Java source file whose compilation failed + * @param page Node representation of JSP page from which the Java source + * file was generated + * + * @return Array of JavacErrorDetail instances corresponding to the + * compilation errors + * @throws JasperException An error occurred + * @throws IOException IO error which usually should not occur + */ + private static JavacErrorDetail[] parseJavacMessage( + String errMsg, String fname, Node.Nodes page) + throws IOException, JasperException { + + List errors = new ArrayList<>(); + StringBuilder errMsgBuf = null; + int lineNum = -1; + JavacErrorDetail javacError = null; + + BufferedReader reader = new BufferedReader(new StringReader(errMsg)); + + /* + * Parse compilation errors. Each compilation error consists of a file + * path and error line number, followed by a number of lines describing + * the error. + */ + String line = null; + while ((line = reader.readLine()) != null) { + + /* + * Error line number is delimited by set of colons. + * Ignore colon following drive letter on Windows (fromIndex = 2). + * XXX Handle deprecation warnings that don't have line info + */ + int beginColon = line.indexOf(':', 2); + int endColon = line.indexOf(':', beginColon + 1); + if ((beginColon >= 0) && (endColon >= 0)) { + if (javacError != null) { + // add previous error to error vector + errors.add(javacError); + } + + String lineNumStr = line.substring(beginColon + 1, endColon); + try { + lineNum = Integer.parseInt(lineNumStr); + } catch (NumberFormatException e) { + lineNum = -1; + } + + errMsgBuf = new StringBuilder(); + + javacError = createJavacError(fname, page, errMsgBuf, lineNum); + } + + // Ignore messages preceding first error + if (errMsgBuf != null) { + errMsgBuf.append(line); + errMsgBuf.append(System.lineSeparator()); + } + } + + // Add last error to error vector + if (javacError != null) { + errors.add(javacError); + } + + reader.close(); + + JavacErrorDetail[] errDetails = null; + if (errors.size() > 0) { + errDetails = errors.toArray(new JavacErrorDetail[0]); + } + + return errDetails; + } + + + /** + * Create a compilation error. + * @param fname The file name + * @param page The page nodes + * @param errMsgBuf The error message + * @param lineNum The source line number of the error + * @return JavacErrorDetail The error details + * @throws JasperException An error occurred + */ + public static JavacErrorDetail createJavacError(String fname, + Node.Nodes page, StringBuilder errMsgBuf, int lineNum) + throws JasperException { + return createJavacError(fname, page, errMsgBuf, lineNum, null); + } + + + /** + * Create a compilation error. + * @param fname The file name + * @param page The page nodes + * @param errMsgBuf The error message + * @param lineNum The source line number of the error + * @param ctxt The compilation context + * @return JavacErrorDetail The error details + * @throws JasperException An error occurred + */ + public static JavacErrorDetail createJavacError(String fname, + Node.Nodes page, StringBuilder errMsgBuf, int lineNum, + JspCompilationContext ctxt) throws JasperException { + JavacErrorDetail javacError; + // Attempt to map javac error line number to line in JSP page + ErrorVisitor errVisitor = new ErrorVisitor(lineNum); + page.visit(errVisitor); + Node errNode = errVisitor.getJspSourceNode(); + if ((errNode != null) && (errNode.getStart() != null)) { + // If this is a scriplet node then there is a one to one mapping + // between JSP lines and Java lines + if (errVisitor.getJspSourceNode() instanceof Node.Scriptlet || + errVisitor.getJspSourceNode() instanceof Node.Declaration) { + javacError = new JavacErrorDetail( + fname, + lineNum, + errNode.getStart().getFile(), + errNode.getStart().getLineNumber() + lineNum - + errVisitor.getJspSourceNode().getBeginJavaLine(), + errMsgBuf, + ctxt); + } else { + javacError = new JavacErrorDetail( + fname, + lineNum, + errNode.getStart().getFile(), + errNode.getStart().getLineNumber(), + errMsgBuf, + ctxt); + } + } else { + /* + * javac error line number cannot be mapped to JSP page + * line number. For example, this is the case if a + * scriptlet is missing a closing brace, which causes + * havoc with the try-catch-finally block that the code + * generator places around all generated code: As a result + * of this, the javac error line numbers will be outside + * the range of begin and end java line numbers that were + * generated for the scriptlet, and therefore cannot be + * mapped to the start line number of the scriptlet in the + * JSP page. + * Include just the javac error info in the error detail. + */ + javacError = new JavacErrorDetail( + fname, + lineNum, + errMsgBuf); + } + return javacError; + } + + + /** + * Visitor responsible for mapping a line number in the generated servlet + * source code to the corresponding JSP node. + */ + private static class ErrorVisitor extends Node.Visitor { + + /** + * Java source line number to be mapped + */ + private final int lineNum; + + /** + * JSP node whose Java source code range in the generated servlet + * contains the Java source line number to be mapped + */ + private Node found; + + /** + * Constructor. + * + * @param lineNum Source line number in the generated servlet code + */ + ErrorVisitor(int lineNum) { + this.lineNum = lineNum; + } + + @Override + public void doVisit(Node n) throws JasperException { + if ((lineNum >= n.getBeginJavaLine()) + && (lineNum < n.getEndJavaLine())) { + found = n; + } + } + + /** + * Gets the JSP node to which the source line number in the generated + * servlet code was mapped. + * + * @return JSP node to which the source line number in the generated + * servlet code was mapped + */ + public Node getJspSourceNode() { + return found; + } + } +} diff --git a/java/org/apache/jasper/compiler/ErrorHandler.java b/java/org/apache/jasper/compiler/ErrorHandler.java new file mode 100644 index 0000000..8c8740e --- /dev/null +++ b/java/org/apache/jasper/compiler/ErrorHandler.java @@ -0,0 +1,76 @@ +/* + * 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.jasper.compiler; + +import org.apache.jasper.JasperException; + +/** + * Interface for handling JSP parse and javac compilation errors. + * + * An implementation of this interface may be registered with the + * ErrorDispatcher by setting the XXX initialization parameter in the JSP + * page compiler and execution servlet in Catalina's web.xml file to the + * implementation's fully qualified class name. + * + * @author Jan Luehe + * @author Kin-man Chung + */ +public interface ErrorHandler { + + /** + * Processes the given JSP parse error. + * + * @param fname Name of the JSP file in which the parse error occurred + * @param line Parse error line number + * @param column Parse error column number + * @param msg Parse error message + * @param exception Parse exception + * @throws JasperException An error occurred + */ + void jspError(String fname, int line, int column, String msg, + Exception exception) throws JasperException; + + /** + * Processes the given JSP parse error. + * + * @param msg Parse error message + * @param exception Parse exception + * @throws JasperException An error occurred + */ + void jspError(String msg, Exception exception) + throws JasperException; + + /** + * Processes the given javac compilation errors. + * + * @param details Array of JavacErrorDetail instances corresponding to the + * compilation errors + * @throws JasperException An error occurred + */ + void javacError(JavacErrorDetail[] details) + throws JasperException; + + /** + * Processes the given javac error report and exception. + * + * @param errorReport Compilation error report + * @param exception Compilation exception + * @throws JasperException An error occurred + */ + void javacError(String errorReport, Exception exception) + throws JasperException; +} diff --git a/java/org/apache/jasper/compiler/Generator.java b/java/org/apache/jasper/compiler/Generator.java new file mode 100644 index 0000000..b305431 --- /dev/null +++ b/java/org/apache/jasper/compiler/Generator.java @@ -0,0 +1,4158 @@ +/* + * 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.jasper.compiler; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.el.MethodExpression; +import jakarta.el.ValueExpression; +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagVariableInfo; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.TrimSpacesOption; +import org.apache.jasper.compiler.Node.ChildInfoBase; +import org.apache.jasper.compiler.Node.NamedAttribute; +import org.apache.jasper.runtime.JspRuntimeLibrary; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.xml.sax.Attributes; + +/** + * Generate Java source from Nodes + * + * @author Anil K. Vijendran + * @author Danno Ferrin + * @author Mandar Raje + * @author Rajiv Mordani + * @author Pierre Delisle + * + * Tomcat 4.1.x and Tomcat 5: + * @author Kin-man Chung + * @author Jan Luehe + * @author Shawn Bayern + * @author Mark Roth + * @author Denis Benoit + * + * Tomcat 6.x + * @author Jacob Hookom + * @author Remy Maucherat + */ + +class Generator { + + private final Log log = LogFactory.getLog(Generator.class); // must not be static + + private static final Class[] OBJECT_CLASS = { Object.class }; + + private static final Pattern PRE_TAG_PATTERN = Pattern.compile("(?s).*(
    |
    ).*"); + + private static final Pattern BLANK_LINE_PATTERN = Pattern.compile("(\\s*(\\n|\\r)+\\s*)"); + + private final ServletWriter out; + + private final ArrayList methodsBuffered; + + private final FragmentHelperClass fragmentHelperClass; + + private final ErrorDispatcher err; + + private final BeanRepository beanInfo; + + private final Set varInfoNames; + + private final JspCompilationContext ctxt; + + private final boolean isPoolingEnabled; + + private final boolean breakAtLF; + + private String jspIdPrefix; + + private int jspId; + + private final PageInfo pageInfo; + + private final List tagHandlerPoolNames; + + private GenBuffer charArrayBuffer; + + private final DateFormat timestampFormat; + + private final ELInterpreter elInterpreter; + + private final StringInterpreter stringInterpreter; + + /** + * @param s + * the input string + * @return quoted and escaped string, per Java rule + */ + static String quote(String s) { + + if (s == null) { + return "null"; + } + + return '"' + escape(s) + '"'; + } + + /** + * @param s the input string - must not be {@code null} + * + * @return escaped string, per Java rule + */ + static String escape(String s) { + + StringBuilder b = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '"') { + b.append('\\').append('"'); + } else if (c == '\\') { + b.append('\\').append('\\'); + } else if (c == '\n') { + b.append('\\').append('n'); + } else if (c == '\r') { + b.append('\\').append('r'); + } else { + b.append(c); + } + } + return b.toString(); + } + + /** + * Single quote and escape a character + */ + static String quote(char c) { + + StringBuilder b = new StringBuilder(); + b.append('\''); + if (c == '\'') { + b.append('\\').append('\''); + } else if (c == '\\') { + b.append('\\').append('\\'); + } else if (c == '\n') { + b.append('\\').append('n'); + } else if (c == '\r') { + b.append('\\').append('r'); + } else { + b.append(c); + } + b.append('\''); + return b.toString(); + } + + private String createJspId() { + if (this.jspIdPrefix == null) { + StringBuilder sb = new StringBuilder(32); + String name = ctxt.getServletJavaFileName(); + sb.append("jsp_"); + // Cast to long to avoid issue with Integer.MIN_VALUE + sb.append(Math.abs((long) name.hashCode())); + sb.append('_'); + this.jspIdPrefix = sb.toString(); + } + return this.jspIdPrefix + (this.jspId++); + } + + /** + * Generates declarations. This includes "info" of the page directive, and + * scriptlet declarations. + */ + private void generateDeclarations(Node.Nodes page) throws JasperException { + + class DeclarationVisitor extends Node.Visitor { + + private boolean getServletInfoGenerated = false; + + /* + * Generates getServletInfo() method that returns the value of the + * page directive's 'info' attribute, if present. + * + * The Validator has already ensured that if the translation unit + * contains more than one page directive with an 'info' attribute, + * their values match. + */ + @Override + public void visit(Node.PageDirective n) throws JasperException { + + if (getServletInfoGenerated) { + return; + } + + String info = n.getAttributeValue("info"); + if (info == null) { + return; + } + + getServletInfoGenerated = true; + out.printil("public java.lang.String getServletInfo() {"); + out.pushIndent(); + out.printin("return "); + out.print(quote(info)); + out.println(";"); + out.popIndent(); + out.printil("}"); + out.println(); + } + + @Override + public void visit(Node.Declaration n) throws JasperException { + n.setBeginJavaLine(out.getJavaLine()); + out.printMultiLn(n.getText()); + out.println(); + n.setEndJavaLine(out.getJavaLine()); + } + + // Custom Tags may contain declarations from tag plugins. + @Override + public void visit(Node.CustomTag n) throws JasperException { + if (n.useTagPlugin()) { + // If a custom tag is configured to use a plug-in + // getAtSTag() and getAtETag() will always be non-null + n.getAtSTag().visit(this); + visitBody(n); + n.getAtETag().visit(this); + } else { + visitBody(n); + } + } + } + + out.println(); + page.visit(new DeclarationVisitor()); + } + + /** + * Compiles list of tag handler pool names. + */ + private void compileTagHandlerPoolList(Node.Nodes page) + throws JasperException { + + class TagHandlerPoolVisitor extends Node.Visitor { + + private final List names; + + /* + * Constructor + * + * @param v Vector of tag handler pool names to populate + */ + TagHandlerPoolVisitor(List v) { + names = v; + } + + /* + * Gets the name of the tag handler pool for the given custom tag + * and adds it to the list of tag handler pool names unless it is + * already contained in it. + */ + @Override + public void visit(Node.CustomTag n) throws JasperException { + + if (!n.implementsSimpleTag()) { + String name = createTagHandlerPoolName(n.getPrefix(), n + .getLocalName(), n.getAttributes(), + n.getNamedAttributeNodes(), n.hasEmptyBody()); + n.setTagHandlerPoolName(name); + if (!names.contains(name)) { + names.add(name); + } + } + visitBody(n); + } + + /* + * Creates the name of the tag handler pool whose tag handlers may + * be (re)used to service this action. + * + * @return The name of the tag handler pool + */ + private String createTagHandlerPoolName(String prefix, + String shortName, Attributes attrs, Node.Nodes namedAttrs, + boolean hasEmptyBody) { + StringBuilder poolName = new StringBuilder(64); + poolName.append("_jspx_tagPool_").append(prefix).append('_') + .append(shortName); + + if (attrs != null) { + String[] attrNames = + new String[attrs.getLength() + namedAttrs.size()]; + for (int i = 0; i < attrNames.length; i++) { + attrNames[i] = attrs.getQName(i); + } + for (int i = 0; i < namedAttrs.size(); i++) { + attrNames[attrs.getLength() + i] = + namedAttrs.getNode(i).getQName(); + } + Arrays.sort(attrNames, Collections.reverseOrder()); + if (attrNames.length > 0) { + poolName.append('&'); + } + for (String attrName : attrNames) { + poolName.append('_'); + poolName.append(attrName); + } + } + if (hasEmptyBody) { + poolName.append("_nobody"); + } + return JspUtil.makeJavaIdentifier(poolName.toString()); + } + } + + page.visit(new TagHandlerPoolVisitor(tagHandlerPoolNames)); + } + + private void declareTemporaryScriptingVars(Node.Nodes page) + throws JasperException { + + class ScriptingVarVisitor extends Node.Visitor { + + private final List vars; + + ScriptingVarVisitor() { + vars = new ArrayList<>(); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + // XXX - Actually there is no need to declare those + // "_jspx_" + varName + "_" + nestingLevel variables when we are + // inside a JspFragment. + + if (n.getCustomNestingLevel() > 0) { + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + + if (varInfos.length > 0) { + for (VariableInfo varInfo : varInfos) { + String varName = varInfo.getVarName(); + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + if (!vars.contains(tmpVarName)) { + vars.add(tmpVarName); + out.printin(varInfo.getClassName()); + out.print(" "); + out.print(tmpVarName); + out.print(" = "); + out.print(null); + out.println(";"); + } + } + } else { + for (TagVariableInfo tagVarInfo : tagVarInfos) { + String varName = tagVarInfo.getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfo.getNameFromAttribute()); + } + else if (tagVarInfo.getNameFromAttribute() != null) { + // alias + continue; + } + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + if (!vars.contains(tmpVarName)) { + vars.add(tmpVarName); + out.printin(tagVarInfo.getClassName()); + out.print(" "); + out.print(tmpVarName); + out.print(" = "); + out.print(null); + out.println(";"); + } + } + } + } + + visitBody(n); + } + } + + page.visit(new ScriptingVarVisitor()); + } + + /* + * Generates getters for + * - instance manager + * - expression factory + * + * For JSPs these methods use lazy init. This is not an option for tag files + * (at least it would be more complicated to generate) because the + * ServletConfig is not readily available. + */ + private void generateGetters() { + out.printil("public jakarta.el.ExpressionFactory _jsp_getExpressionFactory() {"); + out.pushIndent(); + if (!ctxt.isTagFile()) { + out.printin("if ("); + out.print(ctxt.getOptions().getVariableForExpressionFactory()); + out.println(" == null) {"); + out.pushIndent(); + out.printil("synchronized (this) {"); + out.pushIndent(); + out.printin("if ("); + out.print(ctxt.getOptions().getVariableForExpressionFactory()); + out.println(" == null) {"); + out.pushIndent(); + out.printin(ctxt.getOptions().getVariableForExpressionFactory()); + out.println(" = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();"); + out.popIndent(); + out.printil("}"); + out.popIndent(); + out.printil("}"); + out.popIndent(); + out.printil("}"); + } + out.printin("return "); + out.print(ctxt.getOptions().getVariableForExpressionFactory()); + out.println(";"); + out.popIndent(); + out.printil("}"); + + out.println(); + + out.printil("public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {"); + out.pushIndent(); + if (!ctxt.isTagFile()) { + out.printin("if ("); + out.print(ctxt.getOptions().getVariableForInstanceManager()); + out.println(" == null) {"); + out.pushIndent(); + out.printil("synchronized (this) {"); + out.pushIndent(); + out.printin("if ("); + out.print(ctxt.getOptions().getVariableForInstanceManager()); + out.println(" == null) {"); + out.pushIndent(); + out.printin(ctxt.getOptions().getVariableForInstanceManager()); + out.println(" = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());"); + out.popIndent(); + out.printil("}"); + out.popIndent(); + out.printil("}"); + out.popIndent(); + out.printil("}"); + } + out.printin("return "); + out.print(ctxt.getOptions().getVariableForInstanceManager()); + out.println(";"); + out.popIndent(); + out.printil("}"); + + out.println(); + } + + /** + * Generates the _jspInit() method for instantiating the tag handler pools. + * For tag file, _jspInit has to be invoked manually, and the ServletConfig + * object explicitly passed. + * + * In JSP 2.1, we also instantiate an ExpressionFactory + */ + private void generateInit() { + + if (ctxt.isTagFile()) { + out.printil("private void _jspInit(jakarta.servlet.ServletConfig config) {"); + } else { + out.printil("public void _jspInit() {"); + } + + out.pushIndent(); + if (isPoolingEnabled) { + for (int i = 0; i < tagHandlerPoolNames.size(); i++) { + out.printin(tagHandlerPoolNames.get(i)); + out.print(" = org.apache.jasper.runtime.TagHandlerPool.getTagHandlerPool("); + if (ctxt.isTagFile()) { + out.print("config"); + } else { + out.print("getServletConfig()"); + } + out.println(");"); + } + } + + // Tag files can't (easily) use lazy init for these so initialise them + // here. + if (ctxt.isTagFile()) { + out.printin(ctxt.getOptions().getVariableForExpressionFactory()); + out.println(" = _jspxFactory.getJspApplicationContext(config.getServletContext()).getExpressionFactory();"); + out.printin(ctxt.getOptions().getVariableForInstanceManager()); + out.println(" = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(config);"); + } + + out.popIndent(); + out.printil("}"); + out.println(); + } + + /** + * Generates the _jspDestroy() method which is responsible for calling the + * release() method on every tag handler in any of the tag handler pools. + */ + private void generateDestroy() { + + out.printil("public void _jspDestroy() {"); + out.pushIndent(); + + if (isPoolingEnabled) { + for (int i = 0; i < tagHandlerPoolNames.size(); i++) { + out.printin(tagHandlerPoolNames.get(i)); + out.println(".release();"); + } + } + + out.popIndent(); + out.printil("}"); + out.println(); + } + + /** + * Generate preamble package name (shared by servlet and tag handler + * preamble generation). Package is always non-null as neither Servlets nor + * tags can use a default package. + */ + private void genPreamblePackage(String packageName) { + out.printil("package " + packageName + ";"); + out.println(); + } + + /** + * Generate preamble imports (shared by servlet and tag handler preamble + * generation) + */ + private void genPreambleImports() { + for (String i : pageInfo.getImports()) { + out.printin("import "); + out.print(i); + out.println(";"); + } + + out.println(); + } + + /** + * Generation of static initializers in preamble. For example, dependent + * list, el function map, prefix map. (shared by servlet and tag handler + * preamble generation) + */ + private void genPreambleStaticInitializers() { + out.printil("private static final jakarta.servlet.jsp.JspFactory _jspxFactory ="); + out.printil(" jakarta.servlet.jsp.JspFactory.getDefaultFactory();"); + out.println(); + + // Static data for getDependants() + out.printil("private static java.util.Map _jspx_dependants;"); + out.println(); + Map dependants = pageInfo.getDependants(); + if (!dependants.isEmpty()) { + out.printil("static {"); + out.pushIndent(); + out.printin("_jspx_dependants = new java.util.HashMap("); + out.print("" + dependants.size()); + out.println(");"); + for (Entry entry : dependants.entrySet()) { + out.printin("_jspx_dependants.put(\""); + out.print(entry.getKey()); + out.print("\", Long.valueOf("); + out.print(entry.getValue().toString()); + out.println("L));"); + } + out.popIndent(); + out.printil("}"); + out.println(); + } + + // Static data for getImports() + List imports = pageInfo.getImports(); + Set packages = new HashSet<>(); + Set classes = new HashSet<>(); + for (String importName : imports) { + String trimmed = importName.trim(); + if (trimmed.endsWith(".*")) { + packages.add(trimmed.substring(0, trimmed.length() - 2)); + } else { + classes.add(trimmed); + } + } + out.printil("private static final java.util.Set _jspx_imports_packages;"); + out.println(); + out.printil("private static final java.util.Set _jspx_imports_classes;"); + out.println(); + out.printil("static {"); + out.pushIndent(); + // Packages is never empty because o.a.j.Constants.STANDARD_IMPORTS + // contains 3 packages and is always added to the imports. + out.printin("_jspx_imports_packages = new java.util.LinkedHashSet<>("); + out.print(Integer.toString(packages.size())); + out.print(");"); + out.println(); + for (String packageName : packages) { + out.printin("_jspx_imports_packages.add(\""); + out.print(packageName); + out.println("\");"); + } + // classes however, may be empty depending on the import declarations + if (classes.size() == 0) { + out.printin("_jspx_imports_classes = null;"); + out.println(); + } else { + out.printin("_jspx_imports_classes = new java.util.LinkedHashSet<>("); + out.print(Integer.toString(classes.size())); + out.print(");"); + out.println(); + for (String className : classes) { + out.printin("_jspx_imports_classes.add(\""); + out.print(className); + out.println("\");"); + } + } + out.popIndent(); + out.printil("}"); + out.println(); + } + + /** + * Declare tag handler pools (tags of the same type and with the same + * attribute set share the same tag handler pool) (shared by servlet and tag + * handler preamble generation) + * + * In JSP 2.1, we also scope an instance of ExpressionFactory + */ + private void genPreambleClassVariableDeclarations() { + if (isPoolingEnabled && !tagHandlerPoolNames.isEmpty()) { + for (int i = 0; i < tagHandlerPoolNames.size(); i++) { + out.printil("private org.apache.jasper.runtime.TagHandlerPool " + tagHandlerPoolNames.get(i) + ";"); + } + out.println(); + } + out.printin("private volatile jakarta.el.ExpressionFactory "); + out.print(ctxt.getOptions().getVariableForExpressionFactory()); + out.println(";"); + out.printin("private volatile org.apache.tomcat.InstanceManager "); + out.print(ctxt.getOptions().getVariableForInstanceManager()); + out.println(";"); + out.println(); + } + + /** + * Declare general-purpose methods (shared by servlet and tag handler + * preamble generation) + */ + private void genPreambleMethods() { + // Implement JspSourceDependent + out.printil("public java.util.Map getDependants() {"); + out.pushIndent(); + out.printil("return _jspx_dependants;"); + out.popIndent(); + out.printil("}"); + out.println(); + + // Implement JspSourceImports + out.printil("public java.util.Set getPackageImports() {"); + out.pushIndent(); + out.printil("return _jspx_imports_packages;"); + out.popIndent(); + out.printil("}"); + out.println(); + out.printil("public java.util.Set getClassImports() {"); + out.pushIndent(); + out.printil("return _jspx_imports_classes;"); + out.popIndent(); + out.printil("}"); + out.println(); + + // Implement JspSourceDirectives + out.printil("public boolean getErrorOnELNotFound() {"); + out.pushIndent(); + if (pageInfo.isErrorOnELNotFound()) { + out.printil("return true;"); + } else { + out.printil("return false;"); + } + out.popIndent(); + out.printil("}"); + out.println(); + + generateGetters(); + generateInit(); + generateDestroy(); + } + + /** + * Generates the beginning of the static portion of the servlet. + */ + private void generatePreamble(Node.Nodes page) throws JasperException { + + String servletPackageName = ctxt.getServletPackageName(); + String servletClassName = ctxt.getServletClassName(); + String serviceMethodName = ctxt.getOptions().getServiceMethodName(); + + // First the package name: + genPreamblePackage(servletPackageName); + + // Generate imports + genPreambleImports(); + + // Generate class declaration + out.printin("public final class "); + out.print(servletClassName); + out.print(" extends "); + out.println(pageInfo.getExtends()); + out.printin(" implements org.apache.jasper.runtime.JspSourceDependent,"); + out.println(); + out.printin(" org.apache.jasper.runtime.JspSourceImports"); + out.println(","); + out.printin(" org.apache.jasper.runtime.JspSourceDirectives"); + out.println(" {"); + out.pushIndent(); + + // Class body begins here + generateDeclarations(page); + + // Static initializations here + genPreambleStaticInitializers(); + + // Class variable declarations + genPreambleClassVariableDeclarations(); + + // Methods here + genPreambleMethods(); + + // Now the service method + if (pageInfo.isThreadSafe()) { + out.printin("public void "); + } else { + // This is unlikely to perform well. + out.printin("public synchronized void "); + // As required by JSP 3.1, log a warning + log.warn(Localizer.getMessage("jsp.warning.isThreadSafe", ctxt.getJspFile())); + } + out.print(serviceMethodName); + out.println("(final jakarta.servlet.http.HttpServletRequest request, final jakarta.servlet.http.HttpServletResponse response)"); + out.pushIndent(); + out.pushIndent(); + out.printil("throws java.io.IOException, jakarta.servlet.ServletException {"); + out.popIndent(); + out.println(); + + // Method check + if (!pageInfo.isErrorPage()) { + out.printil("if (!jakarta.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {"); + out.pushIndent(); + out.printil("final java.lang.String _jspx_method = request.getMethod();"); + out.printil("if (\"OPTIONS\".equals(_jspx_method)) {"); + out.pushIndent(); + out.printil("response.setHeader(\"Allow\",\"GET, HEAD, POST, OPTIONS\");"); + out.printil("return;"); + out.popIndent(); + out.printil("}"); + out.printil("if (!\"GET\".equals(_jspx_method) && !\"POST\".equals(_jspx_method) && !\"HEAD\".equals(_jspx_method)) {"); + out.pushIndent(); + out.printil("response.setHeader(\"Allow\",\"GET, HEAD, POST, OPTIONS\");"); + out.printin("response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "); + out.println("\"" + Localizer.getMessage("jsp.error.servlet.invalid.method") + "\");"); + out.printil("return;"); + out.popIndent(); + out.printil("}"); + out.popIndent(); + out.printil("}"); + out.println(); + } + + // Local variable declarations + out.printil("final jakarta.servlet.jsp.PageContext pageContext;"); + + if (pageInfo.isSession()) { + out.printil("jakarta.servlet.http.HttpSession session = null;"); + } + + if (pageInfo.isErrorPage()) { + out.printil("java.lang.Throwable exception = org.apache.jasper.runtime.JspRuntimeLibrary.getThrowable(request);"); + out.printil("if (exception != null) {"); + out.pushIndent(); + out.printil("response.setStatus(jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR);"); + out.popIndent(); + out.printil("}"); + } + + out.printil("final jakarta.servlet.ServletContext application;"); + out.printil("final jakarta.servlet.ServletConfig config;"); + out.printil("jakarta.servlet.jsp.JspWriter out = null;"); + out.printil("final java.lang.Object page = this;"); + + out.printil("jakarta.servlet.jsp.JspWriter _jspx_out = null;"); + out.printil("jakarta.servlet.jsp.PageContext _jspx_page_context = null;"); + out.println(); + + declareTemporaryScriptingVars(page); + out.println(); + + out.printil("try {"); + out.pushIndent(); + + out.printin("response.setContentType("); + out.print(quote(pageInfo.getContentType())); + out.println(");"); + + if (ctxt.getOptions().isXpoweredBy()) { + out.printil("response.addHeader(\"X-Powered-By\", \"JSP/3.1\");"); + } + + out.printil("pageContext = _jspxFactory.getPageContext(this, request, response,"); + out.printin("\t\t\t"); + out.print(quote(pageInfo.getErrorPage())); + out.print(", " + pageInfo.isSession()); + out.print(", " + pageInfo.getBuffer()); + out.print(", " + pageInfo.isAutoFlush()); + out.println(");"); + out.printil("_jspx_page_context = pageContext;"); + + out.printil("application = pageContext.getServletContext();"); + out.printil("config = pageContext.getServletConfig();"); + + if (pageInfo.isSession()) { + out.printil("session = pageContext.getSession();"); + } + out.printil("out = pageContext.getOut();"); + out.printil("_jspx_out = out;"); + out.println(); + } + + /** + * Generates an XML Prolog, which includes an XML declaration and an XML + * doctype declaration. + */ + private void generateXmlProlog(Node.Nodes page) { + + /* + * An XML declaration is generated under the following conditions: - + * 'omit-xml-declaration' attribute of action is set to + * "no" or "false" - JSP document without a + */ + String omitXmlDecl = pageInfo.getOmitXmlDecl(); + if ((omitXmlDecl != null && !JspUtil.booleanValue(omitXmlDecl)) || + (omitXmlDecl == null && page.getRoot().isXmlSyntax() && !pageInfo.hasJspRoot() && !ctxt.isTagFile())) { + String cType = pageInfo.getContentType(); + String charSet = cType.substring(cType.indexOf("charset=") + 8); + out.printil("out.write(\"\\n\");"); + } + + /* + * Output a DOCTYPE declaration if the doctype-root-element appears. If + * doctype-public appears: else + */ + + String doctypeName = pageInfo.getDoctypeName(); + if (doctypeName != null) { + String doctypePublic = pageInfo.getDoctypePublic(); + String doctypeSystem = pageInfo.getDoctypeSystem(); + out.printin("out.write(\"\\n\");"); + } + } + + /** + * A visitor that generates codes for the elements in the page. + */ + private class GenerateVisitor extends Node.Visitor { + + /* + * Map containing introspection information on tag handlers: + * : tag prefix : Map containing introspection on tag + * handlers: : tag short name : introspection info of tag + * handler for tag + */ + private final Map> handlerInfos; + + private final Map tagVarNumbers; + + private String parent; + + private boolean isSimpleTagParent; // Is parent a SimpleTag? + + private String pushBodyCountVar; + + private String simpleTagHandlerVar; + + private boolean isSimpleTagHandler; + + private boolean isFragment; + + private final boolean isTagFile; + + private ServletWriter out; + + private final ArrayList methodsBuffered; + + private final FragmentHelperClass fragmentHelperClass; + + private int methodNesting; + + private int charArrayCount; + + private HashMap textMap; + + private final boolean useInstanceManagerForTags; + + GenerateVisitor(boolean isTagFile, ServletWriter out, + ArrayList methodsBuffered, + FragmentHelperClass fragmentHelperClass, + boolean useInstanceManagerForTags) { + + this.isTagFile = isTagFile; + this.out = out; + this.methodsBuffered = methodsBuffered; + this.fragmentHelperClass = fragmentHelperClass; + this.useInstanceManagerForTags = useInstanceManagerForTags; + methodNesting = 0; + handlerInfos = new HashMap<>(); + tagVarNumbers = new HashMap<>(); + textMap = new HashMap<>(); + } + + /** + * Returns an attribute value, optionally URL encoded. If the value is a + * runtime expression, the result is the expression itself, as a string. + * If the result is an EL expression, we insert a call to the + * interpreter. If the result is a Named Attribute we insert the + * generated variable name. Otherwise the result is a string literal, + * quoted and escaped. + * + * @param attr + * An JspAttribute object + * @param encode + * true if to be URL encoded + * @param expectedType + * the expected type for an EL evaluation (ignored for + * attributes that aren't EL expressions) + */ + private String attributeValue(Node.JspAttribute attr, boolean encode, + Class expectedType) { + String v = attr.getValue(); + if (attr.isExpression()) { + if (encode) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode(String.valueOf(" + + v + "), request.getCharacterEncoding())"; + } + return v; + } else if (attr.isELInterpreterInput()) { + v = elInterpreter.interpreterCall(ctxt, this.isTagFile, v, + expectedType, attr.getEL().getMapName()); + if (encode) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode(" + + v + ", request.getCharacterEncoding())"; + } + return v; + } else if (attr.isNamedAttribute()) { + return attr.getNamedAttributeNode().getTemporaryVariableName(); + } else { + if (encode) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode(" + + quote(v) + ", request.getCharacterEncoding())"; + } + return quote(v); + } + } + + + /** + * Prints the attribute value specified in the param action, in the form + * of name=value string. + * + * @param n + * the parent node for the param action nodes. + */ + private void printParams(Node n, String pageParam, boolean literal) + throws JasperException { + + class ParamVisitor extends Node.Visitor { + private String separator; + + ParamVisitor(String separator) { + this.separator = separator; + } + + @Override + public void visit(Node.ParamAction n) throws JasperException { + + out.print(" + "); + out.print(separator); + out.print(" + "); + out.print("org.apache.jasper.runtime.JspRuntimeLibrary." + + "URLEncode(" + quote(n.getTextAttribute("name")) + + ", request.getCharacterEncoding())"); + out.print("+ \"=\" + "); + out.print(attributeValue(n.getValue(), true, String.class)); + + // The separator is '&' after the second use + separator = "\"&\""; + } + } + + String sep; + if (literal) { + sep = pageParam.indexOf('?') > 0 ? "\"&\"" : "\"?\""; + } else { + sep = "((" + pageParam + ").indexOf('?')>0? '&': '?')"; + } + if (n.getBody() != null) { + n.getBody().visit(new ParamVisitor(sep)); + } + } + + @Override + public void visit(Node.Expression n) throws JasperException { + n.setBeginJavaLine(out.getJavaLine()); + out.printin("out.print("); + out.printMultiLn(n.getText()); + out.println(");"); + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.Scriptlet n) throws JasperException { + n.setBeginJavaLine(out.getJavaLine()); + out.printMultiLn(n.getText()); + out.println(); + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.ELExpression n) throws JasperException { + n.setBeginJavaLine(out.getJavaLine()); + out.printil("out.write(" + + elInterpreter.interpreterCall(ctxt, this.isTagFile, + n.getType() + "{" + n.getText() + "}", + String.class, n.getEL().getMapName()) + + ");"); + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.IncludeAction n) throws JasperException { + + String flush = n.getTextAttribute("flush"); + Node.JspAttribute page = n.getPage(); + + boolean isFlush = "true".equals(flush); + + n.setBeginJavaLine(out.getJavaLine()); + + String pageParam; + if (page.isNamedAttribute()) { + // If the page for jsp:include was specified via + // jsp:attribute, first generate code to evaluate + // that body. + pageParam = generateNamedAttributeValue(page + .getNamedAttributeNode()); + } else { + pageParam = attributeValue(page, false, String.class); + } + + // If any of the params have their values specified by + // jsp:attribute, prepare those values first. + Node jspBody = findJspBody(n); + if (jspBody != null) { + prepareParams(jspBody); + } else { + prepareParams(n); + } + + out.printin("org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, " + + pageParam); + printParams(n, pageParam, page.isLiteral()); + out.println(", out, " + isFlush + ");"); + + n.setEndJavaLine(out.getJavaLine()); + } + + /** + * Scans through all child nodes of the given parent for <param> + * subelements. For each <param> element, if its value is specified via + * a Named Attribute (<jsp:attribute>), generate the code to evaluate + * those bodies first. + *

    + * {@code parent} is assumed to be non-null + */ + private void prepareParams(Node parent) throws JasperException { + Node.Nodes subelements = parent.getBody(); + if (subelements != null) { + for (int i = 0; i < subelements.size(); i++) { + Node n = subelements.getNode(i); + // Validation during parsing ensures n is an instance of + // Node.ParamAction + Node.Nodes paramSubElements = n.getBody(); + for (int j = 0; (paramSubElements != null) + && (j < paramSubElements.size()); j++) { + Node m = paramSubElements.getNode(j); + if (m instanceof Node.NamedAttribute) { + generateNamedAttributeValue((Node.NamedAttribute) m); + } + } + } + } + } + + /** + * Finds the <jsp:body> subelement of the given parent node. If not + * found, null is returned. + */ + private Node.JspBody findJspBody(Node parent) { + Node.JspBody result = null; + + Node.Nodes subelements = parent.getBody(); + for (int i = 0; (subelements != null) && (i < subelements.size()); i++) { + Node n = subelements.getNode(i); + if (n instanceof Node.JspBody) { + result = (Node.JspBody) n; + break; + } + } + + return result; + } + + @Override + public void visit(Node.ForwardAction n) throws JasperException { + Node.JspAttribute page = n.getPage(); + + n.setBeginJavaLine(out.getJavaLine()); + + out.printil("if (true) {"); // So that javac won't complain about + out.pushIndent(); // codes after "return" + + String pageParam; + if (page.isNamedAttribute()) { + // If the page for jsp:forward was specified via + // jsp:attribute, first generate code to evaluate + // that body. + pageParam = generateNamedAttributeValue(page.getNamedAttributeNode()); + } else { + pageParam = attributeValue(page, false, String.class); + } + + // If any of the params have their values specified by + // jsp:attribute, prepare those values first. + Node jspBody = findJspBody(n); + if (jspBody != null) { + prepareParams(jspBody); + } else { + prepareParams(n); + } + + out.printin("_jspx_page_context.forward("); + out.print(pageParam); + printParams(n, pageParam, page.isLiteral()); + out.println(");"); + if (isTagFile || isFragment) { + out.printil("throw new jakarta.servlet.jsp.SkipPageException();"); + } else { + out.printil((methodNesting > 0) ? "return true;" : "return;"); + } + out.popIndent(); + out.printil("}"); + + n.setEndJavaLine(out.getJavaLine()); + // XXX Not sure if we can eliminate dead codes after this. + } + + @Override + public void visit(Node.GetProperty n) throws JasperException { + String name = n.getTextAttribute("name"); + String property = n.getTextAttribute("property"); + + n.setBeginJavaLine(out.getJavaLine()); + + if (beanInfo.checkVariable(name)) { + // Bean is defined using useBean, introspect at compile time + Class bean = beanInfo.getBeanType(name); + String beanName = bean.getCanonicalName(); + Method meth = JspRuntimeLibrary.getReadMethod(bean, property); + String methodName = meth.getName(); + out.printil("out.write(org.apache.jasper.runtime.JspRuntimeLibrary.toString(" + + "(((" + + beanName + + ")_jspx_page_context.findAttribute(" + + "\"" + + name + "\"))." + methodName + "())));"); + } else if (!ctxt.getOptions().getStrictGetProperty() || varInfoNames.contains(name)) { + // The object is a custom action with an associated + // VariableInfo entry for this name. + // Get the class name and then introspect at runtime. + out.printil("out.write(org.apache.jasper.runtime.JspRuntimeLibrary.toString" + + "(org.apache.jasper.runtime.JspRuntimeLibrary.handleGetProperty" + + "(_jspx_page_context.findAttribute(\"" + + name + + "\"), \"" + + property + + "\")));"); + } else { + throw new JasperException(Localizer.getMessage("jsp.error.invalid.name", n.getStart(), name)); + } + + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.SetProperty n) throws JasperException { + String name = n.getTextAttribute("name"); + String property = n.getTextAttribute("property"); + String param = n.getTextAttribute("param"); + Node.JspAttribute value = n.getValue(); + + n.setBeginJavaLine(out.getJavaLine()); + + if ("*".equals(property)) { + out.printil("org.apache.jasper.runtime.JspRuntimeLibrary.introspect(" + + "_jspx_page_context.findAttribute(" + + "\"" + + name + "\"), request);"); + } else if (value == null) { + if (param == null) + { + param = property; // default to same as property + } + out.printil("org.apache.jasper.runtime.JspRuntimeLibrary.introspecthelper(" + + "_jspx_page_context.findAttribute(\"" + + name + + "\"), \"" + + property + + "\", request.getParameter(\"" + + param + + "\"), " + + "request, \"" + + param + + "\", false);"); + } else if (value.isExpression()) { + out.printil("org.apache.jasper.runtime.JspRuntimeLibrary.handleSetProperty(" + + "_jspx_page_context.findAttribute(\"" + + name + + "\"), \"" + property + "\","); + out.print(attributeValue(value, false, null)); + out.println(");"); + } else if (value.isELInterpreterInput()) { + // We've got to resolve the very call to the interpreter + // at runtime since we don't know what type to expect + // in the general case; we thus can't hard-wire the call + // into the generated code. (XXX We could, however, + // optimize the case where the bean is exposed with + // , much as the code here does for + // getProperty.) + + // The following holds true for the arguments passed to + // JspRuntimeLibrary.handleSetPropertyExpression(): + // - 'pageContext' is a VariableResolver. + // - 'this' (either the generated Servlet or the generated tag + // handler for Tag files) is a FunctionMapper. + out.printil("org.apache.jasper.runtime.JspRuntimeLibrary.handleSetPropertyExpression(" + + "_jspx_page_context.findAttribute(\"" + + name + + "\"), \"" + + property + + "\", " + + quote(value.getValue()) + + ", " + + "_jspx_page_context, " + + value.getEL().getMapName() + ");"); + } else if (value.isNamedAttribute()) { + // If the value for setProperty was specified via + // jsp:attribute, first generate code to evaluate + // that body. + String valueVarName = generateNamedAttributeValue(value + .getNamedAttributeNode()); + out.printil("org.apache.jasper.runtime.JspRuntimeLibrary.introspecthelper(" + + "_jspx_page_context.findAttribute(\"" + + name + + "\"), \"" + + property + + "\", " + + valueVarName + + ", null, null, false);"); + } else { + out.printin("org.apache.jasper.runtime.JspRuntimeLibrary.introspecthelper(" + + "_jspx_page_context.findAttribute(\"" + + name + + "\"), \"" + property + "\", "); + out.print(attributeValue(value, false, null)); + out.println(", null, null, false);"); + } + + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.UseBean n) throws JasperException { + + String name = n.getTextAttribute("id"); + String scope = n.getTextAttribute("scope"); + String klass = n.getTextAttribute("class"); + String type = n.getTextAttribute("type"); + Node.JspAttribute beanName = n.getBeanName(); + + // If "class" is specified, try an instantiation at compile time + boolean generateNew = false; + String canonicalName = null; // Canonical name for klass + if (klass != null) { + try { + Class bean = ctxt.getClassLoader().loadClass(klass); + if (klass.indexOf('$') >= 0) { + // Obtain the canonical type name + canonicalName = bean.getCanonicalName(); + } else { + canonicalName = klass; + } + // Check that there is a 0 arg constructor + Constructor constructor = bean.getConstructor(new Class[] {}); + // Check the bean is public, not an interface, not abstract + // and in an exported module + int modifiers = bean.getModifiers(); + // No need to test for being an interface here as the + // getConstructor() call above will have already failed for + // any interfaces. + if (!Modifier.isPublic(modifiers) || + Modifier.isAbstract(modifiers) || + !constructor.canAccess(null) ) { + throw new JasperException(Localizer.getMessage("jsp.error.invalid.bean", + Integer.valueOf(modifiers))); + } + // At compile time, we have determined that the bean class + // exists, with a public zero constructor, new() can be + // used for bean instantiation. + generateNew = true; + } catch (Exception e) { + // Cannot instantiate the specified class, either a + // compilation error or a runtime error will be raised, + // depending on a compiler flag. + if (ctxt.getOptions() + .getErrorOnUseBeanInvalidClassAttribute()) { + err.jspError(n, "jsp.error.invalid.bean", klass); + } + if (canonicalName == null) { + // Doing our best here to get a canonical name + // from the binary name, should work 99.99% of time. + canonicalName = klass.replace('$', '.'); + } + } + if (type == null) { + // if type is unspecified, use "class" as type of bean + type = canonicalName; + } + } + + // JSP.5.1, Semantics, para 1 - lock not required for request or + // page scope + String scopename = "jakarta.servlet.jsp.PageContext.PAGE_SCOPE"; // Default to page + String lock = null; + + if ("request".equals(scope)) { + scopename = "jakarta.servlet.jsp.PageContext.REQUEST_SCOPE"; + } else if ("session".equals(scope)) { + scopename = "jakarta.servlet.jsp.PageContext.SESSION_SCOPE"; + lock = "session"; + } else if ("application".equals(scope)) { + scopename = "jakarta.servlet.jsp.PageContext.APPLICATION_SCOPE"; + lock = "application"; + } + + n.setBeginJavaLine(out.getJavaLine()); + + // Declare bean + out.printin(type); + out.print(' '); + out.print(name); + out.println(" = null;"); + + // Lock (if required) while getting or creating bean + if (lock != null) { + out.printin("synchronized ("); + out.print(lock); + out.println(") {"); + out.pushIndent(); + } + + // Locate bean from context + out.printin(name); + out.print(" = ("); + out.print(type); + out.print(") _jspx_page_context.getAttribute("); + out.print(quote(name)); + out.print(", "); + out.print(scopename); + out.println(");"); + + // Create bean + /* + * Check if bean is already there + */ + out.printin("if ("); + out.print(name); + out.println(" == null){"); + out.pushIndent(); + if (klass == null && beanName == null) { + /* + * If both class name and beanName is not specified, the bean + * must be found locally, otherwise it's an error + */ + out.printin("throw new java.lang.InstantiationException(\"bean "); + out.print(name); + out.println(" not found within scope\");"); + } else { + /* + * Instantiate the bean if it is not in the specified scope. + */ + if (!generateNew) { + String binaryName; + if (beanName != null) { + if (beanName.isNamedAttribute()) { + // If the value for beanName was specified via + // jsp:attribute, first generate code to evaluate + // that body. + binaryName = generateNamedAttributeValue(beanName + .getNamedAttributeNode()); + } else { + binaryName = attributeValue(beanName, false, String.class); + } + } else { + // Implies klass is not null + binaryName = quote(klass); + } + out.printil("try {"); + out.pushIndent(); + out.printin(name); + out.print(" = ("); + out.print(type); + out.print(") java.beans.Beans.instantiate("); + out.print("this.getClass().getClassLoader(), "); + out.print(binaryName); + out.println(");"); + out.popIndent(); + /* + * Note: Beans.instantiate throws ClassNotFoundException if + * the bean class is abstract. + */ + out.printil("} catch (java.lang.ClassNotFoundException exc) {"); + out.pushIndent(); + out.printil("throw new InstantiationException(exc.getMessage());"); + out.popIndent(); + out.printil("} catch (java.lang.Exception exc) {"); + out.pushIndent(); + out.printin("throw new jakarta.servlet.ServletException("); + out.print("\"Cannot create bean of class \" + "); + out.print(binaryName); + out.println(", exc);"); + out.popIndent(); + out.printil("}"); // close of try + } else { + // Implies klass is not null + // Generate codes to instantiate the bean class + out.printin(name); + out.print(" = new "); + out.print(canonicalName); + out.println("();"); + } + /* + * Set attribute for bean in the specified scope + */ + out.printin("_jspx_page_context.setAttribute("); + out.print(quote(name)); + out.print(", "); + out.print(name); + out.print(", "); + out.print(scopename); + out.println(");"); + + // Only visit the body when bean is instantiated + visitBody(n); + } + out.popIndent(); + out.printil("}"); + + // End of lock block + if (lock != null) { + out.popIndent(); + out.printil("}"); + } + + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.PlugIn n) throws JasperException { + + // As of JSP 3.1, jsp:plugin must not generate any output + n.setBeginJavaLine(out.getJavaLine()); + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.NamedAttribute n) throws JasperException { + // Don't visit body of this tag - we already did earlier. + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + + // Use plugin to generate more efficient code if there is one. + if (n.useTagPlugin()) { + generateTagPlugin(n); + return; + } + + TagHandlerInfo handlerInfo = getTagHandlerInfo(n); + + // Create variable names + String baseVar = createTagVarName(n.getQName(), n.getPrefix(), n + .getLocalName()); + String tagEvalVar = "_jspx_eval_" + baseVar; + String tagHandlerVar = "_jspx_th_" + baseVar; + String tagPushBodyCountVar = "_jspx_push_body_count_" + baseVar; + + // If the tag contains no scripting element, generate its codes + // to a method. + ServletWriter outSave = null; + Node.ChildInfo ci = n.getChildInfo(); + if (ci.isScriptless() && !ci.hasScriptingVars()) { + // The tag handler and its body code can reside in a separate + // method if it is scriptless and does not have any scripting + // variable defined. + + String tagMethod = "_jspx_meth_" + baseVar; + + // Generate a call to this method + out.printin("if ("); + out.print(tagMethod); + out.print("("); + if (parent != null) { + out.print(parent); + out.print(", "); + } + out.print("_jspx_page_context"); + if (pushBodyCountVar != null) { + out.print(", "); + out.print(pushBodyCountVar); + } + out.println("))"); + out.pushIndent(); + out.printil((methodNesting > 0) ? "return true;" : "return;"); + out.popIndent(); + + // Set up new buffer for the method + outSave = out; + /* + * For fragments, their bodies will be generated in fragment + * helper classes, and the Java line adjustments will be done + * there, hence they are set to null here to avoid double + * adjustments. + */ + GenBuffer genBuffer = new GenBuffer(n, + n.implementsSimpleTag() ? null : n.getBody()); + methodsBuffered.add(genBuffer); + out = genBuffer.getOut(); + + methodNesting++; + // Generate code for method declaration + out.println(); + out.pushIndent(); + out.printin("private boolean "); + out.print(tagMethod); + out.print("("); + if (parent != null) { + out.print("jakarta.servlet.jsp.tagext.JspTag "); + out.print(parent); + out.print(", "); + } + out.print("jakarta.servlet.jsp.PageContext _jspx_page_context"); + if (pushBodyCountVar != null) { + out.print(", int[] "); + out.print(pushBodyCountVar); + } + out.println(")"); + out.printil(" throws java.lang.Throwable {"); + out.pushIndent(); + + // Initialize local variables used in this method. + if (!isTagFile) { + out.printil("jakarta.servlet.jsp.PageContext pageContext = _jspx_page_context;"); + } + // Only need to define out if the tag has a non-empty body, + // implements TryCatchFinally or uses + // ... nodes + if (!n.hasEmptyBody() || n.implementsTryCatchFinally() || n.getNamedAttributeNodes().size() > 0) { + out.printil("jakarta.servlet.jsp.JspWriter out = _jspx_page_context.getOut();"); + } + generateLocalVariables(out, n); + } + + // Add the named objects to the list of 'introduced' names to enable + // a later test as per JSP.5.3 + VariableInfo[] infos = n.getVariableInfos(); + // The Validator always calls setTagData() which ensures infos is + // non-null + if (infos.length > 0) { + for (VariableInfo info : infos) { + // A null variable name will trigger multiple compilation + // failures so assume non-null here + pageInfo.getVarInfoNames().add(info.getVarName()); + } + } + TagVariableInfo[] tagInfos = n.getTagVariableInfos(); + // The way Tomcat constructs the TagInfo, getTagVariableInfos() + // will never return null. + if (tagInfos.length > 0) { + for (TagVariableInfo tagInfo : tagInfos) { + // tagInfo is always non-null + String name = tagInfo.getNameGiven(); + if (name == null) { + String nameFromAttribute = + tagInfo.getNameFromAttribute(); + name = n.getAttributeValue(nameFromAttribute); + } + pageInfo.getVarInfoNames().add(name); + } + } + + + if (n.implementsSimpleTag()) { + generateCustomDoTag(n, handlerInfo, tagHandlerVar); + } else { + /* + * Classic tag handler: Generate code for start element, body, + * and end element + */ + generateCustomStart(n, handlerInfo, tagHandlerVar, tagEvalVar, + tagPushBodyCountVar); + + // visit body + String tmpParent = parent; + parent = tagHandlerVar; + boolean isSimpleTagParentSave = isSimpleTagParent; + isSimpleTagParent = false; + String tmpPushBodyCountVar = null; + if (n.implementsTryCatchFinally()) { + tmpPushBodyCountVar = pushBodyCountVar; + pushBodyCountVar = tagPushBodyCountVar; + } + boolean tmpIsSimpleTagHandler = isSimpleTagHandler; + isSimpleTagHandler = false; + + visitBody(n); + + parent = tmpParent; + isSimpleTagParent = isSimpleTagParentSave; + if (n.implementsTryCatchFinally()) { + pushBodyCountVar = tmpPushBodyCountVar; + } + isSimpleTagHandler = tmpIsSimpleTagHandler; + + generateCustomEnd(n, tagHandlerVar, tagEvalVar, + tagPushBodyCountVar); + } + + if (ci.isScriptless() && !ci.hasScriptingVars()) { + // Generate end of method + out.printil("return false;"); + out.popIndent(); + out.printil("}"); + out.popIndent(); + + methodNesting--; + + // restore previous writer + out = outSave; + } + + } + + private static final String DOUBLE_QUOTE = "\\\""; + + @Override + public void visit(Node.UninterpretedTag n) throws JasperException { + + n.setBeginJavaLine(out.getJavaLine()); + + /* + * Write begin tag + */ + out.printin("out.write(\"<"); + out.print(n.getQName()); + + Attributes attrs = n.getNonTaglibXmlnsAttributes(); + if (attrs != null) { + for (int i = 0; i < attrs.getLength(); i++) { + out.print(" "); + out.print(attrs.getQName(i)); + out.print("="); + out.print(DOUBLE_QUOTE); + out.print(escape(attrs.getValue(i).replace("\"", """))); + out.print(DOUBLE_QUOTE); + } + } + + attrs = n.getAttributes(); + if (attrs != null) { + Node.JspAttribute[] jspAttrs = n.getJspAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + out.print(" "); + out.print(attrs.getQName(i)); + out.print("="); + if (jspAttrs[i].isELInterpreterInput()) { + out.print("\\\"\" + "); + String debug = attributeValue(jspAttrs[i], false, String.class); + out.print(debug); + out.print(" + \"\\\""); + } else { + out.print(DOUBLE_QUOTE); + out.print(escape(jspAttrs[i].getValue().replace("\"", """))); + out.print(DOUBLE_QUOTE); + } + } + } + + if (n.getBody() != null) { + out.println(">\");"); + + // Visit tag body + visitBody(n); + + /* + * Write end tag + */ + out.printin("out.write(\"\");"); + } else { + out.println("/>\");"); + } + + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.JspElement n) throws JasperException { + + n.setBeginJavaLine(out.getJavaLine()); + + // Compute attribute value string for XML-style and named + // attributes + Map map = new HashMap<>(); + // Validator ensures this is non-null + Node.JspAttribute[] attrs = n.getJspAttributes(); + for (int i = 0; i < attrs.length; i++) { + String value = null; + String nvp = null; + if (attrs[i].isNamedAttribute()) { + NamedAttribute attr = attrs[i].getNamedAttributeNode(); + Node.JspAttribute omitAttr = attr.getOmit(); + String omit; + if (omitAttr == null) { + omit = "false"; + } else { + // String literals returned by attributeValue will be + // quoted and escaped. + omit = attributeValue(omitAttr, false, boolean.class); + if ("\"true\"".equals(omit)) { + continue; + } + } + value = generateNamedAttributeValue( + attrs[i].getNamedAttributeNode()); + if ("\"false\"".equals(omit)) { + nvp = " + \" " + attrs[i].getName() + "=\\\"\" + " + + value + " + \"\\\"\""; + } else { + nvp = " + (java.lang.Boolean.valueOf(" + omit + ")?\"\":\" " + + attrs[i].getName() + "=\\\"\" + " + value + + " + \"\\\"\")"; + } + } else { + value = attributeValue(attrs[i], false, Object.class); + nvp = " + \" " + attrs[i].getName() + "=\\\"\" + " + + value + " + \"\\\"\""; + } + map.put(attrs[i].getName(), nvp); + } + + // Write begin tag, using XML-style 'name' attribute as the + // element name + String elemName = attributeValue(n.getNameAttribute(), false, String.class); + out.printin("out.write(\"<\""); + out.print(" + " + elemName); + + // Write remaining attributes + for (Entry attrEntry : map.entrySet()) { + out.print(attrEntry.getValue()); + } + + // Does the have nested tags other than + // + boolean hasBody = false; + Node.Nodes subelements = n.getBody(); + if (subelements != null) { + for (int i = 0; i < subelements.size(); i++) { + Node subelem = subelements.getNode(i); + if (!(subelem instanceof Node.NamedAttribute)) { + hasBody = true; + break; + } + } + } + if (hasBody) { + out.println(" + \">\");"); + + // Smap should not include the body + n.setEndJavaLine(out.getJavaLine()); + + // Visit tag body + visitBody(n); + + // Write end tag + out.printin("out.write(\"\");"); + } else { + out.println(" + \"/>\");"); + n.setEndJavaLine(out.getJavaLine()); + } + } + + @Override + public void visit(Node.TemplateText n) throws JasperException { + + String text = n.getText(); + // If the extended option is being used attempt to minimize the + // frequency of regex operations. + if (ctxt.getOptions().getTrimSpaces().equals(TrimSpacesOption.EXTENDED) && text.contains("\n")) { + // Ensure there are no

     or 
    tags embedded in this + // text - if there are, we want to NOT modify the whitespace. + Matcher preMatcher = PRE_TAG_PATTERN.matcher(text); + if (!preMatcher.matches()) { + Matcher matcher = BLANK_LINE_PATTERN.matcher(text); + String revisedText = matcher.replaceAll("\n"); + // Leading and trailing whitespace can be trimmed so remove + // it here as the regex won't remove it. + text = revisedText.trim(); + } + } + + int textSize = text.length(); + if (textSize == 0) { + return; + } + + if (textSize <= 3) { + // Special case small text strings + n.setBeginJavaLine(out.getJavaLine()); + int lineInc = 0; + for (int i = 0; i < textSize; i++) { + char ch = text.charAt(i); + out.printil("out.write(" + quote(ch) + ");"); + if (i > 0) { + n.addSmap(lineInc); + } + if (ch == '\n') { + lineInc++; + } + } + n.setEndJavaLine(out.getJavaLine()); + return; + } + + if (ctxt.getOptions().genStringAsCharArray()) { + // Generate Strings as char arrays, for performance + ServletWriter caOut; + if (charArrayBuffer == null) { + charArrayBuffer = new GenBuffer(); + caOut = charArrayBuffer.getOut(); + caOut.pushIndent(); + textMap = new HashMap<>(); + } else { + caOut = charArrayBuffer.getOut(); + } + // UTF-8 is up to 4 bytes per character + // String constants are limited to 64k bytes + // Limit string constants here to 16k characters + int textIndex = 0; + int textLength = text.length(); + while (textIndex < textLength) { + int len = 0; + if (textLength - textIndex > 16384) { + len = 16384; + } else { + len = textLength - textIndex; + } + String output = text.substring(textIndex, textIndex + len); + String charArrayName = textMap.get(output); + if (charArrayName == null) { + charArrayName = "_jspx_char_array_" + charArrayCount++; + textMap.put(output, charArrayName); + caOut.printin("static char[] "); + caOut.print(charArrayName); + caOut.print(" = "); + caOut.print(quote(output)); + caOut.println(".toCharArray();"); + } + + n.setBeginJavaLine(out.getJavaLine()); + out.printil("out.write(" + charArrayName + ");"); + n.setEndJavaLine(out.getJavaLine()); + + textIndex = textIndex + len; + } + return; + } + + n.setBeginJavaLine(out.getJavaLine()); + + out.printin(); + StringBuilder sb = new StringBuilder("out.write(\""); + int initLength = sb.length(); + int count = JspUtil.CHUNKSIZE; + int srcLine = 0; // relative to starting source line + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + --count; + switch (ch) { + case '"': + sb.append('\\').append('\"'); + break; + case '\\': + sb.append('\\').append('\\'); + break; + case '\r': + sb.append('\\').append('r'); + break; + case '\n': + sb.append('\\').append('n'); + srcLine++; + + if (breakAtLF || count < 0) { + // Generate an out.write() when see a '\n' in template + sb.append("\");"); + out.println(sb.toString()); + if (i < text.length() - 1) { + out.printin(); + } + sb.setLength(initLength); + count = JspUtil.CHUNKSIZE; + } + // add a Smap for this line + n.addSmap(srcLine); + break; + default: + sb.append(ch); + } + } + + if (sb.length() > initLength) { + sb.append("\");"); + out.println(sb.toString()); + } + + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.JspBody n) throws JasperException { + if (n.getBody() != null) { + if (isSimpleTagHandler) { + out.printin(simpleTagHandlerVar); + out.print(".setJspBody("); + generateJspFragment(n, simpleTagHandlerVar); + out.println(");"); + } else { + visitBody(n); + } + } + } + + @Override + public void visit(Node.InvokeAction n) throws JasperException { + + n.setBeginJavaLine(out.getJavaLine()); + + // Copy virtual page scope of tag file to page scope of invoking + // page + out.printil("((org.apache.jasper.runtime.JspContextWrapper) this.jspContext).syncBeforeInvoke();"); + String varReaderAttr = n.getTextAttribute("varReader"); + String varAttr = n.getTextAttribute("var"); + if (varReaderAttr != null || varAttr != null) { + out.printil("_jspx_sout = new java.io.StringWriter();"); + } else { + out.printil("_jspx_sout = null;"); + } + + // Invoke fragment, unless fragment is null + out.printin("if ("); + out.print(toGetterMethod(n.getTextAttribute("fragment"))); + out.println(" != null) {"); + out.pushIndent(); + out.printin(toGetterMethod(n.getTextAttribute("fragment"))); + out.println(".invoke(_jspx_sout);"); + out.popIndent(); + out.printil("}"); + + // Store varReader in appropriate scope + if (varReaderAttr != null || varAttr != null) { + String scopeName = n.getTextAttribute("scope"); + out.printin("_jspx_page_context.setAttribute("); + if (varReaderAttr != null) { + out.print(quote(varReaderAttr)); + out.print(", new java.io.StringReader(_jspx_sout.toString())"); + } else { + out.print(quote(varAttr)); + out.print(", _jspx_sout.toString()"); + } + if (scopeName != null) { + out.print(", "); + out.print(getScopeConstant(scopeName)); + } + out.println(");"); + } + + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.DoBodyAction n) throws JasperException { + + n.setBeginJavaLine(out.getJavaLine()); + + // Copy virtual page scope of tag file to page scope of invoking + // page + out.printil("((org.apache.jasper.runtime.JspContextWrapper) this.jspContext).syncBeforeInvoke();"); + + // Invoke body + String varReaderAttr = n.getTextAttribute("varReader"); + String varAttr = n.getTextAttribute("var"); + if (varReaderAttr != null || varAttr != null) { + out.printil("_jspx_sout = new java.io.StringWriter();"); + } else { + out.printil("_jspx_sout = null;"); + } + out.printil("if (getJspBody() != null)"); + out.pushIndent(); + out.printil("getJspBody().invoke(_jspx_sout);"); + out.popIndent(); + + // Store varReader in appropriate scope + if (varReaderAttr != null || varAttr != null) { + String scopeName = n.getTextAttribute("scope"); + out.printin("_jspx_page_context.setAttribute("); + if (varReaderAttr != null) { + out.print(quote(varReaderAttr)); + out.print(", new java.io.StringReader(_jspx_sout.toString())"); + } else { + out.print(quote(varAttr)); + out.print(", _jspx_sout.toString()"); + } + if (scopeName != null) { + out.print(", "); + out.print(getScopeConstant(scopeName)); + } + out.println(");"); + } + + // Restore EL context + out.printil("jspContext.getELContext().putContext(jakarta.servlet.jsp.JspContext.class,getJspContext());"); + + n.setEndJavaLine(out.getJavaLine()); + } + + @Override + public void visit(Node.AttributeGenerator n) throws JasperException { + Node.CustomTag tag = n.getTag(); + Node.JspAttribute[] attrs = tag.getJspAttributes(); + // The TagPluginManager only creates AttributeGenerator nodes for + // attributes that are present. + for (int i = 0; i < attrs.length; i++) { + if (attrs[i].getName().equals(n.getName())) { + out.print(evaluateAttribute(getTagHandlerInfo(tag), + attrs[i], tag, null)); + break; + } + } + } + + private TagHandlerInfo getTagHandlerInfo(Node.CustomTag n) + throws JasperException { + Map handlerInfosByShortName = handlerInfos. + computeIfAbsent(n.getPrefix(), k -> new HashMap<>()); + TagHandlerInfo handlerInfo = + handlerInfosByShortName.get(n.getLocalName()); + if (handlerInfo == null) { + handlerInfo = new TagHandlerInfo(n, n.getTagHandlerClass(), err); + handlerInfosByShortName.put(n.getLocalName(), handlerInfo); + } + return handlerInfo; + } + + private void generateTagPlugin(Node.CustomTag n) throws JasperException { + n.getAtSTag().visit(this); + visitBody(n); + n.getAtETag().visit(this); + } + + private void generateCustomStart(Node.CustomTag n, + TagHandlerInfo handlerInfo, String tagHandlerVar, + String tagEvalVar, String tagPushBodyCountVar) + throws JasperException { + + Class tagHandlerClass = + handlerInfo.getTagHandlerClass(); + + out.printin("// "); + out.println(n.getQName()); + n.setBeginJavaLine(out.getJavaLine()); + + // Declare AT_BEGIN scripting variables + declareScriptingVars(n, VariableInfo.AT_BEGIN); + saveScriptingVars(n, VariableInfo.AT_BEGIN); + + String tagHandlerClassName = tagHandlerClass.getCanonicalName(); + if (isPoolingEnabled && !(n.implementsJspIdConsumer())) { + out.printin(tagHandlerClassName); + out.print(" "); + out.print(tagHandlerVar); + out.print(" = "); + out.print("("); + out.print(tagHandlerClassName); + out.print(") "); + out.print(n.getTagHandlerPoolName()); + out.print(".get("); + out.print(tagHandlerClassName); + out.println(".class);"); + out.printin("boolean "); + out.print(tagHandlerVar); + out.println("_reused = false;"); + } else { + writeNewInstance(tagHandlerVar, tagHandlerClass); + } + + // Wrap use of tag in try/finally to ensure clean-up takes place + out.printil("try {"); + out.pushIndent(); + + // includes setting the context + generateSetters(n, tagHandlerVar, handlerInfo, false); + + if (n.implementsTryCatchFinally()) { + out.printin("int[] "); + out.print(tagPushBodyCountVar); + out.println(" = new int[] { 0 };"); + out.printil("try {"); + out.pushIndent(); + } + out.printin("int "); + out.print(tagEvalVar); + out.print(" = "); + out.print(tagHandlerVar); + out.println(".doStartTag();"); + + if (!n.implementsBodyTag()) { + // Synchronize AT_BEGIN scripting variables + syncScriptingVars(n, VariableInfo.AT_BEGIN); + } + + if (!n.hasEmptyBody()) { + out.printin("if ("); + out.print(tagEvalVar); + out.println(" != jakarta.servlet.jsp.tagext.Tag.SKIP_BODY) {"); + out.pushIndent(); + + // Declare NESTED scripting variables + declareScriptingVars(n, VariableInfo.NESTED); + saveScriptingVars(n, VariableInfo.NESTED); + + if (n.implementsBodyTag()) { + out.printin("if ("); + out.print(tagEvalVar); + out.println(" != jakarta.servlet.jsp.tagext.Tag.EVAL_BODY_INCLUDE) {"); + // Assume EVAL_BODY_BUFFERED + out.pushIndent(); + if (n.implementsTryCatchFinally()) { + out.printin(tagPushBodyCountVar); + out.println("[0]++;"); + } else if (pushBodyCountVar != null) { + out.printin(pushBodyCountVar); + out.println("[0]++;"); + } + out.printin("out = org.apache.jasper.runtime.JspRuntimeLibrary.startBufferedBody("); + out.print("_jspx_page_context, "); + out.print(tagHandlerVar); + out.println(");"); + out.popIndent(); + out.printil("}"); + + // Synchronize AT_BEGIN and NESTED scripting variables + syncScriptingVars(n, VariableInfo.AT_BEGIN); + syncScriptingVars(n, VariableInfo.NESTED); + + } else { + // Synchronize NESTED scripting variables + syncScriptingVars(n, VariableInfo.NESTED); + } + + if (n.implementsIterationTag()) { + out.printil("do {"); + out.pushIndent(); + } + } + // Map the Java lines that handles start of custom tags to the + // JSP line for this tag + n.setEndJavaLine(out.getJavaLine()); + } + + private void writeNewInstance(String tagHandlerVar, Class tagHandlerClass) { + String tagHandlerClassName = tagHandlerClass.getCanonicalName(); + out.printin(tagHandlerClassName); + out.print(" "); + out.print(tagHandlerVar); + out.print(" = "); + if (useInstanceManagerForTags) { + out.print("("); + out.print(tagHandlerClassName); + out.print(")"); + out.print("_jsp_getInstanceManager().newInstance(\""); + // Need the binary name here, not the canonical name + out.print(tagHandlerClass.getName()); + out.println("\", this.getClass().getClassLoader());"); + } else { + out.print("new "); + out.print(tagHandlerClassName); + out.println("();"); + out.printin("_jsp_getInstanceManager().newInstance("); + out.print(tagHandlerVar); + out.println(");"); + } + } + + private void writeDestroyInstance(String tagHandlerVar) { + out.printin("_jsp_getInstanceManager().destroyInstance("); + out.print(tagHandlerVar); + out.println(");"); + } + + private void generateCustomEnd(Node.CustomTag n, String tagHandlerVar, + String tagEvalVar, String tagPushBodyCountVar) { + + if (!n.hasEmptyBody()) { + if (n.implementsIterationTag()) { + out.printin("int evalDoAfterBody = "); + out.print(tagHandlerVar); + out.println(".doAfterBody();"); + + // Synchronize AT_BEGIN and NESTED scripting variables + syncScriptingVars(n, VariableInfo.AT_BEGIN); + syncScriptingVars(n, VariableInfo.NESTED); + + out.printil("if (evalDoAfterBody != jakarta.servlet.jsp.tagext.BodyTag.EVAL_BODY_AGAIN)"); + out.pushIndent(); + out.printil("break;"); + out.popIndent(); + + out.popIndent(); + out.printil("} while (true);"); + } + + restoreScriptingVars(n, VariableInfo.NESTED); + + if (n.implementsBodyTag()) { + out.printin("if ("); + out.print(tagEvalVar); + out.println(" != jakarta.servlet.jsp.tagext.Tag.EVAL_BODY_INCLUDE) {"); + out.pushIndent(); + out.printil("out = _jspx_page_context.popBody();"); + if (n.implementsTryCatchFinally()) { + out.printin(tagPushBodyCountVar); + out.println("[0]--;"); + } else if (pushBodyCountVar != null) { + out.printin(pushBodyCountVar); + out.println("[0]--;"); + } + out.popIndent(); + out.printil("}"); + } + + out.popIndent(); // EVAL_BODY + out.printil("}"); + } + + out.printin("if ("); + out.print(tagHandlerVar); + out.println(".doEndTag() == jakarta.servlet.jsp.tagext.Tag.SKIP_PAGE) {"); + out.pushIndent(); + if (isTagFile || isFragment) { + out.printil("throw new jakarta.servlet.jsp.SkipPageException();"); + } else { + out.printil((methodNesting > 0) ? "return true;" : "return;"); + } + out.popIndent(); + out.printil("}"); + // Synchronize AT_BEGIN scripting variables + syncScriptingVars(n, VariableInfo.AT_BEGIN); + + // TryCatchFinally + if (n.implementsTryCatchFinally()) { + out.popIndent(); // try + out.printil("} catch (java.lang.Throwable _jspx_exception) {"); + out.pushIndent(); + + out.printin("while ("); + out.print(tagPushBodyCountVar); + out.println("[0]-- > 0)"); + out.pushIndent(); + out.printil("out = _jspx_page_context.popBody();"); + out.popIndent(); + + out.printin(tagHandlerVar); + out.println(".doCatch(_jspx_exception);"); + out.popIndent(); + out.printil("} finally {"); + out.pushIndent(); + out.printin(tagHandlerVar); + out.println(".doFinally();"); + } + + if (n.implementsTryCatchFinally()) { + out.popIndent(); + out.printil("}"); + } + + // Print tag reuse + if (isPoolingEnabled && !(n.implementsJspIdConsumer())) { + out.printin(n.getTagHandlerPoolName()); + out.print(".reuse("); + out.print(tagHandlerVar); + out.println(");"); + out.printin(tagHandlerVar); + out.println("_reused = true;"); + } + + // Ensure clean-up takes place + // Use JspRuntimeLibrary to minimise code in _jspService() + out.popIndent(); + out.printil("} finally {"); + out.pushIndent(); + out.printin("org.apache.jasper.runtime.JspRuntimeLibrary.releaseTag("); + out.print(tagHandlerVar); + out.print(", _jsp_getInstanceManager(), "); + if (isPoolingEnabled && !(n.implementsJspIdConsumer())) { + out.print(tagHandlerVar); + out.println("_reused);"); + } else { + out.println("false);"); + } + out.popIndent(); + out.printil("}"); + + // Declare and synchronize AT_END scripting variables (must do this + // outside the try/catch/finally block) + declareScriptingVars(n, VariableInfo.AT_END); + syncScriptingVars(n, VariableInfo.AT_END); + + restoreScriptingVars(n, VariableInfo.AT_BEGIN); + } + + private void generateCustomDoTag(Node.CustomTag n, + TagHandlerInfo handlerInfo, String tagHandlerVar) + throws JasperException { + + Class tagHandlerClass = + handlerInfo.getTagHandlerClass(); + + n.setBeginJavaLine(out.getJavaLine()); + out.printin("// "); + out.println(n.getQName()); + + // Declare AT_BEGIN scripting variables + declareScriptingVars(n, VariableInfo.AT_BEGIN); + saveScriptingVars(n, VariableInfo.AT_BEGIN); + + // Declare AT_END scripting variables + declareScriptingVars(n, VariableInfo.AT_END); + + writeNewInstance(tagHandlerVar, tagHandlerClass); + + out.printil("try {"); + out.pushIndent(); + + generateSetters(n, tagHandlerVar, handlerInfo, true); + + // Set the body + if (findJspBody(n) == null) { + /* + * Encapsulate body of custom tag invocation in JspFragment and + * pass it to tag handler's setJspBody(), unless tag body is + * empty + */ + if (!n.hasEmptyBody()) { + out.printin(tagHandlerVar); + out.print(".setJspBody("); + generateJspFragment(n, tagHandlerVar); + out.println(");"); + } + } else { + /* + * Body of tag is the body of the element. The visit + * method for that element is going to encapsulate that + * element's body in a JspFragment and pass it to the tag + * handler's setJspBody() + */ + String tmpTagHandlerVar = simpleTagHandlerVar; + simpleTagHandlerVar = tagHandlerVar; + boolean tmpIsSimpleTagHandler = isSimpleTagHandler; + isSimpleTagHandler = true; + visitBody(n); + simpleTagHandlerVar = tmpTagHandlerVar; + isSimpleTagHandler = tmpIsSimpleTagHandler; + } + + out.printin(tagHandlerVar); + out.println(".doTag();"); + + restoreScriptingVars(n, VariableInfo.AT_BEGIN); + + // Synchronize AT_BEGIN scripting variables + syncScriptingVars(n, VariableInfo.AT_BEGIN); + + // Synchronize AT_END scripting variables + syncScriptingVars(n, VariableInfo.AT_END); + + out.popIndent(); + out.printil("} finally {"); + out.pushIndent(); + + // Resource injection + writeDestroyInstance(tagHandlerVar); + + out.popIndent(); + out.printil("}"); + + n.setEndJavaLine(out.getJavaLine()); + } + + private void declareScriptingVars(Node.CustomTag n, int scope) { + if (isFragment) { + // No need to declare Java variables, if we inside a + // JspFragment, because a fragment is always scriptless. + return; + } + + // Note: ScriptingVariabler$ScriptingVariableVisitor will already + // have skipped any variables where declare is set to false. + List vec = n.getScriptingVars(scope); + if (vec != null) { + for (Object elem : vec) { + if (elem instanceof VariableInfo) { + VariableInfo varInfo = (VariableInfo) elem; + out.printin(varInfo.getClassName()); + out.print(" "); + out.print(varInfo.getVarName()); + out.println(" = null;"); + } else { + TagVariableInfo tagVarInfo = (TagVariableInfo) elem; + String varName = tagVarInfo.getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfo.getNameFromAttribute()); + } else if (tagVarInfo.getNameFromAttribute() != null) { + // alias + continue; + } + out.printin(tagVarInfo.getClassName()); + out.print(" "); + out.print(varName); + out.println(" = null;"); + } + } + } + } + + /* + * This method is called as part of the custom tag's start element. + * + * If the given custom tag has a custom nesting level greater than 0, + * save the current values of its scripting variables to temporary + * variables, so those values may be restored in the tag's end element. + * This way, the scripting variables may be synchronized by the given + * tag without affecting their original values. + */ + private void saveScriptingVars(Node.CustomTag n, int scope) { + if (n.getCustomNestingLevel() == 0) { + return; + } + if (isFragment) { + // No need to declare Java variables, if we inside a + // JspFragment, because a fragment is always scriptless. + // Thus, there is no need to save/ restore/ sync them. + // Note, that JspContextWrapper.syncFoo() methods will take + // care of saving/ restoring/ sync'ing of JspContext attributes. + return; + } + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + if ((varInfos.length == 0) && (tagVarInfos.length == 0)) { + return; + } + + List declaredVariables = n.getScriptingVars(scope); + + if (varInfos.length > 0) { + for (VariableInfo varInfo : varInfos) { + if (varInfo.getScope() != scope) { + continue; + } + // If the scripting variable has been declared, skip codes + // for saving and restoring it. + if (declaredVariables.contains(varInfo)) { + continue; + } + String varName = varInfo.getVarName(); + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + out.printin(tmpVarName); + out.print(" = "); + out.print(varName); + out.println(";"); + } + } else { + for (TagVariableInfo tagVarInfo : tagVarInfos) { + if (tagVarInfo.getScope() != scope) { + continue; + } + // If the scripting variable has been declared, skip codes + // for saving and restoring it. + if (declaredVariables.contains(tagVarInfo)) { + continue; + } + String varName = tagVarInfo.getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfo.getNameFromAttribute()); + } + // Alias is not possible here. + // Alias can only be configured for tag files. As SimpleTag + // implementations, isFragment will always be true above + // hence execution never reaches this point. + + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + out.printin(tmpVarName); + out.print(" = "); + out.print(varName); + out.println(";"); + } + } + } + + /* + * This method is called as part of the custom tag's end element. + * + * If the given custom tag has a custom nesting level greater than 0, + * restore its scripting variables to their original values that were + * saved in the tag's start element. + */ + private void restoreScriptingVars(Node.CustomTag n, int scope) { + if (n.getCustomNestingLevel() == 0) { + return; + } + if (isFragment) { + // No need to declare Java variables, if we inside a + // JspFragment, because a fragment is always scriptless. + // Thus, there is no need to save/ restore/ sync them. + // Note, that JspContextWrapper.syncFoo() methods will take + // care of saving/ restoring/ sync'ing of JspContext attributes. + return; + } + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + if ((varInfos.length == 0) && (tagVarInfos.length == 0)) { + return; + } + + List declaredVariables = n.getScriptingVars(scope); + + if (varInfos.length > 0) { + for (VariableInfo varInfo : varInfos) { + if (varInfo.getScope() != scope) { + continue; + } + // If the scripting variable has been declared, skip codes + // for saving and restoring it. + if (declaredVariables.contains(varInfo)) { + continue; + } + String varName = varInfo.getVarName(); + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + out.printin(varName); + out.print(" = "); + out.print(tmpVarName); + out.println(";"); + } + } else { + for (TagVariableInfo tagVarInfo : tagVarInfos) { + if (tagVarInfo.getScope() != scope) { + continue; + } + // If the scripting variable has been declared, skip codes + // for saving and restoring it. + if (declaredVariables.contains(tagVarInfo)) { + continue; + } + String varName = tagVarInfo.getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfo.getNameFromAttribute()); + } + // Alias is not possible here. + // Alias can only be configured for tag files. As SimpleTag + // implementations, isFragment will always be true above + // hence execution never reaches this point. + + String tmpVarName = "_jspx_" + varName + "_" + + n.getCustomNestingLevel(); + out.printin(varName); + out.print(" = "); + out.print(tmpVarName); + out.println(";"); + } + } + } + + /* + * Synchronizes the scripting variables of the given custom tag for the + * given scope. + */ + private void syncScriptingVars(Node.CustomTag n, int scope) { + if (isFragment) { + // No need to declare Java variables, if we inside a + // JspFragment, because a fragment is always scriptless. + // Thus, there is no need to save/ restore/ sync them. + // Note, that JspContextWrapper.syncFoo() methods will take + // care of saving/ restoring/ sync'ing of JspContext attributes. + return; + } + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + + if ((varInfos.length == 0) && (tagVarInfos.length == 0)) { + return; + } + + if (varInfos.length > 0) { + for (VariableInfo varInfo : varInfos) { + if (varInfo.getScope() == scope) { + out.printin(varInfo.getVarName()); + out.print(" = ("); + out.print(varInfo.getClassName()); + out.print(") _jspx_page_context.findAttribute("); + out.print(quote(varInfo.getVarName())); + out.println(");"); + } + } + } else { + for (TagVariableInfo tagVarInfo : tagVarInfos) { + if (tagVarInfo.getScope() == scope) { + String name = tagVarInfo.getNameGiven(); + if (name == null) { + name = n.getTagData().getAttributeString( + tagVarInfo.getNameFromAttribute()); + } else if (tagVarInfo.getNameFromAttribute() != null) { + // alias + continue; + } + out.printin(name); + out.print(" = ("); + out.print(tagVarInfo.getClassName()); + out.print(") _jspx_page_context.findAttribute("); + out.print(quote(name)); + out.println(");"); + } + } + } + } + + private String getJspContextVar() { + if (this.isTagFile) { + return "this.getJspContext()"; + } + return "_jspx_page_context"; + } + + /* + * Creates a tag variable name by concatenating the given prefix and + * shortName and encoded to make the resultant string a valid Java + * Identifier. + */ + private String createTagVarName(String fullName, String prefix, + String shortName) { + + String varName; + synchronized (tagVarNumbers) { + varName = prefix + "_" + shortName + "_"; + if (tagVarNumbers.get(fullName) != null) { + Integer i = tagVarNumbers.get(fullName); + varName = varName + i.intValue(); + tagVarNumbers.put(fullName, + Integer.valueOf(i.intValue() + 1)); + } else { + tagVarNumbers.put(fullName, Integer.valueOf(1)); + varName = varName + "0"; + } + } + return JspUtil.makeJavaIdentifier(varName); + } + + @SuppressWarnings("null") + private String evaluateAttribute(TagHandlerInfo handlerInfo, + Node.JspAttribute attr, Node.CustomTag n, String tagHandlerVar) + throws JasperException { + + String attrValue = attr.getValue(); + if (attrValue == null) { + // Must be a named attribute + if (n.checkIfAttributeIsJspFragment(attr.getName())) { + // XXX - no need to generate temporary variable here + attrValue = generateNamedAttributeJspFragment(attr + .getNamedAttributeNode(), tagHandlerVar); + } else { + attrValue = generateNamedAttributeValue(attr + .getNamedAttributeNode()); + } + } + + String localName = attr.getLocalName(); + + Method m = null; + Class[] c = null; + if (attr.isDynamic()) { + c = OBJECT_CLASS; + } else { + m = handlerInfo.getSetterMethod(localName); + if (m == null) { + err.jspError(n, "jsp.error.unable.to_find_method", attr + .getName()); + } + c = m.getParameterTypes(); + // XXX assert(c.length > 0) + } + + if (attr.isExpression()) { + // Do nothing + } else if (attr.isNamedAttribute()) { + if (!n.checkIfAttributeIsJspFragment(attr.getName()) + && !attr.isDynamic()) { + attrValue = stringInterpreter.convertString(c[0], attrValue, localName, + handlerInfo.getPropertyEditorClass(localName), true); + } + } else if (attr.isELInterpreterInput()) { + + // results buffer + StringBuilder sb = new StringBuilder(64); + + TagAttributeInfo tai = attr.getTagAttributeInfo(); + + // generate elContext reference + sb.append(getJspContextVar()); + sb.append(".getELContext()"); + String elContext = sb.toString(); + if (attr.getEL() != null && attr.getEL().getMapName() != null) { + sb.setLength(0); + sb.append("new org.apache.jasper.el.ELContextWrapper("); + sb.append(elContext); + sb.append(','); + sb.append(attr.getEL().getMapName()); + sb.append(')'); + elContext = sb.toString(); + } + + // reset buffer + sb.setLength(0); + + // create our mark + sb.append(n.getStart().toString()); + sb.append(" '"); + sb.append(attrValue); + sb.append('\''); + String mark = sb.toString(); + + // reset buffer + sb.setLength(0); + + // depending on type + if (attr.isDeferredInput() + || ((tai != null) && ValueExpression.class.getName().equals(tai.getTypeName()))) { + sb.append("new org.apache.jasper.el.JspValueExpression("); + sb.append(quote(mark)); + sb.append(",_jsp_getExpressionFactory().createValueExpression("); + if (attr.getEL() != null) { // optimize + sb.append(elContext); + sb.append(','); + } + sb.append(quote(attrValue)); + sb.append(','); + sb.append(JspUtil.toJavaSourceTypeFromTld(attr.getExpectedTypeName())); + sb.append("))"); + // should the expression be evaluated before passing to + // the setter? + boolean evaluate = false; + if (tai.canBeRequestTime()) { + evaluate = true; // JSP.2.3.2 + } + if (attr.isDeferredInput()) { + evaluate = false; // JSP.2.3.3 + } + if (attr.isDeferredInput() && tai.canBeRequestTime()) { + evaluate = !attrValue.contains("#{"); // JSP.2.3.5 + } + if (evaluate) { + sb.append(".getValue("); + sb.append(getJspContextVar()); + sb.append(".getELContext()"); + sb.append(')'); + } + attrValue = sb.toString(); + } else if (attr.isDeferredMethodInput() + || ((tai != null) && MethodExpression.class.getName().equals(tai.getTypeName()))) { + sb.append("new org.apache.jasper.el.JspMethodExpression("); + sb.append(quote(mark)); + sb.append(",_jsp_getExpressionFactory().createMethodExpression("); + sb.append(elContext); + sb.append(','); + sb.append(quote(attrValue)); + sb.append(','); + sb.append(JspUtil.toJavaSourceTypeFromTld(attr.getExpectedTypeName())); + sb.append(','); + sb.append("new java.lang.Class[] {"); + + String[] p = attr.getParameterTypeNames(); + for (String s : p) { + sb.append(JspUtil.toJavaSourceTypeFromTld(s)); + sb.append(','); + } + if (p.length > 0) { + sb.setLength(sb.length() - 1); + } + + sb.append("}))"); + attrValue = sb.toString(); + } else { + // Must be EL + // run attrValue through the expression interpreter + String mapName = attr.getEL().getMapName(); + attrValue = elInterpreter.interpreterCall(ctxt, + this.isTagFile, attrValue, c[0], mapName); + } + } else { + attrValue = stringInterpreter.convertString(c[0], attrValue, localName, + handlerInfo.getPropertyEditorClass(localName), false); + } + return attrValue; + } + + /** + * Generate code to create a map for the alias variables + * + * @return the name of the map + */ + private String generateAliasMap(Node.CustomTag n, + String tagHandlerVar) { + + TagVariableInfo[] tagVars = n.getTagVariableInfos(); + String aliasMapVar = null; + + boolean aliasSeen = false; + for (TagVariableInfo tagVar : tagVars) { + + String nameFrom = tagVar.getNameFromAttribute(); + if (nameFrom != null) { + String aliasedName = n.getAttributeValue(nameFrom); + if (!aliasSeen) { + out.printin("java.util.HashMap "); + aliasMapVar = tagHandlerVar + "_aliasMap"; + out.print(aliasMapVar); + out.println(" = new java.util.HashMap();"); + aliasSeen = true; + } + out.printin(aliasMapVar); + out.print(".put("); + out.print(quote(tagVar.getNameGiven())); + out.print(", "); + out.print(quote(aliasedName)); + out.println(");"); + } + } + return aliasMapVar; + } + + private void generateSetters(Node.CustomTag n, String tagHandlerVar, + TagHandlerInfo handlerInfo, boolean simpleTag) + throws JasperException { + + // Set context + if (simpleTag) { + // Generate alias map + String aliasMapVar = null; + if (n.isTagFile()) { + aliasMapVar = generateAliasMap(n, tagHandlerVar); + } + out.printin(tagHandlerVar); + if (aliasMapVar == null) { + out.println(".setJspContext(_jspx_page_context);"); + } else { + out.print(".setJspContext(_jspx_page_context, "); + out.print(aliasMapVar); + out.println(");"); + } + } else { + out.printin(tagHandlerVar); + out.println(".setPageContext(_jspx_page_context);"); + } + + // Set parent + if (isTagFile && parent == null) { + out.printin(tagHandlerVar); + out.print(".setParent("); + out.print("new jakarta.servlet.jsp.tagext.TagAdapter("); + out.println("(jakarta.servlet.jsp.tagext.SimpleTag) this ));"); + } else if (!simpleTag) { + out.printin(tagHandlerVar); + out.print(".setParent("); + if (parent != null) { + if (isSimpleTagParent) { + out.print("new jakarta.servlet.jsp.tagext.TagAdapter("); + out.print("(jakarta.servlet.jsp.tagext.SimpleTag) "); + out.print(parent); + out.println("));"); + } else { + out.print("(jakarta.servlet.jsp.tagext.Tag) "); + out.print(parent); + out.println(");"); + } + } else { + out.println("null);"); + } + } else { + // The setParent() method need not be called if the value being + // passed is null, since SimpleTag instances are not reused + if (parent != null) { + out.printin(tagHandlerVar); + out.print(".setParent("); + out.print(parent); + out.println(");"); + } + } + + // need to handle deferred values and methods + Node.JspAttribute[] attrs = n.getJspAttributes(); + for (int i = 0; attrs != null && i < attrs.length; i++) { + String attrValue = evaluateAttribute(handlerInfo, attrs[i], n, + tagHandlerVar); + + Mark m = n.getStart(); + out.printil("// "+m.getFile()+"("+m.getLineNumber()+","+m.getColumnNumber()+") "+ attrs[i].getTagAttributeInfo()); + if (attrs[i].isDynamic()) { + out.printin(tagHandlerVar); + out.print("."); + out.print("setDynamicAttribute("); + String uri = attrs[i].getURI(); + if ("".equals(uri) || (uri == null)) { + out.print("null"); + } else { + out.print("\"" + attrs[i].getURI() + "\""); + } + out.print(", \""); + out.print(attrs[i].getLocalName()); + out.print("\", "); + out.print(attrValue); + out.println(");"); + } else { + out.printin(tagHandlerVar); + out.print("."); + out.print(handlerInfo.getSetterMethod( + attrs[i].getLocalName()).getName()); + out.print("("); + out.print(attrValue); + out.println(");"); + } + } + + // JspIdConsumer (after context has been set) + if (n.implementsJspIdConsumer()) { + out.printin(tagHandlerVar); + out.print(".setJspId(\""); + out.print(createJspId()); + out.println("\");"); + } + } + + /* + * Converts the scope string representation, whose possible values are + * "page", "request", "session", and "application", to the corresponding + * scope constant. + */ + private String getScopeConstant(String scope) { + String scopeName = "jakarta.servlet.jsp.PageContext.PAGE_SCOPE"; // Default to page + + if ("request".equals(scope)) { + scopeName = "jakarta.servlet.jsp.PageContext.REQUEST_SCOPE"; + } else if ("session".equals(scope)) { + scopeName = "jakarta.servlet.jsp.PageContext.SESSION_SCOPE"; + } else if ("application".equals(scope)) { + scopeName = "jakarta.servlet.jsp.PageContext.APPLICATION_SCOPE"; + } + + return scopeName; + } + + /** + * Generates anonymous JspFragment inner class which is passed as an + * argument to SimpleTag.setJspBody(). + */ + private void generateJspFragment(ChildInfoBase n, String tagHandlerVar) throws JasperException { + // XXX - A possible optimization here would be to check to see + // if the only child of the parent node is TemplateText. If so, + // we know there won't be any parameters, etc, so we can + // generate a low-overhead JspFragment that just echoes its + // body. The implementation of this fragment can come from + // the org.apache.jasper.runtime package as a support class. + FragmentHelperClass.Fragment fragment = fragmentHelperClass + .openFragment(n, methodNesting); + ServletWriter outSave = out; + out = fragment.getGenBuffer().getOut(); + String tmpParent = parent; + parent = "_jspx_parent"; + boolean isSimpleTagParentSave = isSimpleTagParent; + isSimpleTagParent = true; + boolean tmpIsFragment = isFragment; + isFragment = true; + String pushBodyCountVarSave = pushBodyCountVar; + if (pushBodyCountVar != null) { + // Use a fixed name for push body count, to simplify code gen + pushBodyCountVar = "_jspx_push_body_count"; + } + visitBody(n); + out = outSave; + parent = tmpParent; + isSimpleTagParent = isSimpleTagParentSave; + isFragment = tmpIsFragment; + pushBodyCountVar = pushBodyCountVarSave; + fragmentHelperClass.closeFragment(fragment, methodNesting); + // XXX - Need to change pageContext to jspContext if + // we're not in a place where pageContext is defined (e.g. + // in a fragment or in a tag file. + out.print("new " + fragmentHelperClass.getClassName() + "( " + + fragment.getId() + ", _jspx_page_context, " + + tagHandlerVar + ", " + pushBodyCountVar + ")"); + } + + /** + * Generate the code required to obtain the runtime value of the given + * named attribute. + * + * @param n The named attribute node whose value is required + * + * @return The name of the temporary variable the result is stored in. + * + * @throws JasperException If an error + */ + public String generateNamedAttributeValue(Node.NamedAttribute n) + throws JasperException { + + String varName = n.getTemporaryVariableName(); + + // If the only body element for this named attribute node is + // template text, we need not generate an extra call to + // pushBody and popBody. Maybe we can further optimize + // here by getting rid of the temporary variable, but in + // reality it looks like javac does this for us. + Node.Nodes body = n.getBody(); + if (body != null) { + boolean templateTextOptimization = false; + if (body.size() == 1) { + Node bodyElement = body.getNode(0); + if (bodyElement instanceof Node.TemplateText) { + templateTextOptimization = true; + out.printil("java.lang.String " + + varName + + " = " + + quote(bodyElement.getText()) + ";"); + } + } + + // XXX - Another possible optimization would be for + // lone EL expressions (no need to pushBody here either). + + if (!templateTextOptimization) { + out.printil("out = _jspx_page_context.pushBody();"); + visitBody(n); + out.printil("java.lang.String " + varName + " = " + + "((jakarta.servlet.jsp.tagext.BodyContent)" + + "out).getString();"); + out.printil("out = _jspx_page_context.popBody();"); + } + } else { + // Empty body must be treated as "" + out.printil("java.lang.String " + varName + " = \"\";"); + } + + return varName; + } + + /** + * Similar to generateNamedAttributeValue, but create a JspFragment + * instead. + * + * @param n + * The parent node of the named attribute + * @param tagHandlerVar + * The variable the tag handler is stored in, so the fragment + * knows its parent tag. + * @return The name of the temporary variable the fragment is stored in. + * + * @throws JasperException If an error occurs trying to generate the + * fragment + */ + public String generateNamedAttributeJspFragment(Node.NamedAttribute n, + String tagHandlerVar) throws JasperException { + String varName = n.getTemporaryVariableName(); + + out.printin("jakarta.servlet.jsp.tagext.JspFragment " + varName + + " = "); + generateJspFragment(n, tagHandlerVar); + out.println(";"); + + return varName; + } + } + + private static void generateLocalVariables(ServletWriter out, ChildInfoBase n) { + Node.ChildInfo ci = n.getChildInfo(); + + if (ci.hasUseBean()) { + out.printil("jakarta.servlet.http.HttpSession session = _jspx_page_context.getSession();"); + out.printil("jakarta.servlet.ServletContext application = _jspx_page_context.getServletContext();"); + } + if (ci.hasUseBean() || ci.hasIncludeAction() || ci.hasSetProperty() + || ci.hasParamAction()) { + out.printil("jakarta.servlet.http.HttpServletRequest request = (jakarta.servlet.http.HttpServletRequest)_jspx_page_context.getRequest();"); + } + if (ci.hasIncludeAction()) { + out.printil("jakarta.servlet.http.HttpServletResponse response = (jakarta.servlet.http.HttpServletResponse)_jspx_page_context.getResponse();"); + } + } + + /** + * Common part of postamble, shared by both servlets and tag files. + */ + private void genCommonPostamble() { + // Append any methods that were generated in the buffer. + for (GenBuffer methodBuffer : methodsBuffered) { + methodBuffer.adjustJavaLines(out.getJavaLine() - 1); + out.printMultiLn(methodBuffer.toString()); + } + + // Append the helper class + if (fragmentHelperClass.isUsed()) { + fragmentHelperClass.generatePostamble(); + fragmentHelperClass.adjustJavaLines(out.getJavaLine() - 1); + out.printMultiLn(fragmentHelperClass.toString()); + } + + // Append char array declarations + if (charArrayBuffer != null) { + out.printMultiLn(charArrayBuffer.toString()); + } + + // Close the class definition + out.popIndent(); + out.printil("}"); + } + + /** + * Generates the ending part of the static portion of the servlet. + */ + private void generatePostamble() { + out.popIndent(); + out.printil("} catch (java.lang.Throwable t) {"); + out.pushIndent(); + out.printil("if (!(t instanceof jakarta.servlet.jsp.SkipPageException)){"); + out.pushIndent(); + out.printil("out = _jspx_out;"); + out.printil("if (out != null && out.getBufferSize() != 0)"); + out.pushIndent(); + out.printil("try {"); + out.pushIndent(); + out.printil("if (response.isCommitted()) {"); + out.pushIndent(); + out.printil("out.flush();"); + out.popIndent(); + out.printil("} else {"); + out.pushIndent(); + out.printil("out.clearBuffer();"); + out.popIndent(); + out.printil("}"); + out.popIndent(); + out.printil("} catch (java.io.IOException e) {}"); + out.popIndent(); + out.printil("if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);"); + out.printil("else throw new ServletException(t);"); + out.popIndent(); + out.printil("}"); + out.popIndent(); + out.printil("} finally {"); + out.pushIndent(); + out.printil("_jspxFactory.releasePageContext(_jspx_page_context);"); + out.popIndent(); + out.printil("}"); + + // Close the service method + out.popIndent(); + out.printil("}"); + + // Generated methods, helper classes, etc. + genCommonPostamble(); + } + + /** + * Constructor. + */ + Generator(ServletWriter out, Compiler compiler) throws JasperException { + this.out = out; + methodsBuffered = new ArrayList<>(); + charArrayBuffer = null; + err = compiler.getErrorDispatcher(); + ctxt = compiler.getCompilationContext(); + fragmentHelperClass = new FragmentHelperClass("Helper"); + pageInfo = compiler.getPageInfo(); + + ELInterpreter elInterpreter = null; + try { + elInterpreter = ELInterpreterFactory.getELInterpreter( + compiler.getCompilationContext().getServletContext()); + } catch (Exception e) { + err.jspError("jsp.error.el_interpreter_class.instantiation", + e.getMessage()); + } + this.elInterpreter = elInterpreter; + + StringInterpreter stringInterpreter = null; + try { + stringInterpreter = StringInterpreterFactory.getStringInterpreter( + compiler.getCompilationContext().getServletContext()); + } catch (Exception e) { + err.jspError("jsp.error.string_interpreter_class.instantiation", + e.getMessage()); + } + this.stringInterpreter = stringInterpreter; + + /* + * Temporary hack. If a JSP page uses the "extends" attribute of the + * page directive, the _jspInit() method of the generated servlet class + * will not be called (it is only called for those generated servlets + * that extend HttpJspBase, the default), causing the tag handler pools + * not to be initialized and resulting in a NPE. The JSP spec needs to + * clarify whether containers can override init() and destroy(). For + * now, we just disable tag pooling for pages that use "extends". + */ + if (pageInfo.getExtends(false) == null || ctxt.getOptions().getPoolTagsWithExtends()) { + isPoolingEnabled = ctxt.getOptions().isPoolingEnabled(); + } else { + isPoolingEnabled = false; + } + beanInfo = pageInfo.getBeanRepository(); + varInfoNames = pageInfo.getVarInfoNames(); + breakAtLF = ctxt.getOptions().getMappedFile(); + if (isPoolingEnabled) { + tagHandlerPoolNames = new ArrayList<>(); + } else { + tagHandlerPoolNames = null; + } + timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + timestampFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + /** + * The main entry for Generator. + * + * @param out + * The servlet output writer + * @param compiler + * The compiler + * @param page + * The input page + * + * @throws JasperException If something goes wrong during generation + */ + public static void generate(ServletWriter out, Compiler compiler, + Node.Nodes page) throws JasperException { + + Generator gen = new Generator(out, compiler); + + if (gen.isPoolingEnabled) { + gen.compileTagHandlerPoolList(page); + } + gen.generateCommentHeader(); + if (gen.ctxt.isTagFile()) { + JasperTagInfo tagInfo = (JasperTagInfo) gen.ctxt.getTagInfo(); + gen.generateTagHandlerPreamble(tagInfo, page); + + if (gen.ctxt.isPrototypeMode()) { + return; + } + + gen.generateXmlProlog(page); + gen.fragmentHelperClass.generatePreamble(); + page.visit(gen.new GenerateVisitor(gen.ctxt.isTagFile(), out, + gen.methodsBuffered, gen.fragmentHelperClass, + gen.ctxt.getOptions().getUseInstanceManagerForTags())); + gen.generateTagHandlerPostamble(tagInfo); + } else { + gen.generatePreamble(page); + gen.generateXmlProlog(page); + gen.fragmentHelperClass.generatePreamble(); + page.visit(gen.new GenerateVisitor(gen.ctxt.isTagFile(), out, + gen.methodsBuffered, gen.fragmentHelperClass, + gen.ctxt.getOptions().getUseInstanceManagerForTags())); + gen.generatePostamble(); + } + } + + private void generateCommentHeader() { + out.println("/*"); + out.println(" * Generated by the Jasper component of Apache Tomcat"); + out.println(" * Version: " + ctxt.getServletContext().getServerInfo()); + if (ctxt.getOptions().getGeneratedJavaAddTimestamp()) { + out.println(" * Generated at: " + timestampFormat.format(new Date()) + + " UTC"); + } + out.println(" * Note: The last modified time of this file was set to"); + out.println(" * the last modified time of the source file after"); + out.println(" * generation to assist with modification tracking."); + out.println(" */"); + } + + /* + * Generates tag handler preamble. + */ + private void generateTagHandlerPreamble(JasperTagInfo tagInfo, + Node.Nodes tag) throws JasperException { + + // Generate package declaration + String className = tagInfo.getTagClassName(); + int lastIndex = className.lastIndexOf('.'); + String packageName = className.substring(0, lastIndex); + genPreamblePackage(packageName); + className = className.substring(lastIndex + 1); + + // Generate imports + genPreambleImports(); + + // Generate class declaration + out.printin("public final class "); + out.println(className); + out.printil(" extends jakarta.servlet.jsp.tagext.SimpleTagSupport"); + out.printin(" implements org.apache.jasper.runtime.JspSourceDependent,"); + out.println(); + out.printin(" org.apache.jasper.runtime.JspSourceImports"); + if (tagInfo.hasDynamicAttributes()) { + out.println(","); + out.printin(" jakarta.servlet.jsp.tagext.DynamicAttributes"); + } + out.println(","); + out.printin(" org.apache.jasper.runtime.JspSourceDirectives"); + out.println(" {"); + out.pushIndent(); + + /* + * Class body begins here + */ + generateDeclarations(tag); + + // Static initializations here + genPreambleStaticInitializers(); + + out.printil("private jakarta.servlet.jsp.JspContext jspContext;"); + + // Declare writer used for storing result of fragment/body invocation + // if 'varReader' or 'var' attribute is specified + out.printil("private java.io.Writer _jspx_sout;"); + + // Class variable declarations + genPreambleClassVariableDeclarations(); + + generateSetJspContext(tagInfo); + + // Tag-handler specific declarations + generateTagHandlerAttributes(tagInfo); + if (tagInfo.hasDynamicAttributes()) { + generateSetDynamicAttribute(); + } + + // Methods here + genPreambleMethods(); + + // Now the doTag() method + out.printil("public void doTag() throws jakarta.servlet.jsp.JspException, java.io.IOException {"); + + if (ctxt.isPrototypeMode()) { + out.printil("}"); + out.popIndent(); + out.printil("}"); + return; + } + + out.pushIndent(); + + /* + * According to the spec, 'pageContext' must not be made available as an + * implicit object in tag files. Declare _jspx_page_context, so we can + * share the code generator with JSPs. + */ + out.printil("jakarta.servlet.jsp.PageContext _jspx_page_context = (jakarta.servlet.jsp.PageContext)jspContext;"); + + // Declare implicit objects. + out.printil("jakarta.servlet.http.HttpServletRequest request = " + + "(jakarta.servlet.http.HttpServletRequest) _jspx_page_context.getRequest();"); + out.printil("jakarta.servlet.http.HttpServletResponse response = " + + "(jakarta.servlet.http.HttpServletResponse) _jspx_page_context.getResponse();"); + out.printil("jakarta.servlet.http.HttpSession session = _jspx_page_context.getSession();"); + out.printil("jakarta.servlet.ServletContext application = _jspx_page_context.getServletContext();"); + out.printil("jakarta.servlet.ServletConfig config = _jspx_page_context.getServletConfig();"); + out.printil("jakarta.servlet.jsp.JspWriter out = jspContext.getOut();"); + out.printil("_jspInit(config);"); + + // set current JspContext on ELContext + out.printil("jspContext.getELContext().putContext(jakarta.servlet.jsp.JspContext.class,jspContext);"); + + generatePageScopedVariables(tagInfo); + + declareTemporaryScriptingVars(tag); + out.println(); + + out.printil("try {"); + out.pushIndent(); + } + + private void generateTagHandlerPostamble(TagInfo tagInfo) { + out.popIndent(); + + // Have to catch Throwable because a classic tag handler + // helper method is declared to throw Throwable. + out.printil("} catch( java.lang.Throwable t ) {"); + out.pushIndent(); + out.printil("if( t instanceof jakarta.servlet.jsp.SkipPageException )"); + out.printil(" throw (jakarta.servlet.jsp.SkipPageException) t;"); + out.printil("if( t instanceof java.io.IOException )"); + out.printil(" throw (java.io.IOException) t;"); + out.printil("if( t instanceof java.lang.IllegalStateException )"); + out.printil(" throw (java.lang.IllegalStateException) t;"); + out.printil("if( t instanceof jakarta.servlet.jsp.JspException )"); + out.printil(" throw (jakarta.servlet.jsp.JspException) t;"); + out.printil("throw new jakarta.servlet.jsp.JspException(t);"); + out.popIndent(); + out.printil("} finally {"); + out.pushIndent(); + + // handle restoring VariableMapper + TagAttributeInfo[] attrInfos = tagInfo.getAttributes(); + for (int i = 0; i < attrInfos.length; i++) { + if (attrInfos[i].isDeferredMethod() || attrInfos[i].isDeferredValue()) { + out.printin("_el_variablemapper.setVariable("); + out.print(quote(attrInfos[i].getName())); + out.print(",_el_ve"); + out.print(i); + out.println(");"); + } + } + + // restore nested JspContext on ELContext + out.printil("jspContext.getELContext().putContext(jakarta.servlet.jsp.JspContext.class,super.getJspContext());"); + + out.printil("((org.apache.jasper.runtime.JspContextWrapper) jspContext).syncEndTagFile();"); + if (isPoolingEnabled && !tagHandlerPoolNames.isEmpty()) { + out.printil("_jspDestroy();"); + } + out.popIndent(); + out.printil("}"); + + // Close the doTag method + out.popIndent(); + out.printil("}"); + + // Generated methods, helper classes, etc. + genCommonPostamble(); + } + + /** + * Generates declarations for tag handler attributes, and defines the getter + * and setter methods for each. + */ + private void generateTagHandlerAttributes(TagInfo tagInfo) { + + if (tagInfo.hasDynamicAttributes()) { + out.printil("private java.util.HashMap _jspx_dynamic_attrs = new java.util.HashMap();"); + } + + // Declare attributes + TagAttributeInfo[] attrInfos = tagInfo.getAttributes(); + for (TagAttributeInfo info : attrInfos) { + out.printin("private "); + if (info.isFragment()) { + out.print("jakarta.servlet.jsp.tagext.JspFragment "); + } else { + out.print(JspUtil.toJavaSourceType(info.getTypeName())); + out.print(" "); + } + out.print(JspUtil.makeJavaIdentifierForAttribute( + info.getName())); + out.println(";"); + } + out.println(); + + // Define attribute getter and setter methods + for (TagAttributeInfo attrInfo : attrInfos) { + String javaName = + JspUtil.makeJavaIdentifierForAttribute(attrInfo.getName()); + + // getter method + out.printin("public "); + if (attrInfo.isFragment()) { + out.print("jakarta.servlet.jsp.tagext.JspFragment "); + } else { + out.print(JspUtil.toJavaSourceType(attrInfo.getTypeName())); + out.print(" "); + } + out.print(toGetterMethod(attrInfo.getName())); + out.println(" {"); + out.pushIndent(); + out.printin("return this."); + out.print(javaName); + out.println(";"); + out.popIndent(); + out.printil("}"); + out.println(); + + // setter method + out.printin("public void "); + out.print(toSetterMethodName(attrInfo.getName())); + if (attrInfo.isFragment()) { + out.print("(jakarta.servlet.jsp.tagext.JspFragment "); + } else { + out.print("("); + out.print(JspUtil.toJavaSourceType(attrInfo.getTypeName())); + out.print(" "); + } + out.print(javaName); + out.println(") {"); + out.pushIndent(); + out.printin("this."); + out.print(javaName); + out.print(" = "); + out.print(javaName); + out.println(";"); + // Tag files should also set jspContext attributes + // Only called for tag files so always set the jspContext + out.printin("jspContext.setAttribute(\""); + out.print(attrInfo.getName()); + out.print("\", "); + out.print(javaName); + out.println(");"); + out.popIndent(); + out.printil("}"); + out.println(); + } + } + + /* + * Generate setter for JspContext so we can create a wrapper and store both + * the original and the wrapper. We need the wrapper to mask the page + * context from the tag file and simulate a fresh page context. We need the + * original to do things like sync AT_BEGIN and AT_END scripting variables. + */ + private void generateSetJspContext(TagInfo tagInfo) { + + boolean nestedSeen = false; + boolean atBeginSeen = false; + boolean atEndSeen = false; + + // Determine if there are any aliases + boolean aliasSeen = false; + TagVariableInfo[] tagVars = tagInfo.getTagVariableInfos(); + for (TagVariableInfo var : tagVars) { + // If a tag file uses a named attribute, the TagFileDirectiveVisitor + // will ensure that an alias is configured. + if (var.getNameFromAttribute() != null) { + aliasSeen = true; + break; + } + } + + if (aliasSeen) { + out.printil("public void setJspContext(jakarta.servlet.jsp.JspContext ctx, java.util.Map aliasMap) {"); + } else { + out.printil("public void setJspContext(jakarta.servlet.jsp.JspContext ctx) {"); + } + out.pushIndent(); + out.printil("super.setJspContext(ctx);"); + out.printil("java.util.ArrayList _jspx_nested = null;"); + out.printil("java.util.ArrayList _jspx_at_begin = null;"); + out.printil("java.util.ArrayList _jspx_at_end = null;"); + + for (TagVariableInfo tagVar : tagVars) { + + switch (tagVar.getScope()) { + case VariableInfo.NESTED: + if (!nestedSeen) { + out.printil("_jspx_nested = new java.util.ArrayList();"); + nestedSeen = true; + } + out.printin("_jspx_nested.add("); + break; + + case VariableInfo.AT_BEGIN: + if (!atBeginSeen) { + out.printil("_jspx_at_begin = new java.util.ArrayList();"); + atBeginSeen = true; + } + out.printin("_jspx_at_begin.add("); + break; + + case VariableInfo.AT_END: + if (!atEndSeen) { + out.printil("_jspx_at_end = new java.util.ArrayList();"); + atEndSeen = true; + } + out.printin("_jspx_at_end.add("); + break; + } // switch + + out.print(quote(tagVar.getNameGiven())); + out.println(");"); + } + if (aliasSeen) { + out.printil("this.jspContext = new org.apache.jasper.runtime.JspContextWrapper(this, ctx, _jspx_nested, _jspx_at_begin, _jspx_at_end, aliasMap);"); + } else { + out.printil("this.jspContext = new org.apache.jasper.runtime.JspContextWrapper(this, ctx, _jspx_nested, _jspx_at_begin, _jspx_at_end, null);"); + } + out.popIndent(); + out.printil("}"); + out.println(); + out.printil("public jakarta.servlet.jsp.JspContext getJspContext() {"); + out.pushIndent(); + out.printil("return this.jspContext;"); + out.popIndent(); + out.printil("}"); + } + + /* + * Generates implementation of + * jakarta.servlet.jsp.tagext.DynamicAttributes.setDynamicAttribute() method, + * which saves each dynamic attribute that is passed in so that a scoped + * variable can later be created for it. + */ + public void generateSetDynamicAttribute() { + out.printil("public void setDynamicAttribute(java.lang.String uri, java.lang.String localName, java.lang.Object value) throws jakarta.servlet.jsp.JspException {"); + out.pushIndent(); + /* + * According to the spec, only dynamic attributes with no uri are to be + * present in the Map; all other dynamic attributes are ignored. + */ + out.printil("if (uri == null)"); + out.pushIndent(); + out.printil("_jspx_dynamic_attrs.put(localName, value);"); + out.popIndent(); + out.popIndent(); + out.printil("}"); + } + + /* + * Creates a page-scoped variable for each declared tag attribute. Also, if + * the tag accepts dynamic attributes, a page-scoped variable is made + * available for each dynamic attribute that was passed in. + */ + private void generatePageScopedVariables(JasperTagInfo tagInfo) { + + // "normal" attributes + TagAttributeInfo[] attrInfos = tagInfo.getAttributes(); + boolean variableMapperVar = false; + for (int i = 0; i < attrInfos.length; i++) { + String attrName = attrInfos[i].getName(); + + // handle assigning deferred vars to VariableMapper, storing + // previous values under '_el_ve[i]' for later re-assignment + if (attrInfos[i].isDeferredValue() || attrInfos[i].isDeferredMethod()) { + + // we need to scope the modified VariableMapper for consistency and performance + if (!variableMapperVar) { + out.printil("jakarta.el.VariableMapper _el_variablemapper = jspContext.getELContext().getVariableMapper();"); + variableMapperVar = true; + } + + out.printin("jakarta.el.ValueExpression _el_ve"); + out.print(i); + out.print(" = _el_variablemapper.setVariable("); + out.print(quote(attrName)); + out.print(','); + if (attrInfos[i].isDeferredMethod()) { + out.print("_jsp_getExpressionFactory().createValueExpression("); + out.print(toGetterMethod(attrName)); + out.print(",jakarta.el.MethodExpression.class)"); + } else { + out.print(toGetterMethod(attrName)); + } + out.println(");"); + } else { + out.printil("if( " + toGetterMethod(attrName) + " != null ) "); + out.pushIndent(); + out.printin("_jspx_page_context.setAttribute("); + out.print(quote(attrName)); + out.print(", "); + out.print(toGetterMethod(attrName)); + out.println(");"); + out.popIndent(); + } + } + + // Expose the Map containing dynamic attributes as a page-scoped var + if (tagInfo.hasDynamicAttributes()) { + out.printin("_jspx_page_context.setAttribute(\""); + out.print(tagInfo.getDynamicAttributesMapName()); + out.print("\", _jspx_dynamic_attrs);"); + } + } + + /* + * Generates the getter method for the given attribute name. + */ + private String toGetterMethod(String attrName) { + char[] attrChars = attrName.toCharArray(); + attrChars[0] = Character.toUpperCase(attrChars[0]); + return "get" + new String(attrChars) + "()"; + } + + /* + * Generates the setter method name for the given attribute name. + */ + private String toSetterMethodName(String attrName) { + char[] attrChars = attrName.toCharArray(); + attrChars[0] = Character.toUpperCase(attrChars[0]); + return "set" + new String(attrChars); + } + + /** + * Class storing the result of introspecting a custom tag handler. + */ + private static class TagHandlerInfo { + + private Map methodMaps; + + private Map> propertyEditorMaps; + + private Class tagHandlerClass; + + /** + * Constructor. + * + * @param n + * The custom tag whose tag handler class is to be + * introspected + * @param tagHandlerClass + * Tag handler class + * @param err + * Error dispatcher + */ + TagHandlerInfo(Node n, Class tagHandlerClass, + ErrorDispatcher err) throws JasperException { + this.tagHandlerClass = tagHandlerClass; + this.methodMaps = new HashMap<>(); + this.propertyEditorMaps = new HashMap<>(); + + try { + BeanInfo tagClassInfo = Introspector.getBeanInfo(tagHandlerClass); + PropertyDescriptor[] pd = tagClassInfo.getPropertyDescriptors(); + for (PropertyDescriptor propertyDescriptor : pd) { + /* + * FIXME: should probably be checking for things like + * pageContext, bodyContent, and parent here -akv + */ + if (propertyDescriptor.getWriteMethod() != null) { + methodMaps.put(propertyDescriptor.getName(), propertyDescriptor.getWriteMethod()); + } + if (propertyDescriptor.getPropertyEditorClass() != null) { + propertyEditorMaps.put(propertyDescriptor.getName(), propertyDescriptor + .getPropertyEditorClass()); + } + } + } catch (IntrospectionException ie) { + // Likely unreachable code + // When last checked (May 2021), current versions of Java only + // throw IntrospectionException for the 2-arg version of + // getBeanInfo if the stop class is not a super class of the + // bean class. That does not apply here. + err.jspError(n, ie, "jsp.error.introspect.taghandler", tagHandlerClass.getName()); + } + } + + public Method getSetterMethod(String attrName) { + return methodMaps.get(attrName); + } + + public Class getPropertyEditorClass(String attrName) { + return propertyEditorMaps.get(attrName); + } + + public Class getTagHandlerClass() { + return tagHandlerClass; + } + } + + /** + * A class for generating codes to a buffer. Included here are some support + * for tracking source to Java lines mapping. + */ + private static class GenBuffer { + + /* + * For a CustomTag, the codes that are generated at the beginning of the + * tag may not be in the same buffer as those for the body of the tag. + * Two fields are used here to keep this straight. For codes that do not + * corresponds to any JSP lines, they should be null. + */ + private Node node; + + private Node.Nodes body; + + private java.io.CharArrayWriter charWriter; + + protected ServletWriter out; + + GenBuffer() { + this(null, null); + } + + GenBuffer(Node n, Node.Nodes b) { + node = n; + body = b; + if (body != null) { + body.setGeneratedInBuffer(true); + } + charWriter = new java.io.CharArrayWriter(); + out = new ServletWriter(new java.io.PrintWriter(charWriter)); + } + + public ServletWriter getOut() { + return out; + } + + @Override + public String toString() { + return charWriter.toString(); + } + + /** + * Adjust the Java Lines. This is necessary because the Java lines + * stored with the nodes are relative the beginning of this buffer and + * need to be adjusted when this buffer is inserted into the source. + * + * @param offset The offset to apply to the start line and end line of + * and Java lines of nodes in this buffer + */ + public void adjustJavaLines(final int offset) { + + if (node != null) { + adjustJavaLine(node, offset); + } + + if (body != null) { + try { + body.visit(new Node.Visitor() { + + @Override + public void doVisit(Node n) { + adjustJavaLine(n, offset); + } + + @Override + public void visit(Node.CustomTag n) + throws JasperException { + Node.Nodes b = n.getBody(); + if (b != null && !b.isGeneratedInBuffer()) { + // Don't adjust lines for the nested tags that + // are also generated in buffers, because the + // adjustments will be done elsewhere. + b.visit(this); + } + } + }); + } catch (JasperException ex) { + // Ignore + } + } + } + + private static void adjustJavaLine(Node n, int offset) { + if (n.getBeginJavaLine() > 0) { + n.setBeginJavaLine(n.getBeginJavaLine() + offset); + n.setEndJavaLine(n.getEndJavaLine() + offset); + } + } + } + + /** + * Keeps track of the generated Fragment Helper Class + */ + private static class FragmentHelperClass { + + private static class Fragment { + private GenBuffer genBuffer; + + private int id; + + Fragment(int id, Node node) { + this.id = id; + genBuffer = new GenBuffer(null, node.getBody()); + } + + public GenBuffer getGenBuffer() { + return this.genBuffer; + } + + public int getId() { + return this.id; + } + } + + // True if the helper class should be generated. + private boolean used = false; + + private List fragments = new ArrayList<>(); + + private String className; + + // Buffer for entire helper class + private GenBuffer classBuffer = new GenBuffer(); + + FragmentHelperClass(String className) { + this.className = className; + } + + public String getClassName() { + return this.className; + } + + public boolean isUsed() { + return this.used; + } + + public void generatePreamble() { + ServletWriter out = this.classBuffer.getOut(); + out.println(); + out.pushIndent(); + // Note: cannot be static, as we need to reference things like + // _jspx_meth_* + out.printil("private class " + className); + out.printil(" extends " + + "org.apache.jasper.runtime.JspFragmentHelper"); + out.printil("{"); + out.pushIndent(); + out.printil("private jakarta.servlet.jsp.tagext.JspTag _jspx_parent;"); + out.printil("private int[] _jspx_push_body_count;"); + out.println(); + out.printil("public " + className + + "( int discriminator, jakarta.servlet.jsp.JspContext jspContext, " + + "jakarta.servlet.jsp.tagext.JspTag _jspx_parent, " + + "int[] _jspx_push_body_count ) {"); + out.pushIndent(); + out.printil("super( discriminator, jspContext, _jspx_parent );"); + out.printil("this._jspx_parent = _jspx_parent;"); + out.printil("this._jspx_push_body_count = _jspx_push_body_count;"); + out.popIndent(); + out.printil("}"); + } + + public Fragment openFragment(ChildInfoBase parent, int methodNesting) { + Fragment result = new Fragment(fragments.size(), parent); + fragments.add(result); + this.used = true; + parent.setInnerClassName(className); + + ServletWriter out = result.getGenBuffer().getOut(); + out.pushIndent(); + out.pushIndent(); + // XXX - Returns boolean because if a tag is invoked from + // within this fragment, the Generator sometimes might + // generate code like "return true". This is ignored for now, + // meaning only the fragment is skipped. The JSR-152 + // expert group is currently discussing what to do in this case. + // See comment in closeFragment() + if (methodNesting > 0) { + out.printin("public boolean invoke"); + } else { + out.printin("public void invoke"); + } + out.println(result.getId() + "( " + "jakarta.servlet.jsp.JspWriter out ) "); + out.pushIndent(); + // Note: Throwable required because methods like _jspx_meth_* + // throw Throwable. + out.printil("throws java.lang.Throwable"); + out.popIndent(); + out.printil("{"); + out.pushIndent(); + generateLocalVariables(out, parent); + + return result; + } + + public void closeFragment(Fragment fragment, int methodNesting) { + ServletWriter out = fragment.getGenBuffer().getOut(); + // XXX - See comment in openFragment() + if (methodNesting > 0) { + out.printil("return false;"); + } else { + out.printil("return;"); + } + out.popIndent(); + out.printil("}"); + } + + public void generatePostamble() { + ServletWriter out = this.classBuffer.getOut(); + // Generate all fragment methods: + for (Fragment fragment : fragments) { + fragment.getGenBuffer().adjustJavaLines(out.getJavaLine() - 1); + out.printMultiLn(fragment.getGenBuffer().toString()); + } + + // Generate postamble: + out.printil("public void invoke( java.io.Writer writer )"); + out.pushIndent(); + out.printil("throws jakarta.servlet.jsp.JspException"); + out.popIndent(); + out.printil("{"); + out.pushIndent(); + out.printil("jakarta.servlet.jsp.JspWriter out = null;"); + out.printil("if( writer != null ) {"); + out.pushIndent(); + out.printil("out = this.jspContext.pushBody(writer);"); + out.popIndent(); + out.printil("} else {"); + out.pushIndent(); + out.printil("out = this.jspContext.getOut();"); + out.popIndent(); + out.printil("}"); + out.printil("try {"); + out.pushIndent(); + out.printil("Object _jspx_saved_JspContext = this.jspContext.getELContext().getContext(jakarta.servlet.jsp.JspContext.class);"); + out.printil("this.jspContext.getELContext().putContext(jakarta.servlet.jsp.JspContext.class,this.jspContext);"); + out.printil("switch( this.discriminator ) {"); + out.pushIndent(); + for (int i = 0; i < fragments.size(); i++) { + out.printil("case " + i + ":"); + out.pushIndent(); + out.printil("invoke" + i + "( out );"); + out.printil("break;"); + out.popIndent(); + } + out.popIndent(); + out.printil("}"); // switch + + // restore nested JspContext on ELContext + out.printil("jspContext.getELContext().putContext(jakarta.servlet.jsp.JspContext.class,_jspx_saved_JspContext);"); + + out.popIndent(); + out.printil("}"); // try + out.printil("catch( java.lang.Throwable e ) {"); + out.pushIndent(); + out.printil("if (e instanceof jakarta.servlet.jsp.SkipPageException)"); + out.printil(" throw (jakarta.servlet.jsp.SkipPageException) e;"); + out.printil("throw new jakarta.servlet.jsp.JspException( e );"); + out.popIndent(); + out.printil("}"); // catch + out.printil("finally {"); + out.pushIndent(); + + out.printil("if( writer != null ) {"); + out.pushIndent(); + out.printil("this.jspContext.popBody();"); + out.popIndent(); + out.printil("}"); + + out.popIndent(); + out.printil("}"); // finally + out.popIndent(); + out.printil("}"); // invoke method + out.popIndent(); + out.printil("}"); // helper class + out.popIndent(); + } + + @Override + public String toString() { + return classBuffer.toString(); + } + + public void adjustJavaLines(int offset) { + for (Fragment fragment : fragments) { + fragment.getGenBuffer().adjustJavaLines(offset); + } + } + } +} diff --git a/java/org/apache/jasper/compiler/ImplicitTagLibraryInfo.java b/java/org/apache/jasper/compiler/ImplicitTagLibraryInfo.java new file mode 100644 index 0000000..9d70eee --- /dev/null +++ b/java/org/apache/jasper/compiler/ImplicitTagLibraryInfo.java @@ -0,0 +1,203 @@ +/* + * 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.jasper.compiler; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.jsp.tagext.FunctionInfo; +import jakarta.servlet.jsp.tagext.TagFileInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; + +import org.apache.jasper.Constants; +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.tomcat.util.descriptor.tld.ImplicitTldRuleSet; +import org.apache.tomcat.util.descriptor.tld.TaglibXml; +import org.apache.tomcat.util.descriptor.tld.TldParser; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; +import org.xml.sax.SAXException; + +/** + * Class responsible for generating an implicit tag library containing tag + * handlers corresponding to the tag files in "/WEB-INF/tags/" or a + * subdirectory of it. + * + * @author Jan Luehe + */ +class ImplicitTagLibraryInfo extends TagLibraryInfo { + + private static final String WEB_INF_TAGS = "/WEB-INF/tags"; + private static final String TAG_FILE_SUFFIX = ".tag"; + private static final String TAGX_FILE_SUFFIX = ".tagx"; + private static final String TAGS_SHORTNAME = "tags"; + private static final String TLIB_VERSION = "1.0"; + private static final String JSP_VERSION = "2.0"; + private static final String IMPLICIT_TLD = "implicit.tld"; + + // Maps tag names to tag file paths + private final Map tagFileMap; + + private final ParserController pc; + private final PageInfo pi; + private final List list; + + + ImplicitTagLibraryInfo(JspCompilationContext ctxt, + ParserController pc, + PageInfo pi, + String prefix, + String tagdir, + ErrorDispatcher err) throws JasperException { + super(prefix, null); + this.pc = pc; + this.pi = pi; + this.tagFileMap = new ConcurrentHashMap<>(); + this.list = Collections.synchronizedList(new ArrayList<>()); + + // Implicit tag libraries have no functions: + this.functions = new FunctionInfo[0]; + + tlibversion = TLIB_VERSION; + jspversion = JSP_VERSION; + + if (!tagdir.startsWith(WEB_INF_TAGS)) { + err.jspError("jsp.error.invalid.tagdir", tagdir); + } + + // Determine the value of the subelement of the + // "imaginary" element + if (tagdir.equals(WEB_INF_TAGS) + || tagdir.equals( WEB_INF_TAGS + "/")) { + shortname = TAGS_SHORTNAME; + } else { + shortname = tagdir.substring(WEB_INF_TAGS.length()); + shortname = shortname.replace('/', '-'); + } + + // Populate mapping of tag names to tag file paths + Set dirList = ctxt.getResourcePaths(tagdir); + if (dirList != null) { + for (String path : dirList) { + if (path.endsWith(TAG_FILE_SUFFIX) + || path.endsWith(TAGX_FILE_SUFFIX)) { + /* + * Use the filename of the tag file, without the .tag or + * .tagx extension, respectively, as the subelement + * of the "imaginary" element + */ + String suffix = path.endsWith(TAG_FILE_SUFFIX) ? + TAG_FILE_SUFFIX : TAGX_FILE_SUFFIX; + String tagName = path.substring(path.lastIndexOf('/') + 1); + tagName = tagName.substring(0, + tagName.lastIndexOf(suffix)); + tagFileMap.put(tagName, path); + } else if (path.endsWith(IMPLICIT_TLD)) { + TaglibXml taglibXml; + try { + URL url = ctxt.getResource(path); + TldResourcePath resourcePath = new TldResourcePath(url, path); + ServletContext servletContext = ctxt.getServletContext(); + boolean validate = Boolean.parseBoolean( + servletContext.getInitParameter( + Constants.XML_VALIDATION_TLD_INIT_PARAM)); + String blockExternalString = servletContext.getInitParameter( + Constants.XML_BLOCK_EXTERNAL_INIT_PARAM); + boolean blockExternal; + if (blockExternalString == null) { + blockExternal = true; + } else { + blockExternal = Boolean.parseBoolean(blockExternalString); + } + TldParser parser = new TldParser(true, validate, + new ImplicitTldRuleSet(), blockExternal); + taglibXml = parser.parse(resourcePath); + } catch (IOException | SAXException e) { + err.jspError(e); + // unreached + throw new JasperException(e); + } + this.tlibversion = taglibXml.getTlibVersion(); + this.jspversion = taglibXml.getJspVersion(); + try { + double version = Double.parseDouble(this.jspversion); + if (version < 2.0) { + err.jspError("jsp.error.invalid.implicit.version", path); + } + } catch (NumberFormatException e) { + err.jspError("jsp.error.invalid.implicit.version", path); + } + + // Add implicit TLD to dependency list + if (pi != null) { + pi.addDependant(path, ctxt.getLastModified(path)); + } + } + } + } + + } + + /** + * Checks to see if the given tag name maps to a tag file path, + * and if so, parses the corresponding tag file. + * + * @return The TagFileInfo corresponding to the given tag name, or null if + * the given tag name is not implemented as a tag file + */ + @Override + public TagFileInfo getTagFile(String shortName) { + + TagFileInfo tagFile = super.getTagFile(shortName); + if (tagFile == null) { + String path = tagFileMap.get(shortName); + if (path == null) { + return null; + } + + TagInfo tagInfo = null; + try { + tagInfo = TagFileProcessor.parseTagFileDirectives(pc, shortName, path, null, this); + } catch (JasperException je) { + throw new RuntimeException(je.toString(), je); + } + + tagFile = new TagFileInfo(shortName, path, tagInfo); + list.add(tagFile); + + this.tagFiles = list.toArray(new TagFileInfo[0]); + } + + return tagFile; + } + + @Override + public TagLibraryInfo[] getTagLibraryInfos() { + Collection coll = pi.getTaglibs(); + return coll.toArray(new TagLibraryInfo[0]); + } + +} diff --git a/java/org/apache/jasper/compiler/JDTCompiler.java b/java/org/apache/jasper/compiler/JDTCompiler.java new file mode 100644 index 0000000..2179f3a --- /dev/null +++ b/java/org/apache/jasper/compiler/JDTCompiler.java @@ -0,0 +1,541 @@ +/* + * 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.jasper.compiler; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.jasper.JasperException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.eclipse.jdt.core.compiler.IProblem; +import org.eclipse.jdt.internal.compiler.ClassFile; +import org.eclipse.jdt.internal.compiler.CompilationResult; +import org.eclipse.jdt.internal.compiler.Compiler; +import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies; +import org.eclipse.jdt.internal.compiler.ICompilerRequestor; +import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy; +import org.eclipse.jdt.internal.compiler.IProblemFactory; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException; +import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; +import org.eclipse.jdt.internal.compiler.env.INameEnvironment; +import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer; +import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; +import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; + +/** + * JDT class compiler. This compiler will load source dependencies from the + * context classloader, reducing dramatically disk access during + * the compilation process. + * + * Based on code from Cocoon2. + * + * @author Remy Maucherat + */ +public class JDTCompiler extends org.apache.jasper.compiler.Compiler { + + private final Log log = LogFactory.getLog(JDTCompiler.class); // must not be static + + /** + * Compile the servlet from .java file to .class file + */ + @Override + protected void generateClass(Map smaps) + throws FileNotFoundException, JasperException, Exception { + + long t1 = 0; + if (log.isDebugEnabled()) { + t1 = System.currentTimeMillis(); + } + + final String sourceFile = ctxt.getServletJavaFileName(); + final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); + String packageName = ctxt.getServletPackageName(); + final String targetClassName = + ((packageName.length() != 0) ? (packageName + ".") : "") + ctxt.getServletClassName(); + final ClassLoader classLoader = ctxt.getJspLoader(); + String[] fileNames = new String[] {sourceFile}; + String[] classNames = new String[] {targetClassName}; + final List problemList = new ArrayList<>(); + + class CompilationUnit implements ICompilationUnit { + + private final String className; + private final String sourceFile; + + CompilationUnit(String sourceFile, String className) { + this.className = className; + this.sourceFile = sourceFile; + } + + @Override + public char[] getFileName() { + return sourceFile.toCharArray(); + } + + @Override + public char[] getContents() { + char[] result = null; + try (FileInputStream is = new FileInputStream(sourceFile); + InputStreamReader isr = new InputStreamReader(is, ctxt.getOptions().getJavaEncoding()); + Reader reader = new BufferedReader(isr)) { + char[] chars = new char[8192]; + StringBuilder buf = new StringBuilder(); + int count; + while ((count = reader.read(chars, 0, chars.length)) > 0) { + buf.append(chars, 0, count); + } + result = new char[buf.length()]; + buf.getChars(0, result.length, result, 0); + } catch (IOException e) { + log.error(Localizer.getMessage("jsp.error.compilation.source", sourceFile), e); + } + return result; + } + + @Override + public char[] getMainTypeName() { + int dot = className.lastIndexOf('.'); + if (dot > 0) { + return className.substring(dot + 1).toCharArray(); + } + return className.toCharArray(); + } + + @Override + public char[][] getPackageName() { + StringTokenizer izer = new StringTokenizer(className, "."); + char[][] result = new char[izer.countTokens()-1][]; + for (int i = 0; i < result.length; i++) { + String tok = izer.nextToken(); + result[i] = tok.toCharArray(); + } + return result; + } + + @Override + public boolean ignoreOptionalProblems() { + return false; + } + } + + final INameEnvironment env = new INameEnvironment() { + + @Override + public NameEnvironmentAnswer findType(char[][] compoundTypeName) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < compoundTypeName.length; i++) { + if (i > 0) { + result.append('.'); + } + result.append(compoundTypeName[i]); + } + return findType(result.toString()); + } + + @Override + public NameEnvironmentAnswer findType(char[] typeName, char[][] packageName) { + StringBuilder result = new StringBuilder(); + int i=0; + for (; i < packageName.length; i++) { + if (i > 0) { + result.append('.'); + } + result.append(packageName[i]); + } + if (i > 0) { + result.append('.'); + } + result.append(typeName); + return findType(result.toString()); + } + + private NameEnvironmentAnswer findType(String className) { + + if (className.equals(targetClassName)) { + ICompilationUnit compilationUnit = new CompilationUnit(sourceFile, className); + return new NameEnvironmentAnswer(compilationUnit, null); + } + + String resourceName = className.replace('.', '/') + ".class"; + + try (InputStream is = classLoader.getResourceAsStream(resourceName)) { + if (is != null) { + byte[] classBytes; + byte[] buf = new byte[8192]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(buf.length); + int count; + while ((count = is.read(buf, 0, buf.length)) > 0) { + baos.write(buf, 0, count); + } + baos.flush(); + classBytes = baos.toByteArray(); + char[] fileName = className.toCharArray(); + ClassFileReader classFileReader = new ClassFileReader(classBytes, fileName, true); + return new NameEnvironmentAnswer(classFileReader, null); + } + } catch (IOException | ClassFormatException exc) { + log.error(Localizer.getMessage("jsp.error.compilation.dependent", className), exc); + } + return null; + } + + private boolean isPackage(String result) { + if (result.equals(targetClassName) || result.startsWith(targetClassName + '$')) { + return false; + } + String resourceName = result.replace('.', '/') + ".class"; + try (InputStream is = + classLoader.getResourceAsStream(resourceName)) { + return is == null; + } catch (IOException e) { + // we are here, since close on is failed. That means it was not null + return false; + } + } + + @Override + public boolean isPackage(char[][] parentPackageName, char[] packageName) { + StringBuilder result = new StringBuilder(); + int i = 0; + if (parentPackageName != null) { + for (; i < parentPackageName.length; i++) { + if (i > 0) { + result.append('.'); + } + result.append(parentPackageName[i]); + } + } + + if (Character.isUpperCase(packageName[0])) { + if (!isPackage(result.toString())) { + return false; + } + } + if (i > 0) { + result.append('.'); + } + result.append(packageName); + + return isPackage(result.toString()); + } + + @Override + public void cleanup() { + } + + }; + + final IErrorHandlingPolicy policy = DefaultErrorHandlingPolicies.proceedWithAllProblems(); + + final Map settings = new HashMap<>(); + settings.put(CompilerOptions.OPTION_LineNumberAttribute, + CompilerOptions.GENERATE); + settings.put(CompilerOptions.OPTION_SourceFileAttribute, + CompilerOptions.GENERATE); + settings.put(CompilerOptions.OPTION_ReportDeprecation, + CompilerOptions.IGNORE); + if (ctxt.getOptions().getJavaEncoding() != null) { + settings.put(CompilerOptions.OPTION_Encoding, + ctxt.getOptions().getJavaEncoding()); + } + if (ctxt.getOptions().getClassDebugInfo()) { + settings.put(CompilerOptions.OPTION_LocalVariableAttribute, + CompilerOptions.GENERATE); + } + + // Source JVM + if(ctxt.getOptions().getCompilerSourceVM() != null) { + String opt = ctxt.getOptions().getCompilerSourceVM(); + if(opt.equals("1.1")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_1); + } else if(opt.equals("1.2")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_2); + } else if(opt.equals("1.3")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_3); + } else if(opt.equals("1.4")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_4); + } else if(opt.equals("1.5")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_5); + } else if(opt.equals("1.6")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_6); + } else if(opt.equals("1.7")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_7); + } else if(opt.equals("1.8")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_8); + // Version format changed from Java 9 onwards. + // Support old format that was used in EA implementation as well + } else if(opt.equals("9") || opt.equals("1.9")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_9); + } else if(opt.equals("10")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_10); + } else if(opt.equals("11")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_11); + } else if(opt.equals("12")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_12); + } else if(opt.equals("13")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_13); + } else if(opt.equals("14")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_14); + } else if(opt.equals("15")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_15); + } else if(opt.equals("16")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_16); + } else if(opt.equals("17")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_17); + } else if(opt.equals("18")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_18); + } else if (opt.equals("19")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_19); + } else if (opt.equals("20")) { + // Constant not available in latest ECJ version that runs on + // Java 11. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_Source, "20"); + } else if (opt.equals("21")) { + // Constant not available in latest ECJ version that runs on + // Java 11. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_Source, "21"); + } else if (opt.equals("22")) { + // Constant not available in latest ECJ version that runs on + // Java 11. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_Source, "22"); + } else { + log.warn(Localizer.getMessage("jsp.warning.unknown.sourceVM", opt)); + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_11); + } + } else { + // Default to 11 + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_11); + } + + // Target JVM + if(ctxt.getOptions().getCompilerTargetVM() != null) { + String opt = ctxt.getOptions().getCompilerTargetVM(); + if(opt.equals("1.1")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_1); + } else if(opt.equals("1.2")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_2); + } else if(opt.equals("1.3")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_3); + } else if(opt.equals("1.4")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_4); + } else if(opt.equals("1.5")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_5); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_1_5); + } else if(opt.equals("1.6")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_6); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_1_6); + } else if(opt.equals("1.7")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_7); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_1_7); + } else if(opt.equals("1.8")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_8); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_1_8); + // Version format changed from Java 9 onwards. + // Support old format that was used in EA implementation as well + } else if(opt.equals("9") || opt.equals("1.9")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_9); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_9); + } else if(opt.equals("10")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_10); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_10); + } else if(opt.equals("11")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_11); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_11); + } else if(opt.equals("12")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_12); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_12); + } else if(opt.equals("13")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_13); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_13); + } else if(opt.equals("14")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_14); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_14); + } else if(opt.equals("15")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_15); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_15); + } else if(opt.equals("16")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_16); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_16); + } else if(opt.equals("17")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_17); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_17); + } else if(opt.equals("18")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_18); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_18); + } else if (opt.equals("19")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_19); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_19); + } else if (opt.equals("20")) { + // Constant not available in latest ECJ version that runs on + // Java 11. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_TargetPlatform, "20"); + settings.put(CompilerOptions.OPTION_Compliance, "20"); + } else if (opt.equals("21")) { + // Constant not available in latest ECJ version that runs on + // Java 11. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_TargetPlatform, "21"); + settings.put(CompilerOptions.OPTION_Compliance, "21"); + } else if (opt.equals("22")) { + // Constant not available in latest ECJ version that runs on + // Java 11. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_TargetPlatform, "22"); + settings.put(CompilerOptions.OPTION_Compliance, "22"); + } else { + log.warn(Localizer.getMessage("jsp.warning.unknown.targetVM", opt)); + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_11); + } + } else { + // Default to 11 + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_11); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_11); + } + + final IProblemFactory problemFactory = new DefaultProblemFactory(Locale.getDefault()); + + final ICompilerRequestor requestor = new ICompilerRequestor() { + @Override + public void acceptResult(CompilationResult result) { + try { + if (result.hasProblems()) { + IProblem[] problems = result.getProblems(); + for (IProblem problem : problems) { + if (problem.isError()) { + String name = + new String(problem.getOriginatingFileName()); + try { + problemList.add(ErrorDispatcher.createJavacError + (name, pageNodes, new StringBuilder(problem.getMessage()), + problem.getSourceLineNumber(), ctxt)); + } catch (JasperException e) { + log.error(Localizer.getMessage("jsp.error.compilation.jdtProblemError"), e); + } + } + } + } + if (problemList.isEmpty()) { + ClassFile[] classFiles = result.getClassFiles(); + for (ClassFile classFile : classFiles) { + char[][] compoundName = + classFile.getCompoundName(); + StringBuilder classFileName = new StringBuilder(outputDir).append('/'); + for (int j = 0; + j < compoundName.length; j++) { + if (j > 0) { + classFileName.append('/'); + } + classFileName.append(compoundName[j]); + } + byte[] bytes = classFile.getBytes(); + classFileName.append(".class"); + try (FileOutputStream fout = new FileOutputStream(classFileName.toString()); + BufferedOutputStream bos = new BufferedOutputStream(fout)) { + bos.write(bytes); + } + } + } + } catch (IOException exc) { + log.error(Localizer.getMessage("jsp.error.compilation.jdt"), exc); + } + } + }; + + ICompilationUnit[] compilationUnits = + new ICompilationUnit[classNames.length]; + for (int i = 0; i < compilationUnits.length; i++) { + String className = classNames[i]; + compilationUnits[i] = new CompilationUnit(fileNames[i], className); + } + CompilerOptions cOptions = new CompilerOptions(settings); + + // Check source/target JDK versions as the newest versions are allowed + // in Tomcat configuration but may not be supported by the ECJ version + // being used. + String requestedSource = ctxt.getOptions().getCompilerSourceVM(); + if (requestedSource != null) { + String actualSource = CompilerOptions.versionFromJdkLevel(cOptions.sourceLevel); + if (!requestedSource.equals(actualSource)) { + log.warn(Localizer.getMessage("jsp.warning.unsupported.sourceVM", requestedSource, actualSource)); + } + } + String requestedTarget = ctxt.getOptions().getCompilerTargetVM(); + if (requestedTarget != null) { + String actualTarget = CompilerOptions.versionFromJdkLevel(cOptions.targetJDK); + if (!requestedTarget.equals(actualTarget)) { + log.warn(Localizer.getMessage("jsp.warning.unsupported.targetVM", requestedTarget, actualTarget)); + } + } + + cOptions.parseLiteralExpressionsAsConstants = true; + Compiler compiler = new Compiler(env, + policy, + cOptions, + requestor, + problemFactory); + compiler.compile(compilationUnits); + + if (!ctxt.keepGenerated()) { + File javaFile = new File(ctxt.getServletJavaFileName()); + if (!javaFile.delete()) { + throw new JasperException(Localizer.getMessage( + "jsp.warning.compiler.javafile.delete.fail", javaFile)); + } + } + + if (!problemList.isEmpty()) { + JavacErrorDetail[] jeds = + problemList.toArray(new JavacErrorDetail[0]); + errDispatcher.javacError(jeds); + } + + if( log.isDebugEnabled() ) { + long t2=System.currentTimeMillis(); + log.debug(Localizer.getMessage("jsp.compiled", ctxt.getServletJavaFileName(), Long.valueOf(t2 - t1))); + } + + if (ctxt.isPrototypeMode()) { + return; + } + + // JSR45 Support + if (! options.isSmapSuppressed()) { + SmapUtil.installSmap(smaps); + } + } +} diff --git a/java/org/apache/jasper/compiler/JarScannerFactory.java b/java/org/apache/jasper/compiler/JarScannerFactory.java new file mode 100644 index 0000000..b855d06 --- /dev/null +++ b/java/org/apache/jasper/compiler/JarScannerFactory.java @@ -0,0 +1,50 @@ +/* + * 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.jasper.compiler; + +import jakarta.servlet.ServletContext; + +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.util.scan.StandardJarScanner; + +/** + * Provide a mechanism for Jasper to obtain a reference to the JarScanner + * implementation. + */ +public class JarScannerFactory { + + private JarScannerFactory() { + // Don't want any instances so hide the default constructor. + } + + /** + * Obtain the {@link JarScanner} associated with the specified {@link + * ServletContext}. It is obtained via a context parameter. + * @param ctxt The Servlet context + * @return a scanner instance + */ + public static JarScanner getJarScanner(ServletContext ctxt) { + JarScanner jarScanner = + (JarScanner) ctxt.getAttribute(JarScanner.class.getName()); + if (jarScanner == null) { + ctxt.log(Localizer.getMessage("jsp.warning.noJarScanner")); + jarScanner = new StandardJarScanner(); + } + return jarScanner; + } + +} diff --git a/java/org/apache/jasper/compiler/JasperTagInfo.java b/java/org/apache/jasper/compiler/JasperTagInfo.java new file mode 100644 index 0000000..9d11b61 --- /dev/null +++ b/java/org/apache/jasper/compiler/JasperTagInfo.java @@ -0,0 +1,63 @@ +/* + * 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.jasper.compiler; + +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagExtraInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; +import jakarta.servlet.jsp.tagext.TagVariableInfo; + +/** + * TagInfo extension used by tag handlers that are implemented via tag files. + * This class provides access to the name of the Map used to store the + * dynamic attribute names and values passed to the custom action invocation. + * This information is used by the code generator. + */ +class JasperTagInfo extends TagInfo { + + private final String dynamicAttrsMapName; + + JasperTagInfo(String tagName, + String tagClassName, + String bodyContent, + String infoString, + TagLibraryInfo taglib, + TagExtraInfo tagExtraInfo, + TagAttributeInfo[] attributeInfo, + String displayName, + String smallIcon, + String largeIcon, + TagVariableInfo[] tvi, + String mapName) { + + super(tagName, tagClassName, bodyContent, infoString, taglib, + tagExtraInfo, attributeInfo, displayName, smallIcon, largeIcon, + tvi); + + this.dynamicAttrsMapName = mapName; + } + + public String getDynamicAttributesMapName() { + return dynamicAttrsMapName; + } + + @Override + public boolean hasDynamicAttributes() { + return dynamicAttrsMapName != null; + } +} diff --git a/java/org/apache/jasper/compiler/JavacErrorDetail.java b/java/org/apache/jasper/compiler/JavacErrorDetail.java new file mode 100644 index 0000000..9daab47 --- /dev/null +++ b/java/org/apache/jasper/compiler/JavacErrorDetail.java @@ -0,0 +1,230 @@ +/* + * 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.jasper.compiler; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import org.apache.jasper.JspCompilationContext; +import org.apache.tomcat.Jar; + +/** + * Class providing details about a javac compilation error. + * + * @author Jan Luehe + * @author Kin-man Chung + */ +public class JavacErrorDetail { + + private final String javaFileName; + private final int javaLineNum; + private String jspFileName; + private int jspBeginLineNum; + private final StringBuilder errMsg; + private String jspExtract = null; + + /** + * Constructor. + * + * @param javaFileName The name of the Java file in which the + * compilation error occurred + * @param javaLineNum The compilation error line number + * @param errMsg The compilation error message + */ + public JavacErrorDetail(String javaFileName, + int javaLineNum, + StringBuilder errMsg) { + + this(javaFileName, javaLineNum, null, -1, errMsg, null); + } + + /** + * Constructor. + * + * @param javaFileName The name of the Java file in which the + * compilation error occurred + * @param javaLineNum The compilation error line number + * @param jspFileName The name of the JSP file from which the Java source + * file was generated + * @param jspBeginLineNum The start line number of the JSP element + * responsible for the compilation error + * @param errMsg The compilation error message + * @param ctxt The compilation context + */ + public JavacErrorDetail(String javaFileName, + int javaLineNum, + String jspFileName, + int jspBeginLineNum, + StringBuilder errMsg, + JspCompilationContext ctxt) { + + this.javaFileName = javaFileName; + this.javaLineNum = javaLineNum; + this.errMsg = errMsg; + this.jspFileName = jspFileName; + // Note: this.jspBeginLineNum is set at the end of this method as it may + // be modified (corrected) during the execution of this method + + if (jspBeginLineNum > 0 && ctxt != null) { + InputStream is = null; + try { + Jar tagJar = ctxt.getTagFileJar(); + if (tagJar != null) { + // Strip leading '/' + String entryName = jspFileName.substring(1); + is = tagJar.getInputStream(entryName); + this.jspFileName = tagJar.getURL(entryName); + } else { + is = ctxt.getResourceAsStream(jspFileName); + } + // Read both files in, so we can inspect them + String[] jspLines = readFile(is); + + try (FileInputStream fis = new FileInputStream(ctxt.getServletJavaFileName())) { + String[] javaLines = readFile(fis); + + if (jspLines.length < jspBeginLineNum) { + // Avoid ArrayIndexOutOfBoundsException + // Probably bug 48498 but could be some other cause + jspExtract = Localizer.getMessage("jsp.error.bug48498"); + return; + } + + // If the line contains the opening of a multi-line scriptlet + // block, then the JSP line number we got back is probably + // faulty. Scan forward to match the java line... + if (jspLines[jspBeginLineNum-1].lastIndexOf("<%") > + jspLines[jspBeginLineNum-1].lastIndexOf("%>")) { + String javaLine = javaLines[javaLineNum-1].trim(); + + for (int i=jspBeginLineNum-1; i lines = new ArrayList<>(); + String line; + + while ( (line = reader.readLine()) != null ) { + lines.add(line); + } + + return lines.toArray(new String[0]); + } +} diff --git a/java/org/apache/jasper/compiler/JspConfig.java b/java/org/apache/jasper/compiler/JspConfig.java new file mode 100644 index 0000000..2eb20cb --- /dev/null +++ b/java/org/apache/jasper/compiler/JspConfig.java @@ -0,0 +1,544 @@ +/* + * 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.jasper.compiler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.descriptor.JspPropertyGroupDescriptor; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Handles the jsp-config element in WEB_INF/web.xml. This is used + * for specifying the JSP configuration information on a JSP page + * + * @author Kin-man Chung + * @author Remy Maucherat + */ + +public class JspConfig { + + // Logger + private final Log log = LogFactory.getLog(JspConfig.class); // must not be static + + private List jspProperties = null; + private final ServletContext ctxt; + private volatile boolean initialized = false; + + private static final String defaultIsXml = null; // unspecified + private String defaultIsELIgnored = null; // unspecified + private String defaultErrorOnELNotFound = "false"; + private static final String defaultIsScriptingInvalid = null; + private String defaultDeferedSyntaxAllowedAsLiteral = null; + private static final String defaultTrimDirectiveWhitespaces = null; + private static final String defaultDefaultContentType = null; + private static final String defaultBuffer = null; + private static final String defaultErrorOnUndeclaredNamespace = "false"; + private JspProperty defaultJspProperty; + + public JspConfig(ServletContext ctxt) { + this.ctxt = ctxt; + } + + private void processWebDotXml() { + + // Very, very unlikely but just in case... + if (ctxt.getEffectiveMajorVersion() < 2) { + defaultIsELIgnored = "true"; + defaultDeferedSyntaxAllowedAsLiteral = "true"; + return; + } + if (ctxt.getEffectiveMajorVersion() == 2) { + if (ctxt.getEffectiveMinorVersion() < 5) { + defaultDeferedSyntaxAllowedAsLiteral = "true"; + } + if (ctxt.getEffectiveMinorVersion() < 4) { + defaultIsELIgnored = "true"; + return; + } + } + + JspConfigDescriptor jspConfig = ctxt.getJspConfigDescriptor(); + + if (jspConfig == null) { + return; + } + + jspProperties = new ArrayList<>(); + Collection jspPropertyGroups = + jspConfig.getJspPropertyGroups(); + + for (JspPropertyGroupDescriptor jspPropertyGroup : jspPropertyGroups) { + + Collection urlPatterns = jspPropertyGroup.getUrlPatterns(); + + if (urlPatterns.size() == 0) { + continue; + } + + JspProperty property = new JspProperty(jspPropertyGroup.getIsXml(), + jspPropertyGroup.getElIgnored(), + jspPropertyGroup.getErrorOnELNotFound(), + jspPropertyGroup.getScriptingInvalid(), + jspPropertyGroup.getPageEncoding(), + jspPropertyGroup.getIncludePreludes(), + jspPropertyGroup.getIncludeCodas(), + jspPropertyGroup.getDeferredSyntaxAllowedAsLiteral(), + jspPropertyGroup.getTrimDirectiveWhitespaces(), + jspPropertyGroup.getDefaultContentType(), + jspPropertyGroup.getBuffer(), + jspPropertyGroup.getErrorOnUndeclaredNamespace()); + + // Add one JspPropertyGroup for each URL Pattern. This makes + // the matching logic easier. + for (String urlPattern : urlPatterns) { + String path = null; + String extension = null; + + if (urlPattern.indexOf('*') < 0) { + // Exact match + path = urlPattern; + } else { + int i = urlPattern.lastIndexOf('/'); + String file; + if (i >= 0) { + path = urlPattern.substring(0,i+1); + file = urlPattern.substring(i+1); + } else { + file = urlPattern; + } + + // pattern must be "*", or of the form "*.jsp" + if (file.equals("*")) { + extension = "*"; + } else if (file.startsWith("*.")) { + extension = file.substring(file.indexOf('.')+1); + } + + // The url patterns are reconstructed as the following: + // path != null, extension == null: / or /foo/bar.ext + // path == null, extension != null: *.ext + // path != null, extension == "*": /foo/* + boolean isStar = "*".equals(extension); + if ((path == null && (extension == null || isStar)) + || (path != null && !isStar)) { + if (log.isWarnEnabled()) { + log.warn(Localizer.getMessage( + "jsp.warning.bad.urlpattern.propertygroup", + urlPattern)); + } + continue; + } + } + + JspPropertyGroup propertyGroup = + new JspPropertyGroup(path, extension, property); + + jspProperties.add(propertyGroup); + } + } + } + + private void init() { + + if (!initialized) { + synchronized (this) { + if (!initialized) { + processWebDotXml(); + defaultJspProperty = new JspProperty(defaultIsXml, + defaultIsELIgnored, + defaultErrorOnELNotFound, + defaultIsScriptingInvalid, + null, null, null, + defaultDeferedSyntaxAllowedAsLiteral, + defaultTrimDirectiveWhitespaces, + defaultDefaultContentType, + defaultBuffer, + defaultErrorOnUndeclaredNamespace); + initialized = true; + } + } + } + } + + /** + * Select the property group that has more restrictive url-pattern. + * In case of tie, select the first. + */ + @SuppressWarnings("null") // NPE not possible + private JspPropertyGroup selectProperty(JspPropertyGroup prev, + JspPropertyGroup curr) { + if (prev == null) { + return curr; + } + if (prev.getExtension() == null) { + // exact match + return prev; + } + if (curr.getExtension() == null) { + // exact match + return curr; + } + String prevPath = prev.getPath(); + String currPath = curr.getPath(); + if (prevPath == null && currPath == null) { + // Both specifies a *.ext, keep the first one + return prev; + } + if (prevPath == null && currPath != null) { + return curr; + } + if (prevPath != null && currPath == null) { + return prev; + } + if (prevPath.length() >= currPath.length()) { + return prev; + } + return curr; + } + + + /** + * Find a property that best matches the supplied resource. + * @param uri the resource supplied. + * @return a JspProperty indicating the best match, or some default. + */ + public JspProperty findJspProperty(String uri) { + + init(); + + // JSP Configuration settings do not apply to tag files + if (jspProperties == null || uri.endsWith(".tag") + || uri.endsWith(".tagx")) { + return defaultJspProperty; + } + + String uriPath = null; + int index = uri.lastIndexOf('/'); + if (index >=0 ) { + uriPath = uri.substring(0, index+1); + } + String uriExtension = null; + index = uri.lastIndexOf('.'); + if (index >=0) { + uriExtension = uri.substring(index+1); + } + + Collection includePreludes = new ArrayList<>(); + Collection includeCodas = new ArrayList<>(); + + JspPropertyGroup isXmlMatch = null; + JspPropertyGroup elIgnoredMatch = null; + JspPropertyGroup errorOnELNotFoundMatch = null; + JspPropertyGroup scriptingInvalidMatch = null; + JspPropertyGroup pageEncodingMatch = null; + JspPropertyGroup deferedSyntaxAllowedAsLiteralMatch = null; + JspPropertyGroup trimDirectiveWhitespacesMatch = null; + JspPropertyGroup defaultContentTypeMatch = null; + JspPropertyGroup bufferMatch = null; + JspPropertyGroup errorOnUndeclaredNamespaceMatch = null; + + for (JspPropertyGroup jpg : jspProperties) { + JspProperty jp = jpg.getJspProperty(); + + // (arrays will be the same length) + String extension = jpg.getExtension(); + String path = jpg.getPath(); + + if (extension == null) { + // exact match pattern: /a/foo.jsp + if (!uri.equals(path)) { + // not matched; + continue; + } + } else { + // Matching patterns *.ext or /p/* + if (path != null && uriPath != null && + ! uriPath.startsWith(path)) { + // not matched + continue; + } + if (!extension.equals("*") && + !extension.equals(uriExtension)) { + // not matched + continue; + } + } + // We have a match + // Add include-preludes and include-codas + if (jp.getIncludePrelude() != null) { + includePreludes.addAll(jp.getIncludePrelude()); + } + if (jp.getIncludeCoda() != null) { + includeCodas.addAll(jp.getIncludeCoda()); + } + + // If there is a previous match for the same property, remember + // the one that is more restrictive. + if (jp.isXml() != null) { + isXmlMatch = selectProperty(isXmlMatch, jpg); + } + if (jp.isELIgnored() != null) { + elIgnoredMatch = selectProperty(elIgnoredMatch, jpg); + } + if (jp.getErrorOnELNotFound() != null) { + errorOnELNotFoundMatch = selectProperty(errorOnELNotFoundMatch, jpg); + } + if (jp.isScriptingInvalid() != null) { + scriptingInvalidMatch = + selectProperty(scriptingInvalidMatch, jpg); + } + if (jp.getPageEncoding() != null) { + pageEncodingMatch = selectProperty(pageEncodingMatch, jpg); + } + if (jp.isDeferedSyntaxAllowedAsLiteral() != null) { + deferedSyntaxAllowedAsLiteralMatch = + selectProperty(deferedSyntaxAllowedAsLiteralMatch, jpg); + } + if (jp.isTrimDirectiveWhitespaces() != null) { + trimDirectiveWhitespacesMatch = + selectProperty(trimDirectiveWhitespacesMatch, jpg); + } + if (jp.getDefaultContentType() != null) { + defaultContentTypeMatch = + selectProperty(defaultContentTypeMatch, jpg); + } + if (jp.getBuffer() != null) { + bufferMatch = selectProperty(bufferMatch, jpg); + } + if (jp.isErrorOnUndeclaredNamespace() != null) { + errorOnUndeclaredNamespaceMatch = + selectProperty(errorOnUndeclaredNamespaceMatch, jpg); + } + } + + + String isXml = defaultIsXml; + String isELIgnored = defaultIsELIgnored; + String errorOnELNotFound = defaultErrorOnELNotFound; + String isScriptingInvalid = defaultIsScriptingInvalid; + String pageEncoding = null; + String isDeferedSyntaxAllowedAsLiteral = defaultDeferedSyntaxAllowedAsLiteral; + String isTrimDirectiveWhitespaces = defaultTrimDirectiveWhitespaces; + String defaultContentType = defaultDefaultContentType; + String buffer = defaultBuffer; + String errorOnUndeclaredNamespace = defaultErrorOnUndeclaredNamespace; + + if (isXmlMatch != null) { + isXml = isXmlMatch.getJspProperty().isXml(); + } + if (errorOnELNotFoundMatch != null) { + errorOnELNotFound = errorOnELNotFoundMatch.getJspProperty().getErrorOnELNotFound(); + } + if (elIgnoredMatch != null) { + isELIgnored = elIgnoredMatch.getJspProperty().isELIgnored(); + } + if (scriptingInvalidMatch != null) { + isScriptingInvalid = + scriptingInvalidMatch.getJspProperty().isScriptingInvalid(); + } + if (pageEncodingMatch != null) { + pageEncoding = pageEncodingMatch.getJspProperty().getPageEncoding(); + } + if (deferedSyntaxAllowedAsLiteralMatch != null) { + isDeferedSyntaxAllowedAsLiteral = + deferedSyntaxAllowedAsLiteralMatch.getJspProperty().isDeferedSyntaxAllowedAsLiteral(); + } + if (trimDirectiveWhitespacesMatch != null) { + isTrimDirectiveWhitespaces = + trimDirectiveWhitespacesMatch.getJspProperty().isTrimDirectiveWhitespaces(); + } + if (defaultContentTypeMatch != null) { + defaultContentType = + defaultContentTypeMatch.getJspProperty().getDefaultContentType(); + } + if (bufferMatch != null) { + buffer = bufferMatch.getJspProperty().getBuffer(); + } + if (errorOnUndeclaredNamespaceMatch != null) { + errorOnUndeclaredNamespace = + errorOnUndeclaredNamespaceMatch.getJspProperty().isErrorOnUndeclaredNamespace(); + } + + return new JspProperty(isXml, isELIgnored, errorOnELNotFound, isScriptingInvalid, + pageEncoding, includePreludes, includeCodas, + isDeferedSyntaxAllowedAsLiteral, isTrimDirectiveWhitespaces, + defaultContentType, buffer, errorOnUndeclaredNamespace); + } + + /** + * To find out if a uri matches a url pattern in jsp config. If so, + * then the uri is a JSP page. This is used primarily for jspc. + * @param uri The path to check + * @return true if the path denotes a JSP page + */ + public boolean isJspPage(String uri) { + + init(); + if (jspProperties == null) { + return false; + } + + String uriPath = null; + int index = uri.lastIndexOf('/'); + if (index >=0 ) { + uriPath = uri.substring(0, index+1); + } + String uriExtension = null; + index = uri.lastIndexOf('.'); + if (index >=0) { + uriExtension = uri.substring(index+1); + } + + for (JspPropertyGroup jpg : jspProperties) { + + String extension = jpg.getExtension(); + String path = jpg.getPath(); + + if (extension == null) { + if (uri.equals(path)) { + // There is an exact match + return true; + } + } else { + if ((path == null || path.equals(uriPath)) && + (extension.equals("*") || extension.equals(uriExtension))) { + // Matches *, *.ext, /p/*, or /p/*.ext + return true; + } + } + } + return false; + } + + public static class JspPropertyGroup { + private final String path; + private final String extension; + private final JspProperty jspProperty; + + JspPropertyGroup(String path, String extension, + JspProperty jspProperty) { + this.path = path; + this.extension = extension; + this.jspProperty = jspProperty; + } + + public String getPath() { + return path; + } + + public String getExtension() { + return extension; + } + + public JspProperty getJspProperty() { + return jspProperty; + } + } + + public static class JspProperty { + + private final String isXml; + private final String elIgnored; + private final String errorOnELNotFound; + private final String scriptingInvalid; + private final String pageEncoding; + private final Collection includePrelude; + private final Collection includeCoda; + private final String deferedSyntaxAllowedAsLiteral; + private final String trimDirectiveWhitespaces; + private final String defaultContentType; + private final String buffer; + private final String errorOnUndeclaredNamespace; + + public JspProperty(String isXml, String elIgnored, String errorOnELNotFound, + String scriptingInvalid, String pageEncoding, + Collection includePrelude, Collection includeCoda, + String deferedSyntaxAllowedAsLiteral, + String trimDirectiveWhitespaces, + String defaultContentType, + String buffer, + String errorOnUndeclaredNamespace) { + + this.isXml = isXml; + this.elIgnored = elIgnored; + this.errorOnELNotFound = errorOnELNotFound; + this.scriptingInvalid = scriptingInvalid; + this.pageEncoding = pageEncoding; + this.includePrelude = includePrelude; + this.includeCoda = includeCoda; + this.deferedSyntaxAllowedAsLiteral = deferedSyntaxAllowedAsLiteral; + this.trimDirectiveWhitespaces = trimDirectiveWhitespaces; + this.defaultContentType = defaultContentType; + this.buffer = buffer; + this.errorOnUndeclaredNamespace = errorOnUndeclaredNamespace; + } + + public String isXml() { + return isXml; + } + + public String isELIgnored() { + return elIgnored; + } + + public String getErrorOnELNotFound() { + return errorOnELNotFound; + } + + public String isScriptingInvalid() { + return scriptingInvalid; + } + + public String getPageEncoding() { + return pageEncoding; + } + + public Collection getIncludePrelude() { + return includePrelude; + } + + public Collection getIncludeCoda() { + return includeCoda; + } + + public String isDeferedSyntaxAllowedAsLiteral() { + return deferedSyntaxAllowedAsLiteral; + } + + public String isTrimDirectiveWhitespaces() { + return trimDirectiveWhitespaces; + } + + public String getDefaultContentType() { + return defaultContentType; + } + + public String getBuffer() { + return buffer; + } + + public String isErrorOnUndeclaredNamespace() { + return errorOnUndeclaredNamespace; + } + } +} diff --git a/java/org/apache/jasper/compiler/JspDocumentParser.java b/java/org/apache/jasper/compiler/JspDocumentParser.java new file mode 100644 index 0000000..085ba69 --- /dev/null +++ b/java/org/apache/jasper/compiler/JspDocumentParser.java @@ -0,0 +1,1550 @@ +/* + * 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.jasper.compiler; + +import java.io.CharArrayWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.AccessController; +import java.util.Collection; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import jakarta.servlet.jsp.tagext.TagFileInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; + +import org.apache.jasper.Constants; +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.descriptor.DigesterFactory; +import org.apache.tomcat.util.descriptor.LocalResolver; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; +import org.apache.tomcat.util.security.PrivilegedGetTccl; +import org.apache.tomcat.util.security.PrivilegedSetTccl; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; +import org.xml.sax.ext.DefaultHandler2; +import org.xml.sax.ext.EntityResolver2; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Class implementing a parser for a JSP document, that is, a JSP page in XML + * syntax. + * + * @author Jan Luehe + * @author Kin-man Chung + */ + +class JspDocumentParser + extends DefaultHandler2 + implements TagConstants { + + private static final String LEXICAL_HANDLER_PROPERTY = + "http://xml.org/sax/properties/lexical-handler"; + private static final String JSP_URI = "http://java.sun.com/JSP/Page"; + + private final ParserController parserController; + private final JspCompilationContext ctxt; + private final PageInfo pageInfo; + private final String path; + private StringBuilder charBuffer; + + // Node representing the XML element currently being parsed + private Node current; + + /* + * Outermost (in the nesting hierarchy) node whose body is declared to be + * scriptless. If a node's body is declared to be scriptless, all its + * nested nodes must be scriptless, too. + */ + private Node scriptlessBodyNode; + + private Locator locator; + + //Mark representing the start of the current element. Note + //that locator.getLineNumber() and locator.getColumnNumber() + //return the line and column numbers for the character + //immediately _following_ the current element. The underlying + //XMl parser eats white space that is not part of character + //data, so for Nodes that are not created from character data, + //this is the best we can do. But when we parse character data, + //we get an accurate starting location by starting with startMark + //as set by the previous element, and updating it as we advance + //through the characters. + private Mark startMark; + + // Flag indicating whether we are inside DTD declarations + private boolean inDTD; + + private boolean isValidating; + private final EntityResolver2 entityResolver; + + private final ErrorDispatcher err; + private final boolean isTagFile; + private final boolean directivesOnly; + private boolean isTop; + + // Nesting level of Tag dependent bodies + private int tagDependentNesting = 0; + // Flag set to delay incrementing tagDependentNesting until jsp:body + // is first encountered + private boolean tagDependentPending = false; + + /* + * Constructor + */ + JspDocumentParser( + ParserController pc, + String path, + boolean isTagFile, + boolean directivesOnly) { + this.parserController = pc; + this.ctxt = pc.getJspCompilationContext(); + this.pageInfo = pc.getCompiler().getPageInfo(); + this.err = pc.getCompiler().getErrorDispatcher(); + this.path = path; + this.isTagFile = isTagFile; + this.directivesOnly = directivesOnly; + this.isTop = true; + + String blockExternalString = ctxt.getServletContext().getInitParameter( + Constants.XML_BLOCK_EXTERNAL_INIT_PARAM); + boolean blockExternal; + if (blockExternalString == null) { + blockExternal = true; + } else { + blockExternal = Boolean.parseBoolean(blockExternalString); + } + + this.entityResolver = new LocalResolver( + DigesterFactory.SERVLET_API_PUBLIC_IDS, + DigesterFactory.SERVLET_API_SYSTEM_IDS, + blockExternal); + } + + /* + * Parses a JSP document by responding to SAX events. + * + * @throws JasperException + */ + public static Node.Nodes parse( + ParserController pc, + String path, + Jar jar, + Node parent, + boolean isTagFile, + boolean directivesOnly, + String pageEnc, + String jspConfigPageEnc, + boolean isEncodingSpecifiedInProlog, + boolean isBomPresent) + throws JasperException { + + JspDocumentParser jspDocParser = + new JspDocumentParser(pc, path, isTagFile, directivesOnly); + Node.Nodes pageNodes = null; + + try { + + // Create dummy root and initialize it with given page encodings + Node.Root dummyRoot = new Node.Root(null, parent, true, + pc.getJspCompilationContext().getOptions().getTempVariableNamePrefix()); + dummyRoot.setPageEncoding(pageEnc); + dummyRoot.setJspConfigPageEncoding(jspConfigPageEnc); + dummyRoot.setIsEncodingSpecifiedInProlog( + isEncodingSpecifiedInProlog); + dummyRoot.setIsBomPresent(isBomPresent); + jspDocParser.current = dummyRoot; + if (parent == null) { + jspDocParser.addInclude( + dummyRoot, + jspDocParser.pageInfo.getIncludePrelude()); + } else { + jspDocParser.isTop = false; + } + + jspDocParser.isValidating = false; + + // Parse the input + SAXParser saxParser = getSAXParser(false, jspDocParser); + InputSource source = JspUtil.getInputSource(path, jar, jspDocParser.ctxt); + try { + saxParser.parse(source, jspDocParser); + } catch (EnableDTDValidationException e) { + saxParser = getSAXParser(true, jspDocParser); + jspDocParser.isValidating = true; + try { + source.getByteStream().close(); + } catch (IOException e2) { + // ignore + } + source = JspUtil.getInputSource(path, jar, jspDocParser.ctxt); + saxParser.parse(source, jspDocParser); + } finally { + try { + source.getByteStream().close(); + } catch (IOException e) { + // ignore + } + } + + if (parent == null) { + jspDocParser.addInclude( + dummyRoot, + jspDocParser.pageInfo.getIncludeCoda()); + } + + // Create Node.Nodes from dummy root + pageNodes = new Node.Nodes(dummyRoot); + + } catch (IOException ioe) { + jspDocParser.err.jspError(ioe, "jsp.error.data.file.read", path); + } catch (SAXParseException e) { + jspDocParser.err.jspError + (new Mark(jspDocParser.ctxt, path, e.getLineNumber(), + e.getColumnNumber()), + e, e.getMessage()); + } catch (Exception e) { + jspDocParser.err.jspError(e, "jsp.error.data.file.processing", path); + } + + return pageNodes; + } + + /* + * Processes the given list of included files. + * + * This is used to implement the include-prelude and include-coda + * subelements of the jsp-config element in web.xml + */ + private void addInclude(Node parent, Collection files) throws SAXException { + if (files != null) { + for (String file : files) { + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "file", "file", "CDATA", file); + + // Create a dummy Include directive node + Node includeDir = + new Node.IncludeDirective(attrs, null, // XXX + parent); + processIncludeDirective(file, includeDir); + } + } + } + + + @Override + public InputSource getExternalSubset(String name, String baseURI) + throws SAXException, IOException { + return entityResolver.getExternalSubset(name, baseURI); + } + + @Override + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + return entityResolver.resolveEntity(publicId, systemId); + } + + @Override + public InputSource resolveEntity(String name, String publicId, + String baseURI, String systemId) throws SAXException, IOException { + // TODO URLs returned by the Jar abstraction may be of the form jar:jar: + // which is not a URL that can be resolved by the JRE. This should + // use the JarFactory to construct and return a valid InputSource. + return entityResolver.resolveEntity(name, publicId, baseURI, systemId); + } + + /* + * Receives notification of the start of an element. + * + * This method assigns the given tag attributes to one of 3 buckets: + * + * - "xmlns" attributes that represent (standard or custom) tag libraries. + * - "xmlns" attributes that do not represent tag libraries. + * - all remaining attributes. + * + * For each "xmlns" attribute that represents a custom tag library, the + * corresponding TagLibraryInfo object is added to the set of custom + * tag libraries. + */ + @Override + public void startElement( + String uri, + String localName, + String qName, + Attributes attrs) + throws SAXException { + + AttributesImpl taglibAttrs = null; + AttributesImpl nonTaglibAttrs = null; + AttributesImpl nonTaglibXmlnsAttrs = null; + + processChars(); + + checkPrefixes(uri, qName, attrs); + + if (directivesOnly && + !(JSP_URI.equals(uri) && localName.startsWith(DIRECTIVE_ACTION))) { + return; + } + + // jsp:text must not have any subelements + if (current instanceof Node.JspText) { + throw new SAXParseException( + Localizer.getMessage("jsp.error.text.has_subelement"), + locator); + } + + startMark = new Mark(ctxt, path, locator.getLineNumber(), + locator.getColumnNumber()); + + /* + * Notice that due to a bug in the underlying SAX parser, the + * attributes must be enumerated in descending order. + */ + boolean isTaglib = false; + for (int i = attrs.getLength() - 1; i >= 0; i--) { + isTaglib = false; + String attrQName = attrs.getQName(i); + if (!attrQName.startsWith("xmlns")) { + if (nonTaglibAttrs == null) { + nonTaglibAttrs = new AttributesImpl(); + } + nonTaglibAttrs.addAttribute( + attrs.getURI(i), + attrs.getLocalName(i), + attrs.getQName(i), + attrs.getType(i), + attrs.getValue(i)); + } else { + if (attrQName.startsWith("xmlns:jsp")) { + isTaglib = true; + } else { + String attrUri = attrs.getValue(i); + // TaglibInfo for this uri already established in + // startPrefixMapping + isTaglib = pageInfo.hasTaglib(attrUri); + } + if (isTaglib) { + if (taglibAttrs == null) { + taglibAttrs = new AttributesImpl(); + } + taglibAttrs.addAttribute( + attrs.getURI(i), + attrs.getLocalName(i), + attrs.getQName(i), + attrs.getType(i), + attrs.getValue(i)); + } else { + if (nonTaglibXmlnsAttrs == null) { + nonTaglibXmlnsAttrs = new AttributesImpl(); + } + nonTaglibXmlnsAttrs.addAttribute( + attrs.getURI(i), + attrs.getLocalName(i), + attrs.getQName(i), + attrs.getType(i), + attrs.getValue(i)); + } + } + } + + Node node = null; + + if (tagDependentPending && JSP_URI.equals(uri) && + localName.equals(BODY_ACTION)) { + tagDependentPending = false; + tagDependentNesting++; + current = + parseStandardAction( + qName, + localName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + startMark); + return; + } + + if (tagDependentPending && JSP_URI.equals(uri) && + localName.equals(ATTRIBUTE_ACTION)) { + current = + parseStandardAction( + qName, + localName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + startMark); + return; + } + + if (tagDependentPending) { + tagDependentPending = false; + tagDependentNesting++; + } + + if (tagDependentNesting > 0) { + node = + new Node.UninterpretedTag( + qName, + localName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + startMark, + current); + } else if (JSP_URI.equals(uri)) { + node = + parseStandardAction( + qName, + localName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + startMark); + } else { + node = + parseCustomAction( + qName, + localName, + uri, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + startMark, + current); + if (node == null) { + node = + new Node.UninterpretedTag( + qName, + localName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + startMark, + current); + } else { + // custom action + String bodyType = getBodyType((Node.CustomTag) node); + + if (scriptlessBodyNode == null + && bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_SCRIPTLESS)) { + scriptlessBodyNode = node; + } + else if (TagInfo.BODY_CONTENT_TAG_DEPENDENT.equalsIgnoreCase(bodyType)) { + tagDependentPending = true; + } + } + } + + current = node; + } + + /* + * Receives notification of character data inside an element. + * + * The SAX does not call this method with all of the template text, but may + * invoke this method with chunks of it. This is a problem when we try + * to determine if the text contains only whitespaces, or when we are + * looking for an EL expression string. Therefore it is necessary to + * buffer and concatenate the chunks and process the concatenated text + * later (at beginTag and endTag) + * + * @param buf The characters + * @param offset The start position in the character array + * @param len The number of characters to use from the character array + * + * @throws SAXException + */ + @Override + public void characters(char[] buf, int offset, int len) { + + if (charBuffer == null) { + charBuffer = new StringBuilder(); + } + charBuffer.append(buf, offset, len); + } + + private void processChars() throws SAXException { + + if (charBuffer == null || directivesOnly) { + return; + } + + /* + * JSP.6.1.1: All textual nodes that have only white space are to be + * dropped from the document, except for nodes in a jsp:text element, + * and any leading and trailing white-space-only textual nodes in a + * jsp:attribute whose 'trim' attribute is set to FALSE, which are to + * be kept verbatim. + * JSP.6.2.3 defines white space characters. + */ + boolean isAllSpace = true; + if (!(current instanceof Node.JspText) + && !(current instanceof Node.NamedAttribute)) { + for (int i = 0; i < charBuffer.length(); i++) { + char ch = charBuffer.charAt(i); + if (!(ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t')) { + isAllSpace = false; + break; + } + } + } + + if (!isAllSpace && tagDependentPending) { + tagDependentPending = false; + tagDependentNesting++; + } + + if (tagDependentNesting > 0 || pageInfo.isELIgnored() || + current instanceof Node.ScriptingElement) { + if (charBuffer.length() > 0) { + @SuppressWarnings("unused") + Node unused = new Node.TemplateText( + charBuffer.toString(), startMark, current); + } + startMark = new Mark(ctxt, path, locator.getLineNumber(), + locator.getColumnNumber()); + charBuffer = null; + return; + } + + if ((current instanceof Node.JspText) + || (current instanceof Node.NamedAttribute) + || !isAllSpace) { + + int line = startMark.getLineNumber(); + int column = startMark.getColumnNumber(); + + CharArrayWriter ttext = new CharArrayWriter(); + int lastCh = 0, elType = 0; + for (int i = 0; i < charBuffer.length(); i++) { + + int ch = charBuffer.charAt(i); + if (ch == '\n') { + column = 1; + line++; + } else { + column++; + } + if ((lastCh == '$' || lastCh == '#') && ch == '{') { + elType = lastCh; + if (ttext.size() > 0) { + @SuppressWarnings("unused") + Node unused = new Node.TemplateText( + ttext.toString(), startMark, current); + ttext.reset(); + //We subtract two from the column number to + //account for the '[$,#]{' that we've already parsed + startMark = new Mark(ctxt, path, line, column - 2); + } + // following "${" || "#{" to first unquoted "}" + i++; + boolean singleQ = false; + boolean doubleQ = false; + lastCh = 0; + for (;; i++) { + if (i >= charBuffer.length()) { + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.unterminated", + (char) elType + "{"), + locator); + + } + ch = charBuffer.charAt(i); + if (ch == '\n') { + column = 1; + line++; + } else { + column++; + } + if (lastCh == '\\' && (singleQ || doubleQ)) { + ttext.write(ch); + lastCh = 0; + continue; + } + if (ch == '}') { + @SuppressWarnings("unused") + Node unused = new Node.ELExpression( + (char) elType, ttext.toString(), + startMark, current); + ttext.reset(); + startMark = new Mark(ctxt, path, line, column); + break; + } + if (ch == '"') { + doubleQ = !doubleQ; + } else if (ch == '\'') { + singleQ = !singleQ; + } + + ttext.write(ch); + lastCh = ch; + } + } else if (lastCh == '\\' && (ch == '$' || ch == '#')) { + if (pageInfo.isELIgnored()) { + ttext.write('\\'); + } + ttext.write(ch); + ch = 0; // Not start of EL anymore + } else { + if (lastCh == '$' || lastCh == '#' || lastCh == '\\') { + ttext.write(lastCh); + } + if (ch != '$' && ch != '#' && ch != '\\') { + ttext.write(ch); + } + } + lastCh = ch; + } + if (lastCh == '$' || lastCh == '#' || lastCh == '\\') { + ttext.write(lastCh); + } + if (ttext.size() > 0) { + @SuppressWarnings("unused") + Node unused = new Node.TemplateText( + ttext.toString(), startMark, current); + } + } + startMark = new Mark(ctxt, path, locator.getLineNumber(), + locator.getColumnNumber()); + + charBuffer = null; + } + + /* + * Receives notification of the end of an element. + */ + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + + processChars(); + + if (directivesOnly && + !(JSP_URI.equals(uri) && localName.startsWith(DIRECTIVE_ACTION))) { + return; + } + + if (current instanceof Node.NamedAttribute) { + boolean isTrim = ((Node.NamedAttribute)current).isTrim(); + Node.Nodes subElems = current.getBody(); + for (int i = 0; subElems != null && i < subElems.size(); i++) { + Node subElem = subElems.getNode(i); + if (!(subElem instanceof Node.TemplateText)) { + continue; + } + // Ignore any whitespace (including spaces, carriage returns, + // line feeds, and tabs, that appear at the beginning and at + // the end of the body of the action, if the + // action's 'trim' attribute is set to TRUE (default). + // In addition, any textual nodes in the that + // have only white space are dropped from the document, with + // the exception of leading and trailing white-space-only + // textual nodes in a whose 'trim' attribute + // is set to FALSE, which must be kept verbatim. + if (i == 0) { + if (isTrim) { + ((Node.TemplateText)subElem).ltrim(); + } + } else if (i == subElems.size() - 1) { + if (isTrim) { + ((Node.TemplateText)subElem).rtrim(); + } + } else { + if (((Node.TemplateText)subElem).isAllSpace()) { + subElems.remove(subElem); + } + } + } + } else if (current instanceof Node.ScriptingElement) { + checkScriptingBody((Node.ScriptingElement)current); + } + + if ( isTagDependent(current)) { + tagDependentNesting--; + } + + if (scriptlessBodyNode != null + && current.equals(scriptlessBodyNode)) { + scriptlessBodyNode = null; + } + + if (current instanceof Node.CustomTag) { + String bodyType = getBodyType((Node.CustomTag) current); + if (TagInfo.BODY_CONTENT_EMPTY.equalsIgnoreCase(bodyType)) { + // Children - if any - must be JSP attributes + Node.Nodes children = current.getBody(); + if (children != null && children.size() > 0) { + for (int i = 0; i < children.size(); i++) { + Node child = children.getNode(i); + if (!(child instanceof Node.NamedAttribute)) { + throw new SAXParseException(Localizer.getMessage( + "jasper.error.emptybodycontent.nonempty", + current.qName), locator); + } + } + } + } + } + if (current.getParent() != null) { + current = current.getParent(); + } + } + + /* + * Receives the document locator. + * + * @param locator the document locator + */ + @Override + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + + /* + * See org.xml.sax.ext.LexicalHandler. + */ + @Override + public void comment(char[] buf, int offset, int len) throws SAXException { + + processChars(); // Flush char buffer and remove white spaces + + // ignore comments in the DTD + if (!inDTD) { + startMark = new Mark(ctxt, path, locator.getLineNumber(), + locator.getColumnNumber()); + @SuppressWarnings("unused") + Node unused = new Node.Comment(new String(buf, offset, len), startMark, current); + } + } + + /* + * See org.xml.sax.ext.LexicalHandler. + */ + @Override + public void startCDATA() throws SAXException { + + processChars(); // Flush char buffer and remove white spaces + startMark = new Mark(ctxt, path, locator.getLineNumber(), + locator.getColumnNumber()); + } + + /* + * See org.xml.sax.ext.LexicalHandler. + */ + @Override + public void endCDATA() throws SAXException { + processChars(); // Flush char buffer and remove white spaces + } + + /* + * See org.xml.sax.ext.LexicalHandler. + */ + @Override + public void startEntity(String name) throws SAXException { + // do nothing + } + + /* + * See org.xml.sax.ext.LexicalHandler. + */ + @Override + public void endEntity(String name) throws SAXException { + // do nothing + } + + /* + * See org.xml.sax.ext.LexicalHandler. + */ + @Override + public void startDTD(String name, String publicId, String systemId) + throws SAXException { + if (!isValidating) { + fatalError(new EnableDTDValidationException( + "jsp.error.enable_dtd_validation", null)); + } + + inDTD = true; + } + + /* + * See org.xml.sax.ext.LexicalHandler. + */ + @Override + public void endDTD() throws SAXException { + inDTD = false; + } + + /* + * Receives notification of a non-recoverable error. + */ + @Override + public void fatalError(SAXParseException e) throws SAXException { + throw e; + } + + /* + * Receives notification of a recoverable error. + */ + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + + /* + * Receives notification of the start of a Namespace mapping. + */ + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + TagLibraryInfo taglibInfo; + + if (directivesOnly && !(JSP_URI.equals(uri))) { + return; + } + + try { + taglibInfo = getTaglibInfo(prefix, uri); + } catch (JasperException je) { + throw new SAXParseException( + Localizer.getMessage("jsp.error.could.not.add.taglibraries"), + locator, + je); + } + + if (taglibInfo != null) { + if (pageInfo.getTaglib(uri) == null) { + pageInfo.addTaglib(uri, taglibInfo); + } + pageInfo.pushPrefixMapping(prefix, uri); + } else { + pageInfo.pushPrefixMapping(prefix, null); + } + } + + /* + * Receives notification of the end of a Namespace mapping. + */ + @Override + public void endPrefixMapping(String prefix) throws SAXException { + + if (directivesOnly) { + String uri = pageInfo.getURI(prefix); + if (!JSP_URI.equals(uri)) { + return; + } + } + + pageInfo.popPrefixMapping(prefix); + } + + //********************************************************************* + // Private utility methods + + private Node parseStandardAction( + String qName, + String localName, + Attributes nonTaglibAttrs, + Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, + Mark start) + throws SAXException { + + Node node = null; + + if (localName.equals(ROOT_ACTION)) { + if (!(current instanceof Node.Root)) { + throw new SAXParseException( + Localizer.getMessage("jsp.error.nested_jsproot"), + locator); + } + node = + new Node.JspRoot( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + if (isTop) { + pageInfo.setHasJspRoot(true); + } + } else if (localName.equals(PAGE_DIRECTIVE_ACTION)) { + if (isTagFile) { + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.action.istagfile", + localName), + locator); + } + node = + new Node.PageDirective( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + String imports = nonTaglibAttrs.getValue("import"); + // There can only be one 'import' attribute per page directive + if (imports != null) { + ((Node.PageDirective)node).addImport(imports); + } + } else if (localName.equals(INCLUDE_DIRECTIVE_ACTION)) { + node = + new Node.IncludeDirective( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + processIncludeDirective(nonTaglibAttrs.getValue("file"), node); + } else if (localName.equals(DECLARATION_ACTION)) { + if (scriptlessBodyNode != null) { + // We're nested inside a node whose body is + // declared to be scriptless + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.no.scriptlets", + localName), + locator); + } + node = + new Node.Declaration( + qName, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(SCRIPTLET_ACTION)) { + if (scriptlessBodyNode != null) { + // We're nested inside a node whose body is + // declared to be scriptless + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.no.scriptlets", + localName), + locator); + } + node = + new Node.Scriptlet( + qName, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(EXPRESSION_ACTION)) { + if (scriptlessBodyNode != null) { + // We're nested inside a node whose body is + // declared to be scriptless + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.no.scriptlets", + localName), + locator); + } + node = + new Node.Expression( + qName, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(USE_BEAN_ACTION)) { + node = + new Node.UseBean( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(SET_PROPERTY_ACTION)) { + node = + new Node.SetProperty( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(GET_PROPERTY_ACTION)) { + node = + new Node.GetProperty( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(INCLUDE_ACTION)) { + node = + new Node.IncludeAction( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(FORWARD_ACTION)) { + node = + new Node.ForwardAction( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(PARAM_ACTION)) { + node = + new Node.ParamAction( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(PARAMS_ACTION)) { + node = + new Node.ParamsAction( + qName, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(PLUGIN_ACTION)) { + node = + new Node.PlugIn( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(TEXT_ACTION)) { + node = + new Node.JspText( + qName, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(BODY_ACTION)) { + node = + new Node.JspBody( + qName, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(ATTRIBUTE_ACTION)) { + node = + new Node.NamedAttribute( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(OUTPUT_ACTION)) { + node = + new Node.JspOutput( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(TAG_DIRECTIVE_ACTION)) { + if (!isTagFile) { + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.action.isnottagfile", + localName), + locator); + } + node = + new Node.TagDirective( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + String imports = nonTaglibAttrs.getValue("import"); + // There can only be one 'import' attribute per tag directive + if (imports != null) { + ((Node.TagDirective)node).addImport(imports); + } + } else if (localName.equals(ATTRIBUTE_DIRECTIVE_ACTION)) { + if (!isTagFile) { + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.action.isnottagfile", + localName), + locator); + } + node = + new Node.AttributeDirective( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(VARIABLE_DIRECTIVE_ACTION)) { + if (!isTagFile) { + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.action.isnottagfile", + localName), + locator); + } + node = + new Node.VariableDirective( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(INVOKE_ACTION)) { + if (!isTagFile) { + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.action.isnottagfile", + localName), + locator); + } + node = + new Node.InvokeAction( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(DOBODY_ACTION)) { + if (!isTagFile) { + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.action.isnottagfile", + localName), + locator); + } + node = + new Node.DoBodyAction( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(ELEMENT_ACTION)) { + node = + new Node.JspElement( + qName, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else if (localName.equals(FALLBACK_ACTION)) { + node = + new Node.FallBackAction( + qName, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + current); + } else { + throw new SAXParseException( + Localizer.getMessage( + "jsp.error.xml.badStandardAction", + localName), + locator); + } + + return node; + } + + /* + * Checks if the XML element with the given tag name is a custom action, + * and returns the corresponding Node object. + */ + private Node parseCustomAction( + String qName, + String localName, + String uri, + Attributes nonTaglibAttrs, + Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, + Mark start, + Node parent) + throws SAXException { + + // Check if this is a user-defined (custom) tag + TagLibraryInfo tagLibInfo = pageInfo.getTaglib(uri); + if (tagLibInfo == null) { + return null; + } + + TagInfo tagInfo = tagLibInfo.getTag(localName); + TagFileInfo tagFileInfo = tagLibInfo.getTagFile(localName); + if (tagInfo == null && tagFileInfo == null) { + throw new SAXParseException( + Localizer.getMessage("jsp.error.xml.bad_tag", localName, uri), + locator); + } + Class tagHandlerClass = null; + if (tagInfo != null) { + String handlerClassName = tagInfo.getTagClassName(); + try { + tagHandlerClass = + ctxt.getClassLoader().loadClass(handlerClassName); + } catch (Exception e) { + throw new SAXParseException( + Localizer.getMessage("jsp.error.loadclass.taghandler", + handlerClassName, + qName), + locator, e); + } + } + + String prefix = getPrefix(qName); + + Node.CustomTag ret = null; + if (tagInfo != null) { + ret = + new Node.CustomTag( + qName, + prefix, + localName, + uri, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + parent, + tagInfo, + tagHandlerClass); + } else { + ret = + new Node.CustomTag( + qName, + prefix, + localName, + uri, + nonTaglibAttrs, + nonTaglibXmlnsAttrs, + taglibAttrs, + start, + parent, + tagFileInfo); + } + + return ret; + } + + /* + * Creates the tag library associated with the given uri namespace, and + * returns it. + * + * @param prefix The prefix of the xmlns attribute + * @param uri The uri namespace (value of the xmlns attribute) + * + * @return The tag library associated with the given uri namespace + */ + private TagLibraryInfo getTaglibInfo(String prefix, String uri) + throws JasperException { + + TagLibraryInfo result = null; + + if (uri.startsWith(URN_JSPTAGDIR)) { + // uri (of the form "urn:jsptagdir:path") references tag file dir + String tagdir = uri.substring(URN_JSPTAGDIR.length()); + result = + new ImplicitTagLibraryInfo( + ctxt, + parserController, + pageInfo, + prefix, + tagdir, + err); + } else { + // uri references TLD file + boolean isPlainUri = false; + if (uri.startsWith(URN_JSPTLD)) { + // uri is of the form "urn:jsptld:path" + uri = uri.substring(URN_JSPTLD.length()); + } else { + isPlainUri = true; + } + + TldResourcePath tldResourcePath = ctxt.getTldResourcePath(uri); + if (tldResourcePath != null || !isPlainUri) { + if (ctxt.getOptions().isCaching()) { + result = ctxt.getOptions().getCache().get(uri); + } + if (result == null) { + /* + * If the uri value is a plain uri, a translation error must + * not be generated if the uri is not found in the taglib map. + * Instead, any actions in the namespace defined by the uri + * value must be treated as uninterpreted. + */ + result = + new TagLibraryInfoImpl( + ctxt, + parserController, + pageInfo, + prefix, + uri, + tldResourcePath, + err); + if (ctxt.getOptions().isCaching()) { + ctxt.getOptions().getCache().put(uri, result); + } + } + } + } + + return result; + } + + /* + * Ensures that the given body only contains nodes that are instances of + * TemplateText. + * + * This check is performed only for the body of a scripting (that is: + * declaration, scriptlet, or expression) element, after the end tag of a + * scripting element has been reached. + */ + private void checkScriptingBody(Node.ScriptingElement scriptingElem) + throws SAXException { + Node.Nodes body = scriptingElem.getBody(); + if (body != null) { + int size = body.size(); + for (int i = 0; i < size; i++) { + Node n = body.getNode(i); + if (!(n instanceof Node.TemplateText)) { + String elemType = SCRIPTLET_ACTION; + if (scriptingElem instanceof Node.Declaration) { + elemType = DECLARATION_ACTION; + } + if (scriptingElem instanceof Node.Expression) { + elemType = EXPRESSION_ACTION; + } + String msg = + Localizer.getMessage( + "jsp.error.parse.xml.scripting.invalid.body", + elemType); + throw new SAXParseException(msg, locator); + } + } + } + } + + /* + * Parses the given file included via an include directive. + * + * @param fname The path to the included resource, as specified by the + * 'file' attribute of the include directive + * @param parent The Node representing the include directive + */ + private void processIncludeDirective(String fname, Node parent) + throws SAXException { + + if (fname == null) { + return; + } + + try { + parserController.parse(fname, parent, null); + } catch (FileNotFoundException fnfe) { + throw new SAXParseException( + Localizer.getMessage("jsp.error.file.not.found", fname), + locator, + fnfe); + } catch (Exception e) { + throw new SAXParseException(e.getMessage(), locator, e); + } + } + + /* + * Checks an element's given URI, qname, and attributes to see if any + * of them hijack the 'jsp' prefix, that is, bind it to a namespace other + * than http://java.sun.com/JSP/Page. + * + * @param uri The element's URI + * @param qName The element's qname + * @param attrs The element's attributes + */ + private void checkPrefixes(String uri, String qName, Attributes attrs) { + + checkPrefix(uri, qName); + + int len = attrs.getLength(); + for (int i = 0; i < len; i++) { + checkPrefix(attrs.getURI(i), attrs.getQName(i)); + } + } + + /* + * Checks the given URI and qname to see if they hijack the 'jsp' prefix, + * which would be the case if qName contained the 'jsp' prefix and + * uri was different from http://java.sun.com/JSP/Page. + * + * @param uri The URI to check + * @param qName The qname to check + */ + private void checkPrefix(String uri, String qName) { + + String prefix = getPrefix(qName); + if (prefix.length() > 0) { + pageInfo.addPrefix(prefix); + if ("jsp".equals(prefix) && !JSP_URI.equals(uri)) { + pageInfo.setIsJspPrefixHijacked(true); + } + } + } + + private String getPrefix(String qName) { + int index = qName.indexOf(':'); + if (index != -1) { + return qName.substring(0, index); + } + return ""; + } + + /* + * Gets SAXParser. + * + * @param validating Indicates whether the requested SAXParser should + * be validating + * @param jspDocParser The JSP document parser + * + * @return The SAXParser + */ + private static SAXParser getSAXParser( + boolean validating, + JspDocumentParser jspDocParser) + throws Exception { + + ClassLoader original; + Thread currentThread = Thread.currentThread(); + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedGetTccl pa = new PrivilegedGetTccl(currentThread); + original = AccessController.doPrivileged(pa); + } else { + original = currentThread.getContextClassLoader(); + } + try { + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, JspDocumentParser.class.getClassLoader()); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(JspDocumentParser.class.getClassLoader()); + } + + SAXParserFactory factory = SAXParserFactory.newInstance(); + + factory.setNamespaceAware(true); + // Preserve xmlns attributes + factory.setFeature( + "http://xml.org/sax/features/namespace-prefixes", + true); + + factory.setValidating(validating); + if (validating) { + // Enable DTD validation + factory.setFeature( + "http://xml.org/sax/features/validation", + true); + // Enable schema validation + factory.setFeature( + "http://apache.org/xml/features/validation/schema", + true); + } + + // Configure the parser + SAXParser saxParser = factory.newSAXParser(); + XMLReader xmlReader = saxParser.getXMLReader(); + xmlReader.setProperty(LEXICAL_HANDLER_PROPERTY, jspDocParser); + xmlReader.setErrorHandler(jspDocParser); + + return saxParser; + } finally { + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, original); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(original); + } + } + } + + /* + * Exception indicating that a DOCTYPE declaration is present, but + * validation is turned off. + */ + private static class EnableDTDValidationException + extends SAXParseException { + + private static final long serialVersionUID = 1L; + + EnableDTDValidationException(String message, Locator loc) { + super(message, loc); + } + + @Override + public synchronized Throwable fillInStackTrace() { + // This class does not provide a stack trace + return this; + } + } + + private static String getBodyType(Node.CustomTag custom) { + + if (custom.getTagInfo() != null) { + return custom.getTagInfo().getBodyContent(); + } + + return custom.getTagFileInfo().getTagInfo().getBodyContent(); + } + + private boolean isTagDependent(Node n) { + + if (n instanceof Node.CustomTag) { + String bodyType = getBodyType((Node.CustomTag) n); + return + TagInfo.BODY_CONTENT_TAG_DEPENDENT.equalsIgnoreCase(bodyType); + } + return false; + } +} diff --git a/java/org/apache/jasper/compiler/JspReader.java b/java/org/apache/jasper/compiler/JspReader.java new file mode 100644 index 0000000..df3ee8a --- /dev/null +++ b/java/org/apache/jasper/compiler/JspReader.java @@ -0,0 +1,654 @@ +/* + * 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.jasper.compiler; + +import java.io.CharArrayWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.runtime.ExceptionUtils; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.Jar; + +/** + * JspReader is an input buffer for the JSP parser. It should allow + * unlimited lookahead and pushback. It also has a bunch of parsing + * utility methods for understanding htmlesque thingies. + * + * @author Anil K. Vijendran + * @author Anselm Baird-Smith + * @author Harish Prabandham + * @author Rajiv Mordani + * @author Mandar Raje + * @author Danno Ferrin + * @author Kin-man Chung + * @author Shawn Bayern + * @author Mark Roth + */ + +class JspReader { + + /** + * Logger. + */ + private final Log log = LogFactory.getLog(JspReader.class); // must not be static + + /** + * The current spot in the file. + */ + private Mark current; + + /** + * The compilation context. + */ + private final JspCompilationContext context; + + /** + * The Jasper error dispatcher. + */ + private final ErrorDispatcher err; + + /** + * Constructor. + * + * @param ctxt The compilation context + * @param fname The file name + * @param encoding The file encoding + * @param jar ? + * @param err The error dispatcher + * @throws JasperException If a Jasper-internal error occurs + * @throws FileNotFoundException If the JSP file is not found (or is unreadable) + * @throws IOException If an IO-level error occurs, e.g. reading the file + */ + JspReader(JspCompilationContext ctxt, + String fname, + String encoding, + Jar jar, + ErrorDispatcher err) + throws JasperException, FileNotFoundException, IOException { + + this(ctxt, fname, JspUtil.getReader(fname, encoding, jar, ctxt, err), + err); + } + + /** + * Constructor: same as above constructor but with initialized reader + * to the file given. + * + * @param ctxt The compilation context + * @param fname The file name + * @param reader A reader for the JSP source file + * @param err The error dispatcher + * + * @throws JasperException If an error occurs parsing the JSP file + */ + JspReader(JspCompilationContext ctxt, + String fname, + InputStreamReader reader, + ErrorDispatcher err) + throws JasperException { + + this.context = ctxt; + this.err = err; + + try { + CharArrayWriter caw = new CharArrayWriter(); + char buf[] = new char[1024]; + for (int i = 0 ; (i = reader.read(buf)) != -1 ;) { + caw.write(buf, 0, i); + } + caw.close(); + current = new Mark(this, caw.toCharArray(), fname); + } catch (Throwable ex) { + ExceptionUtils.handleThrowable(ex); + log.error(Localizer.getMessage("jsp.error.file.cannot.read", fname), ex); + err.jspError("jsp.error.file.cannot.read", fname); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (Exception any) { + if(log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jsp.error.file.close"), any); + } + } + } + } + } + + + /** + * @return JSP compilation context with which this JspReader is + * associated + */ + JspCompilationContext getJspCompilationContext() { + return context; + } + + /** + * Checks if the current file has more input. + * + * @return True if more reading is possible + */ + boolean hasMoreInput() { + return current.cursor < current.stream.length; + } + + int nextChar() { + if (!hasMoreInput()) { + return -1; + } + + int ch = current.stream[current.cursor]; + + current.cursor++; + + if (ch == '\n') { + current.line++; + current.col = 0; + } else { + current.col++; + } + return ch; + } + + /** + * A faster approach than calling {@link #mark()} & {@link #nextChar()}. + * However, this approach is only safe if the mark is only used within the + * JspReader. + */ + private int nextChar(Mark mark) { + if (!hasMoreInput()) { + return -1; + } + + int ch = current.stream[current.cursor]; + + mark.init(current, true); + + current.cursor++; + + if (ch == '\n') { + current.line++; + current.col = 0; + } else { + current.col++; + } + return ch; + } + + /** + * Search the given character, If it was found, then mark the current cursor + * and the cursor point to next character. + */ + private Boolean indexOf(char c, Mark mark) { + if (!hasMoreInput()) { + return null; + } + + int end = current.stream.length; + int ch; + int line = current.line; + int col = current.col; + int i = current.cursor; + for(; i < end; i ++) { + ch = current.stream[i]; + + if (ch == c) { + mark.update(i, line, col); + } + if (ch == '\n') { + line++; + col = 0; + } else { + col++; + } + if (ch == c) { + current.update(i+1, line, col); + return Boolean.TRUE; + } + } + current.update(i, line, col); + return Boolean.FALSE; + } + + /** + * Back up the current cursor by one char, assumes current.cursor > 0, + * and that the char to be pushed back is not '\n'. + */ + void pushChar() { + current.cursor--; + current.col--; + } + + String getText(Mark start, Mark stop) { + Mark oldstart = mark(); + reset(start); + CharArrayWriter caw = new CharArrayWriter(); + while (!markEquals(stop)) { + caw.write(nextChar()); + } + caw.close(); + setCurrent(oldstart); + return caw.toString(); + } + + /** + * Read ahead one character without moving the cursor. + * + * @return The next character or -1 if no further input is available + */ + int peekChar() { + return peekChar(0); + } + + /** + * Read ahead the given number of characters without moving the cursor. + * + * @param readAhead The number of characters to read ahead. NOTE: This is + * zero based. + * + * @return The requested character or -1 if the end of the input is reached + * first + */ + int peekChar(int readAhead) { + int target = current.cursor + readAhead; + if (target < current.stream.length) { + return current.stream[target]; + } + return -1; + } + + Mark mark() { + return new Mark(current); + } + + + /** + * This method avoids a call to {@link #mark()} when doing comparison. + */ + private boolean markEquals(Mark another) { + return another.equals(current); + } + + void reset(Mark mark) { + current = new Mark(mark); + } + + /** + * Similar to {@link #reset(Mark)} but no new Mark will be created. + * Therefore, the parameter mark must NOT be used in other places. + */ + private void setCurrent(Mark mark) { + current = mark; + } + + /** + * search the stream for a match to a string + * @param string The string to match + * @return true is one is found, the current position + * in stream is positioned after the search string, + * false otherwise, position in stream unchanged. + */ + boolean matches(String string) { + int len = string.length(); + int cursor = current.cursor; + int streamSize = current.stream.length; + if (cursor + len < streamSize) { //Try to scan in memory + int line = current.line; + int col = current.col; + int ch; + int i = 0; + for(; i < len; i ++) { + ch = current.stream[i+cursor]; + if (string.charAt(i) != ch) { + return false; + } + if (ch == '\n') { + line ++; + col = 0; + } else { + col++; + } + } + current.update(i+cursor, line, col); + } else { + Mark mark = mark(); + int ch = 0; + int i = 0; + do { + ch = nextChar(); + if (((char) ch) != string.charAt(i++)) { + setCurrent(mark); + return false; + } + } while (i < len); + } + return true; + } + + boolean matchesETag(String tagName) { + Mark mark = mark(); + + if (!matches("') { + return true; + } + + setCurrent(mark); + return false; + } + + boolean matchesETagWithoutLessThan(String tagName) { + Mark mark = mark(); + + if (!matches("/" + tagName)) { + return false; + } + skipSpaces(); + if (nextChar() == '>') { + return true; + } + + setCurrent(mark); + return false; + } + + + /** + * Looks ahead to see if there are optional spaces followed by + * the given String. If so, true is returned and those spaces and + * characters are skipped. If not, false is returned and the + * position is restored to where we were before. + */ + boolean matchesOptionalSpacesFollowedBy(String s) { + Mark mark = mark(); + + skipSpaces(); + boolean result = matches( s ); + if( !result ) { + setCurrent(mark); + } + + return result; + } + + int skipSpaces() { + int i = 0; + while (hasMoreInput() && isSpace()) { + i++; + nextChar(); + } + return i; + } + + /** + * Skip until the given string is matched in the stream. + * When returned, the context is positioned past the end of the match. + * + * @param limit The String to match. + * @return A non-null Mark instance (positioned immediately + * before the search string) if found, null + * otherwise. + */ + Mark skipUntil(String limit) { + Mark ret = mark(); + int limlen = limit.length(); + char firstChar = limit.charAt(0); + Boolean result = null; + Mark restart = null; + + skip: + while((result = indexOf(firstChar, ret)) != null) { + if (result.booleanValue()) { + if (restart != null) { + restart.init(current, true); + } else { + restart = mark(); + } + for (int i = 1 ; i < limlen ; i++) { + if (peekChar() == limit.charAt(i)) { + nextChar(); + } else { + current.init(restart, true); + continue skip; + } + } + return ret; + } + } + return null; + } + + /** + * Skip until the given string is matched in the stream, but ignoring + * chars initially escaped by a '\' and any EL expressions. + * When returned, the context is positioned past the end of the match. + * + * @param limit The String to match. + * @param ignoreEL true if something that looks like EL should + * not be treated as EL. + * @return A non-null Mark instance (positioned immediately + * before the search string) if found, null + * otherwise. + */ + Mark skipUntilIgnoreEsc(String limit, boolean ignoreEL) { + Mark ret = mark(); + int limlen = limit.length(); + int ch; + int prev = 'x'; // Doesn't matter + char firstChar = limit.charAt(0); + skip: + for (ch = nextChar(ret) ; ch != -1 ; prev = ch, ch = nextChar(ret)) { + if (ch == '\\' && prev == '\\') { + ch = 0; // Double \ is not an escape char anymore + } else if (prev == '\\') { + continue; + } else if (!ignoreEL && (ch == '$' || ch == '#') && peekChar() == '{' ) { + // Move beyond the '{' + nextChar(); + skipELExpression(); + } else if (ch == firstChar) { + for (int i = 1 ; i < limlen ; i++) { + if (peekChar() == limit.charAt(i)) { + nextChar(); + } else { + continue skip; + } + } + return ret; + } + } + return null; + } + + /** + * Skip until the given end tag is matched in the stream. + * When returned, the context is positioned past the end of the tag. + * + * @param tag The name of the tag whose ETag (</tag>) to match. + * @return A non-null Mark instance (positioned immediately + * before the ETag) if found, null otherwise. + */ + Mark skipUntilETag(String tag) { + Mark ret = skipUntil("') { + ret = null; + } + } + return ret; + } + + /** + * Parse ELExpressionBody that is a body of ${} or #{} expression. Initial + * reader position is expected to be just after '${' or '#{' characters. + *

    + * In case of success, this method returns Mark for the last + * character before the terminating '}' and reader is positioned just after + * the '}' character. If no terminating '}' is encountered, this method + * returns null. + *

    + * Starting with EL 3.0, nested paired {}s are supported. + * + * @return Mark for the last character of EL expression or null + */ + Mark skipELExpression() { + // ELExpressionBody. + // Starts with "#{" or "${". Ends with "}". + // May contain quoted "{", "}", '{', or '}' and nested "{...}" + Mark last = mark(); + boolean singleQuoted = false; + boolean doubleQuoted = false; + int nesting = 0; + int currentChar; + do { + currentChar = nextChar(last); + while (currentChar == '\\' && (singleQuoted || doubleQuoted)) { + // skip character following '\' within quotes + // No need to update 'last', as neither of these characters + // can be the closing '}'. + nextChar(); + currentChar = nextChar(); + } + if (currentChar == -1) { + return null; + } + if (currentChar == '"' && !singleQuoted) { + doubleQuoted = !doubleQuoted; + } else if (currentChar == '\'' && !doubleQuoted) { + singleQuoted = !singleQuoted; + } else if (currentChar == '{' && !doubleQuoted && !singleQuoted) { + nesting++; + } else if (currentChar =='}' && !doubleQuoted && !singleQuoted) { + // Note: This also matches the terminating '}' at which point + // nesting will be set to -1 - hence the test for + // while (currentChar != '}' || nesting > -1 ||...) below + // to continue the loop until the final '}' is detected + nesting--; + } + } while (currentChar != '}' || singleQuoted || doubleQuoted || nesting > -1); + + return last; + } + + final boolean isSpace() { + // Note: If this logic changes, also update Node.TemplateText.rtrim() + return peekChar() <= ' '; + } + + /** + * Parse a space delimited token. + * If quoted the token will consume all characters up to a matching quote, + * otherwise, it consumes up to the first delimiter character. + * + * @param quoted If true accept quoted strings. + */ + String parseToken(boolean quoted) throws JasperException { + StringBuilder StringBuilder = new StringBuilder(); + skipSpaces(); + StringBuilder.setLength(0); + + if (!hasMoreInput()) { + return ""; + } + + int ch = peekChar(); + + if (quoted) { + if (ch == '"' || ch == '\'') { + + char endQuote = ch == '"' ? '"' : '\''; + // Consume the open quote: + ch = nextChar(); + for (ch = nextChar(); ch != -1 && ch != endQuote; + ch = nextChar()) { + if (ch == '\\') { + ch = nextChar(); + } + StringBuilder.append((char) ch); + } + // Check end of quote, skip closing quote: + if (ch == -1) { + err.jspError(mark(), "jsp.error.quotes.unterminated"); + } + } else { + err.jspError(mark(), "jsp.error.attr.quoted"); + } + } else { + if (!isDelimiter()) { + // Read value until delimiter is found: + do { + ch = nextChar(); + // Take care of the quoting here. + if (ch == '\\') { + if (peekChar() == '"' || peekChar() == '\'' || + peekChar() == '>' || peekChar() == '%') { + ch = nextChar(); + } + } + StringBuilder.append((char) ch); + } while (!isDelimiter()); + } + } + + return StringBuilder.toString(); + } + + + /** + * Parse utils - Is current character a token delimiter ? + * Delimiters are currently defined to be =, >, <, ", and ' or any + * any space character as defined by isSpace. + * + * @return A boolean. + */ + private boolean isDelimiter() { + if (! isSpace()) { + int ch = peekChar(); + // Look for a single-char work delimiter: + if (ch == '=' || ch == '>' || ch == '"' || ch == '\'' + || ch == '/') { + return true; + } + // Look for an end-of-comment or end-of-tag: + if (ch == '-') { + Mark mark = mark(); + if (((ch = nextChar()) == '>') + || ((ch == '-') && (nextChar() == '>'))) { + setCurrent(mark); + return true; + } else { + setCurrent(mark); + return false; + } + } + return false; + } else { + return true; + } + } +} + diff --git a/java/org/apache/jasper/compiler/JspRuntimeContext.java b/java/org/apache/jasper/compiler/JspRuntimeContext.java new file mode 100644 index 0000000..6685615 --- /dev/null +++ b/java/org/apache/jasper/compiler/JspRuntimeContext.java @@ -0,0 +1,614 @@ +/* + * 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.jasper.compiler; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FilePermission; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.PermissionCollection; +import java.security.Policy; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; + +import org.apache.jasper.Constants; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.Options; +import org.apache.jasper.runtime.ExceptionUtils; +import org.apache.jasper.servlet.JspServletWrapper; +import org.apache.jasper.util.FastRemovalDequeue; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +/** + * Class for tracking JSP compile time file dependencies when the + * >%@include file="..."%< directive is used. + * + * A background thread periodically checks the files a JSP page + * is dependent upon. If a dependent file changes the JSP page + * which included it is recompiled. + * + * Only used if a web application context is a directory. + * + * @author Glenn L. Nielsen + */ +public final class JspRuntimeContext { + + /** + * Logger + */ + private final Log log = LogFactory.getLog(JspRuntimeContext.class); // must not be static + + /** + * Counts how many times the webapp's JSPs have been reloaded. + */ + private final AtomicInteger jspReloadCount = new AtomicInteger(0); + + /** + * Counts how many times JSPs have been unloaded in this webapp. + */ + private final AtomicInteger jspUnloadCount = new AtomicInteger(0); + + // ----------------------------------------------------------- Constructors + + /** + * Create a JspRuntimeContext for a web application context. + * + * Loads in any previously generated dependencies from file. + * + * @param context ServletContext for web application + * @param options The main Jasper options + */ + public JspRuntimeContext(ServletContext context, Options options) { + + this.context = context; + this.options = options; + + // Get the parent class loader + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + if (loader == null) { + loader = this.getClass().getClassLoader(); + } + + if (log.isTraceEnabled()) { + if (loader != null) { + log.trace(Localizer.getMessage("jsp.message.parent_class_loader_is", + loader.toString())); + } else { + log.trace(Localizer.getMessage("jsp.message.parent_class_loader_is", + "")); + } + } + + parentClassLoader = loader; + classpath = initClassPath(); + + if (context instanceof org.apache.jasper.servlet.JspCServletContext) { + codeSource = null; + permissionCollection = null; + return; + } + + if (Constants.IS_SECURITY_ENABLED) { + SecurityHolder holder = initSecurity(); + codeSource = holder.cs; + permissionCollection = holder.pc; + } else { + codeSource = null; + permissionCollection = null; + } + + // If this web application context is running from a + // directory, start the background compilation thread + String appBase = context.getRealPath("/"); + if (!options.getDevelopment() + && appBase != null + && options.getCheckInterval() > 0) { + lastCompileCheck = System.currentTimeMillis(); + } + + if (options.getMaxLoadedJsps() > 0) { + jspQueue = new FastRemovalDequeue<>(options.getMaxLoadedJsps()); + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.message.jsp_queue_created", + "" + options.getMaxLoadedJsps(), context.getContextPath())); + } + } + + /* Init parameter is in seconds, locally we use milliseconds */ + jspIdleTimeout = options.getJspIdleTimeout() * 1000; + } + + // ----------------------------------------------------- Instance Variables + + /** + * This web applications ServletContext + */ + private final ServletContext context; + private final Options options; + private final ClassLoader parentClassLoader; + private final PermissionCollection permissionCollection; + private final CodeSource codeSource; + private final String classpath; + private volatile long lastCompileCheck = -1L; + private volatile long lastJspQueueUpdate = System.currentTimeMillis(); + /* JSP idle timeout in milliseconds */ + private long jspIdleTimeout; + + /** + * Maps JSP pages to their JspServletWrapper's + */ + private final Map jsps = new ConcurrentHashMap<>(); + + /** + * Keeps JSP pages ordered by last access. + */ + private FastRemovalDequeue jspQueue = null; + + /** + * Map of class name to associated source map. This is maintained here as + * multiple JSPs can depend on the same file (included JSP, tag file, etc.) + * so a web application scoped Map is required. + */ + private final Map smaps = new ConcurrentHashMap<>(); + + /** + * Flag that indicates if a background compilation check is in progress. + */ + private volatile boolean compileCheckInProgress = false; + + + // ------------------------------------------------------ Public Methods + + /** + * Add a new JspServletWrapper. + * + * @param jspUri JSP URI + * @param jsw Servlet wrapper for JSP + */ + public void addWrapper(String jspUri, JspServletWrapper jsw) { + jsps.put(jspUri, jsw); + } + + /** + * Get an already existing JspServletWrapper. + * + * @param jspUri JSP URI + * @return JspServletWrapper for JSP + */ + public JspServletWrapper getWrapper(String jspUri) { + return jsps.get(jspUri); + } + + /** + * Remove a JspServletWrapper. + * + * @param jspUri JSP URI of JspServletWrapper to remove + */ + public void removeWrapper(String jspUri) { + jsps.remove(jspUri); + } + + /** + * Push a newly compiled JspServletWrapper into the queue at first + * execution of jsp. Destroy any JSP that has been replaced in the queue. + * + * @param jsw Servlet wrapper for jsp. + * @return an unloadHandle that can be pushed to front of queue at later execution times. + * */ + public FastRemovalDequeue.Entry push(JspServletWrapper jsw) { + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.message.jsp_added", + jsw.getJspUri(), context.getContextPath())); + } + FastRemovalDequeue.Entry entry = jspQueue.push(jsw); + JspServletWrapper replaced = entry.getReplaced(); + if (replaced != null) { + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.message.jsp_removed_excess", + replaced.getJspUri(), context.getContextPath())); + } + unloadJspServletWrapper(replaced); + entry.clearReplaced(); + } + return entry; + } + + /** + * Push unloadHandle for JspServletWrapper to front of the queue. + * + * @param unloadHandle the unloadHandle for the jsp. + * */ + public void makeYoungest(FastRemovalDequeue.Entry unloadHandle) { + if (log.isTraceEnabled()) { + JspServletWrapper jsw = unloadHandle.getContent(); + log.trace(Localizer.getMessage("jsp.message.jsp_queue_update", + jsw.getJspUri(), context.getContextPath())); + } + jspQueue.moveFirst(unloadHandle); + } + + /** + * Returns the number of JSPs for which JspServletWrappers exist, i.e., + * the number of JSPs that have been loaded into the webapp. + * + * @return The number of JSPs that have been loaded into the webapp + */ + public int getJspCount() { + return jsps.size(); + } + + /** + * Get the SecurityManager Policy CodeSource for this web + * application context. + * + * @return CodeSource for JSP + */ + public CodeSource getCodeSource() { + return codeSource; + } + + /** + * Get the parent ClassLoader. + * + * @return ClassLoader parent + */ + public ClassLoader getParentClassLoader() { + return parentClassLoader; + } + + /** + * Get the SecurityManager PermissionCollection for this + * web application context. + * + * @return PermissionCollection permissions + */ + public PermissionCollection getPermissionCollection() { + return permissionCollection; + } + + /** + * Process a "destroy" event for this web application context. + */ + public void destroy() { + for (JspServletWrapper jspServletWrapper : jsps.values()) { + jspServletWrapper.destroy(); + } + } + + /** + * Increments the JSP reload counter. + */ + public void incrementJspReloadCount() { + jspReloadCount.incrementAndGet(); + } + + /** + * Resets the JSP reload counter. + * + * @param count Value to which to reset the JSP reload counter + */ + public void setJspReloadCount(int count) { + jspReloadCount.set(count); + } + + /** + * Gets the current value of the JSP reload counter. + * + * @return The current value of the JSP reload counter + */ + public int getJspReloadCount() { + return jspReloadCount.intValue(); + } + + /** + * Gets the number of JSPs that are in the JSP limiter queue + * + * @return The number of JSPs (in the webapp with which this JspServlet is + * associated) that are in the JSP limiter queue + */ + public int getJspQueueLength() { + if (jspQueue != null) { + return jspQueue.getSize(); + } + return -1; + } + + /** + * Gets the number of JSPs that have been unloaded. + * + * @return The number of JSPs (in the webapp with which this JspServlet is + * associated) that have been unloaded + */ + public int getJspUnloadCount() { + return jspUnloadCount.intValue(); + } + + + /** + * Method used by background thread to check the JSP dependencies + * registered with this class for JSP's. + */ + public void checkCompile() { + + if (lastCompileCheck < 0) { + // Checking was disabled + return; + } + long now = System.currentTimeMillis(); + if (now > (lastCompileCheck + (options.getCheckInterval() * 1000L))) { + lastCompileCheck = now; + } else { + return; + } + + List wrappersToReload = new ArrayList<>(); + // Tell JspServletWrapper to ignore the reload attribute while this + // check is in progress. See BZ 62603. + compileCheckInProgress = true; + + Object [] wrappers = jsps.values().toArray(); + for (Object wrapper : wrappers) { + JspServletWrapper jsw = (JspServletWrapper) wrapper; + JspCompilationContext ctxt = jsw.getJspEngineContext(); + // Sync on JspServletWrapper when calling ctxt.compile() + synchronized (jsw) { + try { + ctxt.compile(); + if (jsw.getReload()) { + wrappersToReload.add(jsw); + } + } catch (FileNotFoundException ex) { + ctxt.incrementRemoved(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + jsw.getServletContext().log(Localizer.getMessage("jsp.error.backgroundCompilationFailed"), t); + } + } + } + + // See BZ 62603. + // OK to process reload flag now. + compileCheckInProgress = false; + // Ensure all servlets and tags that need to be reloaded, are reloaded. + for (JspServletWrapper jsw : wrappersToReload) { + // Triggers reload + try { + if (jsw.isTagFile()) { + // Although this is a public method, all other paths to this + // method use this sync and it is required to prevent race + // conditions during the reload. + synchronized (this) { + jsw.loadTagFile(); + } + } else { + jsw.getServlet(); + } + } catch (ServletException e) { + jsw.getServletContext().log(Localizer.getMessage("jsp.error.reload"), e); + } + } + } + + public boolean isCompileCheckInProgress() { + return compileCheckInProgress; + } + + /** + * @return the classpath that is passed off to the Java compiler. + */ + public String getClassPath() { + return classpath; + } + + /** + * @return Last time the update background task has run + */ + public long getLastJspQueueUpdate() { + return lastJspQueueUpdate; + } + + + public Map getSmaps() { + return smaps; + } + + + public Options getOptions() { + return options; + } + + // -------------------------------------------------------- Private Methods + + /** + * Method used to initialize classpath for compiles. + * @return the compilation classpath + */ + private String initClassPath() { + + StringBuilder cpath = new StringBuilder(); + + if (parentClassLoader instanceof URLClassLoader) { + URL [] urls = ((URLClassLoader)parentClassLoader).getURLs(); + + for (URL url : urls) { + // Tomcat can use URLs other than file URLs. However, a protocol + // other than file: will generate a bad file system path, so + // only add file: protocol URLs to the classpath. + + if (url.getProtocol().equals("file")) { + try { + // Need to decode the URL, primarily to convert %20 + // sequences back to spaces + String decoded = url.toURI().getPath(); + cpath.append(decoded + File.pathSeparator); + } catch (URISyntaxException e) { + log.warn(Localizer.getMessage("jsp.warning.classpathUrl"), e); + } + } + } + } + + cpath.append(options.getScratchDir() + File.pathSeparator); + + String cp = (String) context.getAttribute(options.getServletClasspathAttribute()); + if (cp == null || cp.equals("")) { + cp = options.getClassPath(); + } + + String path = cpath.toString() + cp; + + if(log.isTraceEnabled()) { + log.trace("Compilation classpath initialized: " + path); + } + return path; + } + + /** + * Helper class to allow initSecurity() to return two items + */ + private static class SecurityHolder{ + private final CodeSource cs; + private final PermissionCollection pc; + private SecurityHolder(CodeSource cs, PermissionCollection pc){ + this.cs = cs; + this.pc = pc; + } + } + /** + * Method used to initialize SecurityManager data. + */ + private SecurityHolder initSecurity() { + + // Setup the PermissionCollection for this web app context + // based on the permissions configured for the root of the + // web app context directory, then add a file read permission + // for that directory. + Policy policy = Policy.getPolicy(); + CodeSource source = null; + PermissionCollection permissions = null; + if( policy != null ) { + try { + // Get the permissions for the web app context + String docBase = context.getRealPath("/"); + if( docBase == null ) { + docBase = options.getScratchDir().toString(); + } + String codeBase = docBase; + if (!codeBase.endsWith(File.separator)){ + codeBase = codeBase + File.separator; + } + File contextDir = new File(codeBase); + URL url = contextDir.getCanonicalFile().toURI().toURL(); + source = new CodeSource(url,(Certificate[])null); + permissions = policy.getPermissions(source); + + // Create a file read permission for web app context directory + if (!docBase.endsWith(File.separator)){ + permissions.add + (new FilePermission(docBase,"read")); + docBase = docBase + File.separator; + } else { + permissions.add + (new FilePermission + (docBase.substring(0,docBase.length() - 1),"read")); + } + docBase = docBase + "-"; + permissions.add(new FilePermission(docBase,"read")); + + // Spec says apps should have read/write for their temp + // directory. This is fine, as no security sensitive files, at + // least any that the app doesn't have full control of anyway, + // will be written here. + String workDir = options.getScratchDir().toString(); + if (!workDir.endsWith(File.separator)){ + permissions.add + (new FilePermission(workDir,"read,write")); + workDir = workDir + File.separator; + } + workDir = workDir + "-"; + permissions.add(new FilePermission( + workDir,"read,write,delete")); + + // Allow the JSP to access org.apache.jasper.runtime.HttpJspBase + permissions.add( new RuntimePermission( + "accessClassInPackage.org.apache.jasper.runtime") ); + } catch (RuntimeException | IOException e) { + context.log(Localizer.getMessage("jsp.error.security"), e); + } + } + return new SecurityHolder(source, permissions); + } + + private void unloadJspServletWrapper(JspServletWrapper jsw) { + removeWrapper(jsw.getJspUri()); + synchronized(jsw) { + jsw.destroy(); + } + jspUnloadCount.incrementAndGet(); + } + + + /** + * Method used by background thread to check if any JSP's should be unloaded. + */ + public void checkUnload() { + + if (log.isTraceEnabled()) { + int queueLength = -1; + if (jspQueue != null) { + queueLength = jspQueue.getSize(); + } + log.trace(Localizer.getMessage("jsp.message.jsp_unload_check", + context.getContextPath(), "" + jsps.size(), "" + queueLength)); + } + long now = System.currentTimeMillis(); + if (jspIdleTimeout > 0) { + long unloadBefore = now - jspIdleTimeout; + Object [] wrappers = jsps.values().toArray(); + for (Object wrapper : wrappers) { + JspServletWrapper jsw = (JspServletWrapper) wrapper; + synchronized (jsw) { + if (jsw.getLastUsageTime() < unloadBefore) { + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.message.jsp_removed_idle", + jsw.getJspUri(), context.getContextPath(), + "" + (now - jsw.getLastUsageTime()))); + } + if (jspQueue != null) { + jspQueue.remove(jsw.getUnloadHandle()); + } + unloadJspServletWrapper(jsw); + } + } + } + } + lastJspQueueUpdate = now; + } +} diff --git a/java/org/apache/jasper/compiler/JspUtil.java b/java/org/apache/jasper/compiler/JspUtil.java new file mode 100644 index 0000000..f4f22e1 --- /dev/null +++ b/java/org/apache/jasper/compiler/JspUtil.java @@ -0,0 +1,944 @@ +/* + * 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.jasper.compiler; + +import java.io.BufferedInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.security.Escape; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; + +/** + * This class has all the utility method(s). Ideally should move all the bean + * containers here. + * + * @author Mandar Raje. + * @author Rajiv Mordani. + * @author Danno Ferrin + * @author Pierre Delisle + * @author Shawn Bayern + * @author Mark Roth + */ +public class JspUtil { + + private static final String WEB_INF_TAGS = "/WEB-INF/tags/"; + private static final String META_INF_TAGS = "/META-INF/tags/"; + + // Delimiters for request-time expressions (JSP and XML syntax) + private static final String OPEN_EXPR = "<%="; + private static final String CLOSE_EXPR = "%>"; + + private static final String javaKeywords[] = { "abstract", "assert", + "boolean", "break", "byte", "case", "catch", "char", "class", + "const", "continue", "default", "do", "double", "else", "enum", + "extends", "final", "finally", "float", "for", "goto", "if", + "implements", "import", "instanceof", "int", "interface", "long", + "native", "new", "package", "private", "protected", "public", + "return", "short", "static", "strictfp", "super", "switch", + "synchronized", "this", "throw", "throws", "transient", "try", + "void", "volatile", "while" }; + + static final int JSP_INPUT_STREAM_BUFFER_SIZE = 1024; + + public static final int CHUNKSIZE = 1024; + + /** + * Takes a potential expression and converts it into XML form. + * @param expression The expression to convert + * @return XML view + */ + public static String getExprInXml(String expression) { + String returnString; + int length = expression.length(); + + if (expression.startsWith(OPEN_EXPR) && + expression.endsWith(CLOSE_EXPR)) { + returnString = expression.substring(1, length - 1); + } else { + returnString = expression; + } + + return Escape.xml(returnString); + } + + /** + * Checks to see if the given scope is valid. + * + * @param scope + * The scope to be checked + * @param n + * The Node containing the 'scope' attribute whose value is to be + * checked + * @param err + * error dispatcher + * + * @throws JasperException + * if scope is not null and different from "page", + * "request", "session", and + * "application" + */ + public static void checkScope(String scope, Node n, ErrorDispatcher err) + throws JasperException { + if (scope != null && !scope.equals("page") && !scope.equals("request") + && !scope.equals("session") && !scope.equals("application")) { + err.jspError(n, "jsp.error.invalid.scope", scope); + } + } + + /** + * Checks if all mandatory attributes are present and if all attributes + * present have valid names. Checks attributes specified as XML-style + * attributes as well as attributes specified using the jsp:attribute + * standard action. + * @param typeOfTag The tag type + * @param n The corresponding node + * @param validAttributes The array with the valid attributes + * @param err Dispatcher for errors + * @throws JasperException An error occurred + */ + public static void checkAttributes(String typeOfTag, Node n, + ValidAttribute[] validAttributes, ErrorDispatcher err) + throws JasperException { + Attributes attrs = n.getAttributes(); + Mark start = n.getStart(); + boolean valid = true; + + // AttributesImpl.removeAttribute is broken, so we do this... + int tempLength = (attrs == null) ? 0 : attrs.getLength(); + ArrayList temp = new ArrayList<>(tempLength); + for (int i = 0; i < tempLength; i++) { + @SuppressWarnings("null") // If attrs==null, tempLength == 0 + String qName = attrs.getQName(i); + if ((!qName.equals("xmlns")) && (!qName.startsWith("xmlns:"))) { + temp.add(qName); + } + } + + // Add names of attributes specified using jsp:attribute + Node.Nodes tagBody = n.getBody(); + if (tagBody != null) { + int numSubElements = tagBody.size(); + for (int i = 0; i < numSubElements; i++) { + Node node = tagBody.getNode(i); + if (node instanceof Node.NamedAttribute) { + String attrName = node.getAttributeValue("name"); + temp.add(attrName); + // Check if this value appear in the attribute of the node + if (n.getAttributeValue(attrName) != null) { + err.jspError(n, + "jsp.error.duplicate.name.jspattribute", + attrName); + } + } else { + // Nothing can come before jsp:attribute, and only + // jsp:body can come after it. + break; + } + } + } + + /* + * First check to see if all the mandatory attributes are present. If so + * only then proceed to see if the other attributes are valid for the + * particular tag. + */ + String missingAttribute = null; + + for (ValidAttribute validAttribute : validAttributes) { + int attrPos; + if (validAttribute.mandatory) { + attrPos = temp.indexOf(validAttribute.name); + if (attrPos != -1) { + temp.remove(attrPos); + valid = true; + } else { + valid = false; + missingAttribute = validAttribute.name; + break; + } + } + } + + // If mandatory attribute is missing then the exception is thrown + if (!valid) { + err.jspError(start, "jsp.error.mandatory.attribute", typeOfTag, + missingAttribute); + } + + // Check to see if there are any more attributes for the specified tag. + int attrLeftLength = temp.size(); + if (attrLeftLength == 0) { + return; + } + + // Now check to see if the rest of the attributes are valid too. + for(String attribute : temp) { + valid = false; + for (ValidAttribute validAttribute : validAttributes) { + if (attribute.equals(validAttribute.name)) { + valid = true; + break; + } + } + if (!valid) { + err.jspError(start, "jsp.error.invalid.attribute", typeOfTag, + attribute); + } + } + // XXX *could* move EL-syntax validation here... (sb) + } + + public static class ValidAttribute { + + private final String name; + private final boolean mandatory; + + public ValidAttribute(String name, boolean mandatory) { + this.name = name; + this.mandatory = mandatory; + } + + public ValidAttribute(String name) { + this(name, false); + } + } + + /** + * Convert a String value to 'boolean'. Besides the standard conversions + * done by Boolean.parseBoolean(s), the value "yes" (ignore case) + * is also converted to 'true'. If 's' is null, then 'false' is returned. + * + * @param s + * the string to be converted + * @return the boolean value associated with the string s + */ + public static boolean booleanValue(String s) { + boolean b = false; + if (s != null) { + if (s.equalsIgnoreCase("yes")) { + b = true; + } else { + b = Boolean.parseBoolean(s); + } + } + return b; + } + + /** + * Returns the Class object associated with the class or + * interface with the given string name. + * + *

    + * The Class object is determined by passing the given string + * name to the Class.forName() method, unless the given string + * name represents a primitive type, in which case it is converted to a + * Class object by appending ".class" to it (e.g., + * "int.class"). + * @param type The class name, array or primitive type + * @param loader The class loader + * @return the loaded class + * @throws ClassNotFoundException Loading class failed + */ + public static Class toClass(String type, ClassLoader loader) + throws ClassNotFoundException { + + Class c = null; + int i0 = type.indexOf('['); + int dims = 0; + if (i0 > 0) { + // This is an array. Count the dimensions + for (int i = 0; i < type.length(); i++) { + if (type.charAt(i) == '[') { + dims++; + } + } + type = type.substring(0, i0); + } + + if ("boolean".equals(type)) { + c = boolean.class; + } else if ("char".equals(type)) { + c = char.class; + } else if ("byte".equals(type)) { + c = byte.class; + } else if ("short".equals(type)) { + c = short.class; + } else if ("int".equals(type)) { + c = int.class; + } else if ("long".equals(type)) { + c = long.class; + } else if ("float".equals(type)) { + c = float.class; + } else if ("double".equals(type)) { + c = double.class; + } else if ("void".equals(type)) { + c = void.class; + } else { + c = loader.loadClass(type); + } + + if (dims == 0) { + return c; + } + + if (dims == 1) { + return java.lang.reflect.Array.newInstance(c, 1).getClass(); + } + + // Array of more than i dimension + return java.lang.reflect.Array.newInstance(c, new int[dims]).getClass(); + } + + /** + * Produces a String representing a call to the EL interpreter. + * + * @param isTagFile true if the file is a tag file + * rather than a JSP + * @param expression + * a String containing zero or more "${}" expressions + * @param expectedType + * the expected type of the interpreted result + * @param fnmapvar + * Variable pointing to a function map. + * @return a String representing a call to the EL interpreter. + */ + public static String interpreterCall(boolean isTagFile, String expression, + Class expectedType, String fnmapvar) { + /* + * Determine which context object to use. + */ + String jspCtxt = null; + if (isTagFile) { + jspCtxt = "this.getJspContext()"; + } else { + jspCtxt = "_jspx_page_context"; + } + + /* + * Determine whether to use the expected type's textual name or, if it's + * a primitive, the name of its correspondent boxed type. + */ + String returnType = expectedType.getCanonicalName(); + String targetType = returnType; + String primitiveConverterMethod = null; + if (expectedType.isPrimitive()) { + if (expectedType.equals(Boolean.TYPE)) { + returnType = Boolean.class.getName(); + primitiveConverterMethod = "booleanValue"; + } else if (expectedType.equals(Byte.TYPE)) { + returnType = Byte.class.getName(); + primitiveConverterMethod = "byteValue"; + } else if (expectedType.equals(Character.TYPE)) { + returnType = Character.class.getName(); + primitiveConverterMethod = "charValue"; + } else if (expectedType.equals(Short.TYPE)) { + returnType = Short.class.getName(); + primitiveConverterMethod = "shortValue"; + } else if (expectedType.equals(Integer.TYPE)) { + returnType = Integer.class.getName(); + primitiveConverterMethod = "intValue"; + } else if (expectedType.equals(Long.TYPE)) { + returnType = Long.class.getName(); + primitiveConverterMethod = "longValue"; + } else if (expectedType.equals(Float.TYPE)) { + returnType = Float.class.getName(); + primitiveConverterMethod = "floatValue"; + } else if (expectedType.equals(Double.TYPE)) { + returnType = Double.class.getName(); + primitiveConverterMethod = "doubleValue"; + } + } + + /* + * Build up the base call to the interpreter. + */ + // XXX - We use a proprietary call to the interpreter for now + // as the current standard machinery is inefficient and requires + // lots of wrappers and adapters. This should all clear up once + // the EL interpreter moves out of JSTL and into its own project. + // In the future, this should be replaced by code that calls + // ExpressionEvaluator.parseExpression() and then cache the resulting + // expression objects. The interpreterCall would simply select + // one of the pre-cached expressions and evaluate it. + // Note that PageContextImpl implements VariableResolver and + // the generated Servlet/SimpleTag implements FunctionMapper, so + // that machinery is already in place (mroth). + targetType = toJavaSourceType(targetType); + StringBuilder call = new StringBuilder( + "(" + + returnType + + ") " + + "org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate" + + "(" + Generator.quote(expression) + ", " + targetType + + ".class, " + "(jakarta.servlet.jsp.PageContext)" + jspCtxt + ", " + + fnmapvar + ")"); + + /* + * Add the primitive converter method if we need to. + */ + if (primitiveConverterMethod != null) { + call.insert(0, "("); + call.append(")." + primitiveConverterMethod + "()"); + } + + return call.toString(); + } + + public static String coerceToPrimitiveBoolean(String s, + boolean isNamedAttribute) { + if (isNamedAttribute) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToBoolean(" + + s + ")"; + } else { + if (s == null || s.length() == 0) { + return "false"; + } else { + return Boolean.valueOf(s).toString(); + } + } + } + + public static String coerceToBoolean(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "(java.lang.Boolean) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + + s + ", java.lang.Boolean.class)"; + } else { + if (s == null || s.length() == 0) { + return "java.lang.Boolean.FALSE"; + } else { + // Detect format error at translation time + return "java.lang.Boolean.valueOf(" + Generator.quote(s) + ")"; + } + } + } + + public static String coerceToPrimitiveByte(String s, + boolean isNamedAttribute) { + if (isNamedAttribute) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToByte(" + + s + ")"; + } else { + if (s == null || s.length() == 0) { + return "(byte) 0"; + } else { + return "((byte)" + Byte.valueOf(s).toString() + ")"; + } + } + } + + public static String coerceToByte(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "(java.lang.Byte) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + + s + ", java.lang.Byte.class)"; + } else { + if (s == null || s.length() == 0) { + return "java.lang.Byte.valueOf((byte) 0)"; + } else { + // Detect format error at translation time + return "java.lang.Byte.valueOf(" + Generator.quote(s) + ")"; + } + } + } + + public static String coerceToChar(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToChar(" + + s + ")"; + } else { + if (s == null || s.length() == 0) { + return "(char) 0"; + } else { + char ch = s.charAt(0); + // this trick avoids escaping issues + return "((char) " + (int) ch + ")"; + } + } + } + + public static String coerceToCharacter(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "(java.lang.Character) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + + s + ", java.lang.Character.class)"; + } else { + if (s == null || s.length() == 0) { + return "java.lang.Character.valueOf((char) 0)"; + } else { + char ch = s.charAt(0); + // this trick avoids escaping issues + return "java.lang.Character.valueOf((char) " + (int) ch + ")"; + } + } + } + + public static String coerceToPrimitiveDouble(String s, + boolean isNamedAttribute) { + if (isNamedAttribute) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToDouble(" + + s + ")"; + } else { + if (s == null || s.length() == 0) { + return "(double) 0"; + } else { + return Double.valueOf(s).toString(); + } + } + } + + public static String coerceToDouble(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "(java.lang.Double) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + + s + ", Double.class)"; + } else { + if (s == null || s.length() == 0) { + return "java.lang.Double.valueOf(0)"; + } else { + // Detect format error at translation time + return "java.lang.Double.valueOf(" + Generator.quote(s) + ")"; + } + } + } + + public static String coerceToPrimitiveFloat(String s, + boolean isNamedAttribute) { + if (isNamedAttribute) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToFloat(" + + s + ")"; + } else { + if (s == null || s.length() == 0) { + return "(float) 0"; + } else { + return Float.valueOf(s).toString() + "f"; + } + } + } + + public static String coerceToFloat(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "(java.lang.Float) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + + s + ", java.lang.Float.class)"; + } else { + if (s == null || s.length() == 0) { + return "java.lang.Float.valueOf(0)"; + } else { + // Detect format error at translation time + return "java.lang.Float.valueOf(" + Generator.quote(s) + ")"; + } + } + } + + public static String coerceToInt(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToInt(" + + s + ")"; + } else { + if (s == null || s.length() == 0) { + return "0"; + } else { + return Integer.valueOf(s).toString(); + } + } + } + + public static String coerceToInteger(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "(java.lang.Integer) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + + s + ", java.lang.Integer.class)"; + } else { + if (s == null || s.length() == 0) { + return "java.lang.Integer.valueOf(0)"; + } else { + // Detect format error at translation time + return "java.lang.Integer.valueOf(" + Generator.quote(s) + ")"; + } + } + } + + public static String coerceToPrimitiveShort(String s, + boolean isNamedAttribute) { + if (isNamedAttribute) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToShort(" + + s + ")"; + } else { + if (s == null || s.length() == 0) { + return "(short) 0"; + } else { + return "((short) " + Short.valueOf(s).toString() + ")"; + } + } + } + + public static String coerceToShort(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "(java.lang.Short) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + + s + ", java.lang.Short.class)"; + } else { + if (s == null || s.length() == 0) { + return "java.lang.Short.valueOf((short) 0)"; + } else { + // Detect format error at translation time + return "java.lang.Short.valueOf(" + Generator.quote(s) + ")"; + } + } + } + + public static String coerceToPrimitiveLong(String s, + boolean isNamedAttribute) { + if (isNamedAttribute) { + return "org.apache.jasper.runtime.JspRuntimeLibrary.coerceToLong(" + + s + ")"; + } else { + if (s == null || s.length() == 0) { + return "(long) 0"; + } else { + return Long.valueOf(s).toString() + "l"; + } + } + } + + public static String coerceToLong(String s, boolean isNamedAttribute) { + if (isNamedAttribute) { + return "(java.lang.Long) org.apache.jasper.runtime.JspRuntimeLibrary.coerce(" + + s + ", java.lang.Long.class)"; + } else { + if (s == null || s.length() == 0) { + return "java.lang.Long.valueOf(0)"; + } else { + // Detect format error at translation time + return "java.lang.Long.valueOf(" + Generator.quote(s) + ")"; + } + } + } + + public static BufferedInputStream getInputStream(String fname, Jar jar, + JspCompilationContext ctxt) throws IOException { + + InputStream in = null; + + if (jar != null) { + String jarEntryName = fname.substring(1); + in = jar.getInputStream(jarEntryName); + } else { + in = ctxt.getResourceAsStream(fname); + } + + if (in == null) { + throw new FileNotFoundException(Localizer.getMessage( + "jsp.error.file.not.found", fname)); + } + + return new BufferedInputStream(in, JSP_INPUT_STREAM_BUFFER_SIZE); + } + + public static InputSource getInputSource(String fname, Jar jar, JspCompilationContext ctxt) + throws IOException { + InputSource source; + if (jar != null) { + String jarEntryName = fname.substring(1); + source = new InputSource(jar.getInputStream(jarEntryName)); + source.setSystemId(jar.getURL(jarEntryName)); + } else { + source = new InputSource(ctxt.getResourceAsStream(fname)); + source.setSystemId(ctxt.getResource(fname).toExternalForm()); + } + return source; + } + + /** + * Gets the fully-qualified class name of the tag handler corresponding to + * the given tag file path. + * + * @param path Tag file path + * @param packageName The package name + * @param urn The tag identifier + * @param err Error dispatcher + * + * @return Fully-qualified class name of the tag handler corresponding to + * the given tag file path + * @throws JasperException Failed to generate a class name for the tag + */ + public static String getTagHandlerClassName(String path, String packageName, String urn, + ErrorDispatcher err) throws JasperException { + + + String className = null; + int begin = 0; + int index; + + index = path.lastIndexOf(".tag"); + if (index == -1) { + err.jspError("jsp.error.tagfile.badSuffix", path); + } + + // It's tempting to remove the ".tag" suffix here, but we can't. + // If we remove it, the fully-qualified class name of this tag + // could conflict with the package name of other tags. + // For instance, the tag file + // /WEB-INF/tags/foo.tag + // would have fully-qualified class name + // org.apache.jsp.tag.web.foo + // which would conflict with the package name of the tag file + // /WEB-INF/tags/foo/bar.tag + + index = path.indexOf(WEB_INF_TAGS); + if (index != -1) { + className = packageName + ".web."; + begin = index + WEB_INF_TAGS.length(); + } else { + index = path.indexOf(META_INF_TAGS); + if (index != -1) { + className = getClassNameBase(packageName, urn); + begin = index + META_INF_TAGS.length(); + } else { + err.jspError("jsp.error.tagfile.illegalPath", path); + } + } + + className += makeJavaPackage(path.substring(begin)); + + return className; + } + + private static String getClassNameBase(String packageName, String urn) { + StringBuilder base = + new StringBuilder(packageName + ".meta."); + if (urn != null) { + base.append(makeJavaPackage(urn)); + base.append('.'); + } + return base.toString(); + } + + /** + * Converts the given path to a Java package or fully-qualified class name + * + * @param path + * Path to convert + * + * @return Java package corresponding to the given path + */ + public static final String makeJavaPackage(String path) { + String classNameComponents[] = path.split("/"); + StringBuilder legalClassNames = new StringBuilder(); + for (String classNameComponent : classNameComponents) { + if (classNameComponent.length() > 0) { + if (legalClassNames.length() > 0) { + legalClassNames.append('.'); + } + legalClassNames.append(makeJavaIdentifier(classNameComponent)); + } + } + return legalClassNames.toString(); + } + + /** + * Converts the given identifier to a legal Java identifier + * + * @param identifier + * Identifier to convert + * + * @return Legal Java identifier corresponding to the given identifier + */ + public static final String makeJavaIdentifier(String identifier) { + return makeJavaIdentifier(identifier, true); + } + + /** + * Converts the given identifier to a legal Java identifier + * to be used for JSP Tag file attribute names. + * + * @param identifier + * Identifier to convert + * + * @return Legal Java identifier corresponding to the given identifier + */ + public static final String makeJavaIdentifierForAttribute(String identifier) { + return makeJavaIdentifier(identifier, false); + } + + /** + * Converts the given identifier to a legal Java identifier. + * + * @param identifier + * Identifier to convert + * + * @return Legal Java identifier corresponding to the given identifier + */ + private static String makeJavaIdentifier(String identifier, + boolean periodToUnderscore) { + StringBuilder modifiedIdentifier = new StringBuilder(identifier.length()); + if (!Character.isJavaIdentifierStart(identifier.charAt(0))) { + modifiedIdentifier.append('_'); + } + for (int i = 0; i < identifier.length(); i++) { + char ch = identifier.charAt(i); + if (Character.isJavaIdentifierPart(ch) && + (ch != '_' || !periodToUnderscore)) { + modifiedIdentifier.append(ch); + } else if (ch == '.' && periodToUnderscore) { + modifiedIdentifier.append('_'); + } else { + modifiedIdentifier.append(mangleChar(ch)); + } + } + if (isJavaKeyword(modifiedIdentifier.toString())) { + modifiedIdentifier.append('_'); + } + return modifiedIdentifier.toString(); + } + + /** + * Mangle the specified character to create a legal Java class name. + * @param ch The character + * @return the replacement character as a string + */ + public static final String mangleChar(char ch) { + char[] result = new char[5]; + result[0] = '_'; + result[1] = Character.forDigit((ch >> 12) & 0xf, 16); + result[2] = Character.forDigit((ch >> 8) & 0xf, 16); + result[3] = Character.forDigit((ch >> 4) & 0xf, 16); + result[4] = Character.forDigit(ch & 0xf, 16); + return new String(result); + } + + /** + * Test whether the argument is a Java keyword. + * @param key The name + * @return true if the name is a java identifier + */ + public static boolean isJavaKeyword(String key) { + int i = 0; + int j = javaKeywords.length; + while (i < j) { + int k = (i + j) >>> 1; + int result = javaKeywords[k].compareTo(key); + if (result == 0) { + return true; + } + if (result < 0) { + i = k + 1; + } else { + j = k; + } + } + return false; + } + + static InputStreamReader getReader(String fname, String encoding, + Jar jar, JspCompilationContext ctxt, ErrorDispatcher err) + throws JasperException, IOException { + + return getReader(fname, encoding, jar, ctxt, err, 0); + } + + static InputStreamReader getReader(String fname, String encoding, + Jar jar, JspCompilationContext ctxt, ErrorDispatcher err, int skip) + throws JasperException, IOException { + + InputStreamReader reader = null; + InputStream in = getInputStream(fname, jar, ctxt); + try { + for (int i = 0; i < skip; i++) { + in.read(); + } + } catch (IOException ioe) { + try { + in.close(); + } catch (IOException e) { + // Ignore + } + throw ioe; + } + try { + reader = new InputStreamReader(in, encoding); + } catch (UnsupportedEncodingException ex) { + err.jspError("jsp.error.unsupported.encoding", encoding); + } + + return reader; + } + + /** + * Handles taking input from TLDs 'java.lang.Object' -> + * 'java.lang.Object.class' 'int' -> 'int.class' 'void' -> 'Void.TYPE' + * 'int[]' -> 'int[].class' + * + * @param type The type from the TLD + * @return the Java type + */ + public static String toJavaSourceTypeFromTld(String type) { + if (type == null || "void".equals(type)) { + return "java.lang.Void.TYPE"; + } + return type + ".class"; + } + + /** + * Class.getName() return arrays in the form "[[[<et>", where et, the + * element type can be one of ZBCDFIJS or L<classname>;. It is + * converted into forms that can be understood by javac. + * @param type the type to convert + * @return the equivalent type in Java sources + */ + public static String toJavaSourceType(String type) { + + if (type.charAt(0) != '[') { + return type; + } + + int dims = 1; + String t = null; + for (int i = 1; i < type.length(); i++) { + if (type.charAt(i) == '[') { + dims++; + } else { + switch (type.charAt(i)) { + case 'Z': t = "boolean"; break; + case 'B': t = "byte"; break; + case 'C': t = "char"; break; + case 'D': t = "double"; break; + case 'F': t = "float"; break; + case 'I': t = "int"; break; + case 'J': t = "long"; break; + case 'S': t = "short"; break; + case 'L': t = type.substring(i+1, type.indexOf(';')); break; + } + break; + } + } + + if (t == null) { + // Should never happen + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.unable.getType", type)); + } + + StringBuilder resultType = new StringBuilder(t); + for (; dims > 0; dims--) { + resultType.append("[]"); + } + return resultType.toString(); + } +} diff --git a/java/org/apache/jasper/compiler/Localizer.java b/java/org/apache/jasper/compiler/Localizer.java new file mode 100644 index 0000000..e22803a --- /dev/null +++ b/java/org/apache/jasper/compiler/Localizer.java @@ -0,0 +1,87 @@ +/* + * 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.jasper.compiler; + +import java.text.MessageFormat; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import org.apache.jasper.runtime.ExceptionUtils; + +/** + * Class responsible for converting error codes to corresponding localized + * error messages. + * + * @author Jan Luehe + */ +public class Localizer { + + private static ResourceBundle bundle; + + static { + try { + bundle = ResourceBundle.getBundle("org.apache.jasper.resources.LocalStrings"); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + } + + /* + * Returns the localized error message corresponding to the given error + * code. + * + * If the given error code is not defined in the resource bundle for + * localized error messages, it is used as the error message. + * + * @param errCode Error code to localize + * + * @return Localized error message + */ + public static String getMessage(String errCode) { + String errMsg = errCode; + try { + if (bundle != null) { + errMsg = bundle.getString(errCode); + } + } catch (MissingResourceException e) { + } + return errMsg; + } + + /* + * Returns the localized error message corresponding to the given error + * code. + * + * If the given error code is not defined in the resource bundle for + * localized error messages, it is used as the error message. + * + * @param errCode Error code to localize + * @param args Arguments for parametric replacement + * + * @return Localized error message + */ + public static String getMessage(String errCode, Object... args) { + String errMsg = getMessage(errCode); + + if (args != null && args.length > 0) { + MessageFormat formatter = new MessageFormat(errMsg); + errMsg = formatter.format(args); + } + + return errMsg; + } +} diff --git a/java/org/apache/jasper/compiler/Mark.java b/java/org/apache/jasper/compiler/Mark.java new file mode 100644 index 0000000..a677087 --- /dev/null +++ b/java/org/apache/jasper/compiler/Mark.java @@ -0,0 +1,145 @@ +/* + * 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.jasper.compiler; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.jasper.JspCompilationContext; + +/** + * Mark represents a point in the JSP input. + * + * @author Anil K. Vijendran + */ +final class Mark { + + // position within current stream + int cursor, line, col; + + // current stream + char[] stream = null; + + // name of the current file + private String fileName; + + private JspCompilationContext ctxt; + + /** + * Constructor + * + * @param reader JspReader this mark belongs to + * @param inStream current stream for this mark + * @param name JSP file name + */ + Mark(JspReader reader, char[] inStream, String name) { + this.ctxt = reader.getJspCompilationContext(); + this.stream = inStream; + this.cursor = 0; + this.line = 1; + this.col = 1; + this.fileName = name; + } + + + /** + * Constructor + */ + Mark(Mark other) { + init(other, false); + } + + void update(int cursor, int line, int col) { + this.cursor = cursor; + this.line = line; + this.col = col; + } + + void init(Mark other, boolean singleFile) { + this.cursor = other.cursor; + this.line = other.line; + this.col = other.col; + + if (!singleFile) { + this.ctxt = other.ctxt; + this.stream = other.stream; + this.fileName = other.fileName; + } + } + + + /** + * Constructor + */ + Mark(JspCompilationContext ctxt, String filename, int line, int col) { + this.ctxt = ctxt; + this.stream = null; + this.cursor = 0; + this.line = line; + this.col = col; + this.fileName = filename; + } + + + public int getLineNumber() { + return line; + } + + public int getColumnNumber() { + return col; + } + + @Override + public String toString() { + return getFile()+"("+line+","+col+")"; + } + + public String getFile() { + return this.fileName; + } + + /** + * Gets the URL of the resource with which this Mark is associated + * + * @return URL of the resource with which this Mark is associated + * + * @exception MalformedURLException if the resource pathname is incorrect + */ + public URL getURL() throws MalformedURLException { + return ctxt.getResource(getFile()); + } + + @Override + public boolean equals(Object other) { + if (other instanceof Mark) { + Mark m = (Mark) other; + return this.cursor == m.cursor && this.line == m.line && this.col == m.col; + } + return false; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + col; + result = prime * result + cursor; + result = prime * result + line; + return result; + } +} diff --git a/java/org/apache/jasper/compiler/NewlineReductionServletWriter.java b/java/org/apache/jasper/compiler/NewlineReductionServletWriter.java new file mode 100644 index 0000000..ed6fbfe --- /dev/null +++ b/java/org/apache/jasper/compiler/NewlineReductionServletWriter.java @@ -0,0 +1,54 @@ +/* + * 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.jasper.compiler; + +import java.io.PrintWriter; + +/** + * This class filters duplicate newlines instructions from the compiler output, + * and therefore from the runtime JSP. The duplicates typically happen because + * the compiler has multiple branches that write them, but they operate + * independently and don't realize that the previous output was identical. + * + * Removing these lines makes the JSP more efficient by executing fewer + * operations during runtime. + */ +public class NewlineReductionServletWriter extends ServletWriter { + + private static final String NEWLINE_WRITE_TEXT = "out.write('\\n');"; + + private boolean lastWriteWasNewline; + + public NewlineReductionServletWriter(PrintWriter writer) { + super(writer); + } + + @Override + public void printil(String s) { + if (s.equals(NEWLINE_WRITE_TEXT)) { + if (lastWriteWasNewline) { + // do nothing + return; + } else { + lastWriteWasNewline = true; + } + } else { + lastWriteWasNewline = false; + } + super.printil(s); + } +} \ No newline at end of file diff --git a/java/org/apache/jasper/compiler/Node.java b/java/org/apache/jasper/compiler/Node.java new file mode 100644 index 0000000..daa1675 --- /dev/null +++ b/java/org/apache/jasper/compiler/Node.java @@ -0,0 +1,2595 @@ +/* + * 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.jasper.compiler; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.ExpressionFactory; +import jakarta.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.tagext.DynamicAttributes; +import jakarta.servlet.jsp.tagext.IterationTag; +import jakarta.servlet.jsp.tagext.JspIdConsumer; +import jakarta.servlet.jsp.tagext.SimpleTag; +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagData; +import jakarta.servlet.jsp.tagext.TagFileInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagVariableInfo; +import jakarta.servlet.jsp.tagext.TryCatchFinally; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.apache.jasper.JasperException; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; +import org.xml.sax.Attributes; + +/** + * An internal data representation of a JSP page or a JSP document (XML). Also + * included here is a visitor class for traversing nodes. + * + * @author Kin-man Chung + * @author Jan Luehe + * @author Shawn Bayern + * @author Mark Roth + */ + +abstract class Node implements TagConstants { + + private static final VariableInfo[] ZERO_VARIABLE_INFO = {}; + + protected Attributes attrs; + + // xmlns attributes that represent tag libraries (only in XML syntax) + protected Attributes taglibAttrs; + + /* + * xmlns attributes that do not represent tag libraries (only in XML syntax) + */ + protected Attributes nonTaglibXmlnsAttrs; + + protected Nodes body; + + protected String text; + + protected Mark startMark; + + protected int beginJavaLine; + + protected int endJavaLine; + + protected Node parent; + + protected Nodes namedAttributeNodes; // cached for performance + + protected String qName; + + protected String localName; + + /* + * The name of the inner class to which the codes for this node and its body + * are generated. For instance, for in foo.jsp, this is + * "foo_jspHelper". This is primarily used for communicating such info from + * Generator to Smap generator. + */ + protected String innerClassName; + + + /** + * Zero-arg Constructor. + */ + Node() { + } + + /** + * Constructor. + * + * @param start + * The location of the jsp page + * @param parent + * The enclosing node + */ + Node(Mark start, Node parent) { + this.startMark = start; + addToParent(parent); + } + + /** + * Constructor for Nodes parsed from standard syntax. + * + * @param qName + * The action's qualified name + * @param localName + * The action's local name + * @param attrs + * The attributes for this node + * @param start + * The location of the jsp page + * @param parent + * The enclosing node + */ + Node(String qName, String localName, Attributes attrs, Mark start, + Node parent) { + this.qName = qName; + this.localName = localName; + this.attrs = attrs; + this.startMark = start; + addToParent(parent); + } + + /** + * Constructor for Nodes parsed from XML syntax. + * + * @param qName + * The action's qualified name + * @param localName + * The action's local name + * @param attrs + * The action's attributes whose name does not start with xmlns + * @param nonTaglibXmlnsAttrs + * The action's xmlns attributes that do not represent tag + * libraries + * @param taglibAttrs + * The action's xmlns attributes that represent tag libraries + * @param start + * The location of the jsp page + * @param parent + * The enclosing node + */ + Node(String qName, String localName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, Mark start, + Node parent) { + this.qName = qName; + this.localName = localName; + this.attrs = attrs; + this.nonTaglibXmlnsAttrs = nonTaglibXmlnsAttrs; + this.taglibAttrs = taglibAttrs; + this.startMark = start; + addToParent(parent); + } + + /* + * Constructor. + * + * @param qName The action's qualified name @param localName The action's + * local name @param text The text associated with this node @param start + * The location of the jsp page @param parent The enclosing node + */ + Node(String qName, String localName, String text, Mark start, + Node parent) { + this.qName = qName; + this.localName = localName; + this.text = text; + this.startMark = start; + addToParent(parent); + } + + public String getQName() { + return this.qName; + } + + public String getLocalName() { + return this.localName; + } + + /* + * Gets this Node's attributes. + * + * In the case of a Node parsed from standard syntax, this method returns + * all the Node's attributes. + * + * In the case of a Node parsed from XML syntax, this method returns only + * those attributes whose name does not start with xmlns. + */ + public Attributes getAttributes() { + return this.attrs; + } + + /* + * Gets this Node's xmlns attributes that represent tag libraries (only + * meaningful for Nodes parsed from XML syntax) + */ + public Attributes getTaglibAttributes() { + return this.taglibAttrs; + } + + /* + * Gets this Node's xmlns attributes that do not represent tag libraries + * (only meaningful for Nodes parsed from XML syntax) + */ + public Attributes getNonTaglibXmlnsAttributes() { + return this.nonTaglibXmlnsAttrs; + } + + public void setAttributes(Attributes attrs) { + this.attrs = attrs; + } + + public String getAttributeValue(String name) { + return (attrs == null) ? null : attrs.getValue(name); + } + + /** + * Get the attribute that is non request time expression, either from the + * attribute of the node, or from a jsp:attribute + * + * @param name The name of the attribute + * + * @return The attribute value + */ + public String getTextAttribute(String name) { + + String attr = getAttributeValue(name); + if (attr != null) { + return attr; + } + + NamedAttribute namedAttribute = getNamedAttributeNode(name); + if (namedAttribute == null) { + return null; + } + + return namedAttribute.getText(); + } + + /** + * Searches all sub-nodes of this node for jsp:attribute standard actions + * with the given name. + *

    + * This should always be called and only be called for nodes that accept + * dynamic runtime attribute expressions. + * + * @param name The name of the attribute + * @return the NamedAttribute node of the matching named attribute, nor null + * if no such node is found. + */ + public NamedAttribute getNamedAttributeNode(String name) { + NamedAttribute result = null; + + // Look for the attribute in NamedAttribute children + Nodes nodes = getNamedAttributeNodes(); + int numChildNodes = nodes.size(); + for (int i = 0; i < numChildNodes; i++) { + NamedAttribute na = (NamedAttribute) nodes.getNode(i); + boolean found = false; + int index = name.indexOf(':'); + if (index != -1) { + // qualified name + found = na.getName().equals(name); + } else { + found = na.getLocalName().equals(name); + } + if (found) { + result = na; + break; + } + } + + return result; + } + + /** + * Searches all subnodes of this node for jsp:attribute standard actions, + * and returns that set of nodes as a Node.Nodes object. + * + * @return Possibly empty Node.Nodes object containing any jsp:attribute + * subnodes of this Node + */ + public Node.Nodes getNamedAttributeNodes() { + + if (namedAttributeNodes != null) { + return namedAttributeNodes; + } + + Node.Nodes result = new Node.Nodes(); + + // Look for the attribute in NamedAttribute children + Nodes nodes = getBody(); + if (nodes != null) { + int numChildNodes = nodes.size(); + for (int i = 0; i < numChildNodes; i++) { + Node n = nodes.getNode(i); + if (n instanceof NamedAttribute) { + result.add(n); + } else if (!(n instanceof Comment)) { + // Nothing can come before jsp:attribute, and only + // jsp:body can come after it. + break; + } + } + } + + namedAttributeNodes = result; + return result; + } + + public Nodes getBody() { + return body; + } + + public void setBody(Nodes body) { + this.body = body; + } + + public String getText() { + return text; + } + + public Mark getStart() { + return startMark; + } + + public Node getParent() { + return parent; + } + + public int getBeginJavaLine() { + return beginJavaLine; + } + + public void setBeginJavaLine(int begin) { + beginJavaLine = begin; + } + + public int getEndJavaLine() { + return endJavaLine; + } + + public void setEndJavaLine(int end) { + endJavaLine = end; + } + + public Node.Root getRoot() { + Node n = this; + while (!(n instanceof Node.Root)) { + n = n.getParent(); + } + return (Node.Root) n; + } + + public String getInnerClassName() { + return innerClassName; + } + + public void setInnerClassName(String icn) { + innerClassName = icn; + } + + /** + * Selects and invokes a method in the visitor class based on the node type. + * This is abstract and should be overrode by the extending classes. + * + * @param v + * The visitor class + */ + abstract void accept(Visitor v) throws JasperException; + + // ********************************************************************* + // Private utility methods + + /* + * Adds this Node to the body of the given parent. + */ + private void addToParent(Node parent) { + if (parent != null) { + this.parent = parent; + Nodes parentBody = parent.getBody(); + if (parentBody == null) { + parentBody = new Nodes(); + parent.setBody(parentBody); + } + parentBody.add(this); + } + } + + + /** + * Represents the root of a Jsp page or Jsp document + */ + public static class Root extends Node { + + private final Root parentRoot; + + private final boolean isXmlSyntax; + + private final String variablePrefix; + + // Source encoding of the page containing this Root + private String pageEnc; + + // Page encoding specified in JSP config element + private String jspConfigPageEnc; + + /* + * Flag indicating if the default page encoding is being used (only + * applicable with standard syntax). + * + * True if the page does not provide a page directive with a + * 'contentType' attribute (or the 'contentType' attribute doesn't have + * a CHARSET value), the page does not provide a page directive with a + * 'pageEncoding' attribute, and there is no JSP configuration element + * page-encoding whose URL pattern matches the page. + */ + private boolean isDefaultPageEncoding; + + /* + * Indicates whether an encoding has been explicitly specified in the + * page's XML prolog (only used for pages in XML syntax). This + * information is used to decide whether a translation error must be + * reported for encoding conflicts. + */ + private boolean isEncodingSpecifiedInProlog; + + /* + * Indicates whether an encoding has been explicitly specified in the + * page's dom. + */ + private boolean isBomPresent; + + /* + * Sequence number for temporary variables. + */ + private int tempSequenceNumber = 0; + + /* + * Constructor. + */ + Root(Mark start, Node parent, boolean isXmlSyntax, String variablePrefix) { + super(start, parent); + this.isXmlSyntax = isXmlSyntax; + this.variablePrefix = variablePrefix; + this.qName = JSP_ROOT_ACTION; + this.localName = ROOT_ACTION; + + // Figure out and set the parent root + Node r = parent; + while ((r != null) && !(r instanceof Node.Root)) { + r = r.getParent(); + } + parentRoot = (Node.Root) r; + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public boolean isXmlSyntax() { + return isXmlSyntax; + } + + /* + * Sets the encoding specified in the JSP config element whose URL + * pattern matches the page containing this Root. + */ + public void setJspConfigPageEncoding(String enc) { + jspConfigPageEnc = enc; + } + + /* + * Gets the encoding specified in the JSP config element whose URL + * pattern matches the page containing this Root. + */ + public String getJspConfigPageEncoding() { + return jspConfigPageEnc; + } + + public void setPageEncoding(String enc) { + pageEnc = enc; + } + + public String getPageEncoding() { + return pageEnc; + } + + public void setIsDefaultPageEncoding(boolean isDefault) { + isDefaultPageEncoding = isDefault; + } + + public boolean isDefaultPageEncoding() { + return isDefaultPageEncoding; + } + + public void setIsEncodingSpecifiedInProlog(boolean isSpecified) { + isEncodingSpecifiedInProlog = isSpecified; + } + + public boolean isEncodingSpecifiedInProlog() { + return isEncodingSpecifiedInProlog; + } + + public void setIsBomPresent(boolean isBom) { + isBomPresent = isBom; + } + + public boolean isBomPresent() { + return isBomPresent; + } + + /** + * Generates a new temporary variable name. + * + * @return The name to use for the temporary variable + */ + public String nextTemporaryVariableName() { + if (parentRoot == null) { + return variablePrefix + (tempSequenceNumber++); + } else { + return parentRoot.nextTemporaryVariableName(); + } + + } + } + + /** + * Represents the root of a Jsp document (XML syntax) + */ + public static class JspRoot extends Node { + + JspRoot(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, ROOT_ACTION, attrs, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a page directive + */ + public static class PageDirective extends Node { + + private final List imports; + + PageDirective(Attributes attrs, Mark start, Node parent) { + this(JSP_PAGE_DIRECTIVE_ACTION, attrs, null, null, start, parent); + } + + PageDirective(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, PAGE_DIRECTIVE_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + imports = new ArrayList<>(); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + /** + * Parses the comma-separated list of class or package names in the + * given attribute value and adds each component to this PageDirective's + * vector of imported classes and packages. + * + * @param value + * A comma-separated string of imports. + */ + public void addImport(String value) { + int start = 0; + int index; + while ((index = value.indexOf(',', start)) != -1) { + imports.add(validateImport(value.substring(start, index))); + start = index + 1; + } + if (start == 0) { + // No comma found + imports.add(validateImport(value)); + } else { + imports.add(validateImport(value.substring(start))); + } + } + + public List getImports() { + return imports; + } + + /** + * Just need enough validation to make sure nothing strange is going on. + * The compiler will validate this thoroughly when it tries to compile + * the resulting .java file. + */ + private String validateImport(String importEntry) { + // This should either be a fully-qualified class name or a package + // name with a wildcard + if (importEntry.indexOf(';') > -1) { + throw new IllegalArgumentException( + Localizer.getMessage("jsp.error.page.invalid.import")); + } + return importEntry.trim(); + } + } + + /** + * Represents an include directive + */ + public static class IncludeDirective extends Node { + + IncludeDirective(Attributes attrs, Mark start, Node parent) { + this(JSP_INCLUDE_DIRECTIVE_ACTION, attrs, null, null, start, parent); + } + + IncludeDirective(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, INCLUDE_DIRECTIVE_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a custom taglib directive + */ + public static class TaglibDirective extends Node { + + TaglibDirective(Attributes attrs, Mark start, Node parent) { + super(JSP_TAGLIB_DIRECTIVE_ACTION, TAGLIB_DIRECTIVE_ACTION, attrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a tag directive + */ + public static class TagDirective extends Node { + private final List imports; + + TagDirective(Attributes attrs, Mark start, Node parent) { + this(JSP_TAG_DIRECTIVE_ACTION, attrs, null, null, start, parent); + } + + TagDirective(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, TAG_DIRECTIVE_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + imports = new ArrayList<>(); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + /** + * Parses the comma-separated list of class or package names in the + * given attribute value and adds each component to this PageDirective's + * vector of imported classes and packages. + * + * @param value + * A comma-separated string of imports. + */ + public void addImport(String value) { + int start = 0; + int index; + while ((index = value.indexOf(',', start)) != -1) { + imports.add(value.substring(start, index).trim()); + start = index + 1; + } + if (start == 0) { + // No comma found + imports.add(value.trim()); + } else { + imports.add(value.substring(start).trim()); + } + } + + public List getImports() { + return imports; + } + } + + /** + * Represents an attribute directive + */ + public static class AttributeDirective extends Node { + + AttributeDirective(Attributes attrs, Mark start, Node parent) { + this(JSP_ATTRIBUTE_DIRECTIVE_ACTION, attrs, null, null, start, + parent); + } + + AttributeDirective(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, ATTRIBUTE_DIRECTIVE_ACTION, attrs, + nonTaglibXmlnsAttrs, taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a variable directive + */ + public static class VariableDirective extends Node { + + VariableDirective(Attributes attrs, Mark start, Node parent) { + this(JSP_VARIABLE_DIRECTIVE_ACTION, attrs, null, null, start, + parent); + } + + VariableDirective(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, VARIABLE_DIRECTIVE_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a <jsp:invoke> tag file action + */ + public static class InvokeAction extends Node { + + InvokeAction(Attributes attrs, Mark start, Node parent) { + this(JSP_INVOKE_ACTION, attrs, null, null, start, parent); + } + + InvokeAction(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, INVOKE_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a <jsp:doBody> tag file action + */ + public static class DoBodyAction extends Node { + + DoBodyAction(Attributes attrs, Mark start, Node parent) { + this(JSP_DOBODY_ACTION, attrs, null, null, start, parent); + } + + DoBodyAction(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, DOBODY_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a Jsp comment Comments are kept for completeness. + */ + public static class Comment extends Node { + + Comment(String text, Mark start, Node parent) { + super(null, null, text, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents an expression, declaration, or scriptlet + */ + public abstract static class ScriptingElement extends Node { + + ScriptingElement(String qName, String localName, String text, + Mark start, Node parent) { + super(qName, localName, text, start, parent); + } + + ScriptingElement(String qName, String localName, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, localName, null, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + /** + * When this node was created from a JSP page in JSP syntax, its text + * was stored as a String in the "text" field, whereas when this node + * was created from a JSP document, its text was stored as one or more + * TemplateText nodes in its body. This method handles either case. + * + * @return The text string + */ + @Override + public String getText() { + String ret = text; + if (ret == null) { + if (body != null) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < body.size(); i++) { + buf.append(body.getNode(i).getText()); + } + ret = buf.toString(); + } else { + // Nulls cause NPEs further down the line + ret = ""; + } + } + return ret; + } + + /** + * For the same reason as above, the source line information in the + * contained TemplateText node should be used. + */ + @Override + public Mark getStart() { + if (text == null && body != null && body.size() > 0) { + return body.getNode(0).getStart(); + } else { + return super.getStart(); + } + } + } + + /** + * Represents a declaration + */ + public static class Declaration extends ScriptingElement { + + Declaration(String text, Mark start, Node parent) { + super(JSP_DECLARATION_ACTION, DECLARATION_ACTION, text, start, + parent); + } + + Declaration(String qName, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent) { + super(qName, DECLARATION_ACTION, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents an expression. Expressions in attributes are embedded in the + * attribute string and not here. + */ + public static class Expression extends ScriptingElement { + + Expression(String text, Mark start, Node parent) { + super(JSP_EXPRESSION_ACTION, EXPRESSION_ACTION, text, start, parent); + } + + Expression(String qName, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent) { + super(qName, EXPRESSION_ACTION, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a scriptlet + */ + public static class Scriptlet extends ScriptingElement { + + Scriptlet(String text, Mark start, Node parent) { + super(JSP_SCRIPTLET_ACTION, SCRIPTLET_ACTION, text, start, parent); + } + + Scriptlet(String qName, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent) { + super(qName, SCRIPTLET_ACTION, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents an EL expression. Expressions in attributes are embedded in + * the attribute string and not here. + */ + public static class ELExpression extends Node { + + private ELNode.Nodes el; + + private final char type; + + ELExpression(char type, String text, Mark start, Node parent) { + super(null, null, text, start, parent); + this.type = type; + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setEL(ELNode.Nodes el) { + this.el = el; + } + + public ELNode.Nodes getEL() { + return el; + } + + public char getType() { + return this.type; + } + } + + /** + * Represents a param action + */ + public static class ParamAction extends Node { + + private JspAttribute value; + + ParamAction(Attributes attrs, Mark start, Node parent) { + this(JSP_PARAM_ACTION, attrs, null, null, start, parent); + } + + ParamAction(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, PARAM_ACTION, attrs, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setValue(JspAttribute value) { + this.value = value; + } + + public JspAttribute getValue() { + return value; + } + } + + /** + * Represents a params action + */ + public static class ParamsAction extends Node { + + ParamsAction(Mark start, Node parent) { + this(JSP_PARAMS_ACTION, null, null, start, parent); + } + + ParamsAction(String qName, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent) { + super(qName, PARAMS_ACTION, null, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a fallback action + */ + public static class FallBackAction extends Node { + + FallBackAction(Mark start, Node parent) { + this(JSP_FALLBACK_ACTION, null, null, start, parent); + } + + FallBackAction(String qName, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent) { + super(qName, FALLBACK_ACTION, null, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents an include action + */ + public static class IncludeAction extends Node { + + private JspAttribute page; + + IncludeAction(Attributes attrs, Mark start, Node parent) { + this(JSP_INCLUDE_ACTION, attrs, null, null, start, parent); + } + + IncludeAction(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, INCLUDE_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setPage(JspAttribute page) { + this.page = page; + } + + public JspAttribute getPage() { + return page; + } + } + + /** + * Represents a forward action + */ + public static class ForwardAction extends Node { + + private JspAttribute page; + + ForwardAction(Attributes attrs, Mark start, Node parent) { + this(JSP_FORWARD_ACTION, attrs, null, null, start, parent); + } + + ForwardAction(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, FORWARD_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setPage(JspAttribute page) { + this.page = page; + } + + public JspAttribute getPage() { + return page; + } + } + + /** + * Represents a getProperty action + */ + public static class GetProperty extends Node { + + GetProperty(Attributes attrs, Mark start, Node parent) { + this(JSP_GET_PROPERTY_ACTION, attrs, null, null, start, parent); + } + + GetProperty(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, GET_PROPERTY_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a setProperty action + */ + public static class SetProperty extends Node { + + private JspAttribute value; + + SetProperty(Attributes attrs, Mark start, Node parent) { + this(JSP_SET_PROPERTY_ACTION, attrs, null, null, start, parent); + } + + SetProperty(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, SET_PROPERTY_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setValue(JspAttribute value) { + this.value = value; + } + + public JspAttribute getValue() { + return value; + } + } + + /** + * Represents a useBean action + */ + public static class UseBean extends Node { + + private JspAttribute beanName; + + UseBean(Attributes attrs, Mark start, Node parent) { + this(JSP_USE_BEAN_ACTION, attrs, null, null, start, parent); + } + + UseBean(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, USE_BEAN_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setBeanName(JspAttribute beanName) { + this.beanName = beanName; + } + + public JspAttribute getBeanName() { + return beanName; + } + } + + /** + * Represents a plugin action + */ + public static class PlugIn extends Node { + + private JspAttribute width; + + private JspAttribute height; + + PlugIn(Attributes attrs, Mark start, Node parent) { + this(JSP_PLUGIN_ACTION, attrs, null, null, start, parent); + } + + PlugIn(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, PLUGIN_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setHeight(JspAttribute height) { + this.height = height; + } + + public void setWidth(JspAttribute width) { + this.width = width; + } + + public JspAttribute getHeight() { + return height; + } + + public JspAttribute getWidth() { + return width; + } + } + + /** + * Represents an uninterpreted tag, from a Jsp document + */ + public static class UninterpretedTag extends Node { + + private JspAttribute[] jspAttrs; + + UninterpretedTag(String qName, String localName, + Attributes attrs, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent) { + super(qName, localName, attrs, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setJspAttributes(JspAttribute[] jspAttrs) { + this.jspAttrs = jspAttrs; + } + + public JspAttribute[] getJspAttributes() { + return jspAttrs; + } + } + + /** + * Represents a <jsp:element>. + */ + public static class JspElement extends Node { + + private JspAttribute[] jspAttrs; + + private JspAttribute nameAttr; + + JspElement(Attributes attrs, Mark start, Node parent) { + this(JSP_ELEMENT_ACTION, attrs, null, null, start, parent); + } + + JspElement(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, ELEMENT_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public void setJspAttributes(JspAttribute[] jspAttrs) { + this.jspAttrs = jspAttrs; + } + + public JspAttribute[] getJspAttributes() { + return jspAttrs; + } + + /* + * Sets the XML-style 'name' attribute + */ + public void setNameAttribute(JspAttribute nameAttr) { + this.nameAttr = nameAttr; + } + + /* + * Gets the XML-style 'name' attribute + */ + public JspAttribute getNameAttribute() { + return this.nameAttr; + } + } + + /** + * Represents a <jsp:output>. + */ + public static class JspOutput extends Node { + + JspOutput(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + super(qName, OUTPUT_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Collected information about child elements. Used by nodes like CustomTag, + * JspBody, and NamedAttribute. The information is set in the Collector. + */ + public static class ChildInfo { + private boolean scriptless; // true if the tag and its body + + // contain no scripting elements. + private boolean hasUseBean; + + private boolean hasIncludeAction; + + private boolean hasParamAction; + + private boolean hasSetProperty; + + private boolean hasScriptingVars; + + public void setScriptless(boolean s) { + scriptless = s; + } + + public boolean isScriptless() { + return scriptless; + } + + public void setHasUseBean(boolean u) { + hasUseBean = u; + } + + public boolean hasUseBean() { + return hasUseBean; + } + + public void setHasIncludeAction(boolean i) { + hasIncludeAction = i; + } + + public boolean hasIncludeAction() { + return hasIncludeAction; + } + + public void setHasParamAction(boolean i) { + hasParamAction = i; + } + + public boolean hasParamAction() { + return hasParamAction; + } + + public void setHasSetProperty(boolean s) { + hasSetProperty = s; + } + + public boolean hasSetProperty() { + return hasSetProperty; + } + + public void setHasScriptingVars(boolean s) { + hasScriptingVars = s; + } + + public boolean hasScriptingVars() { + return hasScriptingVars; + } + } + + + public abstract static class ChildInfoBase extends Node { + + private final ChildInfo childInfo = new ChildInfo(); + + ChildInfoBase(String qName, String localName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, Mark start, + Node parent) { + super(qName, localName, attrs, nonTaglibXmlnsAttrs, taglibAttrs, start, parent); + } + + public ChildInfo getChildInfo() { + return childInfo; + } + } + + + /** + * Represents a custom tag + */ + public static class CustomTag extends ChildInfoBase { + + private final String uri; + + private final String prefix; + + private JspAttribute[] jspAttrs; + + private TagData tagData; + + private String tagHandlerPoolName; + + private final TagInfo tagInfo; + + private final TagFileInfo tagFileInfo; + + private Class tagHandlerClass; + + private VariableInfo[] varInfos; + + private final int customNestingLevel; + + private final boolean implementsIterationTag; + + private final boolean implementsBodyTag; + + private final boolean implementsTryCatchFinally; + + private final boolean implementsJspIdConsumer; + + private final boolean implementsSimpleTag; + + private final boolean implementsDynamicAttributes; + + private List atBeginScriptingVars; + + private List atEndScriptingVars; + + private List nestedScriptingVars; + + private Node.CustomTag customTagParent; + + private Integer numCount; + + private boolean useTagPlugin; + + private TagPluginContext tagPluginContext; + + /** + * The following two fields are used for holding the Java scriptlets + * that the tag plugins may generate. Meaningful only if useTagPlugin is + * true; Could move them into TagPluginContextImpl, but we'll need to + * cast tagPluginContext to TagPluginContextImpl all the time... + */ + private Nodes atSTag; + + private Nodes atETag; + + /* + * Constructor for custom action implemented by tag handler. + */ + CustomTag(String qName, String prefix, String localName, + String uri, Attributes attrs, Mark start, Node parent, + TagInfo tagInfo, Class tagHandlerClass) { + this(qName, prefix, localName, uri, attrs, null, null, start, + parent, tagInfo, tagHandlerClass); + } + + /* + * Constructor for custom action implemented by tag handler. + */ + CustomTag(String qName, String prefix, String localName, + String uri, Attributes attrs, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent, + TagInfo tagInfo, Class tagHandlerClass) { + super(qName, localName, attrs, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + + this.uri = uri; + this.prefix = prefix; + this.tagInfo = tagInfo; + this.tagFileInfo = null; + this.tagHandlerClass = tagHandlerClass; + this.customNestingLevel = makeCustomNestingLevel(); + + this.implementsIterationTag = IterationTag.class + .isAssignableFrom(tagHandlerClass); + this.implementsBodyTag = BodyTag.class + .isAssignableFrom(tagHandlerClass); + this.implementsTryCatchFinally = TryCatchFinally.class + .isAssignableFrom(tagHandlerClass); + this.implementsSimpleTag = SimpleTag.class + .isAssignableFrom(tagHandlerClass); + this.implementsDynamicAttributes = DynamicAttributes.class + .isAssignableFrom(tagHandlerClass); + this.implementsJspIdConsumer = JspIdConsumer.class + .isAssignableFrom(tagHandlerClass); + } + + /* + * Constructor for custom action implemented by tag file. + */ + CustomTag(String qName, String prefix, String localName, + String uri, Attributes attrs, Mark start, Node parent, + TagFileInfo tagFileInfo) { + this(qName, prefix, localName, uri, attrs, null, null, start, + parent, tagFileInfo); + } + + /* + * Constructor for custom action implemented by tag file. + */ + CustomTag(String qName, String prefix, String localName, + String uri, Attributes attrs, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent, + TagFileInfo tagFileInfo) { + + super(qName, localName, attrs, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + + this.uri = uri; + this.prefix = prefix; + this.tagFileInfo = tagFileInfo; + this.tagInfo = tagFileInfo.getTagInfo(); + this.customNestingLevel = makeCustomNestingLevel(); + + this.implementsIterationTag = false; + this.implementsBodyTag = false; + this.implementsTryCatchFinally = false; + this.implementsSimpleTag = true; + this.implementsJspIdConsumer = false; + this.implementsDynamicAttributes = tagInfo.hasDynamicAttributes(); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + /** + * @return The URI namespace that this custom action belongs to + */ + public String getURI() { + return this.uri; + } + + /** + * @return The tag prefix + */ + public String getPrefix() { + return prefix; + } + + public void setJspAttributes(JspAttribute[] jspAttrs) { + this.jspAttrs = jspAttrs; + } + + public JspAttribute[] getJspAttributes() { + return jspAttrs; + } + + public void setTagData(TagData tagData) { + this.tagData = tagData; + this.varInfos = tagInfo.getVariableInfo(tagData); + if (this.varInfos == null) { + this.varInfos = ZERO_VARIABLE_INFO; + } + } + + public TagData getTagData() { + return tagData; + } + + public void setTagHandlerPoolName(String s) { + tagHandlerPoolName = s; + } + + public String getTagHandlerPoolName() { + return tagHandlerPoolName; + } + + public TagInfo getTagInfo() { + return tagInfo; + } + + public TagFileInfo getTagFileInfo() { + return tagFileInfo; + } + + /* + * @return true if this custom action is supported by a tag file, false + * otherwise + */ + public boolean isTagFile() { + return tagFileInfo != null; + } + + public Class getTagHandlerClass() { + return tagHandlerClass; + } + + public void setTagHandlerClass(Class hc) { + tagHandlerClass = hc; + } + + public boolean implementsIterationTag() { + return implementsIterationTag; + } + + public boolean implementsBodyTag() { + return implementsBodyTag; + } + + public boolean implementsTryCatchFinally() { + return implementsTryCatchFinally; + } + + public boolean implementsJspIdConsumer() { + return implementsJspIdConsumer; + } + + public boolean implementsSimpleTag() { + return implementsSimpleTag; + } + + public boolean implementsDynamicAttributes() { + return implementsDynamicAttributes; + } + + public TagVariableInfo[] getTagVariableInfos() { + return tagInfo.getTagVariableInfos(); + } + + public VariableInfo[] getVariableInfos() { + return varInfos; + } + + public void setCustomTagParent(Node.CustomTag n) { + this.customTagParent = n; + } + + public Node.CustomTag getCustomTagParent() { + return this.customTagParent; + } + + public void setNumCount(Integer count) { + this.numCount = count; + } + + public Integer getNumCount() { + return this.numCount; + } + + public void setScriptingVars(List vec, int scope) { + switch (scope) { + case VariableInfo.AT_BEGIN: + this.atBeginScriptingVars = vec; + break; + case VariableInfo.AT_END: + this.atEndScriptingVars = vec; + break; + case VariableInfo.NESTED: + this.nestedScriptingVars = vec; + break; + default: + throw new IllegalArgumentException( + Localizer.getMessage("jsp.error.page.invalid.varscope", Integer.valueOf(scope))); + } + } + + /* + * Gets the scripting variables for the given scope that need to be + * declared. + */ + public List getScriptingVars(int scope) { + List vec = null; + + switch (scope) { + case VariableInfo.AT_BEGIN: + vec = this.atBeginScriptingVars; + break; + case VariableInfo.AT_END: + vec = this.atEndScriptingVars; + break; + case VariableInfo.NESTED: + vec = this.nestedScriptingVars; + break; + default: + throw new IllegalArgumentException( + Localizer.getMessage("jsp.error.page.invalid.varscope", Integer.valueOf(scope))); + } + + return vec; + } + + /* + * Gets this custom tag's custom nesting level, which is given as the + * number of times this custom tag is nested inside itself. + */ + public int getCustomNestingLevel() { + return customNestingLevel; + } + + /** + * Checks to see if the attribute of the given name is of type + * JspFragment. + * + * @param name The attribute to check + * + * @return {@code true} if it is a JspFragment + */ + public boolean checkIfAttributeIsJspFragment(String name) { + boolean result = false; + + TagAttributeInfo[] attributes = tagInfo.getAttributes(); + for (TagAttributeInfo attribute : attributes) { + if (attribute.getName().equals(name) + && attribute.isFragment()) { + result = true; + break; + } + } + + return result; + } + + public void setUseTagPlugin(boolean use) { + useTagPlugin = use; + } + + public boolean useTagPlugin() { + return useTagPlugin; + } + + public void setTagPluginContext(TagPluginContext tagPluginContext) { + this.tagPluginContext = tagPluginContext; + } + + public TagPluginContext getTagPluginContext() { + return tagPluginContext; + } + + public void setAtSTag(Nodes sTag) { + atSTag = sTag; + } + + public Nodes getAtSTag() { + return atSTag; + } + + public void setAtETag(Nodes eTag) { + atETag = eTag; + } + + public Nodes getAtETag() { + return atETag; + } + + /* + * Computes this custom tag's custom nesting level, which corresponds to + * the number of times this custom tag is nested inside itself. + * + * Example: + * + * -- nesting level 0 -- nesting level 1 + * -- nesting level 2 -- nesting level 1 + * + * + * @return Custom tag's nesting level + */ + private int makeCustomNestingLevel() { + int n = 0; + Node p = parent; + while (p != null) { + if ((p instanceof Node.CustomTag) + && qName.equals(((Node.CustomTag) p).qName)) { + n++; + } + p = p.parent; + } + return n; + } + + /** + * A custom action is considered to have an empty body if the following + * holds true: - getBody() returns null, or - all immediate children are + * jsp:attribute actions, or - the action's jsp:body is empty. + * + * @return {@code true} if this custom action has an empty body, and + * {@code false} otherwise. + */ + public boolean hasEmptyBody() { + boolean hasEmptyBody = true; + Nodes nodes = getBody(); + if (nodes != null) { + int numChildNodes = nodes.size(); + for (int i = 0; i < numChildNodes; i++) { + Node n = nodes.getNode(i); + if (!(n instanceof NamedAttribute)) { + if (n instanceof JspBody) { + hasEmptyBody = (n.getBody() == null); + } else { + hasEmptyBody = false; + } + break; + } + } + } + + return hasEmptyBody; + } + } + + /** + * Used as a placeholder for the evaluation code of a custom action + * attribute (used by the tag plugin machinery only). + */ + public static class AttributeGenerator extends Node { + private String name; // name of the attribute + + private CustomTag tag; // The tag this attribute belongs to + + AttributeGenerator(Mark start, String name, CustomTag tag) { + super(start, null); + this.name = name; + this.tag = tag; + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public String getName() { + return name; + } + + public CustomTag getTag() { + return tag; + } + } + + /** + * Represents the body of a <jsp:text> element + */ + public static class JspText extends Node { + + JspText(String qName, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent) { + super(qName, TEXT_ACTION, null, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a Named Attribute (<jsp:attribute>) + */ + public static class NamedAttribute extends ChildInfoBase { + + // A unique temporary variable name suitable for code generation + private String temporaryVariableName; + + // True if this node is to be trimmed, or false otherwise + private boolean trim = true; + + // True if this attribute should be omitted from the output if + // used with a , otherwise false + private JspAttribute omit; + + private final String name; + + private String localName; + + private String prefix; + + NamedAttribute(Attributes attrs, Mark start, Node parent) { + this(JSP_ATTRIBUTE_ACTION, attrs, null, null, start, parent); + } + + NamedAttribute(String qName, Attributes attrs, + Attributes nonTaglibXmlnsAttrs, Attributes taglibAttrs, + Mark start, Node parent) { + + super(qName, ATTRIBUTE_ACTION, attrs, nonTaglibXmlnsAttrs, + taglibAttrs, start, parent); + if ("false".equals(this.getAttributeValue("trim"))) { + // (if null or true, leave default of true) + trim = false; + } + name = this.getAttributeValue("name"); + if (name != null) { + // Mandatory attribute "name" will be checked in Validator + localName = name; + int index = name.indexOf(':'); + if (index != -1) { + prefix = name.substring(0, index); + localName = name.substring(index + 1); + } + } + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + public String getName() { + return this.name; + } + + @Override + public String getLocalName() { + return this.localName; + } + + public String getPrefix() { + return this.prefix; + } + + public boolean isTrim() { + return trim; + } + + public void setOmit(JspAttribute omit) { + this.omit = omit; + } + + public JspAttribute getOmit() { + return omit; + } + + /** + * @return A unique temporary variable name to store the result in. + * (this probably could go elsewhere, but it's convenient here) + */ + public String getTemporaryVariableName() { + if (temporaryVariableName == null) { + temporaryVariableName = getRoot().nextTemporaryVariableName(); + } + return temporaryVariableName; + } + + /* + * Get the attribute value from this named attribute (). + * Since this method is only for attributes that are not rtexpr, we can + * assume the body of the jsp:attribute is a template text. + */ + @Override + public String getText() { + + class AttributeVisitor extends Visitor { + private String attrValue = null; + + @Override + public void visit(TemplateText txt) { + attrValue = txt.getText(); + } + + public String getAttrValue() { + return attrValue; + } + } + + // According to JSP 2.0, if the body of the + // action is empty, it is equivalent of specifying "" as the value + // of the attribute. + String text = ""; + if (getBody() != null) { + AttributeVisitor attributeVisitor = new AttributeVisitor(); + try { + getBody().visit(attributeVisitor); + } catch (JasperException e) { + } + text = attributeVisitor.getAttrValue(); + } + + return text; + } + } + + /** + * Represents a JspBody node (<jsp:body>) + */ + public static class JspBody extends ChildInfoBase { + + JspBody(Mark start, Node parent) { + this(JSP_BODY_ACTION, null, null, start, parent); + } + + JspBody(String qName, Attributes nonTaglibXmlnsAttrs, + Attributes taglibAttrs, Mark start, Node parent) { + super(qName, BODY_ACTION, null, nonTaglibXmlnsAttrs, taglibAttrs, + start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + } + + /** + * Represents a template text string + */ + public static class TemplateText extends Node { + + private ArrayList extraSmap = null; + + TemplateText(String text, Mark start, Node parent) { + super(null, null, text, start, parent); + } + + @Override + public void accept(Visitor v) throws JasperException { + v.visit(this); + } + + /** + * Trim all whitespace from the left of the template text + */ + public void ltrim() { + int index = 0; + while ((index < text.length()) && (text.charAt(index) <= ' ')) { + index++; + } + text = text.substring(index); + } + + public void setText(String text) { + this.text = text; + } + + /** + * Trim all whitespace from the right of the template text + */ + public void rtrim() { + int index = text.length(); + while ((index > 0) && (text.charAt(index - 1) <= ' ')) { + index--; + } + text = text.substring(0, index); + } + + /** + * @return true if this template text contains whitespace only. + */ + public boolean isAllSpace() { + boolean isAllSpace = true; + for (int i = 0; i < text.length(); i++) { + if (!Character.isWhitespace(text.charAt(i))) { + isAllSpace = false; + break; + } + } + return isAllSpace; + } + + /** + * Add a source to Java line mapping + * + * @param srcLine + * The position of the source line, relative to the line at + * the start of this node. The corresponding java line is + * assumed to be consecutive, i.e. one more than the last. + */ + public void addSmap(int srcLine) { + if (extraSmap == null) { + extraSmap = new ArrayList<>(); + } + extraSmap.add(Integer.valueOf(srcLine)); + } + + public ArrayList getExtraSmap() { + return extraSmap; + } + } + + /** + * Represents attributes that can be request time expressions. + * + * Can either be a plain attribute, an attribute that represents a request + * time expression value, or a named attribute (specified using the + * jsp:attribute standard action). + */ + + public static class JspAttribute { + + private final String qName; + + private final String uri; + + private final String localName; + + private final String value; + + private final boolean expression; + + private final boolean dynamic; + + private final ELNode.Nodes el; + + private final TagAttributeInfo tai; + + // If true, this JspAttribute represents a + private final boolean namedAttribute; + + // The node in the parse tree for the NamedAttribute + private final NamedAttribute namedAttributeNode; + + JspAttribute(TagAttributeInfo tai, String qName, String uri, + String localName, String value, boolean expr, ELNode.Nodes el, + boolean dyn) { + this.qName = qName; + this.uri = uri; + this.localName = localName; + this.value = value; + this.namedAttributeNode = null; + this.expression = expr; + this.el = el; + this.dynamic = dyn; + this.namedAttribute = false; + this.tai = tai; + } + + /** + * Allow node to validate itself. + * + * @param ef The expression factory to use to evaluate any EL + * @param ctx The context to use to evaluate any EL + * + * @throws ELException If validation fails + */ + public void validateEL(ExpressionFactory ef, ELContext ctx) + throws ELException { + if (this.el != null) { + // determine exact type + ef.createValueExpression(ctx, this.value, String.class); + } + } + + /** + * Use this constructor if the JspAttribute represents a named + * attribute. In this case, we have to store the nodes of the body of + * the attribute. + */ + JspAttribute(NamedAttribute na, TagAttributeInfo tai, boolean dyn) { + this.qName = na.getName(); + this.localName = na.getLocalName(); + this.value = null; + this.namedAttributeNode = na; + this.expression = false; + this.el = null; + this.dynamic = dyn; + this.namedAttribute = true; + this.tai = tai; + this.uri = null; + } + + /** + * @return The name of the attribute + */ + public String getName() { + return qName; + } + + /** + * @return The local name of the attribute + */ + public String getLocalName() { + return localName; + } + + /** + * @return The namespace of the attribute, or null if in the default + * namespace + */ + public String getURI() { + return uri; + } + + public TagAttributeInfo getTagAttributeInfo() { + return this.tai; + } + + /** + * @return return true if there's TagAttributeInfo meaning we need to + * assign a ValueExpression + */ + public boolean isDeferredInput() { + return (this.tai != null) ? this.tai.isDeferredValue() : false; + } + + /** + * @return return true if there's TagAttributeInfo meaning we need to + * assign a MethodExpression + */ + public boolean isDeferredMethodInput() { + return (this.tai != null) ? this.tai.isDeferredMethod() : false; + } + + public String getExpectedTypeName() { + if (this.tai != null) { + if (this.isDeferredInput()) { + return this.tai.getExpectedTypeName(); + } else if (this.isDeferredMethodInput()) { + String m = this.tai.getMethodSignature(); + if (m != null) { + int rti = m.trim().indexOf(' '); + if (rti > 0) { + return m.substring(0, rti).trim(); + } + } + } + } + return "java.lang.Object"; + } + + public String[] getParameterTypeNames() { + if (this.tai != null) { + if (this.isDeferredMethodInput()) { + String m = this.tai.getMethodSignature(); + if (m != null) { + m = m.trim(); + m = m.substring(m.indexOf('(') + 1); + m = m.substring(0, m.length() - 1); + if (m.trim().length() > 0) { + String[] p = m.split(","); + for (int i = 0; i < p.length; i++) { + p[i] = p[i].trim(); + } + return p; + } + } + } + } + return new String[0]; + } + + /** + * Only makes sense if namedAttribute is false. + * + * @return the value for the attribute, or the expression string + * (stripped of "<%=", "%>", "%=", or "%" but containing "${" + * and "}" for EL expressions) + */ + public String getValue() { + return value; + } + + /** + * Only makes sense if namedAttribute is true. + * + * @return the nodes that evaluate to the body of this attribute. + */ + public NamedAttribute getNamedAttributeNode() { + return namedAttributeNode; + } + + /** + * @return true if the value represents a traditional rtexprvalue + */ + public boolean isExpression() { + return expression; + } + + /** + * @return true if the value represents a NamedAttribute value. + */ + public boolean isNamedAttribute() { + return namedAttribute; + } + + /** + * @return true if the value represents an expression that should be fed + * to the expression interpreter + * false for string literals or rtexprvalues that should not be + * interpreted or reevaluated + */ + public boolean isELInterpreterInput() { + return el != null || this.isDeferredInput() + || this.isDeferredMethodInput(); + } + + /** + * @return true if the value is a string literal known at translation + * time. + */ + public boolean isLiteral() { + return !expression && (el == null) && !namedAttribute; + } + + /** + * @return {@code true} if the attribute is a "dynamic" attribute of a + * custom tag that implements DynamicAttributes interface. That is, + * a random extra attribute that is not declared by the tag. + */ + public boolean isDynamic() { + return dynamic; + } + + public ELNode.Nodes getEL() { + return el; + } + } + + /** + * An ordered list of Node, used to represent the body of an element, or a + * jsp page of jsp document. + */ + public static class Nodes { + + private final List list; + + private Node.Root root; // null if this is not a page + + private boolean generatedInBuffer; + + Nodes() { + list = new ArrayList<>(); + } + + Nodes(Node.Root root) { + this.root = root; + list = new ArrayList<>(); + list.add(root); + } + + /** + * Appends a node to the list + * + * @param n + * The node to add + */ + public void add(Node n) { + list.add(n); + root = null; + } + + /** + * Removes the given node from the list. + * + * @param n + * The node to be removed + */ + public void remove(Node n) { + list.remove(n); + } + + /** + * Visit the nodes in the list with the supplied visitor + * + * @param v + * The visitor used + * + * @throws JasperException if an error occurs while visiting a node + */ + public void visit(Visitor v) throws JasperException { + for (Node n : list) { + n.accept(v); + } + } + + public int size() { + return list.size(); + } + + public Node getNode(int index) { + Node n = null; + try { + n = list.get(index); + } catch (ArrayIndexOutOfBoundsException e) { + } + return n; + } + + public Node.Root getRoot() { + return root; + } + + public boolean isGeneratedInBuffer() { + return generatedInBuffer; + } + + public void setGeneratedInBuffer(boolean g) { + generatedInBuffer = g; + } + } + + /** + * A visitor class for visiting the node. This class also provides the + * default action (i.e. nop) for each of the child class of the Node. An + * actual visitor should extend this class and supply the visit method for + * the nodes that it cares. + */ + public static class Visitor { + + /** + * This method provides a place to put actions that are common to all + * nodes. Override this in the child visitor class if need to. + * + * @param n The node to visit + */ + @SuppressWarnings("unused") + protected void doVisit(Node n) throws JasperException { + // NOOP by default + } + + /** + * Visit the body of a node, using the current visitor + * + * @param n The node to visit + */ + protected void visitBody(Node n) throws JasperException { + if (n.getBody() != null) { + n.getBody().visit(this); + } + } + + public void visit(Root n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(JspRoot n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(PageDirective n) throws JasperException { + doVisit(n); + } + + public void visit(TagDirective n) throws JasperException { + doVisit(n); + } + + public void visit(IncludeDirective n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(TaglibDirective n) throws JasperException { + doVisit(n); + } + + public void visit(AttributeDirective n) throws JasperException { + doVisit(n); + } + + public void visit(VariableDirective n) throws JasperException { + doVisit(n); + } + + public void visit(Comment n) throws JasperException { + doVisit(n); + } + + public void visit(Declaration n) throws JasperException { + doVisit(n); + } + + public void visit(Expression n) throws JasperException { + doVisit(n); + } + + public void visit(Scriptlet n) throws JasperException { + doVisit(n); + } + + public void visit(ELExpression n) throws JasperException { + doVisit(n); + } + + public void visit(IncludeAction n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(ForwardAction n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(GetProperty n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(SetProperty n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(ParamAction n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(ParamsAction n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(FallBackAction n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(UseBean n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(PlugIn n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(CustomTag n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(UninterpretedTag n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(JspElement n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(JspText n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(NamedAttribute n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(JspBody n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(InvokeAction n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(DoBodyAction n) throws JasperException { + doVisit(n); + visitBody(n); + } + + public void visit(TemplateText n) throws JasperException { + doVisit(n); + } + + public void visit(JspOutput n) throws JasperException { + doVisit(n); + } + + public void visit(AttributeGenerator n) throws JasperException { + doVisit(n); + } + } +} diff --git a/java/org/apache/jasper/compiler/PageDataImpl.java b/java/org/apache/jasper/compiler/PageDataImpl.java new file mode 100644 index 0000000..2c6219a --- /dev/null +++ b/java/org/apache/jasper/compiler/PageDataImpl.java @@ -0,0 +1,750 @@ +/* + * 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.jasper.compiler; + +import java.io.ByteArrayInputStream; +import java.io.CharArrayWriter; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.jsp.tagext.PageData; + +import org.apache.jasper.JasperException; +import org.apache.tomcat.util.security.Escape; +import org.xml.sax.Attributes; +import org.xml.sax.helpers.AttributesImpl; + +/** + * An implementation of jakarta.servlet.jsp.tagext.PageData which + * builds the XML view of a given page. + * + * The XML view is built in two passes: + * + * During the first pass, the FirstPassVisitor collects the attributes of the + * top-level jsp:root and those of the jsp:root elements of any included + * pages, and adds them to the jsp:root element of the XML view. + * In addition, any taglib directives are converted into xmlns: attributes and + * added to the jsp:root element of the XML view. + * This pass ignores any nodes other than JspRoot and TaglibDirective. + * + * During the second pass, the SecondPassVisitor produces the XML view, using + * the combined jsp:root attributes determined in the first pass and any + * remaining pages nodes (this pass ignores any JspRoot and TaglibDirective + * nodes). + * + * @author Jan Luehe + */ +class PageDataImpl extends PageData implements TagConstants { + + private static final String JSP_VERSION = "2.0"; + private static final String CDATA_START_SECTION = "\n"; + + // string buffer used to build XML view + private final StringBuilder buf; + + /** + * @param page the page nodes from which to generate the XML view + * @param compiler The compiler for this page + * + * @throws JasperException If an error occurs + */ + PageDataImpl(Node.Nodes page, Compiler compiler) + throws JasperException { + + // First pass + FirstPassVisitor firstPass = new FirstPassVisitor(page.getRoot(), + compiler.getPageInfo()); + page.visit(firstPass); + + // Second pass + buf = new StringBuilder(); + SecondPassVisitor secondPass + = new SecondPassVisitor(page.getRoot(), buf, compiler, + firstPass.getJspIdPrefix()); + page.visit(secondPass); + } + + /** + * Returns the input stream of the XML view. + * + * @return the input stream of the XML view + */ + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream( + buf.toString().getBytes(StandardCharsets.UTF_8)); + } + + /* + * First-pass Visitor for JspRoot nodes (representing jsp:root elements) + * and TablibDirective nodes, ignoring any other nodes. + * + * The purpose of this Visitor is to collect the attributes of the + * top-level jsp:root and those of the jsp:root elements of any included + * pages, and add them to the jsp:root element of the XML view. + * In addition, this Visitor converts any taglib directives into xmlns: + * attributes and adds them to the jsp:root element of the XML view. + */ + private static class FirstPassVisitor + extends Node.Visitor implements TagConstants { + + private final Node.Root root; + private final AttributesImpl rootAttrs; + private final PageInfo pageInfo; + + // Prefix for the 'id' attribute + private String jspIdPrefix; + + /* + * Constructor + */ + FirstPassVisitor(Node.Root root, PageInfo pageInfo) { + this.root = root; + this.pageInfo = pageInfo; + this.rootAttrs = new AttributesImpl(); + this.rootAttrs.addAttribute("", "", "version", "CDATA", + JSP_VERSION); + this.jspIdPrefix = "jsp"; + } + + @Override + public void visit(Node.Root n) throws JasperException { + visitBody(n); + if (n == root) { + /* + * Top-level page. + * + * Add + * xmlns:jsp="http://java.sun.com/JSP/Page" + * attribute only if not already present. + */ + if (!JSP_URI.equals(rootAttrs.getValue("xmlns:jsp"))) { + rootAttrs.addAttribute("", "", "xmlns:jsp", "CDATA", + JSP_URI); + } + + if (pageInfo.isJspPrefixHijacked()) { + /* + * 'jsp' prefix has been hijacked, that is, bound to a + * namespace other than the JSP namespace. This means that + * when adding an 'id' attribute to each element, we can't + * use the 'jsp' prefix. Therefore, create a new prefix + * (one that is unique across the translation unit) for use + * by the 'id' attribute, and bind it to the JSP namespace + */ + jspIdPrefix += "jsp"; + while (pageInfo.containsPrefix(jspIdPrefix)) { + jspIdPrefix += "jsp"; + } + rootAttrs.addAttribute("", "", "xmlns:" + jspIdPrefix, + "CDATA", JSP_URI); + } + + root.setAttributes(rootAttrs); + } + } + + @Override + public void visit(Node.JspRoot n) throws JasperException { + addAttributes(n.getTaglibAttributes()); + addAttributes(n.getNonTaglibXmlnsAttributes()); + addAttributes(n.getAttributes()); + + visitBody(n); + } + + /* + * Converts taglib directive into "xmlns:..." attribute of jsp:root + * element. + */ + @Override + public void visit(Node.TaglibDirective n) throws JasperException { + Attributes attrs = n.getAttributes(); + if (attrs != null) { + String qName = "xmlns:" + attrs.getValue("prefix"); + /* + * According to javadocs of org.xml.sax.helpers.AttributesImpl, + * the addAttribute method does not check to see if the + * specified attribute is already contained in the list: This + * is the application's responsibility! + */ + if (rootAttrs.getIndex(qName) == -1) { + String location = attrs.getValue("uri"); + if (location != null) { + if (location.startsWith("/")) { + location = URN_JSPTLD + location; + } + rootAttrs.addAttribute("", "", qName, "CDATA", + location); + } else { + location = attrs.getValue("tagdir"); + rootAttrs.addAttribute("", "", qName, "CDATA", + URN_JSPTAGDIR + location); + } + } + } + } + + public String getJspIdPrefix() { + return jspIdPrefix; + } + + private void addAttributes(Attributes attrs) { + if (attrs != null) { + int len = attrs.getLength(); + + for (int i=0; i"); + } + buf.append("${"); + buf.append(Escape.xml(n.getText())); + buf.append('}'); + if (!n.getRoot().isXmlSyntax()) { + buf.append(JSP_TEXT_ACTION_END); + } + buf.append("\n"); + } + + @Override + public void visit(Node.IncludeAction n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.ForwardAction n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.GetProperty n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.SetProperty n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.ParamAction n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.ParamsAction n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.FallBackAction n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.UseBean n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.PlugIn n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.NamedAttribute n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.JspBody n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + boolean resetDefaultNSSave = resetDefaultNS; + appendTag(n, resetDefaultNS); + resetDefaultNS = resetDefaultNSSave; + } + + @Override + public void visit(Node.UninterpretedTag n) throws JasperException { + boolean resetDefaultNSSave = resetDefaultNS; + appendTag(n, resetDefaultNS); + resetDefaultNS = resetDefaultNSSave; + } + + @Override + public void visit(Node.JspText n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.DoBodyAction n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.InvokeAction n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.TagDirective n) throws JasperException { + appendTagDirective(n); + } + + @Override + public void visit(Node.AttributeDirective n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.VariableDirective n) throws JasperException { + appendTag(n); + } + + @Override + public void visit(Node.TemplateText n) throws JasperException { + /* + * If the template text came from a JSP page written in JSP syntax, + * create a jsp:text element for it (JSP 5.3.2). + */ + appendText(n.getText(), !n.getRoot().isXmlSyntax()); + } + + /* + * Appends the given tag, including its body, to the XML view. + */ + private void appendTag(Node n) throws JasperException { + appendTag(n, false); + } + + /* + * Appends the given tag, including its body, to the XML view, + * and optionally reset default namespace to "", if none specified. + */ + private void appendTag(Node n, boolean addDefaultNS) + throws JasperException { + + Node.Nodes body = n.getBody(); + String text = n.getText(); + + buf.append('<').append(n.getQName()); + buf.append("\n"); + + printAttributes(n, addDefaultNS); + buf.append(" ").append(jspIdPrefix).append(":id").append("=\""); + buf.append(jspId++).append("\"\n"); + + if (ROOT_ACTION.equals(n.getLocalName()) || body != null + || text != null) { + buf.append(">\n"); + if (ROOT_ACTION.equals(n.getLocalName())) { + if (compiler.getCompilationContext().isTagFile()) { + appendTagDirective(); + } else { + appendPageDirective(); + } + } + if (body != null) { + body.visit(this); + } else { + appendText(text, false); + } + buf.append("\n"); + } else { + buf.append("/>\n"); + } + } + + /* + * Appends the page directive with the given attributes to the XML + * view. + * + * Since the import attribute of the page directive is the only page + * attribute that is allowed to appear multiple times within the same + * document, and since XML allows only single-value attributes, + * the values of multiple import attributes must be combined into one, + * separated by comma. + * + * If the given page directive contains just 'contentType' and/or + * 'pageEncoding' attributes, we ignore it, as we've already appended + * a page directive containing just these two attributes. + */ + private void appendPageDirective(Node.PageDirective n) { + boolean append = false; + Attributes attrs = n.getAttributes(); + int len = (attrs == null) ? 0 : attrs.getLength(); + for (int i=0; i 0) { + // Concatenate names of imported classes/packages + boolean first = true; + for (String i : n.getImports()) { + if (first) { + first = false; + buf.append(" import=\""); + } else { + buf.append(','); + } + buf.append(JspUtil.getExprInXml(i)); + } + buf.append("\"\n"); + } + buf.append("/>\n"); + } + + /* + * Appends a page directive with 'pageEncoding' and 'contentType' + * attributes. + * + * The value of the 'pageEncoding' attribute is hard-coded + * to UTF-8, whereas the value of the 'contentType' attribute, which + * is identical to what the container will pass to + * ServletResponse.setContentType(), is derived from the pageInfo. + */ + private void appendPageDirective() { + buf.append('<').append(JSP_PAGE_DIRECTIVE_ACTION); + buf.append("\n"); + + // append jsp:id + buf.append(" ").append(jspIdPrefix).append(":id").append("=\""); + buf.append(jspId++).append("\"\n"); + buf.append(" ").append("pageEncoding").append("=\"UTF-8\"\n"); + buf.append(" ").append("contentType").append("=\""); + buf.append(compiler.getPageInfo().getContentType()).append("\"\n"); + buf.append("/>\n"); + } + + /* + * Appends the tag directive with the given attributes to the XML + * view. + * + * If the given tag directive contains just a 'pageEncoding' + * attributes, we ignore it, as we've already appended + * a tag directive containing just this attributes. + */ + private void appendTagDirective(Node.TagDirective n) + throws JasperException { + + boolean append = false; + Attributes attrs = n.getAttributes(); + int len = (attrs == null) ? 0 : attrs.getLength(); + for (int i=0; i\n"); + } + + private void appendText(String text, boolean createJspTextElement) { + if (createJspTextElement) { + buf.append('<').append(JSP_TEXT_ACTION); + buf.append("\n"); + + // append jsp:id + buf.append(" ").append(jspIdPrefix).append(":id").append("=\""); + buf.append(jspId++).append("\"\n"); + buf.append(">\n"); + + appendCDATA(text); + buf.append(JSP_TEXT_ACTION_END); + buf.append("\n"); + } else { + appendCDATA(text); + } + } + + /* + * Appends the given text as a CDATA section to the XML view, unless + * the text has already been marked as CDATA. + */ + private void appendCDATA(String text) { + buf.append(CDATA_START_SECTION); + buf.append(escapeCDATA(text)); + buf.append(CDATA_END_SECTION); + } + + /* + * Escapes any occurrences of "]]>" (by replacing them with "]]>") + * within the given text, so it can be included in a CDATA section. + */ + private String escapeCDATA(String text) { + if( text==null ) { + return ""; + } + int len = text.length(); + CharArrayWriter result = new CharArrayWriter(len); + for (int i=0; i')) { + // match found + result.write(']'); + result.write(']'); + result.write('&'); + result.write('g'); + result.write('t'); + result.write(';'); + i += 2; + } else { + result.write(text.charAt(i)); + } + } + return result.toString(); + } + + /* + * Appends the attributes of the given Node to the XML view. + */ + private void printAttributes(Node n, boolean addDefaultNS) { + + /* + * Append "xmlns" attributes that represent tag libraries + */ + Attributes attrs = n.getTaglibAttributes(); + int len = (attrs == null) ? 0 : attrs.getLength(); + for (int i=0; i\n"); + } + } +} + diff --git a/java/org/apache/jasper/compiler/PageInfo.java b/java/org/apache/jasper/compiler/PageInfo.java new file mode 100644 index 0000000..9afa659 --- /dev/null +++ b/java/org/apache/jasper/compiler/PageInfo.java @@ -0,0 +1,779 @@ +/* + * 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.jasper.compiler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.el.ExpressionFactory; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; + +import org.apache.jasper.Constants; +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; + +/** + * A repository for various info about the translation unit under compilation. + * + * @author Kin-man Chung + */ + +class PageInfo { + + private final List imports; + private final Map dependants; + + private final BeanRepository beanRepository; + private final Set varInfoNames; + private final HashMap taglibsMap; + private final HashMap jspPrefixMapper; + private final HashMap> xmlPrefixMapper; + private final HashMap nonCustomTagPrefixMap; + private final String jspFile; + private static final String defaultLanguage = "java"; + private String language; + private final String defaultExtends; + private String xtends; + private String contentType = null; + private String session; + private boolean isSession = true; + private String bufferValue; + private int buffer = 8*1024; + private String autoFlush; + private boolean isAutoFlush = true; + private String isThreadSafeValue; + private boolean isThreadSafe = true; + private String isErrorPageValue; + private boolean isErrorPage = false; + private String errorPage = null; + private String info; + + private boolean scriptless = false; + private boolean scriptingInvalid = false; + + private String isELIgnoredValue; + private boolean isELIgnored = false; + + // JSP 2.1 + private String deferredSyntaxAllowedAsLiteralValue; + private boolean deferredSyntaxAllowedAsLiteral = false; + private final ExpressionFactory expressionFactory = + ExpressionFactory.newInstance(); + private String trimDirectiveWhitespacesValue; + private boolean trimDirectiveWhitespaces = false; + + private String omitXmlDecl = null; + private String doctypeName = null; + private String doctypePublic = null; + private String doctypeSystem = null; + + private boolean isJspPrefixHijacked; + + // Set of all element and attribute prefixes used in this translation unit + private final HashSet prefixes; + + private boolean hasJspRoot = false; + private Collection includePrelude; + private Collection includeCoda; + private final List pluginDcls; // Id's for tagplugin declarations + + // JSP 2.2 + private boolean errorOnUndeclaredNamespace = false; + + // JSP 3.1 + private String errorOnELNotFoundValue; + private boolean errorOnELNotFound = false; + + private final boolean isTagFile; + + PageInfo(BeanRepository beanRepository, JspCompilationContext ctxt) { + isTagFile = ctxt.isTagFile(); + jspFile = ctxt.getJspFile(); + defaultExtends = ctxt.getOptions().getJspServletBase(); + this.beanRepository = beanRepository; + this.varInfoNames = new HashSet<>(); + this.taglibsMap = new HashMap<>(); + this.jspPrefixMapper = new HashMap<>(); + this.xmlPrefixMapper = new HashMap<>(); + this.nonCustomTagPrefixMap = new HashMap<>(); + this.dependants = new HashMap<>(); + this.includePrelude = new ArrayList<>(); + this.includeCoda = new ArrayList<>(); + this.pluginDcls = new ArrayList<>(); + this.prefixes = new HashSet<>(); + + // Enter standard imports + this.imports = new ArrayList<>(Constants.STANDARD_IMPORTS); + } + + public boolean isTagFile() { + return isTagFile; + } + + /** + * Check if the plugin ID has been previously declared. Make a note + * that this Id is now declared. + * + * @param id The plugin ID to check + * + * @return true if Id has been declared. + */ + public boolean isPluginDeclared(String id) { + if (pluginDcls.contains(id)) { + return true; + } + pluginDcls.add(id); + return false; + } + + public void addImports(List imports) { + this.imports.addAll(imports); + } + + public void addImport(String imp) { + this.imports.add(imp); + } + + public List getImports() { + return imports; + } + + public String getJspFile() { + return jspFile; + } + + public void addDependant(String d, Long lastModified) { + if (!dependants.containsKey(d) && !jspFile.equals(d)) { + dependants.put(d, lastModified); + } + } + + public Map getDependants() { + return dependants; + } + + public BeanRepository getBeanRepository() { + return beanRepository; + } + + public void setScriptless(boolean s) { + scriptless = s; + } + + public boolean isScriptless() { + return scriptless; + } + + public void setScriptingInvalid(boolean s) { + scriptingInvalid = s; + } + + public boolean isScriptingInvalid() { + return scriptingInvalid; + } + + public Collection getIncludePrelude() { + return includePrelude; + } + + public void setIncludePrelude(Collection prelude) { + includePrelude = prelude; + } + + public Collection getIncludeCoda() { + return includeCoda; + } + + public void setIncludeCoda(Collection coda) { + includeCoda = coda; + } + + public void setHasJspRoot(boolean s) { + hasJspRoot = s; + } + + public boolean hasJspRoot() { + return hasJspRoot; + } + + public String getOmitXmlDecl() { + return omitXmlDecl; + } + + public void setOmitXmlDecl(String omit) { + omitXmlDecl = omit; + } + + public String getDoctypeName() { + return doctypeName; + } + + public void setDoctypeName(String doctypeName) { + this.doctypeName = doctypeName; + } + + public String getDoctypeSystem() { + return doctypeSystem; + } + + public void setDoctypeSystem(String doctypeSystem) { + this.doctypeSystem = doctypeSystem; + } + + public String getDoctypePublic() { + return doctypePublic; + } + + public void setDoctypePublic(String doctypePublic) { + this.doctypePublic = doctypePublic; + } + + /* Tag library and XML namespace management methods */ + + public void setIsJspPrefixHijacked(boolean isHijacked) { + isJspPrefixHijacked = isHijacked; + } + + public boolean isJspPrefixHijacked() { + return isJspPrefixHijacked; + } + + /* + * Adds the given prefix to the set of prefixes of this translation unit. + * + * @param prefix The prefix to add + */ + public void addPrefix(String prefix) { + prefixes.add(prefix); + } + + /* + * Checks to see if this translation unit contains the given prefix. + * + * @param prefix The prefix to check + * + * @return true if this translation unit contains the given prefix, false + * otherwise + */ + public boolean containsPrefix(String prefix) { + return prefixes.contains(prefix); + } + + /* + * Maps the given URI to the given tag library. + * + * @param uri The URI to map + * @param info The tag library to be associated with the given URI + */ + public void addTaglib(String uri, TagLibraryInfo info) { + taglibsMap.put(uri, info); + } + + /* + * Gets the tag library corresponding to the given URI. + * + * @return Tag library corresponding to the given URI + */ + public TagLibraryInfo getTaglib(String uri) { + return taglibsMap.get(uri); + } + + /* + * Gets the collection of tag libraries that are associated with a URI + * + * @return Collection of tag libraries that are associated with a URI + */ + public Collection getTaglibs() { + return taglibsMap.values(); + } + + /* + * Checks to see if the given URI is mapped to a tag library. + * + * @param uri The URI to map + * + * @return true if the given URI is mapped to a tag library, false + * otherwise + */ + public boolean hasTaglib(String uri) { + return taglibsMap.containsKey(uri); + } + + /* + * Maps the given prefix to the given URI. + * + * @param prefix The prefix to map + * @param uri The URI to be associated with the given prefix + */ + public void addPrefixMapping(String prefix, String uri) { + jspPrefixMapper.put(prefix, uri); + } + + /* + * Pushes the given URI onto the stack of URIs to which the given prefix + * is mapped. + * + * @param prefix The prefix whose stack of URIs is to be pushed + * @param uri The URI to be pushed onto the stack + */ + public void pushPrefixMapping(String prefix, String uri) { + // Must be LinkedList as it needs to accept nulls + xmlPrefixMapper.computeIfAbsent(prefix, k -> new LinkedList<>()).addFirst(uri); + } + + /* + * Removes the URI at the top of the stack of URIs to which the given + * prefix is mapped. + * + * @param prefix The prefix whose stack of URIs is to be popped + */ + public void popPrefixMapping(String prefix) { + Deque stack = xmlPrefixMapper.get(prefix); + stack.removeFirst(); + } + + /* + * Returns the URI to which the given prefix maps. + * + * @param prefix The prefix whose URI is sought + * + * @return The URI to which the given prefix maps + */ + public String getURI(String prefix) { + + String uri = null; + + Deque stack = xmlPrefixMapper.get(prefix); + if (stack == null || stack.size() == 0) { + uri = jspPrefixMapper.get(prefix); + } else { + uri = stack.getFirst(); + } + + return uri; + } + + + /* Page/Tag directive attributes */ + + /* + * language + */ + public void setLanguage(String value, Node n, ErrorDispatcher err, + boolean pagedir) + throws JasperException { + + if (!"java".equalsIgnoreCase(value)) { + if (pagedir) { + err.jspError(n, "jsp.error.page.language.nonjava"); + } else { + err.jspError(n, "jsp.error.tag.language.nonjava"); + } + } + + language = value; + } + + public String getLanguage(boolean useDefault) { + return (language == null && useDefault ? defaultLanguage : language); + } + + /* + * extends + */ + public void setExtends(String value) { + xtends = value; + } + + /** + * Gets the value of the 'extends' page directive attribute. + * + * @param useDefault TRUE if the default + * (org.apache.jasper.runtime.HttpJspBase) should be returned if this + * attribute has not been set, FALSE otherwise + * + * @return The value of the 'extends' page directive attribute, or the + * default (org.apache.jasper.runtime.HttpJspBase) if this attribute has + * not been set and useDefault is TRUE + */ + public String getExtends(boolean useDefault) { + return (xtends == null && useDefault ? defaultExtends : xtends); + } + + /** + * Gets the value of the 'extends' page directive attribute. + * + * @return The value of the 'extends' page directive attribute, or the + * default (org.apache.jasper.runtime.HttpJspBase) if this attribute has + * not been set + */ + public String getExtends() { + return getExtends(true); + } + + + /* + * contentType + */ + public void setContentType(String value) { + contentType = value; + } + + public String getContentType() { + return contentType; + } + + + /* + * buffer + */ + public void setBufferValue(String value, Node n, ErrorDispatcher err) + throws JasperException { + + if ("none".equalsIgnoreCase(value)) { + buffer = 0; + } else { + if (value == null || !value.endsWith("kb")) { + if (n == null) { + err.jspError("jsp.error.page.invalid.buffer"); + } else { + err.jspError(n, "jsp.error.page.invalid.buffer"); + } + } + try { + @SuppressWarnings("null") // value can't be null here + int k = Integer.parseInt(value.substring(0, value.length()-2)); + buffer = k * 1024; + } catch (NumberFormatException e) { + if (n == null) { + err.jspError("jsp.error.page.invalid.buffer"); + } else { + err.jspError(n, "jsp.error.page.invalid.buffer"); + } + } + } + + bufferValue = value; + } + + public String getBufferValue() { + return bufferValue; + } + + public int getBuffer() { + return buffer; + } + + + /* + * session + */ + public void setSession(String value, Node n, ErrorDispatcher err) + throws JasperException { + + if ("true".equalsIgnoreCase(value)) { + isSession = true; + } else if ("false".equalsIgnoreCase(value)) { + isSession = false; + } else { + err.jspError(n, "jsp.error.page.invalid.session"); + } + + session = value; + } + + public String getSession() { + return session; + } + + public boolean isSession() { + return isSession; + } + + + /* + * autoFlush + */ + public void setAutoFlush(String value, Node n, ErrorDispatcher err) + throws JasperException { + + if ("true".equalsIgnoreCase(value)) { + isAutoFlush = true; + } else if ("false".equalsIgnoreCase(value)) { + isAutoFlush = false; + } else { + err.jspError(n, "jsp.error.autoFlush.invalid"); + } + + autoFlush = value; + } + + public String getAutoFlush() { + return autoFlush; + } + + public boolean isAutoFlush() { + return isAutoFlush; + } + + + /* + * isThreadSafe + */ + public void setIsThreadSafe(String value, Node n, ErrorDispatcher err) + throws JasperException { + + if ("true".equalsIgnoreCase(value)) { + isThreadSafe = true; + } else if ("false".equalsIgnoreCase(value)) { + isThreadSafe = false; + } else { + err.jspError(n, "jsp.error.page.invalid.isthreadsafe"); + } + + isThreadSafeValue = value; + } + + public String getIsThreadSafe() { + return isThreadSafeValue; + } + + public boolean isThreadSafe() { + return isThreadSafe; + } + + + /* + * info + */ + public void setInfo(String value) { + info = value; + } + + public String getInfo() { + return info; + } + + + /* + * errorPage + */ + public void setErrorPage(String value) { + errorPage = value; + } + + public String getErrorPage() { + return errorPage; + } + + + /* + * isErrorPage + */ + public void setIsErrorPage(String value, Node n, ErrorDispatcher err) + throws JasperException { + + if ("true".equalsIgnoreCase(value)) { + isErrorPage = true; + } else if ("false".equalsIgnoreCase(value)) { + isErrorPage = false; + } else { + err.jspError(n, "jsp.error.page.invalid.iserrorpage"); + } + + isErrorPageValue = value; + } + + public String getIsErrorPage() { + return isErrorPageValue; + } + + public boolean isErrorPage() { + return isErrorPage; + } + + + /* + * isELIgnored + */ + public void setIsELIgnored(String value, Node n, ErrorDispatcher err, + boolean pagedir) + throws JasperException { + + if ("true".equalsIgnoreCase(value)) { + isELIgnored = true; + } else if ("false".equalsIgnoreCase(value)) { + isELIgnored = false; + } else { + if (pagedir) { + err.jspError(n, "jsp.error.page.invalid.iselignored"); + } else { + err.jspError(n, "jsp.error.tag.invalid.iselignored"); + } + } + + isELIgnoredValue = value; + } + + + /* + * errorOnELNotFound + */ + public void setErrorOnELNotFound(String value, Node n, ErrorDispatcher err, + boolean pagedir) + throws JasperException { + + if ("true".equalsIgnoreCase(value)) { + errorOnELNotFound = true; + } else if ("false".equalsIgnoreCase(value)) { + errorOnELNotFound = false; + } else { + if (pagedir) { + err.jspError(n, "jsp.error.page.invalid.errorOnELNotFound"); + } else { + err.jspError(n, "jsp.error.tag.invalid.errorOnELNotFound"); + } + } + + errorOnELNotFoundValue = value; + } + + + /* + * deferredSyntaxAllowedAsLiteral + */ + public void setDeferredSyntaxAllowedAsLiteral(String value, Node n, ErrorDispatcher err, + boolean pagedir) + throws JasperException { + + if ("true".equalsIgnoreCase(value)) { + deferredSyntaxAllowedAsLiteral = true; + } else if ("false".equalsIgnoreCase(value)) { + deferredSyntaxAllowedAsLiteral = false; + } else { + if (pagedir) { + err.jspError(n, "jsp.error.page.invalid.deferredsyntaxallowedasliteral"); + } else { + err.jspError(n, "jsp.error.tag.invalid.deferredsyntaxallowedasliteral"); + } + } + + deferredSyntaxAllowedAsLiteralValue = value; + } + + /* + * trimDirectiveWhitespaces + */ + public void setTrimDirectiveWhitespaces(String value, Node n, ErrorDispatcher err, + boolean pagedir) + throws JasperException { + + if ("true".equalsIgnoreCase(value)) { + trimDirectiveWhitespaces = true; + } else if ("false".equalsIgnoreCase(value)) { + trimDirectiveWhitespaces = false; + } else { + if (pagedir) { + err.jspError(n, "jsp.error.page.invalid.trimdirectivewhitespaces"); + } else { + err.jspError(n, "jsp.error.tag.invalid.trimdirectivewhitespaces"); + } + } + + trimDirectiveWhitespacesValue = value; + } + + public void setELIgnored(boolean s) { + isELIgnored = s; + } + + public String getIsELIgnored() { + return isELIgnoredValue; + } + + public boolean isELIgnored() { + return isELIgnored; + } + + public void setErrorOnELNotFound(boolean s) { + errorOnELNotFound = s; + } + + public String getErrorOnELNotFound() { + return errorOnELNotFoundValue; + } + + public boolean isErrorOnELNotFound() { + return errorOnELNotFound; + } + + public void putNonCustomTagPrefix(String prefix, Mark where) { + nonCustomTagPrefixMap.put(prefix, where); + } + + public Mark getNonCustomTagPrefix(String prefix) { + return nonCustomTagPrefixMap.get(prefix); + } + + public String getDeferredSyntaxAllowedAsLiteral() { + return deferredSyntaxAllowedAsLiteralValue; + } + + public boolean isDeferredSyntaxAllowedAsLiteral() { + return deferredSyntaxAllowedAsLiteral; + } + + public void setDeferredSyntaxAllowedAsLiteral(boolean isELDeferred) { + this.deferredSyntaxAllowedAsLiteral = isELDeferred; + } + + public ExpressionFactory getExpressionFactory() { + return expressionFactory; + } + + public String getTrimDirectiveWhitespaces() { + return trimDirectiveWhitespacesValue; + } + + public boolean isTrimDirectiveWhitespaces() { + return trimDirectiveWhitespaces; + } + + public void setTrimDirectiveWhitespaces(boolean trimDirectiveWhitespaces) { + this.trimDirectiveWhitespaces = trimDirectiveWhitespaces; + } + + public Set getVarInfoNames() { + return varInfoNames; + } + + public boolean isErrorOnUndeclaredNamespace() { + return errorOnUndeclaredNamespace; + } + + public void setErrorOnUndeclaredNamespace( + boolean errorOnUndeclaredNamespace) { + this.errorOnUndeclaredNamespace = errorOnUndeclaredNamespace; + } +} diff --git a/java/org/apache/jasper/compiler/Parser.java b/java/org/apache/jasper/compiler/Parser.java new file mode 100644 index 0000000..cc0be77 --- /dev/null +++ b/java/org/apache/jasper/compiler/Parser.java @@ -0,0 +1,1812 @@ +/* + * 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.jasper.compiler; + +import java.io.CharArrayWriter; +import java.io.FileNotFoundException; +import java.util.Collection; + +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagFileInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.util.UniqueAttributesImpl; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; +import org.xml.sax.Attributes; +import org.xml.sax.helpers.AttributesImpl; + +/** + * This class implements a parser for a JSP page (non-xml view). JSP page + * grammar is included here for reference. The token '#' that appears in the + * production indicates the current input token location in the production. + * + * @author Kin-man Chung + * @author Shawn Bayern + * @author Mark Roth + */ + +class Parser implements TagConstants { + + private final ParserController parserController; + + private final JspCompilationContext ctxt; + + private final JspReader reader; + + private Mark start; + + private final ErrorDispatcher err; + + private int scriptlessCount; + + private final boolean isTagFile; + + private final boolean directivesOnly; + + private final Jar jar; + + private final PageInfo pageInfo; + + // Virtual body content types, to make parsing a little easier. + // These are not accessible from outside the parser. + private static final String JAVAX_BODY_CONTENT_PARAM = + "JAVAX_BODY_CONTENT_PARAM"; + + private static final String JAVAX_BODY_CONTENT_PLUGIN = + "JAVAX_BODY_CONTENT_PLUGIN"; + + private static final String JAVAX_BODY_CONTENT_TEMPLATE_TEXT = + "JAVAX_BODY_CONTENT_TEMPLATE_TEXT"; + + /** + * The constructor + */ + private Parser(ParserController pc, JspReader reader, boolean isTagFile, + boolean directivesOnly, Jar jar) { + this.parserController = pc; + this.ctxt = pc.getJspCompilationContext(); + this.pageInfo = pc.getCompiler().getPageInfo(); + this.err = pc.getCompiler().getErrorDispatcher(); + this.reader = reader; + this.scriptlessCount = 0; + this.isTagFile = isTagFile; + this.directivesOnly = directivesOnly; + this.jar = jar; + start = reader.mark(); + } + + /** + * The main entry for Parser + * + * @param pc The ParseController, use for getting other objects in compiler + * and for parsing included pages + * @param reader To read the page + * @param parent The parent node to this page, null for top level page + * @param isTagFile Is the page being parsed a tag file? + * @param directivesOnly Should only directives be parsed? + * @param jar JAR, if any, that this page was loaded from + * @param pageEnc The encoding of the source + * @param jspConfigPageEnc The encoding for the page + * @param isDefaultPageEncoding Is the page encoding the default? + * @param isBomPresent Is a BOM present in the source + * @return list of nodes representing the parsed page + * + * @throws JasperException If an error occurs during parsing + */ + public static Node.Nodes parse(ParserController pc, JspReader reader, + Node parent, boolean isTagFile, boolean directivesOnly, + Jar jar, String pageEnc, String jspConfigPageEnc, + boolean isDefaultPageEncoding, boolean isBomPresent) + throws JasperException { + + Parser parser = new Parser(pc, reader, isTagFile, directivesOnly, jar); + + Node.Root root = new Node.Root(reader.mark(), parent, false, + pc.getJspCompilationContext().getOptions().getTempVariableNamePrefix()); + root.setPageEncoding(pageEnc); + root.setJspConfigPageEncoding(jspConfigPageEnc); + root.setIsDefaultPageEncoding(isDefaultPageEncoding); + root.setIsBomPresent(isBomPresent); + + // For the Top level page, add include-prelude and include-coda + PageInfo pageInfo = pc.getCompiler().getPageInfo(); + if (parent == null && !isTagFile) { + parser.addInclude(root, pageInfo.getIncludePrelude()); + } + if (directivesOnly) { + parser.parseFileDirectives(root); + } else { + while (reader.hasMoreInput()) { + parser.parseElements(root); + } + } + if (parent == null && !isTagFile) { + parser.addInclude(root, pageInfo.getIncludeCoda()); + } + + Node.Nodes page = new Node.Nodes(root); + return page; + } + + /** + * Attributes ::= (S Attribute)* S? + */ + Attributes parseAttributes() throws JasperException { + return parseAttributes(false); + } + Attributes parseAttributes(boolean pageDirective) throws JasperException { + UniqueAttributesImpl attrs = new UniqueAttributesImpl(pageDirective); + + reader.skipSpaces(); + int ws = 1; + + try { + while (parseAttribute(attrs)) { + if (ws == 0 && ctxt.getOptions().getStrictWhitespace()) { + err.jspError(reader.mark(), + "jsp.error.attribute.nowhitespace"); + } + ws = reader.skipSpaces(); + } + } catch (IllegalArgumentException iae) { + // Duplicate attribute + err.jspError(reader.mark(), "jsp.error.attribute.duplicate"); + } + + return attrs; + } + + /** + * Parse Attributes for a reader, provided for external use + * + * @param pc The parser + * @param reader The source + * + * @return The parsed attributes + * + * @throws JasperException If an error occurs during parsing + */ + public static Attributes parseAttributes(ParserController pc, + JspReader reader) throws JasperException { + Parser tmpParser = new Parser(pc, reader, false, false, null); + return tmpParser.parseAttributes(true); + } + + /** + * Attribute ::= Name S? Eq S? ( '"<%=' RTAttributeValueDouble | '"' + * AttributeValueDouble | "'<%=" RTAttributeValueSingle | "'" + * AttributeValueSingle } Note: JSP and XML spec does not allow while spaces + * around Eq. It is added to be backward compatible with Tomcat, and with + * other xml parsers. + */ + private boolean parseAttribute(AttributesImpl attrs) + throws JasperException { + + // Get the qualified name + String qName = parseName(); + if (qName == null) { + return false; + } + + boolean ignoreEL = pageInfo.isELIgnored(); + + // Determine prefix and local name components + String localName = qName; + String uri = ""; + int index = qName.indexOf(':'); + if (index != -1) { + String prefix = qName.substring(0, index); + uri = pageInfo.getURI(prefix); + if (uri == null) { + err.jspError(reader.mark(), + "jsp.error.attribute.invalidPrefix", prefix); + } + localName = qName.substring(index + 1); + } + + reader.skipSpaces(); + if (!reader.matches("=")) { + err.jspError(reader.mark(), "jsp.error.attribute.noequal"); + } + + reader.skipSpaces(); + char quote = (char) reader.nextChar(); + if (quote != '\'' && quote != '"') { + err.jspError(reader.mark(), "jsp.error.attribute.noquote"); + } + + String watchString = ""; + if (reader.matches("<%=")) { + watchString = "%>"; + // Can't embed EL in a script expression + ignoreEL = true; + } + watchString = watchString + quote; + + String attrValue = parseAttributeValue(qName, watchString, ignoreEL); + attrs.addAttribute(uri, localName, qName, "CDATA", attrValue); + return true; + } + + /** + * Name ::= (Letter | '_' | ':') (Letter | Digit | '.' | '_' | '-' | ':')* + */ + private String parseName() { + char ch = (char) reader.peekChar(); + if (Character.isLetter(ch) || ch == '_' || ch == ':') { + StringBuilder buf = new StringBuilder(); + buf.append(ch); + reader.nextChar(); + ch = (char) reader.peekChar(); + while (Character.isLetter(ch) || Character.isDigit(ch) || ch == '.' + || ch == '_' || ch == '-' || ch == ':') { + buf.append(ch); + reader.nextChar(); + ch = (char) reader.peekChar(); + } + return buf.toString(); + } + return null; + } + + /** + * AttributeValueDouble ::= (QuotedChar - '"')* ('"' | <TRANSLATION_ERROR>) + * RTAttributeValueDouble ::= ((QuotedChar - '"')* - ((QuotedChar-'"')'%>"') + * ('%>"' | TRANSLATION_ERROR) + */ + private String parseAttributeValue(String qName, String watch, boolean ignoreEL) throws JasperException { + boolean quoteAttributeEL = ctxt.getOptions().getQuoteAttributeEL(); + Mark start = reader.mark(); + // In terms of finding the end of the value, quoting EL is equivalent to + // ignoring it. + Mark stop = reader.skipUntilIgnoreEsc(watch, ignoreEL || quoteAttributeEL); + if (stop == null) { + err.jspError(start, "jsp.error.attribute.unterminated", qName); + } + + String ret = null; + try { + char quote = watch.charAt(watch.length() - 1); + + // If watch is longer than 1 character this is a scripting + // expression and EL is always ignored + boolean isElIgnored = + pageInfo.isELIgnored() || watch.length() > 1; + + ret = AttributeParser.getUnquoted(reader.getText(start, stop), + quote, isElIgnored, + pageInfo.isDeferredSyntaxAllowedAsLiteral(), + ctxt.getOptions().getStrictQuoteEscaping(), + quoteAttributeEL); + } catch (IllegalArgumentException iae) { + err.jspError(start, iae.getMessage()); + } + if (watch.length() == 1) { + return ret; + } + + // Put back delimiter '<%=' and '%>', since they are needed if the + // attribute does not allow RTexpression. + return "<%=" + ret + "%>"; + } + + private String parseScriptText(String tx) { + CharArrayWriter cw = new CharArrayWriter(); + int size = tx.length(); + int i = 0; + while (i < size) { + char ch = tx.charAt(i); + if (i + 2 < size && ch == '%' && tx.charAt(i + 1) == '\\' + && tx.charAt(i + 2) == '>') { + cw.write('%'); + cw.write('>'); + i += 3; + } else { + cw.write(ch); + ++i; + } + } + cw.close(); + return cw.toString(); + } + + /* + * Invokes parserController to parse the included page + */ + private void processIncludeDirective(String file, Node parent) + throws JasperException { + if (file == null) { + return; + } + + try { + parserController.parse(file, parent, jar); + } catch (FileNotFoundException ex) { + err.jspError(start, "jsp.error.file.not.found", file); + } catch (Exception ex) { + err.jspError(start, ex.getMessage()); + } + } + + /* + * Parses a page directive with the following syntax: PageDirective ::= ( S + * Attribute)* + */ + private void parsePageDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(true); + Node.PageDirective n = new Node.PageDirective(attrs, start, parent); + + /* + * A page directive may contain multiple 'import' attributes, each of + * which consists of a comma-separated list of package names. Store each + * list with the node, where it is parsed. + */ + for (int i = 0; i < attrs.getLength(); i++) { + if ("import".equals(attrs.getQName(i))) { + n.addImport(attrs.getValue(i)); + } + } + } + + /* + * Parses an include directive with the following syntax: IncludeDirective + * ::= ( S Attribute)* + */ + private void parseIncludeDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + + // Included file expanded here + Node includeNode = new Node.IncludeDirective(attrs, start, parent); + processIncludeDirective(attrs.getValue("file"), includeNode); + } + + /** + * Add a list of files. This is used for implementing include-prelude and + * include-coda of jsp-config element in web.xml + */ + private void addInclude(Node parent, Collection files) throws JasperException { + if (files != null) { + for (String file : files) { + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "file", "file", "CDATA", file); + + // Create a dummy Include directive node + Node includeNode = new Node.IncludeDirective(attrs, reader + .mark(), parent); + processIncludeDirective(file, includeNode); + } + } + } + + /* + * Parses a taglib directive with the following syntax: Directive ::= ( S + * Attribute)* + */ + private void parseTaglibDirective(Node parent) throws JasperException { + + Attributes attrs = parseAttributes(); + String uri = attrs.getValue("uri"); + String prefix = attrs.getValue("prefix"); + if (prefix != null) { + Mark prevMark = pageInfo.getNonCustomTagPrefix(prefix); + if (prevMark != null) { + err.jspError(reader.mark(), "jsp.error.prefix.use_before_dcl", + prefix, prevMark.getFile(), "" + + prevMark.getLineNumber()); + } + if (uri != null) { + String uriPrev = pageInfo.getURI(prefix); + if (uriPrev != null && !uriPrev.equals(uri)) { + err.jspError(reader.mark(), "jsp.error.prefix.refined", + prefix, uri, uriPrev); + } + if (pageInfo.getTaglib(uri) == null) { + TagLibraryInfoImpl impl = null; + if (ctxt.getOptions().isCaching()) { + impl = (TagLibraryInfoImpl) ctxt.getOptions() + .getCache().get(uri); + } + if (impl == null) { + TldResourcePath tldResourcePath = ctxt.getTldResourcePath(uri); + impl = new TagLibraryInfoImpl(ctxt, parserController, + pageInfo, prefix, uri, tldResourcePath, err); + if (ctxt.getOptions().isCaching()) { + ctxt.getOptions().getCache().put(uri, impl); + } + } + pageInfo.addTaglib(uri, impl); + } + pageInfo.addPrefixMapping(prefix, uri); + } else { + String tagdir = attrs.getValue("tagdir"); + if (tagdir != null) { + String urnTagdir = URN_JSPTAGDIR + tagdir; + if (pageInfo.getTaglib(urnTagdir) == null) { + pageInfo.addTaglib(urnTagdir, + new ImplicitTagLibraryInfo(ctxt, + parserController, pageInfo, prefix, + tagdir, err)); + } + pageInfo.addPrefixMapping(prefix, urnTagdir); + } + } + } + + @SuppressWarnings("unused") + Node unused = new Node.TaglibDirective(attrs, start, parent); + } + + /* + * Parses a directive with the following syntax: Directive ::= S? ( 'page' + * PageDirective | 'include' IncludeDirective | 'taglib' TagLibDirective) S? + * '%>' + * + * TagDirective ::= S? ('tag' PageDirective | 'include' IncludeDirective | + * 'taglib' TagLibDirective) | 'attribute AttributeDirective | 'variable + * VariableDirective S? '%>' + */ + private void parseDirective(Node parent) throws JasperException { + reader.skipSpaces(); + + String directive = null; + if (reader.matches("page")) { + directive = "<%@ page"; + if (isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.istagfile", + directive); + } + parsePageDirective(parent); + } else if (reader.matches("include")) { + directive = "<%@ include"; + parseIncludeDirective(parent); + } else if (reader.matches("taglib")) { + if (directivesOnly) { + // No need to get the tagLibInfo objects. This also suppresses + // parsing of any tag files used in this tag file. + return; + } + directive = "<%@ taglib"; + parseTaglibDirective(parent); + } else if (reader.matches("tag")) { + directive = "<%@ tag"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + directive); + } + parseTagDirective(parent); + } else if (reader.matches("attribute")) { + directive = "<%@ attribute"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + directive); + } + parseAttributeDirective(parent); + } else if (reader.matches("variable")) { + directive = "<%@ variable"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + directive); + } + parseVariableDirective(parent); + } else { + err.jspError(reader.mark(), "jsp.error.invalid.directive"); + } + + reader.skipSpaces(); + if (!reader.matches("%>")) { + err.jspError(start, "jsp.error.unterminated", directive); + } + } + + /* + * Parses a directive with the following syntax: + * + * XMLJSPDirectiveBody ::= S? ( ( 'page' PageDirectiveAttrList S? ( '/>' | ( + * '>' S? ETag ) ) | ( 'include' IncludeDirectiveAttrList S? ( '/>' | ( '>' + * S? ETag ) ) | + * + * XMLTagDefDirectiveBody ::= ( ( 'tag' TagDirectiveAttrList S? ( '/>' | ( + * '>' S? ETag ) ) | ( 'include' IncludeDirectiveAttrList S? ( '/>' | ( '>' + * S? ETag ) ) | ( 'attribute' AttributeDirectiveAttrList S? ( '/>' | ( '>' + * S? ETag ) ) | ( 'variable' VariableDirectiveAttrList S? ( '/>' | ( '>' S? + * ETag ) ) ) | + */ + private void parseXMLDirective(Node parent) throws JasperException { + reader.skipSpaces(); + + String eTag = null; + if (reader.matches("page")) { + eTag = "jsp:directive.page"; + if (isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.istagfile", + "<" + eTag); + } + parsePageDirective(parent); + } else if (reader.matches("include")) { + eTag = "jsp:directive.include"; + parseIncludeDirective(parent); + } else if (reader.matches("tag")) { + eTag = "jsp:directive.tag"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + "<" + eTag); + } + parseTagDirective(parent); + } else if (reader.matches("attribute")) { + eTag = "jsp:directive.attribute"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + "<" + eTag); + } + parseAttributeDirective(parent); + } else if (reader.matches("variable")) { + eTag = "jsp:directive.variable"; + if (!isTagFile) { + err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", + "<" + eTag); + } + parseVariableDirective(parent); + } else { + err.jspError(reader.mark(), "jsp.error.invalid.directive"); + } + + reader.skipSpaces(); + if (reader.matches(">")) { + reader.skipSpaces(); + if (!reader.matchesETag(eTag)) { + err.jspError(start, "jsp.error.unterminated", "<" + eTag); + } + } else if (!reader.matches("/>")) { + err.jspError(start, "jsp.error.unterminated", "<" + eTag); + } + } + + /* + * Parses a tag directive with the following syntax: PageDirective ::= ( S + * Attribute)* + */ + private void parseTagDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(true); + Node.TagDirective n = new Node.TagDirective(attrs, start, parent); + + /* + * A page directive may contain multiple 'import' attributes, each of + * which consists of a comma-separated list of package names. Store each + * list with the node, where it is parsed. + */ + for (int i = 0; i < attrs.getLength(); i++) { + if ("import".equals(attrs.getQName(i))) { + n.addImport(attrs.getValue(i)); + } + } + } + + /* + * Parses a attribute directive with the following syntax: + * AttributeDirective ::= ( S Attribute)* + */ + private void parseAttributeDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + @SuppressWarnings("unused") + Node unused = new Node.AttributeDirective(attrs, start, parent); + } + + /* + * Parses a variable directive with the following syntax: + * PageDirective ::= ( S Attribute)* + */ + private void parseVariableDirective(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + @SuppressWarnings("unused") + Node unused = new Node.VariableDirective(attrs, start, parent); + } + + /* + * JSPCommentBody ::= (Char* - (Char* '--%>')) '--%>' + */ + private void parseComment(Node parent) throws JasperException { + start = reader.mark(); + Mark stop = reader.skipUntil("--%>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "<%--"); + } + + @SuppressWarnings("unused") + Node unused = + new Node.Comment(reader.getText(start, stop), start, parent); + } + + /* + * DeclarationBody ::= (Char* - (char* '%>')) '%>' + */ + private void parseDeclaration(Node parent) throws JasperException { + start = reader.mark(); + Mark stop = reader.skipUntil("%>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "<%!"); + } + + @SuppressWarnings("unused") + Node unused = new Node.Declaration( + parseScriptText(reader.getText(start, stop)), start, parent); + } + + /* + * XMLDeclarationBody ::= ( S? '/>' ) | ( S? '>' (Char* - (char* '<')) + * CDSect?)* ETag | CDSect ::= CDStart CData CDEnd + * CDStart ::= '' Char*)) CDEnd + * ::= ']]>' + */ + private void parseXMLDeclaration(Node parent) throws JasperException { + reader.skipSpaces(); + if (!reader.matches("/>")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:declaration>"); + } + Mark stop; + String text; + while (true) { + start = reader.mark(); + stop = reader.skipUntil("<"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:declaration>"); + } + text = parseScriptText(reader.getText(start, stop)); + @SuppressWarnings("unused") + Node unused = new Node.Declaration(text, start, parent); + if (reader.matches("![CDATA[")) { + start = reader.mark(); + stop = reader.skipUntil("]]>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "CDATA"); + } + text = parseScriptText(reader.getText(start, stop)); + @SuppressWarnings("unused") + Node unused2 = new Node.Declaration(text, start, parent); + } else { + break; + } + } + + if (!reader.matchesETagWithoutLessThan("jsp:declaration")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:declaration>"); + } + } + } + + /* + * ExpressionBody ::= (Char* - (char* '%>')) '%>' + */ + private void parseExpression(Node parent) throws JasperException { + start = reader.mark(); + Mark stop = reader.skipUntil("%>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "<%="); + } + + @SuppressWarnings("unused") + Node unused = new Node.Expression( + parseScriptText(reader.getText(start, stop)), start, parent); + } + + /* + * XMLExpressionBody ::= ( S? '/>' ) | ( S? '>' (Char* - (char* '<')) + * CDSect?)* ETag ) | + */ + private void parseXMLExpression(Node parent) throws JasperException { + reader.skipSpaces(); + if (!reader.matches("/>")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:expression>"); + } + Mark stop; + String text; + while (true) { + start = reader.mark(); + stop = reader.skipUntil("<"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:expression>"); + } + text = parseScriptText(reader.getText(start, stop)); + @SuppressWarnings("unused") + Node unused = new Node.Expression(text, start, parent); + if (reader.matches("![CDATA[")) { + start = reader.mark(); + stop = reader.skipUntil("]]>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "CDATA"); + } + text = parseScriptText(reader.getText(start, stop)); + @SuppressWarnings("unused") + Node unused2 = new Node.Expression(text, start, parent); + } else { + break; + } + } + if (!reader.matchesETagWithoutLessThan("jsp:expression")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:expression>"); + } + } + } + + /* + * ELExpressionBody. Starts with "#{" or "${". Ends with "}". + * See JspReader.skipELExpression(). + */ + private void parseELExpression(Node parent, char type) + throws JasperException { + start = reader.mark(); + Mark last = reader.skipELExpression(); + if (last == null) { + err.jspError(start, "jsp.error.unterminated", type + "{"); + } + + @SuppressWarnings("unused") + Node unused = new Node.ELExpression(type, reader.getText(start, last), + start, parent); + } + + /* + * ScriptletBody ::= (Char* - (char* '%>')) '%>' + */ + private void parseScriptlet(Node parent) throws JasperException { + start = reader.mark(); + Mark stop = reader.skipUntil("%>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "<%"); + } + + @SuppressWarnings("unused") + Node unused = new Node.Scriptlet( + parseScriptText(reader.getText(start, stop)), start, parent); + } + + /* + * XMLScriptletBody ::= ( S? '/>' ) | ( S? '>' (Char* - (char* '<')) + * CDSect?)* ETag ) | + */ + private void parseXMLScriptlet(Node parent) throws JasperException { + reader.skipSpaces(); + if (!reader.matches("/>")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:scriptlet>"); + } + Mark stop; + String text; + while (true) { + start = reader.mark(); + stop = reader.skipUntil("<"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:scriptlet>"); + } + text = parseScriptText(reader.getText(start, stop)); + @SuppressWarnings("unused") + Node unused = new Node.Scriptlet(text, start, parent); + if (reader.matches("![CDATA[")) { + start = reader.mark(); + stop = reader.skipUntil("]]>"); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "CDATA"); + } + text = parseScriptText(reader.getText(start, stop)); + @SuppressWarnings("unused") + Node unused2 = new Node.Scriptlet(text, start, parent); + } else { + break; + } + } + + if (!reader.matchesETagWithoutLessThan("jsp:scriptlet")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:scriptlet>"); + } + } + } + + /** + * Param ::= '<jsp:param' S Attributes S? EmptyBody S? + */ + private void parseParam(Node parent) throws JasperException { + if (!reader.matches("' S? ( ' ) S? ETag ) | ( '>' S? Param* ETag ) + * + * EmptyBody ::= '/>' | ( '>' ETag ) | ( '>' S? '' Param* '' + */ + private void parseInclude(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node includeNode = new Node.IncludeAction(attrs, start, parent); + + parseOptionalBody(includeNode, "jsp:include", JAVAX_BODY_CONTENT_PARAM); + } + + /* + * For Forward: StdActionContent ::= Attributes ParamBody + */ + private void parseForward(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node forwardNode = new Node.ForwardAction(attrs, start, parent); + + parseOptionalBody(forwardNode, "jsp:forward", JAVAX_BODY_CONTENT_PARAM); + } + + private void parseInvoke(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node invokeNode = new Node.InvokeAction(attrs, start, parent); + + parseEmptyBody(invokeNode, "jsp:invoke"); + } + + private void parseDoBody(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node doBodyNode = new Node.DoBodyAction(attrs, start, parent); + + parseEmptyBody(doBodyNode, "jsp:doBody"); + } + + private void parseElement(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node elementNode = new Node.JspElement(attrs, start, parent); + + parseOptionalBody(elementNode, "jsp:element", TagInfo.BODY_CONTENT_JSP); + } + + /* + * For GetProperty: StdActionContent ::= Attributes EmptyBody + */ + private void parseGetProperty(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node getPropertyNode = new Node.GetProperty(attrs, start, parent); + + parseOptionalBody(getPropertyNode, "jsp:getProperty", + TagInfo.BODY_CONTENT_EMPTY); + } + + /* + * For SetProperty: StdActionContent ::= Attributes EmptyBody + */ + private void parseSetProperty(Node parent) throws JasperException { + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + Node setPropertyNode = new Node.SetProperty(attrs, start, parent); + + parseOptionalBody(setPropertyNode, "jsp:setProperty", + TagInfo.BODY_CONTENT_EMPTY); + } + + /* + * EmptyBody ::= '/>' | ( '>' ETag ) | ( '>' S? '")) { + // Done + } else if (reader.matches(">")) { + if (reader.matchesETag(tag)) { + // Done + } else if (reader.matchesOptionalSpacesFollowedBy("' ETag ) | ( '>' S? '' Body ETag ) + * + * ScriptlessActionBody ::= JspAttributeAndBody | ( '>' ScriptlessBody ETag ) + * + * TagDependentActionBody ::= JspAttributeAndBody | ( '>' TagDependentBody + * ETag ) + * + */ + private void parseOptionalBody(Node parent, String tag, String bodyType) + throws JasperException { + if (reader.matches("/>")) { + // EmptyBody + return; + } + + if (!reader.matches(">")) { + err.jspError(reader.mark(), "jsp.error.unterminated", "<" + tag); + } + + if (reader.matchesETag(tag)) { + // EmptyBody + return; + } + + if (!parseJspAttributeAndBody(parent, tag, bodyType)) { + // Must be ( '>' # Body ETag ) + parseBody(parent, tag, bodyType); + } + } + + /** + * Attempts to parse 'JspAttributeAndBody' production. Returns true if it + * matched, or false if not. Assumes EmptyBody is okay as well. + * + * JspAttributeAndBody ::= ( '>' # S? ( '<jsp:attribute' NamedAttributes )? '<jsp:body' ( + * JspBodyBody | <TRANSLATION_ERROR> ) S? ETag ) + */ + private boolean parseJspAttributeAndBody(Node parent, String tag, + String bodyType) throws JasperException { + boolean result = false; + + if (reader.matchesOptionalSpacesFollowedBy(" elements: + parseNamedAttributes(parent); + + result = true; + } + + if (reader.matchesOptionalSpacesFollowedBy(" but something other than + // or the end tag, translation error. + err.jspError(reader.mark(), "jsp.error.jspbody.required", "<" + + tag); + } + + return result; + } + + /* + * Params ::= `>' S? ( ( `' ( ( S? Param+ S? `' ) | + * ) ) | Param+ ) '' + */ + private void parseJspParams(Node parent) throws JasperException { + Node jspParamsNode = new Node.ParamsAction(start, parent); + parseOptionalBody(jspParamsNode, "jsp:params", JAVAX_BODY_CONTENT_PARAM); + } + + /* + * Fallback ::= '/>' | ( `>' S? `' ( ( S? ( Char* - ( Char* `' ) ) `' + * S? ) | ) `' ) | ( '>' ( Char* - ( + * Char* '' ) ) '' ) + */ + private void parseFallBack(Node parent) throws JasperException { + Node fallBackNode = new Node.FallBackAction(start, parent); + parseOptionalBody(fallBackNode, "jsp:fallback", + JAVAX_BODY_CONTENT_TEMPLATE_TEXT); + } + + /* + * For Plugin: StdActionContent ::= Attributes PluginBody + * + * PluginBody ::= EmptyBody | ( '>' S? ( ' ) S? ETag ) | ( '>' S? PluginTags + * ETag ) + * + * EmptyBody ::= '/>' | ( '>' ETag ) | ( '>' S? ' + * + * Attributes ::= ( S Attribute )* S? + * + * CustomActionEnd ::= CustomActionTagDependent | CustomActionJSPContent | + * CustomActionScriptlessContent + * + * CustomActionTagDependent ::= TagDependentOptionalBody + * + * CustomActionJSPContent ::= OptionalBody + * + * CustomActionScriptlessContent ::= ScriptlessOptionalBody + */ + @SuppressWarnings("null") // tagFileInfo can't be null after initial test + private boolean parseCustomTag(Node parent) throws JasperException { + + if (reader.peekChar() != '<') { + return false; + } + + // Parse 'CustomAction' production (tag prefix and custom action name) + reader.nextChar(); // skip '<' + String tagName = reader.parseToken(false); + int i = tagName.indexOf(':'); + if (i == -1) { + reader.reset(start); + return false; + } + + String prefix = tagName.substring(0, i); + String shortTagName = tagName.substring(i + 1); + + // Check if this is a user-defined tag. + String uri = pageInfo.getURI(prefix); + if (uri == null) { + if (pageInfo.isErrorOnUndeclaredNamespace()) { + err.jspError(start, "jsp.error.undeclared_namespace", prefix); + } else { + reader.reset(start); + // Remember the prefix for later error checking + pageInfo.putNonCustomTagPrefix(prefix, reader.mark()); + return false; + } + } + + TagLibraryInfo tagLibInfo = pageInfo.getTaglib(uri); + TagInfo tagInfo = tagLibInfo.getTag(shortTagName); + TagFileInfo tagFileInfo = tagLibInfo.getTagFile(shortTagName); + if (tagInfo == null && tagFileInfo == null) { + err.jspError(start, "jsp.error.bad_tag", shortTagName, prefix); + } + Class tagHandlerClass = null; + if (tagInfo != null) { + // Must be a classic tag, load it here. + // tag files will be loaded later, in TagFileProcessor + String handlerClassName = tagInfo.getTagClassName(); + try { + tagHandlerClass = ctxt.getClassLoader().loadClass( + handlerClassName); + } catch (Exception e) { + err.jspError(start, "jsp.error.loadclass.taghandler", + handlerClassName, tagName); + } + } + + // Parse 'CustomActionBody' production: + // At this point we are committed - if anything fails, we produce + // a translation error. + + // Parse 'Attributes' production: + Attributes attrs = parseAttributes(); + reader.skipSpaces(); + + // Parse 'CustomActionEnd' production: + if (reader.matches("/>")) { + if (tagInfo != null) { + @SuppressWarnings("unused") + Node unused = new Node.CustomTag(tagName, prefix, shortTagName, + uri, attrs, start, parent, tagInfo, tagHandlerClass); + } else { + @SuppressWarnings("unused") + Node unused = new Node.CustomTag(tagName, prefix, shortTagName, + uri, attrs, start, parent, tagFileInfo); + } + return true; + } + + // Now we parse one of 'CustomActionTagDependent', + // 'CustomActionJSPContent', or 'CustomActionScriptlessContent'. + // depending on body-content in TLD. + + // Looking for a body, it still can be empty; but if there is a + // a tag body, its syntax would be dependent on the type of + // body content declared in the TLD. + String bc; + if (tagInfo != null) { + bc = tagInfo.getBodyContent(); + } else { + bc = tagFileInfo.getTagInfo().getBodyContent(); + } + + Node tagNode = null; + if (tagInfo != null) { + tagNode = new Node.CustomTag(tagName, prefix, shortTagName, uri, + attrs, start, parent, tagInfo, tagHandlerClass); + } else { + tagNode = new Node.CustomTag(tagName, prefix, shortTagName, uri, + attrs, start, parent, tagFileInfo); + } + + parseOptionalBody(tagNode, tagName, bc); + + return true; + } + + /* + * Parse for a template text string until '<' or "${" or "#{" is encountered, + * recognizing escape sequences "<\%", "\$", and "\#". + * + * Note: JSP uses '\$' as an escape for '$' and '\#' for '#' whereas EL uses + * '\${' for '${' and '\#{' for '#{'. We are processing JSP template + * test here so the JSP escapes apply. + */ + private void parseTemplateText(Node parent) { + + if (!reader.hasMoreInput()) { + return; + } + + CharArrayWriter ttext = new CharArrayWriter(); + + int ch = reader.nextChar(); + while (ch != -1) { + if (ch == '<') { + // Check for "<\%" + if (reader.peekChar(0) == '\\' && reader.peekChar(1) == '%') { + ttext.write(ch); + // Swallow the \ + reader.nextChar(); + ttext.write(reader.nextChar()); + } else { + if (ttext.size() == 0) { + ttext.write(ch); + } else { + reader.pushChar(); + break; + } + } + } else if (ch == '\\' && !pageInfo.isELIgnored()) { + int next = reader.peekChar(0); + if (next == '$' || next == '#') { + ttext.write(reader.nextChar()); + } else { + ttext.write(ch); + } + } else if ((ch == '$' || ch == '#' && !pageInfo.isDeferredSyntaxAllowedAsLiteral()) && + !pageInfo.isELIgnored()) { + if (reader.peekChar(0) == '{') { + reader.pushChar(); + break; + } else { + ttext.write(ch); + } + } else { + ttext.write(ch); + } + ch = reader.nextChar(); + } + + @SuppressWarnings("unused") + Node unused = new Node.TemplateText(ttext.toString(), start, parent); + } + + /* + * XMLTemplateText ::= ( S? '/>' ) | ( S? '>' ( ( Char* - ( Char* ( '<' | + * '${' ) ) ) ( '${' ELExpressionBody )? CDSect? )* ETag ) | + * + */ + private void parseXMLTemplateText(Node parent) throws JasperException { + reader.skipSpaces(); + if (!reader.matches("/>")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:text>"); + } + CharArrayWriter ttext = new CharArrayWriter(); + int ch = reader.nextChar(); + while (ch != -1) { + if (ch == '<') { + // Check for "); + if (stop == null) { + err.jspError(start, "jsp.error.unterminated", "CDATA"); + } + String text = reader.getText(start, stop); + ttext.write(text, 0, text.length()); + } else if (ch == '\\') { + int next = reader.peekChar(0); + if (next == '$' || next =='#') { + ttext.write(reader.nextChar()); + } else { + ttext.write('\\'); + } + } else if (ch == '$' || ch == '#') { + if (reader.peekChar(0) == '{') { + // Swallow the '{' + reader.nextChar(); + + // Create a template text node + @SuppressWarnings("unused") + Node unused = new Node.TemplateText( + ttext.toString(), start, parent); + + // Mark and parse the EL expression and create its node: + parseELExpression(parent, (char) ch); + + start = reader.mark(); + ttext.reset(); + } else { + ttext.write(ch); + } + } else { + ttext.write(ch); + } + ch = reader.nextChar(); + } + + @SuppressWarnings("unused") + Node unused = + new Node.TemplateText(ttext.toString(), start, parent); + + if (!reader.hasMoreInput()) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:text>"); + } else if (!reader.matchesETagWithoutLessThan("jsp:text")) { + err.jspError(start, "jsp.error.jsptext.badcontent"); + } + } + } + + /* + * AllBody ::= ( '<%--' JSPCommentBody ) | ( '<%@' DirectiveBody ) | ( ' 0) { + // vc: ScriptlessBody + // We must follow the ScriptlessBody production if one of + // our parents is ScriptlessBody. + parseElementsScriptless(parent); + return; + } + + start = reader.mark(); + if (reader.matches("<%--")) { + parseComment(parent); + } else if (reader.matches("<%@")) { + parseDirective(parent); + } else if (reader.matches(" ) | ( ' ) | ( '<%=' ) | ( ' ) | ( '<%' ) | ( ' ) | ( ' ) | ( ' ) | ( '<%=' ) | ( ' ) | ( '<%' ) | ( ' ) | ( ' ) | ( '${' + * ) | ( ' ) | TemplateText + */ + private void parseElementsTemplateText(Node parent) throws JasperException { + start = reader.mark(); + if (reader.matches("<%--")) { + parseComment(parent); + } else if (reader.matches("<%@")) { + parseDirective(parent); + } else if (reader.matches("")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", "<jsp:body"); + } + parseBody(bodyNode, "jsp:body", bodyType); + } + } + + /* + * Parse the body as JSP content. @param tag The name of the tag whose end + * tag would terminate the body @param bodyType One of the TagInfo body + * types + */ + private void parseBody(Node parent, String tag, String bodyType) + throws JasperException { + if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_TAG_DEPENDENT)) { + parseTagDependentBody(parent, tag); + } else if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_EMPTY)) { + if (!reader.matchesETag(tag)) { + err.jspError(start, "jasper.error.emptybodycontent.nonempty", + tag); + } + } else if (bodyType == JAVAX_BODY_CONTENT_PLUGIN) { + // (note the == since we won't recognize JAVAX_* + // from outside this module). + parsePluginTags(parent); + if (!reader.matchesETag(tag)) { + err.jspError(reader.mark(), "jsp.error.unterminated", "<" + + tag); + } + } else if (bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_JSP) + || bodyType.equalsIgnoreCase(TagInfo.BODY_CONTENT_SCRIPTLESS) + || (bodyType == JAVAX_BODY_CONTENT_PARAM) + || (bodyType == JAVAX_BODY_CONTENT_TEMPLATE_TEXT)) { + while (reader.hasMoreInput()) { + if (reader.matchesETag(tag)) { + return; + } + + // Check for nested jsp:body or jsp:attribute + if (tag.equals("jsp:body") || tag.equals("jsp:attribute")) { + if (reader.matches("")) { + if (!reader.matches(">")) { + err.jspError(start, "jsp.error.unterminated", + "<jsp:attribute"); + } + if (namedAttributeNode.isTrim()) { + reader.skipSpaces(); + } + parseBody(namedAttributeNode, "jsp:attribute", + getAttributeBodyType(parent, attrs.getValue("name"))); + if (namedAttributeNode.isTrim()) { + Node.Nodes subElems = namedAttributeNode.getBody(); + if (subElems != null) { + Node lastNode = subElems.getNode(subElems.size() - 1); + if (lastNode instanceof Node.TemplateText) { + ((Node.TemplateText) lastNode).rtrim(); + } + } + } + } + reader.skipSpaces(); + } while (reader.matches(" from the enclosing node + */ + private String getAttributeBodyType(Node n, String name) { + + if (n instanceof Node.CustomTag) { + TagInfo tagInfo = ((Node.CustomTag) n).getTagInfo(); + TagAttributeInfo[] tldAttrs = tagInfo.getAttributes(); + for (TagAttributeInfo tldAttr : tldAttrs) { + if (name.equals(tldAttr.getName())) { + if (tldAttr.isFragment()) { + return TagInfo.BODY_CONTENT_SCRIPTLESS; + } + if (tldAttr.canBeRequestTime()) { + return TagInfo.BODY_CONTENT_JSP; + } + } + } + if (tagInfo.hasDynamicAttributes()) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.IncludeAction) { + if ("page".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.ForwardAction) { + if ("page".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.SetProperty) { + if ("value".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.UseBean) { + if ("beanName".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.PlugIn) { + if ("width".equals(name) || "height".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.ParamAction) { + if ("value".equals(name)) { + return TagInfo.BODY_CONTENT_JSP; + } + } else if (n instanceof Node.JspElement) { + return TagInfo.BODY_CONTENT_JSP; + } + + return JAVAX_BODY_CONTENT_TEMPLATE_TEXT; + } + + private void parseFileDirectives(Node parent) throws JasperException { + reader.skipUntil("<"); + while (reader.hasMoreInput()) { + start = reader.mark(); + if (reader.matches("%--")) { + // Comment + reader.skipUntil("--%>"); + } else if (reader.matches("%@")) { + parseDirective(parent); + } else if (reader.matches("jsp:directive.")) { + parseXMLDirective(parent); + } else if (reader.matches("%!")) { + // Declaration + reader.skipUntil("%>"); + } else if (reader.matches("%=")) { + // Expression + reader.skipUntil("%>"); + } else if (reader.matches("%")) { + // Scriptlet + reader.skipUntil("%>"); + } + reader.skipUntil("<"); + } + } +} diff --git a/java/org/apache/jasper/compiler/ParserController.java b/java/org/apache/jasper/compiler/ParserController.java new file mode 100644 index 0000000..01e2b2a --- /dev/null +++ b/java/org/apache/jasper/compiler/ParserController.java @@ -0,0 +1,596 @@ +/* + * 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.jasper.compiler; + +import java.io.BufferedInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayDeque; +import java.util.Deque; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.tomcat.Jar; +import org.xml.sax.Attributes; + +/** + * Controller for the parsing of a JSP page. + *

    + * The same ParserController instance is used for a JSP page and any JSP + * segments included by it (via an include directive), where each segment may + * be provided in standard or XML syntax. This class selects and invokes the + * appropriate parser for the JSP page and its included segments. + * + * @author Pierre Delisle + * @author Jan Luehe + */ +class ParserController implements TagConstants { + + private static final String CHARSET = "charset="; + + private final JspCompilationContext ctxt; + private final Compiler compiler; + private final ErrorDispatcher err; + + /* + * Indicates the syntax (XML or standard) of the file being processed + */ + private boolean isXml; + + /* + * A stack to keep track of the 'current base directory' + * for include directives that refer to relative paths. + */ + private final Deque baseDirStack = new ArrayDeque<>(); + + private boolean isEncodingSpecifiedInProlog; + private boolean isBomPresent; + private int skip; + + private String sourceEnc; + + private boolean isDefaultPageEncoding; + private boolean isTagFile; + private boolean directiveOnly; + + /* + * Constructor + */ + ParserController(JspCompilationContext ctxt, Compiler compiler) { + this.ctxt = ctxt; + this.compiler = compiler; + this.err = compiler.getErrorDispatcher(); + } + + public JspCompilationContext getJspCompilationContext () { + return ctxt; + } + + public Compiler getCompiler () { + return compiler; + } + + /** + * Parses a JSP page or tag file. This is invoked by the compiler. + * + * @param inFileName The path to the JSP page or tag file to be parsed. + * + * @return The parsed nodes + * + * @throws JasperException If an error occurs during parsing + * @throws IOException If an I/O error occurs such as the file not being + * found + */ + public Node.Nodes parse(String inFileName) throws JasperException, IOException { + // If we're parsing a packaged tag file or a resource included by it + // (using an include directive), ctxt.getTagFileJar() returns the + // JAR file from which to read the tag file or included resource, + // respectively. + isTagFile = ctxt.isTagFile(); + directiveOnly = false; + return doParse(inFileName, null, ctxt.getTagFileJar()); + } + + /** + * Parses the directives of a JSP page or tag file. This is invoked by the + * compiler. + * + * @param inFileName The path to the JSP page or tag file to be parsed. + * + * @return The parsed directive nodes + * + * @throws JasperException If an error occurs during parsing + * @throws IOException If an I/O error occurs such as the file not being + * found + */ + public Node.Nodes parseDirectives(String inFileName) throws JasperException, IOException { + // If we're parsing a packaged tag file or a resource included by it + // (using an include directive), ctxt.getTagFileJar() returns the + // JAR file from which to read the tag file or included resource, + // respectively. + isTagFile = ctxt.isTagFile(); + directiveOnly = true; + return doParse(inFileName, null, ctxt.getTagFileJar()); + } + + + /** + * Processes an include directive with the given path. + * + * @param inFileName The path to the resource to be included. + * @param parent The parent node of the include directive. + * @param jar The JAR file from which to read the included resource, + * or null of the included resource is to be read from the filesystem + * + * @return The parsed nodes + * + * @throws JasperException If an error occurs during parsing + * @throws IOException If an I/O error occurs such as the file not being + * found + */ + public Node.Nodes parse(String inFileName, Node parent, Jar jar) + throws JasperException, IOException { + // For files that are statically included, isTagfile and directiveOnly + // remain unchanged. + return doParse(inFileName, parent, jar); + } + + /** + * Extracts tag file directive information from the given tag file. + * + * This is invoked by the compiler + * + * @param inFileName The name of the tag file to be parsed. + * @param jar The location of the tag file. + * + * @return The parsed tag file nodes + * + * @throws JasperException If an error occurs during parsing + * @throws IOException If an I/O error occurs such as the file not being + * found + */ + public Node.Nodes parseTagFileDirectives(String inFileName, Jar jar) + throws JasperException, IOException { + boolean isTagFileSave = isTagFile; + boolean directiveOnlySave = directiveOnly; + isTagFile = true; + directiveOnly = true; + Node.Nodes page = doParse(inFileName, null, jar); + directiveOnly = directiveOnlySave; + isTagFile = isTagFileSave; + return page; + } + + /** + * Parses the JSP page or tag file with the given path name. + * + * @param inFileName The name of the JSP page or tag file to be parsed. + * @param parent The parent node (non-null when processing an include + * directive) + * @param jar The JAR file from which to read the JSP page or tag file, + * or null if the JSP page or tag file is to be read from the filesystem + */ + private Node.Nodes doParse(String inFileName, Node parent, Jar jar) + throws FileNotFoundException, JasperException, IOException { + + Node.Nodes parsedPage = null; + isEncodingSpecifiedInProlog = false; + isBomPresent = false; + isDefaultPageEncoding = false; + + String absFileName = resolveFileName(inFileName); + String jspConfigPageEnc = getJspConfigPageEncoding(absFileName); + + // Figure out what type of JSP document and encoding type we are + // dealing with + determineSyntaxAndEncoding(absFileName, jar, jspConfigPageEnc); + + if (parent != null) { + // Included resource, add to dependent list + if (jar == null) { + compiler.getPageInfo().addDependant(absFileName, + ctxt.getLastModified(absFileName)); + } else { + String entry = absFileName.substring(1); + compiler.getPageInfo().addDependant(jar.getURL(entry), + Long.valueOf(jar.getLastModified(entry))); + + } + } + + if ((isXml && isEncodingSpecifiedInProlog) || isBomPresent) { + /* + * Make sure the encoding explicitly specified in the XML + * prolog (if any) matches that in the JSP config element + * (if any), treating "UTF-16", "UTF-16BE", and "UTF-16LE" as + * identical. + */ + if (jspConfigPageEnc != null && !jspConfigPageEnc.equals(sourceEnc) + && (!jspConfigPageEnc.startsWith("UTF-16") + || !sourceEnc.startsWith("UTF-16"))) { + err.jspError("jsp.error.prolog_config_encoding_mismatch", + sourceEnc, jspConfigPageEnc); + } + } + + // Dispatch to the appropriate parser + if (isXml) { + // JSP document (XML syntax) + // InputStream for jspx page is created and properly closed in + // JspDocumentParser. + parsedPage = JspDocumentParser.parse(this, absFileName, jar, parent, + isTagFile, directiveOnly, sourceEnc, jspConfigPageEnc, + isEncodingSpecifiedInProlog, isBomPresent); + } else { + // Standard syntax + try (InputStreamReader inStreamReader = JspUtil.getReader( + absFileName, sourceEnc, jar, ctxt, err, skip)) { + JspReader jspReader = new JspReader(ctxt, absFileName, + inStreamReader, err); + parsedPage = Parser.parse(this, jspReader, parent, isTagFile, + directiveOnly, jar, sourceEnc, jspConfigPageEnc, + isDefaultPageEncoding, isBomPresent); + } + } + + baseDirStack.remove(); + + return parsedPage; + } + + /* + * Checks to see if the given URI is matched by a URL pattern specified in + * a jsp-property-group in web.xml, and if so, returns the value of the + * element. + * + * @param absFileName The URI to match + * + * @return The value of the attribute of the + * jsp-property-group with matching URL pattern + */ + private String getJspConfigPageEncoding(String absFileName) { + + JspConfig jspConfig = ctxt.getOptions().getJspConfig(); + JspConfig.JspProperty jspProperty + = jspConfig.findJspProperty(absFileName); + return jspProperty.getPageEncoding(); + } + + /** + * Determines the syntax (standard or XML) and page encoding properties + * for the given file, and stores them in the 'isXml' and 'sourceEnc' + * instance variables, respectively. + */ + private void determineSyntaxAndEncoding(String absFileName, Jar jar, + String jspConfigPageEnc) + throws JasperException, IOException { + + isXml = false; + + /* + * 'true' if the syntax (XML or standard) of the file is given + * from external information: either via a JSP configuration element, + * the ".jspx" suffix, or the enclosing file (for included resources) + */ + boolean isExternal = false; + + /* + * Indicates whether we need to revert from temporary usage of + * "ISO-8859-1" back to "UTF-8" + */ + boolean revert = false; + + JspConfig jspConfig = ctxt.getOptions().getJspConfig(); + JspConfig.JspProperty jspProperty = jspConfig.findJspProperty( + absFileName); + if (jspProperty.isXml() != null) { + // If is specified in a , it is used. + isXml = JspUtil.booleanValue(jspProperty.isXml()); + isExternal = true; + } else if (absFileName.endsWith(".jspx") + || absFileName.endsWith(".tagx")) { + isXml = true; + isExternal = true; + } + + if (isExternal && !isXml) { + // JSP (standard) syntax. Use encoding specified in jsp-config + // if provided. + sourceEnc = jspConfigPageEnc; + if (sourceEnc != null) { + return; + } + // We don't know the encoding, so use BOM to determine it + sourceEnc = "ISO-8859-1"; + } else { + // XML syntax or unknown, (auto)detect encoding ... + EncodingDetector encodingDetector; + try (BufferedInputStream bis = JspUtil.getInputStream(absFileName, jar, ctxt)) { + encodingDetector = new EncodingDetector(bis); + } + + sourceEnc = encodingDetector.getEncoding(); + isEncodingSpecifiedInProlog = encodingDetector.isEncodingSpecifiedInProlog(); + isBomPresent = (encodingDetector.getSkip() > 0); + skip = encodingDetector.getSkip(); + + if (!isXml && sourceEnc.equals("UTF-8")) { + /* + * We don't know if we're dealing with XML or standard syntax. + * Therefore, we need to check to see if the page contains + * a element. + * + * We need to be careful, because the page may be encoded in + * ISO-8859-1 (or something entirely different), and may + * contain byte sequences that will cause a UTF-8 converter to + * throw exceptions. + * + * It is safe to use a source encoding of ISO-8859-1 in this + * case, as there are no invalid byte sequences in ISO-8859-1, + * and the byte/character sequences we're looking for (i.e., + * ) are identical in either encoding (both UTF-8 + * and ISO-8859-1 are extensions of ASCII). + */ + sourceEnc = "ISO-8859-1"; + revert = true; + } + } + + if (isXml) { + // (This implies 'isExternal' is TRUE.) + // We know we're dealing with a JSP document (via JSP config or + // ".jspx" suffix), so we're done. + return; + } + + /* + * At this point, 'isExternal' or 'isXml' is FALSE. + * Search for jsp:root action, in order to determine if we're dealing + * with XML or standard syntax (unless we already know what we're + * dealing with, i.e., when 'isExternal' is TRUE and 'isXml' is FALSE). + * No check for XML prolog, since nothing prevents a page from + * outputting XML and still using JSP syntax (in this case, the + * XML prolog is treated as template text). + */ + JspReader jspReader = null; + try { + jspReader = new JspReader(ctxt, absFileName, sourceEnc, jar, err); + } catch (FileNotFoundException ex) { + throw new JasperException(ex); + } + Mark startMark = jspReader.mark(); + if (!isExternal) { + jspReader.reset(startMark); + if (hasJspRoot(jspReader)) { + if (revert) { + sourceEnc = "UTF-8"; + } + isXml = true; + return; + } else { + if (revert && isBomPresent) { + sourceEnc = "UTF-8"; + } + isXml = false; + } + } + + /* + * At this point, we know we're dealing with JSP syntax. + * If an XML prolog is provided, it's treated as template text. + * Determine the page encoding from the page directive, unless it's + * specified via JSP config. + */ + if (!isBomPresent) { + sourceEnc = jspConfigPageEnc; + if (sourceEnc == null) { + sourceEnc = getPageEncodingForJspSyntax(jspReader, startMark); + if (sourceEnc == null) { + // Default to "ISO-8859-1" per JSP spec + sourceEnc = "ISO-8859-1"; + isDefaultPageEncoding = true; + } + } + } + + } + + /* + * Determines page source encoding for page or tag file in JSP syntax, + * by reading (in this order) the value of the 'pageEncoding' page + * directive attribute, or the charset value of the 'contentType' page + * directive attribute. + * + * @return The page encoding, or null if not found + */ + private String getPageEncodingForJspSyntax(JspReader jspReader, + Mark startMark) + throws JasperException { + + String encoding = null; + String saveEncoding = null; + + jspReader.reset(startMark); + + /* + * Determine page encoding from directive of the form <%@ page %>, + * <%@ tag %>, or . + */ + while (true) { + if (jspReader.skipUntil("<") == null) { + break; + } + // If this is a comment, skip until its end + if (jspReader.matches("%--")) { + if (jspReader.skipUntil("--%>") == null) { + // error will be caught in Parser + break; + } + continue; + } + boolean isDirective = jspReader.matches("%@"); + if (isDirective) { + jspReader.skipSpaces(); + } else { + isDirective = jspReader.matches("jsp:directive."); + } + if (!isDirective) { + continue; + } + + // Want to match tag and page but not taglib + if (jspReader.matches("tag") && !jspReader.matches("lib") || jspReader.matches("page")) { + + jspReader.skipSpaces(); + Attributes attrs = Parser.parseAttributes(this, jspReader); + encoding = getPageEncodingFromDirective(attrs, "pageEncoding"); + if (encoding != null) { + break; + } + encoding = getPageEncodingFromDirective(attrs, "contentType"); + if (encoding != null) { + saveEncoding = encoding; + } + } + } + + if (encoding == null) { + encoding = saveEncoding; + } + + return encoding; + } + + /* + * Scans the given attributes for the attribute with the given name, + * which is either 'pageEncoding' or 'contentType', and returns the + * specified page encoding. + * + * In the case of 'contentType', the page encoding is taken from the + * content type's 'charset' component. + * + * @param attrs The page directive attributes + * @param attrName The name of the attribute to search for (either + * 'pageEncoding' or 'contentType') + * + * @return The page encoding, or null + */ + private String getPageEncodingFromDirective(Attributes attrs, + String attrName) { + String value = attrs.getValue(attrName); + if (attrName.equals("pageEncoding")) { + return value; + } + + // attrName = contentType + String contentType = value; + String encoding = null; + if (contentType != null) { + int loc = contentType.indexOf(CHARSET); + if (loc != -1) { + encoding = contentType.substring(loc + CHARSET.length()); + } + } + + return encoding; + } + + /* + * Resolve the name of the file and update baseDirStack() to keep track of + * the current base directory for each included file. + * The 'root' file is always an 'absolute' path, so no need to put an + * initial value in the baseDirStack. + */ + private String resolveFileName(String inFileName) { + String fileName = inFileName.replace('\\', '/'); + boolean isAbsolute = fileName.startsWith("/"); + fileName = isAbsolute ? fileName : baseDirStack.peekFirst() + fileName; + String baseDir = fileName.substring(0, fileName.lastIndexOf('/') + 1); + baseDirStack.addFirst(baseDir); + return fileName; + } + + /* + * Checks to see if the given page contains, as its first element, a + * element whose prefix is bound to the JSP namespace, as in: + * + * + * ... + * + * + * @param reader The reader for this page + * + * @return true if this page contains a root element whose prefix is bound + * to the JSP namespace, and false otherwise + */ + private boolean hasJspRoot(JspReader reader) { + + // :root must be the first element + Mark start = null; + while ((start = reader.skipUntil("<")) != null) { + int c = reader.nextChar(); + if (c != '!' && c != '?') { + break; + } + } + if (start == null) { + return false; + } + Mark stop = reader.skipUntil(":root"); + if (stop == null) { + return false; + } + // call substring to get rid of leading '<' + String prefix = reader.getText(start, stop).substring(1); + + start = stop; + stop = reader.skipUntil(">"); + if (stop == null) { + return false; + } + + // Determine namespace associated with element's prefix + String root = reader.getText(start, stop); + String xmlnsDecl = "xmlns:" + prefix; + int index = root.indexOf(xmlnsDecl); + if (index == -1) { + return false; + } + index += xmlnsDecl.length(); + while (index < root.length() + && Character.isWhitespace(root.charAt(index))) { + index++; + } + if (index < root.length() && root.charAt(index) == '=') { + index++; + while (index < root.length() + && Character.isWhitespace(root.charAt(index))) { + index++; + } + if (index < root.length() + && (root.charAt(index) == '"' || root.charAt(index) == '\'')) { + index++; + if (root.regionMatches(index, JSP_URI, 0, JSP_URI.length())) { + return true; + } + } + } + + return false; + } +} diff --git a/java/org/apache/jasper/compiler/ScriptingVariabler.java b/java/org/apache/jasper/compiler/ScriptingVariabler.java new file mode 100644 index 0000000..fe733e4 --- /dev/null +++ b/java/org/apache/jasper/compiler/ScriptingVariabler.java @@ -0,0 +1,157 @@ +/* + * 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.jasper.compiler; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.jsp.tagext.TagVariableInfo; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.apache.jasper.JasperException; + +/** + * Class responsible for determining the scripting variables that every + * custom action needs to declare. + * + * @author Jan Luehe + */ +class ScriptingVariabler { + + private static final Integer MAX_SCOPE = Integer.valueOf(Integer.MAX_VALUE); + + /* + * Assigns an identifier (of type integer) to every custom tag, in order + * to help identify, for every custom tag, the scripting variables that it + * needs to declare. + */ + private static class CustomTagCounter extends Node.Visitor { + + private int count; + private Node.CustomTag parent; + + @Override + public void visit(Node.CustomTag n) throws JasperException { + n.setCustomTagParent(parent); + Node.CustomTag tmpParent = parent; + parent = n; + visitBody(n); + parent = tmpParent; + n.setNumCount(Integer.valueOf(count++)); + } + } + + /* + * For every custom tag, determines the scripting variables it needs to + * declare. + */ + private static class ScriptingVariableVisitor extends Node.Visitor { + + private final ErrorDispatcher err; + private final Map scriptVars; + + ScriptingVariableVisitor(ErrorDispatcher err) { + this.err = err; + scriptVars = new HashMap<>(); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + setScriptingVars(n, VariableInfo.AT_BEGIN); + setScriptingVars(n, VariableInfo.NESTED); + visitBody(n); + setScriptingVars(n, VariableInfo.AT_END); + } + + private void setScriptingVars(Node.CustomTag n, int scope) + throws JasperException { + + TagVariableInfo[] tagVarInfos = n.getTagVariableInfos(); + VariableInfo[] varInfos = n.getVariableInfos(); + if (tagVarInfos.length == 0 && varInfos.length == 0) { + return; + } + + List vec = new ArrayList<>(); + + Integer ownRange = null; + Node.CustomTag parent = n.getCustomTagParent(); + if (scope == VariableInfo.AT_BEGIN + || scope == VariableInfo.AT_END) { + if (parent == null) { + ownRange = MAX_SCOPE; + } else { + ownRange = parent.getNumCount(); + } + } else { + // NESTED + ownRange = n.getNumCount(); + } + + if (varInfos.length > 0) { + for (VariableInfo varInfo : varInfos) { + if (varInfo.getScope() != scope + || !varInfo.getDeclare()) { + continue; + } + String varName = varInfo.getVarName(); + + Integer currentRange = scriptVars.get(varName); + if (currentRange == null || + ownRange.compareTo(currentRange) > 0) { + scriptVars.put(varName, ownRange); + vec.add(varInfo); + } + } + } else { + for (TagVariableInfo tagVarInfo : tagVarInfos) { + if (tagVarInfo.getScope() != scope + || !tagVarInfo.getDeclare()) { + continue; + } + String varName = tagVarInfo.getNameGiven(); + if (varName == null) { + varName = n.getTagData().getAttributeString( + tagVarInfo.getNameFromAttribute()); + if (varName == null) { + err.jspError(n, + "jsp.error.scripting.variable.missing_name", + tagVarInfo.getNameFromAttribute()); + } + } + + Integer currentRange = scriptVars.get(varName); + if (currentRange == null || + ownRange.compareTo(currentRange) > 0) { + scriptVars.put(varName, ownRange); + vec.add(tagVarInfo); + } + } + } + + n.setScriptingVars(vec, scope); + } + } + + public static void set(Node.Nodes page, ErrorDispatcher err) + throws JasperException { + page.visit(new CustomTagCounter()); + page.visit(new ScriptingVariableVisitor(err)); + } +} diff --git a/java/org/apache/jasper/compiler/ServletWriter.java b/java/org/apache/jasper/compiler/ServletWriter.java new file mode 100644 index 0000000..14d8cbf --- /dev/null +++ b/java/org/apache/jasper/compiler/ServletWriter.java @@ -0,0 +1,172 @@ +/* + * 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.jasper.compiler; + +import java.io.PrintWriter; + +/** + * This is what is used to generate servlets. + * + * @author Anil K. Vijendran + * @author Kin-man Chung + */ +public class ServletWriter implements AutoCloseable { + + private static final int TAB_WIDTH = 2; + private static final String SPACES = " "; + + /** + * Current indent level. + */ + private int indent = 0; + private int virtual_indent = 0; + + /** + * The sink writer. + */ + private final PrintWriter writer; + + /** + * Servlet line numbers start from 1. + */ + private int javaLine = 1; + + + public ServletWriter(PrintWriter writer) { + this.writer = writer; + } + + @Override + public void close() { + writer.close(); + } + + + // -------------------- Access information -------------------- + + public int getJavaLine() { + return javaLine; + } + + + // -------------------- Formatting -------------------- + + public void pushIndent() { + virtual_indent += TAB_WIDTH; + if (virtual_indent >= 0 && virtual_indent <= SPACES.length()) { + indent = virtual_indent; + } + } + + public void popIndent() { + virtual_indent -= TAB_WIDTH; + if (virtual_indent >= 0 && virtual_indent <= SPACES.length()) { + indent = virtual_indent; + } + } + + /** + * Prints the given string followed by '\n' + * @param s The string + */ + public void println(String s) { + javaLine++; + writer.println(s); + } + + /** + * Prints a '\n' + */ + public void println() { + javaLine++; + writer.println(""); + } + + /** + * Prints the current indentation + */ + public void printin() { + writer.print(SPACES.substring(0, indent)); + } + + /** + * Prints the current indentation, followed by the given string + * @param s The string + */ + public void printin(String s) { + writer.print(SPACES.substring(0, indent)); + writer.print(s); + } + + /** + * Prints the current indentation, and then the string, and a '\n'. + * @param s The string + */ + public void printil(String s) { + javaLine++; + writer.print(SPACES.substring(0, indent)); + writer.println(s); + } + + /** + * Prints the given char. + * + * Use println() to print a '\n'. + * @param c The char + */ + public void print(char c) { + writer.print(c); + } + + /** + * Prints the given int. + * @param i The int + */ + public void print(int i) { + writer.print(i); + } + + /** + * Prints the given string. + * + * The string must not contain any '\n', otherwise the line count will be + * off. + * @param s The string + */ + public void print(String s) { + writer.print(s); + } + + /** + * Prints the given string. + * + * If the string spans multiple lines, the line count will be adjusted + * accordingly. + * @param s The string + */ + public void printMultiLn(String s) { + int index = 0; + + // look for hidden newlines inside strings + while ((index=s.indexOf('\n',index)) > -1 ) { + javaLine++; + index++; + } + + writer.print(s); + } +} diff --git a/java/org/apache/jasper/compiler/SmapInput.java b/java/org/apache/jasper/compiler/SmapInput.java new file mode 100644 index 0000000..858c052 --- /dev/null +++ b/java/org/apache/jasper/compiler/SmapInput.java @@ -0,0 +1,39 @@ +/* + * 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.jasper.compiler; + +public class SmapInput { + + private final String fileName; + private final int lineNumber; + + + public SmapInput(String fileName, int lineNumber) { + this.fileName = fileName; + this.lineNumber = lineNumber; + } + + + public String getFileName() { + return fileName; + } + + + public int getLineNumber() { + return lineNumber; + } +} diff --git a/java/org/apache/jasper/compiler/SmapStratum.java b/java/org/apache/jasper/compiler/SmapStratum.java new file mode 100644 index 0000000..5632dda --- /dev/null +++ b/java/org/apache/jasper/compiler/SmapStratum.java @@ -0,0 +1,397 @@ +/* + * 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.jasper.compiler; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the line and file mappings associated with a JSR-045 + * "stratum". + * + * @author Jayson Falkner + * @author Shawn Bayern + */ +public class SmapStratum { + + //********************************************************************* + // Class for storing LineInfo data + + /** + * Represents a single LineSection in an SMAP, associated with + * a particular stratum. + */ + static class LineInfo { + private int inputStartLine = -1; + private int outputStartLine = -1; + private int lineFileID = 0; + private int inputLineCount = 1; + private int outputLineIncrement = 1; + private boolean lineFileIDSet = false; + + public void setInputStartLine(int inputStartLine) { + if (inputStartLine < 0) { + throw new IllegalArgumentException(Localizer.getMessage( + "jsp.error.negativeParameter", Integer.valueOf(inputStartLine))); + } + this.inputStartLine = inputStartLine; + } + + public void setOutputStartLine(int outputStartLine) { + if (outputStartLine < 0) { + throw new IllegalArgumentException(Localizer.getMessage( + "jsp.error.negativeParameter", Integer.valueOf(outputStartLine))); + } + this.outputStartLine = outputStartLine; + } + + /** + * Sets lineFileID. Should be called only when different from + * that of prior LineInfo object (in any given context) or 0 + * if the current LineInfo has no (logical) predecessor. + * LineInfo will print this file number no matter what. + * + * @param lineFileID The new line file ID + */ + public void setLineFileID(int lineFileID) { + if (lineFileID < 0) { + throw new IllegalArgumentException(Localizer.getMessage( + "jsp.error.negativeParameter", Integer.valueOf(lineFileID))); + } + this.lineFileID = lineFileID; + this.lineFileIDSet = true; + } + + public void setInputLineCount(int inputLineCount) { + if (inputLineCount < 0) { + throw new IllegalArgumentException(Localizer.getMessage( + "jsp.error.negativeParameter", Integer.valueOf(inputLineCount))); + } + this.inputLineCount = inputLineCount; + } + + public void setOutputLineIncrement(int outputLineIncrement) { + if (outputLineIncrement < 0) { + throw new IllegalArgumentException(Localizer.getMessage( + "jsp.error.negativeParameter", Integer.valueOf(outputLineIncrement))); + } + this.outputLineIncrement = outputLineIncrement; + } + + public int getMaxOutputLineNumber() { + return outputStartLine + inputLineCount * outputLineIncrement; + } + + /** + * @return the current LineInfo as a String, print all values only when + * appropriate (but LineInfoID if and only if it's been + * specified, as its necessity is sensitive to context). + */ + public String getString() { + if (inputStartLine == -1 || outputStartLine == -1) { + throw new IllegalStateException(); + } + StringBuilder out = new StringBuilder(); + out.append(inputStartLine); + if (lineFileIDSet) { + out.append("#" + lineFileID); + } + if (inputLineCount != 1) { + out.append("," + inputLineCount); + } + out.append(":" + outputStartLine); + if (outputLineIncrement != 1) { + out.append("," + outputLineIncrement); + } + out.append('\n'); + return out.toString(); + } + + @Override + public String toString() { + return getString(); + } + } + + //********************************************************************* + // Private state + + private final List fileNameList = new ArrayList<>(); + private final List filePathList = new ArrayList<>(); + private final List lineData = new ArrayList<>(); + private int lastFileID; + // .java file + private String outputFileName; + // .class file + private String classFileName; + + //********************************************************************* + // Methods to add mapping information + + /** + * Adds record of a new file, by filename. + * + * @param filename the filename to add, unqualified by path. + */ + public void addFile(String filename) { + addFile(filename, filename); + } + + /** + * Adds record of a new file, by filename and path. The path + * may be relative to a source compilation path. + * + * @param filename the filename to add, unqualified by path + * @param filePath the path for the filename, potentially relative + * to a source compilation path + */ + public void addFile(String filename, String filePath) { + int pathIndex = filePathList.indexOf(filePath); + if (pathIndex == -1) { + fileNameList.add(filename); + filePathList.add(filePath); + } + } + + /** + * Combines consecutive LineInfos wherever possible + */ + public void optimizeLineSection() { + +/* Some debugging code + for (int i = 0; i < lineData.size(); i++) { + LineInfo li = (LineInfo)lineData.get(i); + System.out.print(li.toString()); + } + */ + //Incorporate each LineInfo into the previous LineInfo's + //outputLineIncrement, if possible + int i = 0; + while (i < lineData.size() - 1) { + LineInfo li = lineData.get(i); + LineInfo liNext = lineData.get(i + 1); + if (!liNext.lineFileIDSet + && liNext.inputStartLine == li.inputStartLine + && liNext.inputLineCount == 1 + && li.inputLineCount == 1 + && liNext.outputStartLine + == li.outputStartLine + + li.inputLineCount * li.outputLineIncrement) { + li.setOutputLineIncrement( + liNext.outputStartLine + - li.outputStartLine + + liNext.outputLineIncrement); + lineData.remove(i + 1); + } else { + i++; + } + } + + //Incorporate each LineInfo into the previous LineInfo's + //inputLineCount, if possible + i = 0; + while (i < lineData.size() - 1) { + LineInfo li = lineData.get(i); + LineInfo liNext = lineData.get(i + 1); + if (!liNext.lineFileIDSet + && liNext.inputStartLine == li.inputStartLine + li.inputLineCount + && liNext.outputLineIncrement == li.outputLineIncrement + && liNext.outputStartLine + == li.outputStartLine + + li.inputLineCount * li.outputLineIncrement) { + li.setInputLineCount(li.inputLineCount + liNext.inputLineCount); + lineData.remove(i + 1); + } else { + i++; + } + } + } + + /** + * Adds complete information about a simple line mapping. Specify + * all the fields in this method; the back-end machinery takes care + * of printing only those that are necessary in the final SMAP. + * (My view is that fields are optional primarily for spatial efficiency, + * not for programmer convenience. Could always add utility methods + * later.) + * + * @param inputStartLine starting line in the source file + * (SMAP InputStartLine) + * @param inputFileName the filepath (or name) from which the input comes + * (yields SMAP LineFileID) Use unqualified names + * carefully, and only when they uniquely identify a file. + * @param inputLineCount the number of lines in the input to map + * (SMAP LineFileCount) + * @param outputStartLine starting line in the output file + * (SMAP OutputStartLine) + * @param outputLineIncrement number of output lines to map to each + * input line (SMAP OutputLineIncrement). Given the + * fact that the name starts with "output", I continuously have + * the subconscious urge to call this field + * OutputLineExcrement. + */ + public void addLineData( + int inputStartLine, + String inputFileName, + int inputLineCount, + int outputStartLine, + int outputLineIncrement) { + // check the input - what are you doing here?? + int fileIndex = filePathList.indexOf(inputFileName); + if (fileIndex == -1) { + throw new IllegalArgumentException( + "inputFileName: " + inputFileName); + } + + //Jasper incorrectly SMAPs certain Nodes, giving them an + //outputStartLine of 0. This can cause a fatal error in + //optimizeLineSection, making it impossible for Jasper to + //compile the JSP. Until we can fix the underlying + //SMAPping problem, we simply ignore the flawed SMAP entries. + if (outputStartLine == 0) { + return; + } + + // build the LineInfo + LineInfo li = new LineInfo(); + li.setInputStartLine(inputStartLine); + li.setInputLineCount(inputLineCount); + li.setOutputStartLine(outputStartLine); + li.setOutputLineIncrement(outputLineIncrement); + if (fileIndex != lastFileID) { + li.setLineFileID(fileIndex); + } + lastFileID = fileIndex; + + // save it + lineData.add(li); + } + + + public void addLineInfo(LineInfo li) { + lineData.add(li); + } + + + public void setOutputFileName(String outputFileName) { + this.outputFileName = outputFileName; + } + + + public void setClassFileName(String classFileName) { + this.classFileName = classFileName; + } + + + public String getClassFileName() { + return classFileName; + } + + + //********************************************************************* + // Methods to retrieve information + + @Override + public String toString() { + return getSmapStringInternal(); + } + + + public String getSmapString() { + + if (outputFileName == null) { + throw new IllegalStateException(); + } + + return getSmapStringInternal(); + } + + + private String getSmapStringInternal() { + StringBuilder out = new StringBuilder(); + + // start the SMAP + out.append("SMAP\n"); + out.append(outputFileName + '\n'); + out.append("JSP\n"); + + // print StratumSection + out.append("*S JSP\n"); + + // print FileSection + out.append("*F\n"); + int bound = fileNameList.size(); + for (int i = 0; i < bound; i++) { + if (filePathList.get(i) != null) { + out.append("+ " + i + " " + fileNameList.get(i) + "\n"); + // Source paths must be relative, not absolute, so we + // remove the leading "/", if one exists. + String filePath = filePathList.get(i); + if (filePath.startsWith("/")) { + filePath = filePath.substring(1); + } + out.append(filePath + "\n"); + } else { + out.append(i + " " + fileNameList.get(i) + "\n"); + } + } + + // print LineSection + out.append("*L\n"); + bound = lineData.size(); + for (int i = 0; i < bound; i++) { + LineInfo li = lineData.get(i); + out.append(li.getString()); + } + + // end the SMAP + out.append("*E\n"); + + return out.toString(); + } + + + public SmapInput getInputLineNumber(int outputLineNumber) { + // For a given Java line number, provide the associated line number + // in the JSP/tag source + int inputLineNumber = -1; + int fileId = 0; + + for (LineInfo lineInfo : lineData) { + if (lineInfo.lineFileIDSet) { + fileId = lineInfo.lineFileID; + } + if (lineInfo.outputStartLine > outputLineNumber) { + // Didn't find match + break; + } + + if (lineInfo.getMaxOutputLineNumber() < outputLineNumber) { + // Too early + continue; + } + + // This is the match + int inputOffset = + (outputLineNumber - lineInfo.outputStartLine) / lineInfo.outputLineIncrement; + + inputLineNumber = lineInfo.inputStartLine + inputOffset; + } + + return new SmapInput(filePathList.get(fileId), inputLineNumber); + } +} diff --git a/java/org/apache/jasper/compiler/SmapUtil.java b/java/org/apache/jasper/compiler/SmapUtil.java new file mode 100644 index 0000000..53dabf4 --- /dev/null +++ b/java/org/apache/jasper/compiler/SmapUtil.java @@ -0,0 +1,834 @@ +/* + * 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.jasper.compiler; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.compiler.SmapStratum.LineInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Contains static utilities for generating SMAP data based on the + * current version of Jasper. + * + * @author Jayson Falkner + * @author Shawn Bayern + * @author Robert Field (inner SDEInstaller class) + * @author Mark Roth + * @author Kin-man Chung + */ +public class SmapUtil { + + //********************************************************************* + // Constants + + private static final Charset SMAP_ENCODING = StandardCharsets.UTF_8; + + //********************************************************************* + // Public entry points + + /** + * Generates an appropriate SMAP representing the current compilation + * context. (JSR-045.) + * + * @param ctxt Current compilation context + * @param pageNodes The current JSP page + * @return a SMAP for the page + * @throws IOException Error writing SMAP + */ + public static Map generateSmap(JspCompilationContext ctxt, + Node.Nodes pageNodes) throws IOException { + + Map smapInfo = new HashMap<>(); + + // Scan the nodes for presence of Jasper generated inner classes + PreScanVisitor psVisitor = new PreScanVisitor(); + try { + pageNodes.visit(psVisitor); + } catch (JasperException ex) { + } + HashMap map = psVisitor.getMap(); + + // Assemble info about our own stratum (JSP) using JspLineMap + SmapStratum s = new SmapStratum(); + + // Map out Node.Nodes + evaluateNodes(pageNodes, s, map, ctxt.getOptions().getMappedFile()); + s.optimizeLineSection(); + s.setOutputFileName(unqualify(ctxt.getServletJavaFileName())); + + String classFileName = ctxt.getClassFileName(); + s.setClassFileName(classFileName); + + smapInfo.put(ctxt.getFQCN(), s); + + if (ctxt.getOptions().isSmapDumped()) { + File outSmap = new File(classFileName + ".smap"); + PrintWriter so = + new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(outSmap), + SMAP_ENCODING)); + so.print(s.getSmapString()); + so.close(); + } + + for (Map.Entry entry : map.entrySet()) { + String innerClass = entry.getKey(); + s = entry.getValue(); + s.optimizeLineSection(); + s.setOutputFileName(unqualify(ctxt.getServletJavaFileName())); + String innerClassFileName = + classFileName.substring(0, classFileName.indexOf(".class")) + + '$' + innerClass + ".class"; + s.setClassFileName(innerClassFileName); + + smapInfo.put(ctxt.getFQCN() + "." + innerClass, s); + + if (ctxt.getOptions().isSmapDumped()) { + File outSmap = new File(innerClassFileName + ".smap"); + PrintWriter so = + new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(outSmap), + SMAP_ENCODING)); + so.print(s.getSmapString()); + so.close(); + } + } + + return smapInfo; + } + + public static void installSmap(Map smapInfo) + throws IOException { + if (smapInfo == null) { + return; + } + + for (Map.Entry entry : smapInfo.entrySet()) { + File outServlet = new File(entry.getValue().getClassFileName()); + SDEInstaller.install(outServlet, + entry.getValue().getSmapString().getBytes(StandardCharsets.ISO_8859_1)); + } + } + + //********************************************************************* + // Private utilities + + /** + * Returns an unqualified version of the given file path. + */ + private static String unqualify(String path) { + path = path.replace('\\', '/'); + return path.substring(path.lastIndexOf('/') + 1); + } + + //********************************************************************* + // Installation logic (from Robert Field, JSR-045 spec lead) + private static class SDEInstaller { + + private final Log log = LogFactory.getLog(SDEInstaller.class); // must not be static + + static final String nameSDE = "SourceDebugExtension"; + + byte[] orig; + byte[] sdeAttr; + byte[] gen; + + int origPos = 0; + int genPos = 0; + + int sdeIndex; + + static void install(File classFile, byte[] smap) throws IOException { + File tmpFile = new File(classFile.getPath() + "tmp"); + SDEInstaller installer = new SDEInstaller(classFile, smap); + installer.install(tmpFile); + if (!classFile.delete()) { + throw new IOException(Localizer.getMessage("jsp.error.unable.deleteClassFile", + classFile.getAbsolutePath())); + } + if (!tmpFile.renameTo(classFile)) { + throw new IOException(Localizer.getMessage("jsp.error.unable.renameClassFile", + tmpFile.getAbsolutePath(), classFile.getAbsolutePath())); + } + } + + SDEInstaller(File inClassFile, byte[] sdeAttr) + throws IOException { + if (!inClassFile.exists()) { + throw new FileNotFoundException(Localizer.getMessage("jsp.error.noFile", inClassFile)); + } + + this.sdeAttr = sdeAttr; + // get the bytes + orig = readWhole(inClassFile); + gen = new byte[orig.length + sdeAttr.length + 100]; + } + + void install(File outClassFile) throws IOException { + // do it + addSDE(); + + // write result + try (FileOutputStream outStream = new FileOutputStream(outClassFile)) { + outStream.write(gen, 0, genPos); + } + } + + static byte[] readWhole(File input) throws IOException { + int len = (int)input.length(); + byte[] bytes = new byte[len]; + try (FileInputStream inStream = new FileInputStream(input)) { + if (inStream.read(bytes, 0, len) != len) { + throw new IOException(Localizer.getMessage( + "jsp.error.readContent", Integer.valueOf(len))); + } + } + return bytes; + } + + void addSDE() throws UnsupportedEncodingException, IOException { + copy(4 + 2 + 2); // magic min/maj version + int constantPoolCountPos = genPos; + int constantPoolCount = readU2(); + if (log.isTraceEnabled()) { + log.trace("constant pool count: " + constantPoolCount); + } + writeU2(constantPoolCount); + + // copy old constant pool return index of SDE symbol, if found + sdeIndex = copyConstantPool(constantPoolCount); + if (sdeIndex < 0) { + // if "SourceDebugExtension" symbol not there add it + writeUtf8ForSDE(); + + // increment the constantPoolCount + sdeIndex = constantPoolCount; + ++constantPoolCount; + randomAccessWriteU2(constantPoolCountPos, constantPoolCount); + + if (log.isTraceEnabled()) { + log.trace("SourceDebugExtension not found, installed at: " + sdeIndex); + } + } else { + if (log.isTraceEnabled()) { + log.trace("SourceDebugExtension found at: " + sdeIndex); + } + } + copy(2 + 2 + 2); // access, this, super + int interfaceCount = readU2(); + writeU2(interfaceCount); + if (log.isTraceEnabled()) { + log.trace("interfaceCount: " + interfaceCount); + } + copy(interfaceCount * 2); + copyMembers(); // fields + copyMembers(); // methods + int attrCountPos = genPos; + int attrCount = readU2(); + writeU2(attrCount); + if (log.isTraceEnabled()) { + log.trace("class attrCount: " + attrCount); + } + // copy the class attributes, return true if SDE attr found (not copied) + if (!copyAttrs(attrCount)) { + // we will be adding SDE and it isn't already counted + ++attrCount; + randomAccessWriteU2(attrCountPos, attrCount); + if (log.isTraceEnabled()) { + log.trace("class attrCount incremented"); + } + } + writeAttrForSDE(sdeIndex); + } + + void copyMembers() { + int count = readU2(); + writeU2(count); + if (log.isTraceEnabled()) { + log.trace("members count: " + count); + } + for (int i = 0; i < count; ++i) { + copy(6); // access, name, descriptor + int attrCount = readU2(); + writeU2(attrCount); + if (log.isTraceEnabled()) { + log.trace("member attr count: " + attrCount); + } + copyAttrs(attrCount); + } + } + + boolean copyAttrs(int attrCount) { + boolean sdeFound = false; + for (int i = 0; i < attrCount; ++i) { + int nameIndex = readU2(); + // don't write old SDE + if (nameIndex == sdeIndex) { + sdeFound = true; + if (log.isTraceEnabled()) { + log.trace("SDE attr found"); + } + } else { + writeU2(nameIndex); // name + int len = readU4(); + writeU4(len); + copy(len); + if (log.isTraceEnabled()) { + log.trace("attr len: " + len); + } + } + } + return sdeFound; + } + + void writeAttrForSDE(int index) { + writeU2(index); + writeU4(sdeAttr.length); + for (byte b : sdeAttr) { + writeU1(b); + } + } + + void randomAccessWriteU2(int pos, int val) { + int savePos = genPos; + genPos = pos; + writeU2(val); + genPos = savePos; + } + + int readU1() { + return orig[origPos++] & 0xFF; + } + + int readU2() { + int res = readU1(); + return (res << 8) + readU1(); + } + + int readU4() { + int res = readU2(); + return (res << 16) + readU2(); + } + + void writeU1(int val) { + gen[genPos++] = (byte)val; + } + + void writeU2(int val) { + writeU1(val >> 8); + writeU1(val & 0xFF); + } + + void writeU4(int val) { + writeU2(val >> 16); + writeU2(val & 0xFFFF); + } + + void copy(int count) { + for (int i = 0; i < count; ++i) { + gen[genPos++] = orig[origPos++]; + } + } + + byte[] readBytes(int count) { + byte[] bytes = new byte[count]; + for (int i = 0; i < count; ++i) { + bytes[i] = orig[origPos++]; + } + return bytes; + } + + void writeBytes(byte[] bytes) { + for (byte aByte : bytes) { + gen[genPos++] = aByte; + } + } + + int copyConstantPool(int constantPoolCount) + throws UnsupportedEncodingException, IOException { + int sdeIndex = -1; + // copy const pool index zero not in class file + for (int i = 1; i < constantPoolCount; ++i) { + int tag = readU1(); + writeU1(tag); + switch (tag) { + case 7 : // Class + case 8 : // String + case 16 : // MethodType + if (log.isTraceEnabled()) { + log.trace(i + " copying 2 bytes"); + } + copy(2); + break; + case 15 : // MethodHandle + if (log.isTraceEnabled()) { + log.trace(i + " copying 3 bytes"); + } + copy(3); + break; + case 9 : // Field + case 10 : // Method + case 11 : // InterfaceMethod + case 3 : // Integer + case 4 : // Float + case 12 : // NameAndType + case 18 : // InvokeDynamic + if (log.isTraceEnabled()) { + log.trace(i + " copying 4 bytes"); + } + copy(4); + break; + case 5 : // Long + case 6 : // Double + if (log.isTraceEnabled()) { + log.trace(i + " copying 8 bytes"); + } + copy(8); + i++; + break; + case 1 : // Utf8 + int len = readU2(); + writeU2(len); + byte[] utf8 = readBytes(len); + String str = new String(utf8, "UTF-8"); + if (log.isTraceEnabled()) { + log.trace(i + " read class attr -- '" + str + "'"); + } + if (str.equals(nameSDE)) { + sdeIndex = i; + } + writeBytes(utf8); + break; + default : + throw new IOException(Localizer.getMessage( + "jsp.error.unexpectedTag", Integer.valueOf(tag))); + } + } + return sdeIndex; + } + + void writeUtf8ForSDE() { + int len = nameSDE.length(); + writeU1(1); // Utf8 tag + writeU2(len); + for (int i = 0; i < len; ++i) { + writeU1(nameSDE.charAt(i)); + } + } + } + + public static void evaluateNodes( + Node.Nodes nodes, + SmapStratum s, + HashMap innerClassMap, + boolean breakAtLF) { + try { + nodes.visit(new SmapGenVisitor(s, breakAtLF, innerClassMap)); + } catch (JasperException ex) { + } + } + + private static class SmapGenVisitor extends Node.Visitor { + + private SmapStratum smap; + private final boolean breakAtLF; + private final HashMap innerClassMap; + + SmapGenVisitor(SmapStratum s, boolean breakAtLF, HashMap map) { + this.smap = s; + this.breakAtLF = breakAtLF; + this.innerClassMap = map; + } + + @Override + public void visitBody(Node n) throws JasperException { + SmapStratum smapSave = smap; + String innerClass = n.getInnerClassName(); + if (innerClass != null) { + this.smap = innerClassMap.get(innerClass); + } + super.visitBody(n); + smap = smapSave; + } + + @Override + public void visit(Node.Declaration n) throws JasperException { + doSmapText(n); + } + + @Override + public void visit(Node.Expression n) throws JasperException { + doSmapText(n); + } + + @Override + public void visit(Node.Scriptlet n) throws JasperException { + doSmapText(n); + } + + @Override + public void visit(Node.IncludeAction n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.ForwardAction n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.GetProperty n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.SetProperty n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.UseBean n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.PlugIn n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.UninterpretedTag n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.JspElement n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.JspText n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.NamedAttribute n) throws JasperException { + visitBody(n); + } + + @Override + public void visit(Node.JspBody n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.InvokeAction n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.DoBodyAction n) throws JasperException { + doSmap(n); + visitBody(n); + } + + @Override + public void visit(Node.ELExpression n) throws JasperException { + doSmap(n); + } + + @Override + public void visit(Node.TemplateText n) throws JasperException { + Mark mark = n.getStart(); + if (mark == null) { + return; + } + + //Add the file information + String fileName = mark.getFile(); + smap.addFile(unqualify(fileName), fileName); + + //Add a LineInfo that corresponds to the beginning of this node + int iInputStartLine = mark.getLineNumber(); + int iOutputStartLine = n.getBeginJavaLine(); + int iOutputLineIncrement = breakAtLF? 1: 0; + smap.addLineData(iInputStartLine, fileName, 1, iOutputStartLine, + iOutputLineIncrement); + + // Output additional mappings in the text + java.util.ArrayList extraSmap = n.getExtraSmap(); + + if (extraSmap != null) { + for (Integer integer : extraSmap) { + iOutputStartLine += iOutputLineIncrement; + smap.addLineData( + iInputStartLine + integer.intValue(), + fileName, + 1, + iOutputStartLine, + iOutputLineIncrement); + } + } + } + + private void doSmap( + Node n, + int inLineCount, + int outIncrement, + int skippedLines) { + Mark mark = n.getStart(); + if (mark == null) { + return; + } + + String unqualifiedName = unqualify(mark.getFile()); + smap.addFile(unqualifiedName, mark.getFile()); + smap.addLineData( + mark.getLineNumber() + skippedLines, + mark.getFile(), + inLineCount - skippedLines, + n.getBeginJavaLine() + skippedLines, + outIncrement); + } + + private void doSmap(Node n) { + doSmap(n, 1, n.getEndJavaLine() - n.getBeginJavaLine(), 0); + } + + private void doSmapText(Node n) { + String text = n.getText(); + int index = 0; + int next = 0; + int lineCount = 1; + int skippedLines = 0; + boolean slashStarSeen = false; + boolean beginning = true; + + // Count lines inside text, but skipping comment lines at the + // beginning of the text. + while ((next = text.indexOf('\n', index)) > -1) { + if (beginning) { + String line = text.substring(index, next).trim(); + if (!slashStarSeen && line.startsWith("/*")) { + slashStarSeen = true; + } + if (slashStarSeen) { + skippedLines++; + int endIndex = line.indexOf("*/"); + if (endIndex >= 0) { + // End of /* */ comment + slashStarSeen = false; + if (endIndex < line.length() - 2) { + // Some executable code after comment + skippedLines--; + beginning = false; + } + } + } else if (line.length() == 0 || line.startsWith("//")) { + skippedLines++; + } else { + beginning = false; + } + } + lineCount++; + index = next + 1; + } + + doSmap(n, lineCount, 1, skippedLines); + } + } + + private static class PreScanVisitor extends Node.Visitor { + + HashMap map = new HashMap<>(); + + @Override + public void doVisit(Node n) { + String inner = n.getInnerClassName(); + if (inner != null && !map.containsKey(inner)) { + map.put(inner, new SmapStratum()); + } + } + + HashMap getMap() { + return map; + } + } + + public static SmapStratum loadSmap(String className, ClassLoader cl) { + // Extract SMAP from class file. First line "SMAP" is not included + String smap = getSmap(className, cl); + + if (smap == null) { + return null; + } + + SmapStratum smapStratum = new SmapStratum(); + + String[] lines = smap.split("\n"); + int lineIndex = 0; + + // First line is output file name + smapStratum.setOutputFileName(lines[lineIndex]); + + // There is only one stratum (JSP) so skip to the start of the file + // section + lineIndex = 4; + + while (!lines[lineIndex].equals("*L")) { + int i = lines[lineIndex].lastIndexOf(' '); + String fileName = lines[lineIndex].substring(i + 1); + smapStratum.addFile(fileName, lines[++lineIndex]); + lineIndex++; + } + + // Skip *L + lineIndex++; + + while (!lines[lineIndex].equals("*E")) { + LineInfo li = new LineInfo(); + // Split into in and out + String[] inOut = lines[lineIndex].split(":"); + // Split in on comma (might not be one) + String[] in = inOut[0].split(","); + if (in.length == 2) { + // There is a count + li.setInputLineCount(Integer.parseInt(in[1])); + } + // Check for fileID + String[] start = in[0].split("#"); + if (start.length == 2) { + // There is a file ID + li.setLineFileID(Integer.parseInt(start[1])); + } + li.setInputStartLine(Integer.parseInt(start[0])); + // Split out + String[] out = inOut[1].split(","); + if (out.length == 2) { + // There is an increment + li.setOutputLineIncrement(Integer.parseInt(out[1])); + } + li.setOutputStartLine(Integer.parseInt(out[0])); + + smapStratum.addLineInfo(li); + + lineIndex++; + } + + return smapStratum; + } + + + private static String getSmap(String className, ClassLoader cl) { + Charset encoding = StandardCharsets.ISO_8859_1; + boolean found = false; + String smap = null; + + InputStream is = null; + try { + is = cl.getResourceAsStream(className.replace(".","/") + ".smap"); + if (is != null) { + encoding = SMAP_ENCODING; + found = true; + } else { + is = cl.getResourceAsStream(className.replace(".","/") + ".class"); + // Alternative approach would be to read the class file as per the + // JLS. That would require duplicating a lot of BCEL functionality. + int b = is.read(); + while (b != -1) { + if (b == 'S') { + if ((b = is.read()) != 'M') { + continue; + } + if ((b = is.read()) != 'A') { + continue; + } + if ((b = is.read()) != 'P') { + continue; + } + if ((b = is.read()) != '\n') { + continue; + } + found = true; + break; + } + b = is.read(); + } + } + + if (found) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + byte[] buf = new byte[1024]; + int numRead; + while ( (numRead = is.read(buf) ) >= 0) { + baos.write(buf, 0, numRead); + } + + smap = new String(baos.toByteArray(), encoding); + } + } catch (IOException ioe) { + Log log = LogFactory.getLog(SmapUtil.class); + log.warn(Localizer.getMessage("jsp.warning.loadSmap", className), ioe); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ioe) { + Log log = LogFactory.getLog(SmapUtil.class); + log.warn(Localizer.getMessage("jsp.warning.loadSmap", className), ioe); + } + } + } + return smap; + } +} diff --git a/java/org/apache/jasper/compiler/StringInterpreter.java b/java/org/apache/jasper/compiler/StringInterpreter.java new file mode 100644 index 0000000..038e262 --- /dev/null +++ b/java/org/apache/jasper/compiler/StringInterpreter.java @@ -0,0 +1,49 @@ +/* + * 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.jasper.compiler; + +/** + * Defines the interface for the String interpreter. This allows users to + * provide custom String interpreter implementations that can optimise + * String processing for an application by performing code generation for + * a sub-set of Strings. + */ +public interface StringInterpreter { + + /** + * Generates the source code that represents the conversion of the string + * value to the appropriate type. + * + * @param c + * The target class to which to coerce the given string + * @param s + * The string value + * @param attrName + * The name of the attribute whose value is being supplied + * @param propEditorClass + * The property editor for the given attribute + * @param isNamedAttribute + * true if the given attribute is a named attribute (that + * is, specified using the jsp:attribute standard action), + * and false otherwise + * + * @return the string representing the code that will be inserted into the + * source code for the Servlet generated for the JSP. + */ + String convertString(Class c, String s, String attrName, + Class propEditorClass, boolean isNamedAttribute); +} diff --git a/java/org/apache/jasper/compiler/StringInterpreterFactory.java b/java/org/apache/jasper/compiler/StringInterpreterFactory.java new file mode 100644 index 0000000..b035752 --- /dev/null +++ b/java/org/apache/jasper/compiler/StringInterpreterFactory.java @@ -0,0 +1,178 @@ +/* + * 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.jasper.compiler; + +import jakarta.servlet.ServletContext; + +/** + * Provides {@link StringInterpreter} instances for JSP compilation. + * + * The search order is as follows: + *
      + *
    1. StringInterpreter instance or implementation class name provided as a + * ServletContext attribute
    2. + *
    3. Implementation class named in a ServletContext initialisation parameter + *
    4. + *
    5. Default implementation
    6. + *
    + */ +public class StringInterpreterFactory { + + public static final String STRING_INTERPRETER_CLASS_NAME = StringInterpreter.class.getName(); + + private static final StringInterpreter DEFAULT_INSTANCE = new DefaultStringInterpreter(); + + + /** + * Obtain the correct String Interpreter for the given web application. + * @param context The Servlet context + * @return the String interpreter + * @throws Exception If an error occurs creating the interpreter + */ + public static StringInterpreter getStringInterpreter(ServletContext context) + throws Exception { + + StringInterpreter result = null; + + // Search for an implementation + // 1. ServletContext attribute (set by application or cached by a + // previous call to this method). + Object attribute = context.getAttribute(STRING_INTERPRETER_CLASS_NAME); + if (attribute instanceof StringInterpreter) { + return (StringInterpreter) attribute; + } else if (attribute instanceof String) { + result = createInstance(context, (String) attribute); + } + + // 2. ServletContext init parameter + if (result == null) { + String className = context.getInitParameter(STRING_INTERPRETER_CLASS_NAME); + if (className != null) { + result = createInstance(context, className); + } + } + + // 3. Default + if (result == null) { + result = DEFAULT_INSTANCE; + } + + // Cache the result for next time + context.setAttribute(STRING_INTERPRETER_CLASS_NAME, result); + return result; + } + + + private static StringInterpreter createInstance(ServletContext context, + String className) throws Exception { + return (StringInterpreter) context.getClassLoader().loadClass( + className).getConstructor().newInstance(); + } + + + private StringInterpreterFactory() { + // Utility class. Hide default constructor. + } + + + public static class DefaultStringInterpreter implements StringInterpreter { + + @Override + public String convertString(Class c, String s, String attrName, + Class propEditorClass, boolean isNamedAttribute) { + + String quoted = s; + if (!isNamedAttribute) { + quoted = Generator.quote(s); + } + + if (propEditorClass != null) { + String className = c.getCanonicalName(); + return "(" + + className + + ")org.apache.jasper.runtime.JspRuntimeLibrary.getValueFromBeanInfoPropertyEditor(" + + className + ".class, \"" + attrName + "\", " + quoted + + ", " + propEditorClass.getCanonicalName() + ".class)"; + } else if (c == String.class) { + return quoted; + } else if (c == boolean.class) { + return JspUtil.coerceToPrimitiveBoolean(s, isNamedAttribute); + } else if (c == Boolean.class) { + return JspUtil.coerceToBoolean(s, isNamedAttribute); + } else if (c == byte.class) { + return JspUtil.coerceToPrimitiveByte(s, isNamedAttribute); + } else if (c == Byte.class) { + return JspUtil.coerceToByte(s, isNamedAttribute); + } else if (c == char.class) { + return JspUtil.coerceToChar(s, isNamedAttribute); + } else if (c == Character.class) { + return JspUtil.coerceToCharacter(s, isNamedAttribute); + } else if (c == double.class) { + return JspUtil.coerceToPrimitiveDouble(s, isNamedAttribute); + } else if (c == Double.class) { + return JspUtil.coerceToDouble(s, isNamedAttribute); + } else if (c == float.class) { + return JspUtil.coerceToPrimitiveFloat(s, isNamedAttribute); + } else if (c == Float.class) { + return JspUtil.coerceToFloat(s, isNamedAttribute); + } else if (c == int.class) { + return JspUtil.coerceToInt(s, isNamedAttribute); + } else if (c == Integer.class) { + return JspUtil.coerceToInteger(s, isNamedAttribute); + } else if (c == short.class) { + return JspUtil.coerceToPrimitiveShort(s, isNamedAttribute); + } else if (c == Short.class) { + return JspUtil.coerceToShort(s, isNamedAttribute); + } else if (c == long.class) { + return JspUtil.coerceToPrimitiveLong(s, isNamedAttribute); + } else if (c == Long.class) { + return JspUtil.coerceToLong(s, isNamedAttribute); + } else if (c == Object.class) { + return quoted; + } + + String result = coerceToOtherType(c, s, isNamedAttribute); + + if (result != null) { + return result; + } + + String className = c.getCanonicalName(); + return "(" + + className + + ")org.apache.jasper.runtime.JspRuntimeLibrary.getValueFromPropertyEditorManager(" + + className + ".class, \"" + attrName + "\", " + quoted + + ")"; + } + + + /** + * Intended to be used by sub-classes that don't need/want to + * re-implement the logic in + * {@link #convertString(Class, String, String, Class, boolean)}. + * + * @param c unused + * @param s unused + * @param isNamedAttribute unused + * + * @return Always {@code null} + */ + protected String coerceToOtherType(Class c, String s, boolean isNamedAttribute) { + return null; + } + } +} diff --git a/java/org/apache/jasper/compiler/TagConstants.java b/java/org/apache/jasper/compiler/TagConstants.java new file mode 100644 index 0000000..28c8d5e --- /dev/null +++ b/java/org/apache/jasper/compiler/TagConstants.java @@ -0,0 +1,115 @@ +/* + * 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.jasper.compiler; + +public interface TagConstants { + + String JSP_URI = "http://java.sun.com/JSP/Page"; + + String DIRECTIVE_ACTION = "directive."; + + String ROOT_ACTION = "root"; + String JSP_ROOT_ACTION = "jsp:root"; + + String PAGE_DIRECTIVE_ACTION = "directive.page"; + String JSP_PAGE_DIRECTIVE_ACTION = "jsp:directive.page"; + + String INCLUDE_DIRECTIVE_ACTION = "directive.include"; + String JSP_INCLUDE_DIRECTIVE_ACTION = "jsp:directive.include"; + + String DECLARATION_ACTION = "declaration"; + String JSP_DECLARATION_ACTION = "jsp:declaration"; + + String SCRIPTLET_ACTION = "scriptlet"; + String JSP_SCRIPTLET_ACTION = "jsp:scriptlet"; + + String EXPRESSION_ACTION = "expression"; + String JSP_EXPRESSION_ACTION = "jsp:expression"; + + String USE_BEAN_ACTION = "useBean"; + String JSP_USE_BEAN_ACTION = "jsp:useBean"; + + String SET_PROPERTY_ACTION = "setProperty"; + String JSP_SET_PROPERTY_ACTION = "jsp:setProperty"; + + String GET_PROPERTY_ACTION = "getProperty"; + String JSP_GET_PROPERTY_ACTION = "jsp:getProperty"; + + String INCLUDE_ACTION = "include"; + String JSP_INCLUDE_ACTION = "jsp:include"; + + String FORWARD_ACTION = "forward"; + String JSP_FORWARD_ACTION = "jsp:forward"; + + String PARAM_ACTION = "param"; + String JSP_PARAM_ACTION = "jsp:param"; + + String PARAMS_ACTION = "params"; + String JSP_PARAMS_ACTION = "jsp:params"; + + String PLUGIN_ACTION = "plugin"; + String JSP_PLUGIN_ACTION = "jsp:plugin"; + + String FALLBACK_ACTION = "fallback"; + String JSP_FALLBACK_ACTION = "jsp:fallback"; + + String TEXT_ACTION = "text"; + String JSP_TEXT_ACTION = "jsp:text"; + String JSP_TEXT_ACTION_END = ""; + + String ATTRIBUTE_ACTION = "attribute"; + String JSP_ATTRIBUTE_ACTION = "jsp:attribute"; + + String BODY_ACTION = "body"; + String JSP_BODY_ACTION = "jsp:body"; + + String ELEMENT_ACTION = "element"; + String JSP_ELEMENT_ACTION = "jsp:element"; + + String OUTPUT_ACTION = "output"; + String JSP_OUTPUT_ACTION = "jsp:output"; + + String TAGLIB_DIRECTIVE_ACTION = "taglib"; + String JSP_TAGLIB_DIRECTIVE_ACTION = "jsp:taglib"; + + /* + * Tag Files + */ + String INVOKE_ACTION = "invoke"; + String JSP_INVOKE_ACTION = "jsp:invoke"; + + String DOBODY_ACTION = "doBody"; + String JSP_DOBODY_ACTION = "jsp:doBody"; + + /* + * Tag File Directives + */ + String TAG_DIRECTIVE_ACTION = "directive.tag"; + String JSP_TAG_DIRECTIVE_ACTION = "jsp:directive.tag"; + + String ATTRIBUTE_DIRECTIVE_ACTION = "directive.attribute"; + String JSP_ATTRIBUTE_DIRECTIVE_ACTION = "jsp:directive.attribute"; + + String VARIABLE_DIRECTIVE_ACTION = "directive.variable"; + String JSP_VARIABLE_DIRECTIVE_ACTION = "jsp:directive.variable"; + + /* + * Directive attributes + */ + String URN_JSPTAGDIR = "urn:jsptagdir:"; + String URN_JSPTLD = "urn:jsptld:"; +} diff --git a/java/org/apache/jasper/compiler/TagFileProcessor.java b/java/org/apache/jasper/compiler/TagFileProcessor.java new file mode 100644 index 0000000..0663155 --- /dev/null +++ b/java/org/apache/jasper/compiler/TagFileProcessor.java @@ -0,0 +1,711 @@ +/* + * 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.jasper.compiler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import jakarta.el.MethodExpression; +import jakarta.el.ValueExpression; +import jakarta.servlet.jsp.tagext.JspFragment; +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagFileInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; +import jakarta.servlet.jsp.tagext.TagVariableInfo; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.runtime.JspSourceDependent; +import org.apache.jasper.servlet.JspServletWrapper; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; + +/** + * 1. Processes and extracts the directive info in a tag file. 2. Compiles and + * loads tag files used in a JSP file. + * + * @author Kin-man Chung + */ + +class TagFileProcessor { + + private List tempVector; + + /** + * A visitor the tag file + */ + private static class TagFileDirectiveVisitor extends Node.Visitor { + + private static final JspUtil.ValidAttribute[] tagDirectiveAttrs = { + new JspUtil.ValidAttribute("display-name"), + new JspUtil.ValidAttribute("body-content"), + new JspUtil.ValidAttribute("dynamic-attributes"), + new JspUtil.ValidAttribute("small-icon"), + new JspUtil.ValidAttribute("large-icon"), + new JspUtil.ValidAttribute("description"), + new JspUtil.ValidAttribute("example"), + new JspUtil.ValidAttribute("pageEncoding"), + new JspUtil.ValidAttribute("language"), + new JspUtil.ValidAttribute("import"), + new JspUtil.ValidAttribute("deferredSyntaxAllowedAsLiteral"), // JSP 2.1 + new JspUtil.ValidAttribute("trimDirectiveWhitespaces"), // JSP 2.1 + new JspUtil.ValidAttribute("isELIgnored"), + new JspUtil.ValidAttribute("errorOnELNotFound") }; + + private static final JspUtil.ValidAttribute[] attributeDirectiveAttrs = { + new JspUtil.ValidAttribute("name", true), + new JspUtil.ValidAttribute("required"), + new JspUtil.ValidAttribute("fragment"), + new JspUtil.ValidAttribute("rtexprvalue"), + new JspUtil.ValidAttribute("type"), + new JspUtil.ValidAttribute("deferredValue"), // JSP 2.1 + new JspUtil.ValidAttribute("deferredValueType"), // JSP 2.1 + new JspUtil.ValidAttribute("deferredMethod"), // JSP 2 + new JspUtil.ValidAttribute("deferredMethodSignature"), // JSP 21 + new JspUtil.ValidAttribute("description") }; + + private static final JspUtil.ValidAttribute[] variableDirectiveAttrs = { + new JspUtil.ValidAttribute("name-given"), + new JspUtil.ValidAttribute("name-from-attribute"), + new JspUtil.ValidAttribute("alias"), + new JspUtil.ValidAttribute("variable-class"), + new JspUtil.ValidAttribute("scope"), + new JspUtil.ValidAttribute("declare"), + new JspUtil.ValidAttribute("description") }; + + private ErrorDispatcher err; + + private TagLibraryInfo tagLibInfo; + + private String name = null; + + private String path = null; + + private String bodycontent = null; + + private String description = null; + + private String displayName = null; + + private String smallIcon = null; + + private String largeIcon = null; + + private String dynamicAttrsMapName; + + private String example = null; + + private List attributeList; + + private List variableList; + + private static final String ATTR_NAME = "the name attribute of the attribute directive"; + + private static final String VAR_NAME_GIVEN = "the name-given attribute of the variable directive"; + + private static final String VAR_NAME_FROM = "the name-from-attribute attribute of the variable directive"; + + private static final String VAR_ALIAS = "the alias attribute of the variable directive"; + + private static final String TAG_DYNAMIC = "the dynamic-attributes attribute of the tag directive"; + + private Map nameTable = new HashMap<>(); + + private Map nameFromTable = new HashMap<>(); + + TagFileDirectiveVisitor(Compiler compiler, + TagLibraryInfo tagLibInfo, String name, String path) { + err = compiler.getErrorDispatcher(); + this.tagLibInfo = tagLibInfo; + this.name = name; + this.path = path; + attributeList = new ArrayList<>(); + variableList = new ArrayList<>(); + } + + @Override + public void visit(Node.TagDirective n) throws JasperException { + + JspUtil.checkAttributes("Tag directive", n, tagDirectiveAttrs, err); + + bodycontent = checkConflict(n, bodycontent, "body-content"); + if (bodycontent != null + && !bodycontent + .equalsIgnoreCase(TagInfo.BODY_CONTENT_EMPTY) + && !bodycontent + .equalsIgnoreCase(TagInfo.BODY_CONTENT_TAG_DEPENDENT) + && !bodycontent + .equalsIgnoreCase(TagInfo.BODY_CONTENT_SCRIPTLESS)) { + err.jspError(n, "jsp.error.tagdirective.badbodycontent", + bodycontent); + } + dynamicAttrsMapName = checkConflict(n, dynamicAttrsMapName, + "dynamic-attributes"); + if (dynamicAttrsMapName != null) { + checkUniqueName(dynamicAttrsMapName, TAG_DYNAMIC, n); + } + smallIcon = checkConflict(n, smallIcon, "small-icon"); + largeIcon = checkConflict(n, largeIcon, "large-icon"); + description = checkConflict(n, description, "description"); + displayName = checkConflict(n, displayName, "display-name"); + example = checkConflict(n, example, "example"); + } + + private String checkConflict(Node n, String oldAttrValue, String attr) + throws JasperException { + + String result = oldAttrValue; + String attrValue = n.getAttributeValue(attr); + if (attrValue != null) { + if (oldAttrValue != null && !oldAttrValue.equals(attrValue)) { + err.jspError(n, "jsp.error.tag.conflict.attr", attr, + oldAttrValue, attrValue); + } + result = attrValue; + } + return result; + } + + @Override + public void visit(Node.AttributeDirective n) throws JasperException { + + JspUtil.checkAttributes("Attribute directive", n, + attributeDirectiveAttrs, err); + + // JSP 2.1 Table JSP.8-3 + // handle deferredValue and deferredValueType + boolean deferredValue = false; + boolean deferredValueSpecified = false; + String deferredValueString = n.getAttributeValue("deferredValue"); + if (deferredValueString != null) { + deferredValueSpecified = true; + deferredValue = JspUtil.booleanValue(deferredValueString); + } + String deferredValueType = n.getAttributeValue("deferredValueType"); + if (deferredValueType != null) { + if (deferredValueSpecified && !deferredValue) { + err.jspError(n, "jsp.error.deferredvaluetypewithoutdeferredvalue"); + } else { + deferredValue = true; + } + } else if (deferredValue) { + deferredValueType = "java.lang.Object"; + } else { + deferredValueType = "java.lang.String"; + } + + // JSP 2.1 Table JSP.8-3 + // handle deferredMethod and deferredMethodSignature + boolean deferredMethod = false; + boolean deferredMethodSpecified = false; + String deferredMethodString = n.getAttributeValue("deferredMethod"); + if (deferredMethodString != null) { + deferredMethodSpecified = true; + deferredMethod = JspUtil.booleanValue(deferredMethodString); + } + String deferredMethodSignature = n + .getAttributeValue("deferredMethodSignature"); + if (deferredMethodSignature != null) { + if (deferredMethodSpecified && !deferredMethod) { + err.jspError(n, "jsp.error.deferredmethodsignaturewithoutdeferredmethod"); + } else { + deferredMethod = true; + } + } else if (deferredMethod) { + deferredMethodSignature = "void methodname()"; + } + + if (deferredMethod && deferredValue) { + err.jspError(n, "jsp.error.deferredmethodandvalue"); + } + + String attrName = n.getAttributeValue("name"); + boolean required = JspUtil.booleanValue(n + .getAttributeValue("required")); + boolean rtexprvalue = true; + String rtexprvalueString = n.getAttributeValue("rtexprvalue"); + if (rtexprvalueString != null) { + rtexprvalue = JspUtil.booleanValue(rtexprvalueString); + } + boolean fragment = JspUtil.booleanValue(n + .getAttributeValue("fragment")); + String type = n.getAttributeValue("type"); + if (fragment) { + // type is fixed to "JspFragment" and a translation error + // must occur if specified. + if (type != null) { + err.jspError(n, "jsp.error.fragmentwithtype", JspFragment.class.getName()); + } + // rtexprvalue is fixed to "true" and a translation error + // must occur if specified. + rtexprvalue = true; + if (rtexprvalueString != null) { + err.jspError(n, "jsp.error.frgmentwithrtexprvalue"); + } + } else { + if (type == null) { + type = "java.lang.String"; + } + + if (deferredValue) { + type = ValueExpression.class.getName(); + } else if (deferredMethod) { + type = MethodExpression.class.getName(); + } + } + + if (("2.0".equals(tagLibInfo.getRequiredVersion()) || ("1.2".equals(tagLibInfo.getRequiredVersion()))) + && (deferredMethodSpecified || deferredMethod + || deferredValueSpecified || deferredValue)) { + err.jspError("jsp.error.invalid.version", path); + } + + TagAttributeInfo tagAttributeInfo = new TagAttributeInfo(attrName, + required, type, rtexprvalue, fragment, null, deferredValue, + deferredMethod, deferredValueType, deferredMethodSignature); + attributeList.add(tagAttributeInfo); + checkUniqueName(attrName, ATTR_NAME, n, tagAttributeInfo); + } + + @Override + public void visit(Node.VariableDirective n) throws JasperException { + + JspUtil.checkAttributes("Variable directive", n, + variableDirectiveAttrs, err); + + String nameGiven = n.getAttributeValue("name-given"); + String nameFromAttribute = n + .getAttributeValue("name-from-attribute"); + if (nameGiven == null && nameFromAttribute == null) { + err.jspError("jsp.error.variable.either.name"); + } + + if (nameGiven != null && nameFromAttribute != null) { + err.jspError("jsp.error.variable.both.name"); + } + + String alias = n.getAttributeValue("alias"); + if (nameFromAttribute != null && alias == null + || nameFromAttribute == null && alias != null) { + err.jspError("jsp.error.variable.alias"); + } + + String className = n.getAttributeValue("variable-class"); + if (className == null) { + className = "java.lang.String"; + } + + String declareStr = n.getAttributeValue("declare"); + boolean declare = true; + if (declareStr != null) { + declare = JspUtil.booleanValue(declareStr); + } + + int scope = VariableInfo.NESTED; + String scopeStr = n.getAttributeValue("scope"); + if (scopeStr != null) { + if ("NESTED".equals(scopeStr)) { + // Already the default + } else if ("AT_BEGIN".equals(scopeStr)) { + scope = VariableInfo.AT_BEGIN; + } else if ("AT_END".equals(scopeStr)) { + scope = VariableInfo.AT_END; + } + } + + if (nameFromAttribute != null) { + /* + * An alias has been specified. We use 'nameGiven' to hold the + * value of the alias, and 'nameFromAttribute' to hold the name + * of the attribute whose value (at invocation-time) denotes the + * name of the variable that is being aliased + */ + nameGiven = alias; + checkUniqueName(nameFromAttribute, VAR_NAME_FROM, n); + checkUniqueName(alias, VAR_ALIAS, n); + } else { + // name-given specified + checkUniqueName(nameGiven, VAR_NAME_GIVEN, n); + } + + variableList.add(new TagVariableInfo(nameGiven, nameFromAttribute, className, declare, scope)); + } + + public TagInfo getTagInfo(String packageName) throws JasperException { + + if (name == null) { + // XXX Get it from tag file name + } + + if (bodycontent == null) { + bodycontent = TagInfo.BODY_CONTENT_SCRIPTLESS; + } + + String tagClassName = JspUtil.getTagHandlerClassName( + path, packageName, tagLibInfo.getReliableURN(), err); + + TagVariableInfo[] tagVariableInfos = variableList.toArray(new TagVariableInfo[0]); + TagAttributeInfo[] tagAttributeInfo = attributeList.toArray(new TagAttributeInfo[0]); + + return new JasperTagInfo(name, tagClassName, bodycontent, + description, tagLibInfo, null, tagAttributeInfo, + displayName, smallIcon, largeIcon, tagVariableInfos, + dynamicAttrsMapName); + } + + static class NameEntry { + private String type; + + private Node node; + + private TagAttributeInfo attr; + + NameEntry(String type, Node node, TagAttributeInfo attr) { + this.type = type; + this.node = node; + this.attr = attr; + } + + String getType() { + return type; + } + + Node getNode() { + return node; + } + + TagAttributeInfo getTagAttributeInfo() { + return attr; + } + } + + /** + * Reports a translation error if names specified in attributes of + * directives are not unique in this translation unit. + * + * The value of the following attributes must be unique. 1. 'name' + * attribute of an attribute directive 2. 'name-given' attribute of a + * variable directive 3. 'alias' attribute of variable directive 4. + * 'dynamic-attributes' of a tag directive except that + * 'dynamic-attributes' can (and must) have the same value when it + * appears in multiple tag directives. + * + * Also, 'name-from' attribute of a variable directive cannot have the + * same value as that from another variable directive. + */ + private void checkUniqueName(String name, String type, Node n) + throws JasperException { + checkUniqueName(name, type, n, null); + } + + private void checkUniqueName(String name, String type, Node n, + TagAttributeInfo attr) throws JasperException { + + Map table = (VAR_NAME_FROM.equals(type)) ? nameFromTable : nameTable; + NameEntry nameEntry = table.get(name); + if (nameEntry != null) { + if (!TAG_DYNAMIC.equals(type) || + !TAG_DYNAMIC.equals(nameEntry.getType())) { + int line = nameEntry.getNode().getStart().getLineNumber(); + err.jspError(n, "jsp.error.tagfile.nameNotUnique", type, + nameEntry.getType(), Integer.toString(line)); + } + } else { + table.put(name, new NameEntry(type, n, attr)); + } + } + + /** + * Perform miscellaneous checks after the nodes are visited. + */ + void postCheck() throws JasperException { + // Check that var.name-from-attributes has valid values. + for (Entry entry : nameFromTable.entrySet()) { + String key = entry.getKey(); + NameEntry nameEntry = nameTable.get(key); + NameEntry nameFromEntry = entry.getValue(); + Node nameFromNode = nameFromEntry.getNode(); + if (nameEntry == null) { + err.jspError(nameFromNode, + "jsp.error.tagfile.nameFrom.noAttribute", key); + } else { + Node node = nameEntry.getNode(); + TagAttributeInfo tagAttr = nameEntry.getTagAttributeInfo(); + if (!"java.lang.String".equals(tagAttr.getTypeName()) + || !tagAttr.isRequired() + || tagAttr.canBeRequestTime()) { + err.jspError(nameFromNode, + "jsp.error.tagfile.nameFrom.badAttribute", + key, Integer.toString(node.getStart() + .getLineNumber())); + } + } + } + } + } + + /** + * Parses the tag file, and collects information on the directives included + * in it. The method is used to obtain the info on the tag file, when the + * handler that it represents is referenced. The tag file is not compiled + * here. + * + * @param pc + * the current ParserController used in this compilation + * @param name + * the tag name as specified in the TLD + * @param path + * the path for the tagfile + * @param jar + * the Jar resource containing the tag file + * @param tagLibInfo + * the TagLibraryInfo object associated with this TagInfo + * @return a TagInfo object assembled from the directives in the tag file. + * + * @throws JasperException If an error occurs during parsing + */ + @SuppressWarnings("null") // page can't be null + public static TagInfo parseTagFileDirectives(ParserController pc, + String name, String path, Jar jar, TagLibraryInfo tagLibInfo) + throws JasperException { + + + ErrorDispatcher err = pc.getCompiler().getErrorDispatcher(); + + Node.Nodes page = null; + try { + page = pc.parseTagFileDirectives(path, jar); + } catch (IOException e) { + err.jspError("jsp.error.file.not.found", path); + } + + TagFileDirectiveVisitor tagFileVisitor = new TagFileDirectiveVisitor(pc + .getCompiler(), tagLibInfo, name, path); + page.visit(tagFileVisitor); + tagFileVisitor.postCheck(); + + return tagFileVisitor.getTagInfo(pc.getJspCompilationContext().getOptions().getGeneratedTagFilePackageName()); + } + + /** + * Compiles and loads a tagfile. + */ + private Class loadTagFile(Compiler compiler, String tagFilePath, + TagInfo tagInfo, PageInfo parentPageInfo) throws JasperException { + + Jar tagJar = null; + Jar tagJarOriginal = null; + try { + if (tagFilePath.startsWith("/META-INF/")) { + try { + tagJar = compiler.getCompilationContext().getTldResourcePath( + tagInfo.getTagLibrary().getURI()).openJar(); + } catch (IOException ioe) { + throw new JasperException(ioe); + } + } + String wrapperUri; + if (tagJar == null) { + wrapperUri = tagFilePath; + } else { + wrapperUri = tagJar.getURL(tagFilePath); + } + + JspCompilationContext ctxt = compiler.getCompilationContext(); + JspRuntimeContext rctxt = ctxt.getRuntimeContext(); + + synchronized (rctxt) { + JspServletWrapper wrapper = null; + try { + wrapper = rctxt.getWrapper(wrapperUri); + if (wrapper == null) { + wrapper = new JspServletWrapper(ctxt.getServletContext(), ctxt + .getOptions(), tagFilePath, tagInfo, ctxt + .getRuntimeContext(), tagJar); + // Use same classloader and classpath for compiling tag files + wrapper.getJspEngineContext().setClassLoader( + ctxt.getClassLoader()); + wrapper.getJspEngineContext().setClassPath(ctxt.getClassPath()); + rctxt.addWrapper(wrapperUri, wrapper); + } else { + // Make sure that JspCompilationContext gets the latest TagInfo + // for the tag file. TagInfo instance was created the last + // time the tag file was scanned for directives, and the tag + // file may have been modified since then. + wrapper.getJspEngineContext().setTagInfo(tagInfo); + // This compilation needs to use the current tagJar. + // Compilation may be nested in which case the old tagJar + // will need to be restored + tagJarOriginal = wrapper.getJspEngineContext().getTagFileJar(); + wrapper.getJspEngineContext().setTagFileJar(tagJar); + } + + Class tagClazz; + int tripCount = wrapper.incTripCount(); + try { + if (tripCount > 0) { + // When tripCount is greater than zero, a circular + // dependency exists. The circularly dependent tag + // file is compiled in prototype mode, to avoid infinite + // recursion. + + JspServletWrapper tempWrapper = new JspServletWrapper(ctxt + .getServletContext(), ctxt.getOptions(), + tagFilePath, tagInfo, ctxt.getRuntimeContext(), + tagJar); + // Use same classloader and classpath for compiling tag files + tempWrapper.getJspEngineContext().setClassLoader( + ctxt.getClassLoader()); + tempWrapper.getJspEngineContext().setClassPath(ctxt.getClassPath()); + tagClazz = tempWrapper.loadTagFilePrototype(); + tempVector.add(tempWrapper.getJspEngineContext() + .getCompiler()); + } else { + tagClazz = wrapper.loadTagFile(); + } + } finally { + wrapper.decTripCount(); + } + + // Add the dependents for this tag file to its parent's + // Dependent list. The only reliable dependency information + // can only be obtained from the tag instance. + try { + Object tagIns = tagClazz.getConstructor().newInstance(); + if (tagIns instanceof JspSourceDependent) { + for (Entry entry : ((JspSourceDependent) + tagIns).getDependants().entrySet()) { + parentPageInfo.addDependant(entry.getKey(), + entry.getValue()); + } + } + } catch (RuntimeException | ReflectiveOperationException e) { + // ignore errors + } + + return tagClazz; + } finally { + if (wrapper != null && tagJarOriginal != null) { + wrapper.getJspEngineContext().setTagFileJar(tagJarOriginal); + } + } + } + } finally { + if (tagJar != null) { + tagJar.close(); + } + } + } + + /* + * Visitor which scans the page and looks for tag handlers that are tag + * files, compiling (if necessary) and loading them. + */ + private class TagFileLoaderVisitor extends Node.Visitor { + + private Compiler compiler; + + private PageInfo pageInfo; + + TagFileLoaderVisitor(Compiler compiler) { + + this.compiler = compiler; + this.pageInfo = compiler.getPageInfo(); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + TagFileInfo tagFileInfo = n.getTagFileInfo(); + if (tagFileInfo != null) { + String tagFilePath = tagFileInfo.getPath(); + if (tagFilePath.startsWith("/META-INF/")) { + // For tags in JARs, add the TLD and the tag as a dependency + TldResourcePath tldResourcePath = + compiler.getCompilationContext().getTldResourcePath( + tagFileInfo.getTagInfo().getTagLibrary().getURI()); + + try (Jar jar = tldResourcePath.openJar()) { + + if (jar != null) { + // Add TLD + pageInfo.addDependant(jar.getURL(tldResourcePath.getEntryName()), + Long.valueOf(jar.getLastModified(tldResourcePath.getEntryName()))); + // Add Tag + pageInfo.addDependant(jar.getURL(tagFilePath.substring(1)), + Long.valueOf(jar.getLastModified(tagFilePath.substring(1)))); + } else { + pageInfo.addDependant(tagFilePath, + compiler.getCompilationContext().getLastModified(tagFilePath)); + } + } catch (IOException ioe) { + throw new JasperException(ioe); + } + } else { + pageInfo.addDependant(tagFilePath, + compiler.getCompilationContext().getLastModified(tagFilePath)); + } + Class c = loadTagFile(compiler, tagFilePath, n.getTagInfo(), + pageInfo); + n.setTagHandlerClass(c); + } + visitBody(n); + } + } + + /** + * Implements a phase of the translation that compiles (if necessary) the + * tag files used in a JSP files. The directives in the tag files are + * assumed to have been processed and encapsulated as TagFileInfo in the + * CustomTag nodes. + * + * @param compiler Compiler to use to compile tag files + * @param page The page from to scan for tag files to compile + * + * @throws JasperException If an error occurs during the scan or compilation + */ + public void loadTagFiles(Compiler compiler, Node.Nodes page) + throws JasperException { + + tempVector = new ArrayList<>(); + page.visit(new TagFileLoaderVisitor(compiler)); + } + + /** + * Removed the java and class files for the tag prototype generated from the + * current compilation. + * + * @param classFileName + * If non-null, remove only the class file with with this name. + */ + public void removeProtoTypeFiles(String classFileName) { + for (Compiler c : tempVector) { + if (classFileName == null) { + c.removeGeneratedClassFiles(); + } else if (classFileName.equals(c.getCompilationContext().getClassFileName())) { + c.removeGeneratedClassFiles(); + tempVector.remove(c); + return; + } + } + } +} diff --git a/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java b/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java new file mode 100644 index 0000000..fa247aa --- /dev/null +++ b/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java @@ -0,0 +1,409 @@ +/* + * 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.jasper.compiler; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.jsp.tagext.FunctionInfo; +import jakarta.servlet.jsp.tagext.PageData; +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagExtraInfo; +import jakarta.servlet.jsp.tagext.TagFileInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; +import jakarta.servlet.jsp.tagext.TagLibraryValidator; +import jakarta.servlet.jsp.tagext.TagVariableInfo; +import jakarta.servlet.jsp.tagext.ValidationMessage; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.descriptor.tld.TagFileXml; +import org.apache.tomcat.util.descriptor.tld.TagXml; +import org.apache.tomcat.util.descriptor.tld.TaglibXml; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; +import org.apache.tomcat.util.descriptor.tld.ValidatorXml; + +/** + * Implementation of the TagLibraryInfo class from the JSP spec. + * + * @author Anil K. Vijendran + * @author Mandar Raje + * @author Pierre Delisle + * @author Kin-man Chung + * @author Jan Luehe + */ +class TagLibraryInfoImpl extends TagLibraryInfo implements TagConstants { + + private final JspCompilationContext ctxt; + + private final PageInfo pi; + + private final ErrorDispatcher err; + + private final ParserController parserController; + + private static void print(String name, String value, PrintWriter w) { + if (value != null) { + w.print(name + " = {\n\t"); + w.print(value); + w.print("\n}\n"); + } + } + + @Override + public String toString() { + StringWriter sw = new StringWriter(); + PrintWriter out = new PrintWriter(sw); + print("tlibversion", tlibversion, out); + print("jspversion", jspversion, out); + print("shortname", shortname, out); + print("urn", urn, out); + print("info", info, out); + print("uri", uri, out); + print("tagLibraryValidator", "" + tagLibraryValidator, out); + + for (TagInfo tag : tags) { + out.println(tag.toString()); + } + + for (TagFileInfo tagFile : tagFiles) { + out.println(tagFile.toString()); + } + + for (FunctionInfo function : functions) { + out.println(function.toString()); + } + + return sw.toString(); + } + + + TagLibraryInfoImpl(JspCompilationContext ctxt, ParserController pc, + PageInfo pi, String prefix, String uriIn, + TldResourcePath tldResourcePath, ErrorDispatcher err) + throws JasperException { + super(prefix, uriIn); + + this.ctxt = ctxt; + this.parserController = pc; + this.pi = pi; + this.err = err; + + if (tldResourcePath == null) { + // The URI points to the TLD itself or to a JAR file in which the TLD is stored + tldResourcePath = generateTldResourcePath(uri, ctxt); + } + + try (Jar jar = tldResourcePath.openJar()) { + + // Add the dependencies on the TLD to the referencing page + PageInfo pageInfo = ctxt.createCompiler().getPageInfo(); + if (pageInfo != null) { + // If the TLD is in a JAR, that JAR may not be part of the web + // application + String path = tldResourcePath.getWebappPath(); + if (path != null) { + // Add TLD (jar==null) / JAR (jar!=null) file to dependency list + // 2nd parameter is null since the path is always relative + // to the root of the web application even if we are + // processing a reference from a tag packaged in a JAR. + pageInfo.addDependant(path, ctxt.getLastModified(path, null)); + } + if (jar != null) { + if (path == null) { + // JAR not in the web application so add it directly + URL jarUrl = jar.getJarFileURL(); + long lastMod = -1; + URLConnection urlConn = null; + try { + urlConn = jarUrl.openConnection(); + lastMod = urlConn.getLastModified(); + } catch (IOException ioe) { + throw new JasperException(ioe); + } finally { + if (urlConn != null) { + try { + urlConn.getInputStream().close(); + } catch (IOException e) { + // Ignore + } + } + } + pageInfo.addDependant(jarUrl.toExternalForm(), + Long.valueOf(lastMod)); + } + // Add TLD within the JAR to the dependency list + String entryName = tldResourcePath.getEntryName(); + try { + pageInfo.addDependant(jar.getURL(entryName), + Long.valueOf(jar.getLastModified(entryName))); + } catch (IOException ioe) { + throw new JasperException(ioe); + } + } + } + + // Get the representation of the TLD + if (tldResourcePath.getUrl() == null) { + err.jspError("jsp.error.tld.missing", prefix, uri); + } + TaglibXml taglibXml = + ctxt.getOptions().getTldCache().getTaglibXml(tldResourcePath); + if (taglibXml == null) { + err.jspError("jsp.error.tld.missing", prefix, uri); + } + + // Populate the TagLibraryInfo attributes + // Never null. jspError always throws an Exception + // Slightly convoluted so the @SuppressWarnings has minimal scope + @SuppressWarnings("null") + String v = taglibXml.getJspVersion(); + this.jspversion = v; + this.tlibversion = taglibXml.getTlibVersion(); + this.shortname = taglibXml.getShortName(); + this.urn = taglibXml.getUri(); + this.info = taglibXml.getInfo(); + + this.tagLibraryValidator = createValidator(taglibXml.getValidator()); + + List tagInfos = new ArrayList<>(); + for (TagXml tagXml : taglibXml.getTags()) { + tagInfos.add(createTagInfo(tagXml)); + } + + List tagFileInfos = new ArrayList<>(); + for (TagFileXml tagFileXml : taglibXml.getTagFiles()) { + tagFileInfos.add(createTagFileInfo(tagFileXml, jar)); + } + + Set names = new HashSet<>(); + List functionInfos = taglibXml.getFunctions(); + // TODO Move this validation to the parsing stage + for (FunctionInfo functionInfo : functionInfos) { + String name = functionInfo.getName(); + if (!names.add(name)) { + err.jspError("jsp.error.tld.fn.duplicate.name", name, uri); + } + } + + if (tlibversion == null) { + err.jspError("jsp.error.tld.mandatory.element.missing", "tlib-version", uri); + } + if (jspversion == null) { + err.jspError("jsp.error.tld.mandatory.element.missing", "jsp-version", uri); + } + + this.tags = tagInfos.toArray(new TagInfo[0]); + this.tagFiles = tagFileInfos.toArray(new TagFileInfo[0]); + this.functions = functionInfos.toArray(new FunctionInfo[0]); + } catch (IOException ioe) { + throw new JasperException(ioe); + } + } + + @Override + public TagLibraryInfo[] getTagLibraryInfos() { + Collection coll = pi.getTaglibs(); + return coll.toArray(new TagLibraryInfo[0]); + } + + /* + * @param uri The uri of the TLD + * @param ctxt The compilation context + * + * @return the location of the TLD identified by the uri + */ + private TldResourcePath generateTldResourcePath(String uri, + JspCompilationContext ctxt) throws JasperException { + + // TODO: this matches the current implementation but the URL logic looks fishy + // map URI to location per JSP 7.3.6.2 + if (uri.indexOf(':') != -1) { + // abs_uri, this was not found in the taglibMap so raise an error + err.jspError("jsp.error.taglibDirective.absUriCannotBeResolved", uri); + } else if (uri.charAt(0) != '/') { + // noroot_rel_uri, resolve against the current JSP page + uri = ctxt.resolveRelativeUri(uri); + try { + // Can't use RequestUtils.normalize since that package is not + // available to Jasper. + uri = new URI(uri).normalize().toString(); + if (uri.startsWith("../")) { + // Trying to go outside context root + err.jspError("jsp.error.taglibDirective.uriInvalid", uri); + } + } catch (URISyntaxException e) { + err.jspError("jsp.error.taglibDirective.uriInvalid", uri); + } + } + + URL url = null; + try { + url = ctxt.getResource(uri); + /* + * When the TLD cache is built for a TLD contained within a JAR within a WAR, the jar form of the URL is + * used for any nested JAR. + */ + if (url.getProtocol().equals("war") && uri.endsWith(".jar")) { + url = UriUtil.warToJar(url); + } + } catch (Exception ex) { + err.jspError("jsp.error.tld.unable_to_get_jar", uri, ex.toString()); + } + if (uri.endsWith(".jar")) { + if (url == null) { + err.jspError("jsp.error.tld.missing_jar", uri); + } + return new TldResourcePath(url, uri, "META-INF/taglib.tld"); + } else if (uri.startsWith("/WEB-INF/lib/") || uri.startsWith("/WEB-INF/classes/") || + (uri.startsWith("/WEB-INF/tags/") && uri.endsWith(".tld")&& !uri.endsWith("implicit.tld"))) { + err.jspError("jsp.error.tld.invalid_tld_file", uri); + } + return new TldResourcePath(url, uri); + } + + private TagInfo createTagInfo(TagXml tagXml) throws JasperException { + + String teiClassName = tagXml.getTeiClass(); + TagExtraInfo tei = null; + if (teiClassName != null && !teiClassName.isEmpty()) { + try { + Class teiClass = ctxt.getClassLoader().loadClass(teiClassName); + tei = (TagExtraInfo) teiClass.getConstructor().newInstance(); + } catch (Exception e) { + err.jspError(e, "jsp.error.teiclass.instantiation", teiClassName); + } + } + + List attributeInfos = tagXml.getAttributes(); + List variableInfos = tagXml.getVariables(); + + return new TagInfo(tagXml.getName(), + tagXml.getTagClass(), + tagXml.getBodyContent(), + tagXml.getInfo(), + this, + tei, + attributeInfos.toArray(new TagAttributeInfo[0]), + tagXml.getDisplayName(), + tagXml.getSmallIcon(), + tagXml.getLargeIcon(), + variableInfos.toArray(new TagVariableInfo[0]), + tagXml.hasDynamicAttributes()); + } + + @SuppressWarnings("null") // Impossible for path to be null at warning + private TagFileInfo createTagFileInfo(TagFileXml tagFileXml, Jar jar) throws JasperException { + + String name = tagFileXml.getName(); + String path = tagFileXml.getPath(); + + if (path == null) { + // path is required + err.jspError("jsp.error.tagfile.missingPath"); + } else if (!path.startsWith("/META-INF/tags") && !path.startsWith("/WEB-INF/tags")) { + err.jspError("jsp.error.tagfile.illegalPath", path); + } + + if (jar == null && path.startsWith("/META-INF/tags")) { + // This is a tag file that was packaged in a JAR that has been + // unpacked into /WEB-INF/classes (probably by an IDE). Adjust the + // path accordingly. + path = "/WEB-INF/classes" + path; + } + + TagInfo tagInfo = + TagFileProcessor.parseTagFileDirectives(parserController, name, path, jar, this); + return new TagFileInfo(name, path, tagInfo); + } + + private TagLibraryValidator createValidator(ValidatorXml validatorXml) throws JasperException { + + if (validatorXml == null) { + return null; + } + + String validatorClass = validatorXml.getValidatorClass(); + if (validatorClass == null || validatorClass.isEmpty()) { + return null; + } + + Map initParams = new HashMap<>(validatorXml.getInitParams()); + + try { + Class tlvClass = ctxt.getClassLoader().loadClass(validatorClass); + TagLibraryValidator tlv = (TagLibraryValidator) tlvClass.getConstructor().newInstance(); + tlv.setInitParameters(initParams); + return tlv; + } catch (Exception e) { + err.jspError(e, "jsp.error.tlvclass.instantiation", validatorClass); + return null; + } + } + + // ********************************************************************* + // Until jakarta.servlet.jsp.tagext.TagLibraryInfo is fixed + + /** + * The instance (if any) for the TagLibraryValidator class. + * + * @return The TagLibraryValidator instance, if any. + */ + public TagLibraryValidator getTagLibraryValidator() { + return tagLibraryValidator; + } + + /** + * Translation-time validation of the XML document associated with the JSP + * page. This is a convenience method on the associated TagLibraryValidator + * class. + * + * @param thePage + * The JSP page object + * @return A string indicating whether the page is valid or not. + */ + public ValidationMessage[] validate(PageData thePage) { + TagLibraryValidator tlv = getTagLibraryValidator(); + if (tlv == null) { + return null; + } + + String uri = getURI(); + if (uri.startsWith("/")) { + uri = URN_JSPTLD + uri; + } + + return tlv.validate(getPrefixString(), uri, thePage); + } + + private TagLibraryValidator tagLibraryValidator; +} diff --git a/java/org/apache/jasper/compiler/TagPluginManager.java b/java/org/apache/jasper/compiler/TagPluginManager.java new file mode 100644 index 0000000..8925ca8 --- /dev/null +++ b/java/org/apache/jasper/compiler/TagPluginManager.java @@ -0,0 +1,293 @@ +/* + * 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.jasper.compiler; + +import java.io.IOException; +import java.net.URL; +import java.security.AccessController; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.ServletContext; + +import org.apache.jasper.Constants; +import org.apache.jasper.JasperException; +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; +import org.apache.tomcat.util.descriptor.tagplugin.TagPluginParser; +import org.apache.tomcat.util.security.PrivilegedGetTccl; +import org.apache.tomcat.util.security.PrivilegedSetTccl; +import org.xml.sax.SAXException; + +/** + * Manages tag plugin optimizations. + * + * @author Kin-man Chung + */ +public class TagPluginManager { + + private static final String META_INF_JASPER_TAG_PLUGINS_XML = + "META-INF/org.apache.jasper/tagPlugins.xml"; + private static final String TAG_PLUGINS_XML = "/WEB-INF/tagPlugins.xml"; + private final ServletContext ctxt; + private HashMap tagPlugins; + private boolean initialized = false; + + public TagPluginManager(ServletContext ctxt) { + this.ctxt = ctxt; + } + + public void apply(Node.Nodes page, ErrorDispatcher err, PageInfo pageInfo) + throws JasperException { + + init(err); + if (!tagPlugins.isEmpty()) { + page.visit(new NodeVisitor(this, pageInfo)); + } + } + + private void init(ErrorDispatcher err) throws JasperException { + if (initialized) { + return; + } + + String blockExternalString = ctxt.getInitParameter( + Constants.XML_BLOCK_EXTERNAL_INIT_PARAM); + boolean blockExternal; + if (blockExternalString == null) { + blockExternal = true; + } else { + blockExternal = Boolean.parseBoolean(blockExternalString); + } + + TagPluginParser parser; + ClassLoader original; + Thread currentThread = Thread.currentThread(); + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedGetTccl pa = new PrivilegedGetTccl(currentThread); + original = AccessController.doPrivileged(pa); + } else { + original = currentThread.getContextClassLoader(); + } + try { + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, TagPluginManager.class.getClassLoader()); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(TagPluginManager.class.getClassLoader()); + } + + parser = new TagPluginParser(ctxt, blockExternal); + + Enumeration urls = + ctxt.getClassLoader().getResources(META_INF_JASPER_TAG_PLUGINS_XML); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + parser.parse(url); + } + + URL url = ctxt.getResource(TAG_PLUGINS_XML); + if (url != null) { + parser.parse(url); + } + } catch (IOException | SAXException e) { + throw new JasperException(e); + } finally { + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, original); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(original); + } + } + + Map plugins = parser.getPlugins(); + tagPlugins = new HashMap<>(plugins.size()); + for (Map.Entry entry : plugins.entrySet()) { + try { + String tagClass = entry.getKey(); + String pluginName = entry.getValue(); + Class pluginClass = ctxt.getClassLoader().loadClass(pluginName); + TagPlugin plugin = (TagPlugin) pluginClass.getConstructor().newInstance(); + tagPlugins.put(tagClass, plugin); + } catch (Exception e) { + err.jspError(e); + } + } + initialized = true; + } + + /** + * Invoke tag plugin for the given custom tag, if a plugin exists for + * the custom tag's tag handler. + *

    + * The given custom tag node will be manipulated by the plugin. + */ + private void invokePlugin(Node.CustomTag n, PageInfo pageInfo) { + TagPlugin tagPlugin = tagPlugins.get(n.getTagHandlerClass().getName()); + if (tagPlugin == null) { + return; + } + + TagPluginContext tagPluginContext = new TagPluginContextImpl(n, pageInfo); + n.setTagPluginContext(tagPluginContext); + tagPlugin.doTag(tagPluginContext); + } + + private static class NodeVisitor extends Node.Visitor { + private final TagPluginManager manager; + private final PageInfo pageInfo; + + NodeVisitor(TagPluginManager manager, PageInfo pageInfo) { + this.manager = manager; + this.pageInfo = pageInfo; + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + manager.invokePlugin(n, pageInfo); + visitBody(n); + } + } + + private static class TagPluginContextImpl implements TagPluginContext { + private final Node.CustomTag node; + private final PageInfo pageInfo; + private final HashMap pluginAttributes; + private Node.Nodes curNodes; + + TagPluginContextImpl(Node.CustomTag n, PageInfo pageInfo) { + this.node = n; + this.pageInfo = pageInfo; + curNodes = new Node.Nodes(); + n.setAtETag(curNodes); + curNodes = new Node.Nodes(); + n.setAtSTag(curNodes); + n.setUseTagPlugin(true); + pluginAttributes = new HashMap<>(); + } + + @Override + public TagPluginContext getParentContext() { + Node parent = node.getParent(); + if (!(parent instanceof Node.CustomTag)) { + return null; + } + return ((Node.CustomTag) parent).getTagPluginContext(); + } + + @Override + public void setPluginAttribute(String key, Object value) { + pluginAttributes.put(key, value); + } + + @Override + public Object getPluginAttribute(String key) { + return pluginAttributes.get(key); + } + + @Override + public boolean isScriptless() { + return node.getChildInfo().isScriptless(); + } + + @Override + public boolean isConstantAttribute(String attribute) { + Node.JspAttribute attr = getNodeAttribute(attribute); + if (attr == null) { + return false; + } + return attr.isLiteral(); + } + + @Override + public String getConstantAttribute(String attribute) { + Node.JspAttribute attr = getNodeAttribute(attribute); + if (attr == null) { + return null; + } + return attr.getValue(); + } + + @Override + public boolean isAttributeSpecified(String attribute) { + return getNodeAttribute(attribute) != null; + } + + @Override + public String getTemporaryVariableName() { + return node.getRoot().nextTemporaryVariableName(); + } + + @Override + public void generateImport(String imp) { + pageInfo.addImport(imp); + } + + @Override + public void generateDeclaration(String id, String text) { + if (pageInfo.isPluginDeclared(id)) { + return; + } + curNodes.add(new Node.Declaration(text, node.getStart(), null)); + } + + @Override + public void generateJavaSource(String sourceCode) { + curNodes.add(new Node.Scriptlet(sourceCode, node.getStart(), + null)); + } + + @Override + public void generateAttribute(String attributeName) { + curNodes.add(new Node.AttributeGenerator(node.getStart(), + attributeName, + node)); + } + + @Override + public void dontUseTagPlugin() { + node.setUseTagPlugin(false); + } + + @Override + public void generateBody() { + // Since we'll generate the body anyway, this is really a nop, + // except for the fact that it lets us put the Java sources the + // plugins produce in the correct order (w.r.t the body). + curNodes = node.getAtETag(); + } + + @Override + public boolean isTagFile() { + return pageInfo.isTagFile(); + } + + private Node.JspAttribute getNodeAttribute(String attribute) { + Node.JspAttribute[] attrs = node.getJspAttributes(); + for (int i = 0; attrs != null && i < attrs.length; i++) { + if (attrs[i].getName().equals(attribute)) { + return attrs[i]; + } + } + return null; + } + } + +} + diff --git a/java/org/apache/jasper/compiler/TextOptimizer.java b/java/org/apache/jasper/compiler/TextOptimizer.java new file mode 100644 index 0000000..d5e9ffd --- /dev/null +++ b/java/org/apache/jasper/compiler/TextOptimizer.java @@ -0,0 +1,131 @@ +/* + * 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.jasper.compiler; + +import org.apache.jasper.JasperException; +import org.apache.jasper.Options; +import org.apache.jasper.TrimSpacesOption; + +/** + */ +public class TextOptimizer { + + /** + * A visitor to concatenate contiguous template texts. + */ + private static class TextCatVisitor extends Node.Visitor { + + private static final String EMPTY_TEXT = ""; + private static final String SINGLE_SPACE = " "; + + private final Options options; + private final PageInfo pageInfo; + private int textNodeCount = 0; + private Node.TemplateText firstTextNode = null; + private StringBuilder textBuffer; + + TextCatVisitor(Compiler compiler) { + options = compiler.getCompilationContext().getOptions(); + pageInfo = compiler.getPageInfo(); + } + + @Override + public void doVisit(Node n) throws JasperException { + collectText(); + } + + /* + * The following directives are ignored in text concatenation + */ + + @Override + public void visit(Node.PageDirective n) throws JasperException { + } + + @Override + public void visit(Node.TagDirective n) throws JasperException { + } + + @Override + public void visit(Node.TaglibDirective n) throws JasperException { + } + + @Override + public void visit(Node.AttributeDirective n) throws JasperException { + } + + @Override + public void visit(Node.VariableDirective n) throws JasperException { + } + + /* + * Don't concatenate text across body boundaries + */ + @Override + public void visitBody(Node n) throws JasperException { + super.visitBody(n); + collectText(); + } + + @Override + public void visit(Node.TemplateText n) throws JasperException { + if (n.isAllSpace()) { + if ((options.getTrimSpaces() == TrimSpacesOption.TRUE || + pageInfo.isTrimDirectiveWhitespaces())) { + n.setText(EMPTY_TEXT); + return; + } else if (options.getTrimSpaces() == TrimSpacesOption.SINGLE) { + n.setText(SINGLE_SPACE); + return; + } + } + + if (textNodeCount++ == 0) { + firstTextNode = n; + textBuffer = new StringBuilder(n.getText()); + } else { + // Append text to text buffer + textBuffer.append(n.getText()); + n.setText(EMPTY_TEXT); + } + } + + /** + * This method breaks concatenation mode. As a side effect it copies + * the concatenated string to the first text node + */ + private void collectText() { + + if (textNodeCount > 1) { + // Copy the text in buffer into the first template text node. + firstTextNode.setText(textBuffer.toString()); + } + textNodeCount = 0; + } + + } + + public static void concatenate(Compiler compiler, Node.Nodes page) + throws JasperException { + + TextCatVisitor v = new TextCatVisitor(compiler); + page.visit(v); + + // Cleanup, in case the page ends with a template text + v.collectText(); + } +} diff --git a/java/org/apache/jasper/compiler/TldCache.java b/java/org/apache/jasper/compiler/TldCache.java new file mode 100644 index 0000000..ceb6d00 --- /dev/null +++ b/java/org/apache/jasper/compiler/TldCache.java @@ -0,0 +1,188 @@ +/* + * 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.jasper.compiler; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import jakarta.servlet.ServletContext; + +import org.apache.jasper.Constants; +import org.apache.jasper.JasperException; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.descriptor.tld.TaglibXml; +import org.apache.tomcat.util.descriptor.tld.TldParser; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; +import org.xml.sax.SAXException; + +/** + * This class caches parsed instances of TLD files to remove the need for the + * same TLD to be parsed for each JSP that references it. It does not protect + * against multiple threads processing the same, new TLD but it does ensure that + * each all threads will use the same TLD object after parsing. + */ +public class TldCache { + + public static final String SERVLET_CONTEXT_ATTRIBUTE_NAME = + TldCache.class.getName(); + + private final ServletContext servletContext; + private final Map uriTldResourcePathMap = new HashMap<>(); + private final Map tldResourcePathTaglibXmlMap = + new HashMap<>(); + private final TldParser tldParser; + + + public static TldCache getInstance(ServletContext servletContext) { + if (servletContext == null) { + throw new IllegalArgumentException(Localizer.getMessage( + "org.apache.jasper.compiler.TldCache.servletContextNull")); + } + return (TldCache) servletContext.getAttribute(SERVLET_CONTEXT_ATTRIBUTE_NAME); + } + + + public TldCache(ServletContext servletContext, + Map uriTldResourcePathMap, + Map tldResourcePathTaglibXmlMap) { + this.servletContext = servletContext; + this.uriTldResourcePathMap.putAll(uriTldResourcePathMap); + for (Entry entry : tldResourcePathTaglibXmlMap.entrySet()) { + TldResourcePath tldResourcePath = entry.getKey(); + long lastModified[] = getLastModified(tldResourcePath); + TaglibXmlCacheEntry cacheEntry = new TaglibXmlCacheEntry( + entry.getValue(), lastModified[0], lastModified[1]); + this.tldResourcePathTaglibXmlMap.put(tldResourcePath, cacheEntry); + } + boolean validate = Boolean.parseBoolean( + servletContext.getInitParameter(Constants.XML_VALIDATION_TLD_INIT_PARAM)); + String blockExternalString = servletContext.getInitParameter( + Constants.XML_BLOCK_EXTERNAL_INIT_PARAM); + boolean blockExternal; + if (blockExternalString == null) { + blockExternal = true; + } else { + blockExternal = Boolean.parseBoolean(blockExternalString); + } + tldParser = new TldParser(true, validate, blockExternal); + } + + + public TldResourcePath getTldResourcePath(String uri) { + return uriTldResourcePathMap.get(uri); + } + + + public TaglibXml getTaglibXml(TldResourcePath tldResourcePath) throws JasperException { + TaglibXmlCacheEntry cacheEntry = tldResourcePathTaglibXmlMap.get(tldResourcePath); + if (cacheEntry == null) { + return null; + } + long lastModified[] = getLastModified(tldResourcePath); + if (lastModified[0] != cacheEntry.getWebAppPathLastModified() || + lastModified[1] != cacheEntry.getEntryLastModified()) { + synchronized (cacheEntry) { + if (lastModified[0] != cacheEntry.getWebAppPathLastModified() || + lastModified[1] != cacheEntry.getEntryLastModified()) { + // Re-parse TLD + TaglibXml updatedTaglibXml; + try { + updatedTaglibXml = tldParser.parse(tldResourcePath); + } catch (IOException | SAXException e) { + throw new JasperException(e); + } + cacheEntry.setTaglibXml(updatedTaglibXml); + cacheEntry.setWebAppPathLastModified(lastModified[0]); + cacheEntry.setEntryLastModified(lastModified[1]); + } + } + } + return cacheEntry.getTaglibXml(); + } + + + private long[] getLastModified(TldResourcePath tldResourcePath) { + long[] result = new long[2]; + result[0] = -1; + result[1] = -1; + try { + String webappPath = tldResourcePath.getWebappPath(); + if (webappPath != null) { + // webappPath will be null for JARs containing TLDs that are on + // the class path but not part of the web application + URL url = servletContext.getResource(tldResourcePath.getWebappPath()); + URLConnection conn = url.openConnection(); + result[0] = conn.getLastModified(); + if ("file".equals(url.getProtocol())) { + // Reading the last modified time opens an input stream so we + // need to make sure it is closed again otherwise the TLD file + // will be locked until GC runs. + conn.getInputStream().close(); + } + } + try (Jar jar = tldResourcePath.openJar()) { + if (jar != null) { + result[1] = jar.getLastModified(tldResourcePath.getEntryName()); + } + } + } catch (IOException e) { + // Ignore (shouldn't happen) + } + return result; + } + + private static class TaglibXmlCacheEntry { + private volatile TaglibXml taglibXml; + private volatile long webAppPathLastModified; + private volatile long entryLastModified; + + TaglibXmlCacheEntry(TaglibXml taglibXml, long webAppPathLastModified, + long entryLastModified) { + this.taglibXml = taglibXml; + this.webAppPathLastModified = webAppPathLastModified; + this.entryLastModified = entryLastModified; + } + + public TaglibXml getTaglibXml() { + return taglibXml; + } + + public void setTaglibXml(TaglibXml taglibXml) { + this.taglibXml = taglibXml; + } + + public long getWebAppPathLastModified() { + return webAppPathLastModified; + } + + public void setWebAppPathLastModified(long webAppPathLastModified) { + this.webAppPathLastModified = webAppPathLastModified; + } + + public long getEntryLastModified() { + return entryLastModified; + } + + public void setEntryLastModified(long entryLastModified) { + this.entryLastModified = entryLastModified; + } + } +} diff --git a/java/org/apache/jasper/compiler/Validator.java b/java/org/apache/jasper/compiler/Validator.java new file mode 100644 index 0000000..fea0b38 --- /dev/null +++ b/java/org/apache/jasper/compiler/Validator.java @@ -0,0 +1,1947 @@ +/* + * 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.jasper.compiler; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.el.ELException; +import jakarta.el.ExpressionFactory; +import jakarta.el.FunctionMapper; +import jakarta.servlet.jsp.JspFactory; +import jakarta.servlet.jsp.tagext.FunctionInfo; +import jakarta.servlet.jsp.tagext.PageData; +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagData; +import jakarta.servlet.jsp.tagext.TagExtraInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagLibraryInfo; +import jakarta.servlet.jsp.tagext.ValidationMessage; + +import org.apache.jasper.JasperException; +import org.apache.jasper.compiler.ELNode.Text; +import org.apache.jasper.el.ELContextImpl; +import org.apache.tomcat.util.security.Escape; +import org.xml.sax.Attributes; + +/** + * Performs validation on the page elements. Attributes are checked for + * mandatory presence, entry value validity, and consistency. As a side effect, + * some page global value (such as those from page directives) are stored, for + * later use. + * + * @author Kin-man Chung + * @author Jan Luehe + * @author Shawn Bayern + * @author Mark Roth + */ +class Validator { + + /** + * A visitor to validate and extract page directive info + */ + private static class DirectiveVisitor extends Node.Visitor { + + private final PageInfo pageInfo; + + private final ErrorDispatcher err; + + private static final JspUtil.ValidAttribute[] pageDirectiveAttrs = { + new JspUtil.ValidAttribute("language"), + new JspUtil.ValidAttribute("extends"), + new JspUtil.ValidAttribute("import"), + new JspUtil.ValidAttribute("session"), + new JspUtil.ValidAttribute("buffer"), + new JspUtil.ValidAttribute("autoFlush"), + new JspUtil.ValidAttribute("isThreadSafe"), + new JspUtil.ValidAttribute("info"), + new JspUtil.ValidAttribute("errorPage"), + new JspUtil.ValidAttribute("isErrorPage"), + new JspUtil.ValidAttribute("contentType"), + new JspUtil.ValidAttribute("pageEncoding"), + new JspUtil.ValidAttribute("isELIgnored"), + new JspUtil.ValidAttribute("errorOnELNotFound"), + new JspUtil.ValidAttribute("deferredSyntaxAllowedAsLiteral"), + new JspUtil.ValidAttribute("trimDirectiveWhitespaces") + }; + + private boolean pageEncodingSeen = false; + + /* + * Constructor + */ + DirectiveVisitor(Compiler compiler) { + this.pageInfo = compiler.getPageInfo(); + this.err = compiler.getErrorDispatcher(); + } + + @Override + public void visit(Node.IncludeDirective n) throws JasperException { + // Since pageDirectiveSeen flag only applies to the Current page + // save it here and restore it after the file is included. + boolean pageEncodingSeenSave = pageEncodingSeen; + pageEncodingSeen = false; + visitBody(n); + pageEncodingSeen = pageEncodingSeenSave; + } + + @Override + public void visit(Node.PageDirective n) throws JasperException { + + JspUtil.checkAttributes("Page directive", n, pageDirectiveAttrs, + err); + + // JSP.2.10.1 + Attributes attrs = n.getAttributes(); + for (int i = 0; attrs != null && i < attrs.getLength(); i++) { + String attr = attrs.getQName(i); + String value = attrs.getValue(i); + + if ("language".equals(attr)) { + if (pageInfo.getLanguage(false) == null) { + pageInfo.setLanguage(value, n, err, true); + } else if (!pageInfo.getLanguage(false).equals(value)) { + err.jspError(n, "jsp.error.page.conflict.language", + pageInfo.getLanguage(false), value); + } + } else if ("extends".equals(attr)) { + if (pageInfo.getExtends(false) == null) { + pageInfo.setExtends(value); + } else if (!pageInfo.getExtends(false).equals(value)) { + err.jspError(n, "jsp.error.page.conflict.extends", + pageInfo.getExtends(false), value); + } + } else if ("contentType".equals(attr)) { + if (pageInfo.getContentType() == null) { + pageInfo.setContentType(value); + } else if (!pageInfo.getContentType().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.contenttype", + pageInfo.getContentType(), value); + } + } else if ("session".equals(attr)) { + if (pageInfo.getSession() == null) { + pageInfo.setSession(value, n, err); + } else if (!pageInfo.getSession().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.session", + pageInfo.getSession(), value); + } + } else if ("buffer".equals(attr)) { + if (pageInfo.getBufferValue() == null) { + pageInfo.setBufferValue(value, n, err); + } else if (!pageInfo.getBufferValue().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.buffer", + pageInfo.getBufferValue(), value); + } + } else if ("autoFlush".equals(attr)) { + if (pageInfo.getAutoFlush() == null) { + pageInfo.setAutoFlush(value, n, err); + } else if (!pageInfo.getAutoFlush().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.autoflush", + pageInfo.getAutoFlush(), value); + } + } else if ("isThreadSafe".equals(attr)) { + if (pageInfo.getIsThreadSafe() == null) { + pageInfo.setIsThreadSafe(value, n, err); + } else if (!pageInfo.getIsThreadSafe().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.isthreadsafe", + pageInfo.getIsThreadSafe(), value); + } + } else if ("isELIgnored".equals(attr)) { + if (pageInfo.getIsELIgnored() == null) { + pageInfo.setIsELIgnored(value, n, err, true); + } else if (!pageInfo.getIsELIgnored().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.iselignored", + pageInfo.getIsELIgnored(), value); + } + } else if ("errorOnELNotFound".equals(attr)) { + if (pageInfo.getErrorOnELNotFound() == null) { + pageInfo.setErrorOnELNotFound(value, n, err, true); + } else if (!pageInfo.getErrorOnELNotFound().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.errorOnELNotFound", + pageInfo.getErrorOnELNotFound(), value); + } + } else if ("isErrorPage".equals(attr)) { + if (pageInfo.getIsErrorPage() == null) { + pageInfo.setIsErrorPage(value, n, err); + } else if (!pageInfo.getIsErrorPage().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.iserrorpage", + pageInfo.getIsErrorPage(), value); + } + } else if ("errorPage".equals(attr)) { + if (pageInfo.getErrorPage() == null) { + pageInfo.setErrorPage(value); + } else if (!pageInfo.getErrorPage().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.errorpage", + pageInfo.getErrorPage(), value); + } + } else if ("info".equals(attr)) { + if (pageInfo.getInfo() == null) { + pageInfo.setInfo(value); + } else if (!pageInfo.getInfo().equals(value)) { + err.jspError(n, "jsp.error.page.conflict.info", + pageInfo.getInfo(), value); + } + } else if ("pageEncoding".equals(attr)) { + if (pageEncodingSeen) { + err.jspError(n, "jsp.error.page.multi.pageencoding"); + } + // 'pageEncoding' can occur at most once per file + pageEncodingSeen = true; + String actual = comparePageEncodings(value, n); + n.getRoot().setPageEncoding(actual); + } else if ("deferredSyntaxAllowedAsLiteral".equals(attr)) { + if (pageInfo.getDeferredSyntaxAllowedAsLiteral() == null) { + pageInfo.setDeferredSyntaxAllowedAsLiteral(value, n, + err, true); + } else if (!pageInfo.getDeferredSyntaxAllowedAsLiteral() + .equals(value)) { + err + .jspError( + n, + "jsp.error.page.conflict.deferredsyntaxallowedasliteral", + pageInfo + .getDeferredSyntaxAllowedAsLiteral(), + value); + } + } else if ("trimDirectiveWhitespaces".equals(attr)) { + if (pageInfo.getTrimDirectiveWhitespaces() == null) { + pageInfo.setTrimDirectiveWhitespaces(value, n, err, + true); + } else if (!pageInfo.getTrimDirectiveWhitespaces().equals( + value)) { + err + .jspError( + n, + "jsp.error.page.conflict.trimdirectivewhitespaces", + pageInfo.getTrimDirectiveWhitespaces(), + value); + } + } + } + + // Check for bad combinations + if (pageInfo.getBuffer() == 0 && !pageInfo.isAutoFlush()) { + err.jspError(n, "jsp.error.page.badCombo"); + } + + // Attributes for imports for this node have been processed by + // the parsers, just add them to pageInfo. + pageInfo.addImports(n.getImports()); + } + + @Override + public void visit(Node.TagDirective n) throws JasperException { + // Note: Most of the validation is done in TagFileProcessor + // when it created a TagInfo object from the + // tag file in which the directive appeared. + + // This method does additional processing to collect page info + + Attributes attrs = n.getAttributes(); + for (int i = 0; attrs != null && i < attrs.getLength(); i++) { + String attr = attrs.getQName(i); + String value = attrs.getValue(i); + + if ("language".equals(attr)) { + if (pageInfo.getLanguage(false) == null) { + pageInfo.setLanguage(value, n, err, false); + } else if (!pageInfo.getLanguage(false).equals(value)) { + err.jspError(n, "jsp.error.tag.conflict.language", + pageInfo.getLanguage(false), value); + } + } else if ("isELIgnored".equals(attr)) { + if (pageInfo.getIsELIgnored() == null) { + pageInfo.setIsELIgnored(value, n, err, false); + } else if (!pageInfo.getIsELIgnored().equals(value)) { + err.jspError(n, "jsp.error.tag.conflict.iselignored", + pageInfo.getIsELIgnored(), value); + } + } else if ("errorOnELNotFound".equals(attr)) { + if (pageInfo.getErrorOnELNotFound() == null) { + pageInfo.setErrorOnELNotFound(value, n, err, false); + } else if (!pageInfo.getErrorOnELNotFound().equals(value)) { + err.jspError(n, "jsp.error.tag.conflict.errorOnELNotFound", + pageInfo.getErrorOnELNotFound(), value); + } + } else if ("pageEncoding".equals(attr)) { + if (pageEncodingSeen) { + err.jspError(n, "jsp.error.tag.multi.pageencoding"); + } + pageEncodingSeen = true; + compareTagEncodings(value, n); + n.getRoot().setPageEncoding(value); + } else if ("deferredSyntaxAllowedAsLiteral".equals(attr)) { + if (pageInfo.getDeferredSyntaxAllowedAsLiteral() == null) { + pageInfo.setDeferredSyntaxAllowedAsLiteral(value, n, + err, false); + } else if (!pageInfo.getDeferredSyntaxAllowedAsLiteral() + .equals(value)) { + err + .jspError( + n, + "jsp.error.tag.conflict.deferredsyntaxallowedasliteral", + pageInfo + .getDeferredSyntaxAllowedAsLiteral(), + value); + } + } else if ("trimDirectiveWhitespaces".equals(attr)) { + if (pageInfo.getTrimDirectiveWhitespaces() == null) { + pageInfo.setTrimDirectiveWhitespaces(value, n, err, + false); + } else if (!pageInfo.getTrimDirectiveWhitespaces().equals( + value)) { + err + .jspError( + n, + "jsp.error.tag.conflict.trimdirectivewhitespaces", + pageInfo.getTrimDirectiveWhitespaces(), + value); + } + } + } + + // Attributes for imports for this node have been processed by + // the parsers, just add them to pageInfo. + pageInfo.addImports(n.getImports()); + } + + @Override + public void visit(Node.AttributeDirective n) throws JasperException { + // Do nothing, since this attribute directive has already been + // validated by TagFileProcessor when it created a TagInfo object + // from the tag file in which the directive appeared + } + + @Override + public void visit(Node.VariableDirective n) throws JasperException { + // Do nothing, since this variable directive has already been + // validated by TagFileProcessor when it created a TagInfo object + // from the tag file in which the directive appeared + } + + /* + * Compares page encodings specified in various places, and throws + * exception in case of page encoding mismatch. + * + * @param pageDirEnc The value of the pageEncoding attribute of the page + * directive @param pageDir The page directive node + * + * @throws JasperException in case of page encoding mismatch + */ + private String comparePageEncodings(String thePageDirEnc, + Node.PageDirective pageDir) throws JasperException { + + Node.Root root = pageDir.getRoot(); + String configEnc = root.getJspConfigPageEncoding(); + String pageDirEnc = thePageDirEnc.toUpperCase(Locale.ENGLISH); + + /* + * Compare the 'pageEncoding' attribute of the page directive with + * the encoding specified in the JSP config element whose URL + * pattern matches this page. Treat "UTF-16", "UTF-16BE", and + * "UTF-16LE" as identical. + */ + if (configEnc != null) { + configEnc = configEnc.toUpperCase(Locale.ENGLISH); + if (!pageDirEnc.equals(configEnc) + && (!pageDirEnc.startsWith("UTF-16") || !configEnc + .startsWith("UTF-16"))) { + err.jspError(pageDir, + "jsp.error.config_pagedir_encoding_mismatch", + configEnc, pageDirEnc); + } else { + return configEnc; + } + } + + /* + * Compare the 'pageEncoding' attribute of the page directive with + * the encoding specified in the XML prolog (only for XML syntax, + * and only if JSP document contains XML prolog with encoding + * declaration). Treat "UTF-16", "UTF-16BE", and "UTF-16LE" as + * identical. + */ + if ((root.isXmlSyntax() && root.isEncodingSpecifiedInProlog()) || root.isBomPresent()) { + String pageEnc = root.getPageEncoding().toUpperCase(Locale.ENGLISH); + if (!pageDirEnc.equals(pageEnc) + && (!pageDirEnc.startsWith("UTF-16") || !pageEnc + .startsWith("UTF-16"))) { + err.jspError(pageDir, + "jsp.error.prolog_pagedir_encoding_mismatch", + pageEnc, pageDirEnc); + } else { + return pageEnc; + } + } + + return pageDirEnc; + } + + /* + * Compares page encodings specified in various places, and throws + * exception in case of page encoding mismatch. + * + * @param thePageDirEnc The value of the pageEncoding attribute of the page + * directive @param pageDir The page directive node + * + * @throws JasperException in case of page encoding mismatch + */ + private void compareTagEncodings(String thePageDirEnc, + Node.TagDirective pageDir) throws JasperException { + + Node.Root root = pageDir.getRoot(); + String pageDirEnc = thePageDirEnc.toUpperCase(Locale.ENGLISH); + /* + * Compare the 'pageEncoding' attribute of the page directive with + * the encoding specified in the XML prolog (only for XML syntax, + * and only if JSP document contains XML prolog with encoding + * declaration). Treat "UTF-16", "UTF-16BE", and "UTF-16LE" as + * identical. + */ + if ((root.isXmlSyntax() && root.isEncodingSpecifiedInProlog()) || root.isBomPresent()) { + String pageEnc = root.getPageEncoding().toUpperCase(Locale.ENGLISH); + if (!pageDirEnc.equals(pageEnc) + && (!pageDirEnc.startsWith("UTF-16") || !pageEnc + .startsWith("UTF-16"))) { + err.jspError(pageDir, + "jsp.error.prolog_pagedir_encoding_mismatch", + pageEnc, pageDirEnc); + } + } + } + + } + + /** + * A visitor for validating nodes other than page directives + */ + private static class ValidateVisitor extends Node.Visitor { + + // Pattern to extract a method name from a full method signature + private static final Pattern METHOD_NAME_PATTERN = + Pattern.compile(".*[ \t\n\r]+(.+?)[ \t\n\r]*\\(.*", Pattern.DOTALL); + + private final PageInfo pageInfo; + + private final ErrorDispatcher err; + + private final ClassLoader loader; + + private final StringBuilder buf = new StringBuilder(32); + + private static final JspUtil.ValidAttribute[] jspRootAttrs = { + new JspUtil.ValidAttribute("xsi:schemaLocation"), + new JspUtil.ValidAttribute("version", true) }; + + private static final JspUtil.ValidAttribute[] includeDirectiveAttrs = { new JspUtil.ValidAttribute( + "file", true) }; + + private static final JspUtil.ValidAttribute[] taglibDirectiveAttrs = { + new JspUtil.ValidAttribute("uri"), + new JspUtil.ValidAttribute("tagdir"), + new JspUtil.ValidAttribute("prefix", true) }; + + private static final JspUtil.ValidAttribute[] includeActionAttrs = { + new JspUtil.ValidAttribute("page", true), + new JspUtil.ValidAttribute("flush") }; + + private static final JspUtil.ValidAttribute[] paramActionAttrs = { + new JspUtil.ValidAttribute("name", true), + new JspUtil.ValidAttribute("value", true) }; + + private static final JspUtil.ValidAttribute[] forwardActionAttrs = { + new JspUtil.ValidAttribute("page", true) }; + + private static final JspUtil.ValidAttribute[] getPropertyAttrs = { + new JspUtil.ValidAttribute("name", true), + new JspUtil.ValidAttribute("property", true) }; + + private static final JspUtil.ValidAttribute[] setPropertyAttrs = { + new JspUtil.ValidAttribute("name", true), + new JspUtil.ValidAttribute("property", true), + new JspUtil.ValidAttribute("value", false), + new JspUtil.ValidAttribute("param") }; + + private static final JspUtil.ValidAttribute[] useBeanAttrs = { + new JspUtil.ValidAttribute("id", true), + new JspUtil.ValidAttribute("scope"), + new JspUtil.ValidAttribute("class"), + new JspUtil.ValidAttribute("type"), + new JspUtil.ValidAttribute("beanName", false) }; + + private static final JspUtil.ValidAttribute[] plugInAttrs = { + new JspUtil.ValidAttribute("type", true), + new JspUtil.ValidAttribute("code", true), + new JspUtil.ValidAttribute("codebase"), + new JspUtil.ValidAttribute("align"), + new JspUtil.ValidAttribute("archive"), + new JspUtil.ValidAttribute("height", false), + new JspUtil.ValidAttribute("hspace"), + new JspUtil.ValidAttribute("jreversion"), + new JspUtil.ValidAttribute("name"), + new JspUtil.ValidAttribute("vspace"), + new JspUtil.ValidAttribute("width", false), + new JspUtil.ValidAttribute("nspluginurl"), + new JspUtil.ValidAttribute("iepluginurl") }; + + private static final JspUtil.ValidAttribute[] attributeAttrs = { + new JspUtil.ValidAttribute("name", true), + new JspUtil.ValidAttribute("trim"), + new JspUtil.ValidAttribute("omit")}; + + private static final JspUtil.ValidAttribute[] invokeAttrs = { + new JspUtil.ValidAttribute("fragment", true), + new JspUtil.ValidAttribute("var"), + new JspUtil.ValidAttribute("varReader"), + new JspUtil.ValidAttribute("scope") }; + + private static final JspUtil.ValidAttribute[] doBodyAttrs = { + new JspUtil.ValidAttribute("var"), + new JspUtil.ValidAttribute("varReader"), + new JspUtil.ValidAttribute("scope") }; + + private static final JspUtil.ValidAttribute[] jspOutputAttrs = { + new JspUtil.ValidAttribute("omit-xml-declaration"), + new JspUtil.ValidAttribute("doctype-root-element"), + new JspUtil.ValidAttribute("doctype-public"), + new JspUtil.ValidAttribute("doctype-system") }; + + private final ExpressionFactory expressionFactory; + + /* + * Constructor + */ + ValidateVisitor(Compiler compiler) { + this.pageInfo = compiler.getPageInfo(); + this.err = compiler.getErrorDispatcher(); + this.loader = compiler.getCompilationContext().getClassLoader(); + // Get the cached EL expression factory for this context + expressionFactory = + JspFactory.getDefaultFactory().getJspApplicationContext( + compiler.getCompilationContext().getServletContext()). + getExpressionFactory(); + } + + @Override + public void visit(Node.JspRoot n) throws JasperException { + JspUtil.checkAttributes("Jsp:root", n, jspRootAttrs, err); + String version = n.getTextAttribute("version"); + if (!version.equals("1.2") && !version.equals("2.0") && + !version.equals("2.1") && !version.equals("2.2") && + !version.equals("2.3") && !version.equals("3.0") && + !version.equals("3.1")) { + err.jspError(n, "jsp.error.jsproot.version.invalid", version); + } + visitBody(n); + } + + @Override + public void visit(Node.IncludeDirective n) throws JasperException { + JspUtil.checkAttributes("Include directive", n, + includeDirectiveAttrs, err); + visitBody(n); + } + + @Override + public void visit(Node.TaglibDirective n) throws JasperException { + JspUtil.checkAttributes("Taglib directive", n, + taglibDirectiveAttrs, err); + // Either 'uri' or 'tagdir' attribute must be specified + String uri = n.getAttributeValue("uri"); + String tagdir = n.getAttributeValue("tagdir"); + if (uri == null && tagdir == null) { + err.jspError(n, "jsp.error.taglibDirective.missing.location"); + } + if (uri != null && tagdir != null) { + err + .jspError(n, + "jsp.error.taglibDirective.both_uri_and_tagdir"); + } + } + + @Override + public void visit(Node.ParamAction n) throws JasperException { + JspUtil.checkAttributes("Param action", n, paramActionAttrs, err); + // make sure the value of the 'name' attribute is not a + // request-time expression + throwErrorIfExpression(n, "name", "jsp:param"); + n.setValue(getJspAttribute(null, "value", null, null, n + .getAttributeValue("value"), n, null, false)); + visitBody(n); + } + + @Override + public void visit(Node.ParamsAction n) throws JasperException { + // Make sure we've got at least one nested jsp:param + Node.Nodes subElems = n.getBody(); + if (subElems == null) { + err.jspError(n, "jsp.error.params.emptyBody"); + } + visitBody(n); + } + + @Override + public void visit(Node.IncludeAction n) throws JasperException { + JspUtil.checkAttributes("Include action", n, includeActionAttrs, + err); + n.setPage(getJspAttribute(null, "page", null, null, n + .getAttributeValue("page"), n, null, false)); + visitBody(n); + } + + @Override + public void visit(Node.ForwardAction n) throws JasperException { + JspUtil.checkAttributes("Forward", n, forwardActionAttrs, err); + n.setPage(getJspAttribute(null, "page", null, null, n + .getAttributeValue("page"), n, null, false)); + visitBody(n); + } + + @Override + public void visit(Node.GetProperty n) throws JasperException { + JspUtil.checkAttributes("GetProperty", n, getPropertyAttrs, err); + } + + @Override + public void visit(Node.SetProperty n) throws JasperException { + JspUtil.checkAttributes("SetProperty", n, setPropertyAttrs, err); + String property = n.getTextAttribute("property"); + String param = n.getTextAttribute("param"); + String value = n.getAttributeValue("value"); + + n.setValue(getJspAttribute(null, "value", null, null, value, + n, null, false)); + + boolean valueSpecified = n.getValue() != null; + + if ("*".equals(property)) { + if (param != null || valueSpecified) { + err.jspError(n, "jsp.error.setProperty.invalid"); + } + + } else if (param != null && valueSpecified) { + err.jspError(n, "jsp.error.setProperty.invalid"); + } + + visitBody(n); + } + + @Override + public void visit(Node.UseBean n) throws JasperException { + JspUtil.checkAttributes("UseBean", n, useBeanAttrs, err); + + String name = n.getTextAttribute("id"); + String scope = n.getTextAttribute("scope"); + JspUtil.checkScope(scope, n, err); + String className = n.getTextAttribute("class"); + String type = n.getTextAttribute("type"); + BeanRepository beanInfo = pageInfo.getBeanRepository(); + + if (className == null && type == null) { + err.jspError(n, "jsp.error.usebean.missingType"); + } + + if (beanInfo.checkVariable(name)) { + err.jspError(n, "jsp.error.usebean.duplicate"); + } + + if ("session".equals(scope) && !pageInfo.isSession()) { + err.jspError(n, "jsp.error.usebean.noSession"); + } + + Node.JspAttribute jattr = getJspAttribute(null, "beanName", null, + null, n.getAttributeValue("beanName"), n, null, false); + n.setBeanName(jattr); + if (className != null && jattr != null) { + err.jspError(n, "jsp.error.usebean.notBoth"); + } + + if (className == null) { + className = type; + } + + beanInfo.addBean(n, name, className, scope); + + visitBody(n); + } + + @SuppressWarnings("null") // type can't be null after initial test + @Override + public void visit(Node.PlugIn n) throws JasperException { + JspUtil.checkAttributes("Plugin", n, plugInAttrs, err); + + throwErrorIfExpression(n, "type", "jsp:plugin"); + throwErrorIfExpression(n, "code", "jsp:plugin"); + throwErrorIfExpression(n, "codebase", "jsp:plugin"); + throwErrorIfExpression(n, "align", "jsp:plugin"); + throwErrorIfExpression(n, "archive", "jsp:plugin"); + throwErrorIfExpression(n, "hspace", "jsp:plugin"); + throwErrorIfExpression(n, "jreversion", "jsp:plugin"); + throwErrorIfExpression(n, "name", "jsp:plugin"); + throwErrorIfExpression(n, "vspace", "jsp:plugin"); + throwErrorIfExpression(n, "nspluginurl", "jsp:plugin"); + throwErrorIfExpression(n, "iepluginurl", "jsp:plugin"); + + String type = n.getTextAttribute("type"); + if (type == null) { + err.jspError(n, "jsp.error.plugin.notype"); + } + if (!type.equals("bean") && !type.equals("applet")) { + err.jspError(n, "jsp.error.plugin.badtype"); + } + if (n.getTextAttribute("code") == null) { + err.jspError(n, "jsp.error.plugin.nocode"); + } + + Node.JspAttribute width = getJspAttribute(null, "width", null, + null, n.getAttributeValue("width"), n, null, false); + n.setWidth(width); + + Node.JspAttribute height = getJspAttribute(null, "height", null, + null, n.getAttributeValue("height"), n, null, false); + n.setHeight(height); + + visitBody(n); + } + + @Override + public void visit(Node.NamedAttribute n) throws JasperException { + JspUtil.checkAttributes("Attribute", n, attributeAttrs, err); + n.setOmit(getJspAttribute(null, "omit", null, null, n + .getAttributeValue("omit"), n, null, false)); + visitBody(n); + } + + @Override + public void visit(Node.JspBody n) throws JasperException { + visitBody(n); + } + + @Override + public void visit(Node.Declaration n) throws JasperException { + if (pageInfo.isScriptingInvalid()) { + err.jspError(n.getStart(), "jsp.error.no.scriptlets"); + } + } + + @Override + public void visit(Node.Expression n) throws JasperException { + if (pageInfo.isScriptingInvalid()) { + err.jspError(n.getStart(), "jsp.error.no.scriptlets"); + } + } + + @Override + public void visit(Node.Scriptlet n) throws JasperException { + if (pageInfo.isScriptingInvalid()) { + err.jspError(n.getStart(), "jsp.error.no.scriptlets"); + } + } + + @Override + public void visit(Node.ELExpression n) throws JasperException { + // exit if we are ignoring EL all together + if (pageInfo.isELIgnored()) { + return; + } + + // JSP.2.2 - '#{' not allowed in template text + if (n.getType() == '#') { + if (!pageInfo.isDeferredSyntaxAllowedAsLiteral()) { + err.jspError(n, "jsp.error.el.template.deferred"); + } else { + return; + } + } + + // build expression + StringBuilder expr = this.getBuffer(); + expr.append(n.getType()).append('{').append(n.getText()) + .append('}'); + ELNode.Nodes el = ELParser.parse(expr.toString(), pageInfo + .isDeferredSyntaxAllowedAsLiteral()); + + // validate/prepare expression + prepareExpression(el, n, expr.toString()); + + // store it + n.setEL(el); + } + + @Override + public void visit(Node.UninterpretedTag n) throws JasperException { + if (n.getNamedAttributeNodes().size() != 0) { + err.jspError(n, "jsp.error.namedAttribute.invalidUse"); + } + + Attributes attrs = n.getAttributes(); + if (attrs != null) { + int attrSize = attrs.getLength(); + Node.JspAttribute[] jspAttrs = new Node.JspAttribute[attrSize]; + for (int i = 0; i < attrSize; i++) { + // JSP.2.2 - '#{' not allowed in template text + String value = attrs.getValue(i); + if (!pageInfo.isDeferredSyntaxAllowedAsLiteral()) { + if (containsDeferredSyntax(value)) { + err.jspError(n, "jsp.error.el.template.deferred"); + } + } + jspAttrs[i] = getJspAttribute(null, attrs.getQName(i), + attrs.getURI(i), attrs.getLocalName(i), value, n, + null, false); + } + n.setJspAttributes(jspAttrs); + } + + visitBody(n); + } + + /* + * Look for a #{ sequence that isn't preceded by \. + */ + private boolean containsDeferredSyntax(String value) { + if (value == null) { + return false; + } + + int i = 0; + int len = value.length(); + boolean prevCharIsEscape = false; + while (i < value.length()) { + char c = value.charAt(i); + if (c == '#' && (i+1) < len && value.charAt(i+1) == '{' && !prevCharIsEscape) { + return true; + } else if (c == '\\') { + prevCharIsEscape = true; + } else { + prevCharIsEscape = false; + } + i++; + } + return false; + } + + @SuppressWarnings("null") // tagInfo can't be null after initial test + @Override + public void visit(Node.CustomTag n) throws JasperException { + + TagInfo tagInfo = n.getTagInfo(); + if (tagInfo == null) { + err.jspError(n, "jsp.error.missing.tagInfo", n.getQName()); + } + + /* + * The bodycontent of a SimpleTag cannot be JSP. + */ + if (n.implementsSimpleTag() + && tagInfo.getBodyContent().equalsIgnoreCase( + TagInfo.BODY_CONTENT_JSP)) { + err.jspError(n, "jsp.error.simpletag.badbodycontent", tagInfo + .getTagClassName()); + } + + /* + * If the tag handler declares in the TLD that it supports dynamic + * attributes, it also must implement the DynamicAttributes + * interface. + */ + if (tagInfo.hasDynamicAttributes() + && !n.implementsDynamicAttributes()) { + err.jspError(n, "jsp.error.dynamic.attributes.not.implemented", + n.getQName()); + } + + /* + * Make sure all required attributes are present, either as + * attributes or named attributes (). Also make sure + * that the same attribute is not specified in both attributes or + * named attributes. + */ + TagAttributeInfo[] tldAttrs = tagInfo.getAttributes(); + String customActionUri = n.getURI(); + Attributes attrs = n.getAttributes(); + int attrsSize = (attrs == null) ? 0 : attrs.getLength(); + for (TagAttributeInfo tldAttr : tldAttrs) { + String attr = null; + if (attrs != null) { + attr = attrs.getValue(tldAttr.getName()); + if (attr == null) { + attr = attrs.getValue(customActionUri, tldAttr + .getName()); + } + } + Node.NamedAttribute na = n.getNamedAttributeNode(tldAttr + .getName()); + + if (tldAttr.isRequired() && attr == null && na == null) { + err.jspError(n, "jsp.error.missing_attribute", tldAttr + .getName(), n.getLocalName()); + } + if (attr != null && na != null) { + err.jspError(n, "jsp.error.duplicate.name.jspattribute", + tldAttr.getName()); + } + } + + Node.Nodes naNodes = n.getNamedAttributeNodes(); + int jspAttrsSize = naNodes.size() + attrsSize; + Node.JspAttribute[] jspAttrs = null; + if (jspAttrsSize > 0) { + jspAttrs = new Node.JspAttribute[jspAttrsSize]; + } + Hashtable tagDataAttrs = new Hashtable<>(attrsSize); + + checkXmlAttributes(n, jspAttrs, tagDataAttrs); + checkNamedAttributes(n, jspAttrs, attrsSize, tagDataAttrs); + + TagData tagData = new TagData(tagDataAttrs); + + // JSP.C1: It is a (translation time) error for an action that + // has one or more variable subelements to have a TagExtraInfo + // class that returns a non-null object. + TagExtraInfo tei = tagInfo.getTagExtraInfo(); + if (tei != null && tei.getVariableInfo(tagData) != null + && tei.getVariableInfo(tagData).length > 0 + && tagInfo.getTagVariableInfos().length > 0) { + err.jspError("jsp.error.non_null_tei_and_var_subelems", n + .getQName()); + } + + n.setTagData(tagData); + n.setJspAttributes(jspAttrs); + + visitBody(n); + } + + @Override + public void visit(Node.JspElement n) throws JasperException { + + Attributes attrs = n.getAttributes(); + if (attrs == null) { + err.jspError(n, "jsp.error.jspelement.missing.name"); + } + @SuppressWarnings("null") // Exception will have been thrown above + int xmlAttrLen = attrs.getLength(); + + Node.Nodes namedAttrs = n.getNamedAttributeNodes(); + + // XML-style 'name' attribute, which is mandatory, must not be + // included in JspAttribute array + int jspAttrSize = xmlAttrLen - 1 + namedAttrs.size(); + + Node.JspAttribute[] jspAttrs = new Node.JspAttribute[jspAttrSize]; + int jspAttrIndex = 0; + + // Process XML-style attributes + for (int i = 0; i < xmlAttrLen; i++) { + if ("name".equals(attrs.getLocalName(i))) { + n.setNameAttribute(getJspAttribute(null, attrs.getQName(i), + attrs.getURI(i), attrs.getLocalName(i), attrs + .getValue(i), n, null, false)); + } else { + if (jspAttrIndex < jspAttrSize) { + jspAttrs[jspAttrIndex++] = getJspAttribute(null, + attrs.getQName(i), attrs.getURI(i), + attrs.getLocalName(i), attrs.getValue(i), n, + null, false); + } + } + } + if (n.getNameAttribute() == null) { + err.jspError(n, "jsp.error.jspelement.missing.name"); + } + + // Process named attributes + for (int i = 0; i < namedAttrs.size(); i++) { + Node.NamedAttribute na = (Node.NamedAttribute) namedAttrs + .getNode(i); + jspAttrs[jspAttrIndex++] = new Node.JspAttribute(na, null, + false); + } + + n.setJspAttributes(jspAttrs); + + visitBody(n); + } + + @Override + public void visit(Node.JspOutput n) throws JasperException { + JspUtil.checkAttributes("jsp:output", n, jspOutputAttrs, err); + + if (n.getBody() != null) { + err.jspError(n, "jsp.error.jspoutput.nonemptybody"); + } + + String omitXmlDecl = n.getAttributeValue("omit-xml-declaration"); + String doctypeName = n.getAttributeValue("doctype-root-element"); + String doctypePublic = n.getAttributeValue("doctype-public"); + String doctypeSystem = n.getAttributeValue("doctype-system"); + + String omitXmlDeclOld = pageInfo.getOmitXmlDecl(); + String doctypeNameOld = pageInfo.getDoctypeName(); + String doctypePublicOld = pageInfo.getDoctypePublic(); + String doctypeSystemOld = pageInfo.getDoctypeSystem(); + + if (omitXmlDecl != null && omitXmlDeclOld != null + && !omitXmlDecl.equals(omitXmlDeclOld)) { + err.jspError(n, "jsp.error.jspoutput.conflict", + "omit-xml-declaration", omitXmlDeclOld, omitXmlDecl); + } + + if (doctypeName != null && doctypeNameOld != null + && !doctypeName.equals(doctypeNameOld)) { + err.jspError(n, "jsp.error.jspoutput.conflict", + "doctype-root-element", doctypeNameOld, doctypeName); + } + + if (doctypePublic != null && doctypePublicOld != null + && !doctypePublic.equals(doctypePublicOld)) { + err.jspError(n, "jsp.error.jspoutput.conflict", + "doctype-public", doctypePublicOld, doctypePublic); + } + + if (doctypeSystem != null && doctypeSystemOld != null + && !doctypeSystem.equals(doctypeSystemOld)) { + err.jspError(n, "jsp.error.jspoutput.conflict", + "doctype-system", doctypeSystemOld, doctypeSystem); + } + + if (doctypeName == null && doctypeSystem != null + || doctypeName != null && doctypeSystem == null) { + err.jspError(n, "jsp.error.jspoutput.doctypenamesystem"); + } + + if (doctypePublic != null && doctypeSystem == null) { + err.jspError(n, "jsp.error.jspoutput.doctypepublicsystem"); + } + + if (omitXmlDecl != null) { + pageInfo.setOmitXmlDecl(omitXmlDecl); + } + if (doctypeName != null) { + pageInfo.setDoctypeName(doctypeName); + } + if (doctypeSystem != null) { + pageInfo.setDoctypeSystem(doctypeSystem); + } + if (doctypePublic != null) { + pageInfo.setDoctypePublic(doctypePublic); + } + } + + @Override + public void visit(Node.InvokeAction n) throws JasperException { + + JspUtil.checkAttributes("Invoke", n, invokeAttrs, err); + + String scope = n.getTextAttribute("scope"); + JspUtil.checkScope(scope, n, err); + + String var = n.getTextAttribute("var"); + String varReader = n.getTextAttribute("varReader"); + if (scope != null && var == null && varReader == null) { + err.jspError(n, "jsp.error.missing_var_or_varReader"); + } + if (var != null && varReader != null) { + err.jspError(n, "jsp.error.var_and_varReader"); + } + } + + @Override + public void visit(Node.DoBodyAction n) throws JasperException { + + JspUtil.checkAttributes("DoBody", n, doBodyAttrs, err); + + String scope = n.getTextAttribute("scope"); + JspUtil.checkScope(scope, n, err); + + String var = n.getTextAttribute("var"); + String varReader = n.getTextAttribute("varReader"); + if (scope != null && var == null && varReader == null) { + err.jspError(n, "jsp.error.missing_var_or_varReader"); + } + if (var != null && varReader != null) { + err.jspError(n, "jsp.error.var_and_varReader"); + } + } + + /* + * Make sure the given custom action does not have any invalid + * attributes. + * + * A custom action and its declared attributes always belong to the same + * namespace, which is identified by the prefix name of the custom tag + * invocation. For example, in this invocation: + * + * , the action + * + * "test" and its attributes "a", "b", and "c" all belong to the + * namespace identified by the prefix "my". The above invocation would + * be equivalent to: + * + * + * + * An action attribute may have a prefix different from that of the + * action invocation only if the underlying tag handler supports dynamic + * attributes, in which case the attribute with the different prefix is + * considered a dynamic attribute. + */ + private void checkXmlAttributes(Node.CustomTag n, + Node.JspAttribute[] jspAttrs, Map tagDataAttrs) + throws JasperException { + + TagInfo tagInfo = n.getTagInfo(); + TagAttributeInfo[] tldAttrs = tagInfo.getAttributes(); + Attributes attrs = n.getAttributes(); + + for (int i = 0; attrs != null && i < attrs.getLength(); i++) { + boolean found = false; + + boolean runtimeExpression = ((n.getRoot().isXmlSyntax() && attrs.getValue(i).startsWith("%=")) + || (!n.getRoot().isXmlSyntax() && attrs.getValue(i).startsWith("<%="))); + boolean elExpression = false; + boolean deferred = false; + double libraryVersion = Double.parseDouble( + tagInfo.getTagLibrary().getRequiredVersion()); + boolean deferredSyntaxAllowedAsLiteral = + pageInfo.isDeferredSyntaxAllowedAsLiteral() || + libraryVersion < 2.1; + + String xmlAttributeValue = attrs.getValue(i); + + ELNode.Nodes el = null; + if (!runtimeExpression && !pageInfo.isELIgnored()) { + el = ELParser.parse(xmlAttributeValue, + deferredSyntaxAllowedAsLiteral); + Iterator nodes = el.iterator(); + while (nodes.hasNext()) { + ELNode node = nodes.next(); + if (node instanceof ELNode.Root) { + if (((ELNode.Root) node).getType() == '$') { + if (elExpression && deferred) { + err.jspError(n, + "jsp.error.attribute.deferredmix"); + } + elExpression = true; + } else if (((ELNode.Root) node).getType() == '#') { + if (elExpression && !deferred) { + err.jspError(n, + "jsp.error.attribute.deferredmix"); + } + elExpression = true; + deferred = true; + } + } + } + } + + boolean expression = runtimeExpression || elExpression; + + // When attribute is not an expression, + // contains its textual value with \$ and \# escaping removed. + String textAttributeValue; + if (!elExpression && el != null) { + // Should be a single Text node + Iterator it = el.iterator(); + if (it.hasNext()) { + textAttributeValue = ((ELNode.Text) it.next()) + .getText(); + } else { + textAttributeValue = ""; + } + } else { + textAttributeValue = xmlAttributeValue; + } + for (int j = 0; tldAttrs != null && j < tldAttrs.length; j++) { + if (attrs.getLocalName(i).equals(tldAttrs[j].getName()) + && (attrs.getURI(i) == null + || attrs.getURI(i).length() == 0 || attrs + .getURI(i).equals(n.getURI()))) { + + TagAttributeInfo tldAttr = tldAttrs[j]; + if (tldAttr.canBeRequestTime() + || tldAttr.isDeferredMethod() || tldAttr.isDeferredValue()) { // JSP 2.1 + + if (!expression) { + + String expectedType = null; + if (tldAttr.isDeferredMethod()) { + // The String literal must be castable to what is declared as type + // for the attribute + String m = tldAttr.getMethodSignature(); + if (m != null) { + m = m.trim(); + int rti = m.indexOf(' '); + if (rti > 0) { + expectedType = m.substring(0, rti).trim(); + } + } else { + expectedType = "java.lang.Object"; + } + if ("void".equals(expectedType)) { + // Can't specify a literal for a + // deferred method with an expected type + // of void - JSP.2.3.4 + err.jspError(n, + "jsp.error.literal_with_void", + tldAttr.getName()); + } + } + if (tldAttr.isDeferredValue()) { + // The String literal must be castable to what is declared as type + // for the attribute + expectedType = tldAttr.getExpectedTypeName(); + } + if (expectedType != null) { + Class expectedClass = String.class; + try { + expectedClass = JspUtil.toClass(expectedType, loader); + } catch (ClassNotFoundException e) { + err.jspError + (n, "jsp.error.unknown_attribute_type", + tldAttr.getName(), expectedType); + } + // Check casting - not possible for all types + if (String.class.equals(expectedClass) || + expectedClass == Long.TYPE || + expectedClass == Double.TYPE || + expectedClass == Byte.TYPE || + expectedClass == Short.TYPE || + expectedClass == Integer.TYPE || + expectedClass == Float.TYPE || + Number.class.isAssignableFrom(expectedClass) || + Character.class.equals(expectedClass) || + Character.TYPE == expectedClass || + Boolean.class.equals(expectedClass) || + Boolean.TYPE == expectedClass || + expectedClass.isEnum()) { + try { + expressionFactory.coerceToType(textAttributeValue, expectedClass); + } catch (Exception e) { + err.jspError + (n, "jsp.error.coerce_to_type", + tldAttr.getName(), expectedType, textAttributeValue); + } + } + } + + jspAttrs[i] = new Node.JspAttribute(tldAttr, + attrs.getQName(i), attrs.getURI(i), + attrs.getLocalName(i), + textAttributeValue, false, null, false); + } else { + + if (deferred && !tldAttr.isDeferredMethod() && !tldAttr.isDeferredValue()) { + // No deferred expressions allowed for this attribute + err.jspError(n, "jsp.error.attribute.custom.non_rt_with_expr", + tldAttr.getName()); + } + if (!deferred && !tldAttr.canBeRequestTime()) { + // Only deferred expressions are allowed for this attribute + err.jspError(n, "jsp.error.attribute.custom.non_rt_with_expr", + tldAttr.getName()); + } + + // EL or Runtime expression + jspAttrs[i] = getJspAttribute(tldAttr, + attrs.getQName(i), attrs.getURI(i), + attrs.getLocalName(i), + xmlAttributeValue, n, el, false); + } + + } else { + // Attribute does not accept any expressions. + // Make sure its value does not contain any. + if (expression) { + err.jspError(n, "jsp.error.attribute.custom.non_rt_with_expr", + tldAttr.getName()); + } + jspAttrs[i] = new Node.JspAttribute(tldAttr, + attrs.getQName(i), attrs.getURI(i), + attrs.getLocalName(i), + textAttributeValue, false, null, false); + } + if (expression) { + tagDataAttrs.put(attrs.getQName(i), + TagData.REQUEST_TIME_VALUE); + } else { + tagDataAttrs.put(attrs.getQName(i), + textAttributeValue); + } + found = true; + break; + } + } + if (!found) { + if (tagInfo.hasDynamicAttributes()) { + jspAttrs[i] = getJspAttribute(null, attrs.getQName(i), + attrs.getURI(i), attrs.getLocalName(i), + xmlAttributeValue, n, el, true); + } else { + err.jspError(n, "jsp.error.bad_attribute", attrs + .getQName(i), n.getLocalName()); + } + } + } + } + + /* + * Make sure the given custom action does not have any invalid named + * attributes + */ + private void checkNamedAttributes(Node.CustomTag n, + Node.JspAttribute[] jspAttrs, int start, + Map tagDataAttrs) + throws JasperException { + + TagInfo tagInfo = n.getTagInfo(); + TagAttributeInfo[] tldAttrs = tagInfo.getAttributes(); + Node.Nodes naNodes = n.getNamedAttributeNodes(); + + for (int i = 0; i < naNodes.size(); i++) { + Node.NamedAttribute na = (Node.NamedAttribute) naNodes + .getNode(i); + boolean found = false; + for (TagAttributeInfo tldAttr : tldAttrs) { + /* + * See above comment about namespace matches. For named + * attributes, we use the prefix instead of URI as the match + * criterion, because in the case of a JSP document, we'd + * have to keep track of which namespaces are in scope when + * parsing a named attribute, in order to determine the URI + * that the prefix of the named attribute's name matches to. + */ + String attrPrefix = na.getPrefix(); + if (na.getLocalName().equals(tldAttr.getName()) + && (attrPrefix == null || attrPrefix.length() == 0 || attrPrefix + .equals(n.getPrefix()))) { + jspAttrs[start + i] = new Node.JspAttribute(na, + tldAttr, false); + NamedAttributeVisitor nav = null; + if (na.getBody() != null) { + nav = new NamedAttributeVisitor(); + na.getBody().visit(nav); + } + if (nav != null && nav.hasDynamicContent()) { + tagDataAttrs.put(na.getName(), + TagData.REQUEST_TIME_VALUE); + } else { + tagDataAttrs.put(na.getName(), na.getText()); + } + found = true; + break; + } + } + if (!found) { + if (tagInfo.hasDynamicAttributes()) { + jspAttrs[start + i] = new Node.JspAttribute(na, null, + true); + } else { + err.jspError(n, "jsp.error.bad_attribute", + na.getName(), n.getLocalName()); + } + } + } + } + + /** + * Preprocess attributes that can be expressions. Expression delimiters + * are stripped. + *

    + * If value is null, checks if there are any NamedAttribute subelements + * in the tree node, and if so, constructs a JspAttribute out of a child + * NamedAttribute node. + * + * @param el EL expression, if already parsed by the caller (so that we + * can skip re-parsing it) + */ + private Node.JspAttribute getJspAttribute(TagAttributeInfo tai, + String qName, String uri, String localName, String value, + Node n, ELNode.Nodes el, boolean dynamic) + throws JasperException { + + Node.JspAttribute result = null; + + // XXX Is it an error to see "%=foo%" in non-Xml page? + // (We won't see "<%=foo%> in xml page because '<' is not a + // valid attribute value in xml). + + if (value != null) { + if (n.getRoot().isXmlSyntax() && value.startsWith("%=")) { + result = new Node.JspAttribute(tai, qName, uri, localName, + value.substring(2, value.length() - 1), true, null, + dynamic); + } else if (!n.getRoot().isXmlSyntax() + && value.startsWith("<%=")) { + result = new Node.JspAttribute(tai, qName, uri, localName, + value.substring(3, value.length() - 2), true, null, + dynamic); + } else { + if (!pageInfo.isELIgnored()) { + // The attribute can contain expressions but is not a + // scriptlet expression; thus, we want to run it through + // the expression interpreter + + // validate expression syntax if string contains + // expression(s) + if (el == null) { + el = ELParser.parse(value, + pageInfo.isDeferredSyntaxAllowedAsLiteral()); + } + + if (el.containsEL()) { + validateFunctions(el, n); + } else { + // Get text with \$ and \# escaping removed. + // Should be a single Text node + Iterator it = el.iterator(); + if (it.hasNext()) { + value = ((ELNode.Text) it.next()).getText(); + } else { + value = ""; + } + el = null; + } + } + + if (n instanceof Node.UninterpretedTag && + n.getRoot().isXmlSyntax()) { + // Attribute values of uninterpreted tags will have been + // XML un-escaped during parsing. Since these attributes + // are part of an uninterpreted tag the value needs to + // be re-escaped before being included in the output. + // The wrinkle is that the output of any EL must not be + // re-escaped as that must be output as is. + if (el != null) { + XmlEscapeNonELVisitor v = new XmlEscapeNonELVisitor( + pageInfo.isDeferredSyntaxAllowedAsLiteral()); + el.visit(v); + value = v.getText(); + } else { + value = Escape.xml(value); + } + } + + result = new Node.JspAttribute(tai, qName, uri, localName, + value, false, el, dynamic); + + if (el != null) { + ELContextImpl ctx = + new ELContextImpl(expressionFactory); + ctx.setFunctionMapper(getFunctionMapper(el)); + + try { + result.validateEL(this.pageInfo + .getExpressionFactory(), ctx); + } catch (ELException e) { + this.err.jspError(n.getStart(), + "jsp.error.invalid.expression", value, e + .toString()); + } + } + } + } else { + // Value is null. Check for any NamedAttribute subnodes + // that might contain the value for this attribute. + // Otherwise, the attribute wasn't found so we return null. + + Node.NamedAttribute namedAttributeNode = n + .getNamedAttributeNode(qName); + if (namedAttributeNode != null) { + result = new Node.JspAttribute(namedAttributeNode, tai, + dynamic); + } + } + + return result; + } + + + private static class XmlEscapeNonELVisitor extends ELParser.TextBuilder { + + protected XmlEscapeNonELVisitor( + boolean isDeferredSyntaxAllowedAsLiteral) { + super(isDeferredSyntaxAllowedAsLiteral); + } + + @Override + public void visit(Text n) throws JasperException { + output.append(ELParser.escapeLiteralExpression( + Escape.xml(n.getText()), + isDeferredSyntaxAllowedAsLiteral)); + } + } + + + /* + * Return an empty StringBuilder [not thread-safe] + */ + private StringBuilder getBuffer() { + this.buf.setLength(0); + return this.buf; + } + + /* + * Checks to see if the given attribute value represents a runtime or EL + * expression. + */ + private boolean isExpression(Node n, String value, boolean checkDeferred) { + + boolean runtimeExpression = ((n.getRoot().isXmlSyntax() && value.startsWith("%=")) + || (!n.getRoot().isXmlSyntax() && value.startsWith("<%="))); + boolean elExpression = false; + + if (!runtimeExpression && !pageInfo.isELIgnored()) { + Iterator nodes = ELParser.parse(value, + pageInfo.isDeferredSyntaxAllowedAsLiteral()).iterator(); + while (nodes.hasNext()) { + ELNode node = nodes.next(); + if (node instanceof ELNode.Root) { + if (((ELNode.Root) node).getType() == '$') { + elExpression = true; + break; + } else if (checkDeferred && !pageInfo.isDeferredSyntaxAllowedAsLiteral() + && ((ELNode.Root) node).getType() == '#') { + elExpression = true; + break; + } + } + } + } + + return runtimeExpression || elExpression; + + } + + /* + * Throws exception if the value of the attribute with the given name in + * the given node is given as an RT or EL expression, but the spec + * requires a static value. + */ + private void throwErrorIfExpression(Node n, String attrName, + String actionName) throws JasperException { + if (n.getAttributes() != null + && n.getAttributes().getValue(attrName) != null + && isExpression(n, n.getAttributes().getValue(attrName), true)) { + err.jspError(n, + "jsp.error.attribute.standard.non_rt_with_expr", + attrName, actionName); + } + } + + private static class NamedAttributeVisitor extends Node.Visitor { + private boolean hasDynamicContent; + + @Override + public void doVisit(Node n) throws JasperException { + if (!(n instanceof Node.JspText) + && !(n instanceof Node.TemplateText)) { + hasDynamicContent = true; + } + visitBody(n); + } + + public boolean hasDynamicContent() { + return hasDynamicContent; + } + } + + private String findUri(String prefix, Node n) { + + for (Node p = n; p != null; p = p.getParent()) { + Attributes attrs = p.getTaglibAttributes(); + if (attrs == null) { + continue; + } + for (int i = 0; i < attrs.getLength(); i++) { + String name = attrs.getQName(i); + int k = name.indexOf(':'); + if (prefix == null && k < 0) { + // prefix not specified and a default ns found + return attrs.getValue(i); + } + if (prefix != null && k >= 0 + && prefix.equals(name.substring(k + 1))) { + return attrs.getValue(i); + } + } + } + return null; + } + + /** + * Validate functions in EL expressions + */ + private void validateFunctions(ELNode.Nodes el, Node n) + throws JasperException { + + class FVVisitor extends ELNode.Visitor { + + private Node n; + + FVVisitor(Node n) { + this.n = n; + } + + @Override + public void visit(ELNode.Function func) throws JasperException { + String prefix = func.getPrefix(); + String function = func.getName(); + String uri = null; + + if (n.getRoot().isXmlSyntax()) { + uri = findUri(prefix, n); + } else if (prefix != null) { + uri = pageInfo.getURI(prefix); + } + + if (uri == null) { + if (prefix == null) { + // This can occur when lambda expressions define + // functions and when functions are imported. No + // longer able to be sure this is an error. + return; + } else { + err.jspError(n, "jsp.error.attribute.invalidPrefix", + prefix); + } + } + TagLibraryInfo taglib = pageInfo.getTaglib(uri); + FunctionInfo funcInfo = null; + if (taglib != null) { + funcInfo = taglib.getFunction(function); + } + if (funcInfo == null) { + err.jspError(n, "jsp.error.noFunction", function); + } + // Skip TLD function uniqueness check. Done by Schema ? + func.setUri(uri); + func.setFunctionInfo(funcInfo); + processSignature(func); + } + } + + el.visit(new FVVisitor(n)); + } + + private void prepareExpression(ELNode.Nodes el, Node n, String expr) + throws JasperException { + validateFunctions(el, n); + + // test it out + ELContextImpl ctx = new ELContextImpl(expressionFactory); + ctx.setFunctionMapper(this.getFunctionMapper(el)); + ExpressionFactory ef = this.pageInfo.getExpressionFactory(); + try { + ef.createValueExpression(ctx, expr, Object.class); + } catch (ELException e) { + throw new JasperException(e); + } + } + + private void processSignature(ELNode.Function func) + throws JasperException { + func.setMethodName(getMethod(func)); + func.setParameters(getParameters(func)); + } + + /** + * Get the method name from the signature. + */ + private String getMethod(ELNode.Function func) throws JasperException { + FunctionInfo funcInfo = func.getFunctionInfo(); + String signature = funcInfo.getFunctionSignature(); + + Matcher m = METHOD_NAME_PATTERN.matcher(signature); + if (!m.matches()) { + err.jspError("jsp.error.tld.fn.invalid.signature", func + .getPrefix(), func.getName()); + } + + return m.group(1); + } + + /** + * Get the parameters types from the function signature. + * + * @return An array of parameter class names + */ + private String[] getParameters(ELNode.Function func) + throws JasperException { + FunctionInfo funcInfo = func.getFunctionInfo(); + String signature = funcInfo.getFunctionSignature(); + List params = new ArrayList<>(); + // Signature is of the form + // S ( ',' )* )? ')' + int start = signature.indexOf('(') + 1; + boolean lastArg = false; + while (true) { + int p = signature.indexOf(',', start); + if (p < 0) { + p = signature.indexOf(')', start); + if (p < 0) { + err.jspError("jsp.error.tld.fn.invalid.signature", func + .getPrefix(), func.getName()); + } + lastArg = true; + } + String arg = signature.substring(start, p).trim(); + if (!arg.isEmpty()) { + params.add(arg); + } + if (lastArg) { + break; + } + start = p + 1; + } + return params.toArray(new String[0]); + } + + private FunctionMapper getFunctionMapper(ELNode.Nodes el) + throws JasperException { + + class ValidateFunctionMapper extends FunctionMapper { + + private Map fnmap = new HashMap<>(); + + @Override + public void mapFunction(String prefix, String localName, + Method method) { + fnmap.put(prefix + ":" + localName, method); + } + + @Override + public Method resolveFunction(String prefix, String localName) { + return this.fnmap.get(prefix + ":" + localName); + } + } + + class MapperELVisitor extends ELNode.Visitor { + private ValidateFunctionMapper fmapper; + + MapperELVisitor(ValidateFunctionMapper fmapper) { + this.fmapper = fmapper; + } + + @SuppressWarnings("null") // c can't be null after catch block + @Override + public void visit(ELNode.Function n) throws JasperException { + + // Lambda / ImportHandler defined function + if (n.getFunctionInfo() == null) { + return; + } + + Class c = null; + Method method = null; + try { + c = loader.loadClass(n.getFunctionInfo() + .getFunctionClass()); + } catch (ClassNotFoundException e) { + err.jspError("jsp.error.function.classnotfound", n + .getFunctionInfo().getFunctionClass(), n + .getPrefix() + + ':' + n.getName(), e.getMessage()); + } + String paramTypes[] = n.getParameters(); + int size = paramTypes.length; + Class params[] = new Class[size]; + int i = 0; + try { + for (i = 0; i < size; i++) { + params[i] = JspUtil.toClass(paramTypes[i], loader); + } + method = c.getDeclaredMethod(n.getMethodName(), params); + } catch (ClassNotFoundException e) { + err.jspError("jsp.error.signature.classnotfound", + paramTypes[i], n.getPrefix() + ':' + + n.getName(), e.getMessage()); + } catch (NoSuchMethodException e) { + err.jspError("jsp.error.noFunctionMethod", n + .getMethodName(), n.getName(), c.getName()); + } + fmapper.mapFunction(n.getPrefix(), n.getName(), + method); + } + } + + ValidateFunctionMapper fmapper = new ValidateFunctionMapper(); + el.visit(new MapperELVisitor(fmapper)); + return fmapper; + } + } // End of ValidateVisitor + + /** + * A visitor for validating TagExtraInfo classes of all tags + */ + private static class TagExtraInfoVisitor extends Node.Visitor { + + private final ErrorDispatcher err; + + /* + * Constructor + */ + TagExtraInfoVisitor(Compiler compiler) { + this.err = compiler.getErrorDispatcher(); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + TagInfo tagInfo = n.getTagInfo(); + if (tagInfo == null) { + err.jspError(n, "jsp.error.missing.tagInfo", n.getQName()); + } + + @SuppressWarnings("null") // tagInfo can't be null here + ValidationMessage[] errors = tagInfo.validate(n.getTagData()); + if (errors != null && errors.length != 0) { + StringBuilder errMsg = new StringBuilder(); + errMsg.append("

    "); + errMsg.append(Localizer.getMessage( + "jsp.error.tei.invalid.attributes", n.getQName())); + errMsg.append("

    "); + for (ValidationMessage error : errors) { + errMsg.append("

    "); + if (error.getId() != null) { + errMsg.append(error.getId()); + errMsg.append(": "); + } + errMsg.append(error.getMessage()); + errMsg.append("

    "); + } + + err.jspError(n, errMsg.toString()); + } + + visitBody(n); + } + } + + public static void validateDirectives(Compiler compiler, Node.Nodes page) + throws JasperException { + page.visit(new DirectiveVisitor(compiler)); + } + + public static void validateExDirectives(Compiler compiler, Node.Nodes page) + throws JasperException { + // Determine the default output content type + PageInfo pageInfo = compiler.getPageInfo(); + String contentType = pageInfo.getContentType(); + + if (contentType == null || !contentType.contains("charset=")) { + boolean isXml = page.getRoot().isXmlSyntax(); + String defaultType; + if (contentType == null) { + defaultType = isXml ? "text/xml" : "text/html"; + } else { + defaultType = contentType; + } + + String charset = null; + if (isXml) { + charset = "UTF-8"; + } else { + if (!page.getRoot().isDefaultPageEncoding()) { + charset = page.getRoot().getPageEncoding(); + } + } + + if (charset != null) { + pageInfo.setContentType(defaultType + ";charset=" + charset); + } else { + pageInfo.setContentType(defaultType); + } + } + + /* + * Validate all other nodes. This validation step includes checking a + * custom tag's mandatory and optional attributes against information in + * the TLD (first validation step for custom tags according to + * JSP.10.5). + */ + page.visit(new ValidateVisitor(compiler)); + + /* + * Invoke TagLibraryValidator classes of all imported tags (second + * validation step for custom tags according to JSP.10.5). + */ + validateXmlView(new PageDataImpl(page, compiler), compiler); + + /* + * Invoke TagExtraInfo method isValid() for all imported tags (third + * validation step for custom tags according to JSP.10.5). + */ + page.visit(new TagExtraInfoVisitor(compiler)); + + } + + // ********************************************************************* + // Private (utility) methods + + /** + * Validate XML view against the TagLibraryValidator classes of all imported + * tag libraries. + */ + private static void validateXmlView(PageData xmlView, Compiler compiler) + throws JasperException { + + StringBuilder errMsg = null; + ErrorDispatcher errDisp = compiler.getErrorDispatcher(); + + for (Object o : compiler.getPageInfo().getTaglibs()) { + + if (!(o instanceof TagLibraryInfoImpl)) { + continue; + } + TagLibraryInfoImpl tli = (TagLibraryInfoImpl) o; + + ValidationMessage[] errors = tli.validate(xmlView); + if ((errors != null) && (errors.length != 0)) { + if (errMsg == null) { + errMsg = new StringBuilder(); + } + errMsg.append("

    "); + errMsg.append(Localizer.getMessage( + "jsp.error.tlv.invalid.page", tli.getShortName(), + compiler.getPageInfo().getJspFile())); + errMsg.append("

    "); + for (ValidationMessage error : errors) { + if (error != null) { + errMsg.append("

    "); + errMsg.append(error.getId()); + errMsg.append(": "); + errMsg.append(error.getMessage()); + errMsg.append("

    "); + } + } + } + } + + if (errMsg != null) { + errDisp.jspError(errMsg.toString()); + } + } +} diff --git a/java/org/apache/jasper/compiler/tagplugin/TagPlugin.java b/java/org/apache/jasper/compiler/tagplugin/TagPlugin.java new file mode 100644 index 0000000..4227947 --- /dev/null +++ b/java/org/apache/jasper/compiler/tagplugin/TagPlugin.java @@ -0,0 +1,36 @@ +/* + * 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.jasper.compiler.tagplugin; + +/** + * This interface is to be implemented by the plugin author, to supply + * an alternate implementation of the tag handlers. It can be used to + * specify the Java codes to be generated when a tag is invoked. + * + * An implementation of this interface must be registered in a file + * named "tagPlugins.xml" under WEB-INF. + */ + +public interface TagPlugin { + + /** + * Generate codes for a custom tag. + * @param ctxt a TagPluginContext for accessing Jasper functions + */ + void doTag(TagPluginContext ctxt); +} + diff --git a/java/org/apache/jasper/compiler/tagplugin/TagPluginContext.java b/java/org/apache/jasper/compiler/tagplugin/TagPluginContext.java new file mode 100644 index 0000000..b8c7d2b --- /dev/null +++ b/java/org/apache/jasper/compiler/tagplugin/TagPluginContext.java @@ -0,0 +1,136 @@ +/* + * 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.jasper.compiler.tagplugin; + + +/** + * This interface allows the plugin author to make inquiries about the + * properties of the current tag, and to use Jasper resources to generate + * direct Java codes in place of tag handler invocations. + */ + +public interface TagPluginContext { + /** + * @return true if the body of the tag is scriptless. + */ + boolean isScriptless(); + + /** + * @param attribute Name of the attribute + * @return true if the attribute is specified in the tag + */ + boolean isAttributeSpecified(String attribute); + + /** + * @return A unique temporary variable name that the plugin can use. + */ + String getTemporaryVariableName(); + + /** + * Generate an import statement + * @param s Name of the import class, '*' allowed. + */ + void generateImport(String s); + + /** + * Generate a declaration in the of the generated class. This can be + * used to declare an inner class, a method, or a class variable. + * @param id A unique ID identifying the declaration. It is not + * part of the declaration, and is used to ensure that the + * declaration will only appear once. If this method is + * invoked with the same id more than once in the translation + * unit, only the first declaration will be taken. + * @param text The text of the declaration. + */ + void generateDeclaration(String id, String text); + + /** + * Generate Java source code scriptlet + * @param s the scriptlet (raw Java source) + */ + void generateJavaSource(String s); + + /** + * @param attribute The attribute name + * @return true if the attribute is specified and its value is a + * translation-time constant. + */ + boolean isConstantAttribute(String attribute); + + /** + * @param attribute The attribute name + * @return A string that is the value of a constant attribute. Undefined + * if the attribute is not a (translation-time) constant. + * null if the attribute is not specified. + */ + String getConstantAttribute(String attribute); + + /** + * Generate codes to evaluate value of a attribute in the custom tag + * The codes is a Java expression. + * NOTE: Currently cannot handle attributes that are fragments. + * @param attribute The specified attribute + */ + void generateAttribute(String attribute); + + /** + * Generate codes for the body of the custom tag + */ + void generateBody(); + + /** + * Abandon optimization for this tag handler, and instruct + * Jasper to generate the tag handler calls, as usual. + * Should be invoked if errors are detected, or when the tag body + * is deemed too complicated for optimization. + */ + void dontUseTagPlugin(); + + /** + * Get the PluginContext for the parent of this custom tag. NOTE: + * The operations available for PluginContext so obtained is limited + * to getPluginAttribute and setPluginAttribute, and queries (e.g. + * isScriptless(). There should be no calls to generate*(). + * @return The pluginContext for the parent node. + * null if the parent is not a custom tag, or if the pluginContext + * if not available (because useTagPlugin is false, e.g). + */ + TagPluginContext getParentContext(); + + /** + * Associate the attribute with a value in the current tagplugin context. + * The plugin attributes can be used for communication among tags that + * must work together as a group. See <c:when> for an example. + * @param attr The attribute name + * @param value The attribute value + */ + void setPluginAttribute(String attr, Object value); + + /** + * Get the value of an attribute in the current tagplugin context. + * @param attr The attribute name + * @return the attribute value + */ + Object getPluginAttribute(String attr); + + /** + * Is the tag being used inside a tag file? + * @return true if inside a tag file + */ + boolean isTagFile(); +} + diff --git a/java/org/apache/jasper/el/ELContextImpl.java b/java/org/apache/jasper/el/ELContextImpl.java new file mode 100644 index 0000000..273c1f1 --- /dev/null +++ b/java/org/apache/jasper/el/ELContextImpl.java @@ -0,0 +1,153 @@ +/* + * 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.jasper.el; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import jakarta.el.ArrayELResolver; +import jakarta.el.BeanELResolver; +import jakarta.el.CompositeELResolver; +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ELResolver; +import jakarta.el.ExpressionFactory; +import jakarta.el.FunctionMapper; +import jakarta.el.ListELResolver; +import jakarta.el.MapELResolver; +import jakarta.el.ResourceBundleELResolver; +import jakarta.el.StaticFieldELResolver; +import jakarta.el.ValueExpression; +import jakarta.el.VariableMapper; + +import org.apache.jasper.Constants; + +/** + * Implementation of ELContext + * + * @author Jacob Hookom + */ +public class ELContextImpl extends ELContext { + + private static final FunctionMapper NullFunctionMapper = new FunctionMapper() { + @Override + public Method resolveFunction(String prefix, String localName) { + return null; + } + }; + + private static final class VariableMapperImpl extends VariableMapper { + + private Map vars; + + @Override + public ValueExpression resolveVariable(String variable) { + if (vars == null) { + return null; + } + return vars.get(variable); + } + + @Override + public ValueExpression setVariable(String variable, + ValueExpression expression) { + if (vars == null) { + vars = new HashMap<>(); + } + if (expression == null) { + return vars.remove(variable); + } else { + return vars.put(variable, expression); + } + } + } + + private static final ELResolver DefaultResolver; + + static { + if (Constants.IS_SECURITY_ENABLED) { + DefaultResolver = null; + } else { + DefaultResolver = new CompositeELResolver(); + ((CompositeELResolver) DefaultResolver).add( + ELManager.getExpressionFactory().getStreamELResolver()); + ((CompositeELResolver) DefaultResolver).add(new StaticFieldELResolver()); + ((CompositeELResolver) DefaultResolver).add(new MapELResolver()); + ((CompositeELResolver) DefaultResolver).add(new ResourceBundleELResolver()); + ((CompositeELResolver) DefaultResolver).add(new ListELResolver()); + ((CompositeELResolver) DefaultResolver).add(new ArrayELResolver()); + ((CompositeELResolver) DefaultResolver).add(new BeanELResolver()); + } + } + + private final ELResolver resolver; + + private FunctionMapper functionMapper = NullFunctionMapper; + + private VariableMapper variableMapper; + + public ELContextImpl(ExpressionFactory factory) { + this(getDefaultResolver(factory)); + } + + public ELContextImpl(ELResolver resolver) { + this.resolver = resolver; + } + + @Override + public ELResolver getELResolver() { + return this.resolver; + } + + @Override + public FunctionMapper getFunctionMapper() { + return this.functionMapper; + } + + @Override + public VariableMapper getVariableMapper() { + if (this.variableMapper == null) { + this.variableMapper = new VariableMapperImpl(); + } + return this.variableMapper; + } + + public void setFunctionMapper(FunctionMapper functionMapper) { + this.functionMapper = functionMapper; + } + + public void setVariableMapper(VariableMapper variableMapper) { + this.variableMapper = variableMapper; + } + + public static ELResolver getDefaultResolver(ExpressionFactory factory) { + if (Constants.IS_SECURITY_ENABLED) { + CompositeELResolver defaultResolver = new CompositeELResolver(); + defaultResolver.add(factory.getStreamELResolver()); + defaultResolver.add(new StaticFieldELResolver()); + defaultResolver.add(new MapELResolver()); + defaultResolver.add(new ResourceBundleELResolver()); + defaultResolver.add(new ListELResolver()); + defaultResolver.add(new ArrayELResolver()); + defaultResolver.add(new BeanELResolver()); + return defaultResolver; + } else { + return DefaultResolver; + } + } +} diff --git a/java/org/apache/jasper/el/ELContextWrapper.java b/java/org/apache/jasper/el/ELContextWrapper.java new file mode 100644 index 0000000..f8d7aaa --- /dev/null +++ b/java/org/apache/jasper/el/ELContextWrapper.java @@ -0,0 +1,89 @@ +/* + * 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.jasper.el; + +import java.util.Locale; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.el.FunctionMapper; +import jakarta.el.VariableMapper; + +/** + * Simple ELContextWrapper for runtime evaluation of EL w/ dynamic FunctionMappers + * + * @author jhook + */ +public final class ELContextWrapper extends ELContext { + + private final ELContext target; + private final FunctionMapper fnMapper; + + public ELContextWrapper(ELContext target, FunctionMapper fnMapper) { + this.target = target; + this.fnMapper = fnMapper; + } + + @Override + public ELResolver getELResolver() { + return this.target.getELResolver(); + } + + @Override + public FunctionMapper getFunctionMapper() { + if (this.fnMapper != null) { + return this.fnMapper; + } + return this.target.getFunctionMapper(); + } + + @Override + public VariableMapper getVariableMapper() { + return this.target.getVariableMapper(); + } + + @Override + public Object getContext(Class key) { + return this.target.getContext(key); + } + + @Override + public Locale getLocale() { + return this.target.getLocale(); + } + + @Override + public boolean isPropertyResolved() { + return this.target.isPropertyResolved(); + } + + @Override + public void putContext(Class key, Object contextObject) throws NullPointerException { + this.target.putContext(key, contextObject); + } + + @Override + public void setLocale(Locale locale) { + this.target.setLocale(locale); + } + + @Override + public void setPropertyResolved(boolean resolved) { + this.target.setPropertyResolved(resolved); + } + +} diff --git a/java/org/apache/jasper/el/ELResolverImpl.java b/java/org/apache/jasper/el/ELResolverImpl.java new file mode 100644 index 0000000..e2c389f --- /dev/null +++ b/java/org/apache/jasper/el/ELResolverImpl.java @@ -0,0 +1,126 @@ +/* + * 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.jasper.el; + +import java.util.Iterator; +import java.util.Objects; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.ELResolver; +import jakarta.el.ExpressionFactory; +import jakarta.el.PropertyNotWritableException; +import jakarta.servlet.jsp.el.VariableResolver; + +@Deprecated +public final class ELResolverImpl extends ELResolver { + + private final VariableResolver variableResolver; + private final ELResolver elResolver; + + public ELResolverImpl(VariableResolver variableResolver, + ExpressionFactory factory) { + this.variableResolver = variableResolver; + this.elResolver = ELContextImpl.getDefaultResolver(factory); + } + + @Override + public Object getValue(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base == null) { + context.setPropertyResolved(base, property); + if (property != null) { + try { + return this.variableResolver.resolveVariable(property + .toString()); + } catch (jakarta.servlet.jsp.el.ELException e) { + throw new ELException(e.getMessage(), e.getCause()); + } + } + } + + if (!context.isPropertyResolved()) { + return elResolver.getValue(context, base, property); + } + return null; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base == null) { + context.setPropertyResolved(base, property); + if (property != null) { + try { + Object obj = this.variableResolver.resolveVariable(property + .toString()); + return (obj != null) ? obj.getClass() : null; + } catch (jakarta.servlet.jsp.el.ELException e) { + throw new ELException(e.getMessage(), e.getCause()); + } + } + } + + if (!context.isPropertyResolved()) { + return elResolver.getType(context, base, property); + } + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, + Object value) { + Objects.requireNonNull(context); + + if (base == null) { + context.setPropertyResolved(base, property); + throw new PropertyNotWritableException( + "Legacy VariableResolver wrapped, not writable"); + } + + if (!context.isPropertyResolved()) { + elResolver.setValue(context, base, property, value); + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + Objects.requireNonNull(context); + + if (base == null) { + context.setPropertyResolved(base, property); + return true; + } + + return elResolver.isReadOnly(context, base, property); + } + + @Override + public Iterator getFeatureDescriptors(ELContext context, Object base) { + return elResolver.getFeatureDescriptors(context, base); + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base == null) { + return String.class; + } + return elResolver.getCommonPropertyType(context, base); + } +} diff --git a/java/org/apache/jasper/el/ExpressionEvaluatorImpl.java b/java/org/apache/jasper/el/ExpressionEvaluatorImpl.java new file mode 100644 index 0000000..0a9aa98 --- /dev/null +++ b/java/org/apache/jasper/el/ExpressionEvaluatorImpl.java @@ -0,0 +1,62 @@ +/* + * 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.jasper.el; + +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; +import jakarta.servlet.jsp.el.ELException; +import jakarta.servlet.jsp.el.ELParseException; +import jakarta.servlet.jsp.el.Expression; +import jakarta.servlet.jsp.el.ExpressionEvaluator; +import jakarta.servlet.jsp.el.FunctionMapper; +import jakarta.servlet.jsp.el.VariableResolver; + +@Deprecated +public final class ExpressionEvaluatorImpl extends ExpressionEvaluator { + + private final ExpressionFactory factory; + + public ExpressionEvaluatorImpl(ExpressionFactory factory) { + this.factory = factory; + } + + @Override + public Expression parseExpression(String expression, + @SuppressWarnings("rawtypes") Class expectedType, + FunctionMapper fMapper) throws ELException { + try { + ELContextImpl ctx = + new ELContextImpl(ELContextImpl.getDefaultResolver(factory)); + if (fMapper != null) { + ctx.setFunctionMapper(new FunctionMapperImpl(fMapper)); + } + ValueExpression ve = this.factory.createValueExpression(ctx, expression, expectedType); + return new ExpressionImpl(ve, factory); + } catch (jakarta.el.ELException e) { + throw new ELParseException(e.getMessage()); + } + } + + @Override + public Object evaluate(String expression, + @SuppressWarnings("rawtypes") Class expectedType, + VariableResolver vResolver, FunctionMapper fMapper) + throws ELException { + return this.parseExpression(expression, expectedType, fMapper).evaluate(vResolver); + } + +} diff --git a/java/org/apache/jasper/el/ExpressionImpl.java b/java/org/apache/jasper/el/ExpressionImpl.java new file mode 100644 index 0000000..cf0e8fe --- /dev/null +++ b/java/org/apache/jasper/el/ExpressionImpl.java @@ -0,0 +1,44 @@ +/* + * 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.jasper.el; + +import jakarta.el.ELContext; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; +import jakarta.servlet.jsp.el.ELException; +import jakarta.servlet.jsp.el.Expression; +import jakarta.servlet.jsp.el.VariableResolver; + +@Deprecated +public final class ExpressionImpl extends Expression { + + private final ValueExpression ve; + private final ExpressionFactory factory; + + + public ExpressionImpl(ValueExpression ve, ExpressionFactory factory) { + this.ve = ve; + this.factory = factory; + } + + @Override + public Object evaluate(VariableResolver vResolver) throws ELException { + ELContext ctx = + new ELContextImpl(new ELResolverImpl(vResolver, factory)); + return ve.getValue(ctx); + } +} diff --git a/java/org/apache/jasper/el/FunctionMapperImpl.java b/java/org/apache/jasper/el/FunctionMapperImpl.java new file mode 100644 index 0000000..490d488 --- /dev/null +++ b/java/org/apache/jasper/el/FunctionMapperImpl.java @@ -0,0 +1,37 @@ +/* + * 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.jasper.el; + +import java.lang.reflect.Method; + +import jakarta.servlet.jsp.el.FunctionMapper; + +@Deprecated +public final class FunctionMapperImpl extends jakarta.el.FunctionMapper { + + private final FunctionMapper fnMapper; + + public FunctionMapperImpl(FunctionMapper fnMapper) { + this.fnMapper = fnMapper; + } + + @Override + public Method resolveFunction(String prefix, String localName) { + return this.fnMapper.resolveFunction(prefix, localName); + } + +} diff --git a/java/org/apache/jasper/el/JasperELResolver.java b/java/org/apache/jasper/el/JasperELResolver.java new file mode 100644 index 0000000..252ae93 --- /dev/null +++ b/java/org/apache/jasper/el/JasperELResolver.java @@ -0,0 +1,310 @@ +/* + * 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.jasper.el; + +import java.beans.FeatureDescriptor; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.el.ArrayELResolver; +import jakarta.el.BeanELResolver; +import jakarta.el.CompositeELResolver; +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.ELResolver; +import jakarta.el.ListELResolver; +import jakarta.el.MapELResolver; +import jakarta.el.PropertyNotFoundException; +import jakarta.el.ResourceBundleELResolver; +import jakarta.el.StaticFieldELResolver; +import jakarta.servlet.jsp.el.ImplicitObjectELResolver; +import jakarta.servlet.jsp.el.ImportELResolver; +import jakarta.servlet.jsp.el.NotFoundELResolver; +import jakarta.servlet.jsp.el.ScopedAttributeELResolver; + +import org.apache.jasper.runtime.ExceptionUtils; +import org.apache.jasper.runtime.JspRuntimeLibrary; + +/** + * Jasper-specific CompositeELResolver that optimizes certain functions to avoid + * unnecessary resolver calls. + */ +public class JasperELResolver extends CompositeELResolver { + + // Keep aligned with class under test + private static final int STANDARD_RESOLVERS_COUNT = 11; + + private AtomicInteger resolversSize = new AtomicInteger(0); + private volatile ELResolver[] resolvers; + private final int appResolversSize; + + public JasperELResolver(List appResolvers, + ELResolver streamResolver) { + appResolversSize = appResolvers.size(); + resolvers = new ELResolver[appResolversSize + STANDARD_RESOLVERS_COUNT]; + + add(new ImplicitObjectELResolver()); + for (ELResolver appResolver : appResolvers) { + add(appResolver); + } + add(streamResolver); + add(new StaticFieldELResolver()); + add(new MapELResolver()); + add(new ResourceBundleELResolver()); + add(new ListELResolver()); + add(new ArrayELResolver()); + if (JspRuntimeLibrary.GRAAL) { + add(new GraalBeanELResolver()); + } + add(new BeanELResolver()); + add(new ScopedAttributeELResolver()); + add(new ImportELResolver()); + add(new NotFoundELResolver()); + } + + @Override + public synchronized void add(ELResolver elResolver) { + super.add(elResolver); + + int size = resolversSize.get(); + + if (resolvers.length > size) { + resolvers[size] = elResolver; + } else { + ELResolver[] nr = new ELResolver[size + 1]; + System.arraycopy(resolvers, 0, nr, 0, size); + nr[size] = elResolver; + + resolvers = nr; + } + resolversSize.incrementAndGet(); + } + + @Override + public Object getValue(ELContext context, Object base, Object property) + throws NullPointerException, PropertyNotFoundException, ELException { + context.setPropertyResolved(false); + + int start; + Object result = null; + + if (base == null) { + // call implicit and app resolvers + int index = 1 /* implicit */ + appResolversSize; + for (int i = 0; i < index; i++) { + result = resolvers[i].getValue(context, base, property); + if (context.isPropertyResolved()) { + return result; + } + } + // skip stream, static and collection-based resolvers (map, + // resource, list, array) and bean + start = index + 7; + if (JspRuntimeLibrary.GRAAL) { + start++; + } + } else { + // skip implicit resolver only + start = 1; + } + + int size = resolversSize.get(); + for (int i = start; i < size; i++) { + result = resolvers[i].getValue(context, base, property); + if (context.isPropertyResolved()) { + return result; + } + } + + return null; + } + + @Override + public Object invoke(ELContext context, Object base, Object method, + Class[] paramTypes, Object[] params) { + String targetMethod = coerceToString(method); + if (targetMethod.length() == 0) { + throw new ELException(new NoSuchMethodException()); + } + + context.setPropertyResolved(false); + + Object result = null; + + // skip implicit and call app resolvers, stream resolver and static + // resolver + int index = 1 /* implicit */ + appResolversSize + + 2 /* stream + static */; + for (int i = 1; i < index; i++) { + result = resolvers[i].invoke( + context, base, targetMethod, paramTypes, params); + if (context.isPropertyResolved()) { + return result; + } + } + + // skip collection (map, resource, list, and array) resolvers + index += 4; + // call bean and the rest of resolvers + int size = resolversSize.get(); + for (int i = index; i < size; i++) { + result = resolvers[i].invoke( + context, base, targetMethod, paramTypes, params); + if (context.isPropertyResolved()) { + return result; + } + } + + return null; + } + + /* + * Copied from org.apache.el.lang.ELSupport#coerceToString(ELContext,Object) + */ + private static String coerceToString(final Object obj) { + if (obj == null) { + return ""; + } else if (obj instanceof String) { + return (String) obj; + } else if (obj instanceof Enum) { + return ((Enum) obj).name(); + } else { + return obj.toString(); + } + } + + /** + * Extend ELResolver for Graal to avoid bean info use if possible, + * as BeanELResolver needs manual reflection configuration. + */ + public static class GraalBeanELResolver extends ELResolver { + + @Override + public Object getValue(ELContext context, Object base, + Object property) { + Objects.requireNonNull(context); + if (base == null || property == null) { + return null; + } + Object value = null; + Method method = getReadMethod(base.getClass(), property.toString()); + if (method != null) { + context.setPropertyResolved(base, property); + try { + method.setAccessible(true); + value = method.invoke(base, (Object[]) null); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + } + } + return value; + } + + @Override + public void setValue(ELContext context, Object base, Object property, + Object value) { + Objects.requireNonNull(context); + if (base == null || property == null) { + return; + } + Method method = getWriteMethod(base.getClass(), property.toString(), value.getClass()); + if (method != null) { + context.setPropertyResolved(base, property); + try { + method.invoke(base, value); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + } + } + } + + @Override + public boolean isReadOnly(ELContext context, Object base, + Object property) { + Objects.requireNonNull(context); + if (base == null || property == null) { + return false; + } + Class beanClass = base.getClass(); + String prop = property.toString(); + Method readMethod = getReadMethod(beanClass, prop); + return readMethod == null || !(getWriteMethod(beanClass, prop, readMethod.getReturnType()) != null); + } + + private static Method getReadMethod(Class beanClass, String prop) { + Method methods[] = beanClass.getMethods(); + String isGetter = "is" + capitalize(prop); + String getter = "get" + capitalize(prop); + for (Method method : methods) { + if (method.getParameterCount() == 0) { + if (isGetter.equals(method.getName()) && method.getReturnType().equals(boolean.class)) { + return method; + } else if (getter.equals(method.getName())) { + return method; + } + } + } + return null; + } + + private static Method getWriteMethod(Class beanClass, String prop, Class valueClass) { + String setter = "set" + capitalize(prop); + Method methods[] = beanClass.getMethods(); + for (Method method : methods) { + if (method.getParameterCount() == 1 && setter.equals(method.getName()) + && (valueClass == null || valueClass.isAssignableFrom(method.getParameterTypes()[0]))) { + return method; + } + } + return null; + } + + private static String capitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + char chars[] = name.toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + return new String(chars); + } + + @Override + public Class getType(ELContext context, Object base, + Object property) { + return null; + } + + @Override + public Iterator getFeatureDescriptors( + ELContext context, Object base) { + return null; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + if (base != null) { + return Object.class; + } + return null; + } + } +} \ No newline at end of file diff --git a/java/org/apache/jasper/el/JspELException.java b/java/org/apache/jasper/el/JspELException.java new file mode 100644 index 0000000..a881dd3 --- /dev/null +++ b/java/org/apache/jasper/el/JspELException.java @@ -0,0 +1,28 @@ +/* + * 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.jasper.el; + +import jakarta.el.ELException; + +public class JspELException extends ELException { + + private static final long serialVersionUID = 1L; + + public JspELException(String mark, ELException e) { + super(mark + " " + e.getMessage(), e.getCause()); + } +} diff --git a/java/org/apache/jasper/el/JspMethodExpression.java b/java/org/apache/jasper/el/JspMethodExpression.java new file mode 100644 index 0000000..bc9ad0a --- /dev/null +++ b/java/org/apache/jasper/el/JspMethodExpression.java @@ -0,0 +1,165 @@ +/* + * 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.jasper.el; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.MethodExpression; +import jakarta.el.MethodInfo; +import jakarta.el.MethodNotFoundException; +import jakarta.el.MethodReference; +import jakarta.el.PropertyNotFoundException; + +public final class JspMethodExpression extends MethodExpression implements + Externalizable { + + private String mark; + + private MethodExpression target; + + public JspMethodExpression() { + super(); + } + + public JspMethodExpression(String mark, MethodExpression target) { + this.target = target; + this.mark = mark; + } + + @Override + public MethodInfo getMethodInfo(ELContext context) + throws NullPointerException, PropertyNotFoundException, + MethodNotFoundException, ELException { + context.notifyBeforeEvaluation(getExpressionString()); + try { + MethodInfo result = this.target.getMethodInfo(context); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } catch (MethodNotFoundException e) { + if (e instanceof JspMethodNotFoundException) { + throw e; + } + throw new JspMethodNotFoundException(this.mark, e); + } catch (PropertyNotFoundException e) { + if (e instanceof JspPropertyNotFoundException) { + throw e; + } + throw new JspPropertyNotFoundException(this.mark, e); + } catch (ELException e) { + if (e instanceof JspELException) { + throw e; + } + throw new JspELException(this.mark, e); + } + } + + @Override + public Object invoke(ELContext context, Object[] params) + throws NullPointerException, PropertyNotFoundException, + MethodNotFoundException, ELException { + context.notifyBeforeEvaluation(getExpressionString()); + try { + Object result = this.target.invoke(context, params); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } catch (MethodNotFoundException e) { + if (e instanceof JspMethodNotFoundException) { + throw e; + } + throw new JspMethodNotFoundException(this.mark, e); + } catch (PropertyNotFoundException e) { + if (e instanceof JspPropertyNotFoundException) { + throw e; + } + throw new JspPropertyNotFoundException(this.mark, e); + } catch (ELException e) { + if (e instanceof JspELException) { + throw e; + } + throw new JspELException(this.mark, e); + } + } + + @Override + public MethodReference getMethodReference(ELContext context) { + context.notifyBeforeEvaluation(getExpressionString()); + try { + MethodReference result = this.target.getMethodReference(context); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } catch (MethodNotFoundException e) { + if (e instanceof JspMethodNotFoundException) { + throw e; + } + throw new JspMethodNotFoundException(this.mark, e); + } catch (PropertyNotFoundException e) { + if (e instanceof JspPropertyNotFoundException) { + throw e; + } + throw new JspPropertyNotFoundException(this.mark, e); + } catch (ELException e) { + if (e instanceof JspELException) { + throw e; + } + throw new JspELException(this.mark, e); + } + } + + @Override + public boolean isParametersProvided() { + return this.target.isParametersProvided(); + } + + @Override + public boolean equals(Object obj) { + return this.target.equals(obj); + } + + @Override + public int hashCode() { + return this.target.hashCode(); + } + + @Override + public String getExpressionString() { + return this.target.getExpressionString(); + } + + @Override + public boolean isLiteralText() { + return this.target.isLiteralText(); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(this.mark); + out.writeObject(this.target); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.mark = in.readUTF(); + this.target = (MethodExpression) in.readObject(); + } + +} diff --git a/java/org/apache/jasper/el/JspMethodNotFoundException.java b/java/org/apache/jasper/el/JspMethodNotFoundException.java new file mode 100644 index 0000000..2827b19 --- /dev/null +++ b/java/org/apache/jasper/el/JspMethodNotFoundException.java @@ -0,0 +1,28 @@ +/* + * 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.jasper.el; + +import jakarta.el.MethodNotFoundException; + +public class JspMethodNotFoundException extends MethodNotFoundException { + + private static final long serialVersionUID = 1L; + + public JspMethodNotFoundException(String mark, MethodNotFoundException e) { + super(mark + " " + e.getMessage(), e.getCause()); + } +} diff --git a/java/org/apache/jasper/el/JspPropertyNotFoundException.java b/java/org/apache/jasper/el/JspPropertyNotFoundException.java new file mode 100644 index 0000000..4217449 --- /dev/null +++ b/java/org/apache/jasper/el/JspPropertyNotFoundException.java @@ -0,0 +1,30 @@ +/* + * 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.jasper.el; + +import jakarta.el.PropertyNotFoundException; + +public final class JspPropertyNotFoundException extends + PropertyNotFoundException { + + private static final long serialVersionUID = 1L; + + public JspPropertyNotFoundException(String mark, PropertyNotFoundException e) { + super(mark + " " + e.getMessage(), e.getCause()); + } + +} diff --git a/java/org/apache/jasper/el/JspPropertyNotWritableException.java b/java/org/apache/jasper/el/JspPropertyNotWritableException.java new file mode 100644 index 0000000..38bde63 --- /dev/null +++ b/java/org/apache/jasper/el/JspPropertyNotWritableException.java @@ -0,0 +1,29 @@ +/* + * 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.jasper.el; + +import jakarta.el.PropertyNotWritableException; + +public class JspPropertyNotWritableException extends + PropertyNotWritableException { + + private static final long serialVersionUID = 1L; + + public JspPropertyNotWritableException(String mark, PropertyNotWritableException e) { + super(mark + " " + e.getMessage(), e.getCause()); + } +} diff --git a/java/org/apache/jasper/el/JspValueExpression.java b/java/org/apache/jasper/el/JspValueExpression.java new file mode 100644 index 0000000..64f6ccd --- /dev/null +++ b/java/org/apache/jasper/el/JspValueExpression.java @@ -0,0 +1,177 @@ +/* + * 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.jasper.el; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.PropertyNotFoundException; +import jakarta.el.PropertyNotWritableException; +import jakarta.el.ValueExpression; + +/** + * Wrapper for providing context to ValueExpressions + * + * @author Jacob Hookom + */ +public final class JspValueExpression extends ValueExpression implements + Externalizable { + + private ValueExpression target; + + private String mark; + + public JspValueExpression() { + super(); + } + + public JspValueExpression(String mark, ValueExpression target) { + this.target = target; + this.mark = mark; + } + + @Override + public Class getExpectedType() { + return this.target.getExpectedType(); + } + + @Override + public Class getType(ELContext context) throws NullPointerException, + PropertyNotFoundException, ELException { + context.notifyBeforeEvaluation(getExpressionString()); + try { + Class result = this.target.getType(context); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } catch (PropertyNotFoundException e) { + if (e instanceof JspPropertyNotFoundException) { + throw e; + } + throw new JspPropertyNotFoundException(this.mark, e); + } catch (ELException e) { + if (e instanceof JspELException) { + throw e; + } + throw new JspELException(this.mark, e); + } + } + + @Override + public boolean isReadOnly(ELContext context) throws NullPointerException, + PropertyNotFoundException, ELException { + context.notifyBeforeEvaluation(getExpressionString()); + try { + boolean result = this.target.isReadOnly(context); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } catch (PropertyNotFoundException e) { + if (e instanceof JspPropertyNotFoundException) { + throw e; + } + throw new JspPropertyNotFoundException(this.mark, e); + } catch (ELException e) { + if (e instanceof JspELException) { + throw e; + } + throw new JspELException(this.mark, e); + } + } + + @Override + public void setValue(ELContext context, Object value) + throws NullPointerException, PropertyNotFoundException, + PropertyNotWritableException, ELException { + context.notifyBeforeEvaluation(getExpressionString()); + try { + this.target.setValue(context, value); + context.notifyAfterEvaluation(getExpressionString()); + } catch (PropertyNotWritableException e) { + if (e instanceof JspPropertyNotWritableException) { + throw e; + } + throw new JspPropertyNotWritableException(this.mark, e); + } catch (PropertyNotFoundException e) { + if (e instanceof JspPropertyNotFoundException) { + throw e; + } + throw new JspPropertyNotFoundException(this.mark, e); + } catch (ELException e) { + if (e instanceof JspELException) { + throw e; + } + throw new JspELException(this.mark, e); + } + } + + @Override + public T getValue(ELContext context) throws NullPointerException, + PropertyNotFoundException, ELException { + context.notifyBeforeEvaluation(getExpressionString()); + try { + T result = this.target.getValue(context); + context.notifyAfterEvaluation(getExpressionString()); + return result; + } catch (PropertyNotFoundException e) { + if (e instanceof JspPropertyNotFoundException) { + throw e; + } + throw new JspPropertyNotFoundException(this.mark, e); + } catch (ELException e) { + if (e instanceof JspELException) { + throw e; + } + throw new JspELException(this.mark, e); + } + } + + @Override + public boolean equals(Object obj) { + return this.target.equals(obj); + } + + @Override + public int hashCode() { + return this.target.hashCode(); + } + + @Override + public String getExpressionString() { + return this.target.getExpressionString(); + } + + @Override + public boolean isLiteralText() { + return this.target.isLiteralText(); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(this.mark); + out.writeObject(this.target); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.mark = in.readUTF(); + this.target = (ValueExpression) in.readObject(); + } +} diff --git a/java/org/apache/jasper/el/VariableResolverImpl.java b/java/org/apache/jasper/el/VariableResolverImpl.java new file mode 100644 index 0000000..18bd48c --- /dev/null +++ b/java/org/apache/jasper/el/VariableResolverImpl.java @@ -0,0 +1,37 @@ +/* + * 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.jasper.el; + +import jakarta.el.ELContext; +import jakarta.servlet.jsp.el.ELException; +import jakarta.servlet.jsp.el.VariableResolver; + +@Deprecated +public final class VariableResolverImpl implements VariableResolver { + + private final ELContext ctx; + + public VariableResolverImpl(ELContext ctx) { + this.ctx = ctx; + } + + @Override + public Object resolveVariable(String pName) throws ELException { + return this.ctx.getELResolver().getValue(this.ctx, null, pName); + } + +} diff --git a/java/org/apache/jasper/optimizations/ELInterpreterTagSetters.java b/java/org/apache/jasper/optimizations/ELInterpreterTagSetters.java new file mode 100644 index 0000000..e2ea5f1 --- /dev/null +++ b/java/org/apache/jasper/optimizations/ELInterpreterTagSetters.java @@ -0,0 +1,269 @@ +/* + * 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.jasper.optimizations; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.el.ELResolver; + +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.compiler.ELInterpreter; +import org.apache.jasper.compiler.JspUtil; +import org.apache.jasper.compiler.Localizer; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * A non-specification compliant {@link ELInterpreter} that optimizes a subset + * of setters for tag attributes. + *

    + * The cases optimized by this implementation are: + *

      + *
    • expressions that are solely a literal boolean
    • + *
    • expressions that are solely a constant string used (with coercion where + * necessary) with a setter that accepts:
    • + *
      • + *
      • boolean / Boolean
      • + *
      • char / Character
      • + *
      • BigDecimal
      • + *
      • long / Long
      • + *
      • int / Integer
      • + *
      • short / Short
      • + *
      • byte / Byte
      • + *
      • double / Double
      • + *
      • float / Float
      • + *
      • BigInteger
      • + *
      • Enum
      • + *
      • String
      • + *
    • + *
    + * The specification compliance issue is that it essentially skips the first + * three {@link ELResolver}s listed in section JSP.2.9 and effectively hard + * codes the use of the 4th {@link ELResolver} in that list. + * + * @see "https://bz.apache.org/bugzilla/show_bug.cgi?id=64872" + */ +public class ELInterpreterTagSetters implements ELInterpreter { + + // Can't be static + private final Log log = LogFactory.getLog(ELInterpreterTagSetters.class); + + private final Pattern PATTERN_BOOLEAN = Pattern.compile("[$][{]([\"']?)(true|false)\\1[}]"); + private final Pattern PATTERN_STRING_CONSTANT = Pattern.compile("[$][{]([\"'])(\\w+)\\1[}]"); + private final Pattern PATTERN_NUMERIC = Pattern.compile("[$][{]([\"'])([+-]?\\d+(\\.\\d+)?)\\1[}]"); + + @Override + public String interpreterCall(JspCompilationContext context, + boolean isTagFile, String expression, + Class expectedType, String fnmapvar) { + + String result = null; + + // Boolean + if (Boolean.TYPE == expectedType) { + Matcher m = PATTERN_BOOLEAN.matcher(expression); + if (m.matches()) { + result = m.group(2); + } + } else if (Boolean.class == expectedType) { + Matcher m = PATTERN_BOOLEAN.matcher(expression); + if (m.matches()) { + if ("true".equals(m.group(2))) { + result = "Boolean.TRUE"; + } else { + result = "Boolean.FALSE"; + } + } + // Character + } else if (Character.TYPE == expectedType) { + Matcher m = PATTERN_STRING_CONSTANT.matcher(expression); + if (m.matches()) { + return "\'" + m.group(2).charAt(0) + "\'"; + } + } else if (Character.class == expectedType) { + Matcher m = PATTERN_STRING_CONSTANT.matcher(expression); + if (m.matches()) { + return "Character.valueOf(\'" + m.group(2).charAt(0) + "\')"; + } + // Numeric - BigDecimal + } else if (BigDecimal.class == expectedType) { + Matcher m = PATTERN_NUMERIC.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings("unused") + BigDecimal unused = new BigDecimal(m.group(2)); + result = "new java.math.BigDecimal(\"" + m.group(2) + "\")"; + } catch (NumberFormatException e) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "BigDecimal"), e); + // Continue and resolve the value at runtime + } + } + // Numeric - long/Long + } else if (Long.TYPE == expectedType || Long.class == expectedType) { + Matcher m = PATTERN_NUMERIC.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings("unused") + Long unused = Long.valueOf(m.group(2)); + if (expectedType.isPrimitive()) { + // Long requires explicit declaration as a long literal + result = m.group(2) + "L"; + } else { + result = "Long.valueOf(\"" + m.group(2) + "\")"; + } + } catch (NumberFormatException e) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "Long"), e); + // Continue and resolve the value at runtime + } + } + // Numeric - int/Integer + } else if (Integer.TYPE == expectedType || Integer.class == expectedType) { + Matcher m = PATTERN_NUMERIC.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings("unused") + Integer unused = Integer.valueOf(m.group(2)); + if (expectedType.isPrimitive()) { + result = m.group(2); + } else { + result = "Integer.valueOf(\"" + m.group(2) + "\")"; + } + } catch (NumberFormatException e) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "Integer"), e); + // Continue and resolve the value at runtime + } + } + // Numeric - short/Short + } else if (Short.TYPE == expectedType || Short.class == expectedType) { + Matcher m = PATTERN_NUMERIC.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings("unused") + Short unused = Short.valueOf(m.group(2)); + if (expectedType.isPrimitive()) { + // short requires a downcast + result = "(short) " + m.group(2); + } else { + result = "Short.valueOf(\"" + m.group(2) + "\")"; + } + } catch (NumberFormatException e) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "Short"), e); + // Continue and resolve the value at runtime + } + } + // Numeric - byte/Byte + } else if (Byte.TYPE == expectedType || Byte.class == expectedType) { + Matcher m = PATTERN_NUMERIC.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings("unused") + Byte unused = Byte.valueOf(m.group(2)); + if (expectedType.isPrimitive()) { + // byte requires a downcast + result = "(byte) " + m.group(2); + } else { + result = "Byte.valueOf(\"" + m.group(2) + "\")"; + } + } catch (NumberFormatException e) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "Byte"), e); + // Continue and resolve the value at runtime + } + } + // Numeric - double/Double + } else if (Double.TYPE == expectedType || Double.class == expectedType) { + Matcher m = PATTERN_NUMERIC.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings("unused") + Double unused = Double.valueOf(m.group(2)); + if (expectedType.isPrimitive()) { + result = m.group(2); + } else { + result = "Double.valueOf(\"" + m.group(2) + "\")"; + } + } catch (NumberFormatException e) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "Double"), e); + // Continue and resolve the value at runtime + } + } + // Numeric - float/Float + } else if (Float.TYPE == expectedType || Float.class == expectedType) { + Matcher m = PATTERN_NUMERIC.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings("unused") + Float unused = Float.valueOf(m.group(2)); + if (expectedType.isPrimitive()) { + // Float requires explicit declaration as a float literal + result = m.group(2) + "f"; + } else { + result = "Float.valueOf(\"" + m.group(2) + "\")"; + } + } catch (NumberFormatException e) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "Float"), e); + // Continue and resolve the value at runtime + } + } + // Numeric - BigInteger + } else if (BigInteger.class == expectedType) { + Matcher m = PATTERN_NUMERIC.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings("unused") + BigInteger unused = new BigInteger(m.group(2)); + result = "new java.math.BigInteger(\"" + m.group(2) + "\")"; + } catch (NumberFormatException e) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "BigInteger"), e); + // Continue and resolve the value at runtime + } + } + // Enum + } else if (expectedType.isEnum()){ + Matcher m = PATTERN_STRING_CONSTANT.matcher(expression); + if (m.matches()) { + try { + @SuppressWarnings({ "unchecked", "rawtypes" }) + Enum enumValue = Enum.valueOf((Class) expectedType, m.group(2)); + result = expectedType.getName() + "." + enumValue.name(); + } catch (IllegalArgumentException iae) { + log.debug(Localizer.getMessage("jsp.error.typeConversion", m.group(2), "Enum[" + expectedType.getName() + "]"), iae); + // Continue and resolve the value at runtime + } + } + // String + } else if (String.class == expectedType) { + Matcher m = PATTERN_STRING_CONSTANT.matcher(expression); + if (m.matches()) { + result = "\"" + m.group(2) + "\""; + } + } + + if (result == null) { + result = JspUtil.interpreterCall(isTagFile, expression, expectedType, + fnmapvar); + } + + if (log.isTraceEnabled()) { + log.trace("Expression [" + expression + "], type [" + expectedType.getName() + "], returns [" + result + "]"); + } + + return result; + } +} diff --git a/java/org/apache/jasper/optimizations/StringInterpreterEnum.java b/java/org/apache/jasper/optimizations/StringInterpreterEnum.java new file mode 100644 index 0000000..b85f339 --- /dev/null +++ b/java/org/apache/jasper/optimizations/StringInterpreterEnum.java @@ -0,0 +1,37 @@ +/* + * 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.jasper.optimizations; + +import org.apache.jasper.compiler.StringInterpreterFactory.DefaultStringInterpreter; + +/** + * Provides an optimised conversion of string values to Enums. It bypasses the + * check for registered PropertyEditor. + */ +public class StringInterpreterEnum extends DefaultStringInterpreter { + + @Override + protected String coerceToOtherType(Class c, String s, boolean isNamedAttribute) { + if (c.isEnum() && !isNamedAttribute) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + Enum enumValue = Enum.valueOf((Class) c, s); + return c.getName() + "." + enumValue.name(); + } + + return null; + } +} diff --git a/java/org/apache/jasper/resources/LocalStrings.properties b/java/org/apache/jasper/resources/LocalStrings.properties new file mode 100644 index 0000000..d97e126 --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings.properties @@ -0,0 +1,435 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=According to TLD, tag [{0}] must be empty, but is not + +jsp.compiled=Compiled [{0}] {1}ms +jsp.engine.info=Jasper JSP {0} Engine +jsp.error.URLMustStartWithSlash=Path [{0}] must start with a slash character +jsp.error.action.isnottagfile=[{0}] action can be used in tag files only +jsp.error.action.istagfile=[{0}] action cannot be used in a tag file +jsp.error.attempt_to_clear_flushed_buffer=Error: Attempt to clear a buffer that's already been flushed +jsp.error.attr.quoted=Attribute value should be quoted +jsp.error.attribute.custom.non_rt_with_expr=According to TLD or attribute directive in tag file, attribute [{0}] does not accept any expressions +jsp.error.attribute.deferredmix=Cannot use both ${} and #{} EL expressions in the same attribute value +jsp.error.attribute.duplicate=Attribute qualified names must be unique within an element +jsp.error.attribute.invalidPrefix=The attribute prefix [{0}] does not correspond to any imported tag library +jsp.error.attribute.noequal=equal symbol expected +jsp.error.attribute.noescape=Attribute value [{0}] is quoted with [{1}] which must be escaped when used within the value +jsp.error.attribute.noquote=quote symbol expected +jsp.error.attribute.nowhitespace=The JSP specification requires that an attribute name is preceded by whitespace +jsp.error.attribute.null_name=Null attribute name +jsp.error.attribute.standard.non_rt_with_expr=The [{0}] attribute of the [{1}] standard action does not accept any expressions +jsp.error.attribute.unterminated=attribute value for [{0}] is not properly terminated +jsp.error.backgroundCompilationFailed=Background compilation failed +jsp.error.bad.scratch.dir=The scratchDir you specified: [{0}] is unusable. +jsp.error.badStandardAction=Invalid standard action +jsp.error.bad_attribute=Attribute [{0}] invalid for tag [{1}] according to TLD +jsp.error.bad_tag=No tag [{0}] defined in tag library imported with prefix [{1}] +jsp.error.beans.nomethod=Cannot find a method to read property [{0}] in a bean of type [{1}] +jsp.error.beans.nomethod.setproperty=Cannot find a method to write property [{0}] of type [{1}] in a bean of type [{2}] +jsp.error.beans.noproperty=Cannot find any information on property [{0}] in a bean of type [{1}] +jsp.error.beans.nullbean=Attempted a bean operation on a null object. +jsp.error.beans.property.conversion=Unable to convert string [{0}] to class [{1}] for attribute [{2}]: [{3}] +jsp.error.beans.propertyeditor.notregistered=Property Editor not registered with the PropertyEditorManager +jsp.error.beans.setproperty.noindexset=Cannot set indexed property +jsp.error.bug48498=Unable to display JSP extract. Probably due to an XML parser bug (see Tomcat bug 48498 for details). +jsp.error.cannotAddResolver=Cannot call addELResolver after the first request has been made +jsp.error.classname=Cannot determine classname from .class file +jsp.error.coerce_to_type=Cannot coerce value [{2}] to type [{1}] for attribute [{0}]. +jsp.error.compilation=Error compiling file: [{0}] [{1}] +jsp.error.compilation.dependent=Failed to load class [{0}] +jsp.error.compilation.jdt=Compilation error +jsp.error.compilation.jdtProblemError=Error processing JDT problems list +jsp.error.compilation.source=Error loading source file [{0}] +jsp.error.compiler=No Java compiler available +jsp.error.compiler.config=No Java compiler available for configuration options compilerClassName: [{0}] and compiler: [{1}] +jsp.error.compiler.missingResource=Problem accessing resource, treat as outdated +jsp.error.config_pagedir_encoding_mismatch=Page-encoding specified in jsp-property-group [{0}] is different from that specified in page directive [{1}] +jsp.error.corresponding.servlet=Generated servlet error:\n +jsp.error.could.not.add.taglibraries=Could not add one or more tag libraries. +jsp.error.data.file.processing=Error processing file [{0}] +jsp.error.data.file.read=Error reading file [{0}] +jsp.error.data.file.write=Error while writing data file +jsp.error.deferredmethodandvalue='deferredValue' and 'deferredMethod' cannot be both 'true' +jsp.error.deferredmethodsignaturewithoutdeferredmethod=Cannot specify a method signature if 'deferredMethod' is not 'true' +jsp.error.deferredvaluetypewithoutdeferredvalue=Cannot specify a value type if 'deferredValue' is not 'true' +jsp.error.directive.isnottagfile=[{0}] directive can only be used in a tag file +jsp.error.directive.istagfile=[{0}] directive cannot be used in a tag file +jsp.error.duplicate.name.jspattribute=The attribute [{0}] specified in the standard or custom action also appears as the value of the name attribute in the enclosed jsp:attribute +jsp.error.duplicateqname=An attribute with duplicate qualified name [{0}] was found. Attribute qualified names must be unique within an element. +jsp.error.dynamic.attributes.not.implemented=The [{0}] tag declares that it accepts dynamic attributes but does not implement the required interface +jsp.error.el.parse=[{0}] : [{1}] +jsp.error.el.template.deferred=#{...} is not allowed in template text +jsp.error.el_interpreter_class.instantiation=Failed to load or instantiate ELInterpreter class [{0}] +jsp.error.fallback.invalidUse=jsp:fallback must be a direct child of jsp:plugin +jsp.error.file.already.registered=Recursive include of file [{0}] +jsp.error.file.cannot.read=Cannot read file: [{0}] +jsp.error.file.close=Exception closing reader +jsp.error.file.not.found=JSP file [{0}] not found +jsp.error.flush=Exception occurred when flushing data +jsp.error.fragmentwithtype=Cannot specify both ''fragment'' and ''type'' attributes. If ''fragment'' is present, ''type'' is fixed as ''{0}'' +jsp.error.function.classnotfound=The class [{0}] specified in TLD for the function [{1}] cannot be found: [{2}] +jsp.error.include.exception=Unable to include [{0}] +jsp.error.include.tag=Invalid jsp:include tag +jsp.error.internal.filenotfound=Internal Error: File [{0}] not found +jsp.error.invalid.attribute=[{0}] has invalid attribute: [{1}] +jsp.error.invalid.bean=The value for the useBean class attribute [{0}] is invalid. +jsp.error.invalid.directive=Invalid directive +jsp.error.invalid.expression=[{0}] contains invalid expression(s): [{1}] +jsp.error.invalid.implicit=Invalid implicit TLD for tag file at [{0}] +jsp.error.invalid.implicit.version=Invalid JSP version defined in implicit TLD for tag file at [{0}] +jsp.error.invalid.name=File [{0}] uses name [{1}] in jsp:getProperty for a bean that was not previously introduced as per JSP.5.3 +jsp.error.invalid.scope=Illegal value of ''scope'' attribute: [{0}] (must be one of "page", "request", "session", or "application") +jsp.error.invalid.tagdir=Tag file directory [{0}] does not start with "/WEB-INF/tags" +jsp.error.invalid.varscope=Invalid variable scope [{0}] +jsp.error.invalid.version=Invalid JSP version defined for tag file at [{0}] +jsp.error.ise_on_clear=Illegal to clear() when buffer size == 0 +jsp.error.java.line.number=An error occurred at line: [{0}] in the generated java file: [{1}] +jsp.error.javac=Javac exception +jsp.error.javac.env=Environment: +jsp.error.jspbody.emptybody.only=The [{0}] tag can only have jsp:attribute in its body. +jsp.error.jspbody.invalidUse=jsp:body must be the subelement of a standard or custom action +jsp.error.jspbody.required=Must use jsp:body to specify tag body for [{0}] if jsp:attribute is used. +jsp.error.jspc.missingTarget=Missing target: Must specify -webapp or -uriroot, or one or more JSP pages +jsp.error.jspc.no_uriroot=The uriroot is not specified and cannot be located with the specified JSP file(s) +jsp.error.jspc.uriroot_not_dir=The -uriroot option must specify a pre-existing directory +jsp.error.jspelement.missing.name=Mandatory XML-style 'name' attribute missing +jsp.error.jspoutput.conflict=<jsp:output>: illegal to have multiple occurrences of [{0}] with different values (old: [{1}], new: [{2}]) +jsp.error.jspoutput.doctypenamesystem=<jsp:output>: 'doctype-root-element' and 'doctype-system' attributes must appear together +jsp.error.jspoutput.doctypepublicsystem=<jsp:output>: 'doctype-system' attribute must appear if 'doctype-public' attribute appears +jsp.error.jspoutput.invalidUse=<jsp:output> must not be used in standard syntax +jsp.error.jspoutput.nonemptybody=<jsp:output> must not have a body +jsp.error.jsproot.version.invalid=Invalid version number: [{0}], must be "1.2", "2.0", "2.1", "2.2", "2.3", "3.0" or "3.1" +jsp.error.jsptext.badcontent='<', when appears in the body of <jsp:text>, must be encapsulated within a CDATA +jsp.error.lastModified=Unable to determine last modified date for file [{0}] +jsp.error.library.invalid=JSP page is invalid according to library [{0}]: [{1}] +jsp.error.literal_with_void=A literal value was specified for attribute [{0}] that is defined as a deferred method with a return type of void. JSP.2.3.4 does not permit literal values in this case +jsp.error.loadclass.taghandler=Unable to load tag handler class [{0}] for tag [{1}] +jsp.error.location=line: [{0}], column: [{1}] +jsp.error.mandatory.attribute=[{0}]: Mandatory attribute [{1}] missing +jsp.error.missing.tagInfo=TagInfo object for [{0}] is missing from TLD +jsp.error.missing_attribute=According to the TLD or the tag file, attribute [{0}] is mandatory for tag [{1}] +jsp.error.missing_var_or_varReader=Missing 'var' or 'varReader' attribute +jsp.error.namedAttribute.invalidUse=jsp:attribute must be the subelement of a standard or custom action +jsp.error.needAlternateJavaEncoding=Default java encoding [{0}] is invalid on your java platform. An alternate can be specified via the ''javaEncoding'' parameter of JspServlet. +jsp.error.negativeBufferSize=The buffer size is negative +jsp.error.negativeParameter=The parameter [{0}] must not be negative +jsp.error.nested.jspattribute=A jsp:attribute standard action cannot be nested within another jsp:attribute standard action +jsp.error.nested.jspbody=A jsp:body standard action cannot be nested within another jsp:body or jsp:attribute standard action +jsp.error.nested_jsproot=Nested <jsp:root> +jsp.error.no.jsp=Cannot locate JSP file [{0}] +jsp.error.no.more.content=End of content reached while more parsing required: tag nesting error? +jsp.error.no.scratch.dir=The JSP engine is not configured with a scratch dir.\n\ +\ Please add "jsp.initparams=scratchdir=" \n\ +\ in the servlets.properties file for this context. +jsp.error.no.scriptlets=Scripting elements ( <%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) are disallowed here. +jsp.error.noFile=Unable to find file [{0}] +jsp.error.noFunction=The function [{0}] cannot be located with the specified prefix +jsp.error.noFunctionMethod=Method [{0}] for function [{1}] not found in class [{2}] +jsp.error.noInstanceManager=No org.apache.tomcat.InstanceManager set in ServletContext +jsp.error.non_null_tei_and_var_subelems=Tag [{0}] has one or more variable subelements and a TagExtraInfo class that returns one or more VariableInfo +jsp.error.not.in.template=[{0}] not allowed in a template text body. +jsp.error.nullArgument=Null argument +jsp.error.outputfolder=No output directory +jsp.error.outputfolder.detail=Unable to create output directory [{0}] required for JSP compilation +jsp.error.overflow=Error: JSP Buffer overflow +jsp.error.page.conflict.autoflush=Page directive: illegal to have multiple occurrences of ''autoFlush'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.buffer=Page directive: illegal to have multiple occurrences of ''buffer'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.contenttype=Page directive: illegal to have multiple occurrences of ''contentType'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.deferredsyntaxallowedasliteral=Page directive: illegal to have multiple occurrences of ''deferredSyntaxAllowedAsLiteral'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.errorOnELNotFound=Page directive: illegal to have multiple occurrences of ''errorOnELNotFound'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.errorpage=Page directive: illegal to have multiple occurrences of ''errorPage'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.extends=Page directive: illegal to have multiple occurrences of ''extends'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.info=Page directive: illegal to have multiple occurrences of ''info'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.iselignored=Page directive: illegal to have multiple occurrences of ''isELIgnored'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.iserrorpage=Page directive: illegal to have multiple occurrences of ''isErrorPage'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.isthreadsafe=Page directive: illegal to have multiple occurrences of ''isThreadSafe'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.language=Page directive: illegal to have multiple occurrences of ''language'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.session=Page directive: illegal to have multiple occurrences of ''session'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.conflict.trimdirectivewhitespaces=Page directive: illegal to have multiple occurrences of ''trimDirectiveWhitespaces'' with different values (old: [{0}], new: [{1}]) +jsp.error.page.invalid.buffer=Page directive: invalid value for buffer +jsp.error.page.invalid.deferredsyntaxallowedasliteral=Page directive: invalid value for deferredSyntaxAllowedAsLiteral +jsp.error.page.invalid.errorOnELNotFound=Page directive: invalid value for errorOnELNotFound +jsp.error.page.invalid.import=Page directive: invalid value for import +jsp.error.page.invalid.iselignored=Page directive: invalid value for isELIgnored +jsp.error.page.invalid.iserrorpage=Page directive: invalid value for isErrorPage +jsp.error.page.invalid.isthreadsafe=Page directive: invalid value for isThreadSafe +jsp.error.page.invalid.scope=Invalid scope +jsp.error.page.invalid.session=Page directive: invalid value for session +jsp.error.page.invalid.trimdirectivewhitespaces=Page directive: invalid value for trimDirectiveWhitespaces +jsp.error.page.language.nonjava=Page directive: invalid language attribute +jsp.error.page.multi.pageencoding=Page directive must not have multiple occurrences of pageencoding +jsp.error.page.noSession=Cannot access session scope in page that does not participate in any session +jsp.error.page.nullThrowable=Null exception +jsp.error.page.sessionRequired=Page needs a session and none is available +jsp.error.param.invalidUse=The jsp:param action must not be used outside the jsp:include, jsp:forward, or jsp:params elements +jsp.error.paramexpected=Expecting "jsp:param" standard action with "name" and "value" attributes +jsp.error.params.emptyBody=jsp:params must contain at least one nested jsp:param +jsp.error.params.invalidUse=jsp:params must be a direct child of jsp:plugin +jsp.error.parse.error.in.TLD=Parse Error in the tag library descriptor: [{0}] +jsp.error.parse.xml=XML parsing error on file [{0}] +jsp.error.parse.xml.line=XML parsing error on file [{0}]: (line [{1}], col [{2}]) +jsp.error.parse.xml.scripting.invalid.body=Body of [{0}] element must not contain any XML elements +jsp.error.plugin.badtype=Illegal value for 'type' attribute in jsp:plugin: must be 'bean' or 'applet' +jsp.error.plugin.nocode=code not declared in jsp:plugin +jsp.error.plugin.notype=type not declared in jsp:plugin +jsp.error.precompilation=Could not precompile JSP [{0}] +jsp.error.precompilation.parameter=Cannot have precompilation request parameter [{0}] set to [{1}] +jsp.error.prefix.refined=Attempt to redefine the prefix [{0}] to [{1}], when it was already defined as [{2}] in the current scope. +jsp.error.prefix.use_before_dcl=The prefix [{0}] specified in this tag directive has been previously used by an action in file [{1}] line [{2}]. +jsp.error.prolog_config_encoding_mismatch=Page-encoding specified in XML prolog [{0}] is different from that specified in jsp-property-group [{1}] +jsp.error.prolog_pagedir_encoding_mismatch=Page-encoding specified in XML prolog [{0}] is different from that specified in page directive [{1}] +jsp.error.quotes.unterminated=Unterminated quotes +jsp.error.readContent=Unable to read expected length [{0}] +jsp.error.reload=Servlet reload failed +jsp.error.scripting.variable.missing_name=Unable to determine scripting variable name from attribute [{0}] +jsp.error.security=Security initialization failed for context +jsp.error.securityPreload=Error preloading classes +jsp.error.servlet.destroy.failed=Exception during Servlet.destroy() for JSP page +jsp.error.servlet.invalid.method=JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS +jsp.error.setLastModified=Unable to set last modified date for file [{0}] +jsp.error.signature.classnotfound=The class [{0}] specified in the method signature in TLD for the function [{1}] cannot be found. [{2}] +jsp.error.simpletag.badbodycontent=The TLD for the class [{0}] specifies an invalid body-content (JSP) for a SimpleTag. +jsp.error.single.line.number=An error occurred at line: [{0}] in the jsp file: [{1}] +jsp.error.stream.close.failed=Failed to close stream +jsp.error.stream.closed=Stream closed +jsp.error.string_interpreter_class.instantiation=Failed to load or instantiate StringInterpreter class [{0}] +jsp.error.tag.conflict.attr=Tag directive: illegal to have multiple occurrences of the attribute [{0}] with different values (old: [{1}], new: [{2}]) +jsp.error.tag.conflict.deferredsyntaxallowedasliteral=Tag directive: illegal to have multiple occurrences of ''deferredSyntaxAllowedAsLiteral'' with different values (old: [{0}], new: [{1}]) +jsp.error.tag.conflict.errorOnELNotFound=Tag directive: illegal to have multiple occurrences of ''errorOnELNotFound'' with different values (old: [{0}], new: [{1}]) +jsp.error.tag.conflict.iselignored=Tag directive: illegal to have multiple occurrences of ''isELIgnored'' with different values (old: [{0}], new: [{1}]) +jsp.error.tag.conflict.language=Tag directive: illegal to have multiple occurrences of ''language'' with different values (old: [{0}], new: [{1}]) +jsp.error.tag.conflict.trimdirectivewhitespaces=Tag directive: illegal to have multiple occurrences of ''trimDirectiveWhitespaces'' with different values (old: [{0}], new: [{1}]) +jsp.error.tag.invalid.deferredsyntaxallowedasliteral=Tag directive: invalid value for deferredSyntaxAllowedAsLiteral +jsp.error.tag.invalid.errorOnELNotFound=Tag directive: invalid value for errorOnELNotFound +jsp.error.tag.invalid.iselignored=Tag directive: invalid value for isELIgnored +jsp.error.tag.invalid.trimdirectivewhitespaces=Tag directive: invalid value for trimDirectiveWhitespaces +jsp.error.tag.language.nonjava=Tag directive: invalid language attribute +jsp.error.tag.multi.pageencoding=Tag directive must not have multiple occurrences of pageencoding +jsp.error.tagHandlerPool=Cannot create tag handler pool [{0}] +jsp.error.tagdirective.badbodycontent=Invalid body-content [{0}] in tag directive +jsp.error.tagfile.badSuffix=Missing ".tag" suffix in tag file path [{0}] +jsp.error.tagfile.illegalPath=Illegal tag file path: [{0}], must start with "/WEB-INF/tags" or "/META-INF/tags" +jsp.error.tagfile.missingPath=Path not specified to tag file +jsp.error.tagfile.nameFrom.badAttribute=The attribute directive declared at line [{1}] with name [{0}] that matches the name-from-attribute value of this variable directive must be of type java.lang.String, must be "required" and must not be a "rtexprvalue". +jsp.error.tagfile.nameFrom.noAttribute=Cannot find an attribute directive with a name [{0}] that matches the name-from-attribute value of this variable directive +jsp.error.tagfile.nameNotUnique=The value of [{0}] and the value of [{1}] in line [{2}] are the same. +jsp.error.taglibDirective.absUriCannotBeResolved=The absolute uri: [{0}] cannot be resolved in either web.xml or the jar files deployed with this application +jsp.error.taglibDirective.both_uri_and_tagdir=Both 'uri' and 'tagdir' attributes specified +jsp.error.taglibDirective.missing.location=Neither 'uri' nor 'tagdir' attribute specified +jsp.error.taglibDirective.uriInvalid=The URI provided for a tag library [{0}] is not a valid URI +jsp.error.tei.invalid.attributes=Validation error messages from TagExtraInfo for [{0}] +jsp.error.teiclass.instantiation=Failed to load or instantiate TagExtraInfo class: [{0}] +jsp.error.text.has_subelement=<jsp:text> must not have any subelements +jsp.error.tld.fn.duplicate.name=Duplicate function name [{0}] in tag library [{1}] +jsp.error.tld.fn.invalid.signature=Invalid syntax for function signature in TLD. Tag Library: [{0}], Function: [{1}] +jsp.error.tld.invalid_tld_file=Invalid tld file: [{0}], see JSP specification section 7.3.1 for more details +jsp.error.tld.mandatory.element.missing=Mandatory TLD element [{0}] missing or empty in TLD [{1}] +jsp.error.tld.missing=Unable to find taglib [{0}] for URI: [{1}] +jsp.error.tld.missing_jar=Missing JAR resource [{0}] containing TLD +jsp.error.tld.unable_to_get_jar=Unable to get JAR resource [{0}] containing TLD: [{1}] +jsp.error.tld.url=Bad TLD URL [{0}] +jsp.error.tlv.invalid.page=Validation error messages from TagLibraryValidator for [{0}] in [{1}] +jsp.error.tlvclass.instantiation=Failed to load or instantiate TagLibraryValidator class: [{0}] +jsp.error.typeConversion=Failed to convert [{0}] to {1} +jsp.error.unable.compile=Unable to compile class for JSP +jsp.error.unable.deleteClassFile=Unable to delete class file [{0}] +jsp.error.unable.getType=Unable to extract type from [{0}] +jsp.error.unable.load=Unable to load class for JSP +jsp.error.unable.renameClassFile=Unable to rename class file from [{0}] to [{1}] +jsp.error.unable.to_find_method=Unable to find setter method for attribute: [{0}] +jsp.error.unavailable=JSP has been marked unavailable +jsp.error.unbalanced.endtag=The end tag "</{0}" is unbalanced +jsp.error.undeclared_namespace=A custom tag was encountered with an undeclared namespace [{0}] +jsp.error.unexpectedTag=Unexpected tag [{0}] +jsp.error.unknown_attribute_type=Unknown attribute type [{1}] for attribute [{0}]. +jsp.error.unsupported.encoding=Unsupported encoding: [{0}] +jsp.error.unterminated=Unterminated [{0}] tag +jsp.error.usebean.duplicate=useBean: Duplicate bean name: [{0}] +jsp.error.usebean.noSession=Illegal for useBean to use session scope when JSP page declares (via page directive) that it does not participate in sessions +jsp.error.var_and_varReader=Only one of 'var' or 'varReader' may be specified +jsp.error.variable.alias=Both or none of the name-from-attribute and alias attributes must be specified in a variable directive +jsp.error.variable.both.name=Cannot specify both name-given and name-from-attribute attributes in a variable directive +jsp.error.variable.either.name=Either name-given or name-from-attribute attribute must be specified in a variable directive +jsp.error.xml.badStandardAction=Invalid standard action: [{0}] +jsp.error.xml.bad_tag=No tag [{0}] defined in tag library associated with uri [{1}] +jsp.exception=An exception occurred processing [{0}] at line [{1}] +jsp.info.ignoreSetting=Ignored setting for [{0}] of [{1}] because a SecurityManager was enabled +jsp.message.dont.modify.servlets=IMPORTANT: Do not modify the generated servlets +jsp.message.jsp_added=Adding JSP for path [{0}] to queue of context [{1}] +jsp.message.jsp_queue_created=Created jsp queue with length [{0}] for context [{1}] +jsp.message.jsp_queue_update=Updating JSP for path [{0}] in queue of context [{1}] +jsp.message.jsp_removed_excess=Removing excess JSP for path [{0}] from queue of context [{1}] +jsp.message.jsp_removed_idle=Removing idle JSP for path [{0}] in context [{1}] after [{2}] milliseconds +jsp.message.jsp_unload_check=Checking JSPs for unload in context [{0}], JSP count: [{1}] queue length: [{2}] +jsp.message.parent_class_loader_is=Parent class loader is: [{0}] +jsp.message.scratch.dir.is=Scratch dir for the JSP engine is: [{0}] +jsp.tldCache.noTldInDir=No TLD files were found in directory [{0}]. +jsp.tldCache.noTldInJar=No TLD files were found in [{0}]. Consider adding the JAR to the tomcat.util.scan.StandardJarScanFilter.jarsToSkip property in CATALINA_BASE/conf/catalina.properties file. +jsp.tldCache.noTldInResourcePath=No TLD files were found in resource path [{0}]. +jsp.tldCache.noTldSummary=At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time. +jsp.tldCache.tldInDir=TLD files were found in directory [{0}]. +jsp.tldCache.tldInJar=TLD files were found in JAR [{0}]. +jsp.tldCache.tldInResourcePath=TLD files were found in resource path [{0}]. +jsp.warning.bad.urlpattern.propertygroup=Bad value [{0}] in the url-pattern subelement in web.xml +jsp.warning.checkInterval=Warning: Invalid value for the initParam checkInterval. Will use the default value of "300" seconds +jsp.warning.classDebugInfo=Warning: Invalid value for the initParam classdebuginfo. Will use the default value of "false" +jsp.warning.classpathUrl=Invalid URL found in class path. This URL will be ignored +jsp.warning.compiler.classfile.delete.fail=Failed to delete generated class file [{0}] +jsp.warning.compiler.classfile.delete.fail.unknown=Failed to delete generated class file(s) +jsp.warning.compiler.javafile.delete.fail=Failed to delete generated Java file [{0}] +jsp.warning.development=Warning: Invalid value for the initParam development. Will use the default value of "true" +jsp.warning.displaySourceFragment=Warning: Invalid value for the initParam displaySourceFragment. Will use the default value of "true" +jsp.warning.dumpSmap=Warning: Invalid value for the initParam dumpSmap. Will use the default value of "false" +jsp.warning.enablePooling=Warning: Invalid value for the initParam enablePooling. Will use the default value of "true" +jsp.warning.engineOptionsClass=Failed to load engine options class [{0}] +jsp.warning.fork=Warning: Invalid value for the initParam fork. Will use the default value of "true" +jsp.warning.genchararray=Warning: Invalid value for the initParam genStringAsCharArray. Will use the default value of "false" +jsp.warning.isThreadSafe=Warning: The "isThreadSafe" page directive attribute used in [{0}] has been deprecated and will be removed in version 4.0 of the JSP specification +jsp.warning.jspIdleTimeout=Warning: Invalid value for the initParam jspIdleTimeout. Will use the default value of "-1" +jsp.warning.keepgen=Warning: Invalid value for the initParam keepgenerated. Will use the default value of "false" +jsp.warning.loadSmap=Unable to load SMAP data for class [{0}] +jsp.warning.mappedFile=Warning: Invalid value for the initParam mappedFile. Will use the default value of "false" +jsp.warning.maxLoadedJsps=Warning: Invalid value for the initParam maxLoadedJsps. Will use the default value of "-1" +jsp.warning.modificationTestInterval=Warning: Invalid value for the initParam modificationTestInterval. Will use the default value of "4" seconds +jsp.warning.noJarScanner=Warning: No org.apache.tomcat.JarScanner set in ServletContext. Falling back to default JarScanner implementation. +jsp.warning.poolTagsWithExtends=Warning: Invalid value for the initParam poolTagsWithExtends. Will use the default value of "false" +jsp.warning.quoteAttributeEL=Warning: Invalid value for the initParam quoteAttributeEL. Will use the default value of "false" +jsp.warning.recompileOnFail=Warning: Invalid value for the initParam recompileOnFail. Will use the default value of "false" +jsp.warning.strictGetProperty=Warning: Invalid value for the initParam strictGetProperty. Will use the default value of "true" +jsp.warning.strictQuoteEscaping=Warning: Invalid value for the initParam strictQuoteEscaping. Will use the default value of "true" +jsp.warning.strictWhitespace=Warning: Invalid value for the initParam strictWhitespace. Will use the default value of "true" +jsp.warning.suppressSmap=Warning: Invalid value for the initParam suppressSmap. Will use the default value of "false" +jsp.warning.tagPreDestroy=Error processing preDestroy on tag instance of [{0}] +jsp.warning.tagRelease=Error processing release on tag instance of [{0}] +jsp.warning.trimspaces=Warning: Invalid value for the initParam trimSpaces. Will use the default value of "false" +jsp.warning.unknown.sourceVM=Unknown source VM [{0}] ignored +jsp.warning.unknown.targetVM=Unknown target VM [{0}] ignored +jsp.warning.unsupported.sourceVM=Unsupported source VM [{0}] requested, using [{1}] +jsp.warning.unsupported.targetVM=Unsupported target VM [{0}] requested, using [{1}] +jsp.warning.useInstanceManagerForTags=Warning: Invalid value for the initParam useInstanceManagerForTags. Will use the default value of "false" +jsp.warning.xpoweredBy=Warning: Invalid value for the initParam xpoweredBy. Will use the default value of "false" + +jspc.built=Built file [{0}] +jspc.delete.fail=Failed to delete file [{0}] +jspc.error.compilation=Compilation error +jspc.error.fileDoesNotExist=The file argument [{0}] does not exist +jspc.error.generalException=ERROR-the file [{0}] generated the following general exception: +jspc.error.invalidFragment=Failing pre-compilation due to errors in web fragments +jspc.error.invalidWebXml=Failing pre-compilation due to errors in web.xml +jspc.error.minThreadCount=There must be at least one thread [{0}] +jspc.error.parseThreadCount=Cannot parse thread count [{0}] +jspc.error.unknownOption=Unrecognized option [{0}]. Use -help for help. +jspc.errorCount=Error count: [{0}] +jspc.generatingMapping=Generating web mapping for file [{0}] using compilation context [{1}] +jspc.generation.result=Generation completed with [{0}] errors in [{1}] milliseconds +jspc.implicit.uriRoot=uriRoot implicitly set to [{0}] +jspc.outdated=[{0}] is outdated and is compiled +jspc.processing=Processing file [{0}] +jspc.start=Execute starting for {0} pages +jspc.usage=Usage: jspc [--] \n\ +where jsp files is\n\ +\ -webapp A directory containing a web-app, whose JSP pages\n\ +\ will be processed recursively\n\ +or any number of\n\ +\ A file to be parsed as a JSP page\n\ +where options include:\n\ +\ -help Print this help message\n\ +\ -v Verbose mode\n\ +\ -d Output Directory (default -Djava.io.tmpdir)\n\ +\ -l Outputs the name of the JSP page upon failure\n\ +\ -s Outputs the name of the JSP page upon success\n\ +\ -p Name of target package (default org.apache.jsp)\n\ +\ -c Name of target class name (only applies to first JSP page)\n\ +\ -mapped Generates separate write() calls for each HTML line in the JSP\n\ +\ -die[#] Generates an error return code (#) on fatal errors (default 1)\n\ +\ -uribase The uri directory compilations should be relative to\n\ +\ (default "/")\n\ +\ -uriroot Same as -webapp\n\ +\ -compile Compiles generated servlets\n\ +\ -failFast Stop on first compile error\n\ +\ -webinc Creates a partial servlet mappings in the file\n\ +\ -webfrg Creates a complete web-fragment.xml file\n\ +\ -webxml Creates a complete web.xml in the file\n\ +\ -webxmlencoding Set the encoding charset used to read and write the web.xml\n\ +\ file (default is UTF-8)\n\ +\ -addwebxmlmappings Merge generated web.xml fragment into the web.xml file of the\n\ +\ web-app, whose JSP pages we are processing\n\ +\ -ieplugin Java Plugin classid for Internet Explorer\n\ +\ -classpath Overrides java.class.path system property\n\ +\ -xpoweredBy Add X-Powered-By response header\n\ +\ -trimSpaces [single] Remove template text that consists entirely of whitespace\n\ +\ (if "single", replace such template text with a single space)\n\ +\ -javaEncoding Set the encoding charset for Java classes (default UTF-8)\n\ +\ -source Set the -source argument to the compiler (default 1.8)\n\ +\ -target Set the -target argument to the compiler (default 1.8)\n\ +\ -threadCount Number of threads to use for compilation.\n\ +\ ("2.0C" means two threads per core)\n +jspc.warning.tldInWebInfLib=TLD files should not be placed in /WEB-INF/lib" +jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.footer=\n\ +\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webinc.insertEnd= +jspc.webinc.insertStart= +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +jstl.OSAfterWriter=Cannot use an output stream if a writer has been used +jstl.urlMustStartWithSlash=In a URL tag where the ''context'' attribute is specified, both ''url'' and ''context'' must start with a slash character +jstl.writerAfterOS=Cannot use a writer if an output stream has been used + +org.apache.jasper.compiler.ELParser.invalidQuotesForStringLiteral=The String literal [{0}] is not valid. It must be contained within single or double quotes. +org.apache.jasper.compiler.ELParser.invalidQuoting=The expression [{0}] is not valid. Within a quoted String only [], [''] and ["] may be escaped with []. +org.apache.jasper.compiler.TldCache.servletContextNull=The provided ServletContext was null +org.apache.jasper.servlet.JasperInitializer.onStartup=Initializing Jasper for context [{0}] +org.apache.jasper.servlet.TldScanner.webxmlAdd=Loading TLD for URI [{1}] from resource path [{0}] +org.apache.jasper.servlet.TldScanner.webxmlFailPathDoesNotExist=Failed to process TLD with path [{0}] and URI [{1}]. The specified path does not exist. +org.apache.jasper.servlet.TldScanner.webxmlSkip=Skipping load of TLD for URI [{1}] from resource path [{0}] as it has already been defined in diff --git a/java/org/apache/jasper/resources/LocalStrings_cs.properties b/java/org/apache/jasper/resources/LocalStrings_cs.properties new file mode 100644 index 0000000..6293b13 --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_cs.properties @@ -0,0 +1,103 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=Tag [{0}] musí být podle TLD prázdný, ale není + +jsp.error.action.isnottagfile=Akce [{0}] může být použita pouze v tag souborech +jsp.error.attribute.deferredmix=Nelze použít oba EL výrazy ${} a #{} ve stejném atributu +jsp.error.attribute.noequal=jsou oÄekávány stejné symboly +jsp.error.attribute.nowhitespace=JSP specifikace vyžaduje jméno atributu s úvodní mezerou +jsp.error.data.file.processing=Chyba pÅ™i zpracování souboru [{0}] +jsp.error.el.template.deferred=#{...} není dovoleno v textu Å¡ablony +jsp.error.fallback.invalidUse=jsp:fallback musí být přímý potomek jsp:plugin +jsp.error.invalid.tagdir=Adresář tag souborů [{0}] nezaÄíná "/WEB-INF/tags" +jsp.error.invalid.version=Neplatná verze JSP definovaná pro souboru tagů na [{0}] +jsp.error.ise_on_clear=Nedovolené volání metody clear(), když velikost buffer == 0 +jsp.error.jspbody.emptybody.only=Tag [{0}] může mít ve svém tÄ›le pouze jsp:attribute. +jsp.error.jspbody.required=Pro specifikaci tÄ›la tagu [{0}] musí být použito jsp:body, pokud je jsp:attribute použit. +jsp.error.jspelement.missing.name=Chybí požadovaný atribut 'name' +jsp.error.jspoutput.conflict=<jsp:output>: není povoleno více výskytů [{0}] s různými hodnotamy (stará: [{1}], nová: [{2}]) +jsp.error.jsptext.badcontent='<', musí být zapouzdÅ™eno pomocí CDATA, když se objeví v tÄ›le <jsp:text> +jsp.error.mandatory.attribute=[{0}]: Chybí povinný atribut [{1}] +jsp.error.no.scratch.dir=JSP engine není nakonfigurován pracovní (scratch) adresář.\n\ +PÅ™idejte prosím "jsp.initparams=scratchdir=" \n\ +do souboru servlets.properties pro tento kontext +jsp.error.no.scriptlets=Elementy pro skriptování ( <%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) zde nejsou dovoleny. +jsp.error.noFunction=Funkci [{0}] nelze pro uvedený prefix najít +jsp.error.not.in.template=[{0}] není povoleno v tÄ›le textu Å¡ablony. +jsp.error.outputfolder=Výstupní adresář nenalezen +jsp.error.page.conflict.contenttype=Direktiva stránky: více různých vískytů hodnot ''contentType'' neneí dovolené (stará: [{0}], nová: [{1}]) +jsp.error.page.conflict.session=Direktiva stránky: není povoleno více výskytů ''session'' s různými hodnotamy (stará: [{0}], nová: [{1}]) +jsp.error.paramexpected=OÄekávám standardní akci "jsp:param" s atributy "name" a "value" +jsp.error.parse.xml=Chyba parsování XML ze souboru [{0}] +jsp.error.prolog_pagedir_encoding_mismatch=Kódování stránky specifikované v XML prologu [{0}] se liší od nastavení ve stránce [{1}] +jsp.error.scripting.variable.missing_name=Nelze urÄit jméno promÄ›nné ve skriptu z atributu [{0}] +jsp.error.simpletag.badbodycontent=TLD pro třídu [{0}] specifikuje neplatný obsah body(JSP) pro SimpleTag. +jsp.error.tagfile.nameFrom.badAttribute=Nastavení atributu definované na řádce [{1}] s názvem [{0}], které odpovídá hodnotÄ› name-from-atribute nastavení této promÄ›nné, musí být typu java.lang.String, být "required" a nesmí být "rtexprvalue". +jsp.error.tagfile.nameFrom.noAttribute=Nelze najít atribut direktivy pro jméno uvedené v hodnotÄ› [{0}] (hodnota pro name-from-attribute). +jsp.error.taglibDirective.absUriCannotBeResolved=Absolutní uri: [{0}] nelze vyhodnotit v souboru web.xml nebo v jar souborech nasazených s touto aplikací +jsp.error.taglibDirective.missing.location=Není specifikován atribut 'uri' ani 'tagdir' +jsp.error.taglibDirective.uriInvalid=Poskytovatel URI pro knihovnu tagů [{0}] nemá platné URI +jsp.error.teiclass.instantiation=Selhalo nahrání Äi vytvoÅ™ení instance třídy TagExtraInfo: [{0}] +jsp.error.tld.mandatory.element.missing=Povinný TLD element [{0}] chybí nebo je prázdný v TLD [{1}] +jsp.error.unbalanced.endtag=UkonÄovací tag "</{0}" inení symetrický +jsp.error.unknown_attribute_type=Neznámý typ atributu [{1}] pro atribut [{0}]. +jsp.error.variable.either.name=Direktiva promÄ›nné musí specifikovat zadané jméno nebo jméno z atributu +jsp.exception=PÅ™i zpracování se vyskytla výjimka [{0}] na řádku [{1}] +jsp.info.ignoreSetting=Ignorováno nastavení pro [{0}] z [{1}], protože byl aktivní SecurityManager +jsp.message.jsp_queue_update=Aktualizuji JSP na cestÄ› [{0}] ve frontÄ› kontextu [{1}] +jsp.message.jsp_removed_excess=Odstraňuji přístup JSP na cestÄ› [{0}] pro frontu z kontextu [{1}] +jsp.message.jsp_unload_check=Kontroluji JSP pro odebrání z kontextu [{0}], poÄet JSP: [{1}] délka fronty: [{2}] +jsp.warning.displaySourceFragment=UpozornÄ›ní: Neplatná hodnota pro inicializaÄní parametr displaySourceFragment. Bude použita výchozí hodnota "true" +jsp.warning.enablePooling=Varování: Neplatná hodnota inicializaÄního parametru enablePooling. Bude použita výchozí hodnota "true" + +jspc.error.fileDoesNotExist=Argument souboru [{0}] neexistuje +jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +org.apache.jasper.compiler.TldCache.servletContextNull=Poskytnutý ServletContext byl null diff --git a/java/org/apache/jasper/resources/LocalStrings_de.properties b/java/org/apache/jasper/resources/LocalStrings_de.properties new file mode 100644 index 0000000..178e53b --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_de.properties @@ -0,0 +1,120 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=Nach der TLD muss [{0}] leer sein, ist es aber nicht + +jsp.error.URLMustStartWithSlash=Der Pfad [{0}] muss mit ''/'' beginnen +jsp.error.action.isnottagfile=[{0}] Action kann nur in Tag-Dateien benutzt werden +jsp.error.attribute.deferredmix=Kann nicht ${} und #{} gleichzeitig als EL Ausdrücke in demselben Attribut Wert verwenden +jsp.error.attribute.duplicate=Qualifizierte Attributnamen müssen innerhalb eines Elements eindeutig sein +jsp.error.attribute.noequal=Gleichheitszeichen erwartet +jsp.error.attribute.nowhitespace=Die JSP Spezifikation verlangt, dass einem Attribut Namen ein Leerzeichen vorangeht. +jsp.error.backgroundCompilationFailed=Die Hintergrundübersetzung schlug fehl. +jsp.error.cannotAddResolver=Ein ELResolver kann nur hinzugefügt werden, bevor der erste Request gestellt wurde. +jsp.error.compilation.source=Fehler beim Laden der Quelldatei [{0}] +jsp.error.compiler=Keine Java-Compiler verfügbar +jsp.error.data.file.processing=Fehler beim Verarbeiten der Datei [{0}] +jsp.error.el.template.deferred=#{...} is im Template Text nicht erlaubt +jsp.error.fallback.invalidUse=jsp:fallback muss ein direktes Kind von jsp:plugin sein +jsp.error.file.not.found=Datei [{0}] nicht gefunden +jsp.error.internal.filenotfound=Interner Fehler: Datei [{0}] nicht gefunden +jsp.error.invalid.attribute=[{0}] hat ein ungültiges Attribut: [{1}] +jsp.error.invalid.tagdir=Tag Verzeichnis [{0}] beginnt nicht mit "/WEB-INF/tags" +jsp.error.invalid.version=Ungültige JSP Version für tag-Datei in [{0}] definiert +jsp.error.ise_on_clear=Nicht erlaubter Aufruf von clear() wenn Puffer-Größe == 0 +jsp.error.jspbody.emptybody.only=Das [{0}] Tag kann nur jsp:attribute im Body haben. +jsp.error.jspbody.invalidUse=jsp:body muss ein Unterelement einer Standard- oder Custom-Action sein +jsp.error.jspbody.required=jsp:body muss für den Tag Inhalt für [{0}] genutzt werden, wenn jsp:attribute benutzt wird +jsp.error.jspelement.missing.name=Das erforderliche XML Attribut 'name' fehlt +jsp.error.jsptext.badcontent=Wenn '<' im Body von <jsp:text> auftritt, muss es mit einem CDATA eingeschlossen werden +jsp.error.location=Zeile: [{0}], Spalte: [{1}] +jsp.error.mandatory.attribute=[{0}]: Zwingend anzugebendes Attribut [{1}] fehlt +jsp.error.negativeBufferSize=Die Puffergröße ist negativ +jsp.error.no.jsp=Die JSP Datei [{0}] kann nicht gefunden werden +jsp.error.no.scratch.dir=Die JSP-Engine ist nicht mir einem leeren Verzeichnis konfiguriert. Bitte fügen Sie "jsp.initparams=scratchdir=" in die servlets.properties Datei für diesen Kontext. +jsp.error.no.scriptlets=Die Skript-Elemente ( <%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) sind hier nicht erlaubt. +jsp.error.noFunction=Die Funktion [{0}] kann mit dem angegebenen Prefix nicht gefunden werden +jsp.error.noInstanceManager=Es +jsp.error.not.in.template=[{0}] ist nicht im Body des Template Textes erlaubt +jsp.error.outputfolder=Kein Ausgabeordner +jsp.error.page.invalid.scope=Falscher Scope +jsp.error.paramexpected=Erwarte "jsp:param" Standard-Aktion mit den Attributen "name" und "value" +jsp.error.parse.xml=Fehler bei der Verarbeitung der XML Datei [{0}] +jsp.error.precompilation=Die JSP Datei [{0}] konnte nicht vorübersetzt werden. +jsp.error.reload=Erneutes Laden des Servlets schlug fehl +jsp.error.scripting.variable.missing_name=Kann den Namen der Skript Variable vom Attribut [{0}] ableiten +jsp.error.simpletag.badbodycontent=Die TLD für Klasse [{0}] spezifiziert einen ungültigen Body-Content (JSP) für ein SimpleTag. +jsp.error.stream.close.failed=Fehler beim Schließen des Streams +jsp.error.taglibDirective.absUriCannotBeResolved=Die absolute URI: [{0}] kann weder durch web.xml noch durch die JAR-Files dieser Anwendung aufgelöst werden +jsp.error.taglibDirective.missing.location=Weder 'uri' noch 'tagdir' Attribute sind angegeben +jsp.error.taglibDirective.uriInvalid=Die URI, die für die Tag Bibliothek [{0}] zur Verfügung gestellt wurde, ist keine gültige URI +jsp.error.text.has_subelement=<jsp:text> darf keine Subelemente haben +jsp.error.tld.mandatory.element.missing=Notwendiges TLD Element [{0}] fehlt oder ist leer in TLD [{1}] +jsp.error.unable.deleteClassFile=Klassendatei konnte nicht gelöscht werden +jsp.error.unable.renameClassFile=Fehler beim Umbenennen der Klassendatei von [{0}] in [{1}] +jsp.error.unable.to_find_method=Keine Setter Methode für das Attribut [{0}] gefunden. +jsp.error.unavailable=JSP wurde als nicht verfügbar markiert +jsp.error.unknown_attribute_type=Unbekannter Attributstyp [{1}] für Attribut [{0}]. +jsp.error.xml.badStandardAction=Ungültige Standard Aktion: [{0}] +jsp.exception=Beim Verarbeiten von [{0}] ist in Zeile [{1}] eine Ausnahme erzeugt worden +jsp.info.ignoreSetting=Ignoriere Einstellung für [{0}] von [{1}], da ein SecurityManager eingeschaltet war +jsp.message.jsp_queue_update=Passe Queue im Kontext [{1}] für JSP mit Pfad [{0}] an +jsp.message.jsp_removed_excess=Lösche überschüssige JSP für Pfad [{0}] aus der Warteschlange für Context [{1}] +jsp.tldCache.tldInDir=TLD-Dateien wurden gefunden im Verzeichnis [{0}]. +jsp.warning.enablePooling=Warnung: Ungültiger Wert für den initParam enablePooling. Benutze den Standard-Wert "true" +jsp.warning.unknown.targetVM=Unbekannte Ziel-VM [{0}] ignoriert + +jspc.errorCount=Anzahl der Fehler: [{0}] +jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +jstl.OSAfterWriter=Der OutputStream kann nicht mehr benutzt werden, nachdem bereits \n\ +in den Writer geschrieben wurde. +jstl.writerAfterOS=Der Writer kann nicht benutzt werden, wenn bereits der OutputStream benutzt wurde. + +org.apache.jasper.compiler.TldCache.servletContextNull=Der angegebene ServletContext war null diff --git a/java/org/apache/jasper/resources/LocalStrings_es.properties b/java/org/apache/jasper/resources/LocalStrings_es.properties new file mode 100644 index 0000000..e213ba1 --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_es.properties @@ -0,0 +1,339 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=Según el TLD, el tag [{0}] debe de estar vacío, pero no lo está + +jsp.engine.info=Motor Jasper JSP {0} +jsp.error.action.isnottagfile=La acción [{0}] sólo se puede usar en archivos tag +jsp.error.action.istagfile=La acción [{0}] no se puede usar en un archivo tag +jsp.error.attempt_to_clear_flushed_buffer=Error: Se ha intentado limpiar un buffer que ya había sido escrito +jsp.error.attr.quoted=El valor del atributo debería ir entre comillas +jsp.error.attribute.custom.non_rt_with_expr=Según el TLD o la directiva attribute del archivo tag, el atributo [{0}] no acepta expresiones +jsp.error.attribute.deferredmix=No puedo sar ambas espresiones EL ${} y #{} en el mismo valor de atributo +jsp.error.attribute.duplicate=Los nombre cualificados de atributo deben de ser únicos dentro de un elemento +jsp.error.attribute.invalidPrefix=El prefijo de atributo [{0}] no se correponde con ninguna biblioteca importada +jsp.error.attribute.noequal=se esperaba símbolo igual +jsp.error.attribute.noescape=El valor de atributo [{0}] está entrecomillado con [{1}] que debe de usar escape al usarse dentro del valor +jsp.error.attribute.noquote=se esperaba símbolo comillas +jsp.error.attribute.nowhitespace=La especificación JSP requiere que un nombre de atributo sea precedido por un espacio en blanco +jsp.error.attribute.null_name=Nombre de atributo nulo +jsp.error.attribute.standard.non_rt_with_expr=El atributo [{0}] de la acción estándar [{1}] no acepta expresiones +jsp.error.attribute.unterminated=el atributo para [{0}] no está terminado correctamente +jsp.error.bad.scratch.dir=El directorio de trabajo especificado: [{0}] no es utilizable. +jsp.error.badStandardAction=Acción estándar incorrecta +jsp.error.bad_attribute=El atributo [{0}] no es válido según el TLD especificado +jsp.error.bad_tag=No existe el tag [{0}] en la biblioteca importada con prefijo [{1}] +jsp.error.beans.nomethod=No puedo encontrar un método para leer la propiedad [{0}] en un bean del tipo [{1}] +jsp.error.beans.nomethod.setproperty=No puedo encontrar un método para escribir la propiedad [{0}] en un bean del tipo [{2}] +jsp.error.beans.noproperty=No puedo encontrar información de la propiedad [{0}] en un bean del tipo [{1}] +jsp.error.beans.nullbean=Se ha intentado una operación de bean en un objeto nulo +jsp.error.beans.property.conversion=No puedo convertir cadena [{0}] a clase [{1}] para atributo [{2}]: [{3}] +jsp.error.beans.propertyeditor.notregistered=Editor de Propiedades no registrado con el PropertyEditorManager +jsp.error.beans.setproperty.noindexset=No puedo poner la propiedad indexada +jsp.error.bug48498=No puedo mostrar extracto de JSP. Probablemente debido a un error de analizador XML (ver error 48498 de Tomcat para detalles). +jsp.error.classname=No pude determinar el nombre de clase desde el fichero .class +jsp.error.coerce_to_type=No puedo coaccionar el valor [{2}] a tipo [{1}] para atributo [{0}]. +jsp.error.compilation=Error compilando fichero: [{0}] [{1}] +jsp.error.compiler=No hay compilador Java disponible +jsp.error.config_pagedir_encoding_mismatch=El Page-encoding especificado en jsp-property-group [{0}] es diferente del especificado en la diectiva page [{1}] +jsp.error.corresponding.servlet=Error de servlet generado: +jsp.error.could.not.add.taglibraries=No pude añadir una o más bibliotecas. +jsp.error.data.file.processing=Error al procesar el archivo [{0}] +jsp.error.data.file.read=Error leyendo archivo [{0}] +jsp.error.data.file.write=Error mientras escribía el archivo de datos +jsp.error.deferredmethodandvalue='deferredValue' y 'deferredMethod' no pueden ser ambos 'verdadero' +jsp.error.deferredmethodsignaturewithoutdeferredmethod=No puedo especificar firma de método si 'deferredMethod' no es 'verdadero' +jsp.error.deferredvaluetypewithoutdeferredvalue=No puedo especificar un tipo de valor si 'deferredValue' no es 'verdadero' +jsp.error.directive.isnottagfile=La Directiva [{0}] sólo se puede usar en un archivo de tag +jsp.error.directive.istagfile=La Directiva [{0}] no puede usarse en archivo de tag +jsp.error.duplicate.name.jspattribute=El atributo [{0}] especificado en la acción standard o custom también aparece como el valor del atributo name en jsp:attribute +jsp.error.duplicateqname=Se ha hallado un atributo con nombre cualificado duplicado [{0}]. Los nombres de atributos cuallificados deben de se únicos dentro de un elemento. +jsp.error.dynamic.attributes.not.implemented=El tag [{0}] declara que acepta atributos dinámicos pero no implementa la interfaz requerida +jsp.error.el.parse=[{0}] : [{1}] +jsp.error.el.template.deferred=#{..} no está permitido en texto de plantilla +jsp.error.el_interpreter_class.instantiation=No se puede cargar la clase ELInterpreter llamada [{0}] +jsp.error.fallback.invalidUse=jsp:fallback debe de ser un hijo directo de jsp:plugin +jsp.error.file.already.registered=El archivo [{0}] ya se ha visto, ¿podría ser un include recursivo? +jsp.error.file.cannot.read=No se puede leer el archivo: [{0}] +jsp.error.file.not.found=Archivo JSP [{0}] no encontrado +jsp.error.flush=Excepción sucedida al vaciar los datos +jsp.error.fragmentwithtype=No puede especificar ambos atributos ''fragment'' y ''type''. Si está presente ''fragment'', ''type'' se pone como ''{0}'' +jsp.error.function.classnotfound=La clase [{0}] especificada en el TLD para la función [{1}] no se puede hallar: [{2}] +jsp.error.include.exception=No se puede incluir [{0}] +jsp.error.include.tag=Tag jsp:include no válido +jsp.error.internal.filenotfound=Error Interno: Archivo [{0}] no hallado +jsp.error.invalid.attribute=[{0}]: Atributo incorrecto, [{1}] +jsp.error.invalid.bean=El valor el atributo de clsae useBean [{0}] es inválido. +jsp.error.invalid.directive=Directiva no válida +jsp.error.invalid.expression=[{0}] contiene expresiones incorrectas: [{1}] +jsp.error.invalid.implicit=TLD implícito inválido para fichero de marca en [{0}] +jsp.error.invalid.implicit.version=Versión inválida de JSP definida en TLD implícito para fichero de marca en [{0}] +jsp.error.invalid.scope=Valor ilegal de atributo ''scope'': [{0}] (debe de ser uno de "page", "request", "session", o "application") +jsp.error.invalid.tagdir=El directorio de archivo Tag [{0}] no comienza con "/WEB-INF/tags" +jsp.error.invalid.version=Versión inválida de JSP definida para fichero de marca en [{0}] +jsp.error.ise_on_clear=Es ilegal usar clear() cuando el tamaño del buffer es cero +jsp.error.java.line.number=Ha tenido lugar un error en la línea: [{0}] en el fichero java generado: [{1}] +jsp.error.javac=Excepción de Javac +jsp.error.javac.env=Entorno +jsp.error.jspbody.emptybody.only=El tag [{0}] sólo puede tener jsp:attribute en su cuerpo. +jsp.error.jspbody.invalidUse=jsp:body debe de ser el subelemento de una acción estándar o de cliente +jsp.error.jspbody.required=Se debe de usar jsp:body para especificar cuerpo tag para [{0}] si se usa jsp:attribute. +jsp.error.jspc.missingTarget=Falta target: Debe de especificar -webapp o -uriroot o una o más páginas JSP +jsp.error.jspc.no_uriroot=No se ha especificado uriroot y no puede ser localizado en los archivos JSP especificados +jsp.error.jspc.uriroot_not_dir=La opción -uriroot debe de especificar un directorio ya existente +jsp.error.jspelement.missing.name=Falta atributo obligatorio XML-style 'name' +jsp.error.jspoutput.conflict=<jsp:output>: ilegal tener ocurrencias múltiples de [{0}] con diferentes valores (viejo: [{1}], nuevo: [{2}]) +jsp.error.jspoutput.doctypenamesystem=<jsp:output>: atributos 'doctype-root-element' y 'doctype-system' deben de aparecer juntos +jsp.error.jspoutput.doctypepublicsystem=<jsp:output>: atributo 'doctype-system' debe de aparecer si aparece atributo 'doctype-public' +jsp.error.jspoutput.invalidUse=<jsp:output> no se debe de usar en sintáxis estándar +jsp.error.jspoutput.nonemptybody=<jsp:output> no debe de tener un cuerpo +jsp.error.jsproot.version.invalid=Número incorrecto de versión: [{0}], debe de ser "1.2" o "2.0" o "2.1" o "2.2" o "2.3" o "3.0" o "3.1" +jsp.error.jsptext.badcontent='<', cuando aparece en el cuerpo de <jsp:text>, debe de estar encapsulado dentro de un CDATA +jsp.error.lastModified=No puedo determinar la última fecha de modificación para el fichero [{0}] +jsp.error.library.invalid=La página JSP es incorrecta de acuerdo a la biblioteca [{0}]: [{1}] +jsp.error.literal_with_void=Se especificó un valor literal para el atributo [{0}] que está definido como un método diferido con un tipo nulo de retorno. JSP.2.3.4 no permite valores de literal en este caso +jsp.error.loadclass.taghandler=No se puede cargar la clase [{0}] +jsp.error.location=línea: [{0}], columna: [{1}] +jsp.error.mandatory.attribute=[{0}]: Falta atributo obligatorio [{1}] +jsp.error.missing.tagInfo=El objeto TagInfo para [{0}] falta del TLD +jsp.error.missing_attribute=De acuerdo con el TLD el atributo [{0}] es obligatorio para el tag [{1}] +jsp.error.missing_var_or_varReader=Falta atributo 'var' o 'varReader' +jsp.error.namedAttribute.invalidUse=jsp:attribute debe de ser el subelemento de una acción estándar o de cliente +jsp.error.needAlternateJavaEncoding=La codificación java por defecto [{0}] es incorrecta en tu plataforma java. Se puede especificar una alternativa vía parámetro ''javaEncoding'' de JspServlet. +jsp.error.nested.jspattribute=Una acción estándar jsp:attribute no puede estar anidada dentro de otra acción estándar jsp:attribute +jsp.error.nested.jspbody=Una acción estándar jsp:body no puede estar anidada dentro de otra acción estándar jsp:body o jsp:attribute +jsp.error.nested_jsproot=<jsp:root> anidado +jsp.error.no.more.content=Alcanzado fin de contenido mietras se requería más análisis: ¿error de anidamiento de tag? +jsp.error.no.scratch.dir=El motor JSP no tiene configurado un directorio de trabajo.\n\ +\ Añada "jsp.initparams=scratchdir=" \n\ +\ en el fichero servlets.properties para este contexto. +jsp.error.no.scriptlets=Los elementos de Scripting (<%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) no están permitidos aquí. +jsp.error.noFunction=La función [{0}] no puede ser localizada mediante el prefijo especificado +jsp.error.noFunctionMethod=El método [{0}] para la función [{1}] no se pudo hallar en la clase [{2}] +jsp.error.non_null_tei_and_var_subelems=Tag [{0}] tiene uno o más subelementos variable y una clase TagExtraInfo que devuelve una o más VariableInfo +jsp.error.not.in.template=[{0}] no permitido en una plantilla cuerpo de texto. +jsp.error.outputfolder=No hay carpeta de salida +jsp.error.overflow=Error:Buffer de JSP desbordado +jsp.error.page.conflict.autoflush=Directiva Page: es ilegal tener múltiples ocurrencias de ''autoFlush'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.buffer=Directiva Page: es ilegal tener múltiples ocurrencias de ''buffer'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.contenttype=Directiva Page: es ilegal tener múltiples ocurrencias de ''contentType'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.deferredsyntaxallowedasliteral=Directiva de página: es ilegal tener múltiples ocurrencias de ''deferredSyntaxAllowedAsLiteral'' con diferentes valores (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.errorpage=Directiva Page: es ilegal tener múltiples ocurrencias de ''errorPage'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.extends=Directiva Page: es ilegal tener múltiples ocurrencias de ''extends'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.info=Directiva Page: es ilegal tener múltiples ocurrencias de ''info'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.iselignored=Directiva Page: es ilegal tener múltiples ocurrencias de ''isELIgnored'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.iserrorpage=Directiva Page: es ilegal tener múltiples ocurrencias de ''isErrorPage'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.isthreadsafe=Directiva Page: es ilegal tener múltiples ocurrencias de ''isThreadSafe'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.language=Directiva Page: es ilegal tener múltiples ocurrencias de ''language'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.session=Directiva Page: es ilegal tener múltiples ocurrencias de ''session'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.conflict.trimdirectivewhitespaces=Directiva de página: es ilegal tener múltiples ocurrencias de ''trimDirectivewhitespaces'' con diferentes valores (viejo: [{0}], nuevo: [{1}]) +jsp.error.page.invalid.buffer=Directiva Page: valor incorrecto para búfer +jsp.error.page.invalid.deferredsyntaxallowedasliteral=Directiva de página: valor inválido para deferredSyntaxAllowedAsLiteral +jsp.error.page.invalid.import=Directiva de página: valor inválido para importar +jsp.error.page.invalid.iselignored=Directiva Page: valor inválido para isELIgnored +jsp.error.page.invalid.iserrorpage==Directiva Page: valor incorrecto para isErrorPage +jsp.error.page.invalid.isthreadsafe==Directiva Page: valor incorrecto para isThreadSafe +jsp.error.page.invalid.session=Directiva Page: valor incorrecto para session +jsp.error.page.invalid.trimdirectivewhitespaces=Directiva de página: valor inválido para trimDirectiveWhitespaces +jsp.error.page.language.nonjava=Directiva Page: atributo language incorrecto +jsp.error.page.multi.pageencoding=La directiva Page no debe de tener múltiples ocurrencias de pageencoding +jsp.error.page.noSession=No puedo acceder al ámbito de sesión en una página que no participa en una sesión +jsp.error.param.invalidUse=La acción jsp:param no debe de ser usada fuera de los elementos jsp:include, jsp:forward o jsp:params +jsp.error.paramexpected=El tag "param" era esperado con los atributos "name" y "value" después del tag "params". +jsp.error.params.emptyBody=jsp:params debe de contener al menos un jsp:param anidado +jsp.error.params.invalidUse=jsp:params debe de ser un hijo directo de jsp:plugin +jsp.error.parse.error.in.TLD=Error de análisis en el descriptor de biblioteca de tags: [{0}] +jsp.error.parse.xml=Error de análisis XML en archivo [{0}] +jsp.error.parse.xml.line=Error de análisis XML en archivo [{0}]: (línea [{1}], col [{2}]) +jsp.error.parse.xml.scripting.invalid.body=El cuerpo de elemento [{0}] no debe de contener elementos XML +jsp.error.plugin.badtype=Valor ilegal para atributo 'type' en jsp:plugin: debe de ser 'bean' o 'applet' +jsp.error.plugin.nocode=Código no declarado en jsp:plugin +jsp.error.plugin.notype=Tipo no declarado en jsp:plugin +jsp.error.prefix.refined=Intento de redefinir el prefijo [{0}] por [{1}], cuando ya estaba definido como [{2}] en el ámbito en curso. +jsp.error.prefix.use_before_dcl=El prefijo [{0}] especificado en esta directiva de marca ha sido usado previamente mediante un fichero de acción [{1}] línea [{2}]. +jsp.error.prolog_config_encoding_mismatch=El Page-encoding especificado en XML prolog [{0}] difiere del especificado en jsp-property-group [{1}] +jsp.error.prolog_pagedir_encoding_mismatch=El Page-encoding especificado en XML prolog [{0}] difiere del especificado en la directiva page [{1}] +jsp.error.quotes.unterminated=Comillas no terminadas +jsp.error.scripting.variable.missing_name=Imposible determinar nombre de variable de scripting desde atributo [{0}] +jsp.error.signature.classnotfound=La clase [{0}] especificada en la firma del método en el TLD para la función [{1}] no se puede hallar. [{2}] +jsp.error.simpletag.badbodycontent=El TLD para la clase [{0}] especifica un body-content es incorrecto (JSP) para un SimpleTag. +jsp.error.single.line.number=Ha tenido lugar un error en la línea: [{0}] en el archivo jsp: [{1}] +jsp.error.stream.close.failed=No pude cerrar el flujo +jsp.error.stream.closed=Stream cerrado +jsp.error.tag.conflict.attr=Directiva Tag: es ilegal tener múltiples ocurrencias del atributo [{0}] con valores distintos (viejo: [{1}], nuevo: [{2}]) +jsp.error.tag.conflict.deferredsyntaxallowedasliteral=Directiva de marca: es ilegal tener múltiples ocurrencias de ''deferredSyntaxAllowedAsLiteral'' con diferentes valores (viejo: [{0}], nuevo: [{1}]) +jsp.error.tag.conflict.iselignored=Directiva Tag: es ilegal tener múltiples ocurrencias de ''isELIgnored'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.tag.conflict.language=Directiva Tag: es ilegal tener múltiples ocurrencias de ''language'' con valores distintos (viejo: [{0}], nuevo: [{1}]) +jsp.error.tag.conflict.trimdirectivewhitespaces=Directiva de marca: es ilegal tener múltiples ocurrencias de ''trimDirectivewhitespaces'' con diferentes valores (viejo: [{0}], nuevo: [{1}]) +jsp.error.tag.invalid.deferredsyntaxallowedasliteral=Directiva de marca: valor inválido para deferredSyntaxAllowedAsLiteral +jsp.error.tag.invalid.iselignored=Directiva Tag: valor incorrecto para isELIgnored +jsp.error.tag.invalid.trimdirectivewhitespaces=Directiva de marca: valor inválido para trimDirectiveWhitespaces +jsp.error.tag.language.nonjava=Directiva Tag: atributo language incorrecto +jsp.error.tag.multi.pageencoding=La directiva Tag no debe de tener múltiples ocurrencias de pageencoding +jsp.error.tagdirective.badbodycontent=body-content incorrecto [{0}] en directiva tag +jsp.error.tagfile.badSuffix=Falta sufijo ".tag" en trayectoria de archivo de tag [{0}] +jsp.error.tagfile.illegalPath=Trayectoria de archivo de tag: [{0}], debe de comenzar con "/WEB-INF/tags" o "/META-INF/tags" +jsp.error.tagfile.nameFrom.badAttribute=La directiva attribute (declarada en la línea [{1}] y cuyo nombre de atributo es [{0}], el valor de este atributo name-from-attribute) debe de ser del tipo java.lang.String, es "requerido" y no "rtexprvalue". +jsp.error.tagfile.nameFrom.noAttribute=No puedo hallar una directiva attribute con un atributo name con un valor [{0}], el valor de este atributo name-from-attribute. +jsp.error.tagfile.nameNotUnique=El valor de [{0}] y el valor de [{1}] en la línea [{2}] son el mismo. +jsp.error.taglibDirective.absUriCannotBeResolved=La uri absoluta: [{0}] no puede resolverse o en web.xml o el los archivos jar desplegados con esta aplicación +jsp.error.taglibDirective.both_uri_and_tagdir=Se han especificado ambos atributos 'uri' y 'tagdir' +jsp.error.taglibDirective.missing.location=No se ha especificado ni el atributo 'uri' ni el 'tagdir' +jsp.error.taglibDirective.uriInvalid=La URI proveida por la etiqueta librería [{0}] no es una URI válida +jsp.error.tei.invalid.attributes=Mensajes de error de validación desde TagExtraInfo para [{0}] +jsp.error.teiclass.instantiation=No se puede cargar la clase TagExtraInfo llamada: [{0}] +jsp.error.text.has_subelement=<jsp:text> no debe de tener subelementos +jsp.error.tld.fn.duplicate.name=Nombre duplicado de función [{0}] en biblioteca de tag [{1}] +jsp.error.tld.fn.invalid.signature=Sintáxis incorrecta para firma de función en TLD. Biblioteca de Tag: [{0}], Función: [{1}] +jsp.error.tld.mandatory.element.missing=El elemento TLD [{0}] es obligatorio pero falta o está vacío en TLD [{1}] +jsp.error.tld.missing_jar=Falta recurso JAR [{0}] conteniendo TLD +jsp.error.tld.unable_to_get_jar=Imposible obtener recurso JAR [{0}] conteniendo TLD: [{1}] +jsp.error.tlv.invalid.page=Mensajes de error de validación desde TagLibraryValidator para [{0}] in [{1}] +jsp.error.tlvclass.instantiation=No pude cargar o instanciar clase TagLibraryValidator: [{0}] +jsp.error.unable.compile=No se puede compilar la clase para JSP +jsp.error.unable.load=No se puede cargar la clase para JSP +jsp.error.unable.to_find_method=No se puede encontrar el método de escritura para el atributo: [{0}] +jsp.error.unavailable=JSP ha sido marcado como no disponible +jsp.error.unbalanced.endtag=El tgag final "</{0}" está desequilibrado +jsp.error.undeclared_namespace=Se ha encontrado una etiqueta con espacio de nombre [{0}] sin declarar +jsp.error.unknown_attribute_type=Tipo de atributo desconocido [{1}] para atributo [{0}]. +jsp.error.unsupported.encoding=Codificación no soportada: [{0}] +jsp.error.unterminated=Tag [{0}] no terminado +jsp.error.usebean.duplicate=useBean: Nombre de bean duplicado: [{0}] +jsp.error.usebean.noSession=Es ilegal para useBean el usar ámbito de sesión cuando la página JSP declara (vía directiva de página) que no participa en sesiones +jsp.error.var_and_varReader=Sólo se puede especificar uno de 'var' o 'varReader' +jsp.error.variable.alias=Ambos atributos o ninguno de name-from-attribute y alias pueden ser especificados en una directiva variable +jsp.error.variable.both.name=No se puede especificar ambos atributos name-given o name-from-attribute en una directiva variable +jsp.error.variable.either.name=O el atributo name-given o name-from-attribute deben de ser especificados en una directiva variable +jsp.error.xml.badStandardAction=Acción estándar incorrecta: [{0}] +jsp.error.xml.bad_tag=No se ha definido el tag [{0}] en la biblioteca tag asociada con uri [{1}] +jsp.exception=Ha sucedido una excepción al procesar la página JSP [{0}] en línea [{1}] +jsp.info.ignoreSetting=Valor de configuración ignorado para [{0}] de [{1}] debido a que SecurityManager estaba habilitado +jsp.message.dont.modify.servlets=IMPORTANTE: No modifique los servlets generados +jsp.message.jsp_added=Añadiendo JSP para ruta [{0}] a cola de contexto [{1}] +jsp.message.jsp_queue_created=Creada cola jsp con tamaño [{0}] para el contexto [{1}] +jsp.message.jsp_queue_update=Actuallizando JSP para ruta [{0}] en cola de contexto [{1}] +jsp.message.jsp_removed_excess=Quitando exceso de JSP para ruta [{0}] desde cola de contexto [{1}] +jsp.message.jsp_removed_idle=Quitando JSP ocioso para ruta [{0}] en contexto [{1}] tras [{2}] segundos"); +jsp.message.jsp_unload_check=Revisando JSPs para descaga en contexto [{0}], contador JSP: [{1}] tamalo de cola: [{2}] +jsp.message.parent_class_loader_is=El cargador de clases es: [{0}] +jsp.message.scratch.dir.is=El directorio de trabajo para el motor JSP es: [{0}] +jsp.tldCache.noTldInJar=No se han hallado ficheros TLD en [{0}]. Considera añadir el JAR a la propiedad tomcat.util.scan.StandardJarScanFilter.jarsToSkip en el fichero CATALINA_BASE/conf/catalina.propeperties. +jsp.tldCache.noTldSummary=Al menos un JAR, que se ha explorado buscando TLDs, aún no contenía TLDs. Activar historial de depuración para este historiador para una completa lista de los JARs que fueron explorados y de los que nos se halló TLDs. Saltarse JARs no necesarios durante la exploración puede dar lugar a una mejora de tiempo significativa en el arranque y compilación de JSP . +jsp.tldCache.tldInDir=Se encontraron archivos TLD en el directorio [{0}].\n +jsp.warning.bad.urlpattern.propertygroup=Valor malo [{0}] en el subelemento url-pattern en web.xml +jsp.warning.checkInterval=Aviso: valor incorrecto para el initParam checkInterval. Se usará el valor por defecto de "300" segundos +jsp.warning.classDebugInfo=Aviso: valor incorrecto para el initParam classdebuginfo. Se usará el valor por defecto de "false" +jsp.warning.compiler.classfile.delete.fail=No pude borrar el fichero generado de clase [{0}] +jsp.warning.compiler.classfile.delete.fail.unknown=No pude borrar los ficheros generados de clase +jsp.warning.compiler.javafile.delete.fail=No pude borrar el fichero generado de Java [{0}] +jsp.warning.development=Aviso: valor incorrecto para el initParam development. Se usará el valor por defecto de "true" +jsp.warning.displaySourceFragment=Aviso: valor incorrecto para el initParam displaySourceFragment. Se usará el valor por defecto de "verdadero" +jsp.warning.dumpSmap=Aviso: valor incorrecto para el initParam dumpSmap. Se usará el valor por defecto de "false" +jsp.warning.enablePooling=Aviso: valor incorrecto para initParam enablePooling. Se usará el valor por defecto de "true" +jsp.warning.fork=Aviso: valor incorrecto para el initParam fork. Se usará el valor por defecto de "true" +jsp.warning.genchararray=Aviso: valor incorrecto para el initParam genStringAsCharArray. Se usará el valor por defecto de "false" +jsp.warning.jspIdleTimeout=Aviso: Valor inválido para el initParam jspIdleTimeout. Usaré el valor por defecto de "-1" +jsp.warning.keepgen=Aviso: valor incorrecto para el initParam keepgen. Se usará el valor por defecto de "false" +jsp.warning.mappedFile=Aviso: valor incorrecto para el initParam mappedFile. Se usará el valor por defecto de "false" +jsp.warning.maxLoadedJsps=Aviso: Valor inválido para el initParam maxLoadedJsps. Usaré el valor por defecto de "-1" +jsp.warning.modificationTestInterval=Aviso: valor incorrecto para el initParam modificationTestInterval. Se usará el valor por defecto de "4" segundos +jsp.warning.noJarScanner=Aviso: No se ha puesto org.apache.tomcat.JarScanner en ServletContext. Volviendo a la implementación por defecto de JarScanner. +jsp.warning.recompileOnFail=Aviso: Valor inválido para el initParam recompileOnFail. Usaré el valor por defecto de "falso "false" +jsp.warning.suppressSmap=Aviso: valor incorrecto para el initParam suppressSmap. Se usará el valor por defecto de "false" +jsp.warning.xpoweredBy=Aviso: valor incorrecto para el initParam xpoweredBy. Se usará el valor por defecto de "false" + +jspc.delete.fail=No pude borrar el fichero [{0}] +jspc.error.fileDoesNotExist=El archivo [{0}] utilizado como argumento no existe. +jspc.error.generalException=ERROR-el archivo [{0}] ha generado la excepción general siguiente: +jspc.implicit.uriRoot=uriRoot implicitamente puesto a [{0}] +jspc.usage=Uso: jspc [--] \n\ +donde son:\n\ +\ -webapp Un directorio conteniendo una web-app. Todas las\n\ +\ páginas jsp serán compiladas recursivamente\n\ +o cualquier número de\n\ +\ Un Archivo para ser interpretado como una página jsp\n\ +y donde incluyen:\n\ +\ -help Muestra este mensaje de ayuda\n\ +\ -v Modo detallado\n\ +\ -d Directorio de salida\n\ +\ -l Muestra el nombre de la página JSP al ocurrir un fallo\n\ +\ -s Muestra el nombre de la página JSP al tener éxito\n\ +\ -p Nombre del package objetivo\n\ +\ (por defecto org.apache.jsp)\n\ +\ -c Nombre de la clase objetivo\n\ +\ (sólo se aplica a la primera página JSP)\n\ +\ -mapped Genera llamadas separadas a write() para cada línea de\n\ +\ HTML en el JSP\n\ +\ -die[#] Genera un código de retorno de error (#) en errores\n\ +\ fatales. (por defecto 1).\n\ +\ -uribase El directorio uri de donde deben de partir las\n\ +\ compilaciones. (por defecto "/")\n\ +\ -uriroot Igual que -webapp\n\ +\ -compile Compila los servlets generados\n\ +\ -failFast Stop on first compile error\n\ +\ -webinc Crea unos mapeos parciales de servlet en el archivo\n\ +\ -webxml Crea un web.xml completo en el archivo.\n\ +\ -webxmlencoding Set the encoding charset used to read and write the web.xml\n\ +\ file (default is UTF-8)\n\ +\ -addwebxmlmappings Merge generated web.xml fragment into the web.xml file of the\n\ +\ web-app, whose JSP pages we are processing\n\ +\ -ieplugin Java Plugin classid para Internet Explorer\n\ +\ -classpath Pasa por alto la propiedad de sistema java.class.path\n\ +\ -xpoweredBy Añade cabecera de respuesta X-Powered-By\n\ +\ -trimSpaces [single] Remove template text that consists entirely of whitespace\n\ +\ (if "single", replace such template text with a single space)\n\ +\ -javaEncoding Set the encoding charset for Java classes (default UTF-8)\n\ +\ -source Set the -source argument to the compiler (default 1.8)\n\ +\ -target Set the -target argument to the compiler (default 1.8)\n +jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webinc.insertEnd= +jspc.webinc.insertStart= +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +org.apache.jasper.compiler.TldCache.servletContextNull=El contenido proporcionado para ServletContext tiene un valor nulo diff --git a/java/org/apache/jasper/resources/LocalStrings_fr.properties b/java/org/apache/jasper/resources/LocalStrings_fr.properties new file mode 100644 index 0000000..dd0b295 --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_fr.properties @@ -0,0 +1,435 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=D''après la TLD, le tag [{0}] doit être vide, mais ne l''est pas + +jsp.compiled=Compilé [{0}] en {1}ms +jsp.engine.info=Moteur Jasper JSP {0} +jsp.error.URLMustStartWithSlash=Le chemin [{0}] doit commencer par un caractère slash +jsp.error.action.isnottagfile=L''action [{0}] ne peut être utilisée que dans un fichier tag +jsp.error.action.istagfile=L''action [{0}] ne peut être utilisée dans un fichier tag +jsp.error.attempt_to_clear_flushed_buffer=Erreur : Tentative d'effacement d'un tampon qui a déjà été vidangé (flush) +jsp.error.attr.quoted=La valeur de l'attribut doit être entre guillemets +jsp.error.attribute.custom.non_rt_with_expr=D''après la TLD, l''attribut [{0}] n''accepte aucune expression +jsp.error.attribute.deferredmix=Impossible d'utiliser des expressions EL ${} et #{} dans la même valeur d'attribut +jsp.error.attribute.duplicate=Les noms qualifiés d’attributs doivent être uniques au sein d'un élément +jsp.error.attribute.invalidPrefix=Le préfixe d''attribut [{0}] ne correspond à aucune librairie de tags importée +jsp.error.attribute.noequal=Symbole égal (equal) attendu +jsp.error.attribute.noescape=La valeur d''attribut [{0}] est entre guillemets avec [{1}] qui doit être échappé quand il est utilisé dans la valeur +jsp.error.attribute.noquote=Symbole guillemet (quote) attendu +jsp.error.attribute.nowhitespace=La spécification JSP requiert un caractère d'espacement devant le nom d'un attribut +jsp.error.attribute.null_name=Le nom d'attribut est null +jsp.error.attribute.standard.non_rt_with_expr=L''attribut [{0}] de l''action standard [{1}] n''accepte pas d''expressions +jsp.error.attribute.unterminated=L''attribut pour [{0}] n''est pas correctement terminé +jsp.error.backgroundCompilationFailed=La compilation en arrière plan a échoué +jsp.error.bad.scratch.dir=Le paramètre "scratchDir" que vous avez spécifié : [{0}] est inutilisable. +jsp.error.badStandardAction=L'action n'est pas reconnue comme une action standard. +jsp.error.bad_attribute=L''attribut [{0}] est incorrect pour le tag [{1}] d''après la TLD indiquée +jsp.error.bad_tag=Aucun tag [{0}] dans la bibliothèque de tag importée avec le préfixe [{1}] +jsp.error.beans.nomethod=Impossible de trouver une méthode pour lire la propriété [{0}] dans le bean de type [{1}] +jsp.error.beans.nomethod.setproperty=Impossible de trouver une méthode pour mettre à jour la propriété [{0}] de type [{1}]dans le bean de type [{2}] +jsp.error.beans.noproperty==Impossible de trouver de l''information sur la propriété [{0}] dans le bean de type [{1}] +jsp.error.beans.nullbean=Tentative d'opération bean sur un objet nul. +jsp.error.beans.property.conversion=Impossible de convertir la string [{0}] vers la classe [{1}] pour l''attribut [{2}] : [{3}] +jsp.error.beans.propertyeditor.notregistered=L'éditeur de propriétés n'est pas enregistré avec le PropertyEditorManager +jsp.error.beans.setproperty.noindexset=Impossible de renseigner la propriété indéxée +jsp.error.bug48498=Impossible d'afficher un extrait du JSP, ce qui peut être causé par un problème du parser XML (voir le bug 48498 de Tomcat) +jsp.error.cannotAddResolver=Impossible d'appeler addELResolver après qu'une première requête ait été effectuée +jsp.error.classname=Impossible de déterminer le nom de classe d'après le fichier .class +jsp.error.coerce_to_type=Impossible de convertir la valeur [{2}] de l''attribut [{0}] vers le type [{1}] +jsp.error.compilation=Erreur de compilation du fichier : [{0}] [{1}] +jsp.error.compilation.dependent=Erreur de chargement de la classe [{0}] +jsp.error.compilation.jdt=Erreur de compilation +jsp.error.compilation.jdtProblemError=Erreur lors du traitement de la liste des problèmes renvoyée par JDT +jsp.error.compilation.source=Erreur lors du chargement du fichier source [{0}] +jsp.error.compiler=Aucun compilateur Java disponible +jsp.error.compiler.config=Aucun compilateur Java disponible pour les options de configuration compilerClassName : [{0}] et compiler : [{1}] +jsp.error.compiler.missingResource=Problème d'accès à la ressource, elle sera traitée comme périmée +jsp.error.config_pagedir_encoding_mismatch=L''encode de page (Page-encoding) indiqué dans le jsp-property-group [{0}] est différent de celui indiqué dans la directive de page [{1}] +jsp.error.corresponding.servlet=Erreur de servlet générée : +jsp.error.could.not.add.taglibraries=Impossible d'ajouter une ou plusieurs bibliothèques de tag. +jsp.error.data.file.processing=Erreur durant le traitement du fichier [{0}] +jsp.error.data.file.read=Erreur lors de la lecture du fichier [{0}] +jsp.error.data.file.write=Erreur lors de l'écriture du fichier de données +jsp.error.deferredmethodandvalue='deferredValue' et 'deferredMethod' ne peuvent être toutes deux 'true' +jsp.error.deferredmethodsignaturewithoutdeferredmethod=Impossible de sécifier une signature de méthode si 'deferredMethod' n'est pas 'true' +jsp.error.deferredvaluetypewithoutdeferredvalue=Impossible de spécifier un value type si 'deferredValue' n'est pas 'true' +jsp.error.directive.isnottagfile=La directive [{0}] ne peut être utilisée que dans un fichier tag +jsp.error.directive.istagfile=La directive [{0}] ne peut être utilisée dans un fichier tag +jsp.error.duplicate.name.jspattribute=L''attribut [{0}] indiqué dans l''action standard ou spécifique (custom) apparait aussi comme valeur de l''attribut de nom dans le jsp:attribute inclus +jsp.error.duplicateqname=Un attribut avec un nom qualifié [{0}] en double a été trouvé, ils doivent être uniques au sein d''un élément +jsp.error.dynamic.attributes.not.implemented=Le tag [{0}] indique qu''il accepte des attributs dynamics mais n''implémente pas l''interface requise +jsp.error.el.parse=[{0}] : [{1}] +jsp.error.el.template.deferred=#{...} n'est pas admis dans le texte d'un modèle (template) +jsp.error.el_interpreter_class.instantiation=Impossible de charger ou d''instancier la classe ELInterpreter [{0}] +jsp.error.fallback.invalidUse=jsp:fallback doit être un enfant direct de jsp:plugin +jsp.error.file.already.registered=Inclusion récursive du fichier [{0}] +jsp.error.file.cannot.read=Impossible de lire le fichier : [{0}] +jsp.error.file.close=Erreur lors de la fermeture du lecteur +jsp.error.file.not.found=Le fichier [{0}] n''a pas été trouvé +jsp.error.flush=Une exception s'est produite lors de l'envoi des données +jsp.error.fragmentwithtype=On ne peut indiquer à la fois les attributs ''fragment'' et ''type''. Si ''fragment'' est présent, ''type'' est fixé comme ''{0}'' +jsp.error.function.classnotfound=La classe [{0}] spécifiée dans la TLD pour la fonction [{1}] n''a pas été trouvée : [{2}] +jsp.error.include.exception=Impossible d''inclure (include) [{0}] +jsp.error.include.tag=Tag jsp:include incorrect +jsp.error.internal.filenotfound=Erreur interne : Fichier [{0}] introuvable +jsp.error.invalid.attribute=[{0}] : Attribut incorrect : [{1}] +jsp.error.invalid.bean=La valeur [{0}] de l''attribut de classe useBean est invalide +jsp.error.invalid.directive=Directive incorrecte +jsp.error.invalid.expression=[{0}] contient d''incorrecte(s) expression(s) : [{1}] +jsp.error.invalid.implicit=Le TLD implicite est invalide pour le fichier tag [{0}] +jsp.error.invalid.implicit.version=La version JSP déclarée dans le TLD implicite pour le fichier de tag à [{0}] est invalide +jsp.error.invalid.name=Le fichier [{0}] utilise le nom [{1}] dans jsp:getProperty pour un bean qui n''a pas été déclaré conformément à JSP.5.3 +jsp.error.invalid.scope=La valeur de l''attribut "scope" est invalide : [{0}] (elle doit être "page", "request", "session" ou "application") +jsp.error.invalid.tagdir=Le répertoire du fichier Tag [{0}] ne commence pas par "/WEB-INF/tags" +jsp.error.invalid.varscope=L''étendue [{0}] de la variable est invalide +jsp.error.invalid.version=Version JSP invalide pour le fichier tag [{0}] +jsp.error.ise_on_clear=Il est interdit d'utiliser clear() quand la taille de tampon== 0 +jsp.error.java.line.number=Une erreur s''est produite à la ligne : [{0}] dans le fichier Java généré : [{1}] +jsp.error.javac=Exception javac +jsp.error.javac.env=Environnement : +jsp.error.jspbody.emptybody.only=Le tag [{0}] ne peut avoir que jsp:attribute dans son corps. +jsp.error.jspbody.invalidUse=Le jsp:body doit être un sous élément d'une action standard ou personnalisée +jsp.error.jspbody.required=Doit utiliser jsp:body pour indiqué le corps de tag body de [{0}] si jsp:attribute est utilisé. +jsp.error.jspc.missingTarget=Une cible manque, il faut spécifier -webapp, -uriroot, ou une ou plusieurs pages JSP +jsp.error.jspc.no_uriroot=uriroot n'est pas spécifié et ne peut être trouvé avec le(s) fichier(s) JSP spécifié(s) +jsp.error.jspc.uriroot_not_dir=L'option -uriroot doit indiquer un répertoire déjà existant +jsp.error.jspelement.missing.name=L'attribut obligatoire 'name' est absent de jsp:element +jsp.error.jspoutput.conflict=<jsp:output> : il est illégal d''avoir plusieurs occurrences de [{0}] avec des valeurs différentes (ancienne : [{0}], nouvelle [{1}]) +jsp.error.jspoutput.doctypenamesystem=<jsp:output> : les attributs "doctype-root-element" et "doctype-system" doivent apparaître conjointement +jsp.error.jspoutput.doctypepublicsystem=L'attribut <jsp:output> : 'doctype-system' doit être présent lorsque l'attribut 'doctype-public' l'est +jsp.error.jspoutput.invalidUse=<jsp:output> ne doit pas être utilisé en syntaxe standard +jsp.error.jspoutput.nonemptybody=<jsp:output> ne doit pas avoir de corps +jsp.error.jsproot.version.invalid=Le numéro de version [{0}] est invalide, il devrait être "1.2", "2.0", "2.1", "2.2", "2.3", "3.0" ou "3.1" +jsp.error.jsptext.badcontent=Quand '<' apparaît dans le corps d'un <jsp:text>, il doit être encapsulé dans un CDATA\n +jsp.error.lastModified=Impossible de déterminer la date de dernière modification pour le fichier [{0}] +jsp.error.library.invalid=La page JSP page est incorrecte d''après la bibliothèque [{0}] : [{1}] +jsp.error.literal_with_void=Une valeur littérale a été spécifié pour l''attribut [{0}] qui est défini comme étant une méthode différée ne retournant pas d''objet, JSP.2.3.4 ne permet pas de valeur littérale dans ce cas +jsp.error.loadclass.taghandler=Impossible de charger la classe [{0}] +jsp.error.location=ligne : [{0}], colonne : [{1}] +jsp.error.mandatory.attribute=[{0}] : L''attribut obligatoire [{1}] est manquant +jsp.error.missing.tagInfo=L''objet TagInfo de [{0}] est absent de la TLD +jsp.error.missing_attribute=D''après le TLD l''attribut [{0}] est obligatoire pour le tag [{1}] +jsp.error.missing_var_or_varReader=L'attribut "var" ou "varReader" est manquant +jsp.error.namedAttribute.invalidUse=Le jsp:attribute doit être un sous élément d'une action standard ou personnalisée +jsp.error.needAlternateJavaEncoding=L''encodage java par défaut [{0}] est incorrect sur votre environnement java. Une alternative peut être indiquée via le paramêtre ''javaEncoding'' de la JspServlet. +jsp.error.negativeBufferSize=La taille du tampon est négative +jsp.error.negativeParameter=Le paramètre [{0}] ne doit pas être négatif +jsp.error.nested.jspattribute=Une action standard jsp:attribute ne peut pas être nichée dans une autre +jsp.error.nested.jspbody=Une action standard jsp:body ne peut être incluse dans une autre action standard jsp:body ou jsp:attribute +jsp.error.nested_jsproot=<jsp:root> imbriqué +jsp.error.no.jsp=Impossible de trouver le fichier JSP [{0}] +jsp.error.no.more.content=Fin de contenu alors que l'évalution n'était pas terminée : erreur de tags imbriqués ? +jsp.error.no.scratch.dir=Le moteur de JSP engine n'est pas configuré avec un répertoire de travail.\n\ +\ Merci d'ajouter "jsp.initparams=scratchdir=" \n\ +\ dans le fichier "servlets.properties" de ce contexte. +jsp.error.no.scriptlets=Les éléments de scripting ( <%!, [--] \n\ +où les fichiers jsp sont soit\n\ +\ -webapp Un répertoire contenant une application web, toutes les pages jsp\n\ +\ seront récursivement évaluées\n\ +ou n'importe quel nombre de :\n\ +\ Un fichier à évaluer comme page jsp\n\ +et où les options sont :\n\ +\ -help Afficher ce message d'aide\n\ +\ -v Mode de débogage\n\ +\ -d Répertoire de sortie\n\ +\ -l Affiche le nom de la page JSP lors d'un échec\n\ +\ -s Affiche le nom la page JSPlors d'un succès\n\ +\ -p Nom du paquet cible (par défaut org.apache.jsp)\n\ +\ -c Nom de classe cible\n\ +\ (s'applique seulement à la première page JSP)\n\ +\ -mapped Génère des appels à write() séparés pour chaque ligne HTML dans la page JSP\n\ +\ -die[#] Génère un code d'erreur de retour (#) en cas d'erreur fatale (par défaut 1)\n\ +\ -uribase L'uri du répertoire auquel la compilation doit être relative\n\ +\ (par défaut "/")\n\ +\ -uriroot Identique à -webapp\n\ +\ -compile Compilation des Servlets générés\n\ +\ -failFast Arrêt à la première erreur de compilation\n\ +\ -webinc Création d'association de Servlet partiel\n\ +\ -webfrg Création d'un fichier web-fragment.xml complet\n\ +\ -webxml Création d'un fichier web.xml complet\n\ +\ -webxmlencoding Fixe l'encodage des caractères à utiliser pour lire et écrire le fichier\n\ +\ web.xml (par défaut UTF-8)\n\ +\ -addwebxmlmappings Fusionne le fragment de web.xml généré dans le web.xml\n\ +\ de l'application web dont les pages JSP sont générées\n\ +\ -ieplugin Le classid du Plugin Java pour Internet Explorer\n\ +\ -classpath Remplace la propriété système java.class.path\n\ +\ -xpoweredBy Ajoute l'en-tête de réponse X-Powered-By\n\ +\ -trimSpaces [single] Enlève le texte de base qui est constitué entièrement d'espaces blancs\n\ +\ (si "single", remplacement par un seul espace)\n\ +\ -javaEncoding Fixe le code de caractères pour les classes Java (par défaut UTF-8)\n\ +\ -source Fixe l'argument -source pour le compilateur (par défaut 1.8)\n\ +\ -target Fixe l'argument -target pour le compilateur (par défaut 1.8)\n\ +\ -threadCount Nombre de threads à utiliser pour la compilation\n\ +\ ("2.0C" veut dire deux threads par cÅ“ur)\n +jspc.warning.tldInWebInfLib=Les fichiers TLDs ne devraient pas être placés dans /WEB-INF/lib +jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.footer=\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webinc.insertEnd= +jspc.webinc.insertStart= +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +jstl.OSAfterWriter=Impossible d'utiliser un flux de sortie si un writer a été utilisé +jstl.urlMustStartWithSlash=Lorsque l'attribut "context" est spécifiédans l'URL d'un tag, à la fois l'"url" et le "context" doivent commencer par un caractère slash +jstl.writerAfterOS=Impossible d'utiliser un writer si un flux de sortie a été utilisé + +org.apache.jasper.compiler.ELParser.invalidQuotesForStringLiteral=Le littéral chaîne [{0}] est invalide, il doit être contenu entre guillemets simples ou doubles +org.apache.jasper.compiler.ELParser.invalidQuoting=L''expression [{0}] n''est pas valide, dans une chaîne entre guillemets seuls [], [''] et ["] peuvent être échappés avec [] +org.apache.jasper.compiler.TldCache.servletContextNull=Le ServletContext fourni est égal à null +org.apache.jasper.servlet.JasperInitializer.onStartup=Initialisation de Jasper pour le contexte [{0}] +org.apache.jasper.servlet.TldScanner.webxmlAdd=Chargement de la TLD pour l''URI [{1}] à partir du chemin de ressource [{0}] +org.apache.jasper.servlet.TldScanner.webxmlFailPathDoesNotExist=Echec du traitement de la TLD du cheming [{0}] avec l''URI [{1}], le chemin spécifié n''existe pas +org.apache.jasper.servlet.TldScanner.webxmlSkip=Le chargement du TLD à l''URI [{1}] pour le chemin de ressource [{0}] ne sera pas effectué car il a déjà été défini dans diff --git a/java/org/apache/jasper/resources/LocalStrings_ja.properties b/java/org/apache/jasper/resources/LocalStrings_ja.properties new file mode 100644 index 0000000..df27979 --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_ja.properties @@ -0,0 +1,433 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=TLDã«ã‚ˆã‚‹ã¨ã€ã‚¿ã‚° [{0}] ã¯ç©ºã§ã‚ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ãŒãã†ãªã£ã¦ã„ã¾ã›ã‚“ + +jsp.compiled=[{0}] ãŒã‚³ãƒ³ãƒ‘イルã•ã‚Œã¾ã—㟠{1}ms +jsp.engine.info=Jasper JSP {0} エンジン +jsp.error.URLMustStartWithSlash=パス [{0}] ã¯ã‚¹ãƒ©ãƒƒã‚·ãƒ¥æ–‡å­—ã§å§‹ã¾ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ +jsp.error.action.isnottagfile=[{0}] アクションã¯ã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«å†…ã§ã®ã¿ä½¿ç”¨ã§ãã¾ã™ +jsp.error.action.istagfile=[{0}] アクションã¯ã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«å†…ã§ä½¿ç”¨ã§ãã¾ã›ã‚“ +jsp.error.attempt_to_clear_flushed_buffer=エラー: æ—¢ã«ãƒ•ãƒ©ãƒƒã‚·ãƒ¥ã•ã‚Œã¦ã„ã‚‹ãƒãƒƒãƒ•ã‚¡ã‚’クリアã—よã†ã¨ã—ã¾ã—㟠+jsp.error.attr.quoted=属性値ã¯å¼•ç”¨ç¬¦ã§å›²ã‚ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.attribute.custom.non_rt_with_expr=TLDã¾ãŸã¯ã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«ä¸­ã®attributeディレクティブã«ã‚ˆã‚‹ã¨ã€å±žæ€§[{0}]ã¯ã„ã‹ãªã‚‹å¼ã‚‚å—ã‘付ã‘ã¾ã›ã‚“ +jsp.error.attribute.deferredmix=属性値㫠${} 㨠#{} ã®ELå¼ã‚’åŒæ™‚ã«å«ã‚ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +jsp.error.attribute.duplicate=属性修飾åã¯ã€è¦ç´ å†…ã§ä¸€æ„ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“ +jsp.error.attribute.invalidPrefix=属性ã®ãƒ—レフィックス [{0}] ã¯ã‚¤ãƒ³ãƒãƒ¼ãƒˆã•ã‚ŒãŸã„ãšã‚Œã®ã‚¿ã‚°ãƒ©ã‚¤ãƒ–ラリã«ã‚‚対応ã—ã¾ã›ã‚“ +jsp.error.attribute.noequal=ç­‰å·è¨˜å·ãŒå¿…è¦ã§ã™ +jsp.error.attribute.noescape=属性値 [{0}] 㯠[{1}] ã§å¼•ç”¨ã•ã‚Œã€å€¤ã®ä¸­ã§ä½¿ç”¨ã™ã‚‹éš›ã¯ã‚¨ã‚¹ã‚±ãƒ¼ãƒ—ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ +jsp.error.attribute.noquote=引用符ãŒå¿…è¦ã§ã™ +jsp.error.attribute.nowhitespace=JSP ã®ä»•æ§˜ã«ã‚ˆã‚Šã€å±žæ€§åã®å‰ã«ç©ºç™½ãŒå¿…è¦ã§ã™ã€‚ +jsp.error.attribute.null_name=空ã®å±žæ€§åã§ã™ +jsp.error.attribute.standard.non_rt_with_expr=[{1}] 標準アクション㮠[{0}] 属性ã¯ã©ã‚“ãªå¼ã‚‚å—ã‘付ã‘ã¾ã›ã‚“ +jsp.error.attribute.unterminated=[{0}] ã®å±žæ€§ãŒæ­£ã—ã終了ã—ã¦ã„ã¾ã›ã‚“ +jsp.error.backgroundCompilationFailed=ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã®ã‚³ãƒ³ãƒ‘イルã«å¤±æ•—ã—ã¾ã—㟠+jsp.error.bad.scratch.dir=ã‚ãªãŸãŒæŒ‡å®šã—ãŸscratchDir: [{0}] ã¯ä½¿ç”¨ã§ãã¾ã›ã‚“ +jsp.error.badStandardAction=無効ãªæ¨™æº–アクションã§ã™ +jsp.error.bad_attribute=TLDã«ã‚ˆã‚‹ã¨ã€ã‚¿ã‚° [{1}] ã®å±žæ€§ [{0}] ã¯ç„¡åŠ¹ã§ã™ +jsp.error.bad_tag=プレフィックス [{1}]ã§ã‚¤ãƒ³ãƒãƒ¼ãƒˆã•ã‚ŒãŸã‚¿ã‚°ãƒ©ã‚¤ãƒ–ラリã«ã¯ã€ã‚¿ã‚° [{0}] ã¯å­˜åœ¨ã—ã¾ã›ã‚“ +jsp.error.beans.nomethod=タイプ [{1}] ã®Bean中ã®å±žæ€§ [{0}] を読ã¿è¾¼ã‚€ãƒ¡ã‚½ãƒƒãƒ‰ã‚’発見ã§ãã¾ã›ã‚“ã§ã—㟠+jsp.error.beans.nomethod.setproperty=タイプ [{2}] ã®Beanã®ã‚¿ã‚¤ãƒ— [{1}] ã®å±žæ€§ [{0}] を書ã込むメソッドを発見ã§ãã¾ã›ã‚“ã§ã—㟠+jsp.error.beans.noproperty=タイプ [{1}] ã®bean中ã®ãƒ—ロパティ [{0}] ã®æƒ…報を発見ã§ãã¾ã›ã‚“ã§ã—㟠+jsp.error.beans.nullbean=nullオブジェクトã«Beanæ“作をãŠã“ãªãŠã†ã¨ã—ã¾ã—㟠+jsp.error.beans.property.conversion=属性 [{2}]: [{3}] ã®æ–‡å­—列 [{0}] をクラス [{1}] ã«å¤‰æ›ã§ãã¾ã›ã‚“ +jsp.error.beans.propertyeditor.notregistered=PropertyEditorManagerã«ç™»éŒ²ã•ã‚Œã¦ã„ãªã„プロパティエディタ +jsp.error.beans.setproperty.noindexset=インデックス付ãã®ãƒ—ロパティを設定ã§ãã¾ã›ã‚“ +jsp.error.bug48498=JSP抽出を表示ã§ãã¾ã›ã‚“。 XMLパーサーã®ãƒã‚°ãŒåŽŸå› ã§ã‚ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ï¼ˆè©³ç´°ã¯ã€Tomcatãƒã‚°48498ã‚’å‚ç…§ã—ã¦ãã ã•ã„)。 +jsp.error.cannotAddResolver=最åˆã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒè¡Œã‚ã‚ŒãŸå¾Œã«addELResolverを呼ã³å‡ºã›ã¾ã›ã‚“。 +jsp.error.classname=.classファイルã‹ã‚‰ã‚¯ãƒ©ã‚¹åを決定ã§ãã¾ã›ã‚“ +jsp.error.coerce_to_type=属性 [{0}] ã®å€¤ [{2}] をタイプ [{1}] ã«å¤‰æ›ã§ãã¾ã›ã‚“。 +jsp.error.compilation=ファイルã®ã‚³ãƒ³ãƒ‘イル中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸï¼š[{0}] [{1}] +jsp.error.compilation.dependent=クラス [{0}] ã®ãƒ­ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—㟠+jsp.error.compilation.jdt=コンパイルエラー +jsp.error.compilation.jdtProblemError=JDTå•é¡Œãƒªã‚¹ãƒˆå‡¦ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +jsp.error.compilation.source=ソースファイル[{0}]ã®èª­ã¿è¾¼ã¿ä¸­ã®ã‚¨ãƒ©ãƒ¼ +jsp.error.compiler=Java コンパイラãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +jsp.error.compiler.config=設定オプション compilerClassName:[{0}]ãŠã‚ˆã³ã‚³ãƒ³ãƒ‘イラ:[{1}]ã®Javaコンパイラã¯åˆ©ç”¨å¯èƒ½ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +jsp.error.compiler.missingResource=リソースã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚å¤ã„ã‚‚ã®ã¨ã—ã¦æ‰±ã„ã¾ã™ +jsp.error.config_pagedir_encoding_mismatch=jsp-property-group中ã«æŒ‡å®šã•ã‚Œã¦ã„ã‚‹Page-encoding [{0}] ãŒpage指示å­ä¸­ã®æŒ‡å®š [{1}] ã¨é•ã„ã¾ã™ +jsp.error.corresponding.servlet=生æˆã•ã‚ŒãŸã‚µãƒ¼ãƒ–レットエラー:\n\ +\n +jsp.error.could.not.add.taglibraries=1ã¤ä»¥ä¸Šã®ã‚¿ã‚°ãƒ©ã‚¤ãƒ–ラリを追加ã§ãã¾ã›ã‚“ +jsp.error.data.file.processing=ファイル[{0}]を処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +jsp.error.data.file.read=ファイル [{0}] を読ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+jsp.error.data.file.write=データファイルを書ãè¾¼ã¿ä¸­ã®ã‚¨ãƒ©ãƒ¼ +jsp.error.deferredmethodandvalue='deferredValue' 㨠'deferredMethod' ã¯åŒæ™‚ã« 'true' ã«ã§ãã¾ã›ã‚“。 +jsp.error.deferredmethodsignaturewithoutdeferredmethod='deferredMethod' ㌠'true' ã§ãªã„å ´åˆã€ãƒ¡ã‚½ãƒƒãƒ‰ã‚·ã‚°ãƒãƒãƒ£ã‚’指定ã§ãã¾ã›ã‚“。 +jsp.error.deferredvaluetypewithoutdeferredvalue='deferredValue'㌠'true'ã§ãªã„å ´åˆã¯å€¤åž‹ã‚’指定ã§ãã¾ã›ã‚“ +jsp.error.directive.isnottagfile=[{0}] ディレクティブã¯ã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«å†…ã§ã—ã‹ä½¿ç”¨ã§ãã¾ã›ã‚“ +jsp.error.directive.istagfile=[{0}] ディレクティブã¯ã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«å†…ã§ã¯ä½¿ç”¨ã§ãã¾ã›ã‚“ +jsp.error.duplicate.name.jspattribute=標準ã¾ãŸã¯ã‚«ã‚¹ã‚¿ãƒ ã‚¢ã‚¯ã‚·ãƒ§ãƒ³å†…ã§æŒ‡å®šã•ã‚Œã¦ã„る属性 [{0}] ã¯ãã‚Œã«å›²ã¾ã‚ŒãŸjsp:attribute中ã®name属性ã®å€¤ã¨ã—ã¦ã‚‚表れã¾ã™ +jsp.error.duplicateqname=é‡è¤‡ã—ãŸä¿®é£¾å[{0}]ã‚’æŒã¤å±žæ€§ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚属性修飾åã¯ã€è¦ç´ å†…ã§ä¸€æ„ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +jsp.error.dynamic.attributes.not.implemented=[{0}] ã‚¿ã‚°ã¯ãã‚ŒãŒdynamic属性をå—ã‘付ã‘ã‚‹ã¨å®£è¨€ã—ã¦ã„ã¾ã™ãŒã€ãã‚Œã«å¿…è¦ãªã‚¤ãƒ³ã‚¿ãƒ•ã‚§ãƒ¼ã‚¹ã‚’実装ã—ã¦ã„ã¾ã›ã‚“ +jsp.error.el.parse=[{0}] : [{1}] +jsp.error.el.template.deferred=テンプレート文字列㫠#{...} ã‚’å«ã‚ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +jsp.error.el_interpreter_class.instantiation=ELInterpreter class [{0}] ã®ãƒ­ãƒ¼ãƒ‰ã¾ãŸã¯ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹åŒ–ã«å¤±æ•—ã—ã¾ã—㟠+jsp.error.fallback.invalidUse=jsp:fallbackã¯jsp:pluginã®ç›´æŽ¥ã®å­ã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.file.already.registered=ファイル [{0}] ã®å†å¸°çš„ãªå–ã‚Šè¾¼ã¿ã§ã™ +jsp.error.file.cannot.read=ファイルãŒèª­ã‚ã¾ã›ã‚“: [{0}] +jsp.error.file.close=readerクローズ時ã®ä¾‹å¤– +jsp.error.file.not.found=JSP ファイル [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +jsp.error.flush=データをフラッシュã™ã‚‹éš›ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +jsp.error.fragmentwithtype=''fragment''属性ã¨''type''属性を両方指定ã§ãã¾ã›ã‚“。''fragment''ãŒå­˜åœ¨ã™ã‚‹å ´åˆã«ã¯''type''ã¯''{0}''ã«å›ºå®šã•ã‚Œã¾ã™ +jsp.error.function.classnotfound=TLDã®ä¸­ã§é–¢æ•° [{1}] ã«æŒ‡å®šã•ã‚Œã¦ã„るクラス [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“: [{2}] +jsp.error.include.exception=[{0}] ã‚’ include ã§ãã¾ã›ã‚“ +jsp.error.include.tag=無効ãªjsp:includeã‚¿ã‚°ã§ã™ +jsp.error.internal.filenotfound=内部エラー: ファイル [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +jsp.error.invalid.attribute=[{0}]ã¯ç„¡åŠ¹ãªå±žæ€§ã‚’æŒã£ã¦ã„ã¾ã™: [{1}] +jsp.error.invalid.bean=useBeanã®ã‚¯ãƒ©ã‚¹å±žæ€§ [{0}] ã®å€¤ãŒç„¡åŠ¹ã§ã™ +jsp.error.invalid.directive=無効ãªãƒ‡ã‚£ãƒ¬ã‚¯ãƒ†ã‚£ãƒ– +jsp.error.invalid.expression=[{0}] ã¯ç„¡åŠ¹ãªå¼ã‚’å«ã‚“ã§ã„ã¾ã™: [{1}] +jsp.error.invalid.implicit=[{0}]ã®ã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«ã®æš—é»™ã®TLDãŒç„¡åŠ¹ã§ã™ +jsp.error.invalid.implicit.version=[{0}]ã®ã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«ã®æš—é»™ã®TLDã§ç„¡åŠ¹ãªJSPãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒå®šç¾©ã•ã‚Œã¦ã„ã¾ã™ã€‚ +jsp.error.invalid.name=ファイル [{0}] ã¯ã€jsp:getProperty 㧠bean ã®åå‰ã¨ã—ã¦ã€JSP.5.3 ã«å¾“ã£ã¦äº‹å‰ã«å°Žå…¥ã•ã‚Œã¦ã„ãªã„ [{1}] を使用ã—ã¦ã„ã¾ã™ +jsp.error.invalid.scope=''scope''属性ã®å€¤ãŒç„¡åŠ¹ã§ã™: [{0}] ("page"ã€"request"ã€"session"ã¾ãŸã¯"application"ã®ã©ã‚Œã‹ã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“) +jsp.error.invalid.tagdir=タグファイルディレクトリ [{0}] ãŒ"/WEB-INF/tags"ã§å§‹ã¾ã£ã¦ã„ã¾ã›ã‚“ +jsp.error.invalid.varscope=無効ãªå¤‰æ•°ã‚¹ã‚³ãƒ¼ãƒ— [{0}] +jsp.error.invalid.version=タグファイル [{0}] ã«ã¯ç„¡åŠ¹ãª JSP ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ +jsp.error.ise_on_clear=ãƒãƒƒãƒ•ã‚¡ã‚µã‚¤ã‚ºãŒ0ã®æ™‚ã«clear()を実行ã—ã¦ã‚‚無効ã§ã™ +jsp.error.java.line.number=生æˆã•ã‚ŒãŸJavaファイル: [{1}] ã® [{0}] è¡Œã§ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+jsp.error.javac=Javac 例外 +jsp.error.javac.env=Environment: +jsp.error.jspbody.emptybody.only=[{0}] ã‚¿ã‚°ã¯ã€ãã®ãƒœãƒ‡ã‚£å†…ã«jsp:attributeã ã‘ã‚’æŒã¤ã“ã¨ãŒã§ãã¾ã™ +jsp.error.jspbody.invalidUse=jsp:bodyã¯æ¨™æº–ã¾ãŸã¯ã‚«ã‚¹ã‚¿ãƒ ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã®å‰¯è¦ç´ ã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.jspbody.required=jsp:attributeãŒä½¿ç”¨ã•ã‚ŒãŸå ´åˆã€[{0}]ã«ã‚¿ã‚°ãƒœãƒ‡ã‚£ã‚’指定ã™ã‚‹ãŸã‚ã«jsp:bodyを使用ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ +jsp.error.jspc.missingTarget=ターゲットãŒã‚ã‚Šã¾ã›ã‚“: -webappã¾ãŸã¯-urirootã€ã¾ãŸã¯ä¸€ã¤ä»¥ä¸Šã®JSPページを指定ã—ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.jspc.no_uriroot=urirootãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ã®ã§ã€æŒ‡å®šã•ã‚ŒãŸJSPファイル(群)ã‚’é…ç½®ã§ãã¾ã›ã‚“ +jsp.error.jspc.uriroot_not_dir=-uriroot オプションã¯ã€æ—¢ã«å­˜åœ¨ã™ã‚‹ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’指定ã—ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.jspelement.missing.name=å¿…é ˆã®XMLスタイルã®'name'属性ãŒjsp:element内ã«ã‚ã‚Šã¾ã›ã‚“ +jsp.error.jspoutput.conflict=<jsp:output>: [{0}]ã«ç•°ãªã‚‹å€¤ã‚’複数回指定ã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{1}], æ–°: [{2}]) +jsp.error.jspoutput.doctypenamesystem=<jsp:output>: 'doctype-root-element' åŠã³ 'doctype-system' 属性ã¯åŒæ™‚ã«æŒ‡å®šã—ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.jspoutput.doctypepublicsystem=<jsp:output>: 'doctype-public'属性を指定ã™ã‚‹å ´åˆã¯ã€'doctype-system' 属性も指定ã—ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.jspoutput.invalidUse=<jsp:output> 標準構文ã®ä¸­ã§ä½¿ç”¨ã—ã¦ã¯ã„ã‘ã¾ã›ã‚“ +jsp.error.jspoutput.nonemptybody=<jsp:output> ボディをæŒã£ã¦ã¯ã„ã‘ã¾ã›ã‚“ +jsp.error.jsproot.version.invalid=無効ãªãƒãƒ¼ã‚¸ãƒ§ãƒ³ç•ªå·ã§ã™: [{0}]ã€"1.2" "2.0" "2.1" "2.2" "2.3" "3.0" ã¾ãŸã¯ "3.1" ã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.jsptext.badcontent='<'ãŒ<jsp:text>ã®ãƒœãƒ‡ã‚£ã®ä¸­ã«ç¾ã‚Œã‚‹æ™‚ã¯ã€CDATAã®ä¸­ã«ã‚«ãƒ—セル化ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ +jsp.error.lastModified=ファイル [{0}] ã®æœ€çµ‚更新日時をå–å¾—ã§ãã¾ã›ã‚“。 +jsp.error.library.invalid=ライブラリ[{0}]ã«å¾“ã†ã¨JSPページã¯ç„¡åŠ¹ã§ã™: [{1}] +jsp.error.literal_with_void=戻り値ã®åž‹ãŒvoidã§ã‚ã‚‹é…延メソッドã¨ã—ã¦å®šç¾©ã•ã‚Œã¦ã„る属性[{0}]ã«ãƒªãƒ†ãƒ©ãƒ«å€¤ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ JSP.2.3.4ã§ã¯ã€ã“ã®å ´åˆãƒªãƒ†ãƒ©ãƒ«å€¤ã¯è¨±å¯ã•ã‚Œã¾ã›ã‚“。 +jsp.error.loadclass.taghandler=ã‚¿ã‚° [{1}] ã®ã‚¿ã‚°ãƒãƒ³ãƒ‰ãƒ©ã‚¯ãƒ©ã‚¹ [{0}] をロードã§ãã¾ã›ã‚“ +jsp.error.location=行:[{0}]ã€åˆ—:[{1}] +jsp.error.mandatory.attribute=[{0}]: 必須属性 [{1}] ãŒã‚ã‚Šã¾ã›ã‚“ +jsp.error.missing.tagInfo=[{0}] ã«å¯¾ã™ã‚‹TagInfoオブジェクトãŒTLDã‹ã‚‰å¤±ã‚ã‚Œã¾ã—㟠+jsp.error.missing_attribute=TLDã¾ãŸã¯ã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«ã«ã‚ˆã‚‹ã¨ã€å±žæ€§ [{0}] ã¯ã‚¿ã‚° [{1}] ã«ã¯å¿…é ˆã§ã™ +jsp.error.missing_var_or_varReader='var'ã¾ãŸã¯'varReader'属性ãŒã‚ã‚Šã¾ã›ã‚“ +jsp.error.namedAttribute.invalidUse=jsp:attributeã¯æ¨™æº–ã¾ãŸã¯ã‚«ã‚¹ã‚¿ãƒ ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã®å‰¯è¦ç´ ã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.needAlternateJavaEncoding=既定㮠Java エンコーディング [{0}] ã¯ã‚ãªãŸã®ãƒ—ラットフォームã§ã¯ç„¡åŠ¹ã§ã™ã€‚JspServlet ã® ''javaEncoding'' パラメータã§åˆ¥ã®å€¤ã‚’指定ã§ãã¾ã™ã€‚ +jsp.error.negativeBufferSize=ãƒãƒƒãƒ•ã‚¡ãƒ¼ã‚µã‚¤ã‚ºãŒè² ã§ã™ã€‚ +jsp.error.negativeParameter=パラメータ [{0}] ã¯è² ã§ã‚ã£ã¦ã¯ãªã‚Šã¾ã›ã‚“ +jsp.error.nested.jspattribute=jsp:attribute標準アクションã¯åˆ¥ã®jsp:attribute標準アクションã®ç¯„囲内ã§ãƒã‚¹ãƒˆã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +jsp.error.nested.jspbody=jsp:body標準アクションã¯åˆ¥ã®jsp:bodyã¾ãŸã¯jsp:attribute標準アクションã®ç¯„囲内ã§ãƒã‚¹ãƒˆã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +jsp.error.nested_jsproot=入れå­ã«ãªã£ãŸ <jsp:root> ã§ã™ +jsp.error.no.jsp=JSPファイル[{0}]を見ã¤ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。 +jsp.error.no.more.content=å¿…è¦ãªè§£æžä¸­ã«å†…容ã®æœ€å¾Œã¾ã§é”ã—ã¾ã—ãŸ: ã‚¿ã‚°ã®ãƒã‚¹ãƒˆã®ã‚¨ãƒ©ãƒ¼ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“ +jsp.error.no.scratch.dir=JSP エンジンã«æ—¢å®šã® scratchDir ãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã›ã‚“。\n\ +\ ã“ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã® servlets.properties ファイルã«ã€\n\ +\ "jsp.initparams=scratchdir=" を追加ã—ã¦ãã ã•ã„。 +jsp.error.no.scriptlets=スクリプティングè¦ç´  ( <%!ã€<jsp:declarationã€<%=ã€<jsp:expressionã€<%ã€<jsp:scriptlet ) ã¯ã“ã“ã§ã¯è¨±ã•ã‚Œã¾ã›ã‚“。 +jsp.error.noFile=ファイル[{0}]を見ã¤ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。 +jsp.error.noFunction=関数 [{0}] を指定ã•ã‚ŒãŸãƒ—リフィクスã§é…ç½®ã§ãã¾ã›ã‚“ +jsp.error.noFunctionMethod=関数 [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ㌠クラス [{2}] 内ã§è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +jsp.error.noInstanceManager=ServletContextã«org.apache.tomcat.InstanceManagerãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ +jsp.error.non_null_tei_and_var_subelems=ã‚¿ã‚° [{0}] ã¯ä¸€ã¤ä»¥ä¸Šã®variable副è¦ç´ ã¨ä¸€ã¤ä»¥ä¸Šã®VariableInfoã‚’è¿”ã™TagExtraInfoクラスをæŒã£ã¦ã„ã¾ã™ +jsp.error.not.in.template=テンプレートテキストボディ内ã§ã¯ [{0}] ã¯è¨±ã•ã‚Œã¾ã›ã‚“。 +jsp.error.nullArgument=Null 引数 +jsp.error.outputfolder=出力ディレクトリãŒã‚ã‚Šã¾ã›ã‚“ +jsp.error.outputfolder.detail=JSP ã®ã‚³ãƒ³ãƒ‘イルã«å¿…è¦ãªå‡ºåŠ›ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª [{0}] を作æˆã§ãã¾ã›ã‚“ +jsp.error.overflow=エラー: JSPãƒãƒƒãƒ•ã‚¡ãŒã‚ªãƒ¼ãƒãƒ¼ãƒ•ãƒ­ãƒ¼ã—ã¾ã—㟠+jsp.error.page.conflict.autoflush=Pageディレクティブ: ''autoFlush''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.buffer=Pageディレクティブ: ''buffer''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.contenttype=Pageディレクティブ: ''contentType''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.deferredsyntaxallowedasliteral=Pageディレクティブ: "deferredSyntaxAllowedAsLiteral" ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.errorOnELNotFound=ページディレクティブ: ç•°ãªã‚‹å€¤ã‚’æŒã¤ ''errorOnELNotFound'' ãŒè¤‡æ•°å›žå‡ºç¾ã™ã‚‹ã“ã¨ã¯ä¸æ­£ã§ã™ (æ—§: [{0}] ã€æ–°: [{1}] ) +jsp.error.page.conflict.errorpage=Pageディレクティブ: ''errorPage''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.extends=Pageディレクティブ: ''extends''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.info=Pageディレクティブ: ''info''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.iselignored=Pageディレクティブ: ''isELIgnored''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.iserrorpage=Pageディレクティブ: ''isErrorPage''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.isthreadsafe=Tagディレクティブ: ''isThreadSafe''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.language=Pageディレクティブ: ''language''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.session=Pageディレクティブ: ''session''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.conflict.trimdirectivewhitespaces=Pageディレクティブ: ''trimDirectiveWhitespaces''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.page.invalid.buffer=Pageディレクティブ: buffer属性ã®å€¤ãŒç„¡åŠ¹ã§ã™ +jsp.error.page.invalid.deferredsyntaxallowedasliteral=Pageディレクティブ: deferredSyntaxAllowedAsLiteral ã®å€¤ã¯ä¸æ­£ã§ã™ +jsp.error.page.invalid.errorOnELNotFound=ページディレクティブ: errorOnELNotFoundã®å€¤ãŒä¸æ­£ã§ã™ +jsp.error.page.invalid.import=Pageディレクティブ:importã®å€¤ãŒç„¡åŠ¹ã§ã™ +jsp.error.page.invalid.iselignored=Pageディレクティブ: isELIgnored ã®å€¤ã¯ä¸æ­£ã§ã™ +jsp.error.page.invalid.iserrorpage=Pageディレクティブ: isErrorPage属性ã®å€¤ãŒç„¡åŠ¹ã§ã™ +jsp.error.page.invalid.isthreadsafe=Pageディレクティブ: isThreadSafeã®å€¤ãŒç„¡åŠ¹ã§ã™ +jsp.error.page.invalid.scope=スコープãŒç„¡åŠ¹ã§ã™ +jsp.error.page.invalid.session=Pageディレクティブ: session属性ã®å€¤ãŒç„¡åŠ¹ã§ã™ +jsp.error.page.invalid.trimdirectivewhitespaces=Pageディレクティブ:trimDirectiveWhitespacesã®å€¤ãŒç„¡åŠ¹ã§ã™ +jsp.error.page.language.nonjava=Pageディレクティブ: 無効ãªlanguage属性ã§ã™ +jsp.error.page.multi.pageencoding=Pageディレクティブã¯è¤‡æ•°ã®pageencodingã‚’æŒã¤ã“ã¨ã¯ã§ãã¾ã›ã‚“ +jsp.error.page.noSession=セッションã«åŠ ã‚ã£ã¦ã„ãªã„ページã®ä¸­ã§ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¹ã‚³ãƒ¼ãƒ—ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã›ã‚“ +jsp.error.page.nullThrowable=Null 例外 +jsp.error.page.sessionRequired=ページã«ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒå¿…è¦ã§ã‚ã‚Šã€none ãŒåˆ©ç”¨å¯èƒ½ã§ã™ã€‚ +jsp.error.param.invalidUse=jsp:includeã€jsp:forwardã€ã¾ãŸã¯jsp:paramsè¦ç´ ã®å¤–ã§jsp:paramアクションを使用ã—ã¦ã¯ã„ã‘ã¾ã›ã‚“ +jsp.error.paramexpected="name"属性 㨠"value" 属性をæŒã¤ "jsp:param" 標準アクションãŒå¿…è¦ã§ã™ +jsp.error.params.emptyBody=jsp:paramsã¯å°‘ãªãã¨ã‚‚一ã¤ã®ãƒã‚¹ãƒˆã—ãŸjsp:paramã‚’å«ã¾ã­ã°ã„ã‘ã¾ã›ã‚“ +jsp.error.params.invalidUse=jsp:paramsã¯jsp:pluginã®ç›´æŽ¥ã®å­ã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.parse.error.in.TLD=ã‚¿ã‚°ãƒ©ã‚¤ãƒ–ãƒ©ãƒªè¨˜è¿°å­ [{0}] 中ã®è§£æžã‚¨ãƒ©ãƒ¼ã§ã™ +jsp.error.parse.xml=ファイル [{0}] ã®XML解æžä¸­ã®ã‚¨ãƒ©ãƒ¼ +jsp.error.parse.xml.line=ファイル[{0}]ã®XML解æžã‚¨ãƒ©ãƒ¼: (è¡Œ [{1}], 列 [{2}]) +jsp.error.parse.xml.scripting.invalid.body=[{0}] è¦ç´ ã®ãƒœãƒ‡ã‚£ã¯XMLè¦ç´ ã‚’å«ã‚“ã§ã¯ã„ã‘ã¾ã›ã‚“ +jsp.error.plugin.badtype=jsp:pluginã® 'type'属性ã®å€¤ãŒç„¡åŠ¹ã§ã™: 'bean'ã¾ãŸã¯'applet'ã§ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.plugin.nocode=jsp:pluginã§code属性ãŒå®£è¨€ã•ã‚Œã¦ã„ã¾ã›ã‚“ +jsp.error.plugin.notype=jsp:pluginã§type属性ãŒå®£è¨€ã•ã‚Œã¦ã„ã¾ã›ã‚“ +jsp.error.precompilation=JSP [{0}]をプリコンパイルã§ãã¾ã›ã‚“ã§ã—㟠+jsp.error.precompilation.parameter=プリコンパイルリクエストパラメータ [{0}] ã‚’ [{1}] ã«è¨­å®šã§ãã¾ã›ã‚“ +jsp.error.prefix.refined=プリフィックス [{0}] ãŒç¾åœ¨ã®ã‚¹ã‚³ãƒ¼ãƒ—内ã§æ—¢ã« [{2}] ã¨å®šç¾©ã•ã‚Œã¦ã„ã‚‹ã®ã§ [{1}] ã«å†å®šç¾©ã—ã¾ã—㟠+jsp.error.prefix.use_before_dcl=ã“ã®Tagディレクティブã§æŒ‡å®šã•ã‚Œã¦ã„るプリフィックス [{0}] ã¯ã€ã™ã§ã«ãƒ•ã‚¡ã‚¤ãƒ« [{1}] ã® [{2}] 行目ã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ä½¿ç”¨ã•ã‚Œã¦ã„ã¾ã™ +jsp.error.prolog_config_encoding_mismatch=XML宣言部ã§æŒ‡å®šã•ã‚ŒãŸpage-encoding [{0}] ãŒjsp-property-group中ã®æŒ‡å®šã¨ç•°ãªã‚Šã¾ã™ [{1}] +jsp.error.prolog_pagedir_encoding_mismatch=XML宣言部ã§æŒ‡å®šã•ã‚ŒãŸpage-encoding [{0}] ãŒpageディレクティブ中ã®æŒ‡å®š [{1}] ã¨ç•°ãªã£ã¦ã„ã¾ã™ +jsp.error.quotes.unterminated=引用符ãŒçµ‚了ã—ã¦ã„ã¾ã›ã‚“ +jsp.error.readContent=予想ã•ã‚Œã‚‹é•·ã•[{0}]を読ã¿å–ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。 +jsp.error.reload=サーブレットã®ãƒªãƒ­ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—㟠+jsp.error.scripting.variable.missing_name=属性 [{0}] ã‹ã‚‰ã‚¹ã‚¯ãƒªãƒ—ト変数åを決定ã§ãã¾ã›ã‚“ +jsp.error.security=コンテキストã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã®åˆæœŸåŒ–ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +jsp.error.securityPreload=クラスã®ãƒ—リロード中ã®ã‚¨ãƒ©ãƒ¼ +jsp.error.servlet.destroy.failed=JSPページã®Servlet.destroy()中ã®ä¾‹å¤– +jsp.error.servlet.invalid.method=JSPã§ã¯GETã€POSTã€ã¾ãŸã¯HEADã®ã¿ãŒè¨±å¯ã•ã‚Œã¾ã™ã€‚ Jasperã¯OPTIONSも許å¯ã—ã¦ã„ã¾ã™ã€‚ +jsp.error.setLastModified=ファイル[{0}]ã®æœ€çµ‚更新日を設定ã§ãã¾ã›ã‚“ +jsp.error.signature.classnotfound=TLDã®ä¸­ã®ãƒ¡ã‚½ãƒƒãƒ‰ã‚·ã‚°ãƒãƒãƒ£ã§é–¢æ•° [{1}] ã«æŒ‡å®šã•ã‚Œã¦ã„るクラス [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。[{2}] +jsp.error.simpletag.badbodycontent=クラス [{0}] ã®TLDã¯SimpleTagã«ç„¡åŠ¹ãªbody-content (JSP)を指定ã—ã¦ã„ã¾ã™ +jsp.error.single.line.number=JSPファイル: [{1}] ã®ä¸­ã® [{0}] 行目ã§ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+jsp.error.stream.close.failed=ストリームをクローズã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +jsp.error.stream.closed=ストリームãŒã‚¯ãƒ­ãƒ¼ã‚ºã•ã‚Œã¦ã„ã¾ã™ +jsp.error.string_interpreter_class.instantiation=StringInterpreterクラス [{0}] ã®ãƒ­ãƒ¼ãƒ‰ã¾ãŸã¯ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹åŒ–ã«å¤±æ•—ã—ã¾ã—㟠+jsp.error.tag.conflict.attr=Tagディレクティブ: 属性[{0}]ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{1}], æ–°: [{2}]) +jsp.error.tag.conflict.deferredsyntaxallowedasliteral=Tagディレクティブ: ''deferredSyntaxAllowedAsLiteral''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.tag.conflict.errorOnELNotFound=タグディレクティブ: ç•°ãªã‚‹å€¤ã‚’æŒã¤ ''errorOnELNotFound'' ãŒè¤‡æ•°å›žå‡ºç¾ã™ã‚‹ã“ã¨ã¯ä¸æ­£ã§ã™ (æ—§: [{0}] ã€æ–°: [{1}] ) +jsp.error.tag.conflict.iselignored=Tagディレクティブ: ''isELIgnored''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.tag.conflict.language=Tagディレクティブ: ''language''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.tag.conflict.trimdirectivewhitespaces=Tagディレクティブ: ''trimDirectiveWhitespaces''ã‚’ç•°ãªã‚‹å€¤ã§è¤‡æ•°å›žæŒ‡å®šã™ã‚‹ã®ã¯ç„¡åŠ¹ã§ã™ (æ—§: [{0}], æ–°: [{1}]) +jsp.error.tag.invalid.deferredsyntaxallowedasliteral=Tagディレクティブ: deferredSyntaxAllowedAsLiteral ã«ç„¡åŠ¹ãªå€¤ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +jsp.error.tag.invalid.errorOnELNotFound=Tagディレクティブ: errorOnELNotFoundã®å€¤ãŒä¸æ­£ã§ã™ +jsp.error.tag.invalid.iselignored=Tagディレクティブ: isELIgnoredã«ç„¡åŠ¹ãªå€¤ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ +jsp.error.tag.invalid.trimdirectivewhitespaces=Tagディレクティブ: trimDirectiveWhitespaces ã®å€¤ã¯ä¸æ­£ã§ã™ +jsp.error.tag.language.nonjava=Tagディレクティブ: 無効ãªlanguage属性ã§ã™ +jsp.error.tag.multi.pageencoding=Tagディレクティブã¯è¤‡æ•°ã®pageencodingã‚’æŒã¤ã“ã¨ã¯ã§ãã¾ã›ã‚“ +jsp.error.tagHandlerPool=ã‚¿ã‚°ãƒãƒ³ãƒ‰ãƒ©ãƒ—ール [{0}] ãŒä½œæˆã§ãã¾ã›ã‚“ +jsp.error.tagdirective.badbodycontent=tagディレクティブ中ã®ç„¡åŠ¹ãªbody-content [{0}]ã§ã™ +jsp.error.tagfile.badSuffix=タグファイルパス [{0}] ã®ä¸­ã«".tag" æ‹¡å¼µå­ãŒã‚ã‚Šã¾ã›ã‚“ +jsp.error.tagfile.illegalPath=ä¸æ­£ãªã‚¿ã‚°ãƒ•ã‚¡ã‚¤ãƒ«ãƒ‘スã§ã™: [{0}]ã€ã“ã‚Œã¯"/WEB-INF/tags"ã¾ãŸã¯"/META-INF/tags"ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“ +jsp.error.tagfile.missingPath=タグファイルã«ãƒ‘スãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ +jsp.error.tagfile.nameFrom.badAttribute=ã“ã®variable指示å­ã®name-from-attribute属性ã®å€¤ã«ä¸€è‡´ã™ã‚‹ã€[{1}] 行目ã§å®£è¨€ã•ã‚Œname属性㌠[{0}] ã®attribute指示å­ã¯ã€java.lang.Stringåž‹ã§ã€"required"ã§ã‚ã‚‹å¿…è¦ãŒã‚ã‚Šã€"rtexprvalue"ã§ã‚ã£ã¦ã¯ã„ã‘ã¾ã›ã‚“。 +jsp.error.tagfile.nameFrom.noAttribute=ã“ã®name-from-attribute属性ã®å€¤ã§ã‚る値 [{0}] ã®name属性をæŒã¤attributeディレクティブãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +jsp.error.tagfile.nameNotUnique=[{2}]行目㮠[{0}] ã®å€¤ã¨ [{1}] ã®å€¤ã¯åŒã˜ã§ã™ +jsp.error.taglibDirective.absUriCannotBeResolved=絶対URI: [{0}] ã¯web.xmlã¨ã“ã®ã‚¢ãƒ—リケーションをé…å‚™ã—ãŸJARファイルã®ã©ã¡ã‚‰ã‹ã§ã‚‚解決ã§ãã¾ã›ã‚“ +jsp.error.taglibDirective.both_uri_and_tagdir='uri'属性 㨠'tagdir'属性ã®ä¸¡æ–¹ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™ +jsp.error.taglibDirective.missing.location=taglib指示å­ã®ä¸­ã«'uri'属性ã¨'tagdir'属性ã®ã©ã¡ã‚‰ã‚‚指定ã•ã‚Œã¦ã„ã¾ã›ã‚“ +jsp.error.taglibDirective.uriInvalid=タグライブラリ [{0}] ã«ä¸æ­£ãª URI ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +jsp.error.tei.invalid.attributes=[{0}] ã«å¯¾ã™ã‚‹TagExtraInfoã‹ã‚‰ã®æ¤œè¨¼ã‚¨ãƒ©ãƒ¼ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã§ã™ +jsp.error.teiclass.instantiation=TagExtraInfo classã®ãƒ­ãƒ¼ãƒ‰ã¾ãŸã¯ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹åŒ–ã«å¤±æ•—ã—ã¾ã—ãŸ: [{0}] +jsp.error.text.has_subelement=<jsp:text> ã¯å‰¯è¦ç´ ã‚’æŒã£ã¦ã¯ã„ã‘ã¾ã›ã‚“ +jsp.error.tld.fn.duplicate.name=タグライブラリ [{1}] ã®ä¸­ã®é–¢æ•°å [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +jsp.error.tld.fn.invalid.signature=TLDã®ä¸­ã®é–¢æ•°ã‚·ã‚°ãƒãƒãƒ£ã«å¯¾ã™ã‚‹ç„¡åŠ¹ãªæ§‹æ–‡ã§ã™ã€‚タグライブラリ: [{0}]ã€é–¢æ•°: [{1}] +jsp.error.tld.invalid_tld_file=無効ãªtldファイル:[{0}]ã€è©³ç´°ã¯JSP仕様ã®ã‚»ã‚¯ã‚·ãƒ§ãƒ³7.3.1ã‚’å‚ç…§ã—ã¦ãã ã•ã„。 +jsp.error.tld.mandatory.element.missing=TLD [{1}] ã«å¿…é ˆè¦ç´ ã® [{0}] ãŒå­˜åœ¨ã—ãªã„ã‹ç©ºã«ãªã£ã¦ã„ã¾ã™ +jsp.error.tld.missing=URI [{1}] ã® taglib [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +jsp.error.tld.missing_jar=TLDã‚’å«ã‚€JARリソース [{0}] ãŒã‚ã‚Šã¾ã›ã‚“ +jsp.error.tld.unable_to_get_jar=TLDã‚’å«ã‚€JARリソース [{0}] ã‚’å–å¾—ã§ãã¾ã›ã‚“ : [{1}] +jsp.error.tld.url=ä¸æ­£ãªTLD URL [{0}] +jsp.error.tlv.invalid.page=[{1}] 中㮠[{0}] ã«å¯¾ã™ã‚‹TagLibraryValidatorã®æ¤œè¨¼ã‚¨ãƒ©ãƒ¼ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã§ã™ +jsp.error.tlvclass.instantiation=TagLibraryValidatorクラスã®ãƒ­ãƒ¼ãƒ‰ã¾ãŸã¯ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹åŒ–ã«å¤±æ•—ã—ã¾ã—ãŸ: [{0}] +jsp.error.typeConversion=[{0}] ã‹ã‚‰ {1} ã¸ã®å¤‰æ›ã«å¤±æ•—ã—ã¾ã—㟠+jsp.error.unable.compile=JSPã®ã‚¯ãƒ©ã‚¹ã‚’コンパイルã§ãã¾ã›ã‚“ +jsp.error.unable.deleteClassFile=クラスファイルを削除ã§ãã¾ã›ã‚“ +jsp.error.unable.getType=[{0}]ã‹ã‚‰åž‹ã‚’抽出ã§ãã¾ã›ã‚“。 +jsp.error.unable.load=JSP ã®ã‚¯ãƒ©ã‚¹ã‚’ロードã§ãã¾ã›ã‚“ +jsp.error.unable.renameClassFile=クラスファイルã®åå‰ã‚’[{0}]ã‹ã‚‰[{1}]ã«å¤‰æ›´ã§ãã¾ã›ã‚“ +jsp.error.unable.to_find_method=属性 [{0}] ã®setterメソッドãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +jsp.error.unavailable=JSPã¯åˆ©ç”¨ä¸å¯ã¨ãƒžãƒ¼ã‚¯ã•ã‚Œã¦ã„ã¾ã™ +jsp.error.unbalanced.endtag=終了タグ "</{0}" ã®å¯¾å¿œãŒå–ã‚Œã¦ã„ã¾ã›ã‚“ +jsp.error.undeclared_namespace=宣言ã•ã‚Œã¦ã„ãªã„åå‰ç©ºé–“ã§ã‚«ã‚¹ã‚¿ãƒ ã‚¿ã‚°ãŒæ¤œå‡ºã•ã‚Œã¾ã—㟠[{0}] +jsp.error.unexpectedTag=予期ã—ãªã„ã‚¿ã‚° [{0}] +jsp.error.unknown_attribute_type=属性 [{0}] ã«å¯¾ã™ã‚‹æœªçŸ¥ã®å±žæ€§ã‚¿ã‚¤ãƒ— [{1}] ã§ã™ã€‚ +jsp.error.unsupported.encoding=サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„エンコーディングã§ã™: [{0}] +jsp.error.unterminated=[{0}] ã‚¿ã‚°ãŒçµ‚了ã—ã¦ã„ã¾ã›ã‚“ +jsp.error.usebean.duplicate=useBean: beanName属性ãŒé‡è¤‡ã—ã¦ã„ã¾ã™: [{0}] +jsp.error.usebean.noSession=JSPページãŒ(pageディレクティブã«ã‚ˆã‚Š)セッションã«å‚加ã—ãªã„ã“ã¨ã‚’宣言ã—ã¦ã„ã‚‹å ´åˆã€ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¹ã‚³ãƒ¼ãƒ—ã§ã®useBeanã®ä½¿ç”¨ã¯ç„¡åŠ¹ã§ã™ +jsp.error.var_and_varReader='var'ã¾ãŸã¯'varReader'ã®ã©ã¡ã‚‰ã‹ä¸€ã¤ã‚’指定ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ +jsp.error.variable.alias=name-from-attributeãŠã‚ˆã³alias属性ã®ä¸¡æ–¹ã‚’variableディレクティブ内ã«æŒ‡å®šã™ã‚‹ã€ã¾ãŸã¯ã©ã¡ã‚‰ã‚‚指定ã—ãªã„ã“ã¨ãŒã§ãã¾ã™ +jsp.error.variable.both.name=variableディレクティブ内ã§name-givenã¨name-from-attribute属性ã®ä¸¡æ–¹ã‚’指定ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +jsp.error.variable.either.name=name-givenã¾ãŸã¯name-from-attribute属性ã®ã©ã¡ã‚‰ã‹ã‚’variableディレクティブ内ã§æŒ‡å®šã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ +jsp.error.xml.badStandardAction=無効ãªæ¨™æº–アクションã§ã™: [{0}] +jsp.error.xml.bad_tag=URI [{1}] ã«é–¢é€£ä»˜ã‘られãŸã‚¿ã‚°ãƒ©ã‚¤ãƒ–ラリã®ä¸­ã«ã¯ã‚¿ã‚° [{0}] ã¯å®šç¾©ã•ã‚Œã¦ã„ã¾ã›ã‚“ +jsp.exception=[{0}] ã®å‡¦ç†ä¸­ã«è¡Œç•ªå· [{1}] ã§ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +jsp.info.ignoreSetting=SecurityManager ãŒæœ‰åŠ¹ãªãŸã‚ [{1}] ã®è¨­å®š [{0}] を無視ã—ã¾ã—㟠+jsp.message.dont.modify.servlets=é‡è¦: 生æˆã•ã‚ŒãŸã‚µãƒ¼ãƒ–レットを変更ã—ã¦ã¯ã„ã‘ã¾ã›ã‚“ +jsp.message.jsp_added=コンテキスト [{1}] ã®ã‚­ãƒ¥ãƒ¼ã«ãƒ‘ス [{0}] ã®JSPを追加ã—ã¦ã„ã¾ã™ +jsp.message.jsp_queue_created=コンテキスト [{1}] ã®é•·ã• [{0}] ã®ä½œæˆã•ã‚ŒãŸJSPキュー +jsp.message.jsp_queue_update=コンテキスト [{1}] ã®ã‚­ãƒ¥ãƒ¼ã«ã‚るパス [{0}] ã®JSPã‚’æ›´æ–°ã—ã¦ã„ã¾ã™ +jsp.message.jsp_removed_excess=コンテキスト [{1}] ã®ã‚­ãƒ¥ãƒ¼ã‹ã‚‰ãƒ‘ス [{0}] ã® JSP を削除ã—ã¾ã—ãŸã€‚ +jsp.message.jsp_removed_idle=[{2}] 秒後ã«ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆ [{1}] 内ã®ãƒ‘ス [{0}] ã®ã‚¢ã‚¤ãƒ‰ãƒ«çŠ¶æ…‹ã®JSPを削除ã—ã¾ã™ã€‚ +jsp.message.jsp_unload_check=コンテキスト [{0}] ã‹ã‚‰é–‹æ”¾ã™ã‚‹ JSP ã‚’ãƒã‚§ãƒƒã‚¯ã—ã¾ã™ã€‚JSP ã®ç·æ•°: [{1}] キュー長: [{2}] +jsp.message.parent_class_loader_is=親クラスローダ: [{0}] +jsp.message.scratch.dir.is=JSPエンジンã®Scratchdir: [{0}] +jsp.tldCache.noTldInDir=ディレクトリ [{0}] ã«ã¯ TLD ファイルãŒã‚ã‚Šã¾ã›ã‚“。 +jsp.tldCache.noTldInJar=[{0}]ã«TLDファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ CATALINA_BASE/conf/catalina.propertiesファイルã®tomcat.util.scan.StandardJarScanFilter.jarsToSkipプロパティã«JARを追加ã™ã‚‹ã“ã¨ã‚’検討ã—ã¦ãã ã•ã„。 +jsp.tldCache.noTldInResourcePath=リソースパス [{0}] ã«ã¯ TLD ファイルãŒã‚ã‚Šã¾ã›ã‚“。 +jsp.tldCache.noTldSummary=å°‘ãªãã¨ã‚‚1ã¤ã®JARã§TLDをスキャンã—ã¾ã—ãŸãŒã€TLDãŒå«ã¾ã‚Œã¦ã„ã¾ã›ã‚“ã§ã—ãŸã€‚スキャンã—ãŸã‚‚ã®ã®TLDãŒè¦‹ã¤ã‹ã‚‰ãªã‹ã£ãŸJARã®å®Œå…¨ãªãƒªã‚¹ãƒˆã«ã¤ã„ã¦ã¯ã€ã“ã®ãƒ­ã‚¬ãƒ¼ã®ãƒ‡ãƒãƒƒã‚°ãƒ­ã‚°ã‚’有効ã«ã—ã¦ãã ã•ã„。スキャン中ã«ä¸è¦ãªJARをスキップã™ã‚‹ã¨ã€èµ·å‹•æ™‚é–“ã¨JSPã®ã‚³ãƒ³ãƒ‘イル時間ãŒçŸ­ç¸®ã•ã‚Œã¾ã™ã€‚ +jsp.tldCache.tldInDir=TLD ファイルをディレクトリ [{0}] ã§ç™ºè¦‹ã—ã¾ã—ãŸã€‚ +jsp.tldCache.tldInJar=JAR ファイル [{0}] ã®å†…部㫠TLD ファイルを発見ã—ã¾ã—ãŸã€‚ +jsp.tldCache.tldInResourcePath=リソースパス [{0}] ã«TLDファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚ +jsp.warning.bad.urlpattern.propertygroup=web.xml中ã®url-pattern副è¦ç´ å†…ã«èª¤ã£ãŸå€¤ [{0}] ãŒã‚ã‚Šã¾ã™ +jsp.warning.checkInterval=警告: initParam checkIntervalã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値 "300" 秒ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.classDebugInfo=警告: initParam ã® classDebugInfo ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.classpathUrl=クラスパスã«ç„¡åŠ¹ãªURLãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚ ã“ã®URLã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +jsp.warning.compiler.classfile.delete.fail=生æˆã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ãƒ•ã‚¡ã‚¤ãƒ«[{0}]を削除ã§ãã¾ã›ã‚“ã§ã—㟠+jsp.warning.compiler.classfile.delete.fail.unknown=生æˆã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ãƒ•ã‚¡ã‚¤ãƒ«ã®å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—㟠+jsp.warning.compiler.javafile.delete.fail=生æˆã•ã‚ŒãŸJavaファイル[{0}]を削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +jsp.warning.development=警告: initParam ã® development ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「trueã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.displaySourceFragment=警告: initParam ã® displaySourceFragment ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「trueã€ã‚’使用ã—ã¾ã™ã€‚ +jsp.warning.dumpSmap=警告: initParam ã® dumpSmap ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.enablePooling=警告: initParam ã® enablePooling ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「trueã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.engineOptionsClass=engineOptionsClass [{0}] ã®ãƒ­ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +jsp.warning.fork=警告: initParam ã® fork ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「trueã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.genchararray=警告: initParam ã® genStringAsCharArray ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.isThreadSafe=警告: [{0}] ã§ä½¿ç”¨ã•ã‚Œã¦ã„ã‚‹ "isThreadSafe" ページディレクティブ属性ã¯éžæŽ¨å¥¨ã«ãªã‚Šã€JSP仕様ãƒãƒ¼ã‚¸ãƒ§ãƒ³4.0ã§å‰Šé™¤ã•ã‚Œã¾ã™ +jsp.warning.jspIdleTimeout=警告: initParam jspIdleTimeoutã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値 "-1"を使用ã—ã¾ã™ã€‚ +jsp.warning.keepgen=警告: initParam ã® keepgenerated ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.loadSmap=クラス [{0}] ã®SMAPデータをロードã§ãã¾ã›ã‚“ +jsp.warning.mappedFile=警告: initParam ã® mappedFile ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.maxLoadedJsps=警告: initParam maxLoadedJspsã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚ 既定値 "-1"を使用ã—ã¾ã™ã€‚ +jsp.warning.modificationTestInterval=警告: initParam ã® modificationTestInterval ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値 "4" 秒ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.noJarScanner=警告: ServletContext ã« org.apache.tomcat.JarScanner ãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“。既定値㮠JarScanner 実装を使用ã—ã¾ã™ã€‚ +jsp.warning.poolTagsWithExtends=警告: initParam ã® poolTagsWithExtends ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.quoteAttributeEL=警告: initParam ã® quoteAttributeEL ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.recompileOnFail=警告: initParam ã® recompileOnFail ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.strictGetProperty=警告: initParam ã® strictGetProperty ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「trueã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.strictQuoteEscaping=警告: initParam ã® strictQuoteEscaping ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚ 既定値「trueã€ã‚’使用ã—ã¾ã™ã€‚ +jsp.warning.strictWhitespace=警告: initParam ã® strictWhitespace ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「trueã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.suppressSmap=警告: initParam ã® suppressSmap ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.tagPreDestroy=[{0}]ã®ã‚¿ã‚°ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã§preDestroyを処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +jsp.warning.tagRelease=[{0}]ã®ã‚¿ã‚°ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹è§£æ”¾å‡¦ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +jsp.warning.trimspaces=警告: initParamtrimSpaces ã®å€¤ãŒç„¡åŠ¹ã§ã™ã€‚ 既定値ã®ã€Œfalseã€ã‚’使用ã—ã¾ã™ +jsp.warning.unknown.sourceVM=ä¸æ˜Žãª source VM [{0}]ãŒç„¡è¦–ã•ã‚Œã¾ã—㟠+jsp.warning.unknown.targetVM=ä¸æ˜Žãª target VM [{0}]ãŒç„¡è¦–ã•ã‚Œã¾ã—ãŸã€‚ +jsp.warning.unsupported.sourceVM=サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„ source VM [{0}] ãŒãƒªã‚¯ã‚¨ã‚¹ãƒˆã•ã‚Œã¾ã—ãŸã€‚[{1}] ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.unsupported.targetVM=サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„ target VM [{0}] ãŒãƒªã‚¯ã‚¨ã‚¹ãƒˆã•ã‚Œã¾ã—ãŸã€‚[{1}] ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.useInstanceManagerForTags=警告: initParam ã® useInstanceManagerForTagsã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ +jsp.warning.xpoweredBy=警告: initParam ã® xpoweredBy ã®å€¤ã¯ç„¡åŠ¹ã§ã™ã€‚既定値「falseã€ãŒä½¿ç”¨ã•ã‚Œã¾ã™ + +jspc.built=ビルドã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ« [{0}] +jspc.delete.fail=ファイル[{0}]を削除ã§ãã¾ã›ã‚“ã§ã—㟠+jspc.error.compilation=コンパイルエラー +jspc.error.fileDoesNotExist=ファイル引数 [{0}] ã¯å­˜åœ¨ã—ã¾ã›ã‚“。 +jspc.error.generalException=エラー: ファイル [{0}] ã§ä»¥ä¸‹ã®ä¸€èˆ¬çš„ãªä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸ: +jspc.error.invalidFragment=Webフラグメントã®ã‚¨ãƒ©ãƒ¼ã«ã‚ˆã‚Šãƒ—リコンパイルãŒä¸­æ­¢ã•ã‚Œã¾ã—㟠+jspc.error.invalidWebXml=web.xmlã®ã‚¨ãƒ©ãƒ¼ã«ã‚ˆã‚Šãƒ—リコンパイルを中止ã—ã¾ã—㟠+jspc.error.minThreadCount=å°‘ãªãã¨ã‚‚1ã¤ã®ã‚¹ãƒ¬ãƒƒãƒ‰ [{0}] ãŒå¿…è¦ã§ã™ +jspc.error.parseThreadCount=スレッド数[{0}]を解æžã§ãã¾ã›ã‚“。 +jspc.error.unknownOption=èªè­˜ã§ããªã„オプション[{0}]。 ヘルプã«ã¯-helpを使用ã—ã¦ãã ã•ã„。 +jspc.errorCount=エラー数:[{0}] +jspc.generatingMapping=コンパイル コンテキスト [{1}] を使用ã—ã¦ãƒ•ã‚¡ã‚¤ãƒ« [{0}] ã® Web マッピングを生æˆã—ã¦ã„ã¾ã™ +jspc.generation.result=[{1}] ミリ秒ã®é–“ã« [{0}] 個ã®ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¦ç”ŸæˆãŒå®Œäº†ã—ã¾ã—㟠+jspc.implicit.uriRoot=uriRoot ã«ã¯æš—黙的㫠[{0}] ãŒè¨­å®šã•ã‚Œã¾ã™ +jspc.outdated=[{0}] ã¯å¤ã„ãŸã‚コンパイルã•ã‚Œã¦ã„ã¾ã™ +jspc.processing=ファイル [{0}] を処ç†ä¸­ +jspc.start={0} ページã‹ã‚‰å®Ÿè¡Œã—ã¾ã™ +jspc.usage=使用法: jspc [--] \n\ +JSPファイルã®å ´æ‰€ã¯æ¬¡ã®ã‚ªãƒ—ションã§æŒ‡å®šã™ã‚‹ã‹ã€\n\ +\ -webapp web-appã‚’å«ã‚€ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã€‚ã™ã¹ã¦ã®JSPファイルã¯\n\ +\ å†å¸°çš„ã«è§£æžã•ã‚Œã‚‹\n\ +ã¾ãŸã¯æ¬¡ã®ä»»æ„ã®æ•°ã®ãƒ•ã‚¡ã‚¤ãƒ«ã§æŒ‡å®šã—ã¾ã™ã€‚\n\ +\ JSPã¨ã—ã¦è§£æžã•ã‚Œã‚‹ãƒ•ã‚¡ã‚¤ãƒ«\n\ +オプションã¯ä»¥ä¸‹ã®é€šã‚Šã§ã™\n\ +\ -help ã“ã®ãƒ˜ãƒ«ãƒ—メッセージã®è¡¨ç¤º\n\ +\ -v Verboseモード\n\ +\ -d 出力ディレクトリ\n\ +\ -l 失敗ã—ãŸJSPページã®åå‰ã®å‡ºåŠ›\n\ +\ -s æˆåŠŸã—ãŸJSPページã®åå‰ã®å‡ºåŠ›\n\ +\ -p ターゲットパッケージã®åå‰ (デフォルトã¯org.apache.jsp)\n\ +\ -c ターゲットクラスã®åå‰ (最åˆã®JSPページã ã‘ã«é©ç”¨ã•ã‚Œã‚‹)\n\ +\ -mapped JSPã®å„HTMLè¡Œã”ã¨ã«write()コールを生æˆ\n\ +\ -die[#] 致命的エラーã«ã‚¨ãƒ©ãƒ¼ãƒªã‚¿ãƒ¼ãƒ³ã‚³ãƒ¼ãƒ‰(#)ã‚’ç”Ÿæˆ (デフォルトã¯1)\n\ +\ -uribase コンパイルãŒç›¸å¯¾çš„ã«ãŠã“ãªã‚れるuriディレクトリ\n\ +\ (デフォルトã¯"/")\n\ +\ -uriroot -webappã¨åŒã˜\n\ +\ -compile 生æˆã—ãŸã‚µãƒ¼ãƒ–レットã®ã‚³ãƒ³ãƒ‘イル\n\ +\ -failFast Stop on first compile error\n\ +\ -webinc ファイルã«éƒ¨åˆ†çš„ãªã‚µãƒ¼ãƒ–レットマッピングを作æˆ\n\ +\ -webxml ファイルã«å®Œå…¨ãªweb.xmlを作æˆ\n\ +\ -webxmlencoding Set the encoding charset used to read and write the web.xml\n\ +\ file (default is UTF-8)\n\ +\ -addwebxmlmappings Merge generated web.xml fragment into the web.xml file of the\n\ +\ web-app, whose JSP pages we are processing\n\ +\ -ieplugin Internet Explorerã®Java Pluginã®classid\n\ +\ -classpath java.class.pathシステムプロパティã®ä¸Šæ›¸ã\n\ +\ -xpoweredBy X-Powered-Byレスãƒãƒ³ã‚¹ãƒ˜ãƒƒãƒ€ã®è¿½åŠ \n\ +\ -trimSpaces [single] Remove template text that consists entirely of whitespace\n\ +\ (if "single", replace such template text with a single space)\n\ +\ -javaEncoding Set the encoding charset for Java classes (default UTF-8)\n\ +\ -source Set the -source argument to the compiler (default 1.8)\n\ +\ -target Set the -target argument to the compiler (default 1.8)\n +jspc.warning.tldInWebInfLib=TLDファイルã¯/ WEB-INF/libã«ç½®ã‹ãªã„ã§ãã ã•ã„。 +jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.footer=\n\ +\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webinc.insertEnd= +jspc.webinc.insertStart= +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +jstl.OSAfterWriter=Writer ãŒä½¿ç”¨ã•ã‚Œã¦ã„ã‚‹å ´åˆã¯å‡ºåŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’使用ã§ãã¾ã›ã‚“。 +jstl.urlMustStartWithSlash=''context'' 属性ãŒæŒ‡å®šã•ã‚Œã¦ã„ã‚‹URLã‚¿ã‚°ã§ã¯ã€''url'' 㨠''context'' ã®ä¸¡æ–¹ãŒã‚¹ãƒ©ãƒƒã‚·ãƒ¥æ–‡å­—ã§å§‹ã¾ã‚‰ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“ +jstl.writerAfterOS=出力ストリームãŒä½¿ç”¨ã•ã‚Œã¦ã„ã‚‹å ´åˆã¯Writer を使用ã§ãã¾ã›ã‚“ + +org.apache.jasper.compiler.ELParser.invalidQuotesForStringLiteral=ä¸æ­£ãªæ–‡å­—列リテラル [{0}] ã§ã™ã€‚シングルクォートã€ã‚ã‚‹ã„ã¯ã€ãƒ€ãƒ–ルクォートã§å›²ã¾ã‚Œã¦ã„ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +org.apache.jasper.compiler.ELParser.invalidQuoting=å¼ [{0}] ã¯ç„¡åŠ¹ã§ã™ã€‚ 引用符ã§å›²ã¾ã‚ŒãŸæ–‡å­—列内ã§ã®ã¿ []ã€['']ã€["] 㯠[] ã§ã‚¨ã‚¹ã‚±ãƒ¼ãƒ—ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ +org.apache.jasper.compiler.TldCache.servletContextNull=渡ã•ã‚ŒãŸ ServletContext ㌠null ã§ã—㟠+org.apache.jasper.servlet.JasperInitializer.onStartup=コンテキスト [{0}] ã®Jasperã‚’åˆæœŸåŒ–ã—ã¾ã™ +org.apache.jasper.servlet.TldScanner.webxmlAdd=リソースパス [{0}] ã‹ã‚‰URI [{1}] ã®TLDをロードã—ã¦ã„ã¾ã™\n +org.apache.jasper.servlet.TldScanner.webxmlFailPathDoesNotExist=パス [{0}] ã¨URI [{1}] ã®TLDを処ç†ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ 指定ã•ã‚ŒãŸãƒ‘スã¯å­˜åœ¨ã—ã¾ã›ã‚“。 +org.apache.jasper.servlet.TldScanner.webxmlSkip=リソース [{0}] ã«ã¤ã„㦠URI [{1}] ã® TLD ã®èª­ã¿è¾¼ã¿ã‚’回é¿ã—ã¾ã—ãŸã€‚ ã§å®šç¾©æ¸ˆã¿ã§ã™ã€‚ diff --git a/java/org/apache/jasper/resources/LocalStrings_ko.properties b/java/org/apache/jasper/resources/LocalStrings_ko.properties new file mode 100644 index 0000000..d79ac03 --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_ko.properties @@ -0,0 +1,421 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=TLD 파ì¼ì— 따르면, [{0}] 태그는 반드시 비어 있어야 하는ë°, 그렇지 않습니다, + +jsp.engine.info=Jasper JSP {0} 엔진 +jsp.error.URLMustStartWithSlash=경로 [{0}]ì€(는) 반드시 슬래시 문ìžë¡œ 시작해야 합니다. +jsp.error.action.isnottagfile=[{0}] ì•¡ì…˜ì€ ì˜¤ì§ íƒœê·¸ 파ì¼ë“¤ ë‚´ì—서만 ì‚¬ìš©ë  ìˆ˜ 있습니다. +jsp.error.action.istagfile=[{0}] ì•¡ì…˜ì€ íƒœê·¸ íŒŒì¼ ë‚´ì—ì„œ ì‚¬ìš©ë  ìˆ˜ 없습니다. +jsp.error.attempt_to_clear_flushed_buffer=오류: ì´ë¯¸ 배출ë˜ì–´ 버린 버í¼ë¥¼ í기하려는 ì‹œë„ +jsp.error.attr.quoted=ì†ì„± ê°’ì€ ì¸ìš©ë¶€í˜¸ë¡œ 처리ë˜ì–´ì•¼ 합니다. +jsp.error.attribute.custom.non_rt_with_expr=TLD ë˜ëŠ” 태그 íŒŒì¼ ë‚´ì˜ attribute ì§€ì‹œì–´ì— ì˜í•˜ë©´, ì†ì„± [{0}]ì€(는) ì–´ë–¤ 표현ì‹ë„ 받아들ì´ì§€ 않습니다. +jsp.error.attribute.deferredmix=${} 와 #{} 모ë‘를 ë™ì¼í•œ ì†ì„± ê°’ ë‚´ì—ì„œ 표현ì‹ë“¤ë¡œ 사용할 수 없습니다. +jsp.error.attribute.duplicate=엘리먼트 ë‚´ì—ì„œ ì†ì„±ì˜ qualified ì´ë¦„ë“¤ì€ ë°˜ë“œì‹œ 유ì¼í•´ì•¼ 합니다. +jsp.error.attribute.invalidPrefix=ì†ì„± prefix [{0}]ì´(ê°€), ìž„í¬íŠ¸ëœ ì–´ë–¤ 태그 ë¼ì´ë¸ŒëŸ¬ë¦¬ì™€ë„ 대ì‘ë˜ì§€ 않습니다. +jsp.error.attribute.noequal=등호("=")ê°€ 요구ë©ë‹ˆë‹¤. +jsp.error.attribute.noescape=ì†ì„± ê°’ [{0}]ì´(ê°€) [{1}]ì„(를) 사용하여 ì¸ìš©ë¶€ 처리ë˜ì–´ 있는ë°, ì´ëŠ” ê°’ ë‚´ì—ì„œ ì‚¬ìš©ë  ë•Œì—는 반드시 escapeë˜ì–´ì•¼ 하는 것입니다. +jsp.error.attribute.noquote=ì¸ìš©ë¶€í˜¸ê°€ 요구ë©ë‹ˆë‹¤. +jsp.error.attribute.nowhitespace=JSP ìŠ¤íŽ™ì— ë”°ë¥´ë©´, ì†ì„± ì´ë¦„ì€ ë°˜ë“œì‹œ whitespace 다ìŒì— 나타나야 합니다. +jsp.error.attribute.null_name=ì†ì„± ì´ë¦„ì´ ë„ìž„ +jsp.error.attribute.standard.non_rt_with_expr=표준 ì•¡ì…˜ [{1}]ì˜ ì†ì„± [{0}]ì€(는), ì–´ë– í•œ 표현ì‹ë„ 받아들ì´ì§€ 않습니다. +jsp.error.attribute.unterminated=[{0}]ì„(를) 위한 ì†ì„± ê°’ì´ ì˜¬ë°”ë¥´ê²Œ 종료ë˜ì§€ 않았습니다. +jsp.error.backgroundCompilationFailed=백그ë¼ìš´ë“œ ì»´íŒŒì¼ ìž‘ì—… 실패 +jsp.error.bad.scratch.dir=귀하가 지정한 scratchDir [{0}]ì€(는) 사용할 수 없습니다. +jsp.error.badStandardAction=유효하지 ì•Šì€ í‘œì¤€ ì•¡ì…˜ +jsp.error.bad_attribute=TLDì— ë”°ë¥´ë©´, ì†ì„± [{0}]ì€(는) 태그 [{1}]ì„(를) 위해 유효하지 않습니다. +jsp.error.bad_tag=Prefix [{1}]와(ê³¼) 함께 ìž„í¬íŠ¸ëœ 태그 ë¼ì´ë¸ŒëŸ¬ë¦¬ ë‚´ì—, 태그 [{0}]ì´(ê°€) ì •ì˜ë˜ì§€ 않았습니다. +jsp.error.beans.nomethod=타입 [{1}]ì¸ beanì˜ í”„ë¡œí¼í‹° [{0}]ì„(를) ì½ê¸° 위한 메소드를 ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.beans.nomethod.setproperty=íƒ€ìž…ì´ [{2}]ì¸ bean ë‚´ì—ì„œ, 타입 [{1}]ì˜ í”„ë¡œí¼í‹° [{0}]ì— ëŒ€í•˜ì—¬, 쓰기 가능한 메소드를 ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.beans.noproperty=íƒ€ìž…ì´ [{1}]ì¸ beanì˜ í”„ë¡œí¼í‹° [{0}]ì— ëŒ€í•œ 정보를 ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.beans.nullbean=ë„ ê°ì²´ì— 대하여, bean 오í¼ë ˆì´ì…˜ì´ ì‹œë„ë˜ì—ˆìŠµë‹ˆë‹¤. +jsp.error.beans.property.conversion=ì†ì„± [{2}]ì— ì„¤ì •ëœ ë¬¸ìžì—´ [{0}]ì„(를), í´ëž˜ìŠ¤ [{1}](으)ë¡œ 변환할 수 없습니다: [{3}] +jsp.error.beans.propertyeditor.notregistered=PropertyEditorManagerì— PropertyEditorê°€ 등ë¡ë˜ì–´ 있지 않습니다. +jsp.error.beans.setproperty.noindexset=ì¸ë±ìŠ¤í™”ëœ í”„ë¡œí¼í‹°ë¥¼ 설정할 수 없습니다. +jsp.error.bug48498=JSP extract를 표시할 수 없습니다. XML íŒŒì„œì˜ ë²„ê·¸ ë•Œë¬¸ì¼ ìˆ˜ 있습니다 (ìƒì„¸ 정보는 Tomcat 버그 48498ì„ ì°¸ì¡°). +jsp.error.cannotAddResolver=ì²˜ìŒ ìš”ì²­ì´ í•œë²ˆ ì´ë£¨ì–´ì§„ ì´í›„ì—는, addELResolver를 호출할 수 없습니다. +jsp.error.classname=.class 파ì¼ë¡œë¶€í„° í´ëž˜ìŠ¤ëª…ì„ ê²°ì •í•  수 없습니다. +jsp.error.coerce_to_type=ì†ì„± [{0}]ì„(를) 위한 ê°’ [{2}]ì„(를), 타입 [{1}](으)ë¡œ ê°•ì œ 변환 시킬 수 없습니다. +jsp.error.compilation=íŒŒì¼ ì»´íŒŒì¼ ì¤‘ 오류 ë°œìƒ: [{0}] [{1}] +jsp.error.compilation.dependent=í´ëž˜ìŠ¤ [{0}](ì„)를 불러올 수 없습니다. +jsp.error.compilation.jdt=ì»´íŒŒì¼ ì˜¤ë¥˜ +jsp.error.compilation.jdtProblemError=JDT ë¬¸ì œë“¤ì˜ ëª©ë¡ì„ 처리하는 중 오류 ë°œìƒ +jsp.error.compilation.source=소스 íŒŒì¼ [{0}]ì„(를) 로드하는 중 오류 ë°œìƒ +jsp.error.compiler=가용한 ìžë°” 컴파ì¼ëŸ¬ê°€ 없습니다. +jsp.error.compiler.config=설정 ì˜µì…˜ë“¤ì¸ compilerClassName: [{0}]와(ê³¼) compiler: [{1}]ë“¤ì„ ì§€ì›í•˜ëŠ”, 가용한 ìžë°” 컴파ì¼ëŸ¬ê°€ 없습니다. +jsp.error.config_pagedir_encoding_mismatch=jsp-property-group [{0}]ì— ì§€ì •ëœ íŽ˜ì´ì§€ ì¸ì½”딩ì´, 페ì´ì§€ 지시어 [{1}]ì— ì§€ì •ëœ ê²ƒê³¼ 다릅니다. +jsp.error.corresponding.servlet=ìƒì„±ëœ 서블릿 오류:\n\ +\n +jsp.error.could.not.add.taglibraries=하나 ì´ìƒì˜ 태그 ë¼ì´ë¸ŒëŸ¬ë¦¬ë“¤ì„ 추가할 수 없었습니다. +jsp.error.data.file.processing=íŒŒì¼ [{0}]ì„(를) 처리하는 중 오류 ë°œìƒ +jsp.error.data.file.read=íŒŒì¼ [{0}]ì„(를) ì½ëŠ” 중 오류 ë°œìƒ +jsp.error.data.file.write=ë°ì´í„° 파ì¼ì„ 쓰는 중 오류 ë°œìƒ +jsp.error.deferredmethodandvalue='deferredValue'와 'deferredMethod', 둘 다 'true'ì¼ ìˆ˜ 없습니다. +jsp.error.deferredmethodsignaturewithoutdeferredmethod='deferredMethod'ê°€ 'true'ê°€ 아니면, 메소드 signature를 지정할 수 없습니다. +jsp.error.deferredvaluetypewithoutdeferredvalue=ë§Œì¼ 'deferredValue' ê°’ì´ 'true'ê°€ 아니ë¼ë©´, ê°’ì˜ íƒ€ìž…ì„ ì§€ì •í•  수 없습니다. +jsp.error.directive.isnottagfile=[{0}] 지시어는 ì˜¤ì§ íƒœê·¸ íŒŒì¼ ë‚´ì—서만 ì‚¬ìš©ë  ìˆ˜ 있습니다. +jsp.error.directive.istagfile=[{0}] 지시어는 태그 íŒŒì¼ ë‚´ì—ì„œ ì‚¬ìš©ë  ìˆ˜ 없습니다. +jsp.error.duplicate.name.jspattribute=표준 ë˜ëŠ” 커스텀 ì•¡ì…˜ì— ì§€ì •ëœ ì†ì„± [{0}]ì´(ê°€), ë‚´ë¶€ì— í¬í•¨ëœ jsp:attribute ë‚´ì˜ name ì†ì„±ì˜ ê°’ìœ¼ë¡œë„ ì§€ì •ëœ ê²ƒ 같습니다. +jsp.error.duplicateqname=ì¤‘ë³µëœ qualified ì´ë¦„ [{0}]ì„(를) 갖는 ì†ì„±ì´ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. ì†ì„±ì˜ qualified ì´ë¦„ë“¤ì€ ë°˜ë“œì‹œ 엘리먼트 ë‚´ì—ì„œ 유ì¼í•´ì•¼ 합니다. +jsp.error.dynamic.attributes.not.implemented=[{0}] 태그가 ë™ì  ì†ì„±ë“¤ì„ 받아들ì¸ë‹¤ê³  ì„ ì–¸ë˜ì–´ 있으나, 필수ì ì¸ ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 구현하지 않았습니다. +jsp.error.el.parse=[{0}] : [{1}] +jsp.error.el.template.deferred=#{...}ì€(는) 템플릿 í…스트ì—ì„œ 허용ë˜ì§€ 않습니다. +jsp.error.el_interpreter_class.instantiation=ELInterpreter í´ëž˜ìŠ¤ [{0}]ì„(를) 로드하지 못했거나, ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•˜ì§€ 못했습니다. +jsp.error.fallback.invalidUse=jsp:fallbackì€ jsp:pluginì˜ ì§ê³„ ìžì‹ 엘리먼트여야 합니다. +jsp.error.file.already.registered=íŒŒì¼ [{0}]ì˜ ìž¬ê·€ì ì¸ include입니다. +jsp.error.file.cannot.read=파ì¼ì„ ì½ì„ 수 없습니다: [{0}] +jsp.error.file.not.found=íŒŒì¼ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.flush=ë°ì´í„°ë¥¼ 배출하는 중 예외가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. +jsp.error.fragmentwithtype=''fragment''와 ''type'' ì†ì„±, 둘 다를 지정할 수 없습니다. ë§Œì¼ ''fragment''ì´ ì§€ì •ë˜ë©´, ''type''ì€ ''{0}''으로 ê³ ì •ë©ë‹ˆë‹¤. +jsp.error.function.classnotfound=function [{1}]ì„(를) 위하여 TLDì— ì§€ì •ëœ, í´ëž˜ìŠ¤ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다: [{2}] +jsp.error.include.exception=[{0}]ì„(를) includeí•  수 없습니다. +jsp.error.include.tag=유효하지 ì•Šì€ jsp:include 태그 +jsp.error.internal.filenotfound=내부 오류: íŒŒì¼ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.invalid.attribute=[{0}]ì€(는) 유효하지 ì•Šì€ ì†ì„±ì„ 가지고 있습니다: [{1}] +jsp.error.invalid.bean=useBeanì˜ class ì†ì„±ì„ 위한 ê°’ [{0}]ì€(는) 유효하지 않습니다. +jsp.error.invalid.directive=유효하지 ì•Šì€ ì§€ì‹œì–´ +jsp.error.invalid.expression=[{0}]ì´(ê°€) 유효하지 ì•Šì€ í‘œí˜„ì‹(들)ì„ í¬í•¨í•˜ê³  있습니다: [{1}] +jsp.error.invalid.implicit=[{0}]ì— ìœ„ì¹˜í•œ 태그 파ì¼ì„ 위해, 유효하지 ì•Šì€ ë¬µì‹œì  TLD입니다. +jsp.error.invalid.implicit.version=[{0}]ì— ìœ„ì¹˜í•œ 태그 파ì¼ì„ 위한 묵시ì ì¸ TLDì—, 유효하지 ì•Šì€ JSP ë²„ì „ì´ ì •ì˜ë˜ì–´ 있습니다. +jsp.error.invalid.scope=''scope'' ì†ì„±ìœ¼ë¡œ 불허ë˜ëŠ” ê°’: [{0}] (반드시 "page", "request", "session", "application" ì¤‘ì˜ í•˜ë‚˜ì—¬ì•¼ 합니다.) +jsp.error.invalid.tagdir=태그 íŒŒì¼ ë””ë ‰í† ë¦¬ [{0}]ì´(ê°€) "/WEB-INF/tags"ë¡œ 시작하지 않습니다. +jsp.error.invalid.version=[{0}]ì— ìžˆëŠ” tag 파ì¼ì— 유효하지 ì•Šì€ JSP ë²„ì „ì´ ì •ì˜ë˜ì–´ 있습니다. +jsp.error.ise_on_clear=ë²„í¼ í¬ê¸°ê°€ 0ì¼ ë•Œ clear()를 호출하는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +jsp.error.java.line.number=코드 ìƒì„±ëœ ìžë°” íŒŒì¼ [{1}]ì˜ [{0}] í–‰ì—ì„œ 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. +jsp.error.javac=Javac 예외 ë°œìƒ +jsp.error.javac.env=환경: +jsp.error.jspbody.emptybody.only=[{0}] 태그는 ì˜¤ì§ jsp:attributeë§Œì„ body ì•ˆì— í¬í•¨í•  수 있습니다. +jsp.error.jspbody.invalidUse=jsp:body는 반드시 표준 ë˜ëŠ” 커스텀 ì•¡ì…˜ì˜ í•˜ìœ„ 엘리먼트ì´ì–´ì•¼ 합니다. +jsp.error.jspbody.required=jsp:attributeê°€ 사용ë˜ëŠ” 경우, [{0}]ì„(를) 위한 íƒœê·¸ì˜ body를 지정하기 위해, jsp:body를 사용해야 합니다. +jsp.error.jspc.missingTarget=targetì´ ì—†ìŒ: 반드시 -webapp ë˜ëŠ” -uriroot, ë˜ëŠ” 하나 ì´ìƒì˜ JSP pageë“¤ì„ ì§€ì •í•´ì•¼ 합니다. +jsp.error.jspc.no_uriroot=urirootê°€ 지정ë˜ì§€ ì•Šì•„ì„œ, ì§€ì •ëœ í•´ë‹¹ JSP 파ì¼(들)ì— ëŒ€í•œ 위치를 알아낼 수 없습니다. +jsp.error.jspc.uriroot_not_dir=-uriroot ì˜µì…˜ì€ ë°˜ë“œì‹œ ì´ë¯¸ 존재하는 디렉토리를 지정해야 합니다. +jsp.error.jspelement.missing.name=필수 í•­ëª©ì¸ XML 스타ì¼ì˜ 'name' ì†ì„±ì´ 존재하지 않습니다. +jsp.error.jspoutput.conflict=<jsp:output>: [{0}]ì´(ê°€) 다른 값들로 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{1}], ì‹ ê·œ ê°’: [{2}]) +jsp.error.jspoutput.doctypenamesystem=<jsp:output>: 'doctype-root-element'와 'doctype-system' ì†ì„±ë“¤ì€ 반드시 함께 나타나야 합니다. +jsp.error.jspoutput.doctypepublicsystem=<jsp:output>: 'doctype-public' ì†ì„±ì´ 나타나는 경우ì—는, 'doctype-system' ì†ì„±ì´ 반드시 존재해야 합니다. +jsp.error.jspoutput.invalidUse=<jsp:output>ì€ í‘œì¤€ 문법ì—ì„œ 사용ë˜ì„œëŠ” 안ë©ë‹ˆë‹¤. +jsp.error.jspoutput.nonemptybody=<jsp:output>ì€ body를 í¬í•¨í•´ì„œëŠ” 안ë©ë‹ˆë‹¤. +jsp.error.jsproot.version.invalid=유효하지 ì•Šì€ ë²„ì „: [{0}]. 반드시 ë‹¤ìŒ ì¤‘ 하나여야 합니다: "1.2", "2.0", "2.1", "2.2", "2.3", "3.0", "3.1" +jsp.error.jsptext.badcontent='<'ê°€ <jsp:text>ì˜ body ë‚´ì— ì¡´ìž¬í•  ë•Œì—는 반드시 CDATA ë‚´ì— í¬í•¨ë˜ì–´ì•¼ 합니다. +jsp.error.lastModified=íŒŒì¼ [{0}]ì„(를) 위한 최종 변경 ì‹œê°„ì„ ê²°ì •í•  수 없습니다. +jsp.error.library.invalid=ë¼ì´ë¸ŒëŸ¬ë¦¬ [{0}]ì— ì˜í•˜ë©´, JSP 페ì´ì§€ê°€ 유효하지 않습니다: [{1}] +jsp.error.literal_with_void=Void 반환 íƒ€ìž…ì¸ deferred 메소드로서 ì •ì˜ëœ ì†ì„± [{0}]ì„(를) 위해, literal ê°’ì´ ì§€ì •ë˜ì—ˆìŠµë‹ˆë‹¤. JSP.2.3.4는 ì´ëŸ° 경우 literal ê°’ë“¤ì„ í—ˆìš©í•˜ì§€ 않습니다. +jsp.error.loadclass.taghandler=태그 [{1}]ì„(를) 위한 태그 핸들러 í´ëž˜ìŠ¤ [{0}]ì„(를) 로드할 수 없습니다. +jsp.error.location=í–‰: [{0}], ì—´: [{1}] +jsp.error.mandatory.attribute=[{0}]: 필수 ì†ì„± [{1}]ì´(ê°€) 없습니다. +jsp.error.missing.tagInfo=[{0}]ì„(를) 위한 TagInfo ê°ì²´ê°€ TLDì— ì—†ìŠµë‹ˆë‹¤. +jsp.error.missing_attribute=해당 TLD ë˜ëŠ” 태그 파ì¼ì— ì˜í•˜ë©´, ì†ì„± [{0}]ì€(는) 태그 [{1}]ì— í•„ìˆ˜ 사항입니다. +jsp.error.missing_var_or_varReader='var' ë˜ëŠ” 'varReader' ì†ì„±ì´ 없습니다. +jsp.error.namedAttribute.invalidUse=jsp:attribute는 반드시 표준 ë˜ëŠ” 커스텀 ì•¡ì…˜ì˜ í•˜ìœ„ 엘리먼트ì´ì–´ì•¼ 합니다. +jsp.error.needAlternateJavaEncoding=기본 ìžë°” ì¸ì½”딩 [{0}]ì€(는) ê·€í•˜ì˜ ìžë°” 플랫í¼ì—ì„œ 유효하지 않습니다. JspServletì˜ ''javaEncoding'' 파ë¼ë¯¸í„°ë¥¼ 통해, ëŒ€ì•ˆì  ì¸ì½”ë”©ì„ ì„¤ì •í•  수 있습니다. +jsp.error.negativeBufferSize=ë²„í¼ í¬ê¸°ê°€ ìŒìˆ˜ìž…니다. +jsp.error.negativeParameter=파ë¼ë¯¸í„° [{0}]ì€(는) ìŒìˆ˜ì—¬ì„œëŠ” 안ë©ë‹ˆë‹¤. +jsp.error.nested.jspattribute=jsp:attribute 표준 ì•¡ì…˜ì€ ë˜ ë‹¤ë¥¸ jsp:attribute 표준 ì•¡ì…˜ ë‚´ì— í¬í•¨ë  수 없습니다. +jsp.error.nested.jspbody=jsp:body 표준 ì•¡ì…˜ì€, ë˜ ë‹¤ë¥¸ jsp:body ë˜ëŠ” jsp:attribute 표준 ì•¡ì…˜ ë‚´ì— í¬í•¨ë  수 없습니다. +jsp.error.nested_jsproot=Nested <jsp:root> +jsp.error.no.jsp=JSP íŒŒì¼ [{0}]ì˜ ìœ„ì¹˜ë¥¼ ì•Œ 수 없습니다. +jsp.error.no.more.content=íŒŒì‹±ì´ ë” ìš”êµ¬ë˜ëŠ” ìƒí™©ì—ì„œ, 컨í…íŠ¸ì˜ ëì— ë„달했습니다: 태그 nesting 오류ì¼ê¹Œìš”? +jsp.error.no.scratch.dir=JSP ì—”ì§„ì— scratch 디렉토리가 설정ë˜ì§€ 않았습니다. ì´ ì»¨í…스트를 위해 "jsp.initparams=scratchdir="ì„ servlets.properties 파ì¼ì— 추가하십시오. +jsp.error.no.scriptlets=스í¬ë¦½íŒ… ì—˜ë¦¬ë¨¼íŠ¸ë“¤ì€ ( <%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) ì´ê³³ì—ì„œ 허용ë˜ì§€ 않습니다. +jsp.error.noFile=íŒŒì¼ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.noFunction=ì§€ì •ëœ prefix를 사용하여 function [{0}]ì˜ ìœ„ì¹˜ë¥¼ ê²°ì •í•  수 없습니다. +jsp.error.noFunctionMethod=í´ëž˜ìŠ¤ [{2}] ë‚´ì—ì„œ, function [{1}]ì„(를) 위한 메소드 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.noInstanceManager=ServletContextì— org.apache.tomcat.InstanceManagerê°€ 설정ë˜ì§€ 않았습니다. +jsp.error.non_null_tei_and_var_subelems=태그 [{0}]ì´(ê°€) 하나 ì´ìƒì˜ variable 하위 엘리먼트들과, 하나 ì´ìƒì˜ VariableInfoë“¤ì„ ë°˜í™˜í•˜ëŠ” TagExtraInfo í´ëž˜ìŠ¤ë¥¼ í¬í•¨í•˜ê³  있습니다. +jsp.error.not.in.template=JSP 템플릿 í…ìŠ¤íŠ¸ì˜ bodyì—ì„œ, [{0}]ì€(는) 허용ë˜ì§€ 않습니다. +jsp.error.nullArgument=ë„ ì•„ê·œë¨¼íŠ¸ +jsp.error.outputfolder=출력 í´ë”ê°€ ì—†ìŒ +jsp.error.outputfolder.detail=JSP 컴파ì¼ì— 필요한 출력 디렉토리 [{0}](ì„)를 ìƒì„±í•  수 없습니다. +jsp.error.overflow=오류: JSP ë²„í¼ ì˜¤ë²„í”Œë¡œìš° +jsp.error.page.conflict.autoflush=페ì´ì§€ 지시어: ''autoFlush''ê°€ 다른 값들로 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.buffer=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§„ ''buffer''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.contenttype=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''contentType''ì´ ì—¬ëŸ¬ 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.deferredsyntaxallowedasliteral=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''deferredSyntaxAllowedAsLiteral''ì´ ì—¬ëŸ¬ 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.errorOnELNotFound=페ì´ì§€ 지시어: 서로 다른 ê°’ì„ ê°–ëŠ” 복수 ê°œì˜ ''errorOnELNotFound'' ì†ì„±ë“¤ì„ í¬í•¨í•´ì„œëŠ” 안ë©ë‹ˆë‹¤ (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]). +jsp.error.page.conflict.errorpage=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''errorPage''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.extends=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''extends''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.info=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''info''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.iselignored=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''isELIgnored''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.iserrorpage=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''isErrorPage''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.isthreadsafe=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''isThreadSafe''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.language=페ì´ì§€ 지시어: 다른 값들로 ''language''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.session=페ì´ì§€ 지시어: ''session''ì´ ë‹¤ë¥¸ 값들로 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.conflict.trimdirectivewhitespaces=페ì´ì§€ 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''trimDirectiveWhitespaces''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.page.invalid.buffer=페ì´ì§€ 지시어: 유효하지 ì•Šì€ ë²„í¼ ì„¤ì • ê°’ +jsp.error.page.invalid.deferredsyntaxallowedasliteral=페ì´ì§€ 지시어: deferredSyntaxAllowedAsLiteralì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’ +jsp.error.page.invalid.errorOnELNotFound=페ì´ì§€ 지시어: errorOnELNotFound ì†ì„±ì— 유효하지 ì•Šì€ ê°’ +jsp.error.page.invalid.import=페ì´ì§€ 지시어: ìž„í¬íŠ¸ë¥¼ 위한 유효한 ê°’ì´ ì•„ë‹™ë‹ˆë‹¤. +jsp.error.page.invalid.iselignored=페ì´ì§€ 지시어: isELIgnoredì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’ +jsp.error.page.invalid.iserrorpage=페ì´ì§€ 지시어: isErrorPageì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’ +jsp.error.page.invalid.isthreadsafe=페ì´ì§€ 지시어: isThreadSafeì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’ +jsp.error.page.invalid.scope=유효하지 ì•Šì€ scope +jsp.error.page.invalid.session=페ì´ì§€ 지시어: ì„¸ì…˜ì„ ìœ„í•´ 유효하지 ì•Šì€ ê°’ +jsp.error.page.invalid.trimdirectivewhitespaces=페ì´ì§€ 지시어: trimDirectiveWhitespacesì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’ +jsp.error.page.language.nonjava=페ì´ì§€ 지시어: 유효하지 ì•Šì€ language ì†ì„±ìž…니다. +jsp.error.page.multi.pageencoding=페ì´ì§€ 지시어는 여러 ê°œì˜ pageencodingì„ ê°€ì ¸ì„œëŠ” 안ë©ë‹ˆë‹¤. +jsp.error.page.noSession=ì–´ë–¤ 세션ì—ë„ ì°¸ì—¬í•˜ì§€ 않는 페ì´ì§€ì—ì„œ 세션 scopeì— ì ‘ê·¼í•  수 없습니다. +jsp.error.page.nullThrowable=ë„ ì˜ˆì™¸ ë°œìƒ +jsp.error.page.sessionRequired=페ì´ì§€ëŠ” ì„¸ì…˜ì„ í•„ìš”ë¡œ 하나, 가용한 ì„¸ì…˜ì´ ì—†ìŠµë‹ˆë‹¤. +jsp.error.param.invalidUse=jsp:param ì•¡ì…˜ì€, jsp:include, jsp:forward, ë˜ëŠ” jsp:params 엘리먼트들 외부ì—ì„œ 사용ë˜ì„œëŠ” 안ë©ë‹ˆë‹¤. +jsp.error.paramexpected="name"ê³¼ "value" ì†ì„±ë“¤ì„ í¬í•¨í•œ "jsp:param" 표준 ì•¡ì…˜ì´ ìš”êµ¬ë©ë‹ˆë‹¤. +jsp.error.params.emptyBody=jsp:params는 반드시 ì ì–´ë„ 하나 ì´ìƒì˜ jsp:paramì„ í¬í•¨í•´ì•¼ 합니다. +jsp.error.params.invalidUse=jsp:params는 반드시 jsp:pluginì˜ ì§ê³„ ìžì‹ì´ì–´ì•¼ 합니다. +jsp.error.parse.error.in.TLD=태그 ë¼ì´ë¸ŒëŸ¬ë¦¬ descriptor ë‚´ì—ì„œ 파싱 오류 ë°œìƒ: [{0}] +jsp.error.parse.xml=[{0}] 파ì¼ì—ì„œ XML 파싱 오류 ë°œìƒ +jsp.error.parse.xml.line=íŒŒì¼ [{0}]ì—ì„œ XML 파싱 오류 ë°œìƒ: (í–‰: [{1}], ì—´: [{2}]) +jsp.error.parse.xml.scripting.invalid.body=[{0}] ì—˜ë¦¬ë¨¼íŠ¸ì˜ body ë‚´ì—서는, 반드시 ì–´ë– í•œ XML ì—˜ë¦¬ë¨¼íŠ¸ë“¤ë„ í¬í•¨í•´ì„œëŠ” 안ë©ë‹ˆë‹¤. +jsp.error.plugin.badtype=jsp:plugin: ë‚´ì˜ 'type' ì†ì„±ì„ 위해 불허ë˜ëŠ” 값입니다. 반드시 'bean' ë˜ëŠ” 'applet'ì´ì–´ì•¼ 합니다. +jsp.error.plugin.nocode=jsp:pluginì— codeê°€ ì„ ì–¸ ì•ˆë¨ +jsp.error.plugin.notype=jsp:pluginì— typeì´ ì„ ì–¸ë˜ì§€ 않았습니다. +jsp.error.precompilation=JSP [{0}]ì„(를) 사전 ì»´íŒŒì¼ í•  수 없었습니다. +jsp.error.precompilation.parameter=사전 ì»´íŒŒì¼ ìš”ì²­ 파ë¼ë¯¸í„° [{0}]ì„(를) [{1}](으)ë¡œ 설정할 수 없습니다. +jsp.error.prefix.refined=현재 범위ì—ì„œ ì´ë¯¸ [{2}](으)로서 ì •ì˜ë˜ì–´ 있는ë°, prefix를 [{0}]ì—ì„œ [{1}](으)ë¡œ 재정ì˜í•˜ë ¤ëŠ” ì‹œë„입니다. +jsp.error.prefix.use_before_dcl=ì´ íƒœê·¸ ì§€ì‹œì–´ì— ì§€ì •ëœ prefix [{0}]ì€(는), ì´ì „ì— íŒŒì¼ [{1}] ë‚´ì˜ [{2}] í–‰ì— ìžˆëŠ” ì•¡ì…˜ì— ì˜í•´ ì‚¬ìš©ëœ ì ì´ 있습니다. +jsp.error.prolog_config_encoding_mismatch=XML 프롤로그 [{0}]ì— ì§€ì •ëœ íŽ˜ì´ì§€ ì¸ì½”딩ì´, jsp-property-group [{1}]ì— ì§€ì •ëœ ê²ƒê³¼ 다릅니다. +jsp.error.prolog_pagedir_encoding_mismatch=XML 프롤로그 [{0}]ì— ì§€ì •ëœ íŽ˜ì´ì§€ ì¸ì½”딩ì´, 페ì´ì§€ 지시어 [{1}]ì— ì§€ì •ëœ ê²ƒê³¼ 다릅니다. +jsp.error.quotes.unterminated=종료ë˜ì§€ ì•Šì€ ì¸ìš©ë¶€ë“¤ +jsp.error.readContent=기대ë˜ëŠ” ê¸¸ì´ [{0}] ë§Œí¼ ì½ì„ 수 없습니다. +jsp.error.reload=ì„œë¸”ë¦¿ì„ ë‹¤ì‹œ 로드하지 못했습니다. +jsp.error.scripting.variable.missing_name=ì†ì„± [{0}](으)로부터 스í¬ë¦½íŒ… 변수 ì´ë¦„ì„ ê²°ì •í•  수 없습니다. +jsp.error.security=컨í…스트를 위한 보안 초기화 ìž‘ì—…ì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. +jsp.error.securityPreload=í´ëž˜ìŠ¤ë¥¼ ì‚¬ì „ì— ë¡œë“œí•˜ëŠ” 중 오류 ë°œìƒ +jsp.error.servlet.destroy.failed=JSP 페ì´ì§€ë¥¼ 위한 Servlet.destroy() 호출 중 예외 ë°œìƒ +jsp.error.servlet.invalid.method=JSPë“¤ì€ ì˜¤ì§ GET, POST ë˜ëŠ” HEAD ë©”ì†Œë“œë§Œì„ í—ˆìš©í•©ë‹ˆë‹¤. Jasper는 OPTIONS 메소드 ë˜í•œ 허용합니다. +jsp.error.setLastModified=íŒŒì¼ [{0}]ì˜ ìµœì¢… 변경 ì‹œê°„ì„ ì„¤ì •í•  수 없습니다. +jsp.error.signature.classnotfound=TLD ë‚´ì— function [{1}]ì„ ìœ„í•´ ì§€ì •ëœ ë©”ì†Œë“œ signatureì— í¬í•¨ëœ í´ëž˜ìŠ¤ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. [{2}] +jsp.error.simpletag.badbodycontent=í´ëž˜ìŠ¤ [{0}]ì„(를) 위한 TLDê°€ SimpleTag로서 유효하지 ì•Šì€ body-content (JSP)를 지정하고 있습니다. +jsp.error.single.line.number=JSP íŒŒì¼ [{1}]ì˜ [{0}] í–‰ì—ì„œ 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. +jsp.error.stream.close.failed=스트림 닫기 실패 +jsp.error.stream.closed=ìŠ¤íŠ¸ë¦¼ì´ ë‹«í˜”ìŠµë‹ˆë‹¤. +jsp.error.string_interpreter_class.instantiation=StringInterpreter í´ëž˜ìŠ¤ [{0}]ì„(를) 찾지 못하거나 ìƒì„±í•  수 없습니다. +jsp.error.tag.conflict.attr=태그 지시어: 서로 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ì†ì„± [{0}]ì´(ê°€) 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{1}], ì‹ ê·œ ê°’: [{2}]) +jsp.error.tag.conflict.deferredsyntaxallowedasliteral=태그 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” "deferredSyntaxAllowedAsLiteral"ì´ ì—¬ëŸ¬ 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤ (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.tag.conflict.errorOnELNotFound=태그 지시어: 서로 다른 ê°’ì„ ê°–ëŠ” 복수 ê°œì˜ ''errorOnELNotFound'' ì†ì„±ë“¤ì„ í¬í•¨í•´ì„œëŠ” 안ë©ë‹ˆë‹¤ (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]). +jsp.error.tag.conflict.iselignored=태그 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''isELIgnored''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.tag.conflict.language=태그 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''language''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.tag.conflict.trimdirectivewhitespaces=태그 지시어: 다른 ê°’ë“¤ì„ ê°€ì§€ëŠ” ''trimDirectiveWhitespaces''ê°€ 여러 번 나타나는 ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. (ì´ì „ ê°’: [{0}], ì‹ ê·œ ê°’: [{1}]) +jsp.error.tag.invalid.deferredsyntaxallowedasliteral=태그 지시어: deferredSyntaxAllowedAsLiteralì„ ìœ„í•´ 유효하지 ì•Šì€ ê°’ +jsp.error.tag.invalid.errorOnELNotFound=태그 지시어: errorOnELNotFound ì†ì„±ì— 유효하지 ì•Šì€ ê°’ +jsp.error.tag.invalid.iselignored=태그 지시어: isELIgnoredì„ ìœ„í•´ 유효하지 ì•Šì€ ê°’ +jsp.error.tag.invalid.trimdirectivewhitespaces=태그 지시어: trimDirectiveWhitespacesì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’ +jsp.error.tag.language.nonjava=태그 지시어: 유효하지 ì•Šì€ language ì†ì„± +jsp.error.tag.multi.pageencoding=태그 지시어ì—ì„œ pageEncodingì´ ì—¬ëŸ¬ 번 나타나서는 안ë©ë‹ˆë‹¤. +jsp.error.tagdirective.badbodycontent=태그 지시어 ë‚´ì— ìœ íš¨í•˜ì§€ ì•Šì€ body-content: [{0}] +jsp.error.tagfile.badSuffix=태그 íŒŒì¼ ê²½ë¡œ [{0}]ì´(ê°€) ".tag"ë¡œ ë나지 않습니다. +jsp.error.tagfile.illegalPath=불허ë˜ëŠ” 태그 íŒŒì¼ ê²½ë¡œ: [{0}]. 경로는 반드시 "/WEB-INF/tags" ë˜ëŠ” "/META-INF/tags"ë¡œ 시작해야 합니다. +jsp.error.tagfile.missingPath=태그 파ì¼ì— 대한 경로가 지정ë˜ì§€ 않았습니다. +jsp.error.tagfile.nameFrom.badAttribute=attribute 지시어는 ([{1}] í–‰ì— ì„ ì–¸ë˜ê³  ê·¸ name ì†ì„±ì´ [{0}]ì´ë©° ê·¸ ê°’ì€ name-from-attributeì˜ ê°’ì¸) 반드시 java.lang.String 타입ì´ì–´ì•¼ 하고, "required"여야 하며, "rtexprvalue"ê°€ ë˜ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +jsp.error.tagfile.nameFrom.noAttribute=ì´ name-from-attribute ì†ì„± ê°’, [{0}]ì„(를) 가진 name ì†ì„±ì„ 가진 attribute 지시어를 ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.tagfile.nameNotUnique=[{2}] í–‰ì— ìžˆëŠ” [{0}] ê°’ê³¼ [{1}] ê°’ì´ ë™ì¼í•©ë‹ˆë‹¤. +jsp.error.taglibDirective.absUriCannotBeResolved=절대 URIì¸ [{0}]ì„(를), web.xml ë˜ëŠ” ì´ ì• í”Œë¦¬ì¼€ì´ì…˜ê³¼ 함께 ë°°ì¹˜ëœ JAR íŒŒì¼ ë‚´ì—ì„œ ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.taglibDirective.both_uri_and_tagdir='uri'와 'tagdir' ì†ì„±, 둘 다 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +jsp.error.taglibDirective.missing.location='uri'나 'tagdir' 중 ì–´ëŠ ê²ƒë„ ì§€ì •ë˜ì§€ 않았습니다. +jsp.error.taglibDirective.uriInvalid=태그 ë¼ì´ë¸ŒëŸ¬ë¦¬ë¥¼ 위해 ì œê³µëœ URI [{0}]ì´(ê°€) 유효한 URIê°€ 아닙니다. +jsp.error.tei.invalid.attributes=[{0}]ì„(를) 위한 TagExtraInfo로부터 Validation 오류 메시지입니다. +jsp.error.teiclass.instantiation=TagExtraInfo í´ëž˜ìŠ¤ [{0}]ì„(를) 로드하거나 ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•˜ì§€ 못했습니다. +jsp.error.text.has_subelement=<jsp:text>는 하위 ì—˜ë¦¬ë¨¼íŠ¸ë“¤ì„ ê°€ì ¸ì„œëŠ” 안ë©ë‹ˆë‹¤. +jsp.error.tld.fn.duplicate.name=태그 ë¼ì´ë¸ŒëŸ¬ë¦¬ [{1}] ë‚´ì—, ì¤‘ë³µëœ function ì´ë¦„ [{0}]ì´(ê°€) 존재합니다. +jsp.error.tld.fn.invalid.signature=TLD ë‚´ì— function signature로서 유효하지 ì•Šì€ ë¬¸ë²•ìž…ë‹ˆë‹¤. 태그 ë¼ì´ë¸ŒëŸ¬ë¦¬: [{0}], Function: [{1}] +jsp.error.tld.invalid_tld_file=유효하지 ì•Šì€ TLD 파ì¼: [{0}]. 보다 ìƒì„¸í•œ 정보는 JSP 스펙 7.3.1 ìž¥ì„ ì°¸ì¡°í•˜ì‹­ì‹œì˜¤. +jsp.error.tld.mandatory.element.missing=TLD íŒŒì¼ [{1}] ë‚´ì—ì„œ, 필수 í•­ëª©ì¸ TLD 엘리먼트 [{0}]ì´(ê°€) 존재하지 않거나 비어 있습니다. +jsp.error.tld.missing=URI [{1}]ì„(를) 위한 태그ë¼ì´ë¸ŒëŸ¬ë¦¬ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.tld.missing_jar=TLD를 í¬í•¨í•˜ëŠ” JAR 리소스 [{0}]ì´(ê°€) 없습니다. +jsp.error.tld.unable_to_get_jar=TLD [{1}]ì„(를) í¬í•¨í•œ JAR 리소스 [{0}]ì„(를) 구할 수 없습니다. +jsp.error.tld.url=올바르지 ì•Šì€ TLD URL [{0}] +jsp.error.tlv.invalid.page=[{1}] ë‚´ì˜ [{0}]ì„(를) 위한 TagLibraryValidator로부터 Validation 오류 메시지들 +jsp.error.tlvclass.instantiation=TagLibraryValidator í´ëž˜ìŠ¤ [{0}]ì„(를) 로드하거나 ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•˜ì§€ 못했습니다. +jsp.error.unable.compile=JSP를 위한 í´ëž˜ìŠ¤ë¥¼ 컴파ì¼í•  수 없습니다. +jsp.error.unable.deleteClassFile=í´ëž˜ìŠ¤ 파ì¼ì„ 삭제할 수 없습니다. +jsp.error.unable.getType=[{0}](으)로부터 íƒ€ìž…ì„ ì¶”ì¶œí•  수 없습니다. +jsp.error.unable.load=JSP를 위한 í´ëž˜ìŠ¤ë¥¼ 로드할 수 없습니다. +jsp.error.unable.renameClassFile=í´ëž˜ìŠ¤ 파ì¼ì˜ ì´ë¦„ì„ [{0}]ì—ì„œ [{1}] (으)ë¡œ 바꿀 수 없습니다. +jsp.error.unable.to_find_method=ì†ì„± [{0}]ì„(를) 위한 setter 메소드를 ì°¾ì„ ìˆ˜ 없습니다. +jsp.error.unavailable=JSPê°€ 가용하지 ì•Šì€ ìƒíƒœë¡œ 표시ë˜ì–´ 있습니다. +jsp.error.unbalanced.endtag=종료 태그 "</{0}"ì´(ê°€) 시작 태그와 맞지 않습니다. +jsp.error.undeclared_namespace=커스텀 태그가, ì„ ì–¸ë˜ì§€ ì•Šì€ ë„¤ìž„ìŠ¤íŽ˜ì´ìŠ¤ [{0}]ì„(를) í¬í•¨í–ˆìŠµë‹ˆë‹¤. +jsp.error.unexpectedTag=예기치 ì•Šì€ íƒœê·¸ [{0}] +jsp.error.unknown_attribute_type=ì†ì„± [{0}]ì„(를) 위한 ì†ì„± 타입으로 ì•Œ 수 없는 ê°’ [{1}]ì´(ê°€) 설정ë˜ì—ˆìŠµë‹ˆë‹¤. +jsp.error.unsupported.encoding=지ì›ë˜ì§€ 않는 ì¸ì½”딩: [{0}] +jsp.error.unterminated=종료ë˜ì§€ ì•Šì€ [{0}] 태그 +jsp.error.usebean.duplicate=useBean: ì¤‘ë³µëœ bean ì´ë¦„: [{0}] +jsp.error.usebean.noSession=JSP 페ì´ì§€ê°€ ì„¸ì…˜ì— ì°¸ì—¬í•˜ì§€ 않기로 (page 지시어를 통해) ì„ ì–¸ë˜ì–´ ìžˆì„ ë•Œ, useBeanì´ session scopeì„ ì‚¬ìš©í•˜ëŠ” ê²ƒì€ ë¶ˆí—ˆë©ë‹ˆë‹¤. +jsp.error.var_and_varReader='var'ë˜ëŠ” 'varReader' 중 ì˜¤ì§ í•˜ë‚˜ë§Œ 지정할 수 있습니다. +jsp.error.variable.alias=variable 지시어 ë‚´ì—ì„œ, name-from-attributeê³¼ alias ì†ì„±, 둘 다 지정하거나, 둘 다 지정하지 ë§ì•„야 합니다. +jsp.error.variable.both.name=variable 지시어 ë‚´ì—ì„œ, name-givenê³¼ name-from-attribute ì†ì„±, 둘 다를 지정할 수 없습니다. +jsp.error.variable.either.name=name-given ë˜ëŠ” name-from-attribute ì†ì„± 둘 중 하나는, variable 지시어 ë‚´ì—ì„œ 반드시 지정ë˜ì–´ì•¼ 합니다. +jsp.error.xml.badStandardAction=유효하지 ì•Šì€ í‘œì¤€ ì•¡ì…˜: [{0}] +jsp.error.xml.bad_tag=URI [{1}]와(ê³¼) ì—°ê´€ëœ íƒœê·¸ ë¼ì´ë¸ŒëŸ¬ë¦¬ ë‚´ì—, 태그 [{0}]ì´(ê°€) ì •ì˜ë˜ì§€ 않았습니다. +jsp.exception=í–‰ [{1}]ì—ì„œ [{0}]ì„(를) 처리하는 중 예외 ë°œìƒ +jsp.info.ignoreSetting=SecurityManagerê°€ 사용 가능 ìƒíƒœë¡œ 설정ë˜ì—ˆê¸° 때문ì—, [{1}]ì˜ [{0}]ì„(를) 위한 ì„¤ì •ì€ ë¬´ì‹œë©ë‹ˆë‹¤. +jsp.message.dont.modify.servlets=중요사항: 코드 ìƒì„±ëœ ì„œë¸”ë¦¿ë“¤ì„ ë³€ê²½í•˜ì§€ 마시오. +jsp.message.jsp_added=컨í…스트 [{1}]ì˜ íì—, 경로 [{0}]ì„(를) 위한 JSP를 추가합니다. +jsp.message.jsp_queue_created=컨í…스트 [{1}]ì„(를) 위해 길ì´ê°€ [{0}]ì¸ JSP í를 ìƒì„±í–ˆìŠµë‹ˆë‹¤. +jsp.message.jsp_queue_update=컨í…스트 [{1}]ì˜ íì— ì¡´ìž¬í•˜ëŠ” JSP 서블릿(경로: [{0}])ì„ ë³€ê²½í•©ë‹ˆë‹¤. +jsp.message.jsp_removed_excess=컨í…스트 [{1}]ì˜ í로부터, ê³¼ë„하게 오래 수행ë˜ëŠ” JSP (경로: [{0}]) 페ì´ì§€ë¥¼ 제거합니다. +jsp.message.jsp_removed_idle=컨í…스트 [{1}] ë‚´ì˜ ê²½ë¡œ [{0}]ì„(를) 위한 JSPê°€ [{2}] 밀리초 ë™ì•ˆ 유휴 ìƒíƒœì— 있었으므로 제거합니다. +jsp.message.jsp_unload_check=컨í…스트 [{0}] ë‚´ì—ì„œ JSPë“¤ì´ ì–¸ë¡œë“œë˜ì–´ì•¼ 하는지 ì ê²€í•©ë‹ˆë‹¤. JSP 개수: [{1}], í 길ì´: [{2}] +jsp.message.parent_class_loader_is=부모 í´ëž˜ìŠ¤ë¡œë”: [{0}]] +jsp.message.scratch.dir.is=JSP ì—”ì§„ì„ ìœ„í•œ Scratch 디렉토리: [{0}] +jsp.tldCache.noTldInDir=디렉토리 [{0}] ë‚´ì—ì„œ TLD 파ì¼ë“¤ì´ 발견ë˜ì§€ 않았습니다. +jsp.tldCache.noTldInJar=[{0}]ì—ì„œ TLD 파ì¼ë“¤ì„ ì°¾ì„ ìˆ˜ 없습니다. CATALINA_BASE/conf/catalina.properties íŒŒì¼ ë‚´ì˜ tomcat.util.scan.StandardJarScanFilter.jarsToSkip 프로í¼í‹°ì—, 해당 JAR를 추가하는 ê²ƒì„ ê³ ë ¤í•˜ì‹­ì‹œì˜¤. +jsp.tldCache.noTldInResourcePath=리소스 경로 [{0}]ì—ì„œ TLD 파ì¼ë“¤ì„ ì°¾ì„ ìˆ˜ 없습니다. +jsp.tldCache.noTldSummary=ì ì–´ë„ í•˜ë‚˜ì˜ JARê°€ TLDë“¤ì„ ì°¾ê¸° 위해 스캔ë˜ì—ˆìœ¼ë‚˜ 아무 ê²ƒë„ ì°¾ì§€ 못했습니다. 스캔했으나 TLDê°€ 없는 JARë“¤ì˜ ì „ì²´ 목ë¡ì„ 보시려면, 로그 ë ˆë²¨ì„ ë””ë²„ê·¸ 레벨로 설정하십시오. 스캔 과정ì—ì„œ 불필요한 JARë“¤ì„ ê±´ë„ˆë›°ë©´, 시스템 시작 시간과 JSP ì»´íŒŒì¼ ì‹œê°„ì„ ë‹¨ì¶•ì‹œí‚¬ 수 있습니다. +jsp.tldCache.tldInDir=TLD 파ì¼ë“¤ì´ 디렉토리 [{0}] ë‚´ì—ì„œ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. +jsp.tldCache.tldInJar=JAR [{0}] ë‚´ì—ì„œ TLD 파ì¼ë“¤ì„ 찾지 못했습니다. +jsp.tldCache.tldInResourcePath=리소스 경로 [{0}]ì—ì„œ TLD 파ì¼ë“¤ì´ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. +jsp.warning.bad.urlpattern.propertygroup=web.xml ë‚´ url-pattern 하위 ì—˜ë¦¬ë¨¼íŠ¸ì— ìž˜ëª»ëœ ê°’: [{0}] +jsp.warning.checkInterval=경고: initParamì¸ checkIntervalì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "300" 초를 사용할 것입니다. +jsp.warning.classDebugInfo=경고: initParamì¸ classdebuginfoì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용할 것입니다. +jsp.warning.classpathUrl=í´ëž˜ìŠ¤íŒ¨ìŠ¤ ë‚´ì— ìœ íš¨í•˜ì§€ ì•Šì€ URLì´ ë°œê²¬ë¨. ì´ URLì€ ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +jsp.warning.compiler.classfile.delete.fail=ìƒì„±ëœ í´ëž˜ìŠ¤ íŒŒì¼ [{0}]ì„(를) 삭제하지 못했습니다. +jsp.warning.compiler.classfile.delete.fail.unknown=코드 ìƒì„±ëœ í´ëž˜ìŠ¤ 파ì¼(들)ì„ ì‚­ì œí•˜ì§€ 못했습니다. +jsp.warning.compiler.javafile.delete.fail=ìƒì„±ëœ ìžë°” íŒŒì¼ [{0}]ì„(를) 삭제하지 못했습니다. +jsp.warning.development=경고: initParamì¸ developmentì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "true"를 사용할 것입니다. +jsp.warning.displaySourceFragment=경고: initParamì¸ displaySourceFragmentì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "true"를 사용할 것입니다. +jsp.warning.dumpSmap=경고: initParamì¸ dumpSmapì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용할 것입니다. +jsp.warning.enablePooling=주ì˜: initParamì¸ enablePoolingì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "true"를 사용할 것입니다. +jsp.warning.engineOptionsClass=엔진 ì˜µì…˜ë“¤ì˜ í´ëž˜ìŠ¤ [{0}]ì„(를) 로드하지 못했습니다. +jsp.warning.fork=경고: initParamì¸ forkì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "true"를 사용할 것입니다. +jsp.warning.genchararray=경고: initParamì¸ genStringAsCharArrayì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용할 것입니다. +jsp.warning.isThreadSafe=경고: [{0}]ì—ì„œ 사용ë˜ê³  있는 "isThreadSafe" 페ì´ì§€ 지시어 ì†ì„±ì€ ë” ì´ìƒ 지ì›ë˜ì§€ ì•Šì„ ê²ƒì´ë©° JSP 스펙 버전 4.0ì—ì„œ 완전히 사ë¼ì§ˆ 것입니다. +jsp.warning.jspIdleTimeout=경고: initParamì¸ jspIdleTimeoutì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본값 "-1"ì„ ì‚¬ìš©í•  것입니다. +jsp.warning.keepgen=경고: initParamì¸ keepgeneratedì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용할 것입니다. +jsp.warning.loadSmap=í´ëž˜ìŠ¤ [{0}]ì„(를) 위한 SMAP ë°ì´í„°ë¥¼ 로드할 수 없습니다. +jsp.warning.mappedFile=경고: initParamì¸ mappedFileì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용할 것입니다. +jsp.warning.maxLoadedJsps=경고: initParamì¸ maxLoadedJspsì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "-1"ì„ ì‚¬ìš©í•  것입니다. +jsp.warning.modificationTestInterval=경고: initParamì¸ modificationTestIntervalì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "4"초를 사용할 것입니다. +jsp.warning.noJarScanner=경고: ServletContextì— org.apache.tomcat.JarScannerê°€ 설정ë˜ì§€ 않았습니다. 기본 JarScanner 구현 ê°ì²´ë¥¼ 사용할 것입니다. +jsp.warning.poolTagsWithExtends=주ì˜: initParamì¸ poolTagsWithExtendsì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용합니다. +jsp.warning.quoteAttributeEL=경고: initParamì¸ quoteAttributeELì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용할 것입니다. +jsp.warning.recompileOnFail=경고: initParamì¸ recompileOnFailì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ false를 사용할 것입니다. +jsp.warning.strictGetProperty=주ì˜: initParamì¸ strictGetPropertyì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "true"를 사용합니다. +jsp.warning.strictQuoteEscaping=경고: initParamì¸ strictQuoteEscapingì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "true"를 사용할 것입니다. +jsp.warning.strictWhitespace=주ì˜: initParamì¸ strictWhitespaceì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "true"를 사용합니다. +jsp.warning.suppressSmap=경고: initParamì¸ suppressSmapì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. ê¸°ë³¸ê°’ì¸ "false"를 사용할 것입니다. +jsp.warning.tagPreDestroy=[{0}]ì˜ íƒœê·¸ ì¸ìŠ¤í„´ìŠ¤ì— 대하여, preDestroy를 처리 중 오류 ë°œìƒ +jsp.warning.tagRelease=[{0}]ì˜ íƒœê·¸ ì¸ìŠ¤í„´ìŠ¤ì— 대해 release를 처리하는 중 오류 ë°œìƒ +jsp.warning.trimspaces=경고: initParamì¸ trimSpacesì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본값 "false"를 사용할 것입니다. +jsp.warning.unknown.sourceVM=ì•Œ 수 없는 Source VM [{0}]ì€(는) 무시ë©ë‹ˆë‹¤. +jsp.warning.unknown.targetVM=ì•Œ 수 없는 target VM [{0}]ì€(는) 무시ë©ë‹ˆë‹¤. +jsp.warning.unsupported.sourceVM=ìš”ì²­ëœ ì†ŒìŠ¤ VM [{0}]ì€(는) 지ì›ë˜ì§€ 않습니다. [{1}]ì„(를) 사용합니다. +jsp.warning.unsupported.targetVM=ìš”ì²­ëœ ëŒ€ìƒ VM [{0}]ì€(는) 지ì›ë˜ì§€ 않습니다. [{1}]ì„(를) 사용합니다. +jsp.warning.useInstanceManagerForTags=주ì˜: initParamì¸ useInstanceManagerForTagsì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용합니다. +jsp.warning.xpoweredBy=경고: initParamì¸ xpoweredByì— ìœ íš¨í•˜ì§€ ì•Šì€ ê°’. 기본 ê°’ì¸ "false"를 사용할 것입니다. + +jspc.delete.fail=íŒŒì¼ [{0}]ì„(를) 삭제하지 못했습니다. +jspc.error.compilation=ì»´íŒŒì¼ ì˜¤ë¥˜ +jspc.error.fileDoesNotExist=íŒŒì¼ ì•„ê·œë¨¼íŠ¸ [{0}]ì´(ê°€) 존재하지 않습니다. +jspc.error.generalException=오류: íŒŒì¼ [{0}]ì´(ê°€) 다ìŒì˜ ì¼ë°˜ì ì¸ 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤: +jspc.error.invalidFragment=웹 fragment들 ë‚´ì˜ ì˜¤ë¥˜ë“¤ë¡œ ì¸í•˜ì—¬, 사전 컴파ì¼ì„ 중단합니다. +jspc.error.invalidWebXml=web.xml ë‚´ì˜ ì˜¤ë¥˜ë“¤ë¡œ ì¸í•´, 사전 컴파ì¼ì„ 중단합니다. +jspc.error.minThreadCount=ì ì–´ë„ 하나 ì´ìƒì˜ 쓰레드가 있어야만 하는ë°, 쓰레드 개수가 [{0}]입니다. +jspc.error.parseThreadCount=쓰레드 개수 [{0}]ì„(를) 파싱할 수 없습니다. +jspc.error.unknownOption=ì¸ì‹ë˜ì§€ 않는 옵션 [{0}]. ë„움ë§ì„ 보시려면 -help ì˜µì…˜ì„ ì‚¬ìš©í•˜ì‹­ì‹œì˜¤. +jspc.errorCount=오류 개수: [{0}] +jspc.generation.result=[{0}]ê°œì˜ ì˜¤ë¥˜ë¥¼ ë°œìƒì‹œí‚¤ë©°, 코드 ìƒì„±ì´ [{1}] ë°€ë¦¬ì´ˆì— ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. +jspc.implicit.uriRoot=uriRootê°€ 묵시ì ìœ¼ë¡œ [{0}](으)ë¡œ ì„¤ì •ë¨ +jspc.usage=사용법: jspc [--] \n\ +jsp files 아규먼트는 다ìŒê³¼ ê°™ìŒ:\n\ +\ -webapp 웹 애플리케ì´ì…˜ì„ í¬í•¨í•œ 디렉토리로서, ê·¸ 안ì˜\n\ +\ JSP 페ì´ì§€ë“¤ì´ 재귀ì ìœ¼ë¡œ ì²˜ë¦¬ë  ê²ƒìž„\n\ +ë˜ëŠ” ë³µìˆ˜ì˜ ë‹¤ìŒ ì•„ê·œë¨¼íŠ¸ë“¤:\n\ +\ JSP 페ì´ì§€ë¡œì„œ 파싱ë˜ì–´ì•¼ í•  파ì¼\n\ +다ìŒê³¼ ê°™ì€ optionsì´ í¬í•¨ë  수 있ìŒ:\n\ +\ -help ì´ ë„움ë§ì„ 출력\n\ +\ -v Verbose 모드\n\ +\ -d 출력 디렉토리 (ê¸°ë³¸ê°’ì€ -Djava.io.tmpdir)\n\ +\ -l 실패 ì‹œ JSP 페ì´ì§€ ì´ë¦„ì„ ì¶œë ¥\n\ +\ -s 성공 ì‹œ JSP 페ì´ì§€ ì´ë¦„ì„ ì¶œë ¥\n\ +\ -p 타겟 패키지 ì´ë¦„ (ê¸°ë³¸ê°’ì€ org.apache.jsp)\n\ +\ -c 타겟 í´ëž˜ìŠ¤ ì´ë¦„ (첫 JSP 페ì´ì§€ë§Œ ì ìš©)\n\ +\ -mapped JSP 페ì´ì§€ ë‚´ì˜ ê° HTML 행마다 write() 호출 코드 ìƒì„±\n\ +\ -die[#] ì‹¬ê° ì˜¤ë¥˜ë“¤ì— ëŒ€í•´ 프로그램 오류 반환 코드(#) ìƒì„±\n\ +\ -uribase JSP 페ì´ì§€ë“¤ì— 대한 uri ë² ì´ìŠ¤ 디렉토리\n\ +\ (ê¸°ë³¸ê°’ì€ "/")\n\ +\ -uriroot -webappê³¼ ë™ì¼\n\ +\ -compile ìƒì„±ëœ ì„œë¸”ë¦¿ë“¤ì„ ì»´íŒŒì¼í•¨\n\ +\ -failFast 최초 ì»´íŒŒì¼ ì˜¤ë¥˜ ì‹œ 중지함\n\ +\ -webinc íŒŒì¼ ë‚´ì— partial servlet mappings ìƒì„±\n\ +\ -webfrg 완전한 web-fragment.xml 파ì¼ì„ ìƒì„±\n\ +\ -webxml íŒŒì¼ ë‚´ì— ì™„ì „í•œ web.xmlì„ ìƒì„±\n\ +\ -webxmlencoding web.xmlì„ ì½ê³  쓰기 위한 ì¸ì½”딩 문ìžì…‹ 설정\n\ +\ (ê¸°ë³¸ê°’ì€ UTF-8)\n\ +\ -addwebxmlmappings ìƒì„±ëœ web.xml fragment를 우리가 처리하고 있는 JSP 페ì´ì§€ë“¤ì´ 처리ëœ\n\ +\ 해당 web-appì˜ web.xml ì— ë³‘í•©í•¨.\n\ +\ -ieplugin Internet Explorer를 위한 ìžë°” í”ŒëŸ¬ê·¸ì¸ classid\n\ +\ -classpath java.class.path 시스템 프로í¼í‹°ë¥¼ 오버ë¼ì´ë“œ\n\ +\ -xpoweredBy X-Powered-By ì‘답 í—¤ë”를 추가\n\ +\ -trimSpaces [single] ì „ì ìœ¼ë¡œ 공백 문ìžì—´ë¡œ ì´ë£¨ì–´ì§„ 템플릿 í…스트를 제거\n\ +\ (ë§Œì¼ "single"ì¸ ê²½ìš°, 해당 í…스트를 ë‹¨ì¼ ìŠ¤íŽ˜ì´ìŠ¤ 문ìžë¡œ 대체)\n\ +\ -javaEncoding ìžë°” í´ëž˜ìŠ¤ë“¤ì˜ ì¸ì½”딩 문ìžì…‹ì„ 설정 (ê¸°ë³¸ê°’ì€ UTF-8)\n\ +\ -source ìžë°” 컴파ì¼ëŸ¬ì— -source 아규먼트 설정 (ê¸°ë³¸ê°’ì€ 1.8)\n\ +\ -target ìžë°” 컴파ì¼ëŸ¬ì— -target 아규먼트 설정 (ê¸°ë³¸ê°’ì€ 1.8)\n\ +\ -threadCount ì»´íŒŒì¼ ì‹œ 사용할 쓰레드 개수.\n\ +\ ("2.0C"는 코어 당 ë‘ ê°œì˜ ì“°ë ˆë“œë¥¼ ì˜ë¯¸í•¨)\n +jspc.warning.tldInWebInfLib=TLD 파ì¼ë“¤ì„ /WEB-INF/lib ë‚´ì— ë‘어서는 안ë©ë‹ˆë‹¤. +jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.footer=\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webinc.insertEnd= +jspc.webinc.insertStart= +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +jstl.OSAfterWriter=ë§Œì¼ writerê°€ ì´ë¯¸ ì‚¬ìš©ëœ ê²½ìš°ì—는, 출력 ìŠ¤íŠ¸ë¦¼ì„ ì‚¬ìš©í•  수 없습니다. +jstl.urlMustStartWithSlash=''context'' ì†ì„±ì´ ì§€ì •ëœ URL 태그ì—ì„œ, ''url''ê³¼ ''context'', 둘 다 반드시 슬래시 문ìžë¡œ 시작해야 합니다. +jstl.writerAfterOS=출력 ìŠ¤íŠ¸ë¦¼ì´ ì´ë¯¸ 사용 ëœ ê²½ìš°, writer를 사용할 수 없습니다. + +org.apache.jasper.compiler.ELParser.invalidQuotesForStringLiteral=문ìžì—´ literal [{0}]ì€(는) 유효하지 않습니다. 반드시 홑따옴표들 ë˜ëŠ” ìŒë”°ì˜´í‘œë“¤ë¡œ 둘러싸여야 합니다. +org.apache.jasper.compiler.ELParser.invalidQuoting=í‘œí˜„ì‹ [{0}]ì€(는) 유효하지 않습니다. ì¸ìš©ë˜ëŠ” 문ìžì—´ ë‚´ì—서는, ì˜¤ì§ [], [''] 그리고 ["] ë§Œì´ [] ì„ ì‚¬ìš©í•˜ì—¬ escapeë  ìˆ˜ 있습니다. +org.apache.jasper.compiler.TldCache.servletContextNull=ì œê³µëœ ServletContextê°€ ë„ì´ì—ˆìŠµë‹ˆë‹¤. +org.apache.jasper.servlet.JasperInitializer.onStartup=컨í…스트 [{0}]ì„(를) 위한 Jasper를 초기화합니다. +org.apache.jasper.servlet.TldScanner.webxmlAdd=리소스 경로 [{0}](으)로부터 URL [{1}]ì„(를) 위한 TLD를 로드합니다. +org.apache.jasper.servlet.TldScanner.webxmlFailPathDoesNotExist=경로가 [{0}]ì´ê³  URIê°€ [{1}]ì¸ TLD를 처리하지 못했습니다. ì§€ì •ëœ ê²½ë¡œê°€ 존재하지 않습니다. +org.apache.jasper.servlet.TldScanner.webxmlSkip=ì´ë¯¸ ì— ì •ì˜ë˜ì—ˆê¸° 때문ì—, URI [{1}]ì„(를) 위한 TLD를, 리소스 경로 [{0}](으)로부터 로드하는 ê²ƒì„ ê±´ë„ˆëœë‹ˆë‹¤. diff --git a/java/org/apache/jasper/resources/LocalStrings_pt.properties b/java/org/apache/jasper/resources/LocalStrings_pt.properties new file mode 100644 index 0000000..6cf3e8c --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_pt.properties @@ -0,0 +1,16 @@ +# 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. + +jsp.error.outputfolder=Sem pasta destino diff --git a/java/org/apache/jasper/resources/LocalStrings_pt_BR.properties b/java/org/apache/jasper/resources/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..aa034ee --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_pt_BR.properties @@ -0,0 +1,69 @@ +# 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. + +jsp.error.attribute.nowhitespace=A especificação JSP requer que o nome de atributo seja precedido por um espaço em branco +jsp.error.data.file.processing=Erro a processar arquivo [{0}] +jsp.error.el.template.deferred=#{...} não é permitido no texto do template +jsp.error.fallback.invalidUse=jsp:fallback precisa ser um filho direto do jsp:plugin +jsp.error.invalid.varscope=escopo de variável invalida [{0}] +jsp.error.no.scriptlets=Elementos de script ( <%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) não são permitidos aqui. +jsp.error.not.in.template=[{0}] não é permitido no template do texto do body +jsp.error.outputfolder=Sem diretório de saída +jsp.error.parse.xml=Erro no processamento do XML no arquivo [{0}] +jsp.error.prolog_pagedir_encoding_mismatch=O encoding de página especificado no XML [{0}] é diferente do especificado na diretiva de página [{1}] +jsp.error.taglibDirective.absUriCannotBeResolved=A uri absoluta [{0}] não pode ser resolvida pelo web.xml ou pelos arquivos jar instalados com esta aplicação +jsp.error.tld.mandatory.element.missing=Elemento TLD mandatório [{0}] faltando ou vazio no TLD [{1}] +jsp.error.unknown_attribute_type=Tipo de atributo [{1}] inválido para atributo [{0}] +jsp.info.ignoreSetting=Ignorada configuraçõ para [{0}] de [{1}] porque um SecurityManager foi habilitado +jsp.message.jsp_removed_excess=Removendo excesso de JSP para o caminho [{0}] da fila de contexto [{1}]. +jsp.warning.enablePooling=Aviso: valor inválido para o initParam enablePooling. Será usado o valor padrão "true" + +jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +org.apache.jasper.compiler.TldCache.servletContextNull=O ServletContext passado era nulo diff --git a/java/org/apache/jasper/resources/LocalStrings_ru.properties b/java/org/apache/jasper/resources/LocalStrings_ru.properties new file mode 100644 index 0000000..5f0ddbd --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_ru.properties @@ -0,0 +1,58 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=СоглаÑно TLD, тег [{0}] должен быть пуÑтым, но он не пуÑтой + +jsp.error.action.isnottagfile=ДейÑтвие [{0}] может быть иÑпользовано только в файлах тегов +jsp.error.attribute.deferredmix=Ðевозможно иÑпользовать одновременно ${} и #{} EL Ð²Ñ‹Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð² значении атрибута +jsp.error.attribute.noequal=Ðеобходим Ñимвол равенÑтва +jsp.error.compiler=ОтÑутÑтвует компилÑтор Java +jsp.error.data.file.processing=Ошибка при обработке файла [{0}] +jsp.error.el.template.deferred=#{...} не допуÑкаетÑÑ Ð² текÑте шаблона +jsp.error.fallback.invalidUse=jsp:fallback должен быть прÑмым потомком jsp:plugin +jsp.error.invalid.tagdir=Ð”Ð¸Ñ€ÐµÐºÑ‚Ð¾Ñ€Ð¸Ñ Ñ„Ð°Ð¹Ð»Ð° тегов [{0}] не начинаетÑÑ Ñ "/WEB-INF/tags" +jsp.error.ise_on_clear=Ðекорректно вызывать clear() при размере буфера == 0 +jsp.error.jspbody.required=Ð”Ð»Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð¸Ñ Ñ‚ÐµÐ»Ð° тега Ð´Ð»Ñ [{0}] должно быть иÑпользовано jsp: body еÑли иÑпользуетÑÑ jsp: attribute. +jsp.error.jspelement.missing.name=ОбÑзательный атрибут 'name' в XML-Ñтиле отÑутÑтвует +jsp.error.location=Строка:[{0}], Ñтолбец: [{1}] +jsp.error.mandatory.attribute=[{0}]: ОбÑзательный атрибут [{1}] отÑутÑтвует +jsp.error.not.in.template=[{0}] не разрешено в теле текÑта шаблона. +jsp.error.outputfolder=Ðе указана Ð²Ñ‹Ñ…Ð¾Ð´Ð½Ð°Ñ Ð¿Ð°Ð¿ÐºÐ° +jsp.error.parse.xml=XML файл [{0}] Ñодержит ошибки +jsp.error.scripting.variable.missing_name=Ðе возможно определить Ð¸Ð¼Ñ Ñкриптовой переменной из атрибута [{0}] +jsp.error.taglibDirective.absUriCannotBeResolved=ÐбÑолютный uri: [{0}] не может быть разрешен ни в web.xml ни в jar файлах развернутых Ñ Ñтим приложением +jsp.error.taglibDirective.uriInvalid=URI предоÑтавленый Ð´Ð»Ñ Ð±Ð¸Ð±Ð»Ð¸Ð¾Ñ‚ÐµÐºÐ¸ тегов [{0}] не ÑвлÑетÑÑ Ð²Ð°Ð»Ð¸Ð´Ð½Ñ‹Ð¼ URI +jsp.error.tld.mandatory.element.missing=ОбÑзательный TLD Ñлемент [{0}] отÑутÑтвует или пуÑÑ‚ в TLD [{1}] +jsp.error.unable.renameClassFile=не возможно переименовать файл клаÑÑа Ñ [{0}] в [{1}] +jsp.error.unbalanced.endtag=Закрывающий тег "</{0}" не ÑбаланÑирован +jsp.error.unknown_attribute_type=ÐеизвеÑтный тип [{1}] Ð´Ð»Ñ Ð°Ñ‚Ñ€Ð¸Ð±ÑƒÑ‚Ð° [{0}] +jsp.exception=Произошла ошибка при обработке [{0}] в Ñтроке [{1}] +jsp.tldCache.tldInDir=TLD файлы были найдены в директории [{0}] + +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ + +jspc.webxml.footer= + +org.apache.jasper.compiler.TldCache.servletContextNull=ПредоÑтавленый ServletContext был равен null diff --git a/java/org/apache/jasper/resources/LocalStrings_zh_CN.properties b/java/org/apache/jasper/resources/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..04931ca --- /dev/null +++ b/java/org/apache/jasper/resources/LocalStrings_zh_CN.properties @@ -0,0 +1,420 @@ +# 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. + +jasper.error.emptybodycontent.nonempty=æ ¹æ® TLD,[{0}] 标签必须为空,但ä¸æ˜¯ + +jsp.engine.info=Jasper JSP{0}引擎 +jsp.error.URLMustStartWithSlash=路径[{0}]必须以斜线字符开头 +jsp.error.action.isnottagfile=[{0}]行为åªèƒ½ç”¨äºŽæ ‡ç­¾æ–‡ä»¶ +jsp.error.action.istagfile=标签文件中ä¸èƒ½ä½¿ç”¨[{0}]功能 +jsp.error.attempt_to_clear_flushed_buffer=错误:å°è¯•æ¸…空已刷新的缓冲区 +jsp.error.attr.quoted=应引用属性值 +jsp.error.attribute.custom.non_rt_with_expr=æ ¹æ®æ ‡è®°æ–‡ä»¶ä¸­çš„TLD或attribute指令,attribute[{0}]ä¸æŽ¥å—ä»»ä½•è¡¨è¾¾å¼ +jsp.error.attribute.deferredmix=ä¸èƒ½åœ¨åŒä¸€å±žæ€§å€¼ä¸­åŒæ—¶ä½¿ç”¨ ${} å’Œ #{} EL è¡¨è¾¾å¼ +jsp.error.attribute.duplicate=属性é™å®šå在元素中必须是唯一的 +jsp.error.attribute.invalidPrefix=属性å‰ç¼€[{0}]与任何导入的标记库都ä¸å¯¹åº” +jsp.error.attribute.noequal=期望的符å·æ˜¯ç­‰å· +jsp.error.attribute.noescape=属性值[{0}]引用[{1}],在值内使用时必须被转义。 +jsp.error.attribute.noquote=需è¦å¼•å·ã€‚ +jsp.error.attribute.nowhitespace=JSP 规范è¦æ±‚一个属性åå­—å‰æœ‰ç©ºæ ¼ +jsp.error.attribute.null_name=空属性å +jsp.error.attribute.standard.non_rt_with_expr=[{1}]标准æ“作的[{0}]属性ä¸æŽ¥å—ä»»ä½•è¡¨è¾¾å¼ +jsp.error.attribute.unterminated=[{0}]的属性值未正确终止 +jsp.error.backgroundCompilationFailed=åŽå°ç¼–译失败 +jsp.error.bad.scratch.dir=你指定的 scratchDir:[{0}] ä¸å¯ç”¨ã€‚ +jsp.error.badStandardAction=无效的标准æ“作。 +jsp.error.bad_attribute=属性[{0}]无效为tag[{1}] 通过TLD +jsp.error.bad_tag=在用å‰ç¼€[{1}]导入的标记库中未定义标记[{0}] +jsp.error.beans.nomethod=在类型为[{1}]çš„bean中找ä¸åˆ°è¯»å–属性[{0}]的方法 +jsp.error.beans.nomethod.setproperty=在类型为[{2}]çš„bean中找ä¸åˆ°ç”¨äºŽå†™å…¥ç±»åž‹ä¸º[{1}]的属性[{0}]的方法 +jsp.error.beans.noproperty=在[{1}]类型bean中找ä¸åˆ°ä»»ä½•æœ‰å…³å±žæ€§[{0}]çš„ä¿¡æ¯ +jsp.error.beans.nullbean=å°è¯•èŽ·å–一个bean æ“作在一个空对象上. +jsp.error.beans.property.conversion=无法将字符串[{0}]转æ¢ä¸ºå±žæ€§[{2}]çš„ç±»[{1}]:[{3}] +jsp.error.beans.propertyeditor.notregistered=属性编辑器未注册到属性编辑管ç†å™¨ +jsp.error.beans.setproperty.noindexset=无法设置索引属性。 +jsp.error.bug48498=无法显示JSPæå–。å¯èƒ½æ˜¯ç”±äºŽXML解æžå™¨é”™è¯¯ï¼ˆæœ‰å…³è¯¦ç»†ä¿¡æ¯ï¼Œè¯·å‚阅TomcatBug48498)。 +jsp.error.cannotAddResolver=在第一次请求å‘生之åŽä¸èƒ½è°ƒç”¨addELResolver +jsp.error.classname=无法从.class文件确定类å +jsp.error.coerce_to_type=无法将值[{2}]强制为属性[{0}]的类型[{1}]。 +jsp.error.compilation=编译文件时出错:[{0}][{1}] +jsp.error.compilation.dependent=加载类[{0}]失败 +jsp.error.compilation.jdt=编译错误 +jsp.error.compilation.jdtProblemError=处ç†JDT问题列表时出错 +jsp.error.compilation.source=加载æºæ–‡ä»¶æ—¶å‡ºé”™[{0}] +jsp.error.compiler=没有å¯ç”¨çš„Java编译器 +jsp.error.compiler.config=没有å¯ç”¨äºŽé…置选项的Java编译器compilerClassName:[{0}]å’Œcompiler:[{1}] +jsp.error.config_pagedir_encoding_mismatch=jsp属性组[{0}]中指定的页编ç ä¸Žpage指令[{1}]中指定的页编ç ä¸åŒ +jsp.error.corresponding.servlet=生æˆçš„servlet错误:\n +jsp.error.could.not.add.taglibraries=ä¸èƒ½å¢žåŠ ä¸€ä¸ªæˆ–者多个tag 库. +jsp.error.data.file.processing=处ç†æ–‡ä»¶ [{0}] 错误 +jsp.error.data.file.read=读å–文件[{0}]时出错 +jsp.error.data.file.write=写入数æ®æ–‡ä»¶æ—¶å‡ºé”™ +jsp.error.deferredmethodandvalue=“deferredValueâ€å’Œâ€œdeferredMethodâ€ä¸èƒ½åŒæ—¶ä¸ºâ€œtrue†+jsp.error.deferredmethodsignaturewithoutdeferredmethod=如果“deferredMethodâ€ä¸æ˜¯â€œtrueâ€ï¼Œåˆ™æ— æ³•æŒ‡å®šæ–¹æ³•ç­¾å +jsp.error.deferredvaluetypewithoutdeferredvalue=如果“deferredValueâ€çš„值ä¸æ˜¯â€œtrueâ€çš„è¯ï¼Œä¸èƒ½æŒ‡å®šä¸€ä¸ªå€¼ç±»åž‹ +jsp.error.directive.isnottagfile=[{0}]指令åªèƒ½åœ¨æ ‡è®°æ–‡ä»¶ä¸­ä½¿ç”¨ +jsp.error.directive.istagfile=[{0}]指令ä¸èƒ½åœ¨æ ‡è®°æ–‡ä»¶ä¸­ä½¿ç”¨ +jsp.error.duplicate.name.jspattribute=标准或自定义æ“作中指定的属性[{0}]也显示为éšé™„çš„jsp:属性中name属性的值 +jsp.error.duplicateqname=找到具有é‡å¤é™å®šå[{0}]的属性。属性é™å®šå在元素中必须是唯一的。 +jsp.error.dynamic.attributes.not.implemented=[{0}]标记声明它接å—动æ€å±žæ€§ï¼Œä½†æœªå®žçŽ°æ‰€éœ€çš„æŽ¥å£ +jsp.error.el.parse=[{0}]:[{1}] +jsp.error.el.template.deferred=#{...} ä¸å…许出现在模æ¿æ–‡æœ¬ä¸­ +jsp.error.el_interpreter_class.instantiation=加载或实例化ELInterpreterç±»[{0}]失败 +jsp.error.fallback.invalidUse=jsp:fallback必须是jsp:plugin的直接å­ä»£ +jsp.error.file.already.registered=文件[{0}]çš„é€’å½’åŒ…å« +jsp.error.file.cannot.read=无法读å–文件 [{0}] +jsp.error.file.not.found=æ–‡.件[{0}] 未找到 +jsp.error.flush=刷新数æ®æ—¶å‘生异常 +jsp.error.fragmentwithtype=无法åŒæ—¶æŒ‡å®šâ€œç‰‡æ®µâ€å’Œâ€œç±»åž‹â€å±žæ€§ã€‚如果存在“片段â€ï¼Œåˆ™â€œç±»åž‹â€å›ºå®šä¸ºâ€œ {0}†+jsp.error.function.classnotfound=找ä¸åˆ°åœ¨TLD中为函数[{1}]指定的类[{0}]:[{2}] +jsp.error.include.exception=无法包å«[{0}] +jsp.error.include.tag=无效的jsp:include标签 +jsp.error.internal.filenotfound=内部错误:找ä¸åˆ°æ–‡ä»¶ [{0}] +jsp.error.invalid.attribute=[{0}]有一个无效属性:[{1}] +jsp.error.invalid.bean=useBean类属性[{0}]的值无效。 +jsp.error.invalid.directive=无效指令 +jsp.error.invalid.expression=[{0}]包å«æ— æ•ˆè¡¨è¾¾å¼ï¼š[{1}] +jsp.error.invalid.implicit=[{0}]处标记文件的éšå¼TLD无效 +jsp.error.invalid.implicit.version=[{0}]处标记文件的éšå¼TLD中定义的JSP版本无效。 +jsp.error.invalid.scope=éžæ³•çš„scope属性值:[{0}](必须是pageã€requestã€session或application中的一个) +jsp.error.invalid.tagdir=标签文件目录 [{0}] ä¸ä»¥"/WEB-INF/tags"开头 +jsp.error.invalid.version=为标签 [{0}] 定义了无效的 JSP ç‰ˆæœ¬å· +jsp.error.ise_on_clear=当缓存大å°ç­‰äºŽ0时调用clear()函数是éžæ³•çš„ +jsp.error.java.line.number=在生æˆçš„java文件中的第:[{0}]è¡Œå‘生错误:[{1}] +jsp.error.javac=Javac异常 +jsp.error.javac.env=环境: +jsp.error.jspbody.emptybody.only=标签[{}]的标签体内智能包å«jsp:attribute +jsp.error.jspbody.invalidUse=JSP:主体必须是标准或自定义æ“作的å­å…ƒç´  +jsp.error.jspbody.required=如果使用 jsp:attribute,则必须使用 jsp:body 为 [{0}] 指定标记正文。 +jsp.error.jspc.missingTarget=缺少目标:必须指定-webapp或-uriroot或一个或多个jsp页 +jsp.error.jspc.no_uriroot=未指定uriroot,无法用指定的JSPæ–‡ä»¶å®šä½ +jsp.error.jspc.uriroot_not_dir=-uriroot选项必须指定一个预先存在的目录 +jsp.error.jspelement.missing.name=XML强制性约æŸï¼šå±žæ€§name缺失。 +jsp.error.jspoutput.conflict=&lt; jsp:output&gt;:éžæ³•ä½¿å¤šä¸ª[{0}]出现ä¸åŒçš„值(旧:[{1}],新:[{2}]) +jsp.error.jspoutput.doctypenamesystem=<jsp:output>: 'doctype-root-element' å’Œ 'doctype-system' 必须一起出现 +jsp.error.jspoutput.doctypepublicsystem=&ltï¼›jsp:output&gt;:如果显示“doctype publicâ€å±žæ€§ï¼Œåˆ™å¿…须显示“doctype systemâ€å±žæ€§ +jsp.error.jspoutput.invalidUse=&ltï¼›jsp:output&gtï¼›ä¸èƒ½åœ¨æ ‡å‡†è¯­æ³•ä¸­ä½¿ç”¨ +jsp.error.jspoutput.nonemptybody=<jsp:output>ä¸èƒ½æœ‰æ­£æ–‡ +jsp.error.jsproot.version.invalid=ç‰ˆæœ¬å· [{0}] 无效,版本å·å¿…须是"1.2"ã€"2.0"ã€"2.1"ã€"2.2"ã€"2.3"ã€"3.0"ã€"3.1"中的一个 +jsp.error.jsptext.badcontent='&lt;',当出现在&lt; jsp:text&gt;的主体中时,必须å°è£…在CDATA中 +jsp.error.lastModified=无法确定文件 [{0}] 的最åŽä¿®æ”¹æ—¥æœŸ +jsp.error.library.invalid=æ ¹æ®åº“[{0}](:[{1}],jsp页无效 +jsp.error.literal_with_void=为属性[{0}]指定了一个文本值,该属性定义为返回类型为void的延迟方法。在这ç§æƒ…况下,JSP.2.3.4ä¸å…许使用文本值。 +jsp.error.loadclass.taghandler=无法为TAG [{1}]加载标记处ç†ç¨‹åºç±»[{0}] +jsp.error.location=è¡Œ.: [{0}], 列: [{1}] +jsp.error.mandatory.attribute=[{0}]: 强制性属性 [{1}] 缺失。 +jsp.error.missing.tagInfo=TLD中缺少[{0}]çš„TagInfo对象 +jsp.error.missing_attribute=æ ¹æ®TLD或标记文件,标记[{1}]必须使用属性[{0}] +jsp.error.missing_var_or_varReader=缺少“varâ€æˆ–“varReaderâ€å±žæ€§ +jsp.error.namedAttribute.invalidUse=jsp:属性必须是标准或自定义æ“作的å­å…ƒç´  +jsp.error.needAlternateJavaEncoding=默认javaç¼–ç [{0}]在javaå¹³å°ä¸Šæ— æ•ˆã€‚å¯ä»¥é€šè¿‡JspServlet的“javaEncodingâ€å‚数指定备用项。 +jsp.error.negativeBufferSize=缓存大å°æ˜¯è´Ÿæ•° +jsp.error.negativeParameter=å‚æ•°[{0}]ä¸èƒ½ä¸ºè´Ÿ +jsp.error.nested.jspattribute=jsp:attribute标准æ“作ä¸èƒ½åµŒå¥—在å¦ä¸€ä¸ªjsp:attribute标准æ“作中 +jsp.error.nested.jspbody=JSP:体标准动作ä¸èƒ½åµŒå¥—在å¦ä¸€ä¸ªjsp:body 或者 jsp:属性标准动作中 +jsp.error.nested_jsproot=嵌套的<jsp:root> +jsp.error.no.jsp=找ä¸åˆ°JSP文件[{0}] +jsp.error.no.more.content=在需è¦æ›´å¤šåˆ†æžæ—¶åˆ°è¾¾å†…容结尾:标记嵌套错误? +jsp.error.no.scratch.dir=JSP引擎未é…ç½®scratch文件夹。\n\ +请在对应上下文Contextçš„servlets.properties文件中添加"jsp.initparams=scratchdir="。 +jsp.error.no.scriptlets=脚本( <%!, <jsp声明, <%=, <jsp表达å¼, <%, <jsp脚本å˜é‡ )ä¸å…许出现在这里 +jsp.error.noFile=找ä¸åˆ°æ–‡ä»¶[{0}] +jsp.error.noFunction=无法使用指定的å‰ç¼€æ‰¾åˆ°å‡½æ•°[{0}] +jsp.error.noFunctionMethod=在类[{2}]中找ä¸åˆ°å‡½æ•°[{1}]的方法[{0}] +jsp.error.noInstanceManager=ServletContext中没有org.apache.tomcat.InstanceManager集 +jsp.error.non_null_tei_and_var_subelems=标记[{0}]有一个或多个å˜é‡å­å…ƒç´ å’Œè¿”回一个或多个å˜é‡ä¿¡æ¯çš„TagExtraInfoç±» +jsp.error.not.in.template=在模æ¿æ–‡æœ¬ä½“中, [{0}] 是ä¸å…许的. +jsp.error.nullArgument=空å‚æ•° +jsp.error.outputfolder=无输出目录 +jsp.error.outputfolder.detail=无法创建JSP编译所需è¦çš„输出目录[{0}] +jsp.error.overflow=错误:JSP缓冲区溢出 +jsp.error.page.conflict.autoflush=页é¢æŒ‡ä»¤ï¼šâ€œ autoFlushâ€éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆæ—§å€¼: [{0}], 新值: [{1}]) +jsp.error.page.conflict.buffer=页é¢æŒ‡ä»¤ï¼šâ€œ bufferâ€éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆæ—§å€¼: [{0}], 新值: [{1}]) +jsp.error.page.conflict.contenttype=Page指令:éžæ³•å‡ºçŽ°å¤šæ¬¡å‡ºçŽ°çš„''contentType''具有ä¸åŒçš„值(old:[{0}],new:[{1}]) +jsp.error.page.conflict.deferredsyntaxallowedasliteral=页é¢æŒ‡ä»¤ï¼šâ€œ deferredSyntaxAllowedAsLiteralâ€éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆæ—§å€¼: [{0}], 新值: [{1}]) +jsp.error.page.conflict.errorOnELNotFound=页é¢æŒ‡â€‹â€‹ä»¤ï¼šå¤šæ¬¡å‡ºçŽ°å…·æœ‰ä¸åŒå€¼â€œerrorOnELNotFoundâ€æ˜¯éžæ³•çš„(旧:[{0}],新:[{1}]) +jsp.error.page.conflict.errorpage=页指令:ä¸åŒå€¼çš„多次出现“errorPageâ€çš„éžæ³•å€¼ï¼ˆæ—§:[{0}],æ–°:[{1}]) +jsp.error.page.conflict.extends=页é¢æŒ‡ä»¤ï¼šâ€œ extendsâ€éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆæ—§å€¼: [{0}], 新值: [{1}]) +jsp.error.page.conflict.info=页é¢æŒ‡ä»¤ï¼šâ€œ infoâ€éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆæ—§å€¼: [{0}], 新值: [{1}]) +jsp.error.page.conflict.iselignored=页é¢æŒ‡ä»¤ï¼šâ€œ isELIgnoredâ€éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆæ—§å€¼: [{0}], 新值: [{1}]) +jsp.error.page.conflict.iserrorpage=页é¢æŒ‡ä»¤ï¼šâ€œ isErrorPageâ€éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆæ—§å€¼: [{0}], 新值: [{1}]) +jsp.error.page.conflict.isthreadsafe=页é¢æŒ‡ä»¤ï¼šâ€œ isThreadSafeâ€éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆæ—§å€¼: [{0}], 新值: [{1}]) +jsp.error.page.conflict.language=页é¢æŒ‡ä»¤ï¼šâ€œ language†éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆ 旧值:[{0}],新值:[{1}] ) +jsp.error.page.conflict.session=页é¢æŒ‡ä»¤ï¼šâ€œsession†éžæ³•å¤šæ¬¡å‡ºçŽ°ä¸åŒå€¼ï¼ˆ 旧值:[{0}],新值:[{1}] ) +jsp.error.page.conflict.trimdirectivewhitespaces=页é¢æŒ‡ä»¤ï¼šè¿æ³•å‡ºçŽ°å¤šä¸ªæœ‰ä¸åŒçš„值(旧值:[{0}],新值:[{1}])的''trimDirectiveWhitespaces'' +jsp.error.page.invalid.buffer=页é¢æŒ‡ä»¤ï¼šbuffer 值无效 +jsp.error.page.invalid.deferredsyntaxallowedasliteral=页é¢æŒ‡ä»¤ï¼šdeferredSyntaxAllowedAsLiteralçš„ 值无效 +jsp.error.page.invalid.errorOnELNotFound=页é¢æŒ‡â€‹â€‹ä»¤ï¼šerrorOnELNotFound 值无效\n +jsp.error.page.invalid.import=页é¢æŒ‡ä»¤ï¼šimport 值无效 +jsp.error.page.invalid.iselignored=页é¢æŒ‡ä»¤ï¼šisELIgnored 值无效 +jsp.error.page.invalid.iserrorpage=页é¢æŒ‡ä»¤ï¼šisErrorPage 值无效 +jsp.error.page.invalid.isthreadsafe=页é¢æŒ‡ä»¤ï¼šisThreadSafe 值无效 +jsp.error.page.invalid.scope=无效的作用域 +jsp.error.page.invalid.session=页é¢æŒ‡ä»¤ï¼šsession 值无效 +jsp.error.page.invalid.trimdirectivewhitespaces=页é¢æŒ‡ä»¤ï¼štrimDirectiveWhitespaces 值无效 +jsp.error.page.language.nonjava=页é¢æŒ‡ä»¤ï¼šlanguage 属性无效 +jsp.error.page.multi.pageencoding=页指令ä¸èƒ½æœ‰å¤šæ¬¡å‡ºçŽ°çš„é¡µç¼–ç  +jsp.error.page.noSession=无法访问ä¸å‚与任何会è¯çš„页中的会è¯ä½œç”¨åŸŸ +jsp.error.page.nullThrowable=空异常 +jsp.error.page.sessionRequired=页é¢éœ€è¦ä¼šè¯ï¼Œä½†æ²¡æœ‰å¯ç”¨çš„ä¼šè¯ +jsp.error.param.invalidUse=jsp:param ä¸èƒ½åœ¨jsp:includeã€jsp:forward或jsp:params等元素外使用 +jsp.error.paramexpected=使用“nameâ€å’Œâ€œvalueâ€å±žæ€§æœŸæœ›â€œjsp:paramâ€æ ‡å‡†æ“作 +jsp.error.params.emptyBody=jsp:params必须至少包å«ä¸€ä¸ªåµŒå¥—çš„jsp:param +jsp.error.params.invalidUse=å‚æ•°jsp:params必须是jsp:plugin的直接孩å­å‚æ•° +jsp.error.parse.error.in.TLD=标记库æ述符中的分æžé”™è¯¯ï¼š[{0}] +jsp.error.parse.xml=æ— æ³•è§£æž XML 文件 [{0}] +jsp.error.parse.xml.line=文件[{0}]上的XML解æžé”™è¯¯ï¼šï¼ˆç¬¬[{1}]行,第[{2}]行) +jsp.error.parse.xml.scripting.invalid.body=[{0}]元素的主体ä¸èƒ½åŒ…å«ä»»ä½•XML元素 +jsp.error.plugin.badtype=jsp:plugin中“typeâ€å±žæ€§çš„值éžæ³•ï¼šå¿…须是“beanâ€æˆ–“applet†+jsp.error.plugin.nocode=代ç æœªå®šä¹‰åœ¨jsp:plugin中 +jsp.error.plugin.notype=jsp:plugin中未声明type +jsp.error.precompilation=无法预编译JSP[{0}] +jsp.error.precompilation.parameter=ä¸èƒ½å°†é¢„编译请求å‚æ•°[{0}]设置为[{1}] +jsp.error.prefix.refined=å°è¯•å°†å‰ç¼€[{0}]é‡æ–°å®šä¹‰ä¸º[{1}],但当å‰ä½œç”¨åŸŸä¸­å·²å°†å…¶å®šä¹‰ä¸º[{2}]。 +jsp.error.prefix.use_before_dcl=tag指令中设置的å‰ç¼€[{0}]之å‰å·²è¢«[{1}]文件[{2}]行的一个action使用 +jsp.error.prolog_config_encoding_mismatch=XML prolog [{0}]中指定的页é¢ç¼–ç ä¸Žjsp-property-group [{1}]中指定的页é¢ç¼–ç ä¸åŒ +jsp.error.prolog_pagedir_encoding_mismatch=指定在XML语言[{0}]的网页编ç æ˜¯ä¸åŒäºŽæŒ‡å®šåœ¨ç½‘页的指令[{1}]。 +jsp.error.quotes.unterminated=æœªç»ˆç»“çš„å¼•å· +jsp.error.readContent=无法读å–预期长度[{0}] +jsp.error.reload=servleté‡æ–°åŠ è½½å¤±è´¥ +jsp.error.scripting.variable.missing_name=无法从属性[{0}]确定脚本å˜é‡å称 +jsp.error.security=上下文的安全åˆå§‹åŒ–失败 +jsp.error.securityPreload=预加载类时出错 +jsp.error.servlet.destroy.failed=JSP页é¢çš„Servlet.destroy()期间出现异常 +jsp.error.servlet.invalid.method=JSP åªå…许 GETã€POST 或 HEAD。Jasper 还å…许 OPTIONS +jsp.error.setLastModified=无法设置文件[{0}]的上次修改日期 +jsp.error.signature.classnotfound=无法找到TLD中方法签å中为函数[{1}]指定的类[{0}]。[{2}] +jsp.error.simpletag.badbodycontent=对于一下简å•çš„标记符,用于类[{0}]的标记æ述符指定了一个无效的body内容(JSP)中 +jsp.error.single.line.number=JSP文件:[{1}] 的第 [{0}] è¡Œå‘生了一个错误 +jsp.error.stream.close.failed=æµ.关闭失败 +jsp.error.stream.closed=æµ.关闭 +jsp.error.string_interpreter_class.instantiation=加载或者实例化StringInterpreterç±» [{0}]失败 +jsp.error.tag.conflict.attr=Tag指令:具有多个具有ä¸åŒå€¼çš„属性[{0}]çš„é‡å¤å‡ºçŽ°æ˜¯éžæ³•çš„(旧的:[{1}],新的:[{2}]) +jsp.error.tag.conflict.deferredsyntaxallowedasliteral=Tag指令:具有多个具有ä¸åŒå€¼çš„“deferredSyntaxAllowedAsLiteralâ€ï¼ˆæ—§å€¼ï¼š[{0}],新值:[{1}])的é‡å¤å‡ºçŽ°æ˜¯éžæ³•çš„。 +jsp.error.tag.conflict.errorOnELNotFound=标签指令:多次出现具有ä¸åŒå€¼â€œerrorOnELNotFoundâ€æ˜¯éžæ³•çš„(旧:[{0}],新:[{1}])\n +jsp.error.tag.conflict.iselignored=TAG指令:多次出现ä¸åŒå€¼çš„“isELIgnoredâ€(旧:[{0}],New:[{1}]) +jsp.error.tag.conflict.language=标签指令:éžæ³•å‡ºçŽ°å¤šæ¬¡å‡ºçŽ°çš„具有ä¸åŒå€¼çš„“语言â€ï¼ˆæ—§ï¼š[{0}],新:[{1}]) +jsp.error.tag.conflict.trimdirectivewhitespaces=标签指令:éžæ³•åœ°å¤šæ¬¡å‡ºçŽ°å…·æœ‰ä¸åŒå€¼çš„“trimDirectiveWhitespacesâ€ï¼ˆæ—§å€¼ï¼š[{0}],新值:[{1}]) +jsp.error.tag.invalid.deferredsyntaxallowedasliteral=标签指令):deferredSyntaxAllowedAsLiteral的值无效 +jsp.error.tag.invalid.errorOnELNotFound=标记指令:errorOnELNotFound 的无效值\n +jsp.error.tag.invalid.iselignored=Tag指令:对isELIgnoredæ¥è¯´æ˜¯æ— æ•ˆå€¼ +jsp.error.tag.invalid.trimdirectivewhitespaces=Tag指令:trimDirectiveWhitespaces的值无效 +jsp.error.tag.language.nonjava=标记指令:无效的语言属性。 +jsp.error.tag.multi.pageencoding=Tag指令ä¸èƒ½å¤šæ¬¡å‡ºçŽ°pageencoding +jsp.error.tagdirective.badbodycontent=标签指令中的无效的内容体[{0}] +jsp.error.tagfile.badSuffix=在文件路径[{0}]下找ä¸åˆ°".tag"çš„åŽç¼€ +jsp.error.tagfile.illegalPath=éžæ³•çš„标记文件路径:[{0}],必须以“/WEB-INF/tagsâ€æˆ–“/META-INF/tagsâ€å¼€å¤´ã€‚ +jsp.error.tagfile.missingPath=未指定标记文件的路径 +jsp.error.tagfile.nameFrom.badAttribute=属性指令(在行[{1}]中声明并且其name属性为[{0}],此name-from-attribute属性的值)必须是java.lang.String类型,是“requiredâ€è€Œä¸æ˜¯ä¸€ä¸ªâ€œrtexprvalueâ€ã€‚ +jsp.error.tagfile.nameFrom.noAttribute=找ä¸åˆ°å…·æœ‰å€¼[{0}]çš„name属性的属性指令,该属性是name-from-attribute属性的值。 +jsp.error.tagfile.nameNotUnique=[{0}]的值与第[{2}]行中的[{1}]的值相åŒã€‚ +jsp.error.taglibDirective.absUriCannotBeResolved=无法在web.xml或使用此应用程åºéƒ¨ç½²çš„jar文件中解æžç»å¯¹uri:[{0}] +jsp.error.taglibDirective.both_uri_and_tagdir=åŒæ—¶æŒ‡å®šäº†â€œuriâ€å’Œâ€œtagdirâ€å±žæ€§ +jsp.error.taglibDirective.missing.location='uri'å’Œ'tagdir' 属性å‡æœªæŒ‡å®šã€‚ +jsp.error.taglibDirective.uriInvalid=为标签库[{0}]æ供的URIä¸æ˜¯æœ‰æ•ˆçš„URI +jsp.error.tei.invalid.attributes=æ¥è‡ªTagExtraInfoçš„[{0}]的验è¯é”™è¯¯æ¶ˆæ¯ +jsp.error.teiclass.instantiation=无法加载或实例化TagExtraInfo类:[{0}]。 +jsp.error.text.has_subelement=&LT; JSP:文本&GT; ä¸å¾—有任何å­å…ƒç´  +jsp.error.tld.fn.duplicate.name=标记库[{1}]中的函数å[{0}]é‡å¤ +jsp.error.tld.fn.invalid.signature=TLD中函数签å的语法无效。 标签库:[{0}],功能:[{1}] +jsp.error.tld.invalid_tld_file=无效的tld文件:[{0}],有关详细信æ¯ï¼Œè¯·å‚阅JSP规范第7.3.1节。 +jsp.error.tld.mandatory.element.missing=TLD [{1}] 中强制 TLD 元素 [{0}] ä¸å­˜åœ¨æˆ–为空 +jsp.error.tld.missing=找ä¸åˆ°URI:[{1}]çš„taglib[{0}] +jsp.error.tld.missing_jar=丢失了包å«TLDçš„JAR资æº[{0}] +jsp.error.tld.unable_to_get_jar=无法获å–包å«TLD:[{1}]çš„JAR资æº[{0}] +jsp.error.tld.url=错误的TLD URL[{0}] +jsp.error.tlv.invalid.page=):[{0}]å’Œ[{1}]çš„TagLibraryValidator的验è¯é”™è¯¯æ¶ˆæ¯ +jsp.error.tlvclass.instantiation=未能加载或实例化TagLibraryValidator类:[{0}] +jsp.error.unable.compile=无法为JSP编译类 +jsp.error.unable.deleteClassFile=无法删除class文件[{0}] +jsp.error.unable.getType=无法从[{0}]æå–类型 +jsp.error.unable.load=无法加载JSP的相关类 +jsp.error.unable.renameClassFile=无法é‡å‘½å类文件[{0}]为[{1}] +jsp.error.unable.to_find_method=ä¸èƒ½ä¸ºå±žæ€§:[{0}]找到setter 方法. +jsp.error.unavailable=JSP已被标记为ä¸å¯ç”¨ +jsp.error.unbalanced.endtag=结æŸæ ‡ç­¾</{0}ä¸å¯¹ç§° +jsp.error.undeclared_namespace=使用未声明的命å空间[{0}]é‡åˆ°è‡ªå®šä¹‰æ ‡è®° +jsp.error.unexpectedTag=æ„外的标记[{0}] +jsp.error.unknown_attribute_type=属性[{0}]的未知属性类型[{1}]。 +jsp.error.unsupported.encoding=ä¸æ”¯æŒçš„ç¼–ç ï¼š[{0}] +jsp.error.unterminated=未终止的[{0}]标记。 +jsp.error.usebean.duplicate=useBean:é‡å¤çš„beanå称:[{0}] +jsp.error.usebean.noSession=当JSP页声明(通过page指令)useBeanä¸å‚与会è¯æ—¶ï¼ŒuseBean使用会è¯ä½œç”¨åŸŸæ˜¯éžæ³•çš„ +jsp.error.var_and_varReader=åªæœ‰ä¸€ä¸ªâ€œvarâ€æˆ–“varReaderâ€èƒ½è¢«æŒ‡å®š +jsp.error.variable.alias=必须在variable指令中指定属性和别å属性中的name或none +jsp.error.variable.both.name=ä¸èƒ½åœ¨å˜é‡æŒ‡ä»¤çš„属性属性中åŒæ—¶æŒ‡å®šç»™å®šçš„å称和å称 +jsp.error.variable.either.name=必须在å˜é‡æŒ‡ä»¤ä¸­æŒ‡å®š name-given 或 name-from-attribute 属性 +jsp.error.xml.badStandardAction=无效ã€æ ‡å‡†çš„action: [{0}] +jsp.error.xml.bad_tag=在与uri[{1}]å…³è”的标记库中未定义标记[{0}] +jsp.exception=在 [{1}] è¡Œå¤„ç† [{0}] æ—¶å‘生异常 +jsp.info.ignoreSetting=因为 SecurityManager 被å¯ç”¨ï¼Œå¿½ç•¥ [{1}] çš„ [{0}] 的设置 +jsp.message.dont.modify.servlets=é‡è¦æ示:ä¸è¦ä¿®æ”¹ç”Ÿæˆçš„servlet +jsp.message.jsp_added=增加JSP 为路径[{0}]为上下文[{1}]的队列 +jsp.message.jsp_queue_created=用长度[{0}]上下文[{1}]创建了jsp队列 +jsp.message.jsp_queue_update=在上下文[{1}]队列中更新路径为[{0}]çš„JSP +jsp.message.jsp_removed_excess=从上下文[{1}]的队列中移除é¢å¤–在路径[{0}]中JSP, +jsp.message.jsp_removed_idle=在[{2}]毫秒之åŽåˆ é™¤ä¸Šä¸‹æ–‡[{1}]中路径[{0}]的空闲JSP +jsp.message.jsp_unload_check=在context[{0}]中检查未加载的jsp,jsp总共:[{1}]队列长度[{2}] +jsp.message.parent_class_loader_is=父类加载器是:[{0}] +jsp.message.scratch.dir.is=JSP引擎的Scratch目录是:[{0}] +jsp.tldCache.noTldInDir=在目录[{0}]中未找到TLD文件 +jsp.tldCache.noTldInJar=在[{0}]中找ä¸åˆ°TLD文件。考虑将JAR添加到CATALINA_BASE/conf/CATALINA.properties文件中的tomcat.util.scan.StandardJarScanFilter.jarsToSkip属性。 +jsp.tldCache.noTldInResourcePath=在资æºè·¯å¾„[{0}]中找ä¸åˆ°TLD文件。 +jsp.tldCache.noTldSummary=至少有一个JAR被扫æ用于TLD但尚未包å«TLD。 为此记录器å¯ç”¨è°ƒè¯•æ—¥å¿—记录,以获å–已扫æ但未在其中找到TLD的完整JAR列表。 在扫æ期间跳过ä¸éœ€è¦çš„JARå¯ä»¥ç¼©çŸ­å¯åŠ¨æ—¶é—´å’ŒJSP编译时间。 +jsp.tldCache.tldInDir=在目录 [{0}]中找到了TLD文件。 +jsp.tldCache.tldInJar=在JAR[{0}]中找到了TLD文件。 +jsp.tldCache.tldInResourcePath=在资æºè·¯å¾„[{0}]中找到TLD文件。 +jsp.warning.bad.urlpattern.propertygroup=web.xml中url模å¼å­å…ƒç´ ä¸­çš„值[{0}]错误 +jsp.warning.checkInterval=警告:initParam checkInterval的值无效。将使用默认值“300â€ç§’ +jsp.warning.classDebugInfo=警告:initParam classdebuginfo的值无效。将使用默认值“false†+jsp.warning.classpathUrl=在类路径中找到无效的URL。此URL将被忽略 +jsp.warning.compiler.classfile.delete.fail=未能删除生æˆçš„类文件[{0}] +jsp.warning.compiler.classfile.delete.fail.unknown=删除生æˆçš„class文件失败 +jsp.warning.compiler.javafile.delete.fail=未能删除生æˆçš„Java文件[{0}] +jsp.warning.development=警告:initParamå¼€å‘的值无效。将使用默认值“trueâ€ã€‚ +jsp.warning.displaySourceFragment=警告:displaySourceFragmentåˆå§‹åŒ–å‚æ•°æ—¶å‚数值无效,将使用默认的值“true†+jsp.warning.dumpSmap=警告:åˆå§‹åŒ–堆内存的值无效。将使用“falseâ€çš„默认值 +jsp.warning.enablePooling=警告:initParam enablePooling的值无效。将使用默认值“true†+jsp.warning.engineOptionsClass=未能加载引擎选项类[{0}] +jsp.warning.fork=警告:initParam的值无效。将使用“trueâ€çš„默认值 +jsp.warning.genchararray=警告:initParam genstringascharray的值无效。将使用默认值“false†+jsp.warning.isThreadSafe=警告:在[{0}] 中使用的“isThreadSafeâ€é¡µé¢æŒ‡ä»¤å±žæ€§å·²è¢«å¼ƒç”¨ï¼Œå¹¶å°†åœ¨ JSP 规范的 4.0 版中删除\n +jsp.warning.jspIdleTimeout=警告:initParam jspIdleTimeout的值无效。将使用默认值“-1†+jsp.warning.keepgen=警告:initParam keepgenerated的值无效。将使用默认值“false†+jsp.warning.loadSmap=无法加载类[{0}]çš„SMAPæ•°æ® +jsp.warning.mappedFile=警告:initParam mappedFile的值无效。将使用默认值“false†+jsp.warning.maxLoadedJsps=警告:initParam maxLoadedJsps的值无效。将使用默认值“-1â€ã€‚ +jsp.warning.modificationTestInterval=警告:initParam modificationTestInterval的值无效。将使用默认值“4â€ç§’ +jsp.warning.noJarScanner=警告:ServletContext中没有设置org.apache.tomcat.JarScaner。回到默认的JarScaner实现。 +jsp.warning.poolTagsWithExtends=警告:poolTagsWithExtends initParam值无效,将会使用默认值“false†+jsp.warning.quoteAttributeEL=警告:initParam quoteattribeel的值无效。将使用默认值“false†+jsp.warning.recompileOnFail=警告:initParam recompileOnFail的值无效。将使用默认值“false†+jsp.warning.strictGetProperty=警告:无效的åˆå§‹å‚æ•°strictGetProperty值,将会使用默认值“true†+jsp.warning.strictQuoteEscaping=警告:对initParam strictQuoteEscapingæ¥è¯´æ— æ•ˆçš„值,将会使用默认值“true†+jsp.warning.strictWhitespace=警告:无效的åˆå§‹å‚æ•°strictWhitespace值,将使用默认值“true†+jsp.warning.suppressSmap=警告:suppressSmapçš„åˆå§‹åŒ–å‚数无效。将使用默认值“false†+jsp.warning.tagPreDestroy=处ç†æ ‡è®°å®žä¾‹çš„preDestroy时出错[{0}] +jsp.warning.tagRelease=处ç†[{0}]的标记实例上的释放时出错 +jsp.warning.trimspaces=警告:initParam trimSpaces的值无效.将使用默认值"false" +jsp.warning.unknown.sourceVM=忽略未知æºVM[{0}] +jsp.warning.unknown.targetVM=忽略未知目标VM[{0}] +jsp.warning.unsupported.sourceVM=ä¸æ”¯æŒçš„æºVM[{0}]请求,使用[{1}] +jsp.warning.unsupported.targetVM=ä¸æ”¯æŒè¯·æ±‚的目标VM[{0}],使用[{1}] +jsp.warning.useInstanceManagerForTags=警告:无效的åˆå§‹åŒ–å‚æ•°useInstanceManagerForTags值,将会使用默认值“false†+jsp.warning.xpoweredBy=警告:initParam xpoweredBy的值无效。将使用默认值“false†+ +jspc.delete.fail=无法删除文件 [{0}] +jspc.error.compilation=编译错误 +jspc.error.fileDoesNotExist=文件å‚æ•° [{0}] ä¸å­˜åœ¨ +jspc.error.generalException=错误文件[{0}]生æˆä»¥ä¸‹å¸¸è§„异常: +jspc.error.invalidFragment=由于web片段中的错误而中止预编译 +jspc.error.invalidWebXml=由于web.xml中的错误而中止预编译 +jspc.error.minThreadCount=必须至少有一个线程[{0}] +jspc.error.parseThreadCount=ä¸èƒ½å¤Ÿè½¬æ¢çš„线程数[{0}] +jspc.error.unknownOption=无法识别的选项[{0}]。使用-帮助获å–帮助。 +jspc.errorCount=错误计数:[{0}] +jspc.generation.result=生æˆåœ¨[{1}]毫秒内完æˆï¼Œå‡ºçŽ°[{0}]个错误 +jspc.implicit.uriRoot=uriRootéšå¼è®¾ç½®ä¸º[{0}] +jspc.usage=用法: jspc <选项> [--] \n\ +jsp.文件在哪\n\ +\ -webapp 包å«Web.应用程åºçš„目录, 哪个jsp页é¢\n\ +\ 将递归处ç†æˆ–任何数é‡çš„\n\ +\ è¦è§£æžä¸ºJSP.页é¢çš„文件\n\ +其他选项包括:\n\ +\ -help 打å°.帮助信æ¯\n\ +\ -v 冗余.模å¼\n\ +\ -d 输出.目录 (默认-Djava.io.tmpdir)\n\ +\ -l 在失败时输出JSP.页é¢çš„å称\n\ +\ -s æˆåŠŸè¾“出JSP.页é¢çš„å称\n\ +\ -p 目标包.çš„å称 (默认org.apache.jsp)\n\ +\ -c 目标类å.çš„å称(仅适用于第一个JSP页é¢ï¼‰\n\ +\ -mapped 为JSP.中的æ¯ä¸ªHTML行生æˆå•ç‹¬çš„write()调用\n\ +\ -die[#] 在致命错误.上生æˆé”™è¯¯è¿”回代ç (#) (默认1)\n\ +\ -uribase uri.目录编译应该是相对的\n\ +\ (默认"/")\n\ +\ -uriroot 与-webapp.相åŒ\n\ +\ -compile 编译生æˆçš„.servlets\n\ +\ -failFast 首次编译错误.终止\n\ +\ -webinc 在文件中创建.部分servlet映射\n\ +\ -webfrg 在文件中创建.完整的web-fragment.xml\n\ +\ -webxml 在文件中创建.完整的web.xml\n\ +\ -webxmlencoding 设置用于读å–和写入.Web.xmlçš„ç¼–ç å­—符集\n\ +\ 文件 (默认是 UTF-8)\n\ +\ -addwebxmlmappings 将生æˆçš„.Web.xml片段åˆå¹¶åˆ°Web.xml文件中网络应用\n\ +\ , 我们正在处ç†å“ªä¸ª.JSP页é¢\n\ +\ -ieplugin IEæµè§ˆå™¨.Javaæ’件的类id\n\ +\ -classpath é‡å†™.java.class.path环境å˜é‡å‚æ•°\n\ +\ -xpoweredBy 添加.X-Powered-By å“应头\n\ +\ -trimSpaces [single] 移除.完全由空格组æˆçš„模æ¿æ–‡æœ¬\n\ +\ (如果"single", 用å•ä¸ªç©ºæ ¼æ›¿æ¢è¿™æ ·çš„模æ¿æ–‡æœ¬)\n\ +\ -javaEncoding 为Java.类设置编ç å­—符集 (默认UTF-8)\n\ +\ -source 设置-source.å‚数到编译器( (默认1.8)\n\ +\ -target 设置-target.å‚数到编译器(默认1.8)\n\ +\ -threadCount 编译使用的线程数.\n\ +\ ("2.0C" æ„味ç€æ¯ä¸ªå†…核有两个线程)\n +jspc.warning.tldInWebInfLib=TLD文件ä¸åº”放在/WEB-INF/lib中†+jspc.webfrg.footer=\n\ +\n\ +\n +jspc.webfrg.header=\n\ +\n\ +\ org_apache_jasper.jspc\n\ +\ \n\ +\n\ +\n +jspc.webinc.footer=\n\ +\n +jspc.webinc.header=\n\ +\n\ +\n +jspc.webinc.insertEnd= +jspc.webinc.insertStart= +jspc.webxml.footer=\n\ +\n\ +\n +jspc.webxml.header=\n\ +\n\ +\n\ +\n + +jstl.OSAfterWriter=如果已使用写入程åºï¼Œåˆ™æ— æ³•ä½¿ç”¨è¾“å‡ºæµ +jstl.urlMustStartWithSlash=在指定了“contextâ€å±žæ€§çš„URL标记中,“urlâ€å’Œâ€œcontextâ€éƒ½å¿…须以斜æ å­—符开头 +jstl.writerAfterOS=如果已使用输出æµï¼Œåˆ™æ— æ³•ä½¿ç”¨ç¼–写器 + +org.apache.jasper.compiler.ELParser.invalidQuotesForStringLiteral=字符串文本[{0}]无效。它必须包å«åœ¨å•å¼•å·æˆ–åŒå¼•å·ä¸­ã€‚ +org.apache.jasper.compiler.ELParser.invalidQuoting=表达å¼[{0}]无效。在带引å·çš„字符串中,åªæœ‰[],['']å’Œ[“]å¯ä»¥ç”¨[]转义。 +org.apache.jasper.compiler.TldCache.servletContextNull=æ供的 ServletContext 为 null +org.apache.jasper.servlet.JasperInitializer.onStartup=正在åˆå§‹åŒ–上下文[{0}]çš„Jasper +org.apache.jasper.servlet.TldScanner.webxmlAdd=从资æºè·¯å¾„[{0}]加载URI[{1}]çš„TLD +org.apache.jasper.servlet.TldScanner.webxmlFailPathDoesNotExist=无法使用路径 [{0}] å’Œ URI [{1}] 处ç†TLD。指定的路径ä¸å­˜åœ¨ã€‚ +org.apache.jasper.servlet.TldScanner.webxmlSkip=跳过从资æºè·¯å¾„[{0}]加载URI[{1}]çš„TLD,因为它已在中定义 diff --git a/java/org/apache/jasper/runtime/BodyContentImpl.java b/java/org/apache/jasper/runtime/BodyContentImpl.java new file mode 100644 index 0000000..4c698f0 --- /dev/null +++ b/java/org/apache/jasper/runtime/BodyContentImpl.java @@ -0,0 +1,682 @@ +/* + * 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.jasper.runtime; + +import java.io.CharArrayReader; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; + +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.tagext.BodyContent; + +import org.apache.jasper.compiler.Localizer; + +/** + * Write text to a character-output stream, buffering characters so as + * to provide for the efficient writing of single characters, arrays, + * and strings. + * + * Provide support for discarding for the output that has been buffered. + * + * @author Rajiv Mordani + * @author Jan Luehe + */ +public class BodyContentImpl extends BodyContent { + + private final boolean limitBuffer; + private final int tagBufferSize; + + private char[] cb; + private int nextChar; + private boolean closed; + + /** + * Enclosed writer to which any output is written + */ + private Writer writer; + + /** + * Constructor. + * @param enclosingWriter The wrapped writer + * @param limitBuffer true to discard large buffers + * @param tagBufferSize the buffer size + */ + public BodyContentImpl(JspWriter enclosingWriter, boolean limitBuffer, int tagBufferSize) { + super(enclosingWriter); + this.limitBuffer = limitBuffer; + this.tagBufferSize = tagBufferSize; + cb = new char[tagBufferSize]; + bufferSize = cb.length; + nextChar = 0; + closed = false; + } + + /** + * Write a single character. + * @param c The char to write + * @throws IOException Error writing to wrapped writer + */ + @Override + public void write(int c) throws IOException { + if (writer != null) { + writer.write(c); + } else { + ensureOpen(); + if (nextChar >= bufferSize) { + reAllocBuff (1); + } + cb[nextChar++] = (char) c; + } + } + + /** + * Write a portion of an array of characters. + * + *

    Ordinarily this method stores characters from the given array into + * this stream's buffer, flushing the buffer to the underlying stream as + * needed. If the requested length is at least as large as the buffer, + * however, then this method will flush the buffer and write the characters + * directly to the underlying stream. Thus redundant + * DiscardableBufferedWriters will not copy data + * unnecessarily. + * + * @param cbuf A character array + * @param off Offset from which to start reading characters + * @param len Number of characters to write + * @throws IOException Error writing to wrapped writer + */ + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + if (writer != null) { + writer.write(cbuf, off, len); + } else { + ensureOpen(); + + if ((off < 0) || (off > cbuf.length) || (len < 0) || + ((off + len) > cbuf.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + if (len >= bufferSize - nextChar) { + reAllocBuff (len); + } + + System.arraycopy(cbuf, off, cb, nextChar, len); + nextChar+=len; + } + } + + /** + * Write an array of characters. This method cannot be inherited from the + * Writer class because it must suppress I/O exceptions. + * @param buf Content to write + * @throws IOException Error writing to wrapped writer + */ + @Override + public void write(char[] buf) throws IOException { + if (writer != null) { + writer.write(buf); + } else { + write(buf, 0, buf.length); + } + } + + /** + * Write a portion of a String. + * + * @param s String to be written + * @param off Offset from which to start reading characters + * @param len Number of characters to be written + * @throws IOException Error writing to wrapped writer + */ + @Override + public void write(String s, int off, int len) throws IOException { + if (writer != null) { + writer.write(s, off, len); + } else { + ensureOpen(); + if (len >= bufferSize - nextChar) { + reAllocBuff(len); + } + + s.getChars(off, off + len, cb, nextChar); + nextChar += len; + } + } + + /** + * Write a string. This method cannot be inherited from the Writer class + * because it must suppress I/O exceptions. + * @param s String to be written + * @throws IOException Error writing to wrapped writer + */ + @Override + public void write(String s) throws IOException { + if (writer != null) { + writer.write(s); + } else { + write(s, 0, s.length()); + } + } + + /** + * Write a line separator. The line separator string is defined by the + * system property line.separator, and is not necessarily a + * single newline ('\n') character. + * + * @throws IOException Error writing to wrapped writer + */ + @Override + public void newLine() throws IOException { + if (writer != null) { + writer.write(System.lineSeparator()); + } else { + write(System.lineSeparator()); + } + } + + /** + * Print a boolean value. The string produced by {@link + * String#valueOf(boolean)} is translated into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the {@link + * #write(int)} method. + * + * @param b The boolean to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(boolean b) throws IOException { + if (writer != null) { + writer.write(b ? "true" : "false"); + } else { + write(b ? "true" : "false"); + } + } + + /** + * Print a character. The character is translated into one or more bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the {@link + * #write(int)} method. + * + * @param c The char to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(char c) throws IOException { + if (writer != null) { + writer.write(String.valueOf(c)); + } else { + write(String.valueOf(c)); + } + } + + /** + * Print an integer. The string produced by {@link + * String#valueOf(int)} is translated into bytes according + * to the platform's default character encoding, and these bytes are + * written in exactly the manner of the {@link #write(int)} + * method. + * + * @param i The int to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(int i) throws IOException { + if (writer != null) { + writer.write(String.valueOf(i)); + } else { + write(String.valueOf(i)); + } + } + + /** + * Print a long integer. The string produced by {@link + * String#valueOf(long)} is translated into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the + * {@link #write(int)} method. + * + * @param l The long to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(long l) throws IOException { + if (writer != null) { + writer.write(String.valueOf(l)); + } else { + write(String.valueOf(l)); + } + } + + /** + * Print a floating-point number. The string produced by {@link + * String#valueOf(float)} is translated into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the + * {@link #write(int)} method. + * + * @param f The float to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(float f) throws IOException { + if (writer != null) { + writer.write(String.valueOf(f)); + } else { + write(String.valueOf(f)); + } + } + + /** + * Print a double-precision floating-point number. The string produced by + * {@link String#valueOf(double)} is translated into + * bytes according to the platform's default character encoding, and these + * bytes are written in exactly the manner of the {@link + * #write(int)} method. + * + * @param d The double to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(double d) throws IOException { + if (writer != null) { + writer.write(String.valueOf(d)); + } else { + write(String.valueOf(d)); + } + } + + /** + * Print an array of characters. The characters are converted into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the + * {@link #write(int)} method. + * + * @param s The array of chars to be printed + * + * @throws NullPointerException If s is null + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(char[] s) throws IOException { + if (writer != null) { + writer.write(s); + } else { + write(s); + } + } + + /** + * Print a string. If the argument is null then the string + * "null" is printed. Otherwise, the string's characters are + * converted into bytes according to the platform's default character + * encoding, and these bytes are written in exactly the manner of the + * {@link #write(int)} method. + * + * @param s The String to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(String s) throws IOException { + if (s == null) { + s = "null"; + } + if (writer != null) { + writer.write(s); + } else { + write(s); + } + } + + /** + * Print an object. The string produced by the {@link + * String#valueOf(Object)} method is translated into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the + * {@link #write(int)} method. + * + * @param obj The Object to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void print(Object obj) throws IOException { + if (writer != null) { + writer.write(String.valueOf(obj)); + } else { + write(String.valueOf(obj)); + } + } + + /** + * Terminate the current line by writing the line separator string. The + * line separator string is defined by the system property + * line.separator, and is not necessarily a single newline + * character ('\n'). + * + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println() throws IOException { + newLine(); + } + + /** + * Print a boolean value and then terminate the line. This method behaves + * as though it invokes {@link #print(boolean)} and then + * {@link #println()}. + * + * @param x The boolean to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(boolean x) throws IOException { + print(x); + println(); + } + + /** + * Print a character and then terminate the line. This method behaves as + * though it invokes {@link #print(char)} and then + * {@link #println()}. + * + * @param x The char to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(char x) throws IOException { + print(x); + println(); + } + + /** + * Print an integer and then terminate the line. This method behaves as + * though it invokes {@link #print(int)} and then + * {@link #println()}. + * + * @param x The int to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(int x) throws IOException { + print(x); + println(); + } + + /** + * Print a long integer and then terminate the line. This method behaves + * as though it invokes {@link #print(long)} and then + * {@link #println()}. + * + * @param x The long to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(long x) throws IOException { + print(x); + println(); + } + + /** + * Print a floating-point number and then terminate the line. This method + * behaves as though it invokes {@link #print(float)} and then + * {@link #println()}. + * + * @param x The float to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(float x) throws IOException { + print(x); + println(); + } + + /** + * Print a double-precision floating-point number and then terminate the + * line. This method behaves as though it invokes {@link + * #print(double)} and then {@link #println()}. + * + * @param x The double to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(double x) throws IOException{ + print(x); + println(); + } + + /** + * Print an array of characters and then terminate the line. This method + * behaves as though it invokes {@link #print(char[])} and + * then {@link #println()}. + * + * @param x The char array to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(char x[]) throws IOException { + print(x); + println(); + } + + /** + * Print a String and then terminate the line. This method behaves as + * though it invokes {@link #print(String)} and then + * {@link #println()}. + * + * @param x The string to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(String x) throws IOException { + print(x); + println(); + } + + /** + * Print an Object and then terminate the line. This method behaves as + * though it invokes {@link #print(Object)} and then + * {@link #println()}. + * + * @param x The object to be printed + * @throws IOException Error writing to wrapped writer + */ + @Override + public void println(Object x) throws IOException { + print(x); + println(); + } + + /** + * Clear the contents of the buffer. If the buffer has been already + * been flushed then the clear operation shall throw an IOException + * to signal the fact that some data has already been irrevocably + * written to the client response stream. + * + * @throws IOException If there is no wrapped writer + */ + @Override + public void clear() throws IOException { + if (writer != null) { + throw new IOException(); + } else { + nextChar = 0; + if (limitBuffer && (cb.length > tagBufferSize)) { + cb = new char[tagBufferSize]; + bufferSize = cb.length; + } + } + } + + /** + * Clears the current contents of the buffer. Unlike clear(), this + * method will not throw an IOException if the buffer has already been + * flushed. It merely clears the current content of the buffer and + * returns. + * + * @throws IOException Should not happen + */ + @Override + public void clearBuffer() throws IOException { + if (writer == null) { + this.clear(); + } + } + + /** + * Close the stream, flushing it first. Once a stream has been closed, + * further write() or flush() invocations will cause an IOException to be + * thrown. Closing a previously-closed stream, however, has no effect. + * + * @throws IOException Error writing to wrapped writer + */ + @Override + public void close() throws IOException { + if (writer != null) { + writer.close(); + } else { + closed = true; + } + } + + /** + * This method returns the size of the buffer used by the JspWriter. + * + * @return the size of the buffer in bytes, or 0 is unbuffered. + */ + @Override + public int getBufferSize() { + // According to the spec, the JspWriter returned by + // JspContext.pushBody(java.io.Writer writer) must behave as + // though it were unbuffered. This means that its getBufferSize() + // must always return 0. + return (writer == null) ? bufferSize : 0; + } + + /** + * @return the number of bytes unused in the buffer + */ + @Override + public int getRemaining() { + return (writer == null) ? bufferSize-nextChar : 0; + } + + /** + * Return the value of this BodyJspWriter as a Reader. + * Note: this is after evaluation!! There are no scriptlets, + * etc in this stream. + * + * @return the value of this BodyJspWriter as a Reader + */ + @Override + public Reader getReader() { + return (writer == null) ? new CharArrayReader (cb, 0, nextChar) : null; + } + + /** + * Return the value of the BodyJspWriter as a String. + * Note: this is after evaluation!! There are no scriptlets, + * etc in this stream. + * + * @return the value of the BodyJspWriter as a String + */ + @Override + public String getString() { + return (writer == null) ? new String(cb, 0, nextChar) : null; + } + + /** + * Write the contents of this BodyJspWriter into a Writer. + * Subclasses are likely to do interesting things with the + * implementation so some things are extra efficient. + * + * @param out The writer into which to place the contents of this body + * evaluation + * @throws IOException Error writing to writer + */ + @Override + public void writeOut(Writer out) throws IOException { + if (writer == null) { + out.write(cb, 0, nextChar); + // Flush not called as the writer passed could be a BodyContent and + // it doesn't allow to flush. + } + } + + /** + * Sets the writer to which all output is written. + */ + void setWriter(Writer writer) { + this.writer = writer; + closed = false; + if (writer == null) { + clearBody(); + } + } + + /** + * This method shall "reset" the internal state of a BodyContentImpl, + * releasing all internal references, and preparing it for potential + * reuse by a later invocation of {@link PageContextImpl#pushBody(Writer)}. + * + *

    Note, that BodyContentImpl instances are usually owned by a + * PageContextImpl instance, and PageContextImpl instances are recycled + * and reused. + * + * @see PageContextImpl#release() + */ + protected void recycle() { + this.writer = null; + try { + this.clear(); + } catch (IOException ex) { + // ignore + } + } + + private void ensureOpen() throws IOException { + if (closed) { + throw new IOException(Localizer.getMessage("jsp.error.stream.closed")); + } + } + + /** + * Reallocates buffer since the spec requires it to be unbounded. + */ + private void reAllocBuff(int len) { + + if (bufferSize + len <= cb.length) { + bufferSize = cb.length; + return; + } + + if (len < cb.length) { + len = cb.length; + } + + char[] tmp = new char[cb.length + len]; + System.arraycopy(cb, 0, tmp, 0, cb.length); + cb = tmp; + bufferSize = cb.length; + } + + +} diff --git a/java/org/apache/jasper/runtime/ExceptionUtils.java b/java/org/apache/jasper/runtime/ExceptionUtils.java new file mode 100644 index 0000000..344bce2 --- /dev/null +++ b/java/org/apache/jasper/runtime/ExceptionUtils.java @@ -0,0 +1,60 @@ +/* + * 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.jasper.runtime; + +import java.lang.reflect.InvocationTargetException; + + +/** + * Utilities for handling Throwables and Exceptions. + */ +public class ExceptionUtils { + + /** + * Checks whether the supplied Throwable is one that needs to be + * rethrown and swallows all others. + * @param t the Throwable to check + */ + public static void handleThrowable(Throwable t) { + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof StackOverflowError) { + // Swallow silently - it should be recoverable + return; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + // All other instances of Throwable will be silently swallowed + } + + /** + * Checks whether the supplied Throwable is an instance of + * InvocationTargetException and returns the throwable that is + * wrapped by it, if there is any. + * + * @param t the Throwable to check + * @return t or t.getCause() + */ + public static Throwable unwrapInvocationTargetException(Throwable t) { + if (t instanceof InvocationTargetException && t.getCause() != null) { + return t.getCause(); + } + return t; + } +} diff --git a/java/org/apache/jasper/runtime/HttpJspBase.java b/java/org/apache/jasper/runtime/HttpJspBase.java new file mode 100644 index 0000000..e5e974f --- /dev/null +++ b/java/org/apache/jasper/runtime/HttpJspBase.java @@ -0,0 +1,91 @@ +/* + * 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.jasper.runtime; + +import java.io.IOException; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.HttpJspPage; + +import org.apache.jasper.Constants; +import org.apache.jasper.compiler.Localizer; + +/** + * This is the super class of all JSP-generated servlets. + * + * @author Anil K. Vijendran + */ +public abstract class HttpJspBase extends HttpServlet implements HttpJspPage { + + private static final long serialVersionUID = 1L; + + protected HttpJspBase() { + } + + @Override + public final void init(ServletConfig config) + throws ServletException + { + super.init(config); + jspInit(); + _jspInit(); + } + + @Override + public String getServletInfo() { + return Localizer.getMessage("jsp.engine.info", Constants.SPEC_VERSION); + } + + @Override + public final void destroy() { + jspDestroy(); + _jspDestroy(); + } + + /** + * Entry point into service. + */ + @Override + public final void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + _jspService(request, response); + } + + @Override + public void jspInit() { + } + + public void _jspInit() { + } + + @Override + public void jspDestroy() { + } + + protected void _jspDestroy() { + } + + @Override + public abstract void _jspService(HttpServletRequest request, + HttpServletResponse response) + throws ServletException, IOException; +} diff --git a/java/org/apache/jasper/runtime/InstanceManagerFactory.java b/java/org/apache/jasper/runtime/InstanceManagerFactory.java new file mode 100644 index 0000000..4a63ad3 --- /dev/null +++ b/java/org/apache/jasper/runtime/InstanceManagerFactory.java @@ -0,0 +1,38 @@ +/* + * 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.jasper.runtime; + +import jakarta.servlet.ServletConfig; + +import org.apache.jasper.compiler.Localizer; +import org.apache.tomcat.InstanceManager; + +public class InstanceManagerFactory { + + private InstanceManagerFactory() { + } + + public static InstanceManager getInstanceManager(ServletConfig config) { + InstanceManager instanceManager = + (InstanceManager) config.getServletContext().getAttribute(InstanceManager.class.getName()); + if (instanceManager == null) { + throw new IllegalStateException(Localizer.getMessage("jsp.error.noInstanceManager")); + } + return instanceManager; + } + +} diff --git a/java/org/apache/jasper/runtime/JspApplicationContextImpl.java b/java/org/apache/jasper/runtime/JspApplicationContextImpl.java new file mode 100644 index 0000000..82aa1ab --- /dev/null +++ b/java/org/apache/jasper/runtime/JspApplicationContextImpl.java @@ -0,0 +1,138 @@ +/* + * 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.jasper.runtime; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; + +import jakarta.el.CompositeELResolver; +import jakarta.el.ELContext; +import jakarta.el.ELContextEvent; +import jakarta.el.ELContextListener; +import jakarta.el.ELResolver; +import jakarta.el.ExpressionFactory; +import jakarta.servlet.ServletContext; +import jakarta.servlet.jsp.JspApplicationContext; +import jakarta.servlet.jsp.JspContext; + +import org.apache.jasper.Constants; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.el.ELContextImpl; +import org.apache.jasper.el.JasperELResolver; + +/** + * Implementation of JspApplicationContext + * + * @author Jacob Hookom + */ +public class JspApplicationContextImpl implements JspApplicationContext { + + private static final String KEY = JspApplicationContextImpl.class.getName(); + + private final ExpressionFactory expressionFactory = + ExpressionFactory.newInstance(); + + private final List contextListeners = new ArrayList<>(); + + private final List resolvers = new ArrayList<>(); + + private boolean instantiated = false; + + private ELResolver resolver; + + public JspApplicationContextImpl() { + + } + + @Override + public void addELContextListener(ELContextListener listener) { + if (listener == null) { + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.nullArgument")); + } + this.contextListeners.add(listener); + } + + public static JspApplicationContextImpl getInstance(ServletContext context) { + if (context == null) { + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.nullArgument")); + } + JspApplicationContextImpl impl = (JspApplicationContextImpl) context + .getAttribute(KEY); + if (impl == null) { + impl = new JspApplicationContextImpl(); + context.setAttribute(KEY, impl); + } + return impl; + } + + public ELContextImpl createELContext(JspContext context) { + if (context == null) { + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.nullArgument")); + } + + // create ELContext for JspContext + final ELResolver r = this.createELResolver(); + ELContextImpl ctx; + if (Constants.IS_SECURITY_ENABLED) { + ctx = AccessController.doPrivileged((PrivilegedAction) () -> new ELContextImpl(r)); + } else { + ctx = new ELContextImpl(r); + } + ctx.putContext(JspContext.class, context); + + // alert all ELContextListeners + fireListeners(ctx); + + return ctx; + } + + protected void fireListeners(ELContext elContext) { + ELContextEvent event = new ELContextEvent(elContext); + for (ELContextListener contextListener : this.contextListeners) { + contextListener.contextCreated(event); + } + } + + private ELResolver createELResolver() { + this.instantiated = true; + if (this.resolver == null) { + CompositeELResolver r = new JasperELResolver(this.resolvers, + expressionFactory.getStreamELResolver()); + this.resolver = r; + } + return this.resolver; + } + + @Override + public void addELResolver(ELResolver resolver) throws IllegalStateException { + if (resolver == null) { + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.nullArgument")); + } + if (this.instantiated) { + throw new IllegalStateException(Localizer.getMessage("jsp.error.cannotAddResolver")); + } + this.resolvers.add(resolver); + } + + @Override + public ExpressionFactory getExpressionFactory() { + return expressionFactory; + } + +} diff --git a/java/org/apache/jasper/runtime/JspContextWrapper.java b/java/org/apache/jasper/runtime/JspContextWrapper.java new file mode 100644 index 0000000..9601d81 --- /dev/null +++ b/java/org/apache/jasper/runtime/JspContextWrapper.java @@ -0,0 +1,678 @@ +/* + * 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.jasper.runtime; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.el.EvaluationListener; +import jakarta.el.FunctionMapper; +import jakarta.el.ImportHandler; +import jakarta.el.VariableMapper; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.jsp.JspApplicationContext; +import jakarta.servlet.jsp.JspContext; +import jakarta.servlet.jsp.JspFactory; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.el.ELException; +import jakarta.servlet.jsp.el.ExpressionEvaluator; +import jakarta.servlet.jsp.el.NotFoundELResolver; +import jakarta.servlet.jsp.el.VariableResolver; +import jakarta.servlet.jsp.tagext.BodyContent; +import jakarta.servlet.jsp.tagext.JspTag; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.apache.jasper.compiler.Localizer; + +/** + * Implementation of a JSP Context Wrapper. + * + * The JSP Context Wrapper is a JspContext created and maintained by a tag + * handler implementation. It wraps the Invoking JSP Context, that is, the + * JspContext instance passed to the tag handler by the invoking page via + * setJspContext(). + * + * @author Kin-man Chung + * @author Jan Luehe + * @author Jacob Hookom + */ +@SuppressWarnings("deprecation") // Have to support old JSP EL API +public class JspContextWrapper extends PageContext implements VariableResolver { + + private final JspTag jspTag; + + // Invoking JSP context + private final PageContext invokingJspCtxt; + + private final transient HashMap pageAttributes; + + // ArrayList of NESTED scripting variables + private final ArrayList nestedVars; + + // ArrayList of AT_BEGIN scripting variables + private final ArrayList atBeginVars; + + // ArrayList of AT_END scripting variables + private final ArrayList atEndVars; + + private final Map aliases; + + private final HashMap originalNestedVars; + + private ServletContext servletContext = null; + + private ELContext elContext = null; + + private final PageContext rootJspCtxt; + + public JspContextWrapper(JspTag jspTag, JspContext jspContext, + ArrayList nestedVars, ArrayList atBeginVars, + ArrayList atEndVars, Map aliases) { + this.jspTag = jspTag; + this.invokingJspCtxt = (PageContext) jspContext; + if (jspContext instanceof JspContextWrapper) { + rootJspCtxt = ((JspContextWrapper)jspContext).rootJspCtxt; + } else { + rootJspCtxt = invokingJspCtxt; + } + this.nestedVars = nestedVars; + this.atBeginVars = atBeginVars; + this.atEndVars = atEndVars; + this.pageAttributes = new HashMap<>(16); + this.aliases = aliases; + + if (nestedVars != null) { + this.originalNestedVars = new HashMap<>(nestedVars.size()); + } else { + this.originalNestedVars = null; + } + syncBeginTagFile(); + } + + @Override + public void initialize(Servlet servlet, ServletRequest request, + ServletResponse response, String errorPageURL, + boolean needsSession, int bufferSize, boolean autoFlush) + throws IOException, IllegalStateException, IllegalArgumentException { + } + + @Override + public Object getAttribute(String name) { + + if (name == null) { + throw new NullPointerException(Localizer + .getMessage("jsp.error.attribute.null_name")); + } + + return pageAttributes.get(name); + } + + @Override + public Object getAttribute(String name, int scope) { + + if (name == null) { + throw new NullPointerException(Localizer + .getMessage("jsp.error.attribute.null_name")); + } + + if (scope == PAGE_SCOPE) { + return pageAttributes.get(name); + } + + return rootJspCtxt.getAttribute(name, scope); + } + + @Override + public void setAttribute(String name, Object value) { + + if (name == null) { + throw new NullPointerException(Localizer + .getMessage("jsp.error.attribute.null_name")); + } + + if (value != null) { + pageAttributes.put(name, value); + } else { + removeAttribute(name, PAGE_SCOPE); + } + } + + @Override + public void setAttribute(String name, Object value, int scope) { + + if (name == null) { + throw new NullPointerException(Localizer + .getMessage("jsp.error.attribute.null_name")); + } + + if (scope == PAGE_SCOPE) { + if (value != null) { + pageAttributes.put(name, value); + } else { + removeAttribute(name, PAGE_SCOPE); + } + } else { + rootJspCtxt.setAttribute(name, value, scope); + } + } + + @Override + public Object findAttribute(String name) { + + if (name == null) { + throw new NullPointerException(Localizer + .getMessage("jsp.error.attribute.null_name")); + } + + Object o = pageAttributes.get(name); + if (o == null) { + o = rootJspCtxt.getAttribute(name, REQUEST_SCOPE); + if (o == null) { + if (getSession() != null) { + try { + o = rootJspCtxt.getAttribute(name, SESSION_SCOPE); + } catch (IllegalStateException ise) { + // Session has been invalidated. + // Ignore and fall through to application scope. + } + } + if (o == null) { + o = rootJspCtxt.getAttribute(name, APPLICATION_SCOPE); + } + } + } + + return o; + } + + @Override + public void removeAttribute(String name) { + + if (name == null) { + throw new NullPointerException(Localizer + .getMessage("jsp.error.attribute.null_name")); + } + + pageAttributes.remove(name); + rootJspCtxt.removeAttribute(name, REQUEST_SCOPE); + if (getSession() != null) { + rootJspCtxt.removeAttribute(name, SESSION_SCOPE); + } + rootJspCtxt.removeAttribute(name, APPLICATION_SCOPE); + } + + @Override + public void removeAttribute(String name, int scope) { + + if (name == null) { + throw new NullPointerException(Localizer + .getMessage("jsp.error.attribute.null_name")); + } + + if (scope == PAGE_SCOPE) { + pageAttributes.remove(name); + } else { + rootJspCtxt.removeAttribute(name, scope); + } + } + + @Override + public int getAttributesScope(String name) { + + if (name == null) { + throw new NullPointerException(Localizer + .getMessage("jsp.error.attribute.null_name")); + } + + if (pageAttributes.get(name) != null) { + return PAGE_SCOPE; + } else { + return rootJspCtxt.getAttributesScope(name); + } + } + + @Override + public Enumeration getAttributeNamesInScope(int scope) { + if (scope == PAGE_SCOPE) { + return Collections.enumeration(pageAttributes.keySet()); + } + + return rootJspCtxt.getAttributeNamesInScope(scope); + } + + @Override + public void release() { + invokingJspCtxt.release(); + } + + @Override + public JspWriter getOut() { + return rootJspCtxt.getOut(); + } + + @Override + public HttpSession getSession() { + return rootJspCtxt.getSession(); + } + + @Override + public Object getPage() { + return invokingJspCtxt.getPage(); + } + + @Override + public ServletRequest getRequest() { + return invokingJspCtxt.getRequest(); + } + + @Override + public ServletResponse getResponse() { + return rootJspCtxt.getResponse(); + } + + @Override + public Exception getException() { + return invokingJspCtxt.getException(); + } + + @Override + public ServletConfig getServletConfig() { + return invokingJspCtxt.getServletConfig(); + } + + @Override + public ServletContext getServletContext() { + if (servletContext == null) { + servletContext = rootJspCtxt.getServletContext(); + } + return servletContext; + } + + @Override + public void forward(String relativeUrlPath) throws ServletException, + IOException { + invokingJspCtxt.forward(relativeUrlPath); + } + + @Override + public void include(String relativeUrlPath) throws ServletException, + IOException { + invokingJspCtxt.include(relativeUrlPath); + } + + @Override + public void include(String relativeUrlPath, boolean flush) + throws ServletException, IOException { + invokingJspCtxt.include(relativeUrlPath, false); + } + + @Override + @Deprecated + public VariableResolver getVariableResolver() { + return this; + } + + @Override + public BodyContent pushBody() { + return invokingJspCtxt.pushBody(); + } + + @Override + public JspWriter pushBody(Writer writer) { + return invokingJspCtxt.pushBody(writer); + } + + @Override + public JspWriter popBody() { + return invokingJspCtxt.popBody(); + } + + @Override + @Deprecated + public ExpressionEvaluator getExpressionEvaluator() { + return invokingJspCtxt.getExpressionEvaluator(); + } + + @Override + public void handlePageException(Exception ex) throws IOException, + ServletException { + // Should never be called since handleException() called with a + // Throwable in the generated servlet. + handlePageException((Throwable) ex); + } + + @Override + public void handlePageException(Throwable t) throws IOException, + ServletException { + invokingJspCtxt.handlePageException(t); + } + + /** + * VariableResolver interface + */ + @Override + @Deprecated + public Object resolveVariable(String pName) throws ELException { + ELContext ctx = this.getELContext(); + return ctx.getELResolver().getValue(ctx, null, pName); + } + + /** + * Synchronize variables at begin of tag file + */ + public void syncBeginTagFile() { + saveNestedVariables(); + } + + /** + * Synchronize variables before fragment invocation + */ + public void syncBeforeInvoke() { + copyTagToPageScope(VariableInfo.NESTED); + copyTagToPageScope(VariableInfo.AT_BEGIN); + } + + /** + * Synchronize variables at end of tag file + */ + public void syncEndTagFile() { + copyTagToPageScope(VariableInfo.AT_BEGIN); + copyTagToPageScope(VariableInfo.AT_END); + restoreNestedVariables(); + } + + /** + * Copies the variables of the given scope from the virtual page scope of + * this JSP context wrapper to the page scope of the invoking JSP context. + * + * @param scope + * variable scope (one of NESTED, AT_BEGIN, or AT_END) + */ + private void copyTagToPageScope(int scope) { + Iterator iter = null; + + switch (scope) { + case VariableInfo.NESTED: + if (nestedVars != null) { + iter = nestedVars.iterator(); + } + break; + case VariableInfo.AT_BEGIN: + if (atBeginVars != null) { + iter = atBeginVars.iterator(); + } + break; + case VariableInfo.AT_END: + if (atEndVars != null) { + iter = atEndVars.iterator(); + } + break; + } + + while ((iter != null) && iter.hasNext()) { + String varName = iter.next(); + Object obj = getAttribute(varName); + varName = findAlias(varName); + if (obj != null) { + invokingJspCtxt.setAttribute(varName, obj); + } else { + invokingJspCtxt.removeAttribute(varName, PAGE_SCOPE); + } + } + } + + /** + * Saves the values of any NESTED variables that are present in the invoking + * JSP context, so they can later be restored. + */ + private void saveNestedVariables() { + if (nestedVars != null) { + for (String varName : nestedVars) { + varName = findAlias(varName); + Object obj = invokingJspCtxt.getAttribute(varName); + if (obj != null) { + originalNestedVars.put(varName, obj); + } + } + } + } + + /** + * Restores the values of any NESTED variables in the invoking JSP context. + */ + private void restoreNestedVariables() { + if (nestedVars != null) { + for (String varName : nestedVars) { + varName = findAlias(varName); + Object obj = originalNestedVars.get(varName); + if (obj != null) { + invokingJspCtxt.setAttribute(varName, obj); + } else { + invokingJspCtxt.removeAttribute(varName, PAGE_SCOPE); + } + } + } + } + + /** + * Checks to see if the given variable name is used as an alias, and if so, + * returns the variable name for which it is used as an alias. + * + * @param varName + * The variable name to check + * @return The variable name for which varName is used as an alias, or + * varName if it is not being used as an alias + */ + private String findAlias(String varName) { + + if (aliases == null) { + return varName; + } + + String alias = aliases.get(varName); + if (alias == null) { + return varName; + } + return alias; + } + + @Override + public ELContext getELContext() { + if (elContext == null) { + elContext = new ELContextWrapper(rootJspCtxt.getELContext(), jspTag, this); + JspFactory factory = JspFactory.getDefaultFactory(); + JspApplicationContext jspAppCtxt = factory.getJspApplicationContext(servletContext); + if (jspAppCtxt instanceof JspApplicationContextImpl) { + ((JspApplicationContextImpl) jspAppCtxt).fireListeners(elContext); + } + } + return elContext; + } + + + static class ELContextWrapper extends ELContext { + + private final ELContext wrapped; + private final JspTag jspTag; + private final PageContext pageContext; + private ImportHandler importHandler; + + private ELContextWrapper(ELContext wrapped, JspTag jspTag, PageContext pageContext) { + this.wrapped = wrapped; + this.jspTag = jspTag; + this.pageContext = pageContext; + } + + ELContext getWrappedELContext() { + return wrapped; + } + + @Override + public void setPropertyResolved(boolean resolved) { + wrapped.setPropertyResolved(resolved); + } + + @Override + public void setPropertyResolved(Object base, Object property) { + wrapped.setPropertyResolved(base, property); + } + + @Override + public boolean isPropertyResolved() { + return wrapped.isPropertyResolved(); + } + + @Override + public void putContext(Class key, Object contextObject) { + if (key != JspContext.class) { + wrapped.putContext(key, contextObject); + } + } + + @Override + public Object getContext(Class key) { + if (key == JspContext.class) { + return pageContext; + } + if (key == NotFoundELResolver.class) { + if (jspTag instanceof JspSourceDirectives) { + return Boolean.valueOf(((JspSourceDirectives) jspTag).getErrorOnELNotFound()); + } else { + // returning Boolean.FALSE would have the same effect + return null; + } + } + return wrapped.getContext(key); + } + + @Override + public ImportHandler getImportHandler() { + if (importHandler == null) { + importHandler = new ImportHandler(); + if (jspTag instanceof JspSourceImports) { + Set packageImports = ((JspSourceImports) jspTag).getPackageImports(); + if (packageImports != null) { + for (String packageImport : packageImports) { + importHandler.importPackage(packageImport); + } + } + Set classImports = ((JspSourceImports) jspTag).getClassImports(); + if (classImports != null) { + for (String classImport : classImports) { + importHandler.importClass(classImport); + } + } + } + + } + return importHandler; + } + + @Override + public Locale getLocale() { + return wrapped.getLocale(); + } + + @Override + public void setLocale(Locale locale) { + wrapped.setLocale(locale); + } + + @Override + public void addEvaluationListener(EvaluationListener listener) { + wrapped.addEvaluationListener(listener); + } + + @Override + public List getEvaluationListeners() { + return wrapped.getEvaluationListeners(); + } + + @Override + public void notifyBeforeEvaluation(String expression) { + wrapped.notifyBeforeEvaluation(expression); + } + + @Override + public void notifyAfterEvaluation(String expression) { + wrapped.notifyAfterEvaluation(expression); + } + + @Override + public void notifyPropertyResolved(Object base, Object property) { + wrapped.notifyPropertyResolved(base, property); + } + + @Override + public boolean isLambdaArgument(String name) { + return wrapped.isLambdaArgument(name); + } + + @Override + public Object getLambdaArgument(String name) { + return wrapped.getLambdaArgument(name); + } + + @Override + public void enterLambdaScope(Map arguments) { + wrapped.enterLambdaScope(arguments); + } + + @Override + public void exitLambdaScope() { + wrapped.exitLambdaScope(); + } + + @Override + public T convertToType(Object obj, Class type) { + return wrapped.convertToType(obj, type); + } + + @Override + public ELResolver getELResolver() { + return wrapped.getELResolver(); + } + + @Override + public FunctionMapper getFunctionMapper() { + return wrapped.getFunctionMapper(); + } + + @Override + public VariableMapper getVariableMapper() { + return wrapped.getVariableMapper(); + } + } +} diff --git a/java/org/apache/jasper/runtime/JspFactoryImpl.java b/java/org/apache/jasper/runtime/JspFactoryImpl.java new file mode 100644 index 0000000..c84028c --- /dev/null +++ b/java/org/apache/jasper/runtime/JspFactoryImpl.java @@ -0,0 +1,215 @@ +/* + * 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.jasper.runtime; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.jsp.JspApplicationContext; +import jakarta.servlet.jsp.JspEngineInfo; +import jakarta.servlet.jsp.JspFactory; +import jakarta.servlet.jsp.PageContext; + +import org.apache.jasper.Constants; + +/** + * Implementation of JspFactory. + * + * @author Anil K. Vijendran + */ +public class JspFactoryImpl extends JspFactory { + + private final ThreadLocal localPool = new ThreadLocal<>(); + private int poolSize = -1; + + @Override + public PageContext getPageContext(Servlet servlet, ServletRequest request, + ServletResponse response, String errorPageURL, boolean needsSession, + int bufferSize, boolean autoflush) { + + if( Constants.IS_SECURITY_ENABLED ) { + PrivilegedGetPageContext dp = new PrivilegedGetPageContext( + this, servlet, request, response, errorPageURL, + needsSession, bufferSize, autoflush); + return AccessController.doPrivileged(dp); + } else { + return internalGetPageContext(servlet, request, response, + errorPageURL, needsSession, + bufferSize, autoflush); + } + } + + @Override + public void releasePageContext(PageContext pc) { + if( pc == null ) { + return; + } + if( Constants.IS_SECURITY_ENABLED ) { + PrivilegedReleasePageContext dp = new PrivilegedReleasePageContext( + this,pc); + AccessController.doPrivileged(dp); + } else { + internalReleasePageContext(pc); + } + } + + @Override + public JspEngineInfo getEngineInfo() { + return new JspEngineInfo() { + @Override + public String getSpecificationVersion() { + return Constants.SPEC_VERSION; + } + }; + } + + public void setPoolSize(int poolSize) { + this.poolSize = poolSize; + } + + private PageContext internalGetPageContext(Servlet servlet, ServletRequest request, + ServletResponse response, String errorPageURL, boolean needsSession, + int bufferSize, boolean autoflush) { + + PageContext pc; + if (poolSize > 0) { + PageContextPool pool = localPool.get(); + if (pool == null) { + pool = new PageContextPool(poolSize); + localPool.set(pool); + } + pc = pool.get(); + if (pc == null) { + pc = new PageContextImpl(); + } + } else { + pc = new PageContextImpl(); + } + + try { + pc.initialize(servlet, request, response, errorPageURL, + needsSession, bufferSize, autoflush); + } catch (IOException ioe) { + // Implementation never throws IOE but can't change the signature + // since it is part of the JSP API + } + + return pc; + } + + private void internalReleasePageContext(PageContext pc) { + pc.release(); + if (poolSize > 0 && (pc instanceof PageContextImpl)) { + localPool.get().put(pc); + } + } + + private static class PrivilegedGetPageContext + implements PrivilegedAction { + + private JspFactoryImpl factory; + private Servlet servlet; + private ServletRequest request; + private ServletResponse response; + private String errorPageURL; + private boolean needsSession; + private int bufferSize; + private boolean autoflush; + + PrivilegedGetPageContext(JspFactoryImpl factory, Servlet servlet, + ServletRequest request, ServletResponse response, String errorPageURL, + boolean needsSession, int bufferSize, boolean autoflush) { + this.factory = factory; + this.servlet = servlet; + this.request = request; + this.response = response; + this.errorPageURL = errorPageURL; + this.needsSession = needsSession; + this.bufferSize = bufferSize; + this.autoflush = autoflush; + } + + @Override + public PageContext run() { + return factory.internalGetPageContext(servlet, request, response, + errorPageURL, needsSession, bufferSize, autoflush); + } + } + + private static class PrivilegedReleasePageContext + implements PrivilegedAction { + + private JspFactoryImpl factory; + private PageContext pageContext; + + PrivilegedReleasePageContext(JspFactoryImpl factory, + PageContext pageContext) { + this.factory = factory; + this.pageContext = pageContext; + } + + @Override + public Void run() { + factory.internalReleasePageContext(pageContext); + return null; + } + } + + private static final class PageContextPool { + + private final PageContext[] pool; + + private int current = -1; + + PageContextPool(int poolSize) { + this.pool = new PageContext[poolSize]; + } + + public void put(PageContext o) { + if (current < (pool.length - 1)) { + current++; + pool[current] = o; + } + } + + public PageContext get() { + PageContext item = null; + if (current >= 0) { + item = pool[current]; + current--; + } + return item; + } + + } + + @Override + public JspApplicationContext getJspApplicationContext( + final ServletContext context) { + if (Constants.IS_SECURITY_ENABLED) { + return AccessController.doPrivileged( + (PrivilegedAction) () -> JspApplicationContextImpl.getInstance(context)); + } else { + return JspApplicationContextImpl.getInstance(context); + } + } +} diff --git a/java/org/apache/jasper/runtime/JspFragmentHelper.java b/java/org/apache/jasper/runtime/JspFragmentHelper.java new file mode 100644 index 0000000..41d1752 --- /dev/null +++ b/java/org/apache/jasper/runtime/JspFragmentHelper.java @@ -0,0 +1,64 @@ +/* + * 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.jasper.runtime; + +import jakarta.servlet.jsp.JspContext; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.JspFragment; +import jakarta.servlet.jsp.tagext.JspTag; + +/** + * Helper class from which all Jsp Fragment helper classes extend. + * This class allows for the emulation of numerous fragments within + * a single class, which in turn reduces the load on the class loader + * since there are potentially many JspFragments in a single page. + *

    + * The class also provides various utility methods for JspFragment + * implementations. + * + * @author Mark Roth + */ +public abstract class JspFragmentHelper extends JspFragment { + + protected final int discriminator; + protected final JspContext jspContext; + protected final PageContext _jspx_page_context; + protected final JspTag parentTag; + + public JspFragmentHelper( int discriminator, JspContext jspContext, + JspTag parentTag ) + { + this.discriminator = discriminator; + this.jspContext = jspContext; + if(jspContext instanceof PageContext) { + _jspx_page_context = (PageContext)jspContext; + } else { + _jspx_page_context = null; + } + this.parentTag = parentTag; + } + + @Override + public JspContext getJspContext() { + return this.jspContext; + } + + public JspTag getParentTag() { + return this.parentTag; + } + +} diff --git a/java/org/apache/jasper/runtime/JspRuntimeLibrary.java b/java/org/apache/jasper/runtime/JspRuntimeLibrary.java new file mode 100644 index 0000000..d705e6c --- /dev/null +++ b/java/org/apache/jasper/runtime/JspRuntimeLibrary.java @@ -0,0 +1,1113 @@ +/* + * 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.jasper.runtime; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorManager; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.lang.reflect.Method; +import java.util.Enumeration; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.BodyContent; +import jakarta.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.tagext.Tag; + +import org.apache.jasper.JasperException; +import org.apache.jasper.compiler.Localizer; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; + +/** + * Bunch of util methods that are used by code generated for useBean, + * getProperty and setProperty. + * + * The __begin, __end stuff is there so that the JSP engine can + * actually parse this file and inline them if people don't want + * runtime dependencies on this class. However, I'm not sure if that + * works so well right now. It got forgotten at some point. -akv + * + * @author Mandar Raje + * @author Shawn Bayern + */ +public class JspRuntimeLibrary { + + public static final boolean GRAAL; + + static { + boolean result = false; + try { + Class nativeImageClazz = Class.forName("org.graalvm.nativeimage.ImageInfo"); + result = nativeImageClazz.getMethod("inImageCode").invoke(null) != null; + // Note: This will also be true for the Graal substrate VM + } catch (ClassNotFoundException e) { + // Must be Graal + } catch (ReflectiveOperationException | IllegalArgumentException e) { + // Should never happen + } + GRAAL = result || System.getProperty("org.graalvm.nativeimage.imagecode") != null; + } + + /** + * Returns the value of the jakarta.servlet.error.exception request + * attribute value, if present, otherwise the value of the + * jakarta.servlet.jsp.jspException request attribute value. + * + * This method is called at the beginning of the generated servlet code + * for a JSP error page, when the "exception" implicit scripting language + * variable is initialized. + * @param request The Servlet request + * @return the throwable in the error attribute if any + */ + public static Throwable getThrowable(ServletRequest request) { + Throwable error = (Throwable) request.getAttribute( + RequestDispatcher.ERROR_EXCEPTION); + if (error == null) { + error = (Throwable) request.getAttribute(PageContext.EXCEPTION); + if (error != null) { + /* + * The only place that sets JSP_EXCEPTION is + * PageContextImpl.handlePageException(). It really should set + * SERVLET_EXCEPTION, but that would interfere with the + * ErrorReportValve. Therefore, if JSP_EXCEPTION is set, we + * need to set SERVLET_EXCEPTION. + */ + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, error); + } + } + + return error; + } + + public static boolean coerceToBoolean(String s) { + if (s == null || s.length() == 0) { + return false; + } else { + return Boolean.parseBoolean(s); + } + } + + public static byte coerceToByte(String s) { + if (s == null || s.length() == 0) { + return (byte) 0; + } else { + return Byte.parseByte(s); + } + } + + public static char coerceToChar(String s) { + if (s == null || s.length() == 0) { + return (char) 0; + } else { + return s.charAt(0); + } + } + + public static double coerceToDouble(String s) { + if (s == null || s.length() == 0) { + return 0; + } else { + return Double.parseDouble(s); + } + } + + public static float coerceToFloat(String s) { + if (s == null || s.length() == 0) { + return 0; + } else { + return Float.parseFloat(s); + } + } + + public static int coerceToInt(String s) { + if (s == null || s.length() == 0) { + return 0; + } else { + return Integer.parseInt(s); + } + } + + public static short coerceToShort(String s) { + if (s == null || s.length() == 0) { + return (short) 0; + } else { + return Short.parseShort(s); + } + } + + public static long coerceToLong(String s) { + if (s == null || s.length() == 0) { + return 0; + } else { + return Long.parseLong(s); + } + } + + public static Object coerce(String s, Class target) { + + boolean isNullOrEmpty = (s == null || s.length() == 0); + + if (target == Boolean.class) { + if (isNullOrEmpty) { + s = "false"; + } + return Boolean.valueOf(s); + } else if (target == Byte.class) { + if (isNullOrEmpty) { + return Byte.valueOf((byte) 0); + } else { + return Byte.valueOf(s); + } + } else if (target == Character.class) { + if (isNullOrEmpty) { + return Character.valueOf((char) 0); + } else { + @SuppressWarnings("null") + Character result = Character.valueOf(s.charAt(0)); + return result; + } + } else if (target == Double.class) { + if (isNullOrEmpty) { + return Double.valueOf(0); + } else { + return Double.valueOf(s); + } + } else if (target == Float.class) { + if (isNullOrEmpty) { + return Float.valueOf(0); + } else { + return Float.valueOf(s); + } + } else if (target == Integer.class) { + if (isNullOrEmpty) { + return Integer.valueOf(0); + } else { + return Integer.valueOf(s); + } + } else if (target == Short.class) { + if (isNullOrEmpty) { + return Short.valueOf((short) 0); + } else { + return Short.valueOf(s); + } + } else if (target == Long.class) { + if (isNullOrEmpty) { + return Long.valueOf(0); + } else { + return Long.valueOf(s); + } + } else { + return null; + } + } + + // __begin convertMethod + public static Object convert(String propertyName, String s, Class t, + Class propertyEditorClass) + throws JasperException + { + try { + if (s == null) { + if (t.equals(Boolean.class) || t.equals(Boolean.TYPE)) { + s = "false"; + } else { + return null; + } + } + if (propertyEditorClass != null) { + return getValueFromBeanInfoPropertyEditor( + t, propertyName, s, propertyEditorClass); + } else if (t.equals(Boolean.class) || t.equals(Boolean.TYPE)) { + return Boolean.valueOf(s); + } else if (t.equals(Byte.class) || t.equals(Byte.TYPE)) { + if (s.length() == 0) { + return Byte.valueOf((byte)0); + } else { + return Byte.valueOf(s); + } + } else if (t.equals(Character.class) || t.equals(Character.TYPE)) { + if (s.length() == 0) { + return Character.valueOf((char) 0); + } else { + return Character.valueOf(s.charAt(0)); + } + } else if (t.equals(Double.class) || t.equals(Double.TYPE)) { + if (s.length() == 0) { + return Double.valueOf(0); + } else { + return Double.valueOf(s); + } + } else if (t.equals(Integer.class) || t.equals(Integer.TYPE)) { + if (s.length() == 0) { + return Integer.valueOf(0); + } else { + return Integer.valueOf(s); + } + } else if (t.equals(Float.class) || t.equals(Float.TYPE)) { + if (s.length() == 0) { + return Float.valueOf(0); + } else { + return Float.valueOf(s); + } + } else if (t.equals(Long.class) || t.equals(Long.TYPE)) { + if (s.length() == 0) { + return Long.valueOf(0); + } else { + return Long.valueOf(s); + } + } else if (t.equals(Short.class) || t.equals(Short.TYPE)) { + if (s.length() == 0) { + return Short.valueOf((short) 0); + } else { + return Short.valueOf(s); + } + } else if ( t.equals(String.class) ) { + return s; + } else if (t.getName().equals("java.lang.Object")) { + return new String(s); + } else { + return getValueFromPropertyEditorManager( + t, propertyName, s); + } + } catch (Exception ex) { + throw new JasperException(ex); + } + } + // __end convertMethod + + // __begin introspectMethod + public static void introspect(Object bean, ServletRequest request) + throws JasperException + { + Enumeration e = request.getParameterNames(); + while ( e.hasMoreElements() ) { + String name = e.nextElement(); + String value = request.getParameter(name); + introspecthelper(bean, name, value, request, name, true); + } + } + // __end introspectMethod + + // __begin introspecthelperMethod + public static void introspecthelper(Object bean, String prop, + String value, ServletRequest request, + String param, boolean ignoreMethodNF) + throws JasperException { + Method method = null; + Class type = null; + Class propertyEditorClass = null; + try { + if (GRAAL) { + method = getWriteMethod(bean.getClass(), prop); + if (method.getParameterTypes().length > 0) { + type = method.getParameterTypes()[0]; + } + } else { + java.beans.BeanInfo info + = java.beans.Introspector.getBeanInfo(bean.getClass()); + if ( info != null ) { + java.beans.PropertyDescriptor pd[] + = info.getPropertyDescriptors(); + for (java.beans.PropertyDescriptor propertyDescriptor : pd) { + if (propertyDescriptor.getName().equals(prop)) { + method = propertyDescriptor.getWriteMethod(); + type = propertyDescriptor.getPropertyType(); + propertyEditorClass = propertyDescriptor.getPropertyEditorClass(); + break; + } + } + } + } + if (method != null && type != null) { + if (type.isArray()) { + if (request == null) { + throw new JasperException( + Localizer.getMessage("jsp.error.beans.setproperty.noindexset")); + } + Class t = type.getComponentType(); + String[] values = request.getParameterValues(param); + //XXX Please check. + if(values == null) { + return; + } + if(t.equals(String.class)) { + method.invoke(bean, new Object[] { values }); + } else { + createTypedArray (prop, bean, method, values, t, + propertyEditorClass); + } + } else { + if(value == null || (param != null && value.equals(""))) { + return; + } + Object oval = convert(prop, value, type, propertyEditorClass); + if ( oval != null ) { + method.invoke(bean, new Object[] { oval }); + } + } + } + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + if (!ignoreMethodNF && (method == null)) { + if (type == null) { + throw new JasperException( + Localizer.getMessage("jsp.error.beans.noproperty", + prop, + bean.getClass().getName())); + } else { + throw new JasperException( + Localizer.getMessage("jsp.error.beans.nomethod.setproperty", + prop, + type.getName(), + bean.getClass().getName())); + } + } + } + // __end introspecthelperMethod + + //------------------------------------------------------------------- + // functions to convert builtin Java data types to string. + //------------------------------------------------------------------- + // __begin toStringMethod + public static String toString(Object o) { + return String.valueOf(o); + } + + public static String toString(byte b) { + return Byte.toString(b); + } + + public static String toString(boolean b) { + return Boolean.toString(b); + } + + public static String toString(short s) { + return Short.toString(s); + } + + public static String toString(int i) { + return Integer.toString(i); + } + + public static String toString(float f) { + return Float.toString(f); + } + + public static String toString(long l) { + return Long.toString(l); + } + + public static String toString(double d) { + return Double.toString(d); + } + + public static String toString(char c) { + return Character.toString(c); + } + // __end toStringMethod + + + /** + * Create a typed array. + * This is a special case where params are passed through + * the request and the property is indexed. + * @param propertyName The property name + * @param bean The bean + * @param method The method + * @param values Array values + * @param t The class + * @param propertyEditorClass The editor for the property + * @throws JasperException An error occurred + */ + public static void createTypedArray(String propertyName, + Object bean, + Method method, + String[] values, + Class t, + Class propertyEditorClass) + throws JasperException { + + try { + if (propertyEditorClass != null) { + Object[] tmpval = new Integer[values.length]; + for (int i=0; i^()[]{}$\\\n"; + + for (int index = 0; index < unescString.length(); index++) { + char nextChar = unescString.charAt(index); + + if (shellSpChars.indexOf(nextChar) != -1) { + escStringBuilder.append('\\'); + } + + escStringBuilder.append(nextChar); + } + return escStringBuilder.toString(); + } + + // __begin lookupReadMethodMethod + public static Object handleGetProperty(Object o, String prop) + throws JasperException { + if (o == null) { + throw new JasperException( + Localizer.getMessage("jsp.error.beans.nullbean")); + } + Object value = null; + try { + Method method = getReadMethod(o.getClass(), prop); + value = method.invoke(o, (Object[]) null); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException (ex); + } + return value; + } + // __end lookupReadMethodMethod + + // handles with EL expression for 'value' attribute + public static void handleSetPropertyExpression(Object bean, + String prop, String expression, PageContext pageContext, + ProtectedFunctionMapper functionMapper ) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { + PageContextImpl.proprietaryEvaluate( + expression, + method.getParameterTypes()[0], + pageContext, + functionMapper) + }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + Object value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { value }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + int value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { Integer.valueOf(value) }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + short value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { Short.valueOf(value) }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + long value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { Long.valueOf(value) }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + double value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { Double.valueOf(value) }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + float value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { Float.valueOf(value) }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + char value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { Character.valueOf(value) }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + byte value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { Byte.valueOf(value) }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + public static void handleSetProperty(Object bean, String prop, + boolean value) + throws JasperException + { + try { + Method method = getWriteMethod(bean.getClass(), prop); + method.invoke(bean, new Object[] { Boolean.valueOf(value) }); + } catch (Exception ex) { + Throwable thr = ExceptionUtils.unwrapInvocationTargetException(ex); + ExceptionUtils.handleThrowable(thr); + throw new JasperException(ex); + } + } + + /** + * Reverse of Introspector.decapitalize. + * @param name The name + * @return the capitalized string + */ + public static String capitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + char chars[] = name.toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + return new String(chars); + } + + public static Method getWriteMethod(Class beanClass, String prop) + throws JasperException { + Method result = null; + Class type = null; + if (GRAAL) { + String setter = "set" + capitalize(prop); + Method methods[] = beanClass.getMethods(); + for (Method method : methods) { + if (setter.equals(method.getName())) { + return method; + } + } + } else { + try { + java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(beanClass); + java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors(); + for (java.beans.PropertyDescriptor propertyDescriptor : pd) { + if (propertyDescriptor.getName().equals(prop)) { + result = propertyDescriptor.getWriteMethod(); + type = propertyDescriptor.getPropertyType(); + break; + } + } + } catch (Exception ex) { + throw new JasperException (ex); + } + } + if (result == null) { + if (type == null) { + throw new JasperException(Localizer.getMessage( + "jsp.error.beans.noproperty", prop, beanClass.getName())); + } else { + throw new JasperException(Localizer.getMessage( + "jsp.error.beans.nomethod.setproperty", + prop, type.getName(), beanClass.getName())); + } + } + return result; + } + + public static Method getReadMethod(Class beanClass, String prop) + throws JasperException { + Method result = null; + Class type = null; + if (GRAAL) { + String setter = "get" + capitalize(prop); + Method methods[] = beanClass.getMethods(); + for (Method method : methods) { + if (setter.equals(method.getName())) { + return method; + } + } + } else { + try { + java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(beanClass); + java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors(); + for (java.beans.PropertyDescriptor propertyDescriptor : pd) { + if (propertyDescriptor.getName().equals(prop)) { + result = propertyDescriptor.getReadMethod(); + type = propertyDescriptor.getPropertyType(); + break; + } + } + } catch (Exception ex) { + throw new JasperException (ex); + } + } + if (result == null) { + if (type == null) { + throw new JasperException(Localizer.getMessage( + "jsp.error.beans.noproperty", prop, beanClass.getName())); + } else { + throw new JasperException(Localizer.getMessage( + "jsp.error.beans.nomethod", prop, beanClass.getName())); + } + } + return result; + } + + //********************************************************************* + // PropertyEditor Support + + public static Object getValueFromBeanInfoPropertyEditor( + Class attrClass, String attrName, String attrValue, + Class propertyEditorClass) + throws JasperException + { + try { + PropertyEditor pe = (PropertyEditor)propertyEditorClass.getConstructor().newInstance(); + pe.setAsText(attrValue); + return pe.getValue(); + } catch (Exception ex) { + if (attrValue.length() == 0) { + return null; + } else { + throw new JasperException( + Localizer.getMessage("jsp.error.beans.property.conversion", + attrValue, attrClass.getName(), attrName, + ex.getMessage())); + } + } + } + + public static Object getValueFromPropertyEditorManager( + Class attrClass, String attrName, String attrValue) + throws JasperException + { + try { + PropertyEditor propEditor = + PropertyEditorManager.findEditor(attrClass); + if (propEditor != null) { + propEditor.setAsText(attrValue); + return propEditor.getValue(); + } else if (attrValue.length() == 0) { + return null; + } else { + throw new IllegalArgumentException( + Localizer.getMessage("jsp.error.beans.propertyeditor.notregistered")); + } + } catch (IllegalArgumentException ex) { + if (attrValue.length() == 0) { + return null; + } else { + throw new JasperException( + Localizer.getMessage("jsp.error.beans.property.conversion", + attrValue, attrClass.getName(), attrName, + ex.getMessage())); + } + } + } + + + // ************************************************************************ + // General Purpose Runtime Methods + // ************************************************************************ + + + /** + * Convert a possibly relative resource path into a context-relative + * resource path that starts with a '/'. + * + * @param request The servlet request we are processing + * @param relativePath The possibly relative resource path + * @return an absolute path + */ + public static String getContextRelativePath(ServletRequest request, + String relativePath) { + + if (relativePath.startsWith("/")) { + return relativePath; + } + if (!(request instanceof HttpServletRequest)) { + return relativePath; + } + HttpServletRequest hrequest = (HttpServletRequest) request; + String uri = (String) request.getAttribute( + RequestDispatcher.INCLUDE_SERVLET_PATH); + if (uri != null) { + String pathInfo = (String) + request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); + if (pathInfo == null) { + if (uri.lastIndexOf('/') >= 0) { + uri = uri.substring(0, uri.lastIndexOf('/')); + } + } + } else { + uri = hrequest.getServletPath(); + if (uri.lastIndexOf('/') >= 0) { + uri = uri.substring(0, uri.lastIndexOf('/')); + } + } + return uri + '/' + relativePath; + + } + + + /** + * Perform a RequestDispatcher.include() operation, with optional flushing + * of the response beforehand. + * + * @param request The servlet request we are processing + * @param response The servlet response we are processing + * @param relativePath The relative path of the resource to be included + * @param out The Writer to whom we are currently writing + * @param flush Should we flush before the include is processed? + * + * @exception IOException if thrown by the included servlet + * @exception ServletException if thrown by the included servlet + */ + public static void include(ServletRequest request, + ServletResponse response, + String relativePath, + JspWriter out, + boolean flush) + throws IOException, ServletException { + + if (flush && !(out instanceof BodyContent)) { + out.flush(); + } + + // FIXME - It is tempting to use request.getRequestDispatcher() to + // resolve a relative path directly, but Catalina currently does not + // take into account whether the caller is inside a RequestDispatcher + // include or not. Whether Catalina *should* take that into account + // is a spec issue currently under review. In the mean time, + // replicate Jasper's previous behavior + + String resourcePath = getContextRelativePath(request, relativePath); + RequestDispatcher rd = request.getRequestDispatcher(resourcePath); + if (rd != null) { + rd.include(request, + new ServletResponseWrapperInclude(response, out)); + } else { + throw new JasperException( + Localizer.getMessage("jsp.error.include.exception", resourcePath)); + } + + } + + /** + * URL encodes a string, based on the supplied character encoding. + * This performs the same function as java.next.URLEncode.encode + * in J2SDK1.4, and should be removed if the only platform supported + * is 1.4 or higher. + * @param s The String to be URL encoded. + * @param enc The character encoding + * @return The URL encoded String + */ + public static String URLEncode(String s, String enc) { + + if (s == null) { + return "null"; + } + + if (enc == null) { + enc = "ISO-8859-1"; // The default request encoding + } + + StringBuilder out = new StringBuilder(s.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(buf, enc); + } catch (java.io.UnsupportedEncodingException ex) { + // Use the default encoding? + writer = new OutputStreamWriter(buf); + } + + for (int i = 0; i < s.length(); i++) { + int c = s.charAt(i); + if (c == ' ') { + out.append('+'); + } else if (isSafeChar(c)) { + out.append((char)c); + } else { + // convert to external encoding before hex conversion + try { + writer.write(c); + writer.flush(); + } catch(IOException e) { + buf.reset(); + continue; + } + byte[] ba = buf.toByteArray(); + for (byte b : ba) { + out.append('%'); + // Converting each byte in the buffer + out.append(Character.forDigit((b >> 4) & 0xf, 16)); + out.append(Character.forDigit(b & 0xf, 16)); + } + buf.reset(); + } + } + return out.toString(); + } + + private static boolean isSafeChar(int c) { + if (c >= 'a' && c <= 'z') { + return true; + } + if (c >= 'A' && c <= 'Z') { + return true; + } + if (c >= '0' && c <= '9') { + return true; + } + if (c == '-' || c == '_' || c == '.' || c == '!' || + c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') { + return true; + } + return false; + } + + + public static JspWriter startBufferedBody(PageContext pageContext, BodyTag tag) + throws JspException { + BodyContent out = pageContext.pushBody(); + tag.setBodyContent(out); + tag.doInitBody(); + return out; + } + + + public static void releaseTag(Tag tag, InstanceManager instanceManager, boolean reused) { + // Caller ensures pool is non-null if reuse is true + if (!reused) { + releaseTag(tag, instanceManager); + } + } + + + protected static void releaseTag(Tag tag, InstanceManager instanceManager) { + try { + tag.release(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + Log log = LogFactory.getLog(JspRuntimeLibrary.class); + log.warn(Localizer.getMessage("jsp.warning.tagRelease", tag.getClass().getName()), t); + } + try { + instanceManager.destroyInstance(tag); + } catch (Exception e) { + Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + Log log = LogFactory.getLog(JspRuntimeLibrary.class); + log.warn(Localizer.getMessage("jsp.warning.tagPreDestroy", tag.getClass().getName()), t); + } + + } +} diff --git a/java/org/apache/jasper/runtime/JspSourceDependent.java b/java/org/apache/jasper/runtime/JspSourceDependent.java new file mode 100644 index 0000000..819e356 --- /dev/null +++ b/java/org/apache/jasper/runtime/JspSourceDependent.java @@ -0,0 +1,39 @@ +/* + * 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.jasper.runtime; + +import java.util.Map; + +/** + * Interface for tracking the source files dependencies, for the purpose + * of compiling out of date pages. This is used for + * 1) files that are included by page directives + * 2) files that are included by include-prelude and include-coda in jsp:config + * 3) files that are tag files and referenced + * 4) TLDs referenced + */ + +public interface JspSourceDependent { + + /** + * Returns a map of file names and last modified time where the current page + * has a source dependency on the file. + * @return the map of dependent resources + */ + Map getDependants(); + +} diff --git a/java/org/apache/jasper/runtime/JspSourceDirectives.java b/java/org/apache/jasper/runtime/JspSourceDirectives.java new file mode 100644 index 0000000..a9fb46c --- /dev/null +++ b/java/org/apache/jasper/runtime/JspSourceDirectives.java @@ -0,0 +1,27 @@ +/* + * 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.jasper.runtime; + +/** + * Provides runtime access to selected compile time directives. Page directives + * are not added to this interface until there is a requirement to access them + * at runtime. + */ +public interface JspSourceDirectives { + + boolean getErrorOnELNotFound(); +} diff --git a/java/org/apache/jasper/runtime/JspSourceImports.java b/java/org/apache/jasper/runtime/JspSourceImports.java new file mode 100644 index 0000000..09f58d8 --- /dev/null +++ b/java/org/apache/jasper/runtime/JspSourceImports.java @@ -0,0 +1,30 @@ +/* + * 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.jasper.runtime; + +import java.util.Set; + +/** + * The EL engine needs access to the imports used in the JSP page to configure + * the ELContext. The imports are available at compile time but the ELContext + * is created lazily per page. This interface exposes the imports at runtime so + * that they may be added to the ELContext when it is created. + */ +public interface JspSourceImports { + Set getPackageImports(); + Set getClassImports(); +} diff --git a/java/org/apache/jasper/runtime/JspWriterImpl.java b/java/org/apache/jasper/runtime/JspWriterImpl.java new file mode 100644 index 0000000..06b14c1 --- /dev/null +++ b/java/org/apache/jasper/runtime/JspWriterImpl.java @@ -0,0 +1,608 @@ +/* + * 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.jasper.runtime; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; + +import jakarta.servlet.ServletResponse; +import jakarta.servlet.jsp.JspWriter; + +import org.apache.jasper.Constants; +import org.apache.jasper.compiler.Localizer; + +/** + * Write text to a character-output stream, buffering characters so as + * to provide for the efficient writing of single characters, arrays, + * and strings. + * + * Provide support for discarding for the output that has been + * buffered. + * + * This needs revisiting when the buffering problems in the JSP spec + * are fixed -akv + * + * @author Anil K. Vijendran + */ +public class JspWriterImpl extends JspWriter { + + private Writer out; + private ServletResponse response; + private char cb[]; + private int nextChar; + private boolean flushed = false; + private boolean closed = false; + + public JspWriterImpl() { + super( Constants.DEFAULT_BUFFER_SIZE, true ); + } + + /** + * Create a new buffered character-output stream that uses an output + * buffer of the given size. + * + * @param response A Servlet Response + * @param sz Output-buffer size, a positive integer + * @param autoFlush true to automatically flush on buffer + * full, false to throw an overflow exception in that case + * @exception IllegalArgumentException If sz is <= 0 + */ + public JspWriterImpl(ServletResponse response, int sz, + boolean autoFlush) { + super(sz, autoFlush); + if (sz < 0) { + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.negativeBufferSize")); + } + this.response = response; + cb = sz == 0 ? null : new char[sz]; + nextChar = 0; + } + + void init( ServletResponse response, int sz, boolean autoFlush ) { + this.response= response; + if( sz > 0 && ( cb == null || sz > cb.length ) ) { + cb=new char[sz]; + } + nextChar = 0; + this.autoFlush=autoFlush; + this.bufferSize=sz; + } + + /** + * Package-level access + */ + void recycle() { + flushed = false; + closed = false; + out = null; + nextChar = 0; + response = null; + } + + /** + * Flush the output buffer to the underlying character stream, without + * flushing the stream itself. This method is non-private only so that it + * may be invoked by PrintStream. + * @throws IOException Error writing buffered data + */ + protected final void flushBuffer() throws IOException { + if (bufferSize == 0) { + return; + } + flushed = true; + ensureOpen(); + if (nextChar == 0) { + return; + } + initOut(); + out.write(cb, 0, nextChar); + nextChar = 0; + } + + private void initOut() throws IOException { + if (out == null) { + try { + out = response.getWriter(); + } catch (IllegalStateException e) { + /* + * At some point in the processing something (most likely the default servlet as the target of a + * action) wrote directly to the OutputStream rather than the Writer. Wrap the + * OutputStream in a Writer so the JSp engine can use the Writer it is expecting to use. + */ + out = new PrintWriter(response.getOutputStream()); + } + } + } + + /** + * Discard the output buffer. + */ + @Override + public final void clear() throws IOException { + if ((bufferSize == 0) && (out != null)) { + // clear() is illegal after any unbuffered output (JSP.5.5) + throw new IllegalStateException( + Localizer.getMessage("jsp.error.ise_on_clear")); + } + if (flushed) { + throw new IOException( + Localizer.getMessage("jsp.error.attempt_to_clear_flushed_buffer")); + } + ensureOpen(); + nextChar = 0; + } + + @Override + public void clearBuffer() throws IOException { + if (bufferSize == 0) { + throw new IllegalStateException( + Localizer.getMessage("jsp.error.ise_on_clear")); + } + ensureOpen(); + nextChar = 0; + } + + private void bufferOverflow() throws IOException { + throw new IOException(Localizer.getMessage("jsp.error.overflow")); + } + + /** + * Flush the stream. + * + */ + @Override + public void flush() throws IOException { + flushBuffer(); + if (out != null) { + out.flush(); + } + } + + /** + * Close the stream. + * + */ + @Override + public void close() throws IOException { + if (response == null || closed) { + // multiple calls to close is OK + return; + } + flush(); + if (out != null) { + out.close(); + } + out = null; + closed = true; + } + + /** + * @return the number of bytes unused in the buffer + */ + @Override + public int getRemaining() { + return bufferSize - nextChar; + } + + /** check to make sure that the stream has not been closed */ + private void ensureOpen() throws IOException { + if (response == null || closed) { + throw new IOException(Localizer.getMessage("jsp.error.stream.closed")); + } + } + + + /** + * Write a single character. + */ + @Override + public void write(int c) throws IOException { + ensureOpen(); + if (bufferSize == 0) { + initOut(); + out.write(c); + } else { + if (nextChar >= bufferSize) { + if (autoFlush) { + flushBuffer(); + } else { + bufferOverflow(); + } + } + cb[nextChar++] = (char) c; + } + } + + /** + * Our own little min method, to avoid loading java.lang.Math if we've run + * out of file descriptors and we're trying to print a stack trace. + */ + private static int min(int a, int b) { + if (a < b) { + return a; + } + return b; + } + + /** + * Write a portion of an array of characters. + * + *

    Ordinarily this method stores characters from the given array into + * this stream's buffer, flushing the buffer to the underlying stream as + * needed. If the requested length is at least as large as the buffer, + * however, then this method will flush the buffer and write the characters + * directly to the underlying stream. Thus redundant + * DiscardableBufferedWriters will not copy data unnecessarily. + * + * @param cbuf A character array + * @param off Offset from which to start reading characters + * @param len Number of characters to write + */ + @Override + public void write(char cbuf[], int off, int len) + throws IOException + { + ensureOpen(); + + if (bufferSize == 0) { + initOut(); + out.write(cbuf, off, len); + return; + } + + if ((off < 0) || (off > cbuf.length) || (len < 0) || + ((off + len) > cbuf.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + if (len >= bufferSize) { + /* If the request length exceeds the size of the output buffer, + flush the buffer and then write the data directly. In this + way buffered streams will cascade harmlessly. */ + if (autoFlush) { + flushBuffer(); + } else { + bufferOverflow(); + } + initOut(); + out.write(cbuf, off, len); + return; + } + + int b = off, t = off + len; + while (b < t) { + int d = min(bufferSize - nextChar, t - b); + System.arraycopy(cbuf, b, cb, nextChar, d); + b += d; + nextChar += d; + if (nextChar >= bufferSize) { + if (autoFlush) { + flushBuffer(); + } else { + bufferOverflow(); + } + } + } + + } + + /** + * Write an array of characters. This method cannot be inherited from the + * Writer class because it must suppress I/O exceptions. + */ + @Override + public void write(char buf[]) throws IOException { + write(buf, 0, buf.length); + } + + /** + * Write a portion of a String. + * + * @param s String to be written + * @param off Offset from which to start reading characters + * @param len Number of characters to be written + */ + @Override + public void write(String s, int off, int len) throws IOException { + ensureOpen(); + if (bufferSize == 0) { + initOut(); + out.write(s, off, len); + return; + } + int b = off, t = off + len; + while (b < t) { + int d = min(bufferSize - nextChar, t - b); + s.getChars(b, b + d, cb, nextChar); + b += d; + nextChar += d; + if (nextChar >= bufferSize) { + if (autoFlush) { + flushBuffer(); + } else { + bufferOverflow(); + } + } + } + } + + + /** + * Write a line separator. The line separator string is defined by the + * system property line.separator, and is not necessarily a + * single newline ('\n') character. + * + * @exception IOException If an I/O error occurs + */ + + @Override + public void newLine() throws IOException { + write(System.lineSeparator()); + } + + + /* Methods that do not terminate lines */ + + /** + * Print a boolean value. The string produced by {@link + * String#valueOf(boolean)} is translated into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the {@link + * #write(int)} method. + * + * @param b The boolean to be printed + */ + @Override + public void print(boolean b) throws IOException { + write(b ? "true" : "false"); + } + + /** + * Print a character. The character is translated into one or more bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the {@link + * #write(int)} method. + * + * @param c The char to be printed + */ + @Override + public void print(char c) throws IOException { + write(String.valueOf(c)); + } + + /** + * Print an integer. The string produced by {@link + * String#valueOf(int)} is translated into bytes according + * to the platform's default character encoding, and these bytes are + * written in exactly the manner of the {@link #write(int)} + * method. + * + * @param i The int to be printed + */ + @Override + public void print(int i) throws IOException { + write(String.valueOf(i)); + } + + /** + * Print a long integer. The string produced by {@link + * String#valueOf(long)} is translated into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the {@link #write(int)} + * method. + * + * @param l The long to be printed + */ + @Override + public void print(long l) throws IOException { + write(String.valueOf(l)); + } + + /** + * Print a floating-point number. The string produced by {@link + * String#valueOf(float)} is translated into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the {@link #write(int)} + * method. + * + * @param f The float to be printed + */ + @Override + public void print(float f) throws IOException { + write(String.valueOf(f)); + } + + /** + * Print a double-precision floating-point number. The string produced by + * {@link String#valueOf(double)} is translated into + * bytes according to the platform's default character encoding, and these + * bytes are written in exactly the manner of the {@link + * #write(int)} method. + * + * @param d The double to be printed + */ + @Override + public void print(double d) throws IOException { + write(String.valueOf(d)); + } + + /** + * Print an array of characters. The characters are converted into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the {@link #write(int)} + * method. + * + * @param s The array of chars to be printed + * + * @throws NullPointerException If s is null + */ + @Override + public void print(char s[]) throws IOException { + write(s); + } + + /** + * Print a string. If the argument is null then the string + * "null" is printed. Otherwise, the string's characters are + * converted into bytes according to the platform's default character + * encoding, and these bytes are written in exactly the manner of the + * {@link #write(int)} method. + * + * @param s The String to be printed + */ + @Override + public void print(String s) throws IOException { + if (s == null) { + s = "null"; + } + write(s); + } + + /** + * Print an object. The string produced by the {@link + * String#valueOf(Object)} method is translated into bytes + * according to the platform's default character encoding, and these bytes + * are written in exactly the manner of the {@link #write(int)} + * method. + * + * @param obj The Object to be printed + */ + @Override + public void print(Object obj) throws IOException { + write(String.valueOf(obj)); + } + + /* Methods that do terminate lines */ + + /** + * Terminate the current line by writing the line separator string. The + * line separator string is defined by the system property + * line.separator, and is not necessarily a single newline + * character ('\n'). + * + * Need to change this from PrintWriter because the default + * println() writes to the sink directly instead of through the + * write method... + */ + @Override + public void println() throws IOException { + newLine(); + } + + /** + * Print a boolean value and then terminate the line. This method behaves + * as though it invokes {@link #print(boolean)} and then + * {@link #println()}. + */ + @Override + public void println(boolean x) throws IOException { + print(x); + println(); + } + + /** + * Print a character and then terminate the line. This method behaves as + * though it invokes {@link #print(char)} and then {@link + * #println()}. + */ + @Override + public void println(char x) throws IOException { + print(x); + println(); + } + + /** + * Print an integer and then terminate the line. This method behaves as + * though it invokes {@link #print(int)} and then {@link + * #println()}. + */ + @Override + public void println(int x) throws IOException { + print(x); + println(); + } + + /** + * Print a long integer and then terminate the line. This method behaves + * as though it invokes {@link #print(long)} and then + * {@link #println()}. + */ + @Override + public void println(long x) throws IOException { + print(x); + println(); + } + + /** + * Print a floating-point number and then terminate the line. This method + * behaves as though it invokes {@link #print(float)} and then + * {@link #println()}. + */ + @Override + public void println(float x) throws IOException { + print(x); + println(); + } + + /** + * Print a double-precision floating-point number and then terminate the + * line. This method behaves as though it invokes {@link + * #print(double)} and then {@link #println()}. + */ + @Override + public void println(double x) throws IOException { + print(x); + println(); + } + + /** + * Print an array of characters and then terminate the line. This method + * behaves as though it invokes {@link #print(char[])} and then + * {@link #println()}. + */ + @Override + public void println(char x[]) throws IOException { + print(x); + println(); + } + + /** + * Print a String and then terminate the line. This method behaves as + * though it invokes {@link #print(String)} and then + * {@link #println()}. + */ + @Override + public void println(String x) throws IOException { + print(x); + println(); + } + + /** + * Print an Object and then terminate the line. This method behaves as + * though it invokes {@link #print(Object)} and then + * {@link #println()}. + */ + @Override + public void println(Object x) throws IOException { + print(x); + println(); + } + +} diff --git a/java/org/apache/jasper/runtime/PageContextImpl.java b/java/org/apache/jasper/runtime/PageContextImpl.java new file mode 100644 index 0000000..bcec15c --- /dev/null +++ b/java/org/apache/jasper/runtime/PageContextImpl.java @@ -0,0 +1,740 @@ +/* + * 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.jasper.runtime; + +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Set; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.ExpressionFactory; +import jakarta.el.ImportHandler; +import jakarta.el.ValueExpression; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspFactory; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.el.NotFoundELResolver; +import jakarta.servlet.jsp.tagext.BodyContent; + +import org.apache.jasper.Constants; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.el.ELContextImpl; +import org.apache.jasper.runtime.JspContextWrapper.ELContextWrapper; + +/** + * Implementation of the PageContext class from the JSP spec. Also doubles as a + * VariableResolver for the EL. + * + * @author Anil K. Vijendran + * @author Larry Cable + * @author Hans Bergsten + * @author Pierre Delisle + * @author Mark Roth + * @author Jan Luehe + * @author Jacob Hookom + */ +public class PageContextImpl extends PageContext { + + private static final JspFactory jspf = JspFactory.getDefaultFactory(); + + private BodyContentImpl[] outs; + + private int depth; + + // per-servlet state + private Servlet servlet; + + private ServletConfig config; + + private ServletContext context; + + private JspApplicationContextImpl applicationContext; + + private String errorPageURL; + + private boolean limitBodyContentBuffer; + + private int bodyContentTagBufferSize = Constants.DEFAULT_TAG_BUFFER_SIZE; + + // page-scope attributes + private final transient HashMap attributes; + + // per-request state + private transient ServletRequest request; + + private transient ServletResponse response; + + private transient HttpSession session; + + private transient ELContextImpl elContext; + + + // initial output stream + private transient JspWriter out; + + private transient JspWriterImpl baseOut; + + /* + * Constructor. + */ + PageContextImpl() { + this.outs = new BodyContentImpl[0]; + this.attributes = new HashMap<>(16); + this.depth = -1; + } + + @Override + public void initialize(Servlet servlet, ServletRequest request, + ServletResponse response, String errorPageURL, + boolean needsSession, int bufferSize, boolean autoFlush) + throws IOException { + + // initialize state + this.servlet = servlet; + this.config = servlet.getServletConfig(); + this.context = config.getServletContext(); + this.errorPageURL = errorPageURL; + this.request = request; + this.response = response; + + limitBodyContentBuffer = Boolean.parseBoolean(config.getInitParameter("limitBodyContentBuffer")); + String bodyContentTagBufferSize = config.getInitParameter("bodyContentTagBufferSize"); + if (bodyContentTagBufferSize != null) { + this.bodyContentTagBufferSize = Integer.parseInt(bodyContentTagBufferSize); + } + + // initialize application context + this.applicationContext = JspApplicationContextImpl.getInstance(context); + + // Setup session (if required) + if (request instanceof HttpServletRequest && needsSession) { + this.session = ((HttpServletRequest) request).getSession(); + } + if (needsSession && session == null) { + throw new IllegalStateException(Localizer.getMessage("jsp.error.page.sessionRequired")); + } + + // initialize the initial out ... + depth = -1; + if (bufferSize == JspWriter.DEFAULT_BUFFER) { + bufferSize = Constants.DEFAULT_BUFFER_SIZE; + } + if (this.baseOut == null) { + this.baseOut = new JspWriterImpl(response, bufferSize, autoFlush); + } else { + this.baseOut.init(response, bufferSize, autoFlush); + } + this.out = baseOut; + + // register names/values as per spec + setAttribute(OUT, this.out); + setAttribute(REQUEST, request); + setAttribute(RESPONSE, response); + + if (session != null) { + setAttribute(SESSION, session); + } + + setAttribute(PAGE, servlet); + setAttribute(CONFIG, config); + setAttribute(PAGECONTEXT, this); + setAttribute(APPLICATION, context); + } + + @Override + public void release() { + out = baseOut; + try { + ((JspWriterImpl) out).flushBuffer(); + } catch (IOException ex) { + IllegalStateException ise = new IllegalStateException(Localizer.getMessage("jsp.error.flush"), ex); + throw ise; + } finally { + servlet = null; + config = null; + context = null; + applicationContext = null; + elContext = null; + errorPageURL = null; + request = null; + response = null; + depth = -1; + baseOut.recycle(); + session = null; + attributes.clear(); + for (BodyContentImpl body: outs) { + body.recycle(); + } + } + } + + @Override + public Object getAttribute(final String name) { + return getAttribute(name, PAGE_SCOPE); + } + + @Override + public Object getAttribute(final String name, final int scope) { + + if (name == null) { + throw new NullPointerException(Localizer.getMessage("jsp.error.attribute.null_name")); + } + + switch (scope) { + case PAGE_SCOPE: + return attributes.get(name); + + case REQUEST_SCOPE: + return request.getAttribute(name); + + case SESSION_SCOPE: + if (session == null) { + throw new IllegalStateException(Localizer.getMessage("jsp.error.page.noSession")); + } + return session.getAttribute(name); + + case APPLICATION_SCOPE: + return context.getAttribute(name); + + default: + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.page.invalid.scope")); + } + } + + @Override + public void setAttribute(final String name, final Object attribute) { + setAttribute(name, attribute, PAGE_SCOPE); + } + + @Override + public void setAttribute(final String name, final Object o, final int scope) { + + if (name == null) { + throw new NullPointerException(Localizer.getMessage("jsp.error.attribute.null_name")); + } + + if (o == null) { + removeAttribute(name, scope); + } else { + switch (scope) { + case PAGE_SCOPE: + attributes.put(name, o); + break; + + case REQUEST_SCOPE: + request.setAttribute(name, o); + break; + + case SESSION_SCOPE: + if (session == null) { + throw new IllegalStateException(Localizer + .getMessage("jsp.error.page.noSession")); + } + session.setAttribute(name, o); + break; + + case APPLICATION_SCOPE: + context.setAttribute(name, o); + break; + + default: + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.page.invalid.scope")); + } + } + } + + @Override + public void removeAttribute(final String name, final int scope) { + + if (name == null) { + throw new NullPointerException(Localizer.getMessage("jsp.error.attribute.null_name")); + } + + switch (scope) { + case PAGE_SCOPE: + attributes.remove(name); + break; + + case REQUEST_SCOPE: + request.removeAttribute(name); + break; + + case SESSION_SCOPE: + if (session == null) { + throw new IllegalStateException(Localizer.getMessage("jsp.error.page.noSession")); + } + session.removeAttribute(name); + break; + + case APPLICATION_SCOPE: + context.removeAttribute(name); + break; + + default: + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.page.invalid.scope")); + } + } + + @Override + public int getAttributesScope(final String name) { + + if (name == null) { + throw new NullPointerException(Localizer.getMessage("jsp.error.attribute.null_name")); + } + + if (attributes.get(name) != null) { + return PAGE_SCOPE; + } + + if (request.getAttribute(name) != null) { + return REQUEST_SCOPE; + } + + if (session != null) { + try { + if (session.getAttribute(name) != null) { + return SESSION_SCOPE; + } + } catch(IllegalStateException ise) { + // Session has been invalidated. + // Ignore and fall through to application scope. + } + } + + if (context.getAttribute(name) != null) { + return APPLICATION_SCOPE; + } + + return 0; + } + + @Override + public Object findAttribute(final String name) { + if (name == null) { + throw new NullPointerException(Localizer.getMessage("jsp.error.attribute.null_name")); + } + + Object o = attributes.get(name); + if (o != null) { + return o; + } + + o = request.getAttribute(name); + if (o != null) { + return o; + } + + if (session != null) { + try { + o = session.getAttribute(name); + } catch(IllegalStateException ise) { + // Session has been invalidated. + // Ignore and fall through to application scope. + } + if (o != null) { + return o; + } + } + + return context.getAttribute(name); + } + + @Override + public Enumeration getAttributeNamesInScope(final int scope) { + switch (scope) { + case PAGE_SCOPE: + return Collections.enumeration(attributes.keySet()); + + case REQUEST_SCOPE: + return request.getAttributeNames(); + + case SESSION_SCOPE: + if (session == null) { + throw new IllegalStateException(Localizer.getMessage("jsp.error.page.noSession")); + } + return session.getAttributeNames(); + + case APPLICATION_SCOPE: + return context.getAttributeNames(); + + default: + throw new IllegalArgumentException(Localizer.getMessage("jsp.error.page.invalid.scope")); + } + } + + @Override + public void removeAttribute(final String name) { + + if (name == null) { + throw new NullPointerException(Localizer.getMessage("jsp.error.attribute.null_name")); + } + + removeAttribute(name, PAGE_SCOPE); + removeAttribute(name, REQUEST_SCOPE); + if( session != null ) { + try { + removeAttribute(name, SESSION_SCOPE); + } catch(IllegalStateException ise) { + // Session has been invalidated. + // Ignore and fall throw to application scope. + } + } + removeAttribute(name, APPLICATION_SCOPE); + } + + @Override + public JspWriter getOut() { + return out; + } + + @Override + public HttpSession getSession() { + return session; + } + + @Override + public ServletConfig getServletConfig() { + return config; + } + + @Override + public ServletContext getServletContext() { + return config.getServletContext(); + } + + @Override + public ServletRequest getRequest() { + return request; + } + + @Override + public ServletResponse getResponse() { + return response; + } + + /** + * Returns the exception associated with this page context, if any. + *

    + * Added wrapping for Throwables to avoid ClassCastException: see Bugzilla + * 31171 for details. + * + * @return The Exception associated with this page context, if any. + */ + @Override + public Exception getException() { + Throwable t = JspRuntimeLibrary.getThrowable(request); + + // Only wrap if needed + if ((t != null) && (!(t instanceof Exception))) { + t = new JspException(t); + } + + return (Exception) t; + } + + @Override + public Object getPage() { + return servlet; + } + + private String getAbsolutePathRelativeToContext(String relativeUrlPath) { + String path = relativeUrlPath; + + if (!path.startsWith("/")) { + String uri = (String) request.getAttribute( + RequestDispatcher.INCLUDE_SERVLET_PATH); + if (uri == null) { + uri = ((HttpServletRequest) request).getServletPath(); + } + String baseURI = uri.substring(0, uri.lastIndexOf('/')); + path = baseURI + '/' + path; + } + + return path; + } + + @Override + public void include(String relativeUrlPath) throws ServletException, + IOException { + JspRuntimeLibrary + .include(request, response, relativeUrlPath, out, true); + } + + @Override + public void include(final String relativeUrlPath, final boolean flush) + throws ServletException, IOException { + JspRuntimeLibrary.include(request, response, relativeUrlPath, out, flush); + } + + @Override + @Deprecated + public jakarta.servlet.jsp.el.VariableResolver getVariableResolver() { + return new org.apache.jasper.el.VariableResolverImpl( + this.getELContext()); + } + + @Override + public void forward(final String relativeUrlPath) throws ServletException, IOException { + // JSP.4.5 If the buffer was flushed, throw IllegalStateException + try { + out.clear(); + baseOut.clear(); + } catch (IOException ex) { + throw new IllegalStateException(Localizer.getMessage( + "jsp.error.attempt_to_clear_flushed_buffer"), ex); + } + + // Make sure that the response object is not the wrapper for include + while (response instanceof ServletResponseWrapperInclude) { + response = ((ServletResponseWrapperInclude) response).getResponse(); + } + + final String path = getAbsolutePathRelativeToContext(relativeUrlPath); + String includeUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + + if (includeUri != null) { + request.removeAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); + } + try { + context.getRequestDispatcher(path).forward(request, response); + } finally { + if (includeUri != null) { + request.setAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH, includeUri); + } + } + } + + @Override + public BodyContent pushBody() { + return (BodyContent) pushBody(null); + } + + @Override + public JspWriter pushBody(Writer writer) { + depth++; + if (depth >= outs.length) { + BodyContentImpl[] newOuts = Arrays.copyOf(outs, depth + 1); + newOuts[depth] = new BodyContentImpl(out, limitBodyContentBuffer, bodyContentTagBufferSize); + outs = newOuts; + } + + outs[depth].setWriter(writer); + out = outs[depth]; + + // Update the value of the "out" attribute in the page scope + // attribute namespace of this PageContext + setAttribute(OUT, out); + + return outs[depth]; + } + + @Override + public JspWriter popBody() { + depth--; + if (depth >= 0) { + out = outs[depth]; + } else { + out = baseOut; + } + + // Update the value of the "out" attribute in the page scope + // attribute namespace of this PageContext + setAttribute(OUT, out); + + return out; + } + + /** + * Provides programmatic access to the ExpressionEvaluator. The JSP + * Container must return a valid instance of an ExpressionEvaluator that can + * parse EL expressions. + */ + @Override + @Deprecated + public jakarta.servlet.jsp.el.ExpressionEvaluator getExpressionEvaluator() { + return new org.apache.jasper.el.ExpressionEvaluatorImpl( + this.applicationContext.getExpressionFactory()); + } + + @Override + public void handlePageException(Exception ex) throws IOException, + ServletException { + // Should never be called since handleException() called with a + // Throwable in the generated servlet. + handlePageException((Throwable) ex); + } + + @Override + @SuppressWarnings("deprecation") // Still have to support old JSP EL + public void handlePageException(final Throwable t) throws IOException, ServletException { + if (t == null) { + throw new NullPointerException(Localizer.getMessage("jsp.error.page.nullThrowable")); + } + + if (errorPageURL != null && !errorPageURL.equals("")) { + + /* + * Set request attributes. Do not set the + * jakarta.servlet.error.exception attribute here (instead, set in the + * generated servlet code for the error page) in order to prevent + * the ErrorReportValve, which is invoked as part of forwarding the + * request to the error page, from throwing it if the response has + * not been committed (the response will have been committed if the + * error page is a JSP page). + */ + request.setAttribute(EXCEPTION, t); + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, + Integer.valueOf(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); + request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, ((HttpServletRequest) request).getRequestURI()); + request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, config.getServletName()); + try { + forward(errorPageURL); + } catch (IllegalStateException ise) { + include(errorPageURL); + } + + // The error page could be inside an include. + + Object newException = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + + // t==null means the attribute was not set. + if ((newException != null) && (newException == t)) { + request.removeAttribute(RequestDispatcher.ERROR_EXCEPTION); + } + + // now clear the error code - to prevent double handling. + request.removeAttribute(RequestDispatcher.ERROR_STATUS_CODE); + request.removeAttribute(RequestDispatcher.ERROR_REQUEST_URI); + request.removeAttribute(RequestDispatcher.ERROR_SERVLET_NAME); + request.removeAttribute(EXCEPTION); + + } else { + // Otherwise throw the exception wrapped inside a ServletException. + // Set the exception as the root cause in the ServletException + // to get a stack trace for the real problem + if (t instanceof IOException) { + throw (IOException) t; + } + if (t instanceof ServletException) { + throw (ServletException) t; + } + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + + Throwable rootCause = null; + if (t instanceof JspException || t instanceof ELException || + t instanceof jakarta.servlet.jsp.el.ELException) { + rootCause = t.getCause(); + } + + if (rootCause != null) { + throw new ServletException( + t.getClass().getName() + ": " + t.getMessage(), rootCause); + } + + throw new ServletException(t); + } + } + + /** + * Proprietary method to evaluate EL expressions. XXX - This method should + * go away once the EL interpreter moves out of JSTL and into its own + * project. For now, this is necessary because the standard machinery is too + * slow. + * + * @param expression + * The expression to be evaluated + * @param expectedType + * The expected resulting type + * @param pageContext + * The page context + * @param functionMap + * Maps prefix and name to Method + * @return The result of the evaluation + * @throws ELException If an error occurs during the evaluation + */ + public static Object proprietaryEvaluate(final String expression, + final Class expectedType, final PageContext pageContext, + final ProtectedFunctionMapper functionMap) + throws ELException { + final ExpressionFactory exprFactory = jspf.getJspApplicationContext(pageContext.getServletContext()).getExpressionFactory(); + ELContext ctx = pageContext.getELContext(); + ELContextImpl ctxImpl; + if (ctx instanceof ELContextWrapper) { + ctxImpl = (ELContextImpl) ((ELContextWrapper) ctx).getWrappedELContext(); + } else { + ctxImpl = (ELContextImpl) ctx; + } + ctxImpl.setFunctionMapper(functionMap); + ValueExpression ve = exprFactory.createValueExpression(ctx, expression, expectedType); + return ve.getValue(ctx); + } + + @Override + public ELContext getELContext() { + if (elContext == null) { + elContext = applicationContext.createELContext(this); + if (servlet instanceof JspSourceImports) { + ImportHandler ih = elContext.getImportHandler(); + Set packageImports = ((JspSourceImports) servlet).getPackageImports(); + if (packageImports != null) { + for (String packageImport : packageImports) { + ih.importPackage(packageImport); + } + } + Set classImports = ((JspSourceImports) servlet).getClassImports(); + if (classImports != null) { + for (String classImport : classImports) { + if (classImport.startsWith("static ")) { + classImport = classImport.substring(7); + try { + ih.importStatic(classImport); + } catch (ELException e) { + // Ignore - not all static imports are valid for EL + } + } else { + ih.importClass(classImport); + } + } + } + } + if (servlet instanceof JspSourceDirectives) { + if (((JspSourceDirectives) servlet).getErrorOnELNotFound()) { + elContext.putContext(NotFoundELResolver.class, Boolean.TRUE); + } + } + } + return this.elContext; + } +} diff --git a/java/org/apache/jasper/runtime/ProtectedFunctionMapper.java b/java/org/apache/jasper/runtime/ProtectedFunctionMapper.java new file mode 100644 index 0000000..4c02395 --- /dev/null +++ b/java/org/apache/jasper/runtime/ProtectedFunctionMapper.java @@ -0,0 +1,152 @@ +/* + * 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.jasper.runtime; + +import java.lang.reflect.Method; +import java.util.HashMap; + +import jakarta.servlet.jsp.el.FunctionMapper; + +/** + * Maps EL functions to their Java method counterparts. Keeps the actual Method + * objects protected so that JSP pages can't indirectly do reflection. + * + * @author Mark Roth + * @author Kin-man Chung + */ +@SuppressWarnings("deprecation") // Have to support old JSP EL API +public final class ProtectedFunctionMapper extends jakarta.el.FunctionMapper + implements FunctionMapper { + + /** + * Maps "prefix:name" to java.lang.Method objects. + */ + private HashMap fnmap = null; + + /** + * If there is only one function in the map, this is the Method for it. + */ + private Method theMethod = null; + + /** + * Constructor has protected access. + */ + private ProtectedFunctionMapper() { + } + + /** + * Generated Servlet and Tag Handler implementations call this method to + * retrieve an instance of the ProtectedFunctionMapper. + * + * @return A new protected function mapper. + */ + public static ProtectedFunctionMapper getInstance() { + ProtectedFunctionMapper funcMapper = new ProtectedFunctionMapper(); + funcMapper.fnmap = new HashMap<>(); + return funcMapper; + } + + /** + * Stores a mapping from the given EL function prefix and name to the given + * Java method. + * + * @param fnQName + * The EL function qualified name (including prefix) + * @param c + * The class containing the Java method + * @param methodName + * The name of the Java method + * @param args + * The arguments of the Java method + * @throws RuntimeException + * if no method with the given signature could be found. + */ + public void mapFunction(String fnQName, final Class c, + final String methodName, final Class[] args) { + // Skip if null values were passed in. They indicate a function + // added via a lambda or ImportHandler; nether of which need to be + // placed in the Map. + if (fnQName == null) { + return; + } + Method method; + try { + method = c.getMethod(methodName, args); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "Invalid function mapping - no such method: " + + e.getMessage()); + } + + this.fnmap.put(fnQName, method); + } + + /** + * Creates an instance for this class, and stores the Method for the given + * EL function prefix and name. This method is used for the case when there + * is only one function in the EL expression. + * + * @param fnQName + * The EL function qualified name (including prefix) + * @param c + * The class containing the Java method + * @param methodName + * The name of the Java method + * @param args + * The arguments of the Java method + * @throws RuntimeException + * if no method with the given signature could be found. + * @return the mapped function + */ + public static ProtectedFunctionMapper getMapForFunction(String fnQName, + final Class c, final String methodName, final Class[] args) { + Method method = null; + ProtectedFunctionMapper funcMapper = new ProtectedFunctionMapper(); + // Skip if null values were passed in. They indicate a function + // added via a lambda or ImportHandler; nether of which need to be + // placed in the Map. + if (fnQName != null) { + try { + method = c.getMethod(methodName, args); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "Invalid function mapping - no such method: " + + e.getMessage()); + } + } + funcMapper.theMethod = method; + return funcMapper; + } + + /** + * Resolves the specified local name and prefix into a Java.lang.Method. + * Returns null if the prefix and local name are not found. + * + * @param prefix + * the prefix of the function + * @param localName + * the short name of the function + * @return the result of the method mapping. Null means no entry found. + */ + @Override + public Method resolveFunction(String prefix, String localName) { + if (this.fnmap != null) { + return this.fnmap.get(prefix + ":" + localName); + } + return theMethod; + } +} diff --git a/java/org/apache/jasper/runtime/ServletResponseWrapperInclude.java b/java/org/apache/jasper/runtime/ServletResponseWrapperInclude.java new file mode 100644 index 0000000..e0fa3d8 --- /dev/null +++ b/java/org/apache/jasper/runtime/ServletResponseWrapperInclude.java @@ -0,0 +1,78 @@ +/* + * 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.jasper.runtime; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.jsp.JspWriter; + +/** + * ServletResponseWrapper used by the JSP 'include' action. + * + * This wrapper response object is passed to RequestDispatcher.include(), so + * that the output of the included resource is appended to that of the + * including page. + * + * @author Pierre Delisle + */ + +public class ServletResponseWrapperInclude extends HttpServletResponseWrapper { + + /** + * PrintWriter which appends to the JspWriter of the including page. + */ + private final PrintWriter printWriter; + + private final JspWriter jspWriter; + + public ServletResponseWrapperInclude(ServletResponse response, + JspWriter jspWriter) { + super((HttpServletResponse)response); + this.printWriter = new PrintWriter(jspWriter); + this.jspWriter = jspWriter; + } + + /** + * Returns a wrapper around the JspWriter of the including page. + */ + @Override + public PrintWriter getWriter() throws IOException { + return printWriter; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + throw new IllegalStateException(); + } + + /** + * Clears the output buffer of the JspWriter associated with the including + * page. + */ + @Override + public void resetBuffer() { + try { + jspWriter.clearBuffer(); + } catch (IOException ioe) { + } + } +} diff --git a/java/org/apache/jasper/runtime/TagHandlerPool.java b/java/org/apache/jasper/runtime/TagHandlerPool.java new file mode 100644 index 0000000..c501cee --- /dev/null +++ b/java/org/apache/jasper/runtime/TagHandlerPool.java @@ -0,0 +1,182 @@ +/* + * 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.jasper.runtime; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.Tag; + +import org.apache.jasper.Constants; +import org.apache.jasper.compiler.Localizer; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; + +/** + * Pool of tag handlers that can be reused. + * + * @author Jan Luehe + */ +public class TagHandlerPool { + + private Tag[] handlers; + + public static final String OPTION_TAGPOOL = "tagpoolClassName"; + public static final String OPTION_MAXSIZE = "tagpoolMaxSize"; + public static final String OPTION_USEIMFORTAGS = "useInstanceManagerForTags"; + + // index of next available tag handler + private int current; + protected InstanceManager instanceManager = null; + protected boolean useInstanceManagerForTags; + + public static TagHandlerPool getTagHandlerPool(ServletConfig config) { + TagHandlerPool result = null; + + String tpClassName = getOption(config, OPTION_TAGPOOL, null); + if (tpClassName != null) { + try { + Class c = Class.forName(tpClassName); + result = (TagHandlerPool) c.getConstructor().newInstance(); + } catch (Exception e) { + LogFactory.getLog(TagHandlerPool.class).info(Localizer.getMessage("jsp.error.tagHandlerPool"), e); + result = null; + } + } + if (result == null) { + result = new TagHandlerPool(); + } + result.init(config); + + return result; + } + + protected void init(ServletConfig config) { + int maxSize = -1; + String maxSizeS = getOption(config, OPTION_MAXSIZE, null); + if (maxSizeS != null) { + try { + maxSize = Integer.parseInt(maxSizeS); + } catch (Exception ex) { + maxSize = -1; + } + } + if (maxSize < 0) { + maxSize = Constants.MAX_POOL_SIZE; + } + String useInstanceManagerForTagsValue = getOption(config, OPTION_USEIMFORTAGS, "false"); + useInstanceManagerForTags = Boolean.valueOf(useInstanceManagerForTagsValue).booleanValue(); + this.handlers = new Tag[maxSize]; + this.current = -1; + instanceManager = InstanceManagerFactory.getInstanceManager(config); + } + + /** + * Constructs a tag handler pool with the default capacity. + */ + public TagHandlerPool() { + // Nothing - jasper generated servlets call the other constructor, + // this should be used in future + init . + } + + /** + * Gets the next available tag handler from this tag handler pool, + * instantiating one if this tag handler pool is empty. + * + * @param handlerClass + * Tag handler class + * @return Reused or newly instantiated tag handler + * @throws JspException + * if a tag handler cannot be instantiated + */ + public Tag get(Class handlerClass) throws JspException { + Tag handler; + synchronized (this) { + if (current >= 0) { + handler = handlers[current--]; + return handler; + } + } + + // Out of sync block - there is no need for other threads to + // wait for us to construct a tag for this thread. + try { + if (useInstanceManagerForTags) { + return (Tag) instanceManager.newInstance( + handlerClass.getName(), handlerClass.getClassLoader()); + } else { + Tag instance = handlerClass.getConstructor().newInstance(); + instanceManager.newInstance(instance); + return instance; + } + } catch (Exception e) { + Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + throw new JspException(e.getMessage(), t); + } + } + + /** + * Adds the given tag handler to this tag handler pool, unless this tag + * handler pool has already reached its capacity, in which case the tag + * handler's release() method is called. + * + * @param handler + * Tag handler to add to this tag handler pool + */ + public void reuse(Tag handler) { + synchronized (this) { + if (current < (handlers.length - 1)) { + handlers[++current] = handler; + return; + } + } + // There is no need for other threads to wait for us to release + JspRuntimeLibrary.releaseTag(handler, instanceManager); + } + + /** + * Calls the release() method of all available tag handlers in this tag + * handler pool. + */ + public synchronized void release() { + for (int i = current; i >= 0; i--) { + JspRuntimeLibrary.releaseTag(handlers[i], instanceManager); + } + } + + + protected static String getOption(ServletConfig config, String name, + String defaultV) { + if (config == null) { + return defaultV; + } + + String value = config.getInitParameter(name); + if (value != null) { + return value; + } + if (config.getServletContext() == null) { + return defaultV; + } + value = config.getServletContext().getInitParameter(name); + if (value != null) { + return value; + } + return defaultV; + } + +} diff --git a/java/org/apache/jasper/security/SecurityClassLoad.java b/java/org/apache/jasper/security/SecurityClassLoad.java new file mode 100644 index 0000000..3642847 --- /dev/null +++ b/java/org/apache/jasper/security/SecurityClassLoad.java @@ -0,0 +1,64 @@ +/* + * 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.jasper.security; + +import org.apache.jasper.compiler.Localizer; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Static class used to preload java classes when using the + * Java SecurityManager so that the defineClassInPackage + * RuntimePermission does not trigger an AccessControlException. + */ +public final class SecurityClassLoad { + + public static void securityClassLoad(ClassLoader loader) { + + if (System.getSecurityManager() == null) { + return; + } + + final String basePackage = "org.apache.jasper."; + try { + // Ensure XMLInputFactory is loaded with Tomcat's class loader + loader.loadClass(basePackage + "compiler.EncodingDetector"); + + loader.loadClass(basePackage + "runtime.JspContextWrapper"); + loader.loadClass(basePackage + "runtime.JspFactoryImpl$PrivilegedGetPageContext"); + loader.loadClass(basePackage + "runtime.JspFactoryImpl$PrivilegedReleasePageContext"); + loader.loadClass(basePackage + "runtime.JspFragmentHelper"); + + Class clazz = loader.loadClass(basePackage + "runtime.JspRuntimeLibrary"); + clazz.getConstructor().newInstance(); + + loader.loadClass(basePackage + "runtime.PageContextImpl"); + + loader.loadClass(basePackage + "runtime.ProtectedFunctionMapper"); + loader.loadClass(basePackage + "runtime.ServletResponseWrapperInclude"); + loader.loadClass(basePackage + "runtime.TagHandlerPool"); + + // Trigger loading of class and reading of property + SecurityUtil.isPackageProtectionEnabled(); + + loader.loadClass(basePackage + "servlet.JspServletWrapper"); + } catch (Exception ex) { + Log log = LogFactory.getLog(SecurityClassLoad.class); + log.error(Localizer.getMessage("jsp.error.securityPreload"), ex); + } + } +} diff --git a/java/org/apache/jasper/security/SecurityUtil.java b/java/org/apache/jasper/security/SecurityUtil.java new file mode 100644 index 0000000..5b6f69d --- /dev/null +++ b/java/org/apache/jasper/security/SecurityUtil.java @@ -0,0 +1,41 @@ +/* + * 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.jasper.security; + +import org.apache.jasper.Constants; + +/** + * Util class for Security related operations. + */ + +public final class SecurityUtil{ + + private static final boolean packageDefinitionEnabled = + System.getProperty("package.definition") == null ? false : true; + + /** + * Return the SecurityManager only if Security is enabled AND + * package protection mechanism is enabled. + * @return true if package protection is enabled + */ + public static boolean isPackageProtectionEnabled(){ + if (packageDefinitionEnabled && Constants.IS_SECURITY_ENABLED){ + return true; + } + return false; + } +} diff --git a/java/org/apache/jasper/servlet/JasperInitializer.java b/java/org/apache/jasper/servlet/JasperInitializer.java new file mode 100644 index 0000000..f444040 --- /dev/null +++ b/java/org/apache/jasper/servlet/JasperInitializer.java @@ -0,0 +1,117 @@ +/* + * 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.jasper.servlet; + +import java.io.IOException; +import java.util.Set; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.jsp.JspFactory; + +import org.apache.jasper.Constants; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.compiler.TldCache; +import org.apache.jasper.runtime.JspFactoryImpl; +import org.apache.jasper.security.SecurityClassLoad; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.SimpleInstanceManager; +import org.xml.sax.SAXException; + +/** + * Initializer for the Jasper JSP Engine. + */ +public class JasperInitializer implements ServletContainerInitializer { + + private static final String MSG = "org.apache.jasper.servlet.JasperInitializer"; + private final Log log = LogFactory.getLog(JasperInitializer.class); // must not be static + + /* + * Preload classes required at runtime by a JSP servlet so that + * we don't get a defineClassInPackage security exception. + */ + static { + JspFactoryImpl factory = new JspFactoryImpl(); + SecurityClassLoad.securityClassLoad(factory.getClass().getClassLoader()); + if (JspFactory.getDefaultFactory() == null) { + JspFactory.setDefaultFactory(factory); + } + } + + @Override + public void onStartup(Set> types, ServletContext context) throws ServletException { + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage(MSG + ".onStartup", context.getServletContextName())); + } + + // Setup a simple default Instance Manager + if (context.getAttribute(InstanceManager.class.getName())==null) { + context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager()); + } + + boolean validate = Boolean.parseBoolean( + context.getInitParameter(Constants.XML_VALIDATION_TLD_INIT_PARAM)); + String blockExternalString = context.getInitParameter( + Constants.XML_BLOCK_EXTERNAL_INIT_PARAM); + boolean blockExternal; + if (blockExternalString == null) { + blockExternal = true; + } else { + blockExternal = Boolean.parseBoolean(blockExternalString); + } + + // scan the application for TLDs + TldScanner scanner = newTldScanner(context, true, validate, blockExternal); + try { + scanner.scan(); + } catch (IOException | SAXException e) { + throw new ServletException(e); + } + + // add any listeners defined in TLDs + for (String listener : scanner.getListeners()) { + context.addListener(listener); + } + + context.setAttribute(TldCache.SERVLET_CONTEXT_ATTRIBUTE_NAME, + new TldCache(context, scanner.getUriTldResourcePathMap(), + scanner.getTldResourcePathTaglibXmlMap())); + + String poolSizeValue = context.getInitParameter(Constants.JSP_FACTORY_POOL_SIZE_INIT_PARAM); + int poolSize = 8; + if (poolSizeValue != null) { + try { + poolSize = Integer.parseInt(poolSizeValue); + } catch (NumberFormatException e) { + throw new ServletException(e); + } + } + JspFactory factory = JspFactory.getDefaultFactory(); + if (factory instanceof JspFactoryImpl) { + ((JspFactoryImpl) factory).setPoolSize(poolSize); + } + + } + + protected TldScanner newTldScanner(ServletContext context, boolean namespaceAware, + boolean validate, boolean blockExternal) { + return new TldScanner(context, namespaceAware, validate, blockExternal); + } +} diff --git a/java/org/apache/jasper/servlet/JasperLoader.java b/java/org/apache/jasper/servlet/JasperLoader.java new file mode 100644 index 0000000..fa3e4b5 --- /dev/null +++ b/java/org/apache/jasper/servlet/JasperLoader.java @@ -0,0 +1,169 @@ +/* + * 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.jasper.servlet; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.PermissionCollection; + +/** + * Class loader for loading servlet class files (corresponding to JSP files) + * and tag handler class files (corresponding to tag files). + * + * @author Anil K. Vijendran + * @author Harish Prabandham + */ +public class JasperLoader extends URLClassLoader { + + private final PermissionCollection permissionCollection; + private final SecurityManager securityManager; + private final String packageName; + + public JasperLoader(URL[] urls, ClassLoader parent, + String packageName, PermissionCollection permissionCollection) { + super(urls, parent); + this.permissionCollection = permissionCollection; + this.securityManager = System.getSecurityManager(); + this.packageName = packageName; + } + + /** + * Load the class with the specified name. This method searches for + * classes in the same manner as loadClass(String, boolean) + * with false as the second argument. + * + * @param name Name of the class to be loaded + * + * @exception ClassNotFoundException if the class was not found + */ + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return loadClass(name, false); + } + + /** + * Load the class with the specified name, searching using the following + * algorithm until it finds and returns the class. If the class cannot + * be found, returns ClassNotFoundException. + *

      + *
    • Call findLoadedClass(String) to check if the + * class has already been loaded. If it has, the same + * Class object is returned.
    • + *
    • If the delegate property is set to true, + * call the loadClass() method of the parent class + * loader, if any.
    • + *
    • Call findClass() to find this class in our locally + * defined repositories.
    • + *
    • Call the loadClass() method of our parent + * class loader, if any.
    • + *
    + * If the class was found using the above steps, and the + * resolve flag is true, this method will then + * call resolveClass(Class) on the resulting Class object. + * + * @param name Name of the class to be loaded + * @param resolve If true then resolve the class + * + * @exception ClassNotFoundException if the class was not found + */ + @Override + public synchronized Class loadClass(final String name, boolean resolve) + throws ClassNotFoundException { + + Class clazz = null; + + // (0) Check our previously loaded class cache + clazz = findLoadedClass(name); + if (clazz != null) { + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + + // (.5) Permission to access this class when using a SecurityManager + if (securityManager != null) { + int dot = name.lastIndexOf('.'); + if (dot >= 0) { + try { + // Do not call the security manager since by default, we grant that package. + if (!"org.apache.jasper.runtime".equalsIgnoreCase(name.substring(0,dot))){ + securityManager.checkPackageAccess(name.substring(0,dot)); + } + } catch (SecurityException se) { + String error = "Security Violation, attempt to use " + + "Restricted Class: " + name; + se.printStackTrace(); + throw new ClassNotFoundException(error); + } + } + } + + if( !name.startsWith(packageName + '.') ) { + // Class is not in org.apache.jsp, therefore, have our + // parent load it + clazz = getParent().loadClass(name); + if( resolve ) { + resolveClass(clazz); + } + return clazz; + } + + return findClass(name); + } + + + /** + * Delegate to parent + * + * @see java.lang.ClassLoader#getResourceAsStream(String) + */ + @Override + public InputStream getResourceAsStream(String name) { + InputStream is = getParent().getResourceAsStream(name); + if (is == null) { + URL url = findResource(name); + if (url != null) { + try { + is = url.openStream(); + } catch (IOException e) { + // Ignore + } + } + } + return is; + } + + + /** + * Get the Permissions for a CodeSource. + * + * Since this ClassLoader is only used for a JSP page in + * a web application context, we just return our preset + * PermissionCollection for the web app context. + * + * @param codeSource Code source where the code was loaded from + * @return PermissionCollection for CodeSource + */ + @Override + public final PermissionCollection getPermissions(CodeSource codeSource) { + return permissionCollection; + } +} diff --git a/java/org/apache/jasper/servlet/JspCServletContext.java b/java/org/apache/jasper/servlet/JspCServletContext.java new file mode 100644 index 0000000..53743b4 --- /dev/null +++ b/java/org/apache/jasper/servlet/JspCServletContext.java @@ -0,0 +1,778 @@ +/* + * 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.jasper.servlet; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +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.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.FilterRegistration.Dynamic; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.descriptor.JspConfigDescriptor; + +import org.apache.jasper.Constants; +import org.apache.jasper.JasperException; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.runtime.ExceptionUtils; +import org.apache.tomcat.Jar; +import org.apache.tomcat.JarScanType; +import org.apache.tomcat.util.descriptor.web.FragmentJarScannerCallback; +import org.apache.tomcat.util.descriptor.web.WebXml; +import org.apache.tomcat.util.descriptor.web.WebXmlParser; +import org.apache.tomcat.util.scan.JarFactory; +import org.apache.tomcat.util.scan.StandardJarScanFilter; +import org.apache.tomcat.util.scan.StandardJarScanner; + + +/** + * Simple ServletContext implementation without + * HTTP-specific methods. + * + * @author Peter Rossbach (pr@webapp.de) + */ + +public class JspCServletContext implements ServletContext { + + + // ----------------------------------------------------- Instance Variables + + + /** + * Servlet context attributes. + */ + private final Map myAttributes; + + + /** + * Servlet context initialization parameters. + */ + private final Map myParameters = new ConcurrentHashMap<>(); + + + /** + * The log writer we will write log messages to. + */ + private final PrintWriter myLogWriter; + + + /** + * The base URL (document root) for this context. + */ + private final URL myResourceBaseURL; + + + /** + * Merged web.xml for the application. + */ + private WebXml webXml; + + + private List resourceJARs; + + + private JspConfigDescriptor jspConfigDescriptor; + + + /** + * Web application class loader. + */ + private final ClassLoader loader; + + + // ----------------------------------------------------------- Constructors + + /** + * Create a new instance of this ServletContext implementation. + * + * @param aLogWriter PrintWriter which is used for log() calls + * @param aResourceBaseURL Resource base URL + * @param classLoader Class loader for this {@link ServletContext} + * @param validate Should a validating parser be used to parse web.xml? + * @param blockExternal Should external entities be blocked when parsing + * web.xml? + * @throws JasperException An error occurred building the merged web.xml + */ + public JspCServletContext(PrintWriter aLogWriter, URL aResourceBaseURL, + ClassLoader classLoader, boolean validate, boolean blockExternal) + throws JasperException { + + myAttributes = new HashMap<>(); + myParameters.put(Constants.XML_BLOCK_EXTERNAL_INIT_PARAM, + String.valueOf(blockExternal)); + myLogWriter = aLogWriter; + myResourceBaseURL = aResourceBaseURL; + this.loader = classLoader; + this.webXml = buildMergedWebXml(validate, blockExternal); + jspConfigDescriptor = webXml.getJspConfigDescriptor(); + } + + private WebXml buildMergedWebXml(boolean validate, boolean blockExternal) + throws JasperException { + WebXml webXml = new WebXml(); + WebXmlParser webXmlParser = new WebXmlParser(validate, validate, blockExternal); + // Use this class's classloader as Ant will have set the TCCL to its own + webXmlParser.setClassLoader(getClass().getClassLoader()); + + try { + URL url = getResource( + org.apache.tomcat.util.descriptor.web.Constants.WEB_XML_LOCATION); + if (!webXmlParser.parseWebXml(url, webXml, false)) { + throw new JasperException(Localizer.getMessage("jspc.error.invalidWebXml")); + } + } catch (IOException e) { + throw new JasperException(e); + } + + // if the application is metadata-complete then we can skip fragment processing + if (webXml.isMetadataComplete()) { + return webXml; + } + + // If an empty absolute ordering element is present, fragment processing + // may be skipped. + Set absoluteOrdering = webXml.getAbsoluteOrdering(); + if (absoluteOrdering != null && absoluteOrdering.isEmpty()) { + return webXml; + } + + Map fragments = scanForFragments(webXmlParser); + Set orderedFragments = WebXml.orderWebFragments(webXml, fragments, this); + + // Find resource JARs + this.resourceJARs = scanForResourceJARs(orderedFragments, fragments.values()); + + // JspC is not affected by annotations so skip that processing, proceed to merge + webXml.merge(orderedFragments); + return webXml; + } + + + private List scanForResourceJARs(Set orderedFragments, Collection fragments) + throws JasperException { + List resourceJars = new ArrayList<>(); + // Build list of potential resource JARs. Use same ordering as ContextConfig + Set resourceFragments = new LinkedHashSet<>(orderedFragments); + for (WebXml fragment : fragments) { + if (!resourceFragments.contains(fragment)) { + resourceFragments.add(fragment); + } + } + + for (WebXml resourceFragment : resourceFragments) { + try (Jar jar = JarFactory.newInstance(resourceFragment.getURL())) { + if (jar.exists("META-INF/resources/")) { + // This is a resource JAR + resourceJars.add(resourceFragment.getURL()); + } + } catch (IOException ioe) { + throw new JasperException(ioe); + } + } + + return resourceJars; + } + + + private Map scanForFragments(WebXmlParser webXmlParser) throws JasperException { + StandardJarScanner scanner = new StandardJarScanner(); + // TODO - enabling this means initializing the classloader first in JspC + scanner.setScanClassPath(false); + // TODO - configure filter rules from Ant rather then system properties + scanner.setJarScanFilter(new StandardJarScanFilter()); + + FragmentJarScannerCallback callback = + new FragmentJarScannerCallback(webXmlParser, false, true); + scanner.scan(JarScanType.PLUGGABILITY, this, callback); + if (!callback.isOk()) { + throw new JasperException(Localizer.getMessage("jspc.error.invalidFragment")); + } + return callback.getFragments(); + } + + + // --------------------------------------------------------- Public Methods + + /** + * Return the specified context attribute, if any. + * + * @param name Name of the requested attribute + */ + @Override + public Object getAttribute(String name) { + return myAttributes.get(name); + } + + + /** + * Return an enumeration of context attribute names. + */ + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(myAttributes.keySet()); + } + + + /** + * Return the servlet context for the specified path. + * + * @param uripath Server-relative path starting with '/' + */ + @Override + public ServletContext getContext(String uripath) { + return null; + } + + + /** + * Return the context path. + */ + @Override + public String getContextPath() { + return null; + } + + + /** + * Return the specified context initialization parameter. + * + * @param name Name of the requested parameter + */ + @Override + public String getInitParameter(String name) { + return myParameters.get(name); + } + + + /** + * Return an enumeration of the names of context initialization + * parameters. + */ + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(myParameters.keySet()); + } + + + /** + * Return the Servlet API major version number. + */ + @Override + public int getMajorVersion() { + return 4; + } + + + /** + * Return the MIME type for the specified filename. + * + * @param file Filename whose MIME type is requested + */ + @Override + public String getMimeType(String file) { + return null; + } + + + /** + * Return the Servlet API minor version number. + */ + @Override + public int getMinorVersion() { + return 0; + } + + + /** + * Return a request dispatcher for the specified servlet name. + * + * @param name Name of the requested servlet + */ + @Override + public RequestDispatcher getNamedDispatcher(String name) { + return null; + } + + + /** + * Return the real path for the specified context-relative + * virtual path. + * + * @param path The context-relative virtual path to resolve + */ + @Override + public String getRealPath(String path) { + if (!myResourceBaseURL.getProtocol().equals("file")) { + return null; + } + if (!path.startsWith("/")) { + return null; + } + try { + URL url = getResource(path); + if (url == null) { + return null; + } + File f = new File(url.toURI()); + return f.getAbsolutePath(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + return null; + } + } + + + /** + * Return a request dispatcher for the specified context-relative path. + * + * @param path Context-relative path for which to acquire a dispatcher + */ + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return null; + } + + + /** + * Return a URL object of a resource that is mapped to the + * specified context-relative path. + * + * @param path Context-relative path of the desired resource + * + * @exception MalformedURLException if the resource path is + * not properly formed + */ + @Override + public URL getResource(String path) throws MalformedURLException { + + if (!path.startsWith("/")) { + throw new MalformedURLException(Localizer.getMessage("jsp.error.URLMustStartWithSlash", path)); + } + + // Strip leading '/' + path = path.substring(1); + + URL url = null; + try { + URI uri = new URI(myResourceBaseURL.toExternalForm() + path); + url = uri.toURL(); + try (InputStream is = url.openStream()) { + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + url = null; + } + + // During initialisation, getResource() is called before resourceJARs is + // initialised + if (url == null && resourceJARs != null) { + String jarPath = "META-INF/resources/" + path; + for (URL jarUrl : resourceJARs) { + try (Jar jar = JarFactory.newInstance(jarUrl)) { + if (jar.exists(jarPath)) { + return new URI(jar.getURL(jarPath)).toURL(); + } + } catch (IOException | URISyntaxException ioe) { + // Ignore + } + } + } + return url; + } + + + /** + * Return an InputStream allowing access to the resource at the + * specified context-relative path. + * + * @param path Context-relative path of the desired resource + */ + @Override + public InputStream getResourceAsStream(String path) { + try { + URL url = getResource(path); + if (url == null) { + return null; + } + return url.openStream(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + return null; + } + } + + + /** + * Return the set of resource paths for the "directory" at the + * specified context path. + * + * @param path Context-relative base path + */ + @Override + public Set getResourcePaths(String path) { + + Set thePaths = new HashSet<>(); + if (!path.endsWith("/")) { + path += "/"; + } + String basePath = getRealPath(path); + if (basePath != null) { + File theBaseDir = new File(basePath); + if (theBaseDir.isDirectory()) { + String theFiles[] = theBaseDir.list(); + if (theFiles != null) { + for (String theFile : theFiles) { + File testFile = new File(basePath + File.separator + theFile); + if (testFile.isFile()) { + thePaths.add(path + theFile); + } else if (testFile.isDirectory()) { + thePaths.add(path + theFile + "/"); + } + } + } + } + } + + // During initialisation, getResourcePaths() is called before + // resourceJARs is initialised + if (resourceJARs != null) { + String jarPath = "META-INF/resources" + path; + for (URL jarUrl : resourceJARs) { + try (Jar jar = JarFactory.newInstance(jarUrl)) { + jar.nextEntry(); + for (String entryName = jar.getEntryName(); + entryName != null; + jar.nextEntry(), entryName = jar.getEntryName()) { + if (entryName.startsWith(jarPath) && + entryName.length() > jarPath.length()) { + // Let the Set implementation handle duplicates + int sep = entryName.indexOf('/', jarPath.length()); + if (sep < 0) { + // This is a file - strip leading "META-INF/resources" + thePaths.add(entryName.substring(18)); + } else { + // This is a directory - strip leading "META-INF/resources" + thePaths.add(entryName.substring(18, sep + 1)); + } + } + } + } catch (IOException e) { + log(e.getMessage(), e); + } + } + } + + return thePaths; + } + + + /** + * Return descriptive information about this server. + */ + @Override + public String getServerInfo() { + return "JspC/ApacheTomcat10"; + } + + + /** + * Return the name of this servlet context. + */ + @Override + public String getServletContextName() { + return getServerInfo(); + } + + + /** + * Log the specified message. + * + * @param message The message to be logged + */ + @Override + public void log(String message) { + myLogWriter.println(message); + } + + + /** + * Log the specified message and exception. + * + * @param message The message to be logged + * @param exception The exception to be logged + */ + @Override + public void log(String message, Throwable exception) { + myLogWriter.println(message); + exception.printStackTrace(myLogWriter); + } + + + /** + * Remove the specified context attribute. + * + * @param name Name of the attribute to remove + */ + @Override + public void removeAttribute(String name) { + myAttributes.remove(name); + } + + + /** + * Set or replace the specified context attribute. + * + * @param name Name of the context attribute to set + * @param value Corresponding attribute value + */ + @Override + public void setAttribute(String name, Object value) { + myAttributes.put(name, value); + } + + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, + String className) { + return null; + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, + String className) { + return null; + } + + + @Override + public Set getDefaultSessionTrackingModes() { + return EnumSet.noneOf(SessionTrackingMode.class); + } + + + @Override + public Set getEffectiveSessionTrackingModes() { + return EnumSet.noneOf(SessionTrackingMode.class); + } + + + @Override + public SessionCookieConfig getSessionCookieConfig() { + return null; + } + + + @Override + public void setSessionTrackingModes( + Set sessionTrackingModes) { + // Do nothing + } + + + @Override + public Dynamic addFilter(String filterName, Filter filter) { + return null; + } + + + @Override + public Dynamic addFilter(String filterName, + Class filterClass) { + return null; + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, + Servlet servlet) { + return null; + } + + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, + Class servletClass) { + return null; + } + + + @Override + public ServletRegistration.Dynamic addJspFile(String jspName, String jspFile) { + return null; + } + + + @Override + public T createFilter(Class c) + throws ServletException { + return null; + } + + + @Override + public T createServlet(Class c) + throws ServletException { + return null; + } + + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + return null; + } + + + @Override + public ServletRegistration getServletRegistration(String servletName) { + return null; + } + + + @Override + public boolean setInitParameter(String name, String value) { + return myParameters.putIfAbsent(name, value) == null; + } + + + @Override + public void addListener(Class listenerClass) { + // NOOP + } + + + @Override + public void addListener(String className) { + // NOOP + } + + + @Override + public void addListener(T t) { + // NOOP + } + + + @Override + public T createListener(Class c) + throws ServletException { + return null; + } + + + @Override + public void declareRoles(String... roleNames) { + // NOOP + } + + + @Override + public ClassLoader getClassLoader() { + return loader; + } + + + @Override + public int getEffectiveMajorVersion() { + return webXml.getMajorVersion(); + } + + + @Override + public int getEffectiveMinorVersion() { + return webXml.getMinorVersion(); + } + + + @Override + public Map getFilterRegistrations() { + return null; + } + + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return jspConfigDescriptor; + } + + + @Override + public Map getServletRegistrations() { + return null; + } + + + @Override + public String getVirtualServerName() { + return null; + } + + @Override + public int getSessionTimeout() { + return 0; + } + + @Override + public void setSessionTimeout(int sessionTimeout) { + // NO-OP + } + + @Override + public String getRequestCharacterEncoding() { + return null; + } + + @Override + public void setRequestCharacterEncoding(String encoding) { + // NO-OP + } + + @Override + public String getResponseCharacterEncoding() { + return null; + } + + @Override + public void setResponseCharacterEncoding(String encoding) { + // NO-OP + } +} diff --git a/java/org/apache/jasper/servlet/JspServlet.java b/java/org/apache/jasper/servlet/JspServlet.java new file mode 100644 index 0000000..d5deb7a --- /dev/null +++ b/java/org/apache/jasper/servlet/JspServlet.java @@ -0,0 +1,412 @@ +/* + * 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.jasper.servlet; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.net.MalformedURLException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.jasper.Constants; +import org.apache.jasper.EmbeddedServletOptions; +import org.apache.jasper.Options; +import org.apache.jasper.compiler.JspRuntimeContext; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.runtime.ExceptionUtils; +import org.apache.jasper.security.SecurityUtil; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.PeriodicEventListener; +import org.apache.tomcat.util.security.Escape; + +/** + * The JSP engine (a.k.a Jasper). + * + * The servlet container is responsible for providing a + * URLClassLoader for the web application context Jasper + * is being used in. Jasper will try get the Tomcat + * ServletContext attribute for its ServletContext class + * loader, if that fails, it uses the parent class loader. + * In either case, it must be a URLClassLoader. + * + * @author Anil K. Vijendran + * @author Harish Prabandham + * @author Remy Maucherat + * @author Kin-man Chung + * @author Glenn Nielsen + */ +public class JspServlet extends HttpServlet implements PeriodicEventListener { + + private static final long serialVersionUID = 1L; + + // Logger + private final transient Log log = LogFactory.getLog(JspServlet.class); + + private transient ServletContext context; + private ServletConfig config; + private transient Options options; + private transient JspRuntimeContext rctxt; + // jspFile for a jsp configured explicitly as a servlet, in environments where this + // configuration is translated into an init-param for this servlet. + private String jspFile; + + + /* + * Initializes this JspServlet. + */ + @Override + public void init(ServletConfig config) throws ServletException { + + super.init(config); + this.config = config; + this.context = config.getServletContext(); + + // Initialize the JSP Runtime Context + // Check for a custom Options implementation + String engineOptionsName = config.getInitParameter("engineOptionsClass"); + if (Constants.IS_SECURITY_ENABLED && engineOptionsName != null) { + log.info(Localizer.getMessage( + "jsp.info.ignoreSetting", "engineOptionsClass", engineOptionsName)); + engineOptionsName = null; + } + if (engineOptionsName != null) { + // Instantiate the indicated Options implementation + try { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Class engineOptionsClass = loader.loadClass(engineOptionsName); + Class[] ctorSig = { ServletConfig.class, ServletContext.class }; + Constructor ctor = engineOptionsClass.getConstructor(ctorSig); + Object[] args = { config, context }; + options = (Options) ctor.newInstance(args); + } catch (Throwable e) { + e = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(e); + // Need to localize this. + log.warn(Localizer.getMessage("jsp.warning.engineOptionsClass", engineOptionsName), e); + // Use the default Options implementation + options = new EmbeddedServletOptions(config, context); + } + } else { + // Use the default Options implementation + options = new EmbeddedServletOptions(config, context); + } + rctxt = new JspRuntimeContext(context, options); + if (config.getInitParameter("jspFile") != null) { + jspFile = config.getInitParameter("jspFile"); + try { + if (null == context.getResource(jspFile)) { + return; + } + } catch (MalformedURLException e) { + throw new ServletException(Localizer.getMessage("jsp.error.no.jsp", jspFile), e); + } + try { + if (SecurityUtil.isPackageProtectionEnabled()){ + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + serviceJspFile(null, null, jspFile, true); + return null; + }); + } else { + serviceJspFile(null, null, jspFile, true); + } + } catch (IOException e) { + throw new ServletException(Localizer.getMessage("jsp.error.precompilation", jspFile), e); + } catch (PrivilegedActionException e) { + Throwable t = e.getCause(); + if (t instanceof ServletException) { + throw (ServletException)t; + } + throw new ServletException(Localizer.getMessage("jsp.error.precompilation", jspFile), e); + } + } + + if (log.isDebugEnabled()) { + log.debug(Localizer.getMessage("jsp.message.scratch.dir.is", + options.getScratchDir().toString())); + log.debug(Localizer.getMessage("jsp.message.dont.modify.servlets")); + } + } + + + /** + * Returns the number of JSPs for which JspServletWrappers exist, i.e., + * the number of JSPs that have been loaded into the webapp with which + * this JspServlet is associated. + * + *

    This info may be used for monitoring purposes. + * + * @return The number of JSPs that have been loaded into the webapp with + * which this JspServlet is associated + */ + public int getJspCount() { + return this.rctxt.getJspCount(); + } + + + /** + * Resets the JSP reload counter. + * + * @param count Value to which to reset the JSP reload counter + */ + public void setJspReloadCount(int count) { + this.rctxt.setJspReloadCount(count); + } + + + /** + * Gets the number of JSPs that have been reloaded. + * + *

    This info may be used for monitoring purposes. + * + * @return The number of JSPs (in the webapp with which this JspServlet is + * associated) that have been reloaded + */ + public int getJspReloadCount() { + return this.rctxt.getJspReloadCount(); + } + + + /** + * Gets the number of JSPs that are in the JSP limiter queue + * + *

    This info may be used for monitoring purposes. + * + * @return The number of JSPs (in the webapp with which this JspServlet is + * associated) that are in the JSP limiter queue + */ + public int getJspQueueLength() { + return this.rctxt.getJspQueueLength(); + } + + + /** + * Gets the number of JSPs that have been unloaded. + * + *

    This info may be used for monitoring purposes. + * + * @return The number of JSPs (in the webapp with which this JspServlet is + * associated) that have been unloaded + */ + public int getJspUnloadCount() { + return this.rctxt.getJspUnloadCount(); + } + + + /** + *

    Look for a precompilation request as described in + * Section 8.4.2 of the JSP 1.2 Specification. WARNING - + * we cannot use request.getParameter() for this, because + * that will trigger parsing all of the request parameters, and not give + * a servlet the opportunity to call + * request.setCharacterEncoding() first.

    + * + * @param request The servlet request we are processing + * + * @exception ServletException if an invalid parameter value for the + * jsp_precompile parameter name is specified + */ + boolean preCompile(HttpServletRequest request) throws ServletException { + + String precompileParameter = rctxt.getOptions().getJspPrecompilationQueryParameter(); + String queryString = request.getQueryString(); + if (queryString == null) { + return false; + } + int start = queryString.indexOf(precompileParameter); + if (start < 0) { + return false; + } + queryString = + queryString.substring(start + precompileParameter.length()); + if (queryString.length() == 0) { + return true; // ?jsp_precompile + } + if (queryString.startsWith("&")) { + return true; // ?jsp_precompile&foo=bar... + } + if (!queryString.startsWith("=")) { + return false; // part of some other name or value + } + int limit = queryString.length(); + int ampersand = queryString.indexOf('&'); + if (ampersand > 0) { + limit = ampersand; + } + String value = queryString.substring(1, limit); + if (value.equals("true")) { + return true; // ?jsp_precompile=true + } else if (value.equals("false")) { + // Spec says if jsp_precompile=false, the request should not + // be delivered to the JSP page; the easiest way to implement + // this is to set the flag to true, and precompile the page anyway. + // This still conforms to the spec, since it says the + // precompilation request can be ignored. + return true; // ?jsp_precompile=false + } else { + throw new ServletException(Localizer.getMessage("jsp.error.precompilation.parameter", + precompileParameter, value)); + } + + } + + + @Override + public void service (HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + // jspFile may be configured as an init-param for this servlet instance + String jspUri = jspFile; + + if (jspUri == null) { + /* + * Check to see if the requested JSP has been the target of a + * RequestDispatcher.include() + */ + jspUri = (String) request.getAttribute( + RequestDispatcher.INCLUDE_SERVLET_PATH); + if (jspUri != null) { + /* + * Requested JSP has been target of + * RequestDispatcher.include(). Its path is assembled from the + * relevant jakarta.servlet.include.* request attributes + */ + String pathInfo = (String) request.getAttribute( + RequestDispatcher.INCLUDE_PATH_INFO); + if (pathInfo != null) { + jspUri += pathInfo; + } + } else { + /* + * Requested JSP has not been the target of a + * RequestDispatcher.include(). Reconstruct its path from the + * request's getServletPath() and getPathInfo() + */ + jspUri = request.getServletPath(); + String pathInfo = request.getPathInfo(); + if (pathInfo != null) { + jspUri += pathInfo; + } + } + } + + if (log.isTraceEnabled()) { + log.trace("JspEngine --> " + jspUri); + log.trace("\t ServletPath: " + request.getServletPath()); + log.trace("\t PathInfo: " + request.getPathInfo()); + log.trace("\t RealPath: " + context.getRealPath(jspUri)); + log.trace("\t RequestURI: " + request.getRequestURI()); + log.trace("\t QueryString: " + request.getQueryString()); + } + + try { + boolean precompile = preCompile(request); + serviceJspFile(request, response, jspUri, precompile); + } catch (RuntimeException | IOException | ServletException e) { + throw e; + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + throw new ServletException(e); + } + + } + + @Override + public void destroy() { + if (log.isTraceEnabled()) { + log.trace("JspServlet.destroy()"); + } + + rctxt.destroy(); + } + + + @Override + public void periodicEvent() { + rctxt.checkUnload(); + rctxt.checkCompile(); + } + + // -------------------------------------------------------- Private Methods + + private void serviceJspFile(HttpServletRequest request, + HttpServletResponse response, String jspUri, + boolean precompile) + throws ServletException, IOException { + + JspServletWrapper wrapper = rctxt.getWrapper(jspUri); + if (wrapper == null) { + synchronized(this) { + wrapper = rctxt.getWrapper(jspUri); + if (wrapper == null) { + // Check if the requested JSP page exists, to avoid + // creating unnecessary directories and files. + if (null == context.getResource(jspUri)) { + handleMissingResource(request, response, jspUri); + return; + } + wrapper = new JspServletWrapper(config, options, jspUri, + rctxt); + rctxt.addWrapper(jspUri,wrapper); + } + } + } + + try { + wrapper.service(request, response, precompile); + } catch (FileNotFoundException fnfe) { + handleMissingResource(request, response, jspUri); + } + + } + + + private void handleMissingResource(HttpServletRequest request, + HttpServletResponse response, String jspUri) + throws ServletException, IOException { + + String includeRequestUri = + (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); + + String msg = Localizer.getMessage("jsp.error.file.not.found",jspUri); + if (includeRequestUri != null) { + // This file was included. Throw an exception as + // a response.sendError() will be ignored + // Strictly, filtering this is an application + // responsibility but just in case... + throw new ServletException(Escape.htmlElementContent(msg)); + } else { + try { + response.sendError(HttpServletResponse.SC_NOT_FOUND, msg); + } catch (IllegalStateException ise) { + log.error(msg); + } + } + } + + +} diff --git a/java/org/apache/jasper/servlet/JspServletWrapper.java b/java/org/apache/jasper/servlet/JspServletWrapper.java new file mode 100644 index 0000000..b2f53cf --- /dev/null +++ b/java/org/apache/jasper/servlet/JspServletWrapper.java @@ -0,0 +1,615 @@ +/* + * 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.jasper.servlet; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.UnavailableException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.tagext.TagInfo; + +import org.apache.jasper.JasperException; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.Options; +import org.apache.jasper.compiler.JavacErrorDetail; +import org.apache.jasper.compiler.JspRuntimeContext; +import org.apache.jasper.compiler.Localizer; +import org.apache.jasper.compiler.SmapInput; +import org.apache.jasper.compiler.SmapStratum; +import org.apache.jasper.runtime.ExceptionUtils; +import org.apache.jasper.runtime.InstanceManagerFactory; +import org.apache.jasper.runtime.JspSourceDependent; +import org.apache.jasper.util.FastRemovalDequeue; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.Jar; + +/** + * The JSP engine (a.k.a Jasper). + * + * The servlet container is responsible for providing a + * URLClassLoader for the web application context Jasper + * is being used in. Jasper will try get the Tomcat + * ServletContext attribute for its ServletContext class + * loader, if that fails, it uses the parent class loader. + * In either case, it must be a URLClassLoader. + * + * @author Anil K. Vijendran + * @author Harish Prabandham + * @author Remy Maucherat + * @author Kin-man Chung + * @author Glenn Nielsen + * @author Tim Fennell + */ +public class JspServletWrapper { + + private static final Map ALWAYS_OUTDATED_DEPENDENCIES = + new HashMap<>(); + + static { + // If this is missing, + ALWAYS_OUTDATED_DEPENDENCIES.put("/WEB-INF/web.xml", Long.valueOf(-1)); + } + + // Logger + private final Log log = LogFactory.getLog(JspServletWrapper.class); // must not be static + + private volatile Servlet theServlet; + private final String jspUri; + private volatile Class tagHandlerClass; + private final JspCompilationContext ctxt; + private long available = 0L; + private final ServletConfig config; + private final Options options; + /* + * The servlet / tag file needs a compilation check on first access. Use a + * separate flag (rather then theServlet == null / tagHandlerClass == null + * as it avoids the potentially expensive isOutDated() calls in + * ctxt.compile() if there are multiple concurrent requests for the servlet + * / tag before the class has been loaded. + */ + private volatile boolean mustCompile = true; + /* Whether the servlet/tag file needs reloading on next access */ + private volatile boolean reload = true; + private final boolean isTagFile; + private int tripCount; + private JasperException compileException; + /* Timestamp of last time servlet resource was modified */ + private volatile long servletClassLastModifiedTime; + private long lastModificationTest = 0L; + private long lastUsageTime = System.currentTimeMillis(); + private FastRemovalDequeue.Entry unloadHandle; + private final boolean unloadAllowed; + private final boolean unloadByCount; + private final boolean unloadByIdle; + + /* + * JspServletWrapper for JSP pages. + */ + public JspServletWrapper(ServletConfig config, Options options, + String jspUri, JspRuntimeContext rctxt) { + + this.isTagFile = false; + this.config = config; + this.options = options; + this.jspUri = jspUri; + unloadByCount = options.getMaxLoadedJsps() > 0 ? true : false; + unloadByIdle = options.getJspIdleTimeout() > 0 ? true : false; + unloadAllowed = unloadByCount || unloadByIdle ? true : false; + ctxt = new JspCompilationContext(jspUri, options, + config.getServletContext(), + this, rctxt); + } + + /* + * JspServletWrapper for tag files. + */ + public JspServletWrapper(ServletContext servletContext, + Options options, + String tagFilePath, + TagInfo tagInfo, + JspRuntimeContext rctxt, + Jar tagJar) { + + this.isTagFile = true; + this.config = null; // not used + this.options = options; + this.jspUri = tagFilePath; + this.tripCount = 0; + unloadByCount = options.getMaxLoadedJsps() > 0 ? true : false; + unloadByIdle = options.getJspIdleTimeout() > 0 ? true : false; + unloadAllowed = unloadByCount || unloadByIdle ? true : false; + ctxt = new JspCompilationContext(jspUri, tagInfo, options, + servletContext, this, rctxt, + tagJar); + } + + public JspCompilationContext getJspEngineContext() { + return ctxt; + } + + public void setReload(boolean reload) { + this.reload = reload; + } + + public boolean getReload() { + return reload; + } + + private boolean getReloadInternal() { + return reload && !ctxt.getRuntimeContext().isCompileCheckInProgress(); + } + + public Servlet getServlet() throws ServletException { + /* + * DCL on 'reload' requires that 'reload' be volatile + * (this also forces a read memory barrier, ensuring the new servlet + * object is read consistently). + * + * When running in non development mode with a checkInterval it is + * possible (see BZ 62603) for a race condition to cause failures + * if a Servlet or tag is reloaded while a compile check is running + */ + if (getReloadInternal() || theServlet == null) { + synchronized (this) { + // Synchronizing on jsw enables simultaneous loading + // of different pages, but not the same page. + if (getReloadInternal() || theServlet == null) { + // This is to maintain the original protocol. + destroy(); + + final Servlet servlet; + + try { + InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(config); + servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader()); + } catch (Exception e) { + Throwable t = ExceptionUtils + .unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + throw new JasperException(t); + } + + servlet.init(config); + + if (theServlet != null) { + ctxt.getRuntimeContext().incrementJspReloadCount(); + } + + theServlet = servlet; + reload = false; + // Volatile 'reload' forces in order write of 'theServlet' and new servlet object + } + } + } + return theServlet; + } + + public ServletContext getServletContext() { + return ctxt.getServletContext(); + } + + /** + * Sets the compilation exception for this JspServletWrapper. + * + * @param je The compilation exception + */ + public void setCompilationException(JasperException je) { + this.compileException = je; + } + + /** + * Sets the last-modified time of the servlet class file associated with + * this JspServletWrapper. + * + * @param lastModified Last-modified time of servlet class + */ + public void setServletClassLastModifiedTime(long lastModified) { + // DCL requires servletClassLastModifiedTime be volatile + // to force read and write barriers on access/set + // (and to get atomic write of long) + if (this.servletClassLastModifiedTime < lastModified) { + synchronized (this) { + if (this.servletClassLastModifiedTime < lastModified) { + this.servletClassLastModifiedTime = lastModified; + reload = true; + // Really need to unload the old class but can't do that. Do + // the next best thing which is throw away the JspLoader so + // a new loader will be created which will load the new + // class. + // TODO Are there inefficiencies between reload and the + // isOutDated() check? + ctxt.clearJspLoader(); + } + } + } + } + + /** + * Compile (if needed) and load a tag file. + * @return the loaded class + * @throws JasperException Error compiling or loading tag file + */ + public Class loadTagFile() throws JasperException { + + try { + if (ctxt.isRemoved()) { + throw new FileNotFoundException(jspUri); + } + if (options.getDevelopment() || mustCompile) { + synchronized (this) { + if (options.getDevelopment() || mustCompile) { + ctxt.compile(); + mustCompile = false; + } + } + } else { + if (compileException != null) { + throw compileException; + } + } + + if (getReloadInternal() || tagHandlerClass == null) { + synchronized (this) { + if (getReloadInternal() || tagHandlerClass == null) { + tagHandlerClass = ctxt.load(); + // Volatile 'reload' forces in order write of 'tagHandlerClass' + reload = false; + } + } + } + } catch (FileNotFoundException ex) { + throw new JasperException(ex); + } + + return tagHandlerClass; + } + + /** + * Compile and load a prototype for the Tag file. This is needed + * when compiling tag files with circular dependencies. A prototype + * (skeleton) with no dependencies on other other tag files is + * generated and compiled. + * @return the loaded class + * @throws JasperException Error compiling or loading tag file + */ + public Class loadTagFilePrototype() throws JasperException { + + ctxt.setPrototypeMode(true); + try { + return loadTagFile(); + } finally { + ctxt.setPrototypeMode(false); + } + } + + /** + * Get a list of files that the current page has source dependency on. + * @return the map of dependent resources + */ + public Map getDependants() { + try { + Object target; + if (isTagFile) { + if (reload) { + synchronized (this) { + if (reload) { + tagHandlerClass = ctxt.load(); + reload = false; + } + } + } + target = tagHandlerClass.getConstructor().newInstance(); + } else { + target = getServlet(); + } + if (target instanceof JspSourceDependent) { + return ((JspSourceDependent) target).getDependants(); + } + } catch (AbstractMethodError ame) { + // Almost certainly a pre Tomcat 7.0.17 compiled JSP using the old + // version of the interface. Force a re-compile. + return ALWAYS_OUTDATED_DEPENDENCIES; + } catch (Throwable ex) { + ExceptionUtils.handleThrowable(ex); + } + return null; + } + + public boolean isTagFile() { + return this.isTagFile; + } + + public int incTripCount() { + return tripCount++; + } + + public int decTripCount() { + return tripCount--; + } + + public String getJspUri() { + return jspUri; + } + + public FastRemovalDequeue.Entry getUnloadHandle() { + return unloadHandle; + } + + public void service(HttpServletRequest request, + HttpServletResponse response, + boolean precompile) + throws ServletException, IOException, FileNotFoundException { + + Servlet servlet; + + try { + + if (ctxt.isRemoved()) { + throw new FileNotFoundException(jspUri); + } + + if ((available > 0L) && (available < Long.MAX_VALUE)) { + if (available > System.currentTimeMillis()) { + response.setDateHeader("Retry-After", available); + response.sendError + (HttpServletResponse.SC_SERVICE_UNAVAILABLE, + Localizer.getMessage("jsp.error.unavailable")); + return; + } + + // Wait period has expired. Reset. + available = 0; + } + + /* + * (1) Compile + */ + if (options.getDevelopment() || mustCompile) { + synchronized (this) { + if (options.getDevelopment() || mustCompile) { + // The following sets reload to true, if necessary + ctxt.compile(); + mustCompile = false; + } + } + } else { + if (compileException != null) { + // Throw cached compilation exception + throw compileException; + } + } + + /* + * (2) (Re)load servlet class file + */ + servlet = getServlet(); + + // If a page is to be precompiled only, return. + if (precompile) { + return; + } + + } catch (FileNotFoundException fnfe) { + // File has been removed. Let caller handle this. + throw fnfe; + } catch (ServletException | IOException | IllegalStateException ex) { + if (options.getDevelopment()) { + throw handleJspException(ex); + } + throw ex; + } catch (Exception ex) { + if (options.getDevelopment()) { + throw handleJspException(ex); + } + throw new JasperException(ex); + } + + try { + /* + * (3) Handle limitation of number of loaded Jsps + */ + if (unloadAllowed) { + synchronized(this) { + if (unloadByCount) { + if (unloadHandle == null) { + unloadHandle = ctxt.getRuntimeContext().push(this); + } else if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) { + ctxt.getRuntimeContext().makeYoungest(unloadHandle); + lastUsageTime = System.currentTimeMillis(); + } + } else { + if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) { + lastUsageTime = System.currentTimeMillis(); + } + } + } + } + + /* + * (4) Service request + */ + servlet.service(request, response); + } catch (UnavailableException ex) { + String includeRequestUri = (String) + request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); + if (includeRequestUri != null) { + // This file was included. Throw an exception as + // a response.sendError() will be ignored by the + // servlet engine. + throw ex; + } + + int unavailableSeconds = ex.getUnavailableSeconds(); + if (unavailableSeconds <= 0) { + unavailableSeconds = 60; // Arbitrary default + } + available = System.currentTimeMillis() + + (unavailableSeconds * 1000L); + response.sendError + (HttpServletResponse.SC_SERVICE_UNAVAILABLE, + ex.getMessage()); + } catch (ServletException | IllegalStateException ex) { + if(options.getDevelopment()) { + throw handleJspException(ex); + } + throw ex; + } catch (IOException ex) { + if (options.getDevelopment()) { + throw new IOException(handleJspException(ex).getMessage(), ex); + } + throw ex; + } catch (Exception ex) { + if(options.getDevelopment()) { + throw handleJspException(ex); + } + throw new JasperException(ex); + } + } + + public void destroy() { + if (theServlet != null) { + try { + theServlet.destroy(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(Localizer.getMessage("jsp.error.servlet.destroy.failed"), t); + } + InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(config); + try { + instanceManager.destroyInstance(theServlet); + } catch (Exception e) { + Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); + ExceptionUtils.handleThrowable(t); + // Log any exception, since it can't be passed along + log.error(Localizer.getMessage("jsp.error.file.not.found", + e.getMessage()), t); + } + } + } + + /** + * @return Returns the lastModificationTest. + */ + public long getLastModificationTest() { + return lastModificationTest; + } + /** + * @param lastModificationTest The lastModificationTest to set. + */ + public void setLastModificationTest(long lastModificationTest) { + this.lastModificationTest = lastModificationTest; + } + + /** + * @return the lastUsageTime. + */ + public long getLastUsageTime() { + return lastUsageTime; + } + + /** + *

    Attempts to construct a JasperException that contains helpful information + * about what went wrong. Uses the JSP compiler system to translate the line + * number in the generated servlet that originated the exception to a line + * number in the JSP. Then constructs an exception containing that + * information, and a snippet of the JSP to help debugging. + * Please see https://bz.apache.org/bugzilla/show_bug.cgi?id=37062 and + * http://www.tfenne.com/jasper/ for more details. + *

    + * + * @param ex the exception that was the cause of the problem. + * @return a JasperException with more detailed information + */ + protected JasperException handleJspException(Exception ex) { + try { + Throwable realException = ex; + if (ex instanceof ServletException) { + realException = ((ServletException) ex).getRootCause(); + } + + // Find the first stack frame that represents code generated by + // Jasper + StackTraceElement[] frames = realException.getStackTrace(); + StackTraceElement jspFrame = null; + + String servletPackageName = ctxt.getBasePackageName(); + for (StackTraceElement frame : frames) { + if (frame.getClassName().startsWith(servletPackageName)) { + jspFrame = frame; + break; + } + } + + SmapStratum smap = null; + + if (jspFrame != null) { + smap = ctxt.getCompiler().getSmap(jspFrame.getClassName()); + } + + if (smap == null) { + // If we couldn't find a frame in the stack trace corresponding + // to the generated servlet class or we don't have a copy of the + // smap to hand, we can't really add anything + return new JasperException(ex); + } + + @SuppressWarnings("null") + int javaLineNumber = jspFrame.getLineNumber(); + SmapInput source = smap.getInputLineNumber(javaLineNumber); + + // If the line number is less than one we couldn't find out + // where in the JSP things went wrong + if (source.getLineNumber() < 1) { + throw new JasperException(ex); + } + + JavacErrorDetail detail = new JavacErrorDetail(jspFrame.getMethodName(), javaLineNumber, + source.getFileName(), source.getLineNumber(), null, ctxt); + + if (options.getDisplaySourceFragment()) { + return new JasperException(Localizer.getMessage + ("jsp.exception", detail.getJspFileName(), + "" + source.getLineNumber()) + System.lineSeparator() + + System.lineSeparator() + detail.getJspExtract() + + System.lineSeparator() + System.lineSeparator() + + "Stacktrace:", ex); + + } + + return new JasperException(Localizer.getMessage + ("jsp.exception", detail.getJspFileName(), + "" + source.getLineNumber()), ex); + } catch (Exception je) { + // If anything goes wrong, just revert to the original behaviour + if (ex instanceof JasperException) { + return (JasperException) ex; + } + return new JasperException(ex); + } + } +} diff --git a/java/org/apache/jasper/servlet/TldPreScanned.java b/java/org/apache/jasper/servlet/TldPreScanned.java new file mode 100644 index 0000000..84de9c4 --- /dev/null +++ b/java/org/apache/jasper/servlet/TldPreScanned.java @@ -0,0 +1,57 @@ +/* + * 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.jasper.servlet; + +import java.net.URI; +import java.net.URL; +import java.util.Collection; + +import jakarta.servlet.ServletContext; + +import org.apache.jasper.compiler.Localizer; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; + +public class TldPreScanned extends TldScanner { + + private final Collection preScannedURLs; + + public TldPreScanned (ServletContext context, boolean namespaceAware, boolean validation, + boolean blockExternal, Collection preScannedTlds) { + super(context, namespaceAware, validation, blockExternal); + preScannedURLs = preScannedTlds; + } + + @Override + public void scanJars() { + for (URL url : preScannedURLs){ + String str = url.toExternalForm(); + int a = str.indexOf("jar:"); + int b = str.indexOf("!/"); + if (a >= 0 && b> 0) { + String fileUrl = str.substring(a + 4, b); + String path = str.substring(b + 2); + try { + parseTld(new TldResourcePath(new URI(fileUrl).toURL(), null, path)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } else { + throw new IllegalStateException(Localizer.getMessage("jsp.error.tld.url", str)); + } + } + } +} \ No newline at end of file diff --git a/java/org/apache/jasper/servlet/TldScanner.java b/java/org/apache/jasper/servlet/TldScanner.java new file mode 100644 index 0000000..f38340e --- /dev/null +++ b/java/org/apache/jasper/servlet/TldScanner.java @@ -0,0 +1,416 @@ +/* + * 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.jasper.servlet; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.descriptor.TaglibDescriptor; + +import org.apache.jasper.compiler.JarScannerFactory; +import org.apache.jasper.compiler.Localizer; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.Jar; +import org.apache.tomcat.JarScanType; +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.JarScannerCallback; +import org.apache.tomcat.util.descriptor.tld.TaglibXml; +import org.apache.tomcat.util.descriptor.tld.TldParser; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; +import org.xml.sax.SAXException; + +/** + * Scans for and loads Tag Library Descriptors contained in a web application. + */ +public class TldScanner { + private final Log log = LogFactory.getLog(TldScanner.class); // must not be static + private static final String MSG = "org.apache.jasper.servlet.TldScanner"; + private static final String TLD_EXT = ".tld"; + private static final String WEB_INF = "/WEB-INF/"; + private final ServletContext context; + private final TldParser tldParser; + private final Map uriTldResourcePathMap = new HashMap<>(); + private final Map tldResourcePathTaglibXmlMap = new HashMap<>(); + private final List listeners = new ArrayList<>(); + + /** + * Initialise with the application's ServletContext. + * + * @param context the application's servletContext + * @param namespaceAware should the XML parser used to parse TLD files be + * configured to be name space aware + * @param validation should the XML parser used to parse TLD files be + * configured to use validation + * @param blockExternal should the XML parser used to parse TLD files be + * configured to be block references to external + * entities + */ + public TldScanner(ServletContext context, + boolean namespaceAware, + boolean validation, + boolean blockExternal) { + this.context = context; + + this.tldParser = new TldParser(namespaceAware, validation, blockExternal); + } + + /** + * Scan for TLDs in all places defined by the specification: + *
      + *
    1. Tag libraries defined by the platform
    2. + *
    3. Entries from <jsp-config> in web.xml
    4. + *
    5. A resources under /WEB-INF
    6. + *
    7. In jar files from /WEB-INF/lib
    8. + *
    9. Additional entries from the container
    10. + *
    + * + * @throws IOException if there was a problem scanning for or loading a TLD + * @throws SAXException if there was a problem parsing a TLD + */ + public void scan() throws IOException, SAXException { + scanPlatform(); + scanJspConfig(); + scanResourcePaths(WEB_INF); + scanJars(); + } + + /** + * Returns the map of URI to TldResourcePath built by this scanner. + * + * @return the map of URI to TldResourcePath + */ + public Map getUriTldResourcePathMap() { + return uriTldResourcePathMap; + } + + /** + * Returns the map of TldResourcePath to parsed XML files built by this + * scanner. + * + * @return the map of TldResourcePath to parsed XML files + */ + public Map getTldResourcePathTaglibXmlMap() { + return tldResourcePathTaglibXmlMap; + } + + /** + * Returns a list of all listeners declared by scanned TLDs. + * + * @return a list of listener class names + */ + public List getListeners() { + return listeners; + } + + /** + * Set the class loader used by the digester to create objects as a result + * of this scan. Normally this only needs to be set when using JspC. + * + * @param classLoader Class loader to use when creating new objects while + * parsing TLDs + */ + public void setClassLoader(ClassLoader classLoader) { + tldParser.setClassLoader(classLoader); + } + + /** + * Scan for TLDs required by the platform specification. + */ + protected void scanPlatform() { + } + + /** + * Scan for TLDs defined in <jsp-config>. + * @throws IOException Error reading resources + * @throws SAXException XML parsing error + */ + protected void scanJspConfig() throws IOException, SAXException { + JspConfigDescriptor jspConfigDescriptor = context.getJspConfigDescriptor(); + if (jspConfigDescriptor == null) { + return; + } + + Collection descriptors = jspConfigDescriptor.getTaglibs(); + for (TaglibDescriptor descriptor : descriptors) { + String taglibURI = descriptor.getTaglibURI(); + String resourcePath = descriptor.getTaglibLocation(); + // Note: Whilst the Servlet 2.4 DTD implies that the location must + // be a context-relative path starting with '/', JSP.7.3.6.1 states + // explicitly how paths that do not start with '/' should be + // handled. + if (!resourcePath.startsWith("/")) { + resourcePath = WEB_INF + resourcePath; + } + if (uriTldResourcePathMap.containsKey(taglibURI)) { + log.warn(Localizer.getMessage(MSG + ".webxmlSkip", + resourcePath, + taglibURI)); + continue; + } + + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage(MSG + ".webxmlAdd", + resourcePath, + taglibURI)); + } + + URL url = context.getResource(resourcePath); + if (url != null) { + TldResourcePath tldResourcePath; + if (resourcePath.endsWith(".jar")) { + // if the path points to a jar file, the TLD is presumed to be + // inside at META-INF/taglib.tld + tldResourcePath = new TldResourcePath(url, resourcePath, "META-INF/taglib.tld"); + } else { + tldResourcePath = new TldResourcePath(url, resourcePath); + } + // parse TLD but store using the URI supplied in the descriptor + TaglibXml tld = tldParser.parse(tldResourcePath); + uriTldResourcePathMap.put(taglibURI, tldResourcePath); + tldResourcePathTaglibXmlMap.put(tldResourcePath, tld); + if (tld.getListeners() != null) { + listeners.addAll(tld.getListeners()); + } + } else { + log.warn(Localizer.getMessage(MSG + ".webxmlFailPathDoesNotExist", + resourcePath, + taglibURI)); + continue; + } + } + } + + /** + * Scan web application resources for TLDs, recursively. + * + * @param startPath the directory resource to scan + * @throws IOException if there was a problem scanning for or loading a TLD + * @throws SAXException if there was a problem parsing a TLD + */ + protected void scanResourcePaths(String startPath) + throws IOException, SAXException { + + boolean found = false; + Set dirList = context.getResourcePaths(startPath); + if (dirList != null) { + for (String path : dirList) { + if (path.startsWith("/WEB-INF/classes/")) { + // Skip: JSP.7.3.1 + } else if (path.startsWith("/WEB-INF/lib/")) { + // Skip: JSP.7.3.1 + } else if (path.endsWith("/")) { + scanResourcePaths(path); + } else if (path.startsWith("/WEB-INF/tags/")) { + // JSP 7.3.1: in /WEB-INF/tags only consider implicit.tld + if (path.endsWith("/implicit.tld")) { + found = true; + parseTld(path); + } + } else if (path.endsWith(TLD_EXT)) { + found = true; + parseTld(path); + } + } + } + if (found) { + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.tldCache.tldInResourcePath", startPath)); + } + } else { + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.tldCache.noTldInResourcePath", startPath)); + } + } + } + + /** + * Scan for TLDs in JARs in /WEB-INF/lib. + */ + public void scanJars() { + JarScanner scanner = JarScannerFactory.getJarScanner(context); + TldScannerCallback callback = new TldScannerCallback(); + scanner.scan(JarScanType.TLD, context, callback); + if (callback.scanFoundNoTLDs()) { + log.info(Localizer.getMessage("jsp.tldCache.noTldSummary")); + } + } + + protected void parseTld(String resourcePath) throws IOException, SAXException { + TldResourcePath tldResourcePath = + new TldResourcePath(context.getResource(resourcePath), resourcePath); + parseTld(tldResourcePath); + } + + protected void parseTld(TldResourcePath path) throws IOException, SAXException { + TaglibXml tld = tldParser.parse(path); + String uri = tld.getUri(); + if (uri != null) { + if (!uriTldResourcePathMap.containsKey(uri)) { + uriTldResourcePathMap.put(uri, path); + } + } + + if (tldResourcePathTaglibXmlMap.containsKey(path)) { + // TLD has already been parsed as a result of processing web.xml + return; + } + + tldResourcePathTaglibXmlMap.put(path, tld); + if (tld.getListeners() != null) { + listeners.addAll(tld.getListeners()); + } + } + + class TldScannerCallback implements JarScannerCallback { + private boolean foundJarWithoutTld = false; + private boolean foundFileWithoutTld = false; + + + @Override + public void scan(Jar jar, String webappPath, boolean isWebapp) throws IOException { + boolean found = false; + URL jarFileUrl = jar.getJarFileURL(); + jar.nextEntry(); + for (String entryName = jar.getEntryName(); + entryName != null; + jar.nextEntry(), entryName = jar.getEntryName()) { + if (!(entryName.startsWith("META-INF/") && + entryName.endsWith(TLD_EXT))) { + continue; + } + found = true; + TldResourcePath tldResourcePath = + new TldResourcePath(jarFileUrl, webappPath, entryName); + try { + parseTld(tldResourcePath); + } catch (SAXException e) { + throw new IOException(e); + } + } + if (found) { + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.tldCache.tldInJar", jarFileUrl.toString())); + } + } else { + foundJarWithoutTld = true; + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage( + "jsp.tldCache.noTldInJar", jarFileUrl.toString())); + } + } + } + + @Override + public void scan(File file, final String webappPath, boolean isWebapp) + throws IOException { + File metaInf = new File(file, "META-INF"); + if (!metaInf.isDirectory()) { + return; + } + foundFileWithoutTld = false; + final Path filePath = file.toPath(); + Files.walkFileTree(metaInf.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) + throws IOException { + Path fileName = file.getFileName(); + if (fileName == null || !fileName.toString().toLowerCase( + Locale.ENGLISH).endsWith(TLD_EXT)) { + return FileVisitResult.CONTINUE; + } + + foundFileWithoutTld = true; + String resourcePath; + if (webappPath == null) { + resourcePath = null; + } else { + String subPath = file.subpath( + filePath.getNameCount(), file.getNameCount()).toString(); + if ('/' != File.separatorChar) { + subPath = subPath.replace(File.separatorChar, '/'); + } + resourcePath = webappPath + "/" + subPath; + } + + try { + URL url = file.toUri().toURL(); + TldResourcePath path = new TldResourcePath(url, resourcePath); + parseTld(path); + } catch (SAXException e) { + throw new IOException(e); + } + return FileVisitResult.CONTINUE; + } + }); + if (foundFileWithoutTld) { + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.tldCache.tldInDir", + file.getAbsolutePath())); + } + } else { + if (log.isTraceEnabled()) { + log.trace(Localizer.getMessage("jsp.tldCache.noTldInDir", + file.getAbsolutePath())); + } + } + } + + @Override + public void scanWebInfClasses() throws IOException { + // This is used when scanAllDirectories is enabled and one or more + // JARs have been unpacked into WEB-INF/classes as happens with some + // IDEs. + + Set paths = context.getResourcePaths(WEB_INF + "classes/META-INF"); + if (paths == null) { + return; + } + + for (String path : paths) { + if (path.endsWith(TLD_EXT)) { + try { + parseTld(path); + } catch (SAXException e) { + throw new IOException(e); + } + } + } + } + + + boolean scanFoundNoTLDs() { + return foundJarWithoutTld; + } + } +} diff --git a/java/org/apache/jasper/servlet/mbeans-descriptors.xml b/java/org/apache/jasper/servlet/mbeans-descriptors.xml new file mode 100644 index 0000000..97132ee --- /dev/null +++ b/java/org/apache/jasper/servlet/mbeans-descriptors.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/jasper/tagplugins/jstl/Util.java b/java/org/apache/jasper/tagplugins/jstl/Util.java new file mode 100644 index 0000000..da56911 --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/Util.java @@ -0,0 +1,360 @@ +/* + * 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.jasper.tagplugins.jstl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.Locale; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.PageContext; + +import org.apache.jasper.compiler.Localizer; + +/** + * Util contains some often used consts, static methods and embedded class + * to support the JSTL tag plugin. + */ + +public class Util { + + private static final String VALID_SCHEME_CHAR = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-"; + + public static final String DEFAULT_ENCODING = + "ISO-8859-1"; + + private static final int HIGHEST_SPECIAL = '>'; + + private static final char[][] specialCharactersRepresentation = + new char[HIGHEST_SPECIAL + 1][]; + + static { + specialCharactersRepresentation['&'] = "&".toCharArray(); + specialCharactersRepresentation['<'] = "<".toCharArray(); + specialCharactersRepresentation['>'] = ">".toCharArray(); + specialCharactersRepresentation['"'] = """.toCharArray(); + specialCharactersRepresentation['\''] = "'".toCharArray(); + } + + /** + * Converts the given string description of a scope to the corresponding + * PageContext constant. + * + * The validity of the given scope has already been checked by the + * appropriate TLV. + * + * @param scope String description of scope + * + * @return PageContext constant corresponding to given scope description + * + * taken from org.apache.taglibs.standard.tag.common.core.Util + */ + public static int getScope(String scope){ + int ret = PageContext.PAGE_SCOPE; + + if("request".equalsIgnoreCase(scope)){ + ret = PageContext.REQUEST_SCOPE; + }else if("session".equalsIgnoreCase(scope)){ + ret = PageContext.SESSION_SCOPE; + }else if("application".equalsIgnoreCase(scope)){ + ret = PageContext.APPLICATION_SCOPE; + } + + return ret; + } + + /** + * Returns true if our current URL is absolute, + * false otherwise. + * taken from org.apache.taglibs.standard.tag.common.core.ImportSupport + * @param url The URL + * @return true if the URL is absolute + */ + public static boolean isAbsoluteUrl(String url){ + if(url == null){ + return false; + } + + int colonPos = url.indexOf(':'); + if(colonPos == -1){ + return false; + } + + for(int i=0;i and . + + ctxt.generateBody(); + // See comments in When.java for the reason "}" is generated here. + ctxt.generateJavaSource("}"); + } +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/ForEach.java b/java/org/apache/jasper/tagplugins/jstl/core/ForEach.java new file mode 100644 index 0000000..c62283a --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/ForEach.java @@ -0,0 +1,349 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; + +public final class ForEach implements TagPlugin { + + private boolean hasVar, hasBegin, hasEnd, hasStep; + + @Override + public void doTag(TagPluginContext ctxt) { + + String index = null; + + boolean hasVarStatus = ctxt.isAttributeSpecified("varStatus"); + if (hasVarStatus) { + ctxt.dontUseTagPlugin(); + return; + } + + hasVar = ctxt.isAttributeSpecified("var"); + hasBegin = ctxt.isAttributeSpecified("begin"); + hasEnd = ctxt.isAttributeSpecified("end"); + hasStep = ctxt.isAttributeSpecified("step"); + + boolean hasItems = ctxt.isAttributeSpecified("items"); + if (hasItems) { + doCollection(ctxt); + return; + } + + // We must have a begin and end attributes + index = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("for (int " + index + " = "); + ctxt.generateAttribute("begin"); + ctxt.generateJavaSource("; " + index + " <= "); + ctxt.generateAttribute("end"); + if (hasStep) { + ctxt.generateJavaSource("; " + index + "+="); + ctxt.generateAttribute("step"); + ctxt.generateJavaSource(") {"); + } else { + ctxt.generateJavaSource("; " + index + "++) {"); + } + + // If var is specified and the body contains an EL, then sync + // the var attribute + if (hasVar /* && ctxt.hasEL() */) { + ctxt.generateJavaSource("_jspx_page_context.setAttribute("); + ctxt.generateAttribute("var"); + ctxt.generateJavaSource(", String.valueOf(" + index + "));"); + } + ctxt.generateBody(); + ctxt.generateJavaSource("}"); + } + + /** + * Generate codes for Collections + * The pseudo code is: + */ + private void doCollection(TagPluginContext ctxt) { + + ctxt.generateImport("java.util.*"); + generateIterators(ctxt); + + String itemsV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("Object " + itemsV + "= "); + ctxt.generateAttribute("items"); + ctxt.generateJavaSource(";"); + + String indexV=null, beginV=null, endV=null, stepV=null; + if (hasBegin) { + beginV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("int " + beginV + " = "); + ctxt.generateAttribute("begin"); + ctxt.generateJavaSource(";"); + } + if (hasEnd) { + indexV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("int " + indexV + " = 0;"); + endV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("int " + endV + " = "); + ctxt.generateAttribute("end"); + ctxt.generateJavaSource(";"); + } + if (hasStep) { + stepV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("int " + stepV + " = "); + ctxt.generateAttribute("step"); + ctxt.generateJavaSource(";"); + } + + String iterV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("Iterator " + iterV + " = null;"); + // Object[] + ctxt.generateJavaSource("if (" + itemsV + " instanceof Object[])"); + ctxt.generateJavaSource(iterV + "=toIterator((Object[])" + itemsV + ");"); + // boolean[] + ctxt.generateJavaSource("else if (" + itemsV + " instanceof boolean[])"); + ctxt.generateJavaSource(iterV + "=toIterator((boolean[])" + itemsV + ");"); + // byte[] + ctxt.generateJavaSource("else if (" + itemsV + " instanceof byte[])"); + ctxt.generateJavaSource(iterV + "=toIterator((byte[])" + itemsV + ");"); + // char[] + ctxt.generateJavaSource("else if (" + itemsV + " instanceof char[])"); + ctxt.generateJavaSource(iterV + "=toIterator((char[])" + itemsV + ");"); + // short[] + ctxt.generateJavaSource("else if (" + itemsV + " instanceof short[])"); + ctxt.generateJavaSource(iterV + "=toIterator((short[])" + itemsV + ");"); + // int[] + ctxt.generateJavaSource("else if (" + itemsV + " instanceof int[])"); + ctxt.generateJavaSource(iterV + "=toIterator((int[])" + itemsV + ");"); + // long[] + ctxt.generateJavaSource("else if (" + itemsV + " instanceof long[])"); + ctxt.generateJavaSource(iterV + "=toIterator((long[])" + itemsV + ");"); + // float[] + ctxt.generateJavaSource("else if (" + itemsV + " instanceof float[])"); + ctxt.generateJavaSource(iterV + "=toIterator((float[])" + itemsV + ");"); + // double[] + ctxt.generateJavaSource("else if (" + itemsV + " instanceof double[])"); + ctxt.generateJavaSource(iterV + "=toIterator((double[])" + itemsV + ");"); + + // Collection + ctxt.generateJavaSource("else if (" + itemsV + " instanceof Collection)"); + ctxt.generateJavaSource(iterV + "=((Collection)" + itemsV + ").iterator();"); + + // Iterator + ctxt.generateJavaSource("else if (" + itemsV + " instanceof Iterator)"); + ctxt.generateJavaSource(iterV + "=(Iterator)" + itemsV + ";"); + + // Enumeration + ctxt.generateJavaSource("else if (" + itemsV + " instanceof Enumeration)"); + ctxt.generateJavaSource(iterV + "=toIterator((Enumeration)" + itemsV + ");"); + + // Map + ctxt.generateJavaSource("else if (" + itemsV + " instanceof Map)"); + ctxt.generateJavaSource(iterV + "=((Map)" + itemsV + ").entrySet().iterator();"); + + // String + ctxt.generateJavaSource("else if (" + itemsV + " instanceof String)"); + ctxt.generateJavaSource(iterV + "=toIterator(new StringTokenizer((String)" + itemsV + ", \",\"));"); + + // Not null + ctxt.generateJavaSource("if (" + iterV + " != null) {"); + + if (hasBegin) { + String tV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("for (int " + tV + "=" + beginV + ";" + + tV + ">0 && " + iterV + ".hasNext(); " + + tV + "--)"); + ctxt.generateJavaSource(iterV + ".next();"); + } + + ctxt.generateJavaSource("while (" + iterV + ".hasNext()){"); + if (hasVar) { + ctxt.generateJavaSource("_jspx_page_context.setAttribute("); + ctxt.generateAttribute("var"); + ctxt.generateJavaSource(", " + iterV + ".next());"); + } + + ctxt.generateBody(); + + if (hasStep) { + String tV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("for (int " + tV + "=" + stepV + "-1;" + + tV + ">0 && " + iterV + ".hasNext(); " + + tV + "--)"); + ctxt.generateJavaSource(iterV + ".next();"); + } + if (hasEnd) { + if (hasStep) { + ctxt.generateJavaSource(indexV + "+=" + stepV + ";"); + } else { + ctxt.generateJavaSource(indexV + "++;"); + } + if (hasBegin) { + ctxt.generateJavaSource("if(" + beginV + "+" + indexV + + ">"+ endV + ")"); + } else { + ctxt.generateJavaSource("if(" + indexV + ">" + endV + ")"); + } + ctxt.generateJavaSource("break;"); + } + ctxt.generateJavaSource("}"); // while + ctxt.generateJavaSource("}"); // Not Null + } + + /** + * Generate iterators for data types supported in items + */ + private void generateIterators(TagPluginContext ctxt) { + + // Object[] + ctxt.generateDeclaration("ObjectArrayIterator", + "private Iterator toIterator(final Object[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return a[index++];}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // boolean[] + ctxt.generateDeclaration("booleanArrayIterator", + "private Iterator toIterator(final boolean[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return Boolean.valueOf(a[index++]);}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // byte[] + ctxt.generateDeclaration("byteArrayIterator", + "private Iterator toIterator(final byte[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return Byte.valueOf(a[index++]);}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // char[] + ctxt.generateDeclaration("charArrayIterator", + "private Iterator toIterator(final char[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return Character.valueOf(a[index++]);}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // short[] + ctxt.generateDeclaration("shortArrayIterator", + "private Iterator toIterator(final short[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return Short.valueOf(a[index++]);}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // int[] + ctxt.generateDeclaration("intArrayIterator", + "private Iterator toIterator(final int[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return Integer.valueOf(a[index++]);}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // long[] + ctxt.generateDeclaration("longArrayIterator", + "private Iterator toIterator(final long[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return Long.valueOf(a[index++]);}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // float[] + ctxt.generateDeclaration("floatArrayIterator", + "private Iterator toIterator(final float[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return Float.valueOf(a[index++]);}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // double[] + ctxt.generateDeclaration("doubleArrayIterator", + "private Iterator toIterator(final double[] a){\n" + + " return (new Iterator() {\n" + + " int index=0;\n" + + " public boolean hasNext() {\n" + + " return index < a.length;}\n" + + " public Object next() {\n" + + " return Double.valueOf(a[index++]);}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + // Enumeration + ctxt.generateDeclaration("enumIterator", + "private Iterator toIterator(final Enumeration e){\n" + + " return (new Iterator() {\n" + + " public boolean hasNext() {\n" + + " return e.hasMoreElements();}\n" + + " public Object next() {\n" + + " return e.nextElement();}\n" + + " public void remove() {}\n" + + " });\n" + + "}" + ); + + } +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/ForTokens.java b/java/org/apache/jasper/tagplugins/jstl/core/ForTokens.java new file mode 100644 index 0000000..c4e1cab --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/ForTokens.java @@ -0,0 +1,118 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; + +public class ForTokens implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + boolean hasVar, hasVarStatus, hasBegin, hasEnd, hasStep; + + //init the flags + hasVar = ctxt.isAttributeSpecified("var"); + hasVarStatus = ctxt.isAttributeSpecified("varStatus"); + hasBegin = ctxt.isAttributeSpecified("begin"); + hasEnd = ctxt.isAttributeSpecified("end"); + hasStep = ctxt.isAttributeSpecified("step"); + + if(hasVarStatus){ + ctxt.dontUseTagPlugin(); + return; + } + + //define all the temp variables' names + String itemsName = ctxt.getTemporaryVariableName(); + String delimsName = ctxt.getTemporaryVariableName(); + String stName = ctxt.getTemporaryVariableName(); + String beginName = ctxt.getTemporaryVariableName(); + String endName = ctxt.getTemporaryVariableName(); + String stepName = ctxt.getTemporaryVariableName(); + String index = ctxt.getTemporaryVariableName(); + String temp = ctxt.getTemporaryVariableName(); + String tokensCountName = ctxt.getTemporaryVariableName(); + + //get the value of the "items" attribute + ctxt.generateJavaSource("String " + itemsName + " = (String)"); + ctxt.generateAttribute("items"); + ctxt.generateJavaSource(";"); + + //get the value of the "delim" attribute + ctxt.generateJavaSource("String " + delimsName + " = (String)"); + ctxt.generateAttribute("delims"); + ctxt.generateJavaSource(";"); + + //new a StringTokenizer Object according to the "items" and the "delim" + ctxt.generateJavaSource("java.util.StringTokenizer " + stName + " = " + + "new java.util.StringTokenizer(" + itemsName + ", " + delimsName + ");"); + + //if "begin" specified, move the token to the "begin" place + //and record the begin index. default begin place is 0. + ctxt.generateJavaSource("int " + tokensCountName + " = " + stName + ".countTokens();"); + if(hasBegin){ + ctxt.generateJavaSource("int " + beginName + " = " ); + ctxt.generateAttribute("begin"); + ctxt.generateJavaSource(";"); + ctxt.generateJavaSource("for(int " + index + " = 0; " + index + " < " + beginName + " && " + stName + ".hasMoreTokens(); " + index + "++, " + stName + ".nextToken()){}"); + }else{ + ctxt.generateJavaSource("int " + beginName + " = 0;"); + } + + //when "end" is specified, if the "end" is more than the last index, + //record the end place as the last index, otherwise, record it as "end"; + //default end place is the last index + if(hasEnd){ + ctxt.generateJavaSource("int " + endName + " = 0;" ); + ctxt.generateJavaSource("if((" + tokensCountName + " - 1) < "); + ctxt.generateAttribute("end"); + ctxt.generateJavaSource("){"); + ctxt.generateJavaSource(" " + endName + " = " + tokensCountName + " - 1;"); + ctxt.generateJavaSource("}else{"); + ctxt.generateJavaSource(" " + endName + " = "); + ctxt.generateAttribute("end"); + ctxt.generateJavaSource(";}"); + }else{ + ctxt.generateJavaSource("int " + endName + " = " + tokensCountName + " - 1;"); + } + + //get the step value from "step" if specified. + //default step value is 1. + if(hasStep){ + ctxt.generateJavaSource("int " + stepName + " = " ); + ctxt.generateAttribute("step"); + ctxt.generateJavaSource(";"); + }else{ + ctxt.generateJavaSource("int " + stepName + " = 1;"); + } + + //the loop + ctxt.generateJavaSource("for(int " + index + " = " + beginName + "; " + index + " <= " + endName + "; " + index + "++){"); + ctxt.generateJavaSource(" String " + temp + " = " + stName + ".nextToken();"); + ctxt.generateJavaSource(" if(((" + index + " - " + beginName + ") % " + stepName + ") == 0){"); + //if var specified, put the current token into the attribute "var" defines. + if(hasVar){ + String strVar = ctxt.getConstantAttribute("var"); + ctxt.generateJavaSource(" pageContext.setAttribute(\"" + strVar + "\", " + temp + ");"); + } + ctxt.generateBody(); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource("}"); + } + +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/If.java b/java/org/apache/jasper/tagplugins/jstl/core/If.java new file mode 100644 index 0000000..82ef2fe --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/If.java @@ -0,0 +1,50 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; + +public final class If implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + String condV = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource("boolean " + condV + "="); + ctxt.generateAttribute("test"); + ctxt.generateJavaSource(";"); + if (ctxt.isAttributeSpecified("var")) { + String scope = "PageContext.PAGE_SCOPE"; + if (ctxt.isAttributeSpecified("scope")) { + String scopeStr = ctxt.getConstantAttribute("scope"); + if ("request".equals(scopeStr)) { + scope = "PageContext.REQUEST_SCOPE"; + } else if ("session".equals(scopeStr)) { + scope = "PageContext.SESSION_SCOPE"; + } else if ("application".equals(scopeStr)) { + scope = "PageContext.APPLICATION_SCOPE"; + } + } + ctxt.generateJavaSource("_jspx_page_context.setAttribute("); + ctxt.generateAttribute("var"); + ctxt.generateJavaSource(", Boolean.valueOf(" + condV + ")," + scope + ");"); + } + ctxt.generateJavaSource("if (" + condV + "){"); + ctxt.generateBody(); + ctxt.generateJavaSource("}"); + } +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Import.java b/java/org/apache/jasper/tagplugins/jstl/core/Import.java new file mode 100644 index 0000000..aea45c0 --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/Import.java @@ -0,0 +1,380 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; +import org.apache.jasper.tagplugins.jstl.Util; + +public class Import implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + boolean hasContext, hasVar, hasScope, hasVarReader, hasCharEncoding; + + //flags + hasContext = ctxt.isAttributeSpecified("context"); + hasVar = ctxt.isAttributeSpecified("var"); + hasScope = ctxt.isAttributeSpecified("scope"); + hasVarReader = ctxt.isAttributeSpecified("varReader"); + hasCharEncoding = ctxt.isAttributeSpecified("charEncoding"); + + //variables' names + String urlName = ctxt.getTemporaryVariableName(); + String contextName = ctxt.getTemporaryVariableName(); + String iauName = ctxt.getTemporaryVariableName(); // is absolute url + String urlObjName = ctxt.getTemporaryVariableName(); //URL object + String ucName = ctxt.getTemporaryVariableName(); //URLConnection + String inputStreamName = ctxt.getTemporaryVariableName(); + String tempReaderName = ctxt.getTemporaryVariableName(); + String tempReaderName2 = ctxt.getTemporaryVariableName(); + String charSetName = ctxt.getTemporaryVariableName(); + String charEncodingName = ctxt.getTemporaryVariableName(); + String contentTypeName = ctxt.getTemporaryVariableName(); + String varReaderName = ctxt.getTemporaryVariableName(); + String servletContextName = ctxt.getTemporaryVariableName(); + String servletPathName = ctxt.getTemporaryVariableName(); + String requestDispatcherName = ctxt.getTemporaryVariableName(); + String irwName = ctxt.getTemporaryVariableName(); //ImportResponseWrapper name + String brName = ctxt.getTemporaryVariableName(); //BufferedReader name + String sbName = ctxt.getTemporaryVariableName(); //StringBuilder name + String tempStringName = ctxt.getTemporaryVariableName(); + + //is absolute url + ctxt.generateJavaSource("boolean " + iauName + ";"); + + //get the url value + ctxt.generateJavaSource("String " + urlName + " = "); + ctxt.generateAttribute("url"); + ctxt.generateJavaSource(";"); + + //validate the url + ctxt.generateJavaSource("if(" + urlName + " == null || " + urlName + ".equals(\"\")){"); + ctxt.generateJavaSource(" throw new JspTagException(\"The \\\"url\\\" attribute " + + "illegally evaluated to \\\"null\\\" or \\\"\\\" in <import>\");"); + ctxt.generateJavaSource("}"); + + //initialize the is_absolute_url + ctxt.generateJavaSource(iauName + " = " + + "org.apache.jasper.tagplugins.jstl.Util.isAbsoluteUrl(" + urlName + ");"); + + //validate the context + if(hasContext){ + + ctxt.generateJavaSource("String " + contextName + " = "); + ctxt.generateAttribute("context"); + ctxt.generateJavaSource(";"); + + ctxt.generateJavaSource("if((!" + contextName + ".startsWith(\"/\")) " + + "|| (!" + urlName + ".startsWith(\"/\"))){"); + ctxt.generateJavaSource(" throw new JspTagException" + + "(\"In URL tags, when the \\\"context\\\" attribute is specified, " + + "values of both \\\"context\\\" and \\\"url\\\" must start with \\\"/\\\".\");"); + ctxt.generateJavaSource("}"); + + } + + //define charset + ctxt.generateJavaSource("String " + charSetName + " = null;"); + + //if the charEncoding attribute is specified + if(hasCharEncoding){ + + //initialize the charEncoding + ctxt.generateJavaSource("String " + charEncodingName + " = "); + ctxt.generateAttribute("charEncoding"); + ctxt.generateJavaSource(";"); + + //assign appropriate value to the charset + ctxt.generateJavaSource("if(null != " + charEncodingName + " " + + "&& !" + charEncodingName + ".equals(\"\")){"); + ctxt.generateJavaSource(" " + charSetName + " = " + + charEncodingName + ";"); + ctxt.generateJavaSource("}"); + } + + //reshape the url string + ctxt.generateJavaSource("if(!"+iauName+"){"); + ctxt.generateJavaSource(" if(!" + urlName + ".startsWith(\"/\")){"); + ctxt.generateJavaSource(" String " + servletPathName + " = " + + "((HttpServletRequest)pageContext.getRequest()).getServletPath();"); + ctxt.generateJavaSource(" " + urlName + " = " + + servletPathName + ".substring(0," + servletPathName + ".lastIndexOf('/')) + '/' + " + urlName + ";"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource("}"); + + //if the varReader attribute specified + if(hasVarReader){ + + //get the String value of varReader + ctxt.generateJavaSource("String " + varReaderName + " = "); + ctxt.generateAttribute("varReader"); + ctxt.generateJavaSource(";"); + + //if the url is absolute url + ctxt.generateJavaSource("if(" + iauName + "){"); + + //get the content of the target + ctxt.generateJavaSource(" java.net.URL " + urlObjName + " = new java.net.URL(" + urlName + ");"); + ctxt.generateJavaSource(" java.net.URLConnection " + ucName + " = " + + urlObjName + ".openConnection();"); + ctxt.generateJavaSource(" java.io.InputStream " + inputStreamName + " = " + + ucName + ".getInputStream();"); + + ctxt.generateJavaSource(" if(" + charSetName + " == null){"); + ctxt.generateJavaSource(" String " + contentTypeName + " = " + + ucName + ".getContentType();"); + ctxt.generateJavaSource(" if(null != " + contentTypeName + "){"); + ctxt.generateJavaSource(" " + charSetName + " = " + + "org.apache.jasper.tagplugins.jstl.Util.getContentTypeAttribute(" + contentTypeName + ", \"charset\");"); + ctxt.generateJavaSource(" if(" + charSetName + " == null) " + + charSetName + " = org.apache.jasper.tagplugins.jstl.Util.DEFAULT_ENCODING;"); + ctxt.generateJavaSource(" }else{"); + ctxt.generateJavaSource(" " + charSetName + " = org.apache.jasper.tagplugins.jstl.Util.DEFAULT_ENCODING;"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" }"); + + if(!hasCharEncoding){ + ctxt.generateJavaSource(" String " + contentTypeName + " = " + ucName + ".getContentType();"); + } + + //define the Reader + ctxt.generateJavaSource(" java.io.Reader " + tempReaderName + " = null;"); + + //initialize the Reader object + ctxt.generateJavaSource(" try{"); + ctxt.generateJavaSource(" " + tempReaderName + " = new java.io.InputStreamReader(" + inputStreamName + ", " + charSetName + ");"); + ctxt.generateJavaSource(" }catch(Exception ex){"); + ctxt.generateJavaSource(" " + tempReaderName + " = new java.io.InputStreamReader(" + inputStreamName + ", org.apache.jasper.tagplugins.jstl.Util.DEFAULT_ENCODING);"); + ctxt.generateJavaSource(" }"); + + //validate the response + ctxt.generateJavaSource(" if(" + ucName + " instanceof java.net.HttpURLConnection){"); + ctxt.generateJavaSource(" int status = ((java.net.HttpURLConnection) " + ucName + ").getResponseCode();"); + ctxt.generateJavaSource(" if(status < 200 || status > 299){"); + ctxt.generateJavaSource(" throw new JspTagException(status + \" \" + " + urlName + ");"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" }"); + + //set attribute in the page context scope + ctxt.generateJavaSource(" pageContext.setAttribute(" + varReaderName + ", " + tempReaderName + ");"); + + //if the url is relative + ctxt.generateJavaSource("}else{"); + + //if the url is relative, http request is needed + ctxt.generateJavaSource(" if (!(pageContext.getRequest() instanceof HttpServletRequest " + + "&& pageContext.getResponse() instanceof HttpServletResponse)){"); + ctxt.generateJavaSource(" throw new JspTagException(\"Relative <import> from non-HTTP request not allowed\");"); + ctxt.generateJavaSource(" }"); + + //get the servlet context of the context defined in the context attribute + ctxt.generateJavaSource(" ServletContext " + servletContextName + " = null;"); + if(hasContext){ + ctxt.generateJavaSource(" if(null != " + contextName + "){"); + ctxt.generateJavaSource(" " + servletContextName + " = pageContext.getServletContext().getContext(" + contextName + ");" ); + ctxt.generateJavaSource(" }else{"); + ctxt.generateJavaSource(" " + servletContextName + " = pageContext.getServletContext();"); + ctxt.generateJavaSource(" }"); + }else{ + ctxt.generateJavaSource(" " + servletContextName + " = pageContext.getServletContext();"); + } + + // + ctxt.generateJavaSource(" if(" + servletContextName + " == null){"); + if(hasContext){ + ctxt.generateJavaSource(" throw new JspTagException(\"Unable to get RequestDispatcher for Context: \\\" \"+" + contextName + "+\" \\\" and URL: \\\" \" +" + urlName + "+ \" \\\". Verify values and/or enable cross context access.\");"); + }else{ + ctxt.generateJavaSource(" throw new JspTagException(\"Unable to get RequestDispatcher for URL: \\\" \" +" + urlName + "+ \" \\\". Verify values and/or enable cross context access.\");"); + } + ctxt.generateJavaSource(" }"); + + //get the request dispatcher + ctxt.generateJavaSource(" RequestDispatcher " + requestDispatcherName + " = " + servletContextName + ".getRequestDispatcher(org.apache.jasper.tagplugins.jstl.Util.stripSession("+urlName+"));"); + ctxt.generateJavaSource(" if(" + requestDispatcherName + " == null) throw new JspTagException(org.apache.jasper.tagplugins.jstl.Util.stripSession("+urlName+"));"); + + //initialize a ImportResponseWrapper to include the resource + ctxt.generateJavaSource(" org.apache.jasper.tagplugins.jstl.Util.ImportResponseWrapper " + irwName + " = new org.apache.jasper.tagplugins.jstl.Util.ImportResponseWrapper((HttpServletResponse) pageContext.getResponse());"); + ctxt.generateJavaSource(" if(" + charSetName + " == null){"); + ctxt.generateJavaSource(" " + charSetName + " = org.apache.jasper.tagplugins.jstl.Util.DEFAULT_ENCODING;"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" " + irwName + ".setCharEncoding(" + charSetName + ");"); + ctxt.generateJavaSource(" try{"); + ctxt.generateJavaSource(" " + requestDispatcherName + ".include(pageContext.getRequest(), " + irwName + ");"); + ctxt.generateJavaSource(" }catch(java.io.IOException ex){"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" }catch(RuntimeException ex){"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" }catch(ServletException ex){"); + ctxt.generateJavaSource(" Throwable rc = ex.getRootCause();"); + ctxt.generateJavaSource(" if (rc == null)"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" else"); + ctxt.generateJavaSource(" throw new JspException(rc);"); + ctxt.generateJavaSource(" }"); + + //validate the response status + ctxt.generateJavaSource(" if(" + irwName + ".getStatus() < 200 || " + irwName + ".getStatus() > 299){"); + ctxt.generateJavaSource(" throw new JspTagException(" + irwName + ".getStatus()+\" \" + org.apache.jasper.tagplugins.jstl.Util.stripSession(" + urlName + "));"); + ctxt.generateJavaSource(" }"); + + //push in the page context + ctxt.generateJavaSource(" java.io.Reader " + tempReaderName + " = new java.io.StringReader(" + irwName + ".getString());"); + ctxt.generateJavaSource(" pageContext.setAttribute(" + varReaderName + ", " + tempReaderName + ");"); + + ctxt.generateJavaSource("}"); + + //execute the body action + ctxt.generateBody(); + + //close the reader + ctxt.generateJavaSource("java.io.Reader " + tempReaderName2 + " = (java.io.Reader)pageContext.getAttribute(" + varReaderName + ");"); + ctxt.generateJavaSource("if(" + tempReaderName2 + " != null) " + tempReaderName2 + ".close();"); + ctxt.generateJavaSource("pageContext.removeAttribute(" + varReaderName + ",1);"); + } + + //if the varReader is not specified + else{ + + ctxt.generateJavaSource("pageContext.setAttribute(\"url_without_param\"," + urlName + ");"); + ctxt.generateBody(); + ctxt.generateJavaSource(urlName + " = (String)pageContext.getAttribute(\"url_without_param\");"); + ctxt.generateJavaSource("pageContext.removeAttribute(\"url_without_param\");"); + String strScope = "page"; + if(hasScope){ + strScope = ctxt.getConstantAttribute("scope"); + } + int iScope = Util.getScope(strScope); + + ctxt.generateJavaSource("String " + tempStringName + " = null;"); + + ctxt.generateJavaSource("if(" + iauName + "){"); + + //get the content of the target + ctxt.generateJavaSource(" java.net.URL " + urlObjName + " = new java.net.URL(" + urlName + ");"); + ctxt.generateJavaSource(" java.net.URLConnection " + ucName + " = " + urlObjName + ".openConnection();"); + ctxt.generateJavaSource(" java.io.InputStream " + inputStreamName + " = " + ucName + ".getInputStream();"); + ctxt.generateJavaSource(" java.io.Reader " + tempReaderName + " = null;"); + + ctxt.generateJavaSource(" if(" + charSetName + " == null){"); + ctxt.generateJavaSource(" String " + contentTypeName + " = " + + ucName + ".getContentType();"); + ctxt.generateJavaSource(" if(null != " + contentTypeName + "){"); + ctxt.generateJavaSource(" " + charSetName + " = " + + "org.apache.jasper.tagplugins.jstl.Util.getContentTypeAttribute(" + contentTypeName + ", \"charset\");"); + ctxt.generateJavaSource(" if(" + charSetName + " == null) " + + charSetName + " = org.apache.jasper.tagplugins.jstl.Util.DEFAULT_ENCODING;"); + ctxt.generateJavaSource(" }else{"); + ctxt.generateJavaSource(" " + charSetName + " = org.apache.jasper.tagplugins.jstl.Util.DEFAULT_ENCODING;"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" }"); + + ctxt.generateJavaSource(" try{"); + ctxt.generateJavaSource(" " + tempReaderName + " = new java.io.InputStreamReader(" + inputStreamName + "," + charSetName + ");"); + ctxt.generateJavaSource(" }catch(Exception ex){"); + //ctxt.generateJavaSource(" throw new JspTagException(ex.toString());"); + ctxt.generateJavaSource(" " + tempReaderName + " = new java.io.InputStreamReader(" + inputStreamName + ",org.apache.jasper.tagplugins.jstl.Util.DEFAULT_ENCODING);"); + ctxt.generateJavaSource(" }"); + + //validate the response + ctxt.generateJavaSource(" if(" + ucName + " instanceof java.net.HttpURLConnection){"); + ctxt.generateJavaSource(" int status = ((java.net.HttpURLConnection) " + ucName + ").getResponseCode();"); + ctxt.generateJavaSource(" if(status < 200 || status > 299){"); + ctxt.generateJavaSource(" throw new JspTagException(status + \" \" + " + urlName + ");"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" }"); + + ctxt.generateJavaSource(" java.io.BufferedReader " + brName + " = new java.io.BufferedReader(" + tempReaderName + ");"); + ctxt.generateJavaSource(" StringBuilder " + sbName + " = new StringBuilder();"); + String index = ctxt.getTemporaryVariableName(); + ctxt.generateJavaSource(" int " + index + ";"); + ctxt.generateJavaSource(" while(("+index+" = "+brName+".read()) != -1) "+sbName+".append((char)"+index+");"); + ctxt.generateJavaSource(" " + tempStringName + " = " +sbName + ".toString();"); + + ctxt.generateJavaSource("}else{"); + + //if the url is relative, http request is needed. + ctxt.generateJavaSource(" if (!(pageContext.getRequest() instanceof HttpServletRequest " + + "&& pageContext.getResponse() instanceof HttpServletResponse)){"); + ctxt.generateJavaSource(" throw new JspTagException(\"Relative <import> from non-HTTP request not allowed\");"); + ctxt.generateJavaSource(" }"); + + //get the servlet context of the context defined in the context attribute + ctxt.generateJavaSource(" ServletContext " + servletContextName + " = null;"); + if(hasContext){ + ctxt.generateJavaSource(" if(null != " + contextName + "){"); + ctxt.generateJavaSource(" " + servletContextName + " = pageContext.getServletContext().getContext(" + contextName + ");" ); + ctxt.generateJavaSource(" }else{"); + ctxt.generateJavaSource(" " + servletContextName + " = pageContext.getServletContext();"); + ctxt.generateJavaSource(" }"); + }else{ + ctxt.generateJavaSource(" " + servletContextName + " = pageContext.getServletContext();"); + } + + // + ctxt.generateJavaSource(" if(" + servletContextName + " == null){"); + if(hasContext){ + ctxt.generateJavaSource(" throw new JspTagException(\"Unable to get RequestDispatcher for Context: \\\" \" +" + contextName + "+ \" \\\" and URL: \\\" \" +" + urlName + "+ \" \\\". Verify values and/or enable cross context access.\");"); + }else{ + ctxt.generateJavaSource(" throw new JspTagException(\"Unable to get RequestDispatcher for URL: \\\" \" +" + urlName + "+ \" \\\". Verify values and/or enable cross context access.\");"); + } + ctxt.generateJavaSource(" }"); + + //get the request dispatcher + ctxt.generateJavaSource(" RequestDispatcher " + requestDispatcherName + " = " + servletContextName + ".getRequestDispatcher(org.apache.jasper.tagplugins.jstl.Util.stripSession("+urlName+"));"); + ctxt.generateJavaSource(" if(" + requestDispatcherName + " == null) throw new JspTagException(org.apache.jasper.tagplugins.jstl.Util.stripSession("+urlName+"));"); + + //initialize a ImportResponseWrapper to include the resource + ctxt.generateJavaSource(" org.apache.jasper.tagplugins.jstl.Util.ImportResponseWrapper " + irwName + " = new org.apache.jasper.tagplugins.jstl.Util.ImportResponseWrapper((HttpServletResponse) pageContext.getResponse());"); + ctxt.generateJavaSource(" if(" + charSetName + " == null){"); + ctxt.generateJavaSource(" " + charSetName + " = org.apache.jasper.tagplugins.jstl.Util.DEFAULT_ENCODING;"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" " + irwName + ".setCharEncoding(" + charSetName + ");"); + ctxt.generateJavaSource(" try{"); + ctxt.generateJavaSource(" " + requestDispatcherName + ".include(pageContext.getRequest(), " + irwName + ");"); + ctxt.generateJavaSource(" }catch(java.io.IOException ex){"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" }catch(RuntimeException ex){"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" }catch(ServletException ex){"); + ctxt.generateJavaSource(" Throwable rc = ex.getRootCause();"); + ctxt.generateJavaSource(" if (rc == null)"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" else"); + ctxt.generateJavaSource(" throw new JspException(rc);"); + ctxt.generateJavaSource(" }"); + + //validate the response status + ctxt.generateJavaSource(" if(" + irwName + ".getStatus() < 200 || " + irwName + ".getStatus() > 299){"); + ctxt.generateJavaSource(" throw new JspTagException(" + irwName + ".getStatus()+\" \" + org.apache.jasper.tagplugins.jstl.Util.stripSession(" + urlName + "));"); + ctxt.generateJavaSource(" }"); + + ctxt.generateJavaSource(" " + tempStringName + " = " + irwName + ".getString();"); + + ctxt.generateJavaSource("}"); + + if(hasVar){ + String strVar = ctxt.getConstantAttribute("var"); + ctxt.generateJavaSource("pageContext.setAttribute(\""+strVar+"\"," + tempStringName + "," + iScope + ");"); + }else{ + ctxt.generateJavaSource("pageContext.getOut().print(" + tempStringName + ");"); + } + } + } + + +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Otherwise.java b/java/org/apache/jasper/tagplugins/jstl/core/Otherwise.java new file mode 100644 index 0000000..db7e4f9 --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/Otherwise.java @@ -0,0 +1,32 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; + +public final class Otherwise implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + + // See When.java for the reason whey "}" is need at the beginning and + // not at the end. + ctxt.generateJavaSource("} else {"); + ctxt.generateBody(); + } +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Out.java b/java/org/apache/jasper/tagplugins/jstl/core/Out.java new file mode 100644 index 0000000..ff53f1c --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/Out.java @@ -0,0 +1,127 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import java.io.IOException; +import java.io.Reader; + +import jakarta.servlet.jsp.JspWriter; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; +import org.apache.jasper.tagplugins.jstl.Util; + + +public final class Out implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + + //these two data member are to indicate + //whether the corresponding attribute is specified + boolean hasDefault=false, hasEscapeXml=false; + hasDefault = ctxt.isAttributeSpecified("default"); + hasEscapeXml = ctxt.isAttributeSpecified("escapeXml"); + + //strValName, strEscapeXmlName & strDefName are two variables' name + //standing for value, escapeXml and default attribute + String strObjectName = ctxt.getTemporaryVariableName(); + String strValName = ctxt.getTemporaryVariableName(); + String strDefName = ctxt.getTemporaryVariableName(); + String strEscapeXmlName = ctxt.getTemporaryVariableName(); + String strSkipBodyName = ctxt.getTemporaryVariableName(); + + //according to the tag file, the value attribute is mandatory. + ctxt.generateImport("java.io.Reader"); + ctxt.generateJavaSource("Object " + strObjectName + "="); + ctxt.generateAttribute("value"); + ctxt.generateJavaSource(";"); + ctxt.generateJavaSource("String " + strValName + "=null;"); + ctxt.generateJavaSource("if(!(" + strObjectName + + " instanceof Reader) && "+ strObjectName + " != null){"); + ctxt.generateJavaSource( + strValName + " = " + strObjectName + ".toString();"); + ctxt.generateJavaSource("}"); + + //initiate the strDefName with null. + //if the default has been specified, then assign the value to it; + ctxt.generateJavaSource("String " + strDefName + " = null;"); + if(hasDefault){ + ctxt.generateJavaSource("if("); + ctxt.generateAttribute("default"); + ctxt.generateJavaSource(" != null){"); + ctxt.generateJavaSource(strDefName + " = ("); + ctxt.generateAttribute("default"); + ctxt.generateJavaSource(").toString();"); + ctxt.generateJavaSource("}"); + } + + //initiate the strEscapeXmlName with true; + //if the escapeXml is specified, assign the value to it; + ctxt.generateJavaSource("boolean " + strEscapeXmlName + " = true;"); + if(hasEscapeXml){ + ctxt.generateJavaSource(strEscapeXmlName + " = "); + ctxt.generateAttribute("escapeXml"); + ctxt.generateJavaSource(";"); + } + + //main part. + ctxt.generateJavaSource( + "boolean " + strSkipBodyName + " = " + + "org.apache.jasper.tagplugins.jstl.core.Out.output(out, " + + strObjectName + ", " + strValName + ", " + strDefName + ", " + + strEscapeXmlName + ");"); + ctxt.generateJavaSource("if(!" + strSkipBodyName + ") {"); + ctxt.generateBody(); + ctxt.generateJavaSource("}"); + } + + public static boolean output(JspWriter out, Object input, String value, + String defaultValue, boolean escapeXml) throws IOException { + if (input instanceof Reader) { + char[] buffer = new char[8096]; + int read = 0; + while (read != -1) { + read = ((Reader) input).read(buffer); + if (read != -1) { + if (escapeXml) { + String escaped = Util.escapeXml(buffer, read); + if (escaped == null) { + out.write(buffer, 0, read); + } else { + out.print(escaped); + } + } else { + out.write(buffer, 0, read); + } + } + } + return true; + } else { + String v = value != null ? value : defaultValue; + if (v != null) { + if(escapeXml){ + v = Util.escapeXml(v); + } + out.write(v); + return true; + } else { + return false; + } + } + } +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Param.java b/java/org/apache/jasper/tagplugins/jstl/core/Param.java new file mode 100644 index 0000000..1496c5d --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/Param.java @@ -0,0 +1,76 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; + +public class Param implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + + //don't support the body content + + //define names of all the temp variables + String nameName = ctxt.getTemporaryVariableName(); + String valueName = ctxt.getTemporaryVariableName(); + String urlName = ctxt.getTemporaryVariableName(); + String encName = ctxt.getTemporaryVariableName(); + String index = ctxt.getTemporaryVariableName(); + + //if the param tag has no parents, throw a exception + TagPluginContext parent = ctxt.getParentContext(); + if(parent == null){ + ctxt.generateJavaSource(" throw new JspTagException" + + "(\"<param> outside <import> or <urlEncode>\");"); + return; + } + + //get the url string before adding this param + ctxt.generateJavaSource("String " + urlName + " = " + + "(String)pageContext.getAttribute(\"url_without_param\");"); + + //get the value of "name" + ctxt.generateJavaSource("String " + nameName + " = "); + ctxt.generateAttribute("name"); + ctxt.generateJavaSource(";"); + + //if the "name" is null then do nothing. + //else add such string "name=value" to the url. + //and the url should be encoded + ctxt.generateJavaSource("if(" + nameName + " != null && !" + nameName + ".equals(\"\")){"); + ctxt.generateJavaSource(" String " + valueName + " = "); + ctxt.generateAttribute("value"); + ctxt.generateJavaSource(";"); + ctxt.generateJavaSource(" if(" + valueName + " == null) " + valueName + " = \"\";"); + ctxt.generateJavaSource(" String " + encName + " = pageContext.getResponse().getCharacterEncoding();"); + ctxt.generateJavaSource(" " + nameName + " = java.net.URLEncoder.encode(" + nameName + ", " + encName + ");"); + ctxt.generateJavaSource(" " + valueName + " = java.net.URLEncoder.encode(" + valueName + ", " + encName + ");"); + ctxt.generateJavaSource(" int " + index + ";"); + ctxt.generateJavaSource(" " + index + " = " + urlName + ".indexOf(\'?\');"); + //if the current param is the first one, add a "?" ahead of it + //else add a "&" ahead of it + ctxt.generateJavaSource(" if(" + index + " == -1){"); + ctxt.generateJavaSource(" " + urlName + " = " + urlName + " + \"?\" + " + nameName + " + \"=\" + " + valueName + ";"); + ctxt.generateJavaSource(" }else{"); + ctxt.generateJavaSource(" " + urlName + " = " + urlName + " + \"&\" + " + nameName + " + \"=\" + " + valueName + ";"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" pageContext.setAttribute(\"url_without_param\"," + urlName + ");"); + ctxt.generateJavaSource("}"); + } +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Redirect.java b/java/org/apache/jasper/tagplugins/jstl/core/Redirect.java new file mode 100644 index 0000000..cd695ad --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/Redirect.java @@ -0,0 +1,82 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; + +public class Redirect implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + + //flag for the existence of the "context" + boolean hasContext = ctxt.isAttributeSpecified("context"); + + //names of the temp variables + String urlName = ctxt.getTemporaryVariableName(); + String contextName = ctxt.getTemporaryVariableName(); + String baseUrlName = ctxt.getTemporaryVariableName(); + String resultName = ctxt.getTemporaryVariableName(); + String responseName = ctxt.getTemporaryVariableName(); + + //get context + ctxt.generateJavaSource("String " + contextName + " = null;"); + if(hasContext){ + ctxt.generateJavaSource(contextName + " = "); + ctxt.generateAttribute("context"); + ctxt.generateJavaSource(";"); + } + + //get the url + ctxt.generateJavaSource("String " + urlName + " = "); + ctxt.generateAttribute("url"); + ctxt.generateJavaSource(";"); + + //get the raw url according to "url" and "context" + ctxt.generateJavaSource("String " + baseUrlName + " = " + + "org.apache.jasper.tagplugins.jstl.Util.resolveUrl(" + urlName + ", " + contextName + ", pageContext);"); + ctxt.generateJavaSource("pageContext.setAttribute" + + "(\"url_without_param\", " + baseUrlName + ");"); + + //add params + ctxt.generateBody(); + + ctxt.generateJavaSource("String " + resultName + " = " + + "(String)pageContext.getAttribute(\"url_without_param\");"); + ctxt.generateJavaSource("pageContext.removeAttribute" + + "(\"url_without_param\");"); + + //get the response object + ctxt.generateJavaSource("HttpServletResponse " + responseName + " = " + + "((HttpServletResponse) pageContext.getResponse());"); + + //if the url is relative, encode it + ctxt.generateJavaSource("if(!org.apache.jasper.tagplugins.jstl.Util.isAbsoluteUrl(" + resultName + ")){"); + ctxt.generateJavaSource(" " + resultName + " = " + + responseName + ".encodeRedirectURL(" + resultName + ");"); + ctxt.generateJavaSource("}"); + + //do redirect + ctxt.generateJavaSource("try{"); + ctxt.generateJavaSource(" " + responseName + ".sendRedirect(" + resultName + ");"); + ctxt.generateJavaSource("}catch(java.io.IOException ex){"); + ctxt.generateJavaSource(" throw new JspTagException(ex.toString(), ex);"); + ctxt.generateJavaSource("}"); + } + +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Remove.java b/java/org/apache/jasper/tagplugins/jstl/core/Remove.java new file mode 100644 index 0000000..1978fe0 --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/Remove.java @@ -0,0 +1,44 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; +import org.apache.jasper.tagplugins.jstl.Util; + +public class Remove implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + + //scope flag + boolean hasScope = ctxt.isAttributeSpecified("scope"); + + //the value of the "var" + String strVar = ctxt.getConstantAttribute("var"); + + //remove attribute from certain scope. + //default scope is "page". + if(hasScope){ + int iScope = Util.getScope(ctxt.getConstantAttribute("scope")); + ctxt.generateJavaSource("pageContext.removeAttribute(\"" + strVar + "\"," + iScope + ");"); + }else{ + ctxt.generateJavaSource("pageContext.removeAttribute(\"" + strVar + "\");"); + } + } + +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Set.java b/java/org/apache/jasper/tagplugins/jstl/core/Set.java new file mode 100644 index 0000000..81dbf79 --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/Set.java @@ -0,0 +1,178 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; +import org.apache.jasper.tagplugins.jstl.Util; + +public class Set implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + + //the flags to indicate whether the attributes have been specified + boolean hasValue = false, hasVar = false, hasScope = false, + hasTarget = false; + + //the scope name + String strScope; + //the id of the scope + int iScope; + + //initialize the flags + hasValue = ctxt.isAttributeSpecified("value"); + hasVar = ctxt.isAttributeSpecified("var"); + hasScope = ctxt.isAttributeSpecified("scope"); + hasTarget = ctxt.isAttributeSpecified("target"); + + //the temp variables name + String resultName = ctxt.getTemporaryVariableName(); + String targetName = ctxt.getTemporaryVariableName(); + String propertyName = ctxt.getTemporaryVariableName(); + + //initialize the "result" which will be assigned to the var or target.property + ctxt.generateJavaSource("Object " + resultName + " = null;"); + if(hasValue){ + ctxt.generateJavaSource(resultName + " = "); + ctxt.generateAttribute("value"); + ctxt.generateJavaSource(";"); + }else{ + ctxt.dontUseTagPlugin(); + return; + } + + //initialize the strScope + if(hasScope){ + strScope = ctxt.getConstantAttribute("scope"); + }else{ + strScope = "page"; + } + + //get the iScope according to the strScope + iScope = Util.getScope(strScope); + + String jspCtxt = null; + if (ctxt.isTagFile()) { + jspCtxt = "this.getJspContext()"; + } else { + jspCtxt = "_jspx_page_context"; + } + //if the attribute var has been specified then assign the result to the var; + if(hasVar){ + String strVar = ctxt.getConstantAttribute("var"); + ctxt.generateJavaSource("if(null != " + resultName + "){"); + ctxt.generateJavaSource(" " + jspCtxt + ".setAttribute(\"" + strVar + "\"," + resultName + "," + iScope + ");"); + ctxt.generateJavaSource("} else {"); + if(hasScope){ + ctxt.generateJavaSource(" " + jspCtxt + ".removeAttribute(\"" + strVar + "\"," + iScope + ");"); + }else{ + ctxt.generateJavaSource(" " + jspCtxt + ".removeAttribute(\"" + strVar + "\");"); + } + ctxt.generateJavaSource("}"); + + //else assign the result to the target.property + }else if(hasTarget){ + + //generate the temp variable name + String pdName = ctxt.getTemporaryVariableName(); + String successFlagName = ctxt.getTemporaryVariableName(); + String index = ctxt.getTemporaryVariableName(); + String methodName = ctxt.getTemporaryVariableName(); + + //initialize the property + ctxt.generateJavaSource("String " + propertyName + " = null;"); + ctxt.generateJavaSource("if("); + ctxt.generateAttribute("property"); + ctxt.generateJavaSource(" != null){"); + ctxt.generateJavaSource(" " + propertyName + " = ("); + ctxt.generateAttribute("property"); + ctxt.generateJavaSource(").toString();"); + ctxt.generateJavaSource("}"); + + //initialize the target + ctxt.generateJavaSource("Object " + targetName + " = "); + ctxt.generateAttribute("target"); + ctxt.generateJavaSource(";"); + + //the target is ok + ctxt.generateJavaSource("if(" + targetName + " != null){"); + + //if the target is a map, then put the result into the map with the key property + ctxt.generateJavaSource(" if(" + targetName + " instanceof java.util.Map){"); + ctxt.generateJavaSource(" if(null != " + resultName + "){"); + ctxt.generateJavaSource(" ((java.util.Map) " + targetName + ").put(" + propertyName + "," + resultName + ");"); + ctxt.generateJavaSource(" }else{"); + ctxt.generateJavaSource(" ((java.util.Map) " + targetName + ").remove(" + propertyName + ");"); + ctxt.generateJavaSource(" }"); + + //else assign the result to the target.property + ctxt.generateJavaSource(" }else{"); + ctxt.generateJavaSource(" try{"); + + //get all the property of the target + ctxt.generateJavaSource(" java.beans.PropertyDescriptor " + pdName + "[] = java.beans.Introspector.getBeanInfo(" + targetName + ".getClass()).getPropertyDescriptors();"); + + //the success flag is to imply whether the assign is successful + ctxt.generateJavaSource(" boolean " + successFlagName + " = false;"); + + //find the right property + ctxt.generateJavaSource(" for(int " + index + "=0;" + index + "<" + pdName + ".length;" + index + "++){"); + ctxt.generateJavaSource(" if(" + pdName + "[" + index + "].getName().equals(" + propertyName + ")){"); + + //get the "set" method; + ctxt.generateJavaSource(" java.lang.reflect.Method " + methodName + " = " + pdName + "[" + index + "].getWriteMethod();"); + ctxt.generateJavaSource(" if(null == " + methodName + "){"); + ctxt.generateJavaSource(" throw new JspException(\"No setter method in <set> for property \"+" + propertyName + ");"); + ctxt.generateJavaSource(" }"); + + //invoke the method through the reflection + ctxt.generateJavaSource(" if(" + resultName + " != null){"); + ctxt.generateJavaSource(" " + methodName + ".invoke(" + targetName + ", new Object[]{org.apache.el.lang.ELSupport.coerceToType(" + jspCtxt + ".getELContext(), " + resultName + ", " + methodName + ".getParameterTypes()[0])});"); + ctxt.generateJavaSource(" }else{"); + ctxt.generateJavaSource(" " + methodName + ".invoke(" + targetName + ", new Object[]{null});"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" " + successFlagName + " = true;"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" if(!" + successFlagName + "){"); + ctxt.generateJavaSource(" throw new JspException(\"Invalid property in <set>:\"+" + propertyName + ");"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" }"); + + //catch the el exception and throw it as a JspException + ctxt.generateJavaSource(" catch (IllegalAccessException ex) {"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" } catch (java.beans.IntrospectionException ex) {"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" } catch (java.lang.reflect.InvocationTargetException ex) {"); + ctxt.generateJavaSource(" if (ex.getCause() instanceof ThreadDeath) {"); + ctxt.generateJavaSource(" throw (ThreadDeath) ex.getCause();"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" if (ex.getCause() instanceof VirtualMachineError) {"); + ctxt.generateJavaSource(" throw (VirtualMachineError) ex.getCause();"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" throw new JspException(ex);"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource(" }"); + ctxt.generateJavaSource("}else{"); + ctxt.generateJavaSource(" throw new JspException();"); + ctxt.generateJavaSource("}"); + } + } + +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Url.java b/java/org/apache/jasper/tagplugins/jstl/core/Url.java new file mode 100644 index 0000000..6df77f6 --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/Url.java @@ -0,0 +1,100 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; +import org.apache.jasper.tagplugins.jstl.Util; + +public class Url implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + + //flags + boolean hasVar, hasContext, hasScope; + + //init flags + hasVar = ctxt.isAttributeSpecified("var"); + hasContext = ctxt.isAttributeSpecified("context"); + hasScope = ctxt.isAttributeSpecified("scope"); + + //define name of the temp variables + String valueName = ctxt.getTemporaryVariableName(); + String contextName = ctxt.getTemporaryVariableName(); + String baseUrlName = ctxt.getTemporaryVariableName(); + String resultName = ctxt.getTemporaryVariableName(); + String responseName = ctxt.getTemporaryVariableName(); + + //get the scope + String strScope = "page"; + if(hasScope){ + strScope = ctxt.getConstantAttribute("scope"); + } + int iScope = Util.getScope(strScope); + + //get the value + ctxt.generateJavaSource("String " + valueName + " = "); + ctxt.generateAttribute("value"); + ctxt.generateJavaSource(";"); + + //get the context + ctxt.generateJavaSource("String " + contextName + " = null;"); + if(hasContext){ + ctxt.generateJavaSource(contextName + " = "); + ctxt.generateAttribute("context"); + ctxt.generateJavaSource(";"); + } + + //get the raw url + ctxt.generateJavaSource("String " + baseUrlName + " = " + + "org.apache.jasper.tagplugins.jstl.Util.resolveUrl(" + valueName + ", " + contextName + ", pageContext);"); + ctxt.generateJavaSource("pageContext.setAttribute" + + "(\"url_without_param\", " + baseUrlName + ");"); + + //add params + ctxt.generateBody(); + + ctxt.generateJavaSource("String " + resultName + " = " + + "(String)pageContext.getAttribute(\"url_without_param\");"); + ctxt.generateJavaSource("pageContext.removeAttribute(\"url_without_param\");"); + + //if the url is relative, encode it + ctxt.generateJavaSource("if(!org.apache.jasper.tagplugins.jstl.Util.isAbsoluteUrl(" + resultName + ")){"); + ctxt.generateJavaSource(" HttpServletResponse " + responseName + " = " + + "((HttpServletResponse) pageContext.getResponse());"); + ctxt.generateJavaSource(" " + resultName + " = " + + responseName + ".encodeURL(" + resultName + ");"); + ctxt.generateJavaSource("}"); + + //if "var" is specified, the url string store in the attribute var defines + if(hasVar){ + String strVar = ctxt.getConstantAttribute("var"); + ctxt.generateJavaSource("pageContext.setAttribute" + + "(\"" + strVar + "\", " + resultName + ", " + iScope + ");"); + + //if var is not specified, just print out the url string + }else{ + ctxt.generateJavaSource("try{"); + ctxt.generateJavaSource(" pageContext.getOut().print(" + resultName + ");"); + ctxt.generateJavaSource("}catch(java.io.IOException ex){"); + ctxt.generateJavaSource(" throw new JspTagException(ex.toString(), ex);"); + ctxt.generateJavaSource("}"); + } + } + +} diff --git a/java/org/apache/jasper/tagplugins/jstl/core/When.java b/java/org/apache/jasper/tagplugins/jstl/core/When.java new file mode 100644 index 0000000..8a11413 --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/core/When.java @@ -0,0 +1,49 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; + +public final class When implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + // Get the parent context to determine if this is the first + TagPluginContext parentContext = ctxt.getParentContext(); + if (parentContext == null) { + ctxt.dontUseTagPlugin(); + return; + } + + if ("true".equals(parentContext.getPluginAttribute("hasBeenHere"))) { + ctxt.generateJavaSource("} else if("); + // See comment below for the reason we generate the extra "}" here. + } else { + ctxt.generateJavaSource("if("); + parentContext.setPluginAttribute("hasBeenHere", "true"); + } + ctxt.generateAttribute("test"); + ctxt.generateJavaSource("){"); + ctxt.generateBody(); + + // We don't generate the closing "}" for the "if" here because there + // may be whitespaces in between 's. Instead we delay + // generating it until the next or or + // + } +} diff --git a/java/org/apache/jasper/tagplugins/jstl/tagPlugins.xml b/java/org/apache/jasper/tagplugins/jstl/tagPlugins.xml new file mode 100644 index 0000000..9ad6b11 --- /dev/null +++ b/java/org/apache/jasper/tagplugins/jstl/tagPlugins.xml @@ -0,0 +1,63 @@ + + + + + org.apache.taglibs.standard.tag.rt.core.IfTag + org.apache.jasper.tagplugins.jstl.core.If + + + org.apache.taglibs.standard.tag.common.core.ChooseTag + org.apache.jasper.tagplugins.jstl.core.Choose + + + org.apache.taglibs.standard.tag.rt.core.WhenTag + org.apache.jasper.tagplugins.jstl.core.When + + + org.apache.taglibs.standard.tag.common.core.OtherwiseTag + org.apache.jasper.tagplugins.jstl.core.Otherwise + + + org.apache.taglibs.standard.tag.rt.core.ForEachTag + org.apache.jasper.tagplugins.jstl.core.ForEach + + + org.apache.taglibs.standard.tag.rt.core.OutTag + org.apache.jasper.tagplugins.jstl.core.Out + + + org.apache.taglibs.standard.tag.rt.core.SetTag + org.apache.jasper.tagplugins.jstl.core.Set + + + org.apache.taglibs.standard.tag.common.core.RemoveTag + org.apache.jasper.tagplugins.jstl.core.Remove + + + org.apache.taglibs.standard.tag.common.core.CatchTag + org.apache.jasper.tagplugins.jstl.core.Catch + + + org.apache.taglibs.standard.tag.rt.core.ForTokensTag + org.apache.jasper.tagplugins.jstl.core.ForTokens + + + org.apache.taglibs.standard.tag.rt.core.ImportTag + org.apache.jasper.tagplugins.jstl.core.Import + + diff --git a/java/org/apache/jasper/util/FastRemovalDequeue.java b/java/org/apache/jasper/util/FastRemovalDequeue.java new file mode 100644 index 0000000..2241ed6 --- /dev/null +++ b/java/org/apache/jasper/util/FastRemovalDequeue.java @@ -0,0 +1,326 @@ +/* + * 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.jasper.util; + +/** + * The FastRemovalDequeue is a Dequeue that supports constant time removal of + * entries. This is achieved by using a doubly linked list and wrapping any object + * added to the collection with an Entry type, that is returned to the consumer. + * When removing an object from the list, the consumer provides this Entry object. + * + * The Entry type is nearly opaque to the consumer of the queue. The only public + * member is the getter for any object displaced when adding a new object to the + * queue. This can be used to destroy that object. + * + * The Entry object contains the links pointing to the neighbours in the doubly + * linked list, so that removal of an Entry does not need to search for it but + * instead can be done in constant time. + * + * The implementation is fully thread-safe. + * + * Invalidation of Entry objects during removal from the list is done + * by setting their "valid" field to false. All public methods which take Entry + * objects as arguments are NOP if the entry is no longer valid. + * + * A typical use of the FastRemovalDequeue is a list of entries in sorted order, + * where the sort position of an object will only switch to first or last. + * + * Whenever the sort position needs to change, the consumer can remove the object + * and reinsert it in front or at the end in constant time. + * So keeping the list sorted is very cheap. + * + * @param The type of elements in the queue + */ +public class FastRemovalDequeue { + + /** Maximum size of the queue */ + private final int maxSize; + /** First element of the queue. */ + protected Entry first; + /** Last element of the queue. */ + protected Entry last; + /** Size of the queue */ + private int size; + + /** + * Initialize empty queue. + * + * @param maxSize The maximum size to which the queue will be allowed to + * grow + */ + public FastRemovalDequeue(int maxSize) { + if (maxSize <=1 ) { + maxSize = 2; + } + this.maxSize = maxSize; + first = null; + last = null; + size = 0; + } + + /** + * Retrieve the size of the list. + * This method also needs to be externally synchronized to + * ensure correct publication of changes. + * + * @return the size of the list. + * */ + public synchronized int getSize() { + return size; + } + + /** + * Adds an object to the start of the list and returns the entry created for + * said object. The entry can later be reused for moving the entry. + * + * @param object the object to prepend to the start of the list. + * @return an entry for use when the object should be moved. + * */ + public synchronized Entry push(final T object) { + Entry entry = new Entry(object); + if (size >= maxSize) { + entry.setReplaced(pop()); + } + if (first == null) { + first = last = entry; + } else { + first.setPrevious(entry); + entry.setNext(first); + first = entry; + } + size++; + + return entry; + } + + /** + * Adds an object to the end of the list and returns the entry created for + * said object. The entry can later be reused for moving the entry. + * + * @param object the object to append to the end of the list. + * @return an entry for use when the object should be moved. + * */ + public synchronized Entry unpop(final T object) { + Entry entry = new Entry(object); + if (size >= maxSize) { + entry.setReplaced(unpush()); + } + if (first == null) { + first = last = entry; + } else { + last.setNext(entry); + entry.setPrevious(last); + last = entry; + } + size++; + + return entry; + } + + /** + * Removes the first element of the list and returns its content. + * + * @return the content of the first element of the list. + **/ + public synchronized T unpush() { + T content = null; + if (first != null) { + Entry element = first; + first = first.getNext(); + content = element.getContent(); + if (first == null) { + last =null; + } else { + first.setPrevious(null); + } + size--; + element.invalidate(); + } + return content; + } + + /** + * Removes the last element of the list and returns its content. + * + * @return the content of the last element of the list. + **/ + public synchronized T pop() { + T content = null; + if (last != null) { + Entry element = last; + last = last.getPrevious(); + content = element.getContent(); + if (last == null) { + first = null; + } else { + last.setNext(null); + } + size--; + element.invalidate(); + } + return content; + } + + /** + * Removes any element of the list and returns its content. + * + * @param element The element to remove + */ + public synchronized void remove(final Entry element) { + if (element == null || !element.getValid()) { + return; + } + Entry next = element.getNext(); + Entry prev = element.getPrevious(); + if (next != null) { + next.setPrevious(prev); + } else { + last = prev; + } + if (prev != null) { + prev.setNext(next); + } else { + first = next; + } + size--; + element.invalidate(); + } + + /** + * Moves the element in front. + * + * Could also be implemented as remove() and + * push(), but explicitly coding might be a bit faster. + * + * @param element the entry to move in front. + * */ + public synchronized void moveFirst(final Entry element) { + if (element.getValid() && + element.getPrevious() != null) { + Entry prev = element.getPrevious(); + Entry next = element.getNext(); + prev.setNext(next); + if (next != null) { + next.setPrevious(prev); + } else { + last = prev; + } + first.setPrevious(element); + element.setNext(first); + element.setPrevious(null); + first = element; + } + } + + /** + * Moves the element to the back. + * + * Could also be implemented as remove() and + * unpop(), but explicitly coding might be a bit faster. + * + * @param element the entry to move to the back. + * */ + public synchronized void moveLast(final Entry element) { + if (element.getValid() && + element.getNext() != null) { + Entry next = element.getNext(); + Entry prev = element.getPrevious(); + next.setPrevious(prev); + if (prev != null) { + prev.setNext(next); + } else { + first = next; + } + last.setNext(element); + element.setPrevious(last); + element.setNext(null); + last = element; + } + } + + /** + * Implementation of a doubly linked list entry. + * All implementation details are private. + * For the consumer of the above collection, this + * is simply garbage in, garbage out. + */ + public class Entry { + + /** Is this entry still valid? */ + private boolean valid = true; + /** The content this entry is valid for. */ + private final T content; + /** Optional content that was displaced by this entry */ + private T replaced = null; + /** Pointer to next element in queue. */ + private Entry next = null; + /** Pointer to previous element in queue. */ + private Entry previous = null; + + private Entry(T object) { + content = object; + } + + private boolean getValid() { + return valid; + } + + private void invalidate() { + this.valid = false; + this.previous = null; + this.next = null; + } + + public final T getContent() { + return content; + } + + public final T getReplaced() { + return replaced; + } + + private void setReplaced(final T replaced) { + this.replaced = replaced; + } + + public final void clearReplaced() { + this.replaced = null; + } + + private Entry getNext() { + return next; + } + + private void setNext(final Entry next) { + this.next = next; + } + + private Entry getPrevious() { + return previous; + } + + private void setPrevious(final Entry previous) { + this.previous = previous; + } + + @Override + public String toString() { + return "Entry-" + content.toString(); + } + } + +} diff --git a/java/org/apache/jasper/util/UniqueAttributesImpl.java b/java/org/apache/jasper/util/UniqueAttributesImpl.java new file mode 100644 index 0000000..8054b0e --- /dev/null +++ b/java/org/apache/jasper/util/UniqueAttributesImpl.java @@ -0,0 +1,120 @@ +/* + * 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.jasper.util; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.jasper.compiler.Localizer; +import org.xml.sax.Attributes; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Wraps the default attributes implementation and ensures that each attribute + * has a unique qname as required by the JSP specification. + */ +public class UniqueAttributesImpl extends AttributesImpl { + + private static final String IMPORT = "import"; + private static final String PAGE_ENCODING = "pageEncoding"; + + private final boolean pageDirective; + private final Set qNames = new HashSet<>(); + + public UniqueAttributesImpl() { + this.pageDirective = false; + } + + public UniqueAttributesImpl(boolean pageDirective) { + this.pageDirective = pageDirective; + } + + @Override + public void clear() { + qNames.clear(); + super.clear(); + } + + @Override + public void setAttributes(Attributes atts) { + for (int i = 0; i < atts.getLength(); i++) { + if (!qNames.add(atts.getQName(i))) { + handleDuplicate(atts.getQName(i), atts.getValue(i)); + } + } + super.setAttributes(atts); + } + + @Override + public void addAttribute(String uri, String localName, String qName, + String type, String value) { + if (qNames.add(qName)) { + super.addAttribute(uri, localName, qName, type, value); + } else { + handleDuplicate(qName, value); + } + } + + @Override + public void setAttribute(int index, String uri, String localName, + String qName, String type, String value) { + qNames.remove(super.getQName(index)); + if (qNames.add(qName)) { + super.setAttribute(index, uri, localName, qName, type, value); + } else { + handleDuplicate(qName, value); + } + } + + @Override + public void removeAttribute(int index) { + qNames.remove(super.getQName(index)); + super.removeAttribute(index); + } + + @Override + public void setQName(int index, String qName) { + qNames.remove(super.getQName(index)); + super.setQName(index, qName); + } + + private void handleDuplicate(String qName, String value) { + if (pageDirective) { + if (IMPORT.equalsIgnoreCase(qName)) { + // Always merge imports + int i = super.getIndex(IMPORT); + String v = super.getValue(i); + super.setValue(i, v + "," + value); + return; + } else if (PAGE_ENCODING.equalsIgnoreCase(qName)) { + // Page encoding can only occur once per file so a second + // attribute - even one with a duplicate value - is an error + } else { + // Other attributes can be repeated if and only if the values + // are identical + String v = super.getValue(qName); + if (v.equals(value)) { + return; + } + } + } + + // Ordinary tag attributes can't be repeated, even with identical values + throw new IllegalArgumentException( + Localizer.getMessage("jsp.error.duplicateqname", qName)); + } +} diff --git a/java/org/apache/juli/AsyncFileHandler.java b/java/org/apache/juli/AsyncFileHandler.java new file mode 100644 index 0000000..ac667c4 --- /dev/null +++ b/java/org/apache/juli/AsyncFileHandler.java @@ -0,0 +1,233 @@ +/* + * 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.juli; + +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.LogRecord; + +/** + * A {@link FileHandler} implementation that uses a queue of log entries. + *

    + * Configuration properties are inherited from the {@link FileHandler} class. This class does not add its own + * configuration properties for the logging configuration, but relies on the following system properties instead: + *

    + *
      + *
    • org.apache.juli.AsyncOverflowDropType Default value: 1
    • + *
    • org.apache.juli.AsyncMaxRecordCount Default value: 10000
    • + *
    + *

    + * See the System Properties page in the configuration reference of Tomcat. + *

    + */ +public class AsyncFileHandler extends FileHandler { + + static final String THREAD_PREFIX = "AsyncFileHandlerWriter-"; + + public static final int OVERFLOW_DROP_LAST = 1; + public static final int OVERFLOW_DROP_FIRST = 2; + public static final int OVERFLOW_DROP_FLUSH = 3; + public static final int OVERFLOW_DROP_CURRENT = 4; + + public static final int DEFAULT_OVERFLOW_DROP_TYPE = 1; + public static final int DEFAULT_MAX_RECORDS = 10000; + + public static final int OVERFLOW_DROP_TYPE = Integer.parseInt( + System.getProperty("org.apache.juli.AsyncOverflowDropType", Integer.toString(DEFAULT_OVERFLOW_DROP_TYPE))); + public static final int MAX_RECORDS = Integer + .parseInt(System.getProperty("org.apache.juli.AsyncMaxRecordCount", Integer.toString(DEFAULT_MAX_RECORDS))); + + private static final LoggerExecutorService LOGGER_SERVICE = new LoggerExecutorService(OVERFLOW_DROP_TYPE, + MAX_RECORDS); + + private final Object closeLock = new Object(); + protected volatile boolean closed = false; + private final LoggerExecutorService loggerService; + + public AsyncFileHandler() { + this(null, null, null); + } + + public AsyncFileHandler(String directory, String prefix, String suffix) { + this(directory, prefix, suffix, null); + } + + public AsyncFileHandler(String directory, String prefix, String suffix, Integer maxDays) { + this(directory, prefix, suffix, maxDays, LOGGER_SERVICE); + } + + AsyncFileHandler(String directory, String prefix, String suffix, Integer maxDays, + LoggerExecutorService loggerService) { + super(directory, prefix, suffix, maxDays); + loggerService.registerHandler(); + this.loggerService = loggerService; + } + + @Override + public void close() { + if (closed) { + return; + } + synchronized (closeLock) { + if (closed) { + return; + } + closed = true; + } + loggerService.deregisterHandler(); + super.close(); + } + + @Override + public void open() { + if (!closed) { + return; + } + synchronized (closeLock) { + if (!closed) { + return; + } + closed = false; + } + loggerService.registerHandler(); + super.open(); + } + + @Override + public void publish(LogRecord record) { + if (!isLoggable(record)) { + return; + } + // fill source entries, before we hand the record over to another + // thread with another class loader + record.getSourceMethodName(); + loggerService.execute(new Runnable() { + + @Override + public void run() { + /* + * During Tomcat shutdown, the Handlers are closed before the executor queue is flushed therefore the + * closed flag is ignored if the executor is shutting down. + */ + if (!closed || loggerService.isTerminating()) { + publishInternal(record); + } + } + }); + } + + protected void publishInternal(LogRecord record) { + super.publish(record); + } + + + static class LoggerExecutorService extends ThreadPoolExecutor { + + private static final ThreadFactory THREAD_FACTORY = new ThreadFactory(THREAD_PREFIX); + + /* + * Implementation note: Use of this count could be extended to start/stop the LoggerExecutorService but that + * would require careful locking as the current size of the queue also needs to be taken into account and there + * are lost of edge cases when rapidly starting and stopping handlers. + */ + private final AtomicInteger handlerCount = new AtomicInteger(); + + LoggerExecutorService(final int overflowDropType, final int maxRecords) { + super(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(maxRecords), THREAD_FACTORY); + switch (overflowDropType) { + case OVERFLOW_DROP_LAST: + default: + setRejectedExecutionHandler(new DropLastPolicy()); + break; + case OVERFLOW_DROP_FIRST: + setRejectedExecutionHandler(new DiscardOldestPolicy()); + break; + case OVERFLOW_DROP_FLUSH: + setRejectedExecutionHandler(new DropFlushPolicy()); + break; + case OVERFLOW_DROP_CURRENT: + setRejectedExecutionHandler(new DiscardPolicy()); + } + } + + @Override + public LinkedBlockingDeque getQueue() { + return (LinkedBlockingDeque) super.getQueue(); + } + + public void registerHandler() { + handlerCount.incrementAndGet(); + } + + public void deregisterHandler() { + int newCount = handlerCount.decrementAndGet(); + if (newCount == 0) { + try { + Thread dummyHook = new Thread(); + Runtime.getRuntime().addShutdownHook(dummyHook); + Runtime.getRuntime().removeShutdownHook(dummyHook); + } catch (IllegalStateException ise) { + // JVM is shutting down. + // Allow up to 10s for for the queue to be emptied + shutdown(); + try { + awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // Ignore + } + shutdownNow(); + } + } + } + } + + + private static class DropFlushPolicy implements RejectedExecutionHandler { + + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + while (true) { + if (executor.isShutdown()) { + break; + } + try { + if (executor.getQueue().offer(r, 1000, TimeUnit.MILLISECONDS)) { + break; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RejectedExecutionException("Interrupted", e); + } + } + } + } + + private static class DropLastPolicy implements RejectedExecutionHandler { + + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + if (!executor.isShutdown()) { + ((LoggerExecutorService) executor).getQueue().pollLast(); + executor.execute(r); + } + } + } +} diff --git a/java/org/apache/juli/ClassLoaderLogManager.java b/java/org/apache/juli/ClassLoaderLogManager.java new file mode 100644 index 0000000..0eb11f7 --- /dev/null +++ b/java/org/apache/juli/ClassLoaderLogManager.java @@ -0,0 +1,761 @@ +/* + * 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.juli; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.AccessControlException; +import java.security.AccessController; +import java.security.Permission; +import java.security.PrivilegedAction; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + + +/** + * Per classloader LogManager implementation. For light debugging, set the system property + * org.apache.juli.ClassLoaderLogManager.debug=true. Short configuration information will be sent to + * System.err. + */ +public class ClassLoaderLogManager extends LogManager { + + private static ThreadLocal addingLocalRootLogger = ThreadLocal.withInitial(() -> Boolean.FALSE); + + public static final String DEBUG_PROPERTY = ClassLoaderLogManager.class.getName() + ".debug"; + + private final class Cleaner extends Thread { + + @Override + public void run() { + if (useShutdownHook) { + shutdown(); + } + } + + } + + + // ------------------------------------------------------------Constructors + + public ClassLoaderLogManager() { + super(); + try { + Runtime.getRuntime().addShutdownHook(new Cleaner()); + } catch (IllegalStateException ise) { + // We are probably already being shutdown. Ignore this error. + } + } + + + // -------------------------------------------------------------- Variables + + + /** + * Map containing the classloader information, keyed per classloader. A weak hashmap is used to ensure no + * classloader reference is leaked from application redeployment. + */ + protected final Map classLoaderLoggers = new WeakHashMap<>(); // Guarded by this + + + /** + * This prefix is used to allow using prefixes for the properties names of handlers and their subcomponents. + */ + protected final ThreadLocal prefix = new ThreadLocal<>(); + + + /** + * Determines if the shutdown hook is used to perform any necessary clean-up such as flushing buffered handlers on + * JVM shutdown. Defaults to true but may be set to false if another component ensures that + * {@link #shutdown()} is called. + */ + protected volatile boolean useShutdownHook = true; + + + // ------------------------------------------------------------- Properties + + + public boolean isUseShutdownHook() { + return useShutdownHook; + } + + + public void setUseShutdownHook(boolean useShutdownHook) { + this.useShutdownHook = useShutdownHook; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add the specified logger to the classloader local configuration. + * + * @param logger The logger to be added + */ + @Override + public synchronized boolean addLogger(final Logger logger) { + + final String loggerName = logger.getName(); + + ClassLoader classLoader = getClassLoader(); + ClassLoaderLogInfo info = getClassLoaderInfo(classLoader); + if (info.loggers.containsKey(loggerName)) { + return false; + } + info.loggers.put(loggerName, logger); + + // Apply initial level for new logger + final String levelString = getProperty(loggerName + ".level"); + if (levelString != null) { + try { + AccessController.doPrivileged((PrivilegedAction) () -> { + logger.setLevel(Level.parse(levelString.trim())); + return null; + }); + } catch (IllegalArgumentException e) { + // Leave level set to null + } + } + + // Always instantiate parent loggers so that + // we can control log categories even during runtime + int dotIndex = loggerName.lastIndexOf('.'); + if (dotIndex >= 0) { + final String parentName = loggerName.substring(0, dotIndex); + Logger.getLogger(parentName); + } + + // Find associated node + LogNode node = info.rootNode.findNode(loggerName); + node.logger = logger; + + // Set parent logger + Logger parentLogger = node.findParentLogger(); + if (parentLogger != null) { + doSetParentLogger(logger, parentLogger); + } + + // Tell children we are their new parent + node.setParentLogger(logger); + + // Add associated handlers, if any are defined using the .handlers property. + // In this case, handlers of the parent logger(s) will not be used + String handlers = getProperty(loggerName + ".handlers"); + if (handlers != null) { + logger.setUseParentHandlers(false); + StringTokenizer tok = new StringTokenizer(handlers, ","); + while (tok.hasMoreTokens()) { + String handlerName = (tok.nextToken().trim()); + Handler handler = null; + ClassLoader current = classLoader; + while (current != null) { + info = classLoaderLoggers.get(current); + if (info != null) { + handler = info.handlers.get(handlerName); + if (handler != null) { + break; + } + } + current = current.getParent(); + } + if (handler != null) { + logger.addHandler(handler); + } + } + } + + // Parse useParentHandlers to set if the logger should delegate to its parent. + // Unlike java.util.logging, the default is to not delegate if a list of handlers + // has been specified for the logger. + String useParentHandlersString = getProperty(loggerName + ".useParentHandlers"); + if (Boolean.parseBoolean(useParentHandlersString)) { + logger.setUseParentHandlers(true); + } + + return true; + } + + + /** + * Get the logger associated with the specified name inside the classloader local configuration. If this returns + * null, and the call originated for Logger.getLogger, a new logger with the specified name will be instantiated and + * added using addLogger. + * + * @param name The name of the logger to retrieve + */ + @Override + public synchronized Logger getLogger(final String name) { + ClassLoader classLoader = getClassLoader(); + return getClassLoaderInfo(classLoader).loggers.get(name); + } + + + /** + * Get an enumeration of the logger names currently defined in the classloader local configuration. + */ + @Override + public synchronized Enumeration getLoggerNames() { + ClassLoader classLoader = getClassLoader(); + return Collections.enumeration(getClassLoaderInfo(classLoader).loggers.keySet()); + } + + + /** + * Get the value of the specified property in the classloader local configuration. + * + * @param name The property name + */ + @Override + public String getProperty(String name) { + + // Use a ThreadLocal to work around + // https://bugs.openjdk.java.net/browse/JDK-8195096 + if (".handlers".equals(name) && !addingLocalRootLogger.get().booleanValue()) { + return null; + } + + String prefix = this.prefix.get(); + String result = null; + + // If a prefix is defined look for a prefixed property first + if (prefix != null) { + result = findProperty(prefix + name); + } + + // If there is no prefix or no property match with the prefix try just + // the name + if (result == null) { + result = findProperty(name); + } + + // Simple property replacement (mostly for folder names) + if (result != null) { + result = replace(result); + } + return result; + } + + + private synchronized String findProperty(String name) { + ClassLoader classLoader = getClassLoader(); + ClassLoaderLogInfo info = getClassLoaderInfo(classLoader); + String result = info.props.getProperty(name); + // If the property was not found, and the current classloader had no + // configuration (property list is empty), look for the parent classloader + // properties. + if ((result == null) && (info.props.isEmpty())) { + if (classLoader != null) { + ClassLoader current = classLoader.getParent(); + while (current != null) { + info = classLoaderLoggers.get(current); + if (info != null) { + result = info.props.getProperty(name); + if ((result != null) || (!info.props.isEmpty())) { + break; + } + } + current = current.getParent(); + } + } + if (result == null) { + result = super.getProperty(name); + } + } + return result; + } + + @Override + public void readConfiguration() throws IOException, SecurityException { + checkAccess(); + readConfiguration(getClassLoader()); + } + + @Override + public void readConfiguration(InputStream is) throws IOException, SecurityException { + checkAccess(); + reset(); + readConfiguration(is, getClassLoader()); + } + + @Override + public synchronized void reset() throws SecurityException { + Thread thread = Thread.currentThread(); + if (thread.getClass().getName().startsWith("java.util.logging.LogManager$")) { + // Ignore the call from java.util.logging.LogManager.Cleaner, + // because we have our own shutdown hook + return; + } + ClassLoader classLoader = getClassLoader(); + ClassLoaderLogInfo clLogInfo = getClassLoaderInfo(classLoader); + resetLoggers(clLogInfo); + // Do not call super.reset(). It should be a NO-OP as all loggers should + // have been registered via this manager. Very rarely a + // ConcurrentModificationException has been seen in the unit tests when + // calling super.reset() and that exception could cause the stop of a + // web application to fail. + } + + /** + * Shuts down the logging system. + */ + public synchronized void shutdown() { + // The JVM is being shutdown. Make sure all loggers for all class + // loaders are shutdown + for (ClassLoaderLogInfo clLogInfo : classLoaderLoggers.values()) { + resetLoggers(clLogInfo); + } + } + + // -------------------------------------------------------- Private Methods + private void resetLoggers(ClassLoaderLogInfo clLogInfo) { + // This differs from LogManager#resetLogger() in that we close not all + // handlers of all loggers, but only those that are present in our + // ClassLoaderLogInfo#handlers list. That is because our #addLogger(..) + // method can use handlers from the parent class loaders, and closing + // handlers that the current class loader does not own would be not + // good. + for (Logger logger : clLogInfo.loggers.values()) { + Handler[] handlers = logger.getHandlers(); + for (Handler handler : handlers) { + logger.removeHandler(handler); + } + } + for (Handler handler : clLogInfo.handlers.values()) { + try { + handler.close(); + } catch (Exception e) { + // Ignore + } + } + clLogInfo.handlers.clear(); + } + + // ------------------------------------------------------ Protected Methods + + + /** + * Retrieve the configuration associated with the specified classloader. If it does not exist, it will be created. + * If no class loader is specified, the class loader used to load this class is used. + * + * @param classLoader The class loader for which we will retrieve or build the configuration + * + * @return the log configuration + */ + protected synchronized ClassLoaderLogInfo getClassLoaderInfo(ClassLoader classLoader) { + + if (classLoader == null) { + classLoader = this.getClass().getClassLoader(); + } + ClassLoaderLogInfo info = classLoaderLoggers.get(classLoader); + if (info == null) { + final ClassLoader classLoaderParam = classLoader; + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + readConfiguration(classLoaderParam); + } catch (IOException e) { + // Ignore + } + return null; + }); + info = classLoaderLoggers.get(classLoader); + } + return info; + } + + + /** + * Read configuration for the specified classloader. + * + * @param classLoader The classloader + * + * @throws IOException Error reading configuration + */ + protected synchronized void readConfiguration(ClassLoader classLoader) throws IOException { + + InputStream is = null; + // Special case for URL classloaders which are used in containers: + // only look in the local repositories to avoid redefining loggers 20 times + try { + if (classLoader instanceof WebappProperties) { + if (((WebappProperties) classLoader).hasLoggingConfig()) { + is = classLoader.getResourceAsStream("logging.properties"); + } + } else if (classLoader instanceof URLClassLoader) { + URL logConfig = ((URLClassLoader) classLoader).findResource("logging.properties"); + + if (null != logConfig) { + if (Boolean.getBoolean(DEBUG_PROPERTY)) { + System.err.println(getClass().getName() + ".readConfiguration(): " + + "Found logging.properties at " + logConfig); + } + + is = classLoader.getResourceAsStream("logging.properties"); + } else { + if (Boolean.getBoolean(DEBUG_PROPERTY)) { + System.err.println( + getClass().getName() + ".readConfiguration(): " + "Found no logging.properties"); + } + } + } + } catch (AccessControlException ace) { + // No permission to configure logging in context + // Log and carry on + ClassLoaderLogInfo info = classLoaderLoggers.get(ClassLoader.getSystemClassLoader()); + if (info != null) { + Logger log = info.loggers.get(""); + if (log != null) { + Permission perm = ace.getPermission(); + if (perm instanceof FilePermission && perm.getActions().equals("read")) { + log.warning("Reading " + perm.getName() + + " is not permitted. See \"per context logging\" in the default catalina.policy file."); + } else { + log.warning( + "Reading logging.properties is not permitted in some context. See \"per context logging\" in the default catalina.policy file."); + log.warning("Original error was: " + ace.getMessage()); + } + } + } + } + if ((is == null) && (classLoader == ClassLoader.getSystemClassLoader())) { + String configFileStr = System.getProperty("java.util.logging.config.file"); + if (configFileStr != null) { + try { + is = new FileInputStream(replace(configFileStr)); + } catch (IOException e) { + System.err.println("Configuration error"); + e.printStackTrace(); + } + } + // Try the default JVM configuration + if (is == null) { + File defaultFile = new File(new File(System.getProperty("java.home"), "conf"), "logging.properties"); + try { + is = new FileInputStream(defaultFile); + } catch (IOException e) { + System.err.println("Configuration error"); + e.printStackTrace(); + } + } + } + + Logger localRootLogger = new RootLogger(); + if (is == null) { + // Retrieve the root logger of the parent classloader instead + ClassLoader current = classLoader.getParent(); + ClassLoaderLogInfo info = null; + while (current != null && info == null) { + info = getClassLoaderInfo(current); + current = current.getParent(); + } + if (info != null) { + localRootLogger.setParent(info.rootNode.logger); + } + } + ClassLoaderLogInfo info = new ClassLoaderLogInfo(new LogNode(null, localRootLogger)); + classLoaderLoggers.put(classLoader, info); + + if (is != null) { + readConfiguration(is, classLoader); + } + + if (localRootLogger.getParent() == null && localRootLogger.getLevel() == null) { + localRootLogger.setLevel(Level.INFO); + } + try { + // Use a ThreadLocal to work around + // https://bugs.openjdk.java.net/browse/JDK-8195096 + addingLocalRootLogger.set(Boolean.TRUE); + addLogger(localRootLogger); + } finally { + addingLocalRootLogger.set(Boolean.FALSE); + } + } + + + /** + * Load specified configuration. + * + * @param is InputStream to the properties file + * @param classLoader for which the configuration will be loaded + * + * @throws IOException If something wrong happens during loading + */ + protected synchronized void readConfiguration(InputStream is, ClassLoader classLoader) throws IOException { + + ClassLoaderLogInfo info = classLoaderLoggers.get(classLoader); + + try { + info.props.load(is); + } catch (IOException e) { + // Report error + System.err.println("Configuration error"); + e.printStackTrace(); + } finally { + try { + is.close(); + } catch (IOException ioe) { + // Ignore + } + } + + // Create handlers for the root logger of this classloader + String rootHandlers = info.props.getProperty(".handlers"); + String handlers = info.props.getProperty("handlers"); + Logger localRootLogger = info.rootNode.logger; + if (handlers != null) { + StringTokenizer tok = new StringTokenizer(handlers, ","); + while (tok.hasMoreTokens()) { + String handlerName = (tok.nextToken().trim()); + String handlerClassName = handlerName; + String prefix = ""; + if (handlerClassName.length() <= 0) { + continue; + } + // Parse and remove a prefix (prefix start with a digit, such as + // "10WebappFooHandler.") + if (Character.isDigit(handlerClassName.charAt(0))) { + int pos = handlerClassName.indexOf('.'); + if (pos >= 0) { + prefix = handlerClassName.substring(0, pos + 1); + handlerClassName = handlerClassName.substring(pos + 1); + } + } + try { + this.prefix.set(prefix); + Handler handler = (Handler) classLoader.loadClass(handlerClassName).getConstructor().newInstance(); + // The specification strongly implies all configuration should be done + // during the creation of the handler object. + // This includes setting level, filter, formatter and encoding. + this.prefix.set(null); + info.handlers.put(handlerName, handler); + if (rootHandlers == null) { + localRootLogger.addHandler(handler); + } + } catch (Exception e) { + // Report error + System.err.println("Handler error"); + e.printStackTrace(); + } + } + + } + + } + + + /** + * Set parent child relationship between the two specified loggers. + * + * @param logger The logger + * @param parent The parent logger + */ + protected static void doSetParentLogger(final Logger logger, final Logger parent) { + AccessController.doPrivileged((PrivilegedAction) () -> { + logger.setParent(parent); + return null; + }); + } + + + /** + * System property replacement in the given string. + * + * @param str The original string + * + * @return the modified string + */ + protected String replace(String str) { + String result = str; + int pos_start = str.indexOf("${"); + if (pos_start >= 0) { + StringBuilder builder = new StringBuilder(); + int pos_end = -1; + while (pos_start >= 0) { + builder.append(str, pos_end + 1, pos_start); + pos_end = str.indexOf('}', pos_start + 2); + if (pos_end < 0) { + pos_end = pos_start - 1; + break; + } + String propName = str.substring(pos_start + 2, pos_end); + + String replacement = replaceWebApplicationProperties(propName); + if (replacement == null) { + replacement = propName.length() > 0 ? System.getProperty(propName) : null; + } + if (replacement != null) { + builder.append(replacement); + } else { + builder.append(str, pos_start, pos_end + 1); + } + pos_start = str.indexOf("${", pos_end + 1); + } + builder.append(str, pos_end + 1, str.length()); + result = builder.toString(); + } + return result; + } + + + private String replaceWebApplicationProperties(String propName) { + ClassLoader cl = getClassLoader(); + if (cl instanceof WebappProperties) { + WebappProperties wProps = (WebappProperties) cl; + if ("classloader.webappName".equals(propName)) { + return wProps.getWebappName(); + } else if ("classloader.hostName".equals(propName)) { + return wProps.getHostName(); + } else if ("classloader.serviceName".equals(propName)) { + return wProps.getServiceName(); + } else { + return null; + } + } else { + return null; + } + } + + + /** + * Obtain the class loader to use to lookup loggers, obtain configuration etc. The search order is: + *
      + *
    1. Thread.currentThread().getContextClassLoader()
    2. + *
    3. The class laoder of this class
    4. + *
    + * + * @return The class loader to use to lookup loggers, obtain configuration etc. + */ + static ClassLoader getClassLoader() { + ClassLoader result = Thread.currentThread().getContextClassLoader(); + if (result == null) { + result = ClassLoaderLogManager.class.getClassLoader(); + } + return result; + } + + + // ---------------------------------------------------- LogNode Inner Class + + protected static final class LogNode { + Logger logger; + + final Map children = new HashMap<>(); + + final LogNode parent; + + LogNode(final LogNode parent, final Logger logger) { + this.parent = parent; + this.logger = logger; + } + + LogNode(final LogNode parent) { + this(parent, null); + } + + LogNode findNode(String name) { + LogNode currentNode = this; + if (logger.getName().equals(name)) { + return this; + } + while (name != null) { + final int dotIndex = name.indexOf('.'); + final String nextName; + if (dotIndex < 0) { + nextName = name; + name = null; + } else { + nextName = name.substring(0, dotIndex); + name = name.substring(dotIndex + 1); + } + LogNode childNode = currentNode.children.get(nextName); + if (childNode == null) { + childNode = new LogNode(currentNode); + currentNode.children.put(nextName, childNode); + } + currentNode = childNode; + } + return currentNode; + } + + Logger findParentLogger() { + Logger logger = null; + LogNode node = parent; + while (node != null && logger == null) { + logger = node.logger; + node = node.parent; + } + return logger; + } + + void setParentLogger(final Logger parent) { + for (final LogNode childNode : children.values()) { + if (childNode.logger == null) { + childNode.setParentLogger(parent); + } else { + doSetParentLogger(childNode.logger, parent); + } + } + } + + } + + + // -------------------------------------------- ClassLoaderInfo Inner Class + + + protected static final class ClassLoaderLogInfo { + final LogNode rootNode; + final Map loggers = new ConcurrentHashMap<>(); + final Map handlers = new HashMap<>(); + final Properties props = new Properties(); + + ClassLoaderLogInfo(final LogNode rootNode) { + this.rootNode = rootNode; + } + + } + + + // ------------------------------------------------- RootLogger Inner Class + + + /** + * This class is needed to instantiate the root of each per classloader hierarchy. + */ + protected static class RootLogger extends Logger { + public RootLogger() { + super("", null); + } + } + + +} diff --git a/java/org/apache/juli/DateFormatCache.java b/java/org/apache/juli/DateFormatCache.java new file mode 100644 index 0000000..2016c11 --- /dev/null +++ b/java/org/apache/juli/DateFormatCache.java @@ -0,0 +1,193 @@ +/* + * 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.juli; + + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + *

    + * Cache structure for SimpleDateFormat formatted timestamps based on seconds. + *

    + *

    + * Millisecond formatting using S is not supported. You should add the millisecond information after getting back the + * second formatting. + *

    + *

    + * The cache consists of entries for a consecutive range of seconds. The length of the range is configurable. It is + * implemented based on a cyclic buffer. New entries shift the range. + *

    + *

    + * The cache is not threadsafe. It can be used without synchronization via thread local instances, or with + * synchronization as a global cache. + *

    + *

    + * The cache can be created with a parent cache to build a cache hierarchy. Access to the parent cache is threadsafe. + *

    + */ +public class DateFormatCache { + + public static final char MSEC_PATTERN = '#'; + + /* Timestamp format */ + private final String format; + + /* Number of cached entries */ + private final int cacheSize; + + private final Cache cache; + + /** + * Replace the millisecond formatting character 'S' by some dummy characters in order to make the resulting + * formatted time stamps cacheable. Our consumer might choose to replace the dummy chars with the actual + * milliseconds because that's relatively cheap. + */ + private String tidyFormat(String format) { + boolean escape = false; + StringBuilder result = new StringBuilder(); + int len = format.length(); + char x; + for (int i = 0; i < len; i++) { + x = format.charAt(i); + if (escape || x != 'S') { + result.append(x); + } else { + result.append(MSEC_PATTERN); + } + if (x == '\'') { + escape = !escape; + } + } + return result.toString(); + } + + public DateFormatCache(int size, String format, DateFormatCache parent) { + cacheSize = size; + this.format = tidyFormat(format); + Cache parentCache = null; + if (parent != null) { + synchronized (parent) { + parentCache = parent.cache; + } + } + cache = new Cache(parentCache); + } + + public String getFormat(long time) { + return cache.getFormat(time); + } + + public String getTimeFormat() { + return format; + } + + private class Cache { + + /* Second formatted in most recent invocation */ + private long previousSeconds = Long.MIN_VALUE; + /* Formatted timestamp generated in most recent invocation */ + private String previousFormat = ""; + + /* First second contained in cache */ + private long first = Long.MIN_VALUE; + /* Last second contained in cache */ + private long last = Long.MIN_VALUE; + /* Index of "first" in the cyclic cache */ + private int offset = 0; + /* Helper object to be able to call SimpleDateFormat.format(). */ + private final Date currentDate = new Date(); + + private String cache[]; + private SimpleDateFormat formatter; + + private Cache parent = null; + + private Cache(Cache parent) { + cache = new String[cacheSize]; + formatter = new SimpleDateFormat(format, Locale.US); + formatter.setTimeZone(TimeZone.getDefault()); + this.parent = parent; + } + + private String getFormat(long time) { + + long seconds = time / 1000; + + /* + * First step: if we have seen this timestamp during the previous call, return the previous value. + */ + if (seconds == previousSeconds) { + return previousFormat; + } + + /* Second step: Try to locate in cache */ + previousSeconds = seconds; + int index = (offset + (int) (seconds - first)) % cacheSize; + if (index < 0) { + index += cacheSize; + } + if (seconds >= first && seconds <= last) { + if (cache[index] != null) { + /* Found, so remember for next call and return. */ + previousFormat = cache[index]; + return previousFormat; + } + + /* Third step: not found in cache, adjust cache and add item */ + } else if (seconds >= last + cacheSize || seconds <= first - cacheSize) { + first = seconds; + last = first + cacheSize - 1; + index = 0; + offset = 0; + for (int i = 1; i < cacheSize; i++) { + cache[i] = null; + } + } else if (seconds > last) { + for (int i = 1; i < seconds - last; i++) { + cache[(index + cacheSize - i) % cacheSize] = null; + } + first = seconds - (cacheSize - 1); + last = seconds; + offset = (index + 1) % cacheSize; + } else if (seconds < first) { + for (int i = 1; i < first - seconds; i++) { + cache[(index + i) % cacheSize] = null; + } + first = seconds; + last = seconds + (cacheSize - 1); + offset = index; + } + + /* + * Last step: format new timestamp either using parent cache or locally. + */ + if (parent != null) { + synchronized (parent) { + previousFormat = parent.getFormat(time); + } + } else { + currentDate.setTime(time); + previousFormat = formatter.format(currentDate); + } + cache[index] = previousFormat; + return previousFormat; + } + } +} diff --git a/java/org/apache/juli/FileHandler.java b/java/org/apache/juli/FileHandler.java new file mode 100644 index 0000000..c676108 --- /dev/null +++ b/java/org/apache/juli/FileHandler.java @@ -0,0 +1,559 @@ +/* + * 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.juli; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.sql.Timestamp; +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; +import java.util.regex.Pattern; + +/** + * Implementation of Handler that appends log messages to a file named {prefix}{date}{suffix} in a configured + * directory. + *

    + * The following configuration properties are available: + *

    + *
      + *
    • directory - The directory where to create the log file. If the path is not absolute, it is relative + * to the current working directory of the application. The Apache Tomcat configuration files usually specify an + * absolute path for this property, ${catalina.base}/logs Default value: logs
    • + *
    • rotatable - If true, the log file will be rotated on the first write past midnight and + * the filename will be {prefix}{date}{suffix}, where date is yyyy-MM-dd. If false, the file + * will not be rotated and the filename will be {prefix}{suffix}. Default value: true
    • + *
    • prefix - The leading part of the log file name. Default value: juli.
    • + *
    • suffix - The trailing part of the log file name. Default value: .log
    • + *
    • bufferSize - Configures buffering. The value of 0 uses system default buffering + * (typically an 8K buffer will be used). A value of <0 forces a writer flush upon each log write. A + * value >0 uses a BufferedOutputStream with the defined value but note that the system default + * buffering will also be applied. Default value: -1
    • + *
    • encoding - Character set used by the log file. Default value: empty string, which means to use the + * system default character set.
    • + *
    • level - The level threshold for this Handler. See the java.util.logging.Level class for + * the possible levels. Default value: ALL
    • + *
    • filter - The java.util.logging.Filter implementation class name for this Handler. + * Default value: unset
    • + *
    • formatter - The java.util.logging.Formatter implementation class name for this Handler. + * Default value: org.apache.juli.OneLineFormatter
    • + *
    • maxDays - The maximum number of days to keep the log files. If the specified value is + * <=0 then the log files will be kept on the file system forever, otherwise they will be kept the + * specified maximum days. Default value: -1.
    • + *
    + */ +public class FileHandler extends Handler { + + + public static final int DEFAULT_MAX_DAYS = -1; + public static final int DEFAULT_BUFFER_SIZE = -1; + + + private static final ExecutorService DELETE_FILES_SERVICE = Executors + .newSingleThreadExecutor(new ThreadFactory("FileHandlerLogFilesCleaner-")); + + // ------------------------------------------------------------ Constructor + + + public FileHandler() { + this(null, null, null); + } + + + public FileHandler(String directory, String prefix, String suffix) { + this(directory, prefix, suffix, null); + } + + + public FileHandler(String directory, String prefix, String suffix, Integer maxDays) { + this(directory, prefix, suffix, maxDays, null, null); + } + + + public FileHandler(String directory, String prefix, String suffix, Integer maxDays, Boolean rotatable, + Integer bufferSize) { + this.directory = directory; + this.prefix = prefix; + this.suffix = suffix; + this.maxDays = maxDays; + this.rotatable = rotatable; + this.bufferSize = bufferSize; + configure(); + clean(); + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The as-of date for the currently open log file, or null if there is no open log file. + */ + private volatile String date = null; + + + /** + * The directory in which log files are created. + */ + private String directory; + + + /** + * The prefix that is added to log file filenames. + */ + private String prefix; + + + /** + * The suffix that is added to log file filenames. + */ + private String suffix; + + + /** + * Determines whether the log file is rotatable + */ + private Boolean rotatable; + + + /** + * Maximum number of days to keep the log files + */ + private Integer maxDays; + + + /** + * The PrintWriter to which we are currently logging, if any. + */ + private volatile PrintWriter writer = null; + + + /** + * Lock used to control access to the writer. + */ + protected final ReadWriteLock writerLock = new ReentrantReadWriteLock(); + + + /** + * Log buffer size. + */ + private Integer bufferSize; + + + /** + * Represents a file name pattern of type {prefix}{date}{suffix}. The date is YYYY-MM-DD + */ + private Pattern pattern; + + + // --------------------------------------------------------- Public Methods + + + /** + * Format and publish a LogRecord. + * + * @param record description of the log event + */ + @Override + public void publish(LogRecord record) { + + if (!isLoggable(record)) { + return; + } + + final String tsDate; + if (rotatable.booleanValue()) { + // Construct the timestamp we will use + Timestamp ts = new Timestamp(System.currentTimeMillis()); + tsDate = ts.toString().substring(0, 10); + } else { + tsDate = ""; + } + + writerLock.readLock().lock(); + try { + // If the date has changed, switch log files + if (!tsDate.equals(date)) { + // Upgrade to writeLock before we switch + writerLock.readLock().unlock(); + writerLock.writeLock().lock(); + try { + // Make sure another thread hasn't already done this + if (!tsDate.equals(date)) { + closeWriter(); + date = tsDate; + openWriter(); + clean(); + } + } finally { + // Downgrade to read-lock. This ensures the writer remains valid + // until the log message is written + writerLock.readLock().lock(); + writerLock.writeLock().unlock(); + } + } + + String result = null; + try { + result = getFormatter().format(record); + } catch (Exception e) { + reportError(null, e, ErrorManager.FORMAT_FAILURE); + return; + } + + try { + if (writer != null) { + writer.write(result); + if (bufferSize.intValue() < 0) { + writer.flush(); + } + } else { + reportError("FileHandler is closed or not yet initialized, unable to log [" + result + "]", null, + ErrorManager.WRITE_FAILURE); + } + } catch (Exception e) { + reportError(null, e, ErrorManager.WRITE_FAILURE); + } + } finally { + writerLock.readLock().unlock(); + } + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Close the currently open log file (if any). + */ + @Override + public void close() { + closeWriter(); + } + + protected void closeWriter() { + + writerLock.writeLock().lock(); + try { + if (writer == null) { + return; + } + writer.write(getFormatter().getTail(this)); + writer.flush(); + writer.close(); + writer = null; + date = null; + } catch (Exception e) { + reportError(null, e, ErrorManager.CLOSE_FAILURE); + } finally { + writerLock.writeLock().unlock(); + } + } + + + /** + * Flush the writer. + */ + @Override + public void flush() { + + writerLock.readLock().lock(); + try { + if (writer == null) { + return; + } + writer.flush(); + } catch (Exception e) { + reportError(null, e, ErrorManager.FLUSH_FAILURE); + } finally { + writerLock.readLock().unlock(); + } + + } + + + /** + * Configure from LogManager properties. + */ + private void configure() { + + String className = this.getClass().getName(); // allow classes to override + + ClassLoader cl = ClassLoaderLogManager.getClassLoader(); + + // Retrieve configuration of logging file name + if (rotatable == null) { + rotatable = Boolean.valueOf(getProperty(className + ".rotatable", "true")); + } + if (directory == null) { + directory = getProperty(className + ".directory", "logs"); + } + if (prefix == null) { + prefix = getProperty(className + ".prefix", "juli."); + } + if (suffix == null) { + suffix = getProperty(className + ".suffix", ".log"); + } + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=61232 + boolean shouldCheckForRedundantSeparator = !rotatable.booleanValue() && !prefix.isEmpty() && !suffix.isEmpty(); + // assuming separator is just one char, if there are use cases with + // more, the notion of separator might be introduced + if (shouldCheckForRedundantSeparator && (prefix.charAt(prefix.length() - 1) == suffix.charAt(0))) { + suffix = suffix.substring(1); + } + + pattern = Pattern + .compile("^(" + Pattern.quote(prefix) + ")\\d{4}-\\d{1,2}-\\d{1,2}(" + Pattern.quote(suffix) + ")$"); + + if (maxDays == null) { + String sMaxDays = getProperty(className + ".maxDays", String.valueOf(DEFAULT_MAX_DAYS)); + try { + maxDays = Integer.valueOf(sMaxDays); + } catch (NumberFormatException ignore) { + maxDays = Integer.valueOf(DEFAULT_MAX_DAYS); + } + } + + if (bufferSize == null) { + String sBufferSize = getProperty(className + ".bufferSize", String.valueOf(DEFAULT_BUFFER_SIZE)); + try { + bufferSize = Integer.valueOf(sBufferSize); + } catch (NumberFormatException ignore) { + bufferSize = Integer.valueOf(DEFAULT_BUFFER_SIZE); + } + } + + // Get encoding for the logging file + String encoding = getProperty(className + ".encoding", null); + if (encoding != null && encoding.length() > 0) { + try { + setEncoding(encoding); + } catch (UnsupportedEncodingException ex) { + // Ignore + } + } + + // Get logging level for the handler + setLevel(Level.parse(getProperty(className + ".level", "" + Level.ALL))); + + // Get filter configuration + String filterName = getProperty(className + ".filter", null); + if (filterName != null) { + try { + setFilter((Filter) cl.loadClass(filterName).getConstructor().newInstance()); + } catch (Exception e) { + // Ignore + } + } + + // Set formatter + String formatterName = getProperty(className + ".formatter", null); + if (formatterName != null) { + try { + setFormatter((Formatter) cl.loadClass(formatterName).getConstructor().newInstance()); + } catch (Exception e) { + // Ignore and fallback to defaults + setFormatter(new OneLineFormatter()); + } + } else { + setFormatter(new OneLineFormatter()); + } + + // Set error manager + setErrorManager(new ErrorManager()); + } + + + private String getProperty(String name, String defaultValue) { + String value = LogManager.getLogManager().getProperty(name); + if (value == null) { + value = defaultValue; + } else { + value = value.trim(); + } + return value; + } + + + /** + * Open the new log file for the date specified by date. + */ + public void open() { + openWriter(); + } + + protected void openWriter() { + + // Create the directory if necessary + File dir = new File(directory); + if (!dir.mkdirs() && !dir.isDirectory()) { + reportError("Unable to create [" + dir + "]", null, ErrorManager.OPEN_FAILURE); + writer = null; + return; + } + + // Open the current log file + writerLock.writeLock().lock(); + FileOutputStream fos = null; + OutputStream os = null; + try { + File pathname = new File(dir.getAbsoluteFile(), prefix + (rotatable.booleanValue() ? date : "") + suffix); + File parent = pathname.getParentFile(); + if (!parent.mkdirs() && !parent.isDirectory()) { + reportError("Unable to create [" + parent + "]", null, ErrorManager.OPEN_FAILURE); + writer = null; + return; + } + String encoding = getEncoding(); + fos = new FileOutputStream(pathname, true); + os = bufferSize.intValue() > 0 ? new BufferedOutputStream(fos, bufferSize.intValue()) : fos; + writer = new PrintWriter( + (encoding != null) ? new OutputStreamWriter(os, encoding) : new OutputStreamWriter(os), false); + writer.write(getFormatter().getHead(this)); + } catch (Exception e) { + reportError(null, e, ErrorManager.OPEN_FAILURE); + writer = null; + if (fos != null) { + try { + fos.close(); + } catch (IOException e1) { + // Ignore + } + } + if (os != null) { + try { + os.close(); + } catch (IOException e1) { + // Ignore + } + } + } finally { + writerLock.writeLock().unlock(); + } + } + + private void clean() { + if (maxDays.intValue() <= 0 || Files.notExists(getDirectoryAsPath())) { + return; + } + DELETE_FILES_SERVICE.submit(() -> { + try (DirectoryStream files = streamFilesForDelete()) { + for (Path file : files) { + Files.delete(file); + } + } catch (IOException e) { + reportError("Unable to delete log files older than [" + maxDays + "] days", null, + ErrorManager.GENERIC_FAILURE); + } + }); + } + + private DirectoryStream streamFilesForDelete() throws IOException { + LocalDate maxDaysOffset = LocalDate.now().minus(maxDays.intValue(), ChronoUnit.DAYS); + return Files.newDirectoryStream(getDirectoryAsPath(), path -> { + boolean result = false; + String date = obtainDateFromPath(path); + if (date != null) { + try { + LocalDate dateFromFile = LocalDate.from(DateTimeFormatter.ISO_LOCAL_DATE.parse(date)); + result = dateFromFile.isBefore(maxDaysOffset); + } catch (DateTimeException e) { + // no-op + } + } + return result; + }); + } + + private Path getDirectoryAsPath() { + return Path.of(directory); + } + + private String obtainDateFromPath(Path path) { + Path fileName = path.getFileName(); + if (fileName == null) { + return null; + } + String date = fileName.toString(); + if (pattern.matcher(date).matches()) { + date = date.substring(prefix.length()); + return date.substring(0, date.length() - suffix.length()); + } else { + return null; + } + } + + protected static final class ThreadFactory implements java.util.concurrent.ThreadFactory { + private final String namePrefix; + private final boolean isSecurityEnabled; + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + + public ThreadFactory(final String namePrefix) { + this.namePrefix = namePrefix; + SecurityManager s = System.getSecurityManager(); + if (s == null) { + this.isSecurityEnabled = false; + this.group = Thread.currentThread().getThreadGroup(); + } else { + this.isSecurityEnabled = true; + this.group = s.getThreadGroup(); + } + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement()); + // Threads should not have as context classloader a webapp classloader + if (isSecurityEnabled) { + AccessController.doPrivileged((PrivilegedAction) () -> { + t.setContextClassLoader(ThreadFactory.class.getClassLoader()); + return null; + }); + } else { + t.setContextClassLoader(ThreadFactory.class.getClassLoader()); + } + t.setDaemon(true); + return t; + } + } +} diff --git a/java/org/apache/juli/JdkLoggerFormatter.java b/java/org/apache/juli/JdkLoggerFormatter.java new file mode 100644 index 0000000..80489b9 --- /dev/null +++ b/java/org/apache/juli/JdkLoggerFormatter.java @@ -0,0 +1,120 @@ +/* + * 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.juli; + +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * A more compact formatter. Equivalent log4j config: + * + *
    + *  log4j.rootCategory=WARN, A1
    + *  log4j.appender.A1=org.apache.log4j.ConsoleAppender
    + *  log4j.appender.A1.layout=org.apache.log4j.PatternLayout
    + *  log4j.appender.A1.Target=System.err
    + *  log4j.appender.A1.layout.ConversionPattern=%r %-15.15c{2} %-1.1p %m %n
    + * 
    + * + * Example: 1130122891846 Http11BaseProtocol I Initializing Coyote HTTP/1.1 on http-8800 + * + * @author Costin Manolache + */ +public class JdkLoggerFormatter extends Formatter { + + // values from JDK Level + public static final int LOG_LEVEL_TRACE = 400; + public static final int LOG_LEVEL_DEBUG = 500; + public static final int LOG_LEVEL_INFO = 800; + public static final int LOG_LEVEL_WARN = 900; + public static final int LOG_LEVEL_ERROR = 1000; + public static final int LOG_LEVEL_FATAL = 1000; + + @Override + public String format(LogRecord record) { + Throwable t = record.getThrown(); + int level = record.getLevel().intValue(); + String name = record.getLoggerName(); + long time = record.getMillis(); + String message = formatMessage(record); + + + if (name.indexOf('.') >= 0) { + name = name.substring(name.lastIndexOf('.') + 1); + } + + // Use a string buffer for better performance + StringBuilder buf = new StringBuilder(); + + buf.append(time); + + // pad to 8 to make it more readable + for (int i = 0; i < 8 - buf.length(); i++) { + buf.append(' '); + } + + // Append a readable representation of the log level. + switch (level) { + case LOG_LEVEL_TRACE: + buf.append(" T "); + break; + case LOG_LEVEL_DEBUG: + buf.append(" D "); + break; + case LOG_LEVEL_INFO: + buf.append(" I "); + break; + case LOG_LEVEL_WARN: + buf.append(" W "); + break; + case LOG_LEVEL_ERROR: + buf.append(" E "); + break; + // case : buf.append(" F "); break; + default: + buf.append(" "); + } + + + // Append the name of the log instance if so configured + buf.append(name); + buf.append(' '); + + // pad to 20 chars + for (int i = 0; i < 8 - buf.length(); i++) { + buf.append(' '); + } + + // Append the message + buf.append(message); + + // Append stack trace if not null + if (t != null) { + buf.append(System.lineSeparator()); + + java.io.StringWriter sw = new java.io.StringWriter(1024); + java.io.PrintWriter pw = new java.io.PrintWriter(sw); + t.printStackTrace(pw); + pw.close(); + buf.append(sw.toString()); + } + + buf.append(System.lineSeparator()); + // Print to the appropriate destination + return buf.toString(); + } +} diff --git a/java/org/apache/juli/OneLineFormatter.java b/java/org/apache/juli/OneLineFormatter.java new file mode 100644 index 0000000..90e6101 --- /dev/null +++ b/java/org/apache/juli/OneLineFormatter.java @@ -0,0 +1,292 @@ +/* + * 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.juli; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Formatter; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; + +/** + * Provides same information as default log format but on a single line to make it easier to grep the logs. The only + * exception is stacktraces which are always preceded by whitespace to make it simple to skip them. + */ +/* + * Date processing based on AccessLogValve. + */ +public class OneLineFormatter extends Formatter { + + private static final String UNKNOWN_THREAD_NAME = "Unknown thread with ID "; + private static final Object threadMxBeanLock = new Object(); + private static volatile ThreadMXBean threadMxBean = null; + private static final int THREAD_NAME_CACHE_SIZE = 10000; + private static final ThreadLocal threadNameCache = ThreadLocal + .withInitial(() -> new ThreadNameCache(THREAD_NAME_CACHE_SIZE)); + + /* Timestamp format */ + private static final String DEFAULT_TIME_FORMAT = "dd-MMM-yyyy HH:mm:ss.SSS"; + + /** + * The size of our global date format cache + */ + private static final int globalCacheSize = 30; + + /** + * The size of our thread local date format cache + */ + private static final int localCacheSize = 5; + + /** + * Thread local date format cache. + */ + private ThreadLocal localDateCache; + + private volatile MillisHandling millisHandling = MillisHandling.APPEND; + + + public OneLineFormatter() { + String timeFormat = LogManager.getLogManager().getProperty(OneLineFormatter.class.getName() + ".timeFormat"); + if (timeFormat == null) { + timeFormat = DEFAULT_TIME_FORMAT; + } + setTimeFormat(timeFormat); + } + + + /** + * Specify the time format to use for time stamps in log messages. + * + * @param timeFormat The format to use using the {@link java.text.SimpleDateFormat} syntax + */ + public void setTimeFormat(final String timeFormat) { + final String cachedTimeFormat; + + if (timeFormat.endsWith(".SSS")) { + cachedTimeFormat = timeFormat.substring(0, timeFormat.length() - 4); + millisHandling = MillisHandling.APPEND; + } else if (timeFormat.contains("SSS")) { + millisHandling = MillisHandling.REPLACE_SSS; + cachedTimeFormat = timeFormat; + } else if (timeFormat.contains("SS")) { + millisHandling = MillisHandling.REPLACE_SS; + cachedTimeFormat = timeFormat; + } else if (timeFormat.contains("S")) { + millisHandling = MillisHandling.REPLACE_S; + cachedTimeFormat = timeFormat; + } else { + millisHandling = MillisHandling.NONE; + cachedTimeFormat = timeFormat; + } + + final DateFormatCache globalDateCache = new DateFormatCache(globalCacheSize, cachedTimeFormat, null); + localDateCache = ThreadLocal + .withInitial(() -> new DateFormatCache(localCacheSize, cachedTimeFormat, globalDateCache)); + } + + + /** + * Obtain the format currently being used for time stamps in log messages. + * + * @return The current format in {@link java.text.SimpleDateFormat} syntax + */ + public String getTimeFormat() { + return localDateCache.get().getTimeFormat(); + } + + + @Override + public String format(LogRecord record) { + StringBuilder sb = new StringBuilder(); + + // Timestamp + addTimestamp(sb, record.getMillis()); + + // Severity + sb.append(' '); + sb.append(record.getLevel().getLocalizedName()); + + // Thread + sb.append(' '); + sb.append('['); + final String threadName = Thread.currentThread().getName(); + if (threadName != null && threadName.startsWith(AsyncFileHandler.THREAD_PREFIX)) { + // If using the async handler can't get the thread name from the + // current thread. + sb.append(getThreadName(record.getThreadID())); + } else { + sb.append(threadName); + } + sb.append(']'); + + // Source + sb.append(' '); + sb.append(record.getSourceClassName()); + sb.append('.'); + sb.append(record.getSourceMethodName()); + + // Message + sb.append(' '); + sb.append(formatMessage(record)); + + // New line for next record + sb.append(System.lineSeparator()); + + // Stack trace + if (record.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new IndentingPrintWriter(sw); + record.getThrown().printStackTrace(pw); + pw.close(); + sb.append(sw.getBuffer()); + } + + return sb.toString(); + } + + protected void addTimestamp(StringBuilder buf, long timestamp) { + String cachedTimeStamp = localDateCache.get().getFormat(timestamp); + if (millisHandling == MillisHandling.NONE) { + buf.append(cachedTimeStamp); + } else if (millisHandling == MillisHandling.APPEND) { + buf.append(cachedTimeStamp); + long frac = timestamp % 1000; + buf.append('.'); + if (frac < 100) { + if (frac < 10) { + buf.append('0'); + buf.append('0'); + } else { + buf.append('0'); + } + } + buf.append(frac); + } else { + // Some version of replace + long frac = timestamp % 1000; + // Formatted string may vary in length so the insert point may vary + int insertStart = cachedTimeStamp.indexOf(DateFormatCache.MSEC_PATTERN); + buf.append(cachedTimeStamp.subSequence(0, insertStart)); + if (frac < 100 && millisHandling == MillisHandling.REPLACE_SSS) { + buf.append('0'); + if (frac < 10) { + buf.append('0'); + } + } else if (frac < 10 && millisHandling == MillisHandling.REPLACE_SS) { + buf.append('0'); + } + buf.append(frac); + if (millisHandling == MillisHandling.REPLACE_SSS) { + buf.append(cachedTimeStamp.substring(insertStart + 3)); + } else if (millisHandling == MillisHandling.REPLACE_SS) { + buf.append(cachedTimeStamp.substring(insertStart + 2)); + } else { + buf.append(cachedTimeStamp.substring(insertStart + 1)); + } + } + } + + + /** + * LogRecord has threadID but no thread name. LogRecord uses an int for thread ID but thread IDs are longs. If the + * real thread ID > (Integer.MAXVALUE / 2) LogRecord uses it's own ID in an effort to avoid clashes due to overflow. + *

    + * Words fail me to describe what I think of the design decision to use an int in LogRecord for a long value and the + * resulting mess that follows. + */ + private static String getThreadName(int logRecordThreadId) { + Map cache = threadNameCache.get(); + String result = cache.get(Integer.valueOf(logRecordThreadId)); + + if (result != null) { + return result; + } + + if (logRecordThreadId > Integer.MAX_VALUE / 2) { + result = UNKNOWN_THREAD_NAME + logRecordThreadId; + } else { + // Double checked locking OK as threadMxBean is volatile + if (threadMxBean == null) { + synchronized (threadMxBeanLock) { + if (threadMxBean == null) { + threadMxBean = ManagementFactory.getThreadMXBean(); + } + } + } + ThreadInfo threadInfo = threadMxBean.getThreadInfo(logRecordThreadId); + if (threadInfo == null) { + return Long.toString(logRecordThreadId); + } + result = threadInfo.getThreadName(); + } + + cache.put(Integer.valueOf(logRecordThreadId), result); + + return result; + } + + + /* + * This is an LRU cache. + */ + private static class ThreadNameCache extends LinkedHashMap { + + private static final long serialVersionUID = 1L; + + private final int cacheSize; + + ThreadNameCache(int cacheSize) { + super(cacheSize, 0.75f, true); + this.cacheSize = cacheSize; + } + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return (size() > cacheSize); + } + } + + + /* + * Minimal implementation to indent the printing of stack traces. This implementation depends on Throwable using + * WrappedPrintWriter. + */ + private static class IndentingPrintWriter extends PrintWriter { + + IndentingPrintWriter(Writer out) { + super(out); + } + + @Override + public void println(Object x) { + super.print('\t'); + super.println(x); + } + } + + + private enum MillisHandling { + NONE, APPEND, REPLACE_S, REPLACE_SS, REPLACE_SSS, + } +} diff --git a/java/org/apache/juli/VerbatimFormatter.java b/java/org/apache/juli/VerbatimFormatter.java new file mode 100644 index 0000000..1c90a0f --- /dev/null +++ b/java/org/apache/juli/VerbatimFormatter.java @@ -0,0 +1,35 @@ +/* + * 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.juli; + +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * Outputs the just the log message with no additional elements. Stack traces are not logged. Log messages are separated + * by System.lineSeparator(). This is intended for use by access logs and the like that need complete + * control over the output format. + */ +public class VerbatimFormatter extends Formatter { + + @Override + public String format(LogRecord record) { + // Timestamp + New line for next record + return record.getMessage() + System.lineSeparator(); + } + +} diff --git a/java/org/apache/juli/WebappProperties.java b/java/org/apache/juli/WebappProperties.java new file mode 100644 index 0000000..8948702 --- /dev/null +++ b/java/org/apache/juli/WebappProperties.java @@ -0,0 +1,57 @@ +/* + * 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.juli; + +/** + * An interface intended for use by class loaders associated with a web application that enables them to provide + * additional information to JULI about the web application with which they are associated. For any web application the + * combination of {@link #getWebappName()}, {@link #getHostName()} and {@link #getServiceName()} must be unique. + */ +public interface WebappProperties { + + /** + * Returns a name for the logging system to use for the web application, if any, associated with the class loader. + * + * @return The name to use for the web application or null if none is available. + */ + String getWebappName(); + + /** + * Returns a name for the logging system to use for the Host where the web application, if any, associated with the + * class loader is deployed. + * + * @return The name to use for the Host where the web application is deployed or null if none is available. + */ + String getHostName(); + + /** + * Returns a name for the logging system to use for the Service where the Host, if any, associated with the class + * loader is deployed. + * + * @return The name to use for the Service where the Host is deployed or null if none is available. + */ + String getServiceName(); + + /** + * Enables JULI to determine if the web application includes a local configuration without JULI having to look for + * the file which it may not have permission to do when running under a SecurityManager. + * + * @return {@code true} if the web application includes a logging configuration at the standard location of + * /WEB-INF/classes/logging.properties. + */ + boolean hasLoggingConfig(); +} diff --git a/java/org/apache/juli/logging/DirectJDKLog.java b/java/org/apache/juli/logging/DirectJDKLog.java new file mode 100644 index 0000000..90ca719 --- /dev/null +++ b/java/org/apache/juli/logging/DirectJDKLog.java @@ -0,0 +1,185 @@ +/* + * 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.juli.logging; + +import java.util.logging.ConsoleHandler; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Hard-coded java.util.logging commons-logging implementation. + */ +class DirectJDKLog implements Log { + // no reason to hide this - but good reasons to not hide + public final Logger logger; + + // Alternate config reader and console format + private static final String SIMPLE_FMT="java.util.logging.SimpleFormatter"; + private static final String FORMATTER="org.apache.juli.formatter"; + + static { + if (System.getProperty("java.util.logging.config.class") == null && + System.getProperty("java.util.logging.config.file") == null) { + // default configuration - it sucks. Let's override at least the + // formatter for the console + try { + Formatter fmt= (Formatter) Class.forName(System.getProperty( + FORMATTER, SIMPLE_FMT)).getConstructor().newInstance(); + // it is also possible that the user modified jre/lib/logging.properties - + // but that's really stupid in most cases + Logger root=Logger.getLogger(""); + for (Handler handler : root.getHandlers()) { + // I only care about console - that's what's used in default config anyway + if (handler instanceof ConsoleHandler) { + handler.setFormatter(fmt); + } + } + } catch (Throwable t) { + // maybe it wasn't included - the ugly default will be used. + } + + } + } + + DirectJDKLog(String name ) { + logger=Logger.getLogger(name); + } + + @Override + public final boolean isErrorEnabled() { + return logger.isLoggable(Level.SEVERE); + } + + @Override + public final boolean isWarnEnabled() { + return logger.isLoggable(Level.WARNING); + } + + @Override + public final boolean isInfoEnabled() { + return logger.isLoggable(Level.INFO); + } + + @Override + public final boolean isDebugEnabled() { + return logger.isLoggable(Level.FINE); + } + + @Override + public final boolean isFatalEnabled() { + return logger.isLoggable(Level.SEVERE); + } + + @Override + public final boolean isTraceEnabled() { + return logger.isLoggable(Level.FINER); + } + + @Override + public final void debug(Object message) { + log(Level.FINE, String.valueOf(message), null); + } + + @Override + public final void debug(Object message, Throwable t) { + log(Level.FINE, String.valueOf(message), t); + } + + @Override + public final void trace(Object message) { + log(Level.FINER, String.valueOf(message), null); + } + + @Override + public final void trace(Object message, Throwable t) { + log(Level.FINER, String.valueOf(message), t); + } + + @Override + public final void info(Object message) { + log(Level.INFO, String.valueOf(message), null); + } + + @Override + public final void info(Object message, Throwable t) { + log(Level.INFO, String.valueOf(message), t); + } + + @Override + public final void warn(Object message) { + log(Level.WARNING, String.valueOf(message), null); + } + + @Override + public final void warn(Object message, Throwable t) { + log(Level.WARNING, String.valueOf(message), t); + } + + @Override + public final void error(Object message) { + log(Level.SEVERE, String.valueOf(message), null); + } + + @Override + public final void error(Object message, Throwable t) { + log(Level.SEVERE, String.valueOf(message), t); + } + + @Override + public final void fatal(Object message) { + log(Level.SEVERE, String.valueOf(message), null); + } + + @Override + public final void fatal(Object message, Throwable t) { + log(Level.SEVERE, String.valueOf(message), t); + } + + // from commons logging. This would be my number one reason why java.util.logging + // is bad - design by committee can be really bad ! The impact on performance of + // using java.util.logging - and the ugliness if you need to wrap it - is far + // worse than the unfriendly and uncommon default format for logs. + + private void log(Level level, String msg, Throwable ex) { + if (logger.isLoggable(level)) { + // Hack (?) to get the stack trace. + Throwable dummyException=new Throwable(); + StackTraceElement locations[]=dummyException.getStackTrace(); + // Caller will be the third element + String cname = "unknown"; + String method = "unknown"; + if (locations != null && locations.length >2) { + StackTraceElement caller = locations[2]; + cname = caller.getClassName(); + method = caller.getMethodName(); + } + if (ex==null) { + logger.logp(level, cname, method, msg); + } else { + logger.logp(level, cname, method, msg, ex); + } + } + } + + static Log getInstance(String name) { + return new DirectJDKLog( name ); + } +} + + diff --git a/java/org/apache/juli/logging/Log.java b/java/org/apache/juli/logging/Log.java new file mode 100644 index 0000000..270eeee --- /dev/null +++ b/java/org/apache/juli/logging/Log.java @@ -0,0 +1,249 @@ +/* + * 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.juli.logging; + +/** + *

    A simple logging interface abstracting logging APIs. In order to be + * instantiated successfully by {@link LogFactory}, classes that implement + * this interface must have a constructor that takes a single String + * parameter representing the "name" of this Log.

    + * + *

    The six logging levels used by Log are (in order):

    + *
      + *
    1. trace (the least serious)
    2. + *
    3. debug
    4. + *
    5. info
    6. + *
    7. warn
    8. + *
    9. error
    10. + *
    11. fatal (the most serious)
    12. + *
    + *

    The mapping of these log levels to the concepts used by the underlying + * logging system is implementation dependent. + * The implementation should ensure, though, that this ordering behaves + * as expected.

    + * + *

    Performance is often a logging concern. + * By examining the appropriate property, + * a component can avoid expensive operations (producing information + * to be logged).

    + * + *

    For example, + * + * if (log.isDebugEnabled()) { + * ... do something expensive ... + * log.debug(theResult); + * } + * + *

    + * + *

    Configuration of the underlying logging system will generally be done + * external to the Logging APIs, through whatever mechanism is supported by + * that system.

    + * + * @author Scott Sanders + * @author Rod Waldhoff + */ +public interface Log { + + + // ----------------------------------------------------- Logging Properties + + + /** + *

    Is debug logging currently enabled?

    + * + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than debug.

    + * + * @return true if debug level logging is enabled, otherwise + * false + */ + boolean isDebugEnabled(); + + + /** + *

    Is error logging currently enabled?

    + * + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than error.

    + * + * @return true if error level logging is enabled, otherwise + * false + */ + boolean isErrorEnabled(); + + + /** + *

    Is fatal logging currently enabled?

    + * + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than fatal.

    + * + * @return true if fatal level logging is enabled, otherwise + * false + */ + boolean isFatalEnabled(); + + + /** + *

    Is info logging currently enabled?

    + * + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than info.

    + * + * @return true if info level logging is enabled, otherwise + * false + */ + boolean isInfoEnabled(); + + + /** + *

    Is trace logging currently enabled?

    + * + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than trace.

    + * + * @return true if trace level logging is enabled, otherwise + * false + */ + boolean isTraceEnabled(); + + + /** + *

    Is warn logging currently enabled?

    + * + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than warn.

    + * + * @return true if warn level logging is enabled, otherwise + * false + */ + boolean isWarnEnabled(); + + + // -------------------------------------------------------- Logging Methods + + + /** + *

    Log a message with trace log level.

    + * + * @param message log this message + */ + void trace(Object message); + + + /** + *

    Log an error with trace log level.

    + * + * @param message log this message + * @param t log this cause + */ + void trace(Object message, Throwable t); + + + /** + *

    Log a message with debug log level.

    + * + * @param message log this message + */ + void debug(Object message); + + + /** + *

    Log an error with debug log level.

    + * + * @param message log this message + * @param t log this cause + */ + void debug(Object message, Throwable t); + + + /** + *

    Log a message with info log level.

    + * + * @param message log this message + */ + void info(Object message); + + + /** + *

    Log an error with info log level.

    + * + * @param message log this message + * @param t log this cause + */ + void info(Object message, Throwable t); + + + /** + *

    Log a message with warn log level.

    + * + * @param message log this message + */ + void warn(Object message); + + + /** + *

    Log an error with warn log level.

    + * + * @param message log this message + * @param t log this cause + */ + void warn(Object message, Throwable t); + + + /** + *

    Log a message with error log level.

    + * + * @param message log this message + */ + void error(Object message); + + + /** + *

    Log an error with error log level.

    + * + * @param message log this message + * @param t log this cause + */ + void error(Object message, Throwable t); + + + /** + *

    Log a message with fatal log level.

    + * + * @param message log this message + */ + void fatal(Object message); + + + /** + *

    Log an error with fatal log level.

    + * + * @param message log this message + * @param t log this cause + */ + void fatal(Object message, Throwable t); + + +} diff --git a/java/org/apache/juli/logging/LogConfigurationException.java b/java/org/apache/juli/logging/LogConfigurationException.java new file mode 100644 index 0000000..2bc7d9f --- /dev/null +++ b/java/org/apache/juli/logging/LogConfigurationException.java @@ -0,0 +1,71 @@ +/* + * 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.juli.logging; + + +/** + *

    An exception that is thrown only if a suitable LogFactory + * or Log instance cannot be created by the corresponding + * factory methods.

    + * + * @author Craig R. McClanahan + */ +public class LogConfigurationException extends RuntimeException { + + + private static final long serialVersionUID = 1L; + + + /** + * Construct a new exception with null as its detail message. + */ + public LogConfigurationException() { + super(); + } + + + /** + * Construct a new exception with the specified detail message. + * + * @param message The detail message + */ + public LogConfigurationException(String message) { + super(message); + } + + + /** + * Construct a new exception with the specified cause and a derived + * detail message. + * + * @param cause The underlying cause + */ + public LogConfigurationException(Throwable cause) { + super(cause); + } + + + /** + * Construct a new exception with the specified detail message and cause. + * + * @param message The detail message + * @param cause The underlying cause + */ + public LogConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/org/apache/juli/logging/LogFactory.java b/java/org/apache/juli/logging/LogFactory.java new file mode 100644 index 0000000..bfc4238 --- /dev/null +++ b/java/org/apache/juli/logging/LogFactory.java @@ -0,0 +1,251 @@ +/* + * 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.juli.logging; + +import java.lang.reflect.Constructor; +import java.nio.file.FileSystems; +import java.util.ServiceLoader; +import java.util.logging.LogManager; + +import aQute.bnd.annotation.spi.ServiceConsumer; + +/** + * This is a modified LogFactory that uses a simple {@link ServiceLoader} based + * discovery mechanism with a default of using JDK based logging. An + * implementation that uses the full Commons Logging discovery mechanism is + * available as part of the Tomcat extras download. + * + * Why? It is an attempt to strike a balance between simpler code (no discovery) + * and providing flexibility - particularly for those projects that embed Tomcat + * or some of Tomcat's components - is an alternative logging + * implementation is desired. + * + * Note that this implementation is not just a wrapper around JDK logging (like + * the original commons-logging impl). It adds 2 features - a simpler + * configuration (which is in fact a subset of log4j.properties) and a + * formatter that is less ugly. + * + * The removal of 'abstract' preserves binary backward compatibility. It is + * possible to preserve the abstract - and introduce another (hardcoded) factory + * - but I see no benefit. + * + * Since this class is not intended to be extended - all protected methods are + * removed. This can be changed - but again, there is little value in keeping + * dead code. Just take a quick look at the removed code ( and it's complexity). + * + * -------------- + * + * Original comment: + *

    Factory for creating {@link Log} instances, with discovery and + * configuration features similar to that employed by standard Java APIs + * such as JAXP.

    + * + *

    IMPLEMENTATION NOTE - This implementation is heavily + * based on the SAXParserFactory and DocumentBuilderFactory implementations + * (corresponding to the JAXP pluggability APIs) found in Apache Xerces.

    + * + * + * @author Craig R. McClanahan + * @author Costin Manolache + * @author Richard A. Sitze + */ +@ServiceConsumer(value=Log.class) +public class LogFactory { + + private static final LogFactory singleton = new LogFactory(); + + private final Constructor discoveredLogConstructor; + + /** + * Private constructor that is not available for public use. + */ + private LogFactory() { + /* + * Work-around known a JRE bug. + * https://bugs.openjdk.java.net/browse/JDK-8194653 + * + * Pre-load the default file system. No performance impact as we need to + * load the default file system anyway. Just do it earlier to avoid the + * potential deadlock. + * + * This can be removed once the oldest JRE supported by Tomcat includes + * a fix. + */ + FileSystems.getDefault(); + + // Look via a ServiceLoader for a Log implementation that has a + // constructor taking the String name. + ServiceLoader logLoader = ServiceLoader.load(Log.class); + Constructor m=null; + for (Log log: logLoader) { + Class c=log.getClass(); + try { + m=c.getConstructor(String.class); + break; + } + catch (NoSuchMethodException | SecurityException e) { + throw new Error(e); + } + } + discoveredLogConstructor=m; + } + + + // --------------------------------------------------------- Public Methods + + // only those 2 methods need to change to use a different direct logger. + + /** + *

    Construct (if necessary) and return a Log instance, + * using the factory's current set of configuration attributes.

    + * + *

    NOTE - Depending upon the implementation of + * the LogFactory you are using, the Log + * instance you are returned may or may not be local to the current + * application, and may or may not be returned again on a subsequent + * call with the same name argument.

    + * + * @param name Logical name of the Log instance to be + * returned (the meaning of this name is only known to the underlying + * logging implementation that is being wrapped) + * + * @return A log instance with the requested name + * + * @exception LogConfigurationException if a suitable Log + * instance cannot be returned + */ + public Log getInstance(String name) throws LogConfigurationException { + if (discoveredLogConstructor == null) { + return DirectJDKLog.getInstance(name); + } + + try { + return discoveredLogConstructor.newInstance(name); + } catch (ReflectiveOperationException | IllegalArgumentException e) { + throw new LogConfigurationException(e); + } + } + + + /** + * Convenience method to derive a name from the specified class and + * call getInstance(String) with it. + * + * @param clazz Class for which a suitable Log name will be derived + * + * @return A log instance with a name of clazz.getName() + * + * @exception LogConfigurationException if a suitable Log + * instance cannot be returned + */ + public Log getInstance(Class clazz) throws LogConfigurationException { + return getInstance( clazz.getName()); + } + + + // ------------------------------------------------------- Static Variables + + + // --------------------------------------------------------- Static Methods + + + /** + *

    Construct (if necessary) and return a LogFactory + * instance, using the following ordered lookup procedure to determine + * the name of the implementation class to be loaded.

    + *
      + *
    • The org.apache.commons.logging.LogFactory system + * property.
    • + *
    • The JDK 1.3 Service Discovery mechanism
    • + *
    • Use the properties file commons-logging.properties + * file, if found in the class path of this class. The configuration + * file is in standard java.util.Properties format and + * contains the fully qualified name of the implementation class + * with the key being the system property defined above.
    • + *
    • Fall back to a default implementation class + * (org.apache.commons.logging.impl.LogFactoryImpl).
    • + *
    + * + *

    NOTE - If the properties file method of identifying the + * LogFactory implementation class is utilized, all of the + * properties defined in this file will be set as configuration attributes + * on the corresponding LogFactory instance.

    + * + * @return The singleton LogFactory instance + * + * @exception LogConfigurationException if the implementation class is not + * available or cannot be instantiated. + */ + public static LogFactory getFactory() throws LogConfigurationException { + return singleton; + } + + + /** + * Convenience method to return a named logger, without the application + * having to care about factories. + * + * @param clazz Class from which a log name will be derived + * + * @return A log instance with a name of clazz.getName() + * + * @exception LogConfigurationException if a suitable Log + * instance cannot be returned + */ + public static Log getLog(Class clazz) + throws LogConfigurationException { + return getFactory().getInstance(clazz); + } + + + /** + * Convenience method to return a named logger, without the application + * having to care about factories. + * + * @param name Logical name of the Log instance to be + * returned (the meaning of this name is only known to the underlying + * logging implementation that is being wrapped) + * + * @return A log instance with the requested name + * + * @exception LogConfigurationException if a suitable Log + * instance cannot be returned + */ + public static Log getLog(String name) + throws LogConfigurationException { + return getFactory().getInstance(name); + } + + + /** + * Release any internal references to previously created {@link LogFactory} + * instances that have been associated with the specified class loader + * (if any), after calling the instance method release() on + * each of them. + * + * @param classLoader ClassLoader for which to release the LogFactory + */ + public static void release(ClassLoader classLoader) { + // JULI's log manager looks at the current classLoader so there is no + // need to use the passed in classLoader, the default implementation + // does not so calling reset in that case will break things + if (!LogManager.getLogManager().getClass().getName().equals( + "java.util.logging.LogManager")) { + LogManager.getLogManager().reset(); + } + } +} diff --git a/java/org/apache/juli/logging/package.html b/java/org/apache/juli/logging/package.html new file mode 100644 index 0000000..99ab816 --- /dev/null +++ b/java/org/apache/juli/logging/package.html @@ -0,0 +1,37 @@ + + + +

    Overview

    + + +

    This implementation of commons-logging uses a commons-logging.jar + specific to a particular logging framework, instead of discovery. This takes +out the guessing, is simpler, faster and more robust. Just like you chose a +logging implementation, you should also use a matching commons-logging - for +example you download log4j.jar and commons-logging-log4j.jar, or use jdk +logging and use commons-logging-jdk.jar.

    + +

    A similar packaging is used by Eclipse SWT - they provide a common widget API, + but each platform uses a different implementation jar - instead of using a complex + discovery/plugin mechanism. +

    + +

    This package generates commons-logging-jdk14.jar - i.e. the java.util implementation +of commons-logging api.

    + + diff --git a/java/org/apache/naming/AbstractRef.java b/java/org/apache/naming/AbstractRef.java new file mode 100644 index 0000000..e9a657b --- /dev/null +++ b/java/org/apache/naming/AbstractRef.java @@ -0,0 +1,87 @@ +/* + * 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.naming; + +import java.util.Enumeration; + +import javax.naming.Context; +import javax.naming.RefAddr; +import javax.naming.Reference; + +public abstract class AbstractRef extends Reference { + + private static final long serialVersionUID = 1L; + + + public AbstractRef(String className) { + super(className); + } + + + public AbstractRef(String className, String factory, String factoryLocation) { + super(className, factory, factoryLocation); + } + + + /** + * Retrieves the class name of the factory of the object to which this + * reference refers. + */ + @Override + public final String getFactoryClassName() { + String factory = super.getFactoryClassName(); + if (factory != null) { + return factory; + } else { + factory = System.getProperty(Context.OBJECT_FACTORIES); + if (factory != null) { + return null; + } else { + return getDefaultFactoryClassName(); + } + } + } + + + protected abstract String getDefaultFactoryClassName(); + + + /** + * Return a String rendering of this object. + */ + @Override + public final String toString() { + StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()); + sb.append("[className="); + sb.append(getClassName()); + sb.append(",factoryClassLocation="); + sb.append(getFactoryClassLocation()); + sb.append(",factoryClassName="); + sb.append(getFactoryClassName()); + Enumeration refAddrs = getAll(); + while (refAddrs.hasMoreElements()) { + RefAddr refAddr = refAddrs.nextElement(); + sb.append(",{type="); + sb.append(refAddr.getType()); + sb.append(",content="); + sb.append(refAddr.getContent()); + sb.append('}'); + } + sb.append(']'); + return sb.toString(); + } +} diff --git a/java/org/apache/naming/ContextAccessController.java b/java/org/apache/naming/ContextAccessController.java new file mode 100644 index 0000000..0fad089 --- /dev/null +++ b/java/org/apache/naming/ContextAccessController.java @@ -0,0 +1,128 @@ +/* + * 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.naming; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Handles the access control on the JNDI contexts. + * + * @author Remy Maucherat + */ +public class ContextAccessController { + + // -------------------------------------------------------------- Variables + + /** + * Catalina context names on which writing is not allowed. + */ + private static final Map readOnlyContexts = new ConcurrentHashMap<>(); + + + /** + * Security tokens repository. + */ + private static final Map securityTokens = new ConcurrentHashMap<>(); + + + // --------------------------------------------------------- Public Methods + + /** + * Set a security token for a Catalina context. Can be set only once. + * + * @param name Name of the Catalina context + * @param token Security token + */ + public static void setSecurityToken(Object name, Object token) { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new RuntimePermission( + ContextAccessController.class.getName() + + ".setSecurityToken")); + } + if ((!securityTokens.containsKey(name)) && (token != null)) { + securityTokens.put(name, token); + } + } + + + /** + * Remove a security token for a context. + * + * @param name Name of the Catalina context + * @param token Security token + */ + public static void unsetSecurityToken(Object name, Object token) { + if (checkSecurityToken(name, token)) { + securityTokens.remove(name); + } + } + + + /** + * Check a submitted security token. + * + * @param name Name of the Catalina context + * @param token Submitted security token + * + * @return true if the submitted token is equal to the token + * in the repository or if no token is present in the repository. + * Otherwise, false + */ + public static boolean checkSecurityToken + (Object name, Object token) { + Object refToken = securityTokens.get(name); + return (refToken == null || refToken.equals(token)); + } + + + /** + * Allow writing to a context. + * + * @param name Name of the Catalina context + * @param token Security token + */ + public static void setWritable(Object name, Object token) { + if (checkSecurityToken(name, token)) { + readOnlyContexts.remove(name); + } + } + + + /** + * Set whether or not a Catalina context is writable. + * + * @param name Name of the Catalina context + */ + public static void setReadOnly(Object name) { + readOnlyContexts.put(name, name); + } + + + /** + * Is the context is writable? + * + * @param name Name of the Catalina context + * + * @return true if it is writable, otherwise false + */ + public static boolean isWritable(Object name) { + return !(readOnlyContexts.containsKey(name)); + } +} + diff --git a/java/org/apache/naming/ContextBindings.java b/java/org/apache/naming/ContextBindings.java new file mode 100644 index 0000000..9c4895a --- /dev/null +++ b/java/org/apache/naming/ContextBindings.java @@ -0,0 +1,308 @@ +/* + * 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.naming; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.naming.Context; +import javax.naming.NamingException; + +/** + * Handles the associations : + *
      + *
    • Object with a NamingContext
    • + *
    • Calling thread with a NamingContext
    • + *
    • Calling thread with object bound to the same naming context
    • + *
    • Thread context class loader with a NamingContext
    • + *
    • Thread context class loader with object bound to the same + * NamingContext
    • + *
    + * The objects are typically Catalina Server or Context objects. + * + * @author Remy Maucherat + */ +public class ContextBindings { + + // -------------------------------------------------------------- Variables + + /** + * Bindings object - naming context. Keyed by object. + */ + private static final Map objectBindings = new ConcurrentHashMap<>(); + + + /** + * Bindings thread - naming context. Keyed by thread. + */ + private static final Map threadBindings = new ConcurrentHashMap<>(); + + + /** + * Bindings thread - object. Keyed by thread. + */ + private static final Map threadObjectBindings = new ConcurrentHashMap<>(); + + + /** + * Bindings class loader - naming context. Keyed by class loader. + */ + private static final Map clBindings = new ConcurrentHashMap<>(); + + + /** + * Bindings class loader - object. Keyed by class loader. + */ + private static final Map clObjectBindings = new ConcurrentHashMap<>(); + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(ContextBindings.class); + + + // --------------------------------------------------------- Public Methods + + /** + * Binds an object and a naming context. + * + * @param obj Object to bind with naming context + * @param context Associated naming context instance + */ + public static void bindContext(Object obj, Context context) { + bindContext(obj, context, null); + } + + + /** + * Binds an object and a naming context. + * + * @param obj Object to bind with naming context + * @param context Associated naming context instance + * @param token Security token + */ + public static void bindContext(Object obj, Context context, Object token) { + if (ContextAccessController.checkSecurityToken(obj, token)) { + objectBindings.put(obj, context); + } + } + + + /** + * Unbinds an object and a naming context. + * + * @param obj Object to unbind + * @param token Security token + */ + public static void unbindContext(Object obj, Object token) { + if (ContextAccessController.checkSecurityToken(obj, token)) { + objectBindings.remove(obj); + } + } + + + /** + * Retrieve a naming context. + * + * @param obj Object bound to the required naming context + */ + static Context getContext(Object obj) { + return objectBindings.get(obj); + } + + + /** + * Binds a naming context to a thread. + * + * @param obj Object bound to the required naming context + * @param token Security token + * + * @throws NamingException If no naming context is bound to the provided + * object + */ + public static void bindThread(Object obj, Object token) throws NamingException { + if (ContextAccessController.checkSecurityToken(obj, token)) { + Context context = objectBindings.get(obj); + if (context == null) { + throw new NamingException( + sm.getString("contextBindings.unknownContext", obj)); + } + Thread currentThread = Thread.currentThread(); + threadBindings.put(currentThread, context); + threadObjectBindings.put(currentThread, obj); + } + } + + + /** + * Unbinds a thread and a naming context. + * + * @param obj Object bound to the required naming context + * @param token Security token + */ + public static void unbindThread(Object obj, Object token) { + if (ContextAccessController.checkSecurityToken(obj, token)) { + Thread currentThread = Thread.currentThread(); + threadBindings.remove(currentThread); + threadObjectBindings.remove(currentThread); + } + } + + + /** + * Retrieves the naming context bound to the current thread. + * + * @return The naming context bound to the current thread. + * + * @throws NamingException If no naming context is bound to the current + * thread + */ + public static Context getThread() throws NamingException { + Context context = threadBindings.get(Thread.currentThread()); + if (context == null) { + throw new NamingException + (sm.getString("contextBindings.noContextBoundToThread")); + } + return context; + } + + + /** + * Retrieves the name of the object bound to the naming context that is also + * bound to the current thread. + */ + static String getThreadName() throws NamingException { + Object obj = threadObjectBindings.get(Thread.currentThread()); + if (obj == null) { + throw new NamingException + (sm.getString("contextBindings.noContextBoundToThread")); + } + return obj.toString(); + } + + + /** + * Tests if current thread is bound to a naming context. + * + * @return true if the current thread is bound to a naming + * context, otherwise false + */ + public static boolean isThreadBound() { + return threadBindings.containsKey(Thread.currentThread()); + } + + + /** + * Binds a naming context to a class loader. + * + * @param obj Object bound to the required naming context + * @param token Security token + * @param classLoader The class loader to bind to the naming context + * + * @throws NamingException If no naming context is bound to the provided + * object + */ + public static void bindClassLoader(Object obj, Object token, + ClassLoader classLoader) throws NamingException { + if (ContextAccessController.checkSecurityToken(obj, token)) { + Context context = objectBindings.get(obj); + if (context == null) { + throw new NamingException + (sm.getString("contextBindings.unknownContext", obj)); + } + clBindings.put(classLoader, context); + clObjectBindings.put(classLoader, obj); + } + } + + + /** + * Unbinds a naming context and a class loader. + * + * @param obj Object bound to the required naming context + * @param token Security token + * @param classLoader The class loader bound to the naming context + */ + public static void unbindClassLoader(Object obj, Object token, + ClassLoader classLoader) { + if (ContextAccessController.checkSecurityToken(obj, token)) { + Object o = clObjectBindings.get(classLoader); + if (o == null || !o.equals(obj)) { + return; + } + clBindings.remove(classLoader); + clObjectBindings.remove(classLoader); + } + } + + + /** + * Retrieves the naming context bound to a class loader. + * + * @return the naming context bound to current class loader or one of its + * parents + * + * @throws NamingException If no naming context was bound + */ + public static Context getClassLoader() throws NamingException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Context context = null; + do { + context = clBindings.get(cl); + if (context != null) { + return context; + } + } while ((cl = cl.getParent()) != null); + throw new NamingException(sm.getString("contextBindings.noContextBoundToCL")); + } + + + /** + * Retrieves the name of the object bound to the naming context that is also + * bound to the thread context class loader. + */ + static String getClassLoaderName() throws NamingException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Object obj = null; + do { + obj = clObjectBindings.get(cl); + if (obj != null) { + return obj.toString(); + } + } while ((cl = cl.getParent()) != null); + throw new NamingException (sm.getString("contextBindings.noContextBoundToCL")); + } + + + /** + * Tests if the thread context class loader is bound to a context. + * + * @return true if the thread context class loader or one of + * its parents is bound to a naming context, otherwise + * false + */ + public static boolean isClassLoaderBound() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + do { + if (clBindings.containsKey(cl)) { + return true; + } + } while ((cl = cl.getParent()) != null); + return false; + } +} diff --git a/java/org/apache/naming/EjbRef.java b/java/org/apache/naming/EjbRef.java new file mode 100644 index 0000000..55e43d9 --- /dev/null +++ b/java/org/apache/naming/EjbRef.java @@ -0,0 +1,103 @@ +/* + * 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.naming; + +import javax.naming.StringRefAddr; + +/** + * Represents a reference address to an EJB. + * + * @author Remy Maucherat + */ +public class EjbRef extends AbstractRef { + + private static final long serialVersionUID = 1L; + + + /** + * Default factory for this reference. + */ + public static final String DEFAULT_FACTORY = + org.apache.naming.factory.Constants.DEFAULT_EJB_FACTORY; + + + /** + * EJB type address type. + */ + public static final String TYPE = "type"; + + + /** + * Remote interface classname address type. + */ + public static final String REMOTE = "remote"; + + + /** + * Link address type. + */ + public static final String LINK = "link"; + + + /** + * EJB Reference. + * + * @param ejbType EJB type + * @param home Home interface classname + * @param remote Remote interface classname + * @param link EJB link + */ + public EjbRef(String ejbType, String home, String remote, String link) { + this(ejbType, home, remote, link, null, null); + } + + + /** + * EJB Reference. + * + * @param ejbType EJB type + * @param home Home interface classname + * @param remote Remote interface classname + * @param link EJB link + * @param factory The possibly null class name of the object's factory. + * @param factoryLocation The possibly null location from which to load + * the factory (e.g. URL) + */ + public EjbRef(String ejbType, String home, String remote, String link, + String factory, String factoryLocation) { + super(home, factory, factoryLocation); + StringRefAddr refAddr = null; + if (ejbType != null) { + refAddr = new StringRefAddr(TYPE, ejbType); + add(refAddr); + } + if (remote != null) { + refAddr = new StringRefAddr(REMOTE, remote); + add(refAddr); + } + if (link != null) { + refAddr = new StringRefAddr(LINK, link); + add(refAddr); + } + } + + + @Override + protected String getDefaultFactoryClassName() { + return DEFAULT_FACTORY; + } +} diff --git a/java/org/apache/naming/HandlerRef.java b/java/org/apache/naming/HandlerRef.java new file mode 100644 index 0000000..1c7f1c1 --- /dev/null +++ b/java/org/apache/naming/HandlerRef.java @@ -0,0 +1,110 @@ +/* + * 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.naming; + +import javax.naming.StringRefAddr; + +/** + * Represents a reference handler for a web service. + * + * @author Fabien Carrion + */ +public class HandlerRef extends AbstractRef { + + private static final long serialVersionUID = 1L; + + + /** + * Default factory for this reference. + */ + public static final String DEFAULT_FACTORY = + org.apache.naming.factory.Constants.DEFAULT_HANDLER_FACTORY; + + + /** + * HandlerName address type. + */ + public static final String HANDLER_NAME = "handlername"; + + + /** + * Handler Classname address type. + */ + public static final String HANDLER_CLASS = "handlerclass"; + + + /** + * Handler Classname address type. + */ + public static final String HANDLER_LOCALPART = "handlerlocalpart"; + + + /** + * Handler Classname address type. + */ + public static final String HANDLER_NAMESPACE = "handlernamespace"; + + + /** + * Handler Classname address type. + */ + public static final String HANDLER_PARAMNAME = "handlerparamname"; + + + /** + * Handler Classname address type. + */ + public static final String HANDLER_PARAMVALUE = "handlerparamvalue"; + + + /** + * Handler SoapRole address type. + */ + public static final String HANDLER_SOAPROLE = "handlersoaprole"; + + + /** + * Handler PortName address type. + */ + public static final String HANDLER_PORTNAME = "handlerportname"; + + + public HandlerRef(String refname, String handlerClass) { + this(refname, handlerClass, null, null); + } + + + public HandlerRef(String refname, String handlerClass, + String factory, String factoryLocation) { + super(refname, factory, factoryLocation); + StringRefAddr refAddr = null; + if (refname != null) { + refAddr = new StringRefAddr(HANDLER_NAME, refname); + add(refAddr); + } + if (handlerClass != null) { + refAddr = new StringRefAddr(HANDLER_CLASS, handlerClass); + add(refAddr); + } + } + + + @Override + protected String getDefaultFactoryClassName() { + return DEFAULT_FACTORY; + } +} diff --git a/java/org/apache/naming/LocalStrings.properties b/java/org/apache/naming/LocalStrings.properties new file mode 100644 index 0000000..952aaae --- /dev/null +++ b/java/org/apache/naming/LocalStrings.properties @@ -0,0 +1,30 @@ +# 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. + +contextBindings.noContextBoundToCL=No naming context bound to this class loader +contextBindings.noContextBoundToThread=No naming context bound to this thread +contextBindings.unknownContext=Unknown context name : [{0}] + +namingContext.alreadyBound=Name [{0}] is already bound in this Context +namingContext.contextExpected=Name is not bound to a Context +namingContext.failResolvingReference=Unexpected exception resolving reference +namingContext.invalidName=Name is not valid +namingContext.nameNotBound=Name [{0}] is not bound in this Context. Unable to find [{1}]. +namingContext.noAbsoluteName=Cannot generate an absolute name for this namespace +namingContext.readOnly=Context is read only + +selectorContext.methodUsingName=Call to method [{0}] with a Name of [{1}] +selectorContext.methodUsingString=Call to method [{0}] with a String of [{1}] +selectorContext.noJavaUrl=This context must be accessed through a java: URL diff --git a/java/org/apache/naming/LocalStrings_cs.properties b/java/org/apache/naming/LocalStrings_cs.properties new file mode 100644 index 0000000..e83eb63 --- /dev/null +++ b/java/org/apache/naming/LocalStrings_cs.properties @@ -0,0 +1,16 @@ +# 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. + +namingContext.contextExpected=Jméno není svázáno s kontextem diff --git a/java/org/apache/naming/LocalStrings_de.properties b/java/org/apache/naming/LocalStrings_de.properties new file mode 100644 index 0000000..a457a55 --- /dev/null +++ b/java/org/apache/naming/LocalStrings_de.properties @@ -0,0 +1,21 @@ +# 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. + +contextBindings.unknownContext=Unbekannter Kontext-Name: [{0}] + +namingContext.contextExpected=Ein Name ist nicht an den Context gebunden + +selectorContext.methodUsingName=Aufruf der Methode [{0}] mit Namen [{1}] +selectorContext.noJavaUrl=Auf diesen Kontext muss durch eine java:-URL zugegriffen werden diff --git a/java/org/apache/naming/LocalStrings_es.properties b/java/org/apache/naming/LocalStrings_es.properties new file mode 100644 index 0000000..72e75d6 --- /dev/null +++ b/java/org/apache/naming/LocalStrings_es.properties @@ -0,0 +1,30 @@ +# 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. + +contextBindings.noContextBoundToCL=No hay contexto de nombres asociado a este cargador de clase +contextBindings.noContextBoundToThread=No hay contexto de nombres asociado a este hilo +contextBindings.unknownContext=Contexto [{0}] desconocido + +namingContext.alreadyBound=El nombre [{0}] este ya asociado en este Contexto +namingContext.contextExpected=El nombre no esta asociado a ningun Contexto +namingContext.failResolvingReference=Excepción inesperada resolviendo referencia +namingContext.invalidName=Nombre no valido +namingContext.nameNotBound=El nombre [{0}] no este asociado a este contexto +namingContext.noAbsoluteName=No se puede generar un nombre absoluto para este espacio de nombres +namingContext.readOnly=El contexto es de solo lectura + +selectorContext.methodUsingName=Llamada al método [{0}] con un Nombre de [{1}] +selectorContext.methodUsingString=Llamada al método [{0}] con una Cadena de [{1}] +selectorContext.noJavaUrl=Este contexto debe de ser accedido a traves de una URL de tipo java: diff --git a/java/org/apache/naming/LocalStrings_fr.properties b/java/org/apache/naming/LocalStrings_fr.properties new file mode 100644 index 0000000..eb297c8 --- /dev/null +++ b/java/org/apache/naming/LocalStrings_fr.properties @@ -0,0 +1,30 @@ +# 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. + +contextBindings.noContextBoundToCL=Aucun Contexte de nommage lié à ce chargeur de classes +contextBindings.noContextBoundToThread=Aucun Contexte de nommage lié à ce thread +contextBindings.unknownContext=Nom de Contexte inconnu : [{0}] + +namingContext.alreadyBound=Le Nom [{0}] est déjà lié à ce Contexte +namingContext.contextExpected=Le Nom n'est pas lié à un Contexte +namingContext.failResolvingReference=Une erreur s est produite durant la résolution de la référence +namingContext.invalidName=Le Nom est invalide +namingContext.nameNotBound=Le Nom [{0}] n''est pas lié à ce Contexte +namingContext.noAbsoluteName=Impossible de générer un nom absolu pour cet espace de nommage (namespace) +namingContext.readOnly=Le Contexte est en lecture seule + +selectorContext.methodUsingName=Appel de la méthode [{0}] avec le nom [{1}] +selectorContext.methodUsingString=Appel de la méthode [{0}] avec la String [{1}] +selectorContext.noJavaUrl=Ce Contexte doit être accédé par une URL commençant par 'java :' diff --git a/java/org/apache/naming/LocalStrings_ja.properties b/java/org/apache/naming/LocalStrings_ja.properties new file mode 100644 index 0000000..95ccde2 --- /dev/null +++ b/java/org/apache/naming/LocalStrings_ja.properties @@ -0,0 +1,30 @@ +# 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. + +contextBindings.noContextBoundToCL=Naming Contextã¯ã“ã®ã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ã«ãƒã‚¤ãƒ³ãƒ‰ã•ã‚Œã¦ã„ã¾ã›ã‚“ +contextBindings.noContextBoundToThread=åå‰ä»˜ãコンテキストã¯ã“ã®ã‚¹ãƒ¬ãƒƒãƒ‰ã«ãƒã‚¤ãƒ³ãƒ‰ã•ã‚Œã¦ã„ã¾ã›ã‚“ +contextBindings.unknownContext=未知ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆåã§ã™: [{0}] + +namingContext.alreadyBound=åå‰ [{0}] ã¯æ—¢ã«ã“ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã«ãƒã‚¤ãƒ³ãƒ‰ã•ã‚Œã¦ã„ã¾ã™ +namingContext.contextExpected=åå‰ãŒã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã«ãƒã‚¤ãƒ³ãƒ‰ã•ã‚Œã¦ã„ã¾ã›ã‚“ +namingContext.failResolvingReference=å‚ç…§ã®è§£æ±ºä¸­ã«äºˆæ¸¬ã—ãªã„例外ãŒç™ºç”Ÿã—ã¾ã—㟠+namingContext.invalidName=åå‰ã¯ç„¡åŠ¹ã§ã™ +namingContext.nameNotBound=åå‰ [{0}] ã¯ã“ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã«ãƒã‚¤ãƒ³ãƒ‰ã•ã‚Œã¦ã„ã¾ã›ã‚“ +namingContext.noAbsoluteName=ã“ã®åå‰ç©ºé–“ã«çµ¶å¯¾åを生æˆã§ãã¾ã›ã‚“ +namingContext.readOnly=コンテキストã¯ãƒªãƒ¼ãƒ‰ã‚ªãƒ³ãƒªãƒ¼ã§ã™ + +selectorContext.methodUsingName=オブジェクトå [{1}] ã«å¯¾ã—ã¦ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] を呼ã³å‡ºã—ã¾ã™ã€‚ +selectorContext.methodUsingString=メソッド [{0}] ã‚’ [{1}] ã®æ–‡å­—列ã§å‘¼ã³å‡ºã—ã¾ã™ +selectorContext.noJavaUrl=ã“ã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã«ã¯java: URLを用ã„ã¦ã‚¢ã‚¯ã‚»ã‚¹ã•ã‚Œã­ã°ã„ã‘ã¾ã›ã‚“ diff --git a/java/org/apache/naming/LocalStrings_ko.properties b/java/org/apache/naming/LocalStrings_ko.properties new file mode 100644 index 0000000..0404c1b --- /dev/null +++ b/java/org/apache/naming/LocalStrings_ko.properties @@ -0,0 +1,30 @@ +# 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. + +contextBindings.noContextBoundToCL=Naming 컨í…스트가 ì´ í´ëž˜ìŠ¤ë¡œë”ì— ë°”ì¸ë”©ë˜ì§€ 않았습니다. +contextBindings.noContextBoundToThread=ì´ ì“°ë ˆë“œì— Naming 컨í…스트가 ë°”ì¸ë”©ë˜ì§€ 않았습니다. +contextBindings.unknownContext=ì•Œ 수 없는 컨í…스트 ì´ë¦„: [{0}] + +namingContext.alreadyBound=Name [{0}]ì´(ê°€) ì´ë¯¸ ì´ ì»¨í…ìŠ¤íŠ¸ì— ë°”ì¸ë”© ë˜ì–´ 있습니다. +namingContext.contextExpected=Nameì´ ì»¨í…ìŠ¤íŠ¸ì— ë°”ì¸ë”© ë˜ì§€ 않았습니다. +namingContext.failResolvingReference=참조를 결정하는 중 예기치 ì•Šì€ ì˜ˆì™¸ ë°œìƒ +namingContext.invalidName=Nameì´ ìœ íš¨í•˜ì§€ 않습니다. +namingContext.nameNotBound=Name [{0}]ì€(는) ì´ ì»¨í…ìŠ¤íŠ¸ì— ë°”ì¸ë”©ë˜ì§€ 않았습니다. [{1}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +namingContext.noAbsoluteName=ì´ ë„¤ìž„ìŠ¤íŽ˜ì´ìŠ¤ë¥¼ 위한 절대 ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +namingContext.readOnly=컨í…스트가 ì½ê¸° 전용입니다. + +selectorContext.methodUsingName=Name [{1}]ì„(를) 사용하여 메소드 [{0}]ì„(를) 호출합니다. +selectorContext.methodUsingString=문ìžì—´ [{1}]ì„(를) 사용하여 메소드 [{0}]ì„(를) 호출합니다. +selectorContext.noJavaUrl=ì´ ì»¨í…스트는 반드시 java: URLì„ í†µí•´ ì ‘ê·¼ë˜ì–´ì•¼ 합니다. diff --git a/java/org/apache/naming/LocalStrings_ru.properties b/java/org/apache/naming/LocalStrings_ru.properties new file mode 100644 index 0000000..2570558 --- /dev/null +++ b/java/org/apache/naming/LocalStrings_ru.properties @@ -0,0 +1,19 @@ +# 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. + +namingContext.contextExpected=Ð˜Ð¼Ñ Ð½Ðµ привÑзано к контекÑту + +selectorContext.methodUsingName=Вызов метода [{0}] Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ [{1}] +selectorContext.methodUsingString=Вызов метода [{0}] Ð´Ð»Ñ Ñтроки [{1}] diff --git a/java/org/apache/naming/LocalStrings_zh_CN.properties b/java/org/apache/naming/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..822482f --- /dev/null +++ b/java/org/apache/naming/LocalStrings_zh_CN.properties @@ -0,0 +1,30 @@ +# 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. + +contextBindings.noContextBoundToCL=没有绑定到此类加载器的命å上下文 +contextBindings.noContextBoundToThread=没有绑定到此线程的命å上下文 +contextBindings.unknownContext=未知.上下文å:[{0}] + +namingContext.alreadyBound=å称[{0}]已在此上下文中绑定 +namingContext.contextExpected=上下文Context未绑定å称name +namingContext.failResolvingReference=解æžå¼•ç”¨æ—¶æ„外异常 +namingContext.invalidName=å称无效 +namingContext.nameNotBound=å称[{0}]未在此上下文中绑定。找ä¸åˆ°[{1}]。 +namingContext.noAbsoluteName=无法为此命å空间生æˆç»å¯¹å称 +namingContext.readOnly=上下文是åªè¯»çš„ + +selectorContext.methodUsingName=用[{1}]çš„name属性调用方法[{0}] +selectorContext.methodUsingString=使用字符串[{1}]调用方法[{0}] +selectorContext.noJavaUrl=必须通过java:url访问此上下文 diff --git a/java/org/apache/naming/LookupRef.java b/java/org/apache/naming/LookupRef.java new file mode 100644 index 0000000..116a16a --- /dev/null +++ b/java/org/apache/naming/LookupRef.java @@ -0,0 +1,53 @@ +/* + * 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.naming; + +import javax.naming.RefAddr; +import javax.naming.StringRefAddr; + +/** + * Represents a reference to lookup. + */ +public class LookupRef extends AbstractRef { + + private static final long serialVersionUID = 1L; + + /** + * JNDI name for the lookup + */ + public static final String LOOKUP_NAME = "lookup-name"; + + + public LookupRef(String resourceType, String lookupName) { + this(resourceType, null, null, lookupName); + } + + + public LookupRef(String resourceType, String factory, String factoryLocation, String lookupName) { + super(resourceType, factory, factoryLocation); + if (lookupName != null && !lookupName.equals("")) { + RefAddr ref = new StringRefAddr(LOOKUP_NAME, lookupName); + add(ref); + } + } + + + @Override + protected String getDefaultFactoryClassName() { + return org.apache.naming.factory.Constants.DEFAULT_LOOKUP_JNDI_FACTORY; + } +} diff --git a/java/org/apache/naming/NameParserImpl.java b/java/org/apache/naming/NameParserImpl.java new file mode 100644 index 0000000..9b6e4d3 --- /dev/null +++ b/java/org/apache/naming/NameParserImpl.java @@ -0,0 +1,54 @@ +/* + * 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.naming; + +import javax.naming.CompositeName; +import javax.naming.Name; +import javax.naming.NameParser; +import javax.naming.NamingException; + +/** + * Parses names. + * + * @author Remy Maucherat + */ +public class NameParserImpl + implements NameParser { + + + // ----------------------------------------------------- Instance Variables + + + // ----------------------------------------------------- NameParser Methods + + + /** + * Parses a name into its components. + * + * @param name The non-null string name to parse + * @return A non-null parsed form of the name using the naming convention + * of this parser. + */ + @Override + public Name parse(String name) + throws NamingException { + return new CompositeName(name); + } + + +} + diff --git a/java/org/apache/naming/NamingContext.java b/java/org/apache/naming/NamingContext.java new file mode 100644 index 0000000..2cae5f1 --- /dev/null +++ b/java/org/apache/naming/NamingContext.java @@ -0,0 +1,1009 @@ +/* + * 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.naming; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; + +import javax.naming.Binding; +import javax.naming.CompositeName; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.LinkRef; +import javax.naming.Name; +import javax.naming.NameAlreadyBoundException; +import javax.naming.NameClassPair; +import javax.naming.NameNotFoundException; +import javax.naming.NameParser; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.NotContextException; +import javax.naming.OperationNotSupportedException; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.naming.spi.NamingManager; +import javax.naming.spi.ObjectFactory; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Catalina JNDI Context implementation. + * + * @author Remy Maucherat + */ +public class NamingContext implements Context { + + + // -------------------------------------------------------------- Constants + + + /** + * Name parser for this context. + */ + protected static final NameParser nameParser = new NameParserImpl(); + + + private static final Log log = LogFactory.getLog(NamingContext.class); + + + // ----------------------------------------------------------- Constructors + + + /** + * Builds a naming context. + * + * @param env The environment to use to construct the naming context + * @param name The name of the associated Catalina Context + */ + public NamingContext(Hashtable env, String name) { + this(env, name, new HashMap<>()); + } + + + /** + * Builds a naming context. + * + * @param env The environment to use to construct the naming context + * @param name The name of the associated Catalina Context + * @param bindings The initial bindings for the naming context + */ + public NamingContext(Hashtable env, String name, + HashMap bindings) { + + this.env = new Hashtable<>(); + this.name = name; + // Populating the environment hashtable + if (env != null ) { + Enumeration envEntries = env.keys(); + while (envEntries.hasMoreElements()) { + String entryName = envEntries.nextElement(); + addToEnvironment(entryName, env.get(entryName)); + } + } + this.bindings = bindings; + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Environment. + */ + protected final Hashtable env; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(NamingContext.class); + + + /** + * Bindings in this Context. + */ + protected final HashMap bindings; + + + /** + * Name of the associated Catalina Context. + */ + protected final String name; + + + /** + * Determines if an attempt to write to a read-only context results in an + * exception or if the request is ignored. + */ + private boolean exceptionOnFailedWrite = true; + public boolean getExceptionOnFailedWrite() { + return exceptionOnFailedWrite; + } + public void setExceptionOnFailedWrite(boolean exceptionOnFailedWrite) { + this.exceptionOnFailedWrite = exceptionOnFailedWrite; + } + + + // -------------------------------------------------------- Context Methods + + /** + * Retrieves the named object. If name is empty, returns a new instance + * of this context (which represents the same naming context as this + * context, but its environment may be modified independently and it may + * be accessed concurrently). + * + * @param name the name of the object to look up + * @return the object bound to name + * @exception NamingException if a naming exception is encountered + */ + @Override + public Object lookup(Name name) + throws NamingException { + return lookup(name, true); + } + + + /** + * Retrieves the named object. + * + * @param name the name of the object to look up + * @return the object bound to name + * @exception NamingException if a naming exception is encountered + */ + @Override + public Object lookup(String name) + throws NamingException { + return lookup(new CompositeName(name), true); + } + + + /** + * Binds a name to an object. All intermediate contexts and the target + * context (that named by all but terminal atomic component of the name) + * must already exist. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @exception NameAlreadyBoundException if name is already bound + * @exception javax.naming.directory.InvalidAttributesException if object + * did not supply all mandatory attributes + * @exception NamingException if a naming exception is encountered + */ + @Override + public void bind(Name name, Object obj) + throws NamingException { + bind(name, obj, false); + } + + + /** + * Binds a name to an object. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @exception NameAlreadyBoundException if name is already bound + * @exception javax.naming.directory.InvalidAttributesException if object + * did not supply all mandatory attributes + * @exception NamingException if a naming exception is encountered + */ + @Override + public void bind(String name, Object obj) + throws NamingException { + bind(new CompositeName(name), obj); + } + + + /** + * Binds a name to an object, overwriting any existing binding. All + * intermediate contexts and the target context (that named by all but + * terminal atomic component of the name) must already exist. + *

    + * If the object is a DirContext, any existing attributes associated with + * the name are replaced with those of the object. Otherwise, any + * existing attributes associated with the name remain unchanged. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @exception javax.naming.directory.InvalidAttributesException if object + * did not supply all mandatory attributes + * @exception NamingException if a naming exception is encountered + */ + @Override + public void rebind(Name name, Object obj) + throws NamingException { + bind(name, obj, true); + } + + + /** + * Binds a name to an object, overwriting any existing binding. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @exception javax.naming.directory.InvalidAttributesException if object + * did not supply all mandatory attributes + * @exception NamingException if a naming exception is encountered + */ + @Override + public void rebind(String name, Object obj) + throws NamingException { + rebind(new CompositeName(name), obj); + } + + + /** + * Unbinds the named object. Removes the terminal atomic name in name + * from the target context--that named by all but the terminal atomic + * part of name. + *

    + * This method is idempotent. It succeeds even if the terminal atomic + * name is not bound in the target context, but throws + * NameNotFoundException if any of the intermediate contexts do not exist. + * + * @param name the name to bind; may not be empty + * @exception NameNotFoundException if an intermediate context does not + * exist + * @exception NamingException if a naming exception is encountered + */ + @Override + public void unbind(Name name) throws NamingException { + + if (!checkWritable()) { + return; + } + + while ((!name.isEmpty()) && (name.get(0).length() == 0)) { + name = name.getSuffix(1); + } + if (name.isEmpty()) { + throw new NamingException + (sm.getString("namingContext.invalidName")); + } + + NamingEntry entry = bindings.get(name.get(0)); + + if (entry == null) { + throw new NameNotFoundException + (sm.getString("namingContext.nameNotBound", name, name.get(0))); + } + + if (name.size() > 1) { + if (entry.type == NamingEntry.CONTEXT) { + ((Context) entry.value).unbind(name.getSuffix(1)); + } else { + throw new NamingException + (sm.getString("namingContext.contextExpected")); + } + } else { + bindings.remove(name.get(0)); + } + + } + + + /** + * Unbinds the named object. + * + * @param name the name to bind; may not be empty + * @exception NameNotFoundException if an intermediate context does not + * exist + * @exception NamingException if a naming exception is encountered + */ + @Override + public void unbind(String name) + throws NamingException { + unbind(new CompositeName(name)); + } + + + /** + * Binds a new name to the object bound to an old name, and unbinds the + * old name. Both names are relative to this context. Any attributes + * associated with the old name become associated with the new name. + * Intermediate contexts of the old name are not changed. + * + * @param oldName the name of the existing binding; may not be empty + * @param newName the name of the new binding; may not be empty + * @exception NameAlreadyBoundException if newName is already bound + * @exception NamingException if a naming exception is encountered + */ + @Override + public void rename(Name oldName, Name newName) + throws NamingException { + Object value = lookup(oldName); + bind(newName, value); + unbind(oldName); + } + + + /** + * Binds a new name to the object bound to an old name, and unbinds the + * old name. + * + * @param oldName the name of the existing binding; may not be empty + * @param newName the name of the new binding; may not be empty + * @exception NameAlreadyBoundException if newName is already bound + * @exception NamingException if a naming exception is encountered + */ + @Override + public void rename(String oldName, String newName) + throws NamingException { + rename(new CompositeName(oldName), new CompositeName(newName)); + } + + + /** + * Enumerates the names bound in the named context, along with the class + * names of objects bound to them. The contents of any subcontexts are + * not included. + *

    + * If a binding is added to or removed from this context, its effect on + * an enumeration previously returned is undefined. + * + * @param name the name of the context to list + * @return an enumeration of the names and class names of the bindings in + * this context. Each element of the enumeration is of type NameClassPair. + * @exception NamingException if a naming exception is encountered + */ + @Override + public NamingEnumeration list(Name name) + throws NamingException { + // Removing empty parts + while ((!name.isEmpty()) && (name.get(0).length() == 0)) { + name = name.getSuffix(1); + } + if (name.isEmpty()) { + return new NamingContextEnumeration(bindings.values().iterator()); + } + + NamingEntry entry = bindings.get(name.get(0)); + + if (entry == null) { + throw new NameNotFoundException + (sm.getString("namingContext.nameNotBound", name, name.get(0))); + } + + if (entry.type != NamingEntry.CONTEXT) { + throw new NamingException + (sm.getString("namingContext.contextExpected")); + } + return ((Context) entry.value).list(name.getSuffix(1)); + } + + + /** + * Enumerates the names bound in the named context, along with the class + * names of objects bound to them. + * + * @param name the name of the context to list + * @return an enumeration of the names and class names of the bindings in + * this context. Each element of the enumeration is of type NameClassPair. + * @exception NamingException if a naming exception is encountered + */ + @Override + public NamingEnumeration list(String name) + throws NamingException { + return list(new CompositeName(name)); + } + + + /** + * Enumerates the names bound in the named context, along with the + * objects bound to them. The contents of any subcontexts are not + * included. + *

    + * If a binding is added to or removed from this context, its effect on + * an enumeration previously returned is undefined. + * + * @param name the name of the context to list + * @return an enumeration of the bindings in this context. + * Each element of the enumeration is of type Binding. + * @exception NamingException if a naming exception is encountered + */ + @Override + public NamingEnumeration listBindings(Name name) + throws NamingException { + // Removing empty parts + while ((!name.isEmpty()) && (name.get(0).length() == 0)) { + name = name.getSuffix(1); + } + if (name.isEmpty()) { + return new NamingContextBindingsEnumeration(bindings.values().iterator(), this); + } + + NamingEntry entry = bindings.get(name.get(0)); + + if (entry == null) { + throw new NameNotFoundException + (sm.getString("namingContext.nameNotBound", name, name.get(0))); + } + + if (entry.type != NamingEntry.CONTEXT) { + throw new NamingException + (sm.getString("namingContext.contextExpected")); + } + return ((Context) entry.value).listBindings(name.getSuffix(1)); + } + + + /** + * Enumerates the names bound in the named context, along with the + * objects bound to them. + * + * @param name the name of the context to list + * @return an enumeration of the bindings in this context. + * Each element of the enumeration is of type Binding. + * @exception NamingException if a naming exception is encountered + */ + @Override + public NamingEnumeration listBindings(String name) + throws NamingException { + return listBindings(new CompositeName(name)); + } + + + /** + * Destroys the named context and removes it from the namespace. Any + * attributes associated with the name are also removed. Intermediate + * contexts are not destroyed. + *

    + * This method is idempotent. It succeeds even if the terminal atomic + * name is not bound in the target context, but throws + * NameNotFoundException if any of the intermediate contexts do not exist. + * + * In a federated naming system, a context from one naming system may be + * bound to a name in another. One can subsequently look up and perform + * operations on the foreign context using a composite name. However, an + * attempt destroy the context using this composite name will fail with + * NotContextException, because the foreign context is not a "subcontext" + * of the context in which it is bound. Instead, use unbind() to remove + * the binding of the foreign context. Destroying the foreign context + * requires that the destroySubcontext() be performed on a context from + * the foreign context's "native" naming system. + * + * @param name the name of the context to be destroyed; may not be empty + * @exception NameNotFoundException if an intermediate context does not + * exist + * @exception NotContextException if the name is bound but does not name + * a context, or does not name a context of the appropriate type + */ + @Override + public void destroySubcontext(Name name) throws NamingException { + + if (!checkWritable()) { + return; + } + + while ((!name.isEmpty()) && (name.get(0).length() == 0)) { + name = name.getSuffix(1); + } + if (name.isEmpty()) { + throw new NamingException + (sm.getString("namingContext.invalidName")); + } + + NamingEntry entry = bindings.get(name.get(0)); + + if (entry == null) { + throw new NameNotFoundException + (sm.getString("namingContext.nameNotBound", name, name.get(0))); + } + + if (name.size() > 1) { + if (entry.type == NamingEntry.CONTEXT) { + ((Context) entry.value).destroySubcontext(name.getSuffix(1)); + } else { + throw new NamingException + (sm.getString("namingContext.contextExpected")); + } + } else { + if (entry.type == NamingEntry.CONTEXT) { + ((Context) entry.value).close(); + bindings.remove(name.get(0)); + } else { + throw new NotContextException + (sm.getString("namingContext.contextExpected")); + } + } + + } + + + /** + * Destroys the named context and removes it from the namespace. + * + * @param name the name of the context to be destroyed; may not be empty + * @exception NameNotFoundException if an intermediate context does not + * exist + * @exception NotContextException if the name is bound but does not name + * a context, or does not name a context of the appropriate type + */ + @Override + public void destroySubcontext(String name) + throws NamingException { + destroySubcontext(new CompositeName(name)); + } + + + /** + * Creates and binds a new context. Creates a new context with the given + * name and binds it in the target context (that named by all but + * terminal atomic component of the name). All intermediate contexts and + * the target context must already exist. + * + * @param name the name of the context to create; may not be empty + * @return the newly created context + * @exception NameAlreadyBoundException if name is already bound + * @exception javax.naming.directory.InvalidAttributesException if creation + * of the sub-context requires specification of mandatory attributes + * @exception NamingException if a naming exception is encountered + */ + @Override + public Context createSubcontext(Name name) throws NamingException { + if (!checkWritable()) { + return null; + } + + NamingContext newContext = new NamingContext(env, this.name); + bind(name, newContext); + + newContext.setExceptionOnFailedWrite(getExceptionOnFailedWrite()); + + return newContext; + } + + + /** + * Creates and binds a new context. + * + * @param name the name of the context to create; may not be empty + * @return the newly created context + * @exception NameAlreadyBoundException if name is already bound + * @exception javax.naming.directory.InvalidAttributesException if creation + * of the sub-context requires specification of mandatory attributes + * @exception NamingException if a naming exception is encountered + */ + @Override + public Context createSubcontext(String name) + throws NamingException { + return createSubcontext(new CompositeName(name)); + } + + + /** + * Retrieves the named object, following links except for the terminal + * atomic component of the name. If the object bound to name is not a + * link, returns the object itself. + * + * @param name the name of the object to look up + * @return the object bound to name, not following the terminal link + * (if any). + * @exception NamingException if a naming exception is encountered + */ + @Override + public Object lookupLink(Name name) + throws NamingException { + return lookup(name, false); + } + + + /** + * Retrieves the named object, following links except for the terminal + * atomic component of the name. + * + * @param name the name of the object to look up + * @return the object bound to name, not following the terminal link + * (if any). + * @exception NamingException if a naming exception is encountered + */ + @Override + public Object lookupLink(String name) + throws NamingException { + return lookup(new CompositeName(name), false); + } + + + /** + * Retrieves the parser associated with the named context. In a + * federation of namespaces, different naming systems will parse names + * differently. This method allows an application to get a parser for + * parsing names into their atomic components using the naming convention + * of a particular naming system. Within any single naming system, + * NameParser objects returned by this method must be equal (using the + * equals() test). + * + * @param name the name of the context from which to get the parser + * @return a name parser that can parse compound names into their atomic + * components + * @exception NamingException if a naming exception is encountered + */ + @Override + public NameParser getNameParser(Name name) + throws NamingException { + + while ((!name.isEmpty()) && (name.get(0).length() == 0)) { + name = name.getSuffix(1); + } + if (name.isEmpty()) { + return nameParser; + } + + if (name.size() > 1) { + Object obj = bindings.get(name.get(0)); + if (obj instanceof Context) { + return ((Context) obj).getNameParser(name.getSuffix(1)); + } else { + throw new NotContextException + (sm.getString("namingContext.contextExpected")); + } + } + + return nameParser; + + } + + + /** + * Retrieves the parser associated with the named context. + * + * @param name the name of the context from which to get the parser + * @return a name parser that can parse compound names into their atomic + * components + * @exception NamingException if a naming exception is encountered + */ + @Override + public NameParser getNameParser(String name) + throws NamingException { + return getNameParser(new CompositeName(name)); + } + + + /** + * Composes the name of this context with a name relative to this context. + *

    + * Given a name (name) relative to this context, and the name (prefix) + * of this context relative to one of its ancestors, this method returns + * the composition of the two names using the syntax appropriate for the + * naming system(s) involved. That is, if name names an object relative + * to this context, the result is the name of the same object, but + * relative to the ancestor context. None of the names may be null. + * + * @param name a name relative to this context + * @param prefix the name of this context relative to one of its ancestors + * @return the composition of prefix and name + * @exception NamingException if a naming exception is encountered + */ + @Override + public Name composeName(Name name, Name prefix) throws NamingException { + prefix = (Name) prefix.clone(); + return prefix.addAll(name); + } + + + /** + * Composes the name of this context with a name relative to this context. + * + * @param name a name relative to this context + * @param prefix the name of this context relative to one of its ancestors + * @return the composition of prefix and name + */ + @Override + public String composeName(String name, String prefix) { + return prefix + "/" + name; + } + + + /** + * Adds a new environment property to the environment of this context. If + * the property already exists, its value is overwritten. + * + * @param propName the name of the environment property to add; may not + * be null + * @param propVal the value of the property to add; may not be null + */ + @Override + public Object addToEnvironment(String propName, Object propVal) { + return env.put(propName, propVal); + } + + + /** + * Removes an environment property from the environment of this context. + * + * @param propName the name of the environment property to remove; + * may not be null + */ + @Override + public Object removeFromEnvironment(String propName){ + return env.remove(propName); + } + + + /** + * Retrieves the environment in effect for this context. See class + * description for more details on environment properties. + * The caller should not make any changes to the object returned: their + * effect on the context is undefined. The environment of this context + * may be changed using addToEnvironment() and removeFromEnvironment(). + * + * @return the environment of this context; never null + */ + @Override + public Hashtable getEnvironment() { + return env; + } + + + /** + * Closes this context. This method releases this context's resources + * immediately, instead of waiting for them to be released automatically + * by the garbage collector. + * This method is idempotent: invoking it on a context that has already + * been closed has no effect. Invoking any other method on a closed + * context is not allowed, and results in undefined behaviour. + * + * @exception NamingException if a naming exception is encountered + */ + @Override + public void close() throws NamingException { + if (!checkWritable()) { + return; + } + env.clear(); + } + + + /** + * Retrieves the full name of this context within its own namespace. + *

    + * Many naming services have a notion of a "full name" for objects in + * their respective namespaces. For example, an LDAP entry has a + * distinguished name, and a DNS record has a fully qualified name. This + * method allows the client application to retrieve this name. The string + * returned by this method is not a JNDI composite name and should not be + * passed directly to context methods. In naming systems for which the + * notion of full name does not make sense, + * OperationNotSupportedException is thrown. + * + * @return this context's name in its own namespace; never null + * @exception OperationNotSupportedException if the naming system does + * not have the notion of a full name + * @exception NamingException if a naming exception is encountered + */ + @Override + public String getNameInNamespace() + throws NamingException { + throw new OperationNotSupportedException + (sm.getString("namingContext.noAbsoluteName")); + } + + + // ------------------------------------------------------ Protected Methods + + + private static final boolean GRAAL; + + static { + boolean result = false; + try { + Class nativeImageClazz = Class.forName("org.graalvm.nativeimage.ImageInfo"); + result = Boolean.TRUE.equals(nativeImageClazz.getMethod("inImageCode").invoke(null)); + } catch (ClassNotFoundException e) { + // Must be Graal + } catch (ReflectiveOperationException | IllegalArgumentException e) { + // Should never happen + } + GRAAL = result || System.getProperty("org.graalvm.nativeimage.imagecode") != null; + } + + /** + * Retrieves the named object. + * + * @param name the name of the object to look up + * @param resolveLinks If true, the links will be resolved + * @return the object bound to name + * @exception NamingException if a naming exception is encountered + */ + protected Object lookup(Name name, boolean resolveLinks) + throws NamingException { + + // Removing empty parts + while ((!name.isEmpty()) && (name.get(0).length() == 0)) { + name = name.getSuffix(1); + } + if (name.isEmpty()) { + // If name is empty, a newly allocated naming context is returned + return new NamingContext(env, this.name, bindings); + } + + NamingEntry entry = bindings.get(name.get(0)); + + if (entry == null) { + throw new NameNotFoundException + (sm.getString("namingContext.nameNotBound", name, name.get(0))); + } + + if (name.size() > 1) { + // If the size of the name is greater that 1, then we go through a + // number of subcontexts. + if (entry.type != NamingEntry.CONTEXT) { + throw new NamingException + (sm.getString("namingContext.contextExpected")); + } + return ((Context) entry.value).lookup(name.getSuffix(1)); + } else { + if ((resolveLinks) && (entry.type == NamingEntry.LINK_REF)) { + String link = ((LinkRef) entry.value).getLinkName(); + if (link.startsWith(".")) { + // Link relative to this context + return lookup(link.substring(1)); + } else { + return new InitialContext(env).lookup(link); + } + } else if (entry.type == NamingEntry.REFERENCE) { + try { + Object obj = null; + if (!GRAAL) { + obj = NamingManager.getObjectInstance(entry.value, name, this, env); + } else { + // NamingManager.getObjectInstance would simply return the reference here + // Use the configured object factory to resolve it directly if possible + // Note: This may need manual constructor reflection configuration + Reference reference = (Reference) entry.value; + String factoryClassName = reference.getFactoryClassName(); + if (factoryClassName != null) { + Class factoryClass = getClass().getClassLoader().loadClass(factoryClassName); + ObjectFactory factory = (ObjectFactory) factoryClass.getDeclaredConstructor().newInstance(); + obj = factory.getObjectInstance(entry.value, name, this, env); + } + } + if (entry.value instanceof ResourceRef) { + boolean singleton = Boolean.parseBoolean( + (String) ((ResourceRef) entry.value).get( + ResourceRef.SINGLETON).getContent()); + if (singleton) { + entry.type = NamingEntry.ENTRY; + entry.value = obj; + } + } + if (obj == null) { + throw new NamingException(sm.getString("namingContext.failResolvingReference")); + } + return obj; + } catch (NamingException e) { + throw e; + } catch (Exception e) { + String msg = sm.getString("namingContext.failResolvingReference"); + log.warn(msg, e); + NamingException ne = new NamingException(msg); + ne.initCause(e); + throw ne; + } + } else { + return entry.value; + } + } + + } + + + /** + * Binds a name to an object. All intermediate contexts and the target + * context (that named by all but terminal atomic component of the name) + * must already exist. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @param rebind if true, then perform a rebind (ie, overwrite) + * @exception NameAlreadyBoundException if name is already bound + * @exception javax.naming.directory.InvalidAttributesException if object + * did not supply all mandatory attributes + * @exception NamingException if a naming exception is encountered + */ + protected void bind(Name name, Object obj, boolean rebind) + throws NamingException { + + if (!checkWritable()) { + return; + } + + while ((!name.isEmpty()) && (name.get(0).length() == 0)) { + name = name.getSuffix(1); + } + if (name.isEmpty()) { + throw new NamingException + (sm.getString("namingContext.invalidName")); + } + + NamingEntry entry = bindings.get(name.get(0)); + + if (name.size() > 1) { + if (entry == null) { + throw new NameNotFoundException(sm.getString( + "namingContext.nameNotBound", name, name.get(0))); + } + if (entry.type == NamingEntry.CONTEXT) { + if (rebind) { + ((Context) entry.value).rebind(name.getSuffix(1), obj); + } else { + ((Context) entry.value).bind(name.getSuffix(1), obj); + } + } else { + throw new NamingException + (sm.getString("namingContext.contextExpected")); + } + } else { + if ((!rebind) && (entry != null)) { + throw new NameAlreadyBoundException + (sm.getString("namingContext.alreadyBound", name.get(0))); + } else { + // Getting the type of the object and wrapping it within a new + // NamingEntry + Object toBind = + NamingManager.getStateToBind(obj, name, this, env); + if (toBind instanceof Context) { + entry = new NamingEntry(name.get(0), toBind, + NamingEntry.CONTEXT); + } else if (toBind instanceof LinkRef) { + entry = new NamingEntry(name.get(0), toBind, + NamingEntry.LINK_REF); + } else if (toBind instanceof Reference) { + entry = new NamingEntry(name.get(0), toBind, + NamingEntry.REFERENCE); + } else if (toBind instanceof Referenceable) { + toBind = ((Referenceable) toBind).getReference(); + entry = new NamingEntry(name.get(0), toBind, + NamingEntry.REFERENCE); + } else { + entry = new NamingEntry(name.get(0), toBind, + NamingEntry.ENTRY); + } + bindings.put(name.get(0), entry); + } + } + + } + + + /** + * @return true if writing is allowed on this context. + */ + protected boolean isWritable() { + return ContextAccessController.isWritable(name); + } + + + /** + * Throws a naming exception is Context is not writable. + * @return true if the Context is writable + * @throws NamingException if the Context is not writable and + * exceptionOnFailedWrite is true + */ + protected boolean checkWritable() throws NamingException { + if (isWritable()) { + return true; + } else { + if (exceptionOnFailedWrite) { + throw new OperationNotSupportedException(sm.getString("namingContext.readOnly")); + } + } + return false; + } +} + diff --git a/java/org/apache/naming/NamingContextBindingsEnumeration.java b/java/org/apache/naming/NamingContextBindingsEnumeration.java new file mode 100644 index 0000000..fa642bb --- /dev/null +++ b/java/org/apache/naming/NamingContextBindingsEnumeration.java @@ -0,0 +1,130 @@ +/* + * 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.naming; + +import java.util.Iterator; + +import javax.naming.Binding; +import javax.naming.CompositeName; +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; + +/** + * Naming enumeration implementation. + * + * @author Remy Maucherat + */ +public class NamingContextBindingsEnumeration + implements NamingEnumeration { + + + // ----------------------------------------------------------- Constructors + + + public NamingContextBindingsEnumeration(Iterator entries, + Context ctx) { + iterator = entries; + this.ctx = ctx; + } + + // -------------------------------------------------------------- Variables + + + /** + * Underlying enumeration. + */ + protected final Iterator iterator; + + + /** + * The context for which this enumeration is being generated. + */ + private final Context ctx; + + + // --------------------------------------------------------- Public Methods + + + /** + * Retrieves the next element in the enumeration. + */ + @Override + public Binding next() + throws NamingException { + return nextElementInternal(); + } + + + /** + * Determines whether there are any more elements in the enumeration. + */ + @Override + public boolean hasMore() + throws NamingException { + return iterator.hasNext(); + } + + + /** + * Closes this enumeration. + */ + @Override + public void close() + throws NamingException { + } + + + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + + @Override + public Binding nextElement() { + try { + return nextElementInternal(); + } catch (NamingException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + private Binding nextElementInternal() throws NamingException { + NamingEntry entry = iterator.next(); + Object value; + + // If the entry is a reference, resolve it + if (entry.type == NamingEntry.REFERENCE + || entry.type == NamingEntry.LINK_REF) { + try { + value = ctx.lookup(new CompositeName(entry.name)); + } catch (NamingException e) { + throw e; + } catch (Exception e) { + NamingException ne = new NamingException(e.getMessage()); + ne.initCause(e); + throw ne; + } + } else { + value = entry.value; + } + + return new Binding(entry.name, value.getClass().getName(), value, true); + } +} + diff --git a/java/org/apache/naming/NamingContextEnumeration.java b/java/org/apache/naming/NamingContextEnumeration.java new file mode 100644 index 0000000..de579c9 --- /dev/null +++ b/java/org/apache/naming/NamingContextEnumeration.java @@ -0,0 +1,97 @@ +/* + * 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.naming; + +import java.util.Iterator; + +import javax.naming.NameClassPair; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; + +/** + * Naming enumeration implementation. + * + * @author Remy Maucherat + */ +public class NamingContextEnumeration + implements NamingEnumeration { + + + // ----------------------------------------------------------- Constructors + + + public NamingContextEnumeration(Iterator entries) { + iterator = entries; + } + + + // -------------------------------------------------------------- Variables + + + /** + * Underlying enumeration. + */ + protected final Iterator iterator; + + + // --------------------------------------------------------- Public Methods + + + /** + * Retrieves the next element in the enumeration. + */ + @Override + public NameClassPair next() + throws NamingException { + return nextElement(); + } + + + /** + * Determines whether there are any more elements in the enumeration. + */ + @Override + public boolean hasMore() + throws NamingException { + return iterator.hasNext(); + } + + + /** + * Closes this enumeration. + */ + @Override + public void close() + throws NamingException { + } + + + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + + @Override + public NameClassPair nextElement() { + NamingEntry entry = iterator.next(); + return new NameClassPair(entry.name, entry.value.getClass().getName()); + } + + +} + diff --git a/java/org/apache/naming/NamingEntry.java b/java/org/apache/naming/NamingEntry.java new file mode 100644 index 0000000..c1a7273 --- /dev/null +++ b/java/org/apache/naming/NamingEntry.java @@ -0,0 +1,63 @@ +/* + * 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.naming; + + +/** + * Represents a binding in a NamingContext. + * + * @author Remy Maucherat + */ +public class NamingEntry { + + public static final int ENTRY = 0; + public static final int LINK_REF = 1; + public static final int REFERENCE = 2; + public static final int CONTEXT = 10; + + + public NamingEntry(String name, Object value, int type) { + this.name = name; + this.value = value; + this.type = type; + } + + + /** + * The type instance variable is used to avoid using RTTI when doing + * lookups. + */ + public int type; + public final String name; + public Object value; + + + @Override + public boolean equals(Object obj) { + if (obj instanceof NamingEntry) { + return name.equals(((NamingEntry) obj).name); + } else { + return false; + } + } + + + @Override + public int hashCode() { + return name.hashCode(); + } +} diff --git a/java/org/apache/naming/ResourceEnvRef.java b/java/org/apache/naming/ResourceEnvRef.java new file mode 100644 index 0000000..8d1c222 --- /dev/null +++ b/java/org/apache/naming/ResourceEnvRef.java @@ -0,0 +1,50 @@ +/* + * 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.naming; + +/** + * Represents a reference address to a resource environment. + * + * @author Remy Maucherat + */ +public class ResourceEnvRef extends AbstractRef { + + private static final long serialVersionUID = 1L; + + + /** + * Default factory for this reference. + */ + public static final String DEFAULT_FACTORY = + org.apache.naming.factory.Constants.DEFAULT_RESOURCE_ENV_FACTORY; + + + /** + * Resource env reference. + * + * @param resourceType Type + */ + public ResourceEnvRef(String resourceType) { + super(resourceType); + } + + + @Override + protected String getDefaultFactoryClassName() { + return DEFAULT_FACTORY; + } +} diff --git a/java/org/apache/naming/ResourceLinkRef.java b/java/org/apache/naming/ResourceLinkRef.java new file mode 100644 index 0000000..6e315f3 --- /dev/null +++ b/java/org/apache/naming/ResourceLinkRef.java @@ -0,0 +1,68 @@ +/* + * 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.naming; + +import javax.naming.StringRefAddr; + +/** + * Represents a reference address to a resource. + * + * @author Remy Maucherat + */ +public class ResourceLinkRef extends AbstractRef { + + private static final long serialVersionUID = 1L; + + + /** + * Default factory for this reference. + */ + public static final String DEFAULT_FACTORY = + org.apache.naming.factory.Constants.DEFAULT_RESOURCE_LINK_FACTORY; + + + /** + * Description address type. + */ + public static final String GLOBALNAME = "globalName"; + + + /** + * ResourceLink Reference. + * + * @param resourceClass Resource class + * @param globalName Global name + * @param factory The possibly null class name of the object's factory. + * @param factoryLocation The possibly null location from which to load the + * factory (e.g. URL) + */ + public ResourceLinkRef(String resourceClass, String globalName, + String factory, String factoryLocation) { + super(resourceClass, factory, factoryLocation); + StringRefAddr refAddr = null; + if (globalName != null) { + refAddr = new StringRefAddr(GLOBALNAME, globalName); + add(refAddr); + } + } + + + @Override + protected String getDefaultFactoryClassName() { + return DEFAULT_FACTORY; + } +} diff --git a/java/org/apache/naming/ResourceRef.java b/java/org/apache/naming/ResourceRef.java new file mode 100644 index 0000000..57a131f --- /dev/null +++ b/java/org/apache/naming/ResourceRef.java @@ -0,0 +1,118 @@ +/* + * 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.naming; + +import javax.naming.StringRefAddr; + +/** + * Represents a reference address to a resource. + * + * @author Remy Maucherat + */ +public class ResourceRef extends AbstractRef { + + private static final long serialVersionUID = 1L; + + + /** + * Default factory for this reference. + */ + public static final String DEFAULT_FACTORY = + org.apache.naming.factory.Constants.DEFAULT_RESOURCE_FACTORY; + + + /** + * Description address type. + */ + public static final String DESCRIPTION = "description"; + + + /** + * Scope address type. + */ + public static final String SCOPE = "scope"; + + + /** + * Auth address type. + */ + public static final String AUTH = "auth"; + + + /** + * Is this resource a singleton + */ + public static final String SINGLETON = "singleton"; + + + /** + * Resource Reference. + * + * @param resourceClass Resource class + * @param description Description of the resource + * @param scope Resource scope + * @param auth Resource authentication + * @param singleton Is this resource a singleton (every lookup should return + * the same instance rather than a new instance)? + */ + public ResourceRef(String resourceClass, String description, + String scope, String auth, boolean singleton) { + this(resourceClass, description, scope, auth, singleton, null, null); + } + + + /** + * Resource Reference. + * + * @param resourceClass Resource class + * @param description Description of the resource + * @param scope Resource scope + * @param auth Resource authentication + * @param singleton Is this resource a singleton (every lookup should return + * the same instance rather than a new instance)? + * @param factory The possibly null class name of the object's factory. + * @param factoryLocation The possibly null location from which to load the + * factory (e.g. URL) + */ + public ResourceRef(String resourceClass, String description, + String scope, String auth, boolean singleton, + String factory, String factoryLocation) { + super(resourceClass, factory, factoryLocation); + StringRefAddr refAddr = null; + if (description != null) { + refAddr = new StringRefAddr(DESCRIPTION, description); + add(refAddr); + } + if (scope != null) { + refAddr = new StringRefAddr(SCOPE, scope); + add(refAddr); + } + if (auth != null) { + refAddr = new StringRefAddr(AUTH, auth); + add(refAddr); + } + // singleton is a boolean so slightly different handling + refAddr = new StringRefAddr(SINGLETON, Boolean.toString(singleton)); + add(refAddr); + } + + + @Override + protected String getDefaultFactoryClassName() { + return DEFAULT_FACTORY; + } +} diff --git a/java/org/apache/naming/SelectorContext.java b/java/org/apache/naming/SelectorContext.java new file mode 100644 index 0000000..3675b4e --- /dev/null +++ b/java/org/apache/naming/SelectorContext.java @@ -0,0 +1,797 @@ +/* + * 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.naming; + +import java.util.Hashtable; + +import javax.naming.Binding; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NameClassPair; +import javax.naming.NameParser; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Catalina JNDI Context implementation. + * + * @author Remy Maucherat + */ +public class SelectorContext implements Context { + + + // -------------------------------------------------------------- Constants + + + /** + * Namespace URL. + */ + public static final String prefix = "java:"; + + + /** + * Namespace URL length. + */ + public static final int prefixLength = prefix.length(); + + + /** + * Initial context prefix. + */ + public static final String IC_PREFIX = "IC_"; + + + private static final Log log = LogFactory.getLog(SelectorContext.class); + + // ----------------------------------------------------------- Constructors + + + /** + * Builds a Catalina selector context using the given environment. + * @param env The environment + */ + public SelectorContext(Hashtable env) { + this.env = env; + this.initialContext = false; + } + + + /** + * Builds a Catalina selector context using the given environment. + * @param env The environment + * @param initialContext true if this is the main + * initial context + */ + public SelectorContext(Hashtable env, + boolean initialContext) { + this.env = env; + this.initialContext = initialContext; + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Environment. + */ + protected final Hashtable env; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(SelectorContext.class); + + + /** + * Request for an initial context. + */ + protected final boolean initialContext; + + + // --------------------------------------------------------- Public Methods + + + // -------------------------------------------------------- Context Methods + + + /** + * Retrieves the named object. If name is empty, returns a new instance + * of this context (which represents the same naming context as this + * context, but its environment may be modified independently and it may + * be accessed concurrently). + * + * @param name the name of the object to look up + * @return the object bound to name + * @throws NamingException if a naming exception is encountered + */ + @Override + public Object lookup(Name name) + throws NamingException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("selectorContext.methodUsingName", "lookup", + name)); + } + + // Strip the URL header + // Find the appropriate NamingContext according to the current bindings + // Execute the lookup on that context + return getBoundContext().lookup(parseName(name)); + } + + + /** + * Retrieves the named object. + * + * @param name the name of the object to look up + * @return the object bound to name + * @throws NamingException if a naming exception is encountered + */ + @Override + public Object lookup(String name) + throws NamingException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("selectorContext.methodUsingString", "lookup", + name)); + } + + // Strip the URL header + // Find the appropriate NamingContext according to the current bindings + // Execute the lookup on that context + return getBoundContext().lookup(parseName(name)); + } + + + /** + * Binds a name to an object. All intermediate contexts and the target + * context (that named by all but terminal atomic component of the name) + * must already exist. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @throws javax.naming.NameAlreadyBoundException if name is already + * bound + * @throws javax.naming.directory.InvalidAttributesException if object did not + * supply all mandatory attributes + * @throws NamingException if a naming exception is encountered + */ + @Override + public void bind(Name name, Object obj) + throws NamingException { + getBoundContext().bind(parseName(name), obj); + } + + + /** + * Binds a name to an object. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @throws javax.naming.NameAlreadyBoundException if name is already + * bound + * @throws javax.naming.directory.InvalidAttributesException if object did not + * supply all mandatory attributes + * @throws NamingException if a naming exception is encountered + */ + @Override + public void bind(String name, Object obj) + throws NamingException { + getBoundContext().bind(parseName(name), obj); + } + + + /** + * Binds a name to an object, overwriting any existing binding. All + * intermediate contexts and the target context (that named by all but + * terminal atomic component of the name) must already exist. + *

    + * If the object is a DirContext, any existing attributes associated with + * the name are replaced with those of the object. Otherwise, any + * existing attributes associated with the name remain unchanged. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @throws javax.naming.directory.InvalidAttributesException if object did not + * supply all mandatory attributes + * @throws NamingException if a naming exception is encountered + */ + @Override + public void rebind(Name name, Object obj) + throws NamingException { + getBoundContext().rebind(parseName(name), obj); + } + + + /** + * Binds a name to an object, overwriting any existing binding. + * + * @param name the name to bind; may not be empty + * @param obj the object to bind; possibly null + * @throws javax.naming.directory.InvalidAttributesException if object did not + * supply all mandatory attributes + * @throws NamingException if a naming exception is encountered + */ + @Override + public void rebind(String name, Object obj) + throws NamingException { + getBoundContext().rebind(parseName(name), obj); + } + + + /** + * Unbinds the named object. Removes the terminal atomic name in name + * from the target context--that named by all but the terminal atomic + * part of name. + *

    + * This method is idempotent. It succeeds even if the terminal atomic + * name is not bound in the target context, but throws + * NameNotFoundException if any of the intermediate contexts do not exist. + * + * @param name the name to bind; may not be empty + * @throws javax.naming.NameNotFoundException if an intermediate context + * does not exist + * @throws NamingException if a naming exception is encountered + */ + @Override + public void unbind(Name name) + throws NamingException { + getBoundContext().unbind(parseName(name)); + } + + + /** + * Unbinds the named object. + * + * @param name the name to bind; may not be empty + * @throws javax.naming.NameNotFoundException if an intermediate context + * does not exist + * @throws NamingException if a naming exception is encountered + */ + @Override + public void unbind(String name) + throws NamingException { + getBoundContext().unbind(parseName(name)); + } + + + /** + * Binds a new name to the object bound to an old name, and unbinds the + * old name. Both names are relative to this context. Any attributes + * associated with the old name become associated with the new name. + * Intermediate contexts of the old name are not changed. + * + * @param oldName the name of the existing binding; may not be empty + * @param newName the name of the new binding; may not be empty + * @throws javax.naming.NameAlreadyBoundException if name is already + * bound + * @throws NamingException if a naming exception is encountered + */ + @Override + public void rename(Name oldName, Name newName) + throws NamingException { + getBoundContext().rename(parseName(oldName), parseName(newName)); + } + + + /** + * Binds a new name to the object bound to an old name, and unbinds the + * old name. + * + * @param oldName the name of the existing binding; may not be empty + * @param newName the name of the new binding; may not be empty + * @throws javax.naming.NameAlreadyBoundException if name is already + * bound + * @throws NamingException if a naming exception is encountered + */ + @Override + public void rename(String oldName, String newName) + throws NamingException { + getBoundContext().rename(parseName(oldName), parseName(newName)); + } + + + /** + * Enumerates the names bound in the named context, along with the class + * names of objects bound to them. The contents of any subcontexts are + * not included. + *

    + * If a binding is added to or removed from this context, its effect on + * an enumeration previously returned is undefined. + * + * @param name the name of the context to list + * @return an enumeration of the names and class names of the bindings in + * this context. Each element of the enumeration is of type NameClassPair. + * @throws NamingException if a naming exception is encountered + */ + @Override + public NamingEnumeration list(Name name) + throws NamingException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("selectorContext.methodUsingName", "list", + name)); + } + + return getBoundContext().list(parseName(name)); + } + + + /** + * Enumerates the names bound in the named context, along with the class + * names of objects bound to them. + * + * @param name the name of the context to list + * @return an enumeration of the names and class names of the bindings in + * this context. Each element of the enumeration is of type NameClassPair. + * @throws NamingException if a naming exception is encountered + */ + @Override + public NamingEnumeration list(String name) + throws NamingException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("selectorContext.methodUsingString", "list", + name)); + } + + return getBoundContext().list(parseName(name)); + } + + + /** + * Enumerates the names bound in the named context, along with the + * objects bound to them. The contents of any subcontexts are not + * included. + *

    + * If a binding is added to or removed from this context, its effect on + * an enumeration previously returned is undefined. + * + * @param name the name of the context to list + * @return an enumeration of the bindings in this context. + * Each element of the enumeration is of type Binding. + * @throws NamingException if a naming exception is encountered + */ + @Override + public NamingEnumeration listBindings(Name name) + throws NamingException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("selectorContext.methodUsingName", + "listBindings", name)); + } + + return getBoundContext().listBindings(parseName(name)); + } + + + /** + * Enumerates the names bound in the named context, along with the + * objects bound to them. + * + * @param name the name of the context to list + * @return an enumeration of the bindings in this context. + * Each element of the enumeration is of type Binding. + * @throws NamingException if a naming exception is encountered + */ + @Override + public NamingEnumeration listBindings(String name) + throws NamingException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("selectorContext.methodUsingString", + "listBindings", name)); + } + + return getBoundContext().listBindings(parseName(name)); + } + + + /** + * Destroys the named context and removes it from the namespace. Any + * attributes associated with the name are also removed. Intermediate + * contexts are not destroyed. + *

    + * This method is idempotent. It succeeds even if the terminal atomic + * name is not bound in the target context, but throws + * NameNotFoundException if any of the intermediate contexts do not exist. + * + * In a federated naming system, a context from one naming system may be + * bound to a name in another. One can subsequently look up and perform + * operations on the foreign context using a composite name. However, an + * attempt destroy the context using this composite name will fail with + * NotContextException, because the foreign context is not a "subcontext" + * of the context in which it is bound. Instead, use unbind() to remove + * the binding of the foreign context. Destroying the foreign context + * requires that the destroySubcontext() be performed on a context from + * the foreign context's "native" naming system. + * + * @param name the name of the context to be destroyed; may not be empty + * @throws javax.naming.NameNotFoundException if an intermediate context + * does not exist + * @throws javax.naming.NotContextException if the name is bound but does + * not name a context, or does not name a context of the appropriate type + */ + @Override + public void destroySubcontext(Name name) + throws NamingException { + getBoundContext().destroySubcontext(parseName(name)); + } + + + /** + * Destroys the named context and removes it from the namespace. + * + * @param name the name of the context to be destroyed; may not be empty + * @throws javax.naming.NameNotFoundException if an intermediate context + * does not exist + * @throws javax.naming.NotContextException if the name is bound but does + * not name a context, or does not name a context of the appropriate type + */ + @Override + public void destroySubcontext(String name) + throws NamingException { + getBoundContext().destroySubcontext(parseName(name)); + } + + + /** + * Creates and binds a new context. Creates a new context with the given + * name and binds it in the target context (that named by all but + * terminal atomic component of the name). All intermediate contexts and + * the target context must already exist. + * + * @param name the name of the context to create; may not be empty + * @return the newly created context + * @throws javax.naming.NameAlreadyBoundException if name is already + * bound + * @throws javax.naming.directory.InvalidAttributesException if creation of the + * sub-context requires specification of mandatory attributes + * @throws NamingException if a naming exception is encountered + */ + @Override + public Context createSubcontext(Name name) + throws NamingException { + return getBoundContext().createSubcontext(parseName(name)); + } + + + /** + * Creates and binds a new context. + * + * @param name the name of the context to create; may not be empty + * @return the newly created context + * @throws javax.naming.NameAlreadyBoundException if name is already + * bound + * @throws javax.naming.directory.InvalidAttributesException if creation of the + * sub-context requires specification of mandatory attributes + * @throws NamingException if a naming exception is encountered + */ + @Override + public Context createSubcontext(String name) + throws NamingException { + return getBoundContext().createSubcontext(parseName(name)); + } + + + /** + * Retrieves the named object, following links except for the terminal + * atomic component of the name. If the object bound to name is not a + * link, returns the object itself. + * + * @param name the name of the object to look up + * @return the object bound to name, not following the terminal link + * (if any). + * @throws NamingException if a naming exception is encountered + */ + @Override + public Object lookupLink(Name name) + throws NamingException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("selectorContext.methodUsingName", + "lookupLink", name)); + } + + return getBoundContext().lookupLink(parseName(name)); + } + + + /** + * Retrieves the named object, following links except for the terminal + * atomic component of the name. + * + * @param name the name of the object to look up + * @return the object bound to name, not following the terminal link + * (if any). + * @throws NamingException if a naming exception is encountered + */ + @Override + public Object lookupLink(String name) + throws NamingException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("selectorContext.methodUsingString", + "lookupLink", name)); + } + + return getBoundContext().lookupLink(parseName(name)); + } + + + /** + * Retrieves the parser associated with the named context. In a + * federation of namespaces, different naming systems will parse names + * differently. This method allows an application to get a parser for + * parsing names into their atomic components using the naming convention + * of a particular naming system. Within any single naming system, + * NameParser objects returned by this method must be equal (using the + * equals() test). + * + * @param name the name of the context from which to get the parser + * @return a name parser that can parse compound names into their atomic + * components + * @throws NamingException if a naming exception is encountered + */ + @Override + public NameParser getNameParser(Name name) + throws NamingException { + return getBoundContext().getNameParser(parseName(name)); + } + + + /** + * Retrieves the parser associated with the named context. + * + * @param name the name of the context from which to get the parser + * @return a name parser that can parse compound names into their atomic + * components + * @throws NamingException if a naming exception is encountered + */ + @Override + public NameParser getNameParser(String name) + throws NamingException { + return getBoundContext().getNameParser(parseName(name)); + } + + + /** + * Composes the name of this context with a name relative to this context. + *

    + * Given a name (name) relative to this context, and the name (prefix) + * of this context relative to one of its ancestors, this method returns + * the composition of the two names using the syntax appropriate for the + * naming system(s) involved. That is, if name names an object relative + * to this context, the result is the name of the same object, but + * relative to the ancestor context. None of the names may be null. + * + * @param name a name relative to this context + * @param prefix the name of this context relative to one of its ancestors + * @return the composition of prefix and name + * @throws NamingException if a naming exception is encountered + */ + @Override + public Name composeName(Name name, Name prefix) + throws NamingException { + Name prefixClone = (Name) prefix.clone(); + return prefixClone.addAll(name); + } + + + /** + * Composes the name of this context with a name relative to this context. + * + * @param name a name relative to this context + * @param prefix the name of this context relative to one of its ancestors + * @return the composition of prefix and name + * @throws NamingException if a naming exception is encountered + */ + @Override + public String composeName(String name, String prefix) + throws NamingException { + return prefix + "/" + name; + } + + + /** + * Adds a new environment property to the environment of this context. If + * the property already exists, its value is overwritten. + * + * @param propName the name of the environment property to add; may not + * be null + * @param propVal the value of the property to add; may not be null + * @throws NamingException if a naming exception is encountered + */ + @Override + public Object addToEnvironment(String propName, Object propVal) + throws NamingException { + return getBoundContext().addToEnvironment(propName, propVal); + } + + + /** + * Removes an environment property from the environment of this context. + * + * @param propName the name of the environment property to remove; + * may not be null + * @throws NamingException if a naming exception is encountered + */ + @Override + public Object removeFromEnvironment(String propName) + throws NamingException { + return getBoundContext().removeFromEnvironment(propName); + } + + + /** + * Retrieves the environment in effect for this context. See class + * description for more details on environment properties. + * The caller should not make any changes to the object returned: their + * effect on the context is undefined. The environment of this context + * may be changed using addToEnvironment() and removeFromEnvironment(). + * + * @return the environment of this context; never null + * @throws NamingException if a naming exception is encountered + */ + @Override + public Hashtable getEnvironment() + throws NamingException { + return getBoundContext().getEnvironment(); + } + + + /** + * Closes this context. This method releases this context's resources + * immediately, instead of waiting for them to be released automatically + * by the garbage collector. + * This method is idempotent: invoking it on a context that has already + * been closed has no effect. Invoking any other method on a closed + * context is not allowed, and results in undefined behaviour. + * + * @throws NamingException if a naming exception is encountered + */ + @Override + public void close() + throws NamingException { + getBoundContext().close(); + } + + + /** + * Retrieves the full name of this context within its own namespace. + *

    + * Many naming services have a notion of a "full name" for objects in + * their respective namespaces. For example, an LDAP entry has a + * distinguished name, and a DNS record has a fully qualified name. This + * method allows the client application to retrieve this name. The string + * returned by this method is not a JNDI composite name and should not be + * passed directly to context methods. In naming systems for which the + * notion of full name does not make sense, + * OperationNotSupportedException is thrown. + * + * @return this context's name in its own namespace; never null + * @throws javax.naming.OperationNotSupportedException if the naming + * system does not have the notion of a full name + * @throws NamingException if a naming exception is encountered + */ + @Override + public String getNameInNamespace() + throws NamingException { + return prefix; + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Get the bound context. + * @return the Context bound with either the current thread or + * the current classloader + * @throws NamingException Bindings exception + */ + protected Context getBoundContext() + throws NamingException { + + if (initialContext) { + String ICName = IC_PREFIX; + if (ContextBindings.isThreadBound()) { + ICName += ContextBindings.getThreadName(); + } else if (ContextBindings.isClassLoaderBound()) { + ICName += ContextBindings.getClassLoaderName(); + } + Context initialContext = ContextBindings.getContext(ICName); + if (initialContext == null) { + // Allocating a new context and binding it to the appropriate + // name + initialContext = new NamingContext(env, ICName); + ContextBindings.bindContext(ICName, initialContext); + } + return initialContext; + } else { + if (ContextBindings.isThreadBound()) { + return ContextBindings.getThread(); + } else { + return ContextBindings.getClassLoader(); + } + } + + } + + + /** + * Strips the URL header. + * @param name The name + * @return the parsed name + * @throws NamingException if there is no "java:" header or if no + * naming context has been bound to this thread + */ + protected String parseName(String name) + throws NamingException { + + if ((!initialContext) && (name.startsWith(prefix))) { + return name.substring(prefixLength); + } else { + if (initialContext) { + return name; + } else { + throw new NamingException + (sm.getString("selectorContext.noJavaUrl")); + } + } + + } + + + /** + * Strips the URL header. + * @param name The name + * @return the parsed name + * @throws NamingException if there is no "java:" header or if no + * naming context has been bound to this thread + */ + protected Name parseName(Name name) + throws NamingException { + + if (!initialContext && !name.isEmpty() && + name.get(0).startsWith(prefix)) { + if (name.get(0).equals(prefix)) { + return name.getSuffix(1); + } else { + Name result = name.getSuffix(1); + result.add(0, name.get(0).substring(prefixLength)); + return result; + } + } else { + if (initialContext) { + return name; + } else { + throw new NamingException( + sm.getString("selectorContext.noJavaUrl")); + } + } + + } + + +} + diff --git a/java/org/apache/naming/ServiceRef.java b/java/org/apache/naming/ServiceRef.java new file mode 100644 index 0000000..d003538 --- /dev/null +++ b/java/org/apache/naming/ServiceRef.java @@ -0,0 +1,144 @@ +/* + * 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.naming; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.naming.StringRefAddr; + +/** + * Represents a reference web service. + * + * @author Fabien Carrion + */ +public class ServiceRef extends AbstractRef { + + private static final long serialVersionUID = 1L; + + + /** + * Default factory for this reference. + */ + public static final String DEFAULT_FACTORY = + org.apache.naming.factory.Constants.DEFAULT_SERVICE_FACTORY; + + + /** + * Service Classname address type. + */ + public static final String SERVICE_INTERFACE = "serviceInterface"; + + + /** + * ServiceQname address type. + */ + public static final String SERVICE_NAMESPACE = "service namespace"; + public static final String SERVICE_LOCAL_PART = "service local part"; + + + /** + * Wsdl Location address type. + */ + public static final String WSDL = "wsdl"; + + + /** + * Jaxrpcmapping address type. + */ + public static final String JAXRPCMAPPING = "jaxrpcmapping"; + + + /** + * port-component-ref/port-component-link address type. + */ + public static final String PORTCOMPONENTLINK = "portcomponentlink"; + + + /** + * port-component-ref/service-endpoint-interface address type. + */ + public static final String SERVICEENDPOINTINTERFACE = "serviceendpointinterface"; + + + /** + * The list to save the handler Reference objects, because they can't be + * saved in the addrs vector. + */ + private final List handlers = new CopyOnWriteArrayList<>(); + + + public ServiceRef(String refname, String serviceInterface, String[] serviceQname, + String wsdl, String jaxrpcmapping) { + this(refname, serviceInterface, serviceQname, wsdl, jaxrpcmapping, + null, null); + } + + + public ServiceRef(@SuppressWarnings("unused") String refname, + String serviceInterface, String[] serviceQname, + String wsdl, String jaxrpcmapping, + String factory, String factoryLocation) { + super(serviceInterface, factory, factoryLocation); + StringRefAddr refAddr = null; + if (serviceInterface != null) { + refAddr = new StringRefAddr(SERVICE_INTERFACE, serviceInterface); + add(refAddr); + } + if (serviceQname[0] != null) { + refAddr = new StringRefAddr(SERVICE_NAMESPACE, serviceQname[0]); + add(refAddr); + } + if (serviceQname[1] != null) { + refAddr = new StringRefAddr(SERVICE_LOCAL_PART, serviceQname[1]); + add(refAddr); + } + if (wsdl != null) { + refAddr = new StringRefAddr(WSDL, wsdl); + add(refAddr); + } + if (jaxrpcmapping != null) { + refAddr = new StringRefAddr(JAXRPCMAPPING, jaxrpcmapping); + add(refAddr); + } + } + + + /** + * Add and Get Handlers classes. + * @return the handler + */ + public HandlerRef getHandler() { + return handlers.remove(0); + } + + + public int getHandlersSize() { + return handlers.size(); + } + + + public void addHandler(HandlerRef handler) { + handlers.add(handler); + } + + + @Override + protected String getDefaultFactoryClassName() { + return DEFAULT_FACTORY; + } +} diff --git a/java/org/apache/naming/StringManager.java b/java/org/apache/naming/StringManager.java new file mode 100644 index 0000000..c34368a --- /dev/null +++ b/java/org/apache/naming/StringManager.java @@ -0,0 +1,183 @@ +/* + * 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.naming; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +/** + * An internationalization / localization helper class which reduces + * the bother of handling ResourceBundles and takes care of the + * common cases of message formatting which otherwise require the + * creation of Object arrays and such. + * + *

    The StringManager operates on a package basis. One StringManager + * per package can be created and accessed via the getManager method + * call. + * + *

    The StringManager will look for a ResourceBundle named by + * the package name given plus the suffix of "LocalStrings". In + * practice, this means that the localized information will be contained + * in a LocalStrings.properties file located in the package + * directory of the classpath. + * + *

    Please see the documentation for java.util.ResourceBundle for + * more information. + * + * @author James Duncan Davidson [duncan@eng.sun.com] + * @author James Todd [gonzo@eng.sun.com] + * @author Mel Martinez [mmartinez@g1440.com] + * @see java.util.ResourceBundle + */ +public class StringManager { + + /** + * The ResourceBundle for this StringManager. + */ + private final ResourceBundle bundle; + private final Locale locale; + + /** + * Creates a new StringManager for a given package. This is a + * private method and all access to it is arbitrated by the + * static getManager method call so that only one StringManager + * per package will be created. + * + * @param packageName Name of package to create StringManager for. + */ + private StringManager(String packageName) { + String bundleName = packageName + ".LocalStrings"; + ResourceBundle tempBundle = null; + try { + tempBundle = ResourceBundle.getBundle(bundleName, Locale.getDefault()); + } catch( MissingResourceException ex ) { + // Try from the current loader (that's the case for trusted apps) + // Should only be required if using a TC5 style classloader structure + // where common != shared != server + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if( cl != null ) { + try { + tempBundle = ResourceBundle.getBundle( + bundleName, Locale.getDefault(), cl); + } catch(MissingResourceException ex2) { + // Ignore + } + } + } + // Get the actual locale, which may be different from the requested one + if (tempBundle != null) { + locale = tempBundle.getLocale(); + } else { + locale = null; + } + bundle = tempBundle; + } + + /** + * Get a string from the underlying resource bundle or return + * null if the String is not found. + * + * @param key to desired resource String + * @return resource String matching key from underlying + * bundle or null if not found. + * @throws IllegalArgumentException if key is null. + */ + public String getString(String key) { + if(key == null){ + String msg = "key may not have a null value"; + + throw new IllegalArgumentException(msg); + } + + String str = null; + + try { + // Avoid NPE if bundle is null and treat it like an MRE + if (bundle != null) { + str = bundle.getString(key); + } + } catch(MissingResourceException mre) { + //bad: shouldn't mask an exception the following way: + // str = "[cannot find message associated with key '" + key + "' due to " + mre + "]"; + // because it hides the fact that the String was missing + // from the calling code. + //good: could just throw the exception (or wrap it in another) + // but that would probably cause much havoc on existing + // code. + //better: consistent with container pattern to + // simply return null. Calling code can then do + // a null check. + str = null; + } + + return str; + } + + /** + * Get a string from the underlying resource bundle and format + * it with the given set of arguments. + * + * @param key The key for the required message + * @param args The values to insert into the message + * + * @return The request string formatted with the provided arguments or the + * key if the key was not found. + */ + public String getString(final String key, final Object... args) { + String value = getString(key); + if (value == null) { + value = key; + } + + MessageFormat mf = new MessageFormat(value); + mf.setLocale(locale); + return mf.format(args, new StringBuffer(), null).toString(); + } + + // -------------------------------------------------------------- + // STATIC SUPPORT METHODS + // -------------------------------------------------------------- + + private static final Map managers = new HashMap<>(); + + /** + * Get the StringManager for a particular package. If a manager for + * a package already exists, it will be reused, else a new + * StringManager will be created and returned. + * + * @param packageName The package name + * + * @return The instance associated with the given package + */ + public static final synchronized StringManager getManager(String packageName) { + StringManager mgr = managers.get(packageName); + if (mgr == null) { + mgr = new StringManager(packageName); + managers.put(packageName, mgr); + } + return mgr; + } + + + public static final StringManager getManager(Class clazz) { + return getManager(clazz.getPackage().getName()); + } +} diff --git a/java/org/apache/naming/TransactionRef.java b/java/org/apache/naming/TransactionRef.java new file mode 100644 index 0000000..d772144 --- /dev/null +++ b/java/org/apache/naming/TransactionRef.java @@ -0,0 +1,59 @@ +/* + * 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.naming; + +/** + * Represents a reference address to a transaction. + * + * @author Remy Maucherat + */ +public class TransactionRef extends AbstractRef { + + private static final long serialVersionUID = 1L; + + + /** + * Default factory for this reference. + */ + public static final String DEFAULT_FACTORY = + org.apache.naming.factory.Constants.DEFAULT_TRANSACTION_FACTORY; + + + /** + * Resource Reference. + */ + public TransactionRef() { + this(null, null); + } + + + /** + * Resource Reference. + * + * @param factory The factory class + * @param factoryLocation The factory location + */ + public TransactionRef(String factory, String factoryLocation) { + super("jakarta.transaction.UserTransaction", factory, factoryLocation); + } + + + @Override + protected String getDefaultFactoryClassName() { + return DEFAULT_FACTORY; + } +} diff --git a/java/org/apache/naming/factory/BeanFactory.java b/java/org/apache/naming/factory/BeanFactory.java new file mode 100644 index 0000000..ff7b77e --- /dev/null +++ b/java/org/apache/naming/factory/BeanFactory.java @@ -0,0 +1,233 @@ +/* + * 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.naming.factory; + +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.Enumeration; +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.naming.ResourceRef; +import org.apache.naming.StringManager; + +/** + * Object factory for any Resource conforming to the JavaBean spec. + * + *

    This factory can be configured in a <Context> element + * in your conf/server.xml + * configuration file. An example of factory configuration is:

    + *
    + * <Resource name="jdbc/myDataSource" auth="SERVLET"
    + *   type="oracle.jdbc.pool.OracleConnectionCacheImpl"/>
    + * <ResourceParams name="jdbc/myDataSource">
    + *   <parameter>
    + *     <name>factory</name>
    + *     <value>org.apache.naming.factory.BeanFactory</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>driverType</name>
    + *     <value>thin</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>serverName</name>
    + *     <value>hue</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>networkProtocol</name>
    + *     <value>tcp</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>databaseName</name>
    + *     <value>XXXX</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>portNumber</name>
    + *     <value>NNNN</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>user</name>
    + *     <value>XXXX</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>password</name>
    + *     <value>XXXX</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>maxLimit</name>
    + *     <value>5</value>
    + *   </parameter>
    + * </ResourceParams>
    + * 
    + * + * @author Aner Perez [aner at ncstech.com] + */ +public class BeanFactory implements ObjectFactory { + + private static final StringManager sm = StringManager.getManager(BeanFactory.class); + + private final Log log = LogFactory.getLog(BeanFactory.class); // Not static + + /** + * Create a new Bean instance. + * + * @param obj The reference object describing the Bean + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) + throws NamingException { + + if (obj instanceof ResourceRef) { + + try { + Reference ref = (Reference) obj; + String beanClassName = ref.getClassName(); + Class beanClass = null; + ClassLoader tcl = Thread.currentThread().getContextClassLoader(); + try { + if (tcl != null) { + beanClass = tcl.loadClass(beanClassName); + } else { + beanClass = Class.forName(beanClassName); + } + } catch(ClassNotFoundException cnfe) { + NamingException ne = new NamingException(sm.getString("beanFactory.classNotFound", beanClassName)); + ne.initCause(cnfe); + throw ne; + } + + BeanInfo bi = Introspector.getBeanInfo(beanClass); + PropertyDescriptor[] pda = bi.getPropertyDescriptors(); + + Object bean = beanClass.getConstructor().newInstance(); + + // Look for the removed forceString option + RefAddr ra = ref.get("forceString"); + if (ra != null) { + log.warn(sm.getString("beanFactory.noForceString")); + } + + Enumeration e = ref.getAll(); + String value; + + while (e.hasMoreElements()) { + + ra = e.nextElement(); + String propName = ra.getType(); + + if (propName.equals(Constants.FACTORY) || + propName.equals("scope") || propName.equals("auth") || + propName.equals("forceString") || + propName.equals("singleton")) { + continue; + } + + value = (String)ra.getContent(); + + Object[] valueArray = new Object[1]; + + int i = 0; + for (i = 0; i < pda.length; i++) { + + if (pda[i].getName().equals(propName)) { + + Class propType = pda[i].getPropertyType(); + Method setProp = pda[i].getWriteMethod(); + + if (propType.equals(String.class)) { + valueArray[0] = value; + } else if (propType.equals(Character.class) || propType.equals(char.class)) { + valueArray[0] = Character.valueOf(value.charAt(0)); + } else if (propType.equals(Byte.class) || propType.equals(byte.class)) { + valueArray[0] = Byte.valueOf(value); + } else if (propType.equals(Short.class) || propType.equals(short.class)) { + valueArray[0] = Short.valueOf(value); + } else if (propType.equals(Integer.class) || propType.equals(int.class)) { + valueArray[0] = Integer.valueOf(value); + } else if (propType.equals(Long.class) || propType.equals(long.class)) { + valueArray[0] = Long.valueOf(value); + } else if (propType.equals(Float.class) || propType.equals(float.class)) { + valueArray[0] = Float.valueOf(value); + } else if (propType.equals(Double.class) || propType.equals(double.class)) { + valueArray[0] = Double.valueOf(value); + } else if (propType.equals(Boolean.class) || propType.equals(boolean.class)) { + valueArray[0] = Boolean.valueOf(value); + } else if (setProp != null) { + // This is a Tomcat specific extension and is not part of the + // Java Bean specification. + String setterName = setProp.getName(); + try { + setProp = bean.getClass().getMethod(setterName, String.class); + valueArray[0] = value; + } catch (NoSuchMethodException nsme) { + throw new NamingException(sm.getString( + "beanFactory.noStringConversion", propName, propType.getName())); + } + } else { + throw new NamingException(sm.getString( + "beanFactory.noStringConversion", propName, propType.getName())); + } + + if (setProp != null) { + setProp.invoke(bean, valueArray); + } else { + throw new NamingException(sm.getString("beanFactory.readOnlyProperty", propName)); + } + + break; + } + } + + if (i == pda.length) { + throw new NamingException(sm.getString("beanFactory.noSetMethod", propName)); + } + } + + return bean; + + } catch (java.beans.IntrospectionException ie) { + NamingException ne = new NamingException(ie.getMessage()); + ne.setRootCause(ie); + throw ne; + } catch (ReflectiveOperationException e) { + Throwable cause = e.getCause(); + if (cause instanceof ThreadDeath) { + throw (ThreadDeath) cause; + } + if (cause instanceof VirtualMachineError) { + throw (VirtualMachineError) cause; + } + NamingException ne = new NamingException(e.getMessage()); + ne.setRootCause(e); + throw ne; + } + + } else { + return null; + } + } +} diff --git a/java/org/apache/naming/factory/Constants.java b/java/org/apache/naming/factory/Constants.java new file mode 100644 index 0000000..b8690af --- /dev/null +++ b/java/org/apache/naming/factory/Constants.java @@ -0,0 +1,48 @@ +/* + * 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.naming.factory; + +/** + * Static constants for this package. + */ +public final class Constants { + + public static final String Package = "org.apache.naming.factory"; + + public static final String DEFAULT_RESOURCE_FACTORY = Package + ".ResourceFactory"; + + public static final String DEFAULT_RESOURCE_LINK_FACTORY = Package + ".ResourceLinkFactory"; + + public static final String DEFAULT_TRANSACTION_FACTORY = Package + ".TransactionFactory"; + + public static final String DEFAULT_RESOURCE_ENV_FACTORY = Package + ".ResourceEnvFactory"; + + public static final String DEFAULT_EJB_FACTORY = Package + ".EjbFactory"; + + public static final String DEFAULT_SERVICE_FACTORY = Package + ".webservices.ServiceRefFactory"; + + public static final String DEFAULT_HANDLER_FACTORY = Package + ".HandlerFactory"; + + public static final String DBCP_DATASOURCE_FACTORY = + "org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory"; + + public static final String OPENEJB_EJB_FACTORY = Package + ".OpenEjbFactory"; + + public static final String DEFAULT_LOOKUP_JNDI_FACTORY = Package + ".LookupFactory"; + + public static final String FACTORY = "factory"; +} diff --git a/java/org/apache/naming/factory/DataSourceLinkFactory.java b/java/org/apache/naming/factory/DataSourceLinkFactory.java new file mode 100644 index 0000000..ca360a9 --- /dev/null +++ b/java/org/apache/naming/factory/DataSourceLinkFactory.java @@ -0,0 +1,149 @@ +/* + * 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.naming.factory; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.SQLException; +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.sql.DataSource; + + + +/** + *

    Object factory for resource links for shared data sources.

    + * + */ +public class DataSourceLinkFactory extends ResourceLinkFactory { + + public static void setGlobalContext(Context newGlobalContext) { + ResourceLinkFactory.setGlobalContext(newGlobalContext); + } + // ------------------------------------------------- ObjectFactory Methods + + + /** + * Create a new DataSource instance. + * + * @param obj The reference object describing the DataSource + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) + throws NamingException { + Object result = super.getObjectInstance(obj, name, nameCtx, environment); + // Can we process this request? + if (result!=null) { + Reference ref = (Reference) obj; + RefAddr userAttr = ref.get("username"); + RefAddr passAttr = ref.get("password"); + if (userAttr != null && passAttr != null && userAttr.getContent() != null && passAttr.getContent() != null) { + result = wrapDataSource(result, userAttr.getContent().toString(), passAttr.getContent().toString()); + } + } + return result; + } + + protected Object wrapDataSource(Object datasource, String username, String password) throws NamingException { + try { + DataSourceHandler handler = + new DataSourceHandler((DataSource)datasource, username, password); + return Proxy.newProxyInstance(datasource.getClass().getClassLoader(), + datasource.getClass().getInterfaces(), handler); + }catch (Exception x) { + if (x instanceof InvocationTargetException) { + Throwable cause = x.getCause(); + if (cause instanceof ThreadDeath) { + throw (ThreadDeath) cause; + } + if (cause instanceof VirtualMachineError) { + throw (VirtualMachineError) cause; + } + if (cause instanceof Exception) { + x = (Exception) cause; + } + } + if (x instanceof NamingException) { + throw (NamingException)x; + } else { + NamingException nx = new NamingException(x.getMessage()); + nx.initCause(x); + throw nx; + } + } + } + + /** + * Simple wrapper class that will allow a user to configure a ResourceLink for a data source + * so that when {@link javax.sql.DataSource#getConnection()} is called, it will invoke + * {@link javax.sql.DataSource#getConnection(String, String)} with the preconfigured username and password. + */ + public static class DataSourceHandler implements InvocationHandler { + private final DataSource ds; + private final String username; + private final String password; + private final Method getConnection; + public DataSourceHandler(DataSource ds, String username, String password) throws Exception { + this.ds = ds; + this.username = username; + this.password = password; + getConnection = ds.getClass().getMethod("getConnection", String.class, String.class); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + if ("getConnection".equals(method.getName()) && (args==null || args.length==0)) { + args = new String[] {username,password}; + method = getConnection; + } else if ("unwrap".equals(method.getName())) { + return unwrap((Class)args[0]); + } + try { + return method.invoke(ds,args); + }catch (Throwable t) { + if (t instanceof InvocationTargetException + && t.getCause() != null) { + throw t.getCause(); + } else { + throw t; + } + } + } + + public Object unwrap(Class iface) throws SQLException { + if (iface == DataSource.class) { + return ds; + } else { + throw new SQLException(sm.getString("dataSourceLinkFactory.badWrapper", iface.getName())); + } + } + + } + + + + +} + diff --git a/java/org/apache/naming/factory/EjbFactory.java b/java/org/apache/naming/factory/EjbFactory.java new file mode 100644 index 0000000..0820e16 --- /dev/null +++ b/java/org/apache/naming/factory/EjbFactory.java @@ -0,0 +1,78 @@ +/* + * 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.naming.factory; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.naming.EjbRef; + +/** + * Object factory for EJBs. + * + * @author Remy Maucherat + */ +public class EjbFactory extends FactoryBase { + + @Override + protected boolean isReferenceTypeSupported(Object obj) { + return obj instanceof EjbRef; + } + + @Override + protected ObjectFactory getDefaultFactory(Reference ref) throws NamingException { + + ObjectFactory factory; + String javaxEjbFactoryClassName = System.getProperty( + "jakarta.ejb.Factory", Constants.OPENEJB_EJB_FACTORY); + try { + factory = (ObjectFactory) + Class.forName(javaxEjbFactoryClassName).getConstructor().newInstance(); + } catch(Throwable t) { + if (t instanceof NamingException) { + throw (NamingException) t; + } + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + NamingException ex = new NamingException + ("Could not create resource factory instance"); + ex.initCause(t); + throw ex; + } + return factory; + } + + @Override + protected Object getLinked(Reference ref) throws NamingException { + // If ejb-link has been specified, resolving the link using JNDI + RefAddr linkRefAddr = ref.get(EjbRef.LINK); + if (linkRefAddr != null) { + // Retrieving the EJB link + String ejbLink = linkRefAddr.getContent().toString(); + Object beanObj = (new InitialContext()).lookup(ejbLink); + return beanObj; + } + return null; + } +} diff --git a/java/org/apache/naming/factory/FactoryBase.java b/java/org/apache/naming/factory/FactoryBase.java new file mode 100644 index 0000000..a49c4b1 --- /dev/null +++ b/java/org/apache/naming/factory/FactoryBase.java @@ -0,0 +1,141 @@ +/* + * 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.naming.factory; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.naming.StringManager; + +/** + * Abstract base class that provides common functionality required by + * sub-classes. This class exists primarily to reduce code duplication. + */ +public abstract class FactoryBase implements ObjectFactory { + + private static final StringManager sm = StringManager.getManager(FactoryBase.class); + + /** + * Creates a new object instance. + * + * @param obj The reference object describing the object to create + */ + @Override + public final Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) throws Exception { + + if (isReferenceTypeSupported(obj)) { + Reference ref = (Reference) obj; + + Object linked = getLinked(ref); + if (linked != null) { + return linked; + } + + ObjectFactory factory = null; + RefAddr factoryRefAddr = ref.get(Constants.FACTORY); + if (factoryRefAddr != null) { + // Using the specified factory + String factoryClassName = factoryRefAddr.getContent().toString(); + // Loading factory + ClassLoader tcl = Thread.currentThread().getContextClassLoader(); + Class factoryClass = null; + try { + if (tcl != null) { + factoryClass = tcl.loadClass(factoryClassName); + } else { + factoryClass = Class.forName(factoryClassName); + } + } catch(ClassNotFoundException e) { + NamingException ex = new NamingException(sm.getString("factoryBase.factoryClassError")); + ex.initCause(e); + throw ex; + } + try { + factory = (ObjectFactory) factoryClass.getConstructor().newInstance(); + } catch(Throwable t) { + if (t instanceof NamingException) { + throw (NamingException) t; + } + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + NamingException ex = new NamingException(sm.getString("factoryBase.factoryCreationError")); + ex.initCause(t); + throw ex; + } + } else { + // Check for a default factory + factory = getDefaultFactory(ref); + } + + if (factory != null) { + return factory.getObjectInstance(obj, name, nameCtx, environment); + } else { + throw new NamingException(sm.getString("factoryBase.instanceCreationError")); + } + } + + return null; + } + + + /** + * Determines if this factory supports processing the provided reference + * object. + * + * @param obj The object to be processed + * + * @return true if this factory can process the object, + * otherwise false + */ + protected abstract boolean isReferenceTypeSupported(Object obj); + + /** + * If a default factory is available for the given reference type, create + * the default factory. + * + * @param ref The reference object to be processed + * + * @return The default factory for the given reference object or + * null if no default factory exists. + * + * @throws NamingException If the default factory cannot be created + */ + protected abstract ObjectFactory getDefaultFactory(Reference ref) + throws NamingException; + + /** + * If this reference is a link to another JNDI object, obtain that object. + * + * @param ref The reference object to be processed + * + * @return The linked object or null if linked objects are + * not supported by or not configured for this reference object + * @throws NamingException Error accessing linked object + */ + protected abstract Object getLinked(Reference ref) throws NamingException; +} diff --git a/java/org/apache/naming/factory/LocalStrings.properties b/java/org/apache/naming/factory/LocalStrings.properties new file mode 100644 index 0000000..cbd4335 --- /dev/null +++ b/java/org/apache/naming/factory/LocalStrings.properties @@ -0,0 +1,38 @@ +# 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. + +beanFactory.classNotFound=Class not found: [{0}] +beanFactory.noForceString=The forceString option has been removed as a security hardening measure. Instead, if the setter method doesn't use String, a primitive or a primitive wrapper, the factory will look for a method with the same name as the setter that accepts a String and use that if found. +beanFactory.noSetMethod=No set method found for property [{0}] +beanFactory.noStringConversion=String conversion for property [{0}] of type [{1}] not available +beanFactory.readOnlyProperty=Write not allowed for property [{0}] + +dataSourceLinkFactory.badWrapper=Not a wrapper for type [{0}] + +factoryBase.factoryClassError=Could not load resource factory class +factoryBase.factoryCreationError=Could not create resource factory instance +factoryBase.instanceCreationError=Could not create resource instance + +lookupFactory.circularReference=Found a circular reference involving [{0}] +lookupFactory.createFailed=Could not create instance of JNDI lookup factory class +lookupFactory.loadFailed=Could not load JNDI lookup factory class +lookupFactory.typeMismatch=The JNDI reference [{0}] was expected to be of type [{1}] but the lookup [{2}] return an object of type [{3}] + +resourceFactory.factoryCreationError=Could not create resource factory instance + +resourceLinkFactory.invalidGlobalContext=Caller provided invalid global context +resourceLinkFactory.nullType=The local resource link [{0}] that refers to global resource [{1}] does not specify the required attribute type +resourceLinkFactory.unknownType=The local resource link [{0}] that refers to global resource [{1}] specified the unknown type [{2}] +resourceLinkFactory.wrongType=The local resource link [{0}] that refers to global resource [{1}] was expected to return an instance of [{2}] but returned an instance of [{3}] diff --git a/java/org/apache/naming/factory/LocalStrings_cs.properties b/java/org/apache/naming/factory/LocalStrings_cs.properties new file mode 100644 index 0000000..5cad3fe --- /dev/null +++ b/java/org/apache/naming/factory/LocalStrings_cs.properties @@ -0,0 +1,18 @@ +# 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. + +lookupFactory.typeMismatch=Reference JNDI [{0}] se oÄekává typu [{1}], vyhledání [{2}] vÅ¡ak vrátilo objekt typu [{3}] + +resourceLinkFactory.nullType=Lokální zdroj [{0}], který odkazuje na globální zdroj [{1}], nespecifikuje požadovaný typ atributu diff --git a/java/org/apache/naming/factory/LocalStrings_es.properties b/java/org/apache/naming/factory/LocalStrings_es.properties new file mode 100644 index 0000000..926ef6e --- /dev/null +++ b/java/org/apache/naming/factory/LocalStrings_es.properties @@ -0,0 +1,20 @@ +# 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. + +lookupFactory.createFailed=No se pudo crear una instancia de la clase de fábrica JNDI lookup\n +lookupFactory.typeMismatch=La referencia JNDI [{0}] se esperaba que fuera de tipo [{1}] pero la búsqueda [{2}] devolvió un objeto tipo [{3}] + +resourceLinkFactory.nullType=El enlace del recurso local [{0}] que se refiere al recurso global [{1}] no especifica el atributo obligatorio "type" +resourceLinkFactory.unknownType=El enlace del recurso local [{0}] que apunta al recurso global especificado [{1}] de tipo desconocido [{2}]\n diff --git a/java/org/apache/naming/factory/LocalStrings_fr.properties b/java/org/apache/naming/factory/LocalStrings_fr.properties new file mode 100644 index 0000000..92aa087 --- /dev/null +++ b/java/org/apache/naming/factory/LocalStrings_fr.properties @@ -0,0 +1,38 @@ +# 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. + +beanFactory.classNotFound=Class non trouvée: [{0}] +beanFactory.noForceString=L'option forceString a été enlevée pour renforcer la sécurité. A la place si la méthode set n'utilise pas une String, un type primitif ou l'objet équivalent, la fabrique recherchera une méthode avec le même nom que le set qui utilise une String et utilisera cela si une est trouvée. +beanFactory.noSetMethod=Aucune méthode set trouvée pour la propriété [{0}] +beanFactory.noStringConversion=La conversion en String de la propriété [{0}] de type [{1}] n''est pas disponible +beanFactory.readOnlyProperty=Il n''est pas possible de modifier la propriété [{0}] + +dataSourceLinkFactory.badWrapper=Pas un enrobeur pour le type [{0}] + +factoryBase.factoryClassError=Impossible de charger la classe de la fabrique de ressources +factoryBase.factoryCreationError=Impossible de créer l'instance de la fabrique de ressources +factoryBase.instanceCreationError=Impossible de créer l'instance de la ressource + +lookupFactory.circularReference=Trouvé une référence circulaire avec [{0}] +lookupFactory.createFailed=Echec de création de l'instance de la classe de fabrique de recherche JNDI +lookupFactory.loadFailed=Echec de chargement de la classe de fabrique de recherche JNDI +lookupFactory.typeMismatch=La référence JNDI [{0}] devrait être de type [{1}] mais la recherche [{2}] retourne un objet de type [{3}] + +resourceFactory.factoryCreationError=Impossible de créer une instance de la fabrique de ressources + +resourceLinkFactory.invalidGlobalContext=L'appelant a fourni un contexte global invalide +resourceLinkFactory.nullType=Le lien local de ressource [{0}] qui se réfère à la ressource globale [{1}] ne spécifie pas le type d''attribut requis +resourceLinkFactory.unknownType=Le lien local de ressource [{0}] qui se réfère à la ressource globale [{1}] a spécifié le type inconnu [{2}] +resourceLinkFactory.wrongType=Le lien de ressource local [{0}] qui se réfère à la ressource globale [{1}] devait renvoyer une instance de [{2}] mais à renvoyé une instance de [{3}] diff --git a/java/org/apache/naming/factory/LocalStrings_ja.properties b/java/org/apache/naming/factory/LocalStrings_ja.properties new file mode 100644 index 0000000..4700aef --- /dev/null +++ b/java/org/apache/naming/factory/LocalStrings_ja.properties @@ -0,0 +1,38 @@ +# 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. + +beanFactory.classNotFound=クラスãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“: [{0}] +beanFactory.noForceString=forceStringオプションã¯ã€ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£å¼·åŒ–ç­–ã¨ã—ã¦å‰Šé™¤ã•ã‚Œã¾ã—ãŸã€‚ 代ã‚ã‚Šã«ã€setterメソッドãŒStringã€ãƒ—リミティブã€ã¾ãŸã¯ãƒ—リミティブラッパーを使用ã—ãªã„å ´åˆã€ãƒ•ã‚¡ã‚¯ãƒˆãƒªã¯ã€setterã¨åŒã˜åå‰ã§Stringã‚’å—ã‘入れるメソッドを探ã—ã€è¦‹ã¤ã‹ã£ãŸå ´åˆã¯ãれを使用ã—ã¾ã™ã€‚ +beanFactory.noSetMethod=プロパティ [{0}] ã®setメソッドãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +beanFactory.noStringConversion=タイプ [{1}] ã®ãƒ—ロパティ [{0}] ã®æ–‡å­—列変æ›ã¯ä½¿ç”¨ã§ãã¾ã›ã‚“ +beanFactory.readOnlyProperty=プロパティ [{0}] ã¯æ›¸ãè¾¼ã¿ãŒè¨±å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“ + +dataSourceLinkFactory.badWrapper=クラス [{0}] ã®ãƒ©ãƒƒãƒ‘ーã§ã¯ã‚ã‚Šã¾ã›ã‚“。 + +factoryBase.factoryClassError=リソースファクトリクラスを読ã¿è¾¼ã‚ã¾ã›ã‚“ +factoryBase.factoryCreationError=リソースファクトリインスタンスを生æˆã§ãã¾ã›ã‚“ +factoryBase.instanceCreationError=リソースインスタンスを生æˆã§ãã¾ã›ã‚“ + +lookupFactory.circularReference=[{0}]ã‚’å«ã‚€å¾ªç’°å‚ç…§ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚ +lookupFactory.createFailed=JNDI lookup ファクトリークラスã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’作æˆã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +lookupFactory.loadFailed=JNDIルックアップファクトリクラスをロードã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +lookupFactory.typeMismatch=クラス [{1}] を期待ã™ã‚‹ JNDI å‚ç…§ [{0}]ã«ã€lookup [{2}] ã¯ã‚¯ãƒ©ã‚¹ [{3}] ã®ã‚ªãƒ–ジェクトを返å´ã—ã¾ã—ãŸã€‚ + +resourceFactory.factoryCreationError=リソースファクトリーã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’作æˆã§ãã¾ã›ã‚“。 + +resourceLinkFactory.invalidGlobalContext=引数ã«ä¸æ­£ãªå…±é€šã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +resourceLinkFactory.nullType=グローãƒãƒ«ãƒªã‚½ãƒ¼ã‚¹ [{1}] ã‚’å‚ç…§ã™ã‚‹ãƒ­ãƒ¼ã‚«ãƒ«ãƒªã‚½ãƒ¼ã‚¹ãƒªãƒ³ã‚¯ [{0}] ã«å¿…è¦ãªå±žæ€§ãŒã‚ã‚Šã¾ã›ã‚“。 +resourceLinkFactory.unknownType=グローãƒãƒ«ãƒªã‚½ãƒ¼ã‚¹ [{1}] ã‚’å‚ç…§ã™ã‚‹ãƒ­ãƒ¼ã‚«ãƒ«ãƒªã‚½ãƒ¼ã‚¹ãƒªãƒ³ã‚¯ [{0}] ã«æœªçŸ¥ã®ã‚¯ãƒ©ã‚¹ [{2}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +resourceLinkFactory.wrongType=グローãƒãƒ«ãƒªã‚½ãƒ¼ã‚¹ [{1}] ã‚’å‚ç…§ã™ã‚‹ãƒ­ãƒ¼ã‚«ãƒ«ãƒªã‚½ãƒ¼ã‚¹ãƒªãƒ³ã‚¯ [{0}] 㯠[{2}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’è¿”ã™ã¨äºˆæƒ³ã•ã‚Œã¾ã—ãŸãŒã€[{3}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’è¿”ã—ã¾ã—㟠diff --git a/java/org/apache/naming/factory/LocalStrings_ko.properties b/java/org/apache/naming/factory/LocalStrings_ko.properties new file mode 100644 index 0000000..1df0132 --- /dev/null +++ b/java/org/apache/naming/factory/LocalStrings_ko.properties @@ -0,0 +1,38 @@ +# 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. + +beanFactory.classNotFound=í´ëž˜ìŠ¤ë¥¼ ì°¾ì„ ìˆ˜ 없습니다. : [{0}] +beanFactory.noForceString=forceString ì˜µì…˜ì€ ë³´ì•ˆì„± 강화를 위해 제거ë˜ì—ˆìŠµë‹ˆë‹¤. 대신ì—, ë§Œì¼ setter 메소드가 문ìžì—´ 타입, primitive 타입 ë˜ëŠ” primitive wrapper íƒ€ìž…ì„ ì‚¬ìš©í•˜ì§€ 않는 경우ì—는, 팩토리가 ë™ì¼í•œ ì´ë¦„ì„ ê°€ì§„ 메소드들 중 문ìžì—´ì„ 받는 setter 메소드가 있는 ë•Œì—는 ê·¸ 메소드를 사용하게 ë©ë‹ˆë‹¤. +beanFactory.noSetMethod=프로í¼í‹° [{0}]ì„(를) 위한 setter 메소드가 없습니다. +beanFactory.noStringConversion=타입 [{1}]ì˜ í”„ë¡œí¼í‹° [{0}]ì„(를) 위한 문ìžì—´ ë³€í™˜ì„ ì ìš©í•  수 없습니다. +beanFactory.readOnlyProperty=[{0}] 프로í¼í‹°ì— 대한 쓰기가 허용ë˜ì§€ 않습니다. + +dataSourceLinkFactory.badWrapper=타입 [{0}]ì„(를) 위한 wrapperê°€ 아닙니다. + +factoryBase.factoryClassError=리소스 팩토리 í´ëž˜ìŠ¤ë¥¼ 로드하지 못했습니다. +factoryBase.factoryCreationError=리소스 팩토리 ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•˜ì§€ 못했습니다. +factoryBase.instanceCreationError=리소스 ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•˜ì§€ 못했습니다. + +lookupFactory.circularReference=[{0}]ì„(를) 수반한 순환 참조를 발견했습니다. +lookupFactory.createFailed=JNDI lookup 팩토리 í´ëž˜ìŠ¤ì˜ ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•  수 없었습니다. +lookupFactory.loadFailed=JNDI lookup 팩토리 í´ëž˜ìŠ¤ë¥¼ 로드할 수 없었습니다. +lookupFactory.typeMismatch=JNDI 참조 [{0}]ì€(는) íƒ€ìž…ì´ [{1}]ì´ì–´ì•¼ 하지만, lookup으로 찾아진 ê°ì²´ [{2}]ì€(는) íƒ€ìž…ì´ [{3}]입니다. + +resourceFactory.factoryCreationError=리소스 팩토리 ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•  수 없었습니다. + +resourceLinkFactory.invalidGlobalContext=호출ìžê°€, 유효하지 ì•Šì€ ê¸€ë¡œë²Œ 컨í…스트를 제공했습니다. +resourceLinkFactory.nullType=글로벌 리소스 [{1}]ì„(를) 참조하는 해당 로컬 리소스 ë§í¬ [{0}]ì´(ê°€) 필수ì ì¸ ì†ì„± íƒ€ìž…ì„ ì§€ì •í•˜ì§€ 않았습니다. +resourceLinkFactory.unknownType=글로벌 리소스 [{1}]ì„(를) 참조하는 해당 로컬 리소스 ë§í¬ [{0}]ì´(ê°€), ì•Œ 수 없는 타입 [{2}]으로 지정ë˜ì–´ 있습니다. +resourceLinkFactory.wrongType=글로벌 리소스 [{1}]ì„(를) 참조하는 해당 로컬 리소스 ë§í¬ [{0}]ì€(는), 타입 [{2}]ì˜ ì¸ìŠ¤í„´ìŠ¤í‹€ 반환할 것으로 기대ë˜ì—ˆì§€ë§Œ, ì •ìž‘ 타입 [{3}]ì˜ ì¸ìŠ¤í„´ìŠ¤ë¥¼ 반환했습니다. diff --git a/java/org/apache/naming/factory/LocalStrings_pt_BR.properties b/java/org/apache/naming/factory/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..6def3de --- /dev/null +++ b/java/org/apache/naming/factory/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +resourceLinkFactory.nullType=A ligação do recurso local [{0}] que referencia o recurso global [{1}] não especifica o tipo de atributo exigido. diff --git a/java/org/apache/naming/factory/LocalStrings_zh_CN.properties b/java/org/apache/naming/factory/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..b09003e --- /dev/null +++ b/java/org/apache/naming/factory/LocalStrings_zh_CN.properties @@ -0,0 +1,38 @@ +# 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. + +beanFactory.classNotFound=未找到该类:[{0}] +beanFactory.noForceString=为了安全期间,forceString 选项已被删除。相å,如果setter方法没有使用String(原始类型或原始包装类型),工厂将寻找一个与setteråŒå的接å—String的方法,如果找到的è¯å°±ä½¿ç”¨è¯¥æ–¹æ³•ã€‚ +beanFactory.noSetMethod=属性[{0}]未找到set方法 +beanFactory.noStringConversion=类型 [{1}] 的属性 [{0}] 的字符串转æ¢ä¸å¯ç”¨ +beanFactory.readOnlyProperty=属性[{0}]为åªè¯» + +dataSourceLinkFactory.badWrapper=ä¸æ˜¯ç±»åž‹[{0}]的包装 + +factoryBase.factoryClassError=无法加载资æºå·¥åŽ‚ç±» +factoryBase.factoryCreationError=无法创建资æºå·¥åŽ‚实例 +factoryBase.instanceCreationError=无法创建资æºå®žä¾‹ + +lookupFactory.circularReference=找到一个涉åŠ[{0}]的循环引用 +lookupFactory.createFailed=无法创建JNDI查找工厂类实例 +lookupFactory.loadFailed=无法加载JNDI查找工厂类 +lookupFactory.typeMismatch=期望JNDI引用[{0}]的类型为[{1}],但查找[{2}]返回类型为[{3}]的对象 + +resourceFactory.factoryCreationError=无法创建资æºå·¥åŽ‚实例 + +resourceLinkFactory.invalidGlobalContext=调用方æ供的全局上下文无效 +resourceLinkFactory.nullType=å¼•ç”¨å…¨å±€èµ„æº [{1}] 的本地资æºé“¾æŽ¥ [{0}] 未指定所需的属性类型 +resourceLinkFactory.unknownType=引用全局资æº[{1}]的本地资æºé“¾æŽ¥[{0}]指定了未知类型[{2}] +resourceLinkFactory.wrongType=引用全局资æº[{1}]的本地资æºé“¾æŽ¥[{0}]应返回[{2}]的实例,但返回了[{3}]的实例 diff --git a/java/org/apache/naming/factory/LookupFactory.java b/java/org/apache/naming/factory/LookupFactory.java new file mode 100644 index 0000000..672763d --- /dev/null +++ b/java/org/apache/naming/factory/LookupFactory.java @@ -0,0 +1,150 @@ +/* + * 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.naming.factory; + +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Set; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.naming.LookupRef; +import org.apache.naming.StringManager; + +/** + * Object factory for lookups. + */ +public class LookupFactory implements ObjectFactory { + + private static final Log log = LogFactory.getLog(LookupFactory.class); + private static final StringManager sm = StringManager.getManager(LookupFactory.class); + + private static final ThreadLocal> names = ThreadLocal.withInitial(HashSet::new); + + /** + * Create a new Resource env instance. + * + * @param obj The reference object describing the DataSource + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) throws Exception { + + String lookupName = null; + Object result = null; + + if (obj instanceof LookupRef) { + Reference ref = (Reference) obj; + ObjectFactory factory = null; + RefAddr lookupNameRefAddr = ref.get(LookupRef.LOOKUP_NAME); + if (lookupNameRefAddr != null) { + lookupName = lookupNameRefAddr.getContent().toString(); + } + + try { + if (lookupName != null) { + if (!names.get().add(lookupName)) { + String msg = sm.getString("lookupFactory.circularReference", lookupName); + NamingException ne = new NamingException(msg); + log.warn(msg, ne); + throw ne; + } + } + RefAddr factoryRefAddr = ref.get(Constants.FACTORY); + if (factoryRefAddr != null) { + // Using the specified factory + String factoryClassName = factoryRefAddr.getContent().toString(); + // Loading factory + ClassLoader tcl = Thread.currentThread().getContextClassLoader(); + Class factoryClass = null; + if (tcl != null) { + try { + factoryClass = tcl.loadClass(factoryClassName); + } catch (ClassNotFoundException e) { + NamingException ex = new NamingException( + sm.getString("lookupFactory.loadFailed")); + ex.initCause(e); + throw ex; + } + } else { + try { + factoryClass = Class.forName(factoryClassName); + } catch (ClassNotFoundException e) { + NamingException ex = new NamingException( + sm.getString("lookupFactory.loadFailed")); + ex.initCause(e); + throw ex; + } + } + if (factoryClass != null) { + try { + factory = (ObjectFactory) factoryClass.getConstructor().newInstance(); + } catch (Throwable t) { + if (t instanceof NamingException) { + throw (NamingException) t; + } + NamingException ex = new NamingException( + sm.getString("lookupFactory.createFailed")); + ex.initCause(t); + throw ex; + } + } + } + // Note: No defaults here + if (factory != null) { + result = factory.getObjectInstance(obj, name, nameCtx, environment); + } else { + if (lookupName == null) { + throw new NamingException(sm.getString("lookupFactory.createFailed")); + } else { + result = new InitialContext().lookup(lookupName); + } + } + + Class clazz = Class.forName(ref.getClassName()); + if (result != null && !clazz.isAssignableFrom(result.getClass())) { + String msg = sm.getString("lookupFactory.typeMismatch", + name, ref.getClassName(), lookupName, result.getClass().getName()); + NamingException ne = new NamingException(msg); + log.warn(msg, ne); + // Close the resource we no longer need if we know how to do so + if (result instanceof AutoCloseable) { + try { + ((AutoCloseable) result).close(); + } catch (Exception e) { + // Ignore + } + } + throw ne; + } + } finally { + names.get().remove(lookupName); + } + } + + + return result; + } +} diff --git a/java/org/apache/naming/factory/MailSessionFactory.java b/java/org/apache/naming/factory/MailSessionFactory.java new file mode 100644 index 0000000..29c902f --- /dev/null +++ b/java/org/apache/naming/factory/MailSessionFactory.java @@ -0,0 +1,153 @@ +/* + * 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.naming.factory; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import jakarta.mail.Authenticator; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; + +/** + *

    Factory class that creates a JNDI named JavaMail Session factory, + * which can be used for managing inbound and outbound electronic mail + * messages via JavaMail APIs. All messaging environment properties + * described in the JavaMail Specification may be passed to the Session + * factory; however the following properties are the most commonly used:

    + *
      + *
    • + *
    • mail.smtp.host - Hostname for outbound transport + * connections. Defaults to localhost if not specified.
    • + *
    + * + *

    This factory can be configured in a + * <Context> element in your conf/server.xml + * configuration file. An example of factory configuration is:

    + *
    + * <Resource name="mail/smtp" auth="CONTAINER"
    + *           type="jakarta.mail.Session"/>
    + * <ResourceParams name="mail/smtp">
    + *   <parameter>
    + *     <name>factory</name>
    + *     <value>org.apache.naming.factory.MailSessionFactory</value>
    + *   </parameter>
    + *   <parameter>
    + *     <name>mail.smtp.host</name>
    + *     <value>mail.mycompany.com</value>
    + *   </parameter>
    + * </ResourceParams>
    + * 
    + * + * @author Craig R. McClanahan + */ +public class MailSessionFactory implements ObjectFactory { + + + /** + * The Java type for which this factory knows how to create objects. + */ + protected static final String factoryType = "jakarta.mail.Session"; + + + /** + * Create and return an object instance based on the specified + * characteristics. + * + * @param refObj Reference information containing our parameters, or null + * if there are no parameters + * @param name The name of this object, relative to context, or null + * if there is no name + * @param context The context to which name is relative, or null if name + * is relative to the default initial context + * @param env Environment variables, or null if there are none + * + * @exception Exception if an error occurs during object creation + */ + @Override + public Object getObjectInstance(Object refObj, Name name, Context context, + Hashtable env) throws Exception { + + // Return null if we cannot create an object of the requested type + final Reference ref = (Reference) refObj; + if (!ref.getClassName().equals(factoryType)) { + return null; + } + + // Create a new Session inside a doPrivileged block, so that JavaMail + // can read its default properties without throwing Security + // exceptions. + // + // Bugzilla 31288, 33077: add support for authentication. + return AccessController.doPrivileged((PrivilegedAction) () -> { + + // Create the JavaMail properties we will use + Properties props = new Properties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.host", "localhost"); + + String password = null; + + Enumeration attrs = ref.getAll(); + while (attrs.hasMoreElements()) { + RefAddr attr = attrs.nextElement(); + if ("factory".equals(attr.getType())) { + continue; + } + + if ("password".equals(attr.getType())) { + password = (String) attr.getContent(); + continue; + } + + props.put(attr.getType(), attr.getContent()); + } + + Authenticator auth = null; + if (password != null) { + String user = props.getProperty("mail.smtp.user"); + if(user == null) { + user = props.getProperty("mail.user"); + } + + if(user != null) { + final PasswordAuthentication pa = new PasswordAuthentication(user, password); + auth = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return pa; + } + }; + } + } + + // Create and return the new Session object + Session session = Session.getInstance(props, auth); + return session; + + }); + } +} diff --git a/java/org/apache/naming/factory/OpenEjbFactory.java b/java/org/apache/naming/factory/OpenEjbFactory.java new file mode 100644 index 0000000..1c8e18e --- /dev/null +++ b/java/org/apache/naming/factory/OpenEjbFactory.java @@ -0,0 +1,89 @@ +/* + * 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.naming.factory; + +import java.util.Hashtable; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.naming.EjbRef; + +/** + * Object factory for EJBs. + * + * @author Jacek Laskowski + * @author Remy Maucherat + */ +public class OpenEjbFactory implements ObjectFactory { + + + // -------------------------------------------------------------- Constants + + + protected static final String DEFAULT_OPENEJB_FACTORY = + "org.openejb.client.LocalInitialContextFactory"; + + + // -------------------------------------------------- ObjectFactory Methods + + + /** + * Create a new EJB instance using OpenEJB. + * + * @param obj The reference object describing the DataSource + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) + throws Exception { + + Object beanObj = null; + + if (obj instanceof EjbRef) { + + Reference ref = (Reference) obj; + + String factory = DEFAULT_OPENEJB_FACTORY; + RefAddr factoryRefAddr = ref.get("openejb.factory"); + if (factoryRefAddr != null) { + // Retrieving the OpenEJB factory + factory = factoryRefAddr.getContent().toString(); + } + + Properties env = new Properties(); + env.put(Context.INITIAL_CONTEXT_FACTORY, factory); + + RefAddr linkRefAddr = ref.get("openejb.link"); + if (linkRefAddr != null) { + String ejbLink = linkRefAddr.getContent().toString(); + beanObj = (new InitialContext(env)).lookup(ejbLink); + } + + } + + return beanObj; + + } + + +} diff --git a/java/org/apache/naming/factory/ResourceEnvFactory.java b/java/org/apache/naming/factory/ResourceEnvFactory.java new file mode 100644 index 0000000..b999c40 --- /dev/null +++ b/java/org/apache/naming/factory/ResourceEnvFactory.java @@ -0,0 +1,47 @@ +/* + * 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.naming.factory; + +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.naming.ResourceEnvRef; + +/** + * Object factory for Resources env. + * + * @author Remy Maucherat + */ +public class ResourceEnvFactory extends FactoryBase { + + @Override + protected boolean isReferenceTypeSupported(Object obj) { + return obj instanceof ResourceEnvRef; + } + + @Override + protected ObjectFactory getDefaultFactory(Reference ref) { + // No default factory supported. + return null; + } + + @Override + protected Object getLinked(Reference ref) { + // Not supported + return null; + } +} diff --git a/java/org/apache/naming/factory/ResourceFactory.java b/java/org/apache/naming/factory/ResourceFactory.java new file mode 100644 index 0000000..00c3b8b --- /dev/null +++ b/java/org/apache/naming/factory/ResourceFactory.java @@ -0,0 +1,88 @@ +/* + * 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.naming.factory; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.naming.ResourceRef; +import org.apache.naming.StringManager; + +/** + * Object factory for Resources. + * + * @author Remy Maucherat + */ +public class ResourceFactory extends FactoryBase { + + private static final StringManager sm = StringManager.getManager(ResourceFactory.class); + + @Override + protected boolean isReferenceTypeSupported(Object obj) { + return obj instanceof ResourceRef; + } + + @Override + protected ObjectFactory getDefaultFactory(Reference ref) throws NamingException { + + ObjectFactory factory = null; + + if (ref.getClassName().equals("javax.sql.DataSource")) { + String javaxSqlDataSourceFactoryClassName = + System.getProperty("javax.sql.DataSource.Factory", + Constants.DBCP_DATASOURCE_FACTORY); + try { + factory = (ObjectFactory) Class.forName( + javaxSqlDataSourceFactoryClassName).getConstructor().newInstance(); + } catch (Exception e) { + NamingException ex = new NamingException(sm.getString("resourceFactory.factoryCreationError")); + ex.initCause(e); + throw ex; + } + } else if (ref.getClassName().equals("jakarta.mail.Session")) { + String javaxMailSessionFactoryClassName = + System.getProperty("jakarta.mail.Session.Factory", + "org.apache.naming.factory.MailSessionFactory"); + try { + factory = (ObjectFactory) Class.forName( + javaxMailSessionFactoryClassName).getConstructor().newInstance(); + } catch(Throwable t) { + if (t instanceof NamingException) { + throw (NamingException) t; + } + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + NamingException ex = new NamingException(sm.getString("resourceFactory.factoryCreationError")); + ex.initCause(t); + throw ex; + } + } + + return factory; + } + + @Override + protected Object getLinked(Reference ref) { + // Not supported + return null; + } +} diff --git a/java/org/apache/naming/factory/ResourceLinkFactory.java b/java/org/apache/naming/factory/ResourceLinkFactory.java new file mode 100644 index 0000000..17ca733 --- /dev/null +++ b/java/org/apache/naming/factory/ResourceLinkFactory.java @@ -0,0 +1,170 @@ +/* + * 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.naming.factory; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.naming.ResourceLinkRef; +import org.apache.naming.StringManager; + +/** + *

    Object factory for resource links.

    + * + * @author Remy Maucherat + */ +public class ResourceLinkFactory implements ObjectFactory { + + // ------------------------------------------------------- Static Variables + + protected static final StringManager sm = StringManager.getManager(ResourceLinkFactory.class); + + /** + * Global naming context. + */ + private static Context globalContext = null; + + private static Map> globalResourceRegistrations = + new ConcurrentHashMap<>(); + + // --------------------------------------------------------- Public Methods + + /** + * Set the global context (note: can only be used once). + * + * @param newGlobalContext new global context value + */ + public static void setGlobalContext(Context newGlobalContext) { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new RuntimePermission( + ResourceLinkFactory.class.getName() + ".setGlobalContext")); + } + globalContext = newGlobalContext; + } + + + public static void registerGlobalResourceAccess(Context globalContext, String localName, + String globalName) { + validateGlobalContext(globalContext); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + // Web application initialization is single threaded so this is + // safe. + globalResourceRegistrations.computeIfAbsent(cl, k -> new HashMap<>()).put(localName, globalName); + } + + + public static void deregisterGlobalResourceAccess(Context globalContext, String localName) { + validateGlobalContext(globalContext); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Map registrations = globalResourceRegistrations.get(cl); + if (registrations != null) { + registrations.remove(localName); + } + } + + + public static void deregisterGlobalResourceAccess(Context globalContext) { + validateGlobalContext(globalContext); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + globalResourceRegistrations.remove(cl); + } + + + private static void validateGlobalContext(Context globalContext) { + if (ResourceLinkFactory.globalContext != null && + ResourceLinkFactory.globalContext != globalContext) { + throw new SecurityException(sm.getString("resourceLinkFactory.invalidGlobalContext")); + } + } + + + private static boolean validateGlobalResourceAccess(String globalName) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + while (cl != null) { + Map registrations = globalResourceRegistrations.get(cl); + if (registrations != null && registrations.containsValue(globalName)) { + return true; + } + cl = cl.getParent(); + } + return false; + } + + + // -------------------------------------------------- ObjectFactory Methods + + /** + * Create a new DataSource instance. + * + * @param obj The reference object describing the DataSource + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) throws NamingException { + + if (!(obj instanceof ResourceLinkRef)) { + return null; + } + + // Can we process this request? + Reference ref = (Reference) obj; + + // Read the global ref addr + String globalName = null; + RefAddr refAddr = ref.get(ResourceLinkRef.GLOBALNAME); + if (refAddr != null) { + globalName = refAddr.getContent().toString(); + // Confirm that the current web application is currently configured + // to access the specified global resource + if (!validateGlobalResourceAccess(globalName)) { + return null; + } + Object result = null; + result = globalContext.lookup(globalName); + // Check the expected type + String expectedClassName = ref.getClassName(); + if (expectedClassName == null) { + throw new IllegalArgumentException( + sm.getString("resourceLinkFactory.nullType", name, globalName)); + } + try { + Class expectedClazz = Class.forName( + expectedClassName, true, Thread.currentThread().getContextClassLoader()); + if (!expectedClazz.isAssignableFrom(result.getClass())) { + throw new IllegalArgumentException(sm.getString("resourceLinkFactory.wrongType", + name, globalName, expectedClassName, result.getClass().getName())); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(sm.getString("resourceLinkFactory.unknownType", + name, globalName, expectedClassName), e); + } + return result; + } + + return null; + } +} diff --git a/java/org/apache/naming/factory/SendMailFactory.java b/java/org/apache/naming/factory/SendMailFactory.java new file mode 100644 index 0000000..4276d3c --- /dev/null +++ b/java/org/apache/naming/factory/SendMailFactory.java @@ -0,0 +1,127 @@ +/* + * 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.naming.factory; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimePartDataSource; + +/** + * Factory class that creates a JNDI named javamail MimePartDataSource + * object which can be used for sending email using SMTP. + *

    + * Can be configured in the Context scope + * of your server.xml configuration file. + *

    + * Example: + *

    + * <Resource name="mail/send" auth="CONTAINER"
    + *           type="jakarta.mail.internet.MimePartDataSource"/>
    + * <ResourceParams name="mail/send">
    + *   <parameter><name>factory</name>
    + *     <value>org.apache.naming.factory.SendMailFactory</value>
    + *   </parameter>
    + *   <parameter><name>mail.smtp.host</name>
    + *     <value>your.smtp.host</value>
    + *   </parameter>
    + *   <parameter><name>mail.smtp.user</name>
    + *     <value>someuser</value>
    + *   </parameter>
    + *   <parameter><name>mail.from</name>
    + *     <value>someuser@some.host</value>
    + *   </parameter>
    + *   <parameter><name>mail.smtp.sendpartial</name>
    + *     <value>true</value>
    + *   </parameter>
    + *  <parameter><name>mail.smtp.dsn.notify</name>
    + *     <value>FAILURE</value>
    + *   </parameter>
    + *   <parameter><name>mail.smtp.dsn.ret</name>
    + *     <value>FULL</value>
    + *   </parameter>
    + * </ResourceParams>
    + * 
    + * + * @author Glenn Nielsen Rich Catlett + */ + +public class SendMailFactory implements ObjectFactory +{ + // The class name for the javamail MimeMessageDataSource + protected static final String DataSourceClassName = + "jakarta.mail.internet.MimePartDataSource"; + + @Override + public Object getObjectInstance(Object refObj, Name name, Context ctx, + Hashtable env) throws Exception { + final Reference ref = (Reference)refObj; + + // Creation of the DataSource is wrapped inside a doPrivileged + // so that javamail can read its default properties without + // throwing Security Exceptions + if (ref.getClassName().equals(DataSourceClassName)) { + return AccessController.doPrivileged( + (PrivilegedAction) () -> { + // set up the smtp session that will send the message + Properties props = new Properties(); + // enumeration of all refaddr + Enumeration list = ref.getAll(); + // current refaddr to be set + RefAddr refaddr; + // set transport to smtp + props.put("mail.transport.protocol", "smtp"); + + while (list.hasMoreElements()) { + refaddr = list.nextElement(); + + // set property + props.put(refaddr.getType(), refaddr.getContent()); + } + MimeMessage message = new MimeMessage( + Session.getInstance(props)); + try { + RefAddr fromAddr = ref.get("mail.from"); + String from = null; + if (fromAddr != null) { + from = (String) fromAddr.getContent(); + } + if (from != null) { + message.setFrom(new InternetAddress(from)); + } + message.setSubject(""); + } catch (Exception e) {/*Ignore*/} + MimePartDataSource mds = new MimePartDataSource(message); + return mds; + }); + } else { // We can't create an instance of the DataSource + return null; + } + } +} diff --git a/java/org/apache/naming/factory/TransactionFactory.java b/java/org/apache/naming/factory/TransactionFactory.java new file mode 100644 index 0000000..e7aefb0 --- /dev/null +++ b/java/org/apache/naming/factory/TransactionFactory.java @@ -0,0 +1,47 @@ +/* + * 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.naming.factory; + +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.naming.TransactionRef; + +/** + * Object factory for User transactions. + * + * @author Remy Maucherat + */ +public class TransactionFactory extends FactoryBase { + + @Override + protected boolean isReferenceTypeSupported(Object obj) { + return obj instanceof TransactionRef; + } + + @Override + protected ObjectFactory getDefaultFactory(Reference ref) { + // No default factory supported. + return null; + } + + @Override + protected Object getLinked(Reference ref) { + // Not supported + return null; + } +} diff --git a/java/org/apache/naming/factory/package.html b/java/org/apache/naming/factory/package.html new file mode 100644 index 0000000..769d5e6 --- /dev/null +++ b/java/org/apache/naming/factory/package.html @@ -0,0 +1,21 @@ + + + +

    This package contains object factories used by the naming service.

    + + diff --git a/java/org/apache/naming/factory/webservices/LocalStrings.properties b/java/org/apache/naming/factory/webservices/LocalStrings.properties new file mode 100644 index 0000000..0c5d17e --- /dev/null +++ b/java/org/apache/naming/factory/webservices/LocalStrings.properties @@ -0,0 +1,16 @@ +# 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. + +serviceProxy.portNotFound=Port-component-ref [{0}] not found diff --git a/java/org/apache/naming/factory/webservices/LocalStrings_de.properties b/java/org/apache/naming/factory/webservices/LocalStrings_de.properties new file mode 100644 index 0000000..a1f6ad3 --- /dev/null +++ b/java/org/apache/naming/factory/webservices/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +serviceProxy.portNotFound=Kein Port mit dem Namen [{0}] gefunden diff --git a/java/org/apache/naming/factory/webservices/LocalStrings_fr.properties b/java/org/apache/naming/factory/webservices/LocalStrings_fr.properties new file mode 100644 index 0000000..94053ed --- /dev/null +++ b/java/org/apache/naming/factory/webservices/LocalStrings_fr.properties @@ -0,0 +1,16 @@ +# 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. + +serviceProxy.portNotFound=Le port-component-ref [{0}] n''a pas été trouvé diff --git a/java/org/apache/naming/factory/webservices/LocalStrings_ja.properties b/java/org/apache/naming/factory/webservices/LocalStrings_ja.properties new file mode 100644 index 0000000..74c563d --- /dev/null +++ b/java/org/apache/naming/factory/webservices/LocalStrings_ja.properties @@ -0,0 +1,16 @@ +# 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. + +serviceProxy.portNotFound=port-component-ref [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ diff --git a/java/org/apache/naming/factory/webservices/LocalStrings_ko.properties b/java/org/apache/naming/factory/webservices/LocalStrings_ko.properties new file mode 100644 index 0000000..cbb7afc --- /dev/null +++ b/java/org/apache/naming/factory/webservices/LocalStrings_ko.properties @@ -0,0 +1,16 @@ +# 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. + +serviceProxy.portNotFound=Port-component-ref [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. diff --git a/java/org/apache/naming/factory/webservices/LocalStrings_zh_CN.properties b/java/org/apache/naming/factory/webservices/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..08d3248 --- /dev/null +++ b/java/org/apache/naming/factory/webservices/LocalStrings_zh_CN.properties @@ -0,0 +1,16 @@ +# 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. + +serviceProxy.portNotFound=找ä¸åˆ°ç«¯å£ç»„件ref[{0}] diff --git a/java/org/apache/naming/factory/webservices/ServiceProxy.java b/java/org/apache/naming/factory/webservices/ServiceProxy.java new file mode 100644 index 0000000..d6c03b1 --- /dev/null +++ b/java/org/apache/naming/factory/webservices/ServiceProxy.java @@ -0,0 +1,148 @@ +/* + * 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.naming.factory.webservices; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.rmi.Remote; +import java.util.Hashtable; +import java.util.Iterator; + +import javax.xml.namespace.QName; +import javax.xml.rpc.Service; +import javax.xml.rpc.ServiceException; + +import org.apache.naming.StringManager; + +/** + * Object proxy for Web Services. + * + * @author Fabien Carrion + */ +public class ServiceProxy implements InvocationHandler { + + private static final StringManager sm = StringManager.getManager(ServiceProxy.class); + + /** + * Service object. + * used for delegation + */ + private final Service service; + + /** + * changing behavior to method : Service.getPort(QName, Class) + */ + private static Method portQNameClass = null; + + /** + * changing behavior to method : Service.getPort(Class) + */ + private static Method portClass = null; + + /** + * PortComponentRef list + */ + private Hashtable portComponentRef = null; + + /** + * Constructs a new ServiceProxy wrapping given Service instance. + * @param service the wrapped Service instance + * @throws ServiceException should be never thrown + */ + public ServiceProxy(Service service) throws ServiceException { + this.service = service; + try { + portQNameClass = Service.class.getDeclaredMethod("getPort", new Class[]{QName.class, Class.class}); + portClass = Service.class.getDeclaredMethod("getPort", new Class[]{Class.class}); + } catch (Exception e) { + throw new ServiceException(e); + } + } + + /** + * @see InvocationHandler#invoke(Object, Method, Object[]) + */ + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + + if (portQNameClass.equals(method)) { + return getProxyPortQNameClass(args); + } + + if (portClass.equals(method)) { + return getProxyPortClass(args); + } + + try { + return method.invoke(service, args); + } catch (InvocationTargetException ite) { + throw ite.getTargetException(); + } + } + + /** + * @param args Method call arguments + * @return Returns the correct Port + * @throws ServiceException if port's QName is an unknown Port (not defined in WSDL). + */ + private Object getProxyPortQNameClass(Object[] args) throws ServiceException { + QName name = (QName) args[0]; + String nameString = name.getLocalPart(); + Class serviceendpointClass = (Class) args[1]; + + for (@SuppressWarnings("unchecked") Iterator ports = service.getPorts(); ports.hasNext();) { + QName portName = ports.next(); + String portnameString = portName.getLocalPart(); + if (portnameString.equals(nameString)) { + return service.getPort(name, serviceendpointClass); + } + } + + // no ports have been found + throw new ServiceException(sm.getString("serviceProxy.portNotFound", name)); + } + + /** + * @param portComponentRef List + */ + public void setPortComponentRef(Hashtable portComponentRef) { + this.portComponentRef = portComponentRef; + } + + /** + * @param args Method call arguments + * @return Returns the correct Port + * @throws ServiceException if port's QName is an unknown Port + */ + private Remote getProxyPortClass(Object[] args) throws ServiceException { + Class serviceendpointClass = (Class) args[0]; + + if (this.portComponentRef == null) { + return service.getPort(serviceendpointClass); + } + + QName portname = this.portComponentRef.get(serviceendpointClass.getName()); + if (portname != null) { + return service.getPort(portname, serviceendpointClass); + } else { + return service.getPort(serviceendpointClass); + } + } + +} diff --git a/java/org/apache/naming/factory/webservices/ServiceRefFactory.java b/java/org/apache/naming/factory/webservices/ServiceRefFactory.java new file mode 100644 index 0000000..d58933c --- /dev/null +++ b/java/org/apache/naming/factory/webservices/ServiceRefFactory.java @@ -0,0 +1,353 @@ +/* + * 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.naming.factory.webservices; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.spi.ObjectFactory; +import javax.wsdl.Definition; +import javax.wsdl.Port; +import javax.wsdl.extensions.ExtensibilityElement; +import javax.wsdl.extensions.soap.SOAPAddress; +import javax.wsdl.factory.WSDLFactory; +import javax.wsdl.xml.WSDLReader; +import javax.xml.namespace.QName; +import javax.xml.rpc.Service; +import javax.xml.rpc.ServiceFactory; +import javax.xml.rpc.handler.Handler; +import javax.xml.rpc.handler.HandlerChain; +import javax.xml.rpc.handler.HandlerInfo; +import javax.xml.rpc.handler.HandlerRegistry; + +import org.apache.naming.HandlerRef; +import org.apache.naming.ServiceRef; + +/** + * Object factory for Web Services. + * + * @author Fabien Carrion + */ +public class ServiceRefFactory implements ObjectFactory { + + /** + * Create a new serviceref instance. + * + * @param obj The reference object describing the webservice + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) + throws Exception { + + if (obj instanceof ServiceRef) { + ServiceRef ref = (ServiceRef) obj; + + // ClassLoader + ClassLoader tcl = Thread.currentThread().getContextClassLoader(); + if (tcl == null) { + tcl = this.getClass().getClassLoader(); + } + ServiceFactory factory = ServiceFactory.newInstance(); + javax.xml.rpc.Service service = null; + + // Service Interface + RefAddr tmp = ref.get(ServiceRef.SERVICE_INTERFACE); + String serviceInterface = null; + if (tmp != null) { + serviceInterface = (String) tmp.getContent(); + } + + // WSDL + tmp = ref.get(ServiceRef.WSDL); + String wsdlRefAddr = null; + if (tmp != null) { + wsdlRefAddr = (String) tmp.getContent(); + } + + // PortComponent + Hashtable portComponentRef = new Hashtable<>(); + + // Create QName object + QName serviceQname = null; + tmp = ref.get(ServiceRef.SERVICE_LOCAL_PART); + if (tmp != null) { + String serviceLocalPart = (String) tmp.getContent(); + tmp = ref.get(ServiceRef.SERVICE_NAMESPACE); + if (tmp == null) { + serviceQname = new QName(serviceLocalPart); + } else { + String serviceNamespace = (String) tmp.getContent(); + serviceQname = new QName(serviceNamespace, serviceLocalPart); + } + } + Class serviceInterfaceClass = null; + + // Create service object + if (serviceInterface == null) { + if (serviceQname == null) { + throw new NamingException("Could not create service-ref instance"); + } + try { + if (wsdlRefAddr == null) { + service = factory.createService( serviceQname ); + } else { + service = factory.createService(new URI(wsdlRefAddr).toURL(), serviceQname); + } + } catch (Exception e) { + NamingException ex = new NamingException("Could not create service"); + ex.initCause(e); + throw ex; + } + } else { + // Loading service Interface + try { + serviceInterfaceClass = tcl.loadClass(serviceInterface); + } catch(ClassNotFoundException e) { + NamingException ex = new NamingException("Could not load service Interface"); + ex.initCause(e); + throw ex; + } + if (serviceInterfaceClass == null) { + throw new NamingException("Could not load service Interface"); + } + try { + if (wsdlRefAddr == null) { + if (!Service.class.isAssignableFrom(serviceInterfaceClass)) { + throw new NamingException("service Interface should extend javax.xml.rpc.Service"); + } + service = factory.loadService(serviceInterfaceClass); + } else { + service = factory.loadService( + new URI(wsdlRefAddr).toURL(), serviceInterfaceClass, new Properties()); + } + } catch (Exception e) { + NamingException ex = new NamingException("Could not create service"); + ex.initCause(e); + throw ex; + } + } + if (service == null) { + throw new NamingException("Cannot create service object"); + } + serviceQname = service.getServiceName(); + serviceInterfaceClass = service.getClass(); + if (wsdlRefAddr != null) { + try { + WSDLFactory wsdlfactory = WSDLFactory.newInstance(); + WSDLReader reader = wsdlfactory.newWSDLReader(); + reader.setFeature("javax.wsdl.importDocuments", true); + Definition def = reader.readWSDL(new URI(wsdlRefAddr).toURL().toExternalForm()); + + javax.wsdl.Service wsdlservice = def.getService(serviceQname); + @SuppressWarnings("unchecked") // Can't change the API + Map ports = wsdlservice.getPorts(); + Method m = serviceInterfaceClass.getMethod("setEndpointAddress", + new Class[] { java.lang.String.class, java.lang.String.class }); + for (String portName : ports.keySet()) { + Port port = wsdlservice.getPort(portName); + String endpoint = getSOAPLocation(port); + m.invoke(service, new Object[]{port.getName(), endpoint}); + portComponentRef.put(endpoint, new QName(port.getName())); + } + } catch (Exception e) { + if (e instanceof InvocationTargetException) { + Throwable cause = e.getCause(); + if (cause instanceof ThreadDeath) { + throw (ThreadDeath) cause; + } + if (cause instanceof VirtualMachineError) { + throw (VirtualMachineError) cause; + } + } + NamingException ex = new NamingException("Error while reading Wsdl File"); + ex.initCause(e); + throw ex; + } + } + + ServiceProxy proxy = new ServiceProxy(service); + + // Use port-component-ref + for (int i = 0; i < ref.size(); i++) { + if (ServiceRef.SERVICEENDPOINTINTERFACE.equals(ref.get(i).getType())) { + String serviceendpoint = ""; + String portlink = ""; + serviceendpoint = (String) ref.get(i).getContent(); + if (ServiceRef.PORTCOMPONENTLINK.equals(ref.get(i + 1).getType())) { + i++; + portlink = (String) ref.get(i).getContent(); + } + portComponentRef.put(serviceendpoint, new QName(portlink)); + + } + } + proxy.setPortComponentRef(portComponentRef); + + // Instantiate service with proxy class + Class[] serviceInterfaces = serviceInterfaceClass.getInterfaces(); + + Class[] interfaces = Arrays.copyOf(serviceInterfaces, serviceInterfaces.length + 1); + interfaces[interfaces.length - 1] = javax.xml.rpc.Service.class; + + Object proxyInstance = null; + try { + proxyInstance = Proxy.newProxyInstance(tcl, interfaces, proxy); + } catch (IllegalArgumentException e) { + proxyInstance = Proxy.newProxyInstance(tcl, serviceInterfaces, proxy); + } + + // Use handler + if (ref.getHandlersSize() > 0) { + + HandlerRegistry handlerRegistry = service.getHandlerRegistry(); + List soaproles = new ArrayList<>(); + + while (ref.getHandlersSize() > 0) { + HandlerRef handlerRef = ref.getHandler(); + HandlerInfo handlerInfo = new HandlerInfo(); + + // Loading handler Class + tmp = handlerRef.get(HandlerRef.HANDLER_CLASS); + if ((tmp == null) || (tmp.getContent() == null)) { + break; + } + Class handlerClass = null; + try { + handlerClass = tcl.loadClass((String) tmp.getContent()); + } catch(ClassNotFoundException e) { + break; + } + + // Load all data relative to the handler : SOAPHeaders, config init element, + // portNames to be set on + List headers = new ArrayList<>(); + Hashtable config = new Hashtable<>(); + List portNames = new ArrayList<>(); + for (int i = 0; i < handlerRef.size(); i++) { + if (HandlerRef.HANDLER_LOCALPART.equals(handlerRef.get(i).getType())) { + String localpart = ""; + String namespace = ""; + localpart = (String) handlerRef.get(i).getContent(); + if (HandlerRef.HANDLER_NAMESPACE.equals(handlerRef.get(i + 1).getType())) { + i++; + namespace = (String) handlerRef.get(i).getContent(); + } + QName header = new QName(namespace, localpart); + headers.add(header); + } else if (HandlerRef.HANDLER_PARAMNAME.equals(handlerRef.get(i).getType())) { + String paramName = ""; + String paramValue = ""; + paramName = (String) handlerRef.get(i).getContent(); + if (HandlerRef.HANDLER_PARAMVALUE.equals(handlerRef.get(i + 1).getType())) { + i++; + paramValue = (String) handlerRef.get(i).getContent(); + } + config.put(paramName, paramValue); + } else if (HandlerRef.HANDLER_SOAPROLE.equals(handlerRef.get(i).getType())) { + String soaprole = ""; + soaprole = (String) handlerRef.get(i).getContent(); + soaproles.add(soaprole); + } else if (HandlerRef.HANDLER_PORTNAME.equals(handlerRef.get(i).getType())) { + String portName = ""; + portName = (String) handlerRef.get(i).getContent(); + portNames.add(portName); + } + } + + // Set the handlers information + handlerInfo.setHandlerClass(handlerClass); + handlerInfo.setHeaders(headers.toArray(new QName[0])); + handlerInfo.setHandlerConfig(config); + + if (!portNames.isEmpty()) { + for (String portName : portNames) { + initHandlerChain(new QName(portName), handlerRegistry, + handlerInfo, soaproles); + } + } else { + Enumeration e = portComponentRef.elements(); + while(e.hasMoreElements()) { + initHandlerChain(e.nextElement(), handlerRegistry, handlerInfo, soaproles); + } + } + } + } + + return proxyInstance; + + } + + return null; + + } + + /** + * @param port analyzed port + * @return Returns the endpoint URL of the given Port + */ + private String getSOAPLocation(Port port) { + String endpoint = null; + @SuppressWarnings("unchecked") // Can't change the API + List extensions = port.getExtensibilityElements(); + for (ExtensibilityElement ext : extensions) { + if (ext instanceof SOAPAddress) { + SOAPAddress addr = (SOAPAddress) ext; + endpoint = addr.getLocationURI(); + } + } + return endpoint; + } + + + private void initHandlerChain(QName portName, HandlerRegistry handlerRegistry, + HandlerInfo handlerInfo, List soaprolesToAdd) { + HandlerChain handlerChain = (HandlerChain) handlerRegistry.getHandlerChain(portName); + @SuppressWarnings("unchecked") // Can't change the API + Iterator iter = handlerChain.iterator(); + while (iter.hasNext()) { + Handler handler = iter.next(); + handler.init(handlerInfo); + } + String[] soaprolesRegistered = handlerChain.getRoles(); + String [] soaproles = new String[soaprolesRegistered.length + soaprolesToAdd.size()]; + int i; + for (i = 0;i < soaprolesRegistered.length; i++) { + soaproles[i] = soaprolesRegistered[i]; + } + for (int j = 0; j < soaprolesToAdd.size(); j++) { + soaproles[i+j] = soaprolesToAdd.get(j); + } + handlerChain.setRoles(soaproles); + handlerRegistry.setHandlerChain(portName, handlerChain); + } + + +} diff --git a/java/org/apache/naming/java/javaURLContextFactory.java b/java/org/apache/naming/java/javaURLContextFactory.java new file mode 100644 index 0000000..0eeacd8 --- /dev/null +++ b/java/org/apache/naming/java/javaURLContextFactory.java @@ -0,0 +1,120 @@ +/* + * 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.naming.java; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; +import javax.naming.spi.ObjectFactory; + +import org.apache.naming.ContextBindings; +import org.apache.naming.NamingContext; +import org.apache.naming.SelectorContext; + +/** + * Context factory for the "java:" namespace. + *

    + * Important note : This factory MUST be associated with the "java" URL + * prefix, which can be done by either : + *

      + *
    • Adding a + * java.naming.factory.url.pkgs=org.apache.naming property + * to the JNDI properties file
    • + *
    • Setting an environment variable named Context.URL_PKG_PREFIXES with + * its value including the org.apache.naming package name. + * More detail about this can be found in the JNDI documentation : + * {@link javax.naming.spi.NamingManager#getURLContext(String, java.util.Hashtable)}.
    • + *
    + * + * @author Remy Maucherat + */ +public class javaURLContextFactory + implements ObjectFactory, InitialContextFactory { + + + // ----------------------------------------------------------- Constructors + + + // -------------------------------------------------------------- Constants + + + public static final String MAIN = "initialContext"; + + + // ----------------------------------------------------- Instance Variables + + + /** + * Initial context. + */ + protected static volatile Context initialContext = null; + + + // --------------------------------------------------------- Public Methods + + + // -------------------------------------------------- ObjectFactory Methods + + + /** + * Crete a new Context's instance. + */ + @SuppressWarnings("unchecked") + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) + throws NamingException { + if ((ContextBindings.isThreadBound()) || + (ContextBindings.isClassLoaderBound())) { + return new SelectorContext((Hashtable)environment); + } + return null; + } + + + /** + * Get a new (writable) initial context. + */ + @SuppressWarnings("unchecked") + @Override + public Context getInitialContext(Hashtable environment) + throws NamingException { + if (ContextBindings.isThreadBound() || + (ContextBindings.isClassLoaderBound())) { + // Redirect the request to the bound initial context + return new SelectorContext( + (Hashtable)environment, true); + } + + // If the thread is not bound, return a shared writable context + if (initialContext == null) { + synchronized(javaURLContextFactory.class) { + if (initialContext == null) { + initialContext = new NamingContext( + (Hashtable)environment, MAIN); + } + } + } + return initialContext; + } + + +} + diff --git a/java/org/apache/naming/java/package.html b/java/org/apache/naming/java/package.html new file mode 100644 index 0000000..2f5fe9e --- /dev/null +++ b/java/org/apache/naming/java/package.html @@ -0,0 +1,21 @@ + + + +

    This package contains the URL context factory for the "java" namespace.

    + + diff --git a/java/org/apache/naming/package.html b/java/org/apache/naming/package.html new file mode 100644 index 0000000..c16c6d3 --- /dev/null +++ b/java/org/apache/naming/package.html @@ -0,0 +1,21 @@ + + + +

    This package contains a memory based naming service provider.

    + + diff --git a/java/org/apache/tomcat/ContextBind.java b/java/org/apache/tomcat/ContextBind.java new file mode 100644 index 0000000..ab9a1d7 --- /dev/null +++ b/java/org/apache/tomcat/ContextBind.java @@ -0,0 +1,60 @@ +/* + * 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.tomcat; + +public interface ContextBind { + + /** + * Change the current thread context class loader to the web application + * class loader. If no web application class loader is defined, or if the + * current thread is already using the web application class loader then no + * change will be made. If the class loader is changed and a + * {@link org.apache.catalina.ThreadBindingListener} is configured then + * {@link org.apache.catalina.ThreadBindingListener#bind()} will be called + * after the change has been made. + * + * @param usePrivilegedAction + * Should a {@link java.security.PrivilegedAction} be used when + * obtaining the current thread context class loader and setting + * the new one? + * @param originalClassLoader + * The current class loader if known to save this method having to + * look it up + * + * @return If the class loader has been changed by the method it will return + * the thread context class loader in use when the method was + * called. If no change was made then this method returns null. + */ + ClassLoader bind(boolean usePrivilegedAction, ClassLoader originalClassLoader); + + /** + * Restore the current thread context class loader to the original class + * loader in used before {@link #bind(boolean, ClassLoader)} was called. If + * no original class loader is passed to this method then no change will be + * made. If the class loader is changed and a + * {@link org.apache.catalina.ThreadBindingListener} is configured then + * {@link org.apache.catalina.ThreadBindingListener#unbind()} will be called + * before the change is made. + * + * @param usePrivilegedAction + * Should a {@link java.security.PrivilegedAction} be used when + * setting the current thread context class loader? + * @param originalClassLoader + * The class loader to restore as the thread context class loader + */ + void unbind(boolean usePrivilegedAction, ClassLoader originalClassLoader); +} diff --git a/java/org/apache/tomcat/InstanceManager.java b/java/org/apache/tomcat/InstanceManager.java new file mode 100644 index 0000000..978eb8d --- /dev/null +++ b/java/org/apache/tomcat/InstanceManager.java @@ -0,0 +1,51 @@ +/* + * 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.tomcat; + +import java.lang.reflect.InvocationTargetException; + +import javax.naming.NamingException; + +public interface InstanceManager { + + Object newInstance(Class clazz) throws IllegalAccessException, InvocationTargetException, + NamingException, InstantiationException, IllegalArgumentException, + NoSuchMethodException, SecurityException; + + Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, + NamingException, InstantiationException, ClassNotFoundException, + IllegalArgumentException, NoSuchMethodException, SecurityException; + + Object newInstance(String fqcn, ClassLoader classLoader) throws IllegalAccessException, + InvocationTargetException, NamingException, InstantiationException, + ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, + SecurityException; + + void newInstance(Object o) + throws IllegalAccessException, InvocationTargetException, NamingException; + + void destroyInstance(Object o) throws IllegalAccessException, InvocationTargetException; + + /** + * Called by the component using the InstanceManager periodically to perform + * any regular maintenance that might be required. By default, this method + * is a NO-OP. + */ + default void backgroundProcess() { + // NO-OP by default + } +} diff --git a/java/org/apache/tomcat/InstanceManagerBindings.java b/java/org/apache/tomcat/InstanceManagerBindings.java new file mode 100644 index 0000000..7b22eb3 --- /dev/null +++ b/java/org/apache/tomcat/InstanceManagerBindings.java @@ -0,0 +1,35 @@ +/* + * 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.tomcat; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class InstanceManagerBindings { + + private static final Map bindings = new ConcurrentHashMap<>(); + + public static void bind(ClassLoader classLoader, InstanceManager instanceManager) { + bindings.put(classLoader, instanceManager); + } + public static void unbind(ClassLoader classLoader) { + bindings.remove(classLoader); + } + public static InstanceManager get(ClassLoader classLoader) { + return bindings.get(classLoader); + } +} diff --git a/java/org/apache/tomcat/InstrumentableClassLoader.java b/java/org/apache/tomcat/InstrumentableClassLoader.java new file mode 100644 index 0000000..d2e18dc --- /dev/null +++ b/java/org/apache/tomcat/InstrumentableClassLoader.java @@ -0,0 +1,78 @@ +/* + * 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.tomcat; + +import java.lang.instrument.ClassFileTransformer; + +/** + * Specifies a class loader capable of being decorated with + * {@link ClassFileTransformer}s. These transformers can instrument + * (or weave) the byte code of classes loaded through this class loader + * to alter their behavior. Currently only + * {@link org.apache.catalina.loader.WebappClassLoaderBase} implements this + * interface. This allows web application frameworks or JPA providers + * bundled with a web application to instrument web application classes + * as necessary. + *

    + * You should always program against the methods of this interface + * (whether using reflection or otherwise). The methods in + * {@code WebappClassLoaderBase} are protected by the default security + * manager if one is in use. + * + * @since 8.0, 7.0.64 + */ +public interface InstrumentableClassLoader { + + /** + * Adds the specified class file transformer to this class loader. The + * transformer will then be able to instrument the bytecode of any + * classes loaded by this class loader after the invocation of this + * method. + * + * @param transformer The transformer to add to the class loader + * @throws IllegalArgumentException if the {@literal transformer} is null. + */ + void addTransformer(ClassFileTransformer transformer); + + /** + * Removes the specified class file transformer from this class loader. + * It will no longer be able to instrument the byte code of any classes + * loaded by the class loader after the invocation of this method. + * However, any classes already instrumented by this transformer before + * this method call will remain in their instrumented state. + * + * @param transformer The transformer to remove + */ + void removeTransformer(ClassFileTransformer transformer); + + /** + * Returns a copy of this class loader without any class file + * transformers. This is a tool often used by Java Persistence API + * providers to inspect entity classes in the absence of any + * instrumentation, something that can't be guaranteed within the + * context of a {@link ClassFileTransformer}'s + * {@link ClassFileTransformer#transform(ClassLoader, String, Class, + * java.security.ProtectionDomain, byte[]) transform} method. + *

    + * The returned class loader's resource cache will have been cleared + * so that classes already instrumented will not be retained or + * returned. + * + * @return the transformer-free copy of this class loader. + */ + ClassLoader copyWithoutTransformers(); +} diff --git a/java/org/apache/tomcat/Jar.java b/java/org/apache/tomcat/Jar.java new file mode 100644 index 0000000..0e2e61b --- /dev/null +++ b/java/org/apache/tomcat/Jar.java @@ -0,0 +1,136 @@ +/* + * 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.tomcat; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.jar.Manifest; + +/** + * Provides an abstraction for use by the various classes that need to scan + * JARs. The classes provided by the JRE for accessing JARs + * ({@link java.util.jar.JarFile} and {@link java.util.jar.JarInputStream}) have + * significantly different performance characteristics depending on the form of + * the URL used to access the JAR. For file based JAR {@link java.net.URL}s, + * {@link java.util.jar.JarFile} is faster but for non-file based + * {@link java.net.URL}s, {@link java.util.jar.JarFile} creates a copy of the + * JAR in the temporary directory so {@link java.util.jar.JarInputStream} is + * faster. + */ +public interface Jar extends AutoCloseable { + + /** + * @return The URL for accessing the JAR file. + */ + URL getJarFileURL(); + + /** + * Obtain an {@link InputStream} for a given entry in a JAR. The caller is + * responsible for closing the stream. + * + * @param name Entry to obtain an {@link InputStream} for + * @return An {@link InputStream} for the specified entry or null if + * the entry does not exist + * + * @throws IOException if an I/O error occurs while processing the JAR file + */ + InputStream getInputStream(String name) throws IOException; + + /** + * Obtain the last modified time for the given resource in the JAR. + * + * @param name Entry to obtain the modification time for + * + * @return The time (in the same format as + * {@link System#currentTimeMillis()} that the resource was last + * modified. Returns -1 if the entry does not exist + * + * @throws IOException if an I/O error occurs while processing the JAR file + */ + long getLastModified(String name) throws IOException; + + /** + * Determine if the given resource in present in the JAR. + * + * @param name Entry to look for + * + * @return {@code true} if the entry is present in the JAR, otherwise + * {@code false} + * + * @throws IOException if an I/O error occurs while processing the JAR file + */ + boolean exists(String name) throws IOException; + + /** + * Close any resources associated with this JAR. + */ + @Override + void close(); + + /** + * Moves the internal pointer to the next entry in the JAR. + */ + void nextEntry(); + + /** + * Obtains the name of the current entry. + * + * @return The entry name + */ + String getEntryName(); + + /** + * Obtains the input stream for the current entry. + * + * @return The input stream + * @throws IOException If the stream cannot be obtained + */ + InputStream getEntryInputStream() throws IOException; + + /** + * Obtain, in String form, the URL for an entry in this JAR. Note that for + * JARs nested in WAR files, the Tomcat specific war:file:... form will not + * be used, rather the jar:jar:file:... form (that the JRE does not + * understand will be used). Note that this means that any code using these + * URLs will need to understand the jar:jar:file:... form and use the + * {@link org.apache.tomcat.util.scan.JarFactory} to ensure resources are + * accessed correctly. + * + * @param entry The entry to generate the URL for + * + * @return a URL for the specified entry in the JAR + */ + String getURL(String entry); + + /** + * Obtain the manifest for the JAR file. + * + * @return The manifest for this JAR file. + * + * @throws IOException If an I/O error occurs trying to obtain the manifest + */ + Manifest getManifest() throws IOException; + + /** + * Resets the internal pointer used to track JAR entries to the beginning of + * the JAR. + * + * @throws IOException If the pointer cannot be reset + */ + void reset() throws IOException; +} diff --git a/java/org/apache/tomcat/JarScanFilter.java b/java/org/apache/tomcat/JarScanFilter.java new file mode 100644 index 0000000..f616fe4 --- /dev/null +++ b/java/org/apache/tomcat/JarScanFilter.java @@ -0,0 +1,38 @@ +/* + * 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.tomcat; + +public interface JarScanFilter { + + /** + * @param jarScanType The type of JAR scan currently being performed + * @param jarName The name of the JAR file (without any path + * information) to be checked to see if it should + * be included in the results or not + * @return true if the JAR should be returned in the results, + * false if it should be excluded + */ + boolean check(JarScanType jarScanType, String jarName); + + /** + * @return true if all of the scans should be skipped which + * can improve startup performance. The default is false. + */ + default boolean isSkipAll() { + return false; + } +} diff --git a/java/org/apache/tomcat/JarScanType.java b/java/org/apache/tomcat/JarScanType.java new file mode 100644 index 0000000..e9d24f5 --- /dev/null +++ b/java/org/apache/tomcat/JarScanType.java @@ -0,0 +1,23 @@ +/* + * 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.tomcat; + +public enum JarScanType { + TLD, + PLUGGABILITY, + OTHER +} diff --git a/java/org/apache/tomcat/JarScanner.java b/java/org/apache/tomcat/JarScanner.java new file mode 100644 index 0000000..f92e78f --- /dev/null +++ b/java/org/apache/tomcat/JarScanner.java @@ -0,0 +1,45 @@ +/* + * 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.tomcat; + +import jakarta.servlet.ServletContext; + +/** + * Scans a web application and classloader hierarchy for JAR files. Uses + * include TLD scanning and web-fragment.xml scanning. Uses a call-back + * mechanism so the caller can process each JAR found. + */ +public interface JarScanner { + + /** + * Scan the provided ServletContext and classloader for JAR files. Each JAR + * file found will be passed to the callback handler to be processed. + * + * @param scanType The type of JAR scan to perform. This is passed to + * the filter which uses it to determine how to + * filter the results + * @param context The ServletContext - used to locate and access + * WEB-INF/lib + * @param callback The handler to process any JARs found + */ + void scan(JarScanType scanType, ServletContext context, + JarScannerCallback callback); + + JarScanFilter getJarScanFilter(); + + void setJarScanFilter(JarScanFilter jarScanFilter); +} diff --git a/java/org/apache/tomcat/JarScannerCallback.java b/java/org/apache/tomcat/JarScannerCallback.java new file mode 100644 index 0000000..92669cf --- /dev/null +++ b/java/org/apache/tomcat/JarScannerCallback.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat; + +import java.io.File; +import java.io.IOException; + +/** + * This interface is implemented by clients of the {@link JarScanner} to enable + * them to receive notification of a discovered JAR. + */ +public interface JarScannerCallback { + + /** + * A JAR was found and may be accessed for further processing via the + * provided URL connection. The caller is responsible for closing the JAR. + * + * @param jar The JAR to process + * @param webappPath The path, if any, to the JAR within the web application + * @param isWebapp Indicates if the JAR was found within a web + * application. If false the JAR should + * be treated as being provided by the container + * + * @throws IOException if an I/O error occurs while scanning the JAR + */ + void scan(Jar jar, String webappPath, boolean isWebapp) + throws IOException; + + /** + * A directory was found that is to be treated as an unpacked JAR. The + * directory may be accessed for further processing via the provided file. + * + * @param file The directory containing the unpacked JAR. + * @param webappPath The path, if any, to the file within the web + * application + * @param isWebapp Indicates if the JAR was found within a web + * application. If false the JAR should + * be treated as being provided by the container + * + * @throws IOException if an I/O error occurs while scanning the JAR + */ + void scan(File file, String webappPath, boolean isWebapp) throws IOException; + + /** + * A directory structure was found within the web application at + * /WEB-INF/classes that should be handled as an unpacked JAR. Note that all + * resource access must be via the ServletContext to ensure that any + * additional resources are visible. + * + * @throws IOException if an I/O error occurs while scanning WEB-INF/classes + */ + void scanWebInfClasses() throws IOException; +} diff --git a/java/org/apache/tomcat/PeriodicEventListener.java b/java/org/apache/tomcat/PeriodicEventListener.java new file mode 100644 index 0000000..8a1204a --- /dev/null +++ b/java/org/apache/tomcat/PeriodicEventListener.java @@ -0,0 +1,24 @@ +/* + * 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.tomcat; + +public interface PeriodicEventListener { + /** + * Execute a periodic task, such as reloading, etc. + */ + void periodicEvent(); +} diff --git a/java/org/apache/tomcat/SimpleInstanceManager.java b/java/org/apache/tomcat/SimpleInstanceManager.java new file mode 100644 index 0000000..035a1e0 --- /dev/null +++ b/java/org/apache/tomcat/SimpleInstanceManager.java @@ -0,0 +1,68 @@ +/* + * 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.tomcat; + +import java.lang.reflect.InvocationTargetException; + +import javax.naming.NamingException; + +/** + * SimpleInstanceManager + * + * Implement the org.apache.tomcat.InstanceManager interface. + */ +public class SimpleInstanceManager implements InstanceManager { + + public SimpleInstanceManager() { + } + + @Override + public Object newInstance(Class clazz) throws IllegalAccessException, + InvocationTargetException, NamingException, InstantiationException, NoSuchMethodException { + return prepareInstance(clazz.getConstructor().newInstance()); + } + + @Override + public Object newInstance(String className) throws IllegalAccessException, + InvocationTargetException, NamingException, InstantiationException, + ClassNotFoundException, NoSuchMethodException { + Class clazz = Thread.currentThread().getContextClassLoader().loadClass(className); + return prepareInstance(clazz.getConstructor().newInstance()); + } + + @Override + public Object newInstance(String fqcn, ClassLoader classLoader) throws IllegalAccessException, + InvocationTargetException, NamingException, InstantiationException, + ClassNotFoundException, NoSuchMethodException { + Class clazz = classLoader.loadClass(fqcn); + return prepareInstance(clazz.getConstructor().newInstance()); + } + + @Override + public void newInstance(Object o) throws IllegalAccessException, InvocationTargetException, + NamingException { + // NO-OP + } + + @Override + public void destroyInstance(Object o) throws IllegalAccessException, InvocationTargetException { + } + + private Object prepareInstance(Object o) { + return o; + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/buildutil/CheckEol.java b/java/org/apache/tomcat/buildutil/CheckEol.java new file mode 100644 index 0000000..96dd39f --- /dev/null +++ b/java/org/apache/tomcat/buildutil/CheckEol.java @@ -0,0 +1,181 @@ +/* + * 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.tomcat.buildutil; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; + +/** + * Ant task that checks that all the files in the given fileset have end-of-line + * delimiters that are appropriate. + * + *

    + * The goal is to check whether we have problems with Subversion's svn:eol-style + * property or Git's autocrlf setting when files are committed on one OS and then + * checked on another one. + */ +public class CheckEol extends Task { + + /** The files to be checked */ + private final List filesets = new ArrayList<>(); + + /** The line ending mode (either LF, CRLF, or null for OS specific) */ + private Mode mode; + + /** + * Sets the files to be checked + * + * @param fs The fileset to be checked. + */ + public void addFileset( FileSet fs ) { + filesets.add( fs ); + } + + /** + * Sets the line ending mode. + * + * @param mode The line ending mode (either LF or CRLF) + */ + public void setMode( String mode ) { + this.mode = Mode.valueOf( mode.toUpperCase() ); + } + + private Mode getMode() { + if ( mode != null ) { + return mode; + } else { + if ("\n".equals(System.lineSeparator())) { + return Mode.LF; + } else if ("\r\n".equals(System.lineSeparator())) { + return Mode.CRLF; + } + } + + return null; + } + + /** + * Perform the check + * + * @throws BuildException if an error occurs during execution of + * this task. + */ + @Override + public void execute() throws BuildException { + + Mode mode = getMode(); + if ( mode == null ) { + log("Line ends check skipped, because OS line ends setting is neither LF nor CRLF.", Project.MSG_VERBOSE); + return; + } + + int count = 0; + + List errors = new ArrayList<>(); + + // Step through each file and check. + for (FileSet fs : filesets) { + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File basedir = ds.getBasedir(); + String[] files = ds.getIncludedFiles(); + if (files.length > 0) { + log("Checking line ends in " + files.length + " file(s)"); + for (String filename : files) { + File file = new File(basedir, filename); + log("Checking file '" + file + "' for correct line ends", + Project.MSG_DEBUG); + try { + check(file, errors, mode); + } catch (IOException e) { + throw new BuildException("Could not check file '" + + file.getAbsolutePath() + "'", e); + } + count++; + } + } + } + if (count > 0) { + log("Done line ends check in " + count + " file(s), " + + errors.size() + " error(s) found."); + } + if (errors.size() > 0) { + String message = "The following files have wrong line ends: " + + errors; + // We need to explicitly write the message to the log, because + // long BuildException messages may be trimmed. E.g. I observed + // this problem with Eclipse IDE 3.7. + log(message, Project.MSG_ERR); + throw new BuildException(message); + } + } + + private enum Mode { + LF, CRLF + } + + private static class CheckFailure { + private final File file; + private final int line; + private final String value; + + CheckFailure(File file, int line, String value) { + this.file = file; + this.line = line; + this.value = value; + } + + @Override + public String toString() { + return System.lineSeparator() + file + ": uses " + value + " on line " + line; + } + } + + private void check(File file, List errors, Mode mode) throws IOException { + try (FileInputStream fis = new FileInputStream(file); + BufferedInputStream is = new BufferedInputStream(fis)) { + int line = 1; + int prev = -1; + int ch; + while ((ch = is.read()) != -1) { + if (ch == '\n') { + if (mode == Mode.LF && prev == '\r') { + errors.add(new CheckFailure(file, line, "CRLF")); + return; + } else if (mode == Mode.CRLF && prev != '\r') { + errors.add(new CheckFailure(file, line, "LF")); + return; + } + line++; + } else if (prev == '\r') { + errors.add(new CheckFailure(file, line, "CR")); + return; + } + prev = ch; + } + } + } +} diff --git a/java/org/apache/tomcat/buildutil/ForceUtcTimeZone.java b/java/org/apache/tomcat/buildutil/ForceUtcTimeZone.java new file mode 100644 index 0000000..49b4c2f --- /dev/null +++ b/java/org/apache/tomcat/buildutil/ForceUtcTimeZone.java @@ -0,0 +1,30 @@ +/* + * 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.tomcat.buildutil; + +import java.util.TimeZone; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; + +public class ForceUtcTimeZone extends Task { + + @Override + public void execute() throws BuildException { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } +} diff --git a/java/org/apache/tomcat/buildutil/MimeTypeMappings.java b/java/org/apache/tomcat/buildutil/MimeTypeMappings.java new file mode 100644 index 0000000..3f467ad --- /dev/null +++ b/java/org/apache/tomcat/buildutil/MimeTypeMappings.java @@ -0,0 +1,70 @@ +/* + * 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.tomcat.buildutil; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.tomcat.util.descriptor.DigesterFactory; +import org.apache.tomcat.util.descriptor.XmlErrorHandler; +import org.apache.tomcat.util.descriptor.web.WebRuleSet; +import org.apache.tomcat.util.descriptor.web.WebXml; +import org.apache.tomcat.util.digester.Digester; +import org.xml.sax.InputSource; + +public class MimeTypeMappings { + + public static void main(String... args) throws Exception { + InputSource globalWebXml = new InputSource(new File("conf/web.xml").getAbsoluteFile().toURI().toString()); + + WebXml webXmlDefaultFragment = new WebXml(); + webXmlDefaultFragment.setOverridable(true); + webXmlDefaultFragment.setDistributable(true); + webXmlDefaultFragment.setAlwaysAddWelcomeFiles(false); + + Digester digester = DigesterFactory.newDigester(true, true, new WebRuleSet(), true); + XmlErrorHandler handler = new XmlErrorHandler(); + digester.setErrorHandler(handler); + digester.push(webXmlDefaultFragment); + digester.parse(globalWebXml); + + Map webXmlMimeMappings = webXmlDefaultFragment.getMimeMappings(); + SortedMap sortedWebXmlMimeMappings = new TreeMap<>(webXmlMimeMappings); + + File f = new File("java/org/apache/catalina/startup/MimeTypeMappings.properties"); + try (FileOutputStream fos = new FileOutputStream(f); + Writer w = new OutputStreamWriter(fos, StandardCharsets.US_ASCII)) { + + Utils.insertLicense(w); + + w.write(System.lineSeparator()); + + for (Map.Entry mapping : sortedWebXmlMimeMappings.entrySet()) { + w.write(mapping.getKey()); + w.write("="); + w.write(mapping.getValue()); + w.write(System.lineSeparator()); + } + } + } +} diff --git a/java/org/apache/tomcat/buildutil/RepeatableArchive.java b/java/org/apache/tomcat/buildutil/RepeatableArchive.java new file mode 100644 index 0000000..6d12217 --- /dev/null +++ b/java/org/apache/tomcat/buildutil/RepeatableArchive.java @@ -0,0 +1,133 @@ +/* + * 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.tomcat.buildutil; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; + +/** + * Ant task to assist with repeatable builds. + *

    + * While originally written to address an issue with Javadoc output, this task + * takes a generic approach that could be used with any archive. The task takes + * a set of zip (or jar, war etc) files as its input and sets the last modified + * time of every file in the archive to be the same as the last modified time + * of the archive. + */ +public class RepeatableArchive extends Task { + + private final List filesets = new ArrayList<>(); + + private String datetime; + private String pattern; + + /** + * Sets the files to be processed + * + * @param fs The fileset to be processed. + */ + public void addFileset(FileSet fs) { + filesets.add(fs); + } + + + public void setDatetime(String datetime) { + this.datetime = datetime; + } + + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + + @Override + public void execute() throws BuildException { + + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + Date date; + try { + date = sdf.parse(datetime); + } catch (ParseException e) { + throw new BuildException(e); + } + + byte[] buf = new byte[8192]; + FileTime lastModified = FileTime.fromMillis(date.getTime()); + + for (FileSet fs : filesets) { + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File basedir = ds.getBasedir(); + String[] files = ds.getIncludedFiles(); + for (String file : files) { + File archive = new File(basedir, file); + File oldArchive = new File(basedir, file + ".old"); + + try { + Files.move(archive.toPath(), oldArchive.toPath(), StandardCopyOption.ATOMIC_MOVE); + + try (ZipFile oldZipFile = new ZipFile(oldArchive); + ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(archive))) { + + Enumeration oldEntries = oldZipFile.entries(); + while (oldEntries.hasMoreElements()) { + ZipEntry oldEntry = oldEntries.nextElement(); + + ZipEntry entry = new ZipEntry(oldEntry.getName()); + entry.setLastModifiedTime(lastModified); + + zipOut.putNextEntry(entry); + + InputStream is = oldZipFile.getInputStream(oldEntry); + + int numRead; + while ((numRead = is.read(buf)) >= 0) { + zipOut.write(buf, 0, numRead); + } + } + } + + if (!archive.setLastModified(lastModified.toMillis())) { + throw new BuildException("setLastModified failed for [" + archive.getAbsolutePath() + "]"); + } + Files.delete(oldArchive.toPath()); + } catch (IOException ioe) { + throw new BuildException(ioe); + } + } + } + } +} diff --git a/java/org/apache/tomcat/buildutil/Txt2Html.java b/java/org/apache/tomcat/buildutil/Txt2Html.java new file mode 100644 index 0000000..5092f6d --- /dev/null +++ b/java/org/apache/tomcat/buildutil/Txt2Html.java @@ -0,0 +1,176 @@ +/* + * 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.tomcat.buildutil; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.types.FileSet; + +/** + * Ant task to convert a given set of files from Text to HTML. + * Inserts an HTML header including pre tags and replaces special characters + * with their HTML escaped equivalents. + * + *

    This task is currently used by the ant script to build our examples

    + * + * @author Mark Roth + */ +public class Txt2Html + extends Task +{ + + /** The directory to contain the resulting files */ + private File todir; + + /** The file to be converted into HTML */ + private final List filesets = new ArrayList<>(); + + /** + * The encoding of the source files (.java and .jsp). Once they use + * UTF-8, this will need to be updated. + */ + private static final String SOURCE_ENCODING = "ISO-8859-1"; + + /** + * Line terminator to be used for separating lines of the generated + * HTML page, to be independent from "line.separator" system property. + */ + private static final String LINE_SEPARATOR = "\r\n"; + + /** + * Sets the directory to contain the resulting files + * + * @param todir The directory + */ + public void setTodir( File todir ) { + this.todir = todir; + } + + /** + * Sets the files to be converted into HTML + * + * @param fs The fileset to be converted. + */ + public void addFileset( FileSet fs ) { + filesets.add( fs ); + } + + /** + * Perform the conversion + * + * @throws BuildException if an error occurs during execution of + * this task. + */ + @Override + public void execute() + throws BuildException + { + int count = 0; + + // Step through each file and convert. + for (FileSet fs : filesets) { + DirectoryScanner ds = fs.getDirectoryScanner(getProject()); + File basedir = ds.getBasedir(); + String[] files = ds.getIncludedFiles(); + for (String file : files) { + File from = new File(basedir, file); + File to = new File(todir, file + ".html"); + if (!to.exists() || + (from.lastModified() > to.lastModified())) { + log("Converting file '" + from.getAbsolutePath() + + "' to '" + to.getAbsolutePath(), Project.MSG_VERBOSE); + try { + convert(from, to); + } catch (IOException e) { + throw new BuildException("Could not convert '" + + from.getAbsolutePath() + "' to '" + + to.getAbsolutePath() + "'", e); + } + count++; + } + } + if( count > 0 ) { + log( "Converted " + count + " file" + (count > 1 ? "s" : "") + + " to " + todir.getAbsolutePath() ); + } + } + } + + /** + * Perform the actual copy and conversion + * + * @param from The input file + * @param to The output file + * @throws IOException Thrown if an error occurs during the conversion + */ + private void convert( File from, File to ) + throws IOException + { + // Open files: + try (BufferedReader in = new BufferedReader(new InputStreamReader( + new FileInputStream(from), SOURCE_ENCODING))) { + try (PrintWriter out = new PrintWriter(new OutputStreamWriter( + new FileOutputStream(to), "UTF-8"))) { + + // Output header: + out.print("" + + "Source Code
    " );
    +
    +                // Convert, line-by-line:
    +                String line;
    +                while( (line = in.readLine()) != null ) {
    +                    StringBuilder result = new StringBuilder();
    +                    int len = line.length();
    +                    for( int i = 0; i < len; i++ ) {
    +                        char c = line.charAt( i );
    +                        switch( c ) {
    +                            case '&':
    +                                result.append( "&" );
    +                                break;
    +                            case '<':
    +                                result.append( "<" );
    +                                break;
    +                            default:
    +                                result.append( c );
    +                        }
    +                    }
    +                    out.print( result.toString() + LINE_SEPARATOR );
    +                }
    +
    +                // Output footer:
    +                out.print( "
    " ); + + } + } + } + +} + + diff --git a/java/org/apache/tomcat/buildutil/Utils.java b/java/org/apache/tomcat/buildutil/Utils.java new file mode 100644 index 0000000..357a87c --- /dev/null +++ b/java/org/apache/tomcat/buildutil/Utils.java @@ -0,0 +1,59 @@ +/* + * 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.tomcat.buildutil; + +import java.io.IOException; +import java.io.Writer; + +public class Utils { + + private Utils() { + // Utility class. Hide default constructor. + } + + + public static void insertLicense(Writer w) throws IOException { + w.write("# Licensed to the Apache Software Foundation (ASF) under one or more"); + w.write(System.lineSeparator()); + w.write("# contributor license agreements. See the NOTICE file distributed with"); + w.write(System.lineSeparator()); + w.write("# this work for additional information regarding copyright ownership."); + w.write(System.lineSeparator()); + w.write("# The ASF licenses this file to You under the Apache License, Version 2.0"); + w.write(System.lineSeparator()); + w.write("# (the \"License\"); you may not use this file except in compliance with"); + w.write(System.lineSeparator()); + w.write("# the License. You may obtain a copy of the License at"); + w.write(System.lineSeparator()); + w.write("#"); + w.write(System.lineSeparator()); + w.write("# http://www.apache.org/licenses/LICENSE-2.0"); + w.write(System.lineSeparator()); + w.write("#"); + w.write(System.lineSeparator()); + w.write("# Unless required by applicable law or agreed to in writing, software"); + w.write(System.lineSeparator()); + w.write("# distributed under the License is distributed on an \"AS IS\" BASIS,"); + w.write(System.lineSeparator()); + w.write("# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied."); + w.write(System.lineSeparator()); + w.write("# See the License for the specific language governing permissions and"); + w.write(System.lineSeparator()); + w.write("# limitations under the License."); + w.write(System.lineSeparator()); + } +} diff --git a/java/org/apache/tomcat/buildutil/translate/BackportBase.java b/java/org/apache/tomcat/buildutil/translate/BackportBase.java new file mode 100644 index 0000000..1a7ab7e --- /dev/null +++ b/java/org/apache/tomcat/buildutil/translate/BackportBase.java @@ -0,0 +1,65 @@ +/* + * 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.tomcat.buildutil.translate; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Base class providing common implementation for back-port utilities. + */ +public abstract class BackportBase { + + protected final Map sourceTranslations = new HashMap<>(); + protected final Map targetTranslations = new HashMap<>(); + protected final File targetRoot; + protected final Properties sourceEnglish; + protected final Properties targetEnglish; + protected final File storageDir; + + protected BackportBase(String... args) throws IOException { + if (args.length != 1) { + throw new IllegalArgumentException("Missing back-port target"); + } + targetRoot = new File(args[0]); + + if (!targetRoot.isDirectory()) { + throw new IllegalArgumentException("Back-port target not a directory"); + } + + File sourceRoot = new File("."); + for (String dir : Constants.SEARCH_DIRS) { + File directory = new File(dir); + Utils.processDirectory(sourceRoot, directory, sourceTranslations); + } + + for (String dir : Constants.SEARCH_DIRS) { + File directory = new File(targetRoot, dir); + Utils.processDirectory(targetRoot, directory, targetTranslations); + } + + sourceEnglish = sourceTranslations.get(""); + targetEnglish = targetTranslations.get(""); + + storageDir = new File(targetRoot, Constants.STORAGE_DIR); + } + + protected abstract void execute() throws IOException; +} diff --git a/java/org/apache/tomcat/buildutil/translate/BackportEnglish.java b/java/org/apache/tomcat/buildutil/translate/BackportEnglish.java new file mode 100644 index 0000000..d93885e --- /dev/null +++ b/java/org/apache/tomcat/buildutil/translate/BackportEnglish.java @@ -0,0 +1,69 @@ +/* + * 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.tomcat.buildutil.translate; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +/** + * Generates a set of English property files to back-port updates to a previous + * version. Where a key exists in the source and target versions the value is + * copied from the source to the target, overwriting the value in the target. + * The expectation is that the changes will be manually reviewed before + * committing them. + */ +public class BackportEnglish extends BackportBase { + + private static Set keysToExclude = new HashSet<>(); + + + public static void main(String... args) throws IOException { + // Exclude keys known to be different between all versions + keysToExclude.add("java.org.apache.jasper.resources.zzz.jsp.error.jsproot.version.invalid"); + keysToExclude.add("java.org.apache.jasper.resources.zzz.jspc.webfrg.header"); + keysToExclude.add("java.org.apache.jasper.resources.zzz.jspc.webxml.header"); + + // Exclude keys known to be different between 10.1.x and 9.0.x + keysToExclude.add("java.javax.el.zzz.importHandler.invalidClass"); + keysToExclude.add("java.javax.el.zzz.staticFieldELResolver.notFound"); + keysToExclude.add("java.org.apache.catalina.connector.zzz.coyoteConnector.notAsciiSuperset"); + keysToExclude.add("java.org.apache.catalina.loader.zzz.webappClassLoader.addExportsJavaIo"); + keysToExclude.add("java.org.apache.catalina.loader.zzz.webappClassLoader.addExportsRmi"); + keysToExclude.add("java.org.apache.catalina.loader.zzz.webappClassLoader.addExportsThreadLocal"); + + BackportEnglish backport = new BackportEnglish(args); + backport.execute(); + } + + + protected BackportEnglish(String[] args) throws IOException { + super(args); + } + + + @Override + protected void execute() throws IOException { + for (Object key : sourceEnglish.keySet()) { + if (targetEnglish.containsKey(key) && !keysToExclude.contains(key)) { + targetEnglish.put(key, sourceEnglish.get(key)); + } + } + + Utils.export("", targetEnglish, storageDir); + } +} diff --git a/java/org/apache/tomcat/buildutil/translate/BackportTranslations.java b/java/org/apache/tomcat/buildutil/translate/BackportTranslations.java new file mode 100644 index 0000000..ff49322 --- /dev/null +++ b/java/org/apache/tomcat/buildutil/translate/BackportTranslations.java @@ -0,0 +1,68 @@ +/* + * 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.tomcat.buildutil.translate; + +import java.io.IOException; +import java.util.Properties; + +/** + * Generates a set of translated property files to back-port updates to a + * previous version. If the source and target use the same value for the English + * key then any translated value for that key is copied from the source to the + * target. + */ +public class BackportTranslations extends BackportBase { + + public static void main(String... args) throws IOException { + BackportTranslations backport = new BackportTranslations(args); + backport.execute(); + } + + protected BackportTranslations(String[] args) throws IOException { + super(args); + } + + + @Override + protected void execute() throws IOException { + for (String language : targetTranslations.keySet()) { + // Skip source + if (language.length() == 0) { + continue; + } + + Properties sourceTranslated = sourceTranslations.get(language); + Properties targetTranslated = targetTranslations.get(language); + if (targetTranslated == null) { + targetTranslated = new Properties(); + targetTranslations.put(language, targetTranslated); + } + + for (Object key : targetEnglish.keySet()) { + if (sourceTranslated.containsKey(key) && + targetEnglish.get(key).equals(sourceEnglish.get(key))) { + + targetTranslated.put(key, sourceTranslated.get(key)); + } + } + + // Remove translated values for keys that have been removed + targetTranslated.entrySet().removeIf(entry -> !targetEnglish.containsKey(entry.getKey())); + Utils.export(language, targetTranslated, storageDir); + } + } +} diff --git a/java/org/apache/tomcat/buildutil/translate/Constants.java b/java/org/apache/tomcat/buildutil/translate/Constants.java new file mode 100644 index 0000000..62638df --- /dev/null +++ b/java/org/apache/tomcat/buildutil/translate/Constants.java @@ -0,0 +1,32 @@ +/* + * 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.tomcat.buildutil.translate; + +public class Constants { + + public static final String L10N_PREFIX = "LocalStrings"; + public static final String L10N_SUFFIX = ".properties"; + + public static final String[] SEARCH_DIRS = new String[] { "java", "webapps" }; + + public static final String STORAGE_DIR = ".settings/translations"; + + public static final String END_PACKAGE_MARKER = ".zzz."; + + public static final String JAVA_EE_SUBSTRING = "java.javax"; + public static final String JAKARTA_EE_SUBSTRING = "java.jakarta"; +} diff --git a/java/org/apache/tomcat/buildutil/translate/Import.java b/java/org/apache/tomcat/buildutil/translate/Import.java new file mode 100644 index 0000000..ba75987 --- /dev/null +++ b/java/org/apache/tomcat/buildutil/translate/Import.java @@ -0,0 +1,111 @@ +/* + * 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.tomcat.buildutil.translate; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Properties; + +public class Import { + + public static void main(String... args) throws IOException { + File root = new File(Constants.STORAGE_DIR); + + for (File f : root.listFiles()) { + // Not robust but good enough + if (f.isFile() && f.getName().startsWith(Constants.L10N_PREFIX)) { + processFile(f); + } + } + } + + + @SuppressWarnings("null") + private static void processFile(File f) throws IOException { + String language = Utils.getLanguage(f.getName()); + + // Unlike the main branch, don't skip the original so we can import + // updates to the English translations + Properties props = Utils.load(f); + Object[] objKeys = props.keySet().toArray(); + Arrays.sort(objKeys); + + String currentPkg = null; + Writer w = null; + String currentGroup = "zzz"; + + for (Object objKey : objKeys) { + String key = (String) objKey; + String value = props.getProperty(key); + // Skip untranslated values + if (value.trim().length() == 0) { + continue; + } + CompositeKey cKey = new CompositeKey(key); + + if (!cKey.pkg.equals(currentPkg)) { + currentPkg = cKey.pkg; + if (w != null) { + w.close(); + } + File outFile = new File(currentPkg.replace('.', File.separatorChar), Constants.L10N_PREFIX + language + Constants.L10N_SUFFIX); + FileOutputStream fos = new FileOutputStream(outFile); + w = new OutputStreamWriter(fos, StandardCharsets.UTF_8); + org.apache.tomcat.buildutil.Utils.insertLicense(w); + } + + if (!currentGroup.equals(cKey.group)) { + currentGroup = cKey.group; + w.write(System.lineSeparator()); + } + + value = Utils.formatValueImport(value); + value = Utils.fixUnnecessaryEscaping(cKey.key, value); + + w.write(cKey.key + "=" + value); + w.write(System.lineSeparator()); + } + if (w != null) { + w.close(); + } + } + + + private static class CompositeKey { + + public final String pkg; + public final String key; + public final String group; + + CompositeKey(String in) { + int posPkg = in.indexOf(Constants.END_PACKAGE_MARKER); + pkg = in.substring(0, posPkg).replace(Constants.JAVA_EE_SUBSTRING, Constants.JAKARTA_EE_SUBSTRING); + key = in.substring(posPkg + Constants.END_PACKAGE_MARKER.length()); + int posGroup = key.indexOf('.'); + if (posGroup == -1) { + group = ""; + } else { + group = key.substring(0, posGroup); + } + } + } +} diff --git a/java/org/apache/tomcat/buildutil/translate/Utils.java b/java/org/apache/tomcat/buildutil/translate/Utils.java new file mode 100644 index 0000000..91b896f --- /dev/null +++ b/java/org/apache/tomcat/buildutil/translate/Utils.java @@ -0,0 +1,204 @@ +/* + * 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.tomcat.buildutil.translate; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Pattern; + +public class Utils { + + private static final Pattern ESCAPE_LEADING_SPACE = Pattern.compile("^( )", Pattern.MULTILINE); + + private static final Set KEYS_WITH_UNNECESSARY_ESCAPING = new HashSet<>(); + + // Package private so it is visible to tests + static final String PADDING = "POEDITOR_EXPORT_PADDING_DO_NOT_DELETE"; + + static { + KEYS_WITH_UNNECESSARY_ESCAPING.add("arrays.malformed.arrays"); + KEYS_WITH_UNNECESSARY_ESCAPING.add("jsp.error.attribute.deferredmix"); + KEYS_WITH_UNNECESSARY_ESCAPING.add("jsp.error.el.template.deferred"); + } + + + private Utils() { + // Utility class. Hide default constructor. + } + + + static String getLanguage(String name) { + return name.substring(Constants.L10N_PREFIX.length(), name.length() - Constants.L10N_SUFFIX.length()); + } + + + static Properties load(File f) { + Properties props = new Properties(); + + try (FileInputStream fis = new FileInputStream(f); + Reader r = new InputStreamReader(fis, StandardCharsets.UTF_8)) { + props.load(r); + } catch (IOException e) { + e.printStackTrace(); + } + return props; + } + + + static String formatValueExport(String in) { + String result; + + if (in.startsWith("\n")) { + result = PADDING + in; + } else { + result = in; + } + + return formatValueCommon(result); + } + + + static String formatValueImport(String in) { + String result; + + if (in.startsWith(PADDING)) { + result = in.substring(PADDING.length()); + } else { + result = in; + } + + return formatValueCommon(result); + } + + + /* + * Values containing "[{n}]" and "'" need to have the "'" escaped as "''". + * POEditor attempts to do this automatically but does it for any value + * containing "{" or "}" leading to some unnecessary escaping. This method + * undoes the unnecessary escaping. + */ + static String fixUnnecessaryEscaping(String key, String value) { + if (KEYS_WITH_UNNECESSARY_ESCAPING.contains(key)) { + return value.replace("''", "'"); + } + return value; + } + + + /* + * Common formatting to convert a String for storage as a value in a + * property file. + */ + static String formatValueCommon(String in) { + String result = in.replace("\n", "\\n\\\n"); + if (result.endsWith("\\n\\\n")) { + result = result.substring(0, result.length() - 2); + } + + result = ESCAPE_LEADING_SPACE.matcher(result).replaceAll("\\\\$1"); + + result = result.replace("\t", "\\t"); + + return result; + } + + + static void processDirectory(File root, File dir, Map translations) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalArgumentException("Not a directory [" + dir.getAbsolutePath() + "]"); + } + for (File f : files) { + if (f.isDirectory()) { + processDirectory(root, f, translations); + } else if (f.isFile()) { + processFile(root, f, translations); + } + } + } + + + static void processFile(File root, File f, Map translations) throws IOException { + String name = f.getName(); + + // non-l10n files + if (!name.startsWith(Constants.L10N_PREFIX)) { + return; + } + + // Determine language + String language = getLanguage(name); + + String keyPrefix = getKeyPrefix(root, f); + Properties props = load(f); + + // Create a Map for the language if one does not exist. + Properties translation = translations.get(language); + if (translation == null) { + translation = new Properties(); + translations.put(language, translation); + } + + // Add the properties from this file to the combined file, prefixing the + // key with the package name to ensure uniqueness. + for (Object obj : props.keySet()) { + String key = (String) obj; + String value = props.getProperty(key); + + translation.put(keyPrefix + key, value); + } + } + + + static String getKeyPrefix(File root, File f) throws IOException { + String prefix = f.getParentFile().getCanonicalPath(); + prefix = prefix.substring(root.getCanonicalPath().length() + 1); + prefix = prefix.replace(File.separatorChar, '.'); + prefix = prefix + Constants.END_PACKAGE_MARKER; + // POEditor uses javax package names. + // Renaming here is less work than renaming terms in POEditor + prefix = prefix.replace(Constants.JAKARTA_EE_SUBSTRING, Constants.JAVA_EE_SUBSTRING); + return prefix; + } + + + static void export(String language, Properties translation, File storageDir) { + File out = new File(storageDir, Constants.L10N_PREFIX + language + Constants.L10N_SUFFIX); + try (FileOutputStream fos = new FileOutputStream(out); + Writer w = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { + String[] keys = translation.keySet().toArray(new String[0]); + Arrays.sort(keys); + for (Object key : keys) { + w.write(key + "=" + formatValueExport(translation.getProperty((String) key)) + "\n"); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java b/java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java new file mode 100644 index 0000000..e57b410 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java @@ -0,0 +1,228 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.lang.ref.WeakReference; +import java.sql.SQLException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.tomcat.dbcp.pool2.TrackedUse; + +/** + * Tracks connection usage for recovering and reporting abandoned connections. + *

    + * The JDBC Connection, Statement, and ResultSet classes extend this class. + *

    + * + * @since 2.0 + */ +public class AbandonedTrace implements TrackedUse, AutoCloseable { + + static void add(AbandonedTrace receiver, AbandonedTrace trace) { + if (receiver != null) { + receiver.addTrace(trace); + } + } + + /** A list of objects created by children of this object. */ + private final List> traceList = new ArrayList<>(); + + /** Last time this connection was used. */ + private volatile Instant lastUsedInstant = Instant.EPOCH; + + /** + * Creates a new AbandonedTrace without config and without doing abandoned tracing. + */ + public AbandonedTrace() { + init(null); + } + + /** + * Constructs a new AbandonedTrace with a parent object. + * + * @param parent + * AbandonedTrace parent object. + */ + public AbandonedTrace(final AbandonedTrace parent) { + init(parent); + } + + /** + * Adds an object to the list of objects being traced. + * + * @param trace + * AbandonedTrace object to add. + */ + protected void addTrace(final AbandonedTrace trace) { + synchronized (this.traceList) { + this.traceList.add(new WeakReference<>(trace)); + } + setLastUsed(); + } + + /** + * Clears the list of objects being traced by this object. + */ + protected void clearTrace() { + synchronized (this.traceList) { + this.traceList.clear(); + } + } + + /** + * Subclasses can implement this nop. + * + * @throws SQLException Ignored here, for subclasses. + * @since 2.10.0 + */ + @Override + public void close() throws SQLException { + // nop + } + + /** + * Closes this resource and if an exception is caught, then calls {@code exceptionHandler}. + * + * @param exceptionHandler Consumes exception thrown closing this resource. + * @since 2.10.0 + */ + protected void close(final Consumer exceptionHandler) { + Utils.close(this, exceptionHandler); + } + + /** + * Gets the last time this object was used in milliseconds. + * + * @return long time in milliseconds. + */ + @Override + @Deprecated + public long getLastUsed() { + return lastUsedInstant.toEpochMilli(); + } + + @Override + public Instant getLastUsedInstant() { + return lastUsedInstant; + } + + /** + * Gets a list of objects being traced by this object. + * + * @return List of objects. + */ + protected List getTrace() { + final int size = traceList.size(); + if (size == 0) { + return Collections.emptyList(); + } + final ArrayList result = new ArrayList<>(size); + synchronized (this.traceList) { + final Iterator> iter = traceList.iterator(); + while (iter.hasNext()) { + final AbandonedTrace trace = iter.next().get(); + if (trace == null) { + // Clean-up since we are here anyway + iter.remove(); + } else { + result.add(trace); + } + } + } + return result; + } + + /** + * Initializes abandoned tracing for this object. + * + * @param parent + * AbandonedTrace parent object. + */ + private void init(final AbandonedTrace parent) { + AbandonedTrace.add(parent, this); + } + + /** + * Removes this object the source object is tracing. + * + * @param source The object tracing + * @since 2.7.0 + */ + protected void removeThisTrace(final Object source) { + if (source instanceof AbandonedTrace) { + AbandonedTrace.class.cast(source).removeTrace(this); + } + } + + /** + * Removes a child object this object is tracing. + * + * @param trace + * AbandonedTrace object to remove. + */ + protected void removeTrace(final AbandonedTrace trace) { + synchronized (this.traceList) { + final Iterator> iter = traceList.iterator(); + while (iter.hasNext()) { + final AbandonedTrace traceInList = iter.next().get(); + if (trace != null && trace.equals(traceInList)) { + iter.remove(); + break; + } + if (traceInList == null) { + // Clean-up since we are here anyway + iter.remove(); + } + } + } + } + + /** + * Sets the time this object was last used to the current time in milliseconds. + */ + protected void setLastUsed() { + lastUsedInstant = Instant.now(); + } + + /** + * Sets the instant this object was last used. + * + * @param lastUsedInstant + * instant. + * @since 2.10.0 + */ + protected void setLastUsed(final Instant lastUsedInstant) { + this.lastUsedInstant = lastUsedInstant; + } + + /** + * Sets the time in milliseconds this object was last used. + * + * @param lastUsedMillis + * time in milliseconds. + * @deprecated Use {@link #setLastUsed(Instant)} + */ + @Deprecated + protected void setLastUsed(final long lastUsedMillis) { + this.lastUsedInstant = Instant.ofEpochMilli(lastUsedMillis); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java b/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java new file mode 100644 index 0000000..fcd65d5 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java @@ -0,0 +1,2610 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.management.MBeanRegistration; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; +import javax.management.StandardMBean; +import javax.sql.DataSource; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.impl.AbandonedConfig; +import org.apache.tomcat.dbcp.pool2.impl.BaseObjectPoolConfig; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPoolConfig; +import org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.GenericObjectPoolConfig; + +/** + * Basic implementation of {@code javax.sql.DataSource} that is configured via JavaBeans properties. + * + *

    + * This is not the only way to combine the commons-dbcp2 and commons-pool2 packages, but provides a + * one-stop solution for basic requirements. + *

    + * + * @since 2.0 + */ +public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBeanRegistration, AutoCloseable { + + private static final Log log = LogFactory.getLog(BasicDataSource.class); + + static { + // Attempt to prevent deadlocks - see DBCP - 272 + DriverManager.getDrivers(); + try { + // Load classes now to prevent AccessControlExceptions later + // A number of classes are loaded when getConnection() is called + // but the following classes are not loaded and therefore require + // explicit loading. + if (Utils.isSecurityEnabled()) { + final ClassLoader loader = BasicDataSource.class.getClassLoader(); + final String dbcpPackageName = BasicDataSource.class.getPackage().getName(); + loader.loadClass(dbcpPackageName + ".DelegatingCallableStatement"); + loader.loadClass(dbcpPackageName + ".DelegatingDatabaseMetaData"); + loader.loadClass(dbcpPackageName + ".DelegatingPreparedStatement"); + loader.loadClass(dbcpPackageName + ".DelegatingResultSet"); + loader.loadClass(dbcpPackageName + ".PoolableCallableStatement"); + loader.loadClass(dbcpPackageName + ".PoolablePreparedStatement"); + loader.loadClass(dbcpPackageName + ".PoolingConnection$StatementType"); + loader.loadClass(dbcpPackageName + ".PStmtKey"); + + final String poolPackageName = PooledObject.class.getPackage().getName(); + loader.loadClass(poolPackageName + ".impl.LinkedBlockingDeque$Node"); + loader.loadClass(poolPackageName + ".impl.GenericKeyedObjectPool$ObjectDeque"); + } + } catch (final ClassNotFoundException cnfe) { + throw new IllegalStateException("Unable to pre-load classes", cnfe); + } + } + + /** + * Validates the given factory. + * + * @param connectionFactory the factory + * @throws SQLException Thrown by one of the factory methods while managing a temporary pooled object. + */ + protected static void validateConnectionFactory(final PoolableConnectionFactory connectionFactory) throws SQLException { + PoolableConnection conn = null; + PooledObject p = null; + try { + p = connectionFactory.makeObject(); + conn = p.getObject(); + connectionFactory.activateObject(p); + connectionFactory.validateConnection(conn); + connectionFactory.passivateObject(p); + } finally { + if (p != null) { + connectionFactory.destroyObject(p); + } + } + } + + /** + * The default auto-commit state of connections created by this pool. + */ + private volatile Boolean defaultAutoCommit; + + /** + * The default read-only state of connections created by this pool. + */ + private transient Boolean defaultReadOnly; + + /** + * The default TransactionIsolation state of connections created by this pool. + */ + private volatile int defaultTransactionIsolation = PoolableConnectionFactory.UNKNOWN_TRANSACTION_ISOLATION; + + private Duration defaultQueryTimeoutDuration; + + /** + * The default "catalog" of connections created by this pool. + */ + private volatile String defaultCatalog; + + /** + * The default "schema" of connections created by this pool. + */ + private volatile String defaultSchema; + + /** + * The property that controls if the pooled connections cache some state rather than query the database for current + * state to improve performance. + */ + private boolean cacheState = true; + + /** + * The instance of the JDBC Driver to use. + */ + private Driver driver; + + /** + * The fully qualified Java class name of the JDBC driver to be used. + */ + private String driverClassName; + + /** + * The class loader instance to use to load the JDBC driver. If not specified, {@link Class#forName(String)} is used + * to load the JDBC driver. If specified, {@link Class#forName(String, boolean, ClassLoader)} is used. + */ + private ClassLoader driverClassLoader; + + /** + * True means that borrowObject returns the most recently used ("last in") connection in the pool (if there are idle + * connections available). False means that the pool behaves as a FIFO queue - connections are taken from the idle + * instance pool in the order that they are returned to the pool. + */ + private boolean lifo = BaseObjectPoolConfig.DEFAULT_LIFO; + + /** + * The maximum number of active connections that can be allocated from this pool at the same time, or negative for + * no limit. + */ + private int maxTotal = GenericObjectPoolConfig.DEFAULT_MAX_TOTAL; + + /** + * The maximum number of connections that can remain idle in the pool, without extra ones being destroyed, or + * negative for no limit. If maxIdle is set too low on heavily loaded systems it is possible you will see + * connections being closed and almost immediately new connections being opened. This is a result of the active + * threads momentarily closing connections faster than they are opening them, causing the number of idle connections + * to rise above maxIdle. The best value for maxIdle for heavily loaded system will vary but the default is a good + * starting point. + */ + private int maxIdle = GenericObjectPoolConfig.DEFAULT_MAX_IDLE; + + /** + * The minimum number of active connections that can remain idle in the pool, without extra ones being created when + * the evictor runs, or 0 to create none. The pool attempts to ensure that minIdle connections are available when + * the idle object evictor runs. The value of this property has no effect unless + * {@link #durationBetweenEvictionRuns} has a positive value. + */ + private int minIdle = GenericObjectPoolConfig.DEFAULT_MIN_IDLE; + + /** + * The initial number of connections that are created when the pool is started. + */ + private int initialSize; + + /** + * The maximum Duration that the pool will wait (when there are no available connections) for a + * connection to be returned before throwing an exception, or <= 0 to wait indefinitely. + */ + private Duration maxWaitDuration = BaseObjectPoolConfig.DEFAULT_MAX_WAIT; + + /** + * Prepared statement pooling for this pool. When this property is set to {@code true} both PreparedStatements + * and CallableStatements are pooled. + */ + private boolean poolPreparedStatements; + + private boolean clearStatementPoolOnReturn; + + /** + *

    + * The maximum number of open statements that can be allocated from the statement pool at the same time, or negative + * for no limit. Since a connection usually only uses one or two statements at a time, this is mostly used to help + * detect resource leaks. + *

    + *

    + * Note: As of version 1.3, CallableStatements (those produced by {@link Connection#prepareCall}) are pooled along + * with PreparedStatements (produced by {@link Connection#prepareStatement}) and + * {@code maxOpenPreparedStatements} limits the total number of prepared or callable statements that may be in + * use at a given time. + *

    + */ + private int maxOpenPreparedStatements = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL; + + /** + * The indication of whether objects will be validated as soon as they have been created by the pool. If the object + * fails to validate, the borrow operation that triggered the creation will fail. + */ + private boolean testOnCreate; + + /** + * The indication of whether objects will be validated before being borrowed from the pool. If the object fails to + * validate, it will be dropped from the pool, and we will attempt to borrow another. + */ + private boolean testOnBorrow = true; + + /** + * The indication of whether objects will be validated before being returned to the pool. + */ + private boolean testOnReturn; + + /** + * The number of milliseconds to sleep between runs of the idle object evictor thread. When non-positive, no idle + * object evictor thread will be run. + */ + private Duration durationBetweenEvictionRuns = BaseObjectPoolConfig.DEFAULT_DURATION_BETWEEN_EVICTION_RUNS; + + /** + * The number of objects to examine during each run of the idle object evictor thread (if any). + */ + private int numTestsPerEvictionRun = BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN; + + /** + * The minimum amount of time an object may sit idle in the pool before it is eligible for eviction by the idle + * object evictor (if any). + */ + private Duration minEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION; + + /** + * The minimum amount of time a connection may sit idle in the pool before it is eligible for eviction by the idle + * object evictor, with the extra condition that at least "minIdle" connections remain in the pool. Note that + * {@code minEvictableIdleTimeMillis} takes precedence over this parameter. See + * {@link #getSoftMinEvictableIdleDuration()}. + */ + private Duration softMinEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION; + + private String evictionPolicyClassName = BaseObjectPoolConfig.DEFAULT_EVICTION_POLICY_CLASS_NAME; + + /** + * The indication of whether objects will be validated by the idle object evictor (if any). If an object fails to + * validate, it will be dropped from the pool. + */ + private boolean testWhileIdle; + + /** + * The connection password to be passed to our JDBC driver to establish a connection. + */ + private volatile String password; + + /** + * The connection string to be passed to our JDBC driver to establish a connection. + */ + private String connectionString; + + /** + * The connection user name to be passed to our JDBC driver to establish a connection. + */ + private String userName; + + /** + * The SQL query that will be used to validate connections from this pool before returning them to the caller. If + * specified, this query MUST be an SQL SELECT statement that returns at least one row. If not + * specified, {@link Connection#isValid(int)} will be used to validate connections. + */ + private volatile String validationQuery; + + /** + * Timeout in seconds before connection validation queries fail. + */ + private volatile Duration validationQueryTimeoutDuration = Duration.ofSeconds(-1); + + /** + * The fully qualified Java class name of a {@link ConnectionFactory} implementation. + */ + private String connectionFactoryClassName; + + /** + * These SQL statements run once after a Connection is created. + *

    + * This property can be used for example to run ALTER SESSION SET NLS_SORT=XCYECH in an Oracle Database only once + * after connection creation. + *

    + */ + private volatile List connectionInitSqls; + + /** + * Controls access to the underlying connection. + */ + private boolean accessToUnderlyingConnectionAllowed; + + private Duration maxConnDuration = Duration.ofMillis(-1); + + private boolean logExpiredConnections = true; + + private String jmxName; + + private boolean registerConnectionMBean = true; + + private boolean autoCommitOnReturn = true; + + private boolean rollbackOnReturn = true; + + private volatile Set disconnectionSqlCodes; + + private boolean fastFailValidation; + + /** + * The object pool that internally manages our connections. + */ + private volatile GenericObjectPool connectionPool; + + /** + * The connection properties that will be sent to our JDBC driver when establishing new connections. + * NOTE - The "user" and "password" properties will be passed explicitly, so they do not need to be + * included here. + */ + private Properties connectionProperties = new Properties(); + + /** + * The data source we will use to manage connections. This object should be acquired ONLY by calls + * to the {@code createDataSource()} method. + */ + private volatile DataSource dataSource; + + /** + * The PrintWriter to which log messages should be directed. + */ + private volatile PrintWriter logWriter = new PrintWriter( + new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); + + private AbandonedConfig abandonedConfig; + + private boolean closed; + + /** + * Actual name under which this component has been registered. + */ + private ObjectNameWrapper registeredJmxObjectName; + + /** + * Adds a custom connection property to the set that will be passed to our JDBC driver. This MUST + * be called before the first connection is retrieved (along with all the other configuration property setters). + * Calls to this method after the connection pool has been initialized have no effect. + * + * @param name Name of the custom connection property + * @param value Value of the custom connection property + */ + public void addConnectionProperty(final String name, final String value) { + connectionProperties.put(name, value); + } + + /** + * Closes and releases all idle connections that are currently stored in the connection pool associated with this + * data source. + *

    + * Connections that are checked out to clients when this method is invoked are not affected. When client + * applications subsequently invoke {@link Connection#close()} to return these connections to the pool, the + * underlying JDBC connections are closed. + *

    + *

    + * Attempts to acquire connections using {@link #getConnection()} after this method has been invoked result in + * SQLExceptions. To reopen a datasource that has been closed using this method, use {@link #start()}. + *

    + *

    + * This method is idempotent - i.e., closing an already closed BasicDataSource has no effect and does not generate + * exceptions. + *

    + * + * @throws SQLException if an error occurs closing idle connections + */ + @Override + public synchronized void close() throws SQLException { + if (registeredJmxObjectName != null) { + registeredJmxObjectName.unregisterMBean(); + registeredJmxObjectName = null; + } + closed = true; + final GenericObjectPool oldPool = connectionPool; + connectionPool = null; + dataSource = null; + try { + if (oldPool != null) { + oldPool.close(); + } + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException(Utils.getMessage("pool.close.fail"), e); + } + } + + /** + * Closes the connection pool, silently swallowing any exception that occurs. + */ + private void closeConnectionPool() { + final GenericObjectPool oldPool = connectionPool; + connectionPool = null; + Utils.closeQuietly(oldPool); + } + + /** + * Creates a JDBC connection factory for this data source. The JDBC driver is loaded using the following algorithm: + *
      + *
    1. If a Driver instance has been specified via {@link #setDriver(Driver)} use it
    2. + *
    3. If no Driver instance was specified and {code driverClassName} is specified that class is loaded using the + * {@link ClassLoader} of this class or, if {code driverClassLoader} is set, {code driverClassName} is loaded + * with the specified {@link ClassLoader}.
    4. + *
    5. If {code driverClassName} is specified and the previous attempt fails, the class is loaded using the + * context class loader of the current thread.
    6. + *
    7. If a driver still isn't loaded one is loaded via the {@link DriverManager} using the specified {code connectionString}. + *
    + *

    + * This method exists so subclasses can replace the implementation class. + *

    + * + * @return A new connection factory. + * + * @throws SQLException If the connection factory cannot be created + */ + protected ConnectionFactory createConnectionFactory() throws SQLException { + // Load the JDBC driver class + return ConnectionFactoryFactory.createConnectionFactory(this, DriverFactory.createDriver(this)); + } + + + /** + * Creates a connection pool for this datasource. This method only exists so subclasses can replace the + * implementation class. + *

    + * This implementation configures all pool properties other than timeBetweenEvictionRunsMillis. Setting that + * property is deferred to {@link #startPoolMaintenance()}, since setting timeBetweenEvictionRunsMillis to a + * positive value causes {@link GenericObjectPool}'s eviction timer to be started. + *

    + * + * @param factory The factory to use to create new connections for this pool. + */ + protected void createConnectionPool(final PoolableConnectionFactory factory) { + // Create an object pool to contain our active connections + final GenericObjectPoolConfig config = new GenericObjectPoolConfig<>(); + updateJmxName(config); + // Disable JMX on the underlying pool if the DS is not registered: + config.setJmxEnabled(registeredJmxObjectName != null); + final GenericObjectPool gop = createObjectPool(factory, config, abandonedConfig); + gop.setMaxTotal(maxTotal); + gop.setMaxIdle(maxIdle); + gop.setMinIdle(minIdle); + gop.setMaxWait(maxWaitDuration); + gop.setTestOnCreate(testOnCreate); + gop.setTestOnBorrow(testOnBorrow); + gop.setTestOnReturn(testOnReturn); + gop.setNumTestsPerEvictionRun(numTestsPerEvictionRun); + gop.setMinEvictableIdleDuration(minEvictableIdleDuration); + gop.setSoftMinEvictableIdleDuration(softMinEvictableIdleDuration); + gop.setTestWhileIdle(testWhileIdle); + gop.setLifo(lifo); + gop.setSwallowedExceptionListener(new SwallowedExceptionLogger(log, logExpiredConnections)); + gop.setEvictionPolicyClassName(evictionPolicyClassName); + factory.setPool(gop); + connectionPool = gop; + } + + /** + * Creates (if necessary) and return the internal data source we are using to manage our connections. + * + * @return The current internal DataSource or a newly created instance if it has not yet been created. + * @throws SQLException if the object pool cannot be created. + */ + protected synchronized DataSource createDataSource() throws SQLException { + if (closed) { + throw new SQLException("Data source is closed"); + } + + // Return the pool if we have already created it + // This is double-checked locking. This is safe since dataSource is + // volatile and the code is targeted at Java 5 onwards. + if (dataSource != null) { + return dataSource; + } + synchronized (this) { + if (dataSource != null) { + return dataSource; + } + jmxRegister(); + + // create factory which returns raw physical connections + final ConnectionFactory driverConnectionFactory = createConnectionFactory(); + + // Set up the poolable connection factory + final PoolableConnectionFactory poolableConnectionFactory; + try { + poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory); + poolableConnectionFactory.setPoolStatements(poolPreparedStatements); + poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements); + // create a pool for our connections + createConnectionPool(poolableConnectionFactory); + final DataSource newDataSource = createDataSourceInstance(); + newDataSource.setLogWriter(logWriter); + connectionPool.addObjects(initialSize); + // If timeBetweenEvictionRunsMillis > 0, start the pool's evictor + // task + startPoolMaintenance(); + dataSource = newDataSource; + } catch (final SQLException | RuntimeException se) { + closeConnectionPool(); + throw se; + } catch (final Exception ex) { + closeConnectionPool(); + throw new SQLException("Error creating connection factory", ex); + } + + return dataSource; + } + } + + /** + * Creates the actual data source instance. This method only exists so that subclasses can replace the + * implementation class. + * + * @throws SQLException if unable to create a datasource instance + * + * @return A new DataSource instance + */ + protected DataSource createDataSourceInstance() throws SQLException { + final PoolingDataSource pds = new PoolingDataSource<>(connectionPool); + pds.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed()); + return pds; + } + + /** + * Creates an object pool used to provide pooling support for {@link Connection JDBC connections}. + * + * @param factory the object factory + * @param poolConfig the object pool configuration + * @param abandonedConfig the abandoned objects configuration + * @return a non-null instance + */ + protected GenericObjectPool createObjectPool(final PoolableConnectionFactory factory, + final GenericObjectPoolConfig poolConfig, final AbandonedConfig abandonedConfig) { + final GenericObjectPool gop; + if (abandonedConfig != null && (abandonedConfig.getRemoveAbandonedOnBorrow() + || abandonedConfig.getRemoveAbandonedOnMaintenance())) { + gop = new GenericObjectPool<>(factory, poolConfig, abandonedConfig); + } else { + gop = new GenericObjectPool<>(factory, poolConfig); + } + return gop; + } + + /** + * Creates the PoolableConnectionFactory and attaches it to the connection pool. This method only exists so + * subclasses can replace the default implementation. + * + * @param driverConnectionFactory JDBC connection factory + * @throws SQLException if an error occurs creating the PoolableConnectionFactory + * + * @return A new PoolableConnectionFactory configured with the current configuration of this BasicDataSource + */ + protected PoolableConnectionFactory createPoolableConnectionFactory(final ConnectionFactory driverConnectionFactory) + throws SQLException { + PoolableConnectionFactory connectionFactory = null; + try { + if (registerConnectionMBean) { + connectionFactory = new PoolableConnectionFactory(driverConnectionFactory, ObjectNameWrapper.unwrap(registeredJmxObjectName)); + } else { + connectionFactory = new PoolableConnectionFactory(driverConnectionFactory, null); + } + connectionFactory.setValidationQuery(validationQuery); + connectionFactory.setValidationQueryTimeout(validationQueryTimeoutDuration); + connectionFactory.setConnectionInitSql(connectionInitSqls); + connectionFactory.setDefaultReadOnly(defaultReadOnly); + connectionFactory.setDefaultAutoCommit(defaultAutoCommit); + connectionFactory.setDefaultTransactionIsolation(defaultTransactionIsolation); + connectionFactory.setDefaultCatalog(defaultCatalog); + connectionFactory.setDefaultSchema(defaultSchema); + connectionFactory.setCacheState(cacheState); + connectionFactory.setPoolStatements(poolPreparedStatements); + connectionFactory.setClearStatementPoolOnReturn(clearStatementPoolOnReturn); + connectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements); + connectionFactory.setMaxConn(maxConnDuration); + connectionFactory.setRollbackOnReturn(getRollbackOnReturn()); + connectionFactory.setAutoCommitOnReturn(getAutoCommitOnReturn()); + connectionFactory.setDefaultQueryTimeout(getDefaultQueryTimeoutDuration()); + connectionFactory.setFastFailValidation(fastFailValidation); + connectionFactory.setDisconnectionSqlCodes(disconnectionSqlCodes); + validateConnectionFactory(connectionFactory); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Cannot create PoolableConnectionFactory (" + e.getMessage() + ")", e); + } + return connectionFactory; + } + + /** + * Manually evicts idle connections + * + * @throws Exception when there is a problem evicting idle objects. + */ + public void evict() throws Exception { + if (connectionPool != null) { + connectionPool.evict(); + } + } + + /** + * Gets the print writer used by this configuration to log information on abandoned objects. + * + * @return The print writer used by this configuration to log information on abandoned objects. + */ + public PrintWriter getAbandonedLogWriter() { + return abandonedConfig == null ? null : abandonedConfig.getLogWriter(); + } + + /** + * If the connection pool implements {@link org.apache.tomcat.dbcp.pool2.UsageTracking UsageTracking}, should the + * connection pool record a stack trace every time a method is called on a pooled connection and retain the most + * recent stack trace to aid debugging of abandoned connections? + * + * @return {@code true} if usage tracking is enabled + */ + @Override + public boolean getAbandonedUsageTracking() { + return abandonedConfig != null && abandonedConfig.getUseUsageTracking(); + } + + /** + * Gets the value of the flag that controls whether or not connections being returned to the pool will be checked + * and configured with {@link Connection#setAutoCommit(boolean) Connection.setAutoCommit(true)} if the auto commit + * setting is {@code false} when the connection is returned. It is {@code true} by default. + * + * @return Whether or not connections being returned to the pool will be checked and configured with auto-commit. + */ + public boolean getAutoCommitOnReturn() { + return autoCommitOnReturn; + } + + /** + * Gets the state caching flag. + * + * @return the state caching flag + */ + @Override + public boolean getCacheState() { + return cacheState; + } + + /** + * Creates (if necessary) and return a connection to the database. + * + * @throws SQLException if a database access error occurs + * @return a database connection + */ + @Override + public Connection getConnection() throws SQLException { + if (Utils.isSecurityEnabled()) { + final PrivilegedExceptionAction action = () -> createDataSource().getConnection(); + try { + return AccessController.doPrivileged(action); + } catch (final PrivilegedActionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof SQLException) { + throw (SQLException) cause; + } + throw new SQLException(e); + } + } + return createDataSource().getConnection(); + } + + /** + * BasicDataSource does NOT support this method. + * + * @param user Database user on whose behalf the Connection is being made + * @param pass The database user's password + * + * @throws UnsupportedOperationException always thrown. + * @throws SQLException if a database access error occurs + * @return nothing - always throws UnsupportedOperationException + */ + @Override + public Connection getConnection(final String user, final String pass) throws SQLException { + // This method isn't supported by the PoolingDataSource returned by the + // createDataSource + throw new UnsupportedOperationException("Not supported by BasicDataSource"); + } + + /** + * Gets the ConnectionFactoryClassName that has been configured for use by this pool. + *

    + * Note: This getter only returns the last value set by a call to {@link #setConnectionFactoryClassName(String)}. + *

    + * + * @return the ConnectionFactoryClassName that has been configured for use by this pool. + * @since 2.7.0 + */ + public String getConnectionFactoryClassName() { + return this.connectionFactoryClassName; + } + + /** + * Gets the list of SQL statements executed when a physical connection is first created. Returns an empty list if + * there are no initialization statements configured. + * + * @return initialization SQL statements + */ + public List getConnectionInitSqls() { + final List result = connectionInitSqls; + return result == null ? Collections.emptyList() : result; + } + + /** + * Provides the same data as {@link #getConnectionInitSqls()} but in an array so it is accessible via JMX. + */ + @Override + public String[] getConnectionInitSqlsAsArray() { + return getConnectionInitSqls().toArray(Utils.EMPTY_STRING_ARRAY); + } + + /** + * Gets the underlying connection pool. + * + * @return the underlying connection pool. + */ + protected GenericObjectPool getConnectionPool() { + return connectionPool; + } + + Properties getConnectionProperties() { + return connectionProperties; + } + + /** + * Gets the default auto-commit property. + * + * @return true if default auto-commit is enabled + */ + @Override + public Boolean getDefaultAutoCommit() { + return defaultAutoCommit; + } + + /** + * Gets the default catalog. + * + * @return the default catalog + */ + @Override + public String getDefaultCatalog() { + return this.defaultCatalog; + } + + /** + * Gets the default query timeout that will be used for {@link java.sql.Statement Statement}s created from this + * connection. {@code null} means that the driver default will be used. + * + * @return The default query timeout in seconds. + * @deprecated Use {@link #getDefaultQueryTimeoutDuration()}. + */ + @Deprecated + public Integer getDefaultQueryTimeout() { + return defaultQueryTimeoutDuration == null ? null : Integer.valueOf((int) defaultQueryTimeoutDuration.getSeconds()); + } + + /** + * Gets the default query timeout that will be used for {@link java.sql.Statement Statement}s created from this + * connection. {@code null} means that the driver default will be used. + * + * @return The default query timeout Duration. + * @since 2.10.0 + */ + public Duration getDefaultQueryTimeoutDuration() { + return defaultQueryTimeoutDuration; + } + + /** + * Gets the default readOnly property. + * + * @return true if connections are readOnly by default + */ + @Override + public Boolean getDefaultReadOnly() { + return defaultReadOnly; + } + + /** + * Gets the default schema. + * + * @return the default schema. + * @since 2.5.0 + */ + @Override + public String getDefaultSchema() { + return this.defaultSchema; + } + + /** + * Gets the default transaction isolation state of returned connections. + * + * @return the default value for transaction isolation state + * @see Connection#getTransactionIsolation + */ + @Override + public int getDefaultTransactionIsolation() { + return this.defaultTransactionIsolation; + } + + /** + * Gets the set of SQL_STATE codes considered to signal fatal conditions. + * + * @return fatal disconnection state codes + * @see #setDisconnectionSqlCodes(Collection) + * @since 2.1 + */ + public Set getDisconnectionSqlCodes() { + final Set result = disconnectionSqlCodes; + return result == null ? Collections.emptySet() : result; + } + + /** + * Provides the same data as {@link #getDisconnectionSqlCodes} but in an array so it is accessible via JMX. + * + * @since 2.1 + */ + @Override + public String[] getDisconnectionSqlCodesAsArray() { + return getDisconnectionSqlCodes().toArray(Utils.EMPTY_STRING_ARRAY); + } + + /** + * Gets the JDBC Driver that has been configured for use by this pool. + *

    + * Note: This getter only returns the last value set by a call to {@link #setDriver(Driver)}. It does not return any + * driver instance that may have been created from the value set via {@link #setDriverClassName(String)}. + *

    + * + * @return the JDBC Driver that has been configured for use by this pool + */ + public synchronized Driver getDriver() { + return driver; + } + + /** + * Gets the class loader specified for loading the JDBC driver. Returns {@code null} if no class loader has + * been explicitly specified. + *

    + * Note: This getter only returns the last value set by a call to {@link #setDriverClassLoader(ClassLoader)}. It + * does not return the class loader of any driver that may have been set via {@link #setDriver(Driver)}. + *

    + * + * @return The class loader specified for loading the JDBC driver. + */ + public synchronized ClassLoader getDriverClassLoader() { + return this.driverClassLoader; + } + + /** + * Gets the JDBC driver class name. + *

    + * Note: This getter only returns the last value set by a call to {@link #setDriverClassName(String)}. It does not + * return the class name of any driver that may have been set via {@link #setDriver(Driver)}. + *

    + * + * @return the JDBC driver class name + */ + @Override + public synchronized String getDriverClassName() { + return this.driverClassName; + } + + /** + * Gets the value of the flag that controls whether or not connections being returned to the pool will be checked + * and configured with {@link Connection#setAutoCommit(boolean) Connection.setAutoCommit(true)} if the auto commit + * setting is {@code false} when the connection is returned. It is {@code true} by default. + * + * @return Whether or not connections being returned to the pool will be checked and configured with auto-commit. + * @deprecated Use {@link #getAutoCommitOnReturn()}. + */ + @Deprecated + public boolean getEnableAutoCommitOnReturn() { + return autoCommitOnReturn; + } + + /** + * Gets the EvictionPolicy implementation in use with this connection pool. + * + * @return The EvictionPolicy implementation in use with this connection pool. + */ + public synchronized String getEvictionPolicyClassName() { + return evictionPolicyClassName; + } + + /** + * True means that validation will fail immediately for connections that have previously thrown SQLExceptions with + * SQL_STATE indicating fatal disconnection errors. + * + * @return true if connections created by this datasource will fast fail validation. + * @see #setDisconnectionSqlCodes(Collection) + * @since 2.1 + */ + @Override + public boolean getFastFailValidation() { + return fastFailValidation; + } + + /** + * Gets the initial size of the connection pool. + * + * @return the number of connections created when the pool is initialized + */ + @Override + public synchronized int getInitialSize() { + return this.initialSize; + } + + /** + * Gets the JMX name that has been requested for this DataSource. If the requested name is not valid, an + * alternative may be chosen. + * + * @return The JMX name that has been requested for this DataSource. + */ + public String getJmxName() { + return jmxName; + } + + /** + * Gets the LIFO property. + * + * @return true if connection pool behaves as a LIFO queue. + */ + @Override + public synchronized boolean getLifo() { + return this.lifo; + } + + /** + * Flag to log stack traces for application code which abandoned a Statement or Connection. + *

    + * Defaults to false. + *

    + *

    + * Logging of abandoned Statements and Connections adds overhead for every Connection open or new Statement because + * a stack trace has to be generated. + *

    + */ + @Override + public boolean getLogAbandoned() { + return abandonedConfig != null && abandonedConfig.getLogAbandoned(); + } + + /** + * When {@link #getMaxConnDuration()} is set to limit connection lifetime, this property determines whether or + * not log messages are generated when the pool closes connections due to maximum lifetime exceeded. + * + * @since 2.1 + */ + @Override + public boolean getLogExpiredConnections() { + return logExpiredConnections; + } + + /** + * BasicDataSource does NOT support this method. + * + *

    + * Gets the login timeout (in seconds) for connecting to the database. + *

    + *

    + * Calls {@link #createDataSource()}, so has the side effect of initializing the connection pool. + *

    + * + * @throws SQLException if a database access error occurs + * @throws UnsupportedOperationException If the DataSource implementation does not support the login timeout + * feature. + * @return login timeout in seconds + */ + @Override + public int getLoginTimeout() throws SQLException { + // This method isn't supported by the PoolingDataSource returned by the createDataSource + throw new UnsupportedOperationException("Not supported by BasicDataSource"); + } + + /** + * Gets the log writer being used by this data source. + *

    + * Calls {@link #createDataSource()}, so has the side effect of initializing the connection pool. + *

    + * + * @throws SQLException if a database access error occurs + * @return log writer in use + */ + @Override + public PrintWriter getLogWriter() throws SQLException { + return createDataSource().getLogWriter(); + } + + /** + * Gets the maximum permitted duration of a connection. A value of zero or less indicates an + * infinite lifetime. + * @return the maximum permitted duration of a connection. + * @since 2.10.0 + */ + public Duration getMaxConnDuration() { + return maxConnDuration; + } + + /** + * Gets the maximum permitted lifetime of a connection in milliseconds. A value of zero or less indicates an + * infinite lifetime. + * @deprecated Use {@link #getMaxConnDuration()}. + */ + @Override + @Deprecated + public long getMaxConnLifetimeMillis() { + return maxConnDuration.toMillis(); + } + + /** + * Gets the maximum number of connections that can remain idle in the pool. Excess idle connections are destroyed + * on return to the pool. + *

    + * A negative value indicates that there is no limit + *

    + * + * @return the maximum number of idle connections + */ + @Override + public synchronized int getMaxIdle() { + return this.maxIdle; + } + + /** + * Gets the value of the {@code maxOpenPreparedStatements} property. + * + * @return the maximum number of open statements + */ + @Override + public synchronized int getMaxOpenPreparedStatements() { + return this.maxOpenPreparedStatements; + } + + /** + * Gets the maximum number of active connections that can be allocated at the same time. + *

    + * A negative number means that there is no limit. + *

    + * + * @return the maximum number of active connections + */ + @Override + public synchronized int getMaxTotal() { + return this.maxTotal; + } + + /** + * Gets the maximum Duration that the pool will wait for a connection to be returned before throwing an exception. A + * value less than or equal to zero means the pool is set to wait indefinitely. + * + * @return the maxWaitDuration property value. + * @since 2.10.0 + */ + public synchronized Duration getMaxWaitDuration() { + return this.maxWaitDuration; + } + + /** + * Gets the maximum number of milliseconds that the pool will wait for a connection to be returned before + * throwing an exception. A value less than or equal to zero means the pool is set to wait indefinitely. + * + * @return the maxWaitMillis property value. + * @deprecated Use {@link #getMaxWaitDuration()}. + */ + @Deprecated + @Override + public synchronized long getMaxWaitMillis() { + return this.maxWaitDuration.toMillis(); + } + + /** + * Gets the {code minEvictableIdleDuration} property. + * + * @return the value of the {code minEvictableIdleDuration} property + * @see #setMinEvictableIdle(Duration) + * @since 2.10.0 + */ + public synchronized Duration getMinEvictableIdleDuration() { + return this.minEvictableIdleDuration; + } + + /** + * Gets the {code minEvictableIdleDuration} property. + * + * @return the value of the {code minEvictableIdleDuration} property + * @see #setMinEvictableIdle(Duration) + * @deprecated Use {@link #getMinEvictableIdleDuration()}. + */ + @Deprecated + @Override + public synchronized long getMinEvictableIdleTimeMillis() { + return this.minEvictableIdleDuration.toMillis(); + } + + /** + * Gets the minimum number of idle connections in the pool. The pool attempts to ensure that minIdle connections + * are available when the idle object evictor runs. The value of this property has no effect unless + * {code durationBetweenEvictionRuns} has a positive value. + * + * @return the minimum number of idle connections + * @see GenericObjectPool#getMinIdle() + */ + @Override + public synchronized int getMinIdle() { + return this.minIdle; + } + + /** + * [Read Only] The current number of active connections that have been allocated from this data source. + * + * @return the current number of active connections + */ + @Override + public int getNumActive() { + // Copy reference to avoid NPE if close happens after null check + final GenericObjectPool pool = connectionPool; + return pool == null ? 0 : pool.getNumActive(); + } + + /** + * [Read Only] The current number of idle connections that are waiting to be allocated from this data source. + * + * @return the current number of idle connections + */ + @Override + public int getNumIdle() { + // Copy reference to avoid NPE if close happens after null check + final GenericObjectPool pool = connectionPool; + return pool == null ? 0 : pool.getNumIdle(); + } + + /** + * Gets the value of the {code numTestsPerEvictionRun} property. + * + * @return the number of objects to examine during idle object evictor runs + * @see #setNumTestsPerEvictionRun(int) + */ + @Override + public synchronized int getNumTestsPerEvictionRun() { + return this.numTestsPerEvictionRun; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * Gets the password passed to the JDBC driver to establish connections. + * + * @return the connection password + */ + @Override + public String getPassword() { + return this.password; + } + + /** + * Gets the registered JMX ObjectName. + * + * @return the registered JMX ObjectName. + */ + protected ObjectName getRegisteredJmxName() { + return ObjectNameWrapper.unwrap(registeredJmxObjectName); + } + + /** + * Flag to remove abandoned connections if they exceed the removeAbandonedTimeout when borrowObject is invoked. + *

    + * The default value is false. + *

    + *

    + * If set to true a connection is considered abandoned and eligible for removal if it has not been used for more + * than {@link #getRemoveAbandonedTimeoutDuration() removeAbandonedTimeout} seconds. + *

    + *

    + * Abandoned connections are identified and removed when {@link #getConnection()} is invoked and all of the + * following conditions hold: + *

    + *
      + *
    • {@link #getRemoveAbandonedOnBorrow()}
    • + *
    • {@link #getNumActive()} > {@link #getMaxTotal()} - 3
    • + *
    • {@link #getNumIdle()} < 2
    • + *
    + * + * @see #getRemoveAbandonedTimeoutDuration() + */ + @Override + public boolean getRemoveAbandonedOnBorrow() { + return abandonedConfig != null && abandonedConfig.getRemoveAbandonedOnBorrow(); + } + + /** + * Flag to remove abandoned connections if they exceed the removeAbandonedTimeout during pool maintenance. + *

    + * The default value is false. + *

    + *

    + * If set to true a connection is considered abandoned and eligible for removal if it has not been used for more + * than {@link #getRemoveAbandonedTimeoutDuration() removeAbandonedTimeout} seconds. + *

    + * + * @see #getRemoveAbandonedTimeoutDuration() + */ + @Override + public boolean getRemoveAbandonedOnMaintenance() { + return abandonedConfig != null && abandonedConfig.getRemoveAbandonedOnMaintenance(); + } + + /** + * Gets the timeout in seconds before an abandoned connection can be removed. + *

    + * Creating a Statement, PreparedStatement or CallableStatement or using one of these to execute a query (using one + * of the execute methods) resets the lastUsed property of the parent connection. + *

    + *

    + * Abandoned connection cleanup happens when: + *

    + *
      + *
    • {@link #getRemoveAbandonedOnBorrow()} or {@link #getRemoveAbandonedOnMaintenance()} = true
    • + *
    • {@link #getNumIdle() numIdle} < 2
    • + *
    • {@link #getNumActive() numActive} > {@link #getMaxTotal() maxTotal} - 3
    • + *
    + *

    + * The default value is 300 seconds. + *

    + * @deprecated Use {@link #getRemoveAbandonedTimeoutDuration()}. + */ + @Deprecated + @Override + public int getRemoveAbandonedTimeout() { + return (int) getRemoveAbandonedTimeoutDuration().getSeconds(); + } + + /** + * Gets the timeout before an abandoned connection can be removed. + *

    + * Creating a Statement, PreparedStatement or CallableStatement or using one of these to execute a query (using one + * of the execute methods) resets the lastUsed property of the parent connection. + *

    + *

    + * Abandoned connection cleanup happens when: + *

    + *
      + *
    • {@link #getRemoveAbandonedOnBorrow()} or {@link #getRemoveAbandonedOnMaintenance()} = true
    • + *
    • {@link #getNumIdle() numIdle} < 2
    • + *
    • {@link #getNumActive() numActive} > {@link #getMaxTotal() maxTotal} - 3
    • + *
    + *

    + * The default value is 300 seconds. + *

    + * @return Timeout before an abandoned connection can be removed. + * @since 2.10.0 + */ + public Duration getRemoveAbandonedTimeoutDuration() { + return abandonedConfig == null ? Duration.ofSeconds(300) : abandonedConfig.getRemoveAbandonedTimeoutDuration(); + } + + /** + * Gets the current value of the flag that controls whether a connection will be rolled back when it is returned to + * the pool if auto commit is not enabled and the connection is not read only. + * + * @return whether a connection will be rolled back when it is returned to the pool. + */ + public boolean getRollbackOnReturn() { + return rollbackOnReturn; + } + + /** + * Gets the minimum amount of time a connection may sit idle in the pool before it is eligible for eviction by + * the idle object evictor, with the extra condition that at least "minIdle" connections remain in the pool. + *

    + * When {@link #getMinEvictableIdleTimeMillis() minEvictableIdleTimeMillis} is set to a positive value, + * minEvictableIdleTimeMillis is examined first by the idle connection evictor - i.e. when idle connections are + * visited by the evictor, idle time is first compared against {@code minEvictableIdleTimeMillis} (without + * considering the number of idle connections in the pool) and then against {@code softMinEvictableIdleTimeMillis}, + * including the {@code minIdle}, constraint. + *

    + * + * @return minimum amount of time a connection may sit idle in the pool before it is eligible for eviction, assuming + * there are minIdle idle connections in the pool + * @since 2.10.0 + */ + public synchronized Duration getSoftMinEvictableIdleDuration() { + return softMinEvictableIdleDuration; + } + + /** + * Gets the minimum amount of time a connection may sit idle in the pool before it is eligible for eviction by + * the idle object evictor, with the extra condition that at least "minIdle" connections remain in the pool. + *

    + * When {@link #getMinEvictableIdleTimeMillis() minEvictableIdleTimeMillis} is set to a positive value, + * minEvictableIdleTimeMillis is examined first by the idle connection evictor - i.e. when idle connections are + * visited by the evictor, idle time is first compared against {@code minEvictableIdleTimeMillis} (without + * considering the number of idle connections in the pool) and then against {@code softMinEvictableIdleTimeMillis}, + * including the {@code minIdle}, constraint. + *

    + * + * @return minimum amount of time a connection may sit idle in the pool before it is eligible for eviction, assuming + * there are minIdle idle connections in the pool + * @deprecated Use {@link #getSoftMinEvictableIdleDuration()}. + */ + @Deprecated + @Override + public synchronized long getSoftMinEvictableIdleTimeMillis() { + return softMinEvictableIdleDuration.toMillis(); + } + + /** + * Gets the {code testOnBorrow} property. + * + * @return true if objects are validated before being borrowed from the pool + * + * @see #setTestOnBorrow(boolean) + */ + @Override + public synchronized boolean getTestOnBorrow() { + return this.testOnBorrow; + } + + /** + * Gets the {code testOnCreate} property. + * + * @return true if objects are validated immediately after they are created by the pool + * @see #setTestOnCreate(boolean) + */ + @Override + public synchronized boolean getTestOnCreate() { + return this.testOnCreate; + } + + /** + * Gets the value of the {code testOnReturn} property. + * + * @return true if objects are validated before being returned to the pool + * @see #setTestOnReturn(boolean) + */ + public synchronized boolean getTestOnReturn() { + return this.testOnReturn; + } + + /** + * Gets the value of the {code testWhileIdle} property. + * + * @return true if objects examined by the idle object evictor are validated + * @see #setTestWhileIdle(boolean) + */ + @Override + public synchronized boolean getTestWhileIdle() { + return this.testWhileIdle; + } + + /** + * Gets the value of the {code durationBetweenEvictionRuns} property. + * + * @return the time (in milliseconds) between evictor runs + * @see #setDurationBetweenEvictionRuns(Duration) + * @since 2.10.0 + */ + public synchronized Duration getDurationBetweenEvictionRuns() { + return this.durationBetweenEvictionRuns; + } + + /** + * Gets the value of the {code durationBetweenEvictionRuns} property. + * + * @return the time (in milliseconds) between evictor runs + * @see #setDurationBetweenEvictionRuns(Duration) + * @deprecated Use {@link #getDurationBetweenEvictionRuns()}. + */ + @Deprecated + @Override + public synchronized long getTimeBetweenEvictionRunsMillis() { + return this.durationBetweenEvictionRuns.toMillis(); + } + + /** + * Gets the JDBC connection {code connectionString} property. + * + * @return the {code connectionString} passed to the JDBC driver to establish connections + */ + @Override + public synchronized String getUrl() { + return this.connectionString; + } + + /** + * Gets the JDBC connection {code userName} property. + * + * @return the {code userName} passed to the JDBC driver to establish connections + */ + @Override + public String getUsername() { + return this.userName; + } + + /** + * Gets the validation query used to validate connections before returning them. + * + * @return the SQL validation query + * @see #setValidationQuery(String) + */ + @Override + public String getValidationQuery() { + return this.validationQuery; + } + + /** + * Gets the validation query timeout. + * + * @return the timeout in seconds before connection validation queries fail. + */ + public Duration getValidationQueryTimeoutDuration() { + return validationQueryTimeoutDuration; + } + + /** + * Gets the validation query timeout. + * + * @return the timeout in seconds before connection validation queries fail. + * @deprecated Use {@link #getValidationQueryTimeoutDuration()}. + */ + @Deprecated + @Override + public int getValidationQueryTimeout() { + return (int) validationQueryTimeoutDuration.getSeconds(); + } + + /** + * Manually invalidates a connection, effectively requesting the pool to try to close it, remove it from the pool + * and reclaim pool capacity. + * + * @param connection The Connection to invalidate. + * + * @throws IllegalStateException if invalidating the connection failed. + * @since 2.1 + */ + public void invalidateConnection(final Connection connection) throws IllegalStateException { + if (connection == null) { + return; + } + if (connectionPool == null) { + throw new IllegalStateException("Cannot invalidate connection: ConnectionPool is null."); + } + + final PoolableConnection poolableConnection; + try { + poolableConnection = connection.unwrap(PoolableConnection.class); + if (poolableConnection == null) { + throw new IllegalStateException( + "Cannot invalidate connection: Connection is not a poolable connection."); + } + } catch (final SQLException e) { + throw new IllegalStateException("Cannot invalidate connection: Unwrapping poolable connection failed.", e); + } + + try { + connectionPool.invalidateObject(poolableConnection); + } catch (final Exception e) { + throw new IllegalStateException("Invalidating connection threw unexpected exception", e); + } + } + + /** + * Gets the value of the accessToUnderlyingConnectionAllowed property. + * + * @return true if access to the underlying connection is allowed, false otherwise. + */ + @Override + public synchronized boolean isAccessToUnderlyingConnectionAllowed() { + return this.accessToUnderlyingConnectionAllowed; + } + + /** + * Returns true if the statement pool is cleared when the connection is returned to its pool. + * + * @return true if the statement pool is cleared at connection return + * @since 2.8.0 + */ + @Override + public boolean isClearStatementPoolOnReturn() { + return clearStatementPoolOnReturn; + } + + /** + * If true, this data source is closed and no more connections can be retrieved from this data source. + * + * @return true, if the data source is closed; false otherwise + */ + @Override + public synchronized boolean isClosed() { + return closed; + } + + /** + * Delegates in a null-safe manner to {@link String#isEmpty()}. + * + * @param value the string to test, may be null. + * @return boolean false if value is null, otherwise {@link String#isEmpty()}. + */ + private boolean isEmpty(final String value) { + return value == null || value.trim().isEmpty(); + } + + /** + * Returns true if we are pooling statements. + * + * @return true if prepared and callable statements are pooled + */ + @Override + public synchronized boolean isPoolPreparedStatements() { + return this.poolPreparedStatements; + } + + @Override + public boolean isWrapperFor(final Class iface) throws SQLException { + return iface != null && iface.isInstance(this); + } + + private void jmxRegister() { + // Return immediately if this DataSource has already been registered + if (registeredJmxObjectName != null) { + return; + } + // Return immediately if no JMX name has been specified + final String requestedName = getJmxName(); + if (requestedName == null) { + return; + } + registeredJmxObjectName = registerJmxObjectName(requestedName, null); + try { + final StandardMBean standardMBean = new StandardMBean(this, DataSourceMXBean.class); + registeredJmxObjectName.registerMBean(standardMBean); + } catch (final NotCompliantMBeanException e) { + log.warn("The requested JMX name [" + requestedName + "] was not valid and will be ignored."); + } + } + + /** + * Logs the given message. + * + * @param message the message to log. + */ + protected void log(final String message) { + if (logWriter != null) { + logWriter.println(message); + } + } + + /** + * Logs the given throwable. + * @param message TODO + * @param throwable the throwable. + * + * @since 2.7.0 + */ + protected void log(final String message, final Throwable throwable) { + if (logWriter != null) { + logWriter.println(message); + throwable.printStackTrace(logWriter); + } + } + + @Override + public void postDeregister() { + // NO-OP + } + + @Override + public void postRegister(final Boolean registrationDone) { + // NO-OP + } + + @Override + public void preDeregister() throws Exception { + // NO-OP + } + + @Override + public ObjectName preRegister(final MBeanServer server, final ObjectName objectName) { + registeredJmxObjectName = registerJmxObjectName(getJmxName(), objectName); + return ObjectNameWrapper.unwrap(registeredJmxObjectName); + } + + private ObjectNameWrapper registerJmxObjectName(final String requestedName, final ObjectName objectName) { + ObjectNameWrapper objectNameWrapper = null; + if (requestedName != null) { + try { + objectNameWrapper = ObjectNameWrapper.wrap(requestedName); + } catch (final MalformedObjectNameException e) { + log.warn("The requested JMX name '" + requestedName + "' was not valid and will be ignored."); + } + } + if (objectNameWrapper == null) { + objectNameWrapper = ObjectNameWrapper.wrap(objectName); + } + return objectNameWrapper; + } + + /** + * Removes a custom connection property. + * + * @param name Name of the custom connection property to remove + * @see #addConnectionProperty(String, String) + */ + public void removeConnectionProperty(final String name) { + connectionProperties.remove(name); + } + + /** + * Restarts the datasource. + *

    + * This method calls {@link #close()} and {@link #start()} in sequence within synchronized scope so any + * connection requests that come in while the datasource is shutting down will be served by the new pool. + *

    + * Idle connections that are stored in the connection pool when this method is invoked are closed, but + * connections that are checked out to clients when this method is invoked are not affected. When client + * applications subsequently invoke {@link Connection#close()} to return these connections to the pool, the + * underlying JDBC connections are closed. These connections do not count in {@link #getMaxTotal()} or + * {@link #getNumActive()} after invoking this method. For example, if there are 3 connections checked out by + * clients when {@link #restart()} is invoked, after this method is called, {@link #getNumActive()} will + * return 0 and up to {@link #getMaxTotal()} + 3 connections may be open until the connections sourced from + * the original pool are returned. + *

    + * The new connection pool created by this method is initialized with currently set configuration properties. + * + * @throws SQLException if an error occurs initializing the datasource + */ + @Override + public synchronized void restart() throws SQLException { + close(); + start(); + } + + private void setAbandoned(final BiConsumer consumer, final T object) { + if (abandonedConfig == null) { + abandonedConfig = new AbandonedConfig(); + } + consumer.accept(abandonedConfig, object); + final GenericObjectPool gop = this.connectionPool; + if (gop != null) { + gop.setAbandonedConfig(abandonedConfig); + } + } + + private void setConnectionPool(final BiConsumer, T> consumer, final T object) { + if (connectionPool != null) { + consumer.accept(connectionPool, object); + } + } + + /** + * Sets the print writer to be used by this configuration to log information on abandoned objects. + * + * @param logWriter The new log writer + */ + public void setAbandonedLogWriter(final PrintWriter logWriter) { + setAbandoned(AbandonedConfig::setLogWriter, logWriter); + } + + /** + * If the connection pool implements {@link org.apache.tomcat.dbcp.pool2.UsageTracking UsageTracking}, configure whether + * the connection pool should record a stack trace every time a method is called on a pooled connection and retain + * the most recent stack trace to aid debugging of abandoned connections. + * + * @param usageTracking A value of {@code true} will enable the recording of a stack trace on every use of a + * pooled connection + */ + public void setAbandonedUsageTracking(final boolean usageTracking) { + setAbandoned(AbandonedConfig::setUseUsageTracking, Boolean.valueOf(usageTracking)); + } + + /** + * Sets the value of the accessToUnderlyingConnectionAllowed property. It controls if the PoolGuard allows access to + * the underlying connection. (Default: false) + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param allow Access to the underlying connection is granted when true. + */ + public synchronized void setAccessToUnderlyingConnectionAllowed(final boolean allow) { + this.accessToUnderlyingConnectionAllowed = allow; + } + + /** + * Sets the value of the flag that controls whether or not connections being returned to the pool will be checked + * and configured with {@link Connection#setAutoCommit(boolean) Connection.setAutoCommit(true)} if the auto commit + * setting is {@code false} when the connection is returned. It is {@code true} by default. + * + * @param autoCommitOnReturn Whether or not connections being returned to the pool will be checked and configured + * with auto-commit. + * @since 2.6.0 + */ + public void setAutoCommitOnReturn(final boolean autoCommitOnReturn) { + this.autoCommitOnReturn = autoCommitOnReturn; + } + + /** + * Sets the state caching flag. + * + * @param cacheState The new value for the state caching flag + */ + public void setCacheState(final boolean cacheState) { + this.cacheState = cacheState; + } + + /** + * Sets whether the pool of statements (which was enabled with {@link #setPoolPreparedStatements(boolean)}) should + * be cleared when the connection is returned to its pool. Default is false. + * + * @param clearStatementPoolOnReturn clear or not + * @since 2.8.0 + */ + public void setClearStatementPoolOnReturn(final boolean clearStatementPoolOnReturn) { + this.clearStatementPoolOnReturn = clearStatementPoolOnReturn; + } + + /** + * Sets the ConnectionFactory class name. + * + * @param connectionFactoryClassName A class name. + * @since 2.7.0 + */ + public void setConnectionFactoryClassName(final String connectionFactoryClassName) { + this.connectionFactoryClassName = isEmpty(connectionFactoryClassName) ? null : connectionFactoryClassName; + } + + /** + * Sets the list of SQL statements to be executed when a physical connection is first created. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param connectionInitSqls Collection of SQL statements to execute on connection creation + */ + public void setConnectionInitSqls(final Collection connectionInitSqls) { +// if (connectionInitSqls != null && !connectionInitSqls.isEmpty()) { +// ArrayList newVal = null; +// for (final String s : connectionInitSqls) { +// if (!isEmpty(s)) { +// if (newVal == null) { +// newVal = new ArrayList<>(); +// } +// newVal.add(s); +// } +// } +// this.connectionInitSqls = newVal; +// } else { +// this.connectionInitSqls = null; +// } + final List collect = Utils.isEmpty(connectionInitSqls) ? null + : connectionInitSqls.stream().filter(s -> !isEmpty(s)).collect(Collectors.toList()); + this.connectionInitSqls = Utils.isEmpty(collect) ? null : collect; + } + + /** + * Sets the connection properties passed to driver.connect(...). + *

    + * Format of the string must be [propertyName=property;]* + *

    + *

    + * NOTE - The "user" and "password" properties will be added explicitly, so they do not need to be included here. + *

    + * + * @param connectionProperties the connection properties used to create new connections + */ + public void setConnectionProperties(final String connectionProperties) { + Objects.requireNonNull(connectionProperties, "connectionProperties"); + final String[] entries = connectionProperties.split(";"); + final Properties properties = new Properties(); + Stream.of(entries).filter(e -> !e.isEmpty()).forEach(entry -> { + final int index = entry.indexOf('='); + if (index > 0) { + final String name = entry.substring(0, index); + final String value = entry.substring(index + 1); + properties.setProperty(name, value); + } else { + // no value is empty string which is how + // java.util.Properties works + properties.setProperty(entry, ""); + } + }); + this.connectionProperties = properties; + } + + /** + * Sets default auto-commit state of connections returned by this datasource. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param defaultAutoCommit default auto-commit value + */ + public void setDefaultAutoCommit(final Boolean defaultAutoCommit) { + this.defaultAutoCommit = defaultAutoCommit; + } + + /** + * Sets the default catalog. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param defaultCatalog the default catalog + */ + public void setDefaultCatalog(final String defaultCatalog) { + this.defaultCatalog = isEmpty(defaultCatalog) ? null : defaultCatalog; + } + + /** + * Sets the default query timeout that will be used for {@link java.sql.Statement Statement}s created from this + * connection. {@code null} means that the driver default will be used. + * + * @param defaultQueryTimeoutDuration The default query timeout Duration. + * @since 2.10.0 + */ + public void setDefaultQueryTimeout(final Duration defaultQueryTimeoutDuration) { + this.defaultQueryTimeoutDuration = defaultQueryTimeoutDuration; + } + + /** + * Sets the default query timeout that will be used for {@link java.sql.Statement Statement}s created from this + * connection. {@code null} means that the driver default will be used. + * + * @param defaultQueryTimeoutSeconds The default query timeout in seconds. + * @deprecated Use {@link #setDefaultQueryTimeout(Duration)}. + */ + @Deprecated + public void setDefaultQueryTimeout(final Integer defaultQueryTimeoutSeconds) { + this.defaultQueryTimeoutDuration = defaultQueryTimeoutSeconds == null ? null : Duration.ofSeconds(defaultQueryTimeoutSeconds.longValue()); + } + + /** + * Sets defaultReadonly property. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param defaultReadOnly default read-only value + */ + public void setDefaultReadOnly(final Boolean defaultReadOnly) { + this.defaultReadOnly = defaultReadOnly; + } + + /** + * Sets the default schema. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param defaultSchema the default catalog + * @since 2.5.0 + */ + public void setDefaultSchema(final String defaultSchema) { + this.defaultSchema = isEmpty(defaultSchema) ? null : defaultSchema; + } + + /** + * Sets the default transaction isolation state for returned connections. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param defaultTransactionIsolation the default transaction isolation state + * @see Connection#getTransactionIsolation + */ + public void setDefaultTransactionIsolation(final int defaultTransactionIsolation) { + this.defaultTransactionIsolation = defaultTransactionIsolation; + } + + /** + * Sets the SQL_STATE codes considered to signal fatal conditions. + *

    + * Overrides the defaults in {@link Utils#getDisconnectionSqlCodes()} (plus anything starting with + * {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}). If this property is non-null and {@link #getFastFailValidation()} + * is {@code true}, whenever connections created by this datasource generate exceptions with SQL_STATE codes in this + * list, they will be marked as "fatally disconnected" and subsequent validations will fail fast (no attempt at + * isValid or validation query). + *

    + *

    + * If {@link #getFastFailValidation()} is {@code false} setting this property has no effect. + *

    + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: {@code getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter}. + *

    + * + * @param disconnectionSqlCodes SQL_STATE codes considered to signal fatal conditions + * @since 2.1 + */ + public void setDisconnectionSqlCodes(final Collection disconnectionSqlCodes) { +// if (disconnectionSqlCodes != null && !disconnectionSqlCodes.isEmpty()) { +// HashSet newVal = null; +// for (final String s : disconnectionSqlCodes) { +// if (!isEmpty(s)) { +// if (newVal == null) { +// newVal = new HashSet<>(); +// } +// newVal.add(s); +// } +// } +// this.disconnectionSqlCodes = newVal; +// } else { +// this.disconnectionSqlCodes = null; +// } + final Set collect = Utils.isEmpty(disconnectionSqlCodes) ? null + : disconnectionSqlCodes.stream().filter(s -> !isEmpty(s)).collect(Collectors.toSet()); + this.disconnectionSqlCodes = Utils.isEmpty(collect) ? null : collect; + } + + /** + * Sets the JDBC Driver instance to use for this pool. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param driver The JDBC Driver instance to use for this pool. + */ + public synchronized void setDriver(final Driver driver) { + this.driver = driver; + } + + /** + * Sets the class loader to be used to load the JDBC driver. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param driverClassLoader the class loader with which to load the JDBC driver + */ + public synchronized void setDriverClassLoader(final ClassLoader driverClassLoader) { + this.driverClassLoader = driverClassLoader; + } + + /** + * Sets the JDBC driver class name. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param driverClassName the class name of the JDBC driver + */ + public synchronized void setDriverClassName(final String driverClassName) { + this.driverClassName = isEmpty(driverClassName) ? null : driverClassName; + } + + /** + * Sets the value of the flag that controls whether or not connections being returned to the pool will be checked + * and configured with {@link Connection#setAutoCommit(boolean) Connection.setAutoCommit(true)} if the auto commit + * setting is {@code false} when the connection is returned. It is {@code true} by default. + * + * @param autoCommitOnReturn Whether or not connections being returned to the pool will be checked and configured + * with auto-commit. + * @deprecated Use {@link #setAutoCommitOnReturn(boolean)}. + */ + @Deprecated + public void setEnableAutoCommitOnReturn(final boolean autoCommitOnReturn) { + this.autoCommitOnReturn = autoCommitOnReturn; + } + + /** + * Sets the EvictionPolicy implementation to use with this connection pool. + * + * @param evictionPolicyClassName The fully qualified class name of the EvictionPolicy implementation + */ + public synchronized void setEvictionPolicyClassName(final String evictionPolicyClassName) { + setConnectionPool(GenericObjectPool::setEvictionPolicyClassName, evictionPolicyClassName); + this.evictionPolicyClassName = evictionPolicyClassName; + } + + /** + * @see #getFastFailValidation() + * @param fastFailValidation true means connections created by this factory will fast fail validation + * @since 2.1 + */ + public void setFastFailValidation(final boolean fastFailValidation) { + this.fastFailValidation = fastFailValidation; + } + + /** + * Sets the initial size of the connection pool. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param initialSize the number of connections created when the pool is initialized + */ + public synchronized void setInitialSize(final int initialSize) { + this.initialSize = initialSize; + } + + /** + * Sets the JMX name that has been requested for this DataSource. If the requested name is not valid, an alternative + * may be chosen. This DataSource will attempt to register itself using this name. If another component registers + * this DataSource with JMX and this name is valid this name will be used in preference to any specified by the + * other component. + * + * @param jmxName The JMX name that has been requested for this DataSource + */ + public void setJmxName(final String jmxName) { + this.jmxName = jmxName; + } + + /** + * Sets if connection level JMX tracking is requested for this DataSource. If true, each connection will be + * registered for tracking with JMX. + * + * @param registerConnectionMBean connection tracking requested for this DataSource. + */ + public void setRegisterConnectionMBean(final boolean registerConnectionMBean) { + this.registerConnectionMBean = registerConnectionMBean; + } + + /** + * Sets the LIFO property. True means the pool behaves as a LIFO queue; false means FIFO. + * + * @param lifo the new value for the LIFO property + */ + public synchronized void setLifo(final boolean lifo) { + this.lifo = lifo; + setConnectionPool(GenericObjectPool::setLifo, Boolean.valueOf(lifo)); + } + + /** + * @param logAbandoned new logAbandoned property value + */ + public void setLogAbandoned(final boolean logAbandoned) { + setAbandoned(AbandonedConfig::setLogAbandoned, Boolean.valueOf(logAbandoned)); + } + + /** + * When {@link #getMaxConnDuration()} is set to limit connection lifetime, this property determines whether or + * not log messages are generated when the pool closes connections due to maximum lifetime exceeded. Set this + * property to false to suppress log messages when connections expire. + * + * @param logExpiredConnections Whether or not log messages are generated when the pool closes connections due to + * maximum lifetime exceeded. + */ + public void setLogExpiredConnections(final boolean logExpiredConnections) { + this.logExpiredConnections = logExpiredConnections; + } + + /** + * BasicDataSource does NOT support this method. + * + *

    + * Set the login timeout (in seconds) for connecting to the database. + *

    + *

    + * Calls {@link #createDataSource()}, so has the side effect of initializing the connection pool. + *

    + * + * @param loginTimeout The new login timeout, or zero for no timeout + * @throws UnsupportedOperationException If the DataSource implementation does not support the login timeout + * feature. + * @throws SQLException if a database access error occurs + */ + @Override + public void setLoginTimeout(final int loginTimeout) throws SQLException { + // This method isn't supported by the PoolingDataSource returned by the + // createDataSource + throw new UnsupportedOperationException("Not supported by BasicDataSource"); + } + + /** + * Sets the log writer being used by this data source. + *

    + * Calls {@link #createDataSource()}, so has the side effect of initializing the connection pool. + *

    + * + * @param logWriter The new log writer + * @throws SQLException if a database access error occurs + */ + @Override + public void setLogWriter(final PrintWriter logWriter) throws SQLException { + createDataSource().setLogWriter(logWriter); + this.logWriter = logWriter; + } + + /** + * Sets the maximum permitted lifetime of a connection. A value of zero or less indicates an + * infinite lifetime. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param maxConnDuration The maximum permitted lifetime of a connection. + * @since 2.10.0 + */ + public void setMaxConn(final Duration maxConnDuration) { + this.maxConnDuration = maxConnDuration; + } + + /** + * Sets the maximum permitted lifetime of a connection in milliseconds. A value of zero or less indicates an + * infinite lifetime. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param maxConnLifetimeMillis The maximum permitted lifetime of a connection in milliseconds. + * @deprecated Use {@link #setMaxConn(Duration)}. + */ + @Deprecated + public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) { + this.maxConnDuration = Duration.ofMillis(maxConnLifetimeMillis); + } + + /** + * Sets the maximum number of connections that can remain idle in the pool. Excess idle connections are destroyed on + * return to the pool. + * + * @see #getMaxIdle() + * @param maxIdle the new value for maxIdle + */ + public synchronized void setMaxIdle(final int maxIdle) { + this.maxIdle = maxIdle; + setConnectionPool(GenericObjectPool::setMaxIdle, Integer.valueOf(maxIdle)); + } + + /** + * Sets the value of the {@code maxOpenPreparedStatements} property. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param maxOpenStatements the new maximum number of prepared statements + */ + public synchronized void setMaxOpenPreparedStatements(final int maxOpenStatements) { + this.maxOpenPreparedStatements = maxOpenStatements; + } + + /** + * Sets the maximum total number of idle and borrows connections that can be active at the same time. Use a negative + * value for no limit. + * + * @param maxTotal the new value for maxTotal + * @see #getMaxTotal() + */ + public synchronized void setMaxTotal(final int maxTotal) { + this.maxTotal = maxTotal; + setConnectionPool(GenericObjectPool::setMaxTotal, Integer.valueOf(maxTotal)); + } + + /** + * Sets the MaxWaitMillis property. Use -1 to make the pool wait indefinitely. + * + * @param maxWaitDuration the new value for MaxWaitMillis + * @see #getMaxWaitDuration() + * @since 2.10.0 + */ + public synchronized void setMaxWait(final Duration maxWaitDuration) { + this.maxWaitDuration = maxWaitDuration; + setConnectionPool(GenericObjectPool::setMaxWait, maxWaitDuration); + } + + /** + * Sets the MaxWaitMillis property. Use -1 to make the pool wait indefinitely. + * + * @param maxWaitMillis the new value for MaxWaitMillis + * @see #getMaxWaitDuration() + * @deprecated {@link #setMaxWait(Duration)}. + */ + @Deprecated + public synchronized void setMaxWaitMillis(final long maxWaitMillis) { + setMaxWait(Duration.ofMillis(maxWaitMillis)); + } + + /** + * Sets the {code minEvictableIdleDuration} property. + * + * @param minEvictableIdleDuration the minimum amount of time an object may sit idle in the pool + * @see #setMinEvictableIdle(Duration) + * @since 2.10.0 + */ + public synchronized void setMinEvictableIdle(final Duration minEvictableIdleDuration) { + this.minEvictableIdleDuration = minEvictableIdleDuration; + setConnectionPool(GenericObjectPool::setMinEvictableIdleDuration, minEvictableIdleDuration); + } + + /** + * Sets the {code minEvictableIdleDuration} property. + * + * @param minEvictableIdleTimeMillis the minimum amount of time an object may sit idle in the pool + * @see #setMinEvictableIdle(Duration) + * @deprecated Use {@link #setMinEvictableIdle(Duration)}. + */ + @Deprecated + public synchronized void setMinEvictableIdleTimeMillis(final long minEvictableIdleTimeMillis) { + setMinEvictableIdle(Duration.ofMillis(minEvictableIdleTimeMillis)); + } + + /** + * Sets the minimum number of idle connections in the pool. The pool attempts to ensure that minIdle connections are + * available when the idle object evictor runs. The value of this property has no effect unless + * {code durationBetweenEvictionRuns} has a positive value. + * + * @param minIdle the new value for minIdle + * @see GenericObjectPool#setMinIdle(int) + */ + public synchronized void setMinIdle(final int minIdle) { + this.minIdle = minIdle; + setConnectionPool(GenericObjectPool::setMinIdle, Integer.valueOf(minIdle)); + } + + /** + * Sets the value of the {code numTestsPerEvictionRun} property. + * + * @param numTestsPerEvictionRun the new {code numTestsPerEvictionRun} value + * @see #setNumTestsPerEvictionRun(int) + */ + public synchronized void setNumTestsPerEvictionRun(final int numTestsPerEvictionRun) { + this.numTestsPerEvictionRun = numTestsPerEvictionRun; + setConnectionPool(GenericObjectPool::setNumTestsPerEvictionRun, Integer.valueOf(numTestsPerEvictionRun)); + } + + /** + * Sets the {code password}. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param password new value for the password + */ + public void setPassword(final String password) { + this.password = password; + } + + /** + * Sets whether to pool statements or not. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param poolingStatements pooling on or off + */ + public synchronized void setPoolPreparedStatements(final boolean poolingStatements) { + this.poolPreparedStatements = poolingStatements; + } + + /** + * @param removeAbandonedOnBorrow true means abandoned connections may be removed when connections are borrowed from + * the pool. + * @see #getRemoveAbandonedOnBorrow() + */ + public void setRemoveAbandonedOnBorrow(final boolean removeAbandonedOnBorrow) { + setAbandoned(AbandonedConfig::setRemoveAbandonedOnBorrow, Boolean.valueOf(removeAbandonedOnBorrow)); + } + + /** + * @param removeAbandonedOnMaintenance true means abandoned connections may be removed on pool maintenance. + * @see #getRemoveAbandonedOnMaintenance() + */ + public void setRemoveAbandonedOnMaintenance(final boolean removeAbandonedOnMaintenance) { + setAbandoned(AbandonedConfig::setRemoveAbandonedOnMaintenance, Boolean.valueOf(removeAbandonedOnMaintenance)); + } + + /** + * Sets the timeout before an abandoned connection can be removed. + *

    + * Setting this property has no effect if {@link #getRemoveAbandonedOnBorrow()} and + * {code getRemoveAbandonedOnMaintenance()} are false. + *

    + * + * @param removeAbandonedTimeout new abandoned timeout + * @see #getRemoveAbandonedTimeoutDuration() + * @see #getRemoveAbandonedOnBorrow() + * @see #getRemoveAbandonedOnMaintenance() + * @since 2.10.0 + */ + public void setRemoveAbandonedTimeout(final Duration removeAbandonedTimeout) { + setAbandoned(AbandonedConfig::setRemoveAbandonedTimeout, removeAbandonedTimeout); + } + + /** + * Sets the timeout in seconds before an abandoned connection can be removed. + *

    + * Setting this property has no effect if {@link #getRemoveAbandonedOnBorrow()} and + * {@link #getRemoveAbandonedOnMaintenance()} are false. + *

    + * + * @param removeAbandonedTimeout new abandoned timeout in seconds + * @see #getRemoveAbandonedTimeoutDuration() + * @see #getRemoveAbandonedOnBorrow() + * @see #getRemoveAbandonedOnMaintenance() + * @deprecated Use {@link #setRemoveAbandonedTimeout(Duration)}. + */ + @Deprecated + public void setRemoveAbandonedTimeout(final int removeAbandonedTimeout) { + setAbandoned(AbandonedConfig::setRemoveAbandonedTimeout, Duration.ofSeconds(removeAbandonedTimeout)); + } + + /** + * Sets the flag that controls if a connection will be rolled back when it is returned to the pool if auto commit is + * not enabled and the connection is not read only. + * + * @param rollbackOnReturn whether a connection will be rolled back when it is returned to the pool. + */ + public void setRollbackOnReturn(final boolean rollbackOnReturn) { + this.rollbackOnReturn = rollbackOnReturn; + } + + /** + * Sets the minimum amount of time a connection may sit idle in the pool before it is eligible for eviction by the + * idle object evictor, with the extra condition that at least "minIdle" connections remain in the pool. + * + * @param softMinEvictableIdleTimeMillis minimum amount of time a connection may sit idle in the pool before it is + * eligible for eviction, assuming there are minIdle idle connections in the + * pool. + * @see #getSoftMinEvictableIdleTimeMillis + * @since 2.10.0 + */ + public synchronized void setSoftMinEvictableIdle(final Duration softMinEvictableIdleTimeMillis) { + this.softMinEvictableIdleDuration = softMinEvictableIdleTimeMillis; + setConnectionPool(GenericObjectPool::setSoftMinEvictableIdleDuration, softMinEvictableIdleTimeMillis); + } + + /** + * Sets the minimum amount of time a connection may sit idle in the pool before it is eligible for eviction by the + * idle object evictor, with the extra condition that at least "minIdle" connections remain in the pool. + * + * @param softMinEvictableIdleTimeMillis minimum amount of time a connection may sit idle in the pool before it is + * eligible for eviction, assuming there are minIdle idle connections in the + * pool. + * @see #getSoftMinEvictableIdleTimeMillis + * @deprecated Use {@link #setSoftMinEvictableIdle(Duration)}. + */ + @Deprecated + public synchronized void setSoftMinEvictableIdleTimeMillis(final long softMinEvictableIdleTimeMillis) { + setSoftMinEvictableIdle(Duration.ofMillis(softMinEvictableIdleTimeMillis)); + } + + /** + * Sets the {code testOnBorrow} property. This property determines whether or not the pool will validate objects + * before they are borrowed from the pool. + * + * @param testOnBorrow new value for testOnBorrow property + */ + public synchronized void setTestOnBorrow(final boolean testOnBorrow) { + this.testOnBorrow = testOnBorrow; + setConnectionPool(GenericObjectPool::setTestOnBorrow, Boolean.valueOf(testOnBorrow)); + } + + /** + * Sets the {code testOnCreate} property. This property determines whether or not the pool will validate objects + * immediately after they are created by the pool + * + * @param testOnCreate new value for testOnCreate property + */ + public synchronized void setTestOnCreate(final boolean testOnCreate) { + this.testOnCreate = testOnCreate; + setConnectionPool(GenericObjectPool::setTestOnCreate, Boolean.valueOf(testOnCreate)); + } + + /** + * Sets the {@code testOnReturn} property. This property determines whether or not the pool will validate + * objects before they are returned to the pool. + * + * @param testOnReturn new value for testOnReturn property + */ + public synchronized void setTestOnReturn(final boolean testOnReturn) { + this.testOnReturn = testOnReturn; + setConnectionPool(GenericObjectPool::setTestOnReturn, Boolean.valueOf(testOnReturn)); + } + + /** + * Sets the {@code testWhileIdle} property. This property determines whether or not the idle object evictor + * will validate connections. + * + * @param testWhileIdle new value for testWhileIdle property + */ + public synchronized void setTestWhileIdle(final boolean testWhileIdle) { + this.testWhileIdle = testWhileIdle; + setConnectionPool(GenericObjectPool::setTestWhileIdle, Boolean.valueOf(testWhileIdle)); + } + + /** + * Sets the {code durationBetweenEvictionRuns} property. + * + * @param timeBetweenEvictionRunsMillis the new time between evictor runs + * @see #setDurationBetweenEvictionRuns(Duration) + * @since 2.10.0 + */ + public synchronized void setDurationBetweenEvictionRuns(final Duration timeBetweenEvictionRunsMillis) { + this.durationBetweenEvictionRuns = timeBetweenEvictionRunsMillis; + setConnectionPool(GenericObjectPool::setDurationBetweenEvictionRuns, timeBetweenEvictionRunsMillis); + } + + /** + * Sets the {code durationBetweenEvictionRuns} property. + * + * @param timeBetweenEvictionRunsMillis the new time between evictor runs + * @see #setDurationBetweenEvictionRuns(Duration) + * @deprecated Use {@link #setDurationBetweenEvictionRuns(Duration)}. + */ + @Deprecated + public synchronized void setTimeBetweenEvictionRunsMillis(final long timeBetweenEvictionRunsMillis) { + setDurationBetweenEvictionRuns(Duration.ofMillis(timeBetweenEvictionRunsMillis)); + } + + /** + * Sets the {code connection string}. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param connectionString the new value for the JDBC connection connectionString + */ + public synchronized void setUrl(final String connectionString) { + this.connectionString = connectionString; + } + + /** + * Sets the {code userName}. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param userName the new value for the JDBC connection user name + */ + public void setUsername(final String userName) { + this.userName = userName; + } + + /** + * Sets the {code validationQuery}. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param validationQuery the new value for the validation query + */ + public void setValidationQuery(final String validationQuery) { + this.validationQuery = isEmpty(validationQuery) ? null : validationQuery; + } + + /** + * Sets the validation query timeout, the amount of time, in seconds, that connection validation will wait for a + * response from the database when executing a validation query. Use a value less than or equal to 0 for no timeout. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param validationQueryTimeoutDuration new validation query timeout value in seconds + * @since 2.10.0 + */ + public void setValidationQueryTimeout(final Duration validationQueryTimeoutDuration) { + this.validationQueryTimeoutDuration = validationQueryTimeoutDuration; + } + + /** + * Sets the validation query timeout, the amount of time, in seconds, that connection validation will wait for a + * response from the database when executing a validation query. Use a value less than or equal to 0 for no timeout. + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param validationQueryTimeoutSeconds new validation query timeout value in seconds + * @deprecated Use {@link #setValidationQueryTimeout(Duration)}. + */ + @Deprecated + public void setValidationQueryTimeout(final int validationQueryTimeoutSeconds) { + this.validationQueryTimeoutDuration = Duration.ofSeconds(validationQueryTimeoutSeconds); + } + + /** + * Starts the datasource. + *

    + * It is not necessary to call this method before using a newly created BasicDataSource instance, but + * calling it in that context causes the datasource to be immediately initialized (instead of waiting for + * the first {@link #getConnection()} request). Its primary use is to restart and reinitialize a + * datasource that has been closed. + *

    + * When this method is called after {@link #close()}, connections checked out by clients + * before the datasource was stopped do not count in {@link #getMaxTotal()} or {@link #getNumActive()}. + * For example, if there are 3 connections checked out by clients when {@link #close()} is invoked and they are + * not returned before {@link #start()} is invoked, after this method is called, {@link #getNumActive()} will + * return 0. These connections will be physically closed when they are returned, but they will not count against + * the maximum allowed in the newly started datasource. + * + * @throws SQLException if an error occurs initializing the datasource + */ + @Override + public synchronized void start() throws SQLException { + closed = false; + createDataSource(); + } + + /** + * Starts the connection pool maintenance task, if configured. + */ + protected void startPoolMaintenance() { + if (connectionPool != null && durationBetweenEvictionRuns.compareTo(Duration.ZERO) > 0) { + connectionPool.setDurationBetweenEvictionRuns(durationBetweenEvictionRuns); + } + } + + @Override + public T unwrap(final Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return iface.cast(this); + } + throw new SQLException(this + " is not a wrapper for " + iface); + } + + private void updateJmxName(final GenericObjectPoolConfig config) { + if (registeredJmxObjectName == null) { + return; + } + final StringBuilder base = new StringBuilder(registeredJmxObjectName.toString()); + base.append(Constants.JMX_CONNECTION_POOL_BASE_EXT); + config.setJmxNameBase(base.toString()); + config.setJmxNamePrefix(Constants.JMX_CONNECTION_POOL_PREFIX); + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java new file mode 100644 index 0000000..daa7cf0 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java @@ -0,0 +1,453 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.dbcp.pool2.impl.BaseObjectPoolConfig; +import org.apache.tomcat.dbcp.pool2.impl.GenericObjectPoolConfig; + +/** + * JNDI object factory that creates an instance of {@code BasicDataSource} that has been configured based on the + * {@code RefAddr} values of the specified {@code Reference}, which must match the names and data types of the + * {@code BasicDataSource} bean properties with the following exceptions: + *

      + *
    • {@code connectionInitSqls} must be passed to this factory as a single String using semicolon to delimit the + * statements whereas {@code BasicDataSource} requires a collection of Strings.
    • + *
    + * + * @since 2.0 + */ +public class BasicDataSourceFactory implements ObjectFactory { + + private static final Log log = LogFactory.getLog(BasicDataSourceFactory.class); + + private static final String PROP_DEFAULT_AUTO_COMMIT = "defaultAutoCommit"; + private static final String PROP_DEFAULT_READ_ONLY = "defaultReadOnly"; + private static final String PROP_DEFAULT_TRANSACTION_ISOLATION = "defaultTransactionIsolation"; + private static final String PROP_DEFAULT_CATALOG = "defaultCatalog"; + private static final String PROP_DEFAULT_SCHEMA = "defaultSchema"; + private static final String PROP_CACHE_STATE = "cacheState"; + private static final String PROP_DRIVER_CLASS_NAME = "driverClassName"; + private static final String PROP_LIFO = "lifo"; + private static final String PROP_MAX_TOTAL = "maxTotal"; + private static final String PROP_MAX_IDLE = "maxIdle"; + private static final String PROP_MIN_IDLE = "minIdle"; + private static final String PROP_INITIAL_SIZE = "initialSize"; + private static final String PROP_MAX_WAIT_MILLIS = "maxWaitMillis"; + private static final String PROP_TEST_ON_CREATE = "testOnCreate"; + private static final String PROP_TEST_ON_BORROW = "testOnBorrow"; + private static final String PROP_TEST_ON_RETURN = "testOnReturn"; + private static final String PROP_TIME_BETWEEN_EVICTION_RUNS_MILLIS = "timeBetweenEvictionRunsMillis"; + private static final String PROP_NUM_TESTS_PER_EVICTION_RUN = "numTestsPerEvictionRun"; + private static final String PROP_MIN_EVICTABLE_IDLE_TIME_MILLIS = "minEvictableIdleTimeMillis"; + private static final String PROP_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS = "softMinEvictableIdleTimeMillis"; + private static final String PROP_EVICTION_POLICY_CLASS_NAME = "evictionPolicyClassName"; + private static final String PROP_TEST_WHILE_IDLE = "testWhileIdle"; + private static final String PROP_PASSWORD = Constants.KEY_PASSWORD; + private static final String PROP_URL = "url"; + private static final String PROP_USER_NAME = "username"; + private static final String PROP_VALIDATION_QUERY = "validationQuery"; + private static final String PROP_VALIDATION_QUERY_TIMEOUT = "validationQueryTimeout"; + private static final String PROP_JMX_NAME = "jmxName"; + private static final String PROP_REGISTER_CONNECTION_MBEAN = "registerConnectionMBean"; + private static final String PROP_CONNECTION_FACTORY_CLASS_NAME = "connectionFactoryClassName"; + + /** + * The property name for connectionInitSqls. The associated value String must be of the form [query;]* + */ + private static final String PROP_CONNECTION_INIT_SQLS = "connectionInitSqls"; + private static final String PROP_ACCESS_TO_UNDERLYING_CONNECTION_ALLOWED = "accessToUnderlyingConnectionAllowed"; + private static final String PROP_REMOVE_ABANDONED_ON_BORROW = "removeAbandonedOnBorrow"; + private static final String PROP_REMOVE_ABANDONED_ON_MAINTENANCE = "removeAbandonedOnMaintenance"; + private static final String PROP_REMOVE_ABANDONED_TIMEOUT = "removeAbandonedTimeout"; + private static final String PROP_LOG_ABANDONED = "logAbandoned"; + private static final String PROP_ABANDONED_USAGE_TRACKING = "abandonedUsageTracking"; + private static final String PROP_POOL_PREPARED_STATEMENTS = "poolPreparedStatements"; + private static final String PROP_CLEAR_STATEMENT_POOL_ON_RETURN = "clearStatementPoolOnReturn"; + private static final String PROP_MAX_OPEN_PREPARED_STATEMENTS = "maxOpenPreparedStatements"; + private static final String PROP_CONNECTION_PROPERTIES = "connectionProperties"; + private static final String PROP_MAX_CONN_LIFETIME_MILLIS = "maxConnLifetimeMillis"; + private static final String PROP_LOG_EXPIRED_CONNECTIONS = "logExpiredConnections"; + private static final String PROP_ROLLBACK_ON_RETURN = "rollbackOnReturn"; + private static final String PROP_ENABLE_AUTO_COMMIT_ON_RETURN = "enableAutoCommitOnReturn"; + private static final String PROP_DEFAULT_QUERY_TIMEOUT = "defaultQueryTimeout"; + private static final String PROP_FAST_FAIL_VALIDATION = "fastFailValidation"; + + /** + * Value string must be of the form [STATE_CODE,]* + */ + private static final String PROP_DISCONNECTION_SQL_CODES = "disconnectionSqlCodes"; + + /* + * Block with obsolete properties from DBCP 1.x. Warn users that these are ignored and they should use the 2.x + * properties. + */ + private static final String NUPROP_MAX_ACTIVE = "maxActive"; + private static final String NUPROP_REMOVE_ABANDONED = "removeAbandoned"; + private static final String NUPROP_MAXWAIT = "maxWait"; + + /* + * Block with properties expected in a DataSource This props will not be listed as ignored - we know that they may + * appear in Resource, and not listing them as ignored. + */ + private static final String SILENT_PROP_FACTORY = "factory"; + private static final String SILENT_PROP_SCOPE = "scope"; + private static final String SILENT_PROP_SINGLETON = "singleton"; + private static final String SILENT_PROP_AUTH = "auth"; + + private static final List ALL_PROPERTY_NAMES = Arrays.asList(PROP_DEFAULT_AUTO_COMMIT, PROP_DEFAULT_READ_ONLY, + PROP_DEFAULT_TRANSACTION_ISOLATION, PROP_DEFAULT_CATALOG, PROP_DEFAULT_SCHEMA, PROP_CACHE_STATE, + PROP_DRIVER_CLASS_NAME, PROP_LIFO, PROP_MAX_TOTAL, PROP_MAX_IDLE, PROP_MIN_IDLE, PROP_INITIAL_SIZE, + PROP_MAX_WAIT_MILLIS, PROP_TEST_ON_CREATE, PROP_TEST_ON_BORROW, PROP_TEST_ON_RETURN, + PROP_TIME_BETWEEN_EVICTION_RUNS_MILLIS, PROP_NUM_TESTS_PER_EVICTION_RUN, PROP_MIN_EVICTABLE_IDLE_TIME_MILLIS, + PROP_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS, PROP_EVICTION_POLICY_CLASS_NAME, PROP_TEST_WHILE_IDLE, PROP_PASSWORD, + PROP_URL, PROP_USER_NAME, PROP_VALIDATION_QUERY, PROP_VALIDATION_QUERY_TIMEOUT, PROP_CONNECTION_INIT_SQLS, + PROP_ACCESS_TO_UNDERLYING_CONNECTION_ALLOWED, PROP_REMOVE_ABANDONED_ON_BORROW, PROP_REMOVE_ABANDONED_ON_MAINTENANCE, + PROP_REMOVE_ABANDONED_TIMEOUT, PROP_LOG_ABANDONED, PROP_ABANDONED_USAGE_TRACKING, PROP_POOL_PREPARED_STATEMENTS, + PROP_CLEAR_STATEMENT_POOL_ON_RETURN, + PROP_MAX_OPEN_PREPARED_STATEMENTS, PROP_CONNECTION_PROPERTIES, PROP_MAX_CONN_LIFETIME_MILLIS, + PROP_LOG_EXPIRED_CONNECTIONS, PROP_ROLLBACK_ON_RETURN, PROP_ENABLE_AUTO_COMMIT_ON_RETURN, + PROP_DEFAULT_QUERY_TIMEOUT, PROP_FAST_FAIL_VALIDATION, PROP_DISCONNECTION_SQL_CODES, PROP_JMX_NAME, + PROP_REGISTER_CONNECTION_MBEAN, PROP_CONNECTION_FACTORY_CLASS_NAME); + + /** + * Obsolete properties from DBCP 1.x. with warning strings suggesting new properties. LinkedHashMap will guarantee + * that properties will be listed to output in order of insertion into map. + */ + private static final Map NUPROP_WARNTEXT = new LinkedHashMap<>(); + + static { + NUPROP_WARNTEXT.put(NUPROP_MAX_ACTIVE, + "Property " + NUPROP_MAX_ACTIVE + " is not used in DBCP2, use " + PROP_MAX_TOTAL + " instead. " + + PROP_MAX_TOTAL + " default value is " + GenericObjectPoolConfig.DEFAULT_MAX_TOTAL + "."); + NUPROP_WARNTEXT.put(NUPROP_REMOVE_ABANDONED, + "Property " + NUPROP_REMOVE_ABANDONED + " is not used in DBCP2," + " use one or both of " + + PROP_REMOVE_ABANDONED_ON_BORROW + " or " + PROP_REMOVE_ABANDONED_ON_MAINTENANCE + " instead. " + + "Both have default value set to false."); + NUPROP_WARNTEXT.put(NUPROP_MAXWAIT, + "Property " + NUPROP_MAXWAIT + " is not used in DBCP2" + " , use " + PROP_MAX_WAIT_MILLIS + " instead. " + + PROP_MAX_WAIT_MILLIS + " default value is " + BaseObjectPoolConfig.DEFAULT_MAX_WAIT + + "."); + } + + /** + * Silent Properties. These properties will not be listed as ignored - we know that they may appear in JDBC Resource + * references, and we will not list them as ignored. + */ + private static final List SILENT_PROPERTIES = new ArrayList<>(); + + static { + SILENT_PROPERTIES.add(SILENT_PROP_FACTORY); + SILENT_PROPERTIES.add(SILENT_PROP_SCOPE); + SILENT_PROPERTIES.add(SILENT_PROP_SINGLETON); + SILENT_PROPERTIES.add(SILENT_PROP_AUTH); + + } + + private static void accept(final Properties properties, final String name, final Function parser, final Consumer consumer) { + getOptional(properties, name).ifPresent(v -> consumer.accept(parser.apply(v))); + } + + private static void acceptBoolean(final Properties properties, final String name, final Consumer consumer) { + accept(properties, name, Boolean::parseBoolean, consumer); + } + + private static void acceptDurationOfMillis(final Properties properties, final String name, final Consumer consumer) { + accept(properties, name, s -> Duration.ofMillis(Long.parseLong(s)), consumer); + } + + private static void acceptDurationOfSeconds(final Properties properties, final String name, final Consumer consumer) { + accept(properties, name, s -> Duration.ofSeconds(Long.parseLong(s)), consumer); + } + + private static void acceptInt(final Properties properties, final String name, final Consumer consumer) { + accept(properties, name, Integer::parseInt, consumer); + } + + private static void acceptString(final Properties properties, final String name, final Consumer consumer) { + accept(properties, name, Function.identity(), consumer); + } + + /** + * Creates and configures a {@link BasicDataSource} instance based on the given properties. + * + * @param properties + * The data source configuration properties. + * @return A new a {@link BasicDataSource} instance based on the given properties. + * @throws SQLException + * Thrown when an error occurs creating the data source. + */ + public static BasicDataSource createDataSource(final Properties properties) throws SQLException { + final BasicDataSource dataSource = new BasicDataSource(); + acceptBoolean(properties, PROP_DEFAULT_AUTO_COMMIT, dataSource::setDefaultAutoCommit); + acceptBoolean(properties, PROP_DEFAULT_READ_ONLY, dataSource::setDefaultReadOnly); + + getOptional(properties, PROP_DEFAULT_TRANSACTION_ISOLATION).ifPresent(value -> { + value = value.toUpperCase(Locale.ROOT); + int level = PoolableConnectionFactory.UNKNOWN_TRANSACTION_ISOLATION; + if ("NONE".equals(value)) { + level = Connection.TRANSACTION_NONE; + } else if ("READ_COMMITTED".equals(value)) { + level = Connection.TRANSACTION_READ_COMMITTED; + } else if ("READ_UNCOMMITTED".equals(value)) { + level = Connection.TRANSACTION_READ_UNCOMMITTED; + } else if ("REPEATABLE_READ".equals(value)) { + level = Connection.TRANSACTION_REPEATABLE_READ; + } else if ("SERIALIZABLE".equals(value)) { + level = Connection.TRANSACTION_SERIALIZABLE; + } else { + try { + level = Integer.parseInt(value); + } catch (final NumberFormatException e) { + System.err.println("Could not parse defaultTransactionIsolation: " + value); + System.err.println("WARNING: defaultTransactionIsolation not set"); + System.err.println("using default value of database driver"); + level = PoolableConnectionFactory.UNKNOWN_TRANSACTION_ISOLATION; + } + } + dataSource.setDefaultTransactionIsolation(level); + }); + + acceptString(properties, PROP_DEFAULT_SCHEMA, dataSource::setDefaultSchema); + acceptString(properties, PROP_DEFAULT_CATALOG, dataSource::setDefaultCatalog); + acceptBoolean(properties, PROP_CACHE_STATE, dataSource::setCacheState); + acceptString(properties, PROP_DRIVER_CLASS_NAME, dataSource::setDriverClassName); + acceptBoolean(properties, PROP_LIFO, dataSource::setLifo); + acceptInt(properties, PROP_MAX_TOTAL, dataSource::setMaxTotal); + acceptInt(properties, PROP_MAX_IDLE, dataSource::setMaxIdle); + acceptInt(properties, PROP_MIN_IDLE, dataSource::setMinIdle); + acceptInt(properties, PROP_INITIAL_SIZE, dataSource::setInitialSize); + acceptDurationOfMillis(properties, PROP_MAX_WAIT_MILLIS, dataSource::setMaxWait); + acceptBoolean(properties, PROP_TEST_ON_CREATE, dataSource::setTestOnCreate); + acceptBoolean(properties, PROP_TEST_ON_BORROW, dataSource::setTestOnBorrow); + acceptBoolean(properties, PROP_TEST_ON_RETURN, dataSource::setTestOnReturn); + acceptDurationOfMillis(properties, PROP_TIME_BETWEEN_EVICTION_RUNS_MILLIS, dataSource::setDurationBetweenEvictionRuns); + acceptInt(properties, PROP_NUM_TESTS_PER_EVICTION_RUN, dataSource::setNumTestsPerEvictionRun); + acceptDurationOfMillis(properties, PROP_MIN_EVICTABLE_IDLE_TIME_MILLIS, dataSource::setMinEvictableIdle); + acceptDurationOfMillis(properties, PROP_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS, dataSource::setSoftMinEvictableIdle); + acceptString(properties, PROP_EVICTION_POLICY_CLASS_NAME, dataSource::setEvictionPolicyClassName); + acceptBoolean(properties, PROP_TEST_WHILE_IDLE, dataSource::setTestWhileIdle); + acceptString(properties, PROP_PASSWORD, dataSource::setPassword); + acceptString(properties, PROP_URL, dataSource::setUrl); + acceptString(properties, PROP_USER_NAME, dataSource::setUsername); + acceptString(properties, PROP_VALIDATION_QUERY, dataSource::setValidationQuery); + acceptDurationOfSeconds(properties, PROP_VALIDATION_QUERY_TIMEOUT, dataSource::setValidationQueryTimeout); + acceptBoolean(properties, PROP_ACCESS_TO_UNDERLYING_CONNECTION_ALLOWED, dataSource::setAccessToUnderlyingConnectionAllowed); + acceptBoolean(properties, PROP_REMOVE_ABANDONED_ON_BORROW, dataSource::setRemoveAbandonedOnBorrow); + acceptBoolean(properties, PROP_REMOVE_ABANDONED_ON_MAINTENANCE, dataSource::setRemoveAbandonedOnMaintenance); + acceptDurationOfSeconds(properties, PROP_REMOVE_ABANDONED_TIMEOUT, dataSource::setRemoveAbandonedTimeout); + acceptBoolean(properties, PROP_LOG_ABANDONED, dataSource::setLogAbandoned); + acceptBoolean(properties, PROP_ABANDONED_USAGE_TRACKING, dataSource::setAbandonedUsageTracking); + acceptBoolean(properties, PROP_POOL_PREPARED_STATEMENTS, dataSource::setPoolPreparedStatements); + acceptBoolean(properties, PROP_CLEAR_STATEMENT_POOL_ON_RETURN, dataSource::setClearStatementPoolOnReturn); + acceptInt(properties, PROP_MAX_OPEN_PREPARED_STATEMENTS, dataSource::setMaxOpenPreparedStatements); + getOptional(properties, PROP_CONNECTION_INIT_SQLS).ifPresent(v -> dataSource.setConnectionInitSqls(parseList(v, ';'))); + + final String value = properties.getProperty(PROP_CONNECTION_PROPERTIES); + if (value != null) { + for (final Object key : getProperties(value).keySet()) { + final String propertyName = Objects.toString(key, null); + dataSource.addConnectionProperty(propertyName, getProperties(value).getProperty(propertyName)); + } + } + + acceptDurationOfMillis(properties, PROP_MAX_CONN_LIFETIME_MILLIS, dataSource::setMaxConn); + acceptBoolean(properties, PROP_LOG_EXPIRED_CONNECTIONS, dataSource::setLogExpiredConnections); + acceptString(properties, PROP_JMX_NAME, dataSource::setJmxName); + acceptBoolean(properties, PROP_REGISTER_CONNECTION_MBEAN, dataSource::setRegisterConnectionMBean); + acceptBoolean(properties, PROP_ENABLE_AUTO_COMMIT_ON_RETURN, dataSource::setAutoCommitOnReturn); + acceptBoolean(properties, PROP_ROLLBACK_ON_RETURN, dataSource::setRollbackOnReturn); + acceptDurationOfSeconds(properties, PROP_DEFAULT_QUERY_TIMEOUT, dataSource::setDefaultQueryTimeout); + acceptBoolean(properties, PROP_FAST_FAIL_VALIDATION, dataSource::setFastFailValidation); + getOptional(properties, PROP_DISCONNECTION_SQL_CODES).ifPresent(v -> dataSource.setDisconnectionSqlCodes(parseList(v, ','))); + acceptString(properties, PROP_CONNECTION_FACTORY_CLASS_NAME, dataSource::setConnectionFactoryClassName); + + // DBCP-215 + // Trick to make sure that initialSize connections are created + if (dataSource.getInitialSize() > 0) { + dataSource.getLogWriter(); + } + + // Return the configured DataSource instance + return dataSource; + } + + private static Optional getOptional(final Properties properties, final String name) { + return Optional.ofNullable(properties.getProperty(name)); + } + + /** + * Parse properties from the string. Format of the string must be [propertyName=property;]* + * + * @param propText The source text + * @return Properties A new Properties instance + * @throws SQLException When a paring exception occurs + */ + private static Properties getProperties(final String propText) throws SQLException { + final Properties p = new Properties(); + if (propText != null) { + try { + p.load(new ByteArrayInputStream(propText.replace(';', '\n').getBytes(StandardCharsets.ISO_8859_1))); + } catch (IOException e) { + throw new SQLException(propText, e); + } + } + return p; + } + + /** + * Parse list of property values from a delimited string + * + * @param value + * delimited list of values + * @param delimiter + * character used to separate values in the list + * @return String Collection of values + */ + private static Collection parseList(final String value, final char delimiter) { + final StringTokenizer tokenizer = new StringTokenizer(value, Character.toString(delimiter)); + final Collection tokens = new ArrayList<>(tokenizer.countTokens()); + while (tokenizer.hasMoreTokens()) { + tokens.add(tokenizer.nextToken()); + } + return tokens; + } + + /** + * Creates and return a new {@code BasicDataSource} instance. If no instance can be created, return + * {@code null} instead. + * + * @param obj + * The possibly null object containing location or reference information that can be used in creating an + * object + * @param name + * The name of this object relative to {@code nameCtx} + * @param nameCtx + * The context relative to which the {@code name} parameter is specified, or {@code null} if + * {@code name} is relative to the default initial context + * @param environment + * The possibly null environment that is used in creating this object + * + * @throws SQLException + * if an exception occurs creating the instance + */ + @Override + public Object getObjectInstance(final Object obj, final Name name, final Context nameCtx, + final Hashtable environment) throws SQLException { + + // We only know how to deal with {@code javax.naming.Reference}s + // that specify a class name of "javax.sql.DataSource" + if (obj == null || !(obj instanceof Reference)) { + return null; + } + final Reference ref = (Reference) obj; + if (!"javax.sql.DataSource".equals(ref.getClassName())) { + return null; + } + + // Check property names and log warnings about obsolete and / or unknown properties + final List warnMessages = new ArrayList<>(); + final List infoMessages = new ArrayList<>(); + validatePropertyNames(ref, name, warnMessages, infoMessages); + warnMessages.forEach(log::warn); + infoMessages.forEach(log::info); + + final Properties properties = new Properties(); + ALL_PROPERTY_NAMES.forEach(propertyName -> { + final RefAddr ra = ref.get(propertyName); + if (ra != null) { + properties.setProperty(propertyName, Objects.toString(ra.getContent(), null)); + } + }); + + return createDataSource(properties); + } + + /** + * Collects warnings and info messages. Warnings are generated when an obsolete property is set. Unknown properties + * generate info messages. + * + * @param ref + * Reference to check properties of + * @param name + * Name provided to getObject + * @param warnMessages + * container for warning messages + * @param infoMessages + * container for info messages + */ + private void validatePropertyNames(final Reference ref, final Name name, final List warnMessages, + final List infoMessages) { + final String nameString = name != null ? "Name = " + name.toString() + " " : ""; + if (NUPROP_WARNTEXT != null && !NUPROP_WARNTEXT.isEmpty()) { + NUPROP_WARNTEXT.forEach((propertyName, value) -> { + final RefAddr ra = ref.get(propertyName); + if (ra != null && !ALL_PROPERTY_NAMES.contains(ra.getType())) { + final StringBuilder stringBuilder = new StringBuilder(nameString); + final String propertyValue = Objects.toString(ra.getContent(), null); + stringBuilder.append(value).append(" You have set value of \"").append(propertyValue).append("\" for \"").append(propertyName) + .append("\" property, which is being ignored."); + warnMessages.add(stringBuilder.toString()); + } + }); + } + + final Enumeration allRefAddrs = ref.getAll(); + while (allRefAddrs.hasMoreElements()) { + final RefAddr ra = allRefAddrs.nextElement(); + final String propertyName = ra.getType(); + // If property name is not in the properties list, we haven't warned on it + // and it is not in the "silent" list, tell user we are ignoring it. + if (!(ALL_PROPERTY_NAMES.contains(propertyName) || NUPROP_WARNTEXT.containsKey(propertyName) || SILENT_PROPERTIES.contains(propertyName))) { + final String propertyValue = Objects.toString(ra.getContent(), null); + final StringBuilder stringBuilder = new StringBuilder(nameString); + stringBuilder.append("Ignoring unknown property: ").append("value of \"").append(propertyValue).append("\" for \"").append(propertyName) + .append("\" property"); + infoMessages.add(stringBuilder.toString()); + } + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java b/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java new file mode 100644 index 0000000..06798a3 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java @@ -0,0 +1,38 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +/** + * Interface to keep API compatibility. Methods listed here are not made available to + * JMX. + *

    + * As of 2.9.0, this interface extends {@link DataSourceMXBean}. + *

    + * + * @since 2.0 + */ +public interface BasicDataSourceMXBean extends DataSourceMXBean { + + /** + * See {@link BasicDataSource#getPassword()} + * + * @return {@link BasicDataSource#getPassword()} + * @deprecated exposing password via JMX is an Information Exposure issue. + */ + @Deprecated + String getPassword(); +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java new file mode 100644 index 0000000..d01c3e4 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Abstract factory interface for creating {@link java.sql.Connection}s. + * + * @since 2.0 + */ +public interface ConnectionFactory { + /** + * Create a new {@link java.sql.Connection} in an implementation specific fashion. + * + * @return a new {@link java.sql.Connection} + * @throws SQLException + * if a database error occurs creating the connection + */ + Connection createConnection() throws SQLException; +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java new file mode 100644 index 0000000..01d5894 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java @@ -0,0 +1,76 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Driver; +import java.sql.SQLException; +import java.util.Properties; + +/* + * Creates {@link ConnectionFactory} instances. + * + * @since 2.7.0 + */ +final class ConnectionFactoryFactory { + + /** + * Creates a new {@link DriverConnectionFactory} allowing for an override through + * {@link BasicDataSource#getDriverClassName()}. + * + * @param basicDataSource Configures creation. + * @param driver The JDBC driver. + * @return a new {@link DriverConnectionFactory} allowing for a {@link BasicDataSource#getDriverClassName()} + * override. + * @throws SQLException Thrown when instantiation fails. + */ + static ConnectionFactory createConnectionFactory(final BasicDataSource basicDataSource, final Driver driver) + throws SQLException { + final Properties connectionProperties = basicDataSource.getConnectionProperties(); + final String url = basicDataSource.getUrl(); + // Set up the driver connection factory we will use + final String user = basicDataSource.getUsername(); + if (user != null) { + connectionProperties.put(Constants.KEY_USER, user); + } else { + basicDataSource.log(String.format("DBCP DataSource configured without a '%s'", Constants.KEY_USER)); + } + + final String pwd = basicDataSource.getPassword(); + if (pwd != null) { + connectionProperties.put(Constants.KEY_PASSWORD, pwd); + } else { + basicDataSource.log(String.format("DBCP DataSource configured without a '%s'", Constants.KEY_PASSWORD)); + } + final String connectionFactoryClassName = basicDataSource.getConnectionFactoryClassName(); + if (connectionFactoryClassName != null) { + try { + final Class connectionFactoryFromCCL = Class.forName(connectionFactoryClassName); + return (ConnectionFactory) connectionFactoryFromCCL + .getConstructor(Driver.class, String.class, Properties.class) + .newInstance(driver, url, connectionProperties); + } catch (final Exception t) { + final String message = "Cannot load ConnectionFactory implementation '" + connectionFactoryClassName + + "'"; + basicDataSource.log(message, t); + throw new SQLException(message, t); + } + } + // Defaults to DriverConnectionFactory + return new DriverConnectionFactory(driver, url, connectionProperties); + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/Constants.java b/java/org/apache/tomcat/dbcp/dbcp2/Constants.java new file mode 100644 index 0000000..da83187 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/Constants.java @@ -0,0 +1,48 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +/** + * Constants. + * + * @since 2.0 + */ +public class Constants { + + public static final String JMX_CONNECTION_POOL_BASE_EXT = ",connectionpool="; + public static final String JMX_CONNECTION_POOL_PREFIX = "connections"; + + public static final String JMX_CONNECTION_BASE_EXT = JMX_CONNECTION_POOL_BASE_EXT + JMX_CONNECTION_POOL_PREFIX + + ",connection="; + + public static final String JMX_STATEMENT_POOL_BASE_EXT = JMX_CONNECTION_BASE_EXT; + public static final String JMX_STATEMENT_POOL_PREFIX = ",statementpool=statements"; + + /** + * JDBC properties and URL key for passwords. + * + * @since 2.9.0 + */ + public static final String KEY_PASSWORD = "password"; + + /** + * JDBC properties and URL key for users. + * + * @since 2.9.0 + */ + public static final String KEY_USER = "user"; +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java new file mode 100644 index 0000000..334bfbf --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java @@ -0,0 +1,111 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +/** + * A {@link DataSource}-based implementation of {@link ConnectionFactory}. + * + * @since 2.0 + */ +public class DataSourceConnectionFactory implements ConnectionFactory { + + private final DataSource dataSource; + + private final String userName; + + private final char[] userPassword; + + /** + * Constructs an instance for the given DataSource. + * + * @param dataSource + * The DataSource for this factory. + */ + public DataSourceConnectionFactory(final DataSource dataSource) { + this(dataSource, null, (char[]) null); + } + + /** + * Constructs an instance for the given DataSource. + * + * @param dataSource + * The DataSource for this factory. + * @param userName + * The user name. + * @param userPassword + * The user password. + * @since 2.4.0 + */ + public DataSourceConnectionFactory(final DataSource dataSource, final String userName, final char[] userPassword) { + this.dataSource = dataSource; + this.userName = userName; + this.userPassword = Utils.clone(userPassword); + } + + /** + * Constructs an instance for the given DataSource. + * + * @param dataSource + * The DataSource for this factory. + * @param userName + * The user name. + * @param password + * The user password. + */ + public DataSourceConnectionFactory(final DataSource dataSource, final String userName, final String password) { + this.dataSource = dataSource; + this.userName = userName; + this.userPassword = Utils.toCharArray(password); + } + + @Override + public Connection createConnection() throws SQLException { + if (null == userName && null == userPassword) { + return dataSource.getConnection(); + } + return dataSource.getConnection(userName, Utils.toString(userPassword)); + } + + /** + * @return The data source. + * @since 2.6.0 + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * @return The user name. + * @since 2.6.0 + */ + public String getUserName() { + return userName; + } + + /** + * @return The user password. + * @since 2.6.0 + */ + public char[] getUserPassword() { + return Utils.clone(userPassword); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java b/java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java new file mode 100644 index 0000000..f6dbef1 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java @@ -0,0 +1,353 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.SQLException; + +/** + * Defines the methods that will be made available via + * JMX. + * + * @since 2.9.0 + */ +public interface DataSourceMXBean { + + /** + * See {@link BasicDataSource#getAbandonedUsageTracking()}. + * + * @return {@link BasicDataSource#getAbandonedUsageTracking()} + */ + boolean getAbandonedUsageTracking(); + + /** + * See {@link BasicDataSource#getCacheState()}. + * + * @return {@link BasicDataSource#getCacheState()}. + */ + boolean getCacheState(); + + /** + * See {@link BasicDataSource#getConnectionInitSqlsAsArray()}. + * + * @return {@link BasicDataSource#getConnectionInitSqlsAsArray()}. + */ + String[] getConnectionInitSqlsAsArray(); + + /** + * See {@link BasicDataSource#getDefaultAutoCommit()}. + * + * @return {@link BasicDataSource#getDefaultAutoCommit()}. + */ + Boolean getDefaultAutoCommit(); + + /** + * See {@link BasicDataSource#getDefaultCatalog()}. + * + * @return {@link BasicDataSource#getDefaultCatalog()}. + */ + String getDefaultCatalog(); + + /** + * See {@link BasicDataSource#getDefaultReadOnly()}. + * + * @return {@link BasicDataSource#getDefaultReadOnly()}. + */ + Boolean getDefaultReadOnly(); + + /** + * See {@link BasicDataSource#getDefaultSchema()}. + * + * @return {@link BasicDataSource#getDefaultSchema()}. + * @since 2.5.0 + */ + default String getDefaultSchema() { + return null; + } + + /** + * See {@link BasicDataSource#getDefaultTransactionIsolation()}. + * + * @return {@link BasicDataSource#getDefaultTransactionIsolation()}. + */ + int getDefaultTransactionIsolation(); + + /** + * See {@link BasicDataSource#getDisconnectionSqlCodesAsArray()}. + * + * @return {@link BasicDataSource#getDisconnectionSqlCodesAsArray()}. + * @since 2.1 + */ + String[] getDisconnectionSqlCodesAsArray(); + + /** + * See {@link BasicDataSource#getDriverClassName()}. + * + * @return {@link BasicDataSource#getDriverClassName()}. + */ + String getDriverClassName(); + + /** + * See {@link BasicDataSource#getFastFailValidation()}. + * + * @return {@link BasicDataSource#getFastFailValidation()}. + * @since 2.1 + */ + boolean getFastFailValidation(); + + /** + * See {@link BasicDataSource#getInitialSize()}. + * + * @return {@link BasicDataSource#getInitialSize()}. + */ + int getInitialSize(); + + /** + * See {@link BasicDataSource#getLifo()}. + * + * @return {@link BasicDataSource#getLifo()}. + */ + boolean getLifo(); + + /** + * See {@link BasicDataSource#getLogAbandoned()}. + * + * @return {@link BasicDataSource#getLogAbandoned()}. + */ + boolean getLogAbandoned(); + + /** + * See {@link BasicDataSource#getLogExpiredConnections()}. + * + * @return {@link BasicDataSource#getLogExpiredConnections()}. + * @since 2.1 + */ + boolean getLogExpiredConnections(); + + /** + * See {@link BasicDataSource#getMaxConnLifetimeMillis()}. + * + * @return {@link BasicDataSource#getMaxConnLifetimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMaxConnLifetimeMillis(); + + /** + * See {@link BasicDataSource#getMaxIdle()}. + * + * @return {@link BasicDataSource#getMaxIdle()}. + */ + int getMaxIdle(); + + /** + * See {@link BasicDataSource#getMaxOpenPreparedStatements()}. + * + * @return {@link BasicDataSource#getMaxOpenPreparedStatements()}. + */ + int getMaxOpenPreparedStatements(); + + /** + * See {@link BasicDataSource#getMaxTotal()}. + * + * @return {@link BasicDataSource#getMaxTotal()}. + */ + int getMaxTotal(); + + /** + * See {@link BasicDataSource#getMaxWaitMillis()}. + * + * @return {@link BasicDataSource#getMaxWaitMillis()}. + */ + @SuppressWarnings("javadoc") + long getMaxWaitMillis(); + + /** + * See {@link BasicDataSource#getMinEvictableIdleTimeMillis()}. + * + * @return {@link BasicDataSource#getMinEvictableIdleTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMinEvictableIdleTimeMillis(); + + /** + * See {@link BasicDataSource#getMinIdle()}. + * + * @return {@link BasicDataSource#getMinIdle()}. + */ + int getMinIdle(); + + /** + * See {@link BasicDataSource#getNumActive()}. + * + * @return {@link BasicDataSource#getNumActive()}. + */ + int getNumActive(); + + /** + * See {@link BasicDataSource#getNumIdle()}. + * + * @return {@link BasicDataSource#getNumIdle()}. + */ + int getNumIdle(); + + /** + * See {@link BasicDataSource#getNumTestsPerEvictionRun()}. + * + * @return {@link BasicDataSource#getNumTestsPerEvictionRun()}. + */ + int getNumTestsPerEvictionRun(); + + /** + * See {@link BasicDataSource#getRemoveAbandonedOnBorrow()}. + * + * @return {@link BasicDataSource#getRemoveAbandonedOnBorrow()}. + */ + boolean getRemoveAbandonedOnBorrow(); + + /** + * See {@link BasicDataSource#getRemoveAbandonedOnMaintenance()}. + * + * @return {@link BasicDataSource#getRemoveAbandonedOnMaintenance()}. + */ + boolean getRemoveAbandonedOnMaintenance(); + + /** + * See {@link BasicDataSource#getRemoveAbandonedTimeout()}. + * + * @return {@link BasicDataSource#getRemoveAbandonedTimeout()}. + */ + @SuppressWarnings("javadoc") + int getRemoveAbandonedTimeout(); + + /** + * See {@link BasicDataSource#getSoftMinEvictableIdleTimeMillis()}. + * + * @return {@link BasicDataSource#getSoftMinEvictableIdleTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getSoftMinEvictableIdleTimeMillis(); + + /** + * See {@link BasicDataSource#getTestOnBorrow()}. + * + * @return {@link BasicDataSource#getTestOnBorrow()}. + */ + boolean getTestOnBorrow(); + + /** + * See {@link BasicDataSource#getTestOnCreate()}. + * + * @return {@link BasicDataSource#getTestOnCreate()}. + */ + boolean getTestOnCreate(); + + /** + * See {@link BasicDataSource#getTestWhileIdle()}. + * + * @return {@link BasicDataSource#getTestWhileIdle()}. + */ + boolean getTestWhileIdle(); + + /** + * See {@link BasicDataSource#getTimeBetweenEvictionRunsMillis()}. + * + * @return {@link BasicDataSource#getTimeBetweenEvictionRunsMillis()}. + */ + @SuppressWarnings("javadoc") + long getTimeBetweenEvictionRunsMillis(); + + /** + * See {@link BasicDataSource#getUrl()}. + * + * @return {@link BasicDataSource#getUrl()}. + */ + String getUrl(); + + /** + * See {@link BasicDataSource#getUsername()}. + * + * @return {@link BasicDataSource#getUsername()}. + */ + String getUsername(); + + /** + * See {@link BasicDataSource#getValidationQuery()}. + * + * @return {@link BasicDataSource#getValidationQuery()}. + */ + String getValidationQuery(); + + /** + * See {@link BasicDataSource#getValidationQueryTimeout()}. + * + * @return {@link BasicDataSource#getValidationQueryTimeout()}. + */ + @SuppressWarnings("javadoc") + int getValidationQueryTimeout(); + + /** + * See {@link BasicDataSource#isAccessToUnderlyingConnectionAllowed()}. + * + * @return {@link BasicDataSource#isAccessToUnderlyingConnectionAllowed()}. + */ + boolean isAccessToUnderlyingConnectionAllowed(); + + /** + * See {@link BasicDataSource#isClearStatementPoolOnReturn()}. + * + * @return {@link BasicDataSource#isClearStatementPoolOnReturn()}. + * @since 2.8.0 + */ + default boolean isClearStatementPoolOnReturn() { + return false; + } + + /** + * See {@link BasicDataSource#isClosed()}. + * + * @return {@link BasicDataSource#isClosed()}. + */ + boolean isClosed(); + + /** + * See {@link BasicDataSource#isPoolPreparedStatements()}. + * + * @return {@link BasicDataSource#isPoolPreparedStatements()}. + */ + boolean isPoolPreparedStatements(); + + /** + * See {@link BasicDataSource#restart()} + * + * @throws SQLException if an error occurs initializing the data source. + * + * @since 2.8.0 + */ + default void restart() throws SQLException { + // do nothing by default? + } + + /** + * See {@link BasicDataSource#start()} + * + * @throws SQLException if an error occurs initializing the data source. + * + * @since 2.8.0 + */ + default void start() throws SQLException { + // do nothing + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java new file mode 100644 index 0000000..1f365e2 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java @@ -0,0 +1,1382 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLType; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +/** + * A base delegating implementation of {@link CallableStatement}. + *

    + * All of the methods from the {@link CallableStatement} interface simply call the corresponding method on the + * "delegate" provided in my constructor. + *

    + *

    + * Extends AbandonedTrace to implement Statement tracking and logging of code which created the Statement. Tracking the + * Statement ensures that the Connection which created it can close any open Statement's on Connection close. + *

    + * + * @since 2.0 + */ +public class DelegatingCallableStatement extends DelegatingPreparedStatement implements CallableStatement { + + /** + * Creates a wrapper for the Statement which traces this Statement to the Connection which created it and the code + * which created it. + * + * @param connection + * the {@link DelegatingConnection} that created this statement + * @param statement + * the {@link CallableStatement} to delegate all calls to + */ + public DelegatingCallableStatement(final DelegatingConnection connection, final CallableStatement statement) { + super(connection, statement); + } + + @Override + public Array getArray(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getArray(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Array getArray(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getArray(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public BigDecimal getBigDecimal(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBigDecimal(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * @deprecated Use {@link #getBigDecimal(int)} or {@link #getBigDecimal(String)} + */ + @Override + @Deprecated + public BigDecimal getBigDecimal(final int parameterIndex, final int scale) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBigDecimal(parameterIndex, scale); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public BigDecimal getBigDecimal(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBigDecimal(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Blob getBlob(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBlob(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Blob getBlob(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBlob(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public boolean getBoolean(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBoolean(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean getBoolean(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBoolean(parameterName); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public byte getByte(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getByte(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public byte getByte(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getByte(parameterName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public byte[] getBytes(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBytes(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public byte[] getBytes(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getBytes(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Reader getCharacterStream(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getCharacterStream(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Reader getCharacterStream(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getCharacterStream(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Clob getClob(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getClob(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Clob getClob(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getClob(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Date getDate(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getDate(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Date getDate(final int parameterIndex, final Calendar cal) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getDate(parameterIndex, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Date getDate(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getDate(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Date getDate(final String parameterName, final Calendar cal) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getDate(parameterName, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + private CallableStatement getDelegateCallableStatement() { + return (CallableStatement) getDelegate(); + } + + @Override + public double getDouble(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getDouble(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public double getDouble(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getDouble(parameterName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public float getFloat(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getFloat(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public float getFloat(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getFloat(parameterName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getInt(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getInt(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getInt(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getInt(parameterName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public long getLong(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getLong(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public long getLong(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getLong(parameterName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public Reader getNCharacterStream(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getNCharacterStream(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Reader getNCharacterStream(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getNCharacterStream(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public NClob getNClob(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getNClob(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public NClob getNClob(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getNClob(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public String getNString(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getNString(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public String getNString(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getNString(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Object getObject(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getObject(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public T getObject(final int parameterIndex, final Class type) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getObject(parameterIndex, type); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Object getObject(final int i, final Map> map) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getObject(i, map); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Object getObject(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getObject(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public T getObject(final String parameterName, final Class type) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getObject(parameterName, type); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Object getObject(final String parameterName, final Map> map) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getObject(parameterName, map); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Ref getRef(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getRef(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Ref getRef(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getRef(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public RowId getRowId(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getRowId(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public RowId getRowId(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getRowId(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public short getShort(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getShort(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public short getShort(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getShort(parameterName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public SQLXML getSQLXML(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getSQLXML(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public SQLXML getSQLXML(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getSQLXML(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public String getString(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getString(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public String getString(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getString(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Time getTime(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getTime(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Time getTime(final int parameterIndex, final Calendar cal) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getTime(parameterIndex, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Time getTime(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getTime(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Time getTime(final String parameterName, final Calendar cal) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getTime(parameterName, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Timestamp getTimestamp(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getTimestamp(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Timestamp getTimestamp(final int parameterIndex, final Calendar cal) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getTimestamp(parameterIndex, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Timestamp getTimestamp(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getTimestamp(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Timestamp getTimestamp(final String parameterName, final Calendar cal) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getTimestamp(parameterName, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public URL getURL(final int parameterIndex) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getURL(parameterIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public URL getURL(final String parameterName) throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().getURL(parameterName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public void registerOutParameter(final int parameterIndex, final int sqlType) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterIndex, sqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void registerOutParameter(final int parameterIndex, final int sqlType, final int scale) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterIndex, sqlType, scale); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void registerOutParameter(final int paramIndex, final int sqlType, final String typeName) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(paramIndex, sqlType, typeName); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void registerOutParameter(final int parameterIndex, final SQLType sqlType) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterIndex, sqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void registerOutParameter(final int parameterIndex, final SQLType sqlType, final int scale) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterIndex, sqlType, scale); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void registerOutParameter(final int parameterIndex, final SQLType sqlType, final String typeName) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterIndex, sqlType, typeName); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void registerOutParameter(final String parameterName, final int sqlType) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterName, sqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void registerOutParameter(final String parameterName, final int sqlType, final int scale) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterName, sqlType, scale); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void registerOutParameter(final String parameterName, final int sqlType, final String typeName) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterName, sqlType, typeName); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void registerOutParameter(final String parameterName, final SQLType sqlType) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterName, sqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void registerOutParameter(final String parameterName, final SQLType sqlType, final int scale) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterName, sqlType, scale); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void registerOutParameter(final String parameterName, final SQLType sqlType, final String typeName) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().registerOutParameter(parameterName, sqlType, typeName); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setAsciiStream(final String parameterName, final InputStream inputStream) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setAsciiStream(parameterName, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setAsciiStream(final String parameterName, final InputStream x, final int length) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setAsciiStream(parameterName, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setAsciiStream(final String parameterName, final InputStream inputStream, final long length) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setAsciiStream(parameterName, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBigDecimal(final String parameterName, final BigDecimal x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBigDecimal(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBinaryStream(final String parameterName, final InputStream inputStream) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBinaryStream(parameterName, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBinaryStream(final String parameterName, final InputStream x, final int length) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBinaryStream(parameterName, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBinaryStream(final String parameterName, final InputStream inputStream, final long length) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBinaryStream(parameterName, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBlob(final String parameterName, final Blob blob) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBlob(parameterName, blob); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBlob(final String parameterName, final InputStream inputStream) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBlob(parameterName, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBlob(final String parameterName, final InputStream inputStream, final long length) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBlob(parameterName, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBoolean(final String parameterName, final boolean x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBoolean(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setByte(final String parameterName, final byte x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setByte(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBytes(final String parameterName, final byte[] x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setBytes(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setCharacterStream(final String parameterName, final Reader reader) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setCharacterStream(parameterName, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setCharacterStream(final String parameterName, final Reader reader, final int length) + throws SQLException { + checkOpen(); + getDelegateCallableStatement().setCharacterStream(parameterName, reader, length); + } + + @Override + public void setCharacterStream(final String parameterName, final Reader reader, final long length) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setCharacterStream(parameterName, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setClob(final String parameterName, final Clob clob) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setClob(parameterName, clob); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setClob(final String parameterName, final Reader reader) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setClob(parameterName, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setClob(final String parameterName, final Reader reader, final long length) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setClob(parameterName, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setDate(final String parameterName, final Date x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setDate(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setDate(final String parameterName, final Date x, final Calendar cal) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setDate(parameterName, x, cal); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setDouble(final String parameterName, final double x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setDouble(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setFloat(final String parameterName, final float x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setFloat(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setInt(final String parameterName, final int x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setInt(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setLong(final String parameterName, final long x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setLong(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNCharacterStream(final String parameterName, final Reader reader) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setNCharacterStream(parameterName, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNCharacterStream(final String parameterName, final Reader reader, final long length) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setNCharacterStream(parameterName, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNClob(final String parameterName, final NClob value) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setNClob(parameterName, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNClob(final String parameterName, final Reader reader) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setNClob(parameterName, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNClob(final String parameterName, final Reader reader, final long length) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setNClob(parameterName, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNString(final String parameterName, final String value) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setNString(parameterName, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNull(final String parameterName, final int sqlType) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setNull(parameterName, sqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNull(final String parameterName, final int sqlType, final String typeName) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setNull(parameterName, sqlType, typeName); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setObject(final String parameterName, final Object x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setObject(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setObject(final String parameterName, final Object x, final int targetSqlType) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setObject(parameterName, x, targetSqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setObject(final String parameterName, final Object x, final int targetSqlType, final int scale) + throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setObject(parameterName, x, targetSqlType, scale); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void setObject(final String parameterName, final Object x, final SQLType targetSqlType) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setObject(parameterName, x, targetSqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void setObject(final String parameterName, final Object x, final SQLType targetSqlType, + final int scaleOrLength) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setObject(parameterName, x, targetSqlType, scaleOrLength); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setRowId(final String parameterName, final RowId value) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setRowId(parameterName, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setShort(final String parameterName, final short x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setShort(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setSQLXML(final String parameterName, final SQLXML value) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setSQLXML(parameterName, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setString(final String parameterName, final String x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setString(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTime(final String parameterName, final Time x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setTime(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTime(final String parameterName, final Time x, final Calendar cal) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setTime(parameterName, x, cal); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTimestamp(final String parameterName, final Timestamp x) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setTimestamp(parameterName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTimestamp(final String parameterName, final Timestamp x, final Calendar cal) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setTimestamp(parameterName, x, cal); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setURL(final String parameterName, final URL val) throws SQLException { + checkOpen(); + try { + getDelegateCallableStatement().setURL(parameterName, val); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public boolean wasNull() throws SQLException { + checkOpen(); + try { + return getDelegateCallableStatement().wasNull(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java new file mode 100644 index 0000000..a9e620d --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java @@ -0,0 +1,1032 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.ClientInfoStatus; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +import org.apache.tomcat.dbcp.dbcp2.managed.ManagedConnection; + +/** + * A base delegating implementation of {@link Connection}. + *

    + * All of the methods from the {@link Connection} interface simply check to see that the {@link Connection} is active, + * and call the corresponding method on the "delegate" provided in my constructor. + *

    + *

    + * Extends AbandonedTrace to implement Connection tracking and logging of code which created the Connection. Tracking + * the Connection ensures that the AbandonedObjectPool can close this connection and recycle it if its pool of + * connections is nearing exhaustion and this connection's last usage is older than the removeAbandonedTimeout. + *

    + * + * @param + * the Connection type + * + * @since 2.0 + */ +public class DelegatingConnection extends AbandonedTrace implements Connection { + + private static final Map EMPTY_FAILED_PROPERTIES = Collections + .emptyMap(); + + /** My delegate {@link Connection}. */ + private volatile C connection; + + private volatile boolean closed; + + private boolean cacheState = true; + private Boolean cachedAutoCommit; + private Boolean cachedReadOnly; + private String cachedCatalog; + private String cachedSchema; + private Duration defaultQueryTimeoutDuration; + + /** + * Creates a wrapper for the Connection which traces this Connection in the AbandonedObjectPool. + * + * @param connection the {@link Connection} to delegate all calls to, may be null (see {@link ManagedConnection}). + */ + public DelegatingConnection(final C connection) { + this.connection = connection; + } + + @Override + public void abort(final Executor executor) throws SQLException { + try { + Jdbc41Bridge.abort(connection, executor); + } catch (final SQLException e) { + handleException(e); + } + } + + protected void activate() { + closed = false; + setLastUsed(); + if (connection instanceof DelegatingConnection) { + ((DelegatingConnection) connection).activate(); + } + } + + protected void checkOpen() throws SQLException { + if (closed) { + if (null != connection) { + String label; + try { + label = connection.toString(); + } catch (final Exception e) { + // leave label empty + label = ""; + } + throw new SQLException("Connection " + label + " is closed."); + } + throw new SQLException("Connection is null."); + } + } + + /** + * Clears the cached state. Call when you known that the underlying connection may have been accessed + * directly. + */ + public void clearCachedState() { + cachedAutoCommit = null; + cachedReadOnly = null; + cachedSchema = null; + cachedCatalog = null; + if (connection instanceof DelegatingConnection) { + ((DelegatingConnection) connection).clearCachedState(); + } + } + + @Override + public void clearWarnings() throws SQLException { + checkOpen(); + try { + connection.clearWarnings(); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * Closes the underlying connection, and close any Statements that were not explicitly closed. Sub-classes that + * override this method must: + *
      + *
    1. Call {@link #passivate()}
    2. + *
    3. Call close (or the equivalent appropriate action) on the wrapped connection
    4. + *
    5. Set {@code closed} to {@code false}
    6. + *
    + */ + @Override + public void close() throws SQLException { + if (!closed) { + closeInternal(); + } + } + + protected final void closeInternal() throws SQLException { + try { + passivate(); + } finally { + if (connection != null) { + boolean connectionIsClosed; + try { + connectionIsClosed = connection.isClosed(); + } catch (final SQLException e) { + // not sure what the state is, so assume the connection is open. + connectionIsClosed = false; + } + try { + // DBCP-512: Avoid exceptions when closing a connection in multi-threaded use case. + // Avoid closing again, which should be a no-op, but some drivers like H2 throw an exception when + // closing from multiple threads. + if (!connectionIsClosed) { + connection.close(); + } + } finally { + closed = true; + } + } else { + closed = true; + } + } + } + + @Override + public void commit() throws SQLException { + checkOpen(); + try { + connection.commit(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public Array createArrayOf(final String typeName, final Object[] elements) throws SQLException { + checkOpen(); + try { + return connection.createArrayOf(typeName, elements); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Blob createBlob() throws SQLException { + checkOpen(); + try { + return connection.createBlob(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Clob createClob() throws SQLException { + checkOpen(); + try { + return connection.createClob(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public NClob createNClob() throws SQLException { + checkOpen(); + try { + return connection.createNClob(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public SQLXML createSQLXML() throws SQLException { + checkOpen(); + try { + return connection.createSQLXML(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Statement createStatement() throws SQLException { + checkOpen(); + try { + return init(new DelegatingStatement(this, connection.createStatement())); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Statement createStatement(final int resultSetType, final int resultSetConcurrency) throws SQLException { + checkOpen(); + try { + return init(new DelegatingStatement(this, connection.createStatement(resultSetType, resultSetConcurrency))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Statement createStatement(final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + checkOpen(); + try { + return init(new DelegatingStatement(this, + connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Struct createStruct(final String typeName, final Object[] attributes) throws SQLException { + checkOpen(); + try { + return connection.createStruct(typeName, attributes); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public boolean getAutoCommit() throws SQLException { + checkOpen(); + if (cacheState && cachedAutoCommit != null) { + return cachedAutoCommit.booleanValue(); + } + try { + cachedAutoCommit = Boolean.valueOf(connection.getAutoCommit()); + return cachedAutoCommit.booleanValue(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + /** + * Returns the state caching flag. + * + * @return the state caching flag + */ + public boolean getCacheState() { + return cacheState; + } + + @Override + public String getCatalog() throws SQLException { + checkOpen(); + if (cacheState && cachedCatalog != null) { + return cachedCatalog; + } + try { + cachedCatalog = connection.getCatalog(); + return cachedCatalog; + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Properties getClientInfo() throws SQLException { + checkOpen(); + try { + return connection.getClientInfo(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public String getClientInfo(final String name) throws SQLException { + checkOpen(); + try { + return connection.getClientInfo(name); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * Gets the default query timeout that will be used for {@link Statement}s created from this connection. + * {@code null} means that the driver default will be used. + * + * @return query timeout limit in seconds; zero means there is no limit. + * @deprecated Use {@link #getDefaultQueryTimeoutDuration()}. + */ + @Deprecated + public Integer getDefaultQueryTimeout() { + return defaultQueryTimeoutDuration == null ? null : Integer.valueOf((int) defaultQueryTimeoutDuration.getSeconds()); + } + + /** + * Gets the default query timeout that will be used for {@link Statement}s created from this connection. + * {@code null} means that the driver default will be used. + * + * @return query timeout limit; zero means there is no limit. + * @since 2.10.0 + */ + public Duration getDefaultQueryTimeoutDuration() { + return defaultQueryTimeoutDuration; + } + + /** + * Returns my underlying {@link Connection}. + * + * @return my underlying {@link Connection}. + */ + public C getDelegate() { + return getDelegateInternal(); + } + + /** + * Gets the delegate connection. + * + * @return the delegate connection. + */ + protected final C getDelegateInternal() { + return connection; + } + + @Override + public int getHoldability() throws SQLException { + checkOpen(); + try { + return connection.getHoldability(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * If my underlying {@link Connection} is not a {@code DelegatingConnection}, returns it, otherwise recursively + * invokes this method on my delegate. + *

    + * Hence this method will return the first delegate that is not a {@code DelegatingConnection}, or {@code null} when + * no non-{@code DelegatingConnection} delegate can be found by traversing this chain. + *

    + *

    + * This method is useful when you may have nested {@code DelegatingConnection}s, and you want to make sure to obtain + * a "genuine" {@link Connection}. + *

    + * + * @return innermost delegate. + */ + public Connection getInnermostDelegate() { + return getInnermostDelegateInternal(); + } + + /** + * Although this method is public, it is part of the internal API and should not be used by clients. The signature + * of this method may change at any time including in ways that break backwards compatibility. + * + * @return innermost delegate. + */ + public final Connection getInnermostDelegateInternal() { + Connection conn = connection; + while (conn instanceof DelegatingConnection) { + conn = ((DelegatingConnection) conn).getDelegateInternal(); + if (this == conn) { + return null; + } + } + return conn; + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + checkOpen(); + try { + return new DelegatingDatabaseMetaData(this, connection.getMetaData()); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public int getNetworkTimeout() throws SQLException { + checkOpen(); + try { + return Jdbc41Bridge.getNetworkTimeout(connection); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public String getSchema() throws SQLException { + checkOpen(); + if (cacheState && cachedSchema != null) { + return cachedSchema; + } + try { + cachedSchema = Jdbc41Bridge.getSchema(connection); + return cachedSchema; + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public int getTransactionIsolation() throws SQLException { + checkOpen(); + try { + return connection.getTransactionIsolation(); + } catch (final SQLException e) { + handleException(e); + return -1; + } + } + + @Override + public Map> getTypeMap() throws SQLException { + checkOpen(); + try { + return connection.getTypeMap(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public SQLWarning getWarnings() throws SQLException { + checkOpen(); + try { + return connection.getWarnings(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * Handles the given exception by throwing it. + * + * @param e the exception to throw. + * @throws SQLException the exception to throw. + */ + protected void handleException(final SQLException e) throws SQLException { + throw e; + } + + /** + * Handles the given {@code SQLException}. + * + * @param The throwable type. + * @param e The SQLException + * @return the given {@code SQLException} + * @since 2.7.0 + */ + protected T handleExceptionNoThrow(final T e) { + return e; + } + + /** + * Initializes the given statement with this connection's settings. + * + * @param The DelegatingStatement type. + * @param delegatingStatement The DelegatingStatement to initialize. + * @return The given DelegatingStatement. + * @throws SQLException if a database access error occurs, this method is called on a closed Statement. + */ + private T init(final T delegatingStatement) throws SQLException { + if (defaultQueryTimeoutDuration != null && defaultQueryTimeoutDuration.getSeconds() != delegatingStatement.getQueryTimeout()) { + delegatingStatement.setQueryTimeout((int) defaultQueryTimeoutDuration.getSeconds()); + } + return delegatingStatement; + } + + /** + * Compares innermost delegate to the given connection. + * + * @param c + * connection to compare innermost delegate with + * @return true if innermost delegate equals {@code c} + */ + public boolean innermostDelegateEquals(final Connection c) { + final Connection innerCon = getInnermostDelegateInternal(); + if (innerCon == null) { + return c == null; + } + return innerCon.equals(c); + } + + @Override + public boolean isClosed() throws SQLException { + return closed || connection == null || connection.isClosed(); + } + + protected boolean isClosedInternal() { + return closed; + } + + @Override + public boolean isReadOnly() throws SQLException { + checkOpen(); + if (cacheState && cachedReadOnly != null) { + return cachedReadOnly.booleanValue(); + } + try { + cachedReadOnly = Boolean.valueOf(connection.isReadOnly()); + return cachedReadOnly.booleanValue(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + /** + * Tests if the connection has not been closed and is still valid. + * + * @param timeout The duration to wait for the database operation used to validate the connection to complete. + * @return See {@link Connection#isValid(int)}. + * @throws SQLException See {@link Connection#isValid(int)}. + * @see Connection#isValid(int) + * @since 2.10.0 + */ + public boolean isValid(final Duration timeout) throws SQLException { + if (isClosed()) { + return false; + } + try { + return connection.isValid((int) timeout.getSeconds()); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + /** + * @deprecated Use {@link #isValid(Duration)}. + */ + @Override + @Deprecated + public boolean isValid(final int timeoutSeconds) throws SQLException { + return isValid(Duration.ofSeconds(timeoutSeconds)); + } + + @Override + public boolean isWrapperFor(final Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return true; + } + if (iface.isAssignableFrom(connection.getClass())) { + return true; + } + return connection.isWrapperFor(iface); + } + + @Override + public String nativeSQL(final String sql) throws SQLException { + checkOpen(); + try { + return connection.nativeSQL(sql); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + protected void passivate() throws SQLException { + // The JDBC specification requires that a Connection close any open + // Statement's when it is closed. + // DBCP-288. Not all the traced objects will be statements + final List traceList = getTrace(); + if (!Utils.isEmpty(traceList)) { + final List thrownList = new ArrayList<>(); + traceList.forEach(trace -> trace.close(thrownList::add)); + clearTrace(); + if (!thrownList.isEmpty()) { + throw new SQLExceptionList(thrownList); + } + } + setLastUsed(Instant.EPOCH); + } + + @Override + public CallableStatement prepareCall(final String sql) throws SQLException { + checkOpen(); + try { + return init(new DelegatingCallableStatement(this, connection.prepareCall(sql))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency) + throws SQLException { + checkOpen(); + try { + return init(new DelegatingCallableStatement(this, + connection.prepareCall(sql, resultSetType, resultSetConcurrency))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + checkOpen(); + try { + return init(new DelegatingCallableStatement(this, + connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql) throws SQLException { + checkOpen(); + try { + return init(new DelegatingPreparedStatement(this, connection.prepareStatement(sql))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) throws SQLException { + checkOpen(); + try { + return init(new DelegatingPreparedStatement(this, connection.prepareStatement(sql, autoGeneratedKeys))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency) + throws SQLException { + checkOpen(); + try { + return init(new DelegatingPreparedStatement(this, + connection.prepareStatement(sql, resultSetType, resultSetConcurrency))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + checkOpen(); + try { + return init(new DelegatingPreparedStatement(this, + connection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) throws SQLException { + checkOpen(); + try { + return init(new DelegatingPreparedStatement(this, connection.prepareStatement(sql, columnIndexes))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql, final String[] columnNames) throws SQLException { + checkOpen(); + try { + return init(new DelegatingPreparedStatement(this, connection.prepareStatement(sql, columnNames))); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public void releaseSavepoint(final Savepoint savepoint) throws SQLException { + checkOpen(); + try { + connection.releaseSavepoint(savepoint); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void rollback() throws SQLException { + checkOpen(); + try { + connection.rollback(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void rollback(final Savepoint savepoint) throws SQLException { + checkOpen(); + try { + connection.rollback(savepoint); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setAutoCommit(final boolean autoCommit) throws SQLException { + checkOpen(); + try { + connection.setAutoCommit(autoCommit); + if (cacheState) { + cachedAutoCommit = Boolean.valueOf(connection.getAutoCommit()); + } + } catch (final SQLException e) { + cachedAutoCommit = null; + handleException(e); + } + } + + /** + * Sets the state caching flag. + * + * @param cacheState + * The new value for the state caching flag + */ + public void setCacheState(final boolean cacheState) { + this.cacheState = cacheState; + } + + @Override + public void setCatalog(final String catalog) throws SQLException { + checkOpen(); + try { + connection.setCatalog(catalog); + if (cacheState) { + cachedCatalog = connection.getCatalog(); + } + } catch (final SQLException e) { + cachedCatalog = null; + handleException(e); + } + } + + @Override + public void setClientInfo(final Properties properties) throws SQLClientInfoException { + try { + checkOpen(); + connection.setClientInfo(properties); + } catch (final SQLClientInfoException e) { + throw e; + } catch (final SQLException e) { + throw new SQLClientInfoException("Connection is closed.", EMPTY_FAILED_PROPERTIES, e); + } + } + + @Override + public void setClientInfo(final String name, final String value) throws SQLClientInfoException { + try { + checkOpen(); + connection.setClientInfo(name, value); + } catch (final SQLClientInfoException e) { + throw e; + } catch (final SQLException e) { + throw new SQLClientInfoException("Connection is closed.", EMPTY_FAILED_PROPERTIES, e); + } + } + + protected void setClosedInternal(final boolean closed) { + this.closed = closed; + } + + /** + * Sets the default query timeout that will be used for {@link Statement}s created from this connection. + * {@code null} means that the driver default will be used. + * + * @param defaultQueryTimeoutDuration + * the new query timeout limit Duration; zero means there is no limit. + * @since 2.10.0 + */ + public void setDefaultQueryTimeout(final Duration defaultQueryTimeoutDuration) { + this.defaultQueryTimeoutDuration = defaultQueryTimeoutDuration; + } + + /** + * Sets the default query timeout that will be used for {@link Statement}s created from this connection. + * {@code null} means that the driver default will be used. + * + * @param defaultQueryTimeoutSeconds + * the new query timeout limit in seconds; zero means there is no limit. + * @deprecated Use {@link #setDefaultQueryTimeout(Duration)}. + */ + @Deprecated + public void setDefaultQueryTimeout(final Integer defaultQueryTimeoutSeconds) { + this.defaultQueryTimeoutDuration = defaultQueryTimeoutSeconds == null ? null : Duration.ofSeconds(defaultQueryTimeoutSeconds.intValue()); + } + + /** + * Sets my delegate. + * + * @param connection + * my delegate, may be null. + */ + public void setDelegate(final C connection) { + this.connection = connection; + } + + @Override + public void setHoldability(final int holdability) throws SQLException { + checkOpen(); + try { + connection.setHoldability(holdability); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNetworkTimeout(final Executor executor, final int milliseconds) throws SQLException { + checkOpen(); + try { + Jdbc41Bridge.setNetworkTimeout(connection, executor, milliseconds); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setReadOnly(final boolean readOnly) throws SQLException { + checkOpen(); + try { + connection.setReadOnly(readOnly); + if (cacheState) { + cachedReadOnly = Boolean.valueOf(connection.isReadOnly()); + } + } catch (final SQLException e) { + cachedReadOnly = null; + handleException(e); + } + } + + @Override + public Savepoint setSavepoint() throws SQLException { + checkOpen(); + try { + return connection.setSavepoint(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Savepoint setSavepoint(final String name) throws SQLException { + checkOpen(); + try { + return connection.setSavepoint(name); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public void setSchema(final String schema) throws SQLException { + checkOpen(); + try { + Jdbc41Bridge.setSchema(connection, schema); + if (cacheState) { + cachedSchema = connection.getSchema(); + } + } catch (final SQLException e) { + cachedSchema = null; + handleException(e); + } + } + + @Override + public void setTransactionIsolation(final int level) throws SQLException { + checkOpen(); + try { + connection.setTransactionIsolation(level); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTypeMap(final Map> map) throws SQLException { + checkOpen(); + try { + connection.setTypeMap(map); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * Returns a string representation of the metadata associated with the innermost delegate connection. + */ + @Override + public synchronized String toString() { + String str = null; + + final Connection conn = this.getInnermostDelegateInternal(); + if (conn != null) { + try { + if (conn.isClosed()) { + str = "connection is closed"; + } else { + final StringBuilder sb = new StringBuilder(); + sb.append(hashCode()); + final DatabaseMetaData meta = conn.getMetaData(); + if (meta != null) { + sb.append(", URL="); + sb.append(meta.getURL()); + sb.append(", "); + sb.append(meta.getDriverName()); + str = sb.toString(); + } + } + } catch (final SQLException ignored) { + // Ignore + } + } + return str != null ? str : super.toString(); + } + + @Override + public T unwrap(final Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return iface.cast(this); + } + if (iface.isAssignableFrom(connection.getClass())) { + return iface.cast(connection); + } + return connection.unwrap(iface); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java new file mode 100644 index 0000000..fa9bfaa --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java @@ -0,0 +1,1938 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.RowIdLifetime; +import java.sql.SQLException; + +/** + *

    + * A base delegating implementation of {@link DatabaseMetaData}. + *

    + *

    + * Methods that create {@link ResultSet} objects are wrapped to create {@link DelegatingResultSet} objects and the + * remaining methods simply call the corresponding method on the "delegate" provided in the constructor. + *

    + * + * @since 2.0 + */ +public class DelegatingDatabaseMetaData implements DatabaseMetaData { + + /** My delegate {@link DatabaseMetaData} */ + private final DatabaseMetaData databaseMetaData; + + /** The connection that created me. **/ + private final DelegatingConnection connection; + + /** + * Constructs a new instance for the given delegating connection and database meta data. + * + * @param connection + * the delegating connection + * @param databaseMetaData + * the database meta data + */ + public DelegatingDatabaseMetaData(final DelegatingConnection connection, + final DatabaseMetaData databaseMetaData) { + this.connection = connection; + this.databaseMetaData = databaseMetaData; + } + + @Override + public boolean allProceduresAreCallable() throws SQLException { + try { + return databaseMetaData.allProceduresAreCallable(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean allTablesAreSelectable() throws SQLException { + try { + return databaseMetaData.allTablesAreSelectable(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean autoCommitFailureClosesAllResultSets() throws SQLException { + try { + return databaseMetaData.autoCommitFailureClosesAllResultSets(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean dataDefinitionCausesTransactionCommit() throws SQLException { + try { + return databaseMetaData.dataDefinitionCausesTransactionCommit(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean dataDefinitionIgnoredInTransactions() throws SQLException { + try { + return databaseMetaData.dataDefinitionIgnoredInTransactions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean deletesAreDetected(final int type) throws SQLException { + try { + return databaseMetaData.deletesAreDetected(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { + try { + return databaseMetaData.doesMaxRowSizeIncludeBlobs(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean generatedKeyAlwaysReturned() throws SQLException { + connection.checkOpen(); + try { + return Jdbc41Bridge.generatedKeyAlwaysReturned(databaseMetaData); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public ResultSet getAttributes(final String catalog, final String schemaPattern, final String typeNamePattern, + final String attributeNamePattern) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getAttributes(catalog, schemaPattern, typeNamePattern, attributeNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getBestRowIdentifier(final String catalog, final String schema, final String table, + final int scope, final boolean nullable) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getBestRowIdentifier(catalog, schema, table, scope, nullable)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getCatalogs() throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getCatalogs()); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getCatalogSeparator() throws SQLException { + try { + return databaseMetaData.getCatalogSeparator(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getCatalogTerm() throws SQLException { + try { + return databaseMetaData.getCatalogTerm(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getClientInfoProperties() throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getClientInfoProperties()); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getColumnPrivileges(final String catalog, final String schema, final String table, + final String columnNamePattern) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getColumnPrivileges(catalog, schema, table, columnNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getColumns(final String catalog, final String schemaPattern, final String tableNamePattern, + final String columnNamePattern) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public Connection getConnection() throws SQLException { + return connection; + } + + @Override + public ResultSet getCrossReference(final String parentCatalog, final String parentSchema, final String parentTable, + final String foreignCatalog, final String foreignSchema, final String foreignTable) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getCrossReference(parentCatalog, + parentSchema, parentTable, foreignCatalog, foreignSchema, foreignTable)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public int getDatabaseMajorVersion() throws SQLException { + try { + return databaseMetaData.getDatabaseMajorVersion(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getDatabaseMinorVersion() throws SQLException { + try { + return databaseMetaData.getDatabaseMinorVersion(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public String getDatabaseProductName() throws SQLException { + try { + return databaseMetaData.getDatabaseProductName(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getDatabaseProductVersion() throws SQLException { + try { + return databaseMetaData.getDatabaseProductVersion(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public int getDefaultTransactionIsolation() throws SQLException { + try { + return databaseMetaData.getDefaultTransactionIsolation(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * Gets the underlying database meta data. + * + * @return The underlying database meta data. + */ + public DatabaseMetaData getDelegate() { + return databaseMetaData; + } + + @Override + public int getDriverMajorVersion() { + return databaseMetaData.getDriverMajorVersion(); + } + + @Override + public int getDriverMinorVersion() { + return databaseMetaData.getDriverMinorVersion(); + } + + @Override + public String getDriverName() throws SQLException { + try { + return databaseMetaData.getDriverName(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getDriverVersion() throws SQLException { + try { + return databaseMetaData.getDriverVersion(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getExportedKeys(final String catalog, final String schema, final String table) + throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getExportedKeys(catalog, schema, table)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getExtraNameCharacters() throws SQLException { + try { + return databaseMetaData.getExtraNameCharacters(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getFunctionColumns(final String catalog, final String schemaPattern, + final String functionNamePattern, final String columnNamePattern) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getFunctionColumns(catalog, + schemaPattern, functionNamePattern, columnNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getFunctions(final String catalog, final String schemaPattern, final String functionNamePattern) + throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getFunctions(catalog, schemaPattern, functionNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getIdentifierQuoteString() throws SQLException { + try { + return databaseMetaData.getIdentifierQuoteString(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getImportedKeys(final String catalog, final String schema, final String table) + throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getImportedKeys(catalog, schema, table)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getIndexInfo(final String catalog, final String schema, final String table, final boolean unique, + final boolean approximate) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getIndexInfo(catalog, schema, table, unique, approximate)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + /** + * If my underlying {@link ResultSet} is not a {@code DelegatingResultSet}, returns it, otherwise recursively + * invokes this method on my delegate. + *

    + * Hence this method will return the first delegate that is not a {@code DelegatingResultSet}, or {@code null} when + * no non-{@code DelegatingResultSet} delegate can be found by traversing this chain. + *

    + *

    + * This method is useful when you may have nested {@code DelegatingResultSet}s, and you want to make sure to obtain + * a "genuine" {@link ResultSet}. + *

    + * + * @return the innermost database meta data. + */ + public DatabaseMetaData getInnermostDelegate() { + DatabaseMetaData m = databaseMetaData; + while (m instanceof DelegatingDatabaseMetaData) { + m = ((DelegatingDatabaseMetaData) m).getDelegate(); + if (this == m) { + return null; + } + } + return m; + } + + @Override + public int getJDBCMajorVersion() throws SQLException { + try { + return databaseMetaData.getJDBCMajorVersion(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getJDBCMinorVersion() throws SQLException { + try { + return databaseMetaData.getJDBCMinorVersion(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxBinaryLiteralLength() throws SQLException { + try { + return databaseMetaData.getMaxBinaryLiteralLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxCatalogNameLength() throws SQLException { + try { + return databaseMetaData.getMaxCatalogNameLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxCharLiteralLength() throws SQLException { + try { + return databaseMetaData.getMaxCharLiteralLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxColumnNameLength() throws SQLException { + try { + return databaseMetaData.getMaxColumnNameLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxColumnsInGroupBy() throws SQLException { + try { + return databaseMetaData.getMaxColumnsInGroupBy(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxColumnsInIndex() throws SQLException { + try { + return databaseMetaData.getMaxColumnsInIndex(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxColumnsInOrderBy() throws SQLException { + try { + return databaseMetaData.getMaxColumnsInOrderBy(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxColumnsInSelect() throws SQLException { + try { + return databaseMetaData.getMaxColumnsInSelect(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxColumnsInTable() throws SQLException { + try { + return databaseMetaData.getMaxColumnsInTable(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxConnections() throws SQLException { + try { + return databaseMetaData.getMaxConnections(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxCursorNameLength() throws SQLException { + try { + return databaseMetaData.getMaxCursorNameLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxIndexLength() throws SQLException { + try { + return databaseMetaData.getMaxIndexLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * @since 2.5.0 + */ + @Override + public long getMaxLogicalLobSize() throws SQLException { + try { + return databaseMetaData.getMaxLogicalLobSize(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxProcedureNameLength() throws SQLException { + try { + return databaseMetaData.getMaxProcedureNameLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxRowSize() throws SQLException { + try { + return databaseMetaData.getMaxRowSize(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxSchemaNameLength() throws SQLException { + try { + return databaseMetaData.getMaxSchemaNameLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxStatementLength() throws SQLException { + try { + return databaseMetaData.getMaxStatementLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxStatements() throws SQLException { + try { + return databaseMetaData.getMaxStatements(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxTableNameLength() throws SQLException { + try { + return databaseMetaData.getMaxTableNameLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxTablesInSelect() throws SQLException { + try { + return databaseMetaData.getMaxTablesInSelect(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxUserNameLength() throws SQLException { + try { + return databaseMetaData.getMaxUserNameLength(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public String getNumericFunctions() throws SQLException { + try { + return databaseMetaData.getNumericFunctions(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getPrimaryKeys(final String catalog, final String schema, final String table) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getPrimaryKeys(catalog, schema, table)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getProcedureColumns(final String catalog, final String schemaPattern, + final String procedureNamePattern, final String columnNamePattern) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getProcedureColumns(catalog, + schemaPattern, procedureNamePattern, columnNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getProcedures(final String catalog, final String schemaPattern, final String procedureNamePattern) + throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getProcedures(catalog, schemaPattern, procedureNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getProcedureTerm() throws SQLException { + try { + return databaseMetaData.getProcedureTerm(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getPseudoColumns(final String catalog, final String schemaPattern, final String tableNamePattern, + final String columnNamePattern) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, Jdbc41Bridge.getPseudoColumns(databaseMetaData, + catalog, schemaPattern, tableNamePattern, columnNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public int getResultSetHoldability() throws SQLException { + try { + return databaseMetaData.getResultSetHoldability(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public RowIdLifetime getRowIdLifetime() throws SQLException { + try { + return databaseMetaData.getRowIdLifetime(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getSchemas() throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getSchemas()); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getSchemas(final String catalog, final String schemaPattern) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getSchemas(catalog, schemaPattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getSchemaTerm() throws SQLException { + try { + return databaseMetaData.getSchemaTerm(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getSearchStringEscape() throws SQLException { + try { + return databaseMetaData.getSearchStringEscape(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getSQLKeywords() throws SQLException { + try { + return databaseMetaData.getSQLKeywords(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public int getSQLStateType() throws SQLException { + try { + return databaseMetaData.getSQLStateType(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public String getStringFunctions() throws SQLException { + try { + return databaseMetaData.getStringFunctions(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getSuperTables(final String catalog, final String schemaPattern, final String tableNamePattern) + throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getSuperTables(catalog, schemaPattern, tableNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getSuperTypes(final String catalog, final String schemaPattern, final String typeNamePattern) + throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getSuperTypes(catalog, schemaPattern, typeNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getSystemFunctions() throws SQLException { + try { + return databaseMetaData.getSystemFunctions(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getTablePrivileges(final String catalog, final String schemaPattern, final String tableNamePattern) + throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getTablePrivileges(catalog, schemaPattern, tableNamePattern)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getTables(final String catalog, final String schemaPattern, final String tableNamePattern, + final String[] types) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getTables(catalog, schemaPattern, tableNamePattern, types)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getTableTypes() throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getTableTypes()); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getTimeDateFunctions() throws SQLException { + try { + return databaseMetaData.getTimeDateFunctions(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getTypeInfo() throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, databaseMetaData.getTypeInfo()); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getUDTs(final String catalog, final String schemaPattern, final String typeNamePattern, + final int[] types) throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getUDTs(catalog, schemaPattern, typeNamePattern, types)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getURL() throws SQLException { + try { + return databaseMetaData.getURL(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public String getUserName() throws SQLException { + try { + return databaseMetaData.getUserName(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public ResultSet getVersionColumns(final String catalog, final String schema, final String table) + throws SQLException { + connection.checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(connection, + databaseMetaData.getVersionColumns(catalog, schema, table)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + protected void handleException(final SQLException e) throws SQLException { + if (connection == null) { + throw e; + } + connection.handleException(e); + } + + @Override + public boolean insertsAreDetected(final int type) throws SQLException { + try { + return databaseMetaData.insertsAreDetected(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isCatalogAtStart() throws SQLException { + try { + return databaseMetaData.isCatalogAtStart(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isReadOnly() throws SQLException { + try { + return databaseMetaData.isReadOnly(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isWrapperFor(final Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return true; + } + if (iface.isAssignableFrom(databaseMetaData.getClass())) { + return true; + } + return databaseMetaData.isWrapperFor(iface); + } + + @Override + public boolean locatorsUpdateCopy() throws SQLException { + try { + return databaseMetaData.locatorsUpdateCopy(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean nullPlusNonNullIsNull() throws SQLException { + try { + return databaseMetaData.nullPlusNonNullIsNull(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean nullsAreSortedAtEnd() throws SQLException { + try { + return databaseMetaData.nullsAreSortedAtEnd(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean nullsAreSortedAtStart() throws SQLException { + try { + return databaseMetaData.nullsAreSortedAtStart(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean nullsAreSortedHigh() throws SQLException { + try { + return databaseMetaData.nullsAreSortedHigh(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean nullsAreSortedLow() throws SQLException { + try { + return databaseMetaData.nullsAreSortedLow(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean othersDeletesAreVisible(final int type) throws SQLException { + try { + return databaseMetaData.othersDeletesAreVisible(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean othersInsertsAreVisible(final int type) throws SQLException { + try { + return databaseMetaData.othersInsertsAreVisible(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean othersUpdatesAreVisible(final int type) throws SQLException { + try { + return databaseMetaData.othersUpdatesAreVisible(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean ownDeletesAreVisible(final int type) throws SQLException { + try { + return databaseMetaData.ownDeletesAreVisible(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean ownInsertsAreVisible(final int type) throws SQLException { + try { + return databaseMetaData.ownInsertsAreVisible(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean ownUpdatesAreVisible(final int type) throws SQLException { + try { + return databaseMetaData.ownUpdatesAreVisible(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean storesLowerCaseIdentifiers() throws SQLException { + try { + return databaseMetaData.storesLowerCaseIdentifiers(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + try { + return databaseMetaData.storesLowerCaseQuotedIdentifiers(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean storesMixedCaseIdentifiers() throws SQLException { + try { + return databaseMetaData.storesMixedCaseIdentifiers(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + try { + return databaseMetaData.storesMixedCaseQuotedIdentifiers(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean storesUpperCaseIdentifiers() throws SQLException { + try { + return databaseMetaData.storesUpperCaseIdentifiers(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + try { + return databaseMetaData.storesUpperCaseQuotedIdentifiers(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsAlterTableWithAddColumn() throws SQLException { + try { + return databaseMetaData.supportsAlterTableWithAddColumn(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsAlterTableWithDropColumn() throws SQLException { + try { + return databaseMetaData.supportsAlterTableWithDropColumn(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsANSI92EntryLevelSQL() throws SQLException { + try { + return databaseMetaData.supportsANSI92EntryLevelSQL(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsANSI92FullSQL() throws SQLException { + try { + return databaseMetaData.supportsANSI92FullSQL(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsANSI92IntermediateSQL() throws SQLException { + try { + return databaseMetaData.supportsANSI92IntermediateSQL(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsBatchUpdates() throws SQLException { + try { + return databaseMetaData.supportsBatchUpdates(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsCatalogsInDataManipulation() throws SQLException { + try { + return databaseMetaData.supportsCatalogsInDataManipulation(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsCatalogsInIndexDefinitions() throws SQLException { + try { + return databaseMetaData.supportsCatalogsInIndexDefinitions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + try { + return databaseMetaData.supportsCatalogsInPrivilegeDefinitions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsCatalogsInProcedureCalls() throws SQLException { + try { + return databaseMetaData.supportsCatalogsInProcedureCalls(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsCatalogsInTableDefinitions() throws SQLException { + try { + return databaseMetaData.supportsCatalogsInTableDefinitions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsColumnAliasing() throws SQLException { + try { + return databaseMetaData.supportsColumnAliasing(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsConvert() throws SQLException { + try { + return databaseMetaData.supportsConvert(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsConvert(final int fromType, final int toType) throws SQLException { + try { + return databaseMetaData.supportsConvert(fromType, toType); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsCoreSQLGrammar() throws SQLException { + try { + return databaseMetaData.supportsCoreSQLGrammar(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsCorrelatedSubqueries() throws SQLException { + try { + return databaseMetaData.supportsCorrelatedSubqueries(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + try { + return databaseMetaData.supportsDataDefinitionAndDataManipulationTransactions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsDataManipulationTransactionsOnly() throws SQLException { + try { + return databaseMetaData.supportsDataManipulationTransactionsOnly(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsDifferentTableCorrelationNames() throws SQLException { + try { + return databaseMetaData.supportsDifferentTableCorrelationNames(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsExpressionsInOrderBy() throws SQLException { + try { + return databaseMetaData.supportsExpressionsInOrderBy(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsExtendedSQLGrammar() throws SQLException { + try { + return databaseMetaData.supportsExtendedSQLGrammar(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsFullOuterJoins() throws SQLException { + try { + return databaseMetaData.supportsFullOuterJoins(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsGetGeneratedKeys() throws SQLException { + try { + return databaseMetaData.supportsGetGeneratedKeys(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsGroupBy() throws SQLException { + try { + return databaseMetaData.supportsGroupBy(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsGroupByBeyondSelect() throws SQLException { + try { + return databaseMetaData.supportsGroupByBeyondSelect(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsGroupByUnrelated() throws SQLException { + try { + return databaseMetaData.supportsGroupByUnrelated(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsIntegrityEnhancementFacility() throws SQLException { + try { + return databaseMetaData.supportsIntegrityEnhancementFacility(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsLikeEscapeClause() throws SQLException { + try { + return databaseMetaData.supportsLikeEscapeClause(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsLimitedOuterJoins() throws SQLException { + try { + return databaseMetaData.supportsLimitedOuterJoins(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsMinimumSQLGrammar() throws SQLException { + try { + return databaseMetaData.supportsMinimumSQLGrammar(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsMixedCaseIdentifiers() throws SQLException { + try { + return databaseMetaData.supportsMixedCaseIdentifiers(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + try { + return databaseMetaData.supportsMixedCaseQuotedIdentifiers(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsMultipleOpenResults() throws SQLException { + try { + return databaseMetaData.supportsMultipleOpenResults(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsMultipleResultSets() throws SQLException { + try { + return databaseMetaData.supportsMultipleResultSets(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsMultipleTransactions() throws SQLException { + try { + return databaseMetaData.supportsMultipleTransactions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsNamedParameters() throws SQLException { + try { + return databaseMetaData.supportsNamedParameters(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsNonNullableColumns() throws SQLException { + try { + return databaseMetaData.supportsNonNullableColumns(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsOpenCursorsAcrossCommit() throws SQLException { + try { + return databaseMetaData.supportsOpenCursorsAcrossCommit(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsOpenCursorsAcrossRollback() throws SQLException { + try { + return databaseMetaData.supportsOpenCursorsAcrossRollback(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsOpenStatementsAcrossCommit() throws SQLException { + try { + return databaseMetaData.supportsOpenStatementsAcrossCommit(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsOpenStatementsAcrossRollback() throws SQLException { + try { + return databaseMetaData.supportsOpenStatementsAcrossRollback(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsOrderByUnrelated() throws SQLException { + try { + return databaseMetaData.supportsOrderByUnrelated(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsOuterJoins() throws SQLException { + try { + return databaseMetaData.supportsOuterJoins(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsPositionedDelete() throws SQLException { + try { + return databaseMetaData.supportsPositionedDelete(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsPositionedUpdate() throws SQLException { + try { + return databaseMetaData.supportsPositionedUpdate(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + /** + * @since 2.5.0 + */ + @Override + public boolean supportsRefCursors() throws SQLException { + try { + return databaseMetaData.supportsRefCursors(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsResultSetConcurrency(final int type, final int concurrency) throws SQLException { + try { + return databaseMetaData.supportsResultSetConcurrency(type, concurrency); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsResultSetHoldability(final int holdability) throws SQLException { + try { + return databaseMetaData.supportsResultSetHoldability(holdability); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsResultSetType(final int type) throws SQLException { + try { + return databaseMetaData.supportsResultSetType(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSavepoints() throws SQLException { + try { + return databaseMetaData.supportsSavepoints(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSchemasInDataManipulation() throws SQLException { + try { + return databaseMetaData.supportsSchemasInDataManipulation(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSchemasInIndexDefinitions() throws SQLException { + try { + return databaseMetaData.supportsSchemasInIndexDefinitions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { + try { + return databaseMetaData.supportsSchemasInPrivilegeDefinitions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSchemasInProcedureCalls() throws SQLException { + try { + return databaseMetaData.supportsSchemasInProcedureCalls(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSchemasInTableDefinitions() throws SQLException { + try { + return databaseMetaData.supportsSchemasInTableDefinitions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSelectForUpdate() throws SQLException { + try { + return databaseMetaData.supportsSelectForUpdate(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsStatementPooling() throws SQLException { + try { + return databaseMetaData.supportsStatementPooling(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + try { + return databaseMetaData.supportsStoredFunctionsUsingCallSyntax(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsStoredProcedures() throws SQLException { + try { + return databaseMetaData.supportsStoredProcedures(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSubqueriesInComparisons() throws SQLException { + try { + return databaseMetaData.supportsSubqueriesInComparisons(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSubqueriesInExists() throws SQLException { + try { + return databaseMetaData.supportsSubqueriesInExists(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSubqueriesInIns() throws SQLException { + try { + return databaseMetaData.supportsSubqueriesInIns(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsSubqueriesInQuantifieds() throws SQLException { + try { + return databaseMetaData.supportsSubqueriesInQuantifieds(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsTableCorrelationNames() throws SQLException { + try { + return databaseMetaData.supportsTableCorrelationNames(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsTransactionIsolationLevel(final int level) throws SQLException { + try { + return databaseMetaData.supportsTransactionIsolationLevel(level); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsTransactions() throws SQLException { + try { + return databaseMetaData.supportsTransactions(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsUnion() throws SQLException { + try { + return databaseMetaData.supportsUnion(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean supportsUnionAll() throws SQLException { + try { + return databaseMetaData.supportsUnionAll(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + + @Override + public T unwrap(final Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return iface.cast(this); + } + if (iface.isAssignableFrom(databaseMetaData.getClass())) { + return iface.cast(databaseMetaData); + } + return databaseMetaData.unwrap(iface); + } + + @Override + public boolean updatesAreDetected(final int type) throws SQLException { + try { + return databaseMetaData.updatesAreDetected(type); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean usesLocalFilePerTable() throws SQLException { + try { + return databaseMetaData.usesLocalFilePerTable(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean usesLocalFiles() throws SQLException { + try { + return databaseMetaData.usesLocalFiles(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java new file mode 100644 index 0000000..0dd7668 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java @@ -0,0 +1,718 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLType; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +/** + * A base delegating implementation of {@link PreparedStatement}. + *

    + * All of the methods from the {@link PreparedStatement} interface simply check to see that the + * {@link PreparedStatement} is active, and call the corresponding method on the "delegate" provided in my constructor. + *

    + * Extends AbandonedTrace to implement Statement tracking and logging of code which created the Statement. Tracking the + * Statement ensures that the Connection which created it can close any open Statement's on Connection close. + * + * @since 2.0 + */ +public class DelegatingPreparedStatement extends DelegatingStatement implements PreparedStatement { + + /** + * Create a wrapper for the Statement which traces this Statement to the Connection which created it and the code + * which created it. + * + * @param statement + * the {@link PreparedStatement} to delegate all calls to. + * @param connection + * the {@link DelegatingConnection} that created this statement. + */ + public DelegatingPreparedStatement(final DelegatingConnection connection, final PreparedStatement statement) { + super(connection, statement); + } + + @Override + public void addBatch() throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().addBatch(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void clearParameters() throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().clearParameters(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public boolean execute() throws SQLException { + checkOpen(); + if (getConnectionInternal() != null) { + getConnectionInternal().setLastUsed(); + } + try { + return getDelegatePreparedStatement().execute(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + /** + * @since 2.5.0 + */ + @Override + public long executeLargeUpdate() throws SQLException { + checkOpen(); + try { + return getDelegatePreparedStatement().executeLargeUpdate(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public ResultSet executeQuery() throws SQLException { + checkOpen(); + if (getConnectionInternal() != null) { + getConnectionInternal().setLastUsed(); + } + try { + return DelegatingResultSet.wrapResultSet(this, getDelegatePreparedStatement().executeQuery()); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public int executeUpdate() throws SQLException { + checkOpen(); + if (getConnectionInternal() != null) { + getConnectionInternal().setLastUsed(); + } + try { + return getDelegatePreparedStatement().executeUpdate(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + private PreparedStatement getDelegatePreparedStatement() { + return (PreparedStatement) getDelegate(); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + checkOpen(); + try { + return getDelegatePreparedStatement().getMetaData(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public java.sql.ParameterMetaData getParameterMetaData() throws SQLException { + checkOpen(); + try { + return getDelegatePreparedStatement().getParameterMetaData(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public void setArray(final int i, final Array x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setArray(i, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setAsciiStream(final int parameterIndex, final InputStream inputStream) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setAsciiStream(parameterIndex, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setAsciiStream(final int parameterIndex, final InputStream x, final int length) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setAsciiStream(parameterIndex, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setAsciiStream(final int parameterIndex, final InputStream inputStream, final long length) + throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setAsciiStream(parameterIndex, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBigDecimal(final int parameterIndex, final BigDecimal x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBigDecimal(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBinaryStream(final int parameterIndex, final InputStream inputStream) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBinaryStream(parameterIndex, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBinaryStream(final int parameterIndex, final InputStream x, final int length) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBinaryStream(parameterIndex, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBinaryStream(final int parameterIndex, final InputStream inputStream, final long length) + throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBinaryStream(parameterIndex, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBlob(final int i, final Blob x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBlob(i, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBlob(final int parameterIndex, final InputStream inputStream) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBlob(parameterIndex, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBlob(final int parameterIndex, final InputStream inputStream, final long length) + throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBlob(parameterIndex, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBoolean(final int parameterIndex, final boolean x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBoolean(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setByte(final int parameterIndex, final byte x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setByte(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setBytes(final int parameterIndex, final byte[] x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setBytes(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setCharacterStream(final int parameterIndex, final Reader reader) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setCharacterStream(parameterIndex, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setCharacterStream(final int parameterIndex, final Reader reader, final int length) + throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setCharacterStream(parameterIndex, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setCharacterStream(final int parameterIndex, final Reader reader, final long length) + throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setCharacterStream(parameterIndex, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setClob(final int i, final Clob x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setClob(i, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setClob(final int parameterIndex, final Reader reader) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setClob(parameterIndex, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setClob(final int parameterIndex, final Reader reader, final long length) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setClob(parameterIndex, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setDate(final int parameterIndex, final Date x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setDate(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setDate(final int parameterIndex, final Date x, final Calendar cal) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setDate(parameterIndex, x, cal); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setDouble(final int parameterIndex, final double x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setDouble(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setFloat(final int parameterIndex, final float x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setFloat(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setInt(final int parameterIndex, final int x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setInt(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setLong(final int parameterIndex, final long x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setLong(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNCharacterStream(final int parameterIndex, final Reader reader) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setNCharacterStream(parameterIndex, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNCharacterStream(final int parameterIndex, final Reader value, final long length) + throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setNCharacterStream(parameterIndex, value, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNClob(final int parameterIndex, final NClob value) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setNClob(parameterIndex, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNClob(final int parameterIndex, final Reader reader) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setNClob(parameterIndex, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNClob(final int parameterIndex, final Reader reader, final long length) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setNClob(parameterIndex, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNString(final int parameterIndex, final String value) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setNString(parameterIndex, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNull(final int parameterIndex, final int sqlType) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setNull(parameterIndex, sqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setNull(final int paramIndex, final int sqlType, final String typeName) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setNull(paramIndex, sqlType, typeName); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setObject(final int parameterIndex, final Object x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setObject(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setObject(final int parameterIndex, final Object x, final int targetSqlType) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setObject(parameterIndex, x, targetSqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setObject(final int parameterIndex, final Object x, final int targetSqlType, final int scale) + throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setObject(parameterIndex, x, targetSqlType, scale); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void setObject(final int parameterIndex, final Object x, final SQLType targetSqlType) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setObject(parameterIndex, x, targetSqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void setObject(final int parameterIndex, final Object x, final SQLType targetSqlType, final int scaleOrLength) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setRef(final int i, final Ref x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setRef(i, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setRowId(final int parameterIndex, final RowId value) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setRowId(parameterIndex, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setShort(final int parameterIndex, final short x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setShort(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setSQLXML(final int parameterIndex, final SQLXML value) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setSQLXML(parameterIndex, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setString(final int parameterIndex, final String x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setString(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTime(final int parameterIndex, final Time x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setTime(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTime(final int parameterIndex, final Time x, final Calendar cal) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setTime(parameterIndex, x, cal); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTimestamp(final int parameterIndex, final Timestamp x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setTimestamp(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setTimestamp(final int parameterIndex, final Timestamp x, final Calendar cal) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setTimestamp(parameterIndex, x, cal); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @deprecated Use setAsciiStream(), setCharacterStream() or setNCharacterStream() + */ + @Deprecated + @Override + public void setUnicodeStream(final int parameterIndex, final InputStream x, final int length) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setUnicodeStream(parameterIndex, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setURL(final int parameterIndex, final java.net.URL x) throws SQLException { + checkOpen(); + try { + getDelegatePreparedStatement().setURL(parameterIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * Returns a String representation of this object. + * + * @return String + */ + @Override + public synchronized String toString() { + final Statement statement = getDelegate(); + return statement == null ? "NULL" : statement.toString(); + } + + protected void prepareToReturn() throws SQLException { + setClosedInternal(true); + removeThisTrace(getConnectionInternal()); + + // The JDBC spec requires that a statement close any open + // ResultSet's when it is closed. + // FIXME The PreparedStatement we're wrapping should handle this for us. + // See DBCP-10 for what could happen when ResultSets are closed twice. + final List traceList = getTrace(); + if (traceList != null) { + final List thrownList = new ArrayList<>(); + traceList.forEach(trace -> trace.close(thrownList::add)); + clearTrace(); + if (!thrownList.isEmpty()) { + throw new SQLExceptionList(thrownList); + } + } + + super.passivate(); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java new file mode 100644 index 0000000..227d806 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java @@ -0,0 +1,2082 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLType; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +/** + * A base delegating implementation of {@link ResultSet}. + *

    + * All of the methods from the {@link ResultSet} interface simply call the corresponding method on the "delegate" + * provided in my constructor. + *

    + *

    + * Extends AbandonedTrace to implement result set tracking and logging of code which created the ResultSet. Tracking the + * ResultSet ensures that the Statement which created it can close any open ResultSet's on Statement close. + *

    + * + * @since 2.0 + */ +public final class DelegatingResultSet extends AbandonedTrace implements ResultSet { + + /** + * Wraps the given result set in a delegate. + * + * @param connection + * The Connection which created the ResultSet. + * @param resultSet + * The ResultSet to wrap. + * @return a new delegate. + */ + public static ResultSet wrapResultSet(final Connection connection, final ResultSet resultSet) { + if (null == resultSet) { + return null; + } + return new DelegatingResultSet(connection, resultSet); + } + + /** + * Wraps the given result set in a delegate. + * + * @param statement + * The Statement which created the ResultSet. + * @param resultSet + * The ResultSet to wrap. + * @return a new delegate. + */ + public static ResultSet wrapResultSet(final Statement statement, final ResultSet resultSet) { + if (null == resultSet) { + return null; + } + return new DelegatingResultSet(statement, resultSet); + } + + /** My delegate. **/ + private final ResultSet resultSet; + + /** The Statement that created me, if any. **/ + private Statement statement; + + /** The Connection that created me, if any. **/ + private Connection connection; + + /** + * Creates a wrapper for the ResultSet which traces this ResultSet to the Connection which created it (via, for + * example DatabaseMetadata, and the code which created it. + *

    + * Private to ensure all construction is {@link #wrapResultSet(Connection, ResultSet)} + *

    + * + * @param connection + * Connection which created this ResultSet + * @param resultSet + * ResultSet to wrap + */ + private DelegatingResultSet(final Connection connection, final ResultSet resultSet) { + super((AbandonedTrace) connection); + this.connection = connection; + this.resultSet = resultSet; + } + + /** + * Creates a wrapper for the ResultSet which traces this ResultSet to the Statement which created it and the code + * which created it. + *

    + * Private to ensure all construction is {@link #wrapResultSet(Statement, ResultSet)} + *

    + * + * @param statement + * The Statement which created the ResultSet. + * @param resultSet + * The ResultSet to wrap. + */ + private DelegatingResultSet(final Statement statement, final ResultSet resultSet) { + super((AbandonedTrace) statement); + this.statement = statement; + this.resultSet = resultSet; + } + + @Override + public boolean absolute(final int row) throws SQLException { + try { + return resultSet.absolute(row); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public void afterLast() throws SQLException { + try { + resultSet.afterLast(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void beforeFirst() throws SQLException { + try { + resultSet.beforeFirst(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void cancelRowUpdates() throws SQLException { + try { + resultSet.cancelRowUpdates(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void clearWarnings() throws SQLException { + try { + resultSet.clearWarnings(); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * Wrapper for close of ResultSet which removes this result set from being traced then calls close on the original + * ResultSet. + */ + @Override + public void close() throws SQLException { + try { + if (statement != null) { + removeThisTrace(statement); + statement = null; + } + if (connection != null) { + removeThisTrace(connection); + connection = null; + } + resultSet.close(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void deleteRow() throws SQLException { + try { + resultSet.deleteRow(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public int findColumn(final String columnName) throws SQLException { + try { + return resultSet.findColumn(columnName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public boolean first() throws SQLException { + try { + return resultSet.first(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public Array getArray(final int i) throws SQLException { + try { + return resultSet.getArray(i); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Array getArray(final String colName) throws SQLException { + try { + return resultSet.getArray(colName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public InputStream getAsciiStream(final int columnIndex) throws SQLException { + try { + return resultSet.getAsciiStream(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public InputStream getAsciiStream(final String columnName) throws SQLException { + try { + return resultSet.getAsciiStream(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public BigDecimal getBigDecimal(final int columnIndex) throws SQLException { + try { + return resultSet.getBigDecimal(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * @deprecated Use {@link #getBigDecimal(int)} + */ + @Deprecated + @Override + public BigDecimal getBigDecimal(final int columnIndex, final int scale) throws SQLException { + try { + return resultSet.getBigDecimal(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public BigDecimal getBigDecimal(final String columnName) throws SQLException { + try { + return resultSet.getBigDecimal(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * @deprecated Use {@link #getBigDecimal(String)} + */ + @Deprecated + @Override + public BigDecimal getBigDecimal(final String columnName, final int scale) throws SQLException { + try { + return resultSet.getBigDecimal(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public InputStream getBinaryStream(final int columnIndex) throws SQLException { + try { + return resultSet.getBinaryStream(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public InputStream getBinaryStream(final String columnName) throws SQLException { + try { + return resultSet.getBinaryStream(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Blob getBlob(final int i) throws SQLException { + try { + return resultSet.getBlob(i); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Blob getBlob(final String colName) throws SQLException { + try { + return resultSet.getBlob(colName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public boolean getBoolean(final int columnIndex) throws SQLException { + try { + return resultSet.getBoolean(columnIndex); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean getBoolean(final String columnName) throws SQLException { + try { + return resultSet.getBoolean(columnName); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public byte getByte(final int columnIndex) throws SQLException { + try { + return resultSet.getByte(columnIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public byte getByte(final String columnName) throws SQLException { + try { + return resultSet.getByte(columnName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public byte[] getBytes(final int columnIndex) throws SQLException { + try { + return resultSet.getBytes(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public byte[] getBytes(final String columnName) throws SQLException { + try { + return resultSet.getBytes(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Reader getCharacterStream(final int columnIndex) throws SQLException { + try { + return resultSet.getCharacterStream(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Reader getCharacterStream(final String columnName) throws SQLException { + try { + return resultSet.getCharacterStream(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Clob getClob(final int i) throws SQLException { + try { + return resultSet.getClob(i); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Clob getClob(final String colName) throws SQLException { + try { + return resultSet.getClob(colName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public int getConcurrency() throws SQLException { + try { + return resultSet.getConcurrency(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public String getCursorName() throws SQLException { + try { + return resultSet.getCursorName(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Date getDate(final int columnIndex) throws SQLException { + try { + return resultSet.getDate(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Date getDate(final int columnIndex, final Calendar cal) throws SQLException { + try { + return resultSet.getDate(columnIndex, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Date getDate(final String columnName) throws SQLException { + try { + return resultSet.getDate(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Date getDate(final String columnName, final Calendar cal) throws SQLException { + try { + return resultSet.getDate(columnName, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * Gets my delegate. + * + * @return my delegate. + */ + public ResultSet getDelegate() { + return resultSet; + } + + @Override + public double getDouble(final int columnIndex) throws SQLException { + try { + return resultSet.getDouble(columnIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public double getDouble(final String columnName) throws SQLException { + try { + return resultSet.getDouble(columnName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getFetchDirection() throws SQLException { + try { + return resultSet.getFetchDirection(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getFetchSize() throws SQLException { + try { + return resultSet.getFetchSize(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public float getFloat(final int columnIndex) throws SQLException { + try { + return resultSet.getFloat(columnIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public float getFloat(final String columnName) throws SQLException { + try { + return resultSet.getFloat(columnName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getHoldability() throws SQLException { + try { + return resultSet.getHoldability(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * If my underlying {@link ResultSet} is not a {@code DelegatingResultSet}, returns it, otherwise recursively + * invokes this method on my delegate. + *

    + * Hence this method will return the first delegate that is not a {@code DelegatingResultSet}, or {@code null} when + * no non-{@code DelegatingResultSet} delegate can be found by traversing this chain. + *

    + *

    + * This method is useful when you may have nested {@code DelegatingResultSet}s, and you want to make sure to obtain + * a "genuine" {@link ResultSet}. + *

    + * + * @return the innermost delegate. + */ + public ResultSet getInnermostDelegate() { + ResultSet r = resultSet; + while (r instanceof DelegatingResultSet) { + r = ((DelegatingResultSet) r).getDelegate(); + if (this == r) { + return null; + } + } + return r; + } + + @Override + public int getInt(final int columnIndex) throws SQLException { + try { + return resultSet.getInt(columnIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getInt(final String columnName) throws SQLException { + try { + return resultSet.getInt(columnName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public long getLong(final int columnIndex) throws SQLException { + try { + return resultSet.getLong(columnIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public long getLong(final String columnName) throws SQLException { + try { + return resultSet.getLong(columnName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + try { + return resultSet.getMetaData(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Reader getNCharacterStream(final int columnIndex) throws SQLException { + try { + return resultSet.getNCharacterStream(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Reader getNCharacterStream(final String columnLabel) throws SQLException { + try { + return resultSet.getNCharacterStream(columnLabel); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public NClob getNClob(final int columnIndex) throws SQLException { + try { + return resultSet.getNClob(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public NClob getNClob(final String columnLabel) throws SQLException { + try { + return resultSet.getNClob(columnLabel); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public String getNString(final int columnIndex) throws SQLException { + try { + return resultSet.getNString(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public String getNString(final String columnLabel) throws SQLException { + try { + return resultSet.getNString(columnLabel); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Object getObject(final int columnIndex) throws SQLException { + try { + return resultSet.getObject(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public T getObject(final int columnIndex, final Class type) throws SQLException { + try { + return Jdbc41Bridge.getObject(resultSet, columnIndex, type); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Object getObject(final int i, final Map> map) throws SQLException { + try { + return resultSet.getObject(i, map); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Object getObject(final String columnName) throws SQLException { + try { + return resultSet.getObject(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public T getObject(final String columnLabel, final Class type) throws SQLException { + try { + return Jdbc41Bridge.getObject(resultSet, columnLabel, type); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Object getObject(final String colName, final Map> map) throws SQLException { + try { + return resultSet.getObject(colName, map); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Ref getRef(final int i) throws SQLException { + try { + return resultSet.getRef(i); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Ref getRef(final String colName) throws SQLException { + try { + return resultSet.getRef(colName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public int getRow() throws SQLException { + try { + return resultSet.getRow(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public RowId getRowId(final int columnIndex) throws SQLException { + try { + return resultSet.getRowId(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public RowId getRowId(final String columnLabel) throws SQLException { + try { + return resultSet.getRowId(columnLabel); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public short getShort(final int columnIndex) throws SQLException { + try { + return resultSet.getShort(columnIndex); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public short getShort(final String columnName) throws SQLException { + try { + return resultSet.getShort(columnName); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public SQLXML getSQLXML(final int columnIndex) throws SQLException { + try { + return resultSet.getSQLXML(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public SQLXML getSQLXML(final String columnLabel) throws SQLException { + try { + return resultSet.getSQLXML(columnLabel); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Statement getStatement() throws SQLException { + return statement; + } + + @Override + public String getString(final int columnIndex) throws SQLException { + try { + return resultSet.getString(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public String getString(final String columnName) throws SQLException { + try { + return resultSet.getString(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Time getTime(final int columnIndex) throws SQLException { + try { + return resultSet.getTime(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Time getTime(final int columnIndex, final Calendar cal) throws SQLException { + try { + return resultSet.getTime(columnIndex, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Time getTime(final String columnName) throws SQLException { + try { + return resultSet.getTime(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Time getTime(final String columnName, final Calendar cal) throws SQLException { + try { + return resultSet.getTime(columnName, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Timestamp getTimestamp(final int columnIndex) throws SQLException { + try { + return resultSet.getTimestamp(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Timestamp getTimestamp(final int columnIndex, final Calendar cal) throws SQLException { + try { + return resultSet.getTimestamp(columnIndex, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Timestamp getTimestamp(final String columnName) throws SQLException { + try { + return resultSet.getTimestamp(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public Timestamp getTimestamp(final String columnName, final Calendar cal) throws SQLException { + try { + return resultSet.getTimestamp(columnName, cal); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public int getType() throws SQLException { + try { + return resultSet.getType(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * @deprecated Use {@link #getCharacterStream(int)} + */ + @Deprecated + @Override + public InputStream getUnicodeStream(final int columnIndex) throws SQLException { + try { + return resultSet.getUnicodeStream(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * @deprecated Use {@link #getCharacterStream(String)} + */ + @Deprecated + @Override + public InputStream getUnicodeStream(final String columnName) throws SQLException { + try { + return resultSet.getUnicodeStream(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public java.net.URL getURL(final int columnIndex) throws SQLException { + try { + return resultSet.getURL(columnIndex); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public java.net.URL getURL(final String columnName) throws SQLException { + try { + return resultSet.getURL(columnName); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public SQLWarning getWarnings() throws SQLException { + try { + return resultSet.getWarnings(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + protected void handleException(final SQLException e) throws SQLException { + if (statement instanceof DelegatingStatement) { + ((DelegatingStatement) statement).handleException(e); + } else if (connection instanceof DelegatingConnection) { + ((DelegatingConnection) connection).handleException(e); + } else { + throw e; + } + } + + @Override + public void insertRow() throws SQLException { + try { + resultSet.insertRow(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public boolean isAfterLast() throws SQLException { + try { + return resultSet.isAfterLast(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isBeforeFirst() throws SQLException { + try { + return resultSet.isBeforeFirst(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isClosed() throws SQLException { + try { + return resultSet.isClosed(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isFirst() throws SQLException { + try { + return resultSet.isFirst(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isLast() throws SQLException { + try { + return resultSet.isLast(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isWrapperFor(final Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return true; + } + if (iface.isAssignableFrom(resultSet.getClass())) { + return true; + } + return resultSet.isWrapperFor(iface); + } + + @Override + public boolean last() throws SQLException { + try { + return resultSet.last(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public void moveToCurrentRow() throws SQLException { + try { + resultSet.moveToCurrentRow(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void moveToInsertRow() throws SQLException { + try { + resultSet.moveToInsertRow(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public boolean next() throws SQLException { + try { + return resultSet.next(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean previous() throws SQLException { + try { + return resultSet.previous(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public void refreshRow() throws SQLException { + try { + resultSet.refreshRow(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public boolean relative(final int rows) throws SQLException { + try { + return resultSet.relative(rows); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean rowDeleted() throws SQLException { + try { + return resultSet.rowDeleted(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean rowInserted() throws SQLException { + try { + return resultSet.rowInserted(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean rowUpdated() throws SQLException { + try { + return resultSet.rowUpdated(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public void setFetchDirection(final int direction) throws SQLException { + try { + resultSet.setFetchDirection(direction); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setFetchSize(final int rows) throws SQLException { + try { + resultSet.setFetchSize(rows); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public synchronized String toString() { + return super.toString() + "[resultSet=" + resultSet + ", statement=" + statement + ", connection=" + connection + "]"; + } + + @Override + public T unwrap(final Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return iface.cast(this); + } + if (iface.isAssignableFrom(resultSet.getClass())) { + return iface.cast(resultSet); + } + return resultSet.unwrap(iface); + } + + @Override + public void updateArray(final int columnIndex, final Array x) throws SQLException { + try { + resultSet.updateArray(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateArray(final String columnName, final Array x) throws SQLException { + try { + resultSet.updateArray(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateAsciiStream(final int columnIndex, final InputStream inputStream) throws SQLException { + try { + resultSet.updateAsciiStream(columnIndex, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateAsciiStream(final int columnIndex, final InputStream x, final int length) throws SQLException { + try { + resultSet.updateAsciiStream(columnIndex, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateAsciiStream(final int columnIndex, final InputStream inputStream, final long length) + throws SQLException { + try { + resultSet.updateAsciiStream(columnIndex, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateAsciiStream(final String columnLabel, final InputStream inputStream) throws SQLException { + try { + resultSet.updateAsciiStream(columnLabel, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateAsciiStream(final String columnName, final InputStream x, final int length) throws SQLException { + try { + resultSet.updateAsciiStream(columnName, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateAsciiStream(final String columnLabel, final InputStream inputStream, final long length) + throws SQLException { + try { + resultSet.updateAsciiStream(columnLabel, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBigDecimal(final int columnIndex, final BigDecimal x) throws SQLException { + try { + resultSet.updateBigDecimal(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBigDecimal(final String columnName, final BigDecimal x) throws SQLException { + try { + resultSet.updateBigDecimal(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBinaryStream(final int columnIndex, final InputStream inputStream) throws SQLException { + try { + resultSet.updateBinaryStream(columnIndex, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBinaryStream(final int columnIndex, final InputStream x, final int length) throws SQLException { + try { + resultSet.updateBinaryStream(columnIndex, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBinaryStream(final int columnIndex, final InputStream inputStream, final long length) + throws SQLException { + try { + resultSet.updateBinaryStream(columnIndex, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBinaryStream(final String columnLabel, final InputStream inputStream) throws SQLException { + try { + resultSet.updateBinaryStream(columnLabel, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBinaryStream(final String columnName, final InputStream x, final int length) throws SQLException { + try { + resultSet.updateBinaryStream(columnName, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBinaryStream(final String columnLabel, final InputStream inputStream, final long length) + throws SQLException { + try { + resultSet.updateBinaryStream(columnLabel, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBlob(final int columnIndex, final Blob x) throws SQLException { + try { + resultSet.updateBlob(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBlob(final int columnIndex, final InputStream inputStream) throws SQLException { + try { + resultSet.updateBlob(columnIndex, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBlob(final int columnIndex, final InputStream inputStream, final long length) + throws SQLException { + try { + resultSet.updateBlob(columnIndex, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBlob(final String columnName, final Blob x) throws SQLException { + try { + resultSet.updateBlob(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBlob(final String columnLabel, final InputStream inputStream) throws SQLException { + try { + resultSet.updateBlob(columnLabel, inputStream); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBlob(final String columnLabel, final InputStream inputStream, final long length) + throws SQLException { + try { + resultSet.updateBlob(columnLabel, inputStream, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBoolean(final int columnIndex, final boolean x) throws SQLException { + try { + resultSet.updateBoolean(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBoolean(final String columnName, final boolean x) throws SQLException { + try { + resultSet.updateBoolean(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateByte(final int columnIndex, final byte x) throws SQLException { + try { + resultSet.updateByte(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateByte(final String columnName, final byte x) throws SQLException { + try { + resultSet.updateByte(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBytes(final int columnIndex, final byte[] x) throws SQLException { + try { + resultSet.updateBytes(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateBytes(final String columnName, final byte[] x) throws SQLException { + try { + resultSet.updateBytes(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateCharacterStream(final int columnIndex, final Reader reader) throws SQLException { + try { + resultSet.updateCharacterStream(columnIndex, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateCharacterStream(final int columnIndex, final Reader x, final int length) throws SQLException { + try { + resultSet.updateCharacterStream(columnIndex, x, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateCharacterStream(final int columnIndex, final Reader reader, final long length) + throws SQLException { + try { + resultSet.updateCharacterStream(columnIndex, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateCharacterStream(final String columnLabel, final Reader reader) throws SQLException { + try { + resultSet.updateCharacterStream(columnLabel, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateCharacterStream(final String columnName, final Reader reader, final int length) + throws SQLException { + try { + resultSet.updateCharacterStream(columnName, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateCharacterStream(final String columnLabel, final Reader reader, final long length) + throws SQLException { + try { + resultSet.updateCharacterStream(columnLabel, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateClob(final int columnIndex, final Clob x) throws SQLException { + try { + resultSet.updateClob(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateClob(final int columnIndex, final Reader reader) throws SQLException { + try { + resultSet.updateClob(columnIndex, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateClob(final int columnIndex, final Reader reader, final long length) throws SQLException { + try { + resultSet.updateClob(columnIndex, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateClob(final String columnName, final Clob x) throws SQLException { + try { + resultSet.updateClob(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateClob(final String columnLabel, final Reader reader) throws SQLException { + try { + resultSet.updateClob(columnLabel, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateClob(final String columnLabel, final Reader reader, final long length) throws SQLException { + try { + resultSet.updateClob(columnLabel, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateDate(final int columnIndex, final Date x) throws SQLException { + try { + resultSet.updateDate(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateDate(final String columnName, final Date x) throws SQLException { + try { + resultSet.updateDate(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateDouble(final int columnIndex, final double x) throws SQLException { + try { + resultSet.updateDouble(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateDouble(final String columnName, final double x) throws SQLException { + try { + resultSet.updateDouble(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateFloat(final int columnIndex, final float x) throws SQLException { + try { + resultSet.updateFloat(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateFloat(final String columnName, final float x) throws SQLException { + try { + resultSet.updateFloat(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateInt(final int columnIndex, final int x) throws SQLException { + try { + resultSet.updateInt(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateInt(final String columnName, final int x) throws SQLException { + try { + resultSet.updateInt(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateLong(final int columnIndex, final long x) throws SQLException { + try { + resultSet.updateLong(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateLong(final String columnName, final long x) throws SQLException { + try { + resultSet.updateLong(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNCharacterStream(final int columnIndex, final Reader reader) throws SQLException { + try { + resultSet.updateNCharacterStream(columnIndex, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNCharacterStream(final int columnIndex, final Reader reader, final long length) + throws SQLException { + try { + resultSet.updateNCharacterStream(columnIndex, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNCharacterStream(final String columnLabel, final Reader reader) throws SQLException { + try { + resultSet.updateNCharacterStream(columnLabel, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNCharacterStream(final String columnLabel, final Reader reader, final long length) + throws SQLException { + try { + resultSet.updateNCharacterStream(columnLabel, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNClob(final int columnIndex, final NClob value) throws SQLException { + try { + resultSet.updateNClob(columnIndex, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNClob(final int columnIndex, final Reader reader) throws SQLException { + try { + resultSet.updateNClob(columnIndex, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNClob(final int columnIndex, final Reader reader, final long length) throws SQLException { + try { + resultSet.updateNClob(columnIndex, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNClob(final String columnLabel, final NClob value) throws SQLException { + try { + resultSet.updateNClob(columnLabel, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNClob(final String columnLabel, final Reader reader) throws SQLException { + try { + resultSet.updateNClob(columnLabel, reader); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNClob(final String columnLabel, final Reader reader, final long length) throws SQLException { + try { + resultSet.updateNClob(columnLabel, reader, length); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNString(final int columnIndex, final String value) throws SQLException { + try { + resultSet.updateNString(columnIndex, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNString(final String columnLabel, final String value) throws SQLException { + try { + resultSet.updateNString(columnLabel, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNull(final int columnIndex) throws SQLException { + try { + resultSet.updateNull(columnIndex); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateNull(final String columnName) throws SQLException { + try { + resultSet.updateNull(columnName); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateObject(final int columnIndex, final Object x) throws SQLException { + try { + resultSet.updateObject(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateObject(final int columnIndex, final Object x, final int scale) throws SQLException { + try { + resultSet.updateObject(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void updateObject(final int columnIndex, final Object x, final SQLType targetSqlType) throws SQLException { + try { + resultSet.updateObject(columnIndex, x, targetSqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void updateObject(final int columnIndex, final Object x, final SQLType targetSqlType, final int scaleOrLength) throws SQLException { + try { + resultSet.updateObject(columnIndex, x, targetSqlType, scaleOrLength); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateObject(final String columnName, final Object x) throws SQLException { + try { + resultSet.updateObject(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateObject(final String columnName, final Object x, final int scale) throws SQLException { + try { + resultSet.updateObject(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void updateObject(final String columnLabel, final Object x, final SQLType targetSqlType) throws SQLException { + try { + resultSet.updateObject(columnLabel, x, targetSqlType); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void updateObject(final String columnLabel, final Object x, final SQLType targetSqlType, final int scaleOrLength) + throws SQLException { + try { + resultSet.updateObject(columnLabel, x, targetSqlType, scaleOrLength); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateRef(final int columnIndex, final Ref x) throws SQLException { + try { + resultSet.updateRef(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateRef(final String columnName, final Ref x) throws SQLException { + try { + resultSet.updateRef(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateRow() throws SQLException { + try { + resultSet.updateRow(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateRowId(final int columnIndex, final RowId value) throws SQLException { + try { + resultSet.updateRowId(columnIndex, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateRowId(final String columnLabel, final RowId value) throws SQLException { + try { + resultSet.updateRowId(columnLabel, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateShort(final int columnIndex, final short x) throws SQLException { + try { + resultSet.updateShort(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateShort(final String columnName, final short x) throws SQLException { + try { + resultSet.updateShort(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateSQLXML(final int columnIndex, final SQLXML value) throws SQLException { + try { + resultSet.updateSQLXML(columnIndex, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateSQLXML(final String columnLabel, final SQLXML value) throws SQLException { + try { + resultSet.updateSQLXML(columnLabel, value); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateString(final int columnIndex, final String x) throws SQLException { + try { + resultSet.updateString(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateString(final String columnName, final String x) throws SQLException { + try { + resultSet.updateString(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateTime(final int columnIndex, final Time x) throws SQLException { + try { + resultSet.updateTime(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateTime(final String columnName, final Time x) throws SQLException { + try { + resultSet.updateTime(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateTimestamp(final int columnIndex, final Timestamp x) throws SQLException { + try { + resultSet.updateTimestamp(columnIndex, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void updateTimestamp(final String columnName, final Timestamp x) throws SQLException { + try { + resultSet.updateTimestamp(columnName, x); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public boolean wasNull() throws SQLException { + try { + return resultSet.wasNull(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java new file mode 100644 index 0000000..ff5d937 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java @@ -0,0 +1,811 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +/** + * A base delegating implementation of {@link Statement}. + *

    + * All of the methods from the {@link Statement} interface simply check to see that the {@link Statement} is active, and + * call the corresponding method on the "delegate" provided in my constructor. + *

    + * Extends AbandonedTrace to implement Statement tracking and logging of code which created the Statement. Tracking the + * Statement ensures that the Connection which created it can close any open Statement's on Connection close. + * + * @since 2.0 + */ +public class DelegatingStatement extends AbandonedTrace implements Statement { + + /** My delegate. */ + private Statement statement; + + /** The connection that created me. **/ + private DelegatingConnection connection; + + private boolean closed; + + /** + * Create a wrapper for the Statement which traces this Statement to the Connection which created it and the code + * which created it. + * + * @param statement + * the {@link Statement} to delegate all calls to. + * @param connection + * the {@link DelegatingConnection} that created this statement. + */ + public DelegatingStatement(final DelegatingConnection connection, final Statement statement) { + super(connection); + this.statement = statement; + this.connection = connection; + } + + /** + * @throws SQLException + * thrown by the delegating statement. + * @since 2.4.0 made public, was protected in 2.3.0. + */ + public void activate() throws SQLException { + if (statement instanceof DelegatingStatement) { + ((DelegatingStatement) statement).activate(); + } + } + + @Override + public void addBatch(final String sql) throws SQLException { + checkOpen(); + try { + statement.addBatch(sql); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void cancel() throws SQLException { + checkOpen(); + try { + statement.cancel(); + } catch (final SQLException e) { + handleException(e); + } + } + + protected void checkOpen() throws SQLException { + if (isClosed()) { + throw new SQLException(this.getClass().getName() + " with address: \"" + this.toString() + "\" is closed."); + } + } + + @Override + public void clearBatch() throws SQLException { + checkOpen(); + try { + statement.clearBatch(); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void clearWarnings() throws SQLException { + checkOpen(); + try { + statement.clearWarnings(); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * Close this DelegatingStatement, and close any ResultSets that were not explicitly closed. + */ + @Override + public void close() throws SQLException { + if (isClosed()) { + return; + } + final List thrownList = new ArrayList<>(); + try { + if (connection != null) { + connection.removeTrace(this); + connection = null; + } + + // The JDBC spec requires that a statement close any open + // ResultSet's when it is closed. + // FIXME The PreparedStatement we're wrapping should handle this for us. + // See bug 17301 for what could happen when ResultSets are closed twice. + final List traceList = getTrace(); + if (traceList != null) { + traceList.forEach(trace -> trace.close(e -> { + if (connection != null) { + // Does not rethrow e. + connection.handleExceptionNoThrow(e); + } + thrownList.add(e); + })); + clearTrace(); + } + Utils.close(statement, e -> { + if (connection != null) { + // Does not rethrow e. + connection.handleExceptionNoThrow(e); + } + thrownList.add(e); + }); + } finally { + closed = true; + statement = null; + if (!thrownList.isEmpty()) { + throw new SQLExceptionList(thrownList); + } + } + } + + @Override + public void closeOnCompletion() throws SQLException { + checkOpen(); + try { + Jdbc41Bridge.closeOnCompletion(statement); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public boolean execute(final String sql) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.execute(sql); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean execute(final String sql, final int autoGeneratedKeys) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.execute(sql, autoGeneratedKeys); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean execute(final String sql, final int[] columnIndexes) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.execute(sql, columnIndexes); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean execute(final String sql, final String[] columnNames) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.execute(sql, columnNames); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public int[] executeBatch() throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeBatch(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + /** + * @since 2.5.0 + */ + @Override + public long[] executeLargeBatch() throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeLargeBatch(); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * @since 2.5.0 + */ + @Override + public long executeLargeUpdate(final String sql) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeLargeUpdate(sql); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * @since 2.5.0 + */ + @Override + public long executeLargeUpdate(final String sql, final int autoGeneratedKeys) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeLargeUpdate(sql, autoGeneratedKeys); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * @since 2.5.0 + */ + @Override + public long executeLargeUpdate(final String sql, final int[] columnIndexes) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeLargeUpdate(sql, columnIndexes); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * @since 2.5.0 + */ + @Override + public long executeLargeUpdate(final String sql, final String[] columnNames) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeLargeUpdate(sql, columnNames); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public ResultSet executeQuery(final String sql) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return DelegatingResultSet.wrapResultSet(this, statement.executeQuery(sql)); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public int executeUpdate(final String sql) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeUpdate(sql); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int executeUpdate(final String sql, final int autoGeneratedKeys) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeUpdate(sql, autoGeneratedKeys); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int executeUpdate(final String sql, final int[] columnIndexes) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeUpdate(sql, columnIndexes); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int executeUpdate(final String sql, final String[] columnNames) throws SQLException { + checkOpen(); + setLastUsedInParent(); + try { + return statement.executeUpdate(sql, columnNames); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @SuppressWarnings("deprecation") // Need Commons DBCP to address this + @Override + protected void finalize() throws Throwable { + // This is required because of statement pooling. The poolable + // statements will always be strongly held by the statement pool. If the + // delegating statements that wrap the poolable statement are not + // strongly held they will be garbage collected but at that point the + // poolable statements need to be returned to the pool else there will + // be a leak of statements from the pool. Closing this statement will + // close all the wrapped statements and return any poolable statements + // to the pool. + close(); + super.finalize(); + } + + @Override + public Connection getConnection() throws SQLException { + checkOpen(); + return getConnectionInternal(); // return the delegating connection that created this + } + + protected DelegatingConnection getConnectionInternal() { + return connection; + } + + /** + * Returns my underlying {@link Statement}. + * + * @return my underlying {@link Statement}. + * @see #getInnermostDelegate + */ + public Statement getDelegate() { + return statement; + } + + @Override + public int getFetchDirection() throws SQLException { + checkOpen(); + try { + return statement.getFetchDirection(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getFetchSize() throws SQLException { + checkOpen(); + try { + return statement.getFetchSize(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(this, statement.getGeneratedKeys()); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + /** + * If my underlying {@link Statement} is not a {@code DelegatingStatement}, returns it, otherwise recursively + * invokes this method on my delegate. + *

    + * Hence this method will return the first delegate that is not a {@code DelegatingStatement} or {@code null} when + * no non-{@code DelegatingStatement} delegate can be found by traversing this chain. + *

    + *

    + * This method is useful when you may have nested {@code DelegatingStatement}s, and you want to make sure to obtain + * a "genuine" {@link Statement}. + *

    + * + * @return The innermost delegate. + * + * @see #getDelegate + */ + public Statement getInnermostDelegate() { + Statement s = statement; + while (s instanceof DelegatingStatement) { + s = ((DelegatingStatement) s).getDelegate(); + if (this == s) { + return null; + } + } + return s; + } + + /** + * @since 2.5.0 + */ + @Override + public long getLargeMaxRows() throws SQLException { + checkOpen(); + try { + return statement.getLargeMaxRows(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + /** + * @since 2.5.0 + */ + @Override + public long getLargeUpdateCount() throws SQLException { + checkOpen(); + try { + return statement.getLargeUpdateCount(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxFieldSize() throws SQLException { + checkOpen(); + try { + return statement.getMaxFieldSize(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getMaxRows() throws SQLException { + checkOpen(); + try { + return statement.getMaxRows(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public boolean getMoreResults() throws SQLException { + checkOpen(); + try { + return statement.getMoreResults(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean getMoreResults(final int current) throws SQLException { + checkOpen(); + try { + return statement.getMoreResults(current); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public int getQueryTimeout() throws SQLException { + checkOpen(); + try { + return statement.getQueryTimeout(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public ResultSet getResultSet() throws SQLException { + checkOpen(); + try { + return DelegatingResultSet.wrapResultSet(this, statement.getResultSet()); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + @Override + public int getResultSetConcurrency() throws SQLException { + checkOpen(); + try { + return statement.getResultSetConcurrency(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getResultSetHoldability() throws SQLException { + checkOpen(); + try { + return statement.getResultSetHoldability(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getResultSetType() throws SQLException { + checkOpen(); + try { + return statement.getResultSetType(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public int getUpdateCount() throws SQLException { + checkOpen(); + try { + return statement.getUpdateCount(); + } catch (final SQLException e) { + handleException(e); + return 0; + } + } + + @Override + public SQLWarning getWarnings() throws SQLException { + checkOpen(); + try { + return statement.getWarnings(); + } catch (final SQLException e) { + handleException(e); + throw new AssertionError(); + } + } + + protected void handleException(final SQLException e) throws SQLException { + if (connection == null) { + throw e; + } + connection.handleException(e); + } + + /* + * Note: This method was protected prior to JDBC 4. + */ + @Override + public boolean isClosed() throws SQLException { + return closed; + } + + protected boolean isClosedInternal() { + return closed; + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + checkOpen(); + try { + return Jdbc41Bridge.isCloseOnCompletion(statement); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isPoolable() throws SQLException { + checkOpen(); + try { + return statement.isPoolable(); + } catch (final SQLException e) { + handleException(e); + return false; + } + } + + @Override + public boolean isWrapperFor(final Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return true; + } + if (iface.isAssignableFrom(statement.getClass())) { + return true; + } + return statement.isWrapperFor(iface); + } + + /** + * @throws SQLException + * thrown by the delegating statement. + * @since 2.4.0 made public, was protected in 2.3.0. + */ + public void passivate() throws SQLException { + if (statement instanceof DelegatingStatement) { + ((DelegatingStatement) statement).passivate(); + } + } + + protected void setClosedInternal(final boolean closed) { + this.closed = closed; + } + + @Override + public void setCursorName(final String name) throws SQLException { + checkOpen(); + try { + statement.setCursorName(name); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * Sets my delegate. + * + * @param statement + * my delegate. + */ + public void setDelegate(final Statement statement) { + this.statement = statement; + } + + @Override + public void setEscapeProcessing(final boolean enable) throws SQLException { + checkOpen(); + try { + statement.setEscapeProcessing(enable); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setFetchDirection(final int direction) throws SQLException { + checkOpen(); + try { + statement.setFetchDirection(direction); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setFetchSize(final int rows) throws SQLException { + checkOpen(); + try { + statement.setFetchSize(rows); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * @since 2.5.0 + */ + @Override + public void setLargeMaxRows(final long max) throws SQLException { + checkOpen(); + try { + statement.setLargeMaxRows(max); + } catch (final SQLException e) { + handleException(e); + } + } + + private void setLastUsedInParent() { + if (connection != null) { + connection.setLastUsed(); + } + } + + @Override + public void setMaxFieldSize(final int max) throws SQLException { + checkOpen(); + try { + statement.setMaxFieldSize(max); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setMaxRows(final int max) throws SQLException { + checkOpen(); + try { + statement.setMaxRows(max); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setPoolable(final boolean poolable) throws SQLException { + checkOpen(); + try { + statement.setPoolable(poolable); + } catch (final SQLException e) { + handleException(e); + } + } + + @Override + public void setQueryTimeout(final int seconds) throws SQLException { + checkOpen(); + try { + statement.setQueryTimeout(seconds); + } catch (final SQLException e) { + handleException(e); + } + } + + /** + * Returns a String representation of this object. + * + * @return String + */ + @Override + public synchronized String toString() { + return statement == null ? "NULL" : statement.toString(); + } + + @Override + public T unwrap(final Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return iface.cast(this); + } + if (iface.isAssignableFrom(statement.getClass())) { + return iface.cast(statement); + } + return statement.unwrap(iface); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java new file mode 100644 index 0000000..a575291 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java @@ -0,0 +1,84 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.SQLException; +import java.util.Properties; + +/** + * A {@link Driver}-based implementation of {@link ConnectionFactory}. + * + * @since 2.0 + */ +public class DriverConnectionFactory implements ConnectionFactory { + + private final String connectionString; + + private final Driver driver; + + private final Properties properties; + + /** + * Constructs a connection factory for a given Driver. + * + * @param driver The Driver. + * @param connectString The connection string. + * @param properties The connection properties. + */ + public DriverConnectionFactory(final Driver driver, final String connectString, final Properties properties) { + this.driver = driver; + this.connectionString = connectString; + this.properties = properties; + } + + @Override + public Connection createConnection() throws SQLException { + return driver.connect(connectionString, properties); + } + + /** + * @return The connection String. + * @since 2.6.0 + */ + public String getConnectionString() { + return connectionString; + } + + /** + * @return The Driver. + * @since 2.6.0 + */ + public Driver getDriver() { + return driver; + } + + /** + * @return The Properties. + * @since 2.6.0 + */ + public Properties getProperties() { + return properties; + } + + @Override + public String toString() { + return this.getClass().getName() + " [" + driver + ";" + connectionString + ";" + + Utils.cloneWithoutCredentials(properties) + "]"; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java new file mode 100644 index 0000000..bacc31f --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java @@ -0,0 +1,80 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; + +/* + * Creates {@link Driver} instances. + * + * @since 2.7.0 + */ +final class DriverFactory { + + static Driver createDriver(final BasicDataSource basicDataSource) throws SQLException { + // Load the JDBC driver class + Driver driverToUse = basicDataSource.getDriver(); + final String driverClassName = basicDataSource.getDriverClassName(); + final ClassLoader driverClassLoader = basicDataSource.getDriverClassLoader(); + final String url = basicDataSource.getUrl(); + + if (driverToUse == null) { + Class driverFromCCL = null; + if (driverClassName != null) { + try { + try { + if (driverClassLoader == null) { + driverFromCCL = Class.forName(driverClassName); + } else { + driverFromCCL = Class.forName(driverClassName, true, driverClassLoader); + } + } catch (final ClassNotFoundException cnfe) { + driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName); + } + } catch (final Exception t) { + final String message = "Cannot load JDBC driver class '" + driverClassName + "'"; + basicDataSource.log(message, t); + throw new SQLException(message, t); + } + } + + try { + if (driverFromCCL == null) { + driverToUse = DriverManager.getDriver(url); + } else { + // Usage of DriverManager is not possible, as it does not + // respect the ContextClassLoader + // N.B. This cast may cause ClassCastException which is + // handled below + driverToUse = (Driver) driverFromCCL.getConstructor().newInstance(); + if (!driverToUse.acceptsURL(url)) { + throw new SQLException("No suitable driver", "08001"); + } + } + } catch (final Exception t) { + final String message = "Cannot create JDBC driver of class '" + + (driverClassName != null ? driverClassName : "") + "' for connect URL '" + url + "'"; + basicDataSource.log(message, t); + throw new SQLException(message, t); + } + } + return driverToUse; + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java new file mode 100644 index 0000000..5607585 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java @@ -0,0 +1,148 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +/** + * A {@link DriverManager}-based implementation of {@link ConnectionFactory}. + * + * @since 2.0 + */ +public class DriverManagerConnectionFactory implements ConnectionFactory { + + static { + // Related to DBCP-212 + // Driver manager does not sync loading of drivers that use the service + // provider interface. This will cause issues is multi-threaded + // environments. This hack makes sure the drivers are loaded before + // DBCP tries to use them. + DriverManager.getDrivers(); + } + + private final String connectionUri; + + private final String userName; + + private final char[] userPassword; + + private final Properties properties; + + /** + * Constructor for DriverManagerConnectionFactory. + * + * @param connectionUri + * a database connection string of the form {@code jdbc:subprotocol:subname} + * @since 2.2 + */ + public DriverManagerConnectionFactory(final String connectionUri) { + this.connectionUri = connectionUri; + this.properties = new Properties(); + this.userName = null; + this.userPassword = null; + } + + /** + * Constructor for DriverManagerConnectionFactory. + * + * @param connectionUri + * a database connection string of the form {@code jdbc:subprotocol:subname} + * @param properties + * a list of arbitrary string tag/value pairs as connection arguments; normally at least a "user" and + * "password" property should be included. + */ + public DriverManagerConnectionFactory(final String connectionUri, final Properties properties) { + this.connectionUri = connectionUri; + this.properties = properties; + this.userName = null; + this.userPassword = null; + } + + /** + * Constructor for DriverManagerConnectionFactory. + * + * @param connectionUri + * a database connection string of the form {@code jdbc:subprotocol:subname} + * @param userName + * the database user + * @param userPassword + * the user's password + */ + public DriverManagerConnectionFactory(final String connectionUri, final String userName, + final char[] userPassword) { + this.connectionUri = connectionUri; + this.userName = userName; + this.userPassword = Utils.clone(userPassword); + this.properties = null; + } + + /** + * Constructor for DriverManagerConnectionFactory. + * + * @param connectionUri + * a database connection string of the form {@code jdbc:subprotocol:subname} + * @param userName + * the database user + * @param userPassword + * the user's password + */ + public DriverManagerConnectionFactory(final String connectionUri, final String userName, + final String userPassword) { + this.connectionUri = connectionUri; + this.userName = userName; + this.userPassword = Utils.toCharArray(userPassword); + this.properties = null; + } + + @Override + public Connection createConnection() throws SQLException { + if (null == properties) { + if (userName == null && userPassword == null) { + return DriverManager.getConnection(connectionUri); + } + return DriverManager.getConnection(connectionUri, userName, Utils.toString(userPassword)); + } + return DriverManager.getConnection(connectionUri, properties); + } + + /** + * @return The connection URI. + * @since 2.6.0 + */ + public String getConnectionUri() { + return connectionUri; + } + + /** + * @return The Properties. + * @since 2.6.0 + */ + public Properties getProperties() { + return properties; + } + + /** + * @return The user name. + * @since 2.6.0 + */ + public String getUserName() { + return userName; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java b/java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java new file mode 100644 index 0000000..baf1a2a --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java @@ -0,0 +1,488 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.Date; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.sql.CommonDataSource; + +/** + * Defines bridge methods to JDBC 4.1 (Java 7) methods to allow call sites to operate safely (without + * {@link AbstractMethodError}) when using a JDBC driver written for JDBC 4.0 (Java 6). + *

    + * There should be no need to this kind of code for JDBC 4.2 in Java 8 due to JDBC's use of default methods. + *

    + *

    + * This should probably be moved or at least copied in some form to Apache Commons DbUtils. + *

    + * + * @since 2.6.0 + */ +public class Jdbc41Bridge { + + /** + * Delegates to {@link Connection#abort(Executor)} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link Connection#abort(Executor)}, then call {@link Connection#close()}. + *

    + * + * @param connection + * the receiver + * @param executor + * See {@link Connection#abort(Executor)}. + * @throws SQLException + * See {@link Connection#abort(Executor)}. + * @see Connection#abort(Executor) + */ + public static void abort(final Connection connection, final Executor executor) throws SQLException { + try { + connection.abort(executor); + } catch (final AbstractMethodError e) { + connection.close(); + } + } + + /** + * Delegates to {@link Statement#closeOnCompletion()} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link Statement#closeOnCompletion()}, then just check that the connection + * is closed to then throw an SQLException. + *

    + * + * @param statement + * See {@link Statement#closeOnCompletion()} + * @throws SQLException + * See {@link Statement#closeOnCompletion()} + * @see Statement#closeOnCompletion() + */ + public static void closeOnCompletion(final Statement statement) throws SQLException { + try { + statement.closeOnCompletion(); + } catch (final AbstractMethodError e) { + if (statement.isClosed()) { + throw new SQLException("Statement closed"); + } + } + } + + /** + * Delegates to {@link DatabaseMetaData#generatedKeyAlwaysReturned()} without throwing a + * {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link DatabaseMetaData#generatedKeyAlwaysReturned()}, then return false. + *

    + * + * @param databaseMetaData + * See {@link DatabaseMetaData#generatedKeyAlwaysReturned()} + * @return See {@link DatabaseMetaData#generatedKeyAlwaysReturned()} + * @throws SQLException + * See {@link DatabaseMetaData#generatedKeyAlwaysReturned()} + * @see DatabaseMetaData#generatedKeyAlwaysReturned() + */ + public static boolean generatedKeyAlwaysReturned(final DatabaseMetaData databaseMetaData) throws SQLException { + try { + return databaseMetaData.generatedKeyAlwaysReturned(); + } catch (final AbstractMethodError e) { + // do nothing + return false; + } + } + + /** + * Delegates to {@link Connection#getNetworkTimeout()} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link Connection#getNetworkTimeout()}, then return 0. + *

    + * + * @param connection + * the receiver + * @return See {@link Connection#getNetworkTimeout()} + * @throws SQLException + * See {@link Connection#getNetworkTimeout()} + * @see Connection#getNetworkTimeout() + */ + public static int getNetworkTimeout(final Connection connection) throws SQLException { + try { + return connection.getNetworkTimeout(); + } catch (final AbstractMethodError e) { + return 0; + } + } + + /** + * Delegates to {@link ResultSet#getObject(int, Class)} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link ResultSet#getObject(int, Class)}, then return 0. + *

    + * + * @param + * See {@link ResultSet#getObject(int, Class)} + * @param resultSet + * See {@link ResultSet#getObject(int, Class)} + * @param columnIndex + * See {@link ResultSet#getObject(int, Class)} + * @param type + * See {@link ResultSet#getObject(int, Class)} + * @return See {@link ResultSet#getObject(int, Class)} + * @throws SQLException + * See {@link ResultSet#getObject(int, Class)} + * @see ResultSet#getObject(int, Class) + */ + @SuppressWarnings("unchecked") + public static T getObject(final ResultSet resultSet, final int columnIndex, final Class type) + throws SQLException { + try { + return resultSet.getObject(columnIndex, type); + } catch (final AbstractMethodError e) { + if (type == String.class) { + return (T) resultSet.getString(columnIndex); + } + // Numbers + if (type == Integer.class) { + return (T) Integer.valueOf(resultSet.getInt(columnIndex)); + } + if (type == Long.class) { + return (T) Long.valueOf(resultSet.getLong(columnIndex)); + } + if (type == Double.class) { + return (T) Double.valueOf(resultSet.getDouble(columnIndex)); + } + if (type == Float.class) { + return (T) Float.valueOf(resultSet.getFloat(columnIndex)); + } + if (type == Short.class) { + return (T) Short.valueOf(resultSet.getShort(columnIndex)); + } + if (type == BigDecimal.class) { + return (T) resultSet.getBigDecimal(columnIndex); + } + if (type == Byte.class) { + return (T) Byte.valueOf(resultSet.getByte(columnIndex)); + } + // Dates + if (type == Date.class) { + return (T) resultSet.getDate(columnIndex); + } + if (type == Time.class) { + return (T) resultSet.getTime(columnIndex); + } + if (type == Timestamp.class) { + return (T) resultSet.getTimestamp(columnIndex); + } + // Streams + if (type == InputStream.class) { + return (T) resultSet.getBinaryStream(columnIndex); + } + if (type == Reader.class) { + return (T) resultSet.getCharacterStream(columnIndex); + } + // Other + if (type == Object.class) { + return (T) resultSet.getObject(columnIndex); + } + if (type == Boolean.class) { + return (T) Boolean.valueOf(resultSet.getBoolean(columnIndex)); + } + if (type == Array.class) { + return (T) resultSet.getArray(columnIndex); + } + if (type == Blob.class) { + return (T) resultSet.getBlob(columnIndex); + } + if (type == Clob.class) { + return (T) resultSet.getClob(columnIndex); + } + if (type == Ref.class) { + return (T) resultSet.getRef(columnIndex); + } + if (type == RowId.class) { + return (T) resultSet.getRowId(columnIndex); + } + if (type == SQLXML.class) { + return (T) resultSet.getSQLXML(columnIndex); + } + if (type == URL.class) { + return (T) resultSet.getURL(columnIndex); + } + throw new SQLFeatureNotSupportedException( + String.format("resultSet=%s, columnIndex=%,d, type=%s", resultSet, Integer.valueOf(columnIndex), type)); + } + } + + /** + * Delegates to {@link ResultSet#getObject(String, Class)} without throwing an {@link AbstractMethodError}. + * + * @param + * See {@link ResultSet#getObject(String, Class)} + * @param resultSet + * See {@link ResultSet#getObject(String, Class)} + * @param columnLabel + * See {@link ResultSet#getObject(String, Class)} + * @param type + * See {@link ResultSet#getObject(String, Class)} + * @return See {@link ResultSet#getObject(String, Class)} + * @throws SQLException + * See {@link ResultSet#getObject(String, Class)} + * @see ResultSet#getObject(int, Class) + */ + @SuppressWarnings("unchecked") + public static T getObject(final ResultSet resultSet, final String columnLabel, final Class type) + throws SQLException { + try { + return resultSet.getObject(columnLabel, type); + } catch (final AbstractMethodError e) { + // Numbers + if (type == Integer.class) { + return (T) Integer.valueOf(resultSet.getInt(columnLabel)); + } + if (type == Long.class) { + return (T) Long.valueOf(resultSet.getLong(columnLabel)); + } + if (type == Double.class) { + return (T) Double.valueOf(resultSet.getDouble(columnLabel)); + } + if (type == Float.class) { + return (T) Float.valueOf(resultSet.getFloat(columnLabel)); + } + if (type == Short.class) { + return (T) Short.valueOf(resultSet.getShort(columnLabel)); + } + if (type == BigDecimal.class) { + return (T) resultSet.getBigDecimal(columnLabel); + } + if (type == Byte.class) { + return (T) Byte.valueOf(resultSet.getByte(columnLabel)); + } + // Dates + if (type == Date.class) { + return (T) resultSet.getDate(columnLabel); + } + if (type == Time.class) { + return (T) resultSet.getTime(columnLabel); + } + if (type == Timestamp.class) { + return (T) resultSet.getTimestamp(columnLabel); + } + // Streams + if (type == InputStream.class) { + return (T) resultSet.getBinaryStream(columnLabel); + } + if (type == Reader.class) { + return (T) resultSet.getCharacterStream(columnLabel); + } + // Other + if (type == Object.class) { + return (T) resultSet.getObject(columnLabel); + } + if (type == Boolean.class) { + return (T) Boolean.valueOf(resultSet.getBoolean(columnLabel)); + } + if (type == Array.class) { + return (T) resultSet.getArray(columnLabel); + } + if (type == Blob.class) { + return (T) resultSet.getBlob(columnLabel); + } + if (type == Clob.class) { + return (T) resultSet.getClob(columnLabel); + } + if (type == Ref.class) { + return (T) resultSet.getRef(columnLabel); + } + if (type == RowId.class) { + return (T) resultSet.getRowId(columnLabel); + } + if (type == SQLXML.class) { + return (T) resultSet.getSQLXML(columnLabel); + } + if (type == URL.class) { + return (T) resultSet.getURL(columnLabel); + } + throw new SQLFeatureNotSupportedException( + String.format("resultSet=%s, columnLabel=%s, type=%s", resultSet, columnLabel, type)); + } + } + + /** + * Delegates to {@link CommonDataSource#getParentLogger()} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link CommonDataSource#getParentLogger()}, then return null. + *

    + * + * @param commonDataSource + * See {@link CommonDataSource#getParentLogger()} + * @return See {@link CommonDataSource#getParentLogger()} + * @throws SQLFeatureNotSupportedException + * See {@link CommonDataSource#getParentLogger()} + */ + public static Logger getParentLogger(final CommonDataSource commonDataSource) throws SQLFeatureNotSupportedException { + try { + return commonDataSource.getParentLogger(); + } catch (final AbstractMethodError e) { + throw new SQLFeatureNotSupportedException("javax.sql.CommonDataSource#getParentLogger()"); + } + } + + /** + * Delegates to {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)} without throwing a + * {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)}, + * then return null. + *

    + * + * @param databaseMetaData + * the receiver + * @param catalog + * See {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)} + * @param schemaPattern + * See {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)} + * @param tableNamePattern + * See {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)} + * @param columnNamePattern + * See {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)} + * @return See {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)} + * @throws SQLException + * See {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)} + * @see DatabaseMetaData#getPseudoColumns(String, String, String, String) + */ + public static ResultSet getPseudoColumns(final DatabaseMetaData databaseMetaData, final String catalog, + final String schemaPattern, final String tableNamePattern, final String columnNamePattern) + throws SQLException { + try { + return databaseMetaData.getPseudoColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); + } catch (final AbstractMethodError e) { + // do nothing + return null; + } + } + + /** + * Delegates to {@link Connection#getSchema()} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link Connection#getSchema()}, then return null. + *

    + * + * @param connection + * the receiver + * @return null for a JDBC 4 driver or a value per {@link Connection#getSchema()}. + * @throws SQLException + * See {@link Connection#getSchema()}. + * @see Connection#getSchema() + */ + public static String getSchema(final Connection connection) throws SQLException { + try { + return connection.getSchema(); + } catch (final AbstractMethodError e) { + // do nothing + return null; + } + } + + /** + * Delegates to {@link Statement#isCloseOnCompletion()} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link Statement#isCloseOnCompletion()}, then just check that the + * connection is closed to then throw an SQLException. + *

    + * + * @param statement + * See {@link Statement#isCloseOnCompletion()} + * @return See {@link Statement#isCloseOnCompletion()} + * @throws SQLException + * See {@link Statement#isCloseOnCompletion()} + * @see Statement#closeOnCompletion() + */ + public static boolean isCloseOnCompletion(final Statement statement) throws SQLException { + try { + return statement.isCloseOnCompletion(); + } catch (final AbstractMethodError e) { + if (statement.isClosed()) { + throw new SQLException("Statement closed"); + } + return false; + } + } + + /** + * Delegates to {@link Connection#setNetworkTimeout(Executor, int)} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link Connection#setNetworkTimeout(Executor, int)}, then do nothing. + *

    + * + * @param connection + * the receiver + * @param executor + * See {@link Connection#setNetworkTimeout(Executor, int)} + * @param milliseconds + * {@link Connection#setNetworkTimeout(Executor, int)} + * @throws SQLException + * {@link Connection#setNetworkTimeout(Executor, int)} + * @see Connection#setNetworkTimeout(Executor, int) + */ + public static void setNetworkTimeout(final Connection connection, final Executor executor, final int milliseconds) + throws SQLException { + try { + connection.setNetworkTimeout(executor, milliseconds); + } catch (final AbstractMethodError ignored) { + // do nothing + } + } + + /** + * Delegates to {@link Connection#setSchema(String)} without throwing an {@link AbstractMethodError}. + *

    + * If the JDBC driver does not implement {@link Connection#setSchema(String)}, then do nothing. + *

    + * + * @param connection + * the receiver + * @param schema + * See {@link Connection#setSchema(String)}. + * @throws SQLException + * See {@link Connection#setSchema(String)}. + * @see Connection#setSchema(String) + */ + public static void setSchema(final Connection connection, final String schema) throws SQLException { + try { + connection.setSchema(schema); + } catch (final AbstractMethodError ignored) { + // do nothing + } + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java b/java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java new file mode 100644 index 0000000..6f9f84e --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java @@ -0,0 +1,44 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.SQLException; + +/** + * Exception thrown when a connection's maximum lifetime has been exceeded. + * + * @since 2.1 + */ +final class LifetimeExceededException extends SQLException { + + private static final long serialVersionUID = -3783783104516492659L; + + /** + * Constructs a new instance. + */ + LifetimeExceededException() { + } + + /** + * Constructs a new instance with the given message. + * + * @param reason a description of the exception + */ + LifetimeExceededException(final String reason) { + super(reason); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/ListException.java b/java/org/apache/tomcat/dbcp/dbcp2/ListException.java new file mode 100644 index 0000000..5c69768 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/ListException.java @@ -0,0 +1,56 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.util.List; + +/** + * An exception wrapping a list of exceptions. + * + * @since 2.4.0 + */ +public class ListException extends Exception { + + private static final long serialVersionUID = 1L; + + private final List exceptionList; + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message + * the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} + * method. + * @param exceptionList + * a list of exceptions. + */ + public ListException(final String message, final List exceptionList) { + super(message); + this.exceptionList = exceptionList; + } + + /** + * Gets the list of exceptions. + * + * @return the list of exceptions. + */ + public List getExceptionList() { + return exceptionList; + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties new file mode 100644 index 0000000..cf0efed --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties @@ -0,0 +1,26 @@ +# 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. + +connectionFactory.lifetimeExceeded=The lifetime of the connection [{0}] exceeds the maximum permitted value of [{1}]. + +pool.close.fail=Cannot close connection pool. + +poolableConnection.validate.fastFail=Fatal SQLException was thrown previously on this connection. + +poolableConnectionFactory.validateObject.fail=Failed to validate a poolable connection. + +poolingDataSource.factoryConfig=PoolableConnectionFactory not linked to pool. Calling setPool() to fix the configuration. + +swallowedExceptionLogger.onSwallowedException=An internal object pool swallowed an Exception. diff --git a/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_de.properties b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_de.properties new file mode 100644 index 0000000..fe252e4 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +poolableConnection.validate.fastFail=Fatale SQLException wurde bereits vorher von dieser Verbindung geworfen diff --git a/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_fr.properties b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_fr.properties new file mode 100644 index 0000000..090c60d --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_fr.properties @@ -0,0 +1,26 @@ +# 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. + +connectionFactory.lifetimeExceeded=La durée de vie de la connection de [{0}] millisecondes excède la valeur maximale permis de [{1}] millisecondes + +pool.close.fail=Impossible de fermer le pool de connections + +poolableConnection.validate.fastFail=Une exception fatale SQLException avait déjà été lancée pour cette connection + +poolableConnectionFactory.validateObject.fail=Impossible de valider la connection poolable + +poolingDataSource.factoryConfig=La PoolableConnectionFactory n'est pas liée au pool, il faut appeler setPool() pour y remédier et corriger la configuration + +swallowedExceptionLogger.onSwallowedException=Un object interne du pool a avalé une exception diff --git a/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_ja.properties b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_ja.properties new file mode 100644 index 0000000..fbfe2a6 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_ja.properties @@ -0,0 +1,26 @@ +# 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. + +connectionFactory.lifetimeExceeded=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}] ミリ秒ã®æœ‰åŠ¹æœŸé–“㌠[{1}] ミリ秒ã®è¨±å®¹æœ€å¤§å€¤ã‚’超ãˆã¦ã„ã¾ã™ + +pool.close.fail=コãƒã‚¯ã‚·ãƒ§ãƒ³ãƒ—ールをåœæ­¢ã§ãã¾ã›ã‚“。 + +poolableConnection.validate.fastFail=ã“ã®ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã¯éŽåŽ»ã«è‡´å‘½çš„㪠SQLException ã‚’é€å‡ºã—ãŸã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚ + +poolableConnectionFactory.validateObject.fail=プールå¯èƒ½ãªã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã‚’検証ã§ãã¾ã›ã‚“。 + +poolingDataSource.factoryConfig=PoolableConnectionFactory ãŒã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ãƒ—ールã«æŽ¥ç¶šã—ã¦ã„ã¾ã›ã‚“。構æˆã‚’修復ã™ã‚‹ã«ã¯ setPool() を呼ã³å‡ºã—ã¦ãã ã•ã„。 + +swallowedExceptionLogger.onSwallowedException=内部オブジェクトプールãŒä¾‹å¤–を飲ã¿è¾¼ã¿ã¾ã—ãŸã€‚ diff --git a/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_ko.properties b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_ko.properties new file mode 100644 index 0000000..724e0ea --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_ko.properties @@ -0,0 +1,26 @@ +# 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. + +connectionFactory.lifetimeExceeded=해당 ì—°ê²°ì˜ ì¡´ì†ì‹œê°„ [{0}] 밀리초가, 최대 í—ˆìš©ì¹˜ì¸ [{1}] 밀리초를 초과합니다. + +pool.close.fail=ë°ì´í„°ë² ì´ìŠ¤ ì—°ê²° í’€ì„ ë‹«ì„ ìˆ˜ 없습니다. + +poolableConnection.validate.fastFail=ì´ ì—°ê²°ì—ì„œ, 심ê°í•œ SQLExceptionì´ ì´ì „ì— ë°œìƒí–ˆìŠµë‹ˆë‹¤. + +poolableConnectionFactory.validateObject.fail=Poolable connectionì´ ìœ íš¨í•œì§€ 확ì¸í•˜ì§€ 못했습니다. + +poolingDataSource.factoryConfig=PoolableConnectionFactoryê°€ í’€ì— ì—°ê²°ë˜ì§€ 않았습니다. setPool()ì„ í˜¸ì¶œí•˜ì—¬ ì´ ì„¤ì • 문제를 해결합니다. + +swallowedExceptionLogger.onSwallowedException=내부 ê°ì²´ í’€ì´ ì˜ˆì™¸ ë°œìƒì„ 무시했습니다. diff --git a/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..f557591 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings_zh_CN.properties @@ -0,0 +1,26 @@ +# 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. + +connectionFactory.lifetimeExceeded=连接的生存期[{0}]毫秒超过了å…许的最大值[{1}]毫秒。 + +pool.close.fail=无法关闭连接池。 + +poolableConnection.validate.fastFail=此连接上预先抛出了致命的 SQLException。 + +poolableConnectionFactory.validateObject.fail=无法验è¯å¯å…±ç”¨è¿žæŽ¥ã€‚ + +poolingDataSource.factoryConfig=PoolableConnectionFactory 未连接到连接池。请调用 setPool() ä¿®å¤æ­¤é…置。 + +swallowedExceptionLogger.onSwallowedException=一个内部对象池åžå¹¶äº†ä¸€ä¸ªå¼‚常。 diff --git a/java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java b/java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java new file mode 100644 index 0000000..5f81329 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java @@ -0,0 +1,104 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.lang.management.ManagementFactory; +import java.util.Objects; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Internal wrapper class that allows JMX to be a noop if absent or disabled. + * + * @since 2.2.1 + */ +final class ObjectNameWrapper { + + private static final Log log = LogFactory.getLog(ObjectNameWrapper.class); + + private static final MBeanServer MBEAN_SERVER = getPlatformMBeanServer(); + + private static MBeanServer getPlatformMBeanServer() { + try { + return ManagementFactory.getPlatformMBeanServer(); + } catch (final LinkageError | Exception e) { + // ignore - JMX not available + log.debug("Failed to get platform MBeanServer", e); + return null; + } + } + + public static ObjectName unwrap(final ObjectNameWrapper wrapper) { + return wrapper == null ? null : wrapper.unwrap(); + } + + public static ObjectNameWrapper wrap(final ObjectName objectName) { + return new ObjectNameWrapper(objectName); + } + + public static ObjectNameWrapper wrap(final String name) throws MalformedObjectNameException { + return wrap(new ObjectName(name)); + } + + private final ObjectName objectName; + + ObjectNameWrapper(final ObjectName objectName) { + this.objectName = objectName; + } + + public void registerMBean(final Object object) { + if (MBEAN_SERVER == null || objectName == null) { + return; + } + try { + MBEAN_SERVER.registerMBean(object, objectName); + } catch (final LinkageError | Exception e) { + log.warn("Failed to complete JMX registration for " + objectName, e); + } + } + + /** + * @since 2.7.0 + */ + @Override + public String toString() { + return Objects.toString(objectName); + } + + public void unregisterMBean() { + if (MBEAN_SERVER == null || objectName == null) { + return; + } + if (MBEAN_SERVER.isRegistered(objectName)) { + try { + MBEAN_SERVER.unregisterMBean(objectName); + } catch (final LinkageError | Exception e) { + log.warn("Failed to complete JMX unregistration for " + objectName, e); + } + } + } + + public ObjectName unwrap() { + return objectName; + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java b/java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java new file mode 100644 index 0000000..6052f7d --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java @@ -0,0 +1,681 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; + +import org.apache.tomcat.dbcp.dbcp2.PoolingConnection.StatementType; + +/** + * A key uniquely identifying {@link java.sql.PreparedStatement PreparedStatement}s. + * + * @since 2.0 + */ +public class PStmtKey { + + /** + * Interface for Prepared or Callable Statement. + */ + @FunctionalInterface + private interface StatementBuilder { + Statement createStatement(Connection connection, PStmtKey key) throws SQLException; + } + + private static final StatementBuilder CallConcurrency = (c, k) -> c.prepareCall(k.sql, k.resultSetType.intValue(), k.resultSetConcurrency.intValue()); + private static final StatementBuilder CallHoldability = (c, k) -> c.prepareCall(k.sql, k.resultSetType.intValue(), k.resultSetConcurrency.intValue(), k.resultSetHoldability.intValue()); + private static final StatementBuilder CallSQL = (c, k) -> c.prepareCall(k.sql); + private static final StatementBuilder StatementAutoGeneratedKeys = (c, k) -> c.prepareStatement(k.sql, k.autoGeneratedKeys.intValue()); + private static final StatementBuilder StatementColumnIndexes = (c, k) -> c.prepareStatement(k.sql, k.columnIndexes); + private static final StatementBuilder StatementColumnNames = (c, k) -> c.prepareStatement(k.sql, k.columnNames); + private static final StatementBuilder StatementConcurrency = (c, k) -> c.prepareStatement(k.sql, k.resultSetType.intValue(), k.resultSetConcurrency.intValue()); + private static final StatementBuilder StatementHoldability = (c, k) -> c.prepareStatement(k.sql, k.resultSetType.intValue(), k.resultSetConcurrency.intValue(), + k.resultSetHoldability.intValue()); + private static final StatementBuilder StatementSQL = (c, k) -> c.prepareStatement(k.sql); + + private static StatementBuilder match(final StatementType statementType, final StatementBuilder prep, final StatementBuilder call) { + switch (Objects.requireNonNull(statementType, "statementType")) { + case PREPARED_STATEMENT: + return prep; + case CALLABLE_STATEMENT: + return call; + default: + throw new IllegalArgumentException(statementType.toString()); + } + } + + /** + * SQL defining Prepared or Callable Statement + */ + private final String sql; + + /** + * Result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or + * {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + */ + private final Integer resultSetType; + + /** + * Result set concurrency. A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + */ + private final Integer resultSetConcurrency; + + /** + * Result set holdability. One of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} + * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + */ + private final Integer resultSetHoldability; + + /** + * Database catalog. + */ + private final String catalog; + + /** + * Database schema. + */ + private final String schema; + + /** + * A flag indicating whether auto-generated keys should be returned; one of {@code Statement.RETURN_GENERATED_KEYS} or + * {@code Statement.NO_GENERATED_KEYS}. + */ + private final Integer autoGeneratedKeys; + + /** + * An array of column indexes indicating the columns that should be returned from the inserted row or rows. + */ + private final int[] columnIndexes; + + /** + * An array of column names indicating the columns that should be returned from the inserted row or rows. + */ + private final String[] columnNames; + + /** + * Statement builder. + */ + private final transient StatementBuilder statementBuilder; + + /** + * Statement type, prepared or callable. + */ + private final StatementType statementType; + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @deprecated Use {@link #PStmtKey(String, String, String)}. + */ + @Deprecated + public PStmtKey(final String sql) { + this(sql, null, StatementType.PREPARED_STATEMENT); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @deprecated Use {@link #PStmtKey(String, String, String, int, int)}. + */ + @Deprecated + public PStmtKey(final String sql, final int resultSetType, final int resultSetConcurrency) { + this(sql, null, resultSetType, resultSetConcurrency, StatementType.PREPARED_STATEMENT); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @deprecated Use {@link #PStmtKey(String, String, String)}. + */ + @Deprecated + public PStmtKey(final String sql, final String catalog) { + this(sql, catalog, StatementType.PREPARED_STATEMENT); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param autoGeneratedKeys A flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * @deprecated Use {@link #PStmtKey(String, String, String, int)}. + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final int autoGeneratedKeys) { + this(sql, catalog, StatementType.PREPARED_STATEMENT, Integer.valueOf(autoGeneratedKeys)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @deprecated Use {@link #PStmtKey(String, String, String, int, int)}. + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final int resultSetType, final int resultSetConcurrency) { + this(sql, catalog, resultSetType, resultSetConcurrency, StatementType.PREPARED_STATEMENT); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param resultSetType a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE} + * @param resultSetHoldability One of the following {@code ResultSet} constants: + * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @deprecated Use {@link #PStmtKey(String, String, String, int, int, int)}. + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) { + this(sql, catalog, resultSetType, resultSetConcurrency, resultSetHoldability, StatementType.PREPARED_STATEMENT); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param resultSetType a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE} + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetHoldability One of the following {@code ResultSet} constants: + * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @param statementType The SQL statement type, prepared or callable. + * @deprecated Use {@link #PStmtKey(String, String, String, int, int, int, PoolingConnection.StatementType)} + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability, + final StatementType statementType) { + this(sql, catalog, null, Integer.valueOf(resultSetType), Integer.valueOf(resultSetConcurrency), Integer.valueOf(resultSetHoldability), null, null, null, statementType, + k -> match(statementType, StatementHoldability, CallHoldability)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @param statementType The SQL statement type, prepared or callable. + * @deprecated Use {@link #PStmtKey(String, String, String, int, int, PoolingConnection.StatementType)}. + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final int resultSetType, final int resultSetConcurrency, final StatementType statementType) { + this(sql, catalog, null, Integer.valueOf(resultSetType), Integer.valueOf(resultSetConcurrency), null, null, null, null, statementType, + k -> match(statementType, StatementConcurrency, CallConcurrency)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param columnIndexes An array of column indexes indicating the columns that should be returned from the inserted row + * or rows. + * @deprecated Use {@link #PStmtKey(String, String, String, int[])}. + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final int[] columnIndexes) { + this(sql, catalog, null, null, null, null, null, columnIndexes, null, StatementType.PREPARED_STATEMENT, StatementColumnIndexes); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param statementType The SQL statement type, prepared or callable. + * @deprecated Use {@link #PStmtKey(String, String, String, PoolingConnection.StatementType)}. + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final StatementType statementType) { + this(sql, catalog, null, null, null, null, null, null, null, statementType, k -> match(statementType, StatementSQL, CallSQL)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param statementType The SQL statement type, prepared or callable. + * @param autoGeneratedKeys A flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * @deprecated Use {@link #PStmtKey(String, String, String, PoolingConnection.StatementType, Integer)} + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final StatementType statementType, final Integer autoGeneratedKeys) { + this(sql, catalog, null, null, null, null, autoGeneratedKeys, null, null, statementType, + k -> match(statementType, StatementAutoGeneratedKeys, CallSQL)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema + * @since 2.5.0 + */ + public PStmtKey(final String sql, final String catalog, final String schema) { + this(sql, catalog, schema, StatementType.PREPARED_STATEMENT); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema + * @param autoGeneratedKeys A flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * @since 2.5.0 + */ + public PStmtKey(final String sql, final String catalog, final String schema, final int autoGeneratedKeys) { + this(sql, catalog, schema, StatementType.PREPARED_STATEMENT, Integer.valueOf(autoGeneratedKeys)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema + * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + */ + public PStmtKey(final String sql, final String catalog, final String schema, final int resultSetType, final int resultSetConcurrency) { + this(sql, catalog, schema, resultSetType, resultSetConcurrency, StatementType.PREPARED_STATEMENT); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema + * @param resultSetType a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE} + * @param resultSetHoldability One of the following {@code ResultSet} constants: + * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @since 2.5.0 + */ + public PStmtKey(final String sql, final String catalog, final String schema, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) { + this(sql, catalog, schema, resultSetType, resultSetConcurrency, resultSetHoldability, StatementType.PREPARED_STATEMENT); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema. + * @param resultSetType a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE} + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetHoldability One of the following {@code ResultSet} constants: + * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @param statementType The SQL statement type, prepared or callable. + * @since 2.5.0 + */ + public PStmtKey(final String sql, final String catalog, final String schema, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability, final StatementType statementType) { + this(sql, catalog, schema, Integer.valueOf(resultSetType), Integer.valueOf(resultSetConcurrency), Integer.valueOf(resultSetHoldability), null, null, null, statementType, + k -> match(statementType, StatementHoldability, CallHoldability)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema. + * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @param statementType The SQL statement type, prepared or callable. + * @since 2.5.0 + */ + public PStmtKey(final String sql, final String catalog, final String schema, final int resultSetType, final int resultSetConcurrency, + final StatementType statementType) { + this(sql, catalog, schema, Integer.valueOf(resultSetType), Integer.valueOf(resultSetConcurrency), null, null, null, null, statementType, + k -> match(statementType, StatementConcurrency, CallConcurrency)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema. + * @param columnIndexes An array of column indexes indicating the columns that should be returned from the inserted row + * or rows. + */ + public PStmtKey(final String sql, final String catalog, final String schema, final int[] columnIndexes) { + this(sql, catalog, schema, null, null, null, null, columnIndexes, null, StatementType.PREPARED_STATEMENT, StatementColumnIndexes); + } + + private PStmtKey(final String sql, final String catalog, final String schema, final Integer resultSetType, final Integer resultSetConcurrency, + final Integer resultSetHoldability, final Integer autoGeneratedKeys, final int[] columnIndexes, final String[] columnNames, + final StatementType statementType, final Function statementBuilder) { + this.sql = Objects.requireNonNull(sql, "sql").trim(); + this.catalog = catalog; + this.schema = schema; + this.resultSetType = resultSetType; + this.resultSetConcurrency = resultSetConcurrency; + this.resultSetHoldability = resultSetHoldability; + this.autoGeneratedKeys = autoGeneratedKeys; + this.columnIndexes = clone(columnIndexes); + this.columnNames = clone(columnNames); + this.statementBuilder = Objects.requireNonNull(Objects.requireNonNull(statementBuilder, "statementBuilder").apply(this), "statementBuilder"); + this.statementType = statementType; + } + + // Root constructor. + private PStmtKey(final String sql, final String catalog, final String schema, final Integer resultSetType, final Integer resultSetConcurrency, + final Integer resultSetHoldability, final Integer autoGeneratedKeys, final int[] columnIndexes, final String[] columnNames, + final StatementType statementType, final StatementBuilder statementBuilder) { + this.sql = sql; + this.catalog = catalog; + this.schema = schema; + this.resultSetType = resultSetType; + this.resultSetConcurrency = resultSetConcurrency; + this.resultSetHoldability = resultSetHoldability; + this.autoGeneratedKeys = autoGeneratedKeys; + this.columnIndexes = clone(columnIndexes); + this.columnNames = clone(columnNames); + this.statementBuilder = Objects.requireNonNull(statementBuilder, "statementBuilder"); + this.statementType = statementType; + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema. + * @param statementType The SQL statement type, prepared or callable. + * @since 2.5.0 + */ + public PStmtKey(final String sql, final String catalog, final String schema, final StatementType statementType) { + this(sql, catalog, schema, null, null, null, null, null, null, statementType, k -> match(statementType, StatementSQL, CallSQL)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema. + * @param statementType The SQL statement type, prepared or callable. + * @param autoGeneratedKeys A flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * @since 2.5.0 + */ + public PStmtKey(final String sql, final String catalog, final String schema, final StatementType statementType, final Integer autoGeneratedKeys) { + this(sql, catalog, schema, null, null, null, autoGeneratedKeys, null, null, statementType, + k -> match(statementType, StatementAutoGeneratedKeys, CallSQL)); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param schema The schema. + * @param columnNames An array of column names indicating the columns that should be returned from the inserted row or + * rows. + * @since 2.5.0 + */ + public PStmtKey(final String sql, final String catalog, final String schema, final String[] columnNames) { + this(sql, catalog, schema, null, null, null, null, null, columnNames, StatementType.PREPARED_STATEMENT, StatementColumnNames); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql The SQL statement. + * @param catalog The catalog. + * @param columnNames An array of column names indicating the columns that should be returned from the inserted row or + * rows. + * @deprecated Use {@link #PStmtKey(String, String, String, String[])}. + */ + @Deprecated + public PStmtKey(final String sql, final String catalog, final String[] columnNames) { + this(sql, catalog, null, null, null, null, null, null, columnNames, StatementType.PREPARED_STATEMENT, StatementColumnNames); + } + + private int[] clone(final int[] array) { + return array == null ? null : array.clone(); + } + + private String[] clone(final String[] array) { + return array == null ? null : array.clone(); + } + + /** + * Creates a new Statement from the given Connection. + * + * @param connection The Connection to use to create the statement. + * @return The statement. + * @throws SQLException Thrown when there is a problem creating the statement. + */ + public Statement createStatement(final Connection connection) throws SQLException { + return statementBuilder.createStatement(connection, this); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final PStmtKey other = (PStmtKey) obj; + if (!Objects.equals(autoGeneratedKeys, other.autoGeneratedKeys)) { + return false; + } + if (!Objects.equals(catalog, other.catalog)) { + return false; + } + if (!Arrays.equals(columnIndexes, other.columnIndexes)) { + return false; + } + if (!Arrays.equals(columnNames, other.columnNames)) { + return false; + } + if (!Objects.equals(resultSetConcurrency, other.resultSetConcurrency)) { + return false; + } + if (!Objects.equals(resultSetHoldability, other.resultSetHoldability)) { + return false; + } + if (!Objects.equals(resultSetType, other.resultSetType)) { + return false; + } + if (!Objects.equals(schema, other.schema)) { + return false; + } + if (!Objects.equals(sql, other.sql)) { + return false; + } + return statementType == other.statementType; + } + + /** + * Gets a flag indicating whether auto-generated keys should be returned; one of {@code Statement.RETURN_GENERATED_KEYS} + * or {@code Statement.NO_GENERATED_KEYS}. + * + * @return a flag indicating whether auto-generated keys should be returned. + */ + public Integer getAutoGeneratedKeys() { + return autoGeneratedKeys; + } + + /** + * Gets the catalog. + * + * @return The catalog. + */ + public String getCatalog() { + return catalog; + } + + /** + * Gets an array of column indexes indicating the columns that should be returned from the inserted row or rows. + * + * @return An array of column indexes. + */ + public int[] getColumnIndexes() { + return clone(columnIndexes); + } + + /** + * Gets an array of column names indicating the columns that should be returned from the inserted row or rows. + * + * @return An array of column names. + */ + public String[] getColumnNames() { + return clone(columnNames); + } + + /** + * Gets the result set concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * + * @return The result set concurrency type. + */ + public Integer getResultSetConcurrency() { + return resultSetConcurrency; + } + + /** + * Gets the result set holdability, one of the following {@code ResultSet} constants: + * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * + * @return The result set holdability. + */ + public Integer getResultSetHoldability() { + return resultSetHoldability; + } + + /** + * Gets the result set type, one of {@code ResultSet.TYPE_FORWARD_ONLY}, {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or + * {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * + * @return the result set type. + */ + public Integer getResultSetType() { + return resultSetType; + } + + /** + * Gets the schema. + * + * @return The catalog. + */ + public String getSchema() { + return schema; + } + + /** + * Gets the SQL statement. + * + * @return the SQL statement. + */ + public String getSql() { + return sql; + } + + /** + * Gets the SQL statement type. + * + * @return The SQL statement type. + */ + public StatementType getStmtType() { + return statementType; + } + + @Override + public int hashCode() { + return Objects.hash(autoGeneratedKeys, catalog, Integer.valueOf(Arrays.hashCode(columnIndexes)), Integer.valueOf(Arrays.hashCode(columnNames)), + resultSetConcurrency, resultSetHoldability, resultSetType, schema, sql, statementType); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("PStmtKey: sql="); + buf.append(sql); + buf.append(", catalog="); + buf.append(catalog); + buf.append(", schema="); + buf.append(schema); + buf.append(", resultSetType="); + buf.append(resultSetType); + buf.append(", resultSetConcurrency="); + buf.append(resultSetConcurrency); + buf.append(", resultSetHoldability="); + buf.append(resultSetHoldability); + buf.append(", autoGeneratedKeys="); + buf.append(autoGeneratedKeys); + buf.append(", columnIndexes="); + buf.append(Arrays.toString(columnIndexes)); + buf.append(", columnNames="); + buf.append(Arrays.toString(columnNames)); + buf.append(", statementType="); + buf.append(statementType); + return buf.toString(); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java b/java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java new file mode 100644 index 0000000..08d690d --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java @@ -0,0 +1,111 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.SQLException; + +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; + +/** + * A {@link DelegatingCallableStatement} that cooperates with {@link PoolingConnection} to implement a pool of + * {@link CallableStatement}s. + *

    + * The {@link #close} method returns this statement to its containing pool. (See {@link PoolingConnection}.) + * + * @see PoolingConnection + * @since 2.0 + */ +public class PoolableCallableStatement extends DelegatingCallableStatement { + + /** + * The {@link KeyedObjectPool} from which this CallableStatement was obtained. + */ + private final KeyedObjectPool pool; + + /** + * Key for this statement in the containing {@link KeyedObjectPool}. + */ + private final PStmtKey key; + + /** + * Constructor. + * + * @param callableStatement + * the underlying {@link CallableStatement} + * @param key + * the key for this statement in the {@link KeyedObjectPool} + * @param pool + * the {@link KeyedObjectPool} from which this CallableStatement was obtained + * @param connection + * the {@link DelegatingConnection} that created this CallableStatement + */ + public PoolableCallableStatement(final CallableStatement callableStatement, final PStmtKey key, + final KeyedObjectPool pool, + final DelegatingConnection connection) { + super(connection, callableStatement); + this.pool = pool; + this.key = key; + + // Remove from trace now because this statement will be + // added by the activate method. + removeThisTrace(connection); + } + + /** + * Activates after retrieval from the pool. Adds a trace for this CallableStatement to the Connection that created + * it. + * + * @since 2.4.0 made public, was protected in 2.3.0. + */ + @Override + public void activate() throws SQLException { + setClosedInternal(false); + AbandonedTrace.add(getConnectionInternal(), this); + super.activate(); + } + + /** + * Returns the CallableStatement to the pool. If {{@link #isClosed()}, this is a No-op. + */ + @Override + public void close() throws SQLException { + // calling close twice should have no effect + if (!isClosed()) { + try { + pool.returnObject(key, this); + } catch (final SQLException | RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Cannot close CallableStatement (return to pool failed)", e); + } + } + } + + /** + * Passivates to prepare for return to the pool. Removes the trace associated with this CallableStatement from the + * Connection that created it. Also closes any associated ResultSets. + * + * @since 2.4.0 made public, was protected in 2.3.0. + */ + @Override + public void passivate() throws SQLException { + prepareToReturn(); + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java b/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java new file mode 100644 index 0000000..3ad15ad --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java @@ -0,0 +1,402 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.lang.management.ManagementFactory; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Duration; +import java.util.Collection; +import java.util.concurrent.Executor; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; + +import org.apache.tomcat.dbcp.pool2.ObjectPool; + +/** + * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool} + * when closed. + * + * @since 2.0 + */ +public class PoolableConnection extends DelegatingConnection implements PoolableConnectionMXBean { + + private static MBeanServer MBEAN_SERVER; + + static { + try { + MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer(); + } catch (final NoClassDefFoundError | Exception ignored) { + // ignore - JMX not available + } + } + + /** The pool to which I should return. */ + private final ObjectPool pool; + + private final ObjectNameWrapper jmxObjectName; + + // Use a prepared statement for validation, retaining the last used SQL to + // check if the validation query has changed. + private PreparedStatement validationPreparedStatement; + private String lastValidationSql; + + /** + * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be + * considered broken and not pass validation in the future. + */ + private boolean fatalSqlExceptionThrown; + + /** + * SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in + * {@link Utils#getDisconnectionSqlCodes()} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}). + */ + private final Collection disconnectionSqlCodes; + + /** Whether or not to fast fail validation after fatal connection errors */ + private final boolean fastFailValidation; + + /** + * @param conn + * my underlying connection + * @param pool + * the pool to which I should return when closed + * @param jmxName + * JMX name + */ + public PoolableConnection(final Connection conn, final ObjectPool pool, + final ObjectName jmxName) { + this(conn, pool, jmxName, null, true); + } + + /** + * @param conn + * my underlying connection + * @param pool + * the pool to which I should return when closed + * @param jmxObjectName + * JMX name + * @param disconnectSqlCodes + * SQL_STATE codes considered fatal disconnection errors + * @param fastFailValidation + * true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to + * run query or isValid) + */ + public PoolableConnection(final Connection conn, final ObjectPool pool, + final ObjectName jmxObjectName, final Collection disconnectSqlCodes, + final boolean fastFailValidation) { + super(conn); + this.pool = pool; + this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName); + this.disconnectionSqlCodes = disconnectSqlCodes; + this.fastFailValidation = fastFailValidation; + + if (jmxObjectName != null) { + try { + MBEAN_SERVER.registerMBean(this, jmxObjectName); + } catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException ignored) { + // For now, simply skip registration + } + } + } + + /** + * Abort my underlying {@link Connection}. + * + * @since 2.9.0 + */ + @Override + public void abort(final Executor executor) throws SQLException { + if (jmxObjectName != null) { + jmxObjectName.unregisterMBean(); + } + super.abort(executor); + } + + /** + * Returns me to my pool. + */ + @Override + public synchronized void close() throws SQLException { + if (isClosedInternal()) { + // already closed + return; + } + + boolean isUnderlyingConnectionClosed; + try { + isUnderlyingConnectionClosed = getDelegateInternal().isClosed(); + } catch (final SQLException e) { + try { + pool.invalidateObject(this); + } catch (final IllegalStateException ise) { + // pool is closed, so close the connection + passivate(); + getInnermostDelegate().close(); + } catch (final Exception ignored) { + // DO NOTHING the original exception will be rethrown + } + throw new SQLException("Cannot close connection (isClosed check failed)", e); + } + + /* + * Can't set close before this code block since the connection needs to be open when validation runs. Can't set + * close after this code block since by then the connection will have been returned to the pool and may have + * been borrowed by another thread. Therefore, the close flag is set in passivate(). + */ + if (isUnderlyingConnectionClosed) { + // Abnormal close: underlying connection closed unexpectedly, so we + // must destroy this proxy + try { + pool.invalidateObject(this); + } catch (final IllegalStateException e) { + // pool is closed, so close the connection + passivate(); + getInnermostDelegate().close(); + } catch (final Exception e) { + throw new SQLException("Cannot close connection (invalidating pooled object failed)", e); + } + } else { + // Normal close: underlying connection is still open, so we + // simply need to return this proxy to the pool + try { + pool.returnObject(this); + } catch (final IllegalStateException e) { + // pool is closed, so close the connection + passivate(); + getInnermostDelegate().close(); + } catch (final SQLException | RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Cannot close connection (return to pool failed)", e); + } + } + } + + /** + * @return The disconnection SQL codes. + * @since 2.6.0 + */ + public Collection getDisconnectionSqlCodes() { + return disconnectionSqlCodes; + } + + /** + * Expose the {@link #toString()} method via a bean getter so it can be read as a property via JMX. + */ + @Override + public String getToString() { + return toString(); + } + + @Override + protected void handleException(final SQLException e) throws SQLException { + fatalSqlExceptionThrown |= isFatalException(e); + super.handleException(e); + } + + /** + * {@inheritDoc} + *

    + * This method should not be used by a client to determine whether or not a connection should be return to the + * connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool + * once it is no longer required. + */ + @Override + public boolean isClosed() throws SQLException { + if (isClosedInternal()) { + return true; + } + + if (getDelegateInternal().isClosed()) { + // Something has gone wrong. The underlying connection has been + // closed without the connection being returned to the pool. Return + // it now. + close(); + return true; + } + + return false; + } + + /** + * Checks the SQLState of the input exception. + *

    + * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the configured list of fatal + * exception codes. If this property is not set, codes are compared against the default codes in + * {@link Utils#getDisconnectionSqlCodes()} and in this case anything starting with #{link + * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection. + *

    + * + * @param e SQLException to be examined + * @return true if the exception signals a disconnection + */ + boolean isDisconnectionSqlException(final SQLException e) { + boolean fatalException = false; + final String sqlState = e.getSQLState(); + if (sqlState != null) { + fatalException = disconnectionSqlCodes == null + ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX) || Utils.getDisconnectionSqlCodes().contains(sqlState) + : disconnectionSqlCodes.contains(sqlState); + } + return fatalException; + } + + /** + * Checks the SQLState of the input exception and any nested SQLExceptions it wraps. + *

    + * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the + * configured list of fatal exception codes. If this property is not set, codes are compared against the default + * codes in {@link Utils#getDisconnectionSqlCodes()} and in this case anything starting with #{link + * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection. + *

    + * + * @param e + * SQLException to be examined + * @return true if the exception signals a disconnection + */ + boolean isFatalException(final SQLException e) { + boolean fatalException = isDisconnectionSqlException(e); + if (!fatalException) { + SQLException parentException = e; + SQLException nextException = e.getNextException(); + while (nextException != null && nextException != parentException && !fatalException) { + fatalException = isDisconnectionSqlException(nextException); + parentException = nextException; + nextException = parentException.getNextException(); + } + } + return fatalException; + } + + /** + * @return Whether to fail-fast. + * @since 2.6.0 + */ + public boolean isFastFailValidation() { + return fastFailValidation; + } + + @Override + protected void passivate() throws SQLException { + super.passivate(); + setClosedInternal(true); + if (getDelegateInternal() instanceof PoolingConnection) { + ((PoolingConnection) getDelegateInternal()).connectionReturnedToPool(); + } + } + + /** + * Actually close my underlying {@link Connection}. + */ + @Override + public void reallyClose() throws SQLException { + if (jmxObjectName != null) { + jmxObjectName.unregisterMBean(); + } + + if (validationPreparedStatement != null) { + Utils.closeQuietly((AutoCloseable) validationPreparedStatement); + } + + super.closeInternal(); + } + + /** + * Validates the connection, using the following algorithm: + *
      + *
    1. If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously + * thrown a fatal disconnection exception, a {@code SQLException} is thrown.
    2. + *
    3. If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it + * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.
    4. + *
    5. If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at + * least one row, this method returns successfully. If not, {@code SQLException} is thrown.
    6. + *
    + * + * @param sql + * The validation SQL query. + * @param timeoutSeconds + * The validation timeout in seconds. + * @throws SQLException + * Thrown when validation fails or an SQLException occurs during validation + * @deprecated Use {@link #validate(String, Duration)}. + */ + @Deprecated + public void validate(final String sql, final int timeoutSeconds) throws SQLException { + validate(sql, Duration.ofSeconds(timeoutSeconds)); + } + + /** + * Validates the connection, using the following algorithm: + *
      + *
    1. If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously + * thrown a fatal disconnection exception, a {@code SQLException} is thrown.
    2. + *
    3. If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it + * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.
    4. + *
    5. If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at + * least one row, this method returns successfully. If not, {@code SQLException} is thrown.
    6. + *
    + * + * @param sql + * The validation SQL query. + * @param timeoutDuration + * The validation timeout in seconds. + * @throws SQLException + * Thrown when validation fails or an SQLException occurs during validation + * @since 2.10.0 + */ + public void validate(final String sql, Duration timeoutDuration) throws SQLException { + if (fastFailValidation && fatalSqlExceptionThrown) { + throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail")); + } + + if (sql == null || sql.isEmpty()) { + if (timeoutDuration.isNegative()) { + timeoutDuration = Duration.ZERO; + } + if (!isValid(timeoutDuration)) { + throw new SQLException("isValid() returned false"); + } + return; + } + + if (!sql.equals(lastValidationSql)) { + lastValidationSql = sql; + // Has to be the innermost delegate else the prepared statement will + // be closed when the pooled connection is passivated. + validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql); + } + + if (timeoutDuration.compareTo(Duration.ZERO) > 0) { + validationPreparedStatement.setQueryTimeout((int) timeoutDuration.getSeconds()); + } + + try (ResultSet rs = validationPreparedStatement.executeQuery()) { + if (!rs.next()) { + throw new SQLException("validationQuery didn't return a row"); + } + } catch (final SQLException sqle) { + throw sqle; + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java new file mode 100644 index 0000000..b0c9ef9 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java @@ -0,0 +1,766 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.dbcp.pool2.DestroyMode; +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.ObjectPool; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.PooledObjectFactory; +import org.apache.tomcat.dbcp.pool2.impl.DefaultPooledObject; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPoolConfig; + +/** + * A {@link PooledObjectFactory} that creates {@link PoolableConnection}s. + * + * @since 2.0 + */ +public class PoolableConnectionFactory implements PooledObjectFactory { + + private static final Log log = LogFactory.getLog(PoolableConnectionFactory.class); + + /** + * Internal constant to indicate the level is not set. + */ + static final int UNKNOWN_TRANSACTION_ISOLATION = -1; + + private final ConnectionFactory connectionFactory; + + private final ObjectName dataSourceJmxObjectName; + + private volatile String validationQuery; + + private volatile Duration validationQueryTimeoutDuration = Duration.ofSeconds(-1); + + private Collection connectionInitSqls; + + private Collection disconnectionSqlCodes; + + private boolean fastFailValidation = true; + + private volatile ObjectPool pool; + + private Boolean defaultReadOnly; + + private Boolean defaultAutoCommit; + + private boolean autoCommitOnReturn = true; + + private boolean rollbackOnReturn = true; + + private int defaultTransactionIsolation = UNKNOWN_TRANSACTION_ISOLATION; + + private String defaultCatalog; + + private String defaultSchema; + + private boolean cacheState; + + private boolean poolStatements; + + private boolean clearStatementPoolOnReturn; + + private int maxOpenPreparedStatements = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL_PER_KEY; + + private Duration maxConnDuration = Duration.ofMillis(-1); + + private final AtomicLong connectionIndex = new AtomicLong(); + + private Duration defaultQueryTimeoutDuration; + + /** + * Creates a new {@code PoolableConnectionFactory}. + * + * @param connFactory + * the {@link ConnectionFactory} from which to obtain base {@link Connection}s + * @param dataSourceJmxObjectName + * The JMX object name, may be null. + */ + public PoolableConnectionFactory(final ConnectionFactory connFactory, final ObjectName dataSourceJmxObjectName) { + this.connectionFactory = connFactory; + this.dataSourceJmxObjectName = dataSourceJmxObjectName; + } + + @Override + public void activateObject(final PooledObject p) throws SQLException { + + validateLifetime(p); + + final PoolableConnection pConnection = p.getObject(); + pConnection.activate(); + + if (defaultAutoCommit != null && pConnection.getAutoCommit() != defaultAutoCommit.booleanValue()) { + pConnection.setAutoCommit(defaultAutoCommit.booleanValue()); + } + if (defaultTransactionIsolation != UNKNOWN_TRANSACTION_ISOLATION + && pConnection.getTransactionIsolation() != defaultTransactionIsolation) { + pConnection.setTransactionIsolation(defaultTransactionIsolation); + } + if (defaultReadOnly != null && pConnection.isReadOnly() != defaultReadOnly.booleanValue()) { + pConnection.setReadOnly(defaultReadOnly.booleanValue()); + } + if (defaultCatalog != null && !defaultCatalog.equals(pConnection.getCatalog())) { + pConnection.setCatalog(defaultCatalog); + } + if (defaultSchema != null && !defaultSchema.equals(Jdbc41Bridge.getSchema(pConnection))) { + Jdbc41Bridge.setSchema(pConnection, defaultSchema); + } + pConnection.setDefaultQueryTimeout(defaultQueryTimeoutDuration); + } + + @Override + public void destroyObject(final PooledObject p) throws SQLException { + p.getObject().reallyClose(); + } + + /** + * @since 2.9.0 + */ + @Override + public void destroyObject(final PooledObject p, final DestroyMode mode) throws SQLException { + if (mode == DestroyMode.ABANDONED) { + p.getObject().getInnermostDelegate().abort(Runnable::run); + } else { + p.getObject().reallyClose(); + } + } + + /** + * Gets the cache state. + * + * @return The cache state. + * @since 2.6.0. + */ + public boolean getCacheState() { + return cacheState; + } + + /** + * Gets the connection factory. + * + * @return The connection factory. + * @since 2.6.0. + */ + public ConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + protected AtomicLong getConnectionIndex() { + return connectionIndex; + } + + /** + * @return The collection of initialization SQL statements. + * @since 2.6.0 + */ + public Collection getConnectionInitSqls() { + return connectionInitSqls; + } + + /** + * @return The data source JMX ObjectName + * @since 2.6.0. + */ + public ObjectName getDataSourceJmxName() { + return dataSourceJmxObjectName; + } + + /** + * @return The data source JMS ObjectName. + * @since 2.6.0 + */ + public ObjectName getDataSourceJmxObjectName() { + return dataSourceJmxObjectName; + } + + /** + * @return Default auto-commit value. + * @since 2.6.0 + */ + public Boolean getDefaultAutoCommit() { + return defaultAutoCommit; + } + + /** + * @return Default catalog. + * @since 2.6.0 + */ + public String getDefaultCatalog() { + return defaultCatalog; + } + + /** + * @return Default query timeout in seconds. + * @deprecated Use {@link #getDefaultQueryTimeoutDuration()}. + */ + @Deprecated + public Integer getDefaultQueryTimeout() { + return getDefaultQueryTimeoutSeconds(); + } + + /** + * Gets the default query timeout Duration. + * + * @return Default query timeout Duration. + * @since 2.10.0 + */ + public Duration getDefaultQueryTimeoutDuration() { + return defaultQueryTimeoutDuration; + } + + /** + * @return Default query timeout in seconds. + * @since 2.6.0 + * @deprecated Use {@link #getDefaultQueryTimeoutDuration()}. + */ + @Deprecated + public Integer getDefaultQueryTimeoutSeconds() { + return defaultQueryTimeoutDuration == null ? null : Integer.valueOf((int) defaultQueryTimeoutDuration.getSeconds()); + } + + /** + * @return Default read-only-value. + * @since 2.6.0 + */ + public Boolean getDefaultReadOnly() { + return defaultReadOnly; + } + + /** + * @return Default schema. + * @since 2.6.0 + */ + public String getDefaultSchema() { + return defaultSchema; + } + + /** + * @return Default transaction isolation. + * @since 2.6.0 + */ + public int getDefaultTransactionIsolation() { + return defaultTransactionIsolation; + } + + /** + * SQL_STATE codes considered to signal fatal conditions. + *

    + * Overrides the defaults in {@link Utils#getDisconnectionSqlCodes()} (plus anything starting with + * {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}). If this property is non-null and {@link #isFastFailValidation()} is + * {@code true}, whenever connections created by this factory generate exceptions with SQL_STATE codes in this list, + * they will be marked as "fatally disconnected" and subsequent validations will fail fast (no attempt at isValid or + * validation query). + *

    + *

    + * If {@link #isFastFailValidation()} is {@code false} setting this property has no effect. + *

    + * + * @return SQL_STATE codes overriding defaults + * @since 2.1 + */ + public Collection getDisconnectionSqlCodes() { + return disconnectionSqlCodes; + } + + /** + * Gets the Maximum connection duration. + * + * @return Maximum connection duration. + * @since 2.10.0 + */ + public Duration getMaxConnDuration() { + return maxConnDuration; + } + + /** + * Gets the Maximum connection lifetime in milliseconds. + * + * @return Maximum connection lifetime in milliseconds. + * @since 2.6.0 + */ + public long getMaxConnLifetimeMillis() { + return maxConnDuration.toMillis(); + } + + protected int getMaxOpenPreparedStatements() { + return maxOpenPreparedStatements; + } + /** + * Returns the {@link ObjectPool} in which {@link Connection}s are pooled. + * + * @return the connection pool + */ + public synchronized ObjectPool getPool() { + return pool; + } + /** + * @return Whether to pool statements. + * @since 2.6.0. + */ + public boolean getPoolStatements() { + return poolStatements; + } + /** + * @return Validation query. + * @since 2.6.0 + */ + public String getValidationQuery() { + return validationQuery; + } + + /** + * Gets the query timeout in seconds. + * + * @return Validation query timeout in seconds. + * @since 2.10.0 + */ + public Duration getValidationQueryTimeoutDuration() { + return validationQueryTimeoutDuration; + } + + /** + * Gets the query timeout in seconds. + * + * @return Validation query timeout in seconds. + * @since 2.6.0 + * @deprecated Use {@link #getValidationQueryTimeoutDuration()}. + */ + @Deprecated + public int getValidationQueryTimeoutSeconds() { + return (int) validationQueryTimeoutDuration.getSeconds(); + } + + protected void initializeConnection(final Connection conn) throws SQLException { + final Collection sqls = connectionInitSqls; + if (conn.isClosed()) { + throw new SQLException("initializeConnection: connection closed"); + } + if (!Utils.isEmpty(sqls)) { + try (Statement statement = conn.createStatement()) { + for (final String sql : sqls) { + statement.execute(Objects.requireNonNull(sql, "null connectionInitSqls element")); + } + } + } + } + + /** + * @return Whether to auto-commit on return. + * @since 2.6.0 + */ + public boolean isAutoCommitOnReturn() { + return autoCommitOnReturn; + } + + /** + * @return Whether to auto-commit on return. + * @deprecated Use {@link #isAutoCommitOnReturn()}. + */ + @Deprecated + public boolean isEnableAutoCommitOnReturn() { + return autoCommitOnReturn; + } + + /** + * True means that validation will fail immediately for connections that have previously thrown SQLExceptions with + * SQL_STATE indicating fatal disconnection errors. + * + * @return true if connections created by this factory will fast fail validation. + * @see #setDisconnectionSqlCodes(Collection) + * @since 2.1 + * @since 2.5.0 Defaults to true, previous versions defaulted to false. + */ + public boolean isFastFailValidation() { + return fastFailValidation; + } + + /** + * @return Whether to rollback on return. + */ + public boolean isRollbackOnReturn() { + return rollbackOnReturn; + } + + @Override + public PooledObject makeObject() throws SQLException { + Connection conn = connectionFactory.createConnection(); + if (conn == null) { + throw new IllegalStateException("Connection factory returned null from createConnection"); + } + try { + initializeConnection(conn); + } catch (final SQLException e) { + // Make sure the connection is closed + Utils.closeQuietly((AutoCloseable) conn); + // Rethrow original exception so it is visible to caller + throw e; + } + + final long connIndex = connectionIndex.getAndIncrement(); + + if (poolStatements) { + conn = new PoolingConnection(conn); + final GenericKeyedObjectPoolConfig config = new GenericKeyedObjectPoolConfig<>(); + config.setMaxTotalPerKey(-1); + config.setBlockWhenExhausted(false); + config.setMaxWait(Duration.ZERO); + config.setMaxIdlePerKey(1); + config.setMaxTotal(maxOpenPreparedStatements); + if (dataSourceJmxObjectName != null) { + final StringBuilder base = new StringBuilder(dataSourceJmxObjectName.toString()); + base.append(Constants.JMX_CONNECTION_BASE_EXT); + base.append(connIndex); + config.setJmxNameBase(base.toString()); + config.setJmxNamePrefix(Constants.JMX_STATEMENT_POOL_PREFIX); + } else { + config.setJmxEnabled(false); + } + final PoolingConnection poolingConn = (PoolingConnection) conn; + final KeyedObjectPool stmtPool = new GenericKeyedObjectPool<>(poolingConn, config); + poolingConn.setStatementPool(stmtPool); + poolingConn.setClearStatementPoolOnReturn(clearStatementPoolOnReturn); + poolingConn.setCacheState(cacheState); + } + + // Register this connection with JMX + final ObjectName connJmxName; + if (dataSourceJmxObjectName == null) { + connJmxName = null; + } else { + final String name = dataSourceJmxObjectName.toString() + Constants.JMX_CONNECTION_BASE_EXT + connIndex; + try { + connJmxName = new ObjectName(name); + } catch (MalformedObjectNameException e) { + Utils.closeQuietly((AutoCloseable) conn); + throw new SQLException(name, e); + } + } + + final PoolableConnection pc = new PoolableConnection(conn, pool, connJmxName, disconnectionSqlCodes, fastFailValidation); + pc.setCacheState(cacheState); + + return new DefaultPooledObject<>(pc); + } + + @Override + public void passivateObject(final PooledObject p) throws SQLException { + + validateLifetime(p); + + final PoolableConnection conn = p.getObject(); + Boolean connAutoCommit = null; + if (rollbackOnReturn) { + connAutoCommit = Boolean.valueOf(conn.getAutoCommit()); + if (!connAutoCommit.booleanValue() && !conn.isReadOnly()) { + conn.rollback(); + } + } + + conn.clearWarnings(); + + // DBCP-97 / DBCP-399 / DBCP-351 Idle connections in the pool should + // have autoCommit enabled + if (autoCommitOnReturn) { + if (connAutoCommit == null) { + connAutoCommit = Boolean.valueOf(conn.getAutoCommit()); + } + if (!connAutoCommit.booleanValue()) { + conn.setAutoCommit(true); + } + } + + conn.passivate(); + } + + public void setAutoCommitOnReturn(final boolean autoCommitOnReturn) { + this.autoCommitOnReturn = autoCommitOnReturn; + } + + public void setCacheState(final boolean cacheState) { + this.cacheState = cacheState; + } + + /** + * Sets whether the pool of statements (which was enabled with {@link #setPoolStatements(boolean)}) should + * be cleared when the connection is returned to its pool. Default is false. + * + * @param clearStatementPoolOnReturn clear or not + * @since 2.8.0 + */ + public void setClearStatementPoolOnReturn(final boolean clearStatementPoolOnReturn) { + this.clearStatementPoolOnReturn = clearStatementPoolOnReturn; + } + + /** + * Sets the SQL statements I use to initialize newly created {@link Connection}s. Using {@code null} turns off + * connection initialization. + * + * @param connectionInitSqls + * SQL statement to initialize {@link Connection}s. + */ + public void setConnectionInitSql(final Collection connectionInitSqls) { + this.connectionInitSqls = connectionInitSqls; + } + /** + * Sets the default "auto commit" setting for borrowed {@link Connection}s + * + * @param defaultAutoCommit + * the default "auto commit" setting for borrowed {@link Connection}s + */ + public void setDefaultAutoCommit(final Boolean defaultAutoCommit) { + this.defaultAutoCommit = defaultAutoCommit; + } + + /** + * Sets the default "catalog" setting for borrowed {@link Connection}s + * + * @param defaultCatalog + * the default "catalog" setting for borrowed {@link Connection}s + */ + public void setDefaultCatalog(final String defaultCatalog) { + this.defaultCatalog = defaultCatalog; + } + + /** + * Sets the query timeout Duration. + * + * @param defaultQueryTimeoutDuration the query timeout Duration. + * @since 2.10.0 + */ + public void setDefaultQueryTimeout(final Duration defaultQueryTimeoutDuration) { + this.defaultQueryTimeoutDuration = defaultQueryTimeoutDuration; + } + + /** + * Sets the query timeout in seconds. + * + * @param defaultQueryTimeoutSeconds the query timeout in seconds. + * @deprecated Use {@link #setDefaultQueryTimeout(Duration)}. + */ + @Deprecated + public void setDefaultQueryTimeout(final Integer defaultQueryTimeoutSeconds) { + this.defaultQueryTimeoutDuration = defaultQueryTimeoutSeconds == null ? null : Duration.ofSeconds(defaultQueryTimeoutSeconds.longValue()); + } + + /** + * Sets the default "read only" setting for borrowed {@link Connection}s + * + * @param defaultReadOnly + * the default "read only" setting for borrowed {@link Connection}s + */ + public void setDefaultReadOnly(final Boolean defaultReadOnly) { + this.defaultReadOnly = defaultReadOnly; + } + + /** + * Sets the default "schema" setting for borrowed {@link Connection}s + * + * @param defaultSchema + * the default "schema" setting for borrowed {@link Connection}s + * @since 2.5.0 + */ + public void setDefaultSchema(final String defaultSchema) { + this.defaultSchema = defaultSchema; + } + + /** + * Sets the default "Transaction Isolation" setting for borrowed {@link Connection}s + * + * @param defaultTransactionIsolation + * the default "Transaction Isolation" setting for returned {@link Connection}s + */ + public void setDefaultTransactionIsolation(final int defaultTransactionIsolation) { + this.defaultTransactionIsolation = defaultTransactionIsolation; + } + + /** + * @param disconnectionSqlCodes + * The disconnection SQL codes. + * @see #getDisconnectionSqlCodes() + * @since 2.1 + */ + public void setDisconnectionSqlCodes(final Collection disconnectionSqlCodes) { + this.disconnectionSqlCodes = disconnectionSqlCodes; + } + + /** + * @param autoCommitOnReturn Whether to auto-commit on return. + * @deprecated Use {@link #setAutoCommitOnReturn(boolean)}. + */ + @Deprecated + public void setEnableAutoCommitOnReturn(final boolean autoCommitOnReturn) { + this.autoCommitOnReturn = autoCommitOnReturn; + } + + /** + * @see #isFastFailValidation() + * @param fastFailValidation + * true means connections created by this factory will fast fail validation + * @since 2.1 + */ + public void setFastFailValidation(final boolean fastFailValidation) { + this.fastFailValidation = fastFailValidation; + } + + /** + * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation, + * passivation and validation. A value of zero or less indicates an infinite lifetime. The default value is -1. + * + * @param maxConnDuration + * The maximum lifetime in milliseconds. + * @since 2.10.0 + */ + public void setMaxConn(final Duration maxConnDuration) { + this.maxConnDuration = maxConnDuration; + } + + /** + * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation, + * passivation and validation. A value of zero or less indicates an infinite lifetime. The default value is -1. + * + * @param maxConnLifetimeMillis + * The maximum lifetime in milliseconds. + * @deprecated Use {@link #setMaxConn(Duration)}. + */ + @Deprecated + public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) { + this.maxConnDuration = Duration.ofMillis(maxConnLifetimeMillis); + } + + /** + * Sets the maximum number of open prepared statements. + * + * @param maxOpenPreparedStatements + * The maximum number of open prepared statements. + */ + public void setMaxOpenPreparedStatements(final int maxOpenPreparedStatements) { + this.maxOpenPreparedStatements = maxOpenPreparedStatements; + } + + /** + * Deprecated due to typo in method name. + * + * @param maxOpenPreparedStatements + * The maximum number of open prepared statements. + * @deprecated Use {@link #setMaxOpenPreparedStatements(int)}. + */ + @Deprecated // Due to typo in method name. + public void setMaxOpenPrepatedStatements(final int maxOpenPreparedStatements) { + setMaxOpenPreparedStatements(maxOpenPreparedStatements); + } + + /** + * Sets the {@link ObjectPool} in which to pool {@link Connection}s. + * + * @param pool + * the {@link ObjectPool} in which to pool those {@link Connection}s + */ + public synchronized void setPool(final ObjectPool pool) { + if (null != this.pool && pool != this.pool) { + Utils.closeQuietly(this.pool); + } + this.pool = pool; + } + + public void setPoolStatements(final boolean poolStatements) { + this.poolStatements = poolStatements; + } + + public void setRollbackOnReturn(final boolean rollbackOnReturn) { + this.rollbackOnReturn = rollbackOnReturn; + } + + /** + * Sets the query I use to {@link #validateObject validate} {@link Connection}s. Should return at least one row. If + * not specified, {@link Connection#isValid(int)} will be used to validate connections. + * + * @param validationQuery + * a query to use to {@link #validateObject validate} {@link Connection}s. + */ + public void setValidationQuery(final String validationQuery) { + this.validationQuery = validationQuery; + } + + /** + * Sets the validation query timeout, the amount of time, that connection validation will wait for a response from the + * database when executing a validation query. Use a value less than or equal to 0 for no timeout. + * + * @param validationQueryTimeoutDuration new validation query timeout duration. + * @since 2.10.0 + */ + public void setValidationQueryTimeout(final Duration validationQueryTimeoutDuration) { + this.validationQueryTimeoutDuration = validationQueryTimeoutDuration; + } + + /** + * Sets the validation query timeout, the amount of time, in seconds, that connection validation will wait for a + * response from the database when executing a validation query. Use a value less than or equal to 0 for no timeout. + * + * @param validationQueryTimeoutSeconds + * new validation query timeout value in seconds + * @deprecated {@link #setValidationQueryTimeout(Duration)}. + */ + @Deprecated + public void setValidationQueryTimeout(final int validationQueryTimeoutSeconds) { + this.validationQueryTimeoutDuration = Duration.ofSeconds(validationQueryTimeoutSeconds); + } + + /** + * Validates the given connection if it is open. + * + * @param conn the connection to validate. + * @throws SQLException if the connection is closed or validate fails. + */ + public void validateConnection(final PoolableConnection conn) throws SQLException { + if (conn.isClosed()) { + throw new SQLException("validateConnection: connection closed"); + } + conn.validate(validationQuery, validationQueryTimeoutDuration); + } + + private void validateLifetime(final PooledObject p) throws LifetimeExceededException { + Utils.validateLifetime(p, maxConnDuration); + } + + @Override + public boolean validateObject(final PooledObject p) { + try { + validateLifetime(p); + validateConnection(p.getObject()); + return true; + } catch (final Exception e) { + if (log.isDebugEnabled()) { + log.debug(Utils.getMessage("poolableConnectionFactory.validateObject.fail"), e); + } + return false; + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java b/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java new file mode 100644 index 0000000..f72c565 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.SQLException; + +/** + * Defines the attributes and methods that will be exposed via JMX for {@link PoolableConnection} instances. + * + * @since 2.0 + */ +public interface PoolableConnectionMXBean { + + void clearCachedState(); + + void clearWarnings() throws SQLException; + + void close() throws SQLException; + + boolean getAutoCommit() throws SQLException; + + boolean getCacheState(); + + String getCatalog() throws SQLException; + + int getHoldability() throws SQLException; + + String getSchema() throws SQLException; + + String getToString(); + + int getTransactionIsolation() throws SQLException; + + boolean isClosed() throws SQLException; + + boolean isReadOnly() throws SQLException; + + void reallyClose() throws SQLException; + + void setAutoCommit(boolean autoCommit) throws SQLException; + + void setCacheState(boolean cacheState); + + void setCatalog(String catalog) throws SQLException; + + void setHoldability(int holdability) throws SQLException; + + void setReadOnly(boolean readOnly) throws SQLException; + + void setSchema(String schema) throws SQLException; + + void setTransactionIsolation(int level) throws SQLException; +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java b/java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java new file mode 100644 index 0000000..30723e0 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java @@ -0,0 +1,125 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; + +/** + * A {@link DelegatingPreparedStatement} that cooperates with {@link PoolingConnection} to implement a pool of + * {@link PreparedStatement}s. + *

    + * My {@link #close} method returns me to my containing pool. (See {@link PoolingConnection}.) + *

    + * + * @param + * the key type + * + * @see PoolingConnection + * @since 2.0 + */ +public class PoolablePreparedStatement extends DelegatingPreparedStatement { + + /** + * The {@link KeyedObjectPool} from which I was obtained. + */ + private final KeyedObjectPool> pool; + + /** + * My "key" as used by {@link KeyedObjectPool}. + */ + private final K key; + + private volatile boolean batchAdded; + + /** + * Constructor. + * + * @param stmt + * my underlying {@link PreparedStatement} + * @param key + * my key" as used by {@link KeyedObjectPool} + * @param pool + * the {@link KeyedObjectPool} from which I was obtained. + * @param conn + * the {@link java.sql.Connection Connection} from which I was created + */ + public PoolablePreparedStatement(final PreparedStatement stmt, final K key, + final KeyedObjectPool> pool, final DelegatingConnection conn) { + super(conn, stmt); + this.pool = pool; + this.key = key; + + // Remove from trace now because this statement will be + // added by the activate method. + removeThisTrace(conn); + } + + @Override + public void activate() throws SQLException { + setClosedInternal(false); + AbandonedTrace.add(getConnectionInternal(), this); + super.activate(); + } + + /** + * Add batch. + */ + @Override + public void addBatch() throws SQLException { + super.addBatch(); + batchAdded = true; + } + + /** + * Clear Batch. + */ + @Override + public void clearBatch() throws SQLException { + batchAdded = false; + super.clearBatch(); + } + + /** + * Return me to my pool. + */ + @Override + public void close() throws SQLException { + // calling close twice should have no effect + if (!isClosed()) { + try { + pool.returnObject(key, this); + } catch (final SQLException | RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Cannot close preparedstatement (return to pool failed)", e); + } + } + } + + @Override + public void passivate() throws SQLException { + // DBCP-372. clearBatch with throw an exception if called when the + // connection is marked as closed. + if (batchAdded) { + clearBatch(); + } + prepareToReturn(); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java b/java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java new file mode 100644 index 0000000..b35e031 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java @@ -0,0 +1,615 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.NoSuchElementException; +import java.util.Objects; + +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.KeyedPooledObjectFactory; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.impl.DefaultPooledObject; + +/** + * A {@link DelegatingConnection} that pools {@link PreparedStatement}s. + *

    + * The {@link #prepareStatement} and {@link #prepareCall} methods, rather than creating a new PreparedStatement each + * time, may actually pull the statement from a pool of unused statements. The {@link PreparedStatement#close} method of + * the returned statement doesn't actually close the statement, but rather returns it to the pool. (See + * {@link PoolablePreparedStatement}, {@link PoolableCallableStatement}.) + *

    + * + * @see PoolablePreparedStatement + * @since 2.0 + */ +public class PoolingConnection extends DelegatingConnection + implements KeyedPooledObjectFactory { + + /** + * Statement types. + * + * @since 2.0 protected enum. + * @since 2.4.0 public enum. + */ + public enum StatementType { + + /** + * Callable statement. + */ + CALLABLE_STATEMENT, + + /** + * Prepared statement. + */ + PREPARED_STATEMENT + } + + /** Pool of {@link PreparedStatement}s. and {@link CallableStatement}s */ + private KeyedObjectPool pStmtPool; + + private boolean clearStatementPoolOnReturn; + + /** + * Constructor. + * + * @param connection + * the underlying {@link Connection}. + */ + public PoolingConnection(final Connection connection) { + super(connection); + } + + /** + * {@link KeyedPooledObjectFactory} method for activating pooled statements. + * + * @param key + * ignored + * @param pooledObject + * wrapped pooled statement to be activated + */ + @Override + public void activateObject(final PStmtKey key, final PooledObject pooledObject) + throws SQLException { + pooledObject.getObject().activate(); + } + + /** + * Closes and frees all {@link PreparedStatement}s or {@link CallableStatement}s from the pool, and close the + * underlying connection. + */ + @Override + public synchronized void close() throws SQLException { + try { + if (null != pStmtPool) { + final KeyedObjectPool oldPool = pStmtPool; + pStmtPool = null; + try { + oldPool.close(); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Cannot close connection", e); + } + } + } finally { + try { + getDelegateInternal().close(); + } finally { + setClosedInternal(true); + } + } + } + + /** + * Notification from {@link PoolableConnection} that we returned to the pool. + * + * @throws SQLException when {@code clearStatementPoolOnReturn} is true and the statement pool could not be + * cleared + * @since 2.8.0 + */ + public void connectionReturnedToPool() throws SQLException { + if (pStmtPool != null && clearStatementPoolOnReturn) { + try { + pStmtPool.clear(); + } catch (final Exception e) { + throw new SQLException("Error clearing statement pool", e); + } + } + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull()); + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * @param autoGeneratedKeys + * A flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql, final int autoGeneratedKeys) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull(), autoGeneratedKeys); + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * @param resultSetType + * result set type + * @param resultSetConcurrency + * result set concurrency + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency); + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * @param resultSetType + * result set type + * @param resultSetConcurrency + * result set concurrency + * @param resultSetHoldability + * result set holdability + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency, + resultSetHoldability); + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * @param resultSetType + * result set type + * @param resultSetConcurrency + * result set concurrency + * @param resultSetHoldability + * result set holdability + * @param statementType + * statement type + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability, final StatementType statementType) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency, + resultSetHoldability, statementType); + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * @param resultSetType + * result set type + * @param resultSetConcurrency + * result set concurrency + * @param statementType + * statement type + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency, + final StatementType statementType) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency, statementType); + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * @param columnIndexes + * An array of column indexes indicating the columns that should be returned from the inserted row or + * rows. + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql, final int[] columnIndexes) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull(), columnIndexes); + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * @param statementType + * statement type + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql, final StatementType statementType) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull(), statementType, null); + } + + /** + * Creates a PStmtKey for the given arguments. + * + * @param sql + * the SQL string used to define the statement + * @param columnNames + * column names + * + * @return the PStmtKey created for the given arguments. + */ + protected PStmtKey createKey(final String sql, final String[] columnNames) { + return new PStmtKey(normalizeSQL(sql), getCatalogOrNull(), getSchemaOrNull(), columnNames); + } + + /** + * {@link KeyedPooledObjectFactory} method for destroying PoolablePreparedStatements and PoolableCallableStatements. + * Closes the underlying statement. + * + * @param key + * ignored + * @param pooledObject + * the wrapped pooled statement to be destroyed. + */ + @Override + public void destroyObject(final PStmtKey key, final PooledObject pooledObject) + throws SQLException { + pooledObject.getObject().getInnermostDelegate().close(); + } + + private String getCatalogOrNull() { + try { + return getCatalog(); + } catch (final SQLException ignored) { + return null; + } + } + + private String getSchemaOrNull() { + try { + return getSchema(); + } catch (final SQLException ignored) { + return null; + } + } + + /** + * Gets the prepared statement pool. + * + * @return statement pool + * @since 2.8.0 + */ + public KeyedObjectPool getStatementPool() { + return pStmtPool; + } + + /** + * {@link KeyedPooledObjectFactory} method for creating {@link PoolablePreparedStatement}s or + * {@link PoolableCallableStatement}s. The {@code stmtType} field in the key determines whether a + * PoolablePreparedStatement or PoolableCallableStatement is created. + * + * @param key + * the key for the {@link PreparedStatement} to be created + * @see #createKey(String, int, int, StatementType) + */ + @Override + public PooledObject makeObject(final PStmtKey key) throws SQLException { + if (null == key) { + throw new IllegalArgumentException("Prepared statement key is null or invalid."); + } + if (key.getStmtType() == StatementType.PREPARED_STATEMENT) { + final PreparedStatement statement = (PreparedStatement) key.createStatement(getDelegate()); + @SuppressWarnings({"rawtypes", "unchecked" }) // Unable to find way to avoid this + final PoolablePreparedStatement pps = new PoolablePreparedStatement(statement, key, pStmtPool, this); + return new DefaultPooledObject<>(pps); + } + final CallableStatement statement = (CallableStatement) key.createStatement(getDelegate()); + final PoolableCallableStatement pcs = new PoolableCallableStatement(statement, key, pStmtPool, this); + return new DefaultPooledObject<>(pcs); + } + + /** + * Normalizes the given SQL statement, producing a canonical form that is semantically equivalent to the original. + * + * @param sql The statement to be normalized. + * + * @return The canonical form of the supplied SQL statement. + */ + protected String normalizeSQL(final String sql) { + return sql.trim(); + } + + /** + * {@link KeyedPooledObjectFactory} method for passivating {@link PreparedStatement}s or {@link CallableStatement}s. + * Invokes {@link PreparedStatement#clearParameters}. + * + * @param key + * ignored + * @param pooledObject + * a wrapped {@link PreparedStatement} + */ + @Override + public void passivateObject(final PStmtKey key, final PooledObject pooledObject) + throws SQLException { + final DelegatingPreparedStatement dps = pooledObject.getObject(); + dps.clearParameters(); + dps.passivate(); + } + + /** + * Creates or obtains a {@link CallableStatement} from the pool. + * + * @param key + * a {@link PStmtKey} for the given arguments + * @return a {@link PoolableCallableStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + private CallableStatement prepareCall(final PStmtKey key) throws SQLException { + return (CallableStatement) prepareStatement(key); + } + + /** + * Creates or obtains a {@link CallableStatement} from the pool. + * + * @param sql + * the SQL string used to define the CallableStatement + * @return a {@link PoolableCallableStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + @Override + public CallableStatement prepareCall(final String sql) throws SQLException { + return prepareCall(createKey(sql, StatementType.CALLABLE_STATEMENT)); + } + + /** + * Creates or obtains a {@link CallableStatement} from the pool. + * + * @param sql + * the SQL string used to define the CallableStatement + * @param resultSetType + * result set type + * @param resultSetConcurrency + * result set concurrency + * @return a {@link PoolableCallableStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + @Override + public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency) + throws SQLException { + return prepareCall(createKey(sql, resultSetType, resultSetConcurrency, StatementType.CALLABLE_STATEMENT)); + } + + /** + * Creates or obtains a {@link CallableStatement} from the pool. + * + * @param sql + * the SQL string used to define the CallableStatement + * @param resultSetType + * result set type + * @param resultSetConcurrency + * result set concurrency + * @param resultSetHoldability + * result set holdability + * @return a {@link PoolableCallableStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + @Override + public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + return prepareCall(createKey(sql, resultSetType, resultSetConcurrency, + resultSetHoldability, StatementType.CALLABLE_STATEMENT)); + } + + /** + * Creates or obtains a {@link PreparedStatement} from the pool. + * + * @param key + * a {@link PStmtKey} for the given arguments + * @return a {@link PoolablePreparedStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + private PreparedStatement prepareStatement(final PStmtKey key) throws SQLException { + if (null == pStmtPool) { + throw new SQLException("Statement pool is null - closed or invalid PoolingConnection."); + } + try { + return pStmtPool.borrowObject(key); + } catch (final NoSuchElementException e) { + throw new SQLException("MaxOpenPreparedStatements limit reached", e); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareStatement from pool failed", e); + } + } + + /** + * Creates or obtains a {@link PreparedStatement} from the pool. + * + * @param sql + * the SQL string used to define the PreparedStatement + * @return a {@link PoolablePreparedStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + @Override + public PreparedStatement prepareStatement(final String sql) throws SQLException { + return prepareStatement(createKey(sql)); + } + + /* + * Creates or obtains a {@link PreparedStatement} from the pool. + * + * @param sql + * the SQL string used to define the PreparedStatement + * @param autoGeneratedKeys + * A flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * @return a {@link PoolablePreparedStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + @Override + public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) throws SQLException { + return prepareStatement(createKey(sql, autoGeneratedKeys)); + } + + /** + * Creates or obtains a {@link PreparedStatement} from the pool. + * + * @param sql + * the SQL string used to define the PreparedStatement + * @param resultSetType + * result set type + * @param resultSetConcurrency + * result set concurrency + * @return a {@link PoolablePreparedStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + @Override + public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency) + throws SQLException { + return prepareStatement(createKey(sql, resultSetType, resultSetConcurrency)); + } + + /** + * Creates or obtains a {@link PreparedStatement} from the pool. + * + * @param sql + * the SQL string used to define the PreparedStatement + * @param resultSetType + * result set type + * @param resultSetConcurrency + * result set concurrency + * @param resultSetHoldability + * result set holdability + * @return a {@link PoolablePreparedStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + @Override + public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + return prepareStatement(createKey(sql, resultSetType, resultSetConcurrency, resultSetHoldability)); + } + + /** + * Creates or obtains a {@link PreparedStatement} from the pool. + * + * @param sql + * the SQL string used to define the PreparedStatement + * @param columnIndexes + * An array of column indexes indicating the columns that should be returned from the inserted row or + * rows. + * @return a {@link PoolablePreparedStatement} + * @throws SQLException + * Wraps an underlying exception. + * + */ + @Override + public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) throws SQLException { + return prepareStatement(createKey(sql, columnIndexes)); + } + + /** + * Creates or obtains a {@link PreparedStatement} from the pool. + * + * @param sql + * the SQL string used to define the PreparedStatement + * @param columnNames + * column names + * @return a {@link PoolablePreparedStatement} + * @throws SQLException + * Wraps an underlying exception. + */ + @Override + public PreparedStatement prepareStatement(final String sql, final String[] columnNames) throws SQLException { + return prepareStatement(createKey(sql, columnNames)); + } + + /** + * Sets whether the pool of statements should be cleared when the connection is returned to its pool. + * Default is false. + * + * @param clearStatementPoolOnReturn clear or not + * @since 2.8.0 + */ + public void setClearStatementPoolOnReturn(final boolean clearStatementPoolOnReturn) { + this.clearStatementPoolOnReturn = clearStatementPoolOnReturn; + } + + /** + * Sets the prepared statement pool. + * + * @param pool + * the prepared statement pool. + */ + public void setStatementPool(final KeyedObjectPool pool) { + pStmtPool = pool; + } + + @Override + public synchronized String toString() { + return "PoolingConnection: " + Objects.toString(pStmtPool); + } + + /** + * {@link KeyedPooledObjectFactory} method for validating pooled statements. Currently always returns true. + * + * @param key + * ignored + * @param pooledObject + * ignored + * @return {@code true} + */ + @Override + public boolean validateObject(final PStmtKey key, final PooledObject pooledObject) { + return true; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java b/java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java new file mode 100644 index 0000000..f11e1df --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java @@ -0,0 +1,256 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.dbcp.pool2.ObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool; + +/** + * A simple {@link DataSource} implementation that obtains {@link Connection}s from the specified {@link ObjectPool}. + * + * @param + * The connection type + * + * @since 2.0 + */ +public class PoolingDataSource implements DataSource, AutoCloseable { + + /** + * PoolGuardConnectionWrapper is a Connection wrapper that makes sure a closed connection cannot be used anymore. + * + * @since 2.0 + */ + private class PoolGuardConnectionWrapper extends DelegatingConnection { + + PoolGuardConnectionWrapper(final D delegate) { + super(delegate); + } + + @Override + public void close() throws SQLException { + if (getDelegateInternal() != null) { + super.close(); + super.setDelegate(null); + } + } + + /** + * @see org.apache.tomcat.dbcp.dbcp2.DelegatingConnection#getDelegate() + */ + @Override + public D getDelegate() { + return isAccessToUnderlyingConnectionAllowed() ? super.getDelegate() : null; + } + + /** + * @see org.apache.tomcat.dbcp.dbcp2.DelegatingConnection#getInnermostDelegate() + */ + @Override + public Connection getInnermostDelegate() { + return isAccessToUnderlyingConnectionAllowed() ? super.getInnermostDelegate() : null; + } + + @Override + public boolean isClosed() throws SQLException { + return getDelegateInternal() == null || super.isClosed(); + } + } + + private static final Log log = LogFactory.getLog(PoolingDataSource.class); + + /** Controls access to the underlying connection */ + private boolean accessToUnderlyingConnectionAllowed; + + /** My log writer. */ + private PrintWriter logWriter; + + private final ObjectPool pool; + + /** + * Constructs a new instance backed by the given connection pool. + * + * @param pool + * the given connection pool. + */ + public PoolingDataSource(final ObjectPool pool) { + Objects.requireNonNull(pool, "Pool must not be null."); + this.pool = pool; + // Verify that pool's factory refers back to it. If not, log a warning and try to fix. + if (this.pool instanceof GenericObjectPool) { + final PoolableConnectionFactory pcf = (PoolableConnectionFactory) ((GenericObjectPool) this.pool) + .getFactory(); + Objects.requireNonNull(pcf, "PoolableConnectionFactory must not be null."); + if (pcf.getPool() != this.pool) { + log.warn(Utils.getMessage("poolingDataSource.factoryConfig")); + @SuppressWarnings("unchecked") // PCF must have a pool of PCs + final ObjectPool p = (ObjectPool) this.pool; + pcf.setPool(p); + } + } + } + + /** + * Closes and free all {@link Connection}s from the pool. + * + * @since 2.1 + */ + @Override + public void close() throws RuntimeException, SQLException { + try { + pool.close(); + } catch (final RuntimeException rte) { + throw new RuntimeException(Utils.getMessage("pool.close.fail"), rte); + } catch (final Exception e) { + throw new SQLException(Utils.getMessage("pool.close.fail"), e); + } + } + + /** + * Returns a {@link java.sql.Connection} from my pool, according to the contract specified by + * {@link ObjectPool#borrowObject}. + */ + @Override + public Connection getConnection() throws SQLException { + try { + final C conn = pool.borrowObject(); + if (conn == null) { + return null; + } + return new PoolGuardConnectionWrapper<>(conn); + } catch (final NoSuchElementException e) { + throw new SQLException("Cannot get a connection, pool error " + e.getMessage(), e); + } catch (final SQLException | RuntimeException e) { + throw e; + } catch (final InterruptedException e) { + // Reset the interrupt status so it is visible to callers + Thread.currentThread().interrupt(); + throw new SQLException("Cannot get a connection, general error", e); + } catch (final Exception e) { + throw new SQLException("Cannot get a connection, general error", e); + } + } + + /** + * Throws {@link UnsupportedOperationException} + * + * @throws UnsupportedOperationException + * always thrown + */ + @Override + public Connection getConnection(final String uname, final String passwd) throws SQLException { + throw new UnsupportedOperationException(); + } + + // --- DataSource methods ----------------------------------------- + + /** + * Throws {@link UnsupportedOperationException}. + * + * @throws UnsupportedOperationException + * As this implementation does not support this feature. + */ + @Override + public int getLoginTimeout() { + throw new UnsupportedOperationException("Login timeout is not supported."); + } + + /** + * Returns my log writer. + * + * @return my log writer + * @see DataSource#getLogWriter + */ + @Override + public PrintWriter getLogWriter() { + return logWriter; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + protected ObjectPool getPool() { + return pool; + } + + /** + * Returns the value of the accessToUnderlyingConnectionAllowed property. + * + * @return true if access to the underlying {@link Connection} is allowed, false otherwise. + */ + public boolean isAccessToUnderlyingConnectionAllowed() { + return this.accessToUnderlyingConnectionAllowed; + } + + @Override + public boolean isWrapperFor(final Class iface) throws SQLException { + return iface != null && iface.isInstance(this); + } + + /** + * Sets the value of the accessToUnderlyingConnectionAllowed property. It controls if the PoolGuard allows access to + * the underlying connection. (Default: false) + * + * @param allow + * Access to the underlying connection is granted when true. + */ + public void setAccessToUnderlyingConnectionAllowed(final boolean allow) { + this.accessToUnderlyingConnectionAllowed = allow; + } + + /** + * Throws {@link UnsupportedOperationException}. + * + * @throws UnsupportedOperationException + * As this implementation does not support this feature. + */ + @Override + public void setLoginTimeout(final int seconds) { + throw new UnsupportedOperationException("Login timeout is not supported."); + } + + /** + * Sets my log writer. + * + * @see DataSource#setLogWriter + */ + @Override + public void setLogWriter(final PrintWriter out) { + logWriter = out; + } + + @Override + public T unwrap(final Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return iface.cast(this); + } + throw new SQLException(this + " is not a wrapper for " + iface); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java b/java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java new file mode 100644 index 0000000..9b4c878 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java @@ -0,0 +1,260 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.HashMap; +import java.util.NoSuchElementException; +import java.util.Properties; +import java.util.logging.Logger; + +import org.apache.tomcat.dbcp.pool2.ObjectPool; + +/** + * A {@link Driver} implementation that obtains {@link Connection}s from a registered {@link ObjectPool}. + * + * @since 2.0 + */ +public class PoolingDriver implements Driver { + + /** + * PoolGuardConnectionWrapper is a Connection wrapper that makes sure a closed connection cannot be used anymore. + * + * @since 2.0 + */ + private class PoolGuardConnectionWrapper extends DelegatingConnection { + + private final ObjectPool pool; + + PoolGuardConnectionWrapper(final ObjectPool pool, final Connection delegate) { + super(delegate); + this.pool = pool; + } + + /** + * @see org.apache.tomcat.dbcp.dbcp2.DelegatingConnection#getDelegate() + */ + @Override + public Connection getDelegate() { + if (isAccessToUnderlyingConnectionAllowed()) { + return super.getDelegate(); + } + return null; + } + + /** + * @see org.apache.tomcat.dbcp.dbcp2.DelegatingConnection#getInnermostDelegate() + */ + @Override + public Connection getInnermostDelegate() { + if (isAccessToUnderlyingConnectionAllowed()) { + return super.getInnermostDelegate(); + } + return null; + } + } + + private static final DriverPropertyInfo[] EMPTY_DRIVER_PROPERTY_INFO_ARRAY = {}; + + /* Register myself with the {@link DriverManager}. */ + static { + try { + DriverManager.registerDriver(new PoolingDriver()); + } catch (final Exception ignored) { + // Ignored + } + } + + /** The map of registered pools. */ + protected static final HashMap> pools = new HashMap<>(); + + /** My URL prefix */ + public static final String URL_PREFIX = "jdbc:apache:commons:dbcp:"; + + protected static final int URL_PREFIX_LEN = URL_PREFIX.length(); + + // version numbers + protected static final int MAJOR_VERSION = 1; + + protected static final int MINOR_VERSION = 0; + + /** Controls access to the underlying connection */ + private final boolean accessToUnderlyingConnectionAllowed; + + /** + * Constructs a new driver with {@code accessToUnderlyingConnectionAllowed} enabled. + */ + public PoolingDriver() { + this(true); + } + + /** + * For unit testing purposes. + * + * @param accessToUnderlyingConnectionAllowed + * Do {@link DelegatingConnection}s created by this driver permit access to the delegate? + */ + protected PoolingDriver(final boolean accessToUnderlyingConnectionAllowed) { + this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed; + } + + @Override + public boolean acceptsURL(final String url) throws SQLException { + return url != null && url.startsWith(URL_PREFIX); + } + + /** + * Closes a named pool. + * + * @param name + * The pool name. + * @throws SQLException + * Thrown when a problem is caught closing the pool. + */ + public synchronized void closePool(final String name) throws SQLException { + final ObjectPool pool = pools.get(name); + if (pool != null) { + pools.remove(name); + try { + pool.close(); + } catch (final Exception e) { + throw new SQLException("Error closing pool " + name, e); + } + } + } + + @Override + public Connection connect(final String url, final Properties info) throws SQLException { + if (acceptsURL(url)) { + final ObjectPool pool = getConnectionPool(url.substring(URL_PREFIX_LEN)); + + try { + final Connection conn = pool.borrowObject(); + if (conn == null) { + return null; + } + return new PoolGuardConnectionWrapper(pool, conn); + } catch (final NoSuchElementException e) { + throw new SQLException("Cannot get a connection, pool error: " + e.getMessage(), e); + } catch (final SQLException | RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Cannot get a connection, general error: " + e.getMessage(), e); + } + } + return null; + } + + /** + * Gets the connection pool for the given name. + * + * @param name + * The pool name + * @return The pool + * @throws SQLException + * Thrown when the named pool is not registered. + */ + public synchronized ObjectPool getConnectionPool(final String name) throws SQLException { + final ObjectPool pool = pools.get(name); + if (null == pool) { + throw new SQLException("Pool not registered: " + name); + } + return pool; + } + + @Override + public int getMajorVersion() { + return MAJOR_VERSION; + } + + @Override + public int getMinorVersion() { + return MINOR_VERSION; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * Gets the pool names. + * + * @return the pool names. + */ + public synchronized String[] getPoolNames() { + return pools.keySet().toArray(Utils.EMPTY_STRING_ARRAY); + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(final String url, final Properties info) { + return EMPTY_DRIVER_PROPERTY_INFO_ARRAY; + } + /** + * Invalidates the given connection. + * + * @param conn + * connection to invalidate + * @throws SQLException + * if the connection is not a {@code PoolGuardConnectionWrapper} or an error occurs invalidating + * the connection + */ + public void invalidateConnection(final Connection conn) throws SQLException { + if (!(conn instanceof PoolGuardConnectionWrapper)) { + throw new SQLException("Invalid connection class"); + } + final PoolGuardConnectionWrapper pgconn = (PoolGuardConnectionWrapper) conn; + @SuppressWarnings("unchecked") + final ObjectPool pool = (ObjectPool) pgconn.pool; + try { + pool.invalidateObject(pgconn.getDelegateInternal()); + } catch (final Exception ignored) { + // Ignored. + } + } + + /** + * Returns the value of the accessToUnderlyingConnectionAllowed property. + * + * @return true if access to the underlying is allowed, false otherwise. + */ + protected boolean isAccessToUnderlyingConnectionAllowed() { + return accessToUnderlyingConnectionAllowed; + } + + @Override + public boolean jdbcCompliant() { + return true; + } + + /** + * Registers a named pool. + * + * @param name + * The pool name. + * @param pool + * The pool. + */ + public synchronized void registerPool(final String name, final ObjectPool pool) { + pools.put(name, pool); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java b/java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java new file mode 100644 index 0000000..4d37d63 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java @@ -0,0 +1,56 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.SQLException; +import java.util.List; + +/** + * An SQLException based on a list of Throwable causes. + *

    + * The first exception in the list is used as this exception's cause and is accessible with the usual + * {@link #getCause()} while the complete list is accessible with {@link #getCauseList()}. + *

    + * + * @since 2.7.0 + */ +public class SQLExceptionList extends SQLException { + + private static final long serialVersionUID = 1L; + private final List causeList; + + /** + * Creates a new exception caused by a list of exceptions. + * + * @param causeList a list of cause exceptions. + */ + public SQLExceptionList(final List causeList) { + super(String.format("%,d exceptions: %s", Integer.valueOf(causeList == null ? 0 : causeList.size()), causeList), + causeList == null ? null : causeList.get(0)); + this.causeList = causeList; + } + + /** + * Gets the cause list. + * + * @return The list of causes. + */ + public List getCauseList() { + return causeList; + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java b/java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java new file mode 100644 index 0000000..99f0e16 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java @@ -0,0 +1,61 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import org.apache.juli.logging.Log; +import org.apache.tomcat.dbcp.pool2.SwallowedExceptionListener; + +/** + * Class for logging swallowed exceptions. + * + * @since 2.0 + */ +public class SwallowedExceptionLogger implements SwallowedExceptionListener { + + private final Log log; + private final boolean logExpiredConnections; + + /** + * Create a SwallowedExceptionLogger with the given logger. By default, expired connection logging is turned on. + * + * @param log + * logger + */ + public SwallowedExceptionLogger(final Log log) { + this(log, true); + } + + /** + * Create a SwallowedExceptionLogger with the given logger and expired connection logging property. + * + * @param log + * logger + * @param logExpiredConnections + * false suppresses logging of expired connection events + */ + public SwallowedExceptionLogger(final Log log, final boolean logExpiredConnections) { + this.log = log; + this.logExpiredConnections = logExpiredConnections; + } + + @Override + public void onSwallowException(final Exception e) { + if (logExpiredConnections || !(e instanceof LifetimeExceededException)) { + log.warn(Utils.getMessage("swallowedExceptionLogger.onSwallowedException"), e); + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/Utils.java b/java/org/apache/tomcat/dbcp/dbcp2/Utils.java new file mode 100644 index 0000000..7b44dcd --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/Utils.java @@ -0,0 +1,257 @@ +/* + * 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.tomcat.dbcp.dbcp2; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.text.MessageFormat; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.HashSet; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.function.Consumer; + +import org.apache.tomcat.dbcp.pool2.PooledObject; + +/** + * Utility methods. + * + * @since 2.0 + */ +public final class Utils { + + private static final ResourceBundle messages = ResourceBundle + .getBundle(Utils.class.getPackage().getName() + ".LocalStrings"); + + /** + * Whether the security manager is enabled. + * + * @deprecated No replacement. + */ + @Deprecated + public static final boolean IS_SECURITY_ENABLED = isSecurityEnabled(); + + /** Any SQL_STATE starting with this value is considered a fatal disconnect */ + public static final String DISCONNECTION_SQL_CODE_PREFIX = "08"; + + /** + * SQL codes of fatal connection errors. + *
      + *
    • 57P01 (Admin shutdown)
    • + *
    • 57P02 (Crash shutdown)
    • + *
    • 57P03 (Cannot connect now)
    • + *
    • 01002 (SQL92 disconnect error)
    • + *
    • JZ0C0 (Sybase disconnect error)
    • + *
    • JZ0C1 (Sybase disconnect error)
    • + *
    + * @deprecated Use {@link #getDisconnectionSqlCodes()}. + */ + @Deprecated + public static final Set DISCONNECTION_SQL_CODES; + + static final ResultSet[] EMPTY_RESULT_SET_ARRAY = {}; + + static final String[] EMPTY_STRING_ARRAY = {}; + static { + DISCONNECTION_SQL_CODES = new HashSet<>(); + DISCONNECTION_SQL_CODES.add("57P01"); // Admin shutdown + DISCONNECTION_SQL_CODES.add("57P02"); // Crash shutdown + DISCONNECTION_SQL_CODES.add("57P03"); // Cannot connect now + DISCONNECTION_SQL_CODES.add("01002"); // SQL92 disconnect error + DISCONNECTION_SQL_CODES.add("JZ0C0"); // Sybase disconnect error + DISCONNECTION_SQL_CODES.add("JZ0C1"); // Sybase disconnect error + } + + /** + * Clones the given char[] if not null. + * + * @param value may be null. + * @return a cloned char[] or null. + */ + public static char[] clone(final char[] value) { + return value == null ? null : value.clone(); + } + + /** + * Clones the given {@link Properties} without the standard "user" or "password" entries. + * + * @param properties may be null + * @return a clone of the input without the standard "user" or "password" entries. + * @since 2.8.0 + */ + public static Properties cloneWithoutCredentials(final Properties properties) { + if (properties != null) { + final Properties temp = (Properties) properties.clone(); + temp.remove(Constants.KEY_USER); + temp.remove(Constants.KEY_PASSWORD); + return temp; + } + return properties; + } + + /** + * Closes the given {@link AutoCloseable} and if an exception is caught, then calls {@code exceptionHandler}. + * + * @param autoCloseable The resource to close. + * @param exceptionHandler Consumes exception thrown closing this resource. + * @since 2.10.0 + */ + public static void close(AutoCloseable autoCloseable, final Consumer exceptionHandler) { + if (autoCloseable != null) { + try { + autoCloseable.close(); + } catch (final Exception e) { + if (exceptionHandler != null) { + exceptionHandler.accept(e); + } + } + } + } + + /** + * Closes the AutoCloseable (which may be null). + * + * @param autoCloseable an AutoCloseable, may be {@code null} + * @since 2.6.0 + */ + public static void closeQuietly(final AutoCloseable autoCloseable) { + close(autoCloseable, null); + } + + /** + * Closes the Connection (which may be null). + * + * @param connection a Connection, may be {@code null} + * @deprecated Use {@link #closeQuietly(AutoCloseable)}. + */ + @Deprecated + public static void closeQuietly(final Connection connection) { + closeQuietly((AutoCloseable) connection); + } + + /** + * Closes the ResultSet (which may be null). + * + * @param resultSet a ResultSet, may be {@code null} + * @deprecated Use {@link #closeQuietly(AutoCloseable)}. + */ + @Deprecated + public static void closeQuietly(final ResultSet resultSet) { + closeQuietly((AutoCloseable) resultSet); + } + + /** + * Closes the Statement (which may be null). + * + * @param statement a Statement, may be {@code null}. + * @deprecated Use {@link #closeQuietly(AutoCloseable)}. + */ + @Deprecated + public static void closeQuietly(final Statement statement) { + closeQuietly((AutoCloseable) statement); + } + + /** + * Gets a copy of SQL codes of fatal connection errors. + *
      + *
    • 57P01 (Admin shutdown)
    • + *
    • 57P02 (Crash shutdown)
    • + *
    • 57P03 (Cannot connect now)
    • + *
    • 01002 (SQL92 disconnect error)
    • + *
    • JZ0C0 (Sybase disconnect error)
    • + *
    • JZ0C1 (Sybase disconnect error)
    • + *
    + * @return SQL codes of fatal connection errors. + * @since 2.10.0 + */ + public static Set getDisconnectionSqlCodes() { + return new HashSet<>(DISCONNECTION_SQL_CODES); + } + + /** + * Gets the correct i18n message for the given key. + * + * @param key The key to look up an i18n message. + * @return The i18n message. + */ + public static String getMessage(final String key) { + return getMessage(key, (Object[]) null); + } + + /** + * Gets the correct i18n message for the given key with placeholders replaced by the supplied arguments. + * + * @param key A message key. + * @param args The message arguments. + * @return An i18n message. + */ + public static String getMessage(final String key, final Object... args) { + final String msg = messages.getString(key); + if (args == null || args.length == 0) { + return msg; + } + final MessageFormat mf = new MessageFormat(msg); + return mf.format(args, new StringBuffer(), null).toString(); + } + + static boolean isEmpty(final Collection collection) { + return collection == null || collection.isEmpty(); + } + + static boolean isSecurityEnabled() { + return System.getSecurityManager() != null; + } + + /** + * Converts the given String to a char[]. + * + * @param value may be null. + * @return a char[] or null. + */ + public static char[] toCharArray(final String value) { + return value != null ? value.toCharArray() : null; + } + + /** + * Converts the given char[] to a String. + * + * @param value may be null. + * @return a String or null. + */ + public static String toString(final char[] value) { + return value == null ? null : String.valueOf(value); + } + + public static void validateLifetime(final PooledObject p, final Duration maxDuration) throws LifetimeExceededException { + if (maxDuration.compareTo(Duration.ZERO) > 0) { + final Duration lifetimeDuration = Duration.between(p.getCreateInstant(), Instant.now()); + if (lifetimeDuration.compareTo(maxDuration) > 0) { + throw new LifetimeExceededException(Utils.getMessage("connectionFactory.lifetimeExceeded", lifetimeDuration, maxDuration)); + } + } + } + + private Utils() { + // not instantiable + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java new file mode 100644 index 0000000..6b323dd --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java @@ -0,0 +1,307 @@ +/* + * 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.tomcat.dbcp.dbcp2.cpdsadapter; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.apache.tomcat.dbcp.dbcp2.DelegatingCallableStatement; +import org.apache.tomcat.dbcp.dbcp2.DelegatingConnection; +import org.apache.tomcat.dbcp.dbcp2.DelegatingPreparedStatement; + +/** + * This class is the {@code Connection} that will be returned from + * {@code PooledConnectionImpl.getConnection()}. Most methods are wrappers around the JDBC 1.x + * {@code Connection}. A few exceptions include preparedStatement and close. In accordance with the JDBC + * specification this Connection cannot be used after closed() is called. Any further usage will result in an + * SQLException. + *

    + * ConnectionImpl extends DelegatingConnection to enable access to the underlying connection. + *

    + * + * @since 2.0 + */ +final class ConnectionImpl extends DelegatingConnection { + + private final boolean accessToUnderlyingConnectionAllowed; + + /** The object that instantiated this object */ + private final PooledConnectionImpl pooledConnection; + + /** + * Creates a {@code ConnectionImpl}. + * + * @param pooledConnection + * The PooledConnection that is calling the ctor. + * @param connection + * The JDBC 1.x Connection to wrap. + * @param accessToUnderlyingConnectionAllowed + * if true, then access is allowed to the underlying connection + */ + ConnectionImpl(final PooledConnectionImpl pooledConnection, final Connection connection, + final boolean accessToUnderlyingConnectionAllowed) { + super(connection); + this.pooledConnection = pooledConnection; + this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed; + } + + /** + * Marks the Connection as closed, and notifies the pool that the pooled connection is available. + *

    + * In accordance with the JDBC specification this Connection cannot be used after closed() is called. Any further + * usage will result in an SQLException. + *

    + * + * @throws SQLException + * The database connection couldn't be closed. + */ + @Override + public void close() throws SQLException { + if (!isClosedInternal()) { + try { + passivate(); + } finally { + setClosedInternal(true); + pooledConnection.notifyListeners(); + } + } + } + + /** + * Get the delegated connection, if allowed. + * + * @return the internal connection, or null if access is not allowed. + * @see #isAccessToUnderlyingConnectionAllowed() + */ + @Override + public Connection getDelegate() { + if (isAccessToUnderlyingConnectionAllowed()) { + return getDelegateInternal(); + } + return null; + } + + /** + * Get the innermost connection, if allowed. + * + * @return the innermost internal connection, or null if access is not allowed. + * @see #isAccessToUnderlyingConnectionAllowed() + */ + @Override + public Connection getInnermostDelegate() { + if (isAccessToUnderlyingConnectionAllowed()) { + return super.getInnermostDelegateInternal(); + } + return null; + } + + /** + * If false, getDelegate() and getInnermostDelegate() will return null. + * + * @return true if access is allowed to the underlying connection + * @see ConnectionImpl + */ + public boolean isAccessToUnderlyingConnectionAllowed() { + return accessToUnderlyingConnectionAllowed; + } + + /** + * If pooling of {@code CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * + * @param sql + * an SQL statement that may contain one or more '?' parameter placeholders. Typically this statement is + * specified using JDBC call escape syntax. + * @return a default {@code CallableStatement} object containing the pre-compiled SQL statement. + * @throws SQLException + * Thrown if a database access error occurs or this method is called on a closed connection. + * @since 2.4.0 + */ + @Override + public CallableStatement prepareCall(final String sql) throws SQLException { + checkOpen(); + try { + return new DelegatingCallableStatement(this, pooledConnection.prepareCall(sql)); + } catch (final SQLException e) { + handleException(e); // Does not return + return null; + } + } + + /** + * If pooling of {@code CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * + * @param sql + * a {@code String} object that is the SQL statement to be sent to the database; may contain on or + * more '?' parameters. + * @param resultSetType + * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * a concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @return a {@code CallableStatement} object containing the pre-compiled SQL statement that will produce + * {@code ResultSet} objects with the given type and concurrency. + * @throws SQLException + * Thrown if a database access error occurs, this method is called on a closed connection or the given + * parameters are not {@code ResultSet} constants indicating type and concurrency. + * @since 2.4.0 + */ + @Override + public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency) + throws SQLException { + checkOpen(); + try { + return new DelegatingCallableStatement(this, + pooledConnection.prepareCall(sql, resultSetType, resultSetConcurrency)); + } catch (final SQLException e) { + handleException(e); // Does not return + return null; + } + } + + /** + * If pooling of {@code CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * + * @param sql + * a {@code String} object that is the SQL statement to be sent to the database; may contain on or + * more '?' parameters. + * @param resultSetType + * one of the following {@code ResultSet} constants: {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * one of the following {@code ResultSet} constants: {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetHoldability + * one of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} + * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @return a new {@code CallableStatement} object, containing the pre-compiled SQL statement, that will + * generate {@code ResultSet} objects with the given type, concurrency, and holdability. + * @throws SQLException + * Thrown if a database access error occurs, this method is called on a closed connection or the given + * parameters are not {@code ResultSet} constants indicating type, concurrency, and holdability. + * @since 2.4.0 + */ + @Override + public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + checkOpen(); + try { + return new DelegatingCallableStatement(this, + pooledConnection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability)); + } catch (final SQLException e) { + handleException(e); // Does not return + return null; + } + } + + /** + * If pooling of {@code PreparedStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * + * @param sql + * SQL statement to be prepared + * @return the prepared statement + * @throws SQLException + * if this connection is closed or an error occurs in the wrapped connection. + */ + @Override + public PreparedStatement prepareStatement(final String sql) throws SQLException { + checkOpen(); + try { + return new DelegatingPreparedStatement(this, pooledConnection.prepareStatement(sql)); + } catch (final SQLException e) { + handleException(e); // Does not return + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) throws SQLException { + checkOpen(); + try { + return new DelegatingPreparedStatement(this, pooledConnection.prepareStatement(sql, autoGeneratedKeys)); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + /** + * If pooling of {@code PreparedStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * + * @throws SQLException + * if this connection is closed or an error occurs in the wrapped connection. + */ + @Override + public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency) + throws SQLException { + checkOpen(); + try { + return new DelegatingPreparedStatement(this, + pooledConnection.prepareStatement(sql, resultSetType, resultSetConcurrency)); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + // + // Methods for accessing the delegate connection + // + + @Override + public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + checkOpen(); + try { + return new DelegatingPreparedStatement(this, + pooledConnection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability)); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) throws SQLException { + checkOpen(); + try { + return new DelegatingPreparedStatement(this, pooledConnection.prepareStatement(sql, columnIndexes)); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + + @Override + public PreparedStatement prepareStatement(final String sql, final String[] columnNames) throws SQLException { + checkOpen(); + try { + return new DelegatingPreparedStatement(this, pooledConnection.prepareStatement(sql, columnNames)); + } catch (final SQLException e) { + handleException(e); + return null; + } + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java new file mode 100644 index 0000000..3d93a50 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java @@ -0,0 +1,814 @@ +/* + * 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.tomcat.dbcp.dbcp2.cpdsadapter; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.time.Duration; +import java.util.Hashtable; +import java.util.Properties; +import java.util.logging.Logger; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.naming.StringRefAddr; +import javax.naming.spi.ObjectFactory; +import javax.sql.ConnectionPoolDataSource; +import javax.sql.PooledConnection; + +import org.apache.tomcat.dbcp.dbcp2.Constants; +import org.apache.tomcat.dbcp.dbcp2.DelegatingPreparedStatement; +import org.apache.tomcat.dbcp.dbcp2.PStmtKey; +import org.apache.tomcat.dbcp.dbcp2.Utils; +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.BaseObjectPoolConfig; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPoolConfig; + +/** + *

    + * An adapter for JDBC drivers that do not include an implementation of {@link javax.sql.ConnectionPoolDataSource}, but + * still include a {@link java.sql.DriverManager} implementation. {@code ConnectionPoolDataSource}s are not used + * within general applications. They are used by {@code DataSource} implementations that pool + * {@code Connection}s, such as {@link org.apache.tomcat.dbcp.dbcp2.datasources.SharedPoolDataSource}. A J2EE container + * will normally provide some method of initializing the {@code ConnectionPoolDataSource} whose attributes are + * presented as bean getters/setters and then deploying it via JNDI. It is then available as a source of physical + * connections to the database, when the pooling {@code DataSource} needs to create a new physical connection. + *

    + *

    + * Although normally used within a JNDI environment, the DriverAdapterCPDS can be instantiated and initialized as any + * bean and then attached directly to a pooling {@code DataSource}. {@code Jdbc2PoolDataSource} can use the + * {@code ConnectionPoolDataSource} with or without the use of JNDI. + *

    + *

    + * The DriverAdapterCPDS also provides {@code PreparedStatement} pooling which is not generally available in jdbc2 + * {@code ConnectionPoolDataSource} implementation, but is addressed within the jdbc3 specification. The + * {@code PreparedStatement} pool in DriverAdapterCPDS has been in the dbcp package for some time, but it has not + * undergone extensive testing in the configuration used here. It should be considered experimental and can be toggled + * with the poolPreparedStatements attribute. + *

    + *

    + * The package documentation contains an example using catalina and JNDI. The + * datasources package documentation shows how to use + * {@code DriverAdapterCPDS} as a source for {@code Jdbc2PoolDataSource} without the use of JNDI. + *

    + * + * @since 2.0 + */ +public class DriverAdapterCPDS implements ConnectionPoolDataSource, Referenceable, Serializable, ObjectFactory { + + private static final long serialVersionUID = -4820523787212147844L; + + private static final String GET_CONNECTION_CALLED = "A PooledConnection was already requested from this source, " + + "further initialization is not allowed."; + + static { + // Attempt to prevent deadlocks - see DBCP - 272 + DriverManager.getDrivers(); + } + + /** Description */ + private String description; + + /** Connection string */ + private String connectionString; + + /** User name */ + private String userName; + + /** User password */ + private char[] userPassword; + + /** Driver class name */ + private String driver; + + /** Login TimeOut in seconds */ + private int loginTimeout; + + /** Log stream. NOT USED */ + private transient PrintWriter logWriter; + + // PreparedStatement pool properties + private boolean poolPreparedStatements; + private int maxIdle = 10; + private Duration durationBetweenEvictionRuns = BaseObjectPoolConfig.DEFAULT_DURATION_BETWEEN_EVICTION_RUNS; + private int numTestsPerEvictionRun = -1; + private Duration minEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION; + + private int maxPreparedStatements = -1; + + /** Whether or not getConnection has been called */ + private volatile boolean getConnectionCalled; + + /** Connection properties passed to JDBC Driver */ + private Properties connectionProperties; + + /** + * Controls access to the underlying connection + */ + private boolean accessToUnderlyingConnectionAllowed; + + /** + * Default no-argument constructor for Serialization + */ + public DriverAdapterCPDS() { + } + + /** + * Throws an IllegalStateException, if a PooledConnection has already been requested. + */ + private void assertInitializationAllowed() throws IllegalStateException { + if (getConnectionCalled) { + throw new IllegalStateException(GET_CONNECTION_CALLED); + } + } + + private boolean getBooleanContentString(final RefAddr ra) { + return Boolean.parseBoolean(getStringContent(ra)); + } + + /** + * Gets the connection properties passed to the JDBC driver. + * + * @return the JDBC connection properties used when creating connections. + */ + public Properties getConnectionProperties() { + return connectionProperties; + } + + /** + * Gets the value of description. This property is here for use by the code which will deploy this data source. It + * is not used internally. + * + * @return value of description, may be null. + * @see #setDescription(String) + */ + public String getDescription() { + return description; + } + + /** + * Gets the driver class name. + * + * @return value of driver. + */ + public String getDriver() { + return driver; + } + + /** + * Gets the duration to sleep between runs of the idle object evictor thread. When non-positive, no + * idle object evictor thread will be run. + * + * @return the value of the evictor thread timer + * @see #setDurationBetweenEvictionRuns(Duration) + * @since 2.9.0 + */ + public Duration getDurationBetweenEvictionRuns() { + return durationBetweenEvictionRuns; + } + + private int getIntegerStringContent(final RefAddr ra) { + return Integer.parseInt(getStringContent(ra)); + } + + /** + * Gets the maximum time in seconds that this data source can wait while attempting to connect to a database. NOT + * USED. + */ + @Override + public int getLoginTimeout() { + return loginTimeout; + } + + /** + * Gets the log writer for this data source. NOT USED. + */ + @Override + public PrintWriter getLogWriter() { + return logWriter; + } + + /** + * Gets the maximum number of statements that can remain idle in the pool, without extra ones being released, or + * negative for no limit. + * + * @return the value of maxIdle + */ + public int getMaxIdle() { + return maxIdle; + } + + /** + * Gets the maximum number of prepared statements. + * + * @return maxPrepartedStatements value + */ + public int getMaxPreparedStatements() { + return maxPreparedStatements; + } + + /** + * Gets the minimum amount of time a statement may sit idle in the pool before it is eligible for eviction by the + * idle object evictor (if any). + * + * @see #setMinEvictableIdleDuration + * @see #setDurationBetweenEvictionRuns + * @return the minimum amount of time a statement may sit idle in the pool. + * @since 2.9.0 + */ + public Duration getMinEvictableIdleDuration() { + return minEvictableIdleDuration; + } + + /** + * Gets the minimum amount of time a statement may sit idle in the pool before it is eligible for eviction by the + * idle object evictor (if any). + * + * @see #setMinEvictableIdleTimeMillis + * @see #setTimeBetweenEvictionRunsMillis + * @return the minimum amount of time a statement may sit idle in the pool. + * @deprecated USe {@link #getMinEvictableIdleDuration()}. + */ + @Deprecated + public int getMinEvictableIdleTimeMillis() { + return (int) minEvictableIdleDuration.toMillis(); + } + + /** + * Gets the number of statements to examine during each run of the idle object evictor thread (if any.) + * + * @see #setNumTestsPerEvictionRun + * @see #setTimeBetweenEvictionRunsMillis + * @return the number of statements to examine during each run of the idle object evictor thread (if any.) + */ + public int getNumTestsPerEvictionRun() { + return numTestsPerEvictionRun; + } + + /** + * Implements {@link ObjectFactory} to create an instance of this class + */ + @Override + public Object getObjectInstance(final Object refObj, final Name name, final Context context, + final Hashtable env) throws ClassNotFoundException { + // The spec says to return null if we can't create an instance + // of the reference + DriverAdapterCPDS cpds = null; + if (refObj instanceof Reference) { + final Reference ref = (Reference) refObj; + if (ref.getClassName().equals(getClass().getName())) { + RefAddr ra = ref.get("description"); + if (isNotEmpty(ra)) { + setDescription(getStringContent(ra)); + } + + ra = ref.get("driver"); + if (isNotEmpty(ra)) { + setDriver(getStringContent(ra)); + } + ra = ref.get("url"); + if (isNotEmpty(ra)) { + setUrl(getStringContent(ra)); + } + ra = ref.get(Constants.KEY_USER); + if (isNotEmpty(ra)) { + setUser(getStringContent(ra)); + } + ra = ref.get(Constants.KEY_PASSWORD); + if (isNotEmpty(ra)) { + setPassword(getStringContent(ra)); + } + + ra = ref.get("poolPreparedStatements"); + if (isNotEmpty(ra)) { + setPoolPreparedStatements(getBooleanContentString(ra)); + } + ra = ref.get("maxIdle"); + if (isNotEmpty(ra)) { + setMaxIdle(getIntegerStringContent(ra)); + } + + ra = ref.get("timeBetweenEvictionRunsMillis"); + if (isNotEmpty(ra)) { + setTimeBetweenEvictionRunsMillis(getIntegerStringContent(ra)); + } + + ra = ref.get("numTestsPerEvictionRun"); + if (isNotEmpty(ra)) { + setNumTestsPerEvictionRun(getIntegerStringContent(ra)); + } + + ra = ref.get("minEvictableIdleTimeMillis"); + if (isNotEmpty(ra)) { + setMinEvictableIdleTimeMillis(getIntegerStringContent(ra)); + } + + ra = ref.get("maxPreparedStatements"); + if (isNotEmpty(ra)) { + setMaxPreparedStatements(getIntegerStringContent(ra)); + } + + ra = ref.get("accessToUnderlyingConnectionAllowed"); + if (isNotEmpty(ra)) { + setAccessToUnderlyingConnectionAllowed(getBooleanContentString(ra)); + } + + cpds = this; + } + } + return cpds; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * Gets the value of password for the default user. + * + * @return value of password. + */ + public String getPassword() { + return Utils.toString(userPassword); + } + + /** + * Gets the value of password for the default user. + * + * @return value of password. + * @since 2.4.0 + */ + public char[] getPasswordCharArray() { + return Utils.clone(userPassword); + } + + /** + * Attempts to establish a database connection using the default user and password. + */ + @Override + public PooledConnection getPooledConnection() throws SQLException { + return getPooledConnection(getUser(), getPassword()); + } + + /** + * Attempts to establish a database connection. + * + * @param pooledUserName name to be used for the connection + * @param pooledUserPassword password to be used fur the connection + */ + @Override + public PooledConnection getPooledConnection(final String pooledUserName, final String pooledUserPassword) + throws SQLException { + getConnectionCalled = true; + PooledConnectionImpl pooledConnection = null; + // Workaround for buggy WebLogic 5.1 class loader - ignore the exception upon first invocation. + try { + if (connectionProperties != null) { + update(connectionProperties, Constants.KEY_USER, pooledUserName); + update(connectionProperties, Constants.KEY_PASSWORD, pooledUserPassword); + pooledConnection = new PooledConnectionImpl( + DriverManager.getConnection(getUrl(), connectionProperties)); + } else { + pooledConnection = new PooledConnectionImpl( + DriverManager.getConnection(getUrl(), pooledUserName, pooledUserPassword)); + } + pooledConnection.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed()); + } catch (final ClassCircularityError e) { + if (connectionProperties != null) { + pooledConnection = new PooledConnectionImpl( + DriverManager.getConnection(getUrl(), connectionProperties)); + } else { + pooledConnection = new PooledConnectionImpl( + DriverManager.getConnection(getUrl(), pooledUserName, pooledUserPassword)); + } + pooledConnection.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed()); + } + KeyedObjectPool stmtPool = null; + if (isPoolPreparedStatements()) { + final GenericKeyedObjectPoolConfig config = new GenericKeyedObjectPoolConfig<>(); + config.setMaxTotalPerKey(Integer.MAX_VALUE); + config.setBlockWhenExhausted(false); + config.setMaxWait(Duration.ZERO); + config.setMaxIdlePerKey(getMaxIdle()); + if (getMaxPreparedStatements() <= 0) { + // Since there is no limit, create a prepared statement pool with an eviction thread; + // evictor settings are the same as the connection pool settings. + config.setTimeBetweenEvictionRuns(getDurationBetweenEvictionRuns()); + config.setNumTestsPerEvictionRun(getNumTestsPerEvictionRun()); + config.setMinEvictableIdleDuration(getMinEvictableIdleDuration()); + } else { + // Since there is a limit, create a prepared statement pool without an eviction thread; + // pool has LRU functionality so when the limit is reached, 15% of the pool is cleared. + // see org.apache.commons.pool2.impl.GenericKeyedObjectPool.clearOldest method + config.setMaxTotal(getMaxPreparedStatements()); + config.setTimeBetweenEvictionRuns(Duration.ofMillis(-1)); + config.setNumTestsPerEvictionRun(0); + config.setMinEvictableIdleDuration(Duration.ZERO); + } + stmtPool = new GenericKeyedObjectPool<>(pooledConnection, config); + pooledConnection.setStatementPool(stmtPool); + } + return pooledConnection; + } + + /** + * Implements {@link Referenceable}. + */ + @Override + public Reference getReference() throws NamingException { + // this class implements its own factory + final String factory = getClass().getName(); + + final Reference ref = new Reference(getClass().getName(), factory, null); + + ref.add(new StringRefAddr("description", getDescription())); + ref.add(new StringRefAddr("driver", getDriver())); + ref.add(new StringRefAddr("loginTimeout", String.valueOf(getLoginTimeout()))); + ref.add(new StringRefAddr(Constants.KEY_PASSWORD, getPassword())); + ref.add(new StringRefAddr(Constants.KEY_USER, getUser())); + ref.add(new StringRefAddr("url", getUrl())); + + ref.add(new StringRefAddr("poolPreparedStatements", String.valueOf(isPoolPreparedStatements()))); + ref.add(new StringRefAddr("maxIdle", String.valueOf(getMaxIdle()))); + ref.add(new StringRefAddr("numTestsPerEvictionRun", String.valueOf(getNumTestsPerEvictionRun()))); + ref.add(new StringRefAddr("maxPreparedStatements", String.valueOf(getMaxPreparedStatements()))); + // + // Pair of current and deprecated. + ref.add(new StringRefAddr("durationBetweenEvictionRuns", String.valueOf(getDurationBetweenEvictionRuns()))); + ref.add(new StringRefAddr("timeBetweenEvictionRunsMillis", String.valueOf(getTimeBetweenEvictionRunsMillis()))); + // + // Pair of current and deprecated. + ref.add(new StringRefAddr("minEvictableIdleDuration", String.valueOf(getMinEvictableIdleDuration()))); + ref.add(new StringRefAddr("minEvictableIdleTimeMillis", String.valueOf(getMinEvictableIdleTimeMillis()))); + + return ref; + } + + private String getStringContent(final RefAddr ra) { + return ra.getContent().toString(); + } + + /** + * Gets the number of milliseconds to sleep between runs of the idle object evictor thread. When non-positive, no + * idle object evictor thread will be run. + * + * @return the value of the evictor thread timer + * @see #setDurationBetweenEvictionRuns(Duration) + * @deprecated Use {@link #getDurationBetweenEvictionRuns()}. + */ + @Deprecated + public long getTimeBetweenEvictionRunsMillis() { + return durationBetweenEvictionRuns.toMillis(); + } + + /** + * Gets the value of connection string used to locate the database for this data source. + * + * @return value of connection string. + */ + public String getUrl() { + return connectionString; + } + + /** + * Gets the value of default user (login or user name). + * + * @return value of user. + */ + public String getUser() { + return userName; + } + + /** + * Returns the value of the accessToUnderlyingConnectionAllowed property. + * + * @return true if access to the underlying is allowed, false otherwise. + */ + public synchronized boolean isAccessToUnderlyingConnectionAllowed() { + return this.accessToUnderlyingConnectionAllowed; + } + + private boolean isNotEmpty(final RefAddr ra) { + return ra != null && ra.getContent() != null; + } + + /** + * Whether to toggle the pooling of {@code PreparedStatement}s + * + * @return value of poolPreparedStatements. + */ + public boolean isPoolPreparedStatements() { + return poolPreparedStatements; + } + + /** + * Sets the value of the accessToUnderlyingConnectionAllowed property. It controls if the PoolGuard allows access to + * the underlying connection. (Default: false) + * + * @param allow Access to the underlying connection is granted when true. + */ + public synchronized void setAccessToUnderlyingConnectionAllowed(final boolean allow) { + this.accessToUnderlyingConnectionAllowed = allow; + } + + /** + * Sets the connection properties passed to the JDBC driver. + *

    + * If {@code props} contains "user" and/or "password" properties, the corresponding instance properties are + * set. If these properties are not present, they are filled in using {@link #getUser()}, {@link #getPassword()} + * when {@link #getPooledConnection()} is called, or using the actual parameters to the method call when + * {@link #getPooledConnection(String, String)} is called. Calls to {@link #setUser(String)} or + * {@link #setPassword(String)} overwrite the values of these properties if {@code connectionProperties} is not + * null. + *

    + * + * @param props Connection properties to use when creating new connections. + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + */ + public void setConnectionProperties(final Properties props) { + assertInitializationAllowed(); + connectionProperties = props; + if (connectionProperties != null) { + if (connectionProperties.containsKey(Constants.KEY_USER)) { + setUser(connectionProperties.getProperty(Constants.KEY_USER)); + } + if (connectionProperties.containsKey(Constants.KEY_PASSWORD)) { + setPassword(connectionProperties.getProperty(Constants.KEY_PASSWORD)); + } + } + } + + /** + * Sets the value of description. This property is here for use by the code which will deploy this datasource. It is + * not used internally. + * + * @param description Value to assign to description. + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * Sets the driver class name. Setting the driver class name cause the driver to be registered with the + * DriverManager. + * + * @param driver Value to assign to driver. + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + * @throws ClassNotFoundException if the class cannot be located + */ + public void setDriver(final String driver) throws ClassNotFoundException { + assertInitializationAllowed(); + this.driver = driver; + // make sure driver is registered + Class.forName(driver); + } + + /** + * Sets the duration to sleep between runs of the idle object evictor thread. When non-positive, no + * idle object evictor thread will be run. + * + * @param durationBetweenEvictionRuns The duration to sleep between runs of the idle object evictor + * thread. When non-positive, no idle object evictor thread will be run. + * @see #getDurationBetweenEvictionRuns() + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + * @since 2.9.0 + */ + public void setDurationBetweenEvictionRuns(final Duration durationBetweenEvictionRuns) { + assertInitializationAllowed(); + this.durationBetweenEvictionRuns = durationBetweenEvictionRuns; + } + + /** + * Sets the maximum time in seconds that this data source will wait while attempting to connect to a database. NOT + * USED. + */ + @Override + public void setLoginTimeout(final int seconds) { + this.loginTimeout = seconds; + } + + /** + * Sets the log writer for this data source. NOT USED. + */ + @Override + public void setLogWriter(final PrintWriter logWriter) { + this.logWriter = logWriter; + } + + /** + * Gets the maximum number of statements that can remain idle in the pool, without extra ones being released, or + * negative for no limit. + * + * @param maxIdle The maximum number of statements that can remain idle + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + */ + public void setMaxIdle(final int maxIdle) { + assertInitializationAllowed(); + this.maxIdle = maxIdle; + } + + /** + * Sets the maximum number of prepared statements. + * + * @param maxPreparedStatements the new maximum number of prepared statements + */ + public void setMaxPreparedStatements(final int maxPreparedStatements) { + this.maxPreparedStatements = maxPreparedStatements; + } + + /** + * Sets the minimum amount of time a statement may sit idle in the pool before it is eligible for eviction by the + * idle object evictor (if any). When non-positive, no objects will be evicted from the pool due to idle time alone. + * + * @param minEvictableIdleDuration minimum time to set in milliseconds. + * @see #getMinEvictableIdleDuration() + * @see #setDurationBetweenEvictionRuns(Duration) + * @throws IllegalStateException if {@link #getPooledConnection()} has been called. + * @since 2.9.0 + */ + public void setMinEvictableIdleDuration(final Duration minEvictableIdleDuration) { + assertInitializationAllowed(); + this.minEvictableIdleDuration = minEvictableIdleDuration; + } + + /** + * Sets the minimum amount of time a statement may sit idle in the pool before it is eligible for eviction by the + * idle object evictor (if any). When non-positive, no objects will be evicted from the pool due to idle time alone. + * + * @param minEvictableIdleTimeMillis minimum time to set in milliseconds. + * @see #getMinEvictableIdleDuration() + * @see #setDurationBetweenEvictionRuns(Duration) + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + * @deprecated Use {@link #setMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public void setMinEvictableIdleTimeMillis(final int minEvictableIdleTimeMillis) { + assertInitializationAllowed(); + this.minEvictableIdleDuration = Duration.ofMillis(minEvictableIdleTimeMillis); + } + + /** + * Sets the number of statements to examine during each run of the idle object evictor thread (if any). + *

    + * When a negative value is supplied, + * {@code ceil({@link BasicDataSource#getNumIdle})/abs({@link #getNumTestsPerEvictionRun})} tests will be run. + * I.e., when the value is -n, roughly one nth of the idle objects will be tested per run. + *

    + * + * @param numTestsPerEvictionRun number of statements to examine per run + * @see #getNumTestsPerEvictionRun() + * @see #setDurationBetweenEvictionRuns(Duration) + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + */ + public void setNumTestsPerEvictionRun(final int numTestsPerEvictionRun) { + assertInitializationAllowed(); + this.numTestsPerEvictionRun = numTestsPerEvictionRun; + } + + /** + * Sets the value of password for the default user. + * + * @param userPassword Value to assign to password. + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + */ + public void setPassword(final char[] userPassword) { + assertInitializationAllowed(); + this.userPassword = Utils.clone(userPassword); + update(connectionProperties, Constants.KEY_PASSWORD, Utils.toString(this.userPassword)); + } + + /** + * Sets the value of password for the default user. + * + * @param userPassword Value to assign to password. + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + */ + public void setPassword(final String userPassword) { + assertInitializationAllowed(); + this.userPassword = Utils.toCharArray(userPassword); + update(connectionProperties, Constants.KEY_PASSWORD, userPassword); + } + + /** + * Whether to toggle the pooling of {@code PreparedStatement}s + * + * @param poolPreparedStatements true to pool statements. + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + */ + public void setPoolPreparedStatements(final boolean poolPreparedStatements) { + assertInitializationAllowed(); + this.poolPreparedStatements = poolPreparedStatements; + } + + /** + * Sets the number of milliseconds to sleep between runs of the idle object evictor thread. When non-positive, no + * idle object evictor thread will be run. + * + * @param timeBetweenEvictionRunsMillis The number of milliseconds to sleep between runs of the idle object evictor + * thread. When non-positive, no idle object evictor thread will be run. + * @see #getDurationBetweenEvictionRuns() + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + * @deprecated Use {@link #setDurationBetweenEvictionRuns(Duration)}. + */ + @Deprecated + public void setTimeBetweenEvictionRunsMillis(final long timeBetweenEvictionRunsMillis) { + assertInitializationAllowed(); + this.durationBetweenEvictionRuns = Duration.ofMillis(timeBetweenEvictionRunsMillis); + } + + /** + * Sets the value of URL string used to locate the database for this data source. + * + * @param connectionString Value to assign to connection string. + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + */ + public void setUrl(final String connectionString) { + assertInitializationAllowed(); + this.connectionString = connectionString; + } + + /** + * Sets the value of default user (login or user name). + * + * @param userName Value to assign to user. + * @throws IllegalStateException if {@link #getPooledConnection()} has been called + */ + public void setUser(final String userName) { + assertInitializationAllowed(); + this.userName = userName; + update(connectionProperties, Constants.KEY_USER, userName); + } + + /** + * Does not print the userName and userPassword field nor the 'user' or 'password' in the connectionProperties. + * + * @since 2.6.0 + */ + @Override + public synchronized String toString() { + final StringBuilder builder = new StringBuilder(super.toString()); + builder.append("[description="); + builder.append(description); + builder.append(", connectionString="); + // TODO What if the connection string contains a 'user' or 'password' query parameter but that connection string + // is not in a legal URL format? + builder.append(connectionString); + builder.append(", driver="); + builder.append(driver); + builder.append(", loginTimeout="); + builder.append(loginTimeout); + builder.append(", poolPreparedStatements="); + builder.append(poolPreparedStatements); + builder.append(", maxIdle="); + builder.append(maxIdle); + builder.append(", timeBetweenEvictionRunsMillis="); + builder.append(durationBetweenEvictionRuns); + builder.append(", numTestsPerEvictionRun="); + builder.append(numTestsPerEvictionRun); + builder.append(", minEvictableIdleTimeMillis="); + builder.append(minEvictableIdleDuration); + builder.append(", maxPreparedStatements="); + builder.append(maxPreparedStatements); + builder.append(", getConnectionCalled="); + builder.append(getConnectionCalled); + builder.append(", connectionProperties="); + builder.append(Utils.cloneWithoutCredentials(connectionProperties)); + builder.append(", accessToUnderlyingConnectionAllowed="); + builder.append(accessToUnderlyingConnectionAllowed); + builder.append("]"); + return builder.toString(); + } + + private void update(final Properties properties, final String key, final String value) { + if (properties != null && key != null) { + if (value == null) { + properties.remove(key); + } else { + properties.setProperty(key, value); + } + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java new file mode 100644 index 0000000..68e871e --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java @@ -0,0 +1,113 @@ +/* + * 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.tomcat.dbcp.dbcp2.cpdsadapter; + +import org.apache.tomcat.dbcp.dbcp2.PStmtKey; + +/** + * A key uniquely identifying a {@link java.sql.PreparedStatement PreparedStatement}. + * + * @since 2.0 + * @deprecated Use {@link PStmtKey}. + */ +@Deprecated +public class PStmtKeyCPDS extends PStmtKey { + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql + * The SQL statement. + */ + public PStmtKeyCPDS(final String sql) { + super(sql); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql + * The SQL statement. + * @param autoGeneratedKeys + * A flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + */ + public PStmtKeyCPDS(final String sql, final int autoGeneratedKeys) { + super(sql, null, autoGeneratedKeys); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql + * The SQL statement. + * @param resultSetType + * A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + */ + public PStmtKeyCPDS(final String sql, final int resultSetType, final int resultSetConcurrency) { + super(sql, resultSetType, resultSetConcurrency); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql + * The SQL statement. + * @param resultSetType + * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE} + * @param resultSetHoldability + * One of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} + * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + */ + public PStmtKeyCPDS(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) { + super(sql, null, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql + * The SQL statement. + * @param columnIndexes + * An array of column indexes indicating the columns that should be returned from the inserted row or + * rows. + */ + public PStmtKeyCPDS(final String sql, final int[] columnIndexes) { + super(sql, null, columnIndexes); + } + + /** + * Constructs a key to uniquely identify a prepared statement. + * + * @param sql + * The SQL statement. + * @param columnNames + * An array of column names indicating the columns that should be returned from the inserted row or rows. + */ + public PStmtKeyCPDS(final String sql, final String[] columnNames) { + super(sql, null, columnNames); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java new file mode 100644 index 0000000..b2e9e5c --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java @@ -0,0 +1,730 @@ +/* + * 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.tomcat.dbcp.dbcp2.cpdsadapter; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.sql.ConnectionEvent; +import javax.sql.ConnectionEventListener; +import javax.sql.PooledConnection; +import javax.sql.StatementEventListener; + +import org.apache.tomcat.dbcp.dbcp2.DelegatingConnection; +import org.apache.tomcat.dbcp.dbcp2.DelegatingPreparedStatement; +import org.apache.tomcat.dbcp.dbcp2.Jdbc41Bridge; +import org.apache.tomcat.dbcp.dbcp2.PStmtKey; +import org.apache.tomcat.dbcp.dbcp2.PoolableCallableStatement; +import org.apache.tomcat.dbcp.dbcp2.PoolablePreparedStatement; +import org.apache.tomcat.dbcp.dbcp2.PoolingConnection.StatementType; +import org.apache.tomcat.dbcp.dbcp2.Utils; +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.KeyedPooledObjectFactory; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.impl.DefaultPooledObject; + +/** + * Implementation of {@link PooledConnection} that is returned by {@link DriverAdapterCPDS}. + * + * @since 2.0 + */ +final class PooledConnectionImpl + implements PooledConnection, KeyedPooledObjectFactory { + + private static final String CLOSED = "Attempted to use PooledConnection after closed() was called."; + + /** + * The JDBC database connection that represents the physical db connection. + */ + private Connection connection; + + /** + * A DelegatingConnection used to create a PoolablePreparedStatementStub. + */ + private final DelegatingConnection delegatingConnection; + + /** + * The JDBC database logical connection. + */ + private Connection logicalConnection; + + /** + * ConnectionEventListeners. + */ + private final List eventListeners; + + /** + * StatementEventListeners. + */ + private final List statementEventListeners = Collections.synchronizedList(new ArrayList<>()); + + /** + * Flag set to true, once {@link #close()} is called. + */ + private boolean closed; + + /** My pool of {@link PreparedStatement}s. */ + private KeyedObjectPool pStmtPool; + + /** + * Controls access to the underlying connection. + */ + private boolean accessToUnderlyingConnectionAllowed; + + /** + * Wraps the real connection. + * + * @param connection + * the connection to be wrapped. + */ + PooledConnectionImpl(final Connection connection) { + this.connection = connection; + if (connection instanceof DelegatingConnection) { + this.delegatingConnection = (DelegatingConnection) connection; + } else { + this.delegatingConnection = new DelegatingConnection<>(connection); + } + eventListeners = Collections.synchronizedList(new ArrayList<>()); + closed = false; + } + + /** + * My {@link KeyedPooledObjectFactory} method for activating {@link PreparedStatement}s. + * + * @param pooledObject Activates the underlying object. + */ + @Override + public void activateObject(final PStmtKey key, final PooledObject pooledObject) + throws SQLException { + pooledObject.getObject().activate(); + } + + @Override + public void addConnectionEventListener(final ConnectionEventListener listener) { + if (!eventListeners.contains(listener)) { + eventListeners.add(listener); + } + } + + @Override + public void addStatementEventListener(final StatementEventListener listener) { + if (!statementEventListeners.contains(listener)) { + statementEventListeners.add(listener); + } + } + + /** + * Throws an SQLException, if isClosed is true + */ + private void assertOpen() throws SQLException { + if (closed || connection == null) { + throw new SQLException(CLOSED); + } + } + + /** + * Closes the physical connection and marks this {@code PooledConnection} so that it may not be used to + * generate any more logical {@code Connection}s. + * + * @throws SQLException + * Thrown when an error occurs or the connection is already closed. + */ + @Override + public void close() throws SQLException { + assertOpen(); + closed = true; + try { + if (pStmtPool != null) { + try { + pStmtPool.close(); + } finally { + pStmtPool = null; + } + } + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Cannot close connection (return to pool failed)", e); + } finally { + try { + connection.close(); + } finally { + connection = null; + } + } + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @return a {@link PStmtKey} for the given arguments. + */ + protected PStmtKey createKey(final String sql) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull()); + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @param autoGeneratedKeys + * A flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * @return a key to uniquely identify a prepared statement. + */ + protected PStmtKey createKey(final String sql, final int autoGeneratedKeys) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), autoGeneratedKeys); + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @param resultSetType + * A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @return a key to uniquely identify a prepared statement. + */ + protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency); + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @param resultSetType + * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE} + * @param resultSetHoldability + * One of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} + * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @return a key to uniquely identify a prepared statement. + */ + protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency, resultSetHoldability); + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @param resultSetType + * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE} + * @param resultSetConcurrency + * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetHoldability + * One of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} + * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @param statementType + * The SQL statement type, prepared or callable. + * @return a key to uniquely identify a prepared statement. + * @since 2.4.0 + */ + protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability, + final StatementType statementType) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency, resultSetHoldability, statementType); + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @param resultSetType + * A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @param statementType + * The SQL statement type, prepared or callable. + * @return a key to uniquely identify a prepared statement. + * @since 2.4.0 + */ + protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency, final StatementType statementType) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency, statementType); + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @param columnIndexes + * An array of column indexes indicating the columns that should be returned from the inserted row or + * rows. + * @return a key to uniquely identify a prepared statement. + */ + protected PStmtKey createKey(final String sql, final int[] columnIndexes) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), columnIndexes); + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @param statementType + * The SQL statement type, prepared or callable. + * @return a key to uniquely identify a prepared statement. + */ + protected PStmtKey createKey(final String sql, final StatementType statementType) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), statementType); + } + + /** + * Creates a {@link PStmtKey} for the given arguments. + * + * @param sql + * The SQL statement. + * @param columnNames + * An array of column names indicating the columns that should be returned from the inserted row or rows. + * @return a key to uniquely identify a prepared statement. + */ + protected PStmtKey createKey(final String sql, final String[] columnNames) { + return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), columnNames); + } + + /** + * My {@link KeyedPooledObjectFactory} method for destroying {@link PreparedStatement}s. + * + * @param key + * ignored + * @param pooledObject + * the wrapped {@link PreparedStatement} to be destroyed. + */ + @Override + public void destroyObject(final PStmtKey key, final PooledObject pooledObject) + throws SQLException { + pooledObject.getObject().getInnermostDelegate().close(); + } + + /** + * Closes the physical connection and checks that the logical connection was closed as well. + */ + @Override + protected void finalize() throws Throwable { + // Closing the Connection ensures that if anyone tries to use it, + // an error will occur. + Utils.close(connection, null); + // make sure the last connection is marked as closed + if (logicalConnection != null && !logicalConnection.isClosed()) { + throw new SQLException("PooledConnection was gc'ed, without its last Connection being closed."); + } + } + + private String getCatalogOrNull() { + try { + return connection == null ? null : connection.getCatalog(); + } catch (final SQLException e) { + return null; + } + } + + /** + * Returns a JDBC connection. + * + * @return The database connection. + * @throws SQLException + * if the connection is not open or the previous logical connection is still open + */ + @Override + public Connection getConnection() throws SQLException { + assertOpen(); + // make sure the last connection is marked as closed + if (logicalConnection != null && !logicalConnection.isClosed()) { + // should notify pool of error so the pooled connection can + // be removed !FIXME! + throw new SQLException("PooledConnection was reused, without its previous Connection being closed."); + } + + // the spec requires that this return a new Connection instance. + logicalConnection = new ConnectionImpl(this, connection, isAccessToUnderlyingConnectionAllowed()); + return logicalConnection; + } + + private Connection getRawConnection() throws SQLException { + assertOpen(); + return connection; + } + + private String getSchemaOrNull() { + try { + return connection == null ? null : Jdbc41Bridge.getSchema(connection); + } catch (final SQLException e) { + return null; + } + } + + /** + * Returns the value of the accessToUnderlyingConnectionAllowed property. + * + * @return true if access to the underlying is allowed, false otherwise. + */ + public synchronized boolean isAccessToUnderlyingConnectionAllowed() { + return this.accessToUnderlyingConnectionAllowed; + } + + /** + * My {@link KeyedPooledObjectFactory} method for creating {@link PreparedStatement}s. + * + * @param key + * The key for the {@link PreparedStatement} to be created. + */ + @Override + public PooledObject makeObject(final PStmtKey key) throws SQLException { + if (null == key) { + throw new IllegalArgumentException("Prepared statement key is null or invalid."); + } + if (key.getStmtType() == StatementType.PREPARED_STATEMENT) { + final PreparedStatement statement = (PreparedStatement) key.createStatement(connection); + @SuppressWarnings({"rawtypes", "unchecked" }) // Unable to find way to avoid this + final PoolablePreparedStatement pps = new PoolablePreparedStatement(statement, key, pStmtPool, + delegatingConnection); + return new DefaultPooledObject<>(pps); + } + final CallableStatement statement = (CallableStatement) key.createStatement(connection); + @SuppressWarnings("unchecked") + final PoolableCallableStatement pcs = new PoolableCallableStatement(statement, key, pStmtPool, + (DelegatingConnection) delegatingConnection); + return new DefaultPooledObject<>(pcs); + } + + /** + * Sends a connectionClosed event. + */ + void notifyListeners() { + final ConnectionEvent event = new ConnectionEvent(this); + new ArrayList<>(eventListeners).forEach(listener -> listener.connectionClosed(event)); + } + + /** + * My {@link KeyedPooledObjectFactory} method for passivating {@link PreparedStatement}s. Currently invokes + * {@link PreparedStatement#clearParameters}. + * + * @param key + * ignored + * @param pooledObject + * a wrapped {@link PreparedStatement} + */ + @Override + public void passivateObject(final PStmtKey key, final PooledObject pooledObject) + throws SQLException { + final DelegatingPreparedStatement dps = pooledObject.getObject(); + dps.clearParameters(); + dps.passivate(); + } + + /** + * Creates or obtains a {@link CallableStatement} from my pool. + * + * @param sql + * an SQL statement that may contain one or more '?' parameter placeholders. Typically this statement is + * specified using JDBC call escape syntax. + * @return a default {@code CallableStatement} object containing the pre-compiled SQL statement. + * @throws SQLException + * Thrown if a database access error occurs or this method is called on a closed connection. + * @since 2.4.0 + */ + CallableStatement prepareCall(final String sql) throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareCall(sql); + } + try { + return (CallableStatement) pStmtPool.borrowObject(createKey(sql, StatementType.CALLABLE_STATEMENT)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareCall from pool failed", e); + } + } + + /** + * Creates or obtains a {@link CallableStatement} from my pool. + * + * @param sql + * a {@code String} object that is the SQL statement to be sent to the database; may contain on or + * more '?' parameters. + * @param resultSetType + * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * a concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @return a {@code CallableStatement} object containing the pre-compiled SQL statement that will produce + * {@code ResultSet} objects with the given type and concurrency. + * @throws SQLException + * Thrown if a database access error occurs, this method is called on a closed connection or the given + * parameters are not {@code ResultSet} constants indicating type and concurrency. + * @since 2.4.0 + */ + CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency) + throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareCall(sql, resultSetType, resultSetConcurrency); + } + try { + return (CallableStatement) pStmtPool.borrowObject( + createKey(sql, resultSetType, resultSetConcurrency, StatementType.CALLABLE_STATEMENT)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareCall from pool failed", e); + } + } + + /** + * Creates or obtains a {@link CallableStatement} from my pool. + * + * @param sql + * a {@code String} object that is the SQL statement to be sent to the database; may contain on or + * more '?' parameters. + * @param resultSetType + * one of the following {@code ResultSet} constants: {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * one of the following {@code ResultSet} constants: {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetHoldability + * one of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} + * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @return a new {@code CallableStatement} object, containing the pre-compiled SQL statement, that will + * generate {@code ResultSet} objects with the given type, concurrency, and holdability. + * @throws SQLException + * Thrown if a database access error occurs, this method is called on a closed connection or the given + * parameters are not {@code ResultSet} constants indicating type, concurrency, and holdability. + * @since 2.4.0 + */ + CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + try { + return (CallableStatement) pStmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency, + resultSetHoldability, StatementType.CALLABLE_STATEMENT)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareCall from pool failed", e); + } + } + + /** + * Creates or obtains a {@link PreparedStatement} from my pool. + * + * @param sql the SQL statement. + * @return a {@link PoolablePreparedStatement} + * @throws SQLException Thrown if a database access error occurs, this method is called on a closed connection, or + * the borrow failed. + */ + PreparedStatement prepareStatement(final String sql) throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareStatement(sql); + } + try { + return pStmtPool.borrowObject(createKey(sql)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareStatement from pool failed", e); + } + } + + /** + * Creates or obtains a {@link PreparedStatement} from my pool. + * + * @param sql + * an SQL statement that may contain one or more '?' IN parameter placeholders. + * @param autoGeneratedKeys + * a flag indicating whether auto-generated keys should be returned; one of + * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * @return a {@link PoolablePreparedStatement} + * @throws SQLException Thrown if a database access error occurs, this method is called on a closed connection, or + * the borrow failed. + * @see Connection#prepareStatement(String, int) + */ + PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareStatement(sql, autoGeneratedKeys); + } + try { + return pStmtPool.borrowObject(createKey(sql, autoGeneratedKeys)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareStatement from pool failed", e); + } + } + + /** + * Creates or obtains a {@link PreparedStatement} from my pool. + * + * @param sql + * a {@code String} object that is the SQL statement to be sent to the database; may contain one or + * more '?' IN parameters. + * @param resultSetType + * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, + * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency + * a concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or + * {@code ResultSet.CONCUR_UPDATABLE}. + * + * @return a {@link PoolablePreparedStatement}. + * @throws SQLException Thrown if a database access error occurs, this method is called on a closed connection, or + * the borrow failed. + * @see Connection#prepareStatement(String, int, int) + */ + PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency) + throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareStatement(sql, resultSetType, resultSetConcurrency); + } + try { + return pStmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareStatement from pool failed", e); + } + } + + PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency, + final int resultSetHoldability) throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + try { + return pStmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency, resultSetHoldability)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareStatement from pool failed", e); + } + } + + PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareStatement(sql, columnIndexes); + } + try { + return pStmtPool.borrowObject(createKey(sql, columnIndexes)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareStatement from pool failed", e); + } + } + + PreparedStatement prepareStatement(final String sql, final String[] columnNames) throws SQLException { + if (pStmtPool == null) { + return getRawConnection().prepareStatement(sql, columnNames); + } + try { + return pStmtPool.borrowObject(createKey(sql, columnNames)); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Borrow prepareStatement from pool failed", e); + } + } + + @Override + public void removeConnectionEventListener(final ConnectionEventListener listener) { + eventListeners.remove(listener); + } + + @Override + public void removeStatementEventListener(final StatementEventListener listener) { + statementEventListeners.remove(listener); + } + + /** + * Sets the value of the accessToUnderlyingConnectionAllowed property. It controls if the PoolGuard allows access to + * the underlying connection. (Default: false.) + * + * @param allow + * Access to the underlying connection is granted when true. + */ + public synchronized void setAccessToUnderlyingConnectionAllowed(final boolean allow) { + this.accessToUnderlyingConnectionAllowed = allow; + } + + public void setStatementPool(final KeyedObjectPool statementPool) { + pStmtPool = statementPool; + } + + /** + * @since 2.6.0 + */ + @Override + public synchronized String toString() { + final StringBuilder builder = new StringBuilder(super.toString()); + builder.append("[connection="); + builder.append(connection); + builder.append(", delegatingConnection="); + builder.append(delegatingConnection); + builder.append(", logicalConnection="); + builder.append(logicalConnection); + builder.append(", eventListeners="); + builder.append(eventListeners); + builder.append(", statementEventListeners="); + builder.append(statementEventListeners); + builder.append(", closed="); + builder.append(closed); + builder.append(", pStmtPool="); + builder.append(pStmtPool); + builder.append(", accessToUnderlyingConnectionAllowed="); + builder.append(accessToUnderlyingConnectionAllowed); + builder.append("]"); + return builder.toString(); + } + + /** + * My {@link KeyedPooledObjectFactory} method for validating {@link PreparedStatement}s. + * + * @param key + * Ignored. + * @param pooledObject + * Ignored. + * @return {@code true} + */ + @Override + public boolean validateObject(final PStmtKey key, final PooledObject pooledObject) { + return true; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java new file mode 100644 index 0000000..e5496f3 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +/** + *

    + * This package contains one public class which is a + * {@code ConnectionPoolDataSource} (CPDS) implementation that can be used to + * adapt older {@code Driver} based JDBC implementations. Below is an + * example of setting up the CPDS to be available via JNDI in the + * catalina servlet container. + *

    + *

    In server.xml, the following would be added to the <Context> for your + * webapp: + *

    + * + *
    + *  <Resource name="jdbc/bookstoreCPDS" auth="Container"
    + *             type="org.apache.tomcat.dbcp.dbcp2.cpdsadapter.DriverAdapterCPDS"/>
    + *   <ResourceParams name="jdbc/bookstoreCPDS">
    + *     <parameter>
    + *       <name>factory</name>
    + *       <value>org.apache.tomcat.dbcp.dbcp2.cpdsadapter.DriverAdapterCPDS</value>
    + *     </parameter>
    + *         <parameter><name>user</name><value>root</value></parameter>
    + *         <parameter><name>password</name><value></value></parameter>
    + *         <parameter>
    + *             <name>driver</name>
    + *             <value>org.gjt.mm.mysql.Driver</value></parameter>
    + *         <parameter>
    + *              <name>url</name>
    + *              <value>jdbc:mysql://localhost:3306/bookstore</value>
    + *         </parameter>
    + *   </ResourceParams>
    + * 
    + * + *

    + * In web.xml. Note that elements must be given in the order of the dtd + * described in the servlet specification: + *

    + * + *
    + * <resource-ref>
    + *   <description>
    + *     Resource reference to a factory for java.sql.Connection
    + *     instances that may be used for talking to a particular
    + *     database that is configured in the server.xml file.
    + *   </description>
    + *   <res-ref-name>
    + *     jdbc/bookstoreCPDS
    + *   </res-ref-name>
    + *   <res-type>
    + *     org.apache.tomcat.dbcp.dbcp2.cpdsadapter.DriverAdapterCPDS
    + *   </res-type>
    + *   <res-auth>
    + *     Container
    + *   </res-auth>
    + * </resource-ref>
    + * 
    + * + *

    + * Catalina deploys all objects configured similarly to above within the + * java:comp/env namespace. + *

    + */ +package org.apache.tomcat.dbcp.dbcp2.cpdsadapter; diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java new file mode 100644 index 0000000..3f28aeb --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java @@ -0,0 +1,484 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.sql.ConnectionEvent; +import javax.sql.ConnectionEventListener; +import javax.sql.ConnectionPoolDataSource; +import javax.sql.PooledConnection; + +import org.apache.tomcat.dbcp.dbcp2.Utils; +import org.apache.tomcat.dbcp.pool2.ObjectPool; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.PooledObjectFactory; +import org.apache.tomcat.dbcp.pool2.impl.DefaultPooledObject; + +/** + * A {@link PooledObjectFactory} that creates {@link org.apache.tomcat.dbcp.dbcp2.PoolableConnection PoolableConnection}s. + * + * @since 2.0 + */ +final class CPDSConnectionFactory + implements PooledObjectFactory, ConnectionEventListener, PooledConnectionManager { + + private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but I have no record of the underlying PooledConnection."; + + private final ConnectionPoolDataSource cpds; + private final String validationQuery; + private final Duration validationQueryTimeoutDuration; + private final boolean rollbackAfterValidation; + private ObjectPool pool; + private UserPassKey userPassKey; + private Duration maxConnDuration = Duration.ofMillis(-1); + + /** + * Map of PooledConnections for which close events are ignored. Connections are muted when they are being validated. + */ + private final Set validatingSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** + * Map of PooledConnectionAndInfo instances + */ + private final Map pcMap = new ConcurrentHashMap<>(); + + /** + * Creates a new {@code PoolableConnectionFactory}. + * + * @param cpds + * the ConnectionPoolDataSource from which to obtain PooledConnection's + * @param validationQuery + * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one + * row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate + * connections. + * @param validationQueryTimeoutDuration + * Timeout Duration before validation fails + * @param rollbackAfterValidation + * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. + * @param userName + * The user name to use to create connections + * @param userPassword + * The password to use to create connections + * @since 2.10.0 + */ + CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, + final Duration validationQueryTimeoutDuration, final boolean rollbackAfterValidation, final String userName, + final char[] userPassword) { + this.cpds = cpds; + this.validationQuery = validationQuery; + this.validationQueryTimeoutDuration = validationQueryTimeoutDuration; + this.userPassKey = new UserPassKey(userName, userPassword); + this.rollbackAfterValidation = rollbackAfterValidation; + } + + /** + * Creates a new {@code PoolableConnectionFactory}. + * + * @param cpds + * the ConnectionPoolDataSource from which to obtain PooledConnection's + * @param validationQuery + * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one + * row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate + * connections. + * @param validationQueryTimeoutDuration + * Timeout in seconds before validation fails + * @param rollbackAfterValidation + * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. + * @param userName + * The user name to use to create connections + * @param userPassword + * The password to use to create connections + * @since 2.10.0 + */ + CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, final Duration validationQueryTimeoutDuration, + final boolean rollbackAfterValidation, final String userName, final String userPassword) { + this(cpds, validationQuery, validationQueryTimeoutDuration, rollbackAfterValidation, userName, Utils.toCharArray(userPassword)); + } + + /** + * Creates a new {@code PoolableConnectionFactory}. + * + * @param cpds + * the ConnectionPoolDataSource from which to obtain PooledConnection's + * @param validationQuery + * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one + * row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate + * connections. + * @param validationQueryTimeoutSeconds + * Timeout in seconds before validation fails + * @param rollbackAfterValidation + * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. + * @param userName + * The user name to use to create connections + * @param userPassword + * The password to use to create connections + * @since 2.4.0 + * @deprecated Use {@link #CPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean, String, char[])}. + */ + @Deprecated + CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, + final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation, final String userName, + final char[] userPassword) { + this.cpds = cpds; + this.validationQuery = validationQuery; + this.validationQueryTimeoutDuration = Duration.ofSeconds(validationQueryTimeoutSeconds); + this.userPassKey = new UserPassKey(userName, userPassword); + this.rollbackAfterValidation = rollbackAfterValidation; + } + + /** + * Creates a new {@code PoolableConnectionFactory}. + * + * @param cpds + * the ConnectionPoolDataSource from which to obtain PooledConnection's + * @param validationQuery + * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one + * row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate + * connections. + * @param validationQueryTimeoutSeconds + * Timeout in seconds before validation fails + * @param rollbackAfterValidation + * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. + * @param userName + * The user name to use to create connections + * @param userPassword + * The password to use to create connections + * @deprecated Use {@link #CPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean, String, String)}. + */ + @Deprecated + CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, final int validationQueryTimeoutSeconds, + final boolean rollbackAfterValidation, final String userName, final String userPassword) { + this(cpds, validationQuery, validationQueryTimeoutSeconds, rollbackAfterValidation, userName, Utils.toCharArray(userPassword)); + } + + @Override + public void activateObject(final PooledObject p) throws SQLException { + validateLifetime(p); + } + + /** + * Verifies that the user name matches the user whose connections are being managed by this factory and closes the + * pool if this is the case; otherwise does nothing. + */ + @Override + public void closePool(final String userName) throws SQLException { + synchronized (this) { + if (userName == null || !userName.equals(this.userPassKey.getUserName())) { + return; + } + } + try { + pool.close(); + } catch (final Exception ex) { + throw new SQLException("Error closing connection pool", ex); + } + } + + /** + * This will be called if the Connection returned by the getConnection method came from a PooledConnection, and the + * user calls the close() method of this connection object. What we need to do here is to release this + * PooledConnection from our pool... + */ + @Override + public void connectionClosed(final ConnectionEvent event) { + final PooledConnection pc = (PooledConnection) event.getSource(); + // if this event occurred because we were validating, ignore it + // otherwise return the connection to the pool. + if (!validatingSet.contains(pc)) { + final PooledConnectionAndInfo pci = pcMap.get(pc); + if (pci == null) { + throw new IllegalStateException(NO_KEY_MESSAGE); + } + + try { + pool.returnObject(pci); + } catch (final Exception e) { + System.err.println("CLOSING DOWN CONNECTION AS IT COULD " + "NOT BE RETURNED TO THE POOL"); + pc.removeConnectionEventListener(this); + try { + doDestroyObject(pci); + } catch (final Exception e2) { + System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci); + e2.printStackTrace(); + } + } + } + } + + /** + * If a fatal error occurs, close the underlying physical connection so as not to be returned in the future + */ + @Override + public void connectionErrorOccurred(final ConnectionEvent event) { + final PooledConnection pc = (PooledConnection) event.getSource(); + if (null != event.getSQLException()) { + System.err.println("CLOSING DOWN CONNECTION DUE TO INTERNAL ERROR (" + event.getSQLException() + ")"); + } + pc.removeConnectionEventListener(this); + + final PooledConnectionAndInfo pci = pcMap.get(pc); + if (pci == null) { + throw new IllegalStateException(NO_KEY_MESSAGE); + } + try { + pool.invalidateObject(pci); + } catch (final Exception e) { + System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci); + e.printStackTrace(); + } + } + + /** + * Closes the PooledConnection and stops listening for events from it. + */ + @Override + public void destroyObject(final PooledObject p) throws SQLException { + doDestroyObject(p.getObject()); + } + + private void doDestroyObject(final PooledConnectionAndInfo pci) throws SQLException { + final PooledConnection pc = pci.getPooledConnection(); + pc.removeConnectionEventListener(this); + pcMap.remove(pc); + pc.close(); + } + + /** + * (Testing API) Gets the value of password for the default user. + * + * @return value of password. + */ + char[] getPasswordCharArray() { + return userPassKey.getPasswordCharArray(); + } + + /** + * Returns the object pool used to pool connections created by this factory. + * + * @return ObjectPool managing pooled connections + */ + public ObjectPool getPool() { + return pool; + } + + /** + * Invalidates the PooledConnection in the pool. The CPDSConnectionFactory closes the connection and pool counters + * are updated appropriately. Also closes the pool. This ensures that all idle connections are closed and + * connections that are checked out are closed on return. + */ + @Override + public void invalidate(final PooledConnection pc) throws SQLException { + final PooledConnectionAndInfo pci = pcMap.get(pc); + if (pci == null) { + throw new IllegalStateException(NO_KEY_MESSAGE); + } + try { + pool.invalidateObject(pci); // Destroy instance and update pool counters + pool.close(); // Clear any other instances in this pool and kill others as they come back + } catch (final Exception ex) { + throw new SQLException("Error invalidating connection", ex); + } + } + + @Override + public synchronized PooledObject makeObject() { + try { + PooledConnection pc = null; + if (userPassKey.getUserName() == null) { + pc = cpds.getPooledConnection(); + } else { + pc = cpds.getPooledConnection(userPassKey.getUserName(), userPassKey.getPassword()); + } + if (pc == null) { + throw new IllegalStateException("Connection pool data source returned null from getPooledConnection"); + } + // should we add this object as a listener or the pool. + // consider the validateObject method in decision + pc.addConnectionEventListener(this); + final PooledConnectionAndInfo pci = new PooledConnectionAndInfo(pc, userPassKey); + pcMap.put(pc, pci); + return new DefaultPooledObject<>(pci); + } catch (final SQLException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + public void passivateObject(final PooledObject p) throws SQLException { + validateLifetime(p); + } + + /** + * Sets the maximum Duration of a connection after which the connection will always fail activation, + * passivation and validation. + * + * @param maxConnDuration + * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. + * @since 2.10.0 + */ + public void setMaxConn(final Duration maxConnDuration) { + this.maxConnDuration = maxConnDuration; + } + + /** + * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation, + * passivation and validation. + * + * @param maxConnDuration + * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. + * @since 2.9.0 + * @deprecated Use {@link #setMaxConn(Duration)}. + */ + @Deprecated + public void setMaxConnLifetime(final Duration maxConnDuration) { + this.maxConnDuration = maxConnDuration; + } + + /** + * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation, + * passivation and validation. + * + * @param maxConnLifetimeMillis + * A value of zero or less indicates an infinite lifetime. The default value is -1. + * @deprecated Use {@link #setMaxConn(Duration)}. + */ + @Deprecated + public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) { + setMaxConnLifetime(Duration.ofMillis(maxConnLifetimeMillis)); + } + + /** + * Sets the database password used when creating new connections. + * + * @param userPassword + * new password + */ + public synchronized void setPassword(final char[] userPassword) { + this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword); + } + + /** + * Sets the database password used when creating new connections. + * + * @param userPassword + * new password + */ + @Override + public synchronized void setPassword(final String userPassword) { + this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword); + } + + /** + * @param pool + * the {@link ObjectPool} in which to pool those {@link Connection}s + */ + public void setPool(final ObjectPool pool) { + this.pool = pool; + } + + /** + * @since 2.6.0 + */ + @Override + public synchronized String toString() { + final StringBuilder builder = new StringBuilder(super.toString()); + builder.append("[cpds="); + builder.append(cpds); + builder.append(", validationQuery="); + builder.append(validationQuery); + builder.append(", validationQueryTimeoutDuration="); + builder.append(validationQueryTimeoutDuration); + builder.append(", rollbackAfterValidation="); + builder.append(rollbackAfterValidation); + builder.append(", pool="); + builder.append(pool); + builder.append(", maxConnDuration="); + builder.append(maxConnDuration); + builder.append(", validatingSet="); + builder.append(validatingSet); + builder.append(", pcMap="); + builder.append(pcMap); + builder.append("]"); + return builder.toString(); + } + + private void validateLifetime(final PooledObject p) throws SQLException { + Utils.validateLifetime(p, maxConnDuration); + } + + @Override + public boolean validateObject(final PooledObject p) { + try { + validateLifetime(p); + } catch (final Exception e) { + return false; + } + boolean valid = false; + final PooledConnection pconn = p.getObject().getPooledConnection(); + Connection conn = null; + validatingSet.add(pconn); + if (null == validationQuery) { + Duration timeoutDuration = validationQueryTimeoutDuration; + if (timeoutDuration.isNegative()) { + timeoutDuration = Duration.ZERO; + } + try { + conn = pconn.getConnection(); + valid = conn.isValid((int) timeoutDuration.getSeconds()); + } catch (final SQLException e) { + valid = false; + } finally { + Utils.closeQuietly((AutoCloseable) conn); + validatingSet.remove(pconn); + } + } else { + Statement stmt = null; + ResultSet rset = null; + // logical Connection from the PooledConnection must be closed + // before another one can be requested and closing it will + // generate an event. Keep track so we know not to return + // the PooledConnection + validatingSet.add(pconn); + try { + conn = pconn.getConnection(); + stmt = conn.createStatement(); + rset = stmt.executeQuery(validationQuery); + valid = rset.next(); + if (rollbackAfterValidation) { + conn.rollback(); + } + } catch (final Exception e) { + valid = false; + } finally { + Utils.closeQuietly((AutoCloseable) rset); + Utils.closeQuietly((AutoCloseable) stmt); + Utils.closeQuietly((AutoCloseable) conn); + validatingSet.remove(pconn); + } + } + return valid; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java new file mode 100644 index 0000000..e257bda --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java @@ -0,0 +1,84 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.io.Serializable; +import java.util.Arrays; + +import org.apache.tomcat.dbcp.dbcp2.Utils; + +/** + * A {@code char} array wrapper that does not reveal its contents inadvertently through toString(). In contrast to, for + * example, AtomicReference which toString()'s its contents. + * + * May contain null. + * + * @since 2.9.0 + */ +final class CharArray implements Serializable { + + private static final long serialVersionUID = 1L; + + static final CharArray NULL = new CharArray((char[]) null); + + private final char[] chars; + + CharArray(final char[] chars) { + this.chars = Utils.clone(chars); + } + + CharArray(final String string) { + this.chars = Utils.toCharArray(string); + } + + /** + * Converts the value of char array as a String. + * + * @return value as a string, may be null. + */ + String asString() { + return Utils.toString(chars); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CharArray)) { + return false; + } + final CharArray other = (CharArray) obj; + return Arrays.equals(chars, other.chars); + } + + /** + * Gets the value of char array. + * + * @return value, may be null. + */ + char[] get() { + return Utils.clone(chars); + } + + @Override + public int hashCode() { + return Arrays.hashCode(chars); + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java new file mode 100644 index 0000000..6ea4ed2 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java @@ -0,0 +1,1327 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.time.Duration; +import java.util.Properties; +import java.util.logging.Logger; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.Referenceable; +import javax.sql.ConnectionPoolDataSource; +import javax.sql.DataSource; +import javax.sql.PooledConnection; + +import org.apache.tomcat.dbcp.dbcp2.Utils; +import org.apache.tomcat.dbcp.pool2.impl.BaseObjectPoolConfig; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPoolConfig; +import org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool; + +/** + *

    + * The base class for {@code SharedPoolDataSource} and {@code PerUserPoolDataSource}. Many of the + * configuration properties are shared and defined here. This class is declared public in order to allow particular + * usage with commons-beanutils; do not make direct use of it outside of commons-dbcp2. + *

    + * + *

    + * A J2EE container will normally provide some method of initializing the {@code DataSource} whose attributes are + * presented as bean getters/setters and then deploying it via JNDI. It is then available to an application as a source + * of pooled logical connections to the database. The pool needs a source of physical connections. This source is in the + * form of a {@code ConnectionPoolDataSource} that can be specified via the {@link #setDataSourceName(String)} used + * to lookup the source via JNDI. + *

    + * + *

    + * Although normally used within a JNDI environment, A DataSource can be instantiated and initialized as any bean. In + * this case the {@code ConnectionPoolDataSource} will likely be instantiated in a similar manner. This class + * allows the physical source of connections to be attached directly to this pool using the + * {@link #setConnectionPoolDataSource(ConnectionPoolDataSource)} method. + *

    + * + *

    + * The dbcp package contains an adapter, {@link org.apache.tomcat.dbcp.dbcp2.cpdsadapter.DriverAdapterCPDS}, that can be + * used to allow the use of {@code DataSource}'s based on this class with JDBC driver implementations that do not + * supply a {@code ConnectionPoolDataSource}, but still provide a {@link java.sql.Driver} implementation. + *

    + * + *

    + * The package documentation contains an example using Apache Tomcat and JNDI and it + * also contains a non-JNDI example. + *

    + * + * @since 2.0 + */ +public abstract class InstanceKeyDataSource implements DataSource, Referenceable, Serializable, AutoCloseable { + + private static final long serialVersionUID = -6819270431752240878L; + + private static final String GET_CONNECTION_CALLED = "A Connection was already requested from this source, " + + "further initialization is not allowed."; + private static final String BAD_TRANSACTION_ISOLATION = "The requested TransactionIsolation level is invalid."; + + /** + * Internal constant to indicate the level is not set. + */ + protected static final int UNKNOWN_TRANSACTIONISOLATION = -1; + + /** Guards property setters - once true, setters throw IllegalStateException */ + private volatile boolean getConnectionCalled; + + /** Underlying source of PooledConnections */ + private ConnectionPoolDataSource dataSource; + + /** DataSource Name used to find the ConnectionPoolDataSource */ + private String dataSourceName; + + /** Description */ + private String description; + + /** Environment that may be used to set up a JNDI initial context. */ + private Properties jndiEnvironment; + + /** Login Timeout */ + private Duration loginTimeoutDuration = Duration.ZERO; + + /** Log stream */ + private PrintWriter logWriter; + + /** Instance key */ + private String instanceKey; + + // Pool properties + private boolean defaultBlockWhenExhausted = BaseObjectPoolConfig.DEFAULT_BLOCK_WHEN_EXHAUSTED; + private String defaultEvictionPolicyClassName = BaseObjectPoolConfig.DEFAULT_EVICTION_POLICY_CLASS_NAME; + private boolean defaultLifo = BaseObjectPoolConfig.DEFAULT_LIFO; + private int defaultMaxIdle = GenericKeyedObjectPoolConfig.DEFAULT_MAX_IDLE_PER_KEY; + private int defaultMaxTotal = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL; + private Duration defaultMaxWaitDuration = BaseObjectPoolConfig.DEFAULT_MAX_WAIT; + private Duration defaultMinEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION; + private int defaultMinIdle = GenericKeyedObjectPoolConfig.DEFAULT_MIN_IDLE_PER_KEY; + private int defaultNumTestsPerEvictionRun = BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN; + private Duration defaultSoftMinEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION; + private boolean defaultTestOnCreate = BaseObjectPoolConfig.DEFAULT_TEST_ON_CREATE; + private boolean defaultTestOnBorrow = BaseObjectPoolConfig.DEFAULT_TEST_ON_BORROW; + private boolean defaultTestOnReturn = BaseObjectPoolConfig.DEFAULT_TEST_ON_RETURN; + private boolean defaultTestWhileIdle = BaseObjectPoolConfig.DEFAULT_TEST_WHILE_IDLE; + private Duration defaultDurationBetweenEvictionRuns = BaseObjectPoolConfig.DEFAULT_DURATION_BETWEEN_EVICTION_RUNS; + + // Connection factory properties + private String validationQuery; + private Duration validationQueryTimeoutDuration = Duration.ofSeconds(-1); + private boolean rollbackAfterValidation; + private Duration maxConnDuration = Duration.ofMillis(-1); + + // Connection properties + private Boolean defaultAutoCommit; + private int defaultTransactionIsolation = UNKNOWN_TRANSACTIONISOLATION; + private Boolean defaultReadOnly; + + /** + * Default no-arg constructor for Serialization. + */ + public InstanceKeyDataSource() { + } + + /** + * Throws an IllegalStateException, if a PooledConnection has already been requested. + * + * @throws IllegalStateException Thrown if a PooledConnection has already been requested. + */ + protected void assertInitializationAllowed() throws IllegalStateException { + if (getConnectionCalled) { + throw new IllegalStateException(GET_CONNECTION_CALLED); + } + } + + /** + * Closes the connection pool being maintained by this datasource. + */ + @Override + public abstract void close() throws SQLException; + + private void closeDueToException(final PooledConnectionAndInfo info) { + if (info != null) { + try { + info.getPooledConnection().getConnection().close(); + } catch (final Exception e) { + // do not throw this exception because we are in the middle + // of handling another exception. But record it because + // it potentially leaks connections from the pool. + getLogWriter().println("[ERROR] Could not return connection to pool during exception handling. " + e.getMessage()); + } + } + } + + /** + * Attempts to establish a database connection. + */ + @Override + public Connection getConnection() throws SQLException { + return getConnection(null, null); + } + + /** + * Attempts to retrieve a database connection using {@link #getPooledConnectionAndInfo(String, String)} with the + * provided user name and password. The password on the {@code PooledConnectionAndInfo} instance returned by + * {@code getPooledConnectionAndInfo} is compared to the {@code password} parameter. If the comparison + * fails, a database connection using the supplied user name and password is attempted. If the connection attempt + * fails, an SQLException is thrown, indicating that the given password did not match the password used to create + * the pooled connection. If the connection attempt succeeds, this means that the database password has been + * changed. In this case, the {@code PooledConnectionAndInfo} instance retrieved with the old password is + * destroyed and the {@code getPooledConnectionAndInfo} is repeatedly invoked until a + * {@code PooledConnectionAndInfo} instance with the new password is returned. + */ + @Override + public Connection getConnection(final String userName, final String userPassword) throws SQLException { + if (instanceKey == null) { + throw new SQLException("Must set the ConnectionPoolDataSource " + + "through setDataSourceName or setConnectionPoolDataSource" + " before calling getConnection."); + } + getConnectionCalled = true; + PooledConnectionAndInfo info = null; + try { + info = getPooledConnectionAndInfo(userName, userPassword); + } catch (final RuntimeException | SQLException e) { + closeDueToException(info); + throw e; + } catch (final Exception e) { + closeDueToException(info); + throw new SQLException("Cannot borrow connection from pool", e); + } + + // Password on PooledConnectionAndInfo does not match + if (!(null == userPassword ? null == info.getPassword() : userPassword.equals(info.getPassword()))) { + try { // See if password has changed by attempting connection + testCPDS(userName, userPassword); + } catch (final SQLException ex) { + // Password has not changed, so refuse client, but return connection to the pool + closeDueToException(info); + throw new SQLException( + "Given password did not match password used" + " to create the PooledConnection.", ex); + } catch (final javax.naming.NamingException ne) { + throw new SQLException("NamingException encountered connecting to database", ne); + } + /* + * Password must have changed -> destroy connection and keep retrying until we get a new, good one, + * destroying any idle connections with the old password as we pull them from the pool. + */ + final UserPassKey upkey = info.getUserPassKey(); + final PooledConnectionManager manager = getConnectionManager(upkey); + // Destroy and remove from pool + manager.invalidate(info.getPooledConnection()); + // Reset the password on the factory if using CPDSConnectionFactory + manager.setPassword(upkey.getPassword()); + info = null; + for (int i = 0; i < 10; i++) { // Bound the number of retries - only needed if bad instances return + try { + info = getPooledConnectionAndInfo(userName, userPassword); + } catch (final RuntimeException | SQLException e) { + closeDueToException(info); + throw e; + } catch (final Exception e) { + closeDueToException(info); + throw new SQLException("Cannot borrow connection from pool", e); + } + if (info != null && userPassword != null && userPassword.equals(info.getPassword())) { + break; + } + if (info != null) { + manager.invalidate(info.getPooledConnection()); + } + info = null; + } + if (info == null) { + throw new SQLException("Cannot borrow connection from pool - password change failure."); + } + } + + final Connection connection = info.getPooledConnection().getConnection(); + try { + setupDefaults(connection, userName); + connection.clearWarnings(); + return connection; + } catch (final SQLException ex) { + Utils.close(connection, e -> getLogWriter().println("ignoring exception during close: " + e)); + throw ex; + } + } + + protected abstract PooledConnectionManager getConnectionManager(UserPassKey upkey); + + /** + * Gets the value of connectionPoolDataSource. This method will return null, if the backing data source is being + * accessed via JNDI. + * + * @return value of connectionPoolDataSource. + */ + public ConnectionPoolDataSource getConnectionPoolDataSource() { + return dataSource; + } + + /** + * Gets the name of the ConnectionPoolDataSource which backs this pool. This name is used to look up the data source + * from a JNDI service provider. + * + * @return value of dataSourceName. + */ + public String getDataSourceName() { + return dataSourceName; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getBlockWhenExhausted()} for each per user pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getBlockWhenExhausted()} for each per user + * pool. + */ + public boolean getDefaultBlockWhenExhausted() { + return this.defaultBlockWhenExhausted; + } + + /** + * Gets the default value for {@link GenericObjectPool#getDurationBetweenEvictionRuns()} for each per user pool. + * + * @return The default value for {@link GenericObjectPool#getDurationBetweenEvictionRuns()} for each per user pool. + * @since 2.10.0 + */ + public Duration getDefaultDurationBetweenEvictionRuns() { + return this.defaultDurationBetweenEvictionRuns; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getEvictionPolicyClassName()} for each per user + * pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getEvictionPolicyClassName()} for each per user + * pool. + */ + public String getDefaultEvictionPolicyClassName() { + return this.defaultEvictionPolicyClassName; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getLifo()} for each per user pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getLifo()} for each per user pool. + */ + public boolean getDefaultLifo() { + return this.defaultLifo; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getMaxIdlePerKey()} for each per user pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getMaxIdlePerKey()} for each per user pool. + */ + public int getDefaultMaxIdle() { + return this.defaultMaxIdle; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getMaxTotalPerKey()} for each per user pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getMaxTotalPerKey()} for each per user pool. + */ + public int getDefaultMaxTotal() { + return this.defaultMaxTotal; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getMaxWaitDuration()} for each per user pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getMaxWaitDuration()} for each per user pool. + * @since 2.9.0 + */ + public Duration getDefaultMaxWait() { + return this.defaultMaxWaitDuration; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getMaxWaitDuration()} for each per user pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getMaxWaitDuration()} for each per user pool. + * @deprecated Use {@link #getDefaultMaxWait()}. + */ + @Deprecated + public long getDefaultMaxWaitMillis() { + return getDefaultMaxWait().toMillis(); + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getMinEvictableIdleDuration()} for each per user + * pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getMinEvictableIdleDuration()} for each per + * user pool. + * @since 2.10.0 + */ + public Duration getDefaultMinEvictableIdleDuration() { + return this.defaultMinEvictableIdleDuration; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getMinEvictableIdleDuration()} for each per user + * pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getMinEvictableIdleDuration()} for each per + * user pool. + * @deprecated Use {@link #getDefaultMinEvictableIdleDuration()}. + */ + @Deprecated + public long getDefaultMinEvictableIdleTimeMillis() { + return this.defaultMinEvictableIdleDuration.toMillis(); + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getMinIdlePerKey()} for each per user pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getMinIdlePerKey()} for each per user pool. + */ + public int getDefaultMinIdle() { + return this.defaultMinIdle; + } + + /** + * Gets the default value for {@link GenericKeyedObjectPoolConfig#getNumTestsPerEvictionRun()} for each per user + * pool. + * + * @return The default value for {@link GenericKeyedObjectPoolConfig#getNumTestsPerEvictionRun()} for each per user + * pool. + */ + public int getDefaultNumTestsPerEvictionRun() { + return this.defaultNumTestsPerEvictionRun; + } + + /** + * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * + * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * @since 2.10.0 + */ + public Duration getDefaultSoftMinEvictableIdleDuration() { + return this.defaultSoftMinEvictableIdleDuration; + } + + /** + * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * + * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * @deprecated Use {@link #getDefaultSoftMinEvictableIdleDuration()}. + */ + @Deprecated + public long getDefaultSoftMinEvictableIdleTimeMillis() { + return this.defaultSoftMinEvictableIdleDuration.toMillis(); + } + + /** + * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnBorrow()} for each per user pool. + * + * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnBorrow()} for each per user pool. + */ + public boolean getDefaultTestOnBorrow() { + return this.defaultTestOnBorrow; + } + + /** + * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnCreate()} for each per user pool. + * + * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnCreate()} for each per user pool. + */ + public boolean getDefaultTestOnCreate() { + return this.defaultTestOnCreate; + } + + /** + * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnReturn()} for each per user pool. + * + * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnReturn()} for each per user pool. + */ + public boolean getDefaultTestOnReturn() { + return this.defaultTestOnReturn; + } + + /** + * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestWhileIdle()} for each per user pool. + * + * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestWhileIdle()} for each per user pool. + */ + public boolean getDefaultTestWhileIdle() { + return this.defaultTestWhileIdle; + } + + /** + * Gets the default value for {@link GenericObjectPool#getDurationBetweenEvictionRuns ()} for each per user pool. + * + * @return The default value for {@link GenericObjectPool#getDurationBetweenEvictionRuns ()} for each per user pool. + * @deprecated Use {@link #getDefaultDurationBetweenEvictionRuns()}. + */ + @Deprecated + public long getDefaultTimeBetweenEvictionRunsMillis() { + return this.defaultDurationBetweenEvictionRuns.toMillis(); + } + + /** + * Gets the value of defaultTransactionIsolation, which defines the state of connections handed out from this pool. + * The value can be changed on the Connection using Connection.setTransactionIsolation(int). If this method returns + * -1, the default is JDBC driver dependent. + * + * @return value of defaultTransactionIsolation. + */ + public int getDefaultTransactionIsolation() { + return defaultTransactionIsolation; + } + + /** + * Gets the description. This property is defined by JDBC as for use with GUI (or other) tools that might deploy the + * datasource. It serves no internal purpose. + * + * @return value of description. + */ + public String getDescription() { + return description; + } + + /** + * Gets the instance key. + * + * @return the instance key. + */ + protected String getInstanceKey() { + return instanceKey; + } + + /** + * Gets the value of jndiEnvironment which is used when instantiating a JNDI InitialContext. This InitialContext is + * used to locate the back end ConnectionPoolDataSource. + * + * @param key + * JNDI environment key. + * @return value of jndiEnvironment. + */ + public String getJndiEnvironment(final String key) { + String value = null; + if (jndiEnvironment != null) { + value = jndiEnvironment.getProperty(key); + } + return value; + } + + /** + * Gets the value of loginTimeout. + * + * @return value of loginTimeout. + * @deprecated Use {@link #getLoginTimeoutDuration()}. + */ + @Deprecated + @Override + public int getLoginTimeout() { + return (int) loginTimeoutDuration.getSeconds(); + } + + /** + * Gets the value of loginTimeout. + * + * @return value of loginTimeout. + * @since 2.10.0 + */ + public Duration getLoginTimeoutDuration() { + return loginTimeoutDuration; + } + + /** + * Gets the value of logWriter. + * + * @return value of logWriter. + */ + @Override + public PrintWriter getLogWriter() { + if (logWriter == null) { + logWriter = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); + } + return logWriter; + } + + /** + * Gets the maximum permitted lifetime of a connection. A value of zero or less indicates an + * infinite lifetime. + * + * @return The maximum permitted lifetime of a connection. A value of zero or less indicates an + * infinite lifetime. + * @since 2.10.0 + */ + public Duration getMaxConnDuration() { + return maxConnDuration; + } + + /** + * Gets the maximum permitted lifetime of a connection. A value of zero or less indicates an + * infinite lifetime. + * + * @return The maximum permitted lifetime of a connection. A value of zero or less indicates an + * infinite lifetime. + * @deprecated Use {@link #getMaxConnDuration()}. + */ + @Deprecated + public Duration getMaxConnLifetime() { + return maxConnDuration; + } + + /** + * Gets the maximum permitted lifetime of a connection in milliseconds. A value of zero or less indicates an + * infinite lifetime. + * + * @return The maximum permitted lifetime of a connection in milliseconds. A value of zero or less indicates an + * infinite lifetime. + * @deprecated Use {@link #getMaxConnLifetime()}. + */ + @Deprecated + public long getMaxConnLifetimeMillis() { + return maxConnDuration.toMillis(); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * This method is protected but can only be implemented in this package because PooledConnectionAndInfo is a package + * private type. + * + * @param userName The user name. + * @param userPassword The user password. + * @return Matching PooledConnectionAndInfo. + * @throws SQLException Connection or registration failure. + */ + protected abstract PooledConnectionAndInfo getPooledConnectionAndInfo(String userName, String userPassword) + throws SQLException; + + /** + * Gets the SQL query that will be used to validate connections from this pool before returning them to the caller. + * If specified, this query MUST be an SQL SELECT statement that returns at least one row. If not + * specified, {@link Connection#isValid(int)} will be used to validate connections. + * + * @return The SQL query that will be used to validate connections from this pool before returning them to the + * caller. + */ + public String getValidationQuery() { + return this.validationQuery; + } + + /** + * Returns the timeout in seconds before the validation query fails. + * + * @return The timeout in seconds before the validation query fails. + * @deprecated Use {@link #getValidationQueryTimeoutDuration()}. + */ + @Deprecated + public int getValidationQueryTimeout() { + return (int) validationQueryTimeoutDuration.getSeconds(); + } + + /** + * Returns the timeout Duration before the validation query fails. + * + * @return The timeout Duration before the validation query fails. + */ + public Duration getValidationQueryTimeoutDuration() { + return validationQueryTimeoutDuration; + } + + /** + * Gets the value of defaultAutoCommit, which defines the state of connections handed out from this pool. The value + * can be changed on the Connection using Connection.setAutoCommit(boolean). The default is {@code null} which + * will use the default value for the drive. + * + * @return value of defaultAutoCommit. + */ + public Boolean isDefaultAutoCommit() { + return defaultAutoCommit; + } + + /** + * Gets the value of defaultReadOnly, which defines the state of connections handed out from this pool. The value + * can be changed on the Connection using Connection.setReadOnly(boolean). The default is {@code null} which + * will use the default value for the drive. + * + * @return value of defaultReadOnly. + */ + public Boolean isDefaultReadOnly() { + return defaultReadOnly; + } + + /** + * Whether a rollback will be issued after executing the SQL query that will be used to validate connections from + * this pool before returning them to the caller. + * + * @return true if a rollback will be issued after executing the validation query + */ + public boolean isRollbackAfterValidation() { + return this.rollbackAfterValidation; + } + + @Override + public boolean isWrapperFor(final Class iface) throws SQLException { + return iface.isInstance(this); + } + + /** + * Sets the back end ConnectionPoolDataSource. This property should not be set if using JNDI to access the + * data source. + * + * @param dataSource + * Value to assign to connectionPoolDataSource. + */ + public void setConnectionPoolDataSource(final ConnectionPoolDataSource dataSource) { + assertInitializationAllowed(); + if (dataSourceName != null) { + throw new IllegalStateException("Cannot set the DataSource, if JNDI is used."); + } + if (this.dataSource != null) { + throw new IllegalStateException("The CPDS has already been set. It cannot be altered."); + } + this.dataSource = dataSource; + instanceKey = InstanceKeyDataSourceFactory.registerNewInstance(this); + } + + /** + * Sets the name of the ConnectionPoolDataSource which backs this pool. This name is used to look up the data source + * from a JNDI service provider. + * + * @param dataSourceName + * Value to assign to dataSourceName. + */ + public void setDataSourceName(final String dataSourceName) { + assertInitializationAllowed(); + if (dataSource != null) { + throw new IllegalStateException("Cannot set the JNDI name for the DataSource, if already " + + "set using setConnectionPoolDataSource."); + } + if (this.dataSourceName != null) { + throw new IllegalStateException("The DataSourceName has already been set. " + "It cannot be altered."); + } + this.dataSourceName = dataSourceName; + instanceKey = InstanceKeyDataSourceFactory.registerNewInstance(this); + } + + /** + * Sets the value of defaultAutoCommit, which defines the state of connections handed out from this pool. The value + * can be changed on the Connection using Connection.setAutoCommit(boolean). The default is {@code null} which + * will use the default value for the drive. + * + * @param defaultAutoCommit + * Value to assign to defaultAutoCommit. + */ + public void setDefaultAutoCommit(final Boolean defaultAutoCommit) { + assertInitializationAllowed(); + this.defaultAutoCommit = defaultAutoCommit; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getBlockWhenExhausted()} for each per user pool. + * + * @param blockWhenExhausted + * The default value for {@link GenericKeyedObjectPoolConfig#getBlockWhenExhausted()} for each per user + * pool. + */ + public void setDefaultBlockWhenExhausted(final boolean blockWhenExhausted) { + assertInitializationAllowed(); + this.defaultBlockWhenExhausted = blockWhenExhausted; + } + + /** + * Sets the default value for {@link GenericObjectPool#getDurationBetweenEvictionRuns ()} for each per user pool. + * + * @param defaultDurationBetweenEvictionRuns The default value for + * {@link GenericObjectPool#getDurationBetweenEvictionRuns ()} for each per user pool. + * @since 2.10.0 + */ + public void setDefaultDurationBetweenEvictionRuns(final Duration defaultDurationBetweenEvictionRuns) { + assertInitializationAllowed(); + this.defaultDurationBetweenEvictionRuns = defaultDurationBetweenEvictionRuns; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getEvictionPolicyClassName()} for each per user + * pool. + * + * @param evictionPolicyClassName + * The default value for {@link GenericKeyedObjectPoolConfig#getEvictionPolicyClassName()} for each per + * user pool. + */ + public void setDefaultEvictionPolicyClassName(final String evictionPolicyClassName) { + assertInitializationAllowed(); + this.defaultEvictionPolicyClassName = evictionPolicyClassName; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getLifo()} for each per user pool. + * + * @param lifo + * The default value for {@link GenericKeyedObjectPoolConfig#getLifo()} for each per user pool. + */ + public void setDefaultLifo(final boolean lifo) { + assertInitializationAllowed(); + this.defaultLifo = lifo; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getMaxIdlePerKey()} for each per user pool. + * + * @param maxIdle + * The default value for {@link GenericKeyedObjectPoolConfig#getMaxIdlePerKey()} for each per user pool. + */ + public void setDefaultMaxIdle(final int maxIdle) { + assertInitializationAllowed(); + this.defaultMaxIdle = maxIdle; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getMaxTotalPerKey()} for each per user pool. + * + * @param maxTotal + * The default value for {@link GenericKeyedObjectPoolConfig#getMaxTotalPerKey()} for each per user pool. + */ + public void setDefaultMaxTotal(final int maxTotal) { + assertInitializationAllowed(); + this.defaultMaxTotal = maxTotal; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getMaxWaitDuration()} for each per user pool. + * + * @param maxWaitMillis + * The default value for {@link GenericKeyedObjectPoolConfig#getMaxWaitDuration()} for each per user pool. + * @since 2.9.0 + */ + public void setDefaultMaxWait(final Duration maxWaitMillis) { + assertInitializationAllowed(); + this.defaultMaxWaitDuration = maxWaitMillis; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getMaxWaitMillis()} for each per user pool. + * + * @param maxWaitMillis + * The default value for {@link GenericKeyedObjectPoolConfig#getMaxWaitMillis()} for each per user pool. + * @deprecated Use {@link #setDefaultMaxWait(Duration)}. + */ + @Deprecated + public void setDefaultMaxWaitMillis(final long maxWaitMillis) { + setDefaultMaxWait(Duration.ofMillis(maxWaitMillis)); + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getMinEvictableIdleDuration()} for each per user + * pool. + * + * @param defaultMinEvictableIdleDuration + * The default value for {@link GenericKeyedObjectPoolConfig#getMinEvictableIdleDuration()} for each + * per user pool. + * @since 2.10.0 + */ + public void setDefaultMinEvictableIdle(final Duration defaultMinEvictableIdleDuration) { + assertInitializationAllowed(); + this.defaultMinEvictableIdleDuration = defaultMinEvictableIdleDuration; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getMinEvictableIdleDuration()} for each per user + * pool. + * + * @param minEvictableIdleTimeMillis + * The default value for {@link GenericKeyedObjectPoolConfig#getMinEvictableIdleDuration()} for each + * per user pool. + * @deprecated Use {@link #setDefaultMinEvictableIdle(Duration)}. + */ + @Deprecated + public void setDefaultMinEvictableIdleTimeMillis(final long minEvictableIdleTimeMillis) { + assertInitializationAllowed(); + this.defaultMinEvictableIdleDuration = Duration.ofMillis(minEvictableIdleTimeMillis); + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getMinIdlePerKey()} for each per user pool. + * + * @param minIdle + * The default value for {@link GenericKeyedObjectPoolConfig#getMinIdlePerKey()} for each per user pool. + */ + public void setDefaultMinIdle(final int minIdle) { + assertInitializationAllowed(); + this.defaultMinIdle = minIdle; + } + + /** + * Sets the default value for {@link GenericKeyedObjectPoolConfig#getNumTestsPerEvictionRun()} for each per user + * pool. + * + * @param numTestsPerEvictionRun + * The default value for {@link GenericKeyedObjectPoolConfig#getNumTestsPerEvictionRun()} for each per + * user pool. + */ + public void setDefaultNumTestsPerEvictionRun(final int numTestsPerEvictionRun) { + assertInitializationAllowed(); + this.defaultNumTestsPerEvictionRun = numTestsPerEvictionRun; + } + + /** + * Sets the value of defaultReadOnly, which defines the state of connections handed out from this pool. The value + * can be changed on the Connection using Connection.setReadOnly(boolean). The default is {@code null} which + * will use the default value for the drive. + * + * @param defaultReadOnly + * Value to assign to defaultReadOnly. + */ + public void setDefaultReadOnly(final Boolean defaultReadOnly) { + assertInitializationAllowed(); + this.defaultReadOnly = defaultReadOnly; + } + + /** + * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * + * @param defaultSoftMinEvictableIdleDuration + * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * @since 2.10.0 + */ + public void setDefaultSoftMinEvictableIdle(final Duration defaultSoftMinEvictableIdleDuration) { + assertInitializationAllowed(); + this.defaultSoftMinEvictableIdleDuration = defaultSoftMinEvictableIdleDuration; + } + + /** + * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * + * @param softMinEvictableIdleTimeMillis + * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * @deprecated Use {@link #setDefaultSoftMinEvictableIdle(Duration)}. + */ + @Deprecated + public void setDefaultSoftMinEvictableIdleTimeMillis(final long softMinEvictableIdleTimeMillis) { + assertInitializationAllowed(); + this.defaultSoftMinEvictableIdleDuration = Duration.ofMillis(softMinEvictableIdleTimeMillis); + } + + /** + * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnBorrow()} for each per user pool. + * + * @param testOnBorrow + * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnBorrow()} for each per user pool. + */ + public void setDefaultTestOnBorrow(final boolean testOnBorrow) { + assertInitializationAllowed(); + this.defaultTestOnBorrow = testOnBorrow; + } + + /** + * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnCreate()} for each per user pool. + * + * @param testOnCreate + * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnCreate()} for each per user pool. + */ + public void setDefaultTestOnCreate(final boolean testOnCreate) { + assertInitializationAllowed(); + this.defaultTestOnCreate = testOnCreate; + } + + /** + * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnReturn()} for each per user pool. + * + * @param testOnReturn + * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestOnReturn()} for each per user pool. + */ + public void setDefaultTestOnReturn(final boolean testOnReturn) { + assertInitializationAllowed(); + this.defaultTestOnReturn = testOnReturn; + } + + /** + * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestWhileIdle()} for each per user pool. + * + * @param testWhileIdle + * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * GenericObjectPool#getTestWhileIdle()} for each per user pool. + */ + public void setDefaultTestWhileIdle(final boolean testWhileIdle) { + assertInitializationAllowed(); + this.defaultTestWhileIdle = testWhileIdle; + } + + /** + * Sets the default value for {@link GenericObjectPool#getDurationBetweenEvictionRuns()} for each per user pool. + * + * @param timeBetweenEvictionRunsMillis The default value for + * {@link GenericObjectPool#getDurationBetweenEvictionRuns()} for each per user pool. + * @deprecated Use {@link #setDefaultDurationBetweenEvictionRuns(Duration)}. + */ + @Deprecated + public void setDefaultTimeBetweenEvictionRunsMillis(final long timeBetweenEvictionRunsMillis) { + assertInitializationAllowed(); + this.defaultDurationBetweenEvictionRuns = Duration.ofMillis(timeBetweenEvictionRunsMillis); + } + + /** + * Sets the value of defaultTransactionIsolation, which defines the state of connections handed out from this pool. + * The value can be changed on the Connection using Connection.setTransactionIsolation(int). The default is JDBC + * driver dependent. + * + * @param defaultTransactionIsolation + * Value to assign to defaultTransactionIsolation + */ + public void setDefaultTransactionIsolation(final int defaultTransactionIsolation) { + assertInitializationAllowed(); + switch (defaultTransactionIsolation) { + case Connection.TRANSACTION_NONE: + case Connection.TRANSACTION_READ_COMMITTED: + case Connection.TRANSACTION_READ_UNCOMMITTED: + case Connection.TRANSACTION_REPEATABLE_READ: + case Connection.TRANSACTION_SERIALIZABLE: + break; + default: + throw new IllegalArgumentException(BAD_TRANSACTION_ISOLATION); + } + this.defaultTransactionIsolation = defaultTransactionIsolation; + } + + /** + * Sets the description. This property is defined by JDBC as for use with GUI (or other) tools that might deploy the + * datasource. It serves no internal purpose. + * + * @param description + * Value to assign to description. + */ + public void setDescription(final String description) { + this.description = description; + } + + /** + * Sets the JNDI environment to be used when instantiating a JNDI InitialContext. This InitialContext is used to + * locate the back end ConnectionPoolDataSource. + * + * @param properties + * the JNDI environment property to set which will overwrite any current settings + */ + void setJndiEnvironment(final Properties properties) { + if (jndiEnvironment == null) { + jndiEnvironment = new Properties(); + } else { + jndiEnvironment.clear(); + } + jndiEnvironment.putAll(properties); + } + + /** + * Sets the value of the given JNDI environment property to be used when instantiating a JNDI InitialContext. This + * InitialContext is used to locate the back end ConnectionPoolDataSource. + * + * @param key + * the JNDI environment property to set. + * @param value + * the value assigned to specified JNDI environment property. + */ + public void setJndiEnvironment(final String key, final String value) { + if (jndiEnvironment == null) { + jndiEnvironment = new Properties(); + } + jndiEnvironment.setProperty(key, value); + } + + /** + * Sets the value of loginTimeout. + * + * @param loginTimeout + * Value to assign to loginTimeout. + * @since 2.10.0 + */ + public void setLoginTimeout(final Duration loginTimeout) { + this.loginTimeoutDuration = loginTimeout; + } + + /** + * Sets the value of loginTimeout. + * + * @param loginTimeout + * Value to assign to loginTimeout. + * @deprecated Use {@link #setLoginTimeout(Duration)}. + */ + @Deprecated + @Override + public void setLoginTimeout(final int loginTimeout) { + this.loginTimeoutDuration = Duration.ofSeconds(loginTimeout); + } + + /** + * Sets the value of logWriter. + * + * @param logWriter + * Value to assign to logWriter. + */ + @Override + public void setLogWriter(final PrintWriter logWriter) { + this.logWriter = logWriter; + } + + /** + *

    + * Sets the maximum permitted lifetime of a connection. A value of zero or less indicates an + * infinite lifetime. + *

    + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param maxConnLifetimeMillis + * The maximum permitted lifetime of a connection. A value of zero or less indicates an + * infinite lifetime. + * @since 2.9.0 + */ + public void setMaxConnLifetime(final Duration maxConnLifetimeMillis) { + this.maxConnDuration = maxConnLifetimeMillis; + } + + /** + *

    + * Sets the maximum permitted lifetime of a connection in milliseconds. A value of zero or less indicates an + * infinite lifetime. + *

    + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param maxConnLifetimeMillis + * The maximum permitted lifetime of a connection in milliseconds. A value of zero or less indicates an + * infinite lifetime. + * @deprecated Use {@link #setMaxConnLifetime(Duration)}. + */ + @Deprecated + public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) { + setMaxConnLifetime(Duration.ofMillis(maxConnLifetimeMillis)); + } + + /** + * Whether a rollback will be issued after executing the SQL query that will be used to validate connections from + * this pool before returning them to the caller. Default behavior is NOT to issue a rollback. The setting will only + * have an effect if a validation query is set + * + * @param rollbackAfterValidation + * new property value + */ + public void setRollbackAfterValidation(final boolean rollbackAfterValidation) { + assertInitializationAllowed(); + this.rollbackAfterValidation = rollbackAfterValidation; + } + + protected abstract void setupDefaults(Connection connection, String userName) throws SQLException; + + /** + * Sets the SQL query that will be used to validate connections from this pool before returning them to the caller. + * If specified, this query MUST be an SQL SELECT statement that returns at least one row. If not + * specified, connections will be validated using {@link Connection#isValid(int)}. + * + * @param validationQuery + * The SQL query that will be used to validate connections from this pool before returning them to the + * caller. + */ + public void setValidationQuery(final String validationQuery) { + assertInitializationAllowed(); + this.validationQuery = validationQuery; + } + + /** + * Sets the timeout duration before the validation query fails. + * + * @param validationQueryTimeoutDuration + * The new timeout duration. + */ + public void setValidationQueryTimeout(final Duration validationQueryTimeoutDuration) { + this.validationQueryTimeoutDuration = validationQueryTimeoutDuration; + } + + /** + * Sets the timeout in seconds before the validation query fails. + * + * @param validationQueryTimeoutSeconds + * The new timeout in seconds + * @deprecated Use {@link #setValidationQueryTimeout(Duration)}. + */ + @Deprecated + public void setValidationQueryTimeout(final int validationQueryTimeoutSeconds) { + this.validationQueryTimeoutDuration = Duration.ofSeconds(validationQueryTimeoutSeconds); + } + + protected ConnectionPoolDataSource testCPDS(final String userName, final String userPassword) + throws javax.naming.NamingException, SQLException { + // The source of physical db connections + ConnectionPoolDataSource cpds = this.dataSource; + if (cpds == null) { + Context ctx = null; + if (jndiEnvironment == null) { + ctx = new InitialContext(); + } else { + ctx = new InitialContext(jndiEnvironment); + } + final Object ds = ctx.lookup(dataSourceName); + if (!(ds instanceof ConnectionPoolDataSource)) { + throw new SQLException("Illegal configuration: " + "DataSource " + dataSourceName + " (" + + ds.getClass().getName() + ")" + " doesn't implement javax.sql.ConnectionPoolDataSource"); + } + cpds = (ConnectionPoolDataSource) ds; + } + + // try to get a connection with the supplied userName/password + PooledConnection conn = null; + try { + if (userName != null) { + conn = cpds.getPooledConnection(userName, userPassword); + } else { + conn = cpds.getPooledConnection(); + } + if (conn == null) { + throw new SQLException("Cannot connect using the supplied userName/password"); + } + } finally { + if (conn != null) { + try { + conn.close(); + } catch (final SQLException ignored) { + // at least we could connect + } + } + } + return cpds; + } + + /** + * @since 2.6.0 + */ + @Override + public synchronized String toString() { + final StringBuilder builder = new StringBuilder(super.toString()); + builder.append('['); + toStringFields(builder); + builder.append(']'); + return builder.toString(); + } + + protected void toStringFields(final StringBuilder builder) { + builder.append("getConnectionCalled="); + builder.append(getConnectionCalled); + builder.append(", dataSource="); + builder.append(dataSource); + builder.append(", dataSourceName="); + builder.append(dataSourceName); + builder.append(", description="); + builder.append(description); + builder.append(", jndiEnvironment="); + builder.append(jndiEnvironment); + builder.append(", loginTimeoutDuration="); + builder.append(loginTimeoutDuration); + builder.append(", logWriter="); + builder.append(logWriter); + builder.append(", instanceKey="); + builder.append(instanceKey); + builder.append(", defaultBlockWhenExhausted="); + builder.append(defaultBlockWhenExhausted); + builder.append(", defaultEvictionPolicyClassName="); + builder.append(defaultEvictionPolicyClassName); + builder.append(", defaultLifo="); + builder.append(defaultLifo); + builder.append(", defaultMaxIdle="); + builder.append(defaultMaxIdle); + builder.append(", defaultMaxTotal="); + builder.append(defaultMaxTotal); + builder.append(", defaultMaxWaitDuration="); + builder.append(defaultMaxWaitDuration); + builder.append(", defaultMinEvictableIdleDuration="); + builder.append(defaultMinEvictableIdleDuration); + builder.append(", defaultMinIdle="); + builder.append(defaultMinIdle); + builder.append(", defaultNumTestsPerEvictionRun="); + builder.append(defaultNumTestsPerEvictionRun); + builder.append(", defaultSoftMinEvictableIdleDuration="); + builder.append(defaultSoftMinEvictableIdleDuration); + builder.append(", defaultTestOnCreate="); + builder.append(defaultTestOnCreate); + builder.append(", defaultTestOnBorrow="); + builder.append(defaultTestOnBorrow); + builder.append(", defaultTestOnReturn="); + builder.append(defaultTestOnReturn); + builder.append(", defaultTestWhileIdle="); + builder.append(defaultTestWhileIdle); + builder.append(", defaultDurationBetweenEvictionRuns="); + builder.append(defaultDurationBetweenEvictionRuns); + builder.append(", validationQuery="); + builder.append(validationQuery); + builder.append(", validationQueryTimeoutDuration="); + builder.append(validationQueryTimeoutDuration); + builder.append(", rollbackAfterValidation="); + builder.append(rollbackAfterValidation); + builder.append(", maxConnDuration="); + builder.append(maxConnDuration); + builder.append(", defaultAutoCommit="); + builder.append(defaultAutoCommit); + builder.append(", defaultTransactionIsolation="); + builder.append(defaultTransactionIsolation); + builder.append(", defaultReadOnly="); + builder.append(defaultReadOnly); + } + + @Override + @SuppressWarnings("unchecked") + public T unwrap(final Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return (T) this; + } + throw new SQLException(this + " is not a wrapper for " + iface); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java new file mode 100644 index 0000000..325fad1 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java @@ -0,0 +1,340 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.tomcat.dbcp.dbcp2.ListException; +import org.apache.tomcat.dbcp.dbcp2.Utils; + +/** + * A JNDI ObjectFactory which creates {@code SharedPoolDataSource}s or {@code PerUserPoolDataSource}s + * + * @since 2.0 + */ +abstract class InstanceKeyDataSourceFactory implements ObjectFactory { + + private static final Map INSTANCE_MAP = new ConcurrentHashMap<>(); + + /** + * Closes all pools associated with this class. + * + * @throws ListException + * a {@link ListException} containing all exceptions thrown by {@link InstanceKeyDataSource#close()} + * @see InstanceKeyDataSource#close() + * @since 2.4.0 throws a {@link ListException} instead of, in 2.3.0 and before, the first exception thrown by + * {@link InstanceKeyDataSource#close()}. + */ + public static void closeAll() throws ListException { + // Get iterator to loop over all instances of this data source. + final List exceptionList = new ArrayList<>(INSTANCE_MAP.size()); + INSTANCE_MAP.entrySet().forEach(entry -> { + // Bullet-proof to avoid anything else but problems from InstanceKeyDataSource#close(). + if (entry != null) { + final InstanceKeyDataSource value = entry.getValue(); + Utils.close(value, exceptionList::add); + } + }); + INSTANCE_MAP.clear(); + if (!exceptionList.isEmpty()) { + throw new ListException("Could not close all InstanceKeyDataSource instances.", exceptionList); + } + } + + /** + * Deserializes the provided byte array to create an object. + * + * @param data + * Data to deserialize to create the configuration parameter. + * + * @return The Object created by deserializing the data. + * + * @throws ClassNotFoundException + * If a class cannot be found during the deserialization of a configuration parameter. + * @throws IOException + * If an I/O error occurs during the deserialization of a configuration parameter. + */ + protected static final Object deserialize(final byte[] data) throws IOException, ClassNotFoundException { + ObjectInputStream in = null; + try { + in = new ObjectInputStream(new ByteArrayInputStream(data)); + return in.readObject(); + } finally { + Utils.closeQuietly(in); + } + } + + static synchronized String registerNewInstance(final InstanceKeyDataSource ds) { + int max = 0; + for (final String s : INSTANCE_MAP.keySet()) { + if (s != null) { + try { + max = Math.max(max, Integer.parseInt(s)); + } catch (final NumberFormatException ignored) { + // no sweat, ignore those keys + } + } + } + final String instanceKey = String.valueOf(max + 1); + // Put a placeholder here for now, so other instances will not + // take our key. We will replace with a pool when ready. + INSTANCE_MAP.put(instanceKey, ds); + return instanceKey; + } + + static void removeInstance(final String key) { + if (key != null) { + INSTANCE_MAP.remove(key); + } + } + + /** + * Creates an instance of the subclass and sets any properties contained in the Reference. + * + * @param ref + * The properties to be set on the created DataSource + * + * @return A configured DataSource of the appropriate type. + * + * @throws ClassNotFoundException + * If a class cannot be found during the deserialization of a configuration parameter. + * @throws IOException + * If an I/O error occurs during the deserialization of a configuration parameter. + */ + protected abstract InstanceKeyDataSource getNewInstance(Reference ref) throws IOException, ClassNotFoundException; + + /** + * Implements ObjectFactory to create an instance of SharedPoolDataSource or PerUserPoolDataSource + */ + @Override + public Object getObjectInstance(final Object refObj, final Name name, final Context context, + final Hashtable env) throws IOException, ClassNotFoundException { + // The spec says to return null if we can't create an instance + // of the reference + Object obj = null; + if (refObj instanceof Reference) { + final Reference ref = (Reference) refObj; + if (isCorrectClass(ref.getClassName())) { + final RefAddr refAddr = ref.get("instanceKey"); + if (refAddr != null && refAddr.getContent() != null) { + // object was bound to JNDI via Referenceable API. + obj = INSTANCE_MAP.get(refAddr.getContent()); + } else { + // Tomcat JNDI creates a Reference out of server.xml + // configuration and passes it to an + // instance of the factory given in server.xml. + String key = null; + if (name != null) { + key = name.toString(); + obj = INSTANCE_MAP.get(key); + } + if (obj == null) { + final InstanceKeyDataSource ds = getNewInstance(ref); + setCommonProperties(ref, ds); + obj = ds; + if (key != null) { + INSTANCE_MAP.put(key, ds); + } + } + } + } + } + return obj; + } + + /** + * Tests if className is the value returned from getClass().getName().toString(). + * + * @param className + * The class name to test. + * + * @return true if and only if className is the value returned from getClass().getName().toString() + */ + protected abstract boolean isCorrectClass(String className); + + boolean parseBoolean(final RefAddr refAddr) { + return Boolean.parseBoolean(toString(refAddr)); + } + + int parseInt(final RefAddr refAddr) { + return Integer.parseInt(toString(refAddr)); + } + + long parseLong(final RefAddr refAddr) { + return Long.parseLong(toString(refAddr)); + } + + private void setCommonProperties(final Reference ref, final InstanceKeyDataSource ikds) + throws IOException, ClassNotFoundException { + + RefAddr refAddr = ref.get("dataSourceName"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDataSourceName(toString(refAddr)); + } + + refAddr = ref.get("description"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDescription(toString(refAddr)); + } + + refAddr = ref.get("jndiEnvironment"); + if (refAddr != null && refAddr.getContent() != null) { + final byte[] serialized = (byte[]) refAddr.getContent(); + ikds.setJndiEnvironment((Properties) deserialize(serialized)); + } + + refAddr = ref.get("loginTimeout"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setLoginTimeout(Duration.ofSeconds(parseInt(refAddr))); + } + + // Pool properties + refAddr = ref.get("blockWhenExhausted"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultBlockWhenExhausted(parseBoolean(refAddr)); + } + + refAddr = ref.get("evictionPolicyClassName"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultEvictionPolicyClassName(toString(refAddr)); + } + + // Pool properties + refAddr = ref.get("lifo"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultLifo(parseBoolean(refAddr)); + } + + refAddr = ref.get("maxIdlePerKey"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultMaxIdle(parseInt(refAddr)); + } + + refAddr = ref.get("maxTotalPerKey"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultMaxTotal(parseInt(refAddr)); + } + + refAddr = ref.get("maxWaitMillis"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultMaxWait(Duration.ofMillis(parseLong(refAddr))); + } + + refAddr = ref.get("minEvictableIdleTimeMillis"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultMinEvictableIdle(Duration.ofMillis(parseLong(refAddr))); + } + + refAddr = ref.get("minIdlePerKey"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultMinIdle(parseInt(refAddr)); + } + + refAddr = ref.get("numTestsPerEvictionRun"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultNumTestsPerEvictionRun(parseInt(refAddr)); + } + + refAddr = ref.get("softMinEvictableIdleTimeMillis"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultSoftMinEvictableIdle(Duration.ofMillis(parseLong(refAddr))); + } + + refAddr = ref.get("testOnCreate"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultTestOnCreate(parseBoolean(refAddr)); + } + + refAddr = ref.get("testOnBorrow"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultTestOnBorrow(parseBoolean(refAddr)); + } + + refAddr = ref.get("testOnReturn"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultTestOnReturn(parseBoolean(refAddr)); + } + + refAddr = ref.get("testWhileIdle"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultTestWhileIdle(parseBoolean(refAddr)); + } + + refAddr = ref.get("timeBetweenEvictionRunsMillis"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultDurationBetweenEvictionRuns(Duration.ofMillis(parseLong(refAddr))); + } + + // Connection factory properties + + refAddr = ref.get("validationQuery"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setValidationQuery(toString(refAddr)); + } + + refAddr = ref.get("validationQueryTimeout"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setValidationQueryTimeout(Duration.ofSeconds(parseInt(refAddr))); + } + + refAddr = ref.get("rollbackAfterValidation"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setRollbackAfterValidation(parseBoolean(refAddr)); + } + + refAddr = ref.get("maxConnLifetimeMillis"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setMaxConnLifetime(Duration.ofMillis(parseLong(refAddr))); + } + + // Connection properties + + refAddr = ref.get("defaultAutoCommit"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultAutoCommit(Boolean.valueOf(toString(refAddr))); + } + + refAddr = ref.get("defaultTransactionIsolation"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultTransactionIsolation(parseInt(refAddr)); + } + + refAddr = ref.get("defaultReadOnly"); + if (refAddr != null && refAddr.getContent() != null) { + ikds.setDefaultReadOnly(Boolean.valueOf(toString(refAddr))); + } + } + + String toString(final RefAddr refAddr) { + return refAddr.getContent().toString(); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java new file mode 100644 index 0000000..816fd06 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java @@ -0,0 +1,380 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.sql.ConnectionEvent; +import javax.sql.ConnectionEventListener; +import javax.sql.ConnectionPoolDataSource; +import javax.sql.PooledConnection; + +import org.apache.tomcat.dbcp.dbcp2.Utils; +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.KeyedPooledObjectFactory; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.impl.DefaultPooledObject; + +/** + * A {@link KeyedPooledObjectFactory} that creates {@link org.apache.tomcat.dbcp.dbcp2.PoolableConnection + * PoolableConnection}s. + * + * @since 2.0 + */ +final class KeyedCPDSConnectionFactory implements KeyedPooledObjectFactory, + ConnectionEventListener, PooledConnectionManager { + + private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but " + + "I have no record of the underlying PooledConnection."; + + private final ConnectionPoolDataSource cpds; + private final String validationQuery; + private final Duration validationQueryTimeoutDuration; + private final boolean rollbackAfterValidation; + private KeyedObjectPool pool; + private Duration maxConnLifetime = Duration.ofMillis(-1); + + /** + * Map of PooledConnections for which close events are ignored. Connections are muted when they are being validated. + */ + private final Set validatingSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** + * Map of PooledConnectionAndInfo instances + */ + private final Map pcMap = new ConcurrentHashMap<>(); + + /** + * Creates a new {@code KeyedPoolableConnectionFactory}. + * + * @param cpds + * the ConnectionPoolDataSource from which to obtain PooledConnections + * @param validationQuery + * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one + * row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate + * connections. + * @param validationQueryTimeoutSeconds + * The Duration to allow for the validation query to complete + * @param rollbackAfterValidation + * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. + * @since 2.10.0 + */ + KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, + final Duration validationQueryTimeoutSeconds, final boolean rollbackAfterValidation) { + this.cpds = cpds; + this.validationQuery = validationQuery; + this.validationQueryTimeoutDuration = validationQueryTimeoutSeconds; + this.rollbackAfterValidation = rollbackAfterValidation; + } + + /** + * Creates a new {@code KeyedPoolableConnectionFactory}. + * + * @param cpds + * the ConnectionPoolDataSource from which to obtain PooledConnections + * @param validationQuery + * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one + * row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate + * connections. + * @param validationQueryTimeoutSeconds + * The time, in seconds, to allow for the validation query to complete + * @param rollbackAfterValidation + * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. + * @deprecated Use {@link #KeyedCPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean)}. + */ + @Deprecated + KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, + final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation) { + this(cpds, validationQuery, Duration.ofSeconds(validationQueryTimeoutSeconds), rollbackAfterValidation); + } + + @Override + public void activateObject(final UserPassKey key, final PooledObject p) throws SQLException { + validateLifetime(p); + } + + /** + * This implementation does not fully close the KeyedObjectPool, as this would affect all users. Instead, it clears + * the pool associated with the given user. This method is not currently used. + */ + @Override + public void closePool(final String userName) throws SQLException { + try { + pool.clear(new UserPassKey(userName)); + } catch (final Exception ex) { + throw new SQLException("Error closing connection pool", ex); + } + } + + /** + * This will be called if the Connection returned by the getConnection method came from a PooledConnection, and the + * user calls the close() method of this connection object. What we need to do here is to release this + * PooledConnection from our pool... + */ + @Override + public void connectionClosed(final ConnectionEvent event) { + final PooledConnection pc = (PooledConnection) event.getSource(); + // if this event occurred because we were validating, or if this + // connection has been marked for removal, ignore it + // otherwise return the connection to the pool. + if (!validatingSet.contains(pc)) { + final PooledConnectionAndInfo pci = pcMap.get(pc); + if (pci == null) { + throw new IllegalStateException(NO_KEY_MESSAGE); + } + try { + pool.returnObject(pci.getUserPassKey(), pci); + } catch (final Exception e) { + System.err.println("CLOSING DOWN CONNECTION AS IT COULD " + "NOT BE RETURNED TO THE POOL"); + pc.removeConnectionEventListener(this); + try { + pool.invalidateObject(pci.getUserPassKey(), pci); + } catch (final Exception e3) { + System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci); + e3.printStackTrace(); + } + } + } + } + + /** + * If a fatal error occurs, close the underlying physical connection so as not to be returned in the future + */ + @Override + public void connectionErrorOccurred(final ConnectionEvent event) { + final PooledConnection pc = (PooledConnection) event.getSource(); + if (null != event.getSQLException()) { + System.err.println("CLOSING DOWN CONNECTION DUE TO INTERNAL ERROR (" + event.getSQLException() + ")"); + } + pc.removeConnectionEventListener(this); + + final PooledConnectionAndInfo info = pcMap.get(pc); + if (info == null) { + throw new IllegalStateException(NO_KEY_MESSAGE); + } + try { + pool.invalidateObject(info.getUserPassKey(), info); + } catch (final Exception e) { + System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + info); + e.printStackTrace(); + } + } + + /** + * Closes the PooledConnection and stops listening for events from it. + */ + @Override + public void destroyObject(final UserPassKey key, final PooledObject p) throws SQLException { + final PooledConnection pooledConnection = p.getObject().getPooledConnection(); + pooledConnection.removeConnectionEventListener(this); + pcMap.remove(pooledConnection); + pooledConnection.close(); + } + + /** + * Returns the keyed object pool used to pool connections created by this factory. + * + * @return KeyedObjectPool managing pooled connections + */ + public KeyedObjectPool getPool() { + return pool; + } + + /** + * Invalidates the PooledConnection in the pool. The KeyedCPDSConnectionFactory closes the connection and pool + * counters are updated appropriately. Also clears any idle instances associated with the user name that was used to + * create the PooledConnection. Connections associated with this user are not affected and they will not be + * automatically closed on return to the pool. + */ + @Override + public void invalidate(final PooledConnection pc) throws SQLException { + final PooledConnectionAndInfo info = pcMap.get(pc); + if (info == null) { + throw new IllegalStateException(NO_KEY_MESSAGE); + } + final UserPassKey key = info.getUserPassKey(); + try { + pool.invalidateObject(key, info); // Destroy and update pool counters + pool.clear(key); // Remove any idle instances with this key + } catch (final Exception ex) { + throw new SQLException("Error invalidating connection", ex); + } + } + + /** + * Creates a new {@code PooledConnectionAndInfo} from the given {@code UserPassKey}. + * + * @param userPassKey + * {@code UserPassKey} containing user credentials + * @throws SQLException + * if the connection could not be created. + * @see org.apache.tomcat.dbcp.pool2.KeyedPooledObjectFactory#makeObject(Object) + */ + @Override + public synchronized PooledObject makeObject(final UserPassKey userPassKey) throws SQLException { + PooledConnection pooledConnection = null; + final String userName = userPassKey.getUserName(); + final String password = userPassKey.getPassword(); + if (userName == null) { + pooledConnection = cpds.getPooledConnection(); + } else { + pooledConnection = cpds.getPooledConnection(userName, password); + } + + if (pooledConnection == null) { + throw new IllegalStateException("Connection pool data source returned null from getPooledConnection"); + } + + // should we add this object as a listener or the pool. + // consider the validateObject method in decision + pooledConnection.addConnectionEventListener(this); + final PooledConnectionAndInfo pci = new PooledConnectionAndInfo(pooledConnection, userPassKey); + pcMap.put(pooledConnection, pci); + + return new DefaultPooledObject<>(pci); + } + + @Override + public void passivateObject(final UserPassKey key, final PooledObject p) throws SQLException { + validateLifetime(p); + } + + /** + * Sets the maximum lifetime of a connection after which the connection will always fail activation, + * passivation and validation. + * + * @param maxConnLifetimeMillis + * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. + * @since 2.10.0 + */ + public void setMaxConn(final Duration maxConnLifetimeMillis) { + this.maxConnLifetime = maxConnLifetimeMillis; + } + + /** + * Sets the maximum lifetime of a connection after which the connection will always fail activation, + * passivation and validation. + * + * @param maxConnLifetimeMillis + * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. + * @since 2.9.0 + * @deprecated Use {@link #setMaxConn(Duration)}. + */ + @Deprecated + public void setMaxConnLifetime(final Duration maxConnLifetimeMillis) { + this.maxConnLifetime = maxConnLifetimeMillis; + } + + /** + * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation, + * passivation and validation. + * + * @param maxConnLifetimeMillis + * A value of zero or less indicates an infinite lifetime. The default value is -1. + * @deprecated Use {@link #setMaxConnLifetime(Duration)}. + */ + @Deprecated + public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) { + setMaxConn(Duration.ofMillis(maxConnLifetimeMillis)); + } + + /** + * Does nothing. This factory does not cache user credentials. + */ + @Override + public void setPassword(final String password) { + // Does nothing. This factory does not cache user credentials. + } + + public void setPool(final KeyedObjectPool pool) { + this.pool = pool; + } + + private void validateLifetime(final PooledObject pooledObject) throws SQLException { + Utils.validateLifetime(pooledObject, maxConnLifetime); + } + + /** + * Validates a pooled connection. + * + * @param key + * ignored + * @param pooledObject + * wrapped {@code PooledConnectionAndInfo} containing the connection to validate + * @return true if validation succeeds + */ + @Override + public boolean validateObject(final UserPassKey key, final PooledObject pooledObject) { + try { + validateLifetime(pooledObject); + } catch (final Exception e) { + return false; + } + boolean valid = false; + final PooledConnection pooledConn = pooledObject.getObject().getPooledConnection(); + Connection conn = null; + validatingSet.add(pooledConn); + if (null == validationQuery) { + Duration timeoutDuration = validationQueryTimeoutDuration; + if (timeoutDuration.isNegative()) { + timeoutDuration = Duration.ZERO; + } + try { + conn = pooledConn.getConnection(); + valid = conn.isValid((int) timeoutDuration.getSeconds()); + } catch (final SQLException e) { + valid = false; + } finally { + Utils.closeQuietly((AutoCloseable) conn); + validatingSet.remove(pooledConn); + } + } else { + Statement stmt = null; + ResultSet rset = null; + // logical Connection from the PooledConnection must be closed + // before another one can be requested and closing it will + // generate an event. Keep track so we know not to return + // the PooledConnection + validatingSet.add(pooledConn); + try { + conn = pooledConn.getConnection(); + stmt = conn.createStatement(); + rset = stmt.executeQuery(validationQuery); + valid = rset.next(); + if (rollbackAfterValidation) { + conn.rollback(); + } + } catch (final Exception e) { + valid = false; + } finally { + Utils.closeQuietly((AutoCloseable) rset); + Utils.closeQuietly((AutoCloseable) stmt); + Utils.closeQuietly((AutoCloseable) conn); + validatingSet.remove(pooledConn); + } + } + return valid; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java new file mode 100644 index 0000000..ef005ca --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java @@ -0,0 +1,1197 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.StringRefAddr; +import javax.sql.ConnectionPoolDataSource; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.dbcp.dbcp2.SwallowedExceptionLogger; +import org.apache.tomcat.dbcp.dbcp2.Utils; +import org.apache.tomcat.dbcp.pool2.ObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool; + +/** + *

    + * A pooling {@code DataSource} appropriate for deployment within J2EE environment. There are many configuration + * options, most of which are defined in the parent class. This datasource uses individual pools per user, and some + * properties can be set specifically for a given user, if the deployment environment can support initialization of + * mapped properties. So for example, a pool of admin or write-access Connections can be guaranteed a certain number of + * connections, separate from a maximum set for users with read-only connections. + *

    + * + *

    + * User passwords can be changed without re-initializing the datasource. When a + * {@code getConnection(userName, password)} request is processed with a password that is different from those used + * to create connections in the pool associated with {@code userName}, an attempt is made to create a new + * connection using the supplied password and if this succeeds, the existing pool is cleared and a new pool is created + * for connections using the new password. + *

    + * + * @since 2.0 + */ +public class PerUserPoolDataSource extends InstanceKeyDataSource { + + private static final long serialVersionUID = 7872747993848065028L; + + private static final Log log = LogFactory.getLog(PerUserPoolDataSource.class); + + private static HashMap createMap() { + // Should there be a default size different than what this ctor provides? + return new HashMap<>(); + } + + private Map perUserBlockWhenExhausted; + private Map perUserEvictionPolicyClassName; + private Map perUserLifo; + private Map perUserMaxIdle; + private Map perUserMaxTotal; + private Map perUserMaxWaitDuration; + private Map perUserMinEvictableIdleDuration; + private Map perUserMinIdle; + private Map perUserNumTestsPerEvictionRun; + private Map perUserSoftMinEvictableIdleDuration; + private Map perUserTestOnCreate; + private Map perUserTestOnBorrow; + private Map perUserTestOnReturn; + private Map perUserTestWhileIdle; + private Map perUserDurationBetweenEvictionRuns; + private Map perUserDefaultAutoCommit; + private Map perUserDefaultTransactionIsolation; + private Map perUserDefaultReadOnly; + + /** + * Map to keep track of Pools for a given user. + */ + private transient Map managers = createMap(); + + /** + * Default no-arg constructor for Serialization. + */ + public PerUserPoolDataSource() { + } + + /** + * Clears pool(s) maintained by this data source. + * + * @see org.apache.tomcat.dbcp.pool2.ObjectPool#clear() + * @since 2.3.0 + */ + public void clear() { + managers.values().forEach(manager -> { + try { + getCPDSConnectionFactoryPool(manager).clear(); + } catch (final Exception ignored) { + // ignore and try to close others. + } + }); + InstanceKeyDataSourceFactory.removeInstance(getInstanceKey()); + } + + /** + * Closes pool(s) maintained by this data source. + * + * @see org.apache.tomcat.dbcp.pool2.ObjectPool#close() + */ + @Override + public void close() { + managers.values().forEach(manager -> Utils.closeQuietly(getCPDSConnectionFactoryPool(manager))); + InstanceKeyDataSourceFactory.removeInstance(getInstanceKey()); + } + + /** + * Converts a map with Long milliseconds values to another map with Duration values. + */ + private Map convertMap(final Map currentMap, final Map longMap) { + final Map durationMap = createMap(); + longMap.forEach((k, v) -> durationMap.put(k, toDurationOrNull(v))); + if (currentMap == null) { + return durationMap; + } + currentMap.clear(); + currentMap.putAll(durationMap); + return currentMap; + + } + + @Override + protected PooledConnectionManager getConnectionManager(final UserPassKey upKey) { + return managers.get(getPoolKey(upKey.getUserName())); + } + + /** + * Gets the underlying pool but does NOT allocate it. + * + * @param manager A CPDSConnectionFactory. + * @return the underlying pool. + */ + private ObjectPool getCPDSConnectionFactoryPool(final PooledConnectionManager manager) { + return ((CPDSConnectionFactory) manager).getPool(); + } + + /** + * Gets the number of active connections in the default pool. + * + * @return The number of active connections in the default pool. + */ + public int getNumActive() { + return getNumActive(null); + } + + /** + * Gets the number of active connections in the pool for a given user. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public int getNumActive(final String userName) { + final ObjectPool pool = getPool(getPoolKey(userName)); + return pool == null ? 0 : pool.getNumActive(); + } + + /** + * Gets the number of idle connections in the default pool. + * + * @return The number of idle connections in the default pool. + */ + public int getNumIdle() { + return getNumIdle(null); + } + + /** + * Gets the number of idle connections in the pool for a given user. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public int getNumIdle(final String userName) { + final ObjectPool pool = getPool(getPoolKey(userName)); + return pool == null ? 0 : pool.getNumIdle(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getBlockWhenExhausted()} for the specified user's pool + * or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public boolean getPerUserBlockWhenExhausted(final String userName) { + Boolean value = null; + if (perUserBlockWhenExhausted != null) { + value = perUserBlockWhenExhausted.get(userName); + } + if (value == null) { + return getDefaultBlockWhenExhausted(); + } + return value.booleanValue(); + } + + /** + * Gets the user specific default value for {@link Connection#setAutoCommit(boolean)} for the specified user's pool. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public Boolean getPerUserDefaultAutoCommit(final String userName) { + Boolean value = null; + if (perUserDefaultAutoCommit != null) { + value = perUserDefaultAutoCommit.get(userName); + } + return value; + } + + /** + * Gets the user specific default value for {@link Connection#setReadOnly(boolean)} for the specified user's pool. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public Boolean getPerUserDefaultReadOnly(final String userName) { + Boolean value = null; + if (perUserDefaultReadOnly != null) { + value = perUserDefaultReadOnly.get(userName); + } + return value; + } + + /** + * Gets the user specific default value for {@link Connection#setTransactionIsolation(int)} for the specified user's + * pool. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public Integer getPerUserDefaultTransactionIsolation(final String userName) { + Integer value = null; + if (perUserDefaultTransactionIsolation != null) { + value = perUserDefaultTransactionIsolation.get(userName); + } + return value; + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getDurationBetweenEvictionRuns()} for the specified + * user's pool or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + * @since 2.10.0 + */ + public Duration getPerUserDurationBetweenEvictionRuns(final String userName) { + Duration value = null; + if (perUserDurationBetweenEvictionRuns != null) { + value = perUserDurationBetweenEvictionRuns.get(userName); + } + if (value == null) { + return getDefaultDurationBetweenEvictionRuns(); + } + return value; + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getEvictionPolicyClassName()} for the specified user's + * pool or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public String getPerUserEvictionPolicyClassName(final String userName) { + String value = null; + if (perUserEvictionPolicyClassName != null) { + value = perUserEvictionPolicyClassName.get(userName); + } + if (value == null) { + return getDefaultEvictionPolicyClassName(); + } + return value; + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getLifo()} for the specified user's pool or the default + * if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public boolean getPerUserLifo(final String userName) { + Boolean value = null; + if (perUserLifo != null) { + value = perUserLifo.get(userName); + } + if (value == null) { + return getDefaultLifo(); + } + return value.booleanValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getMaxIdle()} for the specified user's pool or the + * default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public int getPerUserMaxIdle(final String userName) { + Integer value = null; + if (perUserMaxIdle != null) { + value = perUserMaxIdle.get(userName); + } + if (value == null) { + return getDefaultMaxIdle(); + } + return value.intValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getMaxTotal()} for the specified user's pool or the + * default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public int getPerUserMaxTotal(final String userName) { + Integer value = null; + if (perUserMaxTotal != null) { + value = perUserMaxTotal.get(userName); + } + if (value == null) { + return getDefaultMaxTotal(); + } + return value.intValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getMaxWaitDuration()} for the specified user's pool or + * the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + * @since 2.10.0 + */ + public Duration getPerUserMaxWaitDuration(final String userName) { + Duration value = null; + if (perUserMaxWaitDuration != null) { + value = perUserMaxWaitDuration.get(userName); + } + if (value == null) { + return getDefaultMaxWait(); + } + return value; + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getMaxWaitDuration()} for the specified user's pool or + * the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + * @deprecated Use {@link #getPerUserMaxWaitDuration}. + */ + @Deprecated + public long getPerUserMaxWaitMillis(final String userName) { + return getPerUserMaxWaitDuration(userName).toMillis(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getMinEvictableIdleDuration()} for the specified + * user's pool or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value, never null. + * @since 2.10.0 + */ + public Duration getPerUserMinEvictableIdleDuration(final String userName) { + Duration value = null; + if (perUserMinEvictableIdleDuration != null) { + value = perUserMinEvictableIdleDuration.get(userName); + } + if (value == null) { + return getDefaultMinEvictableIdleDuration(); + } + return value; + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getMinEvictableIdleDuration()} for the specified + * user's pool or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + * @deprecated Use {@link #getPerUserMinEvictableIdleDuration(String)}. + */ + @Deprecated + public long getPerUserMinEvictableIdleTimeMillis(final String userName) { + return getPerUserMinEvictableIdleDuration(userName).toMillis(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getMinIdle()} for the specified user's pool or the + * default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public int getPerUserMinIdle(final String userName) { + Integer value = null; + if (perUserMinIdle != null) { + value = perUserMinIdle.get(userName); + } + if (value == null) { + return getDefaultMinIdle(); + } + return value.intValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getNumTestsPerEvictionRun()} for the specified user's + * pool or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public int getPerUserNumTestsPerEvictionRun(final String userName) { + Integer value = null; + if (perUserNumTestsPerEvictionRun != null) { + value = perUserNumTestsPerEvictionRun.get(userName); + } + if (value == null) { + return getDefaultNumTestsPerEvictionRun(); + } + return value.intValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getSoftMinEvictableIdleDuration()} for the specified + * user's pool or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + * @since 2.10.0 + */ + public Duration getPerUserSoftMinEvictableIdleDuration(final String userName) { + Duration value = null; + if (perUserSoftMinEvictableIdleDuration != null) { + value = perUserSoftMinEvictableIdleDuration.get(userName); + } + if (value == null) { + return getDefaultSoftMinEvictableIdleDuration(); + } + return value; + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getSoftMinEvictableIdleDuration()} for the specified + * user's pool or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + * @deprecated Use {@link #getPerUserSoftMinEvictableIdleDuration(String)}. + */ + @Deprecated + public long getPerUserSoftMinEvictableIdleTimeMillis(final String userName) { + return getPerUserSoftMinEvictableIdleDuration(userName).toMillis(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getTestOnBorrow()} for the specified user's pool or the + * default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public boolean getPerUserTestOnBorrow(final String userName) { + Boolean value = null; + if (perUserTestOnBorrow != null) { + value = perUserTestOnBorrow.get(userName); + } + if (value == null) { + return getDefaultTestOnBorrow(); + } + return value.booleanValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getTestOnCreate()} for the specified user's pool or the + * default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public boolean getPerUserTestOnCreate(final String userName) { + Boolean value = null; + if (perUserTestOnCreate != null) { + value = perUserTestOnCreate.get(userName); + } + if (value == null) { + return getDefaultTestOnCreate(); + } + return value.booleanValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getTestOnReturn()} for the specified user's pool or the + * default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public boolean getPerUserTestOnReturn(final String userName) { + Boolean value = null; + if (perUserTestOnReturn != null) { + value = perUserTestOnReturn.get(userName); + } + if (value == null) { + return getDefaultTestOnReturn(); + } + return value.booleanValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getTestWhileIdle()} for the specified user's pool or + * the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + */ + public boolean getPerUserTestWhileIdle(final String userName) { + Boolean value = null; + if (perUserTestWhileIdle != null) { + value = perUserTestWhileIdle.get(userName); + } + if (value == null) { + return getDefaultTestWhileIdle(); + } + return value.booleanValue(); + } + + /** + * Gets the user specific value for {@link GenericObjectPool#getDurationBetweenEvictionRuns()} for the specified + * user's pool or the default if no user specific value is defined. + * + * @param userName + * The user name key. + * @return The user specific value. + * @deprecated Use {@link #getPerUserDurationBetweenEvictionRuns(String)}. + */ + @Deprecated + public long getPerUserTimeBetweenEvictionRunsMillis(final String userName) { + return getPerUserDurationBetweenEvictionRuns(userName).toMillis(); + } + + /** + * Returns the object pool associated with the given PoolKey. + * + * @param poolKey + * PoolKey identifying the pool + * @return the GenericObjectPool pooling connections for the userName and datasource specified by the PoolKey + */ + private ObjectPool getPool(final PoolKey poolKey) { + final CPDSConnectionFactory mgr = (CPDSConnectionFactory) managers.get(poolKey); + return mgr == null ? null : mgr.getPool(); + } + + @Override + protected PooledConnectionAndInfo getPooledConnectionAndInfo(final String userName, final String password) + throws SQLException { + + final PoolKey key = getPoolKey(userName); + ObjectPool pool; + PooledConnectionManager manager; + synchronized (this) { + manager = managers.get(key); + if (manager == null) { + try { + registerPool(userName, password); + manager = managers.get(key); + } catch (final NamingException e) { + throw new SQLException("RegisterPool failed", e); + } + } + pool = getCPDSConnectionFactoryPool(manager); + } + + PooledConnectionAndInfo info = null; + try { + info = pool.borrowObject(); + } catch (final NoSuchElementException ex) { + throw new SQLException("Could not retrieve connection info from pool", ex); + } catch (final Exception e) { + // See if failure is due to CPDSConnectionFactory authentication failure + try { + testCPDS(userName, password); + } catch (final Exception ex) { + throw new SQLException("Could not retrieve connection info from pool", ex); + } + // New password works, so kill the old pool, create a new one, and borrow + manager.closePool(userName); + synchronized (this) { + managers.remove(key); + } + try { + registerPool(userName, password); + pool = getPool(key); + } catch (final NamingException ne) { + throw new SQLException("RegisterPool failed", ne); + } + try { + info = pool.borrowObject(); + } catch (final Exception ex) { + throw new SQLException("Could not retrieve connection info from pool", ex); + } + } + return info; + } + + /** + * Creates a pool key from the provided parameters. + * + * @param userName + * User name + * @return The pool key + */ + private PoolKey getPoolKey(final String userName) { + return new PoolKey(getDataSourceName(), userName); + } + + /** + * Returns a {@code PerUserPoolDataSource} {@link Reference}. + */ + @Override + public Reference getReference() throws NamingException { + final Reference ref = new Reference(getClass().getName(), PerUserPoolDataSourceFactory.class.getName(), null); + ref.add(new StringRefAddr("instanceKey", getInstanceKey())); + return ref; + } + + Map put(Map map, final K key, final V value) { + if (map == null) { + map = createMap(); + } + map.put(key, value); + return map; + } + + /** + * Supports Serialization interface. + * + * @param in + * a {@code java.io.ObjectInputStream} value + * @throws IOException + * if an error occurs + * @throws ClassNotFoundException + * if an error occurs + */ + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + in.defaultReadObject(); + final PerUserPoolDataSource oldDS = (PerUserPoolDataSource) new PerUserPoolDataSourceFactory() + .getObjectInstance(getReference(), null, null, null); + this.managers = oldDS.managers; + } catch (final NamingException e) { + throw new IOException("NamingException: " + e); + } + } + + private synchronized void registerPool(final String userName, final String password) + throws NamingException, SQLException { + + final ConnectionPoolDataSource cpds = testCPDS(userName, password); + + // Set up the factory we will use (passing the pool associates + // the factory with the pool, so we do not have to do so + // explicitly) + final CPDSConnectionFactory factory = new CPDSConnectionFactory(cpds, getValidationQuery(), getValidationQueryTimeoutDuration(), + isRollbackAfterValidation(), userName, password); + factory.setMaxConn(getMaxConnDuration()); + + // Create an object pool to contain our PooledConnections + final GenericObjectPool pool = new GenericObjectPool<>(factory); + factory.setPool(pool); + pool.setBlockWhenExhausted(getPerUserBlockWhenExhausted(userName)); + pool.setEvictionPolicyClassName(getPerUserEvictionPolicyClassName(userName)); + pool.setLifo(getPerUserLifo(userName)); + pool.setMaxIdle(getPerUserMaxIdle(userName)); + pool.setMaxTotal(getPerUserMaxTotal(userName)); + pool.setMaxWait(Duration.ofMillis(getPerUserMaxWaitMillis(userName))); + pool.setMinEvictableIdleDuration(getPerUserMinEvictableIdleDuration(userName)); + pool.setMinIdle(getPerUserMinIdle(userName)); + pool.setNumTestsPerEvictionRun(getPerUserNumTestsPerEvictionRun(userName)); + pool.setSoftMinEvictableIdleDuration(getPerUserSoftMinEvictableIdleDuration(userName)); + pool.setTestOnCreate(getPerUserTestOnCreate(userName)); + pool.setTestOnBorrow(getPerUserTestOnBorrow(userName)); + pool.setTestOnReturn(getPerUserTestOnReturn(userName)); + pool.setTestWhileIdle(getPerUserTestWhileIdle(userName)); + pool.setDurationBetweenEvictionRuns(getPerUserDurationBetweenEvictionRuns(userName)); + + pool.setSwallowedExceptionListener(new SwallowedExceptionLogger(log)); + + final PooledConnectionManager old = managers.put(getPoolKey(userName), factory); + if (old != null) { + throw new IllegalStateException("Pool already contains an entry for this user/password: " + userName); + } + } + + private Map replaceAll(final Map currentMap, final Map newMap) { + if (currentMap == null) { + return new HashMap<>(newMap); + } + currentMap.clear(); + currentMap.putAll(newMap); + return currentMap; + } + + void setPerUserBlockWhenExhausted(final Map newMap) { + assertInitializationAllowed(); + perUserBlockWhenExhausted = replaceAll(perUserBlockWhenExhausted, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getBlockWhenExhausted()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserBlockWhenExhausted(final String userName, final Boolean value) { + assertInitializationAllowed(); + perUserBlockWhenExhausted = put(perUserBlockWhenExhausted, userName, value); + } + + void setPerUserDefaultAutoCommit(final Map newMap) { + assertInitializationAllowed(); + perUserDefaultAutoCommit = replaceAll(perUserDefaultAutoCommit, newMap); + } + + /** + * Sets a user specific default value for {@link Connection#setAutoCommit(boolean)} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserDefaultAutoCommit(final String userName, final Boolean value) { + assertInitializationAllowed(); + perUserDefaultAutoCommit = put(perUserDefaultAutoCommit, userName, value); + + } + + void setPerUserDefaultReadOnly(final Map newMap) { + assertInitializationAllowed(); + perUserDefaultReadOnly = replaceAll(perUserDefaultReadOnly, newMap); + } + + /** + * Sets a user specific default value for {@link Connection#setReadOnly(boolean)} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserDefaultReadOnly(final String userName, final Boolean value) { + assertInitializationAllowed(); + perUserDefaultReadOnly = put(perUserDefaultReadOnly, userName, value); + + } + + void setPerUserDefaultTransactionIsolation(final Map newMap) { + assertInitializationAllowed(); + perUserDefaultTransactionIsolation = replaceAll(perUserDefaultTransactionIsolation, newMap); + } + + /** + * Sets a user specific default value for {@link Connection#setTransactionIsolation(int)} for the specified user's + * pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserDefaultTransactionIsolation(final String userName, final Integer value) { + assertInitializationAllowed(); + perUserDefaultTransactionIsolation = put(perUserDefaultTransactionIsolation, userName, value); + + } + + void setPerUserDurationBetweenEvictionRuns(final Map newMap) { + assertInitializationAllowed(); + perUserDurationBetweenEvictionRuns = replaceAll(perUserDurationBetweenEvictionRuns, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getDurationBetweenEvictionRuns()} for the specified + * user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + * @since 2.10.0 + */ + public void setPerUserDurationBetweenEvictionRuns(final String userName, final Duration value) { + assertInitializationAllowed(); + perUserDurationBetweenEvictionRuns = put(perUserDurationBetweenEvictionRuns, userName, value); + + } + + void setPerUserEvictionPolicyClassName(final Map newMap) { + assertInitializationAllowed(); + perUserEvictionPolicyClassName = replaceAll(perUserEvictionPolicyClassName, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getEvictionPolicyClassName()} for the specified user's + * pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserEvictionPolicyClassName(final String userName, final String value) { + assertInitializationAllowed(); + perUserEvictionPolicyClassName = put(perUserEvictionPolicyClassName, userName, value); + } + + void setPerUserLifo(final Map newMap) { + assertInitializationAllowed(); + perUserLifo = replaceAll(perUserLifo, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getLifo()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserLifo(final String userName, final Boolean value) { + assertInitializationAllowed(); + perUserLifo = put(perUserLifo, userName, value); + } + + void setPerUserMaxIdle(final Map newMap) { + assertInitializationAllowed(); + perUserMaxIdle = replaceAll(perUserMaxIdle, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getMaxIdle()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserMaxIdle(final String userName, final Integer value) { + assertInitializationAllowed(); + perUserMaxIdle = put(perUserMaxIdle, userName, value); + } + + void setPerUserMaxTotal(final Map newMap) { + assertInitializationAllowed(); + perUserMaxTotal = replaceAll(perUserMaxTotal, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getMaxTotal()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserMaxTotal(final String userName, final Integer value) { + assertInitializationAllowed(); + perUserMaxTotal = put(perUserMaxTotal, userName, value); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getMaxWaitDuration()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + * @since 2.10.0 + */ + public void setPerUserMaxWait(final String userName, final Duration value) { + assertInitializationAllowed(); + perUserMaxWaitDuration = put(perUserMaxWaitDuration, userName, value); + } + + void setPerUserMaxWaitDuration(final Map newMap) { + assertInitializationAllowed(); + perUserMaxWaitDuration = replaceAll(perUserMaxWaitDuration, newMap); + } + + void setPerUserMaxWaitMillis(final Map newMap) { + assertInitializationAllowed(); + perUserMaxWaitDuration = convertMap(perUserMaxWaitDuration, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getMaxWaitDuration()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + * @deprecated Use {@link #setPerUserMaxWait(String, Duration)}. + */ + @Deprecated + public void setPerUserMaxWaitMillis(final String userName, final Long value) { + setPerUserMaxWait(userName, toDurationOrNull(value)); + } + + void setPerUserMinEvictableIdle(final Map newMap) { + assertInitializationAllowed(); + perUserMinEvictableIdleDuration = replaceAll(perUserMinEvictableIdleDuration, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getMinEvictableIdleDuration()} for the specified user's + * pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + * @since 2.10.0 + */ + public void setPerUserMinEvictableIdle(final String userName, final Duration value) { + assertInitializationAllowed(); + perUserMinEvictableIdleDuration = put(perUserMinEvictableIdleDuration, userName, value); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getMinEvictableIdleDuration()} for the specified user's + * pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + * @deprecated Use {@link #setPerUserMinEvictableIdle(String, Duration)}. + */ + @Deprecated + public void setPerUserMinEvictableIdleTimeMillis(final String userName, final Long value) { + setPerUserMinEvictableIdle(userName, toDurationOrNull(value)); + } + + void setPerUserMinIdle(final Map newMap) { + assertInitializationAllowed(); + perUserMinIdle = replaceAll(perUserMinIdle, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getMinIdle()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserMinIdle(final String userName, final Integer value) { + assertInitializationAllowed(); + perUserMinIdle = put(perUserMinIdle, userName, value); + } + + void setPerUserNumTestsPerEvictionRun(final Map newMap) { + assertInitializationAllowed(); + perUserNumTestsPerEvictionRun = replaceAll(perUserNumTestsPerEvictionRun, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getNumTestsPerEvictionRun()} for the specified user's + * pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserNumTestsPerEvictionRun(final String userName, final Integer value) { + assertInitializationAllowed(); + perUserNumTestsPerEvictionRun = put(perUserNumTestsPerEvictionRun, userName, value); + } + + void setPerUserSoftMinEvictableIdle(final Map newMap) { + assertInitializationAllowed(); + perUserSoftMinEvictableIdleDuration = replaceAll(perUserSoftMinEvictableIdleDuration, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getSoftMinEvictableIdleDuration()} for the specified + * user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + * @since 2.10.0 + */ + public void setPerUserSoftMinEvictableIdle(final String userName, final Duration value) { + assertInitializationAllowed(); + perUserSoftMinEvictableIdleDuration = put(perUserSoftMinEvictableIdleDuration, userName, value); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getSoftMinEvictableIdleDuration()} for the specified + * user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + * @deprecated Use {@link #setPerUserSoftMinEvictableIdle(String, Duration)}. + */ + @Deprecated + public void setPerUserSoftMinEvictableIdleTimeMillis(final String userName, final Long value) { + setPerUserSoftMinEvictableIdle(userName, toDurationOrNull(value)); + } + + void setPerUserTestOnBorrow(final Map newMap) { + assertInitializationAllowed(); + perUserTestOnBorrow = replaceAll(perUserTestOnBorrow, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getTestOnBorrow()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserTestOnBorrow(final String userName, final Boolean value) { + assertInitializationAllowed(); + perUserTestOnBorrow = put(perUserTestOnBorrow, userName, value); + } + + void setPerUserTestOnCreate(final Map newMap) { + assertInitializationAllowed(); + perUserTestOnCreate = replaceAll(perUserTestOnCreate, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getTestOnCreate()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserTestOnCreate(final String userName, final Boolean value) { + assertInitializationAllowed(); + perUserTestOnCreate = put(perUserTestOnCreate, userName, value); + } + + void setPerUserTestOnReturn(final Map newMap) { + assertInitializationAllowed(); + perUserTestOnReturn = replaceAll(perUserTestOnReturn, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getTestOnReturn()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserTestOnReturn(final String userName, final Boolean value) { + assertInitializationAllowed(); + perUserTestOnReturn = put(perUserTestOnReturn, userName, value); + } + + void setPerUserTestWhileIdle(final Map newMap) { + assertInitializationAllowed(); + perUserTestWhileIdle = replaceAll(perUserTestWhileIdle, newMap); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getTestWhileIdle()} for the specified user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + */ + public void setPerUserTestWhileIdle(final String userName, final Boolean value) { + assertInitializationAllowed(); + perUserTestWhileIdle = put(perUserTestWhileIdle, userName, value); + } + + /** + * Sets a user specific value for {@link GenericObjectPool#getDurationBetweenEvictionRuns()} for the specified + * user's pool. + * + * @param userName + * The user name key. + * @param value + * The user specific value. + * @deprecated Use {@link #setPerUserDurationBetweenEvictionRuns(String, Duration)}. + */ + @Deprecated + public void setPerUserTimeBetweenEvictionRunsMillis(final String userName, final Long value) { + setPerUserDurationBetweenEvictionRuns(userName, toDurationOrNull(value)); + } + + @Override + protected void setupDefaults(final Connection con, final String userName) throws SQLException { + Boolean defaultAutoCommit = isDefaultAutoCommit(); + if (userName != null) { + final Boolean userMax = getPerUserDefaultAutoCommit(userName); + if (userMax != null) { + defaultAutoCommit = userMax; + } + } + + Boolean defaultReadOnly = isDefaultReadOnly(); + if (userName != null) { + final Boolean userMax = getPerUserDefaultReadOnly(userName); + if (userMax != null) { + defaultReadOnly = userMax; + } + } + + int defaultTransactionIsolation = getDefaultTransactionIsolation(); + if (userName != null) { + final Integer userMax = getPerUserDefaultTransactionIsolation(userName); + if (userMax != null) { + defaultTransactionIsolation = userMax.intValue(); + } + } + + if (defaultAutoCommit != null && con.getAutoCommit() != defaultAutoCommit.booleanValue()) { + con.setAutoCommit(defaultAutoCommit.booleanValue()); + } + + if (defaultTransactionIsolation != UNKNOWN_TRANSACTIONISOLATION) { + con.setTransactionIsolation(defaultTransactionIsolation); + } + + if (defaultReadOnly != null && con.isReadOnly() != defaultReadOnly.booleanValue()) { + con.setReadOnly(defaultReadOnly.booleanValue()); + } + } + + private Duration toDurationOrNull(final Long millis) { + return millis == null ? null : Duration.ofMillis(millis.longValue()); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java new file mode 100644 index 0000000..df53faa --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java @@ -0,0 +1,95 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; + +import javax.naming.RefAddr; +import javax.naming.Reference; + +/** + * A JNDI ObjectFactory which creates {@code SharedPoolDataSource}s + * + * @since 2.0 + */ +public class PerUserPoolDataSourceFactory extends InstanceKeyDataSourceFactory { + private static final String PER_USER_POOL_CLASSNAME = PerUserPoolDataSource.class.getName(); + + @SuppressWarnings("unchecked") // Avoid warnings on deserialization + @Override + protected InstanceKeyDataSource getNewInstance(final Reference ref) throws IOException, ClassNotFoundException { + final PerUserPoolDataSource pupds = new PerUserPoolDataSource(); + RefAddr refAddr = ref.get("defaultMaxTotal"); + if (refAddr != null && refAddr.getContent() != null) { + pupds.setDefaultMaxTotal(parseInt(refAddr)); + } + + refAddr = ref.get("defaultMaxIdle"); + if (refAddr != null && refAddr.getContent() != null) { + pupds.setDefaultMaxIdle(parseInt(refAddr)); + } + + refAddr = ref.get("defaultMaxWaitMillis"); + if (refAddr != null && refAddr.getContent() != null) { + pupds.setDefaultMaxWait(Duration.ofMillis(parseInt(refAddr))); + } + + refAddr = ref.get("perUserDefaultAutoCommit"); + if (refAddr != null && refAddr.getContent() != null) { + final byte[] serialized = (byte[]) refAddr.getContent(); + pupds.setPerUserDefaultAutoCommit((Map) deserialize(serialized)); + } + + refAddr = ref.get("perUserDefaultTransactionIsolation"); + if (refAddr != null && refAddr.getContent() != null) { + final byte[] serialized = (byte[]) refAddr.getContent(); + pupds.setPerUserDefaultTransactionIsolation((Map) deserialize(serialized)); + } + + refAddr = ref.get("perUserMaxTotal"); + if (refAddr != null && refAddr.getContent() != null) { + final byte[] serialized = (byte[]) refAddr.getContent(); + pupds.setPerUserMaxTotal((Map) deserialize(serialized)); + } + + refAddr = ref.get("perUserMaxIdle"); + if (refAddr != null && refAddr.getContent() != null) { + final byte[] serialized = (byte[]) refAddr.getContent(); + pupds.setPerUserMaxIdle((Map) deserialize(serialized)); + } + + refAddr = ref.get("perUserMaxWaitMillis"); + if (refAddr != null && refAddr.getContent() != null) { + final byte[] serialized = (byte[]) refAddr.getContent(); + pupds.setPerUserMaxWaitMillis((Map) deserialize(serialized)); + } + + refAddr = ref.get("perUserDefaultReadOnly"); + if (refAddr != null && refAddr.getContent() != null) { + final byte[] serialized = (byte[]) refAddr.getContent(); + pupds.setPerUserDefaultReadOnly((Map) deserialize(serialized)); + } + return pupds; + } + + @Override + protected boolean isCorrectClass(final String className) { + return PER_USER_POOL_CLASSNAME.equals(className); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java new file mode 100644 index 0000000..9cc08ab --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.io.Serializable; +import java.util.Objects; + +/** + * @since 2.0 + */ +final class PoolKey implements Serializable { + private static final long serialVersionUID = 2252771047542484533L; + + private final String dataSourceName; + private final String userName; + + PoolKey(final String dataSourceName, final String userName) { + this.dataSourceName = dataSourceName; + this.userName = userName; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final PoolKey other = (PoolKey) obj; + if (!Objects.equals(dataSourceName, other.dataSourceName)) { + return false; + } + return Objects.equals(userName, other.userName); + } + + @Override + public int hashCode() { + return Objects.hash(dataSourceName, userName); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(50); + sb.append("PoolKey("); + sb.append(dataSourceName); + sb.append(')'); + return sb.toString(); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java new file mode 100644 index 0000000..18aa8c1 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java @@ -0,0 +1,77 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import javax.sql.PooledConnection; + +/** + * Immutable poolable object holding a {@link PooledConnection} along with the user name and password used to create the + * connection. + * + * @since 2.0 + */ +final class PooledConnectionAndInfo { + + private final PooledConnection pooledConnection; + + private final UserPassKey userPassKey; + + /** + * Constructs a new instance. + * + * @since 2.4.0 + */ + PooledConnectionAndInfo(final PooledConnection pooledConnection, final char[] userName, final char[] userPassword) { + this(pooledConnection, new UserPassKey(userName, userPassword)); + } + + PooledConnectionAndInfo(final PooledConnection pooledConnection, final UserPassKey userPassKey) { + this.pooledConnection = pooledConnection; + this.userPassKey = userPassKey; + } + + /** + * Gets the value of password. + * + * @return value of password. + */ + String getPassword() { + return userPassKey.getPassword(); + } + + /** + * Gets the pooled connection. + * + * @return the pooled connection. + */ + PooledConnection getPooledConnection() { + return pooledConnection; + } + + /** + * Gets the value of userName. + * + * @return value of userName. + */ + String getUserName() { + return userPassKey.getUserName(); + } + + UserPassKey getUserPassKey() { + return userPassKey; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java new file mode 100644 index 0000000..f72eaa2 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java @@ -0,0 +1,68 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.sql.SQLException; + +import javax.sql.PooledConnection; + +/** + * Methods to manage PoolableConnections and the connection pools that source them. + * + * @since 2.0 + */ +interface PooledConnectionManager { + + /** + * Closes the connection pool associated with the given user. + * + * @param userName + * user name + * @throws SQLException + * if an error occurs closing idle connections in the pool + */ + void closePool(String userName) throws SQLException; + + /** + * Closes the PooledConnection and remove it from the connection pool to which it belongs, adjusting pool counters. + * + * @param pc + * PooledConnection to be invalidated + * @throws SQLException + * if an SQL error occurs closing the connection + */ + void invalidate(PooledConnection pc) throws SQLException; + +// /** +// * Sets the database password used when creating connections. +// * +// * @param password password used when authenticating to the database +// * @since 2.10.0 +// */ +// default void setPassword(char[] password) { +// setPassword(String.copyValueOf(password)); +// } + + /** + * Sets the database password used when creating connections. + * + * @param password + * password used when authenticating to the database + */ + void setPassword(String password); + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java new file mode 100644 index 0000000..7aaaa29 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java @@ -0,0 +1,230 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.sql.Connection; +import java.sql.SQLException; + +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.StringRefAddr; +import javax.sql.ConnectionPoolDataSource; + +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPoolConfig; + +/** + *

    + * A pooling {@code DataSource} appropriate for deployment within J2EE environment. There are many configuration + * options, most of which are defined in the parent class. All users (based on user name) share a single maximum number + * of Connections in this data source. + *

    + * + *

    + * User passwords can be changed without re-initializing the data source. When a + * {@code getConnection(user name, password)} request is processed with a password that is different from those + * used to create connections in the pool associated with {@code user name}, an attempt is made to create a new + * connection using the supplied password and if this succeeds, idle connections created using the old password are + * destroyed and new connections are created using the new password. + *

    + * + * @since 2.0 + */ +public class SharedPoolDataSource extends InstanceKeyDataSource { + + private static final long serialVersionUID = -1458539734480586454L; + + // Pool properties + private int maxTotal = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL; + + private transient KeyedObjectPool pool; + private transient KeyedCPDSConnectionFactory factory; + + /** + * Default no-argument constructor for Serialization + */ + public SharedPoolDataSource() { + // empty. + } + + /** + * Closes pool being maintained by this data source. + */ + @Override + public void close() throws SQLException { + if (pool != null) { + pool.close(); + } + InstanceKeyDataSourceFactory.removeInstance(getInstanceKey()); + } + + @Override + protected PooledConnectionManager getConnectionManager(final UserPassKey userPassKey) { + return factory; + } + + /** + * Gets {@link GenericKeyedObjectPool#getMaxTotal()} for this pool. + * + * @return {@link GenericKeyedObjectPool#getMaxTotal()} for this pool. + */ + public int getMaxTotal() { + return this.maxTotal; + } + + // ---------------------------------------------------------------------- + // Instrumentation Methods + + /** + * Gets the number of active connections in the pool. + * + * @return The number of active connections in the pool. + */ + public int getNumActive() { + return pool == null ? 0 : pool.getNumActive(); + } + + /** + * Gets the number of idle connections in the pool. + * + * @return The number of idle connections in the pool. + */ + public int getNumIdle() { + return pool == null ? 0 : pool.getNumIdle(); + } + + @Override + protected PooledConnectionAndInfo getPooledConnectionAndInfo(final String userName, final String userPassword) + throws SQLException { + + synchronized (this) { + if (pool == null) { + try { + registerPool(userName, userPassword); + } catch (final NamingException e) { + throw new SQLException("registerPool failed", e); + } + } + } + + try { + return pool.borrowObject(new UserPassKey(userName, userPassword)); + } catch (final Exception e) { + throw new SQLException("Could not retrieve connection info from pool", e); + } + } + + /** + * Creates a new {@link Reference} to a {@link SharedPoolDataSource}. + */ + @Override + public Reference getReference() throws NamingException { + final Reference ref = new Reference(getClass().getName(), SharedPoolDataSourceFactory.class.getName(), null); + ref.add(new StringRefAddr("instanceKey", getInstanceKey())); + return ref; + } + + /** + * Supports Serialization interface. + * + * @param in + * a {@code java.io.ObjectInputStream} value + * @throws IOException + * if an error occurs + * @throws ClassNotFoundException + * if an error occurs + */ + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + in.defaultReadObject(); + final SharedPoolDataSource oldDS = (SharedPoolDataSource) new SharedPoolDataSourceFactory().getObjectInstance(getReference(), null, null, null); + this.pool = oldDS.pool; + } catch (final NamingException e) { + throw new IOException("NamingException: " + e); + } + } + + private void registerPool(final String userName, final String password) throws NamingException, SQLException { + + final ConnectionPoolDataSource cpds = testCPDS(userName, password); + + // Create an object pool to contain our PooledConnections + factory = new KeyedCPDSConnectionFactory(cpds, getValidationQuery(), getValidationQueryTimeoutDuration(), isRollbackAfterValidation()); + factory.setMaxConn(getMaxConnDuration()); + + final GenericKeyedObjectPoolConfig config = new GenericKeyedObjectPoolConfig<>(); + config.setBlockWhenExhausted(getDefaultBlockWhenExhausted()); + config.setEvictionPolicyClassName(getDefaultEvictionPolicyClassName()); + config.setLifo(getDefaultLifo()); + config.setMaxIdlePerKey(getDefaultMaxIdle()); + config.setMaxTotal(getMaxTotal()); + config.setMaxTotalPerKey(getDefaultMaxTotal()); + config.setMaxWait(getDefaultMaxWait()); + config.setMinEvictableIdleDuration(getDefaultMinEvictableIdleDuration()); + config.setMinIdlePerKey(getDefaultMinIdle()); + config.setNumTestsPerEvictionRun(getDefaultNumTestsPerEvictionRun()); + config.setSoftMinEvictableIdleDuration(getDefaultSoftMinEvictableIdleDuration()); + config.setTestOnCreate(getDefaultTestOnCreate()); + config.setTestOnBorrow(getDefaultTestOnBorrow()); + config.setTestOnReturn(getDefaultTestOnReturn()); + config.setTestWhileIdle(getDefaultTestWhileIdle()); + config.setTimeBetweenEvictionRuns(getDefaultDurationBetweenEvictionRuns()); + + final KeyedObjectPool tmpPool = new GenericKeyedObjectPool<>(factory, config); + factory.setPool(tmpPool); + pool = tmpPool; + } + + /** + * Sets {@link GenericKeyedObjectPool#getMaxTotal()} for this pool. + * + * @param maxTotal + * {@link GenericKeyedObjectPool#getMaxTotal()} for this pool. + */ + public void setMaxTotal(final int maxTotal) { + assertInitializationAllowed(); + this.maxTotal = maxTotal; + } + + @Override + protected void setupDefaults(final Connection connection, final String userName) throws SQLException { + final Boolean defaultAutoCommit = isDefaultAutoCommit(); + if (defaultAutoCommit != null && connection.getAutoCommit() != defaultAutoCommit.booleanValue()) { + connection.setAutoCommit(defaultAutoCommit.booleanValue()); + } + + final int defaultTransactionIsolation = getDefaultTransactionIsolation(); + if (defaultTransactionIsolation != UNKNOWN_TRANSACTIONISOLATION) { + connection.setTransactionIsolation(defaultTransactionIsolation); + } + + final Boolean defaultReadOnly = isDefaultReadOnly(); + if (defaultReadOnly != null && connection.isReadOnly() != defaultReadOnly.booleanValue()) { + connection.setReadOnly(defaultReadOnly.booleanValue()); + } + } + + @Override + protected void toStringFields(final StringBuilder builder) { + super.toStringFields(builder); + builder.append(", maxTotal="); + builder.append(maxTotal); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java new file mode 100644 index 0000000..b3feafe --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java @@ -0,0 +1,44 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import javax.naming.RefAddr; +import javax.naming.Reference; + +/** + * A JNDI ObjectFactory which creates {@code SharedPoolDataSource}s + * + * @since 2.0 + */ +public class SharedPoolDataSourceFactory extends InstanceKeyDataSourceFactory { + private static final String SHARED_POOL_CLASSNAME = SharedPoolDataSource.class.getName(); + + @Override + protected InstanceKeyDataSource getNewInstance(final Reference ref) { + final SharedPoolDataSource spds = new SharedPoolDataSource(); + final RefAddr ra = ref.get("maxTotal"); + if (ra != null && ra.getContent() != null) { + spds.setMaxTotal(Integer.parseInt(ra.getContent().toString())); + } + return spds; + } + + @Override + protected boolean isCorrectClass(final String className) { + return SHARED_POOL_CLASSNAME.equals(className); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java new file mode 100644 index 0000000..02ed24b --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java @@ -0,0 +1,119 @@ +/* + * 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.tomcat.dbcp.dbcp2.datasources; + +import java.io.Serializable; +import java.util.Objects; + +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; + +/** + *

    + * Holds a user name and password pair. Serves as a poolable object key for the {@link KeyedObjectPool} backing a + * {@link SharedPoolDataSource}. Two instances with the same user name are considered equal. This ensures that there + * will be only one keyed pool for each user in the pool. The password is used (along with the user name) by the + * {@code KeyedCPDSConnectionFactory} when creating new connections. + *

    + * + *

    + * {@link InstanceKeyDataSource#getConnection(String, String)} validates that the password used to create a connection + * matches the password provided by the client. + *

    + * + * @since 2.0 + */ +final class UserPassKey implements Serializable { + private static final long serialVersionUID = 5142970911626584817L; + + private final CharArray name; + private final CharArray password; + + UserPassKey(final char[] userName, final char[] password) { + this(new CharArray(userName), new CharArray(password)); + } + + UserPassKey(final CharArray userName, final CharArray userPassword) { + this.name = userName; + this.password = userPassword; + } + + UserPassKey(final String userName) { + this(new CharArray(userName), CharArray.NULL); + } + + UserPassKey(final String userName, final char[] password) { + this(new CharArray(userName), new CharArray(password)); + } + + UserPassKey(final String userName, final String userPassword) { + this(new CharArray(userName), new CharArray(userPassword)); + } + + /** + * Only takes the user name into account. + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final UserPassKey other = (UserPassKey) obj; + return Objects.equals(name, other.name); + } + + /** + * Gets the value of password. + * + * @return value of password. + */ + String getPassword() { + return password.asString(); + } + + /** + * Gets the value of password. + * + * @return value of password. + */ + char[] getPasswordCharArray() { + return password.get(); + } + + /** + * Gets the value of user name. + * + * @return value of user name. + */ + String getUserName() { + return name.asString(); + } + + /** + * Only takes the user name into account. + */ + @Override + public int hashCode() { + return Objects.hash(name); + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java b/java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java new file mode 100644 index 0000000..25a762d --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java @@ -0,0 +1,183 @@ +/* + * 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. + */ + +/** + *

    + * This package contains two DataSources: {@code PerUserPoolDataSource} and + * {@code SharedPoolDataSource} which provide a database connection pool. + * Below are a couple of usage examples. One shows deployment into a JNDI system. + * The other is a simple example initializing the pool using standard java code. + *

    + * + *

    JNDI

    + * + *

    + * Most + * J2EE containers will provide some way of deploying resources into JNDI. The + * method will vary among containers, but once the resource is available via + * JNDI, the application can access the resource in a container independent + * manner. The following example shows deployment into tomcat (catalina). + *

    + *

    In server.xml, the following would be added to the <Context> for your + * webapp: + *

    + * + * + * <Resource name="jdbc/bookstore" auth="Container" + * type="org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolPoolDataSource"/> + * <ResourceParams name="jdbc/bookstore"> + * <parameter> + * <name>factory</name> + * <value>org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory</value> + * </parameter> + * <parameter> + * <name>dataSourceName</name><value>java:comp/env/jdbc/bookstoreCPDS</value> + * </parameter> + * <parameter> + * <name>defaultMaxTotal</name><value>30</value> + * </parameter> + * </ResourceParams> + * + * + *

    + * In web.xml. Note that elements must be given in the order of the dtd + * described in the servlet specification: + *

    + * + * + * <resource-ref> + * <description> + * Resource reference to a factory for java.sql.Connection + * instances that may be used for talking to a particular + * database that is configured in the server.xml file. + * </description> + * <res-ref-name> + * jdbc/bookstore + * </res-ref-name> + * <res-type> + * org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource + * </res-type> + * <res-auth> + * Container + * </res-auth> + * </resource-ref> + * + * + *

    + * Apache Tomcat deploys all objects configured similarly to above within the + * java:comp/env namespace. So the JNDI path given for + * the dataSourceName parameter is valid for a + * {@code ConnectionPoolDataSource} that is deployed as given in the + * cpdsadapter example + *

    + * + *

    + * The {@code DataSource} is now available to the application as shown + * below: + *

    + * + * + * + * Context ctx = new InitialContext(); + * DataSource ds = (DataSource) + * ctx.lookup("java:comp/env/jdbc/bookstore"); + * Connection con = null; + * try + * { + * con = ds.getConnection(); + * ... + * use the connection + * ... + * } + * finally + * { + * if (con != null) + * con.close(); + * } + * + * + * + *

    + * The reference to the {@code DataSource} could be maintained, for + * multiple getConnection() requests. Or the {@code DataSource} can be + * looked up in different parts of the application code. + * {@code PerUserPoolDataSourceFactory} and + * {@code SharedPoolDataSourceFactory} will maintain the state of the pool + * between different lookups. This behavior may be different in other + * implementations. + *

    + * + *

    Without JNDI

    + * + *

    + * Connection pooling is useful in applications regardless of whether they run + * in a J2EE environment and a {@code DataSource} can be used within a + * simpler environment. The example below shows SharedPoolDataSource using + * DriverAdapterCPDS as the back end source, though any CPDS is applicable. + *

    + * + * + * + * public class Pool + * { + * private static DataSource ds; + * + * static + * { + * DriverAdapterCPDS cpds = new DriverAdapterCPDS(); + * cpds.setDriver("org.gjt.mm.mysql.Driver"); + * cpds.setUrl("jdbc:mysql://localhost:3306/bookstore"); + * cpds.setUser("foo"); + * cpds.setPassword(null); + * + * SharedPoolDataSource tds = new SharedPoolDataSource(); + * tds.setConnectionPoolDataSource(cpds); + * tds.setMaxTotal(10); + * tds.setMaxWaitMillis(50); + * + * ds = tds; + * } + * + * public static getConnection() + * { + * return ds.getConnection(); + * } + * } + * + * + * + *

    + * This class can then be used wherever a connection is needed: + *

    + * + * + * Connection con = null; + * try + * { + * con = Pool.getConnection(); + * ... + * use the connection + * ... + * } + * finally + * { + * if (con != null) + * con.close(); + * } + * + */ +package org.apache.tomcat.dbcp.dbcp2.datasources; diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java new file mode 100644 index 0000000..a81ed4f --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java @@ -0,0 +1,256 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.SQLException; + +import javax.sql.DataSource; +import javax.sql.XADataSource; + +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import org.apache.tomcat.dbcp.dbcp2.BasicDataSource; +import org.apache.tomcat.dbcp.dbcp2.ConnectionFactory; +import org.apache.tomcat.dbcp.dbcp2.PoolableConnection; +import org.apache.tomcat.dbcp.dbcp2.PoolableConnectionFactory; +import org.apache.tomcat.dbcp.dbcp2.PoolingDataSource; +import org.apache.tomcat.dbcp.dbcp2.Utils; + +/** + *

    + * BasicManagedDataSource is an extension of BasicDataSource which creates ManagedConnections. This data source can + * create either full two-phase-commit XA connections or one-phase-commit local connections. Both types of connections + * are committed or rolled back as part of the global transaction (a.k.a. XA transaction or JTA Transaction), but only + * XA connections can be recovered in the case of a system crash. + *

    + *

    + * BasicManagedDataSource adds the TransactionManager and XADataSource properties. The TransactionManager property is + * required and is used to enlist connections in global transactions. The XADataSource is optional and if set is the + * class name of the XADataSource class for a two-phase-commit JDBC driver. If the XADataSource property is set, the + * driverClassName is ignored and a DataSourceXAConnectionFactory is created. Otherwise, a standard + * DriverConnectionFactory is created and wrapped with a LocalXAConnectionFactory. + *

    + * + * @see BasicDataSource + * @see ManagedConnection + * @since 2.0 + */ +public class BasicManagedDataSource extends BasicDataSource { + + /** Transaction Registry */ + private TransactionRegistry transactionRegistry; + + /** Transaction Manager */ + private transient TransactionManager transactionManager; + + /** XA data source class name */ + private String xaDataSource; + + /** XA data source instance */ + private XADataSource xaDataSourceInstance; + + /** Transaction Synchronization Registry */ + private transient TransactionSynchronizationRegistry transactionSynchronizationRegistry; + + @Override + protected ConnectionFactory createConnectionFactory() throws SQLException { + if (transactionManager == null) { + throw new SQLException("Transaction manager must be set before a connection can be created"); + } + + // If xa data source is not specified a DriverConnectionFactory is created and wrapped with a + // LocalXAConnectionFactory + if (xaDataSource == null) { + final ConnectionFactory connectionFactory = super.createConnectionFactory(); + final XAConnectionFactory xaConnectionFactory = new LocalXAConnectionFactory(getTransactionManager(), + getTransactionSynchronizationRegistry(), connectionFactory); + transactionRegistry = xaConnectionFactory.getTransactionRegistry(); + return xaConnectionFactory; + } + + // Create the XADataSource instance using the configured class name if it has not been set + if (xaDataSourceInstance == null) { + Class xaDataSourceClass = null; + try { + xaDataSourceClass = Class.forName(xaDataSource); + } catch (final Exception t) { + final String message = "Cannot load XA data source class '" + xaDataSource + "'"; + throw new SQLException(message, t); + } + + try { + xaDataSourceInstance = (XADataSource) xaDataSourceClass.getConstructor().newInstance(); + } catch (final Exception t) { + final String message = "Cannot create XA data source of class '" + xaDataSource + "'"; + throw new SQLException(message, t); + } + } + + // finally, create the XAConnectionFactory using the XA data source + final XAConnectionFactory xaConnectionFactory = new DataSourceXAConnectionFactory(getTransactionManager(), + xaDataSourceInstance, getUsername(), Utils.toCharArray(getPassword()), getTransactionSynchronizationRegistry()); + transactionRegistry = xaConnectionFactory.getTransactionRegistry(); + return xaConnectionFactory; + } + + @Override + protected DataSource createDataSourceInstance() throws SQLException { + final PoolingDataSource pds = new ManagedDataSource<>(getConnectionPool(), + transactionRegistry); + pds.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed()); + return pds; + } + + /** + * Creates the PoolableConnectionFactory and attaches it to the connection pool. + * + * @param driverConnectionFactory + * JDBC connection factory created by {@link #createConnectionFactory()} + * @throws SQLException + * if an error occurs creating the PoolableConnectionFactory + */ + @Override + protected PoolableConnectionFactory createPoolableConnectionFactory(final ConnectionFactory driverConnectionFactory) + throws SQLException { + PoolableConnectionFactory connectionFactory = null; + try { + connectionFactory = new PoolableManagedConnectionFactory((XAConnectionFactory) driverConnectionFactory, + getRegisteredJmxName()); + connectionFactory.setValidationQuery(getValidationQuery()); + connectionFactory.setValidationQueryTimeout(getValidationQueryTimeoutDuration()); + connectionFactory.setConnectionInitSql(getConnectionInitSqls()); + connectionFactory.setDefaultReadOnly(getDefaultReadOnly()); + connectionFactory.setDefaultAutoCommit(getDefaultAutoCommit()); + connectionFactory.setDefaultTransactionIsolation(getDefaultTransactionIsolation()); + connectionFactory.setDefaultCatalog(getDefaultCatalog()); + connectionFactory.setDefaultSchema(getDefaultSchema()); + connectionFactory.setCacheState(getCacheState()); + connectionFactory.setPoolStatements(isPoolPreparedStatements()); + connectionFactory.setClearStatementPoolOnReturn(isClearStatementPoolOnReturn()); + connectionFactory.setMaxOpenPreparedStatements(getMaxOpenPreparedStatements()); + connectionFactory.setMaxConn(getMaxConnDuration()); + connectionFactory.setRollbackOnReturn(getRollbackOnReturn()); + connectionFactory.setAutoCommitOnReturn(getAutoCommitOnReturn()); + connectionFactory.setDefaultQueryTimeout(getDefaultQueryTimeoutDuration()); + connectionFactory.setFastFailValidation(getFastFailValidation()); + connectionFactory.setDisconnectionSqlCodes(getDisconnectionSqlCodes()); + validateConnectionFactory(connectionFactory); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new SQLException("Cannot create PoolableConnectionFactory (" + e.getMessage() + ")", e); + } + return connectionFactory; + } + + /** + * Gets the required transaction manager property. + * + * @return the transaction manager used to enlist connections + */ + public TransactionManager getTransactionManager() { + return transactionManager; + } + + /** + * Gets the transaction registry. + * + * @return the transaction registry associating XAResources with managed connections + */ + protected synchronized TransactionRegistry getTransactionRegistry() { + return transactionRegistry; + } + + /** + * Gets the optional TransactionSynchronizationRegistry. + * + * @return the TSR that can be used to register synchronizations. + * @since 2.6.0 + */ + public TransactionSynchronizationRegistry getTransactionSynchronizationRegistry() { + return transactionSynchronizationRegistry; + } + + /** + * Gets the optional XADataSource class name. + * + * @return the optional XADataSource class name + */ + public synchronized String getXADataSource() { + return xaDataSource; + } + + /** + * Gets the XADataSource instance used by the XAConnectionFactory. + * + * @return the XADataSource + */ + public synchronized XADataSource getXaDataSourceInstance() { + return xaDataSourceInstance; + } + + /** + * Sets the required transaction manager property. + * + * @param transactionManager + * the transaction manager used to enlist connections + */ + public void setTransactionManager(final TransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + /** + * Sets the optional TransactionSynchronizationRegistry property. + * + * @param transactionSynchronizationRegistry + * the TSR used to register synchronizations + * @since 2.6.0 + */ + public void setTransactionSynchronizationRegistry( + final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + } + + /** + * Sets the optional XADataSource class name. + * + * @param xaDataSource + * the optional XADataSource class name + */ + public synchronized void setXADataSource(final String xaDataSource) { + this.xaDataSource = xaDataSource; + } + + /** + *

    + * Sets the XADataSource instance used by the XAConnectionFactory. + *

    + *

    + * Note: this method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: getConnection, setLogwriter, + * setLoginTimeout, getLoginTimeout, getLogWriter. + *

    + * + * @param xaDataSourceInstance + * XADataSource instance + */ + public synchronized void setXaDataSourceInstance(final XADataSource xaDataSourceInstance) { + this.xaDataSourceInstance = xaDataSourceInstance; + xaDataSource = xaDataSourceInstance == null ? null : xaDataSourceInstance.getClass().getName(); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java new file mode 100644 index 0000000..267c866 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java @@ -0,0 +1,259 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Objects; + +import javax.sql.ConnectionEvent; +import javax.sql.ConnectionEventListener; +import javax.sql.PooledConnection; +import javax.sql.XAConnection; +import javax.sql.XADataSource; +import javax.transaction.xa.XAResource; + +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import org.apache.tomcat.dbcp.dbcp2.Utils; + +/** + * An implementation of XAConnectionFactory which uses a real XADataSource to obtain connections and XAResources. + * + * @since 2.0 + */ +public class DataSourceXAConnectionFactory implements XAConnectionFactory { + + private static final class XAConnectionEventListener implements ConnectionEventListener { + @Override + public void connectionClosed(final ConnectionEvent event) { + final PooledConnection pc = (PooledConnection) event.getSource(); + pc.removeConnectionEventListener(this); + try { + pc.close(); + } catch (final SQLException e) { + System.err.println("Failed to close XAConnection"); + e.printStackTrace(); + } + } + + @Override + public void connectionErrorOccurred(final ConnectionEvent event) { + connectionClosed(event); + } + } + + private final TransactionRegistry transactionRegistry; + private final XADataSource xaDataSource; + private String userName; + private char[] userPassword; + + /** + * Creates an DataSourceXAConnectionFactory which uses the specified XADataSource to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param xaDataSource + * the data source from which connections will be retrieved + * @since 2.6.0 + */ + public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource) { + this(transactionManager, xaDataSource, null, (char[]) null, null); + } + + /** + * Creates an DataSourceXAConnectionFactory which uses the specified XADataSource to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param xaDataSource + * the data source from which connections will be retrieved + * @param userName + * the user name used for authenticating new connections or null for unauthenticated + * @param userPassword + * the password used for authenticating new connections + */ + public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource, + final String userName, final char[] userPassword) { + this(transactionManager, xaDataSource, userName, userPassword, null); + } + + /** + * Creates an DataSourceXAConnectionFactory which uses the specified XADataSource to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param xaDataSource + * the data source from which connections will be retrieved + * @param userName + * the user name used for authenticating new connections or null for unauthenticated + * @param userPassword + * the password used for authenticating new connections + * @param transactionSynchronizationRegistry + * register with this TransactionSynchronizationRegistry + * @since 2.6.0 + */ + public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource, + final String userName, final char[] userPassword, final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { + Objects.requireNonNull(transactionManager, "transactionManager"); + Objects.requireNonNull(xaDataSource, "xaDataSource"); + + // We do allow the transactionSynchronizationRegistry to be null for non-app server environments + + this.transactionRegistry = new TransactionRegistry(transactionManager, transactionSynchronizationRegistry); + this.xaDataSource = xaDataSource; + this.userName = userName; + this.userPassword = Utils.clone(userPassword); + } + + /** + * Creates an DataSourceXAConnectionFactory which uses the specified XADataSource to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param xaDataSource + * the data source from which connections will be retrieved + * @param userName + * the user name used for authenticating new connections or null for unauthenticated + * @param userPassword + * the password used for authenticating new connections + */ + public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource, + final String userName, final String userPassword) { + this(transactionManager, xaDataSource, userName, Utils.toCharArray(userPassword), null); + } + + /** + * Creates an DataSourceXAConnectionFactory which uses the specified XADataSource to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param xaDataSource + * the data source from which connections will be retrieved + * @param transactionSynchronizationRegistry + * register with this TransactionSynchronizationRegistry + */ + public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource, final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { + this(transactionManager, xaDataSource, null, (char[]) null, transactionSynchronizationRegistry); + } + + @Override + public Connection createConnection() throws SQLException { + // create a new XAConnection + final XAConnection xaConnection; + if (userName == null) { + xaConnection = xaDataSource.getXAConnection(); + } else { + xaConnection = xaDataSource.getXAConnection(userName, Utils.toString(userPassword)); + } + + // get the real connection and XAResource from the connection + final Connection connection = xaConnection.getConnection(); + final XAResource xaResource = xaConnection.getXAResource(); + + // register the xa resource for the connection + transactionRegistry.registerConnection(connection, xaResource); + + // The Connection we're returning is a handle on the XAConnection. + // When the pool calling us closes the Connection, we need to + // also close the XAConnection that holds the physical connection. + xaConnection.addConnectionEventListener(new XAConnectionEventListener()); + + return connection; + } + + @Override + public TransactionRegistry getTransactionRegistry() { + return transactionRegistry; + } + + /** + * Gets the user name used to authenticate new connections. + * + * @return the user name or null if unauthenticated connections are used + * @deprecated Use {@link #getUserName()}. + */ + @Deprecated + public String getUsername() { + return userName; + } + + /** + * Gets the user name used to authenticate new connections. + * + * @return the user name or null if unauthenticated connections are used + * @since 2.6.0 + */ + public String getUserName() { + return userName; + } + + /** + * Gets the user password. + * + * @return the user password. + */ + public char[] getUserPassword() { + return Utils.clone(userPassword); + } + + /** + * Gets the XA data source. + * + * @return the XA data source. + */ + public XADataSource getXaDataSource() { + return xaDataSource; + } + + /** + * Sets the password used to authenticate new connections. + * + * @param userPassword + * the password used for authenticating the connection or null for unauthenticated. + * @since 2.4.0 + */ + public void setPassword(final char[] userPassword) { + this.userPassword = Utils.clone(userPassword); + } + + /** + * Sets the password used to authenticate new connections. + * + * @param userPassword + * the password used for authenticating the connection or null for unauthenticated + */ + public void setPassword(final String userPassword) { + this.userPassword = Utils.toCharArray(userPassword); + } + + /** + * Sets the user name used to authenticate new connections. + * + * @param userName + * the user name used for authenticating the connection or null for unauthenticated + */ + public void setUsername(final String userName) { + this.userName = userName; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java new file mode 100644 index 0000000..8906330 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java @@ -0,0 +1,384 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Objects; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import org.apache.tomcat.dbcp.dbcp2.ConnectionFactory; + +/** + * An implementation of XAConnectionFactory which manages non-XA connections in XA transactions. A non-XA connection + * commits and rolls back as part of the XA transaction, but is not recoverable since the connection does not implement + * the 2-phase protocol. + * + * @since 2.0 + */ +public class LocalXAConnectionFactory implements XAConnectionFactory { + + /** + * LocalXAResource is a fake XAResource for non-XA connections. When a transaction is started the connection + * auto-commit is turned off. When the connection is committed or rolled back, the commit or rollback method is + * called on the connection and then the original auto-commit value is restored. + *

    + * The LocalXAResource also respects the connection read-only setting. If the connection is read-only the commit + * method will not be called, and the prepare method returns the XA_RDONLY. + *

    + *

    + * It is assumed that the wrapper around a managed connection disables the setAutoCommit(), commit(), rollback() and + * setReadOnly() methods while a transaction is in progress. + *

    + * + * @since 2.0 + */ + protected static class LocalXAResource implements XAResource { + private static final Xid[] EMPTY_XID_ARRAY = {}; + private final Connection connection; + private Xid currentXid; // @GuardedBy("this") + private boolean originalAutoCommit; // @GuardedBy("this") + + /** + * Construct a new instance for a given connection. + * + * @param localTransaction A connection. + */ + public LocalXAResource(final Connection localTransaction) { + this.connection = localTransaction; + } + + private Xid checkCurrentXid() throws XAException { + if (this.currentXid == null) { + throw new XAException("There is no current transaction"); + } + return currentXid; + } + + /** + * Commits the transaction and restores the original auto commit setting. + * + * @param xid + * the id of the transaction branch for this connection + * @param flag + * ignored + * @throws XAException + * if connection.commit() throws an SQLException + */ + @Override + public synchronized void commit(final Xid xid, final boolean flag) throws XAException { + Objects.requireNonNull(xid, "xid"); + if (!checkCurrentXid().equals(xid)) { + throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid); + } + + try { + // make sure the connection isn't already closed + if (connection.isClosed()) { + throw new XAException("Connection is closed"); + } + + // A read only connection should not be committed + if (!connection.isReadOnly()) { + connection.commit(); + } + } catch (final SQLException e) { + throw (XAException) new XAException().initCause(e); + } finally { + try { + connection.setAutoCommit(originalAutoCommit); + } catch (final SQLException ignored) { + // ignored + } + this.currentXid = null; + } + } + + /** + * This method does nothing. + * + * @param xid + * the id of the transaction branch for this connection + * @param flag + * ignored + * @throws XAException + * if the connection is already enlisted in another transaction + */ + @Override + public synchronized void end(final Xid xid, final int flag) throws XAException { + Objects.requireNonNull(xid, "xid"); + if (!checkCurrentXid().equals(xid)) { + throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid); + } + + // This notification tells us that the application server is done using this + // connection for the time being. The connection is still associated with an + // open transaction, so we must still wait for the commit or rollback method + } + + /** + * Clears the currently associated transaction if it is the specified xid. + * + * @param xid + * the id of the transaction to forget + */ + @Override + public synchronized void forget(final Xid xid) { + if (xid != null && xid.equals(currentXid)) { + currentXid = null; + } + } + + /** + * Always returns 0 since we have no way to set a transaction timeout on a JDBC connection. + * + * @return always 0 + */ + @Override + public int getTransactionTimeout() { + return 0; + } + + /** + * Gets the current xid of the transaction branch associated with this XAResource. + * + * @return the current xid of the transaction branch associated with this XAResource. + */ + public synchronized Xid getXid() { + return currentXid; + } + + /** + * Returns true if the specified XAResource == this XAResource. + * + * @param xaResource + * the XAResource to test + * @return true if the specified XAResource == this XAResource; false otherwise + */ + @Override + public boolean isSameRM(final XAResource xaResource) { + return this == xaResource; + } + + /** + * This method does nothing since the LocalXAConnection does not support two-phase-commit. This method will + * return XAResource.XA_RDONLY if the connection isReadOnly(). This assumes that the physical connection is + * wrapped with a proxy that prevents an application from changing the read-only flag while enrolled in a + * transaction. + * + * @param xid + * the id of the transaction branch for this connection + * @return XAResource.XA_RDONLY if the connection.isReadOnly(); XAResource.XA_OK otherwise + */ + @Override + public synchronized int prepare(final Xid xid) { + // if the connection is read-only, then the resource is read-only + // NOTE: this assumes that the outer proxy throws an exception when application code + // attempts to set this in a transaction + try { + if (connection.isReadOnly()) { + // update the auto commit flag + connection.setAutoCommit(originalAutoCommit); + + // tell the transaction manager we are read only + return XAResource.XA_RDONLY; + } + } catch (final SQLException ignored) { + // no big deal + } + + // this is a local (one phase) only connection, so we can't prepare + return XAResource.XA_OK; + } + + /** + * Always returns a zero length Xid array. The LocalXAConnectionFactory can not support recovery, so no xids + * will ever be found. + * + * @param flag + * ignored since recovery is not supported + * @return always a zero length Xid array. + */ + @Override + public Xid[] recover(final int flag) { + return EMPTY_XID_ARRAY; + } + + /** + * Rolls back the transaction and restores the original auto commit setting. + * + * @param xid + * the id of the transaction branch for this connection + * @throws XAException + * if connection.rollback() throws an SQLException + */ + @Override + public synchronized void rollback(final Xid xid) throws XAException { + Objects.requireNonNull(xid, "xid"); + if (!checkCurrentXid().equals(xid)) { + throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid); + } + + try { + connection.rollback(); + } catch (final SQLException e) { + throw (XAException) new XAException().initCause(e); + } finally { + try { + connection.setAutoCommit(originalAutoCommit); + } catch (final SQLException ignored) { + // Ignored. + } + this.currentXid = null; + } + } + + /** + * Always returns false since we have no way to set a transaction timeout on a JDBC connection. + * + * @param transactionTimeout + * ignored since we have no way to set a transaction timeout on a JDBC connection + * @return always false + */ + @Override + public boolean setTransactionTimeout(final int transactionTimeout) { + return false; + } + + /** + * Signals that a the connection has been enrolled in a transaction. This method saves off the current auto + * commit flag, and then disables auto commit. The original auto commit setting is restored when the transaction + * completes. + * + * @param xid + * the id of the transaction branch for this connection + * @param flag + * either XAResource.TMNOFLAGS or XAResource.TMRESUME + * @throws XAException + * if the connection is already enlisted in another transaction, or if auto-commit could not be + * disabled + */ + @Override + public synchronized void start(final Xid xid, final int flag) throws XAException { + if (flag == XAResource.TMNOFLAGS) { + // first time in this transaction + + // make sure we aren't already in another tx + if (this.currentXid != null) { + throw new XAException("Already enlisted in another transaction with xid " + xid); + } + + // save off the current auto commit flag so it can be restored after the transaction completes + try { + originalAutoCommit = connection.getAutoCommit(); + } catch (final SQLException ignored) { + // no big deal, just assume it was off + originalAutoCommit = true; + } + + // update the auto commit flag + try { + connection.setAutoCommit(false); + } catch (final SQLException e) { + throw (XAException) new XAException("Count not turn off auto commit for a XA transaction") + .initCause(e); + } + + this.currentXid = xid; + } else if (flag == XAResource.TMRESUME) { + if (!xid.equals(this.currentXid)) { + throw new XAException("Attempting to resume in different transaction: expected " + this.currentXid + + ", but was " + xid); + } + } else { + throw new XAException("Unknown start flag " + flag); + } + } + } + private final TransactionRegistry transactionRegistry; + + private final ConnectionFactory connectionFactory; + + /** + * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param connectionFactory + * the connection factory from which connections will be retrieved + */ + public LocalXAConnectionFactory(final TransactionManager transactionManager, + final ConnectionFactory connectionFactory) { + this(transactionManager, null, connectionFactory); + } + + /** + * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param transactionSynchronizationRegistry + * the optional TSR to register synchronizations with + * @param connectionFactory + * the connection factory from which connections will be retrieved + * @since 2.8.0 + */ + public LocalXAConnectionFactory(final TransactionManager transactionManager, + final TransactionSynchronizationRegistry transactionSynchronizationRegistry, + final ConnectionFactory connectionFactory) { + Objects.requireNonNull(transactionManager, "transactionManager"); + Objects.requireNonNull(connectionFactory, "connectionFactory"); + this.transactionRegistry = new TransactionRegistry(transactionManager, transactionSynchronizationRegistry); + this.connectionFactory = connectionFactory; + } + + @Override + public Connection createConnection() throws SQLException { + // create a new connection + final Connection connection = connectionFactory.createConnection(); + + // create a XAResource to manage the connection during XA transactions + final XAResource xaResource = new LocalXAResource(connection); + + // register the xa resource for the connection + transactionRegistry.registerConnection(connection, xaResource); + + return connection; + } + + /** + * @return The connection factory. + * @since 2.6.0 + */ + public ConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + @Override + public TransactionRegistry getTransactionRegistry() { + return transactionRegistry; + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java new file mode 100644 index 0000000..81d370e --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java @@ -0,0 +1,327 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.tomcat.dbcp.dbcp2.DelegatingConnection; +import org.apache.tomcat.dbcp.pool2.ObjectPool; + +/** + * ManagedConnection is responsible for managing a database connection in a transactional environment (typically called + * "Container Managed"). A managed connection operates like any other connection when no global transaction (a.k.a. XA + * transaction or JTA Transaction) is in progress. When a global transaction is active a single physical connection to + * the database is used by all ManagedConnections accessed in the scope of the transaction. Connection sharing means + * that all data access during a transaction has a consistent view of the database. When the global transaction is + * committed or rolled back the enlisted connections are committed or rolled back. Typically upon transaction + * completion, a connection returns to the auto commit setting in effect before being enlisted in the transaction, but + * some vendors do not properly implement this. + *

    + * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods throw a + * SQLException. This is necessary to assure that the transaction completes as a single unit. + *

    + * + * @param + * the Connection type + * + * @since 2.0 + */ +public class ManagedConnection extends DelegatingConnection { + + /** + * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events. + * + * @since 2.0 + */ + protected class CompletionListener implements TransactionContextListener { + @Override + public void afterCompletion(final TransactionContext completedContext, final boolean committed) { + if (completedContext == transactionContext) { + transactionComplete(); + } + } + } + + private final ObjectPool pool; + private final TransactionRegistry transactionRegistry; + private final boolean accessToUnderlyingConnectionAllowed; + private TransactionContext transactionContext; + private boolean isSharedConnection; + private final Lock lock; + + /** + * Constructs a new instance responsible for managing a database connection in a transactional environment. + * + * @param pool + * The connection pool. + * @param transactionRegistry + * The transaction registry. + * @param accessToUnderlyingConnectionAllowed + * Whether or not to allow access to the underlying Connection. + * @throws SQLException + * Thrown when there is problem managing transactions. + */ + public ManagedConnection(final ObjectPool pool, final TransactionRegistry transactionRegistry, + final boolean accessToUnderlyingConnectionAllowed) throws SQLException { + super(null); + this.pool = pool; + this.transactionRegistry = transactionRegistry; + this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed; + this.lock = new ReentrantLock(); + updateTransactionStatus(); + } + + @Override + protected void checkOpen() throws SQLException { + super.checkOpen(); + updateTransactionStatus(); + } + + @Override + public void close() throws SQLException { + if (!isClosedInternal()) { + // Don't actually close the connection if in a transaction. The + // connection will be closed by the transactionComplete method. + // + // DBCP-484 we need to make sure setClosedInternal(true) being + // invoked if transactionContext is not null as this value will + // be modified by the transactionComplete method which could run + // in the different thread with the transaction calling back. + lock.lock(); + try { + if (transactionContext == null || transactionContext.isTransactionComplete()) { + super.close(); + } + } finally { + try { + setClosedInternal(true); + } finally { + lock.unlock(); + } + } + } + } + + @Override + public void commit() throws SQLException { + if (transactionContext != null) { + throw new SQLException("Commit can not be set while enrolled in a transaction"); + } + super.commit(); + } + + @Override + public C getDelegate() { + if (isAccessToUnderlyingConnectionAllowed()) { + return getDelegateInternal(); + } + return null; + } + + // + // The following methods can't be used while enlisted in a transaction + // + + @Override + public Connection getInnermostDelegate() { + if (isAccessToUnderlyingConnectionAllowed()) { + return super.getInnermostDelegateInternal(); + } + return null; + } + + /** + * @return The transaction context. + * @since 2.6.0 + */ + public TransactionContext getTransactionContext() { + return transactionContext; + } + + /** + * @return The transaction registry. + * @since 2.6.0 + */ + public TransactionRegistry getTransactionRegistry() { + return transactionRegistry; + } + + /** + * If false, getDelegate() and getInnermostDelegate() will return null. + * + * @return if false, getDelegate() and getInnermostDelegate() will return null + */ + public boolean isAccessToUnderlyingConnectionAllowed() { + return accessToUnderlyingConnectionAllowed; + } + + @Override + public void rollback() throws SQLException { + if (transactionContext != null) { + throw new SQLException("Commit can not be set while enrolled in a transaction"); + } + super.rollback(); + } + + @Override + public void setAutoCommit(final boolean autoCommit) throws SQLException { + if (transactionContext != null) { + throw new SQLException("Auto-commit can not be set while enrolled in a transaction"); + } + super.setAutoCommit(autoCommit); + } + + @Override + public void setReadOnly(final boolean readOnly) throws SQLException { + if (transactionContext != null) { + throw new SQLException("Read-only can not be set while enrolled in a transaction"); + } + super.setReadOnly(readOnly); + } + + /** + * Completes the transaction. + */ + protected void transactionComplete() { + lock.lock(); + try { + transactionContext.completeTransaction(); + } finally { + lock.unlock(); + } + + // If we were using a shared connection, clear the reference now that + // the transaction has completed + if (isSharedConnection) { + setDelegate(null); + isSharedConnection = false; + } + + // autoCommit may have been changed directly on the underlying connection + clearCachedState(); + + // If this connection was closed during the transaction and there is + // still a delegate present close it + final Connection delegate = getDelegateInternal(); + if (isClosedInternal() && delegate != null) { + try { + setDelegate(null); + + if (!delegate.isClosed()) { + delegate.close(); + } + } catch (final SQLException ignored) { + // Not a whole lot we can do here as connection is closed + // and this is a transaction callback so there is no + // way to report the error. + } + } + } + + private void updateTransactionStatus() throws SQLException { + // if there is a is an active transaction context, assure the transaction context hasn't changed + if (transactionContext != null && !transactionContext.isTransactionComplete()) { + if (transactionContext.isActive()) { + if (transactionContext != transactionRegistry.getActiveTransactionContext()) { + throw new SQLException("Connection can not be used while enlisted in another transaction"); + } + return; + } + // transaction should have been cleared up by TransactionContextListener, but in + // rare cases another lister could have registered which uses the connection before + // our listener is called. In that rare case, trigger the transaction complete call now + transactionComplete(); + } + + // the existing transaction context ended (or we didn't have one), get the active transaction context + transactionContext = transactionRegistry.getActiveTransactionContext(); + + // if there is an active transaction context and it already has a shared connection, use it + if (transactionContext != null && transactionContext.getSharedConnection() != null) { + // A connection for the connection factory has already been enrolled + // in the transaction, replace our delegate with the enrolled connection + + // return current connection to the pool + final C connection = getDelegateInternal(); + setDelegate(null); + if (connection != null && transactionContext.getSharedConnection() != connection) { + try { + pool.returnObject(connection); + } catch (final Exception e) { + // whatever... try to invalidate the connection + try { + pool.invalidateObject(connection); + } catch (final Exception ignored) { + // no big deal + } + } + } + + // add a listener to the transaction context + transactionContext.addTransactionContextListener(new CompletionListener()); + + // Set our delegate to the shared connection. Note that this will + // always be of type C since it has been shared by another + // connection from the same pool. + @SuppressWarnings("unchecked") + final C shared = (C) transactionContext.getSharedConnection(); + setDelegate(shared); + + // remember that we are using a shared connection so it can be cleared after the + // transaction completes + isSharedConnection = true; + } else { + C connection = getDelegateInternal(); + // if our delegate is null, create one + if (connection == null) { + try { + // borrow a new connection from the pool + connection = pool.borrowObject(); + setDelegate(connection); + } catch (final Exception e) { + throw new SQLException("Unable to acquire a new connection from the pool", e); + } + } + + // if we have a transaction, out delegate becomes the shared delegate + if (transactionContext != null) { + // add a listener to the transaction context + transactionContext.addTransactionContextListener(new CompletionListener()); + + // register our connection as the shared connection + try { + transactionContext.setSharedConnection(connection); + } catch (final SQLException e) { + // transaction is hosed + transactionContext = null; + try { + pool.invalidateObject(connection); + } catch (final Exception ignored) { + // we are try but no luck + } + throw e; + } + } + } + // autoCommit may have been changed directly on the underlying + // connection + clearCachedState(); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java new file mode 100644 index 0000000..802b036 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java @@ -0,0 +1,92 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Objects; + +import org.apache.tomcat.dbcp.dbcp2.PoolingDataSource; +import org.apache.tomcat.dbcp.pool2.ObjectPool; + +/** + * The ManagedDataSource is a PoolingDataSource that creates ManagedConnections. + * + * @param + * The kind of {@link Connection} to manage. + * @since 2.0 + */ +public class ManagedDataSource extends PoolingDataSource { + private TransactionRegistry transactionRegistry; + + /** + * Creates a ManagedDataSource which obtains connections from the specified pool and manages them using the + * specified transaction registry. The TransactionRegistry must be the transaction registry obtained from the + * XAConnectionFactory used to create the connection pool. If not, an error will occur when attempting to use the + * connection in a global transaction because the XAResource object associated with the connection will be + * unavailable. + * + * @param pool + * the connection pool + * @param transactionRegistry + * the transaction registry obtained from the XAConnectionFactory used to create the connection pool + * object factory + */ + public ManagedDataSource(final ObjectPool pool, final TransactionRegistry transactionRegistry) { + super(pool); + this.transactionRegistry = transactionRegistry; + } + + @Override + public Connection getConnection() throws SQLException { + if (getPool() == null) { + throw new IllegalStateException("Pool has not been set"); + } + if (transactionRegistry == null) { + throw new IllegalStateException("TransactionRegistry has not been set"); + } + + return new ManagedConnection<>(getPool(), transactionRegistry, isAccessToUnderlyingConnectionAllowed()); + } + + /** + * Gets the transaction registry. + * + * @return The transaction registry. + * @see #setTransactionRegistry(TransactionRegistry) + * @since 2.6.0 + */ + public TransactionRegistry getTransactionRegistry() { + return transactionRegistry; + } + + /** + * Sets the transaction registry from the XAConnectionFactory used to create the pool. The transaction registry can + * only be set once using either a connector or this setter method. + * + * @param transactionRegistry + * the transaction registry acquired from the XAConnectionFactory used to create the pool + */ + public void setTransactionRegistry(final TransactionRegistry transactionRegistry) { + if (this.transactionRegistry != null) { + throw new IllegalStateException("TransactionRegistry already set"); + } + Objects.requireNonNull(transactionRegistry, "transactionRegistry"); + + this.transactionRegistry = transactionRegistry; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java new file mode 100644 index 0000000..f9958d7 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java @@ -0,0 +1,91 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; + +import org.apache.tomcat.dbcp.dbcp2.PoolableConnection; +import org.apache.tomcat.dbcp.pool2.ObjectPool; + +/** + * PoolableConnection that unregisters from TransactionRegistry on Connection real destroy. + * + * @see PoolableConnection + * @since 2.0 + */ +public class PoolableManagedConnection extends PoolableConnection { + private final TransactionRegistry transactionRegistry; + + /** + * Create a PoolableManagedConnection. + * + * @param transactionRegistry + * transaction registry + * @param conn + * underlying connection + * @param pool + * connection pool + */ + public PoolableManagedConnection(final TransactionRegistry transactionRegistry, final Connection conn, + final ObjectPool pool) { + this(transactionRegistry, conn, pool, null, true); + } + + /** + * Create a PoolableManagedConnection. + * + * @param transactionRegistry + * transaction registry + * @param conn + * underlying connection + * @param pool + * connection pool + * @param disconnectSqlCodes + * SQL_STATE codes considered fatal disconnection errors + * @param fastFailValidation + * true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to + * run query or isValid) + */ + public PoolableManagedConnection(final TransactionRegistry transactionRegistry, final Connection conn, + final ObjectPool pool, final Collection disconnectSqlCodes, + final boolean fastFailValidation) { + super(conn, pool, null, disconnectSqlCodes, fastFailValidation); + this.transactionRegistry = transactionRegistry; + } + + /** + * @return The transaction registry. + * @since 2.6.0 + */ + public TransactionRegistry getTransactionRegistry() { + return transactionRegistry; + } + + /** + * Actually close the underlying connection. + */ + @Override + public void reallyClose() throws SQLException { + try { + super.reallyClose(); + } finally { + transactionRegistry.unregisterConnection(this); + } + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java new file mode 100644 index 0000000..28c2046 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java @@ -0,0 +1,110 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.Connection; +import java.sql.SQLException; +import java.time.Duration; + +import javax.management.ObjectName; + +import org.apache.tomcat.dbcp.dbcp2.Constants; +import org.apache.tomcat.dbcp.dbcp2.DelegatingPreparedStatement; +import org.apache.tomcat.dbcp.dbcp2.PStmtKey; +import org.apache.tomcat.dbcp.dbcp2.PoolableConnection; +import org.apache.tomcat.dbcp.dbcp2.PoolableConnectionFactory; +import org.apache.tomcat.dbcp.dbcp2.PoolingConnection; +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.impl.DefaultPooledObject; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPoolConfig; + +/** + * A {@link PoolableConnectionFactory} that creates {@link PoolableManagedConnection}s. + * + * @since 2.0 + */ +public class PoolableManagedConnectionFactory extends PoolableConnectionFactory { + + /** Transaction registry associated with connections created by this factory */ + private final TransactionRegistry transactionRegistry; + + /** + * Creates a PoolableManagedConnectionFactory and attach it to a connection pool. + * + * @param connFactory + * XAConnectionFactory + * @param dataSourceJmxName + * The data source name. + */ + public PoolableManagedConnectionFactory(final XAConnectionFactory connFactory, final ObjectName dataSourceJmxName) { + super(connFactory, dataSourceJmxName); + this.transactionRegistry = connFactory.getTransactionRegistry(); + } + + /** + * @return The transaction registry. + * @since 2.6.0 + */ + public TransactionRegistry getTransactionRegistry() { + return transactionRegistry; + } + + /** + * Uses the configured XAConnectionFactory to create a {@link PoolableManagedConnection}. Throws + * {@code IllegalStateException} if the connection factory returns null. Also initializes the connection using + * configured initialization SQL (if provided) and sets up a prepared statement pool associated with the + * PoolableManagedConnection if statement pooling is enabled. + */ + @Override + public synchronized PooledObject makeObject() throws SQLException { + Connection conn = getConnectionFactory().createConnection(); + if (conn == null) { + throw new IllegalStateException("Connection factory returned null from createConnection"); + } + initializeConnection(conn); + if (getPoolStatements()) { + conn = new PoolingConnection(conn); + final GenericKeyedObjectPoolConfig config = new GenericKeyedObjectPoolConfig<>(); + config.setMaxTotalPerKey(-1); + config.setBlockWhenExhausted(false); + config.setMaxWait(Duration.ZERO); + config.setMaxIdlePerKey(1); + config.setMaxTotal(getMaxOpenPreparedStatements()); + final ObjectName dataSourceJmxName = getDataSourceJmxName(); + final long connIndex = getConnectionIndex().getAndIncrement(); + if (dataSourceJmxName != null) { + final StringBuilder base = new StringBuilder(dataSourceJmxName.toString()); + base.append(Constants.JMX_CONNECTION_BASE_EXT); + base.append(connIndex); + config.setJmxNameBase(base.toString()); + config.setJmxNamePrefix(Constants.JMX_STATEMENT_POOL_PREFIX); + } else { + config.setJmxEnabled(false); + } + final KeyedObjectPool stmtPool = new GenericKeyedObjectPool<>( + (PoolingConnection) conn, config); + ((PoolingConnection) conn).setStatementPool(stmtPool); + ((PoolingConnection) conn).setCacheState(getCacheState()); + } + final PoolableManagedConnection pmc = new PoolableManagedConnection(transactionRegistry, conn, getPool(), + getDisconnectionSqlCodes(), isFastFailValidation()); + pmc.setCacheState(getCacheState()); + return new DefaultPooledObject<>(pmc); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java new file mode 100644 index 0000000..c7969d2 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import jakarta.transaction.Synchronization; + +/** + * Implements {@link Synchronization} for subclasses. + */ +class SynchronizationAdapter implements Synchronization { + + @Override + public void afterCompletion(final int status) { + // Noop + } + + @Override + public void beforeCompletion() { + // Noop + } + +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java new file mode 100644 index 0000000..1a88234 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java @@ -0,0 +1,205 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.lang.ref.WeakReference; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Objects; + +import javax.transaction.xa.XAResource; + +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.Synchronization; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionSynchronizationRegistry; + +/** + * TransactionContext represents the association between a single XAConnectionFactory and a Transaction. This context + * contains a single shared connection which should be used by all ManagedConnections for the XAConnectionFactory, the + * ability to listen for the transaction completion event, and a method to check the status of the transaction. + * + * @since 2.0 + */ +public class TransactionContext { + private final TransactionRegistry transactionRegistry; + private final WeakReference transactionRef; + private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; + private Connection sharedConnection; + private boolean transactionComplete; + + /** + * Provided for backwards compatibility + * + * @param transactionRegistry the TransactionRegistry used to obtain the XAResource for the + * shared connection + * @param transaction the transaction + */ + public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction) { + this (transactionRegistry, transaction, null); + } + + /** + * Creates a TransactionContext for the specified Transaction and TransactionRegistry. The TransactionRegistry is + * used to obtain the XAResource for the shared connection when it is enlisted in the transaction. + * + * @param transactionRegistry + * the TransactionRegistry used to obtain the XAResource for the shared connection + * @param transaction + * the transaction + * @param transactionSynchronizationRegistry + * The optional TSR to register synchronizations with + * @since 2.6.0 + */ + public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction, + final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { + Objects.requireNonNull(transactionRegistry, "transactionRegistry"); + Objects.requireNonNull(transaction, "transaction"); + this.transactionRegistry = transactionRegistry; + this.transactionRef = new WeakReference<>(transaction); + this.transactionComplete = false; + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + } + + /** + * Adds a listener for transaction completion events. + * + * @param listener + * the listener to add + * @throws SQLException + * if a problem occurs adding the listener to the transaction + */ + public void addTransactionContextListener(final TransactionContextListener listener) throws SQLException { + try { + if (!isActive()) { + final Transaction transaction = this.transactionRef.get(); + listener.afterCompletion(this, transaction != null && transaction.getStatus() == Status.STATUS_COMMITTED); + return; + } + final Synchronization s = new SynchronizationAdapter() { + @Override + public void afterCompletion(final int status) { + listener.afterCompletion(TransactionContext.this, status == Status.STATUS_COMMITTED); + } + }; + if (transactionSynchronizationRegistry != null) { + transactionSynchronizationRegistry.registerInterposedSynchronization(s); + } else { + getTransaction().registerSynchronization(s); + } + } catch (final RollbackException ignored) { + // JTA spec doesn't let us register with a transaction marked rollback only + // just ignore this and the tx state will be cleared another way. + } catch (final Exception e) { + throw new SQLException("Unable to register transaction context listener", e); + } + } + + /** + * Sets the transaction complete flag to true. + * + * @since 2.4.0 + */ + public void completeTransaction() { + this.transactionComplete = true; + } + + /** + * Gets the connection shared by all ManagedConnections in the transaction. Specifically, connection using the same + * XAConnectionFactory from which the TransactionRegistry was obtained. + * + * @return the shared connection for this transaction + */ + public Connection getSharedConnection() { + return sharedConnection; + } + + private Transaction getTransaction() throws SQLException { + final Transaction transaction = this.transactionRef.get(); + if (transaction == null) { + throw new SQLException("Unable to enlist connection because the transaction has been garbage collected"); + } + return transaction; + } + + /** + * True if the transaction is active or marked for rollback only. + * + * @return true if the transaction is active or marked for rollback only; false otherwise + * @throws SQLException + * if a problem occurs obtaining the transaction status + */ + public boolean isActive() throws SQLException { + try { + final Transaction transaction = this.transactionRef.get(); + if (transaction == null) { + return false; + } + final int status = transaction.getStatus(); + return status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK; + } catch (final SystemException e) { + throw new SQLException("Unable to get transaction status", e); + } + } + + /** + * Gets the transaction complete flag to true. + * + * @return The transaction complete flag. + * + * @since 2.4.0 + */ + public boolean isTransactionComplete() { + return this.transactionComplete; + } + + /** + * Sets the shared connection for this transaction. The shared connection is enlisted in the transaction. + * + * @param sharedConnection + * the shared connection + * @throws SQLException + * if a shared connection is already set, if XAResource for the connection could not be found in the + * transaction registry, or if there was a problem enlisting the connection in the transaction + */ + public void setSharedConnection(final Connection sharedConnection) throws SQLException { + if (this.sharedConnection != null) { + throw new IllegalStateException("A shared connection is already set"); + } + + // This is the first use of the connection in this transaction, so we must + // enlist it in the transaction + final Transaction transaction = getTransaction(); + try { + final XAResource xaResource = transactionRegistry.getXAResource(sharedConnection); + if (!transaction.enlistResource(xaResource)) { + throw new SQLException("Unable to enlist connection in transaction: enlistResource returns 'false'."); + } + } catch (final IllegalStateException e) { + // This can happen if the transaction is already timed out + throw new SQLException("Unable to enlist connection in the transaction", e); + } catch (final RollbackException ignored) { + // transaction was rolled back... proceed as if there never was a transaction + } catch (final SystemException e) { + throw new SQLException("Unable to enlist connection the transaction", e); + } + + this.sharedConnection = sharedConnection; + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java new file mode 100644 index 0000000..10dd34a --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java @@ -0,0 +1,34 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +/** + * A listener for transaction completion events. + * + * @since 2.0 + */ +public interface TransactionContextListener { + /** + * Occurs after the transaction commits or rolls back. + * + * @param transactionContext + * the transaction context that completed + * @param committed + * true if the transaction committed; false otherwise + */ + void afterCompletion(TransactionContext transactionContext, boolean committed); +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java new file mode 100644 index 0000000..1d0d163 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java @@ -0,0 +1,153 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; + +import javax.transaction.xa.XAResource; + +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import org.apache.tomcat.dbcp.dbcp2.DelegatingConnection; + +/** + * TransactionRegistry tracks Connections and XAResources in a transacted environment for a single XAConnectionFactory. + *

    + * The TransactionRegistry hides the details of transaction processing from the existing DBCP pooling code, and gives + * the ManagedConnection a way to enlist connections in a transaction, allowing for the maximal rescue of DBCP. + *

    + * + * @since 2.0 + */ +public class TransactionRegistry { + private final TransactionManager transactionManager; + private final Map caches = new WeakHashMap<>(); + private final Map xaResources = new WeakHashMap<>(); + private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; + + /** + * Provided for backwards compatibility + * @param transactionManager the transaction manager used to enlist connections + */ + public TransactionRegistry(final TransactionManager transactionManager) { + this (transactionManager, null); + } + + /** + * Creates a TransactionRegistry for the specified transaction manager. + * + * @param transactionManager + * the transaction manager used to enlist connections. + * @param transactionSynchronizationRegistry + * The optional TSR to register synchronizations with + * @since 2.6.0 + */ + public TransactionRegistry(final TransactionManager transactionManager, final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { + this.transactionManager = transactionManager; + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + } + + /** + * Gets the active TransactionContext or null if not Transaction is active. + * + * @return The active TransactionContext or null if no Transaction is active. + * @throws SQLException + * Thrown when an error occurs while fetching the transaction. + */ + public TransactionContext getActiveTransactionContext() throws SQLException { + Transaction transaction = null; + try { + transaction = transactionManager.getTransaction(); + + // was there a transaction? + if (transaction == null) { + return null; + } + + // This is the transaction on the thread so no need to check it's status - we should try to use it and + // fail later based on the subsequent status + } catch (final SystemException e) { + throw new SQLException("Unable to determine current transaction ", e); + } + + // register the context (or create a new one) + synchronized (this) { + return caches.computeIfAbsent(transaction, k -> new TransactionContext(this, k, transactionSynchronizationRegistry)); + } + } + + private Connection getConnectionKey(final Connection connection) { + final Connection result; + if (connection instanceof DelegatingConnection) { + result = ((DelegatingConnection) connection).getInnermostDelegateInternal(); + } else { + result = connection; + } + return result; + } + + /** + * Gets the XAResource registered for the connection. + * + * @param connection + * the connection + * @return The XAResource registered for the connection; never null. + * @throws SQLException + * Thrown when the connection does not have a registered XAResource. + */ + public synchronized XAResource getXAResource(final Connection connection) throws SQLException { + Objects.requireNonNull(connection, "connection"); + final Connection key = getConnectionKey(connection); + final XAResource xaResource = xaResources.get(key); + if (xaResource == null) { + throw new SQLException("Connection does not have a registered XAResource " + connection); + } + return xaResource; + } + + /** + * Registers the association between a Connection and a XAResource. When a connection is enlisted in a transaction, + * it is actually the XAResource that is given to the transaction manager. + * + * @param connection + * The JDBC connection. + * @param xaResource + * The XAResource which managed the connection within a transaction. + */ + public synchronized void registerConnection(final Connection connection, final XAResource xaResource) { + Objects.requireNonNull(connection, "connection"); + Objects.requireNonNull(xaResource, "xaResource"); + xaResources.put(connection, xaResource); + } + + /** + * Unregisters a destroyed connection from {@link TransactionRegistry}. + * + * @param connection + * A destroyed connection from {@link TransactionRegistry}. + */ + public synchronized void unregisterConnection(final Connection connection) { + xaResources.remove(getConnectionKey(connection)); + } +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java new file mode 100644 index 0000000..a5c5420 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.dbcp.dbcp2.managed; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.apache.tomcat.dbcp.dbcp2.ConnectionFactory; + +/** + * XAConnectionFactory is an extension of ConnectionFactory used to create connections in a transaction managed + * environment. The XAConnectionFactory operates like a normal ConnectionFactory except a TransactionRegistry is + * provided from which the XAResource for a connection can be obtained. This allows the existing DBCP pool code to work + * with XAConnections and gives a the ManagedConnection a way to enlist a connection in the transaction. + * + * @since 2.0 + */ +public interface XAConnectionFactory extends ConnectionFactory { + /** + * Create a new {@link java.sql.Connection} in an implementation specific fashion. + *

    + * An implementation can assume that the caller of this will wrap the connection in a proxy that protects access to + * the setAutoCommit, commit and rollback when enrolled in a XA transaction. + *

    + * + * @return a new {@link java.sql.Connection} + * @throws java.sql.SQLException + * if a database error occurs creating the connection + */ + @Override + Connection createConnection() throws SQLException; + + /** + * Gets the TransactionRegistry for this connection factory which contains a the XAResource for every connection + * created by this factory. + * + * @return the transaction registry for this connection factory + */ + TransactionRegistry getTransactionRegistry(); +} diff --git a/java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java b/java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java new file mode 100644 index 0000000..a439660 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + +/** + *

    + * This package provides support for pooling of ManagedConnections. A managed + * connection is responsible for managing a database connection in a + * transactional environment (typically called Container Managed). + * A managed connection operates like any other connection when no global + * transaction (a.k.a. XA transaction or JTA Transaction) is in progress. + * When a global transaction is active a single physical connection to the + * database is used by all ManagedConnections accessed in the scope of the + * transaction. Connection sharing means that all data access during a + * transaction has a consistent view of the database. When the global + * transaction is committed or rolled back the enlisted connections are + * committed or rolled back. + *

    + * + *

    + * This package supports full XADataSources and non-XA data sources using + * local transaction semantics. non-XA data sources commit and rollback as + * part of the transaction but are not recoverable in the case of an error + * because they do not implement the two-phase commit protocol. + *

    + */ +package org.apache.tomcat.dbcp.dbcp2.managed; diff --git a/java/org/apache/tomcat/dbcp/dbcp2/package-info.java b/java/org/apache/tomcat/dbcp/dbcp2/package-info.java new file mode 100644 index 0000000..d63195f --- /dev/null +++ b/java/org/apache/tomcat/dbcp/dbcp2/package-info.java @@ -0,0 +1,131 @@ +/* + * 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. + */ + +/** + *

    + * Database Connection Pool API. + *

    + * + * Overview in Dialog Form + *

    + * Q: How do I use the DBCP package? + *

    + *

    + * A: There are two primary ways to access the DBCP pool, as a {@link java.sql.Driver Driver}, or as a + * {@link javax.sql.DataSource DataSource}. You'll want to create an instance of + * {@link org.apache.tomcat.dbcp.dbcp2.PoolingDriver} or {@link org.apache.tomcat.dbcp.dbcp2.PoolingDataSource}. When using one + * of these interfaces, you can just use your JDBC objects the way you normally would. Closing a + * {@link java.sql.Connection} will simply return it to its pool. + *

    + *

    + * Q: But {@link org.apache.tomcat.dbcp.dbcp2.PoolingDriver PoolingDriver} and + * {@link org.apache.tomcat.dbcp.dbcp2.PoolingDataSource PoolingDataSource} both expect an + * {@link org.apache.tomcat.dbcp.pool2.ObjectPool ObjectPool} as an input. Where do I get one of those? + *

    + *

    + * A: The {@link org.apache.tomcat.dbcp.pool2.ObjectPool ObjectPool} interface is defined in Commons Pool. You can use one + * of the provided implementations such as {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool GenericObjectPool} or + * {@link org.apache.tomcat.dbcp.pool2.impl.SoftReferenceObjectPool SoftReferenceObjectPool} or you can create your own. + *

    + *

    + * Q: Ok, I've found an {@link org.apache.tomcat.dbcp.pool2.ObjectPool ObjectPool} implementation that I think suits my + * connection pooling needs. But it wants a {@link org.apache.tomcat.dbcp.pool2.PooledObjectFactory PooledObjectFactory}. + * What should I use for that? + *

    + *

    + * A: The DBCP package provides a class for this purpose. It's called + * {@link org.apache.tomcat.dbcp.dbcp2.PoolableConnectionFactory}. It implements the factory and lifecycle methods of + * {@link org.apache.tomcat.dbcp.pool2.PooledObjectFactory} for {@link java.sql.Connection}s. But it doesn't create the + * actual database {@link java.sql.Connection}s itself, it uses a {@link org.apache.tomcat.dbcp.dbcp2.ConnectionFactory} for + * that. The {@link org.apache.tomcat.dbcp.dbcp2.PoolableConnectionFactory} will take {@link java.sql.Connection}s created + * by the {@link org.apache.tomcat.dbcp.dbcp2.ConnectionFactory} and wrap them with classes that implement the pooling + * behaviour. + *

    + *

    + * Several implementations of {@link org.apache.tomcat.dbcp.dbcp2.ConnectionFactory} are provided--one that uses + * {@link java.sql.DriverManager} to create connections + * ({@link org.apache.tomcat.dbcp.dbcp2.DriverManagerConnectionFactory}), one that uses a {@link java.sql.Driver} to create + * connections ({@link org.apache.tomcat.dbcp.dbcp2.DriverConnectionFactory}), one that uses a {@link javax.sql.DataSource} + * to create connections ({@link org.apache.tomcat.dbcp.dbcp2.DataSourceConnectionFactory}). + *

    + *

    + * Q: I think I'm starting to get it, but can you walk me though it again? + *

    + *

    + * A: Sure. Let's assume you want to create a {@link javax.sql.DataSource} that pools {@link java.sql.Connection}s. + * Let's also assume that those pooled {@link java.sql.Connection}s should be obtained from the + * {@link java.sql.DriverManager}. You'll want to create a {@link org.apache.tomcat.dbcp.dbcp2.PoolingDataSource}. + *

    + *

    + * The {@link org.apache.tomcat.dbcp.dbcp2.PoolingDataSource} uses an underlying {@link org.apache.tomcat.dbcp.pool2.ObjectPool} + * to create and store its {@link java.sql.Connection}. + *

    + *

    + * To create a {@link org.apache.tomcat.dbcp.pool2.ObjectPool}, you'll need a + * {@link org.apache.tomcat.dbcp.pool2.PooledObjectFactory} that creates the actual {@link java.sql.Connection}s. That's + * what {@link org.apache.tomcat.dbcp.dbcp2.PoolableConnectionFactory} is for. + *

    + *

    + * To create the {@link org.apache.tomcat.dbcp.dbcp2.PoolableConnectionFactory}, you'll need at least two things: + *

    + *
      + *
    1. A {@link org.apache.tomcat.dbcp.dbcp2.ConnectionFactory} from which the actual database {@link java.sql.Connection}s + * will be obtained.
    2. + *
    3. An empty and factory-less {@link org.apache.tomcat.dbcp.pool2.ObjectPool} in which the {@link java.sql.Connection}s + * will be stored.
      + * When you pass an {@link org.apache.tomcat.dbcp.pool2.ObjectPool} into the + * {@link org.apache.tomcat.dbcp.dbcp2.PoolableConnectionFactory}, it will automatically register itself as the + * {@link org.apache.tomcat.dbcp.pool2.PooledObjectFactory} for that pool.
    4. + *
    + *

    + * In code, that might look like this: + *

    + * + *
    + * GenericObjectPool connectionPool = new GenericObjectPool(null);
    + * ConnectionFactory connectionFactory = new DriverManagerConnectionFactory("jdbc:some:connect:string", "userName",
    + *         "password");
    + * PoolableConnectionFactory poolableConnectionFactory = new PoolableConnectionFactory(connectionFactory,
    + *         connectionPool, null, null, false, true);
    + * PoolingDataSource dataSource = new PoolingDataSource(connectionPool);
    + * 
    + *

    + * To create a {@link org.apache.tomcat.dbcp.dbcp2.PoolingDriver}, we do the same thing, except that instead of creating a + * {@link javax.sql.DataSource} on the last line, we create a {@link org.apache.tomcat.dbcp.dbcp2.PoolingDriver}, and + * register the {@code connectionPool} with it. E.g.,: + *

    + * + *
    + * GenericObjectPool connectionPool = new GenericObjectPool(null);
    + * ConnectionFactory connectionFactory = new DriverManagerConnectionFactory("jdbc:some:connect:string", "userName",
    + *         "password");
    + * PoolableConnectionFactory poolableConnectionFactory = new PoolableConnectionFactory(connectionFactory,
    + *         connectionPool, null, null, false, true);
    + * PoolingDriver driver = new PoolingDriver();
    + * driver.registerPool("example", connectionPool);
    + * 
    + *

    + * Since the {@link org.apache.tomcat.dbcp.dbcp2.PoolingDriver} registers itself with the {@link java.sql.DriverManager} + * when it is created, now you can just go to the {@link java.sql.DriverManager} to create your + * {@link java.sql.Connection}s, like you normally would: + *

    + * + *
    + * Connection conn = DriverManager.getConnection("jdbc:apache:commons:dbcp:example");
    + * 
    + */ +package org.apache.tomcat.dbcp.dbcp2; diff --git a/java/org/apache/tomcat/dbcp/pool2/BaseObject.java b/java/org/apache/tomcat/dbcp/pool2/BaseObject.java new file mode 100644 index 0000000..87c6cce --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/BaseObject.java @@ -0,0 +1,45 @@ +/* + * 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.tomcat.dbcp.pool2; + +/** + * A base class for common functionality. + * + * @since 2.4.3 + */ +public abstract class BaseObject { + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append(" ["); + toStringAppendFields(builder); + builder.append("]"); + return builder.toString(); + } + + /** + * Used by sub-classes to include the fields defined by the sub-class in the + * {@link #toString()} output. + * + * @param builder Field names and values are appended to this object + */ + protected void toStringAppendFields(final StringBuilder builder) { + // do nothing by default, needed for b/w compatibility. + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java b/java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java new file mode 100644 index 0000000..b6108c7 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java @@ -0,0 +1,127 @@ +/* + * 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.tomcat.dbcp.pool2; + +/** + * A simple base implementation of {@link ObjectPool}. + * Optional operations are implemented to either do nothing, return a value + * indicating it is unsupported or throw {@link UnsupportedOperationException}. + *

    + * This class is intended to be thread-safe. + *

    + * + * @param Type of element pooled in this pool. + * + * @since 2.0 + */ +public abstract class BaseObjectPool extends BaseObject implements ObjectPool { + + private volatile boolean closed; + + /** + * Not supported in this base implementation. Subclasses should override + * this behavior. + * + * @throws UnsupportedOperationException if the pool does not implement this + * method + */ + @Override + public void addObject() throws Exception { + throw new UnsupportedOperationException(); + } + + /** + * Throws an {@code IllegalStateException} when this pool has been + * closed. + * + * @throws IllegalStateException when this pool has been closed. + * + * @see #isClosed() + */ + protected final void assertOpen() throws IllegalStateException { + if (isClosed()) { + throw new IllegalStateException("Pool not open"); + } + } + + @Override + public abstract T borrowObject() throws Exception; + + /** + * Not supported in this base implementation. + * + * @throws UnsupportedOperationException if the pool does not implement this + * method + */ + @Override + public void clear() throws Exception { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + *

    + * This affects the behavior of {@code isClosed} and + * {@code assertOpen}. + *

    + */ + @Override + public void close() { + closed = true; + } + + /** + * Not supported in this base implementation. + * + * @return a negative value. + */ + @Override + public int getNumActive() { + return -1; + } + + /** + * Not supported in this base implementation. + * + * @return a negative value. + */ + @Override + public int getNumIdle() { + return -1; + } + + @Override + public abstract void invalidateObject(T obj) throws Exception; + + /** + * Has this pool instance been closed. + * + * @return {@code true} when this pool has been closed. + */ + public final boolean isClosed() { + return closed; + } + + @Override + public abstract void returnObject(T obj) throws Exception; + + @Override + protected void toStringAppendFields(final StringBuilder builder) { + builder.append("closed="); + builder.append(closed); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/DestroyMode.java b/java/org/apache/tomcat/dbcp/pool2/DestroyMode.java new file mode 100644 index 0000000..2b8b683 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/DestroyMode.java @@ -0,0 +1,32 @@ +/* + * 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.tomcat.dbcp.pool2; + +/** + * Destroy context provided to object factories via {@code destroyObject} and {@code invalidateObject} methods. Values + * provide information about why the pool is asking for a pooled object to be destroyed. + * + * @since 2.9.0 + */ +public enum DestroyMode { + + /** Normal destroy. */ + NORMAL, + + /** Destroy abandoned object. */ + ABANDONED +} diff --git a/java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java b/java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java new file mode 100644 index 0000000..e0cf320 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java @@ -0,0 +1,334 @@ +/* + * 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.tomcat.dbcp.pool2; + +import java.io.Closeable; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * A "keyed" pooling interface. + *

    + * A keyed pool maintains a pool of instances for each key value. + *

    + *

    + * Example of use: + *

    + *
     Object obj = null;
    + * Object key = "Key";
    + *
    + * try {
    + *     obj = pool.borrowObject(key);
    + *     //...use the object...
    + * } catch (Exception e) {
    + *     // invalidate the object
    + *     pool.invalidateObject(key, obj);
    + *     // do not return the object to the pool twice
    + *     obj = null;
    + * } finally {
    + *     // make sure the object is returned to the pool
    + *     if (null != obj) {
    + *         pool.returnObject(key, obj);
    + *     }
    + * }
    + *

    + * {@link KeyedObjectPool} implementations may choose to store at most + * one instance per key value, or may choose to maintain a pool of instances + * for each key (essentially creating a {@link java.util.Map Map} of + * {@link ObjectPool pools}). + *

    + *

    + * See {@link org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPool + * GenericKeyedObjectPool} for an implementation. + *

    + * + * @param The type of keys maintained by this pool. + * @param Type of element pooled in this pool. + * + * + * @see KeyedPooledObjectFactory + * @see ObjectPool + * @see org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPool GenericKeyedObjectPool + * + * @since 2.0 + */ +public interface KeyedObjectPool extends Closeable { + + /** + * Creates an object using the {@link KeyedPooledObjectFactory factory} or + * other implementation dependent mechanism, passivate it, and then place it + * in the idle object pool. {@code addObject} is useful for + * "pre-loading" a pool with idle objects (Optional operation). + * + * @param key the key a new instance should be added to + * + * @throws Exception + * when {@link KeyedPooledObjectFactory#makeObject} fails. + * @throws IllegalStateException + * after {@link #close} has been called on this pool. + * @throws UnsupportedOperationException + * when this pool cannot add new idle objects. + */ + void addObject(K key) throws Exception; + + /** + * Calls {@link KeyedObjectPool#addObject(Object)} with each + * key in {@code keys} for {@code count} number of times. This has + * the same effect as calling {@link #addObjects(Object, int)} + * for each key in the {@code keys} collection. + * + * @param keys + * {@link Collection} of keys to add objects for. + * @param count + * the number of idle objects to add for each {@code key}. + * @throws Exception + * when {@link KeyedObjectPool#addObject(Object)} fails. + * @throws IllegalArgumentException + * when {@code keyedPool}, {@code keys}, or any value + * in {@code keys} is {@code null}. + * @see #addObjects(Object, int) + */ + default void addObjects(final Collection keys, final int count) throws Exception { + if (keys == null) { + throw new IllegalArgumentException(PoolUtils.MSG_NULL_KEYS); + } + for (final K key : keys) { + addObjects(key, count); + } + } + + /** + * Calls {@link KeyedObjectPool#addObject(Object)} + * {@code key} {@code count} number of times. + * + * @param key + * the key to add objects for. + * @param count + * the number of idle objects to add for {@code key}. + * @throws Exception + * when {@link KeyedObjectPool#addObject(Object)} fails. + * @throws IllegalArgumentException + * when {@code key} is {@code null}. + * @since 2.8.0 + */ + default void addObjects(final K key, final int count) throws Exception { + if (key == null) { + throw new IllegalArgumentException(PoolUtils.MSG_NULL_KEY); + } + for (int i = 0; i < count; i++) { + addObject(key); + } + } + + /** + * Borrows an instance from this pool for the specified {@code key}. + *

    + * Instances returned from this method will have been either newly created + * with {@link KeyedPooledObjectFactory#makeObject makeObject} or will be + * a previously idle object and have been activated with + * {@link KeyedPooledObjectFactory#activateObject activateObject} and then + * (optionally) validated with + * {@link KeyedPooledObjectFactory#validateObject validateObject}. + *

    + *

    + * By contract, clients must return the borrowed object + * using {@link #returnObject returnObject}, + * {@link #invalidateObject invalidateObject}, or a related method as + * defined in an implementation or sub-interface, using a {@code key} + * that is {@link Object#equals equivalent} to the one used to borrow the + * instance in the first place. + *

    + *

    + * The behavior of this method when the pool has been exhausted is not + * strictly specified (although it may be specified by implementations). + *

    + * + * @param key the key used to obtain the object + * + * @return an instance from this pool. + * + * @throws IllegalStateException + * after {@link #close close} has been called on this pool + * @throws Exception + * when {@link KeyedPooledObjectFactory#makeObject + * makeObject} throws an exception + * @throws NoSuchElementException + * when the pool is exhausted and cannot or will not return + * another instance + */ + V borrowObject(K key) throws Exception; + + /** + * Clears the pool, removing all pooled instances (optional operation). + * + * @throws UnsupportedOperationException when this implementation doesn't + * support the operation + * + * @throws Exception if the pool cannot be cleared + */ + void clear() throws Exception; + + /** + * Clears the specified pool, removing all pooled instances corresponding to + * the given {@code key} (optional operation). + * + * @param key the key to clear + * + * @throws UnsupportedOperationException when this implementation doesn't + * support the operation + * + * @throws Exception if the key cannot be cleared + */ + void clear(K key) throws Exception; + + /** + * Closes this pool, and free any resources associated with it. + *

    + * Calling {@link #addObject addObject} or + * {@link #borrowObject borrowObject} after invoking this method on a pool + * will cause them to throw an {@link IllegalStateException}. + *

    + *

    + * Implementations should silently fail if not all resources can be freed. + *

    + */ + @Override + void close(); + + /** + * Gets a copy of the pool key list. + *

    + * Note: The default implementation returns an empty list. + * Implementations should override this method. + *

    + * + * @return a copy of the pool key list. + * @since 2.12.0 + */ + default List getKeys() { + return Collections.emptyList(); + } + + /** + * Gets the total number of instances currently borrowed from this pool but + * not yet returned. Returns a negative value if this information is not + * available. + * @return the total number of instances currently borrowed from this pool but + * not yet returned. + */ + int getNumActive(); + + /** + * Gets the number of instances currently borrowed from but not yet + * returned to the pool corresponding to the given {@code key}. + * Returns a negative value if this information is not available. + * + * @param key the key to query + * @return the number of instances currently borrowed from but not yet + * returned to the pool corresponding to the given {@code key}. + */ + int getNumActive(K key); + + /** + * Gets the total number of instances currently idle in this pool. + * Returns a negative value if this information is not available. + * @return the total number of instances currently idle in this pool. + */ + int getNumIdle(); + + /** + * Gets the number of instances corresponding to the given + * {@code key} currently idle in this pool. Returns a negative value if + * this information is not available. + * + * @param key the key to query + * @return the number of instances corresponding to the given + * {@code key} currently idle in this pool. + */ + int getNumIdle(K key); + + /** + * Invalidates an object from the pool. + *

    + * By contract, {@code obj} must have been obtained + * using {@link #borrowObject borrowObject} or a related method as defined + * in an implementation or sub-interface using a {@code key} that is + * equivalent to the one used to borrow the {@code Object} in the first + * place. + *

    + *

    + * This method should be used when an object that has been borrowed is + * determined (due to an exception or other problem) to be invalid. + *

    + * + * @param key the key used to obtain the object + * @param obj a {@link #borrowObject borrowed} instance to be returned. + * + * @throws Exception if the instance cannot be invalidated + */ + void invalidateObject(K key, V obj) throws Exception; + + /** + * Invalidates an object from the pool, using the provided + * {@link DestroyMode}. + *

    + * By contract, {@code obj} must have been obtained + * using {@link #borrowObject borrowObject} or a related method as defined + * in an implementation or sub-interface using a {@code key} that is + * equivalent to the one used to borrow the {@code Object} in the first + * place. + *

    + *

    + * This method should be used when an object that has been borrowed is + * determined (due to an exception or other problem) to be invalid. + *

    + * + * @param key the key used to obtain the object + * @param obj a {@link #borrowObject borrowed} instance to be returned. + * @param destroyMode destroy activation context provided to the factory + * + * @throws Exception if the instance cannot be invalidated + * @since 2.9.0 + */ + default void invalidateObject(final K key, final V obj, final DestroyMode destroyMode) throws Exception { + invalidateObject(key, obj); + } + + /** + * Return an instance to the pool. By contract, {@code obj} + * must have been obtained using + * {@link #borrowObject borrowObject} or a related method as defined in an + * implementation or sub-interface using a {@code key} that is + * equivalent to the one used to borrow the instance in the first place. + * + * @param key the key used to obtain the object + * @param obj a {@link #borrowObject borrowed} instance to be returned. + * + * @throws IllegalStateException + * if an attempt is made to return an object to the pool that + * is in any state other than allocated (i.e. borrowed). + * Attempting to return an object more than once or attempting + * to return an object that was never borrowed from the pool + * will trigger this exception. + * + * @throws Exception if an instance cannot be returned to the pool + */ + void returnObject(K key, V obj) throws Exception; +} diff --git a/java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java b/java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java new file mode 100644 index 0000000..35a9842 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java @@ -0,0 +1,172 @@ +/* + * 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.tomcat.dbcp.pool2; + +/** + * An interface defining life-cycle methods for + * instances to be served by a {@link KeyedObjectPool}. + *

    + * By contract, when an {@link KeyedObjectPool} + * delegates to a {@link KeyedPooledObjectFactory}, + *

    + *
      + *
    1. + * {@link #makeObject} is called whenever a new instance is needed. + *
    2. + *
    3. + * {@link #activateObject} is invoked on every instance that has been + * {@link #passivateObject passivated} before it is + * {@link KeyedObjectPool#borrowObject borrowed} from the pool. + *
    4. + *
    5. + * {@link #validateObject} may be invoked on {@link #activateObject activated} + * instances to make sure they can be + * {@link KeyedObjectPool#borrowObject borrowed} from the pool. + * {@code validateObject} may also be used to test an + * instance being {@link KeyedObjectPool#returnObject returned} to the pool + * before it is {@link #passivateObject passivated}. It will only be invoked + * on an activated instance. + *
    6. + *
    7. + * {@link #passivateObject passivateObject} + * is invoked on every instance when it is returned to the pool. + *
    8. + *
    9. + * {@link #destroyObject destroyObject} + * is invoked on every instance when it is being "dropped" from the + * pool (whether due to the response from {@code validateObject}, + * or for reasons specific to the pool implementation.) There is no + * guarantee that the instance being destroyed will + * be considered active, passive or in a generally consistent state. + *
    10. + *
    + * {@link KeyedPooledObjectFactory} must be thread-safe. The only promise + * an {@link KeyedObjectPool} makes is that the same instance of an object will + * not be passed to more than one method of a + * {@code KeyedPooledObjectFactory} at a time. + *

    + * While clients of a {@link KeyedObjectPool} borrow and return instances of + * the underlying value type V, the factory methods act on instances of + * {@link PooledObject PooledObject<V>}. These are the object wrappers that + * pools use to track and maintain state informations about the objects that + * they manage. + *

    + * + * @see KeyedObjectPool + * + * @param The type of keys managed by this factory. + * @param Type of element managed by this factory. + * + * @since 2.0 + */ +public interface KeyedPooledObjectFactory { + + /** + * Reinitializes an instance to be returned by the pool. + * + * @param key the key used when selecting the object + * @param p a {@code PooledObject} wrapping the instance to be activated + * + * @throws Exception if there is a problem activating {@code obj}, + * this exception may be swallowed by the pool. + * + * @see #destroyObject + */ + void activateObject(K key, PooledObject p) throws Exception; + + /** + * Destroys an instance no longer needed by the pool. + *

    + * It is important for implementations of this method to be aware that there + * is no guarantee about what state {@code obj} will be in and the + * implementation should be prepared to handle unexpected errors. + *

    + *

    + * Also, an implementation must take in to consideration that instances lost + * to the garbage collector may never be destroyed. + *

    + * + * @param key the key used when selecting the instance + * @param p a {@code PooledObject} wrapping the instance to be destroyed + * + * @throws Exception should be avoided as it may be swallowed by + * the pool implementation. + * + * @see #validateObject + * @see KeyedObjectPool#invalidateObject + */ + void destroyObject(K key, PooledObject p) throws Exception; + + /** + * Destroys an instance no longer needed by the pool, using the provided {@link DestroyMode}. + * + * @param key the key used when selecting the instance + * @param p a {@code PooledObject} wrapping the instance to be destroyed + * @param destroyMode DestroyMode providing context to the factory + * + * @throws Exception should be avoided as it may be swallowed by + * the pool implementation. + * + * @see #validateObject + * @see KeyedObjectPool#invalidateObject + * @see #destroyObject(Object, PooledObject) + * @see DestroyMode + * @since 2.9.0 + */ + default void destroyObject(final K key, final PooledObject p, final DestroyMode destroyMode) throws Exception { + destroyObject(key, p); + } + + /** + * Creates an instance that can be served by the pool and + * wrap it in a {@link PooledObject} to be managed by the pool. + * + * @param key the key used when constructing the object + * + * @return a {@code PooledObject} wrapping an instance that can + * be served by the pool. + * + * @throws Exception if there is a problem creating a new instance, + * this will be propagated to the code requesting an object. + */ + PooledObject makeObject(K key) throws Exception; + + /** + * Uninitializes an instance to be returned to the idle object pool. + * + * @param key the key used when selecting the object + * @param p a {@code PooledObject} wrapping the instance to be passivated + * + * @throws Exception if there is a problem passivating {@code obj}, + * this exception may be swallowed by the pool. + * + * @see #destroyObject + */ + void passivateObject(K key, PooledObject p) throws Exception; + + /** + * Ensures that the instance is safe to be returned by the pool. + * + * @param key the key used when selecting the object + * @param p a {@code PooledObject} wrapping the instance to be validated + * + * @return {@code false} if {@code obj} is not valid and should + * be dropped from the pool, {@code true} otherwise. + */ + boolean validateObject(K key, PooledObject p); +} + diff --git a/java/org/apache/tomcat/dbcp/pool2/ObjectPool.java b/java/org/apache/tomcat/dbcp/pool2/ObjectPool.java new file mode 100644 index 0000000..32b503f --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/ObjectPool.java @@ -0,0 +1,224 @@ +/* + * 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.tomcat.dbcp.pool2; + +import java.io.Closeable; +import java.util.NoSuchElementException; + +/** + * A pooling simple interface. + *

    + * Example of use: + *

    + *
     Object obj = null;
    + *
    + * try {
    + *     obj = pool.borrowObject();
    + *     try {
    + *         //...use the object...
    + *     } catch (Exception e) {
    + *         // invalidate the object
    + *         pool.invalidateObject(obj);
    + *         // do not return the object to the pool twice
    + *         obj = null;
    + *     } finally {
    + *         // make sure the object is returned to the pool
    + *         if (null != obj) {
    + *             pool.returnObject(obj);
    + *        }
    + *     }
    + * } catch(Exception e) {
    + *       // failed to borrow an object
    + * }
    + *

    + * See {@link BaseObjectPool} for a simple base implementation. + *

    + * + * @param Type of element pooled in this pool. + * + * @see PooledObjectFactory + * @see KeyedObjectPool + * @see BaseObjectPool + * + * @since 2.0 + */ +public interface ObjectPool extends Closeable { + + /** + * Creates an object using the {@link PooledObjectFactory factory} or other + * implementation dependent mechanism, passivate it, and then place it in + * the idle object pool. {@code addObject} is useful for "pre-loading" + * a pool with idle objects. (Optional operation). + * + * @throws Exception + * when {@link PooledObjectFactory#makeObject} fails. + * @throws IllegalStateException + * after {@link #close} has been called on this pool. + * @throws UnsupportedOperationException + * when this pool cannot add new idle objects. + */ + void addObject() throws Exception; + + /** + * Calls {@link ObjectPool#addObject()} {@code count} + * number of times. + * + * @param count + * the number of idle objects to add. + * @throws Exception See {@link ObjectPool#addObject()}. + * @since 2.8.0 + */ + default void addObjects(final int count) throws Exception { + for (int i = 0; i < count; i++) { + addObject(); + } + } + + /** + * Borrows an instance from this pool. + *

    + * Instances returned from this method will have been either newly created + * with {@link PooledObjectFactory#makeObject} or will be a previously + * idle object and have been activated with + * {@link PooledObjectFactory#activateObject} and then validated with + * {@link PooledObjectFactory#validateObject}. + *

    + *

    + * By contract, clients must return the borrowed instance + * using {@link #returnObject}, {@link #invalidateObject}, or a related + * method as defined in an implementation or sub-interface. + *

    + *

    + * The behavior of this method when the pool has been exhausted + * is not strictly specified (although it may be specified by + * implementations). + *

    + * + * @return an instance from this pool. + * + * @throws IllegalStateException + * after {@link #close close} has been called on this pool. + * @throws Exception + * when {@link PooledObjectFactory#makeObject} throws an + * exception. + * @throws NoSuchElementException + * when the pool is exhausted and cannot or will not return + * another instance. + */ + T borrowObject() throws Exception; + + /** + * Clears any objects sitting idle in the pool, releasing any associated + * resources (optional operation). Idle objects cleared must be + * {@link PooledObjectFactory#destroyObject(PooledObject)}. + * + * @throws UnsupportedOperationException + * if this implementation does not support the operation + * + * @throws Exception if the pool cannot be cleared + */ + void clear() throws Exception; + + /** + * Closes this pool, and free any resources associated with it. + *

    + * Calling {@link #addObject} or {@link #borrowObject} after invoking this + * method on a pool will cause them to throw an {@link IllegalStateException}. + *

    + *

    + * Implementations should silently fail if not all resources can be freed. + *

    + */ + @Override + void close(); + + /** + * Gets the number of instances currently borrowed from this pool. Returns + * a negative value if this information is not available. + * @return the number of instances currently borrowed from this pool. + */ + int getNumActive(); + + /** + * Gets the number of instances currently idle in this pool. This may be + * considered an approximation of the number of objects that can be + * {@link #borrowObject borrowed} without creating any new instances. + * Returns a negative value if this information is not available. + * @return the number of instances currently idle in this pool. + */ + int getNumIdle(); + + /** + * Invalidates an object from the pool. + *

    + * By contract, {@code obj} must have been obtained + * using {@link #borrowObject} or a related method as defined in an + * implementation or sub-interface. + *

    + *

    + * This method should be used when an object that has been borrowed is + * determined (due to an exception or other problem) to be invalid. + *

    + * + * @param obj a {@link #borrowObject borrowed} instance to be disposed. + * + * @throws Exception if the instance cannot be invalidated + */ + void invalidateObject(T obj) throws Exception; + + /** + * Invalidates an object from the pool, using the provided + * {@link DestroyMode} + *

    + * By contract, {@code obj} must have been obtained + * using {@link #borrowObject} or a related method as defined in an + * implementation or sub-interface. + *

    + *

    + * This method should be used when an object that has been borrowed is + * determined (due to an exception or other problem) to be invalid. + *

    + * + * @param obj a {@link #borrowObject borrowed} instance to be disposed. + * @param destroyMode destroy activation context provided to the factory + * @throws Exception if the instance cannot be invalidated + * @since 2.9.0 + */ + default void invalidateObject(final T obj, final DestroyMode destroyMode) throws Exception { + invalidateObject(obj); + } + + /** + * Returns an instance to the pool. By contract, {@code obj} + * must have been obtained using {@link #borrowObject()} or + * a related method as defined in an implementation or sub-interface. + * + * @param obj a {@link #borrowObject borrowed} instance to be returned. + * + * @throws IllegalStateException + * if an attempt is made to return an object to the pool that + * is in any state other than allocated (i.e. borrowed). + * Attempting to return an object more than once or attempting + * to return an object that was never borrowed from the pool + * will trigger this exception. + * + * @throws Exception if an instance cannot be returned to the pool + */ + void returnObject(T obj) throws Exception; + +} diff --git a/java/org/apache/tomcat/dbcp/pool2/PoolUtils.java b/java/org/apache/tomcat/dbcp/pool2/PoolUtils.java new file mode 100644 index 0000000..7c3f35d --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/PoolUtils.java @@ -0,0 +1,1848 @@ +/* + * 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.tomcat.dbcp.pool2; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +/** + * This class consists exclusively of static methods that operate on or return + * ObjectPool or KeyedObjectPool related interfaces. + * + * @since 2.0 + */ +public final class PoolUtils { + + /** + * Encapsulate the logic for when the next poolable object should be + * discarded. Each time update is called, the next time to shrink is + * recomputed, based on the float factor, number of idle instances in the + * pool and high water mark. Float factor is assumed to be between 0 and 1. + * Values closer to 1 cause less frequent erosion events. Erosion event + * timing also depends on numIdle. When this value is relatively high (close + * to previously established high water mark), erosion occurs more + * frequently. + */ + private static final class ErodingFactor { + /** Determines frequency of "erosion" events */ + private final float factor; + + /** Time of next shrink event */ + private transient volatile long nextShrinkMillis; + + /** High water mark - largest numIdle encountered */ + private transient volatile int idleHighWaterMark; + + /** + * Creates a new ErodingFactor with the given erosion factor. + * + * @param factor + * erosion factor + */ + ErodingFactor(final float factor) { + this.factor = factor; + nextShrinkMillis = System.currentTimeMillis() + (long) (900000 * factor); // now + 15 min * factor + idleHighWaterMark = 1; + } + + /** + * Gets the time of the next erosion event. + * + * @return next shrink time + */ + public long getNextShrink() { + return nextShrinkMillis; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "ErodingFactor{" + "factor=" + factor + + ", idleHighWaterMark=" + idleHighWaterMark + '}'; + } + + /** + * Updates internal state using the supplied time and numIdle. + * + * @param nowMillis + * current time + * @param numIdle + * number of idle elements in the pool + */ + public void update(final long nowMillis, final int numIdle) { + final int idle = Math.max(0, numIdle); + idleHighWaterMark = Math.max(idle, idleHighWaterMark); + final float maxInterval = 15f; + final float minutes = maxInterval + + (1f - maxInterval) / idleHighWaterMark * idle; + nextShrinkMillis = nowMillis + (long) (minutes * 60000f * factor); + } + } + /** + * Decorates a keyed object pool, adding "eroding" behavior. Based on the + * configured erosion factor, objects returning to the pool + * may be invalidated instead of being added to idle capacity. + * + * @param object pool key type + * @param object pool value type + */ + private static class ErodingKeyedObjectPool implements KeyedObjectPool { + + /** Underlying pool */ + private final KeyedObjectPool keyedPool; + + /** Erosion factor */ + private final ErodingFactor erodingFactor; + + /** + * Creates an ErodingObjectPool wrapping the given pool using the + * specified erosion factor. + * + * @param keyedPool + * underlying pool - must not be null + * @param erodingFactor + * erosion factor - determines the frequency of erosion + * events + * @see #erodingFactor + */ + protected ErodingKeyedObjectPool(final KeyedObjectPool keyedPool, + final ErodingFactor erodingFactor) { + if (keyedPool == null) { + throw new IllegalArgumentException( + MSG_NULL_KEYED_POOL); + } + this.keyedPool = keyedPool; + this.erodingFactor = erodingFactor; + } + + /** + * Creates an ErodingObjectPool wrapping the given pool using the + * specified erosion factor. + * + * @param keyedPool + * underlying pool + * @param factor + * erosion factor - determines the frequency of erosion + * events + * @see #erodingFactor + */ + ErodingKeyedObjectPool(final KeyedObjectPool keyedPool, + final float factor) { + this(keyedPool, new ErodingFactor(factor)); + } + + /** + * {@inheritDoc} + */ + @Override + public void addObject(final K key) throws Exception { + keyedPool.addObject(key); + } + + /** + * {@inheritDoc} + */ + @Override + public V borrowObject(final K key) throws Exception { + return keyedPool.borrowObject(key); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() throws Exception { + keyedPool.clear(); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear(final K key) throws Exception { + keyedPool.clear(key); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + try { + keyedPool.close(); + } catch (final Exception ignored) { + // ignored + } + } + + /** + * Gets the eroding factor for the given key + * + * @param key + * key + * @return eroding factor for the given keyed pool + */ + protected ErodingFactor getErodingFactor(final K key) { + return erodingFactor; + } + + /** + * Gets the underlying pool + * + * @return the keyed pool that this ErodingKeyedObjectPool wraps + */ + protected KeyedObjectPool getKeyedPool() { + return keyedPool; + } + + /** + * {@inheritDoc} + */ + @Override + public List getKeys() { + return keyedPool.getKeys(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumActive() { + return keyedPool.getNumActive(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumActive(final K key) { + return keyedPool.getNumActive(key); + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumIdle() { + return keyedPool.getNumIdle(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumIdle(final K key) { + return keyedPool.getNumIdle(key); + } + + /** + * {@inheritDoc} + */ + @Override + public void invalidateObject(final K key, final V obj) { + try { + keyedPool.invalidateObject(key, obj); + } catch (final Exception ignored) { + // ignored + } + } + + /** + * Returns obj to the pool, unless erosion is triggered, in which case + * obj is invalidated. Erosion is triggered when there are idle + * instances in the pool associated with the given key and more than the + * configured {@link #erodingFactor erosion factor} time has elapsed + * since the last returnObject activation. + * + * @param obj + * object to return or invalidate + * @param key + * key + * @see #erodingFactor + */ + @Override + public void returnObject(final K key, final V obj) throws Exception { + boolean discard = false; + final long nowMillis = System.currentTimeMillis(); + final ErodingFactor factor = getErodingFactor(key); + synchronized (keyedPool) { + if (factor.getNextShrink() < nowMillis) { + final int numIdle = getNumIdle(key); + if (numIdle > 0) { + discard = true; + } + + factor.update(nowMillis, numIdle); + } + } + try { + if (discard) { + keyedPool.invalidateObject(key, obj); + } else { + keyedPool.returnObject(key, obj); + } + } catch (final Exception ignored) { + // ignored + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "ErodingKeyedObjectPool{" + "factor=" + + erodingFactor + ", keyedPool=" + keyedPool + '}'; + } + } + + /** + * Decorates an object pool, adding "eroding" behavior. Based on the + * configured {@link #factor erosion factor}, objects returning to the pool + * may be invalidated instead of being added to idle capacity. + * + * @param type of objects in the pool + * + */ + private static class ErodingObjectPool implements ObjectPool { + + /** Underlying object pool */ + private final ObjectPool pool; + + /** Erosion factor */ + private final ErodingFactor factor; + + /** + * Creates an ErodingObjectPool wrapping the given pool using the + * specified erosion factor. + * + * @param pool + * underlying pool + * @param factor + * erosion factor - determines the frequency of erosion + * events + * @see #factor + */ + ErodingObjectPool(final ObjectPool pool, final float factor) { + this.pool = pool; + this.factor = new ErodingFactor(factor); + } + + /** + * {@inheritDoc} + */ + @Override + public void addObject() throws Exception{ + pool.addObject(); + } + + /** + * {@inheritDoc} + */ + @Override + public T borrowObject() throws Exception { + return pool.borrowObject(); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() throws Exception { + pool.clear(); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + try { + pool.close(); + } catch (final Exception ignored) { + // ignored + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumActive() { + return pool.getNumActive(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumIdle() { + return pool.getNumIdle(); + } + + /** + * {@inheritDoc} + */ + @Override + public void invalidateObject(final T obj) { + try { + pool.invalidateObject(obj); + } catch (final Exception ignored) { + // ignored + } + } + + /** + * Returns * Gets obj to the pool, unless erosion is triggered, in which case + * obj is invalidated. Erosion is triggered when there are idle + * instances in the pool and more than the {@link #factor erosion + * factor}-determined time has elapsed since the last returnObject + * activation. + * + * @param obj + * object to return or invalidate + * @see #factor + */ + @Override + public void returnObject(final T obj) { + boolean discard = false; + final long nowMillis = System.currentTimeMillis(); + synchronized (pool) { + if (factor.getNextShrink() < nowMillis) { // XXX: Pool 3: move test + // out of sync block + final int numIdle = pool.getNumIdle(); + if (numIdle > 0) { + discard = true; + } + + factor.update(nowMillis, numIdle); + } + } + try { + if (discard) { + pool.invalidateObject(obj); + } else { + pool.returnObject(obj); + } + } catch (final Exception ignored) { + // ignored + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "ErodingObjectPool{" + "factor=" + factor + ", pool=" + + pool + '}'; + } + } + /** + * Extends ErodingKeyedObjectPool to allow erosion to take place on a + * per-key basis. Timing of erosion events is tracked separately for + * separate keyed pools. + * + * @param object pool key type + * @param object pool value type + */ + private static final class ErodingPerKeyKeyedObjectPool extends ErodingKeyedObjectPool { + + /** Erosion factor - same for all pools */ + private final float factor; + + /** Map of ErodingFactor instances keyed on pool keys */ + private final Map factors = Collections.synchronizedMap(new HashMap<>()); + + /** + * Creates a new ErordingPerKeyKeyedObjectPool decorating the given keyed + * pool with the specified erosion factor. + * + * @param keyedPool + * underlying keyed pool + * @param factor + * erosion factor + */ + ErodingPerKeyKeyedObjectPool(final KeyedObjectPool keyedPool, final float factor) { + super(keyedPool, null); + this.factor = factor; + } + + /** + * {@inheritDoc} + */ + @Override + protected ErodingFactor getErodingFactor(final K key) { + // This may result in two ErodingFactors being created for a key + // since they are small and cheap this is okay. + return factors.computeIfAbsent(key, k -> new ErodingFactor(this.factor)); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "ErodingPerKeyKeyedObjectPool{" + "factor=" + factor + + ", keyedPool=" + getKeyedPool() + '}'; + } + } + /** + * Timer task that adds objects to the pool until the number of idle + * instances for the given key reaches the configured minIdle. Note that + * this is not the same as the pool's minIdle setting. + * + * @param object pool key type + * @param object pool value type + */ + private static final class KeyedObjectPoolMinIdleTimerTask extends TimerTask { + + /** Minimum number of idle instances. Not the same as pool.getMinIdle(). */ + private final int minIdle; + + /** Key to ensure minIdle for */ + private final K key; + + /** Keyed object pool */ + private final KeyedObjectPool keyedPool; + + /** + * Creates a new KeyedObjecPoolMinIdleTimerTask. + * + * @param keyedPool + * keyed object pool + * @param key + * key to ensure minimum number of idle instances + * @param minIdle + * minimum number of idle instances + * @throws IllegalArgumentException + * if the key is null + */ + KeyedObjectPoolMinIdleTimerTask(final KeyedObjectPool keyedPool, + final K key, final int minIdle) throws IllegalArgumentException { + if (keyedPool == null) { + throw new IllegalArgumentException( + MSG_NULL_KEYED_POOL); + } + this.keyedPool = keyedPool; + this.key = key; + this.minIdle = minIdle; + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + boolean success = false; + try { + if (keyedPool.getNumIdle(key) < minIdle) { + keyedPool.addObject(key); + } + success = true; + + } catch (final Exception e) { + cancel(); + + } finally { + // detect other types of Throwable and cancel this Timer + if (!success) { + cancel(); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("KeyedObjectPoolMinIdleTimerTask"); + sb.append("{minIdle=").append(minIdle); + sb.append(", key=").append(key); + sb.append(", keyedPool=").append(keyedPool); + sb.append('}'); + return sb.toString(); + } + } + /** + * Timer task that adds objects to the pool until the number of idle + * instances reaches the configured minIdle. Note that this is not the same + * as the pool's minIdle setting. + * + * @param type of objects in the pool + * + */ + private static final class ObjectPoolMinIdleTimerTask extends TimerTask { + + /** Minimum number of idle instances. Not the same as pool.getMinIdle(). */ + private final int minIdle; + + /** Object pool */ + private final ObjectPool pool; + + /** + * Constructs a new ObjectPoolMinIdleTimerTask for the given pool with the + * given minIdle setting. + * + * @param pool + * object pool + * @param minIdle + * number of idle instances to maintain + * @throws IllegalArgumentException + * if the pool is null + */ + ObjectPoolMinIdleTimerTask(final ObjectPool pool, final int minIdle) + throws IllegalArgumentException { + if (pool == null) { + throw new IllegalArgumentException(MSG_NULL_POOL); + } + this.pool = pool; + this.minIdle = minIdle; + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + boolean success = false; + try { + if (pool.getNumIdle() < minIdle) { + pool.addObject(); + } + success = true; + + } catch (final Exception e) { + cancel(); + } finally { + // detect other types of Throwable and cancel this Timer + if (!success) { + cancel(); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("ObjectPoolMinIdleTimerTask"); + sb.append("{minIdle=").append(minIdle); + sb.append(", pool=").append(pool); + sb.append('}'); + return sb.toString(); + } + } + + /** + * A synchronized (thread-safe) KeyedObjectPool backed by the specified + * KeyedObjectPool. + *

    + * Note: This should not be used on pool implementations that already + * provide proper synchronization such as the pools provided in the Commons + * Pool library. Wrapping a pool that {@link #wait() waits} for poolable + * objects to be returned before allowing another one to be borrowed with + * another layer of synchronization will cause liveliness issues or a + * deadlock. + *

    + * + * @param object pool key type + * @param object pool value type + */ + static final class SynchronizedKeyedObjectPool implements KeyedObjectPool { + + /** + * Object whose monitor is used to synchronize methods on the wrapped + * pool. + */ + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + /** Underlying object pool */ + private final KeyedObjectPool keyedPool; + + /** + * Creates a new SynchronizedKeyedObjectPool wrapping the given pool + * + * @param keyedPool + * KeyedObjectPool to wrap + * @throws IllegalArgumentException + * if keyedPool is null + */ + SynchronizedKeyedObjectPool(final KeyedObjectPool keyedPool) + throws IllegalArgumentException { + if (keyedPool == null) { + throw new IllegalArgumentException( + MSG_NULL_KEYED_POOL); + } + this.keyedPool = keyedPool; + } + + /** + * {@inheritDoc} + */ + @Override + public void addObject(final K key) throws Exception { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + keyedPool.addObject(key); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public V borrowObject(final K key) throws Exception { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + return keyedPool.borrowObject(key); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() throws Exception { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + keyedPool.clear(); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clear(final K key) throws Exception { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + keyedPool.clear(key); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + keyedPool.close(); + } catch (final Exception ignored) { + // ignored as of Pool 2 + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public List getKeys() { + final ReadLock readLock = readWriteLock.readLock(); + readLock.lock(); + try { + return keyedPool.getKeys(); + } finally { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumActive() { + final ReadLock readLock = readWriteLock.readLock(); + readLock.lock(); + try { + return keyedPool.getNumActive(); + } finally { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumActive(final K key) { + final ReadLock readLock = readWriteLock.readLock(); + readLock.lock(); + try { + return keyedPool.getNumActive(key); + } finally { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumIdle() { + final ReadLock readLock = readWriteLock.readLock(); + readLock.lock(); + try { + return keyedPool.getNumIdle(); + } finally { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumIdle(final K key) { + final ReadLock readLock = readWriteLock.readLock(); + readLock.lock(); + try { + return keyedPool.getNumIdle(key); + } finally { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void invalidateObject(final K key, final V obj) { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + keyedPool.invalidateObject(key, obj); + } catch (final Exception ignored) { + // ignored as of Pool 2 + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void returnObject(final K key, final V obj) { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + keyedPool.returnObject(key, obj); + } catch (final Exception ignored) { + // ignored + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("SynchronizedKeyedObjectPool"); + sb.append("{keyedPool=").append(keyedPool); + sb.append('}'); + return sb.toString(); + } + } + + /** + * A fully synchronized KeyedPooledObjectFactory that wraps a + * KeyedPooledObjectFactory and synchronizes access to the wrapped factory + * methods. + *

    + * Note: This should not be used on pool implementations that already + * provide proper synchronization such as the pools provided in the Commons + * Pool library. + *

    + * + * @param pooled object factory key type + * @param pooled object factory value type + */ + private static final class SynchronizedKeyedPooledObjectFactory implements KeyedPooledObjectFactory { + + /** Synchronization lock */ + private final WriteLock writeLock = new ReentrantReadWriteLock().writeLock(); + + /** Wrapped factory */ + private final KeyedPooledObjectFactory keyedFactory; + + /** + * Creates a SynchronizedKeyedPooledObjectFactory wrapping the given + * factory. + * + * @param keyedFactory + * underlying factory to wrap + * @throws IllegalArgumentException + * if the factory is null + */ + SynchronizedKeyedPooledObjectFactory(final KeyedPooledObjectFactory keyedFactory) throws IllegalArgumentException { + if (keyedFactory == null) { + throw new IllegalArgumentException( + "keyedFactory must not be null."); + } + this.keyedFactory = keyedFactory; + } + + /** + * {@inheritDoc} + */ + @Override + public void activateObject(final K key, final PooledObject p) throws Exception { + writeLock.lock(); + try { + keyedFactory.activateObject(key, p); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void destroyObject(final K key, final PooledObject p) throws Exception { + writeLock.lock(); + try { + keyedFactory.destroyObject(key, p); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public PooledObject makeObject(final K key) throws Exception { + writeLock.lock(); + try { + return keyedFactory.makeObject(key); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void passivateObject(final K key, final PooledObject p) throws Exception { + writeLock.lock(); + try { + keyedFactory.passivateObject(key, p); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("SynchronizedKeyedPooledObjectFactory"); + sb.append("{keyedFactory=").append(keyedFactory); + sb.append('}'); + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean validateObject(final K key, final PooledObject p) { + writeLock.lock(); + try { + return keyedFactory.validateObject(key, p); + } finally { + writeLock.unlock(); + } + } + } + + /** + * A synchronized (thread-safe) ObjectPool backed by the specified + * ObjectPool. + *

    + * Note: This should not be used on pool implementations that already + * provide proper synchronization such as the pools provided in the Commons + * Pool library. Wrapping a pool that {@link #wait() waits} for poolable + * objects to be returned before allowing another one to be borrowed with + * another layer of synchronization will cause liveliness issues or a + * deadlock. + *

    + * + * @param type of objects in the pool + * + */ + private static final class SynchronizedObjectPool implements ObjectPool { + + /** + * Object whose monitor is used to synchronize methods on the wrapped + * pool. + */ + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + /** the underlying object pool */ + private final ObjectPool pool; + + /** + * Creates a new SynchronizedObjectPool wrapping the given pool. + * + * @param pool + * the ObjectPool to be "wrapped" in a synchronized + * ObjectPool. + * @throws IllegalArgumentException + * if the pool is null + */ + SynchronizedObjectPool(final ObjectPool pool) + throws IllegalArgumentException { + if (pool == null) { + throw new IllegalArgumentException(MSG_NULL_POOL); + } + this.pool = pool; + } + + /** + * {@inheritDoc} + */ + @Override + public void addObject() throws Exception { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + pool.addObject(); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public T borrowObject() throws Exception { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + return pool.borrowObject(); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() throws Exception { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + pool.clear(); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + pool.close(); + } catch (final Exception ignored) { + // ignored as of Pool 2 + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumActive() { + final ReadLock readLock = readWriteLock.readLock(); + readLock.lock(); + try { + return pool.getNumActive(); + } finally { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumIdle() { + final ReadLock readLock = readWriteLock.readLock(); + readLock.lock(); + try { + return pool.getNumIdle(); + } finally { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void invalidateObject(final T obj) { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + pool.invalidateObject(obj); + } catch (final Exception ignored) { + // ignored as of Pool 2 + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void returnObject(final T obj) { + final WriteLock writeLock = readWriteLock.writeLock(); + writeLock.lock(); + try { + pool.returnObject(obj); + } catch (final Exception ignored) { + // ignored as of Pool 2 + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("SynchronizedObjectPool"); + sb.append("{pool=").append(pool); + sb.append('}'); + return sb.toString(); + } + } + + /** + * A fully synchronized PooledObjectFactory that wraps a + * PooledObjectFactory and synchronizes access to the wrapped factory + * methods. + *

    + * Note: This should not be used on pool implementations that already + * provide proper synchronization such as the pools provided in the Commons + * Pool library. + *

    + * + * @param pooled object factory type + */ + private static final class SynchronizedPooledObjectFactory implements + PooledObjectFactory { + + /** Synchronization lock */ + private final WriteLock writeLock = new ReentrantReadWriteLock().writeLock(); + + /** Wrapped factory */ + private final PooledObjectFactory factory; + + /** + * Creates a SynchronizedPoolableObjectFactory wrapping the given + * factory. + * + * @param factory + * underlying factory to wrap + * @throws IllegalArgumentException + * if the factory is null + */ + SynchronizedPooledObjectFactory(final PooledObjectFactory factory) + throws IllegalArgumentException { + if (factory == null) { + throw new IllegalArgumentException("factory must not be null."); + } + this.factory = factory; + } + + /** + * {@inheritDoc} + */ + @Override + public void activateObject(final PooledObject p) throws Exception { + writeLock.lock(); + try { + factory.activateObject(p); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void destroyObject(final PooledObject p) throws Exception { + writeLock.lock(); + try { + factory.destroyObject(p); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public PooledObject makeObject() throws Exception { + writeLock.lock(); + try { + return factory.makeObject(); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void passivateObject(final PooledObject p) throws Exception { + writeLock.lock(); + try { + factory.passivateObject(p); + } finally { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("SynchronizedPoolableObjectFactory"); + sb.append("{factory=").append(factory); + sb.append('}'); + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean validateObject(final PooledObject p) { + writeLock.lock(); + try { + return factory.validateObject(p); + } finally { + writeLock.unlock(); + } + } + } + + /** + * Timer used to periodically check pools idle object count. Because a + * {@link Timer} creates a {@link Thread}, an IODH is used. + */ + static class TimerHolder { + static final Timer MIN_IDLE_TIMER = new Timer(true); + } + + private static final String MSG_FACTOR_NEGATIVE = "factor must be positive."; + + private static final String MSG_MIN_IDLE = "minIdle must be non-negative."; + + static final String MSG_NULL_KEY = "key must not be null."; + + private static final String MSG_NULL_KEYED_POOL = "keyedPool must not be null."; + + static final String MSG_NULL_KEYS = "keys must not be null."; + + private static final String MSG_NULL_POOL = "pool must not be null."; + + /** + * Periodically check the idle object count for each key in the + * {@code Collection keys} in the keyedPool. At most one idle object will be + * added per period. + * + * @param keyedPool + * the keyedPool to check periodically. + * @param keys + * a collection of keys to check the idle object count. + * @param minIdle + * if the {@link KeyedObjectPool#getNumIdle(Object)} is less than + * this then add an idle object. + * @param periodMillis + * the frequency in milliseconds to check the number of idle objects in a + * keyedPool, see {@link Timer#schedule(TimerTask, long, long)}. + * @param the type of the pool key + * @param the type of pool entries + * + * @return a {@link Map} of key and {@link TimerTask} pairs that will + * periodically check the pools idle object count. + * @throws IllegalArgumentException + * when {@code keyedPool}, {@code keys}, or any of the values in + * the collection is {@code null} or when {@code minIdle} is + * negative or when {@code period} isn't valid for + * {@link Timer#schedule(TimerTask, long, long)}. + * @see #checkMinIdle(KeyedObjectPool, Object, int, long) + */ + public static Map checkMinIdle( + final KeyedObjectPool keyedPool, final Collection keys, + final int minIdle, final long periodMillis) + throws IllegalArgumentException { + if (keys == null) { + throw new IllegalArgumentException(MSG_NULL_KEYS); + } + final Map tasks = new HashMap<>(keys.size()); + for (K key : keys) { + final TimerTask task = checkMinIdle(keyedPool, key, minIdle, periodMillis); + tasks.put(key, task); + } + return tasks; + } + + /** + * Periodically check the idle object count for the key in the keyedPool. At + * most one idle object will be added per period. If there is an exception + * when calling {@link KeyedObjectPool#addObject(Object)} then no more + * checks for that key will be performed. + * + * @param keyedPool + * the keyedPool to check periodically. + * @param key + * the key to check the idle count of. + * @param minIdle + * if the {@link KeyedObjectPool#getNumIdle(Object)} is less than + * this then add an idle object. + * @param periodMillis + * the frequency in milliseconds to check the number of idle objects in a + * keyedPool, see {@link Timer#schedule(TimerTask, long, long)}. + * @param the type of the pool key + * @param the type of pool entries + * + * @return the {@link TimerTask} that will periodically check the pools idle + * object count. + * @throws IllegalArgumentException + * when {@code keyedPool}, {@code key} is {@code null} or + * when {@code minIdle} is negative or when {@code period} isn't + * valid for {@link Timer#schedule(TimerTask, long, long)}. + */ + public static TimerTask checkMinIdle( + final KeyedObjectPool keyedPool, final K key, + final int minIdle, final long periodMillis) + throws IllegalArgumentException { + if (keyedPool == null) { + throw new IllegalArgumentException(MSG_NULL_KEYED_POOL); + } + if (key == null) { + throw new IllegalArgumentException(MSG_NULL_KEY); + } + if (minIdle < 0) { + throw new IllegalArgumentException(MSG_MIN_IDLE); + } + final TimerTask task = new KeyedObjectPoolMinIdleTimerTask<>( + keyedPool, key, minIdle); + getMinIdleTimer().schedule(task, 0L, periodMillis); + return task; + } + + /** + * Periodically check the idle object count for the pool. At most one idle + * object will be added per period. If there is an exception when calling + * {@link ObjectPool#addObject()} then no more checks will be performed. + * + * @param pool + * the pool to check periodically. + * @param minIdle + * if the {@link ObjectPool#getNumIdle()} is less than this then + * add an idle object. + * @param periodMillis + * the frequency in milliseconds to check the number of idle objects in a pool, + * see {@link Timer#schedule(TimerTask, long, long)}. + * @param the type of objects in the pool + * + * @return the {@link TimerTask} that will periodically check the pools idle + * object count. + * @throws IllegalArgumentException + * when {@code pool} is {@code null} or when {@code minIdle} is + * negative or when {@code period} isn't valid for + * {@link Timer#schedule(TimerTask, long, long)} + */ + public static TimerTask checkMinIdle(final ObjectPool pool, + final int minIdle, final long periodMillis) + throws IllegalArgumentException { + if (pool == null) { + throw new IllegalArgumentException(MSG_NULL_KEYED_POOL); + } + if (minIdle < 0) { + throw new IllegalArgumentException(MSG_MIN_IDLE); + } + final TimerTask task = new ObjectPoolMinIdleTimerTask<>(pool, minIdle); + getMinIdleTimer().schedule(task, 0L, periodMillis); + return task; + } + + /** + * Should the supplied Throwable be re-thrown (eg if it is an instance of + * one of the Throwables that should never be swallowed). Used by the pool + * error handling for operations that throw exceptions that normally need to + * be ignored. + * + * @param t + * The Throwable to check + * @throws ThreadDeath + * if that is passed in + * @throws VirtualMachineError + * if that is passed in + */ + public static void checkRethrow(final Throwable t) { + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + // All other instances of Throwable will be silently swallowed + } + + /** + * Returns a pool that adaptively decreases its size when idle objects are + * no longer needed. This is intended as an always thread-safe alternative + * to using an idle object evictor provided by many pool implementations. + * This is also an effective way to shrink FIFO ordered pools that + * experience load spikes. + * + * @param keyedPool + * the KeyedObjectPool to be decorated so it shrinks its idle + * count when possible. + * @param the type of the pool key + * @param the type of pool entries + * + * @throws IllegalArgumentException + * when {@code keyedPool} is {@code null}. + * @return a pool that adaptively decreases its size when idle objects are + * no longer needed. + * @see #erodingPool(KeyedObjectPool, float) + * @see #erodingPool(KeyedObjectPool, float, boolean) + */ + public static KeyedObjectPool erodingPool(final KeyedObjectPool keyedPool) { + return erodingPool(keyedPool, 1f); + } + + /** + * Returns a pool that adaptively decreases its size when idle objects are + * no longer needed. This is intended as an always thread-safe alternative + * to using an idle object evictor provided by many pool implementations. + * This is also an effective way to shrink FIFO ordered pools that + * experience load spikes. + *

    + * The factor parameter provides a mechanism to tweak the rate at which the + * pool tries to shrink its size. Values between 0 and 1 cause the pool to + * try to shrink its size more often. Values greater than 1 cause the pool + * to less frequently try to shrink its size. + *

    + * + * @param keyedPool + * the KeyedObjectPool to be decorated so it shrinks its idle + * count when possible. + * @param factor + * a positive value to scale the rate at which the pool tries to + * reduce its size. If 0 < factor < 1 then the pool + * shrinks more aggressively. If 1 < factor then the pool + * shrinks less aggressively. + * @param the type of the pool key + * @param the type of pool entries + * + * @throws IllegalArgumentException + * when {@code keyedPool} is {@code null} or when {@code factor} + * is not positive. + * @return a pool that adaptively decreases its size when idle objects are + * no longer needed. + * @see #erodingPool(KeyedObjectPool, float, boolean) + */ + public static KeyedObjectPool erodingPool(final KeyedObjectPool keyedPool, final float factor) { + return erodingPool(keyedPool, factor, false); + } + + /** + * Returns a pool that adaptively decreases its size when idle objects are + * no longer needed. This is intended as an always thread-safe alternative + * to using an idle object evictor provided by many pool implementations. + * This is also an effective way to shrink FIFO ordered pools that + * experience load spikes. + *

    + * The factor parameter provides a mechanism to tweak the rate at which the + * pool tries to shrink its size. Values between 0 and 1 cause the pool to + * try to shrink its size more often. Values greater than 1 cause the pool + * to less frequently try to shrink its size. + *

    + *

    + * The perKey parameter determines if the pool shrinks on a whole pool basis + * or a per key basis. When perKey is false, the keys do not have an effect + * on the rate at which the pool tries to shrink its size. When perKey is + * true, each key is shrunk independently. + *

    + * + * @param keyedPool + * the KeyedObjectPool to be decorated so it shrinks its idle + * count when possible. + * @param factor + * a positive value to scale the rate at which the pool tries to + * reduce its size. If 0 < factor < 1 then the pool + * shrinks more aggressively. If 1 < factor then the pool + * shrinks less aggressively. + * @param perKey + * when true, each key is treated independently. + * @param the type of the pool key + * @param the type of pool entries + * + * @throws IllegalArgumentException + * when {@code keyedPool} is {@code null} or when {@code factor} + * is not positive. + * @return a pool that adaptively decreases its size when idle objects are + * no longer needed. + * @see #erodingPool(KeyedObjectPool) + * @see #erodingPool(KeyedObjectPool, float) + */ + public static KeyedObjectPool erodingPool( + final KeyedObjectPool keyedPool, final float factor, + final boolean perKey) { + if (keyedPool == null) { + throw new IllegalArgumentException(MSG_NULL_KEYED_POOL); + } + if (factor <= 0f) { + throw new IllegalArgumentException(MSG_FACTOR_NEGATIVE); + } + if (perKey) { + return new ErodingPerKeyKeyedObjectPool<>(keyedPool, factor); + } + return new ErodingKeyedObjectPool<>(keyedPool, factor); + } + + /** + * Returns a pool that adaptively decreases its size when idle objects are + * no longer needed. This is intended as an always thread-safe alternative + * to using an idle object evictor provided by many pool implementations. + * This is also an effective way to shrink FIFO ordered pools that + * experience load spikes. + * + * @param pool + * the ObjectPool to be decorated so it shrinks its idle count + * when possible. + * @param the type of objects in the pool + * + * @throws IllegalArgumentException + * when {@code pool} is {@code null}. + * @return a pool that adaptively decreases its size when idle objects are + * no longer needed. + * @see #erodingPool(ObjectPool, float) + */ + public static ObjectPool erodingPool(final ObjectPool pool) { + return erodingPool(pool, 1f); + } + + /** + * Returns a pool that adaptively decreases its size when idle objects are + * no longer needed. This is intended as an always thread-safe alternative + * to using an idle object evictor provided by many pool implementations. + * This is also an effective way to shrink FIFO ordered pools that + * experience load spikes. + *

    + * The factor parameter provides a mechanism to tweak the rate at which the + * pool tries to shrink its size. Values between 0 and 1 cause the pool to + * try to shrink its size more often. Values greater than 1 cause the pool + * to less frequently try to shrink its size. + *

    + * + * @param pool + * the ObjectPool to be decorated so it shrinks its idle count + * when possible. + * @param factor + * a positive value to scale the rate at which the pool tries to + * reduce its size. If 0 < factor < 1 then the pool + * shrinks more aggressively. If 1 < factor then the pool + * shrinks less aggressively. + * @param the type of objects in the pool + * + * @throws IllegalArgumentException + * when {@code pool} is {@code null} or when {@code factor} is + * not positive. + * @return a pool that adaptively decreases its size when idle objects are + * no longer needed. + * @see #erodingPool(ObjectPool) + */ + public static ObjectPool erodingPool(final ObjectPool pool, final float factor) { + if (pool == null) { + throw new IllegalArgumentException(MSG_NULL_POOL); + } + if (factor <= 0f) { + throw new IllegalArgumentException(MSG_FACTOR_NEGATIVE); + } + return new ErodingObjectPool<>(pool, factor); + } + + /** + * Gets the {@code Timer} for checking keyedPool's idle count. + * + * @return the {@link Timer} for checking keyedPool's idle count. + */ + private static Timer getMinIdleTimer() { + return TimerHolder.MIN_IDLE_TIMER; + } + + /** + * Calls {@link KeyedObjectPool#addObject(Object)} on {@code keyedPool} with + * each key in {@code keys} for {@code count} number of times. This has + * the same effect as calling {@link #prefill(KeyedObjectPool, Object, int)} + * for each key in the {@code keys} collection. + * + * @param keyedPool + * the keyedPool to prefill. + * @param keys + * {@link Collection} of keys to add objects for. + * @param count + * the number of idle objects to add for each {@code key}. + * @param the type of the pool key + * @param the type of pool entries + * + * @throws Exception + * when {@link KeyedObjectPool#addObject(Object)} fails. + * @throws IllegalArgumentException + * when {@code keyedPool}, {@code keys}, or any value in + * {@code keys} is {@code null}. + * @see #prefill(KeyedObjectPool, Object, int) + * @deprecated Use {@link KeyedObjectPool#addObjects(Collection, int)}. + */ + @Deprecated + public static void prefill(final KeyedObjectPool keyedPool, + final Collection keys, final int count) throws Exception, + IllegalArgumentException { + if (keys == null) { + throw new IllegalArgumentException(MSG_NULL_KEYS); + } + keyedPool.addObjects(keys, count); + } + + /** + * Calls {@link KeyedObjectPool#addObject(Object)} on {@code keyedPool} with + * {@code key} {@code count} number of times. + * + * @param keyedPool + * the keyedPool to prefill. + * @param key + * the key to add objects for. + * @param count + * the number of idle objects to add for {@code key}. + * @param the type of the pool key + * @param the type of pool entries + * + * @throws Exception + * when {@link KeyedObjectPool#addObject(Object)} fails. + * @throws IllegalArgumentException + * when {@code keyedPool} or {@code key} is {@code null}. + * @deprecated Use {@link KeyedObjectPool#addObjects(Object, int)}. + */ + @Deprecated + public static void prefill(final KeyedObjectPool keyedPool, + final K key, final int count) throws Exception, + IllegalArgumentException { + if (keyedPool == null) { + throw new IllegalArgumentException(MSG_NULL_KEYED_POOL); + } + keyedPool.addObjects(key, count); + } + + /** + * Calls {@link ObjectPool#addObject()} on {@code pool} {@code count} number + * of times. + * + * @param pool + * the pool to prefill. + * @param count + * the number of idle objects to add. + * @param the type of objects in the pool + * + * @throws Exception + * when {@link ObjectPool#addObject()} fails. + * @throws IllegalArgumentException + * when {@code pool} is {@code null}. + * @deprecated Use {@link ObjectPool#addObjects(int)}. + */ + @Deprecated + public static void prefill(final ObjectPool pool, final int count) + throws Exception { + if (pool == null) { + throw new IllegalArgumentException(MSG_NULL_POOL); + } + pool.addObjects(count); + } + + /** + * Returns a synchronized (thread-safe) KeyedPooledObjectFactory backed by + * the specified KeyedPooledObjectFactory. + * + * @param keyedFactory + * the KeyedPooledObjectFactory to be "wrapped" in a + * synchronized KeyedPooledObjectFactory. + * @param the type of the pool key + * @param the type of pool entries + * @return a synchronized view of the specified KeyedPooledObjectFactory. + */ + public static KeyedPooledObjectFactory synchronizedKeyedPooledFactory( + final KeyedPooledObjectFactory keyedFactory) { + return new SynchronizedKeyedPooledObjectFactory<>(keyedFactory); + } + + /** + * Returns a synchronized (thread-safe) KeyedObjectPool backed by the + * specified KeyedObjectPool. + *

    + * Note: This should not be used on pool implementations that already + * provide proper synchronization such as the pools provided in the Commons + * Pool library. Wrapping a pool that {@link #wait() waits} for poolable + * objects to be returned before allowing another one to be borrowed with + * another layer of synchronization will cause liveliness issues or a + * deadlock. + *

    + * + * @param keyedPool + * the KeyedObjectPool to be "wrapped" in a synchronized + * KeyedObjectPool. + * @param the type of the pool key + * @param the type of pool entries + * + * @return a synchronized view of the specified KeyedObjectPool. + */ + public static KeyedObjectPool synchronizedPool(final KeyedObjectPool keyedPool) { + /* + * assert !(keyedPool instanceof GenericKeyedObjectPool) : + * "GenericKeyedObjectPool is already thread-safe"; assert !(keyedPool + * instanceof StackKeyedObjectPool) : + * "StackKeyedObjectPool is already thread-safe"; assert + * !"org.apache.commons.pool.composite.CompositeKeyedObjectPool" + * .equals(keyedPool.getClass().getName()) : + * "CompositeKeyedObjectPools are already thread-safe"; + */ + return new SynchronizedKeyedObjectPool<>(keyedPool); + } + + /** + * Returns a synchronized (thread-safe) ObjectPool backed by the specified + * ObjectPool. + *

    + * Note: This should not be used on pool implementations that already + * provide proper synchronization such as the pools provided in the Commons + * Pool library. Wrapping a pool that {@link #wait() waits} for poolable + * objects to be returned before allowing another one to be borrowed with + * another layer of synchronization will cause liveliness issues or a + * deadlock. + *

    + * + * @param the type of objects in the pool + * @param pool + * the ObjectPool to be "wrapped" in a synchronized ObjectPool. + * @throws IllegalArgumentException + * when {@code pool} is {@code null}. + * @return a synchronized view of the specified ObjectPool. + */ + public static ObjectPool synchronizedPool(final ObjectPool pool) { + if (pool == null) { + throw new IllegalArgumentException(MSG_NULL_POOL); + } + + /* + * assert !(pool instanceof GenericObjectPool) : + * "GenericObjectPool is already thread-safe"; assert !(pool instanceof + * SoftReferenceObjectPool) : + * "SoftReferenceObjectPool is already thread-safe"; assert !(pool + * instanceof StackObjectPool) : + * "StackObjectPool is already thread-safe"; assert + * !"org.apache.commons.pool.composite.CompositeObjectPool" + * .equals(pool.getClass().getName()) : + * "CompositeObjectPools are already thread-safe"; + */ + return new SynchronizedObjectPool<>(pool); + } + + /** + * Returns a synchronized (thread-safe) PooledObjectFactory backed by the + * specified PooledObjectFactory. + * + * @param factory + * the PooledObjectFactory to be "wrapped" in a synchronized + * PooledObjectFactory. + * @param the type of objects in the pool + * @return a synchronized view of the specified PooledObjectFactory. + */ + public static PooledObjectFactory synchronizedPooledFactory(final PooledObjectFactory factory) { + return new SynchronizedPooledObjectFactory<>(factory); + } + + /** + * PoolUtils instances should NOT be constructed in standard programming. + * Instead, the class should be used procedurally: PoolUtils.adapt(aPool);. + * This constructor is public to permit tools that require a JavaBean + * instance to operate. + */ + public PoolUtils() { + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/PooledObject.java b/java/org/apache/tomcat/dbcp/pool2/PooledObject.java new file mode 100644 index 0000000..75bbc94 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/PooledObject.java @@ -0,0 +1,358 @@ +/* + * 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.tomcat.dbcp.pool2; + +import java.io.PrintWriter; +import java.time.Duration; +import java.time.Instant; +import java.util.Deque; + +/** + * Defines the wrapper that is used to track the additional information, such as + * state, for the pooled objects. + *

    + * Implementations of this class are required to be thread-safe. + *

    + * + * @param the type of object in the pool. + * @since 2.0 + */ +public interface PooledObject extends Comparable> { + + /** + * Tests whether the given PooledObject is null or contains a null. + * + * @param pooledObject the PooledObject to test. + * @return whether the given PooledObject is null or contains a null. + * @since 2.12.0 + */ + static boolean isNull(final PooledObject pooledObject) { + return pooledObject == null || pooledObject.getObject() == null; + } + + /** + * Allocates the object. + * + * @return {@code true} if the original state was {@link PooledObjectState#IDLE IDLE} + */ + boolean allocate(); + + /** + * Orders instances based on idle time - i.e. the length of time since the + * instance was returned to the pool. Used by the GKOP idle object evictor. + *

    + * Note: This class has a natural ordering that is inconsistent with + * equals if distinct objects have the same identity hash code. + *

    + *

    + * {@inheritDoc} + *

    + */ + @Override + int compareTo(PooledObject other); + + /** + * Deallocates the object and sets it {@link PooledObjectState#IDLE IDLE} + * if it is currently {@link PooledObjectState#ALLOCATED ALLOCATED}. + * + * @return {@code true} if the state was {@link PooledObjectState#ALLOCATED ALLOCATED}. + */ + boolean deallocate(); + + /** + * Notifies the object that the eviction test has ended. + * + * @param idleQueue The queue of idle objects to which the object should be + * returned. + * @return Currently not used. + */ + boolean endEvictionTest(Deque> idleQueue); + + @Override + boolean equals(Object obj); + + /** + * Gets the amount of time this object last spent in the active state (it may still be active in which case + * subsequent calls will return an increased value). + * + * @return The duration last spent in the active state. + * @since 2.11.0 + */ + default Duration getActiveDuration() { + // Take copies to avoid threading issues + final Instant lastReturnInstant = getLastReturnInstant(); + final Instant lastBorrowInstant = getLastBorrowInstant(); + // @formatter:off + return lastReturnInstant.isAfter(lastBorrowInstant) ? + Duration.between(lastBorrowInstant, lastReturnInstant) : + Duration.between(lastBorrowInstant, Instant.now()); + // @formatter:on + } + + /** + * Gets the amount of time this object last spent in the active state (it may still be active in which case + * subsequent calls will return an increased value). + * + * @return The duration last spent in the active state. + * @since 2.10.0 + * @deprecated Use {@link #getActiveDuration()}. + */ + @Deprecated + default Duration getActiveTime() { + return getActiveDuration(); + } + + /** + * Gets the amount of time in milliseconds this object last spent in the + * active state (it may still be active in which case subsequent calls will + * return an increased value). + * + * @return The time in milliseconds last spent in the active state. + * @deprecated Use {@link #getActiveTime()} which offers the best precision. + */ + @Deprecated + long getActiveTimeMillis(); + + /** + * Gets the number of times this object has been borrowed. + * + * @return -1 by default for implementations prior to release 2.7.0. + * @since 2.7.0 + */ + default long getBorrowedCount() { + return -1; + } + + /** + * Gets the time (using the same basis as {@link Instant#now()}) that this object was created. + * + * @return The creation time for the wrapped object. + * @since 2.11.0 + */ + default Instant getCreateInstant() { + return Instant.ofEpochMilli(getCreateTime()); + } + + /** + * Gets the time (using the same basis as + * {@link java.time.Clock#instant()}) that this object was created. + * + * @return The creation time for the wrapped object. + * @deprecated Use {@link #getCreateInstant()} which offers the best precision. + */ + @Deprecated + long getCreateTime(); + + /** + * Gets the duration since this object was created (using {@link Instant#now()}). + * + * @return The duration since this object was created. + * @since 2.12.0 + */ + default Duration getFullDuration() { + return Duration.between(getCreateInstant(), Instant.now()); + } + + /** + * Gets the amount of time that this object last spend in the + * idle state (it may still be idle in which case subsequent calls will + * return an increased value). + * + * @return The amount of time in last spent in the idle state. + * @since 2.11.0 + */ + default Duration getIdleDuration() { + return Duration.ofMillis(getIdleTimeMillis()); + } + + /** + * Gets the amount of time that this object last spend in the + * idle state (it may still be idle in which case subsequent calls will + * return an increased value). + * + * @return The amount of time in last spent in the idle state. + * @since 2.10.0 + * @deprecated Use {@link #getIdleDuration()}. + */ + @Deprecated + default Duration getIdleTime() { + return Duration.ofMillis(getIdleTimeMillis()); + } + + /** + * Gets the amount of time in milliseconds that this object last spend in the + * idle state (it may still be idle in which case subsequent calls will + * return an increased value). + * + * @return The time in milliseconds last spent in the idle state. + * @deprecated Use {@link #getIdleTime()} which offers the best precision. + */ + @Deprecated + long getIdleTimeMillis(); + + /** + * Gets the time the wrapped object was last borrowed. + * + * @return The time the object was last borrowed. + * @since 2.11.0 + */ + default Instant getLastBorrowInstant() { + return Instant.ofEpochMilli(getLastBorrowTime()); + } + + /** + * Gets the time the wrapped object was last borrowed. + * + * @return The time the object was last borrowed. + * @deprecated Use {@link #getLastBorrowInstant()} which offers the best precision. + */ + @Deprecated + long getLastBorrowTime(); + + /** + * Gets the time the wrapped object was last borrowed. + * + * @return The time the object was last borrowed. + * @since 2.11.0 + */ + default Instant getLastReturnInstant() { + return Instant.ofEpochMilli(getLastReturnTime()); + } + + /** + * Gets the time the wrapped object was last returned. + * + * @return The time the object was last returned. + * @deprecated Use {@link #getLastReturnInstant()} which offers the best precision. + */ + @Deprecated + long getLastReturnTime(); + + /** + * Gets an estimate of the last time this object was used. If the class of the pooled object implements + * {@link TrackedUse}, what is returned is the maximum of {@link TrackedUse#getLastUsedInstant()} and + * {@link #getLastBorrowTime()}; otherwise this method gives the same value as {@link #getLastBorrowTime()}. + * + * @return the last time this object was used + * @since 2.11.0 + */ + default Instant getLastUsedInstant() { + return Instant.ofEpochMilli(getLastUsedTime()); + } + + /** + * Gets an estimate of the last time this object was used. If the class + * of the pooled object implements {@link TrackedUse}, what is returned is + * the maximum of {@link TrackedUse#getLastUsedInstant()} and + * {@link #getLastBorrowTime()}; otherwise this method gives the same + * value as {@link #getLastBorrowTime()}. + * + * @return the last time this object was used. + * @deprecated Use {@link #getLastUsedInstant()} which offers the best precision. + */ + @Deprecated + long getLastUsedTime(); + + /** + * Gets the underlying object that is wrapped by this instance of + * {@link PooledObject}. + * + * @return The wrapped object. + */ + T getObject(); + + /** + * Gets the state of this object. + * + * @return state + */ + PooledObjectState getState(); + + @Override + int hashCode(); + + /** + * Sets the state to {@link PooledObjectState#INVALID INVALID}. + */ + void invalidate(); + + /** + * Marks the pooled object as abandoned. + */ + void markAbandoned(); + + /** + * Marks the object as returning to the pool. + */ + void markReturning(); + + /** + * Prints the stack trace of the code that borrowed this pooled object and + * the stack trace of the last code to use this object (if available) to + * the supplied writer. + * + * @param writer The destination for the debug output. + */ + void printStackTrace(PrintWriter writer); + + /** + * Sets whether to use abandoned object tracking. If this is true the + * implementation will need to record the stack trace of the last caller to + * borrow this object. + * + * @param logAbandoned The new configuration setting for abandoned + * object tracking. + */ + void setLogAbandoned(boolean logAbandoned); + + /** + * Sets the stack trace generation strategy based on whether or not fully detailed stack traces are required. + * When set to false, abandoned logs may only include caller class information rather than method names, line + * numbers, and other normal metadata available in a full stack trace. + * + * @param requireFullStackTrace the new configuration setting for abandoned object logging. + * @since 2.7.0 + */ + default void setRequireFullStackTrace(final boolean requireFullStackTrace) { + // noop + } + + /** + * Attempts to place the pooled object in the + * {@link PooledObjectState#EVICTION} state. + * + * @return {@code true} if the object was placed in the + * {@link PooledObjectState#EVICTION} state otherwise + * {@code false}. + */ + boolean startEvictionTest(); + + /** + * Gets a String form of the wrapper for debug purposes. The format is + * not fixed and may change at any time. + * + * {@inheritDoc} + */ + @Override + String toString(); + + /** + * Records the current stack trace as the last time the object was used. + */ + void use(); + +} diff --git a/java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java b/java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java new file mode 100644 index 0000000..96d020d --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java @@ -0,0 +1,162 @@ +/* + * 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.tomcat.dbcp.pool2; + +/** + * An interface defining life-cycle methods for instances to be served by an + * {@link ObjectPool}. + *

    + * By contract, when an {@link ObjectPool} delegates to a + * {@link PooledObjectFactory}, + *

    + *
      + *
    1. + * {@link #makeObject} is called whenever a new instance is needed. + *
    2. + *
    3. + * {@link #activateObject} is invoked on every instance that has been + * {@link #passivateObject passivated} before it is + * {@link ObjectPool#borrowObject borrowed} from the pool. + *
    4. + *
    5. + * {@link #validateObject} may be invoked on {@link #activateObject activated} + * instances to make sure they can be {@link ObjectPool#borrowObject borrowed} + * from the pool. {@link #validateObject} may also be used to + * test an instance being {@link ObjectPool#returnObject returned} to the pool + * before it is {@link #passivateObject passivated}. It will only be invoked + * on an activated instance. + *
    6. + *
    7. + * {@link #passivateObject} is invoked on every instance when it is returned + * to the pool. + *
    8. + *
    9. + * {@link #destroyObject} is invoked on every instance when it is being + * "dropped" from the pool (whether due to the response from + * {@link #validateObject}, or for reasons specific to the pool + * implementation.) There is no guarantee that the instance being destroyed + * will be considered active, passive or in a generally consistent state. + *
    10. + *
    + * {@link PooledObjectFactory} must be thread-safe. The only promise + * an {@link ObjectPool} makes is that the same instance of an object will not + * be passed to more than one method of a {@code PoolableObjectFactory} + * at a time. + *

    + * While clients of a {@link KeyedObjectPool} borrow and return instances of + * the underlying value type {@code V}, the factory methods act on instances of + * {@link PooledObject PooledObject<V>}. These are the object wrappers that + * pools use to track and maintain state information about the objects that + * they manage. + *

    + * + * @param Type of element managed in this factory. + * + * @see ObjectPool + * + * @since 2.0 + */ +public interface PooledObjectFactory { + + /** + * Reinitializes an instance to be returned by the pool. + * + * @param p a {@code PooledObject} wrapping the instance to be activated + * + * @throws Exception if there is a problem activating {@code obj}, + * this exception may be swallowed by the pool. + * + * @see #destroyObject + */ + void activateObject(PooledObject p) throws Exception; + + /** + * Destroys an instance no longer needed by the pool, using the default (NORMAL) + * DestroyMode. + *

    + * It is important for implementations of this method to be aware that there + * is no guarantee about what state {@code obj} will be in and the + * implementation should be prepared to handle unexpected errors. + *

    + *

    + * Also, an implementation must take in to consideration that instances lost + * to the garbage collector may never be destroyed. + *

    + * + * @param p a {@code PooledObject} wrapping the instance to be destroyed + * + * @throws Exception should be avoided as it may be swallowed by + * the pool implementation. + * + * @see #validateObject + * @see ObjectPool#invalidateObject + */ + void destroyObject(PooledObject p) throws Exception; + + /** + * Destroys an instance no longer needed by the pool, using the provided + * DestroyMode. + * + * @param p a {@code PooledObject} wrapping the instance to be destroyed + * @param destroyMode DestroyMode providing context to the factory + * + * @throws Exception should be avoided as it may be swallowed by + * the pool implementation. + * + * @see #validateObject + * @see ObjectPool#invalidateObject + * @see #destroyObject(PooledObject) + * @see DestroyMode + * @since 2.9.0 + */ + default void destroyObject(final PooledObject p, final DestroyMode destroyMode) throws Exception { + destroyObject(p); + } + + /** + * Creates an instance that can be served by the pool and wrap it in a + * {@link PooledObject} to be managed by the pool. + * + * @return a {@code PooledObject} wrapping an instance that can be served by the pool, not null. + * + * @throws Exception if there is a problem creating a new instance, + * this will be propagated to the code requesting an object. + */ + PooledObject makeObject() throws Exception; + + /** + * Uninitializes an instance to be returned to the idle object pool. + * + * @param p a {@code PooledObject} wrapping the instance to be passivated + * + * @throws Exception if there is a problem passivating {@code obj}, + * this exception may be swallowed by the pool. + * + * @see #destroyObject + */ + void passivateObject(PooledObject p) throws Exception; + + /** + * Ensures that the instance is safe to be returned by the pool. + * + * @param p a {@code PooledObject} wrapping the instance to be validated + * + * @return {@code false} if {@code obj} is not valid and should + * be dropped from the pool, {@code true} otherwise. + */ + boolean validateObject(PooledObject p); +} diff --git a/java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java b/java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java new file mode 100644 index 0000000..b405f94 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java @@ -0,0 +1,83 @@ +/* + * 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.tomcat.dbcp.pool2; + +/** + * Provides all possible states of a {@link PooledObject}. + * + * @since 2.0 + */ +public enum PooledObjectState { + + /** + * In the queue, not in use. + */ + IDLE, + + /** + * In use. + */ + ALLOCATED, + + /** + * In the queue, currently being tested for possible eviction. + */ + EVICTION, + + /** + * Not in the queue, currently being tested for possible eviction. An attempt to borrow the object was made while + * being tested which removed it from the queue. It should be returned to the head of the queue once eviction + * testing completes. + *

    + * TODO: Consider allocating object and ignoring the result of the eviction test. + *

    + */ + EVICTION_RETURN_TO_HEAD, + + /** + * In the queue, currently being validated. + */ + VALIDATION, + + /** + * Not in queue, currently being validated. The object was borrowed while being validated and since testOnBorrow was + * configured, it was removed from the queue and pre-allocated. It should be allocated once validation completes. + */ + VALIDATION_PREALLOCATED, + + /** + * Not in queue, currently being validated. An attempt to borrow the object was made while previously being tested + * for eviction which removed it from the queue. It should be returned to the head of the queue once validation + * completes. + */ + VALIDATION_RETURN_TO_HEAD, + + /** + * Failed maintenance (e.g. eviction test or validation) and will be / has been destroyed + */ + INVALID, + + /** + * Deemed abandoned, to be invalidated. + */ + ABANDONED, + + /** + * Returning to the pool. + */ + RETURNING +} diff --git a/java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java b/java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java new file mode 100644 index 0000000..c80b05b --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.dbcp.pool2; + +/** + * Pools that unavoidably swallow exceptions may be configured with an instance + * of this listener so the user may receive notification of when this happens. + * The listener should not throw an exception when called but pools calling + * listeners should protect themselves against exceptions anyway. + * + * @since 2.0 + */ +public interface SwallowedExceptionListener { + + /** + * Notifies this instance every time the implementation unavoidably swallows + * an exception. + * + * @param e The exception that was swallowed + */ + void onSwallowException(Exception e); +} diff --git a/java/org/apache/tomcat/dbcp/pool2/TrackedUse.java b/java/org/apache/tomcat/dbcp/pool2/TrackedUse.java new file mode 100644 index 0000000..cac0ce2 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/TrackedUse.java @@ -0,0 +1,52 @@ +/* + * 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.tomcat.dbcp.pool2; + +import java.time.Instant; + +/** + * Allows pooled objects to make information available about when and how they were used available to the object pool. + * The object pool may, but is not required, to use this information to make more informed decisions when determining + * the state of a pooled object - for instance whether or not the object has been abandoned. + * + * @since 2.0 + */ +public interface TrackedUse { + + /** + * Gets the last time this object was used in milliseconds. + * + * @return the last time this object was used in milliseconds. + * @deprecated Use {@link #getLastUsedInstant()} which offers the best precision. + */ + @Deprecated + long getLastUsed(); + + /** + * Gets the last Instant this object was used. + *

    + * Starting with Java 9, the JRE {@code SystemClock} precision is increased usually down to microseconds, or tenth + * of microseconds, depending on the OS, Hardware, and JVM implementation. + *

    + * + * @return the last Instant this object was used. + * @since 2.11.0 + */ + default Instant getLastUsedInstant() { + return Instant.ofEpochMilli(getLastUsed()); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/UsageTracking.java b/java/org/apache/tomcat/dbcp/pool2/UsageTracking.java new file mode 100644 index 0000000..9d49863 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/UsageTracking.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.dbcp.pool2; + +/** + * This interface may be implemented by an object pool to enable clients (primarily those clients that wrap pools to + * provide pools with extended features) to provide additional information to the pool relating to object using allowing + * more informed decisions and reporting to be made regarding abandoned objects. + * + * @param The type of object provided by the pool. + * + * @since 2.0 + */ +public interface UsageTracking { + + /** + * Called every time a pooled object is used to enable the pool to better track borrowed objects. + * + * @param pooledObject The object that is being used. + */ + void use(T pooledObject); +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java b/java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java new file mode 100644 index 0000000..7f4fc25 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java @@ -0,0 +1,366 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.time.Duration; + +import org.apache.tomcat.dbcp.pool2.TrackedUse; +import org.apache.tomcat.dbcp.pool2.UsageTracking; + +/** + * Configuration settings for abandoned object removal. + * + * @since 2.0 + */ +public class AbandonedConfig { + + /** + * The 5 minutes Duration. + */ + private static final Duration DEFAULT_REMOVE_ABANDONED_TIMEOUT_DURATION = Duration.ofMinutes(5); + + /** + * Creates a new instance with values from the given instance. + * + * @param abandonedConfig the source, may be null. + * @return A new instance or null if the input is null. + * @since 2.11.0 + */ + public static AbandonedConfig copy(final AbandonedConfig abandonedConfig) { + return abandonedConfig == null ? null : new AbandonedConfig(abandonedConfig); + } + + /** + * Whether or not borrowObject performs abandoned object removal. + */ + private boolean removeAbandonedOnBorrow; + + /** + * Whether or not pool maintenance (evictor) performs abandoned object + * removal. + */ + private boolean removeAbandonedOnMaintenance; + + /** + * Timeout before an abandoned object can be removed. + */ + private Duration removeAbandonedTimeoutDuration = DEFAULT_REMOVE_ABANDONED_TIMEOUT_DURATION; + + /** + * Determines whether or not to log stack traces for application code + * which abandoned an object. + */ + private boolean logAbandoned; + + /** + * Determines whether or not to log full stack traces when logAbandoned is true. + * If disabled, then a faster method for logging stack traces with only class data + * may be used if possible. + * + * @since 2.5 + */ + private boolean requireFullStackTrace = true; + + /** + * PrintWriter to use to log information on abandoned objects. + * Use of default system encoding is deliberate. + */ + private PrintWriter logWriter = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset())); + + /** + * If the pool implements {@link UsageTracking}, should the pool record a + * stack trace every time a method is called on a pooled object and retain + * the most recent stack trace to aid debugging of abandoned objects? + */ + private boolean useUsageTracking; + + /** + * Creates a new instance. + */ + public AbandonedConfig() { + // empty + } + + /** + * Creates a new instance with values from the given instance. + * + * @param abandonedConfig the source. + */ + private AbandonedConfig(final AbandonedConfig abandonedConfig) { + this.setLogAbandoned(abandonedConfig.getLogAbandoned()); + this.setLogWriter(abandonedConfig.getLogWriter()); + this.setRemoveAbandonedOnBorrow(abandonedConfig.getRemoveAbandonedOnBorrow()); + this.setRemoveAbandonedOnMaintenance(abandonedConfig.getRemoveAbandonedOnMaintenance()); + this.setRemoveAbandonedTimeout(abandonedConfig.getRemoveAbandonedTimeoutDuration()); + this.setUseUsageTracking(abandonedConfig.getUseUsageTracking()); + this.setRequireFullStackTrace(abandonedConfig.getRequireFullStackTrace()); + } + + /** + * Flag to log stack traces for application code which abandoned + * an object. + * + * Defaults to false. + * Logging of abandoned objects adds overhead for every object created + * because a stack trace has to be generated. + * + * @return boolean true if stack trace logging is turned on for abandoned + * objects + * + */ + public boolean getLogAbandoned() { + return this.logAbandoned; + } + + /** + * Gets the log writer being used by this configuration to log + * information on abandoned objects. If not set, a PrintWriter based on + * System.out with the system default encoding is used. + * + * @return log writer in use + */ + public PrintWriter getLogWriter() { + return logWriter; + } + + /** + *

    Flag to remove abandoned objects if they exceed the + * removeAbandonedTimeout when borrowObject is invoked.

    + * + *

    The default value is false.

    + * + *

    If set to true, abandoned objects are removed by borrowObject if + * there are fewer than 2 idle objects available in the pool and + * {@code getNumActive() > getMaxTotal() - 3}

    + * + * @return true if abandoned objects are to be removed by borrowObject + */ + public boolean getRemoveAbandonedOnBorrow() { + return this.removeAbandonedOnBorrow; + } + + /** + *

    Flag to remove abandoned objects if they exceed the + * removeAbandonedTimeout when pool maintenance (the "evictor") + * runs.

    + * + *

    The default value is false.

    + * + *

    If set to true, abandoned objects are removed by the pool + * maintenance thread when it runs. This setting has no effect + * unless maintenance is enabled by setting + * {@link GenericObjectPool#getDurationBetweenEvictionRuns()} + * to a positive number.

    + * + * @return true if abandoned objects are to be removed by the evictor + */ + public boolean getRemoveAbandonedOnMaintenance() { + return this.removeAbandonedOnMaintenance; + } + + /** + *

    Timeout in seconds before an abandoned object can be removed.

    + * + *

    The time of most recent use of an object is the maximum (latest) of + * {@link TrackedUse#getLastUsedInstant()} (if this class of the object implements + * TrackedUse) and the time when the object was borrowed from the pool.

    + * + *

    The default value is 300 seconds.

    + * + * @return the abandoned object timeout in seconds. + * @deprecated Use {@link #getRemoveAbandonedTimeoutDuration()}. + */ + @Deprecated + public int getRemoveAbandonedTimeout() { + return (int) this.removeAbandonedTimeoutDuration.getSeconds(); + } + + /** + *

    Timeout before an abandoned object can be removed.

    + * + *

    The time of most recent use of an object is the maximum (latest) of + * {@link TrackedUse#getLastUsedInstant()} (if this class of the object implements + * TrackedUse) and the time when the object was borrowed from the pool.

    + * + *

    The default value is 300 seconds.

    + * + * @return the abandoned object timeout. + * @since 2.10.0 + */ + public Duration getRemoveAbandonedTimeoutDuration() { + return this.removeAbandonedTimeoutDuration; + } + + /** + * Indicates if full stack traces are required when {@link #getLogAbandoned() logAbandoned} + * is true. Defaults to true. Logging of abandoned objects requiring a full stack trace will + * generate an entire stack trace to generate for every object created. If this is disabled, + * a faster but less informative stack walking mechanism may be used if available. + * + * @return true if full stack traces are required for logging abandoned connections, or false + * if abbreviated stack traces are acceptable + * @see CallStack + * @since 2.5 + */ + public boolean getRequireFullStackTrace() { + return requireFullStackTrace; + } + + /** + * If the pool implements {@link UsageTracking}, should the pool record a + * stack trace every time a method is called on a pooled object and retain + * the most recent stack trace to aid debugging of abandoned objects? + * + * @return {@code true} if usage tracking is enabled + */ + public boolean getUseUsageTracking() { + return useUsageTracking; + } + + /** + * Sets the flag to log stack traces for application code which abandoned + * an object. + * + * @param logAbandoned true turns on abandoned stack trace logging + * @see #getLogAbandoned() + * + */ + public void setLogAbandoned(final boolean logAbandoned) { + this.logAbandoned = logAbandoned; + } + + /** + * Sets the log writer to be used by this configuration to log + * information on abandoned objects. + * + * @param logWriter The new log writer + */ + public void setLogWriter(final PrintWriter logWriter) { + this.logWriter = logWriter; + } + + /** + * Flag to remove abandoned objects if they exceed the + * removeAbandonedTimeout when borrowObject is invoked. + * + * @param removeAbandonedOnBorrow true means abandoned objects will be + * removed by borrowObject + * @see #getRemoveAbandonedOnBorrow() + */ + public void setRemoveAbandonedOnBorrow(final boolean removeAbandonedOnBorrow) { + this.removeAbandonedOnBorrow = removeAbandonedOnBorrow; + } + + /** + * Flag to remove abandoned objects if they exceed the + * removeAbandonedTimeout when pool maintenance runs. + * + * @param removeAbandonedOnMaintenance true means abandoned objects will be + * removed by pool maintenance + * @see #getRemoveAbandonedOnMaintenance + */ + public void setRemoveAbandonedOnMaintenance(final boolean removeAbandonedOnMaintenance) { + this.removeAbandonedOnMaintenance = removeAbandonedOnMaintenance; + } + + /** + * Sets the timeout before an abandoned object can be + * removed. + * + *

    Setting this property has no effect if + * {@link #getRemoveAbandonedOnBorrow() removeAbandonedOnBorrow} and + * {@link #getRemoveAbandonedOnMaintenance() removeAbandonedOnMaintenance} + * are both false.

    + * + * @param removeAbandonedTimeout new abandoned timeout + * @see #getRemoveAbandonedTimeoutDuration() + * @since 2.10.0 + */ + public void setRemoveAbandonedTimeout(final Duration removeAbandonedTimeout) { + this.removeAbandonedTimeoutDuration = PoolImplUtils.nonNull(removeAbandonedTimeout, DEFAULT_REMOVE_ABANDONED_TIMEOUT_DURATION); + } + + /** + * Sets the timeout in seconds before an abandoned object can be + * removed. + * + *

    Setting this property has no effect if + * {@link #getRemoveAbandonedOnBorrow() removeAbandonedOnBorrow} and + * {@link #getRemoveAbandonedOnMaintenance() removeAbandonedOnMaintenance} + * are both false.

    + * + * @param removeAbandonedTimeoutSeconds new abandoned timeout in seconds + * @see #getRemoveAbandonedTimeoutDuration() + * @deprecated Use {@link #setRemoveAbandonedTimeout(Duration)}. + */ + @Deprecated + public void setRemoveAbandonedTimeout(final int removeAbandonedTimeoutSeconds) { + setRemoveAbandonedTimeout(Duration.ofSeconds(removeAbandonedTimeoutSeconds)); + } + + /** + * Sets the flag to require full stack traces for logging abandoned connections when enabled. + * + * @param requireFullStackTrace indicates whether or not full stack traces are required in + * abandoned connection logs + * @see CallStack + * @see #getRequireFullStackTrace() + * @since 2.5 + */ + public void setRequireFullStackTrace(final boolean requireFullStackTrace) { + this.requireFullStackTrace = requireFullStackTrace; + } + + /** + * If the pool implements {@link UsageTracking}, configure whether the pool + * should record a stack trace every time a method is called on a pooled + * object and retain the most recent stack trace to aid debugging of + * abandoned objects. + * + * @param useUsageTracking A value of {@code true} will enable + * the recording of a stack trace on every use + * of a pooled object + */ + public void setUseUsageTracking(final boolean useUsageTracking) { + this.useUsageTracking = useUsageTracking; + } + + /** + * @since 2.4.3 + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("AbandonedConfig [removeAbandonedOnBorrow="); + builder.append(removeAbandonedOnBorrow); + builder.append(", removeAbandonedOnMaintenance="); + builder.append(removeAbandonedOnMaintenance); + builder.append(", removeAbandonedTimeoutDuration="); + builder.append(removeAbandonedTimeoutDuration); + builder.append(", logAbandoned="); + builder.append(logAbandoned); + builder.append(", logWriter="); + builder.append(logWriter); + builder.append(", useUsageTracking="); + builder.append(useUsageTracking); + builder.append("]"); + return builder.toString(); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java b/java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java new file mode 100644 index 0000000..25f30d0 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java @@ -0,0 +1,2082 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.management.ManagementFactory; +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TimerTask; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; + +import org.apache.tomcat.dbcp.pool2.BaseObject; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.PooledObjectState; +import org.apache.tomcat.dbcp.pool2.SwallowedExceptionListener; + +/** + * Base class that provides common functionality for {@link GenericObjectPool} + * and {@link GenericKeyedObjectPool}. The primary reason this class exists is + * reduce code duplication between the two pool implementations. + *

    + * Concrete implementations of this class are expected to be thread-safe. + *

    + * + * @param Type of element pooled in this pool. + * @since 2.0 + */ +public abstract class BaseGenericObjectPool extends BaseObject implements AutoCloseable { + + /** + * The idle object eviction iterator. Holds a reference to the idle objects. + */ + class EvictionIterator implements Iterator> { + + private final Deque> idleObjects; + private final Iterator> idleObjectIterator; + + /** + * Constructs an EvictionIterator for the provided idle instance deque. + * @param idleObjects underlying deque. + */ + EvictionIterator(final Deque> idleObjects) { + this.idleObjects = idleObjects; + + if (getLifo()) { + idleObjectIterator = idleObjects.descendingIterator(); + } else { + idleObjectIterator = idleObjects.iterator(); + } + } + + /** + * Gets the idle object deque referenced by this iterator. + * @return the idle object deque + */ + public Deque> getIdleObjects() { + return idleObjects; + } + + /** {@inheritDoc} */ + @Override + public boolean hasNext() { + return idleObjectIterator.hasNext(); + } + + /** {@inheritDoc} */ + @Override + public PooledObject next() { + return idleObjectIterator.next(); + } + + /** {@inheritDoc} */ + @Override + public void remove() { + idleObjectIterator.remove(); + } + + } + + /** + * The idle object evictor {@link TimerTask}. + * + * @see GenericKeyedObjectPool#setTimeBetweenEvictionRunsMillis + */ + class Evictor implements Runnable { + + private ScheduledFuture scheduledFuture; + + /** + * Cancels the scheduled future. + */ + void cancel() { + scheduledFuture.cancel(false); + } + + BaseGenericObjectPool owner() { + return BaseGenericObjectPool.this; + } + + /** + * Run pool maintenance. Evict objects qualifying for eviction and then + * ensure that the minimum number of idle instances are available. + * Since the Timer that invokes Evictors is shared for all Pools but + * pools may exist in different class loaders, the Evictor ensures that + * any actions taken are under the class loader of the factory + * associated with the pool. + */ + @Override + public void run() { + final ClassLoader savedClassLoader = Thread.currentThread().getContextClassLoader(); + try { + if (factoryClassLoader != null) { + // Set the class loader for the factory + final ClassLoader cl = factoryClassLoader.get(); + if (cl == null) { + // The pool has been dereferenced and the class loader + // GC'd. Cancel this timer so the pool can be GC'd as + // well. + cancel(); + return; + } + Thread.currentThread().setContextClassLoader(cl); + } + + // Evict from the pool + try { + evict(); + } catch (final Exception e) { + swallowException(e); + } catch (final OutOfMemoryError oome) { + // Log problem but give evictor thread a chance to continue + // in case error is recoverable + oome.printStackTrace(System.err); + } + // Re-create idle instances. + try { + ensureMinIdle(); + } catch (final Exception e) { + swallowException(e); + } + } finally { + // Restore the previous CCL + Thread.currentThread().setContextClassLoader(savedClassLoader); + } + } + + /** + * Sets the scheduled future. + * + * @param scheduledFuture the scheduled future. + */ + void setScheduledFuture(final ScheduledFuture scheduledFuture) { + this.scheduledFuture = scheduledFuture; + } + + @Override + public String toString() { + return getClass().getName() + " [scheduledFuture=" + scheduledFuture + "]"; + } + + } + + /** + * Wrapper for objects under management by the pool. + * + * GenericObjectPool and GenericKeyedObjectPool maintain references to all + * objects under management using maps keyed on the objects. This wrapper + * class ensures that objects can work as hash keys. + * + * @param type of objects in the pool + */ + static class IdentityWrapper { + /** Wrapped object */ + private final T instance; + + /** + * Constructs a wrapper for an instance. + * + * @param instance object to wrap + */ + IdentityWrapper(final T instance) { + this.instance = instance; + } + + @Override + @SuppressWarnings("rawtypes") + public boolean equals(final Object other) { + return other instanceof IdentityWrapper && ((IdentityWrapper) other).instance == instance; + } + + /** + * @return the wrapped object + */ + public T getObject() { + return instance; + } + + @Override + public int hashCode() { + return System.identityHashCode(instance); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("IdentityWrapper [instance="); + builder.append(instance); + builder.append("]"); + return builder.toString(); + } + } + + /** + * Maintains a cache of values for a single metric and reports + * statistics on the cached values. + */ + private static class StatsStore { + + private static final int NONE = -1; + private final AtomicLong[] values; + private final int size; + private int index; + + /** + * Constructs a new instance with the given cache size. + * + * @param size number of values to maintain in the cache. + */ + StatsStore(final int size) { + this.size = size; + this.values = new AtomicLong[size]; + Arrays.setAll(values, i -> new AtomicLong(NONE)); + } + + void add(final Duration value) { + add(value.toMillis()); + } + + /** + * Adds a value to the cache. If the cache is full, one of the + * existing values is replaced by the new value. + * + * @param value new value to add to the cache. + */ + synchronized void add(final long value) { + values[index].set(value); + index++; + if (index == size) { + index = 0; + } + } + + /** + * Gets the mean of the cached values. + * + * @return the mean of the cache, truncated to long + */ + public long getMean() { + double result = 0; + int counter = 0; + for (int i = 0; i < size; i++) { + final long value = values[i].get(); + if (value != NONE) { + counter++; + result = result * ((counter - 1) / (double) counter) + value / (double) counter; + } + } + return (long) result; + } + + /** + * Gets the mean Duration of the cached values. + * + * @return the mean Duration of the cache, truncated to long milliseconds of a Duration. + */ + Duration getMeanDuration() { + return Duration.ofMillis(getMean()); + } + + /** + * Gets the current values as a List. + * + * @return the current values as a List. + */ + synchronized List getValues() { + return Arrays.stream(values, 0, index).collect(Collectors.toList()); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("StatsStore ["); + // Only append what's been filled in. + builder.append(getValues()); + builder.append("], size="); + builder.append(size); + builder.append(", index="); + builder.append(index); + builder.append("]"); + return builder.toString(); + } + + } + + // Constants + /** + * The size of the caches used to store historical data for some attributes + * so that rolling means may be calculated. + */ + public static final int MEAN_TIMING_STATS_CACHE_SIZE = 100; + private static final String EVICTION_POLICY_TYPE_NAME = EvictionPolicy.class.getName(); + private static final Duration DEFAULT_REMOVE_ABANDONED_TIMEOUT = Duration.ofSeconds(Integer.MAX_VALUE); + // Configuration attributes + private volatile int maxTotal = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL; + private volatile boolean blockWhenExhausted = BaseObjectPoolConfig.DEFAULT_BLOCK_WHEN_EXHAUSTED; + private volatile Duration maxWaitDuration = BaseObjectPoolConfig.DEFAULT_MAX_WAIT; + private volatile boolean lifo = BaseObjectPoolConfig.DEFAULT_LIFO; + private final boolean fairness; + private volatile boolean testOnCreate = BaseObjectPoolConfig.DEFAULT_TEST_ON_CREATE; + private volatile boolean testOnBorrow = BaseObjectPoolConfig.DEFAULT_TEST_ON_BORROW; + private volatile boolean testOnReturn = BaseObjectPoolConfig.DEFAULT_TEST_ON_RETURN; + private volatile boolean testWhileIdle = BaseObjectPoolConfig.DEFAULT_TEST_WHILE_IDLE; + private volatile Duration durationBetweenEvictionRuns = BaseObjectPoolConfig.DEFAULT_DURATION_BETWEEN_EVICTION_RUNS; + private volatile int numTestsPerEvictionRun = BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN; + + private volatile Duration minEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION; + private volatile Duration softMinEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION; + private volatile EvictionPolicy evictionPolicy; + private volatile Duration evictorShutdownTimeoutDuration = BaseObjectPoolConfig.DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT; + // Internal (primarily state) attributes + final Object closeLock = new Object(); + volatile boolean closed; + + final Object evictionLock = new Object(); + private Evictor evictor; // @GuardedBy("evictionLock") + EvictionIterator evictionIterator; // @GuardedBy("evictionLock") + + /** + * Class loader for evictor thread to use since, in a JavaEE or similar + * environment, the context class loader for the evictor thread may not have + * visibility of the correct factory. See POOL-161. Uses a weak reference to + * avoid potential memory leaks if the Pool is discarded rather than closed. + */ + private final WeakReference factoryClassLoader; + // Monitoring (primarily JMX) attributes + private final ObjectName objectName; + private final String creationStackTrace; + private final AtomicLong borrowedCount = new AtomicLong(); + private final AtomicLong returnedCount = new AtomicLong(); + final AtomicLong createdCount = new AtomicLong(); + final AtomicLong destroyedCount = new AtomicLong(); + final AtomicLong destroyedByEvictorCount = new AtomicLong(); + final AtomicLong destroyedByBorrowValidationCount = new AtomicLong(); + + private final StatsStore activeTimes = new StatsStore(MEAN_TIMING_STATS_CACHE_SIZE); + private final StatsStore idleTimes = new StatsStore(MEAN_TIMING_STATS_CACHE_SIZE); + private final StatsStore waitTimes = new StatsStore(MEAN_TIMING_STATS_CACHE_SIZE); + + private final AtomicReference maxBorrowWaitDuration = new AtomicReference<>(Duration.ZERO); + + private volatile SwallowedExceptionListener swallowedExceptionListener; + private volatile boolean messageStatistics; + + /** Additional configuration properties for abandoned object tracking. */ + protected volatile AbandonedConfig abandonedConfig; + + /** + * Handles JMX registration (if required) and the initialization required for + * monitoring. + * + * @param config Pool configuration + * @param jmxNameBase The default base JMX name for the new pool unless + * overridden by the config + * @param jmxNamePrefix Prefix to be used for JMX name for the new pool + */ + public BaseGenericObjectPool(final BaseObjectPoolConfig config, + final String jmxNameBase, final String jmxNamePrefix) { + if (config.getJmxEnabled()) { + this.objectName = jmxRegister(config, jmxNameBase, jmxNamePrefix); + } else { + this.objectName = null; + } + + // Populate the creation stack trace + this.creationStackTrace = getStackTrace(new Exception()); + + // save the current TCCL (if any) to be used later by the evictor Thread + final ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + factoryClassLoader = null; + } else { + factoryClassLoader = new WeakReference<>(cl); + } + + fairness = config.getFairness(); + } + + /** + * Appends statistics if enabled. + *

    + * Statistics may not accurately reflect snapshot state at the time of the exception because we do not want to lock the pool when gathering this + * information. + *

    + * + * @param string The root string. + * @return The root string plus statistics. + */ + String appendStats(final String string) { + return messageStatistics ? string + ", " + getStatsString() : string; + } + + /** + * Verifies that the pool is open. + * @throws IllegalStateException if the pool is closed. + */ + final void assertOpen() throws IllegalStateException { + if (isClosed()) { + throw new IllegalStateException("Pool not open"); + } + } + + /** + * Closes the pool, destroys the remaining idle objects and, if registered + * in JMX, deregisters it. + */ + @Override + public abstract void close(); + + /** + * Creates a list of pooled objects to remove based on their state. + * @param abandonedConfig The abandoned configuration. + * @param allObjects PooledObject instances to consider. + * @return a list of pooled objects to remove based on their state. + */ + ArrayList> createRemoveList(final AbandonedConfig abandonedConfig, final Map, PooledObject> allObjects) { + final Instant timeout = Instant.now().minus(abandonedConfig.getRemoveAbandonedTimeoutDuration()); + final ArrayList> remove = new ArrayList<>(); + allObjects.values().forEach(pooledObject -> { + synchronized (pooledObject) { + if (pooledObject.getState() == PooledObjectState.ALLOCATED && + pooledObject.getLastUsedInstant().compareTo(timeout) <= 0) { + pooledObject.markAbandoned(); + remove.add(pooledObject); + } + } + }); + return remove; + } + + /** + * Tries to ensure that the configured minimum number of idle instances are + * available in the pool. + * @throws Exception if an error occurs creating idle instances + */ + abstract void ensureMinIdle() throws Exception; + + /** + * Perform {@code numTests} idle object eviction tests, evicting + * examined objects that meet the criteria for eviction. If + * {@code testWhileIdle} is true, examined objects are validated + * when visited (and removed if invalid); otherwise only objects that + * have been idle for more than {@code minEvicableIdleTimeMillis} + * are removed. + * + * @throws Exception when there is a problem evicting idle objects. + */ + public abstract void evict() throws Exception; + + /** + * Gets whether to block when the {@code borrowObject()} method is + * invoked when the pool is exhausted (the maximum number of "active" + * objects has been reached). + * + * @return {@code true} if {@code borrowObject()} should block + * when the pool is exhausted + * + * @see #setBlockWhenExhausted + */ + public final boolean getBlockWhenExhausted() { + return blockWhenExhausted; + } + + /** + * Gets the total number of objects successfully borrowed from this pool over the + * lifetime of the pool. + * @return the borrowed object count + */ + public final long getBorrowedCount() { + return borrowedCount.get(); + } + + /** + * Gets the total number of objects created for this pool over the lifetime of + * the pool. + * @return the created object count + */ + public final long getCreatedCount() { + return createdCount.get(); + } + + /** + * Gets the stack trace for the call that created this pool. JMX + * registration may trigger a memory leak so it is important that pools are + * deregistered when no longer used by calling the {@link #close()} method. + * This method is provided to assist with identifying code that creates but + * does not close it thereby creating a memory leak. + * @return pool creation stack trace + */ + public final String getCreationStackTrace() { + return creationStackTrace; + } + + /** + * Gets the total number of objects destroyed by this pool as a result of failing + * validation during {@code borrowObject()} over the lifetime of the + * pool. + * @return validation destroyed object count + */ + public final long getDestroyedByBorrowValidationCount() { + return destroyedByBorrowValidationCount.get(); + } + + /** + * Gets the total number of objects destroyed by the evictor associated with this + * pool over the lifetime of the pool. + * @return the evictor destroyed object count + */ + public final long getDestroyedByEvictorCount() { + return destroyedByEvictorCount.get(); + } + + /** + * Gets the total number of objects destroyed by this pool over the lifetime of + * the pool. + * @return the destroyed object count + */ + public final long getDestroyedCount() { + return destroyedCount.get(); + } + + /** + * Gets the duration to sleep between runs of the idle + * object evictor thread. When non-positive, no idle object evictor thread + * will be run. + * + * @return number of milliseconds to sleep between evictor runs + * + * @see #setTimeBetweenEvictionRuns + * @since 2.11.0 + */ + public final Duration getDurationBetweenEvictionRuns() { + return durationBetweenEvictionRuns; + } + + /** + * Gets the {@link EvictionPolicy} defined for this pool. + * + * @return the eviction policy + * @since 2.4 + * @since 2.6.0 Changed access from protected to public. + */ + public EvictionPolicy getEvictionPolicy() { + return evictionPolicy; + } + + /** + * Gets the name of the {@link EvictionPolicy} implementation that is + * used by this pool. + * + * @return The fully qualified class name of the {@link EvictionPolicy} + * + * @see #setEvictionPolicyClassName(String) + */ + public final String getEvictionPolicyClassName() { + return evictionPolicy.getClass().getName(); + } + + /** + * Gets the timeout that will be used when waiting for the Evictor to + * shutdown if this pool is closed and it is the only pool still using the + * the value for the Evictor. + * + * @return The timeout that will be used while waiting for + * the Evictor to shut down. + * @since 2.10.0 + * @deprecated Use {@link #getEvictorShutdownTimeoutDuration()}. + */ + @Deprecated + public final Duration getEvictorShutdownTimeout() { + return evictorShutdownTimeoutDuration; + } + + /** + * Gets the timeout that will be used when waiting for the Evictor to + * shutdown if this pool is closed and it is the only pool still using the + * the value for the Evictor. + * + * @return The timeout that will be used while waiting for + * the Evictor to shut down. + * @since 2.11.0 + */ + public final Duration getEvictorShutdownTimeoutDuration() { + return evictorShutdownTimeoutDuration; + } + + /** + * Gets the timeout that will be used when waiting for the Evictor to + * shutdown if this pool is closed and it is the only pool still using the + * the value for the Evictor. + * + * @return The timeout in milliseconds that will be used while waiting for + * the Evictor to shut down. + * @deprecated Use {@link #getEvictorShutdownTimeoutDuration()}. + */ + @Deprecated + public final long getEvictorShutdownTimeoutMillis() { + return evictorShutdownTimeoutDuration.toMillis(); + } + + /** + * Gets whether or not the pool serves threads waiting to borrow objects fairly. + * True means that waiting threads are served as if waiting in a FIFO queue. + * + * @return {@code true} if waiting threads are to be served + * by the pool in arrival order + */ + public final boolean getFairness() { + return fairness; + } + + /** + * Gets the name under which the pool has been registered with the + * platform MBean server or {@code null} if the pool has not been + * registered. + * @return the JMX name + */ + public final ObjectName getJmxName() { + return objectName; + } + + /** + * Gets whether the pool has LIFO (last in, first out) behavior with + * respect to idle objects - always returning the most recently used object + * from the pool, or as a FIFO (first in, first out) queue, where the pool + * always returns the oldest object in the idle object pool. + * + * @return {@code true} if the pool is configured with LIFO behavior + * or {@code false} if the pool is configured with FIFO + * behavior + * + * @see #setLifo + */ + public final boolean getLifo() { + return lifo; + } + + /** + * Gets whether this pool identifies and logs any abandoned objects. + * + * @return {@code true} if abandoned object removal is configured for this + * pool and removal events are to be logged otherwise {@code false} + * + * @see AbandonedConfig#getLogAbandoned() + * @since 2.11.0 + */ + public boolean getLogAbandoned() { + final AbandonedConfig ac = this.abandonedConfig; + return ac != null && ac.getLogAbandoned(); + } + + /** + * Gets the maximum time a thread has waited to borrow objects from the pool. + * + * @return maximum wait time in milliseconds since the pool was created + * @since 2.12.0 + */ + public final Duration getMaxBorrowWaitDuration() { + return maxBorrowWaitDuration.get(); + } + + /** + * Gets the maximum time a thread has waited to borrow objects from the pool. + * + * @return maximum wait time in milliseconds since the pool was created + * @deprecated Use {@link #getMaxBorrowWaitDuration()}. + */ + @Deprecated + public final long getMaxBorrowWaitTimeMillis() { + return maxBorrowWaitDuration.get().toMillis(); + } + + /** + * Gets the maximum number of objects that can be allocated by the pool + * (checked out to clients, or idle awaiting checkout) at a given time. When + * negative, there is no limit to the number of objects that can be + * managed by the pool at one time. + * + * @return the cap on the total number of object instances managed by the + * pool. + * @see #setMaxTotal + */ + public final int getMaxTotal() { + return maxTotal; + } + + /** + * Gets the maximum duration the + * {@code borrowObject()} method should block before throwing an + * exception when the pool is exhausted and + * {@link #getBlockWhenExhausted} is true. When less than 0, the + * {@code borrowObject()} method may block indefinitely. + * + * @return the maximum number of milliseconds {@code borrowObject()} + * will block. + * + * @see #setMaxWait + * @see #setBlockWhenExhausted + * @since 2.11.0 + */ + public final Duration getMaxWaitDuration() { + return maxWaitDuration; + } + + /** + * Gets the maximum amount of time (in milliseconds) the + * {@code borrowObject()} method should block before throwing an + * exception when the pool is exhausted and + * {@link #getBlockWhenExhausted} is true. When less than 0, the + * {@code borrowObject()} method may block indefinitely. + * + * @return the maximum number of milliseconds {@code borrowObject()} + * will block. + * + * @see #setMaxWait + * @see #setBlockWhenExhausted + * @deprecated Use {@link #getMaxWaitDuration()}. + */ + @Deprecated + public final long getMaxWaitMillis() { + return maxWaitDuration.toMillis(); + } + + /** + * Gets the mean time objects are active for based on the last {@link + * #MEAN_TIMING_STATS_CACHE_SIZE} objects returned to the pool. + * @return mean time an object has been checked out from the pool among + * recently returned objects. + * + * @since 2.12.0 + */ + public final Duration getMeanActiveDuration() { + return activeTimes.getMeanDuration(); + } + + /** + * Gets the mean time objects are active for based on the last {@link + * #MEAN_TIMING_STATS_CACHE_SIZE} objects returned to the pool. + * @return mean time an object has been checked out from the pool among + * recently returned objects. + * + * @deprecated Use {@link #getMeanActiveDuration()}. + */ + @Deprecated + public final long getMeanActiveTimeMillis() { + return activeTimes.getMean(); + } + + /** + * Gets the mean time threads wait to borrow an object based on the last {@link + * #MEAN_TIMING_STATS_CACHE_SIZE} objects borrowed from the pool. + * + * @return mean time in milliseconds that a recently served thread has had + * to wait to borrow an object from the pool. + * @since 2.12.0 + */ + public final Duration getMeanBorrowWaitDuration() { + return waitTimes.getMeanDuration(); + } + + /** + * Gets the mean time threads wait to borrow an object based on the last {@link + * #MEAN_TIMING_STATS_CACHE_SIZE} objects borrowed from the pool. + * + * @return mean time in milliseconds that a recently served thread has had + * to wait to borrow an object from the pool. + * @deprecated Use {@link #getMeanBorrowWaitDuration()}. + */ + @Deprecated + public final long getMeanBorrowWaitTimeMillis() { + return waitTimes.getMean(); + } + + /** + * Gets the mean time objects are idle for based on the last {@link + * #MEAN_TIMING_STATS_CACHE_SIZE} objects borrowed from the pool. + * + * @return mean time an object has been idle in the pool among recently + * borrowed objects. + * @since 2.12.0 + */ + public final Duration getMeanIdleDuration() { + return idleTimes.getMeanDuration(); + } + + /** + * Gets the mean time objects are idle for based on the last {@link + * #MEAN_TIMING_STATS_CACHE_SIZE} objects borrowed from the pool. + * + * @return mean time an object has been idle in the pool among recently + * borrowed objects. + * @deprecated Use {@link #getMeanIdleDuration()}. + */ + @Deprecated + public final long getMeanIdleTimeMillis() { + return idleTimes.getMean(); + } + + /** + * Gets whether to include statistics in exception messages. + *

    + * Statistics may not accurately reflect snapshot state at the time of the exception because we do not want to lock the pool when gathering this + * information. + *

    + * + * @return whether to include statistics in exception messages. + * @since 2.11.0 + */ + public boolean getMessageStatistics() { + return messageStatistics; + } + + /** + * Gets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}). When non-positive, + * no objects will be evicted from the pool due to idle time alone. + * + * @return minimum amount of time an object may sit idle in the pool before + * it is eligible for eviction + * + * @see #setMinEvictableIdleTimeMillis + * @see #setTimeBetweenEvictionRunsMillis + * @since 2.11.0 + */ + public final Duration getMinEvictableIdleDuration() { + return minEvictableIdleDuration; + } + + /** + * Gets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}). When non-positive, + * no objects will be evicted from the pool due to idle time alone. + * + * @return minimum amount of time an object may sit idle in the pool before + * it is eligible for eviction + * + * @see #setMinEvictableIdleTimeMillis + * @see #setTimeBetweenEvictionRunsMillis + * @since 2.10.0 + * @deprecated Use {@link #getMinEvictableIdleDuration()}. + */ + @Deprecated + public final Duration getMinEvictableIdleTime() { + return minEvictableIdleDuration; + } + + /** + * Gets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setTimeBetweenEvictionRunsMillis(long)}). When non-positive, + * no objects will be evicted from the pool due to idle time alone. + * + * @return minimum amount of time an object may sit idle in the pool before + * it is eligible for eviction + * + * @see #setMinEvictableIdleTimeMillis + * @see #setTimeBetweenEvictionRunsMillis + * @deprecated Use {@link #getMinEvictableIdleDuration()}. + */ + @Deprecated + public final long getMinEvictableIdleTimeMillis() { + return minEvictableIdleDuration.toMillis(); + } + + /** + * Gets the number of instances currently idle in this pool. + * @return count of instances available for checkout from the pool + */ + public abstract int getNumIdle(); + + /** + * Gets the maximum number of objects to examine during each run (if any) + * of the idle object evictor thread. When positive, the number of tests + * performed for a run will be the minimum of the configured value and the + * number of idle instances in the pool. When negative, the number of tests + * performed will be ceil({@link #getNumIdle}/ + * abs({@link #getNumTestsPerEvictionRun})) which means that when the + * value is {@code -n} roughly one nth of the idle objects will be + * tested per run. + * + * @return max number of objects to examine during each evictor run + * + * @see #setNumTestsPerEvictionRun + * @see #setTimeBetweenEvictionRunsMillis + */ + public final int getNumTestsPerEvictionRun() { + return numTestsPerEvictionRun; + } + + /** + * Gets whether a check is made for abandoned objects when an object is borrowed + * from this pool. + * + * @return {@code true} if abandoned object removal is configured to be + * activated by borrowObject otherwise {@code false} + * + * @see AbandonedConfig#getRemoveAbandonedOnBorrow() + * @since 2.11.0 + */ + public boolean getRemoveAbandonedOnBorrow() { + final AbandonedConfig ac = this.abandonedConfig; + return ac != null && ac.getRemoveAbandonedOnBorrow(); + } + + /** + * Gets whether a check is made for abandoned objects when the evictor runs. + * + * @return {@code true} if abandoned object removal is configured to be + * activated when the evictor runs otherwise {@code false} + * + * @see AbandonedConfig#getRemoveAbandonedOnMaintenance() + * @since 2.11.0 + */ + public boolean getRemoveAbandonedOnMaintenance() { + final AbandonedConfig ac = this.abandonedConfig; + return ac != null && ac.getRemoveAbandonedOnMaintenance(); + } + + /** + * Gets the timeout before which an object will be considered to be + * abandoned by this pool. + * + * @return The abandoned object timeout in seconds if abandoned object + * removal is configured for this pool; Integer.MAX_VALUE otherwise. + * + * @see AbandonedConfig#getRemoveAbandonedTimeoutDuration() + * @see AbandonedConfig#getRemoveAbandonedTimeoutDuration() + * @deprecated Use {@link #getRemoveAbandonedTimeoutDuration()}. + * @since 2.11.0 + */ + @Deprecated + public int getRemoveAbandonedTimeout() { + return (int) getRemoveAbandonedTimeoutDuration().getSeconds(); + } + + /** + * Gets the timeout before which an object will be considered to be + * abandoned by this pool. + * + * @return The abandoned object timeout in seconds if abandoned object + * removal is configured for this pool; Integer.MAX_VALUE otherwise. + * + * @see AbandonedConfig#getRemoveAbandonedTimeoutDuration() + * @since 2.11.0 + */ + public Duration getRemoveAbandonedTimeoutDuration() { + final AbandonedConfig ac = this.abandonedConfig; + return ac != null ? ac.getRemoveAbandonedTimeoutDuration() : DEFAULT_REMOVE_ABANDONED_TIMEOUT; + } + + /** + * Gets the total number of objects returned to this pool over the lifetime of + * the pool. This excludes attempts to return the same object multiple + * times. + * @return the returned object count + */ + public final long getReturnedCount() { + return returnedCount.get(); + } + + /** + * Gets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}), + * with the extra condition that at least {@code minIdle} object + * instances remain in the pool. This setting is overridden by + * {@link #getMinEvictableIdleTime} (that is, if + * {@link #getMinEvictableIdleTime} is positive, then + * {@link #getSoftMinEvictableIdleTime} is ignored). + * + * @return minimum amount of time an object may sit idle in the pool before + * it is eligible for eviction if minIdle instances are available + * + * @see #setSoftMinEvictableIdleDuration(Duration) + * @since 2.11.0 + */ + public final Duration getSoftMinEvictableIdleDuration() { + return softMinEvictableIdleDuration; + } + + /** + * Gets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}), + * with the extra condition that at least {@code minIdle} object + * instances remain in the pool. This setting is overridden by + * {@link #getMinEvictableIdleTime} (that is, if + * {@link #getMinEvictableIdleTime} is positive, then + * {@link #getSoftMinEvictableIdleTime} is ignored). + * + * @return minimum amount of time an object may sit idle in the pool before + * it is eligible for eviction if minIdle instances are available + * + * @see #setSoftMinEvictableIdleDuration(Duration) + * @since 2.10.0 + * @deprecated Use {@link #getSoftMinEvictableIdleDuration}. + */ + @Deprecated + public final Duration getSoftMinEvictableIdleTime() { + return softMinEvictableIdleDuration; + } + + /** + * Gets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setTimeBetweenEvictionRunsMillis(long)}), + * with the extra condition that at least {@code minIdle} object + * instances remain in the pool. This setting is overridden by + * {@link #getMinEvictableIdleTimeMillis} (that is, if + * {@link #getMinEvictableIdleTimeMillis} is positive, then + * {@link #getSoftMinEvictableIdleTimeMillis} is ignored). + * + * @return minimum amount of time an object may sit idle in the pool before + * it is eligible for eviction if minIdle instances are available + * + * @see #setSoftMinEvictableIdleTimeMillis + * @deprecated Use {@link #getSoftMinEvictableIdleTime()}. + */ + @Deprecated + public final long getSoftMinEvictableIdleTimeMillis() { + return softMinEvictableIdleDuration.toMillis(); + } + + /** + * Gets the stack trace of an exception as a string. + * @param e exception to trace + * @return exception stack trace as a string + */ + private String getStackTrace(final Exception e) { + // Need the exception in string form to prevent the retention of + // references to classes in the stack trace that could trigger a memory + // leak in a container environment. + final Writer w = new StringWriter(); + final PrintWriter pw = new PrintWriter(w); + e.printStackTrace(pw); + return w.toString(); + } + + /** + * Gets a statistics string. + * + * @return a statistics string. + */ + @SuppressWarnings("boxing") // Commons Pool uses auto-boxing + String getStatsString() { + // Simply listed in AB order. + return String.format( + "activeTimes=%s, blockWhenExhausted=%s, borrowedCount=%,d, closed=%s, createdCount=%,d, destroyedByBorrowValidationCount=%,d, " + + "destroyedByEvictorCount=%,d, evictorShutdownTimeoutDuration=%s, fairness=%s, idleTimes=%s, lifo=%s, maxBorrowWaitDuration=%s, " + + "maxTotal=%s, maxWaitDuration=%s, minEvictableIdleDuration=%s, numTestsPerEvictionRun=%s, returnedCount=%s, " + + "softMinEvictableIdleDuration=%s, testOnBorrow=%s, testOnCreate=%s, testOnReturn=%s, testWhileIdle=%s, " + + "durationBetweenEvictionRuns=%s, waitTimes=%s", + activeTimes.getValues(), blockWhenExhausted, borrowedCount.get(), closed, createdCount.get(), destroyedByBorrowValidationCount.get(), + destroyedByEvictorCount.get(), evictorShutdownTimeoutDuration, fairness, idleTimes.getValues(), lifo, maxBorrowWaitDuration.get(), + maxTotal, maxWaitDuration, minEvictableIdleDuration, numTestsPerEvictionRun, returnedCount, softMinEvictableIdleDuration, testOnBorrow, + testOnCreate, testOnReturn, testWhileIdle, durationBetweenEvictionRuns, waitTimes.getValues()); + } + + /** + * Gets the listener used (if any) to receive notifications of exceptions + * unavoidably swallowed by the pool. + * + * @return The listener or {@code null} for no listener + */ + public final SwallowedExceptionListener getSwallowedExceptionListener() { + return swallowedExceptionListener; + } + + /** + * Gets whether objects borrowed from the pool will be validated before + * being returned from the {@code borrowObject()} method. Validation is + * performed by the {@code validateObject()} method of the factory + * associated with the pool. If the object fails to validate, it will be + * removed from the pool and destroyed, and a new attempt will be made to + * borrow an object from the pool. + * + * @return {@code true} if objects are validated before being returned + * from the {@code borrowObject()} method + * + * @see #setTestOnBorrow + */ + public final boolean getTestOnBorrow() { + return testOnBorrow; + } + + /** + * Gets whether objects created for the pool will be validated before + * being returned from the {@code borrowObject()} method. Validation is + * performed by the {@code validateObject()} method of the factory + * associated with the pool. If the object fails to validate, then + * {@code borrowObject()} will fail. + * + * @return {@code true} if newly created objects are validated before + * being returned from the {@code borrowObject()} method + * + * @see #setTestOnCreate + * + * @since 2.2 + */ + public final boolean getTestOnCreate() { + return testOnCreate; + } + + /** + * Gets whether objects borrowed from the pool will be validated when + * they are returned to the pool via the {@code returnObject()} method. + * Validation is performed by the {@code validateObject()} method of + * the factory associated with the pool. Returning objects that fail validation + * are destroyed rather then being returned the pool. + * + * @return {@code true} if objects are validated on return to + * the pool via the {@code returnObject()} method + * + * @see #setTestOnReturn + */ + public final boolean getTestOnReturn() { + return testOnReturn; + } + + /** + * Gets whether objects sitting idle in the pool will be validated by the + * idle object evictor (if any - see + * {@link #setDurationBetweenEvictionRuns(Duration)}). Validation is performed + * by the {@code validateObject()} method of the factory associated + * with the pool. If the object fails to validate, it will be removed from + * the pool and destroyed. + * + * @return {@code true} if objects will be validated by the evictor + * + * @see #setTestWhileIdle + * @see #setTimeBetweenEvictionRunsMillis + */ + public final boolean getTestWhileIdle() { + return testWhileIdle; + } + + /** + * Gets the duration to sleep between runs of the idle + * object evictor thread. When non-positive, no idle object evictor thread + * will be run. + * + * @return number of milliseconds to sleep between evictor runs + * + * @see #setTimeBetweenEvictionRuns + * @since 2.10.0 + * @deprecated {@link #getDurationBetweenEvictionRuns()}. + */ + @Deprecated + public final Duration getTimeBetweenEvictionRuns() { + return durationBetweenEvictionRuns; + } + + /** + * Gets the number of milliseconds to sleep between runs of the idle + * object evictor thread. When non-positive, no idle object evictor thread + * will be run. + * + * @return number of milliseconds to sleep between evictor runs + * + * @see #setTimeBetweenEvictionRunsMillis + * @deprecated Use {@link #getDurationBetweenEvictionRuns()}. + */ + @Deprecated + public final long getTimeBetweenEvictionRunsMillis() { + return durationBetweenEvictionRuns.toMillis(); + } + + /** + * Tests whether or not abandoned object removal is configured for this pool. + * + * @return true if this pool is configured to detect and remove + * abandoned objects + * @since 2.11.0 + */ + public boolean isAbandonedConfig() { + return abandonedConfig != null; + } + + /** + * Tests whether this pool instance been closed. + * @return {@code true} when this pool has been closed. + */ + public final boolean isClosed() { + return closed; + } + + /** + * Registers the pool with the platform MBean server. + * The registered name will be + * {@code jmxNameBase + jmxNamePrefix + i} where i is the least + * integer greater than or equal to 1 such that the name is not already + * registered. Swallows MBeanRegistrationException, NotCompliantMBeanException + * returning null. + * + * @param config Pool configuration + * @param jmxNameBase default base JMX name for this pool + * @param jmxNamePrefix name prefix + * @return registered ObjectName, null if registration fails + */ + private ObjectName jmxRegister(final BaseObjectPoolConfig config, + final String jmxNameBase, String jmxNamePrefix) { + ObjectName newObjectName = null; + final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + int i = 1; + boolean registered = false; + String base = config.getJmxNameBase(); + if (base == null) { + base = jmxNameBase; + } + while (!registered) { + try { + ObjectName objName; + // Skip the numeric suffix for the first pool in case there is + // only one so the names are cleaner. + if (i == 1) { + objName = new ObjectName(base + jmxNamePrefix); + } else { + objName = new ObjectName(base + jmxNamePrefix + i); + } + if (!mbs.isRegistered(objName)) { + mbs.registerMBean(this, objName); + newObjectName = objName; + registered = true; + } else { + // Increment the index and try again + i++; + } + } catch (final MalformedObjectNameException e) { + if (BaseObjectPoolConfig.DEFAULT_JMX_NAME_PREFIX.equals( + jmxNamePrefix) && jmxNameBase.equals(base)) { + // Shouldn't happen. Skip registration if it does. + registered = true; + } else { + // Must be an invalid name. Use the defaults instead. + jmxNamePrefix = + BaseObjectPoolConfig.DEFAULT_JMX_NAME_PREFIX; + base = jmxNameBase; + } + } catch (final InstanceAlreadyExistsException e) { + // Increment the index and try again + i++; + } catch (final MBeanRegistrationException | NotCompliantMBeanException e) { + // Shouldn't happen. Skip registration if it does. + registered = true; + } + } + return newObjectName; + } + + /** + * Unregisters this pool's MBean. + */ + final void jmxUnregister() { + if (objectName != null) { + try { + ManagementFactory.getPlatformMBeanServer().unregisterMBean(objectName); + } catch (final MBeanRegistrationException | InstanceNotFoundException e) { + swallowException(e); + } + } + } + + /** + * Marks the object as returning to the pool. + * @param pooledObject instance to return to the keyed pool + */ + protected void markReturningState(final PooledObject pooledObject) { + synchronized (pooledObject) { + if (pooledObject.getState() != PooledObjectState.ALLOCATED) { + throw new IllegalStateException("Object has already been returned to this pool or is invalid"); + } + pooledObject.markReturning(); // Keep from being marked abandoned + } + } + + /** + * Sets the abandoned object removal configuration. + * + * @param abandonedConfig the new configuration to use. This is used by value. + * + * @see AbandonedConfig + * @since 2.11.0 + */ + public void setAbandonedConfig(final AbandonedConfig abandonedConfig) { + this.abandonedConfig = AbandonedConfig.copy(abandonedConfig); + } + + /** + * Sets whether to block when the {@code borrowObject()} method is + * invoked when the pool is exhausted (the maximum number of "active" + * objects has been reached). + * + * @param blockWhenExhausted {@code true} if + * {@code borrowObject()} should block + * when the pool is exhausted + * + * @see #getBlockWhenExhausted + */ + public final void setBlockWhenExhausted(final boolean blockWhenExhausted) { + this.blockWhenExhausted = blockWhenExhausted; + } + + /** + * Sets the receiver with the given configuration. + * + * @param config Initialization source. + */ + protected void setConfig(final BaseObjectPoolConfig config) { + setLifo(config.getLifo()); + setMaxWait(config.getMaxWaitDuration()); + setBlockWhenExhausted(config.getBlockWhenExhausted()); + setTestOnCreate(config.getTestOnCreate()); + setTestOnBorrow(config.getTestOnBorrow()); + setTestOnReturn(config.getTestOnReturn()); + setTestWhileIdle(config.getTestWhileIdle()); + setNumTestsPerEvictionRun(config.getNumTestsPerEvictionRun()); + setMinEvictableIdleDuration(config.getMinEvictableIdleDuration()); + setDurationBetweenEvictionRuns(config.getDurationBetweenEvictionRuns()); + setSoftMinEvictableIdleDuration(config.getSoftMinEvictableIdleDuration()); + final EvictionPolicy policy = config.getEvictionPolicy(); + if (policy == null) { + // Use the class name (pre-2.6.0 compatible) + setEvictionPolicyClassName(config.getEvictionPolicyClassName()); + } else { + // Otherwise, use the class (2.6.0 feature) + setEvictionPolicy(policy); + } + setEvictorShutdownTimeout(config.getEvictorShutdownTimeoutDuration()); + } + + /** + * Sets the number of milliseconds to sleep between runs of the idle object evictor thread. + *
      + *
    • When positive, the idle object evictor thread starts.
    • + *
    • When null or non-positive, no idle object evictor thread runs.
    • + *
    + * + * @param timeBetweenEvictionRuns + * duration to sleep between evictor runs + * + * @see #getDurationBetweenEvictionRuns() + * @since 2.12.0 + */ + public final void setDurationBetweenEvictionRuns(final Duration timeBetweenEvictionRuns) { + this.durationBetweenEvictionRuns = PoolImplUtils.nonNull(timeBetweenEvictionRuns, BaseObjectPoolConfig.DEFAULT_DURATION_BETWEEN_EVICTION_RUNS); + startEvictor(this.durationBetweenEvictionRuns); + } + + /** + * Sets the eviction policy for this pool. + * + * @param evictionPolicy + * the eviction policy for this pool. + * @since 2.6.0 + */ + public void setEvictionPolicy(final EvictionPolicy evictionPolicy) { + this.evictionPolicy = evictionPolicy; + } + + /** + * Sets the eviction policy. + * + * @param className Eviction policy class name. + * @param classLoader Load the class from this class loader. + * @throws LinkageError if the linkage fails + * @throws ExceptionInInitializerError if the initialization provoked by this method fails + * @throws ClassNotFoundException if the class cannot be located by the specified class loader + * @throws IllegalAccessException if this {@code Constructor} object is enforcing Java language access control and the underlying constructor is + * inaccessible. + * @throws IllegalArgumentException if the number of actual and formal parameters differ; if an unwrapping conversion for primitive arguments fails; or if, + * after possible unwrapping, a parameter value cannot be converted to the corresponding formal parameter type by a method invocation conversion; if + * this constructor pertains to an enum type. + * @throws InstantiationException if the class that declares the underlying constructor represents an abstract class. + * @throws InvocationTargetException if the underlying constructor throws an exception. + * @throws ExceptionInInitializerError if the initialization provoked by this method fails. + * @throws NoSuchMethodException if a matching method is not found. + * @throws SecurityException If a security manage is present and the caller's class loader is not the same as or an ancestor of the class loader for the + * current class and invocation of {@link SecurityManager#checkPackageAccess s.checkPackageAccess()} denies access to the package of this class. + */ + @SuppressWarnings("unchecked") + private void setEvictionPolicy(final String className, final ClassLoader classLoader) + throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { + final Class clazz = Class.forName(className, true, classLoader); + final Object policy = clazz.getConstructor().newInstance(); + this.evictionPolicy = (EvictionPolicy) policy; + } + + /** + * Sets the name of the {@link EvictionPolicy} implementation that is used by this pool. The Pool will attempt to + * load the class using the thread context class loader. If that fails, the use the class loader for the + * {@link EvictionPolicy} interface. + * + * @param evictionPolicyClassName + * the fully qualified class name of the new eviction policy + * + * @see #getEvictionPolicyClassName() + * @since 2.6.0 If loading the class using the thread context class loader fails, use the class loader for the + * {@link EvictionPolicy} interface. + */ + public final void setEvictionPolicyClassName(final String evictionPolicyClassName) { + setEvictionPolicyClassName(evictionPolicyClassName, Thread.currentThread().getContextClassLoader()); + } + + /** + * Sets the name of the {@link EvictionPolicy} implementation that is used by this pool. The Pool will attempt to + * load the class using the given class loader. If that fails, use the class loader for the {@link EvictionPolicy} + * interface. + * + * @param evictionPolicyClassName + * the fully qualified class name of the new eviction policy + * @param classLoader + * the class loader to load the given {@code evictionPolicyClassName}. + * + * @see #getEvictionPolicyClassName() + * @since 2.6.0 If loading the class using the given class loader fails, use the class loader for the + * {@link EvictionPolicy} interface. + */ + public final void setEvictionPolicyClassName(final String evictionPolicyClassName, final ClassLoader classLoader) { + // Getting epClass here and now best matches the caller's environment + final Class epClass = EvictionPolicy.class; + final ClassLoader epClassLoader = epClass.getClassLoader(); + try { + try { + setEvictionPolicy(evictionPolicyClassName, classLoader); + } catch (final ClassCastException | ClassNotFoundException e) { + setEvictionPolicy(evictionPolicyClassName, epClassLoader); + } + } catch (final ClassCastException e) { + throw new IllegalArgumentException("Class " + evictionPolicyClassName + " from class loaders [" + + classLoader + ", " + epClassLoader + "] do not implement " + EVICTION_POLICY_TYPE_NAME); + } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException | + InvocationTargetException | NoSuchMethodException e) { + throw new IllegalArgumentException( + "Unable to create " + EVICTION_POLICY_TYPE_NAME + " instance of type " + evictionPolicyClassName, + e); + } + } + + /** + * Sets the timeout that will be used when waiting for the Evictor to shutdown if this pool is closed and it is the + * only pool still using the value for the Evictor. + * + * @param evictorShutdownTimeout the timeout in milliseconds that will be used while waiting for the Evictor + * to shut down. + * @since 2.10.0 + */ + public final void setEvictorShutdownTimeout(final Duration evictorShutdownTimeout) { + this.evictorShutdownTimeoutDuration = PoolImplUtils.nonNull(evictorShutdownTimeout, BaseObjectPoolConfig.DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT); + } + + /** + * Sets the timeout that will be used when waiting for the Evictor to shutdown if this pool is closed and it is the + * only pool still using the value for the Evictor. + * + * @param evictorShutdownTimeoutMillis the timeout in milliseconds that will be used while waiting for the Evictor + * to shut down. + * @deprecated Use {@link #setEvictorShutdownTimeout(Duration)}. + */ + @Deprecated + public final void setEvictorShutdownTimeoutMillis(final long evictorShutdownTimeoutMillis) { + setEvictorShutdownTimeout(Duration.ofMillis(evictorShutdownTimeoutMillis)); + } + + /** + * Sets whether the pool has LIFO (last in, first out) behavior with + * respect to idle objects - always returning the most recently used object + * from the pool, or as a FIFO (first in, first out) queue, where the pool + * always returns the oldest object in the idle object pool. + * + * @param lifo {@code true} if the pool is to be configured with LIFO + * behavior or {@code false} if the pool is to be + * configured with FIFO behavior + * + * @see #getLifo() + */ + public final void setLifo(final boolean lifo) { + this.lifo = lifo; + } + + /** + * Sets the cap on the number of objects that can be allocated by the pool + * (checked out to clients, or idle awaiting checkout) at a given time. Use + * a negative value for no limit. + * + * @param maxTotal The cap on the total number of object instances managed + * by the pool. Negative values mean that there is no limit + * to the number of objects allocated by the pool. + * + * @see #getMaxTotal + */ + public final void setMaxTotal(final int maxTotal) { + this.maxTotal = maxTotal; + } + + /** + * Sets the maximum duration the + * {@code borrowObject()} method should block before throwing an + * exception when the pool is exhausted and + * {@link #getBlockWhenExhausted} is true. When less than 0, the + * {@code borrowObject()} method may block indefinitely. + * + * @param maxWaitDuration the maximum duration + * {@code borrowObject()} will block or negative + * for indefinitely. + * + * @see #getMaxWaitDuration + * @see #setBlockWhenExhausted + * @since 2.11.0 + */ + public final void setMaxWait(final Duration maxWaitDuration) { + this.maxWaitDuration = PoolImplUtils.nonNull(maxWaitDuration, BaseObjectPoolConfig.DEFAULT_MAX_WAIT); + } + + /** + * Sets the maximum amount of time (in milliseconds) the + * {@code borrowObject()} method should block before throwing an + * exception when the pool is exhausted and + * {@link #getBlockWhenExhausted} is true. When less than 0, the + * {@code borrowObject()} method may block indefinitely. + * + * @param maxWaitMillis the maximum number of milliseconds + * {@code borrowObject()} will block or negative + * for indefinitely. + * + * @see #getMaxWaitDuration + * @see #setBlockWhenExhausted + * @deprecated Use {@link #setMaxWait}. + */ + @Deprecated + public final void setMaxWaitMillis(final long maxWaitMillis) { + setMaxWait(Duration.ofMillis(maxWaitMillis)); + } + + /** + * Sets whether to include statistics in exception messages. + *

    + * Statistics may not accurately reflect snapshot state at the time of the exception because we do not want to lock the pool when gathering this + * information. + *

    + * + * @param messagesDetails whether to include statistics in exception messages. + * @since 2.11.0 + */ + public void setMessagesStatistics(final boolean messagesDetails) { + this.messageStatistics = messagesDetails; + } + + /** + * Sets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}). When non-positive, + * no objects will be evicted from the pool due to idle time alone. + * + * @param minEvictableIdleTime + * minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction + * + * @see #getMinEvictableIdleTime + * @see #setTimeBetweenEvictionRuns + * @since 2.11.0 + * @deprecated Use {@link #setMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public final void setMinEvictableIdle(final Duration minEvictableIdleTime) { + this.minEvictableIdleDuration = PoolImplUtils.nonNull(minEvictableIdleTime, BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION); + } + + /** + * Sets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}). When non-positive, + * no objects will be evicted from the pool due to idle time alone. + * + * @param minEvictableIdleTime + * minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction + * + * @see #getMinEvictableIdleTime + * @see #setTimeBetweenEvictionRuns + * @since 2.12.0 + */ + public final void setMinEvictableIdleDuration(final Duration minEvictableIdleTime) { + this.minEvictableIdleDuration = PoolImplUtils.nonNull(minEvictableIdleTime, BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION); + } + + /** + * Sets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}). When non-positive, + * no objects will be evicted from the pool due to idle time alone. + * + * @param minEvictableIdleTime + * minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction + * + * @see #getMinEvictableIdleTime + * @see #setTimeBetweenEvictionRuns + * @since 2.10.0 + * @deprecated Use {@link #setMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public final void setMinEvictableIdleTime(final Duration minEvictableIdleTime) { + this.minEvictableIdleDuration = PoolImplUtils.nonNull(minEvictableIdleTime, BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION); + } + + /** + * Sets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setTimeBetweenEvictionRunsMillis(long)}). When non-positive, + * no objects will be evicted from the pool due to idle time alone. + * + * @param minEvictableIdleTimeMillis + * minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction + * + * @see #getMinEvictableIdleTimeMillis + * @see #setTimeBetweenEvictionRunsMillis + * @deprecated Use {@link #setMinEvictableIdleTime(Duration)}. + */ + @Deprecated + public final void setMinEvictableIdleTimeMillis(final long minEvictableIdleTimeMillis) { + setMinEvictableIdleTime(Duration.ofMillis(minEvictableIdleTimeMillis)); + } + + /** + * Sets the maximum number of objects to examine during each run (if any) + * of the idle object evictor thread. When positive, the number of tests + * performed for a run will be the minimum of the configured value and the + * number of idle instances in the pool. When negative, the number of tests + * performed will be ceil({@link #getNumIdle}/ + * abs({@link #getNumTestsPerEvictionRun})) which means that when the + * value is {@code -n} roughly one nth of the idle objects will be + * tested per run. + * + * @param numTestsPerEvictionRun + * max number of objects to examine during each evictor run + * + * @see #getNumTestsPerEvictionRun + * @see #setTimeBetweenEvictionRunsMillis + */ + public final void setNumTestsPerEvictionRun(final int numTestsPerEvictionRun) { + this.numTestsPerEvictionRun = numTestsPerEvictionRun; + } + + /** + * Sets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}), + * with the extra condition that at least {@code minIdle} object + * instances remain in the pool. This setting is overridden by + * {@link #getMinEvictableIdleTime} (that is, if + * {@link #getMinEvictableIdleTime} is positive, then + * {@link #getSoftMinEvictableIdleTime} is ignored). + * + * @param softMinEvictableIdleTime + * minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction if minIdle instances are + * available + * + * @see #getSoftMinEvictableIdleTimeMillis + * @since 2.11.0 + * @deprecated Use {@link #setSoftMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public final void setSoftMinEvictableIdle(final Duration softMinEvictableIdleTime) { + this.softMinEvictableIdleDuration = PoolImplUtils.nonNull(softMinEvictableIdleTime, BaseObjectPoolConfig.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION); + } + + /** + * Sets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}), + * with the extra condition that at least {@code minIdle} object + * instances remain in the pool. This setting is overridden by + * {@link #getMinEvictableIdleTime} (that is, if + * {@link #getMinEvictableIdleTime} is positive, then + * {@link #getSoftMinEvictableIdleTime} is ignored). + * + * @param softMinEvictableIdleTime + * minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction if minIdle instances are + * available + * + * @see #getSoftMinEvictableIdleTimeMillis + * @since 2.12.0 + */ + public final void setSoftMinEvictableIdleDuration(final Duration softMinEvictableIdleTime) { + this.softMinEvictableIdleDuration = PoolImplUtils.nonNull(softMinEvictableIdleTime, BaseObjectPoolConfig.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION); + } + + /** + * Sets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setDurationBetweenEvictionRuns(Duration)}), + * with the extra condition that at least {@code minIdle} object + * instances remain in the pool. This setting is overridden by + * {@link #getMinEvictableIdleTime} (that is, if + * {@link #getMinEvictableIdleTime} is positive, then + * {@link #getSoftMinEvictableIdleTime} is ignored). + * + * @param softMinEvictableIdleTime + * minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction if minIdle instances are + * available + * + * @see #getSoftMinEvictableIdleTimeMillis + * @since 2.10.0 + * @deprecated Use {@link #setSoftMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public final void setSoftMinEvictableIdleTime(final Duration softMinEvictableIdleTime) { + this.softMinEvictableIdleDuration = PoolImplUtils.nonNull(softMinEvictableIdleTime, BaseObjectPoolConfig.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION); + } + + /** + * Sets the minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction by the idle object evictor (if any - + * see {@link #setTimeBetweenEvictionRunsMillis(long)}), + * with the extra condition that at least {@code minIdle} object + * instances remain in the pool. This setting is overridden by + * {@link #getMinEvictableIdleTimeMillis} (that is, if + * {@link #getMinEvictableIdleTimeMillis} is positive, then + * {@link #getSoftMinEvictableIdleTimeMillis} is ignored). + * + * @param softMinEvictableIdleTimeMillis + * minimum amount of time an object may sit idle in the pool + * before it is eligible for eviction if minIdle instances are + * available + * + * @see #getSoftMinEvictableIdleTimeMillis + * @deprecated Use {@link #setSoftMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public final void setSoftMinEvictableIdleTimeMillis(final long softMinEvictableIdleTimeMillis) { + setSoftMinEvictableIdleTime(Duration.ofMillis(softMinEvictableIdleTimeMillis)); + } + + /** + * Sets the listener used (if any) to receive notifications of exceptions + * unavoidably swallowed by the pool. + * + * @param swallowedExceptionListener The listener or {@code null} + * for no listener + */ + public final void setSwallowedExceptionListener( + final SwallowedExceptionListener swallowedExceptionListener) { + this.swallowedExceptionListener = swallowedExceptionListener; + } + + /** + * Sets whether objects borrowed from the pool will be validated before + * being returned from the {@code borrowObject()} method. Validation is + * performed by the {@code validateObject()} method of the factory + * associated with the pool. If the object fails to validate, it will be + * removed from the pool and destroyed, and a new attempt will be made to + * borrow an object from the pool. + * + * @param testOnBorrow {@code true} if objects should be validated + * before being returned from the + * {@code borrowObject()} method + * + * @see #getTestOnBorrow + */ + public final void setTestOnBorrow(final boolean testOnBorrow) { + this.testOnBorrow = testOnBorrow; + } + + /** + * Sets whether objects created for the pool will be validated before + * being returned from the {@code borrowObject()} method. Validation is + * performed by the {@code validateObject()} method of the factory + * associated with the pool. If the object fails to validate, then + * {@code borrowObject()} will fail. + * + * @param testOnCreate {@code true} if newly created objects should be + * validated before being returned from the + * {@code borrowObject()} method + * + * @see #getTestOnCreate + * + * @since 2.2 + */ + public final void setTestOnCreate(final boolean testOnCreate) { + this.testOnCreate = testOnCreate; + } + + /** + * Sets whether objects borrowed from the pool will be validated when + * they are returned to the pool via the {@code returnObject()} method. + * Validation is performed by the {@code validateObject()} method of + * the factory associated with the pool. Returning objects that fail validation + * are destroyed rather then being returned the pool. + * + * @param testOnReturn {@code true} if objects are validated on + * return to the pool via the + * {@code returnObject()} method + * + * @see #getTestOnReturn + */ + public final void setTestOnReturn(final boolean testOnReturn) { + this.testOnReturn = testOnReturn; + } + + /** + * Sets whether objects sitting idle in the pool will be validated by the + * idle object evictor (if any - see + * {@link #setDurationBetweenEvictionRuns(Duration)}). Validation is performed + * by the {@code validateObject()} method of the factory associated + * with the pool. If the object fails to validate, it will be removed from + * the pool and destroyed. Note that setting this property has no effect + * unless the idle object evictor is enabled by setting + * {@code timeBetweenEvictionRunsMillis} to a positive value. + * + * @param testWhileIdle + * {@code true} so objects will be validated by the evictor + * + * @see #getTestWhileIdle + * @see #setTimeBetweenEvictionRuns + */ + public final void setTestWhileIdle(final boolean testWhileIdle) { + this.testWhileIdle = testWhileIdle; + } + + /** + * Sets the number of milliseconds to sleep between runs of the idle object evictor thread. + *
      + *
    • When positive, the idle object evictor thread starts.
    • + *
    • When non-positive, no idle object evictor thread runs.
    • + *
    + * + * @param timeBetweenEvictionRuns + * duration to sleep between evictor runs + * + * @see #getDurationBetweenEvictionRuns() + * @since 2.10.0 + * @deprecated Use {@link #setDurationBetweenEvictionRuns(Duration)}. + */ + @Deprecated + public final void setTimeBetweenEvictionRuns(final Duration timeBetweenEvictionRuns) { + this.durationBetweenEvictionRuns = PoolImplUtils.nonNull(timeBetweenEvictionRuns, BaseObjectPoolConfig.DEFAULT_DURATION_BETWEEN_EVICTION_RUNS); + startEvictor(this.durationBetweenEvictionRuns); + } + + /** + * Sets the number of milliseconds to sleep between runs of the idle object evictor thread. + *
      + *
    • When positive, the idle object evictor thread starts.
    • + *
    • When non-positive, no idle object evictor thread runs.
    • + *
    + * + * @param timeBetweenEvictionRunsMillis + * number of milliseconds to sleep between evictor runs + * + * @see #getDurationBetweenEvictionRuns() + * @deprecated Use {@link #setDurationBetweenEvictionRuns(Duration)}. + */ + @Deprecated + public final void setTimeBetweenEvictionRunsMillis(final long timeBetweenEvictionRunsMillis) { + setTimeBetweenEvictionRuns(Duration.ofMillis(timeBetweenEvictionRunsMillis)); + } + + /** + *

    Starts the evictor with the given delay. If there is an evictor + * running when this method is called, it is stopped and replaced with a + * new evictor with the specified delay.

    + * + *

    This method needs to be final, since it is called from a constructor. + * See POOL-195.

    + * + * @param delay time in milliseconds before start and between eviction runs + */ + final void startEvictor(final Duration delay) { + synchronized (evictionLock) { + final boolean isPositiverDelay = PoolImplUtils.isPositive(delay); + if (evictor == null) { // Starting evictor for the first time or after a cancel + if (isPositiverDelay) { // Starting new evictor + evictor = new Evictor(); + EvictionTimer.schedule(evictor, delay, delay); + } + } else if (isPositiverDelay) { // Stop or restart of existing evictor: Restart + synchronized (EvictionTimer.class) { // Ensure no cancel can happen between cancel / schedule calls + EvictionTimer.cancel(evictor, evictorShutdownTimeoutDuration, true); + evictor = null; + evictionIterator = null; + evictor = new Evictor(); + EvictionTimer.schedule(evictor, delay, delay); + } + } else { // Stopping evictor + EvictionTimer.cancel(evictor, evictorShutdownTimeoutDuration, false); + } + } + } + + /** + * Stops the evictor. + */ + void stopEvictor() { + startEvictor(Duration.ofMillis(-1L)); + } + + /** + * Swallows an exception and notifies the configured listener for swallowed + * exceptions queue. + * + * @param swallowException exception to be swallowed + */ + final void swallowException(final Exception swallowException) { + final SwallowedExceptionListener listener = getSwallowedExceptionListener(); + + if (listener == null) { + return; + } + + try { + listener.onSwallowException(swallowException); + } catch (final VirtualMachineError e) { + throw e; + } catch (final Throwable ignored) { + // Ignore. Enjoy the irony. + } + } + + @Override + protected void toStringAppendFields(final StringBuilder builder) { + builder.append("maxTotal="); + builder.append(maxTotal); + builder.append(", blockWhenExhausted="); + builder.append(blockWhenExhausted); + builder.append(", maxWaitDuration="); + builder.append(maxWaitDuration); + builder.append(", lifo="); + builder.append(lifo); + builder.append(", fairness="); + builder.append(fairness); + builder.append(", testOnCreate="); + builder.append(testOnCreate); + builder.append(", testOnBorrow="); + builder.append(testOnBorrow); + builder.append(", testOnReturn="); + builder.append(testOnReturn); + builder.append(", testWhileIdle="); + builder.append(testWhileIdle); + builder.append(", durationBetweenEvictionRuns="); + builder.append(durationBetweenEvictionRuns); + builder.append(", numTestsPerEvictionRun="); + builder.append(numTestsPerEvictionRun); + builder.append(", minEvictableIdleTimeDuration="); + builder.append(minEvictableIdleDuration); + builder.append(", softMinEvictableIdleTimeDuration="); + builder.append(softMinEvictableIdleDuration); + builder.append(", evictionPolicy="); + builder.append(evictionPolicy); + builder.append(", closeLock="); + builder.append(closeLock); + builder.append(", closed="); + builder.append(closed); + builder.append(", evictionLock="); + builder.append(evictionLock); + builder.append(", evictor="); + builder.append(evictor); + builder.append(", evictionIterator="); + builder.append(evictionIterator); + builder.append(", factoryClassLoader="); + builder.append(factoryClassLoader); + builder.append(", oname="); + builder.append(objectName); + builder.append(", creationStackTrace="); + builder.append(creationStackTrace); + builder.append(", borrowedCount="); + builder.append(borrowedCount); + builder.append(", returnedCount="); + builder.append(returnedCount); + builder.append(", createdCount="); + builder.append(createdCount); + builder.append(", destroyedCount="); + builder.append(destroyedCount); + builder.append(", destroyedByEvictorCount="); + builder.append(destroyedByEvictorCount); + builder.append(", destroyedByBorrowValidationCount="); + builder.append(destroyedByBorrowValidationCount); + builder.append(", activeTimes="); + builder.append(activeTimes); + builder.append(", idleTimes="); + builder.append(idleTimes); + builder.append(", waitTimes="); + builder.append(waitTimes); + builder.append(", maxBorrowWaitDuration="); + builder.append(maxBorrowWaitDuration); + builder.append(", swallowedExceptionListener="); + builder.append(swallowedExceptionListener); + } + + /** + * Updates statistics after an object is borrowed from the pool. + * + * @param p object borrowed from the pool + * @param waitDuration that the borrowing thread had to wait + */ + final void updateStatsBorrow(final PooledObject p, final Duration waitDuration) { + borrowedCount.incrementAndGet(); + idleTimes.add(p.getIdleDuration()); + waitTimes.add(waitDuration); + + // lock-free optimistic-locking maximum + Duration currentMaxDuration; + do { + currentMaxDuration = maxBorrowWaitDuration.get(); + if (currentMaxDuration.compareTo(waitDuration) >= 0) { + break; + } + } while (!maxBorrowWaitDuration.compareAndSet(currentMaxDuration, waitDuration)); + } + + /** + * Updates statistics after an object is returned to the pool. + * + * @param activeTime the amount of time (in milliseconds) that the returning + * object was checked out + */ + final void updateStatsReturn(final Duration activeTime) { + returnedCount.incrementAndGet(); + activeTimes.add(activeTime); + } + + +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java b/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java new file mode 100644 index 0000000..cd40a8b --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java @@ -0,0 +1,959 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.time.Duration; + +import org.apache.tomcat.dbcp.pool2.BaseObject; + +/** + * Provides the implementation for the common attributes shared by the sub-classes. New instances of this class will be created using the defaults defined by + * the public constants. + *

    + * This class is not thread-safe. + *

    + * + * @param Type of element pooled. + * @since 2.0 + */ +public abstract class BaseObjectPoolConfig extends BaseObject implements Cloneable { + + /** + * The default value for the {@code lifo} configuration attribute. + * + * @see GenericObjectPool#getLifo() + * @see GenericKeyedObjectPool#getLifo() + */ + public static final boolean DEFAULT_LIFO = true; + + /** + * The default value for the {@code fairness} configuration attribute. + * + * @see GenericObjectPool#getFairness() + * @see GenericKeyedObjectPool#getFairness() + */ + public static final boolean DEFAULT_FAIRNESS = false; + + /** + * The default value for the {@code maxWait} configuration attribute. + * + * @see GenericObjectPool#getMaxWaitDuration() + * @see GenericKeyedObjectPool#getMaxWaitDuration() + * @deprecated Use {@link #DEFAULT_MAX_WAIT}. + */ + @Deprecated + public static final long DEFAULT_MAX_WAIT_MILLIS = -1L; + + /** + * The default value for the {@code maxWait} configuration attribute. + * + * @see GenericObjectPool#getMaxWaitDuration() + * @see GenericKeyedObjectPool#getMaxWaitDuration() + * @since 2.10.0 + */ + public static final Duration DEFAULT_MAX_WAIT = Duration.ofMillis(DEFAULT_MAX_WAIT_MILLIS); + + /** + * The default value for the {@code minEvictableIdleDuration} configuration attribute. + * + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @deprecated Use {@link #DEFAULT_MIN_EVICTABLE_IDLE_TIME}. + */ + @Deprecated + public static final long DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS = 1000L * 60L * 30L; + + /** + * The default value for the {@code minEvictableIdleDuration} configuration attribute. + * + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @since 2.11.0 + */ + public static final Duration DEFAULT_MIN_EVICTABLE_IDLE_DURATION = Duration.ofMillis(DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS); + + /** + * The default value for the {@code minEvictableIdleDuration} configuration attribute. + * + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @since 2.10.0 + * @deprecated Use {@link #DEFAULT_MIN_EVICTABLE_IDLE_DURATION}. + */ + @Deprecated + public static final Duration DEFAULT_MIN_EVICTABLE_IDLE_TIME = Duration.ofMillis(DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS); + + /** + * The default value for the {@code softMinEvictableIdleTime} configuration attribute. + * + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @deprecated Use {@link #DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME}. + */ + @Deprecated + public static final long DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS = -1; + + /** + * The default value for the {@code softMinEvictableIdleTime} configuration attribute. + * + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @since 2.10.0 + * @deprecated Use {@link #DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION}. + */ + @Deprecated + public static final Duration DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME = Duration.ofMillis(DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS); + + /** + * The default value for the {@code softMinEvictableIdleTime} configuration attribute. + * + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @since 2.11.0 + */ + public static final Duration DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION = Duration.ofMillis(DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS); + + /** + * The default value for {@code evictorShutdownTimeout} configuration attribute. + * + * @see GenericObjectPool#getEvictorShutdownTimeoutDuration() + * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration() + * @deprecated Use {@link #DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT}. + */ + @Deprecated + public static final long DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS = 10L * 1000L; + + /** + * The default value for {@code evictorShutdownTimeout} configuration attribute. + * + * @see GenericObjectPool#getEvictorShutdownTimeoutDuration() + * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration() + * @since 2.10.0 + */ + public static final Duration DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT = Duration.ofMillis(DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS); + + /** + * The default value for the {@code numTestsPerEvictionRun} configuration attribute. + * + * @see GenericObjectPool#getNumTestsPerEvictionRun() + * @see GenericKeyedObjectPool#getNumTestsPerEvictionRun() + */ + public static final int DEFAULT_NUM_TESTS_PER_EVICTION_RUN = 3; + + /** + * The default value for the {@code testOnCreate} configuration attribute. + * + * @see GenericObjectPool#getTestOnCreate() + * @see GenericKeyedObjectPool#getTestOnCreate() + * + * @since 2.2 + */ + public static final boolean DEFAULT_TEST_ON_CREATE = false; + + /** + * The default value for the {@code testOnBorrow} configuration attribute. + * + * @see GenericObjectPool#getTestOnBorrow() + * @see GenericKeyedObjectPool#getTestOnBorrow() + */ + public static final boolean DEFAULT_TEST_ON_BORROW = false; + + /** + * The default value for the {@code testOnReturn} configuration attribute. + * + * @see GenericObjectPool#getTestOnReturn() + * @see GenericKeyedObjectPool#getTestOnReturn() + */ + public static final boolean DEFAULT_TEST_ON_RETURN = false; + + /** + * The default value for the {@code testWhileIdle} configuration attribute. + * + * @see GenericObjectPool#getTestWhileIdle() + * @see GenericKeyedObjectPool#getTestWhileIdle() + */ + public static final boolean DEFAULT_TEST_WHILE_IDLE = false; + + /** + * The default value for the {@code timeBetweenEvictionRuns} configuration attribute. + * + * @see GenericObjectPool#getDurationBetweenEvictionRuns() + * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns() + * @deprecated Use {@link #DEFAULT_TIME_BETWEEN_EVICTION_RUNS}. + */ + @Deprecated + public static final long DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS = -1L; + + /** + * The default value for the {@code timeBetweenEvictionRuns} configuration attribute. + * + * @see GenericObjectPool#getDurationBetweenEvictionRuns() + * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns() + * @since 2.12.0 + */ + public static final Duration DEFAULT_DURATION_BETWEEN_EVICTION_RUNS = Duration.ofMillis(DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS); + + /** + * The default value for the {@code timeBetweenEvictionRuns} configuration attribute. + * + * @see GenericObjectPool#getDurationBetweenEvictionRuns() + * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns() + * @deprecated Use {@link #DEFAULT_DURATION_BETWEEN_EVICTION_RUNS}. + */ + @Deprecated + public static final Duration DEFAULT_TIME_BETWEEN_EVICTION_RUNS = Duration.ofMillis(DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS); + + /** + * The default value for the {@code blockWhenExhausted} configuration attribute. + * + * @see GenericObjectPool#getBlockWhenExhausted() + * @see GenericKeyedObjectPool#getBlockWhenExhausted() + */ + public static final boolean DEFAULT_BLOCK_WHEN_EXHAUSTED = true; + + /** + * The default value for enabling JMX for pools created with a configuration instance. + */ + public static final boolean DEFAULT_JMX_ENABLE = true; + + /** + * The default value for the prefix used to name JMX enabled pools created with a configuration instance. + * + * @see GenericObjectPool#getJmxName() + * @see GenericKeyedObjectPool#getJmxName() + */ + public static final String DEFAULT_JMX_NAME_PREFIX = "pool"; + + /** + * The default value for the base name to use to name JMX enabled pools created with a configuration instance. The default is {@code null} which means the + * pool will provide the base name to use. + * + * @see GenericObjectPool#getJmxName() + * @see GenericKeyedObjectPool#getJmxName() + */ + public static final String DEFAULT_JMX_NAME_BASE = null; + + /** + * The default value for the {@code evictionPolicyClassName} configuration attribute. + * + * @see GenericObjectPool#getEvictionPolicyClassName() + * @see GenericKeyedObjectPool#getEvictionPolicyClassName() + */ + public static final String DEFAULT_EVICTION_POLICY_CLASS_NAME = DefaultEvictionPolicy.class.getName(); + + private boolean lifo = DEFAULT_LIFO; + + private boolean fairness = DEFAULT_FAIRNESS; + + private Duration maxWaitDuration = DEFAULT_MAX_WAIT; + + private Duration minEvictableIdleDuration = DEFAULT_MIN_EVICTABLE_IDLE_TIME; + + private Duration evictorShutdownTimeoutDuration = DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT; + + private Duration softMinEvictableIdleDuration = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME; + + private int numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN; + + private EvictionPolicy evictionPolicy; // Only 2.6.0 applications set this + + private String evictionPolicyClassName = DEFAULT_EVICTION_POLICY_CLASS_NAME; + + private boolean testOnCreate = DEFAULT_TEST_ON_CREATE; + + private boolean testOnBorrow = DEFAULT_TEST_ON_BORROW; + + private boolean testOnReturn = DEFAULT_TEST_ON_RETURN; + + private boolean testWhileIdle = DEFAULT_TEST_WHILE_IDLE; + + private Duration durationBetweenEvictionRuns = DEFAULT_DURATION_BETWEEN_EVICTION_RUNS; + + private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED; + + private boolean jmxEnabled = DEFAULT_JMX_ENABLE; + + // TODO Consider changing this to a single property for 3.x + private String jmxNamePrefix = DEFAULT_JMX_NAME_PREFIX; + + private String jmxNameBase = DEFAULT_JMX_NAME_BASE; + + /** + * Gets the value for the {@code blockWhenExhausted} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code blockWhenExhausted} for this configuration instance + * @see GenericObjectPool#getBlockWhenExhausted() + * @see GenericKeyedObjectPool#getBlockWhenExhausted() + */ + public boolean getBlockWhenExhausted() { + return blockWhenExhausted; + } + + /** + * Gets the value for the {@code timeBetweenEvictionRuns} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code timeBetweenEvictionRuns} for this configuration instance + * @see GenericObjectPool#getDurationBetweenEvictionRuns() + * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns() + * @since 2.11.0 + */ + public Duration getDurationBetweenEvictionRuns() { + return durationBetweenEvictionRuns; + } + + /** + * Gets the value for the {@code evictionPolicyClass} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code evictionPolicyClass} for this configuration instance + * @see GenericObjectPool#getEvictionPolicy() + * @see GenericKeyedObjectPool#getEvictionPolicy() + * @since 2.6.0 + */ + public EvictionPolicy getEvictionPolicy() { + return evictionPolicy; + } + + /** + * Gets the value for the {@code evictionPolicyClassName} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code evictionPolicyClassName} for this configuration instance + * @see GenericObjectPool#getEvictionPolicyClassName() + * @see GenericKeyedObjectPool#getEvictionPolicyClassName() + */ + public String getEvictionPolicyClassName() { + return evictionPolicyClassName; + } + + /** + * Gets the value for the {@code evictorShutdownTimeout} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code evictorShutdownTimeout} for this configuration instance + * @see GenericObjectPool#getEvictorShutdownTimeoutDuration() + * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration() + * @since 2.10.0 + * @deprecated Use {@link #getEvictorShutdownTimeoutDuration()}. + */ + @Deprecated + public Duration getEvictorShutdownTimeout() { + return evictorShutdownTimeoutDuration; + } + + /** + * Gets the value for the {@code evictorShutdownTimeout} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code evictorShutdownTimeout} for this configuration instance + * @see GenericObjectPool#getEvictorShutdownTimeoutDuration() + * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration() + * @since 2.11.0 + */ + public Duration getEvictorShutdownTimeoutDuration() { + return evictorShutdownTimeoutDuration; + } + + /** + * Gets the value for the {@code evictorShutdownTimeout} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code evictorShutdownTimeout} for this configuration instance + * @see GenericObjectPool#getEvictorShutdownTimeoutDuration() + * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration() + * @deprecated Use {@link #getEvictorShutdownTimeout()}. + */ + @Deprecated + public long getEvictorShutdownTimeoutMillis() { + return evictorShutdownTimeoutDuration.toMillis(); + } + + /** + * Gets the value for the {@code fairness} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code fairness} for this configuration instance + * @see GenericObjectPool#getFairness() + * @see GenericKeyedObjectPool#getFairness() + */ + public boolean getFairness() { + return fairness; + } + + /** + * Gets the value of the flag that determines if JMX will be enabled for pools created with this configuration instance. + * + * @return The current setting of {@code jmxEnabled} for this configuration instance + */ + public boolean getJmxEnabled() { + return jmxEnabled; + } + + /** + * Gets the value of the JMX name base that will be used as part of the name assigned to JMX enabled pools created with this configuration instance. A value + * of {@code null} means that the pool will define the JMX name base. + * + * @return The current setting of {@code jmxNameBase} for this configuration instance + */ + public String getJmxNameBase() { + return jmxNameBase; + } + + /** + * Gets the value of the JMX name prefix that will be used as part of the name assigned to JMX enabled pools created with this configuration instance. + * + * @return The current setting of {@code jmxNamePrefix} for this configuration instance + */ + public String getJmxNamePrefix() { + return jmxNamePrefix; + } + + /** + * Gets the value for the {@code lifo} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code lifo} for this configuration instance + * + * @see GenericObjectPool#getLifo() + * @see GenericKeyedObjectPool#getLifo() + */ + public boolean getLifo() { + return lifo; + } + + /** + * Gets the value for the {@code maxWait} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code maxWait} for this configuration instance + * @see GenericObjectPool#getMaxWaitDuration() + * @see GenericKeyedObjectPool#getMaxWaitDuration() + * @since 2.11.0 + */ + public Duration getMaxWaitDuration() { + return maxWaitDuration; + } + + /** + * Gets the value for the {@code maxWait} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code maxWait} for this configuration instance + * @see GenericObjectPool#getMaxWaitDuration() + * @see GenericKeyedObjectPool#getMaxWaitDuration() + * @deprecated Use {@link #getMaxWaitDuration()}. + */ + @Deprecated + public long getMaxWaitMillis() { + return maxWaitDuration.toMillis(); + } + + /** + * Gets the value for the {@code minEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code minEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @since 2.11.0 + */ + public Duration getMinEvictableIdleDuration() { + return minEvictableIdleDuration; + } + + /** + * Gets the value for the {@code minEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code minEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @since 2.10.0 + * @deprecated Use {@link #getMinEvictableIdleDuration()}. + */ + @Deprecated + public Duration getMinEvictableIdleTime() { + return minEvictableIdleDuration; + } + + /** + * Gets the value for the {@code minEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code minEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @deprecated Use {@link #getMinEvictableIdleTime()}. + */ + @Deprecated + public long getMinEvictableIdleTimeMillis() { + return minEvictableIdleDuration.toMillis(); + } + + /** + * Gets the value for the {@code numTestsPerEvictionRun} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code numTestsPerEvictionRun} for this configuration instance + * @see GenericObjectPool#getNumTestsPerEvictionRun() + * @see GenericKeyedObjectPool#getNumTestsPerEvictionRun() + */ + public int getNumTestsPerEvictionRun() { + return numTestsPerEvictionRun; + } + + /** + * Gets the value for the {@code softMinEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code softMinEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @since 2.11.0 + */ + public Duration getSoftMinEvictableIdleDuration() { + return softMinEvictableIdleDuration; + } + + /** + * Gets the value for the {@code softMinEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code softMinEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @since 2.10.0 + * @deprecated Use {@link #getSoftMinEvictableIdleDuration()}. + */ + @Deprecated + public Duration getSoftMinEvictableIdleTime() { + return softMinEvictableIdleDuration; + } + + /** + * Gets the value for the {@code softMinEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code softMinEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @deprecated Use {@link #getSoftMinEvictableIdleDuration()}. + */ + @Deprecated + public long getSoftMinEvictableIdleTimeMillis() { + return softMinEvictableIdleDuration.toMillis(); + } + + /** + * Gets the value for the {@code testOnBorrow} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code testOnBorrow} for this configuration instance + * @see GenericObjectPool#getTestOnBorrow() + * @see GenericKeyedObjectPool#getTestOnBorrow() + */ + public boolean getTestOnBorrow() { + return testOnBorrow; + } + + /** + * Gets the value for the {@code testOnCreate} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code testOnCreate} for this configuration instance + * @see GenericObjectPool#getTestOnCreate() + * @see GenericKeyedObjectPool#getTestOnCreate() + * @since 2.2 + */ + public boolean getTestOnCreate() { + return testOnCreate; + } + + /** + * Gets the value for the {@code testOnReturn} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code testOnReturn} for this configuration instance + * @see GenericObjectPool#getTestOnReturn() + * @see GenericKeyedObjectPool#getTestOnReturn() + */ + public boolean getTestOnReturn() { + return testOnReturn; + } + + /** + * Gets the value for the {@code testWhileIdle} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code testWhileIdle} for this configuration instance + * @see GenericObjectPool#getTestWhileIdle() + * @see GenericKeyedObjectPool#getTestWhileIdle() + */ + public boolean getTestWhileIdle() { + return testWhileIdle; + } + + /** + * Gets the value for the {@code timeBetweenEvictionRuns} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code timeBetweenEvictionRuns} for this configuration instance + * @see GenericObjectPool#getDurationBetweenEvictionRuns() + * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns() + * @since 2.10.0 + * @deprecated Use {@link #getDurationBetweenEvictionRuns()}. + */ + @Deprecated + public Duration getTimeBetweenEvictionRuns() { + return durationBetweenEvictionRuns; + } + + /** + * Gets the value for the {@code timeBetweenEvictionRuns} configuration attribute for pools created with this configuration instance. + * + * @return The current setting of {@code timeBetweenEvictionRuns} for this configuration instance + * @see GenericObjectPool#getDurationBetweenEvictionRuns() + * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns() + * @deprecated Use {@link #getDurationBetweenEvictionRuns()}. + */ + @Deprecated + public long getTimeBetweenEvictionRunsMillis() { + return durationBetweenEvictionRuns.toMillis(); + } + + /** + * Sets the value for the {@code blockWhenExhausted} configuration attribute for pools created with this configuration instance. + * + * @param blockWhenExhausted The new setting of {@code blockWhenExhausted} for this configuration instance + * @see GenericObjectPool#getBlockWhenExhausted() + * @see GenericKeyedObjectPool#getBlockWhenExhausted() + */ + public void setBlockWhenExhausted(final boolean blockWhenExhausted) { + this.blockWhenExhausted = blockWhenExhausted; + } + + /** + * Sets the value for the {@code evictionPolicyClass} configuration attribute for pools created with this configuration instance. + * + * @param evictionPolicy The new setting of {@code evictionPolicyClass} for this configuration instance + * @see GenericObjectPool#getEvictionPolicy() + * @see GenericKeyedObjectPool#getEvictionPolicy() + * @since 2.6.0 + */ + public void setEvictionPolicy(final EvictionPolicy evictionPolicy) { + this.evictionPolicy = evictionPolicy; + } + + /** + * Sets the value for the {@code evictionPolicyClassName} configuration attribute for pools created with this configuration instance. + * + * @param evictionPolicyClassName The new setting of {@code evictionPolicyClassName} for this configuration instance + * @see GenericObjectPool#getEvictionPolicyClassName() + * @see GenericKeyedObjectPool#getEvictionPolicyClassName() + */ + public void setEvictionPolicyClassName(final String evictionPolicyClassName) { + this.evictionPolicyClassName = evictionPolicyClassName; + } + + /** + * Sets the value for the {@code evictorShutdownTimeout} configuration attribute for pools created with this configuration instance. + * + * @param evictorShutdownTimeoutDuration The new setting of {@code evictorShutdownTimeout} for this configuration instance + * @see GenericObjectPool#getEvictorShutdownTimeoutDuration() + * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration() + * @since 2.11.0 + */ + public void setEvictorShutdownTimeout(final Duration evictorShutdownTimeoutDuration) { + this.evictorShutdownTimeoutDuration = PoolImplUtils.nonNull(evictorShutdownTimeoutDuration, DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT); + } + + /** + * Sets the value for the {@code evictorShutdownTimeout} configuration attribute for pools created with this configuration instance. + * + * @param evictorShutdownTimeout The new setting of {@code evictorShutdownTimeout} for this configuration instance + * @see GenericObjectPool#getEvictorShutdownTimeoutDuration() + * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration() + * @since 2.10.0 + * @deprecated Use {@link #setEvictorShutdownTimeout(Duration)}. + */ + @Deprecated + public void setEvictorShutdownTimeoutMillis(final Duration evictorShutdownTimeout) { + setEvictorShutdownTimeout(evictorShutdownTimeout); + } + + /** + * Sets the value for the {@code evictorShutdownTimeout} configuration attribute for pools created with this configuration instance. + * + * @param evictorShutdownTimeoutMillis The new setting of {@code evictorShutdownTimeout} for this configuration instance + * @see GenericObjectPool#getEvictorShutdownTimeoutDuration() + * @see GenericKeyedObjectPool#getEvictorShutdownTimeoutDuration() + * @deprecated Use {@link #setEvictorShutdownTimeout(Duration)}. + */ + @Deprecated + public void setEvictorShutdownTimeoutMillis(final long evictorShutdownTimeoutMillis) { + setEvictorShutdownTimeout(Duration.ofMillis(evictorShutdownTimeoutMillis)); + } + + /** + * Sets the value for the {@code fairness} configuration attribute for pools created with this configuration instance. + * + * @param fairness The new setting of {@code fairness} for this configuration instance + * @see GenericObjectPool#getFairness() + * @see GenericKeyedObjectPool#getFairness() + */ + public void setFairness(final boolean fairness) { + this.fairness = fairness; + } + + /** + * Sets the value of the flag that determines if JMX will be enabled for pools created with this configuration instance. + * + * @param jmxEnabled The new setting of {@code jmxEnabled} for this configuration instance + */ + public void setJmxEnabled(final boolean jmxEnabled) { + this.jmxEnabled = jmxEnabled; + } + + /** + * Sets the value of the JMX name base that will be used as part of the name assigned to JMX enabled pools created with this configuration instance. A value + * of {@code null} means that the pool will define the JMX name base. + * + * @param jmxNameBase The new setting of {@code jmxNameBase} for this configuration instance + */ + public void setJmxNameBase(final String jmxNameBase) { + this.jmxNameBase = jmxNameBase; + } + + /** + * Sets the value of the JMX name prefix that will be used as part of the name assigned to JMX enabled pools created with this configuration instance. + * + * @param jmxNamePrefix The new setting of {@code jmxNamePrefix} for this configuration instance + */ + public void setJmxNamePrefix(final String jmxNamePrefix) { + this.jmxNamePrefix = jmxNamePrefix; + } + + /** + * Sets the value for the {@code lifo} configuration attribute for pools created with this configuration instance. + * + * @param lifo The new setting of {@code lifo} for this configuration instance + * @see GenericObjectPool#getLifo() + * @see GenericKeyedObjectPool#getLifo() + */ + public void setLifo(final boolean lifo) { + this.lifo = lifo; + } + + /** + * Sets the value for the {@code maxWait} configuration attribute for pools created with this configuration instance. + * + * @param maxWaitDuration The new setting of {@code maxWaitDuration} for this configuration instance + * @see GenericObjectPool#getMaxWaitDuration() + * @see GenericKeyedObjectPool#getMaxWaitDuration() + * @since 2.11.0 + */ + public void setMaxWait(final Duration maxWaitDuration) { + this.maxWaitDuration = PoolImplUtils.nonNull(maxWaitDuration, DEFAULT_MAX_WAIT); + } + + /** + * Sets the value for the {@code maxWait} configuration attribute for pools created with this configuration instance. + * + * @param maxWaitMillis The new setting of {@code maxWaitMillis} for this configuration instance + * @see GenericObjectPool#getMaxWaitDuration() + * @see GenericKeyedObjectPool#getMaxWaitDuration() + * @deprecated Use {@link #setMaxWait(Duration)}. + */ + @Deprecated + public void setMaxWaitMillis(final long maxWaitMillis) { + setMaxWait(Duration.ofMillis(maxWaitMillis)); + } + + /** + * Sets the value for the {@code minEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @param minEvictableIdleTime The new setting of {@code minEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @since 2.12.0 + */ + public void setMinEvictableIdleDuration(final Duration minEvictableIdleTime) { + this.minEvictableIdleDuration = PoolImplUtils.nonNull(minEvictableIdleTime, DEFAULT_MIN_EVICTABLE_IDLE_TIME); + } + + /** + * Sets the value for the {@code minEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @param minEvictableIdleTime The new setting of {@code minEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @since 2.10.0 + * @deprecated Use {@link #setMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public void setMinEvictableIdleTime(final Duration minEvictableIdleTime) { + this.minEvictableIdleDuration = PoolImplUtils.nonNull(minEvictableIdleTime, DEFAULT_MIN_EVICTABLE_IDLE_TIME); + } + + /** + * Sets the value for the {@code minEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @param minEvictableIdleTimeMillis The new setting of {@code minEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getMinEvictableIdleDuration() + * @deprecated Use {@link #setMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public void setMinEvictableIdleTimeMillis(final long minEvictableIdleTimeMillis) { + this.minEvictableIdleDuration = Duration.ofMillis(minEvictableIdleTimeMillis); + } + + /** + * Sets the value for the {@code numTestsPerEvictionRun} configuration attribute for pools created with this configuration instance. + * + * @param numTestsPerEvictionRun The new setting of {@code numTestsPerEvictionRun} for this configuration instance + * @see GenericObjectPool#getNumTestsPerEvictionRun() + * @see GenericKeyedObjectPool#getNumTestsPerEvictionRun() + */ + public void setNumTestsPerEvictionRun(final int numTestsPerEvictionRun) { + this.numTestsPerEvictionRun = numTestsPerEvictionRun; + } + + /** + * Sets the value for the {@code softMinEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @param softMinEvictableIdleTime The new setting of {@code softMinEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @since 2.12.0 + */ + public void setSoftMinEvictableIdleDuration(final Duration softMinEvictableIdleTime) { + this.softMinEvictableIdleDuration = PoolImplUtils.nonNull(softMinEvictableIdleTime, DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME); + } + + /** + * Sets the value for the {@code softMinEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @param softMinEvictableIdleTime The new setting of {@code softMinEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @since 2.10.0 + * @deprecated Use {@link #setSoftMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public void setSoftMinEvictableIdleTime(final Duration softMinEvictableIdleTime) { + this.softMinEvictableIdleDuration = PoolImplUtils.nonNull(softMinEvictableIdleTime, DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME); + } + + /** + * Sets the value for the {@code softMinEvictableIdleTime} configuration attribute for pools created with this configuration instance. + * + * @param softMinEvictableIdleTimeMillis The new setting of {@code softMinEvictableIdleTime} for this configuration instance + * @see GenericObjectPool#getSoftMinEvictableIdleDuration() + * @see GenericKeyedObjectPool#getSoftMinEvictableIdleDuration() + * @deprecated Use {@link #setSoftMinEvictableIdleDuration(Duration)}. + */ + @Deprecated + public void setSoftMinEvictableIdleTimeMillis(final long softMinEvictableIdleTimeMillis) { + setSoftMinEvictableIdleTime(Duration.ofMillis(softMinEvictableIdleTimeMillis)); + } + + /** + * Sets the value for the {@code testOnBorrow} configuration attribute for pools created with this configuration instance. + * + * @param testOnBorrow The new setting of {@code testOnBorrow} for this configuration instance + * @see GenericObjectPool#getTestOnBorrow() + * @see GenericKeyedObjectPool#getTestOnBorrow() + */ + public void setTestOnBorrow(final boolean testOnBorrow) { + this.testOnBorrow = testOnBorrow; + } + + /** + * Sets the value for the {@code testOnCreate} configuration attribute for pools created with this configuration instance. + * + * @param testOnCreate The new setting of {@code testOnCreate} for this configuration instance + * @see GenericObjectPool#getTestOnCreate() + * @see GenericKeyedObjectPool#getTestOnCreate() + * @since 2.2 + */ + public void setTestOnCreate(final boolean testOnCreate) { + this.testOnCreate = testOnCreate; + } + + /** + * Sets the value for the {@code testOnReturn} configuration attribute for pools created with this configuration instance. + * + * @param testOnReturn The new setting of {@code testOnReturn} for this configuration instance + * @see GenericObjectPool#getTestOnReturn() + * @see GenericKeyedObjectPool#getTestOnReturn() + */ + public void setTestOnReturn(final boolean testOnReturn) { + this.testOnReturn = testOnReturn; + } + + /** + * Sets the value for the {@code testWhileIdle} configuration attribute for pools created with this configuration instance. + * + * @param testWhileIdle The new setting of {@code testWhileIdle} for this configuration instance + * @see GenericObjectPool#getTestWhileIdle() + * @see GenericKeyedObjectPool#getTestWhileIdle() + */ + public void setTestWhileIdle(final boolean testWhileIdle) { + this.testWhileIdle = testWhileIdle; + } + + /** + * Sets the value for the {@code timeBetweenEvictionRuns} configuration attribute for pools created with this configuration instance. + * + * @param timeBetweenEvictionRuns The new setting of {@code timeBetweenEvictionRuns} for this configuration instance + * @see GenericObjectPool#getDurationBetweenEvictionRuns() + * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns() + * @since 2.10.0 + */ + public void setTimeBetweenEvictionRuns(final Duration timeBetweenEvictionRuns) { + this.durationBetweenEvictionRuns = PoolImplUtils.nonNull(timeBetweenEvictionRuns, DEFAULT_DURATION_BETWEEN_EVICTION_RUNS); + } + + /** + * Sets the value for the {@code timeBetweenEvictionRuns} configuration attribute for pools created with this configuration instance. + * + * @param timeBetweenEvictionRunsMillis The new setting of {@code timeBetweenEvictionRuns} for this configuration instance + * @see GenericObjectPool#getDurationBetweenEvictionRuns() + * @see GenericKeyedObjectPool#getDurationBetweenEvictionRuns() + * @deprecated Use {@link #setTimeBetweenEvictionRuns(Duration)}. + */ + @Deprecated + public void setTimeBetweenEvictionRunsMillis(final long timeBetweenEvictionRunsMillis) { + setTimeBetweenEvictionRuns(Duration.ofMillis(timeBetweenEvictionRunsMillis)); + } + + @Override + protected void toStringAppendFields(final StringBuilder builder) { + builder.append("lifo="); + builder.append(lifo); + builder.append(", fairness="); + builder.append(fairness); + builder.append(", maxWaitDuration="); + builder.append(maxWaitDuration); + builder.append(", minEvictableIdleTime="); + builder.append(minEvictableIdleDuration); + builder.append(", softMinEvictableIdleTime="); + builder.append(softMinEvictableIdleDuration); + builder.append(", numTestsPerEvictionRun="); + builder.append(numTestsPerEvictionRun); + builder.append(", evictionPolicyClassName="); + builder.append(evictionPolicyClassName); + builder.append(", testOnCreate="); + builder.append(testOnCreate); + builder.append(", testOnBorrow="); + builder.append(testOnBorrow); + builder.append(", testOnReturn="); + builder.append(testOnReturn); + builder.append(", testWhileIdle="); + builder.append(testWhileIdle); + builder.append(", timeBetweenEvictionRuns="); + builder.append(durationBetweenEvictionRuns); + builder.append(", blockWhenExhausted="); + builder.append(blockWhenExhausted); + builder.append(", jmxEnabled="); + builder.append(jmxEnabled); + builder.append(", jmxNamePrefix="); + builder.append(jmxNamePrefix); + builder.append(", jmxNameBase="); + builder.append(jmxNameBase); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java b/java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java new file mode 100644 index 0000000..95b98fd --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.PrintWriter; + +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.UsageTracking; + +/** + * Strategy for obtaining and printing the current call stack. This is primarily useful for + * {@linkplain UsageTracking usage tracking} so that different JVMs and configurations can use more efficient strategies + * for obtaining the current call stack depending on metadata needs. + * + * @see CallStackUtils + * @since 2.4.3 + */ +public interface CallStack { + + /** + * Clears the current stack trace snapshot. Subsequent calls to {@link #printStackTrace(PrintWriter)} will be + * no-ops until another call to {@link #fillInStackTrace()}. + */ + void clear(); + + /** + * Takes a snapshot of the current call stack. Subsequent calls to {@link #printStackTrace(PrintWriter)} will print + * out that stack trace until it is {@linkplain #clear() cleared}. + */ + void fillInStackTrace(); + + /** + * Prints the current stack trace if available to a PrintWriter. The format is undefined and is primarily useful + * for debugging issues with {@link PooledObject} usage in user code. + * + * @param writer a PrintWriter to write the current stack trace to if available + * @return true if a stack trace was available to print or false if nothing was printed + */ + boolean printStackTrace(PrintWriter writer); +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/CallStackUtils.java b/java/org/apache/tomcat/dbcp/pool2/impl/CallStackUtils.java new file mode 100644 index 0000000..831343f --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/CallStackUtils.java @@ -0,0 +1,85 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.security.AccessControlException; + +/** + * Utility methods for {@link CallStack}. + * + * @since 2.4.3 + */ +public final class CallStackUtils { + + /** + * Tests whether the caller can create a security manager in the current environment. + * + * @return {@code true} if it is able to create a security manager in the current environment, {@code false} + * otherwise. + */ + private static boolean canCreateSecurityManager() { + final SecurityManager manager = System.getSecurityManager(); + if (manager == null) { + return true; + } + try { + manager.checkPermission(new RuntimePermission("createSecurityManager")); + return true; + } catch (final AccessControlException ignored) { + return false; + } + } + + /** + * Constructs a new {@link CallStack} using the fastest allowed strategy. + * + * @param messageFormat message (or format) to print first in stack traces + * @param useTimestamp if true, interpret message as a SimpleDateFormat and print the created timestamp; otherwise, + * print message format literally + * @return a new CallStack + * @deprecated use {@link #newCallStack(String, boolean, boolean)} + */ + @Deprecated + public static CallStack newCallStack(final String messageFormat, final boolean useTimestamp) { + return newCallStack(messageFormat, useTimestamp, false); + } + + /** + * Constructs a new {@link CallStack} using the fasted allowed strategy. + * + * @param messageFormat message (or format) to print first in stack traces + * @param useTimestamp if true, interpret message as a SimpleDateFormat and print the created timestamp; + * otherwise, print message format literally + * @param requireFullStackTrace if true, forces the use of a stack walking mechanism that includes full stack trace + * information; otherwise, uses a faster implementation if possible + * @return a new CallStack + * @since 2.5 + */ + public static CallStack newCallStack(final String messageFormat, + final boolean useTimestamp, + final boolean requireFullStackTrace) { + return canCreateSecurityManager() && !requireFullStackTrace ? + new SecurityManagerCallStack(messageFormat, useTimestamp) : + new ThrowableCallStack(messageFormat, useTimestamp); + } + + /** + * Hidden constructor. + */ + private CallStackUtils() { + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java b/java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java new file mode 100644 index 0000000..0255b6a --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import org.apache.tomcat.dbcp.pool2.PooledObject; + +/** + * Provides the default implementation of {@link EvictionPolicy} used by the pools. + *

    + * Objects will be evicted if the following conditions are met: + *

    + *
      + *
    • The object has been idle longer than + * {@link GenericObjectPool#getMinEvictableIdleDuration()} / + * {@link GenericKeyedObjectPool#getMinEvictableIdleDuration()}
    • + *
    • There are more than {@link GenericObjectPool#getMinIdle()} / + * {@link GenericKeyedObjectPoolConfig#getMinIdlePerKey()} idle objects in + * the pool and the object has been idle for longer than + * {@link GenericObjectPool#getSoftMinEvictableIdleDuration()} / + * {@link GenericKeyedObjectPool#getSoftMinEvictableIdleDuration()} + *
    + *

    + * This class is immutable and thread-safe. + *

    + * + * @param the type of objects in the pool. + * + * @since 2.0 + */ +public class DefaultEvictionPolicy implements EvictionPolicy { + + @Override + public boolean evict(final EvictionConfig config, final PooledObject underTest, final int idleCount) { + // @formatter:off + return config.getIdleSoftEvictDuration().compareTo(underTest.getIdleDuration()) < 0 && + config.getMinIdle() < idleCount || + config.getIdleEvictDuration().compareTo(underTest.getIdleDuration()) < 0; + // @formatter:on + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java b/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java new file mode 100644 index 0000000..348b519 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java @@ -0,0 +1,343 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.PrintWriter; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Deque; + +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.PooledObjectState; +import org.apache.tomcat.dbcp.pool2.TrackedUse; + +/** + * This wrapper is used to track the additional information, such as state, for + * the pooled objects. + *

    + * This class is intended to be thread-safe. + *

    + * + * @param the type of object in the pool + * + * @since 2.0 + */ +public class DefaultPooledObject implements PooledObject { + + private final T object; + private PooledObjectState state = PooledObjectState.IDLE; // @GuardedBy("this") to ensure transitions are valid + private final Clock systemClock = Clock.systemUTC(); + private final Instant createInstant = now(); + + private volatile Instant lastBorrowInstant = createInstant; + private volatile Instant lastUseInstant = createInstant; + private volatile Instant lastReturnInstant = createInstant; + private volatile boolean logAbandoned; + private volatile CallStack borrowedBy = NoOpCallStack.INSTANCE; + private volatile CallStack usedBy = NoOpCallStack.INSTANCE; + private volatile long borrowedCount; + + /** + * Creates a new instance that wraps the provided object so that the pool can + * track the state of the pooled object. + * + * @param object The object to wrap + */ + public DefaultPooledObject(final T object) { + this.object = object; + } + + /** + * Allocates the object. + * + * @return {@code true} if the original state was {@link PooledObjectState#IDLE IDLE} + */ + @Override + public synchronized boolean allocate() { + if (state == PooledObjectState.IDLE) { + state = PooledObjectState.ALLOCATED; + lastBorrowInstant = now(); + lastUseInstant = lastBorrowInstant; + borrowedCount++; + if (logAbandoned) { + borrowedBy.fillInStackTrace(); + } + return true; + } + if (state == PooledObjectState.EVICTION) { + // TODO Allocate anyway and ignore eviction test + state = PooledObjectState.EVICTION_RETURN_TO_HEAD; + } + // TODO if validating and testOnBorrow == true then pre-allocate for + // performance + return false; + } + + @Override + public int compareTo(final PooledObject other) { + final int compareTo = getLastReturnInstant().compareTo(other.getLastReturnInstant()); + if (compareTo == 0) { + // Make sure the natural ordering is broadly consistent with equals + // although this will break down if distinct objects have the same + // identity hash code. + // see java.lang.Comparable Javadocs + return System.identityHashCode(this) - System.identityHashCode(other); + } + return compareTo; + } + + /** + * Deallocates the object and sets it {@link PooledObjectState#IDLE IDLE} + * if it is currently {@link PooledObjectState#ALLOCATED ALLOCATED} + * or {@link PooledObjectState#RETURNING RETURNING}. + * + * @return {@code true} if the state was {@link PooledObjectState#ALLOCATED ALLOCATED} + * or {@link PooledObjectState#RETURNING RETURNING}. + */ + @Override + public synchronized boolean deallocate() { + if (state == PooledObjectState.ALLOCATED || state == PooledObjectState.RETURNING) { + state = PooledObjectState.IDLE; + lastReturnInstant = now(); + borrowedBy.clear(); + return true; + } + + return false; + } + + @Override + public synchronized boolean endEvictionTest( + final Deque> idleQueue) { + if (state == PooledObjectState.EVICTION) { + state = PooledObjectState.IDLE; + return true; + } + if (state == PooledObjectState.EVICTION_RETURN_TO_HEAD) { + state = PooledObjectState.IDLE; + idleQueue.offerFirst(this); + } + + return false; + } + + @Override + public long getActiveTimeMillis() { + return getActiveDuration().toMillis(); + } + + /** + * Gets the number of times this object has been borrowed. + * @return The number of times this object has been borrowed. + * @since 2.1 + */ + @Override + public long getBorrowedCount() { + return borrowedCount; + } + + @Override + public Instant getCreateInstant() { + return createInstant; + } + + @Override + public long getCreateTime() { + return createInstant.toEpochMilli(); + } + + @Override + public Duration getIdleDuration() { + // elapsed may be negative if: + // - another thread updates lastReturnInstant during the calculation window + // - System.currentTimeMillis() is not monotonic (e.g. system time is set back) + final Duration elapsed = Duration.between(lastReturnInstant, now()); + return elapsed.isNegative() ? Duration.ZERO : elapsed; + } + + @Override + public Duration getIdleTime() { + return getIdleDuration(); + } + + @Override + public long getIdleTimeMillis() { + return getIdleDuration().toMillis(); + } + + @Override + public Instant getLastBorrowInstant() { + return lastBorrowInstant; + } + + @Override + public long getLastBorrowTime() { + return lastBorrowInstant.toEpochMilli(); + } + + @Override + public Instant getLastReturnInstant() { + return lastReturnInstant; + } + + @Override + public long getLastReturnTime() { + return lastReturnInstant.toEpochMilli(); + } + + /** + * Gets an estimate of the last time this object was used. If the class + * of the pooled object implements {@link TrackedUse}, what is returned is + * the maximum of {@link TrackedUse#getLastUsedInstant()} and + * {@link #getLastBorrowTime()}; otherwise this method gives the same + * value as {@link #getLastBorrowTime()}. + * + * @return the last Instant this object was used. + */ + @Override + public Instant getLastUsedInstant() { + if (object instanceof TrackedUse) { + return PoolImplUtils.max(((TrackedUse) object).getLastUsedInstant(), lastUseInstant); + } + return lastUseInstant; + } + + /** + * Gets an estimate of the last time this object was used. If the class + * of the pooled object implements {@link TrackedUse}, what is returned is + * the maximum of {@link TrackedUse#getLastUsedInstant()} and + * {@link #getLastBorrowTime()}; otherwise this method gives the same + * value as {@link #getLastBorrowTime()}. + * + * @return the last time this object was used + */ + @Override + public long getLastUsedTime() { + return getLastUsedInstant().toEpochMilli(); + } + + @Override + public T getObject() { + return object; + } + + /** + * Gets the state of this object. + * @return state + */ + @Override + public synchronized PooledObjectState getState() { + return state; + } + + /** + * Sets the state to {@link PooledObjectState#INVALID INVALID}. + */ + @Override + public synchronized void invalidate() { + state = PooledObjectState.INVALID; + } + + /** + * Marks the pooled object as {@link PooledObjectState#ABANDONED ABANDONED}. + */ + @Override + public synchronized void markAbandoned() { + state = PooledObjectState.ABANDONED; + } + + /** + * Marks the pooled object as {@link PooledObjectState#RETURNING RETURNING}. + */ + @Override + public synchronized void markReturning() { + state = PooledObjectState.RETURNING; + } + + /** + * Gets the current instant of the clock. + * + * @return the current instant of the clock. + */ + private Instant now() { + return systemClock.instant(); + } + + @Override + public void printStackTrace(final PrintWriter writer) { + boolean written = borrowedBy.printStackTrace(writer); + written |= usedBy.printStackTrace(writer); + if (written) { + writer.flush(); + } + } + + @Override + public void setLogAbandoned(final boolean logAbandoned) { + this.logAbandoned = logAbandoned; + } + + /** + * Configures the stack trace generation strategy based on whether or not fully + * detailed stack traces are required. When set to false, abandoned logs may + * only include caller class information rather than method names, line numbers, + * and other normal metadata available in a full stack trace. + * + * @param requireFullStackTrace the new configuration setting for abandoned object + * logging + * @since 2.5 + */ + @Override + public void setRequireFullStackTrace(final boolean requireFullStackTrace) { + borrowedBy = CallStackUtils.newCallStack("'Pooled object created' " + + "yyyy-MM-dd HH:mm:ss Z 'by the following code has not been returned to the pool:'", + true, requireFullStackTrace); + usedBy = CallStackUtils.newCallStack("The last code to use this object was:", + false, requireFullStackTrace); + } + + @Override + public synchronized boolean startEvictionTest() { + if (state == PooledObjectState.IDLE) { + state = PooledObjectState.EVICTION; + return true; + } + return false; + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("Object: "); + result.append(object.toString()); + result.append(", State: "); + synchronized (this) { + result.append(state.toString()); + } + return result.toString(); + // TODO add other attributes + } + + @Override + public void use() { + lastUseInstant = now(); + usedBy.fillInStackTrace(); + } + + +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java b/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java new file mode 100644 index 0000000..2d7b0e3 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java @@ -0,0 +1,117 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Objects; + +import org.apache.tomcat.dbcp.pool2.PooledObject; + +/** + * Implementation of object that is used to provide information on pooled + * objects via JMX. + * + * @since 2.0 + */ +public class DefaultPooledObjectInfo implements DefaultPooledObjectInfoMBean { + + private static final String PATTERN = "yyyy-MM-dd HH:mm:ss Z"; + + private final PooledObject pooledObject; + + /** + * Constructs a new instance for the given pooled object. + * + * @param pooledObject The pooled object that this instance will represent + * @throws NullPointerException if {@code obj} is {@code null} + */ + public DefaultPooledObjectInfo(final PooledObject pooledObject) { + this.pooledObject = Objects.requireNonNull(pooledObject, "pooledObject"); + } + + @Override + public long getBorrowedCount() { + return pooledObject.getBorrowedCount(); + } + + @Override + public long getCreateTime() { + return pooledObject.getCreateInstant().toEpochMilli(); + } + + @Override + public String getCreateTimeFormatted() { + return getTimeMillisFormatted(getCreateTime()); + } + + @Override + public long getLastBorrowTime() { + return pooledObject.getLastBorrowInstant().toEpochMilli(); + } + + + @Override + public String getLastBorrowTimeFormatted() { + return getTimeMillisFormatted(getLastBorrowTime()); + } + + @Override + public String getLastBorrowTrace() { + final StringWriter sw = new StringWriter(); + pooledObject.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + @Override + public long getLastReturnTime() { + return pooledObject.getLastReturnInstant().toEpochMilli(); + } + + @Override + public String getLastReturnTimeFormatted() { + return getTimeMillisFormatted(getLastReturnTime()); + } + + @Override + public String getPooledObjectToString() { + return Objects.toString(pooledObject.getObject(), null); + } + + @Override + public String getPooledObjectType() { + final Object object = pooledObject.getObject(); + return object != null ? object.getClass().getName() : null; + } + + private String getTimeMillisFormatted(final long millis) { + return new SimpleDateFormat(PATTERN).format(Long.valueOf(millis)); + } + + /** + * @since 2.4.3 + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("DefaultPooledObjectInfo [pooledObject="); + builder.append(pooledObject); + builder.append("]"); + return builder.toString(); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java b/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java new file mode 100644 index 0000000..fb026de --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java @@ -0,0 +1,106 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +/** + * The interface that defines the information about pooled objects that will be exposed via JMX. + *

    Note

    + *

    + * This interface exists only to define those attributes and methods that will be made available via JMX. It must not be implemented by clients as it is subject + * to change between major, minor and patch version releases of commons pool. Clients that implement this interface may not, therefore, be able to upgrade to a + * new minor or patch release without requiring code changes. + *

    + * + * @since 2.0 + */ +public interface DefaultPooledObjectInfoMBean { + + /** + * Gets the number of times this object has been borrowed. + * + * @return The number of times this object has been borrowed. + * @since 2.1 + */ + long getBorrowedCount(); + + /** + * Gets the time (using the same basis as {@link java.time.Clock#instant()}) that pooled object was created. + * + * @return The creation time for the pooled object. + */ + long getCreateTime(); + + /** + * Gets the time that pooled object was created. + * + * @return The creation time for the pooled object formatted as {@code yyyy-MM-dd HH:mm:ss Z}. + */ + String getCreateTimeFormatted(); + + /** + * Gets the time (using the same basis as {@link java.time.Clock#instant()}) the polled object was last borrowed. + * + * @return The time the pooled object was last borrowed. + */ + long getLastBorrowTime(); + + /** + * Gets the time that pooled object was last borrowed. + * + * @return The last borrowed time for the pooled object formatted as {@code yyyy-MM-dd HH:mm:ss Z}. + */ + String getLastBorrowTimeFormatted(); + + /** + * Gets the stack trace recorded when the pooled object was last borrowed. + * + * @return The stack trace showing which code last borrowed the pooled object. + */ + String getLastBorrowTrace(); + + /** + * Gets the time (using the same basis as {@link java.time.Clock#instant()})the wrapped object was last returned. + * + * @return The time the object was last returned. + */ + long getLastReturnTime(); + + /** + * Gets the time that pooled object was last returned. + * + * @return The last returned time for the pooled object formatted as {@code yyyy-MM-dd HH:mm:ss Z}. + */ + String getLastReturnTimeFormatted(); + + /** + * Gets a String form of the wrapper for debug purposes. The format is not fixed and may change at any time. + * + * @return A string representation of the pooled object. + * + * @see Object#toString() + */ + String getPooledObjectToString(); + + /** + * Gets the name of the class of the pooled object. + * + * @return The pooled object's class name. + * + * @see Class#getName() + */ + String getPooledObjectType(); +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java b/java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java new file mode 100644 index 0000000..b634518 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java @@ -0,0 +1,198 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.time.Duration; + +/** + * This class is used by pool implementations to pass configuration information + * to {@link EvictionPolicy} instances. The {@link EvictionPolicy} may also have + * its own specific configuration attributes. + *

    + * This class is immutable and thread-safe. + *

    + * + * @since 2.0 + */ +public class EvictionConfig { + + private static final Duration MAX_DURATION = Duration.ofMillis(Long.MAX_VALUE); + private final Duration idleEvictDuration; + private final Duration idleSoftEvictDuration; + private final int minIdle; + + /** + * Creates a new eviction configuration with the specified parameters. + * Instances are immutable. + * + * @param idleEvictDuration Expected to be provided by + * {@link BaseGenericObjectPool#getMinEvictableIdleDuration()} + * @param idleSoftEvictDuration Expected to be provided by + * {@link BaseGenericObjectPool#getSoftMinEvictableIdleDuration()} + * @param minIdle Expected to be provided by + * {@link GenericObjectPool#getMinIdle()} or + * {@link GenericKeyedObjectPool#getMinIdlePerKey()} + * @since 2.10.0 + */ + public EvictionConfig(final Duration idleEvictDuration, final Duration idleSoftEvictDuration, final int minIdle) { + this.idleEvictDuration = PoolImplUtils.isPositive(idleEvictDuration) ? idleEvictDuration : MAX_DURATION; + this.idleSoftEvictDuration = PoolImplUtils.isPositive(idleSoftEvictDuration) ? idleSoftEvictDuration : MAX_DURATION; + this.minIdle = minIdle; + } + + /** + * Creates a new eviction configuration with the specified parameters. + * Instances are immutable. + * + * @param poolIdleEvictMillis Expected to be provided by + * {@link BaseGenericObjectPool#getMinEvictableIdleDuration()} + * @param poolIdleSoftEvictMillis Expected to be provided by + * {@link BaseGenericObjectPool#getSoftMinEvictableIdleDuration()} + * @param minIdle Expected to be provided by + * {@link GenericObjectPool#getMinIdle()} or + * {@link GenericKeyedObjectPool#getMinIdlePerKey()} + * @deprecated Use {@link #EvictionConfig(Duration, Duration, int)}. + */ + @Deprecated + public EvictionConfig(final long poolIdleEvictMillis, final long poolIdleSoftEvictMillis, final int minIdle) { + this(Duration.ofMillis(poolIdleEvictMillis), Duration.ofMillis(poolIdleSoftEvictMillis), minIdle); + } + + /** + * Gets the {@code idleEvictTime} for this eviction configuration + * instance. + *

    + * How the evictor behaves based on this value will be determined by the + * configured {@link EvictionPolicy}. + *

    + * + * @return The {@code idleEvictTime}. + * @since 2.11.0 + */ + public Duration getIdleEvictDuration() { + return idleEvictDuration; + } + + /** + * Gets the {@code idleEvictTime} for this eviction configuration + * instance. + *

    + * How the evictor behaves based on this value will be determined by the + * configured {@link EvictionPolicy}. + *

    + * + * @return The {@code idleEvictTime} in milliseconds + * @deprecated Use {@link #getIdleEvictDuration()}. + */ + @Deprecated + public long getIdleEvictTime() { + return idleEvictDuration.toMillis(); + } + + /** + * Gets the {@code idleEvictTime} for this eviction configuration + * instance. + *

    + * How the evictor behaves based on this value will be determined by the + * configured {@link EvictionPolicy}. + *

    + * + * @return The {@code idleEvictTime}. + * @since 2.10.0 + * @deprecated Use {@link #getIdleEvictDuration()}. + */ + @Deprecated + public Duration getIdleEvictTimeDuration() { + return idleEvictDuration; + } + + /** + * Gets the {@code idleSoftEvictTime} for this eviction configuration + * instance. + *

    + * How the evictor behaves based on this value will be determined by the + * configured {@link EvictionPolicy}. + *

    + * + * @return The (@code idleSoftEvictTime} in milliseconds + * @since 2.11.0 + */ + public Duration getIdleSoftEvictDuration() { + return idleSoftEvictDuration; + } + + /** + * Gets the {@code idleSoftEvictTime} for this eviction configuration + * instance. + *

    + * How the evictor behaves based on this value will be determined by the + * configured {@link EvictionPolicy}. + *

    + * + * @return The (@code idleSoftEvictTime} in milliseconds + * @deprecated Use {@link #getIdleSoftEvictDuration()}. + */ + @Deprecated + public long getIdleSoftEvictTime() { + return idleSoftEvictDuration.toMillis(); + } + + /** + * Gets the {@code idleSoftEvictTime} for this eviction configuration + * instance. + *

    + * How the evictor behaves based on this value will be determined by the + * configured {@link EvictionPolicy}. + *

    + * + * @return The (@code idleSoftEvictTime} in milliseconds + * @deprecated Use {@link #getIdleSoftEvictDuration()}. + */ + @Deprecated + public Duration getIdleSoftEvictTimeDuration() { + return idleSoftEvictDuration; + } + + /** + * Gets the {@code minIdle} for this eviction configuration instance. + *

    + * How the evictor behaves based on this value will be determined by the + * configured {@link EvictionPolicy}. + *

    + * + * @return The {@code minIdle} + */ + public int getMinIdle() { + return minIdle; + } + + /** + * @since 2.4 + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("EvictionConfig [idleEvictDuration="); + builder.append(idleEvictDuration); + builder.append(", idleSoftEvictDuration="); + builder.append(idleSoftEvictDuration); + builder.append(", minIdle="); + builder.append(minIdle); + builder.append("]"); + return builder.toString(); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java b/java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java new file mode 100644 index 0000000..281ca36 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java @@ -0,0 +1,42 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import org.apache.tomcat.dbcp.pool2.PooledObject; + +/** + * To provide a custom eviction policy (i.e. something other than {@link + * DefaultEvictionPolicy} for a pool, users must provide an implementation of + * this interface that provides the required eviction policy. + * + * @param the type of objects in the pool + * @since 2.0 + */ +public interface EvictionPolicy { + + /** + * Tests if an idle object in the pool should be evicted or not. + * + * @param config The pool configuration settings related to eviction + * @param underTest The pooled object being tested for eviction + * @param idleCount The current number of idle objects in the pool including + * the object under test + * @return {@code true} if the object should be evicted, otherwise + * {@code false} + */ + boolean evict(EvictionConfig config, PooledObject underTest, int idleCount); +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java b/java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java new file mode 100644 index 0000000..3e2be31 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java @@ -0,0 +1,233 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.lang.ref.WeakReference; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + + +/** + * Provides a shared idle object eviction timer for all pools. + *

    + * This class is currently implemented using {@link ScheduledThreadPoolExecutor}. This implementation may change in any + * future release. This class keeps track of how many pools are using it. If no pools are using the timer, it is + * cancelled. This prevents a thread being left running which, in application server environments, can lead to memory + * leads and/or prevent applications from shutting down or reloading cleanly. + *

    + *

    + * This class has package scope to prevent its inclusion in the pool public API. The class declaration below should + * *not* be changed to public. + *

    + *

    + * This class is intended to be thread-safe. + *

    + * + * @since 2.0 + */ +class EvictionTimer { + + /** + * Thread factory that creates a daemon thread, with the context class loader from this class. + */ + private static class EvictorThreadFactory implements ThreadFactory { + + @Override + public Thread newThread(final Runnable runnable) { + final Thread thread = new Thread(null, runnable, "commons-pool-evictor"); + thread.setDaemon(true); // POOL-363 - Required for applications using Runtime.addShutdownHook(). + AccessController.doPrivileged((PrivilegedAction) () -> { + thread.setContextClassLoader(EvictorThreadFactory.class.getClassLoader()); + return null; + }); + + return thread; + } + } + + /** + * Task that removes references to abandoned tasks and shuts + * down the executor if there are no live tasks left. + */ + private static class Reaper implements Runnable { + @Override + public void run() { + synchronized (EvictionTimer.class) { + for (final Entry.Evictor>, WeakRunner.Evictor>> entry : TASK_MAP + .entrySet()) { + if (entry.getKey().get() == null) { + executor.remove(entry.getValue()); + TASK_MAP.remove(entry.getKey()); + } + } + if (TASK_MAP.isEmpty() && executor != null) { + executor.shutdown(); + executor.setCorePoolSize(0); + executor = null; + } + } + } + } + + /** + * Runnable that runs the referent of a weak reference. When the referent is no + * no longer reachable, run is no-op. + * @param The kind of Runnable. + */ + private static class WeakRunner implements Runnable { + + private final WeakReference ref; + + /** + * Constructs a new instance to track the given reference. + * + * @param ref the reference to track. + */ + private WeakRunner(final WeakReference ref) { + this.ref = ref; + } + + @Override + public void run() { + final Runnable task = ref.get(); + if (task != null) { + task.run(); + } else { + executor.remove(this); + TASK_MAP.remove(ref); + } + } + } + + + /** Executor instance */ + private static ScheduledThreadPoolExecutor executor; //@GuardedBy("EvictionTimer.class") + + /** Keys are weak references to tasks, values are runners managed by executor. */ + private static final HashMap< + WeakReference.Evictor>, + WeakRunner.Evictor>> TASK_MAP = new HashMap<>(); // @GuardedBy("EvictionTimer.class") + + /** + * Removes the specified eviction task from the timer. + * + * @param evictor Task to be cancelled. + * @param timeout If the associated executor is no longer required, how + * long should this thread wait for the executor to + * terminate? + * @param restarting The state of the evictor. + */ + static synchronized void cancel(final BaseGenericObjectPool.Evictor evictor, final Duration timeout, + final boolean restarting) { + if (evictor != null) { + evictor.cancel(); + remove(evictor); + } + if (!restarting && executor != null && TASK_MAP.isEmpty()) { + executor.shutdown(); + try { + executor.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (final InterruptedException e) { + // Swallow + // Significant API changes would be required to propagate this + } + executor.setCorePoolSize(0); + executor = null; + } + } + + /** + * @return the number of eviction tasks under management. + */ + static synchronized int getNumTasks() { + return TASK_MAP.size(); + } + + /** + * Gets the task map. Keys are weak references to tasks, values are runners managed by executor. + * + * @return the task map. + */ + static HashMap.Evictor>, WeakRunner.Evictor>> getTaskMap() { + return TASK_MAP; + } + + /** + * Removes evictor from the task set and executor. + * Only called when holding the class lock. + * + * @param evictor Eviction task to remove + */ + private static void remove(final BaseGenericObjectPool.Evictor evictor) { + for (final Entry.Evictor>, WeakRunner.Evictor>> entry : TASK_MAP.entrySet()) { + if (entry.getKey().get() == evictor) { + executor.remove(entry.getValue()); + TASK_MAP.remove(entry.getKey()); + break; + } + } + } + + /** + * Adds the specified eviction task to the timer. Tasks that are added with + * a call to this method *must* call {@link + * #cancel(BaseGenericObjectPool.Evictor, Duration, boolean)} + * to cancel the task to prevent memory and/or thread leaks in application + * server environments. + * + * @param task Task to be scheduled. + * @param delay Delay in milliseconds before task is executed. + * @param period Time in milliseconds between executions. + */ + static synchronized void schedule( + final BaseGenericObjectPool.Evictor task, final Duration delay, final Duration period) { + if (null == executor) { + executor = new ScheduledThreadPoolExecutor(1, new EvictorThreadFactory()); + executor.setRemoveOnCancelPolicy(true); + executor.scheduleAtFixedRate(new Reaper(), delay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); + } + final WeakReference.Evictor> ref = new WeakReference<>(task); + final WeakRunner.Evictor> runner = new WeakRunner<>(ref); + final ScheduledFuture scheduledFuture = executor.scheduleWithFixedDelay(runner, delay.toMillis(), + period.toMillis(), TimeUnit.MILLISECONDS); + task.setScheduledFuture(scheduledFuture); + TASK_MAP.put(ref, runner); + } + + /** Prevents instantiation */ + private EvictionTimer() { + // Hide the default constructor + } + + /** + * @since 2.4.3 + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("EvictionTimer []"); + return builder.toString(); + } + +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java b/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java new file mode 100644 index 0000000..c03ca77 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java @@ -0,0 +1,1755 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +import org.apache.tomcat.dbcp.pool2.DestroyMode; +import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; +import org.apache.tomcat.dbcp.pool2.KeyedPooledObjectFactory; +import org.apache.tomcat.dbcp.pool2.PoolUtils; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.PooledObjectState; +import org.apache.tomcat.dbcp.pool2.SwallowedExceptionListener; +import org.apache.tomcat.dbcp.pool2.UsageTracking; + +/** + * A configurable {@code KeyedObjectPool} implementation. + *

    + * When coupled with the appropriate {@link KeyedPooledObjectFactory}, + * {@code GenericKeyedObjectPool} provides robust pooling functionality for + * keyed objects. A {@code GenericKeyedObjectPool} can be viewed as a map + * of sub-pools, keyed on the (unique) key values provided to the + * {@link #preparePool preparePool}, {@link #addObject addObject} or + * {@link #borrowObject borrowObject} methods. Each time a new key value is + * provided to one of these methods, a sub-new pool is created under the given + * key to be managed by the containing {@code GenericKeyedObjectPool.} + *

    + *

    + * Note that the current implementation uses a ConcurrentHashMap which uses + * equals() to compare keys. + * This means that distinct instance keys must be distinguishable using equals. + *

    + *

    + * Optionally, one may configure the pool to examine and possibly evict objects + * as they sit idle in the pool and to ensure that a minimum number of idle + * objects is maintained for each key. This is performed by an "idle object + * eviction" thread, which runs asynchronously. Caution should be used when + * configuring this optional feature. Eviction runs contend with client threads + * for access to objects in the pool, so if they run too frequently performance + * issues may result. + *

    + *

    + * Implementation note: To prevent possible deadlocks, care has been taken to + * ensure that no call to a factory method will occur within a synchronization + * block. See POOL-125 and DBCP-44 for more information. + *

    + *

    + * This class is intended to be thread-safe. + *

    + * + * @see GenericObjectPool + * + * @param The type of keys maintained by this pool. + * @param Type of element pooled in this pool. + * + * @since 2.0 + */ +public class GenericKeyedObjectPool extends BaseGenericObjectPool + implements KeyedObjectPool, GenericKeyedObjectPoolMXBean, UsageTracking { + + /** + * Maintains information on the per key queue for a given key. + * + * @param type of objects in the pool + */ + private static class ObjectDeque { + + private final LinkedBlockingDeque> idleObjects; + + /* + * Number of instances created - number destroyed. + * Invariant: createCount <= maxTotalPerKey + */ + private final AtomicInteger createCount = new AtomicInteger(0); + + private long makeObjectCount; + private final Object makeObjectCountLock = new Object(); + + /* + * The map is keyed on pooled instances, wrapped to ensure that + * they work properly as keys. + */ + private final Map, PooledObject> allObjects = + new ConcurrentHashMap<>(); + + /* + * Number of threads with registered interest in this key. + * register(K) increments this counter and deRegister(K) decrements it. + * Invariant: empty keyed pool will not be dropped unless numInterested + * is 0. + */ + private final AtomicLong numInterested = new AtomicLong(); + + /** + * Constructs a new ObjectDeque with the given fairness policy. + * @param fairness true means client threads waiting to borrow / return instances + * will be served as if waiting in a FIFO queue. + */ + ObjectDeque(final boolean fairness) { + idleObjects = new LinkedBlockingDeque<>(fairness); + } + + /** + * Gets all the objects for the current key. + * + * @return All the objects + */ + public Map, PooledObject> getAllObjects() { + return allObjects; + } + + /** + * Gets the number of instances created - number destroyed. + * Should always be less than or equal to maxTotalPerKey. + * + * @return The net instance addition count for this deque + */ + public AtomicInteger getCreateCount() { + return createCount; + } + + /** + * Gets the idle objects for the current key. + * + * @return The idle objects + */ + public LinkedBlockingDeque> getIdleObjects() { + return idleObjects; + } + + /** + * Gets the number of threads with an interest registered in this key. + * + * @return The number of threads with a registered interest in this key + */ + public AtomicLong getNumInterested() { + return numInterested; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("ObjectDeque [idleObjects="); + builder.append(idleObjects); + builder.append(", createCount="); + builder.append(createCount); + builder.append(", allObjects="); + builder.append(allObjects); + builder.append(", numInterested="); + builder.append(numInterested); + builder.append("]"); + return builder.toString(); + } + + } + + private static final Integer ZERO = Integer.valueOf(0); + + // JMX specific attributes + private static final String ONAME_BASE = + "org.apache.commons.pool2:type=GenericKeyedObjectPool,name="; + + private volatile int maxIdlePerKey = + GenericKeyedObjectPoolConfig.DEFAULT_MAX_IDLE_PER_KEY; + + private volatile int minIdlePerKey = + GenericKeyedObjectPoolConfig.DEFAULT_MIN_IDLE_PER_KEY; + + + private volatile int maxTotalPerKey = + GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL_PER_KEY; + + private final KeyedPooledObjectFactory factory; + + private final boolean fairness; + + /* + * My hash of sub-pools (ObjectQueue). The list of keys must be kept + * in step with {@link #poolKeyList} using {@link #keyLock} to ensure any + * changes to the list of current keys is made in a thread-safe manner. + */ + private final Map> poolMap = + new ConcurrentHashMap<>(); // @GuardedBy("keyLock") for write access (and some read access) + + /* + * List of pool keys - used to control eviction order. The list of keys + * must be kept in step with {@link #poolMap} using {@link #keyLock} + * to ensure any changes to the list of current keys is made in a + * thread-safe manner. + */ + private final ArrayList poolKeyList = new ArrayList<>(); // @GuardedBy("keyLock") + + private final ReadWriteLock keyLock = new ReentrantReadWriteLock(true); + + /* + * The combined count of the currently active objects for all keys and those + * in the process of being created. Under load, it may exceed + * {@link #maxTotal} but there will never be more than {@link #maxTotal} + * created at any one time. + */ + private final AtomicInteger numTotal = new AtomicInteger(0); + + private Iterator evictionKeyIterator; // @GuardedBy("evictionLock") + + + private K evictionKey; // @GuardedBy("evictionLock") + + /** + * Constructs a new {@code GenericKeyedObjectPool} using defaults from + * {@link GenericKeyedObjectPoolConfig}. + * @param factory the factory to be used to create entries + */ + public GenericKeyedObjectPool(final KeyedPooledObjectFactory factory) { + this(factory, new GenericKeyedObjectPoolConfig<>()); + } + + /** + * Constructs a new {@code GenericKeyedObjectPool} using a specific + * configuration. + * + * @param factory the factory to be used to create entries + * @param config The configuration to use for this pool instance. The + * configuration is used by value. Subsequent changes to + * the configuration object will not be reflected in the + * pool. + */ + public GenericKeyedObjectPool(final KeyedPooledObjectFactory factory, + final GenericKeyedObjectPoolConfig config) { + + super(config, ONAME_BASE, config.getJmxNamePrefix()); + + if (factory == null) { + jmxUnregister(); // tidy up + throw new IllegalArgumentException("Factory may not be null"); + } + this.factory = factory; + this.fairness = config.getFairness(); + + setConfig(config); + } + + /** + * Creates a new {@code GenericKeyedObjectPool} that tracks and destroys + * objects that are checked out, but never returned to the pool. + * + * @param factory The object factory to be used to create object instances + * used by this pool + * @param config The base pool configuration to use for this pool instance. + * The configuration is used by value. Subsequent changes to + * the configuration object will not be reflected in the + * pool. + * @param abandonedConfig Configuration for abandoned object identification + * and removal. The configuration is used by value. + * @since 2.10.0 + */ + public GenericKeyedObjectPool(final KeyedPooledObjectFactory factory, + final GenericKeyedObjectPoolConfig config, final AbandonedConfig abandonedConfig) { + this(factory, config); + setAbandonedConfig(abandonedConfig); + } + + /** + * Add an object to the set of idle objects for a given key. + * If the object is null this is a no-op. + * + * @param key The key to associate with the idle object + * @param p The wrapped object to add. + * + * @throws Exception If the associated factory fails to passivate the object + */ + private void addIdleObject(final K key, final PooledObject p) throws Exception { + if (!PooledObject.isNull(p)) { + factory.passivateObject(key, p); + final LinkedBlockingDeque> idleObjects = poolMap.get(key).getIdleObjects(); + if (getLifo()) { + idleObjects.addFirst(p); + } else { + idleObjects.addLast(p); + } + } + } + + /** + * Create an object using the {@link KeyedPooledObjectFactory#makeObject + * factory}, passivate it, and then place it in the idle object pool. + * {@code addObject} is useful for "pre-loading" a pool with idle + * objects. + *

    + * If there is no capacity available to add to the pool under the given key, + * this is a no-op (no exception, no impact to the pool). + *

    + *

    + * If the factory returns null when creating an instance, + * a {@code NullPointerException} is thrown. + *

    + * + * @param key the key a new instance should be added to + * + * @throws Exception when {@link KeyedPooledObjectFactory#makeObject} + * fails. + */ + @Override + public void addObject(final K key) throws Exception { + assertOpen(); + register(key); + try { + addIdleObject(key, create(key)); + } finally { + deregister(key); + } + } + + /** + * Equivalent to {@link #borrowObject(Object, long) borrowObject}(key, + * {@link #getMaxWaitDuration()}). + * + * {@inheritDoc} + */ + @Override + public T borrowObject(final K key) throws Exception { + return borrowObject(key, getMaxWaitDuration().toMillis()); + } + + /** + * Borrows an object from the sub-pool associated with the given key using + * the specified waiting time which only applies if + * {@link #getBlockWhenExhausted()} is true. + *

    + * If there is one or more idle instances available in the sub-pool + * associated with the given key, then an idle instance will be selected + * based on the value of {@link #getLifo()}, activated and returned. If + * activation fails, or {@link #getTestOnBorrow() testOnBorrow} is set to + * {@code true} and validation fails, the instance is destroyed and the + * next available instance is examined. This continues until either a valid + * instance is returned or there are no more idle instances available. + *

    + *

    + * If there are no idle instances available in the sub-pool associated with + * the given key, behavior depends on the {@link #getMaxTotalPerKey() + * maxTotalPerKey}, {@link #getMaxTotal() maxTotal}, and (if applicable) + * {@link #getBlockWhenExhausted()} and the value passed in to the + * {@code borrowMaxWaitMillis} parameter. If the number of instances checked + * out from the sub-pool under the given key is less than + * {@code maxTotalPerKey} and the total number of instances in + * circulation (under all keys) is less than {@code maxTotal}, a new + * instance is created, activated and (if applicable) validated and returned + * to the caller. If validation fails, a {@code NoSuchElementException} + * will be thrown. If the factory returns null when creating an instance, + * a {@code NullPointerException} is thrown. + *

    + *

    + * If the associated sub-pool is exhausted (no available idle instances and + * no capacity to create new ones), this method will either block + * ({@link #getBlockWhenExhausted()} is true) or throw a + * {@code NoSuchElementException} + * ({@link #getBlockWhenExhausted()} is false). + * The length of time that this method will block when + * {@link #getBlockWhenExhausted()} is true is determined by the value + * passed in to the {@code borrowMaxWait} parameter. + *

    + *

    + * When {@code maxTotal} is set to a positive value and this method is + * invoked when at the limit with no idle instances available under the requested + * key, an attempt is made to create room by clearing the oldest 15% of the + * elements from the keyed sub-pools. + *

    + *

    + * When the pool is exhausted, multiple calling threads may be + * simultaneously blocked waiting for instances to become available. A + * "fairness" algorithm has been implemented to ensure that threads receive + * available instances in request arrival order. + *

    + * + * @param key pool key + * @param borrowMaxWaitMillis The time to wait in milliseconds for an object + * to become available + * + * @return object instance from the keyed pool + * + * @throws NoSuchElementException if a keyed object instance cannot be + * returned because the pool is exhausted. + * + * @throws Exception if a keyed object instance cannot be returned due to an + * error + */ + public T borrowObject(final K key, final long borrowMaxWaitMillis) throws Exception { + assertOpen(); + + final AbandonedConfig ac = this.abandonedConfig; + if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 && + getNumActive() > getMaxTotal() - 3) { + removeAbandoned(ac); + } + + PooledObject p = null; + + // Get local copy of current config so it is consistent for entire + // method execution + final boolean blockWhenExhausted = getBlockWhenExhausted(); + + boolean create; + final Instant waitTime = Instant.now(); + final ObjectDeque objectDeque = register(key); + + try { + while (p == null) { + create = false; + p = objectDeque.getIdleObjects().pollFirst(); + if (p == null) { + p = create(key); + if (!PooledObject.isNull(p)) { + create = true; + } + } + if (blockWhenExhausted) { + if (PooledObject.isNull(p)) { + p = borrowMaxWaitMillis < 0 ? objectDeque.getIdleObjects().takeFirst(): + objectDeque.getIdleObjects().pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS); + } + if (PooledObject.isNull(p)) { + throw new NoSuchElementException(appendStats( + "Timeout waiting for idle object, borrowMaxWaitMillis=" + borrowMaxWaitMillis)); + } + } else if (PooledObject.isNull(p)) { + throw new NoSuchElementException(appendStats("Pool exhausted")); + } + if (!p.allocate()) { + p = null; + } + + if (!PooledObject.isNull(p)) { + try { + factory.activateObject(key, p); + } catch (final Exception e) { + try { + destroy(key, p, true, DestroyMode.NORMAL); + } catch (final Exception ignored) { + // ignored - activation failure is more important + } + p = null; + if (create) { + final NoSuchElementException nsee = new NoSuchElementException(appendStats("Unable to activate object")); + nsee.initCause(e); + throw nsee; + } + } + if (!PooledObject.isNull(p) && getTestOnBorrow()) { + boolean validate = false; + Throwable validationThrowable = null; + try { + validate = factory.validateObject(key, p); + } catch (final Throwable t) { + PoolUtils.checkRethrow(t); + validationThrowable = t; + } + if (!validate) { + try { + destroy(key, p, true, DestroyMode.NORMAL); + destroyedByBorrowValidationCount.incrementAndGet(); + } catch (final Exception ignored) { + // ignored - validation failure is more important + } + p = null; + if (create) { + final NoSuchElementException nsee = new NoSuchElementException( + appendStats("Unable to validate object")); + nsee.initCause(validationThrowable); + throw nsee; + } + } + } + } + } + } finally { + deregister(key); + } + + updateStatsBorrow(p, Duration.between(waitTime, Instant.now())); + + return p.getObject(); + } + + /** + * Calculate the number of objects that need to be created to attempt to + * maintain the minimum number of idle objects while not exceeded the limits + * on the maximum number of objects either per key or totally. + * + * @param objectDeque The set of objects to check + * + * @return The number of new objects to create + */ + private int calculateDeficit(final ObjectDeque objectDeque) { + + if (objectDeque == null) { + return getMinIdlePerKey(); + } + + // Used more than once so keep a local copy so the value is consistent + final int maxTotal = getMaxTotal(); + final int maxTotalPerKeySave = getMaxTotalPerKey(); + + // Calculate no of objects needed to be created, in order to have + // the number of pooled objects < maxTotalPerKey(); + int objectDefecit = getMinIdlePerKey() - objectDeque.getIdleObjects().size(); + if (maxTotalPerKeySave > 0) { + final int growLimit = Math.max(0, + maxTotalPerKeySave - objectDeque.getIdleObjects().size()); + objectDefecit = Math.min(objectDefecit, growLimit); + } + + // Take the maxTotal limit into account + if (maxTotal > 0) { + final int growLimit = Math.max(0, maxTotal - getNumActive() - getNumIdle()); + objectDefecit = Math.min(objectDefecit, growLimit); + } + + return objectDefecit; + } + + /** + * Clears any objects sitting idle in the pool by removing them from the + * idle instance sub-pools and then invoking the configured + * PoolableObjectFactory's + * {@link KeyedPooledObjectFactory#destroyObject(Object, PooledObject)} + * method on each idle instance. + *

    + * Implementation notes: + *

      + *
    • This method does not destroy or effect in any way instances that are + * checked out when it is invoked.
    • + *
    • Invoking this method does not prevent objects being returned to the + * idle instance pool, even during its execution. Additional instances may + * be returned while removed items are being destroyed.
    • + *
    • Exceptions encountered destroying idle instances are swallowed + * but notified via a {@link SwallowedExceptionListener}.
    • + *
    + */ + @Override + public void clear() { + poolMap.keySet().forEach(key -> clear(key, false)); + } + + /** + * Clears the specified sub-pool, removing all pooled instances + * corresponding to the given {@code key}. Exceptions encountered + * destroying idle instances are swallowed but notified via a + * {@link SwallowedExceptionListener}. + *

    + * If there are clients waiting to borrow objects, this method will + * attempt to reuse the capacity freed by this operation, adding + * instances to the most loaded keyed pools. To avoid triggering + * possible object creation, use {@link #clear(Object, boolean)}. + * + * @param key the key to clear + */ + @Override + public void clear(final K key) { + clear(key, true); + } + + /** + * Clears the specified sub-pool, removing all pooled instances + * corresponding to the given {@code key}. Exceptions encountered + * destroying idle instances are swallowed but notified via a + * {@link SwallowedExceptionListener}. + *

    + * If reuseCapacity is true and there are clients waiting to + * borrow objects, this method will attempt to reuse the capacity freed + * by this operation, adding instances to the most loaded keyed pools. + *

    + * + * @param key the key to clear + * @param reuseCapacity whether or not to reuse freed capacity + * @since 2.12.0 + */ + public void clear(final K key, final boolean reuseCapacity) { + // Return immediately if there is no pool under this key. + if (!poolMap.containsKey(key)) { + return; + } + final ObjectDeque objectDeque = register(key); + int freedCapacity = 0; + try { + final LinkedBlockingDeque> idleObjects = objectDeque.getIdleObjects(); + PooledObject p = idleObjects.poll(); + while (p != null) { + try { + if (destroy(key, p, true, DestroyMode.NORMAL)) { + freedCapacity++; + } + } catch (final Exception e) { + swallowException(e); + } + p = idleObjects.poll(); + } + } finally { + deregister(key); + } + if (reuseCapacity) { + reuseCapacity(freedCapacity); + } + } + + /** + * Clears oldest 15% of objects in pool. The method sorts the objects into + * a TreeMap and then iterates the first 15% for removal. + */ + public void clearOldest() { + + // build sorted map of idle objects + final TreeMap, K> map = new TreeMap<>(); + + poolMap.forEach((key, value) -> { + // Each item into the map using the PooledObject object as the + // key. It then gets sorted based on the idle time + value.getIdleObjects().forEach(p -> map.put(p, key)); + }); + + // Now iterate created map and kill the first 15% plus one to account + // for zero + int itemsToRemove = (int) (map.size() * 0.15) + 1; + final Iterator, K>> iter = map.entrySet().iterator(); + + while (iter.hasNext() && itemsToRemove > 0) { + final Entry, K> entry = iter.next(); + // kind of backwards on naming. In the map, each key is the + // PooledObject because it has the ordering with the timestamp + // value. Each value that the key references is the key of the + // list it belongs to. + final K key = entry.getValue(); + final PooledObject p = entry.getKey(); + // Assume the destruction succeeds + boolean destroyed = true; + try { + destroyed = destroy(key, p, false, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + if (destroyed) { + itemsToRemove--; + } + } + } + + + /** + * Closes the keyed object pool. Once the pool is closed, + * {@link #borrowObject(Object)} will fail with IllegalStateException, but + * {@link #returnObject(Object, Object)} and + * {@link #invalidateObject(Object, Object)} will continue to work, with + * returned objects destroyed on return. + *

    + * Destroys idle instances in the pool by invoking {@link #clear()}. + *

    + */ + @Override + public void close() { + if (isClosed()) { + return; + } + + synchronized (closeLock) { + if (isClosed()) { + return; + } + + // Stop the evictor before the pool is closed since evict() calls + // assertOpen() + stopEvictor(); + + closed = true; + // This clear removes any idle objects + clear(); + + jmxUnregister(); + + // Release any threads that were waiting for an object + poolMap.values().forEach(e -> e.getIdleObjects().interuptTakeWaiters()); + // This clear cleans up the keys now any waiting threads have been + // interrupted + clear(); + } + } + + + /** + * Creates a new pooled object or null. + * + * @param key Key associated with new pooled object. + * + * @return The new, wrapped pooled object. May return null. + * + * @throws Exception If the objection creation fails. + */ + private PooledObject create(final K key) throws Exception { + int maxTotalPerKeySave = getMaxTotalPerKey(); // Per key + if (maxTotalPerKeySave < 0) { + maxTotalPerKeySave = Integer.MAX_VALUE; + } + final int maxTotal = getMaxTotal(); // All keys + + final ObjectDeque objectDeque = poolMap.get(key); + + // Check against the overall limit + boolean loop = true; + + while (loop) { + final int newNumTotal = numTotal.incrementAndGet(); + if (maxTotal > -1 && newNumTotal > maxTotal) { + numTotal.decrementAndGet(); + if (getNumIdle() == 0) { + return null; + } + clearOldest(); + } else { + loop = false; + } + } + + // Flag that indicates if create should: + // - TRUE: call the factory to create an object + // - FALSE: return null + // - null: loop and re-test the condition that determines whether to + // call the factory + Boolean create = null; + while (create == null) { + synchronized (objectDeque.makeObjectCountLock) { + final long newCreateCount = objectDeque.getCreateCount().incrementAndGet(); + // Check against the per key limit + if (newCreateCount > maxTotalPerKeySave) { + // The key is currently at capacity or in the process of + // making enough new objects to take it to capacity. + objectDeque.getCreateCount().decrementAndGet(); + if (objectDeque.makeObjectCount == 0) { + // There are no makeObject() calls in progress for this + // key so the key is at capacity. Do not attempt to + // create a new object. Return and wait for an object to + // be returned. + create = Boolean.FALSE; + } else { + // There are makeObject() calls in progress that might + // bring the pool to capacity. Those calls might also + // fail so wait until they complete and then re-test if + // the pool is at capacity or not. + objectDeque.makeObjectCountLock.wait(); + } + } else { + // The pool is not at capacity. Create a new object. + objectDeque.makeObjectCount++; + create = Boolean.TRUE; + } + } + } + + if (!create.booleanValue()) { + numTotal.decrementAndGet(); + return null; + } + + PooledObject p = null; + try { + p = factory.makeObject(key); + if (PooledObject.isNull(p)) { + numTotal.decrementAndGet(); + objectDeque.getCreateCount().decrementAndGet(); + throw new NullPointerException(String.format("%s.makeObject() = null", factory.getClass().getSimpleName())); + } + if (getTestOnCreate() && !factory.validateObject(key, p)) { + numTotal.decrementAndGet(); + objectDeque.getCreateCount().decrementAndGet(); + return null; + } + } catch (final Exception e) { + numTotal.decrementAndGet(); + objectDeque.getCreateCount().decrementAndGet(); + throw e; + } finally { + synchronized (objectDeque.makeObjectCountLock) { + objectDeque.makeObjectCount--; + objectDeque.makeObjectCountLock.notifyAll(); + } + } + + final AbandonedConfig ac = this.abandonedConfig; + if (ac != null && ac.getLogAbandoned()) { + p.setLogAbandoned(true); + p.setRequireFullStackTrace(ac.getRequireFullStackTrace()); + } + + createdCount.incrementAndGet(); + objectDeque.getAllObjects().put(new IdentityWrapper<>(p.getObject()), p); + return p; + } + + /** + * De-register the use of a key by an object. + *

    + * {@link #register(Object)} and {@link #deregister(Object)} must always be used as a pair. + *

    + * + * @param k The key to de-register + */ + private void deregister(final K k) { + Lock lock = keyLock.readLock(); + try { + lock.lock(); + ObjectDeque objectDeque = poolMap.get(k); + if (objectDeque == null) { + throw new IllegalStateException("Attempt to de-register a key for a non-existent pool"); + } + final long numInterested = objectDeque.getNumInterested().decrementAndGet(); + if (numInterested < 0) { + throw new IllegalStateException("numInterested count for key " + k + " is less than zero"); + } + if (numInterested == 0 && objectDeque.getCreateCount().get() == 0) { + // Potential to remove key + // Upgrade to write lock + lock.unlock(); + lock = keyLock.writeLock(); + lock.lock(); + // Pool may have changed since we released the read lock + // numInterested decrement could lead to removal while waiting for write lock + objectDeque = poolMap.get(k); + if (null != objectDeque && objectDeque.getNumInterested().get() == 0 && objectDeque.getCreateCount().get() == 0) { + // NOTE: Keys must always be removed from both poolMap and + // poolKeyList at the same time while protected by + // keyLock.writeLock() + poolMap.remove(k); + poolKeyList.remove(k); + } + } + } finally { + lock.unlock(); + } + } + + /** + * Destroy the wrapped, pooled object. + * + * @param key The key associated with the object to destroy. + * @param toDestroy The wrapped object to be destroyed + * @param always Should the object be destroyed even if it is not currently + * in the set of idle objects for the given key + * @param destroyMode DestroyMode context provided to the factory + * + * @return {@code true} if the object was destroyed, otherwise {@code false} + * @throws Exception If the object destruction failed + */ + private boolean destroy(final K key, final PooledObject toDestroy, final boolean always, final DestroyMode destroyMode) throws Exception { + + final ObjectDeque objectDeque = register(key); + + try { + boolean isIdle; + synchronized (toDestroy) { + // Check idle state directly + isIdle = toDestroy.getState().equals(PooledObjectState.IDLE); + // If idle, not under eviction test, or always is true, remove instance, + // updating isIdle if instance is found in idle objects + if (isIdle || always) { + isIdle = objectDeque.getIdleObjects().remove(toDestroy); + } + } + if (isIdle || always) { + objectDeque.getAllObjects().remove(new IdentityWrapper<>(toDestroy.getObject())); + toDestroy.invalidate(); + + try { + factory.destroyObject(key, toDestroy, destroyMode); + } finally { + objectDeque.getCreateCount().decrementAndGet(); + destroyedCount.incrementAndGet(); + numTotal.decrementAndGet(); + } + return true; + } + return false; + } finally { + deregister(key); + } + } + + + @Override + void ensureMinIdle() throws Exception { + final int minIdlePerKeySave = getMinIdlePerKey(); + if (minIdlePerKeySave < 1) { + return; + } + + for (final K k : poolMap.keySet()) { + ensureMinIdle(k); + } + } + + /** + * Try to ensure that the configured number of minimum idle objects is available in + * the pool for the given key. + *

    + * If there is no capacity available to add to the pool, this is a no-op + * (no exception, no impact to the pool). + *

    + *

    + * If the factory returns null when creating an object, a {@code NullPointerException} + * is thrown. + *

    + * + * @param key The key to check for idle objects + * + * @throws Exception If a new object is required and cannot be created + */ + private void ensureMinIdle(final K key) throws Exception { + // Calculate current pool objects + ObjectDeque objectDeque = poolMap.get(key); + + // objectDeque == null is OK here. It is handled correctly by both + // methods called below. + + // this method isn't synchronized so the + // calculateDeficit is done at the beginning + // as a loop limit and a second time inside the loop + // to stop when another thread already returned the + // needed objects + final int deficit = calculateDeficit(objectDeque); + + for (int i = 0; i < deficit && calculateDeficit(objectDeque) > 0; i++) { + addObject(key); + // If objectDeque was null, it won't be any more. Obtain a reference + // to it so the deficit can be correctly calculated. It needs to + // take account of objects created in other threads. + if (objectDeque == null) { + objectDeque = poolMap.get(key); + } + } + } + + /** + * {@inheritDoc} + *

    + * Successive activations of this method examine objects in keyed sub-pools + * in sequence, cycling through the keys and examining objects in + * oldest-to-youngest order within the keyed sub-pools. + *

    + */ + @Override + public void evict() throws Exception { + assertOpen(); + + if (getNumIdle() > 0) { + + PooledObject underTest = null; + final EvictionPolicy evictionPolicy = getEvictionPolicy(); + + synchronized (evictionLock) { + final EvictionConfig evictionConfig = new EvictionConfig( + getMinEvictableIdleDuration(), + getSoftMinEvictableIdleDuration(), + getMinIdlePerKey()); + + final boolean testWhileIdle = getTestWhileIdle(); + + for (int i = 0, m = getNumTests(); i < m; i++) { + if (evictionIterator == null || !evictionIterator.hasNext()) { + if (evictionKeyIterator == null || + !evictionKeyIterator.hasNext()) { + final List keyCopy = new ArrayList<>(); + final Lock readLock = keyLock.readLock(); + readLock.lock(); + try { + keyCopy.addAll(poolKeyList); + } finally { + readLock.unlock(); + } + evictionKeyIterator = keyCopy.iterator(); + } + while (evictionKeyIterator.hasNext()) { + evictionKey = evictionKeyIterator.next(); + final ObjectDeque objectDeque = poolMap.get(evictionKey); + if (objectDeque == null) { + continue; + } + + final Deque> idleObjects = objectDeque.getIdleObjects(); + evictionIterator = new EvictionIterator(idleObjects); + if (evictionIterator.hasNext()) { + break; + } + evictionIterator = null; + } + } + if (evictionIterator == null) { + // Pools exhausted + return; + } + final Deque> idleObjects; + try { + underTest = evictionIterator.next(); + idleObjects = evictionIterator.getIdleObjects(); + } catch (final NoSuchElementException nsee) { + // Object was borrowed in another thread + // Don't count this as an eviction test so reduce i; + i--; + evictionIterator = null; + continue; + } + + if (!underTest.startEvictionTest()) { + // Object was borrowed in another thread + // Don't count this as an eviction test so reduce i; + i--; + continue; + } + + // User provided eviction policy could throw all sorts of + // crazy exceptions. Protect against such an exception + // killing the eviction thread. + boolean evict; + try { + evict = evictionPolicy.evict(evictionConfig, underTest, + poolMap.get(evictionKey).getIdleObjects().size()); + } catch (final Throwable t) { + // Slightly convoluted as SwallowedExceptionListener + // uses Exception rather than Throwable + PoolUtils.checkRethrow(t); + swallowException(new Exception(t)); + // Don't evict on error conditions + evict = false; + } + + if (evict) { + destroy(evictionKey, underTest, true, DestroyMode.NORMAL); + destroyedByEvictorCount.incrementAndGet(); + } else { + if (testWhileIdle) { + boolean active = false; + try { + factory.activateObject(evictionKey, underTest); + active = true; + } catch (final Exception e) { + destroy(evictionKey, underTest, true, DestroyMode.NORMAL); + destroyedByEvictorCount.incrementAndGet(); + } + if (active) { + boolean validate = false; + Throwable validationThrowable = null; + try { + validate = factory.validateObject(evictionKey, underTest); + } catch (final Throwable t) { + PoolUtils.checkRethrow(t); + validationThrowable = t; + } + if (!validate) { + destroy(evictionKey, underTest, true, DestroyMode.NORMAL); + destroyedByEvictorCount.incrementAndGet(); + if (validationThrowable != null) { + if (validationThrowable instanceof RuntimeException) { + throw (RuntimeException) validationThrowable; + } + throw (Error) validationThrowable; + } + } else { + try { + factory.passivateObject(evictionKey, underTest); + } catch (final Exception e) { + destroy(evictionKey, underTest, true, DestroyMode.NORMAL); + destroyedByEvictorCount.incrementAndGet(); + } + } + } + } + underTest.endEvictionTest(idleObjects); + // TODO - May need to add code here once additional + // states are used + } + } + } + } + final AbandonedConfig ac = this.abandonedConfig; + if (ac != null && ac.getRemoveAbandonedOnMaintenance()) { + removeAbandoned(ac); + } + } + + + /** + * Gets a reference to the factory used to create, destroy and validate + * the objects used by this pool. + * + * @return the factory + */ + public KeyedPooledObjectFactory getFactory() { + return factory; + } + + /** + * Gets a copy of the pool key list. + * + * @return a copy of the pool key list. + * @since 2.12.0 + */ + @Override + @SuppressWarnings("unchecked") + public List getKeys() { + return (List) poolKeyList.clone(); + } + + /** + * Gets the cap on the number of "idle" instances per key in the pool. + * If maxIdlePerKey is set too low on heavily loaded systems it is possible + * you will see objects being destroyed and almost immediately new objects + * being created. This is a result of the active threads momentarily + * returning objects faster than they are requesting them, causing the + * number of idle objects to rise above maxIdlePerKey. The best value for + * maxIdlePerKey for heavily loaded system will vary but the default is a + * good starting point. + * + * @return the maximum number of "idle" instances that can be held in a + * given keyed sub-pool or a negative value if there is no limit + * + * @see #setMaxIdlePerKey + */ + @Override + public int getMaxIdlePerKey() { + return maxIdlePerKey; + } + + /** + * Gets the limit on the number of object instances allocated by the pool + * (checked out or idle), per key. When the limit is reached, the sub-pool + * is said to be exhausted. A negative value indicates no limit. + * + * @return the limit on the number of active instances per key + * + * @see #setMaxTotalPerKey + */ + @Override + public int getMaxTotalPerKey() { + return maxTotalPerKey; + } + + /** + * Gets the target for the minimum number of idle objects to maintain in + * each of the keyed sub-pools. This setting only has an effect if it is + * positive and {@link #getDurationBetweenEvictionRuns()} is greater than + * zero. If this is the case, an attempt is made to ensure that each + * sub-pool has the required minimum number of instances during idle object + * eviction runs. + *

    + * If the configured value of minIdlePerKey is greater than the configured + * value for maxIdlePerKey then the value of maxIdlePerKey will be used + * instead. + *

    + * + * @return minimum size of the each keyed pool + * + * @see #setTimeBetweenEvictionRunsMillis + */ + @Override + public int getMinIdlePerKey() { + final int maxIdlePerKeySave = getMaxIdlePerKey(); + return Math.min(this.minIdlePerKey, maxIdlePerKeySave); + } + + @Override + public int getNumActive() { + return numTotal.get() - getNumIdle(); + } + + @Override + public int getNumActive(final K key) { + final ObjectDeque objectDeque = poolMap.get(key); + if (objectDeque != null) { + return objectDeque.getAllObjects().size() - + objectDeque.getIdleObjects().size(); + } + return 0; + } + + @Override + public Map getNumActivePerKey() { + return poolMap.entrySet().stream().collect(Collectors.toMap( + e -> e.getKey().toString(), + e -> Integer.valueOf(e.getValue().getAllObjects().size() - e.getValue().getIdleObjects().size()), + (t, u) -> u)); + } + + @Override + public int getNumIdle() { + return poolMap.values().stream().mapToInt(e -> e.getIdleObjects().size()).sum(); + } + + @Override + public int getNumIdle(final K key) { + final ObjectDeque objectDeque = poolMap.get(key); + return objectDeque != null ? objectDeque.getIdleObjects().size() : 0; + } + + /** + * Gets the number of objects to test in a run of the idle object + * evictor. + * + * @return The number of objects to test for validity + */ + private int getNumTests() { + final int totalIdle = getNumIdle(); + final int numTests = getNumTestsPerEvictionRun(); + if (numTests >= 0) { + return Math.min(numTests, totalIdle); + } + return (int) Math.ceil(totalIdle / Math.abs((double) numTests)); + } + + /** + * Gets an estimate of the number of threads currently blocked waiting for + * an object from the pool. This is intended for monitoring only, not for + * synchronization control. + * + * @return The estimate of the number of threads currently blocked waiting + * for an object from the pool + */ + @Override + public int getNumWaiters() { + if (getBlockWhenExhausted()) { + // Assume no overflow + return poolMap.values().stream().mapToInt(e -> e.getIdleObjects().getTakeQueueLength()).sum(); + } + return 0; + } + + /** + * Gets an estimate of the number of threads currently blocked waiting for + * an object from the pool for each key. This is intended for + * monitoring only, not for synchronization control. + * + * @return The estimate of the number of threads currently blocked waiting + * for an object from the pool for each key + */ + @Override + public Map getNumWaitersByKey() { + final Map result = new HashMap<>(); + poolMap.forEach((k, deque) -> result.put(k.toString(), getBlockWhenExhausted() ? + Integer.valueOf(deque.getIdleObjects().getTakeQueueLength()) : + ZERO)); + return result; + } + + @SuppressWarnings("boxing") // Commons Pool uses auto-boxing + @Override + String getStatsString() { + // Simply listed in AB order. + return super.getStatsString() + + String.format(", fairness=%s, maxIdlePerKey%,d, maxTotalPerKey=%,d, minIdlePerKey=%,d, numTotal=%,d", + fairness, maxIdlePerKey, maxTotalPerKey, minIdlePerKey, numTotal.get()); + } + + /** + * Tests to see if there are any threads currently waiting to borrow + * objects but are blocked waiting for more objects to become available. + * + * @return {@code true} if there is at least one thread waiting otherwise + * {@code false} + */ + private boolean hasBorrowWaiters() { + return getBlockWhenExhausted() && poolMap.values().stream().anyMatch(deque -> deque.getIdleObjects().hasTakeWaiters()); + } + + /** + * {@inheritDoc} + *

    + * Activation of this method decrements the active count associated with + * the given keyed pool and attempts to destroy {@code obj.} + *

    + * + * @param key pool key + * @param obj instance to invalidate + * + * @throws Exception if an exception occurs destroying the + * object + * @throws IllegalStateException if obj does not belong to the pool + * under the given key + */ + @Override + public void invalidateObject(final K key, final T obj) throws Exception { + invalidateObject(key, obj, DestroyMode.NORMAL); + } + + /** + * {@inheritDoc} + *

    + * Activation of this method decrements the active count associated with + * the given keyed pool and attempts to destroy {@code obj.} + *

    + * + * @param key pool key + * @param obj instance to invalidate + * @param destroyMode DestroyMode context provided to factory + * + * @throws Exception if an exception occurs destroying the + * object + * @throws IllegalStateException if obj does not belong to the pool + * under the given key + * @since 2.9.0 + */ + @Override + public void invalidateObject(final K key, final T obj, final DestroyMode destroyMode) throws Exception { + final ObjectDeque objectDeque = poolMap.get(key); + final PooledObject p = objectDeque != null ? objectDeque.getAllObjects().get(new IdentityWrapper<>(obj)) : null; + if (p == null) { + throw new IllegalStateException(appendStats("Object not currently part of this pool")); + } + synchronized (p) { + if (p.getState() != PooledObjectState.INVALID) { + destroy(key, p, true, destroyMode); + reuseCapacity(); + } + } + } + + /** + * Provides information on all the objects in the pool, both idle (waiting + * to be borrowed) and active (currently borrowed). + *

    + * Note: This is named listAllObjects so it is presented as an operation via + * JMX. That means it won't be invoked unless the explicitly requested + * whereas all attributes will be automatically requested when viewing the + * attributes for an object in a tool like JConsole. + *

    + * + * @return Information grouped by key on all the objects in the pool + */ + @Override + public Map> listAllObjects() { + return poolMap.entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toString(), + e -> e.getValue().getAllObjects().values().stream().map(DefaultPooledObjectInfo::new).collect(Collectors.toList()))); + } + + /** + * Registers a key for pool control and ensures that + * {@link #getMinIdlePerKey()} idle instances are created. + * + * @param key - The key to register for pool control. + * + * @throws Exception If the associated factory throws an exception + */ + public void preparePool(final K key) throws Exception { + final int minIdlePerKeySave = getMinIdlePerKey(); + if (minIdlePerKeySave < 1) { + return; + } + ensureMinIdle(key); + } + + /** + * Register the use of a key by an object. + *

    + * {@link #register(Object)} and {@link #deregister(Object)} must always be used as a pair. + *

    + * + * @param k The key to register + * + * @return The objects currently associated with the given key. If this + * method returns without throwing an exception then it will never + * return null. + */ + private ObjectDeque register(final K k) { + Lock lock = keyLock.readLock(); + ObjectDeque objectDeque = null; + try { + lock.lock(); + objectDeque = poolMap.get(k); + if (objectDeque == null) { + // Upgrade to write lock + lock.unlock(); + lock = keyLock.writeLock(); + lock.lock(); + final AtomicBoolean allocated = new AtomicBoolean(); + objectDeque = poolMap.computeIfAbsent(k, key -> { + allocated.set(true); + final ObjectDeque deque = new ObjectDeque<>(fairness); + deque.getNumInterested().incrementAndGet(); + // NOTE: Keys must always be added to both poolMap and + // poolKeyList at the same time while protected by + // the write lock + poolKeyList.add(k); + return deque; + }); + if (!allocated.get()) { + // Another thread could have beaten us to creating the deque, while we were + // waiting for the write lock, so re-get it from the map. + objectDeque = poolMap.get(k); + objectDeque.getNumInterested().incrementAndGet(); + } + } else { + objectDeque.getNumInterested().incrementAndGet(); + } + } finally { + lock.unlock(); + } + return objectDeque; + } + + /** + * Recovers abandoned objects which have been checked out but + * not used since longer than the removeAbandonedTimeout. + * + * @param abandonedConfig The configuration to use to identify abandoned objects + */ + private void removeAbandoned(final AbandonedConfig abandonedConfig) { + poolMap.forEach((key, value) -> { + // Generate a list of abandoned objects to remove + final ArrayList> remove = createRemoveList(abandonedConfig, value.getAllObjects()); + // Now remove the abandoned objects + remove.forEach(pooledObject -> { + if (abandonedConfig.getLogAbandoned()) { + pooledObject.printStackTrace(abandonedConfig.getLogWriter()); + } + try { + invalidateObject(key, pooledObject.getObject(), DestroyMode.ABANDONED); + } catch (final Exception e) { + swallowException(e); + } + }); + }); + } + + /** + * Returns an object to a keyed sub-pool. + *

    + * If {@link #getMaxIdlePerKey() maxIdle} is set to a positive value and the + * number of idle instances under the given key has reached this value, the + * returning instance is destroyed. + *

    + *

    + * If {@link #getTestOnReturn() testOnReturn} == true, the returning + * instance is validated before being returned to the idle instance sub-pool + * under the given key. In this case, if validation fails, the instance is + * destroyed. + *

    + *

    + * Exceptions encountered destroying objects for any reason are swallowed + * but notified via a {@link SwallowedExceptionListener}. + *

    + * + * @param key pool key + * @param obj instance to return to the keyed pool + * + * @throws IllegalStateException if an object is returned to the pool that + * was not borrowed from it or if an object is + * returned to the pool multiple times + */ + @Override + public void returnObject(final K key, final T obj) { + + final ObjectDeque objectDeque = poolMap.get(key); + + if (objectDeque == null) { + throw new IllegalStateException("No keyed pool found under the given key."); + } + + final PooledObject p = objectDeque.getAllObjects().get(new IdentityWrapper<>(obj)); + + if (PooledObject.isNull(p)) { + throw new IllegalStateException("Returned object not currently part of this pool"); + } + + markReturningState(p); + + final Duration activeTime = p.getActiveDuration(); + + try { + if (getTestOnReturn() && !factory.validateObject(key, p)) { + try { + destroy(key, p, true, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + whenWaitersAddObject(key, objectDeque.idleObjects); + return; + } + + try { + factory.passivateObject(key, p); + } catch (final Exception e1) { + swallowException(e1); + try { + destroy(key, p, true, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + whenWaitersAddObject(key, objectDeque.idleObjects); + return; + } + + if (!p.deallocate()) { + throw new IllegalStateException("Object has already been returned to this pool"); + } + + final int maxIdle = getMaxIdlePerKey(); + final LinkedBlockingDeque> idleObjects = objectDeque.getIdleObjects(); + + if (isClosed() || maxIdle > -1 && maxIdle <= idleObjects.size()) { + try { + destroy(key, p, true, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + } else { + if (getLifo()) { + idleObjects.addFirst(p); + } else { + idleObjects.addLast(p); + } + if (isClosed()) { + // Pool closed while object was being added to idle objects. + // Make sure the returned object is destroyed rather than left + // in the idle object pool (which would effectively be a leak) + clear(key); + } + } + } finally { + if (hasBorrowWaiters()) { + reuseCapacity(); + } + updateStatsReturn(activeTime); + } + } + + /** + * Attempt to create one new instance to serve from the most heavily + * loaded pool that can add a new instance. + * + * This method exists to ensure liveness in the pool when threads are + * parked waiting and capacity to create instances under the requested keys + * subsequently becomes available. + * + * This method is not guaranteed to create an instance and its selection + * of the most loaded pool that can create an instance may not always be + * correct, since it does not lock the pool and instances may be created, + * borrowed, returned or destroyed by other threads while it is executing. + */ + private void reuseCapacity() { + final int maxTotalPerKeySave = getMaxTotalPerKey(); + int maxQueueLength = 0; + LinkedBlockingDeque> mostLoadedPool = null; + K mostLoadedKey = null; + + // Find the most loaded pool that could take a new instance + for (final Map.Entry> entry : poolMap.entrySet()) { + final K k = entry.getKey(); + final LinkedBlockingDeque> pool = entry.getValue().getIdleObjects(); + final int queueLength = pool.getTakeQueueLength(); + if (getNumActive(k) < maxTotalPerKeySave && queueLength > maxQueueLength) { + maxQueueLength = queueLength; + mostLoadedPool = pool; + mostLoadedKey = k; + } + } + + // Attempt to add an instance to the most loaded pool. + if (mostLoadedPool != null) { + register(mostLoadedKey); + try { + // If there is no capacity to add, create will return null + // and addIdleObject will no-op. + addIdleObject(mostLoadedKey, create(mostLoadedKey)); + } catch (final Exception e) { + swallowException(e); + } finally { + deregister(mostLoadedKey); + } + } + } + + /** + * Call {@link #reuseCapacity()} repeatedly. + *

    + * Always activates {@link #reuseCapacity()} at least once. + * + * @param newCapacity number of new instances to attempt to create. + */ + private void reuseCapacity(final int newCapacity) { + final int bound = newCapacity < 1 ? 1 : newCapacity; + for (int i = 0; i < bound; i++) { + reuseCapacity(); + } + } + + /** + * Sets the configuration. + * + * @param conf the new configuration to use. This is used by value. + * + * @see GenericKeyedObjectPoolConfig + */ + public void setConfig(final GenericKeyedObjectPoolConfig conf) { + super.setConfig(conf); + setMaxIdlePerKey(conf.getMaxIdlePerKey()); + setMaxTotalPerKey(conf.getMaxTotalPerKey()); + setMaxTotal(conf.getMaxTotal()); + setMinIdlePerKey(conf.getMinIdlePerKey()); + } + + /** + * Sets the cap on the number of "idle" instances per key in the pool. + * If maxIdlePerKey is set too low on heavily loaded systems it is possible + * you will see objects being destroyed and almost immediately new objects + * being created. This is a result of the active threads momentarily + * returning objects faster than they are requesting them, causing the + * number of idle objects to rise above maxIdlePerKey. The best value for + * maxIdlePerKey for heavily loaded system will vary but the default is a + * good starting point. + * + * @param maxIdlePerKey the maximum number of "idle" instances that can be + * held in a given keyed sub-pool. Use a negative value + * for no limit + * + * @see #getMaxIdlePerKey + */ + public void setMaxIdlePerKey(final int maxIdlePerKey) { + this.maxIdlePerKey = maxIdlePerKey; + } + + /** + * Sets the limit on the number of object instances allocated by the pool + * (checked out or idle), per key. When the limit is reached, the sub-pool + * is said to be exhausted. A negative value indicates no limit. + * + * @param maxTotalPerKey the limit on the number of active instances per key + * + * @see #getMaxTotalPerKey + */ + public void setMaxTotalPerKey(final int maxTotalPerKey) { + this.maxTotalPerKey = maxTotalPerKey; + } + + /** + * Sets the target for the minimum number of idle objects to maintain in + * each of the keyed sub-pools. This setting only has an effect if it is + * positive and {@link #getDurationBetweenEvictionRuns()} is greater than + * zero. If this is the case, an attempt is made to ensure that each + * sub-pool has the required minimum number of instances during idle object + * eviction runs. + *

    + * If the configured value of minIdlePerKey is greater than the configured + * value for maxIdlePerKey then the value of maxIdlePerKey will be used + * instead. + *

    + * + * @param minIdlePerKey The minimum size of the each keyed pool + * + * @see #getMinIdlePerKey() + * @see #getMaxIdlePerKey() + * @see #setDurationBetweenEvictionRuns(Duration) + */ + public void setMinIdlePerKey(final int minIdlePerKey) { + this.minIdlePerKey = minIdlePerKey; + } + + @Override + protected void toStringAppendFields(final StringBuilder builder) { + super.toStringAppendFields(builder); + builder.append(", maxIdlePerKey="); + builder.append(maxIdlePerKey); + builder.append(", minIdlePerKey="); + builder.append(minIdlePerKey); + builder.append(", maxTotalPerKey="); + builder.append(maxTotalPerKey); + builder.append(", factory="); + builder.append(factory); + builder.append(", fairness="); + builder.append(fairness); + builder.append(", poolMap="); + builder.append(poolMap); + builder.append(", poolKeyList="); + builder.append(poolKeyList); + builder.append(", keyLock="); + builder.append(keyLock); + builder.append(", numTotal="); + builder.append(numTotal); + builder.append(", evictionKeyIterator="); + builder.append(evictionKeyIterator); + builder.append(", evictionKey="); + builder.append(evictionKey); + builder.append(", abandonedConfig="); + builder.append(abandonedConfig); + } + + /** + * @since 2.10.0 + */ + @Override + public void use(final T pooledObject) { + final AbandonedConfig abandonedCfg = this.abandonedConfig; + if (abandonedCfg != null && abandonedCfg.getUseUsageTracking()) { + poolMap.values().stream() + .map(pool -> pool.getAllObjects().get(new IdentityWrapper<>(pooledObject))) + .filter(Objects::nonNull) + .findFirst() + .ifPresent(PooledObject::use); + } + } + + /** + * When there is at least one thread waiting on the given deque, try to add an instance to pool under the given key. + * NOTE: there is no check that the key corresponds to the deque (it is assumed that the key was used to get the deque + * from the poolMap) + * + * @param key pool key. + * @param idleObjects deque backing the pool under the given key + */ + private void whenWaitersAddObject(final K key, final LinkedBlockingDeque> idleObjects) { + if (idleObjects.hasTakeWaiters()) { + try { + addObject(key); + } catch (final Exception e) { + swallowException(e); + } + } + } + +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java b/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java new file mode 100644 index 0000000..9b4ddd0 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java @@ -0,0 +1,198 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +/** + * A simple structure encapsulating the configuration for a + * {@link GenericKeyedObjectPool}. + * + *

    + * This class is not thread-safe; it is only intended to be used to provide + * attributes used when creating a pool. + *

    + * + * @param Type of element pooled. + * @since 2.0 + */ +public class GenericKeyedObjectPoolConfig extends BaseObjectPoolConfig { + + /** + * The default value for the {@code maxTotalPerKey} configuration attribute. + * @see GenericKeyedObjectPool#getMaxTotalPerKey() + */ + public static final int DEFAULT_MAX_TOTAL_PER_KEY = 8; + + /** + * The default value for the {@code maxTotal} configuration attribute. + * @see GenericKeyedObjectPool#getMaxTotal() + */ + public static final int DEFAULT_MAX_TOTAL = -1; + + /** + * The default value for the {@code minIdlePerKey} configuration attribute. + * @see GenericKeyedObjectPool#getMinIdlePerKey() + */ + public static final int DEFAULT_MIN_IDLE_PER_KEY = 0; + + /** + * The default value for the {@code maxIdlePerKey} configuration attribute. + * @see GenericKeyedObjectPool#getMaxIdlePerKey() + */ + public static final int DEFAULT_MAX_IDLE_PER_KEY = 8; + + + private int minIdlePerKey = DEFAULT_MIN_IDLE_PER_KEY; + + private int maxIdlePerKey = DEFAULT_MAX_IDLE_PER_KEY; + + private int maxTotalPerKey = DEFAULT_MAX_TOTAL_PER_KEY; + + private int maxTotal = DEFAULT_MAX_TOTAL; + + /** + * Constructs a new configuration with default settings. + */ + public GenericKeyedObjectPoolConfig() { + } + + @SuppressWarnings("unchecked") + @Override + public GenericKeyedObjectPoolConfig clone() { + try { + return (GenericKeyedObjectPoolConfig) super.clone(); + } catch (final CloneNotSupportedException e) { + throw new AssertionError(); // Can't happen + } + } + + /** + * Get the value for the {@code maxIdlePerKey} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code maxIdlePerKey} for this + * configuration instance + * + * @see GenericKeyedObjectPool#getMaxIdlePerKey() + */ + public int getMaxIdlePerKey() { + return maxIdlePerKey; + } + + /** + * Get the value for the {@code maxTotal} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code maxTotal} for this + * configuration instance + * + * @see GenericKeyedObjectPool#getMaxTotal() + */ + public int getMaxTotal() { + return maxTotal; + } + + /** + * Get the value for the {@code maxTotalPerKey} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code maxTotalPerKey} for this + * configuration instance + * + * @see GenericKeyedObjectPool#getMaxTotalPerKey() + */ + public int getMaxTotalPerKey() { + return maxTotalPerKey; + } + + /** + * Get the value for the {@code minIdlePerKey} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code minIdlePerKey} for this + * configuration instance + * + * @see GenericKeyedObjectPool#getMinIdlePerKey() + */ + public int getMinIdlePerKey() { + return minIdlePerKey; + } + + /** + * Set the value for the {@code maxIdlePerKey} configuration attribute for + * pools created with this configuration instance. + * + * @param maxIdlePerKey The new setting of {@code maxIdlePerKey} + * for this configuration instance + * + * @see GenericKeyedObjectPool#setMaxIdlePerKey(int) + */ + public void setMaxIdlePerKey(final int maxIdlePerKey) { + this.maxIdlePerKey = maxIdlePerKey; + } + + /** + * Set the value for the {@code maxTotal} configuration attribute for + * pools created with this configuration instance. + * + * @param maxTotal The new setting of {@code maxTotal} + * for this configuration instance + * + * @see GenericKeyedObjectPool#setMaxTotal(int) + */ + public void setMaxTotal(final int maxTotal) { + this.maxTotal = maxTotal; + } + + /** + * Set the value for the {@code maxTotalPerKey} configuration attribute for + * pools created with this configuration instance. + * + * @param maxTotalPerKey The new setting of {@code maxTotalPerKey} + * for this configuration instance + * + * @see GenericKeyedObjectPool#setMaxTotalPerKey(int) + */ + public void setMaxTotalPerKey(final int maxTotalPerKey) { + this.maxTotalPerKey = maxTotalPerKey; + } + + /** + * Set the value for the {@code minIdlePerKey} configuration attribute for + * pools created with this configuration instance. + * + * @param minIdlePerKey The new setting of {@code minIdlePerKey} + * for this configuration instance + * + * @see GenericKeyedObjectPool#setMinIdlePerKey(int) + */ + public void setMinIdlePerKey(final int minIdlePerKey) { + this.minIdlePerKey = minIdlePerKey; + } + + @Override + protected void toStringAppendFields(final StringBuilder builder) { + super.toStringAppendFields(builder); + builder.append(", minIdlePerKey="); + builder.append(minIdlePerKey); + builder.append(", maxIdlePerKey="); + builder.append(maxIdlePerKey); + builder.append(", maxTotalPerKey="); + builder.append(maxTotalPerKey); + builder.append(", maxTotal="); + builder.append(maxTotal); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java b/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java new file mode 100644 index 0000000..076e888 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java @@ -0,0 +1,323 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.util.List; +import java.util.Map; + +/** + * Defines the methods that will be made available via JMX. + *

    Note

    + *

    + * This interface exists only to define those attributes and methods that will be made available via JMX. It must not be implemented by clients as it is subject + * to change between major, minor and patch version releases of commons pool. Clients that implement this interface may not, therefore, be able to upgrade to a + * new minor or patch release without requiring code changes. + *

    + * + * @param The type of keys maintained by the pool. + * @since 2.0 + */ +public interface GenericKeyedObjectPoolMXBean { + + /** + * See {@link GenericKeyedObjectPool#getBlockWhenExhausted()}. + * + * @return See {@link GenericKeyedObjectPool#getBlockWhenExhausted()}. + */ + boolean getBlockWhenExhausted(); + + /** + * See {@link GenericKeyedObjectPool#getBorrowedCount()}. + * + * @return See {@link GenericKeyedObjectPool#getBorrowedCount()}. + */ + long getBorrowedCount(); + + /** + * See {@link GenericKeyedObjectPool#getCreatedCount()}. + * + * @return See {@link GenericKeyedObjectPool#getCreatedCount()}. + */ + long getCreatedCount(); + + /** + * See {@link GenericKeyedObjectPool#getCreationStackTrace()}. + * + * @return See {@link GenericKeyedObjectPool#getCreationStackTrace()}. + */ + String getCreationStackTrace(); + + /** + * See {@link GenericKeyedObjectPool#getDestroyedByBorrowValidationCount()}. + * + * @return See {@link GenericKeyedObjectPool#getDestroyedByBorrowValidationCount()}. + */ + long getDestroyedByBorrowValidationCount(); + + /** + * See {@link GenericKeyedObjectPool#getDestroyedByEvictorCount()}. + * + * @return See {@link GenericKeyedObjectPool#getDestroyedByEvictorCount()}. + */ + long getDestroyedByEvictorCount(); + + /** + * See {@link GenericKeyedObjectPool#getDestroyedCount()}. + * + * @return See {@link GenericKeyedObjectPool#getDestroyedCount()}. + */ + long getDestroyedCount(); + + /** + * See {@link GenericKeyedObjectPool#getFairness()}. + * + * @return See {@link GenericKeyedObjectPool#getFairness()}. + */ + boolean getFairness(); + + /** + * See {@link GenericKeyedObjectPool#getLifo()}. + * + * @return See {@link GenericKeyedObjectPool#getLifo()}. + */ + boolean getLifo(); + + /** + * See {@link GenericKeyedObjectPool#getLogAbandoned()}. + * + * @return See {@link GenericKeyedObjectPool#getLogAbandoned()}. + * @since 2.10.0 + */ + default boolean getLogAbandoned() { + return false; + } + + /** + * See {@link GenericKeyedObjectPool#getMaxBorrowWaitTimeMillis()}. + * + * @return See {@link GenericKeyedObjectPool#getMaxBorrowWaitTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMaxBorrowWaitTimeMillis(); + + /** + * See {@link GenericKeyedObjectPool#getMaxIdlePerKey()}. + * + * @return See {@link GenericKeyedObjectPool#getMaxIdlePerKey()}. + */ + int getMaxIdlePerKey(); + + /** + * See {@link GenericKeyedObjectPool#getMaxTotal()}. + * + * @return See {@link GenericKeyedObjectPool#getMaxTotal()}. + */ + int getMaxTotal(); + + /** + * See {@link GenericKeyedObjectPool#getMaxTotalPerKey()}. + * + * @return See {@link GenericKeyedObjectPool#getMaxTotalPerKey()}. + */ + int getMaxTotalPerKey(); + + /** + * See {@link GenericKeyedObjectPool#getMaxWaitDuration()}. + * + * @return See {@link GenericKeyedObjectPool#getMaxWaitDuration()}. + */ + long getMaxWaitMillis(); + + /** + * See {@link GenericKeyedObjectPool#getMeanActiveTimeMillis()}. + * + * @return See {@link GenericKeyedObjectPool#getMeanActiveTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMeanActiveTimeMillis(); + + /** + * See {@link GenericKeyedObjectPool#getMaxBorrowWaitTimeMillis()}. + * + * @return See {@link GenericKeyedObjectPool#getMaxBorrowWaitTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMeanBorrowWaitTimeMillis(); + + /** + * See {@link GenericKeyedObjectPool#getMeanIdleTimeMillis()}. + * + * @return See {@link GenericKeyedObjectPool#getMeanIdleTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMeanIdleTimeMillis(); + + /** + * See {@link GenericKeyedObjectPool#getMinEvictableIdleDuration()}. + * + * @return See {@link GenericKeyedObjectPool#getMinEvictableIdleDuration()}. + */ + long getMinEvictableIdleTimeMillis(); + + // Expose getters for monitoring attributes + + /** + * See {@link GenericKeyedObjectPool#getMinIdlePerKey()}. + * + * @return See {@link GenericKeyedObjectPool#getMinIdlePerKey()}. + */ + int getMinIdlePerKey(); + + /** + * See {@link GenericKeyedObjectPool#getNumActive()}. + * + * @return See {@link GenericKeyedObjectPool#getNumActive()}. + */ + int getNumActive(); + + /** + * See {@link GenericKeyedObjectPool#getNumActivePerKey()}. + * + * @return See {@link GenericKeyedObjectPool#getNumActivePerKey()}. + */ + Map getNumActivePerKey(); + + /** + * See {@link GenericKeyedObjectPool#getNumIdle()}. + * + * @return See {@link GenericKeyedObjectPool#getNumIdle()}. + */ + int getNumIdle(); + + /** + * See {@link GenericKeyedObjectPool#getNumTestsPerEvictionRun()}. + * + * @return See {@link GenericKeyedObjectPool#getNumTestsPerEvictionRun()}. + */ + int getNumTestsPerEvictionRun(); + + /** + * See {@link GenericKeyedObjectPool#getNumWaiters()}. + * + * @return See {@link GenericKeyedObjectPool#getNumWaiters()}. + */ + int getNumWaiters(); + + /** + * See {@link GenericKeyedObjectPool#getNumWaitersByKey()}. + * + * @return See {@link GenericKeyedObjectPool#getNumWaitersByKey()}. + */ + Map getNumWaitersByKey(); + + /** + * See {@link GenericKeyedObjectPool#getRemoveAbandonedOnBorrow()}. + * + * @return See {@link GenericKeyedObjectPool#getRemoveAbandonedOnBorrow()}. + * @since 2.10.0 + */ + default boolean getRemoveAbandonedOnBorrow() { + return false; + } + + /** + * See {@link GenericKeyedObjectPool#getRemoveAbandonedOnMaintenance()}. + * + * @return See {@link GenericKeyedObjectPool#getRemoveAbandonedOnMaintenance()}. + * @since 2.10.0 + */ + default boolean getRemoveAbandonedOnMaintenance() { + return false; + } + + /** + * See {@link GenericKeyedObjectPool#getRemoveAbandonedTimeoutDuration()}. + * + * @return See {@link GenericKeyedObjectPool#getRemoveAbandonedTimeoutDuration()}. + * @since 2.10.0 + */ + default int getRemoveAbandonedTimeout() { + return 0; + } + + /** + * See {@link GenericKeyedObjectPool#getReturnedCount()}. + * + * @return See {@link GenericKeyedObjectPool#getReturnedCount()}. + */ + long getReturnedCount(); + + /** + * See {@link GenericKeyedObjectPool#getTestOnBorrow()}. + * + * @return See {@link GenericKeyedObjectPool#getTestOnBorrow()}. + */ + boolean getTestOnBorrow(); + + /** + * See {@link GenericKeyedObjectPool#getTestOnCreate()}. + * + * @return See {@link GenericKeyedObjectPool#getTestOnCreate()}. + * @since 2.2 + */ + boolean getTestOnCreate(); + + /** + * See {@link GenericKeyedObjectPool#getTestOnReturn()}. + * + * @return See {@link GenericKeyedObjectPool#getTestOnReturn()}. + */ + boolean getTestOnReturn(); + + /** + * See {@link GenericKeyedObjectPool#getTestWhileIdle()}. + * + * @return See {@link GenericKeyedObjectPool#getTestWhileIdle()}. + */ + boolean getTestWhileIdle(); + + /** + * See {@link GenericKeyedObjectPool#getDurationBetweenEvictionRuns} + * + * @return See {@link GenericKeyedObjectPool#getDurationBetweenEvictionRuns()}. + */ + long getTimeBetweenEvictionRunsMillis(); + + /** + * See {@link GenericKeyedObjectPool#isAbandonedConfig()}. + * + * @return See {@link GenericKeyedObjectPool#isAbandonedConfig()}. + * @since 2.10.0 + */ + default boolean isAbandonedConfig() { + return false; + } + + /** + * See {@link GenericKeyedObjectPool#isClosed()}. + * + * @return See {@link GenericKeyedObjectPool#isClosed()}. + */ + boolean isClosed(); + + /** + * See {@link GenericKeyedObjectPool#listAllObjects()}. + * + * @return See {@link GenericKeyedObjectPool#listAllObjects()}. + */ + Map> listAllObjects(); +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java b/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java new file mode 100644 index 0000000..6afb61c --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java @@ -0,0 +1,1195 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.apache.tomcat.dbcp.pool2.DestroyMode; +import org.apache.tomcat.dbcp.pool2.ObjectPool; +import org.apache.tomcat.dbcp.pool2.PoolUtils; +import org.apache.tomcat.dbcp.pool2.PooledObject; +import org.apache.tomcat.dbcp.pool2.PooledObjectFactory; +import org.apache.tomcat.dbcp.pool2.PooledObjectState; +import org.apache.tomcat.dbcp.pool2.SwallowedExceptionListener; +import org.apache.tomcat.dbcp.pool2.TrackedUse; +import org.apache.tomcat.dbcp.pool2.UsageTracking; + +/** + * A configurable {@link ObjectPool} implementation. + *

    + * When coupled with the appropriate {@link PooledObjectFactory}, + * {@code GenericObjectPool} provides robust pooling functionality for + * arbitrary objects. + *

    + *

    + * Optionally, one may configure the pool to examine and possibly evict objects + * as they sit idle in the pool and to ensure that a minimum number of idle + * objects are available. This is performed by an "idle object eviction" thread, + * which runs asynchronously. Caution should be used when configuring this + * optional feature. Eviction runs contend with client threads for access to + * objects in the pool, so if they run too frequently performance issues may + * result. + *

    + *

    + * The pool can also be configured to detect and remove "abandoned" objects, + * i.e. objects that have been checked out of the pool but neither used nor + * returned before the configured + * {@link AbandonedConfig#getRemoveAbandonedTimeoutDuration() removeAbandonedTimeout}. + * Abandoned object removal can be configured to happen when + * {@code borrowObject} is invoked and the pool is close to starvation, or + * it can be executed by the idle object evictor, or both. If pooled objects + * implement the {@link TrackedUse} interface, their last use will be queried + * using the {@code getLastUsed} method on that interface; otherwise + * abandonment is determined by how long an object has been checked out from + * the pool. + *

    + *

    + * Implementation note: To prevent possible deadlocks, care has been taken to + * ensure that no call to a factory method will occur within a synchronization + * block. See POOL-125 and DBCP-44 for more information. + *

    + *

    + * This class is intended to be thread-safe. + *

    + * + * @see GenericKeyedObjectPool + * + * @param Type of element pooled in this pool. + * + * @since 2.0 + */ +public class GenericObjectPool extends BaseGenericObjectPool + implements ObjectPool, GenericObjectPoolMXBean, UsageTracking { + + // JMX specific attributes + private static final String ONAME_BASE = + "org.apache.commons.pool2:type=GenericObjectPool,name="; + + private static void wait(final Object obj, final Duration duration) throws InterruptedException { + obj.wait(duration.toMillis(), duration.getNano() % 1_000_000); + } + + private volatile String factoryType; + + private volatile int maxIdle = GenericObjectPoolConfig.DEFAULT_MAX_IDLE; + + private volatile int minIdle = GenericObjectPoolConfig.DEFAULT_MIN_IDLE; + + private final PooledObjectFactory factory; + + /* + * All of the objects currently associated with this pool in any state. It + * excludes objects that have been destroyed. The size of + * {@link #allObjects} will always be less than or equal to {@link + * #_maxActive}. Map keys are pooled objects, values are the PooledObject + * wrappers used internally by the pool. + */ + private final ConcurrentHashMap, PooledObject> allObjects = new ConcurrentHashMap<>(); + + /* + * The combined count of the currently created objects and those in the + * process of being created. Under load, it may exceed {@link #_maxActive} + * if multiple threads try and create a new object at the same time but + * {@link #create()} will ensure that there are never more than + * {@link #_maxActive} objects created at any one time. + */ + private final AtomicLong createCount = new AtomicLong(); + + private long makeObjectCount; + + private final Object makeObjectCountLock = new Object(); + + private final LinkedBlockingDeque> idleObjects; + + /** + * Creates a new {@code GenericObjectPool} using defaults from + * {@link GenericObjectPoolConfig}. + * + * @param factory The object factory to be used to create object instances + * used by this pool + */ + public GenericObjectPool(final PooledObjectFactory factory) { + this(factory, new GenericObjectPoolConfig<>()); + } + + /** + * Creates a new {@code GenericObjectPool} using a specific + * configuration. + * + * @param factory The object factory to be used to create object instances + * used by this pool + * @param config The configuration to use for this pool instance. The + * configuration is used by value. Subsequent changes to + * the configuration object will not be reflected in the + * pool. + */ + public GenericObjectPool(final PooledObjectFactory factory, + final GenericObjectPoolConfig config) { + + super(config, ONAME_BASE, config.getJmxNamePrefix()); + + if (factory == null) { + jmxUnregister(); // tidy up + throw new IllegalArgumentException("Factory may not be null"); + } + this.factory = factory; + + idleObjects = new LinkedBlockingDeque<>(config.getFairness()); + + setConfig(config); + } + + /** + * Creates a new {@code GenericObjectPool} that tracks and destroys + * objects that are checked out, but never returned to the pool. + * + * @param factory The object factory to be used to create object instances + * used by this pool + * @param config The base pool configuration to use for this pool instance. + * The configuration is used by value. Subsequent changes to + * the configuration object will not be reflected in the + * pool. + * @param abandonedConfig Configuration for abandoned object identification + * and removal. The configuration is used by value. + */ + public GenericObjectPool(final PooledObjectFactory factory, + final GenericObjectPoolConfig config, final AbandonedConfig abandonedConfig) { + this(factory, config); + setAbandonedConfig(abandonedConfig); + } + + /** + * Adds the provided wrapped pooled object to the set of idle objects for + * this pool. The object must already be part of the pool. If {@code p} + * is null, this is a no-op (no exception, but no impact on the pool). + * + * @param p The object to make idle + * + * @throws Exception If the factory fails to passivate the object + */ + private void addIdleObject(final PooledObject p) throws Exception { + if (!PooledObject.isNull(p)) { + factory.passivateObject(p); + if (getLifo()) { + idleObjects.addFirst(p); + } else { + idleObjects.addLast(p); + } + } + } + + /** + * Creates an object, and place it into the pool. addObject() is useful for + * "pre-loading" a pool with idle objects. + *

    + * If there is no capacity available to add to the pool, this is a no-op + * (no exception, no impact to the pool). + *

    + *

    + * If the factory returns null when creating an object, a {@code NullPointerException} + * is thrown. If there is no factory set (factory == null), an {@code IllegalStateException} + * is thrown. + *

    + * + */ + @Override + public void addObject() throws Exception { + assertOpen(); + if (factory == null) { + throw new IllegalStateException("Cannot add objects without a factory."); + } + addIdleObject(create()); + } + + /** + * Equivalent to {@link #borrowObject(long) + * borrowObject}({@link #getMaxWaitDuration()}). + * + * {@inheritDoc} + */ + @Override + public T borrowObject() throws Exception { + return borrowObject(getMaxWaitDuration()); + } + + /** + * Borrows an object from the pool using the specific waiting time which only + * applies if {@link #getBlockWhenExhausted()} is true. + *

    + * If there is one or more idle instance available in the pool, then an + * idle instance will be selected based on the value of {@link #getLifo()}, + * activated and returned. If activation fails, or {@link #getTestOnBorrow() + * testOnBorrow} is set to {@code true} and validation fails, the + * instance is destroyed and the next available instance is examined. This + * continues until either a valid instance is returned or there are no more + * idle instances available. + *

    + *

    + * If there are no idle instances available in the pool, behavior depends on + * the {@link #getMaxTotal() maxTotal}, (if applicable) + * {@link #getBlockWhenExhausted()} and the value passed in to the + * {@code borrowMaxWaitMillis} parameter. If the number of instances + * checked out from the pool is less than {@code maxTotal,} a new + * instance is created, activated and (if applicable) validated and returned + * to the caller. If validation fails, a {@code NoSuchElementException} + * is thrown. If the factory returns null when creating an instance, + * a {@code NullPointerException} is thrown. + *

    + *

    + * If the pool is exhausted (no available idle instances and no capacity to + * create new ones), this method will either block (if + * {@link #getBlockWhenExhausted()} is true) or throw a + * {@code NoSuchElementException} (if + * {@link #getBlockWhenExhausted()} is false). The length of time that this + * method will block when {@link #getBlockWhenExhausted()} is true is + * determined by the value passed in to the {@code borrowMaxWaitMillis} + * parameter. + *

    + *

    + * When the pool is exhausted, multiple calling threads may be + * simultaneously blocked waiting for instances to become available. A + * "fairness" algorithm has been implemented to ensure that threads receive + * available instances in request arrival order. + *

    + * + * @param borrowMaxWaitDuration The time to wait for an object + * to become available + * + * @return object instance from the pool + * @throws NoSuchElementException if an instance cannot be returned + * @throws Exception if an object instance cannot be returned due to an error + * @since 2.10.0 + */ + public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception { + assertOpen(); + + final AbandonedConfig ac = this.abandonedConfig; + if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 && + getNumActive() > getMaxTotal() - 3) { + removeAbandoned(ac); + } + + PooledObject p = null; + + // Get local copy of current config so it is consistent for entire + // method execution + final boolean blockWhenExhausted = getBlockWhenExhausted(); + + boolean create; + final Instant waitTime = Instant.now(); + + while (p == null) { + create = false; + p = idleObjects.pollFirst(); + if (p == null) { + p = create(); + if (!PooledObject.isNull(p)) { + create = true; + } + } + if (blockWhenExhausted) { + if (PooledObject.isNull(p)) { + p = borrowMaxWaitDuration.isNegative() ? idleObjects.takeFirst() : idleObjects.pollFirst(borrowMaxWaitDuration); + } + if (PooledObject.isNull(p)) { + throw new NoSuchElementException(appendStats( + "Timeout waiting for idle object, borrowMaxWaitDuration=" + borrowMaxWaitDuration)); + } + } else if (PooledObject.isNull(p)) { + throw new NoSuchElementException(appendStats("Pool exhausted")); + } + if (!p.allocate()) { + p = null; + } + + if (!PooledObject.isNull(p)) { + try { + factory.activateObject(p); + } catch (final Exception e) { + try { + destroy(p, DestroyMode.NORMAL); + } catch (final Exception ignored) { + // ignored - activation failure is more important + } + p = null; + if (create) { + final NoSuchElementException nsee = new NoSuchElementException( + appendStats("Unable to activate object")); + nsee.initCause(e); + throw nsee; + } + } + if (!PooledObject.isNull(p) && getTestOnBorrow()) { + boolean validate = false; + Throwable validationThrowable = null; + try { + validate = factory.validateObject(p); + } catch (final Throwable t) { + PoolUtils.checkRethrow(t); + validationThrowable = t; + } + if (!validate) { + try { + destroy(p, DestroyMode.NORMAL); + destroyedByBorrowValidationCount.incrementAndGet(); + } catch (final Exception ignored) { + // ignored - validation failure is more important + } + p = null; + if (create) { + final NoSuchElementException nsee = new NoSuchElementException( + appendStats("Unable to validate object")); + nsee.initCause(validationThrowable); + throw nsee; + } + } + } + } + } + + updateStatsBorrow(p, Duration.between(waitTime, Instant.now())); + + return p.getObject(); + } + + /** + * Borrows an object from the pool using the specific waiting time which only + * applies if {@link #getBlockWhenExhausted()} is true. + *

    + * If there is one or more idle instance available in the pool, then an + * idle instance will be selected based on the value of {@link #getLifo()}, + * activated and returned. If activation fails, or {@link #getTestOnBorrow() + * testOnBorrow} is set to {@code true} and validation fails, the + * instance is destroyed and the next available instance is examined. This + * continues until either a valid instance is returned or there are no more + * idle instances available. + *

    + *

    + * If there are no idle instances available in the pool, behavior depends on + * the {@link #getMaxTotal() maxTotal}, (if applicable) + * {@link #getBlockWhenExhausted()} and the value passed in to the + * {@code borrowMaxWaitMillis} parameter. If the number of instances + * checked out from the pool is less than {@code maxTotal,} a new + * instance is created, activated and (if applicable) validated and returned + * to the caller. If validation fails, a {@code NoSuchElementException} + * is thrown. If the factory returns null when creating an instance, + * a {@code NullPointerException} is thrown. + *

    + *

    + * If the pool is exhausted (no available idle instances and no capacity to + * create new ones), this method will either block (if + * {@link #getBlockWhenExhausted()} is true) or throw a + * {@code NoSuchElementException} (if + * {@link #getBlockWhenExhausted()} is false). The length of time that this + * method will block when {@link #getBlockWhenExhausted()} is true is + * determined by the value passed in to the {@code borrowMaxWaitMillis} + * parameter. + *

    + *

    + * When the pool is exhausted, multiple calling threads may be + * simultaneously blocked waiting for instances to become available. A + * "fairness" algorithm has been implemented to ensure that threads receive + * available instances in request arrival order. + *

    + * + * @param borrowMaxWaitMillis The time to wait in milliseconds for an object + * to become available + * + * @return object instance from the pool + * + * @throws NoSuchElementException if an instance cannot be returned + * + * @throws Exception if an object instance cannot be returned due to an + * error + */ + public T borrowObject(final long borrowMaxWaitMillis) throws Exception { + return borrowObject(Duration.ofMillis(borrowMaxWaitMillis)); + } + + /** + * Clears any objects sitting idle in the pool by removing them from the + * idle instance pool and then invoking the configured + * {@link PooledObjectFactory#destroyObject(PooledObject)} method on each + * idle instance. + *

    + * Implementation notes: + *

    + *
      + *
    • This method does not destroy or effect in any way instances that are + * checked out of the pool when it is invoked.
    • + *
    • Invoking this method does not prevent objects being returned to the + * idle instance pool, even during its execution. Additional instances may + * be returned while removed items are being destroyed.
    • + *
    • Exceptions encountered destroying idle instances are swallowed + * but notified via a {@link SwallowedExceptionListener}.
    • + *
    + */ + @Override + public void clear() { + PooledObject p = idleObjects.poll(); + + while (p != null) { + try { + destroy(p, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + p = idleObjects.poll(); + } + } + + /** + * Closes the pool. Once the pool is closed, {@link #borrowObject()} will + * fail with IllegalStateException, but {@link #returnObject(Object)} and + * {@link #invalidateObject(Object)} will continue to work, with returned + * objects destroyed on return. + *

    + * Destroys idle instances in the pool by invoking {@link #clear()}. + *

    + */ + @Override + public void close() { + if (isClosed()) { + return; + } + + synchronized (closeLock) { + if (isClosed()) { + return; + } + + // Stop the evictor before the pool is closed since evict() calls + // assertOpen() + stopEvictor(); + + closed = true; + // This clear removes any idle objects + clear(); + + jmxUnregister(); + + // Release any threads that were waiting for an object + idleObjects.interuptTakeWaiters(); + } + } + + /** + * Attempts to create a new wrapped pooled object. + *

    + * If there are {@link #getMaxTotal()} objects already in circulation or in process of being created, this method + * returns null. + *

    + *

    + * If the factory makeObject returns null, this method throws a NullPointerException. + *

    + * + * @return The new wrapped pooled object or null. + * @throws Exception if the object factory's {@code makeObject} fails + */ + private PooledObject create() throws Exception { + int localMaxTotal = getMaxTotal(); + // This simplifies the code later in this method + if (localMaxTotal < 0) { + localMaxTotal = Integer.MAX_VALUE; + } + + final Instant localStartInstant = Instant.now(); + final Duration maxWaitDurationRaw = getMaxWaitDuration(); + final Duration localMaxWaitDuration = maxWaitDurationRaw.isNegative() ? Duration.ZERO : maxWaitDurationRaw; + + // Flag that indicates if create should: + // - TRUE: call the factory to create an object + // - FALSE: return null + // - null: loop and re-test the condition that determines whether to + // call the factory + Boolean create = null; + while (create == null) { + synchronized (makeObjectCountLock) { + final long newCreateCount = createCount.incrementAndGet(); + if (newCreateCount > localMaxTotal) { + // The pool is currently at capacity or in the process of + // making enough new objects to take it to capacity. + createCount.decrementAndGet(); + if (makeObjectCount == 0) { + // There are no makeObject() calls in progress so the + // pool is at capacity. Do not attempt to create a new + // object. Return and wait for an object to be returned + create = Boolean.FALSE; + } else { + // There are makeObject() calls in progress that might + // bring the pool to capacity. Those calls might also + // fail so wait until they complete and then re-test if + // the pool is at capacity or not. + wait(makeObjectCountLock, localMaxWaitDuration); + } + } else { + // The pool is not at capacity. Create a new object. + makeObjectCount++; + create = Boolean.TRUE; + } + } + + // Do not block more if maxWaitTimeMillis is set. + if (create == null && localMaxWaitDuration.compareTo(Duration.ZERO) > 0 && + Duration.between(localStartInstant, Instant.now()).compareTo(localMaxWaitDuration) >= 0) { + create = Boolean.FALSE; + } + } + + if (!create.booleanValue()) { + return null; + } + + final PooledObject p; + try { + p = factory.makeObject(); + if (PooledObject.isNull(p)) { + createCount.decrementAndGet(); + throw new NullPointerException(String.format("%s.makeObject() = null", factory.getClass().getSimpleName())); + } + if (getTestOnCreate() && !factory.validateObject(p)) { + createCount.decrementAndGet(); + return null; + } + } catch (final Throwable e) { + createCount.decrementAndGet(); + throw e; + } finally { + synchronized (makeObjectCountLock) { + makeObjectCount--; + makeObjectCountLock.notifyAll(); + } + } + + final AbandonedConfig ac = this.abandonedConfig; + if (ac != null && ac.getLogAbandoned()) { + p.setLogAbandoned(true); + p.setRequireFullStackTrace(ac.getRequireFullStackTrace()); + } + + createdCount.incrementAndGet(); + allObjects.put(new IdentityWrapper<>(p.getObject()), p); + return p; + } + + /** + * Destroys a wrapped pooled object. + * + * @param toDestroy The wrapped pooled object to destroy + * @param destroyMode DestroyMode context provided to the factory + * + * @throws Exception If the factory fails to destroy the pooled object + * cleanly + */ + private void destroy(final PooledObject toDestroy, final DestroyMode destroyMode) throws Exception { + toDestroy.invalidate(); + idleObjects.remove(toDestroy); + allObjects.remove(new IdentityWrapper<>(toDestroy.getObject())); + try { + factory.destroyObject(toDestroy, destroyMode); + } finally { + destroyedCount.incrementAndGet(); + createCount.decrementAndGet(); + } + } + + /** + * Tries to ensure that {@code idleCount} idle instances exist in the pool. + *

    + * Creates and adds idle instances until either {@link #getNumIdle()} reaches {@code idleCount} + * or the total number of objects (idle, checked out, or being created) reaches + * {@link #getMaxTotal()}. If {@code always} is false, no instances are created unless + * there are threads waiting to check out instances from the pool. + *

    + *

    + * If the factory returns null when creating an instance, a {@code NullPointerException} + * is thrown. + *

    + * + * @param idleCount the number of idle instances desired + * @param always true means create instances even if the pool has no threads waiting + * @throws Exception if the factory's makeObject throws + */ + private void ensureIdle(final int idleCount, final boolean always) throws Exception { + if (idleCount < 1 || isClosed() || !always && !idleObjects.hasTakeWaiters()) { + return; + } + + while (idleObjects.size() < idleCount) { + final PooledObject p = create(); + if (PooledObject.isNull(p)) { + // Can't create objects, no reason to think another call to + // create will work. Give up. + break; + } + if (getLifo()) { + idleObjects.addFirst(p); + } else { + idleObjects.addLast(p); + } + } + if (isClosed()) { + // Pool closed while object was being added to idle objects. + // Make sure the returned object is destroyed rather than left + // in the idle object pool (which would effectively be a leak) + clear(); + } + } + + @Override + void ensureMinIdle() throws Exception { + ensureIdle(getMinIdle(), true); + } + + /** + * {@inheritDoc} + *

    + * Successive activations of this method examine objects in sequence, + * cycling through objects in oldest-to-youngest order. + *

    + */ + @Override + public void evict() throws Exception { + assertOpen(); + + if (!idleObjects.isEmpty()) { + + PooledObject underTest = null; + final EvictionPolicy evictionPolicy = getEvictionPolicy(); + + synchronized (evictionLock) { + final EvictionConfig evictionConfig = new EvictionConfig( + getMinEvictableIdleDuration(), + getSoftMinEvictableIdleDuration(), + getMinIdle()); + + final boolean testWhileIdle = getTestWhileIdle(); + + for (int i = 0, m = getNumTests(); i < m; i++) { + if (evictionIterator == null || !evictionIterator.hasNext()) { + evictionIterator = new EvictionIterator(idleObjects); + } + if (!evictionIterator.hasNext()) { + // Pool exhausted, nothing to do here + return; + } + + try { + underTest = evictionIterator.next(); + } catch (final NoSuchElementException nsee) { + // Object was borrowed in another thread + // Don't count this as an eviction test so reduce i; + i--; + evictionIterator = null; + continue; + } + + if (!underTest.startEvictionTest()) { + // Object was borrowed in another thread + // Don't count this as an eviction test so reduce i; + i--; + continue; + } + + // User provided eviction policy could throw all sorts of + // crazy exceptions. Protect against such an exception + // killing the eviction thread. + boolean evict; + try { + evict = evictionPolicy.evict(evictionConfig, underTest, + idleObjects.size()); + } catch (final Throwable t) { + // Slightly convoluted as SwallowedExceptionListener + // uses Exception rather than Throwable + PoolUtils.checkRethrow(t); + swallowException(new Exception(t)); + // Don't evict on error conditions + evict = false; + } + + if (evict) { + destroy(underTest, DestroyMode.NORMAL); + destroyedByEvictorCount.incrementAndGet(); + } else { + if (testWhileIdle) { + boolean active = false; + try { + factory.activateObject(underTest); + active = true; + } catch (final Exception e) { + destroy(underTest, DestroyMode.NORMAL); + destroyedByEvictorCount.incrementAndGet(); + } + if (active) { + boolean validate = false; + Throwable validationThrowable = null; + try { + validate = factory.validateObject(underTest); + } catch (final Throwable t) { + PoolUtils.checkRethrow(t); + validationThrowable = t; + } + if (!validate) { + destroy(underTest, DestroyMode.NORMAL); + destroyedByEvictorCount.incrementAndGet(); + if (validationThrowable != null) { + if (validationThrowable instanceof RuntimeException) { + throw (RuntimeException) validationThrowable; + } + throw (Error) validationThrowable; + } + } else { + try { + factory.passivateObject(underTest); + } catch (final Exception e) { + destroy(underTest, DestroyMode.NORMAL); + destroyedByEvictorCount.incrementAndGet(); + } + } + } + } + underTest.endEvictionTest(idleObjects); + // TODO - May need to add code here once additional + // states are used + } + } + } + } + final AbandonedConfig ac = this.abandonedConfig; + if (ac != null && ac.getRemoveAbandonedOnMaintenance()) { + removeAbandoned(ac); + } + } + + /** + * Gets a reference to the factory used to create, destroy and validate + * the objects used by this pool. + * + * @return the factory + */ + public PooledObjectFactory getFactory() { + return factory; + } + + /** + * Gets the type - including the specific type rather than the generic - + * of the factory. + * + * @return A string representation of the factory type + */ + @Override + public String getFactoryType() { + // Not thread safe. Accept that there may be multiple evaluations. + if (factoryType == null) { + final StringBuilder result = new StringBuilder(); + result.append(factory.getClass().getName()); + result.append('<'); + final Class pooledObjectType = + PoolImplUtils.getFactoryType(factory.getClass()); + result.append(pooledObjectType.getName()); + result.append('>'); + factoryType = result.toString(); + } + return factoryType; + } + + /** + * Gets the cap on the number of "idle" instances in the pool. If maxIdle + * is set too low on heavily loaded systems it is possible you will see + * objects being destroyed and almost immediately new objects being created. + * This is a result of the active threads momentarily returning objects + * faster than they are requesting them, causing the number of idle + * objects to rise above maxIdle. The best value for maxIdle for heavily + * loaded system will vary but the default is a good starting point. + * + * @return the maximum number of "idle" instances that can be held in the + * pool or a negative value if there is no limit + * + * @see #setMaxIdle + */ + @Override + public int getMaxIdle() { + return maxIdle; + } + + /** + * Gets the target for the minimum number of idle objects to maintain in + * the pool. This setting only has an effect if it is positive and + * {@link #getDurationBetweenEvictionRuns()} is greater than zero. If this + * is the case, an attempt is made to ensure that the pool has the required + * minimum number of instances during idle object eviction runs. + *

    + * If the configured value of minIdle is greater than the configured value + * for maxIdle then the value of maxIdle will be used instead. + *

    + * + * @return The minimum number of objects. + * + * @see #setMinIdle(int) + * @see #setMaxIdle(int) + * @see #setDurationBetweenEvictionRuns(Duration) + */ + @Override + public int getMinIdle() { + final int maxIdleSave = getMaxIdle(); + return Math.min(this.minIdle, maxIdleSave); + } + + @Override + public int getNumActive() { + return allObjects.size() - idleObjects.size(); + } + + @Override + public int getNumIdle() { + return idleObjects.size(); + } + + /** + * Calculates the number of objects to test in a run of the idle object + * evictor. + * + * @return The number of objects to test for validity + */ + private int getNumTests() { + final int numTestsPerEvictionRun = getNumTestsPerEvictionRun(); + if (numTestsPerEvictionRun >= 0) { + return Math.min(numTestsPerEvictionRun, idleObjects.size()); + } + return (int) Math.ceil(idleObjects.size() / + Math.abs((double) numTestsPerEvictionRun)); + } + + /** + * Gets an estimate of the number of threads currently blocked waiting for + * an object from the pool. This is intended for monitoring only, not for + * synchronization control. + * + * @return The estimate of the number of threads currently blocked waiting + * for an object from the pool + */ + @Override + public int getNumWaiters() { + if (getBlockWhenExhausted()) { + return idleObjects.getTakeQueueLength(); + } + return 0; + } + + PooledObject getPooledObject(final T obj) { + return allObjects.get(new IdentityWrapper<>(obj)); + } + + @SuppressWarnings("boxing") // Commons Pool uses auto-boxing + @Override + String getStatsString() { + // Simply listed in AB order. + return super.getStatsString() + + String.format(", createdCount=%,d, makeObjectCount=%,d, maxIdle=%,d, minIdle=%,d", + createdCount.get(), makeObjectCount, maxIdle, minIdle); + } + + /** + * {@inheritDoc} + *

    + * Activation of this method decrements the active count and attempts to destroy the instance, using the default + * (NORMAL) {@link DestroyMode}. + *

    + * + * @throws Exception if an exception occurs destroying the + * @throws IllegalStateException if obj does not belong to this pool + */ + @Override + public void invalidateObject(final T obj) throws Exception { + invalidateObject(obj, DestroyMode.NORMAL); + } + + /** + * {@inheritDoc} + *

    + * Activation of this method decrements the active count and attempts to destroy the instance, using the provided + * {@link DestroyMode}. + *

    + * + * @throws Exception if an exception occurs destroying the object + * @throws IllegalStateException if obj does not belong to this pool + * @since 2.9.0 + */ + @Override + public void invalidateObject(final T obj, final DestroyMode destroyMode) throws Exception { + final PooledObject p = getPooledObject(obj); + if (p == null) { + if (isAbandonedConfig()) { + return; + } + throw new IllegalStateException("Invalidated object not currently part of this pool"); + } + synchronized (p) { + if (p.getState() != PooledObjectState.INVALID) { + destroy(p, destroyMode); + } + } + ensureIdle(1, false); + } + + /** + * Provides information on all the objects in the pool, both idle (waiting + * to be borrowed) and active (currently borrowed). + *

    + * Note: This is named listAllObjects so it is presented as an operation via + * JMX. That means it won't be invoked unless the explicitly requested + * whereas all attributes will be automatically requested when viewing the + * attributes for an object in a tool like JConsole. + *

    + * + * @return Information grouped on all the objects in the pool + */ + @Override + public Set listAllObjects() { + return allObjects.values().stream().map(DefaultPooledObjectInfo::new).collect(Collectors.toSet()); + } + /** + * Tries to ensure that {@link #getMinIdle()} idle instances are available + * in the pool. + * + * @throws Exception If the associated factory throws an exception + * @since 2.4 + */ + public void preparePool() throws Exception { + if (getMinIdle() < 1) { + return; + } + ensureMinIdle(); + } + + /** + * Recovers abandoned objects which have been checked out but + * not used since longer than the removeAbandonedTimeout. + * + * @param abandonedConfig The configuration to use to identify abandoned objects + */ + private void removeAbandoned(final AbandonedConfig abandonedConfig) { + // Generate a list of abandoned objects to remove + final ArrayList> remove = createRemoveList(abandonedConfig, allObjects); + // Now remove the abandoned objects + remove.forEach(pooledObject -> { + if (abandonedConfig.getLogAbandoned()) { + pooledObject.printStackTrace(abandonedConfig.getLogWriter()); + } + try { + invalidateObject(pooledObject.getObject(), DestroyMode.ABANDONED); + } catch (final Exception e) { + swallowException(e); + } + }); + } + + /** + * {@inheritDoc} + *

    + * If {@link #getMaxIdle() maxIdle} is set to a positive value and the + * number of idle instances has reached this value, the returning instance + * is destroyed. + *

    + *

    + * If {@link #getTestOnReturn() testOnReturn} == true, the returning + * instance is validated before being returned to the idle instance pool. In + * this case, if validation fails, the instance is destroyed. + *

    + *

    + * Exceptions encountered destroying objects for any reason are swallowed + * but notified via a {@link SwallowedExceptionListener}. + *

    + */ + @Override + public void returnObject(final T obj) { + final PooledObject p = getPooledObject(obj); + + if (p == null) { + if (!isAbandonedConfig()) { + throw new IllegalStateException( + "Returned object not currently part of this pool"); + } + return; // Object was abandoned and removed + } + + markReturningState(p); + + final Duration activeTime = p.getActiveDuration(); + + if (getTestOnReturn() && !factory.validateObject(p)) { + try { + destroy(p, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + try { + ensureIdle(1, false); + } catch (final Exception e) { + swallowException(e); + } + updateStatsReturn(activeTime); + return; + } + + try { + factory.passivateObject(p); + } catch (final Exception e1) { + swallowException(e1); + try { + destroy(p, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + try { + ensureIdle(1, false); + } catch (final Exception e) { + swallowException(e); + } + updateStatsReturn(activeTime); + return; + } + + if (!p.deallocate()) { + throw new IllegalStateException( + "Object has already been returned to this pool or is invalid"); + } + + final int maxIdleSave = getMaxIdle(); + if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) { + try { + destroy(p, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + try { + ensureIdle(1, false); + } catch (final Exception e) { + swallowException(e); + } + } else { + if (getLifo()) { + idleObjects.addFirst(p); + } else { + idleObjects.addLast(p); + } + if (isClosed()) { + // Pool closed while object was being added to idle objects. + // Make sure the returned object is destroyed rather than left + // in the idle object pool (which would effectively be a leak) + clear(); + } + } + updateStatsReturn(activeTime); + } + + /** + * Sets the base pool configuration. + * + * @param conf the new configuration to use. This is used by value. + * + * @see GenericObjectPoolConfig + */ + public void setConfig(final GenericObjectPoolConfig conf) { + super.setConfig(conf); + setMaxIdle(conf.getMaxIdle()); + setMinIdle(conf.getMinIdle()); + setMaxTotal(conf.getMaxTotal()); + } + + /** + * Sets the cap on the number of "idle" instances in the pool. If maxIdle + * is set too low on heavily loaded systems it is possible you will see + * objects being destroyed and almost immediately new objects being created. + * This is a result of the active threads momentarily returning objects + * faster than they are requesting them, causing the number of idle + * objects to rise above maxIdle. The best value for maxIdle for heavily + * loaded system will vary but the default is a good starting point. + * + * @param maxIdle + * The cap on the number of "idle" instances in the pool. Use a + * negative value to indicate an unlimited number of idle + * instances + * + * @see #getMaxIdle + */ + public void setMaxIdle(final int maxIdle) { + this.maxIdle = maxIdle; + } + + /** + * Sets the target for the minimum number of idle objects to maintain in + * the pool. This setting only has an effect if it is positive and + * {@link #getDurationBetweenEvictionRuns()} is greater than zero. If this + * is the case, an attempt is made to ensure that the pool has the required + * minimum number of instances during idle object eviction runs. + *

    + * If the configured value of minIdle is greater than the configured value + * for maxIdle then the value of maxIdle will be used instead. + *

    + * + * @param minIdle + * The minimum number of objects. + * + * @see #getMinIdle() + * @see #getMaxIdle() + * @see #getDurationBetweenEvictionRuns() + */ + public void setMinIdle(final int minIdle) { + this.minIdle = minIdle; + } + + @Override + protected void toStringAppendFields(final StringBuilder builder) { + super.toStringAppendFields(builder); + builder.append(", factoryType="); + builder.append(factoryType); + builder.append(", maxIdle="); + builder.append(maxIdle); + builder.append(", minIdle="); + builder.append(minIdle); + builder.append(", factory="); + builder.append(factory); + builder.append(", allObjects="); + builder.append(allObjects); + builder.append(", createCount="); + builder.append(createCount); + builder.append(", idleObjects="); + builder.append(idleObjects); + builder.append(", abandonedConfig="); + builder.append(abandonedConfig); + } + + @Override + public void use(final T pooledObject) { + final AbandonedConfig abandonedCfg = this.abandonedConfig; + if (abandonedCfg != null && abandonedCfg.getUseUsageTracking()) { + final PooledObject po = getPooledObject(pooledObject); + if (po != null) { + po.use(); + } + } + } + +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java b/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java new file mode 100644 index 0000000..f01c3e7 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java @@ -0,0 +1,158 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +/** + * A simple structure encapsulating the configuration for a + * {@link GenericObjectPool}. + * + *

    + * This class is not thread-safe; it is only intended to be used to provide + * attributes used when creating a pool. + *

    + * + * @param Type of element pooled. + * @since 2.0 + */ +public class GenericObjectPoolConfig extends BaseObjectPoolConfig { + + /** + * The default value for the {@code maxTotal} configuration attribute. + * @see GenericObjectPool#getMaxTotal() + */ + public static final int DEFAULT_MAX_TOTAL = 8; + + /** + * The default value for the {@code maxIdle} configuration attribute. + * @see GenericObjectPool#getMaxIdle() + */ + public static final int DEFAULT_MAX_IDLE = 8; + + /** + * The default value for the {@code minIdle} configuration attribute. + * @see GenericObjectPool#getMinIdle() + */ + public static final int DEFAULT_MIN_IDLE = 0; + + + private int maxTotal = DEFAULT_MAX_TOTAL; + + private int maxIdle = DEFAULT_MAX_IDLE; + + private int minIdle = DEFAULT_MIN_IDLE; + + @SuppressWarnings("unchecked") + @Override + public GenericObjectPoolConfig clone() { + try { + return (GenericObjectPoolConfig) super.clone(); + } catch (final CloneNotSupportedException e) { + throw new AssertionError(); // Can't happen + } + } + + /** + * Get the value for the {@code maxIdle} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code maxIdle} for this + * configuration instance + * + * @see GenericObjectPool#getMaxIdle() + */ + public int getMaxIdle() { + return maxIdle; + } + + + /** + * Get the value for the {@code maxTotal} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code maxTotal} for this + * configuration instance + * + * @see GenericObjectPool#getMaxTotal() + */ + public int getMaxTotal() { + return maxTotal; + } + + /** + * Get the value for the {@code minIdle} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code minIdle} for this + * configuration instance + * + * @see GenericObjectPool#getMinIdle() + */ + public int getMinIdle() { + return minIdle; + } + + + /** + * Set the value for the {@code maxIdle} configuration attribute for + * pools created with this configuration instance. + * + * @param maxIdle The new setting of {@code maxIdle} + * for this configuration instance + * + * @see GenericObjectPool#setMaxIdle(int) + */ + public void setMaxIdle(final int maxIdle) { + this.maxIdle = maxIdle; + } + + /** + * Set the value for the {@code maxTotal} configuration attribute for + * pools created with this configuration instance. + * + * @param maxTotal The new setting of {@code maxTotal} + * for this configuration instance + * + * @see GenericObjectPool#setMaxTotal(int) + */ + public void setMaxTotal(final int maxTotal) { + this.maxTotal = maxTotal; + } + + /** + * Set the value for the {@code minIdle} configuration attribute for + * pools created with this configuration instance. + * + * @param minIdle The new setting of {@code minIdle} + * for this configuration instance + * + * @see GenericObjectPool#setMinIdle(int) + */ + public void setMinIdle(final int minIdle) { + this.minIdle = minIdle; + } + + @Override + protected void toStringAppendFields(final StringBuilder builder) { + super.toStringAppendFields(builder); + builder.append(", maxTotal="); + builder.append(maxTotal); + builder.append(", maxIdle="); + builder.append(maxIdle); + builder.append(", minIdle="); + builder.append(minIdle); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java b/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java new file mode 100644 index 0000000..f17aa3f --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java @@ -0,0 +1,294 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.util.Set; + +/** + * Defines the methods that will be made available via JMX. + *

    Note

    + *

    + * This interface exists only to define those attributes and methods that will be made available via JMX. It must not be implemented by clients as it is subject + * to change between major, minor and patch version releases of commons pool. Clients that implement this interface may not, therefore, be able to upgrade to a + * new minor or patch release without requiring code changes. + *

    + * + * @since 2.0 + */ +public interface GenericObjectPoolMXBean { + + /** + * See {@link GenericObjectPool#getBlockWhenExhausted()}. + * + * @return See {@link GenericObjectPool#getBlockWhenExhausted()}. + */ + boolean getBlockWhenExhausted(); + + /** + * See {@link GenericObjectPool#getBorrowedCount()}. + * + * @return See {@link GenericObjectPool#getBorrowedCount()}. + */ + long getBorrowedCount(); + + /** + * See {@link GenericObjectPool#getCreatedCount()}. + * + * @return See {@link GenericObjectPool#getCreatedCount()}. + */ + long getCreatedCount(); + + /** + * See {@link GenericObjectPool#getCreationStackTrace()}. + * + * @return See {@link GenericObjectPool#getCreationStackTrace()}. + */ + String getCreationStackTrace(); + + /** + * See {@link GenericObjectPool#getDestroyedByBorrowValidationCount()}. + * + * @return See {@link GenericObjectPool#getDestroyedByBorrowValidationCount()}. + */ + long getDestroyedByBorrowValidationCount(); + + /** + * See {@link GenericObjectPool#getDestroyedByEvictorCount()}. + * + * @return See {@link GenericObjectPool#getDestroyedByEvictorCount()}. + */ + long getDestroyedByEvictorCount(); + + /** + * See {@link GenericObjectPool#getDestroyedCount()}. + * + * @return See {@link GenericObjectPool#getDestroyedCount()}. + */ + long getDestroyedCount(); + + /** + * See {@link GenericObjectPool#getFactoryType()}. + * + * @return See {@link GenericObjectPool#getFactoryType()}. + */ + String getFactoryType(); + + /** + * See {@link GenericObjectPool#getLifo()}. + * + * @return See {@link GenericObjectPool#getLifo()}. + */ + boolean getFairness(); + + /** + * See {@link GenericObjectPool#getFairness()}. + * + * @return See {@link GenericObjectPool#getFairness()}. + */ + boolean getLifo(); + + /** + * See {@link GenericObjectPool#getLogAbandoned()}. + * + * @return See {@link GenericObjectPool#getLogAbandoned()}. + */ + boolean getLogAbandoned(); + + /** + * See {@link GenericObjectPool#getMaxBorrowWaitTimeMillis()}. + * + * @return See {@link GenericObjectPool#getMaxBorrowWaitTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMaxBorrowWaitTimeMillis(); + + /** + * See {@link GenericObjectPool#getMaxIdle()}. + * + * @return See {@link GenericObjectPool#getMaxIdle()}. + */ + int getMaxIdle(); + + /** + * See {@link GenericObjectPool#getMaxTotal()}. + * + * @return See {@link GenericObjectPool#getMaxTotal()}. + */ + int getMaxTotal(); + + /** + * See {@link GenericObjectPool#getMaxWaitDuration()}. + * + * @return See {@link GenericObjectPool#getMaxWaitDuration()}. + */ + long getMaxWaitMillis(); + + /** + * See {@link GenericObjectPool#getMeanActiveTimeMillis()}. + * + * @return See {@link GenericObjectPool#getMeanActiveTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMeanActiveTimeMillis(); + + /** + * See {@link GenericObjectPool#getMeanBorrowWaitTimeMillis()}. + * + * @return See {@link GenericObjectPool#getMeanBorrowWaitTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMeanBorrowWaitTimeMillis(); + + // Getters for monitoring attributes + + /** + * See {@link GenericObjectPool#getMeanIdleTimeMillis()}. + * + * @return See {@link GenericObjectPool#getMeanIdleTimeMillis()}. + */ + @SuppressWarnings("javadoc") + long getMeanIdleTimeMillis(); + + /** + * See {@link GenericObjectPool#getMinEvictableIdleDuration()}. + * + * @return See {@link GenericObjectPool#getMinEvictableIdleDuration()}. + */ + long getMinEvictableIdleTimeMillis(); + + /** + * See {@link GenericObjectPool#getMinIdle()}. + * + * @return See {@link GenericObjectPool#getMinIdle()}. + */ + int getMinIdle(); + + /** + * See {@link GenericObjectPool#getNumActive()}. + * + * @return See {@link GenericObjectPool#getNumActive()}. + */ + int getNumActive(); + + /** + * See {@link GenericObjectPool#getNumIdle()}. + * + * @return See {@link GenericObjectPool#getNumIdle()}. + */ + int getNumIdle(); + + /** + * See {@link GenericObjectPool#getNumTestsPerEvictionRun()}. + * + * @return See {@link GenericObjectPool#getNumTestsPerEvictionRun()}. + */ + int getNumTestsPerEvictionRun(); + + /** + * See {@link GenericObjectPool#getNumWaiters()}. + * + * @return See {@link GenericObjectPool#getNumWaiters()}. + */ + int getNumWaiters(); + + /** + * See {@link GenericObjectPool#getRemoveAbandonedOnBorrow()}. + * + * @return See {@link GenericObjectPool#getRemoveAbandonedOnBorrow()}. + */ + boolean getRemoveAbandonedOnBorrow(); + + /** + * See {@link GenericObjectPool#getRemoveAbandonedOnMaintenance()}. + * + * @return See {@link GenericObjectPool#getRemoveAbandonedOnMaintenance()}. + */ + boolean getRemoveAbandonedOnMaintenance(); + + /** + * See {@link GenericObjectPool#getRemoveAbandonedTimeoutDuration()}. + * + * @return See {@link GenericObjectPool#getRemoveAbandonedTimeoutDuration()}. + */ + int getRemoveAbandonedTimeout(); + + /** + * See {@link GenericObjectPool#getReturnedCount()}. + * + * @return See {@link GenericObjectPool#getReturnedCount()}. + */ + long getReturnedCount(); + + /** + * See {@link GenericObjectPool#getTestOnBorrow()}. + * + * @return See {@link GenericObjectPool#getTestOnBorrow()}. + */ + boolean getTestOnBorrow(); + + // Getters for abandoned object removal configuration + + /** + * See {@link GenericObjectPool#getTestOnCreate()}. + * + * @return See {@link GenericObjectPool#getTestOnCreate()}. + * @since 2.2 + */ + boolean getTestOnCreate(); + + /** + * See {@link GenericObjectPool#getTestOnReturn()}. + * + * @return See {@link GenericObjectPool#getTestOnReturn()}. + */ + boolean getTestOnReturn(); + + /** + * See {@link GenericObjectPool#getTestWhileIdle()}. + * + * @return See {@link GenericObjectPool#getTestWhileIdle()}. + */ + boolean getTestWhileIdle(); + + /** + * See {@link GenericObjectPool#getDurationBetweenEvictionRuns()}. + * + * @return See {@link GenericObjectPool#getDurationBetweenEvictionRuns()}. + */ + long getTimeBetweenEvictionRunsMillis(); + + /** + * See {@link GenericObjectPool#isAbandonedConfig()}. + * + * @return See {@link GenericObjectPool#isAbandonedConfig()}. + */ + boolean isAbandonedConfig(); + + /** + * See {@link GenericObjectPool#isClosed()}. + * + * @return See {@link GenericObjectPool#isClosed()}. + */ + boolean isClosed(); + + /** + * See {@link GenericObjectPool#listAllObjects()}. + * + * @return See {@link GenericObjectPool#listAllObjects()}. + */ + Set listAllObjects(); +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java b/java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java new file mode 100644 index 0000000..ec06e01 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * This sub-class was created to expose the waiting threads so that they can be + * interrupted when the pool using the queue that uses this lock is closed. The + * class is intended for internal use only. + *

    + * This class is intended to be thread-safe. + *

    + * + * @since 2.0 + */ +class InterruptibleReentrantLock extends ReentrantLock { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new InterruptibleReentrantLock with the given fairness policy. + * + * @param fairness true means threads should acquire contended locks as if + * waiting in a FIFO queue + */ + InterruptibleReentrantLock(final boolean fairness) { + super(fairness); + } + + /** + * Interrupts the threads that are waiting on a specific condition + * + * @param condition the condition on which the threads are waiting. + */ + public void interruptWaiters(final Condition condition) { + getWaitingThreads(condition).forEach(Thread::interrupt); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java b/java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java new file mode 100644 index 0000000..3b95ee9 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java @@ -0,0 +1,1477 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.time.Duration; +import java.util.AbstractQueue; +import java.util.Collection; +import java.util.Deque; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; + +/** + * An optionally-bounded {@linkplain java.util.concurrent.BlockingDeque blocking + * deque} based on linked nodes. + * + *

    The optional capacity bound constructor argument serves as a + * way to prevent excessive expansion. The capacity, if unspecified, + * is equal to {@link Integer#MAX_VALUE}. Linked nodes are + * dynamically created upon each insertion unless this would bring the + * deque above capacity. + *

    + * + *

    Most operations run in constant time (ignoring time spent + * blocking). Exceptions include {@link #remove(Object) remove}, + * {@link #removeFirstOccurrence removeFirstOccurrence}, {@link + * #removeLastOccurrence removeLastOccurrence}, {@link #contains + * contains}, {@link #iterator iterator.remove()}, and the bulk + * operations, all of which run in linear time. + *

    + * + *

    This class and its iterator implement all of the + * optional methods of the {@link Collection} and {@link + * Iterator} interfaces. + *

    + * + *

    This class is a member of the + * + * Java Collections Framework. + *

    + * + * @param the type of elements held in this collection + * + * Note: This was copied from Apache Harmony and modified to suit the needs of + * Commons Pool. + * + * @since 2.0 + */ +class LinkedBlockingDeque extends AbstractQueue + implements Deque, Serializable { + + /* + * Implemented as a simple doubly-linked list protected by a + * single lock and using conditions to manage blocking. + * + * To implement weakly consistent iterators, it appears we need to + * keep all Nodes GC-reachable from a predecessor dequeued Node. + * That would cause two problems: + * - allow a rogue Iterator to cause unbounded memory retention + * - cause cross-generational linking of old Nodes to new Nodes if + * a Node was tenured while live, which generational GCs have a + * hard time dealing with, causing repeated major collections. + * However, only non-deleted Nodes need to be reachable from + * dequeued Nodes, and reachability does not necessarily have to + * be of the kind understood by the GC. We use the trick of + * linking a Node that has just been dequeued to itself. Such a + * self-link implicitly means to jump to "first" (for next links) + * or "last" (for prev links). + */ + + /* + * We have "diamond" multiple interface/abstract class inheritance + * here, and that introduces ambiguities. Often we want the + * BlockingDeque javadoc combined with the AbstractQueue + * implementation, so a lot of method specs are duplicated here. + */ + + /** + * Base class for Iterators for LinkedBlockingDeque + */ + private abstract class AbstractItr implements Iterator { + /** + * The next node to return in next() + */ + Node next; + + /** + * nextItem holds on to item fields because once we claim that + * an element exists in hasNext(), we must return item read + * under lock (in advance()) even if it was in the process of + * being removed when hasNext() was called. + */ + E nextItem; + + /** + * Node returned by most recent call to next. Needed by remove. + * Reset to null if this element is deleted by a call to remove. + */ + private Node lastRet; + + /** + * Constructs a new iterator. Sets the initial position. + */ + AbstractItr() { + // set to initial position + lock.lock(); + try { + next = firstNode(); + nextItem = next == null ? null : next.item; + } finally { + lock.unlock(); + } + } + + /** + * Advances next. + */ + void advance() { + lock.lock(); + try { + // assert next != null; + next = succ(next); + nextItem = next == null ? null : next.item; + } finally { + lock.unlock(); + } + } + + /** + * Obtain the first node to be returned by the iterator. + * + * @return first node + */ + abstract Node firstNode(); + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public E next() { + if (next == null) { + throw new NoSuchElementException(); + } + lastRet = next; + final E x = nextItem; + advance(); + return x; + } + + /** + * For a given node, obtain the next node to be returned by the + * iterator. + * + * @param n given node + * + * @return next node + */ + abstract Node nextNode(Node n); + + @Override + public void remove() { + final Node n = lastRet; + if (n == null) { + throw new IllegalStateException(); + } + lastRet = null; + lock.lock(); + try { + if (n.item != null) { + unlink(n); + } + } finally { + lock.unlock(); + } + } + + /** + * Returns the successor node of the given non-null, but + * possibly previously deleted, node. + * + * @param n node whose successor is sought + * @return successor node + */ + private Node succ(Node n) { + // Chains of deleted nodes ending in null or self-links + // are possible if multiple interior nodes are removed. + for (;;) { + final Node s = nextNode(n); + if (s == null) { + return null; + } + if (s.item != null) { + return s; + } + if (s == n) { + return firstNode(); + } + n = s; + } + } + } + + /** Descending iterator */ + private class DescendingItr extends AbstractItr { + @Override + Node firstNode() { return last; } + @Override + Node nextNode(final Node n) { return n.prev; } + } + + /** Forward iterator */ + private class Itr extends AbstractItr { + @Override + Node firstNode() { return first; } + @Override + Node nextNode(final Node n) { return n.next; } + } + + /** + * Doubly-linked list node class. + * + * @param node item type + */ + private static final class Node { + /** + * The item, or null if this node has been removed. + */ + E item; + + /** + * One of: + * - the real predecessor Node + * - this Node, meaning the predecessor is tail + * - null, meaning there is no predecessor + */ + Node prev; + + /** + * One of: + * - the real successor Node + * - this Node, meaning the successor is head + * - null, meaning there is no successor + */ + Node next; + + /** + * Constructs a new list node. + * + * @param x The list item + * @param p Previous item + * @param n Next item + */ + Node(final E x, final Node p, final Node n) { + item = x; + prev = p; + next = n; + } + } + + private static final long serialVersionUID = -387911632671998426L; + + /** + * Pointer to first node. + * Invariant: (first == null && last == null) || + * (first.prev == null && first.item != null) + */ + private transient Node first; // @GuardedBy("lock") + + /** + * Pointer to last node. + * Invariant: (first == null && last == null) || + * (last.next == null && last.item != null) + */ + private transient Node last; // @GuardedBy("lock") + + /** Number of items in the deque */ + private transient int count; // @GuardedBy("lock") + + /** Maximum number of items in the deque */ + private final int capacity; + + /** Main lock guarding all access */ + private final InterruptibleReentrantLock lock; + + /** Condition for waiting takes */ + private final Condition notEmpty; + + /** Condition for waiting puts */ + private final Condition notFull; + + /** + * Creates a {@code LinkedBlockingDeque} with a capacity of + * {@link Integer#MAX_VALUE}. + */ + LinkedBlockingDeque() { + this(Integer.MAX_VALUE); + } + + /** + * Creates a {@code LinkedBlockingDeque} with a capacity of + * {@link Integer#MAX_VALUE} and the given fairness policy. + * @param fairness true means threads waiting on the deque should be served + * as if waiting in a FIFO request queue + */ + LinkedBlockingDeque(final boolean fairness) { + this(Integer.MAX_VALUE, fairness); + } + + + // Basic linking and unlinking operations, called only while holding lock + + /** + * Creates a {@code LinkedBlockingDeque} with a capacity of + * {@link Integer#MAX_VALUE}, initially containing the elements of + * the given collection, added in traversal order of the + * collection's iterator. + * + * @param c the collection of elements to initially contain + * @throws NullPointerException if the specified collection or any + * of its elements are null + */ + LinkedBlockingDeque(final Collection c) { + this(Integer.MAX_VALUE); + lock.lock(); // Never contended, but necessary for visibility + try { + for (final E e : c) { + Objects.requireNonNull(e); + if (!linkLast(e)) { + throw new IllegalStateException("Deque full"); + } + } + } finally { + lock.unlock(); + } + } + + /** + * Creates a {@code LinkedBlockingDeque} with the given (fixed) capacity. + * + * @param capacity the capacity of this deque + * @throws IllegalArgumentException if {@code capacity} is less than 1 + */ + LinkedBlockingDeque(final int capacity) { + this(capacity, false); + } + + /** + * Creates a {@code LinkedBlockingDeque} with the given (fixed) capacity + * and fairness policy. + * + * @param capacity the capacity of this deque + * @param fairness true means threads waiting on the deque should be served + * as if waiting in a FIFO request queue + * @throws IllegalArgumentException if {@code capacity} is less than 1 + */ + LinkedBlockingDeque(final int capacity, final boolean fairness) { + if (capacity <= 0) { + throw new IllegalArgumentException(); + } + this.capacity = capacity; + lock = new InterruptibleReentrantLock(fairness); + notEmpty = lock.newCondition(); + notFull = lock.newCondition(); + } + + @Override + public boolean add(final E e) { + addLast(e); + return true; + } + + @Override + public void addFirst(final E e) { + if (!offerFirst(e)) { + throw new IllegalStateException("Deque full"); + } + } + + // BlockingDeque methods + + @Override + public void addLast(final E e) { + if (!offerLast(e)) { + throw new IllegalStateException("Deque full"); + } + } + + /** + * Atomically removes all of the elements from this deque. + * The deque will be empty after this call returns. + */ + @Override + public void clear() { + lock.lock(); + try { + for (Node f = first; f != null;) { + f.item = null; + final Node n = f.next; + f.prev = null; + f.next = null; + f = n; + } + first = last = null; + count = 0; + notFull.signalAll(); + } finally { + lock.unlock(); + } + } + + /** + * Returns {@code true} if this deque contains the specified element. + * More formally, returns {@code true} if and only if this deque contains + * at least one element {@code e} such that {@code o.equals(e)}. + * + * @param o object to be checked for containment in this deque + * @return {@code true} if this deque contains the specified element + */ + @Override + public boolean contains(final Object o) { + if (o == null) { + return false; + } + lock.lock(); + try { + for (Node p = first; p != null; p = p.next) { + if (o.equals(p.item)) { + return true; + } + } + return false; + } finally { + lock.unlock(); + } + } + + @Override + public Iterator descendingIterator() { + return new DescendingItr(); + } + + /** + * Drains the queue to the specified collection. + * + * @param c The collection to add the elements to + * + * @return number of elements added to the collection + * + * @throws UnsupportedOperationException if the add operation is not + * supported by the specified collection + * @throws ClassCastException if the class of the elements held by this + * collection prevents them from being added to the specified + * collection + * @throws NullPointerException if c is null + * @throws IllegalArgumentException if c is this instance + */ + public int drainTo(final Collection c) { + return drainTo(c, Integer.MAX_VALUE); + } + + /** + * Drains no more than the specified number of elements from the queue to the + * specified collection. + * + * @param c collection to add the elements to + * @param maxElements maximum number of elements to remove from the queue + * + * @return number of elements added to the collection + * @throws UnsupportedOperationException if the add operation is not + * supported by the specified collection + * @throws ClassCastException if the class of the elements held by this + * collection prevents them from being added to the specified + * collection + * @throws NullPointerException if c is null + * @throws IllegalArgumentException if c is this instance + */ + public int drainTo(final Collection c, final int maxElements) { + Objects.requireNonNull(c, "c"); + if (c == this) { + throw new IllegalArgumentException(); + } + lock.lock(); + try { + final int n = Math.min(maxElements, count); + for (int i = 0; i < n; i++) { + c.add(first.item); // In this order, in case add() throws. + unlinkFirst(); + } + return n; + } finally { + lock.unlock(); + } + } + + /** + * Retrieves, but does not remove, the head of the queue represented by + * this deque. This method differs from {@link #peek peek} only in that + * it throws an exception if this deque is empty. + * + *

    This method is equivalent to {@link #getFirst() getFirst}. + * + * @return the head of the queue represented by this deque + * @throws NoSuchElementException if this deque is empty + */ + @Override + public E element() { + return getFirst(); + } + + @Override + public E getFirst() { + final E x = peekFirst(); + if (x == null) { + throw new NoSuchElementException(); + } + return x; + } + + @Override + public E getLast() { + final E x = peekLast(); + if (x == null) { + throw new NoSuchElementException(); + } + return x; + } + + /** + * Gets the length of the queue of threads waiting to take instances from this deque. See disclaimer on accuracy + * in {@link java.util.concurrent.locks.ReentrantLock#getWaitQueueLength(Condition)}. + * + * @return number of threads waiting on this deque's notEmpty condition. + */ + public int getTakeQueueLength() { + lock.lock(); + try { + return lock.getWaitQueueLength(notEmpty); + } finally { + lock.unlock(); + } + } + + /** + * Returns true if there are threads waiting to take instances from this deque. See disclaimer on accuracy in + * {@link java.util.concurrent.locks.ReentrantLock#hasWaiters(Condition)}. + * + * @return true if there is at least one thread waiting on this deque's notEmpty condition. + */ + public boolean hasTakeWaiters() { + lock.lock(); + try { + return lock.hasWaiters(notEmpty); + } finally { + lock.unlock(); + } + } + + /** + * Interrupts the threads currently waiting to take an object from the pool. See disclaimer on accuracy in + * {@link java.util.concurrent.locks.ReentrantLock#getWaitingThreads(Condition)}. + */ + public void interuptTakeWaiters() { + lock.lock(); + try { + lock.interruptWaiters(notEmpty); + } finally { + lock.unlock(); + } + } + + /** + * Returns an iterator over the elements in this deque in proper sequence. + * The elements will be returned in order from first (head) to last (tail). + * The returned {@code Iterator} is a "weakly consistent" iterator that + * will never throw {@link java.util.ConcurrentModificationException + * ConcurrentModificationException}, + * and guarantees to traverse elements as they existed upon + * construction of the iterator, and may (but is not guaranteed to) + * reflect any modifications subsequent to construction. + * + * @return an iterator over the elements in this deque in proper sequence + */ + @Override + public Iterator iterator() { + return new Itr(); + } + + /** + * Links provided element as first element, or returns false if full. + * + * @param e The element to link as the first element. + * + * @return {@code true} if successful, otherwise {@code false} + */ + private boolean linkFirst(final E e) { + // assert lock.isHeldByCurrentThread(); + if (count >= capacity) { + return false; + } + final Node f = first; + final Node x = new Node<>(e, null, f); + first = x; + if (last == null) { + last = x; + } else { + f.prev = x; + } + ++count; + notEmpty.signal(); + return true; + } + + /** + * Links provided element as last element, or returns false if full. + * + * @param e The element to link as the last element. + * + * @return {@code true} if successful, otherwise {@code false} + */ + private boolean linkLast(final E e) { + // assert lock.isHeldByCurrentThread(); + if (count >= capacity) { + return false; + } + final Node l = last; + final Node x = new Node<>(e, l, null); + last = x; + if (first == null) { + first = x; + } else { + l.next = x; + } + ++count; + notEmpty.signal(); + return true; + } + + @Override + public boolean offer(final E e) { + return offerLast(e); + } + + /** + * Links the provided element as the last in the queue, waiting up to the + * specified time to do so if the queue is full. + *

    + * This method is equivalent to {@link #offerLast(Object, long, TimeUnit)} + * + * @param e element to link + * @param timeout length of time to wait + * + * @return {@code true} if successful, otherwise {@code false} + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whilst waiting + * for space + */ + boolean offer(final E e, final Duration timeout) throws InterruptedException { + return offerLast(e, timeout); + } + + /** + * Links the provided element as the last in the queue, waiting up to the + * specified time to do so if the queue is full. + *

    + * This method is equivalent to {@link #offerLast(Object, long, TimeUnit)} + * + * @param e element to link + * @param timeout length of time to wait + * @param unit units that timeout is expressed in + * + * @return {@code true} if successful, otherwise {@code false} + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whilst waiting + * for space + */ + public boolean offer(final E e, final long timeout, final TimeUnit unit) throws InterruptedException { + return offerLast(e, timeout, unit); + } + + @Override + public boolean offerFirst(final E e) { + Objects.requireNonNull(e, "e"); + lock.lock(); + try { + return linkFirst(e); + } finally { + lock.unlock(); + } + } + + /** + * Links the provided element as the first in the queue, waiting up to the + * specified time to do so if the queue is full. + * + * @param e element to link + * @param timeout length of time to wait + * + * @return {@code true} if successful, otherwise {@code false} + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whilst waiting + * for space + */ + public boolean offerFirst(final E e, final Duration timeout) throws InterruptedException { + Objects.requireNonNull(e, "e"); + long nanos = timeout.toNanos(); + lock.lockInterruptibly(); + try { + while (!linkFirst(e)) { + if (nanos <= 0) { + return false; + } + nanos = notFull.awaitNanos(nanos); + } + return true; + } finally { + lock.unlock(); + } + } + + /** + * Links the provided element as the first in the queue, waiting up to the + * specified time to do so if the queue is full. + * + * @param e element to link + * @param timeout length of time to wait + * @param unit units that timeout is expressed in + * + * @return {@code true} if successful, otherwise {@code false} + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whilst waiting + * for space + */ + public boolean offerFirst(final E e, final long timeout, final TimeUnit unit) throws InterruptedException { + return offerFirst(e, PoolImplUtils.toDuration(timeout, unit)); + } + + @Override + public boolean offerLast(final E e) { + Objects.requireNonNull(e, "e"); + lock.lock(); + try { + return linkLast(e); + } finally { + lock.unlock(); + } + } + + /** + * Links the provided element as the last in the queue, waiting up to the + * specified time to do so if the queue is full. + * + * @param e element to link + * @param timeout length of time to wait + * + * @return {@code true} if successful, otherwise {@code false} + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whist waiting + * for space + */ + boolean offerLast(final E e, final Duration timeout) throws InterruptedException { + Objects.requireNonNull(e, "e"); + long nanos = timeout.toNanos(); + lock.lockInterruptibly(); + try { + while (!linkLast(e)) { + if (nanos <= 0) { + return false; + } + nanos = notFull.awaitNanos(nanos); + } + return true; + } finally { + lock.unlock(); + } + } + + /** + * Links the provided element as the last in the queue, waiting up to the + * specified time to do so if the queue is full. + * + * @param e element to link + * @param timeout length of time to wait + * @param unit units that timeout is expressed in + * + * @return {@code true} if successful, otherwise {@code false} + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whist waiting + * for space + */ + public boolean offerLast(final E e, final long timeout, final TimeUnit unit) throws InterruptedException { + return offerLast(e, PoolImplUtils.toDuration(timeout, unit)); + } + + @Override + public E peek() { + return peekFirst(); + } + + // BlockingQueue methods + + @Override + public E peekFirst() { + lock.lock(); + try { + return first == null ? null : first.item; + } finally { + lock.unlock(); + } + } + + @Override + public E peekLast() { + lock.lock(); + try { + return last == null ? null : last.item; + } finally { + lock.unlock(); + } + } + + @Override + public E poll() { + return pollFirst(); + } + + /** + * Unlinks the first element in the queue, waiting up to the specified time + * to do so if the queue is empty. + * + *

    This method is equivalent to {@link #pollFirst(long, TimeUnit)}. + * + * @param timeout length of time to wait + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + E poll(final Duration timeout) throws InterruptedException { + return pollFirst(timeout); + } + + /** + * Unlinks the first element in the queue, waiting up to the specified time + * to do so if the queue is empty. + * + *

    This method is equivalent to {@link #pollFirst(long, TimeUnit)}. + * + * @param timeout length of time to wait + * @param unit units that timeout is expressed in + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + public E poll(final long timeout, final TimeUnit unit) throws InterruptedException { + return pollFirst(timeout, unit); + } + + @Override + public E pollFirst() { + lock.lock(); + try { + return unlinkFirst(); + } finally { + lock.unlock(); + } + } + + /** + * Unlinks the first element in the queue, waiting up to the specified time + * to do so if the queue is empty. + * + * @param timeout length of time to wait + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + E pollFirst(final Duration timeout) throws InterruptedException { + long nanos = timeout.toNanos(); + lock.lockInterruptibly(); + try { + E x; + while ((x = unlinkFirst()) == null) { + if (nanos <= 0) { + return null; + } + nanos = notEmpty.awaitNanos(nanos); + } + return x; + } finally { + lock.unlock(); + } + } + + /** + * Unlinks the first element in the queue, waiting up to the specified time + * to do so if the queue is empty. + * + * @param timeout length of time to wait + * @param unit units that timeout is expressed in + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + public E pollFirst(final long timeout, final TimeUnit unit) throws InterruptedException { + return pollFirst(PoolImplUtils.toDuration(timeout, unit)); + } + + @Override + public E pollLast() { + lock.lock(); + try { + return unlinkLast(); + } finally { + lock.unlock(); + } + } + + /** + * Unlinks the last element in the queue, waiting up to the specified time + * to do so if the queue is empty. + * + * @param timeout length of time to wait + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + public E pollLast(final Duration timeout) + throws InterruptedException { + long nanos = timeout.toNanos(); + lock.lockInterruptibly(); + try { + E x; + while ((x = unlinkLast()) == null) { + if (nanos <= 0) { + return null; + } + nanos = notEmpty.awaitNanos(nanos); + } + return x; + } finally { + lock.unlock(); + } + } + + /** + * Unlinks the last element in the queue, waiting up to the specified time + * to do so if the queue is empty. + * + * @param timeout length of time to wait + * @param unit units that timeout is expressed in + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + public E pollLast(final long timeout, final TimeUnit unit) + throws InterruptedException { + return pollLast(PoolImplUtils.toDuration(timeout, unit)); + } + + @Override + public E pop() { + return removeFirst(); + } + + @Override + public void push(final E e) { + addFirst(e); + } + + /** + * Links the provided element as the last in the queue, waiting until there + * is space to do so if the queue is full. + * + *

    + * This method is equivalent to {@link #putLast(Object)}. + *

    + * + * @param e element to link + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whilst waiting + * for space + */ + public void put(final E e) throws InterruptedException { + putLast(e); + } + + /** + * Links the provided element as the first in the queue, waiting until there + * is space to do so if the queue is full. + * + * @param e element to link + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whilst waiting + * for space + */ + public void putFirst(final E e) throws InterruptedException { + Objects.requireNonNull(e, "e"); + lock.lock(); + try { + while (!linkFirst(e)) { + notFull.await(); + } + } finally { + lock.unlock(); + } + } + + /** + * Links the provided element as the last in the queue, waiting until there + * is space to do so if the queue is full. + * + * @param e element to link + * + * @throws NullPointerException if e is null + * @throws InterruptedException if the thread is interrupted whilst waiting + * for space + */ + public void putLast(final E e) throws InterruptedException { + Objects.requireNonNull(e, "e"); + lock.lock(); + try { + while (!linkLast(e)) { + notFull.await(); + } + } finally { + lock.unlock(); + } + } + + // Stack methods + + /** + * Reconstitutes this deque from a stream (that is, + * deserialize it). + * @param s the stream + */ + private void readObject(final ObjectInputStream s) + throws IOException, ClassNotFoundException { + s.defaultReadObject(); + count = 0; + first = null; + last = null; + // Read in all elements and place in queue + for (;;) { + @SuppressWarnings("unchecked") + final E item = (E)s.readObject(); + if (item == null) { + break; + } + add(item); + } + } + + /** + * Returns the number of additional elements that this deque can ideally + * (in the absence of memory or resource constraints) accept without + * blocking. This is always equal to the initial capacity of this deque + * less the current {@code size} of this deque. + * + *

    + * Note that you cannot always tell if an attempt to insert + * an element will succeed by inspecting {@code remainingCapacity} + * because it may be the case that another thread is about to + * insert or remove an element. + *

    + * + * @return The number of additional elements the queue is able to accept + */ + public int remainingCapacity() { + lock.lock(); + try { + return capacity - count; + } finally { + lock.unlock(); + } + } + + // Collection methods + + /** + * Retrieves and removes the head of the queue represented by this deque. + * This method differs from {@link #poll poll} only in that it throws an + * exception if this deque is empty. + * + *

    + * This method is equivalent to {@link #removeFirst() removeFirst}. + *

    + * + * @return the head of the queue represented by this deque + * @throws NoSuchElementException if this deque is empty + */ + @Override + public E remove() { + return removeFirst(); + } + + /** + * Removes the first occurrence of the specified element from this deque. + * If the deque does not contain the element, it is unchanged. + * More formally, removes the first element {@code e} such that + * {@code o.equals(e)} (if such an element exists). + * Returns {@code true} if this deque contained the specified element + * (or equivalently, if this deque changed as a result of the call). + * + *

    + * This method is equivalent to + * {@link #removeFirstOccurrence(Object) removeFirstOccurrence}. + *

    + * + * @param o element to be removed from this deque, if present + * @return {@code true} if this deque changed as a result of the call + */ + @Override + public boolean remove(final Object o) { + return removeFirstOccurrence(o); + } + + @Override + public E removeFirst() { + final E x = pollFirst(); + if (x == null) { + throw new NoSuchElementException(); + } + return x; + } + + /* + * TODO: Add support for more efficient bulk operations. + * + * We don't want to acquire the lock for every iteration, but we + * also want other threads a chance to interact with the + * collection, especially when count is close to capacity. + */ + +// /** +// * Adds all of the elements in the specified collection to this +// * queue. Attempts to addAll of a queue to itself result in +// * {@code IllegalArgumentException}. Further, the behavior of +// * this operation is undefined if the specified collection is +// * modified while the operation is in progress. +// * +// * @param c collection containing elements to be added to this queue +// * @return {@code true} if this queue changed as a result of the call +// * @throws ClassCastException +// * @throws NullPointerException +// * @throws IllegalArgumentException +// * @throws IllegalStateException +// * @see #add(Object) +// */ +// public boolean addAll(Collection c) { +// if (c == null) +// throw new NullPointerException(); +// if (c == this) +// throw new IllegalArgumentException(); +// final ReentrantLock lock = this.lock; +// lock.lock(); +// try { +// boolean modified = false; +// for (E e : c) +// if (linkLast(e)) +// modified = true; +// return modified; +// } finally { +// lock.unlock(); +// } +// } + + @Override + public boolean removeFirstOccurrence(final Object o) { + if (o == null) { + return false; + } + lock.lock(); + try { + for (Node p = first; p != null; p = p.next) { + if (o.equals(p.item)) { + unlink(p); + return true; + } + } + return false; + } finally { + lock.unlock(); + } + } + + @Override + public E removeLast() { + final E x = pollLast(); + if (x == null) { + throw new NoSuchElementException(); + } + return x; + } + + @Override + public boolean removeLastOccurrence(final Object o) { + if (o == null) { + return false; + } + lock.lock(); + try { + for (Node p = last; p != null; p = p.prev) { + if (o.equals(p.item)) { + unlink(p); + return true; + } + } + return false; + } finally { + lock.unlock(); + } + } + + /** + * Returns the number of elements in this deque. + * + * @return the number of elements in this deque + */ + @Override + public int size() { + lock.lock(); + try { + return count; + } finally { + lock.unlock(); + } + } + + /** + * Unlinks the first element in the queue, waiting until there is an element + * to unlink if the queue is empty. + * + *

    + * This method is equivalent to {@link #takeFirst()}. + *

    + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + public E take() throws InterruptedException { + return takeFirst(); + } + + /** + * Unlinks the first element in the queue, waiting until there is an element + * to unlink if the queue is empty. + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + public E takeFirst() throws InterruptedException { + lock.lock(); + try { + E x; + while ((x = unlinkFirst()) == null) { + notEmpty.await(); + } + return x; + } finally { + lock.unlock(); + } + } + + /** + * Unlinks the last element in the queue, waiting until there is an element + * to unlink if the queue is empty. + * + * @return the unlinked element + * @throws InterruptedException if the current thread is interrupted + */ + public E takeLast() throws InterruptedException { + lock.lock(); + try { + E x; + while ((x = unlinkLast()) == null) { + notEmpty.await(); + } + return x; + } finally { + lock.unlock(); + } + } + + /** + * Returns an array containing all of the elements in this deque, in + * proper sequence (from first to last element). + * + *

    + * The returned array will be "safe" in that no references to it are + * maintained by this deque. (In other words, this method must allocate + * a new array). The caller is thus free to modify the returned array. + *

    + *

    + * This method acts as bridge between array-based and collection-based + * APIs. + *

    + * + * @return an array containing all of the elements in this deque + */ + @Override + public Object[] toArray() { + lock.lock(); + try { + final Object[] a = new Object[count]; + int k = 0; + for (Node p = first; p != null; p = p.next) { + a[k++] = p.item; + } + return a; + } finally { + lock.unlock(); + } + } + + @SuppressWarnings("unchecked") + @Override + public T[] toArray(T[] a) { + lock.lock(); + try { + if (a.length < count) { + a = (T[])java.lang.reflect.Array.newInstance + (a.getClass().getComponentType(), count); + } + int k = 0; + for (Node p = first; p != null; p = p.next) { + a[k++] = (T)p.item; + } + if (a.length > k) { + a[k] = null; + } + return a; + } finally { + lock.unlock(); + } + } + + @Override + public String toString() { + lock.lock(); + try { + return super.toString(); + } finally { + lock.unlock(); + } + } + + /** + * Unlinks the provided node. + * + * @param x The node to unlink + */ + private void unlink(final Node x) { + // assert lock.isHeldByCurrentThread(); + final Node p = x.prev; + final Node n = x.next; + if (p == null) { + unlinkFirst(); + } else if (n == null) { + unlinkLast(); + } else { + p.next = n; + n.prev = p; + x.item = null; + // Don't mess with x's links. They may still be in use by + // an iterator. + --count; + notFull.signal(); + } + } + + // Monitoring methods + + /** + * Removes and returns the first element, or null if empty. + * + * @return The first element or {@code null} if empty + */ + private E unlinkFirst() { + // assert lock.isHeldByCurrentThread(); + final Node f = first; + if (f == null) { + return null; + } + final Node n = f.next; + final E item = f.item; + f.item = null; + f.next = f; // help GC + first = n; + if (n == null) { + last = null; + } else { + n.prev = null; + } + --count; + notFull.signal(); + return item; + } + + /** + * Removes and returns the last element, or null if empty. + * + * @return The first element or {@code null} if empty + */ + private E unlinkLast() { + // assert lock.isHeldByCurrentThread(); + final Node l = last; + if (l == null) { + return null; + } + final Node p = l.prev; + final E item = l.item; + l.item = null; + l.prev = l; // help GC + last = p; + if (p == null) { + first = null; + } else { + p.next = null; + } + --count; + notFull.signal(); + return item; + } + + /** + * Saves the state of this deque to a stream (that is, serialize it). + * + * @serialData The capacity (int), followed by elements (each an + * {@code Object}) in the proper order, followed by a null + * @param s the stream + * @throws IOException if I/O errors occur while writing to the underlying {@code OutputStream} + */ + private void writeObject(final java.io.ObjectOutputStream s) throws IOException { + lock.lock(); + try { + // Write out capacity and any hidden stuff + s.defaultWriteObject(); + // Write out all elements in the proper order. + for (Node p = first; p != null; p = p.next) { + s.writeObject(p.item); + } + // Use trailing null as sentinel + s.writeObject(null); + } finally { + lock.unlock(); + } + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java b/java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java new file mode 100644 index 0000000..24dcac6 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.PrintWriter; + +/** + * CallStack strategy using no-op implementations of all functionality. Can be used by default when abandoned object + * logging is disabled. + * + * @since 2.5 + */ +public class NoOpCallStack implements CallStack { + + /** + * Singleton instance. + */ + public static final CallStack INSTANCE = new NoOpCallStack(); + + /** + * Constructs the singleton instance. + */ + private NoOpCallStack() { + } + + @Override + public void clear() { + // no-op + } + + @Override + public void fillInStackTrace() { + // no-op + } + + @Override + public boolean printStackTrace(final PrintWriter writer) { + return false; + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java b/java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java new file mode 100644 index 0000000..eaea8ca --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java @@ -0,0 +1,233 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.apache.tomcat.dbcp.pool2.PooledObjectFactory; + +/** + * Implementation specific utilities. + * + * @since 2.0 + */ +class PoolImplUtils { + + /** + * Identifies the concrete type of object that an object factory creates. + * + * @param factoryClass The factory to examine + * + * @return the type of object the factory creates + */ + @SuppressWarnings("rawtypes") + static Class getFactoryType(final Class factoryClass) { + final Class type = PooledObjectFactory.class; + final Object genericType = getGenericType(type, factoryClass); + if (genericType instanceof Integer) { + // POOL-324 org.apache.commons.pool2.impl.GenericObjectPool.getFactoryType() throws + // java.lang.ClassCastException + // + // A bit hackish, but we must handle cases when getGenericType() does not return a concrete types. + final ParameterizedType pi = getParameterizedType(type, factoryClass); + if (pi != null) { + final Type[] bounds = ((TypeVariable) pi.getActualTypeArguments()[((Integer) genericType).intValue()]) + .getBounds(); + if (bounds != null && bounds.length > 0) { + final Type bound0 = bounds[0]; + if (bound0 instanceof Class) { + return (Class) bound0; + } + } + } + // last resort: Always return a Class + return Object.class; + } + return (Class) genericType; + } + + /** + * Gets the concrete type used by an implementation of an interface that uses a generic type. + * + * @param type The interface that defines a generic type + * @param clazz The class that implements the interface with a concrete type + * @param The interface type + * + * @return concrete type used by the implementation + */ + private static Object getGenericType(final Class type, final Class clazz) { + if (type == null || clazz == null) { + // Error will be logged further up the call stack + return null; + } + + // Look to see if this class implements the generic interface + final ParameterizedType pi = getParameterizedType(type, clazz); + if (pi != null) { + return getTypeParameter(clazz, pi.getActualTypeArguments()[0]); + } + + // Interface not found on this class. Look at the superclass. + @SuppressWarnings("unchecked") + final Class superClass = (Class) clazz.getSuperclass(); + + final Object result = getGenericType(type, superClass); + if (result instanceof Class) { + // Superclass implements interface and defines explicit type for generic + return result; + } + if (result instanceof Integer) { + // Superclass implements interface and defines unknown type for generic + // Map that unknown type to the generic types defined in this class + final ParameterizedType superClassType = (ParameterizedType) clazz.getGenericSuperclass(); + return getTypeParameter(clazz, superClassType.getActualTypeArguments()[((Integer) result).intValue()]); + } + // Error will be logged further up the call stack + return null; + } + + /** + * Gets the matching parameterized type or null. + * + * @param type The interface that defines a generic type. + * @param clazz The class that implements the interface with a concrete type. + * @param The interface type. + * @return the matching parameterized type or null. + */ + private static ParameterizedType getParameterizedType(final Class type, final Class clazz) { + for (final Type iface : clazz.getGenericInterfaces()) { + // Only need to check interfaces that use generics + if (iface instanceof ParameterizedType) { + final ParameterizedType pi = (ParameterizedType) iface; + // Look for the generic interface + if (pi.getRawType() instanceof Class && type.isAssignableFrom((Class) pi.getRawType())) { + return pi; + } + } + } + return null; + } + + /** + * For a generic parameter, return either the Class used or if the type is unknown, the index for the type in + * definition of the class + * + * @param clazz defining class + * @param argType the type argument of interest + * + * @return An instance of {@link Class} representing the type used by the type parameter or an instance of + * {@link Integer} representing the index for the type in the definition of the defining class + */ + private static Object getTypeParameter(final Class clazz, final Type argType) { + if (argType instanceof Class) { + return argType; + } + final TypeVariable[] tvs = clazz.getTypeParameters(); + for (int i = 0; i < tvs.length; i++) { + if (tvs[i].equals(argType)) { + return Integer.valueOf(i); + } + } + return null; + } + + static boolean isPositive(final Duration delay) { + return delay != null && !delay.isNegative() && !delay.isZero(); + } + + /** + * Returns the greater of two {@code Instant} values. That is, the result is the argument closer to the value of + * {@link Instant#MAX}. If the arguments have the same value, the result is that same value. + * + * @param a an argument. + * @param b another argument. + * @return the larger of {@code a} and {@code b}. + */ + static Instant max(final Instant a, final Instant b) { + return a.compareTo(b) > 0 ? a : b; + } + + /** + * Returns the smaller of two {@code Instant} values. That is, the result is the argument closer to the value of + * {@link Instant#MIN}. If the arguments have the same value, the result is that same value. + * + * @param a an argument. + * @param b another argument. + * @return the smaller of {@code a} and {@code b}. + */ + static Instant min(final Instant a, final Instant b) { + return a.compareTo(b) < 0 ? a : b; + } + + /** + * Returns a non-null duration, value if non-null, otherwise defaultValue. + * + * @param value May be null. + * @param defaultValue May not be null/ + * @return value if non-null, otherwise defaultValue. + */ + static Duration nonNull(final Duration value, final Duration defaultValue) { + return value != null ? value : Objects.requireNonNull(defaultValue, "defaultValue"); + } + + /** + * Converts a {@link TimeUnit} to a {@link ChronoUnit}. + * + * @param timeUnit A TimeUnit. + * @return The corresponding ChronoUnit. + */ + static ChronoUnit toChronoUnit(final TimeUnit timeUnit) { + // TODO when using Java >= 9: Use TimeUnit.toChronoUnit(). + switch (Objects.requireNonNull(timeUnit)) { + case NANOSECONDS: + return ChronoUnit.NANOS; + case MICROSECONDS: + return ChronoUnit.MICROS; + case MILLISECONDS: + return ChronoUnit.MILLIS; + case SECONDS: + return ChronoUnit.SECONDS; + case MINUTES: + return ChronoUnit.MINUTES; + case HOURS: + return ChronoUnit.HOURS; + case DAYS: + return ChronoUnit.DAYS; + default: + throw new IllegalArgumentException(timeUnit.toString()); + } + } + + /** + * Converts am amount and TimeUnit into a Duration. + * + * @param amount the amount of the duration, measured in terms of the unit, positive or negative + * @param timeUnit the unit that the duration is measured in, must have an exact duration, not null + * @return a Duration. + */ + static Duration toDuration(final long amount, final TimeUnit timeUnit) { + return Duration.of(amount, PoolImplUtils.toChronoUnit(timeUnit)); + } + +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java b/java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java new file mode 100644 index 0000000..68f2fca --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java @@ -0,0 +1,96 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.lang.ref.SoftReference; + +/** + * Extension of {@link DefaultPooledObject} to wrap pooled soft references. + * + *

    This class is intended to be thread-safe.

    + * + * @param the type of the underlying object that the wrapped SoftReference + * refers to. + * + * @since 2.0 + */ +public class PooledSoftReference extends DefaultPooledObject { + + /** SoftReference wrapped by this object */ + private volatile SoftReference reference; + + /** + * Creates a new PooledSoftReference wrapping the provided reference. + * + * @param reference SoftReference to be managed by the pool + */ + public PooledSoftReference(final SoftReference reference) { + super(null); // Null the hard reference in the parent + this.reference = reference; + } + + /** + * Gets the object that the wrapped SoftReference refers to. + *

    + * Note that if the reference has been cleared, this method will return + * null. + *

    + * + * @return Object referred to by the SoftReference + */ + @Override + public T getObject() { + return reference.get(); + } + + /** + * Gets the SoftReference wrapped by this object. + * + * @return underlying SoftReference + */ + public synchronized SoftReference getReference() { + return reference; + } + + /** + * Sets the wrapped reference. + * + *

    This method exists to allow a new, non-registered reference to be + * held by the pool to track objects that have been checked out of the pool. + * The actual parameter should be a reference to the same + * object that {@link #getObject()} returns before calling this method.

    + * + * @param reference new reference + */ + public synchronized void setReference(final SoftReference reference) { + this.reference = reference; + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("Referenced Object: "); + result.append(getObject().toString()); + result.append(", State: "); + synchronized (this) { + result.append(getState().toString()); + } + return result.toString(); + // TODO add other attributes + // TODO encapsulate state and other attribute display in parent + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/SecurityManagerCallStack.java b/java/org/apache/tomcat/dbcp/pool2/impl/SecurityManagerCallStack.java new file mode 100644 index 0000000..04e030a --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/SecurityManagerCallStack.java @@ -0,0 +1,122 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A {@link CallStack} strategy using a {@link SecurityManager}. Obtaining the current call stack is much faster via a + * SecurityManger, but access to the underlying method may be restricted by the current SecurityManager. In environments + * where a SecurityManager cannot be created, {@link ThrowableCallStack} should be used instead. + * + * @see RuntimePermission + * @see SecurityManager#getClassContext() + * @since 2.4.3 + */ +public class SecurityManagerCallStack implements CallStack { + + /** + * A custom security manager. + */ + private static class PrivateSecurityManager extends SecurityManager { + + /** + * Gets the class stack. + * + * @return class stack + */ + private List>> getCallStack() { + final Stream>> map = Stream.of(getClassContext()).map(WeakReference::new); + return map.collect(Collectors.toList()); + } + } + + /** + * A snapshot of a class stack. + */ + private static class Snapshot { + private final long timestampMillis = System.currentTimeMillis(); + private final List>> stack; + + /** + * Constructs a new snapshot with a class stack. + * + * @param stack class stack + */ + private Snapshot(final List>> stack) { + this.stack = stack; + } + } + + private final String messageFormat; + + //@GuardedBy("dateFormat") + private final DateFormat dateFormat; + + private final PrivateSecurityManager securityManager; + + private volatile Snapshot snapshot; + + /** + * Creates a new instance. + * + * @param messageFormat message format + * @param useTimestamp whether to format the dates in the output message or not + */ + public SecurityManagerCallStack(final String messageFormat, final boolean useTimestamp) { + this.messageFormat = messageFormat; + this.dateFormat = useTimestamp ? new SimpleDateFormat(messageFormat) : null; + this.securityManager = AccessController.doPrivileged((PrivilegedAction) PrivateSecurityManager::new); + } + + @Override + public void clear() { + snapshot = null; + } + + @Override + public void fillInStackTrace() { + snapshot = new Snapshot(securityManager.getCallStack()); + } + + @Override + public boolean printStackTrace(final PrintWriter writer) { + final Snapshot snapshotRef = this.snapshot; + if (snapshotRef == null) { + return false; + } + final String message; + if (dateFormat == null) { + message = messageFormat; + } else { + synchronized (dateFormat) { + message = dateFormat.format(Long.valueOf(snapshotRef.timestampMillis)); + } + } + writer.println(message); + snapshotRef.stack.forEach(reference -> writer.println(reference.get())); + return true; + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java b/java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java new file mode 100644 index 0000000..8a232ea --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java @@ -0,0 +1,447 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Optional; + +import org.apache.tomcat.dbcp.pool2.BaseObjectPool; +import org.apache.tomcat.dbcp.pool2.ObjectPool; +import org.apache.tomcat.dbcp.pool2.PoolUtils; +import org.apache.tomcat.dbcp.pool2.PooledObjectFactory; + +/** + * A {@link java.lang.ref.SoftReference SoftReference} based {@link ObjectPool}. + *

    + * This class is intended to be thread-safe. + *

    + * + * @param + * Type of element pooled in this pool. + * + * @since 2.0 + */ +public class SoftReferenceObjectPool extends BaseObjectPool { + + /** Factory to source pooled objects */ + private final PooledObjectFactory factory; + + /** + * Queue of broken references that might be able to be removed from + * {@code _pool}. This is used to help {@link #getNumIdle()} be more + * accurate with minimal performance overhead. + */ + private final ReferenceQueue refQueue = new ReferenceQueue<>(); + + /** Count of instances that have been checkout out to pool clients */ + private int numActive; // @GuardedBy("this") + + /** Total number of instances that have been destroyed */ + private long destroyCount; // @GuardedBy("this") + + + /** Total number of instances that have been created */ + private long createCount; // @GuardedBy("this") + + /** Idle references - waiting to be borrowed */ + private final LinkedBlockingDeque> idleReferences = + new LinkedBlockingDeque<>(); + + /** All references - checked out or waiting to be borrowed. */ + private final ArrayList> allReferences = + new ArrayList<>(); + + /** + * Constructs a {@code SoftReferenceObjectPool} with the specified factory. + * + * @param factory object factory to use. + */ + public SoftReferenceObjectPool(final PooledObjectFactory factory) { + this.factory = factory; + } + + /** + * Creates an object, and places it into the pool. addObject() is useful for + * "pre-loading" a pool with idle objects. + *

    + * Before being added to the pool, the newly created instance is + * {@link PooledObjectFactory#validateObject( + * org.apache.tomcat.dbcp.pool2.PooledObject) validated} and + * {@link PooledObjectFactory#passivateObject( + * org.apache.tomcat.dbcp.pool2.PooledObject) passivated}. If + * validation fails, the new instance is + * {@link PooledObjectFactory#destroyObject( + * org.apache.tomcat.dbcp.pool2.PooledObject) destroyed}. Exceptions + * generated by the factory {@code makeObject} or + * {@code passivate} are propagated to the caller. Exceptions + * destroying instances are silently swallowed. + *

    + * + * @throws IllegalStateException + * if invoked on a {@link #close() closed} pool + * @throws Exception + * when the {@link #getFactory() factory} has a problem creating + * or passivating an object. + */ + @Override + public synchronized void addObject() throws Exception { + assertOpen(); + if (factory == null) { + throw new IllegalStateException( + "Cannot add objects without a factory."); + } + final T obj = factory.makeObject().getObject(); + createCount++; + // Create and register with the queue + final PooledSoftReference ref = new PooledSoftReference<>( + new SoftReference<>(obj, refQueue)); + allReferences.add(ref); + + boolean success = true; + if (!factory.validateObject(ref)) { + success = false; + } else { + factory.passivateObject(ref); + } + + final boolean shouldDestroy = !success; + if (success) { + idleReferences.add(ref); + notifyAll(); // numActive has changed + } + + if (shouldDestroy) { + try { + destroy(ref); + } catch (final Exception ignored) { + // ignored + } + } + } + + /** + * Borrows an object from the pool. If there are no idle instances available + * in the pool, the configured factory's + * {@link PooledObjectFactory#makeObject()} method is invoked to create a + * new instance. + *

    + * All instances are {@link PooledObjectFactory#activateObject( + * org.apache.tomcat.dbcp.pool2.PooledObject) activated} + * and {@link PooledObjectFactory#validateObject( + * org.apache.tomcat.dbcp.pool2.PooledObject) + * validated} before being returned by this method. If validation fails or + * an exception occurs activating or validating an idle instance, the + * failing instance is {@link PooledObjectFactory#destroyObject( + * org.apache.tomcat.dbcp.pool2.PooledObject) + * destroyed} and another instance is retrieved from the pool, validated and + * activated. This process continues until either the pool is empty or an + * instance passes validation. If the pool is empty on activation or it does + * not contain any valid instances, the factory's {@code makeObject} + * method is used to create a new instance. If the created instance either + * raises an exception on activation or fails validation, + * {@code NoSuchElementException} is thrown. Exceptions thrown by + * {@code MakeObject} are propagated to the caller; but other than + * {@code ThreadDeath} or {@code VirtualMachineError}, exceptions + * generated by activation, validation or destroy methods are swallowed + * silently. + *

    + * + * @throws NoSuchElementException + * if a valid object cannot be provided + * @throws IllegalStateException + * if invoked on a {@link #close() closed} pool + * @throws Exception + * if an exception occurs creating a new instance + * @return a valid, activated object instance + */ + @SuppressWarnings("null") // ref cannot be null + @Override + public synchronized T borrowObject() throws Exception { + assertOpen(); + T obj = null; + boolean newlyCreated = false; + PooledSoftReference ref = null; + while (null == obj) { + if (idleReferences.isEmpty()) { + if (null == factory) { + throw new NoSuchElementException(); + } + newlyCreated = true; + obj = factory.makeObject().getObject(); + createCount++; + // Do not register with the queue + ref = new PooledSoftReference<>(new SoftReference<>(obj)); + allReferences.add(ref); + } else { + ref = idleReferences.pollFirst(); + obj = ref.getObject(); + // Clear the reference so it will not be queued, but replace with a + // a new, non-registered reference so we can still track this object + // in allReferences + ref.getReference().clear(); + ref.setReference(new SoftReference<>(obj)); + } + if (null != factory && null != obj) { + try { + factory.activateObject(ref); + if (!factory.validateObject(ref)) { + throw new Exception("ValidateObject failed"); + } + } catch (final Throwable t) { + PoolUtils.checkRethrow(t); + try { + destroy(ref); + } catch (final Throwable t2) { + PoolUtils.checkRethrow(t2); + // Swallowed + } finally { + obj = null; + } + if (newlyCreated) { + throw new NoSuchElementException("Could not create a validated object, cause: " + t); + } + } + } + } + numActive++; + ref.allocate(); + return obj; + } + + /** + * Clears any objects sitting idle in the pool. + */ + @Override + public synchronized void clear() { + if (null != factory) { + idleReferences.forEach(ref -> { + try { + if (null != ref.getObject()) { + factory.destroyObject(ref); + } + } catch (final Exception ignored) { + // ignored, keep destroying the rest + } + }); + } + idleReferences.clear(); + pruneClearedReferences(); + } + + /** + * Closes this pool, and frees any resources associated with it. Invokes + * {@link #clear()} to destroy and remove instances in the pool. + *

    + * Calling {@link #addObject} or {@link #borrowObject} after invoking this + * method on a pool will cause them to throw an + * {@link IllegalStateException}. + *

    + */ + @Override + public void close() { + super.close(); + clear(); + } + + /** + * Destroys a {@code PooledSoftReference} and removes it from the idle and all + * references pools. + * + * @param toDestroy PooledSoftReference to destroy + * + * @throws Exception If an error occurs while trying to destroy the object + */ + private void destroy(final PooledSoftReference toDestroy) throws Exception { + toDestroy.invalidate(); + idleReferences.remove(toDestroy); + allReferences.remove(toDestroy); + try { + factory.destroyObject(toDestroy); + } finally { + destroyCount++; + toDestroy.getReference().clear(); + } + } + + /** + * Finds the PooledSoftReference in allReferences that points to obj. + * + * @param obj returning object + * @return PooledSoftReference wrapping a soft reference to obj + */ + private PooledSoftReference findReference(final T obj) { + final Optional> first = allReferences.stream() + .filter(reference -> reference.getObject() != null && reference.getObject().equals(obj)).findFirst(); + return first.orElse(null); + } + + /** + * Gets the {@link PooledObjectFactory} used by this pool to create and + * manage object instances. + * + * @return the factory + */ + public synchronized PooledObjectFactory getFactory() { + return factory; + } + + /** + * Gets the number of instances currently borrowed from this pool. + * + * @return the number of instances currently borrowed from this pool + */ + @Override + public synchronized int getNumActive() { + return numActive; + } + + /** + * Gets an approximation not less than the of the number of idle + * instances in the pool. + * + * @return estimated number of idle instances in the pool + */ + @Override + public synchronized int getNumIdle() { + pruneClearedReferences(); + return idleReferences.size(); + } + + @Override + public synchronized void invalidateObject(final T obj) throws Exception { + final PooledSoftReference ref = findReference(obj); + if (ref == null) { + throw new IllegalStateException( + "Object to invalidate is not currently part of this pool"); + } + if (factory != null) { + destroy(ref); + } + numActive--; + notifyAll(); // numActive has changed + } + + /** + * If any idle objects were garbage collected, remove their + * {@link Reference} wrappers from the idle object pool. + */ + private void pruneClearedReferences() { + // Remove wrappers for enqueued references from idle and allReferences lists + removeClearedReferences(idleReferences.iterator()); + removeClearedReferences(allReferences.iterator()); + while (refQueue.poll() != null) { // NOPMD + } + } + + /** + * Clears cleared references from iterator's collection + * @param iterator iterator over idle/allReferences + */ + private void removeClearedReferences(final Iterator> iterator) { + PooledSoftReference ref; + while (iterator.hasNext()) { + ref = iterator.next(); + if (ref.getReference() == null || ref.getReference().isEnqueued()) { + iterator.remove(); + } + } + } + + /** + * Returns an instance to the pool after successful validation and + * passivation. The returning instance is destroyed if any of the following + * are true: + *
      + *
    • the pool is closed
    • + *
    • {@link PooledObjectFactory#validateObject( + * org.apache.tomcat.dbcp.pool2.PooledObject) validation} fails + *
    • + *
    • {@link PooledObjectFactory#passivateObject( + * org.apache.tomcat.dbcp.pool2.PooledObject) passivation} + * throws an exception
    • + *
    + * Exceptions passivating or destroying instances are silently swallowed. + * Exceptions validating instances are propagated to the client. + * + * @param obj + * instance to return to the pool + * @throws IllegalArgumentException + * if obj is not currently part of this pool + */ + @Override + public synchronized void returnObject(final T obj) throws Exception { + boolean success = !isClosed(); + final PooledSoftReference ref = findReference(obj); + if (ref == null) { + throw new IllegalStateException( + "Returned object not currently part of this pool"); + } + if (factory != null) { + if (!factory.validateObject(ref)) { + success = false; + } else { + try { + factory.passivateObject(ref); + } catch (final Exception e) { + success = false; + } + } + } + + final boolean shouldDestroy = !success; + numActive--; + if (success) { + + // Deallocate and add to the idle instance pool + ref.deallocate(); + idleReferences.add(ref); + } + notifyAll(); // numActive has changed + + if (shouldDestroy && factory != null) { + try { + destroy(ref); + } catch (final Exception ignored) { + // ignored + } + } + } + + @Override + protected void toStringAppendFields(final StringBuilder builder) { + super.toStringAppendFields(builder); + builder.append(", factory="); + builder.append(factory); + builder.append(", refQueue="); + builder.append(refQueue); + builder.append(", numActive="); + builder.append(numActive); + builder.append(", destroyCount="); + builder.append(destroyCount); + builder.append(", createCount="); + builder.append(createCount); + builder.append(", idleReferences="); + builder.append(idleReferences); + builder.append(", allReferences="); + builder.append(allReferences); + } +} diff --git a/java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java b/java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java new file mode 100644 index 0000000..6f18755 --- /dev/null +++ b/java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java @@ -0,0 +1,87 @@ +/* + * 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.tomcat.dbcp.pool2.impl; + +import java.io.PrintWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +/** + * CallStack strategy that uses the stack trace from a {@link Throwable}. This strategy, while slower than the + * SecurityManager implementation, provides call stack method names and other metadata in addition to the call stack + * of classes. + * + * @see Throwable#fillInStackTrace() + * @since 2.4.3 + */ +public class ThrowableCallStack implements CallStack { + + /** + * A snapshot of a throwable. + */ + private static class Snapshot extends Throwable { + private static final long serialVersionUID = 1L; + private final long timestampMillis = System.currentTimeMillis(); + } + + private final String messageFormat; + + //@GuardedBy("dateFormat") + private final DateFormat dateFormat; + + private volatile Snapshot snapshot; + + /** + * Creates a new instance. + * + * @param messageFormat message format + * @param useTimestamp whether to format the dates in the output message or not + */ + public ThrowableCallStack(final String messageFormat, final boolean useTimestamp) { + this.messageFormat = messageFormat; + this.dateFormat = useTimestamp ? new SimpleDateFormat(messageFormat) : null; + } + + @Override + public void clear() { + snapshot = null; + } + + @Override + public void fillInStackTrace() { + snapshot = new Snapshot(); + } + + @Override + public synchronized boolean printStackTrace(final PrintWriter writer) { + final Snapshot snapshotRef = this.snapshot; + if (snapshotRef == null) { + return false; + } + final String message; + if (dateFormat == null) { + message = messageFormat; + } else { + synchronized (dateFormat) { + message = dateFormat.format(Long.valueOf(snapshotRef.timestampMillis)); + } + } + writer.println(message); + snapshotRef.printStackTrace(writer); + return true; + } +} diff --git a/java/org/apache/tomcat/jni/Buffer.java b/java/org/apache/tomcat/jni/Buffer.java new file mode 100644 index 0000000..15ce569 --- /dev/null +++ b/java/org/apache/tomcat/jni/Buffer.java @@ -0,0 +1,35 @@ +/* + * 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.tomcat.jni; + +import java.nio.ByteBuffer; + +/** + * Provides utilities related to the use of directly allocated + * {@link ByteBuffer} instances with native code. + */ +public class Buffer { + + /** + * Returns the memory address of the ByteBuffer. + * + * @param buf Previously allocated ByteBuffer. + * + * @return the memory address + */ + public static native long address(ByteBuffer buf); +} diff --git a/java/org/apache/tomcat/jni/CertificateVerifier.java b/java/org/apache/tomcat/jni/CertificateVerifier.java new file mode 100644 index 0000000..b9b0d48 --- /dev/null +++ b/java/org/apache/tomcat/jni/CertificateVerifier.java @@ -0,0 +1,34 @@ +/* + * 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.tomcat.jni; + +/** + * Is called during handshake and hooked into openssl via {@code SSL_CTX_set_cert_verify_callback}. + */ +public interface CertificateVerifier { + + /** + * Returns {@code true} if the passed in certificate chain could be verified and so the handshake + * should be successful, {@code false} otherwise. + * + * @param ssl the SSL instance + * @param x509 the {@code X509} certificate chain + * @param authAlgorithm the auth algorithm + * @return verified {@code true} if verified successful, {@code false} otherwise + */ + boolean verify(long ssl, byte[][] x509, String authAlgorithm); +} diff --git a/java/org/apache/tomcat/jni/FileInfo.java b/java/org/apache/tomcat/jni/FileInfo.java new file mode 100644 index 0000000..ff807e4 --- /dev/null +++ b/java/org/apache/tomcat/jni/FileInfo.java @@ -0,0 +1,30 @@ +/* + * 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.tomcat.jni; + +/** + * Tomcat Native 1.2.33 and earlier won't initialise unless this class is + * present. This dummy class ensures initialisation gets as far as being able to + * check the version of the Tomcat Native library and reporting a version error + * if 1.2.33 or earlier is present. + */ +public class FileInfo { + + private FileInfo() { + // Hide default constructor + } +} diff --git a/java/org/apache/tomcat/jni/Library.java b/java/org/apache/tomcat/jni/Library.java new file mode 100644 index 0000000..78a0bfd --- /dev/null +++ b/java/org/apache/tomcat/jni/Library.java @@ -0,0 +1,167 @@ +/* + * 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.tomcat.jni; + +import java.io.File; + +public final class Library { + + /* Default library names - use 2.x in preference to 1.x if both are available */ + private static final String [] NAMES = {"tcnative-2", "libtcnative-2", "tcnative-1", "libtcnative-1"}; + /* System property used to define CATALINA_HOME */ + private static final String CATALINA_HOME_PROP = "catalina.home"; + /* + * A handle to the unique Library singleton instance. + */ + private static Library _instance = null; + + private Library() throws Exception { + boolean loaded = false; + StringBuilder err = new StringBuilder(); + File binLib = new File(System.getProperty(CATALINA_HOME_PROP), "bin"); + for (int i = 0; i < NAMES.length; i++) { + File library = new File(binLib, System.mapLibraryName(NAMES[i])); + try { + System.load(library.getAbsolutePath()); + loaded = true; + } catch (ThreadDeath | VirtualMachineError t) { + throw t; + } catch (Throwable t) { + if (library.exists()) { + // File exists but failed to load + throw t; + } + if (i > 0) { + err.append(", "); + } + err.append(t.getMessage()); + } + if (loaded) { + break; + } + } + if (!loaded) { + String path = System.getProperty("java.library.path"); + String [] paths = path.split(File.pathSeparator); + for (String value : NAMES) { + try { + System.loadLibrary(value); + loaded = true; + } catch (ThreadDeath | VirtualMachineError t) { + throw t; + } catch (Throwable t) { + String name = System.mapLibraryName(value); + for (String s : paths) { + File fd = new File(s, name); + if (fd.exists()) { + // File exists but failed to load + throw t; + } + } + if (err.length() > 0) { + err.append(", "); + } + err.append(t.getMessage()); + } + if (loaded) { + break; + } + } + } + if (!loaded) { + StringBuilder names = new StringBuilder(); + for (String name : NAMES) { + names.append(name); + names.append(", "); + } + throw new LibraryNotFoundError(names.substring(0, names.length() -2), err.toString()); + } + } + + private Library(String libraryName) + { + System.loadLibrary(libraryName); + } + + /** + * Create Tomcat Native's global APR pool. This has to be the first call to TCN library. + */ + private static native boolean initialize(); + /** + * Destroys Tomcat Native's global APR pool. This has to be the last call to TCN library. This will destroy any APR + * root pools that have not been explicitly destroyed. + */ + public static native void terminate(); + /* Internal function for loading APR Features */ + private static native int version(int what); + + /* TCN_MAJOR_VERSION */ + public static int TCN_MAJOR_VERSION = 0; + /* TCN_MINOR_VERSION */ + public static int TCN_MINOR_VERSION = 0; + /* TCN_PATCH_VERSION */ + public static int TCN_PATCH_VERSION = 0; + /* TCN_IS_DEV_VERSION */ + public static int TCN_IS_DEV_VERSION = 0; + /* APR_MAJOR_VERSION */ + public static int APR_MAJOR_VERSION = 0; + /* APR_MINOR_VERSION */ + public static int APR_MINOR_VERSION = 0; + /* APR_PATCH_VERSION */ + public static int APR_PATCH_VERSION = 0; + /* APR_IS_DEV_VERSION */ + public static int APR_IS_DEV_VERSION = 0; + + /* TCN_VERSION_STRING */ + public static native String versionString(); + /* APR_VERSION_STRING */ + public static native String aprVersionString(); + + /** + * Setup any APR internal data structures. This MUST be the first function + * called for any APR library. + * @param libraryName the name of the library to load + * + * @return {@code true} if the native code was initialized successfully + * otherwise {@code false} + * + * @throws Exception if a problem occurred during initialization + */ + public static synchronized boolean initialize(String libraryName) throws Exception { + if (_instance == null) { + if (libraryName == null) { + _instance = new Library(); + } else { + _instance = new Library(libraryName); + } + TCN_MAJOR_VERSION = version(0x01); + TCN_MINOR_VERSION = version(0x02); + TCN_PATCH_VERSION = version(0x03); + TCN_IS_DEV_VERSION = version(0x04); + APR_MAJOR_VERSION = version(0x11); + APR_MINOR_VERSION = version(0x12); + APR_PATCH_VERSION = version(0x13); + APR_IS_DEV_VERSION = version(0x14); + + if (APR_MAJOR_VERSION < 1) { + throw new UnsatisfiedLinkError("Unsupported APR Version (" + + aprVersionString() + ")"); + } + } + return initialize(); + } +} diff --git a/java/org/apache/tomcat/jni/LibraryNotFoundError.java b/java/org/apache/tomcat/jni/LibraryNotFoundError.java new file mode 100644 index 0000000..ede1ee1 --- /dev/null +++ b/java/org/apache/tomcat/jni/LibraryNotFoundError.java @@ -0,0 +1,39 @@ +/* + * 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.tomcat.jni; + +public class LibraryNotFoundError extends UnsatisfiedLinkError { + + private static final long serialVersionUID = 1L; + + private final String libraryNames; + + /** + * @param libraryNames A list of the file names of the native libraries that + * failed to load + * @param errors A list of the error messages received when trying to load + * each of the libraries + */ + public LibraryNotFoundError(String libraryNames, String errors){ + super(errors); + this.libraryNames = libraryNames; + } + + public String getLibraryNames(){ + return libraryNames; + } +} diff --git a/java/org/apache/tomcat/jni/Pool.java b/java/org/apache/tomcat/jni/Pool.java new file mode 100644 index 0000000..062c713 --- /dev/null +++ b/java/org/apache/tomcat/jni/Pool.java @@ -0,0 +1,44 @@ +/* + * 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.tomcat.jni; + +/** + * Provides access to APR memory pools which are used to manage memory + * allocations for natively created instances. + */ +public class Pool { + + /** + * Create a new pool. + * + * @param parent The parent pool. If this is 0, the new pool is a root pool. + * If it is non-zero, the new pool will inherit all of its + * parent pool's attributes, except the apr_pool_t will be a + * sub-pool. + * + * @return The pool we have just created. + */ + public static native long create(long parent); + + /** + * Destroy the pool. This takes similar action as apr_pool_clear() and then + * frees all the memory. This will actually free the memory. + * + * @param pool The pool to destroy + */ + public static native void destroy(long pool); +} diff --git a/java/org/apache/tomcat/jni/SSL.java b/java/org/apache/tomcat/jni/SSL.java new file mode 100644 index 0000000..4fff808 --- /dev/null +++ b/java/org/apache/tomcat/jni/SSL.java @@ -0,0 +1,599 @@ +/* + * 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.tomcat.jni; + +public final class SSL { + + /* + * Type definitions mostly from mod_ssl + */ + public static final int UNSET = -1; + /* + * Define the certificate algorithm types + */ + public static final int SSL_ALGO_UNKNOWN = 0; + public static final int SSL_ALGO_RSA = (1<<0); + public static final int SSL_ALGO_DSA = (1<<1); + public static final int SSL_ALGO_ALL = (SSL_ALGO_RSA|SSL_ALGO_DSA); + + public static final int SSL_AIDX_RSA = 0; + public static final int SSL_AIDX_DSA = 1; + public static final int SSL_AIDX_ECC = 3; + public static final int SSL_AIDX_MAX = 4; + /* + * Define IDs for the temporary RSA keys and DH params + */ + + public static final int SSL_TMP_KEY_RSA_512 = 0; + public static final int SSL_TMP_KEY_RSA_1024 = 1; + public static final int SSL_TMP_KEY_RSA_2048 = 2; + public static final int SSL_TMP_KEY_RSA_4096 = 3; + public static final int SSL_TMP_KEY_DH_512 = 4; + public static final int SSL_TMP_KEY_DH_1024 = 5; + public static final int SSL_TMP_KEY_DH_2048 = 6; + public static final int SSL_TMP_KEY_DH_4096 = 7; + public static final int SSL_TMP_KEY_MAX = 8; + + /* + * Define the SSL options + */ + public static final int SSL_OPT_NONE = 0; + public static final int SSL_OPT_RELSET = (1<<0); + public static final int SSL_OPT_STDENVVARS = (1<<1); + public static final int SSL_OPT_EXPORTCERTDATA = (1<<3); + public static final int SSL_OPT_FAKEBASICAUTH = (1<<4); + public static final int SSL_OPT_STRICTREQUIRE = (1<<5); + public static final int SSL_OPT_OPTRENEGOTIATE = (1<<6); + public static final int SSL_OPT_ALL = (SSL_OPT_STDENVVARS|SSL_OPT_EXPORTCERTDATA|SSL_OPT_FAKEBASICAUTH|SSL_OPT_STRICTREQUIRE|SSL_OPT_OPTRENEGOTIATE); + + /* + * Define the SSL Protocol options + */ + public static final int SSL_PROTOCOL_NONE = 0; + public static final int SSL_PROTOCOL_SSLV2 = (1<<0); + public static final int SSL_PROTOCOL_SSLV3 = (1<<1); + public static final int SSL_PROTOCOL_TLSV1 = (1<<2); + public static final int SSL_PROTOCOL_TLSV1_1 = (1<<3); + public static final int SSL_PROTOCOL_TLSV1_2 = (1<<4); + public static final int SSL_PROTOCOL_TLSV1_3 = (1<<5); + public static final int SSL_PROTOCOL_ALL; + + static { + if (version() >= 0x1010100f) { + SSL_PROTOCOL_ALL = (SSL_PROTOCOL_TLSV1 | SSL_PROTOCOL_TLSV1_1 | SSL_PROTOCOL_TLSV1_2 | + SSL_PROTOCOL_TLSV1_3); + } else { + SSL_PROTOCOL_ALL = (SSL_PROTOCOL_TLSV1 | SSL_PROTOCOL_TLSV1_1 | SSL_PROTOCOL_TLSV1_2); + } + } + + + /* + * Define the SSL verify levels + */ + public static final int SSL_CVERIFY_UNSET = UNSET; + public static final int SSL_CVERIFY_NONE = 0; + public static final int SSL_CVERIFY_OPTIONAL = 1; + public static final int SSL_CVERIFY_REQUIRE = 2; + public static final int SSL_CVERIFY_OPTIONAL_NO_CA = 3; + + /* Use either SSL_VERIFY_NONE or SSL_VERIFY_PEER, the last 2 options + * are 'ored' with SSL_VERIFY_PEER if they are desired + */ + public static final int SSL_VERIFY_NONE = 0; + public static final int SSL_VERIFY_PEER = 1; + public static final int SSL_VERIFY_FAIL_IF_NO_PEER_CERT = 2; + public static final int SSL_VERIFY_CLIENT_ONCE = 4; + public static final int SSL_VERIFY_PEER_STRICT = (SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT); + + public static final int SSL_OP_MICROSOFT_SESS_ID_BUG = 0x00000001; + public static final int SSL_OP_NETSCAPE_CHALLENGE_BUG = 0x00000002; + public static final int SSL_OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG = 0x00000008; + public static final int SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG = 0x00000010; + public static final int SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER = 0x00000020; + public static final int SSL_OP_MSIE_SSLV2_RSA_PADDING = 0x00000040; + public static final int SSL_OP_SSLEAY_080_CLIENT_DH_BUG = 0x00000080; + public static final int SSL_OP_TLS_D5_BUG = 0x00000100; + public static final int SSL_OP_TLS_BLOCK_PADDING_BUG = 0x00000200; + + /* Disable SSL 3.0/TLS 1.0 CBC vulnerability workaround that was added + * in OpenSSL 0.9.6d. Usually (depending on the application protocol) + * the workaround is not needed. Unfortunately some broken SSL/TLS + * implementations cannot handle it at all, which is why we include + * it in SSL_OP_ALL. */ + public static final int SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS = 0x00000800; + + /* SSL_OP_ALL: various bug workarounds that should be rather harmless. + * This used to be 0x000FFFFFL before 0.9.7. */ + public static final int SSL_OP_ALL = 0x00000FFF; + /* As server, disallow session resumption on renegotiation */ + public static final int SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION = 0x00010000; + /* Don't use compression even if supported */ + public static final int SSL_OP_NO_COMPRESSION = 0x00020000; + /* Permit unsafe legacy renegotiation */ + public static final int SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION = 0x00040000; + /* If set, always create a new key when using tmp_eddh parameters */ + public static final int SSL_OP_SINGLE_ECDH_USE = 0x00080000; + /* If set, always create a new key when using tmp_dh parameters */ + public static final int SSL_OP_SINGLE_DH_USE = 0x00100000; + /* Set to always use the tmp_rsa key when doing RSA operations, + * even when this violates protocol specs */ + public static final int SSL_OP_EPHEMERAL_RSA = 0x00200000; + /* Set on servers to choose the cipher according to the server's + * preferences */ + public static final int SSL_OP_CIPHER_SERVER_PREFERENCE = 0x00400000; + /* If set, a server will allow a client to issue an SSLv3.0 version number + * as latest version supported in the premaster secret, even when TLSv1.0 + * (version 3.1) was announced in the client hello. Normally this is + * forbidden to prevent version rollback attacks. */ + public static final int SSL_OP_TLS_ROLLBACK_BUG = 0x00800000; + + public static final int SSL_OP_NO_SSLv2 = 0x01000000; + public static final int SSL_OP_NO_SSLv3 = 0x02000000; + public static final int SSL_OP_NO_TLSv1 = 0x04000000; + public static final int SSL_OP_NO_TLSv1_2 = 0x08000000; + public static final int SSL_OP_NO_TLSv1_1 = 0x10000000; + + public static final int SSL_OP_NO_TICKET = 0x00004000; + + public static final int SSL_OP_NETSCAPE_CA_DN_BUG = 0x20000000; + public static final int SSL_OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG = 0x40000000; + + public static final int SSL_CRT_FORMAT_UNDEF = 0; + public static final int SSL_CRT_FORMAT_ASN1 = 1; + public static final int SSL_CRT_FORMAT_TEXT = 2; + public static final int SSL_CRT_FORMAT_PEM = 3; + public static final int SSL_CRT_FORMAT_NETSCAPE = 4; + public static final int SSL_CRT_FORMAT_PKCS12 = 5; + public static final int SSL_CRT_FORMAT_SMIME = 6; + public static final int SSL_CRT_FORMAT_ENGINE = 7; + + public static final int SSL_MODE_CLIENT = 0; + public static final int SSL_MODE_SERVER = 1; + public static final int SSL_MODE_COMBINED = 2; + + public static final int SSL_CONF_FLAG_CMDLINE = 0x0001; + public static final int SSL_CONF_FLAG_FILE = 0x0002; + public static final int SSL_CONF_FLAG_CLIENT = 0x0004; + public static final int SSL_CONF_FLAG_SERVER = 0x0008; + public static final int SSL_CONF_FLAG_SHOW_ERRORS = 0x0010; + public static final int SSL_CONF_FLAG_CERTIFICATE = 0x0020; + + public static final int SSL_CONF_TYPE_UNKNOWN = 0x0000; + public static final int SSL_CONF_TYPE_STRING = 0x0001; + public static final int SSL_CONF_TYPE_FILE = 0x0002; + public static final int SSL_CONF_TYPE_DIR = 0x0003; + + public static final int SSL_SHUTDOWN_TYPE_UNSET = 0; + public static final int SSL_SHUTDOWN_TYPE_STANDARD = 1; + public static final int SSL_SHUTDOWN_TYPE_UNCLEAN = 2; + public static final int SSL_SHUTDOWN_TYPE_ACCURATE = 3; + + public static final int SSL_INFO_SESSION_ID = 0x0001; + public static final int SSL_INFO_CIPHER = 0x0002; + public static final int SSL_INFO_CIPHER_USEKEYSIZE = 0x0003; + public static final int SSL_INFO_CIPHER_ALGKEYSIZE = 0x0004; + public static final int SSL_INFO_CIPHER_VERSION = 0x0005; + public static final int SSL_INFO_CIPHER_DESCRIPTION = 0x0006; + public static final int SSL_INFO_PROTOCOL = 0x0007; + + /* To obtain the CountryName of the Client Certificate Issuer + * use the SSL_INFO_CLIENT_I_DN + SSL_INFO_DN_COUNTRYNAME + */ + public static final int SSL_INFO_CLIENT_S_DN = 0x0010; + public static final int SSL_INFO_CLIENT_I_DN = 0x0020; + public static final int SSL_INFO_SERVER_S_DN = 0x0040; + public static final int SSL_INFO_SERVER_I_DN = 0x0080; + + public static final int SSL_INFO_DN_COUNTRYNAME = 0x0001; + public static final int SSL_INFO_DN_STATEORPROVINCENAME = 0x0002; + public static final int SSL_INFO_DN_LOCALITYNAME = 0x0003; + public static final int SSL_INFO_DN_ORGANIZATIONNAME = 0x0004; + public static final int SSL_INFO_DN_ORGANIZATIONALUNITNAME = 0x0005; + public static final int SSL_INFO_DN_COMMONNAME = 0x0006; + public static final int SSL_INFO_DN_TITLE = 0x0007; + public static final int SSL_INFO_DN_INITIALS = 0x0008; + public static final int SSL_INFO_DN_GIVENNAME = 0x0009; + public static final int SSL_INFO_DN_SURNAME = 0x000A; + public static final int SSL_INFO_DN_DESCRIPTION = 0x000B; + public static final int SSL_INFO_DN_UNIQUEIDENTIFIER = 0x000C; + public static final int SSL_INFO_DN_EMAILADDRESS = 0x000D; + + public static final int SSL_INFO_CLIENT_M_VERSION = 0x0101; + public static final int SSL_INFO_CLIENT_M_SERIAL = 0x0102; + public static final int SSL_INFO_CLIENT_V_START = 0x0103; + public static final int SSL_INFO_CLIENT_V_END = 0x0104; + public static final int SSL_INFO_CLIENT_A_SIG = 0x0105; + public static final int SSL_INFO_CLIENT_A_KEY = 0x0106; + public static final int SSL_INFO_CLIENT_CERT = 0x0107; + public static final int SSL_INFO_CLIENT_V_REMAIN = 0x0108; + + public static final int SSL_INFO_SERVER_M_VERSION = 0x0201; + public static final int SSL_INFO_SERVER_M_SERIAL = 0x0202; + public static final int SSL_INFO_SERVER_V_START = 0x0203; + public static final int SSL_INFO_SERVER_V_END = 0x0204; + public static final int SSL_INFO_SERVER_A_SIG = 0x0205; + public static final int SSL_INFO_SERVER_A_KEY = 0x0206; + public static final int SSL_INFO_SERVER_CERT = 0x0207; + /* Return client certificate chain. + * Add certificate chain number to that flag (0 ... verify depth) + */ + public static final int SSL_INFO_CLIENT_CERT_CHAIN = 0x0400; + + /* Only support OFF and SERVER for now */ + public static final long SSL_SESS_CACHE_OFF = 0x0000; + public static final long SSL_SESS_CACHE_SERVER = 0x0002; + + public static final int SSL_SELECTOR_FAILURE_NO_ADVERTISE = 0; + public static final int SSL_SELECTOR_FAILURE_CHOOSE_MY_LAST_PROTOCOL = 1; + + /* Return OpenSSL version number (run time version) */ + public static native int version(); + + /* Return OpenSSL version string (run time version) */ + public static native String versionString(); + + /** + * Initialize OpenSSL support. + * This function needs to be called once for the + * lifetime of JVM. Library.init() has to be called before. + * @param engine Support for external a Crypto Device ("engine"), + * usually + * a hardware accelerator card for crypto operations. + * @return APR status code + */ + public static native int initialize(String engine); + + /** + * Get the status of FIPS Mode. + * + * @return FIPS_mode return code. It is 0 if OpenSSL is not + * in FIPS mode, 1 if OpenSSL is in FIPS Mode. + * @throws Exception If tcnative was not compiled with FIPS Mode available. + * @see OpenSSL method FIPS_mode() + */ + public static native int fipsModeGet() throws Exception; + + /** + * Enable/Disable FIPS Mode. + * + * @param mode 1 - enable, 0 - disable + * + * @return FIPS_mode_set return code + * @throws Exception If tcnative was not compiled with FIPS Mode available, + * or if {@code FIPS_mode_set()} call returned an error value. + * @see OpenSSL method FIPS_mode_set() + */ + public static native int fipsModeSet(int mode) throws Exception; + + /** + * Sets global random filename. + * + * @param filename Filename to use. + * If set it will be used for SSL initialization + * and all contexts where explicitly not set. + */ + public static native void randSet(String filename); + + /** + * Return the handshake completed count. + * @param ssl SSL pointer + * @return the count + */ + public static native int getHandshakeCount(long ssl); + + /* + * Begin Twitter API additions + */ + + public static final int SSL_SENT_SHUTDOWN = 1; + public static final int SSL_RECEIVED_SHUTDOWN = 2; + + public static final int SSL_ERROR_NONE = 0; + public static final int SSL_ERROR_SSL = 1; + public static final int SSL_ERROR_WANT_READ = 2; + public static final int SSL_ERROR_WANT_WRITE = 3; + public static final int SSL_ERROR_WANT_X509_LOOKUP = 4; + public static final int SSL_ERROR_SYSCALL = 5; /* look at error stack/return value/errno */ + public static final int SSL_ERROR_ZERO_RETURN = 6; + public static final int SSL_ERROR_WANT_CONNECT = 7; + public static final int SSL_ERROR_WANT_ACCEPT = 8; + + /** + * SSL_new + * @param ctx Server or Client context to use. + * @param server if true configure SSL instance to use accept handshake routines + * if false configure SSL instance to use connect handshake routines + * @return pointer to SSL instance (SSL *) + */ + public static native long newSSL(long ctx, boolean server); + + /** + * BIO_ctrl_pending. + * @param bio BIO pointer (BIO *) + * @return the pending bytes count + */ + public static native int pendingWrittenBytesInBIO(long bio); + + /** + * SSL_pending. + * @param ssl SSL pointer (SSL *) + * @return the pending bytes count + */ + public static native int pendingReadableBytesInSSL(long ssl); + + /** + * BIO_write. + * @param bio BIO pointer + * @param wbuf Buffer pointer + * @param wlen Write length + * @return the bytes count written + */ + public static native int writeToBIO(long bio, long wbuf, int wlen); + + /** + * BIO_read. + * @param bio BIO pointer + * @param rbuf Buffer pointer + * @param rlen Read length + * @return the bytes count read + */ + public static native int readFromBIO(long bio, long rbuf, int rlen); + + /** + * SSL_write. + * @param ssl the SSL instance (SSL *) + * @param wbuf Buffer pointer + * @param wlen Write length + * @return the bytes count written + */ + public static native int writeToSSL(long ssl, long wbuf, int wlen); + + /** + * SSL_read + * @param ssl the SSL instance (SSL *) + * @param rbuf Buffer pointer + * @param rlen Read length + * @return the bytes count read + */ + public static native int readFromSSL(long ssl, long rbuf, int rlen); + + /** + * SSL_get_shutdown + * @param ssl the SSL instance (SSL *) + * @return the operation status + */ + public static native int getShutdown(long ssl); + + /** + * SSL_free + * @param ssl the SSL instance (SSL *) + */ + public static native void freeSSL(long ssl); + + /** + * Wire up internal and network BIOs for the given SSL instance. + * + * Warning: you must explicitly free this resource by calling freeBIO + * + * While the SSL's internal/application data BIO will be freed when freeSSL is called on + * the provided SSL instance, you must call freeBIO on the returned network BIO. + * + * @param ssl the SSL instance (SSL *) + * @return pointer to the Network BIO (BIO *) + */ + public static native long makeNetworkBIO(long ssl); + + /** + * BIO_free + * @param bio BIO pointer + */ + public static native void freeBIO(long bio); + + /** + * SSL_shutdown + * @param ssl the SSL instance (SSL *) + * @return the operation status + */ + public static native int shutdownSSL(long ssl); + + /** + * Get the error number representing the last error OpenSSL encountered on + * this thread. + * @return the last error number + */ + public static native int getLastErrorNumber(); + + /** + * SSL_get_cipher. + * @param ssl the SSL instance (SSL *) + * @return the cipher name + */ + public static native String getCipherForSSL(long ssl); + + /** + * SSL_get_version + * @param ssl the SSL instance (SSL *) + * @return the SSL version in use + */ + public static native String getVersion(long ssl); + + /** + * SSL_do_handshake + * @param ssl the SSL instance (SSL *) + * @return the handshake status + */ + public static native int doHandshake(long ssl); + + /** + * SSL_renegotiate + * @param ssl the SSL instance (SSL *) + * @return the operation status + */ + public static native int renegotiate(long ssl); + + /** + * SSL_renegotiate_pending + * @param ssl the SSL instance (SSL *) + * @return the operation status + */ + public static native int renegotiatePending(long ssl); + + /** + * SSL_verify_client_post_handshake + * @param ssl the SSL instance (SSL *) + * @return the operation status + */ + public static native int verifyClientPostHandshake(long ssl); + + /** + * Is post handshake authentication in progress on this connection? + * @param ssl the SSL instance (SSL *) + * @return the operation status + */ + public static native int getPostHandshakeAuthInProgress(long ssl); + + /** + * SSL_in_init. + * @param ssl the SSL instance (SSL *) + * @return the status + */ + public static native int isInInit(long ssl); + + /* + * End Twitter API Additions + */ + + /** + * SSL_get0_alpn_selected + * @param ssl the SSL instance (SSL *) + * @return the ALPN protocol negotiated + */ + public static native String getAlpnSelected(long ssl); + + /** + * Get the peer certificate chain or {@code null} if non was send. + * @param ssl the SSL instance (SSL *) + * @return the certificate chain bytes + */ + public static native byte[][] getPeerCertChain(long ssl); + + /** + * Get the peer certificate or {@code null} if non was send. + * @param ssl the SSL instance (SSL *) + * @return the certificate bytes + */ + public static native byte[] getPeerCertificate(long ssl); + + /** + * Get the error number representing for the given {@code errorNumber}. + * @param errorNumber The error code + * @return an error message + */ + public static native String getErrorString(long errorNumber); + + /** + * SSL_get_time + * @param ssl the SSL instance (SSL *) + * @return returns the time at which the session ssl was established. The time is given in seconds since the Epoch + */ + public static native long getTime(long ssl); + + /** + * Set Type of Client Certificate verification and Maximum depth of CA Certificates + * in Client Certificate verification. + *
    + * This directive sets the Certificate verification level for the Client + * Authentication. Notice that this directive can be used both in per-server + * and per-directory context. In per-server context it applies to the client + * authentication process used in the standard SSL handshake when a connection + * is established. In per-directory context it forces an SSL renegotiation with + * the reconfigured client verification level after the HTTP request was read + * but before the HTTP response is sent. + *
    + * The following levels are available for level: + *
    +     * SSL_CVERIFY_NONE           - No client Certificate is required at all
    +     * SSL_CVERIFY_OPTIONAL       - The client may present a valid Certificate
    +     * SSL_CVERIFY_REQUIRE        - The client has to present a valid Certificate
    +     * SSL_CVERIFY_OPTIONAL_NO_CA - The client may present a valid Certificate
    +     *                              but it need not to be (successfully) verifiable
    +     * 
    + *
    + * The depth actually is the maximum number of intermediate certificate issuers, + * i.e. the number of CA certificates which are max allowed to be followed while + * verifying the client certificate. A depth of 0 means that self-signed client + * certificates are accepted only, the default depth of 1 means the client + * certificate can be self-signed or has to be signed by a CA which is directly + * known to the server (i.e. the CA's certificate is under + * {@code setCACertificatePath}, etc. + * + * @param ssl the SSL instance (SSL *) + * @param level Type of Client Certificate verification. + * @param depth Maximum depth of CA Certificates in Client Certificate + * verification. + */ + public static native void setVerify(long ssl, int level, int depth); + + /** + * Set OpenSSL Option. + * @param ssl the SSL instance (SSL *) + * @param options See SSL.SSL_OP_* for option flags. + */ + public static native void setOptions(long ssl, int options); + + /** + * Get OpenSSL Option. + * @param ssl the SSL instance (SSL *) + * @return options See SSL.SSL_OP_* for option flags. + */ + public static native int getOptions(long ssl); + + /** + * Returns all cipher suites that are enabled for negotiation in an SSL handshake. + * @param ssl the SSL instance (SSL *) + * @return ciphers + */ + public static native String[] getCiphers(long ssl); + + /** + * Returns the cipher suites available for negotiation in SSL handshake. + *
    + * This complex directive uses a colon-separated cipher-spec string consisting + * of OpenSSL cipher specifications to configure the Cipher Suite the client + * is permitted to negotiate in the SSL handshake phase. Notice that this + * directive can be used both in per-server and per-directory context. + * In per-server context it applies to the standard SSL handshake when a + * connection is established. In per-directory context it forces an SSL + * renegotiation with the reconfigured Cipher Suite after the HTTP request + * was read but before the HTTP response is sent. + * @param ssl the SSL instance (SSL *) + * @param ciphers an SSL cipher specification + * @return true if the operation was successful + * @throws Exception An error occurred + */ + public static native boolean setCipherSuites(long ssl, String ciphers) + throws Exception; + + /** + * Returns the ID of the session as byte array representation. + * + * @param ssl the SSL instance (SSL *) + * @return the session as byte array representation obtained via SSL_SESSION_get_id. + */ + public static native byte[] getSessionId(long ssl); +} diff --git a/java/org/apache/tomcat/jni/SSLConf.java b/java/org/apache/tomcat/jni/SSLConf.java new file mode 100644 index 0000000..2e429a5 --- /dev/null +++ b/java/org/apache/tomcat/jni/SSLConf.java @@ -0,0 +1,110 @@ +/* + * 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.tomcat.jni; + +public final class SSLConf { + + /** + * Create a new SSL_CONF context. + * + * @param pool The pool to use. + * @param flags The SSL_CONF flags to use. It can be any combination of + * the following: + *
    +     * {@link SSL#SSL_CONF_FLAG_CMDLINE}
    +     * {@link SSL#SSL_CONF_FLAG_FILE}
    +     * {@link SSL#SSL_CONF_FLAG_CLIENT}
    +     * {@link SSL#SSL_CONF_FLAG_SERVER}
    +     * {@link SSL#SSL_CONF_FLAG_SHOW_ERRORS}
    +     * {@link SSL#SSL_CONF_FLAG_CERTIFICATE}
    +     * 
    + * + * @return The Java representation of a pointer to the newly created + * SSL_CONF Context + * + * @throws Exception If the SSL_CONF context could not be created + * + * @see OpenSSL SSL_CONF_CTX_new + * @see OpenSSL SSL_CONF_CTX_set_flags + */ + public static native long make(long pool, int flags) throws Exception; + + /** + * Free the resources used by the context + * + * @param cctx SSL_CONF context to free. + * + * @see OpenSSL SSL_CONF_CTX_free + */ + public static native void free(long cctx); + + /** + * Check a command with an SSL_CONF context. + * + * @param cctx SSL_CONF context to use. + * @param name command name. + * @param value command value. + * + * @return The result of the check based on the {@code SSL_CONF_cmd_value_type} + * call. Unknown types will result in an exception, as well as + * file and directory types with invalid file or directory names. + * + * @throws Exception If the check fails. + * + * @see OpenSSL SSL_CONF_cmd_value_type + */ + public static native int check(long cctx, String name, String value) throws Exception; + + /** + * Assign an SSL context to an SSL_CONF context. + * All following calls to {@link #apply(long, String, String)} will be + * applied to this SSL context. + * + * @param cctx SSL_CONF context to use. + * @param ctx SSL context to assign to the given SSL_CONF context. + * + * @see OpenSSL SSL_CONF_CTX_set_ssl_ctx + */ + public static native void assign(long cctx, long ctx); + + /** + * Apply a command to an SSL_CONF context. + * + * @param cctx SSL_CONF context to use. + * @param name command name. + * @param value command value. + * + * @return The result of the native {@code SSL_CONF_cmd} call + * + * @throws Exception If the SSL_CONF context is {@code 0} + * + * @see OpenSSL SSL_CONF_cmd + */ + public static native int apply(long cctx, String name, String value) throws Exception; + + /** + * Finish commands for an SSL_CONF context. + * + * @param cctx SSL_CONF context to use. + * + * @return The result of the native {@code SSL_CONF_CTX_finish} call + * + * @see OpenSSL SSL_CONF_CTX_finish + */ + public static native int finish(long cctx); + +} diff --git a/java/org/apache/tomcat/jni/SSLContext.java b/java/org/apache/tomcat/jni/SSLContext.java new file mode 100644 index 0000000..730a38c --- /dev/null +++ b/java/org/apache/tomcat/jni/SSLContext.java @@ -0,0 +1,440 @@ +/* + * 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.tomcat.jni; + +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class SSLContext { + + public static final byte[] DEFAULT_SESSION_ID_CONTEXT = + new byte[] { 'd', 'e', 'f', 'a', 'u', 'l', 't' }; + + /** + * Create a new SSL context. + * + * @param pool The pool to use. + * @param protocol The SSL protocol to use. It can be any combination of + * the following: + *
    +     * {@link SSL#SSL_PROTOCOL_SSLV2}
    +     * {@link SSL#SSL_PROTOCOL_SSLV3}
    +     * {@link SSL#SSL_PROTOCOL_TLSV1}
    +     * {@link SSL#SSL_PROTOCOL_TLSV1_1}
    +     * {@link SSL#SSL_PROTOCOL_TLSV1_2}
    +     * {@link SSL#SSL_PROTOCOL_TLSV1_3}
    +     * {@link SSL#SSL_PROTOCOL_ALL} ( == all TLS versions, no SSL)
    +     * 
    + * @param mode SSL mode to use + *
    +     * SSL_MODE_CLIENT
    +     * SSL_MODE_SERVER
    +     * SSL_MODE_COMBINED
    +     * 
    + * + * @return The Java representation of a pointer to the newly created SSL + * Context + * + * @throws Exception If the SSL Context could not be created + */ + public static native long make(long pool, int protocol, int mode) throws Exception; + + /** + * Free the resources used by the Context + * @param ctx Server or Client context to free. + * @return APR Status code. + */ + public static native int free(long ctx); + + /** + * Set OpenSSL Option. + * @param ctx Server or Client context to use. + * @param options See SSL.SSL_OP_* for option flags. + */ + public static native void setOptions(long ctx, int options); + + /** + * Get OpenSSL Option. + * @param ctx Server or Client context to use. + * @return options See SSL.SSL_OP_* for option flags. + */ + public static native int getOptions(long ctx); + + /** + * Clears OpenSSL Options. + * @param ctx Server or Client context to use. + * @param options See SSL.SSL_OP_* for option flags. + */ + public static native void clearOptions(long ctx, int options); + + /** + * Returns all cipher suites that are enabled for negotiation in an SSL handshake. + * @param ctx Server or Client context to use. + * @return ciphers + */ + public static native String[] getCiphers(long ctx); + + /** + * Cipher Suite available for negotiation in SSL handshake. + *
    + * This complex directive uses a colon-separated cipher-spec string consisting + * of OpenSSL cipher specifications to configure the Cipher Suite the client + * is permitted to negotiate in the SSL handshake phase. Notice that this + * directive can be used both in per-server and per-directory context. + * In per-server context it applies to the standard SSL handshake when a + * connection is established. In per-directory context it forces an SSL + * renegotiation with the reconfigured Cipher Suite after the HTTP request + * was read but before the HTTP response is sent. + * @param ctx Server or Client context to use. + * @param ciphers An OpenSSL cipher specification. + * @return true if the operation was successful + * @throws Exception An error occurred + */ + public static native boolean setCipherSuite(long ctx, String ciphers) + throws Exception; + + /** + * Set File of concatenated PEM-encoded CA CRLs or + * directory of PEM-encoded CA Certificates for Client Auth + *
    + * This directive sets the all-in-one file where you can assemble the + * Certificate Revocation Lists (CRL) of Certification Authorities (CA) + * whose clients you deal with. These are used for Client Authentication. + * Such a file is simply the concatenation of the various PEM-encoded CRL + * files, in order of preference. + *
    + * The files in this directory have to be PEM-encoded and are accessed through + * hash filenames. So usually you can't just place the Certificate files there: + * you also have to create symbolic links named hash-value.N. And you should + * always make sure this directory contains the appropriate symbolic links. + * Use the Makefile which comes with mod_ssl to accomplish this task. + * @param ctx Server or Client context to use. + * @param file File of concatenated PEM-encoded CA CRLs for Client Auth. + * @param path Directory of PEM-encoded CA Certificates for Client Auth. + * @return true if the operation was successful + * @throws Exception An error occurred + */ + public static native boolean setCARevocation(long ctx, String file, + String path) + throws Exception; + + /** + * Set File of PEM-encoded Server CA Certificates + *
    + * This directive sets the optional all-in-one file where you can assemble the + * certificates of Certification Authorities (CA) which form the certificate + * chain of the server certificate. This starts with the issuing CA certificate + * of of the server certificate and can range up to the root CA certificate. + * Such a file is simply the concatenation of the various PEM-encoded CA + * Certificate files, usually in certificate chain order. + *
    + * But be careful: Providing the certificate chain works only if you are using + * a single (either RSA or DSA) based server certificate. If you are using a + * coupled RSA+DSA certificate pair, this will work only if actually both + * certificates use the same certificate chain. Else the browsers will be + * confused in this situation. + * @param ctx Server or Client context to use. + * @param file File of PEM-encoded Server CA Certificates. + * @param skipfirst Skip first certificate if chain file is inside + * certificate file. + * @return true if the operation was successful + */ + public static native boolean setCertificateChainFile(long ctx, String file, + boolean skipfirst); + + /** + * Set Certificate + *
    + * Point setCertificateFile at a PEM encoded certificate. If + * the certificate is encrypted, then you will be prompted for a + * pass phrase. Note that a kill -HUP will prompt again. A test + * certificate can be generated with 'make certificate' under + * built time. Keep in mind that if you've both a RSA and a DSA + * certificate you can configure both in parallel (to also allow + * the use of DSA ciphers, etc.) + *
    + * If the key is not combined with the certificate, use key param + * to point at the key file. Keep in mind that if + * you've both a RSA and a DSA private key you can configure + * both in parallel (to also allow the use of DSA ciphers, etc.) + * @param ctx Server or Client context to use. + * @param cert Certificate file. + * @param key Private Key file to use if not in cert. + * @param password Certificate password. If null and certificate + * is encrypted, password prompt will be displayed. + * @param idx Certificate index SSL_AIDX_RSA or SSL_AIDX_DSA. + * @return true if the operation was successful + * @throws Exception An error occurred + */ + public static native boolean setCertificate(long ctx, String cert, + String key, String password, + int idx) + throws Exception; + + /** + * Set the size of the internal session cache. + * http://www.openssl.org/docs/ssl/SSL_CTX_sess_set_cache_size.html + * @param ctx Server or Client context to use. + * @param size The cache size + * @return the value set + */ + public static native long setSessionCacheSize(long ctx, long size); + + /** + * Get the size of the internal session cache. + * http://www.openssl.org/docs/ssl/SSL_CTX_sess_get_cache_size.html + * @param ctx Server or Client context to use. + * @return the size + */ + public static native long getSessionCacheSize(long ctx); + + /** + * Set the timeout for the internal session cache in seconds. + * http://www.openssl.org/docs/ssl/SSL_CTX_set_timeout.html + * @param ctx Server or Client context to use. + * @param timeoutSeconds Timeout value + * @return the value set + */ + public static native long setSessionCacheTimeout(long ctx, long timeoutSeconds); + + /** + * Get the timeout for the internal session cache in seconds. + * http://www.openssl.org/docs/ssl/SSL_CTX_set_timeout.html + * @param ctx Server or Client context to use. + * @return the timeout + */ + public static native long getSessionCacheTimeout(long ctx); + + /** + * Set the mode of the internal session cache and return the previous used mode. + * @param ctx Server or Client context to use. + * @param mode The mode to set + * @return the value set + */ + public static native long setSessionCacheMode(long ctx, long mode); + + /** + * Get the mode of the current used internal session cache. + * @param ctx Server or Client context to use. + * @return the value set + */ + public static native long getSessionCacheMode(long ctx); + + /* + * Session resumption statistics methods. + * http://www.openssl.org/docs/ssl/SSL_CTX_sess_number.html + */ + public static native long sessionAccept(long ctx); + public static native long sessionAcceptGood(long ctx); + public static native long sessionAcceptRenegotiate(long ctx); + public static native long sessionCacheFull(long ctx); + public static native long sessionCbHits(long ctx); + public static native long sessionConnect(long ctx); + public static native long sessionConnectGood(long ctx); + public static native long sessionConnectRenegotiate(long ctx); + public static native long sessionHits(long ctx); + public static native long sessionMisses(long ctx); + public static native long sessionNumber(long ctx); + public static native long sessionTimeouts(long ctx); + + /** + * Set TLS session keys. This allows us to share keys across TFEs. + * @param ctx Server or Client context to use. + * @param keys Some session keys + */ + public static native void setSessionTicketKeys(long ctx, byte[] keys); + + /** + * Set File and Directory of concatenated PEM-encoded CA Certificates + * for Client Auth + *
    + * This directive sets the all-in-one file where you can assemble the + * Certificates of Certification Authorities (CA) whose clients you deal with. + * These are used for Client Authentication. Such a file is simply the + * concatenation of the various PEM-encoded Certificate files, in order of + * preference. This can be used alternatively and/or additionally to + * path. + *
    + * The files in this directory have to be PEM-encoded and are accessed through + * hash filenames. So usually you can't just place the Certificate files there: + * you also have to create symbolic links named hash-value.N. And you should + * always make sure this directory contains the appropriate symbolic links. + * Use the Makefile which comes with mod_ssl to accomplish this task. + * @param ctx Server or Client context to use. + * @param file File of concatenated PEM-encoded CA Certificates for + * Client Auth. + * @param path Directory of PEM-encoded CA Certificates for Client Auth. + * @return true if the operation was successful + * @throws Exception An error occurred + */ + public static native boolean setCACertificate(long ctx, String file, + String path) + throws Exception; + + /** + * Set Type of Client Certificate verification and Maximum depth of CA Certificates + * in Client Certificate verification. + *
    + * This directive sets the Certificate verification level for the Client + * Authentication. Notice that this directive can be used both in per-server + * and per-directory context. In per-server context it applies to the client + * authentication process used in the standard SSL handshake when a connection + * is established. In per-directory context it forces an SSL renegotiation with + * the reconfigured client verification level after the HTTP request was read + * but before the HTTP response is sent. + *
    + * The following levels are available for level: + *
    +     * SSL_CVERIFY_NONE           - No client Certificate is required at all
    +     * SSL_CVERIFY_OPTIONAL       - The client may present a valid Certificate
    +     * SSL_CVERIFY_REQUIRE        - The client has to present a valid Certificate
    +     * SSL_CVERIFY_OPTIONAL_NO_CA - The client may present a valid Certificate
    +     *                              but it need not to be (successfully) verifiable
    +     * 
    + *
    + * The depth actually is the maximum number of intermediate certificate issuers, + * i.e. the number of CA certificates which are max allowed to be followed while + * verifying the client certificate. A depth of 0 means that self-signed client + * certificates are accepted only, the default depth of 1 means the client + * certificate can be self-signed or has to be signed by a CA which is directly + * known to the server (i.e. the CA's certificate is under + * setCACertificatePath), etc. + * @param ctx Server or Client context to use. + * @param level Type of Client Certificate verification. + * @param depth Maximum depth of CA Certificates in Client Certificate + * verification. + */ + public static native void setVerify(long ctx, int level, int depth); + + /** + * When tc-native encounters a SNI extension in the TLS handshake it will + * call this method to determine which OpenSSL SSLContext to use for the + * connection. + * + * @param currentCtx The OpenSSL SSLContext that the handshake started to + * use. This will be the default OpenSSL SSLContext for + * the endpoint associated with the socket. + * @param sniHostName The host name requested by the client + * + * @return The Java representation of the pointer to the OpenSSL SSLContext + * to use for the given host or zero if no SSLContext could be + * identified + */ + public static long sniCallBack(long currentCtx, String sniHostName) { + SNICallBack sniCallBack = sniCallBacks.get(Long.valueOf(currentCtx)); + if (sniCallBack == null) { + return 0; + } + // Can't be sure OpenSSL is going to provide the SNI value in lower case + // so convert it before looking up the SSLContext + String hostName = (sniHostName == null) ? null : sniHostName.toLowerCase(Locale.ENGLISH); + return sniCallBack.getSslContext(hostName); + } + + /** + * A map of default SSL Contexts to SNICallBack instances (in Tomcat these + * are instances of AprEndpoint) that will be used to determine the SSL + * Context to use bases on the SNI host name. It is structured this way + * since a Tomcat instance may have several TLS enabled endpoints that each + * have different SSL Context mappings for the same host name. + */ + private static final Map sniCallBacks = new ConcurrentHashMap<>(); + + /** + * Interface implemented by components that will receive the call back to + * select an OpenSSL SSLContext based on the host name requested by the + * client. + */ + public interface SNICallBack { + + /** + * This callback is made during the TLS handshake when the client uses + * the SNI extension to request a specific TLS host. + * + * @param sniHostName The host name requested by the client - must be in + * lower case + * + * @return The Java representation of the pointer to the OpenSSL + * SSLContext to use for the given host or zero if no SSLContext + * could be identified + */ + long getSslContext(String sniHostName); + } + + /** + * Allow to hook {@link CertificateVerifier} into the handshake processing. + * This will call {@code SSL_CTX_set_cert_verify_callback} and so replace the default verification + * callback used by openssl + * @param ctx Server or Client context to use. + * @param verifier the verifier to call during handshake. + */ + public static native void setCertVerifyCallback(long ctx, CertificateVerifier verifier); + + /** + * Set application layer protocol for application layer protocol negotiation extension + * @param ctx Server context to use. + * @param alpnProtos protocols in priority order + * @param selectorFailureBehavior see {@link SSL#SSL_SELECTOR_FAILURE_NO_ADVERTISE} + * and {@link SSL#SSL_SELECTOR_FAILURE_CHOOSE_MY_LAST_PROTOCOL} + */ + public static native void setAlpnProtos(long ctx, String[] alpnProtos, int selectorFailureBehavior); + + /** + * Set the context within which session be reused (server side only) + * http://www.openssl.org/docs/ssl/SSL_CTX_set_session_id_context.html + * + * @param ctx Server context to use. + * @param sidCtx can be any kind of binary data, it is therefore possible to use e.g. the name + * of the application and/or the hostname and/or service name + * @return {@code true} if success, {@code false} otherwise. + */ + public static native boolean setSessionIdContext(long ctx, byte[] sidCtx); + + /** + * Set CertificateRaw + *
    + * Use keystore a certificate and key to fill the BIOP + * @param ctx Server or Client context to use. + * @param cert Byte array with the certificate in DER encoding. + * @param key Byte array with the Private Key file in PEM format. + * @param sslAidxRsa Certificate index SSL_AIDX_RSA or SSL_AIDX_DSA. + * @return {@code true} if success, {@code false} otherwise. + */ + public static native boolean setCertificateRaw(long ctx, byte[] cert, byte[] key, int sslAidxRsa); + + /** + * Add a certificate to the certificate chain. Certs should be added in + * order starting with the issuer of the host certs and working up the + * certificate chain to the CA. + * + *
    + * Use keystore a certificate chain to fill the BIOP + * @param ctx Server or Client context to use. + * @param cert Byte array with the certificate in DER encoding. + * @return {@code true} if success, {@code false} otherwise. + */ + public static native boolean addChainCertificateRaw(long ctx, byte[] cert); + + /** + * Add a CA certificate we accept as issuer for peer certs + * @param ctx Server or Client context to use. + * @param cert Byte array with the certificate in DER encoding. + * @return {@code true} if success, {@code false} otherwise. + */ + public static native boolean addClientCACertificateRaw(long ctx, byte[] cert); +} diff --git a/java/org/apache/tomcat/jni/Sockaddr.java b/java/org/apache/tomcat/jni/Sockaddr.java new file mode 100644 index 0000000..20e73c8 --- /dev/null +++ b/java/org/apache/tomcat/jni/Sockaddr.java @@ -0,0 +1,30 @@ +/* + * 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.tomcat.jni; + +/** + * Tomcat Native 1.2.33 and earlier won't initialise unless this class is + * present. This dummy class ensures initialisation gets as far as being able to + * check the version of the Tomcat Native library and reporting a version error + * if 1.2.33 or earlier is present. + */ +public class Sockaddr { + + private Sockaddr() { + // Hide default constructor + } +} diff --git a/java/org/apache/tomcat/util/Diagnostics.java b/java/org/apache/tomcat/util/Diagnostics.java new file mode 100644 index 0000000..77091e5 --- /dev/null +++ b/java/org/apache/tomcat/util/Diagnostics.java @@ -0,0 +1,699 @@ +/* + * 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. + */ + +// XXX TODO: Source code line length +// XXX TODO: More JavaDoc +// XXX Optional: Add support for com.sun.management specific mbean +// (http://docs.oracle.com/javase/7/docs/jre/api/management/extension/index.html) +// XXX Optional: Wire additional public static methods implemented here +// to the manager (think about manager access roles!) +// setLoggerLevel(), +// setVerboseClassLoading(), +// setThreadContentionMonitoringEnabled(), +// setThreadCpuTimeEnabled(), +// resetPeakThreadCount(), +// setVerboseGarbageCollection() +// gc(), +// resetPeakUsage(), +// setUsageThreshold(), +// setCollectionUsageThreshold() + +package org.apache.tomcat.util; + +import java.lang.management.ClassLoadingMXBean; +import java.lang.management.CompilationMXBean; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.LockInfo; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryManagerMXBean; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.lang.management.MonitorInfo; +import java.lang.management.OperatingSystemMXBean; +import java.lang.management.PlatformLoggingMXBean; +import java.lang.management.RuntimeMXBean; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class Diagnostics { + + private static final String PACKAGE = "org.apache.tomcat.util"; + private static final StringManager sm = StringManager.getManager(PACKAGE); + + private static final String INDENT1 = " "; + private static final String INDENT2 = "\t"; + private static final String INDENT3 = " "; + private static final String CRLF = "\r\n"; + private static final String vminfoSystemProperty = "java.vm.info"; + + private static final Log log = LogFactory.getLog(Diagnostics.class); + + private static final SimpleDateFormat timeformat = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + + /* Some platform MBeans */ + private static final ClassLoadingMXBean classLoadingMXBean = + ManagementFactory.getClassLoadingMXBean(); + private static final CompilationMXBean compilationMXBean = + ManagementFactory.getCompilationMXBean(); + private static final OperatingSystemMXBean operatingSystemMXBean = + ManagementFactory.getOperatingSystemMXBean(); + private static final RuntimeMXBean runtimeMXBean = + ManagementFactory.getRuntimeMXBean(); + private static final ThreadMXBean threadMXBean = + ManagementFactory.getThreadMXBean(); + + // XXX Not sure whether the following MBeans should better + // be retrieved on demand, i.e. whether they can change + // dynamically in the MBeanServer. + private static final PlatformLoggingMXBean loggingMXBean = + ManagementFactory.getPlatformMXBean(PlatformLoggingMXBean.class); + private static final MemoryMXBean memoryMXBean = + ManagementFactory.getMemoryMXBean(); + private static final List garbageCollectorMXBeans = + ManagementFactory.getGarbageCollectorMXBeans(); + private static final List memoryManagerMXBeans = + ManagementFactory.getMemoryManagerMXBeans(); + private static final List memoryPoolMXBeans = + ManagementFactory.getMemoryPoolMXBeans(); + + /** + * Check whether thread contention monitoring is enabled. + * + * @return true if thread contention monitoring is enabled + */ + public static boolean isThreadContentionMonitoringEnabled() { + return threadMXBean.isThreadContentionMonitoringEnabled(); + } + + /** + * Enable or disable thread contention monitoring via the ThreadMxMXBean. + * + * @param enable whether to enable thread contention monitoring + */ + public static void setThreadContentionMonitoringEnabled(boolean enable) { + threadMXBean.setThreadContentionMonitoringEnabled(enable); + boolean checkValue = threadMXBean.isThreadContentionMonitoringEnabled(); + if (enable != checkValue) { + log.error(sm.getString("diagnostics.setPropertyFail", "threadContentionMonitoringEnabled", + Boolean.valueOf(enable), Boolean.valueOf(checkValue))); + } + } + + /** + * Check whether thread cpu time measurement is enabled. + * + * @return true if thread cpu time measurement is enabled + */ + public static boolean isThreadCpuTimeEnabled() { + return threadMXBean.isThreadCpuTimeEnabled(); + } + + /** + * Enable or disable thread cpu time measurement via the ThreadMxMXBean. + * + * @param enable whether to enable thread cpu time measurement + */ + public static void setThreadCpuTimeEnabled(boolean enable) { + threadMXBean.setThreadCpuTimeEnabled(enable); + boolean checkValue = threadMXBean.isThreadCpuTimeEnabled(); + if (enable != checkValue) { + log.error(sm.getString("diagnostics.setPropertyFail", "threadCpuTimeEnabled", + Boolean.valueOf(enable), Boolean.valueOf(checkValue))); + } + } + + /** + * Reset peak thread count in ThreadMXBean + */ + public static void resetPeakThreadCount() { + threadMXBean.resetPeakThreadCount(); + } + + /** + * Set verbose class loading + * + * @param verbose whether to enable verbose class loading + */ + public static void setVerboseClassLoading(boolean verbose) { + classLoadingMXBean.setVerbose(verbose); + boolean checkValue = classLoadingMXBean.isVerbose(); + if (verbose != checkValue) { + log.error(sm.getString("diagnostics.setPropertyFail", "verboseClassLoading", + Boolean.valueOf(verbose), Boolean.valueOf(checkValue))); + } + } + + /** + * Set logger level + * + * @param loggerName the name of the logger + * @param levelName the level to set + */ + public static void setLoggerLevel(String loggerName, String levelName) { + loggingMXBean.setLoggerLevel(loggerName, levelName); + String checkValue = loggingMXBean.getLoggerLevel(loggerName); + if (!checkValue.equals(levelName)) { + String propertyName = "loggerLevel[" + loggerName + "]"; + log.error(sm.getString("diagnostics.setPropertyFail", propertyName, + levelName, checkValue)); + } + } + + /** + * Set verbose garbage collection logging + * + * @param verbose whether to enable verbose gc logging + */ + public static void setVerboseGarbageCollection(boolean verbose) { + memoryMXBean.setVerbose(verbose); + boolean checkValue = memoryMXBean.isVerbose(); + if (verbose != checkValue) { + log.error(sm.getString("diagnostics.setPropertyFail", "verboseGarbageCollection", + Boolean.valueOf(verbose), Boolean.valueOf(checkValue))); + } + } + + /** + * Initiate garbage collection via MX Bean + */ + public static void gc() { + memoryMXBean.gc(); + } + + /** + * Reset peak memory usage data in MemoryPoolMXBean + * + * @param name name of the MemoryPoolMXBean or "all" + */ + public static void resetPeakUsage(String name) { + for (MemoryPoolMXBean mbean: memoryPoolMXBeans) { + if (name.equals("all") || name.equals(mbean.getName())) { + mbean.resetPeakUsage(); + } + } + } + + /** + * Set usage threshold in MemoryPoolMXBean + * + * @param name name of the MemoryPoolMXBean + * @param threshold the threshold to set + * @return true if setting the threshold succeeded + */ + public static boolean setUsageThreshold(String name, long threshold) { + for (MemoryPoolMXBean mbean: memoryPoolMXBeans) { + if (name.equals(mbean.getName())) { + try { + mbean.setUsageThreshold(threshold); + return true; + } catch (IllegalArgumentException | UnsupportedOperationException ex) { + // IGNORE + } + return false; + } + } + return false; + } + + /** + * Set collection usage threshold in MemoryPoolMXBean + * + * @param name name of the MemoryPoolMXBean + * @param threshold the collection threshold to set + * @return true if setting the threshold succeeded + */ + public static boolean setCollectionUsageThreshold(String name, long threshold) { + for (MemoryPoolMXBean mbean: memoryPoolMXBeans) { + if (name.equals(mbean.getName())) { + try { + mbean.setCollectionUsageThreshold(threshold); + return true; + } catch (IllegalArgumentException | UnsupportedOperationException ex) { + // IGNORE + } + return false; + } + } + return false; + } + + /** + * Formats the thread dump header for one thread. + * + * @param ti the ThreadInfo describing the thread + * @return the formatted thread dump header + */ + private static String getThreadDumpHeader(ThreadInfo ti) { + StringBuilder sb = new StringBuilder("\"" + ti.getThreadName() + "\""); + sb.append(" Id=" + ti.getThreadId()); + sb.append(" cpu=" + threadMXBean.getThreadCpuTime(ti.getThreadId()) + + " ns"); + sb.append(" usr=" + threadMXBean.getThreadUserTime(ti.getThreadId()) + + " ns"); + sb.append(" blocked " + ti.getBlockedCount() + " for " + + ti.getBlockedTime() + " ms"); + sb.append(" waited " + ti.getWaitedCount() + " for " + + ti.getWaitedTime() + " ms"); + + if (ti.isSuspended()) { + sb.append(" (suspended)"); + } + if (ti.isInNative()) { + sb.append(" (running in native)"); + } + sb.append(CRLF); + sb.append(INDENT3 + "java.lang.Thread.State: " + ti.getThreadState()); + sb.append(CRLF); + return sb.toString(); + } + + /** + * Formats the thread dump for one thread. + * + * @param ti the ThreadInfo describing the thread + * @return the formatted thread dump + */ + private static String getThreadDump(ThreadInfo ti) { + StringBuilder sb = new StringBuilder(getThreadDumpHeader(ti)); + for (LockInfo li : ti.getLockedSynchronizers()) { + sb.append(INDENT2 + "locks " + + li.toString() + CRLF); + } + boolean start = true; + StackTraceElement[] stes = ti.getStackTrace(); + Object[] monitorDepths = new Object[stes.length]; + MonitorInfo[] mis = ti.getLockedMonitors(); + for (MonitorInfo monitorInfo : mis) { + monitorDepths[monitorInfo.getLockedStackDepth()] = monitorInfo; + } + for (int i = 0; i < stes.length; i++) { + StackTraceElement ste = stes[i]; + sb.append(INDENT2 + + "at " + ste.toString() + CRLF); + if (start) { + if (ti.getLockName() != null) { + sb.append(INDENT2 + "- waiting on (a " + + ti.getLockName() + ")"); + if (ti.getLockOwnerName() != null) { + sb.append(" owned by " + ti.getLockOwnerName() + + " Id=" + ti.getLockOwnerId()); + } + sb.append(CRLF); + } + start = false; + } + if (monitorDepths[i] != null) { + MonitorInfo mi = (MonitorInfo)monitorDepths[i]; + sb.append(INDENT2 + + "- locked (a " + mi.toString() + ")"+ + " index " + mi.getLockedStackDepth() + + " frame " + mi.getLockedStackFrame().toString()); + sb.append(CRLF); + + } + } + return sb.toString(); + } + + /** + * Formats the thread dump for a list of threads. + * + * @param tinfos the ThreadInfo array describing the thread list + * @return the formatted thread dump + */ + private static String getThreadDump(ThreadInfo[] tinfos) { + StringBuilder sb = new StringBuilder(); + for (ThreadInfo tinfo : tinfos) { + sb.append(getThreadDump(tinfo)); + sb.append(CRLF); + } + return sb.toString(); + } + + /** + * Check if any threads are deadlocked. If any, print + * the thread dump for those threads. + * + * @return a deadlock message and the formatted thread dump + * of the deadlocked threads + */ + public static String findDeadlock() { + ThreadInfo[] tinfos = null; + long[] ids = threadMXBean.findDeadlockedThreads(); + if (ids != null) { + tinfos = threadMXBean.getThreadInfo(threadMXBean.findDeadlockedThreads(), + true, true); + if (tinfos != null) { + StringBuilder sb = + new StringBuilder(sm.getString("diagnostics.deadlockFound")); + sb.append(CRLF); + sb.append(getThreadDump(tinfos)); + return sb.toString(); + } + } + return ""; + } + + /** + * Retrieves a formatted JVM thread dump. + * The default StringManager will be used. + * + * @return the formatted JVM thread dump + */ + public static String getThreadDump() { + return getThreadDump(sm); + } + + /** + * Retrieves a formatted JVM thread dump. + * The given list of locales will be used + * to retrieve a StringManager. + * + * @param requestedLocales list of locales to use + * @return the formatted JVM thread dump + */ + public static String getThreadDump(Enumeration requestedLocales) { + return getThreadDump( + StringManager.getManager(PACKAGE, requestedLocales)); + } + + /** + * Retrieve a JVM thread dump formatted + * using the given StringManager. + * + * @param requestedSm the StringManager to use + * @return the formatted JVM thread dump + */ + public static String getThreadDump(StringManager requestedSm) { + StringBuilder sb = new StringBuilder(); + + synchronized(timeformat) { + sb.append(timeformat.format(new Date())); + } + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.threadDumpTitle")); + sb.append(' '); + sb.append(runtimeMXBean.getVmName()); + sb.append(" ("); + sb.append(runtimeMXBean.getVmVersion()); + String vminfo = System.getProperty(vminfoSystemProperty); + if (vminfo != null) { + sb.append(" " + vminfo); + } + sb.append("):" + CRLF); + sb.append(CRLF); + + ThreadInfo[] tis = threadMXBean.dumpAllThreads(true, true); + sb.append(getThreadDump(tis)); + + sb.append(findDeadlock()); + return sb.toString(); + } + + /** + * Format contents of a MemoryUsage object. + * @param name a text prefix used in formatting + * @param usage the MemoryUsage object to format + * @return the formatted contents + */ + private static String formatMemoryUsage(String name, MemoryUsage usage) { + if (usage != null) { + StringBuilder sb = new StringBuilder(); + sb.append(INDENT1 + name + " init: " + usage.getInit() + CRLF); + sb.append(INDENT1 + name + " used: " + usage.getUsed() + CRLF); + sb.append(INDENT1 + name + " committed: " + usage.getCommitted() + CRLF); + sb.append(INDENT1 + name + " max: " + usage.getMax() + CRLF); + return sb.toString(); + } + return ""; + } + + /** + * Retrieves a formatted JVM information text. + * The default StringManager will be used. + * + * @return the formatted JVM information text + */ + public static String getVMInfo() { + return getVMInfo(sm); + } + + /** + * Retrieves a formatted JVM information text. + * The given list of locales will be used + * to retrieve a StringManager. + * + * @param requestedLocales list of locales to use + * @return the formatted JVM information text + */ + public static String getVMInfo(Enumeration requestedLocales) { + return getVMInfo(StringManager.getManager(PACKAGE, requestedLocales)); + } + + /** + * Retrieve a JVM information text formatted + * using the given StringManager. + * + * @param requestedSm the StringManager to use + * @return the formatted JVM information text + */ + public static String getVMInfo(StringManager requestedSm) { + StringBuilder sb = new StringBuilder(); + + synchronized(timeformat) { + sb.append(timeformat.format(new Date())); + } + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoRuntime")); + sb.append(":" + CRLF); + sb.append(INDENT1 + "vmName: " + runtimeMXBean.getVmName() + CRLF); + sb.append(INDENT1 + "vmVersion: " + runtimeMXBean.getVmVersion() + CRLF); + sb.append(INDENT1 + "vmVendor: " + runtimeMXBean.getVmVendor() + CRLF); + sb.append(INDENT1 + "specName: " + runtimeMXBean.getSpecName() + CRLF); + sb.append(INDENT1 + "specVersion: " + runtimeMXBean.getSpecVersion() + CRLF); + sb.append(INDENT1 + "specVendor: " + runtimeMXBean.getSpecVendor() + CRLF); + sb.append(INDENT1 + "managementSpecVersion: " + + runtimeMXBean.getManagementSpecVersion() + CRLF); + sb.append(INDENT1 + "name: " + runtimeMXBean.getName() + CRLF); + sb.append(INDENT1 + "startTime: " + runtimeMXBean.getStartTime() + CRLF); + sb.append(INDENT1 + "uptime: " + runtimeMXBean.getUptime() + CRLF); + sb.append(INDENT1 + "isBootClassPathSupported: " + + runtimeMXBean.isBootClassPathSupported() + CRLF); + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoOs")); + sb.append(":" + CRLF); + sb.append(INDENT1 + "name: " + operatingSystemMXBean.getName() + CRLF); + sb.append(INDENT1 + "version: " + operatingSystemMXBean.getVersion() + CRLF); + sb.append(INDENT1 + "architecture: " + operatingSystemMXBean.getArch() + CRLF); + sb.append(INDENT1 + "availableProcessors: " + + operatingSystemMXBean.getAvailableProcessors() + CRLF); + sb.append(INDENT1 + "systemLoadAverage: " + + operatingSystemMXBean.getSystemLoadAverage() + CRLF); + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoThreadMxBean")); + sb.append(":" + CRLF); + sb.append(INDENT1 + "isCurrentThreadCpuTimeSupported: " + + threadMXBean.isCurrentThreadCpuTimeSupported() + CRLF); + sb.append(INDENT1 + "isThreadCpuTimeSupported: " + + threadMXBean.isThreadCpuTimeSupported() + CRLF); + sb.append(INDENT1 + "isThreadCpuTimeEnabled: " + + threadMXBean.isThreadCpuTimeEnabled() + CRLF); + sb.append(INDENT1 + "isObjectMonitorUsageSupported: " + + threadMXBean.isObjectMonitorUsageSupported() + CRLF); + sb.append(INDENT1 + "isSynchronizerUsageSupported: " + + threadMXBean.isSynchronizerUsageSupported() + CRLF); + sb.append(INDENT1 + "isThreadContentionMonitoringSupported: " + + threadMXBean.isThreadContentionMonitoringSupported() + CRLF); + sb.append(INDENT1 + "isThreadContentionMonitoringEnabled: " + + threadMXBean.isThreadContentionMonitoringEnabled() + CRLF); + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoThreadCounts")); + sb.append(":" + CRLF); + sb.append(INDENT1 + "daemon: " + threadMXBean.getDaemonThreadCount() + CRLF); + sb.append(INDENT1 + "total: " + threadMXBean.getThreadCount() + CRLF); + sb.append(INDENT1 + "peak: " + threadMXBean.getPeakThreadCount() + CRLF); + sb.append(INDENT1 + "totalStarted: " + + threadMXBean.getTotalStartedThreadCount() + CRLF); + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoStartup")); + sb.append(":" + CRLF); + for (String arg: runtimeMXBean.getInputArguments()) { + sb.append(INDENT1 + arg + CRLF); + } + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoPath")); + sb.append(":" + CRLF); + if (runtimeMXBean.isBootClassPathSupported()) { + sb.append(INDENT1 + "bootClassPath: " + runtimeMXBean.getBootClassPath() + CRLF); + } + sb.append(INDENT1 + "classPath: " + runtimeMXBean.getClassPath() + CRLF); + sb.append(INDENT1 + "libraryPath: " + runtimeMXBean.getLibraryPath() + CRLF); + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoClassLoading")); + sb.append(":" + CRLF); + sb.append(INDENT1 + "loaded: " + + classLoadingMXBean.getLoadedClassCount() + CRLF); + sb.append(INDENT1 + "unloaded: " + + classLoadingMXBean.getUnloadedClassCount() + CRLF); + sb.append(INDENT1 + "totalLoaded: " + + classLoadingMXBean.getTotalLoadedClassCount() + CRLF); + sb.append(INDENT1 + "isVerbose: " + + classLoadingMXBean.isVerbose() + CRLF); + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoClassCompilation")); + sb.append(":" + CRLF); + sb.append(INDENT1 + "name: " + compilationMXBean.getName() + CRLF); + sb.append(INDENT1 + "totalCompilationTime: " + + compilationMXBean.getTotalCompilationTime() + CRLF); + sb.append(INDENT1 + "isCompilationTimeMonitoringSupported: " + + compilationMXBean.isCompilationTimeMonitoringSupported() + CRLF); + sb.append(CRLF); + + for (MemoryManagerMXBean mbean: memoryManagerMXBeans) { + sb.append(requestedSm.getString("diagnostics.vmInfoMemoryManagers", mbean.getName())); + sb.append(":" + CRLF); + sb.append(INDENT1 + "isValid: " + mbean.isValid() + CRLF); + sb.append(INDENT1 + "mbean.getMemoryPoolNames: " + CRLF); + String[] names = mbean.getMemoryPoolNames(); + Arrays.sort(names); + for (String name: names) { + sb.append(INDENT2 + name + CRLF); + } + sb.append(CRLF); + } + + for (GarbageCollectorMXBean mbean: garbageCollectorMXBeans) { + sb.append(requestedSm.getString("diagnostics.vmInfoGarbageCollectors", mbean.getName())); + sb.append(":" + CRLF); + sb.append(INDENT1 + "isValid: " + mbean.isValid() + CRLF); + sb.append(INDENT1 + "mbean.getMemoryPoolNames: " + CRLF); + String[] names = mbean.getMemoryPoolNames(); + Arrays.sort(names); + for (String name: names) { + sb.append(INDENT2 + name + CRLF); + } + sb.append(INDENT1 + "getCollectionCount: " + mbean.getCollectionCount() + CRLF); + sb.append(INDENT1 + "getCollectionTime: " + mbean.getCollectionTime() + CRLF); + sb.append(CRLF); + } + + sb.append(requestedSm.getString("diagnostics.vmInfoMemory")); + sb.append(":" + CRLF); + sb.append(INDENT1 + "isVerbose: " + memoryMXBean.isVerbose() + CRLF); + sb.append(INDENT1 + "getObjectPendingFinalizationCount: " + memoryMXBean.getObjectPendingFinalizationCount() + CRLF); + sb.append(formatMemoryUsage("heap", memoryMXBean.getHeapMemoryUsage())); + sb.append(formatMemoryUsage("non-heap", memoryMXBean.getNonHeapMemoryUsage())); + sb.append(CRLF); + + for (MemoryPoolMXBean mbean: memoryPoolMXBeans) { + sb.append(requestedSm.getString("diagnostics.vmInfoMemoryPools", mbean.getName())); + sb.append(":" + CRLF); + sb.append(INDENT1 + "isValid: " + mbean.isValid() + CRLF); + sb.append(INDENT1 + "getType: " + mbean.getType() + CRLF); + sb.append(INDENT1 + "mbean.getMemoryManagerNames: " + CRLF); + String[] names = mbean.getMemoryManagerNames(); + Arrays.sort(names); + for (String name: names) { + sb.append(INDENT2 + name + CRLF); + } + sb.append(INDENT1 + "isUsageThresholdSupported: " + mbean.isUsageThresholdSupported() + CRLF); + try { + sb.append(INDENT1 + "isUsageThresholdExceeded: " + mbean.isUsageThresholdExceeded() + CRLF); + } catch (UnsupportedOperationException ex) { + // IGNORE + } + sb.append(INDENT1 + "isCollectionUsageThresholdSupported: " + mbean.isCollectionUsageThresholdSupported() + CRLF); + try { + sb.append(INDENT1 + "isCollectionUsageThresholdExceeded: " + mbean.isCollectionUsageThresholdExceeded() + CRLF); + } catch (UnsupportedOperationException ex) { + // IGNORE + } + try { + sb.append(INDENT1 + "getUsageThreshold: " + mbean.getUsageThreshold() + CRLF); + } catch (UnsupportedOperationException ex) { + // IGNORE + } + try { + sb.append(INDENT1 + "getUsageThresholdCount: " + mbean.getUsageThresholdCount() + CRLF); + } catch (UnsupportedOperationException ex) { + // IGNORE + } + try { + sb.append(INDENT1 + "getCollectionUsageThreshold: " + mbean.getCollectionUsageThreshold() + CRLF); + } catch (UnsupportedOperationException ex) { + // IGNORE + } + try { + sb.append(INDENT1 + "getCollectionUsageThresholdCount: " + mbean.getCollectionUsageThresholdCount() + CRLF); + } catch (UnsupportedOperationException ex) { + // IGNORE + } + sb.append(formatMemoryUsage("current", mbean.getUsage())); + sb.append(formatMemoryUsage("collection", mbean.getCollectionUsage())); + sb.append(formatMemoryUsage("peak", mbean.getPeakUsage())); + sb.append(CRLF); + } + + + sb.append(requestedSm.getString("diagnostics.vmInfoSystem")); + sb.append(":" + CRLF); + Map props = runtimeMXBean.getSystemProperties(); + ArrayList keys = new ArrayList<>(props.keySet()); + Collections.sort(keys); + for (String prop: keys) { + sb.append(INDENT1 + prop + ": " + props.get(prop) + CRLF); + } + sb.append(CRLF); + + sb.append(requestedSm.getString("diagnostics.vmInfoLogger")); + sb.append(":" + CRLF); + List loggers = loggingMXBean.getLoggerNames(); + Collections.sort(loggers); + for (String logger: loggers) { + sb.append(INDENT1 + logger + + ": level=" + loggingMXBean.getLoggerLevel(logger) + + ", parent=" + loggingMXBean.getParentLoggerName(logger) + CRLF); + } + sb.append(CRLF); + + return sb.toString(); + } +} diff --git a/java/org/apache/tomcat/util/ExceptionUtils.java b/java/org/apache/tomcat/util/ExceptionUtils.java new file mode 100644 index 0000000..3df395f --- /dev/null +++ b/java/org/apache/tomcat/util/ExceptionUtils.java @@ -0,0 +1,71 @@ +/* + * 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.tomcat.util; + +import java.lang.reflect.InvocationTargetException; + + +/** + * Utilities for handling Throwables and Exceptions. + */ +public class ExceptionUtils { + + /** + * Checks whether the supplied Throwable is one that needs to be + * rethrown and swallows all others. + * @param t the Throwable to check + */ + public static void handleThrowable(Throwable t) { + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof StackOverflowError) { + // Swallow silently - it should be recoverable + return; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + // All other instances of Throwable will be silently swallowed + } + + /** + * Checks whether the supplied Throwable is an instance of + * InvocationTargetException and returns the throwable that is + * wrapped by it, if there is any. + * + * @param t the Throwable to check + * @return t or t.getCause() + */ + public static Throwable unwrapInvocationTargetException(Throwable t) { + if (t instanceof InvocationTargetException && t.getCause() != null) { + return t.getCause(); + } + return t; + } + + + /** + * NO-OP method provided to enable simple pre-loading of this class. Since + * the class is used extensively in error handling, it is prudent to + * pre-load it to avoid any failure to load this class masking the true + * problem during error handling. + */ + public static void preload() { + // NO-OP + } +} diff --git a/java/org/apache/tomcat/util/IntrospectionUtils.java b/java/org/apache/tomcat/util/IntrospectionUtils.java new file mode 100644 index 0000000..89a459f --- /dev/null +++ b/java/org/apache/tomcat/util/IntrospectionUtils.java @@ -0,0 +1,622 @@ +/* + * 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.tomcat.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.PermissionCheck; + +/** + * Utils for introspection and reflection + */ +public final class IntrospectionUtils { + + private static final Log log = LogFactory.getLog(IntrospectionUtils.class); + private static final StringManager sm = StringManager.getManager(IntrospectionUtils.class); + + /** + * Find a method with the right name If found, call the method ( if param is + * int or boolean we'll convert value to the right type before) - that means + * you can have setDebug(1). + * @param o The object to set a property on + * @param name The property name + * @param value The property value + * @return true if operation was successful + */ + public static boolean setProperty(Object o, String name, String value) { + return setProperty(o, name, value, true, null); + } + + public static boolean setProperty(Object o, String name, String value, + boolean invokeSetProperty) { + return setProperty(o, name, value, invokeSetProperty, null); + } + + @SuppressWarnings("null") // setPropertyMethodVoid is not null when used + public static boolean setProperty(Object o, String name, String value, + boolean invokeSetProperty, StringBuilder actualMethod) { + if (log.isTraceEnabled()) { + log.trace("IntrospectionUtils: setProperty(" + + o.getClass() + " " + name + "=" + value + ")"); + } + + if (actualMethod == null && XReflectionIntrospectionUtils.isEnabled()) { + return XReflectionIntrospectionUtils.setPropertyInternal(o, name, value, invokeSetProperty); + } + + String setter = "set" + capitalize(name); + + try { + Method methods[] = findMethods(o.getClass()); + Method setPropertyMethodVoid = null; + Method setPropertyMethodBool = null; + + // First, the ideal case - a setFoo( String ) method + for (Method item : methods) { + Class paramT[] = item.getParameterTypes(); + if (setter.equals(item.getName()) && paramT.length == 1 + && "java.lang.String".equals(paramT[0].getName())) { + item.invoke(o, new Object[]{value}); + if (actualMethod != null) { + actualMethod.append(item.getName()).append("(\"").append(escape(value)).append("\")"); + } + return true; + } + } + + // Try a setFoo ( int ) or ( boolean ) + for (Method method : methods) { + boolean ok = true; + if (setter.equals(method.getName()) + && method.getParameterTypes().length == 1) { + + // match - find the type and invoke it + Class paramType = method.getParameterTypes()[0]; + Object params[] = new Object[1]; + + // Try a setFoo ( int ) + if ("java.lang.Integer".equals(paramType.getName()) + || "int".equals(paramType.getName())) { + try { + params[0] = Integer.valueOf(value); + } catch (NumberFormatException ex) { + ok = false; + } + if (actualMethod != null) { + actualMethod.append(method.getName()).append("(Integer.valueOf(\"").append(value).append("\"))"); + } + // Try a setFoo ( long ) + } else if ("java.lang.Long".equals(paramType.getName()) + || "long".equals(paramType.getName())) { + try { + params[0] = Long.valueOf(value); + } catch (NumberFormatException ex) { + ok = false; + } + if (actualMethod != null) { + actualMethod.append(method.getName()).append("(Long.valueOf(\"").append(value).append("\"))"); + } + // Try a setFoo ( boolean ) + } else if ("java.lang.Boolean".equals(paramType.getName()) + || "boolean".equals(paramType.getName())) { + params[0] = Boolean.valueOf(value); + if (actualMethod != null) { + actualMethod.append(method.getName()).append("(Boolean.valueOf(\"").append(value).append("\"))"); + } + // Try a setFoo ( InetAddress ) + } else if ("java.net.InetAddress".equals(paramType + .getName())) { + try { + params[0] = InetAddress.getByName(value); + } catch (UnknownHostException exc) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("introspectionUtils.hostResolutionFail", value)); + } + ok = false; + } + if (actualMethod != null) { + actualMethod.append(method.getName()).append("(InetAddress.getByName(\"").append(value).append("\"))"); + } + // Unknown type + } else { + if (log.isTraceEnabled()) { + log.trace("IntrospectionUtils: Unknown type " + + paramType.getName()); + } + } + + if (ok) { + method.invoke(o, params); + return true; + } + } + + // save "setProperty" for later + if ("setProperty".equals(method.getName())) { + if (method.getReturnType() == Boolean.TYPE) { + setPropertyMethodBool = method; + } else { + setPropertyMethodVoid = method; + } + + } + } + + // Ok, no setXXX found, try a setProperty("name", "value") + if (invokeSetProperty && (setPropertyMethodBool != null || + setPropertyMethodVoid != null)) { + if (actualMethod != null) { + actualMethod.append("setProperty(\"").append(name).append("\", \"").append(escape(value)).append("\")"); + } + Object params[] = new Object[2]; + params[0] = name; + params[1] = value; + if (setPropertyMethodBool != null) { + try { + return ((Boolean) setPropertyMethodBool.invoke(o, + params)).booleanValue(); + }catch (IllegalArgumentException biae) { + //the boolean method had the wrong + //parameter types. lets try the other + if (setPropertyMethodVoid!=null) { + setPropertyMethodVoid.invoke(o, params); + return true; + }else { + throw biae; + } + } + } else { + setPropertyMethodVoid.invoke(o, params); + return true; + } + } + + } catch (IllegalArgumentException | SecurityException | IllegalAccessException e) { + log.warn(sm.getString("introspectionUtils.setPropertyError", name, value, o.getClass()), e); + } catch (InvocationTargetException e) { + ExceptionUtils.handleThrowable(e.getCause()); + log.warn(sm.getString("introspectionUtils.setPropertyError", name, value, o.getClass()), e); + } + return false; + } + + /** + * @param s + * the input string + * @return escaped string, per Java rule + */ + public static String escape(String s) { + + if (s == null) { + return ""; + } + + StringBuilder b = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '"') { + b.append('\\').append('"'); + } else if (c == '\\') { + b.append('\\').append('\\'); + } else if (c == '\n') { + b.append('\\').append('n'); + } else if (c == '\r') { + b.append('\\').append('r'); + } else { + b.append(c); + } + } + return b.toString(); + } + + public static Object getProperty(Object o, String name) { + if (XReflectionIntrospectionUtils.isEnabled()) { + return XReflectionIntrospectionUtils.getPropertyInternal(o, name); + } + String getter = "get" + capitalize(name); + String isGetter = "is" + capitalize(name); + + try { + Method methods[] = findMethods(o.getClass()); + Method getPropertyMethod = null; + + // First, the ideal case - a getFoo() method + for (Method method : methods) { + Class paramT[] = method.getParameterTypes(); + if (getter.equals(method.getName()) && paramT.length == 0) { + return method.invoke(o, (Object[]) null); + } + if (isGetter.equals(method.getName()) && paramT.length == 0) { + return method.invoke(o, (Object[]) null); + } + + if ("getProperty".equals(method.getName())) { + getPropertyMethod = method; + } + } + + // Ok, no setXXX found, try a getProperty("name") + if (getPropertyMethod != null) { + Object params[] = new Object[1]; + params[0] = name; + return getPropertyMethod.invoke(o, params); + } + + } catch (IllegalArgumentException | SecurityException | IllegalAccessException e) { + log.warn(sm.getString("introspectionUtils.getPropertyError", name, o.getClass()), e); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof NullPointerException) { + // Assume the underlying object uses a storage to represent an unset property + return null; + } + ExceptionUtils.handleThrowable(e.getCause()); + log.warn(sm.getString("introspectionUtils.getPropertyError", name, o.getClass()), e); + } + return null; + } + + /** + * Replaces ${NAME} in the value with the value of the property 'NAME'. + * Replaces ${NAME:DEFAULT} with the value of the property 'NAME:DEFAULT', + * if the property 'NAME:DEFAULT' is not set, + * the expression is replaced with the value of the property 'NAME', + * if the property 'NAME' is not set, + * the expression is replaced with 'DEFAULT'. + * If the property is not set and there is no default the value will be + * returned unmodified. + * + * @param value The value + * @param staticProp Replacement properties + * @param dynamicProp Replacement properties + * @param classLoader Class loader associated with the code requesting the + * property + * + * @return the replacement value + */ + public static String replaceProperties(String value, + Hashtable staticProp, PropertySource dynamicProp[], + ClassLoader classLoader) { + return replaceProperties(value, staticProp, dynamicProp, classLoader, 0); + } + + private static String replaceProperties(String value, + Hashtable staticProp, PropertySource dynamicProp[], + ClassLoader classLoader, int iterationCount) { + if (value == null || value.indexOf("${") < 0) { + return value; + } + if (iterationCount >=20) { + log.warn(sm.getString("introspectionUtils.tooManyIterations", value)); + return value; + } + StringBuilder sb = new StringBuilder(); + int prev = 0; + // assert value!=nil + int pos; + while ((pos = value.indexOf('$', prev)) >= 0) { + if (pos > 0) { + sb.append(value.substring(prev, pos)); + } + if (pos == (value.length() - 1)) { + sb.append('$'); + prev = pos + 1; + } else if (value.charAt(pos + 1) != '{') { + sb.append('$'); + prev = pos + 1; // XXX + } else { + int endName = value.indexOf('}', pos); + if (endName < 0) { + sb.append(value.substring(pos)); + prev = value.length(); + continue; + } + String n = value.substring(pos + 2, endName); + String v = getProperty(n, staticProp, dynamicProp, classLoader); + if (v == null) { + // {name:default} + int col = n.indexOf(":-"); + if (col != -1) { + String dV = n.substring(col + 2); + n = n.substring(0, col); + v = getProperty(n, staticProp, dynamicProp, classLoader); + if (v == null) { + v = dV; + } + } else { + v = "${" + n + "}"; + } + } + sb.append(v); + prev = endName + 1; + } + } + if (prev < value.length()) { + sb.append(value.substring(prev)); + } + String newval = sb.toString(); + if (newval.indexOf("${") < 0) { + return newval; + } + if (newval.equals(value)) { + return value; + } + if (log.isTraceEnabled()) { + log.trace("IntrospectionUtils.replaceProperties iter on: " + newval); + } + return replaceProperties(newval, staticProp, dynamicProp, classLoader, iterationCount+1); + } + + private static String getProperty(String name, Hashtable staticProp, + PropertySource[] dynamicProp, ClassLoader classLoader) { + String v = null; + if (staticProp != null) { + v = (String) staticProp.get(name); + } + if (v == null && dynamicProp != null) { + for (PropertySource propertySource : dynamicProp) { + if (propertySource instanceof SecurePropertySource) { + v = ((SecurePropertySource) propertySource).getProperty(name, classLoader); + } else { + v = propertySource.getProperty(name); + } + if (v != null) { + break; + } + } + } + return v; + } + + /** + * Reverse of Introspector.decapitalize. + * @param name The name + * @return the capitalized string + */ + public static String capitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + char chars[] = name.toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + return new String(chars); + } + + // -------------------- other utils -------------------- + public static void clear() { + objectMethods.clear(); + } + + private static final Map,Method[]> objectMethods = new ConcurrentHashMap<>(); + + public static Method[] findMethods(Class c) { + Method methods[] = objectMethods.get(c); + if (methods != null) { + return methods; + } + + methods = c.getMethods(); + objectMethods.put(c, methods); + return methods; + } + + public static Method findMethod(Class c, String name, + Class params[]) { + Method methods[] = findMethods(c); + for (Method method : methods) { + if (method.getName().equals(name)) { + Class methodParams[] = method.getParameterTypes(); + if (params == null) { + if (methodParams.length == 0) { + return method; + } else { + continue; + } + } + if (params.length != methodParams.length) { + continue; + } + boolean found = true; + for (int j = 0; j < params.length; j++) { + if (params[j] != methodParams[j]) { + found = false; + break; + } + } + if (found) { + return method; + } + } + } + return null; + } + + public static Object callMethod1(Object target, String methodN, + Object param1, String typeParam1, ClassLoader cl) throws Exception { + if (target == null || methodN == null || param1 == null) { + throw new IllegalArgumentException(sm.getString("introspectionUtils.nullParameter")); + } + if (log.isTraceEnabled()) { + log.trace("IntrospectionUtils: callMethod1 " + + target.getClass().getName() + " " + + param1.getClass().getName() + " " + typeParam1); + } + + Class params[] = new Class[1]; + if (typeParam1 == null) { + params[0] = param1.getClass(); + } else { + params[0] = cl.loadClass(typeParam1); + } + Method m = findMethod(target.getClass(), methodN, params); + if (m == null) { + throw new NoSuchMethodException(sm.getString("introspectionUtils.noMethod", methodN, target, target.getClass())); + } + try { + return m.invoke(target, new Object[] { param1 }); + } catch (InvocationTargetException ie) { + ExceptionUtils.handleThrowable(ie.getCause()); + throw ie; + } + } + + public static Object callMethodN(Object target, String methodN, + Object params[], Class typeParams[]) throws Exception { + Method m = null; + m = findMethod(target.getClass(), methodN, typeParams); + if (m == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("introspectionUtils.noMethod", methodN, target, target.getClass())); + } + return null; + } + try { + Object o = m.invoke(target, params); + + if (log.isTraceEnabled()) { + StringBuilder sb = new StringBuilder(); + sb.append(target.getClass().getName()).append('.').append(methodN).append('('); + for (int i = 0; i < params.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(params[i]); + } + sb.append(')'); + log.trace("IntrospectionUtils:" + sb.toString()); + } + return o; + } catch (InvocationTargetException ie) { + ExceptionUtils.handleThrowable(ie.getCause()); + throw ie; + } + } + + public static Object convert(String object, Class paramType) { + Object result = null; + if ("java.lang.String".equals(paramType.getName())) { + result = object; + } else if ("java.lang.Integer".equals(paramType.getName()) + || "int".equals(paramType.getName())) { + try { + result = Integer.valueOf(object); + } catch (NumberFormatException ex) { + } + // Try a setFoo ( boolean ) + } else if ("java.lang.Boolean".equals(paramType.getName()) + || "boolean".equals(paramType.getName())) { + result = Boolean.valueOf(object); + + // Try a setFoo ( InetAddress ) + } else if ("java.net.InetAddress".equals(paramType + .getName())) { + try { + result = InetAddress.getByName(object); + } catch (UnknownHostException exc) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("introspectionUtils.hostResolutionFail", object)); + } + } + + // Unknown type + } else { + if (log.isTraceEnabled()) { + log.trace("IntrospectionUtils: Unknown type " + + paramType.getName()); + } + } + if (result == null) { + throw new IllegalArgumentException(sm.getString("introspectionUtils.conversionError", object, paramType.getName())); + } + return result; + } + + + /** + * Checks to see if the specified class is an instance of or assignable from + * the specified type. The class clazz, all its superclasses, + * interfaces and those superinterfaces are tested for a match against + * the type name type. + * + * This is similar to instanceof or {@link Class#isAssignableFrom} + * except that the target type will not be resolved into a Class + * object, which provides some security and memory benefits. + * + * @param clazz The class to test for a match. + * @param type The name of the type that clazz must be. + * + * @return true if the clazz tested is an + * instance of the specified type, + * false otherwise. + */ + public static boolean isInstance(Class clazz, String type) { + if (type.equals(clazz.getName())) { + return true; + } + + Class[] ifaces = clazz.getInterfaces(); + for (Class iface : ifaces) { + if (isInstance(iface, type)) { + return true; + } + } + + Class superClazz = clazz.getSuperclass(); + if (superClazz == null) { + return false; + } else { + return isInstance(superClazz, type); + } + } + + + // -------------------- Get property -------------------- + // This provides a layer of abstraction + + public interface PropertySource { + String getProperty(String key); + } + + + public interface SecurePropertySource extends PropertySource { + + /** + * Obtain a property value, checking that code associated with the + * provided class loader has permission to access the property. If the + * {@code classLoader} is {@code null} or if {@code classLoader} does + * not implement {@link PermissionCheck} then the property value will be + * looked up without a call to + * {@link PermissionCheck#check(java.security.Permission)} + * + * @param key The key of the requested property + * @param classLoader The class loader associated with the code that + * trigger the property lookup + * @return The property value or {@code null} if it could not be found + * or if {@link PermissionCheck#check(java.security.Permission)} + * fails + */ + String getProperty(String key, ClassLoader classLoader); + } +} diff --git a/java/org/apache/tomcat/util/LocalStrings.properties b/java/org/apache/tomcat/util/LocalStrings.properties new file mode 100644 index 0000000..abaa92b --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings.properties @@ -0,0 +1,40 @@ +# 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. + +diagnostics.deadlockFound=Deadlock found between the following threads: +diagnostics.setPropertyFail=Could not set [{0}] to [{1}], got [{2}] instead +diagnostics.threadDumpTitle=Full thread dump +diagnostics.vmInfoClassCompilation=Class compilation +diagnostics.vmInfoClassLoading=Class loading +diagnostics.vmInfoGarbageCollectors=Garbage Collector [{0}] +diagnostics.vmInfoLogger=Logger information +diagnostics.vmInfoMemory=Memory information +diagnostics.vmInfoMemoryManagers=Memory Manager [{0}] +diagnostics.vmInfoMemoryPools=Memory Pool [{0}] +diagnostics.vmInfoOs=OS information +diagnostics.vmInfoPath=Path information +diagnostics.vmInfoRuntime=Runtime information +diagnostics.vmInfoStartup=Startup arguments +diagnostics.vmInfoSystem=System properties +diagnostics.vmInfoThreadCounts=Thread counts +diagnostics.vmInfoThreadMxBean=ThreadMXBean capabilities + +introspectionUtils.conversionError=Error converting [{0}] to type [{1}] +introspectionUtils.getPropertyError=Error getting property [{0}] on class [{1}] +introspectionUtils.hostResolutionFail=Cannot resolve host name [{0}] +introspectionUtils.noMethod=Cannot find method [{0}] in object [{1}] of class [{2}] +introspectionUtils.nullParameter=Method name, parameter and object target arguments must not be null +introspectionUtils.setPropertyError=Error setting property [{0}] to [{1}] on class [{2}] +introspectionUtils.tooManyIterations=Property failed to be resolved and remains [{0}] diff --git a/java/org/apache/tomcat/util/LocalStrings_cs.properties b/java/org/apache/tomcat/util/LocalStrings_cs.properties new file mode 100644 index 0000000..2b79e57 --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_cs.properties @@ -0,0 +1,18 @@ +# 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. + +diagnostics.threadDumpTitle=Plné úložiÅ¡tÄ› vláken +diagnostics.vmInfoClassCompilation=Kompilace třídy +diagnostics.vmInfoLogger=Informace Loggeru diff --git a/java/org/apache/tomcat/util/LocalStrings_de.properties b/java/org/apache/tomcat/util/LocalStrings_de.properties new file mode 100644 index 0000000..7a9a30a --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_de.properties @@ -0,0 +1,19 @@ +# 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. + +diagnostics.threadDumpTitle=Kompletter Thread Dump +diagnostics.vmInfoClassCompilation=Ãœbersetzung der Klasse +diagnostics.vmInfoLogger=Logger-Information +diagnostics.vmInfoSystem=System-Eigenschaften diff --git a/java/org/apache/tomcat/util/LocalStrings_es.properties b/java/org/apache/tomcat/util/LocalStrings_es.properties new file mode 100644 index 0000000..bb96f4e --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_es.properties @@ -0,0 +1,19 @@ +# 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. + +diagnostics.threadDumpTitle=Volcado del hilo completo +diagnostics.vmInfoClassCompilation=Compilación de clase +diagnostics.vmInfoLogger=Información de logueo +diagnostics.vmInfoSystem=Propiedades del sistema diff --git a/java/org/apache/tomcat/util/LocalStrings_fr.properties b/java/org/apache/tomcat/util/LocalStrings_fr.properties new file mode 100644 index 0000000..1a6cc38 --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_fr.properties @@ -0,0 +1,40 @@ +# 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. + +diagnostics.deadlockFound=Un point de verrouillage a été trouvé entre les deux fils d'exécution suivants: +diagnostics.setPropertyFail=Impossible de définir [{0}] en [{1}], [{2}] a été obtenu à la place +diagnostics.threadDumpTitle=Traces complètes des threads +diagnostics.vmInfoClassCompilation=Compilation de la classe +diagnostics.vmInfoClassLoading=Chargeur de classes +diagnostics.vmInfoGarbageCollectors=Garbage Collector [{0}] +diagnostics.vmInfoLogger=Information sur le journal +diagnostics.vmInfoMemory=Information mémoire +diagnostics.vmInfoMemoryManagers=Gestionnaire de mémoire [{0}] +diagnostics.vmInfoMemoryPools=Pool de mémoire [{0}] +diagnostics.vmInfoOs=Information sur l'OS +diagnostics.vmInfoPath=Imformation de chemin +diagnostics.vmInfoRuntime=Information sur l'environnement d'exécution +diagnostics.vmInfoStartup=Arguments de démarrage +diagnostics.vmInfoSystem=Paramètres système +diagnostics.vmInfoThreadCounts=Nombre de fils d'exécution (threads) +diagnostics.vmInfoThreadMxBean=Capacités de ThreadMXBean + +introspectionUtils.conversionError=Erreur de conversion de [{0}] vers le type [{1}] +introspectionUtils.getPropertyError=Erreur en obtenant la propriété [{0}] sur la classe [{1}] +introspectionUtils.hostResolutionFail=Impossible de résoudre le nom d''hôte [{0}] +introspectionUtils.noMethod=Impossible de trouver la méthode [{0}] dans l''objet [{1}] de classe [{2}] +introspectionUtils.nullParameter=Les arguments nom de méthode, les paramètres et l'objet cible ne peuvent pas être null +introspectionUtils.setPropertyError=Erreur en fixant la propriété [{0}] à [{1}] sur la classe [{2}] +introspectionUtils.tooManyIterations=La propriét n''a pu être résolue et reste [{0}] diff --git a/java/org/apache/tomcat/util/LocalStrings_ja.properties b/java/org/apache/tomcat/util/LocalStrings_ja.properties new file mode 100644 index 0000000..090a521 --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_ja.properties @@ -0,0 +1,40 @@ +# 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. + +diagnostics.deadlockFound=次ã®ã‚¹ãƒ¬ãƒƒãƒ‰é–“ã§ãƒ‡ãƒƒãƒ‰ãƒ­ãƒƒã‚¯ãŒæ¤œå‡ºã•ã‚Œã¾ã—ãŸ: +diagnostics.setPropertyFail=[{0}] ã‚’ [{1}] ã«è¨­å®šã§ãã¾ã›ã‚“ã§ã—ãŸã€‚代ã‚ã‚Šã« [{2}] ã‚’å–å¾—ã—ã¾ã—㟠+diagnostics.threadDumpTitle=完全ãªã‚¹ãƒ¬ãƒƒãƒ‰ãƒ€ãƒ³ãƒ— +diagnostics.vmInfoClassCompilation=Class コンパイル +diagnostics.vmInfoClassLoading=クラスローディング +diagnostics.vmInfoGarbageCollectors=ガベージコレクタ [{0}] +diagnostics.vmInfoLogger=Logger 情報 +diagnostics.vmInfoMemory=メモリ情報 +diagnostics.vmInfoMemoryManagers=メモリマãƒãƒ¼ã‚¸ãƒ£[{0}] +diagnostics.vmInfoMemoryPools=メモリプール [{0}] +diagnostics.vmInfoOs=OS情報 +diagnostics.vmInfoPath=パス情報 +diagnostics.vmInfoRuntime=Runtime 情報 +diagnostics.vmInfoStartup=起動引数 +diagnostics.vmInfoSystem=システムプロパティ +diagnostics.vmInfoThreadCounts=スレッドカウント +diagnostics.vmInfoThreadMxBean=ThreadMXBeanã®æ©Ÿèƒ½ + +introspectionUtils.conversionError=[{0}] ã‹ã‚‰ã‚¯ãƒ©ã‚¹ [{1}] ã¸ã®å¤‰æ›ä¸­ã®ã‚¨ãƒ©ãƒ¼ +introspectionUtils.getPropertyError=クラス [{1}] ã®ãƒ—ロパティ [{0}] ã‚’å–得中ã®ã‚¨ãƒ©ãƒ¼ +introspectionUtils.hostResolutionFail=ホストå [{0}] を解決ã§ãã¾ã›ã‚“ +introspectionUtils.noMethod=クラス [{2}] ã®ã‚ªãƒ–ジェクト [{1}] ã«ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +introspectionUtils.nullParameter=引数ã®ã‚ªãƒ–ジェクトã€ãƒ¡ã‚½ãƒƒãƒ‰ã€ãƒ‘ラメータã®ã„ãšã‚Œã‚‚ null を指定ã§ãã¾ã›ã‚“。 +introspectionUtils.setPropertyError=クラス [{2}] ã®ãƒ—ロパティ [{0}] ã« [{1}] を設定中ã®ã‚¨ãƒ©ãƒ¼ +introspectionUtils.tooManyIterations=プロパティã¯è§£æ±ºã§ããªã‹ã£ãŸãŸã‚ [{0}] ã®ã¾ã¾ã§ã™ diff --git a/java/org/apache/tomcat/util/LocalStrings_ko.properties b/java/org/apache/tomcat/util/LocalStrings_ko.properties new file mode 100644 index 0000000..1761165 --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_ko.properties @@ -0,0 +1,35 @@ +# 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. + +diagnostics.threadDumpTitle=í’€ 쓰레드 ë¤í”„ +diagnostics.vmInfoClassCompilation=í´ëž˜ìŠ¤ ì»´íŒŒì¼ +diagnostics.vmInfoClassLoading=í´ëž˜ìŠ¤ 로딩 +diagnostics.vmInfoGarbageCollectors=Garbage Collector [{0}] +diagnostics.vmInfoLogger=Logger ì •ë³´ +diagnostics.vmInfoMemory=메모리 ì •ë³´ +diagnostics.vmInfoMemoryManagers=메모리 매니저 [{0}] +diagnostics.vmInfoMemoryPools=메모리 í’€ [{0}] +diagnostics.vmInfoOs=ìš´ì˜ì²´ì œ ì •ë³´ +diagnostics.vmInfoPath=경로 ì •ë³´ +diagnostics.vmInfoRuntime=런타임 ì •ë³´ +diagnostics.vmInfoStartup=프로그램 시작 아규먼트들 +diagnostics.vmInfoSystem=시스템 프로í¼í‹°ë“¤ +diagnostics.vmInfoThreadCounts=쓰레드 개수 +diagnostics.vmInfoThreadMxBean=ThreadMXBean 용량정보들 + +introspectionUtils.conversionError=[{0}]ì„(를) 타입 [{1}](으)ë¡œ 변환하는 중 오류 ë°œìƒ +introspectionUtils.getPropertyError=í´ëž˜ìŠ¤ [{1}]ì—ì„œ 프로í¼í‹° [{0}]ì„(를) 구하는 중 오류 ë°œìƒ +introspectionUtils.nullParameter=메소드 ì´ë¦„, 파ë¼ë¯¸í„°, 그리고 호출 ëŒ€ìƒ ê°ì²´ ì•„ê·œë¨¼íŠ¸ë“¤ì´ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +introspectionUtils.setPropertyError=í´ëž˜ìŠ¤ [{2}]ì—ì„œ, 프로í¼í‹° [{0}]ì„(를) [{1}](으)ë¡œ 설정하는 중 오류 ë°œìƒ diff --git a/java/org/apache/tomcat/util/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..f5baf54 --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_pt_BR.properties @@ -0,0 +1,17 @@ +# 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. + +diagnostics.threadDumpTitle=Thread Dump Completo +diagnostics.vmInfoClassCompilation=Compilação de classe diff --git a/java/org/apache/tomcat/util/LocalStrings_ru.properties b/java/org/apache/tomcat/util/LocalStrings_ru.properties new file mode 100644 index 0000000..ea9ea5e --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_ru.properties @@ -0,0 +1,17 @@ +# 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. + +diagnostics.threadDumpTitle=Полный дамп потоков +diagnostics.vmInfoClassCompilation=КомпилÑÑ†Ð¸Ñ ÐºÐ»Ð°ÑÑа diff --git a/java/org/apache/tomcat/util/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..e4ad10f --- /dev/null +++ b/java/org/apache/tomcat/util/LocalStrings_zh_CN.properties @@ -0,0 +1,35 @@ +# 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. + +diagnostics.threadDumpTitle=打å°å…¨éƒ¨çº¿ç¨‹ +diagnostics.vmInfoClassCompilation=class汇编 +diagnostics.vmInfoClassLoading=类加载中 +diagnostics.vmInfoGarbageCollectors=垃圾收集器[{0}] +diagnostics.vmInfoLogger=日志记录器(Loggerï¼‰ä¿¡æ¯ +diagnostics.vmInfoMemory=å†…å­˜ä¿¡æ¯ +diagnostics.vmInfoMemoryManagers=内存管ç†å™¨[{0}] +diagnostics.vmInfoMemoryPools=内存池[{0}] +diagnostics.vmInfoOs=æ“ä½œç³»ç»Ÿä¿¡æ¯ +diagnostics.vmInfoPath=è·¯å¾„ä¿¡æ¯ +diagnostics.vmInfoRuntime=è¿è¡Œæ—¶ä¿¡æ¯ +diagnostics.vmInfoStartup=å¯åŠ¨å‚æ•° +diagnostics.vmInfoSystem=系统属性 +diagnostics.vmInfoThreadCounts=线程数 +diagnostics.vmInfoThreadMxBean=ThreadMXBean功能 + +introspectionUtils.conversionError=å°†[{0}]转æ¢ä¸ºç±»åž‹[{1}]时出错 +introspectionUtils.getPropertyError=获å–ç±»[{1}]的属性[{0}]时出错 +introspectionUtils.nullParameter=方法åã€å‚数和对象目标å‚æ•°ä¸èƒ½ä¸ºç©º +introspectionUtils.setPropertyError=在类[{2}]上将属性[{0}]设置为[{1}]时出错 diff --git a/java/org/apache/tomcat/util/MultiThrowable.java b/java/org/apache/tomcat/util/MultiThrowable.java new file mode 100644 index 0000000..433eb8f --- /dev/null +++ b/java/org/apache/tomcat/util/MultiThrowable.java @@ -0,0 +1,99 @@ +/* + * 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.tomcat.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Wraps a list of throwables as a single throwable. This is intended to be used + * when multiple actions are taken where each may throw an exception but all + * actions are taken before any errors are reported. + *

    + * This class is NOT threadsafe. + */ +public class MultiThrowable extends Throwable { + + private static final long serialVersionUID = 1L; + + private List throwables = new ArrayList<>(); + + /** + * Add a throwable to the list of wrapped throwables. + * + * @param t The throwable to add + */ + public void add(Throwable t) { + throwables.add(t); + } + + + /** + * @return A read-only list of the wrapped throwables. + */ + public List getThrowables() { + return Collections.unmodifiableList(throwables); + } + + + /** + * @return {@code null} if there are no wrapped throwables, the Throwable if + * there is a single wrapped throwable or the current instance of + * there are multiple wrapped throwables + */ + public Throwable getThrowable() { + if (size() == 0) { + return null; + } else if (size() == 1) { + return throwables.get(0); + } else { + return this; + } + } + + + /** + * @return The number of throwables currently wrapped by this instance. + */ + public int size() { + return throwables.size(); + } + + + /** + * Overrides the default implementation to provide a concatenation of the + * messages associated with each of the wrapped throwables. Note that the + * format of the returned String is not guaranteed to be fixed and may + * change in a future release. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()); + sb.append(": "); + sb.append(size()); + sb.append(" wrapped Throwables: "); + for (Throwable t : throwables) { + sb.append('['); + sb.append(t.getMessage()); + sb.append(']'); + } + + return sb.toString(); + } +} diff --git a/java/org/apache/tomcat/util/XReflectionIntrospectionUtils.java b/java/org/apache/tomcat/util/XReflectionIntrospectionUtils.java new file mode 100644 index 0000000..021c90e --- /dev/null +++ b/java/org/apache/tomcat/util/XReflectionIntrospectionUtils.java @@ -0,0 +1,51 @@ +/* + * 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.tomcat.util; + +final class XReflectionIntrospectionUtils { + + static boolean isEnabled() { + return false; + } + + /** + * Always throws {@link UnsupportedOperationException} + * + * @param o Unused + * @param name Unused + * + * @return Never returns normally + */ + static Object getPropertyInternal(Object o, String name) { + throw new UnsupportedOperationException(); + } + + /** + * Always throws {@link UnsupportedOperationException} + * + * @param o Unused + * @param name Unused + * @param value Unused + * @param invokeSetProperty Unused + * + * @return Never returns normally + */ + static boolean setPropertyInternal(Object o, String name, String value, boolean invokeSetProperty) { + throw new UnsupportedOperationException(); + } + +} diff --git a/java/org/apache/tomcat/util/bcel/Const.java b/java/org/apache/tomcat/util/bcel/Const.java new file mode 100644 index 0000000..894a63c --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/Const.java @@ -0,0 +1,232 @@ +/* + * 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.tomcat.util.bcel; + +/** + * Constants for the project, mostly defined in the JVM specification. + */ +public final class Const { + + /** + * Java class file format Magic number: {@value}. + * + * @see The ClassFile Structure + * in The Java Virtual Machine Specification + */ + public static final int JVM_CLASSFILE_MAGIC = 0xCAFEBABE; + + /** + * One of the access flags for fields, methods, or classes: {@value}. + * + * @see Flag definitions for + * Classes in the Java Virtual Machine Specification (Java SE 9 Edition). + * @see Flag definitions for Fields + * in the Java Virtual Machine Specification (Java SE 9 Edition). + * @see Flag definitions for Methods + * in the Java Virtual Machine Specification (Java SE 9 Edition). + * @see Flag + * definitions for Inner Classes in the Java Virtual Machine Specification (Java SE 9 Edition). + */ + public static final short ACC_FINAL = 0x0010; + + /** + * One of the access flags for classes: {@value}. + * + * @see #ACC_FINAL + */ + public static final short ACC_INTERFACE = 0x0200; + + /** + * One of the access flags for methods or classes: {@value}. + * + * @see #ACC_FINAL + */ + public static final short ACC_ABSTRACT = 0x0400; + + /** + * One of the access flags for classes: {@value}. + * + * @see #ACC_FINAL + */ + public static final short ACC_ANNOTATION = 0x2000; + + /** + * Marks a constant pool entry as type UTF-8: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Utf8 = 1; + + /* + * The description of the constant pool is at: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4 + * References below are to the individual sections + */ + + /** + * Marks a constant pool entry as type Integer: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Integer = 3; + + /** + * Marks a constant pool entry as type Float: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Float = 4; + + /** + * Marks a constant pool entry as type Long: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Long = 5; + + /** + * Marks a constant pool entry as type Double: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Double = 6; + + /** + * Marks a constant pool entry as a Class: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Class = 7; + + /** + * Marks a constant pool entry as a Field Reference: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Fieldref = 9; + + /** + * Marks a constant pool entry as type String: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_String = 8; + + /** + * Marks a constant pool entry as a Method Reference: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Methodref = 10; + + /** + * Marks a constant pool entry as an Interface Method Reference: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_InterfaceMethodref = 11; + + /** + * Marks a constant pool entry as a name and type: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_NameAndType = 12; + + /** + * Marks a constant pool entry as a Method Handle: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_MethodHandle = 15; + + /** + * Marks a constant pool entry as a Method Type: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_MethodType = 16; + + /** + * Marks a constant pool entry as dynamically computed: {@value}. + * + * @see Change request for JEP + * 309 + */ + public static final byte CONSTANT_Dynamic = 17; + + /** + * Marks a constant pool entry as an Invoke Dynamic: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_InvokeDynamic = 18; + + /** + * Marks a constant pool entry as a Module Reference: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Module = 19; + + /** + * Marks a constant pool entry as a Package Reference: {@value}. + * + * @see The Constant Pool in The + * Java Virtual Machine Specification + */ + public static final byte CONSTANT_Package = 20; + + /** + * The names of the types of entries in a constant pool. Use getConstantName instead + */ + private static final String[] CONSTANT_NAMES = {"", "CONSTANT_Utf8", "", "CONSTANT_Integer", "CONSTANT_Float", "CONSTANT_Long", "CONSTANT_Double", + "CONSTANT_Class", "CONSTANT_String", "CONSTANT_Fieldref", "CONSTANT_Methodref", "CONSTANT_InterfaceMethodref", "CONSTANT_NameAndType", "", "", + "CONSTANT_MethodHandle", "CONSTANT_MethodType", "CONSTANT_Dynamic", "CONSTANT_InvokeDynamic", "CONSTANT_Module", "CONSTANT_Package"}; + + /** + * The maximum number of dimensions in an array: {@value}. One of the limitations of the Java Virtual Machine. + * + * @see Field Descriptors in + * The Java Virtual Machine Specification + */ + public static final int MAX_ARRAY_DIMENSIONS = 255; + + /** + * Get the CONSTANT_NAMES entry at the given index. + * + * @param index index into {@code CONSTANT_NAMES}. + * @return the CONSTANT_NAMES entry at the given index + */ + public static String getConstantName(final int index) { + return CONSTANT_NAMES[index]; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java b/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java new file mode 100644 index 0000000..db09a57 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java @@ -0,0 +1,39 @@ +/* + * 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.tomcat.util.bcel.classfile; + +public class AnnotationElementValue extends ElementValue { + // For annotation element values, this is the annotation + private final AnnotationEntry annotationEntry; + + AnnotationElementValue(final int type, final AnnotationEntry annotationEntry, final ConstantPool cpool) { + super(type, cpool); + if (type != ANNOTATION) { + throw new ClassFormatException("Only element values of type annotation can be built with this ctor - type specified: " + type); + } + this.annotationEntry = annotationEntry; + } + + public AnnotationEntry getAnnotationEntry() { + return annotationEntry; + } + + @Override + public String stringifyValue() { + return annotationEntry.toString(); + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java b/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java new file mode 100644 index 0000000..52e6a62 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java @@ -0,0 +1,70 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents one annotation in the annotation table + */ +public class AnnotationEntry { + + static final AnnotationEntry[] EMPTY_ARRAY = {}; + + private final int typeIndex; + + private final ConstantPool constantPool; + + private final List elementValuePairs; + + /* + * Creates an AnnotationEntry from a DataInputStream + * + * @param input + * @param constantPool + * @throws IOException + */ + AnnotationEntry(final DataInput input, final ConstantPool constantPool) throws IOException { + + this.constantPool = constantPool; + + typeIndex = input.readUnsignedShort(); + final int numElementValuePairs = input.readUnsignedShort(); + + elementValuePairs = new ArrayList<>(numElementValuePairs); + for (int i = 0; i < numElementValuePairs; i++) { + elementValuePairs.add(new ElementValuePair(input, constantPool)); + } + } + + /** + * @return the annotation type name + */ + public String getAnnotationType() { + return constantPool.getConstantUtf8(typeIndex).getBytes(); + } + + /** + * @return the element value pairs in this annotation entry + */ + public List getElementValuePairs() { + return elementValuePairs; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/Annotations.java b/java/org/apache/tomcat/util/bcel/classfile/Annotations.java new file mode 100644 index 0000000..c7d197f --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/Annotations.java @@ -0,0 +1,52 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +/** + * base class for annotations + */ +public class Annotations { + + static final Annotations[] EMPTY_ARRAY = {}; + + private final AnnotationEntry[] annotationTable; + + /** + * @param input Input stream + * @param constantPool Array of constants + */ + Annotations(final DataInput input, final ConstantPool constantPool) throws IOException { + final int annotationTableLength = input.readUnsignedShort(); + annotationTable = new AnnotationEntry[annotationTableLength]; + for (int i = 0; i < annotationTableLength; i++) { + annotationTable[i] = new AnnotationEntry(input, constantPool); + } + } + + + /** + * Gets the array of annotation entries in this annotation + * + * @return the array of annotation entries in this annotation + */ + public AnnotationEntry[] getAnnotationEntries() { + return annotationTable; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ArrayElementValue.java b/java/org/apache/tomcat/util/bcel/classfile/ArrayElementValue.java new file mode 100644 index 0000000..48a3644 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ArrayElementValue.java @@ -0,0 +1,49 @@ +/* + * 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.tomcat.util.bcel.classfile; + +public class ArrayElementValue extends ElementValue +{ + // For array types, this is the array + private final ElementValue[] elementValues; + + ArrayElementValue(final int type, final ElementValue[] datums, final ConstantPool cpool) { + super(type, cpool); + if (type != ARRAY) { + throw new ClassFormatException("Only element values of type array can be built with this ctor - type specified: " + type); + } + this.elementValues = datums; + } + + public ElementValue[] getElementValuesArray() { + return elementValues; + } + + @Override + public String stringifyValue() { + final StringBuilder sb = new StringBuilder(); + sb.append('['); + for (int i = 0; i < elementValues.length; i++) { + sb.append(elementValues[i].stringifyValue()); + if (i + 1 < elementValues.length) { + sb.append(','); + } + } + sb.append(']'); + return sb.toString(); + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ClassElementValue.java b/java/org/apache/tomcat/util/bcel/classfile/ClassElementValue.java new file mode 100644 index 0000000..0c6b21a --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ClassElementValue.java @@ -0,0 +1,35 @@ +/* + * 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.tomcat.util.bcel.classfile; + +public class ClassElementValue extends ElementValue { + // For primitive types and string type, this points to the value entry in + // the cpool + // For 'class' this points to the class entry in the cpool + private final int idx; + + ClassElementValue(final int type, final int idx, final ConstantPool cpool) { + super(type, cpool); + this.idx = idx; + } + + + @Override + public String stringifyValue() { + return super.getConstantPool().getConstantUtf8(idx).getBytes(); + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ClassFormatException.java b/java/org/apache/tomcat/util/bcel/classfile/ClassFormatException.java new file mode 100644 index 0000000..006090f --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ClassFormatException.java @@ -0,0 +1,64 @@ +/* + * 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.tomcat.util.bcel.classfile; + +/** + * Thrown when the BCEL attempts to read a class file and determines that a class is malformed or otherwise cannot be interpreted as a class file. + */ +public class ClassFormatException extends RuntimeException { + + private static final long serialVersionUID = 3243149520175287759L; + + /** + * Constructs a new instance with {@code null} as its detail message. The cause is not initialized, and may subsequently be initialized by a call to + * {@link #initCause}. + */ + public ClassFormatException() { + } + + /** + * Constructs a new instance with the specified detail message. The cause is not initialized, and may subsequently be initialized by a call to + * {@link #initCause}. + * + * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. + */ + public ClassFormatException(final String message) { + super(message); + } + + /** + * Constructs a new instance with the specified detail message and cause. + * + * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). A {@code null} value is permitted, and indicates that + * the cause is nonexistent or unknown. + */ + public ClassFormatException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new instance with the specified cause and a detail message of {@code (cause==null ? null : cause.toString())} (which typically contains the + * class and detail message of {@code cause}). This constructor is useful for runtime exceptions that are little more than wrappers for other throwables. + * + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). A {@code null} value is permitted, and indicates that the + * cause is nonexistent or unknown. + */ + public ClassFormatException(final Throwable cause) { + super(cause); + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java b/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java new file mode 100644 index 0000000..d610b04 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java @@ -0,0 +1,264 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.BufferedInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.tomcat.util.bcel.Const; + +/** + * Wrapper class that parses a given Java .class file. The method parse returns a + * JavaClass object on success. When an I/O error or an inconsistency occurs an + * appropriate exception is propagated back to the caller. + * + * The structure and the names comply, except for a few conveniences, exactly with the + * JVM specification 1.0. See this paper for further details about + * the structure of a bytecode file. + */ +public final class ClassParser { + + private static final int BUFSIZE = 8192; + private final DataInput dataInputStream; + private String className; + private String superclassName; + private int accessFlags; // Access rights of parsed class + private String[] interfaceNames; // Names of implemented interfaces + private ConstantPool constantPool; // collection of constants + private Annotations runtimeVisibleAnnotations; // "RuntimeVisibleAnnotations" attribute defined in the class + private List runtimeVisibleFieldOrMethodAnnotations; // "RuntimeVisibleAnnotations" attribute defined elsewhere + + private static final String[] INTERFACES_EMPTY_ARRAY = {}; + + /** + * Parses class from the given stream. + * + * @param inputStream Input stream + */ + public ClassParser(final InputStream inputStream) { + this.dataInputStream = new DataInputStream(new BufferedInputStream(inputStream, BUFSIZE)); + } + + + /** + * Parses the given Java class file and return an object that represents the contained data, i.e., constants, methods, + * fields and commands. A ClassFormatException is raised, if the file is not a valid .class file. (This does + * not include verification of the byte code as it is performed by the Java interpreter). + * + * @return Class object representing the parsed class file + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + public JavaClass parse() throws IOException, ClassFormatException { + //****************** Read headers ******************************** + // Check magic tag of class file + readID(); + // Get compiler version + readVersion(); + //***************** Read constant pool and related ************** + // Read constant pool entries + readConstantPool(); + // Get class information + readClassInfo(); + // Get interface information, i.e., implemented interfaces + readInterfaces(); + //***************** Read class fields and methods *************** + // Read class fields, i.e., the variables of the class + readFields(); + // Read class methods, i.e., the functions in the class + readMethods(); + // Read class attributes + readAttributes(false); + + // Return the information we have gathered in a new object + return new JavaClass(className, superclassName, accessFlags, constantPool, interfaceNames, + runtimeVisibleAnnotations, runtimeVisibleFieldOrMethodAnnotations); + } + + + /** + * Reads information about the attributes of the class. + * @param fieldOrMethod false if processing a class + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + private void readAttributes(boolean fieldOrMethod) throws IOException, ClassFormatException { + final int attributesCount = dataInputStream.readUnsignedShort(); + for (int i = 0; i < attributesCount; i++) { + ConstantUtf8 c; + String name; + int name_index; + int length; + // Get class name from constant pool via 'name_index' indirection + name_index = dataInputStream.readUnsignedShort(); + c = (ConstantUtf8) constantPool.getConstant(name_index, + Const.CONSTANT_Utf8); + name = c.getBytes(); + // Length of data in bytes + length = dataInputStream.readInt(); + if (name.equals("RuntimeVisibleAnnotations")) { + if (fieldOrMethod) { + Annotations fieldOrMethodAnnotations = new Annotations(dataInputStream, constantPool); + if (runtimeVisibleFieldOrMethodAnnotations == null) { + runtimeVisibleFieldOrMethodAnnotations = new ArrayList<>(); + } + runtimeVisibleFieldOrMethodAnnotations.add(fieldOrMethodAnnotations); + } else { + if (runtimeVisibleAnnotations != null) { + throw new ClassFormatException( + "RuntimeVisibleAnnotations attribute is not allowed more than once in a class file"); + } + runtimeVisibleAnnotations = new Annotations(dataInputStream, constantPool); + } + } else { + // All other attributes are skipped + Utility.skipFully(dataInputStream, length); + } + } + } + + + /** + * Reads information about the class and its super class. + * + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + private void readClassInfo() throws IOException, ClassFormatException { + accessFlags = dataInputStream.readUnsignedShort(); + /* + * Interfaces are implicitly abstract, the flag should be set according to the JVM specification. + */ + if ((accessFlags & Const.ACC_INTERFACE) != 0) { + accessFlags |= Const.ACC_ABSTRACT; + } + if ((accessFlags & Const.ACC_ABSTRACT) != 0 && (accessFlags & Const.ACC_FINAL) != 0) { + throw new ClassFormatException("Class can't be both final and abstract"); + } + + int classNameIndex = dataInputStream.readUnsignedShort(); + className = Utility.getClassName(constantPool, classNameIndex); + + int superclass_name_index = dataInputStream.readUnsignedShort(); + if (superclass_name_index > 0) { + // May be zero -> class is java.lang.Object + superclassName = Utility.getClassName(constantPool, superclass_name_index); + } else { + superclassName = "java.lang.Object"; + } + } + + + /** + * Reads constant pool entries. + * + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + private void readConstantPool() throws IOException, ClassFormatException { + constantPool = new ConstantPool(dataInputStream); + } + + + /** + * Reads information about the fields of the class, i.e., its variables. + * + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + private void readFields() throws IOException, ClassFormatException { + final int fieldsCount = dataInputStream.readUnsignedShort(); + for (int i = 0; i < fieldsCount; i++) { + // file.readUnsignedShort(); // Unused access flags + // file.readUnsignedShort(); // name index + // file.readUnsignedShort(); // signature index + Utility.skipFully(dataInputStream, 6); + + readAttributes(true); + } + } + + + /** + * Checks whether the header of the file is ok. Of course, this has to be the first action on successive file reads. + * + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + private void readID() throws IOException, ClassFormatException { + if (dataInputStream.readInt() != Const.JVM_CLASSFILE_MAGIC) { + throw new ClassFormatException("It is not a Java .class file"); + } + } + + + /** + * Reads information about the interfaces implemented by this class. + * + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + private void readInterfaces() throws IOException, ClassFormatException { + final int interfacesCount = dataInputStream.readUnsignedShort(); + if (interfacesCount > 0) { + interfaceNames = new String[interfacesCount]; + for (int i = 0; i < interfacesCount; i++) { + int index = dataInputStream.readUnsignedShort(); + interfaceNames[i] = Utility.getClassName(constantPool, index); + } + } else { + interfaceNames = INTERFACES_EMPTY_ARRAY; + } + } + + + /** + * Reads information about the methods of the class. + * + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + private void readMethods() throws IOException, ClassFormatException { + final int methodsCount = dataInputStream.readUnsignedShort(); + for (int i = 0; i < methodsCount; i++) { + // file.readUnsignedShort(); // Unused access flags + // file.readUnsignedShort(); // name index + // file.readUnsignedShort(); // signature index + Utility.skipFully(dataInputStream, 6); + + readAttributes(true); + } + } + + + /** + * Reads major and minor version of compiler which created the file. + * + * @throws IOException if an I/O error occurs. + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + */ + private void readVersion() throws IOException, ClassFormatException { + // file.readUnsignedShort(); // Unused minor + // file.readUnsignedShort(); // Unused major + Utility.skipFully(dataInputStream, 4); + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/Constant.java b/java/org/apache/tomcat/util/bcel/classfile/Constant.java new file mode 100644 index 0000000..875c1d8 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/Constant.java @@ -0,0 +1,102 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * Abstract superclass for classes to represent the different constant types in the constant pool of a class file. The + * classes keep closely to the JVM specification. + */ +public abstract class Constant { + + /** + * Reads one constant from the given input, the type depends on a tag byte. + * + * @param dataInput Input stream + * @return Constant object + * @throws IOException if an I/O error occurs reading from the given {@code dataInput}. + * @throws ClassFormatException if the next byte is not recognized + */ + static Constant readConstant(final DataInput dataInput) throws IOException, ClassFormatException { + final byte b = dataInput.readByte(); // Read tag byte + int skipSize; + switch (b) { + case Const.CONSTANT_Class: + return new ConstantClass(dataInput); + case Const.CONSTANT_Integer: + return new ConstantInteger(dataInput); + case Const.CONSTANT_Float: + return new ConstantFloat(dataInput); + case Const.CONSTANT_Long: + return new ConstantLong(dataInput); + case Const.CONSTANT_Double: + return new ConstantDouble(dataInput); + case Const.CONSTANT_Utf8: + return ConstantUtf8.getInstance(dataInput); + case Const.CONSTANT_String: + case Const.CONSTANT_MethodType: + case Const.CONSTANT_Module: + case Const.CONSTANT_Package: + skipSize = 2; // unsigned short + break; + case Const.CONSTANT_MethodHandle: + skipSize = 3; // unsigned byte, unsigned short + break; + case Const.CONSTANT_Fieldref: + case Const.CONSTANT_Methodref: + case Const.CONSTANT_InterfaceMethodref: + case Const.CONSTANT_NameAndType: + case Const.CONSTANT_Dynamic: + case Const.CONSTANT_InvokeDynamic: + skipSize = 4; // unsigned short, unsigned short + break; + default: + throw new ClassFormatException("Invalid byte tag in constant pool: " + b); + } + Utility.skipFully(dataInput, skipSize); + return null; + } + + /* + * In fact this tag is redundant since we can distinguish different 'Constant' objects by their type, i.e., via + * 'instanceof'. In some places we will use the tag for switch()es anyway. + * + * First, we want match the specification as closely as possible. Second we need the tag as an index to select the + * corresponding class name from the 'CONSTANT_NAMES' array. + */ + private final byte tag; + + Constant(final byte tag) { + this.tag = tag; + } + + /** + * @return Tag of constant, i.e., its type. No setTag() method to avoid confusion. + */ + public final byte getTag() { + return tag; + } + + @Override + public String toString() { + return "[" + tag + "]"; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java b/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java new file mode 100644 index 0000000..9f62473 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java @@ -0,0 +1,52 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * This class is derived from the abstract {@link Constant} and represents a reference to a (external) class. + * + * @see Constant + */ +public final class ConstantClass extends Constant { + + private final int nameIndex; // Identical to ConstantString except for the name + + + /** + * Constructs an instance from file data. + * + * @param dataInput Input stream + * @throws IOException if an I/O error occurs reading from the given {@code dataInput}. + */ + ConstantClass(final DataInput dataInput) throws IOException { + super(Const.CONSTANT_Class); + this.nameIndex = dataInput.readUnsignedShort(); + } + + + /** + * @return Name index in constant pool of class name. + */ + public int getNameIndex() { + return nameIndex; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java b/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java new file mode 100644 index 0000000..4641526 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java @@ -0,0 +1,52 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * This class is derived from the abstract {@link Constant} and represents a reference to a Double object. + * + * @see Constant + */ +public final class ConstantDouble extends Constant { + + private final double bytes; + + + /** + * Initialize instance from file data. + * + * @param file Input stream + * @throws IOException if an I/O error occurs. + */ + ConstantDouble(final DataInput file) throws IOException { + super(Const.CONSTANT_Double); + this.bytes = file.readDouble(); + } + + + /** + * @return data, i.e., 8 bytes. + */ + public double getBytes() { + return bytes; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java b/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java new file mode 100644 index 0000000..69bfffd --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java @@ -0,0 +1,52 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * This class is derived from the abstract {@link Constant} and represents a reference to a float object. + * + * @see Constant + */ +public final class ConstantFloat extends Constant { + + private final float bytes; + + + /** + * Initialize instance from file data. + * + * @param file Input stream + * @throws IOException if an I/O error occurs. + */ + ConstantFloat(final DataInput file) throws IOException { + super(Const.CONSTANT_Float); + this.bytes = file.readFloat(); + } + + + /** + * @return data, i.e., 4 bytes. + */ + public float getBytes() { + return bytes; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java b/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java new file mode 100644 index 0000000..f3c4d2d --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java @@ -0,0 +1,52 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * This class is derived from the abstract {@link Constant} and represents a reference to an int object. + * + * @see Constant + */ +public final class ConstantInteger extends Constant { + + private final int bytes; + + + /** + * Initialize instance from file data. + * + * @param file Input stream + * @throws IOException if an I/O error occurs. + */ + ConstantInteger(final DataInput file) throws IOException { + super(Const.CONSTANT_Integer); + this.bytes = file.readInt(); + } + + + /** + * @return data, i.e., 4 bytes. + */ + public int getBytes() { + return bytes; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java b/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java new file mode 100644 index 0000000..6d0d609 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java @@ -0,0 +1,52 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * This class is derived from the abstract {@link Constant} and represents a reference to a long object. + * + * @see Constant + */ +public final class ConstantLong extends Constant { + + private final long bytes; + + + /** + * Initialize instance from file data. + * + * @param file Input stream + * @throws IOException if an I/O error occurs. + */ + ConstantLong(final DataInput file) throws IOException { + super(Const.CONSTANT_Long); + this.bytes = file.readLong(); + } + + + /** + * @return data, i.e., 8 bytes. + */ + public long getBytes() { + return bytes; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java b/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java new file mode 100644 index 0000000..a9639e0 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java @@ -0,0 +1,154 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * This class represents the constant pool, i.e., a table of constants, of a parsed classfile. It may contain null references, due to the JVM specification that + * skips an entry after an 8-byte constant (double, long) entry. Those interested in generating constant pools programmatically should see + * ConstantPoolGen. + * + * @see Constant + */ +public class ConstantPool { + + private final Constant[] constantPool; + + /** + * Reads constants from given input stream. + * + * @param input Input stream + * @throws IOException if an I/O error occurs reading the the InputStream + * @throws ClassFormatException If the .class file is not valid + */ + ConstantPool(final DataInput input) throws IOException, ClassFormatException { + final int constantPoolCount = input.readUnsignedShort(); + constantPool = new Constant[constantPoolCount]; + /* + * constantPool[0] is unused by the compiler and may be used freely by the implementation. + * constantPool[0] is currently unused by the implementation. + */ + for (int i = 1; i < constantPoolCount; i++) { + constantPool[i] = Constant.readConstant(input); + /* + * Quote from the JVM specification: "All eight byte constants take up two spots in the constant pool. If this is the n'th byte in the constant + * pool, then the next item will be numbered n+2" + * + * Thus we have to increment the index counter. + */ + if (constantPool[i] != null) { + byte tag = constantPool[i].getTag(); + if (tag == Const.CONSTANT_Double || tag == Const.CONSTANT_Long) { + i++; + } + } + } + } + + /** + * Gets constant from constant pool. + * + * @param A {@link Constant} subclass + * @param index Index in constant pool + * @return Constant value + * @see Constant + * @throws ClassFormatException if index is invalid + */ + @SuppressWarnings("unchecked") + public T getConstant(final int index) throws ClassFormatException { + return (T) getConstant(index, Constant.class); + } + + /** + * Gets constant from constant pool and check whether it has the expected type. + * + * @param A {@link Constant} subclass + * @param index Index in constant pool + * @param tag Tag of expected constant, i.e., its type + * @return Constant value + * @see Constant + * @throws ClassFormatException if constant type does not match tag + */ + public T getConstant(final int index, final byte tag) throws ClassFormatException { + final T c = getConstant(index); + if (c.getTag() != tag) { + throw new ClassFormatException("Expected class '" + Const.getConstantName(tag) + "' at index " + index + " and got " + c); + } + return c; + } + + /** + * Gets constant from constant pool. + * + * @param A {@link Constant} subclass + * @param index Index in constant pool + * @param castTo The {@link Constant} subclass to cast to. + * @return Constant value + * @see Constant + * @throws ClassFormatException if index is invalid + */ + public T getConstant(final int index, final Class castTo) throws ClassFormatException { + if (index >= constantPool.length || index < 1) { + throw new ClassFormatException("Invalid constant pool reference using index: " + index + ". Constant pool size is: " + constantPool.length); + } + if (constantPool[index] != null && !castTo.isAssignableFrom(constantPool[index].getClass())) { + throw new ClassFormatException("Invalid constant pool reference at index: " + index + + ". Expected " + castTo + " but was " + constantPool[index].getClass()); + } + if (index > 1) { + final Constant prev = constantPool[index - 1]; + if (prev != null && (prev.getTag() == Const.CONSTANT_Double || prev.getTag() == Const.CONSTANT_Long)) { + throw new ClassFormatException("Constant pool at index " + index + " is invalid. The index is unused due to the preceeding " + + Const.getConstantName(prev.getTag()) + "."); + } + } + // Previous check ensures this won't throw a ClassCastException + final T c = castTo.cast(constantPool[index]); + if (c == null) { + throw new ClassFormatException("Constant pool at index " + index + " is null."); + } + return c; + } + + /** + * Gets constant from constant pool and check whether it has the expected type. + * + * @param index Index in constant pool + * @return ConstantInteger value + * @see ConstantInteger + * @throws ClassFormatException if constant type does not match tag + */ + public ConstantInteger getConstantInteger(final int index) { + return getConstant(index, Const.CONSTANT_Integer); + } + + /** + * Gets constant from constant pool and check whether it has the expected type. + * + * @param index Index in constant pool + * @return ConstantUtf8 value + * @see ConstantUtf8 + * @throws ClassFormatException if constant type does not match tag + */ + public ConstantUtf8 getConstantUtf8(final int index) throws ClassFormatException { + return getConstant(index, Const.CONSTANT_Utf8); + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ConstantUtf8.java b/java/org/apache/tomcat/util/bcel/classfile/ConstantUtf8.java new file mode 100644 index 0000000..0de93ab --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ConstantUtf8.java @@ -0,0 +1,62 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; +import java.util.Objects; + +import org.apache.tomcat.util.bcel.Const; + +/** + * Extends the abstract {@link Constant} to represent a reference to a UTF-8 encoded string. + * + * @see Constant + */ +public final class ConstantUtf8 extends Constant { + + /** + * Gets a new or cached instance of the given value. + *

    + * See {@link ConstantUtf8} class Javadoc for details. + *

    + * + * @param dataInput the value. + * @return a new or cached instance of the given value. + * @throws IOException if an I/O error occurs. + */ + static ConstantUtf8 getInstance(final DataInput dataInput) throws IOException { + return new ConstantUtf8(dataInput.readUTF()); + } + + private final String value; + + /** + * @param value Data + */ + private ConstantUtf8(final String value) { + super(Const.CONSTANT_Utf8); + this.value = Objects.requireNonNull(value, "value"); + } + + /** + * @return Data converted to string. + */ + public String getBytes() { + return value; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ElementValue.java b/java/org/apache/tomcat/util/bcel/classfile/ElementValue.java new file mode 100644 index 0000000..8c68b3f --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ElementValue.java @@ -0,0 +1,151 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * The element_value structure is documented at https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7.16.1 + * + *
    + * element_value {
    + *     u1 tag;
    + *     union {
    + *         u2 const_value_index;
    + *
    + *         {   u2 type_name_index;
    + *             u2 const_name_index;
    + *         } enum_const_value;
    + *
    + *         u2 class_info_index;
    + *
    + *         annotation annotation_value;
    + *
    + *         {   u2            num_values;
    + *             element_value values[num_values];
    + *         } array_value;
    + *     } value;
    + * }
    + * 
    + */ +public abstract class ElementValue { + + public static final byte STRING = 's'; + public static final byte ENUM_CONSTANT = 'e'; + public static final byte CLASS = 'c'; + public static final byte ANNOTATION = '@'; + public static final byte ARRAY = '['; + public static final byte PRIMITIVE_INT = 'I'; + public static final byte PRIMITIVE_BYTE = 'B'; + public static final byte PRIMITIVE_CHAR = 'C'; + public static final byte PRIMITIVE_DOUBLE = 'D'; + public static final byte PRIMITIVE_FLOAT = 'F'; + public static final byte PRIMITIVE_LONG = 'J'; + public static final byte PRIMITIVE_SHORT = 'S'; + public static final byte PRIMITIVE_BOOLEAN = 'Z'; + + /** + * Reads an {@code element_value} as an {@code ElementValue}. + * + * @param input Raw data input. + * @param cpool Constant pool. + * @return a new ElementValue. + * @throws IOException if an I/O error occurs. + */ + public static ElementValue readElementValue(final DataInput input, final ConstantPool cpool) throws IOException { + return readElementValue(input, cpool, 0); + } + + /** + * Reads an {@code element_value} as an {@code ElementValue}. + * + * @param input Raw data input. + * @param cpool Constant pool. + * @param arrayNesting level of current array nesting. + * @return a new ElementValue. + * @throws IOException if an I/O error occurs. + * @since 6.7.0 + */ + public static ElementValue readElementValue(final DataInput input, final ConstantPool cpool, int arrayNesting) + throws IOException { + final byte tag = input.readByte(); + switch (tag) { + case PRIMITIVE_BYTE: + case PRIMITIVE_CHAR: + case PRIMITIVE_DOUBLE: + case PRIMITIVE_FLOAT: + case PRIMITIVE_INT: + case PRIMITIVE_LONG: + case PRIMITIVE_SHORT: + case PRIMITIVE_BOOLEAN: + case STRING: + return new SimpleElementValue(tag, input.readUnsignedShort(), cpool); + + case ENUM_CONSTANT: + input.readUnsignedShort(); // Unused type_index + return new EnumElementValue(ENUM_CONSTANT, input.readUnsignedShort(), cpool); + + case CLASS: + return new ClassElementValue(CLASS, input.readUnsignedShort(), cpool); + + case ANNOTATION: + // TODO isRuntimeVisible + return new AnnotationElementValue(ANNOTATION, new AnnotationEntry(input, cpool), cpool); + + case ARRAY: + arrayNesting++; + if (arrayNesting > Const.MAX_ARRAY_DIMENSIONS) { + // JVM spec 4.4.1 + throw new ClassFormatException(String.format("Arrays are only valid if they represent %,d or fewer dimensions.", + Integer.valueOf(Const.MAX_ARRAY_DIMENSIONS))); + } + final int numArrayVals = input.readUnsignedShort(); + final ElementValue[] evalues = new ElementValue[numArrayVals]; + for (int j = 0; j < numArrayVals; j++) { + evalues[j] = readElementValue(input, cpool, arrayNesting); + } + return new ArrayElementValue(ARRAY, evalues, cpool); + + default: + throw new ClassFormatException("Unexpected element value kind in annotation: " + tag); + } + } + + + private final int type; + + private final ConstantPool cpool; + + + ElementValue(final int type, final ConstantPool cpool) { + this.type = type; + this.cpool = cpool; + } + + final ConstantPool getConstantPool() { + return cpool; + } + + final int getType() { + return type; + } + + public abstract String stringifyValue(); +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/ElementValuePair.java b/java/org/apache/tomcat/util/bcel/classfile/ElementValuePair.java new file mode 100644 index 0000000..88e9efc --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/ElementValuePair.java @@ -0,0 +1,50 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * An annotation's element value pair. + * + * @since 6.0 + */ +public class ElementValuePair { + private final ElementValue elementValue; + + private final ConstantPool constantPool; + + private final int elementNameIndex; + + ElementValuePair(final DataInput file, final ConstantPool constantPool) throws IOException { + this.constantPool = constantPool; + this.elementNameIndex = file.readUnsignedShort(); + this.elementValue = ElementValue.readElementValue(file, constantPool); + } + + public String getNameString() { + final ConstantUtf8 c = (ConstantUtf8) constantPool.getConstant(elementNameIndex, Const.CONSTANT_Utf8); + return c.getBytes(); + } + + public final ElementValue getValue() { + return elementValue; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/EnumElementValue.java b/java/org/apache/tomcat/util/bcel/classfile/EnumElementValue.java new file mode 100644 index 0000000..7a1d2a0 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/EnumElementValue.java @@ -0,0 +1,34 @@ +/* + * 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.tomcat.util.bcel.classfile; + +public class EnumElementValue extends ElementValue { + private final int valueIdx; + + EnumElementValue(final int type, final int valueIdx, final ConstantPool cpool) { + super(type, cpool); + if (type != ENUM_CONSTANT) { + throw new ClassFormatException("Only element values of type enum can be built with this ctor - type specified: " + type); + } + this.valueIdx = valueIdx; + } + + @Override + public String stringifyValue() { + return super.getConstantPool().getConstantUtf8(valueIdx).getBytes(); + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java b/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java new file mode 100644 index 0000000..6597677 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java @@ -0,0 +1,131 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.util.HashMap; +import java.util.List; + +/** + * Represents a Java class, i.e., the data structures, constant pool, fields, methods and commands contained in a Java + * .class file. See JVM specification for details. The intent of + * this class is to represent a parsed or otherwise existing class file. Those interested in programmatically generating + * classes should see the ClassGen class. + */ +public class JavaClass { + + private final int accessFlags; + private final String className; + private final String superclassName; + private final String[] interfaceNames; + private final Annotations runtimeVisibleAnnotations; // "RuntimeVisibleAnnotations" attribute defined in the class + private final List runtimeVisibleFieldOrMethodAnnotations; // "RuntimeVisibleAnnotations" attribute defined elsewhere + + /** + * Constructor gets all contents as arguments. + * + * @param className Name of this class. + * @param superclassName Name of this class's superclass. + * @param accessFlags Access rights defined by bit flags + * @param constantPool Array of constants + * @param interfaceNames Implemented interfaces + * @param runtimeVisibleAnnotations "RuntimeVisibleAnnotations" attribute defined on the Class, or null + * @param runtimeVisibleFieldOrMethodAnnotations "RuntimeVisibleAnnotations" attribute defined on the fields or methods, or null + */ + JavaClass(final String className, final String superclassName, + final int accessFlags, final ConstantPool constantPool, final String[] interfaceNames, + final Annotations runtimeVisibleAnnotations, final List runtimeVisibleFieldOrMethodAnnotations) { + this.accessFlags = accessFlags; + this.runtimeVisibleAnnotations = runtimeVisibleAnnotations; + this.runtimeVisibleFieldOrMethodAnnotations = runtimeVisibleFieldOrMethodAnnotations; + this.className = className; + this.superclassName = superclassName; + this.interfaceNames = interfaceNames; + } + + /** + * @return Access flags of the object aka. "modifiers". + */ + public final int getAccessFlags() { + return accessFlags; + } + + /** + * Return annotations entries from "RuntimeVisibleAnnotations" attribute on + * the class, fields or methods if there is any. + * + * @return An array of entries or {@code null} + */ + public AnnotationEntry[] getAllAnnotationEntries() { + HashMap annotationEntries = new HashMap<>(); + if (runtimeVisibleAnnotations != null) { + for (AnnotationEntry annotationEntry : runtimeVisibleAnnotations.getAnnotationEntries()) { + annotationEntries.put(annotationEntry.getAnnotationType(), annotationEntry); + } + } + if (runtimeVisibleFieldOrMethodAnnotations != null) { + for (Annotations annotations : runtimeVisibleFieldOrMethodAnnotations.toArray(Annotations.EMPTY_ARRAY)) { + for (AnnotationEntry annotationEntry : annotations.getAnnotationEntries()) { + annotationEntries.putIfAbsent(annotationEntry.getAnnotationType(), annotationEntry); + } + } + } + if (annotationEntries.isEmpty()) { + return null; + } else { + return annotationEntries.values().toArray(AnnotationEntry.EMPTY_ARRAY); + } + } + + /** + * Return annotations entries from "RuntimeVisibleAnnotations" attribute on + * the class, if there is any. + * + * @return An array of entries or {@code null} + */ + public AnnotationEntry[] getAnnotationEntries() { + if (runtimeVisibleAnnotations != null) { + return runtimeVisibleAnnotations.getAnnotationEntries(); + } + return null; + } + + /** + * @return Class name. + */ + public String getClassName() { + return className; + } + + + /** + * @return Names of implemented interfaces. + */ + public String[] getInterfaceNames() { + return interfaceNames; + } + + + /** + * returns the super class name of this class. In the case that this class is {@link Object}, it will return itself + * ({@link Object}). This is probably incorrect but isn't fixed at this time to not break existing clients. + * + * @return Superclass name. + */ + public String getSuperclassName() { + return superclassName; + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java b/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java new file mode 100644 index 0000000..295a8a9 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java @@ -0,0 +1,75 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import org.apache.tomcat.util.bcel.Const; + +public class SimpleElementValue extends ElementValue { + private final int index; + + SimpleElementValue(final int type, final int index, final ConstantPool cpool) { + super(type, cpool); + this.index = index; + } + + /** + * @return Value entry index in the cpool + */ + public int getIndex() { + return index; + } + + + // Whatever kind of value it is, return it as a string + @Override + public String stringifyValue() { + final ConstantPool cpool = super.getConstantPool(); + final int type = super.getType(); + switch (type) { + case PRIMITIVE_INT: + return Integer.toString(cpool.getConstantInteger(getIndex()).getBytes()); + case PRIMITIVE_LONG: + final ConstantLong j = cpool.getConstant(getIndex(), Const.CONSTANT_Long); + return Long.toString(j.getBytes()); + case PRIMITIVE_DOUBLE: + final ConstantDouble d = cpool.getConstant(getIndex(), Const.CONSTANT_Double); + return Double.toString(d.getBytes()); + case PRIMITIVE_FLOAT: + final ConstantFloat f = cpool.getConstant(getIndex(), Const.CONSTANT_Float); + return Float.toString(f.getBytes()); + case PRIMITIVE_SHORT: + final ConstantInteger s = cpool.getConstantInteger(getIndex()); + return Integer.toString(s.getBytes()); + case PRIMITIVE_BYTE: + final ConstantInteger b = cpool.getConstantInteger(getIndex()); + return Integer.toString(b.getBytes()); + case PRIMITIVE_CHAR: + final ConstantInteger ch = cpool.getConstantInteger(getIndex()); + return String.valueOf((char) ch.getBytes()); + case PRIMITIVE_BOOLEAN: + final ConstantInteger bo = cpool.getConstantInteger(getIndex()); + if (bo.getBytes() == 0) { + return "false"; + } + return "true"; + case STRING: + return cpool.getConstantUtf8(getIndex()).getBytes(); + default: + throw new IllegalStateException("SimpleElementValue class does not know how to stringify type " + type); + } + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/Utility.java b/java/org/apache/tomcat/util/bcel/classfile/Utility.java new file mode 100644 index 0000000..162fac8 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/Utility.java @@ -0,0 +1,64 @@ +/* + * 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.tomcat.util.bcel.classfile; + +import java.io.DataInput; +import java.io.EOFException; +import java.io.IOException; + +import org.apache.tomcat.util.bcel.Const; + +/** + * Utility functions that do not really belong to any class in particular. + */ +final class Utility { + + /** + * Shorten long class name str, i.e., chop off the prefix, + * if the + * class name starts with this string and the flag chopit is true. + * Slashes / are converted to dots .. + * + * @param str The long class name + * @return Compacted class name + */ + static String compactClassName(final String str) { + return str.replace('/', '.'); // Is '/' on all systems, even DOS + } + + static String getClassName(final ConstantPool constantPool, final int index) { + Constant c = constantPool.getConstant(index, Const.CONSTANT_Class); + int i = ((ConstantClass) c).getNameIndex(); + + // Finally get the string from the constant pool + c = constantPool.getConstant(i, Const.CONSTANT_Utf8); + String name = ((ConstantUtf8) c).getBytes(); + + return compactClassName(name); + } + + static void skipFully(final DataInput file, final int length) throws IOException { + int total = file.skipBytes(length); + if (total != length) { + throw new EOFException(); + } + } + + private Utility() { + // Hide default constructor + } +} diff --git a/java/org/apache/tomcat/util/bcel/classfile/package-info.java b/java/org/apache/tomcat/util/bcel/classfile/package-info.java new file mode 100644 index 0000000..0cdf173 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/classfile/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Classes that describe the structure of a Java class file and a class file parser. + */ +package org.apache.tomcat.util.bcel.classfile; \ No newline at end of file diff --git a/java/org/apache/tomcat/util/bcel/package-info.java b/java/org/apache/tomcat/util/bcel/package-info.java new file mode 100644 index 0000000..91b0192 --- /dev/null +++ b/java/org/apache/tomcat/util/bcel/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Basic classes for the Apache Byte Code Engineering Library (BCEL) and constants defined by the + * JVM specification. + */ +package org.apache.tomcat.util.bcel; \ No newline at end of file diff --git a/java/org/apache/tomcat/util/buf/AbstractChunk.java b/java/org/apache/tomcat/util/buf/AbstractChunk.java new file mode 100644 index 0000000..3e06362 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/AbstractChunk.java @@ -0,0 +1,182 @@ +/* + * 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.tomcat.util.buf; + +import java.io.Serializable; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Base class for the *Chunk implementation to reduce duplication. + */ +public abstract class AbstractChunk implements Cloneable, Serializable { + + private static final long serialVersionUID = 1L; + protected static final StringManager sm = StringManager.getManager(AbstractChunk.class); + + /* + * JVMs may limit the maximum array size to slightly less than Integer.MAX_VALUE. On markt's desktop the limit is + * MAX_VALUE - 2. Comments in the JRE source code for ArrayList and other classes indicate that it may be as low as + * MAX_VALUE - 8 on some systems. + */ + public static final int ARRAY_MAX_SIZE = Integer.MAX_VALUE - 8; + + private int hashCode = 0; + protected boolean hasHashCode = false; + + protected boolean isSet; + + private int limit = -1; + + protected int start; + protected int end; + + + /** + * Maximum amount of data in this buffer. If -1 or not set, the buffer will grow to {{@link #ARRAY_MAX_SIZE}. Can be + * smaller than the current buffer size ( which will not shrink ). When the limit is reached, the buffer will be + * flushed (if out is set) or throw exception. + * + * @param limit The new limit + */ + public void setLimit(int limit) { + this.limit = limit; + } + + + public int getLimit() { + return limit; + } + + + protected int getLimitInternal() { + if (limit > 0) { + return limit; + } else { + return ARRAY_MAX_SIZE; + } + } + + + /** + * @return the start position of the data in the buffer + */ + public int getStart() { + return start; + } + + + public int getEnd() { + return end; + } + + + public void setEnd(int i) { + end = i; + } + + + // TODO: Deprecate offset and use start + + public int getOffset() { + return start; + } + + + public void setOffset(int off) { + if (end < off) { + end = off; + } + start = off; + } + + + /** + * @return the length of the data in the buffer + */ + public int getLength() { + return end - start; + } + + + public boolean isNull() { + if (end > 0) { + return false; + } + return !isSet; + } + + + public int indexOf(String src, int srcOff, int srcLen, int myOff) { + char first = src.charAt(srcOff); + + // Look for first char + int srcEnd = srcOff + srcLen; + + mainLoop: for (int i = myOff + start; i <= (end - srcLen); i++) { + if (getBufferElement(i) != first) { + continue; + } + // found first char, now look for a match + int myPos = i + 1; + for (int srcPos = srcOff + 1; srcPos < srcEnd;) { + if (getBufferElement(myPos++) != src.charAt(srcPos++)) { + continue mainLoop; + } + } + return i - start; // found it + } + return -1; + } + + + /** + * Resets the chunk to an uninitialized state. + */ + public void recycle() { + hasHashCode = false; + isSet = false; + start = 0; + end = 0; + } + + + @Override + public int hashCode() { + if (hasHashCode) { + return hashCode; + } + int code = 0; + + code = hash(); + hashCode = code; + hasHashCode = true; + return code; + } + + + public int hash() { + int code = 0; + for (int i = start; i < end; i++) { + code = code * 37 + getBufferElement(i); + } + return code; + } + + + protected abstract int getBufferElement(int index); +} diff --git a/java/org/apache/tomcat/util/buf/Ascii.java b/java/org/apache/tomcat/util/buf/Ascii.java new file mode 100644 index 0000000..2c6d1ee --- /dev/null +++ b/java/org/apache/tomcat/util/buf/Ascii.java @@ -0,0 +1,106 @@ +/* + * 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.tomcat.util.buf; + +/** + * This class implements some basic ASCII character handling functions. + * + * @author dac@eng.sun.com + * @author James Todd [gonzo@eng.sun.com] + */ +public final class Ascii { + /* + * Character translation tables. + */ + private static final byte[] toLower = new byte[256]; + + /* + * Character type tables. + */ + private static final boolean[] isDigit = new boolean[256]; + + private static final long OVERFLOW_LIMIT = Long.MAX_VALUE / 10; + + /* + * Initialize character translation and type tables. + */ + static { + for (int i = 0; i < 256; i++) { + toLower[i] = (byte) i; + } + + for (int lc = 'a'; lc <= 'z'; lc++) { + int uc = lc + 'A' - 'a'; + + toLower[uc] = (byte) lc; + } + + for (int d = '0'; d <= '9'; d++) { + isDigit[d] = true; + } + } + + /** + * Returns the lower case equivalent of the specified ASCII character. + * + * @param c The char + * + * @return the lower case equivalent char + */ + public static int toLower(int c) { + return toLower[c & 0xff] & 0xff; + } + + /** + * @return true if the specified ASCII character is a digit. + * + * @param c The char + */ + private static boolean isDigit(int c) { + return isDigit[c & 0xff]; + } + + /** + * Parses an unsigned long from the specified subarray of bytes. + * + * @param b the bytes to parse + * @param off the start offset of the bytes + * @param len the length of the bytes + * + * @return the long value + * + * @exception NumberFormatException if the long format was invalid + */ + public static long parseLong(byte[] b, int off, int len) throws NumberFormatException { + int c; + + if (b == null || len <= 0 || !isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + long n = c - '0'; + while (--len > 0) { + if (isDigit(c = b[off++]) && (n < OVERFLOW_LIMIT || (n == OVERFLOW_LIMIT && (c - '0') < 8))) { + n = n * 10 + c - '0'; + } else { + throw new NumberFormatException(); + } + } + + return n; + } +} diff --git a/java/org/apache/tomcat/util/buf/Asn1Parser.java b/java/org/apache/tomcat/util/buf/Asn1Parser.java new file mode 100644 index 0000000..1d88b11 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/Asn1Parser.java @@ -0,0 +1,181 @@ +/* + * 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.tomcat.util.buf; + +import java.math.BigInteger; +import java.util.ArrayDeque; +import java.util.Deque; + +import org.apache.tomcat.util.res.StringManager; + +/** + * This is a very basic ASN.1 parser that provides the limited functionality required by Tomcat. It is a long way from a + * complete parser. + *

    + * TODO: Consider extending/re-writing this parser and refactoring the SpnegoTokenFixer to use it. + */ +public class Asn1Parser { + + private static final StringManager sm = StringManager.getManager(Asn1Parser.class); + + public static final int TAG_INTEGER = 0x02; + public static final int TAG_OCTET_STRING = 0x04; + public static final int TAG_NULL = 0x05; + public static final int TAG_OID = 0x06; + public static final int TAG_SEQUENCE = 0x30; + public static final int TAG_ATTRIBUTE_BASE = 0xA0; + + private final byte[] source; + + private int pos = 0; + + /* + * This is somewhat of a hack to work around the simplified design of the parsing API that could result in ambiguous + * results when nested sequences were optional. Checking the current nesting level of sequence tags enables a user + * of the parser to determine if an optional sequence is present or not. + * + * See https://bz.apache.org/bugzilla/show_bug.cgi?id=67675#c24 + */ + private Deque nestedSequenceEndPositions = new ArrayDeque<>(); + + + public Asn1Parser(byte[] source) { + this.source = source; + } + + + public boolean eof() { + return pos == source.length; + } + + + public int peekTag() { + return source[pos] & 0xFF; + } + + + public void parseTagSequence() { + /* + * Check to see if the parser has completely parsed, based on end position for the sequence, any previous + * sequences and remove those sequences from the sequence nesting tracking mechanism if they have been + * completely parsed. + */ + while (nestedSequenceEndPositions.size() > 0) { + if (nestedSequenceEndPositions.peekLast().intValue() <= pos) { + nestedSequenceEndPositions.pollLast(); + } else { + break; + } + } + // Add the new sequence to the sequence nesting tracking mechanism. + parseTag(TAG_SEQUENCE); + nestedSequenceEndPositions.addLast(Integer.valueOf(-1)); + } + + + public void parseTag(int tag) { + int value = next(); + if (value != tag) { + throw new IllegalArgumentException( + sm.getString("asn1Parser.tagMismatch", Integer.valueOf(tag), Integer.valueOf(value))); + } + } + + + public void parseFullLength() { + int len = parseLength(); + if (len + pos != source.length) { + throw new IllegalArgumentException(sm.getString("asn1Parser.lengthInvalid", Integer.valueOf(len), + Integer.valueOf(source.length - pos))); + } + } + + + public int parseLength() { + int len = next(); + if (len > 127) { + int bytes = len - 128; + len = 0; + for (int i = 0; i < bytes; i++) { + len = len << 8; + len = len + next(); + } + } + /* + * If this is the first length parsed after a sequence has been added to the sequence nesting tracking mechansim + * it must be the length of the sequence so update the entry to record the end position of the sequence. Note + * that position recorded is actually the start of the first element after the sequence ends. + */ + if (nestedSequenceEndPositions.peekLast() != null && nestedSequenceEndPositions.peekLast().intValue() == -1) { + nestedSequenceEndPositions.pollLast(); + nestedSequenceEndPositions.addLast(Integer.valueOf(pos + len)); + } + return len; + } + + + public BigInteger parseInt() { + byte[] val = parseBytes(TAG_INTEGER); + return new BigInteger(val); + } + + + public byte[] parseOctetString() { + return parseBytes(TAG_OCTET_STRING); + } + + + public void parseNull() { + parseBytes(TAG_NULL); + } + + + public byte[] parseOIDAsBytes() { + return parseBytes(TAG_OID); + } + + + public byte[] parseAttributeAsBytes(int index) { + return parseBytes(TAG_ATTRIBUTE_BASE + index); + } + + + private byte[] parseBytes(int tag) { + parseTag(tag); + int len = parseLength(); + byte[] result = new byte[len]; + System.arraycopy(source, pos, result, 0, result.length); + pos += result.length; + return result; + } + + + public void parseBytes(byte[] dest) { + System.arraycopy(source, pos, dest, 0, dest.length); + pos += dest.length; + } + + + private int next() { + return source[pos++] & 0xFF; + } + + + public int getNestedSequenceLevel() { + return nestedSequenceEndPositions.size(); + } +} diff --git a/java/org/apache/tomcat/util/buf/Asn1Writer.java b/java/org/apache/tomcat/util/buf/Asn1Writer.java new file mode 100644 index 0000000..57b36c3 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/Asn1Writer.java @@ -0,0 +1,94 @@ +/* + * 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.tomcat.util.buf; + +public class Asn1Writer { + + public static byte[] writeSequence(byte[]... components) { + int len = 0; + for (byte[] component : components) { + len += component.length; + } + + byte[] combined = new byte[len]; + int pos = 0; + for (byte[] component : components) { + System.arraycopy(component, 0, combined, pos, component.length); + pos += component.length; + } + + return writeTag((byte) 0x30, combined); + } + + + public static byte[] writeInteger(int value) { + // How many bytes required to write the value? No more than 4 for int. + int valueSize = 1; + while ((value >> (valueSize * 8)) > 0) { + valueSize++; + } + + byte[] valueBytes = new byte[valueSize]; + int i = 0; + while (valueSize > 0) { + valueBytes[i] = (byte) (value >> (8 * (valueSize - 1))); + value = value >> 8; + valueSize--; + i++; + } + + return writeTag((byte) 0x02, valueBytes); + } + + public static byte[] writeOctetString(byte[] data) { + return writeTag((byte) 0x04, data); + } + + public static byte[] writeTag(byte tagId, byte[] data) { + int dataSize = data.length; + // How many bytes to write the length? + int lengthSize = 1; + if (dataSize > 127) { + // 1 byte we have is now used to record how many bytes we need to + // record a length > 127 + // Result is lengthSize = 1 + number of bytes to record length + do { + lengthSize++; + } while ((dataSize >> (lengthSize * 8)) > 0); + } + + // 1 for tag + lengthSize + dataSize + byte[] result = new byte[1 + lengthSize + dataSize]; + result[0] = tagId; + if (dataSize < 128) { + result[1] = (byte) dataSize; + } else { + // lengthSize is 1 + number of bytes for length + result[1] = (byte) (127 + lengthSize); + int i = lengthSize; + while (dataSize > 0) { + result[i] = (byte) (dataSize & 0xFF); + dataSize = dataSize >> 8; + i--; + } + } + + System.arraycopy(data, 0, result, 1 + lengthSize, data.length); + + return result; + } +} diff --git a/java/org/apache/tomcat/util/buf/B2CConverter.java b/java/org/apache/tomcat/util/buf/B2CConverter.java new file mode 100644 index 0000000..68cbd05 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/B2CConverter.java @@ -0,0 +1,259 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.util.Locale; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * NIO based character decoder. + */ +public class B2CConverter { + + private static final Log log = LogFactory.getLog(B2CConverter.class); + private static final StringManager sm = StringManager.getManager(B2CConverter.class); + + private static final CharsetCache charsetCache = new CharsetCache(); + + + // Protected so unit tests can use it + protected static final int LEFTOVER_SIZE = 9; + + /** + * Obtain the Charset for the given encoding + * + * @param enc The name of the encoding for the required charset + * + * @return The Charset corresponding to the requested encoding + * + * @throws UnsupportedEncodingException If the requested Charset is not available + */ + public static Charset getCharset(String enc) throws UnsupportedEncodingException { + + // Encoding names should all be ASCII + String lowerCaseEnc = enc.toLowerCase(Locale.ENGLISH); + + Charset charset = charsetCache.getCharset(lowerCaseEnc); + + if (charset == null) { + // Pre-population of the cache means this must be invalid + throw new UnsupportedEncodingException(sm.getString("b2cConverter.unknownEncoding", lowerCaseEnc)); + } + return charset; + } + + + private final CharsetDecoder decoder; + private ByteBuffer bb = null; + private CharBuffer cb = null; + + /** + * Leftover buffer used for incomplete characters. + */ + private final ByteBuffer leftovers; + + public B2CConverter(Charset charset) { + this(charset, false); + } + + public B2CConverter(Charset charset, boolean replaceOnError) { + byte[] left = new byte[LEFTOVER_SIZE]; + leftovers = ByteBuffer.wrap(left); + CodingErrorAction action; + if (replaceOnError) { + action = CodingErrorAction.REPLACE; + } else { + action = CodingErrorAction.REPORT; + } + decoder = charset.newDecoder(); + decoder.onMalformedInput(action); + decoder.onUnmappableCharacter(action); + } + + /** + * Reset the decoder state. + */ + public void recycle() { + try { + decoder.reset(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("b2cConverter.decoderResetFail", decoder.charset()), t); + } + leftovers.position(0); + } + + /** + * Convert the given bytes to characters. + * + * @param bc byte input + * @param cc char output + * @param endOfInput Is this all of the available data + * + * @throws IOException If the conversion can not be completed + */ + public void convert(ByteChunk bc, CharChunk cc, boolean endOfInput) throws IOException { + if ((bb == null) || (bb.array() != bc.getBuffer())) { + // Create a new byte buffer if anything changed + bb = ByteBuffer.wrap(bc.getBuffer(), bc.getStart(), bc.getLength()); + } else { + // Initialize the byte buffer + bb.limit(bc.getEnd()); + bb.position(bc.getStart()); + } + if ((cb == null) || (cb.array() != cc.getBuffer())) { + // Create a new char buffer if anything changed + cb = CharBuffer.wrap(cc.getBuffer(), cc.getEnd(), cc.getBuffer().length - cc.getEnd()); + } else { + // Initialize the char buffer + cb.limit(cc.getBuffer().length); + cb.position(cc.getEnd()); + } + CoderResult result = null; + // Parse leftover if any are present + if (leftovers.position() > 0) { + int pos = cb.position(); + // Loop until one char is decoded or there is a decoder error + do { + leftovers.put(bc.subtractB()); + leftovers.flip(); + result = decoder.decode(leftovers, cb, endOfInput); + leftovers.position(leftovers.limit()); + leftovers.limit(leftovers.array().length); + } while (result.isUnderflow() && (cb.position() == pos)); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } + bb.position(bc.getStart()); + leftovers.position(0); + } + // Do the decoding and get the results into the byte chunk and the char + // chunk + result = decoder.decode(bb, cb, endOfInput); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } else if (result.isOverflow()) { + // Propagate current positions to the byte chunk and char chunk, if + // this continues the char buffer will get resized + bc.setOffset(bb.position()); + cc.setEnd(cb.position()); + } else if (result.isUnderflow()) { + // Propagate current positions to the byte chunk and char chunk + bc.setOffset(bb.position()); + cc.setEnd(cb.position()); + // Put leftovers in the leftovers byte buffer + if (bc.getLength() > 0) { + leftovers.limit(leftovers.array().length); + leftovers.position(bc.getLength()); + bc.subtract(leftovers.array(), 0, bc.getLength()); + } + } + } + + /** + * Convert the given bytes to characters. + * + * @param bc byte input + * @param cc char output + * @param ic byte input channel + * @param endOfInput Is this all of the available data + * + * @throws IOException If the conversion can not be completed + */ + public void convert(ByteBuffer bc, CharBuffer cc, ByteChunk.ByteInputChannel ic, boolean endOfInput) + throws IOException { + if ((bb == null) || (bb.array() != bc.array())) { + // Create a new byte buffer if anything changed + bb = ByteBuffer.wrap(bc.array(), bc.arrayOffset() + bc.position(), bc.remaining()); + } else { + // Initialize the byte buffer + bb.limit(bc.limit()); + bb.position(bc.position()); + } + if ((cb == null) || (cb.array() != cc.array())) { + // Create a new char buffer if anything changed + cb = CharBuffer.wrap(cc.array(), cc.limit(), cc.capacity() - cc.limit()); + } else { + // Initialize the char buffer + cb.limit(cc.capacity()); + cb.position(cc.limit()); + } + CoderResult result = null; + // Parse leftover if any are present + if (leftovers.position() > 0) { + int pos = cb.position(); + // Loop until one char is decoded or there is a decoder error + do { + byte chr; + if (bc.remaining() == 0) { + int n = ic.realReadBytes(); + chr = n < 0 ? -1 : bc.get(); + } else { + chr = bc.get(); + } + leftovers.put(chr); + leftovers.flip(); + result = decoder.decode(leftovers, cb, endOfInput); + leftovers.position(leftovers.limit()); + leftovers.limit(leftovers.array().length); + } while (result.isUnderflow() && (cb.position() == pos)); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } + bb.position(bc.position()); + leftovers.position(0); + } + // Do the decoding and get the results into the byte chunk and the char + // chunk + result = decoder.decode(bb, cb, endOfInput); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } else if (result.isOverflow()) { + // Propagate current positions to the byte chunk and char chunk, if + // this continues the char buffer will get resized + bc.position(bb.position()); + cc.limit(cb.position()); + } else if (result.isUnderflow()) { + // Propagate current positions to the byte chunk and char chunk + bc.position(bb.position()); + cc.limit(cb.position()); + // Put leftovers in the leftovers byte buffer + if (bc.remaining() > 0) { + leftovers.limit(leftovers.array().length); + leftovers.position(bc.remaining()); + bc.get(leftovers.array(), 0, bc.remaining()); + } + } + } + + + public Charset getCharset() { + return decoder.charset(); + } +} diff --git a/java/org/apache/tomcat/util/buf/ByteBufferHolder.java b/java/org/apache/tomcat/util/buf/ByteBufferHolder.java new file mode 100644 index 0000000..f50dded --- /dev/null +++ b/java/org/apache/tomcat/util/buf/ByteBufferHolder.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Simple wrapper for a {@link ByteBuffer} that remembers if the buffer has been flipped or not. + */ +public class ByteBufferHolder { + + private final ByteBuffer buf; + private final AtomicBoolean flipped; + + public ByteBufferHolder(ByteBuffer buf, boolean flipped) { + this.buf = buf; + this.flipped = new AtomicBoolean(flipped); + } + + + public ByteBuffer getBuf() { + return buf; + } + + + public boolean isFlipped() { + return flipped.get(); + } + + + public boolean flip() { + if (flipped.compareAndSet(false, true)) { + buf.flip(); + return true; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/buf/ByteBufferUtils.java b/java/org/apache/tomcat/util/buf/ByteBufferUtils.java new file mode 100644 index 0000000..db1f682 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/ByteBufferUtils.java @@ -0,0 +1,125 @@ +/* + * 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.tomcat.util.buf; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class ByteBufferUtils { + + private static final StringManager sm = StringManager.getManager(ByteBufferUtils.class); + private static final Log log = LogFactory.getLog(ByteBufferUtils.class); + + private static final Object unsafe; + private static final Method cleanerMethod; + private static final Method cleanMethod; + private static final Method invokeCleanerMethod; + + static { + ByteBuffer tempBuffer = ByteBuffer.allocateDirect(0); + Method cleanerMethodLocal = null; + Method cleanMethodLocal = null; + Object unsafeLocal = null; + Method invokeCleanerMethodLocal = null; + try { + Class clazz = Class.forName("sun.misc.Unsafe"); + Field theUnsafe = clazz.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + unsafeLocal = theUnsafe.get(null); + invokeCleanerMethodLocal = clazz.getMethod("invokeCleaner", ByteBuffer.class); + invokeCleanerMethodLocal.invoke(unsafeLocal, tempBuffer); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | + SecurityException | ClassNotFoundException | NoSuchFieldException e) { + log.warn(sm.getString("byteBufferUtils.cleaner"), e); + unsafeLocal = null; + invokeCleanerMethodLocal = null; + } + cleanerMethod = cleanerMethodLocal; + cleanMethod = cleanMethodLocal; + unsafe = unsafeLocal; + invokeCleanerMethod = invokeCleanerMethodLocal; + } + + private ByteBufferUtils() { + // Hide the default constructor since this is a utility class. + } + + + /** + * Expands buffer to the given size unless it is already as big or bigger. Buffers are assumed to be in 'write to' + * mode since there would be no need to expand a buffer while it was in 'read from' mode. + * + * @param in Buffer to expand + * @param newSize The size t which the buffer should be expanded + * + * @return The expanded buffer with any data from the input buffer copied in to it or the original buffer if there + * was no need for expansion + */ + public static ByteBuffer expand(ByteBuffer in, int newSize) { + if (in.capacity() >= newSize) { + return in; + } + + ByteBuffer out; + boolean direct = false; + if (in.isDirect()) { + out = ByteBuffer.allocateDirect(newSize); + direct = true; + } else { + out = ByteBuffer.allocate(newSize); + } + + // Copy data + in.flip(); + out.put(in); + + if (direct) { + cleanDirectBuffer(in); + } + + return out; + } + + public static void cleanDirectBuffer(ByteBuffer buf) { + if (cleanMethod != null) { + try { + cleanMethod.invoke(cleanerMethod.invoke(buf)); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | + SecurityException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("byteBufferUtils.cleaner"), e); + } + } + } else if (invokeCleanerMethod != null) { + try { + invokeCleanerMethod.invoke(unsafe, buf); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | + SecurityException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("byteBufferUtils.cleaner"), e); + } + } + } + } + +} diff --git a/java/org/apache/tomcat/util/buf/ByteChunk.java b/java/org/apache/tomcat/util/buf/ByteChunk.java new file mode 100644 index 0000000..e297121 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/ByteChunk.java @@ -0,0 +1,910 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; + +/** + * This class is used to represent a chunk of bytes, and utilities to manipulate byte[]. + *

    + * The buffer can be modified and used for both input and output. + *

    + * There are 2 modes: The chunk can be associated with a sink - ByteInputChannel or ByteOutputChannel, which will be + * used when the buffer is empty (on input) or filled (on output). For output, it can also grow. This operating mode is + * selected by calling setLimit() or allocate(initial, limit) with limit != -1. + *

    + * Various search and append method are defined - similar with String and StringBuffer, but operating on bytes. + *

    + * This is important because it allows processing the http headers directly on the received bytes, without converting to + * chars and Strings until the strings are needed. In addition, the charset is determined later, from headers or user + * code. + *

    + * In a server it is very important to be able to operate on + * the original byte[] without converting everything to chars. + * Some protocols are ASCII only, and some allow different + * non-UNICODE encodings. The encoding is not known beforehand, + * and can even change during the execution of the protocol. + * ( for example a multipart message may have parts with different + * encoding ) + *

    + * For HTTP it is not very clear how the encoding of RequestURI + * and mime values can be determined, but it is a great advantage + * to be able to parse the request without converting to string. + * + * @author dac@sun.com + * @author James Todd [gonzo@sun.com] + * @author Costin Manolache + * @author Remy Maucherat + */ +public final class ByteChunk extends AbstractChunk { + + private static final long serialVersionUID = 1L; + + /** + * Input interface, used when the buffer is empty. Same as java.nio.channels.ReadableByteChannel + */ + public interface ByteInputChannel { + + /** + * Read new bytes. + * + * @return The number of bytes read + * + * @throws IOException If an I/O error occurs during reading + */ + int realReadBytes() throws IOException; + } + + /** + * When we need more space we'll either grow the buffer ( up to the limit ) or send it to a channel. Same as + * java.nio.channel.WritableByteChannel. + */ + public interface ByteOutputChannel { + + /** + * Send the bytes ( usually the internal conversion buffer ). Expect 8k output if the buffer is full. + * + * @param buf bytes that will be written + * @param off offset in the bytes array + * @param len length that will be written + * + * @throws IOException If an I/O occurs while writing the bytes + */ + void realWriteBytes(byte buf[], int off, int len) throws IOException; + + + /** + * Send the bytes ( usually the internal conversion buffer ). Expect 8k output if the buffer is full. + * + * @param from bytes that will be written + * + * @throws IOException If an I/O occurs while writing the bytes + */ + void realWriteBytes(ByteBuffer from) throws IOException; + } + + // -------------------- + + /** + * Default encoding used to convert to strings. It should be UTF8, as most standards seem to converge, but the + * servlet API requires 8859_1, and this object is used mostly for servlets. + */ + public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1; + + private transient Charset charset; + + // byte[] + private byte[] buff; + + // transient as serialization is primarily for values via, e.g. JMX + private transient ByteInputChannel in = null; + private transient ByteOutputChannel out = null; + + + /** + * Creates a new, uninitialized ByteChunk object. + */ + public ByteChunk() { + } + + + public ByteChunk(int initial) { + allocate(initial, -1); + } + + + private void writeObject(ObjectOutputStream oos) throws IOException { + oos.defaultWriteObject(); + oos.writeUTF(getCharset().name()); + } + + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + this.charset = Charset.forName(ois.readUTF()); + } + + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + + @Override + public void recycle() { + super.recycle(); + charset = null; + } + + + // -------------------- Setup -------------------- + + public void allocate(int initial, int limit) { + if (buff == null || buff.length < initial) { + buff = new byte[initial]; + } + setLimit(limit); + start = 0; + end = 0; + isSet = true; + hasHashCode = false; + } + + + /** + * Sets the buffer to the specified subarray of bytes. + * + * @param b the ascii bytes + * @param off the start offset of the bytes + * @param len the length of the bytes + */ + public void setBytes(byte[] b, int off, int len) { + buff = b; + start = off; + end = start + len; + isSet = true; + hasHashCode = false; + } + + + public void setCharset(Charset charset) { + this.charset = charset; + } + + + public Charset getCharset() { + if (charset == null) { + charset = DEFAULT_CHARSET; + } + return charset; + } + + + /** + * @return the buffer. + */ + public byte[] getBytes() { + return getBuffer(); + } + + + /** + * @return the buffer. + */ + public byte[] getBuffer() { + return buff; + } + + + /** + * When the buffer is empty, read the data from the input channel. + * + * @param in The input channel + */ + public void setByteInputChannel(ByteInputChannel in) { + this.in = in; + } + + + /** + * When the buffer is full, write the data to the output channel. Also used when large amount of data is appended. + * If not set, the buffer will grow to the limit. + * + * @param out The output channel + */ + public void setByteOutputChannel(ByteOutputChannel out) { + this.out = out; + } + + + // -------------------- Adding data to the buffer -------------------- + + public void append(byte b) throws IOException { + makeSpace(1); + int limit = getLimitInternal(); + + // couldn't make space + if (end >= limit) { + flushBuffer(); + } + buff[end++] = b; + } + + + public void append(ByteChunk src) throws IOException { + append(src.getBytes(), src.getStart(), src.getLength()); + } + + + /** + * Add data to the buffer. + * + * @param src Bytes array + * @param off Offset + * @param len Length + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void append(byte src[], int off, int len) throws IOException { + // will grow, up to limit + makeSpace(len); + int limit = getLimitInternal(); + + // Optimize on a common case. + // If the buffer is empty and the source is going to fill up all the + // space in buffer, may as well write it directly to the output, + // and avoid an extra copy + if (len == limit && end == start && out != null) { + out.realWriteBytes(src, off, len); + return; + } + + // if we are below the limit + if (len <= limit - end) { + System.arraycopy(src, off, buff, end, len); + end += len; + return; + } + + // Need more space than we can afford, need to flush buffer. + + // The buffer is already at (or bigger than) limit. + + // We chunk the data into slices fitting in the buffer limit, although + // if the data is written directly if it doesn't fit. + + int avail = limit - end; + System.arraycopy(src, off, buff, end, avail); + end += avail; + + flushBuffer(); + + int remain = len - avail; + + while (remain > (limit - end)) { + out.realWriteBytes(src, (off + len) - remain, limit - end); + remain = remain - (limit - end); + } + + System.arraycopy(src, (off + len) - remain, buff, end, remain); + end += remain; + } + + + /** + * Add data to the buffer. + * + * @param from the ByteBuffer with the data + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void append(ByteBuffer from) throws IOException { + int len = from.remaining(); + + // will grow, up to limit + makeSpace(len); + int limit = getLimitInternal(); + + // Optimize on a common case. + // If the buffer is empty and the source is going to fill up all the + // space in buffer, may as well write it directly to the output, + // and avoid an extra copy + if (len == limit && end == start && out != null) { + out.realWriteBytes(from); + from.position(from.limit()); + return; + } + // if we have limit and we're below + if (len <= limit - end) { + // makeSpace will grow the buffer to the limit, + // so we have space + from.get(buff, end, len); + end += len; + return; + } + + // need more space than we can afford, need to flush + // buffer + + // the buffer is already at ( or bigger than ) limit + + // We chunk the data into slices fitting in the buffer limit, although + // if the data is written directly if it doesn't fit + + int avail = limit - end; + from.get(buff, end, avail); + end += avail; + + flushBuffer(); + + int fromLimit = from.limit(); + int remain = len - avail; + avail = limit - end; + while (remain >= avail) { + from.limit(from.position() + avail); + out.realWriteBytes(from); + from.position(from.limit()); + remain = remain - avail; + } + + from.limit(fromLimit); + from.get(buff, end, remain); + end += remain; + } + + + // -------------------- Removing data from the buffer -------------------- + + public int subtract() throws IOException { + if (checkEof()) { + return -1; + } + return buff[start++] & 0xFF; + } + + public byte subtractB() throws IOException { + if (checkEof()) { + return -1; + } + return buff[start++]; + } + + + public int subtract(byte dest[], int off, int len) throws IOException { + if (checkEof()) { + return -1; + } + int n = len; + if (len > getLength()) { + n = getLength(); + } + System.arraycopy(buff, start, dest, off, n); + start += n; + return n; + } + + + /** + * Transfers bytes from the buffer to the specified ByteBuffer. After the operation the position of the ByteBuffer + * will be returned to the one before the operation, the limit will be the position incremented by the number of the + * transferred bytes. + * + * @param to the ByteBuffer into which bytes are to be written. + * + * @return an integer specifying the actual number of bytes read, or -1 if the end of the stream is reached + * + * @throws IOException if an input or output exception has occurred + */ + public int subtract(ByteBuffer to) throws IOException { + if (checkEof()) { + return -1; + } + int n = Math.min(to.remaining(), getLength()); + to.put(buff, start, n); + to.limit(to.position()); + to.position(to.position() - n); + start += n; + return n; + } + + + private boolean checkEof() throws IOException { + if ((end - start) == 0) { + if (in == null) { + return true; + } + int n = in.realReadBytes(); + if (n < 0) { + return true; + } + } + return false; + } + + + /** + * Send the buffer to the sink. Called by append() when the limit is reached. You can also call it explicitly to + * force the data to be written. + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void flushBuffer() throws IOException { + // assert out!=null + if (out == null) { + throw new BufferOverflowException( + sm.getString("chunk.overflow", Integer.valueOf(getLimit()), Integer.valueOf(buff.length))); + } + out.realWriteBytes(buff, start, end - start); + end = start; + } + + + /** + * Make space for len bytes. If len is small, allocate a reserve space too. Never grow bigger than the limit or + * {@link AbstractChunk#ARRAY_MAX_SIZE}. + * + * @param count The size + */ + public void makeSpace(int count) { + byte[] tmp = null; + + int limit = getLimitInternal(); + + long newSize; + long desiredSize = end + count; + + // Can't grow above the limit + if (desiredSize > limit) { + desiredSize = limit; + } + + if (buff == null) { + if (desiredSize < 256) { + desiredSize = 256; // take a minimum + } + buff = new byte[(int) desiredSize]; + } + + // limit < buf.length (the buffer is already big) + // or we already have space + if (desiredSize <= buff.length) { + return; + } + // grow in larger chunks + if (desiredSize < 2L * buff.length) { + newSize = buff.length * 2L; + } else { + newSize = buff.length * 2L + count; + } + + if (newSize > limit) { + newSize = limit; + } + tmp = new byte[(int) newSize]; + + // Compacts buffer + System.arraycopy(buff, start, tmp, 0, end - start); + buff = tmp; + tmp = null; + end = end - start; + start = 0; + } + + + // -------------------- Conversion and getters -------------------- + + @Override + public String toString() { + try { + return toString(CodingErrorAction.REPLACE, CodingErrorAction.REPLACE); + } catch (CharacterCodingException e) { + // Unreachable code. Use of REPLACE above means the exception will never be thrown. + throw new IllegalStateException(e); + } + } + + + public String toString(CodingErrorAction malformedInputAction, CodingErrorAction unmappableCharacterAction) + throws CharacterCodingException { + if (isNull()) { + return null; + } else if (end - start == 0) { + return ""; + } + return StringCache.toString(this, malformedInputAction, unmappableCharacterAction); + } + + + /** + * Converts the current content of the byte buffer to a String using the configured character set. + * + * @return The result of converting the bytes to a String + * + * @deprecated Unused. This method will be removed in Tomcat 11 onwards. + */ + @Deprecated + public String toStringInternal() { + try { + return toStringInternal(CodingErrorAction.REPLACE, CodingErrorAction.REPLACE); + } catch (CharacterCodingException e) { + // Unreachable code. Use of REPLACE above means the exception will never be thrown. + throw new IllegalStateException(e); + } + } + + + /** + * Converts the current content of the byte buffer to a String using the configured character set. + * + * @param malformedInputAction Action to take if the input is malformed + * @param unmappableCharacterAction Action to take if a byte sequence can't be mapped to a character + * + * @return The result of converting the bytes to a String + * + * @throws CharacterCodingException If an error occurs during the conversion + */ + public String toStringInternal(CodingErrorAction malformedInputAction, CodingErrorAction unmappableCharacterAction) + throws CharacterCodingException { + if (charset == null) { + charset = DEFAULT_CHARSET; + } + // new String(byte[], int, int, Charset) takes a defensive copy of the + // entire byte array. This is expensive if only a small subset of the + // bytes will be used. The code below is from Apache Harmony. + CharBuffer cb; + if (malformedInputAction == CodingErrorAction.REPLACE && unmappableCharacterAction == CodingErrorAction.REPLACE) { + cb = charset.decode(ByteBuffer.wrap(buff, start, end - start)); + } else { + cb = charset.newDecoder().onMalformedInput(malformedInputAction) + .onUnmappableCharacter(unmappableCharacterAction).decode(ByteBuffer.wrap(buff, start, end - start)); + } + return new String(cb.array(), cb.arrayOffset(), cb.length()); + } + + + public long getLong() { + return Ascii.parseLong(buff, start, end - start); + } + + + // -------------------- equals -------------------- + + @Override + public boolean equals(Object obj) { + if (obj instanceof ByteChunk) { + return equals((ByteChunk) obj); + } + return false; + } + + + /** + * Compares the message bytes to the specified String object. + *

    + * NOTE: This only works for characters in the range 0-127. + * + * @param s the String to compare + * + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(String s) { + byte[] b = buff; + int len = end - start; + if (b == null || len != s.length()) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (b[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + + /** + * Compares the message bytes to the specified String object. + *

    + * NOTE: This only works for characters in the range 0-127. + * + * @param s the String to compare + * + * @return true if the comparison succeeded, false otherwise + */ + public boolean equalsIgnoreCase(String s) { + byte[] b = buff; + int len = end - start; + if (b == null || len != s.length()) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (Ascii.toLower(b[off++]) != Ascii.toLower(s.charAt(i))) { + return false; + } + } + return true; + } + + + public boolean equals(ByteChunk bb) { + return equals(bb.getBytes(), bb.getStart(), bb.getLength()); + } + + + public boolean equals(byte b2[], int off2, int len2) { + byte b1[] = buff; + if (b1 == null && b2 == null) { + return true; + } + + int len = end - start; + if (len != len2 || b1 == null || b2 == null) { + return false; + } + + int off1 = start; + + while (len-- > 0) { + if (b1[off1++] != b2[off2++]) { + return false; + } + } + return true; + } + + + public boolean equalsIgnoreCase(byte b2[], int off2, int len2) { + byte b1[] = buff; + if (b1 == null && b2 == null) { + return true; + } + + int len = end - start; + if (len != len2 || b1 == null || b2 == null) { + return false; + } + + int off1 = start; + + while (len-- > 0) { + if (Ascii.toLower(b1[off1++]) != Ascii.toLower(b2[off2++])) { + return false; + } + } + return true; + } + + + public boolean equals(CharChunk cc) { + return equals(cc.getChars(), cc.getStart(), cc.getLength()); + } + + + /** + * Compares the message bytes to the specified char array. + *

    + * NOTE: This only works for characters in the range 0-127. + * + * @param c2 the array to compare to + * @param off2 offset + * @param len2 length + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(char c2[], int off2, int len2) { + byte b1[] = buff; + if (c2 == null && b1 == null) { + return true; + } + + if (b1 == null || c2 == null || end - start != len2) { + return false; + } + int off1 = start; + int len = end - start; + + while (len-- > 0) { + if ((char) b1[off1++] != c2[off2++]) { + return false; + } + } + return true; + } + + + /** + * Returns true if the buffer starts with the specified string when tested in a case sensitive manner. + *

    + * NOTE: This only works for characters in the range 0-127. + * + * @param s the string + * @param pos The position + * + * @return true if the start matches + */ + public boolean startsWith(String s, int pos) { + byte[] b = buff; + int len = s.length(); + if (b == null || len + pos > end - start) { + return false; + } + int off = start + pos; + for (int i = 0; i < len; i++) { + if (b[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + + /** + * Returns true if the buffer starts with the specified string when tested in a case insensitive manner. + *

    + * NOTE: This only works for characters in the range 0-127. + * + * @param s the string + * @param pos The position + * + * @return true if the start matches + */ + public boolean startsWithIgnoreCase(String s, int pos) { + byte[] b = buff; + int len = s.length(); + if (b == null || len + pos > end - start) { + return false; + } + int off = start + pos; + for (int i = 0; i < len; i++) { + if (Ascii.toLower(b[off++]) != Ascii.toLower(s.charAt(i))) { + return false; + } + } + return true; + } + + + @Override + protected int getBufferElement(int index) { + return buff[index]; + } + + + /** + * Returns the first instance of the given character in this ByteChunk starting at the specified byte. If the + * character is not found, -1 is returned. + *

    + * NOTE: This only works for characters in the range 0-127. + * + * @param c The character + * @param starting The start position + * + * @return The position of the first instance of the character or -1 if the character is not found. + */ + public int indexOf(char c, int starting) { + int ret = indexOf(buff, start + starting, end, c); + return (ret >= start) ? ret - start : -1; + } + + + /** + * Returns the first instance of the given character in the given byte array between the specified start and end. + *

    + * NOTE: This only works for characters in the range 0-127. + * + * @param bytes The array to search + * @param start The point to start searching from in the array + * @param end The point to stop searching in the array + * @param s The character to search for + * + * @return The position of the first instance of the character or -1 if the character is not found. + */ + public static int indexOf(byte bytes[], int start, int end, char s) { + int offset = start; + + while (offset < end) { + byte b = bytes[offset]; + if (b == s) { + return offset; + } + offset++; + } + return -1; + } + + + /** + * Returns the first instance of the given byte in the byte array between the specified start and end. + * + * @param bytes The byte array to search + * @param start The point to start searching from in the byte array + * @param end The point to stop searching in the byte array + * @param b The byte to search for + * + * @return The position of the first instance of the byte or -1 if the byte is not found. + */ + public static int findByte(byte bytes[], int start, int end, byte b) { + int offset = start; + while (offset < end) { + if (bytes[offset] == b) { + return offset; + } + offset++; + } + return -1; + } + + + /** + * Returns the first instance of any of the given bytes in the byte array between the specified start and end. + * + * @param bytes The byte array to search + * @param start The point to start searching from in the byte array + * @param end The point to stop searching in the byte array + * @param b The array of bytes to search for + * + * @return The position of the first instance of the byte or -1 if the byte is not found. + */ + public static int findBytes(byte bytes[], int start, int end, byte b[]) { + int offset = start; + while (offset < end) { + for (byte value : b) { + if (bytes[offset] == value) { + return offset; + } + } + offset++; + } + return -1; + } + + + /** + * Convert specified String to a byte array. This ONLY WORKS for ascii, UTF chars will be truncated. + * + * @param value to convert to byte array + * + * @return the byte array value + */ + public static byte[] convertToBytes(String value) { + byte[] result = new byte[value.length()]; + for (int i = 0; i < value.length(); i++) { + result[i] = (byte) value.charAt(i); + } + return result; + } + + + public static class BufferOverflowException extends IOException { + + private static final long serialVersionUID = 1L; + + public BufferOverflowException(String message) { + super(message); + } + } +} diff --git a/java/org/apache/tomcat/util/buf/C2BConverter.java b/java/org/apache/tomcat/util/buf/C2BConverter.java new file mode 100644 index 0000000..9c0c34b --- /dev/null +++ b/java/org/apache/tomcat/util/buf/C2BConverter.java @@ -0,0 +1,206 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * NIO based character encoder. + */ +public final class C2BConverter { + + private static final Log log = LogFactory.getLog(C2BConverter.class); + private static final StringManager sm = StringManager.getManager(C2BConverter.class); + + private final CharsetEncoder encoder; + private ByteBuffer bb = null; + private CharBuffer cb = null; + + /** + * Leftover buffer used for multi-characters characters. + */ + private final CharBuffer leftovers; + + public C2BConverter(Charset charset) { + encoder = charset.newEncoder(); + encoder.onUnmappableCharacter(CodingErrorAction.REPLACE).onMalformedInput(CodingErrorAction.REPLACE); + char[] left = new char[4]; + leftovers = CharBuffer.wrap(left); + } + + /** + * Reset the encoder state. + */ + public void recycle() { + try { + encoder.reset(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("c2bConverter.decoderResetFail", encoder.charset()), t); + } + leftovers.position(0); + } + + public boolean isUndeflow() { + return (leftovers.position() > 0); + } + + /** + * Convert the given characters to bytes. + * + * @param cc char input + * @param bc byte output + * + * @throws IOException An encoding error occurred + */ + public void convert(CharChunk cc, ByteChunk bc) throws IOException { + if ((bb == null) || (bb.array() != bc.getBuffer())) { + // Create a new byte buffer if anything changed + bb = ByteBuffer.wrap(bc.getBuffer(), bc.getEnd(), bc.getBuffer().length - bc.getEnd()); + } else { + // Initialize the byte buffer + bb.limit(bc.getBuffer().length); + bb.position(bc.getEnd()); + } + if ((cb == null) || (cb.array() != cc.getBuffer())) { + // Create a new char buffer if anything changed + cb = CharBuffer.wrap(cc.getBuffer(), cc.getStart(), cc.getLength()); + } else { + // Initialize the char buffer + cb.limit(cc.getEnd()); + cb.position(cc.getStart()); + } + CoderResult result = null; + // Parse leftover if any are present + if (leftovers.position() > 0) { + int pos = bb.position(); + // Loop until one char is encoded or there is a encoder error + do { + leftovers.put((char) cc.subtract()); + leftovers.flip(); + result = encoder.encode(leftovers, bb, false); + leftovers.position(leftovers.limit()); + leftovers.limit(leftovers.array().length); + } while (result.isUnderflow() && (bb.position() == pos)); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } + cb.position(cc.getStart()); + leftovers.position(0); + } + // Do the decoding and get the results into the byte chunk and the char + // chunk + result = encoder.encode(cb, bb, false); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } else if (result.isOverflow()) { + // Propagate current positions to the byte chunk and char chunk + bc.setEnd(bb.position()); + cc.setOffset(cb.position()); + } else if (result.isUnderflow()) { + // Propagate current positions to the byte chunk and char chunk + bc.setEnd(bb.position()); + cc.setOffset(cb.position()); + // Put leftovers in the leftovers char buffer + if (cc.getLength() > 0) { + leftovers.limit(leftovers.array().length); + leftovers.position(cc.getLength()); + cc.subtract(leftovers.array(), 0, cc.getLength()); + } + } + } + + /** + * Convert the given characters to bytes. + * + * @param cc char input + * @param bc byte output + * + * @throws IOException An encoding error occurred + */ + public void convert(CharBuffer cc, ByteBuffer bc) throws IOException { + if ((bb == null) || (bb.array() != bc.array())) { + // Create a new byte buffer if anything changed + bb = ByteBuffer.wrap(bc.array(), bc.limit(), bc.capacity() - bc.limit()); + } else { + // Initialize the byte buffer + bb.limit(bc.capacity()); + bb.position(bc.limit()); + } + if ((cb == null) || (cb.array() != cc.array())) { + // Create a new char buffer if anything changed + cb = CharBuffer.wrap(cc.array(), cc.arrayOffset() + cc.position(), cc.remaining()); + } else { + // Initialize the char buffer + cb.limit(cc.limit()); + cb.position(cc.position()); + } + CoderResult result = null; + // Parse leftover if any are present + if (leftovers.position() > 0) { + int pos = bb.position(); + // Loop until one char is encoded or there is a encoder error + do { + leftovers.put(cc.get()); + leftovers.flip(); + result = encoder.encode(leftovers, bb, false); + leftovers.position(leftovers.limit()); + leftovers.limit(leftovers.array().length); + } while (result.isUnderflow() && (bb.position() == pos)); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } + cb.position(cc.position()); + leftovers.position(0); + } + // Do the decoding and get the results into the byte chunk and the char + // chunk + result = encoder.encode(cb, bb, false); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } else if (result.isOverflow()) { + // Propagate current positions to the byte chunk and char chunk + bc.limit(bb.position()); + cc.position(cb.position()); + } else if (result.isUnderflow()) { + // Propagate current positions to the byte chunk and char chunk + bc.limit(bb.position()); + cc.position(cb.position()); + // Put leftovers in the leftovers char buffer + if (cc.remaining() > 0) { + leftovers.limit(leftovers.array().length); + leftovers.position(cc.remaining()); + cc.get(leftovers.array(), 0, cc.remaining()); + } + } + } + + public Charset getCharset() { + return encoder.charset(); + } +} diff --git a/java/org/apache/tomcat/util/buf/CharChunk.java b/java/org/apache/tomcat/util/buf/CharChunk.java new file mode 100644 index 0000000..4f3dcf4 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/CharChunk.java @@ -0,0 +1,649 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; + +/** + * Utilities to manipulate char chunks. While String is the easiest way to manipulate chars ( search, substrings, etc), + * it is known to not be the most efficient solution - Strings are designed as immutable and secure objects. + * + * @author dac@sun.com + * @author James Todd [gonzo@sun.com] + * @author Costin Manolache + * @author Remy Maucherat + */ +public final class CharChunk extends AbstractChunk implements CharSequence { + + private static final long serialVersionUID = 1L; + + /** + * Input interface, used when the buffer is empty. + */ + public interface CharInputChannel { + + /** + * Read new characters. + * + * @return The number of characters read + * + * @throws IOException If an I/O error occurs during reading + */ + int realReadChars() throws IOException; + } + + /** + * When we need more space we'll either grow the buffer ( up to the limit ) or send it to a channel. + */ + public interface CharOutputChannel { + + /** + * Send the bytes ( usually the internal conversion buffer ). Expect 8k output if the buffer is full. + * + * @param buf characters that will be written + * @param off offset in the characters array + * @param len length that will be written + * + * @throws IOException If an I/O occurs while writing the characters + */ + void realWriteChars(char buf[], int off, int len) throws IOException; + } + + // -------------------- + + // char[] + private char[] buff; + + // transient as serialization is primarily for values via, e.g. JMX + private transient CharInputChannel in = null; + private transient CharOutputChannel out = null; + + + /** + * Creates a new, uninitialized CharChunk object. + */ + public CharChunk() { + } + + + public CharChunk(int initial) { + allocate(initial, -1); + } + + + // -------------------- + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + + // -------------------- Setup -------------------- + + public void allocate(int initial, int limit) { + if (buff == null || buff.length < initial) { + buff = new char[initial]; + } + setLimit(limit); + start = 0; + end = 0; + isSet = true; + hasHashCode = false; + } + + + /** + * Sets the buffer to the specified subarray of characters. + * + * @param c the characters + * @param off the start offset of the characters + * @param len the length of the characters + */ + public void setChars(char[] c, int off, int len) { + buff = c; + start = off; + end = start + len; + isSet = true; + hasHashCode = false; + } + + + /** + * @return the buffer. + */ + public char[] getChars() { + return getBuffer(); + } + + + /** + * @return the buffer. + */ + public char[] getBuffer() { + return buff; + } + + + /** + * When the buffer is empty, read the data from the input channel. + * + * @param in The input channel + */ + public void setCharInputChannel(CharInputChannel in) { + this.in = in; + } + + + /** + * When the buffer is full, write the data to the output channel. Also used when large amount of data is appended. + * If not set, the buffer will grow to the limit. + * + * @param out The output channel + */ + public void setCharOutputChannel(CharOutputChannel out) { + this.out = out; + } + + + // -------------------- Adding data to the buffer -------------------- + + public void append(char c) throws IOException { + makeSpace(1); + int limit = getLimitInternal(); + + // couldn't make space + if (end >= limit) { + flushBuffer(); + } + buff[end++] = c; + } + + + public void append(CharChunk src) throws IOException { + append(src.getBuffer(), src.getOffset(), src.getLength()); + } + + + /** + * Add data to the buffer. + * + * @param src Char array + * @param off Offset + * @param len Length + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void append(char src[], int off, int len) throws IOException { + // will grow, up to limit + makeSpace(len); + int limit = getLimitInternal(); + + // Optimize on a common case. + // If the buffer is empty and the source is going to fill up all the + // space in buffer, may as well write it directly to the output, + // and avoid an extra copy + if (len == limit && end == start && out != null) { + out.realWriteChars(src, off, len); + return; + } + + // if we are below the limit + if (len <= limit - end) { + System.arraycopy(src, off, buff, end, len); + end += len; + return; + } + + // Need more space than we can afford, need to flush buffer. + + // The buffer is already at (or bigger than) limit. + + // Optimization: + // If len-avail < length (i.e. after we fill the buffer with what we + // can, the remaining will fit in the buffer) we'll just copy the first + // part, flush, then copy the second part - 1 write and still have some + // space for more. We'll still have 2 writes, but we write more on the first. + + if (len + end < 2 * limit) { + /* + * If the request length exceeds the size of the output buffer, flush the output buffer and then write the + * data directly. We can't avoid 2 writes, but we can write more on the second + */ + int avail = limit - end; + System.arraycopy(src, off, buff, end, avail); + end += avail; + + flushBuffer(); + + System.arraycopy(src, off + avail, buff, end, len - avail); + end += len - avail; + + } else { // len > buf.length + avail + // long write - flush the buffer and write the rest + // directly from source + flushBuffer(); + + out.realWriteChars(src, off, len); + } + } + + + /** + * Append a string to the buffer. + * + * @param s The string + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void append(String s) throws IOException { + append(s, 0, s.length()); + } + + + /** + * Append a string to the buffer. + * + * @param s The string + * @param off Offset + * @param len Length + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void append(String s, int off, int len) throws IOException { + if (s == null) { + return; + } + + // will grow, up to limit + makeSpace(len); + int limit = getLimitInternal(); + + int sOff = off; + int sEnd = off + len; + while (sOff < sEnd) { + int d = min(limit - end, sEnd - sOff); + s.getChars(sOff, sOff + d, buff, end); + sOff += d; + end += d; + if (end >= limit) { + flushBuffer(); + } + } + } + + + // -------------------- Removing data from the buffer -------------------- + + public int subtract() throws IOException { + if (checkEof()) { + return -1; + } + return buff[start++]; + } + + + public int subtract(char dest[], int off, int len) throws IOException { + if (checkEof()) { + return -1; + } + int n = len; + if (len > getLength()) { + n = getLength(); + } + System.arraycopy(buff, start, dest, off, n); + start += n; + return n; + } + + + private boolean checkEof() throws IOException { + if ((end - start) == 0) { + if (in == null) { + return true; + } + int n = in.realReadChars(); + if (n < 0) { + return true; + } + } + return false; + } + + + /** + * Send the buffer to the sink. Called by append() when the limit is reached. You can also call it explicitly to + * force the data to be written. + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void flushBuffer() throws IOException { + // assert out!=null + if (out == null) { + throw new IOException( + sm.getString("chunk.overflow", Integer.valueOf(getLimit()), Integer.valueOf(buff.length))); + } + out.realWriteChars(buff, start, end - start); + end = start; + } + + + /** + * Make space for len chars. If len is small, allocate a reserve space too. Never grow bigger than the limit or + * {@link AbstractChunk#ARRAY_MAX_SIZE}. + * + * @param count The size + */ + public void makeSpace(int count) { + char[] tmp = null; + + int limit = getLimitInternal(); + + long newSize; + long desiredSize = end + count; + + // Can't grow above the limit + if (desiredSize > limit) { + desiredSize = limit; + } + + if (buff == null) { + if (desiredSize < 256) { + desiredSize = 256; // take a minimum + } + buff = new char[(int) desiredSize]; + } + + // limit < buf.length (the buffer is already big) + // or we already have space + if (desiredSize <= buff.length) { + return; + } + // grow in larger chunks + if (desiredSize < 2L * buff.length) { + newSize = buff.length * 2L; + } else { + newSize = buff.length * 2L + count; + } + + if (newSize > limit) { + newSize = limit; + } + tmp = new char[(int) newSize]; + + // Some calling code assumes buffer will not be compacted + System.arraycopy(buff, 0, tmp, 0, end); + buff = tmp; + tmp = null; + } + + + // -------------------- Conversion and getters -------------------- + + @Override + public String toString() { + if (isNull()) { + return null; + } else if (end - start == 0) { + return ""; + } + return StringCache.toString(this); + } + + + public String toStringInternal() { + return new String(buff, start, end - start); + } + + + // -------------------- equals -------------------- + + @Override + public boolean equals(Object obj) { + if (obj instanceof CharChunk) { + return equals((CharChunk) obj); + } + return false; + } + + + /** + * Compares the message bytes to the specified String object. + * + * @param s the String to compare + * + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(String s) { + char[] c = buff; + int len = end - start; + if (c == null || len != s.length()) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (c[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + + /** + * Compares the message bytes to the specified String object. + * + * @param s the String to compare + * + * @return true if the comparison succeeded, false otherwise + */ + public boolean equalsIgnoreCase(String s) { + char[] c = buff; + int len = end - start; + if (c == null || len != s.length()) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (Ascii.toLower(c[off++]) != Ascii.toLower(s.charAt(i))) { + return false; + } + } + return true; + } + + + public boolean equals(CharChunk cc) { + return equals(cc.getChars(), cc.getOffset(), cc.getLength()); + } + + + public boolean equals(char b2[], int off2, int len2) { + char b1[] = buff; + if (b1 == null && b2 == null) { + return true; + } + + int len = end - start; + if (len != len2 || b1 == null || b2 == null) { + return false; + } + + int off1 = start; + + while (len-- > 0) { + if (b1[off1++] != b2[off2++]) { + return false; + } + } + return true; + } + + + /** + * @return true if the message bytes starts with the specified string. + * + * @param s The string + */ + public boolean startsWith(String s) { + char[] c = buff; + int len = s.length(); + if (c == null || len > end - start) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (c[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + + /** + * Returns true if the buffer starts with the specified string. + * + * @param s the string + * @param pos The position + * + * @return true if the start matches + */ + public boolean startsWithIgnoreCase(String s, int pos) { + char[] c = buff; + int len = s.length(); + if (c == null || len + pos > end - start) { + return false; + } + int off = start + pos; + for (int i = 0; i < len; i++) { + if (Ascii.toLower(c[off++]) != Ascii.toLower(s.charAt(i))) { + return false; + } + } + return true; + } + + + /** + * @return true if the message bytes end with the specified string. + * + * @param s The string + */ + public boolean endsWith(String s) { + char[] c = buff; + int len = s.length(); + if (c == null || len > end - start) { + return false; + } + int off = end - len; + for (int i = 0; i < len; i++) { + if (c[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + + @Override + protected int getBufferElement(int index) { + return buff[index]; + } + + + public int indexOf(char c) { + return indexOf(c, start); + } + + + /** + * Returns the first instance of the given character in this CharChunk starting at the specified char. If the + * character is not found, -1 is returned.
    + * + * @param c The character + * @param starting The start position + * + * @return The position of the first instance of the character or -1 if the character is not found. + */ + public int indexOf(char c, int starting) { + int ret = indexOf(buff, start + starting, end, c); + return (ret >= start) ? ret - start : -1; + } + + + /** + * Returns the first instance of the given character in the given char array between the specified start and end. + *
    + * + * @param chars The array to search + * @param start The point to start searching from in the array + * @param end The point to stop searching in the array + * @param s The character to search for + * + * @return The position of the first instance of the character or -1 if the character is not found. + */ + public static int indexOf(char chars[], int start, int end, char s) { + int offset = start; + + while (offset < end) { + char c = chars[offset]; + if (c == s) { + return offset; + } + offset++; + } + return -1; + } + + + // -------------------- utils + private int min(int a, int b) { + if (a < b) { + return a; + } + return b; + } + + + // Char sequence impl + + @Override + public char charAt(int index) { + return buff[index + start]; + } + + + @Override + public CharSequence subSequence(int start, int end) { + try { + CharChunk result = (CharChunk) this.clone(); + result.setOffset(this.start + start); + result.setEnd(this.start + end); + return result; + } catch (CloneNotSupportedException e) { + // Cannot happen + return null; + } + } + + + @Override + public int length() { + return end - start; + } +} diff --git a/java/org/apache/tomcat/util/buf/CharsetCache.java b/java/org/apache/tomcat/util/buf/CharsetCache.java new file mode 100644 index 0000000..7de8f79 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/CharsetCache.java @@ -0,0 +1,235 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class CharsetCache { + + /* Note: Package private to enable testing without reflection */ + static final String[] INITIAL_CHARSETS = new String[] { "iso-8859-1", "utf-8" }; + + /* + * Note: Package private to enable testing without reflection + */ + static final String[] LAZY_CHARSETS = new String[] { + // Initial set from Oracle JDK 8 u192 + "037", "1006", "1025", "1026", "1046", "1047", "1089", "1097", "1098", "1112", "1122", "1123", "1124", + "1140", "1141", "1142", "1143", "1144", "1145", "1146", "1147", "1148", "1149", "1166", "1364", "1381", + "1383", "273", "277", "278", "280", "284", "285", "290", "297", "300", "33722", "420", "424", "437", "500", + "5601", "646", "737", "775", "813", "834", "838", "850", "852", "855", "856", "857", "858", "860", "861", + "862", "863", "864", "865", "866", "868", "869", "870", "871", "874", "875", "8859_13", "8859_15", "8859_2", + "8859_3", "8859_4", "8859_5", "8859_6", "8859_7", "8859_8", "8859_9", "912", "913", "914", "915", "916", + "918", "920", "921", "922", "923", "930", "933", "935", "937", "939", "942", "942c", "943", "943c", "948", + "949", "949c", "950", "964", "970", "ansi-1251", "ansi_x3.4-1968", "ansi_x3.4-1986", "arabic", "ascii", + "ascii7", "asmo-708", "big5", "big5-hkscs", "big5-hkscs", "big5-hkscs-2001", "big5-hkscs:unicode3.0", + "big5_hkscs", "big5_hkscs_2001", "big5_solaris", "big5hk", "big5hk-2001", "big5hkscs", "big5hkscs-2001", + "ccsid00858", "ccsid01140", "ccsid01141", "ccsid01142", "ccsid01143", "ccsid01144", "ccsid01145", + "ccsid01146", "ccsid01147", "ccsid01148", "ccsid01149", "cesu-8", "cesu8", "cns11643", "compound_text", + "cp-ar", "cp-gr", "cp-is", "cp00858", "cp01140", "cp01141", "cp01142", "cp01143", "cp01144", "cp01145", + "cp01146", "cp01147", "cp01148", "cp01149", "cp037", "cp1006", "cp1025", "cp1026", "cp1046", "cp1047", + "cp1089", "cp1097", "cp1098", "cp1112", "cp1122", "cp1123", "cp1124", "cp1140", "cp1141", "cp1142", + "cp1143", "cp1144", "cp1145", "cp1146", "cp1147", "cp1148", "cp1149", "cp1166", "cp1250", "cp1251", + "cp1252", "cp1253", "cp1254", "cp1255", "cp1256", "cp1257", "cp1258", "cp1364", "cp1381", "cp1383", "cp273", + "cp277", "cp278", "cp280", "cp284", "cp285", "cp290", "cp297", "cp300", "cp33722", "cp367", "cp420", + "cp424", "cp437", "cp500", "cp50220", "cp50221", "cp5346", "cp5347", "cp5348", "cp5349", "cp5350", "cp5353", + "cp737", "cp775", "cp813", "cp833", "cp834", "cp838", "cp850", "cp852", "cp855", "cp856", "cp857", "cp858", + "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866", "cp868", "cp869", "cp870", "cp871", "cp874", + "cp875", "cp912", "cp913", "cp914", "cp915", "cp916", "cp918", "cp920", "cp921", "cp922", "cp923", "cp930", + "cp933", "cp935", "cp936", "cp937", "cp939", "cp942", "cp942c", "cp943", "cp943c", "cp948", "cp949", + "cp949c", "cp950", "cp964", "cp970", "cpibm284", "cpibm285", "cpibm297", "cpibm37", "cs-ebcdic-cp-ca", + "cs-ebcdic-cp-nl", "cs-ebcdic-cp-us", "cs-ebcdic-cp-wt", "csascii", "csbig5", "cscesu-8", "cseuckr", + "cseucpkdfmtjapanese", "cshalfwidthkatakana", "csibm037", "csibm278", "csibm284", "csibm285", "csibm290", + "csibm297", "csibm420", "csibm424", "csibm500", "csibm857", "csibm860", "csibm861", "csibm862", "csibm863", + "csibm864", "csibm865", "csibm866", "csibm868", "csibm869", "csibm870", "csibm871", "csiso153gost1976874", + "csiso159jisx02121990", "csiso2022cn", "csiso2022jp", "csiso2022jp2", "csiso2022kr", "csiso87jisx0208", + "csisolatin0", "csisolatin2", "csisolatin3", "csisolatin4", "csisolatin5", "csisolatin9", + "csisolatinarabic", "csisolatincyrillic", "csisolatingreek", "csisolatinhebrew", "csjisencoding", "cskoi8r", + "cspc850multilingual", "cspc862latinhebrew", "cspc8codepage437", "cspcp852", "cspcp855", "csshiftjis", + "cswindows31j", "cyrillic", "default", "ebcdic-cp-ar1", "ebcdic-cp-ar2", "ebcdic-cp-bh", "ebcdic-cp-ca", + "ebcdic-cp-ch", "ebcdic-cp-fr", "ebcdic-cp-gb", "ebcdic-cp-he", "ebcdic-cp-is", "ebcdic-cp-nl", + "ebcdic-cp-roece", "ebcdic-cp-se", "ebcdic-cp-us", "ebcdic-cp-wt", "ebcdic-cp-yu", "ebcdic-de-273+euro", + "ebcdic-dk-277+euro", "ebcdic-es-284+euro", "ebcdic-fi-278+euro", "ebcdic-fr-277+euro", "ebcdic-gb", + "ebcdic-gb-285+euro", "ebcdic-international-500+euro", "ebcdic-it-280+euro", "ebcdic-jp-kana", + "ebcdic-no-277+euro", "ebcdic-s-871+euro", "ebcdic-se-278+euro", "ebcdic-sv", "ebcdic-us-037+euro", + "ecma-114", "ecma-118", "elot_928", "euc-cn", "euc-jp", "euc-jp-linux", "euc-kr", "euc-tw", "euc_cn", + "euc_jp", "euc_jp_linux", "euc_jp_solaris", "euc_kr", "euc_tw", "euccn", "eucjis", "eucjp", "eucjp-open", + "euckr", "euctw", "extended_unix_code_packed_format_for_japanese", "gb18030", "gb18030-2000", "gb2312", + "gb2312", "gb2312-1980", "gb2312-80", "gbk", "greek", "greek8", "hebrew", "ibm-037", "ibm-1006", "ibm-1025", + "ibm-1026", "ibm-1046", "ibm-1047", "ibm-1089", "ibm-1097", "ibm-1098", "ibm-1112", "ibm-1122", "ibm-1123", + "ibm-1124", "ibm-1166", "ibm-1364", "ibm-1381", "ibm-1383", "ibm-273", "ibm-277", "ibm-278", "ibm-280", + "ibm-284", "ibm-285", "ibm-290", "ibm-297", "ibm-300", "ibm-33722", "ibm-33722_vascii_vpua", "ibm-37", + "ibm-420", "ibm-424", "ibm-437", "ibm-500", "ibm-5050", "ibm-737", "ibm-775", "ibm-813", "ibm-833", + "ibm-834", "ibm-838", "ibm-850", "ibm-852", "ibm-855", "ibm-856", "ibm-857", "ibm-860", "ibm-861", + "ibm-862", "ibm-863", "ibm-864", "ibm-865", "ibm-866", "ibm-868", "ibm-869", "ibm-870", "ibm-871", + "ibm-874", "ibm-875", "ibm-912", "ibm-913", "ibm-914", "ibm-915", "ibm-916", "ibm-918", "ibm-920", + "ibm-921", "ibm-922", "ibm-923", "ibm-930", "ibm-933", "ibm-935", "ibm-937", "ibm-939", "ibm-942", + "ibm-942c", "ibm-943", "ibm-943c", "ibm-948", "ibm-949", "ibm-949c", "ibm-950", "ibm-964", "ibm-970", + "ibm-euckr", "ibm-thai", "ibm00858", "ibm01140", "ibm01141", "ibm01142", "ibm01143", "ibm01144", "ibm01145", + "ibm01146", "ibm01147", "ibm01148", "ibm01149", "ibm037", "ibm037", "ibm1006", "ibm1025", "ibm1026", + "ibm1026", "ibm1046", "ibm1047", "ibm1089", "ibm1097", "ibm1098", "ibm1112", "ibm1122", "ibm1123", + "ibm1124", "ibm1166", "ibm1364", "ibm1381", "ibm1383", "ibm273", "ibm273", "ibm277", "ibm277", "ibm278", + "ibm278", "ibm280", "ibm280", "ibm284", "ibm284", "ibm285", "ibm285", "ibm290", "ibm290", "ibm297", + "ibm297", "ibm300", "ibm33722", "ibm367", "ibm420", "ibm420", "ibm424", "ibm424", "ibm437", "ibm437", + "ibm500", "ibm500", "ibm737", "ibm775", "ibm775", "ibm813", "ibm833", "ibm834", "ibm838", "ibm850", + "ibm850", "ibm852", "ibm852", "ibm855", "ibm855", "ibm856", "ibm857", "ibm857", "ibm860", "ibm860", + "ibm861", "ibm861", "ibm862", "ibm862", "ibm863", "ibm863", "ibm864", "ibm864", "ibm865", "ibm865", + "ibm866", "ibm866", "ibm868", "ibm868", "ibm869", "ibm869", "ibm870", "ibm870", "ibm871", "ibm871", + "ibm874", "ibm875", "ibm912", "ibm913", "ibm914", "ibm915", "ibm916", "ibm918", "ibm920", "ibm921", + "ibm922", "ibm923", "ibm930", "ibm933", "ibm935", "ibm937", "ibm939", "ibm942", "ibm942c", "ibm943", + "ibm943c", "ibm948", "ibm949", "ibm949c", "ibm950", "ibm964", "ibm970", "iscii", "iscii91", + "iso-10646-ucs-2", "iso-2022-cn", "iso-2022-cn-cns", "iso-2022-cn-gb", "iso-2022-jp", "iso-2022-jp-2", + "iso-2022-kr", "iso-8859-11", "iso-8859-13", "iso-8859-15", "iso-8859-15", "iso-8859-2", "iso-8859-3", + "iso-8859-4", "iso-8859-5", "iso-8859-6", "iso-8859-7", "iso-8859-8", "iso-8859-9", "iso-ir-101", + "iso-ir-109", "iso-ir-110", "iso-ir-126", "iso-ir-127", "iso-ir-138", "iso-ir-144", "iso-ir-148", + "iso-ir-153", "iso-ir-159", "iso-ir-6", "iso-ir-87", "iso2022cn", "iso2022cn_cns", "iso2022cn_gb", + "iso2022jp", "iso2022jp2", "iso2022kr", "iso646-us", "iso8859-13", "iso8859-15", "iso8859-2", "iso8859-3", + "iso8859-4", "iso8859-5", "iso8859-6", "iso8859-7", "iso8859-8", "iso8859-9", "iso8859_11", "iso8859_13", + "iso8859_15", "iso8859_15_fdis", "iso8859_2", "iso8859_3", "iso8859_4", "iso8859_5", "iso8859_6", + "iso8859_7", "iso8859_8", "iso8859_9", "iso_646.irv:1983", "iso_646.irv:1991", "iso_8859-13", "iso_8859-15", + "iso_8859-2", "iso_8859-2:1987", "iso_8859-3", "iso_8859-3:1988", "iso_8859-4", "iso_8859-4:1988", + "iso_8859-5", "iso_8859-5:1988", "iso_8859-6", "iso_8859-6:1987", "iso_8859-7", "iso_8859-7:1987", + "iso_8859-8", "iso_8859-8:1988", "iso_8859-9", "iso_8859-9:1989", "jis", "jis0201", "jis0208", "jis0212", + "jis_c6226-1983", "jis_encoding", "jis_x0201", "jis_x0201", "jis_x0208-1983", "jis_x0212-1990", + "jis_x0212-1990", "jisautodetect", "johab", "koi8", "koi8-r", "koi8-u", "koi8_r", "koi8_u", + "ks_c_5601-1987", "ksc5601", "ksc5601-1987", "ksc5601-1992", "ksc5601_1987", "ksc5601_1992", "ksc_5601", + "l2", "l3", "l4", "l5", "l9", "latin0", "latin2", "latin3", "latin4", "latin5", "latin9", "macarabic", + "maccentraleurope", "maccroatian", "maccyrillic", "macdingbat", "macgreek", "machebrew", "maciceland", + "macroman", "macromania", "macsymbol", "macthai", "macturkish", "macukraine", "ms-874", "ms1361", "ms50220", + "ms50221", "ms874", "ms932", "ms936", "ms949", "ms950", "ms950_hkscs", "ms950_hkscs_xp", "ms_936", "ms_949", + "ms_kanji", "pc-multilingual-850+euro", "pck", "shift-jis", "shift_jis", "shift_jis", "sjis", + "st_sev_358-88", "sun_eu_greek", "tis-620", "tis620", "tis620.2533", "unicode", "unicodebig", + "unicodebigunmarked", "unicodelittle", "unicodelittleunmarked", "us", "us-ascii", "utf-16", "utf-16be", + "utf-16le", "utf-32", "utf-32be", "utf-32be-bom", "utf-32le", "utf-32le-bom", "utf16", "utf32", "utf_16", + "utf_16be", "utf_16le", "utf_32", "utf_32be", "utf_32be_bom", "utf_32le", "utf_32le_bom", "windows-1250", + "windows-1251", "windows-1252", "windows-1253", "windows-1254", "windows-1255", "windows-1256", + "windows-1257", "windows-1258", "windows-31j", "windows-437", "windows-874", "windows-932", "windows-936", + "windows-949", "windows-950", "windows-iso2022jp", "windows949", "x-big5-hkscs-2001", "x-big5-solaris", + "x-compound-text", "x-compound_text", "x-euc-cn", "x-euc-jp", "x-euc-jp-linux", "x-euc-tw", "x-eucjp", + "x-eucjp-open", "x-ibm1006", "x-ibm1025", "x-ibm1046", "x-ibm1097", "x-ibm1098", "x-ibm1112", "x-ibm1122", + "x-ibm1123", "x-ibm1124", "x-ibm1166", "x-ibm1364", "x-ibm1381", "x-ibm1383", "x-ibm300", "x-ibm33722", + "x-ibm737", "x-ibm833", "x-ibm834", "x-ibm856", "x-ibm874", "x-ibm875", "x-ibm921", "x-ibm922", "x-ibm930", + "x-ibm933", "x-ibm935", "x-ibm937", "x-ibm939", "x-ibm942", "x-ibm942c", "x-ibm943", "x-ibm943c", + "x-ibm948", "x-ibm949", "x-ibm949c", "x-ibm950", "x-ibm964", "x-ibm970", "x-iscii91", "x-iso-2022-cn-cns", + "x-iso-2022-cn-gb", "x-iso-8859-11", "x-jis0208", "x-jisautodetect", "x-johab", "x-macarabic", + "x-maccentraleurope", "x-maccroatian", "x-maccyrillic", "x-macdingbat", "x-macgreek", "x-machebrew", + "x-maciceland", "x-macroman", "x-macromania", "x-macsymbol", "x-macthai", "x-macturkish", "x-macukraine", + "x-ms932_0213", "x-ms950-hkscs", "x-ms950-hkscs-xp", "x-mswin-936", "x-pck", "x-sjis", "x-sjis_0213", + "x-utf-16be", "x-utf-16le", "x-utf-16le-bom", "x-utf-32be", "x-utf-32be-bom", "x-utf-32le", + "x-utf-32le-bom", "x-windows-50220", "x-windows-50221", "x-windows-874", "x-windows-949", "x-windows-950", + "x-windows-iso2022jp", "x0201", "x0208", "x0212", "x11-compound_text", + // Added from Oracle JDK 10.0.2 + "csiso885915", "csiso885916", "iso-8859-16", "iso-ir-226", "iso_8859-16", "iso_8859-16:2001", "l10", + "latin-9", "latin10", "ms932-0213", "ms932:2004", "ms932_0213", "shift_jis:2004", "shift_jis_0213:2004", + "sjis-0213", "sjis:2004", "sjis_0213", "sjis_0213:2004", "windows-932-0213", "windows-932:2004", + // Added from OpenJDK 11.0.1 + "932", "cp932", "cpeuccn", "ibm-1252", "ibm-932", "ibm-euccn", "ibm1252", "ibm932", "ibmeuccn", "x-ibm932", + // Added from OpenJDK 12 ea28 + "1129", "cp1129", "ibm-1129", "ibm-euctw", "ibm1129", "x-ibm1129", + // Added from OpenJDK 13 ea15 + "29626c", "833", "cp29626c", "ibm-1140", "ibm-1141", "ibm-1142", "ibm-1143", "ibm-1144", "ibm-1145", + "ibm-1146", "ibm-1147", "ibm-1148", "ibm-1149", "ibm-29626c", "ibm-858", "ibm-eucjp", "ibm1140", "ibm1141", + "ibm1142", "ibm1143", "ibm1144", "ibm1145", "ibm1146", "ibm1147", "ibm1148", "ibm1149", "ibm29626c", + "ibm858", "x-ibm29626c", + // Added from OpenJDK 15 ea24 + "iso8859_16", + // Added from HPE JVM 1.8.0.17-hp-ux + "cp1051", "cp1386", "cshproman8", "hp-roman8", "ibm-1051", "r8", "roman8", "roman9", + // Added from OpenJDK 21 ea18 + "gb18030-2022" + // If you add and entry to this list, ensure you run + // TestCharsetUtil#testIsAcsiiSupersetAll() + }; + + private static final Charset DUMMY_CHARSET = new DummyCharset("Dummy", null); + + private ConcurrentMap cache = new ConcurrentHashMap<>(); + + public CharsetCache() { + // Pre-populate the cache + for (String charsetName : INITIAL_CHARSETS) { + Charset charset = Charset.forName(charsetName); + addToCache(charsetName, charset); + } + + for (String charsetName : LAZY_CHARSETS) { + addToCache(charsetName, DUMMY_CHARSET); + } + } + + + private void addToCache(String name, Charset charset) { + cache.put(name, charset); + for (String alias : charset.aliases()) { + cache.put(alias.toLowerCase(Locale.ENGLISH), charset); + } + } + + + public Charset getCharset(String charsetName) { + String lcCharsetName = charsetName.toLowerCase(Locale.ENGLISH); + + Charset result = cache.get(lcCharsetName); + + if (result == DUMMY_CHARSET) { + // Name is known but the Charset is not in the cache + Charset charset = Charset.forName(lcCharsetName); + if (charset == null) { + // Charset not available in this JVM - remove cache entry + cache.remove(lcCharsetName); + result = null; + } else { + // Charset is available - populate cache entry + addToCache(lcCharsetName, charset); + result = charset; + } + } + + return result; + } + + + /* + * Placeholder Charset implementation for entries that will be loaded lazily into the cache. + */ + private static class DummyCharset extends Charset { + + protected DummyCharset(String canonicalName, String[] aliases) { + super(canonicalName, aliases); + } + + @Override + public boolean contains(Charset cs) { + return false; + } + + @Override + public CharsetDecoder newDecoder() { + return null; + } + + @Override + public CharsetEncoder newEncoder() { + return null; + } + } +} diff --git a/java/org/apache/tomcat/util/buf/CharsetUtil.java b/java/org/apache/tomcat/util/buf/CharsetUtil.java new file mode 100644 index 0000000..fc0a09e --- /dev/null +++ b/java/org/apache/tomcat/util/buf/CharsetUtil.java @@ -0,0 +1,58 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; + +public class CharsetUtil { + + private CharsetUtil() { + // Utility class. Hide default constructor. + } + + + public static boolean isAsciiSuperset(Charset charset) { + // Bytes 0x00 to 0x7F must decode to the first 128 Unicode characters + CharsetDecoder decoder = charset.newDecoder(); + ByteBuffer inBytes = ByteBuffer.allocate(1); + CharBuffer outChars; + for (int i = 0; i < 128; i++) { + inBytes.clear(); + inBytes.put((byte) i); + inBytes.flip(); + try { + outChars = decoder.decode(inBytes); + } catch (CharacterCodingException e) { + return false; + } + try { + if (outChars.get() != i) { + return false; + } + } catch (BufferUnderflowException e) { + return false; + } + } + + return true; + } +} diff --git a/java/org/apache/tomcat/util/buf/EncodedSolidusHandling.java b/java/org/apache/tomcat/util/buf/EncodedSolidusHandling.java new file mode 100644 index 0000000..ec90364 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/EncodedSolidusHandling.java @@ -0,0 +1,51 @@ +/* + * 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.tomcat.util.buf; + +import java.util.Locale; + +import org.apache.tomcat.util.res.StringManager; + +public enum EncodedSolidusHandling { + DECODE("decode"), + REJECT("reject"), + PASS_THROUGH("passthrough"); + + private static final StringManager sm = StringManager.getManager(EncodedSolidusHandling.class); + + private final String value; + + EncodedSolidusHandling(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static EncodedSolidusHandling fromString(String from) { + String trimmedLower = from.trim().toLowerCase(Locale.ENGLISH); + + for (EncodedSolidusHandling value : values()) { + if (value.getValue().equals(trimmedLower)) { + return value; + } + } + + throw new IllegalStateException(sm.getString("encodedSolidusHandling.invalid", from)); + } +} diff --git a/java/org/apache/tomcat/util/buf/HexUtils.java b/java/org/apache/tomcat/util/buf/HexUtils.java new file mode 100644 index 0000000..76bb7e0 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/HexUtils.java @@ -0,0 +1,124 @@ +/* + * 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.tomcat.util.buf; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Tables useful when converting byte arrays to and from strings of hexadecimal digits. Code from Ajp11, from Apache's + * JServ. + * + * @author Craig R. McClanahan + */ +public final class HexUtils { + + private static final StringManager sm = StringManager.getManager(HexUtils.class); + + // -------------------------------------------------------------- Constants + + /** + * Table for HEX to DEC byte translation. + */ + private static final int[] DEC = { 00, 01, 02, 03, 04, 05, 06, 07, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, + 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, }; + + + /** + * Table for DEC to HEX byte translation. + */ + private static final byte[] HEX = + { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', + (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f' }; + + + /** + * Table for byte to hex string translation. + */ + private static final char[] hex = "0123456789abcdef".toCharArray(); + + + // --------------------------------------------------------- Static Methods + + public static int getDec(int index) { + // Fast for correct values, slower for incorrect ones + try { + return DEC[index - '0']; + } catch (ArrayIndexOutOfBoundsException ex) { + return -1; + } + } + + + public static byte getHex(int index) { + return HEX[index]; + } + + + public static String toHexString(char c) { + // 2 bytes / 4 hex digits + StringBuilder sb = new StringBuilder(4); + + sb.append(hex[(c & 0xf000) >> 12]); + sb.append(hex[(c & 0x0f00) >> 8]); + + sb.append(hex[(c & 0xf0) >> 4]); + sb.append(hex[(c & 0x0f)]); + + return sb.toString(); + } + + + public static String toHexString(byte[] bytes) { + if (null == bytes) { + return null; + } + + StringBuilder sb = new StringBuilder(bytes.length << 1); + + for (byte aByte : bytes) { + sb.append(hex[(aByte & 0xf0) >> 4]).append(hex[(aByte & 0x0f)]); + } + + return sb.toString(); + } + + + public static byte[] fromHexString(String input) { + if (input == null) { + return null; + } + + if ((input.length() & 1) == 1) { + // Odd number of characters + throw new IllegalArgumentException(sm.getString("hexUtils.fromHex.oddDigits")); + } + + char[] inputChars = input.toCharArray(); + byte[] result = new byte[input.length() >> 1]; + for (int i = 0; i < result.length; i++) { + int upperNibble = getDec(inputChars[2 * i]); + int lowerNibble = getDec(inputChars[2 * i + 1]); + if (upperNibble < 0 || lowerNibble < 0) { + // Non hex character + throw new IllegalArgumentException(sm.getString("hexUtils.fromHex.nonHex")); + } + result[i] = (byte) ((upperNibble << 4) + lowerNibble); + } + return result; + } +} diff --git a/java/org/apache/tomcat/util/buf/LocalStrings.properties b/java/org/apache/tomcat/util/buf/LocalStrings.properties new file mode 100644 index 0000000..7fb7314 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings.properties @@ -0,0 +1,42 @@ +# 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. + +asn1Parser.lengthInvalid=Invalid length [{0}] bytes reported when the input data length is [{1}] bytes +asn1Parser.tagMismatch=Expected to find value [{0}] but found value [{1}] + +b2cConverter.decoderResetFail=Failed to reset instance of decoder for character set [{0}] +b2cConverter.unknownEncoding=The character encoding [{0}] is not supported + +byteBufferUtils.cleaner=Cannot use direct ByteBuffer cleaner, memory leaking may occur + +c2bConverter.encoderResetFail=Failed to reset instance of encoder for character set [{0}] + +chunk.overflow=Buffer overflow and no sink is set, limit [{0}] and buffer length [{1}] + +encodedSolidusHandling.invalid=The value [{0}] is not recognised + +hexUtils.fromHex.nonHex=The input must consist only of hex digits +hexUtils.fromHex.oddDigits=The input must consist of an even number of hex digits + +messageBytes.illegalCharacter=The Unicode character [{0}] at code point [{1}] cannot be encoded as it is outside the permitted range of 0 to 255 + +stringCache.byteTime=ByteCache generation time: {0}ms +stringCache.charTime=CharCache generation time: {0}ms + +uDecoder.eof=End of file (EOF) +uDecoder.isHexDigit=The hexadecimal encoding is invalid +uDecoder.noSlash=The encoded slash character is not allowed +uDecoder.urlDecode.conversionError=Failed to decode [{0}] using character set [{1}] +uDecoder.urlDecode.missingDigit=Failed to decode [{0}] because the % character must be followed by two hexadecimal digits diff --git a/java/org/apache/tomcat/util/buf/LocalStrings_cs.properties b/java/org/apache/tomcat/util/buf/LocalStrings_cs.properties new file mode 100644 index 0000000..1a0a121 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings_cs.properties @@ -0,0 +1,16 @@ +# 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. + +messageBytes.illegalCharacter=Unicode znak [{0}] na kódové znaÄce [{1}] nemůže být zakódován, protože je mimo povolený rozsah 0 až 255. diff --git a/java/org/apache/tomcat/util/buf/LocalStrings_de.properties b/java/org/apache/tomcat/util/buf/LocalStrings_de.properties new file mode 100644 index 0000000..2220bbf --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +messageBytes.illegalCharacter=Das Unicode Zeichen [{0}] an Code Punkt [{1}] kann nicht kodiert werden, da es außerhalb des erlaubten Bereiches von 0 bis 255 ist. diff --git a/java/org/apache/tomcat/util/buf/LocalStrings_es.properties b/java/org/apache/tomcat/util/buf/LocalStrings_es.properties new file mode 100644 index 0000000..8946d50 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings_es.properties @@ -0,0 +1,18 @@ +# 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. + +b2cConverter.unknownEncoding=La codificación de carácter [{0}] no está soportada + +messageBytes.illegalCharacter=El carácter Unicode [{0}] en el punto del código [{1}] no puede ser codificado al estar fuera del rango permitido de 0 a 255. diff --git a/java/org/apache/tomcat/util/buf/LocalStrings_fr.properties b/java/org/apache/tomcat/util/buf/LocalStrings_fr.properties new file mode 100644 index 0000000..d2480ec --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings_fr.properties @@ -0,0 +1,42 @@ +# 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. + +asn1Parser.lengthInvalid=Une longueur d''octets invalide [{0}] a été rapportée alors que la longueur de données en entrée est de [{1}] octets +asn1Parser.tagMismatch=La valeur [{0}] était attendue mais la valeur [{1}] a été rencontrée + +b2cConverter.decoderResetFail=Impossible de réinitialiser l''instance du décodeur pour le jeu de caractère [{0}] +b2cConverter.unknownEncoding=L''encodage de caractères [{0}] n''est pas supporté + +byteBufferUtils.cleaner=Impossible d'utiliser le nettoyeur de ByteBuffers directs, une fuite de mémoire peut se produire + +c2bConverter.encoderResetFail=Impossible de réinitialiser l''instance de l''encodeur pour le jeu de caractère [{0}] + +chunk.overflow=Le tampon a débordé et aucun receveur n''a été défini, la limite est de [{0}] et la taille du tampon est [{1}] + +encodedSolidusHandling.invalid=La valeur [{0}] n''est pas reconnue + +hexUtils.fromHex.nonHex=L'entrée doit être uniquement des chiffres héxadécimaux +hexUtils.fromHex.oddDigits=L'entrée doit contenir un nombre pair de chiffres héxadécimaux + +messageBytes.illegalCharacter=Le caractère Unicode [{0}] ayant le code point [{1}] ne peut être encodé, parce qu''il est en-dehors de l''éventail permis 0-255. + +stringCache.byteTime=Temps de génération du ByteCache: {0}ms +stringCache.charTime=Temps de génération du CharCache: {0}ms + +uDecoder.eof=Fin de fichier (EOF) +uDecoder.isHexDigit=L'encodage hexadécimal est invalide +uDecoder.noSlash=Un caractère slash encodé n'est pas autorisé +uDecoder.urlDecode.conversionError=Echec de décodage [{0}] en utilisant le jeu de caractères [{1}] +uDecoder.urlDecode.missingDigit=Impossible de décoder [{0}] parce que le caractère % doit être suivi de deux chiffres héxadécimaux diff --git a/java/org/apache/tomcat/util/buf/LocalStrings_ja.properties b/java/org/apache/tomcat/util/buf/LocalStrings_ja.properties new file mode 100644 index 0000000..13cdc6d --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings_ja.properties @@ -0,0 +1,42 @@ +# 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. + +asn1Parser.lengthInvalid=入力データãƒã‚¤ãƒˆé•· [{1}] ã«å¯¾ã—ã¦ç„¡åŠ¹ãªãƒã‚¤ãƒˆé•· [{0}] ãŒå ±å‘Šã•ã‚Œã¾ã—㟠+asn1Parser.tagMismatch=期待値㯠[{0}] ã§ã—ãŸãŒã€å®Ÿéš›ã«è¦‹ã¤ã‹ã£ãŸå€¤ã¯ [{1}] ã§ã—㟠+ +b2cConverter.decoderResetFail=文字セット [{0}] ã®ãƒ‡ã‚³ãƒ¼ãƒ€ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’リセットã§ãã¾ã›ã‚“ã§ã—㟠+b2cConverter.unknownEncoding=文字エンコーディング [{0}] ã¯æœªå¯¾å¿œã§ã™ã€‚ + +byteBufferUtils.cleaner=直接ByteBufferクリーナーを使用ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ã€ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ãŒç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ + +c2bConverter.encoderResetFail=文字セット [{0}] ã®ã‚¨ãƒ³ã‚³ãƒ¼ãƒ€ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’リセットã§ãã¾ã›ã‚“ã§ã—㟠+ +chunk.overflow=ãƒãƒƒãƒ•ã‚¡ã‚ªãƒ¼ãƒãƒ¼ãƒ•ãƒ­ãƒ¼ã€ã‹ã¤ã€ã‚·ãƒ³ã‚¯ãŒã‚ã‚Šã¾ã›ã‚“。ãƒãƒƒãƒ•ã‚¡é•·ã®ä¸Šé™ã¯ [{0}]ã€ç¾åœ¨ã®ãƒãƒƒãƒ•ã‚¡é•·ã¯ [{1}]。 + +encodedSolidusHandling.invalid=値 [{0}] ã¯èªè­˜ã•ã‚Œã¾ã›ã‚“ + +hexUtils.fromHex.nonHex=入力ã¯16進数ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“ +hexUtils.fromHex.oddDigits=入力ã¯ã€å¶æ•°ã®16進数ã§æ§‹æˆã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ + +messageBytes.illegalCharacter=コードãƒã‚¤ãƒ³ãƒˆ [{1}] ã®ãƒ¦ãƒ‹ã‚³ãƒ¼ãƒ‰æ–‡å­— [{0}] ã¯æœ‰åŠ¹ç¯„囲 0 ã‹ã‚‰ 255 ã®ç¯„囲外ã®ãŸã‚ã€ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã§ãã¾ã›ã‚“。 + +stringCache.byteTime=ByteCache 生æˆæ™‚é–“: {0}ms +stringCache.charTime=CharCache 生æˆæ™‚é–“: {0}ms + +uDecoder.eof=予期ã›ã¬å ´æ‰€ã§çµ‚端ã«é”ã—ã¾ã—ãŸã€‚ +uDecoder.isHexDigit=16進エンコーディングãŒç„¡åŠ¹ã§ã™ +uDecoder.noSlash="/" を符å·åŒ–ã—ã¦å«ã‚ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +uDecoder.urlDecode.conversionError=文字セット [{1}] を使用ã—㟠[{0}] ã®ãƒ‡ã‚³ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—㟠+uDecoder.urlDecode.missingDigit=%文字ã®å¾Œã‚ã«2ã¤ã®16進数字ãŒç¶šãå¿…è¦ãŒã‚ã‚‹ãŸã‚ã€[{0}]ã®ãƒ‡ã‚³ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ diff --git a/java/org/apache/tomcat/util/buf/LocalStrings_ko.properties b/java/org/apache/tomcat/util/buf/LocalStrings_ko.properties new file mode 100644 index 0000000..1341cfd --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings_ko.properties @@ -0,0 +1,35 @@ +# 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. + +asn1Parser.lengthInvalid=ìž…ë ¥ ë°ì´í„°ì˜ ë°”ì´íŠ¸ 길ì´ê°€ [{1}]ì¸ë°, 유효하지 ì•Šì€ ë°”ì´íŠ¸ ê¸¸ì´ [{0}](ì´)ê°€ ë³´ê³ ë˜ì—ˆìŠµë‹ˆë‹¤. +asn1Parser.tagMismatch=[{0}] ê°’ì´ ê¸°ëŒ€ ë˜ì—ˆëŠ”ë°, [{1}] ê°’ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤. + +b2cConverter.unknownEncoding=ë¬¸ìž ì¸ì½”딩 [{0}]ì€(는) 지ì›ë˜ì§€ 않습니다. + +byteBufferUtils.cleaner=ì§ì ‘ì ì¸ ByteBuffer cleaner를 사용할 수 없습니다. 메모리 누수가 ë°œìƒí•  수 있습니다. + +chunk.overflow=ë²„í¼ ì˜¤ë²„í”Œë¡œìš°ê°€ ë°œìƒí–ˆê³ , sinkê°€ 설정ë˜ì–´ 있지 않습니다. 한계값: [{0}], ë²„í¼ ê¸¸ì´: [{1}] + +encodedSolidusHandling.invalid=해당 ê°’ [{0}]ì€(는) ì¸ì‹ë˜ì§€ 않습니다. + +hexUtils.fromHex.nonHex=ìž…ë ¥ì€ ì˜¤ì§ 16진수 숫ìžë¡œë§Œ ì´ë£¨ì–´ì ¸ì•¼ 합니다. +hexUtils.fromHex.oddDigits=ìž…ë ¥ì€ ë°˜ë“œì‹œ ì§ìˆ˜ ê°œì˜ 16진수 숫ìžë“¤ë¡œ ì´ë£¨ì–´ì ¸ì•¼ 합니다. + +messageBytes.illegalCharacter=code point [{1}]ì— ìœ„ì¹˜í•œ 유니코드 ë¬¸ìž [{0}]ì€(는), 0ì—ì„œ 255ê¹Œì§€ì˜ í—ˆìš© 범위 ë°”ê¹¥ì— ìžˆìœ¼ë¯€ë¡œ ì¸ì½”ë”©ë  ìˆ˜ 없습니다. + +uDecoder.eof=파ì¼ì˜ ë (EOF) +uDecoder.noSlash=ì¸ì½”ë”©ëœ ìŠ¬ëž˜ì‹œ 문ìžëŠ” 허용ë˜ì§€ 않습니다. +uDecoder.urlDecode.conversionError=문ìžì…‹ [{1}]ì„(를) 사용하여 [{0}]ì„(를) 디코드하지 못했습니다. +uDecoder.urlDecode.missingDigit=% ë¬¸ìž ë’¤ì— ë‘ ê°œì˜ 16진수 숫ìžë“¤ì´ ì´ì–´ì ¸ì•¼ 하기 때문ì—, [{0}]ì„(를) 디코드하지 못했습니다. diff --git a/java/org/apache/tomcat/util/buf/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/buf/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..0ff8d88 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +messageBytes.illegalCharacter=Caracter Unicode [{0}] no code point [{1}] não pode ser codificado porque está fora do limite (de 0 a 255) diff --git a/java/org/apache/tomcat/util/buf/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/buf/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..8f93702 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/LocalStrings_zh_CN.properties @@ -0,0 +1,35 @@ +# 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. + +asn1Parser.lengthInvalid=无效长度 [{0}]字节报告,但是输入数æ®çš„长度是 [{1}]字节 +asn1Parser.tagMismatch=期望找到值 [{0}]但是å´æ‰¾åˆ°å€¼ [{1}] + +b2cConverter.unknownEncoding=ä¸æ”¯æŒå­—符编ç [{0}] + +byteBufferUtils.cleaner=无法使用直接ByteBuffer清æ´å‰‚,å¯èƒ½ä¼šå‘ç”Ÿå†…å­˜æ³„æ¼ + +chunk.overflow=缓冲区溢出且未设置接收器,请é™åˆ¶[{0}]和缓冲区长度[{1}] + +encodedSolidusHandling.invalid=值[{0}]未识别 + +hexUtils.fromHex.nonHex=输入åªèƒ½ç”±åå…­è¿›åˆ¶æ•°å­—ç»„æˆ +hexUtils.fromHex.oddDigits=输入必须由å¶æ•°ä¸ªåå…­è¿›åˆ¶æ•°å­—ç»„æˆ + +messageBytes.illegalCharacter=代ç ç‚¹[{1}]处的Unicode字符[{0}]无法编ç ï¼Œå› ä¸ºå®ƒè¶…出了å…许的0到255范围。 + +uDecoder.eof=文件结尾(EOF) +uDecoder.noSlash=ä¸å…许使用编ç çš„æ–œæ å­—符 +uDecoder.urlDecode.conversionError=使用编ç [{1}]解ç [{0}]失败 +uDecoder.urlDecode.missingDigit=无法解ç [{0}],因为%字符必须åŽè·Ÿä¸¤ä¸ªå六进制数字。 diff --git a/java/org/apache/tomcat/util/buf/MessageBytes.java b/java/org/apache/tomcat/util/buf/MessageBytes.java new file mode 100644 index 0000000..70ac7d9 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/MessageBytes.java @@ -0,0 +1,656 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.Locale; + +import org.apache.tomcat.util.res.StringManager; + +/** + * This class is used to represent a subarray of bytes in an HTTP message. It represents all request/response elements. + * The byte/char conversions are delayed and cached. Everything is recyclable. + *

    + * The object can represent a byte[], a char[], or a (sub) String. All operations can be made in case sensitive mode or + * not. + * + * @author dac@eng.sun.com + * @author James Todd [gonzo@eng.sun.com] + * @author Costin Manolache + */ +public final class MessageBytes implements Cloneable, Serializable { + + private static final long serialVersionUID = 1L; + + private static final StringManager sm = StringManager.getManager(MessageBytes.class); + + // primary type ( whatever is set as original value ) + private int type = T_NULL; + + public static final int T_NULL = 0; + /** + * getType() is T_STR if the the object used to create the MessageBytes was a String. + */ + public static final int T_STR = 1; + /** + * getType() is T_BYTES if the the object used to create the MessageBytes was a byte[]. + */ + public static final int T_BYTES = 2; + /** + * getType() is T_CHARS if the the object used to create the MessageBytes was a char[]. + */ + public static final int T_CHARS = 3; + + public static final char[] EMPTY_CHAR_ARRAY = new char[0]; + + private int hashCode = 0; + // did we compute the hashcode ? + private boolean hasHashCode = false; + + // Internal objects to represent array + offset, and specific methods + private final ByteChunk byteC = new ByteChunk(); + private final CharChunk charC = new CharChunk(); + + // String + private String strValue; + + /** + * Creates a new, uninitialized MessageBytes object. Use static newInstance() in order to allow future hooks. + */ + private MessageBytes() { + } + + /** + * Construct a new MessageBytes instance. + * + * @return the instance + */ + public static MessageBytes newInstance() { + return factory.newInstance(); + } + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public boolean isNull() { + return type == T_NULL; + } + + /** + * Resets the message bytes to an uninitialized (NULL) state. + */ + public void recycle() { + type = T_NULL; + byteC.recycle(); + charC.recycle(); + + strValue = null; + + hasHashCode = false; + hasLongValue = false; + } + + + /** + * Sets the content to the specified subarray of bytes. + * + * @param b the bytes + * @param off the start offset of the bytes + * @param len the length of the bytes + */ + public void setBytes(byte[] b, int off, int len) { + byteC.setBytes(b, off, len); + type = T_BYTES; + hasHashCode = false; + hasLongValue = false; + } + + /** + * Sets the content to be a char[] + * + * @param c the chars + * @param off the start offset of the chars + * @param len the length of the chars + */ + public void setChars(char[] c, int off, int len) { + charC.setChars(c, off, len); + type = T_CHARS; + hasHashCode = false; + hasLongValue = false; + } + + /** + * Set the content to be a string + * + * @param s The string + */ + public void setString(String s) { + strValue = s; + hasHashCode = false; + hasLongValue = false; + if (s == null) { + type = T_NULL; + } else { + type = T_STR; + } + } + + // -------------------- Conversion and getters -------------------- + + /** + * Compute the string value. + * + * @return the string + */ + @Override + public String toString() { + switch (type) { + case T_NULL: + case T_STR: + // No conversion required + break; + case T_BYTES: + strValue = byteC.toString(); + break; + case T_CHARS: + strValue = charC.toString(); + break; + } + + return strValue; + } + + + /** + * Convert to String (if not already of the String type) and then return the String value. + * + * @return The current value as a String + */ + public String toStringType() { + switch (type) { + case T_NULL: + case T_STR: + // No conversion required + break; + case T_BYTES: + setString(byteC.toString()); + break; + case T_CHARS: + setString(charC.toString()); + break; + } + + return strValue; + } + + + // ---------------------------------------- + /** + * Return the type of the original content. Can be T_STR, T_BYTES, T_CHARS or T_NULL + * + * @return the type + */ + public int getType() { + return type; + } + + /** + * Returns the byte chunk, representing the byte[] and offset/length. Valid only if T_BYTES or after a conversion + * was made. + * + * @return the byte chunk + */ + public ByteChunk getByteChunk() { + return byteC; + } + + /** + * Returns the char chunk, representing the char[] and offset/length. Valid only if T_CHARS or after a conversion + * was made. + * + * @return the char chunk + */ + public CharChunk getCharChunk() { + return charC; + } + + /** + * Returns the string value. Valid only if T_STR or after a conversion was made. + * + * @return the string + */ + public String getString() { + return strValue; + } + + /** + * @return the Charset used for string<->byte conversions. + */ + public Charset getCharset() { + return byteC.getCharset(); + } + + /** + * Set the Charset used for string<->byte conversions. + * + * @param charset The charset + */ + public void setCharset(Charset charset) { + byteC.setCharset(charset); + } + + + /** + * Convert to bytes and fill the ByteChunk with the converted value. + */ + public void toBytes() { + if (type == T_NULL) { + byteC.recycle(); + return; + } + + if (type == T_BYTES) { + // No conversion required + return; + } + + if (getCharset() == ByteChunk.DEFAULT_CHARSET) { + if (type == T_CHARS) { + toBytesSimple(charC.getChars(), charC.getStart(), charC.getLength()); + } else { + // Must be T_STR + char[] chars = strValue.toCharArray(); + toBytesSimple(chars, 0, chars.length); + } + return; + } + + ByteBuffer bb; + if (type == T_CHARS) { + bb = getCharset().encode(CharBuffer.wrap(charC)); + } else { + // Must be T_STR + bb = getCharset().encode(strValue); + } + + byteC.setBytes(bb.array(), bb.arrayOffset(), bb.limit()); + } + + + /** + * Simple conversion of chars to bytes. + * + * @throws IllegalArgumentException if any of the characters to convert are above code point 0xFF. + */ + private void toBytesSimple(char[] chars, int start, int len) { + byte[] bytes = new byte[len]; + + for (int i = 0; i < len; i++) { + if (chars[i + start] > 255) { + throw new IllegalArgumentException(sm.getString("messageBytes.illegalCharacter", + Character.toString(chars[i + start]), Integer.valueOf(chars[i + start]))); + } else { + bytes[i] = (byte) chars[i + start]; + } + } + + byteC.setBytes(bytes, 0, len); + type = T_BYTES; + } + + + /** + * Convert to char[] and fill the CharChunk. + *

    + * Note: The conversion from bytes is not optimised - it converts to String first. However, Tomcat doesn't call this + * method to convert from bytes so there is no benefit from optimising that path. + */ + public void toChars() { + switch (type) { + case T_NULL: + charC.recycle(); + //$FALL-THROUGH$ + case T_CHARS: + // No conversion required + return; + case T_BYTES: + toString(); + //$FALL-THROUGH$ + case T_STR: { + char cc[] = strValue.toCharArray(); + charC.setChars(cc, 0, cc.length); + } + } + } + + + /** + * Returns the length of the original buffer. + *

    + * Note: The length in bytes may be different from the length in chars. + * + * @return the length + */ + public int getLength() { + if (type == T_BYTES) { + return byteC.getLength(); + } + if (type == T_CHARS) { + return charC.getLength(); + } + if (type == T_STR) { + return strValue.length(); + } + toString(); + if (strValue == null) { + return 0; + } + return strValue.length(); + } + + // -------------------- equals -------------------- + + /** + * Compares the message bytes to the specified String object. + * + * @param s the String to compare + * + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(String s) { + switch (type) { + case T_STR: + if (strValue == null) { + return s == null; + } + return strValue.equals(s); + case T_CHARS: + return charC.equals(s); + case T_BYTES: + return byteC.equals(s); + default: + return false; + } + } + + /** + * Compares the message bytes to the specified String object. + * + * @param s the String to compare + * + * @return true if the comparison succeeded, false otherwise + */ + public boolean equalsIgnoreCase(String s) { + switch (type) { + case T_STR: + if (strValue == null) { + return s == null; + } + return strValue.equalsIgnoreCase(s); + case T_CHARS: + return charC.equalsIgnoreCase(s); + case T_BYTES: + return byteC.equalsIgnoreCase(s); + default: + return false; + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof MessageBytes) { + return equals((MessageBytes) obj); + } + return false; + } + + public boolean equals(MessageBytes mb) { + switch (type) { + case T_STR: + return mb.equals(strValue); + } + + if (mb.type != T_CHARS && mb.type != T_BYTES) { + // it's a string or int/date string value + return equals(mb.toString()); + } + + // mb is either CHARS or BYTES. + // this is either CHARS or BYTES + // Deal with the 4 cases ( in fact 3, one is symmetric) + + if (mb.type == T_CHARS && type == T_CHARS) { + return charC.equals(mb.charC); + } + if (mb.type == T_BYTES && type == T_BYTES) { + return byteC.equals(mb.byteC); + } + if (mb.type == T_CHARS && type == T_BYTES) { + return byteC.equals(mb.charC); + } + if (mb.type == T_BYTES && type == T_CHARS) { + return mb.byteC.equals(charC); + } + // can't happen + return true; + } + + + /** + * @return true if the message bytes starts with the specified string. + * + * @param s the string + * @param pos The start position + */ + public boolean startsWithIgnoreCase(String s, int pos) { + switch (type) { + case T_STR: + if (strValue == null) { + return false; + } + if (strValue.length() < pos + s.length()) { + return false; + } + + for (int i = 0; i < s.length(); i++) { + if (Ascii.toLower(s.charAt(i)) != Ascii.toLower(strValue.charAt(pos + i))) { + return false; + } + } + return true; + case T_CHARS: + return charC.startsWithIgnoreCase(s, pos); + case T_BYTES: + return byteC.startsWithIgnoreCase(s, pos); + default: + return false; + } + } + + + // -------------------- Hash code -------------------- + @Override + public int hashCode() { + if (hasHashCode) { + return hashCode; + } + int code = 0; + + code = hash(); + hashCode = code; + hasHashCode = true; + return code; + } + + // normal hash. + private int hash() { + int code = 0; + switch (type) { + case T_STR: + // We need to use the same hash function + for (int i = 0; i < strValue.length(); i++) { + code = code * 37 + strValue.charAt(i); + } + return code; + case T_CHARS: + return charC.hash(); + case T_BYTES: + return byteC.hash(); + default: + return 0; + } + } + + // Inefficient initial implementation. Will be replaced on the next + // round of tune-up + public int indexOf(String s, int starting) { + toString(); + return strValue.indexOf(s, starting); + } + + // Inefficient initial implementation. Will be replaced on the next + // round of tune-up + public int indexOf(String s) { + return indexOf(s, 0); + } + + public int indexOfIgnoreCase(String s, int starting) { + toString(); + String upper = strValue.toUpperCase(Locale.ENGLISH); + String sU = s.toUpperCase(Locale.ENGLISH); + return upper.indexOf(sU, starting); + } + + /** + * Copy the src into this MessageBytes, allocating more space if needed. + * + * @param src The source + * + * @throws IOException Writing overflow data to the output channel failed + */ + public void duplicate(MessageBytes src) throws IOException { + switch (src.getType()) { + case T_BYTES: + type = T_BYTES; + ByteChunk bc = src.getByteChunk(); + byteC.allocate(2 * bc.getLength(), -1); + byteC.append(bc); + break; + case T_CHARS: + type = T_CHARS; + CharChunk cc = src.getCharChunk(); + charC.allocate(2 * cc.getLength(), -1); + charC.append(cc); + break; + case T_STR: + type = T_STR; + String sc = src.getString(); + this.setString(sc); + break; + } + setCharset(src.getCharset()); + } + + // efficient long + private long longValue; + private boolean hasLongValue = false; + + /** + * Set the buffer to the representation of a long. + * + * @param l The long + */ + public void setLong(long l) { + byteC.allocate(32, 64); + long current = l; + byte[] buf = byteC.getBuffer(); + int start = 0; + int end = 0; + if (l == 0) { + buf[end++] = (byte) '0'; + } + if (l < 0) { + current = -l; + buf[end++] = (byte) '-'; + } + while (current > 0) { + int digit = (int) (current % 10); + current = current / 10; + buf[end++] = HexUtils.getHex(digit); + } + byteC.setOffset(0); + byteC.setEnd(end); + // Inverting buffer + end--; + if (l < 0) { + start++; + } + while (end > start) { + byte temp = buf[start]; + buf[start] = buf[end]; + buf[end] = temp; + start++; + end--; + } + longValue = l; + hasHashCode = false; + hasLongValue = true; + type = T_BYTES; + } + + /** + * Convert the buffer to a long, cache the value. Used for headers conversion. + * + * @return the long value + */ + public long getLong() { + if (hasLongValue) { + return longValue; + } + + switch (type) { + case T_BYTES: + longValue = byteC.getLong(); + break; + default: + longValue = Long.parseLong(toString()); + } + + hasLongValue = true; + return longValue; + + } + + // -------------------- Future may be different -------------------- + + private static final MessageBytesFactory factory = new MessageBytesFactory(); + + private static class MessageBytesFactory { + protected MessageBytesFactory() { + } + + public MessageBytes newInstance() { + return new MessageBytes(); + } + } +} diff --git a/java/org/apache/tomcat/util/buf/StringCache.java b/java/org/apache/tomcat/util/buf/StringCache.java new file mode 100644 index 0000000..8f0c664 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/StringCache.java @@ -0,0 +1,739 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.TreeMap; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * This class implements a String cache for ByteChunk and CharChunk. + * + * @author Remy Maucherat + */ +public class StringCache { + + + private static final Log log = LogFactory.getLog(StringCache.class); + private static final StringManager sm = StringManager.getManager(StringCache.class); + + + // ------------------------------------------------------- Static Variables + + + /** + * Enabled ? + */ + protected static boolean byteEnabled = Boolean.getBoolean("tomcat.util.buf.StringCache.byte.enabled"); + + + protected static boolean charEnabled = Boolean.getBoolean("tomcat.util.buf.StringCache.char.enabled"); + + + protected static int trainThreshold = + Integer.getInteger("tomcat.util.buf.StringCache.trainThreshold", 20000).intValue(); + + + protected static int cacheSize = Integer.getInteger("tomcat.util.buf.StringCache.cacheSize", 200).intValue(); + + + protected static final int maxStringSize = + Integer.getInteger("tomcat.util.buf.StringCache.maxStringSize", 128).intValue(); + + + /** + * Statistics hash map for byte chunk. + */ + protected static final HashMap bcStats = new HashMap<>(cacheSize); + + + /** + * toString count for byte chunk. + */ + protected static int bcCount = 0; + + + /** + * Cache for byte chunk. + */ + protected static volatile ByteEntry[] bcCache = null; + + + /** + * Statistics hash map for char chunk. + */ + protected static final HashMap ccStats = new HashMap<>(cacheSize); + + + /** + * toString count for char chunk. + */ + protected static int ccCount = 0; + + + /** + * Cache for char chunk. + */ + protected static volatile CharEntry[] ccCache = null; + + + /** + * Access count. + */ + protected static int accessCount = 0; + + + /** + * Hit count. + */ + protected static int hitCount = 0; + + + // ------------------------------------------------------------ Properties + + + /** + * @return Returns the cacheSize. + */ + public int getCacheSize() { + return cacheSize; + } + + + /** + * @param cacheSize The cacheSize to set. + */ + public void setCacheSize(int cacheSize) { + StringCache.cacheSize = cacheSize; + } + + + /** + * @return Returns the enabled. + */ + public boolean getByteEnabled() { + return byteEnabled; + } + + + /** + * @param byteEnabled The enabled to set. + */ + public void setByteEnabled(boolean byteEnabled) { + StringCache.byteEnabled = byteEnabled; + } + + + /** + * @return Returns the enabled. + */ + public boolean getCharEnabled() { + return charEnabled; + } + + + /** + * @param charEnabled The enabled to set. + */ + public void setCharEnabled(boolean charEnabled) { + StringCache.charEnabled = charEnabled; + } + + + /** + * @return Returns the trainThreshold. + */ + public int getTrainThreshold() { + return trainThreshold; + } + + + /** + * @param trainThreshold The trainThreshold to set. + */ + public void setTrainThreshold(int trainThreshold) { + StringCache.trainThreshold = trainThreshold; + } + + + /** + * @return Returns the accessCount. + */ + public int getAccessCount() { + return accessCount; + } + + + /** + * @return Returns the hitCount. + */ + public int getHitCount() { + return hitCount; + } + + + // -------------------------------------------------- Public Static Methods + + + public void reset() { + hitCount = 0; + accessCount = 0; + synchronized (bcStats) { + bcCache = null; + bcCount = 0; + } + synchronized (ccStats) { + ccCache = null; + ccCount = 0; + } + } + + + public static String toString(ByteChunk bc) { + try { + return toString(bc, CodingErrorAction.REPLACE, CodingErrorAction.REPLACE); + } catch (CharacterCodingException e) { + // Unreachable code. Use of REPLACE above means the exception will never be thrown. + throw new IllegalStateException(e); + } + } + + + public static String toString(ByteChunk bc, CodingErrorAction malformedInputAction, + CodingErrorAction unmappableCharacterAction) throws CharacterCodingException { + + // If the cache is null, then either caching is disabled, or we're + // still training + if (bcCache == null) { + String value = bc.toStringInternal(malformedInputAction, unmappableCharacterAction); + if (byteEnabled && (value.length() < maxStringSize)) { + // If training, everything is synced + synchronized (bcStats) { + // If the cache has been generated on a previous invocation + // while waiting for the lock, just return the toString + // value we just calculated + if (bcCache != null) { + return value; + } + // Two cases: either we just exceeded the train count, in + // which case the cache must be created, or we just update + // the count for the string + if (bcCount > trainThreshold) { + long t1 = System.currentTimeMillis(); + // Sort the entries according to occurrence + TreeMap> tempMap = new TreeMap<>(); + for (Entry item : bcStats.entrySet()) { + ByteEntry entry = item.getKey(); + int[] countA = item.getValue(); + Integer count = Integer.valueOf(countA[0]); + // Add to the list for that count + tempMap.computeIfAbsent(count, k -> new ArrayList<>()).add(entry); + } + // Allocate array of the right size + int size = bcStats.size(); + if (size > cacheSize) { + size = cacheSize; + } + ByteEntry[] tempbcCache = new ByteEntry[size]; + // Fill it up using an alphabetical order + // and a dumb insert sort + ByteChunk tempChunk = new ByteChunk(); + int n = 0; + while (n < size) { + Object key = tempMap.lastKey(); + ArrayList list = tempMap.get(key); + for (int i = 0; i < list.size() && n < size; i++) { + ByteEntry entry = list.get(i); + tempChunk.setBytes(entry.name, 0, entry.name.length); + int insertPos = findClosest(tempChunk, tempbcCache, n); + if (insertPos == n) { + tempbcCache[n + 1] = entry; + } else { + System.arraycopy(tempbcCache, insertPos + 1, tempbcCache, insertPos + 2, + n - insertPos - 1); + tempbcCache[insertPos + 1] = entry; + } + n++; + } + tempMap.remove(key); + } + bcCount = 0; + bcStats.clear(); + bcCache = tempbcCache; + if (log.isDebugEnabled()) { + long t2 = System.currentTimeMillis(); + log.debug(sm.getString("stringCache.byteTime", Long.valueOf(t2 - t1))); + } + } else { + bcCount++; + // Allocate new ByteEntry for the lookup + ByteEntry entry = new ByteEntry(); + entry.value = value; + int[] count = bcStats.get(entry); + if (count == null) { + int end = bc.getEnd(); + int start = bc.getStart(); + // Create byte array and copy bytes + entry.name = new byte[bc.getLength()]; + System.arraycopy(bc.getBuffer(), start, entry.name, 0, end - start); + // Set encoding + entry.charset = bc.getCharset(); + entry.malformedInputAction = malformedInputAction; + entry.unmappableCharacterAction = unmappableCharacterAction; + // Initialize occurrence count to one + count = new int[1]; + count[0] = 1; + // Set in the stats hash map + bcStats.put(entry, count); + } else { + count[0] = count[0] + 1; + } + } + } + } + return value; + } else { + accessCount++; + // Find the corresponding String + String result = find(bc, malformedInputAction, unmappableCharacterAction); + if (result == null) { + return bc.toStringInternal(malformedInputAction, unmappableCharacterAction); + } + // Note: We don't care about safety for the stats + hitCount++; + return result; + } + + } + + + public static String toString(CharChunk cc) { + + // If the cache is null, then either caching is disabled, or we're + // still training + if (ccCache == null) { + String value = cc.toStringInternal(); + if (charEnabled && (value.length() < maxStringSize)) { + // If training, everything is synced + synchronized (ccStats) { + // If the cache has been generated on a previous invocation + // while waiting for the lock, just return the toString + // value we just calculated + if (ccCache != null) { + return value; + } + // Two cases: either we just exceeded the train count, in + // which case the cache must be created, or we just update + // the count for the string + if (ccCount > trainThreshold) { + long t1 = System.currentTimeMillis(); + // Sort the entries according to occurrence + TreeMap> tempMap = new TreeMap<>(); + for (Entry item : ccStats.entrySet()) { + CharEntry entry = item.getKey(); + int[] countA = item.getValue(); + Integer count = Integer.valueOf(countA[0]); + // Add to the list for that count + ArrayList list = tempMap.computeIfAbsent(count, k -> new ArrayList<>()); + list.add(entry); + } + // Allocate array of the right size + int size = ccStats.size(); + if (size > cacheSize) { + size = cacheSize; + } + CharEntry[] tempccCache = new CharEntry[size]; + // Fill it up using an alphabetical order + // and a dumb insert sort + CharChunk tempChunk = new CharChunk(); + int n = 0; + while (n < size) { + Object key = tempMap.lastKey(); + ArrayList list = tempMap.get(key); + for (int i = 0; i < list.size() && n < size; i++) { + CharEntry entry = list.get(i); + tempChunk.setChars(entry.name, 0, entry.name.length); + int insertPos = findClosest(tempChunk, tempccCache, n); + if (insertPos == n) { + tempccCache[n + 1] = entry; + } else { + System.arraycopy(tempccCache, insertPos + 1, tempccCache, insertPos + 2, + n - insertPos - 1); + tempccCache[insertPos + 1] = entry; + } + n++; + } + tempMap.remove(key); + } + ccCount = 0; + ccStats.clear(); + ccCache = tempccCache; + if (log.isDebugEnabled()) { + long t2 = System.currentTimeMillis(); + log.debug(sm.getString("stringCache.charTime", Long.valueOf(t2 - t1))); + } + } else { + ccCount++; + // Allocate new CharEntry for the lookup + CharEntry entry = new CharEntry(); + entry.value = value; + int[] count = ccStats.get(entry); + if (count == null) { + int end = cc.getEnd(); + int start = cc.getStart(); + // Create char array and copy chars + entry.name = new char[cc.getLength()]; + System.arraycopy(cc.getBuffer(), start, entry.name, 0, end - start); + // Initialize occurrence count to one + count = new int[1]; + count[0] = 1; + // Set in the stats hash map + ccStats.put(entry, count); + } else { + count[0] = count[0] + 1; + } + } + } + } + return value; + } else { + accessCount++; + // Find the corresponding String + String result = find(cc); + if (result == null) { + return cc.toStringInternal(); + } + // Note: We don't care about safety for the stats + hitCount++; + return result; + } + + } + + + // ----------------------------------------------------- Protected Methods + + + /** + * Compare given byte chunk with byte array. + * + * @param name The name to compare + * @param compareTo The compared to data + * + * @return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + protected static final int compare(ByteChunk name, byte[] compareTo) { + int result = 0; + + byte[] b = name.getBuffer(); + int start = name.getStart(); + int end = name.getEnd(); + int len = compareTo.length; + + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (b[i + start] > compareTo[i]) { + result = 1; + } else if (b[i + start] < compareTo[i]) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length > (end - start)) { + result = -1; + } else if (compareTo.length < (end - start)) { + result = 1; + } + } + return result; + } + + + /** + * Find an entry given its name in the cache and return the associated String. + * + * @param name The name to find + * + * @return the corresponding value + * + * @deprecated Unused. Will be removed in Tomcat 11. + * Use {@link #find(ByteChunk, CodingErrorAction, CodingErrorAction)} + */ + @Deprecated + protected static final String find(ByteChunk name) { + return find(name, CodingErrorAction.REPLACE, CodingErrorAction.REPLACE); + } + + + /** + * Find an entry given its name in the cache and return the associated String. + * + * @param name The name to find + * @param malformedInputAction Action to take if an malformed input is encountered + * @param unmappableCharacterAction Action to take if an unmappable character is encountered + * + * @return the corresponding value + */ + protected static final String find(ByteChunk name, CodingErrorAction malformedInputAction, + CodingErrorAction unmappableCharacterAction) { + int pos = findClosest(name, bcCache, bcCache.length); + if ((pos < 0) || (compare(name, bcCache[pos].name) != 0) || !(name.getCharset().equals(bcCache[pos].charset)) || + !malformedInputAction.equals(bcCache[pos].malformedInputAction) || + !unmappableCharacterAction.equals(bcCache[pos].unmappableCharacterAction)) { + return null; + } else { + return bcCache[pos].value; + } + } + + + /** + * Find an entry given its name in a sorted array of map elements. This will return the index for the closest + * inferior or equal item in the given array. + * + * @param name The name to find + * @param array The array in which to look + * @param len The effective length of the array + * + * @return the position of the best match + */ + protected static final int findClosest(ByteChunk name, ByteEntry[] array, int len) { + + int a = 0; + int b = len - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (compare(name, array[0].name) < 0) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) >>> 1; + int result = compare(name, array[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compare(name, array[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + + /** + * Compare given char chunk with char array. + * + * @param name The name to compare + * @param compareTo The compared to data + * + * @return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + protected static final int compare(CharChunk name, char[] compareTo) { + int result = 0; + + char[] c = name.getBuffer(); + int start = name.getStart(); + int end = name.getEnd(); + int len = compareTo.length; + + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (c[i + start] > compareTo[i]) { + result = 1; + } else if (c[i + start] < compareTo[i]) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length > (end - start)) { + result = -1; + } else if (compareTo.length < (end - start)) { + result = 1; + } + } + return result; + } + + + /** + * Find an entry given its name in the cache and return the associated String. + * + * @param name The name to find + * + * @return the corresponding value + */ + protected static final String find(CharChunk name) { + int pos = findClosest(name, ccCache, ccCache.length); + if ((pos < 0) || (compare(name, ccCache[pos].name) != 0)) { + return null; + } else { + return ccCache[pos].value; + } + } + + + /** + * Find an entry given its name in a sorted array of map elements. This will return the index for the closest + * inferior or equal item in the given array. + * + * @param name The name to find + * @param array The array in which to look + * @param len The effective length of the array + * + * @return the position of the best match + */ + protected static final int findClosest(CharChunk name, CharEntry[] array, int len) { + + int a = 0; + int b = len - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (compare(name, array[0].name) < 0) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) >>> 1; + int result = compare(name, array[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compare(name, array[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + + // -------------------------------------------------- ByteEntry Inner Class + + private static class ByteEntry { + + private byte[] name = null; + private Charset charset = null; + private CodingErrorAction malformedInputAction = null; + private CodingErrorAction unmappableCharacterAction = null; + private String value = null; + + @Override + public String toString() { + return value; + } + + @Override + public int hashCode() { + return Objects.hash(malformedInputAction, unmappableCharacterAction, value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ByteEntry other = (ByteEntry) obj; + return Objects.equals(malformedInputAction, other.malformedInputAction) && + Objects.equals(unmappableCharacterAction, other.unmappableCharacterAction) && + Objects.equals(value, other.value); + } + } + + + // -------------------------------------------------- CharEntry Inner Class + + + private static class CharEntry { + + private char[] name = null; + private String value = null; + + @Override + public String toString() { + return value; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof CharEntry) { + return value.equals(((CharEntry) obj).value); + } + return false; + } + + } + + +} diff --git a/java/org/apache/tomcat/util/buf/StringUtils.java b/java/org/apache/tomcat/util/buf/StringUtils.java new file mode 100644 index 0000000..f339021 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/StringUtils.java @@ -0,0 +1,121 @@ +/* + * 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.tomcat.util.buf; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Function; + +/** + * Utility methods to build a separated list from a given set (not java.util.Set) of inputs and return that list as a + * string or append it to an existing StringBuilder. If the given set is null or empty, an empty string will be + * returned. + */ +public final class StringUtils { + + private static final String EMPTY_STRING = ""; + + private StringUtils() { + // Utility class + } + + + public static String join(String[] array) { + if (array == null) { + return EMPTY_STRING; + } + return join(Arrays.asList(array)); + } + + + public static void join(String[] array, char separator, StringBuilder sb) { + if (array == null) { + return; + } + join(Arrays.asList(array), separator, sb); + } + + + public static String join(Collection collection) { + return join(collection, ','); + } + + + public static String join(Collection collection, char separator) { + // Shortcut + if (collection == null || collection.isEmpty()) { + return EMPTY_STRING; + } + + StringBuilder result = new StringBuilder(); + join(collection, separator, result); + return result.toString(); + } + + + public static void join(Iterable iterable, char separator, StringBuilder sb) { + join(iterable, separator, (x) -> x, sb); + } + + + public static void join(T[] array, char separator, Function function, StringBuilder sb) { + if (array == null) { + return; + } + join(Arrays.asList(array), separator, function, sb); + } + + + public static void join(Iterable iterable, char separator, Function function, StringBuilder sb) { + if (iterable == null) { + return; + } + boolean first = true; + for (T value : iterable) { + if (first) { + first = false; + } else { + sb.append(separator); + } + sb.append(function.apply(value)); + } + } + + /** + * Splits a comma-separated string into an array of String values. + * + * Whitespace around the commas is removed. + * + * Null or empty values will return a zero-element array. + * + * @param s The string to split by commas. + * + * @return An array of String values. + */ + public static String[] splitCommaSeparated(String s) { + if (s == null || s.length() == 0) { + return new String[0]; + } + + String[] splits = s.split(","); + for (int i = 0; i < splits.length; ++i) { + splits[i] = splits[i].trim(); + } + + return splits; + } +} diff --git a/java/org/apache/tomcat/util/buf/UDecoder.java b/java/org/apache/tomcat/util/buf/UDecoder.java new file mode 100644 index 0000000..ec9a4b7 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/UDecoder.java @@ -0,0 +1,255 @@ +/* + * 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.tomcat.util.buf; + +import java.io.ByteArrayOutputStream; +import java.io.CharConversionException; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.apache.tomcat.util.res.StringManager; + +/** + * All URL decoding happens here. This way we can reuse, review, optimize without adding complexity to the buffers. The + * conversion will modify the original buffer. + * + * @author Costin Manolache + */ +public final class UDecoder { + + private static final StringManager sm = StringManager.getManager(UDecoder.class); + + private static class DecodeException extends CharConversionException { + private static final long serialVersionUID = 1L; + + DecodeException(String s) { + super(s); + } + + @Override + public synchronized Throwable fillInStackTrace() { + // This class does not provide a stack trace + return this; + } + } + + /** Unexpected end of data. */ + private static final IOException EXCEPTION_EOF = new DecodeException(sm.getString("uDecoder.eof")); + + /** %xx with not-hex digit */ + private static final IOException EXCEPTION_NOT_HEX_DIGIT = new DecodeException(sm.getString("uDecoder.isHexDigit")); + + /** %-encoded slash is forbidden in resource path */ + private static final IOException EXCEPTION_SLASH = new DecodeException(sm.getString("uDecoder.noSlash")); + + + /** + * URLDecode, will modify the source. Assumes source bytes are encoded using a superset of US-ASCII as per RFC 7230. + * "%2f" will be rejected unless the input is a query string. + * + * @param mb The URL encoded bytes + * @param query {@code true} if this is a query string. For a query string '+' will be decoded to ' ' + * + * @throws IOException Invalid %xx URL encoding + */ + public void convert(ByteChunk mb, boolean query) throws IOException { + if (query) { + convert(mb, true, EncodedSolidusHandling.DECODE); + } else { + convert(mb, false, EncodedSolidusHandling.REJECT); + } + } + + + /** + * URLDecode, will modify the source. Assumes source bytes are encoded using a superset of US-ASCII as per RFC 7230. + * + * @param mb The URL encoded bytes + * @param encodedSolidusHandling How should the %2f sequence handled by the decoder? For query strings this + * parameter will be ignored and the %2f sequence will be decoded + * + * @throws IOException Invalid %xx URL encoding + */ + public void convert(ByteChunk mb, EncodedSolidusHandling encodedSolidusHandling) throws IOException { + convert(mb, false, encodedSolidusHandling); + } + + + private void convert(ByteChunk mb, boolean query, EncodedSolidusHandling encodedSolidusHandling) + throws IOException { + + int start = mb.getOffset(); + byte buff[] = mb.getBytes(); + int end = mb.getEnd(); + + int idx = ByteChunk.findByte(buff, start, end, (byte) '%'); + int idx2 = -1; + if (query) { + idx2 = ByteChunk.findByte(buff, start, (idx >= 0 ? idx : end), (byte) '+'); + } + if (idx < 0 && idx2 < 0) { + return; + } + + // idx will be the smallest positive index ( first % or + ) + if ((idx2 >= 0 && idx2 < idx) || idx < 0) { + idx = idx2; + } + + for (int j = idx; j < end; j++, idx++) { + if (buff[j] == '+' && query) { + buff[idx] = (byte) ' '; + } else if (buff[j] != '%') { + buff[idx] = buff[j]; + } else { + // read next 2 digits + if (j + 2 >= end) { + throw EXCEPTION_EOF; + } + byte b1 = buff[j + 1]; + byte b2 = buff[j + 2]; + if (!isHexDigit(b1) || !isHexDigit(b2)) { + throw EXCEPTION_NOT_HEX_DIGIT; + } + + j += 2; + int res = x2c(b1, b2); + if (res == '/') { + switch (encodedSolidusHandling) { + case DECODE: { + buff[idx] = (byte) res; + break; + } + case REJECT: { + throw EXCEPTION_SLASH; + } + case PASS_THROUGH: { + buff[idx++] = buff[j - 2]; + buff[idx++] = buff[j - 1]; + buff[idx] = buff[j]; + } + } + } else { + buff[idx] = (byte) res; + } + } + } + + mb.setEnd(idx); + } + + // -------------------- Additional methods -------------------- + + /** + * Decode and return the specified URL-encoded String. It is assumed the string is not a query string. + * + * @param str The url-encoded string + * @param charset The character encoding to use; if null, UTF-8 is used. + * + * @return the decoded string + * + * @exception IllegalArgumentException if a '%' character is not followed by a valid 2-digit hexadecimal number + */ + public static String URLDecode(String str, Charset charset) { + if (str == null) { + return null; + } + + if (str.indexOf('%') == -1) { + // No %nn sequences, so return string unchanged + return str; + } + + if (charset == null) { + charset = StandardCharsets.UTF_8; + } + + /* + * Decoding is required. + * + * Potential complications: + * + * - The source String may be partially decoded so it is not valid to assume that the source String is ASCII. + * + * - Have to process as characters since there is no guarantee that the byte sequence for '%' is going to be the + * same in all character sets. + * + * - We don't know how many '%nn' sequences are required for a single character. It varies between character + * sets and some use a variable length. + */ + + // This isn't perfect but it is a reasonable guess for the size of the + // array required + ByteArrayOutputStream baos = new ByteArrayOutputStream(str.length() * 2); + + OutputStreamWriter osw = new OutputStreamWriter(baos, charset); + + char[] sourceChars = str.toCharArray(); + int len = sourceChars.length; + int ix = 0; + + try { + while (ix < len) { + char c = sourceChars[ix++]; + if (c == '%') { + osw.flush(); + if (ix + 2 > len) { + throw new IllegalArgumentException(sm.getString("uDecoder.urlDecode.missingDigit", str)); + } + char c1 = sourceChars[ix++]; + char c2 = sourceChars[ix++]; + if (isHexDigit(c1) && isHexDigit(c2)) { + baos.write(x2c(c1, c2)); + } else { + throw new IllegalArgumentException(sm.getString("uDecoder.urlDecode.missingDigit", str)); + } + } else { + osw.append(c); + } + } + osw.flush(); + + return baos.toString(charset.name()); + } catch (IOException ioe) { + throw new IllegalArgumentException(sm.getString("uDecoder.urlDecode.conversionError", str, charset.name()), + ioe); + } + } + + + private static boolean isHexDigit(int c) { + return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); + } + + + private static int x2c(byte b1, byte b2) { + int digit = (b1 >= 'A') ? ((b1 & 0xDF) - 'A') + 10 : (b1 - '0'); + digit *= 16; + digit += (b2 >= 'A') ? ((b2 & 0xDF) - 'A') + 10 : (b2 - '0'); + return digit; + } + + + private static int x2c(char b1, char b2) { + int digit = (b1 >= 'A') ? ((b1 & 0xDF) - 'A') + 10 : (b1 - '0'); + digit *= 16; + digit += (b2 >= 'A') ? ((b2 & 0xDF) - 'A') + 10 : (b2 - '0'); + return digit; + } +} diff --git a/java/org/apache/tomcat/util/buf/UEncoder.java b/java/org/apache/tomcat/util/buf/UEncoder.java new file mode 100644 index 0000000..6f2855f --- /dev/null +++ b/java/org/apache/tomcat/util/buf/UEncoder.java @@ -0,0 +1,163 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.BitSet; + +/** + * Efficient implementation of a UTF-8 encoder. This class is not thread safe - you need one encoder per thread. The + * encoder will save and recycle the internal objects, avoiding garbage. You can add extra characters that you want + * preserved, for example while encoding a URL you can add "/". + * + * @author Costin Manolache + */ +public final class UEncoder { + + public enum SafeCharsSet { + WITH_SLASH("/"), + DEFAULT(""); + + private final BitSet safeChars; + + private BitSet getSafeChars() { + return this.safeChars; + } + + SafeCharsSet(String additionalSafeChars) { + safeChars = initialSafeChars(); + for (char c : additionalSafeChars.toCharArray()) { + safeChars.set(c); + } + } + } + + // Not static - the set may differ ( it's better than adding + // an extra check for "/", "+", etc + private BitSet safeChars = null; + private C2BConverter c2b = null; + private ByteChunk bb = null; + private CharChunk cb = null; + private CharChunk output = null; + + /** + * Create a UEncoder with an unmodifiable safe character set. + * + * @param safeCharsSet safe characters for this encoder + */ + public UEncoder(SafeCharsSet safeCharsSet) { + this.safeChars = safeCharsSet.getSafeChars(); + } + + /** + * URL Encode string, using a specified encoding. + * + * @param s string to be encoded + * @param start the beginning index, inclusive + * @param end the ending index, exclusive + * + * @return A new CharChunk contained the URL encoded string + * + * @throws IOException If an I/O error occurs + */ + public CharChunk encodeURL(String s, int start, int end) throws IOException { + if (c2b == null) { + bb = new ByteChunk(8); // small enough. + cb = new CharChunk(2); // small enough. + output = new CharChunk(64); // small enough. + c2b = new C2BConverter(StandardCharsets.UTF_8); + } else { + bb.recycle(); + cb.recycle(); + output.recycle(); + } + + for (int i = start; i < end; i++) { + char c = s.charAt(i); + if (safeChars.get(c)) { + output.append(c); + } else { + cb.append(c); + c2b.convert(cb, bb); + + // "surrogate" - UTF is _not_ 16 bit, but 21 !!!! + // ( while UCS is 31 ). Amazing... + if (c >= 0xD800 && c <= 0xDBFF) { + if ((i + 1) < end) { + char d = s.charAt(i + 1); + if (d >= 0xDC00 && d <= 0xDFFF) { + cb.append(d); + c2b.convert(cb, bb); + i++; + } + } + } + + urlEncode(output, bb); + cb.recycle(); + bb.recycle(); + } + } + + return output; + } + + protected void urlEncode(CharChunk out, ByteChunk bb) throws IOException { + byte[] bytes = bb.getBuffer(); + for (int j = bb.getStart(); j < bb.getEnd(); j++) { + out.append('%'); + char ch = Character.forDigit((bytes[j] >> 4) & 0xF, 16); + out.append(ch); + ch = Character.forDigit(bytes[j] & 0xF, 16); + out.append(ch); + } + } + + // -------------------- Internal implementation -------------------- + + private static BitSet initialSafeChars() { + BitSet initialSafeChars = new BitSet(128); + int i; + for (i = 'a'; i <= 'z'; i++) { + initialSafeChars.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + initialSafeChars.set(i); + } + for (i = '0'; i <= '9'; i++) { + initialSafeChars.set(i); + } + // safe + initialSafeChars.set('$'); + initialSafeChars.set('-'); + initialSafeChars.set('_'); + initialSafeChars.set('.'); + + // Dangerous: someone may treat this as " " + // RFC1738 does allow it, it's not reserved + // initialSafeChars.set('+'); + // extra + initialSafeChars.set('!'); + initialSafeChars.set('*'); + initialSafeChars.set('\''); + initialSafeChars.set('('); + initialSafeChars.set(')'); + initialSafeChars.set(','); + return initialSafeChars; + } +} diff --git a/java/org/apache/tomcat/util/buf/UriUtil.java b/java/org/apache/tomcat/util/buf/UriUtil.java new file mode 100644 index 0000000..8949d9b --- /dev/null +++ b/java/org/apache/tomcat/util/buf/UriUtil.java @@ -0,0 +1,255 @@ +/* + * 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.tomcat.util.buf; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.regex.Pattern; + +/** + * Utility class for working with URIs and URLs. + */ +public final class UriUtil { + + private static final char[] HEX = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + private static final Pattern PATTERN_EXCLAMATION_MARK = Pattern.compile("!/"); + private static final Pattern PATTERN_CARET = Pattern.compile("\\^/"); + private static final Pattern PATTERN_ASTERISK = Pattern.compile("\\*/"); + private static final Pattern PATTERN_CUSTOM; + private static final String REPLACE_CUSTOM; + + private static final String WAR_SEPARATOR; + + static { + String custom = System.getProperty("org.apache.tomcat.util.buf.UriUtil.WAR_SEPARATOR"); + if (custom == null) { + WAR_SEPARATOR = "*/"; + PATTERN_CUSTOM = null; + REPLACE_CUSTOM = null; + } else { + WAR_SEPARATOR = custom + "/"; + PATTERN_CUSTOM = Pattern.compile(Pattern.quote(WAR_SEPARATOR)); + StringBuilder sb = new StringBuilder(custom.length() * 3); + // Deliberately use the platform's default encoding + byte[] ba = custom.getBytes(); + for (byte toEncode : ba) { + // Converting each byte in the buffer + sb.append('%'); + int low = toEncode & 0x0f; + int high = (toEncode & 0xf0) >> 4; + sb.append(HEX[high]); + sb.append(HEX[low]); + } + REPLACE_CUSTOM = sb.toString(); + } + } + + + private UriUtil() { + // Utility class. Hide default constructor + } + + + /** + * Determine if the character is allowed in the scheme of a URI. See RFC 2396, Section 3.1 + * + * @param c The character to test + * + * @return {@code true} if a the character is allowed, otherwise {@code + * false} + */ + private static boolean isSchemeChar(char c) { + return Character.isLetterOrDigit(c) || c == '+' || c == '-' || c == '.'; + } + + + /** + * Determine if a URI string has a scheme component. + * + * @param uri The URI to test + * + * @return {@code true} if a scheme is present, otherwise {code @false} + */ + public static boolean hasScheme(CharSequence uri) { + int len = uri.length(); + for (int i = 0; i < len; i++) { + char c = uri.charAt(i); + if (c == ':') { + return i > 0; + } else if (!isSchemeChar(c)) { + return false; + } + } + return false; + } + + + public static URL buildJarUrl(File jarFile) throws MalformedURLException { + return buildJarUrl(jarFile, null); + } + + + public static URL buildJarUrl(File jarFile, String entryPath) throws MalformedURLException { + return buildJarUrl(jarFile.toURI().toString(), entryPath); + } + + + public static URL buildJarUrl(String fileUrlString) throws MalformedURLException { + return buildJarUrl(fileUrlString, null); + } + + + public static URL buildJarUrl(String fileUrlString, String entryPath) throws MalformedURLException { + String safeString = makeSafeForJarUrl(fileUrlString); + StringBuilder sb = new StringBuilder(); + sb.append(safeString); + sb.append("!/"); + if (entryPath != null) { + sb.append(makeSafeForJarUrl(entryPath)); + } + return new URL("jar", null, -1, sb.toString()); + } + + + public static URL buildJarSafeUrl(File file) throws MalformedURLException { + String safe = makeSafeForJarUrl(file.toURI().toString()); + return new URL(safe); + } + + + /* + * When testing on markt's desktop each iteration was taking ~1420ns when using String.replaceAll(). + * + * Switching the implementation to use pre-compiled patterns and Pattern.matcher(input).replaceAll(replacement) + * reduced this by ~10%. + * + * Note: Given the very small absolute time of a single iteration, even for a web application with 1000 JARs this is + * only going to add ~3ms. It is therefore unlikely that further optimisation will be necessary. + */ + /* + * Pulled out into a separate method in case we need to handle other unusual sequences in the future. + */ + private static String makeSafeForJarUrl(String input) { + // Since "!/" has a special meaning in a JAR URL, make sure that the + // sequence is properly escaped if present. + String tmp = PATTERN_EXCLAMATION_MARK.matcher(input).replaceAll("%21/"); + // Tomcat's custom jar:war: URL handling treats */ and ^/ as special + tmp = PATTERN_CARET.matcher(tmp).replaceAll("%5e/"); + tmp = PATTERN_ASTERISK.matcher(tmp).replaceAll("%2a/"); + if (PATTERN_CUSTOM != null) { + tmp = PATTERN_CUSTOM.matcher(tmp).replaceAll(REPLACE_CUSTOM); + } + return tmp; + } + + + /** + * Convert a URL of the form war:file:... to jar:file:.... + * + * @param warUrl The WAR URL to convert + * + * @return The equivalent JAR URL + * + * @throws MalformedURLException If the conversion fails + */ + public static URL warToJar(URL warUrl) throws MalformedURLException { + // Assumes that the spec is absolute and starts war:file:/... + String file = warUrl.getFile(); + if (file.contains("*/")) { + file = file.replaceFirst("\\*/", "!/"); + } else if (file.contains("^/")) { + file = file.replaceFirst("\\^/", "!/"); + } else if (PATTERN_CUSTOM != null) { + file = file.replaceFirst(PATTERN_CUSTOM.pattern(), "!/"); + } + + return new URL("jar", warUrl.getHost(), warUrl.getPort(), file); + } + + + public static String getWarSeparator() { + return WAR_SEPARATOR; + } + + + /** + * Does the provided path start with file:/ or <protocol>://. + * + * @param path The path to test + * + * @return {@code true} if the supplied path starts with once of the recognised sequences. + */ + public static boolean isAbsoluteURI(String path) { + // Special case as only a single / + if (path.startsWith("file:/")) { + return true; + } + + // Start at the beginning of the path and skip over any valid protocol + // characters + int i = 0; + while (i < path.length() && isSchemeChar(path.charAt(i))) { + i++; + } + // Need at least one protocol character. False positives with Windows + // drives such as C:/... will be caught by the later test for "://" + if (i == 0) { + return false; + } + // path starts with something that might be a protocol. Look for a + // following "://" + if (i + 2 < path.length() && path.charAt(i++) == ':' && path.charAt(i++) == '/' && path.charAt(i) == '/') { + return true; + } + return false; + } + + + /** + * Replicates the behaviour of {@link URI#resolve(String)} and adds support for URIs of the form + * {@code jar:file:/... }. + * + * @param base The base URI to resolve against + * @param target The path to resolve + * + * @return The resulting URI as per {@link URI#resolve(String)} + * + * @throws MalformedURLException If the base URI cannot be converted to a URL + * @throws URISyntaxException If the resulting URL cannot be converted to a URI + */ + public static URI resolve(URI base, String target) throws MalformedURLException, URISyntaxException { + if (base.getScheme().equals("jar")) { + /* + * Previously used: new URL(base.toURL(), target).toURI() This delegated the work to the jar stream handler + * which correctly resolved the target against the base. + * + * Deprecation of all the URL constructors mean a different approach is required. + */ + URI fileUri = new URI(base.getSchemeSpecificPart()); + URI fileUriResolved = fileUri.resolve(target); + + return new URI("jar:" + fileUriResolved.toString()); + } else { + return base.resolve(target); + } + } +} diff --git a/java/org/apache/tomcat/util/buf/Utf8Encoder.java b/java/org/apache/tomcat/util/buf/Utf8Encoder.java new file mode 100644 index 0000000..b6fea53 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/Utf8Encoder.java @@ -0,0 +1,234 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.StandardCharsets; + +/** + * Encodes characters as bytes using UTF-8. Extracted from Apache Harmony with some minor bug fixes applied. + */ +public class Utf8Encoder extends CharsetEncoder { + + public Utf8Encoder() { + super(StandardCharsets.UTF_8, 1.1f, 4.0f); + } + + @Override + protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) { + if (in.hasArray() && out.hasArray()) { + return encodeHasArray(in, out); + } + return encodeNotHasArray(in, out); + } + + private CoderResult encodeHasArray(CharBuffer in, ByteBuffer out) { + int outRemaining = out.remaining(); + int pos = in.position(); + int limit = in.limit(); + byte[] bArr; + char[] cArr; + int x = pos; + bArr = out.array(); + cArr = in.array(); + int outPos = out.position(); + int rem = in.remaining(); + for (x = pos; x < pos + rem; x++) { + int jchar = (cArr[x] & 0xFFFF); + + if (jchar <= 0x7F) { + if (outRemaining < 1) { + in.position(x); + out.position(outPos); + return CoderResult.OVERFLOW; + } + bArr[outPos++] = (byte) (jchar & 0xFF); + outRemaining--; + } else if (jchar <= 0x7FF) { + + if (outRemaining < 2) { + in.position(x); + out.position(outPos); + return CoderResult.OVERFLOW; + } + bArr[outPos++] = (byte) (0xC0 + ((jchar >> 6) & 0x1F)); + bArr[outPos++] = (byte) (0x80 + (jchar & 0x3F)); + outRemaining -= 2; + + } else if (jchar >= 0xD800 && jchar <= 0xDFFF) { + + // in has to have one byte more. + if (limit <= x + 1) { + in.position(x); + out.position(outPos); + return CoderResult.UNDERFLOW; + } + + if (outRemaining < 4) { + in.position(x); + out.position(outPos); + return CoderResult.OVERFLOW; + } + + // The surrogate pair starts with a low-surrogate. + if (jchar >= 0xDC00) { + in.position(x); + out.position(outPos); + return CoderResult.malformedForLength(1); + } + + int jchar2 = cArr[x + 1] & 0xFFFF; + + // The surrogate pair ends with a high-surrogate. + if (jchar2 < 0xDC00) { + in.position(x); + out.position(outPos); + return CoderResult.malformedForLength(1); + } + + // Note, the Unicode scalar value n is defined + // as follows: + // n = (jchar-0xD800)*0x400+(jchar2-0xDC00)+0x10000 + // Where jchar is a high-surrogate, + // jchar2 is a low-surrogate. + int n = (jchar << 10) + jchar2 + 0xFCA02400; + + bArr[outPos++] = (byte) (0xF0 + ((n >> 18) & 0x07)); + bArr[outPos++] = (byte) (0x80 + ((n >> 12) & 0x3F)); + bArr[outPos++] = (byte) (0x80 + ((n >> 6) & 0x3F)); + bArr[outPos++] = (byte) (0x80 + (n & 0x3F)); + outRemaining -= 4; + x++; + + } else { + + if (outRemaining < 3) { + in.position(x); + out.position(outPos); + return CoderResult.OVERFLOW; + } + bArr[outPos++] = (byte) (0xE0 + ((jchar >> 12) & 0x0F)); + bArr[outPos++] = (byte) (0x80 + ((jchar >> 6) & 0x3F)); + bArr[outPos++] = (byte) (0x80 + (jchar & 0x3F)); + outRemaining -= 3; + } + if (outRemaining == 0) { + in.position(x + 1); + out.position(outPos); + // If both input and output are exhausted, return UNDERFLOW + if (x + 1 == limit) { + return CoderResult.UNDERFLOW; + } else { + return CoderResult.OVERFLOW; + } + } + + } + if (rem != 0) { + in.position(x); + out.position(outPos); + } + return CoderResult.UNDERFLOW; + } + + private CoderResult encodeNotHasArray(CharBuffer in, ByteBuffer out) { + int outRemaining = out.remaining(); + int pos = in.position(); + int limit = in.limit(); + try { + while (pos < limit) { + if (outRemaining == 0) { + return CoderResult.OVERFLOW; + } + + int jchar = (in.get() & 0xFFFF); + + if (jchar <= 0x7F) { + + if (outRemaining < 1) { + return CoderResult.OVERFLOW; + } + out.put((byte) jchar); + outRemaining--; + + } else if (jchar <= 0x7FF) { + + if (outRemaining < 2) { + return CoderResult.OVERFLOW; + } + out.put((byte) (0xC0 + ((jchar >> 6) & 0x1F))); + out.put((byte) (0x80 + (jchar & 0x3F))); + outRemaining -= 2; + + } else if (jchar >= 0xD800 && jchar <= 0xDFFF) { + + // in has to have one byte more. + if (limit <= pos + 1) { + return CoderResult.UNDERFLOW; + } + + if (outRemaining < 4) { + return CoderResult.OVERFLOW; + } + + // The surrogate pair starts with a low-surrogate. + if (jchar >= 0xDC00) { + return CoderResult.malformedForLength(1); + } + + int jchar2 = (in.get() & 0xFFFF); + + // The surrogate pair ends with a high-surrogate. + if (jchar2 < 0xDC00) { + return CoderResult.malformedForLength(1); + } + + // Note, the Unicode scalar value n is defined + // as follows: + // n = (jchar-0xD800)*0x400+(jchar2-0xDC00)+0x10000 + // Where jchar is a high-surrogate, + // jchar2 is a low-surrogate. + int n = (jchar << 10) + jchar2 + 0xFCA02400; + + out.put((byte) (0xF0 + ((n >> 18) & 0x07))); + out.put((byte) (0x80 + ((n >> 12) & 0x3F))); + out.put((byte) (0x80 + ((n >> 6) & 0x3F))); + out.put((byte) (0x80 + (n & 0x3F))); + outRemaining -= 4; + pos++; + + } else { + + if (outRemaining < 3) { + return CoderResult.OVERFLOW; + } + out.put((byte) (0xE0 + ((jchar >> 12) & 0x0F))); + out.put((byte) (0x80 + ((jchar >> 6) & 0x3F))); + out.put((byte) (0x80 + (jchar & 0x3F))); + outRemaining -= 3; + } + pos++; + } + } finally { + in.position(pos); + } + return CoderResult.UNDERFLOW; + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/buf/package.html b/java/org/apache/tomcat/util/buf/package.html new file mode 100644 index 0000000..e61b9b3 --- /dev/null +++ b/java/org/apache/tomcat/util/buf/package.html @@ -0,0 +1,37 @@ + + +

    Buffers and Encodings

    + +This package contains buffers and utils to perform encoding/decoding of buffers. That includes byte to char +conversions, URL encodings, etc. + +

    +Encoding is a critical operation for performance. There are few tricks in this package - the C2B and +B2C converters are caching an ISReader/OSWriter and keep everything allocated to do the conversions +in any VM without any garbage. + +

    +This package must accommodate future extensions and additional converters ( most important: the nio.charset, +which should be detected and used if available ). Also, we do have one hand-written UTF8Decoder, and +other tuned encoders could be added. + +

    +My benchmarks ( I'm costin :-) show only small differences between C2B, B2C and hand-written codders/decoders, +so UTF8Decoder may be disabled. + + diff --git a/java/org/apache/tomcat/util/codec/binary/Base64.java b/java/org/apache/tomcat/util/codec/binary/Base64.java new file mode 100644 index 0000000..e38bf3d --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/Base64.java @@ -0,0 +1,661 @@ +/* + * 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.tomcat.util.codec.binary; + +/** + * Provides Base64 encoding and decoding as defined by RFC 2045. + * + *

    + * This class implements section 6.8. Base64 Content-Transfer-Encoding from RFC 2045 Multipurpose + * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies by Freed and Borenstein. + *

    + *

    + * The class can be parameterized in the following manner with various constructors: + *

    + *
      + *
    • URL-safe mode: Default off.
    • + *
    • Line length: Default 76. Line length that aren't multiples of 4 will still essentially end up being multiples of + * 4 in the encoded data. + *
    • Line separator: Default is CRLF ("\r\n")
    • + *
    + *

    + * The URL-safe parameter is only applied to encode operations. Decoding seamlessly handles both modes. + *

    + *

    + * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only + * encode/decode character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, + * UTF-8, etc). + *

    + *

    + * This class is thread-safe. + *

    + * + * @see RFC 2045 + * @since 1.0 + */ +public class Base64 extends BaseNCodec { + + /** + * BASE64 characters are 6 bits in length. + * They are formed by taking a block of 3 octets to form a 24-bit string, + * which is converted into 4 BASE64 characters. + */ + private static final int BITS_PER_ENCODED_BYTE = 6; + private static final int BYTES_PER_UNENCODED_BLOCK = 3; + private static final int BYTES_PER_ENCODED_BLOCK = 4; + + /** + * This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet" + * equivalents as specified in Table 1 of RFC 2045. + *

    + * Thanks to "commons" project in ws.apache.org for this code. + * https://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

    + */ + private static final byte[] STANDARD_ENCODE_TABLE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + /** + * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and / + * changed to - and _ to make the encoded Base64 results more URL-SAFE. + * This table is only used when the Base64's mode is set to URL-SAFE. + */ + private static final byte[] URL_SAFE_ENCODE_TABLE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' + }; + + /** + * This array is a lookup table that translates Unicode characters drawn from the "Base64 Alphabet" (as specified + * in Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64 + * alphabet but fall within the bounds of the array are translated to -1. + *

    + * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This means decoder seamlessly handles both + * URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead of time what to emit). + *

    + *

    + * Thanks to "commons" project in ws.apache.org for this code. + * https://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

    + */ + private static final byte[] STANDARD_DECODE_TABLE = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, // 20-2f + / + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 30-3f 0-9 + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-O + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, // 50-5f P-Z + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 // 70-7a p-z + }; + + private static final byte[] URL_SAFE_DECODE_TABLE = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, // 20-2f - + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 30-3f 0-9 + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-O + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, // 50-5f P-Z _ + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 // 70-7a p-z + }; + + /* + * Base64 uses 6-bit fields. + */ + /** Mask used to extract 6 bits, used when encoding */ + private static final int MASK_6BITS = 0x3f; + /** Mask used to extract 4 bits, used when decoding final trailing character. */ + private static final int MASK_4BITS = 0xf; + /** Mask used to extract 2 bits, used when decoding final trailing character. */ + private static final int MASK_2BITS = 0x3; + + // The static final fields above are used for the original static byte[] methods on Base64. + // The private member fields below are used with the new streaming approach, which requires + // some state be preserved between calls of encode() and decode(). + + public static byte[] decodeBase64( + final byte[] base64Data, final int off, final int len) { + return new Base64().decode(base64Data, off, len); + } + + /** + * Decodes a Base64 String into octets. + *

    + * Note: this method seamlessly handles data encoded in URL-safe or normal mode. + *

    + * + * @param base64String + * String containing Base64 data + * @return Array containing decoded data. + * @since 1.4 + */ + public static byte[] decodeBase64(final String base64String) { + return new Base64().decode(base64String); + } + + public static byte[] decodeBase64URLSafe(final String base64String) { + return new Base64(true).decode(base64String); + } + + // Implementation of integer encoding used for crypto + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if {@code true} this encoder will chunk the base64 output into 76 character blocks + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked) { + return encodeBase64(binaryData, isChunked, false); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if {@code true} this encoder will chunk the base64 output into 76 character blocks + * @param urlSafe + * if {@code true} this encoder will emit - and _ instead of the usual + and / characters. + * Note: no padding is added when encoding using the URL-safe alphabet. + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + * @since 1.4 + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe) { + return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if {@code true} this encoder will chunk the base64 output into 76 character blocks + * @param urlSafe + * if {@code true} this encoder will emit - and _ instead of the usual + and / characters. + * Note: no padding is added when encoding using the URL-safe alphabet. + * @param maxResultSize + * The maximum result size to accept. + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than maxResultSize + * @since 1.4 + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, + final boolean urlSafe, final int maxResultSize) { + if (binaryData == null || binaryData.length == 0) { + return binaryData; + } + + // Create this so can use the super-class method + // Also ensures that the same roundings are performed by the ctor and the code + final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe); + final long len = b64.getEncodedLength(binaryData); + if (len > maxResultSize) { + throw new IllegalArgumentException(sm.getString( + "base64.inputTooLarge", Long.valueOf(len), Integer.valueOf(maxResultSize))); + } + + return b64.encode(binaryData); + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the output. + * + * NOTE: We changed the behavior of this method from multi-line chunking (commons-codec-1.4) to + * single-line non-chunking (commons-codec-1.5). + * + * @param binaryData + * binary data to encode + * @return String containing Base64 characters. + * @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not). + */ + public static String encodeBase64String(final byte[] binaryData) { + return StringUtils.newStringUsAscii(encodeBase64(binaryData, false)); + } + + /** + * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The + * url-safe variation emits - and _ instead of + and / characters. + * Note: no padding is added. + * @param binaryData + * binary data to encode + * @return String containing Base64 characters + * @since 1.4 + */ + public static String encodeBase64URLSafeString(final byte[] binaryData) { + return StringUtils.newStringUsAscii(encodeBase64(binaryData, false, true)); + } + + /** + * Validates whether decoding the final trailing character is possible in the context + * of the set of possible base 64 values. + *

    + * The character is valid if the lower bits within the provided mask are zero. This + * is used to test the final trailing base-64 digit is zero in the bits that will be discarded. + *

    + * + * @param emptyBitsMask The mask of the lower bits that should be empty + * @param context the context to be used + * + * @throws IllegalArgumentException if the bits being checked contain any non-zero value + */ + private static void validateCharacter(final int emptyBitsMask, final Context context) { + if ((context.ibitWorkArea & emptyBitsMask) != 0) { + throw new IllegalArgumentException( + "Last encoded character (before the paddings if any) is a valid " + + "base 64 alphabet but not a possible value. " + + "Expected the discarded bits to be zero."); + } + } + + + public static boolean isInAlphabet(char c) { + // Fast for valid data. May be slow for invalid data. + try { + return STANDARD_DECODE_TABLE[c] != -1; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + /** + * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able + * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch + * between the two modes. + */ + private final byte[] encodeTable; + + /** Only one decode table currently; keep for consistency with Base32 code. */ + private final byte[] decodeTable; + + /** + * Line separator for encoding. Not used when decoding. Only used if lineLength > 0. + */ + private final byte[] lineSeparator; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * {@code decodeSize = 3 + lineSeparator.length;} + */ + private final int decodeSize; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * {@code encodeSize = 4 + lineSeparator.length;} + */ + private final int encodeSize; + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

    + * When encoding the line length is 0 (no chunking), and the encoding table is STANDARD_ENCODE_TABLE. + *

    + * + *

    + * When decoding all variants are supported. + *

    + */ + public Base64() { + this(0); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in the given URL-safe mode. + *

    + * When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE. + *

    + * + *

    + * When decoding all variants are supported. + *

    + * + * @param urlSafe + * if {@code true}, URL-safe encoding is used. In most cases this should be set to + * {@code false}. + * @since 1.4 + */ + public Base64(final boolean urlSafe) { + this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

    + * When encoding the line length is given in the constructor, the line separator is CRLF, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

    + *

    + * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

    + *

    + * When decoding all variants are supported. + *

    + * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to the nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @since 1.4 + */ + public Base64(final int lineLength) { + this(lineLength, CHUNK_SEPARATOR); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

    + * When encoding the line length and line separator are given in the constructor, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

    + *

    + * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

    + *

    + * When decoding all variants are supported. + *

    + * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to the nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @throws IllegalArgumentException + * Thrown when the provided lineSeparator included some base64 characters. + * @since 1.4 + */ + public Base64(final int lineLength, final byte[] lineSeparator) { + this(lineLength, lineSeparator, false); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

    + * When encoding the line length and line separator are given in the constructor, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

    + *

    + * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

    + *

    + * When decoding all variants are supported. + *

    + * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to the nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @param urlSafe + * Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode + * operations. Decoding seamlessly handles both modes. + * Note: no padding is added when using the URL-safe alphabet. + * @throws IllegalArgumentException + * Thrown when the {@code lineSeparator} contains Base64 characters. + * @since 1.4 + */ + public Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) { + super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, + lineLength, + lineSeparator == null ? 0 : lineSeparator.length); + // Needs to be set early to avoid NPE during call to containsAlphabetOrPad() below + this.decodeTable = urlSafe ? URL_SAFE_DECODE_TABLE : STANDARD_DECODE_TABLE; + // TODO could be simplified if there is no requirement to reject invalid line sep when length <=0 + // @see test case Base64Test.testConstructors() + if (lineSeparator != null) { + if (containsAlphabetOrPad(lineSeparator)) { + final String sep = StringUtils.newStringUtf8(lineSeparator); + throw new IllegalArgumentException(sm.getString("base64.lineSeparator", sep)); + } + if (lineLength > 0){ // null line-sep forces no chunking rather than throwing IAE + this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length; + this.lineSeparator = lineSeparator.clone(); + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + this.decodeSize = this.encodeSize - 1; + this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE; + } + + // Implementation of the Encoder Interface + + /** + *

    + * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once + * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1" + * call is not necessary when decoding, but it doesn't hurt, either. + *

    + *

    + * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are + * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in, + * garbage-out philosophy: it will not check the provided data for validity. + *

    + *

    + * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. + * https://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

    + * + * @param input + * byte[] array of ASCII data to base64 decode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for decoding. + * @param context + * the context to be used + */ + @Override + void decode(final byte[] input, int inPos, final int inAvail, final Context context) { + if (context.eof) { + return; + } + if (inAvail < 0) { + context.eof = true; + } + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(decodeSize, context); + final byte b = input[inPos++]; + if (b == pad) { + // We're done. + context.eof = true; + break; + } + if (b >= 0 && b < decodeTable.length) { + final int result = decodeTable[b]; + if (result >= 0) { + context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK; + context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result; + if (context.modulus == 0) { + buffer[context.pos++] = (byte) (context.ibitWorkArea >> 16 & MASK_8BITS); + buffer[context.pos++] = (byte) (context.ibitWorkArea >> 8 & MASK_8BITS); + buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS); + } + } + } + } + + // Two forms of EOF as far as base64 decoder is concerned: actual + // EOF (-1) and first time '=' character is encountered in stream. + // This approach makes the '=' padding characters completely optional. + if (context.eof && context.modulus != 0) { + final byte[] buffer = ensureBufferSize(decodeSize, context); + + // We have some spare bits remaining + // Output all whole multiples of 8 bits and ignore the rest + switch (context.modulus) { +// case 0 : // impossible, as excluded above +// case 1 : // 6 bits - invalid - use default below + case 2 : // 12 bits = 8 + 4 + validateCharacter(MASK_4BITS, context); + context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits + buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS); + break; + case 3 : // 18 bits = 8 + 8 + 2 + validateCharacter(MASK_2BITS, context); + context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits + buffer[context.pos++] = (byte) (context.ibitWorkArea >> 8 & MASK_8BITS); + buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS); + break; + default: + throw new IllegalStateException(sm.getString( + "base64.impossibleModulus", Integer.valueOf(context.modulus))); + } + } + } + + /** + *

    + * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with + * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, to flush last + * remaining bytes (if not multiple of 3). + *

    + *

    Note: no padding is added when encoding using the URL-safe alphabet.

    + *

    + * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. + * https://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

    + * + * @param in + * byte[] array of binary data to base64 encode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + * @param context + * the context to be used + */ + @Override + void encode(final byte[] in, int inPos, final int inAvail, final Context context) { + if (context.eof) { + return; + } + // inAvail < 0 is how we're informed of EOF in the underlying data we're + // encoding. + if (inAvail < 0) { + context.eof = true; + if (0 == context.modulus && lineLength == 0) { + return; // no leftovers to process and not using chunking + } + final byte[] buffer = ensureBufferSize(encodeSize, context); + final int savedPos = context.pos; + switch (context.modulus) { // 0-2 + case 0 : // nothing to do here + break; + case 1 : // 8 bits = 6 + 2 + // top 6 bits: + buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 2 & MASK_6BITS]; + // remaining 2: + buffer[context.pos++] = encodeTable[context.ibitWorkArea << 4 & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + } + break; + + case 2 : // 16 bits = 6 + 6 + 4 + buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 10 & MASK_6BITS]; + buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 4 & MASK_6BITS]; + buffer[context.pos++] = encodeTable[context.ibitWorkArea << 2 & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[context.pos++] = pad; + } + break; + default: + throw new IllegalStateException(sm.getString( + "base64.impossibleModulus", Integer.valueOf(context.modulus))); + } + context.currentLinePos += context.pos - savedPos; // keep track of current line position + // if currentPos == 0 we are at the start of a line, so don't add CRLF + if (lineLength > 0 && context.currentLinePos > 0) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + } + } else { + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(encodeSize, context); + context.modulus = (context.modulus+1) % BYTES_PER_UNENCODED_BLOCK; + int b = in[inPos++]; + if (b < 0) { + b += 256; + } + context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE + if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract + buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 18 & MASK_6BITS]; + buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 12 & MASK_6BITS]; + buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 6 & MASK_6BITS]; + buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS]; + context.currentLinePos += BYTES_PER_ENCODED_BLOCK; + if (lineLength > 0 && lineLength <= context.currentLinePos) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + context.currentLinePos = 0; + } + } + } + } + } + + /** + * Returns whether or not the {@code octet} is in the Base64 alphabet. + * + * @param octet + * The value to test + * @return {@code true} if the value is defined in the Base64 alphabet {@code false} otherwise. + */ + @Override + protected boolean isInAlphabet(final byte octet) { + return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1; + } + + /** + * Returns our current encode mode. True if we're URL-SAFE, false otherwise. + * + * @return true if we're in URL-SAFE mode, false otherwise. + * @since 1.4 + */ + public boolean isUrlSafe() { + return this.encodeTable == URL_SAFE_ENCODE_TABLE; + } +} diff --git a/java/org/apache/tomcat/util/codec/binary/BaseNCodec.java b/java/org/apache/tomcat/util/codec/binary/BaseNCodec.java new file mode 100644 index 0000000..7492773 --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/BaseNCodec.java @@ -0,0 +1,478 @@ +/* + * 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.tomcat.util.codec.binary; + +import java.util.Arrays; + +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Abstract superclass for Base-N encoders and decoders. + * + *

    + * This class is thread-safe. + *

    + */ +public abstract class BaseNCodec { + + protected static final StringManager sm = StringManager.getManager(BaseNCodec.class); + + /** + * Holds thread context so classes can be thread-safe. + * + * This class is not itself thread-safe; each thread must allocate its own copy. + * + * @since 1.7 + */ + static class Context { + + /** + * Placeholder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this variable. + */ + int ibitWorkArea; + + /** + * Buffer for streaming. + */ + byte[] buffer; + + /** + * Position where next character should be written in the buffer. + */ + int pos; + + /** + * Position where next character should be read from the buffer. + */ + int readPos; + + /** + * Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this object becomes useless, + * and must be thrown away. + */ + boolean eof; + + /** + * Variable tracks how many characters have been written to the current line. Only used when encoding. We use + * it to make sure each encoded line never goes beyond lineLength (if lineLength > 0). + */ + int currentLinePos; + + /** + * Writes to the buffer only occur after every 3/5 reads when encoding, and every 4/8 reads when decoding. This + * variable helps track that. + */ + int modulus; + + /** + * Returns a String useful for debugging (especially within a debugger.) + * + * @return a String useful for debugging. + */ + @SuppressWarnings("boxing") // OK to ignore boxing here + @Override + public String toString() { + return String.format("%s[buffer=%s, currentLinePos=%s, eof=%s, ibitWorkArea=%s, " + + "modulus=%s, pos=%s, readPos=%s]", this.getClass().getSimpleName(), HexUtils.toHexString(buffer), + currentLinePos, eof, ibitWorkArea, modulus, pos, readPos); + } + } + + /** + * EOF + * + * @since 1.7 + */ + static final int EOF = -1; + + /** + * MIME chunk size per RFC 2045 section 6.8. + * + *

    + * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any + * equal signs. + *

    + * + * @see RFC 2045 section 6.8 + */ + public static final int MIME_CHUNK_SIZE = 76; + + private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2; + + /** + * Defines the default buffer size - currently {@value} + * - must be large enough for at least one encoded block+separator + */ + private static final int DEFAULT_BUFFER_SIZE = 128; + + /** + * The maximum size buffer to allocate. + * + *

    This is set to the same size used in the JDK {@link java.util.ArrayList}:

    + *
    + * Some VMs reserve some header words in an array. + * Attempts to allocate larger arrays may result in + * OutOfMemoryError: Requested array size exceeds VM limit. + *
    + */ + private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8; + + /** Mask used to extract 8 bits, used in decoding bytes */ + protected static final int MASK_8BITS = 0xff; + + /** + * Byte used to pad output. + */ + protected static final byte PAD_DEFAULT = '='; // Allow static access to default + + /** + * Chunk separator per RFC 2045 section 2.1. + * + * @see RFC 2045 section 2.1 + */ + static final byte[] CHUNK_SEPARATOR = {'\r', '\n'}; + + /** + * Create a positive capacity at least as large the minimum required capacity. + * If the minimum capacity is negative then this throws an OutOfMemoryError as no array + * can be allocated. + * + * @param minCapacity the minimum capacity + * @return the capacity + * @throws OutOfMemoryError if the {@code minCapacity} is negative + */ + private static int createPositiveCapacity(final int minCapacity) { + if (minCapacity < 0) { + // overflow + throw new OutOfMemoryError(sm.getString("base64.outOfMemory", Long.valueOf(minCapacity & 0xffffffffL))); + } + // This is called when we require buffer expansion to a very big array. + // Use the conservative maximum buffer size if possible, otherwise the biggest required. + // + // Note: In this situation JDK 1.8 java.util.ArrayList returns Integer.MAX_VALUE. + // This excludes some VMs that can exceed MAX_BUFFER_SIZE but not allocate a full + // Integer.MAX_VALUE length array. + // The result is that we may have to allocate an array of this size more than once if + // the capacity must be expanded again. + return Math.max(minCapacity, MAX_BUFFER_SIZE); + } + + /** + * Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}. + * @param context the context to be used + * @param minCapacity the minimum required capacity + * @return the resized byte[] buffer + * @throws OutOfMemoryError if the {@code minCapacity} is negative + */ + private static byte[] resizeBuffer(final Context context, final int minCapacity) { + // Overflow-conscious code treats the min and new capacity as unsigned. + final int oldCapacity = context.buffer.length; + int newCapacity = oldCapacity * DEFAULT_BUFFER_RESIZE_FACTOR; + if (Integer.compareUnsigned(newCapacity, minCapacity) < 0) { + newCapacity = minCapacity; + } + if (Integer.compareUnsigned(newCapacity, MAX_BUFFER_SIZE) > 0) { + newCapacity = createPositiveCapacity(minCapacity); + } + + final byte[] b = Arrays.copyOf(context.buffer, newCapacity); + context.buffer = b; + return b; + } + + /** Pad byte. Instance variable just in case it needs to vary later. */ + protected final byte pad; + + /** Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5 for Base32 */ + private final int unencodedBlockSize; + + /** Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8 for Base32 */ + private final int encodedBlockSize; + + /** + * Chunksize for encoding. Not used when decoding. + * A value of zero or less implies no chunking of the encoded data. + * Rounded down to the nearest multiple of encodedBlockSize. + */ + protected final int lineLength; + + /** + * Size of chunk separator. Not used unless {@link #lineLength} > 0. + */ + private final int chunkSeparatorLength; + + /** + * Note {@code lineLength} is rounded down to the nearest multiple of the encoded block size. + * If {@code chunkSeparatorLength} is zero, then chunking is disabled. + * + * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3) + * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4) + * @param lineLength if > 0, use chunking with a length {@code lineLength} + * @param chunkSeparatorLength the chunk separator length, if relevant + */ + protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, + final int lineLength, final int chunkSeparatorLength) { + this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, PAD_DEFAULT); + } + + /** + * Note {@code lineLength} is rounded down to the nearest multiple of the encoded block size. + * If {@code chunkSeparatorLength} is zero, then chunking is disabled. + * + * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3) + * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4) + * @param lineLength if > 0, use chunking with a length {@code lineLength} + * @param chunkSeparatorLength the chunk separator length, if relevant + * @param pad byte used as padding byte. + */ + protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, + final int lineLength, final int chunkSeparatorLength, final byte pad) { + this.unencodedBlockSize = unencodedBlockSize; + this.encodedBlockSize = encodedBlockSize; + final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0; + this.lineLength = useChunking ? lineLength / encodedBlockSize * encodedBlockSize : 0; + this.chunkSeparatorLength = chunkSeparatorLength; + this.pad = pad; + } + + /** + * Returns the amount of buffered data available for reading. + * + * @param context the context to be used + * @return The amount of buffered data available for reading. + */ + int available(final Context context) { // package protected for access from I/O streams + return hasData(context) ? context.pos - context.readPos : 0; + } + + /** + * Tests a given byte array to see if it contains any characters within the alphabet or PAD. + * + * Intended for use in checking line-ending arrays + * + * @param arrayOctet + * byte array to test + * @return {@code true} if any byte is a valid character in the alphabet or PAD; {@code false} otherwise + */ + protected boolean containsAlphabetOrPad(final byte[] arrayOctet) { + if (arrayOctet == null) { + return false; + } + for (final byte element : arrayOctet) { + if (pad == element || isInAlphabet(element)) { + return true; + } + } + return false; + } + + /** + * Decodes a byte[] containing characters in the Base-N alphabet. + * + * @param pArray + * A byte array containing Base-N character data + * @return a byte array containing binary data + */ + public byte[] decode(final byte[] pArray) { + return decode(pArray, 0, pArray.length); + } + + public byte[] decode(final byte[] pArray, final int off, final int len) { + if (pArray == null || len == 0) { + return new byte[0]; + } + final Context context = new Context(); + decode(pArray, off, len, context); + decode(pArray, off, EOF, context); // Notify decoder of EOF. + final byte[] result = new byte[context.pos]; + readResults(result, 0, result.length, context); + return result; + } + + // package protected for access from I/O streams + abstract void decode(byte[] pArray, int i, int length, Context context); + + /** + * Decodes a String containing characters in the Base-N alphabet. + * + * @param pArray + * A String containing Base-N character data + * @return a byte array containing binary data + */ + public byte[] decode(final String pArray) { + return decode(StringUtils.getBytesUtf8(pArray)); + } + + /** + * Encodes a byte[] containing binary data, into a byte[] containing characters in the alphabet. + * + * @param pArray + * a byte array containing binary data + * @return A byte array containing only the base N alphabetic character data + */ + public byte[] encode(final byte[] pArray) { + if (pArray == null || pArray.length == 0) { + return pArray; + } + return encode(pArray, 0, pArray.length); + } + + /** + * Encodes a byte[] containing binary data, into a byte[] containing + * characters in the alphabet. + * + * @param pArray + * a byte array containing binary data + * @param offset + * initial offset of the subarray. + * @param length + * length of the subarray. + * @return A byte array containing only the base N alphabetic character data + * @since 1.11 + */ + public byte[] encode(final byte[] pArray, final int offset, final int length) { + if (pArray == null || pArray.length == 0) { + return pArray; + } + final Context context = new Context(); + encode(pArray, offset, length, context); + encode(pArray, offset, EOF, context); // Notify encoder of EOF. + final byte[] buf = new byte[context.pos - context.readPos]; + readResults(buf, 0, buf.length, context); + return buf; + } + + // package protected for access from I/O streams + abstract void encode(byte[] pArray, int i, int length, Context context); + + /** + * Encodes a byte[] containing binary data, into a String containing characters in the appropriate alphabet. + * Uses UTF8 encoding. + * + * @param pArray a byte array containing binary data + * @return String containing only character data in the appropriate alphabet. + * @since 1.5 + */ + public String encodeAsString(final byte[] pArray){ + return StringUtils.newStringUtf8(encode(pArray)); + } + + /** + * Ensure that the buffer has room for {@code size} bytes + * + * @param size minimum spare space required + * @param context the context to be used + * @return the buffer + */ + protected byte[] ensureBufferSize(final int size, final Context context){ + if (context.buffer == null) { + context.buffer = new byte[Math.max(size, getDefaultBufferSize())]; + context.pos = 0; + context.readPos = 0; + + // Overflow-conscious: + // x + y > z == x + y - z > 0 + } else if (context.pos + size - context.buffer.length > 0) { + return resizeBuffer(context, context.pos + size); + } + return context.buffer; + } + + /** + * Gets the default buffer size. Can be overridden. + * + * @return the default buffer size. + */ + protected int getDefaultBufferSize() { + return DEFAULT_BUFFER_SIZE; + } + + /** + * Calculates the amount of space needed to encode the supplied array. + * + * @param pArray byte[] array which will later be encoded + * + * @return amount of space needed to encode the supplied array. + * Returns a long since a max-len array will require > Integer.MAX_VALUE + */ + public long getEncodedLength(final byte[] pArray) { + // Calculate non-chunked size - rounded up to allow for padding + // cast to long is needed to avoid possibility of overflow + long len = (pArray.length + unencodedBlockSize-1) / unencodedBlockSize * (long) encodedBlockSize; + if (lineLength > 0) { // We're using chunking + // Round up to nearest multiple + len += (len + lineLength-1) / lineLength * chunkSeparatorLength; + } + return len; + } + + /** + * Returns true if this object has buffered data for reading. + * + * @param context the context to be used + * @return true if there is data still available for reading. + */ + boolean hasData(final Context context) { // package protected for access from I/O streams + return context.pos > context.readPos; + } + + /** + * Returns whether or not the {@code octet} is in the current alphabet. + * Does not allow whitespace or pad. + * + * @param value The value to test + * + * @return {@code true} if the value is defined in the current alphabet, {@code false} otherwise. + */ + protected abstract boolean isInAlphabet(byte value); + + /** + * Extracts buffered data into the provided byte[] array, starting at position bPos, up to a maximum of bAvail + * bytes. Returns how many bytes were actually extracted. + *

    + * Package private for access from I/O streams. + *

    + * + * @param b + * byte[] array to extract the buffered data into. + * @param bPos + * position in byte[] array to start extraction at. + * @param bAvail + * amount of bytes we're allowed to extract. We may extract fewer (if fewer are available). + * @param context + * the context to be used + * @return The number of bytes successfully extracted into the provided byte[] array. + */ + int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) { + if (hasData(context)) { + final int len = Math.min(available(context), bAvail); + System.arraycopy(context.buffer, context.readPos, b, bPos, len); + context.readPos += len; + if (!hasData(context)) { + // All data read. + // Reset position markers but do not set buffer to null to allow its reuse. + // hasData(context) will still return false, and this method will return 0 until + // more data is available, or -1 if EOF. + context.pos = context.readPos = 0; + } + return len; + } + return context.eof ? EOF : 0; + } +} diff --git a/java/org/apache/tomcat/util/codec/binary/LocalStrings.properties b/java/org/apache/tomcat/util/codec/binary/LocalStrings.properties new file mode 100644 index 0000000..794f26c --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/LocalStrings.properties @@ -0,0 +1,20 @@ +# 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. + +base64.impossibleModulus=Impossible modulus [{0}] +base64.inputTooLarge=Input array too large, the output array would be bigger [{0}] than the specified maximum size of [{1}] +base64.lineSeparator=Line separator must not contain base64 characters [{0}] +base64.nullEncodeParameter=Cannot encode integer with null parameter +base64.outOfMemory=Unable to allocate array size [{0}] diff --git a/java/org/apache/tomcat/util/codec/binary/LocalStrings_fr.properties b/java/org/apache/tomcat/util/codec/binary/LocalStrings_fr.properties new file mode 100644 index 0000000..5ae85e1 --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/LocalStrings_fr.properties @@ -0,0 +1,20 @@ +# 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. + +base64.impossibleModulus=Modulo [{0}] invalide +base64.inputTooLarge=Le tableau en entrée est trop grand, la taille du tableau de sortie [{0}] serait plus grande que la taille maximale autorisée [{1}] +base64.lineSeparator=Le séparateur de ligne ne doit pas contenir des caractères base64 [{0}] +base64.nullEncodeParameter=Impossible d'encoder unentier en utilisant un paramètre null +base64.outOfMemory=Impossible d''allouer un tableau de taille [{0}] diff --git a/java/org/apache/tomcat/util/codec/binary/LocalStrings_ja.properties b/java/org/apache/tomcat/util/codec/binary/LocalStrings_ja.properties new file mode 100644 index 0000000..342dcfc --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/LocalStrings_ja.properties @@ -0,0 +1,20 @@ +# 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. + +base64.impossibleModulus=計算ã§ããªã„剰余 [{0}] ã§ã™ã€‚ +base64.inputTooLarge=入力é…列ãŒå¤§ãã™ãŽã¾ã™ã€‚出力é…列㯠[{1}] ã®æŒ‡å®šã•ã‚ŒãŸæœ€å¤§ã‚µã‚¤ã‚ºã‚ˆã‚Šã‚‚大ãããªã‚Šã¾ã™ [{0}] +base64.lineSeparator=行区切り記å·ã«ã¯base64文字を使用ã§ãã¾ã›ã‚“[{0}] +base64.nullEncodeParameter=null 値を整数ã«ç¬¦å·åŒ–ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +base64.outOfMemory=é…列サイズ [{0}] を割り当ã¦ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ diff --git a/java/org/apache/tomcat/util/codec/binary/LocalStrings_ko.properties b/java/org/apache/tomcat/util/codec/binary/LocalStrings_ko.properties new file mode 100644 index 0000000..7bdc2bb --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/LocalStrings_ko.properties @@ -0,0 +1,19 @@ +# 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. + +base64.impossibleModulus=불가능한 계수 [{0}] +base64.inputTooLarge=ìž…ë ¥ ë°°ì—´ì´ ë„ˆë¬´ í½ë‹ˆë‹¤. 출력 ë°°ì—´ì˜ í¬ê¸° [{0}]ì´(ê°€), ì§€ì •ëœ ìµœëŒ€ í¬ê¸° [{1}] 보다 í° ê°’ìž…ë‹ˆë‹¤. +base64.lineSeparator=í–‰ êµ¬ë¶„ë¬¸ìž [{0}]ì€(는) Base64 문ìžë“¤ì„ í¬í•¨í•´ì„œëŠ” 안ë©ë‹ˆë‹¤. +base64.nullEncodeParameter=정수를 위한 파ë¼ë¯¸í„°ê°€ ë„ì´ë¼ì„œ ì¸ì½”딩할 수 없습니다. diff --git a/java/org/apache/tomcat/util/codec/binary/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/codec/binary/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..0624dec --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/LocalStrings_zh_CN.properties @@ -0,0 +1,19 @@ +# 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. + +base64.impossibleModulus=ä¸å¯èƒ½çš„模[{0}] +base64.inputTooLarge=输入数组太大,输出数组将比指定的最大大å°[{1}]大[{0}] +base64.lineSeparator=行分隔符ä¸èƒ½åŒ…å«base64个字符[{0}] +base64.nullEncodeParameter=ä¸èƒ½ç”¨ç©ºå‚æ•°ç¼–ç æ•´æ•° diff --git a/java/org/apache/tomcat/util/codec/binary/StringUtils.java b/java/org/apache/tomcat/util/codec/binary/StringUtils.java new file mode 100644 index 0000000..ab050f3 --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/StringUtils.java @@ -0,0 +1,116 @@ +/* + * 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.tomcat.util.codec.binary; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Converts String to and from bytes using the encodings required by the Java specification. These encodings are + * specified in + * Standard charsets. + * + *

    This class is immutable and thread-safe.

    + * + * @see Charset + * @see StandardCharsets + * @since 1.4 + */ +public class StringUtils { + + /** + * Calls {@link String#getBytes(Charset)} + * + * @param string + * The string to encode (if null, return null). + * @param charset + * The {@link Charset} to encode the {@code String} + * @return the encoded bytes + */ + private static byte[] getBytes(final String string, final Charset charset) { + return string == null ? null : string.getBytes(charset); + } + + /** + * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be {@code null} + * @return encoded bytes, or {@code null} if the input string was {@code null} + * @throws NullPointerException + * Thrown if {@link StandardCharsets#UTF_8} is not initialized, which should never happen + * since it is required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + * @see Charset + */ + public static byte[] getBytesUtf8(final String string) { + return getBytes(string, StandardCharsets.UTF_8); + } + + /** + * Constructs a new {@code String} by decoding the specified array of bytes using the given charset. + * + * @param bytes + * The bytes to be decoded into characters + * @param charset + * The {@link Charset} to encode the {@code String}; not {@code null} + * @return A new {@code String} decoded from the specified array of bytes using the given charset, + * or {@code null} if the input byte array was {@code null}. + * @throws NullPointerException + * Thrown if charset is {@code null} + */ + private static String newString(final byte[] bytes, final Charset charset) { + return bytes == null ? null : new String(bytes, charset); + } + + /** + * Constructs a new {@code String} by decoding the specified array of bytes using the US-ASCII charset. + * + * @param bytes + * The bytes to be decoded into characters + * @return A new {@code String} decoded from the specified array of bytes using the US-ASCII charset, + * or {@code null} if the input byte array was {@code null}. + * @throws NullPointerException + * Thrown if {@link StandardCharsets#US_ASCII} is not initialized, which should never happen + * since it is required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + */ + public static String newStringUsAscii(final byte[] bytes) { + return newString(bytes, StandardCharsets.US_ASCII); + } + + /** + * Constructs a new {@code String} by decoding the specified array of bytes using the UTF-8 charset. + * + * @param bytes + * The bytes to be decoded into characters + * @return A new {@code String} decoded from the specified array of bytes using the UTF-8 charset, + * or {@code null} if the input byte array was {@code null}. + * @throws NullPointerException + * Thrown if {@link StandardCharsets#UTF_8} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + */ + public static String newStringUtf8(final byte[] bytes) { + return newString(bytes, StandardCharsets.UTF_8); + } + + private StringUtils() { + // empty + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/codec/binary/package-info.java b/java/org/apache/tomcat/util/codec/binary/package-info.java new file mode 100644 index 0000000..605aede --- /dev/null +++ b/java/org/apache/tomcat/util/codec/binary/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Base64 String encoding and decoding. + */ +package org.apache.tomcat.util.codec.binary; diff --git a/java/org/apache/tomcat/util/collections/CaseInsensitiveKeyMap.java b/java/org/apache/tomcat/util/collections/CaseInsensitiveKeyMap.java new file mode 100644 index 0000000..eb4b981 --- /dev/null +++ b/java/org/apache/tomcat/util/collections/CaseInsensitiveKeyMap.java @@ -0,0 +1,208 @@ +/* + * 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.tomcat.util.collections; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.tomcat.util.res.StringManager; + +/** + * A Map implementation that uses case-insensitive (using {@link + * Locale#ENGLISH}) strings as keys. + *

    + * Keys must be instances of {@link String}. Note that this means that + * null keys are not permitted. + *

    + * This implementation is not thread-safe. + * + * @param Type of values placed in this Map. + */ +public class CaseInsensitiveKeyMap extends AbstractMap { + + private static final StringManager sm = + StringManager.getManager(CaseInsensitiveKeyMap.class); + + private final Map map = new HashMap<>(); + + + @Override + public V get(Object key) { + return map.get(Key.getInstance(key)); + } + + + @Override + public V put(String key, V value) { + Key caseInsensitiveKey = Key.getInstance(key); + if (caseInsensitiveKey == null) { + throw new NullPointerException(sm.getString("caseInsensitiveKeyMap.nullKey")); + } + return map.put(caseInsensitiveKey, value); + } + + + /** + * {@inheritDoc} + *

    + * Use this method with caution. If the input Map contains duplicate + * keys when the keys are compared in a case insensitive manner then some + * values will be lost when inserting via this method. + */ + @Override + public void putAll(Map m) { + super.putAll(m); + } + + + @Override + public boolean containsKey(Object key) { + return map.containsKey(Key.getInstance(key)); + } + + + @Override + public V remove(Object key) { + return map.remove(Key.getInstance(key)); + } + + + @Override + public Set> entrySet() { + return new EntrySet<>(map.entrySet()); + } + + + private static class EntrySet extends AbstractSet> { + + private final Set> entrySet; + + EntrySet(Set> entrySet) { + this.entrySet = entrySet; + } + + @Override + public Iterator> iterator() { + return new EntryIterator<>(entrySet.iterator()); + } + + @Override + public int size() { + return entrySet.size(); + } + } + + + private static class EntryIterator implements Iterator> { + + private final Iterator> iterator; + + EntryIterator(Iterator> iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Entry next() { + Entry entry = iterator.next(); + return new EntryImpl<>(entry.getKey().getKey(), entry.getValue()); + } + + @Override + public void remove() { + iterator.remove(); + } + } + + + private static class EntryImpl implements Entry { + + private final String key; + private final V value; + + EntryImpl(String key, V value) { + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + } + + private static class Key { + + private final String key; + private final String lcKey; + + private Key(String key) { + this.key = key; + this.lcKey = key.toLowerCase(Locale.ENGLISH); + } + + public String getKey() { + return key; + } + + @Override + public int hashCode() { + return lcKey.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Key other = (Key) obj; + return lcKey.equals(other.lcKey); + } + + public static Key getInstance(Object o) { + if (o instanceof String) { + return new Key((String) o); + } + return null; + } + } +} diff --git a/java/org/apache/tomcat/util/collections/ConcurrentCache.java b/java/org/apache/tomcat/util/collections/ConcurrentCache.java new file mode 100644 index 0000000..7eff23e --- /dev/null +++ b/java/org/apache/tomcat/util/collections/ConcurrentCache.java @@ -0,0 +1,59 @@ +/* + * 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.tomcat.util.collections; + +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +public final class ConcurrentCache { + + private final int size; + + private final Map eden; + + private final Map longterm; + + public ConcurrentCache(int size) { + this.size = size; + this.eden = new ConcurrentHashMap<>(size); + this.longterm = new WeakHashMap<>(size); + } + + public V get(K k) { + V v = this.eden.get(k); + if (v == null) { + synchronized (longterm) { + v = this.longterm.get(k); + } + if (v != null) { + this.eden.put(k, v); + } + } + return v; + } + + public void put(K k, V v) { + if (this.eden.size() >= size) { + synchronized (longterm) { + this.longterm.putAll(this.eden); + } + this.eden.clear(); + } + this.eden.put(k, v); + } +} diff --git a/java/org/apache/tomcat/util/collections/ManagedConcurrentWeakHashMap.java b/java/org/apache/tomcat/util/collections/ManagedConcurrentWeakHashMap.java new file mode 100644 index 0000000..630f989 --- /dev/null +++ b/java/org/apache/tomcat/util/collections/ManagedConcurrentWeakHashMap.java @@ -0,0 +1,271 @@ +/* + * 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.tomcat.util.collections; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Concurrent hash map that holds its keys via weak references. Unlike + * WeakHashMap this class does not handle dead keys during common + * access operations, but expects you to call its {@link #maintain()} method + * periodically. Both keys and values are expected to be not-null. + * + * @param The type of keys used with the Map instance + * @param The type of values used with the Map instance + */ +public class ManagedConcurrentWeakHashMap extends AbstractMap implements + ConcurrentMap { + + private final ConcurrentMap map = new ConcurrentHashMap<>(); + private final ReferenceQueue queue = new ReferenceQueue<>(); + + /** + * Method, that has to be invoked periodically to clean dead keys from the + * map. + */ + public void maintain() { + Key key; + while ((key = (Key) queue.poll()) != null) { + if (key.isDead()) { + // No need to lookup if the key is not in the map + continue; + } + key.ackDeath(); + map.remove(key); + } + } + + private static class Key extends WeakReference { + private final int hash; + private boolean dead; + + Key(Object key, ReferenceQueue queue) { + super(key, queue); + hash = key.hashCode(); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (dead) { + // Post-mortem cleanup looks for this specific Reference + // instance + return false; + } + if (!(obj instanceof Reference)) { + return false; + } + Object oA = get(); + Object oB = ((Reference) obj).get(); + if (oA == oB) { + return true; + } + if (oA == null || oB == null) { + return false; + } + return oA.equals(oB); + } + + public void ackDeath() { + this.dead = true; + } + + public boolean isDead() { + return dead; + } + } + + /** + * Creates Key instance to be used to store values in the map. It is + * registered with the ReferenceQueue. + */ + private Key createStoreKey(Object key) { + return new Key(key, queue); + } + + /** + * Creates Key instance to be used only to lookup values in the map. It is + * not registered with the ReferenceQueue. + */ + private Key createLookupKey(Object key) { + return new Key(key, null); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsValue(Object value) { + if (value == null) { + return false; + } + return map.containsValue(value); + } + + @Override + public boolean containsKey(Object key) { + if (key == null) { + return false; + } + return map.containsKey(createLookupKey(key)); + } + + @Override + public V get(Object key) { + if (key == null) { + return null; + } + return map.get(createLookupKey(key)); + } + + @Override + public V put(K key, V value) { + Objects.requireNonNull(value); + return map.put(createStoreKey(key), value); + } + + @Override + public V remove(Object key) { + return map.remove(createLookupKey(key)); + } + + @Override + public void clear() { + map.clear(); + // maintain() clears the queue, though it is not 100% reliable, + // as queue is populated by GC asynchronously + maintain(); + } + + @Override + public V putIfAbsent(K key, V value) { + Objects.requireNonNull(value); + Key storeKey = createStoreKey(key); + V oldValue = map.putIfAbsent(storeKey, value); + if (oldValue != null) { // ack that key has not been stored + storeKey.ackDeath(); + } + return oldValue; + } + + @Override + public boolean remove(Object key, Object value) { + if (value == null) { + return false; + } + return map.remove(createLookupKey(key), value); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + Objects.requireNonNull(newValue); + return map.replace(createLookupKey(key), oldValue, newValue); + } + + @Override + public V replace(K key, V value) { + Objects.requireNonNull(value); + return map.replace(createLookupKey(key), value); + } + + @Override + public Collection values() { + return map.values(); + } + + @Override + public Set> entrySet() { + return new AbstractSet<>() { + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public Iterator> iterator() { + return new Iterator<>() { + private final Iterator> it = map + .entrySet().iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Map.Entry next() { + return new Map.Entry<>() { + private final Map.Entry en = it.next(); + + @SuppressWarnings("unchecked") + @Override + public K getKey() { + return (K) en.getKey().get(); + } + + @Override + public V getValue() { + return en.getValue(); + } + + @Override + public V setValue(V value) { + Objects.requireNonNull(value); + return en.setValue(value); + } + }; + } + + @Override + public void remove() { + it.remove(); + } + }; + } + }; + } +} diff --git a/java/org/apache/tomcat/util/collections/SynchronizedQueue.java b/java/org/apache/tomcat/util/collections/SynchronizedQueue.java new file mode 100644 index 0000000..44c9352 --- /dev/null +++ b/java/org/apache/tomcat/util/collections/SynchronizedQueue.java @@ -0,0 +1,105 @@ +/* + * 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.tomcat.util.collections; + +/** + * This is intended as a (mostly) GC-free alternative to + * {@link java.util.concurrent.ConcurrentLinkedQueue} when the requirement is to + * create an unbounded queue with no requirement to shrink the queue. The aim is + * to provide the bare minimum of required functionality as quickly as possible + * with minimum garbage. + * + * @param The type of object managed by this queue + */ +public class SynchronizedQueue { + + public static final int DEFAULT_SIZE = 128; + + private Object[] queue; + private int size; + private int insert = 0; + private int remove = 0; + + public SynchronizedQueue() { + this(DEFAULT_SIZE); + } + + public SynchronizedQueue(int initialSize) { + queue = new Object[initialSize]; + size = initialSize; + } + + public synchronized boolean offer(T t) { + queue[insert++] = t; + + // Wrap + if (insert == size) { + insert = 0; + } + + if (insert == remove) { + expand(); + } + return true; + } + + public synchronized T poll() { + if (insert == remove) { + // empty + return null; + } + + @SuppressWarnings("unchecked") + T result = (T) queue[remove]; + queue[remove] = null; + remove++; + + // Wrap + if (remove == size) { + remove = 0; + } + + return result; + } + + private void expand() { + int newSize = size * 2; + Object[] newQueue = new Object[newSize]; + + System.arraycopy(queue, insert, newQueue, 0, size - insert); + System.arraycopy(queue, 0, newQueue, size - insert, insert); + + insert = size; + remove = 0; + queue = newQueue; + size = newSize; + } + + public synchronized int size() { + int result = insert - remove; + if (result < 0) { + result += size; + } + return result; + } + + public synchronized void clear() { + queue = new Object[size]; + insert = 0; + remove = 0; + } +} diff --git a/java/org/apache/tomcat/util/collections/SynchronizedStack.java b/java/org/apache/tomcat/util/collections/SynchronizedStack.java new file mode 100644 index 0000000..526c44c --- /dev/null +++ b/java/org/apache/tomcat/util/collections/SynchronizedStack.java @@ -0,0 +1,105 @@ +/* + * 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.tomcat.util.collections; + +/** + * This is intended as a (mostly) GC-free alternative to + * {@link java.util.Stack} when the requirement is to create a pool of re-usable + * objects with no requirement to shrink the pool. The aim is to provide the + * bare minimum of required functionality as quickly as possible with minimum + * garbage. + * + * @param The type of object managed by this stack + */ +public class SynchronizedStack { + + public static final int DEFAULT_SIZE = 128; + private static final int DEFAULT_LIMIT = -1; + + private int size; + private final int limit; + + /* + * Points to the next available object in the stack + */ + private int index = -1; + + private Object[] stack; + + + public SynchronizedStack() { + this(DEFAULT_SIZE, DEFAULT_LIMIT); + } + + public SynchronizedStack(int size, int limit) { + if (limit > -1 && size > limit) { + this.size = limit; + } else { + this.size = size; + } + this.limit = limit; + stack = new Object[this.size]; + } + + + public synchronized boolean push(T obj) { + index++; + if (index == size) { + if (limit == -1 || size < limit) { + expand(); + } else { + index--; + return false; + } + } + stack[index] = obj; + return true; + } + + @SuppressWarnings("unchecked") + public synchronized T pop() { + if (index == -1) { + return null; + } + T result = (T) stack[index]; + stack[index--] = null; + return result; + } + + public synchronized void clear() { + if (index > -1) { + for (int i = 0; i < index + 1; i++) { + stack[i] = null; + } + } + index = -1; + } + + private void expand() { + int newSize = size * 2; + if (limit != -1 && newSize > limit) { + newSize = limit; + } + Object[] newStack = new Object[newSize]; + System.arraycopy(stack, 0, newStack, 0, size); + // This is the only point where garbage is created by throwing away the + // old array. Note it is only the array, not the contents, that becomes + // garbage. + stack = newStack; + size = newSize; + } +} diff --git a/java/org/apache/tomcat/util/compat/Jre16Compat.java b/java/org/apache/tomcat/util/compat/Jre16Compat.java new file mode 100644 index 0000000..fd05da4 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/Jre16Compat.java @@ -0,0 +1,100 @@ +/* + * 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.tomcat.util.compat; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.ProtocolFamily; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +class Jre16Compat extends JreCompat { + + private static final Log log = LogFactory.getLog(Jre16Compat.class); + private static final StringManager sm = StringManager.getManager(Jre16Compat.class); + + private static final Class unixDomainSocketAddressClazz; + private static final Method openServerSocketChannelFamilyMethod; + private static final Method unixDomainSocketAddressOfMethod; + private static final Method openSocketChannelFamilyMethod; + + static { + Class c1 = null; + Method m1 = null; + Method m2 = null; + Method m3 = null; + try { + c1 = Class.forName("java.net.UnixDomainSocketAddress"); + m1 = ServerSocketChannel.class.getMethod("open", ProtocolFamily.class); + m2 = c1.getMethod("of", String.class); + m3 = SocketChannel.class.getMethod("open", ProtocolFamily.class); + } catch (ClassNotFoundException e) { + // Must be pre-Java 16 + log.debug(sm.getString("jre16Compat.javaPre16"), e); + } catch (ReflectiveOperationException | IllegalArgumentException e) { + // Should never happen + log.error(sm.getString("jre16Compat.unexpected"), e); + } + unixDomainSocketAddressClazz = c1; + openServerSocketChannelFamilyMethod = m1; + unixDomainSocketAddressOfMethod = m2; + openSocketChannelFamilyMethod = m3; + } + + static boolean isSupported() { + return unixDomainSocketAddressClazz != null; + } + + + @Override + public SocketAddress getUnixDomainSocketAddress(String path) { + try { + return (SocketAddress) unixDomainSocketAddressOfMethod.invoke(null, path); + } catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } + + + @Override + public ServerSocketChannel openUnixDomainServerSocketChannel() { + try { + return (ServerSocketChannel) openServerSocketChannelFamilyMethod.invoke + (null, StandardProtocolFamily.valueOf("UNIX")); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } + + + @Override + public SocketChannel openUnixDomainSocketChannel() { + try { + return (SocketChannel) openSocketChannelFamilyMethod.invoke + (null, StandardProtocolFamily.valueOf("UNIX")); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } +} diff --git a/java/org/apache/tomcat/util/compat/Jre19Compat.java b/java/org/apache/tomcat/util/compat/Jre19Compat.java new file mode 100644 index 0000000..45ce68b --- /dev/null +++ b/java/org/apache/tomcat/util/compat/Jre19Compat.java @@ -0,0 +1,84 @@ +/* + * 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.tomcat.util.compat; + +import java.lang.reflect.Field; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class Jre19Compat extends Jre16Compat { + + private static final Log log = LogFactory.getLog(Jre19Compat.class); + private static final StringManager sm = StringManager.getManager(Jre19Compat.class); + + private static final boolean supported; + + static { + // Don't need any Java 19 specific classes (yet) so just test for one of + // the new ones for now. + Class c1 = null; + try { + c1 = Class.forName("java.lang.WrongThreadException"); + } catch (ClassNotFoundException cnfe) { + // Must be pre-Java 19 + log.debug(sm.getString("jre19Compat.javaPre19"), cnfe); + } + + supported = (c1 != null); + } + + static boolean isSupported() { + return supported; + } + + @Override + public Object getExecutor(Thread thread) + throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + + Object result = super.getExecutor(thread); + + if (result == null) { + Object holder = null; + Object task = null; + try { + Field holderField = thread.getClass().getDeclaredField("holder"); + holderField.setAccessible(true); + holder = holderField.get(thread); + + Field taskField = holder.getClass().getDeclaredField("task"); + taskField.setAccessible(true); + task = taskField.get(holder); + } catch (NoSuchFieldException nfe) { + return null; + } + + if (task!= null && task.getClass().getCanonicalName() != null && + (task.getClass().getCanonicalName().equals( + "org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") || + task.getClass().getCanonicalName().equals( + "java.util.concurrent.ThreadPoolExecutor.Worker"))) { + Field executorField = task.getClass().getDeclaredField("this$0"); + executorField.setAccessible(true); + result = executorField.get(task); + } + } + + return result; + } +} diff --git a/java/org/apache/tomcat/util/compat/Jre21Compat.java b/java/org/apache/tomcat/util/compat/Jre21Compat.java new file mode 100644 index 0000000..d06c851 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/Jre21Compat.java @@ -0,0 +1,82 @@ +/* + * 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.tomcat.util.compat; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class Jre21Compat extends Jre19Compat { + + private static final Log log = LogFactory.getLog(Jre21Compat.class); + private static final StringManager sm = StringManager.getManager(Jre21Compat.class); + + private static final Method nameMethod; + private static final Method startMethod; + private static final Method ofVirtualMethod; + + + static { + Class c1 = null; + Method m1 = null; + Method m2 = null; + Method m3 = null; + + try { + c1 = Class.forName("java.lang.Thread$Builder"); + m1 = c1.getMethod("name", String.class, long.class); + m2 = c1.getMethod("start", Runnable.class); + m3 = Thread.class.getMethod("ofVirtual", (Class[]) null); + } catch (ClassNotFoundException e) { + // Must be pre-Java 21 + log.debug(sm.getString("jre21Compat.javaPre21"), e); + } catch (ReflectiveOperationException e) { + // Should never happen + log.error(sm.getString("jre21Compat.unexpected"), e); + } + nameMethod = m1; + startMethod = m2; + ofVirtualMethod = m3; + } + + static boolean isSupported() { + return ofVirtualMethod != null; + } + + @Override + public Object createVirtualThreadBuilder(String name) { + try { + Object threadBuilder = ofVirtualMethod.invoke(null, (Object[]) null); + nameMethod.invoke(threadBuilder, name, Long.valueOf(0)); + return threadBuilder; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } + + @Override + public void threadBuilderStart(Object threadBuilder, Runnable command) { + try { + startMethod.invoke(threadBuilder, command); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } +} diff --git a/java/org/apache/tomcat/util/compat/Jre22Compat.java b/java/org/apache/tomcat/util/compat/Jre22Compat.java new file mode 100644 index 0000000..fd45c20 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/Jre22Compat.java @@ -0,0 +1,49 @@ +/* + * 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.tomcat.util.compat; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class Jre22Compat extends Jre21Compat { + + private static final Log log = LogFactory.getLog(Jre22Compat.class); + private static final StringManager sm = StringManager.getManager(Jre22Compat.class); + + private static final boolean supported; + + + static { + // Note: FFM is the main new feature from Java 22, but it was previously + // present as a preview. As a result, it is more accurate to test for another + // new class + Class c1 = null; + try { + c1 = Class.forName("java.text.ListFormat"); + } catch (ClassNotFoundException e) { + // Must be pre-Java 22 + log.debug(sm.getString("jre22Compat.javaPre22"), e); + } + supported = (c1 != null); + } + + static boolean isSupported() { + return supported; + } + +} diff --git a/java/org/apache/tomcat/util/compat/JreCompat.java b/java/org/apache/tomcat/util/compat/JreCompat.java new file mode 100644 index 0000000..86f540b --- /dev/null +++ b/java/org/apache/tomcat/util/compat/JreCompat.java @@ -0,0 +1,231 @@ +/* + * 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.tomcat.util.compat; + +import java.lang.reflect.Field; +import java.net.SocketAddress; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +import org.apache.tomcat.util.res.StringManager; + +/** + * This is the base implementation class for JRE compatibility and provides an + * implementation based on Java 11. Sub-classes may extend this class and provide + * alternative implementations for later JRE versions + */ +public class JreCompat { + + private static final JreCompat instance; + private static final boolean graalAvailable; + private static final boolean jre16Available; + private static final boolean jre19Available; + private static final boolean jre21Available; + private static final boolean jre22Available; + private static final StringManager sm = StringManager.getManager(JreCompat.class); + + static { + boolean result = false; + try { + Class nativeImageClazz = Class.forName("org.graalvm.nativeimage.ImageInfo"); + result = Boolean.TRUE.equals(nativeImageClazz.getMethod("inImageCode").invoke(null)); + } catch (ClassNotFoundException e) { + // Must be Graal + } catch (ReflectiveOperationException | IllegalArgumentException e) { + // Should never happen + } + graalAvailable = result || System.getProperty("org.graalvm.nativeimage.imagecode") != null; + + // This is Tomcat 10.1.x with a minimum Java version of Java 11. + // Look for the highest supported JVM first + if (Jre22Compat.isSupported()) { + instance = new Jre22Compat(); + jre22Available = true; + jre21Available = true; + jre19Available = true; + jre16Available = true; + } else if (Jre21Compat.isSupported()) { + instance = new Jre21Compat(); + jre22Available = false; + jre21Available = true; + jre19Available = true; + jre16Available = true; + } else if (Jre19Compat.isSupported()) { + instance = new Jre19Compat(); + jre22Available = false; + jre21Available = false; + jre19Available = true; + jre16Available = true; + } else if (Jre16Compat.isSupported()) { + instance = new Jre16Compat(); + jre22Available = false; + jre21Available = false; + jre19Available = false; + jre16Available = true; + } else { + instance = new JreCompat(); + jre22Available = false; + jre21Available = false; + jre19Available = false; + jre16Available = false; + } + } + + + public static JreCompat getInstance() { + return instance; + } + + + public static boolean isGraalAvailable() { + return graalAvailable; + } + + + public static boolean isJre16Available() { + return jre16Available; + } + + + public static boolean isJre19Available() { + return jre19Available; + } + + + public static boolean isJre21Available() { + return jre21Available; + } + + + public static boolean isJre22Available() { + return jre22Available; + } + + + // Java 11 implementations of Java 16 methods + + /** + * Return Unix domain socket address for given path. + * @param path The path + * @return the socket address + */ + public SocketAddress getUnixDomainSocketAddress(String path) { + return null; + } + + + /** + * Create server socket channel using the Unix domain socket ProtocolFamily. + * @return the server socket channel + */ + public ServerSocketChannel openUnixDomainServerSocketChannel() { + throw new UnsupportedOperationException(sm.getString("jreCompat.noUnixDomainSocket")); + } + + + /** + * Create socket channel using the Unix domain socket ProtocolFamily. + * @return the socket channel + */ + public SocketChannel openUnixDomainSocketChannel() { + throw new UnsupportedOperationException(sm.getString("jreCompat.noUnixDomainSocket")); + } + + + // Java 11 implementations of Java 19 methods + + /** + * Obtains the executor, if any, used to create the provided thread. + * + * @param thread The thread to examine + * + * @return The executor, if any, that created the provided thread + * + * @throws NoSuchFieldException + * If a field used via reflection to obtain the executor cannot + * be found + * @throws SecurityException + * If a security exception occurs while trying to identify the + * executor + * @throws IllegalArgumentException + * If the instance object does not match the class of the field + * when obtaining a field value via reflection + * @throws IllegalAccessException + * If a field is not accessible due to access restrictions + */ + public Object getExecutor(Thread thread) + throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + + Object result = null; + + // Runnable wrapped by Thread + // "target" in Sun/Oracle JDK + // "runnable" in IBM JDK + // "action" in Apache Harmony + Object target = null; + for (String fieldName : new String[] { "target", "runnable", "action" }) { + try { + Field targetField = thread.getClass().getDeclaredField(fieldName); + targetField.setAccessible(true); + target = targetField.get(thread); + break; + } catch (NoSuchFieldException nfe) { + continue; + } + } + + // "java.util.concurrent" code is in public domain, + // so all implementations are similar including our + // internal fork. + if (target != null && target.getClass().getCanonicalName() != null && + (target.getClass().getCanonicalName().equals( + "org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") || + target.getClass().getCanonicalName().equals( + "java.util.concurrent.ThreadPoolExecutor.Worker"))) { + Field executorField = target.getClass().getDeclaredField("this$0"); + executorField.setAccessible(true); + result = executorField.get(target); + } + + return result; + } + + + // Java 11 implementations of Java 21 methods + + /** + * Create a thread builder for virtual threads using the given name to name the threads. + * + * @param name The base name for the threads + * + * @return The thread buidler for virtual threads + */ + public Object createVirtualThreadBuilder(String name) { + throw new UnsupportedOperationException(sm.getString("jreCompat.noVirtualThreads")); + } + + + /** + * Create a thread with the given thread builder and use it to execute the given runnable. + * + * @param threadBuilder The thread builder to use to create a thread + * @param command The command to run + */ + public void threadBuilderStart(Object threadBuilder, Runnable command) { + throw new UnsupportedOperationException(sm.getString("jreCompat.noVirtualThreads")); + } +} diff --git a/java/org/apache/tomcat/util/compat/JrePlatform.java b/java/org/apache/tomcat/util/compat/JrePlatform.java new file mode 100644 index 0000000..66717d6 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/JrePlatform.java @@ -0,0 +1,57 @@ +/* + * 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.tomcat.util.compat; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Locale; + +public class JrePlatform { + + private static final String OS_NAME_PROPERTY = "os.name"; + + static { + /* + * There are a few places where a) the behaviour of the Java API depends + * on the underlying platform and b) those behavioural differences have + * an impact on Tomcat. + * + * Tomcat therefore needs to be able to determine the platform it is + * running on to account for those differences. + * + * In an ideal world this code would not exist. + */ + + // This check is derived from the check in Apache Commons Lang + String osName; + if (System.getSecurityManager() == null) { + osName = System.getProperty(OS_NAME_PROPERTY); + } else { + osName = AccessController.doPrivileged( + (PrivilegedAction) () -> System.getProperty(OS_NAME_PROPERTY)); + } + + IS_MAC_OS = osName.toLowerCase(Locale.ENGLISH).startsWith("mac os x"); + + IS_WINDOWS = osName.startsWith("Windows"); + } + + + public static final boolean IS_MAC_OS; + + public static final boolean IS_WINDOWS; +} diff --git a/java/org/apache/tomcat/util/compat/JreVendor.java b/java/org/apache/tomcat/util/compat/JreVendor.java new file mode 100644 index 0000000..caa0823 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/JreVendor.java @@ -0,0 +1,49 @@ +/* + * 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.tomcat.util.compat; + +import java.util.Locale; + +public class JreVendor { + + static { + /* + * There are a few places where Tomcat either accesses JVM internals + * (e.g. the memory leak protection) or where feature support varies + * between JVMs (e.g. SPNEGO). These flags exist to enable Tomcat to + * adjust its behaviour based on the vendor of the JVM. In an ideal + * world this code would not exist. + */ + String vendor = System.getProperty("java.vendor", ""); + vendor = vendor.toLowerCase(Locale.ENGLISH); + + if (vendor.startsWith("oracle") || vendor.startsWith("sun")) { + IS_ORACLE_JVM = true; + IS_IBM_JVM = false; + } else if (vendor.contains("ibm")) { + IS_ORACLE_JVM = false; + IS_IBM_JVM = true; + } else { + IS_ORACLE_JVM = false; + IS_IBM_JVM = false; + } + } + + public static final boolean IS_ORACLE_JVM; + + public static final boolean IS_IBM_JVM; +} diff --git a/java/org/apache/tomcat/util/compat/LocalStrings.properties b/java/org/apache/tomcat/util/compat/LocalStrings.properties new file mode 100644 index 0000000..e86cdb5 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/LocalStrings.properties @@ -0,0 +1,28 @@ +# 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. + +jre16Compat.javaPre16=Class not found so assuming code is running on a pre-Java 16 JVM +jre16Compat.unexpected=Failed to create references to Java 16 classes and methods + +jre19Compat.javaPre19=Class not found so assuming code is running on a pre-Java 19 JVM + +jre21Compat.javaPre21=Class not found so assuming code is running on a pre-Java 21 JVM +jre21Compat.unexpected=Failed to create references to Java 21 classes and methods + +jre22Compat.javaPre22=Class not found so assuming code is running on a pre-Java 22 JVM +jre22Compat.unexpected=Failed to create references to Java 22 classes and methods + +jreCompat.noUnixDomainSocket=Java Runtime does not support Unix domain sockets. You must use Java 16 or later to use this feature. +jreCompat.noVirtualThreads=Java Runtime does not support virtual threads. You must use Java 21 or later to use this feature. diff --git a/java/org/apache/tomcat/util/compat/LocalStrings_fr.properties b/java/org/apache/tomcat/util/compat/LocalStrings_fr.properties new file mode 100644 index 0000000..447a3cd --- /dev/null +++ b/java/org/apache/tomcat/util/compat/LocalStrings_fr.properties @@ -0,0 +1,24 @@ +# 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. + +jre16Compat.javaPre16=Le code est considéré être exécuté sur une JVM antérieure à Java 16 car la classe n'a pas été trouvée +jre16Compat.unexpected=Impossible de créer les références vers les classes et méthodes de Java 16 + +jre19Compat.javaPre19=Classe non trouvée donc le code est exécutée sur une JVM antérieure à Java 19 + +jre22Compat.javaPre22=Le code est considéré être exécuté sur une JVM antérieure à Java 22 car la classe n'a pas été trouvée +jre22Compat.unexpected=Impossible de créer les références vers les classes et méthodes de Java 22 + +jreCompat.noUnixDomainSocket=L'environnement Java ne supporte pas les sockets de domaine de Unix, cette fonctionnalité demande Java 16 diff --git a/java/org/apache/tomcat/util/compat/LocalStrings_ja.properties b/java/org/apache/tomcat/util/compat/LocalStrings_ja.properties new file mode 100644 index 0000000..9f09063 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/LocalStrings_ja.properties @@ -0,0 +1,24 @@ +# 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. + +jre16Compat.javaPre16=クラスãŒè¦‹ã¤ã‹ã‚‰ãªã„ãŸã‚ã€ã‚³ãƒ¼ãƒ‰ãŒJava 16未満ã®JVMã§å®Ÿè¡Œã•ã‚Œã¦ã„ã‚‹ã¨ä»®å®šã—ã¾ã™ +jre16Compat.unexpected=Java 16クラスãŠã‚ˆã³ãƒ¡ã‚½ãƒƒãƒ‰ã¸ã®å‚ç…§ã®ä½œæˆã«å¤±æ•—ã—ã¾ã—㟠+ +jre19Compat.javaPre19=クラスãŒè¦‹ã¤ã‹ã‚‰ãªã„ãŸã‚ã€Java 19よりå‰ã®JVMã§ã‚³ãƒ¼ãƒ‰ãŒå®Ÿè¡Œã•ã‚Œã¦ã„ã‚‹ã¨ä»®å®šã—ã¾ã™ + +jre22Compat.javaPre22=クラスãŒè¦‹ã¤ã‹ã‚‰ãªã„ãŸã‚ã€ã‚³ãƒ¼ãƒ‰ãŒ Java 22 以å‰ã® JVM ã§å®Ÿè¡Œã•ã‚Œã¦ã„ã‚‹ã¨ä»®å®šã—ã¾ã™ +jre22Compat.unexpected=Java 22ã®ã‚¯ãƒ©ã‚¹ãƒˆãƒ¡ã‚½ãƒƒãƒ‰ã¸ã®å‚照を作æˆã™ã‚‹ã®ã«å¤±æ•—ã—ã¾ã—㟠+ +jreCompat.noUnixDomainSocket=Java実行環境ã¯Unixドメインソケットã«å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“。 ã“ã®æ©Ÿèƒ½ã‚’使用ã™ã‚‹ã«ã¯ã€Java 16を使用ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ diff --git a/java/org/apache/tomcat/util/compat/LocalStrings_ko.properties b/java/org/apache/tomcat/util/compat/LocalStrings_ko.properties new file mode 100644 index 0000000..7db32e6 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/LocalStrings_ko.properties @@ -0,0 +1,21 @@ +# 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. + +jre16Compat.javaPre16=í´ëž˜ìŠ¤ê°€ 발견ë˜ì§€ 않습니다. ìžë°” 16 ì´ì „ì˜ JVMì—ì„œ 실행 ì¤‘ì¸ ê²ƒìœ¼ë¡œ 보입니다. +jre16Compat.unexpected=ìžë°” 16 í´ëž˜ìŠ¤ë“¤ê³¼ ë©”ì†Œë“œë“¤ì„ ì°¸ì¡°í•  수 없습니다. + +jre19Compat.javaPre19=í´ëž˜ìŠ¤ê°€ 발견ë˜ì§€ 않으므로, 코드가 ìžë°” 19 ì´ì „ ë²„ì „ì˜ JVMì—ì„œ 실행 중ì´ë¼ê³  가정합니다. + +jreCompat.noUnixDomainSocket=ìžë°” ëŸ°íƒ€ìž„ì´ Unix ë„ë©”ì¸ ì†Œì¼“ì„ ì§€ì›í•˜ì§€ 않습니다. ì´ ê¸°ëŠ¥ì„ ì‚¬ìš©í•˜ë ¤ë©´ ìžë°” 16ì„ ì‚¬ìš©í•´ì•¼ 합니다. diff --git a/java/org/apache/tomcat/util/compat/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/compat/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..c7be88b --- /dev/null +++ b/java/org/apache/tomcat/util/compat/LocalStrings_zh_CN.properties @@ -0,0 +1,21 @@ +# 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. + +jre16Compat.javaPre16=类未找到,因此å‡è®¾ä»£ç åœ¨Java16之å‰çš„JVM上è¿è¡Œ +jre16Compat.unexpected=无法创建对Java16中类和方法的引用 + +jre19Compat.javaPre19=该类未找到,因此推测当å‰ä»£ç è¿è¡Œåœ¨Java 19版本之å‰çš„虚拟机上 + +jreCompat.noUnixDomainSocket=Javaè¿è¡ŒçŽ¯å¢ƒä¸æ”¯æŒUnix域å套接字。你必须使用Java16æ¥è¿è¡Œè¯¥ç‰¹æ€§ã€‚ diff --git a/java/org/apache/tomcat/util/descriptor/Constants.java b/java/org/apache/tomcat/util/descriptor/Constants.java new file mode 100644 index 0000000..2ad5996 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/Constants.java @@ -0,0 +1,25 @@ +/* + * 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.tomcat.util.descriptor; + +public class Constants { + + public static final String PACKAGE_NAME = + Constants.class.getPackage().getName(); + + public static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); +} diff --git a/java/org/apache/tomcat/util/descriptor/DigesterFactory.java b/java/org/apache/tomcat/util/descriptor/DigesterFactory.java new file mode 100644 index 0000000..823b4fe --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/DigesterFactory.java @@ -0,0 +1,212 @@ +/* + * 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.tomcat.util.descriptor; + +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.ServletContext; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.ext.EntityResolver2; + +/** + * Wrapper class around the Digester that hide Digester's initialization + * details. + */ +public class DigesterFactory { + + private static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + private static final Class CLASS_SERVLET_CONTEXT; + private static final Class CLASS_JSP_CONTEXT; + + static { + CLASS_SERVLET_CONTEXT = ServletContext.class; + Class jspContext = null; + try { + jspContext = Class.forName("jakarta.servlet.jsp.JspContext"); + } catch (ClassNotFoundException e) { + // Ignore - JSP API is not present. + } + CLASS_JSP_CONTEXT = jspContext; + } + + + /** + * Mapping of well-known public IDs used by the Servlet API to the matching + * local resource. + */ + public static final Map SERVLET_API_PUBLIC_IDS; + + /** + * Mapping of well-known system IDs used by the Servlet API to the matching + * local resource. + */ + public static final Map SERVLET_API_SYSTEM_IDS; + + static { + Map publicIds = new HashMap<>(); + Map systemIds = new HashMap<>(); + + // W3C + add(publicIds, XmlIdentifiers.XSD_10_PUBLIC, locationFor("XMLSchema.dtd")); + add(publicIds, XmlIdentifiers.DATATYPES_PUBLIC, locationFor("datatypes.dtd")); + add(systemIds, XmlIdentifiers.XML_2001_XSD, locationFor("xml.xsd")); + + // from J2EE 1.2 + add(publicIds, XmlIdentifiers.WEB_22_PUBLIC, locationFor("web-app_2_2.dtd")); + add(publicIds, XmlIdentifiers.TLD_11_PUBLIC, locationFor("web-jsptaglibrary_1_1.dtd")); + + // from J2EE 1.3 + add(publicIds, XmlIdentifiers.WEB_23_PUBLIC, locationFor("web-app_2_3.dtd")); + add(publicIds, XmlIdentifiers.TLD_12_PUBLIC, locationFor("web-jsptaglibrary_1_2.dtd")); + + // from J2EE 1.4 + add(systemIds, "http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd", + locationFor("j2ee_web_services_1_1.xsd")); + add(systemIds, "http://www.ibm.com/webservices/xsd/j2ee_web_services_client_1_1.xsd", + locationFor("j2ee_web_services_client_1_1.xsd")); + add(systemIds, XmlIdentifiers.WEB_24_XSD, locationFor("web-app_2_4.xsd")); + add(systemIds, XmlIdentifiers.TLD_20_XSD, locationFor("web-jsptaglibrary_2_0.xsd")); + addSelf(systemIds, "j2ee_1_4.xsd"); + addSelf(systemIds, "jsp_2_0.xsd"); + + // from JavaEE 5 + add(systemIds, XmlIdentifiers.WEB_25_XSD, locationFor("web-app_2_5.xsd")); + add(systemIds, XmlIdentifiers.TLD_21_XSD, locationFor("web-jsptaglibrary_2_1.xsd")); + addSelf(systemIds, "javaee_5.xsd"); + addSelf(systemIds, "jsp_2_1.xsd"); + addSelf(systemIds, "javaee_web_services_1_2.xsd"); + addSelf(systemIds, "javaee_web_services_client_1_2.xsd"); + + // from JavaEE 6 + add(systemIds, XmlIdentifiers.WEB_30_XSD, locationFor("web-app_3_0.xsd")); + add(systemIds, XmlIdentifiers.WEB_FRAGMENT_30_XSD, locationFor("web-fragment_3_0.xsd")); + addSelf(systemIds, "web-common_3_0.xsd"); + addSelf(systemIds, "javaee_6.xsd"); + addSelf(systemIds, "jsp_2_2.xsd"); + addSelf(systemIds, "javaee_web_services_1_3.xsd"); + addSelf(systemIds, "javaee_web_services_client_1_3.xsd"); + + // from JavaEE 7 + add(systemIds, XmlIdentifiers.WEB_31_XSD, locationFor("web-app_3_1.xsd")); + add(systemIds, XmlIdentifiers.WEB_FRAGMENT_31_XSD, locationFor("web-fragment_3_1.xsd")); + addSelf(systemIds, "web-common_3_1.xsd"); + addSelf(systemIds, "javaee_7.xsd"); + addSelf(systemIds, "jsp_2_3.xsd"); + addSelf(systemIds, "javaee_web_services_1_4.xsd"); + addSelf(systemIds, "javaee_web_services_client_1_4.xsd"); + + // from JavaEE 8 + add(systemIds, XmlIdentifiers.WEB_40_XSD, locationFor("web-app_4_0.xsd")); + add(systemIds, XmlIdentifiers.WEB_FRAGMENT_40_XSD, locationFor("web-fragment_4_0.xsd")); + addSelf(systemIds, "web-common_4_0.xsd"); + addSelf(systemIds, "javaee_8.xsd"); + + // from JakartaEE 9 + add(systemIds, XmlIdentifiers.WEB_50_XSD, locationFor("web-app_5_0.xsd")); + add(systemIds, XmlIdentifiers.WEB_FRAGMENT_50_XSD, locationFor("web-fragment_5_0.xsd")); + add(systemIds, XmlIdentifiers.TLD_30_XSD, locationFor("web-jsptaglibrary_3_0.xsd")); + addSelf(systemIds, "web-common_5_0.xsd"); + addSelf(systemIds, "jakartaee_9.xsd"); + addSelf(systemIds, "jsp_3_0.xsd"); + addSelf(systemIds, "jakartaee_web_services_2_0.xsd"); + addSelf(systemIds, "jakartaee_web_services_client_2_0.xsd"); + + // from JakartaEE 10 + add(systemIds, XmlIdentifiers.WEB_60_XSD, locationFor("web-app_6_0.xsd")); + add(systemIds, XmlIdentifiers.WEB_FRAGMENT_60_XSD, locationFor("web-fragment_6_0.xsd")); + add(systemIds, XmlIdentifiers.TLD_31_XSD, locationFor("web-jsptaglibrary_3_1.xsd")); + addSelf(systemIds, "web-common_6_0.xsd"); + addSelf(systemIds, "jakartaee_10.xsd"); + addSelf(systemIds, "jsp_3_1.xsd"); + addSelf(systemIds, "jakartaee_web_services_2_0.xsd"); + addSelf(systemIds, "jakartaee_web_services_client_2_0.xsd"); + + SERVLET_API_PUBLIC_IDS = Collections.unmodifiableMap(publicIds); + SERVLET_API_SYSTEM_IDS = Collections.unmodifiableMap(systemIds); + } + + private static void addSelf(Map ids, String id) { + String location = locationFor(id); + if (location != null) { + ids.put(id, location); + ids.put(location, location); + } + } + + private static void add(Map ids, String id, String location) { + if (location != null) { + ids.put(id, location); + // BZ 63311 + // Support http and https locations as the move away from http and + // towards https continues. + if (id.startsWith("http://")) { + String httpsId = "https://" + id.substring(7); + ids.put(httpsId, location); + } + } + } + + private static String locationFor(String name) { + URL location = CLASS_SERVLET_CONTEXT.getResource("resources/" + name); + if (location == null && CLASS_JSP_CONTEXT != null) { + location = CLASS_JSP_CONTEXT.getResource("resources/" + name); + } + if (location == null) { + Log log = LogFactory.getLog(DigesterFactory.class); + log.warn(sm.getString("digesterFactory.missingSchema", name)); + return null; + } + return location.toExternalForm(); + } + + + /** + * Create a Digester parser. + * @param xmlValidation turn on/off xml validation + * @param xmlNamespaceAware turn on/off namespace validation + * @param rule an instance of RuleSet used for parsing the xml. + * @param blockExternal turn on/off the blocking of external resources + * @return a new digester + */ + public static Digester newDigester(boolean xmlValidation, + boolean xmlNamespaceAware, + RuleSet rule, + boolean blockExternal) { + Digester digester = new Digester(); + digester.setNamespaceAware(xmlNamespaceAware); + digester.setValidating(xmlValidation); + digester.setUseContextClassLoader(true); + EntityResolver2 resolver = new LocalResolver(SERVLET_API_PUBLIC_IDS, + SERVLET_API_SYSTEM_IDS, blockExternal); + digester.setEntityResolver(resolver); + if (rule != null) { + digester.addRuleSet(rule); + } + + return digester; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/InputSourceUtil.java b/java/org/apache/tomcat/util/descriptor/InputSourceUtil.java new file mode 100644 index 0000000..ea70a68 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/InputSourceUtil.java @@ -0,0 +1,47 @@ +/* + * 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.tomcat.util.descriptor; + +import java.io.InputStream; + +import org.apache.tomcat.util.ExceptionUtils; +import org.xml.sax.InputSource; + +public final class InputSourceUtil { + + private InputSourceUtil() { + // Utility class. Hide default constructor. + } + + + public static void close(InputSource inputSource) { + if (inputSource == null) { + // Nothing to do + return; + } + + InputStream is = inputSource.getByteStream(); + if (is != null) { + try { + is.close(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } + } + + } +} diff --git a/java/org/apache/tomcat/util/descriptor/LocalResolver.java b/java/org/apache/tomcat/util/descriptor/LocalResolver.java new file mode 100644 index 0000000..dbfd593 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/LocalResolver.java @@ -0,0 +1,162 @@ +/* + * 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.tomcat.util.descriptor; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.ext.EntityResolver2; + +/** + * A resolver for locally cached XML resources. + */ +public class LocalResolver implements EntityResolver2 { + + private static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + private static final String[] JAVA_EE_NAMESPACES = { + XmlIdentifiers.JAVAEE_1_4_NS, + XmlIdentifiers.JAVAEE_5_NS, + XmlIdentifiers.JAVAEE_7_NS, + XmlIdentifiers.JAKARTAEE_9_NS}; + + + private final Map publicIds; + private final Map systemIds; + private final boolean blockExternal; + + /** + * Constructor providing mappings of public and system identifiers to local + * resources. Each map contains a mapping from a well-known identifier to a + * URL for a local resource path. + * + * @param publicIds mapping of well-known public identifiers to local + * resources + * @param systemIds mapping of well-known system identifiers to local + * resources + * @param blockExternal are external resources blocked that are not + * well-known + */ + public LocalResolver(Map publicIds, + Map systemIds, boolean blockExternal) { + this.publicIds = publicIds; + this.systemIds = systemIds; + this.blockExternal = blockExternal; + } + + + @Override + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + return resolveEntity(null, publicId, null, systemId); + } + + + @Override + public InputSource resolveEntity(String name, String publicId, + String base, String systemId) throws SAXException, IOException { + + // First try resolving using the publicId + String resolved = publicIds.get(publicId); + if (resolved != null) { + InputSource is = new InputSource(resolved); + is.setPublicId(publicId); + return is; + } + + // If there is no systemId, can't try anything else + if (systemId == null) { + throw new FileNotFoundException(sm.getString("localResolver.unresolvedEntity", + name, publicId, null, base)); + } + + // Try resolving with the supplied systemId + resolved = systemIds.get(systemId); + if (resolved != null) { + InputSource is = new InputSource(resolved); + is.setPublicId(publicId); + return is; + } + + // Work-around for XML documents that use just the file name for the + // location to refer to a JavaEE schema + for (String javaEENamespace : JAVA_EE_NAMESPACES) { + String javaEESystemId = javaEENamespace + '/' + systemId; + resolved = systemIds.get(javaEESystemId); + if (resolved != null) { + InputSource is = new InputSource(resolved); + is.setPublicId(publicId); + return is; + } + } + + // Resolve the supplied systemId against the base + URI systemUri; + try { + if (base == null) { + systemUri = new URI(systemId); + } else { + URI baseUri = new URI(base); + systemUri = UriUtil.resolve(baseUri, systemId); + } + systemUri = systemUri.normalize(); + } catch (URISyntaxException e) { + // May be caused by a | being used instead of a : in an absolute + // file URI on Windows. + if (blockExternal) { + // Absolute paths aren't allowed so block it + throw new MalformedURLException(e.getMessage()); + } else { + // See if the URLHandler can resolve it + return new InputSource(systemId); + } + } + if (systemUri.isAbsolute()) { + // Try the resolved systemId + resolved = systemIds.get(systemUri.toString()); + if (resolved != null) { + InputSource is = new InputSource(resolved); + is.setPublicId(publicId); + return is; + } + if (!blockExternal) { + InputSource is = new InputSource(systemUri.toString()); + is.setPublicId(publicId); + return is; + } + } + + throw new FileNotFoundException(sm.getString("localResolver.unresolvedEntity", + name, publicId, systemId, base)); + } + + + @Override + public InputSource getExternalSubset(String name, String baseURI) + throws SAXException, IOException { + return null; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/LocalStrings.properties b/java/org/apache/tomcat/util/descriptor/LocalStrings.properties new file mode 100644 index 0000000..ff19247 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/LocalStrings.properties @@ -0,0 +1,21 @@ +# 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. + +digesterFactory.missingSchema=The XML schema [{0}] could not be found. This is very likely to break XML validation if XML validation is enabled. + +localResolver.unresolvedEntity=Could not resolve XML resource [{0}] with public ID [{1}], system ID [{2}] and base URI [{3}] to a known, local entity. + +xmlErrorHandler.error=Non-fatal error [{0}] reported processing [{1}]. +xmlErrorHandler.warning=Warning [{0}] reported processing [{1}]. diff --git a/java/org/apache/tomcat/util/descriptor/LocalStrings_es.properties b/java/org/apache/tomcat/util/descriptor/LocalStrings_es.properties new file mode 100644 index 0000000..57f49e7 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/LocalStrings_es.properties @@ -0,0 +1,17 @@ +# 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. + +xmlErrorHandler.error=Error no fatal [{0}] reportado por el proceso [{1}]. +xmlErrorHandler.warning=Aviso [{0}] reportado por el proceso [{1}]. diff --git a/java/org/apache/tomcat/util/descriptor/LocalStrings_fr.properties b/java/org/apache/tomcat/util/descriptor/LocalStrings_fr.properties new file mode 100644 index 0000000..a509093 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/LocalStrings_fr.properties @@ -0,0 +1,21 @@ +# 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. + +digesterFactory.missingSchema=Le schema XML [{0}] n''a pu être trouvé, cela empêchera certainement la validation de fonctionner si elle est activée + +localResolver.unresolvedEntity=Impossible de résoudre vers une entité locale connue la ressource XML [{0}] avec un identifiant public [{1}], un identifiant système [{2}] et un URI de base [{3}] + +xmlErrorHandler.error=L''erreur non fatale [{0}] a été retournée lors du traitement [{1}] +xmlErrorHandler.warning=L''avertissement [{0}] a été retournée en traitant [{1}] diff --git a/java/org/apache/tomcat/util/descriptor/LocalStrings_ja.properties b/java/org/apache/tomcat/util/descriptor/LocalStrings_ja.properties new file mode 100644 index 0000000..a5f345e --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/LocalStrings_ja.properties @@ -0,0 +1,21 @@ +# 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. + +digesterFactory.missingSchema=XMLスキーマ [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ã“ã‚Œã¯ã€XML検証ãŒæœ‰åŠ¹ãªå ´åˆã€XML検証を中断ã™ã‚‹å¯èƒ½æ€§ãŒéžå¸¸ã«é«˜ã„ã§ã™ã€‚ + +localResolver.unresolvedEntity=publicID [{1}]ã€system ID [{2}]ã€ãŠã‚ˆã³ãƒ™ãƒ¼ã‚¹URI [{3}] ã‚’æŒã¤XMLリソース [{0}] を既知ã®ãƒ­ãƒ¼ã‚«ãƒ«ã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ã«è§£æ±ºã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ + +xmlErrorHandler.error=処ç†[{1}]ã®é–“ã«è‡´å‘½çš„ã§ã¯ãªã„エラー[{0}]ãŒå ±å‘Šã•ã‚Œã¾ã—ãŸã€‚ +xmlErrorHandler.warning=警告[{0}]ãŒå ±å‘Šã•ã‚Œã¾ã—ãŸã€‚[{1}]を処ç†ä¸­ diff --git a/java/org/apache/tomcat/util/descriptor/LocalStrings_ko.properties b/java/org/apache/tomcat/util/descriptor/LocalStrings_ko.properties new file mode 100644 index 0000000..e723fae --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/LocalStrings_ko.properties @@ -0,0 +1,21 @@ +# 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. + +digesterFactory.missingSchema=XML 스키머 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없었습니다. XML validationì´ ì‚¬ìš© 가능 ìƒíƒœë¼ë©´, ì´ëŠ” XML validation 실패를 ì¼ìœ¼í‚¬ ê°€ëŠ¥ì„±ì´ í½ë‹ˆë‹¤. + +localResolver.unresolvedEntity=Public IDê°€ [{1}]ì´ê³  system IDê°€ [{2}]ì´ë©° base URIê°€ [{3}]ì¸ XML 리소스 [{0}]ì„(를), 알려진 로컬 엔티티로 ê²°ì •í•  수 없습니다. + +xmlErrorHandler.error=[{1}]ì„(를) 처리 중, 치명ì ì´ì§€ ì•Šì€ ì˜¤ë¥˜ [{0}]ì´(ê°€) ë³´ê³ ë˜ì—ˆìŠµë‹ˆë‹¤. +xmlErrorHandler.warning=[{1}]ì„(를) 처리 중 경고가 ë³´ê³ ë¨: [{0}] diff --git a/java/org/apache/tomcat/util/descriptor/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/descriptor/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..c7bc72e --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/LocalStrings_zh_CN.properties @@ -0,0 +1,21 @@ +# 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. + +digesterFactory.missingSchema=XML模型[{0}]未找到,如果XML校验功能开å¯äº†çš„è¯ï¼Œè¿™å¾ˆå¯èƒ½ç»ˆæ­¢XML校验 + +localResolver.unresolvedEntity=ä¸èƒ½è§£å†³XML资æº[{0}]与公共ID[{1}],系统ID[{2}]和基础URI[{3}]到一个已知,当地的实体。 + +xmlErrorHandler.error=éžè‡´å‘½é”™è¯¯[{0}]报告正在处ç†[{1}]。 +xmlErrorHandler.warning=警告[{0}]报告处ç†[{1}]。 diff --git a/java/org/apache/tomcat/util/descriptor/XmlErrorHandler.java b/java/org/apache/tomcat/util/descriptor/XmlErrorHandler.java new file mode 100644 index 0000000..631c256 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/XmlErrorHandler.java @@ -0,0 +1,75 @@ +/* + * 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.tomcat.util.descriptor; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public class XmlErrorHandler implements ErrorHandler { + + private static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + private final List errors = new ArrayList<>(); + + private final List warnings = new ArrayList<>(); + + @Override + public void error(SAXParseException exception) throws SAXException { + // Collect non-fatal errors + errors.add(exception); + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + // Re-throw fatal errors + throw exception; + } + + @Override + public void warning(SAXParseException exception) throws SAXException { + // Collect warnings + warnings.add(exception); + } + + public List getErrors() { + // Internal use only - don't worry about immutability + return errors; + } + + public List getWarnings() { + // Internal use only - don't worry about immutability + return warnings; + } + + public void logFindings(Log log, String source) { + for (SAXParseException e : getWarnings()) { + log.warn(sm.getString( + "xmlErrorHandler.warning", e.getMessage(), source)); + } + for (SAXParseException e : getErrors()) { + log.warn(sm.getString( + "xmlErrorHandler.error", e.getMessage(), source)); + } + } +} diff --git a/java/org/apache/tomcat/util/descriptor/XmlIdentifiers.java b/java/org/apache/tomcat/util/descriptor/XmlIdentifiers.java new file mode 100644 index 0000000..1f492a7 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/XmlIdentifiers.java @@ -0,0 +1,96 @@ +/* + * 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.tomcat.util.descriptor; + +/** + * Defines constants for well-known Public and System identifiers documented by + * the Servlet and JSP specifications. + */ +public final class XmlIdentifiers { + + // from W3C + public static final String XML_2001_XSD = "http://www.w3.org/2001/xml.xsd"; + public static final String DATATYPES_PUBLIC = "datatypes"; + public static final String XSD_10_PUBLIC = + "-//W3C//DTD XMLSCHEMA 200102//EN"; + + // from J2EE 1.2 + public static final String WEB_22_PUBLIC = + "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"; + public static final String WEB_22_SYSTEM = + "http://java.sun.com/dtd/web-app_2_2.dtd"; + public static final String TLD_11_PUBLIC = + "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN"; + public static final String TLD_11_SYSTEM = + "http://java.sun.com/dtd/web-jsptaglibrary_1_1.dtd"; + + // from J2EE 1.3 + public static final String WEB_23_PUBLIC = + "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"; + public static final String WEB_23_SYSTEM = + "http://java.sun.com/dtd/web-app_2_3.dtd"; + public static final String TLD_12_PUBLIC = + "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"; + public static final String TLD_12_SYSTEM = + "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd"; + + // from J2EE 1.4 + public static final String JAVAEE_1_4_NS = "http://java.sun.com/xml/ns/j2ee"; + public static final String WEB_24_XSD = JAVAEE_1_4_NS + "/web-app_2_4.xsd"; + public static final String TLD_20_XSD = JAVAEE_1_4_NS + "/web-jsptaglibrary_2_0.xsd"; + public static final String WEBSERVICES_11_XSD = + "http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd"; + + // from JavaEE 5 + public static final String JAVAEE_5_NS = "http://java.sun.com/xml/ns/javaee"; + public static final String WEB_25_XSD = JAVAEE_5_NS + "/web-app_2_5.xsd"; + public static final String TLD_21_XSD = JAVAEE_5_NS + "/web-jsptaglibrary_2_1.xsd"; + public static final String WEBSERVICES_12_XSD = JAVAEE_5_NS + "javaee_web_services_1_2.xsd"; + + // from JavaEE 6 + public static final String JAVAEE_6_NS = JAVAEE_5_NS; + public static final String WEB_30_XSD = JAVAEE_6_NS + "/web-app_3_0.xsd"; + public static final String WEB_FRAGMENT_30_XSD = JAVAEE_6_NS + "/web-fragment_3_0.xsd"; + public static final String WEBSERVICES_13_XSD = JAVAEE_6_NS + "/javaee_web_services_1_3.xsd"; + + // from JavaEE 7 + public static final String JAVAEE_7_NS = "http://xmlns.jcp.org/xml/ns/javaee"; + public static final String WEB_31_XSD = JAVAEE_7_NS + "/web-app_3_1.xsd"; + public static final String WEB_FRAGMENT_31_XSD = JAVAEE_7_NS + "/web-fragment_3_1.xsd"; + public static final String WEBSERVICES_14_XSD = JAVAEE_7_NS + "/javaee_web_services_1_4.xsd"; + + // from JavaEE 8 + public static final String JAVAEE_8_NS = JAVAEE_7_NS; + public static final String WEB_40_XSD = JAVAEE_8_NS + "/web-app_4_0.xsd"; + public static final String WEB_FRAGMENT_40_XSD = JAVAEE_8_NS + "/web-fragment_4_0.xsd"; + + // from Jakarta EE 9 + public static final String JAKARTAEE_9_NS = "https://jakarta.ee/xml/ns/jakartaee"; + public static final String WEB_50_XSD = JAKARTAEE_9_NS + "/web-app_5_0.xsd"; + public static final String WEB_FRAGMENT_50_XSD = JAKARTAEE_9_NS + "/web-fragment_5_0.xsd"; + public static final String TLD_30_XSD = JAKARTAEE_9_NS + "/web-jsptaglibrary_3_0.xsd"; + public static final String WEBSERVICES_20_XSD = JAKARTAEE_9_NS + "/jakartaee_web_services_2_0.xsd"; + + // from Jakarta EE 10 + public static final String JAKARTAEE_10_NS = JAKARTAEE_9_NS; + public static final String WEB_60_XSD = JAKARTAEE_10_NS + "/web-app_6_0.xsd"; + public static final String WEB_FRAGMENT_60_XSD = JAKARTAEE_10_NS + "/web-fragment_6_0.xsd"; + public static final String TLD_31_XSD = JAKARTAEE_10_NS + "/web-jsptaglibrary_3_1.xsd"; + + private XmlIdentifiers() { + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/descriptor/tagplugin/TagPluginParser.java b/java/org/apache/tomcat/util/descriptor/tagplugin/TagPluginParser.java new file mode 100644 index 0000000..d2d75c6 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tagplugin/TagPluginParser.java @@ -0,0 +1,89 @@ +/* + * 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.tomcat.util.descriptor.tagplugin; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.ServletContext; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.descriptor.DigesterFactory; +import org.apache.tomcat.util.descriptor.XmlErrorHandler; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Parser for Tag Plugin descriptors. + */ +public class TagPluginParser { + private final Log log = LogFactory.getLog(TagPluginParser.class); // must not be static + private static final String PREFIX = "tag-plugins/tag-plugin"; + private final Digester digester; + private final Map plugins = new HashMap<>(); + + public TagPluginParser(ServletContext context, boolean blockExternal) { + digester = DigesterFactory.newDigester( + false, false, new TagPluginRuleSet(), blockExternal); + digester.setClassLoader(context.getClassLoader()); + } + + public void parse(URL url) throws IOException, SAXException { + try (InputStream is = url.openStream()) { + XmlErrorHandler handler = new XmlErrorHandler(); + digester.setErrorHandler(handler); + + digester.push(this); + + InputSource source = new InputSource(url.toExternalForm()); + source.setByteStream(is); + digester.parse(source); + if (!handler.getWarnings().isEmpty() || !handler.getErrors().isEmpty()) { + handler.logFindings(log, source.getSystemId()); + if (!handler.getErrors().isEmpty()) { + // throw the first to indicate there was an error during processing + throw handler.getErrors().iterator().next(); + } + } + } finally { + digester.reset(); + } + } + + public void addPlugin(String tagClass, String pluginClass) { + plugins.put(tagClass, pluginClass); + } + + public Map getPlugins() { + return plugins; + } + + private static class TagPluginRuleSet implements RuleSet { + @Override + public void addRuleInstances(Digester digester) { + digester.addCallMethod(PREFIX, "addPlugin", 2); + digester.addCallParam(PREFIX + "/tag-class", 0); + digester.addCallParam(PREFIX + "/plugin-class", 1); + } + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/ImplicitTldRuleSet.java b/java/org/apache/tomcat/util/descriptor/tld/ImplicitTldRuleSet.java new file mode 100644 index 0000000..9847be5 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/ImplicitTldRuleSet.java @@ -0,0 +1,86 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.Rule; +import org.apache.tomcat.util.digester.RuleSet; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.Attributes; + +/** + * RulesSet for digesting implicit.tld files. + * + * Only version information used and short names are allowed. + */ +public class ImplicitTldRuleSet implements RuleSet { + + private static final StringManager sm = StringManager.getManager(ImplicitTldRuleSet.class); + + private static final String PREFIX = "taglib"; + private static final String VALIDATOR_PREFIX = PREFIX + "/validator"; + private static final String TAG_PREFIX = PREFIX + "/tag"; + private static final String TAGFILE_PREFIX = PREFIX + "/tag-file"; + private static final String FUNCTION_PREFIX = PREFIX + "/function"; + + + @Override + public void addRuleInstances(Digester digester) { + + digester.addCallMethod(PREFIX + "/tlibversion", "setTlibVersion", 0); + digester.addCallMethod(PREFIX + "/tlib-version", "setTlibVersion", 0); + digester.addCallMethod(PREFIX + "/jspversion", "setJspVersion", 0); + digester.addCallMethod(PREFIX + "/jsp-version", "setJspVersion", 0); + digester.addRule(PREFIX, new Rule() { + // for TLD 2.0 and later, jsp-version is set by version attribute + @Override + public void begin(String namespace, String name, Attributes attributes) { + TaglibXml taglibXml = (TaglibXml) digester.peek(); + taglibXml.setJspVersion(attributes.getValue("version")); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(taglibXml)).append(".setJspVersion(\""); + code.append(attributes.getValue("version")).append("\");"); + code.append(System.lineSeparator()); + } + } + }); + digester.addCallMethod(PREFIX + "/shortname", "setShortName", 0); + digester.addCallMethod(PREFIX + "/short-name", "setShortName", 0); + + // Elements not permitted + digester.addRule(PREFIX + "/uri", new ElementNotAllowedRule()); + digester.addRule(PREFIX + "/info", new ElementNotAllowedRule()); + digester.addRule(PREFIX + "/description", new ElementNotAllowedRule()); + digester.addRule(PREFIX + "/listener/listener-class", new ElementNotAllowedRule()); + + digester.addRule(VALIDATOR_PREFIX, new ElementNotAllowedRule()); + digester.addRule(TAG_PREFIX, new ElementNotAllowedRule()); + digester.addRule(TAGFILE_PREFIX, new ElementNotAllowedRule()); + digester.addRule(FUNCTION_PREFIX, new ElementNotAllowedRule()); + } + + + private static class ElementNotAllowedRule extends Rule { + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + throw new IllegalArgumentException( + ImplicitTldRuleSet.sm.getString("implicitTldRule.elementNotAllowed", name)); + } + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/LocalStrings.properties b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings.properties new file mode 100644 index 0000000..36705cc --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings.properties @@ -0,0 +1,16 @@ +# 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. + +implicitTldRule.elementNotAllowed=The element [{0}] is not permitted in an implicit.tld file diff --git a/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_fr.properties b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_fr.properties new file mode 100644 index 0000000..84dcf74 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_fr.properties @@ -0,0 +1,16 @@ +# 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. + +implicitTldRule.elementNotAllowed=L''élément [{0}] n''est pas autorisé dans un fichier TLD implicite diff --git a/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_ja.properties b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_ja.properties new file mode 100644 index 0000000..51c3762 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_ja.properties @@ -0,0 +1,16 @@ +# 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. + +implicitTldRule.elementNotAllowed=è¦ç´ [{0}]ã¯ã€implicit.tldファイルã§ã¯è¨±å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“。 diff --git a/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_ko.properties b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_ko.properties new file mode 100644 index 0000000..c835c97 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_ko.properties @@ -0,0 +1,16 @@ +# 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. + +implicitTldRule.elementNotAllowed=implicit.tld íŒŒì¼ ë‚´ì—ì„œ, 엘리먼트 [{0}]ì€(는) 허용ë˜ì§€ 않습니다. diff --git a/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..7999008 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/LocalStrings_zh_CN.properties @@ -0,0 +1,16 @@ +# 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. + +implicitTldRule.elementNotAllowed=éšå¼.tld文件中ä¸å…许元素[{0}] diff --git a/java/org/apache/tomcat/util/descriptor/tld/TagFileXml.java b/java/org/apache/tomcat/util/descriptor/tld/TagFileXml.java new file mode 100644 index 0000000..9cbf984 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/TagFileXml.java @@ -0,0 +1,79 @@ +/* + * 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.tomcat.util.descriptor.tld; + +/** + * Bare-bone model of a tag file loaded from a TLD. + * This does not contain the tag-specific attributes that requiring parsing + * the actual tag file to derive. + */ +public class TagFileXml { + private String name; + private String path; + private String displayName; + private String smallIcon; + private String largeIcon; + private String info; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getSmallIcon() { + return smallIcon; + } + + public void setSmallIcon(String smallIcon) { + this.smallIcon = smallIcon; + } + + public String getLargeIcon() { + return largeIcon; + } + + public void setLargeIcon(String largeIcon) { + this.largeIcon = largeIcon; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/TagXml.java b/java/org/apache/tomcat/util/descriptor/tld/TagXml.java new file mode 100644 index 0000000..60aa99d --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/TagXml.java @@ -0,0 +1,124 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagInfo; +import jakarta.servlet.jsp.tagext.TagVariableInfo; + +/** + * Model of a tag define in a tag library descriptor. + * This represents the information as parsed from the XML but differs from + * TagInfo in that is does not provide a link back to the tag library that + * defined it. + */ +public class TagXml { + private String name; + private String tagClass; + private String teiClass; + private String bodyContent = TagInfo.BODY_CONTENT_JSP; + private String displayName; + private String smallIcon; + private String largeIcon; + private String info; + private boolean dynamicAttributes; + private final List attributes = new ArrayList<>(); + private final List variables = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTagClass() { + return tagClass; + } + + public void setTagClass(String tagClass) { + this.tagClass = tagClass; + } + + public String getTeiClass() { + return teiClass; + } + + public void setTeiClass(String teiClass) { + this.teiClass = teiClass; + } + + public String getBodyContent() { + return bodyContent; + } + + public void setBodyContent(String bodyContent) { + this.bodyContent = bodyContent; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getSmallIcon() { + return smallIcon; + } + + public void setSmallIcon(String smallIcon) { + this.smallIcon = smallIcon; + } + + public String getLargeIcon() { + return largeIcon; + } + + public void setLargeIcon(String largeIcon) { + this.largeIcon = largeIcon; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } + + public boolean hasDynamicAttributes() { + return dynamicAttributes; + } + + public void setDynamicAttributes(boolean dynamicAttributes) { + this.dynamicAttributes = dynamicAttributes; + } + + public List getAttributes() { + return attributes; + } + + public List getVariables() { + return variables; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/TaglibXml.java b/java/org/apache/tomcat/util/descriptor/tld/TaglibXml.java new file mode 100644 index 0000000..2a45c51 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/TaglibXml.java @@ -0,0 +1,124 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.jsp.tagext.FunctionInfo; + +/** + * Common representation of a Tag Library Descriptor (TLD) XML file. + *

    + * This stores the raw result of parsing an TLD XML file, flattening different + * version of the descriptors to a common format. This is different to a + * TagLibraryInfo instance that would be passed to a tag validator in that it + * does not contain the uri and prefix values used by a JSP to reference this + * tag library. + */ +public class TaglibXml { + private String tlibVersion; + private String jspVersion; + private String shortName; + private String uri; + private String info; + private ValidatorXml validator; + private final List tags = new ArrayList<>(); + private final List tagFiles = new ArrayList<>(); + private final List listeners = new ArrayList<>(); + private final List functions = new ArrayList<>(); + + public String getTlibVersion() { + return tlibVersion; + } + + public void setTlibVersion(String tlibVersion) { + this.tlibVersion = tlibVersion; + } + + public String getJspVersion() { + return jspVersion; + } + + public void setJspVersion(String jspVersion) { + this.jspVersion = jspVersion; + } + + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getInfo() { + return info; + } + + public void setInfo(String info) { + this.info = info; + } + + public ValidatorXml getValidator() { + return validator; + } + + public void setValidator(ValidatorXml validator) { + this.validator = validator; + } + + public void addTag(TagXml tag) { + tags.add(tag); + } + + public List getTags() { + return tags; + } + + public void addTagFile(TagFileXml tag) { + tagFiles.add(tag); + } + + public List getTagFiles() { + return tagFiles; + } + + public void addListener(String listener) { + listeners.add(listener); + } + + public List getListeners() { + return listeners; + } + + public void addFunction(String name, String klass, String signature) { + functions.add(new FunctionInfo(name, klass, signature)); + } + + public List getFunctions() { + return functions; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/TldParser.java b/java/org/apache/tomcat/util/descriptor/tld/TldParser.java new file mode 100644 index 0000000..933a13c --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/TldParser.java @@ -0,0 +1,100 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import java.io.IOException; +import java.io.InputStream; +import java.security.AccessController; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.descriptor.Constants; +import org.apache.tomcat.util.descriptor.DigesterFactory; +import org.apache.tomcat.util.descriptor.XmlErrorHandler; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSet; +import org.apache.tomcat.util.security.PrivilegedGetTccl; +import org.apache.tomcat.util.security.PrivilegedSetTccl; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Parses a Tag Library Descriptor. + */ +public class TldParser { + private final Log log = LogFactory.getLog(TldParser.class); // must not be static + private final Digester digester; + + public TldParser(boolean namespaceAware, boolean validation, + boolean blockExternal) { + this(namespaceAware, validation, new TldRuleSet(), blockExternal); + } + + public TldParser(boolean namespaceAware, boolean validation, RuleSet ruleSet, + boolean blockExternal) { + digester = DigesterFactory.newDigester( + validation, namespaceAware, ruleSet, blockExternal); + } + + public TaglibXml parse(TldResourcePath path) throws IOException, SAXException { + ClassLoader original; + Thread currentThread = Thread.currentThread(); + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedGetTccl pa = new PrivilegedGetTccl(currentThread); + original = AccessController.doPrivileged(pa); + } else { + original = currentThread.getContextClassLoader(); + } + try (InputStream is = path.openStream()) { + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, TldParser.class.getClassLoader()); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(TldParser.class.getClassLoader()); + } + XmlErrorHandler handler = new XmlErrorHandler(); + digester.setErrorHandler(handler); + + TaglibXml taglibXml = new TaglibXml(); + digester.push(taglibXml); + + InputSource source = new InputSource(path.toExternalForm()); + source.setByteStream(is); + digester.parse(source); + if (!handler.getWarnings().isEmpty() || !handler.getErrors().isEmpty()) { + handler.logFindings(log, source.getSystemId()); + if (!handler.getErrors().isEmpty()) { + // throw the first to indicate there was an error during processing + throw handler.getErrors().iterator().next(); + } + } + return taglibXml; + } finally { + digester.reset(); + if (Constants.IS_SECURITY_ENABLED) { + PrivilegedSetTccl pa = new PrivilegedSetTccl(currentThread, original); + AccessController.doPrivileged(pa); + } else { + currentThread.setContextClassLoader(original); + } + } + } + + public void setClassLoader(ClassLoader classLoader) { + digester.setClassLoader(classLoader); + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/TldResourcePath.java b/java/org/apache/tomcat/util/descriptor/tld/TldResourcePath.java new file mode 100644 index 0000000..417a4a5 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/TldResourcePath.java @@ -0,0 +1,171 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Objects; + +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.scan.JarFactory; +import org.apache.tomcat.util.scan.ReferenceCountedJar; + +/** + * A TLD Resource Path as defined in JSP 7.3.2. + *

    + * This encapsulates references to Tag Library Descriptors that can be located + * in different places: + *

      + *
    • As resources within an application
    • + *
    • As entries in JAR files included in the application
    • + *
    • As resources provided by the container
    • + *
    + * When configuring a mapping from a well-known URI to a TLD, a user is allowed + * to specify just the name of a JAR file that implicitly contains a TLD in + * META-INF/taglib.tld. Such a mapping must be explicitly converted + * to a URL and entryName when using this implementation. + */ +public class TldResourcePath { + private final URL url; + private final String webappPath; + private final String entryName; + + /** + * Constructor identifying a TLD resource directly. + * + * @param url the location of the TLD + * @param webappPath the web application path, if any, of the TLD + */ + public TldResourcePath(URL url, String webappPath) { + this(url, webappPath, null); + } + + /** + * Constructor identifying a TLD packaged within a JAR file. + * + * @param url the location of the JAR + * @param webappPath the web application path, if any, of the JAR + * @param entryName the name of the entry in the JAR + */ + public TldResourcePath(URL url, String webappPath, String entryName) { + this.url = url; + this.webappPath = webappPath; + this.entryName = entryName; + } + + /** + * Returns the URL of the TLD or of the JAR containing the TLD. + * + * @return the URL of the TLD + */ + public URL getUrl() { + return url; + } + + /** + * Returns the path within the web application, if any, that the resource + * returned by {@link #getUrl()} was obtained from. + * + * @return the web application path or @null if the the resource is not + * located within a web application + */ + public String getWebappPath() { + return webappPath; + } + + /** + * Returns the name of the JAR entry that contains the TLD. + * May be null to indicate the URL refers directly to the TLD itself. + * + * @return the name of the JAR entry that contains the TLD + */ + public String getEntryName() { + return entryName; + } + + /** + * Return the external form of the URL representing this TLD. + * This can be used as a canonical location for the TLD itself, for example, + * as the systemId to use when parsing its XML. + * + * @return the external form of the URL representing this TLD + */ + public String toExternalForm() { + if (entryName == null) { + return url.toExternalForm(); + } else { + return "jar:" + url.toExternalForm() + "!/" + entryName; + } + } + + /** + * Opens a stream to access the TLD. + * + * @return a stream containing the TLD content + * @throws IOException if there was a problem opening the stream + */ + public InputStream openStream() throws IOException { + if (entryName == null) { + return url.openStream(); + } else { + URL entryUrl = JarFactory.getJarEntryURL(url, entryName); + return entryUrl.openStream(); + } + } + + public Jar openJar() throws IOException { + if (entryName == null) { + return null; + } else { + // Bug 62976 + // Jar files containing tags are typically opened during initial + // compilation and then closed when compilation is complete. The + // reference counting wrapper is used because, when background + // compilation is enabled, the Jar will need to be accessed (to + // check for modifications) after it has been closed at the end + // of the compilation stage. + // Using a reference counted Jar enables the Jar to be re-opened, + // used and then closed again rather than triggering an ISE. + return new ReferenceCountedJar(url); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TldResourcePath other = (TldResourcePath) o; + + return url.equals(other.url) && + Objects.equals(webappPath, other.webappPath) && + Objects.equals(entryName, other.entryName); + } + + @Override + public int hashCode() { + int result = url.hashCode(); + result = result * 31 + Objects.hashCode(webappPath); + result = result * 31 + Objects.hashCode(entryName); + return result; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/TldRuleSet.java b/java/org/apache/tomcat/util/descriptor/tld/TldRuleSet.java new file mode 100644 index 0000000..0fb39b3 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/TldRuleSet.java @@ -0,0 +1,409 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import java.lang.reflect.Method; + +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagVariableInfo; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.Rule; +import org.apache.tomcat.util.digester.RuleSet; +import org.xml.sax.Attributes; + +/** + * RulesSet for digesting TLD files. + */ +public class TldRuleSet implements RuleSet { + private static final String PREFIX = "taglib"; + private static final String VALIDATOR_PREFIX = PREFIX + "/validator"; + private static final String TAG_PREFIX = PREFIX + "/tag"; + private static final String TAGFILE_PREFIX = PREFIX + "/tag-file"; + private static final String FUNCTION_PREFIX = PREFIX + "/function"; + + @Override + public void addRuleInstances(Digester digester) { + + digester.addCallMethod(PREFIX + "/tlibversion", "setTlibVersion", 0); + digester.addCallMethod(PREFIX + "/tlib-version", "setTlibVersion", 0); + digester.addCallMethod(PREFIX + "/jspversion", "setJspVersion", 0); + digester.addCallMethod(PREFIX + "/jsp-version", "setJspVersion", 0); + digester.addRule(PREFIX, new Rule() { + // for TLD 2.0 and later, jsp-version is set by version attribute + @Override + public void begin(String namespace, String name, Attributes attributes) { + TaglibXml taglibXml = (TaglibXml) digester.peek(); + taglibXml.setJspVersion(attributes.getValue("version")); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(taglibXml)).append(".setJspVersion(\""); + code.append(attributes.getValue("version")).append("\");"); + code.append(System.lineSeparator()); + } + } + }); + digester.addCallMethod(PREFIX + "/shortname", "setShortName", 0); + digester.addCallMethod(PREFIX + "/short-name", "setShortName", 0); + + // common rules + digester.addCallMethod(PREFIX + "/uri", "setUri", 0); + digester.addCallMethod(PREFIX + "/info", "setInfo", 0); + digester.addCallMethod(PREFIX + "/description", "setInfo", 0); + digester.addCallMethod(PREFIX + "/listener/listener-class", "addListener", 0); + + // validator + digester.addObjectCreate(VALIDATOR_PREFIX, ValidatorXml.class.getName()); + digester.addCallMethod(VALIDATOR_PREFIX + "/validator-class", "setValidatorClass", 0); + digester.addCallMethod(VALIDATOR_PREFIX + "/init-param", "addInitParam", 2); + digester.addCallParam(VALIDATOR_PREFIX + "/init-param/param-name", 0); + digester.addCallParam(VALIDATOR_PREFIX + "/init-param/param-value", 1); + digester.addSetNext(VALIDATOR_PREFIX, "setValidator", ValidatorXml.class.getName()); + + + // tag + digester.addObjectCreate(TAG_PREFIX, TagXml.class.getName()); + addDescriptionGroup(digester, TAG_PREFIX); + digester.addCallMethod(TAG_PREFIX + "/name", "setName", 0); + digester.addCallMethod(TAG_PREFIX + "/tagclass", "setTagClass", 0); + digester.addCallMethod(TAG_PREFIX + "/tag-class", "setTagClass", 0); + digester.addCallMethod(TAG_PREFIX + "/teiclass", "setTeiClass", 0); + digester.addCallMethod(TAG_PREFIX + "/tei-class", "setTeiClass", 0); + digester.addCallMethod(TAG_PREFIX + "/bodycontent", "setBodyContent", 0); + digester.addCallMethod(TAG_PREFIX + "/body-content", "setBodyContent", 0); + + digester.addRule(TAG_PREFIX + "/variable", new ScriptVariableRule()); + digester.addCallMethod(TAG_PREFIX + "/variable/name-given", "setNameGiven", 0); + digester.addCallMethod(TAG_PREFIX + "/variable/name-from-attribute", + "setNameFromAttribute", 0); + digester.addCallMethod(TAG_PREFIX + "/variable/variable-class", "setClassName", 0); + digester.addRule(TAG_PREFIX + "/variable/declare", + new GenericBooleanRule(Variable.class, "setDeclare")); + digester.addCallMethod(TAG_PREFIX + "/variable/scope", "setScope", 0); + + digester.addRule(TAG_PREFIX + "/attribute", new TagAttributeRule()); + digester.addCallMethod(TAG_PREFIX + "/attribute/description", "setDescription", 0); + digester.addCallMethod(TAG_PREFIX + "/attribute/name", "setName", 0); + digester.addRule(TAG_PREFIX + "/attribute/required", + new GenericBooleanRule(Attribute.class, "setRequired")); + digester.addRule(TAG_PREFIX + "/attribute/rtexprvalue", + new GenericBooleanRule(Attribute.class, "setRequestTime")); + digester.addCallMethod(TAG_PREFIX + "/attribute/type", "setType", 0); + digester.addCallMethod(TAG_PREFIX + "/attribute/deferred-value", "setDeferredValue"); + digester.addCallMethod(TAG_PREFIX + "/attribute/deferred-value/type", + "setExpectedTypeName", 0); + digester.addCallMethod(TAG_PREFIX + "/attribute/deferred-method", "setDeferredMethod"); + digester.addCallMethod(TAG_PREFIX + "/attribute/deferred-method/method-signature", + "setMethodSignature", 0); + digester.addRule(TAG_PREFIX + "/attribute/fragment", + new GenericBooleanRule(Attribute.class, "setFragment")); + + digester.addRule(TAG_PREFIX + "/dynamic-attributes", + new GenericBooleanRule(TagXml.class, "setDynamicAttributes")); + digester.addSetNext(TAG_PREFIX, "addTag", TagXml.class.getName()); + + // tag-file + digester.addObjectCreate(TAGFILE_PREFIX, TagFileXml.class.getName()); + addDescriptionGroup(digester, TAGFILE_PREFIX); + digester.addCallMethod(TAGFILE_PREFIX + "/name", "setName", 0); + digester.addCallMethod(TAGFILE_PREFIX + "/path", "setPath", 0); + digester.addSetNext(TAGFILE_PREFIX, "addTagFile", TagFileXml.class.getName()); + + // function + digester.addCallMethod(FUNCTION_PREFIX, "addFunction", 3); + digester.addCallParam(FUNCTION_PREFIX + "/name", 0); + digester.addCallParam(FUNCTION_PREFIX + "/function-class", 1); + digester.addCallParam(FUNCTION_PREFIX + "/function-signature", 2); + } + + private void addDescriptionGroup(Digester digester, String prefix) { + digester.addCallMethod(prefix + "/info", "setInfo", 0); + digester.addCallMethod(prefix + "small-icon", "setSmallIcon", 0); + digester.addCallMethod(prefix + "large-icon", "setLargeIcon", 0); + + digester.addCallMethod(prefix + "/description", "setInfo", 0); + digester.addCallMethod(prefix + "/display-name", "setDisplayName", 0); + digester.addCallMethod(prefix + "/icon/small-icon", "setSmallIcon", 0); + digester.addCallMethod(prefix + "/icon/large-icon", "setLargeIcon", 0); + } + + private static class TagAttributeRule extends Rule { + private boolean allowShortNames = false; + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + TaglibXml taglibXml = (TaglibXml) digester.peek(digester.getCount() - 1); + allowShortNames = "1.2".equals(taglibXml.getJspVersion()); + Attribute attribute = new Attribute(allowShortNames); + digester.push(attribute); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(TldRuleSet.class.getName()).append(".Attribute ").append(digester.toVariableName(attribute)).append(" = new "); + code.append(TldRuleSet.class.getName()).append(".Attribute").append('(').append(Boolean.toString(allowShortNames)); + code.append(");").append(System.lineSeparator()); + } + } + + @Override + public void end(String namespace, String name) throws Exception { + Attribute attribute = (Attribute) digester.pop(); + TagXml tag = (TagXml) digester.peek(); + tag.getAttributes().add(attribute.toTagAttributeInfo()); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(tag)).append(".getAttributes().add("); + code.append(digester.toVariableName(attribute)).append(".toTagAttributeInfo());"); + code.append(System.lineSeparator()); + } + } + } + + public static class Attribute { + private final boolean allowShortNames; + private String name; + private boolean required; + private String type; + private boolean requestTime; + private boolean fragment; + private String description; + private boolean deferredValue; + private boolean deferredMethod; + private String expectedTypeName; + private String methodSignature; + + private Attribute(boolean allowShortNames) { + this.allowShortNames = allowShortNames; + } + + public void setName(String name) { + this.name = name; + } + + public void setRequired(boolean required) { + this.required = required; + } + + public void setType(String type) { + if (allowShortNames) { + switch (type) { + case "Boolean": + this.type = "java.lang.Boolean"; + break; + case "Character": + this.type = "java.lang.Character"; + break; + case "Byte": + this.type = "java.lang.Byte"; + break; + case "Short": + this.type = "java.lang.Short"; + break; + case "Integer": + this.type = "java.lang.Integer"; + break; + case "Long": + this.type = "java.lang.Long"; + break; + case "Float": + this.type = "java.lang.Float"; + break; + case "Double": + this.type = "java.lang.Double"; + break; + case "String": + this.type = "java.lang.String"; + break; + case "Object": + this.type = "java.lang.Object"; + break; + default: + this.type = type; + break; + } + } else { + this.type = type; + } + } + + public void setRequestTime(boolean requestTime) { + this.requestTime = requestTime; + } + + public void setFragment(boolean fragment) { + this.fragment = fragment; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setDeferredValue() { + this.deferredValue = true; + } + + public void setDeferredMethod() { + this.deferredMethod = true; + } + + public void setExpectedTypeName(String expectedTypeName) { + this.expectedTypeName = expectedTypeName; + } + + public void setMethodSignature(String methodSignature) { + this.methodSignature = methodSignature; + } + + public TagAttributeInfo toTagAttributeInfo() { + if (fragment) { + // JSP8.5.2: for a fragment type is fixed and rexprvalue is true + type = "jakarta.servlet.jsp.tagext.JspFragment"; + requestTime = true; + } else if (deferredValue) { + type = "jakarta.el.ValueExpression"; + if (expectedTypeName == null) { + expectedTypeName = "java.lang.Object"; + } + } else if (deferredMethod) { + type = "jakarta.el.MethodExpression"; + if (methodSignature == null) { + methodSignature = "java.lang.Object method()"; + } + } + + // According to JSP spec, for static values (those determined at + // translation time) the type is fixed at java.lang.String. + if (!requestTime && type == null) { + type = "java.lang.String"; + } + + return new TagAttributeInfo( + name, + required, + type, + requestTime, + fragment, + description, + deferredValue, + deferredMethod, + expectedTypeName, + methodSignature); + } + } + + private static class ScriptVariableRule extends Rule { + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + Variable variable = new Variable(); + digester.push(variable); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(TldRuleSet.class.getName()).append(".Variable ").append(digester.toVariableName(variable)).append(" = new "); + code.append(TldRuleSet.class.getName()).append(".Variable").append("();").append(System.lineSeparator()); + } + } + + @Override + public void end(String namespace, String name) throws Exception { + Variable variable = (Variable) digester.pop(); + TagXml tag = (TagXml) digester.peek(); + tag.getVariables().add(variable.toTagVariableInfo()); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(tag)).append(".getVariables().add("); + code.append(digester.toVariableName(variable)).append(".toTagVariableInfo());"); + code.append(System.lineSeparator()); + } + } + } + + public static class Variable { + private String nameGiven; + private String nameFromAttribute; + private String className = "java.lang.String"; + private boolean declare = true; + private int scope = VariableInfo.NESTED; + + public void setNameGiven(String nameGiven) { + this.nameGiven = nameGiven; + } + + public void setNameFromAttribute(String nameFromAttribute) { + this.nameFromAttribute = nameFromAttribute; + } + + public void setClassName(String className) { + this.className = className; + } + + public void setDeclare(boolean declare) { + this.declare = declare; + } + + public void setScope(String scopeName) { + switch (scopeName) { + case "NESTED": + scope = VariableInfo.NESTED; + break; + case "AT_BEGIN": + scope = VariableInfo.AT_BEGIN; + break; + case "AT_END": + scope = VariableInfo.AT_END; + break; + } + } + + public TagVariableInfo toTagVariableInfo() { + return new TagVariableInfo(nameGiven, nameFromAttribute, className, declare, scope); + } + } + + private static class GenericBooleanRule extends Rule { + private final Method setter; + + private GenericBooleanRule(Class type, String setterName) { + try { + this.setter = type.getMethod(setterName, Boolean.TYPE); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public void body(String namespace, String name, String text) throws Exception { + if(null != text) { + text = text.trim(); + } + boolean value = "true".equalsIgnoreCase(text) || "yes".equalsIgnoreCase(text); + setter.invoke(digester.peek(), Boolean.valueOf(value)); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(digester.peek())).append('.').append(setter.getName()); + code.append('(').append(Boolean.valueOf(value)).append(");"); + code.append(System.lineSeparator()); + } + } + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/ValidatorXml.java b/java/org/apache/tomcat/util/descriptor/tld/ValidatorXml.java new file mode 100644 index 0000000..40534ca --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/ValidatorXml.java @@ -0,0 +1,44 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import java.util.HashMap; +import java.util.Map; + +/** + * Model of a Tag Library Validator from the XML descriptor. + */ +public class ValidatorXml { + private String validatorClass; + private final Map initParams = new HashMap<>(); + + public String getValidatorClass() { + return validatorClass; + } + + public void setValidatorClass(String validatorClass) { + this.validatorClass = validatorClass; + } + + public void addInitParam(String name, String value) { + initParams.put(name, value); + } + + public Map getInitParams() { + return initParams; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/tld/package-info.java b/java/org/apache/tomcat/util/descriptor/tld/package-info.java new file mode 100644 index 0000000..9b85ae6 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/tld/package-info.java @@ -0,0 +1,21 @@ +/* + * 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 containing a Java model of the XML for a Tag Library Descriptor. + */ +package org.apache.tomcat.util.descriptor.tld; \ No newline at end of file diff --git a/java/org/apache/tomcat/util/descriptor/web/ApplicationParameter.java b/java/org/apache/tomcat/util/descriptor/web/ApplicationParameter.java new file mode 100644 index 0000000..4dd2804 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ApplicationParameter.java @@ -0,0 +1,120 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; + + +/** + * Representation of a context initialization parameter that is configured + * in the server configuration file, rather than the application deployment + * descriptor. This is convenient for establishing default values (which + * may be configured to allow application overrides or not) without having + * to modify the application deployment descriptor itself. + * + * @author Craig R. McClanahan + */ +public class ApplicationParameter implements Serializable { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + + /** + * The description of this environment entry. + */ + private String description = null; + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + + /** + * The name of this application parameter. + */ + private String name = null; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + + /** + * Does this application parameter allow overrides by the application + * deployment descriptor? + */ + private boolean override = true; + + public boolean getOverride() { + return this.override; + } + + public void setOverride(boolean override) { + this.override = override; + } + + + /** + * The value of this application parameter. + */ + private String value = null; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ApplicationParameter["); + sb.append("name="); + sb.append(name); + if (description != null) { + sb.append(", description="); + sb.append(description); + } + sb.append(", value="); + sb.append(value); + sb.append(", override="); + sb.append(override); + sb.append(']'); + return sb.toString(); + + } + + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/Constants.java b/java/org/apache/tomcat/util/descriptor/web/Constants.java new file mode 100644 index 0000000..fabf7b4 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/Constants.java @@ -0,0 +1,41 @@ +/* + * 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.tomcat.util.descriptor.web; + +public class Constants { + + public static final String PACKAGE_NAME = + Constants.class.getPackage().getName(); + + public static final String WEB_XML_LOCATION = "/WEB-INF/web.xml"; + + // -------------------------------------------------- Cookie attribute names + public static final String COOKIE_COMMENT_ATTR = "Comment"; + public static final String COOKIE_DOMAIN_ATTR = "Domain"; + public static final String COOKIE_MAX_AGE_ATTR = "Max-Age"; + public static final String COOKIE_PATH_ATTR = "Path"; + public static final String COOKIE_SECURE_ATTR = "Secure"; + public static final String COOKIE_HTTP_ONLY_ATTR = "HttpOnly"; + public static final String COOKIE_SAME_SITE_ATTR = "SameSite"; + /** + * The name of the attribute used to indicate a partitioned cookie as part of + * CHIPS. This cookie attribute is not + * defined by an RFC and may change in a non-backwards compatible way once equivalent functionality is included in + * an RFC. + */ + public static final String COOKIE_PARTITIONED_ATTR = "Partitioned"; +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextEjb.java b/java/org/apache/tomcat/util/descriptor/web/ContextEjb.java new file mode 100644 index 0000000..00964cf --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextEjb.java @@ -0,0 +1,161 @@ +/* + * 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.tomcat.util.descriptor.web; + + + +/** + * Representation of an EJB resource reference for a web application, as + * represented in a <ejb-ref> element in the + * deployment descriptor. + * + * @author Craig R. McClanahan + * @author Peter Rossbach (pero@apache.org) + */ +public class ContextEjb extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + + /** + * The name of the EJB home implementation class. + */ + private String home = null; + + public String getHome() { + return this.home; + } + + public void setHome(String home) { + this.home = home; + } + + + /** + * The link to a Jakarta EE EJB definition. + */ + private String link = null; + + public String getLink() { + return this.link; + } + + public void setLink(String link) { + this.link = link; + } + + /** + * The name of the EJB remote implementation class. + */ + private String remote = null; + + public String getRemote() { + return this.remote; + } + + public void setRemote(String remote) { + this.remote = remote; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ContextEjb["); + sb.append("name="); + sb.append(getName()); + if (getDescription() != null) { + sb.append(", description="); + sb.append(getDescription()); + } + if (getType() != null) { + sb.append(", type="); + sb.append(getType()); + } + if (home != null) { + sb.append(", home="); + sb.append(home); + } + if (remote != null) { + sb.append(", remote="); + sb.append(remote); + } + if (link != null) { + sb.append(", link="); + sb.append(link); + } + sb.append(']'); + return sb.toString(); + + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((home == null) ? 0 : home.hashCode()); + result = prime * result + ((link == null) ? 0 : link.hashCode()); + result = prime * result + ((remote == null) ? 0 : remote.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ContextEjb other = (ContextEjb) obj; + if (home == null) { + if (other.home != null) { + return false; + } + } else if (!home.equals(other.home)) { + return false; + } + if (link == null) { + if (other.link != null) { + return false; + } + } else if (!link.equals(other.link)) { + return false; + } + if (remote == null) { + if (other.remote != null) { + return false; + } + } else if (!remote.equals(other.remote)) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextEnvironment.java b/java/org/apache/tomcat/util/descriptor/web/ContextEnvironment.java new file mode 100644 index 0000000..3cbd075 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextEnvironment.java @@ -0,0 +1,130 @@ +/* + * 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.tomcat.util.descriptor.web; + + + +/** + * Representation of an application environment entry, as represented in + * an <env-entry> element in the deployment descriptor. + * + * @author Craig R. McClanahan + */ +public class ContextEnvironment extends ResourceBase { + + private static final long serialVersionUID = 1L; + + + // ------------------------------------------------------------- Properties + + + /** + * Does this environment entry allow overrides by the application + * deployment descriptor? + */ + private boolean override = true; + + public boolean getOverride() { + return this.override; + } + + public void setOverride(boolean override) { + this.override = override; + } + + + /** + * The value of this environment entry. + */ + private String value = null; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ContextEnvironment["); + sb.append("name="); + sb.append(getName()); + if (getDescription() != null) { + sb.append(", description="); + sb.append(getDescription()); + } + if (getType() != null) { + sb.append(", type="); + sb.append(getType()); + } + if (value != null) { + sb.append(", value="); + sb.append(value); + } + sb.append(", override="); + sb.append(override); + sb.append(']'); + return sb.toString(); + + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (override ? 1231 : 1237); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ContextEnvironment other = (ContextEnvironment) obj; + if (override != other.override) { + return false; + } + if (value == null) { + if (other.value != null) { + return false; + } + } else if (!value.equals(other.value)) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextHandler.java b/java/org/apache/tomcat/util/descriptor/web/ContextHandler.java new file mode 100644 index 0000000..bbbdd59 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextHandler.java @@ -0,0 +1,214 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * Representation of a handler reference for a web service, as + * represented in a <handler> element in the + * deployment descriptor. + * + * @author Fabien Carrion + */ +public class ContextHandler extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + + /** + * The Handler reference class. + */ + private String handlerclass = null; + + public String getHandlerclass() { + return this.handlerclass; + } + + public void setHandlerclass(String handlerclass) { + this.handlerclass = handlerclass; + } + + /** + * A list of QName specifying the SOAP Headers the handler will work on. + * -namespace and localpart values must be found inside the WSDL. + * + * A service-qname is composed by a namespaceURI and a localpart. + * + * soapHeader[0] : namespaceURI + * soapHeader[1] : localpart + */ + private final Map soapHeaders = new HashMap<>(); + + public Iterator getLocalparts() { + return soapHeaders.keySet().iterator(); + } + + public String getNamespaceuri(String localpart) { + return soapHeaders.get(localpart); + } + + public void addSoapHeaders(String localpart, String namespaceuri) { + soapHeaders.put(localpart, namespaceuri); + } + + /** + * Set a configured property. + * @param name The property name + * @param value The property value + */ + public void setProperty(String name, String value) { + this.setProperty(name, (Object) value); + } + + /** + * The soapRole. + */ + private final List soapRoles = new ArrayList<>(); + + public String getSoapRole(int i) { + return this.soapRoles.get(i); + } + + public int getSoapRolesSize() { + return this.soapRoles.size(); + } + + public void addSoapRole(String soapRole) { + this.soapRoles.add(soapRole); + } + + /** + * The portName. + */ + private final List portNames = new ArrayList<>(); + + public String getPortName(int i) { + return this.portNames.get(i); + } + + public int getPortNamesSize() { + return this.portNames.size(); + } + + public void addPortName(String portName) { + this.portNames.add(portName); + } + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ContextHandler["); + sb.append("name="); + sb.append(getName()); + if (handlerclass != null) { + sb.append(", class="); + sb.append(handlerclass); + } + if (this.soapHeaders != null) { + sb.append(", soap-headers="); + sb.append(this.soapHeaders); + } + if (this.getSoapRolesSize() > 0) { + sb.append(", soap-roles="); + sb.append(soapRoles); + } + if (this.getPortNamesSize() > 0) { + sb.append(", port-name="); + sb.append(portNames); + } + if (this.listProperties() != null) { + sb.append(", init-param="); + sb.append(this.listProperties()); + } + sb.append(']'); + return sb.toString(); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + + ((handlerclass == null) ? 0 : handlerclass.hashCode()); + result = prime * result + + ((portNames == null) ? 0 : portNames.hashCode()); + result = prime * result + + ((soapHeaders == null) ? 0 : soapHeaders.hashCode()); + result = prime * result + + ((soapRoles == null) ? 0 : soapRoles.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ContextHandler other = (ContextHandler) obj; + if (handlerclass == null) { + if (other.handlerclass != null) { + return false; + } + } else if (!handlerclass.equals(other.handlerclass)) { + return false; + } + if (portNames == null) { + if (other.portNames != null) { + return false; + } + } else if (!portNames.equals(other.portNames)) { + return false; + } + if (soapHeaders == null) { + if (other.soapHeaders != null) { + return false; + } + } else if (!soapHeaders.equals(other.soapHeaders)) { + return false; + } + if (soapRoles == null) { + if (other.soapRoles != null) { + return false; + } + } else if (!soapRoles.equals(other.soapRoles)) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextLocalEjb.java b/java/org/apache/tomcat/util/descriptor/web/ContextLocalEjb.java new file mode 100644 index 0000000..2f5bc74 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextLocalEjb.java @@ -0,0 +1,160 @@ +/* + * 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.tomcat.util.descriptor.web; + + + +/** + * Representation of a local EJB resource reference for a web application, as + * represented in a <ejb-local-ref> element in the + * deployment descriptor. + * + * @author Craig R. McClanahan + * @author Peter Rossbach (pero@apache.org) + */ +public class ContextLocalEjb extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + /** + * The name of the EJB home implementation class. + */ + private String home = null; + + public String getHome() { + return this.home; + } + + public void setHome(String home) { + this.home = home; + } + + + /** + * The link to a Jakarta EE EJB definition. + */ + private String link = null; + + public String getLink() { + return this.link; + } + + public void setLink(String link) { + this.link = link; + } + + + /** + * The name of the EJB local implementation class. + */ + private String local = null; + + public String getLocal() { + return this.local; + } + + public void setLocal(String local) { + this.local = local; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ContextLocalEjb["); + sb.append("name="); + sb.append(getName()); + if (getDescription() != null) { + sb.append(", description="); + sb.append(getDescription()); + } + if (getType() != null) { + sb.append(", type="); + sb.append(getType()); + } + if (home != null) { + sb.append(", home="); + sb.append(home); + } + if (link != null) { + sb.append(", link="); + sb.append(link); + } + if (local != null) { + sb.append(", local="); + sb.append(local); + } + sb.append(']'); + return sb.toString(); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((home == null) ? 0 : home.hashCode()); + result = prime * result + ((link == null) ? 0 : link.hashCode()); + result = prime * result + ((local == null) ? 0 : local.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ContextLocalEjb other = (ContextLocalEjb) obj; + if (home == null) { + if (other.home != null) { + return false; + } + } else if (!home.equals(other.home)) { + return false; + } + if (link == null) { + if (other.link != null) { + return false; + } + } else if (!link.equals(other.link)) { + return false; + } + if (local == null) { + if (other.local != null) { + return false; + } + } else if (!local.equals(other.local)) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextResource.java b/java/org/apache/tomcat/util/descriptor/web/ContextResource.java new file mode 100644 index 0000000..9d1d3cf --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextResource.java @@ -0,0 +1,188 @@ +/* + * 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.tomcat.util.descriptor.web; + + + +/** + * Representation of a resource reference for a web application, as + * represented in a <resource-ref> element in the + * deployment descriptor. + * + * @author Craig R. McClanahan + * @author Peter Rossbach (pero@apache.org) + */ +public class ContextResource extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + + /** + * The authorization requirement for this resource + * (Application or Container). + */ + private String auth = null; + + public String getAuth() { + return this.auth; + } + + public void setAuth(String auth) { + this.auth = auth; + } + + /** + * The sharing scope of this resource factory (Shareable + * or Unshareable). + */ + private String scope = "Shareable"; + + public String getScope() { + return this.scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + + /** + * Is this resource known to be a singleton resource. The default value is + * true since this is what users expect although the Jakarta EE spec implies + * that the default should be false. + */ + private boolean singleton = true; + + public boolean getSingleton() { + return singleton; + } + + public void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + + /** + * The name of the zero argument method to be called when the resource is + * no longer required to clean-up resources. This method must only speed up + * the clean-up of resources that would otherwise happen via garbage + * collection. + */ + private String closeMethod = null; + private boolean closeMethodConfigured = false; + + public String getCloseMethod() { + return closeMethod; + } + + public void setCloseMethod(String closeMethod) { + closeMethodConfigured = true; + this.closeMethod = closeMethod; + } + + public boolean getCloseMethodConfigured() { + return closeMethodConfigured; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ContextResource["); + sb.append("name="); + sb.append(getName()); + if (getDescription() != null) { + sb.append(", description="); + sb.append(getDescription()); + } + if (getType() != null) { + sb.append(", type="); + sb.append(getType()); + } + if (auth != null) { + sb.append(", auth="); + sb.append(auth); + } + if (scope != null) { + sb.append(", scope="); + sb.append(scope); + } + sb.append(']'); + return sb.toString(); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((auth == null) ? 0 : auth.hashCode()); + result = prime * result + + ((closeMethod == null) ? 0 : closeMethod.hashCode()); + result = prime * result + ((scope == null) ? 0 : scope.hashCode()); + result = prime * result + (singleton ? 1231 : 1237); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ContextResource other = (ContextResource) obj; + if (auth == null) { + if (other.auth != null) { + return false; + } + } else if (!auth.equals(other.auth)) { + return false; + } + if (closeMethod == null) { + if (other.closeMethod != null) { + return false; + } + } else if (!closeMethod.equals(other.closeMethod)) { + return false; + } + if (scope == null) { + if (other.scope != null) { + return false; + } + } else if (!scope.equals(other.scope)) { + return false; + } + if (singleton != other.singleton) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextResourceEnvRef.java b/java/org/apache/tomcat/util/descriptor/web/ContextResourceEnvRef.java new file mode 100644 index 0000000..9f4471b --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextResourceEnvRef.java @@ -0,0 +1,97 @@ +/* + * 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.tomcat.util.descriptor.web; + + + +/** + * Representation of an application resource reference, as represented in + * an <res-env-refy> element in the deployment descriptor. + * + * @author Craig R. McClanahan + * @author Peter Rossbach (pero@apache.org) + */ +public class ContextResourceEnvRef extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + /** + * Does this environment entry allow overrides by the application + * deployment descriptor? + */ + private boolean override = true; + + public boolean getOverride() { + return this.override; + } + + public void setOverride(boolean override) { + this.override = override; + } + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ContextResourceEnvRef["); + sb.append("name="); + sb.append(getName()); + if (getType() != null) { + sb.append(", type="); + sb.append(getType()); + } + sb.append(", override="); + sb.append(override); + sb.append(']'); + return sb.toString(); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (override ? 1231 : 1237); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ContextResourceEnvRef other = (ContextResourceEnvRef) obj; + if (override != other.override) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextResourceLink.java b/java/org/apache/tomcat/util/descriptor/web/ContextResourceLink.java new file mode 100644 index 0000000..67ca79a --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextResourceLink.java @@ -0,0 +1,122 @@ +/* + * 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.tomcat.util.descriptor.web; + + + +/** + * Representation of a resource link for a web application, as + * represented in a <ResourceLink> element in the + * server configuration file. + * + * @author Remy Maucherat + * @author Peter Rossbach (Peter Rossbach (pero@apache.org)) + */ +public class ContextResourceLink extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + /** + * The global name of this resource. + */ + private String global = null; + /** + * The factory to be used for creating the object + */ + private String factory = null; + + public String getGlobal() { + return this.global; + } + + public void setGlobal(String global) { + this.global = global; + } + + public String getFactory() { + return factory; + } + + public void setFactory(String factory) { + this.factory = factory; + } + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ContextResourceLink["); + sb.append("name="); + sb.append(getName()); + if (getType() != null) { + sb.append(", type="); + sb.append(getType()); + } + if (getGlobal() != null) { + sb.append(", global="); + sb.append(getGlobal()); + } + sb.append(']'); + return sb.toString(); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((factory == null) ? 0 : factory.hashCode()); + result = prime * result + ((global == null) ? 0 : global.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ContextResourceLink other = (ContextResourceLink) obj; + if (factory == null) { + if (other.factory != null) { + return false; + } + } else if (!factory.equals(other.factory)) { + return false; + } + if (global == null) { + if (other.global != null) { + return false; + } + } else if (!global.equals(other.global)) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextService.java b/java/org/apache/tomcat/util/descriptor/web/ContextService.java new file mode 100644 index 0000000..932dd68 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextService.java @@ -0,0 +1,357 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + + +/** + * Representation of a web service reference for a web application, as + * represented in a <service-ref> element in the + * deployment descriptor. + * + * @author Fabien Carrion + */ +public class ContextService extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + + /** + * The WebService reference name. + */ + private String displayname = null; + + public String getDisplayname() { + return this.displayname; + } + + public void setDisplayname(String displayname) { + this.displayname = displayname; + } + + /** + * A large icon for this WebService. + */ + private String largeIcon = null; + + public String getLargeIcon() { + return this.largeIcon; + } + + public void setLargeIcon(String largeIcon) { + this.largeIcon = largeIcon; + } + + /** + * A small icon for this WebService. + */ + private String smallIcon = null; + + public String getSmallIcon() { + return this.smallIcon; + } + + public void setSmallIcon(String smallIcon) { + this.smallIcon = smallIcon; + } + + /** + * The fully qualified class name of the JAX-WS Service interface that the + * client depends on. + */ + private String serviceInterface = null; + + public String getInterface() { + return serviceInterface; + } + + public void setInterface(String serviceInterface) { + this.serviceInterface = serviceInterface; + } + + /** + * Contains the location (relative to the root of + * the module) of the web service WSDL description. + */ + private String wsdlfile = null; + + public String getWsdlfile() { + return this.wsdlfile; + } + + public void setWsdlfile(String wsdlfile) { + this.wsdlfile = wsdlfile; + } + + /** + * A file specifying the correlation of the WSDL definition + * to the interfaces (Service Endpoint Interface, Service Interface). + */ + private String jaxrpcmappingfile = null; + + public String getJaxrpcmappingfile() { + return this.jaxrpcmappingfile; + } + + public void setJaxrpcmappingfile(String jaxrpcmappingfile) { + this.jaxrpcmappingfile = jaxrpcmappingfile; + } + + /** + * Declares the specific WSDL service element that is being referred to. + * It is not specified if no wsdl-file is declared or if WSDL contains only + * 1 service element. + * + * A service-qname is composed by a namespaceURI and a localpart. + * It must be defined if more than 1 service is declared in the WSDL. + * + * serviceqname[0] : namespaceURI + * serviceqname[1] : localpart + */ + private String[] serviceqname = new String[2]; + + public String[] getServiceqname() { + return this.serviceqname; + } + + public String getServiceqname(int i) { + return this.serviceqname[i]; + } + + public String getServiceqnameNamespaceURI() { + return this.serviceqname[0]; + } + + public String getServiceqnameLocalpart() { + return this.serviceqname[1]; + } + + public void setServiceqname(String[] serviceqname) { + this.serviceqname = serviceqname; + } + + public void setServiceqname(String serviceqname, int i) { + this.serviceqname[i] = serviceqname; + } + + public void setServiceqnameNamespaceURI(String namespaceuri) { + this.serviceqname[0] = namespaceuri; + } + + public void setServiceqnameLocalpart(String localpart) { + this.serviceqname[1] = localpart; + } + + /** + * Declares a client dependency on the container to resolving a Service Endpoint Interface + * to a WSDL port. It optionally associates the Service Endpoint Interface with a + * particular port-component. + * @return the endpoint names + */ + public Iterator getServiceendpoints() { + return this.listProperties(); + } + + public String getPortlink(String serviceendpoint) { + return (String) this.getProperty(serviceendpoint); + } + + public void addPortcomponent(String serviceendpoint, String portlink) { + if (portlink == null) { + portlink = ""; + } + this.setProperty(serviceendpoint, portlink); + } + + /** + * A list of Handlers to use for this service-ref. + * + * The instantiation of the handler have to be done. + */ + private final Map handlers = new HashMap<>(); + + public Iterator getHandlers() { + return handlers.keySet().iterator(); + } + + public ContextHandler getHandler(String handlername) { + return handlers.get(handlername); + } + + public void addHandler(ContextHandler handler) { + handlers.put(handler.getName(), handler); + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ContextService["); + sb.append("name="); + sb.append(getName()); + if (getDescription() != null) { + sb.append(", description="); + sb.append(getDescription()); + } + if (getType() != null) { + sb.append(", type="); + sb.append(getType()); + } + if (displayname != null) { + sb.append(", displayname="); + sb.append(displayname); + } + if (largeIcon != null) { + sb.append(", largeIcon="); + sb.append(largeIcon); + } + if (smallIcon != null) { + sb.append(", smallIcon="); + sb.append(smallIcon); + } + if (wsdlfile != null) { + sb.append(", wsdl-file="); + sb.append(wsdlfile); + } + if (jaxrpcmappingfile != null) { + sb.append(", jaxrpc-mapping-file="); + sb.append(jaxrpcmappingfile); + } + if (serviceqname[0] != null) { + sb.append(", service-qname/namespaceURI="); + sb.append(serviceqname[0]); + } + if (serviceqname[1] != null) { + sb.append(", service-qname/localpart="); + sb.append(serviceqname[1]); + } + if (this.getServiceendpoints() != null) { + sb.append(", port-component/service-endpoint-interface="); + sb.append(this.getServiceendpoints()); + } + if (handlers != null) { + sb.append(", handler="); + sb.append(handlers); + } + sb.append(']'); + return sb.toString(); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + + ((displayname == null) ? 0 : displayname.hashCode()); + result = prime * result + + ((handlers == null) ? 0 : handlers.hashCode()); + result = prime * + result + + ((jaxrpcmappingfile == null) ? 0 : jaxrpcmappingfile.hashCode()); + result = prime * result + + ((largeIcon == null) ? 0 : largeIcon.hashCode()); + result = prime * result + + ((serviceInterface == null) ? 0 : serviceInterface.hashCode()); + result = prime * result + Arrays.hashCode(serviceqname); + result = prime * result + + ((smallIcon == null) ? 0 : smallIcon.hashCode()); + result = prime * result + + ((wsdlfile == null) ? 0 : wsdlfile.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ContextService other = (ContextService) obj; + if (displayname == null) { + if (other.displayname != null) { + return false; + } + } else if (!displayname.equals(other.displayname)) { + return false; + } + if (handlers == null) { + if (other.handlers != null) { + return false; + } + } else if (!handlers.equals(other.handlers)) { + return false; + } + if (jaxrpcmappingfile == null) { + if (other.jaxrpcmappingfile != null) { + return false; + } + } else if (!jaxrpcmappingfile.equals(other.jaxrpcmappingfile)) { + return false; + } + if (largeIcon == null) { + if (other.largeIcon != null) { + return false; + } + } else if (!largeIcon.equals(other.largeIcon)) { + return false; + } + if (serviceInterface == null) { + if (other.serviceInterface != null) { + return false; + } + } else if (!serviceInterface.equals(other.serviceInterface)) { + return false; + } + if (!Arrays.equals(serviceqname, other.serviceqname)) { + return false; + } + if (smallIcon == null) { + if (other.smallIcon != null) { + return false; + } + } else if (!smallIcon.equals(other.smallIcon)) { + return false; + } + if (wsdlfile == null) { + if (other.wsdlfile != null) { + return false; + } + } else if (!wsdlfile.equals(other.wsdlfile)) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextTransaction.java b/java/org/apache/tomcat/util/descriptor/web/ContextTransaction.java new file mode 100644 index 0000000..6d2e370 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ContextTransaction.java @@ -0,0 +1,87 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + + +/** + * Representation of an application resource reference, as represented in + * an <res-env-refy> element in the deployment descriptor. + * + * @author Craig R. McClanahan + */ +public class ContextTransaction implements Serializable { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + + /** + * Holder for our configured properties. + */ + private final Map properties = new HashMap<>(); + + /** + * @param name The property name + * @return a configured property. + */ + public Object getProperty(String name) { + return properties.get(name); + } + + /** + * Set a configured property. + * @param name The property name + * @param value The property value + */ + public void setProperty(String name, Object value) { + properties.put(name, value); + } + + /** + * Remove a configured property. + * @param name The property name + */ + public void removeProperty(String name) { + properties.remove(name); + } + + /** + * List properties. + * @return the property names iterator + */ + public Iterator listProperties() { + return properties.keySet().iterator(); + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + return "Transaction[]"; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ErrorPage.java b/java/org/apache/tomcat/util/descriptor/web/ErrorPage.java new file mode 100644 index 0000000..6996308 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ErrorPage.java @@ -0,0 +1,163 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; + +import org.apache.tomcat.util.buf.UDecoder; + +/** + * Representation of an error page element for a web application, + * as represented in a <error-page> element in the + * deployment descriptor. + * + * @author Craig R. McClanahan + */ +public class ErrorPage extends XmlEncodingBase implements Serializable { + + private static final long serialVersionUID = 2L; + + + // ----------------------------------------------------- Instance Variables + + /** + * The error (status) code for which this error page is active. Note that + * status code 0 is used for the default error page. + */ + private int errorCode = 0; + + + /** + * The exception type for which this error page is active. + */ + private String exceptionType = null; + + + /** + * The context-relative location to handle this error or exception. + */ + private String location = null; + + + // ------------------------------------------------------------- Properties + + + /** + * @return the error code. + */ + public int getErrorCode() { + return this.errorCode; + } + + + /** + * Set the error code. + * + * @param errorCode The new error code + */ + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + + /** + * Set the error code (hack for default XmlMapper data type). + * + * @param errorCode The new error code + */ + public void setErrorCode(String errorCode) { + + try { + this.errorCode = Integer.parseInt(errorCode); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException(nfe); + } + } + + + /** + * @return the exception type. + */ + public String getExceptionType() { + return this.exceptionType; + } + + + /** + * Set the exception type. + * + * @param exceptionType The new exception type + */ + public void setExceptionType(String exceptionType) { + this.exceptionType = exceptionType; + } + + + /** + * @return the location. + */ + public String getLocation() { + return this.location; + } + + + /** + * Set the location. + * + * @param location The new location + */ + public void setLocation(String location) { + + // if ((location == null) || !location.startsWith("/")) + // throw new IllegalArgumentException + // ("Error Page Location must start with a '/'"); + this.location = UDecoder.URLDecode(location, getCharset()); + + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Render a String representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ErrorPage["); + if (exceptionType == null) { + sb.append("errorCode="); + sb.append(errorCode); + } else { + sb.append("exceptionType="); + sb.append(exceptionType); + } + sb.append(", location="); + sb.append(location); + sb.append(']'); + return sb.toString(); + } + + public String getName() { + if (exceptionType == null) { + return Integer.toString(errorCode); + } else { + return exceptionType; + } + } + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/FilterDef.java b/java/org/apache/tomcat/util/descriptor/web/FilterDef.java new file mode 100644 index 0000000..b742d5c --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/FilterDef.java @@ -0,0 +1,211 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.Filter; + +import org.apache.tomcat.util.res.StringManager; + + +/** + * Representation of a filter definition for a web application, as represented + * in a <filter> element in the deployment descriptor. + * + * @author Craig R. McClanahan + */ +public class FilterDef implements Serializable { + + private static final long serialVersionUID = 1L; + + private static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + // ------------------------------------------------------------- Properties + + + /** + * The description of this filter. + */ + private String description = null; + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + + /** + * The display name of this filter. + */ + private String displayName = null; + + public String getDisplayName() { + return this.displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + + /** + * The filter instance associated with this definition + */ + private transient Filter filter = null; + + public Filter getFilter() { + return filter; + } + + public void setFilter(Filter filter) { + this.filter = filter; + } + + + /** + * The fully qualified name of the Java class that implements this filter. + */ + private String filterClass = null; + + public String getFilterClass() { + return this.filterClass; + } + + public void setFilterClass(String filterClass) { + this.filterClass = filterClass; + } + + + /** + * The name of this filter, which must be unique among the filters + * defined for a particular web application. + */ + private String filterName = null; + + public String getFilterName() { + return this.filterName; + } + + public void setFilterName(String filterName) { + if (filterName == null || filterName.equals("")) { + throw new IllegalArgumentException( + sm.getString("filterDef.invalidFilterName", filterName)); + } + this.filterName = filterName; + } + + + /** + * The large icon associated with this filter. + */ + private String largeIcon = null; + + public String getLargeIcon() { + return this.largeIcon; + } + + public void setLargeIcon(String largeIcon) { + this.largeIcon = largeIcon; + } + + + /** + * The set of initialization parameters for this filter, keyed by + * parameter name. + */ + private final Map parameters = new HashMap<>(); + + public Map getParameterMap() { + return this.parameters; + } + + + /** + * The small icon associated with this filter. + */ + private String smallIcon = null; + + public String getSmallIcon() { + return this.smallIcon; + } + + public void setSmallIcon(String smallIcon) { + this.smallIcon = smallIcon; + } + + private String asyncSupported = null; + + public String getAsyncSupported() { + return asyncSupported; + } + + public void setAsyncSupported(String asyncSupported) { + this.asyncSupported = asyncSupported; + asyncSupportedBoolean = !("false".equalsIgnoreCase(asyncSupported)); + } + + private boolean asyncSupportedBoolean = true; + + public boolean getAsyncSupportedBoolean() { + return asyncSupportedBoolean; + } + + // --------------------------------------------------------- Public Methods + + + /** + * Add an initialization parameter to the set of parameters associated + * with this filter. + * + * @param name The initialization parameter name + * @param value The initialization parameter value + */ + public void addInitParameter(String name, String value) { + + if (parameters.containsKey(name)) { + // The spec does not define this but the TCK expects the first + // definition to take precedence + return; + } + parameters.put(name, value); + + } + + + /** + * Render a String representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("FilterDef["); + sb.append("filterName="); + sb.append(this.filterName); + sb.append(", filterClass="); + sb.append(this.filterClass); + sb.append(']'); + return sb.toString(); + } + + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/FilterMap.java b/java/org/apache/tomcat/util/descriptor/web/FilterMap.java new file mode 100644 index 0000000..22ee9fd --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/FilterMap.java @@ -0,0 +1,225 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import jakarta.servlet.DispatcherType; + +import org.apache.tomcat.util.buf.UDecoder; + +/** + * Representation of a filter mapping for a web application, as represented + * in a <filter-mapping> element in the deployment + * descriptor. Each filter mapping must contain a filter name plus either + * a URL pattern or a servlet name. + * + * @author Craig R. McClanahan + */ +public class FilterMap extends XmlEncodingBase implements Serializable { + + + // ------------------------------------------------------------- Properties + + + private static final long serialVersionUID = 1L; + + /** + * The name of this filter to be executed when this mapping matches + * a particular request. + */ + + public static final int ERROR = 1; + public static final int FORWARD = 2; + public static final int INCLUDE = 4; + public static final int REQUEST = 8; + public static final int ASYNC = 16; + + // represents nothing having been set. This will be seen + // as equal to a REQUEST + private static final int NOT_SET = 0; + + private int dispatcherMapping = NOT_SET; + + private String filterName = null; + + public String getFilterName() { + return this.filterName; + } + + public void setFilterName(String filterName) { + this.filterName = filterName; + } + + + /** + * The servlet name this mapping matches. + */ + private String[] servletNames = new String[0]; + + public String[] getServletNames() { + if (matchAllServletNames) { + return new String[] {}; + } else { + return this.servletNames; + } + } + + public void addServletName(String servletName) { + if ("*".equals(servletName)) { + this.matchAllServletNames = true; + } else { + String[] results = new String[servletNames.length + 1]; + System.arraycopy(servletNames, 0, results, 0, servletNames.length); + results[servletNames.length] = servletName; + servletNames = results; + } + } + + + /** + * The flag that indicates this mapping will match all url-patterns + */ + private boolean matchAllUrlPatterns = false; + + public boolean getMatchAllUrlPatterns() { + return matchAllUrlPatterns; + } + + + /** + * The flag that indicates this mapping will match all servlet-names + */ + private boolean matchAllServletNames = false; + + public boolean getMatchAllServletNames() { + return matchAllServletNames; + } + + + /** + * The URL pattern this mapping matches. + */ + private String[] urlPatterns = new String[0]; + + public String[] getURLPatterns() { + if (matchAllUrlPatterns) { + return new String[] {}; + } else { + return this.urlPatterns; + } + } + + public void addURLPattern(String urlPattern) { + addURLPatternDecoded(UDecoder.URLDecode(urlPattern, getCharset())); + } + public void addURLPatternDecoded(String urlPattern) { + if ("*".equals(urlPattern)) { + this.matchAllUrlPatterns = true; + } else { + String[] results = new String[urlPatterns.length + 1]; + System.arraycopy(urlPatterns, 0, results, 0, urlPatterns.length); + results[urlPatterns.length] = UDecoder.URLDecode(urlPattern, getCharset()); + urlPatterns = results; + } + } + + /** + * This method will be used to set the current state of the FilterMap + * representing the state of when filters should be applied. + * @param dispatcherString the dispatcher type which should + * match this filter + */ + public void setDispatcher(String dispatcherString) { + String dispatcher = dispatcherString.toUpperCase(Locale.ENGLISH); + + if (dispatcher.equals(DispatcherType.FORWARD.name())) { + // apply FORWARD to the global dispatcherMapping. + dispatcherMapping |= FORWARD; + } else if (dispatcher.equals(DispatcherType.INCLUDE.name())) { + // apply INCLUDE to the global dispatcherMapping. + dispatcherMapping |= INCLUDE; + } else if (dispatcher.equals(DispatcherType.REQUEST.name())) { + // apply REQUEST to the global dispatcherMapping. + dispatcherMapping |= REQUEST; + } else if (dispatcher.equals(DispatcherType.ERROR.name())) { + // apply ERROR to the global dispatcherMapping. + dispatcherMapping |= ERROR; + } else if (dispatcher.equals(DispatcherType.ASYNC.name())) { + // apply ERROR to the global dispatcherMapping. + dispatcherMapping |= ASYNC; + } + } + + public int getDispatcherMapping() { + // per the SRV.6.2.5 absence of any dispatcher elements is + // equivalent to a REQUEST value + if (dispatcherMapping == NOT_SET) { + return REQUEST; + } + + return dispatcherMapping; + } + + public String[] getDispatcherNames() { + List result = new ArrayList<>(); + if ((dispatcherMapping & FORWARD) != 0) { + result.add(DispatcherType.FORWARD.name()); + } + if ((dispatcherMapping & INCLUDE) != 0) { + result.add(DispatcherType.INCLUDE.name()); + } + if ((dispatcherMapping & REQUEST) != 0) { + result.add(DispatcherType.REQUEST.name()); + } + if ((dispatcherMapping & ERROR) != 0) { + result.add(DispatcherType.ERROR.name()); + } + if ((dispatcherMapping & ASYNC) != 0) { + result.add(DispatcherType.ASYNC.name()); + } + return result.toArray(new String[0]); + } + + // --------------------------------------------------------- Public Methods + + + /** + * Render a String representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("FilterMap["); + sb.append("filterName="); + sb.append(this.filterName); + for (String servletName : servletNames) { + sb.append(", servletName="); + sb.append(servletName); + } + for (String urlPattern : urlPatterns) { + sb.append(", urlPattern="); + sb.append(urlPattern); + } + sb.append(']'); + return sb.toString(); + } + + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/FragmentJarScannerCallback.java b/java/org/apache/tomcat/util/descriptor/web/FragmentJarScannerCallback.java new file mode 100644 index 0000000..419940f --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/FragmentJarScannerCallback.java @@ -0,0 +1,161 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.apache.tomcat.Jar; +import org.apache.tomcat.JarScannerCallback; +import org.xml.sax.InputSource; + +/** + * Callback handling a web-fragment.xml descriptor. + */ +public class FragmentJarScannerCallback implements JarScannerCallback { + + private static final String FRAGMENT_LOCATION = + "META-INF/web-fragment.xml"; + private final WebXmlParser webXmlParser; + private final boolean delegate; + private final boolean parseRequired; + private final Map fragments = new HashMap<>(); + private boolean ok = true; + + public FragmentJarScannerCallback(WebXmlParser webXmlParser, boolean delegate, + boolean parseRequired) { + this.webXmlParser = webXmlParser; + this.delegate = delegate; + this.parseRequired = parseRequired; + } + + + @Override + public void scan(Jar jar, String webappPath, boolean isWebapp) throws IOException { + + InputStream is = null; + WebXml fragment = new WebXml(); + fragment.setWebappJar(isWebapp); + fragment.setDelegate(delegate); + + try { + // Only web application JARs are checked for web-fragment.xml + // files. + // web-fragment.xml files don't need to be parsed if they are never + // going to be used. + if (isWebapp && parseRequired) { + is = jar.getInputStream(FRAGMENT_LOCATION); + } + + if (is == null) { + // If there is no web.xml, normal JAR no impact on + // distributable + fragment.setDistributable(true); + } else { + String fragmentUrl = jar.getURL(FRAGMENT_LOCATION); + InputSource source = new InputSource(fragmentUrl); + source.setByteStream(is); + if (!webXmlParser.parseWebXml(source, fragment, true)) { + ok = false; + } + } + } finally { + addFragment(fragment, jar.getJarFileURL()); + } + } + + + private String extractJarFileName(URL input) { + String url = input.toString(); + if (url.endsWith("!/")) { + // Remove it + url = url.substring(0, url.length() - 2); + } + + // File name will now be whatever is after the final / + return url.substring(url.lastIndexOf('/') + 1); + } + + + @Override + public void scan(File file, String webappPath, boolean isWebapp) throws IOException { + + WebXml fragment = new WebXml(); + fragment.setWebappJar(isWebapp); + fragment.setDelegate(delegate); + + File fragmentFile = new File(file, FRAGMENT_LOCATION); + try { + if (fragmentFile.isFile()) { + try (InputStream stream = new FileInputStream(fragmentFile)) { + InputSource source = + new InputSource(fragmentFile.toURI().toURL().toString()); + source.setByteStream(stream); + if (!webXmlParser.parseWebXml(source, fragment, true)) { + ok = false; + } + } + } else { + // If there is no web.xml, normal folder no impact on + // distributable + fragment.setDistributable(true); + } + } finally { + addFragment(fragment, file.toURI().toURL()); + } + } + + + private void addFragment(WebXml fragment, URL url) { + fragment.setURL(url); + if (fragment.getName() == null) { + fragment.setName(url.toString()); + } + fragment.setJarName(extractJarFileName(url)); + if (fragments.containsKey(fragment.getName())) { + // Duplicate. Mark the fragment that has already been found with + // this name as having a duplicate so Tomcat can handle it + // correctly when the fragments are being ordered. + String duplicateName = fragment.getName(); + fragments.get(duplicateName).addDuplicate(url.toString()); + // Rename the current fragment so it doesn't clash + fragment.setName(url.toString()); + } + fragments.put(fragment.getName(), fragment); + } + + + @Override + public void scanWebInfClasses() { + // NO-OP. Fragments unpacked in WEB-INF classes are not handled, + // mainly because if there are multiple fragments there is no way to + // handle multiple web-fragment.xml files. + } + + public boolean isOk() { + return ok; + } + + public Map getFragments() { + return fragments; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/Injectable.java b/java/org/apache/tomcat/util/descriptor/web/Injectable.java new file mode 100644 index 0000000..ee02171 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/Injectable.java @@ -0,0 +1,25 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.List; + +public interface Injectable { + String getName(); + void addInjectionTarget(String injectionTargetName, String jndiName); + List getInjectionTargets(); +} diff --git a/java/org/apache/tomcat/util/descriptor/web/InjectionTarget.java b/java/org/apache/tomcat/util/descriptor/web/InjectionTarget.java new file mode 100644 index 0000000..acb7f4c --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/InjectionTarget.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; + +public class InjectionTarget implements Serializable { + + private static final long serialVersionUID = 1L; + + private String targetClass; + private String targetName; + + + public InjectionTarget() { + // NOOP + } + + public InjectionTarget(String targetClass, String targetName) { + this.targetClass = targetClass; + this.targetName = targetName; + } + + public String getTargetClass() { + return targetClass; + } + + public void setTargetClass(String targetClass) { + this.targetClass = targetClass; + } + + public String getTargetName() { + return targetName; + } + + public void setTargetName(String targetName) { + this.targetName = targetName; + } + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/JspConfigDescriptorImpl.java b/java/org/apache/tomcat/util/descriptor/web/JspConfigDescriptorImpl.java new file mode 100644 index 0000000..3418225 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/JspConfigDescriptorImpl.java @@ -0,0 +1,46 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.ArrayList; +import java.util.Collection; + +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.descriptor.JspPropertyGroupDescriptor; +import jakarta.servlet.descriptor.TaglibDescriptor; + +public class JspConfigDescriptorImpl implements JspConfigDescriptor { + + private final Collection jspPropertyGroups; + private final Collection taglibs; + + public JspConfigDescriptorImpl(Collection jspPropertyGroups, + Collection taglibs) { + this.jspPropertyGroups = jspPropertyGroups; + this.taglibs = taglibs; + } + + @Override + public Collection getJspPropertyGroups() { + return new ArrayList<>(jspPropertyGroups); + } + + @Override + public Collection getTaglibs() { + return new ArrayList<>(taglibs); + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/JspPropertyGroup.java b/java/org/apache/tomcat/util/descriptor/web/JspPropertyGroup.java new file mode 100644 index 0000000..07f3dd3 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/JspPropertyGroup.java @@ -0,0 +1,115 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.tomcat.util.buf.UDecoder; + +/** + * Representation of a jsp-property-group element in web.xml. + */ +public class JspPropertyGroup extends XmlEncodingBase { + + private Boolean deferredSyntax = null; + public void setDeferredSyntax(String deferredSyntax) { + this.deferredSyntax = Boolean.valueOf(deferredSyntax); + } + public Boolean getDeferredSyntax() { return deferredSyntax; } + + private Boolean errorOnELNotFound = null; + public void setErrorOnELNotFound(String errorOnELNotFound) { + this.errorOnELNotFound = Boolean.valueOf(errorOnELNotFound); + } + public Boolean getErrorOnELNotFound() { return errorOnELNotFound; } + + private Boolean elIgnored = null; + public void setElIgnored(String elIgnored) { + this.elIgnored = Boolean.valueOf(elIgnored); + } + public Boolean getElIgnored() { return elIgnored; } + + private final Collection includeCodas = new ArrayList<>(); + public void addIncludeCoda(String includeCoda) { + includeCodas.add(includeCoda); + } + public Collection getIncludeCodas() { return includeCodas; } + + private final Collection includePreludes = new ArrayList<>(); + public void addIncludePrelude(String includePrelude) { + includePreludes.add(includePrelude); + } + public Collection getIncludePreludes() { return includePreludes; } + + private Boolean isXml = null; + public void setIsXml(String isXml) { + this.isXml = Boolean.valueOf(isXml); + } + public Boolean getIsXml() { return isXml; } + + private String pageEncoding = null; + public void setPageEncoding(String pageEncoding) { + this.pageEncoding = pageEncoding; + } + public String getPageEncoding() { return this.pageEncoding; } + + private Boolean scriptingInvalid = null; + public void setScriptingInvalid(String scriptingInvalid) { + this.scriptingInvalid = Boolean.valueOf(scriptingInvalid); + } + public Boolean getScriptingInvalid() { return scriptingInvalid; } + + private Boolean trimWhitespace = null; + public void setTrimWhitespace(String trimWhitespace) { + this.trimWhitespace = Boolean.valueOf(trimWhitespace); + } + public Boolean getTrimWhitespace() { return trimWhitespace; } + + private LinkedHashSet urlPattern = new LinkedHashSet<>(); + public void addUrlPattern(String urlPattern) { + addUrlPatternDecoded(UDecoder.URLDecode(urlPattern, getCharset())); + } + public void addUrlPatternDecoded(String urlPattern) { + this.urlPattern.add(urlPattern); + } + public Set getUrlPatterns() { return this.urlPattern; } + + private String defaultContentType = null; + public void setDefaultContentType(String defaultContentType) { + this.defaultContentType = defaultContentType; + } + public String getDefaultContentType() { return this.defaultContentType; } + + private String buffer = null; + public void setBuffer(String buffer) { + this.buffer = buffer; + } + public String getBuffer() { return this.buffer; } + + private Boolean errorOnUndeclaredNamespace = null; + public void setErrorOnUndeclaredNamespace( + String errorOnUndeclaredNamespace) { + this.errorOnUndeclaredNamespace = + Boolean.valueOf(errorOnUndeclaredNamespace); + } + public Boolean getErrorOnUndeclaredNamespace() { + return this.errorOnUndeclaredNamespace; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/JspPropertyGroupDescriptorImpl.java b/java/org/apache/tomcat/util/descriptor/web/JspPropertyGroupDescriptorImpl.java new file mode 100644 index 0000000..0f67f61 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/JspPropertyGroupDescriptorImpl.java @@ -0,0 +1,157 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.ArrayList; +import java.util.Collection; + +import jakarta.servlet.descriptor.JspPropertyGroupDescriptor; + + + +public class JspPropertyGroupDescriptorImpl + implements JspPropertyGroupDescriptor{ + + private final JspPropertyGroup jspPropertyGroup; + + + public JspPropertyGroupDescriptorImpl( + JspPropertyGroup jspPropertyGroup) { + this.jspPropertyGroup = jspPropertyGroup; + } + + + @Override + public String getBuffer() { + return jspPropertyGroup.getBuffer(); + } + + + @Override + public String getDefaultContentType() { + return jspPropertyGroup.getDefaultContentType(); + } + + + @Override + public String getDeferredSyntaxAllowedAsLiteral() { + String result = null; + + if (jspPropertyGroup.getDeferredSyntax() != null) { + result = jspPropertyGroup.getDeferredSyntax().toString(); + } + + return result; + } + + + @Override + public String getElIgnored() { + String result = null; + + if (jspPropertyGroup.getElIgnored() != null) { + result = jspPropertyGroup.getElIgnored().toString(); + } + + return result; + } + + + @Override + public String getErrorOnELNotFound() { + String result = null; + + if (jspPropertyGroup.getErrorOnELNotFound() != null) { + result = jspPropertyGroup.getErrorOnELNotFound().toString(); + } + + return result; + } + + + @Override + public String getErrorOnUndeclaredNamespace() { + String result = null; + + if (jspPropertyGroup.getErrorOnUndeclaredNamespace() != null) { + result = + jspPropertyGroup.getErrorOnUndeclaredNamespace().toString(); + } + + return result; + } + + + @Override + public Collection getIncludeCodas() { + return new ArrayList<>(jspPropertyGroup.getIncludeCodas()); + } + + + @Override + public Collection getIncludePreludes() { + return new ArrayList<>(jspPropertyGroup.getIncludePreludes()); + } + + + @Override + public String getIsXml() { + String result = null; + + if (jspPropertyGroup.getIsXml() != null) { + result = jspPropertyGroup.getIsXml().toString(); + } + + return result; + } + + + @Override + public String getPageEncoding() { + return jspPropertyGroup.getPageEncoding(); + } + + + @Override + public String getScriptingInvalid() { + String result = null; + + if (jspPropertyGroup.getScriptingInvalid() != null) { + result = jspPropertyGroup.getScriptingInvalid().toString(); + } + + return result; + } + + + @Override + public String getTrimDirectiveWhitespaces() { + String result = null; + + if (jspPropertyGroup.getTrimWhitespace() != null) { + result = jspPropertyGroup.getTrimWhitespace().toString(); + } + + return result; + } + + + @Override + public Collection getUrlPatterns() { + return new ArrayList<>(jspPropertyGroup.getUrlPatterns()); + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings.properties new file mode 100644 index 0000000..f9f3b03 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings.properties @@ -0,0 +1,62 @@ +# 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. + +filterDef.invalidFilterName=Invalid [{0}] in filter definition. + +securityConstraint.uncoveredHttpMethod=For security constraints with URL pattern [{0}] only the HTTP methods [{1}] are covered. All other methods are uncovered. +securityConstraint.uncoveredHttpMethodFix=Adding security constraints with URL pattern [{0}] to deny access with the uncovered HTTP methods that are not one of the following [{1}] +securityConstraint.uncoveredHttpOmittedMethod=For security constraints with URL pattern [{0}] the HTTP methods [{1}] are uncovered. +securityConstraint.uncoveredHttpOmittedMethodFix=Adding security constraints with URL pattern [{0}] to deny access with the uncovered HTTP methods [{1}] + +servletDef.invalidServletName=Invalid [{0}] in servlet definition. + +webRuleSet.absoluteOrdering= element not valid in web-fragment.xml and will be ignored +webRuleSet.absoluteOrderingCount= element is limited to 1 occurrence +webRuleSet.nameCount= element is limited to 1 occurrence +webRuleSet.noMethod=Cannot find method [{0}] in object [{1}] of class [{2}] +webRuleSet.postconstruct.duplicate=Duplicate post construct method definition for class [{0}] +webRuleSet.predestroy.duplicate=Duplicate @PreDestroy method definition for class [{0}] +webRuleSet.relativeOrdering= element not valid in web.xml and will be ignored +webRuleSet.relativeOrderingCount= element is limited to 1 occurrence + +webXml.duplicateEnvEntry=Duplicate env-entry name [{0}] +webXml.duplicateFilter=Duplicate filter name [{0}] +webXml.duplicateFragment=More than one fragment with the name [{0}] was found. This is not legal with relative ordering. See section 8.2.2 2c of the Servlet specification for details. Consider using absolute ordering. Duplicate fragments found in [{1}]. +webXml.duplicateMessageDestination=Duplicate message-destination name [{0}] +webXml.duplicateMessageDestinationRef=Duplicate message-destination-ref name [{0}] +webXml.duplicateResourceEnvRef=Duplicate resource-env-ref name [{0}] +webXml.duplicateResourceRef=Duplicate resource-ref name [{0}] +webXml.duplicateServletMapping=The servlets named [{0}] and [{1}] are both mapped to the url-pattern [{2}] which is not permitted +webXml.duplicateTaglibUri=Duplicate tag library URI [{0}] +webXml.mergeConflictDisplayName=The display name was defined in multiple fragments with different values including fragment with name [{0}] located at [{1}] +webXml.mergeConflictFilter=The Filter [{0}] was defined inconsistently in multiple fragments including fragment with name [{1}] located at [{2}] +webXml.mergeConflictLoginConfig=A LoginConfig was defined inconsistently in multiple fragments including fragment with name [{0}] located at [{1}] +webXml.mergeConflictOrder=Fragment relative ordering contains circular references. This can be resolved by using absolute ordering in web.xml. +webXml.mergeConflictResource=The Resource [{0}] was defined inconsistently in multiple fragments including fragment with name [{1}] located at [{2}] +webXml.mergeConflictServlet=The Servlet [{0}] was defined inconsistently in multiple fragments including fragment with name [{1}] located at [{2}] +webXml.mergeConflictSessionCookieAttributes=The session cookie attributes were defined inconsistently in multiple fragments with different values including fragment with name [{0}] located at [{1}] +webXml.mergeConflictSessionCookieName=The session cookie name was defined inconsistently in multiple fragments with different values including fragment with name [{0}] located at [{1}] +webXml.mergeConflictSessionTimeout=The session timeout was defined inconsistently in multiple fragments with different values including fragment with name [{0}] located at [{1}] +webXml.mergeConflictSessionTrackingMode=The session tracking modes were defined inconsistently in multiple fragments including fragment with name [{0}] located at [{1}] +webXml.mergeConflictString=The [{0}] with name [{1}] was defined inconsistently in multiple fragments including fragment with name [{2}] located at [{3}] +webXml.multipleOther=Multiple entries nested in element +webXml.reservedName=A web.xml file was detected using a reserved name [{0}]. The name element will be ignored for this fragment. +webXml.unrecognisedPublicId=The public ID [{0}] did not match any of the known public ID''s for web.xml files so the version could not be identified +webXml.version.unknown=Unknown version string [{0}]. Default version will be used. +webXml.wrongFragmentName=Used a wrong fragment name [{0}] at web.xml absolute-ordering tag! + +webXmlParser.applicationParse=Parse error in application web.xml file at [{0}] +webXmlParser.applicationPosition=Occurred at line [{0}] column [{1}] +webXmlParser.applicationStart=Parsing application web.xml file at [{0}] diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_cs.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_cs.properties new file mode 100644 index 0000000..4292ae1 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_cs.properties @@ -0,0 +1,21 @@ +# 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. + +filterDef.invalidFilterName=Neplatný [{0}] v definici filtru. + +webRuleSet.postconstruct.duplicate=Duplicitní definice post construct metody pro třídu [{0}] + +webXml.duplicateServletMapping=Servlety [{0}] a [{1}] jsou oba mapovány na URL[{2}], která není dovolená. +webXml.mergeConflictSessionTrackingMode=Módy sledování relace byly definovány nekonzistentnÄ› ve více fragmentech, vÄetnÄ› fragmentu se jménem [{0}] umístÄ›ným na [{1}] diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_de.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_de.properties new file mode 100644 index 0000000..8168c7e --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_de.properties @@ -0,0 +1,20 @@ +# 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. + +filterDef.invalidFilterName=Ungültiger [{0}] in der Filter Definition. + +webRuleSet.postconstruct.duplicate=Doppelte Post-Konstruktor-Methoden-Definition für Klasse [{0}] + +webXml.duplicateFilter=Doppelter Filter-Name [{0}] diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_es.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_es.properties new file mode 100644 index 0000000..7508fc9 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_es.properties @@ -0,0 +1,31 @@ +# 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. + +filterDef.invalidFilterName= [{0}] inválido en la definición de filtros.\n + +securityConstraint.uncoveredHttpOmittedMethod=Debido a restricciones de seguridad con el patrón de URL [{0}] los métodos HTTP [{1}] no estan cubiertos. + +webRuleSet.absoluteOrdering=Elemento no válido en web-fragment.xml y será ignorado +webRuleSet.postconstruct.duplicate=La definición del metodo the post construcción para la clase [{0}] esta duplicada\n +webRuleSet.relativeOrdering=elemento no válido en web.xml y será ignorado + +webXml.duplicateFilter=Filtro de nombre duplicado [{0}]\n +webXml.duplicateServletMapping=Los servlets llamados [{0}] y [{1}] estan ambos mapeados al patrón de URL [{2}] el cual no esta permitido +webXml.mergeConflictSessionTrackingMode=Los modos de seguimiento fueron definidos inconsistentemente en multiples fragmentos, incluyendo fragmentos con el nombre [{0}] localizado en [{1}]\n +webXml.reservedName=Un archivo web.xml fue detectado usando un nombre reservado [{0}]. El nombre será ignorado para este fragmento. + +webXmlParser.applicationParse=Error de evaluación (parse) en el archivo web.xml de la aplicación a [{0}] +webXmlParser.applicationPosition=Se ha producido en la línea [{0}] columna [{1}] +webXmlParser.applicationStart=Analizando fichero de aplicación web.xml en [{0}] diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_fr.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_fr.properties new file mode 100644 index 0000000..db54fee --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_fr.properties @@ -0,0 +1,62 @@ +# 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. + +filterDef.invalidFilterName=Valeur Invalide de [{0}] dans la définition du filtre. + +securityConstraint.uncoveredHttpMethod=Les méthodes HTTP [{1}] des contraintes de sécurité du modèle d''URL [{0}] sont protégées, toutes les autres ne le sont pas +securityConstraint.uncoveredHttpMethodFix=Ajout de contraintes de sécurité avec le masque d''URL [{0}] pour empêcher l''accès aux méthodes HTTP non couvertes qui ne sont pas une de celles ci [{1}] +securityConstraint.uncoveredHttpOmittedMethod=Les méthodes HTTP [{1}] des contraintes de sécurité du modèle d''URL [{0}] ne sont pas protégées +securityConstraint.uncoveredHttpOmittedMethodFix=Ajout de contraintes de sécurité avec le masque d''URL [{0}] pour empêcher l''accès aux méthodes HTTP [{1}] non couvertes + +servletDef.invalidServletName= [{0}] invalide dans la définition du Servlet + +webRuleSet.absoluteOrdering=L’élément est invalide dans web-fragment.xml et sera ignoré +webRuleSet.absoluteOrderingCount=L'élément est limité à 1 ocurrence +webRuleSet.nameCount=L'élément est limité à 1 ocurrence +webRuleSet.noMethod=Impossible de trouver la méthode [{0}] dans l''objet [{1}] de la classe [{2}] +webRuleSet.postconstruct.duplicate=La méthode post construct est dupliquée dans la classe [{0}] +webRuleSet.predestroy.duplicate=Double définition de l''annotation de méthode @PreDestroy pour la classe [{0}] +webRuleSet.relativeOrdering=L’élément est invalide dans web.xml et sera ignoré +webRuleSet.relativeOrderingCount=L'élément est limité à 1 ocurrence + +webXml.duplicateEnvEntry=Le nom de l''env-entry [{0}] a été déclaré en double +webXml.duplicateFilter=Nom du filtre dupliqué [{0}] +webXml.duplicateFragment=Il y a plus d''un fragment nommé [{0}], ce qui n''est pas légal avec l''ordre relatif ; voir la section 8.2.2 2c de la spécification Servlet, l''ordre absolu peut éventuellement être utilisé +webXml.duplicateMessageDestination=Le nom de la message-destination [{0}] a été déclaré en double +webXml.duplicateMessageDestinationRef=Le nom de la message-destination-ref [{0}] a été déclaré en double +webXml.duplicateResourceEnvRef=Le nom de la resource-env-ref [{0}] a été déclaré en double +webXml.duplicateResourceRef=Le nom de la resource-ref [{0}] a été déclaré en double +webXml.duplicateServletMapping=Les servlets nommés [{0}] et [{1}] sont tous deux associés au même modèle d''URL [{2}], ce qui n''est pas permis +webXml.duplicateTaglibUri=Librairies de tags déclarée en double avec l''URI [{0}] +webXml.mergeConflictDisplayName=Le nom d''affichage a été défini de manière inconsistante entre différents fragments dont le fragment [{0}] situé à [{1}] +webXml.mergeConflictFilter=Le Filter [{0}] a été défini de manière inconsistante entre différents fragments dont le fragment [{1}] situé à [{2}] +webXml.mergeConflictLoginConfig=Le LoginConfig a été défini de manière inconsistante entre différents fragments dont le fragment [{0}] situé à [{1}] +webXml.mergeConflictOrder=L'ordre relatif des fragments contient des références circulaires, cela peut être résolu en utilisant un ordre absolu dans web.xml +webXml.mergeConflictResource=La Resource [{0}] a été définie de manière inconsistante entre différents fragments dont le fragment [{1}] situé à [{2}] +webXml.mergeConflictServlet=Le Servlet [{0}] a été défini de manière inconsistante entre différents fragments dont le fragment [{1}] situé à [{2}] +webXml.mergeConflictSessionCookieAttributes=Les attributs de cookie de session ont été définis de manière inconsistente dans plusieurs fragments avec des valeurs différentes dont dans celui nommé [{0}] et situé à [{1}] +webXml.mergeConflictSessionCookieName=Le nom de cookie de session a été défini de manière inconsistante avec des valeurs différentes entre différents fragments dont le fragment [{0}] situé à [{1}] +webXml.mergeConflictSessionTimeout=Le timeout de la session a été défini de manière inconsistante avec des valeurs différentes entre différents fragments dont le fragment [{0}] situé à [{1}] +webXml.mergeConflictSessionTrackingMode=Les modes de gestion de la session ont été déclarés de manière inconsistante entre plusieurs fragments nommés [{0}] et localisés à [{1}] +webXml.mergeConflictString=Le [{0}] avec comme nom [{1}] a été défini de manière inconsistante entre différents fragments dont le fragment [{2}] situé à [{3}] +webXml.multipleOther=Plusieurs entrées sont incluses dans l'élément +webXml.reservedName=Un fichier web.xml a été détecté avec un nom réservé [{0}], l''élément name sera ignoré pour ce fragment +webXml.unrecognisedPublicId=L''identifiant public [{0}] ne correspond à aucun des identifiants connus pour les fichiers web.xml, donc la version n''a pu être indentifiée +webXml.version.unknown=Version [{0}] inconnue, utilisation de la version par défaut +webXml.wrongFragmentName=Utilisation d''un mauvais nom de fragment [{0}] dans le tag absolute-ordering de web.xml + +webXmlParser.applicationParse=Erreur de traitement du web.xml de l''application à [{0}] +webXmlParser.applicationPosition=S''est produit à la ligne [{0}] colonne [{1}] +webXmlParser.applicationStart=Traitement du web.xml de l''application à [{0}] diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ja.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ja.properties new file mode 100644 index 0000000..6519dfc --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ja.properties @@ -0,0 +1,62 @@ +# 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. + +filterDef.invalidFilterName=フィルター定義ã«ç„¡åŠ¹ãª <フィルターå> [{0}] ãŒã‚ã‚Šã¾ã™ã€‚ + +securityConstraint.uncoveredHttpMethod=URLパターン [{0}] ã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£åˆ¶ç´„ã®å ´åˆã€HTTPメソッド [{1}] ã®ã¿ãŒå¯¾è±¡ã¨ãªã‚Šã¾ã™ã€‚ãã®ä»–ã®æ–¹æ³•ã¯ã™ã¹ã¦å¯¾è±¡å¤–ã§ã™ã€‚ +securityConstraint.uncoveredHttpMethodFix=次㮠[{1}] ã§ã¯ãªã„特定ã®HTTPメソッドã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’æ‹’å¦ã™ã‚‹URLパターン [{0}] ã‚’æŒã¤ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£åˆ¶ç´„を追加ã—ã¾ã™ +securityConstraint.uncoveredHttpOmittedMethod=URLパターン [{0}] ã‚’æŒã¤ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£åˆ¶ç´„ã®å ´åˆã€HTTPメソッド [{1}] ã¯æ¤œå‡ºã•ã‚Œã¾ã›ã‚“。 +securityConstraint.uncoveredHttpOmittedMethodFix=ä¿è­·ã•ã‚Œã¦ã„ãªã„HTTPメソッド [{1}] ã§ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’æ‹’å¦ã™ã‚‹URLパターン [{0}] ã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£åˆ¶ç´„を追加 + +servletDef.invalidServletName=サーブレット定義㮠[{0}]ãŒç„¡åŠ¹ã§ã™ã€‚ + +webRuleSet.absoluteOrdering=è¦ç´ ã¯web-fragment.xmlã§ç„¡åŠ¹ã§ã‚り無視ã•ã‚Œã¾ã™ã€‚ +webRuleSet.absoluteOrderingCount=è¦ç´ ã¯1回ã®ç™ºç”Ÿã«åˆ¶é™ã•ã‚Œã¾ã™ã€‚ +webRuleSet.nameCount=è¦ç´ ã¯1回ã«åˆ¶é™ã•ã‚Œã¾ã™ã€‚ +webRuleSet.noMethod=クラス [{2}] ã®ã‚ªãƒ–ジェクト [{1}] ã«ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +webRuleSet.postconstruct.duplicate=クラス [{0}]ã«PostConstructメソッド定義ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +webRuleSet.predestroy.duplicate=クラス [{0}] ã®@PreDestroyメソッドã®å®šç¾©ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +webRuleSet.relativeOrdering=è¦ç´ ã¯web.xmlã§ç„¡åŠ¹ã§ã‚り無視ã•ã‚Œã¾ã™ã€‚ +webRuleSet.relativeOrderingCount=è¦ç´ ã¯1回ã®å‡ºç¾ã«åˆ¶é™ã•ã‚Œã¾ã™ã€‚ + +webXml.duplicateEnvEntry=env-entry å [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +webXml.duplicateFilter=åŒã˜åå‰ã®ãƒ•ã‚¡ã‚¤ãƒ« [{0}] ãŒå­˜åœ¨ã—ã¾ã™ã€‚ +webXml.duplicateFragment=åŒåã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{0}] ãŒè¤‡æ•°è¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚相対的ãªé †åºä»˜ã‘ã¯æ­£å¼ãªæ©Ÿèƒ½ã§ã¯ã‚ã‚Šã¾ã›ã‚“。詳細㯠Servlet Speification ã® 8.2.2 節 2c é …ã‚’å‚ç…§ã—ã¦ãã ã•ã„。絶対的ãªé †åºä»˜ã‘ã®åˆ©ç”¨ã‚’検討ã—ã¦ãã ã•ã„。 +webXml.duplicateMessageDestination=message-destination å [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +webXml.duplicateMessageDestinationRef=message-destination-ref å [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +webXml.duplicateResourceEnvRef=resource-env-ref å [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +webXml.duplicateResourceRef=resource-ref å [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +webXml.duplicateServletMapping=サーブレット [{0}] 㨠[{1}] ã‚’åŒã˜ url-pattern [{2}] ã«ãƒžãƒƒãƒ”ングã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +webXml.duplicateTaglibUri=URI [{0}] ã®ã‚¿ã‚°ãƒ©ã‚¤ãƒ–ラリãŒé‡è¤‡ã—ã¦ã„ã¾ã™ã€‚ +webXml.mergeConflictDisplayName=[{1}] ã«é…ç½®ã•ã‚ŒãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{0}] ã®è¡¨ç¤ºåã¯ã€ä»–ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¨ç•°ãªã‚Šã¾ã™ã€‚ +webXml.mergeConflictFilter=[{2}] ã«é…ç½®ã•ã‚ŒãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{1}] ã®ãƒ•ã‚£ãƒ«ã‚¿ [{0}] ã®å€¤ã¯ã€ä»–ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¨ç•°ãªã‚Šã¾ã™ã€‚ +webXml.mergeConflictLoginConfig=[{1}] ã«é…ç½®ã•ã‚ŒãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{0}] ã® LoginConfig ã¯ã€ä»–ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¨ç•°ãªã‚Šã¾ã™ã€‚ +webXml.mergeConflictOrder=フラグメントã®ç›¸å¯¾é †åºã«ã¯å¾ªç’°å‚ç…§ãŒå«ã¾ã‚Œã¾ã™ã€‚ã“ã‚Œã¯ã€web.xmlã§çµ¶å¯¾é †åºã‚’使用ã™ã‚‹ã“ã¨ã§è§£æ±ºã§ãã¾ã™ã€‚ +webXml.mergeConflictResource=[{2}] ã«é…ç½®ã•ã‚ŒãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{1}] ã®ãƒªã‚½ãƒ¼ã‚¹ [{0}] ã®å€¤ã¯ã€ä»–ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¨ç•°ãªã‚Šã¾ã™ã€‚ +webXml.mergeConflictServlet=[{2}] ã«é…ç½®ã•ã‚ŒãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{1}] ã® Servlet [{0}] ã®å€¤ã¯ã€ä»–ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¨ç•°ãªã‚Šã¾ã™ã€‚ +webXml.mergeConflictSessionCookieAttributes=セッションCookie属性ã¯ã€[{1}] ã«ã‚ã‚‹ [{0}] ã¨ã„ã†åå‰ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã‚’å«ã‚€è¤‡æ•°ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã§ç•°ãªã‚‹å€¤ã‚’æŒã¡ä¸€è²«æ€§ãªã定義ã•ã‚Œã¦ã„ã¾ã—㟠+webXml.mergeConflictSessionCookieName=[{1}] ã«é…ç½®ã•ã‚ŒãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{0}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³Cookieåã®å€¤ã¯ã€ä»–ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¨ç•°ãªã‚Šã¾ã™ã€‚ +webXml.mergeConflictSessionTimeout=[{1}] ã«é…ç½®ã•ã‚ŒãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{0}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆæ™‚é–“ã¯ã€ä»–ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¨ç•°ãªã‚Šã¾ã™ã€‚ +webXml.mergeConflictSessionTrackingMode=[{1}] ã«é…ç½®ã•ã‚ŒãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆå [{0}] ã‚’å«ã‚€è¤‡æ•°ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã«ã¤ã„ã¦ã€ã‚»ãƒƒã‚·ãƒ§ãƒ³è¿½è·¡ãƒ¢ãƒ¼ãƒ‰ã®è¨­å®šãŒä¸€è²«ã—ã¾ã›ã‚“ +webXml.mergeConflictString=[{0}] ã® [{1}] 㯠[{3}] ã«é…ç½®ã•ã‚ŒãŸè¤‡æ•°ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆ [{2}] ã§åˆ¥ã®å€¤ãŒå®šç¾©ã•ã‚Œã¦ã„ã¾ã™ã€‚ +webXml.multipleOther= è¦ç´ ã«è¤‡æ•°ã® è¦ç´ ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +webXml.reservedName=予約å[{0}]を使用ã—ã¦web.xmlファイルãŒæ¤œå‡ºã•ã‚Œã¾ã—ãŸã€‚nameè¦ç´ ã¯ã“ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã§ã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +webXml.unrecognisedPublicId=public ID [{0}]ã¯ã€æ—¢çŸ¥ã®web.xmlファイルã®public IDã¨ä¸€è‡´ã—ãªã„ãŸã‚ã€ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’特定ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +webXml.version.unknown=[{0}] ã¯æœªçŸ¥ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³æ–‡å­—列ã§ã™ã€‚既定値を使用ã—ã¾ã™ã€‚ +webXml.wrongFragmentName=web.xmlã®absolute-orderingã‚¿ã‚°ã§é–“é•ã£ãŸãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆå[{0}]を使用ã—ã¾ã—ãŸï¼ + +webXmlParser.applicationParse=アプリケーションweb.xmlファイルã®ä½ç½® [{0}] ã®è§£æžã‚¨ãƒ©ãƒ¼ +webXmlParser.applicationPosition=è¡Œ[{0}]列[{1}]ã§ç™ºç”Ÿã—ã¾ã—ãŸã€‚ +webXmlParser.applicationStart=[{0}]ã®ã‚¢ãƒ—リケーションweb.xmlファイルã®è§£æž diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ko.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ko.properties new file mode 100644 index 0000000..a4f774b --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ko.properties @@ -0,0 +1,61 @@ +# 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. + +filterDef.invalidFilterName=유효하지 ì•Šì€ . [{0}]ì´(ê°€) í•„í„° ì •ì˜ì— í¬í•¨ë˜ì–´ 있습니다. + +securityConstraint.uncoveredHttpMethod=URL 패턴 [{0}]와(ê³¼) 함께 설정ë˜ëŠ” security constraintë“¤ì— ëŒ€í•´, HTTP 메소드들 [{1}]ë§Œì´ ì»¤ë²„ë©ë‹ˆë‹¤. 다른 모든 ë©”ì†Œë“œë“¤ì€ ì»¤ë²„ë˜ì§€ 않습니다. +securityConstraint.uncoveredHttpMethodFix=[{1}] ì¤‘ì˜ í•˜ë‚˜ê°€ ì•„ë‹Œ, ì»¤ë²„ë  ìˆ˜ 없는 HTTP ë©”ì†Œë“œë“¤ì„ ì‚¬ìš©í•œ ì ‘ê·¼ì„ ê±°ë¶€í•˜ê¸° 위하여, URL íŒ¨í„´ì´ [{0}]ì¸ security constraintë“¤ì„ ì¶”ê°€í•©ë‹ˆë‹¤. +securityConstraint.uncoveredHttpOmittedMethod=URL 패턴 [{0}]와(ê³¼) 함께 ì„¤ì •ëœ security constraintë“¤ì— ëŒ€í•˜ì—¬, HTTP 메소드들 [{1}]ì€(는) 커버ë˜ì§€ 않습니다. +securityConstraint.uncoveredHttpOmittedMethodFix=ì»¤ë²„ë  ìˆ˜ 없는 HTTP 메소드들 [{1}]ì— ëŒ€í•œ ì ‘ê·¼ì„ ê±°ë¶€í•˜ê¸° 위하여, URL 패턴 [{0}]와(ê³¼) 함께 security constraint를 추가합니다. + +servletDef.invalidServletName=서블릿 ì •ì˜ì—ì„œ 유효하지 ì•Šì€ [{0}] + +webRuleSet.absoluteOrdering= 엘리먼트는 web-fragment.xml ë‚´ì—ì„œ 유효하지 않으므로, ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +webRuleSet.absoluteOrderingCount= 엘리먼트는 1회만 나타나ë„ë¡ ì œí•œë©ë‹ˆë‹¤. +webRuleSet.nameCount= 엘리먼트는 단 한번만 나타나ë„ë¡ ì œí•œë©ë‹ˆë‹¤. +webRuleSet.postconstruct.duplicate=í´ëž˜ìŠ¤ [{0}]ì— PostConstruct 메소드가 중복 ì •ì˜ë˜ì–´ 있습니다. +webRuleSet.predestroy.duplicate=í´ëž˜ìŠ¤ [{0}]ì— ì¤‘ë³µëœ @PreDestroy 메소드 ì •ì˜ +webRuleSet.relativeOrdering=web.xml ë‚´ì—ì„œ 유효하지 ì•Šì•„ì„œ, 해당 엘리먼트는 ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +webRuleSet.relativeOrderingCount= 엘리먼트는 ì˜¤ì§ í•œë²ˆë§Œ 나타나ë„ë¡ ì œí•œë©ë‹ˆë‹¤. + +webXml.duplicateEnvEntry=ì¤‘ë³µëœ env-entry ì´ë¦„ [{0}] +webXml.duplicateFilter=ì¤‘ë³µëœ í•„í„° ì´ë¦„: [{0}] +webXml.duplicateFragment=ì´ë¦„ì´ [{0}]ì¸, 둘 ì´ìƒì˜ fragmentë“¤ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ëŠ” ìƒëŒ€ì  순서배열ì—ì„œ 불허ë©ë‹ˆë‹¤. ìƒì„¸ 정보는 서블릿 스펙 8.2.2 2c ìž¥ì„ ì°¸ì¡°í•˜ì‹­ì‹œì˜¤. ì ˆëŒ€ì  ìˆœì„œë°°ì—´ì„ ì‚¬ìš©í•˜ëŠ” ê²ƒì„ ê³ ë ¤í•´ 보십시오. +webXml.duplicateMessageDestination=ì¤‘ë³µëœ message-destination ì´ë¦„ [{0}] +webXml.duplicateMessageDestinationRef=ì¤‘ë³µëœ message-destination-ref ì´ë¦„ [{0}] +webXml.duplicateResourceEnvRef=ì¤‘ë³µëœ resource-env-ref ì´ë¦„ [{0}]입니다. +webXml.duplicateResourceRef=ì¤‘ë³µëœ resource-ref ì´ë¦„: [{0}] +webXml.duplicateServletMapping=ì´ë¦„ì´ [{0}]ê³¼ [{1}]ì¸ ë‘ ì„œë¸”ë¦¿ë“¤ ëª¨ë‘ url-pattern [{2}]ì— ë§¤í•‘ë˜ì–´ 있는ë°, ì´ëŠ” 허용ë˜ì§€ 않습니다. +webXml.duplicateTaglibUri=ì¤‘ë³µëœ íƒœê·¸ ë¼ì´ë¸ŒëŸ¬ë¦¬ URI [{0}] +webXml.mergeConflictDisplayName=표시 ì´ë¦„ì´, 위치가 [{1}]ì´ê³  ì´ë¦„ì´ [{0}]ì¸ fragment를 í¬í•¨í•˜ì—¬, 여러 ê°œì˜ fragmentë“¤ì— ë‹¤ë¥¸ 값들로 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.mergeConflictFilter=í•„í„° [{0}]ì´(ê°€), [{2}]ì— ìœ„ì¹˜í•œ [{1}](ì´)ë¼ëŠ” ì´ë¦„ì„ ê°€ì§„ fragment를 í¬í•¨í•˜ì—¬, 여러 ê°œì˜ fragment들ì—ì„œ ì¼ê´€ë˜ì§€ 않게 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.mergeConflictLoginConfig=LoginConfigê°€, [{1}]ì— ìœ„ì¹˜í•˜ê³  ì´ë¦„ì´ [{0}]ì¸ fragment를 í¬í•¨í•˜ì—¬, 여러 ê°œì˜ fragmentë“¤ì— ì¼ê´€ë˜ì§€ 않게 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.mergeConflictOrder=Fragmentì˜ ìƒëŒ€ì  ìˆœì„œë“¤ì´ ìˆœí™˜ 참조를 í¬í•¨í•˜ê³  있습니다. ì´ ë¬¸ì œëŠ” web.xmlì—ì„œ 절대 순서를 ì‚¬ìš©í•¨ìœ¼ë¡œì¨ í•´ê²°ë  ìˆ˜ 있습니다. +webXml.mergeConflictResource=리소스 [{0}]ì€(는), ì´ë¦„ì´ [{1}](ì´)ê³  [{2}]ì— ìœ„ì¹˜í•œ fragment를 í¬í•¨í•˜ì—¬, 여러 ê°œì˜ fragment들ì—ì„œ ì¼ê´€ë˜ì§€ 않게 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.mergeConflictServlet=서블릿 [{0}]ì´(ê°€), [{2}]ì— ìœ„ì¹˜í•˜ê³  [{1}](ì´)ë¼ëŠ” ì´ë¦„ì„ ê°€ì§„ fragment를 í¬í•¨í•˜ì—¬, 여러 ê°œì˜ fragment들ì—ì„œ ì¼ê´€ë˜ì§€ 않게 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.mergeConflictSessionCookieAttributes=세션 쿠키 ì†ì„±ë“¤ì´ 복수 ê°œì˜ í”„ëž˜ê·¸ë¨¼íŠ¸ë“¤ì—ì„œ ì¼ê´€ë˜ì§€ 않게 다른 값들로 ì •ì˜ë˜ì–´ 있습니다. 가령 [{1}]ì— ìœ„ì¹˜í•œ ì´ë¦„ [{0}](ì„)를 í¬í•¨í•´ì„œ. +webXml.mergeConflictSessionCookieName=세션 쿠키 ì´ë¦„ì´, [{1}]ì— ìœ„ì¹˜í•˜ê³  [{0}](ì´)ë¼ëŠ” ì´ë¦„ì„ ê°€ì§„ fragment를 í¬í•¨í•˜ì—¬, 여러 ê°œì˜ fragment들ì—ì„œ 다른 값들로 ì¼ê´€ë˜ì§€ 않게 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.mergeConflictSessionTimeout=세션 제한 시간 초과 ê°’ì´, [{1}]ì— ìœ„ì¹˜í•˜ê³  ì´ë¦„ì´ [{0}]ì¸ fragment를 í¬í•¨í•˜ì—¬, 여러 ê°œì˜ fragmentë“¤ì— ë‹¤ë¥¸ 값들로 ì¼ê´€ë˜ì§€ 않게 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.mergeConflictSessionTrackingMode=세션 트랙킹 모드 설정들ì´, [{1}]ì— ìœ„ì¹˜í•˜ê³  [{0}](ì´)ë¼ëŠ” ì´ë¦„ì˜ fragment를 í¬í•¨í•˜ì—¬, 여러 fragmentë“¤ì— ì¼ê´€ë˜ì§€ 않게 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.mergeConflictString=ì´ë¦„ì´ [{1}]ì¸ [{0}]ì´(ê°€), [{3}]ì— ìœ„ì¹˜í•˜ê³  ì´ë¦„ì´ [{2}]ì¸ fragment를 í¬í•¨í•˜ì—¬, 여러 ê°œì˜ fragment들ì—ì„œ ì¼ê´€ë˜ì§€ 않게 ì •ì˜ë˜ì—ˆìŠµë‹ˆë‹¤. +webXml.multipleOther= 엘리먼트 ì•ˆì— ë‚´ìž¬ëœ, 여러 ê°œì˜ ì—”íŠ¸ë¦¬ë“¤ +webXml.reservedName=ì˜ˆì•½ëœ ì´ë¦„ [{0}]ì„(를) 사용한 web.xmlì´ íƒì§€ë˜ì—ˆìŠµë‹ˆë‹¤. name 엘리먼트는 ì´ fragment를 위해 ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +webXml.unrecognisedPublicId=public ID [{0}]ì´(ê°€), web.xml 파ì¼ë“¤ì„ 위해 알려진 public ID들 중 ì–´ë–¤ ê²ƒê³¼ë„ ë¶€í•©ë˜ì§€ ì•Šì•„, 해당 ë²„ì „ì„ ì•Œì•„ë‚¼ 수 없습니다. +webXml.version.unknown=ì•Œ 수 없는 버전 문ìžì—´ [{0}]. 기본 ë²„ì „ì´ ì‚¬ìš©ë  ê²ƒìž…ë‹ˆë‹¤. +webXml.wrongFragmentName=web.xmlì˜ absolute-ordering 태그ì—ì„œ ìž˜ëª»ëœ fragment ì´ë¦„, [{0}]ì´(ê°€) 사용ë˜ì—ˆìŠµë‹ˆë‹¤! + +webXmlParser.applicationParse=[{0}]ì— ìœ„ì¹˜í•œ 애플리케ì´ì…˜ web.xml ë‚´ì—ì„œ 파싱 오류 ë°œìƒ +webXmlParser.applicationPosition=í–‰ [{0}], ì—´ [{1}]ì—ì„œ ë°œìƒí–ˆìŒ +webXmlParser.applicationStart=[{0}]ì— ìœ„ì¹˜í•œ 애플리케ì´ì…˜ web.xmlì„ íŒŒì‹±í•©ë‹ˆë‹¤. diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..c9a5a76 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +webXml.mergeConflictSessionTrackingMode=O modo de rastreio de sessão foi definido inconsistentemente em múltiplos fragmentos incluindo o fragmento com nome [{0}] localizado em [{1}] diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ru.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ru.properties new file mode 100644 index 0000000..a4339ff --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_ru.properties @@ -0,0 +1,20 @@ +# 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. + +filterDef.invalidFilterName=Ðекорректный [{0}] в определении фильтра. + +webXml.duplicateFilter=Дублированное Ð¸Ð¼Ñ Ñ„Ð¸Ð»ÑŒÑ‚Ñ€Ð° [{0}] +webXml.duplicateServletMapping=Сервлеты названные [{0}] и [{1}] оба привÑзаны к URL-ратерну [{2}], что не разрешено +webXml.multipleOther=ÐеÑколько Ñлементов вложено в Ñлемент diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..a3b35e3 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings_zh_CN.properties @@ -0,0 +1,61 @@ +# 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. + +filterDef.invalidFilterName=过滤器定义中的 [{0}] 无效。 + +securityConstraint.uncoveredHttpMethod=对于URL模å¼ä¸º[{0}]的安全约æŸï¼ŒåªåŒ…括HTTP方法[{1}]。所有其他方法都没有涉åŠã€‚ +securityConstraint.uncoveredHttpMethodFix=添加URL模å¼ä¸º[{0}]的安全约æŸï¼Œä»¥æ‹’ç»ä½¿ç”¨æœªè¦†ç›–çš„HTTP方法进行访问,这些方法ä¸æ˜¯ä¸‹åˆ—方法之一[{1}]。 +securityConstraint.uncoveredHttpOmittedMethod=对于URL模å¼[{0}]的安全性约æŸï¼Œå°†å‘现HTTP方法[{1}]。 +securityConstraint.uncoveredHttpOmittedMethodFix=添加url模å¼ä¸º[{0}]的安全约æŸä»¥æ‹’ç»ä½¿ç”¨æœªè¦†ç›–çš„http方法[{1}]的访问 + +servletDef.invalidServletName=servlet定义中的[{0}]无效。 + +webRuleSet.absoluteOrdering=<ç»å¯¹å€¼æŽ’åº>元素在web片段xml中无效,将被忽略。 +webRuleSet.absoluteOrderingCount=元素é™åˆ¶ä¸º1次出现 +webRuleSet.nameCount=元素åªèƒ½å‡ºçŽ°1次 +webRuleSet.postconstruct.duplicate=class [{0}] 有é‡å¤çš„ post 构造方法声明 +webRuleSet.predestroy.duplicate=ç±»[{0}]çš„@PreDestroy方法定义é‡å¤ +webRuleSet.relativeOrdering=元素在web.xml中无效,将被忽略 +webRuleSet.relativeOrderingCount=元素é™åˆ¶ä¸º1次出现。 + +webXml.duplicateEnvEntry=é‡å¤çš„env-entry å [{0}] +webXml.duplicateFilter=é‡å¤çš„过滤器å称 [{0}] +webXml.duplicateFragment=找到多个å为[{0}]的片段。这是ä¸åˆæ³•çš„相对排åºã€‚有关详细信æ¯ï¼Œè¯·å‚阅Servlet规范的第8.2.2 2c节。考虑使用ç»å¯¹æŽ’åºã€‚ +webXml.duplicateMessageDestination=é‡å¤çš„邮件目标å称[{0}]。 +webXml.duplicateMessageDestinationRef=é‡å¤çš„消æ¯ç›®æ ‡å¼•ç”¨å称[{0}] +webXml.duplicateResourceEnvRef=é‡å¤çš„资æºenv ref name[{0}] +webXml.duplicateResourceRef=é‡å¤çš„资æºå¼•ç”¨å称[{0}] +webXml.duplicateServletMapping=å为 [{0}]å’Œ [{1}] çš„servletä¸èƒ½æ˜ å°„为一个url模å¼(url-pattern) [{2}] +webXml.duplicateTaglibUri=é‡å¤çš„标记库URI[{0}] +webXml.mergeConflictDisplayName=显示å称在多个片段中被定义,这些片段包å«ä¸åŒçš„值,包括ä½äºŽ[{1}]çš„[{0}]的片段。 +webXml.mergeConflictFilter=筛选器[{0}]在多个片段中定义ä¸ä¸€è‡´ï¼ŒåŒ…括ä½äºŽ[{2}]çš„å为[{1}]的片段 +webXml.mergeConflictLoginConfig=在多个片段中定义的LoginConfigä¸ä¸€è‡´ï¼Œå…¶ä¸­åŒ…括ä½äºŽ[{1}]çš„å为[{0}]的片段 +webXml.mergeConflictOrder=片段相对顺åºåŒ…å«å¾ªçŽ¯å¼•ç”¨ã€‚è¿™å¯ä»¥é€šè¿‡åœ¨web.xml中使用ç»å¯¹æŽ’åºæ¥è§£å†³ã€‚ +webXml.mergeConflictResource=资æº[{0}]在多个片段中定义ä¸ä¸€è‡´ï¼ŒåŒ…括ä½äºŽ[{2}]çš„å为[{1}]的片段。 +webXml.mergeConflictServlet=Servlet[{0}]在多个片段中的定义ä¸ä¸€è‡´ï¼ŒåŒ…括ä½äºŽ[{2}]çš„å为[{1}]的片段 +webXml.mergeConflictSessionCookieAttributes=ä¼šè¯ cookie 属性在具有ä¸åŒå€¼çš„多个片段中定义ä¸ä¸€è‡´ï¼ŒåŒ…括ä½äºŽ [{1}] çš„å称为 [{0}] 的片段 +webXml.mergeConflictSessionCookieName=会è¯cookieå称在多个具有ä¸åŒå€¼çš„片段中定义ä¸ä¸€è‡´ï¼ŒåŒ…括ä½äºŽ [{1}] 的片段 [{0}] +webXml.mergeConflictSessionTimeout=会è¯è¶…时以ä¸åŒå€¼çš„多个片段ä¸ä¸€è‡´åœ°å®šä¹‰ï¼Œè¿™äº›ç‰‡æ®µåŒ…括ä½äºŽ[{1}]的具有å称[{0}]的片段。 +webXml.mergeConflictSessionTrackingMode=会è¯è·Ÿè¸ªæ¨¡å¼åœ¨å¤šä¸ªç‰‡æ®µä¸­å®šä¹‰ä¸ä¸€è‡´ï¼ŒåŒ…括ä½äºŽ[{1}]çš„å称为[{0}]的片段 +webXml.mergeConflictString=å称为[{1}]çš„[{0}]在多个片段中定义ä¸ä¸€è‡´ï¼ŒåŒ…括ä½äºŽ[{3}]çš„å称为[{2}]的片段 +webXml.multipleOther=嵌套在元素中的多个æ¡ç›® +webXml.reservedName=使用ä¿ç•™å称[{0}]检测到web.xml文件。 此片段将忽略name元素。 +webXml.unrecognisedPublicId=对于web.xml文件,公共ID[{0}]ä¸åŒ¹é…任何已知的公共ID‘,因此无法识别版本。 +webXml.version.unknown=未知版本字符串 [{0}]。将使用默认版本。 +webXml.wrongFragmentName=在web.xmlç»å¯¹æŽ’åºæ ‡ç­¾ä¸Šä½¿ç”¨äº†é”™è¯¯çš„片段å[{0}]ï¼ + +webXmlParser.applicationParse=解æžåº”用web.xml错误,路径:[{0}] +webXmlParser.applicationPosition=出现在第 [{0}] è¡Œ 第 [{1}] 列 +webXmlParser.applicationStart=正在分æžä½äºŽ[{0}]的应用程åºweb.xml文件 diff --git a/java/org/apache/tomcat/util/descriptor/web/LoginConfig.java b/java/org/apache/tomcat/util/descriptor/web/LoginConfig.java new file mode 100644 index 0000000..61332aa --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LoginConfig.java @@ -0,0 +1,226 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; + +import org.apache.tomcat.util.buf.UDecoder; + +/** + * Representation of a login configuration element for a web application, + * as represented in a <login-config> element in the + * deployment descriptor. + * + * @author Craig R. McClanahan + */ +public class LoginConfig extends XmlEncodingBase implements Serializable { + + + private static final long serialVersionUID = 2L; + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new LoginConfig with default properties. + */ + public LoginConfig() { + + super(); + + } + + + /** + * Construct a new LoginConfig with the specified properties. + * + * @param authMethod The authentication method + * @param realmName The realm name + * @param loginPage The login page URI + * @param errorPage The error page URI + */ + public LoginConfig(String authMethod, String realmName, + String loginPage, String errorPage) { + + super(); + setAuthMethod(authMethod); + setRealmName(realmName); + setLoginPage(loginPage); + setErrorPage(errorPage); + + } + + + // ------------------------------------------------------------- Properties + + + /** + * The authentication method to use for application login. Must be + * BASIC, DIGEST, FORM, or CLIENT-CERT. + */ + private String authMethod = null; + + public String getAuthMethod() { + return this.authMethod; + } + + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + + /** + * The context-relative URI of the error page for form login. + */ + private String errorPage = null; + + public String getErrorPage() { + return this.errorPage; + } + + public void setErrorPage(String errorPage) { + // if ((errorPage == null) || !errorPage.startsWith("/")) + // throw new IllegalArgumentException + // ("Error Page resource path must start with a '/'"); + this.errorPage = UDecoder.URLDecode(errorPage, getCharset()); + } + + + /** + * The context-relative URI of the login page for form login. + */ + private String loginPage = null; + + public String getLoginPage() { + return this.loginPage; + } + + public void setLoginPage(String loginPage) { + // if ((loginPage == null) || !loginPage.startsWith("/")) + // throw new IllegalArgumentException + // ("Login Page resource path must start with a '/'"); + this.loginPage = UDecoder.URLDecode(loginPage, getCharset()); + } + + + /** + * The realm name used when challenging the user for authentication + * credentials. + */ + private String realmName = null; + + public String getRealmName() { + return this.realmName; + } + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("LoginConfig["); + sb.append("authMethod="); + sb.append(authMethod); + if (realmName != null) { + sb.append(", realmName="); + sb.append(realmName); + } + if (loginPage != null) { + sb.append(", loginPage="); + sb.append(loginPage); + } + if (errorPage != null) { + sb.append(", errorPage="); + sb.append(errorPage); + } + sb.append(']'); + return sb.toString(); + } + + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((authMethod == null) ? 0 : authMethod.hashCode()); + result = prime * result + + ((errorPage == null) ? 0 : errorPage.hashCode()); + result = prime * result + + ((loginPage == null) ? 0 : loginPage.hashCode()); + result = prime * result + + ((realmName == null) ? 0 : realmName.hashCode()); + return result; + } + + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof LoginConfig)) { + return false; + } + LoginConfig other = (LoginConfig) obj; + if (authMethod == null) { + if (other.authMethod != null) { + return false; + } + } else if (!authMethod.equals(other.authMethod)) { + return false; + } + if (errorPage == null) { + if (other.errorPage != null) { + return false; + } + } else if (!errorPage.equals(other.errorPage)) { + return false; + } + if (loginPage == null) { + if (other.loginPage != null) { + return false; + } + } else if (!loginPage.equals(other.loginPage)) { + return false; + } + if (realmName == null) { + if (other.realmName != null) { + return false; + } + } else if (!realmName.equals(other.realmName)) { + return false; + } + return true; + } + + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/MessageDestination.java b/java/org/apache/tomcat/util/descriptor/web/MessageDestination.java new file mode 100644 index 0000000..d323a76 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/MessageDestination.java @@ -0,0 +1,158 @@ +/* + * 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.tomcat.util.descriptor.web; + + +/** + *

    Representation of a message destination for a web application, as + * represented in a <message-destination> element + * in the deployment descriptor.

    + * + * @author Craig R. McClanahan + * @since Tomcat 5.0 + */ +public class MessageDestination extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + + /** + * The display name of this destination. + */ + private String displayName = null; + + public String getDisplayName() { + return this.displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + + /** + * The large icon of this destination. + */ + private String largeIcon = null; + + public String getLargeIcon() { + return this.largeIcon; + } + + public void setLargeIcon(String largeIcon) { + this.largeIcon = largeIcon; + } + + + /** + * The small icon of this destination. + */ + private String smallIcon = null; + + public String getSmallIcon() { + return this.smallIcon; + } + + public void setSmallIcon(String smallIcon) { + this.smallIcon = smallIcon; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MessageDestination["); + sb.append("name="); + sb.append(getName()); + if (displayName != null) { + sb.append(", displayName="); + sb.append(displayName); + } + if (largeIcon != null) { + sb.append(", largeIcon="); + sb.append(largeIcon); + } + if (smallIcon != null) { + sb.append(", smallIcon="); + sb.append(smallIcon); + } + if (getDescription() != null) { + sb.append(", description="); + sb.append(getDescription()); + } + sb.append(']'); + return sb.toString(); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + + ((displayName == null) ? 0 : displayName.hashCode()); + result = prime * result + + ((largeIcon == null) ? 0 : largeIcon.hashCode()); + result = prime * result + + ((smallIcon == null) ? 0 : smallIcon.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MessageDestination other = (MessageDestination) obj; + if (displayName == null) { + if (other.displayName != null) { + return false; + } + } else if (!displayName.equals(other.displayName)) { + return false; + } + if (largeIcon == null) { + if (other.largeIcon != null) { + return false; + } + } else if (!largeIcon.equals(other.largeIcon)) { + return false; + } + if (smallIcon == null) { + if (other.smallIcon != null) { + return false; + } + } else if (!smallIcon.equals(other.smallIcon)) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/MessageDestinationRef.java b/java/org/apache/tomcat/util/descriptor/web/MessageDestinationRef.java new file mode 100644 index 0000000..5118898 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/MessageDestinationRef.java @@ -0,0 +1,132 @@ +/* + * 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.tomcat.util.descriptor.web; + + +/** + *

    Representation of a message destination reference for a web application, + * as represented in a <message-destination-ref> element + * in the deployment descriptor.

    + * + * @author Craig R. McClanahan + * @since Tomcat 5.0 + */ +public class MessageDestinationRef extends ResourceBase { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + + + /** + * The link of this destination ref. + */ + private String link = null; + + public String getLink() { + return this.link; + } + + public void setLink(String link) { + this.link = link; + } + + + /** + * The usage of this destination ref. + */ + private String usage = null; + + public String getUsage() { + return this.usage; + } + + public void setUsage(String usage) { + this.usage = usage; + } + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MessageDestination["); + sb.append("name="); + sb.append(getName()); + if (link != null) { + sb.append(", link="); + sb.append(link); + } + if (getType() != null) { + sb.append(", type="); + sb.append(getType()); + } + if (usage != null) { + sb.append(", usage="); + sb.append(usage); + } + if (getDescription() != null) { + sb.append(", description="); + sb.append(getDescription()); + } + sb.append(']'); + return sb.toString(); + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((link == null) ? 0 : link.hashCode()); + result = prime * result + ((usage == null) ? 0 : usage.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MessageDestinationRef other = (MessageDestinationRef) obj; + if (link == null) { + if (other.link != null) { + return false; + } + } else if (!link.equals(other.link)) { + return false; + } + if (usage == null) { + if (other.usage != null) { + return false; + } + } else if (!usage.equals(other.usage)) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/MultipartDef.java b/java/org/apache/tomcat/util/descriptor/web/MultipartDef.java new file mode 100644 index 0000000..ba75689 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/MultipartDef.java @@ -0,0 +1,136 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; + + +/** + * Representation of a the multipart configuration for a servlet. + */ +public class MultipartDef implements Serializable { + + private static final long serialVersionUID = 1L; + + // ------------------------------------------------------------- Properties + private String location; + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + + private String maxFileSize; + + public String getMaxFileSize() { + return maxFileSize; + } + + public void setMaxFileSize(String maxFileSize) { + this.maxFileSize = maxFileSize; + } + + + private String maxRequestSize; + + public String getMaxRequestSize() { + return maxRequestSize; + } + + public void setMaxRequestSize(String maxRequestSize) { + this.maxRequestSize = maxRequestSize; + } + + + private String fileSizeThreshold; + + public String getFileSizeThreshold() { + return fileSizeThreshold; + } + + public void setFileSizeThreshold(String fileSizeThreshold) { + this.fileSizeThreshold = fileSizeThreshold; + } + + + // ---------------------------------------------------------- Object methods + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime + * result + + ((fileSizeThreshold == null) ? 0 : fileSizeThreshold + .hashCode()); + result = prime * result + + ((location == null) ? 0 : location.hashCode()); + result = prime * result + + ((maxFileSize == null) ? 0 : maxFileSize.hashCode()); + result = prime * result + + ((maxRequestSize == null) ? 0 : maxRequestSize.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof MultipartDef)) { + return false; + } + MultipartDef other = (MultipartDef) obj; + if (fileSizeThreshold == null) { + if (other.fileSizeThreshold != null) { + return false; + } + } else if (!fileSizeThreshold.equals(other.fileSizeThreshold)) { + return false; + } + if (location == null) { + if (other.location != null) { + return false; + } + } else if (!location.equals(other.location)) { + return false; + } + if (maxFileSize == null) { + if (other.maxFileSize != null) { + return false; + } + } else if (!maxFileSize.equals(other.maxFileSize)) { + return false; + } + if (maxRequestSize == null) { + if (other.maxRequestSize != null) { + return false; + } + } else if (!maxRequestSize.equals(other.maxRequestSize)) { + return false; + } + return true; + } + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/NamingResources.java b/java/org/apache/tomcat/util/descriptor/web/NamingResources.java new file mode 100644 index 0000000..b2162ef --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/NamingResources.java @@ -0,0 +1,40 @@ +/* + * 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.tomcat.util.descriptor.web; + + + +/** + * Defines an interface for the object that is added to the representation of a + * JNDI resource in web.xml to enable it to also be the implementation of that + * JNDI resource. Only Catalina implements this interface but because the + * web.xml representation is shared this interface has to be visible to Catalina + * and Jasper. + */ +public interface NamingResources { + + void addEnvironment(ContextEnvironment ce); + void removeEnvironment(String name); + + void addResource(ContextResource cr); + void removeResource(String name); + + void addResourceLink(ContextResourceLink crl); + void removeResourceLink(String name); + + Object getContainer(); +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ResourceBase.java b/java/org/apache/tomcat/util/descriptor/web/ResourceBase.java new file mode 100644 index 0000000..37ec8fb --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ResourceBase.java @@ -0,0 +1,234 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * Representation of an Context element + * + * @author Peter Rossbach (pero@apache.org) + */ +public class ResourceBase implements Serializable, Injectable { + + private static final long serialVersionUID = 1L; + + + // ------------------------------------------------------------- Properties + + /** + * The description of this resource. + */ + private String description = null; + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + + + /** + * The name of this resource. + */ + private String name = null; + + @Override + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + + /** + * The name of the resource implementation class. + */ + private String type = null; + + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + + private String lookupName = null; + + public String getLookupName() { + return lookupName; + } + + public void setLookupName(String lookupName) { + if (lookupName == null || lookupName.length() == 0) { + this.lookupName = null; + return; + } + this.lookupName = lookupName; + } + + + /** + * Holder for our configured properties. + */ + private final Map properties = new HashMap<>(); + + /** + * @param name The property name + * @return a configured property. + */ + public Object getProperty(String name) { + return properties.get(name); + } + + /** + * Set a configured property. + * @param name The property name + * @param value The property value + */ + public void setProperty(String name, Object value) { + properties.put(name, value); + } + + /** + * Remove a configured property. + * @param name The property name + */ + public void removeProperty(String name) { + properties.remove(name); + } + + /** + * List properties. + * @return the property names iterator + */ + public Iterator listProperties() { + return properties.keySet().iterator(); + } + + private final List injectionTargets = new ArrayList<>(); + + @Override + public void addInjectionTarget(String injectionTargetName, String jndiName) { + InjectionTarget target = new InjectionTarget(injectionTargetName, jndiName); + injectionTargets.add(target); + } + + @Override + public List getInjectionTargets() { + return injectionTargets; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((injectionTargets == null) ? 0 : injectionTargets.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((properties == null) ? 0 : properties.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((lookupName == null) ? 0 : lookupName.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ResourceBase other = (ResourceBase) obj; + if (description == null) { + if (other.description != null) { + return false; + } + } else if (!description.equals(other.description)) { + return false; + } + if (injectionTargets == null) { + if (other.injectionTargets != null) { + return false; + } + } else if (!injectionTargets.equals(other.injectionTargets)) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + if (properties == null) { + if (other.properties != null) { + return false; + } + } else if (!properties.equals(other.properties)) { + return false; + } + if (type == null) { + if (other.type != null) { + return false; + } + } else if (!type.equals(other.type)) { + return false; + } + if (lookupName == null) { + if (other.lookupName != null) { + return false; + } + } else if (!lookupName.equals(other.lookupName)) { + return false; + } + return true; + } + + + /** + * The NamingResources with which we are associated (if any). + */ + private NamingResources resources = null; + + public NamingResources getNamingResources() { + return this.resources; + } + + public void setNamingResources(NamingResources resources) { + this.resources = resources; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/SecurityCollection.java b/java/org/apache/tomcat/util/descriptor/web/SecurityCollection.java new file mode 100644 index 0000000..6cbde72 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/SecurityCollection.java @@ -0,0 +1,407 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.apache.tomcat.util.buf.UDecoder; + + +/** + * Representation of a web resource collection for a web application's security + * constraint, as represented in a <web-resource-collection> + * element in the deployment descriptor. + *

    + * WARNING: It is assumed that instances of this class will be created + * and modified only within the context of a single thread, before the instance + * is made visible to the remainder of the application. After that, only read + * access is expected. Therefore, none of the read and write access within + * this class is synchronized. + * + * @author Craig R. McClanahan + */ +public class SecurityCollection extends XmlEncodingBase implements Serializable { + + private static final long serialVersionUID = 1L; + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new security collection instance with default values. + */ + public SecurityCollection() { + + this(null, null); + + } + + + /** + * Construct a new security collection instance with specified values. + * + * @param name Name of this security collection + * @param description Description of this security collection + */ + public SecurityCollection(String name, String description) { + + super(); + setName(name); + setDescription(description); + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Description of this web resource collection. + */ + private String description = null; + + + /** + * The HTTP methods explicitly covered by this web resource collection. + */ + private String methods[] = new String[0]; + + + /** + * The HTTP methods explicitly excluded from this web resource collection. + */ + private String omittedMethods[] = new String[0]; + + /** + * The name of this web resource collection. + */ + private String name = null; + + + /** + * The URL patterns protected by this security collection. + */ + private String patterns[] = new String[0]; + + + /** + * This security collection was established by a deployment descriptor. + * Defaults to true. + */ + private boolean isFromDescriptor = true; + + // ------------------------------------------------------------- Properties + + + /** + * @return the description of this web resource collection. + */ + public String getDescription() { + return this.description; + } + + + /** + * Set the description of this web resource collection. + * + * @param description The new description + */ + public void setDescription(String description) { + this.description = description; + } + + + /** + * @return the name of this web resource collection. + */ + public String getName() { + return this.name; + } + + + /** + * Set the name of this web resource collection + * + * @param name The new name + */ + public void setName(String name) { + this.name = name; + } + + + /** + * @return if this constraint was defined in a deployment descriptor. + */ + public boolean isFromDescriptor() { + return isFromDescriptor; + } + + + /** + * Set if this constraint was defined in a deployment descriptor. + * @param isFromDescriptor true was declared in a descriptor + */ + public void setFromDescriptor(boolean isFromDescriptor) { + this.isFromDescriptor = isFromDescriptor; + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add an HTTP request method to be explicitly part of this web resource + * collection. + * @param method The method + */ + public void addMethod(String method) { + + if (method == null) { + return; + } + String[] results = Arrays.copyOf(methods, methods.length + 1); + results[methods.length] = method; + methods = results; + + } + + + /** + * Add an HTTP request method to the methods explicitly excluded from this + * web resource collection. + * @param method The method + */ + public void addOmittedMethod(String method) { + if (method == null) { + return; + } + String[] results = Arrays.copyOf(omittedMethods, omittedMethods.length + 1); + results[omittedMethods.length] = method; + omittedMethods = results; + } + + /** + * Add a URL pattern to be part of this web resource collection. + * @param pattern The pattern + */ + public void addPattern(String pattern) { + addPatternDecoded(UDecoder.URLDecode(pattern, StandardCharsets.UTF_8)); + } + public void addPatternDecoded(String pattern) { + + if (pattern == null) { + return; + } + + String decodedPattern = UDecoder.URLDecode(pattern, getCharset()); + String[] results = Arrays.copyOf(patterns, patterns.length + 1); + results[patterns.length] = decodedPattern; + patterns = results; + } + + + /** + * Check if the collection applies to the specified method. + * @param method Request method to check + * @return true if the specified HTTP request method is + * part of this web resource collection. + */ + public boolean findMethod(String method) { + + if (methods.length == 0 && omittedMethods.length == 0) { + return true; + } + if (methods.length > 0) { + for (String s : methods) { + if (s.equals(method)) { + return true; + } + } + return false; + } + if (omittedMethods.length > 0) { + for (String omittedMethod : omittedMethods) { + if (omittedMethod.equals(method)) { + return false; + } + } + } + return true; + } + + + /** + * @return the set of HTTP request methods that are part of this web + * resource collection, or a zero-length array if no methods have been + * explicitly included. + */ + public String[] findMethods() { + return methods; + } + + + /** + * @return the set of HTTP request methods that are explicitly excluded from + * this web resource collection, or a zero-length array if no request + * methods are excluded. + */ + public String[] findOmittedMethods() { + return omittedMethods; + } + + + /** + * Is the specified pattern part of this web resource collection? + * + * @param pattern Pattern to be compared + * @return true if the pattern is part of the collection + */ + public boolean findPattern(String pattern) { + for (String s : patterns) { + if (s.equals(pattern)) { + return true; + } + } + return false; + } + + + /** + * @return the set of URL patterns that are part of this web resource + * collection. If none have been specified, a zero-length array is + * returned. + */ + public String[] findPatterns() { + return patterns; + } + + + /** + * Remove the specified HTTP request method from those that are part + * of this web resource collection. + * + * @param method Request method to be removed + */ + public void removeMethod(String method) { + + if (method == null) { + return; + } + int n = -1; + for (int i = 0; i < methods.length; i++) { + if (methods[i].equals(method)) { + n = i; + break; + } + } + if (n >= 0) { + int j = 0; + String results[] = new String[methods.length - 1]; + for (int i = 0; i < methods.length; i++) { + if (i != n) { + results[j++] = methods[i]; + } + } + methods = results; + } + + } + + + /** + * Remove the specified HTTP request method from those that are explicitly + * excluded from this web resource collection. + * + * @param method Request method to be removed + */ + public void removeOmittedMethod(String method) { + + if (method == null) { + return; + } + int n = -1; + for (int i = 0; i < omittedMethods.length; i++) { + if (omittedMethods[i].equals(method)) { + n = i; + break; + } + } + if (n >= 0) { + int j = 0; + String results[] = new String[omittedMethods.length - 1]; + for (int i = 0; i < omittedMethods.length; i++) { + if (i != n) { + results[j++] = omittedMethods[i]; + } + } + omittedMethods = results; + } + + } + + + /** + * Remove the specified URL pattern from those that are part of this + * web resource collection. + * + * @param pattern Pattern to be removed + */ + public void removePattern(String pattern) { + + if (pattern == null) { + return; + } + int n = -1; + for (int i = 0; i < patterns.length; i++) { + if (patterns[i].equals(pattern)) { + n = i; + break; + } + } + if (n >= 0) { + int j = 0; + String results[] = new String[patterns.length - 1]; + for (int i = 0; i < patterns.length; i++) { + if (i != n) { + results[j++] = patterns[i]; + } + } + patterns = results; + } + + } + + + /** + * Return a String representation of this security collection. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SecurityCollection["); + sb.append(name); + if (description != null) { + sb.append(", "); + sb.append(description); + } + sb.append(']'); + return sb.toString(); + } + + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/SecurityConstraint.java b/java/org/apache/tomcat/util/descriptor/web/SecurityConstraint.java new file mode 100644 index 0000000..b638ac6 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/SecurityConstraint.java @@ -0,0 +1,784 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.HttpConstraintElement; +import jakarta.servlet.HttpMethodConstraintElement; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic; + +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.res.StringManager; + + +/** + * Representation of a security constraint element for a web application, + * as represented in a <security-constraint> element in the + * deployment descriptor. + *

    + * WARNING: It is assumed that instances of this class will be created + * and modified only within the context of a single thread, before the instance + * is made visible to the remainder of the application. After that, only read + * access is expected. Therefore, none of the read and write access within + * this class is synchronized. + * + * @author Craig R. McClanahan + */ +public class SecurityConstraint extends XmlEncodingBase implements Serializable { + + private static final long serialVersionUID = 1L; + + public static final String ROLE_ALL_ROLES = "*"; + public static final String ROLE_ALL_AUTHENTICATED_USERS = "**"; + + private static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new security constraint instance with default values. + */ + public SecurityConstraint() { + super(); + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Was the "all roles" wildcard - {@link #ROLE_ALL_ROLES} - included in the + * authorization constraints for this security constraint? + */ + private boolean allRoles = false; + + + /** + * Was the "all authenticated users" wildcard - + * {@link #ROLE_ALL_AUTHENTICATED_USERS} - included in the authorization + * constraints for this security constraint? + */ + private boolean authenticatedUsers = false; + + + /** + * Was an authorization constraint included in this security constraint? + * This is necessary to distinguish the case where an auth-constraint with + * no roles (signifying no direct access at all) was requested, versus + * a lack of auth-constraint which implies no access control checking. + */ + private boolean authConstraint = false; + + + /** + * The set of roles permitted to access resources protected by this + * security constraint. + */ + private String authRoles[] = new String[0]; + + + /** + * The set of web resource collections protected by this security + * constraint. + */ + private SecurityCollection collections[] = new SecurityCollection[0]; + + + /** + * The display name of this security constraint. + */ + private String displayName = null; + + + /** + * The user data constraint for this security constraint. Must be NONE, + * INTEGRAL, or CONFIDENTIAL. + */ + private String userConstraint = "NONE"; + + + // ------------------------------------------------------------- Properties + + + /** + * Was the "all roles" wildcard included in this authentication + * constraint? + * @return true if all roles + */ + public boolean getAllRoles() { + + return this.allRoles; + + } + + + /** + * Was the "all authenticated users" wildcard included in this + * authentication constraint? + * @return true if all authenticated users + */ + public boolean getAuthenticatedUsers() { + return this.authenticatedUsers; + } + + + /** + * Return the authorization constraint present flag for this security + * constraint. + * @return true if this needs authorization + */ + public boolean getAuthConstraint() { + + return this.authConstraint; + + } + + + /** + * Set the authorization constraint present flag for this security + * constraint. + * @param authConstraint The new value + */ + public void setAuthConstraint(boolean authConstraint) { + + this.authConstraint = authConstraint; + + } + + + /** + * @return the display name of this security constraint. + */ + public String getDisplayName() { + + return this.displayName; + + } + + + /** + * Set the display name of this security constraint. + * @param displayName The new value + */ + public void setDisplayName(String displayName) { + + this.displayName = displayName; + + } + + + /** + * Return the user data constraint for this security constraint. + * @return the user constraint + */ + public String getUserConstraint() { + + return userConstraint; + + } + + + /** + * Set the user data constraint for this security constraint. + * + * @param userConstraint The new user data constraint + */ + public void setUserConstraint(String userConstraint) { + + if (userConstraint != null) { + this.userConstraint = userConstraint; + } + + } + + + /** + * Called in the unlikely event that an application defines a role named + * "**". + */ + public void treatAllAuthenticatedUsersAsApplicationRole() { + if (authenticatedUsers) { + authenticatedUsers = false; + + String[] results = Arrays.copyOf(authRoles, authRoles.length + 1); + results[authRoles.length] = ROLE_ALL_AUTHENTICATED_USERS; + authRoles = results; + authConstraint = true; + } + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add an authorization role, which is a role name that will be + * permitted access to the resources protected by this security constraint. + * + * @param authRole Role name to be added + */ + public void addAuthRole(String authRole) { + + if (authRole == null) { + return; + } + + if (ROLE_ALL_ROLES.equals(authRole)) { + allRoles = true; + return; + } + + if (ROLE_ALL_AUTHENTICATED_USERS.equals(authRole)) { + authenticatedUsers = true; + return; + } + + String[] results = Arrays.copyOf(authRoles, authRoles.length + 1); + results[authRoles.length] = authRole; + authRoles = results; + authConstraint = true; + } + + + @Override + public void setCharset(Charset charset) { + super.setCharset(charset); + for (SecurityCollection collection : collections) { + collection.setCharset(getCharset()); + } + } + + + /** + * Add a new web resource collection to those protected by this + * security constraint. + * + * @param collection The new web resource collection + */ + public void addCollection(SecurityCollection collection) { + + if (collection == null) { + return; + } + + collection.setCharset(getCharset()); + + SecurityCollection results[] = Arrays.copyOf(collections, collections.length + 1); + results[collections.length] = collection; + collections = results; + + } + + + /** + * Check a role. + * + * @param role Role name to be checked + * @return true if the specified role is permitted access to + * the resources protected by this security constraint. + */ + public boolean findAuthRole(String role) { + + if (role == null) { + return false; + } + for (String authRole : authRoles) { + if (role.equals(authRole)) { + return true; + } + } + return false; + + } + + + /** + * Return the set of roles that are permitted access to the resources + * protected by this security constraint. If none have been defined, + * a zero-length array is returned (which implies that all authenticated + * users are permitted access). + * @return the roles array + */ + public String[] findAuthRoles() { + return authRoles; + } + + + /** + * Return the web resource collection for the specified name, if any; + * otherwise, return null. + * + * @param name Web resource collection name to return + * @return the collection + */ + public SecurityCollection findCollection(String name) { + if (name == null) { + return null; + } + for (SecurityCollection collection : collections) { + if (name.equals(collection.getName())) { + return collection; + } + } + return null; + } + + + /** + * Return all of the web resource collections protected by this + * security constraint. If there are none, a zero-length array is + * returned. + * @return the collections array + */ + public SecurityCollection[] findCollections() { + return collections; + } + + + /** + * Check if the constraint applies to a URI and method. + * @param uri Context-relative URI to check + * @param method Request method being used + * @return true if the specified context-relative URI (and + * associated HTTP method) are protected by this security constraint. + */ + public boolean included(String uri, String method) { + + // We cannot match without a valid request method + if (method == null) { + return false; + } + + // Check all of the collections included in this constraint + for (SecurityCollection collection : collections) { + if (!collection.findMethod(method)) { + continue; + } + String patterns[] = collection.findPatterns(); + for (String pattern : patterns) { + if (matchPattern(uri, pattern)) { + return true; + } + } + } + + // No collection included in this constraint matches this request + return false; + + } + + + /** + * Remove the specified role from the set of roles permitted to access + * the resources protected by this security constraint. + * + * @param authRole Role name to be removed + */ + public void removeAuthRole(String authRole) { + + if (authRole == null) { + return; + } + + if (ROLE_ALL_ROLES.equals(authRole)) { + allRoles = false; + return; + } + + if (ROLE_ALL_AUTHENTICATED_USERS.equals(authRole)) { + authenticatedUsers = false; + return; + } + + int n = -1; + for (int i = 0; i < authRoles.length; i++) { + if (authRoles[i].equals(authRole)) { + n = i; + break; + } + } + if (n >= 0) { + int j = 0; + String results[] = new String[authRoles.length - 1]; + for (int i = 0; i < authRoles.length; i++) { + if (i != n) { + results[j++] = authRoles[i]; + } + } + authRoles = results; + } + } + + + /** + * Remove the specified web resource collection from those protected by + * this security constraint. + * + * @param collection Web resource collection to be removed + */ + public void removeCollection(SecurityCollection collection) { + + if (collection == null) { + return; + } + int n = -1; + for (int i = 0; i < collections.length; i++) { + if (collections[i].equals(collection)) { + n = i; + break; + } + } + if (n >= 0) { + int j = 0; + SecurityCollection results[] = + new SecurityCollection[collections.length - 1]; + for (int i = 0; i < collections.length; i++) { + if (i != n) { + results[j++] = collections[i]; + } + } + collections = results; + } + + } + + + /** + * Return a String representation of this security constraint. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SecurityConstraint["); + for (int i = 0; i < collections.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(collections[i].getName()); + } + sb.append(']'); + return sb.toString(); + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Does the specified request path match the specified URL pattern? + * This method follows the same rules (in the same order) as those used + * for mapping requests to servlets. + * + * @param path Context-relative request path to be checked + * (must start with '/') + * @param pattern URL pattern to be compared against + */ + private boolean matchPattern(String path, String pattern) { + + // Normalize the argument strings + if (path == null || path.length() == 0) { + path = "/"; + } + if (pattern == null || pattern.length() == 0) { + pattern = "/"; + } + + // Check for exact match + if (path.equals(pattern)) { + return true; + } + + // Check for path prefix matching + if (pattern.startsWith("/") && pattern.endsWith("/*")) { + pattern = pattern.substring(0, pattern.length() - 2); + if (pattern.length() == 0) + { + return true; // "/*" is the same as "/" + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + while (true) { + if (pattern.equals(path)) { + return true; + } + int slash = path.lastIndexOf('/'); + if (slash <= 0) { + break; + } + path = path.substring(0, slash); + } + return false; + } + + // Check for suffix matching + if (pattern.startsWith("*.")) { + int slash = path.lastIndexOf('/'); + int period = path.lastIndexOf('.'); + if ((slash >= 0) && (period > slash) && + path.endsWith(pattern.substring(1))) { + return true; + } + return false; + } + + // Check for universal mapping + if (pattern.equals("/")) { + return true; + } + + return false; + + } + + + /** + * Convert a {@link ServletSecurityElement} to an array of + * {@link SecurityConstraint}(s). + * + * @param element The element to be converted + * @param urlPattern The url pattern that the element should be applied + * to + * @return The (possibly zero length) array of constraints that + * are the equivalent to the input + */ + public static SecurityConstraint[] createConstraints( + ServletSecurityElement element, String urlPattern) { + Set result = new HashSet<>(); + + // Add the per method constraints + Collection methods = + element.getHttpMethodConstraints(); + for (HttpMethodConstraintElement methodElement : methods) { + SecurityConstraint constraint = + createConstraint(methodElement, urlPattern, true); + // There will always be a single collection + SecurityCollection collection = constraint.findCollections()[0]; + collection.addMethod(methodElement.getMethodName()); + result.add(constraint); + } + + // Add the constraint for all the other methods + SecurityConstraint constraint = createConstraint(element, urlPattern, false); + if (constraint != null) { + // There will always be a single collection + SecurityCollection collection = constraint.findCollections()[0]; + for (String name : element.getMethodNames()) { + collection.addOmittedMethod(name); + } + + result.add(constraint); + + } + + return result.toArray(new SecurityConstraint[0]); + } + + private static SecurityConstraint createConstraint( + HttpConstraintElement element, String urlPattern, boolean alwaysCreate) { + + SecurityConstraint constraint = new SecurityConstraint(); + SecurityCollection collection = new SecurityCollection(); + boolean create = alwaysCreate; + + if (element.getTransportGuarantee() != + ServletSecurity.TransportGuarantee.NONE) { + constraint.setUserConstraint(element.getTransportGuarantee().name()); + create = true; + } + if (element.getRolesAllowed().length > 0) { + String[] roles = element.getRolesAllowed(); + for (String role : roles) { + constraint.addAuthRole(role); + } + create = true; + } + if (element.getEmptyRoleSemantic() != EmptyRoleSemantic.PERMIT) { + constraint.setAuthConstraint(true); + create = true; + } + + if (create) { + collection.addPattern(urlPattern); + constraint.addCollection(collection); + return constraint; + } + + return null; + } + + + public static SecurityConstraint[] findUncoveredHttpMethods( + SecurityConstraint[] constraints, + boolean denyUncoveredHttpMethods, Log log) { + + Set coveredPatterns = new HashSet<>(); + Map> urlMethodMap = new HashMap<>(); + Map> urlOmittedMethodMap = new HashMap<>(); + + List newConstraints = new ArrayList<>(); + + // First build the lists of covered patterns and those patterns that + // might be uncovered + for (SecurityConstraint constraint : constraints) { + SecurityCollection[] collections = constraint.findCollections(); + for (SecurityCollection collection : collections) { + String[] patterns = collection.findPatterns(); + String[] methods = collection.findMethods(); + String[] omittedMethods = collection.findOmittedMethods(); + // Simple case: no methods + if (methods.length == 0 && omittedMethods.length == 0) { + coveredPatterns.addAll(Arrays.asList(patterns)); + continue; + } + + // Pre-calculate so we don't do this for every iteration of the + // following loop + List omNew = null; + if (omittedMethods.length != 0) { + omNew = Arrays.asList(omittedMethods); + } + + // Only need to process uncovered patterns + for (String pattern : patterns) { + if (!coveredPatterns.contains(pattern)) { + if (methods.length == 0) { + // Build the interset of omitted methods for this + // pattern + Set om = urlOmittedMethodMap.get(pattern); + if (om == null) { + om = new HashSet<>(); + urlOmittedMethodMap.put(pattern, om); + om.addAll(omNew); + } else { + om.retainAll(omNew); + } + } else { + // Build the union of methods for this pattern + urlMethodMap.computeIfAbsent(pattern, k -> new HashSet<>()).addAll(Arrays.asList(methods)); + } + } + } + } + } + + // Now check the potentially uncovered patterns + for (Map.Entry> entry : urlMethodMap.entrySet()) { + String pattern = entry.getKey(); + if (coveredPatterns.contains(pattern)) { + // Fully covered. Ignore any partial coverage + urlOmittedMethodMap.remove(pattern); + continue; + } + + Set omittedMethods = urlOmittedMethodMap.remove(pattern); + Set methods = entry.getValue(); + + if (omittedMethods == null) { + StringBuilder msg = new StringBuilder(); + for (String method : methods) { + msg.append(method); + msg.append(' '); + } + if (denyUncoveredHttpMethods) { + log.info(sm.getString( + "securityConstraint.uncoveredHttpMethodFix", + pattern, msg.toString().trim())); + SecurityCollection collection = new SecurityCollection(); + for (String method : methods) { + collection.addOmittedMethod(method); + } + collection.addPatternDecoded(pattern); + collection.setName("deny-uncovered-http-methods"); + SecurityConstraint constraint = new SecurityConstraint(); + constraint.setAuthConstraint(true); + constraint.addCollection(collection); + newConstraints.add(constraint); + } else { + log.error(sm.getString( + "securityConstraint.uncoveredHttpMethod", + pattern, msg.toString().trim())); + } + continue; + } + + // As long as every omitted method as a corresponding method the + // pattern is fully covered. + omittedMethods.removeAll(methods); + + handleOmittedMethods(omittedMethods, pattern, denyUncoveredHttpMethods, + newConstraints, log); + } + for (Map.Entry> entry : + urlOmittedMethodMap.entrySet()) { + String pattern = entry.getKey(); + if (coveredPatterns.contains(pattern)) { + // Fully covered. Ignore any partial coverage + continue; + } + + handleOmittedMethods(entry.getValue(), pattern, denyUncoveredHttpMethods, + newConstraints, log); + } + + return newConstraints.toArray(new SecurityConstraint[0]); + } + + + private static void handleOmittedMethods(Set omittedMethods, String pattern, + boolean denyUncoveredHttpMethods, List newConstraints, Log log) { + if (omittedMethods.size() > 0) { + StringBuilder msg = new StringBuilder(); + for (String method : omittedMethods) { + msg.append(method); + msg.append(' '); + } + if (denyUncoveredHttpMethods) { + log.info(sm.getString( + "securityConstraint.uncoveredHttpOmittedMethodFix", + pattern, msg.toString().trim())); + SecurityCollection collection = new SecurityCollection(); + for (String method : omittedMethods) { + collection.addMethod(method); + } + collection.addPatternDecoded(pattern); + collection.setName("deny-uncovered-http-methods"); + SecurityConstraint constraint = new SecurityConstraint(); + constraint.setAuthConstraint(true); + constraint.addCollection(collection); + newConstraints.add(constraint); + } else { + log.error(sm.getString( + "securityConstraint.uncoveredHttpOmittedMethod", + pattern, msg.toString().trim())); + } + } + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/SecurityRoleRef.java b/java/org/apache/tomcat/util/descriptor/web/SecurityRoleRef.java new file mode 100644 index 0000000..52f486a --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/SecurityRoleRef.java @@ -0,0 +1,85 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; + + +/** + *

    Representation of a security role reference for a web application, as + * represented in a <security-role-ref> element + * in the deployment descriptor.

    + * + * @since Tomcat 5.5 + */ +public class SecurityRoleRef implements Serializable { + + private static final long serialVersionUID = 1L; + + + // ------------------------------------------------------------- Properties + + /** + * The (required) role name. + */ + private String name = null; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + + /** + * The optional role link. + */ + private String link = null; + + public String getLink() { + return this.link; + } + + public void setLink(String link) { + this.link = link; + } + + + + // --------------------------------------------------------- Public Methods + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SecurityRoleRef["); + sb.append("name="); + sb.append(name); + if (link != null) { + sb.append(", link="); + sb.append(link); + } + sb.append(']'); + return sb.toString(); + } + + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/ServletDef.java b/java/org/apache/tomcat/util/descriptor/web/ServletDef.java new file mode 100644 index 0000000..1499f7b --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/ServletDef.java @@ -0,0 +1,274 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.tomcat.util.res.StringManager; + + +/** + * Representation of a servlet definition for a web application, as represented + * in a <servlet> element in the deployment descriptor. + */ + +public class ServletDef implements Serializable { + + private static final long serialVersionUID = 1L; + + private static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + // ------------------------------------------------------------- Properties + + + /** + * The description of this servlet. + */ + private String description = null; + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + + /** + * The display name of this servlet. + */ + private String displayName = null; + + public String getDisplayName() { + return this.displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + + /** + * The small icon associated with this servlet. + */ + private String smallIcon = null; + + public String getSmallIcon() { + return this.smallIcon; + } + + public void setSmallIcon(String smallIcon) { + this.smallIcon = smallIcon; + } + + /** + * The large icon associated with this servlet. + */ + private String largeIcon = null; + + public String getLargeIcon() { + return this.largeIcon; + } + + public void setLargeIcon(String largeIcon) { + this.largeIcon = largeIcon; + } + + + /** + * The name of this servlet, which must be unique among the servlets + * defined for a particular web application. + */ + private String servletName = null; + + public String getServletName() { + return this.servletName; + } + + public void setServletName(String servletName) { + if (servletName == null || servletName.equals("")) { + throw new IllegalArgumentException( + sm.getString("servletDef.invalidServletName", servletName)); + } + this.servletName = servletName; + } + + + /** + * The fully qualified name of the Java class that implements this servlet. + */ + private String servletClass = null; + + public String getServletClass() { + return this.servletClass; + } + + public void setServletClass(String servletClass) { + this.servletClass = servletClass; + } + + + /** + * The name of the JSP file to which this servlet definition applies + */ + private String jspFile = null; + + public String getJspFile() { + return this.jspFile; + } + + public void setJspFile(String jspFile) { + this.jspFile = jspFile; + } + + + /** + * The set of initialization parameters for this servlet, keyed by + * parameter name. + */ + private final Map parameters = new HashMap<>(); + + public Map getParameterMap() { + return this.parameters; + } + + /** + * Add an initialization parameter to the set of parameters associated + * with this servlet. + * + * @param name The initialisation parameter name + * @param value The initialisation parameter value + */ + public void addInitParameter(String name, String value) { + + if (parameters.containsKey(name)) { + // The spec does not define this but the TCK expects the first + // definition to take precedence + return; + } + parameters.put(name, value); + + } + + /** + * The load-on-startup order for this servlet + */ + private Integer loadOnStartup = null; + + public Integer getLoadOnStartup() { + return this.loadOnStartup; + } + + public void setLoadOnStartup(String loadOnStartup) { + this.loadOnStartup = Integer.valueOf(loadOnStartup); + } + + + /** + * The run-as configuration for this servlet + */ + private String runAs = null; + + public String getRunAs() { + return this.runAs; + } + + public void setRunAs(String runAs) { + this.runAs = runAs; + } + + + /** + * The set of security role references for this servlet + */ + private final Set securityRoleRefs = new HashSet<>(); + + public Set getSecurityRoleRefs() { + return this.securityRoleRefs; + } + + /** + * Add a security-role-ref to the set of security-role-refs associated + * with this servlet. + * @param securityRoleRef The security role + */ + public void addSecurityRoleRef(SecurityRoleRef securityRoleRef) { + securityRoleRefs.add(securityRoleRef); + } + + /** + * The multipart configuration, if any, for this servlet + */ + private MultipartDef multipartDef = null; + + public MultipartDef getMultipartDef() { + return this.multipartDef; + } + + public void setMultipartDef(MultipartDef multipartDef) { + this.multipartDef = multipartDef; + } + + + /** + * Does this servlet support async. + */ + private Boolean asyncSupported = null; + + public Boolean getAsyncSupported() { + return this.asyncSupported; + } + + public void setAsyncSupported(String asyncSupported) { + this.asyncSupported = Boolean.valueOf(asyncSupported); + } + + + /** + * Is this servlet enabled. + */ + private Boolean enabled = null; + + public Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(String enabled) { + this.enabled = Boolean.valueOf(enabled); + } + + + /** + * Can this ServletDef be overridden by an SCI? + */ + private boolean overridable = false; + + public boolean isOverridable() { + return overridable; + } + + public void setOverridable(boolean overridable) { + this.overridable = overridable; + } + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/SessionConfig.java b/java/org/apache/tomcat/util/descriptor/web/SessionConfig.java new file mode 100644 index 0000000..2daddd4 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/SessionConfig.java @@ -0,0 +1,123 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.EnumSet; +import java.util.Map; +import java.util.TreeMap; + +import jakarta.servlet.SessionTrackingMode; + +/** + * Representation of a session configuration element for a web application, + * as represented in a <session-config> element in the + * deployment descriptor. + */ +public class SessionConfig { + + private Integer sessionTimeout; + private String cookieName; + private final Map cookieAttributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private final EnumSet sessionTrackingModes = + EnumSet.noneOf(SessionTrackingMode.class); + + public Integer getSessionTimeout() { + return sessionTimeout; + } + public void setSessionTimeout(String sessionTimeout) { + this.sessionTimeout = Integer.valueOf(sessionTimeout); + } + + public String getCookieName() { + return cookieName; + } + public void setCookieName(String cookieName) { + this.cookieName = cookieName; + } + + public String getCookieDomain() { + return getCookieAttribute(Constants.COOKIE_DOMAIN_ATTR); + } + public void setCookieDomain(String cookieDomain) { + setCookieAttribute(Constants.COOKIE_DOMAIN_ATTR, cookieDomain); + } + + public String getCookiePath() { + return getCookieAttribute(Constants.COOKIE_PATH_ATTR); + } + public void setCookiePath(String cookiePath) { + setCookieAttribute(Constants.COOKIE_PATH_ATTR, cookiePath); + } + + public String getCookieComment() { + return getCookieAttribute(Constants.COOKIE_COMMENT_ATTR); + } + public void setCookieComment(String cookieComment) { + setCookieAttribute(Constants.COOKIE_COMMENT_ATTR, cookieComment); + } + + public Boolean getCookieHttpOnly() { + String httpOnly = getCookieAttribute(Constants.COOKIE_HTTP_ONLY_ATTR); + if (httpOnly == null) { + return null; + } + return Boolean.valueOf(httpOnly); + } + public void setCookieHttpOnly(String cookieHttpOnly) { + setCookieAttribute(Constants.COOKIE_HTTP_ONLY_ATTR, cookieHttpOnly); + } + + public Boolean getCookieSecure() { + String secure = getCookieAttribute(Constants.COOKIE_SECURE_ATTR); + if (secure == null) { + return null; + } + return Boolean.valueOf(secure); + } + public void setCookieSecure(String cookieSecure) { + setCookieAttribute(Constants.COOKIE_SECURE_ATTR, cookieSecure); + } + + public Integer getCookieMaxAge() { + String maxAge = getCookieAttribute(Constants.COOKIE_MAX_AGE_ATTR); + if (maxAge == null) { + return null; + } + return Integer.valueOf(maxAge); + } + public void setCookieMaxAge(String cookieMaxAge) { + setCookieAttribute(Constants.COOKIE_MAX_AGE_ATTR, cookieMaxAge); + } + + public Map getCookieAttributes() { + return cookieAttributes; + } + public void setCookieAttribute(String name, String value) { + cookieAttributes.put(name, value); + } + public String getCookieAttribute(String name) { + return cookieAttributes.get(name); + } + + public EnumSet getSessionTrackingModes() { + return sessionTrackingModes; + } + public void addSessionTrackingMode(String sessionTrackingMode) { + sessionTrackingModes.add( + SessionTrackingMode.valueOf(sessionTrackingMode)); + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/TaglibDescriptorImpl.java b/java/org/apache/tomcat/util/descriptor/web/TaglibDescriptorImpl.java new file mode 100644 index 0000000..87aa0f7 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/TaglibDescriptorImpl.java @@ -0,0 +1,77 @@ +/* + * 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.tomcat.util.descriptor.web; + +import jakarta.servlet.descriptor.TaglibDescriptor; + +public class TaglibDescriptorImpl implements TaglibDescriptor { + + private final String location; + private final String uri; + + public TaglibDescriptorImpl(String location, String uri) { + this.location = location; + this.uri = uri; + } + + @Override + public String getTaglibLocation() { + return location; + } + + @Override + public String getTaglibURI() { + return uri; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((location == null) ? 0 : location.hashCode()); + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TaglibDescriptorImpl)) { + return false; + } + TaglibDescriptorImpl other = (TaglibDescriptorImpl) obj; + if (location == null) { + if (other.location != null) { + return false; + } + } else if (!location.equals(other.location)) { + return false; + } + if (uri == null) { + if (other.uri != null) { + return false; + } + } else if (!uri.equals(other.uri)) { + return false; + } + return true; + } + +} diff --git a/java/org/apache/tomcat/util/descriptor/web/WebRuleSet.java b/java/org/apache/tomcat/util/descriptor/web/WebRuleSet.java new file mode 100644 index 0000000..7fa4327 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/WebRuleSet.java @@ -0,0 +1,1511 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.lang.reflect.Method; +import java.util.ArrayList; + +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.digester.CallMethodRule; +import org.apache.tomcat.util.digester.CallParamRule; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.Rule; +import org.apache.tomcat.util.digester.RuleSet; +import org.apache.tomcat.util.digester.SetNextRule; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.Attributes; + +/** + *

    RuleSet for processing the contents of a web application + * deployment descriptor (/WEB-INF/web.xml) resource.

    + * + * @author Craig R. McClanahan + */ +public class WebRuleSet implements RuleSet { + + /** + * The string resources for this package. + */ + protected static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + // ----------------------------------------------------- Instance Variables + + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected final String prefix; + + /** + * The full pattern matching prefix, including the webapp or web-fragment + * component, to use for matching elements + */ + protected final String fullPrefix; + + /** + * Flag that indicates if this ruleset is for a web-fragment.xml file or for + * a web.xml file. + */ + protected final boolean fragment; + + /** + * The SetSessionConfig rule used to parse the web.xml + */ + protected final SetSessionConfig sessionConfig = new SetSessionConfig(); + + + /** + * The SetLoginConfig rule used to parse the web.xml + */ + protected final SetLoginConfig loginConfig = new SetLoginConfig(); + + + /** + * The SetJspConfig rule used to parse the web.xml + */ + protected final SetJspConfig jspConfig = new SetJspConfig(); + + + /** + * The NameRule rule used to parse the web.xml + */ + protected final NameRule name = new NameRule(); + + + /** + * The AbsoluteOrderingRule rule used to parse the web.xml + */ + protected final AbsoluteOrderingRule absoluteOrdering; + + + /** + * The RelativeOrderingRule rule used to parse the web.xml + */ + protected final RelativeOrderingRule relativeOrdering; + + + + // ------------------------------------------------------------ Constructor + + + /** + * Construct an instance of this RuleSet with the default + * matching pattern prefix and default fragment setting. + */ + public WebRuleSet() { + + this("", false); + + } + + + /** + * Construct an instance of this RuleSet with the default + * matching pattern prefix. + * @param fragment true if this is a web fragment + */ + public WebRuleSet(boolean fragment) { + + this("", fragment); + + } + + + /** + * Construct an instance of this RuleSet with the specified + * matching pattern prefix. + * + * @param prefix Prefix for matching pattern rules (including the + * trailing slash character) + * @param fragment true if this is a web fragment + */ + public WebRuleSet(String prefix, boolean fragment) { + this.prefix = prefix; + this.fragment = fragment; + + if(fragment) { + fullPrefix = prefix + "web-fragment"; + } else { + fullPrefix = prefix + "web-app"; + } + + absoluteOrdering = new AbsoluteOrderingRule(fragment); + relativeOrdering = new RelativeOrderingRule(fragment); + } + + + // --------------------------------------------------------- Public Methods + + /** + *

    Add the set of Rule instances defined in this RuleSet to the + * specified Digester instance, associating them with + * our namespace URI (if any). This method should only be called + * by a Digester instance.

    + * + * @param digester Digester instance to which the new Rule instances + * should be added. + */ + @Override + public void addRuleInstances(Digester digester) { + digester.addRule(fullPrefix, + new SetPublicIdRule("setPublicId")); + digester.addRule(fullPrefix, + new IgnoreAnnotationsRule()); + digester.addRule(fullPrefix, + new VersionRule()); + + // Required for both fragments and non-fragments + digester.addRule(fullPrefix + "/absolute-ordering", absoluteOrdering); + digester.addRule(fullPrefix + "/ordering", relativeOrdering); + + if (fragment) { + // web-fragment.xml + digester.addRule(fullPrefix + "/name", name); + digester.addCallMethod(fullPrefix + "/ordering/after/name", + "addAfterOrdering", 0); + digester.addCallMethod(fullPrefix + "/ordering/after/others", + "addAfterOrderingOthers"); + digester.addCallMethod(fullPrefix + "/ordering/before/name", + "addBeforeOrdering", 0); + digester.addCallMethod(fullPrefix + "/ordering/before/others", + "addBeforeOrderingOthers"); + } else { + // web.xml + digester.addCallMethod(fullPrefix + "/absolute-ordering/name", + "addAbsoluteOrdering", 0); + digester.addCallMethod(fullPrefix + "/absolute-ordering/others", + "addAbsoluteOrderingOthers"); + digester.addRule(fullPrefix + "/deny-uncovered-http-methods", + new SetDenyUncoveredHttpMethodsRule()); + digester.addCallMethod(fullPrefix + "/request-character-encoding", + "setRequestCharacterEncoding", 0); + digester.addCallMethod(fullPrefix + "/response-character-encoding", + "setResponseCharacterEncoding", 0); + } + + digester.addCallMethod(fullPrefix + "/context-param", + "addContextParam", 2); + digester.addCallParam(fullPrefix + "/context-param/param-name", 0); + digester.addCallParam(fullPrefix + "/context-param/param-value", 1); + + digester.addCallMethod(fullPrefix + "/display-name", + "setDisplayName", 0); + + digester.addRule(fullPrefix + "/distributable", + new SetDistributableRule()); + + configureNamingRules(digester); + + digester.addObjectCreate(fullPrefix + "/error-page", + "org.apache.tomcat.util.descriptor.web.ErrorPage"); + digester.addSetNext(fullPrefix + "/error-page", + "addErrorPage", + "org.apache.tomcat.util.descriptor.web.ErrorPage"); + + digester.addCallMethod(fullPrefix + "/error-page/error-code", + "setErrorCode", 0); + digester.addCallMethod(fullPrefix + "/error-page/exception-type", + "setExceptionType", 0); + digester.addCallMethod(fullPrefix + "/error-page/location", + "setLocation", 0); + + digester.addObjectCreate(fullPrefix + "/filter", + "org.apache.tomcat.util.descriptor.web.FilterDef"); + digester.addSetNext(fullPrefix + "/filter", + "addFilter", + "org.apache.tomcat.util.descriptor.web.FilterDef"); + + digester.addCallMethod(fullPrefix + "/filter/description", + "setDescription", 0); + digester.addCallMethod(fullPrefix + "/filter/display-name", + "setDisplayName", 0); + digester.addCallMethod(fullPrefix + "/filter/filter-class", + "setFilterClass", 0); + digester.addCallMethod(fullPrefix + "/filter/filter-name", + "setFilterName", 0); + digester.addCallMethod(fullPrefix + "/filter/icon/large-icon", + "setLargeIcon", 0); + digester.addCallMethod(fullPrefix + "/filter/icon/small-icon", + "setSmallIcon", 0); + digester.addCallMethod(fullPrefix + "/filter/async-supported", + "setAsyncSupported", 0); + + digester.addCallMethod(fullPrefix + "/filter/init-param", + "addInitParameter", 2); + digester.addCallParam(fullPrefix + "/filter/init-param/param-name", + 0); + digester.addCallParam(fullPrefix + "/filter/init-param/param-value", + 1); + + digester.addObjectCreate(fullPrefix + "/filter-mapping", + "org.apache.tomcat.util.descriptor.web.FilterMap"); + digester.addSetNext(fullPrefix + "/filter-mapping", + "addFilterMapping", + "org.apache.tomcat.util.descriptor.web.FilterMap"); + + digester.addCallMethod(fullPrefix + "/filter-mapping/filter-name", + "setFilterName", 0); + digester.addCallMethod(fullPrefix + "/filter-mapping/servlet-name", + "addServletName", 0); + digester.addCallMethod(fullPrefix + "/filter-mapping/url-pattern", + "addURLPattern", 0); + + digester.addCallMethod(fullPrefix + "/filter-mapping/dispatcher", + "setDispatcher", 0); + + digester.addCallMethod(fullPrefix + "/listener/listener-class", + "addListener", 0); + + digester.addRule(fullPrefix + "/jsp-config", + jspConfig); + + digester.addObjectCreate(fullPrefix + "/jsp-config/jsp-property-group", + "org.apache.tomcat.util.descriptor.web.JspPropertyGroup"); + digester.addSetNext(fullPrefix + "/jsp-config/jsp-property-group", + "addJspPropertyGroup", + "org.apache.tomcat.util.descriptor.web.JspPropertyGroup"); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/deferred-syntax-allowed-as-literal", + "setDeferredSyntax", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/el-ignored", + "setElIgnored", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/error-on-el-not-found", + "setErrorOnELNotFound", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/include-coda", + "addIncludeCoda", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/include-prelude", + "addIncludePrelude", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/is-xml", + "setIsXml", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/page-encoding", + "setPageEncoding", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/scripting-invalid", + "setScriptingInvalid", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/trim-directive-whitespaces", + "setTrimWhitespace", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/url-pattern", + "addUrlPattern", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/default-content-type", + "setDefaultContentType", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/buffer", + "setBuffer", 0); + digester.addCallMethod(fullPrefix + "/jsp-config/jsp-property-group/error-on-undeclared-namespace", + "setErrorOnUndeclaredNamespace", 0); + + digester.addRule(fullPrefix + "/login-config", + loginConfig); + + digester.addObjectCreate(fullPrefix + "/login-config", + "org.apache.tomcat.util.descriptor.web.LoginConfig"); + digester.addSetNext(fullPrefix + "/login-config", + "setLoginConfig", + "org.apache.tomcat.util.descriptor.web.LoginConfig"); + + digester.addCallMethod(fullPrefix + "/login-config/auth-method", + "setAuthMethod", 0); + digester.addCallMethod(fullPrefix + "/login-config/realm-name", + "setRealmName", 0); + digester.addCallMethod(fullPrefix + "/login-config/form-login-config/form-error-page", + "setErrorPage", 0); + digester.addCallMethod(fullPrefix + "/login-config/form-login-config/form-login-page", + "setLoginPage", 0); + + digester.addCallMethod(fullPrefix + "/mime-mapping", + "addMimeMapping", 2); + digester.addCallParam(fullPrefix + "/mime-mapping/extension", 0); + digester.addCallParam(fullPrefix + "/mime-mapping/mime-type", 1); + + + digester.addObjectCreate(fullPrefix + "/security-constraint", + "org.apache.tomcat.util.descriptor.web.SecurityConstraint"); + digester.addSetNext(fullPrefix + "/security-constraint", + "addSecurityConstraint", + "org.apache.tomcat.util.descriptor.web.SecurityConstraint"); + + digester.addRule(fullPrefix + "/security-constraint/auth-constraint", + new SetAuthConstraintRule()); + digester.addCallMethod(fullPrefix + "/security-constraint/auth-constraint/role-name", + "addAuthRole", 0); + digester.addCallMethod(fullPrefix + "/security-constraint/display-name", + "setDisplayName", 0); + digester.addCallMethod(fullPrefix + "/security-constraint/user-data-constraint/transport-guarantee", + "setUserConstraint", 0); + + digester.addObjectCreate(fullPrefix + "/security-constraint/web-resource-collection", + "org.apache.tomcat.util.descriptor.web.SecurityCollection"); + digester.addSetNext(fullPrefix + "/security-constraint/web-resource-collection", + "addCollection", + "org.apache.tomcat.util.descriptor.web.SecurityCollection"); + digester.addCallMethod(fullPrefix + "/security-constraint/web-resource-collection/http-method", + "addMethod", 0); + digester.addCallMethod(fullPrefix + "/security-constraint/web-resource-collection/http-method-omission", + "addOmittedMethod", 0); + digester.addCallMethod(fullPrefix + "/security-constraint/web-resource-collection/url-pattern", + "addPattern", 0); + digester.addCallMethod(fullPrefix + "/security-constraint/web-resource-collection/web-resource-name", + "setName", 0); + + digester.addCallMethod(fullPrefix + "/security-role/role-name", + "addSecurityRole", 0); + + digester.addRule(fullPrefix + "/servlet", + new ServletDefCreateRule()); + digester.addSetNext(fullPrefix + "/servlet", + "addServlet", + "org.apache.tomcat.util.descriptor.web.ServletDef"); + + digester.addCallMethod(fullPrefix + "/servlet/init-param", + "addInitParameter", 2); + digester.addCallParam(fullPrefix + "/servlet/init-param/param-name", + 0); + digester.addCallParam(fullPrefix + "/servlet/init-param/param-value", + 1); + + digester.addCallMethod(fullPrefix + "/servlet/jsp-file", + "setJspFile", 0); + digester.addCallMethod(fullPrefix + "/servlet/load-on-startup", + "setLoadOnStartup", 0); + digester.addCallMethod(fullPrefix + "/servlet/run-as/role-name", + "setRunAs", 0); + + digester.addObjectCreate(fullPrefix + "/servlet/security-role-ref", + "org.apache.tomcat.util.descriptor.web.SecurityRoleRef"); + digester.addSetNext(fullPrefix + "/servlet/security-role-ref", + "addSecurityRoleRef", + "org.apache.tomcat.util.descriptor.web.SecurityRoleRef"); + digester.addCallMethod(fullPrefix + "/servlet/security-role-ref/role-link", + "setLink", 0); + digester.addCallMethod(fullPrefix + "/servlet/security-role-ref/role-name", + "setName", 0); + + digester.addCallMethod(fullPrefix + "/servlet/servlet-class", + "setServletClass", 0); + digester.addCallMethod(fullPrefix + "/servlet/servlet-name", + "setServletName", 0); + + digester.addObjectCreate(fullPrefix + "/servlet/multipart-config", + "org.apache.tomcat.util.descriptor.web.MultipartDef"); + digester.addSetNext(fullPrefix + "/servlet/multipart-config", + "setMultipartDef", + "org.apache.tomcat.util.descriptor.web.MultipartDef"); + digester.addCallMethod(fullPrefix + "/servlet/multipart-config/location", + "setLocation", 0); + digester.addCallMethod(fullPrefix + "/servlet/multipart-config/max-file-size", + "setMaxFileSize", 0); + digester.addCallMethod(fullPrefix + "/servlet/multipart-config/max-request-size", + "setMaxRequestSize", 0); + digester.addCallMethod(fullPrefix + "/servlet/multipart-config/file-size-threshold", + "setFileSizeThreshold", 0); + + digester.addCallMethod(fullPrefix + "/servlet/async-supported", + "setAsyncSupported", 0); + digester.addCallMethod(fullPrefix + "/servlet/enabled", + "setEnabled", 0); + + + digester.addRule(fullPrefix + "/servlet-mapping", + new CallMethodMultiRule("addServletMapping", 2, 0)); + digester.addCallParam(fullPrefix + "/servlet-mapping/servlet-name", 1); + digester.addRule(fullPrefix + "/servlet-mapping/url-pattern", new CallParamMultiRule(0)); + + digester.addRule(fullPrefix + "/session-config", sessionConfig); + digester.addObjectCreate(fullPrefix + "/session-config", + "org.apache.tomcat.util.descriptor.web.SessionConfig"); + digester.addSetNext(fullPrefix + "/session-config", "setSessionConfig", + "org.apache.tomcat.util.descriptor.web.SessionConfig"); + digester.addCallMethod(fullPrefix + "/session-config/session-timeout", + "setSessionTimeout", 0); + digester.addCallMethod(fullPrefix + "/session-config/cookie-config/name", + "setCookieName", 0); + digester.addCallMethod(fullPrefix + "/session-config/cookie-config/domain", + "setCookieDomain", 0); + digester.addCallMethod(fullPrefix + "/session-config/cookie-config/path", + "setCookiePath", 0); + digester.addCallMethod(fullPrefix + "/session-config/cookie-config/comment", + "setCookieComment", 0); + digester.addCallMethod(fullPrefix + "/session-config/cookie-config/http-only", + "setCookieHttpOnly", 0); + digester.addCallMethod(fullPrefix + "/session-config/cookie-config/secure", + "setCookieSecure", 0); + digester.addCallMethod(fullPrefix + "/session-config/cookie-config/max-age", + "setCookieMaxAge", 0); + digester.addCallMethod(fullPrefix + "/session-config/cookie-config/attribute", + "setCookieAttribute", 2); + digester.addCallParam(fullPrefix + "/session-config/cookie-config/attribute/attribute-name", 0); + digester.addCallParam(fullPrefix + "/session-config/cookie-config/attribute/attribute-value", 1); + digester.addCallMethod(fullPrefix + "/session-config/tracking-mode", + "addSessionTrackingMode", 0); + + // Taglibs pre Servlet 2.4 + digester.addRule(fullPrefix + "/taglib", new TaglibLocationRule(false)); + digester.addCallMethod(fullPrefix + "/taglib", + "addTaglib", 2); + digester.addCallParam(fullPrefix + "/taglib/taglib-location", 1); + digester.addCallParam(fullPrefix + "/taglib/taglib-uri", 0); + + // Taglibs Servlet 2.4 onwards + digester.addRule(fullPrefix + "/jsp-config/taglib", new TaglibLocationRule(true)); + digester.addCallMethod(fullPrefix + "/jsp-config/taglib", + "addTaglib", 2); + digester.addCallParam(fullPrefix + "/jsp-config/taglib/taglib-location", 1); + digester.addCallParam(fullPrefix + "/jsp-config/taglib/taglib-uri", 0); + + digester.addCallMethod(fullPrefix + "/welcome-file-list/welcome-file", + "addWelcomeFile", 0); + + digester.addCallMethod(fullPrefix + "/locale-encoding-mapping-list/locale-encoding-mapping", + "addLocaleEncodingMapping", 2); + digester.addCallParam(fullPrefix + "/locale-encoding-mapping-list/locale-encoding-mapping/locale", 0); + digester.addCallParam(fullPrefix + "/locale-encoding-mapping-list/locale-encoding-mapping/encoding", 1); + + digester.addRule(fullPrefix + "/post-construct", + new LifecycleCallbackRule("addPostConstructMethods", 2, true)); + digester.addCallParam(fullPrefix + "/post-construct/lifecycle-callback-class", 0); + digester.addCallParam(fullPrefix + "/post-construct/lifecycle-callback-method", 1); + + digester.addRule(fullPrefix + "/pre-destroy", + new LifecycleCallbackRule("addPreDestroyMethods", 2, false)); + digester.addCallParam(fullPrefix + "/pre-destroy/lifecycle-callback-class", 0); + digester.addCallParam(fullPrefix + "/pre-destroy/lifecycle-callback-method", 1); + } + + protected void configureNamingRules(Digester digester) { + //ejb-local-ref + digester.addObjectCreate(fullPrefix + "/ejb-local-ref", + "org.apache.tomcat.util.descriptor.web.ContextLocalEjb"); + digester.addSetNext(fullPrefix + "/ejb-local-ref", + "addEjbLocalRef", + "org.apache.tomcat.util.descriptor.web.ContextLocalEjb"); + digester.addCallMethod(fullPrefix + "/ejb-local-ref/description", + "setDescription", 0); + digester.addCallMethod(fullPrefix + "/ejb-local-ref/ejb-link", + "setLink", 0); + digester.addCallMethod(fullPrefix + "/ejb-local-ref/ejb-ref-name", + "setName", 0); + digester.addCallMethod(fullPrefix + "/ejb-local-ref/ejb-ref-type", + "setType", 0); + digester.addCallMethod(fullPrefix + "/ejb-local-ref/local", + "setLocal", 0); + digester.addCallMethod(fullPrefix + "/ejb-local-ref/local-home", + "setHome", 0); + digester.addRule(fullPrefix + "/ejb-local-ref/mapped-name", + new MappedNameRule()); + digester.addCallMethod(fullPrefix + "/ejb-local-ref/lookup-name", "setLookupName", 0); + configureInjectionRules(digester, "web-app/ejb-local-ref/"); + + //ejb-ref + digester.addObjectCreate(fullPrefix + "/ejb-ref", + "org.apache.tomcat.util.descriptor.web.ContextEjb"); + digester.addSetNext(fullPrefix + "/ejb-ref", + "addEjbRef", + "org.apache.tomcat.util.descriptor.web.ContextEjb"); + digester.addCallMethod(fullPrefix + "/ejb-ref/description", + "setDescription", 0); + digester.addCallMethod(fullPrefix + "/ejb-ref/ejb-link", + "setLink", 0); + digester.addCallMethod(fullPrefix + "/ejb-ref/ejb-ref-name", + "setName", 0); + digester.addCallMethod(fullPrefix + "/ejb-ref/ejb-ref-type", + "setType", 0); + digester.addCallMethod(fullPrefix + "/ejb-ref/home", + "setHome", 0); + digester.addCallMethod(fullPrefix + "/ejb-ref/remote", + "setRemote", 0); + digester.addRule(fullPrefix + "/ejb-ref/mapped-name", + new MappedNameRule()); + digester.addCallMethod(fullPrefix + "/ejb-ref/lookup-name", "setLookupName", 0); + configureInjectionRules(digester, "web-app/ejb-ref/"); + + //env-entry + digester.addObjectCreate(fullPrefix + "/env-entry", + "org.apache.tomcat.util.descriptor.web.ContextEnvironment"); + digester.addSetNext(fullPrefix + "/env-entry", + "addEnvEntry", + "org.apache.tomcat.util.descriptor.web.ContextEnvironment"); + digester.addRule(fullPrefix + "/env-entry", new SetOverrideRule()); + digester.addCallMethod(fullPrefix + "/env-entry/description", + "setDescription", 0); + digester.addCallMethod(fullPrefix + "/env-entry/env-entry-name", + "setName", 0); + digester.addCallMethod(fullPrefix + "/env-entry/env-entry-type", + "setType", 0); + digester.addCallMethod(fullPrefix + "/env-entry/env-entry-value", + "setValue", 0); + digester.addRule(fullPrefix + "/env-entry/mapped-name", + new MappedNameRule()); + digester.addCallMethod(fullPrefix + "/env-entry/lookup-name", "setLookupName", 0); + configureInjectionRules(digester, "web-app/env-entry/"); + + //resource-env-ref + digester.addObjectCreate(fullPrefix + "/resource-env-ref", + "org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef"); + digester.addSetNext(fullPrefix + "/resource-env-ref", + "addResourceEnvRef", + "org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef"); + digester.addCallMethod(fullPrefix + "/resource-env-ref/resource-env-ref-name", + "setName", 0); + digester.addCallMethod(fullPrefix + "/resource-env-ref/resource-env-ref-type", + "setType", 0); + digester.addRule(fullPrefix + "/resource-env-ref/mapped-name", + new MappedNameRule()); + digester.addCallMethod(fullPrefix + "/resource-env-ref/lookup-name", "setLookupName", 0); + configureInjectionRules(digester, "web-app/resource-env-ref/"); + + //message-destination + digester.addObjectCreate(fullPrefix + "/message-destination", + "org.apache.tomcat.util.descriptor.web.MessageDestination"); + digester.addSetNext(fullPrefix + "/message-destination", + "addMessageDestination", + "org.apache.tomcat.util.descriptor.web.MessageDestination"); + digester.addCallMethod(fullPrefix + "/message-destination/description", + "setDescription", 0); + digester.addCallMethod(fullPrefix + "/message-destination/display-name", + "setDisplayName", 0); + digester.addCallMethod(fullPrefix + "/message-destination/icon/large-icon", + "setLargeIcon", 0); + digester.addCallMethod(fullPrefix + "/message-destination/icon/small-icon", + "setSmallIcon", 0); + digester.addCallMethod(fullPrefix + "/message-destination/message-destination-name", + "setName", 0); + digester.addRule(fullPrefix + "/message-destination/mapped-name", + new MappedNameRule()); + digester.addCallMethod(fullPrefix + "/message-destination/lookup-name", "setLookupName", 0); + + //message-destination-ref + digester.addObjectCreate(fullPrefix + "/message-destination-ref", + "org.apache.tomcat.util.descriptor.web.MessageDestinationRef"); + digester.addSetNext(fullPrefix + "/message-destination-ref", + "addMessageDestinationRef", + "org.apache.tomcat.util.descriptor.web.MessageDestinationRef"); + digester.addCallMethod(fullPrefix + "/message-destination-ref/description", + "setDescription", 0); + digester.addCallMethod(fullPrefix + "/message-destination-ref/message-destination-link", + "setLink", 0); + digester.addCallMethod(fullPrefix + "/message-destination-ref/message-destination-ref-name", + "setName", 0); + digester.addCallMethod(fullPrefix + "/message-destination-ref/message-destination-type", + "setType", 0); + digester.addCallMethod(fullPrefix + "/message-destination-ref/message-destination-usage", + "setUsage", 0); + digester.addRule(fullPrefix + "/message-destination-ref/mapped-name", + new MappedNameRule()); + digester.addCallMethod(fullPrefix + "/message-destination-ref/lookup-name", + "setLookupName", 0); + configureInjectionRules(digester, "web-app/message-destination-ref/"); + + //resource-ref + digester.addObjectCreate(fullPrefix + "/resource-ref", + "org.apache.tomcat.util.descriptor.web.ContextResource"); + digester.addSetNext(fullPrefix + "/resource-ref", + "addResourceRef", + "org.apache.tomcat.util.descriptor.web.ContextResource"); + digester.addCallMethod(fullPrefix + "/resource-ref/description", + "setDescription", 0); + digester.addCallMethod(fullPrefix + "/resource-ref/res-auth", + "setAuth", 0); + digester.addCallMethod(fullPrefix + "/resource-ref/res-ref-name", + "setName", 0); + digester.addCallMethod(fullPrefix + "/resource-ref/res-sharing-scope", + "setScope", 0); + digester.addCallMethod(fullPrefix + "/resource-ref/res-type", + "setType", 0); + digester.addRule(fullPrefix + "/resource-ref/mapped-name", + new MappedNameRule()); + digester.addCallMethod(fullPrefix + "/resource-ref/lookup-name", "setLookupName", 0); + configureInjectionRules(digester, "web-app/resource-ref/"); + + //service-ref + digester.addObjectCreate(fullPrefix + "/service-ref", + "org.apache.tomcat.util.descriptor.web.ContextService"); + digester.addSetNext(fullPrefix + "/service-ref", + "addServiceRef", + "org.apache.tomcat.util.descriptor.web.ContextService"); + digester.addCallMethod(fullPrefix + "/service-ref/description", + "setDescription", 0); + digester.addCallMethod(fullPrefix + "/service-ref/display-name", + "setDisplayname", 0); + digester.addCallMethod(fullPrefix + "/service-ref/icon/large-icon", + "setLargeIcon", 0); + digester.addCallMethod(fullPrefix + "/service-ref/icon/small-icon", + "setSmallIcon", 0); + digester.addCallMethod(fullPrefix + "/service-ref/service-ref-name", + "setName", 0); + digester.addCallMethod(fullPrefix + "/service-ref/service-interface", + "setInterface", 0); + digester.addCallMethod(fullPrefix + "/service-ref/service-ref-type", + "setType", 0); + digester.addCallMethod(fullPrefix + "/service-ref/wsdl-file", + "setWsdlfile", 0); + digester.addCallMethod(fullPrefix + "/service-ref/jaxrpc-mapping-file", + "setJaxrpcmappingfile", 0); + digester.addRule(fullPrefix + "/service-ref/service-qname", new ServiceQnameRule()); + + digester.addRule(fullPrefix + "/service-ref/port-component-ref", + new CallMethodMultiRule("addPortcomponent", 2, 1)); + digester.addCallParam(fullPrefix + "/service-ref/port-component-ref/service-endpoint-interface", 0); + digester.addRule(fullPrefix + "/service-ref/port-component-ref/port-component-link", new CallParamMultiRule(1)); + + digester.addObjectCreate(fullPrefix + "/service-ref/handler", + "org.apache.tomcat.util.descriptor.web.ContextHandler"); + digester.addRule(fullPrefix + "/service-ref/handler", + new SetNextRule("addHandler", + "org.apache.tomcat.util.descriptor.web.ContextHandler")); + + digester.addCallMethod(fullPrefix + "/service-ref/handler/handler-name", + "setName", 0); + digester.addCallMethod(fullPrefix + "/service-ref/handler/handler-class", + "setHandlerclass", 0); + + digester.addCallMethod(fullPrefix + "/service-ref/handler/init-param", + "setProperty", 2); + digester.addCallParam(fullPrefix + "/service-ref/handler/init-param/param-name", + 0); + digester.addCallParam(fullPrefix + "/service-ref/handler/init-param/param-value", + 1); + + digester.addRule(fullPrefix + "/service-ref/handler/soap-header", new SoapHeaderRule()); + + digester.addCallMethod(fullPrefix + "/service-ref/handler/soap-role", + "addSoapRole", 0); + digester.addCallMethod(fullPrefix + "/service-ref/handler/port-name", + "addPortName", 0); + digester.addRule(fullPrefix + "/service-ref/mapped-name", + new MappedNameRule()); + digester.addCallMethod(fullPrefix + "/service-ref/lookup-name", "setLookupName", 0); + configureInjectionRules(digester, "web-app/service-ref/"); + } + + protected void configureInjectionRules(Digester digester, String base) { + + digester.addCallMethod(prefix + base + "injection-target", "addInjectionTarget", 2); + digester.addCallParam(prefix + base + "injection-target/injection-target-class", 0); + digester.addCallParam(prefix + base + "injection-target/injection-target-name", 1); + + } + + + /** + * Reset counter used for validating the web.xml file. + */ + public void recycle(){ + jspConfig.isJspConfigSet = false; + sessionConfig.isSessionConfigSet = false; + loginConfig.isLoginConfigSet = false; + name.isNameSet = false; + absoluteOrdering.isAbsoluteOrderingSet = false; + relativeOrdering.isRelativeOrderingSet = false; + } +} + + +// ----------------------------------------------------------- Private Classes + + +/** + * Rule to check that the login-config is occurring + * only 1 time within the web.xml + */ +final class SetLoginConfig extends Rule { + boolean isLoginConfigSet = false; + SetLoginConfig() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + if (isLoginConfigSet){ + throw new IllegalArgumentException( + " element is limited to 1 occurrence"); + } + isLoginConfigSet = true; + } + +} + + +/** + * Rule to check that the jsp-config is occurring + * only 1 time within the web.xml + */ +final class SetJspConfig extends Rule { + boolean isJspConfigSet = false; + SetJspConfig() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + if (isJspConfigSet){ + throw new IllegalArgumentException( + " element is limited to 1 occurrence"); + } + isJspConfigSet = true; + } + +} + + +/** + * Rule to check that the session-config is occurring + * only 1 time within the web.xml + */ +final class SetSessionConfig extends Rule { + boolean isSessionConfigSet = false; + SetSessionConfig() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + if (isSessionConfigSet){ + throw new IllegalArgumentException( + " element is limited to 1 occurrence"); + } + isSessionConfigSet = true; + } + +} + +/** + * A Rule that calls the setAuthConstraint(true) method of + * the top item on the stack, which must be of type + * org.apache.tomcat.util.descriptor.web.SecurityConstraint. + */ + +final class SetAuthConstraintRule extends Rule { + + SetAuthConstraintRule() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + SecurityConstraint securityConstraint = + (SecurityConstraint) digester.peek(); + securityConstraint.setAuthConstraint(true); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger() + .trace("Calling SecurityConstraint.setAuthConstraint(true)"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(securityConstraint)).append(".setAuthConstraint(true);"); + code.append(System.lineSeparator()); + } + } + +} + + +/** + * Class that calls setDistributable(true) for the top object + * on the stack, which must be a {@link WebXml} instance. + */ +final class SetDistributableRule extends Rule { + + SetDistributableRule() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + WebXml webXml = (WebXml) digester.peek(); + webXml.setDistributable(true); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace + (webXml.getClass().getName() + ".setDistributable(true)"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(webXml)).append(".setDistributable(true);"); + code.append(System.lineSeparator()); + } + } +} + + +/** + * Class that calls setDenyUncoveredHttpMethods(true) for the top + * object on the stack, which must be a {@link WebXml} instance. + */ +final class SetDenyUncoveredHttpMethodsRule extends Rule { + + SetDenyUncoveredHttpMethodsRule() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + WebXml webXml = (WebXml) digester.peek(); + webXml.setDenyUncoveredHttpMethods(true); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace(webXml.getClass().getName() + + ".setDenyUncoveredHttpMethods(true)"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(webXml)).append(".setDenyUncoveredHttpMethods(true);"); + code.append(System.lineSeparator()); + } + } +} + + +/** + * Class that calls a property setter for the top object on the stack, + * passing the public ID of the entity we are currently processing. + */ + +final class SetPublicIdRule extends Rule { + + SetPublicIdRule(String method) { + this.method = method; + } + + private String method = null; + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + + Object top = digester.peek(); + Class paramClasses[] = new Class[1]; + paramClasses[0] = "String".getClass(); + String paramValues[] = new String[1]; + paramValues[0] = digester.getPublicId(); + + Method m = null; + try { + m = top.getClass().getMethod(method, paramClasses); + } catch (NoSuchMethodException e) { + digester.getLogger().error(sm.getString("webRuleSet.noMethod", method, top, top.getClass())); + return; + } + + m.invoke(top, (Object [])paramValues); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace("" + top.getClass().getName() + "." + + method + "(" + paramValues[0] + ")"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(top)).append(".").append(method).append("(\""); + code.append(digester.getPublicId()).append("\");"); + code.append(System.lineSeparator()); + } + } + +} + + +/** + * A Rule that calls the factory method on the specified Context to + * create the object that is to be added to the stack. + */ + +final class ServletDefCreateRule extends Rule { + + ServletDefCreateRule() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + ServletDef servletDef = new ServletDef(); + digester.push(servletDef); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace("new " + servletDef.getClass().getName()); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(ServletDef.class.getName()).append(' ').append(digester.toVariableName(servletDef)).append(" = new "); + code.append(ServletDef.class.getName()).append("();").append(System.lineSeparator()); + } + } + + @Override + public void end(String namespace, String name) + throws Exception { + ServletDef servletDef = (ServletDef) digester.pop(); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace("pop " + servletDef.getClass().getName()); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + } + } + +} + + +/** + * A Rule that can be used to call multiple times a method as many times as needed + * (used for addServletMapping). + */ +final class CallParamMultiRule extends CallParamRule { + + CallParamMultiRule(int paramIndex) { + super(paramIndex); + } + + @Override + public void end(String namespace, String name) { + if (bodyTextStack != null && !bodyTextStack.empty()) { + // what we do now is push one parameter onto the top set of parameters + Object parameters[] = (Object[]) digester.peekParams(); + @SuppressWarnings("unchecked") + ArrayList params = (ArrayList) parameters[paramIndex]; + if (params == null) { + params = new ArrayList<>(); + parameters[paramIndex] = params; + } + params.add(bodyTextStack.pop()); + } + } + +} + + +/** + * A Rule that can be used to call multiple times a method as many times as needed + * (used for addServletMapping). + */ +final class CallMethodMultiRule extends CallMethodRule { + + final int multiParamIndex; + + CallMethodMultiRule(String methodName, int paramCount, int multiParamIndex) { + super(methodName, paramCount); + this.multiParamIndex = multiParamIndex; + } + + /** + * Process the end of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + */ + @Override + public void end(String namespace, String name) throws Exception { + + // Retrieve or construct the parameter values array + Object parameters[] = null; + if (paramCount > 0) { + parameters = (Object[]) digester.popParams(); + } else { + parameters = new Object[0]; + super.end(namespace, name); + } + + ArrayList multiParams = (ArrayList) parameters[multiParamIndex]; + + // Construct the parameter values array we will need + // We only do the conversion if the param value is a String and + // the specified paramType is not String. + Object paramValues[] = new Object[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + if (i != multiParamIndex) { + // convert nulls and convert stringy parameters + // for non-stringy param types + if(parameters[i] == null || (parameters[i] instanceof String + && !String.class.isAssignableFrom(paramTypes[i]))) { + paramValues[i] = + IntrospectionUtils.convert((String) parameters[i], paramTypes[i]); + } else { + paramValues[i] = parameters[i]; + } + } + } + + // Determine the target object for the method call + Object target; + if (targetOffset >= 0) { + target = digester.peek(targetOffset); + } else { + target = digester.peek(digester.getCount() + targetOffset); + } + + if (target == null) { + StringBuilder sb = new StringBuilder(); + sb.append("[CallMethodRule]{"); + sb.append(""); + sb.append("} Call target is null ("); + sb.append("targetOffset="); + sb.append(targetOffset); + sb.append(",stackdepth="); + sb.append(digester.getCount()); + sb.append(')'); + throw new org.xml.sax.SAXException(sb.toString()); + } + + if (multiParams == null) { + paramValues[multiParamIndex] = null; + IntrospectionUtils.callMethodN(target, methodName, paramValues, + paramTypes); + return; + } + + for (Object param : multiParams) { + if (param == null || (param instanceof String + && !String.class.isAssignableFrom(paramTypes[multiParamIndex]))) { + paramValues[multiParamIndex] = + IntrospectionUtils.convert((String) param, paramTypes[multiParamIndex]); + } else { + paramValues[multiParamIndex] = param; + } + IntrospectionUtils.callMethodN(target, methodName, paramValues, + paramTypes); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(target)).append('.').append(methodName); + code.append('('); + for (int i = 0; i < paramValues.length; i++) { + if (i > 0) { + code.append(", "); + } + if (paramValues[i] instanceof String) { + code.append("\"").append(paramValues[i].toString()).append("\""); + } else { + code.append(digester.toVariableName(paramValues[i])); + } + } + code.append(");"); + code.append(System.lineSeparator()); + } + } + + } + +} + + + +/** + * A Rule that check if the annotations have to be loaded. + * + */ + +final class IgnoreAnnotationsRule extends Rule { + + IgnoreAnnotationsRule() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + WebXml webXml = (WebXml) digester.peek(digester.getCount() - 1); + String value = attributes.getValue("metadata-complete"); + if ("true".equals(value)) { + webXml.setMetadataComplete(true); + } else if ("false".equals(value)) { + webXml.setMetadataComplete(false); + } else { + value = null; + } + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace + (webXml.getClass().getName() + ".setMetadataComplete( " + + webXml.isMetadataComplete() + ")"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (value != null && code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(webXml)).append(".setMetadataComplete("); + code.append(value).append(");"); + code.append(System.lineSeparator()); + } + } + +} + +/** + * A Rule that records the spec version of the web.xml being parsed + * + */ + +final class VersionRule extends Rule { + + VersionRule() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + WebXml webXml = (WebXml) digester.peek(digester.getCount() - 1); + webXml.setVersion(attributes.getValue("version")); + + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace + (webXml.getClass().getName() + ".setVersion( " + + webXml.getVersion() + ")"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(webXml)).append(".setVersion(\""); + code.append(attributes.getValue("version")).append("\");"); + code.append(System.lineSeparator()); + } + } + +} + + +/** + * A rule that ensures only a single name element is present. + */ +final class NameRule extends Rule { + + boolean isNameSet = false; + + NameRule() { + // NO-OP + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + if (isNameSet){ + throw new IllegalArgumentException(WebRuleSet.sm.getString( + "webRuleSet.nameCount")); + } + isNameSet = true; + } + + @Override + public void body(String namespace, String name, String text) + throws Exception { + ((WebXml) digester.peek()).setName(text); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(digester.peek())).append(".setName(\""); + code.append(text).append("\");"); + code.append(System.lineSeparator()); + } + } +} + + +/** + * A rule that logs a warning if absolute ordering is configured for a fragment + * and fails if multiple absolute orders are configured. + */ +final class AbsoluteOrderingRule extends Rule { + + boolean isAbsoluteOrderingSet = false; + private final boolean fragment; + + AbsoluteOrderingRule(boolean fragment) { + this.fragment = fragment; + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + if (fragment) { + digester.getLogger().warn( + WebRuleSet.sm.getString("webRuleSet.absoluteOrdering")); + } + if (isAbsoluteOrderingSet) { + throw new IllegalArgumentException(WebRuleSet.sm.getString( + "webRuleSet.absoluteOrderingCount")); + } else { + isAbsoluteOrderingSet = true; + WebXml webXml = (WebXml) digester.peek(); + webXml.createAbsoluteOrdering(); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace( + webXml.getClass().getName() + ".setAbsoluteOrdering()"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(webXml)).append(".createAbsoluteOrdering();"); + code.append(System.lineSeparator()); + } + } + } +} + +/** + * A rule that logs a warning if relative ordering is configured. + */ +final class RelativeOrderingRule extends Rule { + + boolean isRelativeOrderingSet = false; + private final boolean fragment; + + RelativeOrderingRule(boolean fragment) { + this.fragment = fragment; + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + if (!fragment) { + digester.getLogger().warn( + WebRuleSet.sm.getString("webRuleSet.relativeOrdering")); + } + if (isRelativeOrderingSet) { + throw new IllegalArgumentException(WebRuleSet.sm.getString( + "webRuleSet.relativeOrderingCount")); + } else { + isRelativeOrderingSet = true; + } + } +} + +/** + * A Rule that sets soap headers on the ContextHandler. + * + */ +final class SoapHeaderRule extends Rule { + + SoapHeaderRule() { + // NO-OP + } + + /** + * Process the body text of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param text The body text of this element + */ + @Override + public void body(String namespace, String name, String text) + throws Exception { + String namespaceuri = null; + String localpart = text; + int colon = text.indexOf(':'); + if (colon >= 0) { + String prefix = text.substring(0,colon); + namespaceuri = digester.findNamespaceURI(prefix); + localpart = text.substring(colon+1); + } + ContextHandler contextHandler = (ContextHandler) digester.peek(); + contextHandler.addSoapHeaders(localpart, namespaceuri); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(contextHandler)).append(".addSoapHeaders(\""); + code.append(localpart).append("\", \"").append(namespaceuri).append("\");"); + code.append(System.lineSeparator()); + } + } +} + +/** + * A Rule that sets service qname on the ContextService. + * + */ +final class ServiceQnameRule extends Rule { + + ServiceQnameRule() { + // NO-OP + } + + /** + * Process the body text of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param text The body text of this element + */ + @Override + public void body(String namespace, String name, String text) + throws Exception { + String namespaceuri = null; + String localpart = text; + int colon = text.indexOf(':'); + if (colon >= 0) { + String prefix = text.substring(0,colon); + namespaceuri = digester.findNamespaceURI(prefix); + localpart = text.substring(colon+1); + } + ContextService contextService = (ContextService) digester.peek(); + contextService.setServiceqnameLocalpart(localpart); + contextService.setServiceqnameNamespaceURI(namespaceuri); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(contextService)).append(".setServiceqnameLocalpart(\""); + code.append(localpart).append("\");"); + code.append(System.lineSeparator()); + code.append(digester.toVariableName(contextService)).append(".setServiceqnameNamespaceURI(\""); + code.append(namespaceuri).append("\");"); + code.append(System.lineSeparator()); + } + } + +} + +/** + * A rule that checks if the taglib element is in the right place. + */ +final class TaglibLocationRule extends Rule { + + final boolean isServlet24OrLater; + + TaglibLocationRule(boolean isServlet24OrLater) { + this.isServlet24OrLater = isServlet24OrLater; + } + + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + WebXml webXml = (WebXml) digester.peek(digester.getCount() - 1); + // If we have a public ID, this is not a 2.4 or later webapp + boolean havePublicId = (webXml.getPublicId() != null); + // havePublicId and isServlet24OrLater should be mutually exclusive + if (havePublicId == isServlet24OrLater) { + throw new IllegalArgumentException( + "taglib definition not consistent with specification version"); + } + } +} + +/** + * A Rule that sets mapped name on the ResourceBase. + */ +final class MappedNameRule extends Rule { + + MappedNameRule() { + // NO-OP + } + + /** + * Process the body text of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param text The body text of this element + */ + @Override + public void body(String namespace, String name, String text) + throws Exception { + ResourceBase resourceBase = (ResourceBase) digester.peek(); + resourceBase.setProperty("mappedName", text.trim()); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(resourceBase)); + code.append(".setProperty(\"mappedName\", \"").append(text.trim()).append("\");"); + code.append(System.lineSeparator()); + } + } +} + +/** + * A rule that fails if more than one post construct or pre destroy methods + * are configured per class. + */ +final class LifecycleCallbackRule extends CallMethodRule { + + private final boolean postConstruct; + + LifecycleCallbackRule(String methodName, int paramCount, + boolean postConstruct) { + super(methodName, paramCount); + this.postConstruct = postConstruct; + } + + @Override + public void end(String namespace, String name) throws Exception { + Object[] params = (Object[]) digester.peekParams(); + if (params != null && params.length == 2) { + WebXml webXml = (WebXml) digester.peek(); + if (postConstruct) { + if (webXml.getPostConstructMethods().containsKey(params[0])) { + throw new IllegalArgumentException(WebRuleSet.sm.getString( + "webRuleSet.postconstruct.duplicate", params[0])); + } + } else { + if (webXml.getPreDestroyMethods().containsKey(params[0])) { + throw new IllegalArgumentException(WebRuleSet.sm.getString( + "webRuleSet.predestroy.duplicate", params[0])); + } + } + } + super.end(namespace, name); + } +} + +final class SetOverrideRule extends Rule { + + SetOverrideRule() { + // no-op + } + + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + ContextEnvironment envEntry = (ContextEnvironment) digester.peek(); + envEntry.setOverride(false); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace(envEntry.getClass().getName() + ".setOverride(false)"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(envEntry)).append(".setOverride(false);"); + code.append(System.lineSeparator()); + } + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/WebXml.java b/java/org/apache/tomcat/util/descriptor/web/WebXml.java new file mode 100644 index 0000000..94c8836 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/WebXml.java @@ -0,0 +1,2362 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.ServletContext; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.descriptor.JspPropertyGroupDescriptor; +import jakarta.servlet.descriptor.TaglibDescriptor; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.descriptor.XmlIdentifiers; +import org.apache.tomcat.util.digester.DocumentProperties; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.Escape; + +/** + * Representation of common elements of web.xml and web-fragment.xml. Provides + * a repository for parsed data before the elements are merged. + * Validation is spread between multiple classes: + * The digester checks for structural correctness (eg single login-config) + * This class checks for invalid duplicates (eg filter/servlet names) + * StandardContext will check validity of values (eg URL formats etc) + */ +public class WebXml extends XmlEncodingBase implements DocumentProperties.Charset { + + protected static final String ORDER_OTHERS = + "org.apache.catalina.order.others"; + + private static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + private final Log log = LogFactory.getLog(WebXml.class); // must not be static + + /** + * Global defaults are overridable but Servlets and Servlet mappings need to + * be unique. Duplicates normally trigger an error. This flag indicates if + * newly added Servlet elements are marked as overridable. + */ + private boolean overridable = false; + public boolean isOverridable() { + return overridable; + } + public void setOverridable(boolean overridable) { + this.overridable = overridable; + } + + /* + * Ideally, fragment names will be unique. If they are not, Tomcat needs + * to know as the action that the specification requires (see 8.2.2 1.e and + * 2.c) varies depending on the ordering method used. + */ + private final List duplicates = new ArrayList<>(); + public boolean isDuplicated() { + return !duplicates.isEmpty(); + } + public void addDuplicate(String duplicate) { + this.duplicates.add(duplicate); + } + public List getDuplicates() { + return new ArrayList<>(this.duplicates); + } + + /** + * web.xml only elements + * Absolute Ordering + */ + private Set absoluteOrdering = null; + public void createAbsoluteOrdering() { + if (absoluteOrdering == null) { + absoluteOrdering = new LinkedHashSet<>(); + } + } + public void addAbsoluteOrdering(String fragmentName) { + createAbsoluteOrdering(); + absoluteOrdering.add(fragmentName); + } + public void addAbsoluteOrderingOthers() { + createAbsoluteOrdering(); + absoluteOrdering.add(ORDER_OTHERS); + } + public Set getAbsoluteOrdering() { + return absoluteOrdering; + } + + /** + * web-fragment.xml only elements + * Relative ordering + */ + private final Set after = new LinkedHashSet<>(); + public void addAfterOrdering(String fragmentName) { + after.add(fragmentName); + } + public void addAfterOrderingOthers() { + if (before.contains(ORDER_OTHERS)) { + throw new IllegalArgumentException(sm.getString( + "webXml.multipleOther")); + } + after.add(ORDER_OTHERS); + } + public Set getAfterOrdering() { return after; } + + private final Set before = new LinkedHashSet<>(); + public void addBeforeOrdering(String fragmentName) { + before.add(fragmentName); + } + public void addBeforeOrderingOthers() { + if (after.contains(ORDER_OTHERS)) { + throw new IllegalArgumentException(sm.getString( + "webXml.multipleOther")); + } + before.add(ORDER_OTHERS); + } + public Set getBeforeOrdering() { return before; } + + // Common elements and attributes + // Required attribute of web-app element + public String getVersion() { + StringBuilder sb = new StringBuilder(3); + sb.append(majorVersion); + sb.append('.'); + sb.append(minorVersion); + return sb.toString(); + } + /** + * Set the version for this web.xml file + * @param version Values of null will be ignored + */ + public void setVersion(String version) { + if (version == null) { + return; + } + switch (version) { + case "2.4": + majorVersion = 2; + minorVersion = 4; + break; + case "2.5": + majorVersion = 2; + minorVersion = 5; + break; + case "3.0": + majorVersion = 3; + minorVersion = 0; + break; + case "3.1": + majorVersion = 3; + minorVersion = 1; + break; + case "4.0": + majorVersion = 4; + minorVersion = 0; + break; + case "5.0": + majorVersion = 5; + minorVersion = 0; + break; + case "6.0": + majorVersion = 6; + minorVersion = 0; + break; + default: + log.warn(sm.getString("webXml.version.unknown", version)); + } + } + + + + // Optional publicId attribute + private String publicId = null; + public String getPublicId() { return publicId; } + public void setPublicId(String publicId) { + // Update major and minor version + if (publicId == null) { + return; + } + switch (publicId) { + case XmlIdentifiers.WEB_22_PUBLIC: + majorVersion = 2; + minorVersion = 2; + this.publicId = publicId; + break; + case XmlIdentifiers.WEB_23_PUBLIC: + majorVersion = 2; + minorVersion = 3; + this.publicId = publicId; + break; + default: + log.warn(sm.getString("webXml.unrecognisedPublicId", publicId)); + break; + } + } + + // Optional metadata-complete attribute + private boolean metadataComplete = false; + public boolean isMetadataComplete() { return metadataComplete; } + public void setMetadataComplete(boolean metadataComplete) { + this.metadataComplete = metadataComplete; } + + // Optional name element + private String name = null; + public String getName() { return name; } + public void setName(String name) { + if (ORDER_OTHERS.equalsIgnoreCase(name)) { + // This is unusual. This name will be ignored. Log the fact. + log.warn(sm.getString("webXml.reservedName", name)); + } else { + this.name = name; + } + } + + // Derived major and minor version attributes + private int majorVersion = 6; + private int minorVersion = 0; + public int getMajorVersion() { return majorVersion; } + public int getMinorVersion() { return minorVersion; } + + // web-app elements + // TODO: Ignored elements: + // - description + // - icon + + // display-name - TODO should support multiple with language + private String displayName = null; + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + // distributable + private boolean distributable = false; + public boolean isDistributable() { return distributable; } + public void setDistributable(boolean distributable) { + this.distributable = distributable; + } + + // deny-uncovered-http-methods + private boolean denyUncoveredHttpMethods = false; + public boolean getDenyUncoveredHttpMethods() { + return denyUncoveredHttpMethods; + } + public void setDenyUncoveredHttpMethods(boolean denyUncoveredHttpMethods) { + this.denyUncoveredHttpMethods = denyUncoveredHttpMethods; + } + + // context-param + // TODO: description (multiple with language) is ignored + private final Map contextParams = new HashMap<>(); + public void addContextParam(String param, String value) { + contextParams.put(param, value); + } + public Map getContextParams() { return contextParams; } + + // filter + // TODO: Should support multiple description elements with language + // TODO: Should support multiple display-name elements with language + // TODO: Should support multiple icon elements + // TODO: Description for init-param is ignored + private final Map filters = new LinkedHashMap<>(); + public void addFilter(FilterDef filter) { + if (filters.containsKey(filter.getFilterName())) { + // Filter names must be unique within a web(-fragment).xml + throw new IllegalArgumentException( + sm.getString("webXml.duplicateFilter", + filter.getFilterName())); + } + filters.put(filter.getFilterName(), filter); + } + public Map getFilters() { return filters; } + + // filter-mapping + private final Set filterMaps = new LinkedHashSet<>(); + private final Set filterMappingNames = new HashSet<>(); + public void addFilterMapping(FilterMap filterMap) { + filterMap.setCharset(getCharset()); + filterMaps.add(filterMap); + filterMappingNames.add(filterMap.getFilterName()); + } + public Set getFilterMappings() { return filterMaps; } + + // listener + // TODO: description (multiple with language) is ignored + // TODO: display-name (multiple with language) is ignored + // TODO: icon (multiple) is ignored + private final Set listeners = new LinkedHashSet<>(); + public void addListener(String className) { + listeners.add(className); + } + public Set getListeners() { return listeners; } + + // servlet + // TODO: description (multiple with language) is ignored + // TODO: display-name (multiple with language) is ignored + // TODO: icon (multiple) is ignored + // TODO: init-param/description (multiple with language) is ignored + // TODO: security-role-ref/description (multiple with language) is ignored + private final Map servlets = new HashMap<>(); + public void addServlet(ServletDef servletDef) { + servlets.put(servletDef.getServletName(), servletDef); + if (overridable) { + servletDef.setOverridable(overridable); + } + } + public Map getServlets() { return servlets; } + + // servlet-mapping + // Note: URLPatterns from web.xml may be URL encoded + // (https://svn.apache.org/r285186) + private final Map servletMappings = new HashMap<>(); + private final Set servletMappingNames = new HashSet<>(); + public void addServletMapping(String urlPattern, String servletName) { + addServletMappingDecoded(UDecoder.URLDecode(urlPattern, getCharset()), servletName); + } + public void addServletMappingDecoded(String urlPattern, String servletName) { + String oldServletName = servletMappings.put(urlPattern, servletName); + if (oldServletName != null) { + // Duplicate mapping. As per clarification from the Servlet EG, + // deployment should fail. + throw new IllegalArgumentException(sm.getString( + "webXml.duplicateServletMapping", oldServletName, + servletName, urlPattern)); + } + servletMappingNames.add(servletName); + } + public Map getServletMappings() { return servletMappings; } + + // session-config + // Digester will check there is only one of these + private SessionConfig sessionConfig = new SessionConfig(); + public void setSessionConfig(SessionConfig sessionConfig) { + this.sessionConfig = sessionConfig; + } + public SessionConfig getSessionConfig() { return sessionConfig; } + + // mime-mapping + private final Map mimeMappings = new HashMap<>(); + public void addMimeMapping(String extension, String mimeType) { + mimeMappings.put(extension, mimeType); + } + public Map getMimeMappings() { return mimeMappings; } + + // welcome-file-list merge control + private boolean replaceWelcomeFiles = false; + private boolean alwaysAddWelcomeFiles = true; + /** + * When merging/parsing web.xml files into this web.xml should the current + * set be completely replaced? + * @param replaceWelcomeFiles true to replace welcome files + * rather than add to the list + */ + public void setReplaceWelcomeFiles(boolean replaceWelcomeFiles) { + this.replaceWelcomeFiles = replaceWelcomeFiles; + } + /** + * When merging from this web.xml, should the welcome files be added to the + * target web.xml even if it already contains welcome file definitions. + * @param alwaysAddWelcomeFiles true to add welcome files + */ + public void setAlwaysAddWelcomeFiles(boolean alwaysAddWelcomeFiles) { + this.alwaysAddWelcomeFiles = alwaysAddWelcomeFiles; + } + + // welcome-file-list + private final Set welcomeFiles = new LinkedHashSet<>(); + public void addWelcomeFile(String welcomeFile) { + if (replaceWelcomeFiles) { + welcomeFiles.clear(); + replaceWelcomeFiles = false; + } + welcomeFiles.add(welcomeFile); + } + public Set getWelcomeFiles() { return welcomeFiles; } + + // error-page + private final Map errorPages = new HashMap<>(); + public void addErrorPage(ErrorPage errorPage) { + errorPage.setCharset(getCharset()); + errorPages.put(errorPage.getName(), errorPage); + } + public Map getErrorPages() { return errorPages; } + + // Digester will check there is only one jsp-config + // jsp-config/taglib or taglib (2.3 and earlier) + private final Map taglibs = new HashMap<>(); + public void addTaglib(String uri, String location) { + if (taglibs.containsKey(uri)) { + // Taglib URIs must be unique within a web(-fragment).xml + throw new IllegalArgumentException( + sm.getString("webXml.duplicateTaglibUri", uri)); + } + taglibs.put(uri, location); + } + public Map getTaglibs() { return taglibs; } + + // jsp-config/jsp-property-group + private final Set jspPropertyGroups = new LinkedHashSet<>(); + public void addJspPropertyGroup(JspPropertyGroup propertyGroup) { + propertyGroup.setCharset(getCharset()); + jspPropertyGroups.add(propertyGroup); + } + public Set getJspPropertyGroups() { + return jspPropertyGroups; + } + + // security-constraint + // TODO: Should support multiple display-name elements with language + // TODO: Should support multiple description elements with language + private final Set securityConstraints = new HashSet<>(); + public void addSecurityConstraint(SecurityConstraint securityConstraint) { + securityConstraint.setCharset(getCharset()); + securityConstraints.add(securityConstraint); + } + public Set getSecurityConstraints() { + return securityConstraints; + } + + // login-config + // Digester will check there is only one of these + private LoginConfig loginConfig = null; + public void setLoginConfig(LoginConfig loginConfig) { + loginConfig.setCharset(getCharset()); + this.loginConfig = loginConfig; + } + public LoginConfig getLoginConfig() { return loginConfig; } + + // security-role + // TODO: description (multiple with language) is ignored + private final Set securityRoles = new HashSet<>(); + public void addSecurityRole(String securityRole) { + securityRoles.add(securityRole); + } + public Set getSecurityRoles() { return securityRoles; } + + // env-entry + // TODO: Should support multiple description elements with language + private final Map envEntries = new HashMap<>(); + public void addEnvEntry(ContextEnvironment envEntry) { + if (envEntries.containsKey(envEntry.getName())) { + // env-entry names must be unique within a web(-fragment).xml + throw new IllegalArgumentException( + sm.getString("webXml.duplicateEnvEntry", + envEntry.getName())); + } + envEntries.put(envEntry.getName(),envEntry); + } + public Map getEnvEntries() { return envEntries; } + + // ejb-ref + // TODO: Should support multiple description elements with language + private final Map ejbRefs = new HashMap<>(); + public void addEjbRef(ContextEjb ejbRef) { + ejbRefs.put(ejbRef.getName(),ejbRef); + } + public Map getEjbRefs() { return ejbRefs; } + + // ejb-local-ref + // TODO: Should support multiple description elements with language + private final Map ejbLocalRefs = new HashMap<>(); + public void addEjbLocalRef(ContextLocalEjb ejbLocalRef) { + ejbLocalRefs.put(ejbLocalRef.getName(),ejbLocalRef); + } + public Map getEjbLocalRefs() { + return ejbLocalRefs; + } + + // service-ref + // TODO: Should support multiple description elements with language + // TODO: Should support multiple display-names elements with language + // TODO: Should support multiple icon elements ??? + private final Map serviceRefs = new HashMap<>(); + public void addServiceRef(ContextService serviceRef) { + serviceRefs.put(serviceRef.getName(), serviceRef); + } + public Map getServiceRefs() { return serviceRefs; } + + // resource-ref + // TODO: Should support multiple description elements with language + private final Map resourceRefs = new HashMap<>(); + public void addResourceRef(ContextResource resourceRef) { + if (resourceRefs.containsKey(resourceRef.getName())) { + // resource-ref names must be unique within a web(-fragment).xml + throw new IllegalArgumentException( + sm.getString("webXml.duplicateResourceRef", + resourceRef.getName())); + } + resourceRefs.put(resourceRef.getName(), resourceRef); + } + public Map getResourceRefs() { + return resourceRefs; + } + + // resource-env-ref + // TODO: Should support multiple description elements with language + private final Map resourceEnvRefs = new HashMap<>(); + public void addResourceEnvRef(ContextResourceEnvRef resourceEnvRef) { + if (resourceEnvRefs.containsKey(resourceEnvRef.getName())) { + // resource-env-ref names must be unique within a web(-fragment).xml + throw new IllegalArgumentException( + sm.getString("webXml.duplicateResourceEnvRef", + resourceEnvRef.getName())); + } + resourceEnvRefs.put(resourceEnvRef.getName(), resourceEnvRef); + } + public Map getResourceEnvRefs() { + return resourceEnvRefs; + } + + // message-destination-ref + // TODO: Should support multiple description elements with language + private final Map messageDestinationRefs = + new HashMap<>(); + public void addMessageDestinationRef( + MessageDestinationRef messageDestinationRef) { + if (messageDestinationRefs.containsKey( + messageDestinationRef.getName())) { + // message-destination-ref names must be unique within a + // web(-fragment).xml + throw new IllegalArgumentException(sm.getString( + "webXml.duplicateMessageDestinationRef", + messageDestinationRef.getName())); + } + messageDestinationRefs.put(messageDestinationRef.getName(), + messageDestinationRef); + } + public Map getMessageDestinationRefs() { + return messageDestinationRefs; + } + + // message-destination + // TODO: Should support multiple description elements with language + // TODO: Should support multiple display-names elements with language + // TODO: Should support multiple icon elements ??? + private final Map messageDestinations = + new HashMap<>(); + public void addMessageDestination( + MessageDestination messageDestination) { + if (messageDestinations.containsKey( + messageDestination.getName())) { + // message-destination names must be unique within a + // web(-fragment).xml + throw new IllegalArgumentException( + sm.getString("webXml.duplicateMessageDestination", + messageDestination.getName())); + } + messageDestinations.put(messageDestination.getName(), + messageDestination); + } + public Map getMessageDestinations() { + return messageDestinations; + } + + // locale-encoding-mapping-list + private final Map localeEncodingMappings = new HashMap<>(); + public void addLocaleEncodingMapping(String locale, String encoding) { + localeEncodingMappings.put(locale, encoding); + } + public Map getLocaleEncodingMappings() { + return localeEncodingMappings; + } + + // post-construct elements + private Map postConstructMethods = new HashMap<>(); + public void addPostConstructMethods(String clazz, String method) { + if (!postConstructMethods.containsKey(clazz)) { + postConstructMethods.put(clazz, method); + } + } + public Map getPostConstructMethods() { + return postConstructMethods; + } + + // pre-destroy elements + private Map preDestroyMethods = new HashMap<>(); + public void addPreDestroyMethods(String clazz, String method) { + if (!preDestroyMethods.containsKey(clazz)) { + preDestroyMethods.put(clazz, method); + } + } + public Map getPreDestroyMethods() { + return preDestroyMethods; + } + + public JspConfigDescriptor getJspConfigDescriptor() { + if (jspPropertyGroups.isEmpty() && taglibs.isEmpty()) { + return null; + } + + Collection descriptors = + new ArrayList<>(jspPropertyGroups.size()); + for (JspPropertyGroup jspPropertyGroup : jspPropertyGroups) { + JspPropertyGroupDescriptor descriptor = + new JspPropertyGroupDescriptorImpl(jspPropertyGroup); + descriptors.add(descriptor); + + } + + Collection tlds = new HashSet<>(taglibs.size()); + for (Entry entry : taglibs.entrySet()) { + TaglibDescriptor descriptor = new TaglibDescriptorImpl( + entry.getValue(), entry.getKey()); + tlds.add(descriptor); + } + return new JspConfigDescriptorImpl(descriptors, tlds); + } + + private String requestCharacterEncoding; + public String getRequestCharacterEncoding() { + return requestCharacterEncoding; + } + public void setRequestCharacterEncoding(String requestCharacterEncoding) { + if (requestCharacterEncoding != null) { + try { + B2CConverter.getCharset(requestCharacterEncoding); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + } + this.requestCharacterEncoding = requestCharacterEncoding; + } + + private String responseCharacterEncoding; + public String getResponseCharacterEncoding() { + return responseCharacterEncoding; + } + public void setResponseCharacterEncoding(String responseCharacterEncoding) { + if (responseCharacterEncoding != null) { + try { + B2CConverter.getCharset(responseCharacterEncoding); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + } + this.responseCharacterEncoding = responseCharacterEncoding; + } + + // Attributes not defined in web.xml or web-fragment.xml + + // URL of JAR / exploded JAR for this web-fragment + private URL uRL = null; + public void setURL(URL url) { this.uRL = url; } + public URL getURL() { return uRL; } + + // Name of jar file + private String jarName = null; + public void setJarName(String jarName) { this.jarName = jarName; } + public String getJarName() { return jarName; } + + // Is this JAR part of the application or is it a container JAR? Assume it + // is. + private boolean webappJar = true; + public void setWebappJar(boolean webappJar) { this.webappJar = webappJar; } + public boolean getWebappJar() { return webappJar; } + + // Does this web application delegate first for class loading? + private boolean delegate = false; + public boolean getDelegate() { return delegate; } + public void setDelegate(boolean delegate) { this.delegate = delegate; } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(32); + buf.append("Name: "); + buf.append(getName()); + buf.append(", URL: "); + buf.append(getURL()); + return buf.toString(); + } + + private static final String INDENT2 = " "; + private static final String INDENT4 = " "; + private static final String INDENT6 = " "; + + /** + * Generate a web.xml in String form that matches the representation stored + * in this object. + * + * @return The complete contents of web.xml as a String + */ + public String toXml() { + StringBuilder sb = new StringBuilder(2048); + // TODO - Various, icon, description etc elements are skipped - mainly + // because they are ignored when web.xml is parsed - see above + + // NOTE - Elements need to be written in the order defined in the 2.3 + // DTD else validation of the merged web.xml will fail + + // NOTE - Some elements need to be skipped based on the version of the + // specification being used. Version is validated and starts at + // 2.2. The version tests used in this method take advantage of + // this. + + // Declaration + sb.append("\n"); + + // Root element + if (publicId != null) { + sb.append("\n"); + sb.append(""); + } else { + String javaeeNamespace = null; + String webXmlSchemaLocation = null; + String version = getVersion(); + if ("2.4".equals(version)) { + javaeeNamespace = XmlIdentifiers.JAVAEE_1_4_NS; + webXmlSchemaLocation = XmlIdentifiers.WEB_24_XSD; + } else if ("2.5".equals(version)) { + javaeeNamespace = XmlIdentifiers.JAVAEE_5_NS; + webXmlSchemaLocation = XmlIdentifiers.WEB_25_XSD; + } else if ("3.0".equals(version)) { + javaeeNamespace = XmlIdentifiers.JAVAEE_6_NS; + webXmlSchemaLocation = XmlIdentifiers.WEB_30_XSD; + } else if ("3.1".equals(version)) { + javaeeNamespace = XmlIdentifiers.JAVAEE_7_NS; + webXmlSchemaLocation = XmlIdentifiers.WEB_31_XSD; + } else if ("4.0".equals(version)) { + javaeeNamespace = XmlIdentifiers.JAVAEE_8_NS; + webXmlSchemaLocation = XmlIdentifiers.WEB_40_XSD; + } else if ("5.0".equals(version)) { + javaeeNamespace = XmlIdentifiers.JAKARTAEE_9_NS; + webXmlSchemaLocation = XmlIdentifiers.WEB_50_XSD; + } else if ("6.0".equals(version)) { + javaeeNamespace = XmlIdentifiers.JAKARTAEE_10_NS; + webXmlSchemaLocation = XmlIdentifiers.WEB_60_XSD; + } + sb.append("\n\n"); + } else { + sb.append("\n metadata-complete=\"true\">\n\n"); + } + } + + appendElement(sb, INDENT2, "display-name", displayName); + + if (isDistributable()) { + sb.append(" \n\n"); + } + + for (Map.Entry entry : contextParams.entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "param-name", entry.getKey()); + appendElement(sb, INDENT4, "param-value", entry.getValue()); + sb.append(" \n"); + } + sb.append('\n'); + + // Filters were introduced in Servlet 2.3 + if (getMajorVersion() > 2 || getMinorVersion() > 2) { + for (Map.Entry entry : filters.entrySet()) { + FilterDef filterDef = entry.getValue(); + sb.append(" \n"); + appendElement(sb, INDENT4, "description", + filterDef.getDescription()); + appendElement(sb, INDENT4, "display-name", + filterDef.getDisplayName()); + appendElement(sb, INDENT4, "filter-name", + filterDef.getFilterName()); + appendElement(sb, INDENT4, "filter-class", + filterDef.getFilterClass()); + // Async support was introduced for Servlet 3.0 onwards + if (getMajorVersion() != 2) { + appendElement(sb, INDENT4, "async-supported", + filterDef.getAsyncSupported()); + } + for (Map.Entry param : + filterDef.getParameterMap().entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "param-name", param.getKey()); + appendElement(sb, INDENT6, "param-value", param.getValue()); + sb.append(" \n"); + } + sb.append(" \n"); + } + sb.append('\n'); + + for (FilterMap filterMap : filterMaps) { + sb.append(" \n"); + appendElement(sb, INDENT4, "filter-name", + filterMap.getFilterName()); + if (filterMap.getMatchAllServletNames()) { + sb.append(" *\n"); + } else { + for (String servletName : filterMap.getServletNames()) { + appendElement(sb, INDENT4, "servlet-name", servletName); + } + } + if (filterMap.getMatchAllUrlPatterns()) { + sb.append(" *\n"); + } else { + for (String urlPattern : filterMap.getURLPatterns()) { + appendElement(sb, INDENT4, "url-pattern", encodeUrl(urlPattern)); + } + } + // dispatcher was added in Servlet 2.4 + if (getMajorVersion() > 2 || getMinorVersion() > 3) { + for (String dispatcher : filterMap.getDispatcherNames()) { + if (getMajorVersion() == 2 && + DispatcherType.ASYNC.name().equals(dispatcher)) { + continue; + } + appendElement(sb, INDENT4, "dispatcher", dispatcher); + } + } + sb.append(" \n"); + } + sb.append('\n'); + } + + // Listeners were introduced in Servlet 2.3 + if (getMajorVersion() > 2 || getMinorVersion() > 2) { + for (String listener : listeners) { + sb.append(" \n"); + appendElement(sb, INDENT4, "listener-class", listener); + sb.append(" \n"); + } + sb.append('\n'); + } + + for (Map.Entry entry : servlets.entrySet()) { + ServletDef servletDef = entry.getValue(); + sb.append(" \n"); + appendElement(sb, INDENT4, "description", + servletDef.getDescription()); + appendElement(sb, INDENT4, "display-name", + servletDef.getDisplayName()); + appendElement(sb, INDENT4, "servlet-name", entry.getKey()); + appendElement(sb, INDENT4, "servlet-class", + servletDef.getServletClass()); + appendElement(sb, INDENT4, "jsp-file", servletDef.getJspFile()); + for (Map.Entry param : + servletDef.getParameterMap().entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "param-name", param.getKey()); + appendElement(sb, INDENT6, "param-value", param.getValue()); + sb.append(" \n"); + } + appendElement(sb, INDENT4, "load-on-startup", + servletDef.getLoadOnStartup()); + appendElement(sb, INDENT4, "enabled", servletDef.getEnabled()); + // Async support was introduced for Servlet 3.0 onwards + if (getMajorVersion() != 2) { + appendElement(sb, INDENT4, "async-supported", + servletDef.getAsyncSupported()); + } + // servlet/run-as was introduced in Servlet 2.3 + if (getMajorVersion() > 2 || getMinorVersion() > 2) { + if (servletDef.getRunAs() != null) { + sb.append(" \n"); + appendElement(sb, INDENT6, "role-name", servletDef.getRunAs()); + sb.append(" \n"); + } + } + for (SecurityRoleRef roleRef : servletDef.getSecurityRoleRefs()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "role-name", roleRef.getName()); + appendElement(sb, INDENT6, "role-link", roleRef.getLink()); + sb.append(" \n"); + } + // multipart-config was added in Servlet 3.0 + if (getMajorVersion() != 2) { + MultipartDef multipartDef = servletDef.getMultipartDef(); + if (multipartDef != null) { + sb.append(" \n"); + appendElement(sb, INDENT6, "location", + multipartDef.getLocation()); + appendElement(sb, INDENT6, "max-file-size", + multipartDef.getMaxFileSize()); + appendElement(sb, INDENT6, "max-request-size", + multipartDef.getMaxRequestSize()); + appendElement(sb, INDENT6, "file-size-threshold", + multipartDef.getFileSizeThreshold()); + sb.append(" \n"); + } + } + sb.append(" \n"); + } + sb.append('\n'); + + for (Map.Entry entry : servletMappings.entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "servlet-name", entry.getValue()); + appendElement(sb, INDENT4, "url-pattern", encodeUrl(entry.getKey())); + sb.append(" \n"); + } + sb.append('\n'); + + if (sessionConfig != null) { + sb.append(" \n"); + appendElement(sb, INDENT4, "session-timeout", + sessionConfig.getSessionTimeout()); + if (majorVersion >= 3) { + sb.append(" \n"); + appendElement(sb, INDENT6, "name", sessionConfig.getCookieName()); + appendElement(sb, INDENT6, "domain", + sessionConfig.getCookieDomain()); + appendElement(sb, INDENT6, "path", sessionConfig.getCookiePath()); + appendElement(sb, INDENT6, "comment", + sessionConfig.getCookieComment()); + appendElement(sb, INDENT6, "http-only", + sessionConfig.getCookieHttpOnly()); + appendElement(sb, INDENT6, "secure", + sessionConfig.getCookieSecure()); + appendElement(sb, INDENT6, "max-age", + sessionConfig.getCookieMaxAge()); + sb.append(" \n"); + for (SessionTrackingMode stm : + sessionConfig.getSessionTrackingModes()) { + appendElement(sb, INDENT4, "tracking-mode", stm.name()); + } + } + sb.append(" \n\n"); + } + + for (Map.Entry entry : mimeMappings.entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "extension", entry.getKey()); + appendElement(sb, INDENT4, "mime-type", entry.getValue()); + sb.append(" \n"); + } + sb.append('\n'); + + if (welcomeFiles.size() > 0) { + sb.append(" \n"); + for (String welcomeFile : welcomeFiles) { + appendElement(sb, INDENT4, "welcome-file", welcomeFile); + } + sb.append(" \n\n"); + } + + for (ErrorPage errorPage : errorPages.values()) { + String exceptionType = errorPage.getExceptionType(); + int errorCode = errorPage.getErrorCode(); + + if (exceptionType == null && errorCode == 0 && getMajorVersion() == 2) { + // Default error pages are only supported from 3.0 onwards + continue; + } + sb.append(" \n"); + if (errorPage.getExceptionType() != null) { + appendElement(sb, INDENT4, "exception-type", exceptionType); + } else if (errorPage.getErrorCode() > 0) { + appendElement(sb, INDENT4, "error-code", + Integer.toString(errorCode)); + } + appendElement(sb, INDENT4, "location", errorPage.getLocation()); + sb.append(" \n"); + } + sb.append('\n'); + + // jsp-config was added in Servlet 2.4. Prior to that, tag-libs was used + // directly and jsp-property-group did not exist + if (taglibs.size() > 0 || jspPropertyGroups.size() > 0) { + if (getMajorVersion() > 2 || getMinorVersion() > 3) { + sb.append(" \n"); + } + for (Map.Entry entry : taglibs.entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "taglib-uri", entry.getKey()); + appendElement(sb, INDENT6, "taglib-location", entry.getValue()); + sb.append(" \n"); + } + if (getMajorVersion() > 2 || getMinorVersion() > 3) { + for (JspPropertyGroup jpg : jspPropertyGroups) { + sb.append(" \n"); + for (String urlPattern : jpg.getUrlPatterns()) { + appendElement(sb, INDENT6, "url-pattern", encodeUrl(urlPattern)); + } + appendElement(sb, INDENT6, "el-ignored", jpg.getElIgnored()); + appendElement(sb, INDENT6, "page-encoding", + jpg.getPageEncoding()); + appendElement(sb, INDENT6, "scripting-invalid", + jpg.getScriptingInvalid()); + appendElement(sb, INDENT6, "is-xml", jpg.getIsXml()); + for (String prelude : jpg.getIncludePreludes()) { + appendElement(sb, INDENT6, "include-prelude", prelude); + } + for (String coda : jpg.getIncludeCodas()) { + appendElement(sb, INDENT6, "include-coda", coda); + } + appendElement(sb, INDENT6, "deferred-syntax-allowed-as-literal", + jpg.getDeferredSyntax()); + appendElement(sb, INDENT6, "trim-directive-whitespaces", + jpg.getTrimWhitespace()); + appendElement(sb, INDENT6, "default-content-type", + jpg.getDefaultContentType()); + appendElement(sb, INDENT6, "buffer", jpg.getBuffer()); + appendElement(sb, INDENT6, "error-on-undeclared-namespace", + jpg.getErrorOnUndeclaredNamespace()); + sb.append(" \n"); + } + sb.append(" \n\n"); + } + } + + // resource-env-ref was introduced in Servlet 2.3 + if (getMajorVersion() > 2 || getMinorVersion() > 2) { + for (ContextResourceEnvRef resourceEnvRef : resourceEnvRefs.values()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "description", + resourceEnvRef.getDescription()); + appendElement(sb, INDENT4, "resource-env-ref-name", + resourceEnvRef.getName()); + appendElement(sb, INDENT4, "resource-env-ref-type", + resourceEnvRef.getType()); + appendElement(sb, INDENT4, "mapped-name", + resourceEnvRef.getProperty("mappedName")); + for (InjectionTarget target : + resourceEnvRef.getInjectionTargets()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "injection-target-class", + target.getTargetClass()); + appendElement(sb, INDENT6, "injection-target-name", + target.getTargetName()); + sb.append(" \n"); + } + appendElement(sb, INDENT4, "lookup-name", resourceEnvRef.getLookupName()); + sb.append(" \n"); + } + sb.append('\n'); + } + + for (ContextResource resourceRef : resourceRefs.values()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "description", + resourceRef.getDescription()); + appendElement(sb, INDENT4, "res-ref-name", resourceRef.getName()); + appendElement(sb, INDENT4, "res-type", resourceRef.getType()); + appendElement(sb, INDENT4, "res-auth", resourceRef.getAuth()); + // resource-ref/res-sharing-scope was introduced in Servlet 2.3 + if (getMajorVersion() > 2 || getMinorVersion() > 2) { + appendElement(sb, INDENT4, "res-sharing-scope", resourceRef.getScope()); + } + appendElement(sb, INDENT4, "mapped-name", resourceRef.getProperty("mappedName")); + for (InjectionTarget target : resourceRef.getInjectionTargets()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "injection-target-class", + target.getTargetClass()); + appendElement(sb, INDENT6, "injection-target-name", + target.getTargetName()); + sb.append(" \n"); + } + appendElement(sb, INDENT4, "lookup-name", resourceRef.getLookupName()); + sb.append(" \n"); + } + sb.append('\n'); + + for (SecurityConstraint constraint : securityConstraints) { + sb.append(" \n"); + // security-constraint/display-name was introduced in Servlet 2.3 + if (getMajorVersion() > 2 || getMinorVersion() > 2) { + appendElement(sb, INDENT4, "display-name", + constraint.getDisplayName()); + } + for (SecurityCollection collection : constraint.findCollections()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "web-resource-name", + collection.getName()); + appendElement(sb, INDENT6, "description", + collection.getDescription()); + for (String urlPattern : collection.findPatterns()) { + appendElement(sb, INDENT6, "url-pattern", encodeUrl(urlPattern)); + } + for (String method : collection.findMethods()) { + appendElement(sb, INDENT6, "http-method", method); + } + for (String method : collection.findOmittedMethods()) { + appendElement(sb, INDENT6, "http-method-omission", method); + } + sb.append(" \n"); + } + if (constraint.findAuthRoles().length > 0) { + sb.append(" \n"); + for (String role : constraint.findAuthRoles()) { + appendElement(sb, INDENT6, "role-name", role); + } + sb.append(" \n"); + } + if (constraint.getUserConstraint() != null) { + sb.append(" \n"); + appendElement(sb, INDENT6, "transport-guarantee", + constraint.getUserConstraint()); + sb.append(" \n"); + } + sb.append(" \n"); + } + sb.append('\n'); + + if (loginConfig != null) { + sb.append(" \n"); + appendElement(sb, INDENT4, "auth-method", + loginConfig.getAuthMethod()); + appendElement(sb,INDENT4, "realm-name", + loginConfig.getRealmName()); + if (loginConfig.getErrorPage() != null || + loginConfig.getLoginPage() != null) { + sb.append(" \n"); + appendElement(sb, INDENT6, "form-login-page", + loginConfig.getLoginPage()); + appendElement(sb, INDENT6, "form-error-page", + loginConfig.getErrorPage()); + sb.append(" \n"); + } + sb.append(" \n\n"); + } + + for (String roleName : securityRoles) { + sb.append(" \n"); + appendElement(sb, INDENT4, "role-name", roleName); + sb.append(" \n"); + } + + for (ContextEnvironment envEntry : envEntries.values()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "description", + envEntry.getDescription()); + appendElement(sb, INDENT4, "env-entry-name", envEntry.getName()); + appendElement(sb, INDENT4, "env-entry-type", envEntry.getType()); + appendElement(sb, INDENT4, "env-entry-value", envEntry.getValue()); + appendElement(sb, INDENT4, "mapped-name", envEntry.getProperty("mappedName")); + for (InjectionTarget target : envEntry.getInjectionTargets()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "injection-target-class", + target.getTargetClass()); + appendElement(sb, INDENT6, "injection-target-name", + target.getTargetName()); + sb.append(" \n"); + } + appendElement(sb, INDENT4, "lookup-name", envEntry.getLookupName()); + sb.append(" \n"); + } + sb.append('\n'); + + for (ContextEjb ejbRef : ejbRefs.values()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "description", ejbRef.getDescription()); + appendElement(sb, INDENT4, "ejb-ref-name", ejbRef.getName()); + appendElement(sb, INDENT4, "ejb-ref-type", ejbRef.getType()); + appendElement(sb, INDENT4, "home", ejbRef.getHome()); + appendElement(sb, INDENT4, "remote", ejbRef.getRemote()); + appendElement(sb, INDENT4, "ejb-link", ejbRef.getLink()); + appendElement(sb, INDENT4, "mapped-name", ejbRef.getProperty("mappedName")); + for (InjectionTarget target : ejbRef.getInjectionTargets()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "injection-target-class", + target.getTargetClass()); + appendElement(sb, INDENT6, "injection-target-name", + target.getTargetName()); + sb.append(" \n"); + } + appendElement(sb, INDENT4, "lookup-name", ejbRef.getLookupName()); + sb.append(" \n"); + } + sb.append('\n'); + + // ejb-local-ref was introduced in Servlet 2.3 + if (getMajorVersion() > 2 || getMinorVersion() > 2) { + for (ContextLocalEjb ejbLocalRef : ejbLocalRefs.values()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "description", + ejbLocalRef.getDescription()); + appendElement(sb, INDENT4, "ejb-ref-name", ejbLocalRef.getName()); + appendElement(sb, INDENT4, "ejb-ref-type", ejbLocalRef.getType()); + appendElement(sb, INDENT4, "local-home", ejbLocalRef.getHome()); + appendElement(sb, INDENT4, "local", ejbLocalRef.getLocal()); + appendElement(sb, INDENT4, "ejb-link", ejbLocalRef.getLink()); + appendElement(sb, INDENT4, "mapped-name", ejbLocalRef.getProperty("mappedName")); + for (InjectionTarget target : ejbLocalRef.getInjectionTargets()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "injection-target-class", + target.getTargetClass()); + appendElement(sb, INDENT6, "injection-target-name", + target.getTargetName()); + sb.append(" \n"); + } + appendElement(sb, INDENT4, "lookup-name", ejbLocalRef.getLookupName()); + sb.append(" \n"); + } + sb.append('\n'); + } + + // service-ref was introduced in Servlet 2.4 + if (getMajorVersion() > 2 || getMinorVersion() > 3) { + for (ContextService serviceRef : serviceRefs.values()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "description", + serviceRef.getDescription()); + appendElement(sb, INDENT4, "display-name", + serviceRef.getDisplayname()); + appendElement(sb, INDENT4, "service-ref-name", + serviceRef.getName()); + appendElement(sb, INDENT4, "service-interface", + serviceRef.getInterface()); + appendElement(sb, INDENT4, "service-ref-type", + serviceRef.getType()); + appendElement(sb, INDENT4, "wsdl-file", serviceRef.getWsdlfile()); + appendElement(sb, INDENT4, "jaxrpc-mapping-file", + serviceRef.getJaxrpcmappingfile()); + String qname = serviceRef.getServiceqnameNamespaceURI(); + if (qname != null) { + qname = qname + ":"; + } + qname = qname + serviceRef.getServiceqnameLocalpart(); + appendElement(sb, INDENT4, "service-qname", qname); + Iterator endpointIter = serviceRef.getServiceendpoints(); + while (endpointIter.hasNext()) { + String endpoint = endpointIter.next(); + sb.append(" \n"); + appendElement(sb, INDENT6, "service-endpoint-interface", + endpoint); + appendElement(sb, INDENT6, "port-component-link", + serviceRef.getProperty(endpoint)); + sb.append(" \n"); + } + Iterator handlerIter = serviceRef.getHandlers(); + while (handlerIter.hasNext()) { + String handler = handlerIter.next(); + sb.append(" \n"); + ContextHandler ch = serviceRef.getHandler(handler); + appendElement(sb, INDENT6, "handler-name", ch.getName()); + appendElement(sb, INDENT6, "handler-class", + ch.getHandlerclass()); + sb.append(" \n"); + } + // TODO handler-chains + appendElement(sb, INDENT4, "mapped-name", serviceRef.getProperty("mappedName")); + for (InjectionTarget target : serviceRef.getInjectionTargets()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "injection-target-class", + target.getTargetClass()); + appendElement(sb, INDENT6, "injection-target-name", + target.getTargetName()); + sb.append(" \n"); + } + appendElement(sb, INDENT4, "lookup-name", serviceRef.getLookupName()); + sb.append(" \n"); + } + sb.append('\n'); + } + + if (!postConstructMethods.isEmpty()) { + for (Entry entry : postConstructMethods + .entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "lifecycle-callback-class", + entry.getKey()); + appendElement(sb, INDENT4, "lifecycle-callback-method", + entry.getValue()); + sb.append(" \n"); + } + sb.append('\n'); + } + + if (!preDestroyMethods.isEmpty()) { + for (Entry entry : preDestroyMethods + .entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "lifecycle-callback-class", + entry.getKey()); + appendElement(sb, INDENT4, "lifecycle-callback-method", + entry.getValue()); + sb.append(" \n"); + } + sb.append('\n'); + } + + // message-destination-ref, message-destination were introduced in + // Servlet 2.4 + if (getMajorVersion() > 2 || getMinorVersion() > 3) { + for (MessageDestinationRef mdr : messageDestinationRefs.values()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "description", mdr.getDescription()); + appendElement(sb, INDENT4, "message-destination-ref-name", + mdr.getName()); + appendElement(sb, INDENT4, "message-destination-type", + mdr.getType()); + appendElement(sb, INDENT4, "message-destination-usage", + mdr.getUsage()); + appendElement(sb, INDENT4, "message-destination-link", + mdr.getLink()); + appendElement(sb, INDENT4, "mapped-name", mdr.getProperty("mappedName")); + for (InjectionTarget target : mdr.getInjectionTargets()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "injection-target-class", + target.getTargetClass()); + appendElement(sb, INDENT6, "injection-target-name", + target.getTargetName()); + sb.append(" \n"); + } + appendElement(sb, INDENT4, "lookup-name", mdr.getLookupName()); + sb.append(" \n"); + } + sb.append('\n'); + + for (MessageDestination md : messageDestinations.values()) { + sb.append(" \n"); + appendElement(sb, INDENT4, "description", md.getDescription()); + appendElement(sb, INDENT4, "display-name", md.getDisplayName()); + appendElement(sb, INDENT4, "message-destination-name", + md.getName()); + appendElement(sb, INDENT4, "mapped-name", md.getProperty("mappedName")); + appendElement(sb, INDENT4, "lookup-name", md.getLookupName()); + sb.append(" \n"); + } + sb.append('\n'); + } + + // locale-encoding-mapping-list was introduced in Servlet 2.4 + if (getMajorVersion() > 2 || getMinorVersion() > 3) { + if (localeEncodingMappings.size() > 0) { + sb.append(" \n"); + for (Map.Entry entry : + localeEncodingMappings.entrySet()) { + sb.append(" \n"); + appendElement(sb, INDENT6, "locale", entry.getKey()); + appendElement(sb, INDENT6, "encoding", entry.getValue()); + sb.append(" \n"); + } + sb.append(" \n"); + sb.append("\n"); + } + } + + // deny-uncovered-http-methods was introduced in Servlet 3.1 + if (getMajorVersion() > 3 || + (getMajorVersion() == 3 && getMinorVersion() > 0)) { + if (denyUncoveredHttpMethods) { + sb.append(" "); + sb.append("\n"); + } + } + + // request-encoding and response-encoding was introduced in Servlet 4.0 + if (getMajorVersion() >= 4) { + appendElement(sb, INDENT2, "request-character-encoding", requestCharacterEncoding); + appendElement(sb, INDENT2, "response-character-encoding", responseCharacterEncoding); + } + sb.append(""); + return sb.toString(); + } + + + private String encodeUrl(String input) { + try { + return URLEncoder.encode(input, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Impossible. UTF-8 is a required character set + return null; + } + } + + + private static void appendElement(StringBuilder sb, String indent, + String elementName, String value) { + if (value == null) { + return; + } + if (value.length() == 0) { + sb.append(indent); + sb.append('<'); + sb.append(elementName); + sb.append("/>\n"); + } else { + sb.append(indent); + sb.append('<'); + sb.append(elementName); + sb.append('>'); + sb.append(Escape.xml(value)); + sb.append("\n"); + } + } + + private static void appendElement(StringBuilder sb, String indent, + String elementName, Object value) { + if (value == null) { + return; + } + appendElement(sb, indent, elementName, value.toString()); + } + + + /** + * Merge the supplied web fragments into this main web.xml. + * + * @param fragments The fragments to merge in + * @return true if merge is successful, else + * false + */ + public boolean merge(Set fragments) { + // As far as possible, process in alphabetical order so it is easy to + // check everything is present + + // Merge rules vary from element to element. See SRV.8.2.3 + + WebXml temp = new WebXml(); + + for (WebXml fragment : fragments) { + if (!mergeMap(fragment.getContextParams(), contextParams, + temp.getContextParams(), fragment, "Context Parameter")) { + return false; + } + } + contextParams.putAll(temp.getContextParams()); + + if (displayName == null) { + for (WebXml fragment : fragments) { + String value = fragment.getDisplayName(); + if (value != null) { + if (temp.getDisplayName() == null) { + temp.setDisplayName(value); + } else { + log.error(sm.getString( + "webXml.mergeConflictDisplayName", + fragment.getName(), + fragment.getURL())); + return false; + } + } + } + displayName = temp.getDisplayName(); + } + + // Note: Not permitted in fragments but we also use fragments for + // per-Host and global defaults so they may appear there + if (!denyUncoveredHttpMethods) { + for (WebXml fragment : fragments) { + if (fragment.getDenyUncoveredHttpMethods()) { + denyUncoveredHttpMethods = true; + break; + } + } + } + if (requestCharacterEncoding == null) { + for (WebXml fragment : fragments) { + if (fragment.getRequestCharacterEncoding() != null) { + requestCharacterEncoding = fragment.getRequestCharacterEncoding(); + } + } + } + if (responseCharacterEncoding == null) { + for (WebXml fragment : fragments) { + if (fragment.getResponseCharacterEncoding() != null) { + responseCharacterEncoding = fragment.getResponseCharacterEncoding(); + } + } + } + + if (distributable) { + for (WebXml fragment : fragments) { + if (!fragment.isDistributable()) { + distributable = false; + break; + } + } + } + + for (WebXml fragment : fragments) { + if (!mergeResourceMap(fragment.getEjbLocalRefs(), ejbLocalRefs, + temp.getEjbLocalRefs(), fragment)) { + return false; + } + } + ejbLocalRefs.putAll(temp.getEjbLocalRefs()); + + for (WebXml fragment : fragments) { + if (!mergeResourceMap(fragment.getEjbRefs(), ejbRefs, + temp.getEjbRefs(), fragment)) { + return false; + } + } + ejbRefs.putAll(temp.getEjbRefs()); + + for (WebXml fragment : fragments) { + if (!mergeResourceMap(fragment.getEnvEntries(), envEntries, + temp.getEnvEntries(), fragment)) { + return false; + } + } + envEntries.putAll(temp.getEnvEntries()); + + for (WebXml fragment : fragments) { + if (!mergeMap(fragment.getErrorPages(), errorPages, + temp.getErrorPages(), fragment, "Error Page")) { + return false; + } + } + errorPages.putAll(temp.getErrorPages()); + + // As per 'clarification' from the Servlet EG, filter definitions in the + // main web.xml override those in fragments and those in fragments + // override those in annotations + List filterMapsToAdd = new ArrayList<>(); + for (WebXml fragment : fragments) { + for (FilterMap filterMap : fragment.getFilterMappings()) { + if (!filterMappingNames.contains(filterMap.getFilterName())) { + filterMapsToAdd.add(filterMap); + } + } + } + for (FilterMap filterMap : filterMapsToAdd) { + // Additive + addFilterMapping(filterMap); + } + + for (WebXml fragment : fragments) { + for (Map.Entry entry : + fragment.getFilters().entrySet()) { + if (filters.containsKey(entry.getKey())) { + mergeFilter(entry.getValue(), + filters.get(entry.getKey()), false); + } else { + if (temp.getFilters().containsKey(entry.getKey())) { + if (!(mergeFilter(entry.getValue(), + temp.getFilters().get(entry.getKey()), true))) { + log.error(sm.getString( + "webXml.mergeConflictFilter", + entry.getKey(), + fragment.getName(), + fragment.getURL())); + + return false; + } + } else { + temp.getFilters().put(entry.getKey(), entry.getValue()); + } + } + } + } + filters.putAll(temp.getFilters()); + + for (WebXml fragment : fragments) { + for (JspPropertyGroup jspPropertyGroup : + fragment.getJspPropertyGroups()) { + // Always additive + addJspPropertyGroup(jspPropertyGroup); + } + } + + for (WebXml fragment : fragments) { + for (String listener : fragment.getListeners()) { + // Always additive + addListener(listener); + } + } + + for (WebXml fragment : fragments) { + if (!mergeMap(fragment.getLocaleEncodingMappings(), + localeEncodingMappings, temp.getLocaleEncodingMappings(), + fragment, "Locale Encoding Mapping")) { + return false; + } + } + localeEncodingMappings.putAll(temp.getLocaleEncodingMappings()); + + if (getLoginConfig() == null) { + LoginConfig tempLoginConfig = null; + for (WebXml fragment : fragments) { + LoginConfig fragmentLoginConfig = fragment.loginConfig; + if (fragmentLoginConfig != null) { + if (tempLoginConfig == null || + fragmentLoginConfig.equals(tempLoginConfig)) { + tempLoginConfig = fragmentLoginConfig; + } else { + log.error(sm.getString( + "webXml.mergeConflictLoginConfig", + fragment.getName(), + fragment.getURL())); + } + } + } + loginConfig = tempLoginConfig; + } + + for (WebXml fragment : fragments) { + if (!mergeResourceMap(fragment.getMessageDestinationRefs(), messageDestinationRefs, + temp.getMessageDestinationRefs(), fragment)) { + return false; + } + } + messageDestinationRefs.putAll(temp.getMessageDestinationRefs()); + + for (WebXml fragment : fragments) { + if (!mergeResourceMap(fragment.getMessageDestinations(), messageDestinations, + temp.getMessageDestinations(), fragment)) { + return false; + } + } + messageDestinations.putAll(temp.getMessageDestinations()); + + for (WebXml fragment : fragments) { + if (!mergeMap(fragment.getMimeMappings(), mimeMappings, + temp.getMimeMappings(), fragment, "Mime Mapping")) { + return false; + } + } + mimeMappings.putAll(temp.getMimeMappings()); + + for (WebXml fragment : fragments) { + if (!mergeResourceMap(fragment.getResourceEnvRefs(), resourceEnvRefs, + temp.getResourceEnvRefs(), fragment)) { + return false; + } + } + resourceEnvRefs.putAll(temp.getResourceEnvRefs()); + + for (WebXml fragment : fragments) { + if (!mergeResourceMap(fragment.getResourceRefs(), resourceRefs, + temp.getResourceRefs(), fragment)) { + return false; + } + } + resourceRefs.putAll(temp.getResourceRefs()); + + for (WebXml fragment : fragments) { + for (SecurityConstraint constraint : fragment.getSecurityConstraints()) { + // Always additive + addSecurityConstraint(constraint); + } + } + + for (WebXml fragment : fragments) { + for (String role : fragment.getSecurityRoles()) { + // Always additive + addSecurityRole(role); + } + } + + for (WebXml fragment : fragments) { + if (!mergeResourceMap(fragment.getServiceRefs(), serviceRefs, + temp.getServiceRefs(), fragment)) { + return false; + } + } + serviceRefs.putAll(temp.getServiceRefs()); + + // As per 'clarification' from the Servlet EG, servlet definitions and + // mappings in the main web.xml override those in fragments and those in + // fragments override those in annotations + // Skip servlet definitions and mappings from fragments that are + // defined in web.xml + List> servletMappingsToAdd = new ArrayList<>(); + for (WebXml fragment : fragments) { + for (Map.Entry servletMap : + fragment.getServletMappings().entrySet()) { + if (!servletMappingNames.contains(servletMap.getValue()) && + !servletMappings.containsKey(servletMap.getKey())) { + servletMappingsToAdd.add(servletMap); + } + } + } + + // Add fragment mappings + for (Map.Entry mapping : servletMappingsToAdd) { + addServletMappingDecoded(mapping.getKey(), mapping.getValue()); + } + + for (WebXml fragment : fragments) { + for (Map.Entry entry : + fragment.getServlets().entrySet()) { + if (servlets.containsKey(entry.getKey())) { + mergeServlet(entry.getValue(), + servlets.get(entry.getKey()), false); + } else { + if (temp.getServlets().containsKey(entry.getKey())) { + if (!(mergeServlet(entry.getValue(), + temp.getServlets().get(entry.getKey()), true))) { + log.error(sm.getString( + "webXml.mergeConflictServlet", + entry.getKey(), + fragment.getName(), + fragment.getURL())); + + return false; + } + } else { + temp.getServlets().put(entry.getKey(), entry.getValue()); + } + } + } + } + servlets.putAll(temp.getServlets()); + + if (sessionConfig.getSessionTimeout() == null) { + for (WebXml fragment : fragments) { + Integer value = fragment.getSessionConfig().getSessionTimeout(); + if (value != null) { + if (temp.getSessionConfig().getSessionTimeout() == null) { + temp.getSessionConfig().setSessionTimeout(value.toString()); + } else if (value.equals( + temp.getSessionConfig().getSessionTimeout())) { + // Fragments use same value - no conflict + } else { + log.error(sm.getString( + "webXml.mergeConflictSessionTimeout", + fragment.getName(), + fragment.getURL())); + return false; + } + } + } + if (temp.getSessionConfig().getSessionTimeout() != null) { + sessionConfig.setSessionTimeout( + temp.getSessionConfig().getSessionTimeout().toString()); + } + } + + if (sessionConfig.getCookieName() == null) { + for (WebXml fragment : fragments) { + String value = fragment.getSessionConfig().getCookieName(); + if (value != null) { + if (temp.getSessionConfig().getCookieName() == null) { + temp.getSessionConfig().setCookieName(value); + } else if (value.equals( + temp.getSessionConfig().getCookieName())) { + // Fragments use same value - no conflict + } else { + log.error(sm.getString( + "webXml.mergeConflictSessionCookieName", + fragment.getName(), + fragment.getURL())); + return false; + } + } + } + sessionConfig.setCookieName( + temp.getSessionConfig().getCookieName()); + } + + Map mainAttributes = getSessionConfig().getCookieAttributes(); + Map mergedFragmentAttributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (WebXml fragment : fragments) { + for (Map.Entry attribute : fragment.getSessionConfig().getCookieAttributes().entrySet()) { + // Skip any attribute in a fragment that is defined in the main web.xml + if (!mainAttributes.containsKey(attribute.getKey())) { + if (mergedFragmentAttributes.containsKey(attribute.getKey())) { + // Attribute has already been seen. + // If values are the same, NO-OP. If they are different + // trigger a merge error + if (!mergedFragmentAttributes.get(attribute.getKey()).equals(attribute.getValue())) { + log.error(sm.getString( + "webXml.mergeConflictSessionCookieAttributes", + fragment.getName(), + fragment.getURL())); + return false; + } + } else { + // First time this attribute has been seen. Add it. + mergedFragmentAttributes.put(attribute.getKey(), attribute.getValue()); + } + } + } + } + mainAttributes.putAll(mergedFragmentAttributes); + + if (sessionConfig.getSessionTrackingModes().size() == 0) { + for (WebXml fragment : fragments) { + EnumSet value = + fragment.getSessionConfig().getSessionTrackingModes(); + if (value.size() > 0) { + if (temp.getSessionConfig().getSessionTrackingModes().size() == 0) { + temp.getSessionConfig().getSessionTrackingModes().addAll(value); + } else if (value.equals( + temp.getSessionConfig().getSessionTrackingModes())) { + // Fragments use same value - no conflict + } else { + log.error(sm.getString( + "webXml.mergeConflictSessionTrackingMode", + fragment.getName(), + fragment.getURL())); + return false; + } + } + } + sessionConfig.getSessionTrackingModes().addAll( + temp.getSessionConfig().getSessionTrackingModes()); + } + + for (WebXml fragment : fragments) { + if (!mergeMap(fragment.getTaglibs(), taglibs, + temp.getTaglibs(), fragment, "Taglibs")) { + return false; + } + } + taglibs.putAll(temp.getTaglibs()); + + for (WebXml fragment : fragments) { + if (fragment.alwaysAddWelcomeFiles || welcomeFiles.size() == 0) { + for (String welcomeFile : fragment.getWelcomeFiles()) { + addWelcomeFile(welcomeFile); + } + } + } + + if (postConstructMethods.isEmpty()) { + for (WebXml fragment : fragments) { + if (!mergeLifecycleCallback(fragment.getPostConstructMethods(), + temp.getPostConstructMethods(), fragment, + "Post Construct Methods")) { + return false; + } + } + postConstructMethods.putAll(temp.getPostConstructMethods()); + } + + if (preDestroyMethods.isEmpty()) { + for (WebXml fragment : fragments) { + if (!mergeLifecycleCallback(fragment.getPreDestroyMethods(), + temp.getPreDestroyMethods(), fragment, + "Pre Destroy Methods")) { + return false; + } + } + preDestroyMethods.putAll(temp.getPreDestroyMethods()); + } + + return true; + } + + private boolean mergeResourceMap( + Map fragmentResources, Map mainResources, + Map tempResources, WebXml fragment) { + for (T resource : fragmentResources.values()) { + String resourceName = resource.getName(); + if (mainResources.containsKey(resourceName)) { + mainResources.get(resourceName).getInjectionTargets().addAll( + resource.getInjectionTargets()); + } else { + // Not defined in main web.xml + T existingResource = tempResources.get(resourceName); + if (existingResource != null) { + if (!existingResource.equals(resource)) { + log.error(sm.getString( + "webXml.mergeConflictResource", + resourceName, + fragment.getName(), + fragment.getURL())); + return false; + } + } else { + tempResources.put(resourceName, resource); + } + } + } + return true; + } + + private boolean mergeMap(Map fragmentMap, + Map mainMap, Map tempMap, WebXml fragment, + String mapName) { + for (Entry entry : fragmentMap.entrySet()) { + final String key = entry.getKey(); + if (!mainMap.containsKey(key)) { + // Not defined in main web.xml + T value = entry.getValue(); + if (tempMap.containsKey(key)) { + if (value != null && !value.equals( + tempMap.get(key))) { + log.error(sm.getString( + "webXml.mergeConflictString", + mapName, + key, + fragment.getName(), + fragment.getURL())); + return false; + } + } else { + tempMap.put(key, value); + } + } + } + return true; + } + + private static boolean mergeFilter(FilterDef src, FilterDef dest, + boolean failOnConflict) { + if (dest.getAsyncSupported() == null) { + dest.setAsyncSupported(src.getAsyncSupported()); + } else if (src.getAsyncSupported() != null) { + if (failOnConflict && + !src.getAsyncSupported().equals(dest.getAsyncSupported())) { + return false; + } + } + + if (dest.getFilterClass() == null) { + dest.setFilterClass(src.getFilterClass()); + } else if (src.getFilterClass() != null) { + if (failOnConflict && + !src.getFilterClass().equals(dest.getFilterClass())) { + return false; + } + } + + for (Map.Entry srcEntry : + src.getParameterMap().entrySet()) { + if (dest.getParameterMap().containsKey(srcEntry.getKey())) { + if (failOnConflict && !dest.getParameterMap().get( + srcEntry.getKey()).equals(srcEntry.getValue())) { + return false; + } + } else { + dest.addInitParameter(srcEntry.getKey(), srcEntry.getValue()); + } + } + return true; + } + + private static boolean mergeServlet(ServletDef src, ServletDef dest, + boolean failOnConflict) { + // These tests should be unnecessary... + if (dest.getServletClass() != null && dest.getJspFile() != null) { + return false; + } + if (src.getServletClass() != null && src.getJspFile() != null) { + return false; + } + + + if (dest.getServletClass() == null && dest.getJspFile() == null) { + dest.setServletClass(src.getServletClass()); + dest.setJspFile(src.getJspFile()); + } else if (failOnConflict) { + if (src.getServletClass() != null && + (dest.getJspFile() != null || + !src.getServletClass().equals(dest.getServletClass()))) { + return false; + } + if (src.getJspFile() != null && + (dest.getServletClass() != null || + !src.getJspFile().equals(dest.getJspFile()))) { + return false; + } + } + + // Additive + for (SecurityRoleRef securityRoleRef : src.getSecurityRoleRefs()) { + dest.addSecurityRoleRef(securityRoleRef); + } + + if (dest.getLoadOnStartup() == null) { + if (src.getLoadOnStartup() != null) { + dest.setLoadOnStartup(src.getLoadOnStartup().toString()); + } + } else if (src.getLoadOnStartup() != null) { + if (failOnConflict && + !src.getLoadOnStartup().equals(dest.getLoadOnStartup())) { + return false; + } + } + + if (dest.getEnabled() == null) { + if (src.getEnabled() != null) { + dest.setEnabled(src.getEnabled().toString()); + } + } else if (src.getEnabled() != null) { + if (failOnConflict && + !src.getEnabled().equals(dest.getEnabled())) { + return false; + } + } + + for (Map.Entry srcEntry : + src.getParameterMap().entrySet()) { + if (dest.getParameterMap().containsKey(srcEntry.getKey())) { + if (failOnConflict && !dest.getParameterMap().get( + srcEntry.getKey()).equals(srcEntry.getValue())) { + return false; + } + } else { + dest.addInitParameter(srcEntry.getKey(), srcEntry.getValue()); + } + } + + if (dest.getMultipartDef() == null) { + dest.setMultipartDef(src.getMultipartDef()); + } else if (src.getMultipartDef() != null) { + return mergeMultipartDef(src.getMultipartDef(), + dest.getMultipartDef(), failOnConflict); + } + + if (dest.getAsyncSupported() == null) { + if (src.getAsyncSupported() != null) { + dest.setAsyncSupported(src.getAsyncSupported().toString()); + } + } else if (src.getAsyncSupported() != null) { + if (failOnConflict && + !src.getAsyncSupported().equals(dest.getAsyncSupported())) { + return false; + } + } + + return true; + } + + private static boolean mergeMultipartDef(MultipartDef src, MultipartDef dest, + boolean failOnConflict) { + + if (dest.getLocation() == null) { + dest.setLocation(src.getLocation()); + } else if (src.getLocation() != null) { + if (failOnConflict && + !src.getLocation().equals(dest.getLocation())) { + return false; + } + } + + if (dest.getFileSizeThreshold() == null) { + dest.setFileSizeThreshold(src.getFileSizeThreshold()); + } else if (src.getFileSizeThreshold() != null) { + if (failOnConflict && + !src.getFileSizeThreshold().equals( + dest.getFileSizeThreshold())) { + return false; + } + } + + if (dest.getMaxFileSize() == null) { + dest.setMaxFileSize(src.getMaxFileSize()); + } else if (src.getMaxFileSize() != null) { + if (failOnConflict && + !src.getMaxFileSize().equals(dest.getMaxFileSize())) { + return false; + } + } + + if (dest.getMaxRequestSize() == null) { + dest.setMaxRequestSize(src.getMaxRequestSize()); + } else if (src.getMaxRequestSize() != null) { + if (failOnConflict && + !src.getMaxRequestSize().equals( + dest.getMaxRequestSize())) { + return false; + } + } + + return true; + } + + + private boolean mergeLifecycleCallback( + Map fragmentMap, Map tempMap, + WebXml fragment, String mapName) { + for (Entry entry : fragmentMap.entrySet()) { + final String key = entry.getKey(); + final String value = entry.getValue(); + if (tempMap.containsKey(key)) { + if (value != null && !value.equals(tempMap.get(key))) { + log.error(sm.getString("webXml.mergeConflictString", + mapName, key, fragment.getName(), fragment.getURL())); + return false; + } + } else { + tempMap.put(key, value); + } + } + return true; + } + + + /** + * Generates the sub-set of the web-fragment.xml files to be processed in + * the order that the fragments must be processed as per the rules in the + * Servlet spec. + * + * @param application The application web.xml file + * @param fragments The map of fragment names to web fragments + * @param servletContext The servlet context the fragments are associated + * with + * @return Ordered list of web-fragment.xml files to process + */ + public static Set orderWebFragments(WebXml application, + Map fragments, ServletContext servletContext) { + return application.orderWebFragments(fragments, servletContext); + } + + + private Set orderWebFragments(Map fragments, + ServletContext servletContext) { + + Set orderedFragments = new LinkedHashSet<>(); + + boolean absoluteOrdering = getAbsoluteOrdering() != null; + boolean orderingPresent = false; + + if (absoluteOrdering) { + orderingPresent = true; + // Only those fragments listed should be processed + Set requestedOrder = getAbsoluteOrdering(); + + for (String requestedName : requestedOrder) { + if (ORDER_OTHERS.equals(requestedName)) { + // Add all fragments not named explicitly at this point + for (Entry entry : fragments.entrySet()) { + if (!requestedOrder.contains(entry.getKey())) { + WebXml fragment = entry.getValue(); + if (fragment != null) { + orderedFragments.add(fragment); + } + } + } + } else { + WebXml fragment = fragments.get(requestedName); + if (fragment != null) { + orderedFragments.add(fragment); + } else { + log.warn(sm.getString("webXml.wrongFragmentName",requestedName)); + } + } + } + } else { + // Stage 0. Check there were no fragments with duplicate names + for (WebXml fragment : fragments.values()) { + if (fragment.isDuplicated()) { + List duplicates = fragment.getDuplicates(); + duplicates.add(0, fragment.getURL().toString()); + throw new IllegalArgumentException( + sm.getString("webXml.duplicateFragment", fragment.getName(), duplicates)); + } + } + // Stage 1. Make all dependencies bi-directional - this makes the + // next stage simpler. + for (WebXml fragment : fragments.values()) { + Iterator before = + fragment.getBeforeOrdering().iterator(); + while (before.hasNext()) { + orderingPresent = true; + String beforeEntry = before.next(); + if (!beforeEntry.equals(ORDER_OTHERS)) { + WebXml beforeFragment = fragments.get(beforeEntry); + if (beforeFragment == null) { + before.remove(); + } else { + beforeFragment.addAfterOrdering(fragment.getName()); + } + } + } + Iterator after = fragment.getAfterOrdering().iterator(); + while (after.hasNext()) { + orderingPresent = true; + String afterEntry = after.next(); + if (!afterEntry.equals(ORDER_OTHERS)) { + WebXml afterFragment = fragments.get(afterEntry); + if (afterFragment == null) { + after.remove(); + } else { + afterFragment.addBeforeOrdering(fragment.getName()); + } + } + } + } + + // Stage 2. Make all fragments that are implicitly before/after + // others explicitly so. This is iterative so the next + // stage doesn't have to be. + for (WebXml fragment : fragments.values()) { + if (fragment.getBeforeOrdering().contains(ORDER_OTHERS)) { + makeBeforeOthersExplicit(fragment.getAfterOrdering(), fragments); + } + if (fragment.getAfterOrdering().contains(ORDER_OTHERS)) { + makeAfterOthersExplicit(fragment.getBeforeOrdering(), fragments); + } + } + + // Stage 3. Separate into three groups + Set beforeSet = new HashSet<>(); + Set othersSet = new HashSet<>(); + Set afterSet = new HashSet<>(); + + for (WebXml fragment : fragments.values()) { + if (fragment.getBeforeOrdering().contains(ORDER_OTHERS)) { + beforeSet.add(fragment); + fragment.getBeforeOrdering().remove(ORDER_OTHERS); + } else if (fragment.getAfterOrdering().contains(ORDER_OTHERS)) { + afterSet.add(fragment); + fragment.getAfterOrdering().remove(ORDER_OTHERS); + } else { + othersSet.add(fragment); + } + } + + // Stage 4. Decouple the groups so the ordering requirements for + // each fragment in the group only refer to other fragments + // in the group. Ordering requirements outside the group + // will be handled by processing the groups in order. + // Note: Only after ordering requirements are considered. + // This is OK because of the processing in stage 1. + decoupleOtherGroups(beforeSet); + decoupleOtherGroups(othersSet); + decoupleOtherGroups(afterSet); + + // Stage 5. Order each group + // Note: Only after ordering requirements are considered. + // This is OK because of the processing in stage 1. + orderFragments(orderedFragments, beforeSet); + orderFragments(orderedFragments, othersSet); + orderFragments(orderedFragments, afterSet); + } + + // Container fragments are always included + Set containerFragments = new LinkedHashSet<>(); + // Find all the container fragments and remove any present from the + // ordered list + for (WebXml fragment : fragments.values()) { + if (!fragment.getWebappJar()) { + containerFragments.add(fragment); + orderedFragments.remove(fragment); + } + } + + // Avoid NPE when unit testing + if (servletContext != null) { + // Publish the ordered fragments. The app does not need to know + // about container fragments + List orderedJarFileNames = null; + if (orderingPresent) { + orderedJarFileNames = new ArrayList<>(); + for (WebXml fragment: orderedFragments) { + orderedJarFileNames.add(fragment.getJarName()); + } + } + servletContext.setAttribute(ServletContext.ORDERED_LIBS, + orderedJarFileNames); + } + + // The remainder of the processing needs to know about container + // fragments + if (containerFragments.size() > 0) { + Set result = new LinkedHashSet<>(); + if (containerFragments.iterator().next().getDelegate()) { + result.addAll(containerFragments); + result.addAll(orderedFragments); + } else { + result.addAll(orderedFragments); + result.addAll(containerFragments); + } + return result; + } else { + return orderedFragments; + } + } + + private static void decoupleOtherGroups(Set group) { + Set names = new HashSet<>(); + for (WebXml fragment : group) { + names.add(fragment.getName()); + } + for (WebXml fragment : group) { + fragment.getAfterOrdering().removeIf(entry -> !names.contains(entry)); + } + } + private static void orderFragments(Set orderedFragments, + Set unordered) { + Set addedThisRound = new HashSet<>(); + Set addedLastRound = new HashSet<>(); + while (unordered.size() > 0) { + Iterator source = unordered.iterator(); + while (source.hasNext()) { + WebXml fragment = source.next(); + for (WebXml toRemove : addedLastRound) { + fragment.getAfterOrdering().remove(toRemove.getName()); + } + if (fragment.getAfterOrdering().isEmpty()) { + addedThisRound.add(fragment); + orderedFragments.add(fragment); + source.remove(); + } + } + if (addedThisRound.size() == 0) { + // Circular + throw new IllegalArgumentException( + sm.getString("webXml.mergeConflictOrder")); + } + addedLastRound.clear(); + addedLastRound.addAll(addedThisRound); + addedThisRound.clear(); + } + } + + private static void makeBeforeOthersExplicit(Set beforeOrdering, + Map fragments) { + for (String before : beforeOrdering) { + if (!before.equals(ORDER_OTHERS)) { + WebXml webXml = fragments.get(before); + if (!webXml.getBeforeOrdering().contains(ORDER_OTHERS)) { + webXml.addBeforeOrderingOthers(); + makeBeforeOthersExplicit(webXml.getAfterOrdering(), fragments); + } + } + } + } + + private static void makeAfterOthersExplicit(Set afterOrdering, + Map fragments) { + for (String after : afterOrdering) { + if (!after.equals(ORDER_OTHERS)) { + WebXml webXml = fragments.get(after); + if (!webXml.getAfterOrdering().contains(ORDER_OTHERS)) { + webXml.addAfterOrderingOthers(); + makeAfterOthersExplicit(webXml.getBeforeOrdering(), fragments); + } + } + } + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/WebXmlParser.java b/java/org/apache/tomcat/util/descriptor/web/WebXmlParser.java new file mode 100644 index 0000000..e728c24 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/WebXmlParser.java @@ -0,0 +1,155 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.IOException; +import java.net.URL; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.descriptor.DigesterFactory; +import org.apache.tomcat.util.descriptor.InputSourceUtil; +import org.apache.tomcat.util.descriptor.XmlErrorHandler; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.InputSource; +import org.xml.sax.SAXParseException; + +public class WebXmlParser { + + private final Log log = LogFactory.getLog(WebXmlParser.class); // must not be static + + /** + * The string resources for this package. + */ + private static final StringManager sm = + StringManager.getManager(Constants.PACKAGE_NAME); + + /** + * The Digester we will use to process web application + * deployment descriptor files. + */ + private final Digester webDigester; + private final WebRuleSet webRuleSet; + + /** + * The Digester we will use to process web fragment + * deployment descriptor files. + */ + private final Digester webFragmentDigester; + private final WebRuleSet webFragmentRuleSet; + + + public WebXmlParser(boolean namespaceAware, boolean validation, + boolean blockExternal) { + webRuleSet = new WebRuleSet(false); + webDigester = DigesterFactory.newDigester(validation, + namespaceAware, webRuleSet, blockExternal); + webDigester.getParser(); + + webFragmentRuleSet = new WebRuleSet(true); + webFragmentDigester = DigesterFactory.newDigester(validation, + namespaceAware, webFragmentRuleSet, blockExternal); + webFragmentDigester.getParser(); + } + + /** + * Parse a web descriptor at a location. + * + * @param url the location; if null no load will be attempted + * @param dest the instance to be populated by the parse operation + * @param fragment indicate if the descriptor is a web-app or web-fragment + * @return true if the descriptor was successfully parsed + * @throws IOException if there was a problem reading from the URL + */ + public boolean parseWebXml(URL url, WebXml dest, boolean fragment) throws IOException { + if (url == null) { + return true; + } + InputSource source = new InputSource(url.toExternalForm()); + source.setByteStream(url.openStream()); + return parseWebXml(source, dest, fragment); + } + + + public boolean parseWebXml(InputSource source, WebXml dest, + boolean fragment) { + + boolean ok = true; + + if (source == null) { + return ok; + } + + XmlErrorHandler handler = new XmlErrorHandler(); + + Digester digester; + WebRuleSet ruleSet; + if (fragment) { + digester = webFragmentDigester; + ruleSet = webFragmentRuleSet; + } else { + digester = webDigester; + ruleSet = webRuleSet; + } + + digester.push(dest); + digester.setErrorHandler(handler); + + if(log.isDebugEnabled()) { + log.debug(sm.getString("webXmlParser.applicationStart", + source.getSystemId())); + } + + try { + digester.parse(source); + + if (handler.getWarnings().size() > 0 || + handler.getErrors().size() > 0) { + ok = false; + handler.logFindings(log, source.getSystemId()); + } + } catch (SAXParseException e) { + log.error(sm.getString("webXmlParser.applicationParse", + source.getSystemId()), e); + log.error(sm.getString("webXmlParser.applicationPosition", + "" + e.getLineNumber(), + "" + e.getColumnNumber())); + ok = false; + } catch (Exception e) { + log.error(sm.getString("webXmlParser.applicationParse", + source.getSystemId()), e); + ok = false; + } finally { + InputSourceUtil.close(source); + digester.reset(); + ruleSet.recycle(); + } + + return ok; + } + + + /** + * Sets the ClassLoader to be used for creating descriptor objects. + * @param classLoader the ClassLoader to be used for creating descriptor objects + */ + public void setClassLoader(ClassLoader classLoader) { + webDigester.setClassLoader(classLoader); + webFragmentDigester.setClassLoader(classLoader); + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/descriptor/web/XmlEncodingBase.java b/java/org/apache/tomcat/util/descriptor/web/XmlEncodingBase.java new file mode 100644 index 0000000..223197a --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/XmlEncodingBase.java @@ -0,0 +1,46 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Base class for those elements that need to track the encoding used in the + * source XML. + */ +public abstract class XmlEncodingBase { + + private Charset charset = StandardCharsets.UTF_8; + + + public void setCharset(Charset charset) { + this.charset = charset; + } + + + /** + * Obtain the character encoding of the XML source that was used to + * populated this object. + * + * @return The character encoding of the associated XML source or + * UTF-8 if the encoding could not be determined + */ + public Charset getCharset() { + return charset; + } +} diff --git a/java/org/apache/tomcat/util/descriptor/web/mbeans-descriptors.xml b/java/org/apache/tomcat/util/descriptor/web/mbeans-descriptors.xml new file mode 100644 index 0000000..90dab0d --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/mbeans-descriptors.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/tomcat/util/descriptor/web/package.html b/java/org/apache/tomcat/util/descriptor/web/package.html new file mode 100644 index 0000000..da90e0c --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/package.html @@ -0,0 +1,26 @@ + + + +

    This package contains Java objects that represent complex data structures +from the web application deployment descriptor file (web.xml). +It is assumed that these objects will be initialized within the context of +a single thread, and then referenced in a read-only manner subsequent to that +time. Therefore, no multi-thread synchronization is utilized within the +implementation classes.

    + + diff --git a/java/org/apache/tomcat/util/digester/AbstractObjectCreationFactory.java b/java/org/apache/tomcat/util/digester/AbstractObjectCreationFactory.java new file mode 100644 index 0000000..01faa53 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/AbstractObjectCreationFactory.java @@ -0,0 +1,78 @@ +/* + * 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.tomcat.util.digester; + + +import org.xml.sax.Attributes; + + +/** + *

    Abstract base class for ObjectCreationFactory + * implementations.

    + */ +public abstract class AbstractObjectCreationFactory + implements ObjectCreationFactory { + + + // ----------------------------------------------------- Instance Variables + + + /** + * The associated Digester instance that was set up by + * {@link FactoryCreateRule} upon initialization. + */ + private Digester digester = null; + + + // --------------------------------------------------------- Public Methods + + + /** + *

    Factory method called by {@link FactoryCreateRule} to supply an + * object based on the element's attributes. + * + * @param attributes the element's attributes + * + * @throws Exception any exception thrown will be propagated upwards + */ + @Override + public abstract Object createObject(Attributes attributes) throws Exception; + + + /** + *

    Returns the {@link Digester} that was set by the + * {@link FactoryCreateRule} upon initialization. + */ + @Override + public Digester getDigester() { + return this.digester; + } + + + /** + *

    Set the {@link Digester} to allow the implementation to do logging, + * classloading based on the digester's classloader, etc. + * + * @param digester parent Digester object + */ + @Override + public void setDigester(Digester digester) { + this.digester = digester; + } + + +} diff --git a/java/org/apache/tomcat/util/digester/ArrayStack.java b/java/org/apache/tomcat/util/digester/ArrayStack.java new file mode 100644 index 0000000..97073f0 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/ArrayStack.java @@ -0,0 +1,141 @@ +/* + * 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.tomcat.util.digester; + +import java.util.ArrayList; +import java.util.EmptyStackException; + +/** + *

    Imported copy of the ArrayStack class from + * Commons Collections, which was the only direct dependency from Digester.

    + * + *

    WARNING - This class is public solely to allow it to be + * used from subpackages of org.apache.commons.digester. + * It should not be considered part of the public API of Commons Digester. + * If you want to use such a class yourself, you should use the one from + * Commons Collections directly.

    + * + *

    An implementation of the {@link java.util.Stack} API that is based on an + * ArrayList instead of a Vector, so it is not + * synchronized to protect against multi-threaded access. The implementation + * is therefore operates faster in environments where you do not need to + * worry about multiple thread contention.

    + * + *

    Unlike Stack, ArrayStack accepts null entries. + *

    + * + * @param Type of object in this stack + * + * @see java.util.Stack + * @since Digester 1.6 (from Commons Collections 1.0) + */ +public class ArrayStack extends ArrayList { + + /** Ensure serialization compatibility */ + private static final long serialVersionUID = 2130079159931574599L; + + /** + * Constructs a new empty ArrayStack. The initial size + * is controlled by ArrayList and is currently 10. + */ + public ArrayStack() { + super(); + } + + /** + * Constructs a new empty ArrayStack with an initial size. + * + * @param initialSize the initial size to use + * @throws IllegalArgumentException if the specified initial size + * is negative + */ + public ArrayStack(int initialSize) { + super(initialSize); + } + + /** + * Return true if this stack is currently empty. + *

    + * This method exists for compatibility with java.util.Stack. + * New users of this class should use isEmpty instead. + * + * @return true if the stack is currently empty + */ + public boolean empty() { + return isEmpty(); + } + + /** + * Returns the top item off of this stack without removing it. + * + * @return the top item on the stack + * @throws EmptyStackException if the stack is empty + */ + public E peek() throws EmptyStackException { + int n = size(); + if (n <= 0) { + throw new EmptyStackException(); + } else { + return get(n - 1); + } + } + + /** + * Returns the n'th item down (zero-relative) from the top of this + * stack without removing it. + * + * @param n the number of items down to go + * @return the n'th item on the stack, zero relative + * @throws EmptyStackException if there are not enough items on the + * stack to satisfy this request + */ + public E peek(int n) throws EmptyStackException { + int m = (size() - n) - 1; + if (m < 0) { + throw new EmptyStackException(); + } else { + return get(m); + } + } + + /** + * Pops the top item off of this stack and return it. + * + * @return the top item on the stack + * @throws EmptyStackException if the stack is empty + */ + public E pop() throws EmptyStackException { + int n = size(); + if (n <= 0) { + throw new EmptyStackException(); + } else { + return remove(n - 1); + } + } + + /** + * Pushes a new item onto the top of this stack. The pushed item is also + * returned. This is equivalent to calling add. + * + * @param item the item to be added + * @return the item just pushed + */ + public E push(E item) { + add(item); + return item; + } +} diff --git a/java/org/apache/tomcat/util/digester/CallMethodRule.java b/java/org/apache/tomcat/util/digester/CallMethodRule.java new file mode 100644 index 0000000..b3fd085 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/CallMethodRule.java @@ -0,0 +1,447 @@ +/* + * 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.tomcat.util.digester; + +import java.util.Arrays; + +import org.apache.tomcat.util.IntrospectionUtils; +import org.xml.sax.Attributes; + +/** + *

    Rule implementation that calls a method on an object on the stack + * (normally the top/parent object), passing arguments collected from + * subsequent CallParamRule rules or from the body of this + * element.

    + * + *

    By using {@link #CallMethodRule(String methodName)} + * a method call can be made to a method which accepts no + * arguments.

    + * + *

    Incompatible method parameter types are converted + * using org.apache.commons.beanutils.ConvertUtils. + *

    + * + *

    This rule now uses + * + * org.apache.commons.beanutils.MethodUtils#invokeMethod + * by default. + * This increases the kinds of methods successfully and allows primitives + * to be matched by passing in wrapper classes. + * There are rare cases when org.apache.commons.beanutils.MethodUtils#invokeExactMethod + * (the old default) is required. + * This method is much stricter in its reflection. + * Setting the UseExactMatch to true reverts to the use of this + * method.

    + * + *

    Note that the target method is invoked when the end of + * the tag the CallMethodRule fired on is encountered, not when the + * last parameter becomes available. This implies that rules which fire on + * tags nested within the one associated with the CallMethodRule will + * fire before the CallMethodRule invokes the target method. This behaviour is + * not configurable.

    + * + *

    Note also that if a CallMethodRule is expecting exactly one parameter + * and that parameter is not available (eg CallParamRule is used with an + * attribute name but the attribute does not exist) then the method will + * not be invoked. If a CallMethodRule is expecting more than one parameter, + * then it is always invoked, regardless of whether the parameters were + * available or not (missing parameters are passed as null values).

    + */ +public class CallMethodRule extends Rule { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a "call method" rule with the specified method name. The + * parameter types (if any) default to java.lang.String. + * + * @param methodName Method name of the parent method to call + * @param paramCount The number of parameters to collect, or + * zero for a single argument from the body of this element. + */ + public CallMethodRule(String methodName, int paramCount) { + this(0, methodName, paramCount); + } + + + /** + * Construct a "call method" rule with the specified method name. The + * parameter types (if any) default to java.lang.String. + * + * @param targetOffset location of the target object. Positive numbers are + * relative to the top of the digester object stack. Negative numbers + * are relative to the bottom of the stack. Zero implies the top + * object on the stack. + * @param methodName Method name of the parent method to call + * @param paramCount The number of parameters to collect, or + * zero for a single argument from the body of this element. + */ + public CallMethodRule(int targetOffset, String methodName, int paramCount) { + this.targetOffset = targetOffset; + this.methodName = methodName; + this.paramCount = paramCount; + if (paramCount == 0) { + this.paramTypes = new Class[] { String.class }; + } else { + this.paramTypes = new Class[paramCount]; + Arrays.fill(this.paramTypes, String.class); + } + } + + + /** + * Construct a "call method" rule with the specified method name. + * The method should accept no parameters. + * + * @param methodName Method name of the parent method to call + */ + public CallMethodRule(String methodName) { + this(0, methodName, 0, null); + } + + + /** + * Construct a "call method" rule with the specified method name and + * parameter types. If paramCount is set to zero the rule + * will use the body of this element as the single argument of the + * method, unless paramTypes is null or empty, in this + * case the rule will call the specified method with no arguments. + * + * @param targetOffset location of the target object. Positive numbers are + * relative to the top of the digester object stack. Negative numbers + * are relative to the bottom of the stack. Zero implies the top + * object on the stack. + * @param methodName Method name of the parent method to call + * @param paramCount The number of parameters to collect, or + * zero for a single argument from the body of this element + * @param paramTypes The Java classes that represent the + * parameter types of the method arguments + * (if you wish to use a primitive type, specify the corresponding + * Java wrapper class instead, such as java.lang.Boolean.TYPE + * for a boolean parameter) + */ + public CallMethodRule(int targetOffset, String methodName, int paramCount, + Class[] paramTypes) { + + this.targetOffset = targetOffset; + this.methodName = methodName; + this.paramCount = paramCount; + if (paramTypes == null) { + this.paramTypes = new Class[paramCount]; + Arrays.fill(this.paramTypes, String.class); + } else { + this.paramTypes = new Class[paramTypes.length]; + System.arraycopy(paramTypes, 0, this.paramTypes, 0, this.paramTypes.length); + } + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The body text collected from this element. + */ + protected String bodyText = null; + + + /** + * location of the target object for the call, relative to the + * top of the digester object stack. The default value of zero + * means the target object is the one on top of the stack. + */ + protected final int targetOffset; + + + /** + * The method name to call on the parent object. + */ + protected final String methodName; + + + /** + * The number of parameters to collect from MethodParam rules. + * If this value is zero, a single parameter will be collected from the + * body of this element. + */ + protected final int paramCount; + + + /** + * The parameter types of the parameters to be collected. + */ + protected Class paramTypes[] = null; + + + /** + * Should MethodUtils.invokeExactMethod be used for reflection. + */ + protected boolean useExactMatch = false; + + + // --------------------------------------------------------- Public Methods + + /** + * Should MethodUtils.invokeExactMethod + * be used for the reflection. + * @return true if invokeExactMethod is used + */ + public boolean getUseExactMatch() { + return useExactMatch; + } + + + /** + * Set whether MethodUtils.invokeExactMethod + * should be used for the reflection. + * @param useExactMatch The flag value + */ + public void setUseExactMatch(boolean useExactMatch) { + this.useExactMatch = useExactMatch; + } + + + /** + * Process the start of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param attributes The attribute list for this element + */ + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + + // Push an array to capture the parameter values if necessary + if (paramCount > 0) { + Object[] parameters = new Object[paramCount]; + digester.pushParams(parameters); + } + + } + + + /** + * Process the body text of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param bodyText The body text of this element + */ + @Override + public void body(String namespace, String name, String bodyText) + throws Exception { + + if (paramCount == 0) { + this.bodyText = bodyText.trim().intern(); + } + + } + + + /** + * Process the end of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + */ + @SuppressWarnings("null") // parameters can't trigger NPE + @Override + public void end(String namespace, String name) throws Exception { + + // Retrieve or construct the parameter values array + Object parameters[] = null; + if (paramCount > 0) { + + parameters = (Object[]) digester.popParams(); + + if (digester.log.isTraceEnabled()) { + for (int i=0,size=parameters.length;i= 0) { + target = digester.peek(targetOffset); + } else { + target = digester.peek( digester.getCount() + targetOffset ); + } + + if (target == null) { + StringBuilder sb = new StringBuilder(); + sb.append("[CallMethodRule]{"); + sb.append(digester.match); + sb.append("} Call target is null ("); + sb.append("targetOffset="); + sb.append(targetOffset); + sb.append(",stackdepth="); + sb.append(digester.getCount()); + sb.append(')'); + throw new org.xml.sax.SAXException(sb.toString()); + } + + // Invoke the required method on the top object + if (digester.log.isTraceEnabled()) { + StringBuilder sb = new StringBuilder("[CallMethodRule]{"); + sb.append(digester.match); + sb.append("} Call "); + sb.append(target.getClass().getName()); + sb.append('.'); + sb.append(methodName); + sb.append('('); + for (int i = 0; i < paramValues.length; i++) { + if (i > 0) { + sb.append(','); + } + if (paramValues[i] == null) { + sb.append("null"); + } else { + sb.append(paramValues[i].toString()); + } + sb.append('/'); + if (paramTypes[i] == null) { + sb.append("null"); + } else { + sb.append(paramTypes[i].getName()); + } + } + sb.append(')'); + digester.log.trace(sb.toString()); + } + Object result = IntrospectionUtils.callMethodN(target, methodName, + paramValues, paramTypes); + processMethodCallResult(result); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(target)).append('.').append(methodName); + code.append('('); + for (int i = 0; i < paramValues.length; i++) { + if (i > 0) { + code.append(", "); + } + if (bodyText != null) { + code.append("\"").append(IntrospectionUtils.escape(bodyText)).append("\""); + } else if (paramValues[i] instanceof String) { + code.append("\"").append(IntrospectionUtils.escape(paramValues[i].toString())).append("\""); + } else { + code.append(digester.toVariableName(paramValues[i])); + } + } + code.append(");"); + code.append(System.lineSeparator()); + } + } + + + /** + * Clean up after parsing is complete. + */ + @Override + public void finish() throws Exception { + bodyText = null; + } + + + /** + * Subclasses may override this method to perform additional processing of the + * invoked method's result. + * + * @param result the Object returned by the method invoked, possibly null + */ + protected void processMethodCallResult(Object result) { + // do nothing + } + + + /** + * Render a printable version of this Rule. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("CallMethodRule["); + sb.append("methodName="); + sb.append(methodName); + sb.append(", paramCount="); + sb.append(paramCount); + sb.append(", paramTypes={"); + if (paramTypes != null) { + for (int i = 0; i < paramTypes.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(paramTypes[i].getName()); + } + } + sb.append('}'); + sb.append(']'); + return sb.toString(); + } +} diff --git a/java/org/apache/tomcat/util/digester/CallParamRule.java b/java/org/apache/tomcat/util/digester/CallParamRule.java new file mode 100644 index 0000000..dd85acf --- /dev/null +++ b/java/org/apache/tomcat/util/digester/CallParamRule.java @@ -0,0 +1,206 @@ +/* + * 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.tomcat.util.digester; + +import org.xml.sax.Attributes; + +/** + *

    Rule implementation that saves a parameter for use by a surrounding + * CallMethodRule.

    + * + *

    This parameter may be:

    + *
      + *
    • from an attribute of the current element + * See {@link #CallParamRule(int paramIndex, String attributeName)} + *
    • from current the element body + * See {@link #CallParamRule(int paramIndex)} + *
    + */ +public class CallParamRule extends Rule { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a "call parameter" rule that will save the body text of this + * element as the parameter value. + * + * @param paramIndex The zero-relative parameter number + */ + public CallParamRule(int paramIndex) { + this(paramIndex, null); + } + + + /** + * Construct a "call parameter" rule that will save the value of the + * specified attribute as the parameter value. + * + * @param paramIndex The zero-relative parameter number + * @param attributeName The name of the attribute to save + */ + public CallParamRule(int paramIndex, + String attributeName) { + this(attributeName, paramIndex, 0, false); + } + + + private CallParamRule(String attributeName, int paramIndex, int stackIndex, + boolean fromStack) { + this.attributeName = attributeName; + this.paramIndex = paramIndex; + this.stackIndex = stackIndex; + this.fromStack = fromStack; + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The attribute from which to save the parameter value + */ + protected final String attributeName; + + + /** + * The zero-relative index of the parameter we are saving. + */ + protected final int paramIndex; + + + /** + * Is the parameter to be set from the stack? + */ + protected final boolean fromStack; + + /** + * The position of the object from the top of the stack + */ + protected final int stackIndex; + + /** + * Stack is used to allow nested body text to be processed. + * Lazy creation. + */ + protected ArrayStack bodyTextStack; + + // --------------------------------------------------------- Public Methods + + + /** + * Process the start of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param attributes The attribute list for this element + */ + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + + Object param = null; + + if (attributeName != null) { + + param = attributes.getValue(attributeName); + + } else if(fromStack) { + + param = digester.peek(stackIndex); + + if (digester.log.isTraceEnabled()) { + + StringBuilder sb = new StringBuilder("[CallParamRule]{"); + sb.append(digester.match); + sb.append("} Save from stack; from stack?").append(fromStack); + sb.append("; object=").append(param); + digester.log.trace(sb.toString()); + } + } + + // Have to save the param object to the param stack frame here. + // Can't wait until end(). Otherwise, the object will be lost. + // We can't save the object as instance variables, as + // the instance variables will be overwritten + // if this CallParamRule is reused in subsequent nesting. + + if(param != null) { + Object parameters[] = (Object[]) digester.peekParams(); + parameters[paramIndex] = param; + } + } + + + /** + * Process the body text of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param bodyText The body text of this element + */ + @Override + public void body(String namespace, String name, String bodyText) + throws Exception { + + if (attributeName == null && !fromStack) { + // We must wait to set the parameter until end + // so that we can make sure that the right set of parameters + // is at the top of the stack + if (bodyTextStack == null) { + bodyTextStack = new ArrayStack<>(); + } + bodyTextStack.push(bodyText.trim()); + } + + } + + /** + * Process any body texts now. + */ + @Override + public void end(String namespace, String name) { + if (bodyTextStack != null && !bodyTextStack.empty()) { + // what we do now is push one parameter onto the top set of parameters + Object parameters[] = (Object[]) digester.peekParams(); + parameters[paramIndex] = bodyTextStack.pop(); + } + } + + /** + * Render a printable version of this Rule. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("CallParamRule["); + sb.append("paramIndex="); + sb.append(paramIndex); + sb.append(", attributeName="); + sb.append(attributeName); + sb.append(", from stack="); + sb.append(fromStack); + sb.append(']'); + return sb.toString(); + } + + +} diff --git a/java/org/apache/tomcat/util/digester/Digester.java b/java/org/apache/tomcat/util/digester/Digester.java new file mode 100644 index 0000000..034aa8b --- /dev/null +++ b/java/org/apache/tomcat/util/digester/Digester.java @@ -0,0 +1,2110 @@ +/* + * 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.tomcat.util.digester; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.EmptyStackException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.IntrospectionUtils.PropertySource; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.Attributes; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; +import org.xml.sax.ext.DefaultHandler2; +import org.xml.sax.ext.EntityResolver2; +import org.xml.sax.ext.Locator2; +import org.xml.sax.helpers.AttributesImpl; + + +/** + *

    A Digester processes an XML input stream by matching a + * series of element nesting patterns to execute Rules that have been added + * prior to the start of parsing. This package was inspired by the + * XmlMapper class that was part of Tomcat 3.0 and 3.1, + * but is organized somewhat differently.

    + * + *

    See the Digester + * Developer Guide for more information.

    + * + *

    IMPLEMENTATION NOTE - A single Digester instance may + * only be used within the context of a single thread at a time, and a call + * to parse() must be completed before another can be initiated + * even from the same thread.

    + * + *

    IMPLEMENTATION NOTE - A bug in Xerces 2.0.2 prevents + * the support of XML schema. You need Xerces 2.1/2.3 and up to make + * this class working with XML schema

    + */ +public class Digester extends DefaultHandler2 { + + // ---------------------------------------------------------- Static Fields + + protected static IntrospectionUtils.PropertySource[] propertySources; + private static boolean propertySourcesSet = false; + protected static final StringManager sm = StringManager.getManager(Digester.class); + + static { + String classNames = System.getProperty("org.apache.tomcat.util.digester.PROPERTY_SOURCE"); + ArrayList sourcesList = new ArrayList<>(); + IntrospectionUtils.PropertySource[] sources = null; + if (classNames != null) { + StringTokenizer classNamesTokenizer = new StringTokenizer(classNames, ","); + while (classNamesTokenizer.hasMoreTokens()) { + String className = classNamesTokenizer.nextToken().trim(); + ClassLoader[] cls = new ClassLoader[] { Digester.class.getClassLoader(), + Thread.currentThread().getContextClassLoader() }; + for (ClassLoader cl : cls) { + try { + Class clazz = Class.forName(className, true, cl); + sourcesList.add((PropertySource) clazz.getConstructor().newInstance()); + break; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + LogFactory.getLog(Digester.class).error(sm.getString("digester.propertySourceLoadError", className), t); + } + } + } + sources = sourcesList.toArray(new IntrospectionUtils.PropertySource[0]); + } + if (sources != null) { + propertySources = sources; + propertySourcesSet = true; + } + if (Boolean.getBoolean("org.apache.tomcat.util.digester.REPLACE_SYSTEM_PROPERTIES")) { + replaceSystemProperties(); + } + } + + public static void setPropertySource(IntrospectionUtils.PropertySource propertySource) { + if (!propertySourcesSet) { + propertySources = new IntrospectionUtils.PropertySource[1]; + propertySources[0] = propertySource; + propertySourcesSet = true; + } + } + + public static void setPropertySource(IntrospectionUtils.PropertySource[] propertySources) { + if (!propertySourcesSet) { + Digester.propertySources = propertySources; + propertySourcesSet = true; + } + } + + private static final HashSet generatedClasses = new HashSet<>(); + + public static void addGeneratedClass(String className) { + generatedClasses.add(className); + } + + public static String[] getGeneratedClasses() { + return generatedClasses.toArray(new String[0]); + } + + public interface GeneratedCodeLoader { + Object loadGeneratedCode(String className); + } + + private static GeneratedCodeLoader generatedCodeLoader; + + public static boolean isGeneratedCodeLoaderSet() { + return generatedCodeLoader != null; + } + + public static void setGeneratedCodeLoader(GeneratedCodeLoader generatedCodeLoader) { + if (Digester.generatedCodeLoader == null) { + Digester.generatedCodeLoader = generatedCodeLoader; + } + } + + public static Object loadGeneratedClass(String className) { + if (generatedCodeLoader != null) { + return generatedCodeLoader.loadGeneratedCode(className); + } + return null; + } + + // --------------------------------------------------- Instance Variables + + + protected IntrospectionUtils.PropertySource[] source; + + + /** + * The body text of the current element. + */ + protected StringBuilder bodyText = new StringBuilder(); + + + /** + * The stack of body text string buffers for surrounding elements. + */ + protected ArrayStack bodyTexts = new ArrayStack<>(); + + + /** + * Stack whose elements are List objects, each containing a list of + * Rule objects as returned from Rules.getMatch(). As each xml element + * in the input is entered, the matching rules are pushed onto this + * stack. After the end tag is reached, the matches are popped again. + * The depth of is stack is therefore exactly the same as the current + * "nesting" level of the input xml. + * + * @since 1.6 + */ + protected ArrayStack> matches = new ArrayStack<>(10); + + /** + * The class loader to use for instantiating application objects. + * If not specified, the context class loader, or the class loader + * used to load Digester itself, is used, based on the value of the + * useContextClassLoader variable. + */ + protected ClassLoader classLoader = null; + + + /** + * Has this Digester been configured yet. + */ + protected boolean configured = false; + + + /** + * The EntityResolver used by the SAX parser. By default it use this class + */ + protected EntityResolver entityResolver; + + /** + * The URLs of entityValidator that have been registered, keyed by the public + * identifier that corresponds. + */ + protected HashMap entityValidator = new HashMap<>(); + + + /** + * The application-supplied error handler that is notified when parsing + * warnings, errors, or fatal errors occur. + */ + protected ErrorHandler errorHandler = null; + + + /** + * The SAXParserFactory that is created the first time we need it. + */ + protected SAXParserFactory factory = null; + + /** + * The Locator associated with our parser. + */ + protected Locator locator = null; + + + /** + * The current match pattern for nested element processing. + */ + protected String match = ""; + + + /** + * Do we want a "namespace aware" parser. + */ + protected boolean namespaceAware = false; + + + /** + * Registered namespaces we are currently processing. The key is the + * namespace prefix that was declared in the document. The value is an + * ArrayStack of the namespace URIs this prefix has been mapped to -- + * the top Stack element is the most current one. (This architecture + * is required because documents can declare nested uses of the same + * prefix for different Namespace URIs). + */ + protected HashMap> namespaces = new HashMap<>(); + + + /** + * The parameters stack being utilized by CallMethodRule and + * CallParamRule rules. + */ + protected ArrayStack params = new ArrayStack<>(); + + + /** + * The SAXParser we will use to parse the input stream. + */ + protected SAXParser parser = null; + + + /** + * The public identifier of the DTD we are currently parsing under + * (if any). + */ + protected String publicId = null; + + + /** + * The XMLReader used to parse digester rules. + */ + protected XMLReader reader = null; + + + /** + * The "root" element of the stack (in other words, the last object + * that was popped. + */ + protected Object root = null; + + + /** + * The Rules implementation containing our collection of + * Rule instances and associated matching policy. If not + * established before the first rule is added, a default implementation + * will be provided. + */ + protected Rules rules = null; + + /** + * The object stack being constructed. + */ + protected ArrayStack stack = new ArrayStack<>(); + + + /** + * Do we want to use the Context ClassLoader when loading classes + * for instantiating new objects. Default is false. + */ + protected boolean useContextClassLoader = false; + + + /** + * Do we want to use a validating parser. + */ + protected boolean validating = false; + + + /** + * Warn on missing attributes and elements. + */ + protected boolean rulesValidation = false; + + + /** + * Fake attributes map (attributes are often used for object creation). + */ + protected Map, List> fakeAttributes = null; + + + /** + * The Log to which most logging calls will be made. + */ + protected Log log = LogFactory.getLog(Digester.class); + + /** + * The Log to which all SAX event related logging calls will be made. + */ + protected Log saxLog = LogFactory.getLog("org.apache.tomcat.util.digester.Digester.sax"); + + /** + * Generated code. + */ + protected StringBuilder code = null; + + public Digester() { + propertySourcesSet = true; + ArrayList sourcesList = new ArrayList<>(); + boolean systemPropertySourceFound = false; + if (propertySources != null) { + for (IntrospectionUtils.PropertySource source : propertySources) { + if (source instanceof SystemPropertySource) { + systemPropertySourceFound = true; + } + sourcesList.add(source); + } + } + if (!systemPropertySourceFound) { + sourcesList.add(new SystemPropertySource()); + } + source = sourcesList.toArray(new IntrospectionUtils.PropertySource[0]); + } + + + public static void replaceSystemProperties() { + Log log = LogFactory.getLog(Digester.class); + if (propertySources != null) { + Properties properties = System.getProperties(); + Set names = properties.stringPropertyNames(); + for (String name : names) { + String value = System.getProperty(name); + if (value != null) { + try { + String newValue = IntrospectionUtils.replaceProperties(value, null, propertySources, null); + if (!value.equals(newValue)) { + System.setProperty(name, newValue); + } + } catch (Exception e) { + log.warn(sm.getString("digester.failedToUpdateSystemProperty", name, value), e); + } + } + } + } + } + + + public void startGeneratingCode() { + code = new StringBuilder(); + } + + public void endGeneratingCode() { + code = null; + known.clear(); + } + + public StringBuilder getGeneratedCode() { + return code; + } + + protected ArrayList known = new ArrayList<>(); + public void setKnown(Object object) { + known.add(object); + } + public String toVariableName(Object object) { + boolean found = false; + int pos = 0; + if (known.size() > 0) { + for (int i = known.size() - 1; i >= 0; i--) { + if (known.get(i) == object) { + pos = i; + found = true; + break; + } + } + } + if (!found) { + pos = known.size(); + known.add(object); + } + return "tc_" + object.getClass().getSimpleName() + "_" + String.valueOf(pos); + } + + // ------------------------------------------------------------- Properties + + /** + * Return the currently mapped namespace URI for the specified prefix, + * if any; otherwise return null. These mappings come and + * go dynamically as the document is parsed. + * + * @param prefix Prefix to look up + * @return the namespace URI + */ + public String findNamespaceURI(String prefix) { + ArrayStack stack = namespaces.get(prefix); + if (stack == null) { + return null; + } + try { + return stack.peek(); + } catch (EmptyStackException e) { + return null; + } + } + + + /** + * Return the class loader to be used for instantiating application objects + * when required. This is determined based upon the following rules: + *
      + *
    • The class loader set by setClassLoader(), if any
    • + *
    • The thread context class loader, if it exists and the + * useContextClassLoader property is set to true
    • + *
    • The class loader used to load the Digester class itself. + *
    + * @return the classloader + */ + public ClassLoader getClassLoader() { + if (this.classLoader != null) { + return this.classLoader; + } + if (this.useContextClassLoader) { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader != null) { + return classLoader; + } + } + return this.getClass().getClassLoader(); + } + + + /** + * Set the class loader to be used for instantiating application objects + * when required. + * + * @param classLoader The new class loader to use, or null + * to revert to the standard rules + */ + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + /** + * @return the current depth of the element stack. + */ + public int getCount() { + return stack.size(); + } + + + /** + * @return the name of the XML element that is currently being processed. + */ + public String getCurrentElementName() { + String elementName = match; + int lastSlash = elementName.lastIndexOf('/'); + if (lastSlash >= 0) { + elementName = elementName.substring(lastSlash + 1); + } + return elementName; + } + + + /** + * @return the error handler for this Digester. + */ + public ErrorHandler getErrorHandler() { + return this.errorHandler; + } + + + /** + * Set the error handler for this Digester. + * + * @param errorHandler The new error handler + */ + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + + /** + * SAX parser factory method. + * @return the SAXParserFactory we will use, creating one if necessary. + * @throws ParserConfigurationException Error creating parser + * @throws SAXNotSupportedException Error creating parser + * @throws SAXNotRecognizedException Error creating parser + */ + public SAXParserFactory getFactory() throws SAXNotRecognizedException, SAXNotSupportedException, + ParserConfigurationException { + + if (factory == null) { + factory = SAXParserFactory.newInstance(); + + factory.setNamespaceAware(namespaceAware); + // Preserve xmlns attributes + if (namespaceAware) { + factory.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + } + + factory.setValidating(validating); + if (validating) { + // Enable DTD validation + factory.setFeature("http://xml.org/sax/features/validation", true); + // Enable schema validation + factory.setFeature("http://apache.org/xml/features/validation/schema", true); + } + } + return factory; + } + + + /** + * Sets a flag indicating whether the requested feature is supported + * by the underlying implementation of org.xml.sax.XMLReader. + * See + * http://www.saxproject.org/apidoc/xml/sax/package-summary.html#package-description + * for information about the standard SAX2 feature flags. In order to be + * effective, this method must be called before the + * getParser() method is called for the first time, either + * directly or indirectly. + * + * @param feature Name of the feature to set the status for + * @param value The new value for this feature + * + * @exception ParserConfigurationException if a parser configuration error + * occurs + * @exception SAXNotRecognizedException if the property name is + * not recognized + * @exception SAXNotSupportedException if the property name is + * recognized but not supported + */ + public void setFeature(String feature, boolean value) throws ParserConfigurationException, + SAXNotRecognizedException, SAXNotSupportedException { + + getFactory().setFeature(feature, value); + + } + + + /** + * @return the current Logger associated with this instance of the Digester + */ + public Log getLogger() { + + return log; + + } + + + /** + * Set the current logger for this Digester. + * @param log The logger that will be used + */ + public void setLogger(Log log) { + + this.log = log; + + } + + /** + * Gets the logger used for logging SAX-related information. + * Note the output is finely grained. + * + * @since 1.6 + * @return the SAX logger + */ + public Log getSAXLogger() { + + return saxLog; + } + + + /** + * Sets the logger used for logging SAX-related information. + * Note the output is finely grained. + * @param saxLog Log, not null + * + * @since 1.6 + */ + public void setSAXLogger(Log saxLog) { + + this.saxLog = saxLog; + } + + /** + * @return the current rule match path + */ + public String getMatch() { + + return match; + + } + + + /** + * @return the "namespace aware" flag for parsers we create. + */ + public boolean getNamespaceAware() { + return this.namespaceAware; + } + + + /** + * Set the "namespace aware" flag for parsers we create. + * + * @param namespaceAware The new "namespace aware" flag + */ + public void setNamespaceAware(boolean namespaceAware) { + this.namespaceAware = namespaceAware; + } + + + /** + * Set the public id of the current file being parse. + * @param publicId the DTD/Schema public's id. + */ + public void setPublicId(String publicId) { + this.publicId = publicId; + } + + + /** + * @return the public identifier of the DTD we are currently + * parsing under, if any. + */ + public String getPublicId() { + return this.publicId; + } + + + /** + * @return the SAXParser we will use to parse the input stream. If there + * is a problem creating the parser, return null. + */ + public SAXParser getParser() { + + // Return the parser we already created (if any) + if (parser != null) { + return parser; + } + + // Create a new parser + try { + parser = getFactory().newSAXParser(); + } catch (Exception e) { + log.error(sm.getString("digester.createParserError"), e); + return null; + } + + return parser; + } + + + /** + * Return the current value of the specified property for the underlying + * XMLReader implementation. + * See + * http://www.saxproject.org/apidoc/xml/sax/package-summary.html#package-description + * for information about the standard SAX2 properties. + * + * @param property Property name to be retrieved + * @return the property value + * @exception SAXNotRecognizedException if the property name is + * not recognized + * @exception SAXNotSupportedException if the property name is + * recognized but not supported + */ + public Object getProperty(String property) + throws SAXNotRecognizedException, SAXNotSupportedException { + + return getParser().getProperty(property); + } + + + /** + * Return the Rules implementation object containing our + * rules collection and associated matching policy. If none has been + * established, a default implementation will be created and returned. + * @return the rules + */ + public Rules getRules() { + if (this.rules == null) { + this.rules = new RulesBase(); + this.rules.setDigester(this); + } + return this.rules; + } + + + /** + * Set the Rules implementation object containing our + * rules collection and associated matching policy. + * + * @param rules New Rules implementation + */ + public void setRules(Rules rules) { + this.rules = rules; + this.rules.setDigester(this); + } + + + /** + * @return the boolean as to whether the context classloader should be used. + */ + public boolean getUseContextClassLoader() { + return useContextClassLoader; + } + + + /** + * Determine whether to use the Context ClassLoader (the one found by + * calling Thread.currentThread().getContextClassLoader()) + * to resolve/load classes that are defined in various rules. If not + * using Context ClassLoader, then the class-loading defaults to + * using the calling-class' ClassLoader. + * + * @param use determines whether to use Context ClassLoader. + */ + public void setUseContextClassLoader(boolean use) { + + useContextClassLoader = use; + + } + + + /** + * @return the validating parser flag. + */ + public boolean getValidating() { + return this.validating; + } + + + /** + * Set the validating parser flag. This must be called before + * parse() is called the first time. + * + * @param validating The new validating parser flag. + */ + public void setValidating(boolean validating) { + this.validating = validating; + } + + + /** + * @return the rules validation flag. + */ + public boolean getRulesValidation() { + return this.rulesValidation; + } + + + /** + * Set the rules validation flag. This must be called before + * parse() is called the first time. + * + * @param rulesValidation The new rules validation flag. + */ + public void setRulesValidation(boolean rulesValidation) { + this.rulesValidation = rulesValidation; + } + + + /** + * @return the fake attributes list. + */ + public Map, List> getFakeAttributes() { + return this.fakeAttributes; + } + + + /** + * Determine if an attribute is a fake attribute. + * @param object The object + * @param name The attribute name + * @return true if this is a fake attribute + */ + public boolean isFakeAttribute(Object object, String name) { + if (fakeAttributes == null) { + return false; + } + List result = fakeAttributes.get(object.getClass()); + if (result == null) { + result = fakeAttributes.get(Object.class); + } + if (result == null) { + return false; + } else { + return result.contains(name); + } + } + + + /** + * Set the fake attributes. + * + * @param fakeAttributes The new fake attributes. + */ + public void setFakeAttributes(Map, List> fakeAttributes) { + + this.fakeAttributes = fakeAttributes; + + } + + + /** + * Return the XMLReader to be used for parsing the input document. + * + * FIX ME: there is a bug in JAXP/XERCES that prevent the use of a + * parser that contains a schema with a DTD. + * @return the XML reader + * @exception SAXException if no XMLReader can be instantiated + */ + public XMLReader getXMLReader() throws SAXException { + if (reader == null) { + reader = getParser().getXMLReader(); + } + + reader.setDTDHandler(this); + reader.setContentHandler(this); + + EntityResolver entityResolver = getEntityResolver(); + if (entityResolver == null) { + entityResolver = this; + } + + // Wrap the resolver so we can perform ${...} property replacement + if (entityResolver instanceof EntityResolver2) { + entityResolver = new EntityResolver2Wrapper((EntityResolver2) entityResolver, source, classLoader); + } else { + entityResolver = new EntityResolverWrapper(entityResolver, source, classLoader); + } + + reader.setEntityResolver(entityResolver); + + reader.setProperty("http://xml.org/sax/properties/lexical-handler", this); + + reader.setErrorHandler(this); + return reader; + } + + // ------------------------------------------------- ContentHandler Methods + + + /** + * Process notification of character data received from the body of + * an XML element. + * + * @param buffer The characters from the XML document + * @param start Starting offset into the buffer + * @param length Number of characters from the buffer + * + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void characters(char buffer[], int start, int length) throws SAXException { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("characters(" + new String(buffer, start, length) + ")"); + } + + bodyText.append(buffer, start, length); + + } + + + /** + * Process notification of the end of the document being reached. + * + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void endDocument() throws SAXException { + + if (saxLog.isTraceEnabled()) { + if (getCount() > 1) { + saxLog.trace("endDocument(): " + getCount() + " elements left"); + } else { + saxLog.trace("endDocument()"); + } + } + + while (getCount() > 1) { + pop(); + } + + // Fire "finish" events for all defined rules + for (Rule rule : getRules().rules()) { + try { + rule.finish(); + } catch (Exception e) { + log.error(sm.getString("digester.error.finish"), e); + throw createSAXException(e); + } catch (Error e) { + log.error(sm.getString("digester.error.finish"), e); + throw e; + } + } + + // Perform final cleanup + clear(); + + } + + + /** + * Process notification of the end of an XML element being reached. + * + * @param namespaceURI - The Namespace URI, or the empty string if the + * element has no Namespace URI or if Namespace processing is not + * being performed. + * @param localName - The local name (without prefix), or the empty + * string if Namespace processing is not being performed. + * @param qName - The qualified XML 1.0 name (with prefix), or the + * empty string if qualified names are not available. + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + + boolean debug = log.isTraceEnabled(); + + if (debug) { + if (saxLog.isDebugEnabled()) { + saxLog.trace("endElement(" + namespaceURI + "," + localName + "," + qName + ")"); + } + log.trace(" match='" + match + "'"); + log.trace(" bodyText='" + bodyText + "'"); + } + + // Parse system properties + bodyText = updateBodyText(bodyText); + + // the actual element name is either in localName or qName, depending + // on whether the parser is namespace aware + String name = localName; + if ((name == null) || (name.length() < 1)) { + name = qName; + } + + // Fire "body" events for all relevant rules + List rules = matches.pop(); + if ((rules != null) && (rules.size() > 0)) { + String bodyText = this.bodyText.toString().intern(); + for (Rule value : rules) { + try { + Rule rule = value; + if (debug) { + log.trace(" Fire body() for " + rule); + } + rule.body(namespaceURI, name, bodyText); + } catch (Exception e) { + log.error(sm.getString("digester.error.body"), e); + throw createSAXException(e); + } catch (Error e) { + log.error(sm.getString("digester.error.body"), e); + throw e; + } + } + } else { + if (debug) { + log.trace(sm.getString("digester.noRulesFound", match)); + } + if (rulesValidation) { + log.warn(sm.getString("digester.noRulesFound", match)); + } + } + + // Recover the body text from the surrounding element + bodyText = bodyTexts.pop(); + + // Fire "end" events for all relevant rules in reverse order + if (rules != null) { + for (int i = 0; i < rules.size(); i++) { + int j = (rules.size() - i) - 1; + try { + Rule rule = rules.get(j); + if (debug) { + log.trace(" Fire end() for " + rule); + } + rule.end(namespaceURI, name); + } catch (Exception e) { + log.error(sm.getString("digester.error.end"), e); + throw createSAXException(e); + } catch (Error e) { + log.error(sm.getString("digester.error.end"), e); + throw e; + } + } + } + + // Recover the previous match expression + int slash = match.lastIndexOf('/'); + if (slash >= 0) { + match = match.substring(0, slash); + } else { + match = ""; + } + + } + + + /** + * Process notification that a namespace prefix is going out of scope. + * + * @param prefix Prefix that is going out of scope + * + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void endPrefixMapping(String prefix) throws SAXException { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("endPrefixMapping(" + prefix + ")"); + } + + // Deregister this prefix mapping + ArrayStack stack = namespaces.get(prefix); + if (stack == null) { + return; + } + try { + stack.pop(); + if (stack.empty()) { + namespaces.remove(prefix); + } + } catch (EmptyStackException e) { + throw createSAXException(sm.getString("digester.emptyStackError")); + } + + } + + + /** + * Process notification of ignorable whitespace received from the body of + * an XML element. + * + * @param buffer The characters from the XML document + * @param start Starting offset into the buffer + * @param len Number of characters from the buffer + * + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void ignorableWhitespace(char buffer[], int start, int len) throws SAXException { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("ignorableWhitespace(" + new String(buffer, start, len) + ")"); + } + + // No processing required + + } + + + /** + * Process notification of a processing instruction that was encountered. + * + * @param target The processing instruction target + * @param data The processing instruction data (if any) + * + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void processingInstruction(String target, String data) throws SAXException { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("processingInstruction('" + target + "','" + data + "')"); + } + + // No processing is required + + } + + + /** + * Gets the document locator associated with our parser. + * + * @return the Locator supplied by the document parser + */ + public Locator getDocumentLocator() { + + return locator; + + } + + /** + * Sets the document locator associated with our parser. + * + * @param locator The new locator + */ + @Override + public void setDocumentLocator(Locator locator) { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("setDocumentLocator(" + locator + ")"); + } + + this.locator = locator; + + } + + + /** + * Process notification of a skipped entity. + * + * @param name Name of the skipped entity + * + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void skippedEntity(String name) throws SAXException { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("skippedEntity(" + name + ")"); + } + + // No processing required + + } + + + /** + * Process notification of the beginning of the document being reached. + * + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void startDocument() throws SAXException { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("startDocument()"); + } + + if (locator instanceof Locator2) { + if (root instanceof DocumentProperties.Charset) { + String enc = ((Locator2) locator).getEncoding(); + if (enc != null) { + try { + ((DocumentProperties.Charset) root).setCharset(B2CConverter.getCharset(enc)); + } catch (UnsupportedEncodingException e) { + log.warn(sm.getString("digester.encodingInvalid", enc), e); + } + } + } + } + + // ensure that the digester is properly configured, as + // the digester could be used as a SAX ContentHandler + // rather than via the parse() methods. + configure(); + } + + + /** + * Process notification of the start of an XML element being reached. + * + * @param namespaceURI The Namespace URI, or the empty string if the element + * has no Namespace URI or if Namespace processing is not being performed. + * @param localName The local name (without prefix), or the empty + * string if Namespace processing is not being performed. + * @param qName The qualified name (with prefix), or the empty + * string if qualified names are not available.\ + * @param list The attributes attached to the element. If there are + * no attributes, it shall be an empty Attributes object. + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void startElement(String namespaceURI, String localName, String qName, Attributes list) + throws SAXException { + boolean debug = log.isTraceEnabled(); + + if (saxLog.isTraceEnabled()) { + saxLog.trace("startElement(" + namespaceURI + "," + localName + "," + qName + ")"); + } + + // Parse system properties + list = updateAttributes(list); + + // Save the body text accumulated for our surrounding element + bodyTexts.push(bodyText); + bodyText = new StringBuilder(); + + // the actual element name is either in localName or qName, depending + // on whether the parser is namespace aware + String name = localName; + if ((name == null) || (name.length() < 1)) { + name = qName; + } + + // Compute the current matching rule + StringBuilder sb = new StringBuilder(match); + if (match.length() > 0) { + sb.append('/'); + } + sb.append(name); + match = sb.toString(); + if (debug) { + log.trace(" New match='" + match + "'"); + } + + // Fire "begin" events for all relevant rules + List rules = getRules().match(namespaceURI, match); + matches.push(rules); + if ((rules != null) && (rules.size() > 0)) { + for (Rule value : rules) { + try { + Rule rule = value; + if (debug) { + log.trace(" Fire begin() for " + rule); + } + rule.begin(namespaceURI, name, list); + } catch (Exception e) { + log.error(sm.getString("digester.error.begin"), e); + throw createSAXException(e); + } catch (Error e) { + log.error(sm.getString("digester.error.begin"), e); + throw e; + } + } + } else { + if (debug) { + log.trace(sm.getString("digester.noRulesFound", match)); + } + } + + } + + + /** + * Process notification that a namespace prefix is coming in to scope. + * + * @param prefix Prefix that is being declared + * @param namespaceURI Corresponding namespace URI being mapped to + * + * @exception SAXException if a parsing error is to be reported + */ + @Override + public void startPrefixMapping(String prefix, String namespaceURI) throws SAXException { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("startPrefixMapping(" + prefix + "," + namespaceURI + ")"); + } + + // Register this prefix mapping + ArrayStack stack = namespaces.get(prefix); + if (stack == null) { + stack = new ArrayStack<>(); + namespaces.put(prefix, stack); + } + stack.push(namespaceURI); + + } + + + // ----------------------------------------------------- DTDHandler Methods + + + /** + * Receive notification of a notation declaration event. + * + * @param name The notation name + * @param publicId The public identifier (if any) + * @param systemId The system identifier (if any) + */ + @Override + public void notationDecl(String name, String publicId, String systemId) { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("notationDecl(" + name + "," + publicId + "," + systemId + ")"); + } + + } + + + /** + * Receive notification of an unparsed entity declaration event. + * + * @param name The unparsed entity name + * @param publicId The public identifier (if any) + * @param systemId The system identifier (if any) + * @param notation The name of the associated notation + */ + @Override + public void unparsedEntityDecl(String name, String publicId, String systemId, String notation) { + + if (saxLog.isTraceEnabled()) { + saxLog.trace("unparsedEntityDecl(" + name + "," + publicId + "," + systemId + "," + + notation + ")"); + } + + } + + + // ----------------------------------------------- EntityResolver Methods + + /** + * Set the EntityResolver used by SAX when resolving + * public id and system id. + * This must be called before the first call to parse(). + * @param entityResolver a class that implement the EntityResolver interface. + */ + public void setEntityResolver(EntityResolver entityResolver) { + this.entityResolver = entityResolver; + } + + + /** + * Return the Entity Resolver used by the SAX parser. + * @return Return the Entity Resolver used by the SAX parser. + */ + public EntityResolver getEntityResolver() { + return entityResolver; + } + + @Override + public InputSource resolveEntity(String name, String publicId, String baseURI, String systemId) + throws SAXException, IOException { + + if (saxLog.isTraceEnabled()) { + saxLog.trace( + "resolveEntity('" + publicId + "', '" + systemId + "', '" + baseURI + "')"); + } + + // Has this system identifier been registered? + String entityURL = null; + if (publicId != null) { + entityURL = entityValidator.get(publicId); + } + + if (entityURL == null) { + if (systemId == null) { + // cannot resolve + if (log.isTraceEnabled()) { + log.trace(" Cannot resolve entity: '" + publicId + "'"); + } + return null; + + } else { + // try to resolve using system ID + if (log.isTraceEnabled()) { + log.trace(" Trying to resolve using system ID '" + systemId + "'"); + } + entityURL = systemId; + // resolve systemId against baseURI if it is not absolute + if (baseURI != null) { + try { + URI uri = new URI(systemId); + if (!uri.isAbsolute()) { + entityURL = new URI(baseURI).resolve(uri).toString(); + } + } catch (URISyntaxException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("digester.invalidURI", baseURI, systemId)); + } + } + } + } + } + + // Return an input source to our alternative URL + if (log.isTraceEnabled()) { + log.trace(" Resolving to alternate DTD '" + entityURL + "'"); + } + + try { + return new InputSource(entityURL); + } catch (Exception e) { + throw createSAXException(e); + } + } + + + // ----------------------------------------------- LexicalHandler Methods + + @Override + public void startDTD(String name, String publicId, String systemId) throws SAXException { + setPublicId(publicId); + } + + + // ------------------------------------------------- ErrorHandler Methods + + /** + * Forward notification of a parsing error to the application supplied + * error handler (if any). + * + * @param exception The error information + * + * @exception SAXException if a parsing exception occurs + */ + @Override + public void error(SAXParseException exception) throws SAXException { + log.error(sm.getString("digester.parseError", Integer.valueOf(exception.getLineNumber()), + Integer.valueOf(exception.getColumnNumber())), exception); + if (errorHandler != null) { + errorHandler.error(exception); + } + } + + + /** + * Forward notification of a fatal parsing error to the application + * supplied error handler (if any). + * + * @param exception The fatal error information + * + * @exception SAXException if a parsing exception occurs + */ + @Override + public void fatalError(SAXParseException exception) throws SAXException { + log.error(sm.getString("digester.parseErrorFatal", Integer.valueOf(exception.getLineNumber()), + Integer.valueOf(exception.getColumnNumber())), exception); + if (errorHandler != null) { + errorHandler.fatalError(exception); + } + } + + + /** + * Forward notification of a parse warning to the application supplied + * error handler (if any). + * + * @param exception The warning information + * + * @exception SAXException if a parsing exception occurs + */ + @Override + public void warning(SAXParseException exception) throws SAXException { + log.error(sm.getString("digester.parseWarning", Integer.valueOf(exception.getLineNumber()), + Integer.valueOf(exception.getColumnNumber()), exception)); + if (errorHandler != null) { + errorHandler.warning(exception); + } + + } + + + // ------------------------------------------------------- Public Methods + + /** + * Parse the content of the specified file using this Digester. Returns + * the root element from the object stack (if any). + * + * @param file File containing the XML data to be parsed + * @return the root object + * @exception IOException if an input/output error occurs + * @exception SAXException if a parsing exception occurs + */ + public Object parse(File file) throws IOException, SAXException { + configure(); + InputSource input = new InputSource(new FileInputStream(file)); + input.setSystemId("file://" + file.getAbsolutePath()); + getXMLReader().parse(input); + return root; + } + + + /** + * Parse the content of the specified input source using this Digester. + * Returns the root element from the object stack (if any). + * + * @param input Input source containing the XML data to be parsed + * @return the root object + * @exception IOException if an input/output error occurs + * @exception SAXException if a parsing exception occurs + */ + public Object parse(InputSource input) throws IOException, SAXException { + configure(); + getXMLReader().parse(input); + return root; + } + + + /** + * Parse the content of the specified input stream using this Digester. + * Returns the root element from the object stack (if any). + * + * @param input Input stream containing the XML data to be parsed + * @return the root object + * @exception IOException if an input/output error occurs + * @exception SAXException if a parsing exception occurs + */ + public Object parse(InputStream input) throws IOException, SAXException { + configure(); + InputSource is = new InputSource(input); + getXMLReader().parse(is); + return root; + } + + + /** + *

    Register the specified DTD URL for the specified public identifier. + * This must be called before the first call to parse(). + *

    + * Digester contains an internal EntityResolver + * implementation. This maps PUBLICID's to URLs + * (from which the resource will be loaded). A common use case for this + * method is to register local URLs (possibly computed at runtime by a + * classloader) for DTDs. This allows the performance advantage of using + * a local version without having to ensure every SYSTEM + * URI on every processed xml document is local. This implementation provides + * only basic functionality. If more sophisticated features are required, + * using {@link #setEntityResolver} to set a custom resolver is recommended. + *

    + * Note: This method will have no effect when a custom + * EntityResolver has been set. (Setting a custom + * EntityResolver overrides the internal implementation.) + *

    + * @param publicId Public identifier of the DTD to be resolved + * @param entityURL The URL to use for reading this DTD + */ + public void register(String publicId, String entityURL) { + + if (log.isTraceEnabled()) { + log.trace("register('" + publicId + "', '" + entityURL + "'"); + } + entityValidator.put(publicId, entityURL); + + } + + + // --------------------------------------------------------- Rule Methods + + + /** + *

    Register a new Rule matching the specified pattern. + * This method sets the Digester property on the rule.

    + * + * @param pattern Element matching pattern + * @param rule Rule to be registered + */ + public void addRule(String pattern, Rule rule) { + + rule.setDigester(this); + getRules().add(pattern, rule); + + } + + + /** + * Register a set of Rule instances defined in a RuleSet. + * + * @param ruleSet The RuleSet instance to configure from + */ + public void addRuleSet(RuleSet ruleSet) { + ruleSet.addRuleInstances(this); + } + + + /** + * Add an "call method" rule for a method which accepts no arguments. + * + * @param pattern Element matching pattern + * @param methodName Method name to be called + * @see CallMethodRule + */ + public void addCallMethod(String pattern, String methodName) { + + addRule(pattern, new CallMethodRule(methodName)); + + } + + /** + * Add an "call method" rule for the specified parameters. + * + * @param pattern Element matching pattern + * @param methodName Method name to be called + * @param paramCount Number of expected parameters (or zero + * for a single parameter from the body of this element) + * @see CallMethodRule + */ + public void addCallMethod(String pattern, String methodName, int paramCount) { + + addRule(pattern, new CallMethodRule(methodName, paramCount)); + + } + + + /** + * Add a "call parameter" rule for the specified parameters. + * + * @param pattern Element matching pattern + * @param paramIndex Zero-relative parameter index to set + * (from the body of this element) + * @see CallParamRule + */ + public void addCallParam(String pattern, int paramIndex) { + + addRule(pattern, new CallParamRule(paramIndex)); + + } + + + /** + * Add a "factory create" rule for the specified parameters. + * + * @param pattern Element matching pattern + * @param creationFactory Previously instantiated ObjectCreationFactory + * to be utilized + * @param ignoreCreateExceptions when true any exceptions thrown during + * object creation will be ignored. + * @see FactoryCreateRule + */ + public void addFactoryCreate(String pattern, ObjectCreationFactory creationFactory, + boolean ignoreCreateExceptions) { + + creationFactory.setDigester(this); + addRule(pattern, new FactoryCreateRule(creationFactory, ignoreCreateExceptions)); + + } + + /** + * Add an "object create" rule for the specified parameters. + * + * @param pattern Element matching pattern + * @param className Java class name to be created + * @see ObjectCreateRule + */ + public void addObjectCreate(String pattern, String className) { + + addRule(pattern, new ObjectCreateRule(className)); + + } + + + /** + * Add an "object create" rule for the specified parameters. + * + * @param pattern Element matching pattern + * @param className Default Java class name to be created + * @param attributeName Attribute name that optionally overrides + * the default Java class name to be created + * @see ObjectCreateRule + */ + public void addObjectCreate(String pattern, String className, String attributeName) { + + addRule(pattern, new ObjectCreateRule(className, attributeName)); + + } + + + /** + * Add a "set next" rule for the specified parameters. + * + * @param pattern Element matching pattern + * @param methodName Method name to call on the parent element + * @param paramType Java class name of the expected parameter type + * (if you wish to use a primitive type, specify the corresponding + * Java wrapper class instead, such as java.lang.Boolean + * for a boolean parameter) + * @see SetNextRule + */ + public void addSetNext(String pattern, String methodName, String paramType) { + + addRule(pattern, new SetNextRule(methodName, paramType)); + + } + + + /** + * Add a "set properties" rule for the specified parameters. + * + * @param pattern Element matching pattern + * @see SetPropertiesRule + */ + public void addSetProperties(String pattern) { + + addRule(pattern, new SetPropertiesRule()); + + } + + + public void addSetProperties(String pattern, String[] excludes) { + + addRule(pattern, new SetPropertiesRule(excludes)); + + } + + + // --------------------------------------------------- Object Stack Methods + + + /** + * Clear the current contents of the object stack. + *

    + * Calling this method might allow another document of the same type + * to be correctly parsed. However this method was not intended for this + * purpose. In general, a separate Digester object should be created for + * each document to be parsed. + */ + public void clear() { + + match = ""; + bodyTexts.clear(); + params.clear(); + publicId = null; + stack.clear(); + log = null; + saxLog = null; + configured = false; + + } + + + public void reset() { + root = null; + setErrorHandler(null); + clear(); + } + + + /** + * Return the top object on the stack without removing it. If there are + * no objects on the stack, return null. + * @return the top object + */ + public Object peek() { + try { + return stack.peek(); + } catch (EmptyStackException e) { + log.warn(sm.getString("digester.emptyStack")); + return null; + } + } + + + /** + * Return the n'th object down the stack, where 0 is the top element + * and [getCount()-1] is the bottom element. If the specified index + * is out of range, return null. + * + * @param n Index of the desired element, where 0 is the top of the stack, + * 1 is the next element down, and so on. + * @return the specified object + */ + public Object peek(int n) { + try { + return stack.peek(n); + } catch (EmptyStackException e) { + log.warn(sm.getString("digester.emptyStack")); + return null; + } + } + + + /** + * Pop the top object off of the stack, and return it. If there are + * no objects on the stack, return null. + * @return the top object + */ + public Object pop() { + try { + return stack.pop(); + } catch (EmptyStackException e) { + log.warn(sm.getString("digester.emptyStack")); + return null; + } + } + + + /** + * Push a new object onto the top of the object stack. + * + * @param object The new object + */ + public void push(Object object) { + + if (stack.size() == 0) { + root = object; + } + stack.push(object); + + } + + /** + * When the Digester is being used as a SAXContentHandler, + * this method allows you to access the root object that has been + * created after parsing. + * + * @return the root object that has been created after parsing + * or null if the digester has not parsed any XML yet. + */ + public Object getRoot() { + return root; + } + + + // ------------------------------------------------ Parameter Stack Methods + + + // ------------------------------------------------------ Protected Methods + + + /** + *

    + * Provide a hook for lazy configuration of this Digester + * instance. The default implementation does nothing, but subclasses + * can override as needed. + *

    + * + *

    + * Note This method may be called more than once. + *

    + */ + protected void configure() { + + // Do not configure more than once + if (configured) { + return; + } + + log = LogFactory.getLog("org.apache.tomcat.util.digester.Digester"); + saxLog = LogFactory.getLog("org.apache.tomcat.util.digester.Digester.sax"); + + // Set the configuration flag to avoid repeating + configured = true; + } + + + /** + *

    Return the top object on the parameters stack without removing it. If there are + * no objects on the stack, return null.

    + * + *

    The parameters stack is used to store CallMethodRule parameters. + * See {@link #params}.

    + * @return the top object on the parameters stack + */ + public Object peekParams() { + try { + return params.peek(); + } catch (EmptyStackException e) { + log.warn(sm.getString("digester.emptyStack")); + return null; + } + } + + + /** + *

    Pop the top object off of the parameters stack, and return it. If there are + * no objects on the stack, return null.

    + * + *

    The parameters stack is used to store CallMethodRule parameters. + * See {@link #params}.

    + * @return the top object on the parameters stack + */ + public Object popParams() { + try { + if (log.isTraceEnabled()) { + log.trace("Popping params"); + } + return params.pop(); + } catch (EmptyStackException e) { + log.warn(sm.getString("digester.emptyStack")); + return null; + } + } + + + /** + *

    Push a new object onto the top of the parameters stack.

    + * + *

    The parameters stack is used to store CallMethodRule parameters. + * See {@link #params}.

    + * + * @param object The new object + */ + public void pushParams(Object object) { + if (log.isTraceEnabled()) { + log.trace("Pushing params"); + } + params.push(object); + + } + + /** + * Create a SAX exception which also understands about the location in + * the digester file where the exception occurs + * @param message The error message + * @param e The root cause + * @return the new exception + */ + public SAXException createSAXException(String message, Exception e) { + if ((e != null) && (e instanceof InvocationTargetException)) { + Throwable t = e.getCause(); + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + if (t instanceof Exception) { + e = (Exception) t; + } + } + if (locator != null) { + String error = sm.getString("digester.errorLocation", + Integer.valueOf(locator.getLineNumber()), + Integer.valueOf(locator.getColumnNumber()), message); + if (e != null) { + return new SAXParseException(error, locator, e); + } else { + return new SAXParseException(error, locator); + } + } + log.error(sm.getString("digester.noLocator")); + if (e != null) { + return new SAXException(message, e); + } else { + return new SAXException(message); + } + } + + /** + * Create a SAX exception which also understands about the location in + * the digester file where the exception occurs + * @param e The root cause + * @return the new exception + */ + public SAXException createSAXException(Exception e) { + if (e instanceof InvocationTargetException) { + Throwable t = e.getCause(); + if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } + if (t instanceof Exception) { + e = (Exception) t; + } + } + return createSAXException(e.getMessage(), e); + } + + /** + * Create a SAX exception which also understands about the location in + * the digester file where the exception occurs + * @param message The error message + * @return the new exception + */ + public SAXException createSAXException(String message) { + return createSAXException(message, null); + } + + + // ------------------------------------------------------- Private Methods + + + /** + * Returns an attributes list which contains all the attributes + * passed in, with any text of form "${xxx}" in an attribute value + * replaced by the appropriate value from the system property. + */ + private Attributes updateAttributes(Attributes list) { + + if (list.getLength() == 0) { + return list; + } + + AttributesImpl newAttrs = new AttributesImpl(list); + int nAttributes = newAttrs.getLength(); + for (int i = 0; i < nAttributes; ++i) { + String value = newAttrs.getValue(i); + try { + newAttrs.setValue(i, IntrospectionUtils.replaceProperties(value, null, source, getClassLoader()).intern()); + } catch (Exception e) { + log.warn(sm.getString("digester.failedToUpdateAttributes", newAttrs.getLocalName(i), value), e); + } + } + + return newAttrs; + } + + + /** + * Return a new StringBuilder containing the same contents as the + * input buffer, except that data of form ${varname} have been + * replaced by the value of that var as defined in the system property. + */ + private StringBuilder updateBodyText(StringBuilder bodyText) { + String in = bodyText.toString(); + String out; + try { + out = IntrospectionUtils.replaceProperties(in, null, source, getClassLoader()); + } catch (Exception e) { + return bodyText; // return unchanged data + } + + if (out == in) { + // No substitutions required. Don't waste memory creating + // a new buffer + return bodyText; + } else { + return new StringBuilder(out); + } + } + + + private static class EntityResolverWrapper implements EntityResolver { + + private final EntityResolver entityResolver; + private final PropertySource[] source; + private final ClassLoader classLoader; + + EntityResolverWrapper(EntityResolver entityResolver, PropertySource[] source, ClassLoader classLoader) { + this.entityResolver = entityResolver; + this.source = source; + this.classLoader = classLoader; + } + + @Override + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + publicId = replace(publicId); + systemId = replace(systemId); + return entityResolver.resolveEntity(publicId, systemId); + } + + protected String replace(String input) { + try { + return IntrospectionUtils.replaceProperties(input, null, source, classLoader); + } catch (Exception e) { + return input; + } + } + } + + + private static class EntityResolver2Wrapper extends EntityResolverWrapper implements EntityResolver2 { + + private final EntityResolver2 entityResolver2; + + EntityResolver2Wrapper(EntityResolver2 entityResolver, PropertySource[] source, + ClassLoader classLoader) { + super(entityResolver, source, classLoader); + this.entityResolver2 = entityResolver; + } + + @Override + public InputSource getExternalSubset(String name, String baseURI) + throws SAXException, IOException { + name = replace(name); + baseURI = replace(baseURI); + return entityResolver2.getExternalSubset(name, baseURI); + } + + @Override + public InputSource resolveEntity(String name, String publicId, String baseURI, + String systemId) throws SAXException, IOException { + name = replace(name); + publicId = replace(publicId); + baseURI = replace(baseURI); + systemId = replace(systemId); + return entityResolver2.resolveEntity(name, publicId, baseURI, systemId); + } + } +} diff --git a/java/org/apache/tomcat/util/digester/DocumentProperties.java b/java/org/apache/tomcat/util/digester/DocumentProperties.java new file mode 100644 index 0000000..c688ee6 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/DocumentProperties.java @@ -0,0 +1,33 @@ +/* + * 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.tomcat.util.digester; + +/** + * A collection of interfaces, one per property, that enables the object being + * populated by the digester to signal to the digester that it supports the + * given property and that the digester should populate that property if + * available. + */ +public interface DocumentProperties { + + /** + * The character encoding used by the source XML document. + */ + interface Charset { + void setCharset(java.nio.charset.Charset charset); + } +} diff --git a/java/org/apache/tomcat/util/digester/EnvironmentPropertySource.java b/java/org/apache/tomcat/util/digester/EnvironmentPropertySource.java new file mode 100644 index 0000000..6b4138c --- /dev/null +++ b/java/org/apache/tomcat/util/digester/EnvironmentPropertySource.java @@ -0,0 +1,78 @@ +/* + * 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.tomcat.util.digester; + +import java.security.Permission; + +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.security.PermissionCheck; + +/** + * A {@link org.apache.tomcat.util.IntrospectionUtils.SecurePropertySource} + * that uses environment variables to resolve expressions. + * + *

    Usage example:

    + * + * Configure the certificate with environment variables. + * + *
    + *   {@code
    + *     
    + *           
    + *      }
    + * 
    + * + * How to configure: + *
    + * {@code
    + *   echo "org.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource" >> conf/catalina.properties}
    + * 
    + * or add this to {@code CATALINA_OPTS} + * + *
    + * {@code
    + *   -Dorg.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource}
    + * 
    + * + * NOTE: When configured the PropertySource for resolving expressions + * from system properties is still active. + * + * @see Digester + * + * @see Tomcat Configuration Reference System Properties + */ +public class EnvironmentPropertySource implements IntrospectionUtils.SecurePropertySource { + + @Override + public String getProperty(String key) { + return null; + } + + @Override + public String getProperty(String key, ClassLoader classLoader) { + if (classLoader instanceof PermissionCheck) { + Permission p = new RuntimePermission("getenv." + key, null); + if (!((PermissionCheck) classLoader).check(p)) { + return null; + } + } + return System.getenv(key); + } +} diff --git a/java/org/apache/tomcat/util/digester/FactoryCreateRule.java b/java/org/apache/tomcat/util/digester/FactoryCreateRule.java new file mode 100644 index 0000000..91f7349 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/FactoryCreateRule.java @@ -0,0 +1,177 @@ +/* + * 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.tomcat.util.digester; + +import org.xml.sax.Attributes; + + +/** + *

    Rule implementation that uses an {@link ObjectCreationFactory} to create + * a new object which it pushes onto the object stack. When the element is + * complete, the object will be popped.

    + * + *

    This rule is intended in situations where the element's attributes are + * needed before the object can be created. A common scenario is for the + * ObjectCreationFactory implementation to use the attributes as parameters + * in a call to either a factory method or to a non-empty constructor. + */ + +public class FactoryCreateRule extends Rule { + + // ----------------------------------------------------------- Fields + + /** Should exceptions thrown by the factory be ignored? */ + private boolean ignoreCreateExceptions; + /** Stock to manage */ + private ArrayStack exceptionIgnoredStack; + + + // ----------------------------------------------------------- Constructors + + /** + * Construct a factory create rule using the given, already instantiated, + * {@link ObjectCreationFactory}. + * + * @param creationFactory called on to create the object. + * @param ignoreCreateExceptions if true, exceptions thrown by the object + * creation factory will be ignored. + */ + public FactoryCreateRule( + ObjectCreationFactory creationFactory, + boolean ignoreCreateExceptions) { + + this.creationFactory = creationFactory; + this.ignoreCreateExceptions = ignoreCreateExceptions; + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The object creation factory we will use to instantiate objects + * as required based on the attributes specified in the matched XML + * element. + */ + protected ObjectCreationFactory creationFactory = null; + + + // --------------------------------------------------------- Public Methods + + + /** + * Process the beginning of this element. + * + * @param attributes The attribute list of this element + */ + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + + if (ignoreCreateExceptions) { + + if (exceptionIgnoredStack == null) { + exceptionIgnoredStack = new ArrayStack<>(); + } + + try { + Object instance = creationFactory.createObject(attributes); + + if (digester.log.isTraceEnabled()) { + digester.log.trace("[FactoryCreateRule]{" + digester.match + + "} New " + instance.getClass().getName()); + } + digester.push(instance); + exceptionIgnoredStack.push(Boolean.FALSE); + + } catch (Exception e) { + // log message and error + if (digester.log.isDebugEnabled()) { + digester.log.debug(sm.getString("rule.createError", + ((e.getMessage() == null) ? e.getClass().getName() : e.getMessage())), e); + } else if (digester.log.isInfoEnabled()) { + digester.log.info(sm.getString("rule.createError", + ((e.getMessage() == null) ? e.getClass().getName() : e.getMessage()))); + } + exceptionIgnoredStack.push(Boolean.TRUE); + } + + } else { + Object instance = creationFactory.createObject(attributes); + + if (digester.log.isTraceEnabled()) { + digester.log.trace("[FactoryCreateRule]{" + digester.match + + "} New " + instance.getClass().getName()); + } + digester.push(instance); + } + } + + + /** + * Process the end of this element. + */ + @Override + public void end(String namespace, String name) throws Exception { + + // check if object was created + // this only happens if an exception was thrown and we're ignoring them + if ( + ignoreCreateExceptions && + exceptionIgnoredStack != null && + !(exceptionIgnoredStack.empty())) { + + if ((exceptionIgnoredStack.pop()).booleanValue()) { + // creation exception was ignored + // nothing was put onto the stack + if (digester.log.isTraceEnabled()) { + digester.log.trace("[FactoryCreateRule] No creation so no push so no pop"); + } + return; + } + } + + Object top = digester.pop(); + if (digester.log.isTraceEnabled()) { + digester.log.trace("[FactoryCreateRule]{" + digester.match + + "} Pop " + top.getClass().getName()); + } + + } + + + /** + * Clean up after parsing is complete. + */ + @Override + public void finish() throws Exception { + // NO-OP + } + + + /** + * Render a printable version of this Rule. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("FactoryCreateRule["); + if (creationFactory != null) { + sb.append("creationFactory="); + sb.append(creationFactory); + } + sb.append(']'); + return sb.toString(); + } +} diff --git a/java/org/apache/tomcat/util/digester/LocalStrings.properties b/java/org/apache/tomcat/util/digester/LocalStrings.properties new file mode 100644 index 0000000..f833ab8 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/LocalStrings.properties @@ -0,0 +1,37 @@ +# 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. + +digester.createParserError=Error creating SAX parser +digester.emptyStack=Empty stack, returning null +digester.emptyStackError=Empty stack +digester.encodingInvalid=The encoding [{0}] is not recognized by the JRE and will be ignored +digester.error.begin=Begin event threw exception +digester.error.body=Body event threw exception +digester.error.end=End event threw exception +digester.error.finish=Finish event threw exception +digester.errorLocation=Error at line [{0}] column [{1}]: [{2}] +digester.failedToUpdateAttributes=Attribute [{0}] failed to update and remains [{1}] +digester.failedToUpdateSystemProperty=System property [{0}] failed to update and remains [{1}] +digester.invalidURI=Invalid URI [{0}] or [{1}] +digester.noLocator=No Locator +digester.noRulesFound=No rules found matching [{0}] +digester.parseError=Parse error at line [{0}] column [{1}] +digester.parseErrorFatal=Parse fatal error at line [{0}] column [{1}] +digester.parseWarning=Parse warning at line [{0}] column [{1}] +digester.propertySourceLoadError=Error loading property source [{0}] + +rule.createError=Error creating object: [{0}] +rule.noClassName=No class name specified for [{0}] [{1}] +rule.noProperty=Match [{0}] failed to set property [{1}] to [{2}] diff --git a/java/org/apache/tomcat/util/digester/LocalStrings_de.properties b/java/org/apache/tomcat/util/digester/LocalStrings_de.properties new file mode 100644 index 0000000..98ccde8 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +digester.failedToUpdateAttributes=Attribut [{0}] konnte nicht angepasst werden und bleibt auf dem Wert [{1}] diff --git a/java/org/apache/tomcat/util/digester/LocalStrings_es.properties b/java/org/apache/tomcat/util/digester/LocalStrings_es.properties new file mode 100644 index 0000000..ec7aedb --- /dev/null +++ b/java/org/apache/tomcat/util/digester/LocalStrings_es.properties @@ -0,0 +1,17 @@ +# 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. + +digester.encodingInvalid=La codificación [{0}] no es reconocida por JRE y será ignorada +digester.failedToUpdateAttributes=El tribunto [{0}] falló la actualización y permanece [{1}]\n diff --git a/java/org/apache/tomcat/util/digester/LocalStrings_fr.properties b/java/org/apache/tomcat/util/digester/LocalStrings_fr.properties new file mode 100644 index 0000000..fbd8c4a --- /dev/null +++ b/java/org/apache/tomcat/util/digester/LocalStrings_fr.properties @@ -0,0 +1,37 @@ +# 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. + +digester.createParserError=Erreur lors de la création de l'analyseur SAX +digester.emptyStack=La pile est vide, retourne null +digester.emptyStackError=La pile est vide +digester.encodingInvalid=L''encodage [{0}] n''est pas reconnu par le JRE and sera ignoré +digester.error.begin=L'évènement début a renvoyé une exception +digester.error.body=L'évènement corps a renvoyé une exception +digester.error.end=L'évènement fin a renvoyé une exception +digester.error.finish=L'évènement finalisation a renvoyé une exception +digester.errorLocation=Erreur à la ligne [{0}] colonne [{1}] : [{2}] +digester.failedToUpdateAttributes=L''attribut [{0}] n''a pu être mis à jour et garde la valeur [{1}] +digester.failedToUpdateSystemProperty=La propriété système [{0}] n''a pu être mise à jour et reste [{1}] +digester.invalidURI=URI invalide [{0}] ou [{1}] +digester.noLocator=Pas de localisateur +digester.noRulesFound=Aucun règle correspondant à [{0}] n''a été trouvée +digester.parseError=Erreur lors de l''analyse à la ligne [{0}] colonne [{1}] +digester.parseErrorFatal=Erreur fatale lors de l''analyse à la ligne [{0}] colonne [{1}] +digester.parseWarning=Avertissement lors de l''analyse à la ligne [{0}] colonne [{1}] +digester.propertySourceLoadError=Erreur de chargement de la source de propriétés [{0}] + +rule.createError=Erreur de création de l''objet [{0}] +rule.noClassName=Aucun nom de classe n''a été spécifié pour [{0}] [{1}] +rule.noProperty=La correspondance [{0}] n''a pu fixer la propriété [{1}] à [{2}] diff --git a/java/org/apache/tomcat/util/digester/LocalStrings_ja.properties b/java/org/apache/tomcat/util/digester/LocalStrings_ja.properties new file mode 100644 index 0000000..cfa4c89 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/LocalStrings_ja.properties @@ -0,0 +1,37 @@ +# 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. + +digester.createParserError=SAX パーサー生æˆä¸­ã®ã‚¨ãƒ©ãƒ¼ +digester.emptyStack=空ã®ã‚¹ã‚¿ãƒƒã‚¯ã§ã™ã€‚null ã‚’è¿”å´ã—ã¾ã™ã€‚ +digester.emptyStackError=空ã®ã‚¹ã‚¿ãƒƒã‚¯ +digester.encodingInvalid=エンコーディング [{0}] ã¯JREã«ã‚ˆã£ã¦èªè­˜ã•ã‚Œãªã„ãŸã‚ã€ç„¡è¦–ã•ã‚Œã¾ã™ +digester.error.begin=Begin イベントã§ä¾‹å¤–ã‚’é€å‡ºã—ã¾ã—ãŸã€‚ +digester.error.body=Body イベントã§ä¾‹å¤–ã‚’é€å‡ºã—ã¾ã—ãŸã€‚ +digester.error.end=End イベントã§ä¾‹å¤–ã‚’é€å‡ºã—ã¾ã—ãŸã€‚ +digester.error.finish=Finish イベントã§ä¾‹å¤–ã‚’é€å‡ºã—ã¾ã—ãŸã€‚ +digester.errorLocation=è¡Œç•ªå· [{0}] åˆ—ç•ªå· [{1}] ã§ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ: [{2}] +digester.failedToUpdateAttributes=属性[{0}]ãŒæ›´æ–°ã«å¤±æ•—ã—ã€[{1}]ã«æ®‹ã‚Šã¾ã—ãŸã€‚ +digester.failedToUpdateSystemProperty=システムプロパティ [{0}] ã‚’æ›´æ–°ã§ãã¾ã›ã‚“。元ã®å€¤ [{1}] ã¯ãã®ã¾ã¾ã§ã™ã€‚ +digester.invalidURI=無効㪠URI [{0}] ã¾ãŸã¯ [{1}] +digester.noLocator=ロケータを利用ã§ãã¾ã›ã‚“。 +digester.noRulesFound=[{0}]ã¨ä¸€è‡´ã™ã‚‹ãƒ«ãƒ¼ãƒ«ã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ +digester.parseError=è¡Œ[{0}]ã®åˆ—[{1}]ã®è§£æžã‚¨ãƒ©ãƒ¼ +digester.parseErrorFatal=è¡Œ[{0}]ã®åˆ—[{1}]ã§è‡´å‘½çš„ãªã‚¨ãƒ©ãƒ¼ã‚’解æžã—ã¾ã™ã€‚ +digester.parseWarning=è¡Œç•ªå· [{0}] 列 [{1}] ã®è§£é‡ˆä¸­ã«è­¦å‘ŠãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +digester.propertySourceLoadError=プロパティソースクラス [{0}] ã®èª­ã¿è¾¼ã¿ä¸­ã®ã‚¨ãƒ©ãƒ¼ + +rule.createError=オブジェクト作æˆä¸­ã®ã‚¨ãƒ©ãƒ¼: [{0}] +rule.noClassName=[{0}] [{1}] ã«ã‚¯ãƒ©ã‚¹åãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“。 +rule.noProperty=マッãƒã—ãŸãƒ‘ターン [{0}] ã§ãƒ—ロパティ [{1}] ã« [{2}] を設定ã§ãã¾ã›ã‚“。 diff --git a/java/org/apache/tomcat/util/digester/LocalStrings_ko.properties b/java/org/apache/tomcat/util/digester/LocalStrings_ko.properties new file mode 100644 index 0000000..25265f1 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/LocalStrings_ko.properties @@ -0,0 +1,36 @@ +# 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. + +digester.createParserError=SAX 파서를 ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ +digester.emptyStack=스íƒì´ 비어 있어, ë„ì„ ë°˜í™˜í•©ë‹ˆë‹¤. +digester.emptyStackError=스íƒì´ 비어 있습니다. +digester.encodingInvalid=ì¸ì½”딩 [{0}]ì´(ê°€) JREì— ì˜í•´ ì¸ì‹ë˜ì§€ ì•Šì•„ì„œ ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +digester.error.begin=Begin ì´ë²¤íŠ¸ê°€ 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +digester.error.body=Body ì´ë²¤íŠ¸ê°€ 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +digester.error.end=End ì´ë²¤íŠ¸ê°€ 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +digester.error.finish=Finish ì´ë²¤íŠ¸ê°€ 예외를 ë°œìƒì‹œì¼°ìŠµë‹ˆë‹¤. +digester.errorLocation=[{0}] í–‰ [{1}] ì—´ì—ì„œ 오류 ë°œìƒ: [{2}] +digester.failedToUpdateAttributes=ì†ì„± [{0}]ì„(를) 변경하지 못하여, [{1}](으)ë¡œ 유지합니다. +digester.failedToUpdateSystemProperty=시스템 프로í¼í‹° [{0}]ì„(를) 변경하지 못했으며, 해당 프로í¼í‹°ëŠ” [{1}] 값으로 남습니다. +digester.noLocator=Locatorê°€ 없습니다. +digester.noRulesFound=[{0}]와(ê³¼) 부합하는 ruleë“¤ì„ ì°¾ì„ ìˆ˜ 없습니다. +digester.parseError=[{0}] í–‰ [{1}] ì—´ì—ì„œ 파싱 오류 ë°œìƒ +digester.parseErrorFatal=[{0}] í–‰ [{1}] ì—´ì—ì„œ 심ê°í•œ 파싱 오류 ë°œìƒ +digester.parseWarning=[{0}] í–‰ [{1}] ì—´ì—ì„œ 파싱 오류 경고 +digester.propertySourceLoadError=PropertySource [{0}]ì„(를) 로드하는 중 오류 ë°œìƒ + +rule.createError=ê°ì²´ë¥¼ ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ: [{0}] +rule.noClassName=[{0}] [{1}]ì„(를) 위해 ì§€ì •ëœ í´ëž˜ìŠ¤ ì´ë¦„ì´ ì—†ìŠµë‹ˆë‹¤. +rule.noProperty=Match [{0}]ì´(ê°€) 프로í¼í‹° [{1}]ì„(를) [{2}](으)ë¡œ 설정하지 못했습니다. diff --git a/java/org/apache/tomcat/util/digester/LocalStrings_ru.properties b/java/org/apache/tomcat/util/digester/LocalStrings_ru.properties new file mode 100644 index 0000000..bd00f9a --- /dev/null +++ b/java/org/apache/tomcat/util/digester/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +digester.encodingInvalid=Кодировка [{0}] не раÑпознана JRE и будет проигнорирована diff --git a/java/org/apache/tomcat/util/digester/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/digester/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..c6a6e85 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/LocalStrings_zh_CN.properties @@ -0,0 +1,36 @@ +# 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. + +digester.createParserError=创建SAX分æžå™¨æ—¶å‡ºé”™ +digester.emptyStack=空堆栈,返回空值 +digester.emptyStackError=空堆栈 +digester.encodingInvalid=JRE无法识别 [{0}]ç¼–ç ï¼Œå°†è¢«å¿½ç•¥ +digester.error.begin=开始事件引å‘异常 +digester.error.body=正文事件引å‘异常 +digester.error.end=结æŸäº‹ä»¶å¼•å‘异常 +digester.error.finish=完æˆäº‹ä»¶å¼•å‘异常 +digester.errorLocation=è¡Œ[{0}]列[{1}]处出错:[{2}] +digester.failedToUpdateAttributes=属性[{0}]更新失败,旧数æ®ä¸º[{1}] +digester.failedToUpdateSystemProperty=系统属性[{0}]更新失败,ä»ä¸º[{1}] +digester.noLocator=没有定ä½å™¨ +digester.noRulesFound=找ä¸åˆ°åŒ¹é…的规则[{0}] +digester.parseError=在第[{0}]行第[{1}]列处出现分æžé”™è¯¯ +digester.parseErrorFatal=分æžç¬¬[{0}]行第[{1}]列处的致命错误 +digester.parseWarning=在第[{0}]行第[{1}]列处分æžè­¦å‘Š +digester.propertySourceLoadError=加载属性æº[{0}]时出错 + +rule.createError=创建对象时出错:[{0}] +rule.noClassName=没有为[{0}][{1}]指定类å +rule.noProperty=Match[{0}]无法将属性[{1}]设置为[{2}] diff --git a/java/org/apache/tomcat/util/digester/ObjectCreateRule.java b/java/org/apache/tomcat/util/digester/ObjectCreateRule.java new file mode 100644 index 0000000..748da61 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/ObjectCreateRule.java @@ -0,0 +1,170 @@ +/* + * 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.tomcat.util.digester; + + +import org.xml.sax.Attributes; + + +/** + * Rule implementation that creates a new object and pushes it + * onto the object stack. When the element is complete, the + * object will be popped + */ + +public class ObjectCreateRule extends Rule { + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct an object create rule with the specified class name. + * + * @param className Java class name of the object to be created + */ + public ObjectCreateRule(String className) { + + this(className, null); + + } + + + /** + * Construct an object create rule with the specified class name and an + * optional attribute name containing an override. + * + * @param className Java class name of the object to be created + * @param attributeName Attribute name which, if present, contains an + * override of the class name to create + */ + public ObjectCreateRule(String className, + String attributeName) { + + this.className = className; + this.attributeName = attributeName; + + } + + + // ----------------------------------------------------- Instance Variables + + /** + * The attribute containing an override class name if it is present. + */ + protected String attributeName = null; + + + /** + * The Java class name of the object to be created. + */ + protected String className = null; + + + // --------------------------------------------------------- Public Methods + + + /** + * Process the beginning of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param attributes The attribute list for this element + */ + @Override + public void begin(String namespace, String name, Attributes attributes) + throws Exception { + + String realClassName = getRealClassName(attributes); + + if (realClassName == null) { + throw new NullPointerException(sm.getString("rule.noClassName", namespace, name)); + } + + // Instantiate the new object and push it on the context stack + Class clazz = digester.getClassLoader().loadClass(realClassName); + Object instance = clazz.getConstructor().newInstance(); + digester.push(instance); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(System.lineSeparator()); + code.append(realClassName).append(' ').append(digester.toVariableName(instance)).append(" = new "); + code.append(realClassName).append("();").append(System.lineSeparator()); + } + } + + + /** + * Return the actual class name of the class to be instantiated. + * @param attributes The attribute list for this element + * @return the class name + */ + protected String getRealClassName(Attributes attributes) { + // Identify the name of the class to instantiate + String realClassName = className; + if (attributeName != null) { + String value = attributes.getValue(attributeName); + if (value != null) { + realClassName = value; + } + } + return realClassName; + } + + + /** + * Process the end of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + */ + @Override + public void end(String namespace, String name) throws Exception { + + Object top = digester.pop(); + if (digester.log.isTraceEnabled()) { + digester.log.trace("[ObjectCreateRule]{" + digester.match + + "} Pop " + top.getClass().getName()); + } + + } + + + /** + * Render a printable version of this Rule. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ObjectCreateRule["); + sb.append("className="); + sb.append(className); + sb.append(", attributeName="); + sb.append(attributeName); + sb.append(']'); + return sb.toString(); + } + + +} diff --git a/java/org/apache/tomcat/util/digester/ObjectCreationFactory.java b/java/org/apache/tomcat/util/digester/ObjectCreationFactory.java new file mode 100644 index 0000000..39cbda2 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/ObjectCreationFactory.java @@ -0,0 +1,58 @@ +/* + * 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.tomcat.util.digester; + + +import org.xml.sax.Attributes; + +/** + *

    Interface for use with {@link FactoryCreateRule}. + * The rule calls {@link #createObject} to create an object + * to be pushed onto the Digester stack + * whenever it is matched.

    + * + *

    {@link AbstractObjectCreationFactory} is an abstract + * implementation suitable for creating anonymous + * ObjectCreationFactory implementations. + */ +public interface ObjectCreationFactory { + + /** + * Factory method called by {@link FactoryCreateRule} to supply an + * object based on the element's attributes. + * + * @param attributes the element's attributes + * @return the created object + * @throws Exception any exception thrown will be propagated upwards + */ + Object createObject(Attributes attributes) throws Exception; + + /** + * @return the {@link Digester} that was set by the + * {@link FactoryCreateRule} upon initialization. + */ + Digester getDigester(); + + /** + * Set the {@link Digester} to allow the implementation to do logging, + * classloading based on the digester's classloader, etc. + * + * @param digester parent Digester object + */ + void setDigester(Digester digester); + +} diff --git a/java/org/apache/tomcat/util/digester/Rule.java b/java/org/apache/tomcat/util/digester/Rule.java new file mode 100644 index 0000000..5c77de4 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/Rule.java @@ -0,0 +1,164 @@ +/* + * 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.tomcat.util.digester; + +import org.apache.tomcat.util.res.StringManager; +import org.xml.sax.Attributes; + +/** + * Concrete implementations of this class implement actions to be taken when + * a corresponding nested pattern of XML elements has been matched. + */ +public abstract class Rule { + + protected static final StringManager sm = StringManager.getManager(Rule.class); + + // ----------------------------------------------------------- Constructors + + /** + *

    Base constructor. + * Now the digester will be set when the rule is added.

    + */ + public Rule() {} + + + // ----------------------------------------------------- Instance Variables + + + /** + * The Digester with which this Rule is associated. + */ + protected Digester digester = null; + + + /** + * The namespace URI for which this Rule is relevant, if any. + */ + protected String namespaceURI = null; + + + // ------------------------------------------------------------- Properties + + /** + * Identify the Digester with which this Rule is associated. + * + * @return the Digester with which this Rule is associated. + */ + public Digester getDigester() { + return digester; + } + + + /** + * Set the Digester with which this Rule is + * associated. + * + * @param digester The digester with which to associate this rule + */ + public void setDigester(Digester digester) { + this.digester = digester; + } + + + /** + * Return the namespace URI for which this Rule is relevant, if any. + * + * @return The namespace URI for which this rule is relevant or + * null if none. + */ + public String getNamespaceURI() { + return namespaceURI; + } + + + /** + * Set the namespace URI for which this Rule is relevant, if any. + * + * @param namespaceURI Namespace URI for which this Rule is relevant, + * or null to match independent of namespace. + */ + public void setNamespaceURI(String namespaceURI) { + this.namespaceURI = namespaceURI; + } + + + // --------------------------------------------------------- Public Methods + + /** + * This method is called when the beginning of a matching XML element + * is encountered. The default implementation is a NO-OP. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the + * element has no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + * @param attributes The attribute list of this element + * + * @throws Exception if an error occurs while processing the event + */ + public void begin(String namespace, String name, Attributes attributes) throws Exception { + // NO-OP by default. + } + + + /** + * This method is called when the body of a matching XML element is + * encountered. If the element has no body, this method is not called at + * all. The default implementation is a NO-OP. + * + * @param namespace the namespace URI of the matching element, or an empty + * string if the parser is not namespace aware or the + * element has no namespace + * @param name the local name if the parser is namespace aware, or just the + * element name otherwise + * @param text The text of the body of this element + * + * @throws Exception if an error occurs while processing the event + */ + public void body(String namespace, String name, String text) throws Exception { + // NO-OP by default. + } + + + /** + * This method is called when the end of a matching XML element + * is encountered. The default implementation is a NO-OP. + * + * @param namespace the namespace URI of the matching element, or an empty + * string if the parser is not namespace aware or the + * element has no namespace + * @param name the local name if the parser is namespace aware, or just the + * element name otherwise + * + * @throws Exception if an error occurs while processing the event + */ + public void end(String namespace, String name) throws Exception { + // NO-OP by default. + } + + + /** + * This method is called after all parsing methods have been + * called, to allow Rules to remove temporary data. + * + * @throws Exception if an error occurs while processing the event + */ + public void finish() throws Exception { + // NO-OP by default. + } +} diff --git a/java/org/apache/tomcat/util/digester/RuleSet.java b/java/org/apache/tomcat/util/digester/RuleSet.java new file mode 100644 index 0000000..990dee3 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/RuleSet.java @@ -0,0 +1,47 @@ +/* + * 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.tomcat.util.digester; + +/** + *

    Public interface defining a shorthand means of configuring a complete + * set of related Rule definitions, possibly associated with + * a particular namespace URI, in one operation. To use an instance of a + * class that implements this interface:

    + *
      + *
    • Create a concrete implementation of this interface.
    • + *
    • Optionally, you can configure a RuleSet to be relevant + * only for a particular namespace URI by configuring the value to be + * returned by getNamespaceURI().
    • + *
    • As you are configuring your Digester instance, call + * digester.addRuleSet() and pass the RuleSet instance.
    • + *
    • Digester will call the addRuleInstances() method of + * your RuleSet to configure the necessary rules.
    • + *
    + */ +public interface RuleSet { + + /** + * Add the set of Rule instances defined in this RuleSet to the + * specified Digester instance, associating them with + * our namespace URI (if any). This method should only be called + * by a Digester instance. + * + * @param digester Digester instance to which the new Rule instances + * should be added. + */ + void addRuleInstances(Digester digester); +} diff --git a/java/org/apache/tomcat/util/digester/Rules.java b/java/org/apache/tomcat/util/digester/Rules.java new file mode 100644 index 0000000..d22a365 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/Rules.java @@ -0,0 +1,87 @@ +/* + * 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.tomcat.util.digester; + +import java.util.List; + +/** + * Public interface defining a collection of Rule instances (and corresponding + * matching patterns) plus an implementation of a matching policy that selects + * the rules that match a particular pattern of nested elements discovered + * during parsing. + */ +public interface Rules { + + // ------------------------------------------------------------- Properties + + /** + * @return the Digester instance with which this Rules instance is + * associated. + */ + Digester getDigester(); + + + /** + * Set the Digester instance with which this Rules instance is associated. + * + * @param digester The newly associated Digester instance + */ + void setDigester(Digester digester); + + + // --------------------------------------------------------- Public Methods + + /** + * Register a new Rule instance matching the specified pattern. + * + * @param pattern Nesting pattern to be matched for this Rule + * @param rule Rule instance to be registered + */ + void add(String pattern, Rule rule); + + + /** + * Clear all existing Rule instance registrations. + */ + void clear(); + + + /** + * Return a List of all registered Rule instances that match the specified + * nesting pattern, or a zero-length List if there are no matches. If more + * than one Rule instance matches, they must be returned + * in the order originally registered through the add() + * method. + * + * @param namespaceURI Namespace URI for which to select matching rules, + * or null to match regardless of namespace URI + * @param pattern Nesting pattern to be matched + * @return a rules list + */ + List match(String namespaceURI, String pattern); + + + /** + * Return a List of all registered Rule instances, or a zero-length List + * if there are no registered Rule instances. If more than one Rule + * instance has been registered, they must be returned + * in the order originally registered through the add() + * method. + * @return a rules list + */ + List rules(); +} diff --git a/java/org/apache/tomcat/util/digester/RulesBase.java b/java/org/apache/tomcat/util/digester/RulesBase.java new file mode 100644 index 0000000..be622f0 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/RulesBase.java @@ -0,0 +1,208 @@ +/* + * 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.tomcat.util.digester; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + *

    Default implementation of the Rules interface that supports + * the standard rule matching behavior. This class can also be used as a + * base class for specialized Rules implementations.

    + * + *

    The matching policies implemented by this class support two different + * types of pattern matching rules:

    + *
      + *
    • Exact Match - A pattern "a/b/c" exactly matches a + * <c> element, nested inside a <b> + * element, which is nested inside an <a> element.
    • + *
    • Tail Match - A pattern "*/a/b" matches a + * <b> element, nested inside an <a> + * element, no matter how deeply the pair is nested.
    • + *
    + */ +public class RulesBase implements Rules { + + // ----------------------------------------------------- Instance Variables + + /** + * The set of registered Rule instances, keyed by the matching pattern. + * Each value is a List containing the Rules for that pattern, in the + * order that they were originally registered. + */ + protected HashMap> cache = new HashMap<>(); + + + /** + * The Digester instance with which this Rules instance is associated. + */ + protected Digester digester = null; + + + /** + * The set of registered Rule instances, in the order that they were + * originally registered. + */ + protected ArrayList rules = new ArrayList<>(); + + + // ------------------------------------------------------------- Properties + + /** + * Return the Digester instance with which this Rules instance is + * associated. + */ + @Override + public Digester getDigester() { + return this.digester; + } + + + /** + * Set the Digester instance with which this Rules instance is associated. + * + * @param digester The newly associated Digester instance + */ + @Override + public void setDigester(Digester digester) { + this.digester = digester; + for (Rule item : rules) { + item.setDigester(digester); + } + } + + + // --------------------------------------------------------- Public Methods + + /** + * Register a new Rule instance matching the specified pattern. + * + * @param pattern Nesting pattern to be matched for this Rule + * @param rule Rule instance to be registered + */ + @Override + public void add(String pattern, Rule rule) { + // to help users who accidentally add '/' to the end of their patterns + int patternLength = pattern.length(); + if (patternLength>1 && pattern.endsWith("/")) { + pattern = pattern.substring(0, patternLength-1); + } + + cache.computeIfAbsent(pattern, k -> new ArrayList<>()).add(rule); + rules.add(rule); + if (this.digester != null) { + rule.setDigester(this.digester); + } + } + + + /** + * Clear all existing Rule instance registrations. + */ + @Override + public void clear() { + cache.clear(); + rules.clear(); + } + + + /** + * Return a List of all registered Rule instances that match the specified + * nesting pattern, or a zero-length List if there are no matches. If more + * than one Rule instance matches, they must be returned + * in the order originally registered through the add() + * method. + * + * @param namespaceURI Namespace URI for which to select matching rules, + * or null to match regardless of namespace URI + * @param pattern Nesting pattern to be matched + */ + @Override + public List match(String namespaceURI, String pattern) { + + // List rulesList = (List) this.cache.get(pattern); + List rulesList = lookup(namespaceURI, pattern); + if ((rulesList == null) || (rulesList.size() < 1)) { + // Find the longest key, ie more discriminant + String longKey = ""; + for (String key : this.cache.keySet()) { + if (key.startsWith("*/")) { + if (pattern.equals(key.substring(2)) || + pattern.endsWith(key.substring(1))) { + if (key.length() > longKey.length()) { + // rulesList = (List) this.cache.get(key); + rulesList = lookup(namespaceURI, key); + longKey = key; + } + } + } + } + } + if (rulesList == null) { + rulesList = new ArrayList<>(); + } + return rulesList; + } + + + /** + * Return a List of all registered Rule instances, or a zero-length List + * if there are no registered Rule instances. If more than one Rule + * instance has been registered, they must be returned + * in the order originally registered through the add() + * method. + */ + @Override + public List rules() { + return this.rules; + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Return a List of Rule instances for the specified pattern that also + * match the specified namespace URI (if any). If there are no such + * rules, return null. + * + * @param namespaceURI Namespace URI to match, or null to + * select matching rules regardless of namespace URI + * @param pattern Pattern to be matched + * @return a rules list + */ + protected List lookup(String namespaceURI, String pattern) { + // Optimize when no namespace URI is specified + List list = this.cache.get(pattern); + if (list == null) { + return null; + } + if (namespaceURI == null || namespaceURI.length() == 0) { + return list; + } + + // Select only Rules that match on the specified namespace URI + List results = new ArrayList<>(); + for (Rule item : list) { + if ((namespaceURI.equals(item.getNamespaceURI())) || + (item.getNamespaceURI() == null)) { + results.add(item); + } + } + return results; + } +} diff --git a/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java b/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java new file mode 100644 index 0000000..997ce35 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java @@ -0,0 +1,157 @@ +/* + * 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.tomcat.util.digester; + +import java.io.FilePermission; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.Permission; + +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.security.PermissionCheck; + +/** + * A {@link org.apache.tomcat.util.IntrospectionUtils.SecurePropertySource} + * that uses Kubernetes service bindings to resolve expressions. + * + *

    Usage example:

    + * + * Configure the certificate with a service binding. + * + * When the service binding is constructed as follows: + * + *
    + *    $SERVICE_BINDING_ROOT/
    + *                         /custom-certificate/
    + *                                            /keyFile
    + *                                            /file
    + *                                            /chainFile
    + *                                            /keyPassword
    + * 
    + *
    + *   {@code
    + *     
    + *           
    + *      }
    + * 
    + * + *

    + * The optional chomp: prefix will cause the ServiceBindingPropertySource + * to trim a single newline (\r\n, \r, or \n) + * from the end of the file, if it exists. This is a convenience for hand-edited + * files/values where removing a trailing newline is difficult, and trailing + * whitespace changes the meaning of the value. + *

    + * + * How to configure: + *
    + * {@code
    + *   echo "org.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.ServiceBindingPropertySource" >> conf/catalina.properties}
    + * 
    + * or add this to {@code CATALINA_OPTS} + * + *
    + * {@code
    + *   -Dorg.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.ServiceBindingPropertySource}
    + * 
    + * + * NOTE: When configured the PropertySource for resolving expressions + * from system properties is still active. + * + * @see Digester + * + * @see Tomcat + * Configuration Reference System Properties + */ +public class ServiceBindingPropertySource implements IntrospectionUtils.SecurePropertySource { + + private static final String SERVICE_BINDING_ROOT_ENV_VAR = "SERVICE_BINDING_ROOT"; + + @Override + public String getProperty(String key) { + return null; + } + + @Override + public String getProperty(String key, ClassLoader classLoader) { + // can we determine the service binding root + if (classLoader instanceof PermissionCheck) { + Permission p = new RuntimePermission("getenv." + SERVICE_BINDING_ROOT_ENV_VAR, null); + if (!((PermissionCheck) classLoader).check(p)) { + return null; + } + } + + // get the root to search from + String serviceBindingRoot = System.getenv(SERVICE_BINDING_ROOT_ENV_VAR); + if (serviceBindingRoot == null) { + return null; + } + + boolean chomp = false; + if (key.startsWith("chomp:")) { + chomp = true; + key = key.substring(6); // Remove the "chomp:" prefix + } + + // we expect the keys to be in the format $SERVICE_BINDING_ROOT// + String[] parts = key.split("\\."); + if (parts.length != 2) { + return null; + } + + Path path = Paths.get(serviceBindingRoot, parts[0], parts[1]); + + if (!path.toFile().exists()) { + return null; + } + + try { + if (classLoader instanceof PermissionCheck) { + Permission p = new FilePermission(path.toString(), "read"); + if (!((PermissionCheck) classLoader).check(p)) { + return null; + } + } + + byte[] bytes = Files.readAllBytes(path); + + int length = bytes.length; + + if (chomp) { + if(length > 1 && bytes[length - 2] == '\r' && bytes[length - 1] == '\n') { + length -= 2; + } else if (length > 0) { + byte c = bytes[length - 1]; + if (c == '\r' || c == '\n') { + length -= 1; + } + } + } + + return new String(bytes, 0, length); + } catch (IOException e) { + return null; + } + } +} diff --git a/java/org/apache/tomcat/util/digester/SetNextRule.java b/java/org/apache/tomcat/util/digester/SetNextRule.java new file mode 100644 index 0000000..5ed5867 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/SetNextRule.java @@ -0,0 +1,170 @@ +/* + * 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.tomcat.util.digester; + +import org.apache.tomcat.util.IntrospectionUtils; + + +/** + *

    Rule implementation that calls a method on the (top-1) (parent) + * object, passing the top object (child) as an argument. It is + * commonly used to establish parent-child relationships.

    + * + *

    This rule now supports more flexible method matching by default. + * It is possible that this may break (some) code + * written against release 1.1.1 or earlier. + * See {@link #isExactMatch()} for more details.

    + */ + +public class SetNextRule extends Rule { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a "set next" rule with the specified method name. + * + * @param methodName Method name of the parent method to call + * @param paramType Java class of the parent method's argument + * (if you wish to use a primitive type, specify the corresponding + * Java wrapper class instead, such as java.lang.Boolean + * for a boolean parameter) + */ + public SetNextRule(String methodName, + String paramType) { + + this.methodName = methodName; + this.paramType = paramType; + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The method name to call on the parent object. + */ + protected String methodName = null; + + + /** + * The Java class name of the parameter type expected by the method. + */ + protected String paramType = null; + + /** + * Should we use exact matching. Default is no. + */ + protected boolean useExactMatch = false; + + // --------------------------------------------------------- Public Methods + + + /** + *

    Is exact matching being used.

    + * + *

    This rule uses org.apache.commons.beanutils.MethodUtils + * to introspect the relevant objects so that the right method can be called. + * Originally, MethodUtils.invokeExactMethod was used. + * This matches methods very strictly + * and so may not find a matching method when one exists. + * This is still the behaviour when exact matching is enabled.

    + * + *

    When exact matching is disabled, MethodUtils.invokeMethod is used. + * This method finds more methods but is less precise when there are several methods + * with correct signatures. + * So, if you want to choose an exact signature you might need to enable this property.

    + * + *

    The default setting is to disable exact matches.

    + * + * @return true iff exact matching is enabled + * @since Digester Release 1.1.1 + */ + public boolean isExactMatch() { + + return useExactMatch; + } + + /** + *

    Set whether exact matching is enabled.

    + * + *

    See {@link #isExactMatch()}.

    + * + * @param useExactMatch should this rule use exact method matching + * @since Digester Release 1.1.1 + */ + public void setExactMatch(boolean useExactMatch) { + + this.useExactMatch = useExactMatch; + } + + /** + * Process the end of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param name the local name if the parser is namespace aware, or just + * the element name otherwise + */ + @Override + public void end(String namespace, String name) throws Exception { + + // Identify the objects to be used + Object child = digester.peek(0); + Object parent = digester.peek(1); + if (digester.log.isTraceEnabled()) { + if (parent == null) { + digester.log.trace("[SetNextRule]{" + digester.match + + "} Call [NULL PARENT]." + + methodName + "(" + child + ")"); + } else { + digester.log.trace("[SetNextRule]{" + digester.match + + "} Call " + parent.getClass().getName() + "." + + methodName + "(" + child + ")"); + } + } + + // Call the specified method + IntrospectionUtils.callMethod1(parent, methodName, + child, paramType, digester.getClassLoader()); + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(digester.toVariableName(parent)).append('.'); + code.append(methodName).append('(').append(digester.toVariableName(child)).append(");"); + code.append(System.lineSeparator()); + } + } + + + /** + * Render a printable version of this Rule. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SetNextRule["); + sb.append("methodName="); + sb.append(methodName); + sb.append(", paramType="); + sb.append(paramType); + sb.append(']'); + return sb.toString(); + } + + +} diff --git a/java/org/apache/tomcat/util/digester/SetPropertiesRule.java b/java/org/apache/tomcat/util/digester/SetPropertiesRule.java new file mode 100644 index 0000000..5c2c6e6 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/SetPropertiesRule.java @@ -0,0 +1,128 @@ +/* + * 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.tomcat.util.digester; + + +import java.util.HashMap; + +import org.apache.tomcat.util.IntrospectionUtils; +import org.xml.sax.Attributes; + + +/** + *

    Rule implementation that sets properties on the object at the top of the + * stack, based on attributes with corresponding names.

    + */ + +public class SetPropertiesRule extends Rule { + + public interface Listener { + void endSetPropertiesRule(); + } + + protected final HashMap excludes; + + public SetPropertiesRule() { + excludes = null; + } + + public SetPropertiesRule(String[] exclude) { + excludes = new HashMap<>(); + for (String s : exclude) { + if (s != null) { + this.excludes.put(s, s); + } + } + } + + /** + * Process the beginning of this element. + * + * @param namespace the namespace URI of the matching element, or an + * empty string if the parser is not namespace aware or the element has + * no namespace + * @param theName the local name if the parser is namespace aware, or just + * the element name otherwise + * @param attributes The attribute list for this element + */ + @Override + public void begin(String namespace, String theName, Attributes attributes) + throws Exception { + + // Populate the corresponding properties of the top object + Object top = digester.peek(); + if (digester.log.isTraceEnabled()) { + digester.log.trace("[SetPropertiesRule]{" + digester.match + + "} Set " + top.getClass().getName() + + " properties"); + } + StringBuilder code = digester.getGeneratedCode(); + String variableName = null; + if (code != null) { + variableName = digester.toVariableName(top); + } + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + if (name.isEmpty()) { + name = attributes.getQName(i); + } + String value = attributes.getValue(i); + + if (digester.log.isTraceEnabled()) { + digester.log.trace("[SetPropertiesRule]{" + digester.match + + "} Setting property '" + name + "' to '" + + value + "'"); + } + if (!digester.isFakeAttribute(top, name) && (excludes == null || !excludes.containsKey(name))) { + StringBuilder actualMethod = null; + if (code != null) { + actualMethod = new StringBuilder(); + } + if (!IntrospectionUtils.setProperty(top, name, value, true, actualMethod)) { + if (digester.getRulesValidation() && !"optional".equals(name)) { + digester.log.warn(sm.getString("rule.noProperty", digester.match, name, value)); + } + } else { + if (code != null) { + code.append(variableName).append('.').append(actualMethod).append(';'); + code.append(System.lineSeparator()); + } + } + } + } + + if (top instanceof Listener) { + ((Listener) top).endSetPropertiesRule(); + if (code != null) { + code.append("((org.apache.tomcat.util.digester.SetPropertiesRule.Listener) "); + code.append(variableName).append(").endSetPropertiesRule();"); + code.append(System.lineSeparator()); + } + } + + } + + + /** + * Render a printable version of this Rule. + */ + @Override + public String toString() { + return "SetPropertiesRule[]"; + } +} diff --git a/java/org/apache/tomcat/util/digester/SystemPropertySource.java b/java/org/apache/tomcat/util/digester/SystemPropertySource.java new file mode 100644 index 0000000..49fc765 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/SystemPropertySource.java @@ -0,0 +1,51 @@ +/* + * 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.tomcat.util.digester; + +import java.security.Permission; +import java.util.PropertyPermission; + +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.security.PermissionCheck; + +/** + * A {@link org.apache.tomcat.util.IntrospectionUtils.SecurePropertySource} + * that uses system properties to resolve expressions. + * This property source is always active by default. + * + * @see Digester + */ +public class SystemPropertySource implements IntrospectionUtils.SecurePropertySource { + + @Override + public String getProperty(String key) { + // For backward compatibility + return getProperty(key, null); + } + + @Override + public String getProperty(String key, ClassLoader classLoader) { + if (classLoader instanceof PermissionCheck) { + Permission p = new PropertyPermission(key, "read"); + if (!((PermissionCheck) classLoader).check(p)) { + return null; + } + } + return System.getProperty(key); + } + +} diff --git a/java/org/apache/tomcat/util/digester/package.html b/java/org/apache/tomcat/util/digester/package.html new file mode 100644 index 0000000..d95fd52 --- /dev/null +++ b/java/org/apache/tomcat/util/digester/package.html @@ -0,0 +1,1188 @@ + + + +Package Documentation for org.apache.commons.digester Package + + +The Digester package provides for rules-based processing of arbitrary +XML documents. +

    + + +

    Introduction

    + +

    In many application environments that deal with XML-formatted data, it is +useful to be able to process an XML document in an "event driven" manner, +where particular Java objects are created (or methods of existing objects +are invoked) when particular patterns of nested XML elements have been +recognized. Developers familiar with the Simple API for XML Parsing (SAX) +approach to processing XML documents will recognize that the Digester provides +a higher level, more developer-friendly interface to SAX events, because most +of the details of navigating the XML element hierarchy are hidden -- allowing +the developer to focus on the processing to be performed.

    + +

    In order to use a Digester, the following basic steps are required:

    +
      +
    • Create a new instance of the + org.apache.commons.digester.Digester class. Previously + created Digester instances may be safely reused, as long as you have + completed any previously requested parse, and you do not try to utilize + a particular Digester instance from more than one thread at a time.
    • +
    • Set any desired configuration properties + that will customize the operation of the Digester when you next initiate + a parse operation.
    • +
    • Optionally, push any desired initial object(s) onto the Digester's + object stack.
    • +
    • Register all of the element matching patterns + for which you wish to have processing rules + fired when this pattern is recognized in an input document. You may + register as many rules as you like for any particular pattern. If there + is more than one rule for a given pattern, the rules will be executed in + the order that they were listed.
    • +
    • Call the digester.parse() method, passing a reference to the + XML document to be parsed in one of a variety of forms. See the + Digester.parse() + documentation for details. Note that you will need to be prepared to + catch any IOException or SAXException that is + thrown by the parser, or any runtime expression that is thrown by one of + the processing rules.
    • +
    + +

    For example code, see the usage +examples, and the FAQ .

    + +

    Digester Configuration Properties

    + +

    A org.apache.commons.digester.Digester instance contains several +configuration properties that can be used to customize its operation. These +properties must be configured before you call one of the +parse() variants, in order for them to take effect on that +parse.

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Digester Configuration Properties
    PropertyDescription
    classLoaderYou can optionally specify the class loader that will be used to + load classes when required by the ObjectCreateRule + and FactoryCreateRule rules. If not specified, + application classes will be loaded from the thread's context + class loader (if the useContextClassLoader property + is set to true) or the same class loader that was + used to load the Digester class itself.
    errorHandlerYou can optionally specify a SAX ErrorHandler that + is notified when parsing errors occur. By default, any parsing + errors that are encountered are logged, but Digester will continue + processing as well.
    namespaceAwareA boolean that is set to true to perform parsing in a + manner that is aware of XML namespaces. Among other things, this + setting affects how elements are matched to processing rules. See + Namespace Aware Parsing for more + information.
    ruleNamespaceURIThe public URI of the namespace for which all subsequently added + rules are associated, or null for adding rules that + are not associated with any namespace. See + Namespace Aware Parsing for more + information.
    rulesThe Rules component that actually performs matching of + Rule instances against the current element nesting + pattern is pluggable. By default, Digester includes a + Rules implementation that behaves as described in this + document. See + Pluggable Rules Processing for + more information.
    useContextClassLoaderA boolean that is set to true if you want application + classes required by FactoryCreateRule and + ObjectCreateRule to be loaded from the context class + loader of the current thread. By default, classes will be loaded + from the class loader that loaded this Digester class. + NOTE - This property is ignored if you set a + value for the classLoader property; that class loader + will be used unconditionally.
    validatingA boolean that is set to true if you wish to validate + the XML document against a Document Type Definition (DTD) that is + specified in its DOCTYPE declaration. The default + value of false requests a parse that only detects + "well formed" XML documents, rather than "valid" ones.
    +
    + +

    In addition to the scalar properties defined above, you can also register +a local copy of a Document Type Definition (DTD) that is referenced in a +DOCTYPE declaration. Such a registration tells the XML parser +that, whenever it encounters a DOCTYPE declaration with the +specified public identifier, it should utilize the actual DTD content at the +registered system identifier (a URL), rather than the one in the +DOCTYPE declaration.

    + +

    For example, the Struts framework controller servlet uses the following +registration in order to tell Struts to use a local copy of the DTD for the +Struts configuration file. This allows usage of Struts in environments that +are not connected to the Internet, and speeds up processing even at Internet +connected sites (because it avoids the need to go across the network).

    + +
    +    URL url = new URL("/org/apache/struts/resources/struts-config_1_0.dtd");
    +    digester.register
    +      ("-//Apache Software Foundation//DTD Struts Configuration 1.0//EN",
    +       url.toString());
    +
    + +

    As a side note, the system identifier used in this example is the path +that would be passed to java.lang.ClassLoader.getResource() +or java.lang.ClassLoader.getResourceAsStream(). The actual DTD +resource is loaded through the same class loader that loads all of the Struts +classes -- typically from the struts.jar file.

    + +

    The Object Stack

    + +

    One very common use of org.apache.commons.digester.Digester +technology is to dynamically construct a tree of Java objects, whose internal +organization, as well as the details of property settings on these objects, +are configured based on the contents of the XML document. In fact, the +primary reason that the Digester package was created (it was originally part +of Struts, and then moved to the Commons project because it was recognized +as being generally useful) was to facilitate the +way that the Struts controller servlet configures itself based on the contents +of your application's struts-config.xml file.

    + +

    To facilitate this usage, the Digester exposes a stack that can be +manipulated by processing rules that are fired when element matching patterns +are satisfied. The usual stack-related operations are made available, +including the following:

    +
      +
    • clear() - Clear the current contents + of the object stack.
    • +
    • peek() - Return a reference to the top + object on the stack, without removing it.
    • +
    • pop() - Remove the top object from the + stack and return it.
    • +
    • push() - Push a new + object onto the top of the stack.
    • +
    + +

    A typical design pattern, then, is to fire a rule that creates a new object +and pushes it on the stack when the beginning of a particular XML element is +encountered. The object will remain there while the nested content of this +element is processed, and it will be popped off when the end of the element +is encountered. As we will see, the standard "object create" processing rule +supports exactly this functionality in a very convenient way.

    + +

    Several potential issues with this design pattern are addressed by other +features of the Digester functionality:

    +
      +
    • How do I relate the objects being created to each other? - The + Digester supports standard processing rules that pass the top object on + the stack as an argument to a named method on the next-to-top object on + the stack (or vice versa). This rule makes it easy to establish + parent-child relationships between these objects. One-to-one and + one-to-many relationships are both easy to construct.
    • +
    • How do I retain a reference to the first object that was created? + As you review the description of what the "object create" processing rule + does, it would appear that the first object you create (i.e. the object + created by the outermost XML element you process) will disappear from the + stack by the time that XML parsing is completed, because the end of the + element would have been encountered. However, Digester will maintain a + reference to the very first object ever pushed onto the object stack, + and will return it to you + as the return value from the parse() call. Alternatively, + you can push a reference to some application object onto the stack before + calling parse(), and arrange that a parent-child relationship + be created (by appropriate processing rules) between this manually pushed + object and the ones that are dynamically created. In this way, + the pushed object will retain a reference to the dynamically created objects + (and therefore all of their children), and will be returned to you after + the parse finishes as well.
    • +
    + +

    Element Matching Patterns

    + +

    A primary feature of the org.apache.commons.digester.Digester +parser is that the Digester automatically navigates the element hierarchy of +the XML document you are parsing for you, without requiring any developer +attention to this process. Instead, you focus on deciding what functions you +would like to have performed whenever a certain arrangement of nested elements +is encountered in the XML document being parsed. The mechanism for specifying +such arrangements are called element matching patterns. + +

    A very simple element matching pattern is a simple string like "a". This +pattern is matched whenever an <a> top-level element is +encountered in the XML document, no matter how many times it occurs. Note that +nested <a> elements will not match this +pattern -- we will describe means to support this kind of matching later.

    + +

    The next step up in matching pattern complexity is "a/b". This pattern will +be matched when a <b> element is found nested inside a +top-level <a> element. Again, this match can occur as many +times as desired, depending on the content of the XML document being parsed. +You can use multiple slashes to define a hierarchy of any desired depth that +will be matched appropriately.

    + +

    For example, assume you have registered processing rules that match patterns +"a", "a/b", and "a/b/c". For an input XML document with the following +contents, the indicated patterns will be matched when the corresponding element +is parsed:

    +
    +  <a>         -- Matches pattern "a"
    +    <b>       -- Matches pattern "a/b"
    +      <c/>    -- Matches pattern "a/b/c"
    +      <c/>    -- Matches pattern "a/b/c"
    +    </b>
    +    <b>       -- Matches pattern "a/b"
    +      <c/>    -- Matches pattern "a/b/c"
    +      <c/>    -- Matches pattern "a/b/c"
    +      <c/>    -- Matches pattern "a/b/c"
    +    </b>
    +  </a>
    +
    + +

    It is also possible to match a particular XML element, no matter how it is +nested (or not nested) in the XML document, by using the "*" wildcard character +in your matching pattern strings. For example, an element matching pattern +of "*/a" will match an <a> element at any nesting position +within the document.

    + +

    It is quite possible that, when a particular XML element is being parsed, +the pattern for more than one registered processing rule will be matched + either because you registered more than one processing rule with the same +matching pattern, or because one more more exact pattern matches and wildcard +pattern matches are satisfied by the same element.

    + +

    When this occurs, the corresponding processing rules will all be fired in order. +begin (and body) method calls are executed in the +order that the Rules where initially registered with the +Digester, whilst end method calls are execute in +reverse order. In other words - the order is first in, last out.

    + +

    Processing Rules

    + +

    The previous section documented how you identify +when you wish to have certain actions take place. The purpose +of processing rules is to define what should happen when the +patterns are matched.

    + +

    Formally, a processing rule is a Java class that subclasses the +org.apache.commons.digester.Rule interface. Each Rule +implements one or more of the following event methods that are called at +well-defined times when the matching patterns corresponding to this rule +trigger it:

    +
      +
    • begin() - + Called when the beginning of the matched XML element is encountered. A + data structure containing all of the attributes corresponding to this + element are passed as well.
    • +
    • body() - + Called when nested content (that is not itself XML elements) of the + matched element is encountered. Any leading or trailing whitespace will + have been removed as part of the parsing process.
    • +
    • end() - Called when the ending of the matched + XML element is encountered. If nested XML elements that matched other + processing rules was included in the body of this element, the appropriate + processing rules for the matched rules will have already been completed + before this method is called.
    • +
    • finish() - Called when the parse has + been completed, to give each rule a chance to clean up any temporary data + they might have created and cached.
    • +
    + +

    As you are configuring your digester, you can call the +addRule() method to register a specific element matching pattern, +along with an instance of a Rule class that will have its event +handling methods called at the appropriate times, as described above. This +mechanism allows you to create Rule implementation classes +dynamically, to implement any desired application specific functionality.

    + +

    In addition, a set of processing rule implementation classes are provided, +which deal with many common programming scenarios. These classes include the +following:

    +
      +
    • ObjectCreateRule - When the + begin() method is called, this rule instantiates a new + instance of a specified Java class, and pushes it on the stack. The + class name to be used is defaulted according to a parameter passed to + this rule's constructor, but can optionally be overridden by a classname + passed via the specified attribute to the XML element being processed. + When the end() method is called, the top object on the stack + (presumably, the one we added in the begin() method) will + be popped, and any reference to it (within the Digester) will be + discarded.
    • +
    • FactoryCreateRule - A variation of + ObjectCreateRule that is useful when the Java class with + which you wish to create an object instance does not have a no-arguments + constructor, or where you wish to perform other setup processing before + the object is handed over to the Digester.
    • +
    • SetPropertiesRule - When the + begin() method is called, the digester uses the standard + Java Reflection API to identify any JavaBeans property setter methods + (on the object at the top of the digester's stack) + who have property names that match the attributes specified on this XML + element, and then call them individually, passing the corresponding + attribute values. These natural mappings can be overridden. This allows + (for example) a class attribute to be mapped correctly. + It is recommended that this feature should not be overused - in most cases, + it's better to use the standard BeanInfo mechanism. + A very common idiom is to define an object create + rule, followed by a set properties rule, with the same element matching + pattern. This causes the creation of a new Java object, followed by + "configuration" of that object's properties based on the attributes + of the same XML element that created this object.
    • +
    • SetNextRule - When the + end() method is called, the digester analyzes the + next-to-top element on the stack, looking for a property setter method + for a specified property. It then calls this method, passing the object + at the top of the stack as an argument. This rule is commonly used to + establish one-to-many relationships between the two objects, with the + method name commonly being something like "addChild".
    • +
    • CallMethodRule - This rule sets up a + method call to a named method of the top object on the digester's stack, + which will actually take place when the end() method is + called. You configure this rule by specifying the name of the method + to be called, the number of arguments it takes, and (optionally) the + Java class name(s) defining the type(s) of the method's arguments. + The actual parameter values, if any, will typically be accumulated from + the body content of nested elements within the element that triggered + this rule, using the CallParamRule discussed next.
    • +
    • CallParamRule - This rule identifies + the source of a particular numbered (zero-relative) parameter for a + CallMethodRule within which we are nested. You can specify that the + parameter value be taken from a particular named attribute, or from the + nested body content of this element.
    • +
    + +

    You can create instances of the standard Rule classes and +register them by calling digester.addRule(), as described above. +However, because their usage is so common, shorthand registration methods are +defined for each of the standard rules, directly on the Digester +class. For example, the following code sequence:

    +
    +    Rule rule = new SetNextRule(digester, "addChild",
    +                                "com.mycompany.mypackage.MyChildClass");
    +    digester.addRule("a/b/c", rule);
    +
    +

    can be replaced by:

    +
    +    digester.addSetNext("a/b/c", "addChild",
    +                        "com.mycompany.mypackage.MyChildClass");
    +
    + +

    Logging

    + +

    Logging is a vital tool for debugging Digester rulesets. Digester can log +copious amounts of debugging information. So, you need to know how logging +works before you start using Digester seriously.

    + +

    Two main logs are used by Digester:

    +
      +
    • SAX-related messages are logged to + org.apache.commons.digester.Digester.sax. + This log gives information about the basic SAX events received by + Digester.
    • +
    • org.apache.commons.digester.Digester is used + for everything else. You'll probably want to have this log turned up during + debugging but turned down during production due to the high message + volume.
    • +
    + +

    Usage Examples

    + + +

    Creating a Simple Object Tree

    + +

    Let's assume that you have two simple JavaBeans, Foo and +Bar, with the following method signatures:

    +
    +  package mypackage;
    +  public class Foo {
    +    public void addBar(Bar bar);
    +    public Bar findBar(int id);
    +    public Iterator getBars();
    +    public String getName();
    +    public void setName(String name);
    +  }
    +
    +  public mypackage;
    +  public class Bar {
    +    public int getId();
    +    public void setId(int id);
    +    public String getTitle();
    +    public void setTitle(String title);
    +  }
    +
    + +

    and you wish to use Digester to parse the following XML document:

    + +
    +  <foo name="The Parent">
    +    <bar id="123" title="The First Child"/>
    +    <bar id="456" title="The Second Child"/>
    +  </foo>
    +
    + +

    A simple approach will be to use the following Digester in the following way +to set up the parsing rules, and then process an input file containing this +document:

    + +
    +  Digester digester = new Digester();
    +  digester.setValidating(false);
    +  digester.addObjectCreate("foo", "mypackage.Foo");
    +  digester.addSetProperties("foo");
    +  digester.addObjectCreate("foo/bar", "mypackage.Bar");
    +  digester.addSetProperties("foo/bar");
    +  digester.addSetNext("foo/bar", "addBar", "mypackage.Bar");
    +  Foo foo = (Foo) digester.parse();
    +
    + +

    In order, these rules do the following tasks:

    +
      +
    1. When the outermost <foo> element is encountered, + create a new instance of mypackage.Foo and push it + on to the object stack. At the end of the <foo> + element, this object will be popped off of the stack.
    2. +
    3. Cause properties of the top object on the stack (i.e. the Foo + object that was just created and pushed) to be set based on the values + of the attributes of this XML element.
    4. +
    5. When a nested <bar> element is encountered, + create a new instance of mypackage.Bar and push it + on to the object stack. At the end of the <bar> + element, this object will be popped off of the stack (i.e. after the + remaining rules matching foo/bar are processed).
    6. +
    7. Cause properties of the top object on the stack (i.e. the Bar + object that was just created and pushed) to be set based on the values + of the attributes of this XML element. Note that type conversions + are automatically performed (such as String to int for the id + property), for all converters registered with the ConvertUtils + class from commons-beanutils package.
    8. +
    9. Cause the addBar method of the next-to-top element on the + object stack (which is why this is called the "set next" rule) + to be called, passing the element that is on the top of the stack, which + must be of type mypackage.Bar. This is the rule that causes + the parent/child relationship to be created.
    10. +
    + +

    Once the parse is completed, the first object that was ever pushed on to the +stack (the Foo object in this case) is returned to you. It will +have had its properties set, and all of its child Bar objects +created for you.

    + + +

    Processing A Struts Configuration File

    + +

    As stated earlier, the primary reason that the +Digester package was created is because the +Struts controller servlet itself needed a robust, flexible, easy to extend +mechanism for processing the contents of the struts-config.xml +configuration that describes nearly every aspect of a Struts-based application. +Because of this, the controller servlet contains a comprehensive, real world, +example of how the Digester can be employed for this type of a use case. +See the initDigester() method of class +org.apache.struts.action.ActionServlet for the code that creates +and configures the Digester to be used, and the initMapping() +method for where the parsing actually takes place.

    + +

    (Struts binary and source distributions can be acquired at +http://struts.apache.org/.)

    + +

    The following discussion highlights a few of the matching patterns and +processing rules that are configured, to illustrate the use of some of the +Digester features. First, let's look at how the Digester instance is +created and initialized:

    +
    +    Digester digester = new Digester();
    +    digester.push(this); // Push controller servlet onto the stack
    +    digester.setValidating(true);
    +
    + +

    We see that a new Digester instance is created, and is configured to use +a validating parser. Validation will occur against the struts-config_1_0.dtd +DTD that is included with Struts (as discussed earlier). In order to provide +a means of tracking the configured objects, the controller servlet instance +itself will be added to the digester's stack.

    + +
    +    digester.addObjectCreate("struts-config/global-forwards/forward",
    +                             forwardClass, "className");
    +    digester.addSetProperties("struts-config/global-forwards/forward");
    +    digester.addSetNext("struts-config/global-forwards/forward",
    +                        "addForward",
    +                        "org.apache.struts.action.ActionForward");
    +    digester.addSetProperty
    +      ("struts-config/global-forwards/forward/set-property",
    +       "property", "value");
    +
    + +

    The rules created by these lines are used to process the global forward +declarations. When a <forward> element is encountered, +the following actions take place:

    +
      +
    • A new object instance is created -- the ActionForward + instance that will represent this definition. The Java class name + defaults to that specified as an initialization parameter (which + we have stored in the String variable forwardClass), but can + be overridden by using the "className" attribute (if it is present in the + XML element we are currently parsing). The new ActionForward + instance is pushed onto the stack.
    • +
    • The properties of the ActionForward instance (at the top of + the stack) are configured based on the attributes of the + <forward> element.
    • +
    • Nested occurrences of the <set-property> element + cause calls to additional property setter methods to occur. This is + required only if you have provided a custom implementation of the + ActionForward class with additional properties that are + not included in the DTD.
    • +
    • The addForward() method of the next-to-top object on + the stack (i.e. the controller servlet itself) will be called, passing + the object at the top of the stack (i.e. the ActionForward + instance) as an argument. This causes the global forward to be + registered, and as a result of this it will be remembered even after + the stack is popped.
    • +
    • At the end of the <forward> element, the top element + (i.e. the ActionForward instance) will be popped off the + stack.
    • +
    + +

    Later on, the digester is actually executed as follows:

    +
    +    InputStream input =
    +      getServletContext().getResourceAsStream(config);
    +    ...
    +    try {
    +        digester.parse(input);
    +        input.close();
    +    } catch (SAXException e) {
    +        ... deal with the problem ...
    +    }
    +
    + +

    As a result of the call to parse(), all of the configuration +information that was defined in the struts-config.xml file is +now represented as collections of objects cached within the Struts controller +servlet, as well as being exposed as servlet context attributes.

    + + +

    Parsing Body Text In XML Files

    + +

    The Digester module also allows you to process the nested body text in an +XML file, not just the elements and attributes that are encountered. The +following example is based on an assumed need to parse the web application +deployment descriptor (/WEB-INF/web.xml) for the current web +application, and record the configuration information for a particular +servlet. To record this information, assume the existence of a bean class +with the following method signatures (among others):

    +
    +  package com.mycompany;
    +  public class ServletBean {
    +    public void setServletName(String servletName);
    +    public void setServletClass(String servletClass);
    +    public void addInitParam(String name, String value);
    +  }
    +
    + +

    We are going to process the web.xml file that declares the +controller servlet in a typical Struts-based application (abridged for +brevity in this example):

    +
    +  <web-app>
    +    ...
    +    <servlet>
    +      <servlet-name>action</servlet-name>
    +      <servlet-class>org.apache.struts.action.ActionServlet<servlet-class>
    +      <init-param>
    +        <param-name>application</param-name>
    +        <param-value>org.apache.struts.example.ApplicationResources<param-value>
    +      </init-param>
    +      <init-param>
    +        <param-name>config</param-name>
    +        <param-value>/WEB-INF/struts-config.xml<param-value>
    +      </init-param>
    +    </servlet>
    +    ...
    +  </web-app>
    +
    + +

    Next, lets define some Digester processing rules for this input file:

    +
    +  digester.addObjectCreate("web-app/servlet",
    +                           "com.mycompany.ServletBean");
    +  digester.addCallMethod("web-app/servlet/servlet-name", "setServletName", 0);
    +  digester.addCallMethod("web-app/servlet/servlet-class",
    +                         "setServletClass", 0);
    +  digester.addCallMethod("web-app/servlet/init-param",
    +                         "addInitParam", 2);
    +  digester.addCallParam("web-app/servlet/init-param/param-name", 0);
    +  digester.addCallParam("web-app/servlet/init-param/param-value", 1);
    +
    + +

    Now, as elements are parsed, the following processing occurs:

    +
      +
    • <servlet> - A new com.mycompany.ServletBean + object is created, and pushed on to the object stack.
    • +
    • <servlet-name> - The setServletName() method + of the top object on the stack (our ServletBean) is called, + passing the body content of this element as a single parameter.
    • +
    • <servlet-class> - The setServletClass() method + of the top object on the stack (our ServletBean) is called, + passing the body content of this element as a single parameter.
    • +
    • <init-param> - A call to the addInitParam + method of the top object on the stack (our ServletBean) is + set up, but it is not called yet. The call will be + expecting two String parameters, which must be set up by + subsequent call parameter rules.
    • +
    • <param-name> - The body content of this element is assigned + as the first (zero-relative) argument to the call we are setting up.
    • +
    • <param-value> - The body content of this element is assigned + as the second (zero-relative) argument to the call we are setting up.
    • +
    • </init-param> - The call to addInitParam() + that we have set up is now executed, which will cause a new name-value + combination to be recorded in our bean.
    • +
    • <init-param> - The same set of processing rules are fired + again, causing a second call to addInitParam() with the + second parameter's name and value.
    • +
    • </servlet> - The element on the top of the object stack + (which should be the ServletBean we pushed earlier) is + popped off the object stack.
    • +
    + + +

    Namespace Aware Parsing

    + +

    For digesting XML documents that do not use XML namespaces, the default +behavior of Digester, as described above, is generally sufficient. +However, if the document you are processing uses namespaces, it is often +convenient to have sets of Rule instances that are only +matched on elements that use the prefix of a particular namespace. This +approach, for example, makes it possible to deal with element names that are +the same in different namespaces, but where you want to perform different +processing for each namespace.

    + +

    Digester does not provide full support for namespaces, but does provide +sufficient to accomplish most tasks. Enabling digester's namespace support +is done by following these steps:

    + +
      +
    1. Tell Digester that you will be doing namespace + aware parsing, by adding this statement in your initialization + of the Digester's properties: +
      +    digester.setNamespaceAware(true);
      +    
    2. +
    3. Declare the public namespace URI of the namespace with which + following rules will be associated. Note that you do not + make any assumptions about the prefix - the XML document author + is free to pick whatever prefix they want: +
      +    digester.setRuleNamespaceURI("http://www.mycompany.com/MyNamespace");
      +    
    4. +
    5. Add the rules that correspond to this namespace, in the usual way, + by calling methods like addObjectCreate() or + addSetProperties(). In the matching patterns you specify, + use only the local name portion of the elements (i.e. the + part after the prefix and associated colon (":") character: +
      +    digester.addObjectCreate("foo/bar", "com.mycompany.MyFoo");
      +    digester.addSetProperties("foo/bar");
      +    
    6. +
    7. Repeat the previous two steps for each additional public namespace URI + that should be recognized on this Digester run.
    8. +
    + +

    Now, consider that you might wish to digest the following document, using +the rules that were set up in the steps above:

    +
    +<m:foo
    +   xmlns:m="http://www.mycompany.com/MyNamespace"
    +   xmlns:y="http://www.yourcompany.com/YourNamespace">
    +
    +  <m:bar name="My Name" value="My Value"/>
    +
    +  <y:bar id="123" product="Product Description"/>L
    +
    +</x:foo>
    +
    + +

    Note that your object create and set properties rules will be fired for the +first occurrence of the bar element, but not the +second one. This is because we declared that our rules only matched +for the particular namespace we are interested in. Any elements in the +document that are associated with other namespaces (or no namespaces at all) +will not be processed. In this way, you can easily create rules that digest +only the portions of a compound document that they understand, without placing +any restrictions on what other content is present in the document.

    + +

    You might also want to look at Encapsulated +Rule Sets if you wish to reuse a particular set of rules, associated +with a particular namespace, in more than one application context.

    + +

    Using Namespace Prefixes In Pattern Matching

    + +

    Using rules with namespaces is very useful when you have orthogonal rulesets. +One ruleset applies to a namespace and is independent of other rulesets applying +to other namespaces. However, if your rule logic requires mixed namespaces, then +matching namespace prefix patterns might be a better strategy.

    + +

    When you set the NamespaceAware property to false, digester uses +the qualified element name (which includes the namespace prefix) rather than the +local name as the pattern component for the element. This means that your pattern +matches can include namespace prefixes as well as element names. So, rather than +create namespace-aware rules, create pattern matches including the namespace +prefixes.

    + +

    For example, (with NamespaceAware false), the pattern +'foo:bar' will match a top level element named 'bar' in the +namespace with (local) prefix 'foo'.

    + +

    Limitations of Digester Namespace support

    +

    Digester does not provide general "xpath-compliant" matching; +only the namespace attached to the last element in the match path +is involved in the matching process. Namespaces attached to parent +elements are ignored for matching purposes.

    + + +

    Pluggable Rules Processing

    + +

    By default, Digester selects the rules that match a particular +pattern of nested elements as described under +Element Matching Patterns. If you prefer to use +different selection policies, however, you can create your own implementation +of the org.apache.commons.digester.Rules interface, +or subclass the corresponding convenience base class +org.apache.commons.digester.RulesBase. +Your implementation of the match() method will be called when the +processing for a particular element is started or ended, and you must return +a List of the rules that are relevant for the current nesting +pattern. The order of the rules you return is significant, +and should match the order in which rules were initially added.

    + +

    Your policy for rule selection should generally be sensitive to whether +Namespace Aware Parsing is taking place. In +general, if namespaceAware is true, you should select only rules +that:

    +
      +
    • Are registered for the public namespace URI that corresponds to the + prefix being used on this element.
    • +
    • Match on the "local name" portion of the element (so that the document + creator can use any prefix that they like).
    • +
    + +

    ExtendedBaseRules

    +

    ExtendedBaseRules, +adds some additional expression syntax for pattern matching +to the default mechanism, but it also executes more slowly. See the +JavaDocs for more details on the new pattern matching syntax, and suggestions +on when this implementation should be used. To use it, simply do the +following as part of your Digester initialization:

    + +
    +  Digester digester = ...
    +  ...
    +  digester.setRules(new ExtendedBaseRules());
    +  ...
    +
    + +

    RegexRules

    +

    RegexRules is an advanced Rules +implementation which does not build on the default pattern matching rules. +It uses a pluggable RegexMatcher implementation to test +if a path matches the pattern for a Rule. All matching rules are returned +(note that this behaviour differs from longest matching rule of the default + pattern matching rules). See the Java Docs for more details. +

    +

    +Example usage: +

    + +
    +  Digester digester = ...
    +  ...
    +  digester.setRules(new RegexRules(new SimpleRegexMatcher()));
    +  ...
    +
    +

    RegexMatchers

    +

    +Digester ships only with one RegexMatcher +implementation: SimpleRegexMatcher. +This implementation is unsophisticated and lacks many good features +lacking in more power Regex libraries. There are some good reasons +why this approach was adopted. The first is that SimpleRegexMatcher +is simple, it is easy to write and runs quickly. The second has to do with +the way that RegexRules is intended to be used. +

    +

    +There are many good regex libraries available. (For example +Jakarta ORO, +Jakarta Regex, +GNU Regex and + +Java Regex) +Not only do different people have different personal tastes when it comes to +regular expression matching but these products all offer different functionality +and different strengths. +

    +

    +The pluggable RegexMatcher is a thin bridge +designed to adapt other Regex systems. This allows any Regex library the user +desires to be plugged in and used just by creating one class. +Digester does not (currently) ship with bridges to the major +regex (to allow the dependencies required by Digester +to be kept to a minimum). +

    + +

    Encapsulated Rule Sets

    + +

    All of the examples above have described a scenario where the rules to be +processed are registered with a Digester instance immediately +after it is created. However, this approach makes it difficult to reuse the +same set of rules in more than one application environment. Ideally, one +could package a set of rules into a single class, which could be easily +loaded and registered with a Digester instance in one easy step. +

    + +

    The RuleSet interface (and the convenience base +class RuleSetBase) make it possible to do this. +In addition, the rule instances registered with a particular +RuleSet can optionally be associated with a particular namespace, +as described under Namespace Aware Processing.

    + +

    An example of creating a RuleSet might be something like this: +

    +
    +public class MyRuleSet extends RuleSetBase {
    +
    +  public MyRuleSet() {
    +    this("");
    +  }
    +
    +  public MyRuleSet(String prefix) {
    +    super();
    +    this.prefix = prefix;
    +    this.namespaceURI = "http://www.mycompany.com/MyNamespace";
    +  }
    +
    +  protected String prefix = null;
    +
    +  public void addRuleInstances(Digester digester) {
    +    digester.addObjectCreate(prefix + "foo/bar",
    +      "com.mycompany.MyFoo");
    +    digester.addSetProperties(prefix + "foo/bar");
    +  }
    +
    +}
    +
    + +

    You might use this RuleSet as follow to initialize a +Digester instance:

    +
    +  Digester digester = new Digester();
    +  ... configure Digester properties ...
    +  digester.addRuleSet(new MyRuleSet("baz/"));
    +
    + +

    A couple of interesting notes about this approach:

    +
      +
    • The application that is using these rules does not need to know anything + about the fact that the RuleSet being used is associated + with a particular namespace URI. That knowledge is embedded inside the + RuleSet class itself.
    • +
    • If desired, you could make a set of rules work for more than one + namespace URI by providing constructors on the RuleSet to + allow this to be specified dynamically.
    • +
    • The MyRuleSet example above illustrates another technique + that increases reusability -- you can specify (as an argument to the + constructor) the leading portion of the matching pattern to be used. + In this way, you can construct a Digester that recognizes + the same set of nested elements at different nesting levels within an + XML document.
    • +
    + +

    Using Named Stacks For Inter-Rule Communication

    +

    +Digester is based on Rule instances working together +to process xml. For anything other than the most trivial processing, +communication between Rule instances is necessary. Since Rule +instances are processed in sequence, this usually means storing an Object +somewhere where later instances can retrieve it. +

    +

    +Digester is based on SAX. The most natural data structure to use with +SAX based xml processing is the stack. This allows more powerful processes to be +specified more simply since the pushing and popping of objects can mimic the +nested structure of the xml. +

    +

    +Digester uses two basic stacks: one for the main beans and the other +for parameters for method calls. These are inadequate for complex processing +where many different Rule instances need to communicate through +different channels. +

    +

    +In this case, it is recommended that named stacks are used. In addition to the +two basic stacks, Digester allows rules to use an unlimited number +of other stacks referred two by an identifying string (the name). (That's where +the term named stack comes from.) These stacks are +accessed through calls to: +

    + +

    +Note: all stack names beginning with org.apache.commons.digester +are reserved for future use by the Digester component. It is also recommended +that users choose stack names prefixed by the name of their own domain to avoid conflicts +with other Rule implementations. +

    + +

    Registering DTDs

    + +

    Brief (But Still Too Long) Introduction To System and Public Identifiers

    +

    A definition for an external entity comes in one of two forms: +

    +
      +
    1. SYSTEM system-identifier
    2. +
    3. PUBLIC public-identifier system-identifier
    4. +
    +

    +The system-identifier is an URI from which the resource can be obtained +(either directly or indirectly). Many valid URIs may identify the same resource. +The public-identifier is an additional free identifier which may be used +(by the parser) to locate the resource. +

    +

    +In practice, the weakness with a system-identifier is that most parsers +will attempt to interpret this URI as a URL, try to download the resource directly +from the URL and stop the parsing if this download fails. So, this means that +almost always the URI will have to be a URL from which the declaration +can be downloaded. +

    +

    +URLs may be local or remote but if the URL is chosen to be local, it is likely only +to function correctly on a small number of machines (which are configured precisely +to allow the xml to be parsed). This is usually unsatisfactory and so a universally +accessible URL is preferred. This usually means an internet URL. +

    +

    +To recap, in practice the system-identifier will (most likely) be an +internet URL. Unfortunately downloading from an internet URL is not only slow +but unreliable (since successfully downloading a document from the internet +relies on the client being connect to the internet and the server being +able to satisfy the request). +

    +

    +The public-identifier is a freely defined name but (in practice) it is +strongly recommended that a unique, readable and open format is used (for reasons +that should become clear later). A Formal Public Identifier (FPI) is a very +common choice. This public identifier is often used to provide a unique and location +independent key which can be used to substitute local resources for remote ones +(hint: this is why ;). +

    +

    +By using the second (PUBLIC) form combined with some form of local +catalog (which matches public-identifiers to local resources) and where +the public-identifier is a unique name and the system-identifier +is an internet URL, the practical disadvantages of specifying just a +system-identifier can be avoided. Those external entities which have been +store locally (on the machine parsing the document) can be identified and used. +Only when no local copy exists is it necessary to download the document +from the internet URL. This naming scheme is recommended when using Digester. +

    + +

    External Entity Resolution Using Digester

    +

    +SAX factors out the resolution of external entities into an EntityResolver. +Digester supports the use of custom EntityResolver +but ships with a simple internal implementation. This implementation allows local URLs +to be easily associated with public-identifiers. +

    +

    For example:

    +
    +    digester.register("-//Example Dot Com //DTD Sample Example//EN", "assets/sample.dtd");
    +
    +

    +will make digester return the relative file path assets/sample.dtd +whenever an external entity with public id +-//Example Dot Com //DTD Sample Example//EN is needed. +

    +

    Note: This is a simple (but useful) implementation. +Greater sophistication requires a custom EntityResolver.

    + +

    Troubleshooting

    +

    Debugging Exceptions

    +

    +Digester is based on SAX. +Digestion throws two kinds of Exception: +

    +
      +
    • java.io.IOException
    • +
    • org.xml.sax.SAXException
    • +
    +

    +The first is rarely thrown and indicates the kind of fundamental IO exception +that developers know all about. The second is thrown by SAX parsers when the processing +of the XML cannot be completed. So, to diagnose the cause a certain familiarity with +the way that SAX error handling works is very useful. +

    +

    Diagnosing SAX Exceptions

    +

    +This is a short, potted guide to SAX error handling strategies. It's not intended as a +proper guide to error handling in SAX. +

    +

    +When a SAX parser encounters a problem with the xml (well, ok - sometime after it +encounters a problem) it will throw a + +SAXParseException. This is a subclass of SAXException and contains +a bit of extra information about what exactly when wrong - and more importantly, +where it went wrong. If you catch an exception of this sort, you can be sure that +the problem is with the XML and not Digester or your rules. +It is usually a good idea to catch this exception and log the extra information +to help with diagnosing the reason for the failure. +

    +

    +General +SAXException instances may wrap a causal exception. When exceptions are +throw by Digester each of these will be wrapped into a +SAXException and rethrown. So, catch these and examine the wrapped +exception to diagnose what went wrong. +

    + +

    Frequently Asked Questions

    +
      +
    • Why do I get warnings when using a JAXP 1.1 parser? +If you're using a JAXP 1.1 parser, you might see the following warning (in your log): +
      +[WARN] Digester - -Error: JAXP SAXParser property not recognized: http://java.sun.com/xml/jaxp/properties/schemaLanguage
      +
      +This property is needed for JAXP 1.2 (XML Schema support) as required +for the Servlet Spec. 2.4 but is not recognized by JAXP 1.1 parsers. +This warning is harmless. +
    • +
    • Why Doesn't Schema Validation Work With Parser XXX Out Of The Box? +

      +Schema location and language settings are often need for validation using schemas. +Unfortunately, there isn't a single standard approach to how these properties are +configured on a parser. +Digester tries to guess the parser being used and configure it appropriately +but it's not infallible. +You might need to grab an instance, configure it and pass it to Digester. +

      +

      +If you want to support more than one parser in a portable manner, +then you'll probably want to take a look at the +org.apache.commons.digester.parsers package +and add a new class to support the particular parser that's causing problems. +

      +
    • +
    • Help! +I'm Validating Against Schema But Digester Ignores Errors! +

      +Digester is based on SAX. The convention for +SAX parsers is that all errors are reported (to any registered +ErrorHandler) but processing continues. Digester (by default) +registers its own ErrorHandler implementation. This logs details +but does not stop the processing (following the usual convention for SAX +based processors). +

      +

      +This means that the errors reported by the validation of the schema will appear in the +Digester logs but the processing will continue. To change this behaviour, call +digester.setErrorHandler with a more suitable implementation. +

      + +
    • Where Can I Find Example Code? +

      Digester ships with a sample application: a mapping for the Rich Site +Summary format used by many newsfeeds. Download the source distribution +to see how it works.

      +

      Digester also ships with a set of examples demonstrating most of the +features described in this document. See the "src/examples" subdirectory +of the source distribution.

      +
    • +
    • When Are You Going To Support Rich Site Summary Version x.y.z? +

      +The Rich Site Summary application is intended to be a sample application. +It works but we have no plans to add support for other versions of the format. +

      +

      +We would consider donations of standard digester applications but it's unlikely that +these would ever be shipped with the base digester distribution. +If you want to discuss this, please post to +commons dev mailing list +

      +
    • +
    + +

    Known Limitations

    +

    Accessing Public Methods In A Default Access Superclass

    +

    There is an issue when invoking public methods contained in a default access superclass. +Reflection locates these methods fine and correctly assigns them as public. +However, an IllegalAccessException is thrown if the method is invoked.

    + +

    MethodUtils contains a workaround for this situation. +It will attempt to call setAccessible on this method. +If this call succeeds, then the method can be invoked as normal. +This call will only succeed when the application has sufficient security privileges. +If this call fails then a warning will be logged and the method may fail.

    + +

    Digester uses MethodUtils and so there may be an issue accessing methods +of this kind from a high security environment. If you think that you might be experiencing this +problem, please ask on the mailing list.

    + + diff --git a/java/org/apache/tomcat/util/file/ConfigFileLoader.java b/java/org/apache/tomcat/util/file/ConfigFileLoader.java new file mode 100644 index 0000000..bfec56c --- /dev/null +++ b/java/org/apache/tomcat/util/file/ConfigFileLoader.java @@ -0,0 +1,59 @@ +/* + * 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.tomcat.util.file; + +import java.io.InputStream; + +/** + * This class is used to obtain {@link InputStream}s for configuration files + * from a given location String. This allows greater flexibility than these + * files having to be loaded directly from a file system. + */ +public class ConfigFileLoader { + + private static ConfigurationSource source; + + /** + * Get the configured configuration source. If none has been configured, + * a default source based on the calling directory will be used. + * @return the configuration source in use + */ + public static final ConfigurationSource getSource() { + if (source == null) { + return ConfigurationSource.DEFAULT; + } + return source; + } + + /** + * Set the configuration source used by Tomcat to locate various + * configuration resources. + * @param source The source + */ + public static final void setSource(ConfigurationSource source) { + if (ConfigFileLoader.source == null) { + ConfigFileLoader.source = source; + } + } + + private ConfigFileLoader() { + // Hide the constructor + } + + +} diff --git a/java/org/apache/tomcat/util/file/ConfigurationSource.java b/java/org/apache/tomcat/util/file/ConfigurationSource.java new file mode 100644 index 0000000..8a93d2a --- /dev/null +++ b/java/org/apache/tomcat/util/file/ConfigurationSource.java @@ -0,0 +1,170 @@ +/* + * 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.tomcat.util.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; + +import org.apache.tomcat.util.buf.UriUtil; + +/** + * Abstracts configuration file storage. Allows Tomcat embedding using the regular + * configuration style. + * This abstraction aims to be very simple and does not cover resource listing, + * which is usually used for dynamic deployments that are usually not used when + * embedding, as well as resource writing. + */ +public interface ConfigurationSource { + + ConfigurationSource DEFAULT = new ConfigurationSource() { + protected final File userDir = new File(System.getProperty("user.dir")); + protected final URI userDirUri = userDir.toURI(); + @Override + public Resource getResource(String name) throws IOException { + if (!UriUtil.isAbsoluteURI(name)) { + File f = new File(name); + if (!f.isAbsolute()) { + f = new File(userDir, name); + } + if (f.isFile()) { + FileInputStream fis = new FileInputStream(f); + return new Resource(fis, f.toURI()); + } + } + URI uri = null; + try { + uri = userDirUri.resolve(name); + } catch (IllegalArgumentException e) { + throw new FileNotFoundException(name); + } + try { + URL url = uri.toURL(); + return new Resource(url.openConnection().getInputStream(), uri); + } catch (MalformedURLException e) { + throw new FileNotFoundException(name); + } + } + @Override + public URI getURI(String name) { + if (!UriUtil.isAbsoluteURI(name)) { + File f = new File(name); + if (!f.isAbsolute()) { + f = new File(userDir, name); + } + if (f.isFile()) { + return f.toURI(); + } + } + return userDirUri.resolve(name); + } + }; + + /** + * Represents a resource: a stream to the resource associated with + * its URI. + */ + class Resource implements AutoCloseable { + private final InputStream inputStream; + private final URI uri; + public Resource(InputStream inputStream, URI uri) { + this.inputStream = inputStream; + this.uri = uri; + } + public InputStream getInputStream() { + return inputStream; + } + public URI getURI() { + return uri; + } + public long getLastModified() + throws MalformedURLException, IOException { + URLConnection connection = null; + try { + connection = uri.toURL().openConnection(); + return connection.getLastModified(); + } finally { + if (connection != null) { + connection.getInputStream().close(); + } + } + } + @Override + public void close() throws IOException { + if (inputStream != null) { + inputStream.close(); + } + } + } + + /** + * Returns the contents of the main conf/server.xml file. + * @return the server.xml as an InputStream + * @throws IOException if an error occurs or if the resource does not exist + */ + default Resource getServerXml() + throws IOException { + return getConfResource("server.xml"); + } + + /** + * Returns the contents of the shared conf/web.xml file. This usually + * contains the declaration of the default and JSP servlets. + * @return the web.xml as an InputStream + * @throws IOException if an error occurs or if the resource does not exist + */ + default Resource getSharedWebXml() + throws IOException { + return getConfResource("web.xml"); + } + + /** + * Get a resource, based on the conf path. + * @param name The resource name + * @return the resource as an InputStream + * @throws IOException if an error occurs or if the resource does not exist + */ + default Resource getConfResource(String name) + throws IOException { + String fullName = "conf/" + name; + return getResource(fullName); + } + + /** + * Get a resource, not based on the conf path. + * @param name The resource name + * @return the resource + * @throws IOException if an error occurs or if the resource does not exist + */ + Resource getResource(String name) + throws IOException; + + /** + * Get a URI to the given resource. Unlike getResource, this will also + * return URIs to locations where no resource exists. + * @param name The resource name + * @return a URI representing the resource location + */ + URI getURI(String name); + +} diff --git a/java/org/apache/tomcat/util/file/Matcher.java b/java/org/apache/tomcat/util/file/Matcher.java new file mode 100644 index 0000000..d2e7864 --- /dev/null +++ b/java/org/apache/tomcat/util/file/Matcher.java @@ -0,0 +1,240 @@ +/* + * 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.tomcat.util.file; + +import java.util.Set; + +/** + *

    This is a utility class to match file globs. + * The class has been derived from + * org.apache.tools.ant.types.selectors.SelectorUtils. + *

    + *

    All methods are static.

    + */ +public final class Matcher { + + /** + * Tests whether or not a given file name matches any file name pattern in + * the given set. The match is performed case-sensitively. + * + * @see #match(String, String, boolean) + * + * @param patternSet The pattern set to match against. Must not be + * null. + * @param fileName The file name to match, as a String. Must not be + * null. It must be just a file name, without + * a path. + * + * @return true if any pattern in the set matches against the + * file name, or false otherwise. + */ + public static boolean matchName(Set patternSet, String fileName) { + char[] fileNameArray = fileName.toCharArray(); + for (String pattern: patternSet) { + if (match(pattern, fileNameArray, true)) { + return true; + } + } + return false; + } + + + /** + * Tests whether or not a string matches against a pattern. + * The pattern may contain two special characters:
    + * '*' means zero or more characters
    + * '?' means one and only one character + * + * @param pattern The pattern to match against. + * Must not be null. + * @param str The string which must be matched against the + * pattern. Must not be null. + * @param caseSensitive Whether or not matching should be performed + * case sensitively. + * + * + * @return true if the string matches against the pattern, + * or false otherwise. + */ + public static boolean match(String pattern, String str, + boolean caseSensitive) { + + return match(pattern, str.toCharArray(), caseSensitive); + } + + + /** + * Tests whether or not a string matches against a pattern. + * The pattern may contain two special characters:
    + * '*' means zero or more characters
    + * '?' means one and only one character + * + * @param pattern The pattern to match against. + * Must not be null. + * @param strArr The character array which must be matched against the + * pattern. Must not be null. + * @param caseSensitive Whether or not matching should be performed + * case sensitively. + * + * + * @return true if the string matches against the pattern, + * or false otherwise. + */ + private static boolean match(String pattern, char[] strArr, + boolean caseSensitive) { + char[] patArr = pattern.toCharArray(); + int patIdxStart = 0; + int patIdxEnd = patArr.length - 1; + int strIdxStart = 0; + int strIdxEnd = strArr.length - 1; + char ch; + + boolean containsStar = false; + for (char c : patArr) { + if (c == '*') { + containsStar = true; + break; + } + } + + if (!containsStar) { + // No '*'s, so we make a shortcut + if (patIdxEnd != strIdxEnd) { + return false; // Pattern and string do not have the same size + } + for (int i = 0; i <= patIdxEnd; i++) { + ch = patArr[i]; + if (ch != '?') { + if (different(caseSensitive, ch, strArr[i])) { + return false; // Character mismatch + } + } + } + return true; // String matches against pattern + } + + if (patIdxEnd == 0) { + return true; // Pattern contains only '*', which matches anything + } + + // Process characters before first star + while (true) { + ch = patArr[patIdxStart]; + if (ch == '*' || strIdxStart > strIdxEnd) { + break; + } + if (ch != '?') { + if (different(caseSensitive, ch, strArr[strIdxStart])) { + return false; // Character mismatch + } + } + patIdxStart++; + strIdxStart++; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + return allStars(patArr, patIdxStart, patIdxEnd); + } + + // Process characters after last star + while (true) { + ch = patArr[patIdxEnd]; + if (ch == '*' || strIdxStart > strIdxEnd) { + break; + } + if (ch != '?') { + if (different(caseSensitive, ch, strArr[strIdxEnd])) { + return false; // Character mismatch + } + } + patIdxEnd--; + strIdxEnd--; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + return allStars(patArr, patIdxStart, patIdxEnd); + } + + // process pattern between stars. padIdxStart and patIdxEnd point + // always to a '*'. + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp = -1; + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*') { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == patIdxStart + 1) { + // Two stars next to each other, skip the first one. + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - patIdxStart - 1); + int strLength = (strIdxEnd - strIdxStart + 1); + int foundIdx = -1; + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + ch = patArr[patIdxStart + j + 1]; + if (ch != '?') { + if (different(caseSensitive, ch, + strArr[strIdxStart + i + j])) { + continue strLoop; + } + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + // All characters in the string are used. Check if only '*'s are left + // in the pattern. If so, we succeeded. Otherwise failure. + return allStars(patArr, patIdxStart, patIdxEnd); + } + + private static boolean allStars(char[] chars, int start, int end) { + for (int i = start; i <= end; ++i) { + if (chars[i] != '*') { + return false; + } + } + return true; + } + + private static boolean different( + boolean caseSensitive, char ch, char other) { + return caseSensitive + ? ch != other + : Character.toUpperCase(ch) != Character.toUpperCase(other); + } + +} diff --git a/java/org/apache/tomcat/util/file/package.html b/java/org/apache/tomcat/util/file/package.html new file mode 100644 index 0000000..b849e73 --- /dev/null +++ b/java/org/apache/tomcat/util/file/package.html @@ -0,0 +1,26 @@ + + + + + + +

    +This package contains utility classes for file handling. +

    + + diff --git a/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java b/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java new file mode 100644 index 0000000..eb678aa --- /dev/null +++ b/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java @@ -0,0 +1,72 @@ +/* + * 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.tomcat.util.http; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Queue; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * A thread safe wrapper around {@link SimpleDateFormat} that does not make use of ThreadLocal and - broadly - only + * creates enough SimpleDateFormat objects to satisfy the concurrency requirements. + */ +public class ConcurrentDateFormat { + + private final String format; + private final Locale locale; + private final TimeZone timezone; + private final Queue queue = new ConcurrentLinkedQueue<>(); + + public ConcurrentDateFormat(String format, Locale locale, TimeZone timezone) { + this.format = format; + this.locale = locale; + this.timezone = timezone; + SimpleDateFormat initial = createInstance(); + queue.add(initial); + } + + public String format(Date date) { + SimpleDateFormat sdf = queue.poll(); + if (sdf == null) { + sdf = createInstance(); + } + String result = sdf.format(date); + queue.add(sdf); + return result; + } + + public Date parse(String source) throws ParseException { + SimpleDateFormat sdf = queue.poll(); + if (sdf == null) { + sdf = createInstance(); + } + Date result = sdf.parse(source); + sdf.setTimeZone(timezone); + queue.add(sdf); + return result; + } + + private SimpleDateFormat createInstance() { + SimpleDateFormat sdf = new SimpleDateFormat(format, locale); + sdf.setTimeZone(timezone); + return sdf; + } +} diff --git a/java/org/apache/tomcat/util/http/CookieProcessor.java b/java/org/apache/tomcat/util/http/CookieProcessor.java new file mode 100644 index 0000000..d84cc87 --- /dev/null +++ b/java/org/apache/tomcat/util/http/CookieProcessor.java @@ -0,0 +1,55 @@ +/* + * 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.tomcat.util.http; + +import java.nio.charset.Charset; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; + +public interface CookieProcessor { + + /** + * Parse the provided headers into server cookie objects. + * + * @param headers The HTTP headers to parse + * @param serverCookies The server cookies object to populate with the results of the parsing + */ + void parseCookieHeader(MimeHeaders headers, ServerCookies serverCookies); + + /** + * Generate the {@code Set-Cookie} HTTP header value for the given Cookie. This method receives as parameter the + * servlet request so that it can make decisions based on request properties. One such use-case is decide if the + * SameSite attribute should be added to the cookie based on the User-Agent or other request header because there + * are browser versions incompatible with the SameSite attribute. This is described by + * the Chromium project. + * + * @param request The servlet request + * @param cookie The cookie for which the header will be generated + * + * @return The header value in a form that can be added directly to the response + */ + String generateHeader(Cookie cookie, HttpServletRequest request); + + /** + * Obtain the character set that will be used when converting between bytes and characters when parsing and/or + * generating HTTP headers for cookies. + * + * @return The character set used for byte<->character conversions + */ + Charset getCharset(); +} diff --git a/java/org/apache/tomcat/util/http/CookieProcessorBase.java b/java/org/apache/tomcat/util/http/CookieProcessorBase.java new file mode 100644 index 0000000..5815ca4 --- /dev/null +++ b/java/org/apache/tomcat/util/http/CookieProcessorBase.java @@ -0,0 +1,84 @@ +/* + * 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.tomcat.util.http; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public abstract class CookieProcessorBase implements CookieProcessor { + + private static final String COOKIE_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + + protected static final ThreadLocal COOKIE_DATE_FORMAT = ThreadLocal.withInitial(() -> { + DateFormat df = new SimpleDateFormat(COOKIE_DATE_PATTERN, Locale.US); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + return df; + }); + + protected static final String ANCIENT_DATE; + + static { + ANCIENT_DATE = COOKIE_DATE_FORMAT.get().format(new Date(10000)); + } + + private SameSiteCookies sameSiteCookies = SameSiteCookies.UNSET; + + private boolean partitioned = false; + + + public SameSiteCookies getSameSiteCookies() { + return sameSiteCookies; + } + + public void setSameSiteCookies(String sameSiteCookies) { + this.sameSiteCookies = SameSiteCookies.fromString(sameSiteCookies); + } + + + /** + * Should the {@code Partitioned} attribute be added by default to cookies created for this web application. + *

    + * The name of the attribute used to indicate a partitioned cookie as part of + * CHIPS is not defined by an RFC and + * may change in a non-backwards compatible way once equivalent functionality is included in an RFC. + * + * @return {@code true} if the {@code Partitioned} attribute should be added by default to cookies created for this + * web application, otherwise {@code false} + */ + public boolean getPartitioned() { + return partitioned; + } + + + /** + * Configure whether the {@code Partitioned} attribute should be added by default to cookies created for this web + * application. + *

    + * The name of the attribute used to indicate a partitioned cookie as part of + * CHIPS is not defined by an RFC and + * may change in a non-backwards compatible way once equivalent functionality is included in an RFC. + * + * @param partitioned {@code true} if the {@code Partitioned} attribute should be added by default to cookies + * created for this web application, otherwise {@code false} + */ + public void setPartitioned(boolean partitioned) { + this.partitioned = partitioned; + } +} diff --git a/java/org/apache/tomcat/util/http/FastHttpDateFormat.java b/java/org/apache/tomcat/util/http/FastHttpDateFormat.java new file mode 100644 index 0000000..f48913b --- /dev/null +++ b/java/org/apache/tomcat/util/http/FastHttpDateFormat.java @@ -0,0 +1,183 @@ +/* + * 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.tomcat.util.http; + +import java.text.ParseException; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Utility class to generate HTTP dates. + * + * @author Remy Maucherat + */ +public final class FastHttpDateFormat { + + + // -------------------------------------------------------------- Variables + + + private static final int CACHE_SIZE = + Integer.getInteger("org.apache.tomcat.util.http.FastHttpDateFormat.CACHE_SIZE", 1000).intValue(); + + + // HTTP date formats + private static final String DATE_RFC5322 = "EEE, dd MMM yyyy HH:mm:ss z"; + private static final String DATE_OBSOLETE_RFC850 = "EEEEEE, dd-MMM-yy HH:mm:ss zzz"; + private static final String DATE_OBSOLETE_ASCTIME = "EEE MMMM d HH:mm:ss yyyy"; + + private static final ConcurrentDateFormat FORMAT_RFC5322; + private static final ConcurrentDateFormat FORMAT_OBSOLETE_RFC850; + private static final ConcurrentDateFormat FORMAT_OBSOLETE_ASCTIME; + + private static final ConcurrentDateFormat[] httpParseFormats; + + static { + // All the formats that use a timezone use GMT + TimeZone tz = TimeZone.getTimeZone("GMT"); + + FORMAT_RFC5322 = new ConcurrentDateFormat(DATE_RFC5322, Locale.US, tz); + FORMAT_OBSOLETE_RFC850 = new ConcurrentDateFormat(DATE_OBSOLETE_RFC850, Locale.US, tz); + FORMAT_OBSOLETE_ASCTIME = new ConcurrentDateFormat(DATE_OBSOLETE_ASCTIME, Locale.US, tz); + + httpParseFormats = + new ConcurrentDateFormat[] { FORMAT_RFC5322, FORMAT_OBSOLETE_RFC850, FORMAT_OBSOLETE_ASCTIME }; + } + + /** + * Instant on which the currentDate object was generated. + */ + private static volatile long currentDateGenerated = 0L; + + + /** + * Current formatted date. + */ + private static String currentDate = null; + + + /** + * Formatter cache. + */ + private static final Map formatCache = new ConcurrentHashMap<>(CACHE_SIZE); + + + /** + * Parser cache. + */ + private static final Map parseCache = new ConcurrentHashMap<>(CACHE_SIZE); + + + // --------------------------------------------------------- Public Methods + + + /** + * Get the current date in HTTP format. + * + * @return the HTTP date + */ + public static String getCurrentDate() { + long now = System.currentTimeMillis(); + // Handle case where time moves backwards (e.g. system time corrected) + if (Math.abs(now - currentDateGenerated) > 1000) { + currentDate = FORMAT_RFC5322.format(new Date(now)); + currentDateGenerated = now; + } + return currentDate; + } + + + /** + * Get the HTTP format of the specified date. + * + * @param value The date + * + * @return the HTTP date + */ + public static String formatDate(long value) { + Long longValue = Long.valueOf(value); + String cachedDate = formatCache.get(longValue); + if (cachedDate != null) { + return cachedDate; + } + + String newDate = FORMAT_RFC5322.format(new Date(value)); + updateFormatCache(longValue, newDate); + return newDate; + } + + + /** + * Try to parse the given date as an HTTP date. + * + * @param value The HTTP date + * + * @return the date as a long or -1 if the value cannot be parsed + */ + public static long parseDate(String value) { + + Long cachedDate = parseCache.get(value); + if (cachedDate != null) { + return cachedDate.longValue(); + } + + long date = -1; + for (int i = 0; (date == -1) && (i < httpParseFormats.length); i++) { + try { + date = httpParseFormats[i].parse(value).getTime(); + updateParseCache(value, Long.valueOf(date)); + } catch (ParseException e) { + // Ignore + } + } + + return date; + } + + + /** + * Update cache. + */ + private static void updateFormatCache(Long key, String value) { + if (value == null) { + return; + } + if (formatCache.size() > CACHE_SIZE) { + formatCache.clear(); + } + formatCache.put(key, value); + } + + + /** + * Update cache. + */ + private static void updateParseCache(String key, Long value) { + if (value == null) { + return; + } + if (parseCache.size() > CACHE_SIZE) { + parseCache.clear(); + } + parseCache.put(key, value); + } + + +} diff --git a/java/org/apache/tomcat/util/http/HeaderUtil.java b/java/org/apache/tomcat/util/http/HeaderUtil.java new file mode 100644 index 0000000..59f06c4 --- /dev/null +++ b/java/org/apache/tomcat/util/http/HeaderUtil.java @@ -0,0 +1,51 @@ +/* + * 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.tomcat.util.http; + +public class HeaderUtil { + + /** + * Converts an HTTP header line in byte form to a printable String. Bytes corresponding to visible ASCII characters + * will converted to those characters. All other bytes (0x00 to 0x1F, 0x7F to OxFF) will be represented in 0xNN + * form. + * + * @param bytes Contains an HTTP header line + * @param offset The start position of the header line in the array + * @param len The length of the HTTP header line + * + * @return A String with non-printing characters replaced by the 0xNN equivalent + */ + public static String toPrintableString(byte[] bytes, int offset, int len) { + StringBuilder result = new StringBuilder(); + for (int i = offset; i < offset + len; i++) { + char c = (char) (bytes[i] & 0xFF); + if (c < 0x20 || c > 0x7E) { + result.append("0x"); + result.append(Character.forDigit((c >> 4) & 0xF, 16)); + result.append(Character.forDigit((c) & 0xF, 16)); + } else { + result.append(c); + } + } + return result.toString(); + } + + + private HeaderUtil() { + // Utility class. Hide default constructor. + } +} diff --git a/java/org/apache/tomcat/util/http/LocalStrings.properties b/java/org/apache/tomcat/util/http/LocalStrings.properties new file mode 100644 index 0000000..a8f60eb --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings.properties @@ -0,0 +1,45 @@ +# 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. + +cookies.fallToDebug=\n\ +\ Note: further occurrences of Cookie errors will be logged at DEBUG level. +cookies.invalidCookieToken=Cookies: Invalid cookie. Value not a token or quoted value +cookies.invalidSameSiteCookies=Unknown setting [{0}], must be one of: unset, none, lax, strict. Default value is unset. +cookies.invalidSpecial=Cookies: Unknown Special Cookie +cookies.maxCountFail=More than the maximum allowed number of cookies, [{0}], were detected. + +headers.maxCountFail=More than the maximum allowed number of headers, [{0}], were detected. + +parameters.bytes=Start processing with input [{0}] +parameters.copyFail=Failed to create copy of original parameter values for debug logging purposes +parameters.decodeFail.debug=Character decoding failed. Parameter [{0}] with value [{1}] has been ignored. +parameters.decodeFail.info=Character decoding failed. Parameter [{0}] with value [{1}] has been ignored. Note that the name and value quoted here may be corrupted due to the failed decoding. Use debug level logging to see the original, non-corrupted values. +parameters.duplicateFail=Failed to create copy of query parameters +parameters.emptyChunk=Empty parameter chunk ignored +parameters.fallToDebug=\n\ +\ Note: further occurrences of Parameter errors will be logged at DEBUG level. +parameters.invalidChunk=Invalid chunk starting at byte [{0}] and ending at byte [{1}] with a value of [{2}] ignored +parameters.maxCountFail=More than the maximum number of request parameters (GET plus POST) for a single request ([{0}]) were detected. Any parameters beyond this limit have been ignored. To change this limit, set the maxParameterCount attribute on the Connector. +parameters.maxCountFail.fallToDebug=\n\ +\ Note: further occurrences of this error will be logged at DEBUG level. +parameters.multipleDecodingFail=Character decoding failed. A total of [{0}] failures were detected but only the first was logged. Enable debug level logging for this logger to log all failures. +parameters.noequal=Parameter starting at position [{0}] and ending at position [{1}] with a value of [{2}] was not followed by an ''='' character + +rfc6265CookieProcessor.expectedBytes=Parsing cookie as String, expected bytes +rfc6265CookieProcessor.invalidAttributeName=An invalid attribute name [{0}] was specified for this cookie +rfc6265CookieProcessor.invalidAttributeValue=An invalid attribute value [{1}] was specified for this cookie attribute [{0}] +rfc6265CookieProcessor.invalidCharInValue=An invalid character [{0}] was present in the Cookie value +rfc6265CookieProcessor.invalidDomain=An invalid domain [{0}] was specified for this cookie +rfc6265CookieProcessor.invalidPath=An invalid path [{0}] was specified for this cookie diff --git a/java/org/apache/tomcat/util/http/LocalStrings_cs.properties b/java/org/apache/tomcat/util/http/LocalStrings_cs.properties new file mode 100644 index 0000000..a2cfdc6 --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_cs.properties @@ -0,0 +1,27 @@ +# 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. + +cookies.fallToDebug=\n\ +\ Poznámka: další výskyty chyb Cookie budou zalogovány v úrovni DEBUG. +cookies.invalidCookieToken=Cookies: neplatné cookie. Hodnota není znak nebo citovaná hodnota + +parameters.copyFail=VytvoÅ™ení kopie původních hodnot parametrů pro úÄely DEBUG logování selhalo +parameters.decodeFail.debug=Dekódování znaku selhalo. Parametr [{0}] s hodnotou [{1}] byl ignorován. +parameters.fallToDebug=\n\ +\ Poznámka: další výskyty chyb parametrů budou zalogovány v DEBUG úrovni. +parameters.maxCountFail.fallToDebug=\n\ +\ Poznámka: další výskyty této chyby budou zalogovány v úrovni DEBUG. + +rfc6265CookieProcessor.invalidPath=Byla specifikována neplatná cesta [{0}] pro toto cookie diff --git a/java/org/apache/tomcat/util/http/LocalStrings_de.properties b/java/org/apache/tomcat/util/http/LocalStrings_de.properties new file mode 100644 index 0000000..22e8b60 --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_de.properties @@ -0,0 +1,27 @@ +# 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. + +cookies.invalidCookieToken=Cookies: Ungültiges Cookie. Wert ist kein Token oder Quoted Token +cookies.invalidSameSiteCookies=Unbekannte Einstellung [{0}], Sollte einer der Werte: ''none'', ''lax'', ''strict'' entsprechen. Standardwert ist ''none'' + +parameters.bytes=Starte Verarbeitung mit Eingabe [{0}] +parameters.copyFail=Konnte keine Kopie der Originalwerte der Parameter für Debug-Ausgaben erzeugen +parameters.decodeFail.debug=Zeichen-Dekodierung fehlgeschlagen. Parameter [{0}] mit Wert [{1}] wurde ignoriert +parameters.fallToDebug=\n\ +\ Beachte: weitere Vorkommen von Parameter Fehlern werden im DEBUG Level geloggt. +parameters.maxCountFail.fallToDebug=\n\ +\ Hinweis: weitere Vorkommen dieses Fehlers werden im DEBUG-Level protokolliert. + +rfc6265CookieProcessor.invalidPath=Ein ungültiger Pfad [{0}] ist für das Cookie spezifiziert diff --git a/java/org/apache/tomcat/util/http/LocalStrings_es.properties b/java/org/apache/tomcat/util/http/LocalStrings_es.properties new file mode 100644 index 0000000..cb51d9f --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_es.properties @@ -0,0 +1,27 @@ +# 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. + +cookies.invalidCookieToken=Cookies: cookie no válida. El valor no es un token o un valor acotado + +parameters.copyFail=Fallo al crear copia de los valores orignales del parámetro para propósitos de debug +parameters.decodeFail.debug=Fallo al decodificar el caracter. Parámetro [{0}] con valor [{1}] ha sido ignorado.\n +parameters.fallToDebug=\n\ +\ Nota: Futuras ocurrencias de error del Parámetro serán loggueadas a nivel DEBUG. +parameters.maxCountFail=Se detectaron más del máximo número de los parámetros solicitados (GET plus POST) para una solicitud simple ([{0}]). Cualquier parámetro por encima de este límite ha sido ignorado. Para cambiar este límite, fije el atributo maxParameterCount attribute en el Conector.\n +parameters.maxCountFail.fallToDebug=\n\ +\ Nota: futuras ocurrencias de este tipo de error serán logueadas a nivel DEBUG +parameters.noequal=El parámetro que inicia en la posición [{0}] y termina en la posición [{1}] con valor de [{2}] no tiene un caracter ''='' a continuación + +rfc6265CookieProcessor.invalidPath=Se ha especificado un camino no válido [{0}] para esta cookie diff --git a/java/org/apache/tomcat/util/http/LocalStrings_fr.properties b/java/org/apache/tomcat/util/http/LocalStrings_fr.properties new file mode 100644 index 0000000..725fc96 --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_fr.properties @@ -0,0 +1,45 @@ +# 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. + +cookies.fallToDebug=\n\ +\ Note : les occurrences suivantes d'erreurs de Cookies seront enregistrées au niveau DEBUG. +cookies.invalidCookieToken=Cookie non valide. Sa valeur n'est ni un "token" ni une valeur entre guillemets +cookies.invalidSameSiteCookies=Valeur inconnue [{0}], seules possibles : unset, none, lax, strict. Valeur par défaut : unset. +cookies.invalidSpecial=Cookie spécial inconnu +cookies.maxCountFail=Le nombre maximum de cookies [{0}] est dépassé + +headers.maxCountFail=Le nombre d''en-têtes [{0}] dépasse le maximum autorisé + +parameters.bytes=Début du traitement avec les données [{0}] +parameters.copyFail=Echec de la copie des valeurs de paramètres originaux pour raisons de journalisation du déboguage +parameters.decodeFail.debug=Echec de décodage de caractère, le paramètre [{0}] de valeur [{1}] a été ignoré +parameters.decodeFail.info=Echec de décodage de caractère, le paramètre [{0}] avec la valeur [{1}] a été ignoré ; le nom et la valeur mentionnés ici peuvent avoir été corrompus à cause de l''erreur de décodage, utilisez le niveau debug pour voir les originaux +parameters.duplicateFail=Impossible de créer une copie des paramètres de la requête +parameters.emptyChunk=Le bloc de paramètres vide a été ignoré +parameters.fallToDebug=\n\ +\ Note : les occurrences suivantes d'erreurs de Paramètres seront enregistrées au niveau DEBUG. +parameters.invalidChunk=Morceau (chunk) invalide démarrant à l''octet [{0}] et se terminant à l''octet [{1}] avec une valeur de [{2}] ignoré +parameters.maxCountFail=Le nombre maximum de paramètres pour une seule requête (GET plus POST) [{0}] a été détecté, les paramètres supplémentaires ont été ignorés ; l''attribut maxParameterCount du Connector permet de changer cette limite +parameters.maxCountFail.fallToDebug=\n\ +\ Note : les occurrences suivantes de cette erreur seront enregistrées au niveau DEBUG. +parameters.multipleDecodingFail=Echec de décodage de caractère, [{0}] erreurs ont été détectées au total mais seule la première a été logguée, activez le niveau debug pour avoir toutes les erreurs +parameters.noequal=Le paramètre qui démarre à la position [{0}] et qui se termine à la position [{1}] avec comme valeur [{2}] n''est pas suivi par un caractère ''='' + +rfc6265CookieProcessor.expectedBytes=Le cookie est traité comme String, des bytes étaient attendus +rfc6265CookieProcessor.invalidAttributeName=Un nom d''attribut invalide [{0}] a été spécifié pour ce cookie +rfc6265CookieProcessor.invalidAttributeValue=Un valeur d''attribut invalide [{0}] a été spécifiée pour cet attribut de cookie [{1}] +rfc6265CookieProcessor.invalidCharInValue=Un caractère invalide [{0}] était présent dans la valeur du cookie +rfc6265CookieProcessor.invalidDomain=Un domaine [{0}] invalide a été spécifié pour ce cookie +rfc6265CookieProcessor.invalidPath=Un chemin (path) invalide [{0}] a été spécifié pour ce biscuit (cookie) diff --git a/java/org/apache/tomcat/util/http/LocalStrings_ja.properties b/java/org/apache/tomcat/util/http/LocalStrings_ja.properties new file mode 100644 index 0000000..3d00774 --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_ja.properties @@ -0,0 +1,45 @@ +# 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. + +cookies.fallToDebug=\n\ +\ 注: 以é™ã®Cookieエラーã®ç™ºç”Ÿã¯DEBUGレベルã§ãƒ­ã‚°ã«å‡ºåŠ›ã•ã‚Œã¾ã™ã€‚ +cookies.invalidCookieToken=Cookie: 無効㪠cookie ã§ã™ã€‚値ãŒãƒˆãƒ¼ã‚¯ãƒ³ã§ã¯ãªã„ã‹ã€ã‚¯ã‚©ãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +cookies.invalidSameSiteCookies=ä¸æ˜Žãªè¨­å®š [{0}] ã¯ã€æ¬¡ã®ã†ã¡ã®ã„ãšã‚Œã‹1ã¤ã§ã‚ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™: unset, none, lax, strict. 既定値㯠unset ã§ã™ã€‚ +cookies.invalidSpecial=Cookies: ä¸æ˜Žãªç‰¹åˆ¥ãª Cookie +cookies.maxCountFail=最大数[{0}]以上ã®ã‚¯ãƒƒã‚­ãƒ¼ãŒæ¤œå‡ºã•ã‚Œã¾ã—ãŸã€‚ + +headers.maxCountFail=検出ã—ãŸãƒ˜ãƒƒãƒ€ãƒ¼æ•° [{0}] ã¯ä¸Šé™å€¤ã‚’越ãˆã¦ã„ã¾ã™ã€‚ + +parameters.bytes=入力[{0}]ã§å‡¦ç†ã‚’開始ã—ã¾ã™ã€‚ +parameters.copyFail=デãƒãƒƒã‚°ãƒ­ã‚°ã®ç›®çš„ã§å…ƒã®ãƒ‘ラメータ値ã®ã‚³ãƒ”ーを作æˆã§ãã¾ã›ã‚“ã§ã—㟠+parameters.decodeFail.debug=文字列ã®ãƒ‡ã‚³ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸã€‚パラメーター [{0}] (値 [{1}]) ã¯ç„¡è¦–ã—ã¾ã—ãŸã€‚ +parameters.decodeFail.info=文字ã®ãƒ‡ã‚³ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ 値 [{1}] ã®ãƒ‘ラメータ [{0}] ã¯ç„¡è¦–ã•ã‚Œã¾ã—ãŸã€‚ ã“ã“ã§å¼•ç”¨ã•ã‚ŒãŸåå‰ã¨å€¤ã¯ã€ãƒ‡ã‚³ãƒ¼ãƒ‰ã«å¤±æ•—ã—ãŸãŸã‚ã«ç ´æã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ã“ã¨ã«æ³¨æ„ã—ã¦ãã ã•ã„。 デãƒãƒƒã‚°ãƒ¬ãƒ™ãƒ«ã®ãƒ­ã‚°ã‚’使用ã—ã¦ã€ç ´æã—ã¦ã„ãªã„å…ƒã®å€¤ã‚’確èªã—ã¦ãã ã•ã„。 +parameters.duplicateFail=クエリパラメータã®ã‚³ãƒ”ー作æˆã«å¤±æ•—ã—ã¾ã—㟠+parameters.emptyChunk=空ã®ãƒ‘ラメータãƒãƒ£ãƒ³ã‚¯ãŒç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +parameters.fallToDebug=\n\ +\ 注: 以é™ã®ãƒ‘ラメータエラーã®ç™ºç”Ÿã¯DEBUGレベルã§ãƒ­ã‚°ã«å‡ºåŠ›ã•ã‚Œã¾ã™ã€‚ +parameters.invalidChunk=ãƒã‚¤ãƒˆå€¤ [{0}] ã§å§‹ã¾ã‚Šãƒã‚¤ãƒˆå€¤ [{1}] ã§çµ‚了ã™ã‚‹ä¸æ­£ãªãƒãƒ£ãƒ³ã‚¯ã§ã™ã€‚値 [{2}] を無視ã—ã¾ã™ã€‚ +parameters.maxCountFail=å˜ç‹¬ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆ ([{0}]) ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ‘ラメーター (GET ãŠã‚ˆã³ POST) ã®æ•°ãŒä¸Šé™å€¤ã‚’超ãˆã¦ã„ã¾ã™ã€‚上é™å€¤ã‚’超ãˆã‚‹ã™ã¹ã¦ã®ãƒ‘ラメーターã¯ç„¡è¦–ã—ã¾ã™ã€‚上é™å€¤ã‚’変更ã™ã‚‹ã«ã¯ Connector è¦ç´ ã® maxParameterCount 属性を設定ã—ã¦ãã ã•ã„。 +parameters.maxCountFail.fallToDebug=\n\ +\ 注: 以é™ã®ã“ã®ã‚¨ãƒ©ãƒ¼ã®ç™ºç”Ÿã¯DEBUGレベルã§ãƒ­ã‚°ã«å‡ºåŠ›ã•ã‚Œã¾ã™ã€‚ +parameters.multipleDecodingFail=文字ã®ãƒ‡ã‚³ãƒ¼ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ åˆè¨ˆ[{0}]個ã®éšœå®³ãŒæ¤œå‡ºã•ã‚Œã¾ã—ãŸãŒã€æœ€åˆã®ã‚‚ã®ã ã‘ãŒè¨˜éŒ²ã•ã‚Œã¾ã—ãŸã€‚ ã“ã®ãƒ­ã‚¬ãƒ¼ãŒã™ã¹ã¦ã®å¤±æ•—を記録ã™ã‚‹ãŸã‚ã«ã¯ãƒ‡ãƒãƒƒã‚°ãƒ¬ãƒ™ãƒ«ã®ãƒ­ã‚®ãƒ³ã‚°ã‚’有効ã«ã—ã¾ã™ã€‚ +parameters.noequal=[{2}] ã®å€¤ã§ [{0}] ã®ä½ç½®ã‹ã‚‰ [{1}] ã®ä½ç½®ã§çµ‚了ã™ã‚‹ãƒ‘ラメータã«ã¯ã€''=''文字ãŒç¶šã„ã¦ã„ã¾ã›ã‚“ã§ã—㟠+ +rfc6265CookieProcessor.expectedBytes=Cookie を文字列ã¨ã—ã¦è§£æžä¸­ã€æœŸå¾…ã•ã‚Œã‚‹ãƒã‚¤ãƒˆæ•° +rfc6265CookieProcessor.invalidAttributeName=ã“ã®Cookieã«ç„¡åŠ¹ãªå±žæ€§å [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—㟠+rfc6265CookieProcessor.invalidAttributeValue=ã“ã®Cookie属性 [{0}] ã«ç„¡åŠ¹ãªå±žæ€§å€¤ [{1}] ãŒæŒ‡å®šã•ã‚Œã¾ã—㟠+rfc6265CookieProcessor.invalidCharInValue=無効ãªæ–‡å­—[{0}]ãŒCookie値ã«å­˜åœ¨ã—ã¾ã™ã€‚ +rfc6265CookieProcessor.invalidDomain=cookie ã«ä¸æ­£ãªãƒ‰ãƒ¡ã‚¤ãƒ³ [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +rfc6265CookieProcessor.invalidPath=Cookie ã® path 属性ã«ç„¡åŠ¹ãªå€¤ [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ diff --git a/java/org/apache/tomcat/util/http/LocalStrings_ko.properties b/java/org/apache/tomcat/util/http/LocalStrings_ko.properties new file mode 100644 index 0000000..274ec87 --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_ko.properties @@ -0,0 +1,43 @@ +# 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. + +cookies.fallToDebug=\n\ +\ 비고: 쿠키 ì˜¤ë¥˜ë“¤ì´ ë” ë°œìƒí•˜ëŠ” 경우 DEBUG 레벨 로그로 기ë¡ë  것입니다. +cookies.invalidCookieToken=쿠키들: 유효하지 ì•Šì€ ì¿ í‚¤ìž…ë‹ˆë‹¤. 유효한 í† í° ë˜ëŠ” ì¸ìš©ë¶€í˜¸ë¡œ ì²˜ë¦¬ëœ ê°’ì´ ì•„ë‹™ë‹ˆë‹¤. +cookies.invalidSameSiteCookies=ì•Œ 수 없는 설정 ê°’: [{0}]. 반드시 ë‹¤ìŒ ì¤‘ 하나여야 합니다: none, lax, strict. 기본 ê°’ì€ none입니다. +cookies.invalidSpecial=쿠키들: ì•Œ 수 없는 특별한 쿠키 +cookies.maxCountFail=í—ˆìš©ëœ ìµœëŒ€ 쿠키 개수 [{0}]ì„(를) 초과한 ì¿ í‚¤ë“¤ì´ íƒì§€ë˜ì—ˆìŠµë‹ˆë‹¤. + +headers.maxCountFail=최대 허용 í—¤ë” ê°œìˆ˜ [{0}]보다 ë” ë§Žì€ í—¤ë”ë“¤ì´ íƒì§€ë˜ì—ˆìŠµë‹ˆë‹¤. + +parameters.bytes=ìž…ë ¥ [{0}]ì„(를) 사용하여 처리를 시작합니다. +parameters.copyFail=디버그 로그를 위한 ì›ëž˜ì˜ 파ë¼ë¯¸í„° ê°’ë“¤ì„ ë³µì‚¬í•˜ì§€ 못했습니다. +parameters.decodeFail.debug=ë¬¸ìž ë””ì½”ë”© 실패. ê°’ [{1}](으)ë¡œ ì„¤ì •ëœ íŒŒë¼ë¯¸í„° [{0}]ì€(는) 무시ë©ë‹ˆë‹¤. +parameters.decodeFail.info=ë¬¸ìž ë””ì½”ë”©ì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. ê°’ [{1}]ì„(를) 가진 파ë¼ë¯¸í„° [{0}]ì€(는) 무시ë˜ì—ˆìŠµë‹ˆë‹¤. 주ì˜: 여기서 ì¸ìš©ëœ ì´ë¦„ê³¼ ê°’ì€ ë””ì½”ë”© 실패로 ì¸í•´ ë°ì´í„°ê°€ ì†ìƒë˜ì—ˆì„ 수 있습니다. ì†ìƒë˜ì§€ ì•Šì€ ì›ë³¸ ë°ì´í„°ë¥¼ 보시려면, 로그 ë ˆë²¨ì„ ë””ë²„ê·¸ 레벨로 하십시오. +parameters.emptyChunk=빈 파ë¼ë¯¸í„° chunk는 무시ë©ë‹ˆë‹¤. +parameters.fallToDebug=\n\ +\ 비고: 파ë¼ë¯¸í„° ì˜¤ë¥˜ë“¤ì´ ë” ë°œìƒí•˜ëŠ” 경우 DEBUG 레벨 로그로 기ë¡ë  것입니다. +parameters.invalidChunk=[{0}] ë°”ì´íŠ¸ì—ì„œ 시작하고 [{1}] ë°”ì´íŠ¸ì—ì„œ ë나며 ê°’ì´ [{2}]ì¸, 유효하지 ì•Šì€ chunk는 무시ë©ë‹ˆë‹¤. +parameters.maxCountFail=ë‹¨ì¼ ìš”ì²­ ([{0}])ì— í—ˆìš©ë˜ëŠ” 최대 요청 파ë¼ë¯¸í„°ë“¤ì˜ 개수 보다 ë” ë§Žì€ íŒŒë¼ë¯¸í„°ë“¤ì´ íƒì§€ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ í•œê³„ê°’ì„ ì´ˆê³¼í•˜ëŠ” 파ë¼ë¯¸í„°ë“¤ì€ 무시ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ í•œê³„ê°’ì„ ë³€ê²½í•˜ê¸° 위해서는 Connectorì˜ maxParameterCount ì†ì„±ì„ 설정하십시오. +parameters.maxCountFail.fallToDebug=\n\ +\ 비고: ì´ ì˜¤ë¥˜ê°€ ë” ë°œìƒí•˜ëŠ” 경우 DEBUG 레벨 로그로 기ë¡ë  것입니다. +parameters.multipleDecodingFail=ë¬¸ìž ë””ì½”ë”©ì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. ì „ì²´ [{0}]ê°œì˜ ì‹¤íŒ¨ê°€ íƒì§€ë˜ì—ˆì§€ë§Œ, ì˜¤ì§ ì²«ë²ˆì§¸ 실패만 ë¡œê·¸ì— ê¸°ë¡ë˜ì—ˆìŠµë‹ˆë‹¤. 모든 ì‹¤íŒ¨ë“¤ì„ ë¡œê·¸ì— ë‚¨ê¸°ë ¤ë©´ 로그 ë ˆë²¨ì„ ë””ë²„ê·¸ 레벨로 설정하십시오. +parameters.noequal=위치 [{0}]ì—ì„œ 시작하고 위치 [{1}]ì—ì„œ ë나며 ê°’ì´ [{2}]ì¸ íŒŒë¼ë¯¸í„° 다ìŒì—, ''='' 문ìžê°€ 뒤따르지 않습니다. + +rfc6265CookieProcessor.invalidAttributeName=ì´ ì¿ í‚¤ì— ìœ íš¨í•˜ì§€ ì•Šì€ ì†ì„± ì´ë¦„ [{0}](ì´)ê°€ 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +rfc6265CookieProcessor.invalidAttributeValue=ì´ ì¿ í‚¤ ì†ì„± [{0}]ì— ìœ íš¨í•˜ì§€ ì•Šì€ ì†ì„± ê°’ [{1}](ì´)ê°€ 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +rfc6265CookieProcessor.invalidCharInValue=쿠키 ê°’ì— ìœ íš¨í•˜ì§€ ì•Šì€ ë¬¸ìž [{0}]ì´(ê°€) 있습니다. +rfc6265CookieProcessor.invalidDomain=ì´ ì¿ í‚¤ë¥¼ 위해, 유효하지 ì•Šì€ ë„ë©”ì¸ [{0}]ì´(ê°€) 지정ë˜ì—ˆìŠµë‹ˆë‹¤. +rfc6265CookieProcessor.invalidPath=ì´ ì¿ í‚¤ë¥¼ 위해 유효하지 ì•Šì€ ê²½ë¡œê°€ 설정ë˜ì—ˆìŠµë‹ˆë‹¤: [{0}] diff --git a/java/org/apache/tomcat/util/http/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/http/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..9d295c7 --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_pt_BR.properties @@ -0,0 +1,17 @@ +# 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. + +parameters.fallToDebug=\n\ +\ Nota: próximas ocorrências do parâmetro erro serão registradas no log do tipo DEBUG diff --git a/java/org/apache/tomcat/util/http/LocalStrings_ru.properties b/java/org/apache/tomcat/util/http/LocalStrings_ru.properties new file mode 100644 index 0000000..82641c9 --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_ru.properties @@ -0,0 +1,18 @@ +# 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. + +parameters.copyFail=Ðевозможно Ñоздать копию оригинальных значений параметров Ð´Ð»Ñ Ð»Ð¾Ð³Ð³Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ + +rfc6265CookieProcessor.invalidPath=Ð”Ð»Ñ Ñтой куки был указан некорректный путь [{0}] diff --git a/java/org/apache/tomcat/util/http/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/http/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..cee2400 --- /dev/null +++ b/java/org/apache/tomcat/util/http/LocalStrings_zh_CN.properties @@ -0,0 +1,43 @@ +# 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. + +cookies.fallToDebug=\n\ +\ 注æ„:将在调试级别记录进一步出现的Cookie错误。 +cookies.invalidCookieToken=Cookie:cookie无效。值ä¸æ˜¯ä»¤ç‰Œæˆ–引用值 +cookies.invalidSameSiteCookies=未知设置[{0}],必须是以下之一:unsetã€noneã€laxã€strict。默认值为unset。 +cookies.invalidSpecial=Cookies:未知特殊的Cookie +cookies.maxCountFail=检测到超过Cookie最大å…许的数é‡[{0}] + +headers.maxCountFail=检测到超过了å…许设置的最大header æ•°[{0}] + +parameters.bytes=开始处ç†è¾“å…¥[{0}] +parameters.copyFail=无法创建以调试日志记录为目的的原始å‚数值的副本 +parameters.decodeFail.debug=字符解ç å¤±è´¥.å‚æ•° [{0}]和值 [{1}]被忽略 +parameters.decodeFail.info=字符解ç å¤±è´¥ã€‚值为[{1}]çš„å‚æ•°[{0}]已被忽略。请注æ„,此处引用的å称和值å¯èƒ½ç”±äºŽè§£ç å¤±è´¥è€ŒæŸå。使用调试级别日志记录查看原始的ã€æœªæŸå的值。 +parameters.emptyChunk=忽略空å‚æ•°å— +parameters.fallToDebug=\n\ +\ 注:更多的å‚数错误将以DEBUG级别日志进行记录。 +parameters.invalidChunk=从字节[{0}]开始到字节[{1}]结æŸçš„无效å—,忽略值[{2}] +parameters.maxCountFail=检测到å•ä¸ªè¯·æ±‚([{0}])的最大请求å‚数数(GET加POST)。 超出此é™åˆ¶çš„任何å‚数都被忽略。 è¦æ›´æ”¹æ­¤é™åˆ¶ï¼Œè¯·åœ¨Connector上设置maxParameterCount属性。 +parameters.maxCountFail.fallToDebug=\n\ +\ 注æ„:更多的错误信æ¯åªåœ¨debug级别日志中记录 +parameters.multipleDecodingFail=字符解ç å¤±è´¥ã€‚总共检测到[{0}]个失败,但åªè®°å½•äº†ç¬¬ä¸€ä¸ªå¤±è´¥ã€‚为此记录器å¯ç”¨è°ƒè¯•çº§åˆ«æ—¥å¿—记录以记录所有故障。 +parameters.noequal=):å‚数从ä½ç½®[{0}]开始,到ä½ç½®[{1}]结æŸï¼Œå€¼ä¸º[{2}],åŽé¢æ²¡æœ‰â€œ=â€å­—符 + +rfc6265CookieProcessor.invalidAttributeName=cookie中包å«æ— æ•ˆå±žæ€§å[{0}] +rfc6265CookieProcessor.invalidAttributeValue=cookie属性[{0}]包å«æ— æ•ˆå±žæ€§å€¼[{1}] +rfc6265CookieProcessor.invalidCharInValue=Cookie值中存在无效字符[{0}] +rfc6265CookieProcessor.invalidDomain=为此cookie指定的域[{0}]无效 +rfc6265CookieProcessor.invalidPath=这个cookie被指定了一个无效的路径 [{0}] diff --git a/java/org/apache/tomcat/util/http/MimeHeaders.java b/java/org/apache/tomcat/util/http/MimeHeaders.java new file mode 100644 index 0000000..070a709 --- /dev/null +++ b/java/org/apache/tomcat/util/http/MimeHeaders.java @@ -0,0 +1,524 @@ +/* + * 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.tomcat.util.http; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Enumeration; + +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.res.StringManager; + +/** + * Memory-efficient repository for Mime Headers. When the object is recycled, it will keep the allocated headers[] and + * all the MimeHeaderField - no GC is generated. + *

    + * For input headers it is possible to use the MessageByte for Fields - so no GC will be generated. + *

    + * The only garbage is generated when using the String for header names/values - this can't be avoided when the servlet + * calls header methods, but is easy to avoid inside tomcat. The goal is to use _only_ MessageByte-based Fields, and + * reduce to 0 the memory overhead of tomcat. + *

    + * This class is used to contain standard internet message headers, + * used for SMTP (RFC822) and HTTP (RFC2068) messages as well as for + * MIME (RFC 2045) applications such as transferring typed data and + * grouping related items in multipart message bodies. + *

    + * Message headers, as specified in RFC822, include a field name + * and a field body. Order has no semantic significance, and several + * fields with the same name may exist. However, most fields do not + * (and should not) exist more than once in a header. + *

    + * Many kinds of field body must conform to a specified syntax, + * including the standard parenthesized comment syntax. This class + * supports only two simple syntaxes, for dates and integers. + *

    + * When processing headers, care must be taken to handle the case of + * multiple same-name fields correctly. The values of such fields are + * only available as strings. They may be accessed by index (treating + * the header as an array of fields), or by name (returning an array + * of string values). + *

    + * Headers are first parsed and stored in the order they are + * received. This is based on the fact that most servlets will not + * directly access all headers, and most headers are single-valued. + * (the alternative - a hash or similar data structure - will add + * an overhead that is not needed in most cases) + *

    + * Apache seems to be using a similar method for storing and manipulating + * headers. + * + * @author dac@eng.sun.com + * @author James Todd [gonzo@eng.sun.com] + * @author Costin Manolache + * @author kevin seguin + */ +public class MimeHeaders { + + /** + * Initial size - should be == average number of headers per request + */ + public static final int DEFAULT_HEADER_SIZE = 8; + + private static final StringManager sm = StringManager.getManager("org.apache.tomcat.util.http"); + + /** + * The header fields. + */ + private MimeHeaderField[] headers = new MimeHeaderField[DEFAULT_HEADER_SIZE]; + + /** + * The current number of header fields. + */ + private int count; + + /** + * The limit on the number of header fields. + */ + private int limit = -1; + + /** + * Creates a new MimeHeaders object using a default buffer size. + */ + public MimeHeaders() { + // NO-OP + } + + /** + * Set limit on the number of header fields. + * + * @param limit The new limit + */ + public void setLimit(int limit) { + this.limit = limit; + if (limit > 0 && headers.length > limit && count < limit) { + // shrink header list array + MimeHeaderField tmp[] = new MimeHeaderField[limit]; + System.arraycopy(headers, 0, tmp, 0, count); + headers = tmp; + } + } + + /** + * Clears all header fields. + */ + public void recycle() { + for (int i = 0; i < count; i++) { + headers[i].recycle(); + } + count = 0; + } + + @Deprecated + public void clear() { + recycle(); + } + + @Override + public String toString() { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + pw.println("=== MimeHeaders ==="); + Enumeration e = names(); + while (e.hasMoreElements()) { + String n = e.nextElement(); + Enumeration ev = values(n); + while (ev.hasMoreElements()) { + pw.print(n); + pw.print(" = "); + pw.println(ev.nextElement()); + } + } + return sw.toString(); + } + + + public void duplicate(MimeHeaders source) throws IOException { + for (int i = 0; i < source.size(); i++) { + MimeHeaderField mhf = createHeader(); + mhf.getName().duplicate(source.getName(i)); + mhf.getValue().duplicate(source.getValue(i)); + } + } + + + // -------------------- Idx access to headers ---------- + + /** + * @return the current number of header fields. + */ + public int size() { + return count; + } + + /** + * @param n The header index + * + * @return the Nth header name, or null if there is no such header. This may be used to iterate through all header + * fields. + */ + public MessageBytes getName(int n) { + return n >= 0 && n < count ? headers[n].getName() : null; + } + + /** + * @param n The header index + * + * @return the Nth header value, or null if there is no such header. This may be used to iterate through all header + * fields. + */ + public MessageBytes getValue(int n) { + return n >= 0 && n < count ? headers[n].getValue() : null; + } + + /** + * Find the index of a header with the given name. + * + * @param name The header name + * @param starting Index on which to start looking + * + * @return the header index + */ + public int findHeader(String name, int starting) { + // We can use a hash - but it's not clear how much + // benefit you can get - there is an overhead + // and the number of headers is small (4-5 ?) + // Another problem is that we'll pay the overhead + // of constructing the hashtable + + // A custom search tree may be better + for (int i = starting; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + return i; + } + } + return -1; + } + + // -------------------- -------------------- + + /** + * Returns an enumeration of strings representing the header field names. Field names may appear multiple times in + * this enumeration, indicating that multiple fields with that name exist in this header. + * + * @return the enumeration + */ + public Enumeration names() { + return new NamesEnumerator(this); + } + + public Enumeration values(String name) { + return new ValuesEnumerator(this, name); + } + + // -------------------- Adding headers -------------------- + + + /** + * Adds a partially constructed field to the header. This field has not had its name or value initialized. + */ + private MimeHeaderField createHeader() { + if (limit > -1 && count >= limit) { + throw new IllegalStateException(sm.getString("headers.maxCountFail", Integer.valueOf(limit))); + } + MimeHeaderField mh; + int len = headers.length; + if (count >= len) { + // expand header list array + int newLength = count * 2; + if (limit > 0 && newLength > limit) { + newLength = limit; + } + MimeHeaderField tmp[] = new MimeHeaderField[newLength]; + System.arraycopy(headers, 0, tmp, 0, len); + headers = tmp; + } + if ((mh = headers[count]) == null) { + headers[count] = mh = new MimeHeaderField(); + } + count++; + return mh; + } + + /** + * Create a new named header , return the MessageBytes container for the new value + * + * @param name The header name + * + * @return the message bytes container for the value + */ + public MessageBytes addValue(String name) { + MimeHeaderField mh = createHeader(); + mh.getName().setString(name); + return mh.getValue(); + } + + /** + * Create a new named header using un-translated byte[]. The conversion to chars can be delayed until encoding is + * known. + * + * @param b The header name bytes + * @param startN Offset + * @param len Length + * + * @return the message bytes container for the value + */ + public MessageBytes addValue(byte b[], int startN, int len) { + MimeHeaderField mhf = createHeader(); + mhf.getName().setBytes(b, startN, len); + return mhf.getValue(); + } + + /** + * Allow "set" operations, which removes all current values for this header. + * + * @param name The header name + * + * @return the message bytes container for the value + */ + public MessageBytes setValue(String name) { + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + for (int j = i + 1; j < count; j++) { + if (headers[j].getName().equalsIgnoreCase(name)) { + removeHeader(j--); + } + } + return headers[i].getValue(); + } + } + MimeHeaderField mh = createHeader(); + mh.getName().setString(name); + return mh.getValue(); + } + + // -------------------- Getting headers -------------------- + + /** + * Finds and returns a header field with the given name. If no such field exists, null is returned. If more than one + * such field is in the header, an arbitrary one is returned. + * + * @param name The header name + * + * @return the value + */ + public MessageBytes getValue(String name) { + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + return headers[i].getValue(); + } + } + return null; + } + + /** + * Finds and returns a unique header field with the given name. If no such field exists, null is returned. If the + * specified header field is not unique then an {@link IllegalArgumentException} is thrown. + * + * @param name The header name + * + * @return the value if unique + * + * @throws IllegalArgumentException if the header has multiple values + */ + public MessageBytes getUniqueValue(String name) { + MessageBytes result = null; + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + if (result == null) { + result = headers[i].getValue(); + } else { + throw new IllegalArgumentException(); + } + } + } + return result; + } + + public String getHeader(String name) { + MessageBytes mh = getValue(name); + return mh != null ? mh.toString() : null; + } + + // -------------------- Removing -------------------- + + /** + * Removes a header field with the specified name. Does nothing if such a field could not be found. + * + * @param name the name of the header field to be removed + */ + public void removeHeader(String name) { + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + removeHeader(i--); + } + } + } + + /** + * Reset, move to the end and then reduce count by 1. + * + * @param idx the index of the header to remove. + */ + public void removeHeader(int idx) { + // Implementation note. This method must not change the order of the + // remaining headers because, if there are multiple header values for + // the same name, the order of those headers is significant. It is + // simpler to retain order for all values than try to determine if there + // are multiple header values for the same name. + + // Clear the header to remove + MimeHeaderField mh = headers[idx]; + mh.recycle(); + + // Move the remaining headers + System.arraycopy(headers, idx + 1, headers, idx, count - idx - 1); + + // Place the removed header at the end + headers[count - 1] = mh; + + // Reduce the count + count--; + } + +} + +/** + * Enumerate the distinct header names. Each nextElement() is O(n) ( a comparison is done with all previous elements ). + * This is less frequent than add() - we want to keep add O(1). + */ +class NamesEnumerator implements Enumeration { + private int pos; + private final int size; + private String next; + private final MimeHeaders headers; + + NamesEnumerator(MimeHeaders headers) { + this.headers = headers; + pos = 0; + size = headers.size(); + findNext(); + } + + private void findNext() { + next = null; + for (; pos < size; pos++) { + next = headers.getName(pos).toStringType(); + for (int j = 0; j < pos; j++) { + if (headers.getName(j).equalsIgnoreCase(next)) { + // duplicate. + next = null; + break; + } + } + if (next != null) { + // it's not a duplicate + break; + } + } + // next time findNext is called it will try the + // next element + pos++; + } + + @Override + public boolean hasMoreElements() { + return next != null; + } + + @Override + public String nextElement() { + String current = next; + findNext(); + return current; + } +} + +/** + * Enumerate the values for a (possibly ) multiple value element. + */ +class ValuesEnumerator implements Enumeration { + private int pos; + private final int size; + private MessageBytes next; + private final MimeHeaders headers; + private final String name; + + ValuesEnumerator(MimeHeaders headers, String name) { + this.name = name; + this.headers = headers; + pos = 0; + size = headers.size(); + findNext(); + } + + private void findNext() { + next = null; + for (; pos < size; pos++) { + MessageBytes n1 = headers.getName(pos); + if (n1.equalsIgnoreCase(name)) { + next = headers.getValue(pos); + break; + } + } + pos++; + } + + @Override + public boolean hasMoreElements() { + return next != null; + } + + @Override + public String nextElement() { + MessageBytes current = next; + findNext(); + return current.toStringType(); + } +} + +class MimeHeaderField { + + private final MessageBytes nameB = MessageBytes.newInstance(); + private final MessageBytes valueB = MessageBytes.newInstance(); + + /** + * Creates a new, uninitialized header field. + */ + MimeHeaderField() { + // NO-OP + } + + public void recycle() { + nameB.recycle(); + valueB.recycle(); + } + + public MessageBytes getName() { + return nameB; + } + + public MessageBytes getValue() { + return valueB; + } + + @Override + public String toString() { + return nameB + ": " + valueB; + } +} diff --git a/java/org/apache/tomcat/util/http/Parameters.java b/java/org/apache/tomcat/util/http/Parameters.java new file mode 100644 index 0000000..31b8abe --- /dev/null +++ b/java/org/apache/tomcat/util/http/Parameters.java @@ -0,0 +1,517 @@ +/* + * 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.tomcat.util.http; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.log.UserDataHelper; +import org.apache.tomcat.util.res.StringManager; + +public final class Parameters { + + private static final Log log = LogFactory.getLog(Parameters.class); + + private static final UserDataHelper userDataLog = new UserDataHelper(log); + + private static final UserDataHelper maxParamCountLog = new UserDataHelper(log); + + private static final StringManager sm = StringManager.getManager("org.apache.tomcat.util.http"); + + private final Map> paramHashValues = new LinkedHashMap<>(); + private boolean didQueryParameters = false; + + private MessageBytes queryMB; + + private UDecoder urlDec; + private final MessageBytes decodedQuery = MessageBytes.newInstance(); + + private Charset charset = StandardCharsets.ISO_8859_1; + private Charset queryStringCharset = StandardCharsets.UTF_8; + + private int limit = -1; + private int parameterCount = 0; + + /** + * Set to the reason for the failure (the first failure if there is more than one) if there were failures during + * parameter parsing. + */ + private FailReason parseFailedReason = null; + + public Parameters() { + // NO-OP + } + + public void setQuery(MessageBytes queryMB) { + this.queryMB = queryMB; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + public Charset getCharset() { + return charset; + } + + public void setCharset(Charset charset) { + if (charset == null) { + charset = DEFAULT_BODY_CHARSET; + } + this.charset = charset; + if (log.isTraceEnabled()) { + log.trace("Set encoding to " + charset.name()); + } + } + + public void setQueryStringCharset(Charset queryStringCharset) { + if (queryStringCharset == null) { + queryStringCharset = DEFAULT_URI_CHARSET; + } + this.queryStringCharset = queryStringCharset; + + if (log.isTraceEnabled()) { + log.trace("Set query string encoding to " + queryStringCharset.name()); + } + } + + + public boolean isParseFailed() { + return parseFailedReason != null; + } + + + public FailReason getParseFailedReason() { + return parseFailedReason; + } + + + public void setParseFailedReason(FailReason failReason) { + if (this.parseFailedReason == null) { + this.parseFailedReason = failReason; + } + } + + + public int size() { + return parameterCount; + } + + + public void recycle() { + parameterCount = 0; + paramHashValues.clear(); + didQueryParameters = false; + charset = DEFAULT_BODY_CHARSET; + decodedQuery.recycle(); + parseFailedReason = null; + } + + + // -------------------- Data access -------------------- + // Access to the current name/values, no side effect ( processing ). + // You must explicitly call handleQueryParameters and the post methods. + + public String[] getParameterValues(String name) { + handleQueryParameters(); + // no "facade" + ArrayList values = paramHashValues.get(name); + if (values == null) { + return null; + } + return values.toArray(new String[0]); + } + + public Enumeration getParameterNames() { + handleQueryParameters(); + return Collections.enumeration(paramHashValues.keySet()); + } + + public String getParameter(String name) { + handleQueryParameters(); + ArrayList values = paramHashValues.get(name); + if (values != null) { + if (values.size() == 0) { + return ""; + } + return values.get(0); + } else { + return null; + } + } + + // -------------------- Processing -------------------- + /** + * Process the query string into parameters + */ + public void handleQueryParameters() { + if (didQueryParameters) { + return; + } + + didQueryParameters = true; + + if (queryMB == null || queryMB.isNull()) { + return; + } + + if (log.isTraceEnabled()) { + log.trace("Decoding query " + decodedQuery + " " + queryStringCharset.name()); + } + + try { + decodedQuery.duplicate(queryMB); + } catch (IOException e) { + // Can't happen, as decodedQuery can't overflow + log.error(sm.getString("parameters.copyFail"), e); + } + processParameters(decodedQuery, queryStringCharset); + } + + + public void addParameter(String key, String value) throws IllegalStateException { + + if (key == null) { + return; + } + + if (limit > -1 && parameterCount >= limit) { + // Processing this parameter will push us over the limit. ISE is + // what Request.parseParts() uses for requests that are too big + setParseFailedReason(FailReason.TOO_MANY_PARAMETERS); + throw new IllegalStateException(sm.getString("parameters.maxCountFail", Integer.valueOf(limit))); + } + parameterCount++; + + paramHashValues.computeIfAbsent(key, k -> new ArrayList<>(1)).add(value); + } + + public void setURLDecoder(UDecoder u) { + urlDec = u; + } + + // -------------------- Parameter parsing -------------------- + // we are called from a single thread - we can do it the hard way + // if needed + private final ByteChunk tmpName = new ByteChunk(); + private final ByteChunk tmpValue = new ByteChunk(); + private final ByteChunk origName = new ByteChunk(); + private final ByteChunk origValue = new ByteChunk(); + private static final Charset DEFAULT_BODY_CHARSET = StandardCharsets.ISO_8859_1; + private static final Charset DEFAULT_URI_CHARSET = StandardCharsets.UTF_8; + + + public void processParameters(byte bytes[], int start, int len) { + processParameters(bytes, start, len, charset); + } + + private void processParameters(byte bytes[], int start, int len, Charset charset) { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("parameters.bytes", new String(bytes, start, len, DEFAULT_BODY_CHARSET))); + } + + int decodeFailCount = 0; + + int pos = start; + int end = start + len; + + while (pos < end) { + int nameStart = pos; + int nameEnd = -1; + int valueStart = -1; + int valueEnd = -1; + + boolean parsingName = true; + boolean decodeName = false; + boolean decodeValue = false; + boolean parameterComplete = false; + + do { + switch (bytes[pos]) { + case '=': + if (parsingName) { + // Name finished. Value starts from next character + nameEnd = pos; + parsingName = false; + valueStart = ++pos; + } else { + // Equals character in value + pos++; + } + break; + case '&': + if (parsingName) { + // Name finished. No value. + nameEnd = pos; + } else { + // Value finished + valueEnd = pos; + } + parameterComplete = true; + pos++; + break; + case '%': + case '+': + // Decoding required + if (parsingName) { + decodeName = true; + } else { + decodeValue = true; + } + pos++; + break; + default: + pos++; + break; + } + } while (!parameterComplete && pos < end); + + if (pos == end) { + if (nameEnd == -1) { + nameEnd = pos; + } else if (valueStart > -1 && valueEnd == -1) { + valueEnd = pos; + } + } + + if (log.isDebugEnabled() && valueStart == -1) { + log.debug(sm.getString("parameters.noequal", Integer.valueOf(nameStart), Integer.valueOf(nameEnd), + new String(bytes, nameStart, nameEnd - nameStart, DEFAULT_BODY_CHARSET))); + } + + if (nameEnd <= nameStart) { + if (valueStart == -1) { + // && + if (log.isDebugEnabled()) { + log.debug(sm.getString("parameters.emptyChunk")); + } + // Do not flag as error + continue; + } + // &=foo& + UserDataHelper.Mode logMode = userDataLog.getNextMode(); + if (logMode != null) { + String extract; + if (valueEnd > nameStart) { + extract = new String(bytes, nameStart, valueEnd - nameStart, DEFAULT_BODY_CHARSET); + } else { + extract = ""; + } + String message = sm.getString("parameters.invalidChunk", Integer.valueOf(nameStart), + Integer.valueOf(valueEnd), extract); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("parameters.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + log.info(message); + break; + case DEBUG: + log.debug(message); + } + } + setParseFailedReason(FailReason.NO_NAME); + continue; + // invalid chunk - it's better to ignore + } + + tmpName.setBytes(bytes, nameStart, nameEnd - nameStart); + if (valueStart >= 0) { + tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart); + } else { + tmpValue.setBytes(bytes, 0, 0); + } + + // Take copies as if anything goes wrong originals will be + // corrupted. This means original values can be logged. + // For performance - only done for debug + if (log.isDebugEnabled()) { + try { + origName.append(bytes, nameStart, nameEnd - nameStart); + if (valueStart >= 0) { + origValue.append(bytes, valueStart, valueEnd - valueStart); + } else { + origValue.append(bytes, 0, 0); + } + } catch (IOException ioe) { + // Should never happen... + log.error(sm.getString("parameters.copyFail"), ioe); + } + } + + try { + String name; + String value; + + if (decodeName) { + urlDecode(tmpName); + } + tmpName.setCharset(charset); + name = tmpName.toString(); + + if (valueStart >= 0) { + if (decodeValue) { + urlDecode(tmpValue); + } + tmpValue.setCharset(charset); + value = tmpValue.toString(); + } else { + value = ""; + } + + try { + addParameter(name, value); + } catch (IllegalStateException ise) { + // Hitting limit stops processing further params but does + // not cause request to fail. + UserDataHelper.Mode logMode = maxParamCountLog.getNextMode(); + if (logMode != null) { + String message = ise.getMessage(); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("parameters.maxCountFail.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + log.info(message); + break; + case DEBUG: + log.debug(message); + } + } + break; + } + } catch (IOException e) { + setParseFailedReason(FailReason.URL_DECODING); + decodeFailCount++; + if (decodeFailCount == 1 || log.isDebugEnabled()) { + if (log.isDebugEnabled()) { + log.debug( + sm.getString("parameters.decodeFail.debug", origName.toString(), origValue.toString()), + e); + } else if (log.isInfoEnabled()) { + UserDataHelper.Mode logMode = userDataLog.getNextMode(); + if (logMode != null) { + String message = + sm.getString("parameters.decodeFail.info", tmpName.toString(), tmpValue.toString()); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("parameters.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + log.info(message); + break; + case DEBUG: + log.debug(message); + } + } + } + } + } + + tmpName.recycle(); + tmpValue.recycle(); + // Only recycle copies if we used them + if (log.isDebugEnabled()) { + origName.recycle(); + origValue.recycle(); + } + } + + if (decodeFailCount > 1 && !log.isDebugEnabled()) { + UserDataHelper.Mode logMode = userDataLog.getNextMode(); + if (logMode != null) { + String message = sm.getString("parameters.multipleDecodingFail", Integer.valueOf(decodeFailCount)); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("parameters.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + log.info(message); + break; + case DEBUG: + log.debug(message); + } + } + } + } + + private void urlDecode(ByteChunk bc) throws IOException { + if (urlDec == null) { + urlDec = new UDecoder(); + } + urlDec.convert(bc, true); + } + + public void processParameters(MessageBytes data, Charset charset) { + if (data == null || data.isNull() || data.getLength() <= 0) { + return; + } + + if (data.getType() != MessageBytes.T_BYTES) { + data.toBytes(); + } + ByteChunk bc = data.getByteChunk(); + processParameters(bc.getBytes(), bc.getOffset(), bc.getLength(), charset); + } + + /** + * Debug purpose + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Map.Entry> e : paramHashValues.entrySet()) { + sb.append(e.getKey()).append('='); + StringUtils.join(e.getValue(), ',', sb); + sb.append('\n'); + } + return sb.toString(); + } + + + public enum FailReason { + CLIENT_DISCONNECT, + MULTIPART_CONFIG_INVALID, + INVALID_CONTENT_TYPE, + IO_ERROR, + NO_NAME, + POST_TOO_LARGE, + /** + * Same as {@link #CLIENT_DISCONNECT}. + * + * @deprecated Unused. Will be removed in Tomcat 11.0.x onwards + */ + @Deprecated + REQUEST_BODY_INCOMPLETE, + TOO_MANY_PARAMETERS, + UNKNOWN, + URL_DECODING + } +} diff --git a/java/org/apache/tomcat/util/http/RequestUtil.java b/java/org/apache/tomcat/util/http/RequestUtil.java new file mode 100644 index 0000000..1c1ed0d --- /dev/null +++ b/java/org/apache/tomcat/util/http/RequestUtil.java @@ -0,0 +1,210 @@ +/* + * 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.tomcat.util.http; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; + +import jakarta.servlet.http.HttpServletRequest; + +public class RequestUtil { + + private RequestUtil() { + // Hide default constructor as this is a utility class + } + + + /** + * Normalize a relative URI path that may have relative values ("/./", "/../", and so on ) it it. + * WARNING - This method is useful only for normalizing application-generated paths. It does not + * try to perform security checks for malicious input. + * + * @param path Relative path to be normalized + * + * @return The normalized path or null if the path cannot be normalized + */ + public static String normalize(String path) { + return normalize(path, true); + } + + + /** + * Normalize a relative URI path that may have relative values ("/./", "/../", and so on ) it it. + * WARNING - This method is useful only for normalizing application-generated paths. It does not + * try to perform security checks for malicious input. + * + * @param path Relative path to be normalized + * @param replaceBackSlash Should '\\' be replaced with '/' + * + * @return The normalized path or null if the path cannot be normalized + */ + public static String normalize(String path, boolean replaceBackSlash) { + + if (path == null) { + return null; + } + + // Create a place for the normalized path + String normalized = path; + + if (replaceBackSlash && normalized.indexOf('\\') >= 0) { + normalized = normalized.replace('\\', '/'); + } + + // Add a leading "/" if necessary + if (!normalized.startsWith("/")) { + normalized = "/" + normalized; + } + + boolean addedTrailingSlash = false; + if (normalized.endsWith("/.") || normalized.endsWith("/..")) { + normalized = normalized + "/"; + addedTrailingSlash = true; + } + + // Resolve occurrences of "//" in the normalized path + while (true) { + int index = normalized.indexOf("//"); + if (index < 0) { + break; + } + normalized = normalized.substring(0, index) + normalized.substring(index + 1); + } + + // Resolve occurrences of "/./" in the normalized path + while (true) { + int index = normalized.indexOf("/./"); + if (index < 0) { + break; + } + normalized = normalized.substring(0, index) + normalized.substring(index + 2); + } + + // Resolve occurrences of "/../" in the normalized path + while (true) { + int index = normalized.indexOf("/../"); + if (index < 0) { + break; + } + if (index == 0) { + return null; // Trying to go outside our context + } + int index2 = normalized.lastIndexOf('/', index - 1); + normalized = normalized.substring(0, index2) + normalized.substring(index + 3); + } + + if (normalized.length() > 1 && addedTrailingSlash) { + // Remove the trailing '/' we added to that input and output are + // consistent w.r.t. to the presence of the trailing '/'. + normalized = normalized.substring(0, normalized.length() - 1); + } + + // Return the normalized path that we have completed + return normalized; + } + + + public static boolean isSameOrigin(HttpServletRequest request, String origin) { + // Build scheme://host:port from request + StringBuilder target = new StringBuilder(); + String scheme = request.getScheme(); + if (scheme == null) { + return false; + } else { + scheme = scheme.toLowerCase(Locale.ENGLISH); + } + target.append(scheme); + target.append("://"); + + String host = request.getServerName(); + if (host == null) { + return false; + } + target.append(host); + + int port = request.getServerPort(); + // Origin may or may not include the (default) port. + // At this point target doesn't include a port. + if (target.length() == origin.length()) { + // origin and target can only be equal if both are using default + // ports. Therefore only append the port to the target if a + // non-default port is used. + if (("http".equals(scheme) || "ws".equals(scheme)) && port != 80 || + ("https".equals(scheme) || "wss".equals(scheme)) && port != 443) { + target.append(':'); + target.append(port); + } + } else { + // origin and target can only be equal if: + // a) origin includes an explicit default port + // b) origin is using a non-default port + // Either way, add the port to the target so it can be compared + target.append(':'); + target.append(port); + } + + + // Both scheme and host are case-insensitive but the CORS spec states + // this check should be case-sensitive + return origin.equals(target.toString()); + } + + + /** + * Checks if a given origin is valid or not. Criteria: + *

      + *
    • If an encoded character is present in origin, it's not valid.
    • + *
    • If origin is "null", it's valid.
    • + *
    • Origin should be a valid {@link URI}
    • + *
    + * + * @param origin The origin URI + * + * @return true if the origin was valid + * + * @see RFC952 + */ + public static boolean isValidOrigin(String origin) { + // Checks for encoded characters. Helps prevent CRLF injection. + if (origin.contains("%")) { + return false; + } + + // "null" is a valid origin + if ("null".equals(origin)) { + return true; + } + + // RFC6454, section 4. "If uri-scheme is file, the implementation MAY + // return an implementation-defined value.". No limits are placed on + // that value so treat all file URIs as valid origins. + if (origin.startsWith("file://")) { + return true; + } + + URI originURI; + try { + originURI = new URI(origin); + } catch (URISyntaxException e) { + return false; + } + // If scheme for URI is null, return false. Return true otherwise. + return originURI.getScheme() != null; + + } +} diff --git a/java/org/apache/tomcat/util/http/ResponseUtil.java b/java/org/apache/tomcat/util/http/ResponseUtil.java new file mode 100644 index 0000000..c767f12 --- /dev/null +++ b/java/org/apache/tomcat/util/http/ResponseUtil.java @@ -0,0 +1,172 @@ +/* + * 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.tomcat.util.http; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.tomcat.util.http.parser.TokenList; + +public class ResponseUtil { + + private static final String VARY_HEADER = "vary"; + private static final String VARY_ALL = "*"; + + private ResponseUtil() { + // Utility class. Hide default constructor. + } + + + public static void addVaryFieldName(MimeHeaders headers, String name) { + addVaryFieldName(new HeaderAdapter(headers), name); + } + + + public static void addVaryFieldName(HttpServletResponse response, String name) { + addVaryFieldName(new ResponseAdapter(response), name); + } + + + private static void addVaryFieldName(Adapter adapter, String name) { + + Collection varyHeaders = adapter.getHeaders(VARY_HEADER); + + // Short-cut if only * has been set + if (varyHeaders.size() == 1 && varyHeaders.iterator().next().trim().equals(VARY_ALL)) { + // No need to add an additional field + return; + } + + // Short-cut if no headers have been set + if (varyHeaders.size() == 0) { + adapter.addHeader(VARY_HEADER, name); + return; + } + + // Short-cut if "*" is added + if (VARY_ALL.equals(name.trim())) { + adapter.setHeader(VARY_HEADER, VARY_ALL); + return; + } + + // May be dealing with an application set header, or multiple headers. + // Header names overlap so can't use String.contains(). Have to parse + // the existing values, check if the new value is already present and + // then add it if not. The good news is field names are tokens which + // makes parsing simpler. + LinkedHashSet fieldNames = new LinkedHashSet<>(); + + for (String varyHeader : varyHeaders) { + StringReader input = new StringReader(varyHeader); + try { + TokenList.parseTokenList(input, fieldNames); + } catch (IOException ioe) { + // Should never happen + } + } + + if (fieldNames.contains(VARY_ALL)) { + // '*' has been added without removing other values. Optimise. + adapter.setHeader(VARY_HEADER, VARY_ALL); + return; + } + + // Build single header to replace current multiple headers + // Replace existing header(s) to ensure any invalid values are removed + fieldNames.add(name); + StringBuilder varyHeader = new StringBuilder(); + Iterator iter = fieldNames.iterator(); + // There must be at least one value as one is added just above + varyHeader.append(iter.next()); + while (iter.hasNext()) { + varyHeader.append(','); + varyHeader.append(iter.next()); + } + adapter.setHeader(VARY_HEADER, varyHeader.toString()); + } + + + private interface Adapter { + + Collection getHeaders(String name); + + void setHeader(String name, String value); + + void addHeader(String name, String value); + } + + + private static final class HeaderAdapter implements Adapter { + private final MimeHeaders headers; + + HeaderAdapter(MimeHeaders headers) { + this.headers = headers; + } + + @Override + public Collection getHeaders(String name) { + Enumeration values = headers.values(name); + List result = new ArrayList<>(); + while (values.hasMoreElements()) { + result.add(values.nextElement()); + } + return result; + } + + @Override + public void setHeader(String name, String value) { + headers.setValue(name).setString(value); + } + + @Override + public void addHeader(String name, String value) { + headers.addValue(name).setString(value); + } + } + + + private static final class ResponseAdapter implements Adapter { + private final HttpServletResponse response; + + ResponseAdapter(HttpServletResponse response) { + this.response = response; + } + + @Override + public Collection getHeaders(String name) { + return response.getHeaders(name); + } + + @Override + public void setHeader(String name, String value) { + response.setHeader(name, value); + } + + @Override + public void addHeader(String name, String value) { + response.addHeader(name, value); + } + } +} diff --git a/java/org/apache/tomcat/util/http/Rfc6265CookieProcessor.java b/java/org/apache/tomcat/util/http/Rfc6265CookieProcessor.java new file mode 100644 index 0000000..76fa906 --- /dev/null +++ b/java/org/apache/tomcat/util/http/Rfc6265CookieProcessor.java @@ -0,0 +1,291 @@ +/* + * 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.tomcat.util.http; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.text.FieldPosition; +import java.util.BitSet; +import java.util.Date; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.descriptor.web.Constants; +import org.apache.tomcat.util.http.parser.Cookie; +import org.apache.tomcat.util.http.parser.HttpParser; +import org.apache.tomcat.util.res.StringManager; + +public class Rfc6265CookieProcessor extends CookieProcessorBase { + + private static final Log log = LogFactory.getLog(Rfc6265CookieProcessor.class); + + private static final StringManager sm = + StringManager.getManager(Rfc6265CookieProcessor.class.getPackage().getName()); + + private static final BitSet domainValid = new BitSet(128); + + static { + for (char c = '0'; c <= '9'; c++) { + domainValid.set(c); + } + for (char c = 'a'; c <= 'z'; c++) { + domainValid.set(c); + } + for (char c = 'A'; c <= 'Z'; c++) { + domainValid.set(c); + } + domainValid.set('.'); + domainValid.set('-'); + } + + + @Override + public Charset getCharset() { + return StandardCharsets.UTF_8; + } + + + @Override + public void parseCookieHeader(MimeHeaders headers, ServerCookies serverCookies) { + + if (headers == null) { + // nothing to process + return; + } + + // process each "cookie" header + int pos = headers.findHeader("Cookie", 0); + while (pos >= 0) { + MessageBytes cookieValue = headers.getValue(pos); + + if (cookieValue != null && !cookieValue.isNull()) { + if (cookieValue.getType() != MessageBytes.T_BYTES) { + if (log.isDebugEnabled()) { + Exception e = new Exception(); + // TODO: Review this in light of HTTP/2 + log.debug(sm.getString("rfc6265CookieProcessor.expectedBytes"), e); + } + cookieValue.toBytes(); + } + if (log.isTraceEnabled()) { + log.trace("Cookies: Parsing b[]: " + cookieValue.toString()); + } + ByteChunk bc = cookieValue.getByteChunk(); + + Cookie.parseCookie(bc.getBytes(), bc.getOffset(), bc.getLength(), serverCookies); + } + + // search from the next position + pos = headers.findHeader("Cookie", ++pos); + } + } + + + @Override + public String generateHeader(jakarta.servlet.http.Cookie cookie, HttpServletRequest request) { + + // Can't use StringBuilder due to DateFormat + StringBuffer header = new StringBuffer(); + + /* + * TODO: Name validation takes place in Cookie and cannot be configured per Context. Moving it to here would + * allow per Context config but delay validation until the header is generated. However, the spec requires an + * IllegalArgumentException on Cookie generation. + */ + header.append(cookie.getName()); + header.append('='); + String value = cookie.getValue(); + if (value != null && value.length() > 0) { + validateCookieValue(value); + header.append(value); + } + + // RFC 6265 prefers Max-Age to Expires but... (see below) + int maxAge = cookie.getMaxAge(); + if (maxAge > -1) { + // Negative Max-Age is equivalent to no Max-Age + header.append("; Max-Age="); + header.append(maxAge); + + // Microsoft IE and Microsoft Edge don't understand Max-Age so send + // expires as well. Without this, persistent cookies fail with those + // browsers. See http://tomcat.markmail.org/thread/g6sipbofsjossacn + + // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format ) + header.append("; Expires="); + // To expire immediately we need to set the time in past + if (maxAge == 0) { + header.append(ANCIENT_DATE); + } else { + COOKIE_DATE_FORMAT.get().format(new Date(System.currentTimeMillis() + maxAge * 1000L), header, + new FieldPosition(0)); + } + } + + String domain = cookie.getDomain(); + if (domain != null && domain.length() > 0) { + validateDomain(domain); + header.append("; Domain="); + header.append(domain); + } + + String path = cookie.getPath(); + if (path != null && path.length() > 0) { + validatePath(path); + header.append("; Path="); + header.append(path); + } + + if (cookie.getSecure()) { + header.append("; Secure"); + } + + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + + String cookieSameSite = cookie.getAttribute(Constants.COOKIE_SAME_SITE_ATTR); + if (cookieSameSite == null) { + // Use processor config + SameSiteCookies sameSiteCookiesValue = getSameSiteCookies(); + if (!sameSiteCookiesValue.equals(SameSiteCookies.UNSET)) { + header.append("; SameSite="); + header.append(sameSiteCookiesValue.getValue()); + } + } else { + // Use explicit config + header.append("; SameSite="); + header.append(cookieSameSite); + } + + String cookiePartitioned = cookie.getAttribute(Constants.COOKIE_PARTITIONED_ATTR); + if (cookiePartitioned == null) { + if (getPartitioned()) { + header.append("; Partitioned"); + } + } else { + if (Boolean.parseBoolean(cookiePartitioned)) { + header.append("; Partitioned"); + } + } + + + // Add the remaining attributes + for (Map.Entry entry : cookie.getAttributes().entrySet()) { + switch (entry.getKey()) { + case Constants.COOKIE_COMMENT_ATTR: + case Constants.COOKIE_DOMAIN_ATTR: + case Constants.COOKIE_MAX_AGE_ATTR: + case Constants.COOKIE_PATH_ATTR: + case Constants.COOKIE_SECURE_ATTR: + case Constants.COOKIE_HTTP_ONLY_ATTR: + case Constants.COOKIE_SAME_SITE_ATTR: + case Constants.COOKIE_PARTITIONED_ATTR: + // Handled above so NO-OP + break; + default: { + validateAttribute(entry.getKey(), entry.getValue()); + header.append("; "); + header.append(entry.getKey()); + header.append('='); + header.append(entry.getValue()); + } + } + } + + return header.toString(); + } + + + private void validateCookieValue(String value) { + int start = 0; + int end = value.length(); + + if (end > 1 && value.charAt(0) == '"' && value.charAt(end - 1) == '"') { + start = 1; + end--; + } + + char[] chars = value.toCharArray(); + for (int i = start; i < end; i++) { + char c = chars[i]; + if (c < 0x21 || c == 0x22 || c == 0x2c || c == 0x3b || c == 0x5c || c == 0x7f) { + throw new IllegalArgumentException( + sm.getString("rfc6265CookieProcessor.invalidCharInValue", Integer.toString(c))); + } + } + } + + + private void validateDomain(String domain) { + int i = 0; + int prev = -1; + int cur = -1; + char[] chars = domain.toCharArray(); + while (i < chars.length) { + prev = cur; + cur = chars[i]; + if (!domainValid.get(cur)) { + throw new IllegalArgumentException(sm.getString("rfc6265CookieProcessor.invalidDomain", domain)); + } + // labels must start with a letter or number + if ((prev == '.' || prev == -1) && (cur == '.' || cur == '-')) { + throw new IllegalArgumentException(sm.getString("rfc6265CookieProcessor.invalidDomain", domain)); + } + // labels must end with a letter or number + if (prev == '-' && cur == '.') { + throw new IllegalArgumentException(sm.getString("rfc6265CookieProcessor.invalidDomain", domain)); + } + i++; + } + // domain must end with a label + if (cur == '.' || cur == '-') { + throw new IllegalArgumentException(sm.getString("rfc6265CookieProcessor.invalidDomain", domain)); + } + } + + + private void validatePath(String path) { + char[] chars = path.toCharArray(); + + for (char ch : chars) { + if (ch < 0x20 || ch > 0x7E || ch == ';') { + throw new IllegalArgumentException(sm.getString("rfc6265CookieProcessor.invalidPath", path)); + } + } + } + + + private void validateAttribute(String name, String value) { + if (!HttpParser.isToken(name)) { + throw new IllegalArgumentException(sm.getString("rfc6265CookieProcessor.invalidAttributeName", name)); + } + + char[] chars = value.toCharArray(); + for (char ch : chars) { + if (ch < 0x20 || ch > 0x7E || ch == ';') { + throw new IllegalArgumentException( + sm.getString("rfc6265CookieProcessor.invalidAttributeValue", name, value)); + } + } + } +} diff --git a/java/org/apache/tomcat/util/http/SameSiteCookies.java b/java/org/apache/tomcat/util/http/SameSiteCookies.java new file mode 100644 index 0000000..6569c76 --- /dev/null +++ b/java/org/apache/tomcat/util/http/SameSiteCookies.java @@ -0,0 +1,64 @@ +/* + * 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.tomcat.util.http; + +import org.apache.tomcat.util.res.StringManager; + +public enum SameSiteCookies { + + /** + * Don't set the SameSite cookie attribute. + */ + UNSET("Unset"), + + /** + * Cookie is always sent in cross-site requests. + */ + NONE("None"), + + /** + * Cookie is only sent on same-site requests and cross-site top level navigation GET requests + */ + LAX("Lax"), + + /** + * Prevents the cookie from being sent by the browser in all cross-site requests + */ + STRICT("Strict"); + + private static final StringManager sm = StringManager.getManager(SameSiteCookies.class); + + private final String value; + + SameSiteCookies(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static SameSiteCookies fromString(String value) { + for (SameSiteCookies sameSiteCookies : values()) { + if (sameSiteCookies.getValue().equalsIgnoreCase(value)) { + return sameSiteCookies; + } + } + + throw new IllegalStateException(sm.getString("cookies.invalidSameSiteCookies", value)); + } +} diff --git a/java/org/apache/tomcat/util/http/ServerCookie.java b/java/org/apache/tomcat/util/http/ServerCookie.java new file mode 100644 index 0000000..12f3c0c --- /dev/null +++ b/java/org/apache/tomcat/util/http/ServerCookie.java @@ -0,0 +1,64 @@ +/* + * 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.tomcat.util.http; + +import java.io.Serializable; + +import org.apache.tomcat.util.buf.MessageBytes; + + +/** + * Server-side cookie representation. Allows recycling and uses MessageBytes as low-level representation ( and thus the + * byte -> char conversion can be delayed until we know the charset ). + *

    + * Tomcat.core uses this recyclable object to represent cookies, and the facade will convert it to the external + * representation. + */ +public class ServerCookie implements Serializable { + + private static final long serialVersionUID = 1L; + + // RFC 6265 + private final MessageBytes name = MessageBytes.newInstance(); + private final MessageBytes value = MessageBytes.newInstance(); + + public ServerCookie() { + // NOOP + } + + public void recycle() { + name.recycle(); + value.recycle(); + } + + public MessageBytes getName() { + return name; + } + + public MessageBytes getValue() { + return value; + } + + + // -------------------- utils -------------------- + + @Override + public String toString() { + return "Cookie " + getName() + "=" + getValue(); + } +} + diff --git a/java/org/apache/tomcat/util/http/ServerCookies.java b/java/org/apache/tomcat/util/http/ServerCookies.java new file mode 100644 index 0000000..f40469f --- /dev/null +++ b/java/org/apache/tomcat/util/http/ServerCookies.java @@ -0,0 +1,94 @@ +/* + * 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.tomcat.util.http; + +import org.apache.tomcat.util.res.StringManager; + +/** + * This class is not thread-safe. + */ +public class ServerCookies { + + private static final StringManager sm = StringManager.getManager(ServerCookies.class); + + private ServerCookie[] serverCookies; + + private int cookieCount = 0; + private int limit = 200; + + + public ServerCookies(int initialSize) { + serverCookies = new ServerCookie[initialSize]; + } + + + /** + * Register a new, initialized cookie. Cookies are recycled, and most of the time an existing ServerCookie object is + * returned. The caller can set the name/value and attributes for the cookie. + * + * @return the new cookie + */ + public ServerCookie addCookie() { + if (limit > -1 && cookieCount >= limit) { + throw new IllegalArgumentException(sm.getString("cookies.maxCountFail", Integer.valueOf(limit))); + } + + if (cookieCount >= serverCookies.length) { + int newSize = limit > -1 ? Math.min(2 * cookieCount, limit) : 2 * cookieCount; + ServerCookie scookiesTmp[] = new ServerCookie[newSize]; + System.arraycopy(serverCookies, 0, scookiesTmp, 0, cookieCount); + serverCookies = scookiesTmp; + } + + ServerCookie c = serverCookies[cookieCount]; + if (c == null) { + c = new ServerCookie(); + serverCookies[cookieCount] = c; + } + cookieCount++; + return c; + } + + + public ServerCookie getCookie(int idx) { + return serverCookies[idx]; + } + + + public int getCookieCount() { + return cookieCount; + } + + + public void setLimit(int limit) { + this.limit = limit; + if (limit > -1 && serverCookies.length > limit && cookieCount <= limit) { + // shrink cookie list array + ServerCookie scookiesTmp[] = new ServerCookie[limit]; + System.arraycopy(serverCookies, 0, scookiesTmp, 0, cookieCount); + serverCookies = scookiesTmp; + } + } + + + public void recycle() { + for (int i = 0; i < cookieCount; i++) { + serverCookies[i].recycle(); + } + cookieCount = 0; + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/ByteArrayOutputStream.java b/java/org/apache/tomcat/util/http/fileupload/ByteArrayOutputStream.java new file mode 100644 index 0000000..aaee63f --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ByteArrayOutputStream.java @@ -0,0 +1,227 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * This class implements an output stream in which the data is + * written into a byte array. The buffer automatically grows as data + * is written to it. + *

    + * The data can be retrieved using toByteArray() and + * toString(). + *

    + * Closing a {@code ByteArrayOutputStream} has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an {@code IOException}. + *

    + * This is an alternative implementation of the {@link java.io.ByteArrayOutputStream} + * class. The original implementation only allocates 32 bytes at the beginning. + * As this class is designed for heavy duty it starts at 1024 bytes. In contrast + * to the original it doesn't reallocate the whole memory block but allocates + * additional buffers. This way no buffers need to be garbage collected and + * the contents don't have to be copied to the new buffer. This class is + * designed to behave exactly like the original. The only exception is the + * deprecated toString(int) method that has been ignored. + */ +public class ByteArrayOutputStream extends OutputStream { + + static final int DEFAULT_SIZE = 1024; + + /** A singleton empty byte array. */ + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** The list of buffers, which grows and never reduces. */ + private final List buffers = new ArrayList<>(); + /** The index of the current buffer. */ + private int currentBufferIndex; + /** The total count of bytes in all the filled buffers. */ + private int filledBufferSum; + /** The current buffer. */ + private byte[] currentBuffer; + /** The total count of bytes written. */ + private int count; + + /** + * Creates a new byte array output stream. The buffer capacity is + * initially 1024 bytes, though its size increases if necessary. + */ + public ByteArrayOutputStream() { + this(DEFAULT_SIZE); + } + + /** + * Creates a new byte array output stream, with a buffer capacity of + * the specified size, in bytes. + * + * @param size the initial size + * @throws IllegalArgumentException if size is negative + */ + public ByteArrayOutputStream(final int size) { + if (size < 0) { + throw new IllegalArgumentException( + "Negative initial size: " + size); + } + synchronized (this) { + needNewBuffer(size); + } + } + + /** + * Makes a new buffer available either by allocating + * a new one or re-cycling an existing one. + * + * @param newcount the size of the buffer if one is created + */ + private void needNewBuffer(final int newcount) { + if (currentBufferIndex < buffers.size() - 1) { + //Recycling old buffer + filledBufferSum += currentBuffer.length; + + currentBufferIndex++; + currentBuffer = buffers.get(currentBufferIndex); + } else { + //Creating new buffer + int newBufferSize; + if (currentBuffer == null) { + newBufferSize = newcount; + filledBufferSum = 0; + } else { + newBufferSize = Math.max( + currentBuffer.length << 1, + newcount - filledBufferSum); + filledBufferSum += currentBuffer.length; + } + + currentBufferIndex++; + currentBuffer = new byte[newBufferSize]; + buffers.add(currentBuffer); + } + } + + /** + * Write the bytes to byte array. + * @param b the bytes to write + * @param off The start offset + * @param len The number of bytes to write + */ + @Override + public void write(final byte[] b, final int off, final int len) { + if ((off < 0) + || (off > b.length) + || (len < 0) + || ((off + len) > b.length) + || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + synchronized (this) { + final int newcount = count + len; + int remaining = len; + int inBufferPos = count - filledBufferSum; + while (remaining > 0) { + final int part = Math.min(remaining, currentBuffer.length - inBufferPos); + System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part); + remaining -= part; + if (remaining > 0) { + needNewBuffer(newcount); + inBufferPos = 0; + } + } + count = newcount; + } + } + + /** + * Write a byte to byte array. + * @param b the byte to write + */ + @Override + public synchronized void write(final int b) { + int inBufferPos = count - filledBufferSum; + if (inBufferPos == currentBuffer.length) { + needNewBuffer(count + 1); + inBufferPos = 0; + } + currentBuffer[inBufferPos] = (byte) b; + count++; + } + + /** + * Closing a {@code ByteArrayOutputStream} has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an {@code IOException}. + * + * @throws IOException never (this method should not declare this exception + * but it has to now due to backwards compatibility) + */ + @Override + public void close() throws IOException { + //nop + } + + /** + * Writes the entire contents of this byte stream to the + * specified output stream. + * + * @param out the output stream to write to + * @throws IOException if an I/O error occurs, such as if the stream is closed + * @see java.io.ByteArrayOutputStream#writeTo(OutputStream) + */ + public synchronized void writeTo(final OutputStream out) throws IOException { + int remaining = count; + for (final byte[] buf : buffers) { + final int c = Math.min(buf.length, remaining); + out.write(buf, 0, c); + remaining -= c; + if (remaining == 0) { + break; + } + } + } + + /** + * Gets the current contents of this byte stream as a byte array. + * The result is independent of this stream. + * + * @return the current contents of this output stream, as a byte array + * @see java.io.ByteArrayOutputStream#toByteArray() + */ + public synchronized byte[] toByteArray() { + int remaining = count; + if (remaining == 0) { + return EMPTY_BYTE_ARRAY; + } + final byte newbuf[] = new byte[remaining]; + int pos = 0; + for (final byte[] buf : buffers) { + final int c = Math.min(buf.length, remaining); + System.arraycopy(buf, 0, newbuf, pos, c); + pos += c; + remaining -= c; + if (remaining == 0) { + break; + } + } + return newbuf; + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/DeferredFileOutputStream.java b/java/org/apache/tomcat/util/http/fileupload/DeferredFileOutputStream.java new file mode 100644 index 0000000..86a4995 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/DeferredFileOutputStream.java @@ -0,0 +1,227 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + + +/** + * An output stream which will retain data in memory until a specified + * threshold is reached, and only then commit it to disk. If the stream is + * closed before the threshold is reached, the data will not be written to + * disk at all. + *

    + * This class originated in FileUpload processing. In this use case, you do + * not know in advance the size of the file being uploaded. If the file is small + * you want to store it in memory (for speed), but if the file is large you want + * to store it to file (to avoid memory issues). + */ +public class DeferredFileOutputStream + extends ThresholdingOutputStream +{ + // ----------------------------------------------------------- Data members + + + /** + * The output stream to which data will be written prior to the threshold + * being reached. + */ + private ByteArrayOutputStream memoryOutputStream; + + + /** + * The output stream to which data will be written at any given time. This + * will always be one of memoryOutputStream or + * diskOutputStream. + */ + private OutputStream currentOutputStream; + + + /** + * The file to which output will be directed if the threshold is exceeded. + */ + private File outputFile; + + /** + * The temporary file prefix. + */ + private final String prefix; + + /** + * The temporary file suffix. + */ + private final String suffix; + + /** + * The directory to use for temporary files. + */ + private final File directory; + + // ----------------------------------------------------------- Constructors + + + /** + * Constructs an instance of this class which will trigger an event at the + * specified threshold, and save data to a file beyond that point. + * The initial buffer size will default to 1024 bytes which is ByteArrayOutputStream's default buffer size. + * + * @param threshold The number of bytes at which to trigger an event. + * @param outputFile The file to which data is saved beyond the threshold. + */ + public DeferredFileOutputStream(final int threshold, final File outputFile) + { + this(threshold, outputFile, null, null, null, ByteArrayOutputStream.DEFAULT_SIZE); + } + + + /** + * Constructs an instance of this class which will trigger an event at the + * specified threshold, and save data either to a file beyond that point. + * + * @param threshold The number of bytes at which to trigger an event. + * @param outputFile The file to which data is saved beyond the threshold. + * @param prefix Prefix to use for the temporary file. + * @param suffix Suffix to use for the temporary file. + * @param directory Temporary file directory. + * @param initialBufferSize The initial size of the in memory buffer. + */ + private DeferredFileOutputStream(final int threshold, final File outputFile, final String prefix, + final String suffix, final File directory, final int initialBufferSize) { + super(threshold); + this.outputFile = outputFile; + this.prefix = prefix; + this.suffix = suffix; + this.directory = directory; + + memoryOutputStream = new ByteArrayOutputStream(initialBufferSize); + currentOutputStream = memoryOutputStream; + } + + + // --------------------------------------- ThresholdingOutputStream methods + + + /** + * Returns the current output stream. This may be memory based or disk + * based, depending on the current state with respect to the threshold. + * + * @return The underlying output stream. + * + * @throws IOException if an error occurs. + */ + @Override + protected OutputStream getStream() throws IOException + { + return currentOutputStream; + } + + + /** + * Switches the underlying output stream from a memory based stream to one + * that is backed by disk. This is the point at which we realise that too + * much data is being written to keep in memory, so we elect to switch to + * disk-based storage. + * + * @throws IOException if an error occurs. + */ + @Override + protected void thresholdReached() throws IOException + { + if (prefix != null) { + outputFile = File.createTempFile(prefix, suffix, directory); + } + FileUtils.forceMkdirParent(outputFile); + final FileOutputStream fos = new FileOutputStream(outputFile); + try { + memoryOutputStream.writeTo(fos); + } catch (final IOException e){ + fos.close(); + throw e; + } + currentOutputStream = fos; + memoryOutputStream = null; + } + + + // --------------------------------------------------------- Public methods + + + /** + * Determines whether or not the data for this output stream has been + * retained in memory. + * + * @return {@code true} if the data is available in memory; + * {@code false} otherwise. + */ + public boolean isInMemory() + { + return !isThresholdExceeded(); + } + + + /** + * Returns the data for this output stream as an array of bytes, assuming + * that the data has been retained in memory. If the data was written to + * disk, this method returns {@code null}. + * + * @return The data for this output stream, or {@code null} if no such + * data is available. + */ + public byte[] getData() + { + if (memoryOutputStream != null) + { + return memoryOutputStream.toByteArray(); + } + return null; + } + + + /** + * Returns either the output file specified in the constructor or + * the temporary file created or null. + *

    + * If the constructor specifying the file is used then it returns that + * same output file, even when threshold has not been reached. + *

    + * If constructor specifying a temporary file prefix/suffix is used + * then the temporary file created once the threshold is reached is returned + * If the threshold was not reached then {@code null} is returned. + * + * @return The file for this output stream, or {@code null} if no such + * file exists. + */ + public File getFile() + { + return outputFile; + } + + + /** + * Closes underlying output stream, and mark this as closed + * + * @throws IOException if an error occurs. + */ + @Override + public void close() throws IOException + { + super.close(); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItem.java b/java/org/apache/tomcat/util/http/fileupload/FileItem.java new file mode 100644 index 0000000..e6f035f --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItem.java @@ -0,0 +1,208 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; + +/** + *

    This class represents a file or form item that was received within a + * {@code multipart/form-data} POST request. + * + *

    After retrieving an instance of this class from a {@link + * org.apache.tomcat.util.http.fileupload.FileUpload FileUpload} instance (see + * {@link org.apache.tomcat.util.http.fileupload.FileUpload + * #parseRequest(RequestContext)}), you may + * either request all contents of the file at once using {@link #get()} or + * request an {@link java.io.InputStream InputStream} with + * {@link #getInputStream()} and process the file without attempting to load + * it into memory, which may come handy with large files. + * + *

    While this interface does not extend + * {@code javax.activation.DataSource} per se (to avoid a seldom used + * dependency), several of the defined methods are specifically defined with + * the same signatures as methods in that interface. This allows an + * implementation of this interface to also implement + * {@code javax.activation.DataSource} with minimal additional work. + * + * @since FileUpload 1.3 additionally implements FileItemHeadersSupport + */ +public interface FileItem extends FileItemHeadersSupport { + + // ------------------------------- Methods from javax.activation.DataSource + + /** + * Returns an {@link java.io.InputStream InputStream} that can be + * used to retrieve the contents of the file. + * + * @return An {@link java.io.InputStream InputStream} that can be + * used to retrieve the contents of the file. + * + * @throws IOException if an error occurs. + */ + InputStream getInputStream() throws IOException; + + /** + * Returns the content type passed by the browser or {@code null} if + * not defined. + * + * @return The content type passed by the browser or {@code null} if + * not defined. + */ + String getContentType(); + + /** + * Returns the original file name in the client's file system, as provided by + * the browser (or other client software). In most cases, this will be the + * base file name, without path information. However, some clients, such as + * the Opera browser, do include path information. + * + * @return The original file name in the client's file system. + * @throws InvalidFileNameException The file name contains a NUL character, + * which might be an indicator of a security attack. If you intend to + * use the file name anyways, catch the exception and use + * InvalidFileNameException#getName(). + */ + String getName(); + + // ------------------------------------------------------- FileItem methods + + /** + * Provides a hint as to whether or not the file contents will be read + * from memory. + * + * @return {@code true} if the file contents will be read from memory; + * {@code false} otherwise. + */ + boolean isInMemory(); + + /** + * Returns the size of the file item. + * + * @return The size of the file item, in bytes. + */ + long getSize(); + + /** + * Returns the contents of the file item as an array of bytes. + * + * @return The contents of the file item as an array of bytes. + * + * @throws UncheckedIOException if an I/O error occurs + */ + byte[] get() throws UncheckedIOException; + + /** + * Returns the contents of the file item as a String, using the specified + * encoding. This method uses {@link #get()} to retrieve the + * contents of the item. + * + * @param encoding The character encoding to use. + * + * @return The contents of the item, as a string. + * + * @throws UnsupportedEncodingException if the requested character + * encoding is not available. + * @throws IOException if an I/O error occurs + */ + String getString(String encoding) throws UnsupportedEncodingException, IOException; + + /** + * Returns the contents of the file item as a String, using the default + * character encoding. This method uses {@link #get()} to retrieve the + * contents of the item. + * + * @return The contents of the item, as a string. + */ + String getString(); + + /** + * A convenience method to write an uploaded item to disk. The client code + * is not concerned with whether or not the item is stored in memory, or on + * disk in a temporary location. They just want to write the uploaded item + * to a file. + *

    + * This method is not guaranteed to succeed if called more than once for + * the same item. This allows a particular implementation to use, for + * example, file renaming, where possible, rather than copying all of the + * underlying data, thus gaining a significant performance benefit. + * + * @param file The {@code File} into which the uploaded item should + * be stored. + * + * @throws Exception if an error occurs. + */ + void write(File file) throws Exception; + + /** + * Deletes the underlying storage for a file item, including deleting any + * associated temporary disk file. Although this storage will be deleted + * automatically when the {@code FileItem} instance is garbage + * collected, this method can be used to ensure that this is done at an + * earlier time, thus preserving system resources. + */ + void delete(); + + /** + * Returns the name of the field in the multipart form corresponding to + * this file item. + * + * @return The name of the form field. + */ + String getFieldName(); + + /** + * Sets the field name used to reference this file item. + * + * @param name The name of the form field. + */ + void setFieldName(String name); + + /** + * Determines whether or not a {@code FileItem} instance represents + * a simple form field. + * + * @return {@code true} if the instance represents a simple form + * field; {@code false} if it represents an uploaded file. + */ + boolean isFormField(); + + /** + * Specifies whether or not a {@code FileItem} instance represents + * a simple form field. + * + * @param state {@code true} if the instance represents a simple form + * field; {@code false} if it represents an uploaded file. + */ + void setFormField(boolean state); + + /** + * Returns an {@link java.io.OutputStream OutputStream} that can + * be used for storing the contents of the file. + * + * @return An {@link java.io.OutputStream OutputStream} that can be used + * for storing the contents of the file. + * + * @throws IOException if an error occurs. + */ + OutputStream getOutputStream() throws IOException; + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemFactory.java b/java/org/apache/tomcat/util/http/fileupload/FileItemFactory.java new file mode 100644 index 0000000..9d4accf --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemFactory.java @@ -0,0 +1,46 @@ +/* + * 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.tomcat.util.http.fileupload; + +/** + *

    A factory interface for creating {@link FileItem} instances. Factories + * can provide their own custom configuration, over and above that provided + * by the default file upload implementation.

    + */ +public interface FileItemFactory { + + /** + * Create a new {@link FileItem} instance from the supplied parameters and + * any local factory configuration. + * + * @param fieldName The name of the form field. + * @param contentType The content type of the form field. + * @param isFormField {@code true} if this is a plain form field; + * {@code false} otherwise. + * @param fileName The name of the uploaded file, if any, as supplied + * by the browser or other client. + * + * @return The newly created file item. + */ + FileItem createItem( + String fieldName, + String contentType, + boolean isFormField, + String fileName + ); + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java b/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java new file mode 100644 index 0000000..890f439 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java @@ -0,0 +1,74 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.util.Iterator; + +/** + *

    This class provides support for accessing the headers for a file or form + * item that was received within a {@code multipart/form-data} POST + * request.

    + * + * @since FileUpload 1.2.1 + */ +public interface FileItemHeaders { + + /** + * Returns the value of the specified part header as a {@code String}. + * + * If the part did not include a header of the specified name, this method + * return {@code null}. If there are multiple headers with the same + * name, this method returns the first header in the item. The header + * name is case insensitive. + * + * @param name a {@code String} specifying the header name + * @return a {@code String} containing the value of the requested + * header, or {@code null} if the item does not have a header + * of that name + */ + String getHeader(String name); + + /** + *

    + * Returns all the values of the specified item header as an + * {@code Iterator} of {@code String} objects. + *

    + *

    + * If the item did not include any headers of the specified name, this + * method returns an empty {@code Iterator}. The header name is + * case insensitive. + *

    + * + * @param name a {@code String} specifying the header name + * @return an {@code Iterator} containing the values of the + * requested header. If the item does not have any headers of + * that name, return an empty {@code Iterator} + */ + Iterator getHeaders(String name); + + /** + *

    + * Returns an {@code Iterator} of all the header names. + *

    + * + * @return an {@code Iterator} containing all of the names of + * headers provided with this file item. If the item does not have + * any headers return an empty {@code Iterator} + */ + Iterator getHeaderNames(); + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersSupport.java b/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersSupport.java new file mode 100644 index 0000000..e5858c7 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemHeadersSupport.java @@ -0,0 +1,48 @@ +/* + * 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.tomcat.util.http.fileupload; + +/** + * Interface that will indicate that {@link FileItem} or {@link FileItemStream} + * implementations will accept the headers read for the item. + * + * @since FileUpload 1.2.1 + * + * @see FileItem + * @see FileItemStream + */ +public interface FileItemHeadersSupport { + + /** + * Returns the collection of headers defined locally within this item. + * + * @return the {@link FileItemHeaders} present for this item. + */ + FileItemHeaders getHeaders(); + + /** + * Sets the headers read from within an item. Implementations of + * {@link FileItem} or {@link FileItemStream} should implement this + * interface to be able to get the raw headers found within the item + * header block. + * + * @param headers the instance that holds onto the headers + * for this instance. + */ + void setHeaders(FileItemHeaders headers); + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemIterator.java b/java/org/apache/tomcat/util/http/fileupload/FileItemIterator.java new file mode 100644 index 0000000..2191c16 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemIterator.java @@ -0,0 +1,101 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.util.List; + +import org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException; +import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException; + +/** + * An iterator, as returned by + * {@link FileUploadBase#getItemIterator(RequestContext)}. + */ +public interface FileItemIterator { + /** + * Returns the maximum size of a single file. An {@link FileSizeLimitExceededException} + * will be thrown, if there is an uploaded file, which is exceeding this value. + * By default, this value will be copied from the {@link FileUploadBase#getFileSizeMax() + * FileUploadBase} object, however, the user may replace the default value with a + * request specific value by invoking {@link #setFileSizeMax(long)} on this object. + * @return The maximum size of a single, uploaded file. The value -1 indicates "unlimited". + */ + long getFileSizeMax(); + + /** + * Sets the maximum size of a single file. An {@link FileSizeLimitExceededException} + * will be thrown, if there is an uploaded file, which is exceeding this value. + * By default, this value will be copied from the {@link FileUploadBase#getFileSizeMax() + * FileUploadBase} object, however, the user may replace the default value with a + * request specific value by invoking {@link #setFileSizeMax(long)} on this object, so + * there is no need to configure it here. + * Note:Changing this value doesn't affect files, that have already been uploaded. + * @param pFileSizeMax The maximum size of a single, uploaded file. The value -1 indicates "unlimited". + */ + void setFileSizeMax(long pFileSizeMax); + + /** + * Returns the maximum size of the complete HTTP request. A {@link SizeLimitExceededException} + * will be thrown, if the HTTP request will exceed this value. + * By default, this value will be copied from the {@link FileUploadBase#getSizeMax() + * FileUploadBase} object, however, the user may replace the default value with a + * request specific value by invoking {@link #setSizeMax(long)} on this object. + * @return The maximum size of the complete HTTP request. The value -1 indicates "unlimited". + */ + long getSizeMax(); + + /** + * Returns the maximum size of the complete HTTP request. A {@link SizeLimitExceededException} + * will be thrown, if the HTTP request will exceed this value. + * By default, this value will be copied from the {@link FileUploadBase#getSizeMax() + * FileUploadBase} object, however, the user may replace the default value with a + * request specific value by invoking {@link #setSizeMax(long)} on this object. + * Note: Setting the maximum size on this object will work only, if the iterator is not + * yet initialized. In other words: If the methods {@link #hasNext()}, {@link #next()} have not + * yet been invoked. + * @param pSizeMax The maximum size of the complete HTTP request. The value -1 indicates "unlimited". + */ + void setSizeMax(long pSizeMax); + + /** + * Returns, whether another instance of {@link FileItemStream} + * is available. + * + * @throws FileUploadException Parsing or processing the + * file item failed. + * @throws IOException Reading the file item failed. + * @return True, if one or more additional file items + * are available, otherwise false. + */ + boolean hasNext() throws FileUploadException, IOException; + + /** + * Returns the next available {@link FileItemStream}. + * + * @throws java.util.NoSuchElementException No more items are available. Use + * {@link #hasNext()} to prevent this exception. + * @throws FileUploadException Parsing or processing the + * file item failed. + * @throws IOException Reading the file item failed. + * @return FileItemStream instance, which provides + * access to the next file item. + */ + FileItemStream next() throws FileUploadException, IOException; + + List getFileItems() throws FileUploadException, IOException; +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java b/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java new file mode 100644 index 0000000..c31d9eb --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java @@ -0,0 +1,102 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.io.InputStream; + +/** + *

    This interface provides access to a file or form item that was + * received within a {@code multipart/form-data} POST request. + * The items contents are retrieved by calling {@link #openStream()}.

    + *

    Instances of this class are created by accessing the + * iterator, returned by + * {@link FileUploadBase#getItemIterator(RequestContext)}.

    + *

    Note: There is an interaction between the iterator and + * its associated instances of {@link FileItemStream}: By invoking + * {@link java.util.Iterator#hasNext()} on the iterator, you discard all data, + * which hasn't been read so far from the previous data.

    + */ +public interface FileItemStream extends FileItemHeadersSupport { + + /** + * This exception is thrown, if an attempt is made to read + * data from the {@link InputStream}, which has been returned + * by {@link FileItemStream#openStream()}, after + * {@link java.util.Iterator#hasNext()} has been invoked on the + * iterator, which created the {@link FileItemStream}. + */ + class ItemSkippedException extends IOException { + + /** + * The exceptions serial version UID, which is being used + * when serializing an exception instance. + */ + private static final long serialVersionUID = -7280778431581963740L; + + } + + /** + * Creates an {@link InputStream}, which allows to read the + * items contents. + * + * @return The input stream, from which the items data may + * be read. + * @throws IllegalStateException The method was already invoked on + * this item. It is not possible to recreate the data stream. + * @throws IOException An I/O error occurred. + * @see ItemSkippedException + */ + InputStream openStream() throws IOException; + + /** + * Returns the content type passed by the browser or {@code null} if + * not defined. + * + * @return The content type passed by the browser or {@code null} if + * not defined. + */ + String getContentType(); + + /** + * Returns the original file name in the client's file system, as provided by + * the browser (or other client software). In most cases, this will be the + * base file name, without path information. However, some clients, such as + * the Opera browser, do include path information. + * + * @return The original file name in the client's file system. + */ + String getName(); + + /** + * Returns the name of the field in the multipart form corresponding to + * this file item. + * + * @return The name of the form field. + */ + String getFieldName(); + + /** + * Determines whether or not a {@code FileItem} instance represents + * a simple form field. + * + * @return {@code true} if the instance represents a simple form + * field; {@code false} if it represents an uploaded file. + */ + boolean isFormField(); + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUpload.java b/java/org/apache/tomcat/util/http/fileupload/FileUpload.java new file mode 100644 index 0000000..06846f1 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileUpload.java @@ -0,0 +1,77 @@ +/* + * 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.tomcat.util.http.fileupload; + +/** + *

    High level API for processing file uploads.

    + * + *

    This class handles multiple files per single HTML widget, sent using + * {@code multipart/mixed} encoding type, as specified by + * RFC 1867. Use {@link + * #parseRequest(RequestContext)} to acquire a list + * of {@link org.apache.tomcat.util.http.fileupload.FileItem FileItems} associated + * with a given HTML widget.

    + * + *

    How the data for individual parts is stored is determined by the factory + * used to create them; a given part may be in memory, on disk, or somewhere + * else.

    + */ +public class FileUpload + extends FileUploadBase { + + // ----------------------------------------------------------- Data members + + /** + * The factory to use to create new form items. + */ + private FileItemFactory fileItemFactory; + + // ----------------------------------------------------------- Constructors + + /** + * Constructs an uninitialized instance of this class. + * + * A factory must be + * configured, using {@code setFileItemFactory()}, before attempting + * to parse requests. + */ + public FileUpload() { + } + + // ----------------------------------------------------- Property accessors + + /** + * Returns the factory class used when creating file items. + * + * @return The factory class for new file items. + */ + @Override + public FileItemFactory getFileItemFactory() { + return fileItemFactory; + } + + /** + * Sets the factory class to use when creating file items. + * + * @param factory The factory class for new file items. + */ + @Override + public void setFileItemFactory(final FileItemFactory factory) { + this.fileItemFactory = factory; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java b/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java new file mode 100644 index 0000000..b9f5cfe --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java @@ -0,0 +1,532 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +import org.apache.tomcat.util.http.fileupload.impl.FileCountLimitExceededException; +import org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl; +import org.apache.tomcat.util.http.fileupload.impl.FileUploadIOException; +import org.apache.tomcat.util.http.fileupload.impl.IOFileUploadException; +import org.apache.tomcat.util.http.fileupload.util.FileItemHeadersImpl; +import org.apache.tomcat.util.http.fileupload.util.Streams; + + +/** + *

    High level API for processing file uploads.

    + * + *

    This class handles multiple files per single HTML widget, sent using + * {@code multipart/mixed} encoding type, as specified by + * RFC 1867. Use {@link + * #parseRequest(RequestContext)} to acquire a list of {@link + * org.apache.tomcat.util.http.fileupload.FileItem}s associated with a given HTML + * widget.

    + * + *

    How the data for individual parts is stored is determined by the factory + * used to create them; a given part may be in memory, on disk, or somewhere + * else.

    + */ +public abstract class FileUploadBase { + + // ----------------------------------------------------- Manifest constants + + /** + * HTTP content type header name. + */ + public static final String CONTENT_TYPE = "Content-type"; + + /** + * HTTP content disposition header name. + */ + public static final String CONTENT_DISPOSITION = "Content-disposition"; + + /** + * HTTP content length header name. + */ + public static final String CONTENT_LENGTH = "Content-length"; + + /** + * Content-disposition value for form data. + */ + public static final String FORM_DATA = "form-data"; + + /** + * Content-disposition value for file attachment. + */ + public static final String ATTACHMENT = "attachment"; + + /** + * Part of HTTP content type header. + */ + public static final String MULTIPART = "multipart/"; + + /** + * HTTP content type header for multipart forms. + */ + public static final String MULTIPART_FORM_DATA = "multipart/form-data"; + + /** + * HTTP content type header for multiple uploads. + */ + public static final String MULTIPART_MIXED = "multipart/mixed"; + + // ----------------------------------------------------------- Data members + + /** + * The maximum size permitted for the complete request, as opposed to + * {@link #fileSizeMax}. A value of -1 indicates no maximum. + */ + private long sizeMax = -1; + + /** + * The maximum size permitted for a single uploaded file, as opposed + * to {@link #sizeMax}. A value of -1 indicates no maximum. + */ + private long fileSizeMax = -1; + + /** + * The maximum permitted number of files that may be uploaded in a single + * request. A value of -1 indicates no maximum. + */ + private long fileCountMax = -1; + + /** + * The content encoding to use when reading part headers. + */ + private String headerEncoding; + + /** + * The progress listener. + */ + private ProgressListener listener; + + // ----------------------------------------------------- Property accessors + + /** + * Returns the factory class used when creating file items. + * + * @return The factory class for new file items. + */ + public abstract FileItemFactory getFileItemFactory(); + + /** + * Sets the factory class to use when creating file items. + * + * @param factory The factory class for new file items. + */ + public abstract void setFileItemFactory(FileItemFactory factory); + + /** + * Returns the maximum allowed size of a complete request, as opposed + * to {@link #getFileSizeMax()}. + * + * @return The maximum allowed size, in bytes. The default value of + * -1 indicates, that there is no limit. + * + * @see #setSizeMax(long) + * + */ + public long getSizeMax() { + return sizeMax; + } + + /** + * Sets the maximum allowed size of a complete request, as opposed + * to {@link #setFileSizeMax(long)}. + * + * @param sizeMax The maximum allowed size, in bytes. The default value of + * -1 indicates, that there is no limit. + * + * @see #getSizeMax() + * + */ + public void setSizeMax(final long sizeMax) { + this.sizeMax = sizeMax; + } + + /** + * Returns the maximum allowed size of a single uploaded file, + * as opposed to {@link #getSizeMax()}. + * + * @see #setFileSizeMax(long) + * @return Maximum size of a single uploaded file. + */ + public long getFileSizeMax() { + return fileSizeMax; + } + + /** + * Sets the maximum allowed size of a single uploaded file, + * as opposed to {@link #getSizeMax()}. + * + * @see #getFileSizeMax() + * @param fileSizeMax Maximum size of a single uploaded file. + */ + public void setFileSizeMax(final long fileSizeMax) { + this.fileSizeMax = fileSizeMax; + } + + /** + * Returns the maximum number of files allowed in a single request. + * + * @return The maximum number of files allowed in a single request. + */ + public long getFileCountMax() { + return fileCountMax; + } + + /** + * Sets the maximum number of files allowed per request. + * + * @param fileCountMax The new limit. {@code -1} means no limit. + */ + public void setFileCountMax(final long fileCountMax) { + this.fileCountMax = fileCountMax; + } + + /** + * Retrieves the character encoding used when reading the headers of an + * individual part. When not specified, or {@code null}, the request + * encoding is used. If that is also not specified, or {@code null}, + * the platform default encoding is used. + * + * @return The encoding used to read part headers. + */ + public String getHeaderEncoding() { + return headerEncoding; + } + + /** + * Specifies the character encoding to be used when reading the headers of + * individual part. When not specified, or {@code null}, the request + * encoding is used. If that is also not specified, or {@code null}, + * the platform default encoding is used. + * + * @param encoding The encoding used to read part headers. + */ + public void setHeaderEncoding(final String encoding) { + headerEncoding = encoding; + } + + // --------------------------------------------------------- Public methods + + /** + * Processes an RFC 1867 + * compliant {@code multipart/form-data} stream. + * + * @param ctx The context for the request to be parsed. + * + * @return An iterator to instances of {@code FileItemStream} + * parsed from the request, in the order that they were + * transmitted. + * + * @throws FileUploadException if there are problems reading/parsing + * the request or storing files. + * @throws IOException An I/O error occurred. This may be a network + * error while communicating with the client or a problem while + * storing the uploaded content. + */ + public FileItemIterator getItemIterator(final RequestContext ctx) + throws FileUploadException, IOException { + try { + return new FileItemIteratorImpl(this, ctx); + } catch (final FileUploadIOException e) { + // unwrap encapsulated SizeException + throw (FileUploadException) e.getCause(); + } + } + + /** + * Processes an RFC 1867 + * compliant {@code multipart/form-data} stream. + * + * @param ctx The context for the request to be parsed. + * + * @return A list of {@code FileItem} instances parsed from the + * request, in the order that they were transmitted. + * + * @throws FileUploadException if there are problems reading/parsing + * the request or storing files. + */ + public List parseRequest(final RequestContext ctx) + throws FileUploadException { + final List items = new ArrayList<>(); + boolean successful = false; + try { + final FileItemIterator iter = getItemIterator(ctx); + final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(), + "No FileItemFactory has been set."); + final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE]; + while (iter.hasNext()) { + if (items.size() == fileCountMax) { + // The next item will exceed the limit. + throw new FileCountLimitExceededException(ATTACHMENT, getFileCountMax()); + } + final FileItemStream item = iter.next(); + // Don't use getName() here to prevent an InvalidFileNameException. + final String fileName = item.getName(); + final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(), + item.isFormField(), fileName); + items.add(fileItem); + try { + Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer); + } catch (final FileUploadIOException e) { + throw (FileUploadException) e.getCause(); + } catch (final IOException e) { + throw new IOFileUploadException(String.format("Processing of %s request failed. %s", + MULTIPART_FORM_DATA, e.getMessage()), e); + } + final FileItemHeaders fih = item.getHeaders(); + fileItem.setHeaders(fih); + } + successful = true; + return items; + } catch (final FileUploadException e) { + throw e; + } catch (final IOException e) { + throw new FileUploadException(e.getMessage(), e); + } finally { + if (!successful) { + for (final FileItem fileItem : items) { + try { + fileItem.delete(); + } catch (final Exception ignored) { + // ignored TODO perhaps add to tracker delete failure list somehow? + } + } + } + } + } + + // ------------------------------------------------------ Protected methods + + /** + * Retrieves the boundary from the {@code Content-type} header. + * + * @param contentType The value of the content type header from which to + * extract the boundary value. + * + * @return The boundary, as a byte array. + */ + public byte[] getBoundary(final String contentType) { + final ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + final Map params = parser.parse(contentType, new char[] {';', ','}); + final String boundaryStr = params.get("boundary"); + + if (boundaryStr == null) { + return null; + } + final byte[] boundary; + boundary = boundaryStr.getBytes(StandardCharsets.ISO_8859_1); + return boundary; + } + + /** + * Retrieves the file name from the {@code Content-disposition} + * header. + * + * @param headers The HTTP headers object. + * + * @return The file name for the current {@code encapsulation}. + */ + public String getFileName(final FileItemHeaders headers) { + return getFileName(headers.getHeader(CONTENT_DISPOSITION)); + } + + /** + * Returns the given content-disposition headers file name. + * @param pContentDisposition The content-disposition headers value. + * @return The file name + */ + private String getFileName(final String pContentDisposition) { + String fileName = null; + if (pContentDisposition != null) { + final String cdl = pContentDisposition.toLowerCase(Locale.ENGLISH); + if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) { + final ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + final Map params = parser.parse(pContentDisposition, ';'); + if (params.containsKey("filename")) { + fileName = params.get("filename"); + if (fileName != null) { + fileName = fileName.trim(); + } else { + // Even if there is no value, the parameter is present, + // so we return an empty file name rather than no file + // name. + fileName = ""; + } + } + } + } + return fileName; + } + + /** + * Retrieves the field name from the {@code Content-disposition} + * header. + * + * @param headers A {@code Map} containing the HTTP request headers. + * + * @return The field name for the current {@code encapsulation}. + */ + public String getFieldName(final FileItemHeaders headers) { + return getFieldName(headers.getHeader(CONTENT_DISPOSITION)); + } + + /** + * Returns the field name, which is given by the content-disposition + * header. + * @param pContentDisposition The content-dispositions header value. + * @return The field jake + */ + private String getFieldName(final String pContentDisposition) { + String fieldName = null; + if (pContentDisposition != null + && pContentDisposition.toLowerCase(Locale.ENGLISH).startsWith(FORM_DATA)) { + final ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + final Map params = parser.parse(pContentDisposition, ';'); + fieldName = params.get("name"); + if (fieldName != null) { + fieldName = fieldName.trim(); + } + } + return fieldName; + } + + /** + *

    Parses the {@code header-part} and returns as key/value + * pairs. + * + *

    If there are multiple headers of the same names, the name + * will map to a comma-separated list containing the values. + * + * @param headerPart The {@code header-part} of the current + * {@code encapsulation}. + * + * @return A {@code Map} containing the parsed HTTP request headers. + */ + public FileItemHeaders getParsedHeaders(final String headerPart) { + final int len = headerPart.length(); + final FileItemHeadersImpl headers = newFileItemHeaders(); + int start = 0; + for (;;) { + int end = parseEndOfLine(headerPart, start); + if (start == end) { + break; + } + final StringBuilder header = new StringBuilder(headerPart.substring(start, end)); + start = end + 2; + while (start < len) { + int nonWs = start; + while (nonWs < len) { + final char c = headerPart.charAt(nonWs); + if (c != ' ' && c != '\t') { + break; + } + ++nonWs; + } + if (nonWs == start) { + break; + } + // Continuation line found + end = parseEndOfLine(headerPart, nonWs); + header.append(' ').append(headerPart, nonWs, end); + start = end + 2; + } + parseHeaderLine(headers, header.toString()); + } + return headers; + } + + /** + * Creates a new instance of {@link FileItemHeaders}. + * @return The new instance. + */ + protected FileItemHeadersImpl newFileItemHeaders() { + return new FileItemHeadersImpl(); + } + + /** + * Skips bytes until the end of the current line. + * @param headerPart The headers, which are being parsed. + * @param end Index of the last byte, which has yet been + * processed. + * @return Index of the \r\n sequence, which indicates + * end of line. + */ + private int parseEndOfLine(final String headerPart, final int end) { + int index = end; + for (;;) { + final int offset = headerPart.indexOf('\r', index); + if (offset == -1 || offset + 1 >= headerPart.length()) { + throw new IllegalStateException( + "Expected headers to be terminated by an empty line."); + } + if (headerPart.charAt(offset + 1) == '\n') { + return offset; + } + index = offset + 1; + } + } + + /** + * Reads the next header line. + * @param headers String with all headers. + * @param header Map where to store the current header. + */ + private void parseHeaderLine(final FileItemHeadersImpl headers, final String header) { + final int colonOffset = header.indexOf(':'); + if (colonOffset == -1) { + // This header line is malformed, skip it. + return; + } + final String headerName = header.substring(0, colonOffset).trim(); + final String headerValue = header.substring(colonOffset + 1).trim(); + headers.addHeader(headerName, headerValue); + } + + /** + * Returns the progress listener. + * + * @return The progress listener, if any, or null. + */ + public ProgressListener getProgressListener() { + return listener; + } + + /** + * Sets the progress listener. + * + * @param pListener The progress listener, if any. Defaults to null. + */ + public void setProgressListener(final ProgressListener pListener) { + listener = pListener; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java b/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java new file mode 100644 index 0000000..d7a8d11 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java @@ -0,0 +1,55 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; + +/** + * Exception for errors encountered while processing the request. + */ +public class FileUploadException extends IOException { + + private static final long serialVersionUID = -4222909057964038517L; + + /** + * Constructs a new {@code FileUploadException} without message. + */ + public FileUploadException() { + super(); + } + + /** + * Constructs a new {@code FileUploadException} with specified detail + * message. + * + * @param msg the error message. + */ + public FileUploadException(final String msg) { + super(msg); + } + + /** + * Creates a new {@code FileUploadException} with the given + * detail message and cause. + * + * @param msg The exceptions detail message. + * @param cause The exceptions cause. + */ + public FileUploadException(final String msg, final Throwable cause) { + super(msg, cause); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUtils.java b/java/org/apache/tomcat/util/http/fileupload/FileUtils.java new file mode 100644 index 0000000..a325fb6 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/FileUtils.java @@ -0,0 +1,309 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + + +/** + * General file manipulation utilities. + *

    + * Facilities are provided in the following areas: + *

      + *
    • writing to a file + *
    • reading from a file + *
    • make a directory including parent directories + *
    • copying files and directories + *
    • deleting files and directories + *
    • converting to and from a URL + *
    • listing files and directories by filter and extension + *
    • comparing file content + *
    • file last changed date + *
    • calculating a checksum + *
    + *

    + * Note that a specific charset should be specified whenever possible. + * Relying on the platform default means that the code is Locale-dependent. + * Only use the default if the files are known to always use the platform default. + *

    + * Origin of code: Excalibur, Alexandria, Commons-Utils + */ +public class FileUtils { + + /** + * Instances should NOT be constructed in standard programming. + */ + public FileUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + * Deletes a directory recursively. + * + * @param directory directory to delete + * @throws IOException in case deletion is unsuccessful + * @throws IllegalArgumentException if {@code directory} does not exist or is not a directory + */ + public static void deleteDirectory(final File directory) throws IOException { + if (!directory.exists()) { + return; + } + + if (!isSymlink(directory)) { + cleanDirectory(directory); + } + + if (!directory.delete()) { + final String message = + "Unable to delete directory " + directory + "."; + throw new IOException(message); + } + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean + * @throws IOException in case cleaning is unsuccessful + * @throws IllegalArgumentException if {@code directory} does not exist or is not a directory + */ + public static void cleanDirectory(final File directory) throws IOException { + if (!directory.exists()) { + final String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + final String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + final File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (File file : files) { + try { + forceDelete(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + //----------------------------------------------------------------------- + /** + * Deletes a file. If file is a directory, delete it and all sub-directories. + *

    + * The difference between File.delete() and this method are: + *

      + *
    • A directory to be deleted does not have to be empty.
    • + *
    • You get exceptions when a file or directory cannot be deleted. + * (java.io.File methods returns a boolean)
    • + *
    + * + * @param file file or directory to delete, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws FileNotFoundException if the file was not found + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDelete(final File file) throws IOException { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + final boolean filePresent = file.exists(); + if (!file.delete()) { + if (!filePresent) { + throw new FileNotFoundException("File does not exist: " + file); + } + final String message = + "Unable to delete file: " + file; + throw new IOException(message); + } + } + } + + /** + * Schedules a file to be deleted when JVM exits. + * If file is directory delete it and all sub-directories. + * + * @param file file or directory to delete, must not be {@code null} + * @throws NullPointerException if the file is {@code null} + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDeleteOnExit(final File file) throws IOException { + if (file.isDirectory()) { + deleteDirectoryOnExit(file); + } else { + file.deleteOnExit(); + } + } + + /** + * Schedules a directory recursively for deletion on JVM exit. + * + * @param directory directory to delete, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws IOException in case deletion is unsuccessful + */ + private static void deleteDirectoryOnExit(final File directory) throws IOException { + if (!directory.exists()) { + return; + } + + directory.deleteOnExit(); + if (!isSymlink(directory)) { + cleanDirectoryOnExit(directory); + } + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws IOException in case cleaning is unsuccessful + */ + private static void cleanDirectoryOnExit(final File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (File file : files) { + try { + forceDeleteOnExit(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + /** + * Makes a directory, including any necessary but nonexistent parent + * directories. If a file already exists with specified name but it is + * not a directory then an IOException is thrown. + * If the directory cannot be created (or does not already exist) + * then an IOException is thrown. + * + * @param directory directory to create, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws IOException if the directory cannot be created or the file already exists but is not a directory + */ + public static void forceMkdir(final File directory) throws IOException { + if (directory.exists()) { + if (!directory.isDirectory()) { + final String message = + "File " + + directory + + " exists and is " + + "not a directory. Unable to create directory."; + throw new IOException(message); + } + } else { + if (!directory.mkdirs()) { + // Double-check that some other thread or process hasn't made + // the directory in the background + if (!directory.isDirectory()) { + final String message = + "Unable to create directory " + directory; + throw new IOException(message); + } + } + } + } + + /** + * Makes any necessary but nonexistent parent directories for a given File. If the parent directory cannot be + * created then an IOException is thrown. + * + * @param file file with parent to create, must not be {@code null} + * @throws NullPointerException if the file is {@code null} + * @throws IOException if the parent directory cannot be created + * @since IO 2.5 + */ + public static void forceMkdirParent(final File file) throws IOException { + final File parent = file.getParentFile(); + if (parent == null) { + return; + } + forceMkdir(parent); + } + + + /** + * Determines whether the specified file is a Symbolic Link rather than an actual file. + *

    + * Will not return true if there is a Symbolic Link anywhere in the path, + * only if the specific file is. + *

    + * Note: the current implementation always returns {@code false} if + * the system is detected as Windows using + * {@link File#separatorChar} == '\\' + * + * @param file the file to check + * @return true if the file is a Symbolic Link + * @throws IOException if an IO error occurs while checking the file + * @since IO 2.0 + */ + public static boolean isSymlink(File file) throws IOException { + if (file == null) { + throw new NullPointerException("File must not be null"); + } + //FilenameUtils.isSystemWindows() + if (File.separatorChar == '\\') { + return false; + } + File fileInCanonicalDir = null; + if (file.getParent() == null) { + fileInCanonicalDir = file; + } else { + File canonicalDir = file.getParentFile().getCanonicalFile(); + fileInCanonicalDir = new File(canonicalDir, file.getName()); + } + + if (fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile())) { + return false; + } else { + return true; + } + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/IOUtils.java b/java/org/apache/tomcat/util/http/fileupload/IOUtils.java new file mode 100644 index 0000000..0941653 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/IOUtils.java @@ -0,0 +1,249 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * General IO stream manipulation utilities. + *

    + * This class provides static utility methods for input/output operations. + *

      + *
    • closeQuietly - these methods close a stream ignoring nulls and exceptions + *
    • toXxx/read - these methods read data from a stream + *
    • write - these methods write data to a stream + *
    • copy - these methods copy all the data from one stream to another + *
    • contentEquals - these methods compare the content of two streams + *
    + *

    + * The byte-to-char methods and char-to-byte methods involve a conversion step. + * Two methods are provided in each case, one that uses the platform default + * encoding and the other which allows you to specify an encoding. You are + * encouraged to always specify an encoding because relying on the platform + * default can lead to unexpected results, for example when moving from + * development to production. + *

    + * All the methods in this class that read a stream are buffered internally. + * This means that there is no cause to use a BufferedInputStream + * or BufferedReader. The default buffer size of 4K has been shown + * to be efficient in tests. + *

    + * Wherever possible, the methods in this class do not flush or close + * the stream. This is to avoid making non-portable assumptions about the + * streams' origin and further use. Thus the caller is still responsible for + * closing streams after use. + *

    + * Origin of code: Excalibur. + */ +public class IOUtils { + // NOTE: This class is focused on InputStream, OutputStream, Reader and + // Writer. Each method should take at least one of these as a parameter, + // or return one of them. + + /** + * Represents the end-of-file (or stream). + * @since IO 2.5 (made public) + */ + public static final int EOF = -1; + + + /** + * The default buffer size ({@value}) to use for + * {@link #copyLarge(InputStream, OutputStream)}. + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Closes a Closeable unconditionally. + *

    + * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. This is typically used in + * finally blocks. + *

    + * Example code: + *

    + *
    +     * Closeable closeable = null;
    +     * try {
    +     *     closeable = new FileReader("foo.txt");
    +     *     // process closeable
    +     *     closeable.close();
    +     * } catch (Exception e) {
    +     *     // error handling
    +     * } finally {
    +     *     IOUtils.closeQuietly(closeable);
    +     * }
    +     * 
    + *

    + * Closing all streams: + *

    + *
    +     * try {
    +     *     return IOUtils.copy(inputStream, outputStream);
    +     * } finally {
    +     *     IOUtils.closeQuietly(inputStream);
    +     *     IOUtils.closeQuietly(outputStream);
    +     * }
    +     * 
    + * + * @param closeable the objects to close, may be null or already closed + * @since IO 2.0 + */ + public static void closeQuietly(final Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (final IOException ioe) { + // ignore + } + } + + // copy from InputStream + //----------------------------------------------------------------------- + /** + * Copies bytes from an InputStream to an + * OutputStream. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

    + * Large streams (over 2GB) will return a bytes copied value of + * -1 after the copy has completed since the correct + * number of bytes cannot be returned as an int. For large streams + * use the copyLarge(InputStream, OutputStream) method. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since IO 1.1 + */ + public static int copy(final InputStream input, final OutputStream output) throws IOException { + final long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copies bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

    + * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since IO 1.3 + */ + public static long copyLarge(final InputStream input, final OutputStream output) + throws IOException { + + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Reads bytes from an input stream. + * This implementation guarantees that it will read as many bytes + * as possible before giving up; this may not always be the case for + * subclasses of {@link InputStream}. + * + * @param input where to read input from + * @param buffer destination + * @param offset initial offset into buffer + * @param length length to read, must be >= 0 + * @return actual length read; may be less than requested if EOF was reached + * @throws IOException if a read error occurs + * @since IO 2.2 + */ + public static int read(final InputStream input, final byte[] buffer, final int offset, final int length) + throws IOException { + if (length < 0) { + throw new IllegalArgumentException("Length must not be negative: " + length); + } + int remaining = length; + while (remaining > 0) { + final int location = length - remaining; + final int count = input.read(buffer, offset + location, remaining); + if (EOF == count) { // EOF + break; + } + remaining -= count; + } + return length - remaining; + } + + /** + * Reads the requested number of bytes or fail if there are not enough left. + *

    + * This allows for the possibility that {@link InputStream#read(byte[], int, int)} may + * not read as many bytes as requested (most likely because of reaching EOF). + * + * @param input where to read input from + * @param buffer destination + * @param offset initial offset into buffer + * @param length length to read, must be >= 0 + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if length is negative + * @throws EOFException if the number of bytes read was incorrect + * @since IO 2.2 + */ + public static void readFully(final InputStream input, final byte[] buffer, final int offset, final int length) + throws IOException { + final int actual = read(input, buffer, offset, length); + if (actual != length) { + throw new EOFException("Length to read: " + length + " actual: " + actual); + } + } + + /** + * Reads the requested number of bytes or fail if there are not enough left. + *

    + * This allows for the possibility that {@link InputStream#read(byte[], int, int)} may + * not read as many bytes as requested (most likely because of reaching EOF). + * + * @param input where to read input from + * @param buffer destination + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if length is negative + * @throws EOFException if the number of bytes read was incorrect + * @since IO 2.2 + */ + public static void readFully(final InputStream input, final byte[] buffer) throws IOException { + readFully(input, buffer, 0, buffer.length); + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java b/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java new file mode 100644 index 0000000..5018620 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java @@ -0,0 +1,62 @@ +/* + * 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.tomcat.util.http.fileupload; + +/** + * This exception is thrown in case of an invalid file name. + * A file name is invalid, if it contains a NUL character. + * Attackers might use this to circumvent security checks: + * For example, a malicious user might upload a file with the name + * "foo.exe\0.png". This file name might pass security checks (i.e. + * checks for the extension ".png"), while, depending on the underlying + * C library, it might create a file named "foo.exe", as the NUL + * character is the string terminator in C. + */ +public class InvalidFileNameException extends RuntimeException { + + /** + * Serial version UID, being used, if the exception + * is serialized. + */ + private static final long serialVersionUID = 7922042602454350470L; + + /** + * The file name causing the exception. + */ + private final String name; + + /** + * Creates a new instance. + * + * @param pName The file name causing the exception. + * @param pMessage A human readable error message. + */ + public InvalidFileNameException(final String pName, final String pMessage) { + super(pMessage); + name = pName; + } + + /** + * Returns the invalid file name. + * + * @return the invalid file name. + */ + public String getName() { + return name; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java b/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java new file mode 100644 index 0000000..0c76e66 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java @@ -0,0 +1,987 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import org.apache.tomcat.util.http.fileupload.impl.FileUploadIOException; +import org.apache.tomcat.util.http.fileupload.util.Closeable; +import org.apache.tomcat.util.http.fileupload.util.Streams; + +/** + *

    Low level API for processing file uploads. + * + *

    This class can be used to process data streams conforming to MIME + * 'multipart' format as defined in + * RFC 1867. Arbitrarily + * large amounts of data in the stream can be processed under constant + * memory usage. + * + *

    The format of the stream is defined in the following way:
    + * + * + * multipart-body := preamble 1*encapsulation close-delimiter epilogue
    + * encapsulation := delimiter body CRLF
    + * delimiter := "--" boundary CRLF
    + * close-delimiter := "--" boundary "--"
    + * preamble := <ignore>
    + * epilogue := <ignore>
    + * body := header-part CRLF body-part
    + * header-part := 1*header CRLF
    + * header := header-name ":" header-value
    + * header-name := <printable ascii characters except ":">
    + * header-value := <any ascii characters except CR & LF>
    + * body-data := <arbitrary data>
    + *
    + * + *

    Note that body-data can contain another mulipart entity. There + * is limited support for single pass processing of such nested + * streams. The nested stream is required to have a + * boundary token of the same length as the parent stream (see {@link + * #setBoundary(byte[])}). + * + *

    Here is an example of usage of this class.
    + * + *

    + *   try {
    + *     MultipartStream multipartStream = new MultipartStream(input, boundary);
    + *     boolean nextPart = multipartStream.skipPreamble();
    + *     OutputStream output;
    + *     while(nextPart) {
    + *       String header = multipartStream.readHeaders();
    + *       // process headers
    + *       // create some output stream
    + *       multipartStream.readBodyData(output);
    + *       nextPart = multipartStream.readBoundary();
    + *     }
    + *   } catch(MultipartStream.MalformedStreamException e) {
    + *     // the stream failed to follow required syntax
    + *   } catch(IOException e) {
    + *     // a read or write error occurred
    + *   }
    + * 
    + */ +public class MultipartStream { + + /** + * Internal class, which is used to invoke the + * {@link ProgressListener}. + */ + public static class ProgressNotifier { + + /** + * The listener to invoke. + */ + private final ProgressListener listener; + + /** + * Number of expected bytes, if known, or -1. + */ + private final long contentLength; + + /** + * Number of bytes, which have been read so far. + */ + private long bytesRead; + + /** + * Number of items, which have been read so far. + */ + private int items; + + /** + * Creates a new instance with the given listener + * and content length. + * + * @param pListener The listener to invoke. + * @param pContentLength The expected content length. + */ + public ProgressNotifier(final ProgressListener pListener, final long pContentLength) { + listener = pListener; + contentLength = pContentLength; + } + + /** + * Called to indicate that bytes have been read. + * + * @param pBytes Number of bytes, which have been read. + */ + void noteBytesRead(final int pBytes) { + /* Indicates, that the given number of bytes have been read from + * the input stream. + */ + bytesRead += pBytes; + notifyListener(); + } + + /** + * Called to indicate, that a new file item has been detected. + */ + public void noteItem() { + ++items; + notifyListener(); + } + + /** + * Called for notifying the listener. + */ + private void notifyListener() { + if (listener != null) { + listener.update(bytesRead, contentLength, items); + } + } + + } + + // ----------------------------------------------------- Manifest constants + + /** + * The Carriage Return ASCII character value. + */ + public static final byte CR = 0x0D; + + /** + * The Line Feed ASCII character value. + */ + public static final byte LF = 0x0A; + + /** + * The dash (-) ASCII character value. + */ + public static final byte DASH = 0x2D; + + /** + * The maximum length of {@code header-part} that will be + * processed (10 kilobytes = 10240 bytes.). + */ + public static final int HEADER_PART_SIZE_MAX = 10240; + + /** + * The default length of the buffer used for processing a request. + */ + protected static final int DEFAULT_BUFSIZE = 4096; + + /** + * A byte sequence that marks the end of {@code header-part} + * ({@code CRLFCRLF}). + */ + protected static final byte[] HEADER_SEPARATOR = {CR, LF, CR, LF}; + + /** + * A byte sequence that that follows a delimiter that will be + * followed by an encapsulation ({@code CRLF}). + */ + protected static final byte[] FIELD_SEPARATOR = {CR, LF}; + + /** + * A byte sequence that that follows a delimiter of the last + * encapsulation in the stream ({@code --}). + */ + protected static final byte[] STREAM_TERMINATOR = {DASH, DASH}; + + /** + * A byte sequence that precedes a boundary ({@code CRLF--}). + */ + protected static final byte[] BOUNDARY_PREFIX = {CR, LF, DASH, DASH}; + + // ----------------------------------------------------------- Data members + + /** + * The input stream from which data is read. + */ + private final InputStream input; + + /** + * The length of the boundary token plus the leading {@code CRLF--}. + */ + private int boundaryLength; + + /** + * The amount of data, in bytes, that must be kept in the buffer in order + * to detect delimiters reliably. + */ + private final int keepRegion; + + /** + * The byte sequence that partitions the stream. + */ + private final byte[] boundary; + + /** + * The table for Knuth-Morris-Pratt search algorithm. + */ + private final int[] boundaryTable; + + /** + * The length of the buffer used for processing the request. + */ + private final int bufSize; + + /** + * The buffer used for processing the request. + */ + private final byte[] buffer; + + /** + * The index of first valid character in the buffer. + *
    + * 0 <= head < bufSize + */ + private int head; + + /** + * The index of last valid character in the buffer + 1. + *
    + * 0 <= tail <= bufSize + */ + private int tail; + + /** + * The content encoding to use when reading headers. + */ + private String headerEncoding; + + /** + * The progress notifier, if any, or null. + */ + private final ProgressNotifier notifier; + + // ----------------------------------------------------------- Constructors + + /** + *

    Constructs a {@code MultipartStream} with a custom size buffer. + * + *

    Note that the buffer must be at least big enough to contain the + * boundary string, plus 4 characters for CR/LF and double dash, plus at + * least one byte of data. Too small a buffer size setting will degrade + * performance. + * + * @param input The {@code InputStream} to serve as a data source. + * @param boundary The token used for dividing the stream into + * {@code encapsulations}. + * @param bufSize The size of the buffer to be used, in bytes. + * @param pNotifier The notifier, which is used for calling the + * progress listener, if any. + * + * @throws IllegalArgumentException If the buffer size is too small + * + * @since FileUpload 1.3.1 + */ + public MultipartStream(final InputStream input, + final byte[] boundary, + final int bufSize, + final ProgressNotifier pNotifier) { + + if (boundary == null) { + throw new IllegalArgumentException("boundary may not be null"); + } + // We prepend CR/LF to the boundary to chop trailing CR/LF from + // body-data tokens. + this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length; + if (bufSize < this.boundaryLength + 1) { + throw new IllegalArgumentException( + "The buffer size specified for the MultipartStream is too small"); + } + + this.input = input; + this.bufSize = Math.max(bufSize, boundaryLength * 2); + this.buffer = new byte[this.bufSize]; + this.notifier = pNotifier; + + this.boundary = new byte[this.boundaryLength]; + this.boundaryTable = new int[this.boundaryLength + 1]; + this.keepRegion = this.boundary.length; + + System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0, + BOUNDARY_PREFIX.length); + System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, + boundary.length); + computeBoundaryTable(); + + head = 0; + tail = 0; + } + + /** + *

    Constructs a {@code MultipartStream} with a default size buffer. + * + * @param input The {@code InputStream} to serve as a data source. + * @param boundary The token used for dividing the stream into + * {@code encapsulations}. + * @param pNotifier An object for calling the progress listener, if any. + * + * + * @see #MultipartStream(InputStream, byte[], int, ProgressNotifier) + */ + public MultipartStream(final InputStream input, + final byte[] boundary, + final ProgressNotifier pNotifier) { + this(input, boundary, DEFAULT_BUFSIZE, pNotifier); + } + + // --------------------------------------------------------- Public methods + + /** + * Retrieves the character encoding used when reading the headers of an + * individual part. When not specified, or {@code null}, the platform + * default encoding is used. + * + * @return The encoding used to read part headers. + */ + public String getHeaderEncoding() { + return headerEncoding; + } + + /** + * Specifies the character encoding to be used when reading the headers of + * individual parts. When not specified, or {@code null}, the platform + * default encoding is used. + * + * @param encoding The encoding used to read part headers. + */ + public void setHeaderEncoding(final String encoding) { + headerEncoding = encoding; + } + + /** + * Reads a byte from the {@code buffer}, and refills it as + * necessary. + * + * @return The next byte from the input stream. + * + * @throws IOException if there is no more data available. + */ + public byte readByte() throws IOException { + // Buffer depleted ? + if (head == tail) { + head = 0; + // Refill. + tail = input.read(buffer, head, bufSize); + if (tail == -1) { + // No more data available. + throw new IOException("No more data is available"); + } + if (notifier != null) { + notifier.noteBytesRead(tail); + } + } + return buffer[head++]; + } + + /** + * Skips a {@code boundary} token, and checks whether more + * {@code encapsulations} are contained in the stream. + * + * @return {@code true} if there are more encapsulations in + * this stream; {@code false} otherwise. + * + * @throws FileUploadIOException if the bytes read from the stream exceeded the size limits + * @throws MalformedStreamException if the stream ends unexpectedly or + * fails to follow required syntax. + */ + public boolean readBoundary() + throws FileUploadIOException, MalformedStreamException { + final byte[] marker = new byte[2]; + final boolean nextChunk; + + head += boundaryLength; + try { + marker[0] = readByte(); + if (marker[0] == LF) { + // Work around IE5 Mac bug with input type=image. + // Because the boundary delimiter, not including the trailing + // CRLF, must not appear within any file (RFC 2046, section + // 5.1.1), we know the missing CR is due to a buggy browser + // rather than a file containing something similar to a + // boundary. + return true; + } + + marker[1] = readByte(); + if (arrayequals(marker, STREAM_TERMINATOR, 2)) { + nextChunk = false; + } else if (arrayequals(marker, FIELD_SEPARATOR, 2)) { + nextChunk = true; + } else { + throw new MalformedStreamException( + "Unexpected characters follow a boundary"); + } + } catch (final FileUploadIOException e) { + // wraps a SizeException, re-throw as it will be unwrapped later + throw e; + } catch (final IOException e) { + throw new MalformedStreamException("Stream ended unexpectedly"); + } + return nextChunk; + } + + /** + *

    Changes the boundary token used for partitioning the stream. + * + *

    This method allows single pass processing of nested multipart + * streams. + * + *

    The boundary token of the nested stream is {@code required} + * to be of the same length as the boundary token in parent stream. + * + *

    Restoring the parent stream boundary token after processing of a + * nested stream is left to the application. + * + * @param boundary The boundary to be used for parsing of the nested + * stream. + * + * @throws IllegalBoundaryException if the {@code boundary} + * has a different length than the one + * being currently parsed. + */ + public void setBoundary(final byte[] boundary) + throws IllegalBoundaryException { + if (boundary.length != boundaryLength - BOUNDARY_PREFIX.length) { + throw new IllegalBoundaryException( + "The length of a boundary token cannot be changed"); + } + System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, + boundary.length); + computeBoundaryTable(); + } + + /** + * Compute the table used for Knuth-Morris-Pratt search algorithm. + */ + private void computeBoundaryTable() { + int position = 2; + int candidate = 0; + + boundaryTable[0] = -1; + boundaryTable[1] = 0; + + while (position <= boundaryLength) { + if (boundary[position - 1] == boundary[candidate]) { + boundaryTable[position] = candidate + 1; + candidate++; + position++; + } else if (candidate > 0) { + candidate = boundaryTable[candidate]; + } else { + boundaryTable[position] = 0; + position++; + } + } + } + + /** + *

    Reads the {@code header-part} of the current + * {@code encapsulation}. + * + *

    Headers are returned verbatim to the input stream, including the + * trailing {@code CRLF} marker. Parsing is left to the + * application. + * + *

    TODO allow limiting maximum header size to + * protect against abuse. + * + * @return The {@code header-part} of the current encapsulation. + * + * @throws FileUploadIOException if the bytes read from the stream exceeded the size limits. + * @throws MalformedStreamException if the stream ends unexpectedly. + */ + public String readHeaders() throws FileUploadIOException, MalformedStreamException { + int i = 0; + byte b; + // to support multi-byte characters + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int size = 0; + while (i < HEADER_SEPARATOR.length) { + try { + b = readByte(); + } catch (final FileUploadIOException e) { + // wraps a SizeException, re-throw as it will be unwrapped later + throw e; + } catch (final IOException e) { + throw new MalformedStreamException("Stream ended unexpectedly"); + } + if (++size > HEADER_PART_SIZE_MAX) { + throw new MalformedStreamException(String.format( + "Header section has more than %s bytes (maybe it is not properly terminated)", + Integer.valueOf(HEADER_PART_SIZE_MAX))); + } + if (b == HEADER_SEPARATOR[i]) { + i++; + } else { + i = 0; + } + baos.write(b); + } + + String headers; + if (headerEncoding != null) { + try { + headers = baos.toString(headerEncoding); + } catch (final UnsupportedEncodingException e) { + // Fall back to platform default if specified encoding is not + // supported. + headers = baos.toString(); + } + } else { + headers = baos.toString(); + } + + return headers; + } + + /** + *

    Reads {@code body-data} from the current + * {@code encapsulation} and writes its contents into the + * output {@code Stream}. + * + *

    Arbitrary large amounts of data can be processed by this + * method using a constant size buffer. (see {@link + * #MultipartStream(InputStream,byte[],int, + * MultipartStream.ProgressNotifier) constructor}). + * + * @param output The {@code Stream} to write data into. May + * be null, in which case this method is equivalent + * to {@link #discardBodyData()}. + * + * @return the amount of data written. + * + * @throws MalformedStreamException if the stream ends unexpectedly. + * @throws IOException if an i/o error occurs. + */ + public int readBodyData(final OutputStream output) + throws MalformedStreamException, IOException { + return (int) Streams.copy(newInputStream(), output, false); // N.B. Streams.copy closes the input stream + } + + /** + * Creates a new {@link ItemInputStream}. + * @return A new instance of {@link ItemInputStream}. + */ + public ItemInputStream newInputStream() { + return new ItemInputStream(); + } + + /** + *

    Reads {@code body-data} from the current + * {@code encapsulation} and discards it. + * + *

    Use this method to skip encapsulations you don't need or don't + * understand. + * + * @return The amount of data discarded. + * + * @throws MalformedStreamException if the stream ends unexpectedly. + * @throws IOException if an i/o error occurs. + */ + public int discardBodyData() throws MalformedStreamException, IOException { + return readBodyData(null); + } + + /** + * Finds the beginning of the first {@code encapsulation}. + * + * @return {@code true} if an {@code encapsulation} was found in + * the stream. + * + * @throws IOException if an i/o error occurs. + */ + public boolean skipPreamble() throws IOException { + // First delimiter may be not preceded with a CRLF. + System.arraycopy(boundary, 2, boundary, 0, boundary.length - 2); + boundaryLength = boundary.length - 2; + computeBoundaryTable(); + try { + // Discard all data up to the delimiter. + discardBodyData(); + + // Read boundary - if succeeded, the stream contains an + // encapsulation. + return readBoundary(); + } catch (final MalformedStreamException e) { + return false; + } finally { + // Restore delimiter. + System.arraycopy(boundary, 0, boundary, 2, boundary.length - 2); + boundaryLength = boundary.length; + boundary[0] = CR; + boundary[1] = LF; + computeBoundaryTable(); + } + } + + /** + * Compares {@code count} first bytes in the arrays + * {@code a} and {@code b}. + * + * @param a The first array to compare. + * @param b The second array to compare. + * @param count How many bytes should be compared. + * + * @return {@code true} if {@code count} first bytes in arrays + * {@code a} and {@code b} are equal. + */ + public static boolean arrayequals(final byte[] a, + final byte[] b, + final int count) { + for (int i = 0; i < count; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + /** + * Searches for the {@code boundary} in the {@code buffer} + * region delimited by {@code head} and {@code tail}. + * + * @return The position of the boundary found, counting from the + * beginning of the {@code buffer}, or {@code -1} if + * not found. + */ + protected int findSeparator() { + + int bufferPos = this.head; + int tablePos = 0; + + while (bufferPos < this.tail) { + while (tablePos >= 0 && buffer[bufferPos] != boundary[tablePos]) { + tablePos = boundaryTable[tablePos]; + } + bufferPos++; + tablePos++; + if (tablePos == boundaryLength) { + return bufferPos - boundaryLength; + } + } + return -1; + } + + /** + * Thrown to indicate that the input stream fails to follow the + * required syntax. + */ + public static class MalformedStreamException extends IOException { + + /** + * The UID to use when serializing this instance. + */ + private static final long serialVersionUID = 6466926458059796677L; + + /** + * Constructs a {@code MalformedStreamException} with no + * detail message. + */ + public MalformedStreamException() { + } + + /** + * Constructs an {@code MalformedStreamException} with + * the specified detail message. + * + * @param message The detail message. + */ + public MalformedStreamException(final String message) { + super(message); + } + + } + + /** + * Thrown upon attempt of setting an invalid boundary token. + */ + public static class IllegalBoundaryException extends IOException { + + /** + * The UID to use when serializing this instance. + */ + private static final long serialVersionUID = -161533165102632918L; + + /** + * Constructs an {@code IllegalBoundaryException} with no + * detail message. + */ + public IllegalBoundaryException() { + } + + /** + * Constructs an {@code IllegalBoundaryException} with + * the specified detail message. + * + * @param message The detail message. + */ + public IllegalBoundaryException(final String message) { + super(message); + } + + } + + /** + * An {@link InputStream} for reading an items contents. + */ + public class ItemInputStream extends InputStream implements Closeable { + + /** + * The number of bytes, which have been read so far. + */ + private long total; + + /** + * The number of bytes, which must be hold, because + * they might be a part of the boundary. + */ + private int pad; + + /** + * The current offset in the buffer. + */ + private int pos; + + /** + * Whether the stream is already closed. + */ + private boolean closed; + + /** + * Creates a new instance. + */ + ItemInputStream() { + findSeparator(); + } + + /** + * Called for finding the separator. + */ + private void findSeparator() { + pos = MultipartStream.this.findSeparator(); + if (pos == -1) { + if (tail - head > keepRegion) { + pad = keepRegion; + } else { + pad = tail - head; + } + } + } + + /** + * Returns the number of bytes, which have been read + * by the stream. + * + * @return Number of bytes, which have been read so far. + */ + public long getBytesRead() { + return total; + } + + /** + * Returns the number of bytes, which are currently + * available, without blocking. + * + * @throws IOException An I/O error occurs. + * @return Number of bytes in the buffer. + */ + @Override + public int available() throws IOException { + if (pos == -1) { + return tail - head - pad; + } + return pos - head; + } + + /** + * Offset when converting negative bytes to integers. + */ + private static final int BYTE_POSITIVE_OFFSET = 256; + + /** + * Returns the next byte in the stream. + * + * @return The next byte in the stream, as a non-negative + * integer, or -1 for EOF. + * @throws IOException An I/O error occurred. + */ + @Override + public int read() throws IOException { + if (closed) { + throw new FileItemStream.ItemSkippedException(); + } + if (available() == 0 && makeAvailable() == 0) { + return -1; + } + ++total; + final int b = buffer[head++]; + if (b >= 0) { + return b; + } + return b + BYTE_POSITIVE_OFFSET; + } + + /** + * Reads bytes into the given buffer. + * + * @param b The destination buffer, where to write to. + * @param off Offset of the first byte in the buffer. + * @param len Maximum number of bytes to read. + * @return Number of bytes, which have been actually read, + * or -1 for EOF. + * @throws IOException An I/O error occurred. + */ + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + if (closed) { + throw new FileItemStream.ItemSkippedException(); + } + if (len == 0) { + return 0; + } + int res = available(); + if (res == 0) { + res = makeAvailable(); + if (res == 0) { + return -1; + } + } + res = Math.min(res, len); + System.arraycopy(buffer, head, b, off, res); + head += res; + total += res; + return res; + } + + /** + * Closes the input stream. + * + * @throws IOException An I/O error occurred. + */ + @Override + public void close() throws IOException { + close(false); + } + + /** + * Closes the input stream. + * + * @param pCloseUnderlying Whether to close the underlying stream + * (hard close) + * @throws IOException An I/O error occurred. + */ + public void close(final boolean pCloseUnderlying) throws IOException { + if (closed) { + return; + } + if (pCloseUnderlying) { + closed = true; + input.close(); + } else { + for (;;) { + int av = available(); + if (av == 0) { + av = makeAvailable(); + if (av == 0) { + break; + } + } + skip(av); + } + } + closed = true; + } + + /** + * Skips the given number of bytes. + * + * @param bytes Number of bytes to skip. + * @return The number of bytes, which have actually been + * skipped. + * @throws IOException An I/O error occurred. + */ + @Override + public long skip(final long bytes) throws IOException { + if (closed) { + throw new FileItemStream.ItemSkippedException(); + } + int av = available(); + if (av == 0) { + av = makeAvailable(); + if (av == 0) { + return 0; + } + } + final long res = Math.min(av, bytes); + head += res; + return res; + } + + /** + * Attempts to read more data. + * + * @return Number of available bytes + * @throws IOException An I/O error occurred. + */ + private int makeAvailable() throws IOException { + if (pos != -1) { + return 0; + } + + // Move the data to the beginning of the buffer. + total += tail - head - pad; + System.arraycopy(buffer, tail - pad, buffer, 0, pad); + + // Refill buffer with new data. + head = 0; + tail = pad; + + for (;;) { + final int bytesRead = input.read(buffer, tail, bufSize - tail); + if (bytesRead == -1) { + // The last pad amount is left in the buffer. + // Boundary can't be in there so signal an error + // condition. + final String msg = "Stream ended unexpectedly"; + throw new MalformedStreamException(msg); + } + if (notifier != null) { + notifier.noteBytesRead(bytesRead); + } + tail += bytesRead; + + findSeparator(); + final int av = available(); + + if (av > 0 || pos != -1) { + return av; + } + } + } + + /** + * Returns, whether the stream is closed. + * + * @return True, if the stream is closed, otherwise false. + */ + @Override + public boolean isClosed() { + return closed; + } + + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java b/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java new file mode 100644 index 0000000..91a97ae --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java @@ -0,0 +1,340 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.apache.tomcat.util.http.fileupload.util.mime.MimeUtility; +import org.apache.tomcat.util.http.fileupload.util.mime.RFC2231Utility; + +/** + * A simple parser intended to parse sequences of name/value pairs. + * + * Parameter values are expected to be enclosed in quotes if they + * contain unsafe characters, such as '=' characters or separators. + * Parameter values are optional and can be omitted. + * + *

    + * {@code param1 = value; param2 = "anything goes; really"; param3} + *

    + */ +public class ParameterParser { + + /** + * String to be parsed. + */ + private char[] chars = null; + + /** + * Current position in the string. + */ + private int pos = 0; + + /** + * Maximum position in the string. + */ + private int len = 0; + + /** + * Start of a token. + */ + private int i1 = 0; + + /** + * End of a token. + */ + private int i2 = 0; + + /** + * Whether names stored in the map should be converted to lower case. + */ + private boolean lowerCaseNames = false; + + /** + * Default ParameterParser constructor. + */ + public ParameterParser() { + } + + /** + * Are there any characters left to parse? + * + * @return {@code true} if there are unparsed characters, + * {@code false} otherwise. + */ + private boolean hasChar() { + return this.pos < this.len; + } + + /** + * A helper method to process the parsed token. This method removes + * leading and trailing blanks as well as enclosing quotation marks, + * when necessary. + * + * @param quoted {@code true} if quotation marks are expected, + * {@code false} otherwise. + * @return the token + */ + private String getToken(final boolean quoted) { + // Trim leading white spaces + while ((i1 < i2) && (Character.isWhitespace(chars[i1]))) { + i1++; + } + // Trim trailing white spaces + while ((i2 > i1) && (Character.isWhitespace(chars[i2 - 1]))) { + i2--; + } + // Strip away quotation marks if necessary + if (quoted + && ((i2 - i1) >= 2) + && (chars[i1] == '"') + && (chars[i2 - 1] == '"')) { + i1++; + i2--; + } + String result = null; + if (i2 > i1) { + result = new String(chars, i1, i2 - i1); + } + return result; + } + + /** + * Tests if the given character is present in the array of characters. + * + * @param ch the character to test for presence in the array of characters + * @param charray the array of characters to test against + * + * @return {@code true} if the character is present in the array of + * characters, {@code false} otherwise. + */ + private boolean isOneOf(final char ch, final char[] charray) { + boolean result = false; + for (final char element : charray) { + if (ch == element) { + result = true; + break; + } + } + return result; + } + + /** + * Parses out a token until any of the given terminators + * is encountered. + * + * @param terminators the array of terminating characters. Any of these + * characters when encountered signify the end of the token + * + * @return the token + */ + private String parseToken(final char[] terminators) { + char ch; + i1 = pos; + i2 = pos; + while (hasChar()) { + ch = chars[pos]; + if (isOneOf(ch, terminators)) { + break; + } + i2++; + pos++; + } + return getToken(false); + } + + /** + * Parses out a token until any of the given terminators + * is encountered outside the quotation marks. + * + * @param terminators the array of terminating characters. Any of these + * characters when encountered outside the quotation marks signify the end + * of the token + * + * @return the token + */ + private String parseQuotedToken(final char[] terminators) { + char ch; + i1 = pos; + i2 = pos; + boolean quoted = false; + boolean charEscaped = false; + while (hasChar()) { + ch = chars[pos]; + if (!quoted && isOneOf(ch, terminators)) { + break; + } + if (!charEscaped && ch == '"') { + quoted = !quoted; + } + charEscaped = (!charEscaped && ch == '\\'); + i2++; + pos++; + + } + return getToken(true); + } + + /** + * Returns {@code true} if parameter names are to be converted to lower + * case when name/value pairs are parsed. + * + * @return {@code true} if parameter names are to be + * converted to lower case when name/value pairs are parsed. + * Otherwise returns {@code false} + */ + public boolean isLowerCaseNames() { + return this.lowerCaseNames; + } + + /** + * Sets the flag if parameter names are to be converted to lower case when + * name/value pairs are parsed. + * + * @param b {@code true} if parameter names are to be + * converted to lower case when name/value pairs are parsed. + * {@code false} otherwise. + */ + public void setLowerCaseNames(final boolean b) { + this.lowerCaseNames = b; + } + + /** + * Extracts a map of name/value pairs from the given string. Names are + * expected to be unique. Multiple separators may be specified and + * the earliest found in the input string is used. + * + * @param str the string that contains a sequence of name/value pairs + * @param separators the name/value pairs separators + * + * @return a map of name/value pairs + */ + public Map parse(final String str, final char[] separators) { + if (separators == null || separators.length == 0) { + return new HashMap<>(); + } + char separator = separators[0]; + if (str != null) { + int idx = str.length(); + for (final char separator2 : separators) { + final int tmp = str.indexOf(separator2); + if (tmp != -1 && tmp < idx) { + idx = tmp; + separator = separator2; + } + } + } + return parse(str, separator); + } + + /** + * Extracts a map of name/value pairs from the given string. Names are + * expected to be unique. + * + * @param str the string that contains a sequence of name/value pairs + * @param separator the name/value pairs separator + * + * @return a map of name/value pairs + */ + public Map parse(final String str, final char separator) { + if (str == null) { + return new HashMap<>(); + } + return parse(str.toCharArray(), separator); + } + + /** + * Extracts a map of name/value pairs from the given array of + * characters. Names are expected to be unique. + * + * @param charArray the array of characters that contains a sequence of + * name/value pairs + * @param separator the name/value pairs separator + * + * @return a map of name/value pairs + */ + public Map parse(final char[] charArray, final char separator) { + if (charArray == null) { + return new HashMap<>(); + } + return parse(charArray, 0, charArray.length, separator); + } + + /** + * Extracts a map of name/value pairs from the given array of + * characters. Names are expected to be unique. + * + * @param charArray the array of characters that contains a sequence of + * name/value pairs + * @param offset - the initial offset. + * @param length - the length. + * @param separator the name/value pairs separator + * + * @return a map of name/value pairs + */ + public Map parse( + final char[] charArray, + final int offset, + final int length, + final char separator) { + + if (charArray == null) { + return new HashMap<>(); + } + final HashMap params = new HashMap<>(); + this.chars = charArray.clone(); + this.pos = offset; + this.len = length; + + String paramName; + String paramValue; + while (hasChar()) { + paramName = parseToken(new char[] { + '=', separator }); + paramValue = null; + if (hasChar() && (charArray[pos] == '=')) { + pos++; // skip '=' + paramValue = parseQuotedToken(new char[] { + separator }); + + if (paramValue != null) { + try { + paramValue = RFC2231Utility.hasEncodedValue(paramName) ? RFC2231Utility.decodeText(paramValue) + : MimeUtility.decodeText(paramValue); + } catch (final UnsupportedEncodingException e) { + // let's keep the original value in this case + } + } + } + if (hasChar() && (charArray[pos] == separator)) { + pos++; // skip separator + } + if ((paramName != null) && !paramName.isEmpty()) { + paramName = RFC2231Utility.stripDelimiter(paramName); + if (this.lowerCaseNames) { + paramName = paramName.toLowerCase(Locale.ENGLISH); + } + params.put(paramName, paramValue); + } + } + return params; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java b/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java new file mode 100644 index 0000000..931c99d --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java @@ -0,0 +1,37 @@ +/* + * 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.tomcat.util.http.fileupload; + +/** + * The {@link ProgressListener} may be used to display a progress bar + * or do stuff like that. + */ +public interface ProgressListener { + + /** + * Updates the listeners status information. + * + * @param pBytesRead The total number of bytes, which have been read + * so far. + * @param pContentLength The total number of bytes, which are being + * read. May be -1, if this number is unknown. + * @param pItems The number of the field, which is currently being + * read. (0 = no item so far, 1 = first item is being read, ...) + */ + void update(long pBytesRead, long pContentLength, int pItems); + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/RequestContext.java b/java/org/apache/tomcat/util/http/fileupload/RequestContext.java new file mode 100644 index 0000000..8fb222c --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/RequestContext.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.io.InputStream; + +/** + *

    Abstracts access to the request information needed for file uploads. This + * interface should be implemented for each type of request that may be + * handled by FileUpload, such as servlets and portlets.

    + * + * @since FileUpload 1.1 + */ +public interface RequestContext { + + /** + * Retrieve the character encoding for the request. + * + * @return The character encoding for the request. + */ + String getCharacterEncoding(); + + /** + * Retrieve the content type of the request. + * + * @return The content type of the request. + */ + String getContentType(); + + /** + * Retrieve the input stream for the request. + * + * @return The input stream for the request. + * + * @throws IOException if a problem occurs. + */ + InputStream getInputStream() throws IOException; + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/ThresholdingOutputStream.java b/java/org/apache/tomcat/util/http/fileupload/ThresholdingOutputStream.java new file mode 100644 index 0000000..8f1fcaf --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/ThresholdingOutputStream.java @@ -0,0 +1,226 @@ +/* + * 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.tomcat.util.http.fileupload; + +import java.io.IOException; +import java.io.OutputStream; + + +/** + * An output stream which triggers an event when a specified number of bytes of + * data have been written to it. The event can be used, for example, to throw + * an exception if a maximum has been reached, or to switch the underlying + * stream type when the threshold is exceeded. + *

    + * This class overrides all OutputStream methods. However, these + * overrides ultimately call the corresponding methods in the underlying output + * stream implementation. + *

    + * NOTE: This implementation may trigger the event before the threshold + * is actually reached, since it triggers when a pending write operation would + * cause the threshold to be exceeded. + */ +public abstract class ThresholdingOutputStream + extends OutputStream +{ + + // ----------------------------------------------------------- Data members + + + /** + * The threshold at which the event will be triggered. + */ + private final int threshold; + + + /** + * The number of bytes written to the output stream. + */ + private long written; + + + /** + * Whether or not the configured threshold has been exceeded. + */ + private boolean thresholdExceeded; + + + // ----------------------------------------------------------- Constructors + + + /** + * Constructs an instance of this class which will trigger an event at the + * specified threshold. + * + * @param threshold The number of bytes at which to trigger an event. + */ + public ThresholdingOutputStream(final int threshold) + { + this.threshold = threshold; + } + + + // --------------------------------------------------- OutputStream methods + + + /** + * Writes the specified byte to this output stream. + * + * @param b The byte to be written. + * + * @throws IOException if an error occurs. + */ + @Override + public void write(final int b) throws IOException + { + checkThreshold(1); + getStream().write(b); + written++; + } + + + /** + * Writes b.length bytes from the specified byte array to this + * output stream. + * + * @param b The array of bytes to be written. + * + * @throws IOException if an error occurs. + */ + @Override + public void write(final byte b[]) throws IOException + { + checkThreshold(b.length); + getStream().write(b); + written += b.length; + } + + + /** + * Writes len bytes from the specified byte array starting at + * offset off to this output stream. + * + * @param b The byte array from which the data will be written. + * @param off The start offset in the byte array. + * @param len The number of bytes to write. + * + * @throws IOException if an error occurs. + */ + @Override + public void write(final byte b[], final int off, final int len) throws IOException + { + checkThreshold(len); + getStream().write(b, off, len); + written += len; + } + + + /** + * Flushes this output stream and forces any buffered output bytes to be + * written out. + * + * @throws IOException if an error occurs. + */ + @Override + public void flush() throws IOException + { + getStream().flush(); + } + + + /** + * Closes this output stream and releases any system resources associated + * with this stream. + * + * @throws IOException if an error occurs. + */ + @Override + public void close() throws IOException + { + try + { + flush(); + } + catch (final IOException ignored) + { + // ignore + } + getStream().close(); + } + + + // --------------------------------------------------------- Public methods + + + /** + * Determines whether or not the configured threshold has been exceeded for + * this output stream. + * + * @return {@code true} if the threshold has been reached; + * {@code false} otherwise. + */ + public boolean isThresholdExceeded() + { + return written > threshold; + } + + + // ------------------------------------------------------ Protected methods + + + /** + * Checks to see if writing the specified number of bytes would cause the + * configured threshold to be exceeded. If so, triggers an event to allow + * a concrete implementation to take action on this. + * + * @param count The number of bytes about to be written to the underlying + * output stream. + * + * @throws IOException if an error occurs. + */ + protected void checkThreshold(final int count) throws IOException + { + if (!thresholdExceeded && written + count > threshold) + { + thresholdExceeded = true; + thresholdReached(); + } + } + + // ------------------------------------------------------- Abstract methods + + + /** + * Returns the underlying output stream, to which the corresponding + * OutputStream methods in this class will ultimately delegate. + * + * @return The underlying output stream. + * + * @throws IOException if an error occurs. + */ + protected abstract OutputStream getStream() throws IOException; + + + /** + * Indicates that the configured threshold has been reached, and that a + * subclass should take whatever action necessary on this event. This may + * include changing the underlying output stream. + * + * @throws IOException if an error occurs. + */ + protected abstract void thresholdReached() throws IOException; +} diff --git a/java/org/apache/tomcat/util/http/fileupload/UploadContext.java b/java/org/apache/tomcat/util/http/fileupload/UploadContext.java new file mode 100644 index 0000000..2807677 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/UploadContext.java @@ -0,0 +1,39 @@ +/* + * 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.tomcat.util.http.fileupload; + +/** + * Enhanced access to the request information needed for file uploads, + * which fixes the Content Length data access in {@link RequestContext}. + * + * The reason of introducing this new interface is just for backward compatibility + * and it might vanish for a refactored 2.x version moving the new method into + * RequestContext again. + * + * @since FileUpload 1.3 + */ +public interface UploadContext extends RequestContext { + + /** + * Retrieve the content length of the request. + * + * @return The content length of the request. + * @since FileUpload 1.3 + */ + long contentLength(); + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java new file mode 100644 index 0000000..e35c1ea --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java @@ -0,0 +1,625 @@ +/* + * 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.tomcat.util.http.fileupload.disk; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; +import java.nio.file.Files; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.tomcat.util.http.fileupload.DeferredFileOutputStream; +import org.apache.tomcat.util.http.fileupload.FileItem; +import org.apache.tomcat.util.http.fileupload.FileItemHeaders; +import org.apache.tomcat.util.http.fileupload.FileUploadException; +import org.apache.tomcat.util.http.fileupload.IOUtils; +import org.apache.tomcat.util.http.fileupload.ParameterParser; +import org.apache.tomcat.util.http.fileupload.util.Streams; + +/** + *

    The default implementation of the + * {@link org.apache.tomcat.util.http.fileupload.FileItem FileItem} interface. + * + *

    After retrieving an instance of this class from a {@link + * org.apache.tomcat.util.http.fileupload.FileUpload FileUpload} instance (see + * {@link org.apache.tomcat.util.http.fileupload.FileUpload + * #parseRequest(org.apache.tomcat.util.http.fileupload.RequestContext)}), you + * may either request all contents of file at once using {@link #get()} or + * request an {@link java.io.InputStream InputStream} with + * {@link #getInputStream()} and process the file without attempting to load + * it into memory, which may come handy with large files. + * + *

    Temporary files, which are created for file items, will be deleted when + * the associated request is recycled.

    + * + * @since FileUpload 1.1 + */ +public class DiskFileItem + implements FileItem { + + // ----------------------------------------------------- Manifest constants + + /** + * Default content charset to be used when no explicit charset + * parameter is provided by the sender. Media subtypes of the + * "text" type are defined to have a default charset value of + * "ISO-8859-1" when received via HTTP. + */ + public static final String DEFAULT_CHARSET = "ISO-8859-1"; + + // ----------------------------------------------------------- Data members + + /** + * UID used in unique file name generation. + */ + private static final String UID = + UUID.randomUUID().toString().replace('-', '_'); + + /** + * Counter used in unique identifier generation. + */ + private static final AtomicInteger COUNTER = new AtomicInteger(0); + + /** + * The name of the form field as provided by the browser. + */ + private String fieldName; + + /** + * The content type passed by the browser, or {@code null} if + * not defined. + */ + private final String contentType; + + /** + * Whether or not this item is a simple form field. + */ + private boolean isFormField; + + /** + * The original file name in the user's file system. + */ + private final String fileName; + + /** + * The size of the item, in bytes. This is used to cache the size when a + * file item is moved from its original location. + */ + private long size = -1; + + + /** + * The threshold above which uploads will be stored on disk. + */ + private final int sizeThreshold; + + /** + * The directory in which uploaded files will be stored, if stored on disk. + */ + private final File repository; + + /** + * Cached contents of the file. + */ + private byte[] cachedContent; + + /** + * Output stream for this item. + */ + private transient DeferredFileOutputStream dfos; + + /** + * The temporary file to use. + */ + private transient File tempFile; + + /** + * The file items headers. + */ + private FileItemHeaders headers; + + /** + * Default content charset to be used when no explicit charset + * parameter is provided by the sender. + */ + private String defaultCharset = DEFAULT_CHARSET; + + // ----------------------------------------------------------- Constructors + + /** + * Constructs a new {@code DiskFileItem} instance. + * + * @param fieldName The name of the form field. + * @param contentType The content type passed by the browser or + * {@code null} if not specified. + * @param isFormField Whether or not this item is a plain form field, as + * opposed to a file upload. + * @param fileName The original file name in the user's file system, or + * {@code null} if not specified. + * @param sizeThreshold The threshold, in bytes, below which items will be + * retained in memory and above which they will be + * stored as a file. + * @param repository The data repository, which is the directory in + * which files will be created, should the item size + * exceed the threshold. + */ + public DiskFileItem(final String fieldName, + final String contentType, final boolean isFormField, final String fileName, + final int sizeThreshold, final File repository) { + this.fieldName = fieldName; + this.contentType = contentType; + this.isFormField = isFormField; + this.fileName = fileName; + this.sizeThreshold = sizeThreshold; + this.repository = repository; + } + + // ------------------------------- Methods from javax.activation.DataSource + + /** + * Returns an {@link java.io.InputStream InputStream} that can be + * used to retrieve the contents of the file. + * + * @return An {@link java.io.InputStream InputStream} that can be + * used to retrieve the contents of the file. + * + * @throws IOException if an error occurs. + */ + @Override + public InputStream getInputStream() + throws IOException { + if (!isInMemory()) { + return Files.newInputStream(dfos.getFile().toPath()); + } + + if (cachedContent == null) { + cachedContent = dfos.getData(); + } + return new ByteArrayInputStream(cachedContent); + } + + /** + * Returns the content type passed by the agent or {@code null} if + * not defined. + * + * @return The content type passed by the agent or {@code null} if + * not defined. + */ + @Override + public String getContentType() { + return contentType; + } + + /** + * Returns the content charset passed by the agent or {@code null} if + * not defined. + * + * @return The content charset passed by the agent or {@code null} if + * not defined. + */ + public String getCharSet() { + final ParameterParser parser = new ParameterParser(); + parser.setLowerCaseNames(true); + // Parameter parser can handle null input + final Map params = parser.parse(getContentType(), ';'); + return params.get("charset"); + } + + /** + * Returns the original file name in the client's file system. + * + * @return The original file name in the client's file system. + * @throws org.apache.tomcat.util.http.fileupload.InvalidFileNameException + * The file name contains a NUL character, which might be an indicator of + * a security attack. If you intend to use the file name anyways, catch + * the exception and use {@link + * org.apache.tomcat.util.http.fileupload.InvalidFileNameException#getName()}. + */ + @Override + public String getName() { + return Streams.checkFileName(fileName); + } + + // ------------------------------------------------------- FileItem methods + + /** + * Provides a hint as to whether or not the file contents will be read + * from memory. + * + * @return {@code true} if the file contents will be read + * from memory; {@code false} otherwise. + */ + @Override + public boolean isInMemory() { + if (cachedContent != null) { + return true; + } + return dfos.isInMemory(); + } + + /** + * Returns the size of the file. + * + * @return The size of the file, in bytes. + */ + @Override + public long getSize() { + if (size >= 0) { + return size; + } + if (cachedContent != null) { + return cachedContent.length; + } + if (dfos.isInMemory()) { + return dfos.getData().length; + } + return dfos.getFile().length(); + } + + /** + * Returns the contents of the file as an array of bytes. If the + * contents of the file were not yet cached in memory, they will be + * loaded from the disk storage and cached. + * + * @return The contents of the file as an array of bytes + * or {@code null} if the data cannot be read + * + * @throws UncheckedIOException if an I/O error occurs + * @throws ArithmeticException if the file {@code size} overflows an int + */ + @Override + public byte[] get() throws UncheckedIOException { + if (isInMemory()) { + if (cachedContent == null && dfos != null) { + cachedContent = dfos.getData(); + } + return cachedContent != null ? cachedContent.clone() : new byte[0]; + } + + final byte[] fileData = new byte[Math.toIntExact(getSize())]; + + try (InputStream fis = Files.newInputStream(dfos.getFile().toPath())) { + IOUtils.readFully(fis, fileData); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + return fileData; + } + + /** + * Returns the contents of the file as a String, using the specified + * encoding. This method uses {@link #get()} to retrieve the + * contents of the file. + * + * @param charset The charset to use. + * + * @return The contents of the file, as a string. + * + * @throws UnsupportedEncodingException if the requested character + * encoding is not available. + */ + @Override + public String getString(final String charset) + throws UnsupportedEncodingException, IOException { + return new String(get(), charset); + } + + /** + * Returns the contents of the file as a String, using the default + * character encoding. This method uses {@link #get()} to retrieve the + * contents of the file. + * + * TODO Consider making this method throw UnsupportedEncodingException. + * + * @return The contents of the file, as a string. + */ + @Override + public String getString() { + try { + final byte[] rawData = get(); + String charset = getCharSet(); + if (charset == null) { + charset = defaultCharset; + } + return new String(rawData, charset); + } catch (final IOException e) { + return ""; + } + } + + /** + * A convenience method to write an uploaded item to disk. The client code + * is not concerned with whether or not the item is stored in memory, or on + * disk in a temporary location. They just want to write the uploaded item + * to a file. + *

    + * This implementation first attempts to rename the uploaded item to the + * specified destination file, if the item was originally written to disk. + * Otherwise, the data will be copied to the specified file. + *

    + * This method is only guaranteed to work once, the first time it + * is invoked for a particular item. This is because, in the event that the + * method renames a temporary file, that file will no longer be available + * to copy or rename again at a later time. + * + * @param file The {@code File} into which the uploaded item should + * be stored. + * + * @throws Exception if an error occurs. + */ + @Override + public void write(final File file) throws Exception { + if (isInMemory()) { + try (OutputStream fout = Files.newOutputStream(file.toPath())) { + fout.write(get()); + } + } else { + final File outputFile = getStoreLocation(); + if (outputFile == null) { + /* + * For whatever reason we cannot write the + * file to disk. + */ + throw new FileUploadException( + "Cannot write uploaded file to disk!"); + } + // Save the length of the file + size = outputFile.length(); + /* + * The uploaded file is being stored on disk + * in a temporary location so move it to the + * desired file. + */ + if (file.exists() && !file.delete()) { + throw new FileUploadException("Cannot write uploaded file to disk!"); + } + if (!outputFile.renameTo(file)) { + BufferedInputStream in = null; + BufferedOutputStream out = null; + try { + in = new BufferedInputStream(new FileInputStream(outputFile)); + out = new BufferedOutputStream(new FileOutputStream(file)); + IOUtils.copy(in, out); + out.close(); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + } + } + } + } + + /** + * Deletes the underlying storage for a file item, including deleting any associated temporary disk file. + * This method can be used to ensure that this is done at an earlier time, thus preserving system resources. + */ + @Override + public void delete() { + cachedContent = null; + final File outputFile = getStoreLocation(); + if (outputFile != null && !isInMemory() && outputFile.exists()) { + if (!outputFile.delete()) { + final String desc = "Cannot delete " + outputFile.toString(); + throw new UncheckedIOException(desc, new IOException(desc)); + } + } + } + + /** + * Returns the name of the field in the multipart form corresponding to + * this file item. + * + * @return The name of the form field. + * + * @see #setFieldName(String) + * + */ + @Override + public String getFieldName() { + return fieldName; + } + + /** + * Sets the field name used to reference this file item. + * + * @param fieldName The name of the form field. + * + * @see #getFieldName() + * + */ + @Override + public void setFieldName(final String fieldName) { + this.fieldName = fieldName; + } + + /** + * Determines whether or not a {@code FileItem} instance represents + * a simple form field. + * + * @return {@code true} if the instance represents a simple form + * field; {@code false} if it represents an uploaded file. + * + * @see #setFormField(boolean) + * + */ + @Override + public boolean isFormField() { + return isFormField; + } + + /** + * Specifies whether or not a {@code FileItem} instance represents + * a simple form field. + * + * @param state {@code true} if the instance represents a simple form + * field; {@code false} if it represents an uploaded file. + * + * @see #isFormField() + * + */ + @Override + public void setFormField(final boolean state) { + isFormField = state; + } + + /** + * Returns an {@link java.io.OutputStream OutputStream} that can + * be used for storing the contents of the file. + * + * @return An {@link java.io.OutputStream OutputStream} that can be used + * for storing the contents of the file. + * + */ + @Override + public OutputStream getOutputStream() { + if (dfos == null) { + final File outputFile = getTempFile(); + dfos = new DeferredFileOutputStream(sizeThreshold, outputFile); + } + return dfos; + } + + // --------------------------------------------------------- Public methods + + /** + * Returns the {@link java.io.File} object for the {@code FileItem}'s + * data's temporary location on the disk. Note that for + * {@code FileItem}s that have their data stored in memory, + * this method will return {@code null}. When handling large + * files, you can use {@link java.io.File#renameTo(java.io.File)} to + * move the file to new location without copying the data, if the + * source and destination locations reside within the same logical + * volume. + * + * @return The data file, or {@code null} if the data is stored in + * memory. + */ + public File getStoreLocation() { + if (dfos == null) { + return null; + } + if (isInMemory()) { + return null; + } + return dfos.getFile(); + } + + // ------------------------------------------------------ Protected methods + + /** + * Creates and returns a {@link java.io.File File} representing a uniquely + * named temporary file in the configured repository path. The lifetime of + * the file is tied to the lifetime of the {@code FileItem} instance; + * the file will be deleted when the instance is garbage collected. + *

    + * Note: Subclasses that override this method must ensure that they return the + * same File each time. + * + * @return The {@link java.io.File File} to be used for temporary storage. + */ + protected File getTempFile() { + if (tempFile == null) { + File tempDir = repository; + if (tempDir == null) { + tempDir = new File(System.getProperty("java.io.tmpdir")); + } + + final String tempFileName = String.format("upload_%s_%s.tmp", UID, getUniqueId()); + + tempFile = new File(tempDir, tempFileName); + } + return tempFile; + } + + // -------------------------------------------------------- Private methods + + /** + * Returns an identifier that is unique within the class loader used to + * load this class, but does not have random-like appearance. + * + * @return A String with the non-random looking instance identifier. + */ + private static String getUniqueId() { + final int limit = 100000000; + final int current = COUNTER.getAndIncrement(); + String id = Integer.toString(current); + + // If you manage to get more than 100 million of ids, you'll + // start getting ids longer than 8 characters. + if (current < limit) { + id = ("00000000" + id).substring(id.length()); + } + return id; + } + + /** + * Returns a string representation of this object. + * + * @return a string representation of this object. + */ + @Override + public String toString() { + return String.format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s", + getName(), getStoreLocation(), Long.valueOf(getSize()), Boolean.valueOf(isFormField()), getFieldName()); + } + + /** + * Returns the file item headers. + * @return The file items headers. + */ + @Override + public FileItemHeaders getHeaders() { + return headers; + } + + /** + * Sets the file item headers. + * @param pHeaders The file items headers. + */ + @Override + public void setHeaders(final FileItemHeaders pHeaders) { + headers = pHeaders; + } + + /** + * Returns the default charset for use when no explicit charset + * parameter is provided by the sender. + * @return the default charset + */ + public String getDefaultCharset() { + return defaultCharset; + } + + /** + * Sets the default charset for use when no explicit charset + * parameter is provided by the sender. + * @param charset the default charset + */ + public void setDefaultCharset(final String charset) { + defaultCharset = charset; + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java new file mode 100644 index 0000000..8b0073d --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java @@ -0,0 +1,205 @@ +/* + * 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.tomcat.util.http.fileupload.disk; + +import java.io.File; + +import org.apache.tomcat.util.http.fileupload.FileItem; +import org.apache.tomcat.util.http.fileupload.FileItemFactory; + +/** + *

    The default {@link org.apache.tomcat.util.http.fileupload.FileItemFactory} + * implementation. This implementation creates + * {@link org.apache.tomcat.util.http.fileupload.FileItem} instances which keep + * their + * content either in memory, for smaller items, or in a temporary file on disk, + * for larger items. The size threshold, above which content will be stored on + * disk, is configurable, as is the directory in which temporary files will be + * created.

    + * + *

    If not otherwise configured, the default configuration values are as + * follows:

    + *
      + *
    • Size threshold is 10 KiB.
    • + *
    • Repository is the system default temp directory, as returned by + * {@code System.getProperty("java.io.tmpdir")}.
    • + *
    + *

    + * NOTE: Files are created in the system default temp directory with + * predictable names. This means that a local attacker with write access to that + * directory can perform a TOUTOC attack to replace any uploaded file with a + * file of the attackers choice. The implications of this will depend on how the + * uploaded file is used but could be significant. When using this + * implementation in an environment with local, untrusted users, + * {@link #setRepository(File)} MUST be used to configure a repository location + * that is not publicly writable. In a Servlet container the location identified + * by the ServletContext attribute {@code jakarta.servlet.context.tempdir} + * may be used. + *

    + * + *

    Temporary files, which are created for file items, will be deleted when + * the associated request is recycled.

    + * + * @since FileUpload 1.1 + */ +public class DiskFileItemFactory implements FileItemFactory { + + // ----------------------------------------------------- Manifest constants + + /** + * The default threshold above which uploads will be stored on disk. + */ + public static final int DEFAULT_SIZE_THRESHOLD = 10240; + + // ----------------------------------------------------- Instance Variables + + /** + * The directory in which uploaded files will be stored, if stored on disk. + */ + private File repository; + + /** + * The threshold above which uploads will be stored on disk. + */ + private int sizeThreshold = DEFAULT_SIZE_THRESHOLD; + + /** + * Default content charset to be used when no explicit charset + * parameter is provided by the sender. + */ + private String defaultCharset = DiskFileItem.DEFAULT_CHARSET; + + // ----------------------------------------------------------- Constructors + + /** + * Constructs an unconfigured instance of this class. The resulting factory + * may be configured by calling the appropriate setter methods. + */ + public DiskFileItemFactory() { + this(DEFAULT_SIZE_THRESHOLD, null); + } + + /** + * Constructs a preconfigured instance of this class. + * + * @param sizeThreshold The threshold, in bytes, below which items will be + * retained in memory and above which they will be + * stored as a file. + * @param repository The data repository, which is the directory in + * which files will be created, should the item size + * exceed the threshold. + */ + public DiskFileItemFactory(final int sizeThreshold, final File repository) { + this.sizeThreshold = sizeThreshold; + this.repository = repository; + } + + // ------------------------------------------------------------- Properties + + /** + * Returns the directory used to temporarily store files that are larger + * than the configured size threshold. + * + * @return The directory in which temporary files will be located. + * + * @see #setRepository(java.io.File) + * + */ + public File getRepository() { + return repository; + } + + /** + * Sets the directory used to temporarily store files that are larger + * than the configured size threshold. + * + * @param repository The directory in which temporary files will be located. + * + * @see #getRepository() + * + */ + public void setRepository(final File repository) { + this.repository = repository; + } + + /** + * Returns the size threshold beyond which files are written directly to + * disk. The default value is 10240 bytes. + * + * @return The size threshold, in bytes. + * + * @see #setSizeThreshold(int) + */ + public int getSizeThreshold() { + return sizeThreshold; + } + + /** + * Sets the size threshold beyond which files are written directly to disk. + * + * @param sizeThreshold The size threshold, in bytes. + * + * @see #getSizeThreshold() + * + */ + public void setSizeThreshold(final int sizeThreshold) { + this.sizeThreshold = sizeThreshold; + } + + // --------------------------------------------------------- Public Methods + + /** + * Create a new {@link DiskFileItem} + * instance from the supplied parameters and the local factory + * configuration. + * + * @param fieldName The name of the form field. + * @param contentType The content type of the form field. + * @param isFormField {@code true} if this is a plain form field; + * {@code false} otherwise. + * @param fileName The name of the uploaded file, if any, as supplied + * by the browser or other client. + * + * @return The newly created file item. + */ + @Override + public FileItem createItem(final String fieldName, final String contentType, + final boolean isFormField, final String fileName) { + final DiskFileItem result = new DiskFileItem(fieldName, contentType, + isFormField, fileName, sizeThreshold, repository); + result.setDefaultCharset(defaultCharset); + return result; + } + + /** + * Returns the default charset for use when no explicit charset + * parameter is provided by the sender. + * @return the default charset + */ + public String getDefaultCharset() { + return defaultCharset; + } + + /** + * Sets the default charset for use when no explicit charset + * parameter is provided by the sender. + * @param pCharset the default charset + */ + public void setDefaultCharset(final String pCharset) { + defaultCharset = pCharset; + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/disk/package-info.java b/java/org/apache/tomcat/util/http/fileupload/disk/package-info.java new file mode 100644 index 0000000..c82f434 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/disk/package-info.java @@ -0,0 +1,51 @@ +/* + * 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. + */ + +/** + *

    + * A disk-based implementation of the + * {@link org.apache.tomcat.util.http.fileupload.FileItem FileItem} + * interface. This implementation retains smaller items in memory, while + * writing larger ones to disk. The threshold between these two is + * configurable, as is the location of files that are written to disk. + *

    + *

    + * In typical usage, an instance of + * {@link org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory DiskFileItemFactory} + * would be created, configured, and then passed to a + * {@link org.apache.tomcat.util.http.fileupload.FileUpload FileUpload} + * implementation. + *

    + *

    + * The following code fragment demonstrates this usage. + *

    + *
    + *        DiskFileItemFactory factory = new DiskFileItemFactory();
    + *        // maximum size that will be stored in memory
    + *        factory.setSizeThreshold(4096);
    + *        // the location for saving data that is larger than getSizeThreshold()
    + *        factory.setRepository(new File("/tmp"));
    + *
    + *        ServletFileUpload upload = new ServletFileUpload(factory);
    + * 
    + *

    + * Please see the FileUpload + * User Guide + * for further details and examples of how to use this package. + *

    + */ +package org.apache.tomcat.util.http.fileupload.disk; diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/FileCountLimitExceededException.java b/java/org/apache/tomcat/util/http/fileupload/impl/FileCountLimitExceededException.java new file mode 100644 index 0000000..958f681 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/FileCountLimitExceededException.java @@ -0,0 +1,50 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +import org.apache.tomcat.util.http.fileupload.FileUploadException; + +/** + * This exception is thrown if a request contains more files than the specified + * limit. + */ +public class FileCountLimitExceededException extends FileUploadException { + + private static final long serialVersionUID = 2408766352570556046L; + + private final long limit; + + /** + * Creates a new instance. + * + * @param message The detail message + * @param limit The limit that was exceeded + */ + public FileCountLimitExceededException(final String message, final long limit) { + super(message); + this.limit = limit; + } + + /** + * Retrieves the limit that was exceeded. + * + * @return The limit that was exceeded by the request + */ + public long getLimit() { + return limit; + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/FileItemIteratorImpl.java b/java/org/apache/tomcat/util/http/fileupload/impl/FileItemIteratorImpl.java new file mode 100644 index 0000000..29e89f6 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/FileItemIteratorImpl.java @@ -0,0 +1,351 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.NoSuchElementException; +import java.util.Objects; + +import org.apache.tomcat.util.http.fileupload.FileItem; +import org.apache.tomcat.util.http.fileupload.FileItemHeaders; +import org.apache.tomcat.util.http.fileupload.FileItemIterator; +import org.apache.tomcat.util.http.fileupload.FileItemStream; +import org.apache.tomcat.util.http.fileupload.FileUploadBase; +import org.apache.tomcat.util.http.fileupload.FileUploadException; +import org.apache.tomcat.util.http.fileupload.IOUtils; +import org.apache.tomcat.util.http.fileupload.MultipartStream; +import org.apache.tomcat.util.http.fileupload.ProgressListener; +import org.apache.tomcat.util.http.fileupload.RequestContext; +import org.apache.tomcat.util.http.fileupload.UploadContext; +import org.apache.tomcat.util.http.fileupload.util.LimitedInputStream; + +/** + * The iterator, which is returned by + * {@link FileUploadBase#getItemIterator(RequestContext)}. + */ +public class FileItemIteratorImpl implements FileItemIterator { + /** + * The file uploads processing utility. + * @see FileUploadBase + */ + private final FileUploadBase fileUploadBase; + /** + * The request context. + * @see RequestContext + */ + private final RequestContext ctx; + /** + * The maximum allowed size of a complete request. + */ + private long sizeMax; + /** + * The maximum allowed size of a single uploaded file. + */ + private long fileSizeMax; + + + @Override + public long getSizeMax() { + return sizeMax; + } + + @Override + public void setSizeMax(final long sizeMax) { + this.sizeMax = sizeMax; + } + + @Override + public long getFileSizeMax() { + return fileSizeMax; + } + + @Override + public void setFileSizeMax(final long fileSizeMax) { + this.fileSizeMax = fileSizeMax; + } + + /** + * The multi part stream to process. + */ + private MultipartStream multiPartStream; + + /** + * The notifier, which used for triggering the + * {@link ProgressListener}. + */ + private MultipartStream.ProgressNotifier progressNotifier; + + /** + * The boundary, which separates the various parts. + */ + private byte[] multiPartBoundary; + + /** + * The item, which we currently process. + */ + private FileItemStreamImpl currentItem; + + /** + * The current items field name. + */ + private String currentFieldName; + + /** + * Whether we are currently skipping the preamble. + */ + private boolean skipPreamble; + + /** + * Whether the current item may still be read. + */ + private boolean itemValid; + + /** + * Whether we have seen the end of the file. + */ + private boolean eof; + + /** + * Creates a new instance. + * + * @param fileUploadBase Main processor. + * @param requestContext The request context. + * @throws FileUploadException An error occurred while + * parsing the request. + * @throws IOException An I/O error occurred. + */ + public FileItemIteratorImpl(final FileUploadBase fileUploadBase, final RequestContext requestContext) + throws FileUploadException, IOException { + this.fileUploadBase = fileUploadBase; + sizeMax = fileUploadBase.getSizeMax(); + fileSizeMax = fileUploadBase.getFileSizeMax(); + ctx = Objects.requireNonNull(requestContext, "requestContext"); + skipPreamble = true; + findNextItem(); + } + + protected void init(final FileUploadBase fileUploadBase, @SuppressWarnings("unused") final RequestContext pRequestContext) + throws FileUploadException, IOException { + final String contentType = ctx.getContentType(); + if ((null == contentType) + || (!contentType.toLowerCase(Locale.ENGLISH).startsWith(FileUploadBase.MULTIPART))) { + throw new InvalidContentTypeException( + String.format("the request doesn't contain a %s or %s stream, content type header is %s", + FileUploadBase.MULTIPART_FORM_DATA, FileUploadBase.MULTIPART_MIXED, contentType)); + } + + final long requestSize = ((UploadContext) ctx).contentLength(); + + final InputStream input; // N.B. this is eventually closed in MultipartStream processing + if (sizeMax >= 0) { + if (requestSize != -1 && requestSize > sizeMax) { + throw new SizeLimitExceededException( + String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", + Long.valueOf(requestSize), Long.valueOf(sizeMax)), + requestSize, sizeMax); + } + // N.B. this is eventually closed in MultipartStream processing + input = new LimitedInputStream(ctx.getInputStream(), sizeMax) { + @Override + protected void raiseError(final long pSizeMax, final long pCount) + throws IOException { + final FileUploadException ex = new SizeLimitExceededException( + String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", + Long.valueOf(pCount), Long.valueOf(pSizeMax)), + pCount, pSizeMax); + throw new FileUploadIOException(ex); + } + }; + } else { + input = ctx.getInputStream(); + } + + String charEncoding = fileUploadBase.getHeaderEncoding(); + if (charEncoding == null) { + charEncoding = ctx.getCharacterEncoding(); + } + + multiPartBoundary = fileUploadBase.getBoundary(contentType); + if (multiPartBoundary == null) { + IOUtils.closeQuietly(input); // avoid possible resource leak + throw new FileUploadException("the request was rejected because no multipart boundary was found"); + } + + progressNotifier = new MultipartStream.ProgressNotifier(fileUploadBase.getProgressListener(), requestSize); + try { + multiPartStream = new MultipartStream(input, multiPartBoundary, progressNotifier); + } catch (final IllegalArgumentException iae) { + IOUtils.closeQuietly(input); // avoid possible resource leak + throw new InvalidContentTypeException( + String.format("The boundary specified in the %s header is too long", FileUploadBase.CONTENT_TYPE), iae); + } + multiPartStream.setHeaderEncoding(charEncoding); + } + + public MultipartStream getMultiPartStream() throws FileUploadException, IOException { + if (multiPartStream == null) { + init(fileUploadBase, ctx); + } + return multiPartStream; + } + + /** + * Called for finding the next item, if any. + * + * @return True, if an next item was found, otherwise false. + * @throws IOException An I/O error occurred. + */ + private boolean findNextItem() throws FileUploadException, IOException { + if (eof) { + return false; + } + if (currentItem != null) { + currentItem.close(); + currentItem = null; + } + final MultipartStream multi = getMultiPartStream(); + for (;;) { + final boolean nextPart; + if (skipPreamble) { + nextPart = multi.skipPreamble(); + } else { + nextPart = multi.readBoundary(); + } + if (!nextPart) { + if (currentFieldName == null) { + // Outer multipart terminated -> No more data + eof = true; + return false; + } + // Inner multipart terminated -> Return to parsing the outer + multi.setBoundary(multiPartBoundary); + currentFieldName = null; + continue; + } + final FileItemHeaders headers = fileUploadBase.getParsedHeaders(multi.readHeaders()); + if (currentFieldName == null) { + // We're parsing the outer multipart + final String fieldName = fileUploadBase.getFieldName(headers); + if (fieldName != null) { + final String subContentType = headers.getHeader(FileUploadBase.CONTENT_TYPE); + if (subContentType != null + && subContentType.toLowerCase(Locale.ENGLISH) + .startsWith(FileUploadBase.MULTIPART_MIXED)) { + currentFieldName = fieldName; + // Multiple files associated with this field name + final byte[] subBoundary = fileUploadBase.getBoundary(subContentType); + multi.setBoundary(subBoundary); + skipPreamble = true; + continue; + } + final String fileName = fileUploadBase.getFileName(headers); + currentItem = new FileItemStreamImpl(this, fileName, + fieldName, headers.getHeader(FileUploadBase.CONTENT_TYPE), + fileName == null, getContentLength(headers)); + currentItem.setHeaders(headers); + progressNotifier.noteItem(); + itemValid = true; + return true; + } + } else { + final String fileName = fileUploadBase.getFileName(headers); + if (fileName != null) { + currentItem = new FileItemStreamImpl(this, fileName, + currentFieldName, + headers.getHeader(FileUploadBase.CONTENT_TYPE), + false, getContentLength(headers)); + currentItem.setHeaders(headers); + progressNotifier.noteItem(); + itemValid = true; + return true; + } + } + multi.discardBodyData(); + } + } + + private long getContentLength(final FileItemHeaders pHeaders) { + try { + return Long.parseLong(pHeaders.getHeader(FileUploadBase.CONTENT_LENGTH)); + } catch (final Exception e) { + return -1; + } + } + + /** + * Returns, whether another instance of {@link FileItemStream} + * is available. + * + * @throws FileUploadException Parsing or processing the + * file item failed. + * @throws IOException Reading the file item failed. + * @return True, if one or more additional file items + * are available, otherwise false. + */ + @Override + public boolean hasNext() throws FileUploadException, IOException { + if (eof) { + return false; + } + if (itemValid) { + return true; + } + try { + return findNextItem(); + } catch (final FileUploadIOException e) { + // unwrap encapsulated SizeException + throw (FileUploadException) e.getCause(); + } + } + + /** + * Returns the next available {@link FileItemStream}. + * + * @throws java.util.NoSuchElementException No more items are + * available. Use {@link #hasNext()} to prevent this exception. + * @throws FileUploadException Parsing or processing the + * file item failed. + * @throws IOException Reading the file item failed. + * @return FileItemStream instance, which provides + * access to the next file item. + */ + @Override + public FileItemStream next() throws FileUploadException, IOException { + if (eof || (!itemValid && !hasNext())) { + throw new NoSuchElementException(); + } + itemValid = false; + return currentItem; + } + + @Override + public List getFileItems() throws FileUploadException, IOException { + final List items = new ArrayList<>(); + while (hasNext()) { + final FileItemStream fis = next(); + final FileItem fi = fileUploadBase.getFileItemFactory().createItem(fis.getFieldName(), + fis.getContentType(), fis.isFormField(), fis.getName()); + items.add(fi); + } + return items; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/FileItemStreamImpl.java b/java/org/apache/tomcat/util/http/fileupload/impl/FileItemStreamImpl.java new file mode 100644 index 0000000..bab5d98 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/FileItemStreamImpl.java @@ -0,0 +1,218 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.tomcat.util.http.fileupload.FileItemHeaders; +import org.apache.tomcat.util.http.fileupload.FileItemStream; +import org.apache.tomcat.util.http.fileupload.FileUploadException; +import org.apache.tomcat.util.http.fileupload.InvalidFileNameException; +import org.apache.tomcat.util.http.fileupload.MultipartStream.ItemInputStream; +import org.apache.tomcat.util.http.fileupload.util.Closeable; +import org.apache.tomcat.util.http.fileupload.util.LimitedInputStream; +import org.apache.tomcat.util.http.fileupload.util.Streams; + + +/** + * Default implementation of {@link FileItemStream}. + */ +public class FileItemStreamImpl implements FileItemStream { + /** + * The File Item iterator implementation. + * + * @see FileItemIteratorImpl + */ + private final FileItemIteratorImpl fileItemIteratorImpl; + + /** + * The file items content type. + */ + private final String contentType; + + /** + * The file items field name. + */ + private final String fieldName; + + /** + * The file items file name. + */ + private final String name; + + /** + * Whether the file item is a form field. + */ + private final boolean formField; + + /** + * The file items input stream. + */ + private final InputStream stream; + + /** + * The headers, if any. + */ + private FileItemHeaders headers; + + /** + * Creates a new instance. + * + * @param pFileItemIterator The {@link FileItemIteratorImpl iterator}, which returned this file + * item. + * @param pName The items file name, or null. + * @param pFieldName The items field name. + * @param pContentType The items content type, or null. + * @param pFormField Whether the item is a form field. + * @param pContentLength The items content length, if known, or -1 + * @throws IOException Creating the file item failed. + * @throws FileUploadException Parsing the incoming data stream failed. + */ + public FileItemStreamImpl(final FileItemIteratorImpl pFileItemIterator, final String pName, final String pFieldName, + final String pContentType, final boolean pFormField, + final long pContentLength) throws FileUploadException, IOException { + fileItemIteratorImpl = pFileItemIterator; + name = pName; + fieldName = pFieldName; + contentType = pContentType; + formField = pFormField; + final long fileSizeMax = fileItemIteratorImpl.getFileSizeMax(); + if (fileSizeMax != -1 && pContentLength != -1 + && pContentLength > fileSizeMax) { + final FileSizeLimitExceededException e = + new FileSizeLimitExceededException( + String.format("The field %s exceeds its maximum permitted size of %s bytes.", + fieldName, Long.valueOf(fileSizeMax)), + pContentLength, fileSizeMax); + e.setFileName(pName); + e.setFieldName(pFieldName); + throw new FileUploadIOException(e); + } + // OK to construct stream now + final ItemInputStream itemStream = fileItemIteratorImpl.getMultiPartStream().newInputStream(); + InputStream istream = itemStream; + if (fileSizeMax != -1) { + istream = new LimitedInputStream(istream, fileSizeMax) { + @Override + protected void raiseError(final long pSizeMax, final long pCount) + throws IOException { + itemStream.close(true); + final FileSizeLimitExceededException e = + new FileSizeLimitExceededException( + String.format("The field %s exceeds its maximum permitted size of %s bytes.", + fieldName, Long.valueOf(pSizeMax)), + pCount, pSizeMax); + e.setFieldName(fieldName); + e.setFileName(name); + throw new FileUploadIOException(e); + } + }; + } + stream = istream; + } + + /** + * Returns the items content type, or null. + * + * @return Content type, if known, or null. + */ + @Override + public String getContentType() { + return contentType; + } + + /** + * Returns the items field name. + * + * @return Field name. + */ + @Override + public String getFieldName() { + return fieldName; + } + + /** + * Returns the items file name. + * + * @return File name, if known, or null. + * @throws InvalidFileNameException The file name contains a NUL character, + * which might be an indicator of a security attack. If you intend to + * use the file name anyways, catch the exception and use + * InvalidFileNameException#getName(). + */ + @Override + public String getName() { + return Streams.checkFileName(name); + } + + /** + * Returns, whether this is a form field. + * + * @return True, if the item is a form field, + * otherwise false. + */ + @Override + public boolean isFormField() { + return formField; + } + + /** + * Returns an input stream, which may be used to + * read the items contents. + * + * @return Opened input stream. + * @throws IOException An I/O error occurred. + */ + @Override + public InputStream openStream() throws IOException { + if (((Closeable) stream).isClosed()) { + throw new FileItemStream.ItemSkippedException(); + } + return stream; + } + + /** + * Closes the file item. + * + * @throws IOException An I/O error occurred. + */ + public void close() throws IOException { + stream.close(); + } + + /** + * Returns the file item headers. + * + * @return The items header object + */ + @Override + public FileItemHeaders getHeaders() { + return headers; + } + + /** + * Sets the file item headers. + * + * @param pHeaders The items header object + */ + @Override + public void setHeaders(final FileItemHeaders pHeaders) { + headers = pHeaders; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/FileSizeLimitExceededException.java b/java/org/apache/tomcat/util/http/fileupload/impl/FileSizeLimitExceededException.java new file mode 100644 index 0000000..dba9715 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/FileSizeLimitExceededException.java @@ -0,0 +1,94 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +/** + * Thrown to indicate that A files size exceeds the configured maximum. + */ +public class FileSizeLimitExceededException + extends SizeException { + + /** + * The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = 8150776562029630058L; + + /** + * File name of the item, which caused the exception. + */ + private String fileName; + + /** + * Field name of the item, which caused the exception. + */ + private String fieldName; + + /** + * Constructs a {@code SizeExceededException} with + * the specified detail message, and actual and permitted sizes. + * + * @param message The detail message. + * @param actual The actual request size. + * @param permitted The maximum permitted request size. + */ + public FileSizeLimitExceededException(final String message, final long actual, + final long permitted) { + super(message, actual, permitted); + } + + /** + * Returns the file name of the item, which caused the + * exception. + * + * @return File name, if known, or null. + */ + public String getFileName() { + return fileName; + } + + /** + * Sets the file name of the item, which caused the + * exception. + * + * @param pFileName the file name of the item, which caused the exception. + */ + public void setFileName(final String pFileName) { + fileName = pFileName; + } + + /** + * Returns the field name of the item, which caused the + * exception. + * + * @return Field name, if known, or null. + */ + public String getFieldName() { + return fieldName; + } + + /** + * Sets the field name of the item, which caused the + * exception. + * + * @param pFieldName the field name of the item, + * which caused the exception. + */ + public void setFieldName(final String pFieldName) { + fieldName = pFieldName; + } + +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/FileUploadIOException.java b/java/org/apache/tomcat/util/http/fileupload/impl/FileUploadIOException.java new file mode 100644 index 0000000..3ac5988 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/FileUploadIOException.java @@ -0,0 +1,63 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +import java.io.IOException; + +import org.apache.tomcat.util.http.fileupload.FileUploadException; + +/** + * This exception is thrown for hiding an inner + * {@link FileUploadException} in an {@link IOException}. + */ +public class FileUploadIOException extends IOException { + + /** + * The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = -7047616958165584154L; + + /** + * The exceptions cause; we overwrite the parent + * classes field, which is available since Java + * 1.4 only. + */ + private final FileUploadException cause; + + /** + * Creates a {@code FileUploadIOException} with the + * given cause. + * + * @param pCause The exceptions cause, if any, or null. + */ + public FileUploadIOException(final FileUploadException pCause) { + // We're not doing super(pCause) cause of 1.3 compatibility. + cause = pCause; + } + + /** + * Returns the exceptions cause. + * + * @return The exceptions cause, if any, or null. + */ + @SuppressWarnings("sync-override") // Field is final + @Override + public Throwable getCause() { + return cause; + } + +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/IOFileUploadException.java b/java/org/apache/tomcat/util/http/fileupload/impl/IOFileUploadException.java new file mode 100644 index 0000000..397be97 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/IOFileUploadException.java @@ -0,0 +1,62 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +import java.io.IOException; + +import org.apache.tomcat.util.http.fileupload.FileUploadException; + +/** + * Thrown to indicate an IOException. + */ +public class IOFileUploadException extends FileUploadException { + + /** + * The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = 1749796615868477269L; + + /** + * The exceptions cause; we overwrite the parent + * classes field, which is available since Java + * 1.4 only. + */ + private final IOException cause; + + /** + * Creates a new instance with the given cause. + * + * @param pMsg The detail message. + * @param pException The exceptions cause. + */ + public IOFileUploadException(final String pMsg, final IOException pException) { + super(pMsg); + cause = pException; + } + + /** + * Returns the exceptions cause. + * + * @return The exceptions cause, if any, or null. + */ + @SuppressWarnings("sync-override") // Field is final + @Override + public Throwable getCause() { + return cause; + } + +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/InvalidContentTypeException.java b/java/org/apache/tomcat/util/http/fileupload/impl/InvalidContentTypeException.java new file mode 100644 index 0000000..699a323 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/InvalidContentTypeException.java @@ -0,0 +1,61 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +import org.apache.tomcat.util.http.fileupload.FileUploadException; + +/** + * Thrown to indicate that the request is not a multipart request. + */ +public class InvalidContentTypeException + extends FileUploadException { + + /** + * The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = -9073026332015646668L; + + /** + * Constructs a {@code InvalidContentTypeException} with no + * detail message. + */ + public InvalidContentTypeException() { + } + + /** + * Constructs an {@code InvalidContentTypeException} with + * the specified detail message. + * + * @param message The detail message. + */ + public InvalidContentTypeException(final String message) { + super(message); + } + + /** + * Constructs an {@code InvalidContentTypeException} with + * the specified detail message and cause. + * + * @param msg The detail message. + * @param cause the original cause + * + * @since FileUpload 1.3.1 + */ + public InvalidContentTypeException(final String msg, final Throwable cause) { + super(msg, cause); + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/SizeException.java b/java/org/apache/tomcat/util/http/fileupload/impl/SizeException.java new file mode 100644 index 0000000..35f25ef --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/SizeException.java @@ -0,0 +1,75 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +import org.apache.tomcat.util.http.fileupload.FileUploadException; + +/** + * This exception is thrown, if a requests permitted size + * is exceeded. + */ +public abstract class SizeException extends FileUploadException { + + /** + * Serial version UID, being used, if serialized. + */ + private static final long serialVersionUID = -8776225574705254126L; + + /** + * The actual size of the request. + */ + private final long actual; + + /** + * The maximum permitted size of the request. + */ + private final long permitted; + + /** + * Creates a new instance. + * + * @param message The detail message. + * @param actual The actual number of bytes in the request. + * @param permitted The requests size limit, in bytes. + */ + protected SizeException(final String message, final long actual, final long permitted) { + super(message); + this.actual = actual; + this.permitted = permitted; + } + + /** + * Retrieves the actual size of the request. + * + * @return The actual size of the request. + * @since FileUpload 1.3 + */ + public long getActualSize() { + return actual; + } + + /** + * Retrieves the permitted size of the request. + * + * @return The permitted size of the request. + * @since FileUpload 1.3 + */ + public long getPermittedSize() { + return permitted; + } + +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/SizeLimitExceededException.java b/java/org/apache/tomcat/util/http/fileupload/impl/SizeLimitExceededException.java new file mode 100644 index 0000000..6a7da5d --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/SizeLimitExceededException.java @@ -0,0 +1,43 @@ +/* + * 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.tomcat.util.http.fileupload.impl; + +/** + * Thrown to indicate that the request size exceeds the configured maximum. + */ +public class SizeLimitExceededException + extends SizeException { + + /** + * The exceptions UID, for serializing an instance. + */ + private static final long serialVersionUID = -2474893167098052828L; + + /** + * Constructs a {@code SizeExceededException} with + * the specified detail message, and actual and permitted sizes. + * + * @param message The detail message. + * @param actual The actual request size. + * @param permitted The maximum permitted request size. + */ + public SizeLimitExceededException(final String message, final long actual, + final long permitted) { + super(message, actual, permitted); + } + +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/http/fileupload/impl/package-info.java b/java/org/apache/tomcat/util/http/fileupload/impl/package-info.java new file mode 100644 index 0000000..134a3f6 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/impl/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Implementations and exceptions utils. + */ +package org.apache.tomcat.util.http.fileupload.impl; diff --git a/java/org/apache/tomcat/util/http/fileupload/package-info.java b/java/org/apache/tomcat/util/http/fileupload/package-info.java new file mode 100644 index 0000000..dee2bb1 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/package-info.java @@ -0,0 +1,86 @@ +/* + * 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. + */ + +/** + *

    NOTE: This code has been copied from commons-fileupload trunk + * 1.3 and commons-io 1.4 and package renamed to avoid clashes with + * any web apps that may wish to use these libraries. + *

    + *

    + * A component for handling HTML file uploads as specified by + * RFC 1867. + * This component provides support for uploads within both servlets (JSR 53) + * and portlets (JSR 168). + *

    + *

    + * While this package provides the generic functionality for file uploads, + * these classes are not typically used directly. Instead, normal usage + * involves one of the provided extensions of + * {@link org.apache.tomcat.util.http.fileupload.FileUpload FileUpload} + * together with a factory for + * {@link org.apache.tomcat.util.http.fileupload.FileItem FileItem} instances, + * such as + * {@link org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory DiskFileItemFactory}. + *

    + *

    + * The following is a brief example of typical usage in a servlet, storing + * the uploaded files on disk. + *

    + *
    public void doPost(HttpServletRequest req, HttpServletResponse res) {
    + *   DiskFileItemFactory factory = new DiskFileItemFactory();
    + *   // maximum size that will be stored in memory
    + *   factory.setSizeThreshold(4096);
    + *   // the location for saving data that is larger than getSizeThreshold()
    + *   factory.setRepository(new File("/tmp"));
    + *
    + *   ServletFileUpload upload = new ServletFileUpload(factory);
    + *   // maximum size before a FileUploadException will be thrown
    + *   upload.setSizeMax(1000000);
    + *
    + *   List fileItems = upload.parseRequest(req);
    + *   // assume we know there are two files. The first file is a small
    + *   // text file, the second is unknown and is written to a file on
    + *   // the server
    + *   Iterator i = fileItems.iterator();
    + *   String comment = ((FileItem)i.next()).getString();
    + *   FileItem fi = (FileItem)i.next();
    + *   // file name on the client
    + *   String fileName = fi.getName();
    + *   // save comment and file name to database
    + *   ...
    + *   // write the file
    + *   fi.write(new File("/www/uploads/", fileName));
    + * }
    + * 
    + *

    + * In the example above, the first file is loaded into memory as a + * {@code String}. Before calling the {@code getString} method, + * the data may have been in memory or on disk depending on its size. The + * second file we assume it will be large and therefore never explicitly + * load it into memory, though if it is less than 4096 bytes it will be + * in memory before it is written to its final location. When writing to + * the final location, if the data is larger than the threshold, an attempt + * is made to rename the temporary file to the given location. If it cannot + * be renamed, it is streamed to the new location. + *

    + *

    + * Please see the FileUpload + * User Guide + * for further details and examples of how to use this package. + *

    + */ +package org.apache.tomcat.util.http.fileupload; diff --git a/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java b/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java new file mode 100644 index 0000000..90f05d1 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java @@ -0,0 +1,115 @@ +/* + * 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.tomcat.util.http.fileupload.servlet; + +import java.io.IOException; +import java.io.InputStream; + +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.tomcat.util.http.fileupload.FileUploadBase; +import org.apache.tomcat.util.http.fileupload.UploadContext; + +/** + *

    Provides access to the request information needed for a request made to + * an HTTP servlet.

    + * + * @since FileUpload 1.1 + */ +public class ServletRequestContext implements UploadContext { + + // ----------------------------------------------------- Instance Variables + + /** + * The request for which the context is being provided. + */ + private final HttpServletRequest request; + + // ----------------------------------------------------------- Constructors + + /** + * Construct a context for this request. + * + * @param request The request to which this context applies. + */ + public ServletRequestContext(final HttpServletRequest request) { + this.request = request; + } + + // --------------------------------------------------------- Public Methods + + /** + * Retrieve the character encoding for the request. + * + * @return The character encoding for the request. + */ + @Override + public String getCharacterEncoding() { + return request.getCharacterEncoding(); + } + + /** + * Retrieve the content type of the request. + * + * @return The content type of the request. + */ + @Override + public String getContentType() { + return request.getContentType(); + } + + /** + * Retrieve the content length of the request. + * + * @return The content length of the request. + * @since FileUpload 1.3 + */ + @Override + public long contentLength() { + long size; + try { + size = Long.parseLong(request.getHeader(FileUploadBase.CONTENT_LENGTH)); + } catch (final NumberFormatException e) { + size = request.getContentLength(); + } + return size; + } + + /** + * Retrieve the input stream for the request. + * + * @return The input stream for the request. + * + * @throws IOException if a problem occurs. + */ + @Override + public InputStream getInputStream() throws IOException { + return request.getInputStream(); + } + + /** + * Returns a string representation of this object. + * + * @return a string representation of this object. + */ + @Override + public String toString() { + return String.format("ContentLength=%s, ContentType=%s", + Long.valueOf(this.contentLength()), this.getContentType()); + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/servlet/package-info.java b/java/org/apache/tomcat/util/http/fileupload/servlet/package-info.java new file mode 100644 index 0000000..8abc92e --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/servlet/package-info.java @@ -0,0 +1,45 @@ +/* + * 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. + */ + +/** + *

    + * An implementation of + * {@link org.apache.tomcat.util.http.fileupload.FileUpload FileUpload} + * for use in servlets conforming to JSR 53. This implementation requires + * only access to the servlet's current {@code HttpServletRequest} + * instance, and a suitable + * {@link org.apache.tomcat.util.http.fileupload.FileItemFactory FileItemFactory} + * implementation, such as + * {@link org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory DiskFileItemFactory}. + *

    + *

    + * The following code fragment demonstrates typical usage. + *

    + *
    + *        DiskFileItemFactory factory = new DiskFileItemFactory();
    + *        // Configure the factory here, if desired.
    + *        ServletFileUpload upload = new ServletFileUpload(factory);
    + *        // Configure the uploader here, if desired.
    + *        List fileItems = upload.parseRequest(request);
    + * 
    + *

    + * Please see the FileUpload + * User Guide + * for further details and examples of how to use this package. + *

    + */ +package org.apache.tomcat.util.http.fileupload.servlet; diff --git a/java/org/apache/tomcat/util/http/fileupload/util/Closeable.java b/java/org/apache/tomcat/util/http/fileupload/util/Closeable.java new file mode 100644 index 0000000..41f9af4 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/Closeable.java @@ -0,0 +1,41 @@ +/* + * 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.tomcat.util.http.fileupload.util; + +import java.io.IOException; + +/** + * Interface of an object, which may be closed. + */ +public interface Closeable { + + /** + * Closes the object. + * + * @throws IOException An I/O error occurred. + */ + void close() throws IOException; + + /** + * Returns, whether the object is already closed. + * + * @return True, if the object is closed, otherwise false. + * @throws IOException An I/O error occurred. + */ + boolean isClosed() throws IOException; + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java b/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java new file mode 100644 index 0000000..a70345f --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java @@ -0,0 +1,86 @@ +/* + * 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.tomcat.util.http.fileupload.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.tomcat.util.http.fileupload.FileItemHeaders; + +/** + * Default implementation of the {@link FileItemHeaders} interface. + * + * @since FileUpload 1.2.1 + */ +public class FileItemHeadersImpl implements FileItemHeaders, Serializable { + + /** + * Serial version UID, being used, if serialized. + */ + private static final long serialVersionUID = -4455695752627032559L; + + /** + * Map of {@code String} keys to a {@code List} of + * {@code String} instances. + */ + private final Map> headerNameToValueListMap = new LinkedHashMap<>(); + + @Override + public String getHeader(final String name) { + final String nameLower = name.toLowerCase(Locale.ENGLISH); + final List headerValueList = headerNameToValueListMap.get(nameLower); + if (null == headerValueList) { + return null; + } + return headerValueList.get(0); + } + + @Override + public Iterator getHeaderNames() { + return headerNameToValueListMap.keySet().iterator(); + } + + @Override + public Iterator getHeaders(final String name) { + final String nameLower = name.toLowerCase(Locale.ENGLISH); + List headerValueList = headerNameToValueListMap.get(nameLower); + if (null == headerValueList) { + headerValueList = Collections.emptyList(); + } + return headerValueList.iterator(); + } + + /** + * Method to add header values to this instance. + * + * @param name name of this header + * @param value value of this header + */ + public synchronized void addHeader(final String name, final String value) { + final String nameLower = name.toLowerCase(Locale.ENGLISH); + final List headerValueList = headerNameToValueListMap. + computeIfAbsent(nameLower, k -> new ArrayList<>()); + headerValueList.add(value); + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java b/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java new file mode 100644 index 0000000..def25ca --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java @@ -0,0 +1,166 @@ +/* + * 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.tomcat.util.http.fileupload.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An input stream, which limits its data size. This stream is + * used, if the content length is unknown. + */ +public abstract class LimitedInputStream extends FilterInputStream implements Closeable { + + /** + * The maximum size of an item, in bytes. + */ + private final long sizeMax; + + /** + * The current number of bytes. + */ + private long count; + + /** + * Whether this stream is already closed. + */ + private boolean closed; + + /** + * Creates a new instance. + * + * @param inputStream The input stream, which shall be limited. + * @param pSizeMax The limit; no more than this number of bytes + * shall be returned by the source stream. + */ + public LimitedInputStream(final InputStream inputStream, final long pSizeMax) { + super(inputStream); + sizeMax = pSizeMax; + } + + /** + * Called to indicate, that the input streams limit has + * been exceeded. + * + * @param pSizeMax The input streams limit, in bytes. + * @param pCount The actual number of bytes. + * @throws IOException The called method is expected + * to raise an IOException. + */ + protected abstract void raiseError(long pSizeMax, long pCount) + throws IOException; + + /** + * Called to check, whether the input streams + * limit is reached. + * + * @throws IOException The given limit is exceeded. + */ + private void checkLimit() throws IOException { + if (count > sizeMax) { + raiseError(sizeMax, count); + } + } + + /** + * Reads the next byte of data from this input stream. The value + * byte is returned as an {@code int} in the range + * {@code 0} to {@code 255}. If no byte is available + * because the end of the stream has been reached, the value + * {@code -1} is returned. This method blocks until input data + * is available, the end of the stream is detected, or an exception + * is thrown. + *

    + * This method + * simply performs {@code in.read()} and returns the result. + * + * @return the next byte of data, or {@code -1} if the end of the + * stream is reached. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + final int res = super.read(); + if (res != -1) { + count++; + checkLimit(); + } + return res; + } + + /** + * Reads up to {@code len} bytes of data from this input stream + * into an array of bytes. If {@code len} is not zero, the method + * blocks until some input is available; otherwise, no + * bytes are read and {@code 0} is returned. + *

    + * This method simply performs {@code in.read(b, off, len)} + * and returns the result. + * + * @param b the buffer into which the data is read. + * @param off The start offset in the destination array + * {@code b}. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * {@code -1} if there is no more data because the end of + * the stream has been reached. + * @throws NullPointerException If {@code b} is {@code null}. + * @throws IndexOutOfBoundsException If {@code off} is negative, + * {@code len} is negative, or {@code len} is greater than + * {@code b.length - off} + * @throws IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + final int res = super.read(b, off, len); + if (res > 0) { + count += res; + checkLimit(); + } + return res; + } + + /** + * Returns, whether this stream is already closed. + * + * @return True, if the stream is closed, otherwise false. + * @throws IOException An I/O error occurred. + */ + @Override + public boolean isClosed() throws IOException { + return closed; + } + + /** + * Closes this input stream and releases any system resources + * associated with the stream. + * This + * method simply performs {@code in.close()}. + * + * @throws IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public void close() throws IOException { + closed = true; + super.close(); + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/util/Streams.java b/java/org/apache/tomcat/util/http/fileupload/util/Streams.java new file mode 100644 index 0000000..7289d06 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/Streams.java @@ -0,0 +1,150 @@ +/* + * 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.tomcat.util.http.fileupload.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.tomcat.util.http.fileupload.InvalidFileNameException; + +/** + * Utility class for working with streams. + */ +public final class Streams { + + /** + * Private constructor, to prevent instantiation. + * This class has only static methods. + */ + private Streams() { + // Does nothing + } + + /** + * Default buffer size for use in + * {@link #copy(InputStream, OutputStream, boolean)}. + */ + public static final int DEFAULT_BUFFER_SIZE = 8192; + + /** + * Copies the contents of the given {@link InputStream} + * to the given {@link OutputStream}. Shortcut for + *

    +     *   copy(pInputStream, pOutputStream, new byte[8192]);
    +     * 
    + * + * @param inputStream The input stream, which is being read. + * It is guaranteed, that {@link InputStream#close()} is called + * on the stream. + * @param outputStream The output stream, to which data should + * be written. May be null, in which case the input streams + * contents are simply discarded. + * @param closeOutputStream True guarantees, that {@link OutputStream#close()} + * is called on the stream. False indicates, that only + * {@link OutputStream#flush()} should be called finally. + * + * @return Number of bytes, which have been copied. + * @throws IOException An I/O error occurred. + */ + public static long copy(final InputStream inputStream, final OutputStream outputStream, + final boolean closeOutputStream) + throws IOException { + return copy(inputStream, outputStream, closeOutputStream, new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copies the contents of the given {@link InputStream} + * to the given {@link OutputStream}. + * + * @param inputStream The input stream, which is being read. + * It is guaranteed, that {@link InputStream#close()} is called + * on the stream. + * @param outputStream The output stream, to which data should + * be written. May be null, in which case the input streams + * contents are simply discarded. + * @param closeOutputStream True guarantees, that {@link OutputStream#close()} + * is called on the stream. False indicates, that only + * {@link OutputStream#flush()} should be called finally. + * @param buffer Temporary buffer, which is to be used for + * copying data. + * @return Number of bytes, which have been copied. + * @throws IOException An I/O error occurred. + */ + public static long copy(final InputStream inputStream, + final OutputStream outputStream, final boolean closeOutputStream, + final byte[] buffer) + throws IOException { + try (OutputStream out = outputStream; + InputStream in = inputStream) { + long total = 0; + for (;;) { + final int res = in.read(buffer); + if (res == -1) { + break; + } + if (res > 0) { + total += res; + if (out != null) { + out.write(buffer, 0, res); + } + } + } + if (out != null) { + if (closeOutputStream) { + out.close(); + } else { + out.flush(); + } + } + in.close(); + return total; + } + } + + /** + * Checks, whether the given file name is valid in the sense, + * that it doesn't contain any NUL characters. If the file name + * is valid, it will be returned without any modifications. Otherwise, + * an {@link InvalidFileNameException} is raised. + * + * @param fileName The file name to check + * @return Unmodified file name, if valid. + * @throws InvalidFileNameException The file name was found to be invalid. + */ + public static String checkFileName(final String fileName) { + if (fileName != null && fileName.indexOf('\u0000') != -1) { + // pFileName.replace("\u0000", "\\0") + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < fileName.length(); i++) { + final char c = fileName.charAt(i); + switch (c) { + case 0: + sb.append("\\0"); + break; + default: + sb.append(c); + break; + } + } + throw new InvalidFileNameException(fileName, + "Invalid file name: " + sb); + } + return fileName; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java new file mode 100644 index 0000000..d551514 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java @@ -0,0 +1,279 @@ +/* + * 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.tomcat.util.http.fileupload.util.mime; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.apache.tomcat.util.codec.binary.Base64; + +/** + * Utility class to decode MIME texts. + * + * @since FileUpload 1.3 + */ +public final class MimeUtility { + + /** + * The marker to indicate text is encoded with BASE64 algorithm. + */ + private static final String BASE64_ENCODING_MARKER = "B"; + + /** + * The marker to indicate text is encoded with QuotedPrintable algorithm. + */ + private static final String QUOTEDPRINTABLE_ENCODING_MARKER = "Q"; + + /** + * If the text contains any encoded tokens, those tokens will be marked with "=?". + */ + private static final String ENCODED_TOKEN_MARKER = "=?"; + + /** + * If the text contains any encoded tokens, those tokens will terminate with "=?". + */ + private static final String ENCODED_TOKEN_FINISHER = "?="; + + /** + * The linear whitespace chars sequence. + */ + private static final String LINEAR_WHITESPACE = " \t\r\n"; + + /** + * Mappings between MIME and Java charset. + */ + private static final Map MIME2JAVA = new HashMap<>(); + + static { + MIME2JAVA.put("iso-2022-cn", "ISO2022CN"); + MIME2JAVA.put("iso-2022-kr", "ISO2022KR"); + MIME2JAVA.put("utf-8", "UTF8"); + MIME2JAVA.put("utf8", "UTF8"); + MIME2JAVA.put("ja_jp.iso2022-7", "ISO2022JP"); + MIME2JAVA.put("ja_jp.eucjp", "EUCJIS"); + MIME2JAVA.put("euc-kr", "KSC5601"); + MIME2JAVA.put("euckr", "KSC5601"); + MIME2JAVA.put("us-ascii", "ISO-8859-1"); + MIME2JAVA.put("x-us-ascii", "ISO-8859-1"); + } + + /** + * Hidden constructor, this class must not be instantiated. + */ + private MimeUtility() { + // do nothing + } + + /** + * Decode a string of text obtained from a mail header into + * its proper form. The text generally will consist of a + * string of tokens, some of which may be encoded using + * base64 encoding. + * + * @param text The text to decode. + * + * @return The decoded text string. + * @throws UnsupportedEncodingException if the detected encoding in the input text is not supported. + */ + public static String decodeText(final String text) throws UnsupportedEncodingException { + // if the text contains any encoded tokens, those tokens will be marked with "=?". If the + // source string doesn't contain that sequent, no decoding is required. + if (!text.contains(ENCODED_TOKEN_MARKER)) { + return text; + } + + int offset = 0; + final int endOffset = text.length(); + + int startWhiteSpace = -1; + int endWhiteSpace = -1; + + final StringBuilder decodedText = new StringBuilder(text.length()); + + boolean previousTokenEncoded = false; + + while (offset < endOffset) { + char ch = text.charAt(offset); + + // is this a whitespace character? + if (LINEAR_WHITESPACE.indexOf(ch) != -1) { // whitespace found + startWhiteSpace = offset; + while (offset < endOffset) { + // step over the white space characters. + ch = text.charAt(offset); + if (LINEAR_WHITESPACE.indexOf(ch) == -1) { + // record the location of the first non lwsp and drop down to process the + // token characters. + endWhiteSpace = offset; + break; + } + offset++; + } + } else { + // we have a word token. We need to scan over the word and then try to parse it. + final int wordStart = offset; + + while (offset < endOffset) { + // step over the non white space characters. + ch = text.charAt(offset); + if (LINEAR_WHITESPACE.indexOf(ch) != -1) { + break; + } + offset++; + + //NB: Trailing whitespace on these header strings will just be discarded. + } + // pull out the word token. + final String word = text.substring(wordStart, offset); + // is the token encoded? decode the word + if (word.startsWith(ENCODED_TOKEN_MARKER)) { + try { + // if this gives a parsing failure, treat it like a non-encoded word. + final String decodedWord = decodeWord(word); + + // are any whitespace characters significant? Append 'em if we've got 'em. + if (!previousTokenEncoded && startWhiteSpace != -1) { + decodedText.append(text, startWhiteSpace, endWhiteSpace); + startWhiteSpace = -1; + } + // this is definitely a decoded token. + previousTokenEncoded = true; + // and add this to the text. + decodedText.append(decodedWord); + // we continue parsing from here...we allow parsing errors to fall through + // and get handled as normal text. + continue; + + } catch (final ParseException e) { + // just ignore it, skip to next word + } + } + // this is a normal token, so it doesn't matter what the previous token was. Add the white space + // if we have it. + if (startWhiteSpace != -1) { + decodedText.append(text, startWhiteSpace, endWhiteSpace); + startWhiteSpace = -1; + } + // this is not a decoded token. + previousTokenEncoded = false; + decodedText.append(word); + } + } + + return decodedText.toString(); + } + + /** + * Parse a string using the RFC 2047 rules for an "encoded-word" + * type. This encoding has the syntax: + * + * encoded-word = "=?" charset "?" encoding "?" encoded-text "?=" + * + * @param word The possibly encoded word value. + * + * @return The decoded word. + * @throws ParseException in case of a parse error of the RFC 2047 + * @throws UnsupportedEncodingException Thrown when Invalid RFC 2047 encoding was found + */ + private static String decodeWord(final String word) throws ParseException, UnsupportedEncodingException { + // encoded words start with the characters "=?". If this not an encoded word, we throw a + // ParseException for the caller. + + if (!word.startsWith(ENCODED_TOKEN_MARKER)) { + throw new ParseException("Invalid RFC 2047 encoded-word: " + word); + } + + final int charsetPos = word.indexOf('?', 2); + if (charsetPos == -1) { + throw new ParseException("Missing charset in RFC 2047 encoded-word: " + word); + } + + // pull out the character set information (this is the MIME name at this point). + final String charset = word.substring(2, charsetPos).toLowerCase(Locale.ENGLISH); + + // now pull out the encoding token the same way. + final int encodingPos = word.indexOf('?', charsetPos + 1); + if (encodingPos == -1) { + throw new ParseException("Missing encoding in RFC 2047 encoded-word: " + word); + } + + final String encoding = word.substring(charsetPos + 1, encodingPos); + + // and finally the encoded text. + final int encodedTextPos = word.indexOf(ENCODED_TOKEN_FINISHER, encodingPos + 1); + if (encodedTextPos == -1) { + throw new ParseException("Missing encoded text in RFC 2047 encoded-word: " + word); + } + + final String encodedText = word.substring(encodingPos + 1, encodedTextPos); + + // seems a bit silly to encode a null string, but easy to deal with. + if (encodedText.isEmpty()) { + return ""; + } + + try { + // the decoder writes directly to an output stream. + final ByteArrayOutputStream out = new ByteArrayOutputStream(encodedText.length()); + + byte[] decodedData; + // Base64 encoded? + if (encoding.equals(BASE64_ENCODING_MARKER)) { + decodedData = Base64.decodeBase64(encodedText); + } else if (encoding.equals(QUOTEDPRINTABLE_ENCODING_MARKER)) { // maybe quoted printable. + byte[] encodedData = encodedText.getBytes(StandardCharsets.US_ASCII); + QuotedPrintableDecoder.decode(encodedData, out); + decodedData = out.toByteArray(); + } else { + throw new UnsupportedEncodingException("Unknown RFC 2047 encoding: " + encoding); + } + // Convert decoded byte data into a string. + return new String(decodedData, javaCharset(charset)); + } catch (final IOException e) { + throw new UnsupportedEncodingException("Invalid RFC 2047 encoding"); + } + } + + /** + * Translate a MIME standard character set name into the Java + * equivalent. + * + * @param charset The MIME standard name. + * + * @return The Java equivalent for this name. + */ + private static String javaCharset(final String charset) { + // nothing in, nothing out. + if (charset == null) { + return null; + } + + final String mappedCharset = MIME2JAVA.get(charset.toLowerCase(Locale.ENGLISH)); + // if there is no mapping, then the original name is used. Many of the MIME character set + // names map directly back into Java. The reverse isn't necessarily true. + if (mappedCharset == null) { + return charset; + } + return mappedCharset; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java new file mode 100644 index 0000000..72e9896 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java @@ -0,0 +1,38 @@ +/* + * 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.tomcat.util.http.fileupload.util.mime; + +/** + * @since FileUpload 1.3 + */ +final class ParseException extends Exception { + + /** + * The UID to use when serializing this instance. + */ + private static final long serialVersionUID = 5355281266579392077L; + + /** + * Constructs a new exception with the specified detail message. + * + * @param message the detail message. + */ + ParseException(final String message) { + super(message); + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java new file mode 100644 index 0000000..6a94f3c --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java @@ -0,0 +1,112 @@ +/* + * 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.tomcat.util.http.fileupload.util.mime; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * @since FileUpload 1.3 + */ +final class QuotedPrintableDecoder { + + /** + * The shift value required to create the upper nibble + * from the first of 2 byte values converted from ascii hex. + */ + private static final int UPPER_NIBBLE_SHIFT = Byte.SIZE / 2; + + /** + * Hidden constructor, this class must not be instantiated. + */ + private QuotedPrintableDecoder() { + // do nothing + } + + /** + * Decode the encoded byte data writing it to the given output stream. + * + * @param data The array of byte data to decode. + * @param out The output stream used to return the decoded data. + * + * @return the number of bytes produced. + * @throws IOException if an IO error occurs + */ + public static int decode(final byte[] data, final OutputStream out) throws IOException { + int off = 0; + final int length = data.length; + final int endOffset = off + length; + int bytesWritten = 0; + + while (off < endOffset) { + final byte ch = data[off++]; + + // space characters were translated to '_' on encode, so we need to translate them back. + if (ch == '_') { + out.write(' '); + } else if (ch == '=') { + // we found an encoded character. Reduce the 3 char sequence to one. + // but first, make sure we have two characters to work with. + if (off + 1 >= endOffset) { + throw new IOException("Invalid quoted printable encoding; truncated escape sequence"); + } + + final byte b1 = data[off++]; + final byte b2 = data[off++]; + + // we've found an encoded carriage return. The next char needs to be a newline + if (b1 == '\r') { + if (b2 != '\n') { + throw new IOException("Invalid quoted printable encoding; CR must be followed by LF"); + } + // this was a soft linebreak inserted by the encoding. We just toss this away + // on decode. + } else { + // this is a hex pair we need to convert back to a single byte. + final int c1 = hexToBinary(b1); + final int c2 = hexToBinary(b2); + out.write((c1 << UPPER_NIBBLE_SHIFT) | c2); + // 3 bytes in, one byte out + bytesWritten++; + } + } else { + // simple character, just write it out. + out.write(ch); + bytesWritten++; + } + } + + return bytesWritten; + } + + /** + * Convert a hex digit to the binary value it represents. + * + * @param b the ascii hex byte to convert (0-0, A-F, a-f) + * @return the int value of the hex byte, 0-15 + * @throws IOException if the byte is not a valid hex digit. + */ + private static int hexToBinary(final byte b) throws IOException { + // CHECKSTYLE IGNORE MagicNumber FOR NEXT 1 LINE + final int i = Character.digit((char) b, 16); + if (i == -1) { + throw new IOException("Invalid quoted printable encoding: not a valid hex digit: " + b); + } + return i; + } + +} diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java new file mode 100644 index 0000000..8a522a3 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java @@ -0,0 +1,154 @@ +/* + * 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.tomcat.util.http.fileupload.util.mime; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +/** + * Utility class to decode/encode character set on HTTP Header fields based on RFC 2231. + * This implementation adheres to RFC 5987 in particular, which was defined for HTTP headers. + *

    + * RFC 5987 builds on RFC 2231, but has lesser scope like + * mandatory charset definition + * and no parameter continuation + * + * @see RFC 2231 + * @see RFC 5987 + */ +public final class RFC2231Utility { + /** + * The Hexadecimal values char array. + */ + private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); + /** + * The Hexadecimal representation of 127. + */ + private static final byte MASK = 0x7f; + /** + * The Hexadecimal representation of 128. + */ + private static final int MASK_128 = 0x80; + /** + * The Hexadecimal decode value. + */ + private static final byte[] HEX_DECODE = new byte[MASK_128]; + + // create a ASCII decoded array of Hexadecimal values + static { + for (int i = 0; i < HEX_DIGITS.length; i++) { + HEX_DECODE[HEX_DIGITS[i]] = (byte) i; + HEX_DECODE[Character.toLowerCase(HEX_DIGITS[i])] = (byte) i; + } + } + + /** + * Private constructor so that no instances can be created. This class + * contains only static utility methods. + */ + private RFC2231Utility() { + } + + /** + * Checks if Asterisk (*) at the end of parameter name to indicate, + * if it has charset and language information to decode the value. + * @param paramName The parameter, which is being checked. + * @return {@code true}, if encoded as per RFC 2231, {@code false} otherwise + */ + public static boolean hasEncodedValue(final String paramName) { + if (paramName != null) { + return paramName.lastIndexOf('*') == (paramName.length() - 1); + } + return false; + } + + /** + * If {@code paramName} has Asterisk (*) at the end, it will be stripped off, + * else the passed value will be returned. + * @param paramName The parameter, which is being inspected. + * @return stripped {@code paramName} of Asterisk (*), if RFC2231 encoded + */ + public static String stripDelimiter(final String paramName) { + if (hasEncodedValue(paramName)) { + final StringBuilder paramBuilder = new StringBuilder(paramName); + paramBuilder.deleteCharAt(paramName.lastIndexOf('*')); + return paramBuilder.toString(); + } + return paramName; + } + + /** + * Decode a string of text obtained from a HTTP header as per RFC 2231 + *

    + * Eg 1. {@code us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A} + * will be decoded to {@code This is ***fun***} + *

    + * Eg 2. {@code iso-8859-1'en'%A3%20rate} + * will be decoded to {@code £ rate} + *

    + * Eg 3. {@code UTF-8''%c2%a3%20and%20%e2%82%ac%20rates} + * will be decoded to {@code £ and € rates} + * + * @param encodedText - Text to be decoded has a format of {@code ''} + * and ASCII only + * @return Decoded text based on charset encoding + * @throws UnsupportedEncodingException The requested character set wasn't found. + */ + public static String decodeText(final String encodedText) throws UnsupportedEncodingException { + final int langDelimitStart = encodedText.indexOf('\''); + if (langDelimitStart == -1) { + // missing charset + return encodedText; + } + final String mimeCharset = encodedText.substring(0, langDelimitStart); + final int langDelimitEnd = encodedText.indexOf('\'', langDelimitStart + 1); + if (langDelimitEnd == -1) { + // missing language + return encodedText; + } + final byte[] bytes = fromHex(encodedText.substring(langDelimitEnd + 1)); + return new String(bytes, getJavaCharset(mimeCharset)); + } + + /** + * Convert {@code text} to their corresponding Hex value. + * @param text - ASCII text input + * @return Byte array of characters decoded from ASCII table + */ + private static byte[] fromHex(final String text) { + final int shift = 4; + final ByteArrayOutputStream out = new ByteArrayOutputStream(text.length()); + for (int i = 0; i < text.length();) { + final char c = text.charAt(i++); + if (c == '%') { + if (i > text.length() - 2) { + break; // unterminated sequence + } + final byte b1 = HEX_DECODE[text.charAt(i++) & MASK]; + final byte b2 = HEX_DECODE[text.charAt(i++) & MASK]; + out.write((b1 << shift) | b2); + } else { + out.write((byte) c); + } + } + return out.toByteArray(); + } + + private static String getJavaCharset(final String mimeCharset) { + // good enough for standard values + return mimeCharset; + } +} diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/package-info.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/package-info.java new file mode 100644 index 0000000..5456e4e --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * MIME decoder implementation, imported and retailed from + * Apache Geronimo. + */ +package org.apache.tomcat.util.http.fileupload.util.mime; diff --git a/java/org/apache/tomcat/util/http/fileupload/util/package-info.java b/java/org/apache/tomcat/util/http/fileupload/util/package-info.java new file mode 100644 index 0000000..fda7c88 --- /dev/null +++ b/java/org/apache/tomcat/util/http/fileupload/util/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * This package contains various IO related utility classes + * or methods, which are basically reusable and not necessarily + * restricted to the scope of a file upload. + */ +package org.apache.tomcat.util.http.fileupload.util; diff --git a/java/org/apache/tomcat/util/http/package.html b/java/org/apache/tomcat/util/http/package.html new file mode 100644 index 0000000..54792a5 --- /dev/null +++ b/java/org/apache/tomcat/util/http/package.html @@ -0,0 +1,30 @@ + + + +util.http + + + + +Special utils for handling HTTP-specific entities - headers, parameters, +cookies, etc. + +The utils are not specific to tomcat, but use util.MessageBytes. + + + diff --git a/java/org/apache/tomcat/util/http/parser/AcceptEncoding.java b/java/org/apache/tomcat/util/http/parser/AcceptEncoding.java new file mode 100644 index 0000000..ba92702 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/AcceptEncoding.java @@ -0,0 +1,74 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +public class AcceptEncoding { + + private final String encoding; + private final double quality; + + protected AcceptEncoding(String encoding, double quality) { + this.encoding = encoding; + this.quality = quality; + } + + public String getEncoding() { + return encoding; + } + + public double getQuality() { + return quality; + } + + + public static List parse(StringReader input) throws IOException { + + List result = new ArrayList<>(); + + do { + String encoding = HttpParser.readToken(input); + if (encoding == null) { + // Invalid encoding, skip to the next one + HttpParser.skipUntil(input, 0, ','); + continue; + } + + if (encoding.length() == 0) { + // No more data to read + break; + } + + // See if a quality has been provided + double quality = 1; + SkipResult lookForSemiColon = HttpParser.skipConstant(input, ";"); + if (lookForSemiColon == SkipResult.FOUND) { + quality = HttpParser.readWeight(input, ','); + } + + if (quality > 0) { + result.add(new AcceptEncoding(encoding, quality)); + } + } while (true); + + return result; + } +} diff --git a/java/org/apache/tomcat/util/http/parser/AcceptLanguage.java b/java/org/apache/tomcat/util/http/parser/AcceptLanguage.java new file mode 100644 index 0000000..dd8de7f --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/AcceptLanguage.java @@ -0,0 +1,78 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class AcceptLanguage { + + private final Locale locale; + private final double quality; + + protected AcceptLanguage(Locale locale, double quality) { + this.locale = locale; + this.quality = quality; + } + + public Locale getLocale() { + return locale; + } + + public double getQuality() { + return quality; + } + + + public static List parse(StringReader input) throws IOException { + + List result = new ArrayList<>(); + + do { + // Token is broader than what is permitted in a language tag + // (alphanumeric + '-') but any invalid values that slip through + // will be caught later + String languageTag = HttpParser.readToken(input); + if (languageTag == null) { + // Invalid tag, skip to the next one + HttpParser.skipUntil(input, 0, ','); + continue; + } + + if (languageTag.length() == 0) { + // No more data to read + break; + } + + // See if a quality has been provided + double quality = 1; + SkipResult lookForSemiColon = HttpParser.skipConstant(input, ";"); + if (lookForSemiColon == SkipResult.FOUND) { + quality = HttpParser.readWeight(input, ','); + } + + if (quality > 0) { + result.add(new AcceptLanguage(Locale.forLanguageTag(languageTag), quality)); + } + } while (true); + + return result; + } +} diff --git a/java/org/apache/tomcat/util/http/parser/Authorization.java b/java/org/apache/tomcat/util/http/parser/Authorization.java new file mode 100644 index 0000000..ec812ce --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/Authorization.java @@ -0,0 +1,136 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Parser for an "Authorization" header. + */ +public class Authorization { + + private static final Map fieldTypes = new HashMap<>(); + + static { + // Digest field types. + // Note: These are more relaxed than RFC2617. This adheres to the + // recommendation of RFC2616 that servers are tolerant of buggy + // clients when they can be so without ambiguity. + fieldTypes.put("username", FieldType.QUOTED_STRING); + fieldTypes.put("realm", FieldType.QUOTED_STRING); + fieldTypes.put("nonce", FieldType.QUOTED_STRING); + fieldTypes.put("digest-uri", FieldType.QUOTED_STRING); + // RFC2617 says response is <">32LHEX<">. 32LHEX will also be accepted + fieldTypes.put("response", FieldType.LHEX); + // RFC2617 says algorithm is token. <">token<"> will also be accepted + fieldTypes.put("algorithm", FieldType.QUOTED_TOKEN); + fieldTypes.put("cnonce", FieldType.QUOTED_STRING); + fieldTypes.put("opaque", FieldType.QUOTED_STRING); + // RFC2617 says qop is token. <">token<"> will also be accepted + fieldTypes.put("qop", FieldType.QUOTED_TOKEN); + // RFC2617 says nc is 8LHEX. <">8LHEX<"> will also be accepted + fieldTypes.put("nc", FieldType.LHEX); + + } + + + private Authorization() { + // Utility class. Hide default constructor. + } + + + /** + * Parses an HTTP Authorization header for DIGEST authentication as per RFC 2617 section 3.2.2. + * + * @param input The header value to parse + * + * @return A map of directives and values as {@link String}s or null if a parsing error occurs. + * Although the values returned are {@link String}s they will have been validated to ensure that they + * conform to RFC 2617. + * + * @throws IllegalArgumentException If the header does not conform to RFC 2617 + * @throws IOException If an error occurs while reading the input + */ + public static Map parseAuthorizationDigest(StringReader input) + throws IllegalArgumentException, IOException { + + Map result = new HashMap<>(); + + if (HttpParser.skipConstant(input, "Digest") != SkipResult.FOUND) { + return null; + } + // All field names are valid tokens + String field = HttpParser.readToken(input); + if (field == null) { + return null; + } + while (!field.equals("")) { + if (HttpParser.skipConstant(input, "=") != SkipResult.FOUND) { + return null; + } + String value = null; + FieldType type = fieldTypes.get(field.toLowerCase(Locale.ENGLISH)); + if (type == null) { + // auth-param = token "=" ( token | quoted-string ) + type = FieldType.TOKEN_OR_QUOTED_STRING; + } + switch (type) { + case QUOTED_STRING: + value = HttpParser.readQuotedString(input, false); + break; + case TOKEN_OR_QUOTED_STRING: + value = HttpParser.readTokenOrQuotedString(input, false); + break; + case LHEX: + value = HttpParser.readLhex(input); + break; + case QUOTED_TOKEN: + value = HttpParser.readQuotedToken(input); + break; + } + + if (value == null) { + return null; + } + result.put(field, value); + + if (HttpParser.skipConstant(input, ",") == SkipResult.NOT_FOUND) { + return null; + } + field = HttpParser.readToken(input); + if (field == null) { + return null; + } + } + + return result; + } + + + private enum FieldType { + // Unused due to buggy clients + // TOKEN, + QUOTED_STRING, + TOKEN_OR_QUOTED_STRING, + LHEX, + QUOTED_TOKEN; + } +} diff --git a/java/org/apache/tomcat/util/http/parser/ContentRange.java b/java/org/apache/tomcat/util/http/parser/ContentRange.java new file mode 100644 index 0000000..d77a0e4 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/ContentRange.java @@ -0,0 +1,108 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; + +public class ContentRange { + + private final String units; + private final long start; + private final long end; + private final long length; + + + public ContentRange(String units, long start, long end, long length) { + this.units = units; + this.start = start; + this.end = end; + this.length = length; + } + + + public String getUnits() { + return units; + } + + + public long getStart() { + return start; + } + + + public long getEnd() { + return end; + } + + + public long getLength() { + return length; + } + + + /** + * Parses a Content-Range header from an HTTP header. + * + * @param input a reader over the header text + * + * @return the range parsed from the input, or null if not valid + * + * @throws IOException if there was a problem reading the input + */ + public static ContentRange parse(StringReader input) throws IOException { + // Units (required) + String units = HttpParser.readToken(input); + if (units == null || units.length() == 0) { + return null; + } + + // Must be followed by SP. Parser is lenient and accepts any LWS here. + // No need for explicit check as something must have terminated the + // token and if that something was anything other than LWS the following + // call to readLong() will fail. + + // Start + long start = HttpParser.readLong(input); + + // Must be followed by '-' + if (HttpParser.skipConstant(input, "-") == SkipResult.NOT_FOUND) { + return null; + } + + // End + long end = HttpParser.readLong(input); + + // Must be followed by '/' + if (HttpParser.skipConstant(input, "/") == SkipResult.NOT_FOUND) { + return null; + } + + // Length + long length = HttpParser.readLong(input); + + // Doesn't matter what we look for, result should be EOF + SkipResult skipResult = HttpParser.skipConstant(input, "X"); + + if (skipResult != SkipResult.EOF) { + // Invalid range + return null; + } + + return new ContentRange(units, start, end, length); + } +} diff --git a/java/org/apache/tomcat/util/http/parser/Cookie.java b/java/org/apache/tomcat/util/http/parser/Cookie.java new file mode 100644 index 0000000..63b30d7 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/Cookie.java @@ -0,0 +1,304 @@ +/* + * 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.tomcat.util.http.parser; + +import java.nio.charset.StandardCharsets; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.ServerCookie; +import org.apache.tomcat.util.http.ServerCookies; +import org.apache.tomcat.util.log.UserDataHelper; +import org.apache.tomcat.util.res.StringManager; + + +/** + *

    + * Cookie header parser based on RFC6265 + *

    + *

    + * The parsing of cookies using RFC6265 is more relaxed that the specification in the following ways: + *

    + *
      + *
    • Values 0x80 to 0xFF are permitted in cookie-octet to support the use of UTF-8 in cookie values as used by HTML + * 5.
    • + *
    • For cookies without a value, the '=' is not required after the name as some browsers do not sent it.
    • + *
    + *

    + * Implementation note:
    + * This class has been carefully tuned. Before committing any changes, ensure that the TesterCookiePerformance unit test + * continues to give results within 1% for the old and new parsers. + *

    + */ +public class Cookie { + + private static final Log log = LogFactory.getLog(Cookie.class); + private static final UserDataHelper invalidCookieLog = new UserDataHelper(log); + private static final StringManager sm = StringManager.getManager("org.apache.tomcat.util.http.parser"); + + private static final boolean isCookieOctet[] = new boolean[256]; + private static final boolean isText[] = new boolean[256]; + private static final byte[] EMPTY_BYTES = new byte[0]; + private static final byte TAB_BYTE = (byte) 0x09; + private static final byte SPACE_BYTE = (byte) 0x20; + private static final byte QUOTE_BYTE = (byte) 0x22; + private static final byte COMMA_BYTE = (byte) 0x2C; + private static final byte SEMICOLON_BYTE = (byte) 0x3B; + private static final byte EQUALS_BYTE = (byte) 0x3D; + private static final byte SLASH_BYTE = (byte) 0x5C; + private static final byte DEL_BYTE = (byte) 0x7F; + + + static { + // %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E (RFC6265) + // %x80 to %xFF (UTF-8) + for (int i = 0; i < 256; i++) { + if (i < 0x21 || i == QUOTE_BYTE || i == COMMA_BYTE || i == SEMICOLON_BYTE || i == SLASH_BYTE || + i == DEL_BYTE) { + isCookieOctet[i] = false; + } else { + isCookieOctet[i] = true; + } + } + for (int i = 0; i < 256; i++) { + if (i < TAB_BYTE || (i > TAB_BYTE && i < SPACE_BYTE) || i == DEL_BYTE) { + isText[i] = false; + } else { + isText[i] = true; + } + } + } + + + private Cookie() { + // Hide default constructor + } + + + public static void parseCookie(byte[] bytes, int offset, int len, ServerCookies serverCookies) { + + // ByteBuffer is used throughout this parser as it allows the byte[] + // and position information to be easily passed between parsing methods + ByteBuffer bb = new ByteBuffer(bytes, offset, len); + + boolean moreToProcess = true; + + while (moreToProcess) { + skipLWS(bb); + + int start = bb.position(); + ByteBuffer name = readToken(bb); + ByteBuffer value = null; + + skipLWS(bb); + + SkipResult skipResult = skipByte(bb, EQUALS_BYTE); + if (skipResult == SkipResult.FOUND) { + skipLWS(bb); + value = readCookieValueRfc6265(bb); + if (value == null) { + // Invalid cookie value. Skip to the next semi-colon + skipUntilSemiColon(bb); + logInvalidHeader(start, bb); + continue; + } + skipLWS(bb); + } + + skipResult = skipByte(bb, SEMICOLON_BYTE); + if (skipResult == SkipResult.FOUND) { + // NO-OP + } else if (skipResult == SkipResult.NOT_FOUND) { + // Invalid cookie. Ignore it and skip to the next semi-colon + skipUntilSemiColon(bb); + logInvalidHeader(start, bb); + continue; + } else { + // SkipResult.EOF + moreToProcess = false; + } + + if (name.hasRemaining()) { + ServerCookie sc = serverCookies.addCookie(); + sc.getName().setBytes(name.array(), name.position(), name.remaining()); + if (value == null) { + sc.getValue().setBytes(EMPTY_BYTES, 0, EMPTY_BYTES.length); + } else { + sc.getValue().setBytes(value.array(), value.position(), value.remaining()); + } + } + } + } + + + private static void skipLWS(ByteBuffer bb) { + while (bb.hasRemaining()) { + byte b = bb.get(); + if (b != TAB_BYTE && b != SPACE_BYTE) { + bb.rewind(); + break; + } + } + } + + + private static void skipUntilSemiColon(ByteBuffer bb) { + while (bb.hasRemaining()) { + if (bb.get() == SEMICOLON_BYTE) { + break; + } + } + } + + + private static SkipResult skipByte(ByteBuffer bb, byte target) { + + if (!bb.hasRemaining()) { + return SkipResult.EOF; + } + if (bb.get() == target) { + return SkipResult.FOUND; + } + + bb.rewind(); + return SkipResult.NOT_FOUND; + } + + + /** + * Similar to readCookieValue() but treats a comma as part of an invalid value. + */ + private static ByteBuffer readCookieValueRfc6265(ByteBuffer bb) { + boolean quoted = false; + if (bb.hasRemaining()) { + if (bb.get() == QUOTE_BYTE) { + quoted = true; + } else { + bb.rewind(); + } + } + int start = bb.position(); + int end = bb.limit(); + while (bb.hasRemaining()) { + byte b = bb.get(); + if (isCookieOctet[(b & 0xFF)]) { + // NO-OP + } else if (b == SEMICOLON_BYTE || b == SPACE_BYTE || b == TAB_BYTE) { + end = bb.position() - 1; + bb.position(end); + break; + } else if (quoted && b == QUOTE_BYTE) { + end = bb.position() - 1; + break; + } else { + // Invalid cookie + return null; + } + } + + return new ByteBuffer(bb.bytes, start, end - start); + } + + + private static ByteBuffer readToken(ByteBuffer bb) { + final int start = bb.position(); + int end = bb.limit(); + while (bb.hasRemaining()) { + if (!HttpParser.isToken(bb.get())) { + end = bb.position() - 1; + bb.position(end); + break; + } + } + + return new ByteBuffer(bb.bytes, start, end - start); + } + + + private static void logInvalidHeader(int start, ByteBuffer bb) { + UserDataHelper.Mode logMode = invalidCookieLog.getNextMode(); + if (logMode != null) { + String headerValue = new String(bb.array(), start, bb.position() - start, StandardCharsets.UTF_8); + String message = sm.getString("cookie.invalidCookieValue", headerValue); + switch (logMode) { + case INFO_THEN_DEBUG: + message += sm.getString("cookie.fallToDebug"); + //$FALL-THROUGH$ + case INFO: + log.info(message); + break; + case DEBUG: + log.debug(message); + } + } + } + + + /** + * Custom implementation that skips many of the safety checks in {@link java.nio.ByteBuffer}. + */ + private static class ByteBuffer { + + private final byte[] bytes; + private int limit; + private int position = 0; + + ByteBuffer(byte[] bytes, int offset, int len) { + this.bytes = bytes; + this.position = offset; + this.limit = offset + len; + } + + public int position() { + return position; + } + + public void position(int position) { + this.position = position; + } + + public int limit() { + return limit; + } + + public int remaining() { + return limit - position; + } + + public boolean hasRemaining() { + return position < limit; + } + + public byte get() { + return bytes[position++]; + } + + public void rewind() { + position--; + } + + public byte[] array() { + return bytes; + } + + // For debug purposes + @Override + public String toString() { + return "position [" + position + "], limit [" + limit + "]"; + } + } +} diff --git a/java/org/apache/tomcat/util/http/parser/EntityTag.java b/java/org/apache/tomcat/util/http/parser/EntityTag.java new file mode 100644 index 0000000..132b480 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/EntityTag.java @@ -0,0 +1,93 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; + +public class EntityTag { + + /** + * Parse the given input as (per RFC 7232) 1#entity-tag. Compare an ETag header with a resource ETag as described in + * RFC 7232 section 2.3.2. + * + * @param input The input to parse + * @param compareWeak Use weak comparison e.g. match "etag" with W/"etag" + * @param resourceETag Resource's ETag + * + * @return {@code true} if ETag matches, {@code false} if ETag doesn't match or {@code null} if the input is invalid + * + * @throws IOException If an I/O occurs during the parsing + */ + public static Boolean compareEntityTag(StringReader input, boolean compareWeak, String resourceETag) + throws IOException { + // The resourceETag may be weak so to do weak comparison remove /W + // before comparison + String comparisonETag; + if (compareWeak && resourceETag.startsWith("W/")) { + comparisonETag = resourceETag.substring(2); + } else { + comparisonETag = resourceETag; + } + + Boolean result = Boolean.FALSE; + + while (true) { + boolean strong = false; + HttpParser.skipLws(input); + + switch (HttpParser.skipConstant(input, "W/")) { + case EOF: + // Empty values are invalid + return null; + case NOT_FOUND: + strong = true; + break; + case FOUND: + strong = false; + break; + } + + // Note: RFC 2616 allowed quoted string + // RFC 7232 does not allow " in the entity-tag + String value = HttpParser.readQuotedString(input, true); + if (value == null) { + // Not a quoted string so the header is invalid + return null; + } + + if (strong || compareWeak) { + if (comparisonETag.equals(value)) { + result = Boolean.TRUE; + } + } + + HttpParser.skipLws(input); + + switch (HttpParser.skipConstant(input, ",")) { + case EOF: + return result; + case NOT_FOUND: + // Not EOF and not "," so must be invalid + return null; + case FOUND: + // Parse next entry + break; + } + } + } +} diff --git a/java/org/apache/tomcat/util/http/parser/Host.java b/java/org/apache/tomcat/util/http/parser/Host.java new file mode 100644 index 0000000..f9cb602 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/Host.java @@ -0,0 +1,140 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; + +public class Host { + + private Host() { + // Utility class. Hide default constructor. + } + + + /** + * Parse the given input as an HTTP Host header value. + * + * @param mb The host header value + * + * @return The position of ':' that separates the host from the port or -1 if it is not present + * + * @throws IllegalArgumentException If the host header value is not specification compliant + */ + public static int parse(MessageBytes mb) { + return parse(new MessageBytesReader(mb)); + } + + + /** + * Parse the given input as an HTTP Host header value. + * + * @param string The host header value + * + * @return The position of ':' that separates the host from the port or -1 if it is not present + * + * @throws IllegalArgumentException If the host header value is not specification compliant + */ + public static int parse(String string) { + return parse(new StringReader(string)); + } + + + private static int parse(Reader reader) { + try { + reader.mark(1); + int first = reader.read(); + reader.reset(); + if (HttpParser.isAlpha(first)) { + return HttpParser.readHostDomainName(reader); + } else if (HttpParser.isNumeric(first)) { + return HttpParser.readHostIPv4(reader, false); + } else if ('[' == first) { + return HttpParser.readHostIPv6(reader); + } else { + // Invalid + throw new IllegalArgumentException(); + } + } catch (IOException ioe) { + // Should never happen + throw new IllegalArgumentException(ioe); + } + } + + + private static class MessageBytesReader extends Reader { + + private final byte[] bytes; + private final int end; + private int pos; + private int mark; + + MessageBytesReader(MessageBytes mb) { + ByteChunk bc = mb.getByteChunk(); + bytes = bc.getBytes(); + pos = bc.getOffset(); + end = bc.getEnd(); + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + for (int i = off; i < off + len; i++) { + // Want output in range 0 to 255, not -128 to 127 + cbuf[i] = (char) (bytes[pos++] & 0xFF); + } + return len; + } + + @Override + public void close() throws IOException { + // NO-OP + } + + // Over-ridden methods to improve performance + + @Override + public int read() throws IOException { + if (pos < end) { + // Want output in range 0 to 255, not -128 to 127 + return bytes[pos++] & 0xFF; + } else { + return -1; + } + } + + // Methods to support mark/reset + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void mark(int readAheadLimit) throws IOException { + mark = pos; + } + + @Override + public void reset() throws IOException { + pos = mark; + } + } +} diff --git a/java/org/apache/tomcat/util/http/parser/HttpParser.java b/java/org/apache/tomcat/util/http/parser/HttpParser.java new file mode 100644 index 0000000..101a4db --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/HttpParser.java @@ -0,0 +1,1049 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.Reader; + +import org.apache.tomcat.util.res.StringManager; + +/** + * HTTP header value parser implementation. Parsing HTTP headers as per RFC2616 is not always as simple as it first + * appears. For headers that only use tokens the simple approach will normally be sufficient. However, for the other + * headers, while simple code meets 99.9% of cases, there are often some edge cases that make things far more + * complicated. The purpose of this parser is to let the parser worry about the edge cases. It provides tolerant (where + * safe to do so) parsing of HTTP header values assuming that wrapped header lines have already been unwrapped. (The + * Tomcat header processing code does the unwrapping.) + */ +public class HttpParser { + + private static final StringManager sm = StringManager.getManager(HttpParser.class); + + private static final int ARRAY_SIZE = 128; + + private static final boolean[] IS_CONTROL = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_HEX = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_HTTP_PROTOCOL = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_ALPHA = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_NUMERIC = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_SCHEME = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_UNRESERVED = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_SUBDELIM = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_USERINFO = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_RELAXABLE = new boolean[ARRAY_SIZE]; + + private static final HttpParser DEFAULT; + + + static { + for (int i = 0; i < ARRAY_SIZE; i++) { + // Control> 0-31, 127 + if (i < 32 || i == 127) { + IS_CONTROL[i] = true; + } + + // Separator + if (i == '(' || i == ')' || i == '<' || i == '>' || i == '@' || i == ',' || i == ';' || i == ':' || + i == '\\' || i == '\"' || i == '/' || i == '[' || i == ']' || i == '?' || i == '=' || i == '{' || + i == '}' || i == ' ' || i == '\t') { + IS_SEPARATOR[i] = true; + } + + // Token: Anything 0-127 that is not a control and not a separator + if (!IS_CONTROL[i] && !IS_SEPARATOR[i] && i < 128) { + IS_TOKEN[i] = true; + } + + // Hex: 0-9, a-f, A-F + if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) { + IS_HEX[i] = true; + } + + // Not valid for HTTP protocol + // "HTTP/" DIGIT "." DIGIT + if (i == 'H' || i == 'T' || i == 'P' || i == '/' || i == '.' || (i >= '0' && i <= '9')) { + IS_HTTP_PROTOCOL[i] = true; + } + + if (i >= '0' && i <= '9') { + IS_NUMERIC[i] = true; + } + + if (i >= 'a' && i <= 'z' || i >= 'A' && i <= 'Z') { + IS_ALPHA[i] = true; + } + + if (IS_ALPHA[i] || IS_NUMERIC[i] || i == '+' || i == '-' || i == '.') { + IS_SCHEME[i] = true; + } + + if (IS_ALPHA[i] || IS_NUMERIC[i] || i == '-' || i == '.' || i == '_' || i == '~') { + IS_UNRESERVED[i] = true; + } + + if (i == '!' || i == '$' || i == '&' || i == '\'' || i == '(' || i == ')' || i == '*' || i == '+' || + i == ',' || i == ';' || i == '=') { + IS_SUBDELIM[i] = true; + } + + // userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) + if (IS_UNRESERVED[i] || i == '%' || IS_SUBDELIM[i] || i == ':') { + IS_USERINFO[i] = true; + } + + // The characters that are normally not permitted for which the + // restrictions may be relaxed when used in the path and/or query + // string + if (i == '\"' || i == '<' || i == '>' || i == '[' || i == '\\' || i == ']' || i == '^' || i == '`' || + i == '{' || i == '|' || i == '}') { + IS_RELAXABLE[i] = true; + } + } + + DEFAULT = new HttpParser(null, null); + } + + + private final boolean[] IS_NOT_REQUEST_TARGET = new boolean[ARRAY_SIZE]; + private final boolean[] IS_ABSOLUTEPATH_RELAXED = new boolean[ARRAY_SIZE]; + private final boolean[] IS_QUERY_RELAXED = new boolean[ARRAY_SIZE]; + + + public HttpParser(String relaxedPathChars, String relaxedQueryChars) { + for (int i = 0; i < ARRAY_SIZE; i++) { + // Not valid for request target. + // Combination of multiple rules from RFC7230 and RFC 3986. Must be + // ASCII, no controls plus a few additional characters excluded + if (IS_CONTROL[i] || i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' || i == '^' || + i == '`' || i == '{' || i == '|' || i == '}') { + IS_NOT_REQUEST_TARGET[i] = true; + } + + /* + * absolute-path = 1*( "/" segment ) segment = *pchar pchar = unreserved / pct-encoded / sub-delims / ":" / + * "@" + * + * Note pchar allows everything userinfo allows plus "@" + */ + if (IS_USERINFO[i] || i == '@' || i == '/') { + IS_ABSOLUTEPATH_RELAXED[i] = true; + } + + /* + * query = *( pchar / "/" / "?" ) + * + * Note query allows everything absolute-path allows plus "?" + */ + if (IS_ABSOLUTEPATH_RELAXED[i] || i == '?') { + IS_QUERY_RELAXED[i] = true; + } + } + + relax(IS_ABSOLUTEPATH_RELAXED, relaxedPathChars); + relax(IS_QUERY_RELAXED, relaxedQueryChars); + } + + + public boolean isNotRequestTargetRelaxed(int c) { + // Fast for valid request target characters, slower for some incorrect + // ones + try { + return IS_NOT_REQUEST_TARGET[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return true; + } + } + + + public boolean isAbsolutePathRelaxed(int c) { + // Fast for valid user info characters, slower for some incorrect + // ones + try { + return IS_ABSOLUTEPATH_RELAXED[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + public boolean isQueryRelaxed(int c) { + // Fast for valid user info characters, slower for some incorrect + // ones + try { + return IS_QUERY_RELAXED[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + public static String unquote(String input) { + if (input == null || input.length() < 2) { + return input; + } + + int start; + int end; + + // Skip surrounding quotes if there are any + if (input.charAt(0) == '"') { + start = 1; + end = input.length() - 1; + } else { + start = 0; + end = input.length(); + } + + StringBuilder result = new StringBuilder(); + for (int i = start; i < end; i++) { + char c = input.charAt(i); + if (input.charAt(i) == '\\') { + i++; + if (i == end) { + // Input (less surrounding quotes) ended with '\'. That is + // invalid so return null. + return null; + } + result.append(input.charAt(i)); + } else { + result.append(c); + } + } + return result.toString(); + } + + + public static boolean isToken(int c) { + // Fast for correct values, slower for incorrect ones + try { + return IS_TOKEN[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + /** + * Is the provided String a token as per RFC 7230?
    + * Note: token = 1 * tchar (RFC 7230)
    + * Since a token requires at least 1 tchar, {@code null} and the empty string ({@code ""}) are not considered to be + * valid tokens. + * + * @param s The string to test + * + * @return {@code true} if the string is a valid token, otherwise {@code false} + */ + public static boolean isToken(String s) { + if (s == null) { + return false; + } + if (s.isEmpty()) { + return false; + } + for (char c : s.toCharArray()) { + if (!isToken(c)) { + return false; + } + } + return true; + } + + + public static boolean isHex(int c) { + // Fast for correct values, slower for some incorrect ones + try { + return IS_HEX[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + public static boolean isNotRequestTarget(int c) { + return DEFAULT.isNotRequestTargetRelaxed(c); + } + + + public static boolean isHttpProtocol(int c) { + // Fast for valid HTTP protocol characters, slower for some incorrect + // ones + try { + return IS_HTTP_PROTOCOL[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + public static boolean isAlpha(int c) { + // Fast for valid alpha characters, slower for some incorrect + // ones + try { + return IS_ALPHA[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + public static boolean isNumeric(int c) { + // Fast for valid numeric characters, slower for some incorrect + // ones + try { + return IS_NUMERIC[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + public static boolean isScheme(int c) { + // Fast for valid scheme characters, slower for some incorrect + // ones + try { + return IS_SCHEME[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + /** + * Is the provided String a scheme as per RFC 3986?
    + * Note: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
    + * Since a scheme requires at least 1 ALPHA, {@code null} and the empty string ({@code ""}) are not considered to be + * valid tokens. + * + * @param s The string to test + * + * @return {@code true} if the string is a valid scheme, otherwise {@code false} + */ + public static boolean isScheme(String s) { + if (s == null) { + return false; + } + if (s.isEmpty()) { + return false; + } + char[] chars = s.toCharArray(); + if (!isAlpha(chars[0])) { + return false; + } + if (chars.length > 1) { + for (int i = 1; i < chars.length; i++) { + if (!isScheme(chars[i])) { + return false; + } + } + } + return true; + } + + + public static boolean isUserInfo(int c) { + // Fast for valid user info characters, slower for some incorrect + // ones + try { + return IS_USERINFO[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + private static boolean isRelaxable(int c) { + // Fast for valid user info characters, slower for some incorrect + // ones + try { + return IS_RELAXABLE[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + public static boolean isAbsolutePath(int c) { + return DEFAULT.isAbsolutePathRelaxed(c); + } + + + public static boolean isQuery(int c) { + return DEFAULT.isQueryRelaxed(c); + } + + + public static boolean isControl(int c) { + // Fast for valid control characters, slower for some incorrect + // ones + try { + return IS_CONTROL[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + // Skip any LWS and position to read the next character. The next character + // is returned as being able to 'peek()' it allows a small optimisation in + // some cases. + static int skipLws(Reader input) throws IOException { + + input.mark(1); + int c = input.read(); + + while (c == 32 || c == 9 || c == 10 || c == 13) { + input.mark(1); + c = input.read(); + } + + input.reset(); + return c; + } + + static SkipResult skipConstant(Reader input, String constant) throws IOException { + int len = constant.length(); + + skipLws(input); + input.mark(len); + int c = input.read(); + + for (int i = 0; i < len; i++) { + if (i == 0 && c == -1) { + return SkipResult.EOF; + } + if (c != constant.charAt(i)) { + input.reset(); + return SkipResult.NOT_FOUND; + } + if (i != (len - 1)) { + c = input.read(); + } + } + return SkipResult.FOUND; + } + + /** + * @return the token if one was found, the empty string if no data was available to read or null if + * data other than a token was found + */ + static String readToken(Reader input) throws IOException { + StringBuilder result = new StringBuilder(); + + skipLws(input); + input.mark(1); + int c = input.read(); + + while (c != -1 && isToken(c)) { + result.append((char) c); + input.mark(1); + c = input.read(); + } + // Use mark(1)/reset() rather than skip(-1) since skip() is a NOP + // once the end of the String has been reached. + input.reset(); + + if (c != -1 && result.length() == 0) { + return null; + } else { + return result.toString(); + } + } + + /** + * @return the digits if any were found, the empty string if no data was found or if data other than digits was + * found + */ + static String readDigits(Reader input) throws IOException { + StringBuilder result = new StringBuilder(); + + skipLws(input); + input.mark(1); + int c = input.read(); + + while (c != -1 && isNumeric(c)) { + result.append((char) c); + input.mark(1); + c = input.read(); + } + // Use mark(1)/reset() rather than skip(-1) since skip() is a NOP + // once the end of the String has been reached. + input.reset(); + + return result.toString(); + } + + /** + * @return the number if digits were found, -1 if no data was found or if data other than digits was found + */ + static long readLong(Reader input) throws IOException { + String digits = readDigits(input); + + if (digits.length() == 0) { + return -1; + } + + return Long.parseLong(digits); + } + + /** + * @return the quoted string if one was found, null if data other than a quoted string was found or null if the end + * of data was reached before the quoted string was terminated + */ + static String readQuotedString(Reader input, boolean returnQuoted) throws IOException { + + skipLws(input); + int c = input.read(); + + if (c != '"') { + return null; + } + + StringBuilder result = new StringBuilder(); + if (returnQuoted) { + result.append('\"'); + } + c = input.read(); + + while (c != '"') { + if (c == -1) { + return null; + } else if (c == '\\') { + c = input.read(); + if (returnQuoted) { + result.append('\\'); + } + result.append((char) c); + } else { + result.append((char) c); + } + c = input.read(); + } + if (returnQuoted) { + result.append('\"'); + } + + return result.toString(); + } + + static String readTokenOrQuotedString(Reader input, boolean returnQuoted) throws IOException { + + // Peek at next character to enable correct method to be called + int c = skipLws(input); + + if (c == '"') { + return readQuotedString(input, returnQuoted); + } else { + return readToken(input); + } + } + + /** + * Token can be read unambiguously with or without surrounding quotes so this parsing method for token permits + * optional surrounding double quotes. This is not defined in any RFC. It is a special case to handle data from + * buggy clients (known buggy clients for DIGEST auth include Microsoft IE 8 & 9, Apple Safari for OSX and iOS) + * that add quotes to values that should be tokens. + * + * @return the token if one was found, null if data other than a token or quoted token was found or null if the end + * of data was reached before a quoted token was terminated + */ + static String readQuotedToken(Reader input) throws IOException { + + StringBuilder result = new StringBuilder(); + boolean quoted = false; + + skipLws(input); + input.mark(1); + int c = input.read(); + + if (c == '"') { + quoted = true; + } else if (c == -1 || !isToken(c)) { + return null; + } else { + result.append((char) c); + } + input.mark(1); + c = input.read(); + + while (c != -1 && isToken(c)) { + result.append((char) c); + input.mark(1); + c = input.read(); + } + + if (quoted) { + if (c != '"') { + return null; + } + } else { + // Use mark(1)/reset() rather than skip(-1) since skip() is a NOP + // once the end of the String has been reached. + input.reset(); + } + + if (c != -1 && result.length() == 0) { + return null; + } else { + return result.toString(); + } + } + + /** + * LHEX can be read unambiguously with or without surrounding quotes so this parsing method for LHEX permits + * optional surrounding double quotes. Some buggy clients (libwww-perl for DIGEST auth) are known to send quoted + * LHEX when the specification requires just LHEX. + *

    + * LHEX are, literally, lower-case hexadecimal digits. This implementation allows for upper-case digits as well, + * converting the returned value to lower-case. + * + * @return the sequence of LHEX (minus any surrounding quotes) if any was found, or null if data other + * LHEX was found + */ + static String readLhex(Reader input) throws IOException { + + StringBuilder result = new StringBuilder(); + boolean quoted = false; + + skipLws(input); + input.mark(1); + int c = input.read(); + + if (c == '"') { + quoted = true; + } else if (c == -1 || !isHex(c)) { + return null; + } else { + if ('A' <= c && c <= 'F') { + c -= ('A' - 'a'); + } + result.append((char) c); + } + input.mark(1); + c = input.read(); + + while (c != -1 && isHex(c)) { + if ('A' <= c && c <= 'F') { + c -= ('A' - 'a'); + } + result.append((char) c); + input.mark(1); + c = input.read(); + } + + if (quoted) { + if (c != '"') { + return null; + } + } else { + // Use mark(1)/reset() rather than skip(-1) since skip() is a NOP + // once the end of the String has been reached. + input.reset(); + } + + if (c != -1 && result.length() == 0) { + return null; + } else { + return result.toString(); + } + } + + static double readWeight(Reader input, char delimiter) throws IOException { + skipLws(input); + int c = input.read(); + if (c == -1 || c == delimiter) { + // No q value just whitespace + return 1; + } else if (c != 'q') { + // Malformed. Use quality of zero so it is dropped. + skipUntil(input, c, delimiter); + return 0; + } + // RFC 7231 does not allow whitespace here but be tolerant + skipLws(input); + c = input.read(); + if (c != '=') { + // Malformed. Use quality of zero so it is dropped. + skipUntil(input, c, delimiter); + return 0; + } + + // RFC 7231 does not allow whitespace here but be tolerant + skipLws(input); + c = input.read(); + + // Should be no more than 3 decimal places + StringBuilder value = new StringBuilder(5); + int decimalPlacesRead = -1; + + if (c == '0' || c == '1') { + value.append((char) c); + c = input.read(); + + while (true) { + if (decimalPlacesRead == -1 && c == '.') { + value.append('.'); + decimalPlacesRead = 0; + } else if (decimalPlacesRead > -1 && c >= '0' && c <= '9') { + if (decimalPlacesRead < 3) { + value.append((char) c); + decimalPlacesRead++; + } + } else { + break; + } + c = input.read(); + } + } else { + // Malformed. Use quality of zero so it is dropped and skip until + // EOF or the next delimiter + skipUntil(input, c, delimiter); + return 0; + } + + if (c == 9 || c == 32) { + skipLws(input); + c = input.read(); + } + + // Must be at delimiter or EOF + if (c != delimiter && c != -1) { + // Malformed. Use quality of zero so it is dropped and skip until + // EOF or the next delimiter + skipUntil(input, c, delimiter); + return 0; + } + + double result = Double.parseDouble(value.toString()); + if (result > 1) { + return 0; + } + return result; + } + + + /** + * @return If inIPv6 is false, the position of ':' that separates the host from the port or -1 if it is not present. + * If inIPv6 is true, the number of characters read + */ + static int readHostIPv4(Reader reader, boolean inIPv6) throws IOException { + int octet = -1; + int octetCount = 1; + int c; + int pos = 0; + + // readAheadLimit doesn't matter as all the readers passed to this + // method buffer the entire content. + reader.mark(1); + do { + c = reader.read(); + if (c == '.') { + if (octet > -1 && octet < 256) { + // Valid + octetCount++; + octet = -1; + } else if (inIPv6 || octet == -1) { + throw new IllegalArgumentException(sm.getString("http.invalidOctet", Integer.toString(octet))); + } else { + // Might not be an IPv4 address. Could be a host / FQDN with + // a fully numeric component. + reader.reset(); + return readHostDomainName(reader); + } + } else if (isNumeric(c)) { + if (octet == -1) { + octet = c - '0'; + } else if (octet == 0) { + // Leading zero in non-zero octet. Not valid (ambiguous). + if (inIPv6) { + throw new IllegalArgumentException(sm.getString("http.invalidLeadingZero")); + } else { + // Could be a host/FQDN + reader.reset(); + return readHostDomainName(reader); + } + } else { + octet = octet * 10 + c - '0'; + // Avoid overflow + if (octet > 255) { + break; + } + } + } else if (c == ':') { + break; + } else if (c == -1) { + if (inIPv6) { + throw new IllegalArgumentException(sm.getString("http.noClosingBracket")); + } else { + pos = -1; + break; + } + } else if (c == ']') { + if (inIPv6) { + pos++; + break; + } else { + throw new IllegalArgumentException(sm.getString("http.closingBracket")); + } + } else if (!inIPv6 && (isAlpha(c) || c == '-')) { + // Go back to the start and parse as a host / FQDN + reader.reset(); + return readHostDomainName(reader); + } else { + throw new IllegalArgumentException( + sm.getString("http.illegalCharacterIpv4", Character.toString((char) c))); + } + pos++; + } while (true); + + if (octetCount != 4 || octet < 0 || octet > 255) { + // Might not be an IPv4 address. Could be a host name or a FQDN with + // fully numeric components. Go back to the start and parse as a + // host / FQDN. + reader.reset(); + return readHostDomainName(reader); + } + + if (inIPv6) { + return pos; + } else { + return validatePort(reader, pos); + } + } + + + /** + * @return The position of ':' that separates the host from the port or -1 if it is not present + */ + static int readHostIPv6(Reader reader) throws IOException { + // Must start with '[' + int c = reader.read(); + if (c != '[') { + throw new IllegalArgumentException(sm.getString("http.noOpeningBracket")); + } + + int h16Count = 0; + int h16Size = 0; + int pos = 1; + boolean parsedDoubleColon = false; + int precedingColonsCount = 0; + + do { + c = reader.read(); + if (h16Count == 0 && precedingColonsCount == 1 && c != ':') { + // Can't start with a single : + throw new IllegalArgumentException(sm.getString("http.singleColonStart")); + } + if (isHex(c)) { + if (h16Size == 0) { + // Start of a new h16 block + precedingColonsCount = 0; + h16Count++; + } + h16Size++; + if (h16Size > 4) { + throw new IllegalArgumentException(sm.getString("http.invalidHextet")); + } + } else if (c == ':') { + if (precedingColonsCount >= 2) { + // ::: is not allowed + throw new IllegalArgumentException(sm.getString("http.tooManyColons")); + } else { + if (precedingColonsCount == 1) { + // End of :: + if (parsedDoubleColon) { + // Only allowed one :: sequence + throw new IllegalArgumentException(sm.getString("http.tooManyDoubleColons")); + } + parsedDoubleColon = true; + // :: represents at least one h16 block + h16Count++; + } + precedingColonsCount++; + // mark if the next symbol is hex before the actual read + reader.mark(4); + } + h16Size = 0; + } else if (c == ']') { + if (precedingColonsCount == 1) { + // Can't end on a single ':' + throw new IllegalArgumentException(sm.getString("http.singleColonEnd")); + } + pos++; + break; + } else if (c == '.') { + if (h16Count == 7 || h16Count < 7 && parsedDoubleColon) { + reader.reset(); + pos -= h16Size; + pos += readHostIPv4(reader, true); + h16Count++; + break; + } else { + throw new IllegalArgumentException(sm.getString("http.invalidIpv4Location")); + } + } else { + throw new IllegalArgumentException( + sm.getString("http.illegalCharacterIpv6", Character.toString((char) c))); + } + pos++; + } while (true); + + if (h16Count > 8) { + throw new IllegalArgumentException(sm.getString("http.tooManyHextets", Integer.toString(h16Count))); + } else if (h16Count != 8 && !parsedDoubleColon) { + throw new IllegalArgumentException(sm.getString("http.tooFewHextets", Integer.toString(h16Count))); + } + + c = reader.read(); + if (c == ':') { + return validatePort(reader, pos); + } else { + if (c == -1) { + return -1; + } + throw new IllegalArgumentException(sm.getString("http.illegalAfterIpv6", Character.toString((char) c))); + } + } + + /** + * @return The position of ':' that separates the host from the port or -1 if it is not present + */ + static int readHostDomainName(Reader reader) throws IOException { + DomainParseState state = DomainParseState.NEW; + int pos = 0; + + while (state.mayContinue()) { + state = state.next(reader.read()); + pos++; + } + + if (DomainParseState.COLON == state) { + // State identifies the state of the previous character + return validatePort(reader, pos - 1); + } else { + return -1; + } + } + + + static int validatePort(Reader reader, int colonPosition) throws IOException { + // Remaining characters should be numeric ... + readLong(reader); + // ... followed by EOS + if (reader.read() == -1) { + return colonPosition; + } else { + // Invalid port + throw new IllegalArgumentException(); + } + } + + + /** + * Skips all characters until EOF or the specified target is found. Normally used to skip invalid input until the + * next separator. + */ + static SkipResult skipUntil(Reader input, int c, char target) throws IOException { + while (c != -1 && c != target) { + c = input.read(); + } + if (c == -1) { + return SkipResult.EOF; + } else { + return SkipResult.FOUND; + } + } + + + private void relax(boolean[] flags, String relaxedChars) { + if (relaxedChars != null && relaxedChars.length() > 0) { + char[] chars = relaxedChars.toCharArray(); + for (char c : chars) { + if (isRelaxable(c)) { + flags[c] = true; + IS_NOT_REQUEST_TARGET[c] = false; + } + } + } + } + + + private enum DomainParseState { + NEW(true, false, false, false, "http.invalidCharacterDomain.atStart"), + ALPHA(true, true, true, true, "http.invalidCharacterDomain.afterLetter"), + NUMERIC(true, true, true, true, "http.invalidCharacterDomain.afterNumber"), + PERIOD(true, false, false, true, "http.invalidCharacterDomain.afterPeriod"), + HYPHEN(true, true, false, false, "http.invalidCharacterDomain.afterHyphen"), + COLON(false, false, false, false, "http.invalidCharacterDomain.afterColon"), + END(false, false, false, false, "http.invalidCharacterDomain.atEnd"); + + private final boolean mayContinue; + private final boolean allowsHyphen; + private final boolean allowsPeriod; + private final boolean allowsEnd; + private final String errorMsg; + + DomainParseState(boolean mayContinue, boolean allowsHyphen, boolean allowsPeriod, boolean allowsEnd, + String errorMsg) { + this.mayContinue = mayContinue; + this.allowsHyphen = allowsHyphen; + this.allowsPeriod = allowsPeriod; + this.allowsEnd = allowsEnd; + this.errorMsg = errorMsg; + } + + public boolean mayContinue() { + return mayContinue; + } + + public DomainParseState next(int c) { + if (c == -1) { + if (allowsEnd) { + return END; + } else { + throw new IllegalArgumentException(sm.getString("http.invalidSegmentEndState", this.name())); + } + } else if (isAlpha(c)) { + return ALPHA; + } else if (isNumeric(c)) { + return NUMERIC; + } else if (c == '.') { + if (allowsPeriod) { + return PERIOD; + } else { + throw new IllegalArgumentException(sm.getString(errorMsg, Character.toString((char) c))); + } + } else if (c == ':') { + if (allowsEnd) { + return COLON; + } else { + throw new IllegalArgumentException(sm.getString(errorMsg, Character.toString((char) c))); + } + } else if (c == '-') { + if (allowsHyphen) { + return HYPHEN; + } else { + throw new IllegalArgumentException(sm.getString(errorMsg, Character.toString((char) c))); + } + } else { + throw new IllegalArgumentException( + sm.getString("http.illegalCharacterDomain", Character.toString((char) c))); + } + } + } +} diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings.properties new file mode 100644 index 0000000..325e7da --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings.properties @@ -0,0 +1,60 @@ +# 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. + +cookie.fallToDebug=\n\ +\ Note: further occurrences of this error will be logged at DEBUG level. +cookie.invalidCookieValue=A cookie header was received [{0}] that contained an invalid cookie. That cookie will be ignored. +cookie.invalidCookieVersion=A cookie header was received using an unrecognised cookie version of [{0}]. The header and the cookies it contains will be ignored. +cookie.valueNotPresent= + +http.closingBracket=A closing bracket ']' was found in a non-IPv6 host name. +http.illegalAfterIpv6=The character [{0}] is not permitted to follow an IPv6 address in a host name +http.illegalCharacterDomain=The character [{0}] is never valid in a domain name. +http.illegalCharacterIpv4=The character [{0}] is never valid in an IPv4 address. +http.illegalCharacterIpv6=The character [{0}] is never valid in an IPv6 address. +http.invalidCharacterDomain.afterColon=The character [{0}] is not valid after a colon in a domain name. +http.invalidCharacterDomain.afterHyphen=The character [{0}] is not valid after a hyphen in a domain name. +http.invalidCharacterDomain.afterLetter=The character [{0}] is not valid after a letter in a domain name. +http.invalidCharacterDomain.afterNumber=The character [{0}] is not valid after a number in a domain name. +http.invalidCharacterDomain.afterPeriod=The character [{0}] is not valid after a period in a domain name. +http.invalidCharacterDomain.atEnd=The character [{0}] is not valid at the end of a domain name. +http.invalidCharacterDomain.atStart=The character [{0}] is not valid at the start of a domain name. +http.invalidHextet=Invalid hextet. A hextet must consist of 4 or less hex characters. +http.invalidIpv4Location=The IPv6 address contains an embedded IPv4 address at an invalid location. +http.invalidLeadingZero=A non-zero IPv4 octet may not contain a leading zero. +http.invalidOctet=Invalid octet [{0}]. The valid range for IPv4 octets is 0 to 255. +http.invalidSegmentEndState=The state [{0}] is not valid for the end of a segment. +http.noClosingBracket=The IPv6 address is missing a closing bracket. +http.noOpeningBracket=The IPv6 address is missing an opening bracket. +http.singleColonEnd=An IPv6 address may not end with a single ':'. +http.singleColonStart=An IPv6 address may not start with a single ':'. +http.tooFewHextets=An IPv6 address must consist of 8 hextets but this address contains [{0}] hextets and no ''::'' sequence to represent one or more zero hextets. +http.tooManyColons=An IPv6 address may not contain more than 2 sequential colon characters. +http.tooManyDoubleColons=An IPv6 address may only contain a single '::' sequence. +http.tooManyHextets=The IPv6 address contains [{0}] hextets but a valid IPv6 address may not have more than 8. + +sf.bareitem.invalidCharacter=The invalid character [{0}] was found parsing when start of a bare item +sf.base64.invalidCharacter=The [{0}] character is not valid inside a base64 sequence +sf.boolean.invalidCharacter=The [{0}] character is not a valid boolean value +sf.invalidCharacter=The [{0}] character is not valid here +sf.key.invalidFirstCharacter=The invalid character [{0}] was found parsing when start of a key +sf.numeric.decimalInvalidFinal=The final character of a decimal value must be a digit +sf.numeric.decimalPartTooLong=More than 3 digits after the decimal point +sf.numeric.decimalTooLong=More than 16 characters found in a decimal +sf.numeric.integerTooLong=More than 15 digits found in an integer +sf.numeric.integralPartTooLong=More than 12 digits found in the integral part of a decimal +sf.numeric.invalidCharacter=The invalid character [{0}] was found parsing a numeric value where a digit was expected +sf.string.invalidCharacter=The [{0}] character is not valid inside a string +sf.string.invalidEscape=The [{0}] character must not be escaped diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_cs.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_cs.properties new file mode 100644 index 0000000..9f4e27b --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_cs.properties @@ -0,0 +1,20 @@ +# 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. + +cookie.fallToDebug=\n\ +\ Poznámka: další výskyty této chyby budou zalogovány v úrovni DEBUG. +cookie.valueNotPresent= + +http.invalidHextet=Neplatný hextet. Hextet se skládá maximálnÄ› ze 4 hexadecimálních znaků. diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_de.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_de.properties new file mode 100644 index 0000000..a53cdea --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_de.properties @@ -0,0 +1,24 @@ +# 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. + +cookie.valueNotPresent= + +http.illegalCharacterIpv4=Das Zeichen [{0}] ist in einer IPv4-Adresse niemals erlaubt. +http.illegalCharacterIpv6=Das Zeichen [{0}] ist kein valides Zeichen in einer IPv6 Addresse. +http.invalidHextet=Ungültiges Hextet. Ein Hextet muss aus 4 oder weniger Hexadecimalzeichen bestehen. +http.invalidIpv4Location=Die IPV6-Adresse enthält eine eingebettete IPv4-Adresse an einer ungültigen Position. +http.invalidOctet=Invalides Oktett [{0}]. Der gültige Bereich für IPv4 Oktette geht von 0 bis 255. +http.invalidSegmentEndState=Der Zustand [{0}] ist nicht gültig für das Ende eines Segments. +http.singleColonEnd=Eine IPv6 Adresse darf nicht mit einem ':' enden. diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_es.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_es.properties new file mode 100644 index 0000000..230cca3 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_es.properties @@ -0,0 +1,22 @@ +# 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. + +cookie.valueNotPresent= + +http.illegalCharacterIpv4=El caracter [{0}] nunca es válido en una dirección IPv4.\n +http.illegalCharacterIpv6=El caracter [{0}] nunca es válido en una dirección IPv6.\n +http.invalidHextet=Hextet no válido. Hextet debe consistir de 4 caracteres hexadecimales o menos. +http.singleColonEnd=Una dirección IPv6 no puede terminar con solo un ':'.\n +http.tooManyColons=Una dirección IPv6 no puede contener más de 2 caracteres ":" seguidos diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties new file mode 100644 index 0000000..a954343 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties @@ -0,0 +1,60 @@ +# 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. + +cookie.fallToDebug=\n\ +\ Note : les occurrences suivantes de cette erreur seront enregistrées au niveau DEBUG. +cookie.invalidCookieValue=Un en-tête de cookie a été reçu [{0}] qui contenait un cookie invalide, celui ci sera ignoré +cookie.invalidCookieVersion=Un en-tête de cookie a été reçu utilisant une version [{0}] non reconnue, les cookies seront ignorés +cookie.valueNotPresent= + +http.closingBracket=Un crochet ']' a été trouvé dans un nom d'hôte non IPv6 +http.illegalAfterIpv6=Le caractère [{0}] n''est pas permis dans un nom d''hôte à la suite d''une adresse IPv6 +http.illegalCharacterDomain=Le caractère [{0}] n''est jamais valide pour un nom de domaine +http.illegalCharacterIpv4=Le caractère [{0}] n''est pas valide pour une adresse IPV4. +http.illegalCharacterIpv6=Le caractère [{0}] n''est jamais valide dans une adresse IPv6 +http.invalidCharacterDomain.afterColon=Le caractère [{0}] n''est pas valide après deux-point pour un nom de domaine +http.invalidCharacterDomain.afterHyphen=Le caractère [{0}] n''est pas valide après un trait d''union pour un nom de domaine +http.invalidCharacterDomain.afterLetter=Le caractère [{0}] n''est pas valide après une lettre pour un nom de domaine +http.invalidCharacterDomain.afterNumber=Le caractère [{0}] n''est pas valide après un nombre pour un nom de domaine +http.invalidCharacterDomain.afterPeriod=Le caractère [{0}] n''est pas valide après une virgule pour un nom de domaine +http.invalidCharacterDomain.atEnd=Le caractère [{0}] n''est pas valide à la fin d''un nom de domaine +http.invalidCharacterDomain.atStart=Le caractère [{0}] n''est pas valide au début d''un nom de domaine +http.invalidHextet="hextet" invalide. Un "hextet" doit consister au maximum de 4 caractères hexadécimaux. +http.invalidIpv4Location=L'adresse IPv6 contient une adresse IPv4 incluse à un endroit invalide +http.invalidLeadingZero=Un octet IPv4 non nul ne doit pas commencer par un zéro +http.invalidOctet=Octet [{0}] invalide. L''éventail valide pour les octets IPv4 est 0-255. +http.invalidSegmentEndState=L''état [{0}] n''est pas valide à la fin d''un segment +http.noClosingBracket=L'adresse IPv6 n'a pas de crochet de fermeture +http.noOpeningBracket=Cette adresse IPv6 n'a pas de crochet d'ouverture '[' +http.singleColonEnd=Une adresse IPv6 ne doit pas se terminer par un seul ':' +http.singleColonStart=Une adresse IPv6 ne doit pas commencer par un seul ':' +http.tooFewHextets=Une adresse IPv6 doit être constitué de 8 groupes de 4 octets mais cette adresse en contient [{0}] et pas de séquence "::" pour représenter un ou plusieurs groupes de 4 octets +http.tooManyColons=Une adresse IPv6 ne peut pas contenir plus de deux caractères deux-points à la suite +http.tooManyDoubleColons=Une adresse IPv6 ne peut contenir qu'une seule séquence "::" +http.tooManyHextets=L''adresse IPv6 contient [{0}] groupes de 4 octets mais une adresse IPv6 valide ne doit pas en avoir plus de 8 + +sf.bareitem.invalidCharacter=Le caractère [{0}] invalide a été rencontré en début d''un objet +sf.base64.invalidCharacter=Le caractère [{0}] est invalide dans une séquence base64 +sf.boolean.invalidCharacter=Le caractère [{0}] n''est pas une valeur booléene valide +sf.invalidCharacter=Le caractère [{0}] n''est pas valide à cet endroit +sf.key.invalidFirstCharacter=Le caractère [{0}] invalide a été rencontré au début d''une clé +sf.numeric.decimalInvalidFinal=Le caractère final d'une valeur décimale doit être un chiffre +sf.numeric.decimalPartTooLong=Plus de 3 chiffres après le point des décimales +sf.numeric.decimalTooLong=Plus de 16 caractères trouvés dans une décimale +sf.numeric.integerTooLong=Plus de 15 chiffres dans un entier +sf.numeric.integralPartTooLong=Plus de 12 chiffres trouvés dans la partie entière d'une décimale +sf.numeric.invalidCharacter=Le caractère [{0}] invalide a été trouvé en traitant une valeur numérique alors qu''un chiffre était attendu +sf.string.invalidCharacter=Le caractère [{0}] n''est pas valide dans une chaîne de caractères +sf.string.invalidEscape=Le caractère [{0}] ne doit pas être échappé diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties new file mode 100644 index 0000000..b9bf412 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties @@ -0,0 +1,60 @@ +# 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. + +cookie.fallToDebug=\n\ +\ 注: 以é™ã®ã“ã®ã‚¨ãƒ©ãƒ¼ã®ç™ºç”Ÿã¯DEBUGレベルã§ãƒ­ã‚°ã«å‡ºåŠ›ã•ã‚Œã¾ã™ã€‚ +cookie.invalidCookieValue=無効ãªCookieã‚’å«ã‚€CookieヘッダーãŒå—ä¿¡ã•ã‚Œã¾ã—㟠[{0}]。 ãã®Cookieã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +cookie.invalidCookieVersion=[{0}]ã®èªè­˜ã§ããªã„クッキーãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’使用ã—ã¦ã€CookieヘッダーãŒå—ä¿¡ã•ã‚Œã¾ã—ãŸã€‚ヘッダーã¨ãã‚Œã«å«ã¾ã‚Œã‚‹ã‚¯ãƒƒã‚­ãƒ¼ã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ +cookie.valueNotPresent=<値ãŒå­˜åœ¨ã—ã¾ã›ã‚“> + +http.closingBracket=éžIPv6ホストåã«é–‰ã˜æ‹¬å¼§ ']'ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸã€‚ +http.illegalAfterIpv6=文字[{0}]ã¯ãƒ›ã‚¹ãƒˆåã®IPv6アドレスã«å¾“ã†ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +http.illegalCharacterDomain=文字 [{0}] をドメインåã«å«ã‚ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +http.illegalCharacterIpv4=文字 [{0}] ã¯æ­£å¸¸ãª IPv4 アドレスã«åˆ©ç”¨ã§ãã¾ã›ã‚“。 +http.illegalCharacterIpv6=IPv6 アドレスã«æ–‡å­— [{0}] を使用ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +http.invalidCharacterDomain.afterColon=ドメインåã®ã‚³ãƒ­ãƒ³ã®å¾Œã®æ–‡å­—[{0}]ã¯ç„¡åŠ¹ã§ã™ã€‚ +http.invalidCharacterDomain.afterHyphen=ドメインåã®ãƒã‚¤ãƒ•ãƒ³ã®å¾Œã®æ–‡å­—[{0}]ã¯ç„¡åŠ¹ã§ã™ +http.invalidCharacterDomain.afterLetter=文字 [{0}] ã¯ãƒ‰ãƒ¡ã‚¤ãƒ³åã«åˆ©ç”¨ã§ãã¾ã›ã‚“。 +http.invalidCharacterDomain.afterNumber=ドメインåã®æ•°å­—ã®å¾Œã®æ–‡å­—[{0}]ã¯ç„¡åŠ¹ã§ã™ã€‚ +http.invalidCharacterDomain.afterPeriod=ドメインåã®ãƒ”リオドã®å¾Œã®æ–‡å­—[{0}]ã¯ç„¡åŠ¹ã§ã™ã€‚ +http.invalidCharacterDomain.atEnd=文字[{0}]ã¯ãƒ‰ãƒ¡ã‚¤ãƒ³åã®æœ€å¾Œã«ã¯ç„¡åŠ¹ã§ã™ã€‚ +http.invalidCharacterDomain.atStart=文字[{0}]ã¯ãƒ‰ãƒ¡ã‚¤ãƒ³åã®å…ˆé ­ã«ã¯ç„¡åŠ¹ã§ã™ã€‚ +http.invalidHextet=無効㪠16 進数文字列ã§ã™ã€‚16 進数文字列ã«ä½¿ç”¨ã§ãã‚‹ã®ã¯ 4 文字以下㮠16 進数ã ã‘ã§ã™ã€‚ +http.invalidIpv4Location=IPv6 アドレスã¯ç„¡åŠ¹ãªä½ç½®ã«åŸ‹ã‚込㿠IPv4 アドレスをå«ã‚“ã§ã„ã¾ã™ã€‚ +http.invalidLeadingZero=IPv4 アドレス㮠0 ã§ãªã„オクテットã¯å…ˆè¡Œã™ã‚‹0ã‚’å«ã¾ãªã„ã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。 +http.invalidOctet=無効ãªã‚ªã‚¯ãƒ†ãƒƒãƒˆ [{0}]。 IPv4オクテットã®æœ‰åŠ¹ç¯„囲ã¯0〜255ã§ã™ã€‚ +http.invalidSegmentEndState=状態[{0}]ã¯ã‚»ã‚°ãƒ¡ãƒ³ãƒˆã®æœ€å¾Œã«ã¯ç„¡åŠ¹ã§ã™ã€‚ +http.noClosingBracket=IPv6アドレスã«é–‰ã˜æ‹¬å¼§ãŒã‚ã‚Šã¾ã›ã‚“。 +http.noOpeningBracket=IPv6 アドレスã«é–‹ã括弧ãŒã‚ã‚Šã¾ã›ã‚“。 +http.singleColonEnd=IPv6 アドレス文字列ã¯å˜ç‹¬ã®ã‚³ãƒ­ãƒ³ (:) ã§çµ‚端ã—ã¦ã¯ãªã‚Šã¾ã›ã‚“。 +http.singleColonStart=IPv6アドレスã¯å˜ä¸€ã® ':'ã§å§‹ã¾ã‚‰ãªã„å ´åˆãŒã‚ã‚Šã¾ã™ +http.tooFewHextets=IPv6 アドレス㯠8 個ã®ãƒ˜ã‚¯ã‚¹ãƒ†ãƒƒãƒˆã§æ§‹æˆã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“㌠[{0}] 個ã—ã‹ã‚ã‚Šã¾ã›ã‚“。ã¾ãŸï¼‘ã¤ä»¥ä¸Šã®ãƒ˜ã‚¯ã‚¹ãƒ†ãƒƒãƒˆã‚’æ„味ã™ã‚‹ "::" ã‚‚ã‚ã‚Šã¾ã›ã‚“。 +http.tooManyColons=IPv6 アドレスã§ã¯æ–‡å­— : ã‚’ 2 ã¤ä»¥ä¸Šé€£ç¶šã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +http.tooManyDoubleColons=IPv6アドレスã¯å˜ä¸€ã® '::'シーケンスã®ã¿ã‚’å«ã‚€ã“ã¨ãŒã§ãã¾ã™ã€‚ +http.tooManyHextets=IPv6 アドレス㯠[{0}] ヘクステットã§æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ãŒã€æ­£å¸¸ãª IPv6 アドレスãªã‚‰ 8 ヘクステット以上ã«ãªã‚Šã¾ã›ã‚“。 + +sf.bareitem.invalidCharacter=ベアアイテムã®é–‹å§‹ã‚’解æžä¸­ã«ç„¡åŠ¹ãªæ–‡å­— [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã—㟠+sf.base64.invalidCharacter=文字 [{0}] 㯠base64 シーケンス内ã§ã¯ç„¡åŠ¹ã§ã™ +sf.boolean.invalidCharacter=文字 [{0}] ã¯æœ‰åŠ¹ãªãƒ–ール値ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +sf.invalidCharacter=文字 [{0}] ã¯ã“ã“ã§ã¯ç„¡åŠ¹ã§ã™ +sf.key.invalidFirstCharacter=キーã®é–‹å§‹ã‚’解æžä¸­ã«ç„¡åŠ¹ãªæ–‡å­— [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã—㟠+sf.numeric.decimalInvalidFinal=10 進数値ã®æœ€å¾Œã®æ–‡å­—ã¯æ•°å­—ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“ +sf.numeric.decimalPartTooLong=å°æ•°ç‚¹ä»¥ä¸‹ãŒ 3 æ¡ä»¥ä¸Š +sf.numeric.decimalTooLong=16 æ¡ä»¥ä¸Šã® 10 進数ãŒè¦‹ã¤ã‹ã‚Šã¾ã—㟠+sf.numeric.integerTooLong=æ•´æ•°ã« 15 æ¡ã‚’超ãˆã‚‹æ•°å­—ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +sf.numeric.integralPartTooLong=10 進数ã®æ•´æ•°éƒ¨åˆ†ã« 12 æ¡ã‚’超ãˆã‚‹æ•°å­—ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ +sf.numeric.invalidCharacter=数値ã®è§£æžä¸­ã«æ•°å€¤ã§ã¯ãªã„無効ãªæ–‡å­— [{0}] ãŒæ¤œå‡ºã•ã‚Œã¾ã—㟠+sf.string.invalidCharacter=文字 [{0}] ã¯æ–‡å­—列内ã§ã¯ç„¡åŠ¹ã§ã™ +sf.string.invalidEscape=文字 [{0}] ã¯ã‚¨ã‚¹ã‚±ãƒ¼ãƒ—ã§ãã¾ã›ã‚“ diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_ko.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_ko.properties new file mode 100644 index 0000000..3ee426b --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_ko.properties @@ -0,0 +1,60 @@ +# 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. + +cookie.fallToDebug=\n\ +\ 비고: ì´ ì˜¤ë¥˜ê°€ ë” ë°œìƒí•˜ëŠ” 경우 DEBUG 레벨 로그로 기ë¡ë  것입니다. +cookie.invalidCookieValue=유효하지 ì•Šì€ ì¿ í‚¤ê°€ í¬í•¨ëœ 쿠키 í—¤ë” [{0}]ì„(를) 받았습니다. ì´ ì¿ í‚¤ëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +cookie.invalidCookieVersion=ì¸ì‹ë˜ì§€ 않는 쿠키 버전 [{0}]ì„(를) 사용한 쿠키 í—¤ë”를 받았습니다. 해당 í—¤ë”와 ê·¸ì— í¬í•¨ëœ ì¿ í‚¤ë“¤ì€ ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +cookie.valueNotPresent= + +http.closingBracket=닫는 대괄호(']')ê°€ IPv6 ì´ë¦„ì´ ì•„ë‹Œ 호스트 ì´ë¦„ì—ì„œ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. +http.illegalAfterIpv6=호스트 ì´ë¦„ ë‚´ì—ì„œ, IPv6 주소 ì´í›„ì— ë¬¸ìž [{0}]ì€(는) 허용ë˜ì§€ 않습니다. +http.illegalCharacterDomain=ë¬¸ìž [{0}]ì€(는) ë„ë©”ì¸ ì´ë¦„ ë‚´ì—ì„œ 유효하지 ì•Šì€ ë¬¸ìžìž…니다. +http.illegalCharacterIpv4=ë¬¸ìž [{0}]ì€(는) IPv4 주소ì—ì„œ 절대 유효하지 ì•Šì€ ê²ƒìž…ë‹ˆë‹¤. +http.illegalCharacterIpv6=ë¬¸ìž [{0}]ì€(는) IPv6 주소 ë‚´ì—ì„œ 유효하지 ì•Šì€ ê²ƒìž…ë‹ˆë‹¤. +http.invalidCharacterDomain.afterColon=ë„ë©”ì¸ ì´ë¦„ ë‚´ì—ì„œ, 콜론 ì´í›„ì˜ ë¬¸ìž [{0}]ì€(는) 유효하지 않습니다. +http.invalidCharacterDomain.afterHyphen=ë„ë©”ì¸ ì´ë¦„ ë‚´ì—ì„œ, 붙임표(하ì´í”ˆ) ì´í›„ì˜ ë¬¸ìž [{0}]ì€(는) 유효하지 않습니다. +http.invalidCharacterDomain.afterLetter=ë„ë©”ì¸ ì´ë¦„ ë‚´ì—ì„œ, í•œ ê¸€ìž ì´í›„ì˜ ë¬¸ìž [{0}]ì€(는) 유효하지 않습니다. +http.invalidCharacterDomain.afterNumber=ë„ë©”ì¸ ì´ë¦„ ë‚´ì—ì„œ, ìˆ«ìž ì´í›„ì˜ ë¬¸ìž [{0}]ì€(는) 유효하지 않습니다. +http.invalidCharacterDomain.afterPeriod=ë„ë©”ì¸ ì´ë¦„ ë‚´ì—ì„œ, 마침표 ì´í›„ì˜ ë¬¸ìž [{0}]ì€(는) 유효하지 않습니다. +http.invalidCharacterDomain.atEnd=ë„ë©”ì¸ ì´ë¦„ì˜ ë 위치ì—, ë¬¸ìž [{0}]ì€(는) 유효하지 않습니다. +http.invalidCharacterDomain.atStart=ë„ë©”ì¸ ì´ë¦„ì˜ ì‹œìž‘ 위치ì—, ë¬¸ìž [{0}]ì€(는) 유효하지 않습니다. +http.invalidHextet=유효하지 ì•Šì€ í—¥ìŠ¤í…Ÿ(hextet)입니다. í—¥ìŠ¤í…Ÿì€ ë°˜ë“œì‹œ 네 ê°œ ì´í•˜ì˜ 문ìžë“¤ì´ì–´ì•¼ 합니다. +http.invalidIpv4Location=IPv6 주소가, 유효하지 ì•Šì€ ìœ„ì¹˜ì— ë‚´ìž¥ IPv4 주소를 í¬í•¨í•˜ê³  있습니다. +http.invalidLeadingZero=IPv4 옥텟(octet)ì€, ê°’ì´ 0ì´ ì•„ë‹Œ ì´ìƒ, 0으로 시작해서는 안ë©ë‹ˆë‹¤. +http.invalidOctet=유효하지 ì•Šì€ ì˜¥í…Ÿ(octet) [{0}]. IPv4 ì˜¥í…Ÿì˜ ìœ íš¨í•œ 범위는 0ì—ì„œ 255까지입니다. +http.invalidSegmentEndState=ìƒíƒœ [{0}]ì€(는) segmentì˜ ë으로 유효하지 않습니다. +http.noClosingBracket=IPv6 ì£¼ì†Œì— ë‹«ëŠ” 대괄호가 없습니다. +http.noOpeningBracket=IPv6 ì£¼ì†Œì— ì—¬ëŠ” 대괄호가 없습니다. +http.singleColonEnd=IPv6 주소는 ë‹¨ì¼ ':' 문ìžë¡œ ë나서는 안ë©ë‹ˆë‹¤. +http.singleColonStart=IPv6 주소는 단ì¼ì˜ ':'으로 시작할 수 없습니다. +http.tooFewHextets=IPv6 주소는 반드시 8ê°œì˜ í—¥ìŠ¤í…Ÿ(hextet)들로 ì´ë£¨ì–´ì ¸ì•¼ 하지만, ì´ ì£¼ì†ŒëŠ” [{0}] ê°œì˜ í—¥ìŠ¤í…Ÿë“¤ìœ¼ë¡œ ì´ë£¨ì–´ì ¸ 있고, 하나 ì´ìƒì˜ 0 í—¥ìŠ¤í…Ÿë“¤ì„ í‘œì‹œí•˜ê¸° 위한 ''::'' ì‹œí€€ìŠ¤ë„ ì¡´ìž¬í•˜ì§€ 않습니다. +http.tooManyColons=IPv6 주소는 ì—°ì†ìœ¼ë¡œ ë‘ ê°œë¥¼ 초과한 콜론 문ìž('':'')ë“¤ì„ í¬í•¨í•  수 없습니다. +http.tooManyDoubleColons=IPv6 주소는 단ì¼í•œ '::' ì‹œí€€ìŠ¤ë§Œì„ í¬í•¨í•´ì•¼ 합니다. +http.tooManyHextets=IPv6 주소가 [{0}]ê°œì˜ í—¥ìŠ¤í…Ÿ(hextet)ë“¤ì„ í¬í•¨í•˜ê³  있지만, 유효한 IPv6 주소는 8개를 초과할 수 없습니다. + +sf.bareitem.invalidCharacter=단순 항목 ê°’ì„ íŒŒì‹±í•˜ëŠ” 중 시작 문ìžë¡œ 유효하지 ì•Šì€ ë¬¸ìž [{0}](ì´)ê°€ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. +sf.base64.invalidCharacter=Base64 문ìžì—´ ë‚´ì— ìœ íš¨í•˜ì§€ ì•Šì€ ë¬¸ìž [{0}]. +sf.boolean.invalidCharacter=불리언 값으로 유효하지 ì•Šì€ ë¬¸ìž [{0}]. +sf.invalidCharacter=ë¬¸ìž [{0}](ì€)는 여기서 유효하지 않습니다. +sf.key.invalidFirstCharacter=í‚¤ì˜ ì‹œìž‘ 문ìžë¡œ 유효하지 ì•Šì€ ë¬¸ìž [{0}](ì´)ê°€ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. +sf.numeric.decimalInvalidFinal=실수 ê°’ 지정 ì‹œ 맨 마지막 문ìžëŠ” 숫ìžì—¬ì•¼ 합니다. +sf.numeric.decimalPartTooLong=십진 ì†Œìˆ˜ì  ì´í›„ë¡œ ìˆ«ìž 3개를 초과했습니다. +sf.numeric.decimalTooLong=실수 ê°’ì„ ì§€ì •í•˜ëŠ” 문ìžì—´ ê°’ì— ë¬¸ìž ê°œìˆ˜ 16개를 초과했습니다. +sf.numeric.integerTooLong=정수 ë‚´ì— ìˆ«ìžê°€ 15개를 초과했습니다. +sf.numeric.integralPartTooLong=ì‹¤ìˆ˜ì˜ ì •ìˆ˜ 부분 ë‚´ì— ìˆ«ìžê°€ 12개를 초과했습니다. +sf.numeric.invalidCharacter=ìˆ«ìž ê°’ì„ íŒŒì‹±í•˜ëŠ” 중, 숫ìžê°€ 기대ë˜ëŠ” ê³³ì—ì„œ 유효하지 ì•Šì€ ë¬¸ìž [{0}](ì´)ê°€ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. +sf.string.invalidCharacter=문ìžì—´ ë‚´ì— ìœ íš¨í•˜ì§€ ì•Šì€ ë¬¸ìž [{0}]. +sf.string.invalidEscape=ë¬¸ìž [{0}](ì€)는 부호화ë˜ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..d2587fd --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +cookie.valueNotPresent= diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_ru.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_ru.properties new file mode 100644 index 0000000..4346d38 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_ru.properties @@ -0,0 +1,20 @@ +# 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. + +cookie.valueNotPresent=отÑутÑтвует + +http.illegalCharacterIpv4=Символ [{0}] никогда не ÑвлÑетÑÑ Ð²Ð°Ð»Ð¸Ð´Ð½Ñ‹Ð¼ Ð´Ð»Ñ Ð°Ð´Ñ€ÐµÑа IPv4. +http.illegalCharacterIpv6=Символ [{0}] никогда не ÑвлÑетÑÑ Ð²Ð°Ð»Ð¸Ð´Ð½Ñ‹Ð¼ Ð´Ð»Ñ Ð°Ð´Ñ€ÐµÑа IPv6. +http.tooManyColons=ÐÐ´Ñ€ÐµÑ IPv6 не может Ñодержать боле двух поÑледовательных Ñимволов двоеточиÑ. diff --git a/java/org/apache/tomcat/util/http/parser/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/http/parser/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..42a5fa1 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/LocalStrings_zh_CN.properties @@ -0,0 +1,55 @@ +# 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. + +cookie.fallToDebug=\n\ +\ 注æ„:此错误的进一步出现将记录在调试级别。 +cookie.invalidCookieValue=收到包å«æ— æ•ˆcookieçš„cookie头[{0}]。将忽略该cookie。 +cookie.invalidCookieVersion=使用无法识别的[{0}] cookie版本接收到cookie标头。标头åŠå…¶åŒ…å«çš„cookie将被忽略。 +cookie.valueNotPresent=<ä¸å­˜åœ¨> + +http.closingBracket=在éžIPv6主机å中找到了å³æ‹¬å·']'。 +http.illegalAfterIpv6=ä¸å…许字符[{0}]è·Ÿéšä¸»æœºå中的IPv6地å€ã€‚ +http.illegalCharacterDomain=字符[{0}]在域å中永远无效。 +http.illegalCharacterIpv4=字符[{0}]为éžæ³•çš„IPv4地å€ã€‚ +http.illegalCharacterIpv6=字符[{0}]为éžæ³•çš„IPv6地å€ã€‚ +http.invalidCharacterDomain.afterColon=字符 [{0}] 在域å中的冒å·åŽæ— æ•ˆã€‚ +http.invalidCharacterDomain.afterHyphen=字符 [{0}] 在域å中的连字符åŽæ— æ•ˆã€‚ +http.invalidCharacterDomain.afterLetter=字符 [{0}] 在域å中的字æ¯åŽæ— æ•ˆã€‚ +http.invalidCharacterDomain.afterNumber=字符 [{0}] 在域å中的数字åŽæ— æ•ˆã€‚ +http.invalidCharacterDomain.afterPeriod=字符 [{0}] 在域å中的å¥å·åŽæ— æ•ˆã€‚ +http.invalidCharacterDomain.atEnd=字符 [{0}] 在域å末尾无效。 +http.invalidCharacterDomain.atStart=字符 [{0}] 在域å开头无效。 +http.invalidHextet=hextet无效。 hextet必须包å«4个或更少的å六进制字符。 +http.invalidIpv4Location=IPv6地å€åœ¨æ— æ•ˆä½ç½®åŒ…å«åµŒå…¥çš„IPv4地å€ã€‚ +http.invalidLeadingZero=éžé›¶çš„IPv4字符å¯èƒ½ä¸åŒ…å«å‰å¯¼é›¶ã€‚ +http.invalidOctet=无效字符[{0}].IPv4字符的有效范围为0~255。 +http.invalidSegmentEndState=状æ€[{0}]对于段的结尾无效。 +http.noClosingBracket=ipv6 地å€ç¼ºå¤±ä¸€ä¸ªé—­åˆçš„åœ†æ‹¬å· +http.noOpeningBracket=IPv6地å€ç¼ºå°‘开括å·( +http.singleColonEnd=IPv6地å€ä¸èƒ½ä»¥å•ä¸ªâ€œ.â€ç»“尾。 +http.singleColonStart=一个IPv6地å€ä¹Ÿè®¸ä¸æ˜¯ä»¥å•ä¸ªå†’å·":"开头的。 +http.tooFewHextets=一个IPv6地å€å¿…须包å«8个16进制数,但是这个IP地å€åŒ…å«äº†[{0}]个16进制数,并且无“::â€åºåˆ—表示一个或多个0个16进制数 +http.tooManyColons=IPv6地å€ä¸èƒ½åŒ…å«è¶…过2个连续冒å·å­—符。 +http.tooManyDoubleColons=一个IPv6地å€åªèƒ½åŒ…å«ä¸€ä¸ª '::' åºåˆ—。 +http.tooManyHextets=IPv6地å€åŒ…å«[{0}]个å六进制数,但有效的IPv6地å€ä¸èƒ½è¶…过8个。 + +sf.base64.invalidCharacter=[{0}]字符在base64åºåˆ—中无效 +sf.boolean.invalidCharacter=[{0}]字符ä¸æ˜¯æœ‰æ•ˆå¸ƒå°”值 +sf.invalidCharacter=[{0}]字符在这里无效 +sf.numeric.decimalInvalidFinal=å进制最åŽä¸€ä¸ªå­—符必须是数字 +sf.numeric.decimalPartTooLong=å°æ•°ç‚¹åŽè¶…过3ä½ +sf.numeric.integerTooLong=一个整数中å‘现超过15ä½æ•°å­— +sf.string.invalidCharacter=字符串中[{0}]字符无效 +sf.string.invalidEscape=[{0}]字符ä¸èƒ½è¢«è½¬ä¹‰ diff --git a/java/org/apache/tomcat/util/http/parser/MediaType.java b/java/org/apache/tomcat/util/http/parser/MediaType.java new file mode 100644 index 0000000..f89f124 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/MediaType.java @@ -0,0 +1,172 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +public class MediaType { + + private final String type; + private final String subtype; + private final LinkedHashMap parameters; + private final String charset; + private volatile String noCharset; + private volatile String withCharset; + + protected MediaType(String type, String subtype, LinkedHashMap parameters) { + this.type = type; + this.subtype = subtype; + this.parameters = parameters; + + String cs = parameters.get("charset"); + if (cs != null && cs.length() > 0 && cs.charAt(0) == '"') { + cs = HttpParser.unquote(cs); + } + this.charset = cs; + } + + public String getType() { + return type; + } + + public String getSubtype() { + return subtype; + } + + public String getCharset() { + return charset; + } + + public int getParameterCount() { + return parameters.size(); + } + + public String getParameterValue(String parameter) { + return parameters.get(parameter.toLowerCase(Locale.ENGLISH)); + } + + @Override + public String toString() { + if (withCharset == null) { + synchronized (this) { + if (withCharset == null) { + StringBuilder result = new StringBuilder(); + result.append(type); + result.append('/'); + result.append(subtype); + for (Map.Entry entry : parameters.entrySet()) { + String value = entry.getValue(); + if (value == null || value.length() == 0) { + continue; + } + result.append(';'); + result.append(entry.getKey()); + result.append('='); + result.append(value); + } + + withCharset = result.toString(); + } + } + } + return withCharset; + } + + public String toStringNoCharset() { + if (noCharset == null) { + synchronized (this) { + if (noCharset == null) { + StringBuilder result = new StringBuilder(); + result.append(type); + result.append('/'); + result.append(subtype); + for (Map.Entry entry : parameters.entrySet()) { + if (entry.getKey().equalsIgnoreCase("charset")) { + continue; + } + result.append(';'); + result.append(entry.getKey()); + result.append('='); + result.append(entry.getValue()); + } + + noCharset = result.toString(); + } + } + } + return noCharset; + } + + /** + * Parses a MediaType value, either from an HTTP header or from an application. + * + * @param input a reader over the header text + * + * @return a MediaType parsed from the input, or null if not valid + * + * @throws IOException if there was a problem reading the input + */ + public static MediaType parseMediaType(StringReader input) throws IOException { + + // Type (required) + String type = HttpParser.readToken(input); + if (type == null || type.length() == 0) { + return null; + } + + if (HttpParser.skipConstant(input, "/") == SkipResult.NOT_FOUND) { + return null; + } + + // Subtype (required) + String subtype = HttpParser.readToken(input); + if (subtype == null || subtype.length() == 0) { + return null; + } + + LinkedHashMap parameters = new LinkedHashMap<>(); + + SkipResult lookForSemiColon = HttpParser.skipConstant(input, ";"); + if (lookForSemiColon == SkipResult.NOT_FOUND) { + return null; + } + while (lookForSemiColon == SkipResult.FOUND) { + String attribute = HttpParser.readToken(input); + + String value = ""; + if (HttpParser.skipConstant(input, "=") == SkipResult.FOUND) { + value = HttpParser.readTokenOrQuotedString(input, true); + } + + if (attribute != null) { + parameters.put(attribute.toLowerCase(Locale.ENGLISH), value); + } + + lookForSemiColon = HttpParser.skipConstant(input, ";"); + if (lookForSemiColon == SkipResult.NOT_FOUND) { + return null; + } + } + + return new MediaType(type, subtype, parameters); + } + +} diff --git a/java/org/apache/tomcat/util/http/parser/MediaTypeCache.java b/java/org/apache/tomcat/util/http/parser/MediaTypeCache.java new file mode 100644 index 0000000..5495d55 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/MediaTypeCache.java @@ -0,0 +1,64 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; + +import org.apache.tomcat.util.collections.ConcurrentCache; + +/** + * Caches the results of parsing content-type headers. + */ +public class MediaTypeCache { + + private final ConcurrentCache cache; + + public MediaTypeCache(int size) { + cache = new ConcurrentCache<>(size); + } + + /** + * Looks in the cache and returns the cached value if one is present. If no match exists in the cache, a new parser + * is created, the input parsed and the results placed in the cache and returned to the user. + * + * @param input The content-type header value to parse + * + * @return The results are provided as a two element String array. The first element is the media type less the + * charset and the second element is the charset + */ + public String[] parse(String input) { + String[] result = cache.get(input); + + if (result != null) { + return result; + } + + MediaType m = null; + try { + m = MediaType.parseMediaType(new StringReader(input)); + } catch (IOException e) { + // Ignore - return null + } + if (m != null) { + result = new String[] { m.toStringNoCharset(), m.getCharset() }; + cache.put(input, result); + } + + return result; + } +} diff --git a/java/org/apache/tomcat/util/http/parser/Priority.java b/java/org/apache/tomcat/util/http/parser/Priority.java new file mode 100644 index 0000000..ce8ec3b --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/Priority.java @@ -0,0 +1,92 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.Reader; + +import org.apache.tomcat.util.http.parser.StructuredField.SfBoolean; +import org.apache.tomcat.util.http.parser.StructuredField.SfDictionary; +import org.apache.tomcat.util.http.parser.StructuredField.SfInteger; +import org.apache.tomcat.util.http.parser.StructuredField.SfListMember; + +/** + * HTTP priority header parser as per RFC 9218. + */ +public class Priority { + + public static final int DEFAULT_URGENCY = 3; + public static final boolean DEFAULT_INCREMENTAL = false; + + // Explicitly set the defaults as per RFC 9218 + private int urgency = DEFAULT_URGENCY; + private boolean incremental = DEFAULT_INCREMENTAL; + + public Priority() { + // Default constructor is NO-OP. + } + + public int getUrgency() { + return urgency; + } + + public void setUrgency(int urgency) { + this.urgency = urgency; + } + + public boolean getIncremental() { + return incremental; + } + + public void setIncremental(boolean incremental) { + this.incremental = incremental; + } + + + /** + * Parsers an HTTP header as a Priority header as defined by RFC 9218. + * + * @param input The header to parse + * + * @return The resulting priority + * + * @throws IOException If an I/O error occurs while reading the input + */ + public static Priority parsePriority(Reader input) throws IOException { + Priority result = new Priority(); + + SfDictionary dictionary = StructuredField.parseSfDictionary(input); + + SfListMember urgencyListMember = dictionary.getDictionaryMember("u"); + // If not an integer, ignore it + if (urgencyListMember instanceof SfInteger) { + long urgency = ((SfInteger) urgencyListMember).getVaue().longValue(); + // If out of range, ignore it + if (urgency > -1 && urgency < 8) { + result.setUrgency((int) urgency); + } + } + + SfListMember incrementalListMember = dictionary.getDictionaryMember("i"); + // If not a boolean, ignore it + if (incrementalListMember instanceof SfBoolean) { + result.setIncremental(((SfBoolean) incrementalListMember).getVaue().booleanValue()); + } + + return result; + } +} diff --git a/java/org/apache/tomcat/util/http/parser/Ranges.java b/java/org/apache/tomcat/util/http/parser/Ranges.java new file mode 100644 index 0000000..d019779 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/Ranges.java @@ -0,0 +1,125 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +public class Ranges { + + private final String units; + private final List entries; + + + public Ranges(String units, List entries) { + // Units are lower case (RFC 9110, section 14.1) + if (units == null) { + this.units = null; + } else { + this.units = units.toLowerCase(Locale.ENGLISH); + } + this.entries = Collections.unmodifiableList(entries); + } + + + public List getEntries() { + return entries; + } + + public String getUnits() { + return units; + } + + + public static class Entry { + + private final long start; + private final long end; + + + public Entry(long start, long end) { + this.start = start; + this.end = end; + } + + + public long getStart() { + return start; + } + + + public long getEnd() { + return end; + } + } + + + /** + * Parses a Range header from an HTTP header. + * + * @param input a reader over the header text + * + * @return a set of ranges parsed from the input, or null if not valid + * + * @throws IOException if there was a problem reading the input + */ + public static Ranges parse(StringReader input) throws IOException { + + // Units (required) + String units = HttpParser.readToken(input); + if (units == null || units.length() == 0) { + return null; + } + + // Must be followed by '=' + if (HttpParser.skipConstant(input, "=") != SkipResult.FOUND) { + return null; + } + + // Range entries + List entries = new ArrayList<>(); + + SkipResult skipResult; + do { + long start = HttpParser.readLong(input); + // Must be followed by '-' + if (HttpParser.skipConstant(input, "-") != SkipResult.FOUND) { + return null; + } + long end = HttpParser.readLong(input); + + if (start == -1 && end == -1) { + // Invalid range + return null; + } + + entries.add(new Entry(start, end)); + + skipResult = HttpParser.skipConstant(input, ","); + if (skipResult == SkipResult.NOT_FOUND) { + // Invalid range + return null; + } + } while (skipResult == SkipResult.FOUND); + + return new Ranges(units, entries); + } +} diff --git a/java/org/apache/tomcat/util/http/parser/SkipResult.java b/java/org/apache/tomcat/util/http/parser/SkipResult.java new file mode 100644 index 0000000..7e72b92 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/SkipResult.java @@ -0,0 +1,23 @@ +/* + * 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.tomcat.util.http.parser; + +enum SkipResult { + FOUND, + NOT_FOUND, + EOF +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/http/parser/StructuredField.java b/java/org/apache/tomcat/util/http/parser/StructuredField.java new file mode 100644 index 0000000..ab31edf --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/StructuredField.java @@ -0,0 +1,598 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.res.StringManager; + +/** + * Parsing of structured fields as per RFC 8941. + *

    + * The parsing implementation is complete but not all elements are currently exposed via getters. Additional getters + * will be added as required as the use of structured fields expands. + *

    + * The serialization of structured fields has not been implemented. + */ +public class StructuredField { + + private static final StringManager sm = StringManager.getManager(StructuredField.class); + + private static final int ARRAY_SIZE = 128; + + private static final boolean[] IS_KEY_FIRST = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_KEY = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_OWS = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_BASE64 = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE]; + + static { + for (int i = 0; i < ARRAY_SIZE; i++) { + if (i == '*' || i >= 'a' && i <= 'z') { + IS_KEY_FIRST[i] = true; + IS_KEY[i] = true; + } else if (i >= '0' && i <= '9' || i == '_' || i == '-' || i == '.') { + IS_KEY[i] = true; + } + } + + for (int i = 0; i < ARRAY_SIZE; i++) { + if (i == 9 || i == ' ') { + IS_OWS[i] = true; + } + } + + for (int i = 0; i < ARRAY_SIZE; i++) { + if (i == '+' || i == '/' || i >= '0' && i <= '9' || i == '=' || i >= 'A' && i <= 'Z' || + i >= 'a' && i <= 'z') { + IS_BASE64[i] = true; + } + } + + for (int i = 0; i < ARRAY_SIZE; i++) { + if (HttpParser.isToken(i) || i == ':' || i == '/') { + IS_TOKEN[i] = true; + } + } + } + + + static SfList parseSfList(Reader input) throws IOException { + skipSP(input); + + SfList result = new SfList(); + + if (peek(input) != -1) { + while (true) { + SfListMember listMember = parseSfListMember(input); + result.addListMember(listMember); + skipOWS(input); + if (peek(input) == -1) { + break; + } + requireChar(input, ','); + skipOWS(input); + requireNotChar(input, -1); + } + } + + skipSP(input); + requireChar(input, -1); + return result; + } + + + // Item or inner list + static SfListMember parseSfListMember(Reader input) throws IOException { + SfListMember listMember; + if (peek(input) == '(') { + listMember = parseSfInnerList(input); + } else { + listMember = parseSfBareItem(input); + } + parseSfParameters(input, listMember); + return listMember; + } + + + static SfInnerList parseSfInnerList(Reader input) throws IOException { + requireChar(input, '('); + + SfInnerList innerList = new SfInnerList(); + + while (true) { + skipSP(input); + if (peek(input) == ')') { + break; + } + SfItem item = parseSfBareItem(input); + parseSfParameters(input, item); + innerList.addListItem(item); + input.mark(1); + requireChar(input, ' ', ')'); + input.reset(); + } + requireChar(input, ')'); + + return innerList; + } + + + static SfDictionary parseSfDictionary(Reader input) throws IOException { + skipSP(input); + + SfDictionary result = new SfDictionary(); + + if (peek(input) != -1) { + while (true) { + String key = parseSfKey(input); + SfListMember listMember; + input.mark(1); + int c = input.read(); + if (c == '=') { + listMember = parseSfListMember(input); + } else { + listMember = new SfBoolean(true); + input.reset(); + } + parseSfParameters(input, listMember); + result.addDictionaryMember(key, listMember); + skipOWS(input); + if (peek(input) == -1) { + break; + } + requireChar(input, ','); + skipOWS(input); + requireNotChar(input, -1); + } + } + + skipSP(input); + requireChar(input, -1); + return result; + } + + + static SfItem parseSfItem(Reader input) throws IOException { + skipSP(input); + + SfItem item = parseSfBareItem(input); + parseSfParameters(input, item); + + skipSP(input); + requireChar(input, -1); + return item; + } + + + static SfItem parseSfBareItem(Reader input) throws IOException { + int c = input.read(); + + SfItem item; + if (c == '-' || HttpParser.isNumeric(c)) { + item = parseSfNumeric(input, c); + } else if (c == '\"') { + item = parseSfString(input); + } else if (c == '*' || HttpParser.isAlpha(c)) { + item = parseSfToken(input, c); + } else if (c == ':') { + item = parseSfByteSequence(input); + } else if (c == '?') { + item = parseSfBoolean(input); + } else { + throw new IllegalArgumentException( + sm.getString("sf.bareitem.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c)))); + } + + return item; + } + + + static void parseSfParameters(Reader input, SfListMember listMember) throws IOException { + while (true) { + if (peek(input) != ';') { + break; + } + requireChar(input, ';'); + skipSP(input); + String key = parseSfKey(input); + SfItem item; + input.mark(1); + int c = input.read(); + if (c == '=') { + item = parseSfBareItem(input); + } else { + item = new SfBoolean(true); + input.reset(); + } + listMember.addParameter(key, item); + } + } + + + static String parseSfKey(Reader input) throws IOException { + StringBuilder result = new StringBuilder(); + + input.mark(1); + int c = input.read(); + if (!isKeyFirst(c)) { + throw new IllegalArgumentException( + sm.getString("sf.key.invalidFirstCharacter", String.format("\\u%40X", Integer.valueOf(c)))); + } + + while (c != -1 && isKey(c)) { + result.append((char) c); + input.mark(1); + c = input.read(); + } + input.reset(); + return result.toString(); + } + + + static SfItem parseSfNumeric(Reader input, int first) throws IOException { + int sign = 1; + boolean integer = true; + int decimalPos = 0; + + StringBuilder result = new StringBuilder(); + + int c; + if (first == '-') { + sign = -1; + c = input.read(); + } else { + c = first; + } + + if (!HttpParser.isNumeric(c)) { + throw new IllegalArgumentException( + sm.getString("sf.numeric.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c)))); + } + result.append((char) c); + input.mark(1); + c = input.read(); + + while (c != -1) { + if (HttpParser.isNumeric(c)) { + result.append((char) c); + } else if (integer && c == '.') { + if (result.length() > 12) { + throw new IllegalArgumentException(sm.getString("sf.numeric.integralPartTooLong")); + } + integer = false; + result.append((char) c); + decimalPos = result.length(); + } else { + input.reset(); + break; + } + if (integer && result.length() > 15) { + throw new IllegalArgumentException(sm.getString("sf.numeric.integerTooLong")); + } + if (!integer && result.length() > 16) { + throw new IllegalArgumentException(sm.getString("sf.numeric.decimalTooLong")); + } + input.mark(1); + c = input.read(); + } + + if (integer) { + return new SfInteger(Long.parseLong(result.toString()) * sign); + } + + if (result.charAt(result.length() - 1) == '.') { + throw new IllegalArgumentException(sm.getString("sf.numeric.decimalInvalidFinal")); + } + + if (result.length() - decimalPos > 3) { + throw new IllegalArgumentException(sm.getString("sf.numeric.decimalPartTooLong")); + } + + return new SfDecimal(Double.parseDouble(result.toString()) * sign); + } + + + static SfString parseSfString(Reader input) throws IOException { + // It is known first character was '"' + StringBuilder result = new StringBuilder(); + + while (true) { + int c = input.read(); + if (c == '\\') { + requireNotChar(input, -1); + c = input.read(); + if (c != '\\' && c != '\"') { + throw new IllegalArgumentException( + sm.getString("sf.string.invalidEscape", String.format("\\u%40X", Integer.valueOf(c)))); + } + } else { + if (c == '\"') { + break; + } + // This test also covers unexpected EOF + if (c < 32 || c > 126) { + throw new IllegalArgumentException( + sm.getString("sf.string.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c)))); + } + } + result.append((char) c); + } + + return new SfString(result.toString()); + } + + + static SfToken parseSfToken(Reader input, int first) throws IOException { + // It is known first character is valid + StringBuilder result = new StringBuilder(); + + result.append((char) first); + while (true) { + input.mark(1); + int c = input.read(); + if (!isToken(c)) { + input.reset(); + break; + } + result.append((char) c); + } + + return new SfToken(result.toString()); + } + + + static SfByteSequence parseSfByteSequence(Reader input) throws IOException { + // It is known first character was ':' + StringBuilder base64 = new StringBuilder(); + + while (true) { + int c = input.read(); + + if (c == ':') { + break; + } else if (isBase64(c)) { + base64.append((char) c); + } else { + throw new IllegalArgumentException( + sm.getString("sf.base64.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c)))); + } + } + + return new SfByteSequence(Base64.decodeBase64(base64.toString())); + } + + + static SfBoolean parseSfBoolean(Reader input) throws IOException { + // It is known first character was '?' + int c = input.read(); + + if (c == '1') { + return new SfBoolean(true); + } else if (c == '0') { + return new SfBoolean(false); + } else { + throw new IllegalArgumentException( + sm.getString("sf.boolean.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c)))); + } + } + + + static void skipSP(Reader input) throws IOException { + input.mark(1); + int c = input.read(); + while (c == 32) { + input.mark(1); + c = input.read(); + } + input.reset(); + } + + + static void skipOWS(Reader input) throws IOException { + input.mark(1); + int c = input.read(); + while (isOws(c)) { + input.mark(1); + c = input.read(); + } + input.reset(); + } + + + static void requireChar(Reader input, int... required) throws IOException { + int c = input.read(); + for (int r : required) { + if (c == r) { + return; + } + } + throw new IllegalArgumentException( + sm.getString("sf.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c)))); + } + + + static void requireNotChar(Reader input, int required) throws IOException { + input.mark(1); + int c = input.read(); + if (c == required) { + throw new IllegalArgumentException( + sm.getString("sf.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c)))); + } + input.reset(); + } + + + static int peek(Reader input) throws IOException { + input.mark(1); + int c = input.read(); + input.reset(); + return c; + } + + + static boolean isKeyFirst(int c) { + try { + return IS_KEY_FIRST[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + static boolean isKey(int c) { + try { + return IS_KEY[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + static boolean isOws(int c) { + try { + return IS_OWS[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + static boolean isBase64(int c) { + try { + return IS_BASE64[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + static boolean isToken(int c) { + try { + return IS_TOKEN[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + private StructuredField() { + // Utility class. Hide default constructor. + } + + + static class SfDictionary { + private Map dictionary = new LinkedHashMap<>(); + + void addDictionaryMember(String key, SfListMember value) { + dictionary.put(key, value); + } + + SfListMember getDictionaryMember(String key) { + return dictionary.get(key); + } + } + + static class SfList { + private List listMembers = new ArrayList<>(); + + void addListMember(SfListMember listMember) { + listMembers.add(listMember); + } + } + + static class SfListMember { + private Map> parameters = null; + + void addParameter(String key, SfItem value) { + if (parameters == null) { + parameters = new LinkedHashMap<>(); + } + parameters.put(key, value); + } + } + + static class SfInnerList extends SfListMember { + List> listItems = new ArrayList<>(); + + SfInnerList() { + // Default constructor is NO-OP + } + + void addListItem(SfItem item) { + listItems.add(item); + } + + List> getListItem() { + return listItems; + } + } + + abstract static class SfItem extends SfListMember { + private final T value; + + SfItem(T value) { + this.value = value; + } + + T getVaue() { + return value; + } + } + + static class SfInteger extends SfItem { + SfInteger(long value) { + super(Long.valueOf(value)); + } + } + + static class SfDecimal extends SfItem { + SfDecimal(double value) { + super(Double.valueOf(value)); + } + } + + static class SfString extends SfItem { + SfString(String value) { + super(value); + } + } + + static class SfToken extends SfItem { + SfToken(String value) { + super(value); + } + } + + static class SfByteSequence extends SfItem { + SfByteSequence(byte[] value) { + super(value); + } + } + + static class SfBoolean extends SfItem { + SfBoolean(boolean value) { + super(Boolean.valueOf(value)); + } + } +} diff --git a/java/org/apache/tomcat/util/http/parser/TokenList.java b/java/org/apache/tomcat/util/http/parser/TokenList.java new file mode 100644 index 0000000..d071044 --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/TokenList.java @@ -0,0 +1,113 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Locale; + +public class TokenList { + + private TokenList() { + // Utility class. Hide default constructor. + } + + + /** + * Parses an enumeration of header values of the form 1#token, forcing all parsed values to lower case. + * + * @param inputs The headers to parse + * @param collection The Collection (usually a list or a set) to which the parsed tokens should be added + * + * @return {@code true} if the header values were parsed cleanly and at least one token was found, otherwise + * {@code false} (e.g. if a non-token value was encountered) + * + * @throws IOException If an I/O error occurs reading the header + */ + public static boolean parseTokenList(Enumeration inputs, Collection collection) throws IOException { + boolean result = true; + while (inputs.hasMoreElements()) { + String nextHeaderValue = inputs.nextElement(); + if (nextHeaderValue != null) { + if (!parseTokenList(new StringReader(nextHeaderValue), collection)) { + result = false; + } + } + } + return result; + } + + + /** + * Parses a header of the form 1#token, forcing all parsed values to lower case. + * + * @param input The header to parse + * @param collection The Collection (usually a list or a set) to which the parsed tokens should be added + * + * @return {@code true} if the header values were parsed cleanly and at least one token was found, otherwise + * {@code false} (e.g. if a non-token value was encountered) + * + * @throws IOException If an I/O error occurs reading the header + */ + public static boolean parseTokenList(Reader input, Collection collection) throws IOException { + boolean invalid = false; + boolean valid = false; + + do { + String element = HttpParser.readToken(input); + if (element == null) { + // No token found. Could be empty element (which is OK for + // 1#token - see RFC 7230 section 7) or a non-token. + if (HttpParser.skipConstant(input, ",") != SkipResult.FOUND) { + // Non-token element, skip to the next one + invalid = true; + HttpParser.skipUntil(input, 0, ','); + } + continue; + } + + if (element.length() == 0) { + // EOF after empty element + break; + } + + SkipResult skipResult = HttpParser.skipConstant(input, ","); + if (skipResult == SkipResult.EOF) { + // EOF + valid = true; + collection.add(element.toLowerCase(Locale.ENGLISH)); + break; + } else if (skipResult == SkipResult.FOUND) { + valid = true; + collection.add(element.toLowerCase(Locale.ENGLISH)); + continue; + } else { + // Not a token - ignore it + invalid = true; + HttpParser.skipUntil(input, 0, ','); + continue; + } + } while (true); + + // Only return true if at least one valid token was read and no invalid + // elements were found + return valid && !invalid; + } +} diff --git a/java/org/apache/tomcat/util/http/parser/Upgrade.java b/java/org/apache/tomcat/util/http/parser/Upgrade.java new file mode 100644 index 0000000..60c754a --- /dev/null +++ b/java/org/apache/tomcat/util/http/parser/Upgrade.java @@ -0,0 +1,106 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +public class Upgrade { + + private final String protocolName; + private final String protocolVersion; + + + private Upgrade(String protocolName, String protocolVersion) { + this.protocolName = protocolName; + this.protocolVersion = protocolVersion; + } + + + public String getProtocolName() { + return protocolName; + } + + + public String getProtocolVersion() { + return protocolVersion; + } + + + @Override + public String toString() { + if (protocolVersion == null) { + return protocolName; + } else { + return protocolName + "/" + protocolVersion; + } + } + + + public static List parse(Enumeration headerValues) { + try { + List result = new ArrayList<>(); + + while (headerValues.hasMoreElements()) { + String headerValue = headerValues.nextElement(); + if (headerValue == null) { + // Invalid + return null; + } + + Reader r = new StringReader(headerValue); + SkipResult skipComma; + do { + // Skip any leading LWS + HttpParser.skipLws(r); + String protocolName = HttpParser.readToken(r); + if (protocolName == null || protocolName.isEmpty()) { + // Invalid + return null; + } + String protocolVersion = null; + if (HttpParser.skipConstant(r, "/") == SkipResult.FOUND) { + protocolVersion = HttpParser.readToken(r); + if (protocolVersion == null || protocolVersion.isEmpty()) { + // Invalid + return null; + } + } + HttpParser.skipLws(r); + + skipComma = HttpParser.skipConstant(r, ","); + if (skipComma == SkipResult.NOT_FOUND) { + // Invalid + return null; + } + + result.add(new Upgrade(protocolName, protocolVersion)); + // SkipResult.EOF will exit this inner loop + } while (skipComma == SkipResult.FOUND); + } + + return result; + } catch (IOException ioe) { + // Should never happen with Strings + return null; + } + } +} diff --git a/java/org/apache/tomcat/util/json/JSONFilter.java b/java/org/apache/tomcat/util/json/JSONFilter.java new file mode 100644 index 0000000..1768674 --- /dev/null +++ b/java/org/apache/tomcat/util/json/JSONFilter.java @@ -0,0 +1,140 @@ +/* + * 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.tomcat.util.json; + +/** + * Provides escaping of values so they can be included in a JSON document. + * Escaping is based on the definition of JSON found in + * RFC 8259. + */ +public class JSONFilter { + + /** + * Escape the given char. + * @param c the char + * @return a char array with the escaped sequence + */ + public static char[] escape(char c) { + if (c < 0x20 || c == 0x22 || c == 0x5c || Character.isHighSurrogate(c) || Character.isLowSurrogate(c)) { + char popular = getPopularChar(c); + if (popular > 0) { + return new char[] { '\\', popular }; + } else { + StringBuilder escaped = new StringBuilder(6); + escaped.append("\\u"); + escaped.append(String.format("%04X", Integer.valueOf(c))); + return escaped.toString().toCharArray(); + } + } else { + char[] result = new char[1]; + result[0] = c; + return result; + } + } + + /** + * Escape the given string. + * @param input the string + * @return the escaped string + */ + public static String escape(String input) { + return escape(input, 0, input.length()).toString(); + } + + /** + * Escape the given char sequence. + * @param input the char sequence + * @return the escaped char sequence + */ + public static CharSequence escape(CharSequence input) { + return escape(input, 0, input.length()); + } + + /** + * Escape the given char sequence. + * @param input the char sequence + * @param off the offset on which escaping will start + * @param length the length which should be escaped + * @return the escaped char sequence corresponding to the specified range + */ + public static CharSequence escape(CharSequence input, int off, int length) { + /* + * While any character MAY be escaped, only U+0000 to U+001F (control + * characters), U+0022 (quotation mark) and U+005C (reverse solidus) + * MUST be escaped. + */ + StringBuilder escaped = null; + int lastUnescapedStart = off; + for (int i = off; i < length; i++) { + char c = input.charAt(i); + if (c < 0x20 || c == 0x22 || c == 0x5c || Character.isHighSurrogate(c) || Character.isLowSurrogate(c)) { + if (escaped == null) { + escaped = new StringBuilder(length + 20); + } + if (lastUnescapedStart < i) { + escaped.append(input.subSequence(lastUnescapedStart, i)); + } + lastUnescapedStart = i + 1; + char popular = getPopularChar(c); + if (popular > 0) { + escaped.append('\\').append(popular); + } else { + escaped.append("\\u"); + escaped.append(String.format("%04X", Integer.valueOf(c))); + } + } + } + if (escaped == null) { + if (off == 0 && length == input.length()) { + return input; + } else { + return input.subSequence(off, length - off); + } + } else { + if (lastUnescapedStart < length) { + escaped.append(input.subSequence(lastUnescapedStart, length)); + } + return escaped.toString(); + } + } + + private JSONFilter() { + // Utility class. Hide the default constructor. + } + + private static char getPopularChar(char c) { + switch (c) { + case '"': + case '\\': + case '/': + return c; + case 0x8: + return 'b'; + case 0xc: + return 'f'; + case 0xa: + return 'n'; + case 0xd: + return 'r'; + case 0x9: + return 't'; + default: + return 0; + } + } + +} diff --git a/java/org/apache/tomcat/util/json/JSONParser.java b/java/org/apache/tomcat/util/json/JSONParser.java new file mode 100644 index 0000000..0842017 --- /dev/null +++ b/java/org/apache/tomcat/util/json/JSONParser.java @@ -0,0 +1,655 @@ +/* JSONParser.java */ +/* Generated By:JavaCC: Do not edit this line. JSONParser.java */ +/* + * 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.tomcat.util.json; + +/** + * Basic JSON parser generated by JavaCC. It consumes the input provided through the constructor when + * {@code parseObject()}, {@code parseList()}, or {@code parse()} are called, and there is no way to directly + * reset the state. + */ +@SuppressWarnings("all") // Ignore warnings in generated code +public class JSONParser implements JSONParserConstants { + + private boolean nativeNumbers = false; + + public JSONParser(String input) { + this(new java.io.StringReader(input)); + } + + /** + * Parses a JSON object into a Java {@code Map}. + */ + public java.util.LinkedHashMap parseObject() throws ParseException { + java.util.LinkedHashMap toReturn = object(); + if (!ensureEOF()) { + throw new IllegalStateException("Expected EOF, but still had content to parse"); + } + return toReturn; + } + + /** + * Parses a JSON array into a Java {@code List}. + */ + public java.util.ArrayList parseArray() throws ParseException { + java.util.ArrayList toReturn = list(); + if (!ensureEOF()) { + throw new IllegalStateException("Expected EOF, but still had content to parse"); + } + return toReturn; + } + + /** + * Parses any JSON-parseable object, returning the value. + */ + public Object parse() throws ParseException { + Object toReturn = anything(); + if (!ensureEOF()) { + throw new IllegalStateException("Expected EOF, but still had content to parse"); + } + return toReturn; + } + + private static String substringBefore(String str, char delim) { + int pos = str.indexOf(delim); + if (pos == -1) { + return str; + } + return str.substring(0, pos); + } + + public void setNativeNumbers(boolean value) { + this.nativeNumbers = value; + } + + public boolean getNativeNumbers() { + return this.nativeNumbers; + } + + final public boolean ensureEOF() throws ParseException { + jj_consume_token(0); +{if ("" != null) { + return true; +}} + throw new Error("Missing return statement in function"); +} + + final public Object anything() throws ParseException {Object x; + switch (jj_nt.kind) { + case BRACE_OPEN:{ + x = object(); + break; + } + case BRACKET_OPEN:{ + x = list(); + break; + } + case NUMBER_INTEGER: + case NUMBER_DECIMAL: + case TRUE: + case FALSE: + case NULL: + case STRING_SINGLE_EMPTY: + case STRING_DOUBLE_EMPTY: + case STRING_SINGLE_NONEMPTY: + case STRING_DOUBLE_NONEMPTY:{ + x = value(); + break; + } + default: + jj_la1[0] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +{if ("" != null) { + return x; +}} + throw new Error("Missing return statement in function"); +} + + final public String objectKey() throws ParseException {Object o; + String key; + switch (jj_nt.kind) { + case STRING_SINGLE_EMPTY: + case STRING_DOUBLE_EMPTY: + case STRING_SINGLE_NONEMPTY: + case STRING_DOUBLE_NONEMPTY:{ + key = string(); + break; + } + case SYMBOL:{ + key = symbol(); + break; + } + case NULL:{ + nullValue(); +key = null; + break; + } + case NUMBER_INTEGER: + case NUMBER_DECIMAL: + case TRUE: + case FALSE:{ + switch (jj_nt.kind) { + case TRUE: + case FALSE:{ + o = booleanValue(); + break; + } + case NUMBER_INTEGER: + case NUMBER_DECIMAL:{ + o = number(); + break; + } + default: + jj_la1[1] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +key = o.toString(); + break; + } + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +{if ("" != null) { + return key; +}} + throw new Error("Missing return statement in function"); +} + + final public java.util.LinkedHashMap object() throws ParseException {final java.util.LinkedHashMap map = new java.util.LinkedHashMap(); + String key; + Object value; + jj_consume_token(BRACE_OPEN); + switch (jj_nt.kind) { + case NUMBER_INTEGER: + case NUMBER_DECIMAL: + case TRUE: + case FALSE: + case NULL: + case STRING_SINGLE_EMPTY: + case STRING_DOUBLE_EMPTY: + case STRING_SINGLE_NONEMPTY: + case STRING_DOUBLE_NONEMPTY: + case SYMBOL:{ + key = objectKey(); + jj_consume_token(COLON); + value = anything(); +map.put(key, value); +key = null; value = null; + label_1: + while (true) { + switch (jj_nt.kind) { + case COMMA:{ + ; + break; + } + default: + jj_la1[3] = jj_gen; + break label_1; + } + jj_consume_token(COMMA); + key = objectKey(); + jj_consume_token(COLON); + value = anything(); +map.put(key, value); +key = null; value = null; + } + break; + } + default: + jj_la1[4] = jj_gen; + ; + } + jj_consume_token(BRACE_CLOSE); +{if ("" != null) { + return map; +}} + throw new Error("Missing return statement in function"); +} + + final public java.util.ArrayList list() throws ParseException {final java.util.ArrayList list = new java.util.ArrayList(); + Object value; + jj_consume_token(BRACKET_OPEN); + switch (jj_nt.kind) { + case BRACE_OPEN: + case BRACKET_OPEN: + case NUMBER_INTEGER: + case NUMBER_DECIMAL: + case TRUE: + case FALSE: + case NULL: + case STRING_SINGLE_EMPTY: + case STRING_DOUBLE_EMPTY: + case STRING_SINGLE_NONEMPTY: + case STRING_DOUBLE_NONEMPTY:{ + value = anything(); +list.add(value); +value = null; + label_2: + while (true) { + switch (jj_nt.kind) { + case COMMA:{ + ; + break; + } + default: + jj_la1[5] = jj_gen; + break label_2; + } + jj_consume_token(COMMA); + value = anything(); +list.add(value); +value = null; + } + break; + } + default: + jj_la1[6] = jj_gen; + ; + } + jj_consume_token(BRACKET_CLOSE); +list.trimToSize(); + {if ("" != null) { + return list; + }} + throw new Error("Missing return statement in function"); +} + + final public Object value() throws ParseException {Object x; + switch (jj_nt.kind) { + case STRING_SINGLE_EMPTY: + case STRING_DOUBLE_EMPTY: + case STRING_SINGLE_NONEMPTY: + case STRING_DOUBLE_NONEMPTY:{ + x = string(); + break; + } + case NUMBER_INTEGER: + case NUMBER_DECIMAL:{ + x = number(); + break; + } + case TRUE: + case FALSE:{ + x = booleanValue(); + break; + } + case NULL:{ + x = nullValue(); + break; + } + default: + jj_la1[7] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +{if ("" != null) { + return x; +}} + throw new Error("Missing return statement in function"); +} + + final public Object nullValue() throws ParseException { + jj_consume_token(NULL); +{if ("" != null) { + return null; +}} + throw new Error("Missing return statement in function"); +} + + final public Boolean booleanValue() throws ParseException {Boolean b; + switch (jj_nt.kind) { + case TRUE:{ + jj_consume_token(TRUE); +b = Boolean.TRUE; + break; + } + case FALSE:{ + jj_consume_token(FALSE); +b = Boolean.FALSE; + break; + } + default: + jj_la1[8] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +{if ("" != null) { + return b; +}} + throw new Error("Missing return statement in function"); +} + + final public Number number() throws ParseException {Token t; + switch (jj_nt.kind) { + case NUMBER_DECIMAL:{ + t = jj_consume_token(NUMBER_DECIMAL); +if (nativeNumbers) { + {if ("" != null) { + return Long.valueOf(t.image); + }} + } else { + {if ("" != null) { + return new java.math.BigDecimal(t.image); + }} + } + break; + } + case NUMBER_INTEGER:{ + t = jj_consume_token(NUMBER_INTEGER); +if (nativeNumbers) { + {if ("" != null) { + return Double.valueOf(t.image); + }} + } else { + {if ("" != null) { + return new java.math.BigInteger(substringBefore(t.image, '.')); + }} + } + break; + } + default: + jj_la1[9] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + throw new Error("Missing return statement in function"); +} + + final public String string() throws ParseException {String s; + switch (jj_nt.kind) { + case STRING_DOUBLE_EMPTY: + case STRING_DOUBLE_NONEMPTY:{ + s = doubleQuoteString(); + break; + } + case STRING_SINGLE_EMPTY: + case STRING_SINGLE_NONEMPTY:{ + s = singleQuoteString(); + break; + } + default: + jj_la1[10] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } +{if ("" != null) { + return s; +}} + throw new Error("Missing return statement in function"); +} + + final public String doubleQuoteString() throws ParseException { + switch (jj_nt.kind) { + case STRING_DOUBLE_EMPTY:{ + jj_consume_token(STRING_DOUBLE_EMPTY); +{if ("" != null) { + return ""; +}} + break; + } + case STRING_DOUBLE_NONEMPTY:{ + jj_consume_token(STRING_DOUBLE_NONEMPTY); +String image = token.image; + {if ("" != null) { + return image.substring(1, image.length() - 1); + }} + break; + } + default: + jj_la1[11] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + throw new Error("Missing return statement in function"); +} + + final public String singleQuoteString() throws ParseException { + switch (jj_nt.kind) { + case STRING_SINGLE_EMPTY:{ + jj_consume_token(STRING_SINGLE_EMPTY); +{if ("" != null) { + return ""; +}} + break; + } + case STRING_SINGLE_NONEMPTY:{ + jj_consume_token(STRING_SINGLE_NONEMPTY); +String image = token.image; + {if ("" != null) { + return image.substring(1, image.length() - 1); + }} + break; + } + default: + jj_la1[12] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + throw new Error("Missing return statement in function"); +} + + final public String symbol() throws ParseException { + jj_consume_token(SYMBOL); +{if ("" != null) { + return token.image; +}} + throw new Error("Missing return statement in function"); +} + + /** Generated Token Manager. */ + public JSONParserTokenManager token_source; + JavaCharStream jj_input_stream; + /** Current token. */ + public Token token; + /** Next token. */ + public Token jj_nt; + private int jj_gen; + final private int[] jj_la1 = new int[13]; + static private int[] jj_la1_0; + static { + jj_la1_init_0(); + } + private static void jj_la1_init_0() { + jj_la1_0 = new int[] {0xccf8480,0x78000,0x1ccf8000,0x40,0x1ccf8000,0x40,0xccf8480,0xccf8000,0x60000,0x18000,0xcc00000,0x8800000,0x4400000,}; + } + + /** Constructor with InputStream. */ + public JSONParser(java.io.InputStream stream) { + this(stream, null); + } + /** Constructor with InputStream and supplied encoding */ + public JSONParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new JavaCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new JSONParserTokenManager(jj_input_stream); + token = new Token(); + token.next = jj_nt = token_source.getNextToken(); + jj_gen = 0; + for (int i = 0; i < 13; i++) { + jj_la1[i] = -1; + } + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + /** Reinitialise. */ + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + token.next = jj_nt = token_source.getNextToken(); + jj_gen = 0; + for (int i = 0; i < 13; i++) { + jj_la1[i] = -1; + } + } + + /** Constructor. */ + public JSONParser(java.io.Reader stream) { + jj_input_stream = new JavaCharStream(stream, 1, 1); + token_source = new JSONParserTokenManager(jj_input_stream); + token = new Token(); + token.next = jj_nt = token_source.getNextToken(); + jj_gen = 0; + for (int i = 0; i < 13; i++) { + jj_la1[i] = -1; + } + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader stream) { + if (jj_input_stream == null) { + jj_input_stream = new JavaCharStream(stream, 1, 1); + } else { + jj_input_stream.ReInit(stream, 1, 1); + } + if (token_source == null) { + token_source = new JSONParserTokenManager(jj_input_stream); + } + + token_source.ReInit(jj_input_stream); + token = new Token(); + token.next = jj_nt = token_source.getNextToken(); + jj_gen = 0; + for (int i = 0; i < 13; i++) { + jj_la1[i] = -1; + } + } + + /** Constructor with generated Token Manager. */ + public JSONParser(JSONParserTokenManager tm) { + token_source = tm; + token = new Token(); + token.next = jj_nt = token_source.getNextToken(); + jj_gen = 0; + for (int i = 0; i < 13; i++) { + jj_la1[i] = -1; + } + } + + /** Reinitialise. */ + public void ReInit(JSONParserTokenManager tm) { + token_source = tm; + token = new Token(); + token.next = jj_nt = token_source.getNextToken(); + jj_gen = 0; + for (int i = 0; i < 13; i++) { + jj_la1[i] = -1; + } + } + + private Token jj_consume_token(int kind) throws ParseException { + Token oldToken = token; + if ((token = jj_nt).next != null) { + jj_nt = jj_nt.next; + } else { + jj_nt = jj_nt.next = token_source.getNextToken(); + } + if (token.kind == kind) { + jj_gen++; + return token; + } + jj_nt = token; + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + +/** Get the next Token. */ + final public Token getNextToken() { + if ((token = jj_nt).next != null) { + jj_nt = jj_nt.next; + } else { + jj_nt = jj_nt.next = token_source.getNextToken(); + } + jj_gen++; + return token; + } + +/** Get the specific Token. */ + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) { + t = t.next; + } else { + t = t.next = token_source.getNextToken(); + } + } + return t; + } + + private java.util.List jj_expentries = new java.util.ArrayList(); + private int[] jj_expentry; + private int jj_kind = -1; + + /** Generate ParseException. */ + public ParseException generateParseException() { + jj_expentries.clear(); + boolean[] la1tokens = new boolean[29]; + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 13; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1< parseObject() throws ParseException { + java.util.LinkedHashMap toReturn = object(); + if (!ensureEOF()) { + throw new IllegalStateException(sm.getString("parser.expectedEOF")); + } + return toReturn; + } + + /** + * Parses a JSON array into a Java {@code List}. + */ + public java.util.ArrayList parseArray() throws ParseException { + java.util.ArrayList toReturn = list(); + if (!ensureEOF()) { + throw new IllegalStateException(sm.getString("parser.expectedEOF")); + } + return toReturn; + } + + /** + * Parses any JSON-parsable object, returning the value. + */ + public Object parse() throws ParseException { + Object toReturn = anything(); + if (!ensureEOF()) { + throw new IllegalStateException(sm.getString("parser.expectedEOF")); + } + return toReturn; + } + + private static String substringBefore(String str, char delim) { + int pos = str.indexOf(delim); + if (pos == -1) { + return str; + } + return str.substring(0, pos); + } + + public void setNativeNumbers(boolean value) { + this.nativeNumbers = value; + } + + public boolean getNativeNumbers() { + return this.nativeNumbers; + } + +} + +PARSER_END(JSONParser) + +// Ignore comments +SKIP: { + > +| +| > +| +| +} + +// Common tokens +TOKEN: { + +} + +// Object tokens +TOKEN:{ + +| +| +} + +// Array tokens +TOKEN:{ + +| +} + +// Number token +TOKEN:{ + <#ZERO: "0"> +| <#DIGIT_NONZERO: ["1"-"9"]> +| <#DIGIT: ( | ) > +| )+ | ( ()* ) ) + > +| )+ | ( ()* ) ) + ("." + ()+ + ( + ["e","E"] + ("+" | "-")? + ()+ + )? + ) + > +} + +// Boolean tokens +TOKEN:{ + +| +} + +// Null token +TOKEN:{ + +} + +// String tokens +TOKEN:{ + <#QUOTE_DOUBLE: "\""> +| <#QUOTE_SINGLE: "'"> +| +| +| <#STRING_SINGLE_BODY: ( + (~["'","\\","\r","\n","\f","\t"]) | + ( "\\" ( "r" | "n" | "f" | "\\" | "/" | "'" | "b" | "t" ) ) + )+> +| <#STRING_DOUBLE_BODY: ( + (~["\"","\\","\r","\n","\f","\t"]) | + ( "\\" ( "r" | "n" | "f" | "\\" | "/" | "\"" | "b" | "t" ) ) + )+> +| > +| > +} + +// Raw symbol tokens +TOKEN:{ + +} + + +boolean ensureEOF() : {}{ + + { return true; } +} + +Object anything() : { + Object x; +}{ + ( x = object() + | x = list() + | x = value() + ) + { return x; } +} + +String objectKey() : { + Object o; + String key; +} { + ( + key = string() + | key = symbol() + | ( + nullValue() + { key = null; } + ) + | ( + ( o = booleanValue() | o = number() ) + { key = o.toString(); } + ) + ) + { return key; } +} + +java.util.LinkedHashMap object() : { + final java.util.LinkedHashMap map = new java.util.LinkedHashMap(); + String key; + Object value; +}{ + + [ + key = objectKey() + + value = anything() + { map.put(key, value); } + { key = null; value = null; } + ( + + key = objectKey() + + value = anything() + { map.put(key, value); } + { key = null; value = null; } + )* + ] + + { return map; } +} + +java.util.ArrayList list() : { + final java.util.ArrayList list = new java.util.ArrayList(); + Object value; +}{ + + [ + value = anything() + { list.add(value); } + { value = null; } + ( + + value = anything() + { list.add(value); } + { value = null; } + )* + ] + + { + list.trimToSize(); + return list; + } +} + +Object value() : { + Object x; +}{ + ( x = string() + | x = number() + | x = booleanValue() + | x = nullValue() + ) + { return x; } +} + +Object nullValue(): {}{ + + { return null; } +} + +Boolean booleanValue(): { + Boolean b; +}{ + ( + ( + + { b = Boolean.TRUE; } + ) | ( + + { b = Boolean.FALSE; } + ) + ) + { return b; } +} + +Number number(): { + Token t; +}{ + ( + t = + { + if (nativeNumbers) { + return Long.valueOf(t.image); + } else { + return new java.math.BigDecimal(t.image); + } + } + ) | ( + t = + { + if (nativeNumbers) { + return Double.valueOf(t.image); + } else { + return new java.math.BigInteger(substringBefore(t.image, '.')); + } + } + ) +} + +String string() : { + String s; +}{ + ( s = doubleQuoteString() + | s = singleQuoteString() + ) + { return s; } +} + +String doubleQuoteString() : { +}{ + ( + + { return ""; } + ) | ( + + { + String image = token.image; + return image.substring(1, image.length() - 1); + } + ) +} + +String singleQuoteString() : { +}{ + ( + + { return ""; } + ) | ( + + { + String image = token.image; + return image.substring(1, image.length() - 1); + } + ) +} + +String symbol() : { +}{ + + { return token.image; } +} diff --git a/java/org/apache/tomcat/util/json/JSONParserConstants.java b/java/org/apache/tomcat/util/json/JSONParserConstants.java new file mode 100644 index 0000000..7f1315d --- /dev/null +++ b/java/org/apache/tomcat/util/json/JSONParserConstants.java @@ -0,0 +1,122 @@ +/* Generated By:JavaCC: Do not edit this line. JSONParserConstants.java */ +/* + * 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.tomcat.util.json; + + +/** + * Token literal values and constants. + * Generated by org.javacc.parser.OtherFilesGen#start() + */ +public interface JSONParserConstants { + + /** End of File. */ + int EOF = 0; + /** RegularExpression Id. */ + int C_SINGLE_COMMENT = 1; + /** RegularExpression Id. */ + int C_MULTILINE_COMMENT = 2; + /** RegularExpression Id. */ + int SH_SINGLE_COMMENT = 3; + /** RegularExpression Id. */ + int WHITESPACE = 4; + /** RegularExpression Id. */ + int EOL = 5; + /** RegularExpression Id. */ + int COMMA = 6; + /** RegularExpression Id. */ + int BRACE_OPEN = 7; + /** RegularExpression Id. */ + int BRACE_CLOSE = 8; + /** RegularExpression Id. */ + int COLON = 9; + /** RegularExpression Id. */ + int BRACKET_OPEN = 10; + /** RegularExpression Id. */ + int BRACKET_CLOSE = 11; + /** RegularExpression Id. */ + int ZERO = 12; + /** RegularExpression Id. */ + int DIGIT_NONZERO = 13; + /** RegularExpression Id. */ + int DIGIT = 14; + /** RegularExpression Id. */ + int NUMBER_INTEGER = 15; + /** RegularExpression Id. */ + int NUMBER_DECIMAL = 16; + /** RegularExpression Id. */ + int TRUE = 17; + /** RegularExpression Id. */ + int FALSE = 18; + /** RegularExpression Id. */ + int NULL = 19; + /** RegularExpression Id. */ + int QUOTE_DOUBLE = 20; + /** RegularExpression Id. */ + int QUOTE_SINGLE = 21; + /** RegularExpression Id. */ + int STRING_SINGLE_EMPTY = 22; + /** RegularExpression Id. */ + int STRING_DOUBLE_EMPTY = 23; + /** RegularExpression Id. */ + int STRING_SINGLE_BODY = 24; + /** RegularExpression Id. */ + int STRING_DOUBLE_BODY = 25; + /** RegularExpression Id. */ + int STRING_SINGLE_NONEMPTY = 26; + /** RegularExpression Id. */ + int STRING_DOUBLE_NONEMPTY = 27; + /** RegularExpression Id. */ + int SYMBOL = 28; + + /** Lexical state. */ + int DEFAULT = 0; + + /** Literal token values. */ + String[] tokenImage = { + "", + "", + "", + "", + "", + "", + "\",\"", + "\"{\"", + "\"}\"", + "\":\"", + "\"[\"", + "\"]\"", + "\"0\"", + "", + "", + "", + "", + "\"true\"", + "\"false\"", + "\"null\"", + "\"\\\"\"", + "\"\\\'\"", + "\"\\\'\\\'\"", + "\"\\\"\\\"\"", + "", + "", + "", + "", + "", + }; + +} diff --git a/java/org/apache/tomcat/util/json/JSONParserTokenManager.java b/java/org/apache/tomcat/util/json/JSONParserTokenManager.java new file mode 100644 index 0000000..adf29f7 --- /dev/null +++ b/java/org/apache/tomcat/util/json/JSONParserTokenManager.java @@ -0,0 +1,869 @@ +/* JSONParserTokenManager.java */ +/* Generated By:JavaCC: Do not edit this line. JSONParserTokenManager.java */ +/* + * 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.tomcat.util.json; + +/** Token Manager. */ +@SuppressWarnings("all") // Ignore warnings in generated code +public class JSONParserTokenManager implements JSONParserConstants { + + /** Debug output. */ + public java.io.PrintStream debugStream = System.out; + /** Set debug output. */ + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0){ + switch (pos) + { + case 0: + if ((active0 & 0xe0000L) != 0L) + { + jjmatchedKind = 28; + return 15; + } + if ((active0 & 0x400000L) != 0L) { + return 38; + } + if ((active0 & 0x800000L) != 0L) { + return 39; + } + return -1; + case 1: + if ((active0 & 0xe0000L) != 0L) + { + jjmatchedKind = 28; + jjmatchedPos = 1; + return 15; + } + return -1; + case 2: + if ((active0 & 0xe0000L) != 0L) + { + jjmatchedKind = 28; + jjmatchedPos = 2; + return 15; + } + return -1; + case 3: + if ((active0 & 0xa0000L) != 0L) { + return 15; + } + if ((active0 & 0x40000L) != 0L) + { + jjmatchedKind = 28; + jjmatchedPos = 3; + return 15; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0){ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private int jjMoveStringLiteralDfa0_0(){ + switch(curChar) + { + case 34: + return jjMoveStringLiteralDfa1_0(0x800000L); + case 39: + return jjMoveStringLiteralDfa1_0(0x400000L); + case 44: + return jjStopAtPos(0, 6); + case 58: + return jjStopAtPos(0, 9); + case 91: + return jjStopAtPos(0, 10); + case 93: + return jjStopAtPos(0, 11); + case 70: + case 102: + return jjMoveStringLiteralDfa1_0(0x40000L); + case 78: + case 110: + return jjMoveStringLiteralDfa1_0(0x80000L); + case 84: + case 116: + return jjMoveStringLiteralDfa1_0(0x20000L); + case 123: + return jjStopAtPos(0, 7); + case 125: + return jjStopAtPos(0, 8); + default : + return jjMoveNfa_0(0, 0); + } +} +private int jjMoveStringLiteralDfa1_0(long active0){ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(0, active0); + return 1; + } + switch(curChar) + { + case 34: + if ((active0 & 0x800000L) != 0L) { + return jjStopAtPos(1, 23); + } + break; + case 39: + if ((active0 & 0x400000L) != 0L) { + return jjStopAtPos(1, 22); + } + break; + case 65: + case 97: + return jjMoveStringLiteralDfa2_0(active0, 0x40000L); + case 82: + case 114: + return jjMoveStringLiteralDfa2_0(active0, 0x20000L); + case 85: + case 117: + return jjMoveStringLiteralDfa2_0(active0, 0x80000L); + default : + break; + } + return jjStartNfa_0(0, active0); +} +private int jjMoveStringLiteralDfa2_0(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_0(0, old0); + } + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(1, active0); + return 2; + } + switch(curChar) + { + case 76: + case 108: + return jjMoveStringLiteralDfa3_0(active0, 0xc0000L); + case 85: + case 117: + return jjMoveStringLiteralDfa3_0(active0, 0x20000L); + default : + break; + } + return jjStartNfa_0(1, active0); +} +private int jjMoveStringLiteralDfa3_0(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_0(1, old0); + } + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(2, active0); + return 3; + } + switch(curChar) + { + case 69: + case 101: + if ((active0 & 0x20000L) != 0L) { + return jjStartNfaWithStates_0(3, 17, 15); + } + break; + case 76: + case 108: + if ((active0 & 0x80000L) != 0L) { + return jjStartNfaWithStates_0(3, 19, 15); + } + break; + case 83: + case 115: + return jjMoveStringLiteralDfa4_0(active0, 0x40000L); + default : + break; + } + return jjStartNfa_0(2, active0); +} +private int jjMoveStringLiteralDfa4_0(long old0, long active0){ + if (((active0 &= old0)) == 0L) { + return jjStartNfa_0(2, old0); + } + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(3, active0); + return 4; + } + switch(curChar) + { + case 69: + case 101: + if ((active0 & 0x40000L) != 0L) { + return jjStartNfaWithStates_0(4, 18, 15); + } + break; + default : + break; + } + return jjStartNfa_0(3, active0); +} +private int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +static final long[] jjbitVec0 = { + 0xfffffffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec2 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private int jjMoveNfa_0(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 38; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) { + ReInitRounds(); + } + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 28) { + kind = 28; + } + { jjCheckNAdd(15); } + } + else if ((0x3400L & l) != 0L) + { + if (kind > 5) { + kind = 5; + } + } + else if ((0x100000200L & l) != 0L) + { + if (kind > 4) { + kind = 4; + } + } + else if (curChar == 45) + { jjCheckNAddStates(0, 3); } + else if (curChar == 47) + { jjAddStates(4, 5); } + else if (curChar == 34) + { jjCheckNAddTwoStates(11, 12); } + else if (curChar == 39) + { jjCheckNAddTwoStates(6, 7); } + else if (curChar == 35) + { jjCheckNAddTwoStates(1, 2); } + if ((0x3fe000000000000L & l) != 0L) + { + if (kind > 15) { + kind = 15; + } + { jjCheckNAddStates(6, 8); } + } + else if (curChar == 48) + { + if (kind > 15) { + kind = 15; + } + { jjCheckNAddStates(9, 11); } + } + break; + case 38: + case 6: + if ((0xffffff7fffffc9ffL & l) != 0L) + { jjCheckNAddStates(12, 14); } + break; + case 39: + case 11: + if ((0xfffffffbffffc9ffL & l) != 0L) + { jjCheckNAddStates(15, 17); } + break; + case 1: + if ((0xffffffffffffcbffL & l) != 0L) + { jjCheckNAddTwoStates(1, 2); } + break; + case 2: + if ((0x3400L & l) != 0L && kind > 3) { + kind = 3; + } + break; + case 3: + if ((0x100000200L & l) != 0L && kind > 4) { + kind = 4; + } + break; + case 4: + if ((0x3400L & l) != 0L && kind > 5) { + kind = 5; + } + break; + case 5: + if (curChar == 39) + { jjCheckNAddTwoStates(6, 7); } + break; + case 8: + if ((0x808000000000L & l) != 0L) + { jjCheckNAddStates(12, 14); } + break; + case 9: + if (curChar == 39 && kind > 26) { + kind = 26; + } + break; + case 10: + if (curChar == 34) + { jjCheckNAddTwoStates(11, 12); } + break; + case 13: + if ((0x800400000000L & l) != 0L) + { jjCheckNAddStates(15, 17); } + break; + case 14: + if (curChar == 34 && kind > 27) { + kind = 27; + } + break; + case 15: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 28) { + kind = 28; + } + { jjCheckNAdd(15); } + break; + case 16: + if (curChar == 47) + { jjAddStates(4, 5); } + break; + case 17: + if (curChar == 47) + { jjCheckNAddTwoStates(18, 19); } + break; + case 18: + if ((0xffffffffffffcbffL & l) != 0L) + { jjCheckNAddTwoStates(18, 19); } + break; + case 19: + if ((0x3400L & l) != 0L && kind > 1) { + kind = 1; + } + break; + case 20: + if (curChar == 42) + { jjCheckNAddTwoStates(21, 23); } + break; + case 21: + { jjCheckNAddTwoStates(21, 23); } + break; + case 22: + if (curChar == 47 && kind > 2) { + kind = 2; + } + break; + case 23: + if (curChar == 42) { + jjstateSet[jjnewStateCnt++] = 22; + } + break; + case 24: + if (curChar == 45) + { jjCheckNAddStates(0, 3); } + break; + case 25: + if (curChar != 48) { + break; + } + if (kind > 15) { + kind = 15; + } + { jjCheckNAdd(25); } + break; + case 26: + if ((0x3fe000000000000L & l) == 0L) { + break; + } + if (kind > 15) { + kind = 15; + } + { jjCheckNAdd(27); } + break; + case 27: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 15) { + kind = 15; + } + { jjCheckNAdd(27); } + break; + case 28: + if (curChar == 48) + { jjCheckNAddTwoStates(28, 29); } + break; + case 29: + if (curChar == 46) + { jjCheckNAdd(30); } + break; + case 30: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 16) { + kind = 16; + } + { jjCheckNAddTwoStates(30, 31); } + break; + case 32: + if ((0x280000000000L & l) != 0L) + { jjCheckNAdd(33); } + break; + case 33: + if ((0x3ff000000000000L & l) == 0L) { + break; + } + if (kind > 16) { + kind = 16; + } + { jjCheckNAdd(33); } + break; + case 34: + if ((0x3fe000000000000L & l) != 0L) + { jjCheckNAddTwoStates(35, 29); } + break; + case 35: + if ((0x3ff000000000000L & l) != 0L) + { jjCheckNAddTwoStates(35, 29); } + break; + case 36: + if (curChar != 48) { + break; + } + if (kind > 15) { + kind = 15; + } + { jjCheckNAddStates(9, 11); } + break; + case 37: + if ((0x3fe000000000000L & l) == 0L) { + break; + } + if (kind > 15) { + kind = 15; + } + { jjCheckNAddStates(6, 8); } + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + case 15: + if ((0x7fffffe07fffffeL & l) == 0L) { + break; + } + if (kind > 28) { + kind = 28; + } + { jjCheckNAdd(15); } + break; + case 38: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddStates(12, 14); } + else if (curChar == 92) { + jjstateSet[jjnewStateCnt++] = 8; + } + break; + case 39: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddStates(15, 17); } + else if (curChar == 92) { + jjstateSet[jjnewStateCnt++] = 13; + } + break; + case 1: + { jjAddStates(18, 19); } + break; + case 6: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddStates(12, 14); } + break; + case 7: + if (curChar == 92) { + jjstateSet[jjnewStateCnt++] = 8; + } + break; + case 8: + if ((0x14404410144044L & l) != 0L) + { jjCheckNAddStates(12, 14); } + break; + case 11: + if ((0xffffffffefffffffL & l) != 0L) + { jjCheckNAddStates(15, 17); } + break; + case 12: + if (curChar == 92) { + jjstateSet[jjnewStateCnt++] = 13; + } + break; + case 13: + if ((0x14404410144044L & l) != 0L) + { jjCheckNAddStates(15, 17); } + break; + case 18: + { jjAddStates(20, 21); } + break; + case 21: + { jjAddStates(22, 23); } + break; + case 31: + if ((0x2000000020L & l) != 0L) + { jjAddStates(24, 25); } + break; + default : break; + } + } while(i != startsAt); + } else { + int hiByte = (curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 38: + case 6: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjCheckNAddStates(12, 14); } + break; + case 39: + case 11: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjCheckNAddStates(15, 17); } + break; + case 1: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjAddStates(18, 19); } + break; + case 18: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjAddStates(20, 21); } + break; + case 21: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { jjAddStates(22, 23); } + break; + default : if (i1 == 0 || l1 == 0 || i2 == 0 || l2 == 0) { + break; + } else { + break; + } + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 38 - (jjnewStateCnt = startsAt))) { + return curPos; + } + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} + +/** Token literal values. */ +public static final String[] jjstrLiteralImages = { +"", null, null, null, null, null, "\54", "\173", "\175", "\72", "\133", +"\135", null, null, null, null, null, null, null, null, null, null, "\47\47", +"\42\42", null, null, null, null, null, }; +protected Token jjFillToken() +{ + final Token t; + final String curTokenImage; + final int beginLine; + final int endLine; + final int beginColumn; + final int endColumn; + String im = jjstrLiteralImages[jjmatchedKind]; + curTokenImage = (im == null) ? input_stream.GetImage() : im; + beginLine = input_stream.getBeginLine(); + beginColumn = input_stream.getBeginColumn(); + endLine = input_stream.getEndLine(); + endColumn = input_stream.getEndColumn(); + t = Token.newToken(jjmatchedKind, curTokenImage); + + t.beginLine = beginLine; + t.endLine = endLine; + t.beginColumn = beginColumn; + t.endColumn = endColumn; + + return t; +} +static final int[] jjnextStates = { + 25, 26, 28, 34, 17, 20, 27, 35, 29, 25, 28, 29, 6, 7, 9, 11, + 12, 14, 1, 2, 18, 19, 21, 23, 32, 33, +}; +private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec2[i2] & l2) != 0L); + default : + if ((jjbitVec0[i1] & l1) != 0L) { + return true; + } + return false; + } +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +/** Get the next Token. */ +public Token getNextToken() +{ + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(Exception e) + { + jjmatchedKind = 0; + jjmatchedPos = -1; + matchedToken = jjFillToken(); + return matchedToken; + } + + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) { + input_stream.backup(curPos - jjmatchedPos - 1); + } + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + return matchedToken; + } else { + continue EOFLoop; + } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } else { + error_column++; + } + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } +} + +void SkipLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + default : + break; + } +} +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + default : + break; + } +} +void TokenLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + default : + break; + } +} +private void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} + +private void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} + + /** Constructor. */ + public JSONParserTokenManager(JavaCharStream stream){ + + if (JavaCharStream.staticFlag) { + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + } + + input_stream = stream; + } + + /** Constructor. */ + public JSONParserTokenManager (JavaCharStream stream, int lexState){ + ReInit(stream); + SwitchTo(lexState); + } + + /** Reinitialise parser. */ + public void ReInit(JavaCharStream stream) + { + jjmatchedPos = + jjnewStateCnt = + 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); + } + + private void ReInitRounds() + { + int i; + jjround = 0x80000001; + for (i = 38; i-- > 0;) { + jjrounds[i] = 0x80000000; + } + } + + /** Reinitialise parser. */ + public void ReInit(JavaCharStream stream, int lexState) + { + ReInit(stream); + SwitchTo(lexState); + } + + /** Switch to specified lex state. */ + public void SwitchTo(int lexState) + { + if (lexState >= 1 || lexState < 0) { + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + } else { + curLexState = lexState; + } + } + + +/** Lexer state names. */ +public static final String[] lexStateNames = { + "DEFAULT", +}; + +/** Lex State array. */ +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, +}; +static final long[] jjtoToken = { + 0x1ccf8fc1L, +}; +static final long[] jjtoSkip = { + 0x3eL, +}; +static final long[] jjtoSpecial = { + 0x0L, +}; +static final long[] jjtoMore = { + 0x0L, +}; + protected JavaCharStream input_stream; + + private final int[] jjrounds = new int[38]; + private final int[] jjstateSet = new int[2 * 38]; + private final StringBuilder jjimage = new StringBuilder(); + private StringBuilder image = jjimage; + private int jjimageLen; + private int lengthOfMatch; + protected int curChar; +} diff --git a/java/org/apache/tomcat/util/json/JavaCharStream.java b/java/org/apache/tomcat/util/json/JavaCharStream.java new file mode 100644 index 0000000..65cf498 --- /dev/null +++ b/java/org/apache/tomcat/util/json/JavaCharStream.java @@ -0,0 +1,641 @@ +/* Generated By:JavaCC: Do not edit this line. JavaCharStream.java Version 7.0 */ +/* JavaCCOptions:STATIC=false,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +/* + * 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.tomcat.util.json; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (with java-like unicode escape processing). + */ +@SuppressWarnings("all") // Ignore warnings in generated code +public +class JavaCharStream +{ + /** Whether parser is static. */ + public static final boolean staticFlag = false; + + static final int hexval(char c) throws java.io.IOException { + switch(c) + { + case '0' : + return 0; + case '1' : + return 1; + case '2' : + return 2; + case '3' : + return 3; + case '4' : + return 4; + case '5' : + return 5; + case '6' : + return 6; + case '7' : + return 7; + case '8' : + return 8; + case '9' : + return 9; + + case 'a' : + case 'A' : + return 10; + case 'b' : + case 'B' : + return 11; + case 'c' : + case 'C' : + return 12; + case 'd' : + case 'D' : + return 13; + case 'e' : + case 'E' : + return 14; + case 'f' : + case 'F' : + return 15; + } + + throw new java.io.IOException(); // Should never come here + } + +/** Position in buffer. */ + public int bufpos = -1; + int bufsize; + int available; + int tokenBegin; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] nextCharBuf; + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int nextCharInd = -1; + protected int inBuf = 0; + protected int tabSize = 1; + protected boolean trackLineColumn = true; + + public void setTabSize(int i) { tabSize = i; } + public int getTabSize() { return tabSize; } + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + bufpos += (bufsize - tokenBegin); + } else { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + bufpos -= tokenBegin; + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + available = (bufsize += 2048); + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + int i; + if (maxNextCharInd == 4096) { + maxNextCharInd = nextCharInd = 0; + } + + try { + if ((i = inputStream.read(nextCharBuf, maxNextCharInd, + 4096 - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } else { + maxNextCharInd += i; + } + return; + } + catch(java.io.IOException e) { + if (bufpos != 0) + { + --bufpos; + backup(0); + } else { + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + throw e; + } + } + + protected char ReadByte() throws java.io.IOException + { + if (++nextCharInd >= maxNextCharInd) { + FillBuff(); + } + + return nextCharBuf[nextCharInd]; + } + +/** @return starting character for token. */ + public char BeginToken() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) { + bufpos = 0; + } + + tokenBegin = bufpos; + return buffer[bufpos]; + } + + tokenBegin = 0; + bufpos = -1; + + return readChar(); + } + + protected void AdjustBuffSize() + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = 0; + available = tokenBegin; + } else { + ExpandBuff(false); + } + } + else if (available > tokenBegin) { + available = bufsize; + } else if ((tokenBegin - available) < 2048) { + ExpandBuff(true); + } else { + available = tokenBegin; + } + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } else { + line += (column = 1); + } + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + +/** Read a character. */ + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) { + bufpos = 0; + } + + return buffer[bufpos]; + } + + char c; + + if (++bufpos == available) { + AdjustBuffSize(); + } + + if ((buffer[bufpos] = c = ReadByte()) == '\\') + { + if (trackLineColumn) { UpdateLineColumn(c); } + + int backSlashCnt = 1; + + for (;;) // Read all the backslashes + { + if (++bufpos == available) { + AdjustBuffSize(); + } + + try + { + if ((buffer[bufpos] = c = ReadByte()) != '\\') + { + if (trackLineColumn) { UpdateLineColumn(c); } + // found a non-backslash char. + if ((c == 'u') && ((backSlashCnt & 1) == 1)) + { + if (--bufpos < 0) { + bufpos = bufsize - 1; + } + + break; + } + + backup(backSlashCnt); + return '\\'; + } + } + catch(java.io.IOException e) + { + // We are returning one backslash so we should only backup (count-1) + if (backSlashCnt > 1) { + backup(backSlashCnt-1); + } + + return '\\'; + } + + if (trackLineColumn) { UpdateLineColumn(c); } + backSlashCnt++; + } + + // Here, we have seen an odd number of backslash's followed by a 'u' + try + { + while ((c = ReadByte()) == 'u') { + ++column; + } + + buffer[bufpos] = c = (char)(hexval(c) << 12 | + hexval(ReadByte()) << 8 | + hexval(ReadByte()) << 4 | + hexval(ReadByte())); + + column += 4; + } + catch(java.io.IOException e) + { + throw new Error("Invalid escape character at line " + line + + " column " + column + "."); + } + + if (backSlashCnt == 1) { + return c; + } else + { + backup(backSlashCnt - 1); + return '\\'; + } + } else { + UpdateLineColumn(c); + return c; + } + } + + @Deprecated + /** + * @deprecated + * @see #getEndColumn + */ + public int getColumn() { + return bufcolumn[bufpos]; + } + + @Deprecated + /** + * @deprecated + * @see #getEndLine + */ + public int getLine() { + return bufline[bufpos]; + } + +/** Get end column. */ + public int getEndColumn() { + return bufcolumn[bufpos]; + } + +/** Get end line. */ + public int getEndLine() { + return bufline[bufpos]; + } + +/** @return column of token start */ + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + +/** @return line number of token start */ + public int getBeginLine() { + return bufline[tokenBegin]; + } + +/** Retreat. */ + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) { + bufpos += bufsize; + } + } + +/** Constructor. */ + public JavaCharStream(java.io.Reader dstream, + int startline, int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + nextCharBuf = new char[4096]; + } + +/** Constructor. */ + public JavaCharStream(java.io.Reader dstream, + int startline, int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + +/** Constructor. */ + public JavaCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } +/** Reinitialise. */ + public void ReInit(java.io.Reader dstream, + int startline, int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + nextCharBuf = new char[4096]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + nextCharInd = bufpos = -1; + } + +/** Reinitialise. */ + public void ReInit(java.io.Reader dstream, + int startline, int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + +/** Reinitialise. */ + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } +/** Constructor. */ + public JavaCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + +/** Constructor. */ + public JavaCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, 4096); + } + +/** Constructor. */ + public JavaCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + +/** Constructor. */ + public JavaCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + +/** Constructor. */ + public JavaCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + +/** Constructor. */ + public JavaCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + +/** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + +/** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } +/** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } +/** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } +/** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + +/** Reinitialise. */ + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + + /** @return token image as String */ + public String GetImage() + { + if (bufpos >= tokenBegin) { + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + } else { + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + } + + /** @return suffix */ + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) { + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + } else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + /** Set buffers back to null when finished. */ + public void Done() + { + nextCharBuf = null; + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } else { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) { + bufline[j] = newLine++; + } else { + bufline[j] = newLine; + } + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + boolean getTrackLineColumn() { return trackLineColumn; } + void setTrackLineColumn(boolean tlc) { trackLineColumn = tlc; } + +} +/* JavaCC - OriginalChecksum=58f718af3466d6064ba0132bca4fbce2 (do not edit this line) */ diff --git a/java/org/apache/tomcat/util/json/LocalStrings.properties b/java/org/apache/tomcat/util/json/LocalStrings.properties new file mode 100644 index 0000000..4a8717d --- /dev/null +++ b/java/org/apache/tomcat/util/json/LocalStrings.properties @@ -0,0 +1,16 @@ +# 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. + +parser.expectedEOF=Expected EOF, but still had content to parse diff --git a/java/org/apache/tomcat/util/json/LocalStrings_de.properties b/java/org/apache/tomcat/util/json/LocalStrings_de.properties new file mode 100644 index 0000000..534d9a7 --- /dev/null +++ b/java/org/apache/tomcat/util/json/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +parser.expectedEOF=Habe EOF erwartet, aber es gab noch weiteren Inhalt zum Parsen diff --git a/java/org/apache/tomcat/util/json/LocalStrings_fr.properties b/java/org/apache/tomcat/util/json/LocalStrings_fr.properties new file mode 100644 index 0000000..4804c7e --- /dev/null +++ b/java/org/apache/tomcat/util/json/LocalStrings_fr.properties @@ -0,0 +1,16 @@ +# 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. + +parser.expectedEOF=L'EOF était attendue mais il y avait toujours du contenu à nalyser diff --git a/java/org/apache/tomcat/util/json/LocalStrings_ja.properties b/java/org/apache/tomcat/util/json/LocalStrings_ja.properties new file mode 100644 index 0000000..a2addc0 --- /dev/null +++ b/java/org/apache/tomcat/util/json/LocalStrings_ja.properties @@ -0,0 +1,16 @@ +# 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. + +parser.expectedEOF=EOFを期待ã—ã¾ã™ãŒã€è§£æžã™ã‚‹å†…容をã¾ã æŒã£ã¦ã„ã¾ã—ãŸã€‚ diff --git a/java/org/apache/tomcat/util/json/LocalStrings_ko.properties b/java/org/apache/tomcat/util/json/LocalStrings_ko.properties new file mode 100644 index 0000000..eb30a12 --- /dev/null +++ b/java/org/apache/tomcat/util/json/LocalStrings_ko.properties @@ -0,0 +1,16 @@ +# 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. + +parser.expectedEOF=예ìƒëœ EOFì´ê¸´ 하나, 여전히 파싱할 컨í…íŠ¸ë“¤ì´ ë‚¨ì•„ 있습니다. diff --git a/java/org/apache/tomcat/util/json/LocalStrings_ru.properties b/java/org/apache/tomcat/util/json/LocalStrings_ru.properties new file mode 100644 index 0000000..e8e95d6 --- /dev/null +++ b/java/org/apache/tomcat/util/json/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +parser.expectedEOF=ОжидалÑÑ EOF, но вÑÑ‘ еще имелоÑÑŒ Ñодержимое Ð´Ð»Ñ Ñ€Ð°Ð·Ð±Ð¾Ñ€Ð° diff --git a/java/org/apache/tomcat/util/json/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/json/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..f39f3c6 --- /dev/null +++ b/java/org/apache/tomcat/util/json/LocalStrings_zh_CN.properties @@ -0,0 +1,16 @@ +# 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. + +parser.expectedEOF=期望EOF,但ä»æœ‰å†…容需è¦è§£æž diff --git a/java/org/apache/tomcat/util/json/ParseException.java b/java/org/apache/tomcat/util/json/ParseException.java new file mode 100644 index 0000000..d569eea --- /dev/null +++ b/java/org/apache/tomcat/util/json/ParseException.java @@ -0,0 +1,212 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 7.0 */ +/* JavaCCOptions:KEEP_LINE_COLUMN=true */ +/* + * 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.tomcat.util.json; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +@SuppressWarnings("all") // Ignore warnings in generated code +public class ParseException extends Exception { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * The end of line string for this machine. + */ + protected static String EOL = System.getProperty("line.separator", "\n"); + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(initialise(currentTokenVal, expectedTokenSequencesVal, tokenImageVal)); + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + } + + /** Constructor with message. */ + public ParseException(String message) { + super(message); + } + + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * following this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * It uses "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser) the correct error message + * gets displayed. + */ + private static String initialise(Token currentToken, + int[][] expectedTokenSequences, + String[] tokenImage) { + + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(' '); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(EOL).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) { + retval += " "; + } + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += " " + tokenImage[tok.kind]; + retval += " \""; + retval += add_escapes(tok.image); + retval += " \""; + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + EOL; + + + if (expectedTokenSequences.length == 0) { + // Nothing to add here + } else { + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + EOL + " "; + } else { + retval += "Was expecting one of:" + EOL + " "; + } + retval += expected.toString(); + } + + return retval; + } + + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + static String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} +/* JavaCC - OriginalChecksum=1ccce09e6dc04d17e33b6d15b9c1f0c3 (do not edit this line) */ diff --git a/java/org/apache/tomcat/util/json/Token.java b/java/org/apache/tomcat/util/json/Token.java new file mode 100644 index 0000000..479e14e --- /dev/null +++ b/java/org/apache/tomcat/util/json/Token.java @@ -0,0 +1,147 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 7.0 */ +/* JavaCCOptions:TOKEN_EXTENDS=,KEEP_LINE_COLUMN=true,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +/* + * 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.tomcat.util.json; + +/** + * Describes the input token stream. + */ +@SuppressWarnings("all") // Ignore warnings in generated code +public class Token implements java.io.Serializable { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** The line number of the first character of this Token. */ + public int beginLine; + /** The column number of the first character of this Token. */ + public int beginColumn; + /** The line number of the last character of this Token. */ + public int endLine; + /** The column number of the last character of this Token. */ + public int endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * An optional attribute value of the Token. + * Tokens which are not used as syntactic sugar will often contain + * meaningful values that will be used later on by the compiler or + * interpreter. This attribute value is often different from the image. + * Any subclass of Token that actually wants to return a non-null value can + * override this method as appropriate. + */ + public Object getValue() { + return null; + } + + /** + * No-argument constructor + */ + public Token() {} + + /** + * Constructs a new token for the specified Image. + */ + public Token(int kind) + { + this(kind, null); + } + + /** + * Constructs a new token for the specified Image and Kind. + */ + public Token(int kind, String image) + { + this.kind = kind; + this.image = image; + } + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simply add something like : + * + * case MyParserConstants.ID : return new IDToken(ofKind, image); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use sit in your lexical actions. + */ + public static Token newToken(int ofKind, String image) + { + switch(ofKind) + { + default : return new Token(ofKind, image); + } + } + + public static Token newToken(int ofKind) + { + return newToken(ofKind, null); + } + +} +/* JavaCC - OriginalChecksum=3fa555852689f4df3b05452671ed7031 (do not edit this line) */ diff --git a/java/org/apache/tomcat/util/json/TokenMgrError.java b/java/org/apache/tomcat/util/json/TokenMgrError.java new file mode 100644 index 0000000..d6f3e2b --- /dev/null +++ b/java/org/apache/tomcat/util/json/TokenMgrError.java @@ -0,0 +1,163 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 7.0 */ +/* JavaCCOptions: */ +/* + * 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.tomcat.util.json; + +/** Token Manager Error. */ +@SuppressWarnings("all") // Ignore warnings in generated code +public class TokenMgrError extends Error +{ + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occurred. + */ + public static final int LEXICAL_ERROR = 0; + + /** + * An attempt was made to create a second instance of a static token manager. + */ + public static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + public static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + public static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their escaped (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexical error + * curLexState : lexical state in which this error occurred + * errorLine : line number when the error occurred + * errorColumn : column number when the error occurred + * errorAfter : prefix that was seen before this error occurred + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalErr(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar) { + char curChar1 = (char)curChar; + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar1)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + /** No arg constructor. */ + public TokenMgrError() { + } + + /** Constructor with message and reason. */ + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + /** Full Constructor. */ + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, int curChar, int reason) { + this(LexicalErr(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} +/* JavaCC - OriginalChecksum=7da4e668c47dee63e10c9b008699a2ca (do not edit this line) */ diff --git a/java/org/apache/tomcat/util/log/CaptureLog.java b/java/org/apache/tomcat/util/log/CaptureLog.java new file mode 100644 index 0000000..5c56b55 --- /dev/null +++ b/java/org/apache/tomcat/util/log/CaptureLog.java @@ -0,0 +1,49 @@ +/* + * 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.tomcat.util.log; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * Per Thread System.err and System.out log capture data. + * + * @author Glenn L. Nielsen + */ + +class CaptureLog { + + protected CaptureLog() { + baos = new ByteArrayOutputStream(); + ps = new PrintStream(baos); + } + + private final ByteArrayOutputStream baos; + private final PrintStream ps; + + protected PrintStream getStream() { + return ps; + } + + protected void reset() { + baos.reset(); + } + + protected String getCapture() { + return baos.toString(); + } +} diff --git a/java/org/apache/tomcat/util/log/SystemLogHandler.java b/java/org/apache/tomcat/util/log/SystemLogHandler.java new file mode 100644 index 0000000..946d9d6 --- /dev/null +++ b/java/org/apache/tomcat/util/log/SystemLogHandler.java @@ -0,0 +1,278 @@ +/* + * 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.tomcat.util.log; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * This helper class may be used to do sophisticated redirection of + * System.out and System.err on a per Thread basis. + * + * A stack is implemented per Thread so that nested startCapture + * and stopCapture can be used. + * + * @author Remy Maucherat + * @author Glenn L. Nielsen + */ +public class SystemLogHandler extends PrintStream { + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct the handler to capture the output of the given steam. + * + * @param wrapped The stream to capture + */ + public SystemLogHandler(PrintStream wrapped) { + super(wrapped); + out = wrapped; + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Wrapped PrintStream. + */ + private final PrintStream out; + + + /** + * Thread <-> CaptureLog associations. + */ + private static final ThreadLocal> logs = new ThreadLocal<>(); + + + /** + * Spare CaptureLog ready for reuse. + */ + private static final Queue reuse = new ConcurrentLinkedQueue<>(); + + + // --------------------------------------------------------- Public Methods + + + /** + * Start capturing thread's output. + */ + public static void startCapture() { + CaptureLog log = null; + if (!reuse.isEmpty()) { + try { + log = reuse.remove(); + } catch (NoSuchElementException e) { + log = new CaptureLog(); + } + } else { + log = new CaptureLog(); + } + Deque stack = logs.get(); + if (stack == null) { + stack = new ArrayDeque<>(); + logs.set(stack); + } + stack.addFirst(log); + } + + + /** + * Stop capturing thread's output. + * + * @return The captured data + */ + public static String stopCapture() { + Queue stack = logs.get(); + if (stack == null || stack.isEmpty()) { + return null; + } + CaptureLog log = stack.remove(); + if (log == null) { + return null; + } + String capture = log.getCapture(); + log.reset(); + reuse.add(log); + return capture; + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Find PrintStream to which the output must be written to. + * @return the print stream + */ + protected PrintStream findStream() { + Queue stack = logs.get(); + if (stack != null && !stack.isEmpty()) { + CaptureLog log = stack.peek(); + if (log != null) { + PrintStream ps = log.getStream(); + if (ps != null) { + return ps; + } + } + } + return out; + } + + + // ---------------------------------------------------- PrintStream Methods + + + @Override + public void flush() { + findStream().flush(); + } + + @Override + public void close() { + findStream().close(); + } + + @Override + public boolean checkError() { + return findStream().checkError(); + } + + @Override + protected void setError() { + //findStream().setError(); + } + + @Override + public void write(int b) { + findStream().write(b); + } + + @Override + public void write(byte[] b) + throws IOException { + findStream().write(b); + } + + @Override + public void write(byte[] buf, int off, int len) { + findStream().write(buf, off, len); + } + + @Override + public void print(boolean b) { + findStream().print(b); + } + + @Override + public void print(char c) { + findStream().print(c); + } + + @Override + public void print(int i) { + findStream().print(i); + } + + @Override + public void print(long l) { + findStream().print(l); + } + + @Override + public void print(float f) { + findStream().print(f); + } + + @Override + public void print(double d) { + findStream().print(d); + } + + @Override + public void print(char[] s) { + findStream().print(s); + } + + @Override + public void print(String s) { + findStream().print(s); + } + + @Override + public void print(Object obj) { + findStream().print(obj); + } + + @Override + public void println() { + findStream().println(); + } + + @Override + public void println(boolean x) { + findStream().println(x); + } + + @Override + public void println(char x) { + findStream().println(x); + } + + @Override + public void println(int x) { + findStream().println(x); + } + + @Override + public void println(long x) { + findStream().println(x); + } + + @Override + public void println(float x) { + findStream().println(x); + } + + @Override + public void println(double x) { + findStream().println(x); + } + + @Override + public void println(char[] x) { + findStream().println(x); + } + + @Override + public void println(String x) { + findStream().println(x); + } + + @Override + public void println(Object x) { + findStream().println(x); + } + +} diff --git a/java/org/apache/tomcat/util/log/UserDataHelper.java b/java/org/apache/tomcat/util/log/UserDataHelper.java new file mode 100644 index 0000000..a4354dd --- /dev/null +++ b/java/org/apache/tomcat/util/log/UserDataHelper.java @@ -0,0 +1,149 @@ +/* + * 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.tomcat.util.log; + +import org.apache.juli.logging.Log; + +/** + * This helper class assists with the logging associated with invalid input + * data. A developer may want all instances of invalid input data logged to + * assist with debugging whereas in production it is likely to be desirable not + * to log anything for invalid data. The following settings may be used: + *
      + *
    • NOTHING: Log nothing.
    • + *
    • DEBUG_ALL: Log all problems at DEBUG log level.
    • + *
    • INFO_THEN_DEBUG: Log first problem at INFO log level and any further + * issues in the following TBD (configurable) seconds at DEBUG level
    • + *
    • INFO_ALL: Log all problems at INFO log level.
    • + *
    + * By default, INFO_THEN_DEBUG is used with a suppression time of 24 hours. + * + * NOTE: This class is not completely thread-safe. When using INFO_THEN_DEBUG it + * is possible that several INFO messages will be logged before dropping to + * DEBUG. + */ +public class UserDataHelper { + + private final Log log; + + private final Config config; + + // A value of 0 is equivalent to using INFO_ALL + // A negative value will trigger infinite suppression + // The value is milliseconds + private final long suppressionTime; + + private volatile long lastInfoTime = 0; + + + public UserDataHelper(Log log) { + this.log = log; + + Config tempConfig; + String configString = System.getProperty( + "org.apache.juli.logging.UserDataHelper.CONFIG"); + if (configString == null) { + tempConfig = Config.INFO_THEN_DEBUG; + } else { + try { + tempConfig = Config.valueOf(configString); + } catch (IllegalArgumentException iae) { + // Ignore - use default + tempConfig = Config.INFO_THEN_DEBUG; + } + } + + // Default suppression time of 1 day. + suppressionTime = Integer.getInteger( + "org.apache.juli.logging.UserDataHelper.SUPPRESSION_TIME", + 60 * 60 * 24).intValue() * 1000L; + + if (suppressionTime == 0) { + tempConfig = Config.INFO_ALL; + } + + config = tempConfig; + } + + + /** + * Returns log mode for the next log message, or null if the + * message should not be logged. + * + *

    + * If INFO_THEN_DEBUG configuration option is enabled, this + * method might change internal state of this object. + * + * @return Log mode, or null + */ + public Mode getNextMode() { + if (Config.NONE == config) { + return null; + } else if (Config.DEBUG_ALL == config) { + return log.isDebugEnabled() ? Mode.DEBUG : null; + } else if (Config.INFO_THEN_DEBUG == config) { + if (logAtInfo()) { + return log.isInfoEnabled() ? Mode.INFO_THEN_DEBUG : null; + } else { + return log.isDebugEnabled() ? Mode.DEBUG : null; + } + } else if (Config.INFO_ALL == config) { + return log.isInfoEnabled() ? Mode.INFO : null; + } + // Should never happen + return null; + } + + + /* + * Not completely thread-safe but good enough for this use case. I couldn't + * see a simple enough way to make it completely thread-safe that was not + * likely to compromise performance. + */ + private boolean logAtInfo() { + + if (suppressionTime < 0 && lastInfoTime > 0) { + return false; + } + + long now = System.currentTimeMillis(); + + if (lastInfoTime + suppressionTime > now) { + return false; + } + + lastInfoTime = now; + return true; + } + + + private enum Config { + NONE, + DEBUG_ALL, + INFO_THEN_DEBUG, + INFO_ALL + } + + /** + * Log mode for the next log message. + */ + public enum Mode { + DEBUG, + INFO_THEN_DEBUG, + INFO + } +} diff --git a/java/org/apache/tomcat/util/modeler/AttributeInfo.java b/java/org/apache/tomcat/util/modeler/AttributeInfo.java new file mode 100644 index 0000000..4c899e2 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/AttributeInfo.java @@ -0,0 +1,167 @@ +/* + * 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.tomcat.util.modeler; + +import javax.management.MBeanAttributeInfo; + + +/** + *

    Internal configuration information for an Attribute + * descriptor.

    + * + * @author Craig R. McClanahan + */ +public class AttributeInfo extends FeatureInfo { + private static final long serialVersionUID = -2511626862303972143L; + + // ----------------------------------------------------- Instance Variables + protected String displayName = null; + + // Information about the method to use + protected String getMethod = null; + protected String setMethod = null; + protected boolean readable = true; + protected boolean writeable = true; + protected boolean is = false; + + // ------------------------------------------------------------- Properties + + /** + * @return the display name of this attribute. + */ + public String getDisplayName() { + return this.displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + /** + * @return the name of the property getter method, if non-standard. + */ + public String getGetMethod() { + if(getMethod == null) { + getMethod = getMethodName(getName(), true, isIs()); + } + return this.getMethod; + } + + public void setGetMethod(String getMethod) { + this.getMethod = getMethod; + } + + /** + * Is this a boolean attribute with an "is" getter? + * @return true if this is a boolean attribute + * with an "is" getter + */ + public boolean isIs() { + return this.is; + } + + public void setIs(boolean is) { + this.is = is; + } + + + /** + * Is this attribute readable by management applications? + * @return true if readable + */ + public boolean isReadable() { + return this.readable; + } + + public void setReadable(boolean readable) { + this.readable = readable; + } + + + /** + * @return the name of the property setter method, if non-standard. + */ + public String getSetMethod() { + if( setMethod == null ) { + setMethod = getMethodName(getName(), false, false); + } + return this.setMethod; + } + + public void setSetMethod(String setMethod) { + this.setMethod = setMethod; + } + + /** + * Is this attribute writable by management applications? + * @return true if writable + */ + public boolean isWriteable() { + return this.writeable; + } + + public void setWriteable(boolean writeable) { + this.writeable = writeable; + } + + // --------------------------------------------------------- Public Methods + + + /** + * Create and return a ModelMBeanAttributeInfo object that + * corresponds to the attribute described by this instance. + * @return the attribute info + */ + MBeanAttributeInfo createAttributeInfo() { + // Return our cached information (if any) + if (info == null) { + info = new MBeanAttributeInfo(getName(), getType(), getDescription(), + isReadable(), isWriteable(), false); + } + return (MBeanAttributeInfo)info; + } + + // -------------------------------------------------------- Private Methods + + + /** + * Create and return the name of a default property getter or setter + * method, according to the specified values. + * + * @param name Name of the property itself + * @param getter Do we want a get method (versus a set method)? + * @param is If returning a getter, do we want the "is" form? + * @return the method name + */ + private String getMethodName(String name, boolean getter, boolean is) { + StringBuilder sb = new StringBuilder(); + if (getter) { + if (is) { + sb.append("is"); + } else { + sb.append("get"); + } + } else { + sb.append("set"); + } + sb.append(Character.toUpperCase(name.charAt(0))); + sb.append(name.substring(1)); + return sb.toString(); + } + + +} diff --git a/java/org/apache/tomcat/util/modeler/BaseAttributeFilter.java b/java/org/apache/tomcat/util/modeler/BaseAttributeFilter.java new file mode 100644 index 0000000..77815f3 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/BaseAttributeFilter.java @@ -0,0 +1,164 @@ +/* + * 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.tomcat.util.modeler; + + +import java.util.HashSet; +import java.util.Set; + +import javax.management.AttributeChangeNotification; +import javax.management.Notification; +import javax.management.NotificationFilter; + + +/** + *

    Implementation of NotificationFilter for attribute change + * notifications. This class is used by BaseModelMBean to + * construct attribute change notification event filters when a filter is not + * supplied by the application.

    + * + * @author Craig R. McClanahan + */ +public class BaseAttributeFilter implements NotificationFilter { + + private static final long serialVersionUID = 1L; + + // ----------------------------------------------------------- Constructors + + /** + * Construct a new filter that accepts only the specified attribute + * name. + * + * @param name Name of the attribute to be accepted by this filter, or + * null to accept all attribute names + */ + public BaseAttributeFilter(String name) { + + super(); + if (name != null) { + addAttribute(name); + } + + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The set of attribute names that are accepted by this filter. If this + * list is empty, all attribute names are accepted. + */ + private Set names = new HashSet<>(); + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new attribute name to the set of names accepted by this filter. + * + * @param name Name of the attribute to be accepted + */ + public void addAttribute(String name) { + + synchronized (names) { + names.add(name); + } + + } + + + /** + * Clear all accepted names from this filter, so that it will accept + * all attribute names. + */ + public void clear() { + + synchronized (names) { + names.clear(); + } + + } + + + /** + * Return the set of names that are accepted by this filter. If this + * filter accepts all attribute names, a zero length array will be + * returned. + * @return the array of names + */ + public String[] getNames() { + + synchronized (names) { + return names.toArray(new String[0]); + } + + } + + + /** + *

    Test whether notification enabled for this event. + * Return true if:

    + *
      + *
    • This is an attribute change notification
    • + *
    • Either the set of accepted names is empty (implying that all + * attribute names are of interest) or the set of accepted names + * includes the name of the attribute in this notification
    • + *
    + */ + @Override + public boolean isNotificationEnabled(Notification notification) { + + if (notification == null) { + return false; + } + if (!(notification instanceof AttributeChangeNotification)) { + return false; + } + AttributeChangeNotification acn = + (AttributeChangeNotification) notification; + if (!AttributeChangeNotification.ATTRIBUTE_CHANGE.equals(acn.getType())) { + return false; + } + synchronized (names) { + if (names.size() < 1) { + return true; + } else { + return names.contains(acn.getAttributeName()); + } + } + + } + + + /** + * Remove an attribute name from the set of names accepted by this + * filter. + * + * @param name Name of the attribute to be removed + */ + public void removeAttribute(String name) { + + synchronized (names) { + names.remove(name); + } + + } + + +} diff --git a/java/org/apache/tomcat/util/modeler/BaseModelMBean.java b/java/org/apache/tomcat/util/modeler/BaseModelMBean.java new file mode 100644 index 0000000..27561b2 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/BaseModelMBean.java @@ -0,0 +1,952 @@ +/* + * 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.tomcat.util.modeler; + + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.management.Attribute; +import javax.management.AttributeChangeNotification; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.InstanceNotFoundException; +import javax.management.InvalidAttributeValueException; +import javax.management.ListenerNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanNotificationInfo; +import javax.management.MBeanRegistration; +import javax.management.MBeanServer; +import javax.management.Notification; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectName; +import javax.management.ReflectionException; +import javax.management.RuntimeErrorException; +import javax.management.RuntimeOperationsException; +import javax.management.modelmbean.InvalidTargetObjectTypeException; +import javax.management.modelmbean.ModelMBeanNotificationBroadcaster; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/* + * Changes from commons.modeler: + * + * - use DynamicMBean + * - remove methods not used in tomcat and redundant/not very generic + * - must be created from the ManagedBean - I don't think there were any direct + * uses, but now it is required. + * - some of the gratuitous flexibility removed - instead this is more predictive and + * strict with the use cases. + * - all Method and metadata is stored in ManagedBean. BaseModelBMean and ManagedBean act + * like Object and Class. + * - setModelMBean is no longer called on resources ( not used in tomcat ) + * - no caching of Methods for now - operations and setters are not called repeatedly in most + * management use cases. Getters shouldn't be called very frequently either - and even if they + * are, the overhead of getting the method should be small compared with other JMX costs ( RMI, etc ). + * We can add getter cache if needed. + * - removed unused constructor, fields + * + * TODO: + * - clean up catalina.mbeans, stop using weird inheritance + */ + +/** + *

    Basic implementation of the DynamicMBean interface, which + * supports the minimal requirements of the interface contract.

    + * + *

    This can be used directly to wrap an existing java bean, or inside + * an mlet or anywhere an MBean would be used. + * + * Limitations: + *

      + *
    • Only managed resources of type objectReference are + * supported.
    • + *
    • Caching of attribute values and operation results is not supported. + * All calls to invoke() are immediately executed.
    • + *
    • Persistence of MBean attributes and operations is not supported.
    • + *
    • All classes referenced as attribute types, operation parameters, or + * operation return values must be one of the following: + *
        + *
      • One of the Java primitive types (boolean, byte, char, double, + * float, integer, long, short). Corresponding value will be wrapped + * in the appropriate wrapper class automatically.
      • + *
      • Operations that return no value should declare a return type of + * void.
      • + *
      + *
    • Attribute caching is not supported
    • + *
    + * + * @author Craig R. McClanahan + * @author Costin Manolache + */ +public class BaseModelMBean implements DynamicMBean, MBeanRegistration, + ModelMBeanNotificationBroadcaster { + + private static final Log log = LogFactory.getLog(BaseModelMBean.class); + private static final StringManager sm = StringManager.getManager(BaseModelMBean.class); + + // ----------------------------------------------------- Instance Variables + + protected ObjectName oname=null; + + /** + * Notification broadcaster for attribute changes. + */ + protected BaseNotificationBroadcaster attributeBroadcaster = null; + + /** + * Notification broadcaster for general notifications. + */ + protected BaseNotificationBroadcaster generalBroadcaster = null; + + /** + * Metadata for the mbean instance. + */ + protected ManagedBean managedBean = null; + + /** + * The managed resource this MBean is associated with (if any). + */ + protected Object resource = null; + + // --------------------------------------------------- DynamicMBean Methods + // TODO: move to ManagedBean + static final Object[] NO_ARGS_PARAM = new Object[0]; + + protected String resourceType = null; + + // key: operation val: invoke method + //private Hashtable invokeAttMap=new Hashtable(); + + /** + * Obtain and return the value of a specific attribute of this MBean. + * + * @param name Name of the requested attribute + * + * @exception AttributeNotFoundException if this attribute is not + * supported by this MBean + * @exception MBeanException if the initializer of an object + * throws an exception + * @exception ReflectionException if a Java reflection exception + * occurs when invoking the getter + */ + @Override + public Object getAttribute(String name) + throws AttributeNotFoundException, MBeanException, + ReflectionException { + // Validate the input parameters + if (name == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullAttributeName")), + sm.getString("baseModelMBean.nullAttributeName")); + } + + if( (resource instanceof DynamicMBean) && + ! ( resource instanceof BaseModelMBean )) { + return ((DynamicMBean)resource).getAttribute(name); + } + + Method m=managedBean.getGetter(name, this, resource); + Object result = null; + try { + Class declaring = m.getDeclaringClass(); + // workaround for catalina weird mbeans - the declaring class is BaseModelMBean. + // but this is the catalina class. + if( declaring.isAssignableFrom(this.getClass()) ) { + result = m.invoke(this, NO_ARGS_PARAM ); + } else { + result = m.invoke(resource, NO_ARGS_PARAM ); + } + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + if (t == null) { + t = e; + } + if (t instanceof RuntimeException) { + throw new RuntimeOperationsException + ((RuntimeException) t, sm.getString("baseModelMBean.invokeError", name)); + } else if (t instanceof Error) { + throw new RuntimeErrorException + ((Error) t, sm.getString("baseModelMBean.invokeError", name)); + } else { + throw new MBeanException + (e, sm.getString("baseModelMBean.invokeError", name)); + } + } catch (Exception e) { + throw new MBeanException + (e, sm.getString("baseModelMBean.invokeError", name)); + } + + // Return the results of this method invocation + return result; + } + + + /** + * Obtain and return the values of several attributes of this MBean. + * + * @param names Names of the requested attributes + */ + @Override + public AttributeList getAttributes(String names[]) { + + // Validate the input parameters + if (names == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullAttributeNameList")), + sm.getString("baseModelMBean.nullAttributeNameList")); + } + + // Prepare our response, eating all exceptions + AttributeList response = new AttributeList(); + for (String name : names) { + try { + response.add(new Attribute(name, getAttribute(name))); + } catch (Exception e) { + // Not having a particular attribute in the response + // is the indication of a getter problem + } + } + return response; + + } + + public void setManagedBean(ManagedBean managedBean) { + this.managedBean = managedBean; + } + + /** + * Return the MBeanInfo object for this MBean. + */ + @Override + public MBeanInfo getMBeanInfo() { + return managedBean.getMBeanInfo(); + } + + + /** + * Invoke a particular method on this MBean, and return any returned + * value. + * + *

    IMPLEMENTATION NOTE - This implementation will + * attempt to invoke this method on the MBean itself, or (if not + * available) on the managed resource object associated with this + * MBean.

    + * + * @param name Name of the operation to be invoked + * @param params Array containing the method parameters of this operation + * @param signature Array containing the class names representing + * the signature of this operation + * + * @exception MBeanException if the initializer of an object + * throws an exception + * @exception ReflectionException if a Java reflection exception + * occurs when invoking a method + */ + @Override + public Object invoke(String name, Object params[], String signature[]) + throws MBeanException, ReflectionException + { + if( (resource instanceof DynamicMBean) && + ! ( resource instanceof BaseModelMBean )) { + return ((DynamicMBean)resource).invoke(name, params, signature); + } + + // Validate the input parameters + if (name == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullMethodName")), + sm.getString("baseModelMBean.nullMethodName")); + } + + if( log.isTraceEnabled()) { + log.trace("Invoke " + name); + } + + Method method= managedBean.getInvoke(name, params, signature, this, resource); + + // Invoke the selected method on the appropriate object + Object result = null; + try { + if( method.getDeclaringClass().isAssignableFrom( this.getClass()) ) { + result = method.invoke(this, params ); + } else { + result = method.invoke(resource, params); + } + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + log.error(sm.getString("baseModelMBean.invokeError", name), t ); + if (t == null) { + t = e; + } + if (t instanceof RuntimeException) { + throw new RuntimeOperationsException + ((RuntimeException) t, sm.getString("baseModelMBean.invokeError", name)); + } else if (t instanceof Error) { + throw new RuntimeErrorException + ((Error) t, sm.getString("baseModelMBean.invokeError", name)); + } else { + throw new MBeanException + ((Exception)t, sm.getString("baseModelMBean.invokeError", name)); + } + } catch (Exception e) { + log.error(sm.getString("baseModelMBean.invokeError", name), e ); + throw new MBeanException + (e, sm.getString("baseModelMBean.invokeError", name)); + } + + // Return the results of this method invocation + return result; + + } + + static Class getAttributeClass(String signature) + throws ReflectionException + { + if (signature.equals(Boolean.TYPE.getName())) { + return Boolean.TYPE; + } else if (signature.equals(Byte.TYPE.getName())) { + return Byte.TYPE; + } else if (signature.equals(Character.TYPE.getName())) { + return Character.TYPE; + } else if (signature.equals(Double.TYPE.getName())) { + return Double.TYPE; + } else if (signature.equals(Float.TYPE.getName())) { + return Float.TYPE; + } else if (signature.equals(Integer.TYPE.getName())) { + return Integer.TYPE; + } else if (signature.equals(Long.TYPE.getName())) { + return Long.TYPE; + } else if (signature.equals(Short.TYPE.getName())) { + return Short.TYPE; + } else { + try { + ClassLoader cl=Thread.currentThread().getContextClassLoader(); + if( cl!=null ) { + return cl.loadClass(signature); + } + } catch( ClassNotFoundException e ) { + } + try { + return Class.forName(signature); + } catch (ClassNotFoundException e) { + throw new ReflectionException(e, sm.getString("baseModelMBean.cnfeForSignature", signature)); + } + } + } + + /** + * Set the value of a specific attribute of this MBean. + * + * @param attribute The identification of the attribute to be set + * and the new value + * + * @exception AttributeNotFoundException if this attribute is not + * supported by this MBean + * @exception MBeanException if the initializer of an object + * throws an exception + * @exception ReflectionException if a Java reflection exception + * occurs when invoking the getter + */ + @Override + public void setAttribute(Attribute attribute) + throws AttributeNotFoundException, MBeanException, + ReflectionException + { + if( log.isTraceEnabled() ) { + log.trace("Setting attribute " + this + " " + attribute ); + } + + if( (resource instanceof DynamicMBean) && + ! ( resource instanceof BaseModelMBean )) { + try { + ((DynamicMBean)resource).setAttribute(attribute); + } catch (InvalidAttributeValueException e) { + throw new MBeanException(e); + } + return; + } + + // Validate the input parameters + if (attribute == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullAttribute")), + sm.getString("baseModelMBean.nullAttribute")); + } + + String name = attribute.getName(); + Object value = attribute.getValue(); + + if (name == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullAttributeName")), + sm.getString("baseModelMBean.nullAttributeName")); + } + + Object oldValue=null; + //if( getAttMap.get(name) != null ) + // oldValue=getAttribute( name ); + + Method m=managedBean.getSetter(name,this,resource); + + try { + if( m.getDeclaringClass().isAssignableFrom( this.getClass()) ) { + m.invoke(this, new Object[] { value }); + } else { + m.invoke(resource, new Object[] { value }); + } + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + if (t == null) { + t = e; + } + if (t instanceof RuntimeException) { + throw new RuntimeOperationsException + ((RuntimeException) t, sm.getString("baseModelMBean.invokeError", name)); + } else if (t instanceof Error) { + throw new RuntimeErrorException + ((Error) t, sm.getString("baseModelMBean.invokeError", name)); + } else { + throw new MBeanException + (e, sm.getString("baseModelMBean.invokeError", name)); + } + } catch (Exception e) { + log.error(sm.getString("baseModelMBean.invokeError", name) , e ); + throw new MBeanException + (e, sm.getString("baseModelMBean.invokeError", name)); + } + try { + sendAttributeChangeNotification(new Attribute( name, oldValue), + attribute); + } catch(Exception ex) { + log.error(sm.getString("baseModelMBean.notificationError", name), ex); + } + //attributes.put( name, value ); +// if( source != null ) { +// // this mbean is associated with a source - maybe we want to persist +// source.updateField(oname, name, value); +// } + } + + @Override + public String toString() { + if( resource==null ) { + return "BaseModelMbean[" + resourceType + "]"; + } + return resource.toString(); + } + + /** + * Set the values of several attributes of this MBean. + * + * @param attributes THe names and values to be set + * + * @return The list of attributes that were set and their new values + */ + @Override + public AttributeList setAttributes(AttributeList attributes) { + AttributeList response = new AttributeList(); + + // Validate the input parameters + if (attributes == null) { + return response; + } + + // Prepare and return our response, eating all exceptions + String names[] = new String[attributes.size()]; + int n = 0; + for (Object attribute : attributes) { + Attribute item = (Attribute) attribute; + names[n++] = item.getName(); + try { + setAttribute(item); + } catch (Exception e) { + // Ignore all exceptions + } + } + + return getAttributes(names); + + } + + + // ----------------------------------------------------- ModelMBean Methods + + + /** + * Get the instance handle of the object against which we execute + * all methods in this ModelMBean management interface. + * + * @return the backend managed object + * @exception InstanceNotFoundException if the managed resource object + * cannot be found + * @exception InvalidTargetObjectTypeException if the managed resource + * object is of the wrong type + * @exception MBeanException if the initializer of the object throws + * an exception + * @exception RuntimeOperationsException if the managed resource or the + * resource type is null or invalid + */ + public Object getManagedResource() + throws InstanceNotFoundException, InvalidTargetObjectTypeException, + MBeanException, RuntimeOperationsException { + + if (resource == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullResource")), + sm.getString("baseModelMBean.nullResource")); + } + + return resource; + + } + + + /** + * Set the instance handle of the object against which we will execute + * all methods in this ModelMBean management interface. + * + * The caller can provide the mbean instance or the object name to + * the resource, if needed. + * + * @param resource The resource object to be managed + * @param type The type of reference for the managed resource + * ("ObjectReference", "Handle", "IOR", "EJBHandle", or + * "RMIReference") + * + * @exception InstanceNotFoundException if the managed resource object + * cannot be found + * @exception MBeanException if the initializer of the object throws + * an exception + * @exception RuntimeOperationsException if the managed resource or the + * resource type is null or invalid + */ + public void setManagedResource(Object resource, String type) + throws InstanceNotFoundException, + MBeanException, RuntimeOperationsException + { + if (resource == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullResource")), + sm.getString("baseModelMBean.nullResource")); + } + +// if (!"objectreference".equalsIgnoreCase(type)) +// throw new InvalidTargetObjectTypeException(type); + + this.resource = resource; + this.resourceType = resource.getClass().getName(); + +// // Make the resource aware of the model mbean. +// try { +// Method m=resource.getClass().getMethod("setModelMBean", +// new Class[] {ModelMBean.class}); +// if( m!= null ) { +// m.invoke(resource, new Object[] {this}); +// } +// } catch( NoSuchMethodException t ) { +// // ignore +// } catch( Throwable t ) { +// log.error( "Can't set model mbean ", t ); +// } + } + + + // ------------------------------ ModelMBeanNotificationBroadcaster Methods + + + /** + * Add an attribute change notification event listener to this MBean. + * + * @param listener Listener that will receive event notifications + * @param name Name of the attribute of interest, or null + * to indicate interest in all attributes + * @param handback Handback object to be sent along with event + * notifications + * + * @exception IllegalArgumentException if the listener parameter is null + */ + @Override + public void addAttributeChangeNotificationListener + (NotificationListener listener, String name, Object handback) + throws IllegalArgumentException { + + if (listener == null) { + throw new IllegalArgumentException(sm.getString("baseModelMBean.nullListener")); + } + if (attributeBroadcaster == null) { + attributeBroadcaster = new BaseNotificationBroadcaster(); + } + + if( log.isTraceEnabled() ) { + log.trace("addAttributeNotificationListener " + listener); + } + + BaseAttributeFilter filter = new BaseAttributeFilter(name); + attributeBroadcaster.addNotificationListener + (listener, filter, handback); + + } + + + /** + * Remove an attribute change notification event listener from + * this MBean. + * + * @param listener The listener to be removed + * @param name The attribute name for which no more events are required + * + * + * @exception ListenerNotFoundException if this listener is not + * registered in the MBean + */ + @Override + public void removeAttributeChangeNotificationListener + (NotificationListener listener, String name) + throws ListenerNotFoundException { + + if (listener == null) { + throw new IllegalArgumentException(sm.getString("baseModelMBean.nullListener")); + } + + // FIXME - currently this removes *all* notifications for this listener + if (attributeBroadcaster != null) { + attributeBroadcaster.removeNotificationListener(listener); + } + + } + + + /** + * Send an AttributeChangeNotification to all registered + * listeners. + * + * @param notification The AttributeChangeNotification + * that will be passed + * + * @exception MBeanException if an object initializer throws an + * exception + * @exception RuntimeOperationsException wraps IllegalArgumentException + * when the specified notification is null or invalid + */ + @Override + public void sendAttributeChangeNotification + (AttributeChangeNotification notification) + throws MBeanException, RuntimeOperationsException { + + if (notification == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullNotification")), + sm.getString("baseModelMBean.nullNotification")); + } + if (attributeBroadcaster == null) + { + return; // This means there are no registered listeners + } + if( log.isTraceEnabled() ) { + log.trace( "AttributeChangeNotification " + notification ); + } + attributeBroadcaster.sendNotification(notification); + + } + + + /** + * Send an AttributeChangeNotification to all registered + * listeners. + * + * @param oldValue The original value of the Attribute + * @param newValue The new value of the Attribute + * + * @exception MBeanException if an object initializer throws an + * exception + * @exception RuntimeOperationsException wraps IllegalArgumentException + * when the specified notification is null or invalid + */ + @Override + public void sendAttributeChangeNotification + (Attribute oldValue, Attribute newValue) + throws MBeanException, RuntimeOperationsException { + + // Calculate the class name for the change notification + String type = null; + if (newValue.getValue() != null) { + type = newValue.getValue().getClass().getName(); + } else if (oldValue.getValue() != null) { + type = oldValue.getValue().getClass().getName(); + } + else { + return; // Old and new are both null == no change + } + + AttributeChangeNotification notification = + new AttributeChangeNotification + (this, 1, System.currentTimeMillis(), + "Attribute value has changed", + oldValue.getName(), type, + oldValue.getValue(), newValue.getValue()); + sendAttributeChangeNotification(notification); + + } + + + /** + * Send a Notification to all registered listeners as a + * jmx.modelmbean.general notification. + * + * @param notification The Notification that will be passed + * + * @exception MBeanException if an object initializer throws an + * exception + * @exception RuntimeOperationsException wraps IllegalArgumentException + * when the specified notification is null or invalid + */ + @Override + public void sendNotification(Notification notification) + throws MBeanException, RuntimeOperationsException { + + if (notification == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullNotification")), + sm.getString("baseModelMBean.nullNotification")); + } + if (generalBroadcaster == null) + { + return; // This means there are no registered listeners + } + generalBroadcaster.sendNotification(notification); + + } + + + /** + * Send a Notification which contains the specified string + * as a jmx.modelmbean.generic notification. + * + * @param message The message string to be passed + * + * @exception MBeanException if an object initializer throws an + * exception + * @exception RuntimeOperationsException wraps IllegalArgumentException + * when the specified notification is null or invalid + */ + @Override + public void sendNotification(String message) + throws MBeanException, RuntimeOperationsException { + + if (message == null) { + throw new RuntimeOperationsException + (new IllegalArgumentException(sm.getString("baseModelMBean.nullMessage")), + sm.getString("baseModelMBean.nullMessage")); + } + Notification notification = new Notification + ("jmx.modelmbean.generic", this, 1, message); + sendNotification(notification); + + } + + + // ---------------------------------------- NotificationBroadcaster Methods + + + /** + * Add a notification event listener to this MBean. + * + * @param listener Listener that will receive event notifications + * @param filter Filter object used to filter event notifications + * actually delivered, or null for no filtering + * @param handback Handback object to be sent along with event + * notifications + * + * @exception IllegalArgumentException if the listener parameter is null + */ + @Override + public void addNotificationListener(NotificationListener listener, + NotificationFilter filter, + Object handback) + throws IllegalArgumentException { + + if (listener == null) { + throw new IllegalArgumentException(sm.getString("baseModelMBean.nullListener")); + } + + if( log.isTraceEnabled() ) { + log.trace("addNotificationListener " + listener); + } + + if (generalBroadcaster == null) { + generalBroadcaster = new BaseNotificationBroadcaster(); + } + generalBroadcaster.addNotificationListener + (listener, filter, handback); + + // We'll send the attribute change notifications to all listeners ( who care ) + // The normal filtering can be used. + // The problem is that there is no other way to add attribute change listeners + // to a model mbean ( AFAIK ). I suppose the spec should be fixed. + if (attributeBroadcaster == null) { + attributeBroadcaster = new BaseNotificationBroadcaster(); + } + + if( log.isTraceEnabled() ) { + log.trace("addAttributeNotificationListener " + listener); + } + + attributeBroadcaster.addNotificationListener + (listener, filter, handback); + } + + + /** + * Return an MBeanNotificationInfo object describing the + * notifications sent by this MBean. + */ + @Override + public MBeanNotificationInfo[] getNotificationInfo() { + + // Acquire the set of application notifications + MBeanNotificationInfo current[] = getMBeanInfo().getNotifications(); + MBeanNotificationInfo response[] = + new MBeanNotificationInfo[current.length + 2]; + // Descriptor descriptor = null; + + // Fill in entry for general notifications +// descriptor = new DescriptorSupport +// (new String[] { "name=GENERIC", +// "descriptorType=notification", +// "log=T", +// "severity=5", +// "displayName=jmx.modelmbean.generic" }); + response[0] = new MBeanNotificationInfo + (new String[] { "jmx.modelmbean.generic" }, + "GENERIC", + "Text message notification from the managed resource"); + //descriptor); + + // Fill in entry for attribute change notifications +// descriptor = new DescriptorSupport +// (new String[] { "name=ATTRIBUTE_CHANGE", +// "descriptorType=notification", +// "log=T", +// "severity=5", +// "displayName=jmx.attribute.change" }); + response[1] = new MBeanNotificationInfo + (new String[] { "jmx.attribute.change" }, + "ATTRIBUTE_CHANGE", + "Observed MBean attribute value has changed"); + //descriptor); + + // Copy remaining notifications as reported by the application + System.arraycopy(current, 0, response, 2, current.length); + return response; + + } + + + /** + * Remove a notification event listener from this MBean. + * + * @param listener The listener to be removed (any and all registrations + * for this listener will be eliminated) + * + * @exception ListenerNotFoundException if this listener is not + * registered in the MBean + */ + @Override + public void removeNotificationListener(NotificationListener listener) + throws ListenerNotFoundException { + + if (listener == null) { + throw new IllegalArgumentException(sm.getString("baseModelMBean.nullListener")); + } + + if (generalBroadcaster != null) { + generalBroadcaster.removeNotificationListener(listener); + } + + if (attributeBroadcaster != null) { + attributeBroadcaster.removeNotificationListener(listener); + } + } + + + public String getModelerType() { + return resourceType; + } + + public String getClassName() { + return getModelerType(); + } + + public ObjectName getJmxName() { + return oname; + } + + public String getObjectName() { + if (oname != null) { + return oname.toString(); + } else { + return null; + } + } + + + // -------------------- Registration -------------------- + // XXX We can add some method patterns here- like setName() and + // setDomain() for code that doesn't implement the Registration + + @Override + public ObjectName preRegister(MBeanServer server, + ObjectName name) + throws Exception + { + if( log.isTraceEnabled()) { + log.trace("preRegister " + resource + " " + name ); + } + oname=name; + if( resource instanceof MBeanRegistration ) { + oname = ((MBeanRegistration)resource).preRegister(server, name ); + } + return oname; + } + + @Override + public void postRegister(Boolean registrationDone) { + if( resource instanceof MBeanRegistration ) { + ((MBeanRegistration)resource).postRegister(registrationDone); + } + } + + @Override + public void preDeregister() throws Exception { + if( resource instanceof MBeanRegistration ) { + ((MBeanRegistration)resource).preDeregister(); + } + } + + @Override + public void postDeregister() { + if( resource instanceof MBeanRegistration ) { + ((MBeanRegistration)resource).postDeregister(); + } + } +} diff --git a/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java b/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java new file mode 100644 index 0000000..e00aeb1 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java @@ -0,0 +1,184 @@ +/* + * 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.tomcat.util.modeler; + + +import java.util.ArrayList; + +import javax.management.ListenerNotFoundException; +import javax.management.MBeanNotificationInfo; +import javax.management.Notification; +import javax.management.NotificationBroadcaster; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; + + +/** + * Implementation of NotificationBroadcaster for attribute + * change notifications. This class is used by BaseModelMBean to + * handle notifications of attribute change events to interested listeners. + * + * @author Craig R. McClanahan + * @author Costin Manolache + */ + +public class BaseNotificationBroadcaster implements NotificationBroadcaster { + + + // ----------------------------------------------------------- Constructors + + + // ----------------------------------------------------- Instance Variables + + + /** + * The set of registered BaseNotificationBroadcasterEntry + * entries. + */ + protected ArrayList entries = + new ArrayList<>(); + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a notification event listener to this MBean. + * + * @param listener Listener that will receive event notifications + * @param filter Filter object used to filter event notifications + * actually delivered, or null for no filtering + * @param handback Handback object to be sent along with event + * notifications + * + * @exception IllegalArgumentException if the listener parameter is null + */ + @Override + public void addNotificationListener(NotificationListener listener, + NotificationFilter filter, + Object handback) + throws IllegalArgumentException { + + synchronized (entries) { + + // Optimization to coalesce attribute name filters + if (filter instanceof BaseAttributeFilter) { + BaseAttributeFilter newFilter = (BaseAttributeFilter) filter; + for (BaseNotificationBroadcasterEntry item : entries) { + if ((item.listener == listener) && + (item.filter != null) && + (item.filter instanceof BaseAttributeFilter) && + (item.handback == handback)) { + BaseAttributeFilter oldFilter = + (BaseAttributeFilter) item.filter; + String newNames[] = newFilter.getNames(); + String oldNames[] = oldFilter.getNames(); + if (newNames.length == 0) { + oldFilter.clear(); + } else { + if (oldNames.length != 0) { + for (String newName : newNames) { + oldFilter.addAttribute(newName); + } + } + } + return; + } + } + } + + // General purpose addition of a new entry + entries.add(new BaseNotificationBroadcasterEntry + (listener, filter, handback)); + } + + } + + + /** + * Return an MBeanNotificationInfo object describing the + * notifications sent by this MBean. + */ + @Override + public MBeanNotificationInfo[] getNotificationInfo() { + return new MBeanNotificationInfo[0]; + } + + + /** + * Remove a notification event listener from this MBean. + * + * @param listener The listener to be removed (any and all registrations + * for this listener will be eliminated) + * + * @exception ListenerNotFoundException if this listener is not + * registered in the MBean + */ + @Override + public void removeNotificationListener(NotificationListener listener) + throws ListenerNotFoundException { + + synchronized (entries) { + entries.removeIf(item -> item.listener == listener); + } + + } + + + /** + * Send the specified notification to all interested listeners. + * + * @param notification The notification to be sent + */ + public void sendNotification(Notification notification) { + + synchronized (entries) { + for (BaseNotificationBroadcasterEntry item : entries) { + if ((item.filter != null) && + (!item.filter.isNotificationEnabled(notification))) { + continue; + } + item.listener.handleNotification(notification, item.handback); + } + } + + } + +} + + +/** + * Utility class representing a particular registered listener entry. + */ + +class BaseNotificationBroadcasterEntry { + + BaseNotificationBroadcasterEntry(NotificationListener listener, + NotificationFilter filter, + Object handback) { + this.listener = listener; + this.filter = filter; + this.handback = handback; + } + + public NotificationFilter filter = null; + + public Object handback = null; + + public NotificationListener listener = null; + +} diff --git a/java/org/apache/tomcat/util/modeler/FeatureInfo.java b/java/org/apache/tomcat/util/modeler/FeatureInfo.java new file mode 100644 index 0000000..4eb7751 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/FeatureInfo.java @@ -0,0 +1,81 @@ +/* + * 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.tomcat.util.modeler; + + +import java.io.Serializable; + +import javax.management.MBeanFeatureInfo; + + +/** + *

    Convenience base class for AttributeInfo and + * OperationInfo classes that will be used to collect configuration + * information for the ModelMBean beans exposed for management.

    + * + * @author Craig R. McClanahan + */ +public class FeatureInfo implements Serializable { + private static final long serialVersionUID = -911529176124712296L; + + protected String description = null; + protected String name = null; + protected MBeanFeatureInfo info = null; + + // all have type except Constructor + protected String type = null; + + + // ------------------------------------------------------------- Properties + + /** + * @return the human-readable description of this feature. + */ + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + + /** + * @return the name of this feature, which must be unique among features + * in the same collection. + */ + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * @return the fully qualified Java class name of this element. + */ + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + +} diff --git a/java/org/apache/tomcat/util/modeler/LocalStrings.properties b/java/org/apache/tomcat/util/modeler/LocalStrings.properties new file mode 100644 index 0000000..90b3af2 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/LocalStrings.properties @@ -0,0 +1,52 @@ +# 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. + +baseModelMBean.cnfeForSignature=Cannot find class for [{0}] +baseModelMBean.invokeError=Exception invoking method [{0}] +baseModelMBean.notificationError=Error sending notification [{0}] +baseModelMBean.nullAttribute=The attribute is null +baseModelMBean.nullAttributeName=The attribute name is null +baseModelMBean.nullAttributeNameList=The attribute name list is null +baseModelMBean.nullListener=The listener is null +baseModelMBean.nullMessage=The message is null +baseModelMBean.nullMethodName=The method name is null +baseModelMBean.nullNotification=The notification is null +baseModelMBean.nullResource=The managed resource is null + +managedMBean.cannotInstantiateClass=Cannot instantiate ModelMBean of class [{0}] +managedMBean.cannotLoadClass=Cannot load ModelMBean class [{0}] +managedMBean.inconsistentArguments=Inconsistent arguments and signature +managedMBean.noAttribute=Cannot find attribute [{0}] on resource [{1}] +managedMBean.noGet=Cannot find getter method [{0}] on resource [{1}] +managedMBean.noMethod=Cannot find method [{0}] with this signature +managedMBean.noOperation=Cannot find operation [{0}] +managedMBean.noSet=Cannot find setter method [{0}] on resource [{1}] + +modules.digesterParseError=Error parsing registry data +modules.readDescriptorsError=Error reading descriptors + +registry.createdServer=Created MBeanServer +registry.existingServer=Using existing MBeanServer +registry.initError=Error initializing [{0}] +registry.invalidSource=Invalid source specified, must be either URL, File, Class or InputStream +registry.loadError=Error loading descriptors from [{0}] +registry.noDisable=The MBean registry cannot be disabled because it has already been initialised +registry.noMetadata=Cannot find metadata for object [{0}] +registry.noTypeMetadata=Cannot find metadata for type [{0}] +registry.nullBean=Cannot register null bean for [{0}] +registry.objectNameCreateError=Error creating object name +registry.registerError=Error registering MBean +registry.unregisterError=Error unregistering MBean +registry.unregisterExisting=Unregistering existing component [{0}] diff --git a/java/org/apache/tomcat/util/modeler/LocalStrings_fr.properties b/java/org/apache/tomcat/util/modeler/LocalStrings_fr.properties new file mode 100644 index 0000000..a9545c4 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/LocalStrings_fr.properties @@ -0,0 +1,52 @@ +# 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. + +baseModelMBean.cnfeForSignature=Impossible de trouver la classe pour [{0}] +baseModelMBean.invokeError=Erreur lors de l''invocation de la méthode [{0}] +baseModelMBean.notificationError=Erreur lors de l''envoi de la notification [{0}] +baseModelMBean.nullAttribute=L'attribut est null +baseModelMBean.nullAttributeName=Le nom d'attribut est null +baseModelMBean.nullAttributeNameList=La liste de noms d'attributs est null +baseModelMBean.nullListener=L'écouteur est null +baseModelMBean.nullMessage=Le message est null +baseModelMBean.nullMethodName=Le nom de la méthode est null +baseModelMBean.nullNotification=La notification est null +baseModelMBean.nullResource=La ressource gérée est null + +managedMBean.cannotInstantiateClass=Impossible d’instancier un ModelMBean de classe [{0}] +managedMBean.cannotLoadClass=Impossible de charger le ModelMBean de classe [{0}] +managedMBean.inconsistentArguments=Les arguments et la signature sont inconsistants +managedMBean.noAttribute=Impossible de trouver l''attribut [{0}] sur la ressource [{1}] +managedMBean.noGet=Impossible de trouver la méthode d''accès [{0}] pour la ressource [{1}] +managedMBean.noMethod=Impossible de trouver la méthode [{0}] avec cette signature +managedMBean.noOperation=Impossible de trouver l''opération [{0}] +managedMBean.noSet=Impossible de trouver la méthode de modification [{0}] sur la ressource [{1}] + +modules.digesterParseError=Erreur lors de l'analyse des données du registre +modules.readDescriptorsError=Erreur lors de la lecture des descripteurs + +registry.createdServer=Le MBeanServer a été crée +registry.existingServer=Le MBeanServer existant est utilisé +registry.initError=Erreur lors de l''initialisation [{0}] +registry.invalidSource=Une source invalide a été specifiée, elle doit être URL, File Class ou InputStream +registry.loadError=Impossible de charger les descripteurs à partir de [{0}] +registry.noDisable=Le registre des MBeans ne peut pas être désactivé car il a déjà été initialisé +registry.noMetadata=Impossible de trouver les métadonnées pour l''objet [{0}] +registry.noTypeMetadata=Impossible de trouver les métadonnées pour le type [{0}] +registry.nullBean=Impossible d''enregistrer un bean null pour [{0}] +registry.objectNameCreateError=Erreur lors de la création du nom d'objet +registry.registerError=Erreur lors de l'enregistrement du MBean +registry.unregisterError=Erreur lors du désenregistrement du MBean +registry.unregisterExisting=Désenregistrement du composant existant [{0}] diff --git a/java/org/apache/tomcat/util/modeler/LocalStrings_ja.properties b/java/org/apache/tomcat/util/modeler/LocalStrings_ja.properties new file mode 100644 index 0000000..55fa502 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/LocalStrings_ja.properties @@ -0,0 +1,52 @@ +# 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. + +baseModelMBean.cnfeForSignature=[{0}] ã«å¯¾å¿œã™ã‚‹ã‚¯ãƒ©ã‚¹ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +baseModelMBean.invokeError=メソッド [{0}] を呼ã³å‡ºã™éš›ã®ä¾‹å¤– +baseModelMBean.notificationError=通知é€ä¿¡ã‚¨ãƒ©ãƒ¼[{0}] +baseModelMBean.nullAttribute=属性㌠null ã§ã™ã€‚ +baseModelMBean.nullAttributeName=属性å㌠null ã§ã™ã€‚ +baseModelMBean.nullAttributeNameList=属性åリスト㌠null ã§ã™ã€‚ +baseModelMBean.nullListener=リスナーãŒnullã§ã™ã€‚ +baseModelMBean.nullMessage=メッセージ㌠null ã§ã™ã€‚ +baseModelMBean.nullMethodName=メソッドå㌠null ã§ã™ã€‚ +baseModelMBean.nullNotification=通知内容㌠null ã§ã™ã€‚ +baseModelMBean.nullResource=管ç†ãƒªã‚½ãƒ¼ã‚¹ãŒ null ã§ã™ã€‚ + +managedMBean.cannotInstantiateClass=クラス [{0}] ã®ModelMBean をインスタンス化ã§ãã¾ã›ã‚“。 +managedMBean.cannotLoadClass=ModelMBean クラス [{0}] を読ã¿è¾¼ã‚€ã“ã¨ãŒã§ãã¾ã›ã‚“。 +managedMBean.inconsistentArguments=引数ã¨ã‚·ã‚°ãƒãƒãƒ£ã®ä¸æ•´åˆã§ã™ã€‚ +managedMBean.noAttribute=リソース [{1}] ã«å±žæ€§ [{0}] ãŒã‚ã‚Šã¾ã›ã‚“。 +managedMBean.noGet=リソース [{1}] ã«getter メソッド [{0}] ãŒã‚ã‚Šã¾ã›ã‚“。 +managedMBean.noMethod=ã“ã®ã‚·ã‚°ãƒãƒãƒ£ã«ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +managedMBean.noOperation=æ“作[{0}]ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +managedMBean.noSet=リソース [{1}] ã«setter メソッド [{0}] ãŒã‚ã‚Šã¾ã›ã‚“。 + +modules.digesterParseError=レジストリデータ解æžä¸­ã®ã‚¨ãƒ©ãƒ¼ +modules.readDescriptorsError=記述å­èª­ã¿å–り中ã®ã‚¨ãƒ©ãƒ¼ + +registry.createdServer=作æˆã•ã‚ŒãŸ MBeanServer +registry.existingServer=既存㮠MBeanServer を使用ã™ã‚‹ +registry.initError=[{0}]åˆæœŸåŒ–中ã®ã‚¨ãƒ©ãƒ¼ +registry.invalidSource=無効ãªã‚½ãƒ¼ã‚¹ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ソースã¯ã€URLã€ãƒ•ã‚¡ã‚¤ãƒ«ã€ã‚¯ãƒ©ã‚¹ã€ã¾ãŸã¯å…¥åŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ã®ã„ãšã‚Œã‹ã§ã‚ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ +registry.loadError=[{0}] ã«é…ç½®ã—㟠MBean 記述å­ã®èª­ã¿è¾¼ã¿ä¸­ã®ã‚¨ãƒ©ãƒ¼ +registry.noDisable=MBean レジストリã¯ã™ã§ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã‚‹ãŸã‚無効ã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ +registry.noMetadata=オブジェクト [{0}] ã®ãƒ¡ã‚¿æƒ…å ±ãŒã‚ã‚Šã¾ã›ã‚“。 +registry.noTypeMetadata=クラス [{0}] ã®ãƒ¡ã‚¿æƒ…å ±ãŒã‚ã‚Šã¾ã›ã‚“。 +registry.nullBean=オブジェクトå [{0}] ã® bean ã« null を登録ã§ãã¾ã›ã‚“。 +registry.objectNameCreateError=ObjectName作æˆä¸­ã®ã‚¨ãƒ©ãƒ¼ +registry.registerError=MBean 登録中ã®ã‚¨ãƒ©ãƒ¼ +registry.unregisterError=MBean 登録解除中ã®ã‚¨ãƒ©ãƒ¼ +registry.unregisterExisting=既存ã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ[{0}]ã®ç™»éŒ²ã‚’解除ã—ã¦ã„ã¾ã™ diff --git a/java/org/apache/tomcat/util/modeler/LocalStrings_ko.properties b/java/org/apache/tomcat/util/modeler/LocalStrings_ko.properties new file mode 100644 index 0000000..46a89fd --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/LocalStrings_ko.properties @@ -0,0 +1,48 @@ +# 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. + +baseModelMBean.cnfeForSignature=[{0}]ì„(를) 위한 í´ëž˜ìŠ¤ë¥¼ ì°¾ì„ ìˆ˜ 없습니다. +baseModelMBean.invokeError=메소드 [{0}]ì„(를) 호출하는 중 예외 ë°œìƒ +baseModelMBean.notificationError=통지 [{0}]ì„(를) 전송하는 중 오류 ë°œìƒ +baseModelMBean.nullAttribute=ì†ì„±ì´ ë„입니다. +baseModelMBean.nullAttributeName=해당 ì†ì„± ì´ë¦„ì´ ë„입니다. +baseModelMBean.nullAttributeNameList=ì†ì„± ì´ë¦„ 목ë¡ì´ ë„입니다. +baseModelMBean.nullListener=리스너가 ë„입니다. +baseModelMBean.nullMessage=메시지가 ë„입니다. +baseModelMBean.nullMethodName=메소드 ì´ë¦„ì´ ë„입니다. +baseModelMBean.nullNotification=통지 ê°ì²´ê°€ ë„입니다. +baseModelMBean.nullResource=관리ë˜ëŠ” 리소스가 ë„입니다. + +managedMBean.cannotInstantiateClass=í´ëž˜ìŠ¤ [{0}]ì¸ ModelMBeanì˜ ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•  수 없습니다. +managedMBean.cannotLoadClass=ModelMBean í´ëž˜ìŠ¤ [{0}]ì„(를) 로드할 수 없습니다. +managedMBean.inconsistentArguments=ì¼ê´€ë˜ì§€ ì•Šì€ ì•„ê·œë¨¼íŠ¸ë“¤ê³¼ signature입니다. +managedMBean.noAttribute=리소스 [{1}]ì—ì„œ ì†ì„± [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +managedMBean.noGet=리소스 [{1}]ì—ì„œ getter 메소드 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +managedMBean.noMethod=ì´ signatureë¡œ 메소드 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +managedMBean.noOperation=오í¼ë ˆì´ì…˜ [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. +managedMBean.noSet=리소스 [{1}]ì—ì„œ setter 메소드 [{0}]ì„(를) ì°¾ì„ ìˆ˜ 없습니다. + +modules.digesterParseError=레지스트리 ë°ì´í„°ë¥¼ 파싱하는 중 오류 ë°œìƒ +modules.readDescriptorsError=Descriptorë“¤ì„ ì½ëŠ” 중 오류 ë°œìƒ + +registry.initError=[{0}]ì„(를) 초기화하는 중 오류 ë°œìƒ +registry.loadError=[{0}](으)로부터 descriptorë“¤ì„ ë¡œë“œí•˜ëŠ” 중 오류 ë°œìƒ +registry.noDisable=ì´ë¯¸ 초기화 ë˜ì—ˆê¸° 때문ì—, MBean 레지스트리는 사용 불능 ìƒíƒœë¡œ ë  ìˆ˜ 없습니다. +registry.noMetadata=ê°ì²´ [{0}]ì„(를) 위한 메타ë°ì´í„°ë¥¼ ì°¾ì„ ìˆ˜ 없습니다. +registry.noTypeMetadata=타입 [{0}]ì„(를) 위한 메타ë°ì´í„°ë¥¼ ì°¾ì„ ìˆ˜ 없습니다. +registry.nullBean=[{0}]ì„(를) 위해 ë„ beanì„ ë“±ë¡í•  수 없습니다. +registry.objectNameCreateError=ê°ì²´ ì´ë¦„ì„ ìƒì„±í•˜ëŠ” 중 오류 ë°œìƒ +registry.registerError=MBeanì„ ë“±ë¡í•˜ëŠ” 중 오류 ë°œìƒ +registry.unregisterError=MBeanì˜ ë“±ë¡ì„ 제거하는 중 오류 ë°œìƒ diff --git a/java/org/apache/tomcat/util/modeler/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/modeler/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..b193af5 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/LocalStrings_zh_CN.properties @@ -0,0 +1,48 @@ +# 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. + +baseModelMBean.cnfeForSignature=找ä¸åˆ°[{0}]çš„ç±» +baseModelMBean.invokeError=调用方法[{0}]æ—¶å‘生异常 +baseModelMBean.notificationError=å‘é€é€šçŸ¥æ—¶å‡ºé”™[{0}] +baseModelMBean.nullAttribute=属性为空 +baseModelMBean.nullAttributeName=属性å为空 +baseModelMBean.nullAttributeNameList=属性å列表为空 +baseModelMBean.nullListener=监å¬å™¨ä¸ºç©º +baseModelMBean.nullMessage=消æ¯ä¸ºç©º +baseModelMBean.nullMethodName=方法å为空 +baseModelMBean.nullNotification=通知为空 +baseModelMBean.nullResource=托管资æºä¸ºç©º + +managedMBean.cannotInstantiateClass=无法实例化类[{0}]çš„ModelMBean +managedMBean.cannotLoadClass=无法加载ModelMBeanç±»[{0}] +managedMBean.inconsistentArguments=å‚数和签åä¸ä¸€è‡´ +managedMBean.noAttribute=在资æº[{1}]上找ä¸åˆ°å±žæ€§[{0}] +managedMBean.noGet=在资æº[{1}]上找ä¸åˆ°getter方法[{0}] +managedMBean.noMethod=找ä¸åˆ°å…·æœ‰æ­¤ç­¾å的方法[{0}] +managedMBean.noOperation=找ä¸åˆ°æ“作[{0}] +managedMBean.noSet=在资æº[{1}]上找ä¸åˆ°setter方法[{0}] + +modules.digesterParseError=分æžæ³¨å†Œè¡¨æ•°æ®æ—¶å‡ºé”™ +modules.readDescriptorsError=读å–æ述文件出错 + +registry.initError=åˆå§‹åŒ–[{0}]时出错 +registry.loadError=从[{0}]加载æ述符时出错 +registry.noDisable=无法ç¦ç”¨MBean注册表,因为它已åˆå§‹åŒ– +registry.noMetadata=找ä¸åˆ°å¯¹è±¡[{0}]çš„å…ƒæ•°æ® +registry.noTypeMetadata=找ä¸åˆ°ç±»åž‹[{0}]çš„å…ƒæ•°æ® +registry.nullBean=无法为[{0}]注册空bean +registry.objectNameCreateError=创建对象å时出错 +registry.registerError=注册 MBean 出错 +registry.unregisterError=注销MBean时出错 diff --git a/java/org/apache/tomcat/util/modeler/ManagedBean.java b/java/org/apache/tomcat/util/modeler/ManagedBean.java new file mode 100644 index 0000000..807281f --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/ManagedBean.java @@ -0,0 +1,581 @@ +/* + * 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.tomcat.util.modeler; + + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanConstructorInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanNotificationInfo; +import javax.management.MBeanOperationInfo; +import javax.management.ReflectionException; +import javax.management.RuntimeOperationsException; +import javax.management.ServiceNotFoundException; + +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.res.StringManager; + + +/** + *

    Internal configuration information for a managed bean (MBean) + * descriptor.

    + * + * @author Craig R. McClanahan + */ +public class ManagedBean implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + private static final StringManager sm = StringManager.getManager(ManagedBean.class); + + private static final String BASE_MBEAN = "org.apache.tomcat.util.modeler.BaseModelMBean"; + // ----------------------------------------------------- Instance Variables + static final Class[] NO_ARGS_PARAM_SIG = new Class[0]; + + + private final ReadWriteLock mBeanInfoLock = new ReentrantReadWriteLock(); + /** + * The ModelMBeanInfo object that corresponds + * to this ManagedBean instance. + */ + private transient volatile MBeanInfo info = null; + + private Map attributes = new HashMap<>(); + + private Map operations = new HashMap<>(); + + protected String className = BASE_MBEAN; + protected String description = null; + protected String domain = null; + protected String group = null; + protected String name = null; + + private NotificationInfo notifications[] = new NotificationInfo[0]; + protected String type = null; + + /** + * Constructor. Will add default attributes. + */ + public ManagedBean() { + AttributeInfo ai=new AttributeInfo(); + ai.setName("modelerType"); + ai.setDescription("Type of the modeled resource. Can be set only once"); + ai.setType("java.lang.String"); + ai.setWriteable(false); + addAttribute(ai); + } + + // ------------------------------------------------------------- Properties + + + /** + * @return the collection of attributes for this MBean. + */ + public AttributeInfo[] getAttributes() { + return attributes.values().toArray(new AttributeInfo[0]); + } + + + /** + * The fully qualified name of the Java class of the MBean + * described by this descriptor. If not specified, the standard JMX + * class (javax.management.modelmbean.RequiredModeLMBean) + * will be utilized. + * @return the class name + */ + public String getClassName() { + return this.className; + } + + public void setClassName(String className) { + mBeanInfoLock.writeLock().lock(); + try { + this.className = className; + this.info = null; + } finally { + mBeanInfoLock.writeLock().unlock(); + } + } + + + /** + * @return the human-readable description of this MBean. + */ + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + mBeanInfoLock.writeLock().lock(); + try { + this.description = description; + this.info = null; + } finally { + mBeanInfoLock.writeLock().unlock(); + } + } + + + /** + * @return the (optional) ObjectName domain in which + * this MBean should be registered in the MBeanServer. + */ + public String getDomain() { + return this.domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + + /** + * @return the (optional) group to which this MBean belongs. + */ + public String getGroup() { + return this.group; + } + + public void setGroup(String group) { + this.group = group; + } + + + /** + * @return the name of this managed bean, which must be unique + * among all MBeans managed by a particular MBeans server. + */ + public String getName() { + return this.name; + } + + public void setName(String name) { + mBeanInfoLock.writeLock().lock(); + try { + this.name = name; + this.info = null; + } finally { + mBeanInfoLock.writeLock().unlock(); + } + } + + + /** + * @return the collection of notifications for this MBean. + */ + public NotificationInfo[] getNotifications() { + return this.notifications; + } + + + /** + * @return the collection of operations for this MBean. + */ + public OperationInfo[] getOperations() { + return operations.values().toArray(new OperationInfo[0]); + } + + + /** + * @return the fully qualified name of the Java class of the resource + * implementation class described by the managed bean described + * by this descriptor. + */ + public String getType() { + return this.type; + } + + public void setType(String type) { + mBeanInfoLock.writeLock().lock(); + try { + this.type = type; + this.info = null; + } finally { + mBeanInfoLock.writeLock().unlock(); + } + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new attribute to the set of attributes for this MBean. + * + * @param attribute The new attribute descriptor + */ + public void addAttribute(AttributeInfo attribute) { + attributes.put(attribute.getName(), attribute); + } + + + /** + * Add a new notification to the set of notifications for this MBean. + * + * @param notification The new notification descriptor + */ + public void addNotification(NotificationInfo notification) { + mBeanInfoLock.writeLock().lock(); + try { + NotificationInfo results[] = + new NotificationInfo[notifications.length + 1]; + System.arraycopy(notifications, 0, results, 0, + notifications.length); + results[notifications.length] = notification; + notifications = results; + this.info = null; + } finally { + mBeanInfoLock.writeLock().unlock(); + } + } + + + /** + * Add a new operation to the set of operations for this MBean. + * + * @param operation The new operation descriptor + */ + public void addOperation(OperationInfo operation) { + operations.put(createOperationKey(operation), operation); + } + + + /** + * Create and return a ModelMBean that has been + * preconfigured with the ModelMBeanInfo information + * for this managed bean, and is associated with the specified + * managed object instance. The returned ModelMBean + * will NOT have been registered with our + * MBeanServer. + * + * @param instance Instanced of the managed object, or null + * for no associated instance + * @return the MBean + * @exception InstanceNotFoundException if the managed resource + * object cannot be found + * @exception MBeanException if a problem occurs instantiating the + * ModelMBean instance + * @exception RuntimeOperationsException if a JMX runtime error occurs + */ + public DynamicMBean createMBean(Object instance) + throws InstanceNotFoundException, + MBeanException, RuntimeOperationsException { + + BaseModelMBean mbean = null; + + // Load the ModelMBean implementation class + if(getClassName().equals(BASE_MBEAN)) { + // Skip introspection + mbean = new BaseModelMBean(); + } else { + Class clazz = null; + Exception ex = null; + try { + clazz = Class.forName(getClassName()); + } catch (Exception e) { + } + + if( clazz==null ) { + try { + ClassLoader cl= Thread.currentThread().getContextClassLoader(); + if ( cl != null) { + clazz= cl.loadClass(getClassName()); + } + } catch (Exception e) { + ex=e; + } + } + + if( clazz==null) { + throw new MBeanException + (ex, sm.getString("managedMBean.cannotLoadClass", getClassName())); + } + try { + // Stupid - this will set the default minfo first.... + mbean = (BaseModelMBean) clazz.getConstructor().newInstance(); + } catch (RuntimeOperationsException e) { + throw e; + } catch (Exception e) { + throw new MBeanException + (e, sm.getString("managedMBean.cannotInstantiateClass", getClassName())); + } + } + + mbean.setManagedBean(this); + + // Set the managed resource (if any) + try { + if (instance != null) { + mbean.setManagedResource(instance, "ObjectReference"); + } + } catch (InstanceNotFoundException e) { + throw e; + } + + return mbean; + } + + + /** + * Create and return a ModelMBeanInfo object that + * describes this entire managed bean. + * @return the MBean info + */ + MBeanInfo getMBeanInfo() { + + // Return our cached information (if any) + mBeanInfoLock.readLock().lock(); + try { + if (info != null) { + return info; + } + } finally { + mBeanInfoLock.readLock().unlock(); + } + + mBeanInfoLock.writeLock().lock(); + try { + if (info == null) { + // Create subordinate information descriptors as required + AttributeInfo attrs[] = getAttributes(); + MBeanAttributeInfo attributes[] = + new MBeanAttributeInfo[attrs.length]; + for (int i = 0; i < attrs.length; i++) { + attributes[i] = attrs[i].createAttributeInfo(); + } + + OperationInfo opers[] = getOperations(); + MBeanOperationInfo operations[] = + new MBeanOperationInfo[opers.length]; + for (int i = 0; i < opers.length; i++) { + operations[i] = opers[i].createOperationInfo(); + } + + + NotificationInfo notifs[] = getNotifications(); + MBeanNotificationInfo notifications[] = + new MBeanNotificationInfo[notifs.length]; + for (int i = 0; i < notifs.length; i++) { + notifications[i] = notifs[i].createNotificationInfo(); + } + + + // Construct and return a new ModelMBeanInfo object + info = new MBeanInfo(getClassName(), + getDescription(), + attributes, + new MBeanConstructorInfo[] {}, + operations, + notifications); + } + + return info; + } finally { + mBeanInfoLock.writeLock().unlock(); + } + } + + + /** + * Return a string representation of this managed bean. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("ManagedBean["); + sb.append("name="); + sb.append(name); + sb.append(", className="); + sb.append(className); + sb.append(", description="); + sb.append(description); + if (group != null) { + sb.append(", group="); + sb.append(group); + } + sb.append(", type="); + sb.append(type); + sb.append(']'); + return sb.toString(); + + } + + Method getGetter(String aname, BaseModelMBean mbean, Object resource) + throws AttributeNotFoundException, ReflectionException { + + Method m = null; + + AttributeInfo attrInfo = attributes.get(aname); + // Look up the actual operation to be used + if (attrInfo == null) { + throw new AttributeNotFoundException(sm.getString("managedMBean.noAttribute", aname, resource)); + } + + String getMethod = attrInfo.getGetMethod(); + + Object object = null; + NoSuchMethodException exception = null; + try { + object = mbean; + m = object.getClass().getMethod(getMethod, NO_ARGS_PARAM_SIG); + } catch (NoSuchMethodException e) { + exception = e; + } + if (m == null && resource != null) { + try { + object = resource; + m = object.getClass().getMethod(getMethod, NO_ARGS_PARAM_SIG); + exception=null; + } catch (NoSuchMethodException e) { + exception = e; + } + } + if (exception != null) { + throw new ReflectionException(exception, sm.getString("managedMBean.noGet", getMethod, resource)); + } + + return m; + } + + public Method getSetter(String aname, BaseModelMBean bean, Object resource) + throws AttributeNotFoundException, ReflectionException { + + Method m = null; + + AttributeInfo attrInfo = attributes.get(aname); + if (attrInfo == null) { + throw new AttributeNotFoundException(sm.getString("managedMBean.noAttribute", aname, resource)); + } + + // Look up the actual operation to be used + String setMethod = attrInfo.getSetMethod(); + String argType=attrInfo.getType(); + + Class signature[] = new Class[] { BaseModelMBean.getAttributeClass( argType ) }; + + Object object = null; + NoSuchMethodException exception = null; + try { + object = bean; + m = object.getClass().getMethod(setMethod, signature); + } catch (NoSuchMethodException e) { + exception = e; + } + if (m == null && resource != null) { + try { + object = resource; + m = object.getClass().getMethod(setMethod, signature); + exception=null; + } catch (NoSuchMethodException e) { + exception = e; + } + } + if (exception != null) { + throw new ReflectionException(exception, sm.getString("managedMBean.noSet", setMethod, resource)); + } + + return m; + } + + public Method getInvoke(String aname, Object[] params, String[] signature, BaseModelMBean bean, Object resource) + throws MBeanException, ReflectionException { + + Method method = null; + + if (params == null) { + params = new Object[0]; + } + if (signature == null) { + signature = new String[0]; + } + if (params.length != signature.length) { + throw new RuntimeOperationsException( + new IllegalArgumentException(sm.getString("managedMBean.inconsistentArguments")), + sm.getString("managedMBean.inconsistentArguments")); + } + + // Acquire the ModelMBeanOperationInfo information for + // the requested operation + OperationInfo opInfo = + operations.get(createOperationKey(aname, signature)); + if (opInfo == null) { + throw new MBeanException(new ServiceNotFoundException(sm.getString("managedMBean.noOperation", aname)), + sm.getString("managedMBean.noOperation", aname)); + } + + // Prepare the signature required by Java reflection APIs + // FIXME - should we use the signature from opInfo? + Class types[] = new Class[signature.length]; + for (int i = 0; i < signature.length; i++) { + types[i] = BaseModelMBean.getAttributeClass(signature[i]); + } + + // Locate the method to be invoked, either in this MBean itself + // or in the corresponding managed resource + // FIXME - Accessible methods in superinterfaces? + Object object = null; + Exception exception = null; + try { + object = bean; + method = object.getClass().getMethod(aname, types); + } catch (NoSuchMethodException e) { + exception = e; + } + try { + if ((method == null) && (resource != null)) { + object = resource; + method = object.getClass().getMethod(aname, types); + } + } catch (NoSuchMethodException e) { + exception = e; + } + if (method == null) { + throw new ReflectionException(exception, sm.getString("managedMBean.noMethod", aname)); + } + + return method; + } + + + private String createOperationKey(OperationInfo operation) { + StringBuilder key = new StringBuilder(operation.getName()); + key.append('('); + StringUtils.join(operation.getSignature(), ',', FeatureInfo::getType, key); + key.append(')'); + + return key.toString().intern(); + } + + + private String createOperationKey(String methodName, String[] parameterTypes) { + StringBuilder key = new StringBuilder(methodName); + key.append('('); + StringUtils.join(parameterTypes, ',', key); + key.append(')'); + + return key.toString().intern(); + } +} diff --git a/java/org/apache/tomcat/util/modeler/NoDescriptorRegistry.java b/java/org/apache/tomcat/util/modeler/NoDescriptorRegistry.java new file mode 100644 index 0000000..33c5d97 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/NoDescriptorRegistry.java @@ -0,0 +1,407 @@ +/* + * 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.tomcat.util.modeler; + +import java.io.ObjectInputStream; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceAlreadyExistsException; +import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; +import javax.management.InvalidAttributeValueException; +import javax.management.ListenerNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.NotCompliantMBeanException; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.OperationsException; +import javax.management.QueryExp; +import javax.management.ReflectionException; +import javax.management.loading.ClassLoaderRepository; + +/** + * An implementation of the MBean registry that effectively disables MBean + * registration. This is typically used when low memory footprint is a primary + * concern. + */ +public class NoDescriptorRegistry extends Registry { + + private final MBeanServer mBeanServer = new NoJmxMBeanServer(); + private final ManagedBean defaultMBean = new PassthroughMBean(); + + + @Override + public void registerComponent(final Object bean, final String oname, final String type) + throws Exception { + // no-op + } + + + @Override + public void unregisterComponent(final String oname) { + // no-op + } + + + @Override + public void invoke(final List mbeans, final String operation, + final boolean failFirst) throws Exception { + // no-op + } + + + @SuppressWarnings("sync-override") + @Override + public int getId(final String domain, final String name) { + return 0; + } + + + @Override + public void addManagedBean(final ManagedBean bean) { + // no-op + } + + + @Override + public ManagedBean findManagedBean(final String name) { + return defaultMBean; + } + + + @Override + public String getType(final ObjectName oname, final String attName) { + return null; + } + + + @Override + public MBeanOperationInfo getMethodInfo(final ObjectName oname, final String opName) { + return null; + } + + + @Override + public ManagedBean findManagedBean(final Object bean, final Class beanClass, + final String type) throws Exception { + return null; + } + + + @Override + public List load(final String sourceType, final Object source, final String param) + throws Exception { + return Collections.emptyList(); + } + + + @Override + public void loadDescriptors(final String packageName, final ClassLoader classLoader) { + // no-op + } + + + @Override + public void registerComponent(final Object bean, final ObjectName oname, final String type) + throws Exception { + // no-op + } + + + @Override + public void unregisterComponent(final ObjectName oname) { + // no-op + } + + + @Override + public MBeanServer getMBeanServer() { + return mBeanServer; + } + + private static class NoJmxMBeanServer implements MBeanServer { + + @Override + public ObjectInstance createMBean(String className, ObjectName name) + throws ReflectionException, InstanceAlreadyExistsException, + MBeanRegistrationException, NotCompliantMBeanException, MBeanRegistrationException { + return null; + } + + + @Override + public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName) + throws ReflectionException, InstanceAlreadyExistsException, + MBeanRegistrationException, NotCompliantMBeanException, InstanceNotFoundException, + MBeanRegistrationException { + return null; + } + + + @Override + public ObjectInstance createMBean(String className, ObjectName name, Object[] params, + String[] signature) throws ReflectionException, InstanceAlreadyExistsException, + MBeanRegistrationException, NotCompliantMBeanException, MBeanRegistrationException { + return null; + } + + + @Override + public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName, + Object[] params, String[] signature) throws ReflectionException, + InstanceAlreadyExistsException, MBeanRegistrationException, + NotCompliantMBeanException, InstanceNotFoundException, MBeanRegistrationException { + return null; + } + + + @Override + public ObjectInstance registerMBean(Object object, ObjectName name) + throws InstanceAlreadyExistsException, MBeanRegistrationException, + NotCompliantMBeanException { + return null; + } + + + @Override + public void unregisterMBean(ObjectName name) + throws InstanceNotFoundException, MBeanRegistrationException { + + } + + + @Override + public ObjectInstance getObjectInstance(ObjectName name) throws InstanceNotFoundException { + return null; + } + + + @Override + public Set queryMBeans(ObjectName name, QueryExp query) { + return Collections.emptySet(); + } + + + @Override + public Set queryNames(ObjectName name, QueryExp query) { + return Collections.emptySet(); + } + + + @Override + public boolean isRegistered(ObjectName name) { + return false; + } + + + @Override + public Integer getMBeanCount() { + return null; + } + + + @Override + public Object getAttribute(ObjectName name, String attribute) throws MBeanException, + AttributeNotFoundException, InstanceNotFoundException, ReflectionException { + return null; + } + + + @Override + public AttributeList getAttributes(ObjectName name, String[] attributes) + throws InstanceNotFoundException, ReflectionException { + return null; + } + + + @Override + public void setAttribute(ObjectName name, Attribute attribute) + throws InstanceNotFoundException, AttributeNotFoundException, + InvalidAttributeValueException, MBeanException, ReflectionException { + + } + + + @Override + public AttributeList setAttributes(ObjectName name, AttributeList attributes) + throws InstanceNotFoundException, ReflectionException { + return null; + } + + + @Override + public Object invoke(ObjectName name, String operationName, Object[] params, + String[] signature) + throws InstanceNotFoundException, MBeanException, ReflectionException { + return null; + } + + + @Override + public String getDefaultDomain() { + return null; + } + + + @Override + public String[] getDomains() { + return new String[0]; + } + + + @Override + public void addNotificationListener(ObjectName name, NotificationListener listener, + NotificationFilter filter, Object handback) throws InstanceNotFoundException { + + } + + + @Override + public void addNotificationListener(ObjectName name, ObjectName listener, + NotificationFilter filter, Object handback) throws InstanceNotFoundException { + + } + + + @Override + public void removeNotificationListener(ObjectName name, ObjectName listener) + throws InstanceNotFoundException, ListenerNotFoundException { + + } + + + @Override + public void removeNotificationListener(ObjectName name, ObjectName listener, + NotificationFilter filter, Object handback) + throws InstanceNotFoundException, ListenerNotFoundException { + + } + + + @Override + public void removeNotificationListener(ObjectName name, NotificationListener listener) + throws InstanceNotFoundException, ListenerNotFoundException { + + } + + + @Override + public void removeNotificationListener(ObjectName name, NotificationListener listener, + NotificationFilter filter, Object handback) + throws InstanceNotFoundException, ListenerNotFoundException { + + } + + + @Override + public MBeanInfo getMBeanInfo(ObjectName name) + throws InstanceNotFoundException, IntrospectionException, ReflectionException { + return null; + } + + + @Override + public boolean isInstanceOf(ObjectName name, String className) + throws InstanceNotFoundException { + return false; + } + + + @Override + public Object instantiate(String className) throws ReflectionException, MBeanException { + return null; + } + + + @Override + public Object instantiate(String className, ObjectName loaderName) + throws ReflectionException, MBeanException, InstanceNotFoundException { + return null; + } + + + @Override + public Object instantiate(String className, Object[] params, String[] signature) + throws ReflectionException, MBeanException { + return null; + } + + + @Override + public Object instantiate(String className, ObjectName loaderName, Object[] params, + String[] signature) + throws ReflectionException, MBeanException, InstanceNotFoundException { + return null; + } + + + @Override + public ObjectInputStream deserialize(ObjectName name, byte[] data) + throws InstanceNotFoundException, OperationsException { + return null; + } + + + @Override + public ObjectInputStream deserialize(String className, byte[] data) + throws OperationsException, ReflectionException { + return null; + } + + + @Override + public ObjectInputStream deserialize(String className, ObjectName loaderName, byte[] data) + throws InstanceNotFoundException, OperationsException, ReflectionException { + return null; + } + + + @Override + public ClassLoader getClassLoaderFor(ObjectName mbeanName) + throws InstanceNotFoundException { + return null; + } + + + @Override + public ClassLoader getClassLoader(ObjectName loaderName) throws InstanceNotFoundException { + return null; + } + + + @Override + public ClassLoaderRepository getClassLoaderRepository() { + return null; + } + } + + private static class PassthroughMBean extends ManagedBean { + + private static final long serialVersionUID = 1L; + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/modeler/NotificationInfo.java b/java/org/apache/tomcat/util/modeler/NotificationInfo.java new file mode 100644 index 0000000..c5c78fb --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/NotificationInfo.java @@ -0,0 +1,157 @@ +/* + * 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.tomcat.util.modeler; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.MBeanNotificationInfo; + +/** + *

    Internal configuration information for a Notification + * descriptor.

    + * + * @author Craig R. McClanahan + */ +public class NotificationInfo extends FeatureInfo { + + private static final long serialVersionUID = -6319885418912650856L; + + // ----------------------------------------------------- Instance Variables + + + /** + * The ModelMBeanNotificationInfo object that corresponds + * to this NotificationInfo instance. + */ + transient MBeanNotificationInfo info = null; + protected String notifTypes[] = new String[0]; + protected final ReadWriteLock notifTypesLock = new ReentrantReadWriteLock(); + + // ------------------------------------------------------------- Properties + + + /** + * Override the description property setter. + * + * @param description The new description + */ + @Override + public void setDescription(String description) { + super.setDescription(description); + this.info = null; + } + + + /** + * Override the name property setter. + * + * @param name The new name + */ + @Override + public void setName(String name) { + super.setName(name); + this.info = null; + } + + + /** + * @return the set of notification types for this MBean. + */ + public String[] getNotifTypes() { + Lock readLock = notifTypesLock.readLock(); + readLock.lock(); + try { + return this.notifTypes; + } finally { + readLock.unlock(); + } + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new notification type to the set managed by an MBean. + * + * @param notifType The new notification type + */ + public void addNotifType(String notifType) { + + Lock writeLock = notifTypesLock.writeLock(); + writeLock.lock(); + try { + + String results[] = new String[notifTypes.length + 1]; + System.arraycopy(notifTypes, 0, results, 0, notifTypes.length); + results[notifTypes.length] = notifType; + notifTypes = results; + this.info = null; + } finally { + writeLock.unlock(); + } + } + + + /** + * Create and return a ModelMBeanNotificationInfo object that + * corresponds to the attribute described by this instance. + * @return the notification info + */ + public MBeanNotificationInfo createNotificationInfo() { + + // Return our cached information (if any) + if (info != null) { + return info; + } + + // Create and return a new information object + info = new MBeanNotificationInfo + (getNotifTypes(), getName(), getDescription()); + //Descriptor descriptor = info.getDescriptor(); + //addFields(descriptor); + //info.setDescriptor(descriptor); + return info; + + } + + + /** + * Return a string representation of this notification descriptor. + */ + @Override + public String toString() { + + StringBuilder sb = new StringBuilder("NotificationInfo["); + sb.append("name="); + sb.append(name); + sb.append(", description="); + sb.append(description); + sb.append(", notifTypes="); + Lock readLock = notifTypesLock.readLock(); + readLock.lock(); + try { + sb.append(notifTypes.length); + } finally { + readLock.unlock(); + } + sb.append(']'); + return sb.toString(); + } +} diff --git a/java/org/apache/tomcat/util/modeler/OperationInfo.java b/java/org/apache/tomcat/util/modeler/OperationInfo.java new file mode 100644 index 0000000..c5dbdee --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/OperationInfo.java @@ -0,0 +1,175 @@ +/* + * 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.tomcat.util.modeler; + +import java.util.Locale; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; + +/** + *

    Internal configuration information for an Operation + * descriptor.

    + * + * @author Craig R. McClanahan + */ +public class OperationInfo extends FeatureInfo { + + private static final long serialVersionUID = 4418342922072614875L; + + // ----------------------------------------------------------- Constructors + + /** + * Standard zero-arguments constructor. + */ + public OperationInfo() { + super(); + } + + + // ----------------------------------------------------- Instance Variables + + protected String impact = "UNKNOWN"; + protected String role = "operation"; + protected final ReadWriteLock parametersLock = new ReentrantReadWriteLock(); + protected ParameterInfo parameters[] = new ParameterInfo[0]; + + + // ------------------------------------------------------------- Properties + + /** + * @return the "impact" of this operation, which should be + * a (case-insensitive) string value "ACTION", "ACTION_INFO", + * "INFO", or "UNKNOWN". + */ + public String getImpact() { + return this.impact; + } + + public void setImpact(String impact) { + if (impact == null) { + this.impact = null; + } else { + this.impact = impact.toUpperCase(Locale.ENGLISH); + } + } + + + /** + * @return the role of this operation ("getter", "setter", "operation", or + * "constructor"). + */ + public String getRole() { + return this.role; + } + + public void setRole(String role) { + this.role = role; + } + + + /** + * @return the fully qualified Java class name of the return type for this + * operation. + */ + public String getReturnType() { + if(type == null) { + type = "void"; + } + return type; + } + + public void setReturnType(String returnType) { + this.type = returnType; + } + + /** + * @return the set of parameters for this operation. + */ + public ParameterInfo[] getSignature() { + Lock readLock = parametersLock.readLock(); + readLock.lock(); + try { + return this.parameters; + } finally { + readLock.unlock(); + } + } + + // --------------------------------------------------------- Public Methods + + + /** + * Add a new parameter to the set of arguments for this operation. + * + * @param parameter The new parameter descriptor + */ + public void addParameter(ParameterInfo parameter) { + + Lock writeLock = parametersLock.writeLock(); + writeLock.lock(); + try { + ParameterInfo results[] = new ParameterInfo[parameters.length + 1]; + System.arraycopy(parameters, 0, results, 0, parameters.length); + results[parameters.length] = parameter; + parameters = results; + this.info = null; + } finally { + writeLock.unlock(); + } + } + + + /** + * Create and return a ModelMBeanOperationInfo object that + * corresponds to the attribute described by this instance. + * @return the operation info + */ + MBeanOperationInfo createOperationInfo() { + + // Return our cached information (if any) + if (info == null) { + // Create and return a new information object + int impact = MBeanOperationInfo.UNKNOWN; + if ("ACTION".equals(getImpact())) { + impact = MBeanOperationInfo.ACTION; + } else if ("ACTION_INFO".equals(getImpact())) { + impact = MBeanOperationInfo.ACTION_INFO; + } else if ("INFO".equals(getImpact())) { + impact = MBeanOperationInfo.INFO; + } + + info = new MBeanOperationInfo(getName(), getDescription(), + getMBeanParameterInfo(), + getReturnType(), impact); + } + return (MBeanOperationInfo)info; + } + + protected MBeanParameterInfo[] getMBeanParameterInfo() { + ParameterInfo params[] = getSignature(); + MBeanParameterInfo parameters[] = + new MBeanParameterInfo[params.length]; + for (int i = 0; i < params.length; i++) { + parameters[i] = params[i].createParameterInfo(); + } + return parameters; + } +} diff --git a/java/org/apache/tomcat/util/modeler/ParameterInfo.java b/java/org/apache/tomcat/util/modeler/ParameterInfo.java new file mode 100644 index 0000000..5cbad02 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/ParameterInfo.java @@ -0,0 +1,55 @@ +/* + * 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.tomcat.util.modeler; + + +import javax.management.MBeanParameterInfo; + + +/** + *

    Internal configuration information for a Parameter + * descriptor.

    + * + * @author Craig R. McClanahan + */ +public class ParameterInfo extends FeatureInfo { + private static final long serialVersionUID = 2222796006787664020L; + // ----------------------------------------------------------- Constructors + + + /** + * Standard zero-arguments constructor. + */ + public ParameterInfo() { + super(); + } + + /** + * Create and return a MBeanParameterInfo object that + * corresponds to the parameter described by this instance. + * @return a parameter info + */ + public MBeanParameterInfo createParameterInfo() { + + // Return our cached information (if any) + if (info == null) { + info = new MBeanParameterInfo + (getName(), getType(), getDescription()); + } + return (MBeanParameterInfo)info; + } +} diff --git a/java/org/apache/tomcat/util/modeler/Registry.java b/java/org/apache/tomcat/util/modeler/Registry.java new file mode 100644 index 0000000..758b8d5 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/Registry.java @@ -0,0 +1,749 @@ +/* + * 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.tomcat.util.modeler; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.lang.management.ManagementFactory; +import java.net.URL; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import javax.management.DynamicMBean; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanRegistration; +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.modeler.modules.ModelerSource; +import org.apache.tomcat.util.res.StringManager; + +/** + * Registry for modeler MBeans. + * + * This is the main entry point into modeler. It provides methods to create and + * manipulate model mbeans and simplify their use. + * + * This class is itself an mbean. + * + * @author Craig R. McClanahan + * @author Costin Manolache + */ +public class Registry implements RegistryMBean, MBeanRegistration { + + /** + * The Log instance to which we will write our log messages. + */ + private static final Log log = LogFactory.getLog(Registry.class); + private static final StringManager sm = StringManager.getManager(Registry.class); + + // Support for the factory methods + + /** + * The registry instance created by our factory method the first time it is + * called. + */ + private static Registry registry = null; + + // Per registry fields + + /** + * The MBeanServer instance that we will use to register + * management beans. + */ + private volatile MBeanServer server = null; + private final Object serverLock = new Object(); + + /** + * The set of ManagedBean instances for the beans this registry knows about, + * keyed by name. + */ + private Map descriptors = new HashMap<>(); + + /** + * List of managed beans, keyed by class name + */ + private Map descriptorsByClass = new HashMap<>(); + + // map to avoid duplicated searching or loading descriptors + private Map searchedPaths = new HashMap<>(); + + private Object guard; + + // Id - small ints to use array access. No reset on stop() + // Used for notifications + private final Hashtable> idDomains = new Hashtable<>(); + private final Hashtable ids = new Hashtable<>(); + + + // ----------------------------------------------------------- Constructors + + protected Registry() { + super(); + } + + + // -------------------- Static methods -------------------- + // Factories + + /** + * Factory method to create (if necessary) and return our + * Registry instance. + * + * @param key Unused + * @param guard Prevent access to the registry by untrusted components + * + * @return the registry + * @since 1.1 + */ + public static synchronized Registry getRegistry(Object key, Object guard) { + if (registry == null) { + registry = new Registry(); + registry.guard = guard; + } + if (registry.guard != null && registry.guard != guard) { + return null; + } + return registry; + } + + + public static synchronized void disableRegistry() { + if (registry == null) { + registry = new NoDescriptorRegistry(); + } else if (!(registry instanceof NoDescriptorRegistry)) { + log.warn(sm.getString("registry.noDisable")); + } + } + + + // -------------------- Generic methods -------------------- + + /** + * Lifecycle method - clean up the registry metadata. Called from + * resetMetadata(). + * + * @since 1.1 + */ + @Override + public void stop() { + descriptorsByClass = new HashMap<>(); + descriptors = new HashMap<>(); + searchedPaths = new HashMap<>(); + } + + + /** + * Register a bean by creating a modeler mbean and adding it to the + * MBeanServer. + * + * If metadata is not loaded, we'll look up and read a file named + * "mbeans-descriptors.ser" or "mbeans-descriptors.xml" in the same package + * or parent. + * + * If the bean is an instance of DynamicMBean. it's metadata will be + * converted to a model mbean and we'll wrap it - so modeler services will + * be supported + * + * If the metadata is still not found, introspection will be used to extract + * it automatically. + * + * If an mbean is already registered under this name, it'll be first + * unregistered. + * + * If the component implements MBeanRegistration, the methods will be + * called. If the method has a method "setRegistry" that takes a + * RegistryMBean as parameter, it'll be called with the current registry. + * + * + * @param bean Object to be registered + * @param oname Name used for registration + * @param type The type of the mbean, as declared in mbeans-descriptors. If + * null, the name of the class will be used. This can be used as + * a hint or by subclasses. + * @throws Exception if a registration error occurred + * @since 1.1 + */ + @Override + public void registerComponent(Object bean, String oname, String type) throws Exception { + registerComponent(bean, new ObjectName(oname), type); + } + + + /** + * Unregister a component. We'll first check if it is registered, and mask + * all errors. This is mostly a helper. + * + * @param oname Name used for unregistration + * + * @since 1.1 + */ + @Override + public void unregisterComponent(String oname) { + try { + unregisterComponent(new ObjectName(oname)); + } catch (MalformedObjectNameException e) { + log.info(sm.getString("registry.objectNameCreateError"), e); + } + } + + + /** + * Invoke a operation on a list of mbeans. Can be used to implement + * lifecycle operations. + * + * @param mbeans list of ObjectName on which we'll invoke the operations + * @param operation Name of the operation ( init, start, stop, etc) + * @param failFirst If false, exceptions will be ignored + * @throws Exception Error invoking operation + * @since 1.1 + */ + @Override + public void invoke(List mbeans, String operation, boolean failFirst) + throws Exception { + + if (mbeans == null) { + return; + } + for (ObjectName current : mbeans) { + try { + if (current == null) { + continue; + } + if (getMethodInfo(current, operation) == null) { + continue; + } + getMBeanServer().invoke(current, operation, new Object[] {}, new String[] {}); + + } catch (Exception t) { + if (failFirst) { + throw t; + } + log.info(sm.getString("registry.initError"), t); + } + } + } + + // -------------------- ID registry -------------------- + + /** + * Return an int ID for faster access. Will be used for notifications and + * for other operations we want to optimize. + * + * @param domain Namespace + * @param name Type of the notification + * @return A unique id for the domain:name combination + * @since 1.1 + */ + @Override + public synchronized int getId(String domain, String name) { + if (domain == null) { + domain = ""; + } + Hashtable domainTable = idDomains.computeIfAbsent(domain, k -> new Hashtable<>()); + if (name == null) { + name = ""; + } + Integer i = domainTable.get(name); + + if (i != null) { + return i.intValue(); + } + + int[] id = ids.computeIfAbsent(domain, k -> new int[1]); + int code = id[0]++; + domainTable.put(name, Integer.valueOf(code)); + return code; + } + + + // -------------------- Metadata -------------------- + // methods from 1.0 + + /** + * Add a new bean metadata to the set of beans known to this registry. This + * is used by internal components. + * + * @param bean The managed bean to be added + * @since 1.0 + */ + public void addManagedBean(ManagedBean bean) { + // XXX Use group + name + descriptors.put(bean.getName(), bean); + if (bean.getType() != null) { + descriptorsByClass.put(bean.getType(), bean); + } + } + + + /** + * Find and return the managed bean definition for the specified bean name, + * if any; otherwise return null. + * + * @param name Name of the managed bean to be returned. Since 1.1, both + * short names or the full name of the class can be used. + * @return the managed bean + * @since 1.0 + */ + public ManagedBean findManagedBean(String name) { + // XXX Group ?? Use Group + Type + ManagedBean mb = descriptors.get(name); + if (mb == null) { + mb = descriptorsByClass.get(name); + } + return mb; + } + + + // -------------------- Helpers -------------------- + + /** + * Get the type of an attribute of the object, from the metadata. + * + * @param oname The bean name + * @param attName The attribute name + * @return null if metadata about the attribute is not found + * @since 1.1 + */ + public String getType(ObjectName oname, String attName) { + String type = null; + MBeanInfo info = null; + try { + info = getMBeanServer().getMBeanInfo(oname); + } catch (Exception e) { + log.info(sm.getString("registry.noMetadata", oname)); + return null; + } + + MBeanAttributeInfo attInfo[] = info.getAttributes(); + for (MBeanAttributeInfo mBeanAttributeInfo : attInfo) { + if (attName.equals(mBeanAttributeInfo.getName())) { + type = mBeanAttributeInfo.getType(); + return type; + } + } + return null; + } + + + /** + * Find the operation info for a method + * + * @param oname The bean name + * @param opName The operation name + * @return the operation info for the specified operation + */ + public MBeanOperationInfo getMethodInfo(ObjectName oname, String opName) { + MBeanInfo info = null; + try { + info = getMBeanServer().getMBeanInfo(oname); + } catch (Exception e) { + log.info(sm.getString("registry.noMetadata", oname)); + return null; + } + MBeanOperationInfo attInfo[] = info.getOperations(); + for (MBeanOperationInfo mBeanOperationInfo : attInfo) { + if (opName.equals(mBeanOperationInfo.getName())) { + return mBeanOperationInfo; + } + } + return null; + } + + /** + * Find the operation info for a method. + * + * @param oname The bean name + * @param opName The operation name + * @param argCount The number of arguments to the method + * @return the operation info for the specified operation + * @throws InstanceNotFoundException If the object name is not bound to an MBean + */ + public MBeanOperationInfo getMethodInfo(ObjectName oname, String opName, int argCount) + throws InstanceNotFoundException + { + MBeanInfo info = null; + try { + info = getMBeanServer().getMBeanInfo(oname); + } catch (InstanceNotFoundException infe) { + throw infe; + } catch (Exception e) { + log.warn(sm.getString("registry.noMetadata", oname), e); + return null; + } + MBeanOperationInfo attInfo[] = info.getOperations(); + for (MBeanOperationInfo mBeanOperationInfo : attInfo) { + if (opName.equals(mBeanOperationInfo.getName()) + && argCount == mBeanOperationInfo.getSignature().length) { + return mBeanOperationInfo; + } + } + return null; + } + + /** + * Unregister a component. This is just a helper that avoids exceptions by + * checking if the mbean is already registered + * + * @param oname The bean name + */ + public void unregisterComponent(ObjectName oname) { + try { + if (oname != null && getMBeanServer().isRegistered(oname)) { + getMBeanServer().unregisterMBean(oname); + } + } catch (Throwable t) { + log.error(sm.getString("registry.unregisterError"), t); + } + } + + + /** + * Factory method to create (if necessary) and return our + * MBeanServer instance. + * + * @return the MBean server + */ + public MBeanServer getMBeanServer() { + if (server == null) { + synchronized (serverLock) { + if (server == null) { + if (MBeanServerFactory.findMBeanServer(null).size() > 0) { + server = MBeanServerFactory.findMBeanServer(null).get(0); + if (log.isDebugEnabled()) { + log.debug(sm.getString("registry.existingServer")); + } + } else { + server = ManagementFactory.getPlatformMBeanServer(); + if (log.isDebugEnabled()) { + log.debug(sm.getString("registry.createdServer")); + } + } + } + } + } + return server; + } + + + /** + * Find or load metadata. + * + * @param bean The bean + * @param beanClass The bean class + * @param type The registry type + * @return the managed bean + * @throws Exception An error occurred + */ + public ManagedBean findManagedBean(Object bean, Class beanClass, String type) + throws Exception { + + if (bean != null && beanClass == null) { + beanClass = bean.getClass(); + } + + if (type == null) { + type = beanClass.getName(); + } + + // first look for existing descriptor + ManagedBean managed = findManagedBean(type); + + // Search for a descriptor in the same package + if (managed == null) { + // check package and parent packages + if (log.isTraceEnabled()) { + log.trace("Looking for descriptor "); + } + findDescriptor(beanClass, type); + + managed = findManagedBean(type); + } + + // Still not found - use introspection + if (managed == null) { + if (log.isTraceEnabled()) { + log.trace("Introspecting "); + } + + // introspection + load("MbeansDescriptorsIntrospectionSource", beanClass, type); + + managed = findManagedBean(type); + if (managed == null) { + log.warn(sm.getString("registry.noTypeMetadata", type)); + return null; + } + managed.setName(type); + addManagedBean(managed); + } + return managed; + } + + + /** + * Convert a string to object, based on type. Used by several + * components. We could provide some pluggability. It is here to keep things + * consistent and avoid duplication in other tasks + * + * @param type Fully qualified class name of the resulting value + * @param value String value to be converted + * @return Converted value + */ + public Object convertValue(String type, String value) { + Object objValue = value; + + if (type == null || "java.lang.String".equals(type)) { + // string is default + objValue = value; + } else if ("javax.management.ObjectName".equals(type) || "ObjectName".equals(type)) { + try { + objValue = new ObjectName(value); + } catch (MalformedObjectNameException e) { + return null; + } + } else if ("java.lang.Integer".equals(type) || "int".equals(type)) { + objValue = Integer.valueOf(value); + } else if ("java.lang.Long".equals(type) || "long".equals(type)) { + objValue = Long.valueOf(value); + } else if ("java.lang.Boolean".equals(type) || "boolean".equals(type)) { + objValue = Boolean.valueOf(value); + } + return objValue; + } + + + /** + * Load descriptors. + * + * @param sourceType The source type + * @param source The bean + * @param param A type to load + * @return List of descriptors + * @throws Exception Error loading descriptors + */ + public List load(String sourceType, Object source, String param) throws Exception { + if (log.isTraceEnabled()) { + log.trace("load " + source); + } + String location = null; + String type = null; + Object inputsource = null; + + if (source instanceof URL) { + URL url = (URL) source; + location = url.toString(); + type = param; + inputsource = url.openStream(); + if (sourceType == null && location.endsWith(".xml")) { + sourceType = "MbeansDescriptorsDigesterSource"; + } + } else if (source instanceof File) { + location = ((File) source).getAbsolutePath(); + inputsource = new FileInputStream((File) source); + type = param; + if (sourceType == null && location.endsWith(".xml")) { + sourceType = "MbeansDescriptorsDigesterSource"; + } + } else if (source instanceof InputStream) { + type = param; + inputsource = source; + } else if (source instanceof Class) { + location = ((Class) source).getName(); + type = param; + inputsource = source; + if (sourceType == null) { + sourceType = "MbeansDescriptorsIntrospectionSource"; + } + } else { + throw new IllegalArgumentException(sm.getString("registry.invalidSource")); + } + + if (sourceType == null) { + sourceType = "MbeansDescriptorsDigesterSource"; + } + ModelerSource ds = getModelerSource(sourceType); + List mbeans = ds.loadDescriptors(this, type, inputsource); + + return mbeans; + } + + + /** + * Register a component + * + * @param bean The bean + * @param oname The object name + * @param type The registry type + * @throws Exception Error registering component + */ + public void registerComponent(Object bean, ObjectName oname, String type) throws Exception { + if (log.isTraceEnabled()) { + log.trace("Managed= " + oname); + } + + if (bean == null) { + log.error(sm.getString("registry.nullBean", oname)); + return; + } + + try { + if (type == null) { + type = bean.getClass().getName(); + } + + ManagedBean managed = findManagedBean(null, bean.getClass(), type); + + // The real mbean is created and registered + DynamicMBean mbean = managed.createMBean(bean); + + if (getMBeanServer().isRegistered(oname)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("registry.unregisterExisting", oname)); + } + getMBeanServer().unregisterMBean(oname); + } + + getMBeanServer().registerMBean(mbean, oname); + } catch (Exception ex) { + log.error(sm.getString("registry.registerError", oname), ex); + throw ex; + } + } + + + /** + * Lookup the component descriptor in the package and in the parent + * packages. + * + * @param packageName The package name + * @param classLoader The class loader + */ + public void loadDescriptors(String packageName, ClassLoader classLoader) { + String res = packageName.replace('.', '/'); + + if (log.isTraceEnabled()) { + log.trace("Finding descriptor " + res); + } + + if (searchedPaths.get(packageName) != null) { + return; + } + + String descriptors = res + "/mbeans-descriptors.xml"; + URL dURL = classLoader.getResource(descriptors); + + if (dURL == null) { + return; + } + + if (log.isTraceEnabled()) { + log.trace("Found " + dURL); + } + searchedPaths.put(packageName, dURL); + try { + load("MbeansDescriptorsDigesterSource", dURL, null); + } catch (Exception ex) { + log.error(sm.getString("registry.loadError", dURL)); + } + } + + + /** + * Lookup the component descriptor in the package and in the parent + * packages. + */ + private void findDescriptor(Class beanClass, String type) { + if (type == null) { + type = beanClass.getName(); + } + ClassLoader classLoader = null; + if (beanClass != null) { + classLoader = beanClass.getClassLoader(); + } + if (classLoader == null) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + if (classLoader == null) { + classLoader = this.getClass().getClassLoader(); + } + + String className = type; + String pkg = className; + while (pkg.indexOf('.') > 0) { + int lastComp = pkg.lastIndexOf('.'); + if (lastComp <= 0) { + return; + } + pkg = pkg.substring(0, lastComp); + if (searchedPaths.get(pkg) != null) { + return; + } + loadDescriptors(pkg, classLoader); + } + } + + + private ModelerSource getModelerSource(String type) throws Exception { + if (type == null) { + type = "MbeansDescriptorsDigesterSource"; + } + if (!type.contains(".")) { + type = "org.apache.tomcat.util.modeler.modules." + type; + } + + Class c = Class.forName(type); + ModelerSource ds = (ModelerSource) c.getConstructor().newInstance(); + return ds; + } + + + // -------------------- Registration -------------------- + + @Override + public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { + synchronized (serverLock) { + this.server = server; + } + return name; + } + + + @Override + public void postRegister(Boolean registrationDone) { + } + + + @Override + public void preDeregister() throws Exception { + } + + + @Override + public void postDeregister() { + } +} diff --git a/java/org/apache/tomcat/util/modeler/RegistryMBean.java b/java/org/apache/tomcat/util/modeler/RegistryMBean.java new file mode 100644 index 0000000..fa650a7 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/RegistryMBean.java @@ -0,0 +1,118 @@ +/* + * 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.tomcat.util.modeler; + + +import java.util.List; + +import javax.management.ObjectName; + +/** + * Interface for modeler MBeans. + * + * This is the main entry point into modeler. It provides methods to create + * and manipulate model mbeans and simplify their use. + * + * Starting with version 1.1, this is no longer a singleton and the static + * methods are strongly deprecated. In a container environment we can expect + * different applications to use different registries. + * + * @author Craig R. McClanahan + * @author Costin Manolache + * + * @since 1.1 + */ +public interface RegistryMBean { + + /** + * Invoke an operation on a set of mbeans. + * + * @param mbeans List of ObjectNames + * @param operation Operation to perform. Typically "init" "start" "stop" or "destroy" + * @param failFirst Behavior in case of exceptions - if false we'll ignore + * errors + * @throws Exception Error invoking operation + */ + void invoke(List mbeans, String operation, boolean failFirst) + throws Exception; + + /** + * Register a bean by creating a modeler mbean and adding it to the + * MBeanServer. + * + * If metadata is not loaded, we'll look up and read a file named + * "mbeans-descriptors.ser" or "mbeans-descriptors.xml" in the same package + * or parent. + * + * If the bean is an instance of DynamicMBean. it's metadata will be converted + * to a model mbean and we'll wrap it - so modeler services will be supported + * + * If the metadata is still not found, introspection will be used to extract + * it automatically. + * + * If an mbean is already registered under this name, it'll be first + * unregistered. + * + * If the component implements MBeanRegistration, the methods will be called. + * If the method has a method "setRegistry" that takes a RegistryMBean as + * parameter, it'll be called with the current registry. + * + * + * @param bean Object to be registered + * @param oname Name used for registration + * @param type The type of the mbean, as declared in mbeans-descriptors. If + * null, the name of the class will be used. This can be used as a hint or + * by subclasses. + * @throws Exception Error registering MBean + * + * @since 1.1 + */ + void registerComponent(Object bean, String oname, String type) + throws Exception; + + /** + * Unregister a component. We'll first check if it is registered, + * and mask all errors. This is mostly a helper. + * + * @param oname The name used by the bean + * + * @since 1.1 + */ + void unregisterComponent(String oname); + + + /** + * Return an int ID for faster access. Will be used for notifications + * and for other operations we want to optimize. + * + * @param domain Namespace + * @param name Type of the notification + * @return A unique id for the domain:name combination + * @since 1.1 + */ + int getId(String domain, String name); + + + /** + * Reset all metadata cached by this registry. Should be called + * to support reloading. Existing mbeans will not be affected or modified. + * + * It will be called automatically if the Registry is unregistered. + * @since 1.1 + */ + void stop(); +} diff --git a/java/org/apache/tomcat/util/modeler/Util.java b/java/org/apache/tomcat/util/modeler/Util.java new file mode 100644 index 0000000..9ece01e --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/Util.java @@ -0,0 +1,34 @@ +/* + * 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.tomcat.util.modeler; + +public class Util { + + private Util() { + // Utility class. Hide default constructor. + } + + public static boolean objectNameValueNeedsQuote(String input) { + for (int i = 0; i < input.length(); i++) { + char ch = input.charAt(i); + if (ch == ',' || ch == '=' || ch == ':' || ch == '*' || ch == '?') { + return true; + } + } + return false; + } +} diff --git a/java/org/apache/tomcat/util/modeler/mbeans-descriptors.dtd b/java/org/apache/tomcat/util/modeler/mbeans-descriptors.dtd new file mode 100644 index 0000000..4c1f523 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/mbeans-descriptors.dtd @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/tomcat/util/modeler/modules/LocalStrings.properties b/java/org/apache/tomcat/util/modeler/modules/LocalStrings.properties new file mode 100644 index 0000000..af8059d --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/modules/LocalStrings.properties @@ -0,0 +1,16 @@ +# 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. + +source.introspectionError=Introspection error creating MBean for class [{0}] diff --git a/java/org/apache/tomcat/util/modeler/modules/LocalStrings_fr.properties b/java/org/apache/tomcat/util/modeler/modules/LocalStrings_fr.properties new file mode 100644 index 0000000..6e2b813 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/modules/LocalStrings_fr.properties @@ -0,0 +1,16 @@ +# 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. + +source.introspectionError=Erreur d''introspection lors de la création du MBean pour la classe [{0}] diff --git a/java/org/apache/tomcat/util/modeler/modules/LocalStrings_ja.properties b/java/org/apache/tomcat/util/modeler/modules/LocalStrings_ja.properties new file mode 100644 index 0000000..1436d50 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/modules/LocalStrings_ja.properties @@ -0,0 +1,16 @@ +# 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. + +source.introspectionError=クラス [{0}] ã® MBean 作æˆæ™‚ã®ã‚¤ãƒ³ãƒˆãƒ­ã‚¹ãƒšã‚¯ã‚·ãƒ§ãƒ³ã‚¨ãƒ©ãƒ¼ diff --git a/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java new file mode 100644 index 0000000..5aeb701 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java @@ -0,0 +1,182 @@ +/* + * 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.tomcat.util.modeler.modules; + +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.Registry; + +public class MbeansDescriptorsDigesterSource extends ModelerSource +{ + private static final Log log = + LogFactory.getLog(MbeansDescriptorsDigesterSource.class); + private static final Object dLock = new Object(); + + private Registry registry; + private final List mbeans = new ArrayList<>(); + private static Digester digester = null; + + private static Digester createDigester() { + + Digester digester = new Digester(); + digester.setNamespaceAware(false); + digester.setValidating(false); + URL url = Registry.getRegistry(null, null).getClass().getResource + ("/org/apache/tomcat/util/modeler/mbeans-descriptors.dtd"); + if (url != null) { + digester.register + ("-//Apache Software Foundation//DTD Model MBeans Configuration File", + url.toString()); + } + + // Configure the parsing rules + digester.addObjectCreate + ("mbeans-descriptors/mbean", + "org.apache.tomcat.util.modeler.ManagedBean"); + digester.addSetProperties + ("mbeans-descriptors/mbean"); + digester.addSetNext + ("mbeans-descriptors/mbean", + "add", + "java.lang.Object"); + + digester.addObjectCreate + ("mbeans-descriptors/mbean/attribute", + "org.apache.tomcat.util.modeler.AttributeInfo"); + digester.addSetProperties + ("mbeans-descriptors/mbean/attribute"); + digester.addSetNext + ("mbeans-descriptors/mbean/attribute", + "addAttribute", + "org.apache.tomcat.util.modeler.AttributeInfo"); + + digester.addObjectCreate + ("mbeans-descriptors/mbean/notification", + "org.apache.tomcat.util.modeler.NotificationInfo"); + digester.addSetProperties + ("mbeans-descriptors/mbean/notification"); + digester.addSetNext + ("mbeans-descriptors/mbean/notification", + "addNotification", + "org.apache.tomcat.util.modeler.NotificationInfo"); + + digester.addObjectCreate + ("mbeans-descriptors/mbean/notification/descriptor/field", + "org.apache.tomcat.util.modeler.FieldInfo"); + digester.addSetProperties + ("mbeans-descriptors/mbean/notification/descriptor/field"); + digester.addSetNext + ("mbeans-descriptors/mbean/notification/descriptor/field", + "addField", + "org.apache.tomcat.util.modeler.FieldInfo"); + + digester.addCallMethod + ("mbeans-descriptors/mbean/notification/notification-type", + "addNotifType", 0); + + digester.addObjectCreate + ("mbeans-descriptors/mbean/operation", + "org.apache.tomcat.util.modeler.OperationInfo"); + digester.addSetProperties + ("mbeans-descriptors/mbean/operation"); + digester.addSetNext + ("mbeans-descriptors/mbean/operation", + "addOperation", + "org.apache.tomcat.util.modeler.OperationInfo"); + + digester.addObjectCreate + ("mbeans-descriptors/mbean/operation/descriptor/field", + "org.apache.tomcat.util.modeler.FieldInfo"); + digester.addSetProperties + ("mbeans-descriptors/mbean/operation/descriptor/field"); + digester.addSetNext + ("mbeans-descriptors/mbean/operation/descriptor/field", + "addField", + "org.apache.tomcat.util.modeler.FieldInfo"); + + digester.addObjectCreate + ("mbeans-descriptors/mbean/operation/parameter", + "org.apache.tomcat.util.modeler.ParameterInfo"); + digester.addSetProperties + ("mbeans-descriptors/mbean/operation/parameter"); + digester.addSetNext + ("mbeans-descriptors/mbean/operation/parameter", + "addParameter", + "org.apache.tomcat.util.modeler.ParameterInfo"); + + return digester; + + } + + public void setRegistry(Registry reg) { + this.registry=reg; + } + + + public void setSource( Object source ) { + this.source=source; + } + + @Override + public List loadDescriptors( Registry registry, String type, + Object source) throws Exception { + setRegistry(registry); + setSource(source); + execute(); + return mbeans; + } + + public void execute() throws Exception { + if (registry == null) { + registry = Registry.getRegistry(null, null); + } + + InputStream stream = (InputStream) source; + + List loadedMbeans = new ArrayList<>(); + synchronized(dLock) { + if (digester == null) { + digester = createDigester(); + } + + // Process the input file to configure our registry + try { + // Push our registry object onto the stack + digester.push(loadedMbeans); + digester.parse(stream); + } catch (Exception e) { + log.error(sm.getString("modules.digesterParseError"), e); + throw e; + } finally { + digester.reset(); + } + + } + for (ManagedBean loadedMbean : loadedMbeans) { + registry.addManagedBean(loadedMbean); + } + } +} diff --git a/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsIntrospectionSource.java b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsIntrospectionSource.java new file mode 100644 index 0000000..07fe2c6 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsIntrospectionSource.java @@ -0,0 +1,390 @@ +/* + * 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.tomcat.util.modeler.modules; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.modeler.AttributeInfo; +import org.apache.tomcat.util.modeler.ManagedBean; +import org.apache.tomcat.util.modeler.OperationInfo; +import org.apache.tomcat.util.modeler.ParameterInfo; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +public class MbeansDescriptorsIntrospectionSource extends ModelerSource +{ + private static final Log log = LogFactory.getLog(MbeansDescriptorsIntrospectionSource.class); + private static final StringManager sm = StringManager.getManager(MbeansDescriptorsIntrospectionSource.class); + + private Registry registry; + private String type; + private final List mbeans = new ArrayList<>(); + + public void setRegistry(Registry reg) { + this.registry=reg; + } + + /** + * Used if a single component is loaded + * + * @param type The type + */ + public void setType( String type ) { + this.type=type; + } + + public void setSource( Object source ) { + this.source=source; + } + + @Override + public List loadDescriptors(Registry registry, String type, + Object source) throws Exception { + setRegistry(registry); + setType(type); + setSource(source); + execute(); + return mbeans; + } + + public void execute() throws Exception { + if( registry==null ) { + registry=Registry.getRegistry(null, null); + } + try { + ManagedBean managed = createManagedBean(registry, null, + (Class)source, type); + if( managed==null ) { + return; + } + managed.setName( type ); + + registry.addManagedBean(managed); + + } catch( Exception ex ) { + log.error(sm.getString("modules.readDescriptorsError"), ex); + } + } + + + + // ------------ Implementation for non-declared introspection classes + + private static final Map specialMethods = new HashMap<>(); + static { + specialMethods.put( "preDeregister", ""); + specialMethods.put( "postDeregister", ""); + } + + private static final Class[] supportedTypes = new Class[] { + Boolean.class, + Boolean.TYPE, + Byte.class, + Byte.TYPE, + Character.class, + Character.TYPE, + Short.class, + Short.TYPE, + Integer.class, + Integer.TYPE, + Long.class, + Long.TYPE, + Float.class, + Float.TYPE, + Double.class, + Double.TYPE, + String.class, + String[].class, + BigDecimal.class, + BigInteger.class, + ObjectName.class, + Object[].class, + java.io.File.class, + }; + + /** + * Check if this class is one of the supported types. + * If the class is supported, returns true. Otherwise, + * returns false. + * @param ret The class to check + * @return boolean True if class is supported + */ + private boolean supportedType(Class ret) { + for (Class supportedType : supportedTypes) { + if (ret == supportedType) { + return true; + } + } + if (isBeanCompatible(ret)) { + return true; + } + return false; + } + + /** + * Check if this class conforms to JavaBeans specifications. + * If the class is conformant, returns true. + * + * @param javaType The class to check + * @return boolean True if the class is compatible. + */ + private boolean isBeanCompatible(Class javaType) { + // Must be a non-primitive and non array + if (javaType.isArray() || javaType.isPrimitive()) { + return false; + } + + // Anything in the java or javax package that + // does not have a defined mapping is excluded. + if (javaType.getName().startsWith("java.") || + javaType.getName().startsWith("javax.")) { + return false; + } + + try { + javaType.getConstructor(new Class[]{}); + } catch (NoSuchMethodException e) { + return false; + } + + // Make sure superclass is compatible + Class superClass = javaType.getSuperclass(); + if (superClass != null && + superClass != Object.class && + superClass != Exception.class && + superClass != Throwable.class) { + if (!isBeanCompatible(superClass)) { + return false; + } + } + return true; + } + + /** + * Process the methods and extract 'attributes', methods, etc. + * + * @param realClass The class to process + * @param attNames The attribute name (complete) + * @param getAttMap The readable attributes map + * @param setAttMap The settable attributes map + * @param invokeAttList The invokable attributes list + */ + private void initMethods(Class realClass, Set attNames, + Map getAttMap, Map setAttMap, + List invokeAttList) { + + Method[] methods = realClass.getMethods(); + for (Method method : methods) { + String name = method.getName(); + + if (Modifier.isStatic(method.getModifiers())) { + continue; + } + if (!Modifier.isPublic(method.getModifiers())) { + if (log.isTraceEnabled()) { + log.trace("Not public " + method); + } + continue; + } + if (method.getDeclaringClass() == Object.class) { + continue; + } + Class[] params = method.getParameterTypes(); + + if (name.startsWith("get") && params.length == 0) { + Class ret = method.getReturnType(); + if (!supportedType(ret)) { + if (log.isTraceEnabled()) { + log.trace("Unsupported type " + method); + } + continue; + } + name = unCapitalize(name.substring(3)); + + getAttMap.put(name, method); + attNames.add(name); + } else if (name.startsWith("is") && params.length == 0) { + Class ret = method.getReturnType(); + if (Boolean.TYPE != ret) { + if (log.isTraceEnabled()) { + log.trace("Unsupported type " + method + " " + ret); + } + continue; + } + name = unCapitalize(name.substring(2)); + + getAttMap.put(name, method); + attNames.add(name); + + } else if (name.startsWith("set") && params.length == 1) { + if (!supportedType(params[0])) { + if (log.isTraceEnabled()) { + log.trace("Unsupported type " + method + " " + params[0]); + } + continue; + } + name = unCapitalize(name.substring(3)); + setAttMap.put(name, method); + attNames.add(name); + } else { + if (params.length == 0) { + if (specialMethods.get(name) != null) { + continue; + } + invokeAttList.add(method); + } else { + boolean supported = true; + for (Class param : params) { + if (!supportedType(param)) { + supported = false; + break; + } + } + if (supported) { + invokeAttList.add(method); + } + } + } + } + } + + /** + * XXX Find if the 'className' is the name of the MBean or + * the real class ( I suppose first ) + * XXX Read (optional) descriptions from a .properties, generated + * from source + * XXX Deal with constructors + * + * @param registry The Bean registry (not used) + * @param domain The bean domain (not used) + * @param realClass The class to analyze + * @param type The bean type + * @return ManagedBean The create MBean + */ + public ManagedBean createManagedBean(Registry registry, String domain, + Class realClass, String type) + { + ManagedBean mbean = new ManagedBean(); + + + Set attrNames = new HashSet<>(); + // key: attribute val: getter method + Map getAttMap = new HashMap<>(); + // key: attribute val: setter method + Map setAttMap = new HashMap<>(); + // key: operation val: invoke method + List invokeAttList = new ArrayList<>(); + + initMethods(realClass, attrNames, getAttMap, setAttMap, invokeAttList); + + try { + for (String name : attrNames) { + AttributeInfo ai = new AttributeInfo(); + ai.setName(name); + Method gm = getAttMap.get(name); + if (gm != null) { + ai.setGetMethod(gm.getName()); + Class t = gm.getReturnType(); + if (t != null) { + ai.setType(t.getName()); + } + } + Method sm = setAttMap.get(name); + if (sm != null) { + Class t = sm.getParameterTypes()[0]; + if (t != null) { + ai.setType(t.getName()); + } + ai.setSetMethod(sm.getName()); + } + ai.setDescription("Introspected attribute " + name); + if (log.isTraceEnabled()) { + log.trace("Introspected attribute " + name + " " + gm + " " + sm); + } + if (gm == null) { + ai.setReadable(false); + } + if (sm == null) { + ai.setWriteable(false); + } + if (sm != null || gm != null) { + mbean.addAttribute(ai); + } + } + + for (Method method : invokeAttList) { + String name = method.getName(); + OperationInfo op = new OperationInfo(); + op.setName(name); + op.setReturnType(method.getReturnType().getName()); + op.setDescription("Introspected operation " + name); + Class[] params = method.getParameterTypes(); + for (int i = 0; i < params.length; i++) { + ParameterInfo pi = new ParameterInfo(); + pi.setType(params[i].getName()); + pi.setName(("param" + i).intern()); + pi.setDescription(("Introspected parameter param" + i).intern()); + op.addParameter(pi); + } + mbean.addOperation(op); + } + + if (log.isTraceEnabled()) { + log.trace("Setting name: " + type); + } + mbean.setName(type); + + return mbean; + } catch (Exception ex) { + log.error(sm.getString("source.introspectionError", realClass.getName()), ex); + return null; + } + } + + + // -------------------- Utils -------------------- + /** + * Converts the first character of the given + * String into lower-case. + * + * @param name The string to convert + * @return String + */ + private static String unCapitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + char chars[] = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + +} + diff --git a/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java b/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java new file mode 100644 index 0000000..6506b0c --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java @@ -0,0 +1,44 @@ +/* + * 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.tomcat.util.modeler.modules; + +import java.util.List; + +import javax.management.ObjectName; + +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.res.StringManager; + +/** + * Source for descriptor data. More sources can be added. + */ +public abstract class ModelerSource { + protected static final StringManager sm = StringManager.getManager(Registry.class); + protected Object source; + + /** + * Load data, returns a list of items. + * + * @param registry The registry + * @param type The bean registry type + * @param source Introspected object or some other source + * @return a list of object names + * @throws Exception Error loading descriptors + */ + public abstract List loadDescriptors(Registry registry, + String type, Object source) throws Exception; +} diff --git a/java/org/apache/tomcat/util/modeler/modules/package.html b/java/org/apache/tomcat/util/modeler/modules/package.html new file mode 100644 index 0000000..e315bf6 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/modules/package.html @@ -0,0 +1,59 @@ + + + +org.apache.commons.modeler.modules + + +

    Implementation classes - should not be used directly. The API is not stable +but eventually the code will be refactored as a collection of mbeans that will be usable +( more or less ) independently.

    + +

    The MbeanDescriptors* classes are used to extract metadata from different sources. They +are result of few stages of refactoring - now they look very similar with ant tasks and are +close to normal mbeans, with an execute() method. DOM, SER, Introspection and Dynamic mbean +will load metadata from the corresponding sources. +

    + +

    MbeansSource will load an extended MLET file, similar with jboss. It is not completely +implemented - only modeler mbeans and dynamic mbeans are loaded. The important characteristic +is that all declared mbeans will be registered in the mbean server as model mbeans. For +regular java classes, the description will be used to construct the model mbean. DynamicMbeans +metadata will be converted to model mbean and the model mbean wrapper will be loaded.

    + +

    The goal of MbeansSource is to implement a simple persistence mechanism. Since all components +are model mbeans, we can detect all changes. The source will be loaded as DOM and modifications +will be made to the tree. The save() method will save the DOM tree - preserving all comments +and having only the changes that are needed.

    + +

    There are few remaining issues. First, we need to use the persistence metadata to avoid +saving transient fields ( we save an attribute when we detect a change - but we don't know +if this attribute should be saved ). The solution is to use the persistence fields in the +spec - with some reasonable defaults or patterns for introspection or backward compat. +

    + +

    Another problem is implementing adding and removing components. In catalina, a +factory is used to create the components, and save will operate on all mbeans. +For creation we need to also use a factory - using the "Type" as a parameter. This +will also work very well with Ant1.6 where we can use the component factory to +do a "natural" mapping ( i.e. mbeans can be treated as tasks, with attributes as +task attributes ). The second part can be solve by either using a parameter on +the factory method ( saveTo ? ), or by having a single mbeans source per domain. +

    + + + diff --git a/java/org/apache/tomcat/util/modeler/package.html b/java/org/apache/tomcat/util/modeler/package.html new file mode 100644 index 0000000..c60fb52 --- /dev/null +++ b/java/org/apache/tomcat/util/modeler/package.html @@ -0,0 +1,248 @@ + + + +Package Documentation for COMMONS-MODELER + + +

    The Modeler component of the Commons project +offers convenient support for configuring and instantiating Model MBeans +(management beans), as described in the JMX Specification. It is typically +used within a server-based application that wants to expose management +features via JMX. See the + +JMX Specification (Version 1.1) for more information about Model MBeans +and other JMX concepts.

    + +

    Model MBeans are very powerful - and the JMX specification includes a +mechanism to use a standard JMX-provided base class to satisfy many of the +requirements, without having to create custom Model MBean implementation +classes yourself. However, one of the requirements in creating such a +Model MBean is to create the corresponding metadata information (i.e. an +implementation of the +javax.management.modelmbean.ModelMBeanInfo interface and its +corresponding subordinate interfaces). Creating this information can be +tedious and error prone. The Modeler package makes the process +much simpler, because the required information is constructed dynamically +from an easy-to-understand XML description of the metadata. Once you have +the metadata defined, and registered at runtime in the provided +Registry, Modeler also supports +convenient factory methods to instantiate new Model MBean instances for you. +

    + +

    The steps required to use Modeler in your server-based application are +described in detail below. You can find some simple usage code in the unit +tests that come with Modeler (in the src/test subdirectory of the +source distribution), and much more complex usage code in Tomcat 4.1 (in the +org.apache.catalina.mbeans package).

    . More advanced uses can +be found in Tomcat 5. + + +

    1. Acquire a JMX Implementation

    + +

    Modeler has been tested with different JMX implementations: +

    + +

    After unpacking the release, you will need to ensure that the appropriate +JAR file (jmxri.jar or mx4j.jar) is included on your +compilation classpath, and in the classpath of your server application when it +is executed.

    + + +

    2. Create a Modeler Configuration File

    + +

    Modeler requires that you construct a configuration file that +describes the metadata ultimately need to construct the +javax.management.modelmbean.ModelMBeanInfo structure that is +required by JMX. Your XML file must conform to the +mbeans-descriptors.dtd +DTD that defines the acceptable structure.

    + +

    Fundamentally, you will be constructing an <mbean> +element for each type of Model MBean that a registry will know how to create. +Nested within this element will be other elements describing the constructors, +attributes, operations, and notifications associated with this MBean. See +the comments in the DTD for detailed information about the valid attributes +and their meanings.

    + +

    A simple example configuration file might include the following components +(abstracted from the real definitions found in Tomcat 4.1's use of Modeler): +

    +
    +
    +  <?xml version="1.0"?>
    +  <!DOCTYPE mbeans-descriptors PUBLIC
    +   "-//Apache Software Foundation//DTD Model MBeans Configuration File"
    +   "http://jakarta.apache.org/commons/dtds/mbeans-descriptors.dtd">
    +
    +  <mbeans-descriptors>
    +
    +    <!-- ... other MBean definitions ... -->
    +
    +    <mbean         name="Group"
    +              className="org.apache.catalina.mbeans.GroupMBean"
    +            description="Group from a user database"
    +                 domain="Users"
    +                  group="Group"
    +                   type="org.apache.catalina.Group">
    +
    +      <attribute   name="description"
    +            description="Description of this group"
    +                   type="java.lang.String"/>
    +
    +      <attribute   name="groupname"
    +            description="Group name of this group"
    +                   type="java.lang.String"/>
    +
    +      <attribute   name="roles"
    +            description="MBean Names of roles for this group"
    +                   type="java.lang.String[]"
    +              writeable="false"/>
    +
    +      <attribute   name="users"
    +            description="MBean Names of user members of this group"
    +                   type="java.lang.String[]"
    +              writeable="false"/>
    +
    +      <operation   name="addRole"
    +            description="Add a new authorized role for this group"
    +                 impact="ACTION"
    +             returnType="void">
    +        <parameter name="role"
    +            description="Role to be added"
    +                   type="java.lang.String"/>
    +      </operation>
    +
    +      <operation   name="removeRole"
    +            description="Remove an old authorized role for this group"
    +                 impact="ACTION"
    +             returnType="void">
    +        <parameter name="role"
    +            description="Role to be removed"
    +                   type="java.lang.String"/>
    +      </operation>
    +
    +      <operation   name="removeRoles"
    +            description="Remove all authorized roles for this group"
    +                 impact="ACTION"
    +             returnType="void">
    +      </operation>
    +
    +    </mbean>
    +
    +    <!-- ... other MBean definitions ... -->
    +
    +  </mbeans-descriptors>
    +
    +
    + +

    This MBean represents an instance of org.apache.catalina.Group, +which is an entity representing a group of users (with a shared set of security +roles that all users in the group inherit) in a user database. This MBean +advertises support for four attributes (description, groupname, roles, and +users) that roughly correspond to JavaBean properties. By default, attributes +are assumed to have read/write access. For this particular MBean, the roles +and users attributes are read-only (writeable="false"). Finally, +this MBean supports three operations (addRole, removeRole, and +removeRoles) that roughly correspond to JavaBean methods on the underlying +component.

    + +

    In general, Modeler provides a standard ModelMBean implementation +that simply passes on JMX calls on attributes and operations directly through +to the managed component that the ModelMBean is associated with. For special +case requirements, you can define a subclass of +BaseModelMBean that provides override +methods for one or more of these attributes (i.e. the property getter and/or +setter methods) and operations (i.e. direct method calls). + +

    For this particular MBean, a custom BaseModelMBean implementation subclass +is described (org.apache.catalina.mbeans.GroupMBean) is +configured. It was necessary in this particular case because several of the +underlying Catalina component's methods deal with internal objects or arrays of +objects, rather than just the Strings and primitives that are supported by all +JMX clients. Thus, the following method on the Group interface: +

    +
    +    public void addRole(Role role);
    +
    +

    is represented, in the MBean, by an addRole method that takes +a String argument representing the role name of the required role. The MBean's +implementation class acts as an adapter, and looks up the required Role +object (by name) before calling the addRole method on the +underlying Group instance within the Server.

    + + +

    3. Create Modeler Registry at Startup Time

    + +

    The metadata information, and the corresponding Model MBean factory, is +represented at runtime in an instance of Registry +whose contents are initialized from the configuration file prepared as was +described above. Typically, such a file will be included in the JAR file +containing the MBean implementation classes themselves, and loaded as follows: +

    +
    +    URL url= this.getClass().getResource
    +      ("/com/mycompany/mypackage/mbeans-descriptors.xml");
    +    Registry registry = Registry.getRegistry();
    +    registry.loadMetadata(url);
    +
    + +

    Besides using the configuration file, it is possible to configure the +registry metadata by hand, using the addManagedBean() and +removeManagedBean() methods. However, most users will find +the standard support for loading a configuration file to be convenient +and sufficient.

    + +

    Modeler will also look for an mbeans-descriptors.xml in the same package +with the class being registered and in its parent. If no metadata is found, +modeler will use a number of simple patterns, similar with the ones used by +ant, to determine a reasonable metadata

    + +

    In a future version we should also support xdoclet-based generation of the +descriptors

    + + +

    4. Instantiate Model MBeans As Needed

    + +

    When your server application needs to instantiate a new MBean and register +it with the corresponding MBeanServer, it can execute code like +this:

    + +
    +  Group group = ... managed component instance ...;
    +
    +  MBeanServer mserver = registry.getMBeanServer();
    +
    +  String oname="myDomain:type=Group,name=myGroup";
    +
    +  registry.registerComponent( group, oname, "Group" );
    +
    + +

    After the Model MBean has been created and registered, it is accessible to +JMX clients through the standard JMX client APIs. +

    + + + diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java new file mode 100644 index 0000000..bfb90b0 --- /dev/null +++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java @@ -0,0 +1,1593 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.IntrospectionUtils; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.net.Acceptor.AcceptorState; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.StoreType; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.threads.LimitLatch; +import org.apache.tomcat.util.threads.ResizableExecutor; +import org.apache.tomcat.util.threads.TaskQueue; +import org.apache.tomcat.util.threads.TaskThreadFactory; +import org.apache.tomcat.util.threads.ThreadPoolExecutor; +import org.apache.tomcat.util.threads.VirtualThreadExecutor; + +/** + * @param The type used by the socket wrapper associated with this endpoint. + * May be the same as U. + * @param The type of the underlying socket used by this endpoint. May be + * the same as S. + * + * @author Mladen Turk + * @author Remy Maucherat + */ +public abstract class AbstractEndpoint { + + // -------------------------------------------------------------- Constants + + protected static final StringManager sm = StringManager.getManager(AbstractEndpoint.class); + + public interface Handler { + + /** + * Different types of socket states to react upon. + */ + enum SocketState { + // TODO Add a new state to the AsyncStateMachine and remove + // ASYNC_END (if possible) + OPEN, CLOSED, LONG, ASYNC_END, SENDFILE, UPGRADING, UPGRADED, ASYNC_IO, SUSPENDED + } + + + /** + * Process the provided socket with the given current status. + * + * @param socket The socket to process + * @param status The current socket status + * + * @return The state of the socket after processing + */ + SocketState process(SocketWrapperBase socket, + SocketEvent status); + + + /** + * Obtain the GlobalRequestProcessor associated with the handler. + * + * @return the GlobalRequestProcessor + */ + Object getGlobal(); + + + /** + * Release any resources associated with the given SocketWrapper. + * + * @param socketWrapper The socketWrapper to release resources for + */ + void release(SocketWrapperBase socketWrapper); + + + /** + * Inform the handler that the endpoint has stopped accepting any new + * connections. Typically, the endpoint will be stopped shortly + * afterwards but it is possible that the endpoint will be resumed so + * the handler should not assume that a stop will follow. + */ + void pause(); + + + /** + * Recycle resources associated with the handler. + */ + void recycle(); + } + + protected enum BindState { + UNBOUND(false, false), + BOUND_ON_INIT(true, true), + BOUND_ON_START(true, true), + SOCKET_CLOSED_ON_STOP(false, true); + + private final boolean bound; + private final boolean wasBound; + + BindState(boolean bound, boolean wasBound) { + this.bound = bound; + this.wasBound = wasBound; + } + + public boolean isBound() { + return bound; + } + + public boolean wasBound() { + return wasBound; + } + } + + + public static long toTimeout(long timeout) { + // Many calls can't do infinite timeout so use Long.MAX_VALUE if timeout is <= 0 + return (timeout > 0) ? timeout : Long.MAX_VALUE; + } + + // ----------------------------------------------------------------- Fields + + /** + * Running state of the endpoint. + */ + protected volatile boolean running = false; + + + /** + * Will be set to true whenever the endpoint is paused. + */ + protected volatile boolean paused = false; + + /** + * Are we using an internal executor + */ + protected volatile boolean internalExecutor = true; + + + /** + * counter for nr of connections handled by an endpoint + */ + private volatile LimitLatch connectionLimitLatch = null; + + /** + * Socket properties + */ + protected final SocketProperties socketProperties = new SocketProperties(); + public SocketProperties getSocketProperties() { + return socketProperties; + } + + /** + * Thread used to accept new connections and pass them to worker threads. + */ + protected Acceptor acceptor; + + /** + * Cache for SocketProcessor objects + */ + protected SynchronizedStack> processorCache; + + private ObjectName oname = null; + + /** + * Map holding all current connections keyed with the sockets. + */ + protected Map> connections = new ConcurrentHashMap<>(); + + /** + * Get a set with the current open connections. + * @return A set with the open socket wrappers + */ + public Set> getConnections() { + return new HashSet<>(connections.values()); + } + + // ----------------------------------------------------------------- Properties + + private String defaultSSLHostConfigName = SSLHostConfig.DEFAULT_SSL_HOST_NAME; + /** + * @return The host name for the default SSL configuration for this endpoint + * - always in lower case. + */ + public String getDefaultSSLHostConfigName() { + return defaultSSLHostConfigName; + } + public void setDefaultSSLHostConfigName(String defaultSSLHostConfigName) { + this.defaultSSLHostConfigName = defaultSSLHostConfigName.toLowerCase(Locale.ENGLISH); + } + + + protected ConcurrentMap sslHostConfigs = new ConcurrentHashMap<>(); + /** + * Add the given SSL Host configuration. + * + * @param sslHostConfig The configuration to add + * + * @throws IllegalArgumentException If the host name is not valid or if a + * configuration has already been provided + * for that host + */ + public void addSslHostConfig(SSLHostConfig sslHostConfig) throws IllegalArgumentException { + addSslHostConfig(sslHostConfig, false); + } + /** + * Add the given SSL Host configuration, optionally replacing the existing + * configuration for the given host. + * + * @param sslHostConfig The configuration to add + * @param replace If {@code true} replacement of an existing + * configuration is permitted, otherwise any such + * attempted replacement will trigger an exception + * + * @throws IllegalArgumentException If the host name is not valid or if a + * configuration has already been provided + * for that host and replacement is not + * allowed + */ + public void addSslHostConfig(SSLHostConfig sslHostConfig, boolean replace) throws IllegalArgumentException { + String key = sslHostConfig.getHostName(); + if (key == null || key.length() == 0) { + throw new IllegalArgumentException(sm.getString("endpoint.noSslHostName")); + } + if (bindState != BindState.UNBOUND && bindState != BindState.SOCKET_CLOSED_ON_STOP && + isSSLEnabled()) { + try { + createSSLContext(sslHostConfig); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + if (replace) { + SSLHostConfig previous = sslHostConfigs.put(key, sslHostConfig); + if (previous != null) { + unregisterJmx(sslHostConfig); + } + registerJmx(sslHostConfig); + + // Do not release any SSLContexts associated with a replaced + // SSLHostConfig. They may still be in used by existing connections + // and releasing them would break the connection at best. Let GC + // handle the clean up. + } else { + SSLHostConfig duplicate = sslHostConfigs.putIfAbsent(key, sslHostConfig); + if (duplicate != null) { + releaseSSLContext(sslHostConfig); + throw new IllegalArgumentException(sm.getString("endpoint.duplicateSslHostName", key)); + } + registerJmx(sslHostConfig); + } + } + /** + * Removes the SSL host configuration for the given host name, if such a + * configuration exists. + * + * @param hostName The host name associated with the SSL host configuration + * to remove + * + * @return The SSL host configuration that was removed, if any + */ + public SSLHostConfig removeSslHostConfig(String hostName) { + if (hostName == null) { + return null; + } + // Host names are case insensitive but stored/processed in lower case + // internally because they are used as keys in a ConcurrentMap where + // keys are compared in a case sensitive manner. + String hostNameLower = hostName.toLowerCase(Locale.ENGLISH); + if (hostNameLower.equals(getDefaultSSLHostConfigName())) { + throw new IllegalArgumentException( + sm.getString("endpoint.removeDefaultSslHostConfig", hostName)); + } + SSLHostConfig sslHostConfig = sslHostConfigs.remove(hostNameLower); + unregisterJmx(sslHostConfig); + return sslHostConfig; + } + /** + * Re-read the configuration files for the SSL host and replace the existing + * SSL configuration with the updated settings. Note this replacement will + * happen even if the settings remain unchanged. + * + * @param hostName The SSL host for which the configuration should be + * reloaded. This must match a current SSL host + */ + public void reloadSslHostConfig(String hostName) { + // Host names are case insensitive but stored/processed in lower case + // internally because they are used as keys in a ConcurrentMap where + // keys are compared in a case sensitive manner. + // This method can be called via various paths so convert the supplied + // host name to lower case here to ensure the conversion occurs whatever + // the call path. + SSLHostConfig sslHostConfig = sslHostConfigs.get(hostName.toLowerCase(Locale.ENGLISH)); + if (sslHostConfig == null) { + throw new IllegalArgumentException( + sm.getString("endpoint.unknownSslHostName", hostName)); + } + addSslHostConfig(sslHostConfig, true); + } + /** + * Re-read the configuration files for all SSL hosts and replace the + * existing SSL configuration with the updated settings. Note this + * replacement will happen even if the settings remain unchanged. + */ + public void reloadSslHostConfigs() { + for (String hostName : sslHostConfigs.keySet()) { + reloadSslHostConfig(hostName); + } + } + public SSLHostConfig[] findSslHostConfigs() { + return sslHostConfigs.values().toArray(new SSLHostConfig[0]); + } + + /** + * Create the SSLContext for the given SSLHostConfig. + * + * @param sslHostConfig The SSLHostConfig for which the SSLContext should be + * created + * @throws Exception If the SSLContext cannot be created for the given + * SSLHostConfig + */ + protected abstract void createSSLContext(SSLHostConfig sslHostConfig) throws Exception; + + + protected void logCertificate(SSLHostConfigCertificate certificate) { + SSLHostConfig sslHostConfig = certificate.getSSLHostConfig(); + + String certificateInfo; + + if (certificate.getStoreType() == StoreType.PEM) { + // PEM file based + certificateInfo = sm.getString("endpoint.tls.info.cert.pem", certificate.getCertificateKeyFile(), + certificate.getCertificateFile(), certificate.getCertificateChainFile()); + } else { + // Keystore based + String keyAlias = certificate.getCertificateKeyAlias(); + if (keyAlias == null) { + keyAlias = SSLUtilBase.DEFAULT_KEY_ALIAS; + } + certificateInfo = + sm.getString("endpoint.tls.info.cert.keystore", certificate.getCertificateKeystoreFile(), keyAlias); + } + + String trustStoreSource = sslHostConfig.getTruststoreFile(); + if (trustStoreSource == null) { + trustStoreSource = sslHostConfig.getCaCertificateFile(); + } + if (trustStoreSource == null) { + trustStoreSource = sslHostConfig.getCaCertificatePath(); + } + + getLogCertificate().info(sm.getString("endpoint.tls.info", getName(), sslHostConfig.getHostName(), + certificate.getType(), certificateInfo, trustStoreSource)); + + if (getLogCertificate().isDebugEnabled()) { + String alias = certificate.getCertificateKeyAlias(); + if (alias == null) { + alias = SSLUtilBase.DEFAULT_KEY_ALIAS; + } + X509Certificate[] x509Certificates = certificate.getSslContext().getCertificateChain(alias); + if (x509Certificates != null && x509Certificates.length > 0) { + getLogCertificate().debug(generateCertificateDebug(x509Certificates[0])); + } else { + getLogCertificate().debug(sm.getString("endpoint.tls.cert.noCerts")); + } + } + } + + + protected String generateCertificateDebug(X509Certificate certificate) { + StringBuilder sb = new StringBuilder(); + sb.append("\n["); + try { + byte[] certBytes = certificate.getEncoded(); + // SHA-256 fingerprint + sb.append("\nSHA-256 fingerprint: "); + MessageDigest sha512Digest = MessageDigest.getInstance("SHA-256"); + sha512Digest.update(certBytes); + sb.append(HexUtils.toHexString(sha512Digest.digest())); + // SHA-1 fingerprint + sb.append("\nSHA-1 fingerprint: "); + MessageDigest sha1Digest = MessageDigest.getInstance("SHA-1"); + sha1Digest.update(certBytes); + sb.append(HexUtils.toHexString(sha1Digest.digest())); + } catch (CertificateEncodingException e) { + getLogCertificate().warn(sm.getString("endpoint.tls.cert.encodingError"), e); + } catch (NoSuchAlgorithmException e) { + // Unreachable code + // All JREs are required to support SHA-1 and SHA-256 + throw new RuntimeException(e); + } + sb.append("\n"); + sb.append(certificate); + sb.append("\n]"); + return sb.toString(); + } + + + protected void destroySsl() throws Exception { + if (isSSLEnabled()) { + for (SSLHostConfig sslHostConfig : sslHostConfigs.values()) { + releaseSSLContext(sslHostConfig); + } + } + } + + + /** + * Release the SSLContext, if any, associated with the SSLHostConfig. + * + * @param sslHostConfig The SSLHostConfig for which the SSLContext should be + * released + */ + protected void releaseSSLContext(SSLHostConfig sslHostConfig) { + for (SSLHostConfigCertificate certificate : sslHostConfig.getCertificates()) { + if (certificate.getSslContext() != null) { + // Only release the SSLContext if we generated it. + SSLContext sslContext = certificate.getSslContextGenerated(); + if (sslContext != null) { + sslContext.destroy(); + } + } + } + } + + + /** + * Look up the SSLHostConfig for the given host name. Lookup order is: + *
      + *
    1. exact match
    2. + *
    3. wild card match
    4. + *
    5. default SSLHostConfig
    6. + *
    + * + * @param sniHostName Host name - must be in lower case + * + * @return The SSLHostConfig for the given host name. + */ + protected SSLHostConfig getSSLHostConfig(String sniHostName) { + SSLHostConfig result = null; + + if (sniHostName != null) { + // First choice - direct match + result = sslHostConfigs.get(sniHostName); + if (result != null) { + return result; + } + // Second choice, wildcard match + int indexOfDot = sniHostName.indexOf('.'); + if (indexOfDot > -1) { + result = sslHostConfigs.get("*" + sniHostName.substring(indexOfDot)); + } + } + + // Fall-back. Use the default + if (result == null) { + result = sslHostConfigs.get(getDefaultSSLHostConfigName()); + } + if (result == null) { + // Should never happen. + throw new IllegalStateException(); + } + return result; + } + + + /** + * Has the user requested that send file be used where possible? + */ + private boolean useSendfile = true; + public boolean getUseSendfile() { + return useSendfile; + } + public void setUseSendfile(boolean useSendfile) { + this.useSendfile = useSendfile; + } + + + /** + * Time to wait for the internal executor (if used) to terminate when the + * endpoint is stopped in milliseconds. Defaults to 5000 (5 seconds). + */ + private long executorTerminationTimeoutMillis = 5000; + + public long getExecutorTerminationTimeoutMillis() { + return executorTerminationTimeoutMillis; + } + + public void setExecutorTerminationTimeoutMillis( + long executorTerminationTimeoutMillis) { + this.executorTerminationTimeoutMillis = executorTerminationTimeoutMillis; + } + + + /** + * Priority of the acceptor threads. + */ + protected int acceptorThreadPriority = Thread.NORM_PRIORITY; + public void setAcceptorThreadPriority(int acceptorThreadPriority) { + this.acceptorThreadPriority = acceptorThreadPriority; + } + public int getAcceptorThreadPriority() { return acceptorThreadPriority; } + + + private int maxConnections = 8*1024; + public void setMaxConnections(int maxCon) { + this.maxConnections = maxCon; + LimitLatch latch = this.connectionLimitLatch; + if (latch != null) { + // Update the latch that enforces this + if (maxCon == -1) { + releaseConnectionLatch(); + } else { + latch.setLimit(maxCon); + } + } else if (maxCon > 0) { + initializeConnectionLatch(); + } + } + public int getMaxConnections() { return this.maxConnections; } + + /** + * Return the current count of connections handled by this endpoint, if the + * connections are counted (which happens when the maximum count of + * connections is limited), or -1 if they are not. This + * property is added here so that this value can be inspected through JMX. + * It is visible on "ThreadPool" MBean. + * + *

    The count is incremented by the Acceptor before it tries to accept a + * new connection. Until the limit is reached and thus the count cannot be + * incremented, this value is more by 1 (the count of acceptors) than the + * actual count of connections that are being served. + * + * @return The count + */ + public long getConnectionCount() { + LimitLatch latch = connectionLimitLatch; + if (latch != null) { + return latch.getCount(); + } + return -1; + } + + /** + * External Executor based thread pool. + */ + private Executor executor = null; + public void setExecutor(Executor executor) { + this.executor = executor; + this.internalExecutor = (executor == null); + } + public Executor getExecutor() { return executor; } + + + private boolean useVirtualThreads = false; + public void setUseVirtualThreads(boolean useVirtualThreads) { + this.useVirtualThreads = useVirtualThreads; + } + public boolean getUseVirtualThreads() { + return useVirtualThreads; + } + + + /** + * External Executor based thread pool for utility tasks. + */ + private ScheduledExecutorService utilityExecutor = null; + public void setUtilityExecutor(ScheduledExecutorService utilityExecutor) { + this.utilityExecutor = utilityExecutor; + } + public ScheduledExecutorService getUtilityExecutor() { + if (utilityExecutor == null) { + getLog().warn(sm.getString("endpoint.warn.noUtilityExecutor")); + utilityExecutor = new ScheduledThreadPoolExecutor(1); + } + return utilityExecutor; + } + + + /** + * Server socket port. + */ + private int port = -1; + public int getPort() { return port; } + public void setPort(int port ) { this.port=port; } + + + private int portOffset = 0; + public int getPortOffset() { return portOffset; } + public void setPortOffset(int portOffset ) { + if (portOffset < 0) { + throw new IllegalArgumentException( + sm.getString("endpoint.portOffset.invalid", Integer.valueOf(portOffset))); + } + this.portOffset = portOffset; + } + + + public int getPortWithOffset() { + // Zero is a special case and negative values are invalid + int port = getPort(); + if (port > 0) { + return port + getPortOffset(); + } + return port; + } + + + public final int getLocalPort() { + try { + InetSocketAddress localAddress = getLocalAddress(); + if (localAddress == null) { + return -1; + } + return localAddress.getPort(); + } catch (IOException ioe) { + return -1; + } + } + + + /** + * Address for the server socket. + */ + private InetAddress address; + public InetAddress getAddress() { return address; } + public void setAddress(InetAddress address) { this.address = address; } + + + /** + * Obtain the network address the server socket is bound to. This primarily + * exists to enable the correct address to be used when unlocking the server + * socket since it removes the guess-work involved if no address is + * specifically set. + * + * @return The network address that the server socket is listening on or + * null if the server socket is not currently bound. + * + * @throws IOException If there is a problem determining the currently bound + * socket + */ + protected abstract InetSocketAddress getLocalAddress() throws IOException; + + + /** + * Allows the server developer to specify the acceptCount (backlog) that + * should be used for server sockets. By default, this value + * is 100. + */ + private int acceptCount = 100; + public void setAcceptCount(int acceptCount) { if (acceptCount > 0) { + this.acceptCount = acceptCount; + } } + public int getAcceptCount() { return acceptCount; } + + /** + * Controls when the Endpoint binds the port. true, the default + * binds the port on {@link #init()} and unbinds it on {@link #destroy()}. + * If set to false the port is bound on {@link #start()} and + * unbound on {@link #stop()}. + */ + private boolean bindOnInit = true; + public boolean getBindOnInit() { return bindOnInit; } + public void setBindOnInit(boolean b) { this.bindOnInit = b; } + private volatile BindState bindState = BindState.UNBOUND; + protected BindState getBindState() { + return bindState; + } + + /** + * Keepalive timeout, if not set the soTimeout is used. + */ + private Integer keepAliveTimeout = null; + public int getKeepAliveTimeout() { + if (keepAliveTimeout == null) { + return getConnectionTimeout(); + } else { + return keepAliveTimeout.intValue(); + } + } + public void setKeepAliveTimeout(int keepAliveTimeout) { + this.keepAliveTimeout = Integer.valueOf(keepAliveTimeout); + } + + + /** + * Socket TCP no delay. + * + * @return The current TCP no delay setting for sockets created by this + * endpoint + */ + public boolean getTcpNoDelay() { return socketProperties.getTcpNoDelay();} + public void setTcpNoDelay(boolean tcpNoDelay) { socketProperties.setTcpNoDelay(tcpNoDelay); } + + + /** + * Socket linger. + * + * @return The current socket linger time for sockets created by this + * endpoint + */ + public int getConnectionLinger() { return socketProperties.getSoLingerTime(); } + public void setConnectionLinger(int connectionLinger) { + socketProperties.setSoLingerTime(connectionLinger); + socketProperties.setSoLingerOn(connectionLinger>=0); + } + + + /** + * Socket timeout. + * + * @return The current socket timeout for sockets created by this endpoint + */ + public int getConnectionTimeout() { return socketProperties.getSoTimeout(); } + public void setConnectionTimeout(int soTimeout) { socketProperties.setSoTimeout(soTimeout); } + + /** + * SSL engine. + */ + private boolean SSLEnabled = false; + public boolean isSSLEnabled() { return SSLEnabled; } + public void setSSLEnabled(boolean SSLEnabled) { this.SSLEnabled = SSLEnabled; } + + private int minSpareThreads = 10; + public void setMinSpareThreads(int minSpareThreads) { + this.minSpareThreads = minSpareThreads; + Executor executor = this.executor; + if (internalExecutor && executor instanceof ThreadPoolExecutor) { + // The internal executor should always be an instance of + // org.apache.tomcat.util.threads.ThreadPoolExecutor but it may be + // null if the endpoint is not running. + // This check also avoids various threading issues. + ((ThreadPoolExecutor) executor).setCorePoolSize(minSpareThreads); + } + } + public int getMinSpareThreads() { + return Math.min(getMinSpareThreadsInternal(), getMaxThreads()); + } + private int getMinSpareThreadsInternal() { + if (internalExecutor) { + return minSpareThreads; + } else { + return -1; + } + } + + + /** + * Maximum amount of worker threads. + */ + private int maxThreads = 200; + public void setMaxThreads(int maxThreads) { + this.maxThreads = maxThreads; + Executor executor = this.executor; + if (internalExecutor && executor instanceof ThreadPoolExecutor) { + // The internal executor should always be an instance of + // org.apache.tomcat.util.threads.ThreadPoolExecutor but it may be + // null if the endpoint is not running. + // This check also avoids various threading issues. + ((ThreadPoolExecutor) executor).setMaximumPoolSize(maxThreads); + } + } + public int getMaxThreads() { + if (internalExecutor) { + return maxThreads; + } else { + return -1; + } + } + + + /** + * Amount of time in milliseconds before the internal thread pool stops any idle threads + * if the amount of thread is greater than the minimum amount of spare threads. + */ + private int threadsMaxIdleTime = 60000; + public void setThreadsMaxIdleTime(int threadsMaxIdleTime) { + this.threadsMaxIdleTime = threadsMaxIdleTime; + Executor executor = this.executor; + if (internalExecutor && executor instanceof ThreadPoolExecutor) { + // The internal executor should always be an instance of + // org.apache.tomcat.util.threads.ThreadPoolExecutor but it may be + // null if the endpoint is not running. + // This check also avoids various threading issues. + ((ThreadPoolExecutor) executor).setKeepAliveTime(threadsMaxIdleTime, TimeUnit.MILLISECONDS); + } + } + public int getThreadsMaxIdleTime() { + if (internalExecutor) { + return threadsMaxIdleTime; + } else { + return -1; + } + } + + /** + * Priority of the worker threads. + */ + protected int threadPriority = Thread.NORM_PRIORITY; + public void setThreadPriority(int threadPriority) { + // Can't change this once the executor has started + this.threadPriority = threadPriority; + } + public int getThreadPriority() { + if (internalExecutor) { + return threadPriority; + } else { + return -1; + } + } + + + /** + * Max keep alive requests + */ + private int maxKeepAliveRequests=100; // as in Apache HTTPD server + public int getMaxKeepAliveRequests() { + // Disable keep-alive if the server socket is not bound + if (bindState.isBound()) { + return maxKeepAliveRequests; + } else { + return 1; + } + } + public void setMaxKeepAliveRequests(int maxKeepAliveRequests) { + this.maxKeepAliveRequests = maxKeepAliveRequests; + } + + + /** + * Name of the thread pool, which will be used for naming child threads. + */ + private String name = "TP"; + public void setName(String name) { this.name = name; } + public String getName() { return name; } + + + /** + * Name of domain to use for JMX registration. + */ + private String domain; + public void setDomain(String domain) { this.domain = domain; } + public String getDomain() { return domain; } + + + /** + * The default is true - the created threads will be + * in daemon mode. If set to false, the control thread + * will not be daemon - and will keep the process alive. + */ + private boolean daemon = true; + public void setDaemon(boolean b) { daemon = b; } + public boolean getDaemon() { return daemon; } + + + /** + * Expose asynchronous IO capability. + */ + private boolean useAsyncIO = true; + public void setUseAsyncIO(boolean useAsyncIO) { this.useAsyncIO = useAsyncIO; } + public boolean getUseAsyncIO() { return useAsyncIO; } + + + /** + * Always returns {@code false}. + * + * @return Always {@code false} + * + * @deprecated This code will be removed in Tomcat 11 onwards + */ + @Deprecated + protected boolean getDeferAccept() { + return false; + } + + + /** + * The default behavior is to identify connectors uniquely with address + * and port. However, certain connectors are not using that and need + * some other identifier, which then can be used as a replacement. + * @return the id + */ + public String getId() { + return null; + } + + + protected final List negotiableProtocols = new ArrayList<>(); + public void addNegotiatedProtocol(String negotiableProtocol) { + negotiableProtocols.add(negotiableProtocol); + } + public boolean hasNegotiableProtocols() { + return (negotiableProtocols.size() > 0); + } + + + /** + * Handling of accepted sockets. + */ + private Handler handler = null; + public void setHandler(Handler handler ) { this.handler = handler; } + public Handler getHandler() { return handler; } + + + /** + * Attributes provide a way for configuration to be passed to sub-components + * without the {@link org.apache.coyote.ProtocolHandler} being aware of the + * properties available on those sub-components. + */ + protected HashMap attributes = new HashMap<>(); + + /** + * Generic property setter called when a property for which a specific + * setter already exists within the + * {@link org.apache.coyote.ProtocolHandler} needs to be made available to + * sub-components. The specific setter will call this method to populate the + * attributes. + * + * @param name Name of property to set + * @param value The value to set the property to + */ + public void setAttribute(String name, Object value) { + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("endpoint.setAttribute", name, value)); + } + attributes.put(name, value); + } + /** + * Used by sub-components to retrieve configuration information. + * + * @param key The name of the property for which the value should be + * retrieved + * + * @return The value of the specified property + */ + public Object getAttribute(String key) { + Object value = attributes.get(key); + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("endpoint.getAttribute", key, value)); + } + return value; + } + + + + public boolean setProperty(String name, String value) { + setAttribute(name, value); + final String socketName = "socket."; + try { + if (name.startsWith(socketName)) { + return IntrospectionUtils.setProperty(socketProperties, name.substring(socketName.length()), value); + } else { + return IntrospectionUtils.setProperty(this,name,value,false); + } + }catch ( Exception x ) { + getLog().error(sm.getString("endpoint.setAttributeError", name, value), x); + return false; + } + } + public String getProperty(String name) { + String value = (String) getAttribute(name); + final String socketName = "socket."; + if (value == null && name.startsWith(socketName)) { + Object result = IntrospectionUtils.getProperty(socketProperties, name.substring(socketName.length())); + if (result != null) { + value = result.toString(); + } + } + return value; + } + + /** + * Return the amount of threads that are managed by the pool. + * + * @return the amount of threads that are managed by the pool + */ + public int getCurrentThreadCount() { + Executor executor = this.executor; + if (executor != null) { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getPoolSize(); + } else if (executor instanceof java.util.concurrent.ThreadPoolExecutor) { + return ((java.util.concurrent.ThreadPoolExecutor) executor).getPoolSize(); + } else if (executor instanceof ResizableExecutor) { + return ((ResizableExecutor) executor).getPoolSize(); + } else { + return -1; + } + } else { + return -2; + } + } + + /** + * Return the amount of threads that are in use + * + * @return the amount of threads that are in use + */ + public int getCurrentThreadsBusy() { + Executor executor = this.executor; + if (executor != null) { + if (executor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) executor).getActiveCount(); + } else if (executor instanceof java.util.concurrent.ThreadPoolExecutor) { + return ((java.util.concurrent.ThreadPoolExecutor) executor).getActiveCount(); + } else if (executor instanceof ResizableExecutor) { + return ((ResizableExecutor) executor).getActiveCount(); + } else { + return -1; + } + } else { + return -2; + } + } + + public boolean isRunning() { + return running; + } + + public boolean isPaused() { + return paused; + } + + + public void createExecutor() { + internalExecutor = true; + if (getUseVirtualThreads()) { + executor = new VirtualThreadExecutor(getName() + "-virt-"); + } else { + TaskQueue taskqueue = new TaskQueue(); + TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority()); + executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), getThreadsMaxIdleTime(), + TimeUnit.MILLISECONDS, taskqueue, tf); + taskqueue.setParent((ThreadPoolExecutor) executor); + } + } + + + public void shutdownExecutor() { + Executor executor = this.executor; + if (executor != null && internalExecutor) { + this.executor = null; + if (executor instanceof ThreadPoolExecutor) { + //this is our internal one, so we need to shut it down + ThreadPoolExecutor tpe = (ThreadPoolExecutor) executor; + tpe.shutdownNow(); + long timeout = getExecutorTerminationTimeoutMillis(); + if (timeout > 0) { + try { + tpe.awaitTermination(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Ignore + } + if (tpe.isTerminating()) { + getLog().warn(sm.getString("endpoint.warn.executorShutdown", getName())); + } + } + TaskQueue queue = (TaskQueue) tpe.getQueue(); + queue.setParent(null); + } + } + } + + /** + * Unlock the server socket acceptor threads using bogus connections. + */ + protected void unlockAccept() { + // Only try to unlock the acceptor if it is necessary + if (acceptor == null || acceptor.getState() != AcceptorState.RUNNING) { + return; + } + + InetSocketAddress unlockAddress = null; + InetSocketAddress localAddress = null; + try { + localAddress = getLocalAddress(); + } catch (IOException ioe) { + getLog().debug(sm.getString("endpoint.debug.unlock.localFail", getName()), ioe); + } + if (localAddress == null) { + getLog().warn(sm.getString("endpoint.debug.unlock.localNone", getName())); + return; + } + + try { + unlockAddress = getUnlockAddress(localAddress); + + try (java.net.Socket s = new java.net.Socket()) { + int stmo = 2 * 1000; + int utmo = 2 * 1000; + if (getSocketProperties().getSoTimeout() > stmo) { + stmo = getSocketProperties().getSoTimeout(); + } + if (getSocketProperties().getUnlockTimeout() > utmo) { + utmo = getSocketProperties().getUnlockTimeout(); + } + s.setSoTimeout(stmo); + // Newer MacOS versions (e.g. Ventura 13.2) appear to linger for ~1s on close when linger is disabled. + // That causes delays when running the unit tests. Explicitly enableing linger but with a timeout of + // zero seconds seems to fix the issue. + s.setSoLinger(true, 0); + if (getLog().isTraceEnabled()) { + getLog().trace("About to unlock socket for:" + unlockAddress); + } + s.connect(unlockAddress,utmo); + if (getDeferAccept()) { + /* + * In the case of a deferred accept / accept filters we need to + * send data to wake up the accept. Send OPTIONS * to bypass + * even BSD accept filters. The Acceptor will discard it. + */ + OutputStreamWriter sw; + + sw = new OutputStreamWriter(s.getOutputStream(), "ISO-8859-1"); + sw.write("OPTIONS * HTTP/1.0\r\n" + + "User-Agent: Tomcat wakeup connection\r\n\r\n"); + sw.flush(); + } + if (getLog().isTraceEnabled()) { + getLog().trace("Socket unlock completed for:" + unlockAddress); + } + } + // Wait for up to 1000ms acceptor threads to unlock. Particularly + // for the unit tests, we want to exit this loop as quickly as + // possible. However, we also don't want to trigger excessive CPU + // usage if the unlock takes longer than expected. Therefore, we + // initially wait for the unlock in a tight loop but if that takes + // more than 1ms we start using short sleeps to reduce CPU usage. + long startTime = System.nanoTime(); + while (startTime + 1_000_000_000 > System.nanoTime() && acceptor.getState() == AcceptorState.RUNNING) { + if (startTime + 1_000_000 < System.nanoTime()) { + Thread.sleep(1); + } + } + } catch(Throwable t) { + ExceptionUtils.handleThrowable(t); + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString( + "endpoint.debug.unlock.fail", String.valueOf(getPortWithOffset())), t); + } + } + } + + + private static InetSocketAddress getUnlockAddress(InetSocketAddress localAddress) throws SocketException { + if (localAddress.getAddress().isAnyLocalAddress()) { + // Need a local address of the same type (IPv4 or IPV6) as the + // configured bind address since the connector may be configured + // to not map between types. + InetAddress loopbackUnlockAddress = null; + InetAddress linkLocalUnlockAddress = null; + + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + Enumeration inetAddresses = networkInterface.getInetAddresses(); + while (inetAddresses.hasMoreElements()) { + InetAddress inetAddress = inetAddresses.nextElement(); + if (localAddress.getAddress().getClass().isAssignableFrom(inetAddress.getClass())) { + if (inetAddress.isLoopbackAddress()) { + if (loopbackUnlockAddress == null) { + loopbackUnlockAddress = inetAddress; + } + } else if (inetAddress.isLinkLocalAddress()) { + if (linkLocalUnlockAddress == null) { + linkLocalUnlockAddress = inetAddress; + } + } else { + // Use a non-link local, non-loop back address by default + return new InetSocketAddress(inetAddress, localAddress.getPort()); + } + } + } + } + // Prefer loop back over link local since on some platforms (e.g. + // OSX) some link local addresses are not included when listening on + // all local addresses. + if (loopbackUnlockAddress != null) { + return new InetSocketAddress(loopbackUnlockAddress, localAddress.getPort()); + } + if (linkLocalUnlockAddress != null) { + return new InetSocketAddress(linkLocalUnlockAddress, localAddress.getPort()); + } + // Fallback + return new InetSocketAddress("localhost", localAddress.getPort()); + } else { + return localAddress; + } + } + + + // ---------------------------------------------- Request processing methods + + /** + * Process the given SocketWrapper with the given status. Used to trigger + * processing as if the Poller (for those endpoints that have one) + * selected the socket. + * + * @param socketWrapper The socket wrapper to process + * @param event The socket event to be processed + * @param dispatch Should the processing be performed on a new + * container thread + * + * @return if processing was triggered successfully + */ + public boolean processSocket(SocketWrapperBase socketWrapper, + SocketEvent event, boolean dispatch) { + try { + if (socketWrapper == null) { + return false; + } + SocketProcessorBase sc = null; + if (processorCache != null) { + sc = processorCache.pop(); + } + if (sc == null) { + sc = createSocketProcessor(socketWrapper, event); + } else { + sc.reset(socketWrapper, event); + } + Executor executor = getExecutor(); + if (dispatch && executor != null) { + executor.execute(sc); + } else { + sc.run(); + } + } catch (RejectedExecutionException ree) { + getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree); + return false; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // This means we got an OOM or similar creating a thread, or that + // the pool and its queue are full + getLog().error(sm.getString("endpoint.process.fail"), t); + return false; + } + return true; + } + + + protected abstract SocketProcessorBase createSocketProcessor( + SocketWrapperBase socketWrapper, SocketEvent event); + + + // ------------------------------------------------------- Lifecycle methods + + /* + * NOTE: There is no maintenance of state or checking for valid transitions + * within this class other than ensuring that bind/unbind are called in the + * right place. It is expected that the calling code will maintain state and + * prevent invalid state transitions. + */ + + public abstract void bind() throws Exception; + public abstract void unbind() throws Exception; + + public abstract void startInternal() throws Exception; + public abstract void stopInternal() throws Exception; + + + private void bindWithCleanup() throws Exception { + try { + bind(); + } catch (Throwable t) { + // Ensure open sockets etc. are cleaned up if something goes + // wrong during bind + ExceptionUtils.handleThrowable(t); + unbind(); + throw t; + } + } + + + public final void init() throws Exception { + if (bindOnInit) { + bindWithCleanup(); + bindState = BindState.BOUND_ON_INIT; + } + if (this.domain != null) { + // Register endpoint (as ThreadPool - historical name) + oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\""); + Registry.getRegistry(null, null).registerComponent(this, oname, null); + + ObjectName socketPropertiesOname = new ObjectName(domain + + ":type=SocketProperties,name=\"" + getName() + "\""); + socketProperties.setObjectName(socketPropertiesOname); + Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null); + + for (SSLHostConfig sslHostConfig : findSslHostConfigs()) { + registerJmx(sslHostConfig); + } + } + } + + + private void registerJmx(SSLHostConfig sslHostConfig) { + if (domain == null) { + // Before init the domain is null + return; + } + ObjectName sslOname = null; + try { + sslOname = new ObjectName(domain + ":type=SSLHostConfig,ThreadPool=\"" + + getName() + "\",name=" + ObjectName.quote(sslHostConfig.getHostName())); + sslHostConfig.setObjectName(sslOname); + try { + Registry.getRegistry(null, null).registerComponent(sslHostConfig, sslOname, null); + } catch (Exception e) { + getLog().warn(sm.getString("endpoint.jmxRegistrationFailed", sslOname), e); + } + } catch (MalformedObjectNameException e) { + getLog().warn(sm.getString("endpoint.invalidJmxNameSslHost", + sslHostConfig.getHostName()), e); + } + + for (SSLHostConfigCertificate sslHostConfigCert : sslHostConfig.getCertificates()) { + ObjectName sslCertOname = null; + try { + sslCertOname = new ObjectName(domain + + ":type=SSLHostConfigCertificate,ThreadPool=\"" + getName() + + "\",Host=" + ObjectName.quote(sslHostConfig.getHostName()) + + ",name=" + sslHostConfigCert.getType()); + sslHostConfigCert.setObjectName(sslCertOname); + try { + Registry.getRegistry(null, null).registerComponent( + sslHostConfigCert, sslCertOname, null); + } catch (Exception e) { + getLog().warn(sm.getString("endpoint.jmxRegistrationFailed", sslCertOname), e); + } + } catch (MalformedObjectNameException e) { + getLog().warn(sm.getString("endpoint.invalidJmxNameSslHostCert", + sslHostConfig.getHostName(), sslHostConfigCert.getType()), e); + } + } + } + + + private void unregisterJmx(SSLHostConfig sslHostConfig) { + Registry registry = Registry.getRegistry(null, null); + registry.unregisterComponent(sslHostConfig.getObjectName()); + for (SSLHostConfigCertificate sslHostConfigCert : sslHostConfig.getCertificates()) { + registry.unregisterComponent(sslHostConfigCert.getObjectName()); + } + } + + + public final void start() throws Exception { + if (bindState == BindState.UNBOUND) { + bindWithCleanup(); + bindState = BindState.BOUND_ON_START; + } + startInternal(); + } + + + protected void startAcceptorThread() { + acceptor = new Acceptor<>(this); + String threadName = getName() + "-Acceptor"; + acceptor.setThreadName(threadName); + Thread t = new Thread(acceptor, threadName); + t.setPriority(getAcceptorThreadPriority()); + t.setDaemon(getDaemon()); + t.start(); + } + + + /** + * Pause the endpoint, which will stop it accepting new connections and + * unlock the acceptor. + */ + public void pause() { + if (running && !paused) { + paused = true; + releaseConnectionLatch(); + unlockAccept(); + getHandler().pause(); + } + } + + /** + * Resume the endpoint, which will make it start accepting new connections + * again. + */ + public void resume() { + if (running) { + paused = false; + } + } + + public final void stop() throws Exception { + stopInternal(); + if (bindState == BindState.BOUND_ON_START || bindState == BindState.SOCKET_CLOSED_ON_STOP) { + unbind(); + bindState = BindState.UNBOUND; + } + } + + public final void destroy() throws Exception { + if (bindState == BindState.BOUND_ON_INIT) { + unbind(); + bindState = BindState.UNBOUND; + } + Registry registry = Registry.getRegistry(null, null); + registry.unregisterComponent(oname); + registry.unregisterComponent(socketProperties.getObjectName()); + for (SSLHostConfig sslHostConfig : findSslHostConfigs()) { + unregisterJmx(sslHostConfig); + } + } + + + protected abstract Log getLog(); + + protected Log getLogCertificate() { + return getLog(); + } + + protected LimitLatch initializeConnectionLatch() { + if (maxConnections==-1) { + return null; + } + if (connectionLimitLatch==null) { + connectionLimitLatch = new LimitLatch(getMaxConnections()); + } + return connectionLimitLatch; + } + + private void releaseConnectionLatch() { + LimitLatch latch = connectionLimitLatch; + if (latch!=null) { + latch.releaseAll(); + } + connectionLimitLatch = null; + } + + protected void countUpOrAwaitConnection() throws InterruptedException { + if (maxConnections==-1) { + return; + } + LimitLatch latch = connectionLimitLatch; + if (latch!=null) { + latch.countUpOrAwait(); + } + } + + protected long countDownConnection() { + if (maxConnections==-1) { + return -1; + } + LimitLatch latch = connectionLimitLatch; + if (latch!=null) { + long result = latch.countDown(); + if (result<0) { + getLog().warn(sm.getString("endpoint.warn.incorrectConnectionCount")); + } + return result; + } else { + return -1; + } + } + + + /** + * Close the server socket (to prevent further connections) if the server + * socket was originally bound on {@link #start()} (rather than on + * {@link #init()}). + * + * @see #getBindOnInit() + */ + public final void closeServerSocketGraceful() { + if (bindState == BindState.BOUND_ON_START) { + // Stop accepting new connections + acceptor.stop(-1); + // Release locks that may be preventing the acceptor from stopping + releaseConnectionLatch(); + unlockAccept(); + // Signal to any multiplexed protocols (HTTP/2) that they may wish + // to stop accepting new streams + getHandler().pause(); + // Update the bindState. This has the side-effect of disabling + // keep-alive for any in-progress connections + bindState = BindState.SOCKET_CLOSED_ON_STOP; + try { + doCloseServerSocket(); + } catch (IOException ioe) { + getLog().warn(sm.getString("endpoint.serverSocket.closeFailed", getName()), ioe); + } + } + } + + + /** + * Wait for the client connections to the server to close gracefully. The + * method will return when all of the client connections have closed or the + * method has been waiting for {@code waitTimeMillis}. + * + * @param waitMillis The maximum time to wait in milliseconds for the + * client connections to close. + * + * @return The wait time, if any remaining when the method returned + */ + public final long awaitConnectionsClose(long waitMillis) { + while (waitMillis > 0 && !connections.isEmpty()) { + try { + Thread.sleep(50); + waitMillis -= 50; + } catch (InterruptedException e) { + Thread.interrupted(); + waitMillis = 0; + } + } + return waitMillis; + } + + + /** + * Actually close the server socket but don't perform any other clean-up. + * + * @throws IOException If an error occurs closing the socket + */ + protected abstract void doCloseServerSocket() throws IOException; + + protected abstract U serverSocketAccept() throws Exception; + + protected abstract boolean setSocketOptions(U socket); + + /** + * Close the socket when the connection has to be immediately closed when + * an error occurs while configuring the accepted socket or trying to + * dispatch it for processing. The wrapper associated with the socket will + * be used for the close. + * @param socket The newly accepted socket + */ + protected void closeSocket(U socket) { + SocketWrapperBase socketWrapper = connections.get(socket); + if (socketWrapper != null) { + socketWrapper.close(); + } + } + + /** + * Close the socket. This is used when the connector is not in a state + * which allows processing the socket, or if there was an error which + * prevented the allocation of the socket wrapper. + * @param socket The newly accepted socket + */ + protected abstract void destroySocket(U socket); +} + diff --git a/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java b/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java new file mode 100644 index 0000000..bffb874 --- /dev/null +++ b/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java @@ -0,0 +1,240 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.channels.NetworkChannel; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; + +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; + +public abstract class AbstractJsseEndpoint extends AbstractEndpoint { + + private String sslImplementationName = null; + private int sniParseLimit = 64 * 1024; + + private SSLImplementation sslImplementation = null; + + public String getSslImplementationName() { + return sslImplementationName; + } + + + public void setSslImplementationName(String s) { + this.sslImplementationName = s; + } + + + public SSLImplementation getSslImplementation() { + return sslImplementation; + } + + + public int getSniParseLimit() { + return sniParseLimit; + } + + + public void setSniParseLimit(int sniParseLimit) { + this.sniParseLimit = sniParseLimit; + } + + + protected void initialiseSsl() throws Exception { + if (isSSLEnabled()) { + sslImplementation = SSLImplementation.getInstance(getSslImplementationName()); + + for (SSLHostConfig sslHostConfig : sslHostConfigs.values()) { + createSSLContext(sslHostConfig); + } + + // Validate default SSLHostConfigName + if (sslHostConfigs.get(getDefaultSSLHostConfigName()) == null) { + throw new IllegalArgumentException(sm.getString("endpoint.noSslHostConfig", + getDefaultSSLHostConfigName(), getName())); + } + + } + } + + + @Override + protected void createSSLContext(SSLHostConfig sslHostConfig) throws IllegalArgumentException { + + // HTTP/2 does not permit optional certificate authentication with any + // version of TLS. + if (sslHostConfig.getCertificateVerification().isOptional() && + negotiableProtocols.contains("h2")) { + getLog().warn(sm.getString("sslHostConfig.certificateVerificationWithHttp2", sslHostConfig.getHostName())); + } + + boolean firstCertificate = true; + for (SSLHostConfigCertificate certificate : sslHostConfig.getCertificates(true)) { + SSLUtil sslUtil = sslImplementation.getSSLUtil(certificate); + if (firstCertificate) { + firstCertificate = false; + sslHostConfig.setEnabledProtocols(sslUtil.getEnabledProtocols()); + sslHostConfig.setEnabledCiphers(sslUtil.getEnabledCiphers()); + } + + SSLContext sslContext = certificate.getSslContext(); + SSLContext sslContextGenerated = certificate.getSslContextGenerated(); + // Generate the SSLContext from configuration unless (e.g. embedded) an SSLContext has been provided. + // Need to handle both initial configuration and reload. + // Initial, SSLContext provided - sslContext will be non-null and sslContextGenerated will be null + // Initial, SSLContext not provided - sslContext null and sslContextGenerated will be null + // Reload, SSLContext provided - sslContext will be non-null and sslContextGenerated will be null + // Reload, SSLContext not provided - sslContext non-null and equal to sslContextGenerated + if (sslContext == null || sslContext == sslContextGenerated) { + try { + sslContext = sslUtil.createSSLContext(negotiableProtocols); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + + certificate.setSslContextGenerated(sslContext); + } + + logCertificate(certificate); + } + } + + + protected SSLEngine createSSLEngine(String sniHostName, List clientRequestedCiphers, + List clientRequestedApplicationProtocols) { + SSLHostConfig sslHostConfig = getSSLHostConfig(sniHostName); + + SSLHostConfigCertificate certificate = selectCertificate(sslHostConfig, clientRequestedCiphers); + + SSLContext sslContext = certificate.getSslContext(); + if (sslContext == null) { + throw new IllegalStateException( + sm.getString("endpoint.jsse.noSslContext", sniHostName)); + } + + SSLEngine engine = sslContext.createSSLEngine(); + engine.setUseClientMode(false); + engine.setEnabledCipherSuites(sslHostConfig.getEnabledCiphers()); + engine.setEnabledProtocols(sslHostConfig.getEnabledProtocols()); + + SSLParameters sslParameters = engine.getSSLParameters(); + sslParameters.setUseCipherSuitesOrder(sslHostConfig.getHonorCipherOrder()); + if (clientRequestedApplicationProtocols != null + && clientRequestedApplicationProtocols.size() > 0 + && negotiableProtocols.size() > 0) { + // Only try to negotiate if both client and server have at least + // one protocol in common + // Note: Tomcat does not explicitly negotiate http/1.1 + // TODO: Is this correct? Should it change? + List commonProtocols = new ArrayList<>(negotiableProtocols); + commonProtocols.retainAll(clientRequestedApplicationProtocols); + if (commonProtocols.size() > 0) { + String[] commonProtocolsArray = commonProtocols.toArray(new String[0]); + sslParameters.setApplicationProtocols(commonProtocolsArray); + } + } + switch (sslHostConfig.getCertificateVerification()) { + case NONE: + sslParameters.setNeedClientAuth(false); + sslParameters.setWantClientAuth(false); + break; + case OPTIONAL: + case OPTIONAL_NO_CA: + sslParameters.setWantClientAuth(true); + break; + case REQUIRED: + sslParameters.setNeedClientAuth(true); + break; + } + // The getter (at least in OpenJDK and derivatives) returns a defensive copy + engine.setSSLParameters(sslParameters); + + return engine; + } + + + private SSLHostConfigCertificate selectCertificate( + SSLHostConfig sslHostConfig, List clientCiphers) { + + Set certificates = sslHostConfig.getCertificates(true); + if (certificates.size() == 1) { + return certificates.iterator().next(); + } + + LinkedHashSet serverCiphers = sslHostConfig.getCipherList(); + + List candidateCiphers = new ArrayList<>(); + if (sslHostConfig.getHonorCipherOrder()) { + candidateCiphers.addAll(serverCiphers); + candidateCiphers.retainAll(clientCiphers); + } else { + candidateCiphers.addAll(clientCiphers); + candidateCiphers.retainAll(serverCiphers); + } + + for (Cipher candidate : candidateCiphers) { + for (SSLHostConfigCertificate certificate : certificates) { + if (certificate.getType().isCompatibleWith(candidate.getAu())) { + return certificate; + } + } + } + + // No matches. Just return the first certificate. The handshake will + // then fail due to no matching ciphers. + return certificates.iterator().next(); + } + + + @Override + public void unbind() throws Exception { + for (SSLHostConfig sslHostConfig : sslHostConfigs.values()) { + for (SSLHostConfigCertificate certificate : sslHostConfig.getCertificates()) { + /* + * Only remove any generated SSLContext. If the SSLContext was provided it is left in place in case the + * endpoint is re-started. + */ + certificate.setSslContextGenerated(null); + } + } + } + + + protected abstract NetworkChannel getServerSocket(); + + + @Override + protected final InetSocketAddress getLocalAddress() throws IOException { + NetworkChannel serverSock = getServerSocket(); + if (serverSock == null) { + return null; + } + SocketAddress sa = serverSock.getLocalAddress(); + if (sa instanceof InetSocketAddress) { + return (InetSocketAddress) sa; + } + return null; + } +} diff --git a/java/org/apache/tomcat/util/net/Acceptor.java b/java/org/apache/tomcat/util/net/Acceptor.java new file mode 100644 index 0000000..b1083b2 --- /dev/null +++ b/java/org/apache/tomcat/util/net/Acceptor.java @@ -0,0 +1,223 @@ +/* + * 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.tomcat.util.net; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +public class Acceptor implements Runnable { + + private static final Log log = LogFactory.getLog(Acceptor.class); + private static final StringManager sm = StringManager.getManager(Acceptor.class); + + private static final int INITIAL_ERROR_DELAY = 50; + private static final int MAX_ERROR_DELAY = 1600; + + private final AbstractEndpoint endpoint; + private String threadName; + /* + * Tracked separately rather than using endpoint.isRunning() as calls to + * endpoint.stop() and endpoint.start() in quick succession can cause the + * acceptor to continue running when it should terminate. + */ + private volatile boolean stopCalled = false; + private final CountDownLatch stopLatch = new CountDownLatch(1); + protected volatile AcceptorState state = AcceptorState.NEW; + + + public Acceptor(AbstractEndpoint endpoint) { + this.endpoint = endpoint; + } + + + public final AcceptorState getState() { + return state; + } + + + final void setThreadName(final String threadName) { + this.threadName = threadName; + } + + + final String getThreadName() { + return threadName; + } + + + @Override + public void run() { + + int errorDelay = 0; + long pauseStart = 0; + + try { + // Loop until we receive a shutdown command + while (!stopCalled) { + + // Loop if endpoint is paused. + // There are two likely scenarios here. + // The first scenario is that Tomcat is shutting down. In this + // case - and particularly for the unit tests - we want to exit + // this loop as quickly as possible. The second scenario is a + // genuine pause of the connector. In this case we want to avoid + // excessive CPU usage. + // Therefore, we start with a tight loop but if there isn't a + // rapid transition to stop then sleeps are introduced. + // < 1ms - tight loop + // 1ms to 10ms - 1ms sleep + // > 10ms - 10ms sleep + while (endpoint.isPaused() && !stopCalled) { + if (state != AcceptorState.PAUSED) { + pauseStart = System.nanoTime(); + // Entered pause state + state = AcceptorState.PAUSED; + } + if ((System.nanoTime() - pauseStart) > 1_000_000) { + // Paused for more than 1ms + try { + if ((System.nanoTime() - pauseStart) > 10_000_000) { + Thread.sleep(10); + } else { + Thread.sleep(1); + } + } catch (InterruptedException e) { + // Ignore + } + } + } + + if (stopCalled) { + break; + } + state = AcceptorState.RUNNING; + + try { + //if we have reached max connections, wait + endpoint.countUpOrAwaitConnection(); + + // Endpoint might have been paused while waiting for latch + // If that is the case, don't accept new connections + if (endpoint.isPaused()) { + continue; + } + + U socket = null; + try { + // Accept the next incoming connection from the server + // socket + socket = endpoint.serverSocketAccept(); + } catch (Exception ioe) { + // We didn't get a socket + endpoint.countDownConnection(); + if (endpoint.isRunning()) { + // Introduce delay if necessary + errorDelay = handleExceptionWithDelay(errorDelay); + // re-throw + throw ioe; + } else { + break; + } + } + // Successful accept, reset the error delay + errorDelay = 0; + + // Configure the socket + if (!stopCalled && !endpoint.isPaused()) { + // setSocketOptions() will hand the socket off to + // an appropriate processor if successful + if (!endpoint.setSocketOptions(socket)) { + endpoint.closeSocket(socket); + } + } else { + endpoint.destroySocket(socket); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + String msg = sm.getString("endpoint.accept.fail"); + log.error(msg, t); + } + } + } finally { + stopLatch.countDown(); + } + state = AcceptorState.ENDED; + } + + + /** + * Signals the Acceptor to stop, optionally waiting for that stop process + * to complete before returning. If a wait is requested and the stop does + * not complete in that time a warning will be logged. + * + * @param waitSeconds The time to wait in seconds. Use a value less than + * zero for no wait. + */ + public void stop(int waitSeconds) { + stopCalled = true; + if (waitSeconds > 0) { + try { + if (!stopLatch.await(waitSeconds, TimeUnit.SECONDS)) { + log.warn(sm.getString("acceptor.stop.fail", getThreadName())); + } + } catch (InterruptedException e) { + log.warn(sm.getString("acceptor.stop.interrupted", getThreadName()), e); + } + } + } + + + /** + * Handles exceptions where a delay is required to prevent a Thread from + * entering a tight loop which will consume CPU and may also trigger large + * amounts of logging. For example, this can happen if the ulimit for open + * files is reached. + * + * @param currentErrorDelay The current delay being applied on failure + * @return The delay to apply on the next failure + */ + protected int handleExceptionWithDelay(int currentErrorDelay) { + // Don't delay on first exception + if (currentErrorDelay > 0) { + try { + Thread.sleep(currentErrorDelay); + } catch (InterruptedException e) { + // Ignore + } + } + + // On subsequent exceptions, start the delay at 50ms, doubling the delay + // on every subsequent exception until the delay reaches 1.6 seconds. + if (currentErrorDelay == 0) { + return INITIAL_ERROR_DELAY; + } else if (currentErrorDelay < MAX_ERROR_DELAY) { + return currentErrorDelay * 2; + } else { + return MAX_ERROR_DELAY; + } + } + + + public enum AcceptorState { + NEW, RUNNING, PAUSED, ENDED + } +} diff --git a/java/org/apache/tomcat/util/net/ApplicationBufferHandler.java b/java/org/apache/tomcat/util/net/ApplicationBufferHandler.java new file mode 100644 index 0000000..d0a41b4 --- /dev/null +++ b/java/org/apache/tomcat/util/net/ApplicationBufferHandler.java @@ -0,0 +1,48 @@ +/* + * 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.tomcat.util.net; + +import java.nio.ByteBuffer; + +/** + * Callback interface to be able to expand buffers when buffer overflow + * exceptions happen or to replace buffers + */ +public interface ApplicationBufferHandler { + + ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); + + ApplicationBufferHandler EMPTY = new ApplicationBufferHandler() { + @Override + public void expand(int newSize) { + } + @Override + public void setByteBuffer(ByteBuffer buffer) { + } + @Override + public ByteBuffer getByteBuffer() { + return EMPTY_BUFFER; + } + }; + + void setByteBuffer(ByteBuffer buffer); + + ByteBuffer getByteBuffer(); + + void expand(int size); + +} diff --git a/java/org/apache/tomcat/util/net/Constants.java b/java/org/apache/tomcat/util/net/Constants.java new file mode 100644 index 0000000..9cda5e1 --- /dev/null +++ b/java/org/apache/tomcat/util/net/Constants.java @@ -0,0 +1,43 @@ +/* + * 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.tomcat.util.net; + +public class Constants { + + /** + * Name of the system property containing + * the tomcat instance installation path + */ + public static final String CATALINA_BASE_PROP = "catalina.base"; + + /** + * JSSE and OpenSSL protocol names + */ + public static final String SSL_PROTO_ALL = "all"; + public static final String SSL_PROTO_TLS = "TLS"; + public static final String SSL_PROTO_TLSv1_3 = "TLSv1.3"; + public static final String SSL_PROTO_TLSv1_2 = "TLSv1.2"; + public static final String SSL_PROTO_TLSv1_1 = "TLSv1.1"; + // Two different forms for TLS 1.0 + public static final String SSL_PROTO_TLSv1_0 = "TLSv1.0"; + public static final String SSL_PROTO_TLSv1 = "TLSv1"; + public static final String SSL_PROTO_SSLv3 = "SSLv3"; + public static final String SSL_PROTO_SSLv2 = "SSLv2"; + public static final String SSL_PROTO_SSLv2Hello = "SSLv2Hello"; + + public static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); +} diff --git a/java/org/apache/tomcat/util/net/DispatchType.java b/java/org/apache/tomcat/util/net/DispatchType.java new file mode 100644 index 0000000..5914c87 --- /dev/null +++ b/java/org/apache/tomcat/util/net/DispatchType.java @@ -0,0 +1,38 @@ +/* + * 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.tomcat.util.net; + +/** + * This enumeration lists the different types of dispatches that request + * processing can trigger. In this instance, dispatch means re-process this + * request using the given socket status. + */ +public enum DispatchType { + + NON_BLOCKING_READ(SocketEvent.OPEN_READ), + NON_BLOCKING_WRITE(SocketEvent.OPEN_WRITE); + + private final SocketEvent status; + + DispatchType(SocketEvent status) { + this.status = status; + } + + public SocketEvent getSocketStatus() { + return status; + } +} diff --git a/java/org/apache/tomcat/util/net/IPv6Utils.java b/java/org/apache/tomcat/util/net/IPv6Utils.java new file mode 100644 index 0000000..19b5d83 --- /dev/null +++ b/java/org/apache/tomcat/util/net/IPv6Utils.java @@ -0,0 +1,252 @@ +/* + * 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.tomcat.util.net; + +/** + *

    IPv6 utilities. + *

    For the moment, it only contains function to canonicalize IPv6 address + * into RFC 5952 form. + */ +public class IPv6Utils { + + private static final int MAX_NUMBER_OF_GROUPS = 8; + private static final int MAX_GROUP_LENGTH = 4; + + /** + *

    Convert IPv6 address into RFC 5952 form. + * E.g. 2001:db8:0:1:0:0:0:1 -> 2001:db8:0:1::1

    + * + *

    Method is null safe, and if IPv4 address or host name is passed to the + * method it is returned without any processing.

    + * + *

    Method also supports IPv4 in IPv6 (e.g. 0:0:0:0:0:ffff:192.0.2.1 -> + * ::ffff:192.0.2.1), and zone ID (e.g. fe80:0:0:0:f0f0:c0c0:1919:1234%4 + * -> fe80::f0f0:c0c0:1919:1234%4).

    + * + *

    The behaviour of this method is undefined if an invalid IPv6 address + * is passed in as input.

    + * + * @param ipv6Address String representing valid IPv6 address. + * @return String representing IPv6 in canonical form. + * @throws IllegalArgumentException if IPv6 format is unacceptable. + */ + public static String canonize(String ipv6Address) throws IllegalArgumentException { + + if (ipv6Address == null) { + return null; + } + + // Definitely not an IPv6, return untouched input. + if (!mayBeIPv6Address(ipv6Address)) { + return ipv6Address; + } + + // Length without zone ID (%zone) or IPv4 address + int ipv6AddressLength = ipv6Address.length(); + if (ipv6Address.contains(".")) { + // IPv4 in IPv6 + // e.g. 0:0:0:0:0:FFFF:127.0.0.1 + int lastColonPos = ipv6Address.lastIndexOf(':'); + int lastColonsPos = ipv6Address.lastIndexOf("::"); + if (lastColonsPos >= 0 && lastColonPos == lastColonsPos + 1) { + /* + * IPv6 part ends with two consecutive colons, + * last colon is part of IPv6 format. + * e.g. ::127.0.0.1 + */ + ipv6AddressLength = lastColonPos + 1; + } else { + /* + * IPv6 part ends with only one colon, + * last colon is not part of IPv6 format. + * e.g. ::FFFF:127.0.0.1 + */ + ipv6AddressLength = lastColonPos; + } + } else if (ipv6Address.contains("%")) { + // Zone ID + // e.g. fe80:0:0:0:f0f0:c0c0:1919:1234%4 + ipv6AddressLength = ipv6Address.lastIndexOf('%'); + } + + StringBuilder result = new StringBuilder(); + char [][] groups = new char[MAX_NUMBER_OF_GROUPS][MAX_GROUP_LENGTH]; + int groupCounter = 0; + int charInGroupCounter = 0; + + // Index of the current zeroGroup, -1 means not found. + int zeroGroupIndex = -1; + int zeroGroupLength = 0; + + // maximum length zero group, if there is more then one, then first one + int maxZeroGroupIndex = -1; + int maxZeroGroupLength = 0; + + boolean isZero = true; + boolean groupStart = true; + + /* + * Two consecutive colons, initial expansion. + * e.g. 2001:db8:0:0:1::1 -> 2001:db8:0:0:1:0:0:1 + */ + + StringBuilder expanded = new StringBuilder(ipv6Address); + int colonsPos = ipv6Address.indexOf("::"); + int length = ipv6AddressLength; + int change = 0; + + if (colonsPos >= 0 && colonsPos < ipv6AddressLength - 1) { + int colonCounter = 0; + for (int i = 0; i < ipv6AddressLength; i++) { + if (ipv6Address.charAt(i) == ':') { + colonCounter++; + } + } + + if (colonsPos == 0) { + expanded.insert(0, "0"); + change = change + 1; + } + + for (int i = 0; i < MAX_NUMBER_OF_GROUPS - colonCounter; i++) { + expanded.insert(colonsPos + 1, "0:"); + change = change + 2; + } + + + if (colonsPos == ipv6AddressLength - 2) { + expanded.setCharAt(colonsPos + change + 1, '0'); + } else { + expanded.deleteCharAt(colonsPos + change + 1); + change = change - 1; + } + length = length + change; + } + + + // Processing one char at the time + for (int charCounter = 0; charCounter < length; charCounter++) { + char c = expanded.charAt(charCounter); + if (c >= 'A' && c <= 'F') { + c = (char) (c + 32); + } + if (c != ':') { + groups[groupCounter][charInGroupCounter] = c; + if (!(groupStart && c == '0')) { + ++charInGroupCounter; + groupStart = false; + } + if (c != '0') { + isZero = false; + } + } + if (c == ':' || charCounter == (length - 1)) { + // We reached end of current group + if (isZero) { + ++zeroGroupLength; + if (zeroGroupIndex == -1) { + zeroGroupIndex = groupCounter; + } + } + + if (!isZero || charCounter == (length - 1)) { + // We reached end of zero group + if (zeroGroupLength > maxZeroGroupLength) { + maxZeroGroupLength = zeroGroupLength; + maxZeroGroupIndex = zeroGroupIndex; + } + zeroGroupLength = 0; + zeroGroupIndex = -1; + } + ++groupCounter; + charInGroupCounter = 0; + isZero = true; + groupStart = true; + } + } + + int numberOfGroups = groupCounter; + + // Output results + for (groupCounter = 0; groupCounter < numberOfGroups; groupCounter++) { + if (maxZeroGroupLength <= 1 || groupCounter < maxZeroGroupIndex + || groupCounter >= maxZeroGroupIndex + maxZeroGroupLength) { + for (int j = 0; j < MAX_GROUP_LENGTH; j++) { + if (groups[groupCounter][j] != 0) { + result.append(groups[groupCounter][j]); + } + } + if (groupCounter < (numberOfGroups - 1) + && (groupCounter != maxZeroGroupIndex - 1 + || maxZeroGroupLength <= 1)) { + result.append(':'); + } + } else if (groupCounter == maxZeroGroupIndex) { + result.append("::"); + } + } + + // Solve problem with three colons in IPv4 in IPv6 format + // e.g. 0:0:0:0:0:0:127.0.0.1 -> :::127.0.0.1 -> ::127.0.0.1 + int resultLength = result.length(); + if (result.charAt(resultLength - 1) == ':' && ipv6AddressLength < ipv6Address.length() + && ipv6Address.charAt(ipv6AddressLength) == ':') { + result.delete(resultLength - 1, resultLength); + } + + /* + * Append IPv4 from IPv4-in-IPv6 format or Zone ID + */ + for (int i = ipv6AddressLength; i < ipv6Address.length(); i++) { + result.append(ipv6Address.charAt(i)); + } + + return result.toString(); + } + + /** + * Heuristic check if string might be an IPv6 address. + * + * @param input Any string or null + * @return true, if input string contains only hex digits and at least two colons, before '.' or '%' character + */ + static boolean mayBeIPv6Address(String input) { + if (input == null) { + return false; + } + + int colonsCounter = 0; + int length = input.length(); + for (int i = 0; i < length; i++) { + char c = input.charAt(i); + if (c == '.' || c == '%') { + // IPv4 in IPv6 or Zone ID detected, end of checking. + break; + } + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F') || c == ':')) { + return false; + } else if (c == ':') { + colonsCounter++; + } + } + if (colonsCounter < 2) { + return false; + } + return true; + } +} diff --git a/java/org/apache/tomcat/util/net/LocalStrings.properties b/java/org/apache/tomcat/util/net/LocalStrings.properties new file mode 100644 index 0000000..8557463 --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings.properties @@ -0,0 +1,173 @@ +# 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. + +acceptor.stop.fail=The acceptor thread [{0}] did not stop cleanly +acceptor.stop.interrupted=Interrupt was received while waiting for the acceptor thread [{0}] to stop + +channel.nio.interrupted=The current thread was interrupted +channel.nio.ssl.appInputNotEmpty=Application input buffer still contains data. Data would have been lost. +channel.nio.ssl.appOutputNotEmpty=Application output buffer still contains data. Data would have been lost. +channel.nio.ssl.closeSilentError=As expected, there was an exception trying to close the connection cleanly. +channel.nio.ssl.closing=Channel is in closing state. +channel.nio.ssl.eofDuringHandshake=EOF during handshake. +channel.nio.ssl.expandNetInBuffer=Expanding network input buffer to [{0}] bytes +channel.nio.ssl.expandNetOutBuffer=Expanding network output buffer to [{0}] bytes +channel.nio.ssl.foundHttp=Found an plain text HTTP request on what should be an encrypted TLS connection +channel.nio.ssl.handshakeError=Handshake error +channel.nio.ssl.incompleteHandshake=Handshake incomplete, you must complete handshake before reading data. +channel.nio.ssl.invalidCloseState=Invalid close state, will not send network data. +channel.nio.ssl.invalidStatus=Unexpected status [{0}]. +channel.nio.ssl.netInputNotEmpty=Network input buffer still contains data. Handshake will fail. +channel.nio.ssl.netOutputNotEmpty=Network output buffer still contains data. Handshake will fail. +channel.nio.ssl.notHandshaking=NOT_HANDSHAKING during handshake +channel.nio.ssl.pendingWriteDuringClose=Pending write, so remaining data in the network buffer, can't send SSL close message, socket closed anyway +channel.nio.ssl.remainingDataDuringClose=Remaining data in the network buffer, can't send SSL close message, socket closes anyway +channel.nio.ssl.sniDefault=Unable to buffer enough data to determine requested SNI host name. Using default +channel.nio.ssl.sniHostName=The SNI host name extracted for connection [{0}] was [{1}] +channel.nio.ssl.timeoutDuringHandshake=Timeout during handshake. +channel.nio.ssl.unexpectedStatusDuringUnwrap=Unexpected status [{0}] during handshake UNWRAP. +channel.nio.ssl.unexpectedStatusDuringWrap=Unexpected status [{0}] during handshake WRAP. +channel.nio.ssl.unwrapFail=Unable to unwrap data, invalid status [{0}] +channel.nio.ssl.unwrapFailResize=Unable to unwrap data because buffer is too small, invalid status [{0}] +channel.nio.ssl.wrapFail=Unable to wrap data, invalid status [{0}] + +endpoint.accept.fail=Socket accept failed +endpoint.alpn.fail=Failed to configure endpoint for ALPN using [{0}] +endpoint.alpn.negotiated=Negotiated [{0}] protocol using ALPN +endpoint.apr.failSslContextMake=Unable to create SSLContext. Check that SSLEngine is enabled in the AprLifecycleListener, the AprLifecycleListener has initialised correctly and that a valid SSLProtocol has been specified +endpoint.apr.invalidSslProtocol=An invalid value [{0}] was provided for the SSLProtocol attribute +endpoint.debug.channelCloseFail=Failed to close channel +endpoint.debug.destroySocket=Destroying socket [{0}] +endpoint.debug.handlerRelease=Handler failed to release socket wrapper +endpoint.debug.pollerAdd=Add to addList socket [{0}], timeout [{1}], flags [{2}] +endpoint.debug.pollerAddDo=Add to poller socket [{0}] +endpoint.debug.pollerProcess=Processing socket [{0}] for event(s) [{1}] +endpoint.debug.pollerRemove=Attempting to remove [{0}] from poller +endpoint.debug.pollerRemoved=Removed [{0}] from poller +endpoint.debug.registerRead=Registered read interest for [{0}] +endpoint.debug.registerWrite=Registered write interest for [{0}] +endpoint.debug.socket=socket [{0}] +endpoint.debug.socketTimeout=Timing out [{0}] +endpoint.debug.unlock.fail=Caught exception trying to unlock accept on port [{0}] +endpoint.debug.unlock.localFail=Unable to determine local address for [{0}] +endpoint.debug.unlock.localNone=Failed to unlock acceptor for [{0}] because the local address was not available +endpoint.duplicateSslHostName=Multiple SSLHostConfig elements were provided for the host name [{0}]. Host names must be unique. +endpoint.err.close=Caught exception trying to close socket +endpoint.err.duplicateAccept=Duplicate socket accept detected. This is a known Linux kernel bug. The original connection has been processed normally and the duplicate has been ignored. The client should be unaffected. Updating the OS to a version that uses kernel 5.10 or later should fix the duplicate accept bug. +endpoint.err.handshake=Handshake failed for client connection from IP address [{0}] and port [{1}] +endpoint.err.unexpected=Unexpected error processing socket +endpoint.executor.fail=Executor rejected socket [{0}] for processing +endpoint.getAttribute=[{0}] is [{1}] +endpoint.init.bind=Socket bind failed: [{0}] [{1}] +endpoint.init.bind.inherited=No inherited channel while the connector was configured to use one +endpoint.init.listen=Socket listen failed: [{0}] [{1}] +endpoint.init.unixnotavail=Unix Domain Socket support not available +endpoint.invalidJmxNameSslHost=Unable to generate a valid JMX object name for the SSLHostConfig associated with host [{0}] +endpoint.invalidJmxNameSslHostCert=Unable to generate a valid JMX object name for the SSLHostConfigCertificate associated with host [{0}] and certificate type [{1}] +endpoint.jmxRegistrationFailed=Failed to register the JMX object with name [{0}] +endpoint.jsse.noSslContext=No SSLContext could be found for the host name [{0}] +endpoint.launch.fail=Failed to launch new runnable +endpoint.nio.keyProcessingError=Error processing selection key +endpoint.nio.latchMustBeZero=Latch must be at count zero or null +endpoint.nio.nullLatch=Latch cannot be null +endpoint.nio.nullSocketChannel=Invalid null socket channel while processing poller event +endpoint.nio.perms.readFail=Failed to set read permissions for Unix domain socket [{0}] +endpoint.nio.perms.writeFail=Failed to set write permissions for Unix domain socket [{0}] +endpoint.nio.registerFail=Failed to register socket with selector from poller +endpoint.nio.selectorCloseFail=Failed to close selector when closing the poller +endpoint.nio.selectorLoopError=Error in selector loop +endpoint.nio.stopLatchAwaitFail=The pollers did not stop within the expected time +endpoint.nio.stopLatchAwaitInterrupted=This thread was interrupted while waiting for the pollers to stop +endpoint.nio.timeoutCme=Exception during processing of timeouts. The code has been checked repeatedly and no concurrent modification has been found. If you are able to repeat this error please open a Tomcat bug and provide the steps to reproduce. +endpoint.nio2.exclusiveExecutor=The NIO2 connector requires an exclusive executor to operate properly on shutdown +endpoint.nio2.executorService=The NIO2 connector requires an executor service, the internal JVM threads will be used +endpoint.noSslHostConfig=No SSLHostConfig element was found with the hostName [{0}] to match the defaultSSLHostConfigName for the connector [{1}] +endpoint.noSslHostName=No host name was provided for the SSL host configuration +endpoint.poll.error=Unexpected poller error +endpoint.poll.fail=Critical poller failure (restarting poller): [{0}] [{1}] +endpoint.poll.initfail=Poller creation failed +endpoint.poll.limitedpollsize=Failed to create poller with specified size of [{0}] +endpoint.pollerThreadStop=The poller thread failed to stop in a timely manner +endpoint.portOffset.invalid=The value [{0}] for portOffset is not valid as portOffset may not be negative +endpoint.process.fail=Error allocating socket processor +endpoint.processing.fail=Error running socket processor +endpoint.rejectedExecution=Socket processing request was rejected for [{0}] +endpoint.removeDefaultSslHostConfig=The default SSLHostConfig (named [{0}]) may not be removed +endpoint.sendfile.addfail=Sendfile failure: [{0}] [{1}] +endpoint.sendfile.closeError=Error closing sendfile resources +endpoint.sendfile.error=Unexpected sendfile error +endpoint.sendfile.tooMuchData=Sendfile configured to send more data than was available +endpoint.sendfileThreadStop=The sendfile thread failed to stop in a timely manner +endpoint.serverSocket.closeFailed=Failed to close server socket for [{0}] +endpoint.setAttribute=Set [{0}] to [{1}] +endpoint.setAttributeError=Unable to set attribute [{0}] to [{1}] +endpoint.socketOptionsError=Error setting socket options +endpoint.timeout.err=Error processing socket timeout +endpoint.tls.cert.encodingError=Certificate fingerprints not available +endpoint.tls.cert.noCerts=Certificate details not available as the certificate chain returned from the SSLContext was empty +endpoint.tls.info=Connector [{0}], TLS virtual host [{1}], certificate type [{2}] configured from {3} with trust store [{4}] +endpoint.tls.info.cert.keystore=keystore [{0}] using alias [{1}] +endpoint.tls.info.cert.pem=key [{0}], certificate [{1}] and certificate chain [{2}] +endpoint.unknownSslHostName=The SSL host name [{0}] is not recognised for this endpoint +endpoint.warn.executorShutdown=The executor associated with thread pool [{0}] has not fully shutdown. Some application threads may still be running. +endpoint.warn.incorrectConnectionCount=Incorrect connection count, multiple calls to socket.close for the same socket. +endpoint.warn.noLocalAddr=Unable to determine local address for socket [{0}] +endpoint.warn.noLocalName=Unable to determine local host name for socket [{0}] +endpoint.warn.noLocalPort=Unable to determine local port for socket [{0}] +endpoint.warn.noRemoteAddr=Unable to determine remote address for socket [{0}] +endpoint.warn.noRemoteHost=Unable to determine remote host name for socket [{0}] +endpoint.warn.noRemotePort=Unable to determine remote port for socket [{0}] +endpoint.warn.noUtilityExecutor=No utility executor was set, creating one +endpoint.warn.unlockAcceptorFailed=Acceptor thread [{0}] failed to unlock. Forcing hard socket shutdown. + +sniExtractor.clientHelloInvalid=The ClientHello message was not correctly formatted +sniExtractor.clientHelloTooBig=The ClientHello was not presented in a single TLS record so no SNI information could be extracted +sniExtractor.tooEarly=It is illegal to call this method before the client hello has been parsed + +socket.closed=The socket associated with this connection has been closed. +socket.sslreneg=Exception re-negotiating SSL connection + +socketWrapper.readTimeout=Read timeout +socketWrapper.writeTimeout=Write timeout + +sslHostConfig.certificate.notype=Multiple certificates were specified and at least one is missing the required attribute type +sslHostConfig.certificateVerificationInvalid=The certificate verification value [{0}] is not recognised +sslHostConfig.certificateVerificationWithHttp2=The TLS virtual host [{0}] is configured for optional certificate verification and the enclosing connector is configured to support upgrade to h2. HTTP/2 over TLS does not permit optional certificate verification. +sslHostConfig.fileNotFound=Configured file [{0}] does not exist +sslHostConfig.invalid_truststore_password=The provided trust store password could not be used to unlock and/or validate the trust store. Retrying to access the trust store with a null password which will skip validation. +sslHostConfig.mismatch=The property [{0}] was set on the SSLHostConfig named [{1}] and is for the [{2}] configuration syntax but the SSLHostConfig is being used with the [{3}] configuration syntax +sslHostConfig.opensslconf.alreadyset=Attempt to set another OpenSSLConf ignored +sslHostConfig.opensslconf.null=Attempt to set null OpenSSLConf ignored +sslHostConfig.prefix_missing=The protocol [{0}] was added to the list of protocols on the SSLHostConfig named [{1}]. Check if a +/- prefix is missing. + +sslHostConfigCertificate.mismatch=The property [{0}] was set on the SSLHostConfigCertificate named [{1}] and is for certificate storage type [{2}] but the certificate is being used with a storage of type [{3}] + +sslImplementation.cnfe=Unable to create SSLImplementation for class [{0}] + +sslUtilBase.active=The [{0}] that are active are : [{1}] +sslUtilBase.aliasIgnored=FIPS enabled so alias name [{0}] will be ignored. If there is more than one key in the key store, the key used will depend on the key store implementation +sslUtilBase.alias_no_key_entry=Alias name [{0}] does not identify a key entry +sslUtilBase.invalidTrustManagerClassName=The trustManagerClassName provided [{0}] does not implement javax.net.ssl.TrustManager +sslUtilBase.keystore_load_failed=Failed to load keystore type [{0}] with path [{1}] due to [{2}] +sslUtilBase.noCertFile=SSLHostConfig attribute certificateFile must be defined when using an SSL connector +sslUtilBase.noCrlSupport=The truststoreProvider [{0}] does not support the certificateRevocationFile configuration option +sslUtilBase.noKeys=No aliases for private keys found in key store +sslUtilBase.noVerificationDepth=The truststoreProvider [{0}] does not support the certificateVerificationDepth configuration option +sslUtilBase.noneSupported=None of the [{0}] specified are supported by the SSL engine : [{1}] +sslUtilBase.skipped=Tomcat interprets the [{0}] attribute in a manner consistent with the latest OpenSSL development branch. Some of the specified [{0}] are not supported by the configured SSL engine for this connector (which may use JSSE or an older OpenSSL version) and have been skipped: [{1}] +sslUtilBase.ssl3=SSLv3 has been explicitly enabled. This protocol is known to be insecure. +sslUtilBase.tls13.auth=The JSSE TLS 1.3 implementation does not support post handshake authentication (PHA) and is therefore incompatible with optional certificate authentication +sslUtilBase.trustedCertNotChecked=The validity dates of the trusted certificate with alias [{0}] were not checked as the certificate was of an unknown type +sslUtilBase.trustedCertNotValid=The trusted certificate with alias [{0}] and DN [{1}] is not valid due to [{2}]. Certificates signed by this trusted certificate WILL be accepted diff --git a/java/org/apache/tomcat/util/net/LocalStrings_cs.properties b/java/org/apache/tomcat/util/net/LocalStrings_cs.properties new file mode 100644 index 0000000..a1647aa --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_cs.properties @@ -0,0 +1,38 @@ +# 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. + +channel.nio.ssl.sniDefault=Nelze naÄíst dostatek dat k urÄení poÅ™adovaného SNI host jména. Je použit default +channel.nio.ssl.sniHostName=Host name SNI serveru získané pro spojení [{0}] bylo [{1}] +channel.nio.ssl.unwrapFail=Nelze rozbalit data, neplatný status [{0}] + +endpoint.debug.channelCloseFail=UzavÅ™ení kanálu selhalo +endpoint.invalidJmxNameSslHostCert=Nelze vygenerovat platné JMX jméno objektu pro SSLHostConfigCertificate pÅ™iÅ™azené k hostu [{0}] a typu certifikátu [{1}] +endpoint.nio.stopLatchAwaitInterrupted=Toto vlákno bylo pÅ™eruÄeno pÅ™i Äekání na ukonÄení ÄítaÄů (poller) +endpoint.noSslHostConfig=Žádný SSLHostConfig element nebyl nalezen pro hostName [{0}] odpovídající defaultSSLHostConfigName pro konektor [{1}] +endpoint.poll.fail=Kritické selhání ÄítaÄe (restartování ÄítaÄe): [{0}] [{1}] +endpoint.poll.limitedpollsize=Selhalo vytvoÅ™ení ÄítaÄe pro specifikovanou velikost [{0}] +endpoint.sendfile.addfail=Selhání Sendfile: [{0}] [{1}] +endpoint.sendfileThreadStop=Zastavení vlákna pro odesílání souborů selhalo ve stanoveném Äase +endpoint.serverSocket.closeFailed=Selhalo uzavÅ™ení server socketu pro [{0}] +endpoint.setAttribute=Nastavte [{0}] na [{1}] +endpoint.warn.incorrectConnectionCount=Nesprávný poÄet spojení, více volání metody socket.close pro stejný socket. +endpoint.warn.noLocalName=Nelze urÄit lokální jméno serveru pro socket [{0}] + +socket.closed=Socket pÅ™iÅ™azený k tomuto spojení byl uzavÅ™en. + +sslHostConfig.fileNotFound=KonfiguraÄní soubor [{0}] neexistuje + +sslUtilBase.noneSupported=Žádný z uvedených [{0}] není podporován SSL enginem: [{1}] +sslUtilBase.ssl3=SSLv3 byl explicitnÄ› povolen. Tento protokol není bezpeÄný. diff --git a/java/org/apache/tomcat/util/net/LocalStrings_de.properties b/java/org/apache/tomcat/util/net/LocalStrings_de.properties new file mode 100644 index 0000000..1552d0d --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_de.properties @@ -0,0 +1,41 @@ +# 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. + +channel.nio.ssl.sniDefault=Puffer ist nicht groß genug um den angefragten SNI Host-Namen zu ermitteln. Benutze den Standard Wert +channel.nio.ssl.sniHostName=Aus Verbindung [{0}] konnte der SNI Hostname [{1}] extrahiert werden +channel.nio.ssl.unwrapFail=Daten können nicht entpackt werden, ungültiger Status [{0}] + +endpoint.debug.channelCloseFail=Kanal konnte nicht geschlossen werden +endpoint.init.bind=Binden des Sockets fehlgeschlagen: [{0}] [{1}] +endpoint.noSslHostName=Es wurde kein Hostname für die SSL Host Konfiguration angegeben +endpoint.poll.limitedpollsize=Konnte Poller mit der angegebenen Große von [{0}] nicht erzeugen +endpoint.removeDefaultSslHostConfig=Die Standard SSLHostConfig (namens [{0}]) darf nicht entfernt werden +endpoint.sendfile.addfail=Sendfile-Fehler: [{0}] [{1}] +endpoint.sendfile.closeError=Fehler beim Schließen der sendfile Ressourcen +endpoint.sendfile.error=Unerwarteter sendfile-Fehler +endpoint.sendfileThreadStop=Der Sendfile Thread hat sich nicht rechtzeitig beendet +endpoint.serverSocket.closeFailed=Konnte Server Socket für [{0}] nicht schliessen +endpoint.setAttribute=Setze [{0}] auf [{1}] +endpoint.warn.incorrectConnectionCount=Falsche Verbindungsanzahl, mehrere socket.close-Aufrufe auf dem gleichen Socket +endpoint.warn.noLocalName=Lokaler Hostname für Socket [{0}] konnte nicht ermittelt werden + +sslHostConfig.certificate.notype=Es wurden mehrere Zertifikate angegeben und mindestens einem fehlt ein erforderlicher Attributs Typ +sslHostConfig.fileNotFound=Die konfigurierte Datei [{0}] existiert nicht.\n +sslHostConfig.opensslconf.null=Versuch eine null OpenSSLConf zu setzen ignoriert + +sslUtilBase.noVerificationDepth=Der truststoreProvider [{0}] unterstützt nicht die Option certificateVerificationDepth +sslUtilBase.noneSupported=Keine der spezifizierten [{0}] wird von der SSL Engine unterstützt: [{1}] +sslUtilBase.ssl3=SSLv3 wurde explizit eingeschalten. Dieses Protokoll ist als unsicher bekannt. +sslUtilBase.trustedCertNotValid=Das vertrauenswürdige Zertifikat mit alias [{0}] und DN [{1}] ist auf Grund von [{2}] nicht gültig. Zertifikate die von diesem signiert worden sind WERDEN akzeptiert. diff --git a/java/org/apache/tomcat/util/net/LocalStrings_es.properties b/java/org/apache/tomcat/util/net/LocalStrings_es.properties new file mode 100644 index 0000000..be9eba2 --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_es.properties @@ -0,0 +1,68 @@ +# 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. + +channel.nio.ssl.notHandshaking=NOT_HANDSHAKING durante el handshake +channel.nio.ssl.sniDefault=Imposible almacenar los datos suficientes para determinar el SNI del host solicitado. Utilizando el default +channel.nio.ssl.sniHostName=El nombre del servidor (SNI) extraído de la conexión [{0}] es [{1}] +channel.nio.ssl.unexpectedStatusDuringWrap=Estado inesperado [{0}] durante el handshake WRAP.\n +channel.nio.ssl.unwrapFail=Incapáz de desenvolver los datos, estatus no válido [{0}]\n + +endpoint.accept.fail=Aceptación de conector falló +endpoint.apr.invalidSslProtocol=Se ha proporcionado un valor inválido [{0}] para el atributo SSLProtocol +endpoint.debug.channelCloseFail=No puede cerrar el canal +endpoint.debug.unlock.fail=Excepción cogida intentando desbloquear aceptación en puerto [{0}] +endpoint.err.close=Excepción cogida intentando cerrar conector +endpoint.err.handshake=Acuerdo fallido +endpoint.err.unexpected=Error inesperado al procesar conector +endpoint.init.bind=Ligado de conector falló: [{0}] [{1}] +endpoint.init.listen=Escucha de conector falló: [{0}] [{1}] +endpoint.invalidJmxNameSslHostCert=Imposible generar un nombre de objeto JMX válido para el SSLHostConfigCertificate asociado con el servidor [{0}] y el tipo de certificado [{1}]\n +endpoint.jsse.noSslContext=No se encontró ningún conexto SSLContext para el hostname [{0}]\n +endpoint.nio.stopLatchAwaitInterrupted=Este hilo fue interrumpido mientras esperaba porque los encuestadores se detuvieran +endpoint.noSslHostConfig=No se encontró elemento SSLHostConfig con nombre de máquina [{0}] para machear el defaultSSLHostConfigName para el conector [{1}]\n +endpoint.noSslHostName=No se proveió un nombre de host para la configuración SSL +endpoint.poll.error=Error inesperado de encuestador +endpoint.poll.fail=Fallo crítico de encuestador (reiniciando encuestador): [{0}] [{1}] +endpoint.poll.initfail=Falló la creación del encuestador +endpoint.poll.limitedpollsize=No pude crear encuestador de medida específica de [{0}] +endpoint.process.fail=Error reservando procesador de conector +endpoint.sendfile.addfail=Fallo en Sendfile: [{0}] [{1}] +endpoint.sendfile.error=Error inesperado de envío de fichero +endpoint.sendfileThreadStop=El hilo de envió de archivo no pudo ser detenido en el tiempo apropiado +endpoint.serverSocket.closeFailed=Fallo al cerrar el socket del servidor para [{0}] +endpoint.setAttribute=Fijando [{0}] a [{1}]\n +endpoint.warn.executorShutdown=El ejecutor asociado con el hilo del pool [{0}] no fue totalmente apagado. Algunos hilos de la aplicación pudieran estar corriendo aún. +endpoint.warn.incorrectConnectionCount=Conteo de conexión errónea, hay varias llamadas a socket.close en el mismo socket +endpoint.warn.noLocalName=Imposible determinar el nombre de la máquina local para el socket [{0}]\n +endpoint.warn.noLocalPort=Uncapaz de determinar el puerto local para el socket [{0}]\n +endpoint.warn.unlockAcceptorFailed=El hilo aceptador [{0}] falló al desbloquear. Forzando apagado de enchufe (socket). + +sniExtractor.clientHelloTooBig=El ClientHello no fue presentado en un sólo registro TLS por lo cual no se pudo extraer la información SNI + +socket.sslreneg=Excepción renegociando la conexión SSL + +sslHostConfig.certificate.notype=Se especificaron multiples certificados y al menos uno de ellos no tiene el tipo de atributo requerido +sslHostConfig.fileNotFound=No existe el archivo configurado [{0}] +sslHostConfig.invalid_truststore_password=La clave del almacén de confianza suministrada no se pudo usar para desbloquear y/o validar el almacén de confianza. Reintentando acceder el almacén de confianza con una clave nula que se saltará la validación. +sslHostConfig.opensslconf.null=El intento de fijar OpenSSLConf en nulo fue ignorado + +sslImplementation.cnfe=Incapaz de crear SSLImplementation para la clase [{0}] + +sslUtilBase.alias_no_key_entry=El nombre de Alias [{0}] no identifica una entrada de clave +sslUtilBase.invalidTrustManagerClassName=El trustManagerClassName suministrado [{0}] no implementa javax.net.ssl.TrustManager +sslUtilBase.keystore_load_failed=No pude cargar almacén de claves de tipo [{0}] con ruta [{1}] debido a [{2}] +sslUtilBase.noneSupported=Ninguno de los [{0}] especificados es soportado por el motor SSL : [{1}] +sslUtilBase.ssl3=SSLv3 ha sido explicitamente habilitado. Se conoce que este protocolo es inseguro +sslUtilBase.trustedCertNotValid=El certificado confiable con alias [{0}] y DN [{1}] no es válido debido a [{2}]. Los certificados firmados por este certificados confiable SERAN aceptados\n diff --git a/java/org/apache/tomcat/util/net/LocalStrings_fr.properties b/java/org/apache/tomcat/util/net/LocalStrings_fr.properties new file mode 100644 index 0000000..037cdb5 --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_fr.properties @@ -0,0 +1,174 @@ +# 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. + +acceptor.stop.fail=Le thread [{0}] de l''accepteur ne s''est pas arrêté proprement +acceptor.stop.interrupted=Une interruption s''est produite lors de l''attente de l''arrêt du thread [{0}] de l''accepteur + +channel.nio.interrupted=Le thread en cours a été interrompu +channel.nio.ssl.appInputNotEmpty=Le tampon d'entrée de l'application contient toujours des données, des données ont été perdues +channel.nio.ssl.appOutputNotEmpty=Le tampon de sortie de l'application contient toujours des données, des données ont été perdues +channel.nio.ssl.closeSilentError=Il y a eu une exception en essayant de fermer proprement la connection, comme prévu +channel.nio.ssl.closing=Le canal est en état de fermeture +channel.nio.ssl.eofDuringHandshake=EOF pendant la négociation +channel.nio.ssl.expandNetInBuffer=Augmentation de la taille du tampon d''entrée réseau à [{0}] octets +channel.nio.ssl.expandNetOutBuffer=Augmentation de la taille du tampon de sortie réseau à [{0}] octets +channel.nio.ssl.foundHttp=Une requête HTTP non cryptée a été trouvée sur la connection qui aurait dû être cryptée par TLS +channel.nio.ssl.handshakeError=Erreur lors de la négociation +channel.nio.ssl.incompleteHandshake=La négociation est incomplète, elle doit être terminée pour pouvoir lire des données +channel.nio.ssl.invalidCloseState=Etat de fermeture invalide, aucune donnée ne sera envoyée sur le réseau +channel.nio.ssl.invalidStatus=Etat inattendu [{0}] +channel.nio.ssl.netInputNotEmpty=Le tampon d'entrée du réseau contient toujours des données, la négociation va échouer +channel.nio.ssl.netOutputNotEmpty=Le tampon de sortie du réseau contient toujours des données, la négociation va échouer +channel.nio.ssl.notHandshaking=NOT_HANDSHAKING pendant la négociation SSL +channel.nio.ssl.pendingWriteDuringClose=Une écriture est en cours donc des données sont toujours présentes dans le tampon réseau, impossible d'envoyer le message de fermeture de SSL mais le socket sera fermé de toutes manières +channel.nio.ssl.remainingDataDuringClose=Des données sont toujours présentes dans le tampon réseau, impossible d'envoyer le message de fermeture de SSL mais le socket sera fermé de toutes manières +channel.nio.ssl.sniDefault=Incapacité d'accumuler assez d'information pour déterminer le nom du hôte SNI demandé. Valeur par défaut utilisée. +channel.nio.ssl.sniHostName=Le nom d''hôte SNI extrait pour la connexion [{0}] est [{1}] +channel.nio.ssl.timeoutDuringHandshake=Timeout pendant la négociation +channel.nio.ssl.unexpectedStatusDuringUnwrap=Statut inattendu [{0}] lors de l''UNWRAP de la négociation +channel.nio.ssl.unexpectedStatusDuringWrap=Statut inattendu [{0}] lors du WRAP de la négociation +channel.nio.ssl.unwrapFail=Incapable de désenrober les données ("unwrap data"), statut invalide [{0}] +channel.nio.ssl.unwrapFailResize=Impossible de faire l''unwrap des données parce que le tampon est trop petit, statut invalide [{0}] +channel.nio.ssl.wrapFail=Impossible d''enrober (wrap) les données, le status est invalide [{0}] + +endpoint.accept.fail=Aucun socket n'a pu être accepté +endpoint.alpn.fail=Erreur de configuration de la terminaison pour ALPN en utilisant [{0}] +endpoint.alpn.negotiated=Le protocole [{0}] a été négocié en utilisant ALPN +endpoint.apr.failSslContextMake=Incapable de créer un SSLContext. Vérifier que SSLEngine est activé dans l'AprLifecycleListener, que l'AprLifecycleListener a été correctement initialisé et qu'un protocole SSL valide a été spécifié. +endpoint.apr.invalidSslProtocol=La valeur invalide [{0}] a été fournie pour l''attribut SSLProtocol +endpoint.debug.channelCloseFail=Echec de la fermeture du canal (channel) +endpoint.debug.destroySocket=Destruction du socket [{0}] +endpoint.debug.handlerRelease=Le gestionnaire n'a pas pu retirer le wrapper du socket +endpoint.debug.pollerAdd=Ajout à la addList socket [{0}], inactivité maximale [{1}], drapeaux [{2}] +endpoint.debug.pollerAddDo=Ajout du socket [{0}] au poller +endpoint.debug.pollerProcess=Traitement de(s) évènement(s) [{1}] pour la socket [{0}] +endpoint.debug.pollerRemove=Essai d''enlever [{0}] du poller +endpoint.debug.pollerRemoved=Enlevé [{0}] du poller +endpoint.debug.registerRead=Enregistrement de l’intérêt en lecture pour [{0}] +endpoint.debug.registerWrite=Enregistrement de l’intérêt en écriture pour [{0}] +endpoint.debug.socket=socket [{0}] +endpoint.debug.socketTimeout=Expiration [{0}] +endpoint.debug.unlock.fail=Reçu une exception en essayant de déverrouiller l''accepteur sur le port [{0}] +endpoint.debug.unlock.localFail=Impossible de déterminer l''adresse locales pour [{0}] +endpoint.debug.unlock.localNone=Impossible de débloquer l''accepteur pour [{0}] car l''adresse locale n''était pas disponible +endpoint.duplicateSslHostName=Plusieurs éléments SSLHostConfig ont été fournis pour le nom d''hôte [{0}], les noms d''hôte doivent être uniques +endpoint.err.close=Une exception s'est produite en essayant de fermer le socket +endpoint.err.duplicateAccept=Le socket a été accpeté deux fois. Ceci est un bug connu du kernel Linux. La connection originelle a été traitée normalement et le doublon a été ignoré. Le client ne devrait pas être affecté. Mettre à jour l'OS vers une version qui utilise un kernel 5.10 ou plus récent devrait corriger le problème. +endpoint.err.handshake=Echec de négociation +endpoint.err.unexpected=Erreur inattendue lors du traitement du socket +endpoint.errorCreatingSSLContext=Erreur lors de la création du SSLContext +endpoint.executor.fail=L''exécuteur a rejeté le traitement du socket [{0}] +endpoint.getAttribute=[{0}] est [{1}] +endpoint.init.bind=L''association du socket a échoué : [{0}] [{1}] +endpoint.init.bind.inherited=Pas de canal hérité alors que le connecteur était configuré pour en utiliser un +endpoint.init.listen=L''écoute sur le socket a échoué : [{0}] [{1}] +endpoint.init.unixnotavail=Le support des sockets de domaine Unix n'est pas disponible +endpoint.invalidJmxNameSslHost=Impossible de générer un nom d''objet JMX valide pour le SSLHostConfig associé à l''hôte [{0}] +endpoint.invalidJmxNameSslHostCert=Impossible de générer un nom d''objet JMX valide pour le SSLHostConfigCertificate associé à l''hôte [{0}] et au type de certificat [{1}] +endpoint.jmxRegistrationFailed=Echec de l''enregistrement JMX de l''objet avec le nom [{0}] +endpoint.jsse.noSslContext=Aucun SSLContext n''a été trouvé pour le nom d''hôte [{0}] +endpoint.launch.fail=Impossible de démarrer le nouvel exécutable +endpoint.nio.keyProcessingError=Erreur lors du traitement de la clé de sélection +endpoint.nio.latchMustBeZero=Le compte du latch doit être à zéro ou null +endpoint.nio.nullLatch=Le latch ne peut être null +endpoint.nio.nullSocketChannel=Le canal du socket est invalide car null lors du traitement de l'évênement du poller +endpoint.nio.perms.readFail=Echec d''ajout des permissions en lecture pour le socket de domaine Unix [{0}] +endpoint.nio.perms.writeFail=Echec d''ajout des permissions en écriture pour le socket de domaine Unix [{0}] +endpoint.nio.registerFail=Echec d'enregistrement du socket avec le sélecteur du poller +endpoint.nio.selectorCloseFail=Impossible de fermer le sélecteur lors de la fermeture du poller +endpoint.nio.selectorLoopError=Erreur dans la boucle du sélecteur +endpoint.nio.stopLatchAwaitFail=Les pollers ne se sont pas arrêtés dans le temps imparti +endpoint.nio.stopLatchAwaitInterrupted=Ce thread a été interrompu pendant qu'il attendait l'arrêt des scrutateurs ("pollers") +endpoint.nio.timeoutCme=Exception pendant le traitement du délai d'attente maximum ; le code a été vérifié de manière répétée et aucune modification concurrence n'a pu être trouvée, si vous obtenez cette erreur de manière reproductible merci d'ouvrir un rapport d'erreur sur Tomcat en fournissant les informations pour la reproduire +endpoint.nio2.exclusiveExecutor=Le connecteur NIO2 a besoin d'un accès exclusif à un exécuteur pour pouvoir avoir un comportement prévisible lors de son arrêt +endpoint.nio2.executorService=Le connecteur NIO2 nécessite un service d'exécution, les fils d'exécutions internes de la JVM seront utilisés +endpoint.noSslHostConfig=Pas d''élément SSLHostConfig trouvé avec hostName [{0}] correspondant au defaultSSLHostConfigName du connecteur [{1}] +endpoint.noSslHostName=Aucun nom d'hôte n'a été fourni pour la configuration de l'hôte SSL +endpoint.poll.error=Erreur inattendue du poller +endpoint.poll.fail=Echec critique du poller, redémarrage : [{0}] [{1}] +endpoint.poll.initfail=Echec de création du poller +endpoint.poll.limitedpollsize=Echec de création d''un poller avec la taille spécifiée [{0}] +endpoint.pollerThreadStop=Le thread du poller ne s'est pas arrêté dans le temps imparti +endpoint.portOffset.invalid=La valeur [{0}] pour portOffset est invalide car elle ne peut être négative +endpoint.process.fail=Erreur lors de l'allocation d'un processeur de socket +endpoint.processing.fail=Erreur lors de l’exécution du processeur du socket +endpoint.rejectedExecution=Le traitement du socket a été rejeté pour [{0}] +endpoint.removeDefaultSslHostConfig=Le SSLHostConfig par défaut (de nom [{0}]) ne peut pas être retiré +endpoint.sendfile.addfail=Echec de Sendfile : [{0}] [{1}] +endpoint.sendfile.closeError=Erreur lors de la fermeture des ressources de sendfile +endpoint.sendfile.error=Erreur lors de sendfile +endpoint.sendfile.tooMuchData=Sendfile a été configuré pour envoyer plus de données que ce qui était disponible +endpoint.sendfileThreadStop=Le thread "sendfile" ne s'est pas arrêté endéans le temps imparti +endpoint.serverSocket.closeFailed=Le socket serveur [{0}] n''a pas pu être fermé +endpoint.setAttribute=Met [{0}] à [{1}] +endpoint.setAttributeError=Impossible de définir l''attribut [{0}] en [{1}] +endpoint.socketOptionsError=Erreur en définissant les options du socket +endpoint.timeout.err=Erreur en traitant le dépassement de temps d'attente du socket +endpoint.tls.cert.encodingError=Les empreintes du certificat ne sont pas disponibles +endpoint.tls.cert.noCerts=Les détails du certificat ne sont pas disponibles car la chaîne de certificats retournée par le SSLContext est vide +endpoint.tls.info=Connecteur [{0}], hôte virtuel TLS [{1}], type de certificat [{2}] configuré depuis {3} et la trust store [{4}] +endpoint.tls.info.cert.keystore=[{0}] avec l''alias [{1}] +endpoint.tls.info.cert.pem=clé [{0}], certificat [{1}] et chaîne de certificats [{2}] +endpoint.unknownSslHostName=Le nom d''hôte SSL [{0}] n''est pas reconnu pour cette terminaison +endpoint.warn.executorShutdown=L''exécuteur associé au pool de threads [{0}] n''est pas complètement arrêté, certains threads d''application peuvent toujours être en cours d''exécution +endpoint.warn.incorrectConnectionCount=Le décompte du nombre de connections est incorrect, la méthode de fermeture d'un même socket a été appelée plusieurs fois +endpoint.warn.noLocalAddr=Impossible de déterminer l''addresse locale pour le socket [{0}] +endpoint.warn.noLocalName=Incapable de déterminer l''hôte local ("local host") pour la socket [{0}] +endpoint.warn.noLocalPort=Impossible de déterminer le port local pour le socket [{0}] +endpoint.warn.noRemoteAddr=Impossible de déterminer l''adresse distante pour le socket [{0}] +endpoint.warn.noRemoteHost=Impossible de déterminer le nom d''hôte distant pour le socket [{0}] +endpoint.warn.noRemotePort=Impossible de déterminer le port distant pour le socket [{0}] +endpoint.warn.noUtilityExecutor=Aucun exécuteur utilitaire configuré, un nouveau sera crée +endpoint.warn.unlockAcceptorFailed=Le thread qui accepte les sockets [{0}] n''a pu être débloqué, arrêt forcé su socket serveur + +sniExtractor.clientHelloInvalid=Le message ClientHello n'était pas formaté correctement +sniExtractor.clientHelloTooBig=Le ClientHello n'a pas été présenté dans un seul enregistrement TLS donc l'information SNI n'a pu être extraite +sniExtractor.tooEarly=Il est illégal d'appeler cette méthode avant que le hello du client ait été traité + +socket.closed=Le socket associé à cette connection a été fermé +socket.sslreneg=Exception lors de la renégociation de la connection SSL + +socketWrapper.readTimeout=Timeout en lecture +socketWrapper.writeTimeout=Timeout en écriture + +sslHostConfig.certificate.notype=Plusieurs certificats ont été spécifiés et au moins un n'a pas d'attribut type +sslHostConfig.certificateVerificationInvalid=La valeur de vérification de certificat [{0}] n''est pas reconnue +sslHostConfig.certificateVerificationWithHttp2=L''hôte virtuel TLS [{0}] est configuré avec la validation optionelle du certificat, et le connecteur qui l''utilise est configuré pour supporter l''upgrade vers h2. HTTP/2 sur TLS ne permet pas la validation optionelle du certificat. +sslHostConfig.fileNotFound=Le fichier [{0}] configuré n''existe pas. +sslHostConfig.invalid_truststore_password=Le mot de passe de la base de confiance n'a pas pu être utilisé pour déverrouiller et ou valider celle ci, nouvel essai en utilisant un mot de passe null pour passer la validation +sslHostConfig.mismatch=La propriété [{0}] a été fixée sur le SSLHostConfig nommé [{1}] et est pour la syntaxe de configuration [{2}] mais le SSLHostConfig est utilisé avec la syntaxe de configuration [{3}] +sslHostConfig.opensslconf.alreadyset=La tentative de définition d'un autre OpenSSLConf est ignorée +sslHostConfig.opensslconf.null=L'OpenSSLConf nul a été ignoré +sslHostConfig.prefix_missing=Le protocole [{0}] a été ajouté à la liste des protocoles du SSLHostConfig nommé [{1}], vérifier qu''un préfixe +/- ne manque pas + +sslHostConfigCertificate.mismatch=La propriété [{0}] a été définie sur le SSLHostConfigCertificate nommé [{1}] et est pour un certificat de stockage de type [{2}] mais le certificat est utilisé avec un stockage de type [{3}] + +sslImplementation.cnfe=Impossible de créer une SSLImplementation avec la class [{0}] + +sslUtilBase.active=Les [{0}] qui sont actifs sont : [{1}] +sslUtilBase.aliasIgnored=Le mode FIPS est activé donc le nom d''alias [{0}] sera ignoré, s''il il y a plus d''une clé dans la keystore, la clé utilisée dépendra de son implémentation +sslUtilBase.alias_no_key_entry=Le nom alias [{0}] n''identifie pas une entrée de clé +sslUtilBase.invalidTrustManagerClassName=Le trustManagerClassName fourni [{0}] n''implémente pas javax.net.ssl.TrustManager +sslUtilBase.keystore_load_failed=Impossible de changer la base de clés de type [{0}] avec le chemin [{1}] à cause de [{2}] +sslUtilBase.noCertFile=L'attribut certificateFile de SSLHostConfig doit être défini lorsque un connecteur SSL est utilisé +sslUtilBase.noCrlSupport=Le truststoreProvider [{0}] ne supporte pas d''option de configuration certificateRevocationFile +sslUtilBase.noKeys=Aucun alias pour les clés privées n'a été trouvé dans la base de clés +sslUtilBase.noVerificationDepth=Le truststoreProvider [{0}] ne supporte pas l''option de configuration certificateVerificationDepth +sslUtilBase.noneSupported=Aucun des [{0}] spécifiés n''est supporté par le moteur SSL : [{1}] +sslUtilBase.skipped=Tomcat interprête l''attribut [{0}] de manière à être cohérent ave la dernière branche de développement d''OpenSSL. Certains de ceux qui ont été spéifiés [{0}] ne sont pas suportés par le moteur SSL configré pour ce connecteur (qui pourrait utiliser JSSE ou une version antérieure d''OpenSSL) et ont été ignorés: [{1}] +sslUtilBase.ssl3=SSLv3 a été explicitement activé. Ce protocole est connu comme non-sécurisé. +sslUtilBase.tls13.auth=L’implémentation JSSE de TLS 1.3 ne supporte pas l'authentification après la négociation initiale, elle est donc incompatible avec l’authentification optionnelle du client +sslUtilBase.trustedCertNotChecked=Les dates de validité du certificat de confiance dont l''alias est [{0}] n''ont pas été vérifiées car sont type est inconnu +sslUtilBase.trustedCertNotValid=Le certificat de confiance avec l''alias [{0}] et le DN [{1}] n''est pas valide à cause de [{2}], les certificats signés par ce certificat de confiance SERONT acceptés diff --git a/java/org/apache/tomcat/util/net/LocalStrings_ja.properties b/java/org/apache/tomcat/util/net/LocalStrings_ja.properties new file mode 100644 index 0000000..5d6c959 --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_ja.properties @@ -0,0 +1,174 @@ +# 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. + +acceptor.stop.fail=アクセプタースレッド [{0}] ãŒæ­£å¸¸ã«åœæ­¢ã—ã¾ã›ã‚“ã§ã—㟠+acceptor.stop.interrupted=アクセプタースレッド [{0}] ãŒåœæ­¢ã™ã‚‹ã®ã‚’å¾…ã£ã¦ã„ã‚‹é–“ã«å‰²ã‚Šè¾¼ã¿ã‚’å—ä¿¡ã—ã¾ã—㟠+ +channel.nio.interrupted=ç¾åœ¨ã®ã‚¹ãƒ¬ãƒƒãƒ‰ãŒä¸­æ–­ã•ã‚Œã¾ã—㟠+channel.nio.ssl.appInputNotEmpty=アプリケーションã®å…¥åŠ›ãƒãƒƒãƒ•ã‚¡ã«ã¯ãƒ‡ãƒ¼ã‚¿ãŒæ®‹ã£ã¦ã„ã¾ã™ã€‚残ã£ãŸãƒ‡ãƒ¼ã‚¿ã¯å¤±ã‚ã‚Œã¾ã™ã€‚ +channel.nio.ssl.appOutputNotEmpty=アプリケーション出力ãƒãƒƒãƒ•ã‚¡ã«ã¯ã¾ã ãƒ‡ãƒ¼ã‚¿ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚データã¯å¤±ã‚ã‚Œã¾ã—ãŸã€‚ +channel.nio.ssl.closeSilentError=コãƒã‚¯ã‚·ãƒ§ãƒ³ã‚’完全ã«åˆ‡æ–­ã—よã†ã¨ã—ã¾ã—ãŸãŒä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +channel.nio.ssl.closing=ãƒãƒ£ãƒãƒ«ã¯ã‚¯ãƒ­ãƒ¼ã‚¸ãƒ³ã‚°çŠ¶æ…‹ã§ã™ +channel.nio.ssl.eofDuringHandshake=ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ä¸­ã®EOF。 +channel.nio.ssl.expandNetInBuffer=ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯å…¥åŠ›ãƒãƒƒãƒ•ã‚¡ã‚’[{0}]ãƒã‚¤ãƒˆã«æ‹¡å¼µã—ã¦ã„ã¾ã™ +channel.nio.ssl.expandNetOutBuffer=ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯å‡ºåŠ›ãƒãƒƒãƒ•ã‚¡ã‚’[{0}]ãƒã‚¤ãƒˆã«æ‹¡å¼µã—ã¾ã™ +channel.nio.ssl.foundHttp=æš—å·åŒ–ã•ã‚ŒãŸ TLS 接続ã‹ã‚‰å¹³æ–‡ã® HTTP リクエストをå—ä¿¡ã—ã¾ã—ãŸã€‚ +channel.nio.ssl.handshakeError=ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ã‚¨ãƒ©ãƒ¼ +channel.nio.ssl.incompleteHandshake=ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ãŒä¸å®Œå…¨ã§ã™ã€‚データを読ã¿å–ã‚‹å‰ã«ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ã‚’完了ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +channel.nio.ssl.invalidCloseState=無効ãªã‚¯ãƒ­ãƒ¼ã‚ºçŠ¶æ…‹ã§ã€ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ãƒ‡ãƒ¼ã‚¿ã‚’é€ä¿¡ã—ã¾ã›ã‚“。 +channel.nio.ssl.invalidStatus=予期ã—ãªã„ステータス [{0}]。 +channel.nio.ssl.netInputNotEmpty=ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯å…¥åŠ›ãƒãƒƒãƒ•ã‚¡ã«ã¯ã¾ã ãƒ‡ãƒ¼ã‚¿ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ã¯å¤±æ•—ã—ã¾ã™ã€‚ +channel.nio.ssl.netOutputNotEmpty=ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯å‡ºåŠ›ãƒãƒƒãƒ•ã‚¡ã«ã¯ã¾ã ãƒ‡ãƒ¼ã‚¿ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ã¯å¤±æ•—ã—ã¾ã™ã€‚ +channel.nio.ssl.notHandshaking=ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ä¸­ã«NOT_HANDSHAKING +channel.nio.ssl.pendingWriteDuringClose=書ãè¾¼ã¿ã‚’ä¿ç•™ã—ã¦ã„ã‚‹ãŸã‚ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ãƒãƒƒãƒ•ã‚¡ãƒ¼ã«ãƒ‡ãƒ¼ã‚¿ãŒæ®‹ã£ã¦ã„ã¾ã™ã€‚SSL 切断メッセージをé€ä¿¡ã§ãã¾ã›ã‚“。代ã‚ã‚Šã« close(true) ã§å¼·åˆ¶çš„ã«åˆ‡æ–­ã—ã¦ãã ã•ã„。 +channel.nio.ssl.remainingDataDuringClose=ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ãƒãƒƒãƒ•ã‚¡å†…ã«ãƒ‡ãƒ¼ã‚¿ãŒæ®‹ã£ã¦ã„ã¦ã€SSLクローズメッセージをé€ä¿¡ã§ãã¾ã›ã‚“。代ã‚ã‚Šã«close(true)ã§ã‚¯ãƒ­ãƒ¼ã‚ºã—ã¾ã™ã€‚ +channel.nio.ssl.sniDefault=è¦æ±‚ã•ã‚ŒãŸ SNI ホストåã‚’å–å¾—ã™ã‚‹ãŸã‚ã«å分ãªãƒ‡ãƒ¼ã‚¿ã‚’è“„ç©ã§ããªã„ãŸã‚既定値を使用ã—ã¾ã™ +channel.nio.ssl.sniHostName=コãƒã‚¯ã‚·ãƒ§ãƒ³ [{0}] ã‹ã‚‰å–å¾—ã—㟠SNI ホストå㯠[{1}] ã§ã™ +channel.nio.ssl.timeoutDuringHandshake=ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ãŒã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã—ãŸã€‚ +channel.nio.ssl.unexpectedStatusDuringUnwrap=UNWRAPãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ä¸­ã«äºˆæœŸã—ãªã„ステータス [{0}] ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +channel.nio.ssl.unexpectedStatusDuringWrap=ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯WRAP中ã«äºˆæœŸã—ãªã„ステータス [{0}] ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +channel.nio.ssl.unwrapFail=データをアンラップã§ãã¾ã›ã‚“ã€ç„¡åŠ¹ãªã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ [{0}] +channel.nio.ssl.unwrapFailResize=ãƒãƒƒãƒ•ã‚¡ãŒå°ã•ã™ãŽã‚‹ãŸã‚データをアンラップã§ãã¾ã›ã‚“。無効ãªã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ [{0}] +channel.nio.ssl.wrapFail=データをラップã§ãã¾ã›ã‚“。無効ãªã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ [{0}] + +endpoint.accept.fail=ソケットå—ã‘付ã‘失敗 +endpoint.alpn.fail=[{0}]を使用ã—ã¦ALPNã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã‚’設定ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +endpoint.alpn.negotiated=ALPNを使用ã—ã¦äº¤æ¸‰ã•ã‚ŒãŸ [{0}] プロトコル +endpoint.apr.failSslContextMake=SSLContextを作æˆã§ãã¾ã›ã‚“。SSLEngineãŒAprLifecycleListenerã§æœ‰åŠ¹ã«ãªã£ã¦ã„ã‚‹ã“ã¨ã€AprLifecycleListenerãŒæ­£ã—ãåˆæœŸåŒ–ã•ã‚Œã¦ã„ã‚‹ã“ã¨ã€æœ‰åŠ¹ãªSSLProtocolãŒæŒ‡å®šã•ã‚Œã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ä¸‹ã•ã„ +endpoint.apr.invalidSslProtocol=SSLProtocol属性ã«ç„¡åŠ¹ãªå€¤[{0}]ãŒæŒ‡å®šã•ã‚Œã¾ã—㟠+endpoint.debug.channelCloseFail=ãƒãƒ£ãƒ³ãƒãƒ«ã‚’切断ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +endpoint.debug.destroySocket=ソケット [{0}] を破棄ã—ã¾ã™ã€‚ +endpoint.debug.handlerRelease=ãƒãƒ³ãƒ‰ãƒ©ã¯ã‚½ã‚±ãƒƒãƒˆãƒ©ãƒƒãƒ‘ーã®è§£æ”¾ã«å¤±æ•—ã—ã¾ã—㟠+endpoint.debug.pollerAdd=addListã«è¿½åŠ ã€ã‚½ã‚±ãƒƒãƒˆ [{0}]ã€ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆ [{1}]ã€ãƒ•ãƒ©ã‚° [{2}] +endpoint.debug.pollerAddDo=Pollerソケット [{0}] ã«è¿½åŠ  +endpoint.debug.pollerProcess=イベント [{1}] ã®ã‚½ã‚±ãƒƒãƒˆ [{0}] ã®å‡¦ç†ä¸­ +endpoint.debug.pollerRemove=Pollerã‹ã‚‰[{0}]ã‚’å–り除ã“ã†ã¨ã—ã¦ã„ã¾ã™ã€‚ +endpoint.debug.pollerRemoved=poller ã‹ã‚‰ [{0}] を削除ã—ã¾ã—ãŸã€‚ +endpoint.debug.registerRead=[{0}] ã«å¯¾ã™ã‚‹èª­ã¿è¾¼ã¿ã®ç›£è¦–を登録ã—ã¾ã—㟠+endpoint.debug.registerWrite=[{0}] ã«å¯¾ã™ã‚‹æ›¸ãè¾¼ã¿ã®ç›£è¦–を登録ã—ã¾ã—㟠+endpoint.debug.socket=ソケット [{0}] +endpoint.debug.socketTimeout=タイムアウト [{0}] +endpoint.debug.unlock.fail=port [{0}]ã®accept をロック解除ã—よã†ã—ãŸéš›ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +endpoint.debug.unlock.localFail=[{0}]ã®ãƒ­ãƒ¼ã‚«ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’特定ã§ãã¾ã›ã‚“ +endpoint.debug.unlock.localNone=ローカルアドレスãŒåˆ©ç”¨ã§ããªã‹ã£ãŸãŸã‚ã€[{0}]ã®ã‚¢ã‚¯ã‚»ãƒ—ã‚¿ã®ãƒ­ãƒƒã‚¯ã‚’解除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +endpoint.duplicateSslHostName=ホストå[{0}]ã«è¤‡æ•°ã®SSLHostConfigè¦ç´ ãŒæä¾›ã•ã‚Œã¾ã—ãŸã€‚ ホストåã¯ä¸€æ„ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +endpoint.err.close=ソケットをクローズã—よã†ã¨ã—ãŸéš›ã«ä¾‹å¤–ãŒç™ºç”Ÿã—ã¾ã—㟠+endpoint.err.duplicateAccept=é‡è¤‡ã—ãŸã‚½ã‚±ãƒƒãƒˆå—ã‘付ã‘ãŒæ¤œå‡ºã•ã‚Œã¾ã—ãŸã€‚ ã“ã‚Œã¯Linuxカーãƒãƒ«ã®æ—¢çŸ¥ã®ãƒã‚°ã§ã™ã€‚ 最åˆã®ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã¯æ­£å¸¸ã«å‡¦ç†ã•ã‚Œã€é‡è¤‡å—ã‘付ã‘ã¯ç„¡è¦–ã•ã‚Œã¾ã—ãŸã€‚ クライアントã¯å½±éŸ¿ã‚’å—ã‘ãªã„ã¯ãšã§ã™ã€‚Linuxカーãƒãƒ«ã‚’ãƒãƒ¼ã‚¸ãƒ§ãƒ³5.10以é™ã«æ›´æ–°ã™ã‚‹ã¨ã€é‡è¤‡å—ã‘付ã‘ã®ãƒã‚°ãŒä¿®æ­£ã•ã‚Œã¾ã™ã€‚ +endpoint.err.handshake=ãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯å¤±æ•— +endpoint.err.unexpected=ソケット処ç†ä¸­ã®äºˆæœŸã›ã¬ã‚¨ãƒ©ãƒ¼ +endpoint.errorCreatingSSLContext=SSLContextã®ä½œæˆä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+endpoint.executor.fail=エグゼキュータã¯å‡¦ç†ã™ã‚‹ã‚½ã‚±ãƒƒãƒˆ [{0}] ã‚’æ‹’å¦ã—ã¾ã—㟠+endpoint.getAttribute=[{0}] 㯠[{1}] ã§ã™ +endpoint.init.bind=ソケットãƒã‚¤ãƒ³ãƒ‰ã«å¤±æ•—ã—ã¾ã—ãŸï¼š[{0}] [{1}] +endpoint.init.bind.inherited=コãƒã‚¯ã‚¿ãŒ1ã¤ã‚’使用ã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã‚‹é–“ã€ç¶™æ‰¿ã•ã‚ŒãŸãƒãƒ£ãƒãƒ«ã¯ã‚ã‚Šã¾ã›ã‚“。 +endpoint.init.listen=ソケットã®å¾…ã¡å—ã‘を開始ã§ãã¾ã›ã‚“: [{0}] [{1}] +endpoint.init.unixnotavail=Unixドメインソケットã®ã‚µãƒãƒ¼ãƒˆã¯åˆ©ç”¨ã§ãã¾ã›ã‚“ +endpoint.invalidJmxNameSslHost=ホスト [{0}] ã«é–¢é€£ä»˜ã‘られ㟠SSLHostConfig ã«æœ‰åŠ¹ãª JMX オブジェクトåを生æˆã§ãã¾ã›ã‚“。 +endpoint.invalidJmxNameSslHostCert=ホストå [{0}]ã€è¨¼æ˜Žæ›¸ã‚¿ã‚¤ãƒ— [{1}] ã® SSLHostConfigCertificate ã®ãŸã‚ã®æ­£å¸¸ãª JMX オブジェクトåを生æˆã§ãã¾ã›ã‚“ +endpoint.jmxRegistrationFailed=åå‰ [{0}] ã® JMX オブジェクトを登録ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +endpoint.jsse.noSslContext=ホストå[{0}]ã®SSLContextãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+endpoint.launch.fail=new Runnableã®èµ·å‹•ã«å¤±æ•—ã—ã¾ã—㟠+endpoint.nio.keyProcessingError=é¸æŠžã‚­ãƒ¼å‡¦ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +endpoint.nio.latchMustBeZero=Latchã®ã‚«ã‚¦ãƒ³ãƒˆã¯ 0 ã‹ null ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +endpoint.nio.nullLatch=Latch ã« null ã¯æŒ‡å®šã§ãã¾ã›ã‚“。 +endpoint.nio.nullSocketChannel=pollerイベントã®å‡¦ç†ä¸­ã®ç„¡åŠ¹ãªnullソケットãƒãƒ£ãƒãƒ« +endpoint.nio.perms.readFail=Unixドメインソケット [{0}] ã®èª­ã¿å–り権é™ã®è¨­å®šã«å¤±æ•—ã—ã¾ã—㟠+endpoint.nio.perms.writeFail=Unixドメインソケット [{0}] ã®æ›¸ãè¾¼ã¿æ¨©é™ã®è¨­å®šã«å¤±æ•—ã—ã¾ã—㟠+endpoint.nio.registerFail=Pollerã‹ã‚‰ã‚½ã‚±ãƒƒãƒˆã®ã‚»ãƒ¬ã‚¯ã‚¿ã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +endpoint.nio.selectorCloseFail=Pollerã‚’é–‰ã˜ã‚‹ã¨ãã«ã‚»ãƒ¬ã‚¯ã‚¿ãƒ¼ã‚’é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +endpoint.nio.selectorLoopError=セレクタã®å‡¦ç†ãƒ«ãƒ¼ãƒ—中ã®ã‚¨ãƒ©ãƒ¼ +endpoint.nio.stopLatchAwaitFail=Pollerã¯äºˆæƒ³ã•ã‚ŒãŸæ™‚間内ã«æ­¢ã¾ã‚Šã¾ã›ã‚“ã§ã—㟠+endpoint.nio.stopLatchAwaitInterrupted=ã“ã®ã‚¹ãƒ¬ãƒƒãƒ‰ã¯PollerãŒåœæ­¢ã™ã‚‹ã®ã‚’å¾…ã¤é–“ã«ä¸­æ–­ã•ã‚Œã¾ã—㟠+endpoint.nio.timeoutCme=タイムアウトã®å‡¦ç†ä¸­ã®ä¾‹å¤–。 コードã¯ç¹°ã‚Šè¿”ã—ãƒã‚§ãƒƒã‚¯ã•ã‚Œã¦ãŠã‚Šã€åŒæ™‚ã«å¤‰æ›´ã•ã‚Œã¦ã„ã¾ã›ã‚“。 ã“ã®ã‚¨ãƒ©ãƒ¼ã‚’ç¹°ã‚Šè¿”ã™ã“ã¨ãŒã§ãã‚‹å ´åˆã¯ã€Tomcatã®ãƒã‚°ã‚’é–‹ã„ã¦ã€å†ç¾æ‰‹é †ã‚’æ示ã—ã¦ãã ã•ã„。 +endpoint.nio2.exclusiveExecutor=NIO2コãƒã‚¯ã‚¿ã¯ã‚·ãƒ£ãƒƒãƒˆãƒ€ã‚¦ãƒ³æ™‚ã«æŽ’他的エグゼキュータを正ã—ã動作ã•ã›ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ +endpoint.nio2.executorService=NIO2 Connector ã«ã¯ Executor サービスãŒå¿…è¦ã§ã™ã€‚内部 JVM スレッドãŒä½¿ç”¨ã•ã‚Œã¾ã™ã€‚ +endpoint.noSslHostConfig=コãƒã‚¯ã‚¿ [{1}] ã®defaultSSLHostConfigName ã«ä¸€è‡´ã™ã‚‹SSLHostConfigè¦ç´ ãŒhostName [{0}] ã§è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+endpoint.noSslHostName=SSL ã®ãƒ›ã‚¹ãƒˆè¨­å®šã«ãƒ›ã‚¹ãƒˆåãŒã‚ã‚Šã¾ã›ã‚“。 +endpoint.poll.error=予期ã›ã¬ poller エラー +endpoint.poll.fail=é‡å¤§ãªPoller障害(Pollerã®å†å§‹å‹•ï¼‰ï¼š[{0}] [{1}] +endpoint.poll.initfail=Pollerã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +endpoint.poll.limitedpollsize=サイズ [{0}] ã® Poller インスタンスを作æˆã§ãã¾ã›ã‚“。 +endpoint.pollerThreadStop=時間内ã«Poller スレッドをåœæ­¢ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +endpoint.portOffset.invalid=portOffset ã«ä¸æ­£ãªå€¤ [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚è² ã®å€¤ã¯æŒ‡å®šã§ãã¾ã›ã‚“。 +endpoint.process.fail=ソケットプロセッサーã®å‰²ã‚Šå½“ã¦ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +endpoint.processing.fail=ソケットプロセッサã®å®Ÿè¡Œä¸­ã‚¨ãƒ©ãƒ¼ +endpoint.rejectedExecution=[{0}]ã®ãŸã‚ã€ã‚½ã‚±ãƒƒãƒˆå‡¦ç†è¦æ±‚ãŒæ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ +endpoint.removeDefaultSslHostConfig=既定ã®SSLHostConfig([{0}])ã¯å‰Šé™¤ã§ãã¾ã›ã‚“ +endpoint.sendfile.addfail=Sendfile 失敗: [{0}] [{1}] +endpoint.sendfile.closeError=sendfileリソースã®ã‚¯ãƒ­ãƒ¼ã‚ºä¸­ã®ã‚¨ãƒ©ãƒ¼ +endpoint.sendfile.error=予期ã›ã¬ sendfile エラー +endpoint.sendfile.tooMuchData=利用å¯èƒ½ä»¥ä¸Šã®ãƒ‡ãƒ¼ã‚¿ã‚’é€ä¿¡ã™ã‚‹ã‚ˆã†ã«SendfileãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã™ã€‚ +endpoint.sendfileThreadStop=ファイルé€ä¿¡ã‚¹ãƒ¬ãƒƒãƒ‰ã¯æ™‚間内ã«åœæ­¢ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +endpoint.serverSocket.closeFailed=[{0}] ã«ã‚ˆã‚Šã‚µãƒ¼ãƒãƒ¼ã‚½ã‚±ãƒƒãƒˆã®åˆ‡æ–­ã«å¤±æ•—ã—ã¾ã—㟠+endpoint.setAttribute=[{0}]ã‚’[{1}]ã«è¨­å®š +endpoint.setAttributeError=属性 [{0}] ã« [{1}] を設定ã§ãã¾ã›ã‚“。 +endpoint.socketOptionsError=ソケットオプション設定中ã®ã‚¨ãƒ©ãƒ¼ +endpoint.timeout.err=ソケットタイムアウト処ç†ä¸­ã®ã‚¨ãƒ©ãƒ¼ +endpoint.tls.cert.encodingError=証明書ã®ãƒ•ã‚£ãƒ³ã‚¬ãƒ¼ãƒ—リントãŒåˆ©ç”¨ã§ãã¾ã›ã‚“ +endpoint.tls.cert.noCerts=SSLContext ã‹ã‚‰è¿”ã•ã‚ŒãŸè¨¼æ˜Žæ›¸ãƒã‚§ãƒ¼ãƒ³ãŒç©ºã ã£ãŸãŸã‚ã€è¨¼æ˜Žæ›¸ã®è©³ç´°ã‚’利用ã§ãã¾ã›ã‚“ +endpoint.tls.info=トラストストア [{4}] を使用ã—㦠{3} ã‹ã‚‰æ§‹æˆã•ã‚ŒãŸã‚³ãƒã‚¯ã‚¿ [{0}]ã€TLS 仮想ホスト [{1}]ã€ãŠã‚ˆã³è¨¼æ˜Žæ›¸ã‚¿ã‚¤ãƒ— [{2}] +endpoint.tls.info.cert.keystore=エイリアス [{1}] を使用ã—ãŸã‚­ãƒ¼ã‚¹ãƒˆã‚¢ [{0}] +endpoint.tls.info.cert.pem=キー [{0}]ã€è¨¼æ˜Žæ›¸ [{1}]ã€ãŠã‚ˆã³è¨¼æ˜Žæ›¸ãƒã‚§ãƒ¼ãƒ³ [{2}] +endpoint.unknownSslHostName=SSL ホストå [{0}] ã¯ã“ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã‹ã‚‰èªè­˜ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +endpoint.warn.executorShutdown=スレッドプール [{0}] ã¨é–¢é€£ä»˜ã‘られãŸã‚¨ã‚°ã‚¼ã‚­ãƒ¥ãƒ¼ã‚¿ã¯å®Œå…¨ã«åœæ­¢ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ã„ãã¤ã‹ã®ã‚¢ãƒ—リケーションスレッドã¯ã¾ã å‹•ä½œã—続ã‘ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ +endpoint.warn.incorrectConnectionCount=ä¸æ­£ãªã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³æ•°ã€‚複数ã®socket.closeãŒåŒã˜ã‚½ã‚±ãƒƒãƒˆã§å‘¼ã³å‡ºã•ã‚Œã¾ã—ãŸã€‚ +endpoint.warn.noLocalAddr=ソケット [{0}] ã®ãƒ­ãƒ¼ã‚«ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’å–å¾—ã§ãã¾ã›ã‚“。 +endpoint.warn.noLocalName=ソケット [{0}] ã®ãƒ­ãƒ¼ã‚«ãƒ«ãƒ›ã‚¹ãƒˆåã‚’å–å¾—ã§ãã¾ã›ã‚“ +endpoint.warn.noLocalPort=ソケット [{0}] ã®ãƒ­ãƒ¼ã‚«ãƒ«ãƒãƒ¼ãƒˆãŒå–å¾—ã§ãã¾ã›ã‚“ +endpoint.warn.noRemoteAddr=ソケット [{0}] ã®ãƒªãƒ¢ãƒ¼ãƒˆã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’å–å¾—ã§ãã¾ã›ã‚“ +endpoint.warn.noRemoteHost=ソケット [{0}] ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ›ã‚¹ãƒˆåã‚’å–å¾—ã§ãã¾ã›ã‚“ +endpoint.warn.noRemotePort=ソケット [{0}] ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒãƒ¼ãƒˆç•ªå·ã‚’å–å¾—ã§ãã¾ã›ã‚“ +endpoint.warn.noUtilityExecutor=ユーティリティエグゼキュータãŒæ§‹æˆã•ã‚Œã¦ã„ã¾ã›ã‚“。新ãŸã«ä½œæˆã—ã¾ã™ã€‚ +endpoint.warn.unlockAcceptorFailed=Acceptor スレッド [{0}] ã®ãƒ­ãƒƒã‚¯ã‚’解除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚強制的ã«ãƒãƒ¼ãƒ‰ã‚½ã‚±ãƒƒãƒˆã‚’シャットダウンã—ã¾ã™ã€‚ + +sniExtractor.clientHelloInvalid=ClientHelloメッセージãŒæ­£ã—ãフォーマットã•ã‚Œã¦ã„ã¾ã›ã‚“。 +sniExtractor.clientHelloTooBig=ClientHelloã¯å˜ä¸€ã®TLSレコードã«ã¯è¡¨ç¤ºã•ã‚Œãªã„ãŸã‚ã€SNI情報ã¯æŠ½å‡ºã§ãã¾ã›ã‚“ã§ã—㟠+sniExtractor.tooEarly=クライアントã®helloãŒè§£æžã•ã‚Œã‚‹å‰ã«ã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ã‚’呼ã³å‡ºã™ã“ã¨ã¯é•æ³•ã§ã™ + +socket.closed=ã“ã®ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ã«é–¢é€£ä»˜ã‘られãŸã‚½ã‚±ãƒƒãƒˆã¯é–‰ã˜ã‚‰ã‚Œã¾ã—ãŸã€‚ +socket.sslreneg=SSLコãƒã‚¯ã‚·ãƒ§ãƒ³ã®å†ãƒã‚´ã‚·ã‚¨ãƒ¼ã‚·ãƒ§ãƒ³æ™‚ã®ä¾‹å¤– + +socketWrapper.readTimeout=読ã¿è¾¼ã¿ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆ +socketWrapper.writeTimeout=書ãè¾¼ã¿ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆ + +sslHostConfig.certificate.notype=指定ã•ã‚ŒãŸè¤‡æ•°ã®è¨¼æ˜Žæ›¸ã®ä¸­ã«ã€å°‘ãªãã¨ã‚‚1ã¤ã¯å¿…é ˆè¦ç´ ã®å­˜åœ¨ã—ãªã„証明書ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ +sslHostConfig.certificateVerificationInvalid=証明書検証値[{0}]ãŒèªè­˜ã•ã‚Œã¾ã›ã‚“ +sslHostConfig.certificateVerificationWithHttp2=TLS仮想ホスト[{0}]ã¯ã‚ªãƒ—ションã®è¨¼æ˜Žæ›¸æ¤œè¨¼ç”¨ã«æ§‹æˆã•ã‚Œã¦ãŠã‚Šã€ã‚³ãƒã‚¯ã‚¿ã¯h2ã¸ã®ã‚¢ãƒƒãƒ—グレードをサãƒãƒ¼ãƒˆã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ã€‚ HTTP/2 over TLSã§ã¯ã€ã‚ªãƒ—ションã®è¨¼æ˜Žæ›¸æ¤œè¨¼ã¯è¨±å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“。 +sslHostConfig.fileNotFound=構æˆãƒ•ã‚¡ã‚¤ãƒ«[{0}]ã¯å­˜åœ¨ã—ã¾ã›ã‚“ +sslHostConfig.invalid_truststore_password=æä¾›ã•ã‚ŒãŸãƒˆãƒ©ã‚¹ãƒˆã‚¹ãƒˆã‚¢ãƒ‘スワードã¯ã€ãƒˆãƒ©ã‚¹ãƒˆã‚¹ãƒˆã‚¢ã®ãƒ­ãƒƒã‚¯è§£é™¤ãŠã‚ˆã³æ¤œè¨¼ã«ä½¿ç”¨ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ 検証をスキップã™ã‚‹nullパスワードã§ãƒˆãƒ©ã‚¹ãƒˆã‚¹ãƒˆã‚¢ã«ã‚¢ã‚¯ã‚»ã‚¹ã—よã†ã¨ã—ã¾ã—ãŸã€‚ +sslHostConfig.mismatch=[{0}] プロパティ㯠[{1}] ã¨ã„ã†åå‰ã®SSLHostConfigã§è¨­å®šã•ã‚Œã€[{2}] 構æˆæ§‹æ–‡ç”¨ã§ã™ãŒã€[{3}] 構æˆæ§‹æ–‡ã§SSLHostConfigãŒä½¿ç”¨ã•ã‚Œã¦ã„ã¾ã™ +sslHostConfig.opensslconf.alreadyset=別ã®OpenSSLConfを設定ã—よã†ã¨ã™ã‚‹ã¨ç„¡è¦–ã•ã‚Œã¾ã™ +sslHostConfig.opensslconf.null=Null OpenSSLConfを設定ã—よã†ã¨ã—ã¾ã—ãŸãŒç„¡è¦–ã•ã‚Œã¾ã—㟠+sslHostConfig.prefix_missing=[{1}]ã¨ã„ã†SSLHostConfigã®ãƒ—ロトコルã®ãƒªã‚¹ãƒˆã«ãƒ—ロトコル[{0}]ãŒè¿½åŠ ã•ã‚Œã¾ã—ãŸã€‚+/-接頭辞ãŒãªã„ã‹ç¢ºèªã—ã¦ãã ã•ã„。 + +sslHostConfigCertificate.mismatch=プロパティ [{0}] 㯠[{1}] ã¨ã„ã†åå‰ã®SSLHostConfigCertificateã«è¨­å®šã•ã‚Œã¦ãŠã‚Šã€è¨¼æ˜Žæ›¸ã®æ ¼ç´ã‚¿ã‚¤ãƒ— [{2}] 用ã§ã™ãŒã€è¨¼æ˜Žæ›¸ã¯ [{3}] タイプã®ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã§ä½¿ç”¨ã•ã‚Œã¦ã„ã¾ã™ã€‚ + +sslImplementation.cnfe=クラス [{0}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’ SSLImplementation ã¨ã—ã¦ä½œæˆã§ãã¾ã›ã‚“ + +sslUtilBase.active=アクティブ㪠[{0}] ã¯æ¬¡ã®ã¨ãŠã‚Šã§ã™: [{1}] +sslUtilBase.aliasIgnored=FIPSãŒæœ‰åŠ¹ã«ãªã£ã¦ã„ã‚‹ãŸã‚ã€ã‚¨ã‚¤ãƒªã‚¢ã‚¹å[{0}]ã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ キーストアã«è¤‡æ•°ã®ã‚­ãƒ¼ãŒã‚ã‚‹å ´åˆã€ä½¿ç”¨ã•ã‚Œã‚‹ã‚­ãƒ¼ã¯ã‚­ãƒ¼ã‚¹ãƒˆã‚¢ã®å®Ÿè£…ã«ä¾å­˜ã—ã¾ã™ +sslUtilBase.alias_no_key_entry=別å [{0}] ã¯ã‚­ãƒ¼ã‚¨ãƒ³ãƒˆãƒªã‚’発見ã§ãã¾ã›ã‚“ +sslUtilBase.invalidTrustManagerClassName=[{0}]ãŒæä¾›ã™ã‚‹trustManagerClassNameã¯javax.net.ssl.TrustManagerを実装ã—ã¦ã„ã¾ã›ã‚“。 +sslUtilBase.keystore_load_failed=[{0}] ã®ã‚­ãƒ¼ã‚¹ãƒˆã‚¢ [{1}] ã®èª­ã¿è¾¼ã¿ã¯ [{2}] ã«ã‚ˆã‚Šå¤±æ•—ã—ã¾ã—ãŸã€‚ +sslUtilBase.noCertFile=SSLHostConfig ã®å±žæ€§ certificateFile ã¯ã€SSLコãƒã‚¯ã‚¿ã‚’使用ã™ã‚‹å ´åˆã¯å¿…ãšå®šç¾©ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +sslUtilBase.noCrlSupport=トラストストアプロãƒã‚¤ãƒ€ãƒ¼ [{0}] ã¯è¨­å®šé …ç›® certificateRevocationFile ã«å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“。 +sslUtilBase.noKeys=キーストアã§è¦‹ã¤ã‹ã£ãŸç§˜å¯†ã‚­ãƒ¼ã®ã‚¨ã‚¤ãƒªã‚¢ã‚¹ãŒã‚ã‚Šã¾ã›ã‚“。 +sslUtilBase.noVerificationDepth=トラストストアプロãƒã‚¤ãƒ€ãƒ¼ [{0}] ã¯è¨­å®šé …ç›® certificateVerificationDepth ã«æœªå¯¾å¿œã§ã™ã€‚ +sslUtilBase.noneSupported=指定ã•ã‚ŒãŸ [{0}] ã®ã©ã‚Œã‚‚SSLエンジンã§ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“: [{1}] +sslUtilBase.skipped=Tomcat ã¯ã€æœ€æ–°ã® OpenSSL 開発ブランãƒã¨ä¸€è²«ã—ãŸæ–¹æ³•ã§ [{0}] 属性を解釈ã—ã¾ã™ã€‚ 指定ã•ã‚ŒãŸ [{0}] ã®ä¸€éƒ¨ã¯ã€ã“ã®ã‚³ãƒã‚¯ã‚¿ç”¨ã«æ§‹æˆã•ã‚ŒãŸ SSL エンジン (JSSE ã¾ãŸã¯å¤ã„ OpenSSL ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’使用ã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™) ã§ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„ãŸã‚ã€ã‚¹ã‚­ãƒƒãƒ—ã•ã‚Œã¾ã—ãŸ: [{1}] +sslUtilBase.ssl3=SSLv3 ãŒæ˜Žç¤ºçš„ã«æœ‰åŠ¹åŒ–化ã•ã‚Œã¦ã„ã¾ã™ã€‚ã“ã®ãƒ—ロトコルã¯å®‰å…¨ã§ã¯ã‚ã‚Šã¾ã›ã‚“。 +sslUtilBase.tls13.auth=JSSE TLS 1.3実装ã¯ã€åˆæœŸãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯å¾Œã®èªè¨¼ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ãªã„ãŸã‚ã€ã‚ªãƒ—ションã®ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆèªè¨¼ã¨äº’æ›æ€§ãŒã‚ã‚Šã¾ã›ã‚“。 +sslUtilBase.trustedCertNotChecked=エイリアス [{0}] ã‚’æŒã¤ä¿¡é ¼ã§ãる証明書ã®æœ‰åŠ¹æœŸé™ã¯ã€è¨¼æ˜Žæ›¸ãŒä¸æ˜Žãªåž‹ã§ã‚ã‚‹ãŸã‚ãƒã‚§ãƒƒã‚¯ã•ã‚Œã¾ã›ã‚“ã§ã—㟠+sslUtilBase.trustedCertNotValid=エイリアス [{0}] ã¨DN [{1}] ã‚’æŒã¤ä¿¡é ¼ã§ãる証明書㌠[{2}] ã®ãŸã‚ã«ç„¡åŠ¹ã§ã™ã€‚ã“ã®ä¿¡é ¼ã§ãる証明書ã§ç½²åã•ã‚ŒãŸè¨¼æ˜Žæ›¸ãŒå—ã‘入れられるã§ã—ょㆠdiff --git a/java/org/apache/tomcat/util/net/LocalStrings_ko.properties b/java/org/apache/tomcat/util/net/LocalStrings_ko.properties new file mode 100644 index 0000000..06b7b46 --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_ko.properties @@ -0,0 +1,166 @@ +# 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. + +acceptor.stop.fail=acceptor 쓰레드 [{0}](ì´)ê°€ 깨ë—ì´ ì¤‘ì§€ë˜ì§€ 않았습니다. +acceptor.stop.interrupted=acceptor 쓰레드 [{0}]ì´(ê°€) 중지ë˜ê¸°ë¥¼ 기다리는 중 interrupt를 받았습니다. + +channel.nio.interrupted=현재 쓰레드가 중단ë˜ì—ˆìŠµë‹ˆë‹¤. +channel.nio.ssl.appInputNotEmpty=애플리케ì´ì…˜ ìž…ë ¥ 버í¼ê°€ 여전히 ë°ì´í„°ë¥¼ í¬í•¨í•˜ê³  있습니다. ë°ì´í„°ë¥¼ ìžƒì„ ë»”í–ˆìŠµë‹ˆë‹¤. +channel.nio.ssl.appOutputNotEmpty=애플리케ì´ì…˜ 출력 버í¼ê°€ 여전히 ë°ì´í„°ë¥¼ í¬í•¨í•˜ê³  있습니다. ë°ì´í„°ë¥¼ ìžƒì„ ë»”í–ˆìŠµë‹ˆë‹¤. +channel.nio.ssl.closeSilentError=ì´ë¯¸ 예ìƒí–ˆë˜ëŒ€ë¡œ, ì—°ê²°ì„ ê¹¨ë—하게 닫으려 ì‹œë„í•˜ë˜ ì¤‘ 예외 ë°œìƒì´ 있었습니다. +channel.nio.ssl.closing=채ë„ì´ ë‹«ëŠ” 중 ìƒíƒœì— 있습니다. +channel.nio.ssl.eofDuringHandshake=Handshake 하는 ë™ì•ˆ EOF ë°œìƒ +channel.nio.ssl.expandNetInBuffer=ë„¤íŠ¸ì›Œí¬ ìž…ë ¥ 버í¼ë¥¼ [{0}] ë°”ì´íŠ¸ í¬ê¸°ë¡œ 확장합니다. +channel.nio.ssl.expandNetOutBuffer=ë„¤íŠ¸ì›Œí¬ ì¶œë ¥ 버í¼ë¥¼ [{0}] ë°”ì´íŠ¸ í¬ê¸°ë¡œ 확장합니다. +channel.nio.ssl.foundHttp=ì•”í˜¸í™”ëœ TLS ì—°ê²°ì´ì–´ì•¼ 하는 ê³³ì—ì„œ, plain í…스트 HTTP ìš”ì²­ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤. +channel.nio.ssl.handshakeError=Handshake 오류 +channel.nio.ssl.incompleteHandshake=Handshakeê°€ 완료ë˜ì§€ 않았습니다. ë°ì´í„°ë¥¼ ì½ê¸° ì „ 반드시 handshake를 완료해야 합니다. +channel.nio.ssl.invalidCloseState=유효하지 ì•Šì€, 닫힘 ìƒíƒœìž…니다. ë„¤íŠ¸ì›Œí¬ ë°ì´í„°ë¥¼ 보내지 ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. +channel.nio.ssl.invalidStatus=예기치 ì•Šì€ ìƒíƒœ [{0}]. +channel.nio.ssl.netInputNotEmpty=ë„¤íŠ¸ì›Œí¬ ìž…ë ¥ 버í¼ê°€ 여전히 ë°ì´í„°ë¥¼ í¬í•¨í•˜ê³  있습니다. Handshakeê°€ 실패할 것입니다. +channel.nio.ssl.netOutputNotEmpty=ë„¤íŠ¸ì›Œí¬ ì¶œë ¥ 버í¼ê°€ 여전히 ë°ì´í„°ë¥¼ í¬í•¨í•˜ê³  있습니다. Handshake는 실패할 것입니다. +channel.nio.ssl.notHandshaking=Handshake 중 NOT_HANDSHAKING ë°œìƒ +channel.nio.ssl.pendingWriteDuringClose=쓰기가 진행 중ì´ì–´ì„œ, ë„¤íŠ¸ì›Œí¬ ë²„í¼ì— ë°ì´í„°ê°€ 남아 있습니다. SSL 닫기 메시지를 보낼 수 없습니다. close(true)를 대신 사용하여 강제로 ë‹«ì„ ê²ƒìž…ë‹ˆë‹¤. +channel.nio.ssl.remainingDataDuringClose=ë„¤íŠ¸ì›Œí¬ ë²„í¼ì— ë°ì´í„°ê°€ 남아 있어, SSL 닫기 메시지를 보낼 수 없습니다. close(true)를 대신 사용하여 강제로 닫으려 합니다. +channel.nio.ssl.sniDefault=ìš”ì²­ëœ SNI 호스트 ì´ë¦„ì„ ê²°ì •í•˜ê¸° 위한 버í¼ë¥¼ 충분히 채울 수 없습니다. 기본 ê°’ì„ ì‚¬ìš©í•©ë‹ˆë‹¤. +channel.nio.ssl.sniHostName=ì—°ê²° [{0}]ì„(를) 위해 ì¶”ì¶œëœ SNI 호스트 ì´ë¦„ì€ [{1}]입니다. +channel.nio.ssl.timeoutDuringHandshake=Handshake ë„중 제한 시간 초과 +channel.nio.ssl.unexpectedStatusDuringUnwrap=Handshake UNWRAP 처리 중 예기치 ì•Šì€ ìƒíƒœ: [{0}] +channel.nio.ssl.unexpectedStatusDuringWrap=WRAPì„ ìœ„í•´ handshake 수행 중 예기치 ì•Šì€ ìƒíƒœ [{0}]입니다. +channel.nio.ssl.unwrapFail=ë°ì´í„°ë¥¼ unwrapí•  수 없습니다. 유효하지 ìƒíƒœ: [{0}] +channel.nio.ssl.unwrapFailResize=버í¼ê°€ 너무 ìž‘ì•„ì„œ ë°ì´í„°ë¥¼ unwrapí•  수 없습니다. 유효하지 ì•Šì€ ìƒíƒœ [{0}] +channel.nio.ssl.wrapFail=ë°ì´í„°ë¥¼ wrapí•  수 없습니다. 유효하지 ì•Šì€ ìƒíƒœ [{0}] + +endpoint.accept.fail=소켓 accept 실패 +endpoint.alpn.fail=[{0}]ì„(를) 사용하여 ALPNì„ ìœ„í•œ 엔드í¬ì¸íŠ¸ë¥¼ 설정하지 못했습니다. +endpoint.alpn.negotiated=ALPNì„ ì‚¬ìš©í•˜ì—¬ [{0}] 프로토콜로 negotiate 했습니다. +endpoint.apr.failSslContextMake=SSLContext를 ìƒì„±í•  수 없습니다. AprLifecycleListenerì—ì„œ SSLEngineì´ ì‚¬ìš©ê°€ëŠ¥ ìƒíƒœë¡œ 설정ë˜ì—ˆëŠ”지, AprLifecycleListenerê°€ 올바로 초기화ë˜ì—ˆëŠ”지, 그리고 유효한 SSLProtocolì´ ì§€ì •ë˜ì—ˆëŠ”지 ì ê²€í•˜ì‹­ì‹œì˜¤. +endpoint.apr.invalidSslProtocol=SSLProtocol ì†ì„±ì„ 위해 유효하지 ì•Šì€ ê°’ [{0}]ì´(ê°€) 제공ë˜ì—ˆìŠµë‹ˆë‹¤. +endpoint.debug.channelCloseFail=채ë„ì„ ë‹«ì§€ 못했습니다. +endpoint.debug.destroySocket=소켓 [{0}]ì„(를) 소멸시킵니다. +endpoint.debug.handlerRelease=핸들러가 소켓 wrapper를 해제하지 못했습니다. +endpoint.debug.pollerAdd=addListì— ì¶”ê°€í•©ë‹ˆë‹¤: 소켓 [{0}], 제한시간 [{1}], 플래그들 [{2}] +endpoint.debug.pollerAddDo=Pollerì— ì†Œì¼“ [{0}]ì„(를) 추가합니다. +endpoint.debug.pollerProcess=ë‹¤ìŒ ì´ë²¤íŠ¸(들)ì„ ìœ„í•´ 소켓 [{0}]ì„(를) 처리합니다: [{1}] +endpoint.debug.pollerRemove=Poller로부터 [{0}]ì„(를) 제거하려 ì‹œë„ ì¤‘ +endpoint.debug.pollerRemoved=Poller로부터 [{0}]ì„(를) 제거했습니다. +endpoint.debug.registerRead=[{0}]ì„(를) 위한 readInterest를 등ë¡í–ˆìŠµë‹ˆë‹¤. +endpoint.debug.registerWrite=[{0}]ì„(를) 위한 writeInterest를 등ë¡í–ˆìŠµë‹ˆë‹¤. +endpoint.debug.socket=소켓 [{0}] +endpoint.debug.socketTimeout=제한 시간 초과로 처리합니다: [{0}] +endpoint.debug.unlock.fail=í¬íŠ¸ [{0}]ì— ëŒ€í•œ acceptì˜ ìž ê¸ˆì„ í’€ê³ ìž ì‹œë„하는 중 예외 ë°œìƒ +endpoint.debug.unlock.localFail=[{0}]ì„(를) 위한 로컬 주소를 ê²°ì •í•  수 없습니다. +endpoint.debug.unlock.localNone=로컬 주소가 가용하지 않기 때문ì—, [{0}]ì„(를) 위한 acceptorì˜ ìž ê¸ˆ ìƒíƒœë¥¼ 풀지 못했습니다. +endpoint.duplicateSslHostName=호스트 ì´ë¦„ [{0}]ì„(를) 위해 여러 ê°œì˜ SSLHostConfig ì—˜ë¦¬ë¨¼íŠ¸ë“¤ì´ ì œê³µë˜ì—ˆìŠµë‹ˆë‹¤. 호스트 ì´ë¦„ë“¤ì€ ë°˜ë“œì‹œ 유ì¼í•´ì•¼ 합니다. +endpoint.err.close=ì†Œì¼“ì„ ë‹«ìœ¼ë ¤ ì‹œë„하는 중 예외 ë°œìƒ +endpoint.err.duplicateAccept=ì¤‘ë³µëœ ì†Œì¼“ì´ acceptëœ ê²ƒì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ëŠ” 리눅스 커ë„ì˜ ì•Œë ¤ì§„ 버그입니다. ì›ëž˜ì˜ ì—°ê²°ì€ ì •ìƒì ìœ¼ë¡œ 처리ë˜ì—ˆìœ¼ë©° ì¤‘ë³µëœ ì—°ê²°ì€ ë¬´ì‹œë˜ì—ˆìŠµë‹ˆë‹¤. í´ë¼ì´ì–¸íŠ¸ì—는 아무런 ì˜í–¥ì´ 없습니다. ìš´ì˜ì²´ê³„를 ì»¤ë„ 5.10 ë˜ëŠ” ì´í›„ 버전으로 업그레ì´ë“œí•˜ì—¬ ì´ ì¤‘ë³µ accept 버그를 수정하십시오. +endpoint.err.handshake=Handshakeê°€ 실패했습니다. +endpoint.err.unexpected=소켓 처리 중 예기치 ì•Šì€ ì˜¤ë¥˜ ë°œìƒ +endpoint.executor.fail=Executorê°€ 소켓 [{0}]ì„(를) 처리하기를 거부했습니다. +endpoint.getAttribute=[{0}]ì˜ ê°’ì€ [{1}]입니다. +endpoint.init.bind=소켓 ë°”ì¸ë”© 실패: [{0}] [{1}] +endpoint.init.bind.inherited=Connectorê°€ ìƒì†ëœ 채ë„ì„ ì‚¬ìš©í•˜ë„ë¡ ì„¤ì •ë˜ì—ˆëŠ”ë°, ìƒì†ëœ 채ë„ì´ ì—†ìŠµë‹ˆë‹¤. +endpoint.init.listen=소켓 listen 실패: [{0}] [{1}] +endpoint.init.unixnotavail=Unix ë„ë©”ì¸ ì†Œì¼“ì´ ì§€ì›ë˜ì§€ 않습니다. +endpoint.invalidJmxNameSslHost=호스트 [{0}]와(ê³¼) ì—°ê´€ëœ SSLHostConfig를 위한, 유효한 JMX ê°ì²´ ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +endpoint.invalidJmxNameSslHostCert=호스트가 [{0}]ì´ê³  ì¸ì¦ì„œ íƒ€ìž…ì´ [{1}]ì¸ SSLHostConfigCertificateì„ ìœ„í•œ, 유효한 JMX ê°ì²´ ì´ë¦„ì„ ìƒì„±í•  수 없습니다. +endpoint.jmxRegistrationFailed=JMX ê°ì²´ë¥¼ [{0}](ì´)ë¼ëŠ” ì´ë¦„으로 등ë¡ì‹œí‚¤ì§€ 못했습니다. +endpoint.jsse.noSslContext=호스트 ì´ë¦„ [{0}]ì„(를) 위한 SSLContext를 ì°¾ì„ ìˆ˜ 없습니다. +endpoint.launch.fail=새로운 Runnableì„ ì‹œìž‘í•˜ì§€ 못했습니다. +endpoint.nio.keyProcessingError=Selection 키를 처리 중 오류 ë°œìƒ +endpoint.nio.latchMustBeZero=Latch는 반드시 ë„ì´ê±°ë‚˜ countê°€ 0ì´ì–´ì•¼ 합니다. +endpoint.nio.nullLatch=Latchê°€ ë„ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +endpoint.nio.nullSocketChannel=PollerEvent를 처리하는 중 유효하지 ì•Šì€ ë„ ì†Œì¼“ 채ë„ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤. +endpoint.nio.perms.readFail=Unix ë„ë©”ì¸ ì†Œì¼“ [{0}]ì— ì½ê¸°ë¥¼ 허용하는 ë°ì— 실패했습니다. +endpoint.nio.perms.writeFail=Unix ë„ë©”ì¸ ì†Œì¼“ [{0}]ì— ì“°ê¸°ë¥¼ 허용하는 ë°ì— 실패했습니다. +endpoint.nio.registerFail=Pollerë¡œë¶€í„°ì˜ selector와 함께, ì†Œì¼“ì„ ë“±ë¡í•˜ì§€ 못했습니다. +endpoint.nio.selectorCloseFail=Poller를 ë‹«ì„ ë•Œ, selector를 닫지 못했습니다. +endpoint.nio.selectorLoopError=Selector 루프 ë‚´ì—ì„œ 오류 ë°œìƒ +endpoint.nio.stopLatchAwaitFail=Pollerë“¤ì´ ìš”êµ¬ë˜ëŠ” 시간 ë‚´ì— ì¤‘ì§€ë˜ì§€ 않았습니다. +endpoint.nio.stopLatchAwaitInterrupted=ì´ ì“°ë ˆë“œëŠ” pollerë“¤ì´ ì¤‘ì§€ë˜ê¸°ë¥¼ 기다리는 ë™ì•ˆ 중단ë˜ì—ˆìŠµë‹ˆë‹¤. +endpoint.nio.timeoutCme=제한 시간 ì´ˆê³¼ë“¤ì„ ì²˜ë¦¬í•˜ëŠ” ë™ì•ˆ 예외 ë°œìƒ. 해당 코드는 반복해서 ì ê²€ë˜ì–´ 왔고 ConcurrentModificationExceptionì´ ë°œìƒí•˜ì§€ ì•Šì•„ 왔습니다. ì´ ì˜¤ë¥˜ë¥¼ 재현할 수 있다면, Tomcat 버그 í‹°ì¼“ì„ ì—¬ì‹œê³  오류 재현 ê³¼ì •ë“¤ì„ ì œê³µí•´ 주십시오. +endpoint.nio2.exclusiveExecutor=NIO2 Connectorê°€ 셧다운 ì‹œì— ì •ìƒì ìœ¼ë¡œ ë™ìž‘하기 위해서는, 배타ì ì¸ Executorê°€ 필수ì ìœ¼ë¡œ 요구ë©ë‹ˆë‹¤. +endpoint.noSslHostConfig=호스트 ì´ë¦„ [{0}]ì„(를) 사용하여, Connector [{1}]ì„(를) 위한 defaultSSLHostConfigNameê³¼ 부합하는 SSLHostConfig 엘리먼트를 찾지 못했습니다. +endpoint.noSslHostName=SSL 호스트 ì„¤ì •ì„ ìœ„í•œ 호스트 ì´ë¦„ì´ ì œê³µë˜ì§€ 않았습니다. +endpoint.poll.error=예기치 ì•Šì€ poller 오류 ë°œìƒ +endpoint.poll.fail=심ê°í•œ poller 실패 (poller를 재시작합니다): [{0}] [{1}] +endpoint.poll.initfail=Poller ìƒì„±ì´ 실패했습니다. +endpoint.poll.limitedpollsize=ì§€ì •ëœ í¬ê¸° [{0}]ë¡œ poller를 ìƒì„±í•˜ì§€ 못했습니다. +endpoint.pollerThreadStop=Poller 쓰레드가 ì ì ˆí•œ 시간 ë‚´ì— ì¤‘ì§€ë˜ì§€ 못했습니다. +endpoint.portOffset.invalid=portOffset ê°’ì€ ìŒìˆ˜ì¼ 수 없기ì—, portOffsetì„ ìœ„í•œ ê°’ [{0}]ì€(는) 유효하지 않습니다. +endpoint.process.fail=소켓 프로세서를 할당하는 중 오류 ë°œìƒ +endpoint.processing.fail=소켓 프로세서 실행 중 오류 ë°œìƒ +endpoint.rejectedExecution=[{0}]ì„(를) 위한 소켓 처리 ìš”ì²­ì´ ê±°ì ˆë˜ì—ˆìŠµë‹ˆë‹¤. +endpoint.removeDefaultSslHostConfig=기본 SSLHostConfig(ì´ë¦„: [{0}])는 ì œê±°ë  ìˆ˜ 없습니다. +endpoint.sendfile.addfail=Sendfile 실패: [{0}] [{1}] +endpoint.sendfile.closeError=sendfile ë¦¬ì†ŒìŠ¤ë“¤ì„ ë‹«ëŠ” 중 오류 ë°œìƒ +endpoint.sendfile.error=예기치 ì•Šì€ sendfile 오류 +endpoint.sendfile.tooMuchData=가용한 ë°ì´í„°ë³´ë‹¤ ë” ë§Žì€ ë°ì´í„°ë¥¼ 전송하ë„ë¡, Sendfileì´ ì„¤ì •ë˜ì—ˆìŠµë‹ˆë‹¤. +endpoint.sendfileThreadStop=sendfile 쓰레드가 정해진 시간 ë‚´ì— ì¢…ë£Œë˜ì§€ 못했습니다. +endpoint.serverSocket.closeFailed=엔드í¬ì¸íŠ¸ [{0}]ì„(를) 위한 서버 ì†Œì¼“ì„ ë‹«ì§€ 못했습니다. +endpoint.setAttribute=[{1}]ì— [{0}]ì„(를) 설정합니다. +endpoint.setAttributeError=ì†ì„± [{0}]ì„(를) [{1}](으)ë¡œ 설정할 수 없습니다. +endpoint.socketOptionsError=소켓 ì˜µì…˜ë“¤ì„ ì„¤ì •í•˜ëŠ” 중 오류 ë°œìƒ +endpoint.timeout.err=소켓 제한 시간 초과 처리 중 오류 ë°œìƒ +endpoint.unknownSslHostName=SSL 호스트 ì´ë¦„ [{0}]ì€(는), ì´ ì—”ë“œí¬ì¸íŠ¸ë¥¼ 위해 ì¸ì‹ë˜ì§€ 않는 ì´ë¦„입니다. +endpoint.warn.executorShutdown=쓰레드 í’€ [{0}]와(ê³¼) ì—°ê´€ëœ í•´ë‹¹ Executor는 완전히 종료ë˜ì§€ 않았습니다. ì¼ë¶€ 애플리케ì´ì…˜ ì“°ë ˆë“œë“¤ì´ ì—¬ì „ížˆ 실행 ì¤‘ì¼ ìˆ˜ 있습니다. +endpoint.warn.incorrectConnectionCount=ìž˜ëª»ëœ ì—°ê²° 개수. ë™ì¼í•œ ì†Œì¼“ì— ì—¬ëŸ¬ ë²ˆì˜ socket.closeê°€ 호출ë˜ì—ˆìŒ. +endpoint.warn.noLocalAddr=소켓 [{0}]ì„(를) 위한 로컬 주소를 ê²°ì •í•  수 없습니다. +endpoint.warn.noLocalName=소켓 [{0}]ì„(를) 위한 로컬 호스트 ì´ë¦„ì„ ê²°ì •í•  수 없습니다. +endpoint.warn.noLocalPort=소켓 [{0}]ì„(를) 위한 로컬 í¬íŠ¸ë¥¼ ê²°ì •í•  수 없습니다. +endpoint.warn.noRemoteAddr=소켓 [{0}]ì„(를) 위한 ì›ê²© 주소를 ê²°ì •í•  수 없습니다. +endpoint.warn.noRemoteHost=소켓 [{0}]ì„(를) 위한 ì›ê²© 호스트 ì´ë¦„ì„ ê²°ì •í•  수 없습니다. +endpoint.warn.noRemotePort=소켓 [{0}]ì„(를) 위한 ì›ê²© í¬íŠ¸ë¥¼ ê²°ì •í•  수 없습니다. +endpoint.warn.noUtilityExecutor=UtilityExecutorê°€ 설정ë˜ì§€ ì•Šì•„, 새로 ìƒì„±í•©ë‹ˆë‹¤. +endpoint.warn.unlockAcceptorFailed=Acceptor 쓰레드 [{0}]ì´(ê°€) ìž ê¸ˆì„ í’€ì§€ 못했습니다. 강제로 ì†Œì¼“ì„ ì…§ë‹¤ìš´í•©ë‹ˆë‹¤. + +sniExtractor.clientHelloInvalid=ClientHello 메시지가 정확히 í¬ë§·ë˜ì§€ 않았습니다. +sniExtractor.clientHelloTooBig=ClientHelloê°€ ë‹¨ì¼ TLS ë ˆì½”ë“œì— ì¡´ìž¬í•˜ì§€ 않았기ì—, SNI 정보를 추출할 수 없었습니다. +sniExtractor.tooEarly=í´ë¼ì´ì–¸íŠ¸ 헬로 메시지가 파싱ë˜ê¸° ì „ì— ì´ ë©”ì†Œë“œë¥¼ 호출하는 ê²ƒì€ í—ˆìš©ë˜ì§€ 않습니다. + +socket.closed=ì´ ì—°ê²°ê³¼ ì—°ê´€ëœ í•´ë‹¹ ì†Œì¼“ì€ ì´ë¯¸ 닫혔습니다. +socket.sslreneg=SSL ì—°ê²°ì„ re-negotiate하는 ë™ì•ˆ 예외 ë°œìƒ + +socketWrapper.readTimeout=ì½ê¸° 타임아웃 +socketWrapper.writeTimeout=쓰기 타임아웃 + +sslHostConfig.certificate.notype=여러 ê°œì˜ ì¸ì¦ì„œë“¤ì´ 지정ë˜ì—ˆëŠ”ë°, ì ì–´ë„ í•˜ë‚˜ì˜ ì¸ì¦ì„œì— 필수 ì†ì„± íƒ€ìž…ì´ ì—†ìŠµë‹ˆë‹¤. +sslHostConfig.certificateVerificationInvalid=ì¸ì¦ì„œ ê²€ì¦ ê°’ [{0}]ì€(는) ì¸ì‹ë˜ì§€ 않는 값입니다. +sslHostConfig.certificateVerificationWithHttp2=TLS ê°€ìƒ í˜¸ìŠ¤íŠ¸ [{0}](ì€)는 ì„ íƒì  ì¸ì¦ì„œ 확ì¸ì„ 하ë„ë¡ ì„¤ì •ë˜ì—ˆê³ , ë‚´ë¶€ì— ì •ì˜ëœ 커넥터는 HTTP/2ë¡œ 업그레ì´ë“œë¥¼ 지ì›í•˜ë„ë¡ ì„¤ì •ë˜ì—ˆìŠµë‹ˆë‹¤. TLS ê¸°ë°˜ì˜ HTTP/2는 ì„ íƒì  ì¸ì¦ì„œ 확ì¸ì„ 허용하지 않습니다. +sslHostConfig.fileNotFound=ì„¤ì •ëœ íŒŒì¼ [{0}]ì´(ê°€) 존재하지 않습니다. +sslHostConfig.invalid_truststore_password=Trust 저장소를 ìž ê¸ˆì„ í’€ê±°ë‚˜ 유효한지 확ì¸í•˜ëŠ” ìš©ë„ë¡œ, ì œê³µëœ Trust 저장소 비밀번호를 사용할 수 없었습니다. ë„ ë¹„ë°€ë²ˆí˜¸ë¥¼ 사용하여, 해당 Trust ì €ìž¥ì†Œì— ëŒ€í•œ ì ‘ê·¼ì„ ë‹¤ì‹œ ì‹œë„합니다. ì´ëŠ” 유효한지 확ì¸í•˜ëŠ” ìž‘ì—…ì„ ê±´ë„ˆë›¸ 것입니다. +sslHostConfig.mismatch=[{1}](ì´)ë¼ëŠ” ì´ë¦„ì˜ SSLHostConfigì— í”„ë¡œí¼í‹° [{0}]ì´(ê°€) 설정ë˜ì—ˆëŠ”ë°, ì´ í”„ë¡œí¼í‹°ëŠ” [{2}] 설정 ë¬¸ë²•ì„ ìœ„í•œ 것ì´ë‚˜, 해당 SSLHostConfigì€ [{3}] 설정 문법으로 사용ë˜ê³  있습니다. +sslHostConfig.opensslconf.alreadyset=ë˜ ë‹¤ë¥¸ OpenSSLConf 설정 ì‹œë„ê°€ 있어 ì´ë¥¼ 무시합니다. +sslHostConfig.opensslconf.null=ë„ì¸ OpenSSLConf를 설정하려는 ì‹œë„ê°€ 무시ë˜ì—ˆìŠµë‹ˆë‹¤. +sslHostConfig.prefix_missing=프로토콜 [{0}]ì´(ê°€) [{1}](ì´)ë¼ëŠ” ì´ë¦„ì„ ê°€ì§„ SSLHostConfigì˜ í”„ë¡œí† ì½œ 목ë¡ì— 추가ë˜ì–´ 있습니다. +/- prefixê°€ 누ë½ë˜ì—ˆëŠ”지 ì ê²€í•˜ì‹­ì‹œì˜¤. + +sslHostConfigCertificate.mismatch=프로í¼í‹° [{0}]ì´(ê°€) [{1}](ì´)ë¼ëŠ” ì´ë¦„ì˜ SSLHostConfigCertificateì— ì„¤ì •ë˜ì—ˆê³ , ì´ëŠ” ì¸ì¦ì„œ 저장소 타입 [{2}]ì„(를) 위한 것ì´ì§€ë§Œ, ì¸ì¦ì„œê°€ 타입 [{3}]ì˜ ì¸ì¦ì„œ 저장소와 함께 사용ë˜ê³  있습니다. + +sslImplementation.cnfe=í´ëž˜ìŠ¤ [{0}]ì˜ SSLImplementation ê°ì²´ë¥¼ ìƒì„±í•  수 없습니다. + +sslUtilBase.active=활성화 ëœ [{0}]ì€(는) 다ìŒê³¼ 같습니다: [{1}] +sslUtilBase.aliasIgnored=FIPSì´ í™œì„±í™”ë˜ì–´ 있어, 별칭 [{0}]ì€(는) 무시ë©ë‹ˆë‹¤. 키 ì €ìž¥ì†Œì— ë‘˜ ì´ìƒì˜ 키가 존재하는 경우, ì–´ë–¤ 키를 사용할지는 키 스토어 êµ¬í˜„ì— ì˜ì¡´í•˜ê²Œ ë©ë‹ˆë‹¤. +sslUtilBase.alias_no_key_entry=별칭 ì´ë¦„ [{0}]ì„(를) 사용하여 키 엔트리를 ì‹ë³„해낼 수 없습니다. +sslUtilBase.invalidTrustManagerClassName=trustManagerClassNameì— ì˜í•´ ì œê³µëœ í´ëž˜ìŠ¤ [{0}]ì€(는) javax.net.ssl.TrustManager를 구현하지 않았습니다. +sslUtilBase.keystore_load_failed=[{2}](으)ë¡œ ì¸í•˜ì—¬, 경로 [{1}]ì— ìžˆê³  íƒ€ìž…ì´ [{0}]ì¸ í‚¤ 저장소를 로드하지 못했습니다. +sslUtilBase.noCertFile=SSL connector를 사용 ì‹œ SSLHostConfig certificateFile 어트리뷰트가 ì •ì˜ë˜ì–´ì•¼ 합니다. +sslUtilBase.noCrlSupport=truststoreProvider [{0}]ì€(는) certificateRevocationFile 설정 ì˜µì…˜ì„ ì§€ì›í•˜ì§€ 않습니다. +sslUtilBase.noKeys=ê°œì¸ í‚¤ë“¤ì— ëŒ€í•œ ë³„ì¹­ë“¤ì´ í‚¤ ì €ìž¥ì†Œì— ì—†ìŠµë‹ˆë‹¤. +sslUtilBase.noVerificationDepth=truststoreProvider [{0}]ì€(는) certificateVerificationDepth 설정 ì˜µì…˜ì„ ì§€ì›í•˜ì§€ 않습니다. +sslUtilBase.noneSupported=ì§€ì •ëœ [{0}]ì˜ ì–´ëŠ ê²ƒë„ SSL ì—”ì§„ì— ì˜í•´ 지ì›ë˜ì§€ 않습니다: [{1}] +sslUtilBase.ssl3=SSLv3ì´ ëª…ì‹œì ìœ¼ë¡œ 사용 가능 ìƒíƒœë¡œ 설정ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ í”„ë¡œí† ì½œì€ ì•ˆì „í•˜ì§€ ì•Šì€ ê²ƒìœ¼ë¡œ 알려져 있습니다. +sslUtilBase.tls13.auth=JSSE TLS 1.3 êµ¬í˜„ì´ ì´ˆê¸° handshake ì´í›„ì˜ ì¸ì¦ì„ 지ì›í•˜ì§€ ì•ŠìŒì— ë”°ë¼, ì„ íƒì‚¬í•­ì¸ í´ë¼ì´ì–¸íŠ¸ ì¸ì¦ê³¼ 호환ë˜ì§€ 않습니다. +sslUtilBase.trustedCertNotChecked=ì¸ì¦ì„œê°€ ì•Œ 수 없는 타입ì´ë¼ì„œ, ë³„ì¹­ì´ [{0}]ì¸ ì‹ ë¢°ë˜ëŠ” ì¸ì¦ì„œì˜ 유효ì¼ìžë“¤ì´ ì ê²€ë˜ì§€ 않았습니다. +sslUtilBase.trustedCertNotValid=ë³„ì¹­ì´ [{0}](ì´)ê³  DNì´ [{1}]ì¸ í•´ë‹¹ 신뢰받는 ì¸ì¦ì„œëŠ” [{2}](으)ë¡œ ì¸í•˜ì—¬ 유효하지 않습니다. ì´ ì‹ ë¢°ë˜ëŠ” ì¸ì¦ì„œì— ì˜í•´ ì„œëª…ëœ ì¸ì¦ì„œë“¤ì€ 받아들여질 것입니다. diff --git a/java/org/apache/tomcat/util/net/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/net/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..0c85fcb --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_pt_BR.properties @@ -0,0 +1,21 @@ +# 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. + +channel.nio.ssl.sniHostName=O nome do host SSI extraíd para conexão [{0}] foi [{1}] + +endpoint.invalidJmxNameSslHostCert=Impossível gerar um nome de objeto JMX válido para SSLHostConfigCertificate associado ao host [{0}] e tipo de certificado [{1}]. +endpoint.serverSocket.closeFailed=Falha ao fechar socket de servidor para [{0}] + +sslHostConfig.fileNotFound=Arquivo de configuração [{0}] não existe diff --git a/java/org/apache/tomcat/util/net/LocalStrings_ru.properties b/java/org/apache/tomcat/util/net/LocalStrings_ru.properties new file mode 100644 index 0000000..7b8ea54 --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_ru.properties @@ -0,0 +1,30 @@ +# 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. + +channel.nio.ssl.closing=Канал в ÑоÑтоÑнии закрытиÑ. +channel.nio.ssl.sniHostName=Ð˜Ð¼Ñ Ñ…Ð¾Ñта SNI извлеченное Ð´Ð»Ñ ÑÐ¾ÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ [{0}] было [{1}] +channel.nio.ssl.unwrapFail=Ðевозможно развернуть данные, некорректный ÑÑ‚Ð°Ñ‚ÑƒÑ [{0}] + +endpoint.debug.channelCloseFail=Ðе возможно закрыть канал +endpoint.sendfile.addfail=Ошибка Sendfile: [{0}] [{1}] +endpoint.sendfileThreadStop=Поток Sendfile не Ñмог оÑтановитьÑÑ Ð²Ð¾Ð²Ñ€ÐµÐ¼Ñ +endpoint.serverSocket.closeFailed=Ошибка при попытке Ð·Ð°ÐºÑ€Ñ‹Ñ‚Ð¸Ñ Ñокета Ñервера Ð´Ð»Ñ [{0}] +endpoint.setAttribute=УÑтановка [{0}] в [{1}] +endpoint.warn.incorrectConnectionCount=Ðеверное количеÑтво подключений, множеÑтвенные вызовы socket.close Ð´Ð»Ñ Ð¾Ð´Ð½Ð¾Ð³Ð¾ и того же Ñокета + +sslHostConfig.fileNotFound=Сконфигурированный файл [{0}] не ÑущеÑтвует + +sslUtilBase.noneSupported=Ðи один из указанных [{0}] не поддерживаетÑÑ Ð´Ð²Ð¸Ð¶ÐºÐ¾Ð¼ SSL: [{1}] +sslUtilBase.ssl3=SSLv3 был включен diff --git a/java/org/apache/tomcat/util/net/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/net/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..f7008b0 --- /dev/null +++ b/java/org/apache/tomcat/util/net/LocalStrings_zh_CN.properties @@ -0,0 +1,168 @@ +# 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. + +acceptor.stop.fail=接收器线程[{0}]未能干净的åœæ­¢ +acceptor.stop.interrupted=等待接收器线程[{0}]åœæ­¢æ—¶æ”¶åˆ°ä¸­æ–­ä¿¡å· + +channel.nio.interrupted=当å‰çº¿ç¨‹è¢«ä¸­æ–­ã€‚ +channel.nio.ssl.appInputNotEmpty=应用程åºè¾“入缓冲区ä»ç„¶åŒ…å«æ•°æ®ã€‚æ•°æ®å°†ä¼šä¸¢å¤±ã€‚ +channel.nio.ssl.appOutputNotEmpty=应用程åºè¾“出缓冲区ä»åŒ…å«æ•°æ®ã€‚æ•°æ®å¯èƒ½ä¼šä¸¢å¤±ã€‚ +channel.nio.ssl.closeSilentError=如所料,å°è¯•å®Œå…¨å…³é—­è¿žæŽ¥æ—¶å‡ºçŽ°å¼‚常。 +channel.nio.ssl.closing=通é“å¤„äºŽå…³é—­çŠ¶æ€ +channel.nio.ssl.eofDuringHandshake=æ¡æ‰‹æœŸé—´EOF +channel.nio.ssl.expandNetInBuffer=增加网络入å£ç¼“冲到[{0}]字节 +channel.nio.ssl.expandNetOutBuffer=正在将网络输出缓冲区扩展到[{0}]字节。 +channel.nio.ssl.foundHttp=找到一个明文HTTP请求,它应该是一个加密的TLS连接 +channel.nio.ssl.handshakeError=æ¡æ‰‹é”™è¯¯ã€‚ +channel.nio.ssl.incompleteHandshake=æ¡æ‰‹æœªå®Œæˆï¼Œæ‚¨å¿…须在读å–æ•°æ®ä¹‹å‰å®Œæˆæ¡æ‰‹ã€‚ +channel.nio.ssl.invalidCloseState=无效的关闭状æ€ï¼Œä¸ä¼šå‘é€ç½‘络数æ®ã€‚ +channel.nio.ssl.invalidStatus=æ„外状æ€[{0}]。 +channel.nio.ssl.netInputNotEmpty=网络输入缓冲区ä»ç„¶åŒ…å«æ•°æ®ã€‚æ¡æ‰‹ä¼šå¤±è´¥çš„。 +channel.nio.ssl.netOutputNotEmpty=网络输出缓冲区ä»ç„¶åŒ…å«æ•°æ®ã€‚æ¡æ‰‹ä¼šå¤±è´¥çš„。 +channel.nio.ssl.notHandshaking=æ¡æ‰‹è®¤è¯æœŸé—´NOT_HANDSHAKING +channel.nio.ssl.pendingWriteDuringClose=挂起写入,因此网络缓冲区中的剩余数æ®æ— æ³•å‘é€SSL close消æ¯ï¼Œå¥—接字已关闭 +channel.nio.ssl.remainingDataDuringClose=网络缓冲区中的剩余数æ®ï¼Œæ— æ³•å‘é€SSL关闭消æ¯ï¼Œå¥—接字ä»å°†å…³é—­ +channel.nio.ssl.sniDefault=无法缓冲足够的数æ®æ¥ç¡®å®šè¯·æ±‚çš„SNI主机å。使用默认值 +channel.nio.ssl.sniHostName=连接[{0}]中æå–çš„SNI主机å称是[{1}] +channel.nio.ssl.timeoutDuringHandshake=æ¡æ‰‹æœŸé—´è¶…时。 +channel.nio.ssl.unexpectedStatusDuringUnwrap=æ¡æ‰‹å±•å¼€æœŸé—´å‡ºçŽ°æ„外状æ€[{0}]。 +channel.nio.ssl.unexpectedStatusDuringWrap=æ¡æ‰‹WRAP期间出现æ„外状æ€[{0}]。 +channel.nio.ssl.unwrapFail=无法解包数æ®ï¼Œæ— æ•ˆçŠ¶æ€ [{0}] +channel.nio.ssl.unwrapFailResize=由于缓冲区太å°æ— æ³•è§£åŒ…æ•°æ®ï¼Œæ— æ•ˆçŠ¶æ€ [{0}] +channel.nio.ssl.wrapFail=无法包装数æ®ï¼ŒçŠ¶æ€æ— æ•ˆ[{0}] + +endpoint.accept.fail=套接字接å—失败 +endpoint.alpn.fail=无法使用[{0}]为ALPNé…置终结点。 +endpoint.alpn.negotiated=使用ALPNå商的[{0}]åè®® +endpoint.apr.failSslContextMake=无法创建SSLContext。检查AprLifecycleListener中的SSLEngine 是å¦å·²å¯ç”¨ï¼ŒAprLifecycleListener是å¦å·²æ­£ç¡®åˆå§‹åŒ–,以åŠæ˜¯å¦å·²æŒ‡å®šæœ‰æ•ˆçš„SSLProtocol +endpoint.apr.invalidSslProtocol=为SSLProtocol属性æ供了无效值[{0}] +endpoint.debug.channelCloseFail=关闭频é“失败 +endpoint.debug.destroySocket=将销æ¯socket [{0}] +endpoint.debug.handlerRelease=处ç†ç¨‹åºæ— æ³•é‡Šæ”¾å¥—接字包装 +endpoint.debug.pollerAdd=添加到addList socket[{0}],timeout[{1}],flags[{2}] +endpoint.debug.pollerAddDo=添加到轮询器套接字[{0}] +endpoint.debug.pollerProcess=为事件[{1}]处ç†å¥—接字[{0}] +endpoint.debug.pollerRemove=正在å°è¯•ä»Žè½®è¯¢å™¨ä¸­åˆ é™¤[{0}]。 +endpoint.debug.pollerRemoved=已从轮询器中删除[{0}] +endpoint.debug.registerRead=[{0}]的注册读兴趣 +endpoint.debug.registerWrite=[{0}]çš„æ³¨å†Œå†™åˆ©æ¯ +endpoint.debug.socket=socket [{0}] +endpoint.debug.socketTimeout=超时 [{0}] +endpoint.debug.unlock.fail=å°è¯•åœ¨ç«¯å£[{0}]上解除接å—é”定时æ•èŽ·åˆ°å¼‚常 +endpoint.debug.unlock.localFail=无法确定[{0}]çš„æœ¬åœ°åœ°å€ +endpoint.debug.unlock.localNone=无法解除 [{0}] 的接å—器,因为本地地å€ä¸å¯ç”¨ +endpoint.duplicateSslHostName=为主机å[{0}]æ供了多个SSLHostConfig元素。主机å必须唯一 +endpoint.err.close=抓ä½å¼‚常试图关闭socket +endpoint.err.duplicateAccept=检测到接å—é‡å¤å¥—接字. 这是一个已知的Linux内核BUG. 原始连接被正常处ç†ï¼Œé‡å¤è¿žæŽ¥å·²è¢«å¿½ç•¥. 客户端ä¸å—å½±å“.å°†OS内核更新到5.10åŠä»¥ä¸Šç‰ˆæœ¬åº”该å¯ä»¥ä¿®å¤è¿™ä¸ªé—®é¢˜. +endpoint.err.handshake=æ¥è‡ªIP地å€[{0}]和端å£[{1}]的客户端连接æ¡æ‰‹å¤±è´¥ +endpoint.err.unexpected=处ç†å¥—接字时æ„外错误 +endpoint.executor.fail=执行器拒ç»äº†ç”¨äºŽå¤„ç†çš„套接字[{0}] +endpoint.getAttribute=[{0}] 是 [{1}] +endpoint.init.bind=套接字绑定失败: [{0}] [{1}] +endpoint.init.bind.inherited=连接器é…置为使用继承通é“æ—¶æ²¡æœ‰ç»§æ‰¿é€šé“ +endpoint.init.listen=套接字侦å¬å¤±è´¥ï¼š[{0}][{1}] +endpoint.init.unixnotavail=ä¸æ”¯æŒåŸŸå¥—接字 +endpoint.invalidJmxNameSslHost=无法为与主机[{0}]å…³è”çš„SSLHostConfig生æˆæœ‰æ•ˆçš„JMX对象å +endpoint.invalidJmxNameSslHostCert=对于SSLHostConfigCertificateå…³è”的主机[{0}]å’Œè¯ä¹¦ç±»åž‹[{1}],无法生æˆæœ‰æ•ˆçš„XML对象å称 +endpoint.jmxRegistrationFailed=未能使用å称[{0}]注册JMX对象 +endpoint.jsse.noSslContext=):找ä¸åˆ°ä¸»æœºå[{0}]çš„SSLContext +endpoint.launch.fail=无法å¯åŠ¨æ–°çš„å¯è¿è¡Œæ–‡ä»¶ +endpoint.nio.keyProcessingError=处ç†é€‰æ‹©é”®æ—¶å‡ºé”™ +endpoint.nio.latchMustBeZero=é—©é”必须为0或空 +endpoint.nio.nullLatch=é—©é”ä¸èƒ½ä¸ºç©º +endpoint.nio.nullSocketChannel=处ç†è½®è®­äº‹ä»¶æ—¶å‡ºçŽ°æ— æ•ˆçš„ç©ºå¥—æŽ¥å­—é€šé“ +endpoint.nio.perms.readFail=设置Unix域套接字[{0}]的读å–æƒé™å¤±è´¥ +endpoint.nio.perms.writeFail=设置Unix域套接字[{0}]的写å–æƒé™å¤±è´¥ +endpoint.nio.registerFail=无法用轮询器中的选择器注册套接字。 +endpoint.nio.selectorCloseFail=关闭轮询器时未能关闭选择器 +endpoint.nio.selectorLoopError=选择器循环出错 +endpoint.nio.stopLatchAwaitFail=在预期的时间内,pollers未åœæ­¢ +endpoint.nio.stopLatchAwaitInterrupted=在等待轮询器åœæ­¢æ—¶ï¼Œè¯¥çº¿ç¨‹è¢«ä¸­æ–­ +endpoint.nio.timeoutCme=处ç†è¶…时异常. 这段代ç å·²ç»è¢«é‡å¤æ£€æŸ¥å¹¶ä¸”没有并å‘修改å‘现。如果你能é‡çŽ°è¿™ä¸ªé”™è¯¯ï¼Œè¯·æ交一个tomcat bug, æä¾›é‡çŽ°æ­¥éª¤. +endpoint.nio2.exclusiveExecutor=NIO2连接器需è¦ä¸€ä¸ªç‹¬å çš„执行器æ‰èƒ½åœ¨å…³æœºæ—¶æ­£å¸¸è¿è¡Œ +endpoint.noSslHostConfig=没有找到带有hostName[{0}]çš„SSLHostConfig元素,以匹é…连接器[{1}]的默认SSLHostConfigName +endpoint.noSslHostName=SSL主机中没有被æ供主机å +endpoint.poll.error=æ„外的轮询器错误 +endpoint.poll.fail=严é‡è½®è¯¢å™¨æ•…障(é‡æ–°å¯åŠ¨è½®è¯¢å™¨ï¼‰[{0}] [{1}] +endpoint.poll.initfail=轮询器创建失败。 +endpoint.poll.limitedpollsize=创建轮询器失败,大å°ï¼š[{0}] +endpoint.pollerThreadStop=轮询线程未能åŠæ—¶åœæ­¢ã€‚ +endpoint.portOffset.invalid=portOffset的值[{0}]无效,因为portOffsetä¸èƒ½ä¸ºè´Ÿã€‚ +endpoint.process.fail=åˆ†é… socket 处ç†å™¨å‡ºé”™ +endpoint.processing.fail=è¿è¡Œ.套接字处ç†å™¨å‡ºé”™ +endpoint.rejectedExecution=[{0}]的套接字处ç†è¯·æ±‚è¢«æ‹’ç» +endpoint.removeDefaultSslHostConfig=默认SSLHostConfig(å为[{0}])å¯èƒ½æœªè¢«ç§»é™¤ +endpoint.sendfile.addfail=å‘é€æ–‡ä»¶(Sendfile)失败: [{0}] [{1}] +endpoint.sendfile.closeError=关闭å‘é€æ–‡ä»¶èµ„æºæ—¶å‡ºé”™ +endpoint.sendfile.error=未知的sendfile异常。 +endpoint.sendfile.tooMuchData=Sendfileé…置为å‘é€æ¯”å¯ç”¨æ•°æ®æ›´å¤šçš„æ•°æ® +endpoint.sendfileThreadStop=sendfile线程无法åŠæ—¶åœæ­¢ +endpoint.serverSocket.closeFailed=无法为 [{0}] 关闭æœåŠ¡å™¨ socket +endpoint.setAttribute=设置. [{0}] 到 [{1}] +endpoint.setAttributeError=无法将属性[{0}]设置为[{1}] +endpoint.socketOptionsError=设置套接字选项时出错 +endpoint.timeout.err=处ç†å¥—接字超时出错 +endpoint.tls.cert.encodingError=è¯ä¹¦æŒ‡çº¹ä¸å¯ç”¨ +endpoint.tls.cert.noCerts=è¯ä¹¦è¯¦ç»†ä¿¡æ¯ä¸å¯ç”¨,因为SSLContext返回的è¯ä¹¦é“¾ä¸ºç©º +endpoint.unknownSslHostName=此终结点无法识别SSL主机å[{0}] +endpoint.warn.executorShutdown=与线程池[{0}]å…³è”的执行程åºå°šæœªå®Œå…¨å…³é—­ã€‚ æŸäº›åº”用程åºçº¿ç¨‹å¯èƒ½ä»åœ¨è¿è¡Œã€‚ +endpoint.warn.incorrectConnectionCount=连接数ä¸æ­£ç¡®ï¼Œåœ¨åŒä¸€ä¸ªå¥—接字上调用多个socket.close。 +endpoint.warn.noLocalAddr=无法确定套接字 [{0}] çš„æœ¬åœ°åœ°å€ +endpoint.warn.noLocalName=无法确定 socket [{0}] 的本地主机å +endpoint.warn.noLocalPort=无法确定套接字 [{0}] çš„æœ¬åœ°ç«¯å£ +endpoint.warn.noRemoteAddr=无法确定套接字[{0}]çš„è¿œç¨‹åœ°å€ +endpoint.warn.noRemoteHost=无法确定套接字[{0}]的远程主机å +endpoint.warn.noRemotePort=无法确定 socket [{0}] çš„è¿œç¨‹ç«¯å£ +endpoint.warn.noUtilityExecutor=没有公共的executor 被设置时,创建一个. +endpoint.warn.unlockAcceptorFailed=接收器线程[{0}]解é”失败。强制硬套接字关闭。 + +sniExtractor.clientHelloInvalid=ClientHelloä¿¡æ¯æœªæ­£å¸¸æ ¼å¼åŒ– +sniExtractor.clientHelloTooBig=):ClientHello 没有出现在å•ä¸ªTLS记录中,因此无法æå–SNIä¿¡æ¯ +sniExtractor.tooEarly=在客户端问候被解æžä¹‹å‰è°ƒç”¨è¿™ä¸ªæ–¹æ³•æ˜¯éžæ³•çš„ + +socket.closed=与此连接关è”的套接字已关闭。 +socket.sslreneg=é‡æ–°å商SSL连接时出现异常 + +socketWrapper.readTimeout=读å–超时 +socketWrapper.writeTimeout=写入超时 + +sslHostConfig.certificate.notype=指定了多个è¯ä¹¦ï¼Œå¹¶ä¸”至少有一个è¯ä¹¦ç¼ºå°‘必需的属性类型 +sslHostConfig.certificateVerificationInvalid=è¯ä¹¦è®¤è¯å€¼[{0}]未识别 +sslHostConfig.certificateVerificationWithHttp2=TLS虚拟主机[{0}]被é…置为å¯é€‰çš„è¯ä¹¦éªŒè¯ï¼ŒåŒ…围的连接器被é…置为支æŒå‡çº§åˆ°h2。HTTP/2 over TLSä¸å…许å¯é€‰çš„è¯ä¹¦éªŒè¯ã€‚ +sslHostConfig.fileNotFound=é…置文件 [{0}] ä¸å­˜åœ¨ +sslHostConfig.invalid_truststore_password=æ供的信任存储密ç æ— æ³•ç”¨äºŽè§£é”å’Œ/或验è¯ä¿¡ä»»å­˜å‚¨ã€‚正在é‡è¯•ä½¿ç”¨ç©ºå¯†ç è®¿é—®ä¿¡ä»»å­˜å‚¨ï¼Œè¯¥å¯†ç å°†è·³è¿‡éªŒè¯ã€‚ +sslHostConfig.mismatch=属性[{0}]是在å为[{1}]çš„SSLHostConfig 上设置的,用于[{2}]é…置语法,但SSLHostConfig 正与[{3}]é…置语法一起使用 +sslHostConfig.opensslconf.alreadyset=å°è¯•è®¾ç½®çš„å¦ä¸€ä¸ª OpenSSLConf 被忽略\n +sslHostConfig.opensslconf.null=(:忽略设置空OpenSSLConf çš„å°è¯• +sslHostConfig.prefix_missing=åè®®[{0}]已添加到å为[{1}]çš„SSLHostConfig 上的å议列表中。检查是å¦ç¼ºå°‘一个+/-å‰ç¼€ã€‚ + +sslHostConfigCertificate.mismatch=属性[{0}]是在å为[{1}]çš„SSLHostConfigCertificate上设置的,用于è¯ä¹¦å­˜å‚¨ç±»åž‹[{2}],但该è¯ä¹¦æ­£ä¸Žç±»åž‹ä¸º[{3}]的存储一起使用。 + +sslImplementation.cnfe=无法为类 [{0}] 创建SSLImplementation + +sslUtilBase.active=活跃的[{0}]是:[{1}] +sslUtilBase.aliasIgnored=FIPSå·²å¯ç”¨æ‰€ä»¥åˆ«å[{0}]将被忽略。如果key存储中的key大于一个,使用的keyå°†å–决于key存储的实现 +sslUtilBase.alias_no_key_entry=别å[{0}]ä¸æ ‡è¯†å¯†é’¥é¡¹ +sslUtilBase.invalidTrustManagerClassName=æ供的trustManagerClassName[{0}]未实现javax.net.ssl.TrustManager +sslUtilBase.keystore_load_failed=由于[{2}],无法加载路径为[{1}]的密钥库类型[{0}] +sslUtilBase.noCertFile=在使用SSL连接器时,必须定义SSLHostConfigçš„certificateFile属性 +sslUtilBase.noCrlSupport=truststoreProvider [{0}]ä¸æ”¯æŒcertificateRevocationFileé…置选项 +sslUtilBase.noKeys=在密钥存储中找ä¸åˆ°ç§é’¥çš„别å +sslUtilBase.noVerificationDepth=truststoreProvider[{0}]ä¸æ”¯æŒCertificationDepthé…置选项 +sslUtilBase.noneSupported=SSL引擎ä¸æ”¯æŒæŒ‡å®šçš„[{0}]:[{1}] +sslUtilBase.ssl3=SSLv3 已显å¼å¯ç”¨ã€‚ 已知该å议是ä¸å®‰å…¨ã€‚ +sslUtilBase.tls13.auth=JSSE TLS 1.3实现ä¸æ”¯æŒåˆå§‹æ¡æ‰‹åŽçš„身份验è¯ï¼Œå› æ­¤ä¸Žå¯é€‰çš„客户端身份验è¯ä¸å…¼å®¹ +sslUtilBase.trustedCertNotChecked=未检查别å为[{0}]çš„å—ä¿¡ä»»è¯ä¹¦çš„有效日期,因为该è¯ä¹¦å±žäºŽæœªçŸ¥ç±»åž‹ +sslUtilBase.trustedCertNotValid=由于[{2}],别å为[{0}]且DN [{1}]çš„å¯ä¿¡è¯ä¹¦æ— æ•ˆã€‚ 将接å—由此å¯ä¿¡è¯ä¹¦ç­¾ç½²çš„è¯ä¹¦ diff --git a/java/org/apache/tomcat/util/net/Nio2Channel.java b/java/org/apache/tomcat/util/net/Nio2Channel.java new file mode 100644 index 0000000..be816eb --- /dev/null +++ b/java/org/apache/tomcat/util/net/Nio2Channel.java @@ -0,0 +1,308 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousByteChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Base class for a SocketChannel wrapper used by the endpoint. + * This way, logic for an SSL socket channel remains the same as for + * a non SSL, making sure we don't need to code for any exception cases. + */ +public class Nio2Channel implements AsynchronousByteChannel { + + protected static final ByteBuffer emptyBuf = ByteBuffer.allocate(0); + + protected final SocketBufferHandler bufHandler; + protected AsynchronousSocketChannel sc = null; + protected SocketWrapperBase socketWrapper = null; + + public Nio2Channel(SocketBufferHandler bufHandler) { + this.bufHandler = bufHandler; + } + + /** + * Reset the channel. + * + * @param channel The new async channel to associate with this NIO2 channel + * @param socketWrapper The new socket to associate with this NIO2 channel + * + * @throws IOException If a problem was encountered resetting the channel + */ + public void reset(AsynchronousSocketChannel channel, SocketWrapperBase socketWrapper) + throws IOException { + this.sc = channel; + this.socketWrapper = socketWrapper; + bufHandler.reset(); + } + + /** + * Free the channel memory + */ + public void free() { + bufHandler.free(); + } + + SocketWrapperBase getSocketWrapper() { + return socketWrapper; + } + + + /** + * Closes this channel. + * + * @throws IOException If an I/O error occurs + */ + @Override + public void close() throws IOException { + sc.close(); + } + + + /** + * Close the connection. + * + * @param force Should the underlying socket be forcibly closed? + * + * @throws IOException If closing the secure channel fails. + */ + public void close(boolean force) throws IOException { + if (isOpen() || force) { + close(); + } + } + + + /** + * Tells whether or not this channel is open. + * + * @return true if, and only if, this channel is open + */ + @Override + public boolean isOpen() { + return sc.isOpen(); + } + + public SocketBufferHandler getBufHandler() { + return bufHandler; + } + + public AsynchronousSocketChannel getIOChannel() { + return sc; + } + + public boolean isClosing() { + return false; + } + + public boolean isHandshakeComplete() { + return true; + } + + /** + * Performs SSL handshake hence is a no-op for the non-secure + * implementation. + * + * @return Always returns zero + * + * @throws IOException Never for non-secure channel + */ + public int handshake() throws IOException { + return 0; + } + + @Override + public String toString() { + return super.toString() + ":" + sc; + } + + @Override + public Future read(ByteBuffer dst) { + return sc.read(dst); + } + + @Override + public void read(ByteBuffer dst, A attachment, + CompletionHandler handler) { + read(dst, 0L, TimeUnit.MILLISECONDS, attachment, handler); + } + + public void read(ByteBuffer dst, + long timeout, TimeUnit unit, A attachment, + CompletionHandler handler) { + sc.read(dst, timeout, unit, attachment, handler); + } + + public void read(ByteBuffer[] dsts, + int offset, int length, long timeout, TimeUnit unit, + A attachment, CompletionHandler handler) { + sc.read(dsts, offset, length, timeout, unit, attachment, handler); + } + + @Override + public Future write(ByteBuffer src) { + return sc.write(src); + } + + @Override + public void write(ByteBuffer src, A attachment, + CompletionHandler handler) { + write(src, 0L, TimeUnit.MILLISECONDS, attachment, handler); + } + + public void write(ByteBuffer src, long timeout, TimeUnit unit, A attachment, + CompletionHandler handler) { + sc.write(src, timeout, unit, attachment, handler); + } + + public void write(ByteBuffer[] srcs, int offset, int length, + long timeout, TimeUnit unit, A attachment, + CompletionHandler handler) { + sc.write(srcs, offset, length, timeout, unit, attachment, handler); + } + + private static final Future DONE = new Future<>() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + @Override + public boolean isCancelled() { + return false; + } + @Override + public boolean isDone() { + return true; + } + @Override + public Boolean get() throws InterruptedException, + ExecutionException { + return Boolean.TRUE; + } + @Override + public Boolean get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + return Boolean.TRUE; + } + }; + + public Future flush() { + return DONE; + } + + private ApplicationBufferHandler appReadBufHandler; + public void setAppReadBufHandler(ApplicationBufferHandler handler) { + this.appReadBufHandler = handler; + } + protected ApplicationBufferHandler getAppReadBufHandler() { + return appReadBufHandler; + } + + private static final Future DONE_INT = new Future<>() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + @Override + public boolean isCancelled() { + return false; + } + @Override + public boolean isDone() { + return true; + } + @Override + public Integer get() throws InterruptedException, + ExecutionException { + return Integer.valueOf(-1); + } + @Override + public Integer get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + return Integer.valueOf(-1); + } + }; + + static final Nio2Channel CLOSED_NIO2_CHANNEL = new Nio2Channel(SocketBufferHandler.EMPTY) { + @Override + public void close() throws IOException { + } + @Override + public boolean isOpen() { + return false; + } + @Override + public void reset(AsynchronousSocketChannel channel, SocketWrapperBase socket) throws IOException { + } + @Override + public void free() { + } + @Override + protected ApplicationBufferHandler getAppReadBufHandler() { + return ApplicationBufferHandler.EMPTY; + } + @Override + public void setAppReadBufHandler(ApplicationBufferHandler handler) { + } + @Override + public Future read(ByteBuffer dst) { + return DONE_INT; + } + @Override + public void read(ByteBuffer dst, + long timeout, TimeUnit unit, A attachment, + CompletionHandler handler) { + handler.failed(new ClosedChannelException(), attachment); + } + @Override + public void read(ByteBuffer[] dsts, + int offset, int length, long timeout, TimeUnit unit, + A attachment, CompletionHandler handler) { + handler.failed(new ClosedChannelException(), attachment); + } + @Override + public Future write(ByteBuffer src) { + return DONE_INT; + } + @Override + public void write(ByteBuffer src, long timeout, TimeUnit unit, A attachment, + CompletionHandler handler) { + handler.failed(new ClosedChannelException(), attachment); + } + @Override + public void write(ByteBuffer[] srcs, int offset, int length, + long timeout, TimeUnit unit, A attachment, + CompletionHandler handler) { + handler.failed(new ClosedChannelException(), attachment); + } + @Override + public String toString() { + return "Closed Nio2Channel"; + } + }; +} diff --git a/java/org/apache/tomcat/util/net/Nio2Endpoint.java b/java/org/apache/tomcat/util/net/Nio2Endpoint.java new file mode 100644 index 0000000..5bbc3a1 --- /dev/null +++ b/java/org/apache/tomcat/util/net/Nio2Endpoint.java @@ -0,0 +1,1739 @@ +/* + * 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.tomcat.util.net; + +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannelGroup; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.AsynchronousServerSocketChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.nio.channels.FileChannel; +import java.nio.channels.NetworkChannel; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.net.ssl.SSLEngine; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.compat.JrePlatform; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.Acceptor.AcceptorState; +import org.apache.tomcat.util.net.jsse.JSSESupport; + +/** + * NIO2 endpoint. + */ +public class Nio2Endpoint extends AbstractJsseEndpoint { + + + // -------------------------------------------------------------- Constants + + + private static final Log log = LogFactory.getLog(Nio2Endpoint.class); + private static final Log logCertificate = LogFactory.getLog(Nio2Endpoint.class.getName() + ".certificate"); + private static final Log logHandshake = LogFactory.getLog(Nio2Endpoint.class.getName() + ".handshake"); + + + // ----------------------------------------------------------------- Fields + + /** + * Server socket "pointer". + */ + private volatile AsynchronousServerSocketChannel serverSock = null; + + /** + * Allows detecting if a completion handler completes inline. + */ + private static ThreadLocal inlineCompletion = new ThreadLocal<>(); + + /** + * Thread group associated with the server socket. + */ + private AsynchronousChannelGroup threadGroup = null; + + private volatile boolean allClosed; + + /** + * Bytebuffer cache, each channel holds a set of buffers (two, except for SSL holds four) + */ + private SynchronizedStack nioChannels; + + private SocketAddress previousAcceptedSocketRemoteAddress = null; + private long previousAcceptedSocketNanoTime = 0; + + + // --------------------------------------------------------- Public Methods + + /** + * Number of keep-alive sockets. + * + * @return Always returns -1. + */ + public int getKeepAliveCount() { + // For this connector, only the overall connection count is relevant + return -1; + } + + + // ----------------------------------------------- Public Lifecycle Methods + + + /** + * Initialize the endpoint. + */ + @Override + public void bind() throws Exception { + + // Create worker collection + if (getExecutor() == null) { + createExecutor(); + } + if (getExecutor() instanceof ExecutorService) { + threadGroup = AsynchronousChannelGroup.withThreadPool((ExecutorService) getExecutor()); + } else { + log.info(sm.getString("endpoint.nio2.executorService")); + } + // AsynchronousChannelGroup needs exclusive access to its executor service + if (!internalExecutor) { + log.warn(sm.getString("endpoint.nio2.exclusiveExecutor")); + } + + serverSock = AsynchronousServerSocketChannel.open(threadGroup); + socketProperties.setProperties(serverSock); + InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset()); + serverSock.bind(addr, getAcceptCount()); + + // Initialize SSL if needed + initialiseSsl(); + } + + + /** + * Start the NIO2 endpoint, creating acceptor. + */ + @Override + public void startInternal() throws Exception { + + if (!running) { + allClosed = false; + running = true; + paused = false; + + if (socketProperties.getProcessorCache() != 0) { + processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, + socketProperties.getProcessorCache()); + } + int actualBufferPool = + socketProperties.getActualBufferPool(isSSLEnabled() ? getSniParseLimit() * 2 : 0); + if (actualBufferPool != 0) { + nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, + actualBufferPool); + } + // Create worker collection + if (getExecutor() == null) { + createExecutor(); + } + + initializeConnectionLatch(); + startAcceptorThread(); + } + } + + @Override + protected void startAcceptorThread() { + // Instead of starting a real acceptor thread, this will instead call + // an asynchronous accept operation + if (acceptor == null) { + acceptor = new Nio2Acceptor(this); + acceptor.setThreadName(getName() + "-Acceptor"); + } + acceptor.state = AcceptorState.RUNNING; + getExecutor().execute(acceptor); + } + + @Override + public void resume() { + super.resume(); + if (isRunning()) { + acceptor.state = AcceptorState.RUNNING; + getExecutor().execute(acceptor); + } + } + + /** + * Stop the endpoint. This will cause all processing threads to stop. + */ + @Override + public void stopInternal() { + if (!paused) { + pause(); + } + if (running) { + running = false; + acceptor.stop(10); + // Use the executor to avoid binding the main thread if something bad + // occurs and unbind will also wait for a bit for it to complete + getExecutor().execute(() -> { + // Then close all active connections if any remain + try { + for (SocketWrapperBase wrapper : getConnections()) { + wrapper.close(); + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + } finally { + allClosed = true; + } + }); + if (nioChannels != null) { + Nio2Channel socket; + while ((socket = nioChannels.pop()) != null) { + socket.free(); + } + nioChannels = null; + } + if (processorCache != null) { + processorCache.clear(); + processorCache = null; + } + } + } + + + /** + * Deallocate NIO memory pools, and close server socket. + */ + @Override + public void unbind() throws Exception { + if (running) { + stop(); + } + doCloseServerSocket(); + destroySsl(); + super.unbind(); + // Unlike other connectors, the thread pool is tied to the server socket + shutdownExecutor(); + if (getHandler() != null) { + getHandler().recycle(); + } + } + + + @Override + protected void doCloseServerSocket() throws IOException { + // Close server socket + if (serverSock != null) { + serverSock.close(); + serverSock = null; + } + } + + + @Override + public void shutdownExecutor() { + if (threadGroup != null && internalExecutor) { + try { + long timeout = getExecutorTerminationTimeoutMillis(); + while (timeout > 0 && !allClosed) { + timeout -= 1; + Thread.sleep(1); + } + threadGroup.shutdownNow(); + if (timeout > 0) { + threadGroup.awaitTermination(timeout, TimeUnit.MILLISECONDS); + } + } catch (IOException e) { + getLog().warn(sm.getString("endpoint.warn.executorShutdown", getName()), e); + } catch (InterruptedException e) { + // Ignore + } + if (!threadGroup.isTerminated()) { + getLog().warn(sm.getString("endpoint.warn.executorShutdown", getName())); + } + threadGroup = null; + } + // Mostly to cleanup references + super.shutdownExecutor(); + } + + + // ------------------------------------------------------ Protected Methods + + /** + * Process the specified connection. + * @param socket The socket channel + * @return true if the socket was correctly configured + * and processing may continue, false if the socket needs to be + * close immediately + */ + @Override + protected boolean setSocketOptions(AsynchronousSocketChannel socket) { + Nio2SocketWrapper socketWrapper = null; + try { + // Allocate channel and wrapper + Nio2Channel channel = null; + if (nioChannels != null) { + channel = nioChannels.pop(); + } + if (channel == null) { + SocketBufferHandler bufhandler = new SocketBufferHandler( + socketProperties.getAppReadBufSize(), + socketProperties.getAppWriteBufSize(), + socketProperties.getDirectBuffer()); + if (isSSLEnabled()) { + channel = new SecureNio2Channel(bufhandler, this); + } else { + channel = new Nio2Channel(bufhandler); + } + } + Nio2SocketWrapper newWrapper = new Nio2SocketWrapper(channel, this); + channel.reset(socket, newWrapper); + connections.put(socket, newWrapper); + socketWrapper = newWrapper; + + // Set socket properties + socketProperties.setProperties(socket); + + socketWrapper.setReadTimeout(getConnectionTimeout()); + socketWrapper.setWriteTimeout(getConnectionTimeout()); + socketWrapper.setKeepAliveLeft(Nio2Endpoint.this.getMaxKeepAliveRequests()); + // Continue processing on the same thread as the acceptor is async + return processSocket(socketWrapper, SocketEvent.OPEN_READ, false); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("endpoint.socketOptionsError"), t); + if (socketWrapper == null) { + destroySocket(socket); + } + } + // Tell to close the socket if needed + return false; + } + + + @Override + protected void destroySocket(AsynchronousSocketChannel socket) { + countDownConnection(); + try { + socket.close(); + } catch (IOException ioe) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("endpoint.err.close"), ioe); + } + } + } + + + protected SynchronizedStack getNioChannels() { + return nioChannels; + } + + + @Override + protected NetworkChannel getServerSocket() { + return serverSock; + } + + + @Override + protected AsynchronousSocketChannel serverSocketAccept() throws Exception { + AsynchronousSocketChannel result = serverSock.accept().get(); + + // Bug does not affect Windows. Skip the check on that platform. + if (!JrePlatform.IS_WINDOWS) { + SocketAddress currentRemoteAddress = result.getRemoteAddress(); + long currentNanoTime = System.nanoTime(); + if (currentRemoteAddress.equals(previousAcceptedSocketRemoteAddress) && + currentNanoTime - previousAcceptedSocketNanoTime < 1000) { + throw new IOException(sm.getString("endpoint.err.duplicateAccept")); + } + previousAcceptedSocketRemoteAddress = currentRemoteAddress; + previousAcceptedSocketNanoTime = currentNanoTime; + } + + return result; + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + protected Log getLogCertificate() { + return logCertificate; + } + + + @Override + protected SocketProcessorBase createSocketProcessor( + SocketWrapperBase socketWrapper, SocketEvent event) { + return new SocketProcessor(socketWrapper, event); + } + + + protected class Nio2Acceptor extends Acceptor + implements CompletionHandler { + + protected int errorDelay = 0; + + public Nio2Acceptor(AbstractEndpoint endpoint) { + super(endpoint); + } + + @Override + public void run() { + // The initial accept will be called in a separate utility thread + if (!isPaused()) { + //if we have reached max connections, wait + try { + countUpOrAwaitConnection(); + } catch (InterruptedException e) { + // Ignore + } + if (!isPaused()) { + // Note: as a special behavior, the completion handler for accept is + // always called in a separate thread. + serverSock.accept(null, this); + } else { + state = AcceptorState.PAUSED; + } + } else { + state = AcceptorState.PAUSED; + } + } + + /** + * Signals the Acceptor to stop. + * + * @param waitSeconds Ignored for NIO2. + * + */ + @Override + public void stop(int waitSeconds) { + acceptor.state = AcceptorState.ENDED; + } + + @Override + public void completed(AsynchronousSocketChannel socket, + Void attachment) { + // Successful accept, reset the error delay + errorDelay = 0; + // Continue processing the socket on the current thread + // Configure the socket + if (isRunning() && !isPaused()) { + if (getMaxConnections() == -1) { + serverSock.accept(null, this); + } else if (getConnectionCount() < getMaxConnections()) { + try { + // This will not block + countUpOrAwaitConnection(); + } catch (InterruptedException e) { + // Ignore + } + serverSock.accept(null, this); + } else { + // Accept again on a new thread since countUpOrAwaitConnection may block + getExecutor().execute(this); + } + if (!setSocketOptions(socket)) { + closeSocket(socket); + } + } else { + if (isRunning()) { + state = AcceptorState.PAUSED; + } + destroySocket(socket); + } + } + + @Override + public void failed(Throwable t, Void attachment) { + if (isRunning()) { + if (!isPaused()) { + if (getMaxConnections() == -1) { + serverSock.accept(null, this); + } else { + // Accept again on a new thread since countUpOrAwaitConnection may block + getExecutor().execute(this); + } + } else { + state = AcceptorState.PAUSED; + } + // We didn't get a socket + countDownConnection(); + // Introduce delay if necessary + errorDelay = handleExceptionWithDelay(errorDelay); + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("endpoint.accept.fail"), t); + } else { + // We didn't get a socket + countDownConnection(); + } + } + + } + + public static class Nio2SocketWrapper extends SocketWrapperBase { + + private final SynchronizedStack nioChannels; + + private SendfileData sendfileData = null; + + private final CompletionHandler readCompletionHandler; + private boolean readInterest = false; // Guarded by readCompletionHandler + private boolean readNotify = false; + + private final CompletionHandler writeCompletionHandler; + private final CompletionHandler gatheringWriteCompletionHandler; + private boolean writeInterest = false; // Guarded by writeCompletionHandler + private boolean writeNotify = false; + + private CompletionHandler sendfileHandler + = new CompletionHandler<>() { + + @Override + public void completed(Integer nWrite, SendfileData attachment) { + if (nWrite.intValue() < 0) { + failed(new EOFException(), attachment); + return; + } + attachment.pos += nWrite.intValue(); + ByteBuffer buffer = getSocket().getBufHandler().getWriteBuffer(); + if (!buffer.hasRemaining()) { + if (attachment.length <= 0) { + // All data has now been written + setSendfileData(null); + try { + attachment.fchannel.close(); + } catch (IOException e) { + // Ignore + } + if (isInline()) { + attachment.doneInline = true; + } else { + switch (attachment.keepAliveState) { + case NONE: { + getEndpoint().processSocket(Nio2SocketWrapper.this, + SocketEvent.DISCONNECT, false); + break; + } + case PIPELINED: { + if (!getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.OPEN_READ, true)) { + close(); + } + break; + } + case OPEN: { + registerReadInterest(); + break; + } + } + } + return; + } else { + getSocket().getBufHandler().configureWriteBufferForWrite(); + int nRead = -1; + try { + nRead = attachment.fchannel.read(buffer); + } catch (IOException e) { + failed(e, attachment); + return; + } + if (nRead > 0) { + getSocket().getBufHandler().configureWriteBufferForRead(); + if (attachment.length < buffer.remaining()) { + buffer.limit(buffer.limit() - buffer.remaining() + (int) attachment.length); + } + attachment.length -= nRead; + } else { + failed(new EOFException(), attachment); + return; + } + } + } + getSocket().write(buffer, toTimeout(getWriteTimeout()), TimeUnit.MILLISECONDS, attachment, this); + } + + @Override + public void failed(Throwable exc, SendfileData attachment) { + try { + attachment.fchannel.close(); + } catch (IOException e) { + // Ignore + } + if (!isInline()) { + getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.ERROR, false); + } else { + attachment.doneInline = true; + attachment.error = true; + } + } + }; + + public Nio2SocketWrapper(Nio2Channel channel, final Nio2Endpoint endpoint) { + super(channel, endpoint); + nioChannels = endpoint.getNioChannels(); + socketBufferHandler = channel.getBufHandler(); + + this.readCompletionHandler = new CompletionHandler<>() { + @Override + public void completed(Integer nBytes, ByteBuffer attachment) { + if (log.isTraceEnabled()) { + log.trace("Socket: [" + Nio2SocketWrapper.this + "], Interest: [" + readInterest + "]"); + } + boolean notify = false; + synchronized (readCompletionHandler) { + readNotify = false; + if (nBytes.intValue() < 0) { + failed(new EOFException(), attachment); + } else { + if (readInterest && !isInline()) { + readNotify = true; + } else { + // Release here since there will be no + // notify/dispatch to do the release. + readPending.release(); + } + readInterest = false; + } + notify = readNotify; + } + if (notify) { + getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.OPEN_READ, false); + } + } + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + IOException ioe; + if (exc instanceof IOException) { + ioe = (IOException) exc; + } else { + ioe = new IOException(exc); + } + setError(ioe); + if (exc instanceof AsynchronousCloseException) { + // Release here since there will be no + // notify/dispatch to do the release. + readPending.release(); + // If already closed, don't call onError and close again + getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.STOP, false); + } else if (!getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.ERROR, true)) { + close(); + } + } + }; + + this.writeCompletionHandler = new CompletionHandler<>() { + @Override + public void completed(Integer nBytes, ByteBuffer attachment) { + boolean notify = false; + synchronized (writeCompletionHandler) { + writeNotify = false; + if (nBytes.intValue() < 0) { + failed(new EOFException(sm.getString("iob.failedwrite")), attachment); + } else if (!nonBlockingWriteBuffer.isEmpty()) { + // Continue writing data using a gathering write + ByteBuffer[] array = nonBlockingWriteBuffer.toArray(attachment); + getSocket().write(array, 0, array.length, + toTimeout(getWriteTimeout()), TimeUnit.MILLISECONDS, + array, gatheringWriteCompletionHandler); + } else if (attachment.hasRemaining()) { + // Regular write + getSocket().write(attachment, toTimeout(getWriteTimeout()), + TimeUnit.MILLISECONDS, attachment, writeCompletionHandler); + } else { + // All data has been written + if (writeInterest && !isInline()) { + writeNotify = true; + // Set extra flag so that write nesting does not cause multiple notifications + notify = true; + } else { + // Release here since there will be no + // notify/dispatch to do the release. + writePending.release(); + } + writeInterest = false; + } + } + if (notify) { + if (!endpoint.processSocket(Nio2SocketWrapper.this, SocketEvent.OPEN_WRITE, true)) { + close(); + } + } + } + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + IOException ioe; + if (exc instanceof IOException) { + ioe = (IOException) exc; + } else { + ioe = new IOException(exc); + } + setError(ioe); + writePending.release(); + if (!endpoint.processSocket(Nio2SocketWrapper.this, SocketEvent.ERROR, true)) { + close(); + } + } + }; + + gatheringWriteCompletionHandler = new CompletionHandler<>() { + @Override + public void completed(Long nBytes, ByteBuffer[] attachment) { + boolean notify = false; + synchronized (writeCompletionHandler) { + writeNotify = false; + if (nBytes.longValue() < 0) { + failed(new EOFException(sm.getString("iob.failedwrite")), attachment); + } else if (!nonBlockingWriteBuffer.isEmpty() || buffersArrayHasRemaining(attachment, 0, attachment.length)) { + // Continue writing data using a gathering write + ByteBuffer[] array = nonBlockingWriteBuffer.toArray(attachment); + getSocket().write(array, 0, array.length, + toTimeout(getWriteTimeout()), TimeUnit.MILLISECONDS, + array, gatheringWriteCompletionHandler); + } else { + // All data has been written + if (writeInterest && !isInline()) { + writeNotify = true; + // Set extra flag so that write nesting does not cause multiple notifications + notify = true; + } else { + // Release here since there will be no + // notify/dispatch to do the release. + writePending.release(); + } + writeInterest = false; + } + } + if (notify) { + if (!endpoint.processSocket(Nio2SocketWrapper.this, SocketEvent.OPEN_WRITE, true)) { + close(); + } + } + } + @Override + public void failed(Throwable exc, ByteBuffer[] attachment) { + IOException ioe; + if (exc instanceof IOException) { + ioe = (IOException) exc; + } else { + ioe = new IOException(exc); + } + setError(ioe); + writePending.release(); + if (!endpoint.processSocket(Nio2SocketWrapper.this, SocketEvent.ERROR, true)) { + close(); + } + } + }; + + } + + + public void setSendfileData(SendfileData sf) { this.sendfileData = sf; } + public SendfileData getSendfileData() { return this.sendfileData; } + + @Override + public boolean isReadyForRead() throws IOException { + synchronized (readCompletionHandler) { + // A notification has been sent, it is possible to read at least once + if (readNotify) { + return true; + } + // If a read is pending, reading is not possible until a notification is sent + if (!readPending.tryAcquire()) { + readInterest = true; + return false; + } + // It is possible to read directly from the buffer contents + if (!socketBufferHandler.isReadBufferEmpty()) { + readPending.release(); + return true; + } + // Try to read some data + boolean isReady = fillReadBuffer(false) > 0; + if (!isReady) { + readInterest = true; + } + return isReady; + } + } + + + @Override + public boolean isReadyForWrite() { + synchronized (writeCompletionHandler) { + // A notification has been sent, it is possible to write at least once + if (writeNotify) { + return true; + } + // If a write is pending, writing is not possible until a notification is sent + if (!writePending.tryAcquire()) { + writeInterest = true; + return false; + } + // If the buffer is empty, it is possible to write to it + if (socketBufferHandler.isWriteBufferEmpty() && nonBlockingWriteBuffer.isEmpty()) { + writePending.release(); + return true; + } + // Try to flush all data + boolean isReady = !flushNonBlockingInternal(true); + if (!isReady) { + writeInterest = true; + } + return isReady; + } + } + + + @Override + public int read(boolean block, byte[] b, int off, int len) throws IOException { + checkError(); + + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], block: [" + block + "], length: [" + len + "]"); + } + + if (socketBufferHandler == null) { + throw new IOException(sm.getString("socket.closed")); + } + + boolean notify = false; + synchronized (readCompletionHandler) { + notify = readNotify; + } + + if (!notify) { + if (block) { + try { + readPending.acquire(); + } catch (InterruptedException e) { + throw new IOException(e); + } + } else { + if (!readPending.tryAcquire()) { + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read in progress. Returning [0]"); + } + return 0; + } + } + } + + int nRead = populateReadBuffer(b, off, len); + if (nRead > 0) { + // The code that was notified is now reading its data + synchronized (readCompletionHandler) { + readNotify = false; + } + // This may be sufficient to complete the request and we + // don't want to trigger another read since if there is no + // more data to read and this request takes a while to + // process the read will timeout triggering an error. + readPending.release(); + return nRead; + } + + synchronized (readCompletionHandler) { + // Fill the read buffer as best we can. + nRead = fillReadBuffer(block); + // Fill as much of the remaining byte array as possible with the + // data that was just read + if (nRead > 0) { + socketBufferHandler.configureReadBufferForRead(); + nRead = Math.min(nRead, len); + socketBufferHandler.getReadBuffer().get(b, off, nRead); + } else if (nRead == 0 && !block) { + readInterest = true; + } + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read: [" + nRead + "]"); + } + return nRead; + } + } + + + @Override + public int read(boolean block, ByteBuffer to) throws IOException { + checkError(); + + if (socketBufferHandler == null) { + throw new IOException(sm.getString("socket.closed")); + } + + boolean notify = false; + synchronized (readCompletionHandler) { + notify = readNotify; + } + + if (!notify) { + if (block) { + try { + readPending.acquire(); + } catch (InterruptedException e) { + throw new IOException(e); + } + } else { + if (!readPending.tryAcquire()) { + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read in progress. Returning [0]"); + } + return 0; + } + } + } + + int nRead = populateReadBuffer(to); + if (nRead > 0) { + // The code that was notified is now reading its data + synchronized (readCompletionHandler) { + readNotify = false; + } + // This may be sufficient to complete the request and we + // don't want to trigger another read since if there is no + // more data to read and this request takes a while to + // process the read will timeout triggering an error. + readPending.release(); + return nRead; + } + + synchronized (readCompletionHandler) { + // The socket read buffer capacity is socket.appReadBufSize + int limit = socketBufferHandler.getReadBuffer().capacity(); + if (block && to.remaining() >= limit) { + to.limit(to.position() + limit); + nRead = fillReadBuffer(block, to); + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read direct from socket: [" + nRead + "]"); + } + } else { + // Fill the read buffer as best we can. + nRead = fillReadBuffer(block); + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read into buffer: [" + nRead + "]"); + } + // Fill as much of the remaining byte array as possible with the + // data that was just read + if (nRead > 0) { + nRead = populateReadBuffer(to); + } else if (nRead == 0 && !block) { + readInterest = true; + } + } + return nRead; + } + } + + + @Override + protected void doClose() { + if (log.isTraceEnabled()) { + log.trace("Calling [" + getEndpoint() + "].closeSocket([" + this + "])"); + } + try { + getEndpoint().connections.remove(getSocket().getIOChannel()); + if (getSocket().isOpen()) { + getSocket().close(true); + } + if (getEndpoint().running) { + if (nioChannels == null || !nioChannels.push(getSocket())) { + getSocket().free(); + } + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + if (log.isDebugEnabled()) { + log.error(sm.getString("endpoint.debug.channelCloseFail"), e); + } + } finally { + socketBufferHandler = SocketBufferHandler.EMPTY; + nonBlockingWriteBuffer.clear(); + reset(Nio2Channel.CLOSED_NIO2_CHANNEL); + } + try { + SendfileData data = getSendfileData(); + if (data != null && data.fchannel != null && data.fchannel.isOpen()) { + data.fchannel.close(); + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + if (log.isDebugEnabled()) { + log.error(sm.getString("endpoint.sendfile.closeError"), e); + } + } + } + + @Override + public boolean hasAsyncIO() { + return getEndpoint().getUseAsyncIO(); + } + + @Override + public boolean needSemaphores() { + return true; + } + + @Override + public boolean hasPerOperationTimeout() { + return true; + } + + @Override + protected OperationState newOperationState(boolean read, + ByteBuffer[] buffers, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, + CompletionCheck check, CompletionHandler handler, + Semaphore semaphore, VectoredIOCompletionHandler completion) { + return new Nio2OperationState<>(read, buffers, offset, length, block, + timeout, unit, attachment, check, handler, semaphore, completion); + } + + private class Nio2OperationState extends OperationState { + private Nio2OperationState(boolean read, ByteBuffer[] buffers, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, + CompletionCheck check, CompletionHandler handler, + Semaphore semaphore, VectoredIOCompletionHandler completion) { + super(read, buffers, offset, length, block, + timeout, unit, attachment, check, handler, semaphore, completion); + } + + @Override + protected boolean isInline() { + return Nio2Endpoint.isInline(); + } + + @Override + protected void start() { + if (read) { + // Disable any regular read notifications caused by registerReadInterest + synchronized (readCompletionHandler) { + readNotify = true; + } + } else { + // Disable any regular write notifications caused by registerWriteInterest + synchronized (writeCompletionHandler) { + writeNotify = true; + } + } + startInline(); + try { + run(); + } finally { + endInline(); + } + } + + @Override + public void run() { + if (read) { + long nBytes = 0; + // If there is still data inside the main read buffer, it needs to be read first + if (!socketBufferHandler.isReadBufferEmpty()) { + synchronized (readCompletionHandler) { + socketBufferHandler.configureReadBufferForRead(); + for (int i = 0; i < length && !socketBufferHandler.isReadBufferEmpty(); i++) { + nBytes += transfer(socketBufferHandler.getReadBuffer(), buffers[offset + i]); + } + } + if (nBytes > 0) { + completion.completed(Long.valueOf(nBytes), this); + } + } + if (nBytes == 0) { + getSocket().read(buffers, offset, length, timeout, unit, this, completion); + } + } else { + // If there is still data inside the main write buffer, it needs to be written first + if (!socketBufferHandler.isWriteBufferEmpty()) { + synchronized (writeCompletionHandler) { + socketBufferHandler.configureWriteBufferForRead(); + ByteBuffer[] array = nonBlockingWriteBuffer.toArray(socketBufferHandler.getWriteBuffer()); + if (buffersArrayHasRemaining(array, 0, array.length)) { + getSocket().write(array, 0, array.length, timeout, unit, + array, new CompletionHandler() { + @Override + public void completed(Long nBytes, ByteBuffer[] buffers) { + if (nBytes.longValue() < 0) { + failed(new EOFException(), null); + } else if (buffersArrayHasRemaining(buffers, 0, buffers.length)) { + getSocket().write(buffers, 0, buffers.length, toTimeout(getWriteTimeout()), + TimeUnit.MILLISECONDS, buffers, this); + } else { + // Continue until everything is written + process(); + } + } + @Override + public void failed(Throwable exc, ByteBuffer[] buffers) { + completion.failed(exc, Nio2OperationState.this); + } + }); + return; + } + } + } + getSocket().write(buffers, offset, length, timeout, unit, this, completion); + } + } + } + + /* Callers of this method must: + * - have acquired the readPending semaphore + * - have acquired a lock on readCompletionHandler + * + * This method will release (or arrange for the release of) the + * readPending semaphore once the read has completed. + */ + private int fillReadBuffer(boolean block) throws IOException { + socketBufferHandler.configureReadBufferForWrite(); + return fillReadBuffer(block, socketBufferHandler.getReadBuffer()); + } + + private int fillReadBuffer(boolean block, ByteBuffer to) throws IOException { + int nRead = 0; + Future integer = null; + if (block) { + try { + integer = getSocket().read(to); + long timeout = getReadTimeout(); + if (timeout > 0) { + nRead = integer.get(timeout, TimeUnit.MILLISECONDS).intValue(); + } else { + nRead = integer.get().intValue(); + } + } catch (ExecutionException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else { + throw new IOException(e); + } + } catch (InterruptedException e) { + throw new IOException(e); + } catch (TimeoutException e) { + integer.cancel(true); + throw new SocketTimeoutException(); + } finally { + // Blocking read so need to release here since there will + // not be a callback to a completion handler. + readPending.release(); + } + } else { + startInline(); + getSocket().read(to, toTimeout(getReadTimeout()), TimeUnit.MILLISECONDS, to, + readCompletionHandler); + endInline(); + if (readPending.availablePermits() == 1) { + nRead = to.position(); + } + } + return nRead; + } + + + /** + * {@inheritDoc} + *

    + * Overridden for NIO2 to enable a gathering write to be used to write + * all of the remaining data in a single additional write should a + * non-blocking write leave data in the buffer. + */ + @Override + protected void writeNonBlocking(byte[] buf, int off, int len) throws IOException { + // Note: Possible alternate behavior: + // If there's non blocking abuse (like a test writing 1MB in a single + // "non blocking" write), then block until the previous write is + // done rather than continue buffering + // Also allows doing autoblocking + // Could be "smart" with coordination with the main CoyoteOutputStream to + // indicate the end of a write + // Uses: if (writePending.tryAcquire(socketWrapper.getTimeout(), TimeUnit.MILLISECONDS)) + synchronized (writeCompletionHandler) { + checkError(); + if (writeNotify || writePending.tryAcquire()) { + // No pending completion handler, so writing to the main buffer + // is possible + socketBufferHandler.configureWriteBufferForWrite(); + int thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer()); + len = len - thisTime; + off = off + thisTime; + if (len > 0) { + // Remaining data must be buffered + nonBlockingWriteBuffer.add(buf, off, len); + } + flushNonBlockingInternal(true); + } else { + nonBlockingWriteBuffer.add(buf, off, len); + } + } + } + + + /** + * {@inheritDoc} + *

    + * Overridden for NIO2 to enable a gathering write to be used to write + * all of the remaining data in a single additional write should a + * non-blocking write leave data in the buffer. + */ + @Override + protected void writeNonBlocking(ByteBuffer from) throws IOException { + writeNonBlockingInternal(from); + } + + + /** + * {@inheritDoc} + *

    + * Overridden for NIO2 to enable a gathering write to be used to write + * all of the remaining data in a single additional write should a + * non-blocking write leave data in the buffer. + */ + @Override + protected void writeNonBlockingInternal(ByteBuffer from) throws IOException { + // Note: Possible alternate behavior: + // If there's non blocking abuse (like a test writing 1MB in a single + // "non blocking" write), then block until the previous write is + // done rather than continue buffering + // Also allows doing autoblocking + // Could be "smart" with coordination with the main CoyoteOutputStream to + // indicate the end of a write + // Uses: if (writePending.tryAcquire(socketWrapper.getTimeout(), TimeUnit.MILLISECONDS)) + synchronized (writeCompletionHandler) { + checkError(); + if (writeNotify || writePending.tryAcquire()) { + // No pending completion handler, so writing to the main buffer + // is possible + socketBufferHandler.configureWriteBufferForWrite(); + transfer(from, socketBufferHandler.getWriteBuffer()); + if (from.remaining() > 0) { + // Remaining data must be buffered + nonBlockingWriteBuffer.add(from); + } + flushNonBlockingInternal(true); + } else { + nonBlockingWriteBuffer.add(from); + } + } + } + + + /** + * @param block Ignored since this method is only called in the + * blocking case + */ + @Override + protected void doWrite(boolean block, ByteBuffer from) throws IOException { + Future integer = null; + try { + do { + integer = getSocket().write(from); + long timeout = getWriteTimeout(); + if (timeout > 0) { + if (integer.get(timeout, TimeUnit.MILLISECONDS).intValue() < 0) { + throw new EOFException(sm.getString("iob.failedwrite")); + } + } else { + if (integer.get().intValue() < 0) { + throw new EOFException(sm.getString("iob.failedwrite")); + } + } + } while (from.hasRemaining()); + } catch (ExecutionException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else { + throw new IOException(e); + } + } catch (InterruptedException e) { + throw new IOException(e); + } catch (TimeoutException e) { + integer.cancel(true); + throw new SocketTimeoutException(); + } + } + + + @Override + protected void flushBlocking() throws IOException { + checkError(); + + // Before doing a blocking flush, make sure that any pending non + // blocking write has completed. + try { + if (writePending.tryAcquire(toTimeout(getWriteTimeout()), TimeUnit.MILLISECONDS)) { + writePending.release(); + } else { + throw new SocketTimeoutException(); + } + } catch (InterruptedException e) { + // Ignore + } + + super.flushBlocking(); + } + + @Override + protected boolean flushNonBlocking() throws IOException { + checkError(); + return flushNonBlockingInternal(false); + } + + private boolean flushNonBlockingInternal(boolean hasPermit) { + synchronized (writeCompletionHandler) { + if (writeNotify || hasPermit || writePending.tryAcquire()) { + // The code that was notified is now writing its data + writeNotify = false; + socketBufferHandler.configureWriteBufferForRead(); + if (!nonBlockingWriteBuffer.isEmpty()) { + ByteBuffer[] array = nonBlockingWriteBuffer.toArray(socketBufferHandler.getWriteBuffer()); + startInline(); + getSocket().write(array, 0, array.length, toTimeout(getWriteTimeout()), + TimeUnit.MILLISECONDS, array, gatheringWriteCompletionHandler); + endInline(); + } else if (socketBufferHandler.getWriteBuffer().hasRemaining()) { + // Regular write + startInline(); + getSocket().write(socketBufferHandler.getWriteBuffer(), toTimeout(getWriteTimeout()), + TimeUnit.MILLISECONDS, socketBufferHandler.getWriteBuffer(), + writeCompletionHandler); + endInline(); + } else { + // Nothing was written + if (!hasPermit) { + writePending.release(); + } + writeInterest = false; + } + } + return hasDataToWrite(); + } + } + + + @Override + public boolean hasDataToRead() { + synchronized (readCompletionHandler) { + return !socketBufferHandler.isReadBufferEmpty() + || readNotify || getError() != null; + } + } + + + @Override + public boolean hasDataToWrite() { + synchronized (writeCompletionHandler) { + return !socketBufferHandler.isWriteBufferEmpty() || !nonBlockingWriteBuffer.isEmpty() + || writeNotify || writePending.availablePermits() == 0 || getError() != null; + } + } + + + @Override + public boolean isReadPending() { + synchronized (readCompletionHandler) { + return readPending.availablePermits() == 0; + } + } + + + @Override + public boolean isWritePending() { + synchronized (writeCompletionHandler) { + return writePending.availablePermits() == 0; + } + } + + + @Override + public void registerReadInterest() { + synchronized (readCompletionHandler) { + // A notification is already being sent + if (readNotify) { + return; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("endpoint.debug.registerRead", this)); + } + readInterest = true; + if (readPending.tryAcquire()) { + // No read pending, so do a read + try { + if (fillReadBuffer(false) > 0) { + // Special case where the read completed inline, there is no notification + // in that case so it has to be done here + if (!getEndpoint().processSocket(this, SocketEvent.OPEN_READ, true)) { + close(); + } + } + } catch (IOException e) { + // Will never happen + setError(e); + } + } + } + } + + + @Override + public void registerWriteInterest() { + synchronized (writeCompletionHandler) { + // A notification is already being sent + if (writeNotify) { + return; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("endpoint.debug.registerWrite", this)); + } + writeInterest = true; + if (writePending.availablePermits() == 1) { + // If no write is pending, notify that writing is possible + if (!getEndpoint().processSocket(this, SocketEvent.OPEN_WRITE, true)) { + close(); + } + } + } + } + + + @Override + public SendfileDataBase createSendfileData(String filename, long pos, long length) { + return new SendfileData(filename, pos, length); + } + + + @Override + public SendfileState processSendfile(SendfileDataBase sendfileData) { + SendfileData data = (SendfileData) sendfileData; + setSendfileData(data); + // Configure the send file data + if (data.fchannel == null || !data.fchannel.isOpen()) { + java.nio.file.Path path = new File(sendfileData.fileName).toPath(); + try { + data.fchannel = FileChannel.open(path, StandardOpenOption.READ).position(sendfileData.pos); + } catch (IOException e) { + return SendfileState.ERROR; + } + } + getSocket().getBufHandler().configureWriteBufferForWrite(); + ByteBuffer buffer = getSocket().getBufHandler().getWriteBuffer(); + int nRead = -1; + try { + nRead = data.fchannel.read(buffer); + } catch (IOException e1) { + return SendfileState.ERROR; + } + + if (nRead >= 0) { + data.length -= nRead; + getSocket().getBufHandler().configureWriteBufferForRead(); + startInline(); + getSocket().write(buffer, toTimeout(getWriteTimeout()), TimeUnit.MILLISECONDS, + data, sendfileHandler); + endInline(); + if (data.doneInline) { + if (data.error) { + return SendfileState.ERROR; + } else { + return SendfileState.DONE; + } + } else { + return SendfileState.PENDING; + } + } else { + return SendfileState.ERROR; + } + } + + + @Override + protected void populateRemoteAddr() { + AsynchronousSocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + SocketAddress socketAddress = null; + try { + socketAddress = sc.getRemoteAddress(); + } catch (IOException e) { + // Ignore + } + if (socketAddress instanceof InetSocketAddress) { + remoteAddr = ((InetSocketAddress) socketAddress).getAddress().getHostAddress(); + } + } + } + + + @Override + protected void populateRemoteHost() { + AsynchronousSocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + SocketAddress socketAddress = null; + try { + socketAddress = sc.getRemoteAddress(); + } catch (IOException e) { + log.warn(sm.getString("endpoint.warn.noRemoteHost", getSocket()), e); + } + if (socketAddress instanceof InetSocketAddress) { + remoteHost = ((InetSocketAddress) socketAddress).getAddress().getHostName(); + if (remoteAddr == null) { + remoteAddr = ((InetSocketAddress) socketAddress).getAddress().getHostAddress(); + } + } + } + } + + + @Override + protected void populateRemotePort() { + AsynchronousSocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + SocketAddress socketAddress = null; + try { + socketAddress = sc.getRemoteAddress(); + } catch (IOException e) { + log.warn(sm.getString("endpoint.warn.noRemotePort", getSocket()), e); + } + if (socketAddress instanceof InetSocketAddress) { + remotePort = ((InetSocketAddress) socketAddress).getPort(); + } + } + } + + + @Override + protected void populateLocalName() { + AsynchronousSocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + SocketAddress socketAddress = null; + try { + socketAddress = sc.getLocalAddress(); + } catch (IOException e) { + log.warn(sm.getString("endpoint.warn.noLocalName", getSocket()), e); + } + if (socketAddress instanceof InetSocketAddress) { + localName = ((InetSocketAddress) socketAddress).getHostName(); + } + } + } + + + @Override + protected void populateLocalAddr() { + AsynchronousSocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + SocketAddress socketAddress = null; + try { + socketAddress = sc.getLocalAddress(); + } catch (IOException e) { + log.warn(sm.getString("endpoint.warn.noLocalAddr", getSocket()), e); + } + if (socketAddress instanceof InetSocketAddress) { + localAddr = ((InetSocketAddress) socketAddress).getAddress().getHostAddress(); + } + } + } + + + @Override + protected void populateLocalPort() { + AsynchronousSocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + SocketAddress socketAddress = null; + try { + socketAddress = sc.getLocalAddress(); + } catch (IOException e) { + log.warn(sm.getString("endpoint.warn.noLocalPort", getSocket()), e); + } + if (socketAddress instanceof InetSocketAddress) { + localPort = ((InetSocketAddress) socketAddress).getPort(); + } + } + } + + + @Override + public SSLSupport getSslSupport() { + if (getSocket() instanceof SecureNio2Channel) { + SecureNio2Channel ch = (SecureNio2Channel) getSocket(); + return ch.getSSLSupport(); + } + return null; + } + + + @Override + public void doClientAuth(SSLSupport sslSupport) throws IOException { + SecureNio2Channel sslChannel = (SecureNio2Channel) getSocket(); + SSLEngine engine = sslChannel.getSslEngine(); + if (!engine.getNeedClientAuth()) { + // Need to re-negotiate SSL connection + engine.setNeedClientAuth(true); + sslChannel.rehandshake(); + ((JSSESupport) sslSupport).setSession(engine.getSession()); + } + } + + + @Override + public void setAppReadBufHandler(ApplicationBufferHandler handler) { + getSocket().setAppReadBufHandler(handler); + } + } + + public static void startInline() { + inlineCompletion.set(Boolean.TRUE); + } + + public static void endInline() { + inlineCompletion.set(Boolean.FALSE); + } + + public static boolean isInline() { + Boolean flag = inlineCompletion.get(); + if (flag == null) { + return false; + } else { + return flag.booleanValue(); + } + } + + + // ---------------------------------------------- SocketProcessor Inner Class + /** + * This class is the equivalent of the Worker, but will simply use in an + * external Executor thread pool. + */ + protected class SocketProcessor extends SocketProcessorBase { + + public SocketProcessor(SocketWrapperBase socketWrapper, SocketEvent event) { + super(socketWrapper, event); + } + + @Override + protected void doRun() { + boolean launch = false; + try { + int handshake = -1; + + try { + if (socketWrapper.getSocket().isHandshakeComplete()) { + // No TLS handshaking required. Let the handler + // process this socket / event combination. + handshake = 0; + } else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT || + event == SocketEvent.ERROR) { + // Unable to complete the TLS handshake. Treat it as + // if the handshake failed. + handshake = -1; + } else { + handshake = socketWrapper.getSocket().handshake(); + // The handshake process reads/writes from/to the + // socket. status may therefore be OPEN_WRITE once + // the handshake completes. However, the handshake + // happens when the socket is opened so the status + // must always be OPEN_READ after it completes. It + // is OK to always set this as it is only used if + // the handshake completes. + event = SocketEvent.OPEN_READ; + } + } catch (IOException x) { + handshake = -1; + if (logHandshake.isDebugEnabled()) { + logHandshake.debug(sm.getString("endpoint.err.handshake", + socketWrapper.getRemoteAddr(), Integer.toString(socketWrapper.getRemotePort())), x); + } + } + if (handshake == 0) { + SocketState state = SocketState.OPEN; + // Process the request from this socket + if (event == null) { + state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ); + } else { + state = getHandler().process(socketWrapper, event); + } + if (state == SocketState.CLOSED) { + // Close socket and pool + socketWrapper.close(); + } else if (state == SocketState.UPGRADING) { + launch = true; + } + } else if (handshake == -1 ) { + getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL); + socketWrapper.close(); + } + } catch (VirtualMachineError vme) { + ExceptionUtils.handleThrowable(vme); + } catch (Throwable t) { + log.error(sm.getString("endpoint.processing.fail"), t); + if (socketWrapper != null) { + ((Nio2SocketWrapper) socketWrapper).close(); + } + } finally { + if (launch) { + try { + getExecutor().execute(new SocketProcessor(socketWrapper, SocketEvent.OPEN_READ)); + } catch (NullPointerException npe) { + if (running) { + log.error(sm.getString("endpoint.launch.fail"), + npe); + } + } + } + socketWrapper = null; + event = null; + //return to cache + if (running && processorCache != null) { + processorCache.push(this); + } + } + } + } + + // ----------------------------------------------- SendfileData Inner Class + /** + * SendfileData class. + */ + public static class SendfileData extends SendfileDataBase { + private FileChannel fchannel; + // Internal use only + private boolean doneInline = false; + private boolean error = false; + + public SendfileData(String filename, long pos, long length) { + super(filename, pos, length); + } + } +} diff --git a/java/org/apache/tomcat/util/net/NioChannel.java b/java/org/apache/tomcat/util/net/NioChannel.java new file mode 100644 index 0000000..6ddf563 --- /dev/null +++ b/java/org/apache/tomcat/util/net/NioChannel.java @@ -0,0 +1,277 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.ScatteringByteChannel; +import java.nio.channels.SocketChannel; + +import org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper; +import org.apache.tomcat.util.res.StringManager; + +/** + * Base class for a SocketChannel wrapper used by the endpoint. + * This way, logic for an SSL socket channel remains the same as for + * a non SSL, making sure we don't need to code for any exception cases. + */ +public class NioChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel { + + protected static final StringManager sm = StringManager.getManager(NioChannel.class); + + protected static final ByteBuffer emptyBuf = ByteBuffer.allocate(0); + + protected final SocketBufferHandler bufHandler; + protected SocketChannel sc = null; + protected NioSocketWrapper socketWrapper = null; + + public NioChannel(SocketBufferHandler bufHandler) { + this.bufHandler = bufHandler; + } + + /** + * Reset the channel + * + * @param channel the socket channel + * @param socketWrapper the socket wrapper + * @throws IOException If a problem was encountered resetting the channel + */ + public void reset(SocketChannel channel, NioSocketWrapper socketWrapper) throws IOException { + this.sc = channel; + this.socketWrapper = socketWrapper; + bufHandler.reset(); + } + + /** + * @return the socketWrapper + */ + NioSocketWrapper getSocketWrapper() { + return socketWrapper; + } + + /** + * Free the channel memory + */ + public void free() { + bufHandler.free(); + } + + /** + * Closes this channel. + * + * @throws IOException If an I/O error occurs + */ + @Override + public void close() throws IOException { + sc.close(); + } + + /** + * Close the connection. + * + * @param force Should the underlying socket be forcibly closed? + * @throws IOException If closing the secure channel fails. + */ + public void close(boolean force) throws IOException { + if (isOpen() || force) { + close(); + } + } + + /** + * Tells whether or not this channel is open. + * + * @return true if, and only if, this channel is open + */ + @Override + public boolean isOpen() { + return sc.isOpen(); + } + + /** + * Writes a sequence of bytes to this channel from the given buffer. + * + * @param src The buffer from which bytes are to be retrieved + * @return The number of bytes written, possibly zero + * @throws IOException If some other I/O error occurs + */ + @Override + public int write(ByteBuffer src) throws IOException { + checkInterruptStatus(); + return sc.write(src); + } + + @Override + public long write(ByteBuffer[] srcs) throws IOException { + return write(srcs, 0, srcs.length); + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) + throws IOException { + checkInterruptStatus(); + return sc.write(srcs, offset, length); + } + + /** + * Reads a sequence of bytes from this channel into the given buffer. + * + * @param dst The buffer into which bytes are to be transferred + * @return The number of bytes read, possibly zero, or -1 if + * the channel has reached end-of-stream + * @throws IOException If some other I/O error occurs + */ + @Override + public int read(ByteBuffer dst) throws IOException { + return sc.read(dst); + } + + @Override + public long read(ByteBuffer[] dsts) throws IOException { + return read(dsts, 0, dsts.length); + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) + throws IOException { + return sc.read(dsts, offset, length); + } + + public SocketBufferHandler getBufHandler() { + return bufHandler; + } + + public SocketChannel getIOChannel() { + return sc; + } + + public boolean isClosing() { + return false; + } + + public boolean isHandshakeComplete() { + return true; + } + + /** + * Performs SSL handshake hence is a no-op for the non-secure + * implementation. + * + * @param read Unused in non-secure implementation + * @param write Unused in non-secure implementation + * @return Always returns zero + * @throws IOException Never for non-secure channel + */ + public int handshake(boolean read, boolean write) throws IOException { + return 0; + } + + @Override + public String toString() { + return super.toString() + ":" + sc; + } + + public int getOutboundRemaining() { + return 0; + } + + /** + * Return true if the buffer wrote data. NO-OP for non-secure channel. + * + * @return Always returns {@code false} for non-secure channel + * @throws IOException Never for non-secure channel + */ + public boolean flushOutbound() throws IOException { + return false; + } + + /** + * This method should be used to check the interrupt status before + * attempting a write. + * + * If a thread has been interrupted and the interrupt has not been cleared + * then an attempt to write to the socket will fail. When this happens the + * socket is removed from the poller without the socket being selected. This + * results in a connection limit leak for NIO as the endpoint expects the + * socket to be selected even in error conditions. + * + * @throws IOException If the current thread was interrupted + */ + protected void checkInterruptStatus() throws IOException { + if (Thread.interrupted()) { + throw new IOException(sm.getString("channel.nio.interrupted")); + } + } + + private ApplicationBufferHandler appReadBufHandler; + public void setAppReadBufHandler(ApplicationBufferHandler handler) { + this.appReadBufHandler = handler; + } + protected ApplicationBufferHandler getAppReadBufHandler() { + return appReadBufHandler; + } + + static final NioChannel CLOSED_NIO_CHANNEL = new NioChannel(SocketBufferHandler.EMPTY) { + @Override + public void close() throws IOException { + } + @Override + public boolean isOpen() { + return false; + } + @Override + public void reset(SocketChannel channel, NioSocketWrapper socketWrapper) throws IOException { + } + @Override + public void free() { + } + @Override + protected ApplicationBufferHandler getAppReadBufHandler() { + return ApplicationBufferHandler.EMPTY; + } + @Override + public void setAppReadBufHandler(ApplicationBufferHandler handler) { + } + @Override + public int read(ByteBuffer dst) throws IOException { + return -1; + } + @Override + public long read(ByteBuffer[] dsts, int offset, int length) + throws IOException { + return -1L; + } + @Override + public int write(ByteBuffer src) throws IOException { + checkInterruptStatus(); + throw new ClosedChannelException(); + } + @Override + public long write(ByteBuffer[] srcs, int offset, int length) + throws IOException { + throw new ClosedChannelException(); + } + @Override + public String toString() { + return "Closed NioChannel"; + } + }; + +} diff --git a/java/org/apache/tomcat/util/net/NioEndpoint.java b/java/org/apache/tomcat/util/net/NioEndpoint.java new file mode 100644 index 0000000..508682c --- /dev/null +++ b/java/org/apache/tomcat/util/net/NioEndpoint.java @@ -0,0 +1,1782 @@ +/* + * 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.tomcat.util.net; + +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.CompletionHandler; +import java.nio.channels.FileChannel; +import java.nio.channels.NetworkChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import javax.net.ssl.SSLEngine; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.collections.SynchronizedQueue; +import org.apache.tomcat.util.collections.SynchronizedStack; +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.compat.JrePlatform; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.Acceptor.AcceptorState; +import org.apache.tomcat.util.net.jsse.JSSESupport; + +/** + * NIO endpoint. + * + * @author Mladen Turk + */ +public class NioEndpoint extends AbstractJsseEndpoint { + + + // -------------------------------------------------------------- Constants + + + private static final Log log = LogFactory.getLog(NioEndpoint.class); + private static final Log logCertificate = LogFactory.getLog(NioEndpoint.class.getName() + ".certificate"); + private static final Log logHandshake = LogFactory.getLog(NioEndpoint.class.getName() + ".handshake"); + + + public static final int OP_REGISTER = 0x100; //register interest op + + // ----------------------------------------------------------------- Fields + + /** + * Server socket "pointer". + */ + private volatile ServerSocketChannel serverSock = null; + + /** + * Stop latch used to wait for poller stop + */ + private volatile CountDownLatch stopLatch = null; + + /** + * Cache for poller events + */ + private SynchronizedStack eventCache; + + /** + * Bytebuffer cache, each channel holds a set of buffers (two, except for SSL holds four) + */ + private SynchronizedStack nioChannels; + + private SocketAddress previousAcceptedSocketRemoteAddress = null; + private long previousAcceptedSocketNanoTime = 0; + + + // ------------------------------------------------------------- Properties + + /** + * Use System.inheritableChannel to obtain channel from stdin/stdout. + */ + private boolean useInheritedChannel = false; + public void setUseInheritedChannel(boolean useInheritedChannel) { this.useInheritedChannel = useInheritedChannel; } + public boolean getUseInheritedChannel() { return useInheritedChannel; } + + + /** + * Path for the Unix domain socket, used to create the socket address. + */ + private String unixDomainSocketPath = null; + public String getUnixDomainSocketPath() { return this.unixDomainSocketPath; } + public void setUnixDomainSocketPath(String unixDomainSocketPath) { + this.unixDomainSocketPath = unixDomainSocketPath; + } + + + /** + * Permissions which will be set on the Unix domain socket if it is created. + */ + private String unixDomainSocketPathPermissions = null; + public String getUnixDomainSocketPathPermissions() { return this.unixDomainSocketPathPermissions; } + public void setUnixDomainSocketPathPermissions(String unixDomainSocketPathPermissions) { + this.unixDomainSocketPathPermissions = unixDomainSocketPathPermissions; + } + + + /** + * Priority of the poller thread. + */ + private int pollerThreadPriority = Thread.NORM_PRIORITY; + public void setPollerThreadPriority(int pollerThreadPriority) { this.pollerThreadPriority = pollerThreadPriority; } + public int getPollerThreadPriority() { return pollerThreadPriority; } + + + private long selectorTimeout = 1000; + public void setSelectorTimeout(long timeout) { this.selectorTimeout = timeout;} + public long getSelectorTimeout() { return this.selectorTimeout; } + + /** + * The socket poller. + */ + private Poller poller = null; + + + // --------------------------------------------------------- Public Methods + + /** + * Number of keep-alive sockets. + * + * @return The number of sockets currently in the keep-alive state waiting + * for the next request to be received on the socket + */ + public int getKeepAliveCount() { + if (poller == null) { + return 0; + } else { + return poller.getKeyCount(); + } + } + + + @Override + public String getId() { + if (getUseInheritedChannel()) { + return "JVMInheritedChannel"; + } else if (getUnixDomainSocketPath() != null) { + return getUnixDomainSocketPath(); + } else { + return null; + } + } + + + // ----------------------------------------------- Public Lifecycle Methods + + /** + * Initialize the endpoint. + */ + @Override + public void bind() throws Exception { + initServerSocket(); + + setStopLatch(new CountDownLatch(1)); + + // Initialize SSL if needed + initialiseSsl(); + } + + // Separated out to make it easier for folks that extend NioEndpoint to + // implement custom [server]sockets + protected void initServerSocket() throws Exception { + if (getUseInheritedChannel()) { + // Retrieve the channel provided by the OS + Channel ic = System.inheritedChannel(); + if (ic instanceof ServerSocketChannel) { + serverSock = (ServerSocketChannel) ic; + } + if (serverSock == null) { + throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited")); + } + } else if (getUnixDomainSocketPath() != null) { + SocketAddress sa = JreCompat.getInstance().getUnixDomainSocketAddress(getUnixDomainSocketPath()); + serverSock = JreCompat.getInstance().openUnixDomainServerSocketChannel(); + serverSock.bind(sa, getAcceptCount()); + if (getUnixDomainSocketPathPermissions() != null) { + Path path = Paths.get(getUnixDomainSocketPath()); + Set permissions = + PosixFilePermissions.fromString(getUnixDomainSocketPathPermissions()); + if (path.getFileSystem().supportedFileAttributeViews().contains("posix")) { + FileAttribute> attrs = PosixFilePermissions.asFileAttribute(permissions); + Files.setAttribute(path, attrs.name(), attrs.value()); + } else { + File file = path.toFile(); + if (permissions.contains(PosixFilePermission.OTHERS_READ) && !file.setReadable(true, false)) { + log.warn(sm.getString("endpoint.nio.perms.readFail", file.getPath())); + } + if (permissions.contains(PosixFilePermission.OTHERS_WRITE) && !file.setWritable(true, false)) { + log.warn(sm.getString("endpoint.nio.perms.writeFail", file.getPath())); + } + } + } + } else { + serverSock = ServerSocketChannel.open(); + socketProperties.setProperties(serverSock.socket()); + InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset()); + serverSock.bind(addr, getAcceptCount()); + } + serverSock.configureBlocking(true); //mimic APR behavior + } + + + /** + * Start the NIO endpoint, creating acceptor, poller threads. + */ + @Override + public void startInternal() throws Exception { + + if (!running) { + running = true; + paused = false; + + if (socketProperties.getProcessorCache() != 0) { + processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, + socketProperties.getProcessorCache()); + } + if (socketProperties.getEventCache() != 0) { + eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, + socketProperties.getEventCache()); + } + int actualBufferPool = + socketProperties.getActualBufferPool(isSSLEnabled() ? getSniParseLimit() * 2 : 0); + if (actualBufferPool != 0) { + nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, + actualBufferPool); + } + + // Create worker collection + if (getExecutor() == null) { + createExecutor(); + } + + initializeConnectionLatch(); + + // Start poller thread + poller = new Poller(); + Thread pollerThread = new Thread(poller, getName() + "-Poller"); + pollerThread.setPriority(threadPriority); + pollerThread.setDaemon(true); + pollerThread.start(); + + startAcceptorThread(); + } + } + + + /** + * Stop the endpoint. This will cause all processing threads to stop. + */ + @Override + public void stopInternal() { + if (!paused) { + pause(); + } + if (running) { + running = false; + acceptor.stop(10); + if (poller != null) { + poller.destroy(); + poller = null; + } + try { + if (!getStopLatch().await(selectorTimeout + 100, TimeUnit.MILLISECONDS)) { + log.warn(sm.getString("endpoint.nio.stopLatchAwaitFail")); + } + } catch (InterruptedException e) { + log.warn(sm.getString("endpoint.nio.stopLatchAwaitInterrupted"), e); + } + shutdownExecutor(); + if (eventCache != null) { + eventCache.clear(); + eventCache = null; + } + if (nioChannels != null) { + NioChannel socket; + while ((socket = nioChannels.pop()) != null) { + socket.free(); + } + nioChannels = null; + } + if (processorCache != null) { + processorCache.clear(); + processorCache = null; + } + } + } + + + /** + * Deallocate NIO memory pools, and close server socket. + */ + @Override + public void unbind() throws Exception { + if (log.isTraceEnabled()) { + log.trace("Destroy initiated for " + + new InetSocketAddress(getAddress(),getPortWithOffset())); + } + if (running) { + stop(); + } + try { + doCloseServerSocket(); + } catch (IOException ioe) { + getLog().warn(sm.getString("endpoint.serverSocket.closeFailed", getName()), ioe); + } + destroySsl(); + super.unbind(); + if (getHandler() != null ) { + getHandler().recycle(); + } + if (log.isTraceEnabled()) { + log.trace("Destroy completed for " + + new InetSocketAddress(getAddress(), getPortWithOffset())); + } + } + + + @Override + protected void doCloseServerSocket() throws IOException { + try { + if (!getUseInheritedChannel() && serverSock != null) { + // Close server socket + serverSock.close(); + } + serverSock = null; + } finally { + if (getUnixDomainSocketPath() != null && getBindState().wasBound()) { + Files.delete(Paths.get(getUnixDomainSocketPath())); + } + } + } + + + // ------------------------------------------------------ Protected Methods + + + @Override + protected void unlockAccept() { + if (getUnixDomainSocketPath() == null) { + super.unlockAccept(); + } else { + // Only try to unlock the acceptor if it is necessary + if (acceptor == null || acceptor.getState() != AcceptorState.RUNNING) { + return; + } + try { + SocketAddress sa = JreCompat.getInstance().getUnixDomainSocketAddress(getUnixDomainSocketPath()); + try (SocketChannel socket = JreCompat.getInstance().openUnixDomainSocketChannel()) { + // With a UDS, expect no delay connecting and no defer accept + socket.connect(sa); + } + // Wait for up to 1000ms acceptor threads to unlock + long waitLeft = 1000; + while (waitLeft > 0 && + acceptor.getState() == AcceptorState.RUNNING) { + Thread.sleep(5); + waitLeft -= 5; + } + } catch(Throwable t) { + ExceptionUtils.handleThrowable(t); + if (getLog().isDebugEnabled()) { + getLog().debug(sm.getString( + "endpoint.debug.unlock.fail", String.valueOf(getPortWithOffset())), t); + } + } + } + } + + + protected SynchronizedStack getNioChannels() { + return nioChannels; + } + + + protected Poller getPoller() { + return poller; + } + + + protected CountDownLatch getStopLatch() { + return stopLatch; + } + + + protected void setStopLatch(CountDownLatch stopLatch) { + this.stopLatch = stopLatch; + } + + + /** + * Process the specified connection. + * @param socket The socket channel + * @return true if the socket was correctly configured + * and processing may continue, false if the socket needs to be + * close immediately + */ + @Override + protected boolean setSocketOptions(SocketChannel socket) { + NioSocketWrapper socketWrapper = null; + try { + // Allocate channel and wrapper + NioChannel channel = null; + if (nioChannels != null) { + channel = nioChannels.pop(); + } + if (channel == null) { + SocketBufferHandler bufhandler = new SocketBufferHandler( + socketProperties.getAppReadBufSize(), + socketProperties.getAppWriteBufSize(), + socketProperties.getDirectBuffer()); + if (isSSLEnabled()) { + channel = new SecureNioChannel(bufhandler, this); + } else { + channel = new NioChannel(bufhandler); + } + } + NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this); + channel.reset(socket, newWrapper); + connections.put(socket, newWrapper); + socketWrapper = newWrapper; + + // Set socket properties + // Disable blocking, polling will be used + socket.configureBlocking(false); + if (getUnixDomainSocketPath() == null) { + socketProperties.setProperties(socket.socket()); + } + + socketWrapper.setReadTimeout(getConnectionTimeout()); + socketWrapper.setWriteTimeout(getConnectionTimeout()); + socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests()); + poller.register(socketWrapper); + return true; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + try { + log.error(sm.getString("endpoint.socketOptionsError"), t); + } catch (Throwable tt) { + ExceptionUtils.handleThrowable(tt); + } + if (socketWrapper == null) { + destroySocket(socket); + } + } + // Tell to close the socket if needed + return false; + } + + + @Override + protected void destroySocket(SocketChannel socket) { + countDownConnection(); + try { + socket.close(); + } catch (IOException ioe) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("endpoint.err.close"), ioe); + } + } + } + + + @Override + protected NetworkChannel getServerSocket() { + return serverSock; + } + + + @Override + protected SocketChannel serverSocketAccept() throws Exception { + SocketChannel result = serverSock.accept(); + + // Bug does not affect Windows platform and Unix Domain Socket. Skip the check. + if (!JrePlatform.IS_WINDOWS && getUnixDomainSocketPath() == null) { + SocketAddress currentRemoteAddress = result.getRemoteAddress(); + long currentNanoTime = System.nanoTime(); + if (currentRemoteAddress.equals(previousAcceptedSocketRemoteAddress) && + currentNanoTime - previousAcceptedSocketNanoTime < 1000) { + throw new IOException(sm.getString("endpoint.err.duplicateAccept")); + } + previousAcceptedSocketRemoteAddress = currentRemoteAddress; + previousAcceptedSocketNanoTime = currentNanoTime; + } + + return result; + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + protected Log getLogCertificate() { + return logCertificate; + } + + + @Override + protected SocketProcessorBase createSocketProcessor( + SocketWrapperBase socketWrapper, SocketEvent event) { + return new SocketProcessor(socketWrapper, event); + } + + // ----------------------------------------------------- Poller Inner Classes + + /** + * PollerEvent, cacheable object for poller events to avoid GC + */ + public static class PollerEvent { + + private NioSocketWrapper socketWrapper; + private int interestOps; + + public PollerEvent(NioSocketWrapper socketWrapper, int intOps) { + reset(socketWrapper, intOps); + } + + public void reset(NioSocketWrapper socketWrapper, int intOps) { + this.socketWrapper = socketWrapper; + interestOps = intOps; + } + + public NioSocketWrapper getSocketWrapper() { + return socketWrapper; + } + + public int getInterestOps() { + return interestOps; + } + + public void reset() { + reset(null, 0); + } + + @Override + public String toString() { + return "Poller event: socket [" + socketWrapper.getSocket() + "], socketWrapper [" + socketWrapper + + "], interestOps [" + interestOps + "]"; + } + } + + /** + * Poller class. + */ + public class Poller implements Runnable { + + private Selector selector; + private final SynchronizedQueue events = + new SynchronizedQueue<>(); + + private volatile boolean close = false; + // Optimize expiration handling + private long nextExpiration = 0; + + private AtomicLong wakeupCounter = new AtomicLong(0); + + private volatile int keyCount = 0; + + public Poller() throws IOException { + this.selector = Selector.open(); + } + + public int getKeyCount() { return keyCount; } + + public Selector getSelector() { return selector; } + + /** + * Destroy the poller. + */ + protected void destroy() { + // Wait for polltime before doing anything, so that the poller threads + // exit, otherwise parallel closure of sockets which are still + // in the poller can cause problems + close = true; + selector.wakeup(); + } + + private void addEvent(PollerEvent event) { + events.offer(event); + if (wakeupCounter.incrementAndGet() == 0) { + selector.wakeup(); + } + } + + private PollerEvent createPollerEvent(NioSocketWrapper socketWrapper, int interestOps) { + PollerEvent r = null; + if (eventCache != null) { + r = eventCache.pop(); + } + if (r == null) { + r = new PollerEvent(socketWrapper, interestOps); + } else { + r.reset(socketWrapper, interestOps); + } + return r; + } + + /** + * Add specified socket and associated pool to the poller. The socket will + * be added to a temporary array, and polled first after a maximum amount + * of time equal to pollTime (in most cases, latency will be much lower, + * however). + * + * @param socketWrapper to add to the poller + * @param interestOps Operations for which to register this socket with + * the Poller + */ + public void add(NioSocketWrapper socketWrapper, int interestOps) { + PollerEvent pollerEvent = createPollerEvent(socketWrapper, interestOps); + addEvent(pollerEvent); + if (close) { + processSocket(socketWrapper, SocketEvent.STOP, false); + } + } + + /** + * Processes events in the event queue of the Poller. + * + * @return true if some events were processed, + * false if queue was empty + */ + public boolean events() { + boolean result = false; + + PollerEvent pe = null; + for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) { + result = true; + NioSocketWrapper socketWrapper = pe.getSocketWrapper(); + SocketChannel sc = socketWrapper.getSocket().getIOChannel(); + int interestOps = pe.getInterestOps(); + if (sc == null) { + log.warn(sm.getString("endpoint.nio.nullSocketChannel")); + socketWrapper.close(); + } else if (interestOps == OP_REGISTER) { + try { + sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper); + } catch (Exception x) { + log.error(sm.getString("endpoint.nio.registerFail"), x); + } + } else { + final SelectionKey key = sc.keyFor(getSelector()); + if (key == null) { + // The key was cancelled (e.g. due to socket closure) + // and removed from the selector while it was being + // processed. Count down the connections at this point + // since it won't have been counted down when the socket + // closed. + socketWrapper.close(); + } else { + final NioSocketWrapper attachment = (NioSocketWrapper) key.attachment(); + if (attachment != null) { + // We are registering the key to start with, reset the fairness counter. + try { + int ops = key.interestOps() | interestOps; + attachment.interestOps(ops); + key.interestOps(ops); + } catch (CancelledKeyException ckx) { + socketWrapper.close(); + } + } else { + socketWrapper.close(); + } + } + } + if (running && eventCache != null) { + pe.reset(); + eventCache.push(pe); + } + } + + return result; + } + + /** + * Registers a newly created socket with the poller. + * + * @param socketWrapper The socket wrapper + */ + public void register(final NioSocketWrapper socketWrapper) { + socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into. + PollerEvent pollerEvent = createPollerEvent(socketWrapper, OP_REGISTER); + addEvent(pollerEvent); + } + + /** + * The background thread that adds sockets to the Poller, checks the + * poller for triggered events and hands the associated socket off to an + * appropriate processor as events occur. + */ + @Override + public void run() { + // Loop until destroy() is called + while (true) { + + boolean hasEvents = false; + + try { + if (!close) { + hasEvents = events(); + if (wakeupCounter.getAndSet(-1) > 0) { + // If we are here, means we have other stuff to do + // Do a non blocking select + keyCount = selector.selectNow(); + } else { + keyCount = selector.select(selectorTimeout); + } + wakeupCounter.set(0); + } + if (close) { + events(); + timeout(0, false); + try { + selector.close(); + } catch (IOException ioe) { + log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe); + } + break; + } + // Either we timed out or we woke up, process events first + if (keyCount == 0) { + hasEvents = (hasEvents | events()); + } + } catch (Throwable x) { + ExceptionUtils.handleThrowable(x); + log.error(sm.getString("endpoint.nio.selectorLoopError"), x); + continue; + } + + Iterator iterator = + keyCount > 0 ? selector.selectedKeys().iterator() : null; + // Walk through the collection of ready keys and dispatch + // any active event. + while (iterator != null && iterator.hasNext()) { + SelectionKey sk = iterator.next(); + iterator.remove(); + NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment(); + // Attachment may be null if another thread has called + // cancelledKey() + if (socketWrapper != null) { + processKey(sk, socketWrapper); + } + } + + // Process timeouts + timeout(keyCount,hasEvents); + } + + getStopLatch().countDown(); + } + + protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) { + try { + if (close) { + socketWrapper.close(); + } else if (sk.isValid()) { + if (sk.isReadable() || sk.isWritable()) { + if (socketWrapper.getSendfileData() != null) { + processSendfile(sk, socketWrapper, false); + } else { + unreg(sk, socketWrapper, sk.readyOps()); + boolean closeSocket = false; + // Read goes before write + if (sk.isReadable()) { + if (socketWrapper.readOperation != null) { + if (!socketWrapper.readOperation.process()) { + closeSocket = true; + } + } else if (socketWrapper.readBlocking) { + synchronized (socketWrapper.readLock) { + socketWrapper.readBlocking = false; + socketWrapper.readLock.notify(); + } + } else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) { + closeSocket = true; + } + } + if (!closeSocket && sk.isWritable()) { + if (socketWrapper.writeOperation != null) { + if (!socketWrapper.writeOperation.process()) { + closeSocket = true; + } + } else if (socketWrapper.writeBlocking) { + synchronized (socketWrapper.writeLock) { + socketWrapper.writeBlocking = false; + socketWrapper.writeLock.notify(); + } + } else if (!processSocket(socketWrapper, SocketEvent.OPEN_WRITE, true)) { + closeSocket = true; + } + } + if (closeSocket) { + socketWrapper.close(); + } + } + } + } else { + // Invalid key + socketWrapper.close(); + } + } catch (CancelledKeyException ckx) { + socketWrapper.close(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("endpoint.nio.keyProcessingError"), t); + } + } + + public SendfileState processSendfile(SelectionKey sk, NioSocketWrapper socketWrapper, + boolean calledByProcessor) { + NioChannel sc = null; + try { + unreg(sk, socketWrapper, sk.readyOps()); + SendfileData sd = socketWrapper.getSendfileData(); + + if (log.isTraceEnabled()) { + log.trace("Processing send file for: " + sd.fileName); + } + + if (sd.fchannel == null) { + // Setup the file channel + File f = new File(sd.fileName); + @SuppressWarnings("resource") // Closed when channel is closed + FileInputStream fis = new FileInputStream(f); + sd.fchannel = fis.getChannel(); + } + + // Configure output channel + sc = socketWrapper.getSocket(); + // TLS/SSL channel is slightly different + WritableByteChannel wc = ((sc instanceof SecureNioChannel) ? sc : sc.getIOChannel()); + + // We still have data in the buffer + if (sc.getOutboundRemaining() > 0) { + if (sc.flushOutbound()) { + socketWrapper.updateLastWrite(); + } + } else { + long written = sd.fchannel.transferTo(sd.pos, sd.length, wc); + if (written > 0) { + sd.pos += written; + sd.length -= written; + socketWrapper.updateLastWrite(); + } else { + // Unusual not to be able to transfer any bytes + // Check the length was set correctly + if (sd.fchannel.size() <= sd.pos) { + throw new IOException(sm.getString("endpoint.sendfile.tooMuchData")); + } + } + } + if (sd.length <= 0 && sc.getOutboundRemaining()<=0) { + if (log.isTraceEnabled()) { + log.trace("Send file complete for: " + sd.fileName); + } + socketWrapper.setSendfileData(null); + try { + sd.fchannel.close(); + } catch (Exception ignore) { + } + // For calls from outside the Poller, the caller is + // responsible for registering the socket for the + // appropriate event(s) if sendfile completes. + if (!calledByProcessor) { + switch (sd.keepAliveState) { + case NONE: { + if (log.isTraceEnabled()) { + log.trace("Send file connection is being closed"); + } + socketWrapper.close(); + break; + } + case PIPELINED: { + if (log.isTraceEnabled()) { + log.trace("Connection is keep alive, processing pipe-lined data"); + } + if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) { + socketWrapper.close(); + } + break; + } + case OPEN: { + if (log.isTraceEnabled()) { + log.trace("Connection is keep alive, registering back for OP_READ"); + } + reg(sk, socketWrapper, SelectionKey.OP_READ); + break; + } + } + } + return SendfileState.DONE; + } else { + if (log.isTraceEnabled()) { + log.trace("OP_WRITE for sendfile: " + sd.fileName); + } + if (calledByProcessor) { + add(socketWrapper, SelectionKey.OP_WRITE); + } else { + reg(sk, socketWrapper, SelectionKey.OP_WRITE); + } + return SendfileState.PENDING; + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("endpoint.sendfile.error"), e); + } + if (!calledByProcessor && sc != null) { + socketWrapper.close(); + } + return SendfileState.ERROR; + } catch (Throwable t) { + log.error(sm.getString("endpoint.sendfile.error"), t); + if (!calledByProcessor && sc != null) { + socketWrapper.close(); + } + return SendfileState.ERROR; + } + } + + protected void unreg(SelectionKey sk, NioSocketWrapper socketWrapper, int readyOps) { + // This is a must, so that we don't have multiple threads messing with the socket + reg(sk, socketWrapper, sk.interestOps() & (~readyOps)); + } + + protected void reg(SelectionKey sk, NioSocketWrapper socketWrapper, int intops) { + sk.interestOps(intops); + socketWrapper.interestOps(intops); + } + + protected void timeout(int keyCount, boolean hasEvents) { + long now = System.currentTimeMillis(); + // This method is called on every loop of the Poller. Don't process + // timeouts on every loop of the Poller since that would create too + // much load and timeouts can afford to wait a few seconds. + // However, do process timeouts if any of the following are true: + // - the selector simply timed out (suggests there isn't much load) + // - the nextExpiration time has passed + // - the server socket is being closed + if (nextExpiration > 0 && (keyCount > 0 || hasEvents) && (now < nextExpiration) && !close) { + return; + } + int keycount = 0; + try { + for (SelectionKey key : selector.keys()) { + keycount++; + NioSocketWrapper socketWrapper = (NioSocketWrapper) key.attachment(); + try { + if (socketWrapper == null) { + // We don't support any keys without attachments + if (key.isValid()) { + key.cancel(); + } + } else if (close) { + key.interestOps(0); + // Avoid duplicate stop calls + socketWrapper.interestOps(0); + socketWrapper.close(); + } else if (socketWrapper.interestOpsHas(SelectionKey.OP_READ) || + socketWrapper.interestOpsHas(SelectionKey.OP_WRITE)) { + boolean readTimeout = false; + boolean writeTimeout = false; + // Check for read timeout + if (socketWrapper.interestOpsHas(SelectionKey.OP_READ)) { + long delta = now - socketWrapper.getLastRead(); + long timeout = socketWrapper.getReadTimeout(); + if (timeout > 0 && delta > timeout) { + readTimeout = true; + } + } + // Check for write timeout + if (!readTimeout && socketWrapper.interestOpsHas(SelectionKey.OP_WRITE)) { + long delta = now - socketWrapper.getLastWrite(); + long timeout = socketWrapper.getWriteTimeout(); + if (timeout > 0 && delta > timeout) { + writeTimeout = true; + } + } + if (readTimeout || writeTimeout) { + key.interestOps(0); + // Avoid duplicate timeout calls + socketWrapper.interestOps(0); + socketWrapper.setError(new SocketTimeoutException()); + if (readTimeout && socketWrapper.readOperation != null) { + if (!socketWrapper.readOperation.process()) { + socketWrapper.close(); + } + } else if (writeTimeout && socketWrapper.writeOperation != null) { + if (!socketWrapper.writeOperation.process()) { + socketWrapper.close(); + } + } else if (!processSocket(socketWrapper, SocketEvent.ERROR, true)) { + socketWrapper.close(); + } + } + } + } catch (CancelledKeyException ckx) { + if (socketWrapper != null) { + socketWrapper.close(); + } + } + } + } catch (ConcurrentModificationException cme) { + // See https://bz.apache.org/bugzilla/show_bug.cgi?id=57943 + log.warn(sm.getString("endpoint.nio.timeoutCme"), cme); + } + // For logging purposes only + long prevExp = nextExpiration; + nextExpiration = System.currentTimeMillis() + + socketProperties.getTimeoutInterval(); + if (log.isTraceEnabled()) { + log.trace("timeout completed: keys processed=" + keycount + + "; now=" + now + "; nextExpiration=" + prevExp + + "; keyCount=" + keyCount + "; hasEvents=" + hasEvents + + "; eval=" + ((now < prevExp) && (keyCount>0 || hasEvents) && (!close) )); + } + + } + } + + // --------------------------------------------------- Socket Wrapper Class + + public static class NioSocketWrapper extends SocketWrapperBase { + + private final SynchronizedStack nioChannels; + private final Poller poller; + + private int interestOps = 0; + private volatile SendfileData sendfileData = null; + private volatile long lastRead = System.currentTimeMillis(); + private volatile long lastWrite = lastRead; + + private final Object readLock; + private volatile boolean readBlocking = false; + private final Object writeLock; + private volatile boolean writeBlocking = false; + + public NioSocketWrapper(NioChannel channel, NioEndpoint endpoint) { + super(channel, endpoint); + if (endpoint.getUnixDomainSocketPath() != null) { + // Pretend localhost for easy compatibility + localAddr = "127.0.0.1"; + localName = "localhost"; + localPort = 0; + remoteAddr = "127.0.0.1"; + remoteHost = "localhost"; + remotePort = 0; + } + nioChannels = endpoint.getNioChannels(); + poller = endpoint.getPoller(); + socketBufferHandler = channel.getBufHandler(); + readLock = (readPending == null) ? new Object() : readPending; + writeLock = (writePending == null) ? new Object() : writePending; + } + + public Poller getPoller() { return poller; } + public int interestOps() { return interestOps; } + public int interestOps(int ops) { this.interestOps = ops; return ops; } + public boolean interestOpsHas(int targetOp) { + return (this.interestOps() & targetOp) == targetOp; + } + + public void setSendfileData(SendfileData sf) { this.sendfileData = sf;} + public SendfileData getSendfileData() { return this.sendfileData; } + + public void updateLastWrite() { lastWrite = System.currentTimeMillis(); } + public long getLastWrite() { return lastWrite; } + public void updateLastRead() { lastRead = System.currentTimeMillis(); } + public long getLastRead() { return lastRead; } + + @Override + public boolean isReadyForRead() throws IOException { + socketBufferHandler.configureReadBufferForRead(); + + if (socketBufferHandler.getReadBuffer().remaining() > 0) { + return true; + } + + fillReadBuffer(false); + + boolean isReady = socketBufferHandler.getReadBuffer().position() > 0; + return isReady; + } + + + @Override + public int read(boolean block, byte[] b, int off, int len) throws IOException { + int nRead = populateReadBuffer(b, off, len); + if (nRead > 0) { + return nRead; + /* + * Since more bytes may have arrived since the buffer was last + * filled, it is an option at this point to perform a + * non-blocking read. However correctly handling the case if + * that read returns end of stream adds complexity. Therefore, + * at the moment, the preference is for simplicity. + */ + } + + // Fill the read buffer as best we can. + nRead = fillReadBuffer(block); + updateLastRead(); + + // Fill as much of the remaining byte array as possible with the + // data that was just read + if (nRead > 0) { + socketBufferHandler.configureReadBufferForRead(); + nRead = Math.min(nRead, len); + socketBufferHandler.getReadBuffer().get(b, off, nRead); + } + return nRead; + } + + + @Override + public int read(boolean block, ByteBuffer to) throws IOException { + int nRead = populateReadBuffer(to); + if (nRead > 0) { + return nRead; + /* + * Since more bytes may have arrived since the buffer was last + * filled, it is an option at this point to perform a + * non-blocking read. However correctly handling the case if + * that read returns end of stream adds complexity. Therefore, + * at the moment, the preference is for simplicity. + */ + } + + // The socket read buffer capacity is socket.appReadBufSize + int limit = socketBufferHandler.getReadBuffer().capacity(); + if (to.remaining() >= limit) { + to.limit(to.position() + limit); + nRead = fillReadBuffer(block, to); + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read direct from socket: [" + nRead + "]"); + } + updateLastRead(); + } else { + // Fill the read buffer as best we can. + nRead = fillReadBuffer(block); + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read into buffer: [" + nRead + "]"); + } + updateLastRead(); + + // Fill as much of the remaining byte array as possible with the + // data that was just read + if (nRead > 0) { + nRead = populateReadBuffer(to); + } + } + return nRead; + } + + + @Override + protected void doClose() { + if (log.isTraceEnabled()) { + log.trace("Calling [" + getEndpoint() + "].closeSocket([" + this + "])"); + } + try { + getEndpoint().connections.remove(getSocket().getIOChannel()); + if (getSocket().isOpen()) { + getSocket().close(true); + } + if (getEndpoint().running) { + if (nioChannels == null || !nioChannels.push(getSocket())) { + getSocket().free(); + } + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + if (log.isDebugEnabled()) { + log.error(sm.getString("endpoint.debug.channelCloseFail"), e); + } + } finally { + socketBufferHandler = SocketBufferHandler.EMPTY; + nonBlockingWriteBuffer.clear(); + reset(NioChannel.CLOSED_NIO_CHANNEL); + } + try { + SendfileData data = getSendfileData(); + if (data != null && data.fchannel != null && data.fchannel.isOpen()) { + data.fchannel.close(); + } + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + if (log.isDebugEnabled()) { + log.error(sm.getString("endpoint.sendfile.closeError"), e); + } + } + } + + private int fillReadBuffer(boolean block) throws IOException { + socketBufferHandler.configureReadBufferForWrite(); + return fillReadBuffer(block, socketBufferHandler.getReadBuffer()); + } + + + private int fillReadBuffer(boolean block, ByteBuffer buffer) throws IOException { + int n = 0; + if (getSocket() == NioChannel.CLOSED_NIO_CHANNEL) { + throw new ClosedChannelException(); + } + if (block) { + long timeout = getReadTimeout(); + long startNanos = 0; + do { + if (startNanos > 0) { + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); + if (elapsedMillis == 0) { + elapsedMillis = 1; + } + timeout -= elapsedMillis; + if (timeout <= 0) { + throw new SocketTimeoutException(); + } + } + synchronized (readLock) { + n = getSocket().read(buffer); + if (n == -1) { + throw new EOFException(); + } else if (n == 0) { + // Ensure a spurious wake-up doesn't trigger a duplicate registration + if (!readBlocking) { + readBlocking = true; + registerReadInterest(); + } + try { + if (timeout > 0) { + startNanos = System.nanoTime(); + readLock.wait(timeout); + } else { + readLock.wait(); + } + } catch (InterruptedException e) { + // Continue + } + } + } + } while (n == 0); // TLS needs to loop as reading zero application bytes is possible + } else { + n = getSocket().read(buffer); + if (n == -1) { + throw new EOFException(); + } + } + return n; + } + + + @Override + protected boolean flushNonBlocking() throws IOException { + boolean dataLeft = socketOrNetworkBufferHasDataLeft(); + + // Write to the socket, if there is anything to write + if (dataLeft) { + doWrite(false); + dataLeft = socketOrNetworkBufferHasDataLeft(); + } + + if (!dataLeft && !nonBlockingWriteBuffer.isEmpty()) { + dataLeft = nonBlockingWriteBuffer.write(this, false); + + if (!dataLeft && socketOrNetworkBufferHasDataLeft()) { + doWrite(false); + dataLeft = socketOrNetworkBufferHasDataLeft(); + } + } + + return dataLeft; + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=66076 + * + * When using TLS an additional buffer is used for the encrypted data + * before it is written to the network. It is possible for this network + * output buffer to contain data while the socket write buffer is empty. + * + * For NIO with non-blocking I/O, this case is handling by ensuring that + * flush only returns false (i.e. no data left to flush) if all buffers + * are empty. + */ + private boolean socketOrNetworkBufferHasDataLeft() { + return !socketBufferHandler.isWriteBufferEmpty() || getSocket().getOutboundRemaining() > 0; + } + + + @Override + protected void doWrite(boolean block, ByteBuffer buffer) throws IOException { + int n = 0; + if (getSocket() == NioChannel.CLOSED_NIO_CHANNEL) { + throw new ClosedChannelException(); + } + if (block) { + if (previousIOException != null) { + /* + * Socket has previously timed out. + * + * Blocking writes assume that buffer is always fully + * written so there is no code checking for incomplete + * writes, retaining the unwritten data and attempting to + * write it as part of a subsequent write call. + * + * Because of the above, when a timeout is triggered we need + * to skip subsequent attempts to write as otherwise it will + * appear to the client as if some data was dropped just + * before the connection is lost. It is better if the client + * just sees the dropped connection. + */ + throw new IOException(previousIOException); + } + long timeout = getWriteTimeout(); + long startNanos = 0; + do { + if (startNanos > 0) { + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); + if (elapsedMillis == 0) { + elapsedMillis = 1; + } + timeout -= elapsedMillis; + if (timeout <= 0) { + previousIOException = new SocketTimeoutException(); + throw previousIOException; + } + } + synchronized (writeLock) { + n = getSocket().write(buffer); + // n == 0 could be an incomplete write but it could also + // indicate that a previous incomplete write of the + // outbound buffer (for TLS) has now completed. Only + // block if there is still data to write. + if (n == 0 && (buffer.hasRemaining() || getSocket().getOutboundRemaining() > 0)) { + // Ensure a spurious wake-up doesn't trigger a duplicate registration + if (!writeBlocking) { + writeBlocking = true; + registerWriteInterest(); + } + try { + if (timeout > 0) { + startNanos = System.nanoTime(); + writeLock.wait(timeout); + } else { + writeLock.wait(); + } + } catch (InterruptedException e) { + // Continue + } + } else if (startNanos > 0) { + // If something was written, reset timeout + timeout = getWriteTimeout(); + startNanos = 0; + } + } + } while (buffer.hasRemaining() || getSocket().getOutboundRemaining() > 0); + } else { + do { + n = getSocket().write(buffer); + } while (n > 0 && buffer.hasRemaining()); + // If there is data left in the buffer the socket will be registered for + // write further up the stack. This is to ensure the socket is only + // registered for write once as both container and user code can trigger + // write registration. + } + updateLastWrite(); + } + + + @Override + public void registerReadInterest() { + if (log.isTraceEnabled()) { + log.trace(sm.getString("endpoint.debug.registerRead", this)); + } + getPoller().add(this, SelectionKey.OP_READ); + } + + + @Override + public void registerWriteInterest() { + if (log.isTraceEnabled()) { + log.trace(sm.getString("endpoint.debug.registerWrite", this)); + } + getPoller().add(this, SelectionKey.OP_WRITE); + } + + + @Override + public SendfileDataBase createSendfileData(String filename, long pos, long length) { + return new SendfileData(filename, pos, length); + } + + + @Override + public SendfileState processSendfile(SendfileDataBase sendfileData) { + setSendfileData((SendfileData) sendfileData); + SelectionKey key = getSocket().getIOChannel().keyFor(getPoller().getSelector()); + if (key == null) { + return SendfileState.ERROR; + } else { + // Might as well do the first write on this thread + return getPoller().processSendfile(key, this, true); + } + } + + + @Override + protected void populateRemoteAddr() { + SocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + InetAddress inetAddr = sc.socket().getInetAddress(); + if (inetAddr != null) { + remoteAddr = inetAddr.getHostAddress(); + } + } + } + + + @Override + protected void populateRemoteHost() { + SocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + InetAddress inetAddr = sc.socket().getInetAddress(); + if (inetAddr != null) { + remoteHost = inetAddr.getHostName(); + if (remoteAddr == null) { + remoteAddr = inetAddr.getHostAddress(); + } + } + } + } + + + @Override + protected void populateRemotePort() { + SocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + remotePort = sc.socket().getPort(); + } + } + + + @Override + protected void populateLocalName() { + SocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + InetAddress inetAddr = sc.socket().getLocalAddress(); + if (inetAddr != null) { + localName = inetAddr.getHostName(); + } + } + } + + + @Override + protected void populateLocalAddr() { + SocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + InetAddress inetAddr = sc.socket().getLocalAddress(); + if (inetAddr != null) { + localAddr = inetAddr.getHostAddress(); + } + } + } + + + @Override + protected void populateLocalPort() { + SocketChannel sc = getSocket().getIOChannel(); + if (sc != null) { + localPort = sc.socket().getLocalPort(); + } + } + + + @Override + public SSLSupport getSslSupport() { + if (getSocket() instanceof SecureNioChannel) { + SecureNioChannel ch = (SecureNioChannel) getSocket(); + return ch.getSSLSupport(); + } + return null; + } + + + @Override + public void doClientAuth(SSLSupport sslSupport) throws IOException { + SecureNioChannel sslChannel = (SecureNioChannel) getSocket(); + SSLEngine engine = sslChannel.getSslEngine(); + if (!engine.getNeedClientAuth()) { + // Need to re-negotiate SSL connection + engine.setNeedClientAuth(true); + sslChannel.rehandshake(getEndpoint().getConnectionTimeout()); + ((JSSESupport) sslSupport).setSession(engine.getSession()); + } + } + + + @Override + public void setAppReadBufHandler(ApplicationBufferHandler handler) { + getSocket().setAppReadBufHandler(handler); + } + + @Override + protected OperationState newOperationState(boolean read, + ByteBuffer[] buffers, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, + CompletionCheck check, CompletionHandler handler, + Semaphore semaphore, VectoredIOCompletionHandler completion) { + return new NioOperationState<>(read, buffers, offset, length, block, + timeout, unit, attachment, check, handler, semaphore, completion); + } + + private class NioOperationState extends OperationState { + private volatile boolean inline = true; + private NioOperationState(boolean read, ByteBuffer[] buffers, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, CompletionCheck check, + CompletionHandler handler, Semaphore semaphore, + VectoredIOCompletionHandler completion) { + super(read, buffers, offset, length, block, + timeout, unit, attachment, check, handler, semaphore, completion); + } + + @Override + protected boolean isInline() { + return inline; + } + + @Override + protected boolean hasOutboundRemaining() { + return getSocket().getOutboundRemaining() > 0; + } + + @Override + public void run() { + // Perform the IO operation + // Called from the poller to continue the IO operation + long nBytes = 0; + if (getError() == null) { + try { + synchronized (this) { + if (!completionDone) { + // This filters out same notification until processing + // of the current one is done + if (log.isTraceEnabled()) { + log.trace("Skip concurrent " + (read ? "read" : "write") + " notification"); + } + return; + } + if (read) { + // Read from main buffer first + if (!socketBufferHandler.isReadBufferEmpty()) { + // There is still data inside the main read buffer, it needs to be read first + socketBufferHandler.configureReadBufferForRead(); + for (int i = 0; i < length && !socketBufferHandler.isReadBufferEmpty(); i++) { + nBytes += transfer(socketBufferHandler.getReadBuffer(), buffers[offset + i]); + } + } + if (nBytes == 0) { + nBytes = getSocket().read(buffers, offset, length); + updateLastRead(); + } + } else { + boolean doWrite = true; + // Write from main buffer first + if (socketOrNetworkBufferHasDataLeft()) { + // There is still data inside the main write buffer, it needs to be written first + socketBufferHandler.configureWriteBufferForRead(); + do { + nBytes = getSocket().write(socketBufferHandler.getWriteBuffer()); + } while (socketOrNetworkBufferHasDataLeft() && nBytes > 0); + if (socketOrNetworkBufferHasDataLeft()) { + doWrite = false; + } + // Preserve a negative value since it is an error + if (nBytes > 0) { + nBytes = 0; + } + } + if (doWrite) { + long n = 0; + do { + n = getSocket().write(buffers, offset, length); + if (n == -1) { + nBytes = n; + } else { + nBytes += n; + } + } while (n > 0); + updateLastWrite(); + } + } + if (nBytes != 0 || (!buffersArrayHasRemaining(buffers, offset, length) && + (read || !socketOrNetworkBufferHasDataLeft()))) { + completionDone = false; + } + } + } catch (IOException e) { + setError(e); + } + } + if (nBytes > 0 || (nBytes == 0 && !buffersArrayHasRemaining(buffers, offset, length) && + (read || !socketOrNetworkBufferHasDataLeft()))) { + // The bytes processed are only updated in the completion handler + completion.completed(Long.valueOf(nBytes), this); + } else if (nBytes < 0 || getError() != null) { + IOException error = getError(); + if (error == null) { + error = new EOFException(); + } + completion.failed(error, this); + } else { + // As soon as the operation uses the poller, it is no longer inline + inline = false; + if (read) { + registerReadInterest(); + } else { + registerWriteInterest(); + } + } + } + } + } + + + // ---------------------------------------------- SocketProcessor Inner Class + + /** + * This class is the equivalent of the Worker, but will simply use in an + * external Executor thread pool. + */ + protected class SocketProcessor extends SocketProcessorBase { + + public SocketProcessor(SocketWrapperBase socketWrapper, SocketEvent event) { + super(socketWrapper, event); + } + + @Override + protected void doRun() { + /* + * Do not cache and re-use the value of socketWrapper.getSocket() in + * this method. If the socket closes the value will be updated to + * CLOSED_NIO_CHANNEL and the previous value potentially re-used for + * a new connection. That can result in a stale cached value which + * in turn can result in unintentionally closing currently active + * connections. + */ + Poller poller = NioEndpoint.this.poller; + if (poller == null) { + socketWrapper.close(); + return; + } + + try { + int handshake = -1; + try { + if (socketWrapper.getSocket().isHandshakeComplete()) { + // No TLS handshaking required. Let the handler + // process this socket / event combination. + handshake = 0; + } else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT || + event == SocketEvent.ERROR) { + // Unable to complete the TLS handshake. Treat it as + // if the handshake failed. + handshake = -1; + } else { + handshake = socketWrapper.getSocket().handshake(event == SocketEvent.OPEN_READ, event == SocketEvent.OPEN_WRITE); + // The handshake process reads/writes from/to the + // socket. status may therefore be OPEN_WRITE once + // the handshake completes. However, the handshake + // happens when the socket is opened so the status + // must always be OPEN_READ after it completes. It + // is OK to always set this as it is only used if + // the handshake completes. + event = SocketEvent.OPEN_READ; + } + } catch (IOException x) { + handshake = -1; + if (logHandshake.isDebugEnabled()) { + logHandshake.debug(sm.getString("endpoint.err.handshake", + socketWrapper.getRemoteAddr(), Integer.toString(socketWrapper.getRemotePort())), x); + } + } catch (CancelledKeyException ckx) { + handshake = -1; + } + if (handshake == 0) { + SocketState state = SocketState.OPEN; + // Process the request from this socket + if (event == null) { + state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ); + } else { + state = getHandler().process(socketWrapper, event); + } + if (state == SocketState.CLOSED) { + socketWrapper.close(); + } + } else if (handshake == -1 ) { + getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL); + socketWrapper.close(); + } else if (handshake == SelectionKey.OP_READ){ + socketWrapper.registerReadInterest(); + } else if (handshake == SelectionKey.OP_WRITE){ + socketWrapper.registerWriteInterest(); + } + } catch (CancelledKeyException cx) { + socketWrapper.close(); + } catch (VirtualMachineError vme) { + ExceptionUtils.handleThrowable(vme); + } catch (Throwable t) { + log.error(sm.getString("endpoint.processing.fail"), t); + socketWrapper.close(); + } finally { + socketWrapper = null; + event = null; + //return to cache + if (running && processorCache != null) { + processorCache.push(this); + } + } + } + + } + + + // ----------------------------------------------- SendfileData Inner Class + + /** + * SendfileData class. + */ + public static class SendfileData extends SendfileDataBase { + + public SendfileData(String filename, long pos, long length) { + super(filename, pos, length); + } + + protected volatile FileChannel fchannel; + } +} diff --git a/java/org/apache/tomcat/util/net/SSLContext.java b/java/org/apache/tomcat/util/net/SSLContext.java new file mode 100644 index 0000000..0074782 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SSLContext.java @@ -0,0 +1,53 @@ +/* + * 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.tomcat.util.net; + +import java.security.KeyManagementException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.TrustManager; + +/** + * This interface is needed to override the default SSLContext class + * to allow SSL implementation pluggability without having to use JCE. With + * regular JSSE it will do nothing but delegate to the SSLContext. + */ +public interface SSLContext { + + void init(KeyManager[] kms, TrustManager[] tms, + SecureRandom sr) throws KeyManagementException; + + void destroy(); + + SSLSessionContext getServerSessionContext(); + + SSLEngine createSSLEngine(); + + SSLServerSocketFactory getServerSocketFactory(); + + SSLParameters getSupportedSSLParameters(); + + X509Certificate[] getCertificateChain(String alias); + + X509Certificate[] getAcceptedIssuers(); +} diff --git a/java/org/apache/tomcat/util/net/SSLHostConfig.java b/java/org/apache/tomcat/util/net/SSLHostConfig.java new file mode 100644 index 0000000..9917fe6 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SSLHostConfig.java @@ -0,0 +1,855 @@ +/* + * 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.tomcat.util.net; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Serializable; +import java.security.KeyStore; +import java.security.UnrecoverableKeyException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import javax.management.ObjectName; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.openssl.OpenSSLConf; +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser; +import org.apache.tomcat.util.res.StringManager; + +/** + * Represents the TLS configuration for a virtual host. + */ +public class SSLHostConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(SSLHostConfig.class); + private static final StringManager sm = StringManager.getManager(SSLHostConfig.class); + + // Must be lower case. SSL host names are always stored using lower case as + // they are case insensitive but are used by case sensitive code such as + // keys in Maps. + protected static final String DEFAULT_SSL_HOST_NAME = "_default_"; + protected static final Set SSL_PROTO_ALL_SET = new HashSet<>(); + public static final String DEFAULT_TLS_CIPHERS = "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!kRSA"; + + static { + /* Default used if protocols are not configured, also used if + * protocols="All" + */ + SSL_PROTO_ALL_SET.add(Constants.SSL_PROTO_SSLv2Hello); + SSL_PROTO_ALL_SET.add(Constants.SSL_PROTO_TLSv1); + SSL_PROTO_ALL_SET.add(Constants.SSL_PROTO_TLSv1_1); + SSL_PROTO_ALL_SET.add(Constants.SSL_PROTO_TLSv1_2); + SSL_PROTO_ALL_SET.add(Constants.SSL_PROTO_TLSv1_3); + } + + private Type configType = null; + + private String hostName = DEFAULT_SSL_HOST_NAME; + + private transient volatile Long openSslConfContext = Long.valueOf(0); + // OpenSSL can handle multiple certs in a single config so the reference to + // the context is here at the virtual host level. JSSE can't so the + // reference is held on the certificate. + private transient volatile Long openSslContext = Long.valueOf(0); + + private boolean tls13RenegotiationAvailable = false; + + // Configuration properties + + // Internal + private String[] enabledCiphers; + private String[] enabledProtocols; + private ObjectName oname; + // Need to know if TLS 1.3 has been explicitly requested as a warning needs + // to generated if it is explicitly requested for a JVM that does not + // support it. Uses a set so it is extensible for TLS 1.4 etc. + private Set explicitlyRequestedProtocols = new HashSet<>(); + // Nested + private SSLHostConfigCertificate defaultCertificate = null; + private Set certificates = new LinkedHashSet<>(4); + // Common + private String certificateRevocationListFile; + private CertificateVerification certificateVerification = CertificateVerification.NONE; + private int certificateVerificationDepth = 10; + // Used to track if certificateVerificationDepth has been explicitly set + private boolean certificateVerificationDepthConfigured = false; + private String ciphers = DEFAULT_TLS_CIPHERS; + private LinkedHashSet cipherList = null; + private List jsseCipherNames = null; + private boolean honorCipherOrder = false; + private Set protocols = new HashSet<>(); + // Values <0 mean use the implementation default + private int sessionCacheSize = -1; + private int sessionTimeout = 86400; + // JSSE + private String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + private boolean revocationEnabled = false; + private String sslProtocol = Constants.SSL_PROTO_TLS; + private String trustManagerClassName; + private String truststoreAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + private String truststoreFile = System.getProperty("javax.net.ssl.trustStore"); + private String truststorePassword = System.getProperty("javax.net.ssl.trustStorePassword"); + private String truststoreProvider = System.getProperty("javax.net.ssl.trustStoreProvider"); + private String truststoreType = System.getProperty("javax.net.ssl.trustStoreType"); + private transient KeyStore truststore = null; + // OpenSSL + private String certificateRevocationListPath; + private String caCertificateFile; + private String caCertificatePath; + private boolean disableCompression = true; + private boolean disableSessionTickets = false; + private boolean insecureRenegotiation = false; + private OpenSSLConf openSslConf = null; + + public SSLHostConfig() { + // Set defaults that can't be (easily) set when defining the fields. + setProtocols(Constants.SSL_PROTO_ALL); + } + + + public boolean isTls13RenegotiationAvailable() { + return tls13RenegotiationAvailable; + } + + + public void setTls13RenegotiationAvailable(boolean tls13RenegotiationAvailable) { + this.tls13RenegotiationAvailable = tls13RenegotiationAvailable; + } + + + public Long getOpenSslConfContext() { + return openSslConfContext; + } + + + public void setOpenSslConfContext(Long openSslConfContext) { + this.openSslConfContext = openSslConfContext; + } + + + public Long getOpenSslContext() { + return openSslContext; + } + + + public void setOpenSslContext(Long openSslContext) { + this.openSslContext = openSslContext; + } + + + // Expose in String form for JMX + public String getConfigType() { + return configType.name(); + } + + + /** + * Set property which belongs to the specified configuration type. + * @param name the property name + * @param configType the configuration type + * @return true if the property belongs to the current configuration, + * and false otherwise + */ + boolean setProperty(String name, Type configType) { + if (this.configType == null) { + this.configType = configType; + } else { + if (configType != this.configType) { + log.warn(sm.getString("sslHostConfig.mismatch", + name, getHostName(), configType, this.configType)); + return false; + } + } + return true; + } + + + // ----------------------------------------------------- Internal properties + + /** + * @see SSLUtil#getEnabledProtocols() + * + * @return The protocols enabled for this TLS virtual host + */ + public String[] getEnabledProtocols() { + return enabledProtocols; + } + + + public void setEnabledProtocols(String[] enabledProtocols) { + this.enabledProtocols = enabledProtocols; + } + + + /** + * @see SSLUtil#getEnabledCiphers() + * + * @return The ciphers enabled for this TLS virtual host + */ + public String[] getEnabledCiphers() { + return enabledCiphers; + } + + + public void setEnabledCiphers(String[] enabledCiphers) { + this.enabledCiphers = enabledCiphers; + } + + + public ObjectName getObjectName() { + return oname; + } + + + public void setObjectName(ObjectName oname) { + this.oname = oname; + } + + + // ------------------------------------------- Nested configuration elements + + private void registerDefaultCertificate() { + if (defaultCertificate == null) { + SSLHostConfigCertificate defaultCertificate = new SSLHostConfigCertificate( + this, SSLHostConfigCertificate.Type.UNDEFINED); + addCertificate(defaultCertificate); + this.defaultCertificate = defaultCertificate; + } + } + + + public void addCertificate(SSLHostConfigCertificate certificate) { + // Need to make sure that if there is more than one certificate, none of + // them have a type of undefined. + if (certificates.size() == 0) { + certificates.add(certificate); + return; + } + + if (certificates.size() == 1 && + certificates.iterator().next().getType() == SSLHostConfigCertificate.Type.UNDEFINED || + certificate.getType() == SSLHostConfigCertificate.Type.UNDEFINED) { + // Invalid config + throw new IllegalArgumentException(sm.getString("sslHostConfig.certificate.notype")); + } + + certificates.add(certificate); + } + + + public OpenSSLConf getOpenSslConf() { + return openSslConf; + } + + + public void setOpenSslConf(OpenSSLConf conf) { + if (conf == null) { + throw new IllegalArgumentException(sm.getString("sslHostConfig.opensslconf.null")); + } else if (openSslConf != null) { + throw new IllegalArgumentException(sm.getString("sslHostConfig.opensslconf.alreadySet")); + } + setProperty("", Type.OPENSSL); + openSslConf = conf; + } + + + public Set getCertificates() { + return getCertificates(false); + } + + + public Set getCertificates(boolean createDefaultIfEmpty) { + if (certificates.size() == 0 && createDefaultIfEmpty) { + registerDefaultCertificate(); + } + return certificates; + } + + + // ----------------------------------------- Common configuration properties + + public void setCertificateRevocationListFile(String certificateRevocationListFile) { + this.certificateRevocationListFile = certificateRevocationListFile; + } + + + public String getCertificateRevocationListFile() { + return certificateRevocationListFile; + } + + + public void setCertificateVerification(String certificateVerification) { + try { + this.certificateVerification = + CertificateVerification.fromString(certificateVerification); + } catch (IllegalArgumentException iae) { + // If the specified value is not recognised, default to the + // strictest possible option. + this.certificateVerification = CertificateVerification.REQUIRED; + throw iae; + } + } + + + public CertificateVerification getCertificateVerification() { + return certificateVerification; + } + + + public void setCertificateVerificationAsString(String certificateVerification) { + setCertificateVerification(certificateVerification); + } + + + public String getCertificateVerificationAsString() { + return certificateVerification.toString(); + } + + + public void setCertificateVerificationDepth(int certificateVerificationDepth) { + this.certificateVerificationDepth = certificateVerificationDepth; + certificateVerificationDepthConfigured = true; + } + + + public int getCertificateVerificationDepth() { + return certificateVerificationDepth; + } + + + public boolean isCertificateVerificationDepthConfigured() { + return certificateVerificationDepthConfigured; + } + + + /** + * Set the new cipher configuration. Note: Regardless of the format used to + * set the configuration, it is always stored in OpenSSL format. + * + * @param ciphersList The new cipher configuration in OpenSSL or JSSE format + */ + public void setCiphers(String ciphersList) { + // Ciphers is stored in OpenSSL format. Convert the provided value if + // necessary. + if (ciphersList != null && !ciphersList.contains(":")) { + StringBuilder sb = new StringBuilder(); + // Not obviously in OpenSSL format. May be a single OpenSSL or JSSE + // cipher name. May be a comma separated list of cipher names + String ciphers[] = ciphersList.split(","); + for (String cipher : ciphers) { + String trimmed = cipher.trim(); + if (trimmed.length() > 0) { + String openSSLName = OpenSSLCipherConfigurationParser.jsseToOpenSSL(trimmed); + if (openSSLName == null) { + // Not a JSSE name. Maybe an OpenSSL name or alias + openSSLName = trimmed; + } + if (sb.length() > 0) { + sb.append(':'); + } + sb.append(openSSLName); + } + } + this.ciphers = sb.toString(); + } else { + this.ciphers = ciphersList; + } + this.cipherList = null; + this.jsseCipherNames = null; + } + + + /** + * @return An OpenSSL cipher string for the current configuration. + */ + public String getCiphers() { + return ciphers; + } + + + public LinkedHashSet getCipherList() { + if (cipherList == null) { + cipherList = OpenSSLCipherConfigurationParser.parse(getCiphers()); + } + return cipherList; + } + + + /** + * Obtain the list of JSSE cipher names for the current configuration. + * Ciphers included in the configuration but not supported by JSSE will be + * excluded from this list. + * + * @return A list of the JSSE cipher names + */ + public List getJsseCipherNames() { + if (jsseCipherNames == null) { + jsseCipherNames = OpenSSLCipherConfigurationParser.convertForJSSE(getCipherList()); + } + return jsseCipherNames; + } + + + public void setHonorCipherOrder(boolean honorCipherOrder) { + this.honorCipherOrder = honorCipherOrder; + } + + + public boolean getHonorCipherOrder() { + return honorCipherOrder; + } + + + public void setHostName(String hostName) { + this.hostName = hostName.toLowerCase(Locale.ENGLISH); + } + + + /** + * @return The host name associated with this SSL configuration - always in + * lower case. + */ + public String getHostName() { + return hostName; + } + + + public void setProtocols(String input) { + protocols.clear(); + explicitlyRequestedProtocols.clear(); + + // List of protocol names, separated by ",", "+" or "-". + // Semantics is adding ("+") or removing ("-") from left + // to right, starting with an empty protocol set. + // Tokens are individual protocol names or "all" for a + // default set of supported protocols. + // Separator "," is only kept for compatibility and has the + // same semantics as "+", except that it warns about a potentially + // missing "+" or "-". + + // Split using a positive lookahead to keep the separator in + // the capture so we can check which case it is. + for (String value: input.split("(?=[-+,])")) { + String trimmed = value.trim(); + // Ignore token which only consists of prefix character + if (trimmed.length() > 1) { + if (trimmed.charAt(0) == '+') { + trimmed = trimmed.substring(1).trim(); + if (trimmed.equalsIgnoreCase(Constants.SSL_PROTO_ALL)) { + protocols.addAll(SSL_PROTO_ALL_SET); + } else { + protocols.add(trimmed); + explicitlyRequestedProtocols.add(trimmed); + } + } else if (trimmed.charAt(0) == '-') { + trimmed = trimmed.substring(1).trim(); + if (trimmed.equalsIgnoreCase(Constants.SSL_PROTO_ALL)) { + protocols.removeAll(SSL_PROTO_ALL_SET); + } else { + protocols.remove(trimmed); + explicitlyRequestedProtocols.remove(trimmed); + } + } else { + if (trimmed.charAt(0) == ',') { + trimmed = trimmed.substring(1).trim(); + } + if (!protocols.isEmpty()) { + log.warn(sm.getString("sslHostConfig.prefix_missing", + trimmed, getHostName())); + } + if (trimmed.equalsIgnoreCase(Constants.SSL_PROTO_ALL)) { + protocols.addAll(SSL_PROTO_ALL_SET); + } else { + protocols.add(trimmed); + explicitlyRequestedProtocols.add(trimmed); + } + } + } + } + } + + + public Set getProtocols() { + return protocols; + } + + + boolean isExplicitlyRequestedProtocol(String protocol) { + return explicitlyRequestedProtocols.contains(protocol); + } + + + public void setSessionCacheSize(int sessionCacheSize) { + this.sessionCacheSize = sessionCacheSize; + } + + + public int getSessionCacheSize() { + return sessionCacheSize; + } + + + public void setSessionTimeout(int sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + + public int getSessionTimeout() { + return sessionTimeout; + } + + + // ---------------------------------- JSSE specific configuration properties + + public void setKeyManagerAlgorithm(String keyManagerAlgorithm) { + setProperty("keyManagerAlgorithm", Type.JSSE); + this.keyManagerAlgorithm = keyManagerAlgorithm; + } + + + public String getKeyManagerAlgorithm() { + return keyManagerAlgorithm; + } + + + public void setRevocationEnabled(boolean revocationEnabled) { + setProperty("revocationEnabled", Type.JSSE); + this.revocationEnabled = revocationEnabled; + } + + + public boolean getRevocationEnabled() { + return revocationEnabled; + } + + + public void setSslProtocol(String sslProtocol) { + setProperty("sslProtocol", Type.JSSE); + this.sslProtocol = sslProtocol; + } + + + public String getSslProtocol() { + return sslProtocol; + } + + + public void setTrustManagerClassName(String trustManagerClassName) { + setProperty("trustManagerClassName", Type.JSSE); + this.trustManagerClassName = trustManagerClassName; + } + + + public String getTrustManagerClassName() { + return trustManagerClassName; + } + + + public void setTruststoreAlgorithm(String truststoreAlgorithm) { + setProperty("truststoreAlgorithm", Type.JSSE); + this.truststoreAlgorithm = truststoreAlgorithm; + } + + + public String getTruststoreAlgorithm() { + return truststoreAlgorithm; + } + + + public void setTruststoreFile(String truststoreFile) { + setProperty("truststoreFile", Type.JSSE); + this.truststoreFile = truststoreFile; + } + + + public String getTruststoreFile() { + return truststoreFile; + } + + + public void setTruststorePassword(String truststorePassword) { + setProperty("truststorePassword", Type.JSSE); + this.truststorePassword = truststorePassword; + } + + + public String getTruststorePassword() { + return truststorePassword; + } + + + public void setTruststoreProvider(String truststoreProvider) { + setProperty("truststoreProvider", Type.JSSE); + this.truststoreProvider = truststoreProvider; + } + + + public String getTruststoreProvider() { + if (truststoreProvider == null) { + Set certificates = getCertificates(); + if (certificates.size() == 1) { + return certificates.iterator().next().getCertificateKeystoreProvider(); + } + return SSLHostConfigCertificate.DEFAULT_KEYSTORE_PROVIDER; + } else { + return truststoreProvider; + } + } + + + public void setTruststoreType(String truststoreType) { + setProperty("truststoreType", Type.JSSE); + this.truststoreType = truststoreType; + } + + + public String getTruststoreType() { + if (truststoreType == null) { + Set certificates = getCertificates(); + if (certificates.size() == 1) { + String keystoreType = certificates.iterator().next().getCertificateKeystoreType(); + // Don't use keystore type as the default if we know it is not + // going to be used as a trust store type + if (!"PKCS12".equalsIgnoreCase(keystoreType)) { + return keystoreType; + } + } + return SSLHostConfigCertificate.DEFAULT_KEYSTORE_TYPE; + } else { + return truststoreType; + } + } + + + public void setTrustStore(KeyStore truststore) { + this.truststore = truststore; + } + + + public KeyStore getTruststore() throws IOException { + KeyStore result = truststore; + if (result == null) { + if (truststoreFile != null){ + try { + result = SSLUtilBase.getStore(getTruststoreType(), getTruststoreProvider(), + getTruststoreFile(), getTruststorePassword(), null); + } catch (IOException ioe) { + Throwable cause = ioe.getCause(); + if (cause instanceof UnrecoverableKeyException) { + // Log a warning we had a password issue + log.warn(sm.getString("sslHostConfig.invalid_truststore_password"), + cause); + // Re-try + result = SSLUtilBase.getStore(getTruststoreType(), getTruststoreProvider(), + getTruststoreFile(), null, null); + } else { + // Something else went wrong - re-throw + throw ioe; + } + } + } + } + return result; + } + + + // ------------------------------- OpenSSL specific configuration properties + + public void setCertificateRevocationListPath(String certificateRevocationListPath) { + setProperty("certificateRevocationListPath", Type.OPENSSL); + this.certificateRevocationListPath = certificateRevocationListPath; + } + + + public String getCertificateRevocationListPath() { + return certificateRevocationListPath; + } + + + public void setCaCertificateFile(String caCertificateFile) { + if (setProperty("caCertificateFile", Type.OPENSSL)) { + // Reset default JSSE trust store if not a JSSE configuration + if (truststoreFile != null) { + truststoreFile = null; + } + } + this.caCertificateFile = caCertificateFile; + } + + + public String getCaCertificateFile() { + return caCertificateFile; + } + + + public void setCaCertificatePath(String caCertificatePath) { + if (setProperty("caCertificatePath", Type.OPENSSL)) { + // Reset default JSSE trust store if not a JSSE configuration + if (truststoreFile != null) { + truststoreFile = null; + } + } + this.caCertificatePath = caCertificatePath; + } + + + public String getCaCertificatePath() { + return caCertificatePath; + } + + + public void setDisableCompression(boolean disableCompression) { + setProperty("disableCompression", Type.OPENSSL); + this.disableCompression = disableCompression; + } + + + public boolean getDisableCompression() { + return disableCompression; + } + + + public void setDisableSessionTickets(boolean disableSessionTickets) { + setProperty("disableSessionTickets", Type.OPENSSL); + this.disableSessionTickets = disableSessionTickets; + } + + + public boolean getDisableSessionTickets() { + return disableSessionTickets; + } + + + public void setInsecureRenegotiation(boolean insecureRenegotiation) { + setProperty("insecureRenegotiation", Type.OPENSSL); + this.insecureRenegotiation = insecureRenegotiation; + } + + + public boolean getInsecureRenegotiation() { + return insecureRenegotiation; + } + + + // --------------------------------------------------------- Support methods + + public Set certificatesExpiringBefore(Date date) { + Set result = new HashSet<>(); + Set sslHostConfigCertificates = getCertificates(); + for (SSLHostConfigCertificate sslHostConfigCertificate : sslHostConfigCertificates) { + SSLContext sslContext = sslHostConfigCertificate.getSslContext(); + if (sslContext != null) { + String alias = sslHostConfigCertificate.getCertificateKeyAlias(); + if (alias == null) { + alias = SSLUtilBase.DEFAULT_KEY_ALIAS; + } + X509Certificate[] certificates = sslContext.getCertificateChain(alias); + if (certificates != null && certificates.length > 0) { + X509Certificate certificate = certificates[0]; + Date expirationDate = certificate.getNotAfter(); + if (date.after(expirationDate)) { + result.add(certificate); + } + } + } + } + return result; + } + + + public static String adjustRelativePath(String path) throws FileNotFoundException { + // Empty or null path can't point to anything useful. The assumption is + // that the value is deliberately empty / null so leave it that way. + if (path == null || path.length() == 0) { + return path; + } + String newPath = path; + File f = new File(newPath); + if ( !f.isAbsolute()) { + newPath = System.getProperty(Constants.CATALINA_BASE_PROP) + File.separator + newPath; + f = new File(newPath); + } + if (!f.exists()) { + throw new FileNotFoundException(sm.getString("sslHostConfig.fileNotFound", newPath)); + } + return newPath; + } + + + // ----------------------------------------------------------- Inner classes + + public enum Type { + JSSE, + OPENSSL + } + + + public enum CertificateVerification { + NONE(false), + OPTIONAL_NO_CA(true), + OPTIONAL(true), + REQUIRED(false); + + private final boolean optional; + + CertificateVerification(boolean optional) { + this.optional = optional; + } + + public boolean isOptional() { + return optional; + } + + public static CertificateVerification fromString(String value) { + if ("true".equalsIgnoreCase(value) || + "yes".equalsIgnoreCase(value) || + "require".equalsIgnoreCase(value) || + "required".equalsIgnoreCase(value)) { + return REQUIRED; + } else if ("optional".equalsIgnoreCase(value) || + "want".equalsIgnoreCase(value)) { + return OPTIONAL; + } else if ("optionalNoCA".equalsIgnoreCase(value) || + "optional_no_ca".equalsIgnoreCase(value)) { + return OPTIONAL_NO_CA; + } else if ("false".equalsIgnoreCase(value) || + "no".equalsIgnoreCase(value) || + "none".equalsIgnoreCase(value)) { + return NONE; + } else { + // Could be a typo. Don't default to NONE since that is not + // secure. Force user to fix config. Could default to REQUIRED + // instead. + throw new IllegalArgumentException( + sm.getString("sslHostConfig.certificateVerificationInvalid", value)); + } + } + } +} diff --git a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java new file mode 100644 index 0000000..e50b4b0 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java @@ -0,0 +1,347 @@ +/* + * 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.tomcat.util.net; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.management.ObjectName; +import javax.net.ssl.X509KeyManager; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.openssl.ciphers.Authentication; +import org.apache.tomcat.util.res.StringManager; + +public class SSLHostConfigCertificate implements Serializable { + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(SSLHostConfigCertificate.class); + private static final StringManager sm = StringManager.getManager(SSLHostConfigCertificate.class); + + public static final Type DEFAULT_TYPE = Type.UNDEFINED; + + static final String DEFAULT_KEYSTORE_PROVIDER = System.getProperty("javax.net.ssl.keyStoreProvider"); + static final String DEFAULT_KEYSTORE_TYPE = System.getProperty("javax.net.ssl.keyStoreType", "JKS"); + private static final String DEFAULT_KEYSTORE_FILE = + System.getProperty("user.home") + File.separator + ".keystore"; + private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit"; + + // Internal + private ObjectName oname; + + /* + * OpenSSL can handle multiple certs in a single config so the reference to the context is at the virtual host + * level. JSSE can't so the reference is held here on the certificate. Typically, the SSLContext is generated from + * the configuration but, particularly in embedded scenarios, it can be provided directly. + */ + private transient volatile SSLContext sslContextProvided; + private transient volatile SSLContext sslContextGenerated; + + + // Common + private final SSLHostConfig sslHostConfig; + private final Type type; + private String certificateKeyPassword = null; + private String certificateKeyPasswordFile = null; + + // JSSE + private String certificateKeyAlias; + private String certificateKeystorePassword = DEFAULT_KEYSTORE_PASSWORD; + private String certificateKeystorePasswordFile = null; + private String certificateKeystoreFile = DEFAULT_KEYSTORE_FILE; + private String certificateKeystoreProvider = DEFAULT_KEYSTORE_PROVIDER; + private String certificateKeystoreType = DEFAULT_KEYSTORE_TYPE; + private transient KeyStore certificateKeystore = null; + private transient X509KeyManager certificateKeyManager = null; + + // OpenSSL + private String certificateChainFile; + private String certificateFile; + private String certificateKeyFile; + + // Certificate store type + private StoreType storeType = null; + + public SSLHostConfigCertificate() { + this(null, Type.UNDEFINED); + } + + public SSLHostConfigCertificate(SSLHostConfig sslHostConfig, Type type) { + this.sslHostConfig = sslHostConfig; + this.type = type; + } + + + public SSLContext getSslContext() { + if (sslContextProvided != null) { + return sslContextProvided; + } + return sslContextGenerated; + } + + + public void setSslContext(SSLContext sslContext) { + this.sslContextProvided = sslContext; + } + + + public SSLContext getSslContextGenerated() { + return sslContextGenerated; + } + + + void setSslContextGenerated(SSLContext sslContext) { + this.sslContextGenerated = sslContext; + } + + + public SSLHostConfig getSSLHostConfig() { + return sslHostConfig; + } + + + // Internal + + public ObjectName getObjectName() { + return oname; + } + + + public void setObjectName(ObjectName oname) { + this.oname = oname; + } + + + // Common + + public Type getType() { + return type; + } + + + public String getCertificateKeyPassword() { + return certificateKeyPassword; + } + + + public void setCertificateKeyPassword(String certificateKeyPassword) { + this.certificateKeyPassword = certificateKeyPassword; + } + + + public String getCertificateKeyPasswordFile() { + return certificateKeyPasswordFile; + } + + + public void setCertificateKeyPasswordFile(String certificateKeyPasswordFile) { + this.certificateKeyPasswordFile = certificateKeyPasswordFile; + } + + + // JSSE + + public void setCertificateKeyAlias(String certificateKeyAlias) { + sslHostConfig.setProperty( + "Certificate.certificateKeyAlias", SSLHostConfig.Type.JSSE); + this.certificateKeyAlias = certificateKeyAlias; + } + + + public String getCertificateKeyAlias() { + return certificateKeyAlias; + } + + + public void setCertificateKeystoreFile(String certificateKeystoreFile) { + sslHostConfig.setProperty( + "Certificate.certificateKeystoreFile", SSLHostConfig.Type.JSSE); + setStoreType("Certificate.certificateKeystoreFile", StoreType.KEYSTORE); + this.certificateKeystoreFile = certificateKeystoreFile; + } + + + public String getCertificateKeystoreFile() { + return certificateKeystoreFile; + } + + + public void setCertificateKeystorePassword(String certificateKeystorePassword) { + sslHostConfig.setProperty( + "Certificate.certificateKeystorePassword", SSLHostConfig.Type.JSSE); + setStoreType("Certificate.certificateKeystorePassword", StoreType.KEYSTORE); + this.certificateKeystorePassword = certificateKeystorePassword; + } + + + public String getCertificateKeystorePassword() { + return certificateKeystorePassword; + } + + + public void setCertificateKeystorePasswordFile(String certificateKeystorePasswordFile) { + sslHostConfig.setProperty( + "Certificate.certificateKeystorePasswordFile", SSLHostConfig.Type.JSSE); + setStoreType("Certificate.certificateKeystorePasswordFile", StoreType.KEYSTORE); + this.certificateKeystorePasswordFile = certificateKeystorePasswordFile; + } + + + public String getCertificateKeystorePasswordFile() { + return certificateKeystorePasswordFile; + } + + + public void setCertificateKeystoreProvider(String certificateKeystoreProvider) { + sslHostConfig.setProperty( + "Certificate.certificateKeystoreProvider", SSLHostConfig.Type.JSSE); + setStoreType("Certificate.certificateKeystoreProvider", StoreType.KEYSTORE); + this.certificateKeystoreProvider = certificateKeystoreProvider; + } + + + public String getCertificateKeystoreProvider() { + return certificateKeystoreProvider; + } + + + public void setCertificateKeystoreType(String certificateKeystoreType) { + sslHostConfig.setProperty( + "Certificate.certificateKeystoreType", SSLHostConfig.Type.JSSE); + setStoreType("Certificate.certificateKeystoreType", StoreType.KEYSTORE); + this.certificateKeystoreType = certificateKeystoreType; + } + + + public String getCertificateKeystoreType() { + return certificateKeystoreType; + } + + + public void setCertificateKeystore(KeyStore certificateKeystore) { + this.certificateKeystore = certificateKeystore; + if (certificateKeystore != null) { + setCertificateKeystoreType(certificateKeystore.getType()); + } + } + + + public KeyStore getCertificateKeystore() throws IOException { + KeyStore result = certificateKeystore; + + if (result == null && storeType == StoreType.KEYSTORE) { + result = SSLUtilBase.getStore(getCertificateKeystoreType(), + getCertificateKeystoreProvider(), getCertificateKeystoreFile(), + getCertificateKeystorePassword(), getCertificateKeystorePasswordFile()); + } + + return result; + } + + + public void setCertificateKeyManager(X509KeyManager certificateKeyManager) { + this.certificateKeyManager = certificateKeyManager; + } + + + public X509KeyManager getCertificateKeyManager() { + return certificateKeyManager; + } + + + // OpenSSL + + public void setCertificateChainFile(String certificateChainFile) { + setStoreType("Certificate.certificateChainFile", StoreType.PEM); + this.certificateChainFile = certificateChainFile; + } + + + public String getCertificateChainFile() { + return certificateChainFile; + } + + + public void setCertificateFile(String certificateFile) { + setStoreType("Certificate.certificateFile", StoreType.PEM); + this.certificateFile = certificateFile; + } + + + public String getCertificateFile() { + return certificateFile; + } + + + public void setCertificateKeyFile(String certificateKeyFile) { + setStoreType("Certificate.certificateKeyFile", StoreType.PEM); + this.certificateKeyFile = certificateKeyFile; + } + + + public String getCertificateKeyFile() { + return certificateKeyFile; + } + + + private void setStoreType(String name, StoreType type) { + if (storeType == null) { + storeType = type; + } else if (storeType != type) { + log.warn(sm.getString("sslHostConfigCertificate.mismatch", + name, sslHostConfig.getHostName(), type, this.storeType)); + } + } + + StoreType getStoreType() { + return storeType; + } + + + public enum Type { + + UNDEFINED, + RSA(Authentication.RSA), + DSA(Authentication.DSS), + EC(Authentication.ECDH, Authentication.ECDSA); + + private final Set compatibleAuthentications; + + Type(Authentication... authentications) { + compatibleAuthentications = new HashSet<>(); + if (authentications != null) { + compatibleAuthentications.addAll(Arrays.asList(authentications)); + } + } + + public boolean isCompatibleWith(Authentication au) { + return compatibleAuthentications.contains(au); + } + } + + enum StoreType { + KEYSTORE, + PEM + } +} diff --git a/java/org/apache/tomcat/util/net/SSLImplementation.java b/java/org/apache/tomcat/util/net/SSLImplementation.java new file mode 100644 index 0000000..8f9dfd0 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SSLImplementation.java @@ -0,0 +1,82 @@ +/* + * 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.tomcat.util.net; + +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLSession; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.jsse.JSSEImplementation; +import org.apache.tomcat.util.res.StringManager; + +/** + * Provides a factory and base implementation for the Tomcat specific mechanism + * that allows alternative SSL/TLS implementations to be used without requiring + * the implementation of a full JSSE provider. + */ +public abstract class SSLImplementation { + + private static final Log logger = LogFactory.getLog(SSLImplementation.class); + private static final StringManager sm = StringManager.getManager(SSLImplementation.class); + + /** + * Obtain an instance (not a singleton) of the implementation with the given + * class name. + * + * @param className The class name of the required implementation or null to + * use the default (currently {@link JSSEImplementation}. + * + * @return An instance of the required implementation + * + * @throws ClassNotFoundException If an instance of the requested class + * cannot be created + */ + public static SSLImplementation getInstance(String className) + throws ClassNotFoundException { + if (className == null) { + return new JSSEImplementation(); + } + + try { + Class clazz = Class.forName(className); + return (SSLImplementation) clazz.getConstructor().newInstance(); + } catch (Exception e) { + String msg = sm.getString("sslImplementation.cnfe", className); + if (logger.isDebugEnabled()) { + logger.debug(msg, e); + } + throw new ClassNotFoundException(msg, e); + } + } + + /** + * Obtain an instance of SSLSupport. + * + * @param session The SSL session + * @param additionalAttributes Additional SSL attributes that are not + * available from the session. + * + * @return An instance of SSLSupport based on the given session and the + * provided additional attributes + */ + public abstract SSLSupport getSSLSupport(SSLSession session, Map> additionalAttributes); + + public abstract SSLUtil getSSLUtil(SSLHostConfigCertificate certificate); +} diff --git a/java/org/apache/tomcat/util/net/SSLSessionManager.java b/java/org/apache/tomcat/util/net/SSLSessionManager.java new file mode 100644 index 0000000..17e1dd9 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SSLSessionManager.java @@ -0,0 +1,28 @@ +/* + * 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.tomcat.util.net; + +/** + * Defines an interface used to manage SSL sessions. The manager operates on a + * single session. + */ +public interface SSLSessionManager { + /** + * Invalidate the SSL session + */ + void invalidateSession(); +} diff --git a/java/org/apache/tomcat/util/net/SSLSupport.java b/java/org/apache/tomcat/util/net/SSLSupport.java new file mode 100644 index 0000000..d98e205 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SSLSupport.java @@ -0,0 +1,164 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.security.cert.X509Certificate; + +/** + * Defines an interface to interact with SSL sessions. + */ +public interface SSLSupport { + /** + * The Request attribute key for the cipher suite. + */ + String CIPHER_SUITE_KEY = + "jakarta.servlet.request.cipher_suite"; + + /** + * The Request attribute key for the key size. + */ + String KEY_SIZE_KEY = "jakarta.servlet.request.key_size"; + + /** + * The Request attribute key for the client certificate chain. + */ + String CERTIFICATE_KEY = + "jakarta.servlet.request.X509Certificate"; + + /** + * The Request attribute key for the session id. + * This one is a Tomcat extension to the Servlet spec. + */ + String SESSION_ID_KEY = + "jakarta.servlet.request.ssl_session_id"; + + /** + * The request attribute key for the session manager. + * This one is a Tomcat extension to the Servlet spec. + */ + String SESSION_MGR = + "jakarta.servlet.request.ssl_session_mgr"; + + /** + * The request attribute key under which the String indicating the protocol + * that created the SSL socket is recorded - e.g. TLSv1 or TLSv1.2 etc. + */ + String PROTOCOL_VERSION_KEY = + "org.apache.tomcat.util.net.secure_protocol_version"; + + /** + * The request attribute key under which the String indicating the ciphers + * requested by the client are recorded. + */ + String REQUESTED_CIPHERS_KEY = + "org.apache.tomcat.util.net.secure_requested_ciphers"; + + /** + * The request attribute key under which the String indicating the protocols + * requested by the client are recorded. + */ + String REQUESTED_PROTOCOL_VERSIONS_KEY = + "org.apache.tomcat.util.net.secure_requested_protocol_versions"; + + /** + * The cipher suite being used on this connection. + * + * @return The name of the cipher suite as returned by the SSL/TLS + * implementation + * + * @throws IOException If an error occurs trying to obtain the cipher suite + */ + String getCipherSuite() throws IOException; + + /** + * The client certificate chain (if any). + * + * @return The certificate chain presented by the client with the peer's + * certificate first, followed by those of any certificate + * authorities + * + * @throws IOException If an error occurs trying to obtain the certificate + * chain + */ + X509Certificate[] getPeerCertificateChain() throws IOException; + + /** + * The server certificate chain (if any) that were sent to the peer. + * + * @return The certificate chain sent with the server + * certificate first, followed by those of any certificate + * authorities + */ + default X509Certificate[] getLocalCertificateChain() { + return null; + } + + /** + * Get the keysize. + * + * What we're supposed to put here is ill-defined by the + * Servlet spec (S 4.7 again). There are at least 4 potential + * values that might go here: + * + * (a) The size of the encryption key + * (b) The size of the MAC key + * (c) The size of the key-exchange key + * (d) The size of the signature key used by the server + * + * Unfortunately, all of these values are nonsensical. + * + * @return The effective key size for the current cipher suite + * + * @throws IOException If an error occurs trying to obtain the key size + */ + Integer getKeySize() throws IOException; + + /** + * The current session Id. + * + * @return The current SSL/TLS session ID + * + * @throws IOException If an error occurs trying to obtain the session ID + */ + String getSessionId() throws IOException; + + /** + * @return the protocol String indicating how the SSL socket was created + * e.g. TLSv1 or TLSv1.2 etc. + * + * @throws IOException If an error occurs trying to obtain the protocol + * information from the socket + */ + String getProtocol() throws IOException; + + /** + * @return the list of SSL/TLS protocol versions requested by the client + * + * @throws IOException If an error occurs trying to obtain the client + * requested protocol information from the socket + */ + String getRequestedProtocols() throws IOException; + + /** + * @return the list of SSL/TLS ciphers requested by the client + * + * @throws IOException If an error occurs trying to obtain the client + * request cipher information from the socket + */ + String getRequestedCiphers() throws IOException; +} diff --git a/java/org/apache/tomcat/util/net/SSLUtil.java b/java/org/apache/tomcat/util/net/SSLUtil.java new file mode 100644 index 0000000..c2272d6 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SSLUtil.java @@ -0,0 +1,82 @@ +/* + * 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.tomcat.util.net; + +import java.util.List; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.TrustManager; + +/** + * Provides a common interface for {@link SSLImplementation}s to create the + * necessary JSSE implementation objects for TLS connections created via the + * JSSE API. + */ +public interface SSLUtil { + + SSLContext createSSLContext(List negotiableProtocols) throws Exception; + + KeyManager[] getKeyManagers() throws Exception; + + TrustManager[] getTrustManagers() throws Exception; + + void configureSessionContext(SSLSessionContext sslSessionContext); + + /** + * The set of enabled protocols is the intersection of the implemented + * protocols and the configured protocols. If no protocols are explicitly + * configured, then all of the implemented protocols will be included in the + * returned array. + * + * @return The protocols currently enabled and available for clients to + * select from for the associated connection + * + * @throws IllegalArgumentException If there is no intersection between the + * implemented and configured protocols + */ + String[] getEnabledProtocols() throws IllegalArgumentException; + + /** + * The set of enabled ciphers is the intersection of the implemented ciphers + * and the configured ciphers. If no ciphers are explicitly configured, then + * the default ciphers will be included in the returned array. + *

    + * The ciphers used during the TLS handshake may be further restricted by + * the {@link #getEnabledProtocols()} and the certificates. + * + * @return The ciphers currently enabled and available for clients to select + * from for the associated connection + * + * @throws IllegalArgumentException If there is no intersection between the + * implemented and configured ciphers + */ + String[] getEnabledCiphers() throws IllegalArgumentException; + + /** + * Optional interface that can be implemented by + * {@link javax.net.ssl.SSLEngine}s to indicate that they support ALPN and + * can provided the protocol agreed with the client. + */ + interface ProtocolInfo { + /** + * ALPN information. + * @return the protocol selected using ALPN + */ + String getNegotiatedProtocol(); + } +} diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java new file mode 100644 index 0000000..99d214c --- /dev/null +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java @@ -0,0 +1,586 @@ +/* + * 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.tomcat.util.net; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.DomainLoadStoreParameter; +import java.security.Key; +import java.security.KeyStore; +import java.security.cert.CRL; +import java.security.cert.CRLException; +import java.security.cert.CertPathParameters; +import java.security.cert.CertStore; +import java.security.cert.CertStoreParameters; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import javax.net.ssl.CertPathTrustManagerParameters; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509KeyManager; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.net.jsse.JSSEKeyManager; +import org.apache.tomcat.util.net.jsse.PEMFile; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.KeyStoreUtil; + +/** + * Common base class for {@link SSLUtil} implementations. + */ +public abstract class SSLUtilBase implements SSLUtil { + + private static final Log log = LogFactory.getLog(SSLUtilBase.class); + private static final StringManager sm = StringManager.getManager(SSLUtilBase.class); + + public static final String DEFAULT_KEY_ALIAS = "tomcat"; + + protected final SSLHostConfig sslHostConfig; + protected final SSLHostConfigCertificate certificate; + + private final String[] enabledProtocols; + private final String[] enabledCiphers; + + + protected SSLUtilBase(SSLHostConfigCertificate certificate) { + this(certificate, true); + } + + + protected SSLUtilBase(SSLHostConfigCertificate certificate, boolean warnTls13) { + this.certificate = certificate; + this.sslHostConfig = certificate.getSSLHostConfig(); + + // Calculate the enabled protocols + Set configuredProtocols = sslHostConfig.getProtocols(); + Set implementedProtocols = getImplementedProtocols(); + // If TLSv1.3 is not implemented and not explicitly requested we can + // ignore it. It is included in the defaults so it may be configured. + if (!implementedProtocols.contains(Constants.SSL_PROTO_TLSv1_3) && + !sslHostConfig.isExplicitlyRequestedProtocol(Constants.SSL_PROTO_TLSv1_3)) { + configuredProtocols.remove(Constants.SSL_PROTO_TLSv1_3); + } + // Newer JREs are dropping support for SSLv2Hello. If it is not + // implemented and not explicitly requested we can ignore it. It is + // included in the defaults so it may be configured. + if (!implementedProtocols.contains(Constants.SSL_PROTO_SSLv2Hello) && + !sslHostConfig.isExplicitlyRequestedProtocol(Constants.SSL_PROTO_SSLv2Hello)) { + configuredProtocols.remove(Constants.SSL_PROTO_SSLv2Hello); + } + + List enabledProtocols = + getEnabled("protocols", getLog(), warnTls13, configuredProtocols, implementedProtocols); + if (enabledProtocols.contains("SSLv3")) { + log.warn(sm.getString("sslUtilBase.ssl3")); + } + this.enabledProtocols = enabledProtocols.toArray(new String[0]); + + if (enabledProtocols.contains(Constants.SSL_PROTO_TLSv1_3) && + sslHostConfig.getCertificateVerification().isOptional() && + !isTls13RenegAuthAvailable() && warnTls13) { + log.warn(sm.getString("sslUtilBase.tls13.auth")); + } + + // Make TLS 1.3 renegotiation status visible further up the stack + sslHostConfig.setTls13RenegotiationAvailable(isTls13RenegAuthAvailable()); + + // Calculate the enabled ciphers + if (sslHostConfig.getCiphers().startsWith("PROFILE=")) { + // OpenSSL profiles + // TODO: sslHostConfig can query that with Panama, but skip for now + this.enabledCiphers = new String[0]; + } else { + boolean warnOnSkip = !sslHostConfig.getCiphers().equals(SSLHostConfig.DEFAULT_TLS_CIPHERS); + List configuredCiphers = sslHostConfig.getJsseCipherNames(); + Set implementedCiphers = getImplementedCiphers(); + List enabledCiphers = + getEnabled("ciphers", getLog(), warnOnSkip, configuredCiphers, implementedCiphers); + this.enabledCiphers = enabledCiphers.toArray(new String[0]); + } + } + + + static List getEnabled(String name, Log log, boolean warnOnSkip, Collection configured, + Collection implemented) { + + List enabled = new ArrayList<>(); + + if (implemented.size() == 0) { + // Unable to determine the list of available protocols. This will + // have been logged previously. + // Use the configuredProtocols and hope they work. If not, an error + // will be generated when the list is used. Not ideal but no more + // can be done at this point. + enabled.addAll(configured); + } else { + enabled.addAll(configured); + enabled.retainAll(implemented); + + if (enabled.isEmpty()) { + // Don't use the defaults in this case. They may be less secure + // than the configuration the user intended. + // Force the failure of the connector + throw new IllegalArgumentException( + sm.getString("sslUtilBase.noneSupported", name, configured)); + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("sslUtilBase.active", name, enabled)); + } + if (log.isDebugEnabled() || warnOnSkip) { + if (enabled.size() != configured.size()) { + List skipped = new ArrayList<>(configured); + skipped.removeAll(enabled); + String msg = sm.getString("sslUtilBase.skipped", name, skipped); + if (warnOnSkip) { + log.warn(msg); + } else { + log.debug(msg); + } + } + } + } + + return enabled; + } + + + /* + * Gets the key- or truststore with the specified type, path, password and password file. + */ + static KeyStore getStore(String type, String provider, String path, + String pass, String passFile) throws IOException { + + KeyStore ks = null; + InputStream istream = null; + try { + if (provider == null) { + ks = KeyStore.getInstance(type); + } else { + ks = KeyStore.getInstance(type, provider); + } + if ("DKS".equalsIgnoreCase(type)) { + URI uri = ConfigFileLoader.getSource().getURI(path); + ks.load(new DomainLoadStoreParameter(uri, Collections.emptyMap())); + } else { + // Some key store types (e.g. hardware) expect the InputStream + // to be null + if(!("PKCS11".equalsIgnoreCase(type) || + path.isEmpty() || + "NONE".equalsIgnoreCase(path))) { + istream = ConfigFileLoader.getSource().getResource(path).getInputStream(); + } + + // The digester cannot differentiate between null and "". + // Unfortunately, some key stores behave differently with null + // and "". + // JKS key stores treat null and "" interchangeably. + // PKCS12 key stores don't return the cert if null is used. + // Key stores that do not use passwords expect null + // Therefore: + // - generally use null if pass is null or "" + // - for JKS or PKCS12 only use null if pass is null + // (because JKS will auto-switch to PKCS12) + char[] storePass = null; + String passToUse = null; + if (passFile != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader( + ConfigFileLoader.getSource().getResource(passFile).getInputStream(), + StandardCharsets.UTF_8))) { + passToUse = reader.readLine(); + } + } else { + passToUse = pass; + } + + if (passToUse != null && (!"".equals(passToUse) || + "JKS".equalsIgnoreCase(type) || "PKCS12".equalsIgnoreCase(type))) { + storePass = passToUse.toCharArray(); + } + KeyStoreUtil.load(ks, istream, storePass); + } + } catch (IOException ioe) { + // May be expected when working with a trust store + // Re-throw. Caller will catch and log as required + throw ioe; + } catch(Exception ex) { + String msg = sm.getString("sslUtilBase.keystore_load_failed", type, path, + ex.getMessage()); + log.error(msg, ex); + throw new IOException(msg); + } finally { + if (istream != null) { + try { + istream.close(); + } catch (IOException ioe) { + // Do nothing + } + } + } + + return ks; + } + + + @Override + public final SSLContext createSSLContext(List negotiableProtocols) throws Exception { + SSLContext sslContext = createSSLContextInternal(negotiableProtocols); + sslContext.init(getKeyManagers(), getTrustManagers(), null); + + SSLSessionContext sessionContext = sslContext.getServerSessionContext(); + if (sessionContext != null) { + configureSessionContext(sessionContext); + } + + return sslContext; + } + + + @Override + public void configureSessionContext(SSLSessionContext sslSessionContext) { + // <0 - don't set anything - use the implementation default + if (sslHostConfig.getSessionCacheSize() >= 0) { + sslSessionContext.setSessionCacheSize(sslHostConfig.getSessionCacheSize()); + } + + // <0 - don't set anything - use the implementation default + if (sslHostConfig.getSessionTimeout() >= 0) { + sslSessionContext.setSessionTimeout(sslHostConfig.getSessionTimeout()); + } + } + + + @Override + public KeyManager[] getKeyManagers() throws Exception { + String keyAlias = certificate.getCertificateKeyAlias(); + String algorithm = sslHostConfig.getKeyManagerAlgorithm(); + String keyPassFile = certificate.getCertificateKeyPasswordFile(); + String keyPass = certificate.getCertificateKeyPassword(); + // This has to be here as it can't be moved to SSLHostConfig since the + // defaults vary between JSSE and OpenSSL. + if (keyPassFile == null) { + keyPassFile = certificate.getCertificateKeystorePasswordFile(); + } + if (keyPass == null) { + keyPass = certificate.getCertificateKeystorePassword(); + } + + KeyStore ks = certificate.getCertificateKeystore(); + KeyStore ksUsed = ks; + + /* + * Use an in memory key store where possible. + * For PEM format keys and certificates, it allows them to be imported + * into the expected format. + * For Java key stores with PKCS8 encoded keys (e.g. JKS files), it + * enables Tomcat to handle the case where multiple keys exist in the + * key store, each with a different password. The KeyManagerFactory + * can't handle that so using an in memory key store with just the + * required key works around that. + * Other keys stores (hardware, MS, etc.) will be used as is. + */ + char[] keyPassArray = null; + String keyPassToUse = null; + if (keyPassFile != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader( + ConfigFileLoader.getSource().getResource(keyPassFile).getInputStream(), + StandardCharsets.UTF_8))) { + keyPassToUse = reader.readLine(); + } + } else { + keyPassToUse = keyPass; + } + + if (keyPassToUse != null) { + keyPassArray = keyPassToUse.toCharArray(); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); + if (kmf.getProvider().getInfo().contains("FIPS")) { + // FIPS doesn't like ANY wrapping nor key manipulation. + if (keyAlias != null) { + log.warn(sm.getString("sslUtilBase.aliasIgnored", keyAlias)); + } + kmf.init(ksUsed, keyPassArray); + return kmf.getKeyManagers(); + } + + if (ks == null) { + if (certificate.getCertificateFile() == null) { + throw new IOException(sm.getString("sslUtilBase.noCertFile")); + } + + PEMFile privateKeyFile = new PEMFile( + certificate.getCertificateKeyFile() != null ? certificate.getCertificateKeyFile() : certificate.getCertificateFile(), + keyPass, keyPassFile, null); + PEMFile certificateFile = new PEMFile(certificate.getCertificateFile()); + + Collection chain = new ArrayList<>(certificateFile.getCertificates()); + if (certificate.getCertificateChainFile() != null) { + PEMFile certificateChainFile = new PEMFile(certificate.getCertificateChainFile()); + chain.addAll(certificateChainFile.getCertificates()); + } + + if (keyAlias == null) { + keyAlias = DEFAULT_KEY_ALIAS; + } + + // Switch to in-memory key store + ksUsed = KeyStore.getInstance("JKS"); + ksUsed.load(null, null); + ksUsed.setKeyEntry(keyAlias, privateKeyFile.getPrivateKey(), keyPassArray, + chain.toArray(new Certificate[0])); + } else { + if (keyAlias != null && !ks.isKeyEntry(keyAlias)) { + throw new IOException(sm.getString("sslUtilBase.alias_no_key_entry", keyAlias)); + } else if (keyAlias == null) { + Enumeration aliases = ks.aliases(); + if (!aliases.hasMoreElements()) { + throw new IOException(sm.getString("sslUtilBase.noKeys")); + } + while (aliases.hasMoreElements() && keyAlias == null) { + keyAlias = aliases.nextElement(); + if (!ks.isKeyEntry(keyAlias)) { + keyAlias = null; + } + } + if (keyAlias == null) { + throw new IOException(sm.getString("sslUtilBase.alias_no_key_entry", (Object) null)); + } + } + + Key k = ks.getKey(keyAlias, keyPassArray); + if (k != null && !"DKS".equalsIgnoreCase(certificate.getCertificateKeystoreType()) && + "PKCS#8".equalsIgnoreCase(k.getFormat())) { + // Switch to in-memory key store + String provider = certificate.getCertificateKeystoreProvider(); + if (provider == null) { + ksUsed = KeyStore.getInstance(certificate.getCertificateKeystoreType()); + } else { + ksUsed = KeyStore.getInstance(certificate.getCertificateKeystoreType(), + provider); + } + ksUsed.load(null, null); + ksUsed.setKeyEntry(keyAlias, k, keyPassArray, ks.getCertificateChain(keyAlias)); + } + // Non-PKCS#8 key stores will use the original key store + } + + + kmf.init(ksUsed, keyPassArray); + + KeyManager[] kms = kmf.getKeyManagers(); + + // Only need to filter keys by alias if there are key managers to filter + // and the original key store was used. The in memory key stores only + // have a single key so don't need filtering + if (kms != null && ksUsed == ks) { + String alias = keyAlias; + // JKS keystores always convert the alias name to lower case + if ("JKS".equals(certificate.getCertificateKeystoreType())) { + alias = alias.toLowerCase(Locale.ENGLISH); + } + for(int i = 0; i < kms.length; i++) { + kms[i] = new JSSEKeyManager((X509KeyManager)kms[i], alias); + } + } + + return kms; + } + + + @Override + public String[] getEnabledProtocols() { + return enabledProtocols; + } + + + @Override + public String[] getEnabledCiphers() { + return enabledCiphers; + } + + + @Override + public TrustManager[] getTrustManagers() throws Exception { + + String className = sslHostConfig.getTrustManagerClassName(); + if(className != null && className.length() > 0) { + ClassLoader classLoader = getClass().getClassLoader(); + Class clazz = classLoader.loadClass(className); + if(!(TrustManager.class.isAssignableFrom(clazz))){ + throw new InstantiationException(sm.getString( + "sslUtilBase.invalidTrustManagerClassName", className)); + } + Object trustManagerObject = clazz.getConstructor().newInstance(); + TrustManager trustManager = (TrustManager) trustManagerObject; + return new TrustManager[]{ trustManager }; + } + + TrustManager[] tms = null; + + KeyStore trustStore = sslHostConfig.getTruststore(); + if (trustStore != null) { + checkTrustStoreEntries(trustStore); + String algorithm = sslHostConfig.getTruststoreAlgorithm(); + String crlf = sslHostConfig.getCertificateRevocationListFile(); + boolean revocationEnabled = sslHostConfig.getRevocationEnabled(); + + if ("PKIX".equalsIgnoreCase(algorithm)) { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); + CertPathParameters params = getParameters(crlf, trustStore, revocationEnabled); + ManagerFactoryParameters mfp = new CertPathTrustManagerParameters(params); + tmf.init(mfp); + tms = tmf.getTrustManagers(); + } else { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); + tmf.init(trustStore); + tms = tmf.getTrustManagers(); + if (crlf != null && crlf.length() > 0) { + throw new CRLException(sm.getString("sslUtilBase.noCrlSupport", algorithm)); + } + // Only warn if the attribute has been explicitly configured + if (sslHostConfig.isCertificateVerificationDepthConfigured()) { + log.warn(sm.getString("sslUtilBase.noVerificationDepth", algorithm)); + } + } + } + + return tms; + } + + + private void checkTrustStoreEntries(KeyStore trustStore) throws Exception { + Enumeration aliases = trustStore.aliases(); + if (aliases != null) { + Date now = new Date(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + if (trustStore.isCertificateEntry(alias)) { + Certificate cert = trustStore.getCertificate(alias); + if (cert instanceof X509Certificate) { + try { + ((X509Certificate) cert).checkValidity(now); + } catch (CertificateExpiredException | CertificateNotYetValidException e) { + String msg = sm.getString("sslUtilBase.trustedCertNotValid", alias, + ((X509Certificate) cert).getSubjectX500Principal(), e.getMessage()); + if (log.isDebugEnabled()) { + log.warn(msg, e); + } else { + log.warn(msg); + } + } + } else { + if (log.isDebugEnabled()) { + log.debug(sm.getString("sslUtilBase.trustedCertNotChecked", alias)); + } + } + } + } + } + } + + + /** + * Return the initialization parameters for the TrustManager. + * Currently, only the default PKIX is supported. + * + * @param crlf The path to the CRL file. + * @param trustStore The configured TrustStore. + * @param revocationEnabled Should the JSSE provider perform revocation + * checks? Ignored if {@code crlf} is non-null. + * Configuration of revocation checks are expected + * to be via proprietary JSSE provider methods. + * @return The parameters including the CRLs and TrustStore. + * @throws Exception An error occurred + */ + protected CertPathParameters getParameters(String crlf, KeyStore trustStore, + boolean revocationEnabled) throws Exception { + + PKIXBuilderParameters xparams = + new PKIXBuilderParameters(trustStore, new X509CertSelector()); + if (crlf != null && crlf.length() > 0) { + Collection crls = getCRLs(crlf); + CertStoreParameters csp = new CollectionCertStoreParameters(crls); + CertStore store = CertStore.getInstance("Collection", csp); + xparams.addCertStore(store); + xparams.setRevocationEnabled(true); + } else { + xparams.setRevocationEnabled(revocationEnabled); + } + xparams.setMaxPathLength(sslHostConfig.getCertificateVerificationDepth()); + return xparams; + } + + + /** + * Load the collection of CRLs. + * @param crlf The path to the CRL file. + * @return the CRLs collection + * @throws IOException Error reading CRL file + * @throws CRLException CRL error + * @throws CertificateException Error processing certificate + */ + protected Collection getCRLs(String crlf) + throws IOException, CRLException, CertificateException { + + Collection crls = null; + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + try (InputStream is = ConfigFileLoader.getSource().getResource(crlf).getInputStream()) { + crls = cf.generateCRLs(is); + } + } catch(IOException | CRLException | CertificateException e) { + throw e; + } + return crls; + } + + + protected abstract Set getImplementedProtocols(); + protected abstract Set getImplementedCiphers(); + protected abstract Log getLog(); + protected abstract boolean isTls13RenegAuthAvailable(); + protected abstract SSLContext createSSLContextInternal(List negotiableProtocols) throws Exception; +} diff --git a/java/org/apache/tomcat/util/net/SecureNio2Channel.java b/java/org/apache/tomcat/util/net/SecureNio2Channel.java new file mode 100644 index 0000000..076f336 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SecureNio2Channel.java @@ -0,0 +1,1296 @@ +/* + * 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.tomcat.util.net; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.nio.channels.WritePendingException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteBufferUtils; +import org.apache.tomcat.util.net.TLSClientHelloExtractor.ExtractorResult; +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implementation of a secure socket channel for NIO2. + */ +public class SecureNio2Channel extends Nio2Channel { + + private static final Log log = LogFactory.getLog(SecureNio2Channel.class); + private static final StringManager sm = StringManager.getManager(SecureNio2Channel.class); + + // Value determined by observation of what the SSL Engine requested in + // various scenarios + private static final int DEFAULT_NET_BUFFER_SIZE = 16921; + + protected final Nio2Endpoint endpoint; + + protected ByteBuffer netInBuffer; + protected ByteBuffer netOutBuffer; + + protected SSLEngine sslEngine; + + protected volatile boolean sniComplete = false; + + private volatile boolean handshakeComplete = false; + private volatile HandshakeStatus handshakeStatus; //gets set by handshake + + protected boolean closed; + protected boolean closing; + + private final Map> additionalTlsAttributes = new HashMap<>(); + + private volatile boolean unwrapBeforeRead; + private final CompletionHandler> handshakeReadCompletionHandler; + private final CompletionHandler> handshakeWriteCompletionHandler; + + public SecureNio2Channel(SocketBufferHandler bufHandler, Nio2Endpoint endpoint) { + super(bufHandler); + this.endpoint = endpoint; + if (endpoint.getSocketProperties().getDirectSslBuffer()) { + netInBuffer = ByteBuffer.allocateDirect(DEFAULT_NET_BUFFER_SIZE); + netOutBuffer = ByteBuffer.allocateDirect(DEFAULT_NET_BUFFER_SIZE); + } else { + netInBuffer = ByteBuffer.allocate(DEFAULT_NET_BUFFER_SIZE); + netOutBuffer = ByteBuffer.allocate(DEFAULT_NET_BUFFER_SIZE); + } + handshakeReadCompletionHandler = new HandshakeReadCompletionHandler(); + handshakeWriteCompletionHandler = new HandshakeWriteCompletionHandler(); + } + + + private class HandshakeReadCompletionHandler + implements CompletionHandler> { + @Override + public void completed(Integer result, SocketWrapperBase attachment) { + if (result.intValue() < 0) { + failed(new EOFException(), attachment); + } else { + endpoint.processSocket(attachment, SocketEvent.OPEN_READ, false); + } + } + @Override + public void failed(Throwable exc, SocketWrapperBase attachment) { + endpoint.processSocket(attachment, SocketEvent.ERROR, false); + } + } + + + private class HandshakeWriteCompletionHandler + implements CompletionHandler> { + @Override + public void completed(Integer result, SocketWrapperBase attachment) { + if (result.intValue() < 0) { + failed(new EOFException(), attachment); + } else { + endpoint.processSocket(attachment, SocketEvent.OPEN_WRITE, false); + } + } + @Override + public void failed(Throwable exc, SocketWrapperBase attachment) { + endpoint.processSocket(attachment, SocketEvent.ERROR, false); + } + } + + + @Override + public void reset(AsynchronousSocketChannel channel, SocketWrapperBase socket) + throws IOException { + super.reset(channel, socket); + sslEngine = null; + sniComplete = false; + handshakeComplete = false; + unwrapBeforeRead = true; + closed = false; + closing = false; + netInBuffer.clear(); + } + + @Override + public void free() { + super.free(); + if (endpoint.getSocketProperties().getDirectSslBuffer()) { + ByteBufferUtils.cleanDirectBuffer(netInBuffer); + ByteBufferUtils.cleanDirectBuffer(netOutBuffer); + } + } + + private class FutureFlush implements Future { + private Future integer; + private Exception e = null; + protected FutureFlush() { + try { + integer = sc.write(netOutBuffer); + } catch (IllegalStateException e) { + this.e = e; + } + } + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return (e != null) ? true : integer.cancel(mayInterruptIfRunning); + } + @Override + public boolean isCancelled() { + return (e != null) ? true : integer.isCancelled(); + } + @Override + public boolean isDone() { + return (e != null) ? true : integer.isDone(); + } + @Override + public Boolean get() throws InterruptedException, + ExecutionException { + if (e != null) { + throw new ExecutionException(e); + } + return Boolean.valueOf(integer.get().intValue() >= 0); + } + @Override + public Boolean get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + if (e != null) { + throw new ExecutionException(e); + } + return Boolean.valueOf(integer.get(timeout, unit).intValue() >= 0); + } + } + + /** + * Flush the channel. + * + * @return true if the network buffer has been flushed out and + * is empty else false (as a future) + */ + @Override + public Future flush() { + return new FutureFlush(); + } + + /** + * Performs SSL handshake, non blocking, but performs NEED_TASK on the same + * thread. Hence, you should never call this method using your Acceptor + * thread, as you would slow down your system significantly. + *

    + * The return for this operation is 0 if the handshake is complete and a + * positive value if it is not complete. In the event of a positive value + * coming back, the appropriate read/write will already have been called + * with an appropriate CompletionHandler. + * + * @return 0 if hand shake is complete, negative if the socket needs to + * close and positive if the handshake is incomplete + * + * @throws IOException if an error occurs during the handshake + */ + @Override + public int handshake() throws IOException { + return handshakeInternal(true); + } + + protected int handshakeInternal(boolean async) throws IOException { + if (handshakeComplete) { + return 0; //we have done our initial handshake + } + + if (!sniComplete) { + int sniResult = processSNI(); + if (sniResult == 0) { + sniComplete = true; + } else { + return sniResult; + } + } + + SSLEngineResult handshake = null; + long timeout = endpoint.getConnectionTimeout(); + + while (!handshakeComplete) { + switch (handshakeStatus) { + case NOT_HANDSHAKING: { + //should never happen + throw new IOException(sm.getString("channel.nio.ssl.notHandshaking")); + } + case FINISHED: { + if (endpoint.hasNegotiableProtocols()) { + if (sslEngine instanceof SSLUtil.ProtocolInfo) { + socketWrapper.setNegotiatedProtocol( + ((SSLUtil.ProtocolInfo) sslEngine).getNegotiatedProtocol()); + } else { + socketWrapper.setNegotiatedProtocol(sslEngine.getApplicationProtocol()); + } + } + //we are complete if we have delivered the last package + handshakeComplete = !netOutBuffer.hasRemaining(); + //return 0 if we are complete, otherwise we still have data to write + if (handshakeComplete) { + return 0; + } else { + if (async) { + sc.write(netOutBuffer, AbstractEndpoint.toTimeout(timeout), + TimeUnit.MILLISECONDS, socketWrapper, handshakeWriteCompletionHandler); + } else { + try { + if (timeout > 0) { + sc.write(netOutBuffer).get(timeout, TimeUnit.MILLISECONDS); + } else { + sc.write(netOutBuffer).get(); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new IOException(sm.getString("channel.nio.ssl.handshakeError")); + } + } + return 1; + } + } + case NEED_WRAP: { + //perform the wrap function + try { + handshake = handshakeWrap(); + } catch (SSLException e) { + handshake = handshakeWrap(); + throw e; + } + if (handshake.getStatus() == Status.OK) { + if (handshakeStatus == HandshakeStatus.NEED_TASK) { + handshakeStatus = tasks(); + } + } else if (handshake.getStatus() == Status.CLOSED) { + return -1; + } else { + //wrap should always work with our buffers + throw new IOException(sm.getString("channel.nio.ssl.unexpectedStatusDuringWrap", handshake.getStatus())); + } + if (handshakeStatus != HandshakeStatus.NEED_UNWRAP || netOutBuffer.remaining() > 0) { + //should actually return OP_READ if we have NEED_UNWRAP + if (async) { + sc.write(netOutBuffer, AbstractEndpoint.toTimeout(timeout), + TimeUnit.MILLISECONDS, socketWrapper, handshakeWriteCompletionHandler); + } else { + try { + if (timeout > 0) { + sc.write(netOutBuffer).get(timeout, TimeUnit.MILLISECONDS); + } else { + sc.write(netOutBuffer).get(); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new IOException(sm.getString("channel.nio.ssl.handshakeError")); + } + } + return 1; + } + //fall down to NEED_UNWRAP on the same call, will result in a + //BUFFER_UNDERFLOW if it needs data + } + //$FALL-THROUGH$ + case NEED_UNWRAP: { + //perform the unwrap function + handshake = handshakeUnwrap(); + if (handshake.getStatus() == Status.OK) { + if (handshakeStatus == HandshakeStatus.NEED_TASK) { + handshakeStatus = tasks(); + } + } else if (handshake.getStatus() == Status.BUFFER_UNDERFLOW) { + //read more data + if (async) { + sc.read(netInBuffer, AbstractEndpoint.toTimeout(timeout), + TimeUnit.MILLISECONDS, socketWrapper, handshakeReadCompletionHandler); + } else { + try { + int read; + if (timeout > 0) { + read = sc.read(netInBuffer).get(timeout, TimeUnit.MILLISECONDS).intValue(); + } else { + read = sc.read(netInBuffer).get().intValue(); + } + if (read == -1) { + throw new EOFException(); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new IOException(sm.getString("channel.nio.ssl.handshakeError")); + } + } + return 1; + } else { + throw new IOException(sm.getString("channel.nio.ssl.unexpectedStatusDuringUnwrap", handshake.getStatus())); + } + break; + } + case NEED_TASK: { + handshakeStatus = tasks(); + break; + } + default: throw new IllegalStateException(sm.getString("channel.nio.ssl.invalidStatus", handshakeStatus)); + } + } + //return 0 if we are complete, otherwise recurse to process the task + return handshakeComplete ? 0 : handshakeInternal(async); + } + + + /* + * Peeks at the initial network bytes to determine if the SNI extension is + * present and, if it is, what host name has been requested. Based on the + * provided host name, configure the SSLEngine for this connection. + */ + private int processSNI() throws IOException { + // If there is no data to process, trigger a read immediately. This is + // an optimisation for the typical case so we don't create an + // SNIExtractor only to discover there is no data to process + if (netInBuffer.position() == 0) { + sc.read(netInBuffer, AbstractEndpoint.toTimeout(endpoint.getConnectionTimeout()), + TimeUnit.MILLISECONDS, socketWrapper, handshakeReadCompletionHandler); + return 1; + } + + TLSClientHelloExtractor extractor = new TLSClientHelloExtractor(netInBuffer); + + if (extractor.getResult() == ExtractorResult.UNDERFLOW && + netInBuffer.capacity() < endpoint.getSniParseLimit()) { + // extractor needed more data to process but netInBuffer was full so + // expand the buffer and read some more data. + int newLimit = Math.min(netInBuffer.capacity() * 2, endpoint.getSniParseLimit()); + log.info(sm.getString("channel.nio.ssl.expandNetInBuffer", + Integer.toString(newLimit))); + + netInBuffer = ByteBufferUtils.expand(netInBuffer, newLimit); + sc.read(netInBuffer, AbstractEndpoint.toTimeout(endpoint.getConnectionTimeout()), + TimeUnit.MILLISECONDS, socketWrapper, handshakeReadCompletionHandler); + return 1; + } + + String hostName = null; + List clientRequestedCiphers = null; + List clientRequestedApplicationProtocols = null; + switch (extractor.getResult()) { + case COMPLETE: + hostName = extractor.getSNIValue(); + clientRequestedApplicationProtocols = + extractor.getClientRequestedApplicationProtocols(); + //$FALL-THROUGH$ to set the client requested ciphers + case NOT_PRESENT: + clientRequestedCiphers = extractor.getClientRequestedCiphers(); + break; + case NEED_READ: + sc.read(netInBuffer, AbstractEndpoint.toTimeout(endpoint.getConnectionTimeout()), + TimeUnit.MILLISECONDS, socketWrapper, handshakeReadCompletionHandler); + return 1; + case UNDERFLOW: + // Unable to buffer enough data to read SNI extension data + if (log.isDebugEnabled()) { + log.debug(sm.getString("channel.nio.ssl.sniDefault")); + } + hostName = endpoint.getDefaultSSLHostConfigName(); + clientRequestedCiphers = Collections.emptyList(); + break; + case NON_SECURE: + netOutBuffer.clear(); + netOutBuffer.put(TLSClientHelloExtractor.USE_TLS_RESPONSE); + netOutBuffer.flip(); + flush(); + throw new IOException(sm.getString("channel.nio.ssl.foundHttp")); + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("channel.nio.ssl.sniHostName", sc, hostName)); + } + + sslEngine = endpoint.createSSLEngine(hostName, clientRequestedCiphers, + clientRequestedApplicationProtocols); + + // Populate additional TLS attributes obtained from the handshake that + // aren't available from the session + additionalTlsAttributes.put(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY, + extractor.getClientRequestedProtocols()); + additionalTlsAttributes.put(SSLSupport.REQUESTED_CIPHERS_KEY, + extractor.getClientRequestedCipherNames()); + + // Ensure the application buffers (which have to be created earlier) are + // big enough. + getBufHandler().expand(sslEngine.getSession().getApplicationBufferSize()); + if (netOutBuffer.capacity() < sslEngine.getSession().getApplicationBufferSize()) { + // Info for now as we may need to increase DEFAULT_NET_BUFFER_SIZE + log.info(sm.getString("channel.nio.ssl.expandNetOutBuffer", + Integer.toString(sslEngine.getSession().getApplicationBufferSize()))); + } + netInBuffer = ByteBufferUtils.expand(netInBuffer, sslEngine.getSession().getPacketBufferSize()); + netOutBuffer = ByteBufferUtils.expand(netOutBuffer, sslEngine.getSession().getPacketBufferSize()); + + // Set limit and position to expected values + netOutBuffer.position(0); + netOutBuffer.limit(0); + + // Initiate handshake + sslEngine.beginHandshake(); + handshakeStatus = sslEngine.getHandshakeStatus(); + + return 0; + } + + + /** + * Force a blocking handshake to take place for this key. + * This requires that both network and application buffers have been emptied out prior to this call taking place, or a + * IOException will be thrown. + * @throws IOException - if an IO exception occurs or if application or network buffers contain data + * @throws java.net.SocketTimeoutException - if a socket operation timed out + */ + public void rehandshake() throws IOException { + //validate the network buffers are empty + if (netInBuffer.position() > 0 && netInBuffer.position() < netInBuffer.limit()) { + throw new IOException(sm.getString("channel.nio.ssl.netInputNotEmpty")); + } + if (netOutBuffer.position() > 0 && netOutBuffer.position() < netOutBuffer.limit()) { + throw new IOException(sm.getString("channel.nio.ssl.netOutputNotEmpty")); + } + if (!getBufHandler().isReadBufferEmpty()) { + throw new IOException(sm.getString("channel.nio.ssl.appInputNotEmpty")); + } + if (!getBufHandler().isWriteBufferEmpty()) { + throw new IOException(sm.getString("channel.nio.ssl.appOutputNotEmpty")); + } + + netOutBuffer.position(0); + netOutBuffer.limit(0); + netInBuffer.position(0); + netInBuffer.limit(0); + getBufHandler().reset(); + + handshakeComplete = false; + //initiate handshake + sslEngine.beginHandshake(); + handshakeStatus = sslEngine.getHandshakeStatus(); + + boolean handshaking = true; + try { + while (handshaking) { + int hsStatus = handshakeInternal(false); + switch (hsStatus) { + case -1 : throw new EOFException(sm.getString("channel.nio.ssl.eofDuringHandshake")); + case 0 : handshaking = false; break; + default : // Some blocking IO occurred, so iterate + } + } + } catch (IOException x) { + closeSilently(); + throw x; + } catch (Exception cx) { + closeSilently(); + IOException x = new IOException(cx); + throw x; + } + } + + + /** + * Executes all the tasks needed on the same thread. + * @return the status + */ + protected SSLEngineResult.HandshakeStatus tasks() { + Runnable r = null; + while ( (r = sslEngine.getDelegatedTask()) != null) { + r.run(); + } + return sslEngine.getHandshakeStatus(); + } + + /** + * Performs the WRAP function + * @return the result + * @throws IOException An IO error occurred + */ + protected SSLEngineResult handshakeWrap() throws IOException { + //this should never be called with a network buffer that contains data + //so we can clear it here. + netOutBuffer.clear(); + //perform the wrap + getBufHandler().configureWriteBufferForRead(); + SSLEngineResult result = sslEngine.wrap(getBufHandler().getWriteBuffer(), netOutBuffer); + //prepare the results to be written + netOutBuffer.flip(); + //set the status + handshakeStatus = result.getHandshakeStatus(); + return result; + } + + /** + * Perform handshake unwrap + * @return the result + * @throws IOException An IO error occurred + */ + protected SSLEngineResult handshakeUnwrap() throws IOException { + SSLEngineResult result; + boolean cont = false; + //loop while we can perform pure SSLEngine data + do { + //prepare the buffer with the incoming data + netInBuffer.flip(); + //call unwrap + getBufHandler().configureReadBufferForWrite(); + result = sslEngine.unwrap(netInBuffer, getBufHandler().getReadBuffer()); + /* + * ByteBuffer.compact() is an optional method but netInBuffer is created from either ByteBuffer.allocate() + * or ByteBuffer.allocateDirect() and the ByteBuffers returned by those methods do implement compact(). + * The ByteBuffer must be in 'read from' mode when compact() is called and will be in 'write to' mode + * afterwards. + */ + netInBuffer.compact(); + //read in the status + handshakeStatus = result.getHandshakeStatus(); + if (result.getStatus() == SSLEngineResult.Status.OK && + result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + //execute tasks if we need to + handshakeStatus = tasks(); + } + //perform another unwrap? + cont = result.getStatus() == SSLEngineResult.Status.OK && + handshakeStatus == HandshakeStatus.NEED_UNWRAP; + } while (cont); + return result; + } + + public SSLSupport getSSLSupport() { + if (sslEngine != null) { + SSLSession session = sslEngine.getSession(); + return endpoint.getSslImplementation().getSSLSupport(session, additionalTlsAttributes); + } + return null; + } + + /** + * Sends an SSL close message, will not physically close the connection here.
    + * To close the connection, you could do something like + *

    
    +     *   close();
    +     *   while (isOpen() && !myTimeoutFunction()) Thread.sleep(25);
    +     *   if ( isOpen() ) close(true); //forces a close if you timed out
    +     * 
    + * @throws IOException if an I/O error occurs + * @throws IOException if there is data on the outgoing network buffer and we are unable to flush it + */ + @Override + public void close() throws IOException { + if (closing) { + return; + } + closing = true; + if (sslEngine == null) { + netOutBuffer.clear(); + closed = true; + return; + } + sslEngine.closeOutbound(); + long timeout = endpoint.getConnectionTimeout(); + + try { + if (timeout > 0) { + if (!flush().get(timeout, TimeUnit.MILLISECONDS).booleanValue()) { + closeSilently(); + throw new IOException(sm.getString("channel.nio.ssl.remainingDataDuringClose")); + } + } else { + if (!flush().get().booleanValue()) { + closeSilently(); + throw new IOException(sm.getString("channel.nio.ssl.remainingDataDuringClose")); + } + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + closeSilently(); + throw new IOException(sm.getString("channel.nio.ssl.remainingDataDuringClose"), e); + } catch (WritePendingException e) { + closeSilently(); + throw new IOException(sm.getString("channel.nio.ssl.pendingWriteDuringClose"), e); + } + //prep the buffer for the close message + netOutBuffer.clear(); + //perform the close, since we called sslEngine.closeOutbound + SSLEngineResult handshake = sslEngine.wrap(getEmptyBuf(), netOutBuffer); + //we should be in a close state + if (handshake.getStatus() != SSLEngineResult.Status.CLOSED) { + throw new IOException(sm.getString("channel.nio.ssl.invalidCloseState")); + } + //prepare the buffer for writing + netOutBuffer.flip(); + //if there is data to be written + try { + if (timeout > 0) { + if (!flush().get(timeout, TimeUnit.MILLISECONDS).booleanValue()) { + closeSilently(); + throw new IOException(sm.getString("channel.nio.ssl.remainingDataDuringClose")); + } + } else { + if (!flush().get().booleanValue()) { + closeSilently(); + throw new IOException(sm.getString("channel.nio.ssl.remainingDataDuringClose")); + } + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + closeSilently(); + throw new IOException(sm.getString("channel.nio.ssl.remainingDataDuringClose"), e); + } catch (WritePendingException e) { + closeSilently(); + throw new IOException(sm.getString("channel.nio.ssl.pendingWriteDuringClose"), e); + } + + //is the channel closed? + closed = (!netOutBuffer.hasRemaining() && (handshake.getHandshakeStatus() != HandshakeStatus.NEED_WRAP)); + } + + + @Override + public void close(boolean force) throws IOException { + try { + close(); + } finally { + if (force || closed) { + closed = true; + sc.close(); + } + } + } + + + private void closeSilently() { + try { + close(true); + } catch (IOException ioe) { + // This is expected - swallowing the exception is the reason this + // method exists. Log at debug in case someone is interested. + log.debug(sm.getString("channel.nio.ssl.closeSilentError"), ioe); + } + } + + + private class FutureRead implements Future { + private ByteBuffer dst; + private Future integer; + private FutureRead(ByteBuffer dst) { + this.dst = dst; + if (unwrapBeforeRead || netInBuffer.position() > 0) { + this.integer = null; + } else { + this.integer = sc.read(netInBuffer); + } + } + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return (integer == null) ? false : integer.cancel(mayInterruptIfRunning); + } + @Override + public boolean isCancelled() { + return (integer == null) ? false : integer.isCancelled(); + } + @Override + public boolean isDone() { + return (integer == null) ? true : integer.isDone(); + } + @Override + public Integer get() throws InterruptedException, ExecutionException { + try { + return (integer == null) ? unwrap(netInBuffer.position(), -1, TimeUnit.MILLISECONDS) : unwrap(integer.get().intValue(), -1, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + // Cannot happen: no timeout + throw new ExecutionException(e); + } + } + @Override + public Integer get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + return (integer == null) ? unwrap(netInBuffer.position(), timeout, unit) : unwrap(integer.get(timeout, unit).intValue(), timeout, unit); + } + private Integer unwrap(int nRead, long timeout, TimeUnit unit) throws ExecutionException, TimeoutException, InterruptedException { + //are we in the middle of closing or closed? + if (closing || closed) { + return Integer.valueOf(-1); + } + //did we reach EOF? if so send EOF up one layer. + if (nRead < 0) { + return Integer.valueOf(-1); + } + //the data read + int read = 0; + //the SSL engine result + SSLEngineResult unwrap; + do { + //prepare the buffer + netInBuffer.flip(); + //unwrap the data + try { + unwrap = sslEngine.unwrap(netInBuffer, dst); + } catch (SSLException e) { + throw new ExecutionException(e); + } + //compact the buffer + netInBuffer.compact(); + if (unwrap.getStatus() == Status.OK || unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + //we did receive some data, add it to our total + read += unwrap.bytesProduced(); + //perform any tasks if needed + if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + //if we need more network data, then bail out for now. + if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + if (read == 0) { + integer = sc.read(netInBuffer); + if (timeout > 0) { + return unwrap(integer.get(timeout, unit).intValue(), timeout, unit); + } else { + return unwrap(integer.get().intValue(), -1, TimeUnit.MILLISECONDS); + } + } else { + break; + } + } + } else if (unwrap.getStatus() == Status.BUFFER_OVERFLOW) { + if (read > 0) { + // Buffer overflow can happen if we have read data. Return + // so the destination buffer can be emptied before another + // read is attempted + break; + } else { + // The SSL session has increased the required buffer size + // since the buffer was created. + if (dst == getBufHandler().getReadBuffer()) { + // This is the normal case for this code + getBufHandler() + .expand(sslEngine.getSession().getApplicationBufferSize()); + dst = getBufHandler().getReadBuffer(); + } else if (dst == getAppReadBufHandler().getByteBuffer()) { + getAppReadBufHandler() + .expand(sslEngine.getSession().getApplicationBufferSize()); + dst = getAppReadBufHandler().getByteBuffer(); + } else { + // Can't expand the buffer as there is no way to signal + // to the caller that the buffer has been replaced. + throw new ExecutionException(new IOException(sm.getString("channel.nio.ssl.unwrapFailResize", unwrap.getStatus()))); + } + } + } else { + // Something else went wrong + throw new ExecutionException(new IOException(sm.getString("channel.nio.ssl.unwrapFail", unwrap.getStatus()))); + } + } while (netInBuffer.position() != 0); //continue to unwrapping as long as the input buffer has stuff + if (!dst.hasRemaining()) { + unwrapBeforeRead = true; + } else { + unwrapBeforeRead = false; + } + return Integer.valueOf(read); + } + } + + /** + * Reads a sequence of bytes from this channel into the given buffer. + * + * @param dst The buffer into which bytes are to be transferred + * @return The number of bytes read, possibly zero, or -1 if the channel has reached end-of-stream + * @throws IllegalStateException if the handshake was not completed + */ + @Override + public Future read(ByteBuffer dst) { + if (!handshakeComplete) { + throw new IllegalStateException(sm.getString("channel.nio.ssl.incompleteHandshake")); + } + return new FutureRead(dst); + } + + private class FutureWrite implements Future { + private final ByteBuffer src; + private Future integer = null; + private int written = 0; + private Throwable t = null; + private FutureWrite(ByteBuffer src) { + this.src = src; + //are we closing or closed? + if (closing || closed) { + t = new IOException(sm.getString("channel.nio.ssl.closing")); + } else { + wrap(); + } + } + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return integer.cancel(mayInterruptIfRunning); + } + @Override + public boolean isCancelled() { + return integer.isCancelled(); + } + @Override + public boolean isDone() { + return integer.isDone(); + } + @Override + public Integer get() throws InterruptedException, ExecutionException { + if (t != null) { + throw new ExecutionException(t); + } + if (integer.get().intValue() > 0 && written == 0) { + wrap(); + return get(); + } else if (netOutBuffer.hasRemaining()) { + integer = sc.write(netOutBuffer); + return get(); + } else { + return Integer.valueOf(written); + } + } + @Override + public Integer get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + if (t != null) { + throw new ExecutionException(t); + } + if (integer.get(timeout, unit).intValue() > 0 && written == 0) { + wrap(); + return get(timeout, unit); + } else if (netOutBuffer.hasRemaining()) { + integer = sc.write(netOutBuffer); + return get(timeout, unit); + } else { + return Integer.valueOf(written); + } + } + protected void wrap() { + try { + if (!netOutBuffer.hasRemaining()) { + netOutBuffer.clear(); + SSLEngineResult result = sslEngine.wrap(src, netOutBuffer); + written = result.bytesConsumed(); + netOutBuffer.flip(); + if (result.getStatus() == Status.OK) { + if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + } else { + t = new IOException(sm.getString("channel.nio.ssl.wrapFail", result.getStatus())); + } + } + integer = sc.write(netOutBuffer); + } catch (SSLException e) { + t = e; + } + } + } + + /** + * Writes a sequence of bytes to this channel from the given buffer. + * + * @param src The buffer from which bytes are to be retrieved + * @return The number of bytes written, possibly zero + */ + @Override + public Future write(ByteBuffer src) { + return new FutureWrite(src); + } + + @Override + public void read(final ByteBuffer dst, + final long timeout, final TimeUnit unit, final A attachment, + final CompletionHandler handler) { + // Check state + if (closing || closed) { + handler.completed(Integer.valueOf(-1), attachment); + return; + } + if (!handshakeComplete) { + throw new IllegalStateException(sm.getString("channel.nio.ssl.incompleteHandshake")); + } + CompletionHandler readCompletionHandler = new CompletionHandler<>() { + @Override + public void completed(Integer nBytes, A attach) { + if (nBytes.intValue() < 0) { + failed(new EOFException(), attach); + } else { + try { + ByteBuffer dst2 = dst; + //the data read + int read = 0; + //the SSL engine result + SSLEngineResult unwrap; + do { + //prepare the buffer + netInBuffer.flip(); + //unwrap the data + unwrap = sslEngine.unwrap(netInBuffer, dst2); + //compact the buffer + netInBuffer.compact(); + if (unwrap.getStatus() == Status.OK || unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + //we did receive some data, add it to our total + read += unwrap.bytesProduced(); + //perform any tasks if needed + if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + //if we need more network data, then bail out for now. + if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + if (read == 0) { + sc.read(netInBuffer, timeout, unit, attachment, this); + return; + } else { + break; + } + } + } else if (unwrap.getStatus() == Status.BUFFER_OVERFLOW) { + if (read > 0) { + // Buffer overflow can happen if we have read data. Return + // so the destination buffer can be emptied before another + // read is attempted + break; + } else { + // The SSL session has increased the required buffer size + // since the buffer was created. + if (dst2 == getBufHandler().getReadBuffer()) { + // This is the normal case for this code + getBufHandler().expand( + sslEngine.getSession().getApplicationBufferSize()); + dst2 = getBufHandler().getReadBuffer(); + } else if (getAppReadBufHandler() != null && dst2 == getAppReadBufHandler().getByteBuffer()) { + getAppReadBufHandler() + .expand(sslEngine.getSession().getApplicationBufferSize()); + dst2 = getAppReadBufHandler().getByteBuffer(); + } else { + // Can't expand the buffer as there is no way to signal + // to the caller that the buffer has been replaced. + throw new IOException( + sm.getString("channel.nio.ssl.unwrapFailResize", unwrap.getStatus())); + } + } + } else { + // Something else went wrong + throw new IOException(sm.getString("channel.nio.ssl.unwrapFail", unwrap.getStatus())); + } + // continue to unwrap as long as the input buffer has stuff + } while (netInBuffer.position() != 0); + if (!dst2.hasRemaining()) { + unwrapBeforeRead = true; + } else { + unwrapBeforeRead = false; + } + // If everything is OK, so complete + handler.completed(Integer.valueOf(read), attach); + } catch (Exception e) { + failed(e, attach); + } + } + } + @Override + public void failed(Throwable exc, A attach) { + handler.failed(exc, attach); + } + }; + if (unwrapBeforeRead || netInBuffer.position() > 0) { + readCompletionHandler.completed(Integer.valueOf(netInBuffer.position()), attachment); + } else { + sc.read(netInBuffer, timeout, unit, attachment, readCompletionHandler); + } + } + + @Override + public void read(final ByteBuffer[] dsts, final int offset, final int length, + final long timeout, final TimeUnit unit, final A attachment, + final CompletionHandler handler) { + if (offset < 0 || dsts == null || (offset + length) > dsts.length) { + throw new IllegalArgumentException(); + } + if (closing || closed) { + handler.completed(Long.valueOf(-1), attachment); + return; + } + if (!handshakeComplete) { + throw new IllegalStateException(sm.getString("channel.nio.ssl.incompleteHandshake")); + } + CompletionHandler readCompletionHandler = new CompletionHandler<>() { + @Override + public void completed(Integer nBytes, A attach) { + if (nBytes.intValue() < 0) { + failed(new EOFException(), attach); + } else { + try { + //the data read + long read = 0; + //the SSL engine result + SSLEngineResult unwrap; + ByteBuffer[] dsts2 = dsts; + int length2 = length; + OverflowState overflowState = OverflowState.NONE; + do { + if (overflowState == OverflowState.PROCESSING) { + overflowState = OverflowState.DONE; + } + //prepare the buffer + netInBuffer.flip(); + //unwrap the data + unwrap = sslEngine.unwrap(netInBuffer, dsts2, offset, length2); + //compact the buffer + netInBuffer.compact(); + if (unwrap.getStatus() == Status.OK || unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + //we did receive some data, add it to our total + read += unwrap.bytesProduced(); + if (overflowState == OverflowState.DONE) { + // Remove the data read into the overflow buffer + read -= getBufHandler().getReadBuffer().position(); + } + //perform any tasks if needed + if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + //if we need more network data, then bail out for now. + if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + if (read == 0) { + sc.read(netInBuffer, timeout, unit, attachment, this); + return; + } else { + break; + } + } + } else if (unwrap.getStatus() == Status.BUFFER_OVERFLOW && read > 0) { + //buffer overflow can happen, if we have read data, then + //empty out the dst buffer before we do another read + break; + } else if (unwrap.getStatus() == Status.BUFFER_OVERFLOW) { + //here we should trap BUFFER_OVERFLOW and call expand on the buffer + //for now, throw an exception, as we initialized the buffers + //in the constructor + ByteBuffer readBuffer = getBufHandler().getReadBuffer(); + boolean found = false; + boolean resized = true; + for (int i = 0; i < length2; i++) { + // The SSL session has increased the required buffer size + // since the buffer was created. + if (dsts[offset + i] == getBufHandler().getReadBuffer()) { + getBufHandler().expand(sslEngine.getSession().getApplicationBufferSize()); + if (dsts[offset + i] == getBufHandler().getReadBuffer()) { + resized = false; + } + dsts[offset + i] = getBufHandler().getReadBuffer(); + found = true; + } else if (getAppReadBufHandler() != null && dsts[offset + i] == getAppReadBufHandler().getByteBuffer()) { + getAppReadBufHandler().expand(sslEngine.getSession().getApplicationBufferSize()); + if (dsts[offset + i] == getAppReadBufHandler().getByteBuffer()) { + resized = false; + } + dsts[offset + i] = getAppReadBufHandler().getByteBuffer(); + found = true; + } + } + if (found) { + if (!resized) { + throw new IOException(sm.getString("channel.nio.ssl.unwrapFail", unwrap.getStatus())); + } + } else { + // Add the main read buffer in the destinations and try again + dsts2 = new ByteBuffer[dsts.length + 1]; + int dstOffset = 0; + for (int i = 0; i < dsts.length + 1; i++) { + if (i == offset + length) { + dsts2[i] = readBuffer; + dstOffset = -1; + } else { + dsts2[i] = dsts[i + dstOffset]; + } + } + length2 = length + 1; + getBufHandler().configureReadBufferForWrite(); + overflowState = OverflowState.PROCESSING; + } + } else if (unwrap.getStatus() == Status.CLOSED) { + break; + } else { + throw new IOException(sm.getString("channel.nio.ssl.unwrapFail", unwrap.getStatus())); + } + } while ((netInBuffer.position() != 0 || overflowState == OverflowState.PROCESSING) && + overflowState != OverflowState.DONE); + int capacity = 0; + final int endOffset = offset + length; + for (int i = offset; i < endOffset; i++) { + capacity += dsts[i].remaining(); + } + if (capacity == 0) { + unwrapBeforeRead = true; + } else { + unwrapBeforeRead = false; + } + // If everything is OK, so complete + handler.completed(Long.valueOf(read), attach); + } catch (Exception e) { + failed(e, attach); + } + } + } + @Override + public void failed(Throwable exc, A attach) { + handler.failed(exc, attach); + } + }; + if (unwrapBeforeRead || netInBuffer.position() > 0) { + readCompletionHandler.completed(Integer.valueOf(netInBuffer.position()), attachment); + } else { + sc.read(netInBuffer, timeout, unit, attachment, readCompletionHandler); + } + } + + @Override + public void write(final ByteBuffer src, final long timeout, final TimeUnit unit, + final A attachment, final CompletionHandler handler) { + // Check state + if (closing || closed) { + handler.failed(new IOException(sm.getString("channel.nio.ssl.closing")), attachment); + return; + } + try { + // Prepare the output buffer + netOutBuffer.clear(); + // Wrap the source data into the internal buffer + SSLEngineResult result = sslEngine.wrap(src, netOutBuffer); + final int written = result.bytesConsumed(); + netOutBuffer.flip(); + if (result.getStatus() == Status.OK) { + if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + // Write data to the channel + sc.write(netOutBuffer, timeout, unit, attachment, + new CompletionHandler() { + @Override + public void completed(Integer nBytes, A attach) { + if (nBytes.intValue() < 0) { + failed(new EOFException(), attach); + } else if (netOutBuffer.hasRemaining()) { + sc.write(netOutBuffer, timeout, unit, attachment, this); + } else if (written == 0) { + // Special case, start over to avoid code duplication + write(src, timeout, unit, attachment, handler); + } else { + // Call the handler completed method with the + // consumed bytes number + handler.completed(Integer.valueOf(written), attach); + } + } + @Override + public void failed(Throwable exc, A attach) { + handler.failed(exc, attach); + } + }); + } else { + throw new IOException(sm.getString("channel.nio.ssl.wrapFail", result.getStatus())); + } + } catch (Exception e) { + handler.failed(e, attachment); + } + } + + @Override + public void write(final ByteBuffer[] srcs, final int offset, final int length, + final long timeout, final TimeUnit unit, final A attachment, + final CompletionHandler handler) { + if ((offset < 0) || (length < 0) || (offset > srcs.length - length)) { + throw new IndexOutOfBoundsException(); + } + // Check state + if (closing || closed) { + handler.failed(new IOException(sm.getString("channel.nio.ssl.closing")), attachment); + return; + } + try { + // Prepare the output buffer + netOutBuffer.clear(); + // Wrap the source data into the internal buffer + SSLEngineResult result = sslEngine.wrap(srcs, offset, length, netOutBuffer); + final int written = result.bytesConsumed(); + netOutBuffer.flip(); + if (result.getStatus() == Status.OK) { + if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + // Write data to the channel + sc.write(netOutBuffer, timeout, unit, attachment, new CompletionHandler() { + @Override + public void completed(Integer nBytes, A attach) { + if (nBytes.intValue() < 0) { + failed(new EOFException(), attach); + } else if (netOutBuffer.hasRemaining()) { + sc.write(netOutBuffer, timeout, unit, attachment, this); + } else if (written == 0) { + // Special case, start over to avoid code duplication + write(srcs, offset, length, timeout, unit, attachment, handler); + } else { + // Call the handler completed method with the + // consumed bytes number + handler.completed(Long.valueOf(written), attach); + } + } + @Override + public void failed(Throwable exc, A attach) { + handler.failed(exc, attach); + } + }); + } else { + throw new IOException(sm.getString("channel.nio.ssl.wrapFail", result.getStatus())); + } + } catch (Exception e) { + handler.failed(e, attachment); + } + } + + @Override + public boolean isHandshakeComplete() { + return handshakeComplete; + } + + @Override + public boolean isClosing() { + return closing; + } + + public SSLEngine getSslEngine() { + return sslEngine; + } + + public ByteBuffer getEmptyBuf() { + return emptyBuf; + } + + + private enum OverflowState { + NONE, + PROCESSING, + DONE; + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/net/SecureNioChannel.java b/java/org/apache/tomcat/util/net/SecureNioChannel.java new file mode 100644 index 0000000..f3238f9 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SecureNioChannel.java @@ -0,0 +1,904 @@ +/* + * 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.tomcat.util.net; + +import java.io.EOFException; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteBufferUtils; +import org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper; +import org.apache.tomcat.util.net.TLSClientHelloExtractor.ExtractorResult; +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implementation of a secure socket channel + */ +public class SecureNioChannel extends NioChannel { + + private static final Log log = LogFactory.getLog(SecureNioChannel.class); + private static final StringManager sm = StringManager.getManager(SecureNioChannel.class); + + // Value determined by observation of what the SSL Engine requested in + // various scenarios + private static final int DEFAULT_NET_BUFFER_SIZE = 16921; + + private final NioEndpoint endpoint; + + protected ByteBuffer netInBuffer; + protected ByteBuffer netOutBuffer; + + protected SSLEngine sslEngine; + + protected boolean sniComplete = false; + + protected boolean handshakeComplete = false; + protected HandshakeStatus handshakeStatus; //gets set by handshake + + protected boolean closed = false; + protected boolean closing = false; + + private final Map> additionalTlsAttributes = new HashMap<>(); + + public SecureNioChannel(SocketBufferHandler bufHandler, NioEndpoint endpoint) { + super(bufHandler); + + // Create the network buffers (these hold the encrypted data). + if (endpoint.getSocketProperties().getDirectSslBuffer()) { + netInBuffer = ByteBuffer.allocateDirect(DEFAULT_NET_BUFFER_SIZE); + netOutBuffer = ByteBuffer.allocateDirect(DEFAULT_NET_BUFFER_SIZE); + } else { + netInBuffer = ByteBuffer.allocate(DEFAULT_NET_BUFFER_SIZE); + netOutBuffer = ByteBuffer.allocate(DEFAULT_NET_BUFFER_SIZE); + } + + this.endpoint = endpoint; + } + + @Override + public void reset(SocketChannel channel, NioSocketWrapper socketWrapper) throws IOException { + super.reset(channel, socketWrapper); + sslEngine = null; + sniComplete = false; + handshakeComplete = false; + closed = false; + closing = false; + netInBuffer.clear(); + } + + @Override + public void free() { + super.free(); + if (endpoint.getSocketProperties().getDirectSslBuffer()) { + ByteBufferUtils.cleanDirectBuffer(netInBuffer); + ByteBufferUtils.cleanDirectBuffer(netOutBuffer); + } + } + +//=========================================================================================== +// NIO SSL METHODS +//=========================================================================================== + + /** + * Flushes the buffer to the network, non blocking + * @param buf ByteBuffer + * @return boolean true if the buffer has been emptied out, false otherwise + * @throws IOException An IO error occurred writing data + */ + protected boolean flush(ByteBuffer buf) throws IOException { + int remaining = buf.remaining(); + if (remaining > 0) { + return (sc.write(buf) >= remaining); + } else { + return true; + } + } + + /** + * Performs SSL handshake, non blocking, but performs NEED_TASK on the same + * thread. Hence, you should never call this method using your Acceptor + * thread, as you would slow down your system significantly. If the return + * value from this method is positive, the selection key should be + * registered interestOps given by the return value. + * + * @param read boolean - true if the underlying channel is readable + * @param write boolean - true if the underlying channel is writable + * + * @return 0 if hand shake is complete, -1 if an error (other than an + * IOException) occurred, otherwise it returns a SelectionKey + * interestOps value + * + * @throws IOException If an I/O error occurs during the handshake or if the + * handshake fails during wrapping or unwrapping + */ + @Override + public int handshake(boolean read, boolean write) throws IOException { + if (handshakeComplete) { + return 0; //we have done our initial handshake + } + + if (!sniComplete) { + int sniResult = processSNI(); + if (sniResult == 0) { + sniComplete = true; + } else { + return sniResult; + } + } + + if (!flush(netOutBuffer)) { + return SelectionKey.OP_WRITE; //we still have data to write + } + + SSLEngineResult handshake = null; + + while (!handshakeComplete) { + switch (handshakeStatus) { + case NOT_HANDSHAKING: + //should never happen + throw new IOException(sm.getString("channel.nio.ssl.notHandshaking")); + case FINISHED: + if (endpoint.hasNegotiableProtocols()) { + if (sslEngine instanceof SSLUtil.ProtocolInfo) { + socketWrapper.setNegotiatedProtocol( + ((SSLUtil.ProtocolInfo) sslEngine).getNegotiatedProtocol()); + } else { + socketWrapper.setNegotiatedProtocol(sslEngine.getApplicationProtocol()); + } + } + //we are complete if we have delivered the last package + handshakeComplete = !netOutBuffer.hasRemaining(); + //return 0 if we are complete, otherwise we still have data to write + return handshakeComplete ? 0 : SelectionKey.OP_WRITE; + case NEED_WRAP: + //perform the wrap function + try { + handshake = handshakeWrap(write); + } catch (SSLException e) { + handshake = handshakeWrap(write); + throw e; + } + if (handshake.getStatus() == Status.OK) { + if (handshakeStatus == HandshakeStatus.NEED_TASK) { + handshakeStatus = tasks(); + } + } else if (handshake.getStatus() == Status.CLOSED) { + flush(netOutBuffer); + return -1; + } else { + //wrap should always work with our buffers + throw new IOException(sm.getString("channel.nio.ssl.unexpectedStatusDuringWrap", handshake.getStatus())); + } + if (handshakeStatus != HandshakeStatus.NEED_UNWRAP || (!flush(netOutBuffer))) { + //should actually return OP_READ if we have NEED_UNWRAP + return SelectionKey.OP_WRITE; + } + //fall down to NEED_UNWRAP on the same call, will result in a + //BUFFER_UNDERFLOW if it needs data + //$FALL-THROUGH$ + case NEED_UNWRAP: + //perform the unwrap function + handshake = handshakeUnwrap(read); + if (handshake.getStatus() == Status.OK) { + if (handshakeStatus == HandshakeStatus.NEED_TASK) { + handshakeStatus = tasks(); + } + } else if ( handshake.getStatus() == Status.BUFFER_UNDERFLOW ){ + //read more data, reregister for OP_READ + return SelectionKey.OP_READ; + } else { + throw new IOException(sm.getString("channel.nio.ssl.unexpectedStatusDuringWrap", handshake.getStatus())); + } + break; + case NEED_TASK: + handshakeStatus = tasks(); + break; + default: + throw new IllegalStateException(sm.getString("channel.nio.ssl.invalidStatus", handshakeStatus)); + } + } + // Handshake is complete if this point is reached + return 0; + } + + + /* + * Peeks at the initial network bytes to determine if the SNI extension is + * present and, if it is, what host name has been requested. Based on the + * provided host name, configure the SSLEngine for this connection. + * + * @return 0 if SNI processing is complete, -1 if an error (other than an + * IOException) occurred, otherwise it returns a SelectionKey + * interestOps value + * + * @throws IOException If an I/O error occurs during the SNI processing + */ + private int processSNI() throws IOException { + // Read some data into the network input buffer so we can peek at it. + int bytesRead = sc.read(netInBuffer); + if (bytesRead == -1) { + // Reached end of stream before SNI could be processed. + return -1; + } + TLSClientHelloExtractor extractor = new TLSClientHelloExtractor(netInBuffer); + + while (extractor.getResult() == ExtractorResult.UNDERFLOW && + netInBuffer.capacity() < endpoint.getSniParseLimit()) { + // extractor needed more data to process but netInBuffer was full so + // expand the buffer and read some more data. + int newLimit = Math.min(netInBuffer.capacity() * 2, endpoint.getSniParseLimit()); + log.info(sm.getString("channel.nio.ssl.expandNetInBuffer", + Integer.toString(newLimit))); + + netInBuffer = ByteBufferUtils.expand(netInBuffer, newLimit); + if (sc.read(netInBuffer) < 0) { + return -1; + } + extractor = new TLSClientHelloExtractor(netInBuffer); + } + + String hostName = null; + List clientRequestedCiphers = null; + List clientRequestedApplicationProtocols = null; + switch (extractor.getResult()) { + case COMPLETE: + hostName = extractor.getSNIValue(); + clientRequestedApplicationProtocols = + extractor.getClientRequestedApplicationProtocols(); + //$FALL-THROUGH$ to set the client requested ciphers + case NOT_PRESENT: + clientRequestedCiphers = extractor.getClientRequestedCiphers(); + break; + case NEED_READ: + return SelectionKey.OP_READ; + case UNDERFLOW: + // Unable to buffer enough data to read SNI extension data + if (log.isDebugEnabled()) { + log.debug(sm.getString("channel.nio.ssl.sniDefault")); + } + hostName = endpoint.getDefaultSSLHostConfigName(); + clientRequestedCiphers = Collections.emptyList(); + break; + case NON_SECURE: + netOutBuffer.clear(); + netOutBuffer.put(TLSClientHelloExtractor.USE_TLS_RESPONSE); + netOutBuffer.flip(); + flushOutbound(); + throw new IOException(sm.getString("channel.nio.ssl.foundHttp")); + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("channel.nio.ssl.sniHostName", sc, hostName)); + } + + sslEngine = endpoint.createSSLEngine(hostName, clientRequestedCiphers, + clientRequestedApplicationProtocols); + + // Populate additional TLS attributes obtained from the handshake that + // aren't available from the session + additionalTlsAttributes.put(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY, + extractor.getClientRequestedProtocols()); + additionalTlsAttributes.put(SSLSupport.REQUESTED_CIPHERS_KEY, + extractor.getClientRequestedCipherNames()); + + // Ensure the application buffers (which have to be created earlier) are + // big enough. + getBufHandler().expand(sslEngine.getSession().getApplicationBufferSize()); + if (netOutBuffer.capacity() < sslEngine.getSession().getApplicationBufferSize()) { + // Info for now as we may need to increase DEFAULT_NET_BUFFER_SIZE + log.info(sm.getString("channel.nio.ssl.expandNetOutBuffer", + Integer.toString(sslEngine.getSession().getApplicationBufferSize()))); + } + netInBuffer = ByteBufferUtils.expand(netInBuffer, sslEngine.getSession().getPacketBufferSize()); + netOutBuffer = ByteBufferUtils.expand(netOutBuffer, sslEngine.getSession().getPacketBufferSize()); + + // Set limit and position to expected values + netOutBuffer.position(0); + netOutBuffer.limit(0); + + // Initiate handshake + sslEngine.beginHandshake(); + handshakeStatus = sslEngine.getHandshakeStatus(); + + return 0; + } + + + /** + * Force a blocking handshake to take place for this key. + * This requires that both network and application buffers have been emptied out prior to this call taking place, or a + * IOException will be thrown. + * @param timeout - timeout in milliseconds for each socket operation + * @throws IOException - if an IO exception occurs or if application or network buffers contain data + * @throws SocketTimeoutException - if a socket operation timed out + */ + @SuppressWarnings("null") // key cannot be null + public void rehandshake(long timeout) throws IOException { + //validate the network buffers are empty + if (netInBuffer.position() > 0 && netInBuffer.position() < netInBuffer.limit()) { + throw new IOException(sm.getString("channel.nio.ssl.netInputNotEmpty")); + } + if (netOutBuffer.position() > 0 && netOutBuffer.position() < netOutBuffer.limit()) { + throw new IOException(sm.getString("channel.nio.ssl.netOutputNotEmpty")); + } + if (!getBufHandler().isReadBufferEmpty()) { + throw new IOException(sm.getString("channel.nio.ssl.appInputNotEmpty")); + } + if (!getBufHandler().isWriteBufferEmpty()) { + throw new IOException(sm.getString("channel.nio.ssl.appOutputNotEmpty")); + } + handshakeComplete = false; + boolean isReadable = false; + boolean isWritable = false; + boolean handshaking = true; + Selector selector = null; + SelectionKey key = null; + try { + sslEngine.beginHandshake(); + handshakeStatus = sslEngine.getHandshakeStatus(); + while (handshaking) { + int hsStatus = this.handshake(isReadable, isWritable); + switch (hsStatus) { + case -1 : + throw new EOFException(sm.getString("channel.nio.ssl.eofDuringHandshake")); + case 0 : + handshaking = false; + break; + default : + long now = System.currentTimeMillis(); + if (selector == null) { + selector = Selector.open(); + key = getIOChannel().register(selector, hsStatus); + } else { + key.interestOps(hsStatus); // null warning suppressed + } + int keyCount = selector.select(timeout); + if (keyCount == 0 && ((System.currentTimeMillis()-now) >= timeout)) { + throw new SocketTimeoutException(sm.getString("channel.nio.ssl.timeoutDuringHandshake")); + } + isReadable = key.isReadable(); + isWritable = key.isWritable(); + } + } + } catch (IOException x) { + closeSilently(); + throw x; + } catch (Exception cx) { + closeSilently(); + IOException x = new IOException(cx); + throw x; + } finally { + if (key != null) { + try { + key.cancel(); + } catch (Exception ignore) { + } + } + if (selector != null) { + try { + selector.close(); + } catch (Exception ignore) { + } + } + } + } + + + + /** + * Executes all the tasks needed on the same thread. + * @return the status + */ + protected SSLEngineResult.HandshakeStatus tasks() { + Runnable r = null; + while ((r = sslEngine.getDelegatedTask()) != null) { + r.run(); + } + return sslEngine.getHandshakeStatus(); + } + + /** + * Performs the WRAP function + * @param doWrite boolean + * @return the result + * @throws IOException An IO error occurred + */ + protected SSLEngineResult handshakeWrap(boolean doWrite) throws IOException { + //this should never be called with a network buffer that contains data + //so we can clear it here. + netOutBuffer.clear(); + //perform the wrap + getBufHandler().configureWriteBufferForRead(); + SSLEngineResult result = sslEngine.wrap(getBufHandler().getWriteBuffer(), netOutBuffer); + //prepare the results to be written + netOutBuffer.flip(); + //set the status + handshakeStatus = result.getHandshakeStatus(); + //optimization, if we do have a writable channel, write it now + if (doWrite) { + flush(netOutBuffer); + } + return result; + } + + /** + * Perform handshake unwrap + * @param doread boolean + * @return the result + * @throws IOException An IO error occurred + */ + protected SSLEngineResult handshakeUnwrap(boolean doread) throws IOException { + + if (doread) { + //if we have data to read, read it + int read = sc.read(netInBuffer); + if (read == -1) { + throw new IOException(sm.getString("channel.nio.ssl.eofDuringHandshake")); + } + } + SSLEngineResult result; + boolean cont = false; + //loop while we can perform pure SSLEngine data + do { + //prepare the buffer with the incoming data + netInBuffer.flip(); + //call unwrap + getBufHandler().configureReadBufferForWrite(); + result = sslEngine.unwrap(netInBuffer, getBufHandler().getReadBuffer()); + /* + * ByteBuffer.compact() is an optional method but netInBuffer is created from either ByteBuffer.allocate() + * or ByteBuffer.allocateDirect() and the ByteBuffers returned by those methods do implement compact(). + * The ByteBuffer must be in 'read from' mode when compact() is called and will be in 'write to' mode + * afterwards. + */ + netInBuffer.compact(); + //read in the status + handshakeStatus = result.getHandshakeStatus(); + if (result.getStatus() == SSLEngineResult.Status.OK && + result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + //execute tasks if we need to + handshakeStatus = tasks(); + } + //perform another unwrap? + cont = result.getStatus() == SSLEngineResult.Status.OK && + handshakeStatus == HandshakeStatus.NEED_UNWRAP; + } while (cont); + return result; + } + + public SSLSupport getSSLSupport() { + if (sslEngine != null) { + SSLSession session = sslEngine.getSession(); + return endpoint.getSslImplementation().getSSLSupport(session, additionalTlsAttributes); + } + return null; + } + + /** + * Sends an SSL close message, will not physically close the connection here. + *
    To close the connection, you could do something like + *
    
    +     *   close();
    +     *   while (isOpen() && !myTimeoutFunction()) Thread.sleep(25);
    +     *   if ( isOpen() ) close(true); //forces a close if you timed out
    +     * 
    + * @throws IOException if an I/O error occurs + * @throws IOException if there is data on the outgoing network buffer and + * we are unable to flush it + */ + @Override + public void close() throws IOException { + if (closing) { + return; + } + closing = true; + if (sslEngine == null) { + netOutBuffer.clear(); + closed = true; + return; + } + sslEngine.closeOutbound(); + + if (!flush(netOutBuffer)) { + throw new IOException(sm.getString("channel.nio.ssl.remainingDataDuringClose")); + } + //prep the buffer for the close message + netOutBuffer.clear(); + //perform the close, since we called sslEngine.closeOutbound + SSLEngineResult handshake = sslEngine.wrap(getEmptyBuf(), netOutBuffer); + //we should be in a close state + if (handshake.getStatus() != SSLEngineResult.Status.CLOSED) { + throw new IOException(sm.getString("channel.nio.ssl.invalidCloseState")); + } + //prepare the buffer for writing + netOutBuffer.flip(); + //if there is data to be written + flush(netOutBuffer); + + //is the channel closed? + closed = (!netOutBuffer.hasRemaining() && (handshake.getHandshakeStatus() != HandshakeStatus.NEED_WRAP)); + } + + + @Override + public void close(boolean force) throws IOException { + try { + close(); + } finally { + if (force || closed) { + closed = true; + sc.close(); + } + } + } + + + private void closeSilently() { + try { + close(true); + } catch (IOException ioe) { + // This is expected - swallowing the exception is the reason this + // method exists. Log at debug in case someone is interested. + log.debug(sm.getString("channel.nio.ssl.closeSilentError"), ioe); + } + } + + + /** + * Reads a sequence of bytes from this channel into the given buffer. + * + * @param dst The buffer into which bytes are to be transferred + * @return The number of bytes read, possibly zero, or -1 if + * the channel has reached end-of-stream + * @throws IOException If some other I/O error occurs + * @throws IllegalArgumentException if the destination buffer is different + * than getBufHandler().getReadBuffer() + */ + @Override + public int read(ByteBuffer dst) throws IOException { + //are we in the middle of closing or closed? + if (closing || closed) { + return -1; + } + //did we finish our handshake? + if (!handshakeComplete) { + throw new IllegalStateException(sm.getString("channel.nio.ssl.incompleteHandshake")); + } + + //read from the network + int netread = sc.read(netInBuffer); + //did we reach EOF? if so send EOF up one layer. + if (netread == -1) { + return -1; + } + + //the data read + int read = 0; + //the SSL engine result + SSLEngineResult unwrap; + do { + //prepare the buffer + netInBuffer.flip(); + //unwrap the data + unwrap = sslEngine.unwrap(netInBuffer, dst); + //compact the buffer + netInBuffer.compact(); + + if (unwrap.getStatus() == Status.OK || unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + //we did receive some data, add it to our total + read += unwrap.bytesProduced(); + //perform any tasks if needed + if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + //if we need more network data, then bail out for now. + if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + break; + } + } else if (unwrap.getStatus() == Status.BUFFER_OVERFLOW) { + if (read > 0) { + // Buffer overflow can happen if we have read data. Return + // so the destination buffer can be emptied before another + // read is attempted + break; + } else { + // The SSL session has increased the required buffer size + // since the buffer was created. + if (dst == getBufHandler().getReadBuffer()) { + // This is the normal case for this code + getBufHandler().expand(sslEngine.getSession().getApplicationBufferSize()); + dst = getBufHandler().getReadBuffer(); + } else if (getAppReadBufHandler() != null && dst == getAppReadBufHandler().getByteBuffer()) { + getAppReadBufHandler().expand(sslEngine.getSession().getApplicationBufferSize()); + dst = getAppReadBufHandler().getByteBuffer(); + } else { + // Can't expand the buffer as there is no way to signal + // to the caller that the buffer has been replaced. + throw new IOException( + sm.getString("channel.nio.ssl.unwrapFailResize", unwrap.getStatus())); + } + } + } else if (unwrap.getStatus() == Status.CLOSED && netInBuffer.position() == 0 && read > 0) { + // Clean TLS close on input side but there is application data + // to process. Can't tell if the client closed the connection + // mid-request or if the client is performing a half-close after + // a complete request. Assume it is a half-close and allow + // processing to continue. If the connection has been closed + // mid-request then the next attempt to read will trigger an + // EOF. + } else { + // Something else went wrong + throw new IOException(sm.getString("channel.nio.ssl.unwrapFail", unwrap.getStatus())); + } + } while (netInBuffer.position() != 0); //continue to unwrapping as long as the input buffer has stuff + return read; + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) + throws IOException { + //are we in the middle of closing or closed? + if (closing || closed) { + return -1; + } + //did we finish our handshake? + if (!handshakeComplete) { + throw new IllegalStateException(sm.getString("channel.nio.ssl.incompleteHandshake")); + } + + //read from the network + int netread = sc.read(netInBuffer); + //did we reach EOF? if so send EOF up one layer. + if (netread == -1) { + return -1; + } + + //the data read + int read = 0; + //the SSL engine result + SSLEngineResult unwrap; + OverflowState overflowState = OverflowState.NONE; + do { + if (overflowState == OverflowState.PROCESSING) { + overflowState = OverflowState.DONE; + } + //prepare the buffer + netInBuffer.flip(); + //unwrap the data + unwrap = sslEngine.unwrap(netInBuffer, dsts, offset, length); + //compact the buffer + netInBuffer.compact(); + + if (unwrap.getStatus() == Status.OK || unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + //we did receive some data, add it to our total + read += unwrap.bytesProduced(); + if (overflowState == OverflowState.DONE) { + // Remove the data read into the overflow buffer + read -= getBufHandler().getReadBuffer().position(); + } + //perform any tasks if needed + if (unwrap.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + //if we need more network data, then bail out for now. + if (unwrap.getStatus() == Status.BUFFER_UNDERFLOW) { + break; + } + } else if (unwrap.getStatus() == Status.BUFFER_OVERFLOW) { + if (read > 0) { + // Buffer overflow can happen if we have read data. Return + // so the destination buffer can be emptied before another + // read is attempted + break; + } else { + ByteBuffer readBuffer = getBufHandler().getReadBuffer(); + boolean found = false; + boolean resized = true; + for (int i = 0; i < length; i++) { + // The SSL session has increased the required buffer size + // since the buffer was created. + if (dsts[offset + i] == getBufHandler().getReadBuffer()) { + getBufHandler().expand(sslEngine.getSession().getApplicationBufferSize()); + if (dsts[offset + i] == getBufHandler().getReadBuffer()) { + resized = false; + } + dsts[offset + i] = getBufHandler().getReadBuffer(); + found = true; + } else if (getAppReadBufHandler() != null && dsts[offset + i] == getAppReadBufHandler().getByteBuffer()) { + getAppReadBufHandler().expand(sslEngine.getSession().getApplicationBufferSize()); + if (dsts[offset + i] == getAppReadBufHandler().getByteBuffer()) { + resized = false; + } + dsts[offset + i] = getAppReadBufHandler().getByteBuffer(); + found = true; + } + } + if (found) { + if (!resized) { + throw new IOException(sm.getString("channel.nio.ssl.unwrapFail", unwrap.getStatus())); + } + } else { + // Add the main read buffer in the destinations and try again + ByteBuffer[] dsts2 = new ByteBuffer[dsts.length + 1]; + int dstOffset = 0; + for (int i = 0; i < dsts.length + 1; i++) { + if (i == offset + length) { + dsts2[i] = readBuffer; + dstOffset = -1; + } else { + dsts2[i] = dsts[i + dstOffset]; + } + } + dsts = dsts2; + length++; + getBufHandler().configureReadBufferForWrite(); + overflowState = OverflowState.PROCESSING; + } + } + } else { + // Something else went wrong + throw new IOException(sm.getString("channel.nio.ssl.unwrapFail", unwrap.getStatus())); + } + } while ((netInBuffer.position() != 0 || overflowState == OverflowState.PROCESSING) && + overflowState != OverflowState.DONE); + return read; + } + + /** + * Writes a sequence of bytes to this channel from the given buffer. + * + * @param src The buffer from which bytes are to be retrieved + * @return The number of bytes written, possibly zero + * @throws IOException If some other I/O error occurs + */ + @Override + public int write(ByteBuffer src) throws IOException { + checkInterruptStatus(); + if (src == this.netOutBuffer) { + int written = sc.write(src); + return written; + } else { + // Are we closing or closed? + if (closing || closed) { + throw new IOException(sm.getString("channel.nio.ssl.closing")); + } + + if (!flush(netOutBuffer)) { + // We haven't emptied out the buffer yet + return 0; + } + + if (!src.hasRemaining()) { + // Nothing left to write + return 0; + } + + // The data buffer is empty, we can reuse the entire buffer. + netOutBuffer.clear(); + + SSLEngineResult result = sslEngine.wrap(src, netOutBuffer); + // The number of bytes written + int written = result.bytesConsumed(); + netOutBuffer.flip(); + + if (result.getStatus() == Status.OK) { + if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + } else { + throw new IOException(sm.getString("channel.nio.ssl.wrapFail", result.getStatus())); + } + + // Force a flush + flush(netOutBuffer); + + return written; + } + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) + throws IOException { + checkInterruptStatus(); + // Are we closing or closed? + if (closing || closed) { + throw new IOException(sm.getString("channel.nio.ssl.closing")); + } + + if (!flush(netOutBuffer)) { + // We haven't emptied out the buffer yet + return 0; + } + + // The data buffer is empty, we can reuse the entire buffer. + netOutBuffer.clear(); + + SSLEngineResult result = sslEngine.wrap(srcs, offset, length, netOutBuffer); + // The number of bytes written + int written = result.bytesConsumed(); + netOutBuffer.flip(); + + if (result.getStatus() == Status.OK) { + if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + } else { + throw new IOException(sm.getString("channel.nio.ssl.wrapFail", result.getStatus())); + } + + // Force a flush + flush(netOutBuffer); + + return written; + } + + @Override + public int getOutboundRemaining() { + return netOutBuffer.remaining(); + } + + @Override + public boolean flushOutbound() throws IOException { + int remaining = netOutBuffer.remaining(); + flush(netOutBuffer); + int remaining2 = netOutBuffer.remaining(); + return remaining2 < remaining; + } + + @Override + public boolean isHandshakeComplete() { + return handshakeComplete; + } + + @Override + public boolean isClosing() { + return closing; + } + + public SSLEngine getSslEngine() { + return sslEngine; + } + + public ByteBuffer getEmptyBuf() { + return emptyBuf; + } + + + private enum OverflowState { + NONE, + PROCESSING, + DONE; + } +} diff --git a/java/org/apache/tomcat/util/net/SendfileDataBase.java b/java/org/apache/tomcat/util/net/SendfileDataBase.java new file mode 100644 index 0000000..ca0ee3b --- /dev/null +++ b/java/org/apache/tomcat/util/net/SendfileDataBase.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.util.net; + +public abstract class SendfileDataBase { + + /** + * Is the current request being processed on a keep-alive connection? This + * determines if the socket is closed once the send file completes or if + * processing continues with the next request on the connection or waiting + * for that next request to arrive. + */ + public SendfileKeepAliveState keepAliveState = SendfileKeepAliveState.NONE; + + /** + * The full path to the file that contains the data to be written to the + * socket. + */ + public final String fileName; + + /** + * The position of the next byte in the file to be written to the socket. + * This is initialised to the start point and then updated as the file is + * written. + */ + public long pos; + + /** + * The number of bytes remaining to be written from the file (from the + * current {@link #pos}. This is initialised to the end point - the start + * point and then updated as the file is written. + */ + public long length; + + public SendfileDataBase(String filename, long pos, long length) { + this.fileName = filename; + this.pos = pos; + this.length = length; + } +} diff --git a/java/org/apache/tomcat/util/net/SendfileKeepAliveState.java b/java/org/apache/tomcat/util/net/SendfileKeepAliveState.java new file mode 100644 index 0000000..b27a9f1 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SendfileKeepAliveState.java @@ -0,0 +1,39 @@ +/* + * 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.tomcat.util.net; + +public enum SendfileKeepAliveState { + + /** + * Keep-alive is not in use. The socket can be closed when the response has + * been written. + */ + NONE, + + /** + * Keep-alive is in use and there is pipelined data in the input buffer to + * be read as soon as the current response has been written. + */ + PIPELINED, + + /** + * Keep-alive is in use. The socket should be added to the poller (or + * equivalent) to await more data as soon as the current response has been + * written. + */ + OPEN +} diff --git a/java/org/apache/tomcat/util/net/SendfileState.java b/java/org/apache/tomcat/util/net/SendfileState.java new file mode 100644 index 0000000..b354e2f --- /dev/null +++ b/java/org/apache/tomcat/util/net/SendfileState.java @@ -0,0 +1,37 @@ +/* + * 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.tomcat.util.net; + +public enum SendfileState { + + /** + * The sending of the file has started but has not completed. Sendfile is + * still using the socket. + */ + PENDING, + + /** + * The file has been fully sent. Sendfile is no longer using the socket. + */ + DONE, + + /** + * Something went wrong. The file may or may not have been sent. The socket + * is in an unknown state. + */ + ERROR +} diff --git a/java/org/apache/tomcat/util/net/ServletConnectionImpl.java b/java/org/apache/tomcat/util/net/ServletConnectionImpl.java new file mode 100644 index 0000000..9b32dc7 --- /dev/null +++ b/java/org/apache/tomcat/util/net/ServletConnectionImpl.java @@ -0,0 +1,55 @@ +/* + * 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.tomcat.util.net; + +import jakarta.servlet.ServletConnection; + + +public class ServletConnectionImpl implements ServletConnection { + + private final String connectionId; + private final String protocol; + private final String protocolConnectionId; + private final boolean secure; + + public ServletConnectionImpl(String connectionId, String protocol, String protocolConnectionId, boolean secure) { + this.connectionId = connectionId; + this.protocol = protocol; + this.protocolConnectionId = protocolConnectionId; + this.secure = secure; + } + + @Override + public String getConnectionId() { + return connectionId; + } + + @Override + public String getProtocol() { + return protocol; + } + + @Override + public String getProtocolConnectionId() { + return protocolConnectionId; + } + + @Override + public boolean isSecure() { + return secure; + } +} diff --git a/java/org/apache/tomcat/util/net/SocketBufferHandler.java b/java/org/apache/tomcat/util/net/SocketBufferHandler.java new file mode 100644 index 0000000..78adf30 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SocketBufferHandler.java @@ -0,0 +1,234 @@ +/* + * 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.tomcat.util.net; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +import org.apache.tomcat.util.buf.ByteBufferUtils; + +public class SocketBufferHandler { + + static SocketBufferHandler EMPTY = new SocketBufferHandler(0, 0, false) { + @Override + public void expand(int newSize) { + } + /* + * Http2AsyncParser$FrameCompletionHandler will return incomplete + * frame(s) to the buffer. If the previous frame (or concurrent write to + * a stream) triggered a connection close this call would fail with a + * BufferOverflowException as data can't be returned to a buffer of zero + * length. Override the method and make it a NO-OP to avoid triggering + * the exception. + */ + @Override + public void unReadReadBuffer(ByteBuffer returnedData) { + } + }; + + private volatile boolean readBufferConfiguredForWrite = true; + private volatile ByteBuffer readBuffer; + + private volatile boolean writeBufferConfiguredForWrite = true; + private volatile ByteBuffer writeBuffer; + + private final boolean direct; + + public SocketBufferHandler(int readBufferSize, int writeBufferSize, + boolean direct) { + this.direct = direct; + if (direct) { + readBuffer = ByteBuffer.allocateDirect(readBufferSize); + writeBuffer = ByteBuffer.allocateDirect(writeBufferSize); + } else { + readBuffer = ByteBuffer.allocate(readBufferSize); + writeBuffer = ByteBuffer.allocate(writeBufferSize); + } + } + + + public void configureReadBufferForWrite() { + setReadBufferConfiguredForWrite(true); + } + + + public void configureReadBufferForRead() { + setReadBufferConfiguredForWrite(false); + } + + + private void setReadBufferConfiguredForWrite(boolean readBufferConFiguredForWrite) { + // NO-OP if buffer is already in correct state + if (this.readBufferConfiguredForWrite != readBufferConFiguredForWrite) { + if (readBufferConFiguredForWrite) { + // Switching to write + int remaining = readBuffer.remaining(); + if (remaining == 0) { + readBuffer.clear(); + } else { + readBuffer.compact(); + } + } else { + // Switching to read + readBuffer.flip(); + } + this.readBufferConfiguredForWrite = readBufferConFiguredForWrite; + } + } + + + public ByteBuffer getReadBuffer() { + return readBuffer; + } + + + public boolean isReadBufferEmpty() { + if (readBufferConfiguredForWrite) { + return readBuffer.position() == 0; + } else { + return readBuffer.remaining() == 0; + } + } + + + public void unReadReadBuffer(ByteBuffer returnedData) { + if (isReadBufferEmpty()) { + configureReadBufferForWrite(); + readBuffer.put(returnedData); + } else { + int bytesReturned = returnedData.remaining(); + if (readBufferConfiguredForWrite) { + // Writes always start at position zero + if ((readBuffer.position() + bytesReturned) > readBuffer.capacity()) { + throw new BufferOverflowException(); + } else { + // Move the bytes up to make space for the returned data + for (int i = 0; i < readBuffer.position(); i++) { + readBuffer.put(i + bytesReturned, readBuffer.get(i)); + } + // Insert the bytes returned + for (int i = 0; i < bytesReturned; i++) { + readBuffer.put(i, returnedData.get()); + } + // Update the position + readBuffer.position(readBuffer.position() + bytesReturned); + } + } else { + // Reads will start at zero but may have progressed + int shiftRequired = bytesReturned - readBuffer.position(); + if (shiftRequired > 0) { + if ((readBuffer.capacity() - readBuffer.limit()) < shiftRequired) { + throw new BufferOverflowException(); + } + // Move the bytes up to make space for the returned data + int oldLimit = readBuffer.limit(); + readBuffer.limit(oldLimit + shiftRequired); + for (int i = readBuffer.position(); i < oldLimit; i++) { + readBuffer.put(i + shiftRequired, readBuffer.get(i)); + } + } else { + shiftRequired = 0; + } + // Insert the returned bytes + int insertOffset = readBuffer.position() + shiftRequired - bytesReturned; + for (int i = insertOffset; i < bytesReturned + insertOffset; i++) { + readBuffer.put(i, returnedData.get()); + } + readBuffer.position(insertOffset); + } + } + } + + + public void configureWriteBufferForWrite() { + setWriteBufferConfiguredForWrite(true); + } + + + public void configureWriteBufferForRead() { + setWriteBufferConfiguredForWrite(false); + } + + + private void setWriteBufferConfiguredForWrite(boolean writeBufferConfiguredForWrite) { + // NO-OP if buffer is already in correct state + if (this.writeBufferConfiguredForWrite != writeBufferConfiguredForWrite) { + if (writeBufferConfiguredForWrite) { + // Switching to write + int remaining = writeBuffer.remaining(); + if (remaining == 0) { + writeBuffer.clear(); + } else { + writeBuffer.compact(); + writeBuffer.position(remaining); + writeBuffer.limit(writeBuffer.capacity()); + } + } else { + // Switching to read + writeBuffer.flip(); + } + this.writeBufferConfiguredForWrite = writeBufferConfiguredForWrite; + } + } + + + public boolean isWriteBufferWritable() { + if (writeBufferConfiguredForWrite) { + return writeBuffer.hasRemaining(); + } else { + return writeBuffer.remaining() == 0; + } + } + + + public ByteBuffer getWriteBuffer() { + return writeBuffer; + } + + + public boolean isWriteBufferEmpty() { + if (writeBufferConfiguredForWrite) { + return writeBuffer.position() == 0; + } else { + return writeBuffer.remaining() == 0; + } + } + + + public void reset() { + readBuffer.clear(); + readBufferConfiguredForWrite = true; + writeBuffer.clear(); + writeBufferConfiguredForWrite = true; + } + + + public void expand(int newSize) { + configureReadBufferForWrite(); + readBuffer = ByteBufferUtils.expand(readBuffer, newSize); + configureWriteBufferForWrite(); + writeBuffer = ByteBufferUtils.expand(writeBuffer, newSize); + } + + public void free() { + if (direct) { + ByteBufferUtils.cleanDirectBuffer(readBuffer); + ByteBufferUtils.cleanDirectBuffer(writeBuffer); + } + } + +} diff --git a/java/org/apache/tomcat/util/net/SocketEvent.java b/java/org/apache/tomcat/util/net/SocketEvent.java new file mode 100644 index 0000000..9d86028 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SocketEvent.java @@ -0,0 +1,73 @@ +/* + * 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.tomcat.util.net; + +/** + * Defines events that occur per socket that require further processing by the + * container. Usually these events are triggered by the socket implementation + * but they may be triggered by the container. + */ +public enum SocketEvent { + + /** + * Data is available to be read. + */ + OPEN_READ, + + /** + * The socket is ready to be written to. + */ + OPEN_WRITE, + + /** + * The associated Connector/Endpoint is stopping and the connection/socket + * needs to be closed cleanly. + */ + STOP, + + /** + * A timeout has occurred and the connection needs to be closed cleanly. + * Currently this is only used by the Servlet 3.0 async processing. + */ + TIMEOUT, + + /** + * The client has disconnected. + */ + DISCONNECT, + + /** + * An error has occurred on a non-container thread and processing needs to + * return to the container for any necessary clean-up. Examples of where + * this is used include: + *
      + *
    • by NIO2 to signal the failure of a completion handler
    • + *
    • by the container to signal an I/O error on a non-container thread + * during Servlet 3.0 asynchronous processing.
    • + *
    + */ + ERROR, + + /** + * A client attempted to establish a connection but failed. Examples of + * where this is used include: + *
      + *
    • TLS handshake failures
    • + *
    + */ + CONNECT_FAIL +} diff --git a/java/org/apache/tomcat/util/net/SocketProcessorBase.java b/java/org/apache/tomcat/util/net/SocketProcessorBase.java new file mode 100644 index 0000000..ab11ce9 --- /dev/null +++ b/java/org/apache/tomcat/util/net/SocketProcessorBase.java @@ -0,0 +1,60 @@ +/* + * 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.tomcat.util.net; + +import java.util.Objects; +import java.util.concurrent.locks.Lock; + +public abstract class SocketProcessorBase implements Runnable { + + protected SocketWrapperBase socketWrapper; + protected SocketEvent event; + + public SocketProcessorBase(SocketWrapperBase socketWrapper, SocketEvent event) { + reset(socketWrapper, event); + } + + + public void reset(SocketWrapperBase socketWrapper, SocketEvent event) { + Objects.requireNonNull(event); + this.socketWrapper = socketWrapper; + this.event = event; + } + + + @Override + public final void run() { + Lock lock = socketWrapper.getLock(); + lock.lock(); + try { + // It is possible that processing may be triggered for read and + // write at the same time. The lock above makes sure that processing + // does not occur in parallel. The test below ensures that if the + // first event to be processed results in the socket being closed, + // the subsequent events are not processed. + if (socketWrapper.isClosed()) { + return; + } + doRun(); + } finally { + lock.unlock(); + } + } + + + protected abstract void doRun(); +} diff --git a/java/org/apache/tomcat/util/net/SocketProperties.java b/java/org/apache/tomcat/util/net/SocketProperties.java new file mode 100644 index 0000000..b91d54f --- /dev/null +++ b/java/org/apache/tomcat/util/net/SocketProperties.java @@ -0,0 +1,503 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.StandardSocketOptions; +import java.nio.channels.AsynchronousServerSocketChannel; +import java.nio.channels.AsynchronousSocketChannel; + +import javax.management.ObjectName; + +/** + * Properties that can be set in the <Connector> element + * in server.xml. All properties are prefixed with "socket." + * and are currently only working for the Nio connector + */ +public class SocketProperties { + + /** + * Enable/disable socket processor cache, this bounded cache stores + * SocketProcessor objects to reduce GC + * Default is 0 + * -1 is unlimited + * 0 is disabled + */ + protected int processorCache = 0; + + /** + * Enable/disable poller event cache, this bounded cache stores + * PollerEvent objects to reduce GC for the poller + * Default is 0 + * -1 is unlimited + * 0 is disabled + * >0 the max number of objects to keep in cache. + */ + protected int eventCache = 0; + + /** + * Enable/disable direct buffers for the network buffers + * Default value is disabled + */ + protected boolean directBuffer = false; + + /** + * Enable/disable direct buffers for the network buffers for SSL + * Default value is disabled + */ + protected boolean directSslBuffer = false; + + /** + * Socket receive buffer size in bytes (SO_RCVBUF). + * JVM default used if not set. + */ + protected Integer rxBufSize = null; + + /** + * Socket send buffer size in bytes (SO_SNDBUF). + * JVM default used if not set. + */ + protected Integer txBufSize = null; + + /** + * The application read buffer size in bytes. + * Default value is 8192 + */ + protected int appReadBufSize = 8192; + + /** + * The application write buffer size in bytes + * Default value is 8192 + */ + protected int appWriteBufSize = 8192; + + /** + * NioChannel pool size for the endpoint, + * this value is how many channels + * -1 means unlimited cached, 0 means no cache, + * -2 means bufferPoolSize will be used + * Default value is -2 + */ + protected int bufferPool = -2; + + /** + * Buffer pool size in bytes to be cached + * -1 means unlimited, 0 means no cache + * Default value is based on the max memory reported by the JVM, + * if less than 1GB, then 0, else the value divided by 32. This value + * will then be used to compute bufferPool if its value is -2 + */ + protected int bufferPoolSize = -2; + + /** + * TCP_NO_DELAY option. JVM default used if not set. + */ + protected Boolean tcpNoDelay = Boolean.TRUE; + + /** + * SO_KEEPALIVE option. JVM default used if not set. + */ + protected Boolean soKeepAlive = null; + + /** + * OOBINLINE option. JVM default used if not set. + */ + protected Boolean ooBInline = null; + + /** + * SO_REUSEADDR option. JVM default used if not set. + */ + protected Boolean soReuseAddress = null; + + /** + * SO_LINGER option, paired with the soLingerTime value. + * JVM defaults used unless both attributes are set. + */ + protected Boolean soLingerOn = null; + + /** + * SO_LINGER option, paired with the soLingerOn value. + * JVM defaults used unless both attributes are set. + */ + protected Integer soLingerTime = null; + + /** + * SO_TIMEOUT option. default is 20000. + */ + protected Integer soTimeout = Integer.valueOf(20000); + + /** + * Performance preferences according to + * http://docs.oracle.com/javase/1.5.0/docs/api/java/net/Socket.html#setPerformancePreferences(int,%20int,%20int) + * All three performance attributes must be set or the JVM defaults will be + * used. + */ + protected Integer performanceConnectionTime = null; + + /** + * Performance preferences according to + * http://docs.oracle.com/javase/1.5.0/docs/api/java/net/Socket.html#setPerformancePreferences(int,%20int,%20int) + * All three performance attributes must be set or the JVM defaults will be + * used. + */ + protected Integer performanceLatency = null; + + /** + * Performance preferences according to + * http://docs.oracle.com/javase/1.5.0/docs/api/java/net/Socket.html#setPerformancePreferences(int,%20int,%20int) + * All three performance attributes must be set or the JVM defaults will be + * used. + */ + protected Integer performanceBandwidth = null; + + /** + * The minimum frequency of the timeout interval to avoid excess load from + * the poller during high traffic + */ + protected long timeoutInterval = 1000; + + /** + * Timeout in milliseconds for an unlock to take place. + */ + protected int unlockTimeout = 250; + + private ObjectName oname = null; + + + public void setProperties(Socket socket) throws SocketException{ + if (rxBufSize != null) { + socket.setReceiveBufferSize(rxBufSize.intValue()); + } + if (txBufSize != null) { + socket.setSendBufferSize(txBufSize.intValue()); + } + if (ooBInline !=null) { + socket.setOOBInline(ooBInline.booleanValue()); + } + if (soKeepAlive != null) { + socket.setKeepAlive(soKeepAlive.booleanValue()); + } + if (performanceConnectionTime != null && performanceLatency != null && + performanceBandwidth != null) { + socket.setPerformancePreferences( + performanceConnectionTime.intValue(), + performanceLatency.intValue(), + performanceBandwidth.intValue()); + } + if (soReuseAddress != null) { + socket.setReuseAddress(soReuseAddress.booleanValue()); + } + if (soLingerOn != null && soLingerTime != null) { + socket.setSoLinger(soLingerOn.booleanValue(), + soLingerTime.intValue()); + } + if (soTimeout != null && soTimeout.intValue() >= 0) { + socket.setSoTimeout(soTimeout.intValue()); + } + if (tcpNoDelay != null) { + try { + socket.setTcpNoDelay(tcpNoDelay.booleanValue()); + } catch (SocketException e) { + // Some socket types may not support this option which is set by default + } + } + } + + public void setProperties(ServerSocket socket) throws SocketException{ + if (rxBufSize != null) { + socket.setReceiveBufferSize(rxBufSize.intValue()); + } + if (performanceConnectionTime != null && performanceLatency != null && + performanceBandwidth != null) { + socket.setPerformancePreferences( + performanceConnectionTime.intValue(), + performanceLatency.intValue(), + performanceBandwidth.intValue()); + } + if (soReuseAddress != null) { + socket.setReuseAddress(soReuseAddress.booleanValue()); + } + if (soTimeout != null && soTimeout.intValue() >= 0) { + socket.setSoTimeout(soTimeout.intValue()); + } + } + + public void setProperties(AsynchronousSocketChannel socket) throws IOException { + if (rxBufSize != null) { + socket.setOption(StandardSocketOptions.SO_RCVBUF, rxBufSize); + } + if (txBufSize != null) { + socket.setOption(StandardSocketOptions.SO_SNDBUF, txBufSize); + } + if (soKeepAlive != null) { + socket.setOption(StandardSocketOptions.SO_KEEPALIVE, soKeepAlive); + } + if (soReuseAddress != null) { + socket.setOption(StandardSocketOptions.SO_REUSEADDR, soReuseAddress); + } + if (soLingerOn != null && soLingerOn.booleanValue() && soLingerTime != null) { + socket.setOption(StandardSocketOptions.SO_LINGER, soLingerTime); + } + if (tcpNoDelay != null) { + socket.setOption(StandardSocketOptions.TCP_NODELAY, tcpNoDelay); + } + } + + public void setProperties(AsynchronousServerSocketChannel socket) throws IOException { + if (rxBufSize != null) { + socket.setOption(StandardSocketOptions.SO_RCVBUF, rxBufSize); + } + if (soReuseAddress != null) { + socket.setOption(StandardSocketOptions.SO_REUSEADDR, soReuseAddress); + } + } + + public boolean getDirectBuffer() { + return directBuffer; + } + + public boolean getDirectSslBuffer() { + return directSslBuffer; + } + + public boolean getOoBInline() { + return ooBInline.booleanValue(); + } + + public int getPerformanceBandwidth() { + return performanceBandwidth.intValue(); + } + + public int getPerformanceConnectionTime() { + return performanceConnectionTime.intValue(); + } + + public int getPerformanceLatency() { + return performanceLatency.intValue(); + } + + public int getRxBufSize() { + return rxBufSize.intValue(); + } + + public boolean getSoKeepAlive() { + return soKeepAlive.booleanValue(); + } + + public boolean getSoLingerOn() { + return soLingerOn.booleanValue(); + } + + public int getSoLingerTime() { + return soLingerTime.intValue(); + } + + public boolean getSoReuseAddress() { + return soReuseAddress.booleanValue(); + } + + public int getSoTimeout() { + return soTimeout.intValue(); + } + + public boolean getTcpNoDelay() { + return tcpNoDelay.booleanValue(); + } + + public int getTxBufSize() { + return txBufSize.intValue(); + } + + public int getBufferPool() { + return bufferPool; + } + + public int getBufferPoolSize() { + return bufferPoolSize; + } + + public int getEventCache() { + return eventCache; + } + + public int getAppReadBufSize() { + return appReadBufSize; + } + + public int getAppWriteBufSize() { + return appWriteBufSize; + } + + public int getProcessorCache() { + return processorCache; + } + + public long getTimeoutInterval() { + return timeoutInterval; + } + + public int getDirectBufferPool() { + return bufferPool; + } + + public void setPerformanceConnectionTime(int performanceConnectionTime) { + this.performanceConnectionTime = + Integer.valueOf(performanceConnectionTime); + } + + public void setTxBufSize(int txBufSize) { + this.txBufSize = Integer.valueOf(txBufSize); + } + + public void setTcpNoDelay(boolean tcpNoDelay) { + this.tcpNoDelay = Boolean.valueOf(tcpNoDelay); + } + + public void setSoTimeout(int soTimeout) { + this.soTimeout = Integer.valueOf(soTimeout); + } + + public void setSoReuseAddress(boolean soReuseAddress) { + this.soReuseAddress = Boolean.valueOf(soReuseAddress); + } + + public void setSoLingerTime(int soLingerTime) { + this.soLingerTime = Integer.valueOf(soLingerTime); + } + + public void setSoKeepAlive(boolean soKeepAlive) { + this.soKeepAlive = Boolean.valueOf(soKeepAlive); + } + + public void setRxBufSize(int rxBufSize) { + this.rxBufSize = Integer.valueOf(rxBufSize); + } + + public void setPerformanceLatency(int performanceLatency) { + this.performanceLatency = Integer.valueOf(performanceLatency); + } + + public void setPerformanceBandwidth(int performanceBandwidth) { + this.performanceBandwidth = Integer.valueOf(performanceBandwidth); + } + + public void setOoBInline(boolean ooBInline) { + this.ooBInline = Boolean.valueOf(ooBInline); + } + + public void setDirectBuffer(boolean directBuffer) { + this.directBuffer = directBuffer; + } + + public void setDirectSslBuffer(boolean directSslBuffer) { + this.directSslBuffer = directSslBuffer; + } + + public void setSoLingerOn(boolean soLingerOn) { + this.soLingerOn = Boolean.valueOf(soLingerOn); + } + + public void setBufferPool(int bufferPool) { + this.bufferPool = bufferPool; + } + + public void setBufferPoolSize(int bufferPoolSize) { + this.bufferPoolSize = bufferPoolSize; + } + + public void setEventCache(int eventCache) { + this.eventCache = eventCache; + } + + public void setAppReadBufSize(int appReadBufSize) { + this.appReadBufSize = appReadBufSize; + } + + public void setAppWriteBufSize(int appWriteBufSize) { + this.appWriteBufSize = appWriteBufSize; + } + + public void setProcessorCache(int processorCache) { + this.processorCache = processorCache; + } + + public void setTimeoutInterval(long timeoutInterval) { + this.timeoutInterval = timeoutInterval; + } + + public void setDirectBufferPool(int directBufferPool) { + this.bufferPool = directBufferPool; + } + + public int getUnlockTimeout() { + return unlockTimeout; + } + + public void setUnlockTimeout(int unlockTimeout) { + this.unlockTimeout = unlockTimeout; + } + + /** + * Get the actual buffer pool size to use. + * @param bufferOverhead When TLS is enabled, additional network buffers + * are needed and will be added to the application buffer size + * @return the actual buffer pool size that will be used + */ + public int getActualBufferPool(int bufferOverhead) { + if (bufferPool != -2) { + return bufferPool; + } else { + if (bufferPoolSize == -1) { + return -1; + } else if (bufferPoolSize == 0) { + return 0; + } else { + long actualBufferPoolSize = bufferPoolSize; + long poolSize = 0; + if (actualBufferPoolSize == -2) { + long maxMemory = Runtime.getRuntime().maxMemory(); + if (maxMemory > Integer.MAX_VALUE) { + actualBufferPoolSize = maxMemory / 32; + } else { + return 0; + } + } + int bufSize = appReadBufSize + appWriteBufSize + bufferOverhead; + if (bufSize == 0) { + return 0; + } + poolSize = actualBufferPoolSize / (bufSize); + if (poolSize > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) poolSize; + } + } + } + } + + void setObjectName(ObjectName oname) { + this.oname = oname; + } + + ObjectName getObjectName() { + return oname; + } +} diff --git a/java/org/apache/tomcat/util/net/SocketWrapperBase.java b/java/org/apache/tomcat/util/net/SocketWrapperBase.java new file mode 100644 index 0000000..03a13fd --- /dev/null +++ b/java/org/apache/tomcat/util/net/SocketWrapperBase.java @@ -0,0 +1,1521 @@ +/* + * 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.tomcat.util.net; + +import java.io.EOFException; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; +import java.nio.channels.InterruptedByTimeoutException; +import java.nio.channels.ReadPendingException; +import java.nio.channels.WritePendingException; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import jakarta.servlet.ServletConnection; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +public abstract class SocketWrapperBase { + + private static final Log log = LogFactory.getLog(SocketWrapperBase.class); + + protected static final StringManager sm = StringManager.getManager(SocketWrapperBase.class); + + /* + * At 100,000 connections a second there are enough IDs here for ~3,000,000 + * years before it overflows (and then we have another 3,000,000 years + * before it gets back to zero). + * + * Local testing shows that 5 threads can obtain 60,000,000+ IDs a second + * from a single AtomicLong. That is about about 17ns per request. It does + * not appear that the introduction of this counter will cause a bottleneck + * for connection processing. + */ + private static final AtomicLong connectionIdGenerator = new AtomicLong(0); + + private E socket; + private final AbstractEndpoint endpoint; + private final Lock lock = new ReentrantLock(); + + protected final AtomicBoolean closed = new AtomicBoolean(false); + + // Volatile because I/O and setting the timeout values occurs on a different + // thread to the thread checking the timeout. + private volatile long readTimeout = -1; + private volatile long writeTimeout = -1; + + protected volatile IOException previousIOException = null; + + private volatile int keepAliveLeft = 100; + private String negotiatedProtocol = null; + + private final String connectionId; + + /* + * Following cached for speed / reduced GC + */ + protected String localAddr = null; + protected String localName = null; + protected int localPort = -1; + protected String remoteAddr = null; + protected String remoteHost = null; + protected int remotePort = -1; + protected volatile ServletConnection servletConnection = null; + + /** + * Used to record the first IOException that occurs during non-blocking + * read/writes that can't be usefully propagated up the stack since there is + * no user code or appropriate container code in the stack to handle it. + */ + private volatile IOException error = null; + + /** + * The buffers used for communicating with the socket. + */ + protected volatile SocketBufferHandler socketBufferHandler = null; + + /** + * The max size of the individual buffered write buffers + */ + protected int bufferedWriteSize = 64 * 1024; // 64k default write buffer + + /** + * Additional buffer used for non-blocking writes. Non-blocking writes need + * to return immediately even if the data cannot be written immediately but + * the socket buffer may not be big enough to hold all of the unwritten + * data. This structure provides an additional buffer to hold the data until + * it can be written. + * Not that while the Servlet API only allows one non-blocking write at a + * time, due to buffering and the possible need to write HTTP headers, this + * layer may see multiple writes. + */ + protected final WriteBuffer nonBlockingWriteBuffer = new WriteBuffer(bufferedWriteSize); + + /* + * Asynchronous operations. + */ + protected final Semaphore readPending; + protected volatile OperationState readOperation = null; + protected final Semaphore writePending; + protected volatile OperationState writeOperation = null; + + /** + * The org.apache.coyote.Processor instance currently associated with the + * wrapper. Only populated when required to maintain wrapper<->Processor + * mapping between calls to + * {@link AbstractEndpoint.Handler#process(SocketWrapperBase, SocketEvent)}. + */ + private final AtomicReference currentProcessor = new AtomicReference<>(); + + public SocketWrapperBase(E socket, AbstractEndpoint endpoint) { + this.socket = socket; + this.endpoint = endpoint; + if (endpoint.getUseAsyncIO() || needSemaphores()) { + readPending = new Semaphore(1); + writePending = new Semaphore(1); + } else { + readPending = null; + writePending = null; + } + connectionId = Long.toHexString(connectionIdGenerator.getAndIncrement()); + } + + public E getSocket() { + return socket; + } + + protected void reset(E closedSocket) { + socket = closedSocket; + } + + protected AbstractEndpoint getEndpoint() { + return endpoint; + } + + public Lock getLock() { + return lock; + } + + public Object getCurrentProcessor() { + return currentProcessor.get(); + } + + public void setCurrentProcessor(Object currentProcessor) { + this.currentProcessor.set(currentProcessor); + } + + public Object takeCurrentProcessor() { + return currentProcessor.getAndSet(null); + } + + /** + * Transfers processing to a container thread. + * + * @param runnable The actions to process on a container thread + * + * @throws RejectedExecutionException If the runnable cannot be executed + */ + public void execute(Runnable runnable) { + Executor executor = endpoint.getExecutor(); + if (!endpoint.isRunning() || executor == null) { + throw new RejectedExecutionException(); + } + executor.execute(runnable); + } + + public IOException getError() { return error; } + public void setError(IOException error) { + // Not perfectly thread-safe but good enough. Just needs to ensure that + // once this.error is non-null, it can never be null. + if (this.error != null) { + return; + } + this.error = error; + } + public void checkError() throws IOException { + if (error != null) { + throw error; + } + } + + public String getNegotiatedProtocol() { return negotiatedProtocol; } + public void setNegotiatedProtocol(String negotiatedProtocol) { + this.negotiatedProtocol = negotiatedProtocol; + } + + /** + * Set the timeout for reading. Values of zero or less will be changed to + * -1. + * + * @param readTimeout The timeout in milliseconds. A value of -1 indicates + * an infinite timeout. + */ + public void setReadTimeout(long readTimeout) { + if (readTimeout > 0) { + this.readTimeout = readTimeout; + } else { + this.readTimeout = -1; + } + } + + public long getReadTimeout() { + return this.readTimeout; + } + + /** + * Set the timeout for writing. Values of zero or less will be changed to + * -1. + * + * @param writeTimeout The timeout in milliseconds. A value of zero or less + * indicates an infinite timeout. + */ + public void setWriteTimeout(long writeTimeout) { + if (writeTimeout > 0) { + this.writeTimeout = writeTimeout; + } else { + this.writeTimeout = -1; + } + } + + public long getWriteTimeout() { + return this.writeTimeout; + } + + + public void setKeepAliveLeft(int keepAliveLeft) { this.keepAliveLeft = keepAliveLeft; } + public int decrementKeepAlive() { return (--keepAliveLeft); } + + public String getRemoteHost() { + if (remoteHost == null) { + populateRemoteHost(); + } + return remoteHost; + } + protected abstract void populateRemoteHost(); + + public String getRemoteAddr() { + if (remoteAddr == null) { + populateRemoteAddr(); + } + return remoteAddr; + } + protected abstract void populateRemoteAddr(); + + public int getRemotePort() { + if (remotePort == -1) { + populateRemotePort(); + } + return remotePort; + } + protected abstract void populateRemotePort(); + + public String getLocalName() { + if (localName == null) { + populateLocalName(); + } + return localName; + } + protected abstract void populateLocalName(); + + public String getLocalAddr() { + if (localAddr == null) { + populateLocalAddr(); + } + return localAddr; + } + protected abstract void populateLocalAddr(); + + public int getLocalPort() { + if (localPort == -1) { + populateLocalPort(); + } + return localPort; + } + protected abstract void populateLocalPort(); + + public SocketBufferHandler getSocketBufferHandler() { return socketBufferHandler; } + + public boolean hasDataToRead() { + // Return true because it is always safe to make a read attempt + return true; + } + + public boolean hasDataToWrite() { + return !socketBufferHandler.isWriteBufferEmpty() || !nonBlockingWriteBuffer.isEmpty(); + } + + /** + * Checks to see if there are any writes pending and if there are calls + * {@link #registerWriteInterest()} to trigger a callback once the pending + * writes have completed. + *

    + * Note: Once this method has returned false it MUST NOT + * be called again until the pending write has completed and the + * callback has been fired. + * TODO: Modify {@link #registerWriteInterest()} so the above + * restriction is enforced there rather than relying on the caller. + * + * @return true if no writes are pending and data can be + * written otherwise false + */ + public boolean isReadyForWrite() { + boolean result = canWrite(); + if (!result) { + registerWriteInterest(); + } + return result; + } + + + public boolean canWrite() { + if (socketBufferHandler == null) { + throw new IllegalStateException(sm.getString("socket.closed")); + } + return socketBufferHandler.isWriteBufferWritable() && nonBlockingWriteBuffer.isEmpty(); + } + + + /** + * Overridden for debug purposes. No guarantees are made about the format of + * this message which may vary significantly between point releases. + *

    + * {@inheritDoc} + */ + @Override + public String toString() { + return super.toString() + ":" + String.valueOf(socket); + } + + + public abstract int read(boolean block, byte[] b, int off, int len) throws IOException; + public abstract int read(boolean block, ByteBuffer to) throws IOException; + public abstract boolean isReadyForRead() throws IOException; + public abstract void setAppReadBufHandler(ApplicationBufferHandler handler); + + protected int populateReadBuffer(byte[] b, int off, int len) { + socketBufferHandler.configureReadBufferForRead(); + ByteBuffer readBuffer = socketBufferHandler.getReadBuffer(); + int remaining = readBuffer.remaining(); + + // Is there enough data in the read buffer to satisfy this request? + // Copy what data there is in the read buffer to the byte array + if (remaining > 0) { + remaining = Math.min(remaining, len); + readBuffer.get(b, off, remaining); + + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read from buffer: [" + remaining + "]"); + } + } + return remaining; + } + + + protected int populateReadBuffer(ByteBuffer to) { + // Is there enough data in the read buffer to satisfy this request? + // Copy what data there is in the read buffer to the byte array + socketBufferHandler.configureReadBufferForRead(); + int nRead = transfer(socketBufferHandler.getReadBuffer(), to); + + if (log.isTraceEnabled()) { + log.trace("Socket: [" + this + "], Read from buffer: [" + nRead + "]"); + } + return nRead; + } + + + /** + * Return input that has been read to the input buffer for re-reading by the + * correct component. There are times when a component may read more data + * than it needs before it passes control to another component. One example + * of this is during HTTP upgrade. If an (arguably misbehaving client) sends + * data associated with the upgraded protocol before the HTTP upgrade + * completes, the HTTP handler may read it. This method provides a way for + * that data to be returned so it can be processed by the correct component. + * + * @param returnedInput The input to return to the input buffer. + */ + public void unRead(ByteBuffer returnedInput) { + if (returnedInput != null) { + socketBufferHandler.unReadReadBuffer(returnedInput); + } + } + + + /** + * Close the socket wrapper. + */ + public void close() { + if (closed.compareAndSet(false, true)) { + try { + getEndpoint().getHandler().release(this); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + if (log.isDebugEnabled()) { + log.error(sm.getString("endpoint.debug.handlerRelease"), e); + } + } finally { + getEndpoint().countDownConnection(); + doClose(); + } + } + } + + /** + * Perform the actual close. The closed atomic boolean guarantees this will + * be called only once per wrapper. + */ + protected abstract void doClose(); + + /** + * @return true if the wrapper has been closed + */ + public boolean isClosed() { + return closed.get(); + } + + + /** + * Writes the provided data to the socket write buffer. If the socket write + * buffer fills during the write, the content of the socket write buffer is + * written to the network and this method starts to fill the socket write + * buffer again. Depending on the size of the data to write, there may be + * multiple writes to the network. + *

    + * Non-blocking writes must return immediately and the byte array holding + * the data to be written must be immediately available for re-use. It may + * not be possible to write sufficient data to the network to allow this to + * happen. In this case data that cannot be written to the network and + * cannot be held by the socket buffer is stored in the non-blocking write + * buffer. + *

    + * Note: There is an implementation assumption that, before switching from + * non-blocking writes to blocking writes, any data remaining in the + * non-blocking write buffer will have been written to the network. + * + * @param block true if a blocking write should be used, + * otherwise a non-blocking write will be used + * @param buf The byte array containing the data to be written + * @param off The offset within the byte array of the data to be written + * @param len The length of the data to be written + * + * @throws IOException If an IO error occurs during the write + */ + public final void write(boolean block, byte[] buf, int off, int len) throws IOException { + if (len == 0 || buf == null) { + return; + } + + /* + * While the implementations for blocking and non-blocking writes are + * very similar they have been split into separate methods: + * - To allow sub-classes to override them individually. NIO2, for + * example, overrides the non-blocking write but not the blocking + * write. + * - To enable a marginally more efficient implemented for blocking + * writes which do not require the additional checks related to the + * use of the non-blocking write buffer + */ + if (block) { + writeBlocking(buf, off, len); + } else { + writeNonBlocking(buf, off, len); + } + } + + + /** + * Writes the provided data to the socket write buffer. If the socket write + * buffer fills during the write, the content of the socket write buffer is + * written to the network and this method starts to fill the socket write + * buffer again. Depending on the size of the data to write, there may be + * multiple writes to the network. + *

    + * Non-blocking writes must return immediately and the ByteBuffer holding + * the data to be written must be immediately available for re-use. It may + * not be possible to write sufficient data to the network to allow this to + * happen. In this case data that cannot be written to the network and + * cannot be held by the socket buffer is stored in the non-blocking write + * buffer. + *

    + * Note: There is an implementation assumption that, before switching from + * non-blocking writes to blocking writes, any data remaining in the + * non-blocking write buffer will have been written to the network. + * + * @param block true if a blocking write should be used, + * otherwise a non-blocking write will be used + * @param from The ByteBuffer containing the data to be written + * + * @throws IOException If an IO error occurs during the write + */ + public final void write(boolean block, ByteBuffer from) throws IOException { + if (from == null || from.remaining() == 0) { + return; + } + + /* + * While the implementations for blocking and non-blocking writes are + * very similar they have been split into separate methods: + * - To allow sub-classes to override them individually. NIO2, for + * example, overrides the non-blocking write but not the blocking + * write. + * - To enable a marginally more efficient implemented for blocking + * writes which do not require the additional checks related to the + * use of the non-blocking write buffer + */ + if (block) { + writeBlocking(from); + } else { + writeNonBlocking(from); + } + } + + + /** + * Writes the provided data to the socket write buffer. If the socket write + * buffer fills during the write, the content of the socket write buffer is + * written to the network using a blocking write. Once that blocking write + * is complete, this method starts to fill the socket write buffer again. + * Depending on the size of the data to write, there may be multiple writes + * to the network. On completion of this method there will always be space + * remaining in the socket write buffer. + * + * @param buf The byte array containing the data to be written + * @param off The offset within the byte array of the data to be written + * @param len The length of the data to be written + * + * @throws IOException If an IO error occurs during the write + */ + protected void writeBlocking(byte[] buf, int off, int len) throws IOException { + if (len > 0) { + socketBufferHandler.configureWriteBufferForWrite(); + int thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer()); + len -= thisTime; + while (len > 0) { + off += thisTime; + doWrite(true); + socketBufferHandler.configureWriteBufferForWrite(); + thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer()); + len -= thisTime; + } + } + } + + + /** + * Writes the provided data to the socket write buffer. If the socket write + * buffer fills during the write, the content of the socket write buffer is + * written to the network using a blocking write. Once that blocking write + * is complete, this method starts to fill the socket write buffer again. + * Depending on the size of the data to write, there may be multiple writes + * to the network. On completion of this method there will always be space + * remaining in the socket write buffer. + * + * @param from The ByteBuffer containing the data to be written + * + * @throws IOException If an IO error occurs during the write + */ + protected void writeBlocking(ByteBuffer from) throws IOException { + if (from.hasRemaining()) { + socketBufferHandler.configureWriteBufferForWrite(); + transfer(from, socketBufferHandler.getWriteBuffer()); + while (from.hasRemaining()) { + doWrite(true); + socketBufferHandler.configureWriteBufferForWrite(); + transfer(from, socketBufferHandler.getWriteBuffer()); + } + } + } + + + /** + * Transfers the data to the socket write buffer (writing that data to the + * socket if the buffer fills up using a non-blocking write) until either + * all the data has been transferred and space remains in the socket write + * buffer or a non-blocking write leaves data in the socket write buffer. + * After an incomplete write, any data remaining to be transferred to the + * socket write buffer will be copied to the socket write buffer. If the + * remaining data is too big for the socket write buffer, the socket write + * buffer will be filled and the additional data written to the non-blocking + * write buffer. + * + * @param buf The byte array containing the data to be written + * @param off The offset within the byte array of the data to be written + * @param len The length of the data to be written + * + * @throws IOException If an IO error occurs during the write + */ + protected void writeNonBlocking(byte[] buf, int off, int len) throws IOException { + if (len > 0 && nonBlockingWriteBuffer.isEmpty() + && socketBufferHandler.isWriteBufferWritable()) { + socketBufferHandler.configureWriteBufferForWrite(); + int thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer()); + len -= thisTime; + while (len > 0) { + off = off + thisTime; + doWrite(false); + if (len > 0 && socketBufferHandler.isWriteBufferWritable()) { + socketBufferHandler.configureWriteBufferForWrite(); + thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer()); + } else { + // Didn't write any data in the last non-blocking write. + // Therefore the write buffer will still be full. Nothing + // else to do here. Exit the loop. + break; + } + len -= thisTime; + } + } + + if (len > 0) { + // Remaining data must be buffered + nonBlockingWriteBuffer.add(buf, off, len); + } + } + + + /** + * Transfers the data to the socket write buffer (writing that data to the + * socket if the buffer fills up using a non-blocking write) until either + * all the data has been transferred and space remains in the socket write + * buffer or a non-blocking write leaves data in the socket write buffer. + * After an incomplete write, any data remaining to be transferred to the + * socket write buffer will be copied to the socket write buffer. If the + * remaining data is too big for the socket write buffer, the socket write + * buffer will be filled and the additional data written to the non-blocking + * write buffer. + * + * @param from The ByteBuffer containing the data to be written + * + * @throws IOException If an IO error occurs during the write + */ + protected void writeNonBlocking(ByteBuffer from) + throws IOException { + + if (from.hasRemaining() && nonBlockingWriteBuffer.isEmpty() + && socketBufferHandler.isWriteBufferWritable()) { + writeNonBlockingInternal(from); + } + + if (from.hasRemaining()) { + // Remaining data must be buffered + nonBlockingWriteBuffer.add(from); + } + } + + + /** + * Separate method so it can be re-used by the socket write buffer to write + * data to the network + * + * @param from The ByteBuffer containing the data to be written + * + * @throws IOException If an IO error occurs during the write + */ + protected void writeNonBlockingInternal(ByteBuffer from) throws IOException { + socketBufferHandler.configureWriteBufferForWrite(); + transfer(from, socketBufferHandler.getWriteBuffer()); + while (from.hasRemaining()) { + doWrite(false); + if (socketBufferHandler.isWriteBufferWritable()) { + socketBufferHandler.configureWriteBufferForWrite(); + transfer(from, socketBufferHandler.getWriteBuffer()); + } else { + break; + } + } + } + + + /** + * Writes as much data as possible from any that remains in the buffers. + * + * @param block true if a blocking write should be used, + * otherwise a non-blocking write will be used + * + * @return true if data remains to be flushed after this method + * completes, otherwise false. In blocking mode + * therefore, the return value should always be false + * + * @throws IOException If an IO error occurs during the write + */ + public boolean flush(boolean block) throws IOException { + boolean result = false; + if (block) { + // A blocking flush will always empty the buffer. + flushBlocking(); + } else { + result = flushNonBlocking(); + } + + return result; + } + + + /** + * Writes all remaining data from the buffers and blocks until the write is + * complete. + * + * @throws IOException If an IO error occurs during the write + */ + protected void flushBlocking() throws IOException { + doWrite(true); + + if (!nonBlockingWriteBuffer.isEmpty()) { + nonBlockingWriteBuffer.write(this, true); + + if (!socketBufferHandler.isWriteBufferEmpty()) { + doWrite(true); + } + } + + } + + + /** + * Writes as much data as possible from any that remains in the buffers. + * + * @return true if data remains to be flushed after this method + * completes, otherwise false. + * + * @throws IOException If an IO error occurs during the write + */ + protected abstract boolean flushNonBlocking() throws IOException; + + + /** + * Write the contents of the socketWriteBuffer to the socket. For blocking + * writes either then entire contents of the buffer will be written or an + * IOException will be thrown. Partial blocking writes will not occur. + * + * @param block Should the write be blocking or not? + * + * @throws IOException If an I/O error such as a timeout occurs during the + * write + */ + protected void doWrite(boolean block) throws IOException { + socketBufferHandler.configureWriteBufferForRead(); + doWrite(block, socketBufferHandler.getWriteBuffer()); + } + + + /** + * Write the contents of the ByteBuffer to the socket. For blocking writes + * either then entire contents of the buffer will be written or an + * IOException will be thrown. Partial blocking writes will not occur. + * + * @param block Should the write be blocking or not? + * @param from the ByteBuffer containing the data to be written + * + * @throws IOException If an I/O error such as a timeout occurs during the + * write + */ + protected abstract void doWrite(boolean block, ByteBuffer from) throws IOException; + + + public void processSocket(SocketEvent socketStatus, boolean dispatch) { + endpoint.processSocket(this, socketStatus, dispatch); + } + + + public abstract void registerReadInterest(); + + public abstract void registerWriteInterest(); + + public abstract SendfileDataBase createSendfileData(String filename, long pos, long length); + + /** + * Starts the sendfile process. It is expected that if the sendfile process + * does not complete during this call and does not report an error, that the + * caller will not add the socket to the poller (or equivalent). That + * is the responsibility of this method. + * + * @param sendfileData Data representing the file to send + * + * @return The state of the sendfile process after the first write. + */ + public abstract SendfileState processSendfile(SendfileDataBase sendfileData); + + /** + * Require the client to perform CLIENT-CERT authentication if it hasn't + * already done so. + * + * @param sslSupport The SSL/TLS support instance currently being used by + * the connection that may need updating after the client + * authentication + * + * @throws IOException If authentication is required then there will be I/O + * with the client and this exception will be thrown if + * that goes wrong + */ + public abstract void doClientAuth(SSLSupport sslSupport) throws IOException; + + /** + * Obtain an SSLSupport instance for this socket. + * + * @return An SSLSupport instance for this socket. + */ + public abstract SSLSupport getSslSupport(); + + + // ------------------------------------------------------- NIO 2 style APIs + + + public enum BlockingMode { + /** + * The operation will not block. If there are pending operations, + * the operation will throw a pending exception. + */ + CLASSIC, + /** + * The operation will not block. If there are pending operations, + * the operation will return CompletionState.NOT_DONE. + */ + NON_BLOCK, + /** + * The operation will block until pending operations are completed, but + * will not block after performing it. + */ + SEMI_BLOCK, + /** + * The operation will block until completed. + */ + BLOCK + } + + public enum CompletionState { + /** + * Operation is still pending. + */ + PENDING, + /** + * Operation was pending and non blocking. + */ + NOT_DONE, + /** + * The operation completed inline. + */ + INLINE, + /** + * The operation completed inline but failed. + */ + ERROR, + /** + * The operation completed, but not inline. + */ + DONE + } + + public enum CompletionHandlerCall { + /** + * Operation should continue, the completion handler shouldn't be + * called. + */ + CONTINUE, + /** + * The operation completed but the completion handler shouldn't be + * called. + */ + NONE, + /** + * The operation is complete, the completion handler should be + * called. + */ + DONE + } + + public interface CompletionCheck { + /** + * Determine what call, if any, should be made to the completion + * handler. + * + * @param state of the operation (done or done in-line since the + * IO call is done) + * @param buffers ByteBuffer[] that has been passed to the + * original IO call + * @param offset that has been passed to the original IO call + * @param length that has been passed to the original IO call + * + * @return The call, if any, to make to the completion handler + */ + CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers, + int offset, int length); + } + + /** + * This utility CompletionCheck will cause the write to fully write + * all remaining data. If the operation completes inline, the + * completion handler will not be called. + */ + public static final CompletionCheck COMPLETE_WRITE = new CompletionCheck() { + @Override + public CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers, + int offset, int length) { + for (int i = 0; i < length; i++) { + if (buffers[offset + i].hasRemaining()) { + return CompletionHandlerCall.CONTINUE; + } + } + return (state == CompletionState.DONE) ? CompletionHandlerCall.DONE + : CompletionHandlerCall.NONE; + } + }; + + /** + * This utility CompletionCheck will cause the write to fully write + * all remaining data. The completion handler will then be called. + */ + public static final CompletionCheck COMPLETE_WRITE_WITH_COMPLETION = new CompletionCheck() { + @Override + public CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers, + int offset, int length) { + for (int i = 0; i < length; i++) { + if (buffers[offset + i].hasRemaining()) { + return CompletionHandlerCall.CONTINUE; + } + } + return CompletionHandlerCall.DONE; + } + }; + + /** + * This utility CompletionCheck will cause the completion handler + * to be called once some data has been read. If the operation + * completes inline, the completion handler will not be called. + */ + public static final CompletionCheck READ_DATA = new CompletionCheck() { + @Override + public CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers, + int offset, int length) { + return (state == CompletionState.DONE) ? CompletionHandlerCall.DONE + : CompletionHandlerCall.NONE; + } + }; + + /** + * This utility CompletionCheck will cause the completion handler + * to be called once the given buffers are full. The completion + * handler will then be called. + */ + public static final CompletionCheck COMPLETE_READ_WITH_COMPLETION = COMPLETE_WRITE_WITH_COMPLETION; + + /** + * This utility CompletionCheck will cause the completion handler + * to be called once the given buffers are full. If the operation + * completes inline, the completion handler will not be called. + */ + public static final CompletionCheck COMPLETE_READ = COMPLETE_WRITE; + + /** + * Internal state tracker for vectored operations. + */ + protected abstract class OperationState implements Runnable { + protected final boolean read; + protected final ByteBuffer[] buffers; + protected final int offset; + protected final int length; + protected final A attachment; + protected final long timeout; + protected final TimeUnit unit; + protected final BlockingMode block; + protected final CompletionCheck check; + protected final CompletionHandler handler; + protected final Semaphore semaphore; + protected final VectoredIOCompletionHandler completion; + protected final AtomicBoolean callHandler; + protected OperationState(boolean read, ByteBuffer[] buffers, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, + CompletionCheck check, CompletionHandler handler, + Semaphore semaphore, VectoredIOCompletionHandler completion) { + this.read = read; + this.buffers = buffers; + this.offset = offset; + this.length = length; + this.block = block; + this.timeout = timeout; + this.unit = unit; + this.attachment = attachment; + this.check = check; + this.handler = handler; + this.semaphore = semaphore; + this.completion = completion; + callHandler = (handler != null) ? new AtomicBoolean(true) : null; + } + protected volatile long nBytes = 0; + protected volatile CompletionState state = CompletionState.PENDING; + protected boolean completionDone = true; + + /** + * @return true if the operation is still inline, false if the operation + * is running on a thread that is not the original caller + */ + protected abstract boolean isInline(); + + protected boolean hasOutboundRemaining() { + // NIO2 and APR never have remaining outbound data when the + // completion handler is called. NIO needs to override this. + return false; + } + + + /** + * Process the operation using the connector executor. + * @return true if the operation was accepted, false if the executor + * rejected execution + */ + protected boolean process() { + try { + getEndpoint().getExecutor().execute(this); + return true; + } catch (RejectedExecutionException ree) { + log.warn(sm.getString("endpoint.executor.fail", SocketWrapperBase.this) , ree); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // This means we got an OOM or similar creating a thread, or that + // the pool and its queue are full + log.error(sm.getString("endpoint.process.fail"), t); + } + return false; + } + + /** + * Start the operation, this will typically call run. + */ + protected void start() { + run(); + } + + /** + * End the operation. + */ + protected void end() { + } + + } + + /** + * Completion handler for vectored operations. This will check the completion of the operation, + * then either continue or call the user provided completion handler. + */ + protected class VectoredIOCompletionHandler implements CompletionHandler> { + @Override + public void completed(Long nBytes, OperationState state) { + if (nBytes.longValue() < 0) { + failed(new EOFException(), state); + } else { + state.nBytes += nBytes.longValue(); + CompletionState currentState = state.isInline() ? CompletionState.INLINE : CompletionState.DONE; + boolean complete = true; + boolean completion = true; + if (state.check != null) { + CompletionHandlerCall call = state.check.callHandler(currentState, state.buffers, state.offset, state.length); + if (call == CompletionHandlerCall.CONTINUE || (!state.read && state.hasOutboundRemaining())) { + complete = false; + } else if (call == CompletionHandlerCall.NONE) { + completion = false; + } + } + if (complete) { + boolean notify = false; + if (state.read) { + readOperation = null; + } else { + writeOperation = null; + } + // Semaphore must be released after [read|write]Operation is cleared + // to ensure that the next thread to hold the semaphore hasn't + // written a new value to [read|write]Operation by the time it is + // cleared. + state.semaphore.release(); + if (state.block == BlockingMode.BLOCK && currentState != CompletionState.INLINE) { + notify = true; + } else { + state.state = currentState; + } + state.end(); + if (completion && state.handler != null && state.callHandler.compareAndSet(true, false)) { + state.handler.completed(Long.valueOf(state.nBytes), state.attachment); + } + synchronized (state) { + state.completionDone = true; + if (notify) { + state.state = currentState; + state.notify(); + } + } + } else { + synchronized (state) { + state.completionDone = true; + } + state.run(); + } + } + } + @Override + public void failed(Throwable exc, OperationState state) { + IOException ioe = null; + if (exc instanceof InterruptedByTimeoutException) { + ioe = new SocketTimeoutException(); + exc = ioe; + } else if (exc instanceof IOException) { + ioe = (IOException) exc; + } + setError(ioe); + boolean notify = false; + if (state.read) { + readOperation = null; + } else { + writeOperation = null; + } + // Semaphore must be released after [read|write]Operation is cleared + // to ensure that the next thread to hold the semaphore hasn't + // written a new value to [read|write]Operation by the time it is + // cleared. + state.semaphore.release(); + if (state.block == BlockingMode.BLOCK) { + notify = true; + } else { + state.state = state.isInline() ? CompletionState.ERROR : CompletionState.DONE; + } + state.end(); + if (state.handler != null && state.callHandler.compareAndSet(true, false)) { + state.handler.failed(exc, state.attachment); + } + synchronized (state) { + state.completionDone = true; + if (notify) { + state.state = state.isInline() ? CompletionState.ERROR : CompletionState.DONE; + state.notify(); + } + } + } + } + + /** + * Allows using NIO2 style read/write. + * + * @return {@code true} if the connector has the capability enabled + */ + public boolean hasAsyncIO() { + // The semaphores are only created if async IO is enabled + return (readPending != null); + } + + /** + * Allows indicating if the connector needs semaphores. + * + * @return This default implementation always returns {@code false} + */ + public boolean needSemaphores() { + return false; + } + + /** + * Allows indicating if the connector supports per operation timeout. + * + * @return This default implementation always returns {@code false} + */ + public boolean hasPerOperationTimeout() { + return false; + } + + /** + * Allows checking if an asynchronous read operation is currently pending. + * @return true if the endpoint supports asynchronous IO and + * a read operation is being processed asynchronously + */ + public boolean isReadPending() { + return false; + } + + /** + * Allows checking if an asynchronous write operation is currently pending. + * @return true if the endpoint supports asynchronous IO and + * a write operation is being processed asynchronously + */ + public boolean isWritePending() { + return false; + } + + /** + * Scatter read. The completion handler will be called once some + * data has been read or an error occurred. The default NIO2 + * behavior is used: the completion handler will be called as soon + * as some data has been read, even if the read has completed inline. + * + * @param timeout timeout duration for the read + * @param unit units for the timeout duration + * @param attachment an object to attach to the I/O operation that will be + * used when calling the completion handler + * @param handler to call when the IO is complete + * @param dsts buffers + * @param The attachment type + * @return the completion state (done, done inline, or still pending) + */ + public final CompletionState read(long timeout, TimeUnit unit, A attachment, + CompletionHandler handler, ByteBuffer... dsts) { + if (dsts == null) { + throw new IllegalArgumentException(); + } + return read(dsts, 0, dsts.length, BlockingMode.CLASSIC, timeout, unit, attachment, null, handler); + } + + /** + * Scatter read. The completion handler will be called once some + * data has been read or an error occurred. If a CompletionCheck + * object has been provided, the completion handler will only be + * called if the callHandler method returned true. If no + * CompletionCheck object has been provided, the default NIO2 + * behavior is used: the completion handler will be called as soon + * as some data has been read, even if the read has completed inline. + * + * @param block is the blocking mode that will be used for this operation + * @param timeout timeout duration for the read + * @param unit units for the timeout duration + * @param attachment an object to attach to the I/O operation that will be + * used when calling the completion handler + * @param check for the IO operation completion + * @param handler to call when the IO is complete + * @param dsts buffers + * @param The attachment type + * @return the completion state (done, done inline, or still pending) + */ + public final CompletionState read(BlockingMode block, long timeout, + TimeUnit unit, A attachment, CompletionCheck check, + CompletionHandler handler, ByteBuffer... dsts) { + if (dsts == null) { + throw new IllegalArgumentException(); + } + return read(dsts, 0, dsts.length, block, timeout, unit, attachment, check, handler); + } + + /** + * Scatter read. The completion handler will be called once some + * data has been read or an error occurred. If a CompletionCheck + * object has been provided, the completion handler will only be + * called if the callHandler method returned true. If no + * CompletionCheck object has been provided, the default NIO2 + * behavior is used: the completion handler will be called as soon + * as some data has been read, even if the read has completed inline. + * + * @param dsts buffers + * @param offset in the buffer array + * @param length in the buffer array + * @param block is the blocking mode that will be used for this operation + * @param timeout timeout duration for the read + * @param unit units for the timeout duration + * @param attachment an object to attach to the I/O operation that will be + * used when calling the completion handler + * @param check for the IO operation completion + * @param handler to call when the IO is complete + * @param The attachment type + * @return the completion state (done, done inline, or still pending) + */ + public final CompletionState read(ByteBuffer[] dsts, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, + CompletionCheck check, CompletionHandler handler) { + return vectoredOperation(true, dsts, offset, length, block, timeout, unit, attachment, check, handler); + } + + /** + * Gather write. The completion handler will be called once some + * data has been written or an error occurred. The default NIO2 + * behavior is used: the completion handler will be called, even + * if the write is incomplete and data remains in the buffers, or + * if the write completed inline. + * + * @param timeout timeout duration for the write + * @param unit units for the timeout duration + * @param attachment an object to attach to the I/O operation that will be + * used when calling the completion handler + * @param handler to call when the IO is complete + * @param srcs buffers + * @param The attachment type + * @return the completion state (done, done inline, or still pending) + */ + public final CompletionState write(long timeout, TimeUnit unit, A attachment, + CompletionHandler handler, ByteBuffer... srcs) { + if (srcs == null) { + throw new IllegalArgumentException(); + } + return write(srcs, 0, srcs.length, BlockingMode.CLASSIC, timeout, unit, attachment, null, handler); + } + + /** + * Gather write. The completion handler will be called once some + * data has been written or an error occurred. If a CompletionCheck + * object has been provided, the completion handler will only be + * called if the callHandler method returned true. If no + * CompletionCheck object has been provided, the default NIO2 + * behavior is used: the completion handler will be called, even + * if the write is incomplete and data remains in the buffers, or + * if the write completed inline. + * + * @param block is the blocking mode that will be used for this operation + * @param timeout timeout duration for the write + * @param unit units for the timeout duration + * @param attachment an object to attach to the I/O operation that will be + * used when calling the completion handler + * @param check for the IO operation completion + * @param handler to call when the IO is complete + * @param srcs buffers + * @param The attachment type + * @return the completion state (done, done inline, or still pending) + */ + public final CompletionState write(BlockingMode block, long timeout, + TimeUnit unit, A attachment, CompletionCheck check, + CompletionHandler handler, ByteBuffer... srcs) { + if (srcs == null) { + throw new IllegalArgumentException(); + } + return write(srcs, 0, srcs.length, block, timeout, unit, attachment, check, handler); + } + + /** + * Gather write. The completion handler will be called once some + * data has been written or an error occurred. If a CompletionCheck + * object has been provided, the completion handler will only be + * called if the callHandler method returned true. If no + * CompletionCheck object has been provided, the default NIO2 + * behavior is used: the completion handler will be called, even + * if the write is incomplete and data remains in the buffers, or + * if the write completed inline. + * + * @param srcs buffers + * @param offset in the buffer array + * @param length in the buffer array + * @param block is the blocking mode that will be used for this operation + * @param timeout timeout duration for the write + * @param unit units for the timeout duration + * @param attachment an object to attach to the I/O operation that will be + * used when calling the completion handler + * @param check for the IO operation completion + * @param handler to call when the IO is complete + * @param The attachment type + * @return the completion state (done, done inline, or still pending) + */ + public final CompletionState write(ByteBuffer[] srcs, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, + CompletionCheck check, CompletionHandler handler) { + return vectoredOperation(false, srcs, offset, length, block, timeout, unit, attachment, check, handler); + } + + + /** + * Vectored operation. The completion handler will be called once + * the operation is complete or an error occurred. If a CompletionCheck + * object has been provided, the completion handler will only be + * called if the callHandler method returned true. If no + * CompletionCheck object has been provided, the default NIO2 + * behavior is used: the completion handler will be called, even + * if the operation is incomplete, or if the operation completed inline. + * + * @param read true if the operation is a read, false if it is a write + * @param buffers buffers + * @param offset in the buffer array + * @param length in the buffer array + * @param block is the blocking mode that will be used for this operation + * @param timeout timeout duration for the write + * @param unit units for the timeout duration + * @param attachment an object to attach to the I/O operation that will be + * used when calling the completion handler + * @param check for the IO operation completion + * @param handler to call when the IO is complete + * @param The attachment type + * @return the completion state (done, done inline, or still pending) + */ + protected final CompletionState vectoredOperation(boolean read, + ByteBuffer[] buffers, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, + CompletionCheck check, CompletionHandler handler) { + IOException ioe = getError(); + if (ioe != null) { + handler.failed(ioe, attachment); + return CompletionState.ERROR; + } + if (timeout == -1) { + timeout = AbstractEndpoint.toTimeout(read ? getReadTimeout() : getWriteTimeout()); + unit = TimeUnit.MILLISECONDS; + } else if (!hasPerOperationTimeout() && (unit.toMillis(timeout) != (read ? getReadTimeout() : getWriteTimeout()))) { + if (read) { + setReadTimeout(unit.toMillis(timeout)); + } else { + setWriteTimeout(unit.toMillis(timeout)); + } + } + if (block == BlockingMode.BLOCK || block == BlockingMode.SEMI_BLOCK) { + try { + if (read ? !readPending.tryAcquire(timeout, unit) : !writePending.tryAcquire(timeout, unit)) { + handler.failed(new SocketTimeoutException(), attachment); + return CompletionState.ERROR; + } + } catch (InterruptedException e) { + handler.failed(e, attachment); + return CompletionState.ERROR; + } + } else { + if (read ? !readPending.tryAcquire() : !writePending.tryAcquire()) { + if (block == BlockingMode.NON_BLOCK) { + return CompletionState.NOT_DONE; + } else { + handler.failed(read ? new ReadPendingException() : new WritePendingException(), attachment); + return CompletionState.ERROR; + } + } + } + VectoredIOCompletionHandler completion = new VectoredIOCompletionHandler<>(); + OperationState state = newOperationState(read, buffers, offset, length, block, timeout, unit, + attachment, check, handler, read ? readPending : writePending, completion); + if (read) { + readOperation = state; + } else { + writeOperation = state; + } + state.start(); + if (block == BlockingMode.BLOCK) { + synchronized (state) { + if (state.state == CompletionState.PENDING) { + try { + state.wait(unit.toMillis(timeout)); + if (state.state == CompletionState.PENDING) { + if (handler != null && state.callHandler.compareAndSet(true, false)) { + handler.failed(new SocketTimeoutException(getTimeoutMsg(read)), attachment); + } + return CompletionState.ERROR; + } + } catch (InterruptedException e) { + if (handler != null && state.callHandler.compareAndSet(true, false)) { + handler.failed(new SocketTimeoutException(getTimeoutMsg(read)), attachment); + } + return CompletionState.ERROR; + } + } + } + } + return state.state; + } + + + private String getTimeoutMsg(boolean read) { + if (read) { + return sm.getString("socketWrapper.readTimeout"); + } else { + return sm.getString("socketWrapper.writeTimeout"); + } + } + + + protected abstract OperationState newOperationState(boolean read, + ByteBuffer[] buffers, int offset, int length, + BlockingMode block, long timeout, TimeUnit unit, A attachment, + CompletionCheck check, CompletionHandler handler, + Semaphore semaphore, VectoredIOCompletionHandler completion); + + + // --------------------------------------------------------- Utility methods + + protected static int transfer(byte[] from, int offset, int length, ByteBuffer to) { + int max = Math.min(length, to.remaining()); + if (max > 0) { + to.put(from, offset, max); + } + return max; + } + + protected static int transfer(ByteBuffer from, ByteBuffer to) { + int max = Math.min(from.remaining(), to.remaining()); + if (max > 0) { + int fromLimit = from.limit(); + from.limit(from.position() + max); + to.put(from); + from.limit(fromLimit); + } + return max; + } + + protected static boolean buffersArrayHasRemaining(ByteBuffer[] buffers, int offset, int length) { + for (int pos = offset; pos < offset + length; pos++) { + if (buffers[pos].hasRemaining()) { + return true; + } + } + return false; + } + + + // -------------------------------------------------------------- ID methods + + public ServletConnection getServletConnection(String protocol, String protocolConnectionId) { + if (servletConnection == null) { + servletConnection = new ServletConnectionImpl( + connectionId, protocol, protocolConnectionId, endpoint.isSSLEnabled()); + } + return servletConnection; + } +} diff --git a/java/org/apache/tomcat/util/net/TLSClientHelloExtractor.java b/java/org/apache/tomcat/util/net/TLSClientHelloExtractor.java new file mode 100644 index 0000000..28d3358 --- /dev/null +++ b/java/org/apache/tomcat/util/net/TLSClientHelloExtractor.java @@ -0,0 +1,444 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.http.parser.HttpParser; +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.res.StringManager; + +/** + * This class extracts the SNI host name and ALPN protocols from a TLS + * client-hello message. + */ +public class TLSClientHelloExtractor { + + private static final Log log = LogFactory.getLog(TLSClientHelloExtractor.class); + private static final StringManager sm = StringManager.getManager(TLSClientHelloExtractor.class); + + private final ExtractorResult result; + private final List clientRequestedCiphers; + private final List clientRequestedCipherNames; + private final String sniValue; + private final List clientRequestedApplicationProtocols; + private final List clientRequestedProtocols; + + private static final int TLS_RECORD_HEADER_LEN = 5; + + private static final int TLS_EXTENSION_SERVER_NAME = 0; + private static final int TLS_EXTENSION_ALPN = 16; + private static final int TLS_EXTENSION_SUPPORTED_VERSION = 43; + + public static byte[] USE_TLS_RESPONSE = ("HTTP/1.1 400 \r\n" + + "Content-Type: text/plain;charset=UTF-8\r\n" + + "Connection: close\r\n" + + "\r\n" + + "Bad Request\r\n" + + "This combination of host and port requires TLS.\r\n").getBytes(StandardCharsets.UTF_8); + + + /** + * Creates the instance of the parser and processes the provided buffer. The + * buffer position and limit will be modified during the execution of this + * method but they will be returned to the original values before the method + * exits. + * + * @param netInBuffer The buffer containing the TLS data to process + * @throws IOException If the client hello message is malformed + */ + public TLSClientHelloExtractor(ByteBuffer netInBuffer) throws IOException { + // Buffer is in write mode at this point. Record the current position so + // the buffer state can be restored at the end of this method. + int pos = netInBuffer.position(); + int limit = netInBuffer.limit(); + ExtractorResult result = ExtractorResult.NOT_PRESENT; + List clientRequestedCiphers = new ArrayList<>(); + List clientRequestedCipherNames = new ArrayList<>(); + List clientRequestedApplicationProtocols = new ArrayList<>(); + List clientRequestedProtocols = new ArrayList<>(); + String sniValue = null; + try { + // Switch to read mode. + netInBuffer.flip(); + + // A complete TLS record header is required before we can figure out + // how many bytes there are in the record. + if (!isAvailable(netInBuffer, TLS_RECORD_HEADER_LEN)) { + result = handleIncompleteRead(netInBuffer); + return; + } + + if (!isTLSHandshake(netInBuffer)) { + // Is the client trying to use clear text HTTP? + if (isHttp(netInBuffer)) { + result = ExtractorResult.NON_SECURE; + } + return; + } + + if (!isAllRecordAvailable(netInBuffer)) { + result = handleIncompleteRead(netInBuffer); + return; + } + + if (!isClientHello(netInBuffer)) { + return; + } + + if (!isAllClientHelloAvailable(netInBuffer)) { + // Client hello didn't fit into single TLS record. + // Treat this as not present. + log.warn(sm.getString("sniExtractor.clientHelloTooBig")); + return; + } + + // Protocol Version + String legacyVersion = readProtocol(netInBuffer); + // Random + skipBytes(netInBuffer, 32); + // Session ID (single byte for length) + skipBytes(netInBuffer, (netInBuffer.get() & 0xFF)); + + // Cipher Suites + // (2 bytes for length, each cipher ID is 2 bytes) + int cipherCount = netInBuffer.getChar() / 2; + for (int i = 0; i < cipherCount; i++) { + char cipherId = netInBuffer.getChar(); + Cipher c = Cipher.valueOf(cipherId); + // Some clients transmit grease values (see RFC 8701) + if (c == null) { + clientRequestedCipherNames.add("Unknown(0x" + HexUtils.toHexString(cipherId) + ")"); + } else { + clientRequestedCiphers.add(c); + clientRequestedCipherNames.add(c.name()); + } + } + + // Compression methods (single byte for length) + skipBytes(netInBuffer, (netInBuffer.get() & 0xFF)); + + if (!netInBuffer.hasRemaining()) { + // No more data means no extensions present + return; + } + + // Extension length + skipBytes(netInBuffer, 2); + // Read the extensions until we run out of data or find the data + // we need + while (netInBuffer.hasRemaining() && (sniValue == null || + clientRequestedApplicationProtocols.isEmpty() || clientRequestedProtocols.isEmpty())) { + // Extension type is two byte + char extensionType = netInBuffer.getChar(); + // Extension size is another two bytes + char extensionDataSize = netInBuffer.getChar(); + switch (extensionType) { + case TLS_EXTENSION_SERVER_NAME: { + sniValue = readSniExtension(netInBuffer); + break; + } + case TLS_EXTENSION_ALPN: + readAlpnExtension(netInBuffer, clientRequestedApplicationProtocols); + break; + case TLS_EXTENSION_SUPPORTED_VERSION: + readSupportedVersions(netInBuffer, clientRequestedProtocols); + break; + default: { + skipBytes(netInBuffer, extensionDataSize); + } + } + } + if (clientRequestedProtocols.isEmpty()) { + clientRequestedProtocols.add(legacyVersion); + } + result = ExtractorResult.COMPLETE; + } catch (BufferUnderflowException | IllegalArgumentException e) { + throw new IOException(sm.getString("sniExtractor.clientHelloInvalid"), e); + } finally { + this.result = result; + this.clientRequestedCiphers = clientRequestedCiphers; + this.clientRequestedCipherNames = clientRequestedCipherNames; + this.clientRequestedApplicationProtocols = clientRequestedApplicationProtocols; + this.sniValue = sniValue; + this.clientRequestedProtocols = clientRequestedProtocols; + // Whatever happens, return the buffer to its original state + netInBuffer.limit(limit); + netInBuffer.position(pos); + } + } + + + public ExtractorResult getResult() { + return result; + } + + + /** + * @return The SNI value provided by the client converted to lower case if + * not already lower case. + */ + public String getSNIValue() { + if (result == ExtractorResult.COMPLETE) { + return sniValue; + } else { + throw new IllegalStateException(sm.getString("sniExtractor.tooEarly")); + } + } + + + public List getClientRequestedCiphers() { + if (result == ExtractorResult.COMPLETE || result == ExtractorResult.NOT_PRESENT) { + return clientRequestedCiphers; + } else { + throw new IllegalStateException(sm.getString("sniExtractor.tooEarly")); + } + } + + + public List getClientRequestedCipherNames() { + if (result == ExtractorResult.COMPLETE || result == ExtractorResult.NOT_PRESENT) { + return clientRequestedCipherNames; + } else { + throw new IllegalStateException(sm.getString("sniExtractor.tooEarly")); + } + } + + + public List getClientRequestedApplicationProtocols() { + if (result == ExtractorResult.COMPLETE || result == ExtractorResult.NOT_PRESENT) { + return clientRequestedApplicationProtocols; + } else { + throw new IllegalStateException(sm.getString("sniExtractor.tooEarly")); + } + } + + + public List getClientRequestedProtocols() { + if (result == ExtractorResult.COMPLETE || result == ExtractorResult.NOT_PRESENT) { + return clientRequestedProtocols; + } else { + throw new IllegalStateException(sm.getString("sniExtractor.tooEarly")); + } + } + + + private static ExtractorResult handleIncompleteRead(ByteBuffer bb) { + if (bb.limit() == bb.capacity()) { + // Buffer not big enough + return ExtractorResult.UNDERFLOW; + } else { + // Need to read more data into buffer + return ExtractorResult.NEED_READ; + } + } + + + private static boolean isAvailable(ByteBuffer bb, int size) { + if (bb.remaining() < size) { + bb.position(bb.limit()); + return false; + } + return true; + } + + + private static boolean isTLSHandshake(ByteBuffer bb) { + // For a TLS client hello the first byte must be 22 - handshake + if (bb.get() != 22) { + return false; + } + // Next two bytes are major/minor version. We need at least 3.1. + byte b2 = bb.get(); + byte b3 = bb.get(); + if (b2 < 3 || b2 == 3 && b3 == 0) { + return false; + } + return true; + } + + + private static boolean isHttp(ByteBuffer bb) { + // Based on code in Http11InputBuffer + // Note: The actual request is not important. This code only checks that + // the buffer contains a correctly formatted HTTP request line. + // The method, target and protocol are not validated. + byte chr = 0; + bb.position(0); + + // Skip blank lines + do { + if (!bb.hasRemaining()) { + return false; + } + chr = bb.get(); + } while (chr == '\r' || chr == '\n'); + + // Read the method + do { + if (!HttpParser.isToken(chr) || !bb.hasRemaining()) { + return false; + } + chr = bb.get(); + } while (chr != ' ' && chr != '\t'); + + // Whitespace between method and target + while (chr == ' ' || chr == '\t') { + if (!bb.hasRemaining()) { + return false; + } + chr = bb.get(); + } + + // Read the target + while (chr != ' ' && chr != '\t') { + if (HttpParser.isNotRequestTarget(chr) || !bb.hasRemaining()) { + return false; + } + chr = bb.get(); + } + + // Whitespace between target and protocol + while (chr == ' ' || chr == '\t') { + if (!bb.hasRemaining()) { + return false; + } + chr = bb.get(); + } + + // Read protocol + do { + if (!HttpParser.isHttpProtocol(chr) || !bb.hasRemaining()) { + return false; + } + chr = bb.get(); + + } while (chr != '\r' && chr != '\n'); + + return true; + } + + + private static boolean isAllRecordAvailable(ByteBuffer bb) { + // Next two bytes (unsigned) are the size of the record. We need all of + // it. + int size = bb.getChar(); + return isAvailable(bb, size); + } + + + private static boolean isClientHello(ByteBuffer bb) { + // Client hello is handshake type 1 + if (bb.get() == 1) { + return true; + } + return false; + } + + + private static boolean isAllClientHelloAvailable(ByteBuffer bb) { + // Next three bytes (unsigned) are the size of the client hello. We need + // all of it. + int size = ((bb.get() & 0xFF) << 16) + ((bb.get() & 0xFF) << 8) + (bb.get() & 0xFF); + return isAvailable(bb, size); + } + + + private static void skipBytes(ByteBuffer bb, int size) { + bb.position(bb.position() + size); + } + + + private static String readProtocol(ByteBuffer bb) { + char protocol = bb.getChar(); + switch (protocol) { + case 0x0300: { + return Constants.SSL_PROTO_SSLv3; + } + case 0x0301: { + return Constants.SSL_PROTO_TLSv1_0; + } + case 0x0302: { + return Constants.SSL_PROTO_TLSv1_1; + } + case 0x0303: { + return Constants.SSL_PROTO_TLSv1_2; + } + case 0x0304: { + return Constants.SSL_PROTO_TLSv1_3; + } + default: + return "Unknown(0x" + HexUtils.toHexString(protocol) + ")"; + } + } + + + private static String readSniExtension(ByteBuffer bb) { + // First 2 bytes are size of server name list (only expecting one) + // Next byte is type (0 for hostname) + skipBytes(bb, 3); + // Next 2 bytes are length of host name + char serverNameSize = bb.getChar(); + byte[] serverNameBytes = new byte[serverNameSize]; + bb.get(serverNameBytes); + return new String(serverNameBytes, StandardCharsets.UTF_8).toLowerCase(Locale.ENGLISH); + } + + + private static void readAlpnExtension(ByteBuffer bb, List protocolNames) { + // First 2 bytes are size of the protocol list + char toRead = bb.getChar(); + byte[] inputBuffer = new byte[255]; + while (toRead > 0) { + // Each list entry has one byte for length followed by a string of + // that length + int len = bb.get() & 0xFF; + bb.get(inputBuffer, 0, len); + protocolNames.add(new String(inputBuffer, 0, len, StandardCharsets.UTF_8)); + toRead--; + toRead -= len; + } + } + + + private static void readSupportedVersions(ByteBuffer bb, List protocolNames) { + // First byte is the size of the list in bytes + int count = (bb.get() & 0xFF) / 2; + // Then the list of protocols + for (int i = 0; i < count; i++) { + protocolNames.add(readProtocol(bb)); + } + } + + + public enum ExtractorResult { + COMPLETE, + NOT_PRESENT, + UNDERFLOW, + NEED_READ, + NON_SECURE + } +} diff --git a/java/org/apache/tomcat/util/net/WriteBuffer.java b/java/org/apache/tomcat/util/net/WriteBuffer.java new file mode 100644 index 0000000..fa6599d --- /dev/null +++ b/java/org/apache/tomcat/util/net/WriteBuffer.java @@ -0,0 +1,147 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.LinkedBlockingDeque; + +import org.apache.tomcat.util.buf.ByteBufferHolder; + +/** + * Provides an expandable set of buffers for writes. Non-blocking writes can be + * of any size and may not be able to be written immediately or wholly contained + * in the buffer used to perform the writes to the next layer. This class + * provides a buffering capability to allow such writes to return immediately + * and also allows for the user provided buffers to be re-used / recycled as + * required. + */ +public class WriteBuffer { + + private final int bufferSize; + + private final LinkedBlockingDeque buffers = new LinkedBlockingDeque<>(); + + public WriteBuffer(int bufferSize) { + this.bufferSize = bufferSize; + } + + void clear() { + buffers.clear(); + } + + void add(byte[] buf, int offset, int length) { + ByteBufferHolder holder = getByteBufferHolder(length); + holder.getBuf().put(buf, offset, length); + } + + + public void add(ByteBuffer from) { + ByteBufferHolder holder = getByteBufferHolder(from.remaining()); + holder.getBuf().put(from); + } + + + private ByteBufferHolder getByteBufferHolder(int capacity) { + ByteBufferHolder holder = buffers.peekLast(); + if (holder == null || holder.isFlipped() || holder.getBuf().remaining() < capacity) { + ByteBuffer buffer = ByteBuffer.allocate(Math.max(bufferSize, capacity)); + holder = new ByteBufferHolder(buffer, false); + buffers.add(holder); + } + return holder; + } + + + public boolean isEmpty() { + return buffers.isEmpty(); + } + + + /** + * Create an array of ByteBuffers from the current WriteBuffer, prefixing + * that array with the provided ByteBuffers. + * + * @param prefixes The additional ByteBuffers to add to the start of the + * array + * + * @return an array of ByteBuffers from the current WriteBuffer prefixed by + * the provided ByteBuffers + */ + ByteBuffer[] toArray(ByteBuffer... prefixes) { + List result = new ArrayList<>(); + for (ByteBuffer prefix : prefixes) { + if (prefix.hasRemaining()) { + result.add(prefix); + } + } + for (ByteBufferHolder buffer : buffers) { + buffer.flip(); + result.add(buffer.getBuf()); + } + buffers.clear(); + return result.toArray(new ByteBuffer[0]); + } + + + boolean write(SocketWrapperBase socketWrapper, boolean blocking) throws IOException { + Iterator bufIter = buffers.iterator(); + boolean dataLeft = false; + while (!dataLeft && bufIter.hasNext()) { + ByteBufferHolder buffer = bufIter.next(); + buffer.flip(); + if (blocking) { + socketWrapper.writeBlocking(buffer.getBuf()); + } else { + socketWrapper.writeNonBlockingInternal(buffer.getBuf()); + } + if (buffer.getBuf().remaining() == 0) { + bufIter.remove(); + } else { + dataLeft = true; + } + } + return dataLeft; + } + + + public boolean write(Sink sink, boolean blocking) throws IOException { + Iterator bufIter = buffers.iterator(); + boolean dataLeft = false; + while (!dataLeft && bufIter.hasNext()) { + ByteBufferHolder buffer = bufIter.next(); + buffer.flip(); + dataLeft = sink.writeFromBuffer(buffer.getBuf(), blocking); + if (!dataLeft) { + bufIter.remove(); + } + } + return dataLeft; + } + + + /** + * Interface implemented by clients of the WriteBuffer to enable data to be + * written back out from the buffer. + */ + public interface Sink { + boolean writeFromBuffer(ByteBuffer buffer, boolean block) throws IOException; + } +} diff --git a/java/org/apache/tomcat/util/net/jsse/JSSEImplementation.java b/java/org/apache/tomcat/util/net/jsse/JSSEImplementation.java new file mode 100644 index 0000000..9ea9ce4 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/JSSEImplementation.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.util.net.jsse; + +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLSession; + +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLImplementation; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SSLUtil; + +/* JSSEImplementation: + + Concrete implementation class for JSSE + + @author EKR + */ + +public class JSSEImplementation extends SSLImplementation { + + public JSSEImplementation() { + // Make sure the keySizeCache is loaded now as part of connector startup + // else the cache will be populated on first use which will slow that + // request down. + JSSESupport.init(); + } + + @Override + public SSLSupport getSSLSupport(SSLSession session, Map> additionalAttributes) { + return new JSSESupport(session, additionalAttributes); + } + + @Override + public SSLUtil getSSLUtil(SSLHostConfigCertificate certificate) { + return new JSSEUtil(certificate); + } +} diff --git a/java/org/apache/tomcat/util/net/jsse/JSSEKeyManager.java b/java/org/apache/tomcat/util/net/jsse/JSSEKeyManager.java new file mode 100644 index 0000000..30777d6 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/JSSEKeyManager.java @@ -0,0 +1,122 @@ +/* + * 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.tomcat.util.net.jsse; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; + +/** + * X509KeyManager which allows selection of a specific key pair and certificate + * chain (identified by their keystore alias name) to be used by the server to + * authenticate itself to SSL clients. + * + * @author Jan Luehe + */ +public final class JSSEKeyManager extends X509ExtendedKeyManager { + + private X509KeyManager delegate; + private String serverKeyAlias; + + + /** + * Constructor. + * + * @param mgr The X509KeyManager used as a delegate + * @param serverKeyAlias The alias name of the server's key pair and + * supporting certificate chain + */ + public JSSEKeyManager(X509KeyManager mgr, String serverKeyAlias) { + super(); + this.delegate = mgr; + this.serverKeyAlias = serverKeyAlias; + } + + + /** + * Returns the server key alias that was provided in the constructor or the + * result from {@link X509KeyManager#chooseServerAlias(String, Principal[], + * Socket)} for the delegate if no alias is specified. + */ + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + if (serverKeyAlias != null) { + return serverKeyAlias; + } + + return delegate.chooseServerAlias(keyType, issuers, socket); + } + + + /** + * Returns the server key alias that was provided in the constructor or the + * result from {@link X509ExtendedKeyManager#chooseEngineServerAlias(String, + * Principal[], SSLEngine)} for the delegate if no alias is specified. + */ + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, + SSLEngine engine) { + if (serverKeyAlias!=null) { + return serverKeyAlias; + } + + return super.chooseEngineServerAlias(keyType, issuers, engine); + } + + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, + Socket socket) { + return delegate.chooseClientAlias(keyType, issuers, socket); + } + + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return delegate.getCertificateChain(alias); + } + + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return delegate.getClientAliases(keyType, issuers); + } + + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return delegate.getServerAliases(keyType, issuers); + } + + + @Override + public PrivateKey getPrivateKey(String alias) { + return delegate.getPrivateKey(alias); + } + + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, + SSLEngine engine) { + return delegate.chooseClientAlias(keyType, issuers, null); + } +} diff --git a/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java b/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java new file mode 100644 index 0000000..6d229cb --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java @@ -0,0 +1,106 @@ +/* + * 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.tomcat.util.net.jsse; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.tomcat.util.net.SSLContext; + +class JSSESSLContext implements SSLContext { + + private javax.net.ssl.SSLContext context; + private KeyManager[] kms; + private TrustManager[] tms; + + JSSESSLContext(String protocol) throws NoSuchAlgorithmException { + context = javax.net.ssl.SSLContext.getInstance(protocol); + } + + @Override + public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) + throws KeyManagementException { + this.kms = kms; + this.tms = tms; + context.init(kms, tms, sr); + } + + @Override + public void destroy() { + } + + @Override + public SSLSessionContext getServerSessionContext() { + return context.getServerSessionContext(); + } + + @Override + public SSLEngine createSSLEngine() { + return context.createSSLEngine(); + } + + @Override + public SSLServerSocketFactory getServerSocketFactory() { + return context.getServerSocketFactory(); + } + + @Override + public SSLParameters getSupportedSSLParameters() { + return context.getSupportedSSLParameters(); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + X509Certificate[] result = null; + if (kms != null) { + for (int i = 0; i < kms.length && result == null; i++) { + if (kms[i] instanceof X509KeyManager) { + result = ((X509KeyManager) kms[i]).getCertificateChain(alias); + } + } + } + return result; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + Set certs = new HashSet<>(); + if (tms != null) { + for (TrustManager tm : tms) { + if (tm instanceof X509TrustManager) { + X509Certificate[] accepted = ((X509TrustManager) tm).getAcceptedIssuers(); + certs.addAll(Arrays.asList(accepted)); + } + } + } + return certs.toArray(new X509Certificate[0]); + } +} diff --git a/java/org/apache/tomcat/util/net/jsse/JSSESupport.java b/java/org/apache/tomcat/util/net/jsse/JSSESupport.java new file mode 100644 index 0000000..059cf77 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/JSSESupport.java @@ -0,0 +1,228 @@ +/* + * 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.tomcat.util.net.jsse; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLSession; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.net.SSLSessionManager; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.res.StringManager; + +/** + * JSSESupport. + * + * Concrete implementation class for JSSE Support classes. + * + * @author EKR + * @author Craig R. McClanahan + * Parts cribbed from JSSECertCompat + * Parts cribbed from CertificatesValve + */ +public class JSSESupport implements SSLSupport, SSLSessionManager { + + private static final Log log = LogFactory.getLog(JSSESupport.class); + + private static final StringManager sm = StringManager.getManager(JSSESupport.class); + + private static final Map keySizeCache = new HashMap<>(); + + static { + for (Cipher cipher : Cipher.values()) { + for (String jsseName : cipher.getJsseNames()) { + keySizeCache.put(jsseName, Integer.valueOf(cipher.getStrength_bits())); + } + } + } + + /* + * NO-OP method provided to make it easy for other classes in this package + * to trigger the loading of this class and the population of the + * keySizeCache. + */ + static void init() { + // NO-OP + } + + private SSLSession session; + private Map> additionalAttributes; + + public JSSESupport(SSLSession session, Map> additionalAttributes) { + this.session = session; + this.additionalAttributes = additionalAttributes; + } + + @Override + public String getCipherSuite() throws IOException { + // Look up the current SSLSession + if (session == null) { + return null; + } + return session.getCipherSuite(); + } + + @Override + public X509Certificate[] getLocalCertificateChain() { + if (session == null) { + return null; + } + return convertCertificates(session.getLocalCertificates()); + } + + @Override + public X509Certificate[] getPeerCertificateChain() throws IOException { + // Look up the current SSLSession + if (session == null) { + return null; + } + + Certificate [] certs=null; + try { + certs = session.getPeerCertificates(); + } catch( Throwable t ) { + log.debug(sm.getString("jsseSupport.clientCertError"), t); + return null; + } + + return convertCertificates(certs); + } + + + private static X509Certificate[] convertCertificates(Certificate[] certs) { + if( certs==null ) { + return null; + } + + X509Certificate [] x509Certs = new X509Certificate[certs.length]; + for(int i=0; i < certs.length; i++) { + if (certs[i] instanceof X509Certificate ) { + // always currently true with the JSSE 1.1.x + x509Certs[i] = (X509Certificate) certs[i]; + } else { + try { + byte [] buffer = certs[i].getEncoded(); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream stream = new ByteArrayInputStream(buffer); + x509Certs[i] = (X509Certificate) cf.generateCertificate(stream); + } catch(Exception ex) { + log.info(sm.getString("jsseSupport.certTranslationError", certs[i]), ex); + return null; + } + } + if(log.isTraceEnabled()) { + log.trace("Cert #" + i + " = " + x509Certs[i]); + } + } + if(x509Certs.length < 1) { + return null; + } + return x509Certs; + } + + + /** + * {@inheritDoc} + *

    + * This returns the effective bits for the current cipher suite. + */ + @Override + public Integer getKeySize() throws IOException { + // Look up the current SSLSession + if (session == null) { + return null; + } + + return keySizeCache.get(session.getCipherSuite()); + } + + @Override + public String getSessionId() + throws IOException { + // Look up the current SSLSession + if (session == null) { + return null; + } + // Expose ssl_session (getId) + byte [] ssl_session = session.getId(); + if ( ssl_session == null) { + return null; + } + StringBuilder buf=new StringBuilder(); + for (byte b : ssl_session) { + String digit = Integer.toHexString(b); + if (digit.length() < 2) { + buf.append('0'); + } + if (digit.length() > 2) { + digit = digit.substring(digit.length() - 2); + } + buf.append(digit); + } + return buf.toString(); + } + + + public void setSession(SSLSession session) { + this.session = session; + } + + + /** + * Invalidate the session this support object is associated with. + */ + @Override + public void invalidateSession() { + session.invalidate(); + } + + @Override + public String getProtocol() throws IOException { + if (session == null) { + return null; + } + return session.getProtocol(); + } + + @Override + public String getRequestedProtocols() throws IOException { + if (additionalAttributes == null) { + return null; + } + return StringUtils.join(additionalAttributes.get(REQUESTED_PROTOCOL_VERSIONS_KEY)); + } + + @Override + public String getRequestedCiphers() throws IOException { + if (additionalAttributes == null) { + return null; + } + return StringUtils.join(additionalAttributes.get(REQUESTED_CIPHERS_KEY)); + } +} + diff --git a/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java b/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java new file mode 100644 index 0000000..be34001 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java @@ -0,0 +1,157 @@ +/* + * 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.tomcat.util.net.jsse; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.compat.JreVendor; +import org.apache.tomcat.util.net.SSLContext; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLUtilBase; +import org.apache.tomcat.util.res.StringManager; + +/** + * SSLUtil implementation for JSSE. + * + * @author Harish Prabandham + * @author Costin Manolache + * @author Stefan Freyr Stefansson + * @author EKR + * @author Jan Luehe + */ +public class JSSEUtil extends SSLUtilBase { + + private static final Log log = LogFactory.getLog(JSSEUtil.class); + private static final StringManager sm = StringManager.getManager(JSSEUtil.class); + + private volatile boolean initialized = false; + + private volatile Set implementedProtocols; + private volatile Set implementedCiphers; + + + public JSSEUtil (SSLHostConfigCertificate certificate) { + this(certificate, true); + } + + + public JSSEUtil (SSLHostConfigCertificate certificate, boolean warnOnSkip) { + super(certificate, warnOnSkip); + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + protected Set getImplementedProtocols() { + initialise(); + return implementedProtocols; + } + + + @Override + protected Set getImplementedCiphers() { + initialise(); + return implementedCiphers; + } + + + @Override + protected boolean isTls13RenegAuthAvailable() { + // TLS 1.3 does not support authentication after the initial handshake + return false; + } + + + @Override + public SSLContext createSSLContextInternal(List negotiableProtocols) + throws NoSuchAlgorithmException { + return new JSSESSLContext(sslHostConfig.getSslProtocol()); + } + + + private void initialise() { + if (!initialized) { + synchronized (this) { + if (!initialized) { + SSLContext context; + try { + context = new JSSESSLContext(sslHostConfig.getSslProtocol()); + context.init(null, null, null); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + // This is fatal for the connector so throw an exception to prevent + // it from starting + throw new IllegalArgumentException(e); + } + + String[] implementedProtocolsArray = context.getSupportedSSLParameters().getProtocols(); + implementedProtocols = new HashSet<>(implementedProtocolsArray.length); + + // Filter out SSLv2 from the list of implemented protocols (just in case + // we are running on a JVM that supports it) since it is no longer + // considered secure but allow SSLv2Hello. + // Note SSLv3 is allowed despite known insecurities because some users + // still have a requirement for it. + for (String protocol : implementedProtocolsArray) { + String protocolUpper = protocol.toUpperCase(Locale.ENGLISH); + if (!"SSLV2HELLO".equals(protocolUpper) && !"SSLV3".equals(protocolUpper)) { + if (protocolUpper.contains("SSL")) { + log.debug(sm.getString("jsseUtil.excludeProtocol", protocol)); + continue; + } + } + implementedProtocols.add(protocol); + } + + if (implementedProtocols.size() == 0) { + log.warn(sm.getString("jsseUtil.noDefaultProtocols")); + } + + String[] implementedCipherSuiteArray = context.getSupportedSSLParameters().getCipherSuites(); + // The IBM JRE will accept cipher suites names SSL_xxx or TLS_xxx but + // only returns the SSL_xxx form for supported cipher suites. Therefore + // need to filter the requested cipher suites using both forms with an + // IBM JRE. + if (JreVendor.IS_IBM_JVM) { + implementedCiphers = new HashSet<>(implementedCipherSuiteArray.length * 2); + for (String name : implementedCipherSuiteArray) { + implementedCiphers.add(name); + if (name.startsWith("SSL")) { + implementedCiphers.add("TLS" + name.substring(3)); + } + } + } else { + implementedCiphers = new HashSet<>(Arrays.asList(implementedCipherSuiteArray)); + } + initialized = true; + } + } + } + } +} diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties new file mode 100644 index 0000000..6a3d1fd --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties @@ -0,0 +1,30 @@ +# 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. + +jsseSupport.certTranslationError=Error translating certificate [{0}] +jsseSupport.clientCertError=Error trying to obtain a certificate from the client + +jsseUtil.excludeProtocol=The SSL protocol [{0}] which is supported in this JRE was excluded from the protocols available to Tomcat +jsseUtil.noDefaultProtocols=Unable to determine a default for sslEnabledProtocols. Set an explicit value to ensure the connector can start. + +pemFile.noMultiPrimes=The PKCS#1 certificate is in multi-prime format and Java does not provide an API for constructing an RSA private key object from that format +pemFile.noPassword=A password is required to decrypt the private key +pemFile.notPbkdf2=The OID [{0}] is not the correct OID for PKBDF2 which is the only permitted KDF for PBES2 +pemFile.notValidRFC5915=The provided key file does not conform to RFC 5915 +pemFile.parseError=Unable to parse the key from [{0}] +pemFile.unknownEncryptedFormat=The format [{0}] is not a recognised encrypted PEM file format +pemFile.unknownEncryptionAlgorithm=The encryption algorithm with DER encoded OID of [{0}] was not recognised +pemFile.unknownPkcs8Algorithm=The PKCS#8 encryption algorithm with DER encoded OID of [{0}] was not recognised +pemFile.unknownPrfAlgorithm=The pseudo random function with DER encoded OID of [{0}] was not recognised diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_cs.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_cs.properties new file mode 100644 index 0000000..6811538 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_cs.properties @@ -0,0 +1,16 @@ +# 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. + +pemFile.parseError=Nelze parsovat klÃ­Ä z [{0}] diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_de.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_de.properties new file mode 100644 index 0000000..14941f1 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_de.properties @@ -0,0 +1,19 @@ +# 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. + +jsseSupport.certTranslationError=Fehler beim Ãœbersetzen des Zertifikates [{0}] +jsseSupport.clientCertError=Fehler beim Versuch ein Zertifikat vom Client zu erhalten. + +pemFile.parseError=Der Schlüssel konnte nicht aus [{0}] geparst werden. diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_es.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_es.properties new file mode 100644 index 0000000..5fffe6f --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_es.properties @@ -0,0 +1,18 @@ +# 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. + +jsseSupport.clientCertError=Error tratando de obtener un certificado desde el cliente + +pemFile.parseError=Imposible parsear la clave desde [{0}] diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties new file mode 100644 index 0000000..73ea740 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties @@ -0,0 +1,30 @@ +# 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. + +jsseSupport.certTranslationError=Erreur lors de la traduction du certificat [{0}] +jsseSupport.clientCertError=Echec de l'obtention d'un certificat de la part du client + +jsseUtil.excludeProtocol=Le protocole SSL [{0}] qui est supporté par ce JRE a été exclu des protocoles disponibles dans Tomcat +jsseUtil.noDefaultProtocols=Impossible de déterminer un défaut pour sslEnabledProtocols de [{0}], indiquez une valeur explicite pour permettre le démarrage du connecteur + +pemFile.noMultiPrimes=Le certificat PKCS#1 est dans un format mutli-prime et Java ne fournit pas d'API pour construire une clé privée RSA à partir de ce format +pemFile.noPassword=Un mot de passe est requis pour déchiffrer la clé privée +pemFile.notPbkdf2=L''OID [{0}] n''est pas un OID correct pour PKBDF2 qui est le seul KDF autorisé pour PBES2 +pemFile.notValidRFC5915=La fichier de clé fourni ne se conforme pas à la RFC 5915 +pemFile.parseError=Impossible de parser la clé de [{0}] +pemFile.unknownEncryptedFormat=Le format [{0}] n''est pas un format de cryptage reconnu pour un fichier PEM +pemFile.unknownEncryptionAlgorithm=L''algorithme de cryptage avec un OID encodé en DER de [{0}] n''est pas reconnu +pemFile.unknownPkcs8Algorithm=L''algorithme de cryptage PKCS#8 avec un OID encodé en DER de [{0}] n''est pas reconnu +pemFile.unknownPrfAlgorithm=La fonction pseudo aléatoire avec un OID encodé en DER de [{0}] n''est pas reconnu diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties new file mode 100644 index 0000000..09c1bc6 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties @@ -0,0 +1,30 @@ +# 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. + +jsseSupport.certTranslationError=証明書ã®ç¿»è¨³ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠[{0}] +jsseSupport.clientCertError=クライアント証明書をå–得中ã®ã‚¨ãƒ©ãƒ¼ + +jsseUtil.excludeProtocol=JRE 㯠SSL プロトコル [{0}] ã«å¯¾å¿œã—ã¦ã„ã¾ã™ã€‚ã—ã‹ã— Tomcat ã®åˆ©ç”¨å¯èƒ½ãƒ—ロトコルã‹ã‚‰ã¯é™¤å¤–ã•ã‚Œã¦ã„ã¾ã™ã€‚ +jsseUtil.noDefaultProtocols=sslEnableProtocols ã®æ—¢å®šå€¤ã‚’å–å¾—ã§ãã¾ã›ã‚“。コãƒã‚¯ã‚¿ãƒ¼ã‚’開始ã§ãるよã†æ˜Žç¤ºçš„ã«å€¤ã‚’設定ã—ã¦ãã ã•ã„。 + +pemFile.noMultiPrimes=PKCS#1 証明書㯠multi-prime RSA フォーマットã§ã™ãŒã€Java ã¯ãã®ã‚ˆã†ãªãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã«å¯¾ã™ã‚‹ RSA 秘密éµã‚’構築ã™ã‚‹ API ã‚’æä¾›ã—ã¦ã„ã¾ã›ã‚“ +pemFile.noPassword=秘密éµã‚’復å·ã™ã‚‹ã«ã¯ãƒ‘スワードãŒå¿…è¦ã§ã™ +pemFile.notPbkdf2=OID [{0}] ã¯ã€PBES2 ã«å”¯ä¸€è¨±å¯ã•ã‚Œã‚‹ KDF ã§ã‚ã‚‹ PKBDF2 ã®æ­£ã—ã„ OID ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +pemFile.notValidRFC5915=与ãˆã‚‰ã‚ŒãŸã‚­ãƒ¼ãƒ•ã‚¡ã‚¤ãƒ«ã¯ RFC 5915 ã«æº–æ‹ ã—ã¦ã„ã¾ã›ã‚“ +pemFile.parseError=秘密éµãƒ•ã‚¡ã‚¤ãƒ« [{0}] を解æžã§ãã¾ã›ã‚“ +pemFile.unknownEncryptedFormat=フォーマット [{0}] ã¯èªè­˜ã•ã‚Œã‚‹æš—å·åŒ– PEM ファイル形å¼ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +pemFile.unknownEncryptionAlgorithm=[{0}] ã® DER エンコードã•ã‚ŒãŸ OID ã‚’æŒã¤æš—å·åŒ–アルゴリズムãŒèªè­˜ã•ã‚Œã¾ã›ã‚“ã§ã—㟠+pemFile.unknownPkcs8Algorithm=[{0}] ã® DER エンコードã•ã‚ŒãŸ OID ã‚’æŒã¤ PKCS#8 æš—å·åŒ–アルゴリズムãŒèªè­˜ã•ã‚Œã¾ã›ã‚“ã§ã—㟠+pemFile.unknownPrfAlgorithm=[{0}] ã® DER エンコードã•ã‚ŒãŸ OID ã‚’æŒã¤æ“¬ä¼¼ãƒ©ãƒ³ãƒ€ãƒ é–¢æ•°ãŒèªè­˜ã•ã‚Œã¾ã›ã‚“ã§ã—㟠diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_ko.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_ko.properties new file mode 100644 index 0000000..1882888 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_ko.properties @@ -0,0 +1,24 @@ +# 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. + +jsseSupport.certTranslationError=ì¸ì¦ì„œ [{0}]ì— ëŒ€í•œ ì¸ì¦ì„œ ë³€í™˜ì„ í•˜ëŠ” 중 오류 ë°œìƒ +jsseSupport.clientCertError=í´ë¼ì´ì–¸íŠ¸ë¡œë¶€í„° ì¸ì¦ì„œë¥¼ 구하려 ì‹œë„하는 중 오류 ë°œìƒ + +jsseUtil.excludeProtocol=ì´ JREì—ì„œ 지ì›ë˜ëŠ” 해당 SSL 프로토콜 [{0}]ì´(ê°€), Tomcatì˜ ê°€ìš© 프로토콜 목ë¡ì—ì„œ 제외ë˜ì–´ 있습니다. +jsseUtil.noDefaultProtocols=sslEnabledProtocolsì˜ ê¸°ë³¸ê°’ì„ ê²°ì •í•  수 없습니다. Connectorê°€ 제대로 시작ë˜ëŠ”지 ë³´ì¦í•˜ë ¤ë©´ 명시ì ìœ¼ë¡œ ê°’ì„ ì„¤ì •í•˜ì‹­ì‹œì˜¤. + +pemFile.noMultiPrimes=해당 PKCS#1 ì¸ì¦ì„œëŠ” multi-prime í¬ë§·ìœ¼ë¡œ ë˜ì–´ 있는ë°, ìžë°”는 해당 í¬ë§·ìœ¼ë¡œë¶€í„° RSA ê°œì¸ í‚¤ ê°ì²´ë¥¼ ìƒì„±í•  API를 제공하지 않습니다. +pemFile.notValidRFC5915=ì œê³µëœ í‚¤ëŠ” RFC 5915를 따르지 않습니다 +pemFile.parseError=[{0}](으)로부터 키를 파싱할 수 없습니다. diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..34d568f --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +pemFile.parseError=Impossível parsear a chave de [{0}] diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_ru.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_ru.properties new file mode 100644 index 0000000..4781a78 --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_ru.properties @@ -0,0 +1,18 @@ +# 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. + +jsseSupport.clientCertError=Ошибка при попытке Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ñертификата от клиента + +pemFile.parseError=Ðевозможно получить ключ из [{0}] diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..fc763bb --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings_zh_CN.properties @@ -0,0 +1,24 @@ +# 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. + +jsseSupport.certTranslationError=错误的转æ¢è¯ä¹¦[{0}] +jsseSupport.clientCertError=å°è¯•ä»Žå®¢æˆ·ç«¯èŽ·å–è¯ä¹¦æ—¶å‡ºé”™ + +jsseUtil.excludeProtocol=æ­¤JRE支æŒçš„SSLåè®®[{0}]已从Tomcatå¯ç”¨çš„å议中排除 +jsseUtil.noDefaultProtocols=无法确定sslEnabledProtocols的默认值。设置显å¼å€¼ä»¥ç¡®ä¿è¿žæŽ¥å™¨å¯ä»¥å¯åŠ¨ã€‚ + +pemFile.noMultiPrimes=PKCS#1è¯ä¹¦æ˜¯å¤šç´ æ•°æ ¼å¼çš„,Javaä¸æ供从该格å¼æž„造RSAç§é’¥å¯¹è±¡çš„API +pemFile.notValidRFC5915=æ供的key文件ä¸ç¬¦åˆRFC 5915 +pemFile.parseError=无法从 [{0}] è§£æž key diff --git a/java/org/apache/tomcat/util/net/jsse/PEMFile.java b/java/org/apache/tomcat/util/net/jsse/PEMFile.java new file mode 100644 index 0000000..8b5e8fd --- /dev/null +++ b/java/org/apache/tomcat/util/net/jsse/PEMFile.java @@ -0,0 +1,695 @@ +/* + * 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.tomcat.util.net.jsse; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.tomcat.util.buf.Asn1Parser; +import org.apache.tomcat.util.buf.Asn1Writer; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.res.StringManager; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.Oid; + +/** + * RFC 1421 PEM file containing X509 certificates or private keys. + */ +public class PEMFile { + + private static final StringManager sm = StringManager.getManager(PEMFile.class); + + private static final byte[] OID_EC_PUBLIC_KEY = + new byte[] { 0x06, 0x07, 0x2A, (byte) 0x86, 0x48, (byte) 0xCE, 0x3D, 0x02, 0x01 }; + // 1.2.840.113549.1.5.13 + private static final byte[] OID_PBES2 = + new byte[] { 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0x0D, 0x01, 0x05, 0x0D }; + // 1.2.840.113549.1.5.12 + private static final byte[] OID_PBKDF2 = + new byte[] { 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0x0D, 0x01, 0x05, 0x0C }; + + // Default defined in RFC 8018 + private static final String DEFAULT_PRF = "HmacSHA1"; + + private static final Map OID_TO_PRF = new HashMap<>(); + static { + // 1.2.840.113549.2.7 + OID_TO_PRF.put("2a864886f70d0207", DEFAULT_PRF); + // 1.2.840.113549.2.8 + OID_TO_PRF.put("2a864886f70d0208", "HmacSHA224"); + // 1.2.840.113549.2.9 + OID_TO_PRF.put("2a864886f70d0209", "HmacSHA256"); + // 1.2.840.113549.2.10 + OID_TO_PRF.put("2a864886f70d020a", "HmacSHA384"); + // 1.2.840.113549.2.11 + OID_TO_PRF.put("2a864886f70d020b", "HmacSHA512"); + // 1.2.840.113549.2.12 + OID_TO_PRF.put("2a864886f70d020c", "HmacSHA512/224"); + // 1.2.840.113549.2.13 + OID_TO_PRF.put("2a864886f70d020d", "HmacSHA512/256"); + } + + private static final Map OID_TO_ALGORITHM = new HashMap<>(); + static { + // 1.2.840.113549.3.7 + OID_TO_ALGORITHM.put("2a864886f70d0307", Algorithm.DES_EDE3_CBC); + // 2.16.840.1.101.3.4.1.2 + OID_TO_ALGORITHM.put("608648016503040102", Algorithm.AES128_CBC_PAD); + // 2.16.840.1.101.3.4.1.42 + OID_TO_ALGORITHM.put("60864801650304012a", Algorithm.AES256_CBC_PAD); + } + + public static String toPEM(X509Certificate certificate) throws CertificateEncodingException { + StringBuilder result = new StringBuilder(); + result.append(Part.BEGIN_BOUNDARY + Part.CERTIFICATE + Part.FINISH_BOUNDARY); + result.append(System.lineSeparator()); + Base64 b64 = new Base64(64); + result.append(b64.encodeAsString(certificate.getEncoded())); + result.append(Part.END_BOUNDARY + Part.CERTIFICATE + Part.FINISH_BOUNDARY); + return result.toString(); + } + + private List certificates = new ArrayList<>(); + private PrivateKey privateKey; + + public List getCertificates() { + return certificates; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public PEMFile(String filename) throws IOException, GeneralSecurityException { + this(filename, null); + } + + public PEMFile(String filename, String password) throws IOException, GeneralSecurityException { + this(filename, password, null); + } + + public PEMFile(String filename, String password, String keyAlgorithm) throws IOException, GeneralSecurityException { + this(filename, ConfigFileLoader.getSource().getResource(filename).getInputStream(), password, keyAlgorithm); + } + + public PEMFile(String filename, String password, String passwordFilename, String keyAlgorithm) + throws IOException, GeneralSecurityException { + this(filename, ConfigFileLoader.getSource().getResource(filename).getInputStream(), password, passwordFilename, + passwordFilename != null ? ConfigFileLoader.getSource().getResource(passwordFilename).getInputStream() : + null, + keyAlgorithm); + } + + public PEMFile(String filename, InputStream fileStream, String password, String keyAlgorithm) + throws IOException, GeneralSecurityException { + this(filename, fileStream, password, null, null, keyAlgorithm); + } + + /** + * @param filename the filename to mention in error messages, not used for anything else. + * @param fileStream the stream containing the pem(s). + * @param password password to load the pem objects. + * @param passwordFilename the password filename to mention in error messages, not used for anything else. + * @param passwordFileStream stream containing the password to load the pem objects. + * @param keyAlgorithm the algorithm to help to know how to load the objects (guessed if null). + * + * @throws IOException if input can't be read. + * @throws GeneralSecurityException if input can't be parsed/loaded. + */ + public PEMFile(String filename, InputStream fileStream, String password, String passwordFilename, + InputStream passwordFileStream, String keyAlgorithm) throws IOException, GeneralSecurityException { + List parts = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(fileStream, StandardCharsets.US_ASCII))) { + Part part = null; + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith(Part.BEGIN_BOUNDARY)) { + part = new Part(); + part.type = + line.substring(Part.BEGIN_BOUNDARY.length(), line.length() - Part.FINISH_BOUNDARY.length()) + .trim(); + } else if (line.startsWith(Part.END_BOUNDARY)) { + parts.add(part); + part = null; + } else if (part != null && !line.contains(":") && !line.startsWith(" ")) { + part.content += line; + } else if (part != null && line.contains(":") && !line.startsWith(" ")) { + /* Something like DEK-Info: DES-EDE3-CBC,B5A53CB8B7E50064 */ + if (line.startsWith("DEK-Info: ")) { + String[] pieces = line.split(" "); + pieces = pieces[1].split(","); + if (pieces.length == 2) { + part.algorithm = pieces[0]; + part.ivHex = pieces[1]; + } + } + } + } + } + + String passwordToUse = null; + if (passwordFileStream != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(passwordFileStream, StandardCharsets.UTF_8))) { + passwordToUse = reader.readLine(); + } + } else { + passwordToUse = password; + } + + for (Part part : parts) { + switch (part.type) { + case Part.PRIVATE_KEY: + privateKey = part.toPrivateKey(keyAlgorithm, Format.PKCS8, filename); + break; + case Part.EC_PRIVATE_KEY: + privateKey = part.toPrivateKey("EC", Format.RFC5915, filename); + break; + case Part.ENCRYPTED_PRIVATE_KEY: + privateKey = part.toPrivateKey(passwordToUse, keyAlgorithm, Format.PKCS8, filename); + break; + case Part.RSA_PRIVATE_KEY: + if (part.algorithm == null) { + // If no encryption algorithm was detected, ignore any + // (probably default) key password provided. + privateKey = part.toPrivateKey(keyAlgorithm, Format.PKCS1, filename); + } else { + privateKey = part.toPrivateKey(passwordToUse, keyAlgorithm, Format.PKCS1, filename); + } + break; + case Part.CERTIFICATE: + case Part.X509_CERTIFICATE: + certificates.add(part.toCertificate()); + break; + } + } + } + + private static class Part { + public static final String BEGIN_BOUNDARY = "-----BEGIN "; + public static final String END_BOUNDARY = "-----END "; + public static final String FINISH_BOUNDARY = "-----"; + + public static final String PRIVATE_KEY = "PRIVATE KEY"; + public static final String EC_PRIVATE_KEY = "EC PRIVATE KEY"; + public static final String ENCRYPTED_PRIVATE_KEY = "ENCRYPTED PRIVATE KEY"; + public static final String RSA_PRIVATE_KEY = "RSA PRIVATE KEY"; + public static final String CERTIFICATE = "CERTIFICATE"; + public static final String X509_CERTIFICATE = "X509 CERTIFICATE"; + + public String type; + public String content = ""; + public String algorithm = null; + public String ivHex = null; + + private byte[] decode() { + return Base64.decodeBase64(content); + } + + public X509Certificate toCertificate() throws CertificateException { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(decode())); + } + + + /** + * Extracts the private key from an unencrypted PEMFile. + * + * @param keyAlgorithm Key algorithm if known or null if it needs to be obtained from the PEM file + * @param format The format used to encode the private key + * @param filename The name of the PEM file + * + * @return The clear text private key extracted from the PEM file + * + * @throws GeneralSecurityException If there is a cryptographic error processing the PEM file + */ + public PrivateKey toPrivateKey(String keyAlgorithm, Format format, String filename) + throws GeneralSecurityException { + return toPrivateKey(keyAlgorithm, format, filename, decode()); + } + + + /** + * Extracts the private key from an encrypted PEMFile. + * + * @param password Password to decrypt the private key + * @param keyAlgorithm Key algorithm if known or null if it needs to be obtained from the PEM file + * @param format The format used to encode the private key + * @param filename The name of the PEM file + * + * @return The clear text private key extracted from the PEM file + * + * @throws GeneralSecurityException If there is a cryptographic error processing the PEM file + * @throws IOException If there is an I/O error reading the PEM file + */ + public PrivateKey toPrivateKey(String password, String keyAlgorithm, Format format, String filename) + throws GeneralSecurityException, IOException { + + String secretKeyAlgorithm; + String cipherTransformation; + int keyLength; + + switch (format) { + case PKCS1: { + + switch (algorithm) { + case "DES-CBC": { + secretKeyAlgorithm = "DES"; + cipherTransformation = "DES/CBC/PKCS5Padding"; + keyLength = 8; + break; + } + case "DES-EDE3-CBC": { + secretKeyAlgorithm = "DESede"; + cipherTransformation = "DESede/CBC/PKCS5Padding"; + keyLength = 24; + break; + } + case "AES-256-CBC": { + secretKeyAlgorithm = "AES"; + cipherTransformation = "AES/CBC/PKCS5Padding"; + keyLength = 32; + break; + } + default: + // This will almost certainly trigger errors + secretKeyAlgorithm = algorithm; + cipherTransformation = algorithm; + keyLength = 8; + break; + } + + byte[] iv = fromHex(ivHex); + // The IV is also used as salt for the password generation + byte[] key = deriveKeyPBKDF1(keyLength, password, iv); + SecretKey secretKey = new SecretKeySpec(key, secretKeyAlgorithm); + Cipher cipher = Cipher.getInstance(cipherTransformation); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); + byte[] pkcs1 = cipher.doFinal(decode()); + return toPrivateKey(keyAlgorithm, format, filename, pkcs1); + } + case PKCS8: { + // Encrypted PEM file is PKCS8 + Asn1Parser p = new Asn1Parser(decode()); + + //@formatter:off + /* + * RFC 5208 - PKCS #8 + * RFC 8018 - PKCS #5 + * + * Nesting + * SEQ 1 - PKCS #8 + * SEQ 2 - PKCS #8 encryptionAlgorithm + * OID - PKCS #5 PBES2 OID + * SEQ 3 - PKCS #5 PBES2-params + * SEQ 4 - PKCS #5 PBES2 key derivation function + * OID - PKCS #5 PBES2 KDF OID - must be PBKDF2 + * SEQ 5 - PKCS #5 PBKDF2-params + * OCTET STRING - PKCS #5 PBKDF2 salt + * INT - PKCS #5 PBKDF2 interationCount + * INT - PKCS #5 PBKDF2 key length OPTIONAL + * SEQ 6 - PKCS #5 PBKDF2 PRF defaults to HmacSHA1 if not present + * OID - PKCS #5 PBKDF2 PRF OID + * NULL - PKCS #5 PBKDF2 PRF parameters + * SEQ 4 - PKCS #5 PBES2 encryption scheme + * OID - PKCS #5 PBES2 algorithm OID + * OCTET STRING - PKCS #5 PBES2 algorithm iv + * OCTET STRING - PKCS #8 encryptedData + */ + //@formatter:on + + // Parse the PKCS #8 outer sequence and validate the length + p.parseTagSequence(); + p.parseFullLength(); + + // Parse the PKCS #8 encryption algorithm + p.parseTagSequence(); + p.parseLength(); + + // PBES2 OID + byte[] oidEncryptionAlgorithm = p.parseOIDAsBytes(); + /* + * Implementation note. If other algorithms are ever supported, the KDF check below is likely to + * need to be adjusted. + */ + if (!Arrays.equals(oidEncryptionAlgorithm, OID_PBES2)) { + throw new NoSuchAlgorithmException(sm.getString("pemFile.unknownPkcs8Algorithm", + toDottedOidString(oidEncryptionAlgorithm))); + } + + // PBES2-params + p.parseTagSequence(); + p.parseLength(); + + // PBES2 KDF + p.parseTagSequence(); + p.parseLength(); + byte[] oidKDF = p.parseOIDAsBytes(); + if (!Arrays.equals(oidKDF, OID_PBKDF2)) { + throw new NoSuchAlgorithmException( + sm.getString("pemFile.notPbkdf2", toDottedOidString(oidKDF))); + } + + // PBES2 KDF-params + p.parseTagSequence(); + p.parseLength(); + byte[] salt = p.parseOctetString(); + int iterationCount = p.parseInt().intValue(); + if (p.peekTag() == Asn1Parser.TAG_INTEGER) { + keyLength = p.parseInt().intValue(); + } + + // PBKDF2 PRF + p.parseTagSequence(); + p.parseLength(); + String prf = null; + // This tag is optional. If present the nested sequence level will be 6 else if will be 4. + if (p.getNestedSequenceLevel() == 6) { + byte[] oidPRF = p.parseOIDAsBytes(); + prf = OID_TO_PRF.get(HexUtils.toHexString(oidPRF)); + if (prf == null) { + throw new NoSuchAlgorithmException(sm.getString("pemFile.unknownPrfAlgorithm", toDottedOidString(oidPRF))); + } + p.parseNull(); + + // Read the sequence tag for the PBES2 encryption scheme + p.parseTagSequence(); + p.parseLength(); + } else { + // Use the default + prf = DEFAULT_PRF; + } + + // PBES2 encryption scheme + byte[] oidCipher = p.parseOIDAsBytes(); + Algorithm algorithm = OID_TO_ALGORITHM.get(HexUtils.toHexString(oidCipher)); + if (algorithm == null) { + throw new NoSuchAlgorithmException( + sm.getString("pemFile.unknownEncryptionAlgorithm", toDottedOidString(oidCipher))); + } + + byte[] iv = p.parseOctetString(); + + // Encrypted data + byte[] encryptedData = p.parseOctetString(); + + // ASN.1 parsing complete + + // Build secret key to decrypt encrypted data + byte[] key = deriveKeyPBKDF2("PBKDF2With" + prf, password, salt, iterationCount, + algorithm.getKeyLength()); + SecretKey secretKey = new SecretKeySpec(key, algorithm.getSecretKeyAlgorithm()); + + // Configure algorithm to decrypt encrypted data + Cipher cipher = Cipher.getInstance(algorithm.getTransformation()); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); + + // Decrypt the encrypted key and call this method again to process the key + byte[] decryptedData = cipher.doFinal(encryptedData); + return toPrivateKey(keyAlgorithm, format, filename, decryptedData); + } + default: { + // Expected to be unencrypted + throw new NoSuchAlgorithmException(sm.getString("pemFile.unknownEncryptedFormat", format)); + } + } + } + + + private PrivateKey toPrivateKey(String keyAlgorithm, Format format, String filename, byte[] source) + throws GeneralSecurityException { + + KeySpec keySpec = null; + + switch (format) { + case PKCS1: { + keySpec = parsePKCS1(source); + break; + } + case PKCS8: { + keySpec = new PKCS8EncodedKeySpec(source); + break; + } + case RFC5915: { + keySpec = new PKCS8EncodedKeySpec(rfc5915ToPkcs8(source)); + break; + } + } + + InvalidKeyException exception = new InvalidKeyException(sm.getString("pemFile.parseError", filename)); + if (keyAlgorithm == null) { + for (String algorithm : new String[] { "RSA", "DSA", "EC" }) { + try { + return KeyFactory.getInstance(algorithm).generatePrivate(keySpec); + } catch (InvalidKeySpecException e) { + exception.addSuppressed(e); + } + } + } else { + try { + return KeyFactory.getInstance(keyAlgorithm).generatePrivate(keySpec); + } catch (InvalidKeySpecException e) { + exception.addSuppressed(e); + } + } + + throw exception; + } + + + private byte[] deriveKeyPBKDF1(int keyLength, String password, byte[] salt) throws NoSuchAlgorithmException { + if (password == null) { + throw new IllegalArgumentException(sm.getString("pemFile.noPassword")); + } + // PBKDF1-MD5 as specified by PKCS#5 + byte[] key = new byte[keyLength]; + + int insertPosition = 0; + + MessageDigest digest = MessageDigest.getInstance("MD5"); + byte[] pw = password.getBytes(StandardCharsets.UTF_8); + + while (insertPosition < keyLength) { + digest.update(pw); + digest.update(salt, 0, 8); + byte[] round = digest.digest(); + digest.update(round); + + System.arraycopy(round, 0, key, insertPosition, Math.min(keyLength - insertPosition, round.length)); + insertPosition += round.length; + } + + return key; + } + + + private byte[] deriveKeyPBKDF2(String algorithm, String password, byte[] salt, int iterations, int keyLength) + throws GeneralSecurityException { + if (password == null) { + throw new IllegalArgumentException(sm.getString("pemFile.noPassword")); + } + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm); + KeySpec keySpec; + keySpec = new PBEKeySpec(password.toCharArray(), salt, iterations, keyLength); + SecretKey secretKey = secretKeyFactory.generateSecret(keySpec); + return secretKey.getEncoded(); + } + + + //@formatter:off + /* + * RFC5915: SEQ + * INT value = 1 + * OCTET STRING len = 32 bytes + * [0] + * OID named EC + * [1] + * BIT STRING len = 520 bits + * + * PKCS8: SEQ + * INT value = 0 + * SEQ + * OID 1.2.840.10045.2.1 (EC public key) + * OID named EC + * OCTET STRING + * SEQ + * INT value = 1 + * OCTET STRING len = 32 bytes + * [1] + * BIT STRING len = 520 bits + * + */ + //@formatter:on + private byte[] rfc5915ToPkcs8(byte[] source) { + // Parse RFC 5915 format EC private key + Asn1Parser p = new Asn1Parser(source); + + // Type (sequence) + p.parseTag(0x30); + // Length + p.parseFullLength(); + + // Version + BigInteger version = p.parseInt(); + if (version.intValue() != 1) { + throw new IllegalArgumentException(sm.getString("pemFile.notValidRFC5915")); + } + + // Private key + p.parseTag(0x04); + int privateKeyLen = p.parseLength(); + byte[] privateKey = new byte[privateKeyLen]; + p.parseBytes(privateKey); + + // [0] OID + p.parseTag(0xA0); + int oidLen = p.parseLength(); + byte[] oid = new byte[oidLen]; + p.parseBytes(oid); + if (oid[0] != 0x06) { + throw new IllegalArgumentException(sm.getString("pemFile.notValidRFC5915")); + } + + // [1] Public key + p.parseTag(0xA1); + int publicKeyLen = p.parseLength(); + byte[] publicKey = new byte[publicKeyLen]; + p.parseBytes(publicKey); + if (publicKey[0] != 0x03) { + throw new IllegalArgumentException(sm.getString("pemFile.notValidRFC5915")); + } + + + // Write out PKCS#8 format + return Asn1Writer.writeSequence(Asn1Writer.writeInteger(0), + Asn1Writer.writeSequence(OID_EC_PUBLIC_KEY, oid), + Asn1Writer.writeOctetString(Asn1Writer.writeSequence(Asn1Writer.writeInteger(1), + Asn1Writer.writeOctetString(privateKey), Asn1Writer.writeTag((byte) 0xA1, publicKey)))); + } + + + private RSAPrivateCrtKeySpec parsePKCS1(byte[] source) { + Asn1Parser p = new Asn1Parser(source); + + // https://en.wikipedia.org/wiki/X.690#BER_encoding + // https://tools.ietf.org/html/rfc8017#page-55 + + // Type (sequence) + p.parseTag(0x30); + // Length + p.parseFullLength(); + + BigInteger version = p.parseInt(); + if (version.intValue() == 1) { + // JRE doesn't provide a suitable constructor for multi-prime + // keys + throw new IllegalArgumentException(sm.getString("pemFile.noMultiPrimes")); + } + return new RSAPrivateCrtKeySpec(p.parseInt(), p.parseInt(), p.parseInt(), p.parseInt(), p.parseInt(), + p.parseInt(), p.parseInt(), p.parseInt()); + } + + + private byte[] fromHex(String hexString) { + byte[] bytes = new byte[hexString.length() / 2]; + for (int i = 0; i < hexString.length(); i += 2) { + bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + + Character.digit(hexString.charAt(i + 1), 16)); + } + return bytes; + } + + + private String toDottedOidString(byte[] oidBytes) { + try { + Oid oid = new Oid(oidBytes); + return oid.toString(); + } catch (GSSException e) { + return HexUtils.toHexString(oidBytes); + } + } + } + + + private enum Format { + PKCS1, + PKCS8, + RFC5915 + } + + + private enum Algorithm { + AES128_CBC_PAD("AES/CBC/PKCS5PADDING", "AES", 128), + AES256_CBC_PAD("AES/CBC/PKCS5PADDING", "AES", 256), + DES_EDE3_CBC("DESede/CBC/PKCS5Padding", "DESede", 192); + + private final String transformation; + private final String secretKeyAlgorithm; + private final int keyLength; + + Algorithm(String transformation, String secretKeyAlgorithm, int keyLength) { + this.transformation = transformation; + this.secretKeyAlgorithm = secretKeyAlgorithm; + this.keyLength = keyLength; + } + + public String getTransformation() { + return transformation; + } + + public String getSecretKeyAlgorithm() { + return secretKeyAlgorithm; + } + + public int getKeyLength() { + return keyLength; + } + } +} diff --git a/java/org/apache/tomcat/util/net/mbeans-descriptors.xml b/java/org/apache/tomcat/util/net/mbeans-descriptors.xml new file mode 100644 index 0000000..38713f8 --- /dev/null +++ b/java/org/apache/tomcat/util/net/mbeans-descriptors.xml @@ -0,0 +1,473 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties new file mode 100644 index 0000000..7823c29 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties @@ -0,0 +1,67 @@ +# 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. + +engine.ciphersFailure=Failed getting cipher list +engine.emptyCipherSuite=Empty cipher suite +engine.engineClosed=Engine is closed +engine.failedCipherSuite=Failed to enable cipher suite [{0}] +engine.failedToReadAvailableBytes=There are plain text bytes available to read but no bytes were read +engine.inboundClose=Inbound closed before receiving peer's close_notify +engine.invalidBufferArray=offset: [{0}], length: [{1}] (expected: offset <= offset + length <= srcs.length [{2}]) +engine.invalidDestinationBuffersState=The state of the destination buffers changed concurrently while unwrapping bytes +engine.noRestrictSessionCreation=OpenSslEngine does not permit restricting the engine to only resuming existing sessions +engine.noSSLContext=No SSL context +engine.noSession=SSL session ID not available +engine.nullBuffer=Null buffer +engine.nullBufferInArray=Null buffer in array +engine.nullCipherSuite=Null cipher suite +engine.nullName=Null value name +engine.nullValue=Null value +engine.openSSLError=OpenSSL error: [{0}] message: [{1}] +engine.oversizedPacket=Encrypted packet is oversized +engine.unsupportedCipher=Unsupported cipher suite: [{0}] [{1}] +engine.unsupportedProtocol=Protocol [{0}] is not supported +engine.unverifiedPeer=Peer unverified +engine.writeToSSLFailed=Failed writing to SSL, returned: [{0}] + +openssl.X509FactoryError=Error getting X509 factory instance +openssl.addedClientCaCert=Added client CA cert: [{0}] +openssl.applyConf=Applying OpenSSLConfCmd to SSL context +openssl.certificateVerificationFailed=Certificate verification failed +openssl.checkConf=Checking OpenSSLConf +openssl.doubleInit=SSL context already initialized, ignoring +openssl.errApplyConf=Could not apply OpenSSLConf to SSL context +openssl.errCheckConf=Error during OpenSSLConf check +openssl.errMakeConf=Could not create OpenSSLConf context +openssl.errorSSLCtxInit=Error initializing SSL context +openssl.failSslContextMake=Unable to create SSLContext. Check that SSLEngine is enabled in the AprLifecycleListener, the AprLifecycleListener has initialised correctly and that a valid SSLProtocol has been specified +openssl.invalidSslProtocol=An invalid value [{0}] was provided for the SSLProtocol attribute +openssl.keyManagerMissing=No key manager found +openssl.keyManagerMissing.warn=No key manager found. TLS will work but the certificate will not be visible to Tomcat so management/monitoring features will not work for this certificate +openssl.makeConf=Creating OpenSSLConf context +openssl.nonJsseCertificate=The certificate [{0}] or its private key [{1}] could not be processed using a JSSE key manager and will be given directly to OpenSSL +openssl.nonJsseChain=The certificate chain [{0}] was not specified or was not valid and JSSE requires a valid certificate chain so attempting to use OpenSSL directly +openssl.trustManagerMissing=No trust manager found + +opensslconf.applyCommand=OpenSSLConf applying command (name [{0}], value [{1}]) +opensslconf.applyFailed=Failure while applying OpenSSLConf to SSL context +opensslconf.checkCommand=OpenSSLConf checking command (name [{0}], value [{1}]) +opensslconf.checkFailed=Failure while checking OpenSSLConf +opensslconf.failedCommand=OpenSSLConf failed command (name [{0}], value [{1}]) with result [{2}] - will be ignored +opensslconf.finishFailed=OpenSSLConf finish failed with result [{0}] +opensslconf.noCommandName=OpenSSLConf no command name - will be ignored (command value [{0}]) +opensslconf.resultCommand=OpenSSLConf command (name [{0}], value [{1}]) returned [{2}] + +sessionContext.nullTicketKeys=Null keys diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_cs.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_cs.properties new file mode 100644 index 0000000..6666eec --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_cs.properties @@ -0,0 +1,20 @@ +# 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. + +engine.emptyCipherSuite=Prázdná Å¡ifrovací Å™ada +engine.noSession=SSL session ID není dostupné +engine.openSSLError=Chyba OpenSSL: [{0}] zpráva: [{1}] + +opensslconf.checkFailed=Chyba pÅ™i kontrole OpenSSLConf diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_de.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_de.properties new file mode 100644 index 0000000..ceca395 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_de.properties @@ -0,0 +1,33 @@ +# 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. + +engine.ciphersFailure=Fehler beim Abfragen der Cipher Liste +engine.emptyCipherSuite=leere Cipher-Suite +engine.inboundClose=Die eingehende Verbindung wurde vor einer close_notify Nachricht der Gegenstelle geschlossen +engine.noSession=SSL Session-ID nicht vorhanden +engine.openSSLError=OpenSSL Fehler: [{0}] Nachricht: [{1}] +engine.unsupportedProtocol=Protokoll [{0}] ist nicht unterstützt + +openssl.addedClientCaCert=Client CA Zertifikat hinzugefügt: [{0}] +openssl.certificateVerificationFailed=Zertifikatsprüfung fehlgeschlagen +openssl.errApplyConf=Die OpenSSLConf konnte nicht auf den SSL Context angewandt werden +openssl.errCheckConf=Fehler beim Prüfen der OpenSSLConf +openssl.errMakeConf=Der OpenSSLConf Context konnte nicht erzeugt werden +openssl.errorSSLCtxInit=Fehler beim Initialisieren des SSL Contexts +openssl.keyManagerMissing=Kein Key-Manager gefunden +openssl.trustManagerMissing=Kein Trust-Manager gefunden + +opensslconf.applyFailed=Fehler bei der Anwendung der OpenSSLConf auf den SSL Context +opensslconf.checkFailed=Fehler beim Prüfen der OpenSSLConf diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_es.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_es.properties new file mode 100644 index 0000000..c8ea25f --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_es.properties @@ -0,0 +1,27 @@ +# 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. + +engine.emptyCipherSuite=Suite de cifrado vacía +engine.engineClosed=El notor esta cerrado +engine.noSession=El identificador de la sesión SSL no está disponible +engine.nullValue=Valor nulo +engine.openSSLError=Error de OpenSSL: [{0}] mensage: [{1}]\n +engine.writeToSSLFailed=Fallo al escribir hacia SSL, resultado: [{0}] + +openssl.addedClientCaCert=Ceritifcado CA de cliente adicionado: [{0}] +openssl.keyManagerMissing=No se encontró gerente de llave +openssl.trustManagerMissing=No se encontró un manejador confiable + +opensslconf.checkFailed=Fallo mientras se chequeaba OpenSSLConf\n diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_fr.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_fr.properties new file mode 100644 index 0000000..cf612d0 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_fr.properties @@ -0,0 +1,67 @@ +# 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. + +engine.ciphersFailure=Echec en essayant d'obtenir la liste des chiffres +engine.emptyCipherSuite=La suite de chiffrement (cipher suite) est vide +engine.engineClosed=Le moteur a déjà été fermé +engine.failedCipherSuite=Impossible d''activer la suite de chiffres [{0}] +engine.failedToReadAvailableBytes=Il y a des octets en clair disponibles à lire mais aucun octet n'a été lu +engine.inboundClose=L'entrée a été fermée avant d'avoir reçu le close_notify du pair +engine.invalidBufferArray=offset : [{0}], length : [{1}] (attendu : offset <= offset + length <= srcs.length [{2}]) +engine.invalidDestinationBuffersState=L'état des buffers de destination a changé de manière concurrente lors de l'unwrap des octets +engine.noRestrictSessionCreation=OpenSslEngine ne permet pas de restreindre le moteur à la récupération des sessions existantes +engine.noSSLContext=Pas de contexte SSL +engine.noSession=ID de session SSL non disponible +engine.nullBuffer=Tampon null +engine.nullBufferInArray=Tampon null dans le tableau +engine.nullCipherSuite=Suite de chiffres nulle +engine.nullName=La valeur du nom est null +engine.nullValue=La valeur est null +engine.openSSLError=Erreur OpenSSL : [{0}] message : [{1}] +engine.oversizedPacket=Le paquet crypté est trop gros +engine.unsupportedCipher=Suite de chiffres non supportée : [{0}] [{1}] +engine.unsupportedProtocol=Le protocole [{0}] n''est pas supporté +engine.unverifiedPeer=Le pair n'est pas vérifié +engine.writeToSSLFailed=Echec d''écriture vers SSL, code de retour : [{0}] + +openssl.X509FactoryError=Impossible d'obtenir l'instance de la fabrique X509 +openssl.addedClientCaCert=Ajout du certificat CA du client : [{0}] +openssl.applyConf=Application de OpenSSLConfCmd au contexte SSL +openssl.certificateVerificationFailed=La vérification du certificat a échoué +openssl.checkConf=Vérification de OpenSSLConf en cours +openssl.doubleInit=Le contexte SSL a déjà été initialisé, ignoré +openssl.errApplyConf=Impossible d'appliquer la OpenSSLConf au contexte SSL +openssl.errCheckConf=Erreur pendant la vérification de OpenSSLConf +openssl.errMakeConf=Impossible de créer le contexte de OpenSSLConf +openssl.errorSSLCtxInit=Erreur d'initialisation du contexte SSL +openssl.failSslContextMake=Impossible de créer le SSLContext. Vérfiez que le SSLEngine est activé dans l'AprLifecycleListener, que celui ci a été initialisé correctement et qu'un SSLProtocol valide a été spécifié. +openssl.invalidSslProtocol=La valeur invalide [{0}] a été fournie pour l''attribut SSLProtocol +openssl.keyManagerMissing=Aucun gestionnaire de clés trouvé +openssl.keyManagerMissing.warn=Aucun gestionnaire de clés trouvé. TLS fonctionnera mais les certificats ne seront pas exposés dans Tomcat donc les fonctionnalités de gestion ne seront pas disponibles pour ce certificat. +openssl.makeConf=Création du contexte de OpenSSLConf +openssl.nonJsseCertificate=Le certificat [{0}] ou sa clé privée [{1}] n''a pas pu être traité en utilisant un gestionnaire de clé de JSSE, et sera directement passée à OpenSSL +openssl.nonJsseChain=La chaîne de certificat [{0}] n''a pas été spécifiée ou est invalide et JSSE requiert une chaîne de certificats valide, donc OpenSSL sera utilisé directement +openssl.trustManagerMissing=Gestionnaire de confiance non trouvé + +opensslconf.applyCommand=Application de la commande OpenSSLConf (nom [{0}] valeur [{1}]) +opensslconf.applyFailed=Erreur en appliquant OpenSSLConf au contexte SSL +opensslconf.checkCommand=Vérification de la commande OpenSSLConf (nom [{0}] valeur [{1}]) +opensslconf.checkFailed=Echec de la vérification de OpenSSLConf +opensslconf.failedCommand=La commande OpenSSLConf (nom [{0}] valeur [{1}]) a échoué avec le résultat [{2}] qui sera ignoré +opensslconf.finishFailed=OpenSSLConf s''est terminé en échec avec le résultat [{0}] +opensslconf.noCommandName=Pas de nom de commande OpenSSLConf (valeur [{0}]), cela sera ignoré +opensslconf.resultCommand=La commande OpenSSLConf (nom [{0}] valeur [{1}]) a retourné [{2}] + +sessionContext.nullTicketKeys=Clés nulles diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_ja.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_ja.properties new file mode 100644 index 0000000..f0c0168 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_ja.properties @@ -0,0 +1,67 @@ +# 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. + +engine.ciphersFailure=æš—å·ãƒªã‚¹ãƒˆã‚’å–å¾—ã§ãã¾ã›ã‚“。 +engine.emptyCipherSuite=æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆãŒã‚ã‚Šã¾ã›ã‚“ +engine.engineClosed=エンジンãŒé–‰ã˜ã‚‰ã‚Œã¦ã„ã¾ã™ +engine.failedCipherSuite=æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆ [{0}] を有効ã«ã§ãã¾ã›ã‚“ã§ã—㟠+engine.failedToReadAvailableBytes=読ã¿å–ã‚‹ã“ã¨ãŒã§ãるプレーンテキストãƒã‚¤ãƒˆãŒã‚ã‚Šã¾ã™ãŒã€èª­ã¿å–られãŸãƒã‚¤ãƒˆã¯ã‚ã‚Šã¾ã›ã‚“ +engine.inboundClose=ピアã®close_notifyã‚’å—ä¿¡ã™ã‚‹å‰ã®ã‚¤ãƒ³ãƒã‚¦ãƒ³ãƒ‰ã‚¯ãƒ­ãƒ¼ã‚º +engine.invalidBufferArray=オフセット: [{0}], é•·ã•: [{1}] (期待値: offset <= offset + length <= srcs.length [{2}]) +engine.invalidDestinationBuffersState=ãƒã‚¤ãƒˆã®ã‚¢ãƒ³ãƒ©ãƒƒãƒ—中ã«å®›å…ˆãƒãƒƒãƒ•ã‚¡ãƒ¼ã®çŠ¶æ…‹ãŒåŒæ™‚ã«å¤‰æ›´ã•ã‚Œã¾ã—㟠+engine.noRestrictSessionCreation=OpenSslEngine ã§ã¯æ—¢å­˜ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®ãƒ¬ã‚¸ãƒ¥ãƒ¼ãƒ ã®ã¿ã«ã‚¨ãƒ³ã‚¸ãƒ³ã‚’制é™ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +engine.noSSLContext=SSLコンテキストãŒã‚ã‚Šã¾ã›ã‚“ +engine.noSession=SSL セッション ID ãŒåˆ©ç”¨å¯èƒ½ã§ã¯ã‚ã‚Šã¾ã›ã‚“ +engine.nullBuffer=Null ãƒãƒƒãƒ•ã‚¡ +engine.nullBufferInArray=é…列内ã®Null ãƒãƒƒãƒ•ã‚¡ +engine.nullCipherSuite=Null æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆ +engine.nullName=Null値å +engine.nullValue=Null値 +engine.openSSLError=OpenSSLエラー:[{0}] メッセージ:[{1}] +engine.oversizedPacket=æš—å·åŒ–パケットã®ã‚µã‚¤ã‚ºãŒè¶…éŽã—ã¦ã„ã¾ã™ã€‚ +engine.unsupportedCipher=サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆï¼š[{0}] [{1}] +engine.unsupportedProtocol=プロトコル [{0}] ã«ã¯å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“。 +engine.unverifiedPeer=未確èªã®ãƒ”ã‚¢ +engine.writeToSSLFailed=SSLã¸ã®æ›¸ãè¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸã€‚è¿”å´å€¤ï¼š[{0}] + +openssl.X509FactoryError=X509ファクトリインスタンスã®å–得エラー +openssl.addedClientCaCert=クライアント CA 証明書を登録ã—ã¾ã—ãŸ: [{0}] +openssl.applyConf=OpenSSLConfCmdã‚’SSLコンテキストã«é©ç”¨ã—ã¾ã™ã€‚ +openssl.certificateVerificationFailed=証明書確èªã«å¤±æ•—ã—ã¾ã—㟠+openssl.checkConf=OpenSSLConfã®ç¢ºèª +openssl.doubleInit=SSLコンテキストãŒæ—¢ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã™ã€‚無視ã—ã¾ã™ã€‚ +openssl.errApplyConf=OpenSSLConfã‚’SSLコンテキストã«é©ç”¨ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +openssl.errCheckConf=OpenSSLConfãƒã‚§ãƒƒã‚¯ä¸­ã®ã‚¨ãƒ©ãƒ¼ +openssl.errMakeConf=OpenSSLConfコンテキストを作æˆã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +openssl.errorSSLCtxInit=SSL コンテキストåˆæœŸåŒ–中ã®ã‚¨ãƒ©ãƒ¼ +openssl.failSslContextMake=SSLContextを作æˆã§ãã¾ã›ã‚“。 SSLEngine ㌠AprLifecycleListener ã§æœ‰åŠ¹ã«ãªã£ã¦ã„ã‚‹ã“ã¨ã€AprLifecycleListener ãŒæ­£ã—ãåˆæœŸåŒ–ã•ã‚Œã¦ã„ã‚‹ã“ã¨ã€ãŠã‚ˆã³æœ‰åŠ¹ãª SSLProtocol ãŒæŒ‡å®šã•ã‚Œã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„ +openssl.invalidSslProtocol=無効ãªå€¤ [{0}] ㌠SSLProtocol 属性ã«æŒ‡å®šã•ã‚Œã¾ã—㟠+openssl.keyManagerMissing=キーマãƒãƒ¼ã‚¸ãƒ£ãƒ¼ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。 +openssl.keyManagerMissing.warn=キーマãƒãƒ¼ã‚¸ãƒ£ãƒ¼ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ TLS ã¯æ©Ÿèƒ½ã—ã¾ã™ãŒã€è¨¼æ˜Žæ›¸ã¯ Tomcat ã‹ã‚‰ã¯èªè­˜ã•ã‚Œãªã„ãŸã‚ã€ã“ã®è¨¼æ˜Žæ›¸ã«å¯¾ã—ã¦ç®¡ç†/監視機能ã¯æ©Ÿèƒ½ã—ã¾ã›ã‚“ +openssl.makeConf=OpenSSLConfコンテキストã®ä½œæˆ +openssl.nonJsseCertificate=証明書 [{0}] ã¾ãŸã¯ãã®ç§˜å¯†éµ [{1}] ã¯ã€JSSE キーマãƒãƒ¼ã‚¸ãƒ£ã‚’使用ã—ã¦å‡¦ç†ã§ããªã‹ã£ãŸãŸã‚ã€OpenSSL ã«ç›´æŽ¥æ¸¡ã•ã‚Œã¾ã™ +openssl.nonJsseChain=証明書ãƒã‚§ãƒ¼ãƒ³ [{0}] ãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ã‹ç„¡åŠ¹ã§ã‚ã‚Šã€JSSE ã«ã¯æœ‰åŠ¹ãªè¨¼æ˜Žæ›¸ãƒã‚§ãƒ¼ãƒ³ãŒå¿…è¦ã§ã‚ã‚‹ãŸã‚ã€OpenSSL を直接使用ã—よã†ã¨ã—ã¦ã„ã¾ã™ +openssl.trustManagerMissing=トラストマãƒãƒ¼ã‚¸ãƒ£ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ + +opensslconf.applyCommand=OpenSSLConfã¯ã‚³ãƒžãƒ³ãƒ‰ï¼ˆåå‰[{0}]ã€å€¤[{1}])をé©ç”¨ã—ã¦ã„ã¾ã™ã€‚ +opensslconf.applyFailed=OpenSSLConfã‚’SSLコンテキストã«é©ç”¨ã™ã‚‹éš›ã®å¤±æ•— +opensslconf.checkCommand=OpenSSLConfãƒã‚§ãƒƒã‚¯ã‚³ãƒžãƒ³ãƒ‰ï¼ˆåå‰[{0}]ã€å€¤[{1}]) +opensslconf.checkFailed=OpenSSLConf ã®ãƒã‚§ãƒƒã‚¯ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ +opensslconf.failedCommand=çµæžœ[{2}]ã§OpenSSLConfãŒã‚³ãƒžãƒ³ãƒ‰ï¼ˆåå‰[{0}]ã€å€¤[{1}])ã«å¤±æ•—ã—ã¾ã—ãŸã€‚無視ã•ã‚Œã¾ã™ã€‚ +opensslconf.finishFailed=çµæžœ[{0}]ã§OpenSSLConfã®finish処ç†ãŒå¤±æ•—ã—ã¾ã—㟠+opensslconf.noCommandName=OpenSSLConfコマンドåãªã— - 無視ã•ã‚Œã¾ã™ï¼ˆã‚³ãƒžãƒ³ãƒ‰å€¤[{0}]) +opensslconf.resultCommand=OpenSSLConfコマンド (åå‰ [{0}]ã€å€¤ [{1}]) ㌠[{2}] ã‚’è¿”ã—ã¾ã—㟠+ +sessionContext.nullTicketKeys=Null キー diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_ko.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_ko.properties new file mode 100644 index 0000000..044990f --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_ko.properties @@ -0,0 +1,64 @@ +# 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. + +engine.ciphersFailure=Cipherë“¤ì˜ ëª©ë¡ì„ 얻지 못했습니다. +engine.emptyCipherSuite=CipherSuite ì´ë¦„ì´ ì¡´ìž¬í•˜ì§€ 않습니다. +engine.engineClosed=ì—”ì§„ì´ ë‹«í˜€ 있습니다. +engine.failedCipherSuite=Cipher suite [{0}]ì„(를) 사용가능 ìƒíƒœë¡œ 설정하지 못했습니다. +engine.failedToReadAvailableBytes=ì½ì–´ì•¼ í•  plain í…스트 ë°”ì´íŠ¸ë“¤ì´ 남아 있ìŒì—ë„, 아무 ê²ƒë„ ì½ì§€ 못했습니다. +engine.inboundClose=Peerì˜ close_notify를 받기 ì „ì—, Inboundê°€ 닫혔습니다. +engine.invalidBufferArray=offset: [{0}], 길ì´: [{1}] (요구사항: offset <= offset + length <= srcs.length [{2}]) +engine.invalidDestinationBuffersState=ë°”ì´íŠ¸ ë°ì´í„°ë¥¼ unwrap하는 중, ëŒ€ìƒ ë²„í¼ì˜ ìƒíƒœê°€ ë™ì‹œì— 변경ë˜ì—ˆìŠµë‹ˆë‹¤. +engine.noRestrictSessionCreation=OpenSslEngineì€, 새로운 SSL ì„¸ì…˜ì„ ìƒì„±í•  수 없고 ì˜¤ì§ ê¸°ì¡´ SSL ì„¸ì…˜ë“¤ì„ ìž¬ê°œë§Œ í•  수 있게 하는 ì˜µì…˜ì„ ì§€ì›í•˜ì§€ 않습니다. +engine.noSSLContext=SSL 컨í…스트가 ì—†ìŒ. +engine.noSession=SSL 세션 IDê°€ 존재하지 않습니다. +engine.nullBuffer=ë„ ë²„í¼ +engine.nullBufferInArray=ë°°ì—´ ë‚´ì— ë„ ë²„í¼ìž„ +engine.nullCipherSuite=ë„ cipher suite +engine.nullName=nameì´ ë„입니다. +engine.nullValue=ë„ ê°’ +engine.openSSLError=OpenSSL 오류: [{0}], 메시지: [{1}] +engine.oversizedPacket=ì•”í˜¸í™”ëœ íŒ¨í‚·ì´ ë„ˆë¬´ í½ë‹ˆë‹¤. +engine.unsupportedCipher=지ì›ë˜ì§€ 않는 cipher suite: [{0}] [{1}] +engine.unsupportedProtocol=프로토콜 [{0}]ì€(는) 지ì›ë˜ì§€ 않습니다. +engine.unverifiedPeer=ê²€ì¦ë˜ì§€ ì•Šì€ Peer +engine.writeToSSLFailed=SSLì— ì“°ê¸° 실패, 반환 ê°’: [{0}] + +openssl.X509FactoryError=X509 팩토리 ì¸ìŠ¤í„´ìŠ¤ë¥¼ 얻는 중 오류 ë°œìƒ +openssl.addedClientCaCert=í´ë¼ì´ì–¸íŠ¸ CA ì¸ì¦ì„œë¥¼ 추가했습니다: [{0}] +openssl.applyConf=OpenSSLConfCmd를 SSL 컨í…ìŠ¤íŠ¸ì— ì ìš©í•©ë‹ˆë‹¤. +openssl.certificateVerificationFailed=ì¸ì¦ì„œ ê²€ì¦ì— 실패했습니다. +openssl.checkConf=OpenSSLConf를 ì ê²€í•©ë‹ˆë‹¤. +openssl.doubleInit=SSL 컨í…스트가 ì´ë¯¸ 초기화ë˜ì–´ 있으므로, 초기화 í˜¸ì¶œì„ ë¬´ì‹œí•©ë‹ˆë‹¤. +openssl.errApplyConf=SSL 컨í…ìŠ¤íŠ¸ì— OpenSSLConf를 ì ìš©í•  수 없었습니다. +openssl.errCheckConf=OpenSSLConf ì ê²€ 중 오류 ë°œìƒ +openssl.errMakeConf=OpenSSLConf 컨í…스트를 ìƒì„±í•  수 없었습니다. +openssl.errorSSLCtxInit=SSL 컨í…스트를 초기화 하는 중 오류 ë°œìƒ +openssl.keyManagerMissing=키 매니저를 ì°¾ì„ ìˆ˜ 없습니다. +openssl.makeConf=OpenSSLConf 컨í…스트를 ìƒì„±í•©ë‹ˆë‹¤. +openssl.nonJsseCertificate=ì¸ì¦ì„œ [{0}] ë˜ëŠ” ê·¸ê²ƒì˜ ê°œì¸ í‚¤ [{1}]ì´(ê°€) JSSE 키 매니저를 사용하여 처리ë˜ì§€ 못하였으므로, OpenSSLì— ì§ì ‘ 전달할 것입니다. +openssl.nonJsseChain=해당 ì¸ì¦ì„œ ì²´ì¸ [{0}]ì´(ê°€) 지정ë˜ì§€ 않았거나 유효하지 않으며, JSSE는 유효한 ì¸ì¦ì„œ ì²´ì¸ì„ 요구하므로, OpenSSLì„ ì§ì ‘ 사용하려 ì‹œë„합니다. +openssl.trustManagerMissing=Trust 매니저를 ì°¾ì„ ìˆ˜ 없습니다. + +opensslconf.applyCommand=OpenSSLConfì´ ëª…ë ¹ì„ ì ìš©í•©ë‹ˆë‹¤ (ì´ë¦„ [{0}], ê°’ [{1}]). +opensslconf.applyFailed=OpenSSLConf를 SSL 컨í…ìŠ¤íŠ¸ì— ì ìš©í•˜ëŠ” 중 실패 +opensslconf.checkCommand=OpenSSLConf ì ê²€ 명령 (ì´ë¦„ [{0}], ê°’ [{1}]) +opensslconf.checkFailed=OpenSSLConf ì ê²€ 실패 +opensslconf.failedCommand=OpenSSLConfê°€ 명령(ì´ë¦„: [{0}], ê°’: [{1}])ì„ ì²˜ë¦¬í•˜ì§€ 못했습니다 (ê²°ê³¼: [{2}]). ì´ëŠ” ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. +opensslconf.finishFailed=OpenSSLConfì˜ ì™„ë£Œê°€ 실패했습니다 (ê²°ê³¼ ê°’: [{0}]). +opensslconf.noCommandName=OpenSSLConf: 명령 ì´ë¦„ì´ ì—†ìŠµë‹ˆë‹¤ - ë¬´ì‹œë  ê²ƒìž…ë‹ˆë‹¤. (명령 ê°’ [{0}]) +opensslconf.resultCommand=OpenSSLConf 명령(ì´ë¦„: [{0}], ê°’: [{1}])ì´ [{2}]ì„(를) 반환했습니다. + +sessionContext.nullTicketKeys=ë„ í‚¤ë“¤ diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..83356a7 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_pt_BR.properties @@ -0,0 +1,17 @@ +# 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. + +engine.noSession=ID da sessão SSL indisponível +engine.openSSLError=Erro OpenSSL: [{0}] mensagem: [{1}] diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_ru.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_ru.properties new file mode 100644 index 0000000..e527b0e --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_ru.properties @@ -0,0 +1,20 @@ +# 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. + +engine.emptyCipherSuite=ПуÑтой набор шифров +engine.noSession=Идентификатор SSL ÑеÑÑии недоÑтупен +engine.openSSLError=Ошибка OpenSSL: [{0]}] Ñообщение: [{1}] + +opensslconf.checkFailed=Ошибка при проверке OpenSSLConf diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..44f6798 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings_zh_CN.properties @@ -0,0 +1,64 @@ +# 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. + +engine.ciphersFailure=获å–密ç åˆ—表失败 +engine.emptyCipherSuite=空密ç å¥—件 +engine.engineClosed=引擎已ç»å…³é—­ +engine.failedCipherSuite=无法å¯ç”¨å¯†ç å¥—件[{0}] +engine.failedToReadAvailableBytes=有å¯è¯»å–的纯文本字节,但未读å–任何字节 +engine.inboundClose=入站在收到对等方的关闭通知之å‰å…³é—­ +engine.invalidBufferArray=å移é‡ï¼š[{0}],长度:[{1}](应为:å移é‡<=å移é‡+长度<=srcs.length[{2}]) +engine.invalidDestinationBuffersState=在打开字节包装时,目标缓冲区的状æ€å‘生了更改 +engine.noRestrictSessionCreation=OpenSslEngineä¸å…许é™åˆ¶å¼•æ“Žä»…仅去唤醒已ç»å­˜åœ¨çš„ä¼šè¯ +engine.noSSLContext=没有SSL上下文 +engine.noSession=SSL会è¯IDä¸å¯ç”¨ +engine.nullBuffer=空缓冲区 +engine.nullBufferInArray=数组中的空缓冲区 +engine.nullCipherSuite=无加密套件 +engine.nullName=空值å称 +engine.nullValue=空值 +engine.openSSLError=OpenSSL 错误:[{0}] ä¿¡æ¯: [{1}] +engine.oversizedPacket=加密包过大 +engine.unsupportedCipher=ä¸æ”¯æŒçš„密ç å¥—件:[{0}][{1}]。 +engine.unsupportedProtocol=ä¸æ”¯æŒåè®® [{0}] +engine.unverifiedPeer=未ç»æ ¸å®žçš„åŒè¡Œ +engine.writeToSSLFailed=写入SSL失败,返回:[{0}] + +openssl.X509FactoryError=获å–X509工厂实例时出错 +openssl.addedClientCaCert=添加了客户端 CA è¯ä¹¦ï¼š[{0}] +openssl.applyConf=å°†OpenSSLConfCmd应用于SSL上下文 +openssl.certificateVerificationFailed=è¯ä¹¦éªŒè¯å¤±è´¥ +openssl.checkConf=检查OpenSSLConf +openssl.doubleInit=SSL环境已ç»åˆå§‹åŒ–,忽略 +openssl.errApplyConf=无法将OpenSSLConf 应用于SSL 上下文 +openssl.errCheckConf=OpenSSLConf检查期间出错。 +openssl.errMakeConf=无法创建OpenSSLConf上下文 +openssl.errorSSLCtxInit=åˆå§‹åŒ–SSL上下文时出错 +openssl.keyManagerMissing=key管ç†å™¨æœªæ‰¾åˆ° +openssl.makeConf=创建OpenSSLConf上下文 +openssl.nonJsseCertificate=无法使用JSSE密钥管ç†å™¨å¤„ç†è¯ä¹¦[{0}]或其ç§é’¥[{1}],该è¯ä¹¦å°†ç›´æŽ¥æ供给OpenSSL +openssl.nonJsseChain=è¯ä¹¦é“¾[{0}]未指定或无效,JSSE需è¦æœ‰æ•ˆçš„è¯ä¹¦é“¾ï¼Œå› æ­¤å°è¯•ç›´æŽ¥ä½¿ç”¨OpenSSL +openssl.trustManagerMissing=没有找到.信任管ç†è€… + +opensslconf.applyCommand=OpenSSLConf正在应用命令(name[{0}],value[{1}]) +opensslconf.applyFailed=å°†OpenSSLConf应用于SSL上下文时失败 +opensslconf.checkCommand=OpenSSLConf检查命令(name[{0}],value[{1}]) +opensslconf.checkFailed=检查OpenSSLConf时失败。 +opensslconf.failedCommand=OpenSSLConf失败的命令(å称为[{0]},值为[{1}]),结果为[{2}]-将被忽略 +opensslconf.finishFailed=OpenSSLConf é…置失败结果为 [{0}] +opensslconf.noCommandName=OpenSSLConf没有命令å-将被忽略(命令值[{0}]) +opensslconf.resultCommand=OpenSSLConf命令(name[{0}],value[{1}])返回了[{2}] + +sessionContext.nullTicketKeys=Null keys diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLConf.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLConf.java new file mode 100644 index 0000000..f234e03 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLConf.java @@ -0,0 +1,37 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class OpenSSLConf implements Serializable { + + private static final long serialVersionUID = 1L; + + private final List commands = new ArrayList<>(); + + public void addCmd(OpenSSLConfCmd cmd) { + commands.add(cmd); + } + + public List getCommands() { + return commands; + } + +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java new file mode 100644 index 0000000..3706009 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java @@ -0,0 +1,43 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.io.Serializable; + +public class OpenSSLConfCmd implements Serializable { + + private static final long serialVersionUID = 1L; + + private String name = null; + private String value = null; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java new file mode 100644 index 0000000..f1d7b09 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java @@ -0,0 +1,662 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.lang.ref.Cleaner; +import java.lang.ref.Cleaner.Cleanable; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Iterator; +import java.util.List; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jni.CertificateVerifier; +import org.apache.tomcat.jni.Pool; +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.jni.SSLConf; +import org.apache.tomcat.jni.SSLContext; +import org.apache.tomcat.util.net.Constants; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.SSLUtilBase; +import org.apache.tomcat.util.res.StringManager; + +public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { + + private static final Log log = LogFactory.getLog(OpenSSLContext.class); + private static final StringManager sm = StringManager.getManager(OpenSSLContext.class); + + private static final String defaultProtocol = "TLS"; + + private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY-----\n"; + private static final Object END_KEY = "\n-----END PRIVATE KEY-----"; + + static final CertificateFactory X509_CERT_FACTORY; + static { + try { + X509_CERT_FACTORY = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new IllegalStateException(sm.getString("openssl.X509FactoryError"), e); + } + } + + private static final Cleaner cleaner = Cleaner.create(); + + private final SSLHostConfig sslHostConfig; + private final SSLHostConfigCertificate certificate; + private final List negotiableProtocols; + + private OpenSSLSessionContext sessionContext; + private X509TrustManager x509TrustManager; + private String enabledProtocol; + private boolean initialized = false; + + private final OpenSSLState state; + private final Cleanable cleanable; + + public OpenSSLContext(SSLHostConfigCertificate certificate, List negotiableProtocols) + throws SSLException { + this.sslHostConfig = certificate.getSSLHostConfig(); + this.certificate = certificate; + long aprPool = Pool.create(0); + long cctx = 0; + long ctx = 0; + boolean success = false; + try { + // Create OpenSSLConfCmd context if used + OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); + if (openSslConf != null) { + try { + if (log.isTraceEnabled()) { + log.trace(sm.getString("openssl.makeConf")); + } + cctx = SSLConf.make(aprPool, + SSL.SSL_CONF_FLAG_FILE | + SSL.SSL_CONF_FLAG_SERVER | + SSL.SSL_CONF_FLAG_CERTIFICATE | + SSL.SSL_CONF_FLAG_SHOW_ERRORS); + } catch (Exception e) { + throw new SSLException(sm.getString("openssl.errMakeConf"), e); + } + } + sslHostConfig.setOpenSslConfContext(Long.valueOf(cctx)); + + // SSL protocol + int value = SSL.SSL_PROTOCOL_NONE; + for (String protocol : sslHostConfig.getEnabledProtocols()) { + if (Constants.SSL_PROTO_SSLv2Hello.equalsIgnoreCase(protocol)) { + // NO-OP. OpenSSL always supports SSLv2Hello + } else if (Constants.SSL_PROTO_SSLv2.equalsIgnoreCase(protocol)) { + value |= SSL.SSL_PROTOCOL_SSLV2; + } else if (Constants.SSL_PROTO_SSLv3.equalsIgnoreCase(protocol)) { + value |= SSL.SSL_PROTOCOL_SSLV3; + } else if (Constants.SSL_PROTO_TLSv1.equalsIgnoreCase(protocol)) { + value |= SSL.SSL_PROTOCOL_TLSV1; + } else if (Constants.SSL_PROTO_TLSv1_1.equalsIgnoreCase(protocol)) { + value |= SSL.SSL_PROTOCOL_TLSV1_1; + } else if (Constants.SSL_PROTO_TLSv1_2.equalsIgnoreCase(protocol)) { + value |= SSL.SSL_PROTOCOL_TLSV1_2; + } else if (Constants.SSL_PROTO_TLSv1_3.equalsIgnoreCase(protocol)) { + value |= SSL.SSL_PROTOCOL_TLSV1_3; + } else if (Constants.SSL_PROTO_ALL.equalsIgnoreCase(protocol)) { + value |= SSL.SSL_PROTOCOL_ALL; + } else { + // Should not happen since filtering to build + // enabled protocols removes invalid values. + throw new Exception(sm.getString("openssl.invalidSslProtocol", protocol)); + } + } + + // Create SSL Context + try { + ctx = SSLContext.make(aprPool, value, SSL.SSL_MODE_SERVER); + } catch (Exception e) { + // If the sslEngine is disabled on the AprLifecycleListener + // there will be an Exception here but there is no way to check + // the AprLifecycleListener settings from here + throw new Exception(sm.getString("openssl.failSslContextMake"), e); + } + + this.negotiableProtocols = negotiableProtocols; + + success = true; + } catch(Exception e) { + throw new SSLException(sm.getString("openssl.errorSSLCtxInit"), e); + } finally { + state = new OpenSSLState(aprPool, cctx, ctx); + /* + * When an SSLHostConfig is replaced at runtime, it is not possible to + * call destroy() on the associated OpenSSLContext since it is likely + * that there will be in-progress connections using the OpenSSLContext. + * A reference chain has been deliberately established (see + * OpenSSLSessionContext) to ensure that the OpenSSLContext remains + * ineligible for GC while those connections are alive. Once those + * connections complete, the OpenSSLContext will become eligible for GC + * and this method will ensure that the associated native resources are + * cleaned up. + */ + cleanable = cleaner.register(this, state); + + if (!success) { + destroy(); + } + } + } + + + public String getEnabledProtocol() { + return enabledProtocol; + } + + + public void setEnabledProtocol(String protocol) { + enabledProtocol = (protocol == null) ? defaultProtocol : protocol; + } + + + @Override + public void destroy() { + cleanable.clean(); + } + + + protected static boolean checkConf(OpenSSLConf conf, long cctx) throws Exception { + boolean result = true; + OpenSSLConfCmd cmd; + String name; + String value; + int rc; + for (OpenSSLConfCmd command : conf.getCommands()) { + cmd = command; + name = cmd.getName(); + value = cmd.getValue(); + if (name == null) { + log.error(sm.getString("opensslconf.noCommandName", value)); + result = false; + continue; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("opensslconf.checkCommand", name, value)); + } + try { + rc = SSLConf.check(cctx, name, value); + } catch (Exception e) { + log.error(sm.getString("opensslconf.checkFailed")); + return false; + } + if (rc <= 0) { + log.error(sm.getString("opensslconf.failedCommand", name, value, + Integer.toString(rc))); + result = false; + } else if (log.isTraceEnabled()) { + log.trace(sm.getString("opensslconf.resultCommand", name, value, + Integer.toString(rc))); + } + } + if (!result) { + log.error(sm.getString("opensslconf.checkFailed")); + } + return result; + } + + protected static boolean applyConf(OpenSSLConf conf, long cctx, long ctx) throws Exception { + boolean result = true; + SSLConf.assign(cctx, ctx); + OpenSSLConfCmd cmd; + String name; + String value; + int rc; + for (OpenSSLConfCmd command : conf.getCommands()) { + cmd = command; + name = cmd.getName(); + value = cmd.getValue(); + if (name == null) { + log.error(sm.getString("opensslconf.noCommandName", value)); + result = false; + continue; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("opensslconf.applyCommand", name, value)); + } + try { + rc = SSLConf.apply(cctx, name, value); + } catch (Exception e) { + log.error(sm.getString("opensslconf.applyFailed")); + return false; + } + if (rc <= 0) { + log.error(sm.getString("opensslconf.failedCommand", name, value, + Integer.toString(rc))); + result = false; + } else if (log.isTraceEnabled()) { + log.trace(sm.getString("opensslconf.resultCommand", name, value, + Integer.toString(rc))); + } + } + rc = SSLConf.finish(cctx); + if (rc <= 0) { + log.error(sm.getString("opensslconf.finishFailed", Integer.toString(rc))); + result = false; + } + if (!result) { + log.error(sm.getString("opensslconf.applyFailed")); + } + return result; + } + + /** + * Setup the SSL_CTX. + * + * @param kms Must contain a KeyManager of the type + * {@code OpenSSLKeyManager} + * @param tms Must contain a TrustManager of the type + * {@code X509TrustManager} + * @param sr Is not used for this implementation. + */ + @Override + public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) { + if (initialized) { + log.warn(sm.getString("openssl.doubleInit")); + return; + } + try { + if (sslHostConfig.getInsecureRenegotiation()) { + SSLContext.setOptions(state.ctx, SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); + } else { + SSLContext.clearOptions(state.ctx, SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); + } + + // Use server's preference order for ciphers (rather than + // client's) + if (sslHostConfig.getHonorCipherOrder()) { + SSLContext.setOptions(state.ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); + } else { + SSLContext.clearOptions(state.ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); + } + + // Disable compression if requested + if (sslHostConfig.getDisableCompression()) { + SSLContext.setOptions(state.ctx, SSL.SSL_OP_NO_COMPRESSION); + } else { + SSLContext.clearOptions(state.ctx, SSL.SSL_OP_NO_COMPRESSION); + } + + // Disable TLS Session Tickets (RFC4507) to protect perfect forward secrecy + if (sslHostConfig.getDisableSessionTickets()) { + SSLContext.setOptions(state.ctx, SSL.SSL_OP_NO_TICKET); + } else { + SSLContext.clearOptions(state.ctx, SSL.SSL_OP_NO_TICKET); + } + + // List the ciphers that the client is permitted to negotiate + SSLContext.setCipherSuite(state.ctx, sslHostConfig.getCiphers()); + + // If there is no certificate file must be using a KeyStore so a KeyManager is required. + // If there is a certificate file a KeyManager is helpful but not strictly necessary. + certificate.setCertificateKeyManager( + OpenSSLUtil.chooseKeyManager(kms, certificate.getCertificateFile() == null)); + + addCertificate(certificate); + + // Client certificate verification + int value = 0; + switch (sslHostConfig.getCertificateVerification()) { + case NONE: + value = SSL.SSL_CVERIFY_NONE; + break; + case OPTIONAL: + value = SSL.SSL_CVERIFY_OPTIONAL; + break; + case OPTIONAL_NO_CA: + value = SSL.SSL_CVERIFY_OPTIONAL_NO_CA; + break; + case REQUIRED: + value = SSL.SSL_CVERIFY_REQUIRE; + break; + } + SSLContext.setVerify(state.ctx, value, sslHostConfig.getCertificateVerificationDepth()); + + if (tms != null) { + // Client certificate verification based on custom trust managers + x509TrustManager = chooseTrustManager(tms); + SSLContext.setCertVerifyCallback(state.ctx, new CertificateVerifier() { + @Override + public boolean verify(long ssl, byte[][] chain, String auth) { + X509Certificate[] peerCerts = certificates(chain); + try { + x509TrustManager.checkClientTrusted(peerCerts, auth); + return true; + } catch (Exception e) { + log.debug(sm.getString("openssl.certificateVerificationFailed"), e); + } + return false; + } + }); + // Pass along the DER encoded certificates of the accepted client + // certificate issuers, so that their subjects can be presented + // by the server during the handshake to allow the client choosing + // an acceptable certificate + for (X509Certificate caCert : x509TrustManager.getAcceptedIssuers()) { + SSLContext.addClientCACertificateRaw(state.ctx, caCert.getEncoded()); + if (log.isDebugEnabled()) { + log.debug(sm.getString("openssl.addedClientCaCert", caCert.toString())); + } + } + } else { + // Client certificate verification based on trusted CA files and dirs + SSLContext.setCACertificate(state.ctx, + SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificateFile()), + SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificatePath())); + } + + if (negotiableProtocols != null && negotiableProtocols.size() > 0) { + List protocols = new ArrayList<>(negotiableProtocols); + protocols.add("http/1.1"); + String[] protocolsArray = protocols.toArray(new String[0]); + SSLContext.setAlpnProtos(state.ctx, protocolsArray, SSL.SSL_SELECTOR_FAILURE_NO_ADVERTISE); + } + + // Apply OpenSSLConfCmd if used + OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); + if (openSslConf != null && state.cctx != 0) { + // Check OpenSSLConfCmd if used + if (log.isTraceEnabled()) { + log.trace(sm.getString("openssl.checkConf")); + } + try { + if (!checkConf(openSslConf, state.cctx)) { + log.error(sm.getString("openssl.errCheckConf")); + throw new Exception(sm.getString("openssl.errCheckConf")); + } + } catch (Exception e) { + throw new Exception(sm.getString("openssl.errCheckConf"), e); + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("openssl.applyConf")); + } + try { + if (!applyConf(openSslConf, state.cctx, state.ctx)) { + log.error(sm.getString("openssl.errApplyConf")); + throw new SSLException(sm.getString("openssl.errApplyConf")); + } + } catch (Exception e) { + throw new SSLException(sm.getString("openssl.errApplyConf"), e); + } + // Reconfigure the enabled protocols + int opts = SSLContext.getOptions(state.ctx); + List enabled = new ArrayList<>(); + // Seems like there is no way to explicitly disable SSLv2Hello + // in OpenSSL so it is always enabled + enabled.add(Constants.SSL_PROTO_SSLv2Hello); + if ((opts & SSL.SSL_OP_NO_TLSv1) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1); + } + if ((opts & SSL.SSL_OP_NO_TLSv1_1) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_1); + } + if ((opts & SSL.SSL_OP_NO_TLSv1_2) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_2); + } + if ((opts & SSL.SSL_OP_NO_SSLv2) == 0) { + enabled.add(Constants.SSL_PROTO_SSLv2); + } + if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) { + enabled.add(Constants.SSL_PROTO_SSLv3); + } + sslHostConfig.setEnabledProtocols( + enabled.toArray(new String[0])); + // Reconfigure the enabled ciphers + sslHostConfig.setEnabledCiphers(SSLContext.getCiphers(state.ctx)); + } + + sessionContext = new OpenSSLSessionContext(this); + // If client authentication is being used, OpenSSL requires that + // this is set so always set it in case an app is configured to + // require it + sessionContext.setSessionIdContext(SSLContext.DEFAULT_SESSION_ID_CONTEXT); + sslHostConfig.setOpenSslContext(Long.valueOf(state.ctx)); + initialized = true; + } catch (Exception e) { + log.warn(sm.getString("openssl.errorSSLCtxInit"), e); + destroy(); + } + } + + + public void addCertificate(SSLHostConfigCertificate certificate) throws Exception { + // Load Server key and certificate + if (certificate.getCertificateFile() != null) { + // Set certificate + String passwordToUse = null; + if (certificate.getCertificateKeyPasswordFile() != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader( + new FileInputStream( + SSLHostConfig.adjustRelativePath(certificate.getCertificateKeyPasswordFile())), + StandardCharsets.UTF_8))) { + passwordToUse = reader.readLine(); + } + } else { + passwordToUse = certificate.getCertificateKeyPassword(); + } + SSLContext.setCertificate(state.ctx, + SSLHostConfig.adjustRelativePath(certificate.getCertificateFile()), + SSLHostConfig.adjustRelativePath(certificate.getCertificateKeyFile()), + passwordToUse, getCertificateIndex(certificate)); + // Set certificate chain file + SSLContext.setCertificateChainFile(state.ctx, + SSLHostConfig.adjustRelativePath(certificate.getCertificateChainFile()), false); + // Set revocation + SSLContext.setCARevocation(state.ctx, + SSLHostConfig.adjustRelativePath( + sslHostConfig.getCertificateRevocationListFile()), + SSLHostConfig.adjustRelativePath( + sslHostConfig.getCertificateRevocationListPath())); + } else { + String alias = certificate.getCertificateKeyAlias(); + X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); + if (alias == null) { + alias = SSLUtilBase.DEFAULT_KEY_ALIAS; + } + X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); + if (chain == null) { + alias = findAlias(x509KeyManager, certificate); + chain = x509KeyManager.getCertificateChain(alias); + } + PrivateKey key = x509KeyManager.getPrivateKey(alias); + StringBuilder sb = new StringBuilder(BEGIN_KEY); + sb.append(Base64.getMimeEncoder(64, new byte[] {'\n'}).encodeToString(key.getEncoded())); + sb.append(END_KEY); + SSLContext.setCertificateRaw(state.ctx, chain[0].getEncoded(), + sb.toString().getBytes(StandardCharsets.US_ASCII), + getCertificateIndex(certificate)); + for (int i = 1; i < chain.length; i++) { + SSLContext.addChainCertificateRaw(state.ctx, chain[i].getEncoded()); + } + } + } + + + private static int getCertificateIndex(SSLHostConfigCertificate certificate) { + int result; + // If the type is undefined there will only be one certificate (enforced + // in SSLHostConfig) so use the RSA slot. + if (certificate.getType() == Type.RSA || certificate.getType() == Type.UNDEFINED) { + result = SSL.SSL_AIDX_RSA; + } else if (certificate.getType() == Type.EC) { + result = SSL.SSL_AIDX_ECC; + } else if (certificate.getType() == Type.DSA) { + result = SSL.SSL_AIDX_DSA; + } else { + result = SSL.SSL_AIDX_MAX; + } + return result; + } + + + /* + * Find a valid alias when none was specified in the config. + */ + private static String findAlias(X509KeyManager keyManager, + SSLHostConfigCertificate certificate) { + + Type type = certificate.getType(); + String result = null; + + List candidateTypes = new ArrayList<>(); + if (Type.UNDEFINED.equals(type)) { + // Try all types to find an suitable alias + candidateTypes.addAll(Arrays.asList(Type.values())); + candidateTypes.remove(Type.UNDEFINED); + } else { + // Look for the specific type to find a suitable alias + candidateTypes.add(type); + } + + Iterator iter = candidateTypes.iterator(); + while (result == null && iter.hasNext()) { + result = keyManager.chooseServerAlias(iter.next().toString(), null, null); + } + + return result; + } + + private static X509TrustManager chooseTrustManager(TrustManager[] managers) { + for (TrustManager m : managers) { + if (m instanceof X509TrustManager) { + return (X509TrustManager) m; + } + } + throw new IllegalStateException(sm.getString("openssl.trustManagerMissing")); + } + + private static X509Certificate[] certificates(byte[][] chain) { + X509Certificate[] peerCerts = new X509Certificate[chain.length]; + for (int i = 0; i < peerCerts.length; i++) { + peerCerts[i] = new OpenSSLX509Certificate(chain[i]); + } + return peerCerts; + } + + + long getSSLContextID() { + return state.ctx; + } + + + @Override + public SSLSessionContext getServerSessionContext() { + return sessionContext; + } + + @Override + public SSLEngine createSSLEngine() { + return new OpenSSLEngine(cleaner, state.ctx, defaultProtocol, false, sessionContext, + (negotiableProtocols != null && negotiableProtocols.size() > 0), initialized, + sslHostConfig.getCertificateVerificationDepth(), + sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL_NO_CA); + } + + @Override + public SSLServerSocketFactory getServerSocketFactory() { + throw new UnsupportedOperationException(); + } + + @Override + public SSLParameters getSupportedSSLParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + X509Certificate[] chain = null; + X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); + if (x509KeyManager != null) { + if (alias == null) { + alias = SSLUtilBase.DEFAULT_KEY_ALIAS; + } + chain = x509KeyManager.getCertificateChain(alias); + if (chain == null) { + alias = findAlias(x509KeyManager, certificate); + chain = x509KeyManager.getCertificateChain(alias); + } + } + + return chain; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + X509Certificate[] acceptedCerts = null; + if (x509TrustManager != null) { + acceptedCerts = x509TrustManager.getAcceptedIssuers(); + } + return acceptedCerts; + } + + + private static class OpenSSLState implements Runnable { + + final long aprPool; + // OpenSSLConfCmd context + final long cctx; + // SSL context + final long ctx; + + private OpenSSLState(long aprPool, long cctx, long ctx) { + this.aprPool = aprPool; + this.cctx = cctx; + this.ctx = ctx; + } + + @Override + public void run() { + if (ctx != 0) { + SSLContext.free(ctx); + } + if (cctx != 0) { + SSLConf.free(cctx); + } + if (aprPool != 0) { + Pool.destroy(aprPool); + } + } + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java new file mode 100644 index 0000000..39ee3f9 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java @@ -0,0 +1,1473 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.lang.ref.Cleaner; +import java.lang.ref.Cleaner.Cleanable; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionBindingEvent; +import javax.net.ssl.SSLSessionBindingListener; +import javax.net.ssl.SSLSessionContext; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jni.Buffer; +import org.apache.tomcat.jni.Pool; +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.jni.SSLContext; +import org.apache.tomcat.util.buf.ByteBufferUtils; +import org.apache.tomcat.util.net.Constants; +import org.apache.tomcat.util.net.SSLUtil; +import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implements a {@link SSLEngine} using + * OpenSSL + * BIO abstractions. + */ +public final class OpenSSLEngine extends SSLEngine implements SSLUtil.ProtocolInfo { + + private static final Log logger = LogFactory.getLog(OpenSSLEngine.class); + private static final StringManager sm = StringManager.getManager(OpenSSLEngine.class); + + private static final Certificate[] EMPTY_CERTIFICATES = new Certificate[0]; + + public static final Set AVAILABLE_CIPHER_SUITES; + + public static final Set IMPLEMENTED_PROTOCOLS_SET; + + static { + final Set availableCipherSuites = new LinkedHashSet<>(128); + final long aprPool = Pool.create(0); + try { + final long sslCtx = SSLContext.make(aprPool, SSL.SSL_PROTOCOL_ALL, SSL.SSL_MODE_SERVER); + try { + SSLContext.setOptions(sslCtx, SSL.SSL_OP_ALL); + SSLContext.setCipherSuite(sslCtx, "ALL"); + final long ssl = SSL.newSSL(sslCtx, true); + try { + for (String c: SSL.getCiphers(ssl)) { + // Filter out bad input. + if (c == null || c.length() == 0 || availableCipherSuites.contains(c)) { + continue; + } + availableCipherSuites.add(OpenSSLCipherConfigurationParser.openSSLToJsse(c)); + } + } finally { + SSL.freeSSL(ssl); + } + } finally { + SSLContext.free(sslCtx); + } + } catch (Exception e) { + logger.warn(sm.getString("engine.ciphersFailure"), e); + } finally { + Pool.destroy(aprPool); + } + AVAILABLE_CIPHER_SUITES = Collections.unmodifiableSet(availableCipherSuites); + + HashSet protocols = new HashSet<>(); + protocols.add(Constants.SSL_PROTO_SSLv2Hello); + protocols.add(Constants.SSL_PROTO_SSLv2); + protocols.add(Constants.SSL_PROTO_SSLv3); + protocols.add(Constants.SSL_PROTO_TLSv1); + protocols.add(Constants.SSL_PROTO_TLSv1_1); + protocols.add(Constants.SSL_PROTO_TLSv1_2); + if (SSL.version() >= 0x1010100f) { + protocols.add(Constants.SSL_PROTO_TLSv1_3); + } + + IMPLEMENTED_PROTOCOLS_SET = Collections.unmodifiableSet(protocols); + } + + private static final int MAX_PLAINTEXT_LENGTH = 16 * 1024; // 2^14 + private static final int MAX_COMPRESSED_LENGTH = MAX_PLAINTEXT_LENGTH + 1024; + private static final int MAX_CIPHERTEXT_LENGTH = MAX_COMPRESSED_LENGTH + 1024; + + // Protocols + static final int VERIFY_DEPTH = 10; + + // Header (5) + Data (2^14) + Compression (1024) + Encryption (1024) + MAC (20) + Padding (256) + static final int MAX_ENCRYPTED_PACKET_LENGTH = MAX_CIPHERTEXT_LENGTH + 5 + 20 + 256; + + static final int MAX_ENCRYPTION_OVERHEAD_LENGTH = MAX_ENCRYPTED_PACKET_LENGTH - MAX_PLAINTEXT_LENGTH; + + enum ClientAuthMode { + NONE, + OPTIONAL, + REQUIRE, + } + + private static final String INVALID_CIPHER = "SSL_NULL_WITH_NULL_NULL"; + + private static final long EMPTY_ADDR = Buffer.address(ByteBuffer.allocate(0)); + + private final OpenSSLState state; + private final Cleanable cleanable; + + private enum Accepted { NOT, IMPLICIT, EXPLICIT } + private Accepted accepted = Accepted.NOT; + private boolean handshakeFinished; + private int currentHandshake; + private boolean receivedShutdown; + private volatile boolean destroyed; + + // Use an invalid cipherSuite until the handshake is completed + // See http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLEngine.html#getSession() + private volatile String version; + private volatile String cipher; + private volatile String applicationProtocol; + + private volatile Certificate[] peerCerts; + @Deprecated + private volatile javax.security.cert.X509Certificate[] x509PeerCerts; + private volatile ClientAuthMode clientAuth = ClientAuthMode.NONE; + + // SSL Engine status variables + private boolean isInboundDone; + private boolean isOutboundDone; + private boolean engineClosed; + private boolean sendHandshakeError = false; + + private final boolean clientMode; + private final String fallbackApplicationProtocol; + private final OpenSSLSessionContext sessionContext; + private final boolean alpn; + private final boolean initialized; + private final int certificateVerificationDepth; + private final boolean certificateVerificationOptionalNoCA; + + private String selectedProtocol = null; + + private final OpenSSLSession session; + + /** + * Creates a new instance + * + * @param cleaner Used to clean up references to instances before they are + * garbage collected + * @param sslCtx an OpenSSL {@code SSL_CTX} object + * @param fallbackApplicationProtocol the fallback application protocol + * @param clientMode {@code true} if this is used for clients, {@code false} + * otherwise + * @param sessionContext the {@link OpenSSLSessionContext} this + * {@link SSLEngine} belongs to. + * @param alpn {@code true} if alpn should be used, {@code false} + * otherwise + * @param initialized {@code true} if this instance gets its protocol, + * cipher and client verification from the {@code SSL_CTX} {@code sslCtx} + * @param certificateVerificationDepth Certificate verification depth + * @param certificateVerificationOptionalNoCA Skip CA verification in + * optional mode + */ + OpenSSLEngine(Cleaner cleaner, long sslCtx, String fallbackApplicationProtocol, + boolean clientMode, OpenSSLSessionContext sessionContext, boolean alpn, + boolean initialized, int certificateVerificationDepth, + boolean certificateVerificationOptionalNoCA) { + if (sslCtx == 0) { + throw new IllegalArgumentException(sm.getString("engine.noSSLContext")); + } + session = new OpenSSLSession(); + long ssl = SSL.newSSL(sslCtx, !clientMode); + long networkBIO = SSL.makeNetworkBIO(ssl); + state = new OpenSSLState(ssl, networkBIO); + cleanable = cleaner.register(this, state); + this.fallbackApplicationProtocol = fallbackApplicationProtocol; + this.clientMode = clientMode; + this.sessionContext = sessionContext; + this.alpn = alpn; + this.initialized = initialized; + this.certificateVerificationDepth = certificateVerificationDepth; + this.certificateVerificationOptionalNoCA = certificateVerificationOptionalNoCA; + } + + @Override + public String getNegotiatedProtocol() { + return selectedProtocol; + } + + /** + * Destroys this engine. + */ + public synchronized void shutdown() { + if (!destroyed) { + destroyed = true; + cleanable.clean(); + // internal errors can cause shutdown without marking the engine closed + isInboundDone = isOutboundDone = engineClosed = true; + } + } + + /** + * Write plain text data to the OpenSSL internal BIO + * + * Calling this function with src.remaining == 0 is undefined. + * @throws SSLException if the OpenSSL error check fails + */ + private int writePlaintextData(final long ssl, final ByteBuffer src) throws SSLException { + clearLastError(); + final int pos = src.position(); + final int limit = src.limit(); + final int len = Math.min(limit - pos, MAX_PLAINTEXT_LENGTH); + final int sslWrote; + + if (src.isDirect()) { + final long addr = Buffer.address(src) + pos; + sslWrote = SSL.writeToSSL(ssl, addr, len); + if (sslWrote <= 0) { + checkLastError(); + } + if (sslWrote >= 0) { + src.position(pos + sslWrote); + return sslWrote; + } + } else { + ByteBuffer buf = ByteBuffer.allocateDirect(len); + try { + final long addr = Buffer.address(buf); + + src.limit(pos + len); + + buf.put(src); + src.limit(limit); + + sslWrote = SSL.writeToSSL(ssl, addr, len); + if (sslWrote <= 0) { + checkLastError(); + } + if (sslWrote >= 0) { + src.position(pos + sslWrote); + return sslWrote; + } else { + src.position(pos); + } + } finally { + buf.clear(); + ByteBufferUtils.cleanDirectBuffer(buf); + } + } + + throw new IllegalStateException( + sm.getString("engine.writeToSSLFailed", Integer.toString(sslWrote))); + } + + /** + * Write encrypted data to the OpenSSL network BIO. + * @throws SSLException if the OpenSSL error check fails + */ + private int writeEncryptedData(final long networkBIO, final ByteBuffer src) throws SSLException { + clearLastError(); + final int pos = src.position(); + final int len = src.remaining(); + if (src.isDirect()) { + final long addr = Buffer.address(src) + pos; + final int netWrote = SSL.writeToBIO(networkBIO, addr, len); + if (netWrote <= 0) { + checkLastError(); + } + if (netWrote >= 0) { + src.position(pos + netWrote); + return netWrote; + } + } else { + ByteBuffer buf = ByteBuffer.allocateDirect(len); + try { + final long addr = Buffer.address(buf); + + buf.put(src); + + final int netWrote = SSL.writeToBIO(networkBIO, addr, len); + if (netWrote <= 0) { + checkLastError(); + } + if (netWrote >= 0) { + src.position(pos + netWrote); + return netWrote; + } else { + src.position(pos); + } + } finally { + buf.clear(); + ByteBufferUtils.cleanDirectBuffer(buf); + } + } + + return 0; + } + + /** + * Read plain text data from the OpenSSL internal BIO + * @throws SSLException if the OpenSSL error check fails + */ + private int readPlaintextData(final long ssl, final ByteBuffer dst) throws SSLException { + clearLastError(); + if (dst.isDirect()) { + final int pos = dst.position(); + final long addr = Buffer.address(dst) + pos; + final int len = dst.limit() - pos; + final int sslRead = SSL.readFromSSL(ssl, addr, len); + if (sslRead > 0) { + dst.position(pos + sslRead); + return sslRead; + } else { + checkLastError(); + } + } else { + final int pos = dst.position(); + final int limit = dst.limit(); + final int len = Math.min(MAX_ENCRYPTED_PACKET_LENGTH, limit - pos); + final ByteBuffer buf = ByteBuffer.allocateDirect(len); + try { + final long addr = Buffer.address(buf); + + final int sslRead = SSL.readFromSSL(ssl, addr, len); + if (sslRead > 0) { + buf.limit(sslRead); + dst.limit(pos + sslRead); + dst.put(buf); + dst.limit(limit); + return sslRead; + } else { + checkLastError(); + } + } finally { + buf.clear(); + ByteBufferUtils.cleanDirectBuffer(buf); + } + } + + return 0; + } + + /** + * Read encrypted data from the OpenSSL network BIO + * @throws SSLException if the OpenSSL error check fails + */ + private int readEncryptedData(final long networkBIO, final ByteBuffer dst, final int pending) throws SSLException { + clearLastError(); + if (dst.isDirect() && dst.remaining() >= pending) { + final int pos = dst.position(); + final long addr = Buffer.address(dst) + pos; + final int bioRead = SSL.readFromBIO(networkBIO, addr, pending); + if (bioRead > 0) { + dst.position(pos + bioRead); + return bioRead; + } else { + checkLastError(); + } + } else { + final ByteBuffer buf = ByteBuffer.allocateDirect(pending); + try { + final long addr = Buffer.address(buf); + + final int bioRead = SSL.readFromBIO(networkBIO, addr, pending); + if (bioRead > 0) { + buf.limit(bioRead); + int oldLimit = dst.limit(); + dst.limit(dst.position() + bioRead); + dst.put(buf); + dst.limit(oldLimit); + return bioRead; + } else { + checkLastError(); + } + } finally { + buf.clear(); + ByteBufferUtils.cleanDirectBuffer(buf); + } + } + + return 0; + } + + @Override + public synchronized SSLEngineResult wrap(final ByteBuffer[] srcs, final int offset, final int length, final ByteBuffer dst) throws SSLException { + + // Check to make sure the engine has not been closed + if (destroyed) { + return new SSLEngineResult(SSLEngineResult.Status.CLOSED, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, 0, 0); + } + + // Throw required runtime exceptions + if (srcs == null || dst == null) { + throw new IllegalArgumentException(sm.getString("engine.nullBuffer")); + } + if (offset >= srcs.length || offset + length > srcs.length) { + throw new IndexOutOfBoundsException(sm.getString("engine.invalidBufferArray", + Integer.toString(offset), Integer.toString(length), + Integer.toString(srcs.length))); + } + if (dst.isReadOnly()) { + throw new ReadOnlyBufferException(); + } + + // Prepare OpenSSL to work in server mode and receive handshake + if (accepted == Accepted.NOT) { + beginHandshakeImplicitly(); + } + + // In handshake or close_notify stages, check if call to wrap was made + // without regard to the handshake status. + SSLEngineResult.HandshakeStatus handshakeStatus = getHandshakeStatus(); + + if ((!handshakeFinished || engineClosed) && handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { + return new SSLEngineResult(getEngineStatus(), SSLEngineResult.HandshakeStatus.NEED_UNWRAP, 0, 0); + } + + int bytesProduced = 0; + int pendingNet; + + // Check for pending data in the network BIO + pendingNet = SSL.pendingWrittenBytesInBIO(state.networkBIO); + if (pendingNet > 0) { + // Do we have enough room in destination to write encrypted data? + int capacity = dst.remaining(); + if (capacity < pendingNet) { + return new SSLEngineResult(SSLEngineResult.Status.BUFFER_OVERFLOW, handshakeStatus, 0, 0); + } + + // Write the pending data from the network BIO into the dst buffer + try { + bytesProduced = readEncryptedData(state.networkBIO, dst, pendingNet); + } catch (Exception e) { + throw new SSLException(e); + } + + // If isOutboundDone is set, then the data from the network BIO + // was the close_notify message -- we are not required to wait + // for the receipt the peer's close_notify message -- shutdown. + if (isOutboundDone) { + shutdown(); + } + + return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), 0, bytesProduced); + } + + // There was no pending data in the network BIO -- encrypt any application data + int bytesConsumed = 0; + int endOffset = offset + length; + for (int i = offset; i < endOffset; ++i) { + final ByteBuffer src = srcs[i]; + if (src == null) { + throw new IllegalArgumentException(sm.getString("engine.nullBufferInArray")); + } + while (src.hasRemaining()) { + + // Write plain text application data to the SSL engine + try { + bytesConsumed += writePlaintextData(state.ssl, src); + } catch (Exception e) { + throw new SSLException(e); + } + + // Check to see if the engine wrote data into the network BIO + pendingNet = SSL.pendingWrittenBytesInBIO(state.networkBIO); + if (pendingNet > 0) { + // Do we have enough room in dst to write encrypted data? + int capacity = dst.remaining(); + if (capacity < pendingNet) { + return new SSLEngineResult( + SSLEngineResult.Status.BUFFER_OVERFLOW, getHandshakeStatus(), bytesConsumed, bytesProduced); + } + + // Write the pending data from the network BIO into the dst buffer + try { + bytesProduced += readEncryptedData(state.networkBIO, dst, pendingNet); + } catch (Exception e) { + throw new SSLException(e); + } + + return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), bytesConsumed, bytesProduced); + } + } + } + return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), bytesConsumed, bytesProduced); + } + + @Override + public synchronized SSLEngineResult unwrap(final ByteBuffer src, final ByteBuffer[] dsts, final int offset, final int length) throws SSLException { + // Check to make sure the engine has not been closed + if (destroyed) { + return new SSLEngineResult(SSLEngineResult.Status.CLOSED, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, 0, 0); + } + + // Throw required runtime exceptions + if (src == null || dsts == null) { + throw new IllegalArgumentException(sm.getString("engine.nullBuffer")); + } + if (offset >= dsts.length || offset + length > dsts.length) { + throw new IndexOutOfBoundsException(sm.getString("engine.invalidBufferArray", + Integer.toString(offset), Integer.toString(length), + Integer.toString(dsts.length))); + } + int capacity = 0; + final int endOffset = offset + length; + for (int i = offset; i < endOffset; i++) { + ByteBuffer dst = dsts[i]; + if (dst == null) { + throw new IllegalArgumentException(sm.getString("engine.nullBufferInArray")); + } + if (dst.isReadOnly()) { + throw new ReadOnlyBufferException(); + } + capacity += dst.remaining(); + } + + // Prepare OpenSSL to work in server mode and receive handshake + if (accepted == Accepted.NOT) { + beginHandshakeImplicitly(); + } + + // In handshake or close_notify stages, check if call to unwrap was made + // without regard to the handshake status. + SSLEngineResult.HandshakeStatus handshakeStatus = getHandshakeStatus(); + if ((!handshakeFinished || engineClosed) && handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP) { + return new SSLEngineResult(getEngineStatus(), SSLEngineResult.HandshakeStatus.NEED_WRAP, 0, 0); + } + + int len = src.remaining(); + + // protect against protocol overflow attack vector + if (len > MAX_ENCRYPTED_PACKET_LENGTH) { + isInboundDone = true; + isOutboundDone = true; + engineClosed = true; + shutdown(); + throw new SSLException(sm.getString("engine.oversizedPacket")); + } + + // Write encrypted data to network BIO + int written = 0; + try { + written = writeEncryptedData(state.networkBIO, src); + } catch (Exception e) { + throw new SSLException(e); + } + + // There won't be any application data until we're done handshaking + // + // We first check handshakeFinished to eliminate the overhead of extra JNI call if possible. + int pendingApp = pendingReadableBytesInSSL(); + if (!handshakeFinished) { + pendingApp = 0; + } + int bytesProduced = 0; + int idx = offset; + // Do we have enough room in dsts to write decrypted data? + if (capacity == 0) { + return new SSLEngineResult(SSLEngineResult.Status.BUFFER_OVERFLOW, getHandshakeStatus(), written, 0); + } + + while (pendingApp > 0) { + if (idx == endOffset) { + // Destination buffer state changed (no remaining space although + // capacity is still available), so break loop with an error + throw new IllegalStateException(sm.getString("engine.invalidDestinationBuffersState")); + } + // Write decrypted data to dsts buffers + while (idx < endOffset) { + ByteBuffer dst = dsts[idx]; + if (!dst.hasRemaining()) { + idx++; + continue; + } + + if (pendingApp <= 0) { + break; + } + + int bytesRead; + try { + bytesRead = readPlaintextData(state.ssl, dst); + } catch (Exception e) { + throw new SSLException(e); + } + + if (bytesRead == 0) { + // This should not be possible. pendingApp is positive + // therefore the read should have read at least one byte. + throw new IllegalStateException(sm.getString("engine.failedToReadAvailableBytes")); + } + + bytesProduced += bytesRead; + pendingApp -= bytesRead; + capacity -= bytesRead; + + if (!dst.hasRemaining()) { + idx++; + } + } + if (capacity == 0) { + break; + } else if (pendingApp == 0) { + pendingApp = pendingReadableBytesInSSL(); + } + } + + // Check to see if we received a close_notify message from the peer + if (!receivedShutdown && (SSL.getShutdown(state.ssl) & SSL.SSL_RECEIVED_SHUTDOWN) == SSL.SSL_RECEIVED_SHUTDOWN) { + receivedShutdown = true; + closeOutbound(); + closeInbound(); + } + if (bytesProduced == 0 && (written == 0 || (written > 0 && !src.hasRemaining() && handshakeFinished))) { + return new SSLEngineResult(SSLEngineResult.Status.BUFFER_UNDERFLOW, getHandshakeStatus(), written, 0); + } else { + return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), written, bytesProduced); + } + } + + private int pendingReadableBytesInSSL() + throws SSLException { + // NOTE: Calling a fake read is necessary before calling pendingReadableBytesInSSL because + // SSL_pending will return 0 if OpenSSL has not started the current TLS record + // See https://www.openssl.org/docs/manmaster/man3/SSL_pending.html + clearLastError(); + int lastPrimingReadResult = SSL.readFromSSL(state.ssl, EMPTY_ADDR, 0); // priming read + // check if SSL_read returned <= 0. In this case we need to check the error and see if it was something + // fatal. + if (lastPrimingReadResult <= 0) { + checkLastError(); + } + int pendingReadableBytesInSSL = SSL.pendingReadableBytesInSSL(state.ssl); + + // TLS 1.0 needs additional handling + // TODO Figure out why this is necessary and if a simpler / better + // solution is available + if (Constants.SSL_PROTO_TLSv1.equals(version) && lastPrimingReadResult == 0 && + pendingReadableBytesInSSL == 0) { + // Perform another priming read + lastPrimingReadResult = SSL.readFromSSL(state.ssl, EMPTY_ADDR, 0); + if (lastPrimingReadResult <= 0) { + checkLastError(); + } + pendingReadableBytesInSSL = SSL.pendingReadableBytesInSSL(state.ssl); + } + + return pendingReadableBytesInSSL; + } + + @Override + public Runnable getDelegatedTask() { + // Currently, we do not delegate SSL computation tasks + return null; + } + + @Override + public synchronized void closeInbound() throws SSLException { + if (isInboundDone) { + return; + } + + isInboundDone = true; + engineClosed = true; + + shutdown(); + + if (accepted != Accepted.NOT && !receivedShutdown) { + throw new SSLException(sm.getString("engine.inboundClose")); + } + } + + @Override + public synchronized boolean isInboundDone() { + return isInboundDone || engineClosed; + } + + @Override + public synchronized void closeOutbound() { + if (isOutboundDone) { + return; + } + + isOutboundDone = true; + engineClosed = true; + + if (accepted != Accepted.NOT && !destroyed) { + int mode = SSL.getShutdown(state.ssl); + if ((mode & SSL.SSL_SENT_SHUTDOWN) != SSL.SSL_SENT_SHUTDOWN) { + SSL.shutdownSSL(state.ssl); + } + } else { + // engine closing before initial handshake + shutdown(); + } + } + + @Override + public synchronized boolean isOutboundDone() { + return isOutboundDone; + } + + @Override + public String[] getSupportedCipherSuites() { + Set availableCipherSuites = AVAILABLE_CIPHER_SUITES; + return availableCipherSuites.toArray(new String[0]); + } + + @Override + public synchronized String[] getEnabledCipherSuites() { + if (destroyed) { + return new String[0]; + } + String[] enabled = SSL.getCiphers(state.ssl); + if (enabled == null) { + return new String[0]; + } else { + for (int i = 0; i < enabled.length; i++) { + String mapped = OpenSSLCipherConfigurationParser.openSSLToJsse(enabled[i]); + if (mapped != null) { + enabled[i] = mapped; + } + } + return enabled; + } + } + + @Override + public synchronized void setEnabledCipherSuites(String[] cipherSuites) { + if (initialized) { + return; + } + if (cipherSuites == null) { + throw new IllegalArgumentException(sm.getString("engine.nullCipherSuite")); + } + if (destroyed) { + return; + } + final StringBuilder buf = new StringBuilder(); + for (String cipherSuite : cipherSuites) { + if (cipherSuite == null) { + break; + } + String converted = OpenSSLCipherConfigurationParser.jsseToOpenSSL(cipherSuite); + if (!AVAILABLE_CIPHER_SUITES.contains(cipherSuite)) { + logger.debug(sm.getString("engine.unsupportedCipher", cipherSuite, converted)); + } + if (converted != null) { + cipherSuite = converted; + } + + buf.append(cipherSuite); + buf.append(':'); + } + + if (buf.length() == 0) { + throw new IllegalArgumentException(sm.getString("engine.emptyCipherSuite")); + } + buf.setLength(buf.length() - 1); + + final String cipherSuiteSpec = buf.toString(); + try { + SSL.setCipherSuites(state.ssl, cipherSuiteSpec); + } catch (Exception e) { + throw new IllegalStateException(sm.getString("engine.failedCipherSuite", cipherSuiteSpec), e); + } + } + + @Override + public String[] getSupportedProtocols() { + return IMPLEMENTED_PROTOCOLS_SET.toArray(new String[0]); + } + + @Override + public synchronized String[] getEnabledProtocols() { + if (destroyed) { + return new String[0]; + } + List enabled = new ArrayList<>(); + // Seems like there is no way to explicitly disable SSLv2Hello in OpenSSL so it is always enabled + enabled.add(Constants.SSL_PROTO_SSLv2Hello); + int opts = SSL.getOptions(state.ssl); + if ((opts & SSL.SSL_OP_NO_TLSv1) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1); + } + if ((opts & SSL.SSL_OP_NO_TLSv1_1) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_1); + } + if ((opts & SSL.SSL_OP_NO_TLSv1_2) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_2); + } + if ((opts & SSL.SSL_OP_NO_SSLv2) == 0) { + enabled.add(Constants.SSL_PROTO_SSLv2); + } + if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) { + enabled.add(Constants.SSL_PROTO_SSLv3); + } + return enabled.toArray(new String[0]); + } + + @Override + public synchronized void setEnabledProtocols(String[] protocols) { + if (initialized) { + return; + } + if (protocols == null) { + // This is correct from the API docs + throw new IllegalArgumentException(); + } + if (destroyed) { + return; + } + boolean sslv2 = false; + boolean sslv3 = false; + boolean tlsv1 = false; + boolean tlsv1_1 = false; + boolean tlsv1_2 = false; + for (String p : protocols) { + if (!IMPLEMENTED_PROTOCOLS_SET.contains(p)) { + throw new IllegalArgumentException(sm.getString("engine.unsupportedProtocol", p)); + } + if (p.equals(Constants.SSL_PROTO_SSLv2)) { + sslv2 = true; + } else if (p.equals(Constants.SSL_PROTO_SSLv3)) { + sslv3 = true; + } else if (p.equals(Constants.SSL_PROTO_TLSv1)) { + tlsv1 = true; + } else if (p.equals(Constants.SSL_PROTO_TLSv1_1)) { + tlsv1_1 = true; + } else if (p.equals(Constants.SSL_PROTO_TLSv1_2)) { + tlsv1_2 = true; + } + } + // Enable all and then disable what we not want + SSL.setOptions(state.ssl, SSL.SSL_OP_ALL); + + if (!sslv2) { + SSL.setOptions(state.ssl, SSL.SSL_OP_NO_SSLv2); + } + if (!sslv3) { + SSL.setOptions(state.ssl, SSL.SSL_OP_NO_SSLv3); + } + if (!tlsv1) { + SSL.setOptions(state.ssl, SSL.SSL_OP_NO_TLSv1); + } + if (!tlsv1_1) { + SSL.setOptions(state.ssl, SSL.SSL_OP_NO_TLSv1_1); + } + if (!tlsv1_2) { + SSL.setOptions(state.ssl, SSL.SSL_OP_NO_TLSv1_2); + } + } + + @Override + public SSLSession getSession() { + return session; + } + + @Override + public synchronized void beginHandshake() throws SSLException { + if (engineClosed || destroyed) { + throw new SSLException(sm.getString("engine.engineClosed")); + } + switch (accepted) { + case NOT: + handshake(); + accepted = Accepted.EXPLICIT; + break; + case IMPLICIT: + // A user did not start handshake by calling this method by themselves, + // but handshake has been started already by wrap() or unwrap() implicitly. + // Because it's the user's first time to call this method, it is unfair to + // raise an exception. From the user's standpoint, they never asked for + // renegotiation. + + accepted = Accepted.EXPLICIT; // Next time this method is invoked by the user, we should raise an exception. + break; + case EXPLICIT: + renegotiate(); + break; + } + } + + private void beginHandshakeImplicitly() throws SSLException { + handshake(); + accepted = Accepted.IMPLICIT; + } + + private void handshake() throws SSLException { + currentHandshake = SSL.getHandshakeCount(state.ssl); + clearLastError(); + int code = SSL.doHandshake(state.ssl); + if (code <= 0) { + checkLastError(); + } else { + if (alpn) { + selectedProtocol = SSL.getAlpnSelected(state.ssl); + } + session.lastAccessedTime = System.currentTimeMillis(); + // if SSL_do_handshake returns > 0 it means the handshake was finished. This means we can update + // handshakeFinished directly and so eliminate unnecessary calls to SSL.isInInit(...) + handshakeFinished = true; + } + } + + private synchronized void renegotiate() throws SSLException { + clearLastError(); + int code; + if (SSL.getVersion(state.ssl).equals(Constants.SSL_PROTO_TLSv1_3)) { + code = SSL.verifyClientPostHandshake(state.ssl); + } else { + code = SSL.renegotiate(state.ssl); + } + if (code <= 0) { + checkLastError(); + } + handshakeFinished = false; + peerCerts = null; + x509PeerCerts = null; + currentHandshake = SSL.getHandshakeCount(state.ssl); + int code2 = SSL.doHandshake(state.ssl); + if (code2 <= 0) { + checkLastError(); + } + } + + private void checkLastError() throws SSLException { + String sslError = getLastError(); + if (sslError != null) { + // Many errors can occur during handshake and need to be reported + if (!handshakeFinished) { + sendHandshakeError = true; + } else { + throw new SSLException(sslError); + } + } + } + + + /** + * Clear out any errors, but log a warning. + */ + private static void clearLastError() { + getLastError(); + } + + /** + * Many calls to SSL methods do not check the last error. Those that do + * check the last error need to ensure that any previously ignored error is + * cleared prior to the method call else errors may be falsely reported. + * Ideally, before any SSL_read, SSL_write, clearLastError should always + * be called, and getLastError should be called after on any negative or + * zero result. + * @return the first error in the stack + */ + private static String getLastError() { + String sslError = null; + long error; + while ((error = SSL.getLastErrorNumber()) != SSL.SSL_ERROR_NONE) { + // Loop until getLastErrorNumber() returns SSL_ERROR_NONE + String err = SSL.getErrorString(error); + if (sslError == null) { + sslError = err; + } + if (logger.isDebugEnabled()) { + logger.debug(sm.getString("engine.openSSLError", Long.toString(error), err)); + } + } + return sslError; + } + + private SSLEngineResult.Status getEngineStatus() { + return engineClosed ? SSLEngineResult.Status.CLOSED : SSLEngineResult.Status.OK; + } + + @Override + public synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() { + if (accepted == Accepted.NOT || destroyed) { + return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; + } + + // Check if we are in the initial handshake phase + if (!handshakeFinished) { + + // There is pending data in the network BIO -- call wrap + if (sendHandshakeError || SSL.pendingWrittenBytesInBIO(state.networkBIO) != 0) { + if (sendHandshakeError) { + // After a last wrap, consider it is going to be done + sendHandshakeError = false; + currentHandshake++; + } + return SSLEngineResult.HandshakeStatus.NEED_WRAP; + } + + /* + * Tomcat Native stores a count of the completed handshakes in the + * SSL instance and increments it every time a handshake is + * completed. Comparing the handshake count when the handshake + * started to the current handshake count enables this code to + * detect when the handshake has completed. + * + * Obtaining client certificates after the connection has been + * established requires additional checks. We need to trigger + * additional reads until the certificates have been read but we + * don't know how many reads we will need as it depends on both + * client and network behaviour. + * + * The additional reads are triggered by returning NEED_UNWRAP + * rather than FINISHED. This allows the standard I/O code to be + * used. + * + * For TLSv1.2 and below, the handshake completes before the + * renegotiation. We therefore use SSL.renegotiatePending() to + * check on the current status of the renegotiation and return + * NEED_UNWRAP until it completes which means the client + * certificates will have been read from the client. + * + * For TLSv1.3, Tomcat Native sets a flag when post handshake + * authentication is started and updates it once the client + * certificate has been received. We therefore use + * SSL.getPostHandshakeAuthInProgress() to check the current status + * and return NEED_UNWRAP until that methods indicates that PHA is + * no longer in progress. + */ + + // No pending data to be sent to the peer + // Check to see if we have finished handshaking + int handshakeCount = SSL.getHandshakeCount(state.ssl); + if (handshakeCount != currentHandshake && SSL.renegotiatePending(state.ssl) == 0 && + (SSL.getPostHandshakeAuthInProgress(state.ssl) == 0)) { + if (alpn) { + selectedProtocol = SSL.getAlpnSelected(state.ssl); + } + session.lastAccessedTime = System.currentTimeMillis(); + version = SSL.getVersion(state.ssl); + handshakeFinished = true; + return SSLEngineResult.HandshakeStatus.FINISHED; + } + + // No pending data + // Still handshaking / renegotiation / post-handshake auth pending + // Must be waiting on the peer to send more data + return SSLEngineResult.HandshakeStatus.NEED_UNWRAP; + } + + // Check if we are in the shutdown phase + if (engineClosed) { + // Waiting to send the close_notify message + if (SSL.pendingWrittenBytesInBIO(state.networkBIO) != 0) { + return SSLEngineResult.HandshakeStatus.NEED_WRAP; + } + + // Must be waiting to receive the close_notify message + return SSLEngineResult.HandshakeStatus.NEED_UNWRAP; + } + + return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; + } + + @Override + public void setUseClientMode(boolean clientMode) { + if (clientMode != this.clientMode) { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean getUseClientMode() { + return clientMode; + } + + @Override + public void setNeedClientAuth(boolean b) { + setClientAuth(b ? ClientAuthMode.REQUIRE : ClientAuthMode.NONE); + } + + @Override + public boolean getNeedClientAuth() { + return clientAuth == ClientAuthMode.REQUIRE; + } + + @Override + public void setWantClientAuth(boolean b) { + setClientAuth(b ? ClientAuthMode.OPTIONAL : ClientAuthMode.NONE); + } + + @Override + public boolean getWantClientAuth() { + return clientAuth == ClientAuthMode.OPTIONAL; + } + + private void setClientAuth(ClientAuthMode mode) { + if (clientMode) { + return; + } + synchronized (this) { + if (clientAuth == mode) { + // No need to issue any JNI calls if the mode is the same + return; + } + switch (mode) { + case NONE: + SSL.setVerify(state.ssl, SSL.SSL_CVERIFY_NONE, certificateVerificationDepth); + break; + case REQUIRE: + SSL.setVerify(state.ssl, SSL.SSL_CVERIFY_REQUIRE, certificateVerificationDepth); + break; + case OPTIONAL: + SSL.setVerify(state.ssl, + certificateVerificationOptionalNoCA ? SSL.SSL_CVERIFY_OPTIONAL_NO_CA : SSL.SSL_CVERIFY_OPTIONAL, + certificateVerificationDepth); + break; + } + clientAuth = mode; + } + } + + @Override + public void setEnableSessionCreation(boolean b) { + if (!b) { + String msg = sm.getString("engine.noRestrictSessionCreation"); + throw new UnsupportedOperationException(msg); + } + } + + @Override + public boolean getEnableSessionCreation() { + return true; + } + + + private class OpenSSLSession implements SSLSession { + + // lazy init for memory reasons + private Map values; + + // Last accessed time + private long lastAccessedTime = -1; + + @Override + public byte[] getId() { + byte[] id = null; + synchronized (OpenSSLEngine.this) { + if (!destroyed) { + id = SSL.getSessionId(state.ssl); + } + } + + return id; + } + + @Override + public SSLSessionContext getSessionContext() { + return sessionContext; + } + + @Override + public long getCreationTime() { + // We need to multiply by 1000 as OpenSSL uses seconds and we need milliseconds. + long creationTime = 0; + synchronized (OpenSSLEngine.this) { + if (!destroyed) { + creationTime = SSL.getTime(state.ssl); + } + } + return creationTime * 1000L; + } + + @Override + public long getLastAccessedTime() { + return (lastAccessedTime > 0) ? lastAccessedTime : getCreationTime(); + } + + @Override + public void invalidate() { + // NOOP + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public void putValue(String name, Object value) { + if (name == null) { + throw new IllegalArgumentException(sm.getString("engine.nullName")); + } + if (value == null) { + throw new IllegalArgumentException(sm.getString("engine.nullValue")); + } + Map values = this.values; + if (values == null) { + // Use size of 2 to keep the memory overhead small + values = this.values = new HashMap<>(2); + } + Object old = values.put(name, value); + if (value instanceof SSLSessionBindingListener) { + ((SSLSessionBindingListener) value).valueBound(new SSLSessionBindingEvent(this, name)); + } + notifyUnbound(old, name); + } + + @Override + public Object getValue(String name) { + if (name == null) { + throw new IllegalArgumentException(sm.getString("engine.nullName")); + } + if (values == null) { + return null; + } + return values.get(name); + } + + @Override + public void removeValue(String name) { + if (name == null) { + throw new IllegalArgumentException(sm.getString("engine.nullName")); + } + Map values = this.values; + if (values == null) { + return; + } + Object old = values.remove(name); + notifyUnbound(old, name); + } + + @Override + public String[] getValueNames() { + Map values = this.values; + if (values == null || values.isEmpty()) { + return new String[0]; + } + return values.keySet().toArray(new String[0]); + } + + private void notifyUnbound(Object value, String name) { + if (value instanceof SSLSessionBindingListener) { + ((SSLSessionBindingListener) value).valueUnbound(new SSLSessionBindingEvent(this, name)); + } + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + // these are lazy created to reduce memory overhead + Certificate[] c = peerCerts; + if (c == null) { + byte[] clientCert; + byte[][] chain; + synchronized (OpenSSLEngine.this) { + if (destroyed || SSL.isInInit(state.ssl) != 0) { + throw new SSLPeerUnverifiedException(sm.getString("engine.unverifiedPeer")); + } + chain = SSL.getPeerCertChain(state.ssl); + if (!clientMode) { + // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer certificate. + // We use SSL_get_peer_certificate to get it in this case and add it to our array later. + // + // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html + clientCert = SSL.getPeerCertificate(state.ssl); + } else { + clientCert = null; + } + } + if (chain == null && clientCert == null) { + return null; + } + int len = 0; + if (chain != null) { + len += chain.length; + } + + int i = 0; + Certificate[] certificates; + if (clientCert != null) { + len++; + certificates = new Certificate[len]; + certificates[i++] = new OpenSSLX509Certificate(clientCert); + } else { + certificates = new Certificate[len]; + } + if (chain != null) { + int a = 0; + for (; i < certificates.length; i++) { + certificates[i] = new OpenSSLX509Certificate(chain[a++]); + } + } + c = peerCerts = certificates; + } + return c; + } + + @Override + public Certificate[] getLocalCertificates() { + // FIXME (if possible): Not available in the OpenSSL API + return EMPTY_CERTIFICATES; + } + + @Deprecated + @Override + public javax.security.cert.X509Certificate[] getPeerCertificateChain() + throws SSLPeerUnverifiedException { + // these are lazy created to reduce memory overhead + javax.security.cert.X509Certificate[] c = x509PeerCerts; + if (c == null) { + byte[][] chain; + synchronized (OpenSSLEngine.this) { + if (destroyed || SSL.isInInit(state.ssl) != 0) { + throw new SSLPeerUnverifiedException(sm.getString("engine.unverifiedPeer")); + } + chain = SSL.getPeerCertChain(state.ssl); + } + if (chain == null) { + throw new SSLPeerUnverifiedException(sm.getString("engine.unverifiedPeer")); + } + javax.security.cert.X509Certificate[] peerCerts = + new javax.security.cert.X509Certificate[chain.length]; + for (int i = 0; i < peerCerts.length; i++) { + try { + peerCerts[i] = javax.security.cert.X509Certificate.getInstance(chain[i]); + } catch (javax.security.cert.CertificateException e) { + throw new IllegalStateException(e); + } + } + c = x509PeerCerts = peerCerts; + } + return c; + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + Certificate[] peer = getPeerCertificates(); + if (peer == null || peer.length == 0) { + return null; + } + return principal(peer); + } + + @Override + public Principal getLocalPrincipal() { + Certificate[] local = getLocalCertificates(); + if (local == null || local.length == 0) { + return null; + } + return principal(local); + } + + private Principal principal(Certificate[] certs) { + return ((java.security.cert.X509Certificate) certs[0]).getIssuerX500Principal(); + } + + @Override + public String getCipherSuite() { + if (cipher == null) { + String ciphers; + synchronized (OpenSSLEngine.this) { + if (!handshakeFinished) { + return INVALID_CIPHER; + } + if (destroyed) { + return INVALID_CIPHER; + } + ciphers = SSL.getCipherForSSL(state.ssl); + } + String c = OpenSSLCipherConfigurationParser.openSSLToJsse(ciphers); + if (c != null) { + cipher = c; + } + } + return cipher; + } + + @Override + public String getProtocol() { + String applicationProtocol = OpenSSLEngine.this.applicationProtocol; + if (applicationProtocol == null) { + applicationProtocol = fallbackApplicationProtocol; + if (applicationProtocol != null) { + OpenSSLEngine.this.applicationProtocol = applicationProtocol.replace(':', '_'); + } else { + OpenSSLEngine.this.applicationProtocol = applicationProtocol = ""; + } + } + String version = null; + synchronized (OpenSSLEngine.this) { + if (!destroyed) { + version = SSL.getVersion(state.ssl); + } + } + if (applicationProtocol.isEmpty()) { + return version; + } else { + return version + ':' + applicationProtocol; + } + } + + @Override + public String getPeerHost() { + // Not available for now in Tomcat (needs to be passed during engine creation) + return null; + } + + @Override + public int getPeerPort() { + // Not available for now in Tomcat (needs to be passed during engine creation) + return 0; + } + + @Override + public int getPacketBufferSize() { + return MAX_ENCRYPTED_PACKET_LENGTH; + } + + @Override + public int getApplicationBufferSize() { + return MAX_PLAINTEXT_LENGTH; + } + + } + + + private static class OpenSSLState implements Runnable { + + private final long ssl; + private final long networkBIO; + + private OpenSSLState(long ssl, long networkBIO) { + this.ssl = ssl; + this.networkBIO = networkBIO; + } + + @Override + public void run() { + if (networkBIO != 0) { + SSL.freeBIO(networkBIO); + } + if (ssl != 0) { + SSL.freeSSL(ssl); + } + } + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLImplementation.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLImplementation.java new file mode 100644 index 0000000..d496e7d --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLImplementation.java @@ -0,0 +1,41 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLSession; + +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLImplementation; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SSLUtil; +import org.apache.tomcat.util.net.jsse.JSSESupport; + +public class OpenSSLImplementation extends SSLImplementation { + + @Override + public SSLSupport getSSLSupport(SSLSession session, Map> additionalAttributes) { + return new JSSESupport(session, additionalAttributes); + } + + @Override + public SSLUtil getSSLUtil(SSLHostConfigCertificate certificate) { + return new OpenSSLUtil(certificate); + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLSessionContext.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLSessionContext.java new file mode 100644 index 0000000..d28fafb --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLSessionContext.java @@ -0,0 +1,148 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; + +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.jni.SSLContext; +import org.apache.tomcat.util.res.StringManager; + +/** + * OpenSSL specific {@link SSLSessionContext} implementation. + */ +public class OpenSSLSessionContext implements SSLSessionContext { + private static final StringManager sm = StringManager.getManager(OpenSSLSessionContext.class); + private static final Enumeration EMPTY = new EmptyEnumeration(); + + private final OpenSSLSessionStats stats; + // This is deliberately unused. The reference is retained so that a + // reference chain is established and maintained to the OpenSSLContext while + // there is a connection that is using the OpenSSLContext. Therefore, the + // OpenSSLContext can not be eligible for GC while it is in use. + @SuppressWarnings("unused") + private final OpenSSLContext context; + private final long contextID; + + OpenSSLSessionContext(OpenSSLContext context) { + this.context = context; + this.contextID = context.getSSLContextID(); + stats = new OpenSSLSessionStats(contextID); + } + + @Override + public SSLSession getSession(byte[] bytes) { + return null; + } + + @Override + public Enumeration getIds() { + return EMPTY; + } + + /** + * Sets the SSL session ticket keys of this context. + * + * @param keys The session ticket keys + */ + public void setTicketKeys(byte[] keys) { + if (keys == null) { + throw new IllegalArgumentException(sm.getString("sessionContext.nullTicketKeys")); + } + SSLContext.setSessionTicketKeys(contextID, keys); + } + + /** + * Enable or disable caching of SSL sessions. + * + * @param enabled {@code true} to enable caching, {@code false} to disable + */ + public void setSessionCacheEnabled(boolean enabled) { + long mode = enabled ? SSL.SSL_SESS_CACHE_SERVER : SSL.SSL_SESS_CACHE_OFF; + SSLContext.setSessionCacheMode(contextID, mode); + } + + /** + * @return {@code true} if caching of SSL sessions is enabled, {@code false} + * otherwise. + */ + public boolean isSessionCacheEnabled() { + return SSLContext.getSessionCacheMode(contextID) == SSL.SSL_SESS_CACHE_SERVER; + } + + /** + * @return The statistics for this context. + */ + public OpenSSLSessionStats stats() { + return stats; + } + + @Override + public void setSessionTimeout(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException(); + } + SSLContext.setSessionCacheTimeout(contextID, seconds); + } + + @Override + public int getSessionTimeout() { + return (int) SSLContext.getSessionCacheTimeout(contextID); + } + + @Override + public void setSessionCacheSize(int size) { + if (size < 0) { + throw new IllegalArgumentException(); + } + SSLContext.setSessionCacheSize(contextID, size); + } + + @Override + public int getSessionCacheSize() { + return (int) SSLContext.getSessionCacheSize(contextID); + } + + /** + * Set the context within which session be reused (server side only) + * See + * man SSL_CTX_set_session_id_context + * + * @param sidCtx can be any kind of binary data, it is therefore possible to use e.g. the name + * of the application and/or the hostname and/or service name + * @return {@code true} if success, {@code false} otherwise. + */ + public boolean setSessionIdContext(byte[] sidCtx) { + return SSLContext.setSessionIdContext(contextID, sidCtx); + } + + private static final class EmptyEnumeration implements Enumeration { + @Override + public boolean hasMoreElements() { + return false; + } + + @Override + public byte[] nextElement() { + throw new NoSuchElementException(); + } + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLSessionStats.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLSessionStats.java new file mode 100644 index 0000000..12c44f7 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLSessionStats.java @@ -0,0 +1,126 @@ +/* + * 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.tomcat.util.net.openssl; + +import org.apache.tomcat.jni.SSLContext; + +/** + * Stats exposed by an OpenSSL session context. + * + * @see SSL_CTX_sess_number + */ +public final class OpenSSLSessionStats { + + private final long context; + + OpenSSLSessionStats(long context) { + this.context = context; + } + + /** + * @return The current number of sessions in the internal session cache. + */ + public long number() { + return SSLContext.sessionNumber(context); + } + + /** + * @return The number of started SSL/TLS handshakes in client mode. + */ + public long connect() { + return SSLContext.sessionConnect(context); + } + + /** + * @return The number of successfully established SSL/TLS sessions in client mode. + */ + public long connectGood() { + return SSLContext.sessionConnectGood(context); + } + + /** + * @return The number of start renegotiations in client mode. + */ + public long connectRenegotiate() { + return SSLContext.sessionConnectRenegotiate(context); + } + + /** + * @return The number of started SSL/TLS handshakes in server mode. + */ + public long accept() { + return SSLContext.sessionAccept(context); + } + + /** + * @return The number of successfully established SSL/TLS sessions in server mode. + */ + public long acceptGood() { + return SSLContext.sessionAcceptGood(context); + } + + /** + * @return The number of start renegotiations in server mode. + */ + public long acceptRenegotiate() { + return SSLContext.sessionAcceptRenegotiate(context); + } + + /** + * @return The number of successfully reused sessions. In client mode, a + * session set with {@code SSL_set_session} successfully reused is + * counted as a hit. In server mode, a session successfully + * retrieved from internal or external cache is counted as a hit. + */ + public long hits() { + return SSLContext.sessionHits(context); + } + + /** + * @return The number of successfully retrieved sessions from the external + * session cache in server mode. + */ + public long cbHits() { + return SSLContext.sessionCbHits(context); + } + + /** + * @return The number of sessions proposed by clients that were not found in + * the internal session cache in server mode. + */ + public long misses() { + return SSLContext.sessionMisses(context); + } + + /** + * @return The number of sessions proposed by clients and either found in + * the internal or external session cache in server mode, but that + * were invalid due to timeout. These sessions are not included in + * the {@link #hits()} count. + */ + public long timeouts() { + return SSLContext.sessionTimeouts(context); + } + + /** + * @return The number of sessions that were removed because the maximum + * session cache size was exceeded. + */ + public long cacheFull() { + return SSLContext.sessionCacheFull(context); + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java new file mode 100644 index 0000000..682e878 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java @@ -0,0 +1,69 @@ +/* + * 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.tomcat.util.net.openssl; + +/** + * Holds OpenSSL status without the need to load other classes. + */ +public class OpenSSLStatus { + private static volatile boolean libraryInitialized = false; + private static volatile boolean initialized = false; + private static volatile boolean available = false; + private static volatile boolean useOpenSSL = true; + private static volatile boolean instanceCreated = false; + + + public static boolean isLibraryInitialized() { + return libraryInitialized; + } + + public static boolean isInitialized() { + return initialized; + } + + public static boolean isAvailable() { + return available; + } + + public static boolean getUseOpenSSL() { + return useOpenSSL; + } + + public static boolean isInstanceCreated() { + return instanceCreated; + } + + public static void setLibraryInitialized(boolean libraryInitialized) { + OpenSSLStatus.libraryInitialized = libraryInitialized; + } + + public static void setInitialized(boolean initialized) { + OpenSSLStatus.initialized = initialized; + } + + public static void setAvailable(boolean available) { + OpenSSLStatus.available = available; + } + + public static void setUseOpenSSL(boolean useOpenSSL) { + OpenSSLStatus.useOpenSSL = useOpenSSL; + } + + public static void setInstanceCreated(boolean instanceCreated) { + OpenSSLStatus.instanceCreated = instanceCreated; + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java new file mode 100644 index 0000000..1e5edcd --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java @@ -0,0 +1,138 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.X509KeyManager; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.SSLContext; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLUtilBase; +import org.apache.tomcat.util.net.jsse.JSSEKeyManager; +import org.apache.tomcat.util.res.StringManager; + +public class OpenSSLUtil extends SSLUtilBase { + + private static final Log log = LogFactory.getLog(OpenSSLUtil.class); + private static final StringManager sm = StringManager.getManager(OpenSSLUtil.class); + + + public OpenSSLUtil(SSLHostConfigCertificate certificate) { + super(certificate); + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + protected Set getImplementedProtocols() { + return OpenSSLEngine.IMPLEMENTED_PROTOCOLS_SET; + } + + + @Override + protected Set getImplementedCiphers() { + return OpenSSLEngine.AVAILABLE_CIPHER_SUITES; + } + + + @Override + protected boolean isTls13RenegAuthAvailable() { + // OpenSSL does support authentication after the initial handshake + return true; + } + + + @Override + public SSLContext createSSLContextInternal(List negotiableProtocols) throws Exception { + return new OpenSSLContext(certificate, negotiableProtocols); + } + + @Deprecated + public static X509KeyManager chooseKeyManager(KeyManager[] managers) throws Exception { + return chooseKeyManager(managers, true); + } + + + public static X509KeyManager chooseKeyManager(KeyManager[] managers, boolean throwOnMissing) throws Exception { + if (managers == null) { + return null; + } + for (KeyManager manager : managers) { + if (manager instanceof JSSEKeyManager) { + return (JSSEKeyManager) manager; + } + } + for (KeyManager manager : managers) { + if (manager instanceof X509KeyManager) { + return (X509KeyManager) manager; + } + } + if (throwOnMissing) { + throw new IllegalStateException(sm.getString("openssl.keyManagerMissing")); + } + + log.warn(sm.getString("openssl.keyManagerMissing.warn")); + return null; + } + + + @Override + public KeyManager[] getKeyManagers() throws Exception { + try { + return super.getKeyManagers(); + } catch (IllegalArgumentException e) { + // No (or invalid?) certificate chain was provided for the cert + String msg = sm.getString("openssl.nonJsseChain", certificate.getCertificateChainFile()); + if (log.isDebugEnabled()) { + log.info(msg, e); + } else { + log.info(msg); + } + return null; + } catch (KeyStoreException | IOException e) { + // Depending on what is presented, JSSE may also throw + // KeyStoreException or IOException if it doesn't understand the + // provided file. + if (certificate.getCertificateFile() != null) { + String msg = sm.getString("openssl.nonJsseCertificate", + certificate.getCertificateFile(), certificate.getCertificateKeyFile()); + if (log.isDebugEnabled()) { + log.info(msg, e); + } else { + log.info(msg); + } + // Assume JSSE processing of the certificate failed, try again with OpenSSL + // without a key manager + return null; + } + throw e; + } + } + +} diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLX509Certificate.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLX509Certificate.java new file mode 100644 index 0000000..fb15ab7 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLX509Certificate.java @@ -0,0 +1,192 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Set; + +final class OpenSSLX509Certificate extends X509Certificate { + + private final byte[] bytes; + private X509Certificate wrapped; + + OpenSSLX509Certificate(byte[] bytes) { + this.bytes = bytes; + } + + @Override + public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException { + unwrap().checkValidity(); + } + + @Override + public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException { + unwrap().checkValidity(date); + } + + @Override + public int getVersion() { + return unwrap().getVersion(); + } + + @Override + public BigInteger getSerialNumber() { + return unwrap().getSerialNumber(); + } + + @Override + @Deprecated + public Principal getIssuerDN() { + return unwrap().getIssuerDN(); + } + + @Override + @Deprecated + public Principal getSubjectDN() { + return unwrap().getSubjectDN(); + } + + @Override + public Date getNotBefore() { + return unwrap().getNotBefore(); + } + + @Override + public Date getNotAfter() { + return unwrap().getNotAfter(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return unwrap().getTBSCertificate(); + } + + @Override + public byte[] getSignature() { + return unwrap().getSignature(); + } + + @Override + public String getSigAlgName() { + return unwrap().getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return unwrap().getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return unwrap().getSigAlgParams(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return unwrap().getIssuerUniqueID(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return unwrap().getSubjectUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return unwrap().getKeyUsage(); + } + + @Override + public int getBasicConstraints() { + return unwrap().getBasicConstraints(); + } + + @Override + public byte[] getEncoded() { + return bytes.clone(); + } + + @Override + public void verify(PublicKey key) + throws CertificateException, NoSuchAlgorithmException, + InvalidKeyException, NoSuchProviderException, SignatureException { + unwrap().verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, SignatureException { + unwrap().verify(key, sigProvider); + } + + @Override + public String toString() { + return unwrap().toString(); + } + + @Override + public PublicKey getPublicKey() { + return unwrap().getPublicKey(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return unwrap().hasUnsupportedCriticalExtension(); + } + + @Override + public Set getCriticalExtensionOIDs() { + return unwrap().getCriticalExtensionOIDs(); + } + + @Override + public Set getNonCriticalExtensionOIDs() { + return unwrap().getNonCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return unwrap().getExtensionValue(oid); + } + + private X509Certificate unwrap() { + X509Certificate wrapped = this.wrapped; + if (wrapped == null) { + try { + wrapped = this.wrapped = (X509Certificate) OpenSSLContext.X509_CERT_FACTORY.generateCertificate( + new ByteArrayInputStream(bytes)); + } catch (CertificateException e) { + throw new IllegalStateException(e); + } + } + return wrapped; + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/Authentication.java b/java/org/apache/tomcat/util/net/openssl/ciphers/Authentication.java new file mode 100644 index 0000000..a465248 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/Authentication.java @@ -0,0 +1,33 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +public enum Authentication { + RSA /* RSA auth */, + DSS /* DSS auth */, + aNULL /* no auth (i.e. use ADH or AECDH) */, + DH /* Fixed DH auth (kDHd or kDHr) */, + ECDH /* Fixed ECDH auth (kECDHe or kECDHr) */, + KRB5 /* KRB5 auth */, + ECDSA /* ECDSA auth*/, + PSK /* PSK auth */, + GOST94 /* GOST R 34.10-94 signature auth */, + GOST01 /* GOST R 34.10-2001 */, + FZA /* Fortezza */, + SRP /* Secure Remote Password */, + ANY /* TLS 1.3 */ +} diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/Cipher.java b/java/org/apache/tomcat/util/net/openssl/ciphers/Cipher.java new file mode 100644 index 0000000..a8c9686 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/Cipher.java @@ -0,0 +1,5117 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * All the standard cipher suites for SSL/TSL. + * + * @see OpenSSL cipher definitions + * @see The cipher suite registry + * @see Another list of cipher suites with some non-standard IDs + * @see Oracle standard names for cipher suites + * @see Mapping of OpenSSL cipher suites names to registry names + * @see SSL Labs tool - list of ciphers + * @see OpenJDK source code + */ +public enum Cipher { + + /* Cipher 0 + * TLS_NULL_WITH_NULL_NULL + * Must never be negotiated. Used internally to represent the initial + * unprotected state of a connection. + */ + + /* The RSA ciphers */ + // Cipher 01 + TLS_RSA_WITH_NULL_MD5( + 0x0001, + "NULL-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.eNULL, + MessageDigest.MD5, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + false, + 0, + 0, + new String[] {"SSL_RSA_WITH_NULL_MD5"}, + null + ), + // Cipher 02 + TLS_RSA_WITH_NULL_SHA( + 0x0002, + "NULL-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + new String[] {"SSL_RSA_WITH_NULL_SHA"}, + null + ), + // Cipher 03 + TLS_RSA_EXPORT_WITH_RC4_40_MD5( + 0x0003, + "EXP-RC4-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC4, + MessageDigest.MD5, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + new String[] {"SSL_RSA_EXPORT_WITH_RC4_40_MD5"}, + null + ), + // Cipher 04 + TLS_RSA_WITH_RC4_128_MD5( + 0x0004, + "RC4-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC4, + MessageDigest.MD5, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + new String[] {"SSL_RSA_WITH_RC4_128_MD5"}, + null + ), + // Cipher 05 + TLS_RSA_WITH_RC4_128_SHA( + 0x0005, + "RC4-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + new String[] {"SSL_RSA_WITH_RC4_128_SHA"}, + null + ), + // Cipher 06 + TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5( + 0x0006, + "EXP-RC2-CBC-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC2, + MessageDigest.MD5, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + new String[] {"SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5"}, + null + ), + // Cipher 07 + TLS_RSA_WITH_IDEA_CBC_SHA( + 0x0007, + "IDEA-CBC-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.IDEA, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + new String[] {"SSL_RSA_WITH_IDEA_CBC_SHA"}, + null + ), + // Cipher 08 + TLS_RSA_EXPORT_WITH_DES40_CBC_SHA( + 0x0008, + "EXP-DES-CBC-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 56, + new String[] {"SSL_RSA_EXPORT_WITH_DES40_CBC_SHA"}, + null + ), + // Cipher 09 + TLS_RSA_WITH_DES_CBC_SHA( + 0x0009, + "DES-CBC-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + new String[] {"SSL_RSA_WITH_DES_CBC_SHA"}, + null + ), + // Cipher 0A + TLS_RSA_WITH_3DES_EDE_CBC_SHA( + 0x000A, + "DES-CBC3-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + new String[] {"SSL_RSA_WITH_3DES_EDE_CBC_SHA"}, + null + ), + /* The DH ciphers */ + // Cipher 0B + TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA( + 0x000B, + "EXP-DH-DSS-DES-CBC-SHA", + KeyExchange.DHd, + Authentication.DH, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 56, + new String[] {"SSL_DH_DSS_EXPORT_WITH_DES40_CBC_SHA"}, + null + ), + // Cipher 0C + TLS_DH_DSS_WITH_DES_CBC_SHA( + 0x000C, + "DH-DSS-DES-CBC-SHA", + KeyExchange.DHd, + Authentication.DH, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + new String[] {"SSL_DH_DSS_WITH_DES_CBC_SHA"}, + null + ), + // Cipher 0D + TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA( + 0x000D, + "DH-DSS-DES-CBC3-SHA", + KeyExchange.DHd, + Authentication.DH, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + new String[] {"SSL_DH_DSS_WITH_3DES_EDE_CBC_SHA"}, + null + ), + // Cipher 0E + TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA( + 0x000E, + "EXP-DH-RSA-DES-CBC-SHA", + KeyExchange.DHr, + Authentication.DH, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 56, + new String[] {"SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA"}, + null + ), + // Cipher 0F + TLS_DH_RSA_WITH_DES_CBC_SHA( + 0x000F, + "DH-RSA-DES-CBC-SHA", + KeyExchange.DHr, + Authentication.DH, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + new String[] {"SSL_DH_RSA_WITH_DES_CBC_SHA"}, + null + ), + // Cipher 10 + TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA( + 0x0010, + "DH-RSA-DES-CBC3-SHA", + KeyExchange.DHr, + Authentication.DH, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + new String[] {"SSL_DH_RSA_WITH_3DES_EDE_CBC_SHA"}, + null + ), + /* The Ephemeral DH ciphers */ + // Cipher 11 + TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA( + 0x0011, + "EXP-DHE-DSS-DES-CBC-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 56, + new String[] {"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA"}, + new String[] {"EXP-EDH-DSS-DES-CBC-SHA"} + ), + // Cipher 12 + TLS_DHE_DSS_WITH_DES_CBC_SHA( + 0x0012, + "DHE-DSS-DES-CBC-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + new String[] {"SSL_DHE_DSS_WITH_DES_CBC_SHA"}, + new String[] {"EDH-DSS-DES-CBC-SHA"} + ), + // Cipher 13 + TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA( + 0x0013, + "DHE-DSS-DES-CBC3-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + new String[] {"SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA"}, + new String[] {"EDH-DSS-DES-CBC3-SHA"} + ), + // Cipher 14 + TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA( + 0x0014, + "EXP-DHE-RSA-DES-CBC-SHA", + KeyExchange.EDH, + Authentication.RSA, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 56, + new String[] {"SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA"}, + new String[] {"EXP-EDH-RSA-DES-CBC-SHA"} + ), + // Cipher 15 + TLS_DHE_RSA_WITH_DES_CBC_SHA( + 0x0015, + "DHE-RSA-DES-CBC-SHA", + KeyExchange.EDH, + Authentication.RSA, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + new String[] {"SSL_DHE_RSA_WITH_DES_CBC_SHA"}, + new String[] {"EDH-RSA-DES-CBC-SHA"} + ), + // Cipher 16 + TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA( + 0x0016, + "DHE-RSA-DES-CBC3-SHA", + KeyExchange.EDH, + Authentication.RSA, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + new String[] {"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA"}, + new String[] {"EDH-RSA-DES-CBC3-SHA"} + ), + // Cipher 17 + TLS_DH_anon_EXPORT_WITH_RC4_40_MD5( + 0x0017, + "EXP-ADH-RC4-MD5", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.RC4, + MessageDigest.MD5, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + new String[] {"SSL_DH_anon_EXPORT_WITH_RC4_40_MD5"}, + null + ), + // Cipher 18 + TLS_DH_anon_WITH_RC4_128_MD5( + 0x0018, + "ADH-RC4-MD5", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.RC4, + MessageDigest.MD5, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + new String[] {"SSL_DH_anon_WITH_RC4_128_MD5"}, + null + ), + // Cipher 19 + TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA( + 0x0019, + "EXP-ADH-DES-CBC-SHA", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + new String[] {"SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"}, + null + ), + // Cipher 1A + TLS_DH_anon_WITH_DES_CBC_SHA( + 0x001A, + "ADH-DES-CBC-SHA", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + new String[] {"SSL_DH_anon_WITH_DES_CBC_SHA"}, + null + ), + // Cipher 1B + TLS_DH_anon_WITH_3DES_EDE_CBC_SHA( + 0x001B, + "ADH-DES-CBC3-SHA", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + new String[] {"SSL_DH_anon_WITH_3DES_EDE_CBC_SHA"}, + null + ), + /* Fortezza ciphersuite from SSL 3.0 spec + * Neither OpenSSL nor Java implement these ciphers and the IDs used + * overlap partially with the IDs used by the Kerberos ciphers + // Cipher 1C + SSL_FORTEZZA_DMS_WITH_NULL_SHA( + "FZA-NULL-SHA", + KeyExchange.FZA, + Authentication.FZA, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + false, + 0, + 0, + null, + null + ), + // Cipher 1D + SSL_FORTEZZA_DMS_WITH_FORTEZZA_CBC_SHA( + "FZA-FZA-CBC-SHA", + KeyExchange.FZA, + Authentication.FZA, + Encryption.FZA, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + false, + 0, + 0, + null, + null + ), + // Cipher 1E - overlaps with Kerberos below + SSL_FORTEZZA_DMS_WITH_RC4_128_SHA( + "FZA-RC4-SHA", + KeyExchange.FZA, + Authentication.FZA, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + */ + /* The Kerberos ciphers. OpenSSL doesn't support these. Java does but they + * are used for Kerberos authentication. + */ + // Cipher 1E - overlaps with Fortezza above + /*TLS_KRB5_WITH_DES_CBC_SHA( + "KRB5-DES-CBC-SHA", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + null, + null + ), + // Cipher 1F + TLS_KRB5_WITH_3DES_EDE_CBC_SHA( + "KRB5-DES-CBC3-SHA", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 112, + 168, + null, + null + ), + // Cipher 20 + TLS_KRB5_WITH_RC4_128_SHA( + "KRB5-RC4-SHA", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 21 + TLS_KRB5_WITH_IDEA_CBC_SHA( + "KRB5-IDEA-CBC-SHA", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.IDEA, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 22 + TLS_KRB5_WITH_DES_CBC_MD5( + "KRB5-DES-CBC-MD5", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.DES, + MessageDigest.MD5, + Protocol.SSLv3, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + null, + null + ), + // Cipher 23 + TLS_KRB5_WITH_3DES_EDE_CBC_MD5( + "KRB5-DES-CBC3-MD5", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.TRIPLE_DES, + MessageDigest.MD5, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 112, + 168, + null, + null + ), + // Cipher 24 + TLS_KRB5_WITH_RC4_128_MD5( + "KRB5-RC4-MD5", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.RC4, + MessageDigest.MD5, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 25 + TLS_KRB5_WITH_IDEA_CBC_MD5( + "KRB5-IDEA-CBC-MD5", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.IDEA, + MessageDigest.MD5, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 26 + TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA( + "EXP-KRB5-DES-CBC-SHA", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.DES, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 56, + null, + null + ), + // Cipher 27 + TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA( + "EXP-KRB5-RC2-CBC-SHA", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.RC2, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + null, + null + ), + // Cipher 28 + TLS_KRB5_EXPORT_WITH_RC4_40_SHA( + "EXP-KRB5-RC4-SHA", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + null, + null + ), + // Cipher 29 + TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5( + "EXP-KRB5-DES-CBC-MD5", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.DES, + MessageDigest.MD5, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 56, + null, + null + ), + // Cipher 2A + TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5( + "EXP-KRB5-RC2-CBC-MD5", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.RC2, + MessageDigest.MD5, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + null, + null + ), + // Cipher 2B + TLS_KRB5_EXPORT_WITH_RC4_40_MD5( + "EXP-KRB5-RC4-MD5", + KeyExchange.KRB5, + Authentication.KRB5, + Encryption.RC4, + MessageDigest.MD5, + Protocol.SSLv3, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + null, + null + ),*/ + + /* PSK cipher suites from RFC 4785 */ + // Cipher 2C + TLS_PSK_WITH_NULL_SHA( + 0x002c, + "PSK-NULL-SHA", + KeyExchange.PSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher 2D + TLS_DHE_PSK_WITH_NULL_SHA( + 0x002d, + "DHE-PSK-NULL-SHA", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher 2E + TLS_RSA_PSK_WITH_NULL_SHA( + 0x002e, + "RSA-PSK-NULL-SHA", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + /* New AES ciphersuites */ + // Cipher 2F + TLS_RSA_WITH_AES_128_CBC_SHA( + 0x002f, + "AES128-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 30 + TLS_DH_DSS_WITH_AES_128_CBC_SHA( + 0x0030, + "DH-DSS-AES128-SHA", + KeyExchange.DHd, + Authentication.DH, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 31 + TLS_DH_RSA_WITH_AES_128_CBC_SHA( + 0x0031, + "DH-RSA-AES128-SHA", + KeyExchange.DHr, + Authentication.DH, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 32 + TLS_DHE_DSS_WITH_AES_128_CBC_SHA( + 0x0032, + "DHE-DSS-AES128-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 33 + TLS_DHE_RSA_WITH_AES_128_CBC_SHA( + 0x0033, + "DHE-RSA-AES128-SHA", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 34 + TLS_DH_anon_WITH_AES_128_CBC_SHA( + 0x0034, + "ADH-AES128-SHA", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 35 + TLS_RSA_WITH_AES_256_CBC_SHA( + 0x0035, + "AES256-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 36 + TLS_DH_DSS_WITH_AES_256_CBC_SHA( + 0x0036, + "DH-DSS-AES256-SHA", + KeyExchange.DHd, + Authentication.DH, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 37 + TLS_DH_RSA_WITH_AES_256_CBC_SHA( + 0x0037, + "DH-RSA-AES256-SHA", + KeyExchange.DHr, + Authentication.DH, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 38 + TLS_DHE_DSS_WITH_AES_256_CBC_SHA( + 0x0038, + "DHE-DSS-AES256-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 39 + TLS_DHE_RSA_WITH_AES_256_CBC_SHA( + 0x0039, + "DHE-RSA-AES256-SHA", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 3A + TLS_DH_anon_WITH_AES_256_CBC_SHA( + 0x003A, + "ADH-AES256-SHA", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + /* TLS v1.2 ciphersuites */ + // Cipher 3B + TLS_RSA_WITH_NULL_SHA256( + 0x003B, + "NULL-SHA256", + KeyExchange.RSA, + Authentication.RSA, + Encryption.eNULL, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher 3C + TLS_RSA_WITH_AES_128_CBC_SHA256( + 0x003C, + "AES128-SHA256", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 3D + TLS_RSA_WITH_AES_256_CBC_SHA256( + 0x003D, + "AES256-SHA256", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 3E + TLS_DH_DSS_WITH_AES_128_CBC_SHA256( + 0x003E, + "DH-DSS-AES128-SHA256", + KeyExchange.DHd, + Authentication.DH, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 3F + TLS_DH_RSA_WITH_AES_128_CBC_SHA256( + 0x003F, + "DH-RSA-AES128-SHA256", + KeyExchange.DHr, + Authentication.DH, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 40 + TLS_DHE_DSS_WITH_AES_128_CBC_SHA256( + 0x0040, + "DHE-DSS-AES128-SHA256", + KeyExchange.EDH, + Authentication.DSS, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + /* Camellia ciphersuites from RFC4132 ( + 128-bit portion) */ + // Cipher 41 + TLS_RSA_WITH_CAMELLIA_128_CBC_SHA( + 0x0041, + "CAMELLIA128-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.CAMELLIA128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher 42 + TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA( + 0x0042, + "DH-DSS-CAMELLIA128-SHA", + KeyExchange.DHd, + Authentication.DH, + Encryption.CAMELLIA128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher 43 + TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA( + 0x0043, + "DH-RSA-CAMELLIA128-SHA", + KeyExchange.DHr, + Authentication.DH, + Encryption.CAMELLIA128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher 44 + TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA( + 0x0044, + "DHE-DSS-CAMELLIA128-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.CAMELLIA128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher 45 + TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA( + 0x0045, + "DHE-RSA-CAMELLIA128-SHA", + KeyExchange.EDH, + Authentication.RSA, + Encryption.CAMELLIA128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher 46 + TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA( + 0x0046, + "ADH-CAMELLIA128-SHA", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.CAMELLIA128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + /* Experimental (and now expired) TLSv1 versions of SSLv3 ciphers. + * Unsupported by Java and OpenSSL 1.1.x onwards. Some earlier OpenSSL + * versions do support these. */ + // Cipher 60 + TLS_RSA_EXPORT1024_WITH_RC4_56_MD5( + 0x0060, + "EXP1024-RC4-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC4, + MessageDigest.MD5, + Protocol.TLSv1, + true, + EncryptionLevel.EXP56, + false, + 56, + 128, + new String[] {"SSL_RSA_EXPORT1024_WITH_RC4_56_MD5"}, + null + ), + // Cipher 61 + TLS_RSA_EXPORT1024_WITH_RC2_CBC_56_MD5( + 0x0061, + "EXP1024-RC2-CBC-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC2, + MessageDigest.MD5, + Protocol.TLSv1, + true, + EncryptionLevel.EXP56, + false, + 56, + 128, + new String[] {"SSL_RSA_EXPORT1024_WITH_RC2_CBC_56_MD5"}, + null + ), + // Cipher 62 + TLS_RSA_EXPORT1024_WITH_DES_CBC_SHA( + 0x0062, + "EXP1024-DES-CBC-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.DES, + MessageDigest.SHA1, + Protocol.TLSv1, + true, + EncryptionLevel.EXP56, + false, + 56, + 56, + new String[] {"SSL_RSA_EXPORT1024_WITH_DES_CBC_SHA"}, + null + ), + // Cipher 63 + TLS_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA( + 0x0063, + "EXP1024-DHE-DSS-DES-CBC-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.DES, + MessageDigest.SHA1, + Protocol.TLSv1, + true, + EncryptionLevel.EXP56, + false, + 56, + 56, + new String[] {"SSL_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA"}, + null + ), + // Cipher 64 + TLS_RSA_EXPORT1024_WITH_RC4_56_SHA( + 0x0064, + "EXP1024-RC4-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.TLSv1, + true, + EncryptionLevel.EXP56, + false, + 56, + 128, + new String[] {"SSL_RSA_EXPORT1024_WITH_RC4_56_SHA"}, + null + ), + // Cipher 65 + TLS_DHE_DSS_EXPORT1024_WITH_RC4_56_SHA( + 0x0065, + "EXP1024-DHE-DSS-RC4-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.TLSv1, + true, + EncryptionLevel.EXP56, + false, + 56, + 128, + new String[] {"SSL_DHE_DSS_EXPORT1024_WITH_RC4_56_SHA"}, + null + ), + // Cipher 66 + TLS_DHE_DSS_WITH_RC4_128_SHA( + 0x0066, + "DHE-DSS-RC4-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + new String[] {"SSL_DHE_DSS_WITH_RC4_128_SHA"}, + null + ), + + /* TLS v1.2 ciphersuites */ + // Cipher 67 + TLS_DHE_RSA_WITH_AES_128_CBC_SHA256( + 0x0067, + "DHE-RSA-AES128-SHA256", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 68 + TLS_DH_DSS_WITH_AES_256_CBC_SHA256( + 0x0068, + "DH-DSS-AES256-SHA256", + KeyExchange.DHd, + Authentication.DH, + Encryption.AES256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 69 + TLS_DH_RSA_WITH_AES_256_CBC_SHA256( + 0x0069, + "DH-RSA-AES256-SHA256", + KeyExchange.DHr, + Authentication.DH, + Encryption.AES256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 6A + TLS_DHE_DSS_WITH_AES_256_CBC_SHA256( + 0x006A, + "DHE-DSS-AES256-SHA256", + KeyExchange.EDH, + Authentication.DSS, + Encryption.AES256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 6B + TLS_DHE_RSA_WITH_AES_256_CBC_SHA256( + 0x006B, + "DHE-RSA-AES256-SHA256", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 6C + TLS_DH_anon_WITH_AES_128_CBC_SHA256( + 0x006C, + "ADH-AES128-SHA256", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 6D + TLS_DH_anon_WITH_AES_256_CBC_SHA256( + 0x006D, + "ADH-AES256-SHA256", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.AES256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + /* GOST Ciphersuites. Unsupported by Java. OpenSSL lists them with IDs + * 0x3000080 to 0x3000083 + * The ciphers are not listed in the IANA registry. */ + /* + // Cipher 80 + TLS_GOSTR341094_WITH_28147_CNT_IMIT( + "GOST94-GOST89-GOST89", + KeyExchange.GOST, + Authentication.GOST94, + Encryption.eGOST2814789CNT, + MessageDigest.GOST89MAC, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher 81 + TLS_GOSTR341001_WITH_28147_CNT_IMIT( + "GOST2001-GOST89-GOST89", + KeyExchange.GOST, + Authentication.GOST01, + Encryption.eGOST2814789CNT, + MessageDigest.GOST89MAC, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher 82 + TLS_GOSTR341094_WITH_NULL_GOSTR3411( + "GOST94-NULL-GOST94", + KeyExchange.GOST, + Authentication.GOST94, + Encryption.eNULL, + MessageDigest.GOST94, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + false, + 0, + 0, + null, + null + ), + // Cipher 83 + TLS_GOSTR341001_WITH_NULL_GOSTR3411( + "GOST2001-NULL-GOST94", + KeyExchange.GOST, + Authentication.GOST01, + Encryption.eNULL, + MessageDigest.GOST94, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + false, + 0, + 0, + null, + null + ),*/ + /* Camellia ciphersuites from RFC4132 ( + 256-bit portion) */ + // Cipher 84 + TLS_RSA_WITH_CAMELLIA_256_CBC_SHA( + 0x0084, + "CAMELLIA256-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.CAMELLIA256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher 85 + TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA( + 0x0085, + "DH-DSS-CAMELLIA256-SHA", + KeyExchange.DHd, + Authentication.DH, + Encryption.CAMELLIA256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher 86 + TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA( + 0x0086, + "DH-RSA-CAMELLIA256-SHA", + KeyExchange.DHr, + Authentication.DH, + Encryption.CAMELLIA256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher 87 + TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA( + 0x0087, + "DHE-DSS-CAMELLIA256-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.CAMELLIA256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher 88 + TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA( + 0x0088, + "DHE-RSA-CAMELLIA256-SHA", + KeyExchange.EDH, + Authentication.RSA, + Encryption.CAMELLIA256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher 89 + TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA( + 0x0089, + "ADH-CAMELLIA256-SHA", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.CAMELLIA256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher 8A + TLS_PSK_WITH_RC4_128_SHA( + 0x008A, + "PSK-RC4-SHA", + KeyExchange.PSK, + Authentication.PSK, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 8B + TLS_PSK_WITH_3DES_EDE_CBC_SHA( + 0x008B, + "PSK-3DES-EDE-CBC-SHA", + KeyExchange.PSK, + Authentication.PSK, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher 8C + TLS_PSK_WITH_AES_128_CBC_SHA( + 0x008C, + "PSK-AES128-CBC-SHA", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 8D + TLS_PSK_WITH_AES_256_CBC_SHA( + 0x008D, + "PSK-AES256-CBC-SHA", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 8E + TLS_DHE_PSK_WITH_RC4_128_SHA( + 0x008E, + "DHE-PSK-RC4-SHA", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 8F + TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA( + 0x008F, + "DHE-PSK-3DES-EDE-CBC-SHA", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher 90 + TLS_DHE_PSK_WITH_AES_128_CBC_SHA( + 0x0090, + "DHE-PSK-AES128-CBC-SHA", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 91 + TLS_DHE_PSK_WITH_AES_256_CBC_SHA( + 0x0091, + "DHE-PSK-AES256-CBC-SHA", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 92 + TLS_RSA_PSK_WITH_RC4_128_SHA( + 0x0092, + "RSA-PSK-RC4-SHA", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 93 + TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA( + 0x0093, + "RSA-PSK-3DES-EDE-CBC-SHA", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher 94 + TLS_RSA_PSK_WITH_AES_128_CBC_SHA( + 0x0094, + "RSA-PSK-AES128-CBC-SHA", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 95 + TLS_RSA_PSK_WITH_AES_256_CBC_SHA( + 0x0095, + "RSA-PSK-AES256-CBC-SHA", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + /* SEED ciphersuites from RFC4162 */ + // Cipher 96 + TLS_RSA_WITH_SEED_CBC_SHA( + 0x0096, + "SEED-SHA", + KeyExchange.RSA, + Authentication.RSA, + Encryption.SEED, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 97 + TLS_DH_DSS_WITH_SEED_CBC_SHA( + 0x0097, + "DH-DSS-SEED-SHA", + KeyExchange.DHd, + Authentication.DH, + Encryption.SEED, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 98 + TLS_DH_RSA_WITH_SEED_CBC_SHA( + 0x0098, + "DH-RSA-SEED-SHA", + KeyExchange.DHr, + Authentication.DH, + Encryption.SEED, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 99 + TLS_DHE_DSS_WITH_SEED_CBC_SHA( + 0x0099, + "DHE-DSS-SEED-SHA", + KeyExchange.EDH, + Authentication.DSS, + Encryption.SEED, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 9A + TLS_DHE_RSA_WITH_SEED_CBC_SHA( + 0x009A, + "DHE-RSA-SEED-SHA", + KeyExchange.EDH, + Authentication.RSA, + Encryption.SEED, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 9B + TLS_DH_anon_WITH_SEED_CBC_SHA( + 0x009B, + "ADH-SEED-SHA", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.SEED, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + /* GCM ciphersuites from RFC5288 */ + // Cipher 9C + TLS_RSA_WITH_AES_128_GCM_SHA256( + 0x009C, + "AES128-GCM-SHA256", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 9D + TLS_RSA_WITH_AES_256_GCM_SHA384( + 0x009D, + "AES256-GCM-SHA384", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 9E + TLS_DHE_RSA_WITH_AES_128_GCM_SHA256( + 0x009E, + "DHE-RSA-AES128-GCM-SHA256", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 9F + TLS_DHE_RSA_WITH_AES_256_GCM_SHA384( + 0x009F, + "DHE-RSA-AES256-GCM-SHA384", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher A0 + TLS_DH_RSA_WITH_AES_128_GCM_SHA256( + 0x00A0, + "DH-RSA-AES128-GCM-SHA256", + KeyExchange.DHr, + Authentication.DH, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher A1 + TLS_DH_RSA_WITH_AES_256_GCM_SHA384( + 0x00A1, + "DH-RSA-AES256-GCM-SHA384", + KeyExchange.DHr, + Authentication.DH, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher A2 + TLS_DHE_DSS_WITH_AES_128_GCM_SHA256( + 0x00A2, + "DHE-DSS-AES128-GCM-SHA256", + KeyExchange.EDH, + Authentication.DSS, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher A3 + TLS_DHE_DSS_WITH_AES_256_GCM_SHA384( + 0x00A3, + "DHE-DSS-AES256-GCM-SHA384", + KeyExchange.EDH, + Authentication.DSS, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher A4 + TLS_DH_DSS_WITH_AES_128_GCM_SHA256( + 0x00A4, + "DH-DSS-AES128-GCM-SHA256", + KeyExchange.DHd, + Authentication.DH, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher A5 + TLS_DH_DSS_WITH_AES_256_GCM_SHA384( + 0x00A5, + "DH-DSS-AES256-GCM-SHA384", + KeyExchange.DHd, + Authentication.DH, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher A6 + TLS_DH_anon_WITH_AES_128_GCM_SHA256( + 0x00A6, + "ADH-AES128-GCM-SHA256", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher A7 + TLS_DH_anon_WITH_AES_256_GCM_SHA384( + 0x00A7, + "ADH-AES256-GCM-SHA384", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher A8 + TLS_PSK_WITH_AES_128_GCM_SHA256( + 0x00A8, + "PSK-AES128-GCM-SHA256", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher A9 + TLS_PSK_WITH_AES_256_GCM_SHA384( + 0x00A9, + "PSK-AES256-GCM-SHA384", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher AA + TLS_DHE_PSK_WITH_AES_128_GCM_SHA256( + 0x00AA, + "DHE-PSK-AES128-GCM-SHA256", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher AB + TLS_DHE_PSK_WITH_AES_256_GCM_SHA384( + 0x00AB, + "DHE-PSK-AES256-GCM-SHA384", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher AC + TLS_RSA_PSK_WITH_AES_128_GCM_SHA256( + 0x00AC, + "RSA-PSK-AES128-GCM-SHA256", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher AD + TLS_RSA_PSK_WITH_AES_256_GCM_SHA384( + 0x00AD, + "RSA-PSK-AES256-GCM-SHA384", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher AE + TLS_PSK_WITH_AES_128_CBC_SHA256 ( + 0x00AE, + "PSK-AES128-CBC-SHA256", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher AF + TLS_PSK_WITH_AES_256_CBC_SHA384 ( + 0x00AF, + "PSK-AES256-CBC-SHA384", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES256, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher B0 + TLS_PSK_WITH_NULL_SHA256 ( + 0x00B0, + "PSK-NULL-SHA256", + KeyExchange.PSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher B1 + TLS_PSK_WITH_NULL_SHA384 ( + 0x00B1, + "PSK-NULL-SHA384", + KeyExchange.PSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher B2 + TLS_DHE_PSK_WITH_AES_128_CBC_SHA256( + 0x00B2, + "DHE-PSK-AES128-CBC-SHA256", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher B3 + TLS_DHE_PSK_WITH_AES_256_CBC_SHA384( + 0x00B3, + "DHE-PSK-AES256-CBC-SHA384", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES256, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher B4 + TLS_DHE_PSK_WITH_NULL_SHA256 ( + 0x00B4, + "DHE-PSK-NULL-SHA256", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher B5 + TLS_DHE_PSK_WITH_NULL_SHA384 ( + 0x00B5, + "DHE-PSK-NULL-SHA384", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher B6 + TLS_RSA_PSK_WITH_AES_128_CBC_SHA256( + 0x00B6, + "RSA-PSK-AES128-CBC-SHA256", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher B7 + TLS_RSA_PSK_WITH_AES_256_CBC_SHA384( + 0x00B7, + "RSA-PSK-AES256-CBC-SHA384", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher B8 + TLS_RSA_PSK_WITH_NULL_SHA256 ( + 0x00B8, + "RSA-PSK-NULL-SHA256", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.eNULL, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher B9 + TLS_RSA_PSK_WITH_NULL_SHA384 ( + 0x00B9, + "RSA-PSK-NULL-SHA384", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.eNULL, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + + // Cipher BA + TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256( + 0x00BA, + "CAMELLIA128-SHA256", + KeyExchange.RSA, + Authentication.RSA, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher BB + TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256( + 0x00BB, + "DH-DSS-CAMELLIA128-SHA256", + KeyExchange.DHd, + Authentication.DH, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher BC + TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256( + 0x00BC, + "DH-RSA-CAMELLIA128-SHA256", + KeyExchange.DHr, + Authentication.DH, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher BD + TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256( + 0x00BD, + "DHE-DSS-CAMELLIA128-SHA256", + KeyExchange.EDH, + Authentication.DSS, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher BE + TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256( + 0x00BE, + "DHE-RSA-CAMELLIA128-SHA256", + KeyExchange.EDH, + Authentication.RSA, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher BF + TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256( + 0x00BF, + "ADH-CAMELLIA128-SHA256", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C0 + TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256( + 0x00C0, + "CAMELLIA256-SHA256", + KeyExchange.RSA, + Authentication.RSA, + Encryption.CAMELLIA256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C1 + TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256( + 0x00C1, + "DH-DSS-CAMELLIA256-SHA256", + KeyExchange.DHd, + Authentication.DH, + Encryption.CAMELLIA256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C2 + TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256( + 0x00C2, + "DH-RSA-CAMELLIA256-SHA256", + KeyExchange.DHr, + Authentication.DH, + Encryption.CAMELLIA256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C3 + TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256( + 0x00C3, + "DHE-DSS-CAMELLIA256-SHA256", + KeyExchange.EDH, + Authentication.DSS, + Encryption.CAMELLIA256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C4 + TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256( + 0x00C4, + "DHE-RSA-CAMELLIA256-SHA256", + KeyExchange.EDH, + Authentication.RSA, + Encryption.CAMELLIA256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C5 + TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256( + 0x00C5, + "ADH-CAMELLIA256-SHA256", + KeyExchange.EDH, + Authentication.aNULL, + Encryption.CAMELLIA256, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + + // Cipher 0x00FF TLS_EMPTY_RENEGOTIATION_INFO_SCSV + + // TLS 1.3 ciphers (draft - v26) + // Cipher 1301 + TLS_AES_128_GCM_SHA256( + 0x1301, + "TLS_AES_128_GCM_SHA256", + KeyExchange.ANY, + Authentication.ANY, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 1302 + TLS_AES_256_GCM_SHA384( + 0x1302, + "TLS_AES_256_GCM_SHA384", + KeyExchange.ANY, + Authentication.ANY, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 1303 + TLS_CHACHA20_POLY1305_SHA256( + 0x1303, + "TLS_CHACHA20_POLY1305_SHA256", + KeyExchange.ANY, + Authentication.ANY, + Encryption.CHACHA20POLY1305, + MessageDigest.AEAD, + Protocol.TLSv1_3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher 1304 + TLS_AES_128_CCM_SHA256( + 0x1304, + "TLS_AES_128_CCM_SHA256", + KeyExchange.ANY, + Authentication.ANY, + Encryption.AES128CCM, + MessageDigest.AEAD, + Protocol.TLSv1_3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher 1305 + TLS_AES_128_CCM_8_SHA256( + 0x1305, + "TLS_AES_128_CCM_8_SHA256", + KeyExchange.ANY, + Authentication.ANY, + Encryption.AES128CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_3, + false, + EncryptionLevel.MEDIUM, + true, + 128, + 128, + null, + null + ), + + /* + * Cipher 0x5600 TLS_FALLBACK_SCSV + * + * No other ciphers defined until 0xC001 below + */ + + /* ECC ciphersuites from draft-ietf-tls-ecc-01.txt ( + Mar 15, 2001) */ + // Cipher C001 + TLS_ECDH_ECDSA_WITH_NULL_SHA( + 0xC001, + "ECDH-ECDSA-NULL-SHA", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher C002 + TLS_ECDH_ECDSA_WITH_RC4_128_SHA( + 0xC002, + "ECDH-ECDSA-RC4-SHA", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C003 + TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA( + 0xC003, + "ECDH-ECDSA-DES-CBC3-SHA", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher C004 + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA( + 0xC004, + "ECDH-ECDSA-AES128-SHA", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C005 + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA( + 0xC005, + "ECDH-ECDSA-AES256-SHA", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C006 + TLS_ECDHE_ECDSA_WITH_NULL_SHA( + 0xC006, + "ECDHE-ECDSA-NULL-SHA", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher C007 + TLS_ECDHE_ECDSA_WITH_RC4_128_SHA( + 0xC007, + "ECDHE-ECDSA-RC4-SHA", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C008 + TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA( + 0xC008, + "ECDHE-ECDSA-DES-CBC3-SHA", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher C009 + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA( + 0xC009, + "ECDHE-ECDSA-AES128-SHA", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C00A + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA( + 0xC00A, + "ECDHE-ECDSA-AES256-SHA", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C00B + TLS_ECDH_RSA_WITH_NULL_SHA( + 0xC00B, + "ECDH-RSA-NULL-SHA", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher C00C + TLS_ECDH_RSA_WITH_RC4_128_SHA( + 0xC00C, + "ECDH-RSA-RC4-SHA", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C00D + TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA( + 0xC00D, + "ECDH-RSA-DES-CBC3-SHA", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher C00E + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA( + 0xC00E, + "ECDH-RSA-AES128-SHA", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C00F + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA( + 0xC00F, + "ECDH-RSA-AES256-SHA", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C010 + TLS_ECDHE_RSA_WITH_NULL_SHA( + 0xC010, + "ECDHE-RSA-NULL-SHA", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher C011 + TLS_ECDHE_RSA_WITH_RC4_128_SHA( + 0xC011, + "ECDHE-RSA-RC4-SHA", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C012 + TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA( + 0xC012, + "ECDHE-RSA-DES-CBC3-SHA", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher C013 + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA( + 0xC013, + "ECDHE-RSA-AES128-SHA", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C014 + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA( + 0xC014, + "ECDHE-RSA-AES256-SHA", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C015 + TLS_ECDH_anon_WITH_NULL_SHA( + 0xC015, + "AECDH-NULL-SHA", + KeyExchange.EECDH, + Authentication.aNULL, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + // Cipher C016 + TLS_ECDH_anon_WITH_RC4_128_SHA( + 0xC016, + "AECDH-RC4-SHA", + KeyExchange.EECDH, + Authentication.aNULL, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C017 + TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA( + 0xC017, + "AECDH-DES-CBC3-SHA", + KeyExchange.EECDH, + Authentication.aNULL, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher C018 + TLS_ECDH_anon_WITH_AES_128_CBC_SHA( + 0xC018, + "AECDH-AES128-SHA", + KeyExchange.EECDH, + Authentication.aNULL, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C019 + TLS_ECDH_anon_WITH_AES_256_CBC_SHA( + 0xC019, + "AECDH-AES256-SHA", + KeyExchange.EECDH, + Authentication.aNULL, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + /* SRP ciphersuite from RFC 5054 */ + // Cipher C01A + TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA( + 0xC01A, + "SRP-3DES-EDE-CBC-SHA", + KeyExchange.SRP, + Authentication.SRP, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 112, + 168, + null, + null + ), + // Cipher C01B + TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA( + 0xC01B, + "SRP-RSA-3DES-EDE-CBC-SHA", + KeyExchange.SRP, + Authentication.RSA, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 112, + 168, + null, + null + ), + // Cipher C01C + TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA( + 0xC01C, + "SRP-DSS-3DES-EDE-CBC-SHA", + KeyExchange.SRP, + Authentication.DSS, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.MEDIUM, + false, + 112, + 168, + null, + null + ), + // Cipher C01D + TLS_SRP_SHA_WITH_AES_128_CBC_SHA( + 0xC01D, + "SRP-AES-128-CBC-SHA", + KeyExchange.SRP, + Authentication.SRP, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C01E + TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA( + 0xC01E, + "SRP-RSA-AES-128-CBC-SHA", + KeyExchange.SRP, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C01F + TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA( + 0xC01F, + "SRP-DSS-AES-128-CBC-SHA", + KeyExchange.SRP, + Authentication.DSS, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C020 + TLS_SRP_SHA_WITH_AES_256_CBC_SHA( + 0xC020, + "SRP-AES-256-CBC-SHA", + KeyExchange.SRP, + Authentication.SRP, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C021 + TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA( + 0xC021, + "SRP-RSA-AES-256-CBC-SHA", + KeyExchange.SRP, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C022 + TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA( + 0xC022, + "SRP-DSS-AES-256-CBC-SHA", + KeyExchange.SRP, + Authentication.DSS, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.SSLv3, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + /* HMAC based TLS v1.2 ciphersuites from RFC5289 */ + // Cipher C023 + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256( + 0xC023, + "ECDHE-ECDSA-AES128-SHA256", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C024 + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384( + 0xC024, + "ECDHE-ECDSA-AES256-SHA384", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES256, + MessageDigest.SHA384, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C025 + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256( + 0xC025, + "ECDH-ECDSA-AES128-SHA256", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C026 + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384( + 0xC026, + "ECDH-ECDSA-AES256-SHA384", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.AES256, + MessageDigest.SHA384, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C027 + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256( + 0xC027, + "ECDHE-RSA-AES128-SHA256", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C028 + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384( + 0xC028, + "ECDHE-RSA-AES256-SHA384", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.AES256, + MessageDigest.SHA384, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C029 + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256( + 0xC029, + "ECDH-RSA-AES128-SHA256", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C02A + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384( + 0xC02A, + "ECDH-RSA-AES256-SHA384", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.AES256, + MessageDigest.SHA384, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + /* GCM based TLS v1.2 ciphersuites from RFC5289 */ + // Cipher C02B + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256( + 0xC02B, + "ECDHE-ECDSA-AES128-GCM-SHA256", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C02C + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384( + 0xC02C, + "ECDHE-ECDSA-AES256-GCM-SHA384", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C02D + TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256( + 0xC02D, + "ECDH-ECDSA-AES128-GCM-SHA256", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C02E + TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384( + 0xC02E, + "ECDH-ECDSA-AES256-GCM-SHA384", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C02F + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256( + 0xC02F, + "ECDHE-RSA-AES128-GCM-SHA256", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C030 + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384( + 0xC030, + "ECDHE-RSA-AES256-GCM-SHA384", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C031 + TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256( + 0xC031, + "ECDH-RSA-AES128-GCM-SHA256", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.AES128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C032 + TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384( + 0xC032, + "ECDH-RSA-AES256-GCM-SHA384", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.AES256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C033 + TLS_ECDHE_PSK_WITH_RC4_128_SHA( + 0xC033, + "ECDHE-PSK-RC4-SHA", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.RC4, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C034 + TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA( + 0xC034, + "ECDHE-PSK-3DES-EDE-CBC-SHA", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.TRIPLE_DES, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.MEDIUM, + true, + 112, + 168, + null, + null + ), + // Cipher C035 + TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA( + 0xC035, + "ECDHE-PSK-AES128-CBC-SHA", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.AES128, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C036 + TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA( + 0xC036, + "ECDHE-PSK-AES256-CBC-SHA", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.AES256, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + + TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256( + 0xC037, + "ECDHE-PSK-AES128-CBC-SHA256", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.AES128, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384( + 0xC038, + "ECDHE-PSK-AES256-CBC-SHA384", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.AES256, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + TLS_ECDHE_PSK_WITH_NULL_SHA( + 0xC039, + "ECDHE-PSK-NULL-SHA", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA1, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + TLS_ECDHE_PSK_WITH_NULL_SHA256( + 0xC03A, + "ECDHE-PSK-NULL-SHA256", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + TLS_ECDHE_PSK_WITH_NULL_SHA384( + 0xC03B, + "ECDHE-PSK-NULL-SHA384", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.eNULL, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.STRONG_NONE, + true, + 0, + 0, + null, + null + ), + + /* ARIA ciphers 0xC03C to 0xC04F + * Unsupported by both Java and OpenSSL + */ + + TLS_RSA_WITH_ARIA_128_GCM_SHA256( + 0xC050, + "ARIA128-GCM-SHA256", + KeyExchange.RSA, + Authentication.RSA, + Encryption.ARIA128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + TLS_RSA_WITH_ARIA_256_GCM_SHA384( + 0xC051, + "ARIA256-GCM-SHA384", + KeyExchange.RSA, + Authentication.RSA, + Encryption.ARIA256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256( + 0xC052, + "DHE-RSA-ARIA128-GCM-SHA256", + KeyExchange.EDH, + Authentication.RSA, + Encryption.ARIA128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384( + 0xC053, + "DHE-RSA-ARIA256-GCM-SHA384", + KeyExchange.EDH, + Authentication.RSA, + Encryption.ARIA256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + + /* ARIA ciphers 0xC054 to 0xC055 + * Unsupported by both Java and OpenSSL + */ + + TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256( + 0xC056, + "DHE-DSS-ARIA128-GCM-SHA256", + KeyExchange.EDH, + Authentication.DSS, + Encryption.ARIA128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384( + 0xC057, + "DHE-DSS-ARIA256-GCM-SHA384", + KeyExchange.EDH, + Authentication.DSS, + Encryption.ARIA256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + + /* ARIA ciphers 0xC058 to 0xC05B + * Unsupported by both Java and OpenSSL + */ + + TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256( + 0xC05C, + "ECDHE-ECDSA-ARIA128-GCM-SHA256", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.ARIA128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384( + 0xC05D, + "ECDHE-ECDSA-ARIA256-GCM-SHA384", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.ARIA256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + + /* ARIA ciphers 0xC05E to 0xC05F + * Unsupported by both Java and OpenSSL + */ + + TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256( + 0xC060, + "ECDHE-ARIA128-GCM-SHA256", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.ARIA128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384( + 0xC061, + "ECDHE-ARIA256-GCM-SHA384", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.ARIA256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + + /* ARIA ciphers 0xC062 to 0xC069 + * Unsupported by both Java and OpenSSL + */ + + TLS_PSK_WITH_ARIA_128_GCM_SHA256( + 0xC06A, + "PSK-ARIA128-GCM-SHA256", + KeyExchange.PSK, + Authentication.PSK, + Encryption.ARIA128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + TLS_PSK_WITH_ARIA_256_GCM_SHA384( + 0xC06B, + "PSK-ARIA256-GCM-SHA384", + KeyExchange.PSK, + Authentication.PSK, + Encryption.ARIA256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256( + 0xC06C, + "DHE-PSK-ARIA128-GCM-SHA256", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.ARIA128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384( + 0xC06D, + "DHE-PSK-ARIA256-GCM-SHA384", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.ARIA256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256( + 0xC06E, + "RSA-PSK-ARIA128-GCM-SHA256", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.ARIA128GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384( + 0xC06F, + "RSA-PSK-ARIA256-GCM-SHA384", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.ARIA256GCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + + /* ARIA ciphers 0xC070 to 0xC071 + * Unsupported by both Java and OpenSSL + */ + + // Cipher C072 + TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256( + 0xC072, + "ECDHE-ECDSA-CAMELLIA128-SHA256", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C073 + TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384( + 0xC073, + "ECDHE-ECDSA-CAMELLIA256-SHA384", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.CAMELLIA256, + MessageDigest.SHA384, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C074 + TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256( + 0xC074, + "ECDH-ECDSA-CAMELLIA128-SHA256", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C075 + TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384( + 0xC075, + "ECDH-ECDSA-CAMELLIA256-SHA384", + KeyExchange.ECDHe, + Authentication.ECDH, + Encryption.CAMELLIA256, + MessageDigest.SHA384, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C076 + TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256( + 0xC076, + "ECDHE-RSA-CAMELLIA128-SHA256", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C077 + TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384( + 0xC077, + "ECDHE-RSA-CAMELLIA256-SHA384", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.CAMELLIA256, + MessageDigest.SHA384, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + // Cipher C078 + TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256( + 0xC078, + "ECDH-RSA-CAMELLIA128-SHA256", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 128, + 128, + null, + null + ), + // Cipher C079 + TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384( + 0xC079, + "ECDH-RSA-CAMELLIA256-SHA384", + KeyExchange.ECDHr, + Authentication.ECDH, + Encryption.CAMELLIA256, + MessageDigest.SHA384, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + true, + 256, + 256, + null, + null + ), + + // Cipher C094 + TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256( + 0xC094, + "PSK-CAMELLIA128-SHA256", + KeyExchange.PSK, + Authentication.PSK, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C095 + TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384( + 0xC095, + "PSK-CAMELLIA256-SHA384", + KeyExchange.PSK, + Authentication.PSK, + Encryption.CAMELLIA256, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C096 + TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256( + 0xC096, + "DHE-PSK-CAMELLIA128-SHA256", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C097 + TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384( + 0xC097, + "DHE-PSK-CAMELLIA256-SHA384", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.CAMELLIA256, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C098 + TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256( + 0xC098, + "RSA-PSK-CAMELLIA128-SHA256", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C099 + TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384( + 0xC099, + "RSA-PSK-CAMELLIA256-SHA384", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.CAMELLIA256, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C09A + TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256( + 0xC09A, + "ECDHE-PSK-CAMELLIA128-SHA256", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.CAMELLIA128, + MessageDigest.SHA256, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C09B + TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384( + 0xC09B, + "ECDHE-PSK-CAMELLIA256-SHA384", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.CAMELLIA256, + MessageDigest.SHA384, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // CCM ciphersuites from RFC6655 + // Cipher C09C + TLS_RSA_WITH_AES_128_CCM( + 0xC09C, + "AES128-CCM", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES128CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C09D + TLS_RSA_WITH_AES_256_CCM( + 0xC09D, + "AES256-CCM", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES256CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C09E + TLS_DHE_RSA_WITH_AES_128_CCM( + 0xC09E, + "DHE-RSA-AES128-CCM", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES128CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C09F + TLS_DHE_RSA_WITH_AES_256_CCM( + 0xC09F, + "DHE-RSA-AES256-CCM", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES256CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C0A0 + TLS_RSA_WITH_AES_128_CCM_8( + 0xC0A0, + "AES128-CCM8", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES128CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C0A1 + TLS_RSA_WITH_AES_256_CCM_8( + 0xC0A1, + "AES256-CCM8", + KeyExchange.RSA, + Authentication.RSA, + Encryption.AES256CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 256, + 256, + null, + null + ), + // Cipher C0A2 + TLS_DHE_RSA_WITH_AES_128_CCM_8( + 0xC0A2, + "DHE-RSA-AES128-CCM8", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES128CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C0A3 + TLS_DHE_RSA_WITH_AES_256_CCM_8( + 0xC0A3, + "DHE-RSA-AES256-CCM8", + KeyExchange.EDH, + Authentication.RSA, + Encryption.AES256CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 256, + 256, + null, + null + ), + // Cipher C0A4 + TLS_PSK_WITH_AES_128_CCM( + 0xC0A4, + "PSK-AES128-CCM", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES128CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C0A5 + TLS_PSK_WITH_AES_256_CCM( + 0xC0A5, + "PSK-AES256-CCM", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES256CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C0A6 + TLS_DHE_PSK_WITH_AES_128_CCM( + 0xC0A6, + "DHE-PSK-AES128-CCM", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES128CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C0A7 + TLS_DHE_PSK_WITH_AES_256_CCM( + 0xC0A7, + "DHE-PSK-AES256-CCM", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES256CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C0A8 + TLS_PSK_WITH_AES_128_CCM_8( + 0xC0A8, + "PSK-AES128-CCM8", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES128CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C0A9 + TLS_PSK_WITH_AES_256_CCM_8( + 0xC0A9, + "PSK-AES256-CCM8", + KeyExchange.PSK, + Authentication.PSK, + Encryption.AES256CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 256, + 256, + null, + null + ), + // Cipher C0AA + TLS_PSK_DHE_WITH_AES_128_CCM_8( + 0xC0AA, + "DHE-PSK-AES128-CCM8", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES128CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C0AB + TLS_PSK_DHE_WITH_AES_256_CCM_8( + 0xC0AB, + "DHE-PSK-AES256-CCM8", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.AES256CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 256, + 256, + null, + null + ), + // CCM ciphersuites from RFC7251 + // Cipher C0AC + TLS_ECDHE_ECDSA_WITH_AES_128_CCM( + 0xC0AC, + "ECDHE-ECDSA-AES128-CCM", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES128CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 128, + 128, + null, + null + ), + // Cipher C0AD + TLS_ECDHE_ECDSA_WITH_AES_256_CCM( + 0xC0AD, + "ECDHE-ECDSA-AES256-CCM", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES256CCM, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + // Cipher C0AE + TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8( + 0xC0AE, + "ECDHE-ECDSA-AES128-CCM8", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES128CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher C0AF + TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8( + 0xC0AF, + "ECDHE-ECDSA-AES256-CCM8", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.AES256CCM8, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.MEDIUM, + false, + 256, + 256, + null, + null + ), + // Draft: https://tools.ietf.org/html/draft-ietf-tls-chacha20-poly1305-04 + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256( + 0xCCA8, + "ECDHE-RSA-CHACHA20-POLY1305", + KeyExchange.EECDH, + Authentication.RSA, + Encryption.CHACHA20POLY1305, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256( + 0xCCA9, + "ECDHE-ECDSA-CHACHA20-POLY1305", + KeyExchange.EECDH, + Authentication.ECDSA, + Encryption.CHACHA20POLY1305, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256( + 0xCCAA, + "DHE-RSA-CHACHA20-POLY1305", + KeyExchange.EDH, + Authentication.RSA, + Encryption.CHACHA20POLY1305, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_PSK_WITH_CHACHA20_POLY1305_SHA256( + 0xCCAB, + "PSK-CHACHA20-POLY1305", + KeyExchange.PSK, + Authentication.PSK, + Encryption.CHACHA20POLY1305, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256( + 0xCCAC, + "ECDHE-PSK-CHACHA20-POLY1305", + KeyExchange.ECDHEPSK, + Authentication.PSK, + Encryption.CHACHA20POLY1305, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256( + 0xCCAD, + "DHE-PSK-CHACHA20-POLY1305", + KeyExchange.DHEPSK, + Authentication.PSK, + Encryption.CHACHA20POLY1305, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256( + 0xCCAE, + "RSA-PSK-CHACHA20-POLY1305", + KeyExchange.RSAPSK, + Authentication.RSA, + Encryption.CHACHA20POLY1305, + MessageDigest.AEAD, + Protocol.TLSv1_2, + false, + EncryptionLevel.HIGH, + false, + 256, + 256, + null, + null + ), + + // Cipher 0x010080 (SSLv2) + // RC4_128_WITH_MD5 + SSL_CK_RC4_128_WITH_MD5( + -1, + "RC4-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC4, + MessageDigest.MD5, + Protocol.SSLv2, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 0x020080 (SSLv2) + SSL2_RC4_128_EXPORT40_WITH_MD5( + -1, + "EXP-RC4-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC4, + MessageDigest.MD5, + Protocol.SSLv2, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + new String[] {"SSL_RC4_128_EXPORT40_WITH_MD5"}, + null + ), + // Cipher 0x030080 (SSLv2) + // RC2_128_CBC_WITH_MD5 + SSL_CK_RC2_128_CBC_WITH_MD5( + -1, + "RC2-CBC-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC2, + MessageDigest.MD5, + Protocol.SSLv2, + false, + EncryptionLevel.MEDIUM, + false, + 128, + 128, + null, + null + ), + // Cipher 0x040080 (SSLv2) + // RC2_128_CBC_EXPORT40_WITH_MD5 + SSL_CK_RC2_128_CBC_EXPORT40_WITH_MD5( + -1, + "EXP-RC2-CBC-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.RC2, + MessageDigest.MD5, + Protocol.SSLv2, + true, + EncryptionLevel.EXP40, + false, + 40, + 128, + null, + null + ), + // Cipher 0x050080 (SSLv2) + // IDEA_128_CBC_WITH_MD5 + SSL2_IDEA_128_CBC_WITH_MD5( + -1, + "IDEA-CBC-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.IDEA, + MessageDigest.MD5, + Protocol.SSLv2, + false, EncryptionLevel.MEDIUM, + false, + 128, + 128, + new String[] {"SSL_CK_IDEA_128_CBC_WITH_MD5"}, + null + ), + // Cipher 0x060040 (SSLv2) + // DES_64_CBC_WITH_MD5 + SSL2_DES_64_CBC_WITH_MD5( + -1, + "DES-CBC-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.DES, + MessageDigest.MD5, + Protocol.SSLv2, + false, + EncryptionLevel.LOW, + false, + 56, + 56, + new String[] {"SSL_CK_DES_64_CBC_WITH_MD5"}, + null + ), + // Cipher 0x0700C0 (SSLv2) + // DES_192_EDE3_CBC_WITH_MD5 + SSL2_DES_192_EDE3_CBC_WITH_MD5( + -1, + "DES-CBC3-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.TRIPLE_DES, + MessageDigest.MD5, + Protocol.SSLv2, + false, + EncryptionLevel.MEDIUM, + false, + 112, + 168, + new String[] {"SSL_CK_DES_192_EDE3_CBC_WITH_MD5"}, + null + ); + + /* TEMP_GOST_TLS*/ + /* + // Cipher FF00 + TLS_GOSTR341094_RSA_WITH_28147_CNT_MD5( + "GOST-MD5", + KeyExchange.RSA, + Authentication.RSA, + Encryption.eGOST2814789CNT, + MessageDigest.MD5, + Protocol.TLSv1, + false, + EncryptionLevel.HIGH, + false, + 256, + 256 + ), + TLS_RSA_WITH_28147_CNT_GOST94( + "GOST-GOST94", + KeyExchange.RSA, + Authentication.RSA, + Encryption.eGOST2814789CNT, + MessageDigest.GOST94, + Protocol.TLSv1, + false, EncryptionLevel.HIGH,false, + 256, + 256 + ), + { + 1, + "GOST-GOST89MAC", + 0x0300ff02, + KeyExchange.RSA, + Authentication.RSA, + Encryption.eGOST2814789CNT, + MessageDigest.GOST89MAC, + Protocol.TLSv1, + false, EncryptionLevel.HIGH,false, + + 256, + 256 + ), + { + 1, + "GOST-GOST89STREAM", + 0x0300ff03, + KeyExchange.RSA, + Authentication.RSA, + Encryption.eGOST2814789CNT, + MessageDigest.GOST89MAC, + Protocol.TLSv1, + false, EncryptionLevel.HIGH,false, + 256, + 256 + },*/ + + + private final int id; + private final String openSSLAlias; + private final Set openSSLAltNames; + private final Set jsseNames; + private final KeyExchange kx; + private final Authentication au; + private final Encryption enc; + private final MessageDigest mac; + private final Protocol protocol; + private final boolean export; + private final EncryptionLevel level; + private final boolean fipsCompatible; + /** + * Number of bits really used + */ + private final int strength_bits; + /** + * Number of bits for algorithm + */ + private final int alg_bits; + + Cipher(int id, String openSSLAlias, KeyExchange kx, Authentication au, Encryption enc, + MessageDigest mac, Protocol protocol, boolean export, EncryptionLevel level, + boolean fipsCompatible, int strength_bits, int alg_bits, String[] jsseAltNames, + String[] openSSlAltNames) { + this.id = id; + this.openSSLAlias = openSSLAlias; + if (openSSlAltNames != null && openSSlAltNames.length != 0) { + Set altNames = new HashSet<>(Arrays.asList(openSSlAltNames)); + this.openSSLAltNames = Collections.unmodifiableSet(altNames); + } else { + this.openSSLAltNames = Collections.emptySet(); + } + Set jsseNames = new LinkedHashSet<>(); + if (jsseAltNames != null && jsseAltNames.length != 0) { + jsseNames.addAll(Arrays.asList(jsseAltNames)); + } + jsseNames.add(name()); + this.jsseNames = Collections.unmodifiableSet(jsseNames); + this.kx = kx; + this.au = au; + this.enc = enc; + this.mac = mac; + this.protocol = protocol; + this.export = export; + this.level = level; + this.fipsCompatible = fipsCompatible; + this.strength_bits = strength_bits; + this.alg_bits = alg_bits; + } + + public int getId() { + return id; + } + + public String getOpenSSLAlias() { + return openSSLAlias; + } + + public Set getOpenSSLAltNames() { + return openSSLAltNames; + } + + public Set getJsseNames() { + return jsseNames; + } + + public KeyExchange getKx() { + return kx; + } + + public Authentication getAu() { + return au; + } + + public Encryption getEnc() { + return enc; + } + + public MessageDigest getMac() { + return mac; + } + + public Protocol getProtocol() { + return protocol; + } + + public boolean isExport() { + return export; + } + + public EncryptionLevel getLevel() { + return level; + } + + public boolean isFipsCompatible() { + return fipsCompatible; + } + + public int getStrength_bits() { + return strength_bits; + } + + public int getAlg_bits() { + return alg_bits; + } + + + private static final Map idMap = new HashMap<>(); + + static { + for (Cipher cipher : values()) { + int id = cipher.getId(); + + if (id > 0 && id < 0xFFFF) { + idMap.put(Integer.valueOf(id), cipher); + } + } + } + + + public static Cipher valueOf(int cipherId) { + return idMap.get(Integer.valueOf(cipherId)); + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/Encryption.java b/java/org/apache/tomcat/util/net/openssl/ciphers/Encryption.java new file mode 100644 index 0000000..14c678c --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/Encryption.java @@ -0,0 +1,42 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +public enum Encryption { + AES128, + AES128CCM, + AES128CCM8, + AES128GCM, + AES256, + AES256CCM, + AES256CCM8, + AES256GCM, + ARIA128GCM, + ARIA256GCM, + CAMELLIA256, + CAMELLIA128, + CHACHA20POLY1305, + TRIPLE_DES, + DES, + IDEA, + eGOST2814789CNT, + SEED, + FZA, + RC4, + RC2, + eNULL +} diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/EncryptionLevel.java b/java/org/apache/tomcat/util/net/openssl/ciphers/EncryptionLevel.java new file mode 100644 index 0000000..4ced078 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/EncryptionLevel.java @@ -0,0 +1,27 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +public enum EncryptionLevel { + STRONG_NONE, + EXP40, + EXP56, + LOW, + MEDIUM, + HIGH, + FIPS +} diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/KeyExchange.java b/java/org/apache/tomcat/util/net/openssl/ciphers/KeyExchange.java new file mode 100644 index 0000000..8407eb7 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/KeyExchange.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +public enum KeyExchange { + EECDH /* SSL_kEECDH - ephemeral ECDH */, + RSA /* SSL_kRSA - RSA key exchange */, + DHr /* SSL_kDHr - DH cert, RSA CA cert */ /* no such ciphersuites supported! */, + DHd /* SSL_kDHd - DH cert, DSA CA cert */ /* no such ciphersuite supported! */, + EDH /* SSL_kDHE - tmp DH key no DH cert */, + PSK /* SSK_kPSK - PSK */, + FZA /* SSL_kFZA - Fortezza */ /* no such ciphersuite supported! */, + KRB5 /* SSL_kKRB5 - Kerberos 5 key exchange */, + ECDHr /* SSL_kECDHr - ECDH cert, RSA CA cert */, + ECDHe /* SSL_kECDHe - ECDH cert, ECDSA CA cert */, + GOST /* SSL_kGOST - GOST key exchange */, + SRP /* SSL_kSRP - SRP */, + RSAPSK, + ECDHEPSK, + DHEPSK, + ANY /* TLS 1.3 */ +} diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings.properties b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings.properties new file mode 100644 index 0000000..27c7e49 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings.properties @@ -0,0 +1,18 @@ +# 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. + +opensslCipherConfigurationParser.effectiveCiphers=Ciphers used: [{0}] +opensslCipherConfigurationParser.unknownElement=Unknown element in cipher string: [{0}] +opensslCipherConfigurationParser.unknownProfile=Cannot use OpenSSL to resolve profile [{0}], it will be passed along as the cipher suite diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_de.properties b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_de.properties new file mode 100644 index 0000000..4eded96 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_de.properties @@ -0,0 +1,16 @@ +# 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. + +opensslCipherConfigurationParser.effectiveCiphers=Verwendete Ciphers: [{0}] diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_es.properties b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_es.properties new file mode 100644 index 0000000..a36ea19 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_es.properties @@ -0,0 +1,16 @@ +# 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. + +opensslCipherConfigurationParser.effectiveCiphers=Cifradores usados: [{0}] diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_fr.properties b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_fr.properties new file mode 100644 index 0000000..c880b9b --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_fr.properties @@ -0,0 +1,18 @@ +# 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. + +opensslCipherConfigurationParser.effectiveCiphers=Chiffres utilisés : [{0}] +opensslCipherConfigurationParser.unknownElement=Elément inconnu dans la chaîne de chiffres : [{0}] +opensslCipherConfigurationParser.unknownProfile=Impossible d''utiliser OpenSSL pour obtenir le profil [{0}], celui ci sera utilisé en tant que liste d''algorithme de chiffrage diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_ja.properties b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_ja.properties new file mode 100644 index 0000000..5f8784b --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_ja.properties @@ -0,0 +1,18 @@ +# 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. + +opensslCipherConfigurationParser.effectiveCiphers=使用ã•ã‚ŒãŸæš—å·ï¼š[{0}] +opensslCipherConfigurationParser.unknownElement=æš—å·æ–‡å­—列ã®ä¸æ˜Žãªè¦ç´ ï¼š[{0}] +opensslCipherConfigurationParser.unknownProfile=OpenSSL を使用ã—ã¦ãƒ—ロファイル [{0}] を解決ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。プロファイルã¯æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆã¨ã—ã¦æ¸¡ã•ã‚Œã¾ã™ã€‚ diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_ko.properties b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_ko.properties new file mode 100644 index 0000000..d2c5e52 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_ko.properties @@ -0,0 +1,17 @@ +# 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. + +opensslCipherConfigurationParser.effectiveCiphers=사용ë˜ëŠ” Cipher들: [{0}] +opensslCipherConfigurationParser.unknownElement=Cipher 문ìžì—´ì— ì•Œ 수 없는 엘리먼트: [{0}] diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..e62d7f6 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/LocalStrings_zh_CN.properties @@ -0,0 +1,17 @@ +# 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. + +opensslCipherConfigurationParser.effectiveCiphers=使用的密ç ï¼š[{0}] +opensslCipherConfigurationParser.unknownElement=密ç å­—符串中的未知元素:[{0}]。 diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/MessageDigest.java b/java/org/apache/tomcat/util/net/openssl/ciphers/MessageDigest.java new file mode 100644 index 0000000..c257812 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/MessageDigest.java @@ -0,0 +1,27 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +public enum MessageDigest { + MD5, + SHA1, + GOST94, + GOST89MAC, + SHA256, + SHA384, + AEAD +} diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java b/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java new file mode 100644 index 0000000..a32bb33 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java @@ -0,0 +1,910 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.Constants; +import org.apache.tomcat.util.res.StringManager; + +/** + * Class in charge with parsing openSSL expressions to define a list of ciphers. + */ +public class OpenSSLCipherConfigurationParser { + + private static final Log log = LogFactory.getLog(OpenSSLCipherConfigurationParser.class); + private static final StringManager sm = StringManager.getManager(OpenSSLCipherConfigurationParser.class); + + private static boolean initialized = false; + + private static final String SEPARATOR = ":|,| "; + /** + * If ! is used then the ciphers are permanently deleted from the list. The ciphers deleted can never reappear in the list + * even if they are explicitly stated. + */ + private static final String EXCLUDE = "!"; + /** + * If - is used then the ciphers are deleted from the list, but some or all of the ciphers can be added again by later + * options. + */ + private static final String DELETE = "-"; + /** + * If + is used then the ciphers are moved to the end of the list. This option doesn't add any new ciphers it just moves + * matching existing ones. + */ + private static final String TO_END = "+"; + /** + * Lists of cipher suites can be combined in a single cipher string using the + character. + * This is used as a logical and operation. + * For example SHA1+DES represents all cipher suites containing the SHA1 and the DES algorithms. + */ + private static final String AND = "+"; + /** + * All ciphers by their openssl alias name. + */ + private static final Map> aliases = new LinkedHashMap<>(); + + /** + * the 'NULL' ciphers that is those offering no encryption. Because these offer no encryption at all and are a security risk + * they are disabled unless explicitly included. + */ + private static final String eNULL = "eNULL"; + /** + * The cipher suites offering no authentication. This is currently the anonymous DH algorithms. T These cipher suites are + * vulnerable to a 'man in the middle' attack and so their use is normally discouraged. + */ + private static final String aNULL = "aNULL"; + + /** + * 'high' encryption cipher suites. This currently means those with key lengths larger than 128 bits, and some cipher suites + * with 128-bit keys. + */ + private static final String HIGH = "HIGH"; + /** + * 'medium' encryption cipher suites, currently some of those using 128 bit encryption. + */ + private static final String MEDIUM = "MEDIUM"; + /** + * 'low' encryption cipher suites, currently those using 64 or 56 bit encryption algorithms but excluding export cipher + * suites. + */ + private static final String LOW = "LOW"; + /** + * Export encryption algorithms. Including 40 and 56 bits algorithms. + */ + private static final String EXPORT = "EXPORT"; + /** + * 40 bit export encryption algorithms. + */ + private static final String EXPORT40 = "EXPORT40"; + /** + * 56 bit export encryption algorithms. + */ + private static final String EXPORT56 = "EXPORT56"; + /** + * Cipher suites using RSA key exchange. + */ + private static final String kRSA = "kRSA"; + /** + * Cipher suites using RSA authentication. + */ + private static final String aRSA = "aRSA"; + /** + * Cipher suites using RSA for key exchange + * Despite what the docs say, RSA is equivalent to kRSA. + */ + private static final String RSA = "RSA"; + /** + * Cipher suites using ephemeral DH key agreement. + */ + private static final String kEDH = "kEDH"; + /** + * Cipher suites using ephemeral DH key agreement. + */ + private static final String kDHE = "kDHE"; + /** + * Cipher suites using ephemeral DH key agreement. equivalent to kEDH:-ADH + */ + private static final String EDH = "EDH"; + /** + * Cipher suites using ephemeral DH key agreement. equivalent to kEDH:-ADH + */ + private static final String DHE = "DHE"; + /** + * Cipher suites using DH key agreement and DH certificates signed by CAs with RSA keys. + */ + private static final String kDHr = "kDHr"; + /** + * Cipher suites using DH key agreement and DH certificates signed by CAs with DSS keys. + */ + private static final String kDHd = "kDHd"; + /** + * Cipher suites using DH key agreement and DH certificates signed by CAs with RSA or DSS keys. + */ + private static final String kDH = "kDH"; + /** + * Cipher suites using fixed ECDH key agreement signed by CAs with RSA keys. + */ + private static final String kECDHr = "kECDHr"; + /** + * Cipher suites using fixed ECDH key agreement signed by CAs with ECDSA keys. + */ + private static final String kECDHe = "kECDHe"; + /** + * Cipher suites using fixed ECDH key agreement signed by CAs with RSA and ECDSA keys or either respectively. + */ + private static final String kECDH = "kECDH"; + /** + * Cipher suites using ephemeral ECDH key agreement, including anonymous cipher suites. + */ + private static final String kEECDH = "kEECDH"; + /** + * Cipher suites using ephemeral ECDH key agreement, excluding anonymous cipher suites. + * Same as "kEECDH:-AECDH" + */ + private static final String EECDH = "EECDH"; + /** + * Cipher suitesusing ECDH key exchange, including anonymous, ephemeral and fixed ECDH. + */ + private static final String ECDH = "ECDH"; + /** + * Cipher suites using ephemeral ECDH key agreement, including anonymous cipher suites. + */ + private static final String kECDHE = "kECDHE"; + /** + * Cipher suites using authenticated ephemeral ECDH key agreement + */ + private static final String ECDHE = "ECDHE"; + /** + * Anonymous Elliptic Curve Diffie Hellman cipher suites. + */ + private static final String AECDH = "AECDH"; + /** + * Cipher suites using DSS for key exchange + */ + private static final String DSS = "DSS"; + /** + * Cipher suites using DSS authentication, i.e. the certificates carry DSS keys. + */ + private static final String aDSS = "aDSS"; + /** + * Cipher suites effectively using DH authentication, i.e. the certificates carry DH keys. + */ + private static final String aDH = "aDH"; + /** + * Cipher suites effectively using ECDH authentication, i.e. the certificates carry ECDH keys. + */ + private static final String aECDH = "aECDH"; + /** + * Cipher suites effectively using ECDSA authentication, i.e. the certificates carry ECDSA keys. + */ + private static final String aECDSA = "aECDSA"; + /** + * Cipher suites effectively using ECDSA authentication, i.e. the certificates carry ECDSA keys. + */ + private static final String ECDSA = "ECDSA"; + /** + * Ciphers suites using FORTEZZA key exchange algorithms. + */ + private static final String kFZA = "kFZA"; + /** + * Ciphers suites using FORTEZZA authentication algorithms. + */ + private static final String aFZA = "aFZA"; + /** + * Ciphers suites using FORTEZZA encryption algorithms. + */ + private static final String eFZA = "eFZA"; + /** + * Ciphers suites using all FORTEZZA algorithms. + */ + private static final String FZA = "FZA"; + /** + * Cipher suites using DH, including anonymous DH, ephemeral DH and fixed DH. + */ + private static final String DH = "DH"; + /** + * Anonymous DH cipher suites. + */ + private static final String ADH = "ADH"; + /** + * Cipher suites using 128 bit AES. + */ + private static final String AES128 = "AES128"; + /** + * Cipher suites using 256 bit AES. + */ + private static final String AES256 = "AES256"; + /** + * Cipher suites using either 128 or 256 bit AES. + */ + private static final String AES = "AES"; + /** + * AES in Galois Counter Mode (GCM): these cipher suites are only supported in TLS v1.2. + */ + private static final String AESGCM = "AESGCM"; + /** + * AES in Counter with CBC-MAC Mode (CCM). + */ + private static final String AESCCM = "AESCCM"; + /** + * AES in Counter with CBC-MAC Mode and 8-byte authentication (CCM8). + */ + private static final String AESCCM8 = "AESCCM8"; + /** + * Cipher suites using either 128 bit ARIA. + */ + private static final String ARIA128 = "ARIA128"; + /** + * Cipher suites using either 256 bit ARIA. + */ + private static final String ARIA256 = "ARIA256"; + /** + * Cipher suites using either 128 or 256 bit ARIA. + */ + private static final String ARIA = "ARIA"; + /** + * Cipher suites using 128 bit CAMELLIA. + */ + private static final String CAMELLIA128 = "CAMELLIA128"; + /** + * Cipher suites using 256 bit CAMELLIA. + */ + private static final String CAMELLIA256 = "CAMELLIA256"; + /** + * Cipher suites using either 128 or 256 bit CAMELLIA. + */ + private static final String CAMELLIA = "CAMELLIA"; + /** + * Cipher suites using CHACHA20. + */ + private static final String CHACHA20 = "CHACHA20"; + /** + * Cipher suites using triple DES. + */ + private static final String TRIPLE_DES = "3DES"; + /** + * Cipher suites using DES (not triple DES). + */ + private static final String DES = "DES"; + /** + * Cipher suites using RC4. + */ + private static final String RC4 = "RC4"; + /** + * Cipher suites using RC2. + */ + private static final String RC2 = "RC2"; + /** + * Cipher suites using IDEA. + */ + private static final String IDEA = "IDEA"; + /** + * Cipher suites using SEED. + */ + private static final String SEED = "SEED"; + /** + * Cipher suites using MD5. + */ + private static final String MD5 = "MD5"; + /** + * Cipher suites using SHA1. + */ + private static final String SHA1 = "SHA1"; + /** + * Cipher suites using SHA1. + */ + private static final String SHA = "SHA"; + /** + * Cipher suites using SHA256. + */ + private static final String SHA256 = "SHA256"; + /** + * Cipher suites using SHA384. + */ + private static final String SHA384 = "SHA384"; + /** + * Cipher suites using KRB5. + */ + private static final String KRB5 = "KRB5"; + /** + * Cipher suites using GOST R 34.10 (either 2001 or 94) for authentication. + */ + private static final String aGOST = "aGOST"; + /** + * Cipher suites using GOST R 34.10-2001 for authentication. + */ + private static final String aGOST01 = "aGOST01"; + /** + * Cipher suites using GOST R 34.10-94 authentication (note that R 34.10-94 standard has been expired so use GOST R + * 34.10-2001) + */ + private static final String aGOST94 = "aGOST94"; + /** + * Cipher suites using using VKO 34.10 key exchange, specified in the RFC 4357. + */ + private static final String kGOST = "kGOST"; + /** + * Cipher suites, using HMAC based on GOST R 34.11-94. + */ + private static final String GOST94 = "GOST94"; + /** + * Cipher suites using GOST 28147-89 MAC instead of HMAC. + */ + private static final String GOST89MAC = "GOST89MAC"; + /** + * Cipher suites using SRP authentication, specified in the RFC 5054. + */ + private static final String aSRP = "aSRP"; + /** + * Cipher suites using SRP key exchange, specified in the RFC 5054. + */ + private static final String kSRP = "kSRP"; + /** + * Same as kSRP + */ + private static final String SRP = "SRP"; + /** + * Cipher suites using pre-shared keys (PSK). + */ + private static final String PSK = "PSK"; + /** + * Cipher suites using PSK authentication. + */ + private static final String aPSK = "aPSK"; + /** + * Cipher suites using PSK key 'exchange'. + */ + private static final String kPSK = "kPSK"; + private static final String kRSAPSK = "kRSAPSK"; + private static final String kECDHEPSK = "kECDHEPSK"; + private static final String kDHEPSK = "kDHEPSK"; + + private static final String DEFAULT = "DEFAULT"; + private static final String COMPLEMENTOFDEFAULT = "COMPLEMENTOFDEFAULT"; + + private static final String ALL = "ALL"; + private static final String COMPLEMENTOFALL = "COMPLEMENTOFALL"; + + private static final Map jsseToOpenSSL = new HashMap<>(); + + private static void init() { + + for (Cipher cipher : Cipher.values()) { + String alias = cipher.getOpenSSLAlias(); + if (aliases.containsKey(alias)) { + aliases.get(alias).add(cipher); + } else { + List list = new ArrayList<>(); + list.add(cipher); + aliases.put(alias, list); + } + aliases.put(cipher.name(), Collections.singletonList(cipher)); + + for (String openSSlAltName : cipher.getOpenSSLAltNames()) { + if (aliases.containsKey(openSSlAltName)) { + aliases.get(openSSlAltName).add(cipher); + } else { + List list = new ArrayList<>(); + list.add(cipher); + aliases.put(openSSlAltName, list); + } + + } + + jsseToOpenSSL.put(cipher.name(), cipher.getOpenSSLAlias()); + Set jsseNames = cipher.getJsseNames(); + for (String jsseName : jsseNames) { + jsseToOpenSSL.put(jsseName, cipher.getOpenSSLAlias()); + } + } + List allCiphersList = Arrays.asList(Cipher.values()); + Collections.reverse(allCiphersList); + LinkedHashSet allCiphers = defaultSort(new LinkedHashSet<>(allCiphersList)); + addListAlias(eNULL, filterByEncryption(allCiphers, Collections.singleton(Encryption.eNULL))); + LinkedHashSet all = new LinkedHashSet<>(allCiphers); + remove(all, eNULL); + addListAlias(ALL, all); + addListAlias(HIGH, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.HIGH))); + addListAlias(MEDIUM, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.MEDIUM))); + addListAlias(LOW, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.LOW))); + addListAlias(EXPORT, filterByEncryptionLevel(allCiphers, new HashSet<>(Arrays.asList(EncryptionLevel.EXP40, EncryptionLevel.EXP56)))); + aliases.put("EXP", aliases.get(EXPORT)); + addListAlias(EXPORT40, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.EXP40))); + addListAlias(EXPORT56, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.EXP56))); + aliases.put("NULL", aliases.get(eNULL)); + aliases.put(COMPLEMENTOFALL, aliases.get(eNULL)); + addListAlias(aNULL, filterByAuthentication(allCiphers, Collections.singleton(Authentication.aNULL))); + addListAlias(kRSA, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.RSA))); + addListAlias(aRSA, filterByAuthentication(allCiphers, Collections.singleton(Authentication.RSA))); + // Despite what the docs say, RSA is equivalent to kRSA + aliases.put(RSA, aliases.get(kRSA)); + addListAlias(kEDH, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EDH))); + addListAlias(kDHE, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EDH))); + Set edh = filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EDH)); + edh.removeAll(filterByAuthentication(allCiphers, Collections.singleton(Authentication.aNULL))); + addListAlias(EDH, edh); + addListAlias(DHE, edh); + addListAlias(kDHr, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.DHr))); + addListAlias(kDHd, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.DHd))); + addListAlias(kDH, filterByKeyExchange(allCiphers, new HashSet<>(Arrays.asList(KeyExchange.DHr, KeyExchange.DHd)))); + + addListAlias(kECDHr, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.ECDHr))); + addListAlias(kECDHe, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.ECDHe))); + addListAlias(kECDH, filterByKeyExchange(allCiphers, new HashSet<>(Arrays.asList(KeyExchange.ECDHe, KeyExchange.ECDHr)))); + addListAlias(ECDH, filterByKeyExchange(allCiphers, new HashSet<>(Arrays.asList(KeyExchange.ECDHe, KeyExchange.ECDHr, KeyExchange.EECDH)))); + addListAlias(kECDHE, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EECDH))); + + Set ecdhe = filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EECDH)); + remove(ecdhe, aNULL); + addListAlias(ECDHE, ecdhe); + + addListAlias(kEECDH, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EECDH))); + Set eecdh = filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EECDH)); + eecdh.removeAll(filterByAuthentication(allCiphers, Collections.singleton(Authentication.aNULL))); + addListAlias(EECDH, eecdh); + addListAlias(aDSS, filterByAuthentication(allCiphers, Collections.singleton(Authentication.DSS))); + aliases.put(DSS, aliases.get(aDSS)); + addListAlias(aDH, filterByAuthentication(allCiphers, Collections.singleton(Authentication.DH))); + Set aecdh = filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EECDH)); + addListAlias(AECDH, filterByAuthentication(aecdh, Collections.singleton(Authentication.aNULL))); + addListAlias(aECDH, filterByAuthentication(allCiphers, Collections.singleton(Authentication.ECDH))); + addListAlias(ECDSA, filterByAuthentication(allCiphers, Collections.singleton(Authentication.ECDSA))); + aliases.put(aECDSA, aliases.get(ECDSA)); + addListAlias(kFZA, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.FZA))); + addListAlias(aFZA, filterByAuthentication(allCiphers, Collections.singleton(Authentication.FZA))); + addListAlias(eFZA, filterByEncryption(allCiphers, Collections.singleton(Encryption.FZA))); + addListAlias(FZA, filter(allCiphers, null, Collections.singleton(KeyExchange.FZA), Collections.singleton(Authentication.FZA), Collections.singleton(Encryption.FZA), null, null)); + addListAlias(Constants.SSL_PROTO_TLSv1_2, filterByProtocol(allCiphers, Collections.singleton(Protocol.TLSv1_2))); + addListAlias(Constants.SSL_PROTO_TLSv1_0, filterByProtocol(allCiphers, Collections.singleton(Protocol.TLSv1))); + addListAlias(Constants.SSL_PROTO_SSLv3, filterByProtocol(allCiphers, Collections.singleton(Protocol.SSLv3))); + aliases.put(Constants.SSL_PROTO_TLSv1, aliases.get(Constants.SSL_PROTO_TLSv1_0)); + addListAlias(Constants.SSL_PROTO_SSLv2, filterByProtocol(allCiphers, Collections.singleton(Protocol.SSLv2))); + addListAlias(DH, filterByKeyExchange(allCiphers, new HashSet<>(Arrays.asList(KeyExchange.DHr, KeyExchange.DHd, KeyExchange.EDH)))); + Set adh = filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EDH)); + adh.retainAll(filterByAuthentication(allCiphers, Collections.singleton(Authentication.aNULL))); + addListAlias(ADH, adh); + addListAlias(AES128, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.AES128, Encryption.AES128CCM, Encryption.AES128CCM8, Encryption.AES128GCM)))); + addListAlias(AES256, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.AES256, Encryption.AES256CCM, Encryption.AES256CCM8, Encryption.AES256GCM)))); + addListAlias(AES, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.AES128, Encryption.AES128CCM, Encryption.AES128CCM8, Encryption.AES128GCM, Encryption.AES256, Encryption.AES256CCM, Encryption.AES256CCM8, Encryption.AES256GCM)))); + addListAlias(ARIA128, filterByEncryption(allCiphers, Collections.singleton(Encryption.ARIA128GCM))); + addListAlias(ARIA256, filterByEncryption(allCiphers, Collections.singleton(Encryption.ARIA256GCM))); + addListAlias(ARIA, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.ARIA128GCM, Encryption.ARIA256GCM)))); + addListAlias(AESGCM, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.AES128GCM, Encryption.AES256GCM)))); + addListAlias(AESCCM, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.AES128CCM, Encryption.AES128CCM8, Encryption.AES256CCM, Encryption.AES256CCM8)))); + addListAlias(AESCCM8, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.AES128CCM8, Encryption.AES256CCM8)))); + addListAlias(CAMELLIA, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.CAMELLIA128, Encryption.CAMELLIA256)))); + addListAlias(CAMELLIA128, filterByEncryption(allCiphers, Collections.singleton(Encryption.CAMELLIA128))); + addListAlias(CAMELLIA256, filterByEncryption(allCiphers, Collections.singleton(Encryption.CAMELLIA256))); + addListAlias(CHACHA20, filterByEncryption(allCiphers, Collections.singleton(Encryption.CHACHA20POLY1305))); + addListAlias(TRIPLE_DES, filterByEncryption(allCiphers, Collections.singleton(Encryption.TRIPLE_DES))); + addListAlias(DES, filterByEncryption(allCiphers, Collections.singleton(Encryption.DES))); + addListAlias(RC4, filterByEncryption(allCiphers, Collections.singleton(Encryption.RC4))); + addListAlias(RC2, filterByEncryption(allCiphers, Collections.singleton(Encryption.RC2))); + addListAlias(IDEA, filterByEncryption(allCiphers, Collections.singleton(Encryption.IDEA))); + addListAlias(SEED, filterByEncryption(allCiphers, Collections.singleton(Encryption.SEED))); + addListAlias(MD5, filterByMessageDigest(allCiphers, Collections.singleton(MessageDigest.MD5))); + addListAlias(SHA1, filterByMessageDigest(allCiphers, Collections.singleton(MessageDigest.SHA1))); + aliases.put(SHA, aliases.get(SHA1)); + addListAlias(SHA256, filterByMessageDigest(allCiphers, Collections.singleton(MessageDigest.SHA256))); + addListAlias(SHA384, filterByMessageDigest(allCiphers, Collections.singleton(MessageDigest.SHA384))); + addListAlias(aGOST, filterByAuthentication(allCiphers, new HashSet<>(Arrays.asList(Authentication.GOST01, Authentication.GOST94)))); + addListAlias(aGOST01, filterByAuthentication(allCiphers, Collections.singleton(Authentication.GOST01))); + addListAlias(aGOST94, filterByAuthentication(allCiphers, Collections.singleton(Authentication.GOST94))); + addListAlias(kGOST, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.GOST))); + addListAlias(GOST94, filterByMessageDigest(allCiphers, Collections.singleton(MessageDigest.GOST94))); + addListAlias(GOST89MAC, filterByMessageDigest(allCiphers, Collections.singleton(MessageDigest.GOST89MAC))); + addListAlias(PSK, filter(allCiphers, null, new HashSet<>(Arrays.asList(KeyExchange.PSK, KeyExchange.RSAPSK, KeyExchange.DHEPSK, KeyExchange.ECDHEPSK)), Collections.singleton(Authentication.PSK), null, null, null)); + addListAlias(aPSK, filterByAuthentication(allCiphers, Collections.singleton(Authentication.PSK))); + addListAlias(kPSK, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.PSK))); + addListAlias(kRSAPSK, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.RSAPSK))); + addListAlias(kECDHEPSK, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.ECDHEPSK))); + addListAlias(kDHEPSK, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.DHEPSK))); + addListAlias(KRB5, filter(allCiphers, null, Collections.singleton(KeyExchange.KRB5), Collections.singleton(Authentication.KRB5), null, null, null)); + addListAlias(aSRP, filterByAuthentication(allCiphers, Collections.singleton(Authentication.SRP))); + addListAlias(kSRP, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.SRP))); + addListAlias(SRP, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.SRP))); + initialized = true; + // Despite what the OpenSSL docs say, DEFAULT also excludes SSLv2 + addListAlias(DEFAULT, parse("ALL:!EXPORT:!eNULL:!aNULL:!SSLv2:!DES:!RC2:!RC4:!DSS:!SEED:!IDEA:!CAMELLIA:!AESCCM:!3DES:!ARIA")); + // COMPLEMENTOFDEFAULT is also not exactly as defined by the docs + LinkedHashSet complementOfDefault = filterByKeyExchange(all, new HashSet<>(Arrays.asList(KeyExchange.EDH,KeyExchange.EECDH))); + complementOfDefault = filterByAuthentication(complementOfDefault, Collections.singleton(Authentication.aNULL)); + aliases.get(eNULL).forEach(complementOfDefault::remove); + complementOfDefault.addAll(aliases.get(Constants.SSL_PROTO_SSLv2)); + complementOfDefault.addAll(aliases.get(EXPORT)); + complementOfDefault.addAll(aliases.get(DES)); + complementOfDefault.addAll(aliases.get(TRIPLE_DES)); + complementOfDefault.addAll(aliases.get(RC2)); + complementOfDefault.addAll(aliases.get(RC4)); + complementOfDefault.addAll(aliases.get(aDSS)); + complementOfDefault.addAll(aliases.get(SEED)); + complementOfDefault.addAll(aliases.get(IDEA)); + complementOfDefault.addAll(aliases.get(CAMELLIA)); + complementOfDefault.addAll(aliases.get(AESCCM)); + complementOfDefault.addAll(aliases.get(ARIA)); + defaultSort(complementOfDefault); + addListAlias(COMPLEMENTOFDEFAULT, complementOfDefault); + } + + static void addListAlias(String alias, Set ciphers) { + aliases.put(alias, new ArrayList<>(ciphers)); + } + + static void moveToEnd(final LinkedHashSet ciphers, final String alias) { + moveToEnd(ciphers, aliases.get(alias)); + } + + static void moveToEnd(final LinkedHashSet ciphers, final Collection toBeMovedCiphers) { + List movedCiphers = new ArrayList<>(toBeMovedCiphers); + movedCiphers.retainAll(ciphers); + movedCiphers.forEach(ciphers::remove); + ciphers.addAll(movedCiphers); + } + + static void moveToStart(final LinkedHashSet ciphers, final Collection toBeMovedCiphers) { + List movedCiphers = new ArrayList<>(toBeMovedCiphers); + List originalCiphers = new ArrayList<>(ciphers); + movedCiphers.retainAll(ciphers); + ciphers.clear(); + ciphers.addAll(movedCiphers); + ciphers.addAll(originalCiphers); + } + + static void add(final LinkedHashSet ciphers, final String alias) { + ciphers.addAll(aliases.get(alias)); + } + + static void remove(final Set ciphers, final String alias) { + aliases.get(alias).forEach(ciphers::remove); + } + + static LinkedHashSet strengthSort(final LinkedHashSet ciphers) { + /* + * This routine sorts the ciphers with descending strength. The sorting + * must keep the pre-sorted sequence, so we apply the normal sorting + * routine as '+' movement to the end of the list. + */ + Set keySizes = new HashSet<>(); + for (Cipher cipher : ciphers) { + keySizes.add(Integer.valueOf(cipher.getStrength_bits())); + } + List strength_bits = new ArrayList<>(keySizes); + Collections.sort(strength_bits); + Collections.reverse(strength_bits); + final LinkedHashSet result = new LinkedHashSet<>(ciphers); + for (int strength : strength_bits) { + moveToEnd(result, filterByStrengthBits(ciphers, strength)); + } + return result; + } + + /* + * See + * https://github.com/openssl/openssl/blob/7c96dbcdab959fef74c4caae63cdebaa354ab252/ssl/ssl_ciph.c#L1371 + */ + static LinkedHashSet defaultSort(final LinkedHashSet ciphers) { + final LinkedHashSet result = new LinkedHashSet<>(ciphers.size()); + final LinkedHashSet ecdh = new LinkedHashSet<>(ciphers.size()); + + /* Everything else being equal, prefer ephemeral ECDH over other key exchange mechanisms */ + ecdh.addAll(filterByKeyExchange(ciphers, Collections.singleton(KeyExchange.EECDH))); + + /* AES is our preferred symmetric cipher */ + Set aes = new HashSet<>(Arrays.asList(Encryption.AES128, Encryption.AES128CCM, + Encryption.AES128CCM8, Encryption.AES128GCM, Encryption.AES256, + Encryption.AES256CCM, Encryption.AES256CCM8, Encryption.AES256GCM)); + + /* Now arrange all ciphers by preference: */ + result.addAll(filterByEncryption(ecdh, aes)); + result.addAll(filterByEncryption(ciphers, aes)); + + /* Add everything else */ + result.addAll(ecdh); + result.addAll(ciphers); + + /* Low priority for MD5 */ + moveToEnd(result, filterByMessageDigest(result, Collections.singleton(MessageDigest.MD5))); + + /* Move anonymous ciphers to the end. Usually, these will remain disabled. + * (For applications that allow them, they aren't too bad, but we prefer + * authenticated ciphers.) */ + moveToEnd(result, filterByAuthentication(result, Collections.singleton(Authentication.aNULL))); + + /* Move ciphers without forward secrecy to the end */ + moveToEnd(result, filterByAuthentication(result, Collections.singleton(Authentication.ECDH))); + moveToEnd(result, filterByKeyExchange(result, Collections.singleton(KeyExchange.RSA))); + moveToEnd(result, filterByKeyExchange(result, Collections.singleton(KeyExchange.PSK))); + + /* RC4 is sort-of broken -- move the the end */ + moveToEnd(result, filterByEncryption(result, Collections.singleton(Encryption.RC4))); + return strengthSort(result); + } + + static Set filterByStrengthBits(Set ciphers, int strength_bits) { + Set result = new LinkedHashSet<>(ciphers.size()); + for (Cipher cipher : ciphers) { + if (cipher.getStrength_bits() == strength_bits) { + result.add(cipher); + } + } + return result; + } + + static Set filterByProtocol(Set ciphers, Set protocol) { + return filter(ciphers, protocol, null, null, null, null, null); + } + + static LinkedHashSet filterByKeyExchange(Set ciphers, Set kx) { + return filter(ciphers, null, kx, null, null, null, null); + } + + static LinkedHashSet filterByAuthentication(Set ciphers, Set au) { + return filter(ciphers, null, null, au, null, null, null); + } + + static Set filterByEncryption(Set ciphers, Set enc) { + return filter(ciphers, null, null, null, enc, null, null); + } + + static Set filterByEncryptionLevel(Set ciphers, Set level) { + return filter(ciphers, null, null, null, null, level, null); + } + + static Set filterByMessageDigest(Set ciphers, Set mac) { + return filter(ciphers, null, null, null, null, null, mac); + } + + static LinkedHashSet filter(Set ciphers, Set protocol, Set kx, + Set au, Set enc, Set level, Set mac) { + LinkedHashSet result = new LinkedHashSet<>(ciphers.size()); + for (Cipher cipher : ciphers) { + if (protocol != null && protocol.contains(cipher.getProtocol())) { + result.add(cipher); + } + if (kx != null && kx.contains(cipher.getKx())) { + result.add(cipher); + } + if (au != null && au.contains(cipher.getAu())) { + result.add(cipher); + } + if (enc != null && enc.contains(cipher.getEnc())) { + result.add(cipher); + } + if (level != null && level.contains(cipher.getLevel())) { + result.add(cipher); + } + if (mac != null && mac.contains(cipher.getMac())) { + result.add(cipher); + } + } + return result; + } + + public static LinkedHashSet parse(String expression) { + if (!initialized) { + init(); + } + String[] elements = expression.split(SEPARATOR); + LinkedHashSet ciphers = new LinkedHashSet<>(); + Set removedCiphers = new HashSet<>(); + for (String element : elements) { + if (element.startsWith(DELETE)) { + String alias = element.substring(1); + if (aliases.containsKey(alias)) { + remove(ciphers, alias); + } + } else if (element.startsWith(EXCLUDE)) { + String alias = element.substring(1); + if (aliases.containsKey(alias)) { + removedCiphers.addAll(aliases.get(alias)); + } else { + log.warn(sm.getString("opensslCipherConfigurationParser.unknownElement", alias)); + } + } else if (element.startsWith(TO_END)) { + String alias = element.substring(1); + if (aliases.containsKey(alias)) { + moveToEnd(ciphers, alias); + } + } else if ("@STRENGTH".equals(element)) { + strengthSort(ciphers); + break; + } else if (aliases.containsKey(element)) { + add(ciphers, element); + } else if (element.contains(AND)) { + String[] intersections = element.split("\\" + AND); + if(intersections.length > 0 && aliases.containsKey(intersections[0])) { + List result = new ArrayList<>(aliases.get(intersections[0])); + for(int i = 1; i < intersections.length; i++) { + if(aliases.containsKey(intersections[i])) { + result.retainAll(aliases.get(intersections[i])); + } + } + ciphers.addAll(result); + } + } + } + ciphers.removeAll(removedCiphers); + return ciphers; + } + + public static List convertForJSSE(Collection ciphers) { + List result = new ArrayList<>(ciphers.size()); + for (Cipher cipher : ciphers) { + result.addAll(cipher.getJsseNames()); + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("opensslCipherConfigurationParser.effectiveCiphers", displayResult(ciphers, true, ","))); + } + return result; + } + + /** + * Parse the specified expression according to the OpenSSL syntax and + * returns a list of standard JSSE cipher names. + * + * @param expression the openssl expression to define a list of cipher. + * @return the corresponding list of ciphers. + */ + public static List parseExpression(String expression) { + return convertForJSSE(parse(expression)); + } + + + /** + * Converts a JSSE cipher name to an OpenSSL cipher name. + * + * @param jsseCipherName The JSSE name for a cipher + * + * @return The OpenSSL name for the specified JSSE cipher + */ + public static String jsseToOpenSSL(String jsseCipherName) { + if (!initialized) { + init(); + } + return jsseToOpenSSL.get(jsseCipherName); + } + + + /** + * Converts an OpenSSL cipher name to a JSSE cipher name. + * + * @param opensslCipherName The OpenSSL name for a cipher + * + * @return The JSSE name for the specified OpenSSL cipher. If none is known, + * the IANA standard name will be returned instead + */ + public static String openSSLToJsse(String opensslCipherName) { + if (!initialized) { + init(); + } + List ciphers = aliases.get(opensslCipherName); + if (ciphers == null || ciphers.size() != 1) { + // Not an OpenSSL cipher name + return null; + } + Cipher cipher = ciphers.get(0); + // Each Cipher always has at least one JSSE name + return cipher.getJsseNames().iterator().next(); + } + + + static String displayResult(Collection ciphers, boolean useJSSEFormat, String separator) { + if (ciphers.isEmpty()) { + return ""; + } + StringBuilder builder = new StringBuilder(ciphers.size() * 16); + for (Cipher cipher : ciphers) { + if (useJSSEFormat) { + for (String name : cipher.getJsseNames()) { + builder.append(name); + builder.append(separator); + } + } else { + builder.append(cipher.getOpenSSLAlias()); + } + builder.append(separator); + } + return builder.toString().substring(0, builder.length() - 1); + } + + public static void usage() { + System.out.println("Usage: java " + OpenSSLCipherConfigurationParser.class.getName() + " [options] cipherspec"); + System.out.println(); + System.out.println("Displays the TLS cipher suites matching the cipherspec."); + System.out.println(); + System.out.println(" --help,"); + System.out.println(" -h Print this help message"); + System.out.println(" --openssl Show OpenSSL cipher suite names instead of IANA cipher suite names."); + System.out.println(" --verbose,"); + System.out.println(" -v Provide detailed cipher listing"); + } + + public static void main(String[] args) throws Exception + { + boolean verbose = false; + boolean useOpenSSLNames = false; + int argindex; + for(argindex = 0; argindex < args.length; ++argindex) + { + String arg = args[argindex]; + if("--verbose".equals(arg) || "-v".equals(arg)) { + verbose = true; + } else if("--openssl".equals(arg)) { + useOpenSSLNames = true; + } else if("--help".equals(arg) || "-h".equals(arg)) { + usage(); + System.exit(0); + } + else if("--".equals(arg)) { + ++argindex; + break; + } else if(arg.startsWith("-")) { + System.out.println("Unknown option: " + arg); + usage(); + System.exit(1); + } else { + // Non-switch argument... probably the cipher spec + break; + } + } + + String cipherSpec; + if(argindex < args.length) { + cipherSpec = args[argindex]; + } else { + cipherSpec = "DEFAULT"; + } + Set ciphers = parse(cipherSpec); + boolean first = true; + if(null != ciphers && 0 < ciphers.size()) { + for(Cipher cipher : ciphers) + { + if(first) { + first = false; + } else { + if(!verbose) { + System.out.print(','); + } + } + if(useOpenSSLNames) { + System.out.print(cipher.getOpenSSLAlias()); + } else { + System.out.print(cipher.name()); + } + if(verbose) { + System.out.println("\t" + cipher.getProtocol() + "\tKx=" + cipher.getKx() + "\tAu=" + cipher.getAu() + "\tEnc=" + cipher.getEnc() + "\tMac=" + cipher.getMac()); + } + } + System.out.println(); + } else { + System.out.println("No ciphers match '" + cipherSpec + "'"); + } + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/Protocol.java b/java/org/apache/tomcat/util/net/openssl/ciphers/Protocol.java new file mode 100644 index 0000000..53477df --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/Protocol.java @@ -0,0 +1,43 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +import org.apache.tomcat.util.net.Constants; + +public enum Protocol { + + SSLv3(Constants.SSL_PROTO_SSLv3), + SSLv2(Constants.SSL_PROTO_SSLv2), + TLSv1(Constants.SSL_PROTO_TLSv1), + TLSv1_2(Constants.SSL_PROTO_TLSv1_2), + TLSv1_3(Constants.SSL_PROTO_TLSv1_3); + + private final String openSSLName; + + Protocol(String openSSLName) { + this.openSSLName = openSSLName; + } + + /** + * The name returned by OpenSSL in the protocol column when using + * openssl ciphers -v. This is currently only used by the unit + * tests hence it is package private. + */ + String getOpenSSLName() { + return openSSLName; + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties new file mode 100644 index 0000000..5b4ef5c --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties @@ -0,0 +1,101 @@ +# 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. + +engine.emptyCipherSuite=Empty cipher suite +engine.engineClosed=Engine is closed +engine.failedCipherList=Some or all of cipher list [{0}] for TLS 1.2- could not be enabled +engine.failedCipherSuite=Some or all of cipher suite [{0}] for TLS 1.3+ could not be enabled +engine.failedToReadAvailableBytes=There are plain text bytes available to read but no bytes were read +engine.failedToWriteBytes=Failed to write bytes +engine.inboundClose=Inbound closed before receiving peer's close_notify +engine.invalidBufferArray=offset: [{0}], length: [{1}] (expected: offset <= offset + length <= srcs.length [{2}]) +engine.invalidDestinationBuffersState=The state of the destination buffers changed concurrently while unwrapping bytes +engine.invalidOCSPURL=The OCSP URL is invalid: [{0}] +engine.noRestrictSessionCreation=OpenSslEngine does not permit restricting the engine to only resuming existing sessions +engine.noSSLContext=No SSL context +engine.noSession=SSL session ID not available +engine.nullBuffer=Null buffer +engine.nullBufferInArray=Null buffer in array +engine.nullCipherSuite=Null cipher suite +engine.nullName=Null value name +engine.nullValue=Null value +engine.ocspParseError=Error parsing OCSP URLs +engine.ocspRequestError=Error processing OCSP request for URL [{0}] +engine.ocspResponse=OCSP response for URL [{0}] was [{1}] +engine.openSSLError=OpenSSL error: [{0}] message: [{1}] +engine.oversizedPacket=Encrypted packet is oversized +engine.unsupportedCipher=Unsupported cipher suite: [{0}] [{1}] +engine.unsupportedProtocol=Protocol [{0}] is not supported +engine.unverifiedPeer=Peer unverified + +openssl.X509FactoryError=Error getting X509 factory instance +openssl.addedClientCaCert=Added client CA cert: [{0}] +openssl.applyConf=Applying OpenSSLConfCmd to SSL context +openssl.certificateVerificationFailed=Certificate verification failed +openssl.checkConf=Checking OpenSSLConf +openssl.doubleInit=SSL context already initialized, ignoring +openssl.errApplyConf=Could not apply OpenSSLConf to SSL context +openssl.errCheckConf=Error during OpenSSLConf check +openssl.errMakeConf=Could not create OpenSSLConf context [{0}] +openssl.errorAddingCertificate=Error adding certificate to chain: [{0}] +openssl.errorConfiguringLocations=Error configuring CA certificate locations: [{0}] +openssl.errorLoadingCertificate=Error loading certificate: [{0}] +openssl.errorLoadingCertificateWithError=Error loading certificate [{0}] with error [{1}] +openssl.errorLoadingPassword=Error loading password file: [{0}] +openssl.errorLoadingPrivateKey=Error loading private key: [{0}] +openssl.errorLoadingCertificateRevocationListWithError=Error loading certificate revocation [{0}] with error [{1}] +openssl.errorPrivateKeyCheck=Private key does not match the certificate public key: [{0}] +openssl.errorSSLCtxInit=Error initializing SSL context +openssl.invalidSslProtocol=An invalid value [{0}] was provided for the SSLProtocol attribute +openssl.keyManagerMissing=No key manager found +openssl.makeConf=Creating OpenSSLConf context +openssl.noCACerts=No CA certificates were configured +openssl.nonJsseCertificate=The certificate [{0}] or its private key [{1}] could not be processed using a JSSE key manager and will be given directly to OpenSSL +openssl.nonJsseChain=The certificate chain [{0}] was not specified or was not valid and JSSE requires a valid certificate chain so attempting to use OpenSSL directly +openssl.passwordTooLong=The certificate password is too long +openssl.setCustomDHParameters=Setting custom DH parameters ([{0}] bits) for the key [{1}] +openssl.setECDHCurve=Setting ECDH curve ([{0}]) for the key [{1}] +openssl.trustManagerMissing=No trust manager found + +opensslconf.applyCommand=OpenSSLConf applying command (name [{0}], value [{1}]) +opensslconf.applyFailed=Failure while applying OpenSSLConf to SSL context +opensslconf.badDirectory=Command name [{0}] uses missing directory [{1}]) +opensslconf.badFile=Command name [{0}] uses missing or unreadable file [{1}]) +opensslconf.checkCommand=OpenSSLConf checking command (name [{0}], value [{1}]) +opensslconf.checkFailed=Failure while checking OpenSSLConf [{0}] +opensslconf.commandError=OpenSSLConf failed command (name [{0}], value [{1}]) caused error [{2}] +opensslconf.failedCommand=OpenSSLConf failed command (name [{0}], value [{1}]) with result [{2}] - will be ignored +opensslconf.finishFailed=OpenSSLConf finish failed with result [{0}] +opensslconf.noCommandName=OpenSSLConf no command name - will be ignored (command value [{0}]) +opensslconf.resultCommand=OpenSSLConf command (name [{0}], value [{1}]) returned [{2}] +opensslconf.unknownCommandType=SSL_CONF command [{0}] type unknown + +sessionContext.nullTicketKeys=Null keys + +openssllibrary.ciphersFailure=Failed getting cipher list +openssllibrary.currentFIPSMode=Current FIPS mode: [{0}] +openssllibrary.engineError=Error creating engine +openssllibrary.enterAlreadyInFIPSMode=AprLifecycleListener is configured to force entering FIPS mode, but library is already in FIPS mode [{0}] +openssllibrary.initializeFIPSFailed=Failed to enter FIPS mode +openssllibrary.initializeFIPSSuccess=Successfully entered FIPS mode +openssllibrary.initializedOpenSSL=OpenSSL successfully initialized using FFM [{0}] +openssllibrary.initializingFIPS=Initializing FIPS mode... +openssllibrary.requireNotInFIPSMode=The listener is configured to require the library to already be in FIPS mode, but it was not in FIPS mode +openssllibrary.skipFIPSInitialization=Already in FIPS mode; skipping FIPS initialization. +openssllibrary.tooLateForFIPSMode=Cannot setFIPSMode: SSL has already been initialized +openssllibrary.tooLateForSSLEngine=Cannot setSSLEngine: SSL has already been initialized +openssllibrary.tooLateForSSLRandomSeed=Cannot setSSLRandomSeed: SSL has already been initialized +openssllibrary.wrongFIPSMode=Unexpected value of FIPSMode option of AprLifecycleListener: [{0}] + diff --git a/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings_fr.properties b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings_fr.properties new file mode 100644 index 0000000..3d40e40 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings_fr.properties @@ -0,0 +1,98 @@ +# 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. + +engine.emptyCipherSuite=La suite de chiffrement (cipher suite) est vide +engine.engineClosed=Le moteur a déjà été fermé +engine.failedCipherList=Tout ou partie de la liste de chiffres [{0}] pour TLS 1.2 ou inférieur n''a pas pu être activée +engine.failedCipherSuite=Tout ou partie de la liste de chiffres [{0}] pour TLS 1.3 ou supérieur n''a pas pu être activée +engine.failedToReadAvailableBytes=Il y a des octets en clair disponibles à lire mais aucun octet n'a été lu +engine.failedToWriteBytes=Erreur d'écriture des octets +engine.inboundClose=L'entrée a été fermée avant d'avoir recu le close_notify du pair +engine.invalidBufferArray=offset : [{0}], length : [{1}] (attendu : offset <= offset + length <= srcs.length [{2}]) +engine.invalidDestinationBuffersState=L'état des buffers de destination a changé de manière concurrente lors de l'unwrap des octets +engine.invalidOCSPURL=L''URL OCSP est invalide: [{0}] +engine.noRestrictSessionCreation=OpenSslEngine ne permet pas de restreindre le moteur à la récupération des sessions existantes +engine.noSSLContext=Pas de contexte SSL +engine.noSession=ID de session SSL non disponible +engine.nullBuffer=Tampon null +engine.nullBufferInArray=Tampon null dans le tableau +engine.nullCipherSuite=Suite de chiffres null +engine.nullName=La valeur du nom est null +engine.nullValue=La valeur est null +engine.ocspParseError=Erreur de traitement des URLs OCSP +engine.ocspRequestError=Erreur de traitement de la requête OCSP pour l''URL [{0}] +engine.openSSLError=Erreur OpenSSL : [{0}] message : [{1}] +engine.oversizedPacket=Le paquet crypté est trop gros +engine.unsupportedCipher=Suite de chiffres non supportée : [{0}] [{1}] +engine.unsupportedProtocol=Le protocole [{0}] n''est pas supporté +engine.unverifiedPeer=Le pair n'est pas vérifié + +openssl.X509FactoryError=Impossible d'obtenir l'instance de la fabrique X509 +openssl.addedClientCaCert=Ajout du certificat CA du client : [{0}] +openssl.applyConf=Application de OpenSSLConfCmd au contexte SSL +openssl.certificateVerificationFailed=La vérification du certificat a échoué +openssl.checkConf=Vérification de OpenSSLConf en cours +openssl.doubleInit=Le contexte SSL a déjà  été initialisé, ignoré +openssl.errApplyConf=Impossible d'appliquer la OpenSSLConf au contexte SSL +openssl.errCheckConf=Erreur pendant la vérification de OpenSSLConf +openssl.errMakeConf=Impossible de créer le contexte de OpenSSLConf +openssl.errorAddingCertificate=Erreur lors de l''ajout du certificat à la chaîne: [{0}] +openssl.errorConfiguringLocations=Erreur lors de la configuration des emplacements des certificats CA: [{0}] +openssl.errorLoadingCertificate=Erreur lors du chargement du certificat: [{0}] +openssl.errorLoadingCertificateRevocationListWithError=Erreur de chargement de la révocation des certificats [{0}] avec l''erreur [{1}] +openssl.errorLoadingCertificateWithError=Erreur de chargement du certificat [{0}] avec l''erreur [{1}] +openssl.errorLoadingPassword=Erreur lors du chargment du fichier mot de passe: [{0}] +openssl.errorLoadingPrivateKey=Erreur lors du chargment de la clé privée: [{0}] +openssl.errorPrivateKeyCheck=La clé privée ne correspond pas à la clé publique du certificat: [{0}] +openssl.errorSSLCtxInit=Erreur d'initialisation du contexte SSL +openssl.keyManagerMissing=Aucun gestionnaire de clés trouvé +openssl.makeConf=Création du contexte de OpenSSLConf +openssl.noCACerts=Aucun certificat CA n'a été configuré +openssl.nonJsseCertificate=Le certificat [{0}] ou sa clé privée [{1}] n''a pas pu être traité en utilisant un gestionnaire de clé de JSSE, et sera directement passé à OpenSSL +openssl.nonJsseChain=La chaîne de certificat [{0}] n''a pas été spécifiée ou est invalide et JSSE requiert une chaîne de certificats valide, donc OpenSSL sera utilisé directement +openssl.passwordTooLong=Le mot de passe du certificat est trop long +openssl.setCustomDHParameters=Configuration de paramètres DH personnalisés ([{0}] bits) pour la clé [{1}] +openssl.setECDHCurve=Configuration de la courbure ECDH ([{0}]) pour la clé [{1}] +openssl.trustManagerMissing=Gestionnaire de confiance non trouvé + +opensslconf.applyCommand=Application de la commande OpenSSLConf (nom [{0}] valeur [{1}]) +opensslconf.applyFailed=Erreur en appliquant OpenSSLConf au contexte SSL +opensslconf.badDirectory=La commande de nom [{0}] utilise un répertoire absent [{1}] +opensslconf.badFile=La commande de nom [{0}] utilise un fichier illisible ou absent [{1}] +opensslconf.checkCommand=Vérification de la commande OpenSSLConf (nom [{0}] valeur [{1}]) +opensslconf.checkFailed=Echec de la vérification de OpenSSLConf [{0}] +opensslconf.commandError=La commande OpenSSLConf (nom [{0}] valeur [{1}]) a causé une erreur [{2}] +opensslconf.failedCommand=La commande OpenSSLConf (nom [{0}] valeur [{1}]) a échoué avec le résultat [{2}] qui sera ignoré +opensslconf.finishFailed=OpenSSLConf s''est terminé en échec avec le résultat [{0}] +opensslconf.noCommandName=Pas de nom de commande OpenSSLConf (valeur [{0}]), cela sera ignoré +opensslconf.resultCommand=La commande OpenSSLConf (nom [{0}] valeur [{1}]) a retourné [{2}] +opensslconf.unknownCommandType=La commande SSL_CONF [{0}] est de type inconnu + +openssllibrary.ciphersFailure=Echec en essayant d'obtenir la liste des chiffres +openssllibrary.currentFIPSMode=Mode FIPS actuel : [{0}] +openssllibrary.engineError=Erreur lors de la création du moteur SSL +openssllibrary.enterAlreadyInFIPSMode=La librarie est configurée pour forcer le mode FIPS mais la librairie est déjà en mode FIPS [{0}] +openssllibrary.initializeFIPSFailed=Echec d'entrée en mode FIPS +openssllibrary.initializeFIPSSuccess=Entrée avec succés en mode FIPS +openssllibrary.initializedOpenSSL=OpenSSL a été initialisé avec succés en utilisant FFM [{0}] +openssllibrary.initializingFIPS=Initialisation du mode FIPS ... +openssllibrary.requireNotInFIPSMode=La librairie est configurée pour demander que la librarie soit déjà en mode FIPS et elle ne l'était pas +openssllibrary.skipFIPSInitialization=Déjà en mode FIPS, l'initialisation de FIPS n'est pas effectuée +openssllibrary.tooLateForFIPSMode=Ne peut pas passer en mode FIPS: SSL a déjà été initialisé +openssllibrary.tooLateForSSLEngine=Impossible d'appeler setSSLEngine: SSL a déjà été initialisé +openssllibrary.tooLateForSSLRandomSeed=setSSLRandomSeed impossible : SSL a déjà été initialisé +openssllibrary.wrongFIPSMode=Valeur inattendue de l''option FIPSMode: [{0}] + +sessionContext.nullTicketKeys=Null keys diff --git a/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings_ja.properties b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings_ja.properties new file mode 100644 index 0000000..e4fb062 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings_ja.properties @@ -0,0 +1,98 @@ +# 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. + +engine.emptyCipherSuite=空ã®æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆ +engine.engineClosed=EngineãŒé–‰ã˜ã‚‰ã‚Œã¾ã—㟠+engine.failedCipherList=TLS 1.2 以å‰ã®æš—å·ãƒªã‚¹ãƒˆ [{0}] ã®ä¸€éƒ¨ã¾ãŸã¯ã™ã¹ã¦ã‚’有効ã«ã§ãã¾ã›ã‚“ã§ã—㟠+engine.failedCipherSuite=TLS 1.3 ä»¥é™ ã®æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆ [{0}] ã®ä¸€éƒ¨ã¾ãŸã¯ã™ã¹ã¦ã‚’有効ã«ã§ãã¾ã›ã‚“ã§ã—㟠+engine.failedToReadAvailableBytes=読ã¿å–ã‚Šå¯èƒ½ãªãƒ—レーン テキスト ãƒã‚¤ãƒˆãŒã‚ã‚Šã¾ã™ãŒã€ãƒã‚¤ãƒˆã¯èª­ã¿å–られã¾ã›ã‚“ã§ã—㟠+engine.failedToWriteBytes=ãƒã‚¤ãƒˆã®æ›¸ãè¾¼ã¿ã«å¤±æ•— +engine.inboundClose=ピア㮠close_notify ã‚’å—ä¿¡ã™ã‚‹å‰ã«ã‚¤ãƒ³ãƒã‚¦ãƒ³ãƒ‰ãŒã‚¯ãƒ­ãƒ¼ã‚ºã•ã‚Œã¾ã—㟠+engine.invalidBufferArray=オフセット: [{0}], é•·ã•: [{1}] (期待値: offset <= offset + length <= srcs.length [{2}]) +engine.invalidDestinationBuffersState=ãƒã‚¤ãƒˆã®ã‚¢ãƒ³ãƒ©ãƒƒãƒ—中ã«å®›å…ˆãƒãƒƒãƒ•ã‚¡ã®çŠ¶æ…‹ãŒåŒæ™‚ã«å¤‰æ›´ã•ã‚Œã¾ã—㟠+engine.invalidOCSPURL=OCSP URLãŒç„¡åŠ¹: [{0}] +engine.noRestrictSessionCreation=OpenSslEngine ã§ã¯ã€ã‚¨ãƒ³ã‚¸ãƒ³ã‚’既存ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®å†é–‹ã®ã¿ã«åˆ¶é™ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +engine.noSSLContext=SSLコンテキストãŒã‚ã‚Šã¾ã›ã‚“ +engine.noSession=SSLセッションIDãŒä½¿ç”¨ã§ãã¾ã›ã‚“ +engine.nullBuffer=Nullãƒãƒƒãƒ•ã‚¡ +engine.nullBufferInArray=é…列内ã®Nullãƒãƒƒãƒ•ã‚¡ +engine.nullCipherSuite=Nullæš—å·ã‚¹ã‚¤ãƒ¼ãƒˆ +engine.nullName=Null値ã®åå‰ +engine.nullValue=Null値 +engine.ocspParseError=OCSP URLã®ãƒ‘ース失敗 +engine.ocspRequestError=URL [{0}] ã® OCSP リクエストã®å‡¦ç†ã‚¨ãƒ©ãƒ¼ +engine.openSSLError=OpenSSLエラー: [{0}] メッセージ: [{1}] +engine.oversizedPacket=æš—å·åŒ–ã•ã‚ŒãŸãƒ‘ケットã®ã‚µã‚¤ã‚ºãŒå¤§ãã™ãŽã¾ã™ +engine.unsupportedCipher=サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„æš—å·ã‚¹ã‚¤ãƒ¼ãƒˆ: [{0}] [{1}] +engine.unsupportedProtocol=プロトコル [{0}] ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“ +engine.unverifiedPeer=ピアãŒæœªæ¤œè¨¼ + +openssl.X509FactoryError=X509 ファクトリ インスタンスã®å–得エラー +openssl.addedClientCaCert=追加ã•ã‚ŒãŸã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆCA証明書: [{0}] +openssl.applyConf=OpenSSLConfCmd ã‚’ SSL コンテキストã«é©ç”¨ +openssl.certificateVerificationFailed=証明書ã®æ¤œè¨¼ã«å¤±æ•— +openssl.checkConf=OpenSSLConf ã®ãƒã‚§ãƒƒã‚¯ +openssl.doubleInit=SSL コンテキストã¯ã™ã§ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€ç„¡è¦–ã•ã‚Œã¾ã™ +openssl.errApplyConf=OpenSSLConf ã‚’ SSL コンテキストã«é©ç”¨ã§ãã¾ã›ã‚“ã§ã—㟠+openssl.errCheckConf=OpenSSLConf ãƒã‚§ãƒƒã‚¯ä¸­ã®ã‚¨ãƒ©ãƒ¼ +openssl.errMakeConf=OpenSSLConfコンテキストãŒä½œæˆã§ãã¾ã›ã‚“ã§ã—㟠+openssl.errorAddingCertificate=証明書をãƒã‚§ãƒ¼ãƒ³ã«è¿½åŠ ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ: [{0}] +openssl.errorConfiguringLocations=CA 証明書ã®å ´æ‰€ã®æ§‹æˆã‚¨ãƒ©ãƒ¼: [{0}] +openssl.errorLoadingCertificate=証明書ã®èª­ã¿è¾¼ã¿ã‚¨ãƒ©ãƒ¼: [{0}] +openssl.errorLoadingCertificateRevocationListWithError=証明書失効 [{0}] ã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ [{1}] ãŒç™ºç”Ÿã—ã¾ã—㟠+openssl.errorLoadingCertificateWithError=証明書 [{0}] ã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ [{1}] ãŒç™ºç”Ÿã—ã¾ã—㟠+openssl.errorLoadingPassword=パスワードファイルã®èª­ã¿è¾¼ã¿ã‚¨ãƒ©ãƒ¼: [{0}] +openssl.errorLoadingPrivateKey=秘密éµã®èª­ã¿è¾¼ã¿ã‚¨ãƒ©ãƒ¼: [{0}] +openssl.errorPrivateKeyCheck=秘密éµãŒè¨¼æ˜Žæ›¸ã®å…¬é–‹éµã¨ä¸€è‡´ã—ã¾ã›ã‚“: [{0}] +openssl.errorSSLCtxInit=SSLコンテキストã®åˆæœŸåŒ–エラー +openssl.keyManagerMissing=キーマãƒãƒ¼ã‚¸ãƒ£ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ +openssl.makeConf=OpenSSLConf コンテキストã®ä½œæˆ +openssl.noCACerts=CA証明書ãŒè¨­å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ã§ã—㟠+openssl.nonJsseCertificate=証明書 [{0}] ã¾ãŸã¯ãã®ç§˜å¯†éµ [{1}] ã¯ã€JSSE キーマãƒãƒ¼ã‚¸ãƒ£ã‚’使用ã—ã¦å‡¦ç†ã§ããªã‹ã£ãŸãŸã‚ã€OpenSSL ã«ç›´æŽ¥æ¸¡ã•ã‚Œã¾ã™ +openssl.nonJsseChain=証明書ãƒã‚§ãƒ¼ãƒ³ [{0}] ãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ã‹ç„¡åŠ¹ã§ã‚ã‚Šã€JSSE ã«ã¯æœ‰åŠ¹ãªè¨¼æ˜Žæ›¸ãƒã‚§ãƒ¼ãƒ³ãŒå¿…è¦ã§ã‚ã‚‹ãŸã‚ã€OpenSSL を直接使用ã—よã†ã¨ã—ã¦ã„ã¾ã™ +openssl.passwordTooLong=証明書ã®ãƒ‘スワードãŒé•·ã™ãŽã¾ã™ +openssl.setCustomDHParameters=キー [{1}] ã®ã‚«ã‚¹ã‚¿ãƒ  DH パラメータ ([{0}] ビット) を設定ã—ã¾ã™ +openssl.setECDHCurve=キー [{1}] ã« ECDH curve ([{0}]) を設定ã—ã¾ã™ +openssl.trustManagerMissing=トラストマãƒãƒ¼ã‚¸ãƒ£ãƒ¼ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ + +opensslconf.applyCommand=OpenSSLConfã¯ã‚³ãƒžãƒ³ãƒ‰ï¼ˆåå‰ [{0}], 値 [{1}])をé©ç”¨ã—ã¦ã„ã¾ã™ +opensslconf.applyFailed=SSL コンテキストã¸ã®OpenSSLConf é©ç”¨ã«å¤±æ•— +opensslconf.badDirectory=コマンドå [{0}] ã¯å­˜åœ¨ã—ãªã„ディレクトリ [{1}] を使用ã—ã¦ã„ã¾ã™) +opensslconf.badFile=コマンドå [{0}] ã¯ã€å­˜åœ¨ã—ãªã„ã‹èª­ã¿å–ã‚Šä¸å¯èƒ½ãªãƒ•ã‚¡ã‚¤ãƒ« [{1}] を使用ã—ã¦ã„ã¾ã™) +opensslconf.checkCommand=OpenSSLConf ã¯ã‚³ãƒžãƒ³ãƒ‰ (åå‰ [{0}]ã€å€¤ [{1}])ã‚’ãƒã‚§ãƒƒã‚¯ã—ã¦ã„ã¾ã™ +opensslconf.checkFailed=OpenSSLConf [{0}] ã®ãƒã‚§ãƒƒã‚¯ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—㟠+opensslconf.commandError=エラー [{2}] ã®ãŸã‚ã€OpenSSLConf ã¯ã‚³ãƒžãƒ³ãƒ‰ï¼ˆåå‰[{0}]ã€å€¤ [{1}])ãŒå¤±æ•—ã—ã¾ã—㟠+opensslconf.failedCommand=OpenSSLConfã¯å®Ÿè¡Œçµæžœ[{2}] ã®ã‚³ãƒžãƒ³ãƒ‰ï¼ˆåå‰: [{0}], 値: [{1})ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ +opensslconf.finishFailed=OpenSSLConf ã®çµ‚了ãŒçµæžœ [{0}] ã§å¤±æ•—ã—ã¾ã—㟠+opensslconf.noCommandName=OpenSSLConf コマンドåãŒã‚ã‚Šã¾ã›ã‚“ - 無視ã•ã‚Œã¾ã™ (コマンド値 [{0}]) +opensslconf.resultCommand=OpenSSLConf コマンド (name [{0}], value [{1}]) ã¯[{2}]ã‚’è¿”ã—ã¾ã—㟠+opensslconf.unknownCommandType=SSL_CONFコマンド [{0}]タイプãŒä¸æ˜Žã§ã™ + +openssllibrary.ciphersFailure=æš—å·ãƒªã‚¹ãƒˆã®å–å¾—ã«å¤±æ•—ã—ã¾ã—㟠+openssllibrary.currentFIPSMode=ç¾åœ¨ã®FIPSモード: [{0}] +openssllibrary.engineError=エンジンã®ä½œæˆã‚¨ãƒ©ãƒ¼ +openssllibrary.enterAlreadyInFIPSMode=AprLifecycleListener 㯠FIPS モードã«å¼·åˆ¶çš„ã«å…¥ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ãŒã€ãƒ©ã‚¤ãƒ–ラリã¯ã™ã§ã« FIPS モードã«ãªã£ã¦ã„ã¾ã™ [{0}] +openssllibrary.initializeFIPSFailed=FIPS モードã«å…¥ã‚‹ã®ã«å¤±æ•—ã—ã¾ã—㟠+openssllibrary.initializeFIPSSuccess=FIPS モードã«å…¥ã‚Šã¾ã—㟠+openssllibrary.initializedOpenSSL=OpenSSLã¯FFM [{0}] を使用ã—ã¦æ­£å¸¸ã«åˆæœŸåŒ–ã•ã‚Œã¾ã—㟠+openssllibrary.initializingFIPS=FIPSモードをåˆæœŸåŒ–中... +openssllibrary.requireNotInFIPSMode=リスナーã¯ã€ãƒ©ã‚¤ãƒ–ラリãŒã™ã§ã« FIPS モードã§ã‚ã‚‹ã“ã¨ã‚’è¦æ±‚ã™ã‚‹ã‚ˆã†ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ãŒã€FIPS モードã§ã¯ã‚ã‚Šã¾ã›ã‚“ã§ã—㟠+openssllibrary.skipFIPSInitialization=æ—¢ã«FIPSモードã§ã™; FIPSã®åˆæœŸåŒ–をスキップã—ã¾ã™ã€‚ +openssllibrary.tooLateForFIPSMode=FIPSModeを設定ã§ãã¾ã›ã‚“: SSL ã¯ã™ã§ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã™ +openssllibrary.tooLateForSSLEngine=SSLEngineを設定ã§ãã¾ã›ã‚“: SSLã¯æ—¢ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã™ +openssllibrary.tooLateForSSLRandomSeed=SSLRandomSeedを設定ã§ãã¾ã›ã‚“: SSLã¯æ—¢ã«åˆæœŸåŒ–ã•ã‚Œã¦ã„ã¾ã™ +openssllibrary.wrongFIPSMode=AprLifecycleListener ã® FIPSMode オプションã®äºˆæœŸã—ãªã„値: [{0}] + +sessionContext.nullTicketKeys=Nullキー diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java new file mode 100644 index 0000000..f72606b --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java @@ -0,0 +1,1421 @@ +/* + * 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.tomcat.util.net.openssl.panama; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.ref.Cleaner; +import java.lang.ref.Cleaner.Cleanable; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Iterator; +import java.util.List; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import static org.apache.tomcat.util.openssl.openssl_h.*; +import static org.apache.tomcat.util.openssl.openssl_h_Compatibility.*; +import static org.apache.tomcat.util.openssl.openssl_h_Macros.*; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.file.ConfigFileLoader; +import org.apache.tomcat.util.file.ConfigurationSource.Resource; +import org.apache.tomcat.util.net.Constants; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.openssl.OpenSSLConf; +import org.apache.tomcat.util.net.openssl.OpenSSLConfCmd; +import org.apache.tomcat.util.net.openssl.OpenSSLStatus; +import org.apache.tomcat.util.net.openssl.OpenSSLUtil; +import org.apache.tomcat.util.openssl.SSL_CTX_set_alpn_select_cb$cb; +import org.apache.tomcat.util.openssl.SSL_CTX_set_cert_verify_callback$cb; +import org.apache.tomcat.util.openssl.SSL_CTX_set_tmp_dh_callback$dh; +import org.apache.tomcat.util.openssl.SSL_CTX_set_verify$callback; +import org.apache.tomcat.util.openssl.pem_password_cb; +import org.apache.tomcat.util.res.StringManager; + +public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { + + private static final Log log = LogFactory.getLog(OpenSSLContext.class); + private static final StringManager sm = StringManager.getManager(OpenSSLContext.class); + + private static final Cleaner cleaner = Cleaner.create(); + + private static final String defaultProtocol = "TLS"; + + private static final int SSL_AIDX_RSA = 0; + private static final int SSL_AIDX_DSA = 1; + private static final int SSL_AIDX_ECC = 3; + private static final int SSL_AIDX_MAX = 4; + + public static final int SSL_PROTOCOL_NONE = 0; + public static final int SSL_PROTOCOL_SSLV2 = (1<<0); + public static final int SSL_PROTOCOL_SSLV3 = (1<<1); + public static final int SSL_PROTOCOL_TLSV1 = (1<<2); + public static final int SSL_PROTOCOL_TLSV1_1 = (1<<3); + public static final int SSL_PROTOCOL_TLSV1_2 = (1<<4); + public static final int SSL_PROTOCOL_TLSV1_3 = (1<<5); + public static final int SSL_PROTOCOL_ALL = (SSL_PROTOCOL_TLSV1 | SSL_PROTOCOL_TLSV1_1 | SSL_PROTOCOL_TLSV1_2 | + SSL_PROTOCOL_TLSV1_3); + + static final int OPTIONAL_NO_CA = 3; + + private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY-----\n"; + private static final Object END_KEY = "\n-----END PRIVATE KEY-----"; + + private static final byte[] HTTP_11_PROTOCOL = + new byte[] { 'h', 't', 't', 'p', '/', '1', '.', '1' }; + + private static final byte[] DEFAULT_SESSION_ID_CONTEXT = + new byte[] { 'd', 'e', 'f', 'a', 'u', 'l', 't' }; + + static final CertificateFactory X509_CERT_FACTORY; + static { + try { + X509_CERT_FACTORY = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new IllegalStateException(sm.getString("openssl.X509FactoryError"), e); + } + } + + static final boolean OPENSSL_3 = (OpenSSL_version_num() >= 0x3000000fL); + + private final SSLHostConfig sslHostConfig; + private final SSLHostConfigCertificate certificate; + private final boolean alpn; + private final int minTlsVersion; + private final int maxTlsVersion; + private final List negotiableProtocols; + + private OpenSSLSessionContext sessionContext; + private String enabledProtocol; + private boolean initialized = false; + + private boolean noOcspCheck = false; + private X509TrustManager x509TrustManager; + + private final ContextState state; + private final Arena contextArena; + private final Cleanable cleanable; + + private static String[] getCiphers(MemorySegment sslCtx) { + MemorySegment sk = SSL_CTX_get_ciphers(sslCtx); + int len = OPENSSL_sk_num(sk); + if (len <= 0) { + return null; + } + ArrayList ciphers = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + MemorySegment cipher = OPENSSL_sk_value(sk, i); + MemorySegment cipherName = SSL_CIPHER_get_name(cipher); + ciphers.add(cipherName.getString(0)); + } + return ciphers.toArray(new String[0]); + } + + public OpenSSLContext(SSLHostConfigCertificate certificate, List negotiableProtocols) + throws SSLException { + + // Check that OpenSSL was initialized + if (!OpenSSLStatus.isInitialized()) { + try { + OpenSSLLibrary.init(); + } catch (Exception e) { + throw new SSLException(e); + } + } + + this.sslHostConfig = certificate.getSSLHostConfig(); + this.certificate = certificate; + contextArena = Arena.ofAuto(); + + MemorySegment sslCtx = MemorySegment.NULL; + MemorySegment confCtx = MemorySegment.NULL; + List negotiableProtocolsBytes = null; + boolean success = false; + try { + // Create OpenSSLConfCmd context if used + OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); + if (openSslConf != null) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("openssl.makeConf")); + } + confCtx = SSL_CONF_CTX_new(); + if (MemorySegment.NULL.equals(confCtx)) { + throw new SSLException(sm.getString("openssl.errMakeConf", getLastError())); + } + SSL_CONF_CTX_set_flags(confCtx, SSL_CONF_FLAG_FILE() | + SSL_CONF_FLAG_SERVER() | + SSL_CONF_FLAG_CERTIFICATE() | + SSL_CONF_FLAG_SHOW_ERRORS()); + } + + // SSL protocol + sslCtx = SSL_CTX_new(TLS_server_method()); + + int protocol = SSL_PROTOCOL_NONE; + for (String enabledProtocol : sslHostConfig.getEnabledProtocols()) { + if (Constants.SSL_PROTO_SSLv2Hello.equalsIgnoreCase(enabledProtocol)) { + // NO-OP. OpenSSL always supports SSLv2Hello + } else if (Constants.SSL_PROTO_SSLv2.equalsIgnoreCase(enabledProtocol)) { + protocol |= SSL_PROTOCOL_SSLV2; + } else if (Constants.SSL_PROTO_SSLv3.equalsIgnoreCase(enabledProtocol)) { + protocol |= SSL_PROTOCOL_SSLV3; + } else if (Constants.SSL_PROTO_TLSv1.equalsIgnoreCase(enabledProtocol)) { + protocol |= SSL_PROTOCOL_TLSV1; + } else if (Constants.SSL_PROTO_TLSv1_1.equalsIgnoreCase(enabledProtocol)) { + protocol |= SSL_PROTOCOL_TLSV1_1; + } else if (Constants.SSL_PROTO_TLSv1_2.equalsIgnoreCase(enabledProtocol)) { + protocol |= SSL_PROTOCOL_TLSV1_2; + } else if (Constants.SSL_PROTO_TLSv1_3.equalsIgnoreCase(enabledProtocol)) { + protocol |= SSL_PROTOCOL_TLSV1_3; + } else if (Constants.SSL_PROTO_ALL.equalsIgnoreCase(enabledProtocol)) { + protocol |= SSL_PROTOCOL_ALL; + } else { + // Should not happen since filtering to build + // enabled protocols removes invalid values. + throw new Exception(sm.getString("openssl.invalidSslProtocol", enabledProtocol)); + } + } + // Set maximum and minimum protocol versions + int prot = SSL2_VERSION(); + if ((protocol & SSL_PROTOCOL_TLSV1_3) > 0) { + prot = TLS1_3_VERSION(); + } else if ((protocol & SSL_PROTOCOL_TLSV1_2) > 0) { + prot = TLS1_2_VERSION(); + } else if ((protocol & SSL_PROTOCOL_TLSV1_1) > 0) { + prot = TLS1_1_VERSION(); + } else if ((protocol & SSL_PROTOCOL_TLSV1) > 0) { + prot = TLS1_VERSION(); + } else if ((protocol & SSL_PROTOCOL_SSLV3) > 0) { + prot = SSL3_VERSION(); + } + maxTlsVersion = prot; + SSL_CTX_set_max_proto_version(sslCtx, prot); + if (prot == TLS1_3_VERSION() && (protocol & SSL_PROTOCOL_TLSV1_2) > 0) { + prot = TLS1_2_VERSION(); + } + if (prot == TLS1_2_VERSION() && (protocol & SSL_PROTOCOL_TLSV1_1) > 0) { + prot = TLS1_1_VERSION(); + } + if (prot == TLS1_1_VERSION() && (protocol & SSL_PROTOCOL_TLSV1) > 0) { + prot = TLS1_VERSION(); + } + if (prot == TLS1_VERSION() && (protocol & SSL_PROTOCOL_SSLV3) > 0) { + prot = SSL3_VERSION(); + } + minTlsVersion = prot; + SSL_CTX_set_min_proto_version(sslCtx, prot); + + // Disable compression, usually unsafe + SSL_CTX_set_options(sslCtx, SSL_OP_NO_COMPRESSION()); + + // Disallow a session from being resumed during a renegotiation, + // so that an acceptable cipher suite can be negotiated. + SSL_CTX_set_options(sslCtx, SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION()); + + SSL_CTX_set_options(sslCtx, SSL_OP_SINGLE_DH_USE()); + SSL_CTX_set_options(sslCtx, SSL_OP_SINGLE_ECDH_USE()); + + // Default session context id and cache size + SSL_CTX_sess_set_cache_size(sslCtx, 256); + + // Session cache is disabled by default + SSL_CTX_set_session_cache_mode(sslCtx, SSL_SESS_CACHE_OFF()); + + // Longer session timeout + SSL_CTX_set_timeout(sslCtx, 14400); + + // Set int pem_password_cb(char *buf, int size, int rwflag, void *u) callback + SSL_CTX_set_default_passwd_cb(sslCtx, + pem_password_cb.allocate(new PasswordCallback(null), contextArena)); + + if (negotiableProtocols != null && negotiableProtocols.size() > 0) { + alpn = true; + negotiableProtocolsBytes = new ArrayList<>(negotiableProtocols.size() + 1); + for (String negotiableProtocol : negotiableProtocols) { + negotiableProtocolsBytes.add(negotiableProtocol.getBytes(StandardCharsets.ISO_8859_1)); + } + negotiableProtocolsBytes.add(HTTP_11_PROTOCOL); + } else { + alpn = false; + } + + success = true; + } catch(Exception e) { + throw new SSLException(sm.getString("openssl.errorSSLCtxInit"), e); + } finally { + this.negotiableProtocols = negotiableProtocolsBytes; + state = new ContextState(sslCtx, confCtx); + /* + * When an SSLHostConfig is replaced at runtime, it is not possible to + * call destroy() on the associated OpenSSLContext since it is likely + * that there will be in-progress connections using the OpenSSLContext. + * A reference chain has been deliberately established (see + * OpenSSLSessionContext) to ensure that the OpenSSLContext remains + * ineligible for GC while those connections are alive. Once those + * connections complete, the OpenSSLContext will become eligible for GC + * and the memory session will ensure that the associated native + * resources are cleaned up. + */ + cleanable = cleaner.register(this, state); + + if (!success) { + destroy(); + } + } + } + + + public String getEnabledProtocol() { + return enabledProtocol; + } + + + public void setEnabledProtocol(String protocol) { + enabledProtocol = (protocol == null) ? defaultProtocol : protocol; + } + + + @Override + public void destroy() { + cleanable.clean(); + } + + + private boolean checkConf(OpenSSLConf conf) throws Exception { + boolean result = true; + OpenSSLConfCmd cmd; + String name; + String value; + int rc; + for (OpenSSLConfCmd command : conf.getCommands()) { + cmd = command; + name = cmd.getName(); + value = cmd.getValue(); + if (name == null) { + log.error(sm.getString("opensslconf.noCommandName", value)); + result = false; + continue; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("opensslconf.checkCommand", name, value)); + } + try (var localArena = Arena.ofConfined()) { + if (name.equals("NO_OCSP_CHECK")) { + rc = 1; + } else { + int code = SSL_CONF_cmd_value_type(state.confCtx, localArena.allocateFrom(name)); + rc = 1; + String errorMessage = getLastError(); + if (errorMessage != null) { + log.error(sm.getString("opensslconf.checkFailed", errorMessage)); + rc = 0; + } + if (code == SSL_CONF_TYPE_UNKNOWN()) { + log.error(sm.getString("opensslconf.typeUnknown", name)); + rc = 0; + } + if (code == SSL_CONF_TYPE_FILE()) { + // Check file + File file = new File(value); + if (!file.isFile() && !file.canRead()) { + log.error(sm.getString("opensslconf.badFile", name, value)); + rc = 0; + } + } + if (code == SSL_CONF_TYPE_DIR()) { + // Check dir + File file = new File(value); + if (!file.isDirectory()) { + log.error(sm.getString("opensslconf.badDirectory", name, value)); + rc = 0; + } + } + } + } catch (Exception e) { + log.error(sm.getString("opensslconf.checkFailed", e.getLocalizedMessage())); + return false; + } + if (rc <= 0) { + log.error(sm.getString("opensslconf.failedCommand", name, value, + Integer.toString(rc))); + result = false; + } else if (log.isTraceEnabled()) { + log.trace(sm.getString("opensslconf.resultCommand", name, value, + Integer.toString(rc))); + } + } + if (!result) { + log.error(sm.getString("opensslconf.checkFailed")); + } + return result; + } + + + private boolean applyConf(OpenSSLConf conf) throws Exception { + boolean result = true; + SSL_CONF_CTX_set_ssl_ctx(state.confCtx, state.sslCtx); + OpenSSLConfCmd cmd; + String name; + String value; + int rc; + for (OpenSSLConfCmd command : conf.getCommands()) { + cmd = command; + name = cmd.getName(); + value = cmd.getValue(); + if (name == null) { + log.error(sm.getString("opensslconf.noCommandName", value)); + result = false; + continue; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("opensslconf.applyCommand", name, value)); + } + try (var localArena = Arena.ofConfined()) { + if (name.equals("NO_OCSP_CHECK")) { + noOcspCheck = Boolean.parseBoolean(value); + rc = 1; + } else { + rc = SSL_CONF_cmd(state.confCtx, localArena.allocateFrom(name), + localArena.allocateFrom(value)); + String errorMessage = getLastError(); + if (rc <= 0 || errorMessage != null) { + log.error(sm.getString("opensslconf.commandError", name, value, errorMessage)); + rc = 0; + } + } + } catch (Exception e) { + log.error(sm.getString("opensslconf.applyFailed")); + return false; + } + if (rc <= 0) { + log.error(sm.getString("opensslconf.failedCommand", name, value, + Integer.toString(rc))); + result = false; + } else if (log.isTraceEnabled()) { + log.trace(sm.getString("opensslconf.resultCommand", name, value, + Integer.toString(rc))); + } + } + // rc = SSLConf.finish(confCtx); + rc = SSL_CONF_CTX_finish(state.confCtx); + if (rc <= 0) { + log.error(sm.getString("opensslconf.finishFailed", Integer.toString(rc))); + result = false; + } + if (!result) { + log.error(sm.getString("opensslconf.applyFailed")); + } + return result; + } + + /** + * Setup the SSL_CTX. + * + * @param kms Must contain a KeyManager of the type + * {@code OpenSSLKeyManager} + * @param tms Must contain a TrustManager of the type + * {@code X509TrustManager} + * @param sr Is not used for this implementation. + */ + @Override + public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) { + if (initialized) { + log.warn(sm.getString("openssl.doubleInit")); + return; + } + boolean success = true; + try (var localArena = Arena.ofConfined()) { + if (sslHostConfig.getInsecureRenegotiation()) { + SSL_CTX_set_options(state.sslCtx, SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION()); + } else { + SSL_CTX_clear_options(state.sslCtx, SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION()); + } + + // Use server's preference order for ciphers (rather than + // client's) + if (sslHostConfig.getHonorCipherOrder()) { + SSL_CTX_set_options(state.sslCtx, SSL_OP_CIPHER_SERVER_PREFERENCE()); + } else { + SSL_CTX_clear_options(state.sslCtx, SSL_OP_CIPHER_SERVER_PREFERENCE()); + } + + // Disable compression if requested + if (sslHostConfig.getDisableCompression()) { + SSL_CTX_set_options(state.sslCtx, SSL_OP_NO_COMPRESSION()); + } else { + SSL_CTX_clear_options(state.sslCtx, SSL_OP_NO_COMPRESSION()); + } + + // Disable TLS Session Tickets (RFC4507) to protect perfect forward secrecy + if (sslHostConfig.getDisableSessionTickets()) { + SSL_CTX_set_options(state.sslCtx, SSL_OP_NO_TICKET()); + } else { + SSL_CTX_clear_options(state.sslCtx, SSL_OP_NO_TICKET()); + } + + // List the ciphers that the client is permitted to negotiate + if (minTlsVersion <= TLS1_2_VERSION()) { + if (SSL_CTX_set_cipher_list(state.sslCtx, + localArena.allocateFrom(sslHostConfig.getCiphers())) <= 0) { + log.warn(sm.getString("engine.failedCipherList", sslHostConfig.getCiphers())); + } + } + if (maxTlsVersion >= TLS1_3_VERSION() && (sslHostConfig.getCiphers() != SSLHostConfig.DEFAULT_TLS_CIPHERS)) { + if (SSL_CTX_set_ciphersuites(state.sslCtx, + localArena.allocateFrom(sslHostConfig.getCiphers())) <= 0) { + log.warn(sm.getString("engine.failedCipherSuite", sslHostConfig.getCiphers())); + } + } + + // If there is no certificate file must be using a KeyStore so a KeyManager is required. + // If there is a certificate file a KeyManager is helpful but not strictly necessary. + certificate.setCertificateKeyManager( + OpenSSLUtil.chooseKeyManager(kms, certificate.getCertificateFile() == null)); + + success = addCertificate(certificate, localArena); + + // Client certificate verification + int value = 0; + switch (sslHostConfig.getCertificateVerification()) { + case NONE: + value = SSL_VERIFY_NONE(); + break; + case OPTIONAL: + value = SSL_VERIFY_PEER(); + break; + case OPTIONAL_NO_CA: + value = OPTIONAL_NO_CA; + break; + case REQUIRED: + value = SSL_VERIFY_FAIL_IF_NO_PEER_CERT(); + break; + } + + // Set int verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx) callback + SSL_CTX_set_verify(state.sslCtx, value, + SSL_CTX_set_verify$callback.allocate(new OpenSSLEngine.VerifyCallback(), contextArena)); + + // Trust and certificate verification + if (tms != null) { + // Client certificate verification based on custom trust managers + x509TrustManager = chooseTrustManager(tms); + SSL_CTX_set_cert_verify_callback(state.sslCtx, + SSL_CTX_set_cert_verify_callback$cb.allocate(new CertVerifyCallback(x509TrustManager), contextArena), state.sslCtx); + + // Pass along the DER encoded certificates of the accepted client + // certificate issuers, so that their subjects can be presented + // by the server during the handshake to allow the client choosing + // an acceptable certificate + for (X509Certificate caCert : x509TrustManager.getAcceptedIssuers()) { + var rawCACertificate = localArena.allocateFrom(ValueLayout.JAVA_BYTE, caCert.getEncoded()); + var rawCACertificatePointer = localArena.allocateFrom(ValueLayout.ADDRESS, rawCACertificate); + var x509CACert = d2i_X509(MemorySegment.NULL, rawCACertificatePointer, rawCACertificate.byteSize()); + if (MemorySegment.NULL.equals(x509CACert)) { + logLastError("openssl.errorLoadingCertificate"); + } else if (SSL_CTX_add_client_CA(state.sslCtx, x509CACert) <= 0) { + logLastError("openssl.errorAddingCertificate"); + } else if (log.isDebugEnabled()) { + log.debug(sm.getString("openssl.addedClientCaCert", caCert.toString())); + } + } + } else { + // Client certificate verification based on trusted CA files and dirs + MemorySegment caCertificateFileNative = sslHostConfig.getCaCertificateFile() != null + ? localArena.allocateFrom(SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificateFile())) : null; + MemorySegment caCertificatePathNative = sslHostConfig.getCaCertificatePath() != null + ? localArena.allocateFrom(SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificatePath())) : null; + if ((sslHostConfig.getCaCertificateFile() != null || sslHostConfig.getCaCertificatePath() != null) + && SSL_CTX_load_verify_locations(state.sslCtx, + caCertificateFileNative == null ? MemorySegment.NULL : caCertificateFileNative, + caCertificatePathNative == null ? MemorySegment.NULL : caCertificatePathNative) <= 0) { + logLastError("openssl.errorConfiguringLocations"); + } else { + var caCerts = SSL_CTX_get_client_CA_list(state.sslCtx); + if (MemorySegment.NULL.equals(caCerts)) { + caCerts = SSL_load_client_CA_file(caCertificateFileNative == null ? MemorySegment.NULL : caCertificateFileNative); + if (!MemorySegment.NULL.equals(caCerts)) { + SSL_CTX_set_client_CA_list(state.sslCtx, caCerts); + } + } else { + if (SSL_add_file_cert_subjects_to_stack(caCerts, + caCertificateFileNative == null ? MemorySegment.NULL : caCertificateFileNative) <= 0) { + caCerts = MemorySegment.NULL; + } + } + if (MemorySegment.NULL.equals(caCerts)) { + log.warn(sm.getString("openssl.noCACerts")); + } + } + } + + if (negotiableProtocols != null && negotiableProtocols.size() > 0) { + SSL_CTX_set_alpn_select_cb(state.sslCtx, + SSL_CTX_set_alpn_select_cb$cb.allocate(new ALPNSelectCallback(negotiableProtocols), contextArena), state.sslCtx); + } + + // Apply OpenSSLConfCmd if used + OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); + if (openSslConf != null && !MemorySegment.NULL.equals(state.confCtx)) { + // Check OpenSSLConfCmd if used + if (log.isTraceEnabled()) { + log.trace(sm.getString("openssl.checkConf")); + } + try { + if (!checkConf(openSslConf)) { + log.error(sm.getString("openssl.errCheckConf")); + } + } catch (Exception e) { + log.error(sm.getString("openssl.errCheckConf"), e); + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("openssl.applyConf")); + } + try { + if (!applyConf(openSslConf)) { + log.error(sm.getString("openssl.errApplyConf")); + } + } catch (Exception e) { + log.error(sm.getString("openssl.errApplyConf"), e); + } + // Reconfigure the enabled protocols + long opts = SSL_CTX_get_options(state.sslCtx); + List enabled = new ArrayList<>(); + // Seems like there is no way to explicitly disable SSLv2Hello + // in OpenSSL so it is always enabled + enabled.add(Constants.SSL_PROTO_SSLv2Hello); + if ((opts & SSL_OP_NO_TLSv1()) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1); + } + if ((opts & SSL_OP_NO_TLSv1_1()) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_1); + } + if ((opts & SSL_OP_NO_TLSv1_2()) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_2); + } + if ((opts & SSL_OP_NO_TLSv1_3()) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_3); + } + if ((opts & SSL_OP_NO_SSLv2()) == 0) { + enabled.add(Constants.SSL_PROTO_SSLv2); + } + if ((opts & SSL_OP_NO_SSLv3()) == 0) { + enabled.add(Constants.SSL_PROTO_SSLv3); + } + sslHostConfig.setEnabledProtocols( + enabled.toArray(new String[0])); + // Reconfigure the enabled ciphers + sslHostConfig.setEnabledCiphers(getCiphers(state.sslCtx)); + } + + sessionContext = new OpenSSLSessionContext(this); + // If client authentication is being used, OpenSSL requires that + // this is set so always set it in case an app is configured to + // require it + sessionContext.setSessionIdContext(DEFAULT_SESSION_ID_CONTEXT); + sslHostConfig.setOpenSslContext(Long.valueOf(state.sslCtx.address())); + initialized = true; + } catch (Exception e) { + log.warn(sm.getString("openssl.errorSSLCtxInit"), e); + success = false; + } + if (!success) { + destroy(); + } + } + + + public MemorySegment getSSLContext() { + return state.sslCtx; + } + + // DH *(*tmp_dh_callback)(SSL *ssl, int is_export, int keylength) + private static class TmpDHCallback implements SSL_CTX_set_tmp_dh_callback$dh.Function { + @Override + public MemorySegment apply(MemorySegment ssl, int isExport, int keylength) { + var pkey = SSL_get_privatekey(ssl); + int type = (MemorySegment.NULL.equals(pkey)) ? EVP_PKEY_NONE() : EVP_PKEY_base_id(pkey); + /* + * OpenSSL will call us with either keylen == 512 or keylen == 1024 + * (see the definition of SSL_EXPORT_PKEYLENGTH in ssl_locl.h). + * Adjust the DH parameter length according to the size of the + * RSA/DSA private key used for the current connection, and always + * use at least 1024-bit parameters. + * Note: This may cause interoperability issues with implementations + * which limit their DH support to 1024 bit - e.g. Java 7 and earlier. + * In this case, SSLCertificateFile can be used to specify fixed + * 1024-bit DH parameters (with the effect that OpenSSL skips this + * callback). + */ + int keylen = 0; + if (type == EVP_PKEY_RSA() || type == EVP_PKEY_DSA()) { + keylen = EVP_PKEY_bits(pkey); + } + for (int i = 0; i < OpenSSLLibrary.dhParameters.length; i++) { + if (keylen >= OpenSSLLibrary.dhParameters[i].min) { + return OpenSSLLibrary.dhParameters[i].dh; + } + } + return MemorySegment.NULL; + } + } + + // int SSL_callback_alpn_select_proto(SSL* ssl, const unsigned char **out, unsigned char *outlen, + // const unsigned char *in, unsigned int inlen, void *arg) + private static class ALPNSelectCallback implements SSL_CTX_set_alpn_select_cb$cb.Function { + private final List negotiableProtocols; + ALPNSelectCallback(List negotiableProtocols) { + this.negotiableProtocols = negotiableProtocols; + } + @Override + public int apply(MemorySegment ssl, MemorySegment out, + MemorySegment outlen, MemorySegment in, int inlen, MemorySegment arg) { + try (var localArena = Arena.ofConfined()) { + MemorySegment inSeg = in.reinterpret(inlen, localArena, null); + byte[] advertisedBytes = inSeg.toArray(ValueLayout.JAVA_BYTE); + for (byte[] negotiableProtocolBytes : negotiableProtocols) { + for (int i = 0; i <= advertisedBytes.length - negotiableProtocolBytes.length; i++) { + if (advertisedBytes[i] == negotiableProtocolBytes[0]) { + for (int j = 0; j < negotiableProtocolBytes.length; j++) { + if (advertisedBytes[i + j] == negotiableProtocolBytes[j]) { + if (j == negotiableProtocolBytes.length - 1) { + // Match + MemorySegment outSeg = out.reinterpret(ValueLayout.ADDRESS.byteSize(), localArena, null); + outSeg.set(ValueLayout.ADDRESS, 0, inSeg.asSlice(i)); + MemorySegment outlenSeg = outlen.reinterpret(ValueLayout.JAVA_BYTE.byteSize(), localArena, null); + outlenSeg.set(ValueLayout.JAVA_BYTE, 0, (byte) negotiableProtocolBytes.length); + return SSL_TLSEXT_ERR_OK(); + } + } else { + break; + } + } + } + } + } + } + return SSL_TLSEXT_ERR_NOACK(); + } + } + + + private static class CertVerifyCallback implements SSL_CTX_set_cert_verify_callback$cb.Function { + private final X509TrustManager x509TrustManager; + CertVerifyCallback(X509TrustManager x509TrustManager) { + this.x509TrustManager = x509TrustManager; + } + @Override + public int apply(MemorySegment /*X509_STORE_CTX*/ x509_ctx, MemorySegment param) { + if (log.isTraceEnabled()) { + log.trace("Certificate verification"); + } + if (MemorySegment.NULL.equals(param)) { + return 0; + } + MemorySegment ssl = X509_STORE_CTX_get_ex_data(x509_ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + MemorySegment /*STACK_OF(X509)*/ sk = X509_STORE_CTX_get0_untrusted(x509_ctx); + int len = OPENSSL_sk_num(sk); + byte[][] certificateChain = new byte[len][]; + try (var localArena = Arena.ofConfined()) { + for (int i = 0; i < len; i++) { + MemorySegment/*(X509*)*/ x509 = OPENSSL_sk_value(sk, i); + MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + int length = i2d_X509(x509, bufPointer); + if (length < 0) { + certificateChain[i] = new byte[0]; + continue; + } + MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); + certificateChain[i] = buf.reinterpret(length, localArena, null).toArray(ValueLayout.JAVA_BYTE); + OPENSSL_free(buf); + } + MemorySegment cipher = SSL_get_current_cipher(ssl); + String authMethod = (MemorySegment.NULL.equals(cipher)) ? "UNKNOWN" + : getCipherAuthenticationMethod(SSL_CIPHER_get_auth_nid(cipher), SSL_CIPHER_get_kx_nid(cipher)); + X509Certificate[] peerCerts = certificates(certificateChain); + try { + x509TrustManager.checkClientTrusted(peerCerts, authMethod); + return 1; + } catch (Exception e) { + log.debug(sm.getString("openssl.certificateVerificationFailed"), e); + } + } + return 0; + } + } + + private static final int NID_kx_rsa = 1037/*NID_kx_rsa()*/; + //private static final int NID_kx_dhe = NID_kx_dhe(); + //private static final int NID_kx_ecdhe = NID_kx_ecdhe(); + + //private static final int NID_auth_rsa = NID_auth_rsa(); + //private static final int NID_auth_dss = NID_auth_dss(); + //private static final int NID_auth_null = NID_auth_null(); + //private static final int NID_auth_ecdsa = NID_auth_ecdsa(); + + //private static final int SSL_kRSA = 1; + private static final int SSL_kDHr = 2; + private static final int SSL_kDHd = 4; + private static final int SSL_kEDH = 8; + private static final int SSL_kDHE = SSL_kEDH; + private static final int SSL_kKRB5 = 10; + private static final int SSL_kECDHr = 20; + private static final int SSL_kECDHe = 40; + private static final int SSL_kEECDH = 80; + private static final int SSL_kECDHE = SSL_kEECDH; + //private static final int SSL_kPSK = 100; + //private static final int SSL_kGOST = 200; + //private static final int SSL_kSRP = 400; + + private static final int SSL_aRSA = 1; + private static final int SSL_aDSS = 2; + private static final int SSL_aNULL = 4; + //private static final int SSL_aDH = 8; + //private static final int SSL_aECDH = 10; + //private static final int SSL_aKRB5 = 20; + private static final int SSL_aECDSA = 40; + //private static final int SSL_aPSK = 80; + //private static final int SSL_aGOST94 = 100; + //private static final int SSL_aGOST01 = 200; + //private static final int SSL_aSRP = 400; + + private static final String SSL_TXT_RSA = "RSA"; + private static final String SSL_TXT_DH = "DH"; + private static final String SSL_TXT_DSS = "DSS"; + private static final String SSL_TXT_KRB5 = "KRB5"; + private static final String SSL_TXT_ECDH = "ECDH"; + private static final String SSL_TXT_ECDSA = "ECDSA"; + + private static String getCipherAuthenticationMethod(int auth, int kx) { + switch (kx) { + case NID_kx_rsa: + return SSL_TXT_RSA; + case SSL_kDHr: + return SSL_TXT_DH + "_" + SSL_TXT_RSA; + case SSL_kDHd: + return SSL_TXT_DH + "_" + SSL_TXT_DSS; + case SSL_kDHE: + switch (auth) { + case SSL_aDSS: + return "DHE_" + SSL_TXT_DSS; + case SSL_aRSA: + return "DHE_" + SSL_TXT_RSA; + case SSL_aNULL: + return SSL_TXT_DH + "_anon"; + default: + return "UNKNOWN"; + } + case SSL_kKRB5: + return SSL_TXT_KRB5; + case SSL_kECDHr: + return SSL_TXT_ECDH + "_" + SSL_TXT_RSA; + case SSL_kECDHe: + return SSL_TXT_ECDH + "_" + SSL_TXT_ECDSA; + case SSL_kECDHE: + switch (auth) { + case SSL_aECDSA: + return "ECDHE_" + SSL_TXT_ECDSA; + case SSL_aRSA: + return "ECDHE_" + SSL_TXT_RSA; + case SSL_aNULL: + return SSL_TXT_ECDH + "_anon"; + default: + return "UNKNOWN"; + } + default: + return "UNKNOWN"; + } + } + + private static class PasswordCallback implements pem_password_cb.Function { + private final String callbackPassword; + PasswordCallback(String callbackPassword) { + this.callbackPassword = callbackPassword; + } + @Override + public int apply(MemorySegment /* char **/ buf, int bufsiz, int verify, MemorySegment /* void **/ cb) { + if (log.isTraceEnabled()) { + log.trace("Return password for certificate"); + } + if (callbackPassword != null && callbackPassword.length() > 0) { + try (var localArena = Arena.ofConfined()) { + MemorySegment callbackPasswordNative = localArena.allocateFrom(callbackPassword); + if (callbackPasswordNative.byteSize() > bufsiz) { + // The password is too long + log.error(sm.getString("openssl.passwordTooLong")); + } else { + MemorySegment bufSegment = buf.reinterpret(bufsiz, localArena, null); + bufSegment.copyFrom(callbackPasswordNative); + return (int) callbackPasswordNative.byteSize(); + } + } + } + return 0; + } + } + + + private boolean addCertificate(SSLHostConfigCertificate certificate, Arena localArena) throws Exception { + int index = getCertificateIndex(certificate); + // Load Server key and certificate + if (certificate.getCertificateFile() != null) { + // Pick right key password + String keyPassToUse = null; + String keyPass = certificate.getCertificateKeyPassword(); + if (keyPass == null) { + keyPass = certificate.getCertificateKeystorePassword(); + } + String keyPassFile = certificate.getCertificateKeyPasswordFile(); + if (keyPassFile == null) { + keyPassFile = certificate.getCertificateKeystorePasswordFile(); + } + if (keyPassFile != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader( + ConfigFileLoader.getSource().getResource(keyPassFile).getInputStream(), + StandardCharsets.UTF_8))) { + keyPassToUse = reader.readLine(); + } catch (IOException e) { + log.error(sm.getString("openssl.errorLoadingPassword", keyPassFile), e); + return false; + } + } else { + keyPassToUse = keyPass; + } + // Set certificate + byte[] certificateFileBytes = null; + try (Resource resource = ConfigFileLoader.getSource().getResource(certificate.getCertificateFile())) { + certificateFileBytes = resource.getInputStream().readAllBytes(); + } catch (IOException e) { + log.error(sm.getString("openssl.errorLoadingCertificate", certificate.getCertificateFile()), e); + return false; + } + MemorySegment certificateFileBytesNative = localArena.allocateFrom(ValueLayout.JAVA_BYTE, certificateFileBytes); + MemorySegment certificateBIO = BIO_new(BIO_s_mem()); + try { + if (BIO_write(certificateBIO, certificateFileBytesNative, certificateFileBytes.length) <= 0) { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificate.getCertificateFile(), getLastError())); + return false; + } + MemorySegment cert = MemorySegment.NULL; + MemorySegment key = MemorySegment.NULL; + if (certificate.getCertificateFile().endsWith(".pkcs12")) { + // Load pkcs12 + MemorySegment p12 = d2i_PKCS12_bio(certificateBIO, MemorySegment.NULL); + if (MemorySegment.NULL.equals(p12)) { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificate.getCertificateFile(), getLastError())); + return false; + } + MemorySegment passwordAddress = MemorySegment.NULL; + int passwordLength = 0; + if (keyPassToUse != null && keyPassToUse.length() > 0) { + passwordAddress = localArena.allocateFrom(keyPassToUse); + passwordLength = (int) (passwordAddress.byteSize() - 1); + } + if (PKCS12_verify_mac(p12, passwordAddress, passwordLength) <= 0) { + // Bad password + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificate.getCertificateFile(), getLastError())); + PKCS12_free(p12); + return false; + } + MemorySegment certPointer = localArena.allocate(ValueLayout.ADDRESS); + MemorySegment keyPointer = localArena.allocate(ValueLayout.ADDRESS); + if (PKCS12_parse(p12, passwordAddress, keyPointer, certPointer, MemorySegment.NULL) <= 0) { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificate.getCertificateFile(), getLastError())); + PKCS12_free(p12); + return false; + } + PKCS12_free(p12); + cert = certPointer.get(ValueLayout.ADDRESS, 0); + key = keyPointer.get(ValueLayout.ADDRESS, 0); + } else { + String certificateKeyFileName = (certificate.getCertificateKeyFile() == null) + ? certificate.getCertificateFile() : certificate.getCertificateKeyFile(); + // Load key + byte[] certificateKeyFileBytes = null; + try (Resource resource = ConfigFileLoader.getSource().getResource(certificateKeyFileName)) { + certificateKeyFileBytes = resource.getInputStream().readAllBytes(); + } catch (IOException e) { + log.error(sm.getString("openssl.errorLoadingCertificate", certificateKeyFileName), e); + return false; + } + MemorySegment certificateKeyFileBytesNative = localArena.allocateFrom(ValueLayout.JAVA_BYTE, certificateKeyFileBytes); + MemorySegment keyBIO = BIO_new(BIO_s_mem()); + try { + if (BIO_write(keyBIO, certificateKeyFileBytesNative, certificateKeyFileBytes.length) <= 0) { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificateKeyFileName, getLastError())); + return false; + } + key = MemorySegment.NULL; + for (int i = 0; i < 3; i++) { + key = PEM_read_bio_PrivateKey(keyBIO, MemorySegment.NULL, + pem_password_cb.allocate(new PasswordCallback(keyPassToUse), contextArena), + MemorySegment.NULL); + if (!MemorySegment.NULL.equals(key)) { + break; + } + BIO_reset(keyBIO); + } + } finally { + BIO_free(keyBIO); + } + if (MemorySegment.NULL.equals(key)) { + if (!MemorySegment.NULL.equals(OpenSSLLibrary.enginePointer)) { + // This needs a real file + key = ENGINE_load_private_key(OpenSSLLibrary.enginePointer, + localArena.allocateFrom(SSLHostConfig.adjustRelativePath(certificateKeyFileName)), + MemorySegment.NULL, MemorySegment.NULL); + } + } + if (MemorySegment.NULL.equals(key)) { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificateKeyFileName, getLastError())); + return false; + } + // Load certificate + cert = PEM_read_bio_X509_AUX(certificateBIO, MemorySegment.NULL, + pem_password_cb.allocate(new PasswordCallback(keyPassToUse), contextArena), + MemorySegment.NULL); + if (MemorySegment.NULL.equals(cert) && + // EOF is accepted, then try again + ((ERR_peek_last_error() & ERR_REASON_MASK()) == PEM_R_NO_START_LINE())) { + ERR_clear_error(); + BIO_reset(certificateBIO); + cert = d2i_X509_bio(certificateBIO, MemorySegment.NULL); + } + if (MemorySegment.NULL.equals(cert)) { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificate.getCertificateFile(), getLastError())); + return false; + } + } + if (SSL_CTX_use_certificate(state.sslCtx, cert) <= 0) { + logLastError("openssl.errorLoadingCertificate"); + return false; + } + if (SSL_CTX_use_PrivateKey(state.sslCtx, key) <= 0) { + logLastError("openssl.errorLoadingPrivateKey"); + return false; + } + if (SSL_CTX_check_private_key(state.sslCtx) <= 0) { + logLastError("openssl.errorPrivateKeyCheck"); + return false; + } + // Try to read DH parameters from the (first) SSLCertificateFile + if (index == SSL_AIDX_RSA) { + BIO_reset(certificateBIO); + if (!OPENSSL_3) { + var dh = PEM_read_bio_DHparams(certificateBIO, MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); + if (!MemorySegment.NULL.equals(dh)) { + SSL_CTX_set_tmp_dh(state.sslCtx, dh); + DH_free(dh); + } + } else { + var pkey = PEM_read_bio_Parameters(certificateBIO, MemorySegment.NULL); + if (!MemorySegment.NULL.equals(pkey)) { + int numBits = EVP_PKEY_get_bits(pkey); + if (SSL_CTX_set0_tmp_dh_pkey(state.sslCtx, pkey) <= 0) { + EVP_PKEY_free(pkey); + } else { + log.debug(sm.getString("openssl.setCustomDHParameters", Integer.valueOf(numBits), certificate.getCertificateFile())); + } + } else { + SSL_CTX_ctrl(state.sslCtx, SSL_CTRL_SET_DH_AUTO(), 1, MemorySegment.NULL); + } + } + } + // Similarly, try to read the ECDH curve name from SSLCertificateFile... + BIO_reset(certificateBIO); + if (!OPENSSL_3) { + var ecparams = PEM_read_bio_ECPKParameters(certificateBIO, MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); + if (!MemorySegment.NULL.equals(ecparams)) { + int nid = EC_GROUP_get_curve_name(ecparams); + var eckey = EC_KEY_new_by_curve_name(nid); + SSL_CTX_set_tmp_ecdh(state.sslCtx, eckey); + EC_KEY_free(eckey); + EC_GROUP_free(ecparams); + } + // Set callback for DH parameters + SSL_CTX_set_tmp_dh_callback(state.sslCtx, SSL_CTX_set_tmp_dh_callback$dh.allocate(new TmpDHCallback(), contextArena)); + } else { + var ecparams = PEM_ASN1_read_bio(d2i_ECPKParameters$SYMBOL(), + PEM_STRING_ECPARAMETERS(), certificateBIO, MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); + if (!MemorySegment.NULL.equals(ecparams)) { + int curveNid = EC_GROUP_get_curve_name(ecparams); + var curveNidAddress = localArena.allocateFrom(ValueLayout.JAVA_INT, curveNid); + if (SSL_CTX_set1_groups(state.sslCtx, curveNidAddress, 1) <= 0) { + curveNid = 0; + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("openssl.setECDHCurve", Integer.valueOf(curveNid), + certificate.getCertificateFile())); + } + EC_GROUP_free(ecparams); + } + } + // Set certificate chain file + if (certificate.getCertificateChainFile() != null) { + byte[] certificateChainBytes = null; + try (Resource resource = ConfigFileLoader.getSource().getResource(certificate.getCertificateChainFile())) { + certificateChainBytes = resource.getInputStream().readAllBytes(); + } catch (IOException e) { + log.error(sm.getString("openssl.errorLoadingCertificate", certificate.getCertificateChainFile()), e); + return false; + } + MemorySegment certificateChainBytesNative = localArena.allocateFrom(ValueLayout.JAVA_BYTE, certificateChainBytes); + MemorySegment certificateChainBIO = BIO_new(BIO_s_mem()); + try { + if (BIO_write(certificateChainBIO, certificateChainBytesNative, certificateChainBytes.length) <= 0) { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificate.getCertificateChainFile(), getLastError())); + return false; + } + MemorySegment certChainEntry = + PEM_read_bio_X509_AUX(certificateChainBIO, MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); + while (!MemorySegment.NULL.equals(certChainEntry)) { + if (SSL_CTX_add0_chain_cert(state.sslCtx, certChainEntry) <= 0) { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificate.getCertificateChainFile(), getLastError())); + } + certChainEntry = + PEM_read_bio_X509_AUX(certificateChainBIO, MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); + } + // EOF is accepted, otherwise log an error + if ((ERR_peek_last_error() & ERR_REASON_MASK()) == PEM_R_NO_START_LINE()) { + ERR_clear_error(); + } else { + log.error(sm.getString("openssl.errorLoadingCertificateWithError", + certificate.getCertificateChainFile(), getLastError())); + } + } finally { + BIO_free(certificateChainBIO); + } + } + // Set revocation + MemorySegment certificateStore = SSL_CTX_get_cert_store(state.sslCtx); + if (sslHostConfig.getCertificateRevocationListFile() != null) { + MemorySegment x509Lookup = X509_STORE_add_lookup(certificateStore, X509_LOOKUP_file()); + var certificateRevocationListFileNative = + localArena.allocateFrom(SSLHostConfig.adjustRelativePath(sslHostConfig.getCertificateRevocationListFile())); + if (X509_LOOKUP_load_file(x509Lookup, certificateRevocationListFileNative, + X509_FILETYPE_PEM()) <= 0) { + log.error(sm.getString("openssl.errorLoadingCertificateRevocationListWithError", + sslHostConfig.getCertificateRevocationListFile(), getLastError())); + } + } + if (sslHostConfig.getCertificateRevocationListPath() != null) { + MemorySegment x509Lookup = X509_STORE_add_lookup(certificateStore, X509_LOOKUP_hash_dir()); + var certificateRevocationListPathNative = + localArena.allocateFrom(SSLHostConfig.adjustRelativePath(sslHostConfig.getCertificateRevocationListPath())); + if (X509_LOOKUP_add_dir(x509Lookup, certificateRevocationListPathNative, + X509_FILETYPE_PEM()) <= 0) { + log.error(sm.getString("openssl.errorLoadingCertificateRevocationListWithError", + sslHostConfig.getCertificateRevocationListPath(), getLastError())); + } + } + X509_STORE_set_flags(certificateStore, X509_V_FLAG_CRL_CHECK() | X509_V_FLAG_CRL_CHECK_ALL()); + } finally { + BIO_free(certificateBIO); + } + } else { + String alias = certificate.getCertificateKeyAlias(); + X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); + if (alias == null) { + alias = "tomcat"; + } + X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); + if (chain == null) { + alias = findAlias(x509KeyManager, certificate); + chain = x509KeyManager.getCertificateChain(alias); + } + StringBuilder sb = new StringBuilder(BEGIN_KEY); + sb.append(Base64.getMimeEncoder(64, new byte[] {'\n'}).encodeToString(x509KeyManager.getPrivateKey(alias).getEncoded())); + sb.append(END_KEY); + var rawCertificate = localArena.allocateFrom(ValueLayout.JAVA_BYTE, chain[0].getEncoded()); + var rawCertificatePointer = localArena.allocateFrom(ValueLayout.ADDRESS, rawCertificate); + var rawKey = localArena.allocateFrom(ValueLayout.JAVA_BYTE, sb.toString().getBytes(StandardCharsets.US_ASCII)); + var x509cert = d2i_X509(MemorySegment.NULL, rawCertificatePointer, rawCertificate.byteSize()); + if (MemorySegment.NULL.equals(x509cert)) { + logLastError("openssl.errorLoadingCertificate"); + return false; + } + MemorySegment keyBIO = BIO_new(BIO_s_mem()); + try { + BIO_write(keyBIO, rawKey, (int) rawKey.byteSize()); + MemorySegment privateKeyAddress = PEM_read_bio_PrivateKey(keyBIO, MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); + if (MemorySegment.NULL.equals(privateKeyAddress)) { + logLastError("openssl.errorLoadingPrivateKey"); + return false; + } + if (SSL_CTX_use_certificate(state.sslCtx, x509cert) <= 0) { + logLastError("openssl.errorLoadingCertificate"); + return false; + } + if (SSL_CTX_use_PrivateKey(state.sslCtx, privateKeyAddress) <= 0) { + logLastError("openssl.errorLoadingPrivateKey"); + return false; + } + if (SSL_CTX_check_private_key(state.sslCtx) <= 0) { + logLastError("openssl.errorPrivateKeyCheck"); + return false; + } + if (!OPENSSL_3) { + // Set callback for DH parameters + SSL_CTX_set_tmp_dh_callback(state.sslCtx, + SSL_CTX_set_tmp_dh_callback$dh.allocate(new TmpDHCallback(), contextArena)); + } else { + BIO_reset(keyBIO); + var pkey = PEM_read_bio_Parameters(keyBIO, MemorySegment.NULL); + if (!MemorySegment.NULL.equals(pkey)) { + int numBits = EVP_PKEY_get_bits(pkey); + if (SSL_CTX_set0_tmp_dh_pkey(state.sslCtx, pkey) <= 0) { + EVP_PKEY_free(pkey); + } else { + log.debug(sm.getString("openssl.setCustomDHParameters", Integer.valueOf(numBits), + certificate.getCertificateFile())); + } + } else { + SSL_CTX_ctrl(state.sslCtx, SSL_CTRL_SET_DH_AUTO(), 1, MemorySegment.NULL); + } + } + for (int i = 1; i < chain.length; i++) { + var rawCertificateChain = localArena.allocateFrom(ValueLayout.JAVA_BYTE, chain[i].getEncoded()); + var rawCertificateChainPointer = localArena.allocateFrom(ValueLayout.ADDRESS, rawCertificateChain); + var x509certChain = d2i_X509(MemorySegment.NULL, rawCertificateChainPointer, rawCertificateChain.byteSize()); + if (MemorySegment.NULL.equals(x509certChain)) { + logLastError("openssl.errorLoadingCertificate"); + return false; + } + if (SSL_CTX_add0_chain_cert(state.sslCtx, x509certChain) <= 0) { + logLastError("openssl.errorAddingCertificate"); + return false; + } + } + } finally { + BIO_free(keyBIO); + } + } + return true; + } + + + private static int getCertificateIndex(SSLHostConfigCertificate certificate) { + int result = -1; + // If the type is undefined there will only be one certificate (enforced + // in SSLHostConfig) so use the RSA slot. + if (certificate.getType() == Type.RSA || certificate.getType() == Type.UNDEFINED) { + result = SSL_AIDX_RSA; + } else if (certificate.getType() == Type.EC) { + result = SSL_AIDX_ECC; + } else if (certificate.getType() == Type.DSA) { + result = SSL_AIDX_DSA; + } else { + result = SSL_AIDX_MAX; + } + return result; + } + + + /* + * Find a valid alias when none was specified in the config. + */ + private static String findAlias(X509KeyManager keyManager, + SSLHostConfigCertificate certificate) { + + Type type = certificate.getType(); + String result = null; + + List candidateTypes = new ArrayList<>(); + if (Type.UNDEFINED.equals(type)) { + // Try all types to find an suitable alias + candidateTypes.addAll(Arrays.asList(Type.values())); + candidateTypes.remove(Type.UNDEFINED); + } else { + // Look for the specific type to find a suitable alias + candidateTypes.add(type); + } + + Iterator iter = candidateTypes.iterator(); + while (result == null && iter.hasNext()) { + result = keyManager.chooseServerAlias(iter.next().toString(), null, null); + } + + return result; + } + + private static X509TrustManager chooseTrustManager(TrustManager[] managers) { + for (TrustManager m : managers) { + if (m instanceof X509TrustManager) { + return (X509TrustManager) m; + } + } + throw new IllegalStateException(sm.getString("openssl.trustManagerMissing")); + } + + private static X509Certificate[] certificates(byte[][] chain) { + X509Certificate[] peerCerts = new X509Certificate[chain.length]; + for (int i = 0; i < peerCerts.length; i++) { + peerCerts[i] = new OpenSSLX509Certificate(chain[i]); + } + return peerCerts; + } + + + private static void logLastError(String string) { + String message = getLastError(); + if (message != null) { + log.error(sm.getString(string, message)); + } + } + + + /** + * Many calls to SSL methods do not check the last error. Those that do + * check the last error need to ensure that any previously ignored error is + * cleared prior to the method call else errors may be falsely reported. + * Ideally, before any SSL_read, SSL_write, clearLastError should always + * be called, and getLastError should be called after on any negative or + * zero result. + * @return the first error in the stack + */ + static String getLastError() { + String sslError = null; + long error = ERR_get_error(); + if (error != SSL_ERROR_NONE()) { + try (var localArena = Arena.ofConfined()) { + do { + // Loop until getLastErrorNumber() returns SSL_ERROR_NONE + var buf = localArena.allocate(ValueLayout.JAVA_BYTE, 128); + ERR_error_string(error, buf); + String err = buf.getString(0); + if (sslError == null) { + sslError = err; + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("engine.openSSLError", Long.toString(error), err)); + } + } while ((error = ERR_get_error()) != SSL_ERROR_NONE()); + } + } + return sslError; + } + + + @Override + public SSLSessionContext getServerSessionContext() { + return sessionContext; + } + + @Override + public SSLEngine createSSLEngine() { + return new OpenSSLEngine(cleaner, state.sslCtx, defaultProtocol, false, sessionContext, + alpn, initialized, + sslHostConfig.getCertificateVerificationDepth(), + sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL_NO_CA, + noOcspCheck); + } + + @Override + public SSLServerSocketFactory getServerSocketFactory() { + throw new UnsupportedOperationException(); + } + + @Override + public SSLParameters getSupportedSSLParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + X509Certificate[] chain = null; + X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); + if (x509KeyManager != null) { + if (alias == null) { + alias = "tomcat"; + } + chain = x509KeyManager.getCertificateChain(alias); + if (chain == null) { + alias = findAlias(x509KeyManager, certificate); + chain = x509KeyManager.getCertificateChain(alias); + } + } + + return chain; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + X509Certificate[] acceptedCerts = null; + if (x509TrustManager != null) { + acceptedCerts = x509TrustManager.getAcceptedIssuers(); + } + return acceptedCerts; + } + + + private static class ContextState implements Runnable { + + private final Arena stateArena = Arena.ofShared(); + private final MemorySegment sslCtx; + private final MemorySegment confCtx; + + private ContextState(MemorySegment sslCtx, MemorySegment confCtx) { + // Use another arena to avoid keeping a reference through segments + // This also allows making further accesses to the main pointers safer + this.sslCtx = sslCtx.reinterpret(ValueLayout.ADDRESS.byteSize(), stateArena, + (MemorySegment t) -> SSL_CTX_free(t)); + if (!MemorySegment.NULL.equals(confCtx)) { + this.confCtx = confCtx.reinterpret(ValueLayout.ADDRESS.byteSize(), stateArena, + (MemorySegment t) -> SSL_CONF_CTX_free(t)); + } else { + this.confCtx = null; + } + } + + @Override + public void run() { + stateArena.close(); + } + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java new file mode 100644 index 0000000..d80b48d --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java @@ -0,0 +1,1697 @@ +/* + * 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.tomcat.util.net.openssl.panama; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.ref.Cleaner; +import java.lang.ref.Cleaner.Cleanable; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionBindingEvent; +import javax.net.ssl.SSLSessionBindingListener; +import javax.net.ssl.SSLSessionContext; + +import static org.apache.tomcat.util.openssl.openssl_h.*; +import static org.apache.tomcat.util.openssl.openssl_h_Compatibility.*; +import static org.apache.tomcat.util.openssl.openssl_h_Macros.*; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.Asn1Parser; +import org.apache.tomcat.util.net.Constants; +import org.apache.tomcat.util.net.SSLUtil; +import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser; +import org.apache.tomcat.util.openssl.SSL_CTX_set_verify$callback; +import org.apache.tomcat.util.openssl.SSL_set_info_callback$cb; +import org.apache.tomcat.util.openssl.SSL_set_verify$callback; +import org.apache.tomcat.util.res.StringManager; + +/** + * Implements a {@link SSLEngine} using + * OpenSSL + * BIO abstractions. + */ +public final class OpenSSLEngine extends SSLEngine implements SSLUtil.ProtocolInfo { + + private static final Log log = LogFactory.getLog(OpenSSLEngine.class); + private static final StringManager sm = StringManager.getManager(OpenSSLEngine.class); + + private static final Certificate[] EMPTY_CERTIFICATES = new Certificate[0]; + + public static final Set AVAILABLE_CIPHER_SUITES; + + public static final Set IMPLEMENTED_PROTOCOLS_SET; + + static { + final Set availableCipherSuites = new LinkedHashSet<>(128); + availableCipherSuites.addAll(OpenSSLLibrary.findCiphers("ALL")); + AVAILABLE_CIPHER_SUITES = Collections.unmodifiableSet(availableCipherSuites); + HashSet protocols = new HashSet<>(); + protocols.add(Constants.SSL_PROTO_SSLv2Hello); + protocols.add(Constants.SSL_PROTO_SSLv2); + protocols.add(Constants.SSL_PROTO_SSLv3); + protocols.add(Constants.SSL_PROTO_TLSv1); + protocols.add(Constants.SSL_PROTO_TLSv1_1); + protocols.add(Constants.SSL_PROTO_TLSv1_2); + protocols.add(Constants.SSL_PROTO_TLSv1_3); + IMPLEMENTED_PROTOCOLS_SET = Collections.unmodifiableSet(protocols); + } + + private static final int MAX_PLAINTEXT_LENGTH = 16 * 1024; // 2^14 + private static final int MAX_COMPRESSED_LENGTH = MAX_PLAINTEXT_LENGTH + 1024; + private static final int MAX_CIPHERTEXT_LENGTH = MAX_COMPRESSED_LENGTH + 1024; + + // Header (5) + Data (2^14) + Compression (1024) + Encryption (1024) + MAC (20) + Padding (256) + private static final int MAX_ENCRYPTED_PACKET_LENGTH = MAX_CIPHERTEXT_LENGTH + 5 + 20 + 256; + + private enum ClientAuthMode { + NONE, + OPTIONAL, + REQUIRE, + } + + private static final String INVALID_CIPHER = "SSL_NULL_WITH_NULL_NULL"; + + private static final ConcurrentHashMap states = new ConcurrentHashMap<>(); + private static EngineState getState(MemorySegment ssl) { + return states.get(Long.valueOf(ssl.address())); + } + + private final EngineState state; + private final Arena engineArena; + private final Cleanable cleanable; + private MemorySegment bufSegment = null; + + private enum Accepted { NOT, IMPLICIT, EXPLICIT } + private Accepted accepted = Accepted.NOT; + private enum PHAState { NONE, START, COMPLETE } + private boolean handshakeFinished; + private int currentHandshake; + private boolean receivedShutdown; + private volatile boolean destroyed; + + // Use an invalid cipherSuite until the handshake is completed + // See http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLEngine.html#getSession() + private volatile String version; + private volatile String cipher; + private volatile String applicationProtocol; + + private volatile Certificate[] peerCerts; + private volatile ClientAuthMode clientAuth = ClientAuthMode.NONE; + + // SSL Engine status variables + private boolean isInboundDone; + private boolean isOutboundDone; + private boolean engineClosed; + private boolean sendHandshakeError = false; + + private final boolean clientMode; + private final String fallbackApplicationProtocol; + private final OpenSSLSessionContext sessionContext; + private final boolean alpn; + private final boolean initialized; + private final boolean certificateVerificationOptionalNoCA; + + private String selectedProtocol = null; + + private final OpenSSLSession session; + + /** + * Creates a new instance + * + * @param sslCtx an OpenSSL {@code SSL_CTX} object + * @param fallbackApplicationProtocol the fallback application protocol + * @param clientMode {@code true} if this is used for clients, {@code false} + * otherwise + * @param sessionContext the {@link OpenSSLSessionContext} this + * {@link SSLEngine} belongs to. + * @param alpn {@code true} if alpn should be used, {@code false} + * otherwise + * @param initialized {@code true} if this instance gets its protocol, + * cipher and client verification from the {@code SSL_CTX} {@code sslCtx} + * @param certificateVerificationDepth Certificate verification depth + * @param certificateVerificationOptionalNoCA Skip CA verification in + * optional mode + */ + OpenSSLEngine(Cleaner cleaner, MemorySegment sslCtx, String fallbackApplicationProtocol, + boolean clientMode, OpenSSLSessionContext sessionContext, boolean alpn, + boolean initialized, int certificateVerificationDepth, + boolean certificateVerificationOptionalNoCA, boolean noOcspCheck) { + if (sslCtx == null) { + throw new IllegalArgumentException(sm.getString("engine.noSSLContext")); + } + engineArena = Arena.ofAuto(); + bufSegment = engineArena.allocate(MAX_ENCRYPTED_PACKET_LENGTH); + session = new OpenSSLSession(); + var ssl = SSL_new(sslCtx); + // Set ssl_info_callback + SSL_set_info_callback(ssl, SSL_set_info_callback$cb.allocate(new InfoCallback(), engineArena)); + if (clientMode) { + SSL_set_connect_state(ssl); + } else { + SSL_set_accept_state(ssl); + } + SSL_set_verify_result(ssl, X509_V_OK()); + try (var localArena = Arena.ofConfined()) { + var internalBIOPointer = localArena.allocate(ValueLayout.ADDRESS); + var networkBIOPointer = localArena.allocate(ValueLayout.ADDRESS); + BIO_new_bio_pair(internalBIOPointer, 0, networkBIOPointer, 0); + var internalBIO = internalBIOPointer.get(ValueLayout.ADDRESS, 0); + var networkBIO = networkBIOPointer.get(ValueLayout.ADDRESS, 0); + SSL_set_bio(ssl, internalBIO, internalBIO); + state = new EngineState(ssl, networkBIO, certificateVerificationDepth, noOcspCheck); + } + this.fallbackApplicationProtocol = fallbackApplicationProtocol; + this.clientMode = clientMode; + this.sessionContext = sessionContext; + this.alpn = alpn; + this.initialized = initialized; + this.certificateVerificationOptionalNoCA = certificateVerificationOptionalNoCA; + cleanable = cleaner.register(this, state); + } + + @Override + public String getNegotiatedProtocol() { + return selectedProtocol; + } + + /** + * Destroys this engine. + */ + public synchronized void shutdown() { + if (!destroyed) { + destroyed = true; + cleanable.clean(); + // internal errors can cause shutdown without marking the engine closed + isInboundDone = isOutboundDone = engineClosed = true; + bufSegment = null; + } + } + + /** + * Write plain text data to the OpenSSL internal BIO + * + * Calling this function with src.remaining == 0 is undefined. + * @throws SSLException if the OpenSSL error check fails + */ + private int writePlaintextData(final MemorySegment ssl, final ByteBuffer src) throws SSLException { + clearLastError(); + final int pos = src.position(); + final int len = Math.min(src.remaining(), MAX_PLAINTEXT_LENGTH); + MemorySegment srcSegment = src.isDirect() ? MemorySegment.ofBuffer(src) : bufSegment; + if (!src.isDirect()) { + MemorySegment.copy(src.array(), pos, bufSegment, ValueLayout.JAVA_BYTE, 0, len); + } + final int sslWrote = SSL_write(ssl, srcSegment, len); + if (sslWrote > 0) { + src.position(pos + sslWrote); + return sslWrote; + } else { + checkLastError(); + } + return 0; + } + + /** + * Write encrypted data to the OpenSSL network BIO. + * @throws SSLException if the OpenSSL error check fails + */ + private int writeEncryptedData(final MemorySegment networkBIO, final ByteBuffer src) throws SSLException { + clearLastError(); + final int pos = src.position(); + final int len = src.remaining(); + MemorySegment srcSegment = src.isDirect() ? MemorySegment.ofBuffer(src) : bufSegment; + if (!src.isDirect()) { + MemorySegment.copy(src.array(), pos, bufSegment, ValueLayout.JAVA_BYTE, 0, len); + } + final int netWrote = BIO_write(networkBIO, srcSegment, len); + if (netWrote > 0) { + src.position(pos + netWrote); + return netWrote; + } else { + checkLastError(); + } + return 0; + } + + /** + * Read plain text data from the OpenSSL internal BIO + * @throws SSLException if the OpenSSL error check fails + */ + private int readPlaintextData(final MemorySegment ssl, final ByteBuffer dst) throws SSLException { + clearLastError(); + final int pos = dst.position(); + final int len = Math.min(dst.remaining(), MAX_ENCRYPTED_PACKET_LENGTH); + MemorySegment dstSegment = dst.isDirect() ? MemorySegment.ofBuffer(dst) : bufSegment; + final int sslRead = SSL_read(ssl, dstSegment, len); + if (sslRead > 0) { + if (!dst.isDirect()) { + MemorySegment.copy(dstSegment, ValueLayout.JAVA_BYTE, 0, dst.array(), pos, sslRead); + } + dst.position(pos + sslRead); + return sslRead; + } else { + checkLastError(); + } + return 0; + } + + /** + * Read encrypted data from the OpenSSL network BIO + * @throws SSLException if the OpenSSL error check fails + */ + private int readEncryptedData(final MemorySegment networkBIO, final ByteBuffer dst, final int pending) throws SSLException { + clearLastError(); + final int pos = dst.position(); + MemorySegment dstSegment = dst.isDirect() ? MemorySegment.ofBuffer(dst) : bufSegment; + final int bioRead = BIO_read(networkBIO, dstSegment, pending); + if (bioRead > 0) { + if (!dst.isDirect()) { + MemorySegment.copy(dstSegment, ValueLayout.JAVA_BYTE, 0, dst.array(), pos, bioRead); + } + dst.position(pos + bioRead); + return bioRead; + } else { + checkLastError(); + } + return 0; + } + + @Override + public synchronized SSLEngineResult wrap(final ByteBuffer[] srcs, final int offset, final int length, final ByteBuffer dst) throws SSLException { + // Check to make sure the engine has not been closed + if (destroyed) { + return new SSLEngineResult(SSLEngineResult.Status.CLOSED, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, 0, 0); + } + + // Throw required runtime exceptions + if (srcs == null || dst == null) { + throw new IllegalArgumentException(sm.getString("engine.nullBuffer")); + } + if (offset >= srcs.length || offset + length > srcs.length) { + throw new IndexOutOfBoundsException(sm.getString("engine.invalidBufferArray", + Integer.toString(offset), Integer.toString(length), + Integer.toString(srcs.length))); + } + if (dst.isReadOnly()) { + throw new ReadOnlyBufferException(); + } + // Prepare OpenSSL to work in server mode and receive handshake + if (accepted == Accepted.NOT) { + beginHandshakeImplicitly(); + } + + // In handshake or close_notify stages, check if call to wrap was made + // without regard to the handshake status. + SSLEngineResult.HandshakeStatus handshakeStatus = getHandshakeStatus(); + + if ((!handshakeFinished || engineClosed) && handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { + return new SSLEngineResult(getEngineStatus(), SSLEngineResult.HandshakeStatus.NEED_UNWRAP, 0, 0); + } + + int bytesProduced = 0; + int pendingNet; + + // Check for pending data in the network BIO + pendingNet = (int) BIO_ctrl_pending(state.networkBIO); + if (pendingNet > 0) { + // Do we have enough room in destination to write encrypted data? + int capacity = dst.remaining(); + if (capacity < pendingNet) { + return new SSLEngineResult(SSLEngineResult.Status.BUFFER_OVERFLOW, handshakeStatus, 0, 0); + } + + // Write the pending data from the network BIO into the dst buffer + try { + bytesProduced = readEncryptedData(state.networkBIO, dst, pendingNet); + } catch (Exception e) { + throw new SSLException(e); + } + + // If isOutboundDone is set, then the data from the network BIO + // was the close_notify message -- we are not required to wait + // for the receipt the peer's close_notify message -- shutdown. + if (isOutboundDone) { + shutdown(); + } + + return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), 0, bytesProduced); + } + + // There was no pending data in the network BIO -- encrypt any application data + int bytesConsumed = 0; + int endOffset = offset + length; + for (int i = offset; i < endOffset; ++i) { + final ByteBuffer src = srcs[i]; + if (src == null) { + throw new IllegalArgumentException(sm.getString("engine.nullBufferInArray")); + } + while (src.hasRemaining()) { + + int bytesWritten = 0; + // Write plain text application data to the SSL engine + try { + bytesWritten = writePlaintextData(state.ssl, src); + bytesConsumed += bytesWritten; + } catch (Exception e) { + throw new SSLException(e); + } + + if (bytesWritten == 0) { + throw new IllegalStateException(sm.getString("engine.failedToWriteBytes")); + } + + // Check to see if the engine wrote data into the network BIO + pendingNet = (int) BIO_ctrl_pending(state.networkBIO); + if (pendingNet > 0) { + // Do we have enough room in dst to write encrypted data? + int capacity = dst.remaining(); + if (capacity < pendingNet) { + return new SSLEngineResult( + SSLEngineResult.Status.BUFFER_OVERFLOW, getHandshakeStatus(), bytesConsumed, bytesProduced); + } + + // Write the pending data from the network BIO into the dst buffer + try { + bytesProduced += readEncryptedData(state.networkBIO, dst, pendingNet); + } catch (Exception e) { + throw new SSLException(e); + } + + return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), bytesConsumed, bytesProduced); + } + } + } + + return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), bytesConsumed, bytesProduced); + } + + @Override + public synchronized SSLEngineResult unwrap(final ByteBuffer src, final ByteBuffer[] dsts, final int offset, final int length) throws SSLException { + // Check to make sure the engine has not been closed + if (destroyed) { + return new SSLEngineResult(SSLEngineResult.Status.CLOSED, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, 0, 0); + } + + // Throw required runtime exceptions + if (src == null || dsts == null) { + throw new IllegalArgumentException(sm.getString("engine.nullBuffer")); + } + if (offset >= dsts.length || offset + length > dsts.length) { + throw new IndexOutOfBoundsException(sm.getString("engine.invalidBufferArray", + Integer.toString(offset), Integer.toString(length), + Integer.toString(dsts.length))); + } + int capacity = 0; + final int endOffset = offset + length; + for (int i = offset; i < endOffset; i++) { + ByteBuffer dst = dsts[i]; + if (dst == null) { + throw new IllegalArgumentException(sm.getString("engine.nullBufferInArray")); + } + if (dst.isReadOnly()) { + throw new ReadOnlyBufferException(); + } + capacity += dst.remaining(); + } + + // Prepare OpenSSL to work in server mode and receive handshake + if (accepted == Accepted.NOT) { + beginHandshakeImplicitly(); + } + + // In handshake or close_notify stages, check if call to unwrap was made + // without regard to the handshake status. + SSLEngineResult.HandshakeStatus handshakeStatus = getHandshakeStatus(); + if ((!handshakeFinished || engineClosed) && handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP) { + return new SSLEngineResult(getEngineStatus(), SSLEngineResult.HandshakeStatus.NEED_WRAP, 0, 0); + } + + int len = src.remaining(); + + // protect against protocol overflow attack vector + if (len > MAX_ENCRYPTED_PACKET_LENGTH) { + isInboundDone = true; + isOutboundDone = true; + engineClosed = true; + shutdown(); + throw new SSLException(sm.getString("engine.oversizedPacket")); + } + + // Write encrypted data to network BIO + int written = 0; + try { + written = writeEncryptedData(state.networkBIO, src); + } catch (Exception e) { + throw new SSLException(e); + } + + // There won't be any application data until we're done handshaking + // + // We first check handshakeFinished to eliminate the overhead of extra JNI call if possible. + int pendingApp = pendingReadableBytesInSSL(); + if (!handshakeFinished) { + pendingApp = 0; + } + int bytesProduced = 0; + int idx = offset; + // Do we have enough room in dsts to write decrypted data? + if (capacity == 0) { + return new SSLEngineResult(SSLEngineResult.Status.BUFFER_OVERFLOW, getHandshakeStatus(), written, 0); + } + + while (pendingApp > 0) { + if (idx == endOffset) { + // Destination buffer state changed (no remaining space although + // capacity is still available), so break loop with an error + throw new IllegalStateException(sm.getString("engine.invalidDestinationBuffersState")); + } + // Write decrypted data to dsts buffers + while (idx < endOffset) { + ByteBuffer dst = dsts[idx]; + if (!dst.hasRemaining()) { + idx++; + continue; + } + + if (pendingApp <= 0) { + break; + } + + int bytesRead; + try { + bytesRead = readPlaintextData(state.ssl, dst); + } catch (Exception e) { + throw new SSLException(e); + } + + if (bytesRead == 0) { + // This should not be possible. pendingApp is positive + // therefore the read should have read at least one byte. + throw new IllegalStateException(sm.getString("engine.failedToReadAvailableBytes")); + } + + bytesProduced += bytesRead; + pendingApp -= bytesRead; + capacity -= bytesRead; + + if (!dst.hasRemaining()) { + idx++; + } + } + if (capacity == 0) { + break; + } else if (pendingApp == 0) { + pendingApp = pendingReadableBytesInSSL(); + } + } + + // Check to see if we received a close_notify message from the peer + if (!receivedShutdown && (SSL_get_shutdown(state.ssl) & SSL_RECEIVED_SHUTDOWN()) == SSL_RECEIVED_SHUTDOWN()) { + receivedShutdown = true; + closeOutbound(); + closeInbound(); + } + + if (bytesProduced == 0 && (written == 0 || (written > 0 && !src.hasRemaining() && handshakeFinished))) { + return new SSLEngineResult(SSLEngineResult.Status.BUFFER_UNDERFLOW, getHandshakeStatus(), written, 0); + } else { + return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), written, bytesProduced); + } + } + + private int pendingReadableBytesInSSL() + throws SSLException { + // NOTE: Calling a fake read is necessary before calling pendingReadableBytesInSSL because + // SSL_pending will return 0 if OpenSSL has not started the current TLS record + // See https://www.openssl.org/docs/manmaster/man3/SSL_pending.html + clearLastError(); + int lastPrimingReadResult = SSL_read(state.ssl, MemorySegment.NULL, 0); // priming read + // check if SSL_read returned <= 0. In this case we need to check the error and see if it was something + // fatal. + if (lastPrimingReadResult <= 0) { + checkLastError(); + } + int pendingReadableBytesInSSL = SSL_pending(state.ssl); + + // TLS 1.0 needs additional handling + if (Constants.SSL_PROTO_TLSv1.equals(version) && lastPrimingReadResult == 0 && + pendingReadableBytesInSSL == 0) { + // Perform another priming read + lastPrimingReadResult = SSL_read(state.ssl, MemorySegment.NULL, 0); + if (lastPrimingReadResult <= 0) { + checkLastError(); + } + pendingReadableBytesInSSL = SSL_pending(state.ssl); + } + + return pendingReadableBytesInSSL; + } + + @Override + public Runnable getDelegatedTask() { + // Currently, we do not delegate SSL computation tasks + return null; + } + + @Override + public synchronized void closeInbound() throws SSLException { + if (isInboundDone) { + return; + } + + isInboundDone = true; + engineClosed = true; + + shutdown(); + + if (accepted != Accepted.NOT && !receivedShutdown) { + throw new SSLException(sm.getString("engine.inboundClose")); + } + } + + @Override + public synchronized boolean isInboundDone() { + return isInboundDone || engineClosed; + } + + @Override + public synchronized void closeOutbound() { + if (isOutboundDone) { + return; + } + + isOutboundDone = true; + engineClosed = true; + + if (accepted != Accepted.NOT && !destroyed) { + int mode = SSL_get_shutdown(state.ssl); + if ((mode & SSL_SENT_SHUTDOWN()) != SSL_SENT_SHUTDOWN()) { + SSL_shutdown(state.ssl); + } + } else { + // engine closing before initial handshake + shutdown(); + } + } + + @Override + public synchronized boolean isOutboundDone() { + return isOutboundDone; + } + + @Override + public String[] getSupportedCipherSuites() { + Set availableCipherSuites = AVAILABLE_CIPHER_SUITES; + return availableCipherSuites.toArray(new String[0]); + } + + @Override + public synchronized String[] getEnabledCipherSuites() { + if (destroyed) { + return new String[0]; + } + String[] enabled = OpenSSLLibrary.getCiphers(state.ssl); + if (enabled == null) { + return new String[0]; + } else { + for (int i = 0; i < enabled.length; i++) { + String mapped = OpenSSLCipherConfigurationParser.openSSLToJsse(enabled[i]); + if (mapped != null) { + enabled[i] = mapped; + } + } + return enabled; + } + } + + @Override + public synchronized void setEnabledCipherSuites(String[] cipherSuites) { + if (initialized) { + return; + } + if (cipherSuites == null) { + throw new IllegalArgumentException(sm.getString("engine.nullCipherSuite")); + } + if (destroyed) { + return; + } + final StringBuilder buf = new StringBuilder(); + for (String cipherSuite : cipherSuites) { + if (cipherSuite == null) { + break; + } + String converted = OpenSSLCipherConfigurationParser.jsseToOpenSSL(cipherSuite); + if (!AVAILABLE_CIPHER_SUITES.contains(cipherSuite)) { + log.debug(sm.getString("engine.unsupportedCipher", cipherSuite, converted)); + } + if (converted != null) { + cipherSuite = converted; + } + + buf.append(cipherSuite); + buf.append(':'); + } + + if (buf.length() == 0) { + throw new IllegalArgumentException(sm.getString("engine.emptyCipherSuite")); + } + buf.setLength(buf.length() - 1); + + final String cipherSuiteSpec = buf.toString(); + try (var localArena = Arena.ofConfined()) { + SSL_set_cipher_list(state.ssl, localArena.allocateFrom(cipherSuiteSpec)); + } catch (Exception e) { + throw new IllegalStateException(sm.getString("engine.failedCipherSuite", cipherSuiteSpec), e); + } + } + + @Override + public String[] getSupportedProtocols() { + return IMPLEMENTED_PROTOCOLS_SET.toArray(new String[0]); + } + + @Override + public synchronized String[] getEnabledProtocols() { + if (destroyed) { + return new String[0]; + } + List enabled = new ArrayList<>(); + // Seems like there is no way to explicitly disable SSLv2Hello in OpenSSL so it is always enabled + enabled.add(Constants.SSL_PROTO_SSLv2Hello); + long opts = SSL_get_options(state.ssl); + if ((opts & SSL_OP_NO_TLSv1()) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1); + } + if ((opts & SSL_OP_NO_TLSv1_1()) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_1); + } + if ((opts & SSL_OP_NO_TLSv1_2()) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_2); + } + if ((opts & SSL_OP_NO_TLSv1_3()) == 0) { + enabled.add(Constants.SSL_PROTO_TLSv1_3); + } + if ((opts & SSL_OP_NO_SSLv2()) == 0) { + enabled.add(Constants.SSL_PROTO_SSLv2); + } + if ((opts & SSL_OP_NO_SSLv3()) == 0) { + enabled.add(Constants.SSL_PROTO_SSLv3); + } + int size = enabled.size(); + if (size == 0) { + return new String[0]; + } else { + return enabled.toArray(new String[size]); + } + } + + @Override + public synchronized void setEnabledProtocols(String[] protocols) { + if (initialized) { + return; + } + if (protocols == null) { + // This is correct from the API docs + throw new IllegalArgumentException(); + } + if (destroyed) { + return; + } + boolean sslv2 = false; + boolean sslv3 = false; + boolean tlsv1 = false; + boolean tlsv1_1 = false; + boolean tlsv1_2 = false; + boolean tlsv1_3 = false; + for (String p : protocols) { + if (!IMPLEMENTED_PROTOCOLS_SET.contains(p)) { + throw new IllegalArgumentException(sm.getString("engine.unsupportedProtocol", p)); + } + if (p.equals(Constants.SSL_PROTO_SSLv2)) { + sslv2 = true; + } else if (p.equals(Constants.SSL_PROTO_SSLv3)) { + sslv3 = true; + } else if (p.equals(Constants.SSL_PROTO_TLSv1)) { + tlsv1 = true; + } else if (p.equals(Constants.SSL_PROTO_TLSv1_1)) { + tlsv1_1 = true; + } else if (p.equals(Constants.SSL_PROTO_TLSv1_2)) { + tlsv1_2 = true; + } else if (p.equals(Constants.SSL_PROTO_TLSv1_3)) { + tlsv1_3 = true; + } + } + // Enable all and then disable what we not want + SSL_set_options(state.ssl, SSL_OP_ALL()); + + if (!sslv2) { + SSL_set_options(state.ssl, SSL_OP_NO_SSLv2()); + } + if (!sslv3) { + SSL_set_options(state.ssl, SSL_OP_NO_SSLv3()); + } + if (!tlsv1) { + SSL_set_options(state.ssl, SSL_OP_NO_TLSv1()); + } + if (!tlsv1_1) { + SSL_set_options(state.ssl, SSL_OP_NO_TLSv1_1()); + } + if (!tlsv1_2) { + SSL_set_options(state.ssl, SSL_OP_NO_TLSv1_2()); + } + if (!tlsv1_3) { + SSL_set_options(state.ssl, SSL_OP_NO_TLSv1_3()); + } + } + + @Override + public SSLSession getSession() { + return session; + } + + @Override + public synchronized void beginHandshake() throws SSLException { + if (engineClosed || destroyed) { + throw new SSLException(sm.getString("engine.engineClosed")); + } + switch (accepted) { + case NOT: + handshake(); + accepted = Accepted.EXPLICIT; + break; + case IMPLICIT: + // A user did not start handshake by calling this method by themselves, + // but handshake has been started already by wrap() or unwrap() implicitly. + // Because it's the user's first time to call this method, it is unfair to + // raise an exception. From the user's standpoint, they never asked for + // renegotiation. + + accepted = Accepted.EXPLICIT; // Next time this method is invoked by the user, we should raise an exception. + break; + case EXPLICIT: + renegotiate(); + break; + } + } + + private byte[] getPeerCertificate() { + try (var localArena = Arena.ofConfined()) { + MemorySegment/*(X509*)*/ x509 = (OpenSSLContext.OPENSSL_3 ? SSL_get1_peer_certificate(state.ssl) : SSL_get_peer_certificate(state.ssl)); + MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + int length = i2d_X509(x509, bufPointer); + if (length <= 0) { + return null; + } + MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); + byte[] certificate = buf.reinterpret(length, localArena, null).toArray(ValueLayout.JAVA_BYTE); + X509_free(x509); + OPENSSL_free(buf); + return certificate; + } + } + + private byte[][] getPeerCertChain() { + MemorySegment/*STACK_OF(X509)*/ sk = SSL_get_peer_cert_chain(state.ssl); + int len = OPENSSL_sk_num(sk); + if (len <= 0) { + return null; + } + byte[][] certificateChain = new byte[len][]; + try (var localArena = Arena.ofConfined()) { + for (int i = 0; i < len; i++) { + MemorySegment/*(X509*)*/ x509 = OPENSSL_sk_value(sk, i); + MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + int length = i2d_X509(x509, bufPointer); + if (length < 0) { + certificateChain[i] = new byte[0]; + continue; + } + MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); + byte[] certificate = buf.reinterpret(length, localArena, null).toArray(ValueLayout.JAVA_BYTE); + certificateChain[i] = certificate; + OPENSSL_free(buf); + } + return certificateChain; + } + } + + private String getProtocolNegotiated() { + try (var localArena = Arena.ofConfined()) { + MemorySegment lenAddress = localArena.allocate(ValueLayout.JAVA_INT); + MemorySegment protocolPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + SSL_get0_alpn_selected(state.ssl, protocolPointer, lenAddress); + if (MemorySegment.NULL.equals(protocolPointer)) { + return null; + } + int length = lenAddress.get(ValueLayout.JAVA_INT, 0); + if (length == 0) { + return null; + } + MemorySegment protocolAddress = protocolPointer.get(ValueLayout.ADDRESS, 0); + byte[] name = protocolAddress.reinterpret(length, localArena, null).toArray(ValueLayout.JAVA_BYTE); + if (log.isTraceEnabled()) { + log.trace("Protocol negotiated [" + new String(name) + "]"); + } + return new String(name); + } + } + + private void beginHandshakeImplicitly() throws SSLException { + handshake(); + accepted = Accepted.IMPLICIT; + } + + private void handshake() throws SSLException { + currentHandshake = state.handshakeCount; + clearLastError(); + int code = SSL_do_handshake(state.ssl); + if (code <= 0) { + checkLastError(); + } else { + if (alpn) { + selectedProtocol = getProtocolNegotiated(); + } + session.lastAccessedTime = System.currentTimeMillis(); + // if SSL_do_handshake returns > 0 it means the handshake was finished. This means we can update + // handshakeFinished directly and so eliminate unnecessary calls to SSL.isInInit(...) + handshakeFinished = true; + } + } + + private void renegotiate() throws SSLException { + if (log.isTraceEnabled()) { + log.trace("Start renegotiate"); + } + clearLastError(); + int code; + if (SSL_get_version(state.ssl).getString(0).equals(Constants.SSL_PROTO_TLSv1_3)) { + state.phaState = PHAState.START; + code = SSL_verify_client_post_handshake(state.ssl); + } else { + code = SSL_renegotiate(state.ssl); + } + if (code <= 0) { + checkLastError(); + } + handshakeFinished = false; + peerCerts = null; + currentHandshake = state.handshakeCount; + int code2 = SSL_do_handshake(state.ssl); + if (code2 <= 0) { + checkLastError(); + } + } + + private void checkLastError() throws SSLException { + String sslError = OpenSSLContext.getLastError(); + if (sslError != null) { + // Many errors can occur during handshake and need to be reported + if (!handshakeFinished) { + sendHandshakeError = true; + } else { + throw new SSLException(sslError); + } + } + } + + + /** + * Clear out any errors, but log a warning. + */ + private static void clearLastError() { + OpenSSLContext.getLastError(); + } + + private SSLEngineResult.Status getEngineStatus() { + return engineClosed ? SSLEngineResult.Status.CLOSED : SSLEngineResult.Status.OK; + } + + @Override + public synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() { + if (accepted == Accepted.NOT || destroyed) { + return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; + } + + // Check if we are in the initial handshake phase + if (!handshakeFinished) { + + // There is pending data in the network BIO -- call wrap + if (sendHandshakeError || BIO_ctrl_pending(state.networkBIO) != 0) { + if (sendHandshakeError) { + // After a last wrap, consider it is going to be done + sendHandshakeError = false; + currentHandshake++; + } + return SSLEngineResult.HandshakeStatus.NEED_WRAP; + } + + /* + * Tomcat Native stores a count of the completed handshakes in the + * SSL instance and increments it every time a handshake is + * completed. Comparing the handshake count when the handshake + * started to the current handshake count enables this code to + * detect when the handshake has completed. + * + * Obtaining client certificates after the connection has been + * established requires additional checks. We need to trigger + * additional reads until the certificates have been read but we + * don't know how many reads we will need as it depends on both + * client and network behaviour. + * + * The additional reads are triggered by returning NEED_UNWRAP + * rather than FINISHED. This allows the standard I/O code to be + * used. + * + * For TLSv1.2 and below, the handshake completes before the + * renegotiation. We therefore use SSL.renegotiatePending() to + * check on the current status of the renegotiation and return + * NEED_UNWRAP until it completes which means the client + * certificates will have been read from the client. + * + * For TLSv1.3, Tomcat Native sets a flag when post handshake + * authentication is started and updates it once the client + * certificate has been received. We therefore use + * SSL.getPostHandshakeAuthInProgress() to check the current status + * and return NEED_UNWRAP until that methods indicates that PHA is + * no longer in progress. + */ + + // No pending data to be sent to the peer + // Check to see if we have finished handshaking + if (state.handshakeCount != currentHandshake && SSL_renegotiate_pending(state.ssl) == 0 && + (state.phaState != PHAState.START)) { + if (alpn) { + selectedProtocol = getProtocolNegotiated(); + } + session.lastAccessedTime = System.currentTimeMillis(); + version = SSL_get_version(state.ssl).getString(0); + handshakeFinished = true; + return SSLEngineResult.HandshakeStatus.FINISHED; + } + + // No pending data + // Still handshaking / renegotiation / post-handshake auth pending + // Must be waiting on the peer to send more data + return SSLEngineResult.HandshakeStatus.NEED_UNWRAP; + } + + // Check if we are in the shutdown phase + if (engineClosed) { + // Waiting to send the close_notify message + if (BIO_ctrl_pending(state.networkBIO) != 0) { + return SSLEngineResult.HandshakeStatus.NEED_WRAP; + } + + // Must be waiting to receive the close_notify message + return SSLEngineResult.HandshakeStatus.NEED_UNWRAP; + } + + return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; + } + + @Override + public void setUseClientMode(boolean clientMode) { + if (clientMode != this.clientMode) { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean getUseClientMode() { + return clientMode; + } + + @Override + public void setNeedClientAuth(boolean b) { + setClientAuth(b ? ClientAuthMode.REQUIRE : ClientAuthMode.NONE); + } + + @Override + public boolean getNeedClientAuth() { + return clientAuth == ClientAuthMode.REQUIRE; + } + + @Override + public void setWantClientAuth(boolean b) { + setClientAuth(b ? ClientAuthMode.OPTIONAL : ClientAuthMode.NONE); + } + + @Override + public boolean getWantClientAuth() { + return clientAuth == ClientAuthMode.OPTIONAL; + } + + private void setClientAuth(ClientAuthMode mode) { + if (clientMode) { + return; + } + synchronized (this) { + if (clientAuth == mode) { + return; + } + state.certificateVerifyMode = switch (mode) { + case NONE -> SSL_VERIFY_NONE(); + case REQUIRE -> SSL_VERIFY_FAIL_IF_NO_PEER_CERT(); + case OPTIONAL -> certificateVerificationOptionalNoCA ? OpenSSLContext.OPTIONAL_NO_CA : SSL_VERIFY_PEER(); + }; + // Set int verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx) callback + int value = switch (mode) { + case NONE -> SSL_VERIFY_NONE(); + case REQUIRE -> SSL_VERIFY_PEER() | SSL_VERIFY_FAIL_IF_NO_PEER_CERT(); + case OPTIONAL -> SSL_VERIFY_PEER(); + }; + // Note: Since a callback is always set by the context, the callback here could in theory + // be set to NULL (at the time of creation of the SSL, the SSL_CTX will have a non null callback) + SSL_set_verify(state.ssl, value, SSL_set_verify$callback.allocate(new VerifyCallback(), engineArena)); + clientAuth = mode; + } + } + + private static class InfoCallback implements SSL_set_info_callback$cb.Function { + @Override + public void apply(MemorySegment ssl, int where, int ret) { + EngineState state = getState(ssl); + if (state == null) { + log.warn(sm.getString("engine.noSSL", Long.valueOf(ssl.address()))); + return; + } + if (0 != (where & SSL_CB_HANDSHAKE_DONE())) { + state.handshakeCount++; + } + } + } + + static class VerifyCallback implements SSL_set_verify$callback.Function, SSL_CTX_set_verify$callback.Function { + @Override + public int apply(int preverify_ok, MemorySegment /*X509_STORE_CTX*/ x509ctx) { + MemorySegment ssl = X509_STORE_CTX_get_ex_data(x509ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + EngineState state = getState(ssl); + if (state == null) { + log.warn(sm.getString("engine.noSSL", Long.valueOf(ssl.address()))); + return 0; + } + if (log.isTraceEnabled()) { + log.trace("Verification in engine with mode [" + state.certificateVerifyMode + "] for " + state.ssl); + } + int ok = preverify_ok; + int errnum = X509_STORE_CTX_get_error(x509ctx); + int errdepth = X509_STORE_CTX_get_error_depth(x509ctx); + state.phaState = PHAState.COMPLETE; + if (state.certificateVerifyMode == -1 /*SSL_CVERIFY_UNSET*/ || state.certificateVerifyMode == SSL_VERIFY_NONE()) { + return 1; + } + /*SSL_VERIFY_ERROR_IS_OPTIONAL(errnum) -> ((errnum == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) + || (errnum == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) + || (errnum == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) + || (errnum == X509_V_ERR_CERT_UNTRUSTED) + || (errnum == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE))*/ + boolean verifyErrorIsOptional = (errnum == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT()) + || (errnum == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN()) + || (errnum == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY()) + || (errnum == X509_V_ERR_CERT_UNTRUSTED()) + || (errnum == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE()); + if (verifyErrorIsOptional && (state.certificateVerifyMode == OpenSSLContext.OPTIONAL_NO_CA)) { + ok = 1; + SSL_set_verify_result(state.ssl, X509_V_OK()); + } + /* + * Expired certificates vs. "expired" CRLs: by default, OpenSSL + * turns X509_V_ERR_CRL_HAS_EXPIRED into a "certificate_expired(45)" + * SSL alert, but that's not really the message we should convey to the + * peer (at the very least, it's confusing, and in many cases, it's also + * inaccurate, as the certificate itself may very well not have expired + * yet). We set the X509_STORE_CTX error to something which OpenSSL's + * s3_both.c:ssl_verify_alarm_type() maps to SSL_AD_CERTIFICATE_UNKNOWN, + * i.e. the peer will receive a "certificate_unknown(46)" alert. + * We do not touch errnum, though, so that later on we will still log + * the "real" error, as returned by OpenSSL. + */ + if (ok == 0 && errnum == X509_V_ERR_CRL_HAS_EXPIRED()) { + X509_STORE_CTX_set_error(x509ctx, -1); + } + + // OCSP + if (!state.noOcspCheck && (ok > 0)) { + /* If there was an optional verification error, it's not + * possible to perform OCSP validation since the issuer may be + * missing/untrusted. Fail in that case. + */ + if (verifyErrorIsOptional) { + if (state.certificateVerifyMode != OpenSSLContext.OPTIONAL_NO_CA) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_APPLICATION_VERIFICATION()); + errnum = X509_V_ERR_APPLICATION_VERIFICATION(); + ok = 0; + } + } else { + int ocspResponse = processOCSP(x509ctx); + if (ocspResponse == V_OCSP_CERTSTATUS_REVOKED()) { + ok = 0; + errnum = X509_STORE_CTX_get_error(x509ctx); + } else if (ocspResponse == V_OCSP_CERTSTATUS_UNKNOWN()) { + errnum = X509_STORE_CTX_get_error(x509ctx); + if (errnum <= 0) { + ok = 0; + } + } + } + } + + if (errdepth > state.certificateVerificationDepth) { + // Certificate Verification: Certificate Chain too long + ok = 0; + } + return ok; + } + } + + private static int processOCSP(MemorySegment /*X509_STORE_CTX*/ x509ctx) { + int ocspResponse = V_OCSP_CERTSTATUS_UNKNOWN(); + MemorySegment x509 = X509_STORE_CTX_get_current_cert(x509ctx); + if (!MemorySegment.NULL.equals(x509)) { + // No need to check cert->valid, because ssl_verify_OCSP() only + // is called if OpenSSL already successfully verified the certificate + // (parameter "ok" in SSL_callback_SSL_verify() must be true). + if (X509_check_issued(x509, x509) == X509_V_OK()) { + // don't do OCSP checking for valid self-issued certs + X509_STORE_CTX_set_error(x509ctx, X509_V_OK()); + } else { + // If we can't get the issuer, we cannot perform OCSP verification + MemorySegment issuer = X509_STORE_CTX_get0_current_issuer(x509ctx); + if (!MemorySegment.NULL.equals(issuer)) { + // sslutils.c ssl_ocsp_request(x509, issuer, x509ctx); + int nid = X509_get_ext_by_NID(x509, NID_info_access(), -1); + if (nid >= 0) { + try (var localArenal = Arena.ofConfined()) { + MemorySegment ext = X509_get_ext(x509, nid); + MemorySegment os = X509_EXTENSION_get_data(ext); + int length = ASN1_STRING_length(os); + MemorySegment data = ASN1_STRING_get0_data(os); + // ocsp_urls = decode_OCSP_url(os); + byte[] asn1String = data.reinterpret(length, localArenal, null).toArray(ValueLayout.JAVA_BYTE); + Asn1Parser parser = new Asn1Parser(asn1String); + // Parse the byte sequence + ArrayList urls = new ArrayList<>(); + try { + parseOCSPURLs(parser, urls); + } catch (Exception e) { + log.error(sm.getString("engine.ocspParseError"), e); + } + if (!urls.isEmpty()) { + // Use OpenSSL to build OCSP request + for (String urlString : urls) { + try { + URL url = (new URI(urlString)).toURL(); + ocspResponse = processOCSPRequest(url, issuer, x509, x509ctx, localArenal); + if (log.isDebugEnabled()) { + log.debug(sm.getString("engine.ocspResponse", urlString, + Integer.toString(ocspResponse))); + } + } catch (MalformedURLException | URISyntaxException e) { + log.warn(sm.getString("engine.invalidOCSPURL", urlString)); + } + if (ocspResponse != V_OCSP_CERTSTATUS_UNKNOWN()) { + break; + } + } + } + } + } + } + } + } + return ocspResponse; + } + + private static final int ASN1_SEQUENCE = 0x30; + private static final int ASN1_OID = 0x06; + private static final int ASN1_STRING = 0x86; + private static final byte[] OCSP_OID = {0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01}; + + private static void parseOCSPURLs(Asn1Parser parser, ArrayList urls) { + while (!parser.eof()) { + int tag = parser.peekTag(); + if (tag == ASN1_SEQUENCE) { + parser.parseTag(ASN1_SEQUENCE); + parser.parseFullLength(); + } else if (tag == ASN1_OID) { + parser.parseTag(ASN1_OID); + int oidLen = parser.parseLength(); + byte[] oid = new byte[oidLen]; + parser.parseBytes(oid); + if (Arrays.compareUnsigned(oid, 0, OCSP_OID.length, OCSP_OID, 0, OCSP_OID.length) == 0) { + parser.parseTag(ASN1_STRING); + int urlLen = parser.parseLength(); + byte[] url = new byte[urlLen]; + parser.parseBytes(url); + urls.add(new String(url)); + } + } else { + return; + } + } + } + + private static int processOCSPRequest(URL url, MemorySegment issuer, MemorySegment x509, + MemorySegment /*X509_STORE_CTX*/ x509ctx, Arena localArena) { + MemorySegment ocspRequest = MemorySegment.NULL; + MemorySegment ocspResponse = MemorySegment.NULL; + MemorySegment id = MemorySegment.NULL; + MemorySegment ocspOneReq = MemorySegment.NULL; + HttpURLConnection connection = null; + MemorySegment basicResponse = MemorySegment.NULL; + MemorySegment certId = MemorySegment.NULL; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + ocspRequest = OCSP_REQUEST_new(); + if (MemorySegment.NULL.equals(ocspRequest)) { + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + id = OCSP_cert_to_id(MemorySegment.NULL, x509, issuer); + if (MemorySegment.NULL.equals(id)) { + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + ocspOneReq = OCSP_request_add0_id(ocspRequest, id); + if (MemorySegment.NULL.equals(ocspOneReq)) { + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + int requestLength = i2d_OCSP_REQUEST(ocspRequest, bufPointer); + if (requestLength <= 0) { + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); + // HTTP request with the following header: + // POST urlPath HTTP/1.1 + // Host: urlHost:urlPort + // Content-Type: application/ocsp-request + // Content-Length: ocspRequestData.length + byte[] ocspRequestData = buf.reinterpret(requestLength, localArena, null).toArray(ValueLayout.JAVA_BYTE); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setFixedLengthStreamingMode(requestLength); + connection.setRequestProperty("Content-Type", "application/ocsp-request"); + connection.connect(); + connection.getOutputStream().write(ocspRequestData); + int responseCode = connection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + InputStream is = connection.getInputStream(); + int read = 0; + byte[] responseBuf = new byte[1024]; + while ((read = is.read(responseBuf)) > 0) { + baos.write(responseBuf, 0, read); + } + byte[] responseData = baos.toByteArray(); + var nativeResponseData = localArena.allocateFrom(ValueLayout.JAVA_BYTE, responseData); + var nativeResponseDataPointer = localArena.allocateFrom(ValueLayout.ADDRESS, nativeResponseData); + ocspResponse = d2i_OCSP_RESPONSE(MemorySegment.NULL, nativeResponseDataPointer, responseData.length); + if (!MemorySegment.NULL.equals(ocspResponse)) { + if (OCSP_response_status(ocspResponse) == OCSP_RESPONSE_STATUS_SUCCESSFUL()) { + basicResponse = OCSP_response_get1_basic(ocspResponse); + certId = OCSP_cert_to_id(MemorySegment.NULL, x509, issuer); + if (MemorySegment.NULL.equals(certId)) { + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + // Find by serial number and get the matching response + MemorySegment singleResponse = OCSP_resp_get0(basicResponse, OCSP_resp_find(basicResponse, certId, -1)); + return OCSP_single_get0_status(singleResponse, MemorySegment.NULL, + MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); + } + } + } catch (Exception e) { + log.warn(sm.getString("engine.ocspRequestError", url.toString()), e); + } finally { + if (MemorySegment.NULL.equals(ocspResponse)) { + // Failed to get a valid response + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_APPLICATION_VERIFICATION()); + } + OCSP_CERTID_free(certId); + OCSP_BASICRESP_free(basicResponse); + OCSP_RESPONSE_free(ocspResponse); + OCSP_REQUEST_free(ocspRequest); + if (connection != null) { + connection.disconnect(); + } + } + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + + @Override + public void setEnableSessionCreation(boolean b) { + if (!b) { + String msg = sm.getString("engine.noRestrictSessionCreation"); + throw new UnsupportedOperationException(msg); + } + } + + @Override + public boolean getEnableSessionCreation() { + return true; + } + + + private class OpenSSLSession implements SSLSession { + + // lazy init for memory reasons + private Map values; + + // Last accessed time + private long lastAccessedTime = -1; + + @Override + public byte[] getId() { + byte[] id = null; + synchronized (OpenSSLEngine.this) { + if (!destroyed) { + try (var localArena = Arena.ofConfined()) { + MemorySegment lenPointer = localArena.allocate(ValueLayout.JAVA_INT); + var session = SSL_get_session(state.ssl); + if (MemorySegment.NULL.equals(session)) { + return new byte[0]; + } + MemorySegment sessionId = SSL_SESSION_get_id(session, lenPointer); + int len = lenPointer.get(ValueLayout.JAVA_INT, 0); + id = (len == 0) ? new byte[0] + : sessionId.reinterpret(len, localArena, null).toArray(ValueLayout.JAVA_BYTE); + } + } + } + + return id; + } + + @Override + public SSLSessionContext getSessionContext() { + return sessionContext; + } + + @Override + public long getCreationTime() { + // We need to multiply by 1000 as OpenSSL uses seconds and we need milliseconds. + long creationTime = 0; + synchronized (OpenSSLEngine.this) { + if (!destroyed) { + var session = SSL_get_session(state.ssl); + if (!MemorySegment.NULL.equals(session)) { + creationTime = SSL_SESSION_get_time(session); + } + } + } + return creationTime * 1000L; + } + + @Override + public long getLastAccessedTime() { + return (lastAccessedTime > 0) ? lastAccessedTime : getCreationTime(); + } + + @Override + public void invalidate() { + // NOOP + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public void putValue(String name, Object value) { + if (name == null) { + throw new IllegalArgumentException(sm.getString("engine.nullName")); + } + if (value == null) { + throw new IllegalArgumentException(sm.getString("engine.nullValue")); + } + Map values = this.values; + if (values == null) { + // Use size of 2 to keep the memory overhead small + values = this.values = new HashMap<>(2); + } + Object old = values.put(name, value); + if (value instanceof SSLSessionBindingListener) { + ((SSLSessionBindingListener) value).valueBound(new SSLSessionBindingEvent(this, name)); + } + notifyUnbound(old, name); + } + + @Override + public Object getValue(String name) { + if (name == null) { + throw new IllegalArgumentException(sm.getString("engine.nullName")); + } + if (values == null) { + return null; + } + return values.get(name); + } + + @Override + public void removeValue(String name) { + if (name == null) { + throw new IllegalArgumentException(sm.getString("engine.nullName")); + } + Map values = this.values; + if (values == null) { + return; + } + Object old = values.remove(name); + notifyUnbound(old, name); + } + + @Override + public String[] getValueNames() { + Map values = this.values; + if (values == null || values.isEmpty()) { + return new String[0]; + } + return values.keySet().toArray(new String[0]); + } + + private void notifyUnbound(Object value, String name) { + if (value instanceof SSLSessionBindingListener) { + ((SSLSessionBindingListener) value).valueUnbound(new SSLSessionBindingEvent(this, name)); + } + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + // these are lazy created to reduce memory overhead + Certificate[] c = peerCerts; + if (c == null) { + byte[] clientCert; + byte[][] chain; + synchronized (OpenSSLEngine.this) { + if (destroyed || SSL_in_init(state.ssl) != 0) { + throw new SSLPeerUnverifiedException(sm.getString("engine.unverifiedPeer")); + } + chain = getPeerCertChain(); + if (!clientMode) { + // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer certificate. + // We use SSL_get_peer_certificate to get it in this case and add it to our array later. + // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html + clientCert = getPeerCertificate(); + } else { + clientCert = null; + } + } + if (chain == null && clientCert == null) { + return null; + } + int len = 0; + if (chain != null) { + len += chain.length; + } + + int i = 0; + Certificate[] certificates; + if (clientCert != null) { + len++; + certificates = new Certificate[len]; + certificates[i++] = new OpenSSLX509Certificate(clientCert); + } else { + certificates = new Certificate[len]; + } + if (chain != null) { + int a = 0; + for (; i < certificates.length; i++) { + certificates[i] = new OpenSSLX509Certificate(chain[a++]); + } + } + c = peerCerts = certificates; + } + return c; + } + + @Override + public Certificate[] getLocalCertificates() { + // FIXME (if possible): Not available in the OpenSSL API + return EMPTY_CERTIFICATES; + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + Certificate[] peer = getPeerCertificates(); + if (peer == null || peer.length == 0) { + return null; + } + return principal(peer); + } + + @Override + public Principal getLocalPrincipal() { + Certificate[] local = getLocalCertificates(); + if (local == null || local.length == 0) { + return null; + } + return principal(local); + } + + private Principal principal(Certificate[] certs) { + return ((java.security.cert.X509Certificate) certs[0]).getIssuerX500Principal(); + } + + @Override + public String getCipherSuite() { + if (cipher == null) { + String ciphers; + synchronized (OpenSSLEngine.this) { + if (!handshakeFinished) { + return INVALID_CIPHER; + } + if (destroyed) { + return INVALID_CIPHER; + } + ciphers = SSL_CIPHER_get_name(SSL_get_current_cipher(state.ssl)).getString(0); + } + String c = OpenSSLCipherConfigurationParser.openSSLToJsse(ciphers); + if (c != null) { + cipher = c; + } + } + return cipher; + } + + @Override + public String getProtocol() { + String applicationProtocol = OpenSSLEngine.this.applicationProtocol; + if (applicationProtocol == null) { + applicationProtocol = fallbackApplicationProtocol; + if (applicationProtocol != null) { + OpenSSLEngine.this.applicationProtocol = applicationProtocol.replace(':', '_'); + } else { + OpenSSLEngine.this.applicationProtocol = applicationProtocol = ""; + } + } + String version = null; + synchronized (OpenSSLEngine.this) { + if (!destroyed) { + version = SSL_get_version(state.ssl).getString(0); + } + } + if (applicationProtocol.isEmpty()) { + return version; + } else { + return version + ':' + applicationProtocol; + } + } + + @Override + public String getPeerHost() { + // Not available for now in Tomcat (needs to be passed during engine creation) + return null; + } + + @Override + public int getPeerPort() { + // Not available for now in Tomcat (needs to be passed during engine creation) + return 0; + } + + @Override + public int getPacketBufferSize() { + return MAX_ENCRYPTED_PACKET_LENGTH; + } + + @Override + public int getApplicationBufferSize() { + return MAX_PLAINTEXT_LENGTH; + } + + } + + private static class EngineState implements Runnable { + + private final Arena stateArena = Arena.ofShared(); + private final MemorySegment ssl; + private final MemorySegment networkBIO; + private final int certificateVerificationDepth; + private final boolean noOcspCheck; + + private PHAState phaState = PHAState.NONE; + private int certificateVerifyMode = 0; + private int handshakeCount = 0; + + private EngineState(MemorySegment ssl, MemorySegment networkBIO, + int certificateVerificationDepth, boolean noOcspCheck) { + states.put(Long.valueOf(ssl.address()), this); + this.certificateVerificationDepth = certificateVerificationDepth; + this.noOcspCheck = noOcspCheck; + // Use another arena to avoid keeping a reference through segments + // This also allows making further accesses to the main pointers safer + this.ssl = ssl.reinterpret(ValueLayout.ADDRESS.byteSize(), stateArena, + (MemorySegment t) -> SSL_free(t)); + this.networkBIO = networkBIO.reinterpret(ValueLayout.ADDRESS.byteSize(), stateArena, + (MemorySegment t) -> BIO_free(t)); + } + + @Override + public void run() { + states.remove(Long.valueOf(ssl.address())); + stateArena.close(); + } + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLImplementation.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLImplementation.java new file mode 100644 index 0000000..260d196 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLImplementation.java @@ -0,0 +1,42 @@ +/* + * 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.tomcat.util.net.openssl.panama; + +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLSession; + +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLImplementation; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SSLUtil; +import org.apache.tomcat.util.net.jsse.JSSESupport; + +public class OpenSSLImplementation extends SSLImplementation { + + @Override + public SSLSupport getSSLSupport(SSLSession session, Map> additionalAttributes) { + return new JSSESupport(session, additionalAttributes); + } + + @Override + public SSLUtil getSSLUtil(SSLHostConfigCertificate certificate) { + return new OpenSSLUtil(certificate); + } + +} diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java new file mode 100644 index 0000000..45e3178 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java @@ -0,0 +1,447 @@ +/* + * 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.tomcat.util.net.openssl.panama; + + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; + +import static org.apache.tomcat.util.openssl.openssl_h.*; +import static org.apache.tomcat.util.openssl.openssl_h_Compatibility.*; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.openssl.OpenSSLStatus; +import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser; +import org.apache.tomcat.util.res.StringManager; + + + +/** + * Implementation of a global initialization of OpenSSL according to specified + * configuration parameters. + * Using this from a listener is completely optional, but is needed for + * configuration and full cleanup of a few native memory allocations. + */ +public class OpenSSLLibrary { + + private static final Log log = LogFactory.getLog(OpenSSLLibrary.class); + + /** + * The string manager for this package. + */ + protected static final StringManager sm = StringManager.getManager(OpenSSLLibrary.class); + + + // ---------------------------------------------- Properties + protected static String SSLEngine = "on"; //default on + protected static String FIPSMode = "off"; // default off, valid only when SSLEngine="on" + protected static String SSLRandomSeed = "builtin"; + protected static boolean fipsModeActive = false; + + /** + * The "FIPS mode" level that we use as the argument to OpenSSL method + * FIPS_mode_set() to enable FIPS mode and that we expect as + * the return value of FIPS_mode() when FIPS mode is enabled. + *

    + * In the future the OpenSSL library might grow support for different + * non-zero "FIPS" modes that specify different allowed subsets of ciphers + * or whatever, but nowadays only "1" is the supported value. + *

    + * @see OpenSSL method FIPS_mode_set() + * @see OpenSSL method FIPS_mode() + */ + private static final int FIPS_ON = 1; + + private static final int FIPS_OFF = 0; + + protected static final Object lock = new Object(); + + static MemorySegment enginePointer = MemorySegment.NULL; + + static void initLibrary() { + synchronized (lock) { + if (OpenSSLStatus.isLibraryInitialized()) { + return; + } + long initParam = (OpenSSL_version_num() >= 0x3000000fL) ? 0 : OPENSSL_INIT_ENGINE_ALL_BUILTIN(); + OPENSSL_init_ssl(initParam, MemorySegment.NULL); + OpenSSLStatus.setLibraryInitialized(true); + } + } + + /* + { BN_get_rfc3526_prime_8192, NULL, 6145 }, + { BN_get_rfc3526_prime_6144, NULL, 4097 }, + { BN_get_rfc3526_prime_4096, NULL, 3073 }, + { BN_get_rfc3526_prime_3072, NULL, 2049 }, + { BN_get_rfc3526_prime_2048, NULL, 1025 }, + { BN_get_rfc2409_prime_1024, NULL, 0 } + */ + static final class DHParam { + final MemorySegment dh; + final int min; + private DHParam(MemorySegment dh, int min) { + this.dh = dh; + this.min = min; + } + } + static final DHParam[] dhParameters = new DHParam[6]; + + private static void initDHParameters() { + var dh = DH_new(); + var p = BN_get_rfc3526_prime_8192(MemorySegment.NULL); + var g = BN_new(); + BN_set_word(g, 2); + DH_set0_pqg(dh, p, MemorySegment.NULL, g); + dhParameters[0] = new DHParam(dh, 6145); + dh = DH_new(); + p = BN_get_rfc3526_prime_6144(MemorySegment.NULL); + g = BN_new(); + BN_set_word(g, 2); + DH_set0_pqg(dh, p, MemorySegment.NULL, g); + dhParameters[1] = new DHParam(dh, 4097); + dh = DH_new(); + p = BN_get_rfc3526_prime_4096(MemorySegment.NULL); + g = BN_new(); + BN_set_word(g, 2); + DH_set0_pqg(dh, p, MemorySegment.NULL, g); + dhParameters[2] = new DHParam(dh, 3073); + dh = DH_new(); + p = BN_get_rfc3526_prime_3072(MemorySegment.NULL); + g = BN_new(); + BN_set_word(g, 2); + DH_set0_pqg(dh, p, MemorySegment.NULL, g); + dhParameters[3] = new DHParam(dh, 2049); + dh = DH_new(); + p = BN_get_rfc3526_prime_2048(MemorySegment.NULL); + g = BN_new(); + BN_set_word(g, 2); + DH_set0_pqg(dh, p, MemorySegment.NULL, g); + dhParameters[4] = new DHParam(dh, 1025); + dh = DH_new(); + p = BN_get_rfc2409_prime_1024(MemorySegment.NULL); + g = BN_new(); + BN_set_word(g, 2); + DH_set0_pqg(dh, p, MemorySegment.NULL, g); + dhParameters[5] = new DHParam(dh, 0); + } + + private static void freeDHParameters() { + for (int i = 0; i < dhParameters.length; i++) { + if (dhParameters[i] != null) { + MemorySegment dh = dhParameters[i].dh; + if (dh != null && !MemorySegment.NULL.equals(dh)) { + DH_free(dh); + dhParameters[i] = null; + } + } + } + } + + public static void init() { + synchronized (lock) { + + if (OpenSSLStatus.isInitialized()) { + return; + } + OpenSSLStatus.setInitialized(true); + + if ("off".equalsIgnoreCase(SSLEngine)) { + return; + } + + try (var memorySession = Arena.ofConfined()) { + + // Main library init + initLibrary(); + + // OpenSSL 3 onwards uses providers + boolean isOpenSSL3 = (OpenSSL_version_num() >= 0x3000000fL); + + // Setup engine + String engineName = "on".equalsIgnoreCase(SSLEngine) ? null : SSLEngine; + if (!isOpenSSL3 && engineName != null) { + if ("auto".equals(engineName)) { + ENGINE_register_all_complete(); + } else { + var engine = memorySession.allocateFrom(engineName); + enginePointer = ENGINE_by_id(engine); + if (MemorySegment.NULL.equals(enginePointer)) { + enginePointer = ENGINE_by_id(memorySession.allocateFrom("dynamic")); + if (enginePointer != null) { + if (ENGINE_ctrl_cmd_string(enginePointer, memorySession.allocateFrom("SO_PATH"), engine, 0) == 0 + || ENGINE_ctrl_cmd_string(enginePointer, memorySession.allocateFrom("LOAD"), + MemorySegment.NULL, 0) == 0) { + // Engine load error + ENGINE_free(enginePointer); + enginePointer = MemorySegment.NULL; + } + } + } + if (!MemorySegment.NULL.equals(enginePointer)) { + if (ENGINE_set_default(enginePointer, ENGINE_METHOD_ALL()) == 0) { + // Engine load error + ENGINE_free(enginePointer); + enginePointer = MemorySegment.NULL; + } + } + if (MemorySegment.NULL.equals(enginePointer)) { + throw new IllegalStateException(sm.getString("openssllibrary.engineError")); + } + } + } + + // Set the random seed, translated to the Java way + boolean seedDone = false; + if (SSLRandomSeed != null || SSLRandomSeed.length() != 0 || !"builtin".equals(SSLRandomSeed)) { + var randomSeed = memorySession.allocateFrom(SSLRandomSeed); + seedDone = RAND_load_file(randomSeed, 128) > 0; + } + if (!seedDone) { + // Use a regular random to get some bytes + SecureRandom random = new SecureRandom(); + byte[] randomBytes = random.generateSeed(128); + RAND_seed(memorySession.allocateFrom(ValueLayout.JAVA_BYTE, randomBytes), 128); + } + + if (!isOpenSSL3) { + initDHParameters(); + } + + if (isOpenSSL3 || !(null == FIPSMode || "off".equalsIgnoreCase(FIPSMode))) { + fipsModeActive = false; + final boolean enterFipsMode; + int fipsModeState = FIPS_OFF; + if (isOpenSSL3) { + var md = EVP_MD_fetch(MemorySegment.NULL, memorySession.allocateFrom("SHA-512"), MemorySegment.NULL); + var provider = EVP_MD_get0_provider(md); + String name = OSSL_PROVIDER_get0_name(provider).getString(0); + EVP_MD_free(md); + if ("fips".equals(name)) { + fipsModeState = FIPS_ON; + } + } else { + fipsModeState = FIPS_mode(); + } + + if(log.isDebugEnabled()) { + log.debug(sm.getString("openssllibrary.currentFIPSMode", Integer.valueOf(fipsModeState))); + } + + if (null == FIPSMode || "off".equalsIgnoreCase(FIPSMode)) { + if (fipsModeState == FIPS_ON) { + fipsModeActive = true; + } + enterFipsMode = false; + } else if ("on".equalsIgnoreCase(FIPSMode)) { + if (fipsModeState == FIPS_ON) { + if (!isOpenSSL3) { + log.info(sm.getString("openssllibrary.skipFIPSInitialization")); + } + fipsModeActive = true; + enterFipsMode = false; + } else { + if (isOpenSSL3) { + throw new IllegalStateException(sm.getString("openssllibrary.FIPSProviderNotDefault", FIPSMode)); + } else { + enterFipsMode = true; + } + } + } else if ("require".equalsIgnoreCase(FIPSMode)) { + if (fipsModeState == FIPS_ON) { + fipsModeActive = true; + enterFipsMode = false; + } else { + if (isOpenSSL3) { + throw new IllegalStateException(sm.getString("openssllibrary.FIPSProviderNotDefault", FIPSMode)); + } else { + throw new IllegalStateException(sm.getString("openssllibrary.requireNotInFIPSMode")); + } + } + } else if ("enter".equalsIgnoreCase(FIPSMode)) { + if (fipsModeState == FIPS_OFF) { + if (isOpenSSL3) { + throw new IllegalStateException(sm.getString("openssllibrary.FIPSProviderNotDefault", FIPSMode)); + } else { + enterFipsMode = true; + } + } else { + if (isOpenSSL3) { + fipsModeActive = true; + enterFipsMode = false; + } else { + throw new IllegalStateException(sm.getString( + "openssllibrary.enterAlreadyInFIPSMode", Integer.valueOf(fipsModeState))); + } + } + } else { + throw new IllegalArgumentException(sm.getString( + "openssllibrary.wrongFIPSMode", FIPSMode)); + } + + if (enterFipsMode) { + log.info(sm.getString("openssllibrary.initializingFIPS")); + + fipsModeState = FIPS_mode_set(FIPS_ON); + if (fipsModeState != FIPS_ON) { + // This case should be handled by the native method, + // but we'll make absolutely sure, here. + String message = sm.getString("openssllibrary.initializeFIPSFailed"); + log.error(message); + throw new IllegalStateException(message); + } + + fipsModeActive = true; + log.info(sm.getString("openssllibrary.initializeFIPSSuccess")); + } + + if (isOpenSSL3 && fipsModeActive) { + log.info(sm.getString("aprListener.usingFIPSProvider")); + } + } + + log.info(sm.getString("openssllibrary.initializedOpenSSL", OpenSSL_version(0).getString(0))); + OpenSSLStatus.setAvailable(true); + } + } + } + + + public static void destroy() { + synchronized (lock) { + if (!OpenSSLStatus.isInitialized()) { + return; + } + OpenSSLStatus.setAvailable(false); + + try { + if (OpenSSL_version_num() < 0x3000000fL) { + freeDHParameters(); + if (!MemorySegment.NULL.equals(enginePointer)) { + ENGINE_free(enginePointer); + } + FIPS_mode_set(0); + } + } finally { + OpenSSLStatus.setInitialized(false); + fipsModeActive = false; + } + } + } + + public static String getSSLEngine() { + return SSLEngine; + } + + public static void setSSLEngine(String SSLEngine) { + if (!SSLEngine.equals(OpenSSLLibrary.SSLEngine)) { + // Ensure that the SSLEngine is consistent with that used for SSL init + if (OpenSSLStatus.isInitialized()) { + throw new IllegalStateException( + sm.getString("openssllibrary.tooLateForSSLEngine")); + } + + OpenSSLLibrary.SSLEngine = SSLEngine; + } + } + + public static String getSSLRandomSeed() { + return SSLRandomSeed; + } + + public static void setSSLRandomSeed(String SSLRandomSeed) { + if (!SSLRandomSeed.equals(OpenSSLLibrary.SSLRandomSeed)) { + // Ensure that the random seed is consistent with that used for SSL init + if (OpenSSLStatus.isInitialized()) { + throw new IllegalStateException( + sm.getString("openssllibrary.tooLateForSSLRandomSeed")); + } + + OpenSSLLibrary.SSLRandomSeed = SSLRandomSeed; + } + } + + public static String getFIPSMode() { + return FIPSMode; + } + + public static void setFIPSMode(String FIPSMode) { + if (!FIPSMode.equals(OpenSSLLibrary.FIPSMode)) { + // Ensure that the FIPS mode is consistent with that used for SSL init + if (OpenSSLStatus.isInitialized()) { + throw new IllegalStateException( + sm.getString("openssllibrary.tooLateForFIPSMode")); + } + + OpenSSLLibrary.FIPSMode = FIPSMode; + } + } + + public static boolean isFIPSModeActive() { + return fipsModeActive; + } + + public static List findCiphers(String ciphers) { + ArrayList ciphersList = new ArrayList<>(); + try (var localArena = Arena.ofConfined()) { + initLibrary(); + var sslCtx = SSL_CTX_new(TLS_server_method()); + try { + SSL_CTX_set_options(sslCtx, SSL_OP_ALL()); + SSL_CTX_set_cipher_list(sslCtx, localArena.allocateFrom(ciphers)); + var ssl = SSL_new(sslCtx); + SSL_set_accept_state(ssl); + try { + for (String c : getCiphers(ssl)) { + // Filter out bad input. + if (c == null || c.length() == 0 || ciphersList.contains(c)) { + continue; + } + ciphersList.add(OpenSSLCipherConfigurationParser.openSSLToJsse(c)); + } + } finally { + SSL_free(ssl); + } + } finally { + SSL_CTX_free(sslCtx); + } + } catch (Exception e) { + log.warn(sm.getString("openssllibrary.ciphersFailure"), e); + } + return ciphersList; + } + + static String[] getCiphers(MemorySegment ssl) { + MemorySegment sk = SSL_get_ciphers(ssl); + int len = OPENSSL_sk_num(sk); + if (len <= 0) { + return null; + } + ArrayList ciphers = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + MemorySegment cipher = OPENSSL_sk_value(sk, i); + MemorySegment cipherName = SSL_CIPHER_get_name(cipher); + ciphers.add(cipherName.getString(0)); + } + return ciphers.toArray(new String[0]); + } + +} diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLSessionContext.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLSessionContext.java new file mode 100644 index 0000000..82ece56 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLSessionContext.java @@ -0,0 +1,155 @@ +/* + * 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.tomcat.util.net.openssl.panama; + +import java.lang.foreign.Arena; +import java.lang.foreign.ValueLayout; +import java.util.Enumeration; +import java.util.NoSuchElementException; + +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; + +import static org.apache.tomcat.util.openssl.openssl_h.*; +import static org.apache.tomcat.util.openssl.openssl_h_Macros.*; +import org.apache.tomcat.util.res.StringManager; + +/** + * OpenSSL specific {@link SSLSessionContext} implementation. + */ +public class OpenSSLSessionContext implements SSLSessionContext { + private static final StringManager sm = StringManager.getManager(OpenSSLSessionContext.class); + private static final Enumeration EMPTY = new EmptyEnumeration(); + + private static final int TICKET_KEYS_SIZE = 48; + + private final OpenSSLSessionStats stats; + private final OpenSSLContext context; + + OpenSSLSessionContext(OpenSSLContext context) { + this.context = context; + stats = new OpenSSLSessionStats(context.getSSLContext()); + } + + @Override + public SSLSession getSession(byte[] bytes) { + return null; + } + + @Override + public Enumeration getIds() { + return EMPTY; + } + + /** + * Sets the SSL session ticket keys of this context. + * + * @param keys The session ticket keys + */ + public void setTicketKeys(byte[] keys) { + if (keys == null) { + throw new IllegalArgumentException(sm.getString("sessionContext.nullTicketKeys")); + } + if (keys.length != TICKET_KEYS_SIZE) { + throw new IllegalArgumentException(sm.getString("sessionContext.invalidTicketKeysLength", + Integer.valueOf(keys.length))); + } + try (var memorySession = Arena.ofConfined()) { + var array = memorySession.allocateFrom(ValueLayout.JAVA_BYTE, keys); + SSL_CTX_set_tlsext_ticket_keys(context.getSSLContext(), array, TICKET_KEYS_SIZE); + } + } + + /** + * Enable or disable caching of SSL sessions. + * + * @param enabled {@code true} to enable caching, {@code false} to disable + */ + public void setSessionCacheEnabled(boolean enabled) { + long mode = enabled ? SSL_SESS_CACHE_SERVER() : SSL_SESS_CACHE_OFF(); + SSL_CTX_set_session_cache_mode(context.getSSLContext(), mode); + } + + /** + * @return {@code true} if caching of SSL sessions is enabled, {@code false} + * otherwise. + */ + public boolean isSessionCacheEnabled() { + return SSL_CTX_get_session_cache_mode(context.getSSLContext()) == SSL_SESS_CACHE_SERVER(); + } + + /** + * @return The statistics for this context. + */ + public OpenSSLSessionStats stats() { + return stats; + } + + @Override + public void setSessionTimeout(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException(); + } + SSL_CTX_set_timeout(context.getSSLContext(), seconds); + } + + @Override + public int getSessionTimeout() { + return (int) SSL_CTX_get_timeout(context.getSSLContext()); + } + + @Override + public void setSessionCacheSize(int size) { + if (size < 0) { + throw new IllegalArgumentException(); + } + SSL_CTX_sess_set_cache_size(context.getSSLContext(), size); + } + + @Override + public int getSessionCacheSize() { + return (int) SSL_CTX_sess_get_cache_size(context.getSSLContext()); + } + + /** + * Set the context within which session be reused (server side only) + * See + * man SSL_CTX_set_session_id_context + * + * @param sidCtx can be any kind of binary data, it is therefore possible to use e.g. the name + * of the application and/or the hostname and/or service name + * @return {@code true} if success, {@code false} otherwise. + */ + public boolean setSessionIdContext(byte[] sidCtx) { + try (var memorySession = Arena.ofConfined()) { + var array = memorySession.allocateFrom(ValueLayout.JAVA_BYTE, sidCtx); + return (SSL_CTX_set_session_id_context(context.getSSLContext(), array, sidCtx.length) == 1); + } + } + + private static final class EmptyEnumeration implements Enumeration { + @Override + public boolean hasMoreElements() { + return false; + } + + @Override + public byte[] nextElement() { + throw new NoSuchElementException(); + } + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLSessionStats.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLSessionStats.java new file mode 100644 index 0000000..a63e524 --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLSessionStats.java @@ -0,0 +1,128 @@ +/* + * 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.tomcat.util.net.openssl.panama; + +import java.lang.foreign.MemorySegment; + +import static org.apache.tomcat.util.openssl.openssl_h.*; + +/** + * Stats exposed by an OpenSSL session context. + * + * @see SSL_CTX_sess_number + */ +public final class OpenSSLSessionStats { + + private final MemorySegment ctx; + + OpenSSLSessionStats(MemorySegment ctx) { + this.ctx = ctx; + } + + /** + * @return The current number of sessions in the internal session cache. + */ + public long number() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_NUMBER(), 0, null); + } + + /** + * @return The number of started SSL/TLS handshakes in client mode. + */ + public long connect() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_CONNECT() ,0, null); + } + + /** + * @return The number of successfully established SSL/TLS sessions in client mode. + */ + public long connectGood() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_CONNECT_GOOD() , 0, null); + } + + /** + * @return The number of start renegotiations in client mode. + */ + public long connectRenegotiate() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_CONNECT_RENEGOTIATE() , 0, null); + } + + /** + * @return The number of started SSL/TLS handshakes in server mode. + */ + public long accept() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_ACCEPT(), 0, null); + } + + /** + * @return The number of successfully established SSL/TLS sessions in server mode. + */ + public long acceptGood() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_ACCEPT_GOOD(), 0, null); + } + + /** + * @return The number of start renegotiations in server mode. + */ + public long acceptRenegotiate() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_ACCEPT_RENEGOTIATE(), 0, null); + } + + /** + * @return The number of successfully reused sessions. In client mode, a + * session set with {@code SSL_set_session} successfully reused is + * counted as a hit. In server mode, a session successfully + * retrieved from internal or external cache is counted as a hit. + */ + public long hits() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_HIT(), 0, null); + } + + /** + * @return The number of successfully retrieved sessions from the external + * session cache in server mode. + */ + public long cbHits() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_CB_HIT(), 0, null); + } + + /** + * @return The number of sessions proposed by clients that were not found in + * the internal session cache in server mode. + */ + public long misses() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_MISSES(), 0, null); + } + + /** + * @return The number of sessions proposed by clients and either found in + * the internal or external session cache in server mode, but that + * were invalid due to timeout. These sessions are not included in + * the {@link #hits()} count. + */ + public long timeouts() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_TIMEOUTS(), 0, null); + } + + /** + * @return The number of sessions that were removed because the maximum + * session cache size was exceeded. + */ + public long cacheFull() { + return SSL_CTX_ctrl(ctx, SSL_CTRL_SESS_CACHE_FULL(), 0, null); + } +} diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java new file mode 100644 index 0000000..6080dfd --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java @@ -0,0 +1,108 @@ +/* + * 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.tomcat.util.net.openssl.panama; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.KeyManager; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.SSLContext; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLUtilBase; +import org.apache.tomcat.util.res.StringManager; + +public class OpenSSLUtil extends SSLUtilBase { + + private static final Log log = LogFactory.getLog(OpenSSLUtil.class); + private static final StringManager sm = StringManager.getManager(OpenSSLUtil.class); + + + public OpenSSLUtil(SSLHostConfigCertificate certificate) { + super(certificate); + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + protected Set getImplementedProtocols() { + return OpenSSLEngine.IMPLEMENTED_PROTOCOLS_SET; + } + + + @Override + protected Set getImplementedCiphers() { + return OpenSSLEngine.AVAILABLE_CIPHER_SUITES; + } + + + @Override + protected boolean isTls13RenegAuthAvailable() { + // OpenSSL does support authentication after the initial handshake + return true; + } + + + @Override + public SSLContext createSSLContextInternal(List negotiableProtocols) throws Exception { + return new OpenSSLContext(certificate, negotiableProtocols); + } + + + @Override + public KeyManager[] getKeyManagers() throws Exception { + try { + return super.getKeyManagers(); + } catch (IllegalArgumentException e) { + // No (or invalid?) certificate chain was provided for the cert + String msg = sm.getString("openssl.nonJsseChain", certificate.getCertificateChainFile()); + if (log.isDebugEnabled()) { + log.info(msg, e); + } else { + log.info(msg); + } + return null; + } catch (KeyStoreException | IOException e) { + // Depending on what is presented, JSSE may also throw + // KeyStoreException or IOException if it doesn't understand the + // provided file. + if (certificate.getCertificateFile() != null) { + String msg = sm.getString("openssl.nonJsseCertificate", + certificate.getCertificateFile(), certificate.getCertificateKeyFile()); + if (log.isDebugEnabled()) { + log.info(msg, e); + } else { + log.info(msg); + } + // Assume JSSE processing of the certificate failed, try again with OpenSSL + // without a key manager + return null; + } + throw e; + } + } + +} diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLX509Certificate.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLX509Certificate.java new file mode 100644 index 0000000..fc52abe --- /dev/null +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLX509Certificate.java @@ -0,0 +1,192 @@ +/* + * 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.tomcat.util.net.openssl.panama; + +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Set; + +final class OpenSSLX509Certificate extends X509Certificate { + + private final byte[] bytes; + private X509Certificate wrapped; + + OpenSSLX509Certificate(byte[] bytes) { + this.bytes = bytes; + } + + @Override + public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException { + unwrap().checkValidity(); + } + + @Override + public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException { + unwrap().checkValidity(date); + } + + @Override + public int getVersion() { + return unwrap().getVersion(); + } + + @Override + public BigInteger getSerialNumber() { + return unwrap().getSerialNumber(); + } + + @Override + @Deprecated + public Principal getIssuerDN() { + return unwrap().getIssuerDN(); + } + + @Override + @Deprecated + public Principal getSubjectDN() { + return unwrap().getSubjectDN(); + } + + @Override + public Date getNotBefore() { + return unwrap().getNotBefore(); + } + + @Override + public Date getNotAfter() { + return unwrap().getNotAfter(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return unwrap().getTBSCertificate(); + } + + @Override + public byte[] getSignature() { + return unwrap().getSignature(); + } + + @Override + public String getSigAlgName() { + return unwrap().getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return unwrap().getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return unwrap().getSigAlgParams(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return unwrap().getIssuerUniqueID(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return unwrap().getSubjectUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return unwrap().getKeyUsage(); + } + + @Override + public int getBasicConstraints() { + return unwrap().getBasicConstraints(); + } + + @Override + public byte[] getEncoded() { + return bytes.clone(); + } + + @Override + public void verify(PublicKey key) + throws CertificateException, NoSuchAlgorithmException, + InvalidKeyException, NoSuchProviderException, SignatureException { + unwrap().verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, SignatureException { + unwrap().verify(key, sigProvider); + } + + @Override + public String toString() { + return unwrap().toString(); + } + + @Override + public PublicKey getPublicKey() { + return unwrap().getPublicKey(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return unwrap().hasUnsupportedCriticalExtension(); + } + + @Override + public Set getCriticalExtensionOIDs() { + return unwrap().getCriticalExtensionOIDs(); + } + + @Override + public Set getNonCriticalExtensionOIDs() { + return unwrap().getNonCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return unwrap().getExtensionValue(oid); + } + + private X509Certificate unwrap() { + X509Certificate wrapped = this.wrapped; + if (wrapped == null) { + try { + wrapped = this.wrapped = (X509Certificate) OpenSSLContext.X509_CERT_FACTORY.generateCertificate( + new ByteArrayInputStream(bytes)); + } catch (CertificateException e) { + throw new IllegalStateException(e); + } + } + return wrapped; + } +} diff --git a/java/org/apache/tomcat/util/openssl/SSL_CTX_set_alpn_select_cb$cb.java b/java/org/apache/tomcat/util/openssl/SSL_CTX_set_alpn_select_cb$cb.java new file mode 100644 index 0000000..b216da3 --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/SSL_CTX_set_alpn_select_cb$cb.java @@ -0,0 +1,70 @@ +/* + * 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. + */ + +// Generated by jextract + +package org.apache.tomcat.util.openssl; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; + +/** + * {@snippet lang=c : + * SSL_CTX_alpn_select_cb_func cb + * } + */ +@SuppressWarnings("javadoc") +public class SSL_CTX_set_alpn_select_cb$cb { + + public interface Function { + int apply(MemorySegment _x0, MemorySegment _x1, MemorySegment _x2, MemorySegment _x3, int _x4, MemorySegment _x5); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = openssl_h.upcallHandle(SSL_CTX_set_alpn_select_cb$cb.Function.class, "apply", $DESC); + + public static MemorySegment allocate(SSL_CTX_set_alpn_select_cb$cb.Function fi, Arena scope) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, scope); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + public static int invoke(MemorySegment funcPtr,MemorySegment _x0, MemorySegment _x1, MemorySegment _x2, MemorySegment _x3, int _x4, MemorySegment _x5) { + try { + return (int) DOWN$MH.invokeExact(funcPtr, _x0, _x1, _x2, _x3, _x4, _x5); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} + diff --git a/java/org/apache/tomcat/util/openssl/SSL_CTX_set_cert_verify_callback$cb.java b/java/org/apache/tomcat/util/openssl/SSL_CTX_set_cert_verify_callback$cb.java new file mode 100644 index 0000000..dbd1cd7 --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/SSL_CTX_set_cert_verify_callback$cb.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +// Generated by jextract + +package org.apache.tomcat.util.openssl; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; + +/** + * {@snippet lang=c : + * int (*cb)(X509_STORE_CTX *, void *) + * } + */ +@SuppressWarnings("javadoc") +public class SSL_CTX_set_cert_verify_callback$cb { + + public interface Function { + int apply(MemorySegment _x0, MemorySegment _x1); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = openssl_h.upcallHandle(SSL_CTX_set_cert_verify_callback$cb.Function.class, "apply", $DESC); + + public static MemorySegment allocate(SSL_CTX_set_cert_verify_callback$cb.Function fi, Arena scope) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, scope); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + public static int invoke(MemorySegment funcPtr,MemorySegment _x0, MemorySegment _x1) { + try { + return (int) DOWN$MH.invokeExact(funcPtr, _x0, _x1); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} + diff --git a/java/org/apache/tomcat/util/openssl/SSL_CTX_set_tmp_dh_callback$dh.java b/java/org/apache/tomcat/util/openssl/SSL_CTX_set_tmp_dh_callback$dh.java new file mode 100644 index 0000000..ed4c821 --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/SSL_CTX_set_tmp_dh_callback$dh.java @@ -0,0 +1,67 @@ +/* + * 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. + */ + +// Generated by jextract + +package org.apache.tomcat.util.openssl; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; + +/** + * {@snippet lang=c : + * DH *(*dh)(SSL *, int, int) + * } + */ +@SuppressWarnings("javadoc") +public class SSL_CTX_set_tmp_dh_callback$dh { + + public interface Function { + MemorySegment apply(MemorySegment _x0, int _x1, int _x2); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_INT + ); + + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = openssl_h.upcallHandle(SSL_CTX_set_tmp_dh_callback$dh.Function.class, "apply", $DESC); + + public static MemorySegment allocate(SSL_CTX_set_tmp_dh_callback$dh.Function fi, Arena scope) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, scope); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + public static MemorySegment invoke(MemorySegment funcPtr,MemorySegment _x0, int _x1, int _x2) { + try { + return (MemorySegment) DOWN$MH.invokeExact(funcPtr, _x0, _x1, _x2); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} + diff --git a/java/org/apache/tomcat/util/openssl/SSL_CTX_set_verify$callback.java b/java/org/apache/tomcat/util/openssl/SSL_CTX_set_verify$callback.java new file mode 100644 index 0000000..bf966a2 --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/SSL_CTX_set_verify$callback.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +// Generated by jextract + +package org.apache.tomcat.util.openssl; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; + +/** + * {@snippet lang=c : + * SSL_verify_cb callback + * } + */ +@SuppressWarnings("javadoc") +public class SSL_CTX_set_verify$callback { + + public interface Function { + int apply(int _x0, MemorySegment _x1); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = openssl_h.upcallHandle(SSL_CTX_set_verify$callback.Function.class, "apply", $DESC); + + public static MemorySegment allocate(SSL_CTX_set_verify$callback.Function fi, Arena scope) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, scope); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + public static int invoke(MemorySegment funcPtr,int _x0, MemorySegment _x1) { + try { + return (int) DOWN$MH.invokeExact(funcPtr, _x0, _x1); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} + diff --git a/java/org/apache/tomcat/util/openssl/SSL_set_info_callback$cb.java b/java/org/apache/tomcat/util/openssl/SSL_set_info_callback$cb.java new file mode 100644 index 0000000..226d680 --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/SSL_set_info_callback$cb.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +// Generated by jextract + +package org.apache.tomcat.util.openssl; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; + +/** + * {@snippet lang=c : + * void (*cb)(const SSL *, int, int) + * } + */ +@SuppressWarnings("javadoc") +public class SSL_set_info_callback$cb { + + public interface Function { + void apply(MemorySegment _x0, int _x1, int _x2); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_INT + ); + + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = openssl_h.upcallHandle(SSL_set_info_callback$cb.Function.class, "apply", $DESC); + + public static MemorySegment allocate(SSL_set_info_callback$cb.Function fi, Arena scope) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, scope); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + public static void invoke(MemorySegment funcPtr,MemorySegment _x0, int _x1, int _x2) { + try { + DOWN$MH.invokeExact(funcPtr, _x0, _x1, _x2); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} + diff --git a/java/org/apache/tomcat/util/openssl/SSL_set_verify$callback.java b/java/org/apache/tomcat/util/openssl/SSL_set_verify$callback.java new file mode 100644 index 0000000..5c11c2f --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/SSL_set_verify$callback.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +// Generated by jextract + +package org.apache.tomcat.util.openssl; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; + +/** + * {@snippet lang=c : + * SSL_verify_cb callback + * } + */ +@SuppressWarnings("javadoc") +public class SSL_set_verify$callback { + + public interface Function { + int apply(int _x0, MemorySegment _x1); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = openssl_h.upcallHandle(SSL_set_verify$callback.Function.class, "apply", $DESC); + + public static MemorySegment allocate(SSL_set_verify$callback.Function fi, Arena scope) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, scope); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + public static int invoke(MemorySegment funcPtr,int _x0, MemorySegment _x1) { + try { + return (int) DOWN$MH.invokeExact(funcPtr, _x0, _x1); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} + diff --git a/java/org/apache/tomcat/util/openssl/openssl_h.java b/java/org/apache/tomcat/util/openssl/openssl_h.java new file mode 100644 index 0000000..9d9d701 --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/openssl_h.java @@ -0,0 +1,6404 @@ +/* + * 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. + */ + +// Generated by jextract + +package org.apache.tomcat.util.openssl; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Locale; +import java.util.stream.Collectors; +import java.lang.foreign.*; +import static java.lang.foreign.ValueLayout.*; + +@SuppressWarnings({"javadoc", "boxing"}) +public class openssl_h { + + openssl_h() { + // Suppresses public default constructor, ensuring non-instantiability, + // but allows generated subclasses in same package. + } + + public static final ValueLayout.OfBoolean C_BOOL = ValueLayout.JAVA_BOOLEAN; + public static final ValueLayout.OfByte C_CHAR = ValueLayout.JAVA_BYTE; + public static final ValueLayout.OfShort C_SHORT = ValueLayout.JAVA_SHORT; + public static final ValueLayout.OfInt C_INT = ValueLayout.JAVA_INT; + public static final ValueLayout.OfLong C_LONG_LONG = ValueLayout.JAVA_LONG; + public static final ValueLayout.OfFloat C_FLOAT = ValueLayout.JAVA_FLOAT; + public static final ValueLayout.OfDouble C_DOUBLE = ValueLayout.JAVA_DOUBLE; + public static final AddressLayout C_POINTER = ValueLayout.ADDRESS + .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, JAVA_BYTE)); + public static final ValueLayout.OfLong C_LONG = ValueLayout.JAVA_LONG; + + static final Arena LIBRARY_ARENA = Arena.ofAuto(); + static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls"); + static final SymbolLookup SYMBOL_LOOKUP; + static { + String os = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); + // Note: Library loading is not portable for MacOS https://github.com/sergot/openssl/issues/81 + if (os.indexOf("mac") >= 0) { + System.loadLibrary("ssl"); + SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup()); + } else { + SYMBOL_LOOKUP = SymbolLookup.libraryLookup(System.mapLibraryName("ssl"), LIBRARY_ARENA) + .or(SymbolLookup.loaderLookup()) + .or(Linker.nativeLinker().defaultLookup()); + } + } + + static void traceDowncall(String name, Object... args) { + String traceArgs = Arrays.stream(args) + .map(Object::toString) + .collect(Collectors.joining(", ")); + System.out.printf("%s(%s)\n", name, traceArgs); + } + + static MemorySegment findOrThrow(String symbol) { + return SYMBOL_LOOKUP.find(symbol) + .orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol: " + symbol)); + } + + static MemoryLayout[] inferVariadicLayouts(Object[] varargs) { + MemoryLayout[] result = new MemoryLayout[varargs.length]; + for (int i = 0; i < varargs.length; i++) { + result[i] = variadicLayout(varargs[i].getClass()); + } + return result; + } + + static MethodHandle upcallHandle(Class fi, String name, FunctionDescriptor fdesc) { + try { + return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType()); + } catch (ReflectiveOperationException ex) { + throw new AssertionError(ex); + } + } + + static MethodHandle downcallHandleVariadic(String name, FunctionDescriptor baseDesc, MemoryLayout[] variadicLayouts) { + FunctionDescriptor variadicDesc = baseDesc.appendArgumentLayouts(variadicLayouts); + Linker.Option fva = Linker.Option.firstVariadicArg(baseDesc.argumentLayouts().size()); + return SYMBOL_LOOKUP.find(name) + .map(addr -> Linker.nativeLinker().downcallHandle(addr, variadicDesc, fva) + .asSpreader(Object[].class, variadicLayouts.length)) + .orElse(null); + } + + // Internals only below this point + + private static MemoryLayout variadicLayout(Class c) { + // apply default argument promotions per C spec + // note that all primitives are boxed, since they are passed through an Object[] + if (c == Boolean.class || c == Byte.class || c == Character.class || c == Short.class || c == Integer.class) { + return JAVA_INT; + } else if (c == Long.class) { + return JAVA_LONG; + } else if (c == Float.class || c == Double.class) { + return JAVA_DOUBLE; + } else if (MemorySegment.class.isAssignableFrom(c)) { + return ADDRESS; + } + throw new IllegalArgumentException("Invalid type for ABI: " + c.getTypeName()); + } + private static final int BIO_CLOSE = (int)1L; + /** + * {@snippet lang=c : + * #define BIO_CLOSE 1 + * } + */ + public static int BIO_CLOSE() { + return BIO_CLOSE; + } + private static final int BIO_CTRL_RESET = (int)1L; + /** + * {@snippet lang=c : + * #define BIO_CTRL_RESET 1 + * } + */ + public static int BIO_CTRL_RESET() { + return BIO_CTRL_RESET; + } + private static final int BIO_FP_READ = (int)2L; + /** + * {@snippet lang=c : + * #define BIO_FP_READ 2 + * } + */ + public static int BIO_FP_READ() { + return BIO_FP_READ; + } + private static final int BIO_C_SET_FILENAME = (int)108L; + /** + * {@snippet lang=c : + * #define BIO_C_SET_FILENAME 108 + * } + */ + public static int BIO_C_SET_FILENAME() { + return BIO_C_SET_FILENAME; + } + private static final int NID_info_access = (int)177L; + /** + * {@snippet lang=c : + * #define NID_info_access 177 + * } + */ + public static int NID_info_access() { + return NID_info_access; + } + private static final int X509_FILETYPE_PEM = (int)1L; + /** + * {@snippet lang=c : + * #define X509_FILETYPE_PEM 1 + * } + */ + public static int X509_FILETYPE_PEM() { + return X509_FILETYPE_PEM; + } + private static final int X509_L_FILE_LOAD = (int)1L; + /** + * {@snippet lang=c : + * #define X509_L_FILE_LOAD 1 + * } + */ + public static int X509_L_FILE_LOAD() { + return X509_L_FILE_LOAD; + } + private static final int X509_L_ADD_DIR = (int)2L; + /** + * {@snippet lang=c : + * #define X509_L_ADD_DIR 2 + * } + */ + public static int X509_L_ADD_DIR() { + return X509_L_ADD_DIR; + } + private static final int X509_V_OK = (int)0L; + /** + * {@snippet lang=c : + * #define X509_V_OK 0 + * } + */ + public static int X509_V_OK() { + return X509_V_OK; + } + private static final int X509_V_ERR_CRL_HAS_EXPIRED = (int)12L; + /** + * {@snippet lang=c : + * #define X509_V_ERR_CRL_HAS_EXPIRED 12 + * } + */ + public static int X509_V_ERR_CRL_HAS_EXPIRED() { + return X509_V_ERR_CRL_HAS_EXPIRED; + } + private static final int X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT = (int)18L; + /** + * {@snippet lang=c : + * #define X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT 18 + * } + */ + public static int X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT() { + return X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT; + } + private static final int X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN = (int)19L; + /** + * {@snippet lang=c : + * #define X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN 19 + * } + */ + public static int X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN() { + return X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN; + } + private static final int X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY = (int)20L; + /** + * {@snippet lang=c : + * #define X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY 20 + * } + */ + public static int X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY() { + return X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY; + } + private static final int X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE = (int)21L; + /** + * {@snippet lang=c : + * #define X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE 21 + * } + */ + public static int X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE() { + return X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE; + } + private static final int X509_V_ERR_CERT_UNTRUSTED = (int)27L; + /** + * {@snippet lang=c : + * #define X509_V_ERR_CERT_UNTRUSTED 27 + * } + */ + public static int X509_V_ERR_CERT_UNTRUSTED() { + return X509_V_ERR_CERT_UNTRUSTED; + } + private static final int X509_V_ERR_APPLICATION_VERIFICATION = (int)50L; + /** + * {@snippet lang=c : + * #define X509_V_ERR_APPLICATION_VERIFICATION 50 + * } + */ + public static int X509_V_ERR_APPLICATION_VERIFICATION() { + return X509_V_ERR_APPLICATION_VERIFICATION; + } + private static final int X509_V_FLAG_CRL_CHECK = (int)4L; + /** + * {@snippet lang=c : + * #define X509_V_FLAG_CRL_CHECK 4 + * } + */ + public static int X509_V_FLAG_CRL_CHECK() { + return X509_V_FLAG_CRL_CHECK; + } + private static final int X509_V_FLAG_CRL_CHECK_ALL = (int)8L; + /** + * {@snippet lang=c : + * #define X509_V_FLAG_CRL_CHECK_ALL 8 + * } + */ + public static int X509_V_FLAG_CRL_CHECK_ALL() { + return X509_V_FLAG_CRL_CHECK_ALL; + } + private static final int PEM_R_NO_START_LINE = (int)108L; + /** + * {@snippet lang=c : + * #define PEM_R_NO_START_LINE 108 + * } + */ + public static int PEM_R_NO_START_LINE() { + return PEM_R_NO_START_LINE; + } + private static final int SSL3_VERSION = (int)768L; + /** + * {@snippet lang=c : + * #define SSL3_VERSION 768 + * } + */ + public static int SSL3_VERSION() { + return SSL3_VERSION; + } + private static final int TLS1_VERSION = (int)769L; + /** + * {@snippet lang=c : + * #define TLS1_VERSION 769 + * } + */ + public static int TLS1_VERSION() { + return TLS1_VERSION; + } + private static final int TLS1_1_VERSION = (int)770L; + /** + * {@snippet lang=c : + * #define TLS1_1_VERSION 770 + * } + */ + public static int TLS1_1_VERSION() { + return TLS1_1_VERSION; + } + private static final int TLS1_2_VERSION = (int)771L; + /** + * {@snippet lang=c : + * #define TLS1_2_VERSION 771 + * } + */ + public static int TLS1_2_VERSION() { + return TLS1_2_VERSION; + } + private static final int TLS1_3_VERSION = (int)772L; + /** + * {@snippet lang=c : + * #define TLS1_3_VERSION 772 + * } + */ + public static int TLS1_3_VERSION() { + return TLS1_3_VERSION; + } + private static final int SSL_SENT_SHUTDOWN = (int)1L; + /** + * {@snippet lang=c : + * #define SSL_SENT_SHUTDOWN 1 + * } + */ + public static int SSL_SENT_SHUTDOWN() { + return SSL_SENT_SHUTDOWN; + } + private static final int SSL_RECEIVED_SHUTDOWN = (int)2L; + /** + * {@snippet lang=c : + * #define SSL_RECEIVED_SHUTDOWN 2 + * } + */ + public static int SSL_RECEIVED_SHUTDOWN() { + return SSL_RECEIVED_SHUTDOWN; + } + private static final int SSL_OP_SINGLE_ECDH_USE = (int)0L; + /** + * {@snippet lang=c : + * #define SSL_OP_SINGLE_ECDH_USE 0 + * } + */ + public static int SSL_OP_SINGLE_ECDH_USE() { + return SSL_OP_SINGLE_ECDH_USE; + } + private static final int SSL_OP_SINGLE_DH_USE = (int)0L; + /** + * {@snippet lang=c : + * #define SSL_OP_SINGLE_DH_USE 0 + * } + */ + public static int SSL_OP_SINGLE_DH_USE() { + return SSL_OP_SINGLE_DH_USE; + } + private static final int SSL_OP_NO_SSLv2 = (int)0L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_SSLv2 0 + * } + */ + public static int SSL_OP_NO_SSLv2() { + return SSL_OP_NO_SSLv2; + } + private static final int SSL_CONF_FLAG_FILE = (int)2L; + /** + * {@snippet lang=c : + * #define SSL_CONF_FLAG_FILE 2 + * } + */ + public static int SSL_CONF_FLAG_FILE() { + return SSL_CONF_FLAG_FILE; + } + private static final int SSL_CONF_FLAG_SERVER = (int)8L; + /** + * {@snippet lang=c : + * #define SSL_CONF_FLAG_SERVER 8 + * } + */ + public static int SSL_CONF_FLAG_SERVER() { + return SSL_CONF_FLAG_SERVER; + } + private static final int SSL_CONF_FLAG_SHOW_ERRORS = (int)16L; + /** + * {@snippet lang=c : + * #define SSL_CONF_FLAG_SHOW_ERRORS 16 + * } + */ + public static int SSL_CONF_FLAG_SHOW_ERRORS() { + return SSL_CONF_FLAG_SHOW_ERRORS; + } + private static final int SSL_CONF_FLAG_CERTIFICATE = (int)32L; + /** + * {@snippet lang=c : + * #define SSL_CONF_FLAG_CERTIFICATE 32 + * } + */ + public static int SSL_CONF_FLAG_CERTIFICATE() { + return SSL_CONF_FLAG_CERTIFICATE; + } + private static final int SSL_CONF_TYPE_UNKNOWN = (int)0L; + /** + * {@snippet lang=c : + * #define SSL_CONF_TYPE_UNKNOWN 0 + * } + */ + public static int SSL_CONF_TYPE_UNKNOWN() { + return SSL_CONF_TYPE_UNKNOWN; + } + private static final int SSL_CONF_TYPE_FILE = (int)2L; + /** + * {@snippet lang=c : + * #define SSL_CONF_TYPE_FILE 2 + * } + */ + public static int SSL_CONF_TYPE_FILE() { + return SSL_CONF_TYPE_FILE; + } + private static final int SSL_CONF_TYPE_DIR = (int)3L; + /** + * {@snippet lang=c : + * #define SSL_CONF_TYPE_DIR 3 + * } + */ + public static int SSL_CONF_TYPE_DIR() { + return SSL_CONF_TYPE_DIR; + } + private static final int SSL_SESS_CACHE_OFF = (int)0L; + /** + * {@snippet lang=c : + * #define SSL_SESS_CACHE_OFF 0 + * } + */ + public static int SSL_SESS_CACHE_OFF() { + return SSL_SESS_CACHE_OFF; + } + private static final int SSL_SESS_CACHE_SERVER = (int)2L; + /** + * {@snippet lang=c : + * #define SSL_SESS_CACHE_SERVER 2 + * } + */ + public static int SSL_SESS_CACHE_SERVER() { + return SSL_SESS_CACHE_SERVER; + } + private static final int SSL2_VERSION = (int)2L; + /** + * {@snippet lang=c : + * #define SSL2_VERSION 2 + * } + */ + public static int SSL2_VERSION() { + return SSL2_VERSION; + } + private static final int SSL_TLSEXT_ERR_OK = (int)0L; + /** + * {@snippet lang=c : + * #define SSL_TLSEXT_ERR_OK 0 + * } + */ + public static int SSL_TLSEXT_ERR_OK() { + return SSL_TLSEXT_ERR_OK; + } + private static final int SSL_TLSEXT_ERR_NOACK = (int)3L; + /** + * {@snippet lang=c : + * #define SSL_TLSEXT_ERR_NOACK 3 + * } + */ + public static int SSL_TLSEXT_ERR_NOACK() { + return SSL_TLSEXT_ERR_NOACK; + } + private static final int SSL_CB_HANDSHAKE_DONE = (int)32L; + /** + * {@snippet lang=c : + * #define SSL_CB_HANDSHAKE_DONE 32 + * } + */ + public static int SSL_CB_HANDSHAKE_DONE() { + return SSL_CB_HANDSHAKE_DONE; + } + private static final int SSL_VERIFY_NONE = (int)0L; + /** + * {@snippet lang=c : + * #define SSL_VERIFY_NONE 0 + * } + */ + public static int SSL_VERIFY_NONE() { + return SSL_VERIFY_NONE; + } + private static final int SSL_VERIFY_PEER = (int)1L; + /** + * {@snippet lang=c : + * #define SSL_VERIFY_PEER 1 + * } + */ + public static int SSL_VERIFY_PEER() { + return SSL_VERIFY_PEER; + } + private static final int SSL_VERIFY_FAIL_IF_NO_PEER_CERT = (int)2L; + /** + * {@snippet lang=c : + * #define SSL_VERIFY_FAIL_IF_NO_PEER_CERT 2 + * } + */ + public static int SSL_VERIFY_FAIL_IF_NO_PEER_CERT() { + return SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + } + private static final int SSL_ERROR_NONE = (int)0L; + /** + * {@snippet lang=c : + * #define SSL_ERROR_NONE 0 + * } + */ + public static int SSL_ERROR_NONE() { + return SSL_ERROR_NONE; + } + private static final int SSL_CTRL_SET_TMP_DH = (int)3L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_TMP_DH 3 + * } + */ + public static int SSL_CTRL_SET_TMP_DH() { + return SSL_CTRL_SET_TMP_DH; + } + private static final int SSL_CTRL_SET_TMP_ECDH = (int)4L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_TMP_ECDH 4 + * } + */ + public static int SSL_CTRL_SET_TMP_ECDH() { + return SSL_CTRL_SET_TMP_ECDH; + } + private static final int SSL_CTRL_SESS_NUMBER = (int)20L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_NUMBER 20 + * } + */ + public static int SSL_CTRL_SESS_NUMBER() { + return SSL_CTRL_SESS_NUMBER; + } + private static final int SSL_CTRL_SESS_CONNECT = (int)21L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_CONNECT 21 + * } + */ + public static int SSL_CTRL_SESS_CONNECT() { + return SSL_CTRL_SESS_CONNECT; + } + private static final int SSL_CTRL_SESS_CONNECT_GOOD = (int)22L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_CONNECT_GOOD 22 + * } + */ + public static int SSL_CTRL_SESS_CONNECT_GOOD() { + return SSL_CTRL_SESS_CONNECT_GOOD; + } + private static final int SSL_CTRL_SESS_CONNECT_RENEGOTIATE = (int)23L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_CONNECT_RENEGOTIATE 23 + * } + */ + public static int SSL_CTRL_SESS_CONNECT_RENEGOTIATE() { + return SSL_CTRL_SESS_CONNECT_RENEGOTIATE; + } + private static final int SSL_CTRL_SESS_ACCEPT = (int)24L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_ACCEPT 24 + * } + */ + public static int SSL_CTRL_SESS_ACCEPT() { + return SSL_CTRL_SESS_ACCEPT; + } + private static final int SSL_CTRL_SESS_ACCEPT_GOOD = (int)25L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_ACCEPT_GOOD 25 + * } + */ + public static int SSL_CTRL_SESS_ACCEPT_GOOD() { + return SSL_CTRL_SESS_ACCEPT_GOOD; + } + private static final int SSL_CTRL_SESS_ACCEPT_RENEGOTIATE = (int)26L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_ACCEPT_RENEGOTIATE 26 + * } + */ + public static int SSL_CTRL_SESS_ACCEPT_RENEGOTIATE() { + return SSL_CTRL_SESS_ACCEPT_RENEGOTIATE; + } + private static final int SSL_CTRL_SESS_HIT = (int)27L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_HIT 27 + * } + */ + public static int SSL_CTRL_SESS_HIT() { + return SSL_CTRL_SESS_HIT; + } + private static final int SSL_CTRL_SESS_CB_HIT = (int)28L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_CB_HIT 28 + * } + */ + public static int SSL_CTRL_SESS_CB_HIT() { + return SSL_CTRL_SESS_CB_HIT; + } + private static final int SSL_CTRL_SESS_MISSES = (int)29L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_MISSES 29 + * } + */ + public static int SSL_CTRL_SESS_MISSES() { + return SSL_CTRL_SESS_MISSES; + } + private static final int SSL_CTRL_SESS_TIMEOUTS = (int)30L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_TIMEOUTS 30 + * } + */ + public static int SSL_CTRL_SESS_TIMEOUTS() { + return SSL_CTRL_SESS_TIMEOUTS; + } + private static final int SSL_CTRL_SESS_CACHE_FULL = (int)31L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SESS_CACHE_FULL 31 + * } + */ + public static int SSL_CTRL_SESS_CACHE_FULL() { + return SSL_CTRL_SESS_CACHE_FULL; + } + private static final int SSL_CTRL_SET_SESS_CACHE_SIZE = (int)42L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_SESS_CACHE_SIZE 42 + * } + */ + public static int SSL_CTRL_SET_SESS_CACHE_SIZE() { + return SSL_CTRL_SET_SESS_CACHE_SIZE; + } + private static final int SSL_CTRL_GET_SESS_CACHE_SIZE = (int)43L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_GET_SESS_CACHE_SIZE 43 + * } + */ + public static int SSL_CTRL_GET_SESS_CACHE_SIZE() { + return SSL_CTRL_GET_SESS_CACHE_SIZE; + } + private static final int SSL_CTRL_SET_SESS_CACHE_MODE = (int)44L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_SESS_CACHE_MODE 44 + * } + */ + public static int SSL_CTRL_SET_SESS_CACHE_MODE() { + return SSL_CTRL_SET_SESS_CACHE_MODE; + } + private static final int SSL_CTRL_GET_SESS_CACHE_MODE = (int)45L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_GET_SESS_CACHE_MODE 45 + * } + */ + public static int SSL_CTRL_GET_SESS_CACHE_MODE() { + return SSL_CTRL_GET_SESS_CACHE_MODE; + } + private static final int SSL_CTRL_SET_TLSEXT_TICKET_KEYS = (int)59L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_TLSEXT_TICKET_KEYS 59 + * } + */ + public static int SSL_CTRL_SET_TLSEXT_TICKET_KEYS() { + return SSL_CTRL_SET_TLSEXT_TICKET_KEYS; + } + private static final int SSL_CTRL_CHAIN_CERT = (int)89L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_CHAIN_CERT 89 + * } + */ + public static int SSL_CTRL_CHAIN_CERT() { + return SSL_CTRL_CHAIN_CERT; + } + private static final int SSL_CTRL_SET_GROUPS = (int)91L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_GROUPS 91 + * } + */ + public static int SSL_CTRL_SET_GROUPS() { + return SSL_CTRL_SET_GROUPS; + } + private static final int SSL_CTRL_SET_DH_AUTO = (int)118L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_DH_AUTO 118 + * } + */ + public static int SSL_CTRL_SET_DH_AUTO() { + return SSL_CTRL_SET_DH_AUTO; + } + private static final int SSL_CTRL_SET_MIN_PROTO_VERSION = (int)123L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_MIN_PROTO_VERSION 123 + * } + */ + public static int SSL_CTRL_SET_MIN_PROTO_VERSION() { + return SSL_CTRL_SET_MIN_PROTO_VERSION; + } + private static final int SSL_CTRL_SET_MAX_PROTO_VERSION = (int)124L; + /** + * {@snippet lang=c : + * #define SSL_CTRL_SET_MAX_PROTO_VERSION 124 + * } + */ + public static int SSL_CTRL_SET_MAX_PROTO_VERSION() { + return SSL_CTRL_SET_MAX_PROTO_VERSION; + } + private static final int ERR_REASON_MASK = (int)8388607L; + /** + * {@snippet lang=c : + * #define ERR_REASON_MASK 8388607 + * } + */ + public static int ERR_REASON_MASK() { + return ERR_REASON_MASK; + } + private static final int OCSP_RESPONSE_STATUS_SUCCESSFUL = (int)0L; + /** + * {@snippet lang=c : + * #define OCSP_RESPONSE_STATUS_SUCCESSFUL 0 + * } + */ + public static int OCSP_RESPONSE_STATUS_SUCCESSFUL() { + return OCSP_RESPONSE_STATUS_SUCCESSFUL; + } + private static final int V_OCSP_CERTSTATUS_GOOD = (int)0L; + /** + * {@snippet lang=c : + * #define V_OCSP_CERTSTATUS_GOOD 0 + * } + */ + public static int V_OCSP_CERTSTATUS_GOOD() { + return V_OCSP_CERTSTATUS_GOOD; + } + private static final int V_OCSP_CERTSTATUS_REVOKED = (int)1L; + /** + * {@snippet lang=c : + * #define V_OCSP_CERTSTATUS_REVOKED 1 + * } + */ + public static int V_OCSP_CERTSTATUS_REVOKED() { + return V_OCSP_CERTSTATUS_REVOKED; + } + private static final int V_OCSP_CERTSTATUS_UNKNOWN = (int)2L; + /** + * {@snippet lang=c : + * #define V_OCSP_CERTSTATUS_UNKNOWN 2 + * } + */ + public static int V_OCSP_CERTSTATUS_UNKNOWN() { + return V_OCSP_CERTSTATUS_UNKNOWN; + } + + private static MethodHandle OPENSSL_sk_num$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OPENSSL_sk_num"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int OPENSSL_sk_num(const OPENSSL_STACK *) + * } + */ + public static int OPENSSL_sk_num(MemorySegment x0) { + var mh$ = OPENSSL_sk_num$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OPENSSL_sk_num", x0); + } + return (int) mh$.invokeExact(x0); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OPENSSL_sk_value$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OPENSSL_sk_value"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void *OPENSSL_sk_value(const OPENSSL_STACK *, int) + * } + */ + public static MemorySegment OPENSSL_sk_value(MemorySegment x0, int x1) { + var mh$ = OPENSSL_sk_value$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OPENSSL_sk_value", x0, x1); + } + return (MemorySegment) mh$.invokeExact(x0, x1); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OpenSSL_version_num$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OpenSSL_version_num"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * unsigned long OpenSSL_version_num(void) + * } + */ + public static long OpenSSL_version_num() { + var mh$ = OpenSSL_version_num$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OpenSSL_version_num"); + } + return (long) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OpenSSL_version$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OpenSSL_version"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const char *OpenSSL_version(int type) + * } + */ + public static MemorySegment OpenSSL_version(int type) { + var mh$ = OpenSSL_version$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OpenSSL_version", type); + } + return (MemorySegment) mh$.invokeExact(type); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle CRYPTO_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("CRYPTO_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void CRYPTO_free(void *ptr, const char *file, int line) + * } + */ + public static void CRYPTO_free(MemorySegment ptr, MemorySegment file, int line) { + var mh$ = CRYPTO_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("CRYPTO_free", ptr, file, line); + } + mh$.invokeExact(ptr, file, line); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_ctrl_pending$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_ctrl_pending"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * size_t BIO_ctrl_pending(BIO *b) + * } + */ + public static long BIO_ctrl_pending(MemorySegment b) { + var mh$ = BIO_ctrl_pending$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_ctrl_pending", b); + } + return (long) mh$.invokeExact(b); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_s_file$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_s_file"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const BIO_METHOD *BIO_s_file(void) + * } + */ + public static MemorySegment BIO_s_file() { + var mh$ = BIO_s_file$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_s_file"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_new_file$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_new_file"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIO *BIO_new_file(const char *filename, const char *mode) + * } + */ + public static MemorySegment BIO_new_file(MemorySegment filename, MemorySegment mode) { + var mh$ = BIO_new_file$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_new_file", filename, mode); + } + return (MemorySegment) mh$.invokeExact(filename, mode); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_new$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_new"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIO *BIO_new(const BIO_METHOD *type) + * } + */ + public static MemorySegment BIO_new(MemorySegment type) { + var mh$ = BIO_new$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_new", type); + } + return (MemorySegment) mh$.invokeExact(type); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int BIO_free(BIO *a) + * } + */ + public static int BIO_free(MemorySegment a) { + var mh$ = BIO_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_free", a); + } + return (int) mh$.invokeExact(a); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_read$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_read"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int BIO_read(BIO *b, void *data, int dlen) + * } + */ + public static int BIO_read(MemorySegment b, MemorySegment data, int dlen) { + var mh$ = BIO_read$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_read", b, data, dlen); + } + return (int) mh$.invokeExact(b, data, dlen); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_write$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_write"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int BIO_write(BIO *b, const void *data, int dlen) + * } + */ + public static int BIO_write(MemorySegment b, MemorySegment data, int dlen) { + var mh$ = BIO_write$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_write", b, data, dlen); + } + return (int) mh$.invokeExact(b, data, dlen); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_ctrl$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_ctrl"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * long BIO_ctrl(BIO *bp, int cmd, long larg, void *parg) + * } + */ + public static long BIO_ctrl(MemorySegment bp, int cmd, long larg, MemorySegment parg) { + var mh$ = BIO_ctrl$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_ctrl", bp, cmd, larg, parg); + } + return (long) mh$.invokeExact(bp, cmd, larg, parg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_s_mem$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_s_mem"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const BIO_METHOD *BIO_s_mem(void) + * } + */ + public static MemorySegment BIO_s_mem() { + var mh$ = BIO_s_mem$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_s_mem"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_s_bio$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_s_bio"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const BIO_METHOD *BIO_s_bio(void) + * } + */ + public static MemorySegment BIO_s_bio() { + var mh$ = BIO_s_bio$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_s_bio"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BIO_new_bio_pair$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_LONG, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BIO_new_bio_pair"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int BIO_new_bio_pair(BIO **bio1, size_t writebuf1, BIO **bio2, size_t writebuf2) + * } + */ + public static int BIO_new_bio_pair(MemorySegment bio1, long writebuf1, MemorySegment bio2, long writebuf2) { + var mh$ = BIO_new_bio_pair$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BIO_new_bio_pair", bio1, writebuf1, bio2, writebuf2); + } + return (int) mh$.invokeExact(bio1, writebuf1, bio2, writebuf2); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_new$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_new"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_new(void) + * } + */ + public static MemorySegment BN_new() { + var mh$ = BN_new$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_new"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_set_word$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_set_word"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int BN_set_word(BIGNUM *a, unsigned long w) + * } + */ + public static int BN_set_word(MemorySegment a, long w) { + var mh$ = BN_set_word$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_set_word", a, w); + } + return (int) mh$.invokeExact(a, w); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_get_rfc2409_prime_768$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_get_rfc2409_prime_768"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_get_rfc2409_prime_768(BIGNUM *bn) + * } + */ + public static MemorySegment BN_get_rfc2409_prime_768(MemorySegment bn) { + var mh$ = BN_get_rfc2409_prime_768$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_get_rfc2409_prime_768", bn); + } + return (MemorySegment) mh$.invokeExact(bn); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_get_rfc2409_prime_1024$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_get_rfc2409_prime_1024"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_get_rfc2409_prime_1024(BIGNUM *bn) + * } + */ + public static MemorySegment BN_get_rfc2409_prime_1024(MemorySegment bn) { + var mh$ = BN_get_rfc2409_prime_1024$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_get_rfc2409_prime_1024", bn); + } + return (MemorySegment) mh$.invokeExact(bn); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_get_rfc3526_prime_1536$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_get_rfc3526_prime_1536"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_get_rfc3526_prime_1536(BIGNUM *bn) + * } + */ + public static MemorySegment BN_get_rfc3526_prime_1536(MemorySegment bn) { + var mh$ = BN_get_rfc3526_prime_1536$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_get_rfc3526_prime_1536", bn); + } + return (MemorySegment) mh$.invokeExact(bn); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_get_rfc3526_prime_2048$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_get_rfc3526_prime_2048"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_get_rfc3526_prime_2048(BIGNUM *bn) + * } + */ + public static MemorySegment BN_get_rfc3526_prime_2048(MemorySegment bn) { + var mh$ = BN_get_rfc3526_prime_2048$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_get_rfc3526_prime_2048", bn); + } + return (MemorySegment) mh$.invokeExact(bn); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_get_rfc3526_prime_3072$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_get_rfc3526_prime_3072"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_get_rfc3526_prime_3072(BIGNUM *bn) + * } + */ + public static MemorySegment BN_get_rfc3526_prime_3072(MemorySegment bn) { + var mh$ = BN_get_rfc3526_prime_3072$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_get_rfc3526_prime_3072", bn); + } + return (MemorySegment) mh$.invokeExact(bn); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_get_rfc3526_prime_4096$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_get_rfc3526_prime_4096"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_get_rfc3526_prime_4096(BIGNUM *bn) + * } + */ + public static MemorySegment BN_get_rfc3526_prime_4096(MemorySegment bn) { + var mh$ = BN_get_rfc3526_prime_4096$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_get_rfc3526_prime_4096", bn); + } + return (MemorySegment) mh$.invokeExact(bn); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_get_rfc3526_prime_6144$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_get_rfc3526_prime_6144"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_get_rfc3526_prime_6144(BIGNUM *bn) + * } + */ + public static MemorySegment BN_get_rfc3526_prime_6144(MemorySegment bn) { + var mh$ = BN_get_rfc3526_prime_6144$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_get_rfc3526_prime_6144", bn); + } + return (MemorySegment) mh$.invokeExact(bn); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle BN_get_rfc3526_prime_8192$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("BN_get_rfc3526_prime_8192"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * BIGNUM *BN_get_rfc3526_prime_8192(BIGNUM *bn) + * } + */ + public static MemorySegment BN_get_rfc3526_prime_8192(MemorySegment bn) { + var mh$ = BN_get_rfc3526_prime_8192$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("BN_get_rfc3526_prime_8192", bn); + } + return (MemorySegment) mh$.invokeExact(bn); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ASN1_STRING_length$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ASN1_STRING_length"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int ASN1_STRING_length(const ASN1_STRING *x) + * } + */ + public static int ASN1_STRING_length(MemorySegment x) { + var mh$ = ASN1_STRING_length$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ASN1_STRING_length", x); + } + return (int) mh$.invokeExact(x); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ASN1_STRING_get0_data$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ASN1_STRING_get0_data"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const unsigned char *ASN1_STRING_get0_data(const ASN1_STRING *x) + * } + */ + public static MemorySegment ASN1_STRING_get0_data(MemorySegment x) { + var mh$ = ASN1_STRING_get0_data$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ASN1_STRING_get0_data", x); + } + return (MemorySegment) mh$.invokeExact(x); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EVP_MD_get0_provider$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EVP_MD_get0_provider"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const OSSL_PROVIDER *EVP_MD_get0_provider(const EVP_MD *md) + * } + */ + public static MemorySegment EVP_MD_get0_provider(MemorySegment md) { + var mh$ = EVP_MD_get0_provider$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EVP_MD_get0_provider", md); + } + return (MemorySegment) mh$.invokeExact(md); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EVP_MD_fetch$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EVP_MD_fetch"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * EVP_MD *EVP_MD_fetch(OSSL_LIB_CTX *ctx, const char *algorithm, const char *properties) + * } + */ + public static MemorySegment EVP_MD_fetch(MemorySegment ctx, MemorySegment algorithm, MemorySegment properties) { + var mh$ = EVP_MD_fetch$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EVP_MD_fetch", ctx, algorithm, properties); + } + return (MemorySegment) mh$.invokeExact(ctx, algorithm, properties); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EVP_MD_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EVP_MD_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void EVP_MD_free(EVP_MD *md) + * } + */ + public static void EVP_MD_free(MemorySegment md) { + var mh$ = EVP_MD_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EVP_MD_free", md); + } + mh$.invokeExact(md); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EVP_PKEY_get_base_id$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EVP_PKEY_get_base_id"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int EVP_PKEY_get_base_id(const EVP_PKEY *pkey) + * } + */ + public static int EVP_PKEY_get_base_id(MemorySegment pkey) { + var mh$ = EVP_PKEY_get_base_id$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EVP_PKEY_get_base_id", pkey); + } + return (int) mh$.invokeExact(pkey); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EVP_PKEY_get_bits$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EVP_PKEY_get_bits"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int EVP_PKEY_get_bits(const EVP_PKEY *pkey) + * } + */ + public static int EVP_PKEY_get_bits(MemorySegment pkey) { + var mh$ = EVP_PKEY_get_bits$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EVP_PKEY_get_bits", pkey); + } + return (int) mh$.invokeExact(pkey); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EVP_PKEY_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EVP_PKEY_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void EVP_PKEY_free(EVP_PKEY *pkey) + * } + */ + public static void EVP_PKEY_free(MemorySegment pkey) { + var mh$ = EVP_PKEY_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EVP_PKEY_free", pkey); + } + mh$.invokeExact(pkey); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EC_GROUP_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EC_GROUP_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void EC_GROUP_free(EC_GROUP *group) + * } + */ + public static void EC_GROUP_free(MemorySegment group) { + var mh$ = EC_GROUP_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EC_GROUP_free", group); + } + mh$.invokeExact(group); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EC_GROUP_get_curve_name$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EC_GROUP_get_curve_name"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int EC_GROUP_get_curve_name(const EC_GROUP *group) + * } + */ + public static int EC_GROUP_get_curve_name(MemorySegment group) { + var mh$ = EC_GROUP_get_curve_name$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EC_GROUP_get_curve_name", group); + } + return (int) mh$.invokeExact(group); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle d2i_ECPKParameters$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("d2i_ECPKParameters"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * EC_GROUP *d2i_ECPKParameters(EC_GROUP **, const unsigned char **in, long len) + * } + */ + public static MemorySegment d2i_ECPKParameters(MemorySegment x0, MemorySegment in, long len) { + var mh$ = d2i_ECPKParameters$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("d2i_ECPKParameters", x0, in, len); + } + return (MemorySegment) mh$.invokeExact(x0, in, len); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EC_KEY_new_by_curve_name$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EC_KEY_new_by_curve_name"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * EC_KEY *EC_KEY_new_by_curve_name(int nid) + * } + */ + public static MemorySegment EC_KEY_new_by_curve_name(int nid) { + var mh$ = EC_KEY_new_by_curve_name$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EC_KEY_new_by_curve_name", nid); + } + return (MemorySegment) mh$.invokeExact(nid); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle EC_KEY_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("EC_KEY_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void EC_KEY_free(EC_KEY *key) + * } + */ + public static void EC_KEY_free(MemorySegment key) { + var mh$ = EC_KEY_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("EC_KEY_free", key); + } + mh$.invokeExact(key); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle DH_new$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("DH_new"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * DH *DH_new(void) + * } + */ + public static MemorySegment DH_new() { + var mh$ = DH_new$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("DH_new"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle DH_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("DH_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void DH_free(DH *dh) + * } + */ + public static void DH_free(MemorySegment dh) { + var mh$ = DH_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("DH_free", dh); + } + mh$.invokeExact(dh); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle DH_set0_pqg$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("DH_set0_pqg"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int DH_set0_pqg(DH *dh, BIGNUM *p, BIGNUM *q, BIGNUM *g) + * } + */ + public static int DH_set0_pqg(MemorySegment dh, MemorySegment p, MemorySegment q, MemorySegment g) { + var mh$ = DH_set0_pqg$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("DH_set0_pqg", dh, p, q, g); + } + return (int) mh$.invokeExact(dh, p, q, g); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_set_flags$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_set_flags"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int X509_STORE_set_flags(X509_STORE *ctx, unsigned long flags) + * } + */ + public static int X509_STORE_set_flags(MemorySegment ctx, long flags) { + var mh$ = X509_STORE_set_flags$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_set_flags", ctx, flags); + } + return (int) mh$.invokeExact(ctx, flags); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_CTX_get0_untrusted$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_CTX_get0_untrusted"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * struct stack_st_X509 *X509_STORE_CTX_get0_untrusted(const X509_STORE_CTX *ctx) + * } + */ + public static MemorySegment X509_STORE_CTX_get0_untrusted(MemorySegment ctx) { + var mh$ = X509_STORE_CTX_get0_untrusted$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_CTX_get0_untrusted", ctx); + } + return (MemorySegment) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_add_lookup$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_add_lookup"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509_LOOKUP *X509_STORE_add_lookup(X509_STORE *v, X509_LOOKUP_METHOD *m) + * } + */ + public static MemorySegment X509_STORE_add_lookup(MemorySegment v, MemorySegment m) { + var mh$ = X509_STORE_add_lookup$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_add_lookup", v, m); + } + return (MemorySegment) mh$.invokeExact(v, m); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_LOOKUP_hash_dir$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_LOOKUP_hash_dir"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509_LOOKUP_METHOD *X509_LOOKUP_hash_dir(void) + * } + */ + public static MemorySegment X509_LOOKUP_hash_dir() { + var mh$ = X509_LOOKUP_hash_dir$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_LOOKUP_hash_dir"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_LOOKUP_file$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_LOOKUP_file"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509_LOOKUP_METHOD *X509_LOOKUP_file(void) + * } + */ + public static MemorySegment X509_LOOKUP_file() { + var mh$ = X509_LOOKUP_file$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_LOOKUP_file"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_LOOKUP_ctrl$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_LOOKUP_ctrl"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int X509_LOOKUP_ctrl(X509_LOOKUP *ctx, int cmd, const char *argc, long argl, char **ret) + * } + */ + public static int X509_LOOKUP_ctrl(MemorySegment ctx, int cmd, MemorySegment argc, long argl, MemorySegment ret) { + var mh$ = X509_LOOKUP_ctrl$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_LOOKUP_ctrl", ctx, cmd, argc, argl, ret); + } + return (int) mh$.invokeExact(ctx, cmd, argc, argl, ret); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_CTX_get_ex_data$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_CTX_get_ex_data"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void *X509_STORE_CTX_get_ex_data(const X509_STORE_CTX *ctx, int idx) + * } + */ + public static MemorySegment X509_STORE_CTX_get_ex_data(MemorySegment ctx, int idx) { + var mh$ = X509_STORE_CTX_get_ex_data$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_CTX_get_ex_data", ctx, idx); + } + return (MemorySegment) mh$.invokeExact(ctx, idx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_CTX_get_error$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_CTX_get_error"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int X509_STORE_CTX_get_error(const X509_STORE_CTX *ctx) + * } + */ + public static int X509_STORE_CTX_get_error(MemorySegment ctx) { + var mh$ = X509_STORE_CTX_get_error$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_CTX_get_error", ctx); + } + return (int) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_CTX_set_error$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_CTX_set_error"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void X509_STORE_CTX_set_error(X509_STORE_CTX *ctx, int s) + * } + */ + public static void X509_STORE_CTX_set_error(MemorySegment ctx, int s) { + var mh$ = X509_STORE_CTX_set_error$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_CTX_set_error", ctx, s); + } + mh$.invokeExact(ctx, s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_CTX_get_error_depth$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_CTX_get_error_depth"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int X509_STORE_CTX_get_error_depth(const X509_STORE_CTX *ctx) + * } + */ + public static int X509_STORE_CTX_get_error_depth(MemorySegment ctx) { + var mh$ = X509_STORE_CTX_get_error_depth$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_CTX_get_error_depth", ctx); + } + return (int) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_CTX_get_current_cert$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_CTX_get_current_cert"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509 *X509_STORE_CTX_get_current_cert(const X509_STORE_CTX *ctx) + * } + */ + public static MemorySegment X509_STORE_CTX_get_current_cert(MemorySegment ctx) { + var mh$ = X509_STORE_CTX_get_current_cert$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_CTX_get_current_cert", ctx); + } + return (MemorySegment) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_STORE_CTX_get0_current_issuer$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_STORE_CTX_get0_current_issuer"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509 *X509_STORE_CTX_get0_current_issuer(const X509_STORE_CTX *ctx) + * } + */ + public static MemorySegment X509_STORE_CTX_get0_current_issuer(MemorySegment ctx) { + var mh$ = X509_STORE_CTX_get0_current_issuer$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_CTX_get0_current_issuer", ctx); + } + return (MemorySegment) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle d2i_X509_bio$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("d2i_X509_bio"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509 *d2i_X509_bio(BIO *bp, X509 **x509) + * } + */ + public static MemorySegment d2i_X509_bio(MemorySegment bp, MemorySegment x509) { + var mh$ = d2i_X509_bio$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("d2i_X509_bio", bp, x509); + } + return (MemorySegment) mh$.invokeExact(bp, x509); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern void X509_free(X509 *a) + * } + */ + public static void X509_free(MemorySegment a) { + var mh$ = X509_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_free", a); + } + mh$.invokeExact(a); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle d2i_X509$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("d2i_X509"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern X509 *d2i_X509(X509 **a, const unsigned char **in, long len) + * } + */ + public static MemorySegment d2i_X509(MemorySegment a, MemorySegment in, long len) { + var mh$ = d2i_X509$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("d2i_X509", a, in, len); + } + return (MemorySegment) mh$.invokeExact(a, in, len); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle i2d_X509$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("i2d_X509"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern int i2d_X509(const X509 *a, unsigned char **out) + * } + */ + public static int i2d_X509(MemorySegment a, MemorySegment out) { + var mh$ = i2d_X509$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("i2d_X509", a, out); + } + return (int) mh$.invokeExact(a, out); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_get_ext_by_NID$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_get_ext_by_NID"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int X509_get_ext_by_NID(const X509 *x, int nid, int lastpos) + * } + */ + public static int X509_get_ext_by_NID(MemorySegment x, int nid, int lastpos) { + var mh$ = X509_get_ext_by_NID$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_get_ext_by_NID", x, nid, lastpos); + } + return (int) mh$.invokeExact(x, nid, lastpos); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_get_ext$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_get_ext"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509_EXTENSION *X509_get_ext(const X509 *x, int loc) + * } + */ + public static MemorySegment X509_get_ext(MemorySegment x, int loc) { + var mh$ = X509_get_ext$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_get_ext", x, loc); + } + return (MemorySegment) mh$.invokeExact(x, loc); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_EXTENSION_get_data$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_EXTENSION_get_data"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * ASN1_OCTET_STRING *X509_EXTENSION_get_data(X509_EXTENSION *ne) + * } + */ + public static MemorySegment X509_EXTENSION_get_data(MemorySegment ne) { + var mh$ = X509_EXTENSION_get_data$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_EXTENSION_get_data", ne); + } + return (MemorySegment) mh$.invokeExact(ne); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PEM_ASN1_read_bio$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PEM_ASN1_read_bio"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void *PEM_ASN1_read_bio(d2i_of_void *d2i, const char *name, BIO *bp, void **x, pem_password_cb *cb, void *u) + * } + */ + public static MemorySegment PEM_ASN1_read_bio(MemorySegment d2i, MemorySegment name, MemorySegment bp, MemorySegment x, MemorySegment cb, MemorySegment u) { + var mh$ = PEM_ASN1_read_bio$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PEM_ASN1_read_bio", d2i, name, bp, x, cb, u); + } + return (MemorySegment) mh$.invokeExact(d2i, name, bp, x, cb, u); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PEM_read_bio_X509_AUX$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PEM_read_bio_X509_AUX"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern X509 *PEM_read_bio_X509_AUX(BIO *out, X509 **x, pem_password_cb *cb, void *u) + * } + */ + public static MemorySegment PEM_read_bio_X509_AUX(MemorySegment out, MemorySegment x, MemorySegment cb, MemorySegment u) { + var mh$ = PEM_read_bio_X509_AUX$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PEM_read_bio_X509_AUX", out, x, cb, u); + } + return (MemorySegment) mh$.invokeExact(out, x, cb, u); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PEM_read_bio_ECPKParameters$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PEM_read_bio_ECPKParameters"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * EC_GROUP *PEM_read_bio_ECPKParameters(BIO *out, EC_GROUP **x, pem_password_cb *cb, void *u) + * } + */ + public static MemorySegment PEM_read_bio_ECPKParameters(MemorySegment out, MemorySegment x, MemorySegment cb, MemorySegment u) { + var mh$ = PEM_read_bio_ECPKParameters$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PEM_read_bio_ECPKParameters", out, x, cb, u); + } + return (MemorySegment) mh$.invokeExact(out, x, cb, u); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PEM_read_bio_DHparams$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PEM_read_bio_DHparams"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * DH *PEM_read_bio_DHparams(BIO *out, DH **x, pem_password_cb *cb, void *u) + * } + */ + public static MemorySegment PEM_read_bio_DHparams(MemorySegment out, MemorySegment x, MemorySegment cb, MemorySegment u) { + var mh$ = PEM_read_bio_DHparams$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PEM_read_bio_DHparams", out, x, cb, u); + } + return (MemorySegment) mh$.invokeExact(out, x, cb, u); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PEM_read_bio_PrivateKey$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PEM_read_bio_PrivateKey"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern EVP_PKEY *PEM_read_bio_PrivateKey(BIO *out, EVP_PKEY **x, pem_password_cb *cb, void *u) + * } + */ + public static MemorySegment PEM_read_bio_PrivateKey(MemorySegment out, MemorySegment x, MemorySegment cb, MemorySegment u) { + var mh$ = PEM_read_bio_PrivateKey$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PEM_read_bio_PrivateKey", out, x, cb, u); + } + return (MemorySegment) mh$.invokeExact(out, x, cb, u); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PEM_read_bio_Parameters$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PEM_read_bio_Parameters"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * EVP_PKEY *PEM_read_bio_Parameters(BIO *bp, EVP_PKEY **x) + * } + */ + public static MemorySegment PEM_read_bio_Parameters(MemorySegment bp, MemorySegment x) { + var mh$ = PEM_read_bio_Parameters$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PEM_read_bio_Parameters", bp, x); + } + return (MemorySegment) mh$.invokeExact(bp, x); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_get_options$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_get_options"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * uint64_t SSL_CTX_get_options(const SSL_CTX *ctx) + * } + */ + public static long SSL_CTX_get_options(MemorySegment ctx) { + var mh$ = SSL_CTX_get_options$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_get_options", ctx); + } + return (long) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_options$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_options"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * uint64_t SSL_get_options(const SSL *s) + * } + */ + public static long SSL_get_options(MemorySegment s) { + var mh$ = SSL_get_options$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_options", s); + } + return (long) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_clear_options$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_clear_options"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * uint64_t SSL_CTX_clear_options(SSL_CTX *ctx, uint64_t op) + * } + */ + public static long SSL_CTX_clear_options(MemorySegment ctx, long op) { + var mh$ = SSL_CTX_clear_options$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_clear_options", ctx, op); + } + return (long) mh$.invokeExact(ctx, op); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_options$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_options"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * uint64_t SSL_CTX_set_options(SSL_CTX *ctx, uint64_t op) + * } + */ + public static long SSL_CTX_set_options(MemorySegment ctx, long op) { + var mh$ = SSL_CTX_set_options$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_options", ctx, op); + } + return (long) mh$.invokeExact(ctx, op); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_set_options$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_set_options"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * uint64_t SSL_set_options(SSL *s, uint64_t op) + * } + */ + public static long SSL_set_options(MemorySegment s, long op) { + var mh$ = SSL_set_options$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_set_options", s, op); + } + return (long) mh$.invokeExact(s, op); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_alpn_select_cb$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_alpn_select_cb"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CTX_set_alpn_select_cb(SSL_CTX *ctx, SSL_CTX_alpn_select_cb_func cb, void *arg) + * } + */ + public static void SSL_CTX_set_alpn_select_cb(MemorySegment ctx, MemorySegment cb, MemorySegment arg) { + var mh$ = SSL_CTX_set_alpn_select_cb$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_alpn_select_cb", ctx, cb, arg); + } + mh$.invokeExact(ctx, cb, arg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get0_alpn_selected$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get0_alpn_selected"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_get0_alpn_selected(const SSL *ssl, const unsigned char **data, unsigned int *len) + * } + */ + public static void SSL_get0_alpn_selected(MemorySegment ssl, MemorySegment data, MemorySegment len) { + var mh$ = SSL_get0_alpn_selected$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get0_alpn_selected", ssl, data, len); + } + mh$.invokeExact(ssl, data, len); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_in_init$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_in_init"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_in_init(const SSL *s) + * } + */ + public static int SSL_in_init(MemorySegment s) { + var mh$ = SSL_in_init$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_in_init", s); + } + return (int) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set0_tmp_dh_pkey$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set0_tmp_dh_pkey"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_set0_tmp_dh_pkey(SSL_CTX *ctx, EVP_PKEY *dhpkey) + * } + */ + public static int SSL_CTX_set0_tmp_dh_pkey(MemorySegment ctx, MemorySegment dhpkey) { + var mh$ = SSL_CTX_set0_tmp_dh_pkey$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set0_tmp_dh_pkey", ctx, dhpkey); + } + return (int) mh$.invokeExact(ctx, dhpkey); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_cipher_list$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_cipher_list"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_set_cipher_list(SSL_CTX *, const char *str) + * } + */ + public static int SSL_CTX_set_cipher_list(MemorySegment x0, MemorySegment str) { + var mh$ = SSL_CTX_set_cipher_list$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_cipher_list", x0, str); + } + return (int) mh$.invokeExact(x0, str); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_new$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_new"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth) + * } + */ + public static MemorySegment SSL_CTX_new(MemorySegment meth) { + var mh$ = SSL_CTX_new$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_new", meth); + } + return (MemorySegment) mh$.invokeExact(meth); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CTX_free(SSL_CTX *) + * } + */ + public static void SSL_CTX_free(MemorySegment x0) { + var mh$ = SSL_CTX_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_free", x0); + } + mh$.invokeExact(x0); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_timeout$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_timeout"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * long SSL_CTX_set_timeout(SSL_CTX *ctx, long t) + * } + */ + public static long SSL_CTX_set_timeout(MemorySegment ctx, long t) { + var mh$ = SSL_CTX_set_timeout$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_timeout", ctx, t); + } + return (long) mh$.invokeExact(ctx, t); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_get_timeout$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_get_timeout"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * long SSL_CTX_get_timeout(const SSL_CTX *ctx) + * } + */ + public static long SSL_CTX_get_timeout(MemorySegment ctx) { + var mh$ = SSL_CTX_get_timeout$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_get_timeout", ctx); + } + return (long) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_get_cert_store$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_get_cert_store"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509_STORE *SSL_CTX_get_cert_store(const SSL_CTX *) + * } + */ + public static MemorySegment SSL_CTX_get_cert_store(MemorySegment x0) { + var mh$ = SSL_CTX_get_cert_store$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_get_cert_store", x0); + } + return (MemorySegment) mh$.invokeExact(x0); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_current_cipher$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_current_cipher"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const SSL_CIPHER *SSL_get_current_cipher(const SSL *s) + * } + */ + public static MemorySegment SSL_get_current_cipher(MemorySegment s) { + var mh$ = SSL_get_current_cipher$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_current_cipher", s); + } + return (MemorySegment) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CIPHER_get_name$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CIPHER_get_name"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const char *SSL_CIPHER_get_name(const SSL_CIPHER *c) + * } + */ + public static MemorySegment SSL_CIPHER_get_name(MemorySegment c) { + var mh$ = SSL_CIPHER_get_name$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CIPHER_get_name", c); + } + return (MemorySegment) mh$.invokeExact(c); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CIPHER_get_kx_nid$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CIPHER_get_kx_nid"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CIPHER_get_kx_nid(const SSL_CIPHER *c) + * } + */ + public static int SSL_CIPHER_get_kx_nid(MemorySegment c) { + var mh$ = SSL_CIPHER_get_kx_nid$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CIPHER_get_kx_nid", c); + } + return (int) mh$.invokeExact(c); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CIPHER_get_auth_nid$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CIPHER_get_auth_nid"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CIPHER_get_auth_nid(const SSL_CIPHER *c) + * } + */ + public static int SSL_CIPHER_get_auth_nid(MemorySegment c) { + var mh$ = SSL_CIPHER_get_auth_nid$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CIPHER_get_auth_nid", c); + } + return (int) mh$.invokeExact(c); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_pending$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_pending"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_pending(const SSL *s) + * } + */ + public static int SSL_pending(MemorySegment s) { + var mh$ = SSL_pending$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_pending", s); + } + return (int) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_set_bio$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_set_bio"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_set_bio(SSL *s, BIO *rbio, BIO *wbio) + * } + */ + public static void SSL_set_bio(MemorySegment s, MemorySegment rbio, MemorySegment wbio) { + var mh$ = SSL_set_bio$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_set_bio", s, rbio, wbio); + } + mh$.invokeExact(s, rbio, wbio); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_set_cipher_list$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_set_cipher_list"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_set_cipher_list(SSL *s, const char *str) + * } + */ + public static int SSL_set_cipher_list(MemorySegment s, MemorySegment str) { + var mh$ = SSL_set_cipher_list$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_set_cipher_list", s, str); + } + return (int) mh$.invokeExact(s, str); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_ciphersuites$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_ciphersuites"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_set_ciphersuites(SSL_CTX *ctx, const char *str) + * } + */ + public static int SSL_CTX_set_ciphersuites(MemorySegment ctx, MemorySegment str) { + var mh$ = SSL_CTX_set_ciphersuites$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_ciphersuites", ctx, str); + } + return (int) mh$.invokeExact(ctx, str); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_set_verify$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_set_verify"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_set_verify(SSL *s, int mode, SSL_verify_cb callback) + * } + */ + public static void SSL_set_verify(MemorySegment s, int mode, MemorySegment callback) { + var mh$ = SSL_set_verify$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_set_verify", s, mode, callback); + } + mh$.invokeExact(s, mode, callback); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_use_certificate_chain_file$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_use_certificate_chain_file"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_use_certificate_chain_file(SSL_CTX *ctx, const char *file) + * } + */ + public static int SSL_CTX_use_certificate_chain_file(MemorySegment ctx, MemorySegment file) { + var mh$ = SSL_CTX_use_certificate_chain_file$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_use_certificate_chain_file", ctx, file); + } + return (int) mh$.invokeExact(ctx, file); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_load_client_CA_file$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_load_client_CA_file"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * struct stack_st_X509_NAME *SSL_load_client_CA_file(const char *file) + * } + */ + public static MemorySegment SSL_load_client_CA_file(MemorySegment file) { + var mh$ = SSL_load_client_CA_file$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_load_client_CA_file", file); + } + return (MemorySegment) mh$.invokeExact(file); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_add_file_cert_subjects_to_stack$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_add_file_cert_subjects_to_stack"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_add_file_cert_subjects_to_stack(struct stack_st_X509_NAME *stackCAs, const char *file) + * } + */ + public static int SSL_add_file_cert_subjects_to_stack(MemorySegment stackCAs, MemorySegment file) { + var mh$ = SSL_add_file_cert_subjects_to_stack$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_add_file_cert_subjects_to_stack", stackCAs, file); + } + return (int) mh$.invokeExact(stackCAs, file); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_SESSION_get_time$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_SESSION_get_time"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * long SSL_SESSION_get_time(const SSL_SESSION *s) + * } + */ + public static long SSL_SESSION_get_time(MemorySegment s) { + var mh$ = SSL_SESSION_get_time$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_SESSION_get_time", s); + } + return (long) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_SESSION_get_id$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_SESSION_get_id"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const unsigned char *SSL_SESSION_get_id(const SSL_SESSION *s, unsigned int *len) + * } + */ + public static MemorySegment SSL_SESSION_get_id(MemorySegment s, MemorySegment len) { + var mh$ = SSL_SESSION_get_id$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_SESSION_get_id", s, len); + } + return (MemorySegment) mh$.invokeExact(s, len); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get1_peer_certificate$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get1_peer_certificate"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * X509 *SSL_get1_peer_certificate(const SSL *s) + * } + */ + public static MemorySegment SSL_get1_peer_certificate(MemorySegment s) { + var mh$ = SSL_get1_peer_certificate$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get1_peer_certificate", s); + } + return (MemorySegment) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_peer_cert_chain$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_peer_cert_chain"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * struct stack_st_X509 *SSL_get_peer_cert_chain(const SSL *s) + * } + */ + public static MemorySegment SSL_get_peer_cert_chain(MemorySegment s) { + var mh$ = SSL_get_peer_cert_chain$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_peer_cert_chain", s); + } + return (MemorySegment) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_verify$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_verify"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, SSL_verify_cb callback) + * } + */ + public static void SSL_CTX_set_verify(MemorySegment ctx, int mode, MemorySegment callback) { + var mh$ = SSL_CTX_set_verify$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_verify", ctx, mode, callback); + } + mh$.invokeExact(ctx, mode, callback); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_cert_verify_callback$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_cert_verify_callback"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CTX_set_cert_verify_callback(SSL_CTX *ctx, int (*cb)(X509_STORE_CTX *, void *), void *arg) + * } + */ + public static void SSL_CTX_set_cert_verify_callback(MemorySegment ctx, MemorySegment cb, MemorySegment arg) { + var mh$ = SSL_CTX_set_cert_verify_callback$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_cert_verify_callback", ctx, cb, arg); + } + mh$.invokeExact(ctx, cb, arg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_use_PrivateKey$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_use_PrivateKey"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_use_PrivateKey(SSL_CTX *ctx, EVP_PKEY *pkey) + * } + */ + public static int SSL_CTX_use_PrivateKey(MemorySegment ctx, MemorySegment pkey) { + var mh$ = SSL_CTX_use_PrivateKey$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_use_PrivateKey", ctx, pkey); + } + return (int) mh$.invokeExact(ctx, pkey); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_use_certificate$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_use_certificate"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_use_certificate(SSL_CTX *ctx, X509 *x) + * } + */ + public static int SSL_CTX_use_certificate(MemorySegment ctx, MemorySegment x) { + var mh$ = SSL_CTX_use_certificate$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_use_certificate", ctx, x); + } + return (int) mh$.invokeExact(ctx, x); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_default_passwd_cb$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_default_passwd_cb"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CTX_set_default_passwd_cb(SSL_CTX *ctx, pem_password_cb *cb) + * } + */ + public static void SSL_CTX_set_default_passwd_cb(MemorySegment ctx, MemorySegment cb) { + var mh$ = SSL_CTX_set_default_passwd_cb$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_default_passwd_cb", ctx, cb); + } + mh$.invokeExact(ctx, cb); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_check_private_key$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_check_private_key"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_check_private_key(const SSL_CTX *ctx) + * } + */ + public static int SSL_CTX_check_private_key(MemorySegment ctx) { + var mh$ = SSL_CTX_check_private_key$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_check_private_key", ctx); + } + return (int) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_session_id_context$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_session_id_context"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_set_session_id_context(SSL_CTX *ctx, const unsigned char *sid_ctx, unsigned int sid_ctx_len) + * } + */ + public static int SSL_CTX_set_session_id_context(MemorySegment ctx, MemorySegment sid_ctx, int sid_ctx_len) { + var mh$ = SSL_CTX_set_session_id_context$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_session_id_context", ctx, sid_ctx, sid_ctx_len); + } + return (int) mh$.invokeExact(ctx, sid_ctx, sid_ctx_len); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_new$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_new"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * SSL *SSL_new(SSL_CTX *ctx) + * } + */ + public static MemorySegment SSL_new(MemorySegment ctx) { + var mh$ = SSL_new$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_new", ctx); + } + return (MemorySegment) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_free(SSL *ssl) + * } + */ + public static void SSL_free(MemorySegment ssl) { + var mh$ = SSL_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_free", ssl); + } + mh$.invokeExact(ssl); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_read$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_read"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_read(SSL *ssl, void *buf, int num) + * } + */ + public static int SSL_read(MemorySegment ssl, MemorySegment buf, int num) { + var mh$ = SSL_read$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_read", ssl, buf, num); + } + return (int) mh$.invokeExact(ssl, buf, num); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_write$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_write"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_write(SSL *ssl, const void *buf, int num) + * } + */ + public static int SSL_write(MemorySegment ssl, MemorySegment buf, int num) { + var mh$ = SSL_write$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_write", ssl, buf, num); + } + return (int) mh$.invokeExact(ssl, buf, num); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_ctrl$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG, + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_ctrl"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * long SSL_CTX_ctrl(SSL_CTX *ctx, int cmd, long larg, void *parg) + * } + */ + public static long SSL_CTX_ctrl(MemorySegment ctx, int cmd, long larg, MemorySegment parg) { + var mh$ = SSL_CTX_ctrl$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_ctrl", ctx, cmd, larg, parg); + } + return (long) mh$.invokeExact(ctx, cmd, larg, parg); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_version$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_version"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const char *SSL_get_version(const SSL *s) + * } + */ + public static MemorySegment SSL_get_version(MemorySegment s) { + var mh$ = SSL_get_version$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_version", s); + } + return (MemorySegment) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle TLS_server_method$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("TLS_server_method"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const SSL_METHOD *TLS_server_method(void) + * } + */ + public static MemorySegment TLS_server_method() { + var mh$ = TLS_server_method$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("TLS_server_method"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_ciphers$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_ciphers"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * struct stack_st_SSL_CIPHER *SSL_get_ciphers(const SSL *s) + * } + */ + public static MemorySegment SSL_get_ciphers(MemorySegment s) { + var mh$ = SSL_get_ciphers$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_ciphers", s); + } + return (MemorySegment) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_get_ciphers$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_get_ciphers"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * struct stack_st_SSL_CIPHER *SSL_CTX_get_ciphers(const SSL_CTX *ctx) + * } + */ + public static MemorySegment SSL_CTX_get_ciphers(MemorySegment ctx) { + var mh$ = SSL_CTX_get_ciphers$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_get_ciphers", ctx); + } + return (MemorySegment) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_do_handshake$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_do_handshake"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_do_handshake(SSL *s) + * } + */ + public static int SSL_do_handshake(MemorySegment s) { + var mh$ = SSL_do_handshake$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_do_handshake", s); + } + return (int) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_renegotiate$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_renegotiate"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_renegotiate(SSL *s) + * } + */ + public static int SSL_renegotiate(MemorySegment s) { + var mh$ = SSL_renegotiate$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_renegotiate", s); + } + return (int) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_renegotiate_pending$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_renegotiate_pending"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_renegotiate_pending(const SSL *s) + * } + */ + public static int SSL_renegotiate_pending(MemorySegment s) { + var mh$ = SSL_renegotiate_pending$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_renegotiate_pending", s); + } + return (int) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_shutdown$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_shutdown"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_shutdown(SSL *s) + * } + */ + public static int SSL_shutdown(MemorySegment s) { + var mh$ = SSL_shutdown$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_shutdown", s); + } + return (int) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_verify_client_post_handshake$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_verify_client_post_handshake"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_verify_client_post_handshake(SSL *s) + * } + */ + public static int SSL_verify_client_post_handshake(MemorySegment s) { + var mh$ = SSL_verify_client_post_handshake$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_verify_client_post_handshake", s); + } + return (int) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_client_CA_list$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_client_CA_list"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CTX_set_client_CA_list(SSL_CTX *ctx, struct stack_st_X509_NAME *name_list) + * } + */ + public static void SSL_CTX_set_client_CA_list(MemorySegment ctx, MemorySegment name_list) { + var mh$ = SSL_CTX_set_client_CA_list$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_client_CA_list", ctx, name_list); + } + mh$.invokeExact(ctx, name_list); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_get_client_CA_list$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_get_client_CA_list"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * struct stack_st_X509_NAME *SSL_CTX_get_client_CA_list(const SSL_CTX *s) + * } + */ + public static MemorySegment SSL_CTX_get_client_CA_list(MemorySegment s) { + var mh$ = SSL_CTX_get_client_CA_list$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_get_client_CA_list", s); + } + return (MemorySegment) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_add_client_CA$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_add_client_CA"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_add_client_CA(SSL_CTX *ctx, X509 *x) + * } + */ + public static int SSL_CTX_add_client_CA(MemorySegment ctx, MemorySegment x) { + var mh$ = SSL_CTX_add_client_CA$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_add_client_CA", ctx, x); + } + return (int) mh$.invokeExact(ctx, x); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_set_connect_state$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_set_connect_state"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_set_connect_state(SSL *s) + * } + */ + public static void SSL_set_connect_state(MemorySegment s) { + var mh$ = SSL_set_connect_state$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_set_connect_state", s); + } + mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_set_accept_state$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_set_accept_state"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_set_accept_state(SSL *s) + * } + */ + public static void SSL_set_accept_state(MemorySegment s) { + var mh$ = SSL_set_accept_state$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_set_accept_state", s); + } + mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_privatekey$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_privatekey"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * struct evp_pkey_st *SSL_get_privatekey(const SSL *ssl) + * } + */ + public static MemorySegment SSL_get_privatekey(MemorySegment ssl) { + var mh$ = SSL_get_privatekey$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_privatekey", ssl); + } + return (MemorySegment) mh$.invokeExact(ssl); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_shutdown$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_shutdown"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_get_shutdown(const SSL *ssl) + * } + */ + public static int SSL_get_shutdown(MemorySegment ssl) { + var mh$ = SSL_get_shutdown$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_shutdown", ssl); + } + return (int) mh$.invokeExact(ssl); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_default_verify_paths$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_default_verify_paths"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_set_default_verify_paths(SSL_CTX *ctx) + * } + */ + public static int SSL_CTX_set_default_verify_paths(MemorySegment ctx) { + var mh$ = SSL_CTX_set_default_verify_paths$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_default_verify_paths", ctx); + } + return (int) mh$.invokeExact(ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_load_verify_locations$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_load_verify_locations"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath) + * } + */ + public static int SSL_CTX_load_verify_locations(MemorySegment ctx, MemorySegment CAfile, MemorySegment CApath) { + var mh$ = SSL_CTX_load_verify_locations$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_load_verify_locations", ctx, CAfile, CApath); + } + return (int) mh$.invokeExact(ctx, CAfile, CApath); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_session$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_session"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * SSL_SESSION *SSL_get_session(const SSL *ssl) + * } + */ + public static MemorySegment SSL_get_session(MemorySegment ssl) { + var mh$ = SSL_get_session$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_session", ssl); + } + return (MemorySegment) mh$.invokeExact(ssl); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_set_info_callback$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_set_info_callback"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_set_info_callback(SSL *ssl, void (*cb)(const SSL *, int, int)) + * } + */ + public static void SSL_set_info_callback(MemorySegment ssl, MemorySegment cb) { + var mh$ = SSL_set_info_callback$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_set_info_callback", ssl, cb); + } + mh$.invokeExact(ssl, cb); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_set_verify_result$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_set_verify_result"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_set_verify_result(SSL *ssl, long v) + * } + */ + public static void SSL_set_verify_result(MemorySegment ssl, long v) { + var mh$ = SSL_set_verify_result$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_set_verify_result", ssl, v); + } + mh$.invokeExact(ssl, v); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_get_ex_data_X509_STORE_CTX_idx$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_get_ex_data_X509_STORE_CTX_idx"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_get_ex_data_X509_STORE_CTX_idx(void) + * } + */ + public static int SSL_get_ex_data_X509_STORE_CTX_idx() { + var mh$ = SSL_get_ex_data_X509_STORE_CTX_idx$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_get_ex_data_X509_STORE_CTX_idx"); + } + return (int) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CTX_set_tmp_dh_callback$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CTX_set_tmp_dh_callback"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CTX_set_tmp_dh_callback(SSL_CTX *ctx, DH *(*dh)(SSL *, int, int)) + * } + */ + public static void SSL_CTX_set_tmp_dh_callback(MemorySegment ctx, MemorySegment dh) { + var mh$ = SSL_CTX_set_tmp_dh_callback$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CTX_set_tmp_dh_callback", ctx, dh); + } + mh$.invokeExact(ctx, dh); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CONF_CTX_new$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CONF_CTX_new"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * SSL_CONF_CTX *SSL_CONF_CTX_new(void) + * } + */ + public static MemorySegment SSL_CONF_CTX_new() { + var mh$ = SSL_CONF_CTX_new$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CONF_CTX_new"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CONF_CTX_finish$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CONF_CTX_finish"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CONF_CTX_finish(SSL_CONF_CTX *cctx) + * } + */ + public static int SSL_CONF_CTX_finish(MemorySegment cctx) { + var mh$ = SSL_CONF_CTX_finish$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CONF_CTX_finish", cctx); + } + return (int) mh$.invokeExact(cctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CONF_CTX_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CONF_CTX_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CONF_CTX_free(SSL_CONF_CTX *cctx) + * } + */ + public static void SSL_CONF_CTX_free(MemorySegment cctx) { + var mh$ = SSL_CONF_CTX_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CONF_CTX_free", cctx); + } + mh$.invokeExact(cctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CONF_CTX_set_flags$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CONF_CTX_set_flags"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * unsigned int SSL_CONF_CTX_set_flags(SSL_CONF_CTX *cctx, unsigned int flags) + * } + */ + public static int SSL_CONF_CTX_set_flags(MemorySegment cctx, int flags) { + var mh$ = SSL_CONF_CTX_set_flags$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CONF_CTX_set_flags", cctx, flags); + } + return (int) mh$.invokeExact(cctx, flags); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CONF_CTX_set_ssl_ctx$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CONF_CTX_set_ssl_ctx"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void SSL_CONF_CTX_set_ssl_ctx(SSL_CONF_CTX *cctx, SSL_CTX *ctx) + * } + */ + public static void SSL_CONF_CTX_set_ssl_ctx(MemorySegment cctx, MemorySegment ctx) { + var mh$ = SSL_CONF_CTX_set_ssl_ctx$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CONF_CTX_set_ssl_ctx", cctx, ctx); + } + mh$.invokeExact(cctx, ctx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CONF_cmd$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CONF_cmd"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CONF_cmd(SSL_CONF_CTX *cctx, const char *cmd, const char *value) + * } + */ + public static int SSL_CONF_cmd(MemorySegment cctx, MemorySegment cmd, MemorySegment value) { + var mh$ = SSL_CONF_cmd$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CONF_cmd", cctx, cmd, value); + } + return (int) mh$.invokeExact(cctx, cmd, value); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle SSL_CONF_cmd_value_type$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("SSL_CONF_cmd_value_type"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int SSL_CONF_cmd_value_type(SSL_CONF_CTX *cctx, const char *cmd) + * } + */ + public static int SSL_CONF_cmd_value_type(MemorySegment cctx, MemorySegment cmd) { + var mh$ = SSL_CONF_cmd_value_type$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("SSL_CONF_cmd_value_type", cctx, cmd); + } + return (int) mh$.invokeExact(cctx, cmd); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OPENSSL_init_ssl$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OPENSSL_init_ssl"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int OPENSSL_init_ssl(uint64_t opts, const OPENSSL_INIT_SETTINGS *settings) + * } + */ + public static int OPENSSL_init_ssl(long opts, MemorySegment settings) { + var mh$ = OPENSSL_init_ssl$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OPENSSL_init_ssl", opts, settings); + } + return (int) mh$.invokeExact(opts, settings); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ERR_get_error$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ERR_get_error"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * unsigned long ERR_get_error(void) + * } + */ + public static long ERR_get_error() { + var mh$ = ERR_get_error$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ERR_get_error"); + } + return (long) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ERR_peek_last_error$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_LONG ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ERR_peek_last_error"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * unsigned long ERR_peek_last_error(void) + * } + */ + public static long ERR_peek_last_error() { + var mh$ = ERR_peek_last_error$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ERR_peek_last_error"); + } + return (long) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ERR_clear_error$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ERR_clear_error"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void ERR_clear_error(void) + * } + */ + public static void ERR_clear_error() { + var mh$ = ERR_clear_error$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ERR_clear_error"); + } + mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ERR_error_string$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_LONG, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ERR_error_string"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * char *ERR_error_string(unsigned long e, char *buf) + * } + */ + public static MemorySegment ERR_error_string(long e, MemorySegment buf) { + var mh$ = ERR_error_string$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ERR_error_string", e, buf); + } + return (MemorySegment) mh$.invokeExact(e, buf); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PKCS12_verify_mac$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PKCS12_verify_mac"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int PKCS12_verify_mac(PKCS12 *p12, const char *pass, int passlen) + * } + */ + public static int PKCS12_verify_mac(MemorySegment p12, MemorySegment pass, int passlen) { + var mh$ = PKCS12_verify_mac$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PKCS12_verify_mac", p12, pass, passlen); + } + return (int) mh$.invokeExact(p12, pass, passlen); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PKCS12_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PKCS12_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern void PKCS12_free(PKCS12 *a) + * } + */ + public static void PKCS12_free(MemorySegment a) { + var mh$ = PKCS12_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PKCS12_free", a); + } + mh$.invokeExact(a); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle PKCS12_parse$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("PKCS12_parse"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int PKCS12_parse(PKCS12 *p12, const char *pass, EVP_PKEY **pkey, X509 **cert, struct stack_st_X509 **ca) + * } + */ + public static int PKCS12_parse(MemorySegment p12, MemorySegment pass, MemorySegment pkey, MemorySegment cert, MemorySegment ca) { + var mh$ = PKCS12_parse$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("PKCS12_parse", p12, pass, pkey, cert, ca); + } + return (int) mh$.invokeExact(p12, pass, pkey, cert, ca); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle d2i_PKCS12_bio$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("d2i_PKCS12_bio"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * PKCS12 *d2i_PKCS12_bio(BIO *bp, PKCS12 **p12) + * } + */ + public static MemorySegment d2i_PKCS12_bio(MemorySegment bp, MemorySegment p12) { + var mh$ = d2i_PKCS12_bio$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("d2i_PKCS12_bio", bp, p12); + } + return (MemorySegment) mh$.invokeExact(bp, p12); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle RAND_seed$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("RAND_seed"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * void RAND_seed(const void *buf, int num) + * } + */ + public static void RAND_seed(MemorySegment buf, int num) { + var mh$ = RAND_seed$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("RAND_seed", buf, num); + } + mh$.invokeExact(buf, num); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle RAND_load_file$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("RAND_load_file"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int RAND_load_file(const char *file, long max_bytes) + * } + */ + public static int RAND_load_file(MemorySegment file, long max_bytes) { + var mh$ = RAND_load_file$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("RAND_load_file", file, max_bytes); + } + return (int) mh$.invokeExact(file, max_bytes); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle X509_check_issued$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("X509_check_issued"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int X509_check_issued(X509 *issuer, X509 *subject) + * } + */ + public static int X509_check_issued(MemorySegment issuer, MemorySegment subject) { + var mh$ = X509_check_issued$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_check_issued", issuer, subject); + } + return (int) mh$.invokeExact(issuer, subject); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ENGINE_by_id$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ENGINE_by_id"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * ENGINE *ENGINE_by_id(const char *id) + * } + */ + public static MemorySegment ENGINE_by_id(MemorySegment id) { + var mh$ = ENGINE_by_id$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ENGINE_by_id", id); + } + return (MemorySegment) mh$.invokeExact(id); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ENGINE_register_all_complete$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ENGINE_register_all_complete"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int ENGINE_register_all_complete(void) + * } + */ + public static int ENGINE_register_all_complete() { + var mh$ = ENGINE_register_all_complete$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ENGINE_register_all_complete"); + } + return (int) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ENGINE_ctrl_cmd_string$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ENGINE_ctrl_cmd_string"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int ENGINE_ctrl_cmd_string(ENGINE *e, const char *cmd_name, const char *arg, int cmd_optional) + * } + */ + public static int ENGINE_ctrl_cmd_string(MemorySegment e, MemorySegment cmd_name, MemorySegment arg, int cmd_optional) { + var mh$ = ENGINE_ctrl_cmd_string$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ENGINE_ctrl_cmd_string", e, cmd_name, arg, cmd_optional); + } + return (int) mh$.invokeExact(e, cmd_name, arg, cmd_optional); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ENGINE_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ENGINE_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int ENGINE_free(ENGINE *e) + * } + */ + public static int ENGINE_free(MemorySegment e) { + var mh$ = ENGINE_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ENGINE_free", e); + } + return (int) mh$.invokeExact(e); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ENGINE_load_private_key$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ENGINE_load_private_key"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * EVP_PKEY *ENGINE_load_private_key(ENGINE *e, const char *key_id, UI_METHOD *ui_method, void *callback_data) + * } + */ + public static MemorySegment ENGINE_load_private_key(MemorySegment e, MemorySegment key_id, MemorySegment ui_method, MemorySegment callback_data) { + var mh$ = ENGINE_load_private_key$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ENGINE_load_private_key", e, key_id, ui_method, callback_data); + } + return (MemorySegment) mh$.invokeExact(e, key_id, ui_method, callback_data); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle ENGINE_set_default$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("ENGINE_set_default"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int ENGINE_set_default(ENGINE *e, unsigned int flags) + * } + */ + public static int ENGINE_set_default(MemorySegment e, int flags) { + var mh$ = ENGINE_set_default$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("ENGINE_set_default", e, flags); + } + return (int) mh$.invokeExact(e, flags); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_cert_to_id$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_cert_to_id"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * OCSP_CERTID *OCSP_cert_to_id(const EVP_MD *dgst, const X509 *subject, const X509 *issuer) + * } + */ + public static MemorySegment OCSP_cert_to_id(MemorySegment dgst, MemorySegment subject, MemorySegment issuer) { + var mh$ = OCSP_cert_to_id$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_cert_to_id", dgst, subject, issuer); + } + return (MemorySegment) mh$.invokeExact(dgst, subject, issuer); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_request_add0_id$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_request_add0_id"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * OCSP_ONEREQ *OCSP_request_add0_id(OCSP_REQUEST *req, OCSP_CERTID *cid) + * } + */ + public static MemorySegment OCSP_request_add0_id(MemorySegment req, MemorySegment cid) { + var mh$ = OCSP_request_add0_id$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_request_add0_id", req, cid); + } + return (MemorySegment) mh$.invokeExact(req, cid); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_response_status$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_response_status"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int OCSP_response_status(OCSP_RESPONSE *resp) + * } + */ + public static int OCSP_response_status(MemorySegment resp) { + var mh$ = OCSP_response_status$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_response_status", resp); + } + return (int) mh$.invokeExact(resp); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_response_get1_basic$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_response_get1_basic"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * OCSP_BASICRESP *OCSP_response_get1_basic(OCSP_RESPONSE *resp) + * } + */ + public static MemorySegment OCSP_response_get1_basic(MemorySegment resp) { + var mh$ = OCSP_response_get1_basic$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_response_get1_basic", resp); + } + return (MemorySegment) mh$.invokeExact(resp); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_resp_get0$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_resp_get0"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * OCSP_SINGLERESP *OCSP_resp_get0(OCSP_BASICRESP *bs, int idx) + * } + */ + public static MemorySegment OCSP_resp_get0(MemorySegment bs, int idx) { + var mh$ = OCSP_resp_get0$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_resp_get0", bs, idx); + } + return (MemorySegment) mh$.invokeExact(bs, idx); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_resp_find$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_INT + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_resp_find"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int OCSP_resp_find(OCSP_BASICRESP *bs, OCSP_CERTID *id, int last) + * } + */ + public static int OCSP_resp_find(MemorySegment bs, MemorySegment id, int last) { + var mh$ = OCSP_resp_find$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_resp_find", bs, id, last); + } + return (int) mh$.invokeExact(bs, id, last); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_single_get0_status$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_single_get0_status"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * int OCSP_single_get0_status(OCSP_SINGLERESP *single, int *reason, ASN1_GENERALIZEDTIME **revtime, ASN1_GENERALIZEDTIME **thisupd, ASN1_GENERALIZEDTIME **nextupd) + * } + */ + public static int OCSP_single_get0_status(MemorySegment single, MemorySegment reason, MemorySegment revtime, MemorySegment thisupd, MemorySegment nextupd) { + var mh$ = OCSP_single_get0_status$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_single_get0_status", single, reason, revtime, thisupd, nextupd); + } + return (int) mh$.invokeExact(single, reason, revtime, thisupd, nextupd); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_BASICRESP_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_BASICRESP_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern void OCSP_BASICRESP_free(OCSP_BASICRESP *a) + * } + */ + public static void OCSP_BASICRESP_free(MemorySegment a) { + var mh$ = OCSP_BASICRESP_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_BASICRESP_free", a); + } + mh$.invokeExact(a); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_RESPONSE_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_RESPONSE_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern void OCSP_RESPONSE_free(OCSP_RESPONSE *a) + * } + */ + public static void OCSP_RESPONSE_free(MemorySegment a) { + var mh$ = OCSP_RESPONSE_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_RESPONSE_free", a); + } + mh$.invokeExact(a); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle d2i_OCSP_RESPONSE$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("d2i_OCSP_RESPONSE"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern OCSP_RESPONSE *d2i_OCSP_RESPONSE(OCSP_RESPONSE **a, const unsigned char **in, long len) + * } + */ + public static MemorySegment d2i_OCSP_RESPONSE(MemorySegment a, MemorySegment in, long len) { + var mh$ = d2i_OCSP_RESPONSE$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("d2i_OCSP_RESPONSE", a, in, len); + } + return (MemorySegment) mh$.invokeExact(a, in, len); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_CERTID_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_CERTID_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern void OCSP_CERTID_free(OCSP_CERTID *a) + * } + */ + public static void OCSP_CERTID_free(MemorySegment a) { + var mh$ = OCSP_CERTID_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_CERTID_free", a); + } + mh$.invokeExact(a); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_REQUEST_new$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_REQUEST_new"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern OCSP_REQUEST *OCSP_REQUEST_new(void) + * } + */ + public static MemorySegment OCSP_REQUEST_new() { + var mh$ = OCSP_REQUEST_new$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_REQUEST_new"); + } + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OCSP_REQUEST_free$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OCSP_REQUEST_free"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern void OCSP_REQUEST_free(OCSP_REQUEST *a) + * } + */ + public static void OCSP_REQUEST_free(MemorySegment a) { + var mh$ = OCSP_REQUEST_free$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_REQUEST_free", a); + } + mh$.invokeExact(a); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle i2d_OCSP_REQUEST$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("i2d_OCSP_REQUEST"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * extern int i2d_OCSP_REQUEST(const OCSP_REQUEST *a, unsigned char **out) + * } + */ + public static int i2d_OCSP_REQUEST(MemorySegment a, MemorySegment out) { + var mh$ = i2d_OCSP_REQUEST$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("i2d_OCSP_REQUEST", a, out); + } + return (int) mh$.invokeExact(a, out); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static MethodHandle OSSL_PROVIDER_get0_name$MH() { + class Holder { + static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + static final MethodHandle MH = Linker.nativeLinker().downcallHandle( + openssl_h.findOrThrow("OSSL_PROVIDER_get0_name"), + DESC); + } + return Holder.MH; + } + + /** + * {@snippet lang=c : + * const char *OSSL_PROVIDER_get0_name(const OSSL_PROVIDER *prov) + * } + */ + public static MemorySegment OSSL_PROVIDER_get0_name(MemorySegment prov) { + var mh$ = OSSL_PROVIDER_get0_name$MH(); + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OSSL_PROVIDER_get0_name", prov); + } + return (MemorySegment) mh$.invokeExact(prov); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + /** + * {@snippet lang=c : + * #define OPENSSL_FILE "/tmp/jextract$7967504926277577155.h" + * } + */ + public static MemorySegment OPENSSL_FILE() { + class Holder { + static final MemorySegment OPENSSL_FILE + = openssl_h.LIBRARY_ARENA.allocateFrom("/tmp/jextract$7967504926277577155.h"); + } + return Holder.OPENSSL_FILE; + } + private static final int OPENSSL_LINE = (int)50L; + /** + * {@snippet lang=c : + * #define OPENSSL_LINE 50 + * } + */ + public static int OPENSSL_LINE() { + return OPENSSL_LINE; + } + private static final long OPENSSL_INIT_ENGINE_ALL_BUILTIN = 30208L; + /** + * {@snippet lang=c : + * #define OPENSSL_INIT_ENGINE_ALL_BUILTIN 30208 + * } + */ + public static long OPENSSL_INIT_ENGINE_ALL_BUILTIN() { + return OPENSSL_INIT_ENGINE_ALL_BUILTIN; + } + private static final int EVP_PKEY_NONE = (int)0L; + /** + * {@snippet lang=c : + * #define EVP_PKEY_NONE 0 + * } + */ + public static int EVP_PKEY_NONE() { + return EVP_PKEY_NONE; + } + private static final int EVP_PKEY_RSA = (int)6L; + /** + * {@snippet lang=c : + * #define EVP_PKEY_RSA 6 + * } + */ + public static int EVP_PKEY_RSA() { + return EVP_PKEY_RSA; + } + private static final int EVP_PKEY_DSA = (int)116L; + /** + * {@snippet lang=c : + * #define EVP_PKEY_DSA 116 + * } + */ + public static int EVP_PKEY_DSA() { + return EVP_PKEY_DSA; + } + /** + * {@snippet lang=c : + * #define PEM_STRING_ECPARAMETERS "EC PARAMETERS" + * } + */ + public static MemorySegment PEM_STRING_ECPARAMETERS() { + class Holder { + static final MemorySegment PEM_STRING_ECPARAMETERS + = openssl_h.LIBRARY_ARENA.allocateFrom("EC PARAMETERS"); + } + return Holder.PEM_STRING_ECPARAMETERS; + } + private static final long SSL_OP_NO_TICKET = 16384L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_TICKET 16384 + * } + */ + public static long SSL_OP_NO_TICKET() { + return SSL_OP_NO_TICKET; + } + private static final long SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION = 65536L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION 65536 + * } + */ + public static long SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION() { + return SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; + } + private static final long SSL_OP_NO_COMPRESSION = 131072L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_COMPRESSION 131072 + * } + */ + public static long SSL_OP_NO_COMPRESSION() { + return SSL_OP_NO_COMPRESSION; + } + private static final long SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION = 262144L; + /** + * {@snippet lang=c : + * #define SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION 262144 + * } + */ + public static long SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION() { + return SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION; + } + private static final long SSL_OP_CIPHER_SERVER_PREFERENCE = 4194304L; + /** + * {@snippet lang=c : + * #define SSL_OP_CIPHER_SERVER_PREFERENCE 4194304 + * } + */ + public static long SSL_OP_CIPHER_SERVER_PREFERENCE() { + return SSL_OP_CIPHER_SERVER_PREFERENCE; + } + private static final long SSL_OP_NO_SSLv3 = 33554432L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_SSLv3 33554432 + * } + */ + public static long SSL_OP_NO_SSLv3() { + return SSL_OP_NO_SSLv3; + } + private static final long SSL_OP_NO_TLSv1 = 67108864L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_TLSv1 67108864 + * } + */ + public static long SSL_OP_NO_TLSv1() { + return SSL_OP_NO_TLSv1; + } + private static final long SSL_OP_NO_TLSv1_2 = 134217728L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_TLSv1_2 134217728 + * } + */ + public static long SSL_OP_NO_TLSv1_2() { + return SSL_OP_NO_TLSv1_2; + } + private static final long SSL_OP_NO_TLSv1_1 = 268435456L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_TLSv1_1 268435456 + * } + */ + public static long SSL_OP_NO_TLSv1_1() { + return SSL_OP_NO_TLSv1_1; + } + private static final long SSL_OP_NO_TLSv1_3 = 536870912L; + /** + * {@snippet lang=c : + * #define SSL_OP_NO_TLSv1_3 536870912 + * } + */ + public static long SSL_OP_NO_TLSv1_3() { + return SSL_OP_NO_TLSv1_3; + } + private static final long SSL_OP_ALL = 2147485776L; + /** + * {@snippet lang=c : + * #define SSL_OP_ALL 2147485776 + * } + */ + public static long SSL_OP_ALL() { + return SSL_OP_ALL; + } + private static final int ENGINE_METHOD_ALL = (int)65535L; + /** + * {@snippet lang=c : + * #define ENGINE_METHOD_ALL 65535 + * } + */ + public static int ENGINE_METHOD_ALL() { + return ENGINE_METHOD_ALL; + } +} + diff --git a/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java b/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java new file mode 100644 index 0000000..df2709f --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java @@ -0,0 +1,120 @@ +/* + * 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.tomcat.util.openssl; + +import java.lang.invoke.MethodHandle; +import java.lang.foreign.*; +import static java.lang.foreign.ValueLayout.*; + +/** + * Methods used present in older OpenSSL versions but not in the current major version. + */ +public class openssl_h_Compatibility { + + // OpenSSL 1.1 FIPS_mode + public static int FIPS_mode() { + class Holder { + static final String NAME = "FIPS_mode"; + static final FunctionDescriptor DESC = FunctionDescriptor.of(JAVA_INT); + static final MethodHandle MH = Linker.nativeLinker().downcallHandle(openssl_h.findOrThrow(NAME), DESC); + } + var mh$ = Holder.MH; + try { + if (openssl_h.TRACE_DOWNCALLS) { + openssl_h.traceDowncall(Holder.NAME); + } + return (int) mh$.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + // OpenSSL 1.1 FIPS_mode_set + public static int FIPS_mode_set(int r) { + class Holder { + static final String NAME = "FIPS_mode_set"; + static final FunctionDescriptor DESC = FunctionDescriptor.of(JAVA_INT, JAVA_INT); + static final MethodHandle MH = Linker.nativeLinker().downcallHandle(openssl_h.findOrThrow(NAME), DESC); + } + var mh$ = Holder.MH; + try { + if (openssl_h.TRACE_DOWNCALLS) { + openssl_h.traceDowncall(Holder.NAME, Integer.valueOf(r)); + } + return (int) mh$.invokeExact(r); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + // OpenSSL 1.1 EVP_PKEY_base_id + public static int EVP_PKEY_base_id(MemorySegment pkey) { + class Holder { + static final String NAME = "EVP_PKEY_base_id"; + static final FunctionDescriptor DESC = FunctionDescriptor.of(JAVA_INT, openssl_h.C_POINTER); + static final MethodHandle MH = Linker.nativeLinker().downcallHandle(openssl_h.findOrThrow(NAME), DESC); + } + var mh$ = Holder.MH; + try { + if (openssl_h.TRACE_DOWNCALLS) { + openssl_h.traceDowncall(Holder.NAME, pkey); + } + return (int) mh$.invokeExact(pkey); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + // OpenSSL 1.1 EVP_PKEY_bits + public static int EVP_PKEY_bits(MemorySegment pkey) { + class Holder { + static final String NAME = "EVP_PKEY_bits"; + static final FunctionDescriptor DESC = FunctionDescriptor.of(JAVA_INT, openssl_h.C_POINTER); + static final MethodHandle MH = Linker.nativeLinker().downcallHandle(openssl_h.findOrThrow(NAME), DESC); + } + var mh$ = Holder.MH; + try { + if (openssl_h.TRACE_DOWNCALLS) { + openssl_h.traceDowncall(Holder.NAME, pkey); + } + return (int) mh$.invokeExact(pkey); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + // OpenSSL 1.1 SSL_get_peer_certificate + public static MemorySegment SSL_get_peer_certificate(MemorySegment s) { + class Holder { + static final String NAME = "SSL_get_peer_certificate"; + static final FunctionDescriptor DESC = FunctionDescriptor.of(openssl_h.C_POINTER, openssl_h.C_POINTER); + static final MethodHandle MH = Linker.nativeLinker().downcallHandle(openssl_h.findOrThrow(NAME), DESC); + } + var mh$ = Holder.MH; + try { + if (openssl_h.TRACE_DOWNCALLS) { + openssl_h.traceDowncall(Holder.NAME, s); + } + return (java.lang.foreign.MemorySegment) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + +} + diff --git a/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java b/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java new file mode 100644 index 0000000..9472c31 --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java @@ -0,0 +1,293 @@ +/* + * 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.tomcat.util.openssl; + +import java.lang.foreign.MemorySegment; + +import static org.apache.tomcat.util.openssl.openssl_h.*; + +/** + * Functional macros not handled by jextract. + */ +@SuppressWarnings("javadoc") +public class openssl_h_Macros { + + + /** + * Set maximum protocol version on the given context. + * {@snippet lang = c : # define SSL_CTX_set_max_proto_version(sslCtx, version) \ + * SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_MAX_PROTO_VERSION, version, NULL) + * } + * + * @param sslCtx the SSL context + * @param version the maximum version + * + * @return > 0 if successful + */ + public static long SSL_CTX_set_max_proto_version(MemorySegment sslCtx, long version) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_MAX_PROTO_VERSION(), version, MemorySegment.NULL); + } + + + /** + * Set minimum protocol version on the given context. + * {@snippet lang = c : # define SSL_CTX_set_min_proto_version(sslCtx, version) \ + * SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_MIN_PROTO_VERSION, version, NULL) + * } + * + * @param sslCtx the SSL context + * @param version the maximum version + * + * @return > 0 if successful + */ + public static long SSL_CTX_set_min_proto_version(MemorySegment sslCtx, long version) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_MIN_PROTO_VERSION(), version, MemorySegment.NULL); + } + + + /** + * Get the session cache size. + * {@snippet lang = c : # define SSL_CTX_sess_get_cache_size(sslCtx) \ + * SSL_CTX_ctrl(sslCtx, SSL_CTRL_GET_SESS_CACHE_SIZE, 0, NULL) + * } + * + * @param sslCtx the SSL context + * + * @return > 0 if successful + */ + public static long SSL_CTX_sess_get_cache_size(MemorySegment sslCtx) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_GET_SESS_CACHE_SIZE(), 0, MemorySegment.NULL); + } + + + /** + * Set the session cache size. + * {@snippet lang = c : # define SSL_CTX_sess_set_cache_size(sslCtx, t) \ + * SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_SESS_CACHE_SIZE, t, NULL) + * } + * + * @param sslCtx the SSL context + * @param cacheSize the session cache size + * + * @return > 0 if successful + */ + public static long SSL_CTX_sess_set_cache_size(MemorySegment sslCtx, long cacheSize) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_SESS_CACHE_SIZE(), cacheSize, MemorySegment.NULL); + } + + + /** + * Get the session cache mode. + * {@snippet lang = c : # define SSL_CTX_get_session_cache_mode(sslCtx) \ + * SSL_CTX_ctrl(sslCtx, SSL_CTRL_GET_SESS_CACHE_MODE, 0, NULL) + * } + * + * @param sslCtx the SSL context + * + * @return > 0 if successful + */ + public static long SSL_CTX_get_session_cache_mode(MemorySegment sslCtx) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_GET_SESS_CACHE_MODE(), 0, MemorySegment.NULL); + } + + + /** + * Set the session cache mode. + * {@snippet lang = c : # define SSL_CTX_set_session_cache_mode(sslCtx, m) \ + * SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_SESS_CACHE_MODE, m, NULL) + * } + * + * @param sslCtx the SSL context + * @param cacheMode the cache mode, SSL_SESS_CACHE_OFF to disable + * + * @return > 0 if successful + */ + public static long SSL_CTX_set_session_cache_mode(MemorySegment sslCtx, long cacheMode) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_SESS_CACHE_MODE(), cacheMode, MemorySegment.NULL); + } + + + /** + * Set the certificate. + * {@snippet lang = c : # define SSL_CTX_add0_chain_cert(sslCtx,x509) \ + * SSL_CTX_ctrl(sslCtx, SSL_CTRL_CHAIN_CERT, 0, (char *)(x509)) + * } + * + * @param sslCtx the SSL context + * @param x509 the certificate + * + * @return > 0 if successful + */ + public static long SSL_CTX_add0_chain_cert(MemorySegment sslCtx, MemorySegment x509) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_CHAIN_CERT(), 0, x509); + } + + + /** + * Set ticket keys. + * {@snippet lang = c : # define SSL_CTX_set_tlsext_ticket_keys(ctx, keys, keylen) \ + * SSL_CTX_ctrl((ctx),SSL_CTRL_SET_TLSEXT_TICKET_KEYS, (keylen), (keys)) + * } + * + * @param sslCtx the SSL context + * @param keys the keys + * @param keyLength the length + * + * @return > 0 if successful + */ + public static long SSL_CTX_set_tlsext_ticket_keys(MemorySegment sslCtx, MemorySegment keys, long keyLength) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_TLSEXT_TICKET_KEYS(), keyLength, keys); + } + + + /** + * Read the specified file. + * {@snippet lang = c : # define BIO_read_filename(b,name) \ + * (int)BIO_ctrl(b,BIO_C_SET_FILENAME,BIO_CLOSE|BIO_FP_READ,(char *)(name)) + * } + * + * @param bio The BIO to read into + * @param name the file name + * + * @return > 0 if successful + */ + public static long BIO_read_filename(MemorySegment bio, MemorySegment name) { + return BIO_ctrl(bio, BIO_C_SET_FILENAME(), BIO_CLOSE() | BIO_FP_READ(), name); + } + + + /** + * Set tmp dh. + * {@snippet lang = c : # define SSL_CTX_set_tmp_dh(sslCtx,dh) \ + * SSL_CTX_ctrl(sslCtx,SSL_CTRL_SET_TMP_DH,0,(char *)(dh)) + * } + * + * @param sslCtx the SSL context + * @param dh the dh + * + * @return > 0 if successful + */ + public static long SSL_CTX_set_tmp_dh(MemorySegment sslCtx, MemorySegment dh) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_TMP_DH(), 0, dh); + } + + + /** + * Set tmp ecdh. + * {@snippet lang = c : # define SSL_CTX_set_tmp_ecdh(sslCtx,ecdh) \ + * SSL_CTX_ctrl(sslCtx,SSL_CTRL_SET_TMP_ECDH,0,(char *)(ecdh)) + * } + * + * @param sslCtx the SSL context + * @param ecdh the ecdh + * + * @return > 0 if successful + */ + public static long SSL_CTX_set_tmp_ecdh(MemorySegment sslCtx, MemorySegment ecdh) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_TMP_ECDH(), 0, ecdh); + } + + + /** + * Free memory. + * {@snippet lang = c : # define OPENSSL_free(addr) CRYPTO_free(addr, OPENSSL_FILE, OPENSSL_LINE) + * } + * + * @param segment The address to free + */ + public static void OPENSSL_free(MemorySegment segment) { + CRYPTO_free(segment, MemorySegment.NULL, 0); + } + + + /** + * Reset BIO position. + * {@snippet lang = c : # define BIO_reset(b) (int)BIO_ctrl(b,BIO_CTRL_RESET,0,NULL) + * } + * + * @param bio The BIO to reset + * + * @return > 0 if successful + */ + public static long BIO_reset(MemorySegment bio) { + return BIO_ctrl(bio, BIO_CTRL_RESET(), 0, MemorySegment.NULL); + } + + + /** + * Set NIDs of groups in preference order. + * {@snippet lang = c : # define SSL_CTX_set1_curves SSL_CTX_set1_groups + * # define SSL_CTX_set1_groups(ctx, glist, glistlen) \ + * SSL_CTX_ctrl(ctx,SSL_CTRL_SET_GROUPS,glistlen,(int *)(glist)) + * } + * + * @param sslCtx the SSL context + * @param groupsList the groups list + * @param listLength the list length + * + * @return > 0 if successful + */ + public static long SSL_CTX_set1_groups(MemorySegment sslCtx, MemorySegment groupsList, int listLength) { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_GROUPS(), listLength, groupsList); + } + + + /** + * Pass a path from which certificates are loaded into the store. + * {@snippet lang = c : # define X509_LOOKUP_add_dir(x,name,type) \ + * X509_LOOKUP_ctrl((x),X509_L_ADD_DIR,(name),(long)(type),NULL) + * } + * + * @param x509Lookup the X509 lookup + * @param name the path name + * @param type the type used + * + * @return > 0 if successful + */ + public static long X509_LOOKUP_add_dir(MemorySegment x509Lookup, MemorySegment name, long type) { + return X509_LOOKUP_ctrl(x509Lookup, X509_L_ADD_DIR(), name, X509_FILETYPE_PEM(), MemorySegment.NULL); + } + + + /** + * Pass a file which will be loaded into the store. + * {@snippet lang = c : # define X509_LOOKUP_load_file(x,name,type) \ + * X509_LOOKUP_ctrl((x),X509_L_FILE_LOAD,(name),(long)(type),NULL) + * } + * + * @param x509Lookup the X509 lookup + * @param name the path name + * @param type the type used + * + * @return > 0 if successful + */ + public static long X509_LOOKUP_load_file(MemorySegment x509Lookup, MemorySegment name, long type) { + return X509_LOOKUP_ctrl(x509Lookup, X509_L_FILE_LOAD(), name, X509_FILETYPE_PEM(), MemorySegment.NULL); + } + + + /** + * Return the d2i_ECPKParameters symbol. + * @return the symbol + */ + public static MemorySegment d2i_ECPKParameters$SYMBOL() { + return openssl_h.findOrThrow("d2i_ECPKParameters"); + } + +} + diff --git a/java/org/apache/tomcat/util/openssl/pem_password_cb.java b/java/org/apache/tomcat/util/openssl/pem_password_cb.java new file mode 100644 index 0000000..888c3c3 --- /dev/null +++ b/java/org/apache/tomcat/util/openssl/pem_password_cb.java @@ -0,0 +1,81 @@ +/* + * 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. + */ + +// Generated by jextract + +package org.apache.tomcat.util.openssl; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.invoke.MethodHandle; + +/** + * {@snippet lang=c : + * typedef int (pem_password_cb)(char *, int, int, void *) + * } + */ +@SuppressWarnings("javadoc") +public class pem_password_cb { + + /** + * The function pointer signature, expressed as a functional interface + */ + public interface Function { + int apply(MemorySegment buf, int size, int rwflag, MemorySegment userdata); + } + + private static final FunctionDescriptor $DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_INT, + openssl_h.C_INT, + openssl_h.C_POINTER + ); + + /** + * The descriptor of this function pointer + */ + public static FunctionDescriptor descriptor() { + return $DESC; + } + + private static final MethodHandle UP$MH = openssl_h.upcallHandle(pem_password_cb.Function.class, "apply", $DESC); + + /** + * Allocates a new upcall stub, whose implementation is defined by {@code fi}. + * The lifetime of the returned segment is managed by {@code arena} + */ + public static MemorySegment allocate(pem_password_cb.Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena); + } + + private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC); + + /** + * Invoke the upcall stub {@code funcPtr}, with given parameters + */ + public static int invoke(MemorySegment funcPtr,MemorySegment buf, int size, int rwflag, MemorySegment userdata) { + try { + return (int) DOWN$MH.invokeExact(funcPtr, buf, size, rwflag, userdata); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } +} + diff --git a/java/org/apache/tomcat/util/res/StringManager.java b/java/org/apache/tomcat/util/res/StringManager.java new file mode 100644 index 0000000..78d05f9 --- /dev/null +++ b/java/org/apache/tomcat/util/res/StringManager.java @@ -0,0 +1,269 @@ +/* + * 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.tomcat.util.res; + +import java.text.MessageFormat; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +/** + * An internationalization / localization helper class which reduces the bother of handling ResourceBundles and takes + * care of the common cases of message formatting which otherwise require the creation of Object arrays and such. + *

    + * The StringManager operates on a package basis. One StringManager per package can be created and accessed via the + * getManager method call. + *

    + * The StringManager will look for a ResourceBundle named by the package name given plus the suffix of "LocalStrings". + * In practice, this means that the localized information will be contained in a LocalStrings.properties file located in + * the package directory of the class path. + *

    + * Please see the documentation for java.util.ResourceBundle for more information. + * + * @author James Duncan Davidson [duncan@eng.sun.com] + * @author James Todd [gonzo@eng.sun.com] + * @author Mel Martinez [mmartinez@g1440.com] + * + * @see java.util.ResourceBundle + */ +public class StringManager { + + private static int LOCALE_CACHE_SIZE = 10; + + /** + * The ResourceBundle for this StringManager. + */ + private final ResourceBundle bundle; + private final Locale locale; + + + /** + * Creates a new StringManager for a given package. This is a private method and all access to it is arbitrated by + * the static getManager method call so that only one StringManager per package will be created. + * + * @param packageName Name of package to create StringManager for. + */ + private StringManager(String packageName, Locale locale) { + String bundleName = packageName + ".LocalStrings"; + ResourceBundle bnd = null; + try { + // The ROOT Locale uses English. If English is requested, force the + // use of the ROOT Locale else incorrect results may be obtained if + // the system default locale is not English and translations are + // available for the system default locale. + if (locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { + locale = Locale.ROOT; + } + bnd = ResourceBundle.getBundle(bundleName, locale); + } catch (MissingResourceException ex) { + // Try from the current loader (that's the case for trusted apps) + // Should only be required if using a TC5 style classloader structure + // where common != shared != server + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl != null) { + try { + bnd = ResourceBundle.getBundle(bundleName, locale, cl); + } catch (MissingResourceException ex2) { + // Ignore + } + } + } + bundle = bnd; + // Get the actual locale, which may be different from the requested one + if (bundle != null) { + Locale bundleLocale = bundle.getLocale(); + if (bundleLocale.equals(Locale.ROOT)) { + this.locale = Locale.ENGLISH; + } else { + this.locale = bundleLocale; + } + } else { + this.locale = null; + } + } + + + /** + * Get a string from the underlying resource bundle or return null if the String is not found. + * + * @param key to desired resource String + * + * @return resource String matching key from underlying bundle or null if not found. + * + * @throws IllegalArgumentException if key is null + */ + public String getString(String key) { + if (key == null) { + String msg = "key may not have a null value"; + throw new IllegalArgumentException(msg); + } + + String str = null; + + try { + // Avoid NPE if bundle is null and treat it like an MRE + if (bundle != null) { + str = bundle.getString(key); + } + } catch (MissingResourceException mre) { + // bad: shouldn't mask an exception the following way: + // str = "[cannot find message associated with key '" + key + + // "' due to " + mre + "]"; + // because it hides the fact that the String was missing + // from the calling code. + // good: could just throw the exception (or wrap it in another) + // but that would probably cause much havoc on existing + // code. + // better: consistent with container pattern to + // simply return null. Calling code can then do + // a null check. + str = null; + } + + return str; + } + + + /** + * Get a string from the underlying resource bundle and format it with the given set of arguments. + * + * @param key The key for the required message + * @param args The values to insert into the message + * + * @return The request string formatted with the provided arguments or the key if the key was not found. + */ + public String getString(final String key, final Object... args) { + String value = getString(key); + if (value == null) { + value = key; + } + + MessageFormat mf = new MessageFormat(value); + mf.setLocale(locale); + return mf.format(args, new StringBuffer(), null).toString(); + } + + + /** + * Identify the Locale this StringManager is associated with. + * + * @return The Locale associated with the StringManager + */ + public Locale getLocale() { + return locale; + } + + + // -------------------------------------------------------------- + // STATIC SUPPORT METHODS + // -------------------------------------------------------------- + + private static final Map> managers = new HashMap<>(); + + + /** + * Get the StringManager for a given class. The StringManager will be returned for the package in which the class is + * located. If a manager for that package already exists, it will be reused, else a new StringManager will be + * created and returned. + * + * @param clazz The class for which to retrieve the StringManager + * + * @return The instance associated with the package of the provide class + */ + public static final StringManager getManager(Class clazz) { + return getManager(clazz.getPackage().getName()); + } + + + /** + * Get the StringManager for a particular package. If a manager for a package already exists, it will be reused, + * else a new StringManager will be created and returned. + * + * @param packageName The package name + * + * @return The instance associated with the given package and the default Locale + */ + public static final StringManager getManager(String packageName) { + return getManager(packageName, Locale.getDefault()); + } + + + /** + * Get the StringManager for a particular package and Locale. If a manager for a package/Locale combination already + * exists, it will be reused, else a new StringManager will be created and returned. + * + * @param packageName The package name + * @param locale The Locale + * + * @return The instance associated with the given package and Locale + */ + public static final synchronized StringManager getManager(String packageName, Locale locale) { + + Map map = managers.get(packageName); + if (map == null) { + /* + * Don't want the HashMap size to exceed LOCALE_CACHE_SIZE. Expansion occurs when size() exceeds capacity. + * Therefore keep size at or below capacity. removeEldestEntry() executes after insertion therefore the test + * for removal needs to use one less than the maximum desired size. Note this is an LRU cache. + */ + map = new LinkedHashMap<>(LOCALE_CACHE_SIZE, 0.75f, true) { + private static final long serialVersionUID = 1L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > (LOCALE_CACHE_SIZE - 1)) { + return true; + } + return false; + } + }; + managers.put(packageName, map); + } + + StringManager mgr = map.get(locale); + if (mgr == null) { + mgr = new StringManager(packageName, locale); + map.put(locale, mgr); + } + return mgr; + } + + + /** + * Retrieve the StringManager for a list of Locales. The first StringManager found will be returned. + * + * @param packageName The package for which the StringManager was requested + * @param requestedLocales The list of Locales + * + * @return the found StringManager or the default StringManager + */ + public static StringManager getManager(String packageName, Enumeration requestedLocales) { + while (requestedLocales.hasMoreElements()) { + Locale locale = requestedLocales.nextElement(); + StringManager result = getManager(packageName, locale); + if (result.getLocale().equals(locale)) { + return result; + } + } + // Return the default + return getManager(packageName); + } +} diff --git a/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java b/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java new file mode 100644 index 0000000..fa685ba --- /dev/null +++ b/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java @@ -0,0 +1,284 @@ +/* + * 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.tomcat.util.scan; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +import org.apache.tomcat.Jar; + +/** + * Base implementation of Jar for implementations that use a JarInputStream to + * access the JAR file. + */ +public abstract class AbstractInputStreamJar implements Jar { + + private final URL jarFileURL; + + private NonClosingJarInputStream jarInputStream = null; + private JarEntry entry = null; + private Boolean multiRelease = null; + private Map mrMap = null; + + public AbstractInputStreamJar(URL jarFileUrl) { + this.jarFileURL = jarFileUrl; + } + + + @Override + public URL getJarFileURL() { + return jarFileURL; + } + + + @Override + public void nextEntry() { + if (jarInputStream == null) { + try { + reset(); + } catch (IOException e) { + entry = null; + return; + } + } + try { + entry = jarInputStream.getNextJarEntry(); + if (multiRelease.booleanValue()) { + // Skip base entries where there is a multi-release entry + // Skip multi-release entries that are not being used + while (entry != null && + (mrMap.containsKey(entry.getName()) || + entry.getName().startsWith("META-INF/versions/") && + !mrMap.containsValue(entry.getName()))) { + entry = jarInputStream.getNextJarEntry(); + } + } else { + // Skip multi-release entries + while (entry != null && entry.getName().startsWith("META-INF/versions/")) { + entry = jarInputStream.getNextJarEntry(); + } + } + } catch (IOException ioe) { + entry = null; + } + } + + + @Override + public String getEntryName() { + // Given how the entry name is used, there is no requirement to convert + // the name for a multi-release entry to the corresponding base name. + if (entry == null) { + return null; + } else { + return entry.getName(); + } + } + + + @Override + public InputStream getEntryInputStream() throws IOException { + return jarInputStream; + } + + + @Override + public InputStream getInputStream(String name) throws IOException { + gotoEntry(name); + if (entry == null) { + return null; + } else { + // Clear the entry so that multiple calls to this method for the + // same entry will result in a new InputStream for each call + // (BZ 60798) + entry = null; + return jarInputStream; + } + } + + + @Override + public long getLastModified(String name) throws IOException { + gotoEntry(name); + if (entry == null) { + return -1; + } else { + return entry.getTime(); + } + } + + + @Override + public boolean exists(String name) throws IOException { + gotoEntry(name); + return entry != null; + } + + + @Override + public String getURL(String entry) { + StringBuilder result = new StringBuilder("jar:"); + result.append(getJarFileURL().toExternalForm()); + result.append("!/"); + result.append(entry); + + return result.toString(); + } + + + @Override + public Manifest getManifest() throws IOException { + reset(); + return jarInputStream.getManifest(); + } + + + @Override + public void reset() throws IOException { + closeStream(); + entry = null; + jarInputStream = createJarInputStream(); + // Only perform multi-release processing on first access + if (multiRelease == null) { + Manifest manifest = jarInputStream.getManifest(); + if (manifest == null) { + multiRelease = Boolean.FALSE; + } else { + String mrValue = manifest.getMainAttributes().getValue("Multi-Release"); + if (mrValue == null) { + multiRelease = Boolean.FALSE; + } else { + multiRelease = Boolean.valueOf(mrValue); + } + } + if (multiRelease.booleanValue()) { + if (mrMap == null) { + populateMrMap(); + } + } + } + } + + + protected void closeStream() { + if (jarInputStream != null) { + try { + jarInputStream.reallyClose(); + } catch (IOException ioe) { + // Ignore + } + } + } + + + protected abstract NonClosingJarInputStream createJarInputStream() throws IOException; + + + private void gotoEntry(String name) throws IOException { + boolean needsReset = true; + if (multiRelease == null) { + reset(); + needsReset = false; + } + + // Need to convert requested name to multi-release name (if one exists) + if (multiRelease.booleanValue()) { + String mrName = mrMap.get(name); + if (mrName != null) { + name = mrName; + } + } else if (name.startsWith("META-INF/versions/")) { + entry = null; + return; + } + + if (entry != null && name.equals(entry.getName())) { + return; + } + if (needsReset) { + reset(); + } + + JarEntry jarEntry = jarInputStream.getNextJarEntry(); + while (jarEntry != null) { + if (name.equals(jarEntry.getName())) { + entry = jarEntry; + break; + } + jarEntry = jarInputStream.getNextJarEntry(); + } + } + + + private void populateMrMap() throws IOException { + int targetVersion = Runtime.version().feature(); + + Map mrVersions = new HashMap<>(); + + JarEntry jarEntry = jarInputStream.getNextJarEntry(); + + // Tracking the base name and the latest valid version found is + // sufficient to be able to create the renaming map required + while (jarEntry != null) { + String name = jarEntry.getName(); + if (name.startsWith("META-INF/versions/") && name.endsWith(".class")) { + + // Get the base name and version for this versioned entry + int i = name.indexOf('/', 18); + if (i > 0) { + String baseName = name.substring(i + 1); + int version = Integer.parseInt(name.substring(18, i)); + + // Ignore any entries targeting for a later version than + // the target for this runtime + if (version <= targetVersion) { + Integer mappedVersion = mrVersions.get(baseName); + if (mappedVersion == null) { + // No version found for this name. Create one. + mrVersions.put(baseName, Integer.valueOf(version)); + } else { + // Ignore any entry for which we have already found + // a later version + if (version > mappedVersion.intValue()) { + // Replace the earlier version + mrVersions.put(baseName, Integer.valueOf(version)); + } + } + } + } + } + jarEntry = jarInputStream.getNextJarEntry(); + } + + mrMap = new HashMap<>(); + + for (Entry mrVersion : mrVersions.entrySet()) { + mrMap.put(mrVersion.getKey() , "META-INF/versions/" + mrVersion.getValue().toString() + + "/" + mrVersion.getKey()); + } + + // Reset stream back to the beginning of the JAR + closeStream(); + jarInputStream = createJarInputStream(); + } +} diff --git a/java/org/apache/tomcat/util/scan/Constants.java b/java/org/apache/tomcat/util/scan/Constants.java new file mode 100644 index 0000000..1edd13e --- /dev/null +++ b/java/org/apache/tomcat/util/scan/Constants.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.util.scan; + +/** + * String constants for the scan package. + */ +public final class Constants { + + public static final String Package = "org.apache.tomcat.util.scan"; + + /* System properties */ + public static final String SKIP_JARS_PROPERTY = + "tomcat.util.scan.StandardJarScanFilter.jarsToSkip"; + public static final String SCAN_JARS_PROPERTY = + "tomcat.util.scan.StandardJarScanFilter.jarsToScan"; + + /* Commons strings */ + public static final String JAR_EXT = ".jar"; + public static final String WEB_INF_LIB = "/WEB-INF/lib/"; + public static final String WEB_INF_CLASSES = "/WEB-INF/classes"; +} diff --git a/java/org/apache/tomcat/util/scan/JarFactory.java b/java/org/apache/tomcat/util/scan/JarFactory.java new file mode 100644 index 0000000..51c0011 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/JarFactory.java @@ -0,0 +1,71 @@ +/* + * 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.tomcat.util.scan; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.regex.Matcher; + +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.buf.UriUtil; + +/** + * Provide a mechanism to obtain objects that implement {@link Jar}. + */ +public class JarFactory { + + private JarFactory() { + // Factory class. Hide public constructor. + } + + + public static Jar newInstance(URL url) throws IOException { + String urlString = url.toString(); + if (urlString.startsWith("jar:file:")) { + if (urlString.endsWith("!/")) { + return new JarFileUrlJar(url, true); + } else { + return new JarFileUrlNestedJar(url); + } + } else if (urlString.startsWith("war:file:")) { + URL jarUrl = UriUtil.warToJar(url); + return new JarFileUrlNestedJar(jarUrl); + } else if (urlString.startsWith("file:")) { + return new JarFileUrlJar(url, false); + } else { + return new UrlJar(url); + } + } + + + public static URL getJarEntryURL(URL baseUrl, String entryName) + throws MalformedURLException { + + String baseExternal = baseUrl.toExternalForm(); + + if (baseExternal.startsWith("jar")) { + // Assume this is pointing to a JAR file within a WAR. Java doesn't + // support jar:jar:file:... so switch to Tomcat's war:file:... + baseExternal = baseExternal.replaceFirst("^jar:", "war:"); + baseExternal = baseExternal.replaceFirst("!/", + Matcher.quoteReplacement(UriUtil.getWarSeparator())); + } + + return new URL("jar:" + baseExternal + "!/" + entryName); + } +} diff --git a/java/org/apache/tomcat/util/scan/JarFileUrlJar.java b/java/org/apache/tomcat/util/scan/JarFileUrlJar.java new file mode 100644 index 0000000..b482f45 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/JarFileUrlJar.java @@ -0,0 +1,224 @@ +/* + * 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.tomcat.util.scan; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.apache.tomcat.Jar; + +/** + * Implementation of {@link Jar} that is optimised for file based JAR URLs that + * refer directly to a JAR file (e.g URLs of the form jar:file: ... .jar!/ or + * file:... .jar). + */ +public class JarFileUrlJar implements Jar { + + private final JarFile jarFile; + private final URL jarFileURL; + private final boolean multiRelease; + private Enumeration entries; + private Set entryNamesSeen; + private JarEntry entry = null; + + public JarFileUrlJar(URL url, boolean startsWithJar) throws IOException { + if (startsWithJar) { + // jar:file:... + JarURLConnection jarConn = (JarURLConnection) url.openConnection(); + jarConn.setUseCaches(false); + jarFile = jarConn.getJarFile(); + jarFileURL = jarConn.getJarFileURL(); + } else { + // file:... + File f; + try { + f = new File(url.toURI()); + } catch (URISyntaxException e) { + throw new IOException(e); + } + jarFile = new JarFile(f, true, ZipFile.OPEN_READ, Runtime.version()); + jarFileURL = url; + } + boolean multiReleaseValue = false; + try { + multiReleaseValue = jarFile.isMultiRelease(); + } catch (IllegalStateException e) { + // ISE can be thrown if the JAR URL is bad, for example: + // https://github.com/spring-projects/spring-boot/issues/33633 + // The Javadoc does not document that ISE and given what it does for a vanilla IOE, + // this looks like a Java bug, it should return false instead. + } + multiRelease = multiReleaseValue; + } + + + @Override + public URL getJarFileURL() { + return jarFileURL; + } + + + @Override + public InputStream getInputStream(String name) throws IOException { + // JarFile#getEntry() is multi-release aware + ZipEntry entry = jarFile.getEntry(name); + if (entry == null) { + return null; + } else { + return jarFile.getInputStream(entry); + } + } + + @Override + public long getLastModified(String name) throws IOException { + // JarFile#getEntry() is multi-release aware + ZipEntry entry = jarFile.getEntry(name); + if (entry == null) { + return -1; + } else { + return entry.getTime(); + } + } + + @Override + public boolean exists(String name) throws IOException { + // JarFile#getEntry() is multi-release aware + ZipEntry entry = jarFile.getEntry(name); + return entry != null; + } + + @Override + public String getURL(String entry) { + StringBuilder result = new StringBuilder("jar:"); + result.append(getJarFileURL().toExternalForm()); + result.append("!/"); + result.append(entry); + + return result.toString(); + } + + @Override + public void close() { + if (jarFile != null) { + try { + jarFile.close(); + } catch (IOException e) { + // Ignore + } + } + } + + @Override + public void nextEntry() { + // JarFile#entries() is NOT multi-release aware + if (entries == null) { + entries = jarFile.entries(); + if (multiRelease) { + entryNamesSeen = new HashSet<>(); + } + } + + if (multiRelease) { + // Need to ensure that: + // - the one, correct entry is returned where multiple versions + // are available + // - that the order of entries in the JAR doesn't prevent the + // correct entries being returned + // - the case where an entry appears in the versions location + // but not in the the base location is handled correctly + + // Enumerate the entries until one is reached that represents an + // entry that has not been seen before. + String name = null; + while (true) { + if (entries.hasMoreElements()) { + entry = entries.nextElement(); + name = entry.getName(); + // Get 'base' name + if (name.startsWith("META-INF/versions/")) { + int i = name.indexOf('/', 18); + if (i == -1) { + continue; + } + name = name.substring(i + 1); + } + if (name.length() == 0 || entryNamesSeen.contains(name)) { + continue; + } + + entryNamesSeen.add(name); + + // JarFile.getJarEntry is version aware so use it + entry = jarFile.getJarEntry(entry.getName()); + break; + } else { + entry = null; + break; + } + } + } else { + if (entries.hasMoreElements()) { + entry = entries.nextElement(); + } else { + entry = null; + } + } + } + + @Override + public String getEntryName() { + if (entry == null) { + return null; + } else { + return entry.getName(); + } + } + + @Override + public InputStream getEntryInputStream() throws IOException { + if (entry == null) { + return null; + } else { + return jarFile.getInputStream(entry); + } + } + + @Override + public Manifest getManifest() throws IOException { + return jarFile.getManifest(); + } + + + @Override + public void reset() throws IOException { + entries = null; + entryNamesSeen = null; + entry = null; + } +} diff --git a/java/org/apache/tomcat/util/scan/JarFileUrlNestedJar.java b/java/org/apache/tomcat/util/scan/JarFileUrlNestedJar.java new file mode 100644 index 0000000..0eca7f7 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/JarFileUrlNestedJar.java @@ -0,0 +1,65 @@ +/* + * 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.tomcat.util.scan; + +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Implementation of {@link org.apache.tomcat.Jar} that is optimised for file + * based JAR URLs that refer to a JAR file nested inside a WAR + * (e.g URLs of the form jar:file: ... .war!/ ... .jar). + */ +public class JarFileUrlNestedJar extends AbstractInputStreamJar { + + private final JarFile warFile; + private final JarEntry jarEntry; + + public JarFileUrlNestedJar(URL url) throws IOException { + super(url); + JarURLConnection jarConn = (JarURLConnection) url.openConnection(); + jarConn.setUseCaches(false); + warFile = jarConn.getJarFile(); + + String urlAsString = url.toString(); + int pathStart = urlAsString.indexOf("!/") + 2; + String jarPath = urlAsString.substring(pathStart); + jarEntry = warFile.getJarEntry(jarPath); + } + + + @Override + public void close() { + closeStream(); + if (warFile != null) { + try { + warFile.close(); + } catch (IOException e) { + // Ignore + } + } + } + + + @Override + protected NonClosingJarInputStream createJarInputStream() throws IOException { + return new NonClosingJarInputStream(warFile.getInputStream(jarEntry)); + } +} diff --git a/java/org/apache/tomcat/util/scan/LocalStrings.properties b/java/org/apache/tomcat/util/scan/LocalStrings.properties new file mode 100644 index 0000000..58d659c --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings.properties @@ -0,0 +1,27 @@ +# 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. + +jarScan.classPath.badEntry=The class path entry [{0}] could not be converted to a URL +jarScan.classloaderFail=Failed to scan [{0}] from classloader hierarchy +jarScan.classloaderJarNoScan=Not performing JAR scanning on file [{0}] from classpath +jarScan.classloaderJarScan=Scanning JAR [{0}] from classpath +jarScan.classloaderStart=Scanning for JARs in classloader hierarchy +jarScan.invalidModuleUri=The module URI provided [{0}] could not be converted to a URL for the JarScanner to process +jarScan.jarUrlStart=Scanning JAR at URL [{0}] +jarScan.webinfclassesFail=Failed to scan /WEB-INF/classes +jarScan.webinflibFail=Failed to scan JAR [{0}] from /WEB-INF/lib +jarScan.webinflibJarNoScan=Not performing JAR scanning on file [{0}] from /WEB-INF/lib +jarScan.webinflibJarScan=Scanning JAR [{0}] from /WEB-INF/lib +jarScan.webinflibStart=Scanning /WEB-INF/lib for JARs diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_cs.properties b/java/org/apache/tomcat/util/scan/LocalStrings_cs.properties new file mode 100644 index 0000000..5c319c2 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_cs.properties @@ -0,0 +1,19 @@ +# 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. + +jarScan.classPath.badEntry=Classpath záznam [{0}] se nepodaÅ™ilo pÅ™evést na URL +jarScan.classloaderFail=Selhalo skenování [{0}] z hierarchie classloaderu +jarScan.classloaderJarNoScan=Skenování JAR souboru [{0}] na classpath neprovedeno +jarScan.webinflibJarNoScan=Neprovádí se skenování JAR souboru [{0}] z /WEB-INF/lib diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_de.properties b/java/org/apache/tomcat/util/scan/LocalStrings_de.properties new file mode 100644 index 0000000..1a2ac71 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_de.properties @@ -0,0 +1,19 @@ +# 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. + +jarScan.classPath.badEntry=Der Classpath Eintrag [{0}] konnte nicht in eine URL gewandelt werden +jarScan.classloaderFail=Fehler beim Scannen von [{0}] aus der Classloader Historie +jarScan.classloaderJarNoScan=Werde auf Datei [{0}] aus dem Classpath kein JAR Scanning durchführen +jarScan.webinflibStart=Durchsuche /WEB-INF/lib nach JARs diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_es.properties b/java/org/apache/tomcat/util/scan/LocalStrings_es.properties new file mode 100644 index 0000000..dee583f --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_es.properties @@ -0,0 +1,21 @@ +# 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. + +jarScan.classPath.badEntry=El camino de la clase entrada [{0}] no se puede convertir en una URL +jarScan.classloaderFail=Fallo al escanear [{0}] desde la herarquia classloader hierarchy +jarScan.classloaderJarNoScan=No se ejecutó el escaneo de JAR en el archivo [{0}] de la ruta de clase +jarScan.invalidModuleUri=El módulo URI proveído [{0}] no pudo ser convertiod a una URL para ser procesado por JarScanner +jarScan.webinflibJarNoScan=No se ejecutó escaneo JAR en el archivo [{0}] de /WEB-INF/lib\n +jarScan.webinflibStart=Buscando JARs en /WEB-INF/lib diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_fr.properties b/java/org/apache/tomcat/util/scan/LocalStrings_fr.properties new file mode 100644 index 0000000..abe6dce --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_fr.properties @@ -0,0 +1,27 @@ +# 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. + +jarScan.classPath.badEntry=L''entrée "class path" [{0}] n''a pas pu être convertie en URL. +jarScan.classloaderFail=Echec de recherche dans [{0}] de la hiérarchie de chargeurs de classes +jarScan.classloaderJarNoScan=Le JAR [{0}] dans le chemin de classes ne sera pas analysé +jarScan.classloaderJarScan=Analyse du JAR [{0}] du chemin de classes +jarScan.classloaderStart=Recherche dans les JARs de la hiérarchie de chargeurs de classe +jarScan.invalidModuleUri=L''URI du module fournie [{0}] n''a pas pu être convertie en URL pour être traitée par le JarScanner +jarScan.jarUrlStart=Recherche dans le JAR à l''URL [{0}] +jarScan.webinfclassesFail=Impossible de parcourir /WEB-INF/classes +jarScan.webinflibFail=Échec de scan du JAR [{0}] de /WEB-INF/lib +jarScan.webinflibJarNoScan=Le JAR [{0}] dans /WEB-INF/lib ne sera pas analysé +jarScan.webinflibJarScan=Analyse du JAR [{0}] dans /WEB-INF/lib +jarScan.webinflibStart=Recherche de JARs dans /WEB-INF/lib diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_ja.properties b/java/org/apache/tomcat/util/scan/LocalStrings_ja.properties new file mode 100644 index 0000000..96864b1 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_ja.properties @@ -0,0 +1,27 @@ +# 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. + +jarScan.classPath.badEntry=クラスパスã®ã‚¨ãƒ³ãƒˆãƒª [{0}] ã‚’ URL å½¢å¼ã«å¤‰æ›ã§ãã¾ã›ã‚“ã§ã—㟠+jarScan.classloaderFail=クラスローダー階層ã‹ã‚‰ [{0}] をスキャンã§ãã¾ã›ã‚“ã§ã—㟠+jarScan.classloaderJarNoScan=クラスパス中ã®ãƒ•ã‚¡ã‚¤ãƒ« [{0}] 㯠JAR スキャンをã—ã¾ã›ã‚“ã§ã—ãŸã€‚ +jarScan.classloaderJarScan=クラスパス㮠JAR ファイル [{0}] をスキャンã—ã¾ã™ã€‚ +jarScan.classloaderStart=クラスローダー階層中㮠JAR ファイルをスキャンã—ã¾ã™ã€‚ +jarScan.invalidModuleUri=モジュール URI [{0}] ã‚’ JarScanner ã§å‡¦ç†ã™ã‚‹ URL ã«å¤‰æ›ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +jarScan.jarUrlStart=URL [{0}] ã® JAR ファイルをスキャンã—ã¾ã™ã€‚ +jarScan.webinfclassesFail=/WEB-INF/classesã®ã‚¹ã‚­ãƒ£ãƒ³ã«å¤±æ•—ã—ã¾ã—㟠+jarScan.webinflibFail=/WEB-INF/libã‹ã‚‰JAR [{0}]をスキャンã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +jarScan.webinflibJarNoScan=/WEB-INF/lib ã®ãƒ•ã‚¡ã‚¤ãƒ« [{0}] 㯠JAR スキャンãŒè¡Œã‚ã‚Œã¾ã›ã‚“ã§ã—㟠+jarScan.webinflibJarScan=/WEB-INF/libã‹ã‚‰JAR [{0}]ã®ã‚¹ã‚­ãƒ£ãƒ³ã‚’è¡Œã„ã¾ã™ã€‚ +jarScan.webinflibStart=/WEB-INF/lib ã® JAR ファイルを検査ã—ã¦ã„ã¾ã™ã€‚ diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_ko.properties b/java/org/apache/tomcat/util/scan/LocalStrings_ko.properties new file mode 100644 index 0000000..7b4768c --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_ko.properties @@ -0,0 +1,27 @@ +# 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. + +jarScan.classPath.badEntry=í´ëž˜ìŠ¤íŒ¨ìŠ¤ 엔트리 [{0}]ì€(는) URLë¡œ ë³€í™˜ë  ìˆ˜ 없었습니다. +jarScan.classloaderFail=í´ëž˜ìŠ¤ë¡œë” 계층구조로부터 [{0}]ì„(를) 스캔하지 못했습니다. +jarScan.classloaderJarNoScan=í´ëž˜ìŠ¤íŒ¨ìŠ¤ë¡œë¶€í„° íŒŒì¼ [{0}]ì— ëŒ€í•œ JAR ìŠ¤ìº”ì„ ìˆ˜í–‰í•˜ì§€ 않습니다. +jarScan.classloaderJarScan=í´ëž˜ìŠ¤íŒ¨ìŠ¤ë¡œë¶€í„° JAR [{0}]ì„(를) 스캔합니다. +jarScan.classloaderStart=í´ëž˜ìŠ¤ë¡œë” 계층 구조ì—ì„œ JARë“¤ì„ ìŠ¤ìº”í•©ë‹ˆë‹¤. +jarScan.invalidModuleUri=[{0}](으)ë¡œ ì œê³µëœ ëª¨ë“ˆ URI는 JarScannerê°€ 처리할 수 있는 URLë¡œ ë³€í™˜ë  ìˆ˜ 없습니다. +jarScan.jarUrlStart=URL [{0}]ì— ìœ„ì¹˜í•œ JAR를 스캔합니다. +jarScan.webinfclassesFail=/WEB-INF/classes를 스캔하지 못했습니다. +jarScan.webinflibFail=/WEB-INF/lib으로부터 JAR [{0}]ì„(를) 스캔하지 못했습니다. +jarScan.webinflibJarNoScan=/WEB-INF/lib ë‚´ì˜ íŒŒì¼ [{0}]ì— ëŒ€í•œ JAR ìŠ¤ìº”ì„ ìˆ˜í–‰í•˜ì§€ 않습니다. +jarScan.webinflibJarScan=/WEB-INF/lib으로부터 JAR [{0}]ì„(를) 스캔합니다. +jarScan.webinflibStart=JARë“¤ì„ ì°¾ê¸° 위해 /WEB-INF/libì„ ìŠ¤ìº”í•©ë‹ˆë‹¤. diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/util/scan/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..8d9fdd2 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_pt_BR.properties @@ -0,0 +1,17 @@ +# 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. + +jarScan.classPath.badEntry=O path da classe [{0}] não pode ser convertido para url +jarScan.classloaderFail=Falha ao verificar [{0}] da hierarquia de classloader diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_ru.properties b/java/org/apache/tomcat/util/scan/LocalStrings_ru.properties new file mode 100644 index 0000000..40983d3 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +jarScan.webinflibJarNoScan=Сканирование JAR не производитÑÑ Ð´Ð»Ñ Ñ„Ð°Ð¹Ð»Ð° [{0}] из /WEB-INF/lib diff --git a/java/org/apache/tomcat/util/scan/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/scan/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..5b15b52 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/LocalStrings_zh_CN.properties @@ -0,0 +1,27 @@ +# 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. + +jarScan.classPath.badEntry=class路径[{0}]ä¸èƒ½è¢«è½¬åŒ–æˆURL +jarScan.classloaderFail=在多级类加载器中扫æ[{0}]失败 +jarScan.classloaderJarNoScan=跳过classpath路径[{0}]下的jar包扫æ。 +jarScan.classloaderJarScan=从classpath扫æJAR[{0}] +jarScan.classloaderStart=在类加载器层次结构中扫æJAR +jarScan.invalidModuleUri=æ供的模å—URI [{0}]无法转æ¢ä¸ºJarScannerè¦å¤„ç†çš„URL +jarScan.jarUrlStart=正在扫æURL [{0}] 上的JAR文件 +jarScan.webinfclassesFail=无法扫æ/WEB-INF/classes +jarScan.webinflibFail=无法从/WEB-INF/lib扫æJAR[{0}]。 +jarScan.webinflibJarNoScan=没有扫æ到/WEB-INF/lib目录下的JAR [{0}] +jarScan.webinflibJarScan=从/WEB-INF/lib扫æJAR[{0}] +jarScan.webinflibStart=扫æ./WEB-INF/lib 中的JARs diff --git a/java/org/apache/tomcat/util/scan/NonClosingJarInputStream.java b/java/org/apache/tomcat/util/scan/NonClosingJarInputStream.java new file mode 100644 index 0000000..1488624 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/NonClosingJarInputStream.java @@ -0,0 +1,48 @@ +/* + * 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.tomcat.util.scan; + +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarInputStream; + +/** + * When using a {@link JarInputStream} with an XML parser, the stream will be + * closed by the parser. This causes problems if multiple entries from the JAR + * need to be parsed. This implementation makes {{@link #close()} a NO-OP and + * adds {@link #reallyClose()} that will close the stream. + */ +public class NonClosingJarInputStream extends JarInputStream { + + public NonClosingJarInputStream(InputStream in, boolean verify) + throws IOException { + super(in, verify); + } + + public NonClosingJarInputStream(InputStream in) throws IOException { + super(in); + } + + @Override + public void close() throws IOException { + // Make this a NO-OP so that further entries can be read from the stream + } + + public void reallyClose() throws IOException { + super.close(); + } +} diff --git a/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java b/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java new file mode 100644 index 0000000..1d53573 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java @@ -0,0 +1,146 @@ +/* + * 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.tomcat.util.scan; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.jar.Manifest; + +import org.apache.tomcat.Jar; + +/** + * This class provides a wrapper around {@link Jar} that uses reference counting + * to close and re-create the wrapped {@link Jar} instance as required. + */ +public class ReferenceCountedJar implements Jar { + + private final URL url; + private Jar wrappedJar; + private int referenceCount = 0; + + public ReferenceCountedJar(URL url) throws IOException { + this.url = url; + open(); + } + + + /* + * Note: Returns this instance so it can be used with try-with-resources + */ + private synchronized ReferenceCountedJar open() throws IOException { + if (wrappedJar == null) { + wrappedJar = JarFactory.newInstance(url); + } + referenceCount++; + return this; + } + + + @Override + public synchronized void close() { + referenceCount--; + if (referenceCount == 0) { + wrappedJar.close(); + wrappedJar = null; + } + } + + + @Override + public URL getJarFileURL() { + return url; + } + + + @Override + public InputStream getInputStream(String name) throws IOException { + try (ReferenceCountedJar jar = open()) { + return jar.wrappedJar.getInputStream(name); + } + } + + + @Override + public long getLastModified(String name) throws IOException { + try (ReferenceCountedJar jar = open()) { + return jar.wrappedJar.getLastModified(name); + } + } + + + @Override + public boolean exists(String name) throws IOException { + try (ReferenceCountedJar jar = open()) { + return jar.wrappedJar.exists(name); + } + } + + + @Override + public void nextEntry() { + try (ReferenceCountedJar jar = open()) { + jar.wrappedJar.nextEntry(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + + @Override + public String getEntryName() { + try (ReferenceCountedJar jar = open()) { + return jar.wrappedJar.getEntryName(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + + @Override + public InputStream getEntryInputStream() throws IOException { + try (ReferenceCountedJar jar = open()) { + return jar.wrappedJar.getEntryInputStream(); + } + } + + + @Override + public String getURL(String entry) { + try (ReferenceCountedJar jar = open()) { + return jar.wrappedJar.getURL(entry); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + + @Override + public Manifest getManifest() throws IOException { + try (ReferenceCountedJar jar = open()) { + return jar.wrappedJar.getManifest(); + } + } + + + @Override + public void reset() throws IOException { + try (ReferenceCountedJar jar = open()) { + jar.wrappedJar.reset(); + } + } +} diff --git a/java/org/apache/tomcat/util/scan/StandardJarScanFilter.java b/java/org/apache/tomcat/util/scan/StandardJarScanFilter.java new file mode 100644 index 0000000..1451de4 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/StandardJarScanFilter.java @@ -0,0 +1,261 @@ +/* + * 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.tomcat.util.scan; + +import java.util.HashSet; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.tomcat.JarScanFilter; +import org.apache.tomcat.JarScanType; +import org.apache.tomcat.util.file.Matcher; + +public class StandardJarScanFilter implements JarScanFilter { + + private final ReadWriteLock configurationLock = + new ReentrantReadWriteLock(); + + private static final String defaultSkip; + private static final String defaultScan; + private static final Set defaultSkipSet = new HashSet<>(); + private static final Set defaultScanSet = new HashSet<>(); + private static final boolean defaultSkipAll; + + static { + // Initialize defaults. There are no setter methods for them. + defaultSkip = System.getProperty(Constants.SKIP_JARS_PROPERTY); + populateSetFromAttribute(defaultSkip, defaultSkipSet); + defaultScan = System.getProperty(Constants.SCAN_JARS_PROPERTY); + populateSetFromAttribute(defaultScan, defaultScanSet); + defaultSkipAll = (defaultSkipSet.contains("*") || defaultSkipSet.contains("*.jar")) && defaultScanSet.isEmpty(); + } + + private String tldSkip; + private String tldScan; + private final Set tldSkipSet; + private final Set tldScanSet; + private boolean defaultTldScan = true; + + private String pluggabilitySkip; + private String pluggabilityScan; + private final Set pluggabilitySkipSet; + private final Set pluggabilityScanSet; + private boolean defaultPluggabilityScan = true; + + /** + * This is the standard implementation of {@link JarScanFilter}. By default, + * the following filtering rules are used: + *

      + *
    • JARs that match neither the skip nor the scan list will be included + * in scan results.
    • + *
    • JARs that match the skip list but not the scan list will be excluded + * from scan results.
    • + *
    • JARs that match the scan list will be included from scan results. + *
    • + *
    + * The default skip list and default scan list are obtained from the system + * properties {@link Constants#SKIP_JARS_PROPERTY} and + * {@link Constants#SCAN_JARS_PROPERTY} respectively. These default values + * may be over-ridden for the {@link JarScanType#TLD} and + * {@link JarScanType#PLUGGABILITY} scans. The filtering rules may also be + * modified for these scan types using {@link #setDefaultTldScan(boolean)} + * and {@link #setDefaultPluggabilityScan(boolean)}. If set to + * false, the following filtering rules are used for associated + * type: + *
      + *
    • JARs that match neither the skip nor the scan list will be excluded + * from scan results.
    • + *
    • JARs that match the scan list but not the skip list will be included + * in scan results.
    • + *
    • JARs that match the skip list will be excluded from scan results. + *
    • + *
    + */ + public StandardJarScanFilter() { + tldSkip = defaultSkip; + tldSkipSet = new HashSet<>(defaultSkipSet); + tldScan = defaultScan; + tldScanSet = new HashSet<>(defaultScanSet); + pluggabilitySkip = defaultSkip; + pluggabilitySkipSet = new HashSet<>(defaultSkipSet); + pluggabilityScan = defaultScan; + pluggabilityScanSet = new HashSet<>(defaultScanSet); + } + + + public String getTldSkip() { + return tldSkip; + } + + + public void setTldSkip(String tldSkip) { + this.tldSkip = tldSkip; + Lock writeLock = configurationLock.writeLock(); + writeLock.lock(); + try { + populateSetFromAttribute(tldSkip, tldSkipSet); + } finally { + writeLock.unlock(); + } + } + + + public String getTldScan() { + return tldScan; + } + + + public void setTldScan(String tldScan) { + this.tldScan = tldScan; + Lock writeLock = configurationLock.writeLock(); + writeLock.lock(); + try { + populateSetFromAttribute(tldScan, tldScanSet); + } finally { + writeLock.unlock(); + } + } + + + @Override + public boolean isSkipAll() { + return defaultSkipAll; + } + + + public boolean isDefaultTldScan() { + return defaultTldScan; + } + + + public void setDefaultTldScan(boolean defaultTldScan) { + this.defaultTldScan = defaultTldScan; + } + + + public String getPluggabilitySkip() { + return pluggabilitySkip; + } + + + public void setPluggabilitySkip(String pluggabilitySkip) { + this.pluggabilitySkip = pluggabilitySkip; + Lock writeLock = configurationLock.writeLock(); + writeLock.lock(); + try { + populateSetFromAttribute(pluggabilitySkip, pluggabilitySkipSet); + } finally { + writeLock.unlock(); + } + } + + + public String getPluggabilityScan() { + return pluggabilityScan; + } + + + public void setPluggabilityScan(String pluggabilityScan) { + this.pluggabilityScan = pluggabilityScan; + Lock writeLock = configurationLock.writeLock(); + writeLock.lock(); + try { + populateSetFromAttribute(pluggabilityScan, pluggabilityScanSet); + } finally { + writeLock.unlock(); + } + } + + + public boolean isDefaultPluggabilityScan() { + return defaultPluggabilityScan; + } + + + public void setDefaultPluggabilityScan(boolean defaultPluggabilityScan) { + this.defaultPluggabilityScan = defaultPluggabilityScan; + } + + + @Override + public boolean check(JarScanType jarScanType, String jarName) { + Lock readLock = configurationLock.readLock(); + readLock.lock(); + try { + final boolean defaultScan; + final Set toSkip; + final Set toScan; + switch (jarScanType) { + case TLD: { + defaultScan = defaultTldScan; + toSkip = tldSkipSet; + toScan = tldScanSet; + break; + } + case PLUGGABILITY: { + defaultScan = defaultPluggabilityScan; + toSkip = pluggabilitySkipSet; + toScan = pluggabilityScanSet; + break; + } + case OTHER: + default: { + defaultScan = true; + toSkip = defaultSkipSet; + toScan = defaultScanSet; + } + } + if (defaultScan) { + if (Matcher.matchName(toSkip, jarName)) { + if (Matcher.matchName(toScan, jarName)) { + return true; + } else { + return false; + } + } + return true; + } else { + if (Matcher.matchName(toScan, jarName)) { + if (Matcher.matchName(toSkip, jarName)) { + return false; + } else { + return true; + } + } + return false; + } + } finally { + readLock.unlock(); + } + } + + private static void populateSetFromAttribute(String attribute, Set set) { + set.clear(); + if (attribute != null) { + StringTokenizer tokenizer = new StringTokenizer(attribute, ","); + while (tokenizer.hasMoreElements()) { + String token = tokenizer.nextToken().trim(); + if (token.length() > 0) { + set.add(token); + } + } + } + } +} diff --git a/java/org/apache/tomcat/util/scan/StandardJarScanner.java b/java/org/apache/tomcat/util/scan/StandardJarScanner.java new file mode 100644 index 0000000..1d93bbe --- /dev/null +++ b/java/org/apache/tomcat/util/scan/StandardJarScanner.java @@ -0,0 +1,516 @@ +/* + * 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.tomcat.util.scan; + +import java.io.File; +import java.io.IOException; +import java.lang.module.ResolvedModule; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import jakarta.servlet.ServletContext; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.Jar; +import org.apache.tomcat.JarScanFilter; +import org.apache.tomcat.JarScanType; +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.JarScannerCallback; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.UriUtil; +import org.apache.tomcat.util.res.StringManager; + +/** + * The default {@link JarScanner} implementation scans the WEB-INF/lib directory + * followed by the provided classloader and then works up the classloader + * hierarchy. This implementation is sufficient to meet the requirements of the + * Servlet 3.0 specification as well as to provide a number of Tomcat specific + * extensions. The extensions are: + *
      + *
    • Scanning the classloader hierarchy (enabled by default)
    • + *
    • Testing all files to see if they are JARs (disabled by default)
    • + *
    • Testing all directories to see if they are exploded JARs + * (disabled by default)
    • + *
    + * All of the extensions may be controlled via configuration. + */ +public class StandardJarScanner implements JarScanner { + + private final Log log = LogFactory.getLog(StandardJarScanner.class); // must not be static + + /** + * The string resources for this package. + */ + private static final StringManager sm = StringManager.getManager(Constants.Package); + + private static final Set CLASSLOADER_HIERARCHY; + + static { + Set cls = new HashSet<>(); + + ClassLoader cl = StandardJarScanner.class.getClassLoader(); + while (cl != null) { + cls.add(cl); + cl = cl.getParent(); + } + + CLASSLOADER_HIERARCHY = Collections.unmodifiableSet(cls); + } + + /** + * Controls the classpath scanning extension. + */ + private boolean scanClassPath = true; + public boolean isScanClassPath() { + return scanClassPath; + } + public void setScanClassPath(boolean scanClassPath) { + this.scanClassPath = scanClassPath; + } + + /** + * Controls the JAR file Manifest scanning extension. + */ + private boolean scanManifest = true; + public boolean isScanManifest() { + return scanManifest; + } + public void setScanManifest(boolean scanManifest) { + this.scanManifest = scanManifest; + } + + /** + * Controls the testing all files to see of they are JAR files extension. + */ + private boolean scanAllFiles = false; + public boolean isScanAllFiles() { + return scanAllFiles; + } + public void setScanAllFiles(boolean scanAllFiles) { + this.scanAllFiles = scanAllFiles; + } + + /** + * Controls the testing all directories to see of they are exploded JAR + * files extension. + */ + private boolean scanAllDirectories = true; + public boolean isScanAllDirectories() { + return scanAllDirectories; + } + public void setScanAllDirectories(boolean scanAllDirectories) { + this.scanAllDirectories = scanAllDirectories; + } + + /** + * Controls the testing of the bootstrap classpath which consists of the + * runtime classes provided by the JVM and any installed system extensions. + */ + private boolean scanBootstrapClassPath = false; + public boolean isScanBootstrapClassPath() { + return scanBootstrapClassPath; + } + public void setScanBootstrapClassPath(boolean scanBootstrapClassPath) { + this.scanBootstrapClassPath = scanBootstrapClassPath; + } + + /** + * Controls the filtering of the results from the scan for JARs + */ + private JarScanFilter jarScanFilter = new StandardJarScanFilter(); + @Override + public JarScanFilter getJarScanFilter() { + return jarScanFilter; + } + @Override + public void setJarScanFilter(JarScanFilter jarScanFilter) { + this.jarScanFilter = jarScanFilter; + } + + /** + * Scan the provided ServletContext and class loader for JAR files. Each JAR + * file found will be passed to the callback handler to be processed. + * + * @param scanType The type of JAR scan to perform. This is passed to + * the filter which uses it to determine how to + * filter the results + * @param context The ServletContext - used to locate and access + * WEB-INF/lib + * @param callback The handler to process any JARs found + */ + @Override + public void scan(JarScanType scanType, ServletContext context, + JarScannerCallback callback) { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("jarScan.webinflibStart")); + } + + if (jarScanFilter.isSkipAll()) { + return; + } + + Set processedURLs = new HashSet<>(); + + // Scan WEB-INF/lib + Set dirList = context.getResourcePaths(Constants.WEB_INF_LIB); + if (dirList != null) { + for (String path : dirList) { + if (path.endsWith(Constants.JAR_EXT) && + getJarScanFilter().check(scanType, + path.substring(path.lastIndexOf('/')+1))) { + // Need to scan this JAR + if (log.isDebugEnabled()) { + log.debug(sm.getString("jarScan.webinflibJarScan", path)); + } + URL url = null; + try { + url = context.getResource(path); + if (url != null) { + processedURLs.add(url); + process(scanType, callback, url, path, true, null); + } else { + log.warn(sm.getString("jarScan.webinflibFail", path)); + } + } catch (IOException e) { + log.warn(sm.getString("jarScan.webinflibFail", url), e); + } + } else { + if (log.isTraceEnabled()) { + log.trace(sm.getString("jarScan.webinflibJarNoScan", path)); + } + } + } + } + + // Scan WEB-INF/classes + try { + URL webInfURL = context.getResource(Constants.WEB_INF_CLASSES); + if (webInfURL != null) { + // WEB-INF/classes will also be included in the URLs returned + // by the web application class loader so ensure the class path + // scanning below does not re-scan this location. + processedURLs.add(webInfURL); + + if (isScanAllDirectories()) { + URL url = context.getResource(Constants.WEB_INF_CLASSES + "/META-INF"); + if (url != null) { + try { + callback.scanWebInfClasses(); + } catch (IOException e) { + log.warn(sm.getString("jarScan.webinfclassesFail"), e); + } + } + } + } + } catch (MalformedURLException e) { + // Ignore. Won't happen. URLs are of the correct form. + } + + // Scan the classpath + if (isScanClassPath()) { + doScanClassPath(scanType, context, callback, processedURLs); + } + } + + + protected void doScanClassPath(JarScanType scanType, ServletContext context, + JarScannerCallback callback, Set processedURLs) { + if (log.isTraceEnabled()) { + log.trace(sm.getString("jarScan.classloaderStart")); + } + + ClassLoader stopLoader = null; + if (!isScanBootstrapClassPath()) { + // Stop when we reach the bootstrap class loader + stopLoader = ClassLoader.getSystemClassLoader().getParent(); + } + + ClassLoader classLoader = context.getClassLoader(); + + // JARs are treated as application provided until the common class + // loader is reached. + boolean isWebapp = true; + + // Use a Deque so URLs can be removed as they are processed + // and new URLs can be added as they are discovered during + // processing. + Deque classPathUrlsToProcess = new ArrayDeque<>(); + + while (classLoader != null && classLoader != stopLoader) { + if (classLoader instanceof URLClassLoader) { + if (isWebapp) { + isWebapp = isWebappClassLoader(classLoader); + } + + classPathUrlsToProcess.addAll( + Arrays.asList(((URLClassLoader) classLoader).getURLs())); + + processURLs(scanType, callback, processedURLs, isWebapp, classPathUrlsToProcess); + } + classLoader = classLoader.getParent(); + } + + // The application and platform class loaders are not + // instances of URLClassLoader. Use the class path in this + // case. + addClassPath(classPathUrlsToProcess); + + // Also add any modules + for (ResolvedModule module : ModuleLayer.boot().configuration().modules()) { + Optional uri = module.reference().location(); + if (uri.isPresent()) { + try { + classPathUrlsToProcess.add(uri.get().toURL()); + } catch (MalformedURLException e) { + log.warn(sm.getString("jarScan.invalidModuleUri", uri), e); + } + } + } + + processURLs(scanType, callback, processedURLs, false, classPathUrlsToProcess); + } + + + protected void processURLs(JarScanType scanType, JarScannerCallback callback, + Set processedURLs, boolean isWebapp, Deque classPathUrlsToProcess) { + + if (jarScanFilter.isSkipAll()) { + return; + } + + while (!classPathUrlsToProcess.isEmpty()) { + URL url = classPathUrlsToProcess.pop(); + + if (processedURLs.contains(url)) { + // Skip this URL it has already been processed + continue; + } + + ClassPathEntry cpe = new ClassPathEntry(url); + + // JARs are scanned unless the filter says not to. + // Directories are scanned for pluggability scans or + // if scanAllDirectories is enabled unless the + // filter says not to. + if ((cpe.isJar() || + scanType == JarScanType.PLUGGABILITY || + isScanAllDirectories()) && + getJarScanFilter().check(scanType, + cpe.getName())) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jarScan.classloaderJarScan", url)); + } + try { + processedURLs.add(url); + process(scanType, callback, url, null, isWebapp, classPathUrlsToProcess); + } catch (IOException ioe) { + log.warn(sm.getString("jarScan.classloaderFail", url), ioe); + } + } else { + // JAR / directory has been skipped + if (log.isTraceEnabled()) { + log.trace(sm.getString("jarScan.classloaderJarNoScan", url)); + } + } + } + } + + + protected void addClassPath(Deque classPathUrlsToProcess) { + String classPath = System.getProperty("java.class.path"); + + if (classPath == null || classPath.length() == 0) { + return; + } + + String[] classPathEntries = classPath.split(File.pathSeparator); + for (String classPathEntry : classPathEntries) { + File f = new File(classPathEntry); + try { + classPathUrlsToProcess.add(f.toURI().toURL()); + } catch (MalformedURLException e) { + log.warn(sm.getString("jarScan.classPath.badEntry", classPathEntry), e); + } + } + } + + + /* + * Since class loader hierarchies can get complicated, this method attempts + * to apply the following rule: A class loader is a web application class + * loader unless it loaded this class (StandardJarScanner) or is a parent + * of the class loader that loaded this class. + * + * This should mean: + * the webapp class loader is an application class loader + * the shared class loader is an application class loader + * the server class loader is not an application class loader + * the common class loader is not an application class loader + * the system class loader is not an application class loader + * the bootstrap class loader is not an application class loader + */ + private static boolean isWebappClassLoader(ClassLoader classLoader) { + return !CLASSLOADER_HIERARCHY.contains(classLoader); + } + + + /* + * Scan a URL for JARs with the optional extensions to look at all files + * and all directories. + */ + protected void process(JarScanType scanType, JarScannerCallback callback, + URL url, String webappPath, boolean isWebapp, Deque classPathUrlsToProcess) + throws IOException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("jarScan.jarUrlStart", url)); + } + + if ("jar".equals(url.getProtocol()) || url.getPath().endsWith(Constants.JAR_EXT)) { + try (Jar jar = JarFactory.newInstance(url)) { + if (isScanManifest()) { + processManifest(jar, isWebapp, classPathUrlsToProcess); + } + callback.scan(jar, webappPath, isWebapp); + } + } else if ("file".equals(url.getProtocol())) { + File f; + try { + f = new File(url.toURI()); + if (f.isFile() && isScanAllFiles()) { + // Treat this file as a JAR + URL jarURL = UriUtil.buildJarUrl(f); + try (Jar jar = JarFactory.newInstance(jarURL)) { + if (isScanManifest()) { + processManifest(jar, isWebapp, classPathUrlsToProcess); + } + callback.scan(jar, webappPath, isWebapp); + } + } else if (f.isDirectory()) { + if (scanType == JarScanType.PLUGGABILITY) { + callback.scan(f, webappPath, isWebapp); + } else { + File metainf = new File(f.getAbsoluteFile() + File.separator + "META-INF"); + if (metainf.isDirectory()) { + callback.scan(f, webappPath, isWebapp); + } + } + } + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + // Wrap the exception and re-throw + throw new IOException(t); + } + } + } + + + private void processManifest(Jar jar, boolean isWebapp, + Deque classPathUrlsToProcess) throws IOException { + + // Not processed for web application JARs nor if the caller did not + // provide a Deque of URLs to append to. + if (isWebapp || classPathUrlsToProcess == null) { + return; + } + + Manifest manifest = jar.getManifest(); + if (manifest != null) { + Attributes attributes = manifest.getMainAttributes(); + String classPathAttribute = attributes.getValue("Class-Path"); + if (classPathAttribute == null) { + return; + } + String[] classPathEntries = classPathAttribute.split(" "); + for (String classPathEntry : classPathEntries) { + classPathEntry = classPathEntry.trim(); + if (classPathEntry.length() == 0) { + continue; + } + URL jarURL = jar.getJarFileURL(); + URL classPathEntryURL; + try { + URI jarURI = jarURL.toURI(); + /* + * Note: Resolving the relative URLs from the manifest has the + * potential to introduce security concerns. However, since + * only JARs provided by the container and NOT those provided + * by web applications are processed, there should be no + * issues. + * If this feature is ever extended to include JARs provided + * by web applications, checks should be added to ensure that + * any relative URL does not step outside the web application. + */ + URI classPathEntryURI = jarURI.resolve(classPathEntry); + classPathEntryURL = classPathEntryURI.toURL(); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jarScan.invalidUri", jarURL), e); + } + continue; + } + classPathUrlsToProcess.add(classPathEntryURL); + } + } + } + + + private static class ClassPathEntry { + + private final boolean jar; + private final String name; + + ClassPathEntry(URL url) { + String path = url.getPath(); + int end = path.lastIndexOf(Constants.JAR_EXT); + if (end != -1) { + jar = true; + int start = path.lastIndexOf('/', end); + name = path.substring(start + 1, end + 4); + } else { + jar = false; + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + int start = path.lastIndexOf('/'); + name = path.substring(start + 1); + } + + } + + public boolean isJar() { + return jar; + } + + public String getName() { + return name; + } + } +} diff --git a/java/org/apache/tomcat/util/scan/UrlJar.java b/java/org/apache/tomcat/util/scan/UrlJar.java new file mode 100644 index 0000000..6764800 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/UrlJar.java @@ -0,0 +1,49 @@ +/* + * 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.tomcat.util.scan; + +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; + +/** + * Implementation of {@link org.apache.tomcat.Jar} that is optimised for + * non-file based JAR URLs. + */ +public class UrlJar extends AbstractInputStreamJar { + + public UrlJar(URL jarFileURL) { + super(jarFileURL); + } + + + @Override + public void close() { + closeStream(); + } + + + @Override + protected NonClosingJarInputStream createJarInputStream() throws IOException { + JarURLConnection jarConn = (JarURLConnection) getJarFileURL().openConnection(); + URL resourceURL = jarConn.getJarFileURL(); + URLConnection resourceConn = resourceURL.openConnection(); + resourceConn.setUseCaches(false); + return new NonClosingJarInputStream(resourceConn.getInputStream()); + } +} diff --git a/java/org/apache/tomcat/util/scan/package.html b/java/org/apache/tomcat/util/scan/package.html new file mode 100644 index 0000000..25e1e89 --- /dev/null +++ b/java/org/apache/tomcat/util/scan/package.html @@ -0,0 +1,27 @@ + + + + + + +

    +This package contains the common classes used to perform configuration scanning +for Catalina and Jasper. +

    + + diff --git a/java/org/apache/tomcat/util/security/ConcurrentMessageDigest.java b/java/org/apache/tomcat/util/security/ConcurrentMessageDigest.java new file mode 100644 index 0000000..c59a0fe --- /dev/null +++ b/java/org/apache/tomcat/util/security/ConcurrentMessageDigest.java @@ -0,0 +1,127 @@ +/* + * 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.tomcat.util.security; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.apache.tomcat.util.res.StringManager; + +/** + * A thread safe wrapper around {@link MessageDigest} that does not make use + * of ThreadLocal and - broadly - only creates enough MessageDigest objects + * to satisfy the concurrency requirements. + */ +public class ConcurrentMessageDigest { + + private static final StringManager sm = StringManager.getManager(ConcurrentMessageDigest.class); + + private static final String MD5 = "MD5"; + private static final String SHA1 = "SHA-1"; + + private static final Map> queues = + new ConcurrentHashMap<>(); + + + private ConcurrentMessageDigest() { + // Hide default constructor for this utility class + } + + static { + try { + // Init commonly used algorithms + init(MD5); + init(SHA1); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(sm.getString("concurrentMessageDigest.noDigest"), e); + } + } + + public static byte[] digestMD5(byte[]... input) { + return digest(MD5, input); + } + + public static byte[] digestSHA1(byte[]... input) { + return digest(SHA1, input); + } + + public static byte[] digest(String algorithm, byte[]... input) { + return digest(algorithm, 1, input); + } + + + public static byte[] digest(String algorithm, int iterations, byte[]... input) { + + Queue queue = queues.get(algorithm); + if (queue == null) { + throw new IllegalStateException(sm.getString("concurrentMessageDigest.noDigest")); + } + + MessageDigest md = queue.poll(); + if (md == null) { + try { + md = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + // Ignore. Impossible if init() has been successfully called + // first. + throw new IllegalStateException(sm.getString("concurrentMessageDigest.noDigest"), e); + } + } + + // Round 1 + for (byte[] bytes : input) { + md.update(bytes); + } + byte[] result = md.digest(); + + // Subsequent rounds + if (iterations > 1) { + for (int i = 1; i < iterations; i++) { + md.update(result); + result = md.digest(); + } + } + + queue.add(md); + + return result; + } + + + /** + * Ensures that {@link #digest(String, byte[][])} will support the specified + * algorithm. This method must be called and return successfully + * before using {@link #digest(String, byte[][])}. + * + * @param algorithm The message digest algorithm to be supported + * + * @throws NoSuchAlgorithmException If the algorithm is not supported by the + * JVM + */ + public static void init(String algorithm) throws NoSuchAlgorithmException { + if (!queues.containsKey(algorithm)) { + MessageDigest md = MessageDigest.getInstance(algorithm); + Queue queue = new ConcurrentLinkedQueue<>(); + queue.add(md); + queues.putIfAbsent(algorithm, queue); + } + } +} diff --git a/java/org/apache/tomcat/util/security/Escape.java b/java/org/apache/tomcat/util/security/Escape.java new file mode 100644 index 0000000..7533839 --- /dev/null +++ b/java/org/apache/tomcat/util/security/Escape.java @@ -0,0 +1,165 @@ +/* + * 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.tomcat.util.security; + +/** + * Provides utility methods to escape content for different contexts. It is + * critical that the escaping used is correct for the context in which the data + * is to be used. + */ +public class Escape { + + private Escape() { + // Hide default constructor for this utility class + } + + + /** + * Escape content for use in HTML. This escaping is suitable for the + * following uses: + *
      + *
    • Element content when the escaped data will be placed directly inside + * tags such as <p>, <td> etc.
    • + *
    • Attribute values when the attribute value is quoted with " or + * '.
    • + *
    + * + * @param content The content to escape + * + * @return The escaped content or {@code null} if the content was + * {@code null} + */ + public static String htmlElementContent(String content) { + if (content == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + if (c == '<') { + sb.append("<"); + } else if (c == '>') { + sb.append(">"); + } else if (c == '\'') { + sb.append("'"); + } else if (c == '&') { + sb.append("&"); + } else if (c == '"') { + sb.append("""); + } else if (c == '/') { + sb.append("/"); + } else { + sb.append(c); + } + } + + return (sb.length() > content.length()) ? sb.toString() : content; + } + + + /** + * Convert the object to a string via {@link Object#toString()} and HTML + * escape the resulting string for use in HTML content. + * + * @param obj The object to convert to String and then escape + * + * @return The escaped content or "?" if obj is + * {@code null} + */ + public static String htmlElementContent(Object obj) { + if (obj == null) { + return "?"; + } + + try { + return htmlElementContent(obj.toString()); + } catch (Exception e) { + return null; + } + } + + + /** + * Escape content for use in XML. + * + * @param content The content to escape + * + * @return The escaped content or {@code null} if the content was + * {@code null} + */ + public static String xml(String content) { + return xml(null, content); + } + + + /** + * Escape content for use in XML. + * + * @param ifNull The value to return if content is {@code null} + * @param content The content to escape + * + * @return The escaped content or the value of {@code ifNull} if the + * content was {@code null} + */ + public static String xml(String ifNull, String content) { + return xml(ifNull, false, content); + } + + + /** + * Escape content for use in XML. + * + * @param ifNull The value to return if content is {@code null} + * @param escapeCRLF Should CR and LF also be escaped? + * @param content The content to escape + * + * @return The escaped content or the value of ifNull if the content was + * {@code null} + */ + public static String xml(String ifNull, boolean escapeCRLF, String content) { + if (content == null) { + return ifNull; + } + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + if (c == '<') { + sb.append("<"); + } else if (c == '>') { + sb.append(">"); + } else if (c == '\'') { + sb.append("'"); + } else if (c == '&') { + sb.append("&"); + } else if (c == '"') { + sb.append("""); + } else if (escapeCRLF && c == '\r') { + sb.append(" "); + } else if (escapeCRLF && c == '\n') { + sb.append(" "); + } else { + sb.append(c); + } + } + + return (sb.length() > content.length()) ? sb.toString(): content; + } +} diff --git a/java/org/apache/tomcat/util/security/KeyStoreUtil.java b/java/org/apache/tomcat/util/security/KeyStoreUtil.java new file mode 100644 index 0000000..862ef22 --- /dev/null +++ b/java/org/apache/tomcat/util/security/KeyStoreUtil.java @@ -0,0 +1,72 @@ +/* + * 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.tomcat.util.security; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class KeyStoreUtil { + + private KeyStoreUtil() { + // Utility class + } + + /** + * Loads a KeyStore from an InputStream working around the known JDK bug + * https://bugs.openjdk.java.net/browse/JDK-8157404. + * + * This code can be removed once the minimum Java version for Tomcat is 13. + * + * + * @param keystore The KeyStore to load from the InputStream + * @param is The InputStream to use to populate the KeyStore + * @param storePass The password to access the KeyStore + * + * @throws IOException + * If an I/O occurs reading from the given InputStream + * @throws CertificateException + * If one or more certificates can't be loaded into the + * KeyStore + * @throws NoSuchAlgorithmException + * If the algorithm specified to validate the integrity of the + * KeyStore cannot be found + */ + public static void load(KeyStore keystore, InputStream is, char[] storePass) + throws NoSuchAlgorithmException, CertificateException, IOException { + if (keystore.getType().equals("PKCS12")) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int numRead; + while ((numRead = is.read(buf)) >= 0) { + baos.write(buf, 0, numRead); + } + baos.close(); + // Don't close is. That remains the callers responsibility. + + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + + keystore.load(bais, storePass); + } else { + keystore.load(is, storePass); + } + } +} diff --git a/java/org/apache/tomcat/util/security/LocalStrings.properties b/java/org/apache/tomcat/util/security/LocalStrings.properties new file mode 100644 index 0000000..39e92df --- /dev/null +++ b/java/org/apache/tomcat/util/security/LocalStrings.properties @@ -0,0 +1,19 @@ +# 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. + +concurrentMessageDigest.noDigest=Digest algorithm unavailable + +privilegedSetAccessControlContext.lookupFailed=Unable to obtain reference to field Thread.inheritedAccessControlContext +privilegedSetAccessControlContext.setFailed=Unable to set field Thread.inheritedAccessControlContext diff --git a/java/org/apache/tomcat/util/security/LocalStrings_fr.properties b/java/org/apache/tomcat/util/security/LocalStrings_fr.properties new file mode 100644 index 0000000..79bf7f8 --- /dev/null +++ b/java/org/apache/tomcat/util/security/LocalStrings_fr.properties @@ -0,0 +1,19 @@ +# 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. + +concurrentMessageDigest.noDigest=L'algorithme de digest est introuvable + +privilegedSetAccessControlContext.lookupFailed=Impossible d'obtenir une référence pour le champ Thread.inheritedAccessControlContext +privilegedSetAccessControlContext.setFailed=Impossible de fixer la valeur du champ Thread.inheritedAccessControlContext diff --git a/java/org/apache/tomcat/util/security/LocalStrings_ja.properties b/java/org/apache/tomcat/util/security/LocalStrings_ja.properties new file mode 100644 index 0000000..5ae31f8 --- /dev/null +++ b/java/org/apache/tomcat/util/security/LocalStrings_ja.properties @@ -0,0 +1,19 @@ +# 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. + +concurrentMessageDigest.noDigest=ダイジェストアルゴリズムを利用ã§ãã¾ã›ã‚“。 + +privilegedSetAccessControlContext.lookupFailed=フィールド Thread.inheritedAccessControlContext ã¸ã®å‚照をå–å¾—ã§ãã¾ã›ã‚“ +privilegedSetAccessControlContext.setFailed=フィールド Thread.inheritedAccessControlContext を設定ã§ãã¾ã›ã‚“ diff --git a/java/org/apache/tomcat/util/security/LocalStrings_ko.properties b/java/org/apache/tomcat/util/security/LocalStrings_ko.properties new file mode 100644 index 0000000..24fba21 --- /dev/null +++ b/java/org/apache/tomcat/util/security/LocalStrings_ko.properties @@ -0,0 +1,19 @@ +# 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. + +concurrentMessageDigest.noDigest=Digest ì•Œê³ ë¦¬ì¦˜ì´ ê°€ìš©í•˜ì§€ 않습니다. + +privilegedSetAccessControlContext.lookupFailed=Thread.inheritedAccessControlContext 필드를 참조할 수 없습니다. +privilegedSetAccessControlContext.setFailed=Thread.inheritedAccessControlContext 필드를 설정할 수 없습니다. diff --git a/java/org/apache/tomcat/util/security/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/security/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..bc021bb --- /dev/null +++ b/java/org/apache/tomcat/util/security/LocalStrings_zh_CN.properties @@ -0,0 +1,19 @@ +# 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. + +concurrentMessageDigest.noDigest=摘è¦ç®—法ä¸å¯ç”¨ + +privilegedSetAccessControlContext.lookupFailed=无法获å–对字段 Thread.inheritedAccessControlContext 的引用 +privilegedSetAccessControlContext.setFailed=无法设置字段 Thread.inheritedAccessControlContext diff --git a/java/org/apache/tomcat/util/security/MD5Encoder.java b/java/org/apache/tomcat/util/security/MD5Encoder.java new file mode 100644 index 0000000..c42f3e3 --- /dev/null +++ b/java/org/apache/tomcat/util/security/MD5Encoder.java @@ -0,0 +1,70 @@ +/* + * 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.tomcat.util.security; + +import org.apache.tomcat.util.buf.HexUtils; + +/** + * Encode an MD5 digest into a String. + *

    + * The 128 bit MD5 hash is converted into a 32 character long String. + * Each character of the String is the hexadecimal representation of 4 bits + * of the digest. + * + * @author Remy Maucherat + * + * @deprecated Unused. Use {@link HexUtils} instead. Will be removed in Tomcat 11. + */ +@Deprecated +public final class MD5Encoder { + + + private MD5Encoder() { + // Hide default constructor for utility class + } + + + private static final char[] hexadecimal = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + + /** + * Encodes the 128 bit (16 bytes) MD5 into a 32 character String. + * + * @param binaryData Array containing the digest + * + * @return Encoded MD5, or null if encoding failed + */ + public static String encode(byte[] binaryData) { + + if (binaryData.length != 16) { + return null; + } + + char[] buffer = new char[32]; + + for (int i=0; i<16; i++) { + int low = binaryData[i] & 0x0f; + int high = (binaryData[i] & 0xf0) >> 4; + buffer[i*2] = hexadecimal[high]; + buffer[i*2 + 1] = hexadecimal[low]; + } + + return new String(buffer); + } +} + diff --git a/java/org/apache/tomcat/util/security/PermissionCheck.java b/java/org/apache/tomcat/util/security/PermissionCheck.java new file mode 100644 index 0000000..c2a9b86 --- /dev/null +++ b/java/org/apache/tomcat/util/security/PermissionCheck.java @@ -0,0 +1,43 @@ +/* + * 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.tomcat.util.security; + +import java.security.Permission; + +/** + * This interface is implemented by components to enable privileged code to + * check whether the component has a given permission. + * This is typically used when a privileged component (e.g. the container) is + * performing an action on behalf of an untrusted component (e.g. a web + * application) without the current thread having passed through a code source + * provided by the untrusted component. Because the current thread has not + * passed through a code source provided by the untrusted component the + * SecurityManager assumes the code is trusted so the standard checking + * mechanisms can't be used. + */ +public interface PermissionCheck { + + /** + * Does this component have the given permission? + * + * @param permission The permission to test + * + * @return {@code false} if a SecurityManager is enabled and the component + * does not have the given permission, otherwise {@code true} + */ + boolean check(Permission permission); +} diff --git a/java/org/apache/tomcat/util/security/PrivilegedGetTccl.java b/java/org/apache/tomcat/util/security/PrivilegedGetTccl.java new file mode 100644 index 0000000..c580cfa --- /dev/null +++ b/java/org/apache/tomcat/util/security/PrivilegedGetTccl.java @@ -0,0 +1,37 @@ +/* + * 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.tomcat.util.security; + +import java.security.PrivilegedAction; + +public class PrivilegedGetTccl implements PrivilegedAction { + private final Thread currentThread; + + @Deprecated + public PrivilegedGetTccl() { + this(Thread.currentThread()); + } + + public PrivilegedGetTccl(Thread currentThread) { + this.currentThread = currentThread; + } + + @Override + public ClassLoader run() { + return currentThread.getContextClassLoader(); + } +} diff --git a/java/org/apache/tomcat/util/security/PrivilegedSetAccessControlContext.java b/java/org/apache/tomcat/util/security/PrivilegedSetAccessControlContext.java new file mode 100644 index 0000000..2f53480 --- /dev/null +++ b/java/org/apache/tomcat/util/security/PrivilegedSetAccessControlContext.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat.util.security; + +import java.lang.reflect.Field; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class PrivilegedSetAccessControlContext implements PrivilegedAction { + + private static final Log log = LogFactory.getLog(PrivilegedSetAccessControlContext.class); + private static final StringManager sm = StringManager.getManager(PrivilegedSetAccessControlContext.class); + + private static final AccessControlContext acc; + private static final Field field; + + static { + acc = AccessController.getContext(); + Field f = null; + try { + f = Thread.class.getDeclaredField("inheritedAccessControlContext"); + f.trySetAccessible(); + } catch (NoSuchFieldException | SecurityException e) { + log.warn(sm.getString("privilegedSetAccessControlContext.lookupFailed"), e); + } + field = f; + } + + private final Thread t; + + + public PrivilegedSetAccessControlContext(Thread t) { + this.t = t; + } + + + @Override + public Void run() { + try { + if (field != null) { + field.set(t, acc); + } + } catch (IllegalArgumentException | IllegalAccessException e) { + log.warn(sm.getString("privilegedSetAccessControlContext.setFailed"), e); + } + return null; + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/security/PrivilegedSetTccl.java b/java/org/apache/tomcat/util/security/PrivilegedSetTccl.java new file mode 100644 index 0000000..188a8ae --- /dev/null +++ b/java/org/apache/tomcat/util/security/PrivilegedSetTccl.java @@ -0,0 +1,42 @@ +/* + * 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.tomcat.util.security; + +import java.security.PrivilegedAction; + +public class PrivilegedSetTccl implements PrivilegedAction { + + private final ClassLoader cl; + private final Thread t; + + @Deprecated + public PrivilegedSetTccl(ClassLoader cl) { + this(Thread.currentThread(), cl); + } + + public PrivilegedSetTccl(Thread t, ClassLoader cl) { + this.t = t; + this.cl = cl; + } + + + @Override + public Void run() { + t.setContextClassLoader(cl); + return null; + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/threads/Constants.java b/java/org/apache/tomcat/util/threads/Constants.java new file mode 100644 index 0000000..5dcacce --- /dev/null +++ b/java/org/apache/tomcat/util/threads/Constants.java @@ -0,0 +1,30 @@ +/* + * 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.tomcat.util.threads; + +/** + * Static constants for this package. + */ +public final class Constants { + + public static final long DEFAULT_THREAD_RENEWAL_DELAY = 1000L; + + /** + * Has security been turned on? + */ + public static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null); +} diff --git a/java/org/apache/tomcat/util/threads/InlineExecutorService.java b/java/org/apache/tomcat/util/threads/InlineExecutorService.java new file mode 100644 index 0000000..ca3eec9 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/InlineExecutorService.java @@ -0,0 +1,84 @@ +/* + * 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.tomcat.util.threads; + +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +public class InlineExecutorService extends AbstractExecutorService { + + private volatile boolean shutdown; + private volatile boolean taskRunning; + private volatile boolean terminated; + + private final Object lock = new Object(); + + @Override + public void shutdown() { + shutdown = true; + synchronized (lock) { + terminated = !taskRunning; + } + } + + @Override + public List shutdownNow() { + shutdown(); + return null; + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return terminated; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + synchronized (lock) { + if (terminated) { + return true; + } + lock.wait(unit.toMillis(timeout)); + return terminated; + } + } + + @Override + public void execute(Runnable command) { + synchronized (lock) { + if (shutdown) { + throw new RejectedExecutionException(); + } + taskRunning = true; + } + command.run(); + synchronized (lock) { + taskRunning = false; + if (shutdown) { + terminated = true; + lock.notifyAll(); + } + } + } +} diff --git a/java/org/apache/tomcat/util/threads/LimitLatch.java b/java/org/apache/tomcat/util/threads/LimitLatch.java new file mode 100644 index 0000000..06675dd --- /dev/null +++ b/java/org/apache/tomcat/util/threads/LimitLatch.java @@ -0,0 +1,169 @@ +/* + * 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.tomcat.util.threads; + +import java.util.Collection; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.AbstractQueuedSynchronizer; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Shared latch that allows the latch to be acquired a limited number of times + * after which all subsequent requests to acquire the latch will be placed in a + * FIFO queue until one of the shares is returned. + */ +public class LimitLatch { + + private static final Log log = LogFactory.getLog(LimitLatch.class); + + private class Sync extends AbstractQueuedSynchronizer { + private static final long serialVersionUID = 1L; + + Sync() { + } + + @Override + protected int tryAcquireShared(int ignored) { + long newCount = count.incrementAndGet(); + if (!released && newCount > limit) { + // Limit exceeded + count.decrementAndGet(); + return -1; + } else { + return 1; + } + } + + @Override + protected boolean tryReleaseShared(int arg) { + count.decrementAndGet(); + return true; + } + } + + private final Sync sync; + private final AtomicLong count; + private volatile long limit; + private volatile boolean released = false; + + /** + * Instantiates a LimitLatch object with an initial limit. + * @param limit - maximum number of concurrent acquisitions of this latch + */ + public LimitLatch(long limit) { + this.limit = limit; + this.count = new AtomicLong(0); + this.sync = new Sync(); + } + + /** + * Returns the current count for the latch + * @return the current count for latch + */ + public long getCount() { + return count.get(); + } + + /** + * Obtain the current limit. + * @return the limit + */ + public long getLimit() { + return limit; + } + + + /** + * Sets a new limit. If the limit is decreased there may be a period where + * more shares of the latch are acquired than the limit. In this case no + * more shares of the latch will be issued until sufficient shares have been + * returned to reduce the number of acquired shares of the latch to below + * the new limit. If the limit is increased, threads currently in the queue + * may not be issued one of the newly available shares until the next + * request is made for a latch. + * + * @param limit The new limit + */ + public void setLimit(long limit) { + this.limit = limit; + } + + + /** + * Acquires a shared latch if one is available or waits for one if no shared + * latch is current available. + * @throws InterruptedException If the current thread is interrupted + */ + public void countUpOrAwait() throws InterruptedException { + if (log.isTraceEnabled()) { + log.trace("Counting up["+Thread.currentThread().getName()+"] latch="+getCount()); + } + sync.acquireSharedInterruptibly(1); + } + + /** + * Releases a shared latch, making it available for another thread to use. + * @return the previous counter value + */ + public long countDown() { + sync.releaseShared(0); + long result = getCount(); + if (log.isTraceEnabled()) { + log.trace("Counting down["+Thread.currentThread().getName()+"] latch="+result); + } + return result; + } + + /** + * Releases all waiting threads and causes the {@link #limit} to be ignored + * until {@link #reset()} is called. + * @return true if release was done + */ + public boolean releaseAll() { + released = true; + return sync.releaseShared(0); + } + + /** + * Resets the latch and initializes the shared acquisition counter to zero. + * @see #releaseAll() + */ + public void reset() { + this.count.set(0); + released = false; + } + + /** + * Returns true if there is at least one thread waiting to + * acquire the shared lock, otherwise returns false. + * @return true if threads are waiting + */ + public boolean hasQueuedThreads() { + return sync.hasQueuedThreads(); + } + + /** + * Provide access to the list of threads waiting to acquire this limited + * shared latch. + * @return a collection of threads + */ + public Collection getQueuedThreads() { + return sync.getQueuedThreads(); + } +} diff --git a/java/org/apache/tomcat/util/threads/LocalStrings.properties b/java/org/apache/tomcat/util/threads/LocalStrings.properties new file mode 100644 index 0000000..53856f5 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/LocalStrings.properties @@ -0,0 +1,25 @@ +# 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. + +taskQueue.notRunning=Executor not running, can't force a command into the queue + +taskThread.exiting=Thread exiting on purpose + +threadPoolExecutor.invalidKeepAlive=Core threads must have positive keep alive times +threadPoolExecutor.queueFull=Queue capacity is full +threadPoolExecutor.taskRejected=Task [{0}] rejected from [{1}] +threadPoolExecutor.threadStoppedToAvoidPotentialLeak=Stopping thread [{0}] to avoid potential memory leaks after a context was stopped. + +virtualThreadExecutor.taskRejected=Task [{0}] rejected from [{1}] diff --git a/java/org/apache/tomcat/util/threads/LocalStrings_es.properties b/java/org/apache/tomcat/util/threads/LocalStrings_es.properties new file mode 100644 index 0000000..694bee7 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/LocalStrings_es.properties @@ -0,0 +1,16 @@ +# 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. + +threadPoolExecutor.threadStoppedToAvoidPotentialLeak=Parando hilo [{0}] para evita fallos potenciales de memoria tras haberse parado un contexto. diff --git a/java/org/apache/tomcat/util/threads/LocalStrings_fr.properties b/java/org/apache/tomcat/util/threads/LocalStrings_fr.properties new file mode 100644 index 0000000..ff5b2b0 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/LocalStrings_fr.properties @@ -0,0 +1,25 @@ +# 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. + +taskQueue.notRunning=L'exécuteur n'a pas été démarré, impossible d'ajouter une commande à la file d'attente + +taskThread.exiting=Le fil d'exécution s'arrête de manière volontaire + +threadPoolExecutor.invalidKeepAlive=Les threads principaux doivent avoir un temps d'inactivité positif +threadPoolExecutor.queueFull=La file d'attente est pleine +threadPoolExecutor.taskRejected=La tâche [{0}] a été rejetée par [{1}] +threadPoolExecutor.threadStoppedToAvoidPotentialLeak=Arrêt du thread [{0}] pour éviter de potentielles fuites de mémoire après l''arrêt d''un contexte + +virtualThreadExecutor.taskRejected=La tâche [{0}] a été rejetée de [{1}] diff --git a/java/org/apache/tomcat/util/threads/LocalStrings_ja.properties b/java/org/apache/tomcat/util/threads/LocalStrings_ja.properties new file mode 100644 index 0000000..c734424 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/LocalStrings_ja.properties @@ -0,0 +1,25 @@ +# 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. + +taskQueue.notRunning=Executor ãŒå‹•ä½œã—ã¦ã„ãªã„ãŸã‚ã€ã‚³ãƒžãƒ³ãƒ‰ã‚’強制的ã«ã‚­ãƒ¥ãƒ¼ã¸ç™»éŒ²ã§ãã¾ã›ã‚“ + +taskThread.exiting=スレッドãŒæ„図的ã«çµ‚了ã—ã¾ã—㟠+ +threadPoolExecutor.invalidKeepAlive=コアスレッドã¯æ­£ã®ã‚­ãƒ¼ãƒ—アライブ時間ãŒå¿…è¦ã§ã™ +threadPoolExecutor.queueFull=キューãŒä¸€æ¯ã§ã™ +threadPoolExecutor.taskRejected=タスク [{0}] ㌠[{1}] ã‹ã‚‰æ‹’å¦ã•ã‚Œã¾ã—㟠+threadPoolExecutor.threadStoppedToAvoidPotentialLeak=コンテキストãŒåœæ­¢ã—ãŸå¾Œã®æ½œåœ¨çš„ãªãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã‚’防ããŸã‚ã€ã‚¹ãƒ¬ãƒƒãƒ‰ [{0}] ã‚’åœæ­¢ã—ã¦ã„ã¾ã™ã€‚ + +virtualThreadExecutor.taskRejected=タスク [{0}] ㌠[{1}] ã‹ã‚‰æ‹’å¦ã•ã‚Œã¾ã—㟠diff --git a/java/org/apache/tomcat/util/threads/LocalStrings_ko.properties b/java/org/apache/tomcat/util/threads/LocalStrings_ko.properties new file mode 100644 index 0000000..4360b98 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/LocalStrings_ko.properties @@ -0,0 +1,19 @@ +# 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. + +taskQueue.notRunning=Executorê°€ 실행 중ì´ì§€ 않습니다. ëª…ë ¹ì„ ê°•ì œë¡œ íì— ë„£ì„ ìˆ˜ 없습니다. + +threadPoolExecutor.queueFull=íì˜ ìš©ëŸ‰ì´ ê½‰ 찼습니다. +threadPoolExecutor.threadStoppedToAvoidPotentialLeak=컨í…스트가 ì¤‘ì§€ëœ í›„ì— ë°œìƒí•  수 있는 ìž ìž¬ì  ë©”ëª¨ë¦¬ 누수를 피하기 위하여, 쓰레드 [{0}]ì„(를) 중지시킵니다. diff --git a/java/org/apache/tomcat/util/threads/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/util/threads/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..26d70de --- /dev/null +++ b/java/org/apache/tomcat/util/threads/LocalStrings_zh_CN.properties @@ -0,0 +1,19 @@ +# 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. + +taskQueue.notRunning=执行器未è¿è¡Œï¼Œæ— æ³•å¼ºåˆ¶å‘½ä»¤è¿›å…¥é˜Ÿåˆ— + +threadPoolExecutor.queueFull=队列容é‡å·²æ»¡ +threadPoolExecutor.threadStoppedToAvoidPotentialLeak=åœæ­¢çº¿ç¨‹[{0}],从而é¿å…contextåœæ­¢åŽçš„æ½œåœ¨çš„å†…å­˜æ³„æ¼ diff --git a/java/org/apache/tomcat/util/threads/ResizableExecutor.java b/java/org/apache/tomcat/util/threads/ResizableExecutor.java new file mode 100644 index 0000000..f73957e --- /dev/null +++ b/java/org/apache/tomcat/util/threads/ResizableExecutor.java @@ -0,0 +1,44 @@ +/* + * 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.tomcat.util.threads; + +import java.util.concurrent.Executor; + +public interface ResizableExecutor extends Executor { + + /** + * Returns the current number of threads in the pool. + * + * @return the number of threads + */ + int getPoolSize(); + + int getMaxThreads(); + + /** + * Returns the approximate number of threads that are actively executing + * tasks. + * + * @return the number of threads + */ + int getActiveCount(); + + boolean resizePool(int corePoolSize, int maximumPoolSize); + + boolean resizeQueue(int capacity); + +} diff --git a/java/org/apache/tomcat/util/threads/ScheduledThreadPoolExecutor.java b/java/org/apache/tomcat/util/threads/ScheduledThreadPoolExecutor.java new file mode 100644 index 0000000..45d9c93 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/ScheduledThreadPoolExecutor.java @@ -0,0 +1,142 @@ +/* + * 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.tomcat.util.threads; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Class which wraps a ScheduledExecutorService, while preventing + * lifecycle and configuration operations. + */ +public class ScheduledThreadPoolExecutor implements ScheduledExecutorService { + + protected final ScheduledExecutorService executor; + + /** + * Builds a wrapper for the given executor. + * @param executor the wrapped executor + */ + public ScheduledThreadPoolExecutor(ScheduledExecutorService executor) { + this.executor = executor; + } + + @Override + public void shutdown() { + // Do nothing + } + + + @Override + public List shutdownNow() { + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return executor.isShutdown(); + } + + @Override + public boolean isTerminated() { + return executor.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) + throws InterruptedException { + return executor.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + return executor.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return executor.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + return executor.submit(task); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return executor.invokeAll(tasks); + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, + TimeUnit unit) throws InterruptedException { + return executor.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + return executor.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return executor.invokeAny(tasks, timeout, unit); + } + + @Override + public void execute(Runnable command) { + executor.execute(command); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, + TimeUnit unit) { + return executor.schedule(command, delay, unit); + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, + TimeUnit unit) { + return executor.schedule(callable, delay, unit); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable command, + long initialDelay, long period, TimeUnit unit) { + return executor.scheduleAtFixedRate(command, initialDelay, period, unit); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable command, + long initialDelay, long delay, TimeUnit unit) { + return executor.scheduleWithFixedDelay(command, initialDelay, delay, unit); + } + +} diff --git a/java/org/apache/tomcat/util/threads/StopPooledThreadException.java b/java/org/apache/tomcat/util/threads/StopPooledThreadException.java new file mode 100644 index 0000000..e1447e2 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/StopPooledThreadException.java @@ -0,0 +1,31 @@ +/* + * 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.tomcat.util.threads; + + +/** + * A custom {@link RuntimeException} thrown by the {@link ThreadPoolExecutor} + * to signal that the thread should be disposed of. + */ +public class StopPooledThreadException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public StopPooledThreadException(String msg) { + super(msg, null, false, false); + } +} diff --git a/java/org/apache/tomcat/util/threads/TaskQueue.java b/java/org/apache/tomcat/util/threads/TaskQueue.java new file mode 100644 index 0000000..7ff4e3e --- /dev/null +++ b/java/org/apache/tomcat/util/threads/TaskQueue.java @@ -0,0 +1,119 @@ +/* + * 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.tomcat.util.threads; + +import java.util.Collection; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +import org.apache.tomcat.util.res.StringManager; + +/** + * As task queue specifically designed to run with a thread pool executor. The + * task queue is optimised to properly utilize threads within a thread pool + * executor. If you use a normal queue, the executor will spawn threads when + * there are idle threads and you won't be able to force items onto the queue + * itself. + */ +public class TaskQueue extends LinkedBlockingQueue { + + private static final long serialVersionUID = 1L; + protected static final StringManager sm = StringManager.getManager(TaskQueue.class); + + private transient volatile ThreadPoolExecutor parent = null; + + public TaskQueue() { + super(); + } + + public TaskQueue(int capacity) { + super(capacity); + } + + public TaskQueue(Collection c) { + super(c); + } + + public void setParent(ThreadPoolExecutor tp) { + parent = tp; + } + + + /** + * Used to add a task to the queue if the task has been rejected by the Executor. + * + * @param o The task to add to the queue + * + * @return {@code true} if the task was added to the queue, + * otherwise {@code false} + */ + public boolean force(Runnable o) { + if (parent == null || parent.isShutdown()) { + throw new RejectedExecutionException(sm.getString("taskQueue.notRunning")); + } + return super.offer(o); //forces the item onto the queue, to be used if the task is rejected + } + + + @Override + public boolean offer(Runnable o) { + //we can't do any checks + if (parent==null) { + return super.offer(o); + } + //we are maxed out on threads, simply queue the object + if (parent.getPoolSizeNoLock() == parent.getMaximumPoolSize()) { + return super.offer(o); + } + //we have idle threads, just add it to the queue + if (parent.getSubmittedCount() <= parent.getPoolSizeNoLock()) { + return super.offer(o); + } + //if we have less threads than maximum force creation of a new thread + if (parent.getPoolSizeNoLock() < parent.getMaximumPoolSize()) { + return false; + } + //if we reached here, we need to add it to the queue + return super.offer(o); + } + + + @Override + public Runnable poll(long timeout, TimeUnit unit) + throws InterruptedException { + Runnable runnable = super.poll(timeout, unit); + if (runnable == null && parent != null) { + // the poll timed out, it gives an opportunity to stop the current + // thread if needed to avoid memory leaks. + parent.stopCurrentThreadIfNeeded(); + } + return runnable; + } + + @Override + public Runnable take() throws InterruptedException { + if (parent != null && parent.currentThreadShouldBeStopped()) { + return poll(parent.getKeepAliveTime(TimeUnit.MILLISECONDS), + TimeUnit.MILLISECONDS); + // yes, this may return null (in case of timeout) which normally + // does not occur with take() + // but the ThreadPoolExecutor implementation allows this + } + return super.take(); + } +} diff --git a/java/org/apache/tomcat/util/threads/TaskThread.java b/java/org/apache/tomcat/util/threads/TaskThread.java new file mode 100644 index 0000000..b0dbb0a --- /dev/null +++ b/java/org/apache/tomcat/util/threads/TaskThread.java @@ -0,0 +1,73 @@ +/* + * 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.tomcat.util.threads; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * A Thread implementation that records the time at which it was created. + * + */ +public class TaskThread extends Thread { + + private static final Log log = LogFactory.getLog(TaskThread.class); + private static final StringManager sm = StringManager.getManager(TaskThread.class); + private final long creationTime; + + public TaskThread(ThreadGroup group, Runnable target, String name) { + super(group, new WrappingRunnable(target), name); + this.creationTime = System.currentTimeMillis(); + } + + public TaskThread(ThreadGroup group, Runnable target, String name, + long stackSize) { + super(group, new WrappingRunnable(target), name, stackSize); + this.creationTime = System.currentTimeMillis(); + } + + /** + * @return the time (in ms) at which this thread was created + */ + public final long getCreationTime() { + return creationTime; + } + + /** + * Wraps a {@link Runnable} to swallow any {@link StopPooledThreadException} + * instead of letting it go and potentially trigger a break in a debugger. + */ + private static class WrappingRunnable implements Runnable { + private Runnable wrappedRunnable; + WrappingRunnable(Runnable wrappedRunnable) { + this.wrappedRunnable = wrappedRunnable; + } + @Override + public void run() { + try { + wrappedRunnable.run(); + } catch(StopPooledThreadException exc) { + //expected : we just swallow the exception to avoid disturbing + //debuggers like eclipse's + log.debug(sm.getString("taskThread.exiting"), exc); + } + } + + } + +} diff --git a/java/org/apache/tomcat/util/threads/TaskThreadFactory.java b/java/org/apache/tomcat/util/threads/TaskThreadFactory.java new file mode 100644 index 0000000..4320f9b --- /dev/null +++ b/java/org/apache/tomcat/util/threads/TaskThreadFactory.java @@ -0,0 +1,71 @@ +/* + * 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.tomcat.util.threads; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.tomcat.util.security.PrivilegedSetAccessControlContext; +import org.apache.tomcat.util.security.PrivilegedSetTccl; + +/** + * Simple task thread factory to use to create threads for an executor + * implementation. + */ +public class TaskThreadFactory implements ThreadFactory { + + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + private final boolean daemon; + private final int threadPriority; + + public TaskThreadFactory(String namePrefix, boolean daemon, int priority) { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + this.namePrefix = namePrefix; + this.daemon = daemon; + this.threadPriority = priority; + } + + @Override + public Thread newThread(Runnable r) { + TaskThread t = new TaskThread(group, r, namePrefix + threadNumber.getAndIncrement()); + t.setDaemon(daemon); + t.setPriority(threadPriority); + + if (Constants.IS_SECURITY_ENABLED) { + // Set the context class loader of newly created threads to be the + // class loader that loaded this factory. This avoids retaining + // references to web application class loaders and similar. + PrivilegedAction pa = new PrivilegedSetTccl( + t, getClass().getClassLoader()); + AccessController.doPrivileged(pa); + + // This method may be triggered from an InnocuousThread. Ensure that + // the thread inherits an appropriate AccessControlContext + pa = new PrivilegedSetAccessControlContext(t); + AccessController.doPrivileged(pa); + } else { + t.setContextClassLoader(getClass().getClassLoader()); + } + + return t; + } +} diff --git a/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java b/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java new file mode 100644 index 0000000..7b7a919 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java @@ -0,0 +1,2359 @@ +/* + * 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. + */ + +/* + * The original version of this file carried the following notice: + * + * Written by Doug Lea with assistance from members of JCP JSR-166 + * Expert Group and released to the public domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +package org.apache.tomcat.util.threads; + +import java.util.ArrayList; +import java.util.ConcurrentModificationException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.AbstractQueuedSynchronizer; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.tomcat.util.res.StringManager; + +/** + * An {@link java.util.concurrent.ExecutorService} + * that executes each submitted task using + * one of possibly several pooled threads, normally configured + * using {@link Executors} factory methods. + * + *

    Thread pools address two different problems: they usually + * provide improved performance when executing large numbers of + * asynchronous tasks, due to reduced per-task invocation overhead, + * and they provide a means of bounding and managing the resources, + * including threads, consumed when executing a collection of tasks. + * Each {@code ThreadPoolExecutor} also maintains some basic + * statistics, such as the number of completed tasks. + * + *

    To be useful across a wide range of contexts, this class + * provides many adjustable parameters and extensibility + * hooks. However, programmers are urged to use the more convenient + * {@link Executors} factory methods {@link + * Executors#newCachedThreadPool} (unbounded thread pool, with + * automatic thread reclamation), {@link Executors#newFixedThreadPool} + * (fixed size thread pool) and {@link + * Executors#newSingleThreadExecutor} (single background thread), that + * preconfigure settings for the most common usage + * scenarios. Otherwise, use the following guide when manually + * configuring and tuning this class: + * + *

    + * + *
    Core and maximum pool sizes
    + * + *
    A {@code ThreadPoolExecutor} will automatically adjust the + * pool size (see {@link #getPoolSize}) + * according to the bounds set by + * corePoolSize (see {@link #getCorePoolSize}) and + * maximumPoolSize (see {@link #getMaximumPoolSize}). + * + * When a new task is submitted in method {@link #execute(Runnable)}, + * if fewer than corePoolSize threads are running, a new thread is + * created to handle the request, even if other worker threads are + * idle. Else if fewer than maximumPoolSize threads are running, a + * new thread will be created to handle the request only if the queue + * is full. By setting corePoolSize and maximumPoolSize the same, you + * create a fixed-size thread pool. By setting maximumPoolSize to an + * essentially unbounded value such as {@code Integer.MAX_VALUE}, you + * allow the pool to accommodate an arbitrary number of concurrent + * tasks. Most typically, core and maximum pool sizes are set only + * upon construction, but they may also be changed dynamically using + * {@link #setCorePoolSize} and {@link #setMaximumPoolSize}.
    + * + *
    On-demand construction
    + * + *
    By default, even core threads are initially created and + * started only when new tasks arrive, but this can be overridden + * dynamically using method {@link #prestartCoreThread} or {@link + * #prestartAllCoreThreads}. You probably want to prestart threads if + * you construct the pool with a non-empty queue.
    + * + *
    Creating new threads
    + * + *
    New threads are created using a {@link ThreadFactory}. If not + * otherwise specified, a {@link Executors#defaultThreadFactory} is + * used, that creates threads to all be in the same {@link + * ThreadGroup} and with the same {@code NORM_PRIORITY} priority and + * non-daemon status. By supplying a different ThreadFactory, you can + * alter the thread's name, thread group, priority, daemon status, + * etc. If a {@code ThreadFactory} fails to create a thread when asked + * by returning null from {@code newThread}, the executor will + * continue, but might not be able to execute any tasks. Threads + * should possess the "modifyThread" {@code RuntimePermission}. If + * worker threads or other threads using the pool do not possess this + * permission, service may be degraded: configuration changes may not + * take effect in a timely manner, and a shutdown pool may remain in a + * state in which termination is possible but not completed.
    + * + *
    Keep-alive times
    + * + *
    If the pool currently has more than corePoolSize threads, + * excess threads will be terminated if they have been idle for more + * than the keepAliveTime (see {@link #getKeepAliveTime(TimeUnit)}). + * This provides a means of reducing resource consumption when the + * pool is not being actively used. If the pool becomes more active + * later, new threads will be constructed. This parameter can also be + * changed dynamically using method {@link #setKeepAliveTime(long, + * TimeUnit)}. Using a value of {@code Long.MAX_VALUE} {@link + * TimeUnit#NANOSECONDS} effectively disables idle threads from ever + * terminating prior to shut down. By default, the keep-alive policy + * applies only when there are more than corePoolSize threads, but + * method {@link #allowCoreThreadTimeOut(boolean)} can be used to + * apply this time-out policy to core threads as well, so long as the + * keepAliveTime value is non-zero.
    + * + *
    Queuing
    + * + *
    Any {@link BlockingQueue} may be used to transfer and hold + * submitted tasks. The use of this queue interacts with pool sizing: + * + *
      + * + *
    • If fewer than corePoolSize threads are running, the Executor + * always prefers adding a new thread + * rather than queuing. + * + *
    • If corePoolSize or more threads are running, the Executor + * always prefers queuing a request rather than adding a new + * thread. + * + *
    • If a request cannot be queued, a new thread is created unless + * this would exceed maximumPoolSize, in which case, the task will be + * rejected. + * + *
    + * + * There are three general strategies for queuing: + *
      + * + *
    1. Direct handoffs. A good default choice for a work + * queue is a {@link java.util.concurrent.SynchronousQueue} + * that hands off tasks to threads + * without otherwise holding them. Here, an attempt to queue a task + * will fail if no threads are immediately available to run it, so a + * new thread will be constructed. This policy avoids lockups when + * handling sets of requests that might have internal dependencies. + * Direct handoffs generally require unbounded maximumPoolSizes to + * avoid rejection of new submitted tasks. This in turn admits the + * possibility of unbounded thread growth when commands continue to + * arrive on average faster than they can be processed. + * + *
    2. Unbounded queues. Using an unbounded queue (for + * example a {@link java.util.concurrent.LinkedBlockingQueue} + * without a predefined + * capacity) will cause new tasks to wait in the queue when all + * corePoolSize threads are busy. Thus, no more than corePoolSize + * threads will ever be created. (And the value of the maximumPoolSize + * therefore doesn't have any effect.) This may be appropriate when + * each task is completely independent of others, so tasks cannot + * affect each others execution; for example, in a web page server. + * While this style of queuing can be useful in smoothing out + * transient bursts of requests, it admits the possibility of + * unbounded work queue growth when commands continue to arrive on + * average faster than they can be processed. + * + *
    3. Bounded queues. A bounded queue (for example, an + * {@link java.util.concurrent.ArrayBlockingQueue}) + * helps prevent resource exhaustion when + * used with finite maximumPoolSizes, but can be more difficult to + * tune and control. Queue sizes and maximum pool sizes may be traded + * off for each other: Using large queues and small pools minimizes + * CPU usage, OS resources, and context-switching overhead, but can + * lead to artificially low throughput. If tasks frequently block (for + * example if they are I/O bound), a system may be able to schedule + * time for more threads than you otherwise allow. Use of small queues + * generally requires larger pool sizes, which keeps CPUs busier but + * may encounter unacceptable scheduling overhead, which also + * decreases throughput. + * + *
    + * + *
    + * + *
    Rejected tasks
    + * + *
    New tasks submitted in method {@link #execute(Runnable)} will be + * rejected when the Executor has been shut down, and also when + * the Executor uses finite bounds for both maximum threads and work queue + * capacity, and is saturated. In either case, the {@code execute} method + * invokes the {@link + * RejectedExecutionHandler#rejectedExecution(Runnable, ThreadPoolExecutor)} + * method of its {@link RejectedExecutionHandler}. Four predefined handler + * policies are provided: + * + *
      + * + *
    1. In the default {@link ThreadPoolExecutor.AbortPolicy}, the handler + * throws a runtime {@link RejectedExecutionException} upon rejection. + * + *
    2. In {@link ThreadPoolExecutor.CallerRunsPolicy}, the thread + * that invokes {@code execute} itself runs the task. This provides a + * simple feedback control mechanism that will slow down the rate that + * new tasks are submitted. + * + *
    3. In {@link ThreadPoolExecutor.DiscardPolicy}, a task that cannot + * be executed is simply dropped. This policy is designed only for + * those rare cases in which task completion is never relied upon. + * + *
    4. In {@link ThreadPoolExecutor.DiscardOldestPolicy}, if the + * executor is not shut down, the task at the head of the work queue + * is dropped, and then execution is retried (which can fail again, + * causing this to be repeated.) This policy is rarely acceptable. In + * nearly all cases, you should also cancel the task to cause an + * exception in any component waiting for its completion, and/or log + * the failure, as illustrated in {@link + * ThreadPoolExecutor.DiscardOldestPolicy} documentation. + * + *
    + * + * It is possible to define and use other kinds of {@link + * RejectedExecutionHandler} classes. Doing so requires some care + * especially when policies are designed to work only under particular + * capacity or queuing policies.
    + * + *
    Hook methods
    + * + *
    This class provides {@code protected} overridable + * {@link #beforeExecute(Thread, Runnable)} and + * {@link #afterExecute(Runnable, Throwable)} methods that are called + * before and after execution of each task. These can be used to + * manipulate the execution environment; for example, reinitializing + * ThreadLocals, gathering statistics, or adding log entries. + * Additionally, method {@link #terminated} can be overridden to perform + * any special processing that needs to be done once the Executor has + * fully terminated. + * + *

    If hook, callback, or BlockingQueue methods throw exceptions, + * internal worker threads may in turn fail, abruptly terminate, and + * possibly be replaced.

    + * + *
    Queue maintenance
    + * + *
    Method {@link #getQueue()} allows access to the work queue + * for purposes of monitoring and debugging. Use of this method for + * any other purpose is strongly discouraged. Two supplied methods, + * {@link #remove(Runnable)} and {@link #purge} are available to + * assist in storage reclamation when large numbers of queued tasks + * become cancelled.
    + * + *
    Reclamation
    + * + *
    A pool that is no longer referenced in a program AND + * has no remaining threads may be reclaimed (garbage collected) + * without being explicitly shutdown. You can configure a pool to + * allow all unused threads to eventually die by setting appropriate + * keep-alive times, using a lower bound of zero core threads and/or + * setting {@link #allowCoreThreadTimeOut(boolean)}.
    + * + *
    + * + *

    Extension example. Most extensions of this class + * override one or more of the protected hook methods. For example, + * here is a subclass that adds a simple pause/resume feature: + * + *

     {@code
    + * class PausableThreadPoolExecutor extends ThreadPoolExecutor {
    + *   private boolean isPaused;
    + *   private ReentrantLock pauseLock = new ReentrantLock();
    + *   private Condition unpaused = pauseLock.newCondition();
    + *
    + *   public PausableThreadPoolExecutor(...) { super(...); }
    + *
    + *   protected void beforeExecute(Thread t, Runnable r) {
    + *     super.beforeExecute(t, r);
    + *     pauseLock.lock();
    + *     try {
    + *       while (isPaused) unpaused.await();
    + *     } catch (InterruptedException ie) {
    + *       t.interrupt();
    + *     } finally {
    + *       pauseLock.unlock();
    + *     }
    + *   }
    + *
    + *   public void pause() {
    + *     pauseLock.lock();
    + *     try {
    + *       isPaused = true;
    + *     } finally {
    + *       pauseLock.unlock();
    + *     }
    + *   }
    + *
    + *   public void resume() {
    + *     pauseLock.lock();
    + *     try {
    + *       isPaused = false;
    + *       unpaused.signalAll();
    + *     } finally {
    + *       pauseLock.unlock();
    + *     }
    + *   }
    + * }}
    + * + * @since 1.5 + * @author Doug Lea + */ +public class ThreadPoolExecutor extends AbstractExecutorService { + + protected static final StringManager sm = StringManager.getManager(ThreadPoolExecutor.class); + + /** + * The main pool control state, ctl, is an atomic integer packing + * two conceptual fields + * workerCount, indicating the effective number of threads + * runState, indicating whether running, shutting down etc + * + * In order to pack them into one int, we limit workerCount to + * (2^29)-1 (about 500 million) threads rather than (2^31)-1 (2 + * billion) otherwise representable. If this is ever an issue in + * the future, the variable can be changed to be an AtomicLong, + * and the shift/mask constants below adjusted. But until the need + * arises, this code is a bit faster and simpler using an int. + * + * The workerCount is the number of workers that have been + * permitted to start and not permitted to stop. The value may be + * transiently different from the actual number of live threads, + * for example when a ThreadFactory fails to create a thread when + * asked, and when exiting threads are still performing + * bookkeeping before terminating. The user-visible pool size is + * reported as the current size of the workers set. + * + * The runState provides the main lifecycle control, taking on values: + * + * RUNNING: Accept new tasks and process queued tasks + * SHUTDOWN: Don't accept new tasks, but process queued tasks + * STOP: Don't accept new tasks, don't process queued tasks, + * and interrupt in-progress tasks + * TIDYING: All tasks have terminated, workerCount is zero, + * the thread transitioning to state TIDYING + * will run the terminated() hook method + * TERMINATED: terminated() has completed + * + * The numerical order among these values matters, to allow + * ordered comparisons. The runState monotonically increases over + * time, but need not hit each state. The transitions are: + * + * RUNNING -> SHUTDOWN + * On invocation of shutdown() + * (RUNNING or SHUTDOWN) -> STOP + * On invocation of shutdownNow() + * SHUTDOWN -> TIDYING + * When both queue and pool are empty + * STOP -> TIDYING + * When pool is empty + * TIDYING -> TERMINATED + * When the terminated() hook method has completed + * + * Threads waiting in awaitTermination() will return when the + * state reaches TERMINATED. + * + * Detecting the transition from SHUTDOWN to TIDYING is less + * straightforward than you'd like because the queue may become + * empty after non-empty and vice versa during SHUTDOWN state, but + * we can only terminate if, after seeing that it is empty, we see + * that workerCount is 0 (which sometimes entails a recheck -- see + * below). + */ + private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + private static final int COUNT_BITS = Integer.SIZE - 3; + private static final int COUNT_MASK = (1 << COUNT_BITS) - 1; + + // runState is stored in the high-order bits + private static final int RUNNING = -1 << COUNT_BITS; + private static final int SHUTDOWN = 0 << COUNT_BITS; + private static final int STOP = 1 << COUNT_BITS; + private static final int TIDYING = 2 << COUNT_BITS; + private static final int TERMINATED = 3 << COUNT_BITS; + + // Packing and unpacking ctl + private static int workerCountOf(int c) { return c & COUNT_MASK; } + private static int ctlOf(int rs, int wc) { return rs | wc; } + + /* + * Bit field accessors that don't require unpacking ctl. + * These depend on the bit layout and on workerCount being never negative. + */ + + private static boolean runStateLessThan(int c, int s) { + return c < s; + } + + private static boolean runStateAtLeast(int c, int s) { + return c >= s; + } + + private static boolean isRunning(int c) { + return c < SHUTDOWN; + } + + /** + * Attempts to CAS-increment the workerCount field of ctl. + */ + private boolean compareAndIncrementWorkerCount(int expect) { + return ctl.compareAndSet(expect, expect + 1); + } + + /** + * Attempts to CAS-decrement the workerCount field of ctl. + */ + private boolean compareAndDecrementWorkerCount(int expect) { + return ctl.compareAndSet(expect, expect - 1); + } + + /** + * Decrements the workerCount field of ctl. This is called only on + * abrupt termination of a thread (see processWorkerExit). Other + * decrements are performed within getTask. + */ + private void decrementWorkerCount() { + ctl.addAndGet(-1); + } + + /** + * The queue used for holding tasks and handing off to worker + * threads. We do not require that workQueue.poll() returning + * null necessarily means that workQueue.isEmpty(), so rely + * solely on isEmpty to see if the queue is empty (which we must + * do for example when deciding whether to transition from + * SHUTDOWN to TIDYING). This accommodates special-purpose + * queues such as DelayQueues for which poll() is allowed to + * return null even if it may later return non-null when delays + * expire. + */ + private final BlockingQueue workQueue; + + /** + * Lock held on access to workers set and related bookkeeping. + * While we could use a concurrent set of some sort, it turns out + * to be generally preferable to use a lock. Among the reasons is + * that this serializes interruptIdleWorkers, which avoids + * unnecessary interrupt storms, especially during shutdown. + * Otherwise exiting threads would concurrently interrupt those + * that have not yet interrupted. It also simplifies some of the + * associated statistics bookkeeping of largestPoolSize etc. We + * also hold mainLock on shutdown and shutdownNow, for the sake of + * ensuring workers set is stable while separately checking + * permission to interrupt and actually interrupting. + */ + private final ReentrantLock mainLock = new ReentrantLock(); + + /** + * Set containing all worker threads in pool. Accessed only when + * holding mainLock. + */ + private final HashSet workers = new HashSet<>(); + + /** + * Wait condition to support awaitTermination. + */ + private final Condition termination = mainLock.newCondition(); + + /** + * Tracks largest attained pool size. Accessed only under + * mainLock. + */ + private int largestPoolSize; + + /** + * Counter for completed tasks. Updated only on termination of + * worker threads. Accessed only under mainLock. + */ + private long completedTaskCount; + + /** + * The number of tasks submitted but not yet finished. This includes tasks + * in the queue and tasks that have been handed to a worker thread but the + * latter did not start executing the task yet. + * This number is always greater or equal to {@link #getActiveCount()}. + */ + private final AtomicInteger submittedCount = new AtomicInteger(0); + private final AtomicLong lastContextStoppedTime = new AtomicLong(0L); + + /** + * Most recent time in ms when a thread decided to kill itself to avoid + * potential memory leaks. Useful to throttle the rate of renewals of + * threads. + */ + private final AtomicLong lastTimeThreadKilledItself = new AtomicLong(0L); + + /* + * All user control parameters are declared as volatiles so that + * ongoing actions are based on freshest values, but without need + * for locking, since no internal invariants depend on them + * changing synchronously with respect to other actions. + */ + + /** + * Delay in ms between 2 threads being renewed. If negative, do not renew threads. + */ + private volatile long threadRenewalDelay = Constants.DEFAULT_THREAD_RENEWAL_DELAY; + + /** + * Factory for new threads. All threads are created using this + * factory (via method addWorker). All callers must be prepared + * for addWorker to fail, which may reflect a system or user's + * policy limiting the number of threads. Even though it is not + * treated as an error, failure to create threads may result in + * new tasks being rejected or existing ones remaining stuck in + * the queue. + * + * We go further and preserve pool invariants even in the face of + * errors such as OutOfMemoryError, that might be thrown while + * trying to create threads. Such errors are rather common due to + * the need to allocate a native stack in Thread.start, and users + * will want to perform clean pool shutdown to clean up. There + * will likely be enough memory available for the cleanup code to + * complete without encountering yet another OutOfMemoryError. + */ + private volatile ThreadFactory threadFactory; + + /** + * Handler called when saturated or shutdown in execute. + */ + private volatile RejectedExecutionHandler handler; + + /** + * Timeout in nanoseconds for idle threads waiting for work. + * Threads use this timeout when there are more than corePoolSize + * present or if allowCoreThreadTimeOut. Otherwise they wait + * forever for new work. + */ + private volatile long keepAliveTime; + + /** + * If false (default), core threads stay alive even when idle. + * If true, core threads use keepAliveTime to time out waiting + * for work. + */ + private volatile boolean allowCoreThreadTimeOut; + + /** + * Core pool size is the minimum number of workers to keep alive + * (and not allow to time out etc) unless allowCoreThreadTimeOut + * is set, in which case the minimum is zero. + * + * Since the worker count is actually stored in COUNT_BITS bits, + * the effective limit is {@code corePoolSize & COUNT_MASK}. + */ + private volatile int corePoolSize; + + /** + * Maximum pool size. + * + * Since the worker count is actually stored in COUNT_BITS bits, + * the effective limit is {@code maximumPoolSize & COUNT_MASK}. + */ + private volatile int maximumPoolSize; + + /** + * The default rejected execution handler. + */ + private static final RejectedExecutionHandler defaultHandler = new RejectPolicy(); + + /** + * Permission required for callers of shutdown and shutdownNow. + * We additionally require (see checkShutdownAccess) that callers + * have permission to actually interrupt threads in the worker set + * (as governed by Thread.interrupt, which relies on + * ThreadGroup.checkAccess, which in turn relies on + * SecurityManager.checkAccess). Shutdowns are attempted only if + * these checks pass. + * + * All actual invocations of Thread.interrupt (see + * interruptIdleWorkers and interruptWorkers) ignore + * SecurityExceptions, meaning that the attempted interrupts + * silently fail. In the case of shutdown, they should not fail + * unless the SecurityManager has inconsistent policies, sometimes + * allowing access to a thread and sometimes not. In such cases, + * failure to actually interrupt threads may disable or delay full + * termination. Other uses of interruptIdleWorkers are advisory, + * and failure to actually interrupt will merely delay response to + * configuration changes so is not handled exceptionally. + */ + private static final RuntimePermission shutdownPerm = + new RuntimePermission("modifyThread"); + + /** + * Class Worker mainly maintains interrupt control state for + * threads running tasks, along with other minor bookkeeping. + * This class opportunistically extends AbstractQueuedSynchronizer + * to simplify acquiring and releasing a lock surrounding each + * task execution. This protects against interrupts that are + * intended to wake up a worker thread waiting for a task from + * instead interrupting a task being run. We implement a simple + * non-reentrant mutual exclusion lock rather than use + * ReentrantLock because we do not want worker tasks to be able to + * reacquire the lock when they invoke pool control methods like + * setCorePoolSize. Additionally, to suppress interrupts until + * the thread actually starts running tasks, we initialize lock + * state to a negative value, and clear it upon start (in + * runWorker). + */ + private final class Worker + extends AbstractQueuedSynchronizer + implements Runnable + { + /** + * This class will never be serialized, but we provide a + * serialVersionUID to suppress a javac warning. + */ + private static final long serialVersionUID = 6138294804551838833L; + + /** Thread this worker is running in. Null if factory fails. */ + final Thread thread; + /** Initial task to run. Possibly null. */ + Runnable firstTask; + /** Per-thread task counter */ + volatile long completedTasks; + + // TODO: switch to AbstractQueuedLongSynchronizer and move + // completedTasks into the lock word. + + /** + * Creates with given first task and thread from ThreadFactory. + * @param firstTask the first task (null if none) + */ + Worker(Runnable firstTask) { + setState(-1); // inhibit interrupts until runWorker + this.firstTask = firstTask; + this.thread = getThreadFactory().newThread(this); + } + + /** Delegates main run loop to outer runWorker. */ + @Override + public void run() { + runWorker(this); + } + + // Lock methods + // + // The value 0 represents the unlocked state. + // The value 1 represents the locked state. + + @Override + protected boolean isHeldExclusively() { + return getState() != 0; + } + + @Override + protected boolean tryAcquire(int unused) { + if (compareAndSetState(0, 1)) { + setExclusiveOwnerThread(Thread.currentThread()); + return true; + } + return false; + } + + @Override + protected boolean tryRelease(int unused) { + setExclusiveOwnerThread(null); + setState(0); + return true; + } + + public void lock() { acquire(1); } + public boolean tryLock() { return tryAcquire(1); } + public void unlock() { release(1); } + public boolean isLocked() { return isHeldExclusively(); } + + void interruptIfStarted() { + Thread t; + if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { + try { + t.interrupt(); + } catch (SecurityException ignore) { + } + } + } + } + + /* + * Methods for setting control state + */ + + /** + * Transitions runState to given target, or leaves it alone if + * already at least the given target. + * + * @param targetState the desired state, either SHUTDOWN or STOP + * (but not TIDYING or TERMINATED -- use tryTerminate for that) + */ + private void advanceRunState(int targetState) { + // assert targetState == SHUTDOWN || targetState == STOP; + for (;;) { + int c = ctl.get(); + if (runStateAtLeast(c, targetState) || + ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))) { + break; + } + } + } + + /** + * Transitions to TERMINATED state if either (SHUTDOWN and pool + * and queue empty) or (STOP and pool empty). If otherwise + * eligible to terminate but workerCount is nonzero, interrupts an + * idle worker to ensure that shutdown signals propagate. This + * method must be called following any action that might make + * termination possible -- reducing worker count or removing tasks + * from the queue during shutdown. The method is non-private to + * allow access from ScheduledThreadPoolExecutor. + */ + final void tryTerminate() { + for (;;) { + int c = ctl.get(); + if (isRunning(c) || + runStateAtLeast(c, TIDYING) || + (runStateLessThan(c, STOP) && ! workQueue.isEmpty())) { + return; + } + if (workerCountOf(c) != 0) { // Eligible to terminate + interruptIdleWorkers(ONLY_ONE); + return; + } + + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { + try { + terminated(); + } finally { + ctl.set(ctlOf(TERMINATED, 0)); + termination.signalAll(); + } + return; + } + } finally { + mainLock.unlock(); + } + // else retry on failed CAS + } + } + + /* + * Methods for controlling interrupts to worker threads. + */ + + /** + * If there is a security manager, makes sure caller has + * permission to shut down threads in general (see shutdownPerm). + * If this passes, additionally makes sure the caller is allowed + * to interrupt each worker thread. This might not be true even if + * first check passed, if the SecurityManager treats some threads + * specially. + */ + private void checkShutdownAccess() { + // assert mainLock.isHeldByCurrentThread(); + SecurityManager security = System.getSecurityManager(); + if (security != null) { + security.checkPermission(shutdownPerm); + for (Worker w : workers) { + security.checkAccess(w.thread); + } + } + } + + /** + * Interrupts all threads, even if active. Ignores SecurityExceptions + * (in which case some threads may remain uninterrupted). + */ + private void interruptWorkers() { + // assert mainLock.isHeldByCurrentThread(); + for (Worker w : workers) { + w.interruptIfStarted(); + } + } + + /** + * Interrupts threads that might be waiting for tasks (as + * indicated by not being locked) so they can check for + * termination or configuration changes. Ignores + * SecurityExceptions (in which case some threads may remain + * uninterrupted). + * + * @param onlyOne If true, interrupt at most one worker. This is + * called only from tryTerminate when termination is otherwise + * enabled but there are still other workers. In this case, at + * most one waiting worker is interrupted to propagate shutdown + * signals in case all threads are currently waiting. + * Interrupting any arbitrary thread ensures that newly arriving + * workers since shutdown began will also eventually exit. + * To guarantee eventual termination, it suffices to always + * interrupt only one idle worker, but shutdown() interrupts all + * idle workers so that redundant workers exit promptly, not + * waiting for a straggler task to finish. + */ + private void interruptIdleWorkers(boolean onlyOne) { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + for (Worker w : workers) { + Thread t = w.thread; + if (!t.isInterrupted() && w.tryLock()) { + try { + t.interrupt(); + } catch (SecurityException ignore) { + } finally { + w.unlock(); + } + } + if (onlyOne) { + break; + } + } + } finally { + mainLock.unlock(); + } + } + + /** + * Common form of interruptIdleWorkers, to avoid having to + * remember what the boolean argument means. + */ + private void interruptIdleWorkers() { + interruptIdleWorkers(false); + } + + private static final boolean ONLY_ONE = true; + + /* + * Misc utilities, most of which are also exported to + * ScheduledThreadPoolExecutor + */ + + /** + * Invokes the rejected execution handler for the given command. + * Package-protected for use by ScheduledThreadPoolExecutor. + */ + final void reject(Runnable command) { + handler.rejectedExecution(command, this); + } + + /** + * Performs any further cleanup following run state transition on + * invocation of shutdown. A no-op here, but used by + * ScheduledThreadPoolExecutor to cancel delayed tasks. + */ + void onShutdown() { + } + + /** + * Drains the task queue into a new list, normally using + * drainTo. But if the queue is a DelayQueue or any other kind of + * queue for which poll or drainTo may fail to remove some + * elements, it deletes them one by one. + */ + private List drainQueue() { + BlockingQueue q = workQueue; + ArrayList taskList = new ArrayList<>(); + q.drainTo(taskList); + if (!q.isEmpty()) { + for (Runnable r : q.toArray(new Runnable[0])) { + if (q.remove(r)) { + taskList.add(r); + } + } + } + return taskList; + } + + /* + * Methods for creating, running and cleaning up after workers + */ + + /** + * Checks if a new worker can be added with respect to current + * pool state and the given bound (either core or maximum). If so, + * the worker count is adjusted accordingly, and, if possible, a + * new worker is created and started, running firstTask as its + * first task. This method returns false if the pool is stopped or + * eligible to shut down. It also returns false if the thread + * factory fails to create a thread when asked. If the thread + * creation fails, either due to the thread factory returning + * null, or due to an exception (typically OutOfMemoryError in + * Thread.start()), we roll back cleanly. + * + * @param firstTask the task the new thread should run first (or + * null if none). Workers are created with an initial first task + * (in method execute()) to bypass queuing when there are fewer + * than corePoolSize threads (in which case we always start one), + * or when the queue is full (in which case we must bypass queue). + * Initially idle threads are usually created via + * prestartCoreThread or to replace other dying workers. + * + * @param core if true use corePoolSize as bound, else + * maximumPoolSize. (A boolean indicator is used here rather than a + * value to ensure reads of fresh values after checking other pool + * state). + * @return true if successful + */ + private boolean addWorker(Runnable firstTask, boolean core) { + retry: + for (int c = ctl.get();;) { + // Check if queue empty only if necessary. + if (runStateAtLeast(c, SHUTDOWN) + && (runStateAtLeast(c, STOP) + || firstTask != null + || workQueue.isEmpty())) { + return false; + } + + for (;;) { + if (workerCountOf(c) + >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK)) { + return false; + } + if (compareAndIncrementWorkerCount(c)) { + break retry; + } + c = ctl.get(); // Re-read ctl + if (runStateAtLeast(c, SHUTDOWN)) + { + continue retry; + // else CAS failed due to workerCount change; retry inner loop + } + } + } + + boolean workerStarted = false; + boolean workerAdded = false; + Worker w = null; + try { + w = new Worker(firstTask); + final Thread t = w.thread; + if (t != null) { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + // Recheck while holding lock. + // Back out on ThreadFactory failure or if + // shut down before lock acquired. + int c = ctl.get(); + + if (isRunning(c) || + (runStateLessThan(c, STOP) && firstTask == null)) { + if (t.getState() != Thread.State.NEW) { + throw new IllegalThreadStateException(); + } + workers.add(w); + workerAdded = true; + int s = workers.size(); + if (s > largestPoolSize) { + largestPoolSize = s; + } + } + } finally { + mainLock.unlock(); + } + if (workerAdded) { + t.start(); + workerStarted = true; + } + } + } finally { + if (! workerStarted) { + addWorkerFailed(w); + } + } + return workerStarted; + } + + /** + * Rolls back the worker thread creation. + * - removes worker from workers, if present + * - decrements worker count + * - rechecks for termination, in case the existence of this + * worker was holding up termination + */ + private void addWorkerFailed(Worker w) { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + if (w != null) { + workers.remove(w); + } + decrementWorkerCount(); + tryTerminate(); + } finally { + mainLock.unlock(); + } + } + + /** + * Performs cleanup and bookkeeping for a dying worker. Called + * only from worker threads. Unless completedAbruptly is set, + * assumes that workerCount has already been adjusted to account + * for exit. This method removes thread from worker set, and + * possibly terminates the pool or replaces the worker if either + * it exited due to user task exception or if fewer than + * corePoolSize workers are running or queue is non-empty but + * there are no workers. + * + * @param w the worker + * @param completedAbruptly if the worker died due to user exception + */ + private void processWorkerExit(Worker w, boolean completedAbruptly) { + if (completedAbruptly) { + decrementWorkerCount(); + } + + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + completedTaskCount += w.completedTasks; + workers.remove(w); + } finally { + mainLock.unlock(); + } + + tryTerminate(); + + int c = ctl.get(); + if (runStateLessThan(c, STOP)) { + if (!completedAbruptly) { + int min = allowCoreThreadTimeOut ? 0 : corePoolSize; + if (min == 0 && ! workQueue.isEmpty()) { + min = 1; + } + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65454 + // If the work queue is not empty, it is likely that a task was + // added to the work queue between this thread timing out and + // the worker count being decremented a few lines above this + // comment. In this case, create a replacement worker so that + // the task isn't held in the queue waiting for one of the other + // workers to finish. + if (workerCountOf(c) >= min && workQueue.isEmpty()) { + return; // replacement not needed + } + } + addWorker(null, false); + } + } + + /** + * Performs blocking or timed wait for a task, depending on + * current configuration settings, or returns null if this worker + * must exit because of any of: + * 1. There are more than maximumPoolSize workers (due to + * a call to setMaximumPoolSize). + * 2. The pool is stopped. + * 3. The pool is shutdown and the queue is empty. + * 4. This worker timed out waiting for a task, and timed-out + * workers are subject to termination (that is, + * {@code allowCoreThreadTimeOut || workerCount > corePoolSize}) + * both before and after the timed wait, and if the queue is + * non-empty, this worker is not the last thread in the pool. + * + * @return task, or null if the worker must exit, in which case + * workerCount is decremented + */ + private Runnable getTask() { + boolean timedOut = false; // Did the last poll() time out? + + for (;;) { + int c = ctl.get(); + + // Check if queue empty only if necessary. + if (runStateAtLeast(c, SHUTDOWN) + && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) { + decrementWorkerCount(); + return null; + } + + int wc = workerCountOf(c); + + // Are workers subject to culling? + boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; + + if ((wc > maximumPoolSize || (timed && timedOut)) + && (wc > 1 || workQueue.isEmpty())) { + if (compareAndDecrementWorkerCount(c)) { + return null; + } + continue; + } + + try { + Runnable r = timed ? + workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : + workQueue.take(); + if (r != null) { + return r; + } + timedOut = true; + } catch (InterruptedException retry) { + timedOut = false; + } + } + } + + /** + * Main worker run loop. Repeatedly gets tasks from queue and + * executes them, while coping with a number of issues: + * + * 1. We may start out with an initial task, in which case we + * don't need to get the first one. Otherwise, as long as pool is + * running, we get tasks from getTask. If it returns null then the + * worker exits due to changed pool state or configuration + * parameters. Other exits result from exception throws in + * external code, in which case completedAbruptly holds, which + * usually leads processWorkerExit to replace this thread. + * + * 2. Before running any task, the lock is acquired to prevent + * other pool interrupts while the task is executing, and then we + * ensure that unless pool is stopping, this thread does not have + * its interrupt set. + * + * 3. Each task run is preceded by a call to beforeExecute, which + * might throw an exception, in which case we cause thread to die + * (breaking loop with completedAbruptly true) without processing + * the task. + * + * 4. Assuming beforeExecute completes normally, we run the task, + * gathering any of its thrown exceptions to send to afterExecute. + * We separately handle RuntimeException, Error (both of which the + * specs guarantee that we trap) and arbitrary Throwables. + * Because we cannot rethrow Throwables within Runnable.run, we + * wrap them within Errors on the way out (to the thread's + * UncaughtExceptionHandler). Any thrown exception also + * conservatively causes thread to die. + * + * 5. After task.run completes, we call afterExecute, which may + * also throw an exception, which will also cause thread to + * die. According to JLS Sec 14.20, this exception is the one that + * will be in effect even if task.run throws. + * + * The net effect of the exception mechanics is that afterExecute + * and the thread's UncaughtExceptionHandler have as accurate + * information as we can provide about any problems encountered by + * user code. + * + * @param w the worker + */ + @SuppressWarnings("null") // task cannot be null + final void runWorker(Worker w) { + Thread wt = Thread.currentThread(); + Runnable task = w.firstTask; + w.firstTask = null; + w.unlock(); // allow interrupts + boolean completedAbruptly = true; + try { + while (task != null || (task = getTask()) != null) { + w.lock(); + // If pool is stopping, ensure thread is interrupted; + // if not, ensure thread is not interrupted. This + // requires a recheck in second case to deal with + // shutdownNow race while clearing interrupt + if ((runStateAtLeast(ctl.get(), STOP) || + (Thread.interrupted() && + runStateAtLeast(ctl.get(), STOP))) && + !wt.isInterrupted()) { + wt.interrupt(); + } + try { + beforeExecute(wt, task); + try { + task.run(); + afterExecute(task, null); + } catch (Throwable ex) { + afterExecute(task, ex); + throw ex; + } + } finally { + task = null; + w.completedTasks++; + w.unlock(); + } + } + completedAbruptly = false; + } finally { + processWorkerExit(w, completedAbruptly); + } + } + + // Public constructors and methods + + /** + * Creates a new {@code ThreadPoolExecutor} with the given initial + * parameters, the + * {@linkplain Executors#defaultThreadFactory default thread factory} + * and the {@linkplain ThreadPoolExecutor.RejectPolicy + * default rejected execution handler}. + * + *

    It may be more convenient to use one of the {@link Executors} + * factory methods instead of this general purpose constructor. + * + * @param corePoolSize the number of threads to keep in the pool, even + * if they are idle, unless {@code allowCoreThreadTimeOut} is set + * @param maximumPoolSize the maximum number of threads to allow in the + * pool + * @param keepAliveTime when the number of threads is greater than + * the core, this is the maximum time that excess idle threads + * will wait for new tasks before terminating. + * @param unit the time unit for the {@code keepAliveTime} argument + * @param workQueue the queue to use for holding tasks before they are + * executed. This queue will hold only the {@code Runnable} + * tasks submitted by the {@code execute} method. + * @throws IllegalArgumentException if one of the following holds:
    + * {@code corePoolSize < 0}
    + * {@code keepAliveTime < 0}
    + * {@code maximumPoolSize <= 0}
    + * {@code maximumPoolSize < corePoolSize} + * @throws NullPointerException if {@code workQueue} is null + */ + public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue) { + this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, + Executors.defaultThreadFactory(), defaultHandler); + } + + /** + * Creates a new {@code ThreadPoolExecutor} with the given initial + * parameters and the {@linkplain ThreadPoolExecutor.RejectPolicy + * default rejected execution handler}. + * + * @param corePoolSize the number of threads to keep in the pool, even + * if they are idle, unless {@code allowCoreThreadTimeOut} is set + * @param maximumPoolSize the maximum number of threads to allow in the + * pool + * @param keepAliveTime when the number of threads is greater than + * the core, this is the maximum time that excess idle threads + * will wait for new tasks before terminating. + * @param unit the time unit for the {@code keepAliveTime} argument + * @param workQueue the queue to use for holding tasks before they are + * executed. This queue will hold only the {@code Runnable} + * tasks submitted by the {@code execute} method. + * @param threadFactory the factory to use when the executor + * creates a new thread + * @throws IllegalArgumentException if one of the following holds:
    + * {@code corePoolSize < 0}
    + * {@code keepAliveTime < 0}
    + * {@code maximumPoolSize <= 0}
    + * {@code maximumPoolSize < corePoolSize} + * @throws NullPointerException if {@code workQueue} + * or {@code threadFactory} is null + */ + public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory) { + this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, + threadFactory, defaultHandler); + } + + /** + * Creates a new {@code ThreadPoolExecutor} with the given initial + * parameters and the + * {@linkplain Executors#defaultThreadFactory default thread factory}. + * + * @param corePoolSize the number of threads to keep in the pool, even + * if they are idle, unless {@code allowCoreThreadTimeOut} is set + * @param maximumPoolSize the maximum number of threads to allow in the + * pool + * @param keepAliveTime when the number of threads is greater than + * the core, this is the maximum time that excess idle threads + * will wait for new tasks before terminating. + * @param unit the time unit for the {@code keepAliveTime} argument + * @param workQueue the queue to use for holding tasks before they are + * executed. This queue will hold only the {@code Runnable} + * tasks submitted by the {@code execute} method. + * @param handler the handler to use when execution is blocked + * because the thread bounds and queue capacities are reached + * @throws IllegalArgumentException if one of the following holds:
    + * {@code corePoolSize < 0}
    + * {@code keepAliveTime < 0}
    + * {@code maximumPoolSize <= 0}
    + * {@code maximumPoolSize < corePoolSize} + * @throws NullPointerException if {@code workQueue} + * or {@code handler} is null + */ + public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + RejectedExecutionHandler handler) { + this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, + Executors.defaultThreadFactory(), handler); + } + + /** + * Creates a new {@code ThreadPoolExecutor} with the given initial + * parameters. + * + * @param corePoolSize the number of threads to keep in the pool, even + * if they are idle, unless {@code allowCoreThreadTimeOut} is set + * @param maximumPoolSize the maximum number of threads to allow in the + * pool + * @param keepAliveTime when the number of threads is greater than + * the core, this is the maximum time that excess idle threads + * will wait for new tasks before terminating. + * @param unit the time unit for the {@code keepAliveTime} argument + * @param workQueue the queue to use for holding tasks before they are + * executed. This queue will hold only the {@code Runnable} + * tasks submitted by the {@code execute} method. + * @param threadFactory the factory to use when the executor + * creates a new thread + * @param handler the handler to use when execution is blocked + * because the thread bounds and queue capacities are reached + * @throws IllegalArgumentException if one of the following holds:
    + * {@code corePoolSize < 0}
    + * {@code keepAliveTime < 0}
    + * {@code maximumPoolSize <= 0}
    + * {@code maximumPoolSize < corePoolSize} + * @throws NullPointerException if {@code workQueue} + * or {@code threadFactory} or {@code handler} is null + */ + public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + if (corePoolSize < 0 || + maximumPoolSize <= 0 || + maximumPoolSize < corePoolSize || + keepAliveTime < 0) { + throw new IllegalArgumentException(); + } + if (workQueue == null || threadFactory == null || handler == null) { + throw new NullPointerException(); + } + this.corePoolSize = corePoolSize; + this.maximumPoolSize = maximumPoolSize; + this.workQueue = workQueue; + this.keepAliveTime = unit.toNanos(keepAliveTime); + this.threadFactory = threadFactory; + this.handler = handler; + + prestartAllCoreThreads(); + } + + + @Override + public void execute(Runnable command) { + submittedCount.incrementAndGet(); + try { + executeInternal(command); + } catch (RejectedExecutionException rx) { + if (getQueue() instanceof TaskQueue) { + // If the Executor is close to maximum pool size, concurrent + // calls to execute() may result (due to Tomcat's use of + // TaskQueue) in some tasks being rejected rather than queued. + // If this happens, add them to the queue. + final TaskQueue queue = (TaskQueue) getQueue(); + if (!queue.force(command)) { + submittedCount.decrementAndGet(); + throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull")); + } + } else { + submittedCount.decrementAndGet(); + throw rx; + } + } + } + + + /** + * Executes the given task sometime in the future. The task + * may execute in a new thread or in an existing pooled thread. + * + * If the task cannot be submitted for execution, either because this + * executor has been shutdown or because its capacity has been reached, + * the task is handled by the current {@link RejectedExecutionHandler}. + * + * @param command the task to execute + * @throws RejectedExecutionException at discretion of + * {@code RejectedExecutionHandler}, if the task + * cannot be accepted for execution + * @throws NullPointerException if {@code command} is null + */ + private void executeInternal(Runnable command) { + if (command == null) { + throw new NullPointerException(); + } + /* + * Proceed in 3 steps: + * + * 1. If fewer than corePoolSize threads are running, try to + * start a new thread with the given command as its first + * task. The call to addWorker atomically checks runState and + * workerCount, and so prevents false alarms that would add + * threads when it shouldn't, by returning false. + * + * 2. If a task can be successfully queued, then we still need + * to double-check whether we should have added a thread + * (because existing ones died since last checking) or that + * the pool shut down since entry into this method. So we + * recheck state and if necessary roll back the enqueuing if + * stopped, or start a new thread if there are none. + * + * 3. If we cannot queue task, then we try to add a new + * thread. If it fails, we know we are shut down or saturated + * and so reject the task. + */ + int c = ctl.get(); + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) { + return; + } + c = ctl.get(); + } + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + if (! isRunning(recheck) && remove(command)) { + reject(command); + } else if (workerCountOf(recheck) == 0) { + addWorker(null, false); + } + } + else if (!addWorker(command, false)) { + reject(command); + } + } + + /** + * Initiates an orderly shutdown in which previously submitted + * tasks are executed, but no new tasks will be accepted. + * Invocation has no additional effect if already shut down. + * + *

    This method does not wait for previously submitted tasks to + * complete execution. Use {@link #awaitTermination awaitTermination} + * to do that. + * + * @throws SecurityException {@inheritDoc} + */ + @Override + public void shutdown() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + checkShutdownAccess(); + advanceRunState(SHUTDOWN); + interruptIdleWorkers(); + onShutdown(); // hook for ScheduledThreadPoolExecutor + } finally { + mainLock.unlock(); + } + tryTerminate(); + } + + /** + * Attempts to stop all actively executing tasks, halts the + * processing of waiting tasks, and returns a list of the tasks + * that were awaiting execution. These tasks are drained (removed) + * from the task queue upon return from this method. + * + *

    This method does not wait for actively executing tasks to + * terminate. Use {@link #awaitTermination awaitTermination} to + * do that. + * + *

    There are no guarantees beyond best-effort attempts to stop + * processing actively executing tasks. This implementation + * interrupts tasks via {@link Thread#interrupt}; any task that + * fails to respond to interrupts may never terminate. + * + * @throws SecurityException {@inheritDoc} + */ + @Override + public List shutdownNow() { + List tasks; + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + checkShutdownAccess(); + advanceRunState(STOP); + interruptWorkers(); + tasks = drainQueue(); + } finally { + mainLock.unlock(); + } + tryTerminate(); + return tasks; + } + + @Override + public boolean isShutdown() { + return runStateAtLeast(ctl.get(), SHUTDOWN); + } + + /** Used by ScheduledThreadPoolExecutor. */ + boolean isStopped() { + return runStateAtLeast(ctl.get(), STOP); + } + + /** + * Returns true if this executor is in the process of terminating + * after {@link #shutdown} or {@link #shutdownNow} but has not + * completely terminated. This method may be useful for + * debugging. A return of {@code true} reported a sufficient + * period after shutdown may indicate that submitted tasks have + * ignored or suppressed interruption, causing this executor not + * to properly terminate. + * + * @return {@code true} if terminating but not yet terminated + */ + public boolean isTerminating() { + int c = ctl.get(); + return runStateAtLeast(c, SHUTDOWN) && runStateLessThan(c, TERMINATED); + } + + @Override + public boolean isTerminated() { + return runStateAtLeast(ctl.get(), TERMINATED); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) + throws InterruptedException { + long nanos = unit.toNanos(timeout); + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + while (runStateLessThan(ctl.get(), TERMINATED)) { + if (nanos <= 0L) { + return false; + } + nanos = termination.awaitNanos(nanos); + } + return true; + } finally { + mainLock.unlock(); + } + } + + /** + * Sets the thread factory used to create new threads. + * + * @param threadFactory the new thread factory + * @throws NullPointerException if threadFactory is null + * @see #getThreadFactory + */ + public void setThreadFactory(ThreadFactory threadFactory) { + if (threadFactory == null) { + throw new NullPointerException(); + } + this.threadFactory = threadFactory; + } + + /** + * Returns the thread factory used to create new threads. + * + * @return the current thread factory + * @see #setThreadFactory(ThreadFactory) + */ + public ThreadFactory getThreadFactory() { + return threadFactory; + } + + /** + * Sets a new handler for unexecutable tasks. + * + * @param handler the new handler + * @throws NullPointerException if handler is null + * @see #getRejectedExecutionHandler + */ + public void setRejectedExecutionHandler(RejectedExecutionHandler handler) { + if (handler == null) { + throw new NullPointerException(); + } + this.handler = handler; + } + + /** + * Returns the current handler for unexecutable tasks. + * + * @return the current handler + * @see #setRejectedExecutionHandler(RejectedExecutionHandler) + */ + public RejectedExecutionHandler getRejectedExecutionHandler() { + return handler; + } + + /** + * Sets the core number of threads. This overrides any value set + * in the constructor. If the new value is smaller than the + * current value, excess existing threads will be terminated when + * they next become idle. If larger, new threads will, if needed, + * be started to execute any queued tasks. + * + * @param corePoolSize the new core size + * @throws IllegalArgumentException if {@code corePoolSize < 0} + * or {@code corePoolSize} is greater than the {@linkplain + * #getMaximumPoolSize() maximum pool size} + * @see #getCorePoolSize + */ + public void setCorePoolSize(int corePoolSize) { + if (corePoolSize < 0 || maximumPoolSize < corePoolSize) { + throw new IllegalArgumentException(); + } + int delta = corePoolSize - this.corePoolSize; + this.corePoolSize = corePoolSize; + if (workerCountOf(ctl.get()) > corePoolSize) { + interruptIdleWorkers(); + } else if (delta > 0) { + // We don't really know how many new threads are "needed". + // As a heuristic, prestart enough new workers (up to new + // core size) to handle the current number of tasks in + // queue, but stop if queue becomes empty while doing so. + int k = Math.min(delta, workQueue.size()); + while (k-- > 0 && addWorker(null, true)) { + if (workQueue.isEmpty()) { + break; + } + } + } + } + + /** + * Returns the core number of threads. + * + * @return the core number of threads + * @see #setCorePoolSize + */ + public int getCorePoolSize() { + return corePoolSize; + } + + /** + * Starts a core thread, causing it to idly wait for work. This + * overrides the default policy of starting core threads only when + * new tasks are executed. This method will return {@code false} + * if all core threads have already been started. + * + * @return {@code true} if a thread was started + */ + public boolean prestartCoreThread() { + return workerCountOf(ctl.get()) < corePoolSize && + addWorker(null, true); + } + + /** + * Same as prestartCoreThread except arranges that at least one + * thread is started even if corePoolSize is 0. + */ + void ensurePrestart() { + int wc = workerCountOf(ctl.get()); + if (wc < corePoolSize) { + addWorker(null, true); + } else if (wc == 0) { + addWorker(null, false); + } + } + + /** + * Starts all core threads, causing them to idly wait for work. This + * overrides the default policy of starting core threads only when + * new tasks are executed. + * + * @return the number of threads started + */ + public int prestartAllCoreThreads() { + int n = 0; + while (addWorker(null, true)) { + ++n; + } + return n; + } + + /** + * Returns true if this pool allows core threads to time out and + * terminate if no tasks arrive within the keepAlive time, being + * replaced if needed when new tasks arrive. When true, the same + * keep-alive policy applying to non-core threads applies also to + * core threads. When false (the default), core threads are never + * terminated due to lack of incoming tasks. + * + * @return {@code true} if core threads are allowed to time out, + * else {@code false} + * + * @since 1.6 + */ + public boolean allowsCoreThreadTimeOut() { + return allowCoreThreadTimeOut; + } + + /** + * Sets the policy governing whether core threads may time out and + * terminate if no tasks arrive within the keep-alive time, being + * replaced if needed when new tasks arrive. When false, core + * threads are never terminated due to lack of incoming + * tasks. When true, the same keep-alive policy applying to + * non-core threads applies also to core threads. To avoid + * continual thread replacement, the keep-alive time must be + * greater than zero when setting {@code true}. This method + * should in general be called before the pool is actively used. + * + * @param value {@code true} if should time out, else {@code false} + * @throws IllegalArgumentException if value is {@code true} + * and the current keep-alive time is not greater than zero + * + * @since 1.6 + */ + public void allowCoreThreadTimeOut(boolean value) { + if (value && keepAliveTime <= 0) { + throw new IllegalArgumentException(sm.getString("threadPoolExecutor.invalidKeepAlive")); + } + if (value != allowCoreThreadTimeOut) { + allowCoreThreadTimeOut = value; + if (value) { + interruptIdleWorkers(); + } + } + } + + /** + * Sets the maximum allowed number of threads. This overrides any + * value set in the constructor. If the new value is smaller than + * the current value, excess existing threads will be + * terminated when they next become idle. + * + * @param maximumPoolSize the new maximum + * @throws IllegalArgumentException if the new maximum is + * less than or equal to zero, or + * less than the {@linkplain #getCorePoolSize core pool size} + * @see #getMaximumPoolSize + */ + public void setMaximumPoolSize(int maximumPoolSize) { + if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize) { + throw new IllegalArgumentException(); + } + this.maximumPoolSize = maximumPoolSize; + if (workerCountOf(ctl.get()) > maximumPoolSize) { + interruptIdleWorkers(); + } + } + + /** + * Returns the maximum allowed number of threads. + * + * @return the maximum allowed number of threads + * @see #setMaximumPoolSize + */ + public int getMaximumPoolSize() { + return maximumPoolSize; + } + + /** + * Sets the thread keep-alive time, which is the amount of time + * that threads may remain idle before being terminated. + * Threads that wait this amount of time without processing a + * task will be terminated if there are more than the core + * number of threads currently in the pool, or if this pool + * {@linkplain #allowsCoreThreadTimeOut() allows core thread timeout}. + * This overrides any value set in the constructor. + * + * @param time the time to wait. A time value of zero will cause + * excess threads to terminate immediately after executing tasks. + * @param unit the time unit of the {@code time} argument + * @throws IllegalArgumentException if {@code time} less than zero or + * if {@code time} is zero and {@code allowsCoreThreadTimeOut} + * @see #getKeepAliveTime(TimeUnit) + */ + public void setKeepAliveTime(long time, TimeUnit unit) { + if (time < 0) { + throw new IllegalArgumentException(sm.getString("threadPoolExecutor.invalidKeepAlive")); + } + if (time == 0 && allowsCoreThreadTimeOut()) { + throw new IllegalArgumentException(sm.getString("threadPoolExecutor.invalidKeepAlive")); + } + long keepAliveTime = unit.toNanos(time); + long delta = keepAliveTime - this.keepAliveTime; + this.keepAliveTime = keepAliveTime; + if (delta < 0) { + interruptIdleWorkers(); + } + } + + /** + * Returns the thread keep-alive time, which is the amount of time + * that threads may remain idle before being terminated. + * Threads that wait this amount of time without processing a + * task will be terminated if there are more than the core + * number of threads currently in the pool, or if this pool + * {@linkplain #allowsCoreThreadTimeOut() allows core thread timeout}. + * + * @param unit the desired time unit of the result + * @return the time limit + * @see #setKeepAliveTime(long, TimeUnit) + */ + public long getKeepAliveTime(TimeUnit unit) { + return unit.convert(keepAliveTime, TimeUnit.NANOSECONDS); + } + + + public long getThreadRenewalDelay() { + return threadRenewalDelay; + } + + + public void setThreadRenewalDelay(long threadRenewalDelay) { + this.threadRenewalDelay = threadRenewalDelay; + } + + + /* User-level queue utilities */ + + /** + * Returns the task queue used by this executor. Access to the + * task queue is intended primarily for debugging and monitoring. + * This queue may be in active use. Retrieving the task queue + * does not prevent queued tasks from executing. + * + * @return the task queue + */ + public BlockingQueue getQueue() { + return workQueue; + } + + /** + * Removes this task from the executor's internal queue if it is + * present, thus causing it not to be run if it has not already + * started. + * + *

    This method may be useful as one part of a cancellation + * scheme. It may fail to remove tasks that have been converted + * into other forms before being placed on the internal queue. + * For example, a task entered using {@code submit} might be + * converted into a form that maintains {@code Future} status. + * However, in such cases, method {@link #purge} may be used to + * remove those Futures that have been cancelled. + * + * @param task the task to remove + * @return {@code true} if the task was removed + */ + public boolean remove(Runnable task) { + boolean removed = workQueue.remove(task); + tryTerminate(); // In case SHUTDOWN and now empty + return removed; + } + + /** + * Tries to remove from the work queue all {@link Future} + * tasks that have been cancelled. This method can be useful as a + * storage reclamation operation, that has no other impact on + * functionality. Cancelled tasks are never executed, but may + * accumulate in work queues until worker threads can actively + * remove them. Invoking this method instead tries to remove them now. + * However, this method may fail to remove tasks in + * the presence of interference by other threads. + */ + public void purge() { + final BlockingQueue q = workQueue; + try { + Iterator it = q.iterator(); + while (it.hasNext()) { + Runnable r = it.next(); + if (r instanceof Future && ((Future)r).isCancelled()) { + it.remove(); + } + } + } catch (ConcurrentModificationException fallThrough) { + // Take slow path if we encounter interference during traversal. + // Make copy for traversal and call remove for cancelled entries. + // The slow path is more likely to be O(N*N). + for (Object r : q.toArray()) { + if (r instanceof Future && ((Future)r).isCancelled()) { + q.remove(r); + } + } + } + + tryTerminate(); // In case SHUTDOWN and now empty + } + + + public void contextStopping() { + this.lastContextStoppedTime.set(System.currentTimeMillis()); + + // save the current pool parameters to restore them later + int savedCorePoolSize = this.getCorePoolSize(); + + // setCorePoolSize(0) wakes idle threads + this.setCorePoolSize(0); + + // TaskQueue.take() takes care of timing out, so that we are sure that + // all threads of the pool are renewed in a limited time, something like + // (threadKeepAlive + longest request time) + + this.setCorePoolSize(savedCorePoolSize); + } + + + /* Statistics */ + + /** + * Returns the current number of threads in the pool. + * + * @return the number of threads + */ + public int getPoolSize() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + // Remove rare and surprising possibility of + // isTerminated() && getPoolSize() > 0 + return runStateAtLeast(ctl.get(), TIDYING) ? 0 + : workers.size(); + } finally { + mainLock.unlock(); + } + } + + /** + * Returns the current number of threads in the pool. + *
    NOTE: this method only used in {@link TaskQueue#offer(Runnable)}, + * where operations are frequent, can greatly reduce unnecessary + * performance overhead by a lock-free way. + * @return the number of threads + */ + protected int getPoolSizeNoLock() { + return runStateAtLeast(ctl.get(), TIDYING) ? 0 + : workers.size(); + } + + /** + * Returns the approximate number of threads that are actively + * executing tasks. + * + * @return the number of threads + */ + public int getActiveCount() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + int n = 0; + for (Worker w : workers) { + if (w.isLocked()) { + ++n; + } + } + return n; + } finally { + mainLock.unlock(); + } + } + + /** + * Returns the largest number of threads that have ever + * simultaneously been in the pool. + * + * @return the number of threads + */ + public int getLargestPoolSize() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + return largestPoolSize; + } finally { + mainLock.unlock(); + } + } + + /** + * Returns the approximate total number of tasks that have ever been + * scheduled for execution. Because the states of tasks and + * threads may change dynamically during computation, the returned + * value is only an approximation. + * + * @return the number of tasks + */ + public long getTaskCount() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + long n = completedTaskCount; + for (Worker w : workers) { + n += w.completedTasks; + if (w.isLocked()) { + ++n; + } + } + return n + workQueue.size(); + } finally { + mainLock.unlock(); + } + } + + /** + * Returns the approximate total number of tasks that have + * completed execution. Because the states of tasks and threads + * may change dynamically during computation, the returned value + * is only an approximation, but one that does not ever decrease + * across successive calls. + * + * @return the number of tasks + */ + public long getCompletedTaskCount() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + long n = completedTaskCount; + for (Worker w : workers) { + n += w.completedTasks; + } + return n; + } finally { + mainLock.unlock(); + } + } + + + public int getSubmittedCount() { + return submittedCount.get(); + } + + + /** + * Returns a string identifying this pool, as well as its state, + * including indications of run state and estimated worker and + * task counts. + * + * @return a string identifying this pool, as well as its state + */ + @Override + public String toString() { + long ncompleted; + int nworkers, nactive; + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + ncompleted = completedTaskCount; + nactive = 0; + nworkers = workers.size(); + for (Worker w : workers) { + ncompleted += w.completedTasks; + if (w.isLocked()) { + ++nactive; + } + } + } finally { + mainLock.unlock(); + } + int c = ctl.get(); + String runState = + isRunning(c) ? "Running" : + runStateAtLeast(c, TERMINATED) ? "Terminated" : + "Shutting down"; + return super.toString() + + "[" + runState + + ", pool size = " + nworkers + + ", active threads = " + nactive + + ", queued tasks = " + workQueue.size() + + ", completed tasks = " + ncompleted + + "]"; + } + + /* Extension hooks */ + + /** + * Method invoked prior to executing the given Runnable in the + * given thread. This method is invoked by thread {@code t} that + * will execute task {@code r}, and may be used to re-initialize + * ThreadLocals, or to perform logging. + * + *

    This implementation does nothing, but may be customized in + * subclasses. Note: To properly nest multiple overridings, subclasses + * should generally invoke {@code super.beforeExecute} at the end of + * this method. + * + * @param t the thread that will run task {@code r} + * @param r the task that will be executed + */ + protected void beforeExecute(Thread t, Runnable r) { } + + + /** + * Method invoked upon completion of execution of the given Runnable. + * This method is invoked by the thread that executed the task. If + * non-null, the Throwable is the uncaught {@code RuntimeException} + * or {@code Error} that caused execution to terminate abruptly. + * + *

    This implementation does nothing, but may be customized in + * subclasses. Note: To properly nest multiple overridings, subclasses + * should generally invoke {@code super.afterExecute} at the + * beginning of this method. + * + *

    Note: When actions are enclosed in tasks (such as + * {@link java.util.concurrent.FutureTask}) + * either explicitly or via methods such as + * {@code submit}, these task objects catch and maintain + * computational exceptions, and so they do not cause abrupt + * termination, and the internal exceptions are not + * passed to this method. If you would like to trap both kinds of + * failures in this method, you can further probe for such cases, + * as in this sample subclass that prints either the direct cause + * or the underlying exception if a task has been aborted: + * + *

     {@code
    +     * class ExtendedExecutor extends ThreadPoolExecutor {
    +     *   // ...
    +     *   protected void afterExecute(Runnable r, Throwable t) {
    +     *     super.afterExecute(r, t);
    +     *     if (t == null
    +     *         && r instanceof Future
    +     *         && ((Future)r).isDone()) {
    +     *       try {
    +     *         Object result = ((Future) r).get();
    +     *       } catch (CancellationException ce) {
    +     *         t = ce;
    +     *       } catch (ExecutionException ee) {
    +     *         t = ee.getCause();
    +     *       } catch (InterruptedException ie) {
    +     *         // ignore/reset
    +     *         Thread.currentThread().interrupt();
    +     *       }
    +     *     }
    +     *     if (t != null)
    +     *       System.out.println(t);
    +     *   }
    +     * }}
    + * + * @param r the runnable that has completed + * @param t the exception that caused termination, or null if + * execution completed normally + */ + protected void afterExecute(Runnable r, Throwable t) { + // Throwing StopPooledThreadException is likely to cause this method to + // be called more than once for a given task based on the typical + // implementations of the parent class. This test ensures that + // decrementAndGet() is only called once after each task execution. + if (!(t instanceof StopPooledThreadException)) { + submittedCount.decrementAndGet(); + } + + if (t == null) { + stopCurrentThreadIfNeeded(); + } + } + + + /** + * If the current thread was started before the last time when a context was + * stopped, an exception is thrown so that the current thread is stopped. + */ + protected void stopCurrentThreadIfNeeded() { + if (currentThreadShouldBeStopped()) { + long lastTime = lastTimeThreadKilledItself.longValue(); + if (lastTime + threadRenewalDelay < System.currentTimeMillis()) { + if (lastTimeThreadKilledItself.compareAndSet(lastTime, + System.currentTimeMillis() + 1)) { + // OK, it's really time to dispose of this thread + + final String msg = sm.getString( + "threadPoolExecutor.threadStoppedToAvoidPotentialLeak", + Thread.currentThread().getName()); + + throw new StopPooledThreadException(msg); + } + } + } + } + + + protected boolean currentThreadShouldBeStopped() { + Thread currentThread = Thread.currentThread(); + if (threadRenewalDelay >= 0 && currentThread instanceof TaskThread) { + TaskThread currentTaskThread = (TaskThread) currentThread; + if (currentTaskThread.getCreationTime() < + this.lastContextStoppedTime.longValue()) { + return true; + } + } + return false; + } + + + /** + * Method invoked when the Executor has terminated. Default + * implementation does nothing. Note: To properly nest multiple + * overridings, subclasses should generally invoke + * {@code super.terminated} within this method. + */ + protected void terminated() { } + + /* Predefined RejectedExecutionHandlers */ + + /** + * A handler for rejected tasks that runs the rejected task + * directly in the calling thread of the {@code execute} method, + * unless the executor has been shut down, in which case the task + * is discarded. + */ + public static class CallerRunsPolicy implements RejectedExecutionHandler { + /** + * Creates a {@code CallerRunsPolicy}. + */ + public CallerRunsPolicy() { } + + /** + * Executes task r in the caller's thread, unless the executor + * has been shut down, in which case the task is discarded. + * + * @param r the runnable task requested to be executed + * @param e the executor attempting to execute this task + */ + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + if (!e.isShutdown()) { + r.run(); + } + } + } + + /** + * A handler for rejected tasks that throws a + * {@link RejectedExecutionException}. + */ + public static class AbortPolicy implements RejectedExecutionHandler { + /** + * Creates an {@code AbortPolicy}. + */ + public AbortPolicy() { } + + /** + * Always throws RejectedExecutionException. + * + * @param r the runnable task requested to be executed + * @param e the executor attempting to execute this task + * @throws RejectedExecutionException always + */ + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + throw new RejectedExecutionException( + sm.getString("threadPoolExecutor.taskRejected", r.toString(), e.toString())); + } + } + + /** + * A handler for rejected tasks that silently discards the + * rejected task. + */ + public static class DiscardPolicy implements RejectedExecutionHandler { + /** + * Creates a {@code DiscardPolicy}. + */ + public DiscardPolicy() { } + + /** + * Does nothing, which has the effect of discarding task r. + * + * @param r the runnable task requested to be executed + * @param e the executor attempting to execute this task + */ + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + } + } + + /** + * A handler for rejected tasks that discards the oldest unhandled + * request and then retries {@code execute}, unless the executor + * is shut down, in which case the task is discarded. This policy is + * rarely useful in cases where other threads may be waiting for + * tasks to terminate, or failures must be recorded. Instead consider + * using a handler of the form: + *
     {@code
    +     * new RejectedExecutionHandler() {
    +     *   public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    +     *     Runnable dropped = e.getQueue().poll();
    +     *     if (dropped instanceof Future) {
    +     *       ((Future)dropped).cancel(false);
    +     *       // also consider logging the failure
    +     *     }
    +     *     e.execute(r);  // retry
    +     * }}}
    + */ + public static class DiscardOldestPolicy implements RejectedExecutionHandler { + /** + * Creates a {@code DiscardOldestPolicy} for the given executor. + */ + public DiscardOldestPolicy() { } + + /** + * Obtains and ignores the next task that the executor + * would otherwise execute, if one is immediately available, + * and then retries execution of task r, unless the executor + * is shut down, in which case task r is instead discarded. + * + * @param r the runnable task requested to be executed + * @param e the executor attempting to execute this task + */ + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + if (!e.isShutdown()) { + e.getQueue().poll(); + e.execute(r); + } + } + } + + private static class RejectPolicy implements RejectedExecutionHandler { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + throw new RejectedExecutionException(); + } + } + + + public interface RejectedExecutionHandler { + + /** + * Method that may be invoked by a {@link ThreadPoolExecutor} when + * {@link ThreadPoolExecutor#execute execute} cannot accept a + * task. This may occur when no more threads or queue slots are + * available because their bounds would be exceeded, or upon + * shutdown of the Executor. + * + *

    In the absence of other alternatives, the method may throw + * an unchecked {@link RejectedExecutionException}, which will be + * propagated to the caller of {@code execute}. + * + * @param r the runnable task requested to be executed + * @param executor the executor attempting to execute this task + * @throws RejectedExecutionException if there is no remedy + */ + void rejectedExecution(Runnable r, ThreadPoolExecutor executor); + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java b/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java new file mode 100644 index 0000000..01dc547 --- /dev/null +++ b/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java @@ -0,0 +1,98 @@ +/* + * 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.tomcat.util.threads; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +import org.apache.tomcat.util.compat.JreCompat; +import org.apache.tomcat.util.res.StringManager; + +/** + * An executor that uses a new virtual thread for each task. + */ +public class VirtualThreadExecutor extends AbstractExecutorService { + + private static final StringManager sm = StringManager.getManager(VirtualThreadExecutor.class); + + private CountDownLatch shutdown = new CountDownLatch(1); + + private final JreCompat jreCompat = JreCompat.getInstance(); + + private Object threadBuilder; + + public VirtualThreadExecutor(String namePrefix) { + threadBuilder = jreCompat.createVirtualThreadBuilder(namePrefix); + } + + @Override + public void execute(Runnable command) { + if (isShutdown()) { + throw new RejectedExecutionException( + sm.getString("virtualThreadExecutor.taskRejected", command.toString(), this.toString())); + } + jreCompat.threadBuilderStart(threadBuilder, command); + } + + @Override + public void shutdown() { + shutdown.countDown(); + } + + /** + * {@inheritDoc} + *

    + * The VirtualThreadExecutor does not track in-progress tasks so calling this method is equivalent to calling + * {@link #shutdown()}. + */ + @Override + public List shutdownNow() { + shutdown(); + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return shutdown.getCount() == 0; + } + + /** + * {@inheritDoc} + *

    + * The VirtualThreadExecutor does not track in-progress tasks so calling this method is equivalent to calling + * {@link #isShutdown()}. + */ + @Override + public boolean isTerminated() { + return isShutdown(); + } + + /** + * {@inheritDoc} + *

    + * The VirtualThreadExecutor does not track in-progress tasks so calling this method is effectively waiting for + * {@link #shutdown()} to be called. + */ + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return shutdown.await(timeout, unit); + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/util/xreflection/ObjectReflectionPropertyInspector.java b/java/org/apache/tomcat/util/xreflection/ObjectReflectionPropertyInspector.java new file mode 100644 index 0000000..88f2da4 --- /dev/null +++ b/java/org/apache/tomcat/util/xreflection/ObjectReflectionPropertyInspector.java @@ -0,0 +1,277 @@ +/* + * 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.tomcat.util.xreflection; + + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.tomcat.util.IntrospectionUtils; + +public final class ObjectReflectionPropertyInspector { + + public static void main(String... args) throws Exception { + if (args.length == 0) { + System.err.println("Usage:\n\t"+ + "org.apache.tomcat.util.xreflection.ObjectReflectionPropertyInspector" + + " " + ); + System.exit(1); + } + + File outputDir = new File(args[0]); + if (!outputDir.exists() || !outputDir.isDirectory()) { + System.err.println("Invalid output directory: "+ outputDir.getAbsolutePath()); + System.exit(1); + } + + + Set baseClasses = getKnownClasses() + .stream() + .map(ObjectReflectionPropertyInspector::processClass) + .collect(Collectors.toCollection(LinkedHashSet::new)); + generateCode( + baseClasses, + "org.apache.tomcat.util", + outputDir, + "XReflectionIntrospectionUtils" + ); + } + + private static Set> getKnownClasses() throws ClassNotFoundException { + return + Collections.unmodifiableSet(new LinkedHashSet<>( + Arrays.asList( + Class.forName("org.apache.catalina.authenticator.jaspic.SimpleAuthConfigProvider"), + Class.forName("org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations$Property"), + Class.forName("org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations$Provider"), + Class.forName("org.apache.catalina.connector.Connector"), + Class.forName("org.apache.catalina.core.ContainerBase"), + Class.forName("org.apache.catalina.core.StandardContext"), + Class.forName("org.apache.catalina.core.StandardEngine"), + Class.forName("org.apache.catalina.core.StandardHost"), + Class.forName("org.apache.catalina.core.StandardServer"), + Class.forName("org.apache.catalina.core.StandardService"), + Class.forName("org.apache.catalina.filters.AddDefaultCharsetFilter"), + Class.forName("org.apache.catalina.filters.RestCsrfPreventionFilter"), + Class.forName("org.apache.catalina.loader.ParallelWebappClassLoader"), + Class.forName("org.apache.catalina.loader.WebappClassLoaderBase"), + Class.forName("org.apache.catalina.realm.UserDatabaseRealm"), + Class.forName("org.apache.catalina.valves.AccessLogValve"), + Class.forName("org.apache.coyote.AbstractProtocol"), + Class.forName("org.apache.coyote.ajp.AbstractAjpProtocol"), + Class.forName("org.apache.coyote.ajp.AjpNio2Protocol"), + Class.forName("org.apache.coyote.ajp.AjpNioProtocol"), + Class.forName("org.apache.coyote.http11.AbstractHttp11JsseProtocol"), + Class.forName("org.apache.coyote.http11.AbstractHttp11Protocol"), + Class.forName("org.apache.coyote.http11.Http11Nio2Protocol"), + Class.forName("org.apache.coyote.http11.Http11NioProtocol"), + Class.forName("org.apache.tomcat.util.descriptor.web.ContextResource"), + Class.forName("org.apache.tomcat.util.descriptor.web.ResourceBase"), + Class.forName("org.apache.tomcat.util.modeler.AttributeInfo"), + Class.forName("org.apache.tomcat.util.modeler.FeatureInfo"), + Class.forName("org.apache.tomcat.util.modeler.ManagedBean"), + Class.forName("org.apache.tomcat.util.modeler.OperationInfo"), + Class.forName("org.apache.tomcat.util.modeler.ParameterInfo"), + Class.forName("org.apache.tomcat.util.net.AbstractEndpoint"), + Class.forName("org.apache.tomcat.util.net.Nio2Endpoint"), + Class.forName("org.apache.tomcat.util.net.NioEndpoint"), + Class.forName("org.apache.tomcat.util.net.SocketProperties") + ) + ) + ); + } + + //types of properties that IntrospectionUtils.setProperty supports + private static final Set> ALLOWED_TYPES = Collections.unmodifiableSet(new LinkedHashSet<>( + Arrays.asList( + Boolean.TYPE, + Integer.TYPE, + Long.TYPE, + String.class, + InetAddress.class + ) + )); + private static Map, SetPropertyClass> classes = new LinkedHashMap<>(); + + public static void generateCode(Set baseClasses, String packageName, File location, String className) throws Exception { + String packageDirectory = packageName.replace('.','/'); + File sourceFileLocation = new File(location, packageDirectory); + ReflectionLessCodeGenerator.generateCode(sourceFileLocation, className, packageName, baseClasses); + } + + + private static boolean isAllowedField(Field field) { + return ALLOWED_TYPES.contains(field.getType()) && !Modifier.isFinal(field.getModifiers()); + } + + private static boolean isAllowedSetMethod(Method method) { + return method.getName().startsWith("set") && + method.getParameterTypes() != null && + method.getParameterTypes().length == 1 && + ALLOWED_TYPES.contains(method.getParameterTypes()[0]) && + !Modifier.isPrivate(method.getModifiers()); + } + + private static boolean isAllowedGetMethod(Method method) { + return (method.getName().startsWith("get") || method.getName().startsWith("is")) && + (method.getParameterTypes() == null || + method.getParameterTypes().length == 0) && + ALLOWED_TYPES.contains(method.getReturnType()) && + !Modifier.isPrivate(method.getModifiers()); + } + + + private static SetPropertyClass getOrCreateSetPropertyClass(Class clazz) { + boolean base = (clazz.getSuperclass() == null || clazz.getSuperclass() == Object.class); + SetPropertyClass spc = classes.get(clazz); + if (spc == null) { + spc = new SetPropertyClass(clazz, base ? null : getOrCreateSetPropertyClass(clazz.getSuperclass())); + classes.put(clazz, spc); + } + return spc; + } + + static Method findGetter(Class declaringClass, String propertyName) { + for (String getterName : Arrays.asList("get" + IntrospectionUtils.capitalize(propertyName), "is" + propertyName)) { + try { + Method method = declaringClass.getMethod(getterName); + if (!Modifier.isPrivate(method.getModifiers())) { + return method; + } + } catch (NoSuchMethodException e) { + } + } + try { + Method method = declaringClass.getMethod("getProperty", String.class, String.class); + if (!Modifier.isPrivate(method.getModifiers())) { + return method; + } + } catch (NoSuchMethodException e) { + } + + return null; + } + + static Method findSetter(Class declaringClass, String propertyName, Class propertyType) { + try { + Method method = declaringClass.getMethod("set" + IntrospectionUtils.capitalize(propertyName), propertyType); + if (!Modifier.isPrivate(method.getModifiers())) { + return method; + } + } catch (NoSuchMethodException e) { + } + try { + Method method = declaringClass.getMethod("setProperty", String.class, String.class); + if (!Modifier.isPrivate(method.getModifiers())) { + return method; + } + } catch (NoSuchMethodException e) { + } + return null; + } + + static String decapitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && + Character.isUpperCase(name.charAt(0))) { + return name; + } + char chars[] = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + + + static SetPropertyClass processClass(Class clazz) { + SetPropertyClass spc = getOrCreateSetPropertyClass(clazz); + final Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (isAllowedSetMethod(method)) { + String propertyName = decapitalize(method.getName().substring(3)); + Class propertyType = method.getParameterTypes()[0]; + Method getter = findGetter(clazz, propertyName); + Method setter = findSetter(clazz, propertyName, propertyType); + ReflectionProperty property = new ReflectionProperty( + spc.getClazz().getName(), + propertyName, + propertyType, + setter, + getter + ); + spc.addProperty(property); + } else if (isAllowedGetMethod(method)) { + boolean startsWithIs = method.getName().startsWith("is"); + String propertyName = decapitalize(method.getName().substring(startsWithIs ? 2 : 3)); + Class propertyType = method.getReturnType(); + Method getter = findGetter(clazz, propertyName); + Method setter = findSetter(clazz, propertyName, propertyType); + ReflectionProperty property = new ReflectionProperty( + spc.getClazz().getName(), + propertyName, + propertyType, + setter, + getter + ); + spc.addProperty(property); + } + } + + final Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (isAllowedField(field)) { + Method getter = findGetter( + field.getDeclaringClass(), + IntrospectionUtils.capitalize(field.getName()) + ); + Method setter = findSetter( + field.getDeclaringClass(), + IntrospectionUtils.capitalize(field.getName()), + field.getType() + ); + ReflectionProperty property = new ReflectionProperty( + spc.getClazz().getName(), + field.getName(), + field.getType(), + setter, + getter + ); + spc.addProperty(property); + } + } + + if (!spc.isBaseClass()) { + SetPropertyClass parent = getOrCreateSetPropertyClass(spc.getClazz().getSuperclass()); + parent.addSubClass(spc); + return processClass(parent.getClazz()); + } else { + return spc; + } + } +} diff --git a/java/org/apache/tomcat/util/xreflection/ReflectionLessCodeGenerator.java b/java/org/apache/tomcat/util/xreflection/ReflectionLessCodeGenerator.java new file mode 100644 index 0000000..ac83673 --- /dev/null +++ b/java/org/apache/tomcat/util/xreflection/ReflectionLessCodeGenerator.java @@ -0,0 +1,273 @@ +/* + * 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.tomcat.util.xreflection; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Set; + +final class ReflectionLessCodeGenerator { + private static final String INDENT = " "; + + static StringBuilder getIndent(int multiplier) { + StringBuilder indent = new StringBuilder(); + while ((multiplier--) > 0) { + indent.append(INDENT); + } + return indent; + } + + static void generateCode( + File directory, + String className, + String packageName, + Set baseClasses + ) throws IOException { + //begin - class + StringBuilder code = new StringBuilder(AL20_HEADER) + .append("package ") + .append(packageName) + .append(';') + .append(System.lineSeparator()) + .append(System.lineSeparator()) + .append("final class ") + .append(className) + .append(" {") + .append(System.lineSeparator()) + .append(System.lineSeparator()); + + //begin - isEnabled method + code.append(getIndent(1)) + .append("static boolean isEnabled() {") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("return true;") + .append(System.lineSeparator()) + .append(getIndent(1)) + .append('}') + .append(System.lineSeparator()) + .append(System.lineSeparator()) + ; + //end - isEnabled method + + //begin - getInetAddress method + code.append(getIndent(1)) + .append("private static java.net.InetAddress getInetAddress(String value) {") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("try {") + .append(System.lineSeparator()) + .append(getIndent(3)) + .append("return java.net.InetAddress.getByName(value);") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("} catch (java.net.UnknownHostException x) { throw new RuntimeException(x); }") + .append(System.lineSeparator()) + .append(getIndent(1)) + .append('}') + .append(System.lineSeparator()) + .append(System.lineSeparator()) + ; + //end - getInetAddress method + + //begin - getPropertyInternal method + code.append(getIndent(1)) + .append("static Object getPropertyInternal(Object ") + .append(SetPropertyClass.OBJECT_VAR_NAME) + .append(", String ") + .append(SetPropertyClass.NAME_VAR_NAME) + .append(") {") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("Class checkThisClass = o.getClass();") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("Object result = null;") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("while (checkThisClass != Object.class && result == null) {") + .append(System.lineSeparator()) + .append(getIndent(3)) + .append("switch (checkThisClass.getName()) {") + .append(System.lineSeparator()); + + //generate case statements for getPropertyInternal + generateCaseStatementsForGetPropertyInternal(baseClasses, code); + + + code + .append(getIndent(3)) + .append('}') + .append(System.lineSeparator()) + .append(getIndent(3)) + .append("checkThisClass = checkThisClass.getSuperclass();") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append('}') + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("return result;") + .append(System.lineSeparator()) + .append(getIndent(1)) + .append('}') + .append(System.lineSeparator()); + //end - getPropertyInternal method + + //begin - getPropertyForXXX methods + generateGetPropertyForMethods(baseClasses, code); + //end - getPropertyForXXX methods + + //begin - setPropertyInternal method + code.append(getIndent(1)) + .append("static boolean setPropertyInternal(Object ") + .append(SetPropertyClass.OBJECT_VAR_NAME) + .append(", String ") + .append(SetPropertyClass.NAME_VAR_NAME) + .append(", String ") + .append(SetPropertyClass.VALUE_VAR_NAME) + .append(", boolean ") + .append(SetPropertyClass.SETP_VAR_NAME) + .append(") {") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("Class checkThisClass = o.getClass();") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("while (checkThisClass != Object.class) {") + .append(System.lineSeparator()) + .append(getIndent(3)) + .append("switch (checkThisClass.getName()) {") + .append(System.lineSeparator()); + + //generate case statements for setPropertyInternal + generateCaseStatementsForSetPropertyInternal(baseClasses, code); + + + code + .append(getIndent(3)) + .append('}') + .append(System.lineSeparator()) + .append(getIndent(3)) + .append("checkThisClass = checkThisClass.getSuperclass();") + .append(System.lineSeparator()) + .append(getIndent(2)) + .append('}') + .append(System.lineSeparator()) + .append(getIndent(2)) + .append("return false;") + .append(System.lineSeparator()) + .append(getIndent(1)) + .append('}') + .append(System.lineSeparator()); + //end - setPropertyInternal method + + //begin - setPropertyForXXX methods + generateSetPropertyForMethods(baseClasses, code); + //end - setPropertyForXXX methods + + code.append('}') + .append(System.lineSeparator()); + //end - class + File destination = new File(directory, className+".java"); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(destination, false))) { + writer.write(code.toString()); + writer.flush(); + } + + } + + private static void generateCaseStatementForSetPropertyInternal(SetPropertyClass clazz, StringBuilder code) { + for (SetPropertyClass child : clazz.getChildren()) { + generateCaseStatementForSetPropertyInternal(child, code); + } + if (!clazz.isAbstract()) { + code.append(clazz.generateInvocationSetForPropertyCaseStatement(4)); + } + } + + private static void generateCaseStatementsForSetPropertyInternal(Set baseClasses, StringBuilder code) { + for (SetPropertyClass clazz : baseClasses) { + generateCaseStatementForSetPropertyInternal(clazz, code); + } + } + + private static void generateSetPropertyForMethod(SetPropertyClass clazz, StringBuilder code) { + for (SetPropertyClass child : clazz.getChildren()) { + generateSetPropertyForMethod(child, code); + } + code.append(clazz.generateSetPropertyForMethod()) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + } + + private static void generateSetPropertyForMethods(Set baseClasses, StringBuilder code) { + for (SetPropertyClass clazz : baseClasses) { + generateSetPropertyForMethod(clazz, code); + } + } + + + + private static void generateCaseStatementForGetPropertyInternal(SetPropertyClass clazz, StringBuilder code) { + for (SetPropertyClass child : clazz.getChildren()) { + generateCaseStatementForGetPropertyInternal(child, code); + } + if (!clazz.isAbstract()) { + code.append(clazz.generateInvocationGetForPropertyCaseStatement(4)); + } + } + + private static void generateCaseStatementsForGetPropertyInternal(Set baseClasses, StringBuilder code) { + for (SetPropertyClass clazz : baseClasses) { + generateCaseStatementForGetPropertyInternal(clazz, code); + } + } + + private static void generateGetPropertyForMethod(SetPropertyClass clazz, StringBuilder code) { + for (SetPropertyClass child : clazz.getChildren()) { + generateGetPropertyForMethod(child, code); + } + code.append(clazz.generateGetPropertyForMethod()) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + } + + private static void generateGetPropertyForMethods(Set baseClasses, StringBuilder code) { + for (SetPropertyClass clazz : baseClasses) { + generateGetPropertyForMethod(clazz, code); + } + } + + private static final String AL20_HEADER = "/*\n" + + " * Licensed to the Apache Software Foundation (ASF) under one or more\n" + + " * contributor license agreements. See the NOTICE file distributed with\n" + + " * this work for additional information regarding copyright ownership.\n" + + " * The ASF licenses this file to You under the Apache License, Version 2.0\n" + + " * (the \"License\"); you may not use this file except in compliance with\n" + + " * the License. You may obtain a copy of the License at\n" + + " *\n" + + " * http://www.apache.org/licenses/LICENSE-2.0\n" + + " *\n" + + " * Unless required by applicable law or agreed to in writing, software\n" + + " * distributed under the License is distributed on an \"AS IS\" BASIS,\n" + + " * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" + + " * See the License for the specific language governing permissions and\n" + + " * limitations under the License.\n" + + " */\n"; +} diff --git a/java/org/apache/tomcat/util/xreflection/ReflectionProperty.java b/java/org/apache/tomcat/util/xreflection/ReflectionProperty.java new file mode 100644 index 0000000..2036af9 --- /dev/null +++ b/java/org/apache/tomcat/util/xreflection/ReflectionProperty.java @@ -0,0 +1,133 @@ +/* + * 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.tomcat.util.xreflection; + +import java.lang.reflect.Method; +import java.net.InetAddress; + +final class ReflectionProperty implements Comparable { + private final String clazz; + private final String propertyName; + private final Class propertyType; + private final Method setMethod; + private final Method getMethod; + + ReflectionProperty(String clazz, String propertyName, Class propertyType, Method setMethod, Method getMethod) { + this.clazz = clazz; + this.propertyName = propertyName; + this.propertyType = propertyType; + this.setMethod = setMethod; + this.getMethod = getMethod; + } + + public String getPropertyName() { + return propertyName; + } + + public Class getPropertyType() { + return propertyType; + } + + public boolean hasSetPropertySetter() { + return hasSetter() && "setProperty".equals(setMethod.getName()); + } + + public boolean hasGetPropertyGetter() { + return hasGetter() && "getProperty".equals(getMethod.getName()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ReflectionProperty property1 = (ReflectionProperty) o; + + if (!clazz.equals(property1.clazz)) { + return false; + } + return propertyName.equals(property1.propertyName); + } + + @Override + public int hashCode() { + int result = clazz.hashCode(); + result = 31 * result + propertyName.hashCode(); + return result; + } + + public String getClazz() { + return clazz; + } + + public Method getGetMethod() { + return getMethod; + } + + public String getConversion(String valueVarName) { + if (getPropertyType() == String.class) { + return valueVarName; + } + if (getPropertyType() == Boolean.TYPE) { + return "Boolean.valueOf(" + valueVarName + ")"; + } + if (getPropertyType() == Long.TYPE) { + return "Long.valueOf(" + valueVarName + ")"; + } + if (getPropertyType() == Integer.TYPE) { + return "Integer.valueOf(" + valueVarName + ")"; + } + if (getPropertyType() == InetAddress.class) { + return "getInetAddress(" + valueVarName + ")"; + } + throw new IllegalStateException("Unexpected Type:" + getPropertyType()); + + } + + public boolean hasSetter() { + return setMethod != null; + } + + public boolean hasGetter() { + return getMethod != null; + } + + public Method getSetMethod() { + return setMethod; + } + + @Override + public String toString() { + return "ReflectionProperty{" + "name='" + propertyName + '\'' + + ", type=" + propertyType + + '}'; + } + + @Override + public int compareTo(ReflectionProperty o) { + // Class then property name + int result = clazz.compareTo(o.clazz); + if (result == 0) { + result = propertyName.compareTo(o.propertyName); + } + return result; + } +} diff --git a/java/org/apache/tomcat/util/xreflection/SetPropertyClass.java b/java/org/apache/tomcat/util/xreflection/SetPropertyClass.java new file mode 100644 index 0000000..9354aa2 --- /dev/null +++ b/java/org/apache/tomcat/util/xreflection/SetPropertyClass.java @@ -0,0 +1,443 @@ +/* + * 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.tomcat.util.xreflection; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.tomcat.util.IntrospectionUtils; + +final class SetPropertyClass implements Comparable { + + static final String OBJECT_VAR_NAME = "o"; + static final String NAME_VAR_NAME = "name"; + static final String VALUE_VAR_NAME = "value"; + static final String SETP_VAR_NAME = "invokeSetProperty"; + + private final SetPropertyClass parent; + private final Class clazz; + private Set children = new TreeSet<>(); + private Set properties = new TreeSet<>(); + private final boolean isAbstract; + private final Method genericSetPropertyMethod; + private final Method genericGetPropertyMethod; + + SetPropertyClass(Class clazz, SetPropertyClass parent) { + this.clazz = clazz; + this.parent = parent; + this.isAbstract = Modifier.isAbstract(clazz.getModifiers()); + Method classSetter, classGetter; + try { + classSetter = clazz.getDeclaredMethod("setProperty", String.class, String.class); + } catch (NoSuchMethodException e) { + try { + classSetter = clazz.getDeclaredMethod("setProperty", String.class, Object.class); + } catch (NoSuchMethodException x) { + classSetter = null; + } + } + try { + classGetter = clazz.getDeclaredMethod("getProperty", String.class); + } catch (NoSuchMethodException e) { + classGetter = null; + } + genericSetPropertyMethod = classSetter; + genericGetPropertyMethod = classGetter; + } + + boolean isAbstract() { + return isAbstract; + } + + void addSubClass(SetPropertyClass clazz) { + this.children.add(clazz); + } + + boolean isBaseClass() { + return parent == null; + } + + public Set getChildren() { + return children; + } + + public Set getProperties() { + return properties; + } + + public Method getGenericSetPropertyMethod() { + return genericSetPropertyMethod; + } + + public Method getGenericGetPropertyMethod() { + return genericGetPropertyMethod; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SetPropertyClass that = (SetPropertyClass) o; + + return clazz.equals(that.clazz); + } + + @Override + public int hashCode() { + return clazz.hashCode(); + } + + public SetPropertyClass getParent() { + return parent; + } + + public Class getClazz() { + return clazz; + } + + @Override + public String toString() { + return "SetPropertyClass{" + "clazz=" + clazz.getName() + + '}'; + } + + public void addProperty(ReflectionProperty property) { + properties.add(property); + } + + + + public String generateSetPropertyMethod(ReflectionProperty property) { + //this property has a setProperty method + if (property.hasSetPropertySetter()) { + return "((" + this.getClazz().getName().replace('$','.') + ")" + OBJECT_VAR_NAME + ")." + + property.getSetMethod().getName() + "(" + NAME_VAR_NAME + ", " + VALUE_VAR_NAME + ");"; + } + + //direct setter + if (property.hasSetter()) { + return "((" + this.getClazz().getName().replace('$','.') + ")" + OBJECT_VAR_NAME + ")." + + property.getSetMethod().getName() + "(" + property.getConversion(VALUE_VAR_NAME) + ");"; + } + return null; + } + + public String generateGetPropertyMethod(ReflectionProperty property) { + //this property has a getProperty method + if (property.hasGetPropertyGetter()) { + return "result = ((" + this.getClazz().getName().replace('$','.') + ")" + OBJECT_VAR_NAME + ")." + + property.getGetMethod().getName() + "(" + NAME_VAR_NAME + ");"; + } + + //direct getter + if (property.hasGetter()) { + return "result = ((" + this.getClazz().getName().replace('$','.') + ")" + OBJECT_VAR_NAME + ")." + + property.getGetMethod().getName() + "();"; + } + return null; + } + + public String generateSetPropertyForMethod() { + StringBuilder code = new StringBuilder(ReflectionLessCodeGenerator.getIndent(1)) + .append(generatesSetPropertyForMethodHeader()) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(2)) + .append("switch (") + .append(NAME_VAR_NAME) + .append(") {") + .append(System.lineSeparator()); + + //case statements for each property + for (ReflectionProperty property : getProperties()) { + String invocation = generateSetPropertyMethod(property); + if (invocation != null) { + code.append(ReflectionLessCodeGenerator.getIndent(3)) + .append("case \"") + .append(property.getPropertyName()) + .append("\" : ") + .append(System.lineSeparator()); + + code.append(ReflectionLessCodeGenerator.getIndent(4)) + .append(invocation) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(4)) + .append("return true;") + .append(System.lineSeparator()) + ; + + } else { + code.append(ReflectionLessCodeGenerator.getIndent(3)) + .append("//no set" + IntrospectionUtils.capitalize(property.getPropertyName())+ " method found on this class") + .append(System.lineSeparator()) + ; + } + } + + + + //end switch statement + code.append(ReflectionLessCodeGenerator.getIndent(2)) + .append('}') + .append(System.lineSeparator()); + + //we have a generic setProperty(String, String) method, invoke it + if (getGenericSetPropertyMethod() != null) { + ReflectionProperty p = new ReflectionProperty( + clazz.getName(), + "property", + String.class, + getGenericSetPropertyMethod(), + null + ); + code.append(ReflectionLessCodeGenerator.getIndent(2)) + .append("if (") + .append(SETP_VAR_NAME) + .append(") {") + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(3)) + .append(generateSetPropertyMethod(p)) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(3)) + .append("return true;") + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(2)) + .append('}') + .append(System.lineSeparator()); + } + + //invoke parent or return false + code.append(ReflectionLessCodeGenerator.getIndent(2)) + .append("return ") + .append(getSetPropertyForExitStatement()) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(1)) + .append('}'); + + return code.toString(); + } + + private String getSetPropertyForExitStatement() { + + return (getParent() != null) ? + //invoke the parent if we have one + getParent().generateParentSetPropertyForMethodInvocation() : + //if we invoke setProperty, return true, return false otherwise + getGenericSetPropertyMethod() != null ? "true;" : "false;"; + } + + public String generateInvocationSetForPropertyCaseStatement(int level) { + StringBuilder code = new StringBuilder(ReflectionLessCodeGenerator.getIndent(level)) + .append("case \"") + .append(getClazz().getName()) + .append("\" : ") + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(level+1)) + .append("return ") + .append(generateParentSetPropertyForMethodInvocation()) + .append(System.lineSeparator()); + return code.toString(); + } + + public String generateParentSetPropertyForMethodInvocation() { + String[] classParts = clazz.getName().split("\\.|\\$"); + StringBuilder methodInvocation = new StringBuilder("setPropertyFor"); + for (String s : classParts) { + methodInvocation.append(IntrospectionUtils.capitalize(s)); + } + methodInvocation.append('(') + .append(OBJECT_VAR_NAME) + .append(", ") + .append(NAME_VAR_NAME) + .append(", ") + .append(VALUE_VAR_NAME) + .append(", ") + .append(SETP_VAR_NAME) + .append(");"); + return methodInvocation.toString(); + } + + public String generatesSetPropertyForMethodHeader() { + String[] classParts = clazz.getName().split("\\.|\\$"); + StringBuilder methodInvocation = new StringBuilder("private static boolean setPropertyFor"); + for (String s : classParts) { + methodInvocation.append(IntrospectionUtils.capitalize(s)); + } + methodInvocation.append("(Object ") + .append(OBJECT_VAR_NAME) + .append(", String ") + .append(NAME_VAR_NAME) + .append(", String ") + .append(VALUE_VAR_NAME) + .append(", boolean ") + .append(SETP_VAR_NAME) + .append(") {"); + return methodInvocation.toString(); + } + + public String generateInvocationGetForPropertyCaseStatement(int level) { + StringBuilder code = new StringBuilder(ReflectionLessCodeGenerator.getIndent(level)) + .append("case \"") + .append(getClazz().getName()) + .append("\" : ") + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(level+1)) + .append("result = ") + .append(generateParentGetPropertyForMethodInvocation()) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(level+1)) + .append("break;") + .append(System.lineSeparator()) + ; + return code.toString(); + } + + public String generateParentGetPropertyForMethodInvocation() { + String[] classParts = clazz.getName().split("\\.|\\$"); + StringBuilder methodInvocation = new StringBuilder("getPropertyFor"); + for (String s : classParts) { + methodInvocation.append(IntrospectionUtils.capitalize(s)); + } + methodInvocation.append('(') + .append(OBJECT_VAR_NAME) + .append(", ") + .append(NAME_VAR_NAME) + .append(");"); + return methodInvocation.toString(); + } + + public String generatesGetPropertyForMethodHeader() { + String[] classParts = clazz.getName().split("\\.|\\$"); + StringBuilder methodInvocation = new StringBuilder("private static Object getPropertyFor"); + for (String s : classParts) { + methodInvocation.append(IntrospectionUtils.capitalize(s)); + } + methodInvocation.append("(Object ") + .append(OBJECT_VAR_NAME) + .append(", String ") + .append(NAME_VAR_NAME) + .append(") {"); + return methodInvocation.toString(); + } + + private String getGetPropertyForExitStatement() { + if (getParent() != null) { + return getParent().generateParentGetPropertyForMethodInvocation(); + } + return "null;"; + } + + + public String generateGetPropertyForMethod() { + StringBuilder code = new StringBuilder(ReflectionLessCodeGenerator.getIndent(1)) + .append(generatesGetPropertyForMethodHeader()) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(2)) + .append("Object result = null;") + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(2)) + .append("switch (") + .append(NAME_VAR_NAME) + .append(") {") + .append(System.lineSeparator()); + + //case statements for each property + for (ReflectionProperty property : getProperties()) { + String invocation = generateGetPropertyMethod(property); + if (invocation != null) { + code.append(ReflectionLessCodeGenerator.getIndent(3)) + .append("case \"") + .append(property.getPropertyName()) + .append("\" : ") + .append(System.lineSeparator()); + + code.append(ReflectionLessCodeGenerator.getIndent(4)) + .append(invocation) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(4)) + .append("break;") + .append(System.lineSeparator()) + ; + + } else { + code.append(ReflectionLessCodeGenerator.getIndent(3)) + .append("//no get" + IntrospectionUtils.capitalize(property.getPropertyName())+ " method found on this class") + .append(System.lineSeparator()) + ; + } + } + + //end switch statement + code.append(ReflectionLessCodeGenerator.getIndent(2)) + .append('}') + .append(System.lineSeparator()); + + //invoke parent or return null + code.append(ReflectionLessCodeGenerator.getIndent(2)) + .append("if (result == null) {") + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(3)) + .append("result = ") + .append(getGetPropertyForExitStatement()) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(2)) + .append('}') + .append(System.lineSeparator()) + ; + + //we have a generic getProperty(String, String) method, invoke it + if (getGenericGetPropertyMethod() != null) { + ReflectionProperty p = new ReflectionProperty( + clazz.getName(), + "property", + String.class, + null, + getGenericGetPropertyMethod() + ); + code.append(ReflectionLessCodeGenerator.getIndent(2)) + .append("if (result == null) {") + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(3)) + .append(generateGetPropertyMethod(p)) + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(2)) + .append('}') + .append(System.lineSeparator()); + } + code.append(ReflectionLessCodeGenerator.getIndent(2)) + .append("return result;") + .append(System.lineSeparator()) + .append(ReflectionLessCodeGenerator.getIndent(1)) + .append('}') + .append(System.lineSeparator()); + + + + return code.toString(); + } + + @Override + public int compareTo(SetPropertyClass o) { + return clazz.getName().compareTo(o.clazz.getName()); + } +} diff --git a/java/org/apache/tomcat/websocket/AsyncChannelGroupUtil.java b/java/org/apache/tomcat/websocket/AsyncChannelGroupUtil.java new file mode 100644 index 0000000..492fdc5 --- /dev/null +++ b/java/org/apache/tomcat/websocket/AsyncChannelGroupUtil.java @@ -0,0 +1,144 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.nio.channels.AsynchronousChannelGroup; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.threads.ThreadPoolExecutor; + +/** + * This is a utility class that enables multiple {@link WsWebSocketContainer} instances to share a single + * {@link AsynchronousChannelGroup} while ensuring that the group is destroyed when no longer required. + */ +public class AsyncChannelGroupUtil { + + private static final StringManager sm = StringManager.getManager(AsyncChannelGroupUtil.class); + + private static AsynchronousChannelGroup group = null; + private static int usageCount = 0; + private static final Object lock = new Object(); + + + private AsyncChannelGroupUtil() { + // Hide the default constructor + } + + + public static AsynchronousChannelGroup register() { + synchronized (lock) { + if (usageCount == 0) { + group = createAsynchronousChannelGroup(); + } + usageCount++; + return group; + } + } + + + public static void unregister() { + synchronized (lock) { + usageCount--; + if (usageCount == 0) { + group.shutdown(); + group = null; + } + } + } + + + private static AsynchronousChannelGroup createAsynchronousChannelGroup() { + // Need to do this with the right thread context class loader else the + // first web app to call this will trigger a leak + Thread currentThread = Thread.currentThread(); + ClassLoader original = currentThread.getContextClassLoader(); + + try { + currentThread.setContextClassLoader(AsyncIOThreadFactory.class.getClassLoader()); + + // These are the same settings as the default + // AsynchronousChannelGroup + int initialSize = Runtime.getRuntime().availableProcessors(); + ExecutorService executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, + TimeUnit.SECONDS, new SynchronousQueue<>(), new AsyncIOThreadFactory()); + + try { + return AsynchronousChannelGroup.withCachedThreadPool(executorService, initialSize); + } catch (IOException e) { + // No good reason for this to happen. + throw new IllegalStateException(sm.getString("asyncChannelGroup.createFail")); + } + } finally { + currentThread.setContextClassLoader(original); + } + } + + + private static class AsyncIOThreadFactory implements ThreadFactory { + + static { + // Load NewThreadPrivilegedAction since newThread() will not be able + // to if called from an InnocuousThread. + // See https://bz.apache.org/bugzilla/show_bug.cgi?id=57490 + NewThreadPrivilegedAction.load(); + } + + + @Override + public Thread newThread(final Runnable r) { + // Create the new Thread within a doPrivileged block to ensure that + // the thread inherits the current ProtectionDomain which is + // essential to be able to use this with a Java Applet. See + // https://bz.apache.org/bugzilla/show_bug.cgi?id=57091 + return AccessController.doPrivileged(new NewThreadPrivilegedAction(r)); + } + + // Non-anonymous class so that AsyncIOThreadFactory can load it + // explicitly + private static class NewThreadPrivilegedAction implements PrivilegedAction { + + private static AtomicInteger count = new AtomicInteger(0); + + private final Runnable r; + + NewThreadPrivilegedAction(Runnable r) { + this.r = r; + } + + @Override + public Thread run() { + Thread t = new Thread(r); + t.setName("WebSocketClient-AsyncIO-" + count.incrementAndGet()); + t.setContextClassLoader(this.getClass().getClassLoader()); + t.setDaemon(true); + return t; + } + + private static void load() { + // NO-OP. Just provides a hook to enable the class to be loaded + } + } + } +} diff --git a/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java b/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java new file mode 100644 index 0000000..978dc3f --- /dev/null +++ b/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java @@ -0,0 +1,48 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLException; + +/** + * This is a wrapper for a {@link java.nio.channels.AsynchronousSocketChannel} that limits the methods available thereby + * simplifying the process of implementing SSL/TLS support since there are fewer methods to intercept. + */ +public interface AsyncChannelWrapper { + + Future read(ByteBuffer dst); + + void read(ByteBuffer dst, A attachment, CompletionHandler handler); + + Future write(ByteBuffer src); + + void write(ByteBuffer[] srcs, int offset, int length, long timeout, TimeUnit unit, A attachment, + CompletionHandler handler); + + void close(); + + Future handshake() throws SSLException; + + SocketAddress getLocalAddress() throws IOException; +} diff --git a/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java b/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java new file mode 100644 index 0000000..31f8998 --- /dev/null +++ b/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java @@ -0,0 +1,112 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Generally, just passes calls straight to the wrapped {@link AsynchronousSocketChannel}. In some cases exceptions may + * be swallowed to save them being swallowed by the calling code. + */ +public class AsyncChannelWrapperNonSecure implements AsyncChannelWrapper { + + private static final Future NOOP_FUTURE = new NoOpFuture(); + + private final AsynchronousSocketChannel socketChannel; + + public AsyncChannelWrapperNonSecure(AsynchronousSocketChannel socketChannel) { + this.socketChannel = socketChannel; + } + + @Override + public Future read(ByteBuffer dst) { + return socketChannel.read(dst); + } + + @Override + public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { + socketChannel.read(dst, attachment, handler); + } + + @Override + public Future write(ByteBuffer src) { + return socketChannel.write(src); + } + + @Override + public void write(ByteBuffer[] srcs, int offset, int length, long timeout, TimeUnit unit, + A attachment, CompletionHandler handler) { + socketChannel.write(srcs, offset, length, timeout, unit, attachment, handler); + } + + @Override + public void close() { + try { + socketChannel.close(); + } catch (IOException e) { + // Ignore + } + } + + @Override + public Future handshake() { + return NOOP_FUTURE; + } + + + @Override + public SocketAddress getLocalAddress() throws IOException { + return socketChannel.getLocalAddress(); + } + + + private static final class NoOpFuture implements Future { + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + return null; + } + + @Override + public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return null; + } + } +} diff --git a/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java b/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java new file mode 100644 index 0000000..6836ba0 --- /dev/null +++ b/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java @@ -0,0 +1,556 @@ +/* + * 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.tomcat.websocket; + +import java.io.EOFException; +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +/** + * Wraps the {@link AsynchronousSocketChannel} with SSL/TLS. This needs a lot more testing before it can be considered + * robust. + */ +public class AsyncChannelWrapperSecure implements AsyncChannelWrapper { + + private final Log log = LogFactory.getLog(AsyncChannelWrapperSecure.class); + private static final StringManager sm = StringManager.getManager(AsyncChannelWrapperSecure.class); + + private static final ByteBuffer DUMMY = ByteBuffer.allocate(16921); + private final AsynchronousSocketChannel socketChannel; + private final SSLEngine sslEngine; + private final ByteBuffer socketReadBuffer; + private final ByteBuffer socketWriteBuffer; + // One thread for read, one for write + private final ExecutorService executor = Executors.newFixedThreadPool(2, new SecureIOThreadFactory()); + private AtomicBoolean writing = new AtomicBoolean(false); + private AtomicBoolean reading = new AtomicBoolean(false); + + public AsyncChannelWrapperSecure(AsynchronousSocketChannel socketChannel, SSLEngine sslEngine) { + this.socketChannel = socketChannel; + this.sslEngine = sslEngine; + + int socketBufferSize = sslEngine.getSession().getPacketBufferSize(); + socketReadBuffer = ByteBuffer.allocateDirect(socketBufferSize); + socketWriteBuffer = ByteBuffer.allocateDirect(socketBufferSize); + } + + @Override + public Future read(ByteBuffer dst) { + WrapperFuture future = new WrapperFuture<>(); + + if (!reading.compareAndSet(false, true)) { + throw new IllegalStateException(sm.getString("asyncChannelWrapperSecure.concurrentRead")); + } + + ReadTask readTask = new ReadTask(dst, future); + + executor.execute(readTask); + + return future; + } + + @Override + public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { + + WrapperFuture future = new WrapperFuture<>(handler, attachment); + + if (!reading.compareAndSet(false, true)) { + throw new IllegalStateException(sm.getString("asyncChannelWrapperSecure.concurrentRead")); + } + + ReadTask readTask = new ReadTask(dst, future); + + executor.execute(readTask); + } + + @Override + public Future write(ByteBuffer src) { + + WrapperFuture inner = new WrapperFuture<>(); + + if (!writing.compareAndSet(false, true)) { + throw new IllegalStateException(sm.getString("asyncChannelWrapperSecure.concurrentWrite")); + } + + WriteTask writeTask = new WriteTask(new ByteBuffer[] { src }, 0, 1, inner); + + executor.execute(writeTask); + + Future future = new LongToIntegerFuture(inner); + return future; + } + + @Override + public void write(ByteBuffer[] srcs, int offset, int length, long timeout, TimeUnit unit, + A attachment, CompletionHandler handler) { + + WrapperFuture future = new WrapperFuture<>(handler, attachment); + + if (!writing.compareAndSet(false, true)) { + throw new IllegalStateException(sm.getString("asyncChannelWrapperSecure.concurrentWrite")); + } + + WriteTask writeTask = new WriteTask(srcs, offset, length, future); + + executor.execute(writeTask); + } + + @Override + public void close() { + try { + socketChannel.close(); + } catch (IOException e) { + log.info(sm.getString("asyncChannelWrapperSecure.closeFail")); + } + executor.shutdownNow(); + } + + @Override + public Future handshake() throws SSLException { + + WrapperFuture wFuture = new WrapperFuture<>(); + + Thread t = new WebSocketSslHandshakeThread(wFuture); + t.start(); + + return wFuture; + } + + + @Override + public SocketAddress getLocalAddress() throws IOException { + return socketChannel.getLocalAddress(); + } + + + private class WriteTask implements Runnable { + + private final ByteBuffer[] srcs; + private final int offset; + private final int length; + private final WrapperFuture future; + + WriteTask(ByteBuffer[] srcs, int offset, int length, WrapperFuture future) { + this.srcs = srcs; + this.future = future; + this.offset = offset; + this.length = length; + } + + @Override + public void run() { + long written = 0; + + try { + for (int i = offset; i < offset + length; i++) { + ByteBuffer src = srcs[i]; + while (src.hasRemaining()) { + socketWriteBuffer.clear(); + + // Encrypt the data + SSLEngineResult r = sslEngine.wrap(src, socketWriteBuffer); + written += r.bytesConsumed(); + Status s = r.getStatus(); + + if (s == Status.OK || s == Status.BUFFER_OVERFLOW) { + // Need to write out the bytes and may need to read from + // the source again to empty it + } else { + // Status.BUFFER_UNDERFLOW - only happens on unwrap + // Status.CLOSED - unexpected + throw new IllegalStateException(sm.getString("asyncChannelWrapperSecure.statusWrap")); + } + + // Check for tasks + if (r.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + Runnable runnable = sslEngine.getDelegatedTask(); + while (runnable != null) { + runnable.run(); + runnable = sslEngine.getDelegatedTask(); + } + } + + socketWriteBuffer.flip(); + + // Do the write + int toWrite = r.bytesProduced(); + while (toWrite > 0) { + Future f = socketChannel.write(socketWriteBuffer); + Integer socketWrite = f.get(); + toWrite -= socketWrite.intValue(); + } + } + } + + + if (writing.compareAndSet(true, false)) { + future.complete(Long.valueOf(written)); + } else { + future.fail(new IllegalStateException(sm.getString("asyncChannelWrapperSecure.wrongStateWrite"))); + } + } catch (Exception e) { + writing.set(false); + future.fail(e); + } + } + } + + + private class ReadTask implements Runnable { + + private final ByteBuffer dest; + private final WrapperFuture future; + + ReadTask(ByteBuffer dest, WrapperFuture future) { + this.dest = dest; + this.future = future; + } + + @Override + public void run() { + int read = 0; + + boolean forceRead = false; + + try { + while (read == 0) { + socketReadBuffer.compact(); + + if (forceRead) { + forceRead = false; + Future f = socketChannel.read(socketReadBuffer); + Integer socketRead = f.get(); + if (socketRead.intValue() == -1) { + throw new EOFException(sm.getString("asyncChannelWrapperSecure.eof")); + } + } + + socketReadBuffer.flip(); + + if (socketReadBuffer.hasRemaining()) { + // Decrypt the data in the buffer + SSLEngineResult r = sslEngine.unwrap(socketReadBuffer, dest); + read += r.bytesProduced(); + Status s = r.getStatus(); + + if (s == Status.OK) { + // Bytes available for reading and there may be + // sufficient data in the socketReadBuffer to + // support further reads without reading from the + // socket + } else if (s == Status.BUFFER_UNDERFLOW) { + // There is partial data in the socketReadBuffer + if (read == 0) { + // Need more data before the partial data can be + // processed and some output generated + forceRead = true; + } + // else return the data we have and deal with the + // partial data on the next read + } else if (s == Status.BUFFER_OVERFLOW) { + // Not enough space in the destination buffer to + // store all of the data. We could use a bytes read + // value of -bufferSizeRequired to signal the new + // buffer size required but an explicit exception is + // clearer. + if (reading.compareAndSet(true, false)) { + throw new ReadBufferOverflowException( + sslEngine.getSession().getApplicationBufferSize()); + } else { + future.fail(new IllegalStateException( + sm.getString("asyncChannelWrapperSecure.wrongStateRead"))); + } + } else { + // Status.CLOSED - unexpected + throw new IllegalStateException(sm.getString("asyncChannelWrapperSecure.statusUnwrap")); + } + + // Check for tasks + if (r.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + Runnable runnable = sslEngine.getDelegatedTask(); + while (runnable != null) { + runnable.run(); + runnable = sslEngine.getDelegatedTask(); + } + } + } else { + forceRead = true; + } + } + + + if (reading.compareAndSet(true, false)) { + future.complete(Integer.valueOf(read)); + } else { + future.fail(new IllegalStateException(sm.getString("asyncChannelWrapperSecure.wrongStateRead"))); + } + } catch (RuntimeException | ReadBufferOverflowException | SSLException | EOFException | ExecutionException + | InterruptedException e) { + reading.set(false); + future.fail(e); + } + } + } + + + private class WebSocketSslHandshakeThread extends Thread { + + private final WrapperFuture hFuture; + + private HandshakeStatus handshakeStatus; + private Status resultStatus; + + WebSocketSslHandshakeThread(WrapperFuture hFuture) { + this.hFuture = hFuture; + } + + @Override + public void run() { + try { + sslEngine.beginHandshake(); + // So the first compact does the right thing + socketReadBuffer.position(socketReadBuffer.limit()); + + handshakeStatus = sslEngine.getHandshakeStatus(); + resultStatus = Status.OK; + + boolean handshaking = true; + + while (handshaking) { + switch (handshakeStatus) { + case NEED_WRAP: { + socketWriteBuffer.clear(); + SSLEngineResult r = sslEngine.wrap(DUMMY, socketWriteBuffer); + checkResult(r, true); + socketWriteBuffer.flip(); + Future fWrite = socketChannel.write(socketWriteBuffer); + fWrite.get(); + break; + } + case NEED_UNWRAP: { + socketReadBuffer.compact(); + if (socketReadBuffer.position() == 0 || resultStatus == Status.BUFFER_UNDERFLOW) { + Future fRead = socketChannel.read(socketReadBuffer); + fRead.get(); + } + socketReadBuffer.flip(); + SSLEngineResult r = sslEngine.unwrap(socketReadBuffer, DUMMY); + checkResult(r, false); + break; + } + case NEED_TASK: { + Runnable r = null; + while ((r = sslEngine.getDelegatedTask()) != null) { + r.run(); + } + handshakeStatus = sslEngine.getHandshakeStatus(); + break; + } + case FINISHED: { + handshaking = false; + break; + } + case NOT_HANDSHAKING: + // Don't expect to see this during a handshake + case NEED_UNWRAP_AGAIN: { + // Only applies to DLTS + throw new SSLException(sm.getString("asyncChannelWrapperSecure.unexpectedHandshakeState", + handshakeStatus)); + } + } + } + } catch (Exception e) { + hFuture.fail(e); + return; + } + + hFuture.complete(null); + } + + private void checkResult(SSLEngineResult result, boolean wrap) throws SSLException { + + handshakeStatus = result.getHandshakeStatus(); + resultStatus = result.getStatus(); + + if (resultStatus != Status.OK && (wrap || resultStatus != Status.BUFFER_UNDERFLOW)) { + throw new SSLException(sm.getString("asyncChannelWrapperSecure.check.notOk", resultStatus)); + } + if (wrap && result.bytesConsumed() != 0) { + throw new SSLException(sm.getString("asyncChannelWrapperSecure.check.wrap")); + } + if (!wrap && result.bytesProduced() != 0) { + throw new SSLException(sm.getString("asyncChannelWrapperSecure.check.unwrap")); + } + } + } + + + private static class WrapperFuture implements Future { + + private final CompletionHandler handler; + private final A attachment; + + private volatile T result = null; + private volatile Throwable throwable = null; + private CountDownLatch completionLatch = new CountDownLatch(1); + + WrapperFuture() { + this(null, null); + } + + WrapperFuture(CompletionHandler handler, A attachment) { + this.handler = handler; + this.attachment = attachment; + } + + public void complete(T result) { + this.result = result; + completionLatch.countDown(); + if (handler != null) { + handler.completed(result, attachment); + } + } + + public void fail(Throwable t) { + throwable = t; + completionLatch.countDown(); + if (handler != null) { + handler.failed(throwable, attachment); + } + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + // Could support cancellation by closing the connection + return false; + } + + @Override + public final boolean isCancelled() { + // Could support cancellation by closing the connection + return false; + } + + @Override + public final boolean isDone() { + return completionLatch.getCount() > 0; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + completionLatch.await(); + if (throwable != null) { + throw new ExecutionException(throwable); + } + return result; + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + boolean latchResult = completionLatch.await(timeout, unit); + if (latchResult == false) { + throw new TimeoutException(); + } + if (throwable != null) { + throw new ExecutionException(throwable); + } + return result; + } + } + + private static final class LongToIntegerFuture implements Future { + + private final Future wrapped; + + LongToIntegerFuture(Future wrapped) { + this.wrapped = wrapped; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return wrapped.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return wrapped.isCancelled(); + } + + @Override + public boolean isDone() { + return wrapped.isDone(); + } + + @Override + public Integer get() throws InterruptedException, ExecutionException { + Long result = wrapped.get(); + if (result.longValue() > Integer.MAX_VALUE) { + throw new ExecutionException(sm.getString("asyncChannelWrapperSecure.tooBig", result), null); + } + return Integer.valueOf(result.intValue()); + } + + @Override + public Integer get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + Long result = wrapped.get(timeout, unit); + if (result.longValue() > Integer.MAX_VALUE) { + throw new ExecutionException(sm.getString("asyncChannelWrapperSecure.tooBig", result), null); + } + return Integer.valueOf(result.intValue()); + } + } + + + private static class SecureIOThreadFactory implements ThreadFactory { + + private AtomicInteger count = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setName("WebSocketClient-SecureIO-" + count.incrementAndGet()); + // No need to set the context class loader. The threads will be + // cleaned up when the connection is closed. + t.setDaemon(true); + return t; + } + } +} diff --git a/java/org/apache/tomcat/websocket/AuthenticationException.java b/java/org/apache/tomcat/websocket/AuthenticationException.java new file mode 100644 index 0000000..9efc2d0 --- /dev/null +++ b/java/org/apache/tomcat/websocket/AuthenticationException.java @@ -0,0 +1,35 @@ +/* + * 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.tomcat.websocket; + +/** + * Exception thrown on authentication error connecting to a remote websocket endpoint. + */ +public class AuthenticationException extends Exception { + + private static final long serialVersionUID = 5709887412240096441L; + + /** + * Create authentication exception. + * + * @param message the error message + */ + public AuthenticationException(String message) { + super(message); + } + +} diff --git a/java/org/apache/tomcat/websocket/AuthenticationType.java b/java/org/apache/tomcat/websocket/AuthenticationType.java new file mode 100644 index 0000000..740a956 --- /dev/null +++ b/java/org/apache/tomcat/websocket/AuthenticationType.java @@ -0,0 +1,63 @@ +/* + * 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.tomcat.websocket; + +public enum AuthenticationType { + + WWW(Constants.AUTHORIZATION_HEADER_NAME, Constants.WWW_AUTHENTICATE_HEADER_NAME, + Constants.WS_AUTHENTICATION_USER_NAME, Constants.WS_AUTHENTICATION_PASSWORD, + Constants.WS_AUTHENTICATION_REALM), + + PROXY(Constants.PROXY_AUTHORIZATION_HEADER_NAME, Constants.PROXY_AUTHENTICATE_HEADER_NAME, + Constants.WS_AUTHENTICATION_PROXY_USER_NAME, Constants.WS_AUTHENTICATION_PROXY_PASSWORD, + Constants.WS_AUTHENTICATION_PROXY_REALM); + + private final String authorizationHeaderName; + private final String authenticateHeaderName; + private final String userNameProperty; + private final String userPasswordProperty; + private final String userRealmProperty; + + AuthenticationType(String authorizationHeaderName, String authenticateHeaderName, String userNameProperty, + String userPasswordProperty, String userRealmProperty) { + this.authorizationHeaderName = authorizationHeaderName; + this.authenticateHeaderName = authenticateHeaderName; + this.userNameProperty = userNameProperty; + this.userPasswordProperty = userPasswordProperty; + this.userRealmProperty = userRealmProperty; + } + + public String getAuthorizationHeaderName() { + return authorizationHeaderName; + } + + public String getAuthenticateHeaderName() { + return authenticateHeaderName; + } + + public String getUserNameProperty() { + return userNameProperty; + } + + public String getUserPasswordProperty() { + return userPasswordProperty; + } + + public String getUserRealmProperty() { + return userRealmProperty; + } +} diff --git a/java/org/apache/tomcat/websocket/Authenticator.java b/java/org/apache/tomcat/websocket/Authenticator.java new file mode 100644 index 0000000..43979ea --- /dev/null +++ b/java/org/apache/tomcat/websocket/Authenticator.java @@ -0,0 +1,124 @@ +/* + * 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.tomcat.websocket; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Base class for the authentication methods used by the WebSocket client. + */ +public abstract class Authenticator { + + private static final StringManager sm = StringManager.getManager(Authenticator.class); + + private static final Pattern pattern = Pattern.compile("(\\w+)\\s*=\\s*(\"([^\"]+)\"|([^,=\"]+))\\s*,?"); + + + /** + * Generate the authorization header value that will be sent to the server. + * + * @param requestUri The request URI + * @param authenticateHeader The server authentication header received + * @param userName The user name + * @param userPassword The user password + * @param userRealm The realm for which the provided user name and password are valid. {@code null} to + * indicate all realms. + * + * @return The generated authorization header value + * + * @throws AuthenticationException When an error occurs + */ + public abstract String getAuthorization(String requestUri, String authenticateHeader, String userName, + String userPassword, String userRealm) throws AuthenticationException; + + + /** + * Get the authentication method. + * + * @return the authentication scheme + */ + public abstract String getSchemeName(); + + + /** + * Utility method to parse the authentication header. + * + * @param authenticateHeader The server authenticate header received + * + * @return a map of authentication parameter names and values + */ + public Map parseAuthenticateHeader(String authenticateHeader) { + + Matcher m = pattern.matcher(authenticateHeader); + Map parameterMap = new HashMap<>(); + + while (m.find()) { + String key = m.group(1); + String qtedValue = m.group(3); + String value = m.group(4); + + parameterMap.put(key, qtedValue != null ? qtedValue : value); + + } + + return parameterMap; + } + + + protected void validateUsername(String userName) throws AuthenticationException { + if (userName == null) { + throw new AuthenticationException(sm.getString("authenticator.nullUserName")); + } + } + + + protected void validatePassword(String password) throws AuthenticationException { + if (password == null) { + throw new AuthenticationException(sm.getString("authenticator.nullPassword")); + } + } + + + protected void validateRealm(String userRealm, String serverRealm) throws AuthenticationException { + if (userRealm == null) { + return; + } + + userRealm = userRealm.trim(); + if (userRealm.length() == 0) { + return; + } + + /* + * User has configured a realm. Only allow authentication to proceed if the realm in the authentication + * challenge matches (both BASIC and DIGEST are required to include a realm). + */ + if (serverRealm != null) { + serverRealm = serverRealm.trim(); + if (userRealm.equals(serverRealm)) { + return; + } + } + + throw new AuthenticationException(sm.getString("authenticator.realmMismatch", userRealm, serverRealm)); + } +} diff --git a/java/org/apache/tomcat/websocket/AuthenticatorFactory.java b/java/org/apache/tomcat/websocket/AuthenticatorFactory.java new file mode 100644 index 0000000..9718db4 --- /dev/null +++ b/java/org/apache/tomcat/websocket/AuthenticatorFactory.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat.websocket; + +import java.util.ServiceLoader; + +/** + * Utility method to return the appropriate authenticator according to the scheme that the server uses. + */ +public class AuthenticatorFactory { + + /** + * Return a new authenticator instance. + * + * @param authScheme The scheme used + * + * @return the authenticator + */ + public static Authenticator getAuthenticator(String authScheme) { + + Authenticator auth = null; + switch (authScheme.toLowerCase()) { + + case BasicAuthenticator.schemeName: + auth = new BasicAuthenticator(); + break; + + case DigestAuthenticator.schemeName: + auth = new DigestAuthenticator(); + break; + + default: + auth = loadAuthenticators(authScheme); + break; + } + + return auth; + + } + + private static Authenticator loadAuthenticators(String authScheme) { + ServiceLoader serviceLoader = ServiceLoader.load(Authenticator.class); + + for (Authenticator auth : serviceLoader) { + if (auth.getSchemeName().equalsIgnoreCase(authScheme)) { + return auth; + } + } + + return null; + } + +} diff --git a/java/org/apache/tomcat/websocket/BackgroundProcess.java b/java/org/apache/tomcat/websocket/BackgroundProcess.java new file mode 100644 index 0000000..510df57 --- /dev/null +++ b/java/org/apache/tomcat/websocket/BackgroundProcess.java @@ -0,0 +1,26 @@ +/* + * 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.tomcat.websocket; + +public interface BackgroundProcess { + + void backgroundProcess(); + + void setProcessPeriod(int period); + + int getProcessPeriod(); +} diff --git a/java/org/apache/tomcat/websocket/BackgroundProcessManager.java b/java/org/apache/tomcat/websocket/BackgroundProcessManager.java new file mode 100644 index 0000000..ce91249 --- /dev/null +++ b/java/org/apache/tomcat/websocket/BackgroundProcessManager.java @@ -0,0 +1,144 @@ +/* + * 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.tomcat.websocket; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Provides a background processing mechanism that triggers roughly once a second. The class maintains a thread that + * only runs when there is at least one instance of {@link BackgroundProcess} registered. + */ +public class BackgroundProcessManager { + + private final Log log = LogFactory.getLog(BackgroundProcessManager.class); + private static final StringManager sm = StringManager.getManager(BackgroundProcessManager.class); + private static final BackgroundProcessManager instance; + + + static { + instance = new BackgroundProcessManager(); + } + + + public static BackgroundProcessManager getInstance() { + return instance; + } + + private final Set processes = new HashSet<>(); + private final Object processesLock = new Object(); + private WsBackgroundThread wsBackgroundThread = null; + + private BackgroundProcessManager() { + // Hide default constructor + } + + + public void register(BackgroundProcess process) { + synchronized (processesLock) { + if (processes.size() == 0) { + wsBackgroundThread = new WsBackgroundThread(this); + wsBackgroundThread.setContextClassLoader(this.getClass().getClassLoader()); + wsBackgroundThread.setDaemon(true); + wsBackgroundThread.start(); + } + processes.add(process); + } + } + + + public void unregister(BackgroundProcess process) { + synchronized (processesLock) { + processes.remove(process); + if (wsBackgroundThread != null && processes.size() == 0) { + wsBackgroundThread.halt(); + wsBackgroundThread = null; + } + } + } + + + private void process() { + Set currentProcesses; + synchronized (processesLock) { + currentProcesses = new HashSet<>(processes); + } + for (BackgroundProcess process : currentProcesses) { + try { + process.backgroundProcess(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("backgroundProcessManager.processFailed"), t); + } + } + } + + + /* + * For unit testing. + */ + int getProcessCount() { + synchronized (processesLock) { + return processes.size(); + } + } + + + void shutdown() { + synchronized (processesLock) { + processes.clear(); + if (wsBackgroundThread != null) { + wsBackgroundThread.halt(); + wsBackgroundThread = null; + } + } + } + + + private static class WsBackgroundThread extends Thread { + + private final BackgroundProcessManager manager; + private volatile boolean running = true; + + WsBackgroundThread(BackgroundProcessManager manager) { + setName("WebSocket background processing"); + this.manager = manager; + } + + @Override + public void run() { + while (running) { + try { + sleep(1000); + } catch (InterruptedException e) { + // Ignore + } + manager.process(); + } + } + + public void halt() { + setName("WebSocket background processing - stopping"); + running = false; + } + } +} diff --git a/java/org/apache/tomcat/websocket/BasicAuthenticator.java b/java/org/apache/tomcat/websocket/BasicAuthenticator.java new file mode 100644 index 0000000..c8e6385 --- /dev/null +++ b/java/org/apache/tomcat/websocket/BasicAuthenticator.java @@ -0,0 +1,63 @@ +/* + * 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.tomcat.websocket; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +/** + * Authenticator supporting the BASIC authentication method. + */ +public class BasicAuthenticator extends Authenticator { + + public static final String schemeName = "basic"; + public static final String charsetparam = "charset"; + + @Override + public String getAuthorization(String requestUri, String authenticateHeader, String userName, String userPassword, + String userRealm) throws AuthenticationException { + + validateUsername(userName); + validatePassword(userPassword); + + Map parameterMap = parseAuthenticateHeader(authenticateHeader); + String realm = parameterMap.get("realm"); + + validateRealm(userRealm, realm); + + String userPass = userName + ":" + userPassword; + Charset charset; + + if (parameterMap.get(charsetparam) != null && parameterMap.get(charsetparam).equalsIgnoreCase("UTF-8")) { + charset = StandardCharsets.UTF_8; + } else { + charset = StandardCharsets.ISO_8859_1; + } + + String base64 = Base64.getEncoder().encodeToString(userPass.getBytes(charset)); + + return " Basic " + base64; + } + + @Override + public String getSchemeName() { + return schemeName; + } + +} diff --git a/java/org/apache/tomcat/websocket/ClientEndpointHolder.java b/java/org/apache/tomcat/websocket/ClientEndpointHolder.java new file mode 100644 index 0000000..028b922 --- /dev/null +++ b/java/org/apache/tomcat/websocket/ClientEndpointHolder.java @@ -0,0 +1,28 @@ +/* + * 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.tomcat.websocket; + +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; + +import org.apache.tomcat.InstanceManager; + +public interface ClientEndpointHolder { + String getClassName(); + + Endpoint getInstance(InstanceManager instanceManager) throws DeploymentException; +} diff --git a/java/org/apache/tomcat/websocket/Constants.java b/java/org/apache/tomcat/websocket/Constants.java new file mode 100644 index 0000000..16f3f81 --- /dev/null +++ b/java/org/apache/tomcat/websocket/Constants.java @@ -0,0 +1,155 @@ +/* + * 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.tomcat.websocket; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.Extension; + +/** + * Internal implementation constants. + */ +public class Constants { + + // OP Codes + public static final byte OPCODE_CONTINUATION = 0x00; + public static final byte OPCODE_TEXT = 0x01; + public static final byte OPCODE_BINARY = 0x02; + public static final byte OPCODE_CLOSE = 0x08; + public static final byte OPCODE_PING = 0x09; + public static final byte OPCODE_PONG = 0x0A; + + // Internal OP Codes + // RFC 6455 limits OP Codes to 4 bits so these should never clash + // Always set bit 4 so these will be treated as control codes + static final byte INTERNAL_OPCODE_FLUSH = 0x18; + + // Buffers + static final int DEFAULT_BUFFER_SIZE = Integer + .getInteger("org.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE", 8 * 1024).intValue(); + + // Client connection + /** + * Property name to set to configure the value that is passed to + * {@link javax.net.ssl.SSLEngine#setEnabledProtocols(String[])}. The value should be a comma separated string. + * + * @deprecated This will be removed in Tomcat 11. Use {@link ClientEndpointConfig#getSSLContext()} + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.x") + public static final String SSL_PROTOCOLS_PROPERTY = "org.apache.tomcat.websocket.SSL_PROTOCOLS"; + @Deprecated(forRemoval = true, since = "Tomcat 10.1.x") + public static final String SSL_TRUSTSTORE_PROPERTY = "org.apache.tomcat.websocket.SSL_TRUSTSTORE"; + @Deprecated(forRemoval = true, since = "Tomcat 10.1.x") + public static final String SSL_TRUSTSTORE_PWD_PROPERTY = "org.apache.tomcat.websocket.SSL_TRUSTSTORE_PWD"; + @Deprecated(forRemoval = true, since = "Tomcat 10.1.x") + public static final String SSL_TRUSTSTORE_PWD_DEFAULT = "changeit"; + /** + * Property name to set to configure used SSLContext. The value should be an instance of SSLContext. If this + * property is present, the SSL_TRUSTSTORE* properties are ignored. + * + * @deprecated This will be removed in Tomcat 11. Use {@link ClientEndpointConfig#getSSLContext()} + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.x") + public static final String SSL_CONTEXT_PROPERTY = "org.apache.tomcat.websocket.SSL_CONTEXT"; + /** + * Property name to set to configure the timeout (in milliseconds) when establishing a WebSocket connection to + * server. The default is {@link #IO_TIMEOUT_MS_DEFAULT}. + */ + public static final String IO_TIMEOUT_MS_PROPERTY = "org.apache.tomcat.websocket.IO_TIMEOUT_MS"; + public static final long IO_TIMEOUT_MS_DEFAULT = 5000; + + // RFC 2068 recommended a limit of 5 + // Most browsers have a default limit of 20 + public static final String MAX_REDIRECTIONS_PROPERTY = "org.apache.tomcat.websocket.MAX_REDIRECTIONS"; + public static final int MAX_REDIRECTIONS_DEFAULT = 20; + + // HTTP upgrade header names and values + public static final String HOST_HEADER_NAME = "Host"; + public static final String UPGRADE_HEADER_NAME = "Upgrade"; + public static final String UPGRADE_HEADER_VALUE = "websocket"; + public static final String ORIGIN_HEADER_NAME = "Origin"; + public static final String CONNECTION_HEADER_NAME = "Connection"; + public static final String CONNECTION_HEADER_VALUE = "upgrade"; + public static final String LOCATION_HEADER_NAME = "Location"; + public static final String AUTHORIZATION_HEADER_NAME = "Authorization"; + public static final String WWW_AUTHENTICATE_HEADER_NAME = "WWW-Authenticate"; + public static final String PROXY_AUTHORIZATION_HEADER_NAME = "Proxy-Authorization"; + public static final String PROXY_AUTHENTICATE_HEADER_NAME = "Proxy-Authenticate"; + public static final String WS_VERSION_HEADER_NAME = "Sec-WebSocket-Version"; + public static final String WS_VERSION_HEADER_VALUE = "13"; + public static final String WS_KEY_HEADER_NAME = "Sec-WebSocket-Key"; + public static final String WS_PROTOCOL_HEADER_NAME = "Sec-WebSocket-Protocol"; + public static final String WS_EXTENSIONS_HEADER_NAME = "Sec-WebSocket-Extensions"; + + // HTTP status codes + public static final int MULTIPLE_CHOICES = 300; + public static final int MOVED_PERMANENTLY = 301; + public static final int FOUND = 302; + public static final int SEE_OTHER = 303; + public static final int USE_PROXY = 305; + public static final int TEMPORARY_REDIRECT = 307; + public static final int UNAUTHORIZED = 401; + public static final int PROXY_AUTHENTICATION_REQUIRED = 407; + + // Configuration for Origin header in client + static final String DEFAULT_ORIGIN_HEADER_VALUE = System + .getProperty("org.apache.tomcat.websocket.DEFAULT_ORIGIN_HEADER_VALUE"); + + // Configuration for blocking sends + public static final String BLOCKING_SEND_TIMEOUT_PROPERTY = "org.apache.tomcat.websocket.BLOCKING_SEND_TIMEOUT"; + // Milliseconds so this is 20 seconds + public static final long DEFAULT_BLOCKING_SEND_TIMEOUT = 20 * 1000; + + // Configuration for session close timeout + public static final String SESSION_CLOSE_TIMEOUT_PROPERTY = "org.apache.tomcat.websocket.SESSION_CLOSE_TIMEOUT"; + // Default is 30 seconds - setting is in milliseconds + public static final long DEFAULT_SESSION_CLOSE_TIMEOUT = TimeUnit.SECONDS.toMillis(30); + + // Configuration for read idle timeout on WebSocket session + public static final String READ_IDLE_TIMEOUT_MS = "org.apache.tomcat.websocket.READ_IDLE_TIMEOUT_MS"; + + // Configuration for write idle timeout on WebSocket session + public static final String WRITE_IDLE_TIMEOUT_MS = "org.apache.tomcat.websocket.WRITE_IDLE_TIMEOUT_MS"; + + // Configuration for background processing checks intervals + static final int DEFAULT_PROCESS_PERIOD = Integer + .getInteger("org.apache.tomcat.websocket.DEFAULT_PROCESS_PERIOD", 10).intValue(); + + public static final String WS_AUTHENTICATION_USER_NAME = "org.apache.tomcat.websocket.WS_AUTHENTICATION_USER_NAME"; + public static final String WS_AUTHENTICATION_PASSWORD = "org.apache.tomcat.websocket.WS_AUTHENTICATION_PASSWORD"; + public static final String WS_AUTHENTICATION_REALM = "org.apache.tomcat.websocket.WS_AUTHENTICATION_REALM"; + + public static final String WS_AUTHENTICATION_PROXY_USER_NAME = "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_USER_NAME"; + public static final String WS_AUTHENTICATION_PROXY_PASSWORD = "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_PASSWORD"; + public static final String WS_AUTHENTICATION_PROXY_REALM = "org.apache.tomcat.websocket.WS_AUTHENTICATION_PROXY_REALM"; + + public static final List INSTALLED_EXTENSIONS; + + static { + List installed = new ArrayList<>(1); + installed.add(new WsExtension("permessage-deflate")); + INSTALLED_EXTENSIONS = Collections.unmodifiableList(installed); + } + + private Constants() { + // Hide default constructor + } +} diff --git a/java/org/apache/tomcat/websocket/DecoderEntry.java b/java/org/apache/tomcat/websocket/DecoderEntry.java new file mode 100644 index 0000000..ed81e99 --- /dev/null +++ b/java/org/apache/tomcat/websocket/DecoderEntry.java @@ -0,0 +1,38 @@ +/* + * 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.tomcat.websocket; + +import jakarta.websocket.Decoder; + +public class DecoderEntry { + + private final Class clazz; + private final Class decoderClazz; + + public DecoderEntry(Class clazz, Class decoderClazz) { + this.clazz = clazz; + this.decoderClazz = decoderClazz; + } + + public Class getClazz() { + return clazz; + } + + public Class getDecoderClazz() { + return decoderClazz; + } +} \ No newline at end of file diff --git a/java/org/apache/tomcat/websocket/DigestAuthenticator.java b/java/org/apache/tomcat/websocket/DigestAuthenticator.java new file mode 100644 index 0000000..dd67f3f --- /dev/null +++ b/java/org/apache/tomcat/websocket/DigestAuthenticator.java @@ -0,0 +1,157 @@ +/* + * 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.tomcat.websocket; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Map; + +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Authenticator supporting the DIGEST authentication method. + */ +public class DigestAuthenticator extends Authenticator { + + private static final StringManager sm = StringManager.getManager(DigestAuthenticator.class); + + public static final String schemeName = "digest"; + private static final Object cnonceGeneratorLock = new Object(); + private static volatile SecureRandom cnonceGenerator; + private int nonceCount = 0; + private long cNonce; + + @Override + public String getAuthorization(String requestUri, String authenticateHeader, String userName, String userPassword, + String userRealm) throws AuthenticationException { + + validateUsername(userName); + validatePassword(userPassword); + + Map parameterMap = parseAuthenticateHeader(authenticateHeader); + String realm = parameterMap.get("realm"); + + validateRealm(userRealm, realm); + + String nonce = parameterMap.get("nonce"); + String messageQop = parameterMap.get("qop"); + String algorithm = parameterMap.get("algorithm") == null ? "MD5" : parameterMap.get("algorithm"); + String opaque = parameterMap.get("opaque"); + + StringBuilder challenge = new StringBuilder(); + + if (!messageQop.isEmpty()) { + if (cnonceGenerator == null) { + synchronized (cnonceGeneratorLock) { + if (cnonceGenerator == null) { + cnonceGenerator = new SecureRandom(); + } + } + } + + cNonce = cnonceGenerator.nextLong(); + nonceCount++; + } + + challenge.append("Digest "); + challenge.append("username =\"" + userName + "\","); + challenge.append("realm=\"" + realm + "\","); + challenge.append("nonce=\"" + nonce + "\","); + challenge.append("uri=\"" + requestUri + "\","); + + try { + challenge.append("response=\"" + + calculateRequestDigest(requestUri, userName, userPassword, realm, nonce, messageQop, algorithm) + + "\","); + } + + catch (NoSuchAlgorithmException e) { + throw new AuthenticationException(sm.getString("digestAuthenticator.algorithm", e.getMessage())); + } + + challenge.append("algorithm=" + algorithm + ","); + challenge.append("opaque=\"" + opaque + "\","); + + if (!messageQop.isEmpty()) { + challenge.append("qop=\"" + messageQop + "\""); + challenge.append(",cnonce=\"" + cNonce + "\","); + challenge.append("nc=" + String.format("%08X", Integer.valueOf(nonceCount))); + } + + return challenge.toString(); + + } + + private String calculateRequestDigest(String requestUri, String userName, String password, String realm, + String nonce, String qop, String algorithm) throws NoSuchAlgorithmException { + + boolean session = false; + if (algorithm.endsWith("-sess")) { + algorithm = algorithm.substring(0, algorithm.length() - 5); + session = true; + } + + StringBuilder preDigest = new StringBuilder(); + String A1; + + if (session) { + A1 = encode(algorithm, userName + ":" + realm + ":" + password) + ":" + nonce + ":" + cNonce; + } else { + A1 = userName + ":" + realm + ":" + password; + } + + /* + * If the "qop" value is "auth-int", then A2 is: A2 = Method ":" digest-uri-value ":" H(entity-body) since we do + * not have an entity-body, A2 = Method ":" digest-uri-value for auth and auth_int + */ + String A2 = "GET:" + requestUri; + + preDigest.append(encode(algorithm, A1)); + preDigest.append(':'); + preDigest.append(nonce); + + if (qop.toLowerCase().contains("auth")) { + preDigest.append(':'); + preDigest.append(String.format("%08X", Integer.valueOf(nonceCount))); + preDigest.append(':'); + preDigest.append(String.valueOf(cNonce)); + preDigest.append(':'); + preDigest.append(qop); + } + + preDigest.append(':'); + preDigest.append(encode(algorithm, A2)); + + return encode(algorithm, preDigest.toString()); + } + + private String encode(String algorithm, String value) throws NoSuchAlgorithmException { + byte[] bytesOfMessage = value.getBytes(StandardCharsets.ISO_8859_1); + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] thedigest = md.digest(bytesOfMessage); + + return HexUtils.toHexString(thedigest); + } + + @Override + public String getSchemeName() { + return schemeName; + } +} diff --git a/java/org/apache/tomcat/websocket/EndpointClassHolder.java b/java/org/apache/tomcat/websocket/EndpointClassHolder.java new file mode 100644 index 0000000..7a2cf21 --- /dev/null +++ b/java/org/apache/tomcat/websocket/EndpointClassHolder.java @@ -0,0 +1,57 @@ +/* + * 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.tomcat.websocket; + +import javax.naming.NamingException; + +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; + +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; + +public class EndpointClassHolder implements ClientEndpointHolder { + + private static final StringManager sm = StringManager.getManager(EndpointClassHolder.class); + + private final Class clazz; + + + public EndpointClassHolder(Class clazz) { + this.clazz = clazz; + } + + + @Override + public String getClassName() { + return clazz.getName(); + } + + + @Override + public Endpoint getInstance(InstanceManager instanceManager) throws DeploymentException { + try { + if (instanceManager == null) { + return clazz.getConstructor().newInstance(); + } else { + return (Endpoint) instanceManager.newInstance(clazz); + } + } catch (ReflectiveOperationException | NamingException e) { + throw new DeploymentException(sm.getString("clientEndpointHolder.instanceCreationFailed"), e); + } + } +} diff --git a/java/org/apache/tomcat/websocket/EndpointHolder.java b/java/org/apache/tomcat/websocket/EndpointHolder.java new file mode 100644 index 0000000..dcf9a9a --- /dev/null +++ b/java/org/apache/tomcat/websocket/EndpointHolder.java @@ -0,0 +1,56 @@ +/* + * 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.tomcat.websocket; + +import javax.naming.NamingException; + +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; + +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; + +public class EndpointHolder implements ClientEndpointHolder { + + private static final StringManager sm = StringManager.getManager(EndpointHolder.class); + + private final Endpoint endpoint; + + + public EndpointHolder(Endpoint endpoint) { + this.endpoint = endpoint; + } + + + @Override + public String getClassName() { + return endpoint.getClass().getName(); + } + + + @Override + public Endpoint getInstance(InstanceManager instanceManager) throws DeploymentException { + if (instanceManager != null) { + try { + instanceManager.newInstance(endpoint); + } catch (ReflectiveOperationException | NamingException e) { + throw new DeploymentException(sm.getString("clientEndpointHolder.instanceRegistrationFailed"), e); + } + } + return endpoint; + } +} diff --git a/java/org/apache/tomcat/websocket/FutureToSendHandler.java b/java/org/apache/tomcat/websocket/FutureToSendHandler.java new file mode 100644 index 0000000..17a01d3 --- /dev/null +++ b/java/org/apache/tomcat/websocket/FutureToSendHandler.java @@ -0,0 +1,109 @@ +/* + * 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.tomcat.websocket; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.websocket.SendHandler; +import jakarta.websocket.SendResult; + +import org.apache.tomcat.util.res.StringManager; + + +/** + * Converts a Future to a SendHandler. + */ +class FutureToSendHandler implements Future, SendHandler { + + private static final StringManager sm = StringManager.getManager(FutureToSendHandler.class); + + private final CountDownLatch latch = new CountDownLatch(1); + private final WsSession wsSession; + private volatile AtomicReference result = new AtomicReference<>(null); + + FutureToSendHandler(WsSession wsSession) { + this.wsSession = wsSession; + } + + + // --------------------------------------------------------- SendHandler + + @Override + public void onResult(SendResult result) { + this.result.compareAndSet(null, result); + latch.countDown(); + } + + + // -------------------------------------------------------------- Future + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + // Cancelling the task is not supported + return false; + } + + @Override + public boolean isCancelled() { + // Cancelling the task is not supported + return false; + } + + @Override + public boolean isDone() { + return latch.getCount() == 0; + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + try { + wsSession.registerFuture(this); + latch.await(); + } finally { + wsSession.unregisterFuture(this); + } + if (result.get().getException() != null) { + throw new ExecutionException(result.get().getException()); + } + return null; + } + + @Override + public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + boolean retval = false; + try { + wsSession.registerFuture(this); + retval = latch.await(timeout, unit); + } finally { + wsSession.unregisterFuture(this); + + } + if (retval == false) { + throw new TimeoutException( + sm.getString("futureToSendHandler.timeout", Long.valueOf(timeout), unit.toString().toLowerCase())); + } + if (result.get().getException() != null) { + throw new ExecutionException(result.get().getException()); + } + return null; + } +} diff --git a/java/org/apache/tomcat/websocket/LocalStrings.properties b/java/org/apache/tomcat/websocket/LocalStrings.properties new file mode 100644 index 0000000..79b8472 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings.properties @@ -0,0 +1,149 @@ +# 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. + +asyncChannelGroup.createFail=Unable to create dedicated AsynchronousChannelGroup for WebSocket clients which is required to prevent memory leaks in complex class loader environments like Jakarta EE containers + +asyncChannelWrapperSecure.check.notOk=TLS handshake returned an unexpected status [{0}] +asyncChannelWrapperSecure.check.unwrap=Bytes were written to the output during a read +asyncChannelWrapperSecure.check.wrap=Bytes were consumed from the input during a write +asyncChannelWrapperSecure.closeFail=Failed to close channel cleanly +asyncChannelWrapperSecure.concurrentRead=Concurrent read operations are not permitted +asyncChannelWrapperSecure.concurrentWrite=Concurrent write operations are not permitted +asyncChannelWrapperSecure.eof=Unexpected end of stream +asyncChannelWrapperSecure.statusUnwrap=Unexpected Status of SSLEngineResult after an unwrap() operation +asyncChannelWrapperSecure.statusWrap=Unexpected Status of SSLEngineResult after a wrap() operation +asyncChannelWrapperSecure.tooBig=The result [{0}] is too big to be expressed as an Integer +asyncChannelWrapperSecure.unexpectedHandshakeState=Unexpected state [{0}] during TLS handshake +asyncChannelWrapperSecure.wrongStateRead=Flag that indicates a read is in progress was found to be false (it should have been true) when trying to complete a read operation +asyncChannelWrapperSecure.wrongStateWrite=Flag that indicates a write is in progress was found to be false (it should have been true) when trying to complete a write operation + +authenticator.nullPassword=No password was provided to use for authentication +authenticator.nullUserName=No user name was provided to use for authentication +authenticator.realmMismatch=The user provided authentication realm [{0}] did not match the realm in the authentication challenge received from the server [{1}] + +backgroundProcessManager.processFailed=A background process failed + +caseInsensitiveKeyMap.nullKey=Null keys are not permitted + +clientEndpointHolder.instanceCreationFailed=Failed to create WebSocketEndpoint +clientEndpointHolder.instanceRegistrationFailed=Failed to register Endpoint instance with the InstanceManager + +digestAuthenticator.algorithm=Unable to generate request digest [{0}] + +futureToSendHandler.timeout=Operation timed out after waiting [{0}] [{1}] to complete + +perMessageDeflate.alreadyClosed=The transformer has been closed and may no longer be used +perMessageDeflate.deflateFailed=Failed to decompress a compressed WebSocket frame +perMessageDeflate.duplicateParameter=Duplicate definition of the [{0}] extension parameter +perMessageDeflate.invalidState=Invalid state +perMessageDeflate.invalidWindowSize=An invalid windows of [{1}] size was specified for [{0}]. Valid values are whole numbers from 8 to 15 inclusive. +perMessageDeflate.unknownParameter=An unknown extension parameter [{0}] was defined + +util.invalidMessageHandler=The message handler provided does not have an onMessage(Object) method +util.invalidType=Unable to coerce value [{0}] to type [{1}]. That type is not supported. +util.invalidValue=Value [{0}] contains delimiters +util.notToken=An illegal extension parameter was specified with name [{0}] and value [{1}] +util.unknownDecoderType=The Decoder type [{0}] is not recognized + +wsFrame.alreadyResumed=Message receiving has already been resumed. +wsFrame.alreadySuspended=Message receiving has already been suspended. +wsFrame.bufferTooSmall=No async message support and buffer too small. Buffer size: [{0}], Message size: [{1}] +wsFrame.byteToLongFail=Too many bytes ([{0}]) were provided to be converted into a long +wsFrame.closed=New frame received after a close control frame +wsFrame.controlFragmented=A fragmented control frame was received but control frames may not be fragmented +wsFrame.controlNoFin=A control frame was sent that did not have the fin bit set. Control frames are not permitted to use continuation frames. +wsFrame.controlPayloadTooBig=A control frame was sent with a payload of size [{0}] which is larger than the maximum permitted of 125 bytes +wsFrame.illegalReadState=Unexpected read state [{0}] +wsFrame.invalidOpCode=A WebSocket frame was sent with an unrecognised opCode of [{0}] +wsFrame.invalidUtf8=A WebSocket text frame was received that could not be decoded to UTF-8 because it contained invalid byte sequences +wsFrame.invalidUtf8Close=A WebSocket close frame was received with a close reason that contained invalid UTF-8 byte sequences +wsFrame.ioeTriggeredClose=An unrecoverable IOException occurred so the connection was closed +wsFrame.messageTooBig=The message was [{0}] bytes long but the MessageHandler has a limit of [{1}] bytes +wsFrame.noContinuation=A new message was started when a continuation frame was expected +wsFrame.notMasked=The client frame was not masked but all client frames must be masked +wsFrame.oneByteCloseCode=The client sent a close frame with a single byte payload which is not valid +wsFrame.partialHeaderComplete=WebSocket frame received. fin [{0}], rsv [{1}], OpCode [{2}], payload length [{3}] +wsFrame.payloadMsbInvalid=An invalid WebSocket frame was received - the most significant bit of a 64-bit payload was illegally set +wsFrame.readFailed=Async client read failed +wsFrame.sessionClosed=The client data cannot be processed because the session has already been closed +wsFrame.suspendRequested=Suspend of the message receiving has already been requested. +wsFrame.textMessageTooBig=The decoded text message was too big for the output buffer and the endpoint does not support partial messages +wsFrame.wrongRsv=The client frame set the reserved bits to [{0}] for a message with opCode [{1}] which was not supported by this endpoint + +wsFrameClient.ioe=Failure while reading data sent by server + +wsHandshakeRequest.invalidUri=The string [{0}] cannot be used to construct a valid URI +wsHandshakeRequest.unknownScheme=The scheme [{0}] in the request is not recognised + +wsRemoteEndpoint.acquireTimeout=The current message was not fully sent within the specified timeout +wsRemoteEndpoint.changeType=When sending a fragmented message, all fragments must be of the same type +wsRemoteEndpoint.closed=Message will not be sent because the WebSocket session has been closed +wsRemoteEndpoint.closedDuringMessage=The remainder of the message will not be sent because the WebSocket session has been closed +wsRemoteEndpoint.closedOutputStream=This method may not be called as the OutputStream has been closed +wsRemoteEndpoint.closedWriter=This method may not be called as the Writer has been closed +wsRemoteEndpoint.encoderDestoryFailed=Failed to destroy the encoder of type [{0}] +wsRemoteEndpoint.encoderError=Encoding error [{0}] +wsRemoteEndpoint.flushOnCloseFailed=Batched messages still enabled after session has been closed. Unable to flush remaining batched message. +wsRemoteEndpoint.invalidEncoder=The specified encoder of type [{0}] could not be instantiated +wsRemoteEndpoint.noEncoder=No encoder specified for object of class [{0}] +wsRemoteEndpoint.nullData=Invalid null data argument +wsRemoteEndpoint.nullHandler=Invalid null handler argument +wsRemoteEndpoint.sendInterrupt=The current thread was interrupted while waiting for a blocking send to complete +wsRemoteEndpoint.tooMuchData=Ping or pong may not send more than 125 bytes +wsRemoteEndpoint.writeTimeout=Blocking write timeout +wsRemoteEndpoint.wrongState=The remote endpoint was in state [{0}] which is an invalid state for called method + +wsSession.closed=The WebSocket session [{0}] has been closed and no method (apart from close()) may be called on a closed session +wsSession.created=Created WebSocket session [{0}] +wsSession.doClose=Closing WebSocket session [{0}] +wsSession.duplicateHandlerBinary=A binary message handler has already been configured +wsSession.duplicateHandlerPong=A pong message handler has already been configured +wsSession.duplicateHandlerText=A text message handler has already been configured +wsSession.flushFailOnClose=Failed to flush batched messages on session close +wsSession.instanceCreateFailed=Endpoint instance creation failed +wsSession.instanceNew=Endpoint instance registration failed +wsSession.invalidHandlerTypePong=A pong message handler must implement MessageHandler.Whole +wsSession.messageFailed=Unable to write the complete message as the WebSocket connection has been closed +wsSession.removeHandlerFailed=Unable to remove the handler [{0}] as it was not registered with this session +wsSession.sendCloseFail=Failed to send close message for session [{0}] to remote endpoint +wsSession.timeout=The WebSocket session [{0}] idle timeout expired +wsSession.timeoutRead=The WebSocket session [{0}] read idle timeout expired +wsSession.timeoutWrite=The WebSocket session [{0}] write idle timeout expired +wsSession.unknownHandler=Unable to add the message handler [{0}] as it was for the unrecognised type [{1}] +wsSession.unknownHandlerType=Unable to add the message handler [{0}] as it was wrapped as the unrecognised type [{1}] + +wsWebSocketContainer.asynchronousSocketChannelFail=Unable to open a connection to the server +wsWebSocketContainer.connect.entry=Connecting endpoint instance of type [{0}] to [{1}] +wsWebSocketContainer.connect.write=Writing the HTTP upgrade request from buffer starting at [{0}] with a limit of [{1}] from local address [{2}] +wsWebSocketContainer.defaultConfiguratorFail=Failed to create the default configurator +wsWebSocketContainer.failedAuthentication=Failed to handle HTTP response code [{0}]. [{1}] header was not accepted by server. +wsWebSocketContainer.httpRequestFailed=The HTTP request to initiate the WebSocket connection to [{0}] failed +wsWebSocketContainer.invalidExtensionParameters=The server responded with extension parameters the client is unable to support +wsWebSocketContainer.invalidHeader=Unable to parse HTTP header as no colon is present to delimit header name and header value in [{0}]. The header has been skipped. +wsWebSocketContainer.invalidStatus=The HTTP response from the server [{0}] did not permit the HTTP upgrade to WebSocket +wsWebSocketContainer.invalidSubProtocol=The WebSocket server returned multiple values for the Sec-WebSocket-Protocol header +wsWebSocketContainer.maxBuffer=This implementation limits the maximum size of a buffer to Integer.MAX_VALUE +wsWebSocketContainer.missingAnnotation=Cannot use POJO class [{0}] as it is not annotated with @ClientEndpoint +wsWebSocketContainer.missingAuthenticateHeader=Failed to handle HTTP response code [{0}]. Missing [{1}] header in response +wsWebSocketContainer.missingLocationHeader=Failed to handle HTTP response code [{0}]. Missing Location header in response +wsWebSocketContainer.pathNoHost=No host was specified in URI +wsWebSocketContainer.pathWrongScheme=The scheme [{0}] is not supported. The supported schemes are ws and wss +wsWebSocketContainer.proxyConnectFail=Failed to connect to the configured Proxy [{0}]. The HTTP response code was [{1}] +wsWebSocketContainer.redirectThreshold=Cyclic Location header [{0}] detected / reached max number of redirects [{1}] of max [{2}] +wsWebSocketContainer.responseFail=The HTTP upgrade to WebSocket failed but partial data may have been received: Status Code [{0}], HTTP headers [{1}] +wsWebSocketContainer.sessionCloseFail=Session with ID [{0}] did not close cleanly +wsWebSocketContainer.shutdown=The web application is stopping +wsWebSocketContainer.sslEngineFail=Unable to create SSLEngine to support SSL/TLS connections +wsWebSocketContainer.unsupportedAuthScheme=Failed to handle HTTP response code [{0}]. Unsupported Authentication scheme [{1}] returned in response diff --git a/java/org/apache/tomcat/websocket/LocalStrings_cs.properties b/java/org/apache/tomcat/websocket/LocalStrings_cs.properties new file mode 100644 index 0000000..a97bb1a --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_cs.properties @@ -0,0 +1,32 @@ +# 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. + +caseInsensitiveKeyMap.nullKey=Null klíÄe nejsou povoleny + +wsFrame.closed=Obdržen nový rámec po uzavírajícím rámci +wsFrame.wrongRsv=Klientský rámec nastavil rezervované bity na [{0}] pro zprávu s opCode [{1}], což pro tento koncový bod není podporováno + +wsHandshakeRequest.invalidUri=String [{0}] nelze použít pro vytvoÅ™ení platné URI + +wsRemoteEndpoint.closedDuringMessage=Zbytek zprávy nebude odeslán, protože WebSocket relace byla ukonÄena +wsRemoteEndpoint.flushOnCloseFailed=Dávkové zprávy jsou po uzavÅ™ení relace stále povoleny. Nelze vyprázdnit zbývající dávkovou zprávu. +wsRemoteEndpoint.wrongState=Vzdálený koncový bod byl ve stavu [{0}], což je pro volanou metodu neplatný stav + +wsSession.doClose=Zavírám WebSocket session [{0}] +wsSession.duplicateHandlerText=Jednotka (handler) pro textové zprávy již byla nakonfigurována +wsSession.instanceNew=Selhala registrace instance endpointu + +wsWebSocketContainer.missingAuthenticateHeader=Zpracování HTTP odpovÄ›di pro kód [{0}] selhalo. V odpovÄ›di chybí hlaviÄka [{1}] +wsWebSocketContainer.sessionCloseFail=Relace s ID [{0}] nebyla ukonÄena ÄistÄ› diff --git a/java/org/apache/tomcat/websocket/LocalStrings_de.properties b/java/org/apache/tomcat/websocket/LocalStrings_de.properties new file mode 100644 index 0000000..ebcc1f3 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_de.properties @@ -0,0 +1,36 @@ +# 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. + +caseInsensitiveKeyMap.nullKey=Null Schlüssel sind nicht erlaubt + +wsFrame.closed=Weiterer Frame empfangen nachdem bereits ein Kontroll Frame vom Typ Close empfangen wurde +wsFrame.illegalReadState=Unerwarteter Lesestatus [{0}] +wsFrame.wrongRsv=Der Client Frame setzt die reservierten Bits auf [{0}] für eine Nachricht mit dem opCode [{1}], der von diesem Endpunkt nicht unterstüzt wird + +wsHandshakeRequest.invalidUri=Aus dem String [{0}] kann keine valide URI konstruiert werden + +wsRemoteEndpoint.closedDuringMessage=Der Rest der Message wird nicht gesendet werden, da die WebSocket Session bereits beendet wurde +wsRemoteEndpoint.tooMuchData=Ping oder Pong darf nicht mehr als 125 Bytes senden +wsRemoteEndpoint.wrongState=Der entfernte Endpunkt war im Zustand [{0}] welcher für die aufgerufene Methode ungültig ist + +wsSession.created=Websocket Sitzung [{0}] erzeugt +wsSession.doClose=Schließe WebSocket-Sitzung [{0}] +wsSession.duplicateHandlerText=Ein Text Message Handler ist bereits konfiguriert +wsSession.instanceNew=Registrierung der Endpunkt-Instanz ist fehlgeschlagen + +wsWebSocketContainer.asynchronousSocketChannelFail=Es kann keine Verbindung zum Server hergestellt werden +wsWebSocketContainer.missingAnnotation=Die POJO Klasse [{0}] kann nicht verwendet werden da sie nicht mit @ClientEndpoint annotiert ist. +wsWebSocketContainer.pathNoHost=In der URI wurde kein Host angegeben +wsWebSocketContainer.sessionCloseFail=Die Sitzung mit der ID [{0}] wurde nicht sauber geschlossen. diff --git a/java/org/apache/tomcat/websocket/LocalStrings_es.properties b/java/org/apache/tomcat/websocket/LocalStrings_es.properties new file mode 100644 index 0000000..cdf18a8 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_es.properties @@ -0,0 +1,45 @@ +# 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. + +caseInsensitiveKeyMap.nullKey=No se permiten llaves nulas (Null) + +perMessageDeflate.duplicateParameter=La definición del parámetro de extención [{0}] esta duplicada +perMessageDeflate.invalidWindowSize=Una ventana inválida de tamaño [{1}] fue especificada para [{0}]. Los valores válidos son números entre 8 y 15 incluyendo los extremos. + +util.notToken=Un parámetro con extención ilegal fue especificado con nombre [{0}] y valor [{1}] +util.unknownDecoderType=No se reconoce el decodificador tipo [{0}] + +wsFrame.closed=Nuevo cuadro recibido luego de cerrar el cuadro de control +wsFrame.notMasked=El cuadro del cliente no fue enmascarado, pero todos los cuadros de clientes deben ser enmascarados +wsFrame.wrongRsv=El rango del cliente fija los bits reservados a [{0}] para un mensaje con opCode [{1}] el cual no fue soportado por este endpoint\n + +wsHandshakeRequest.invalidUri=La cadena [{0}] no puede ser usada para construir una URI válida + +wsRemoteEndpoint.closed=El mensaje no será enviado porque la sesión WebSocket ha sido cerrada +wsRemoteEndpoint.closedDuringMessage=No se enviara el resto del mensaje debido a que la sesión WebSocket esta cerrada +wsRemoteEndpoint.flushOnCloseFailed=Los mensages de lote estan habilitados aún después de haberse cerrado la sesión. Imposible descartar los messages de lote restantes. +wsRemoteEndpoint.sendInterrupt=El hilo actual fue interrumpido mientras esperaba que se completara un envio de bloqueo +wsRemoteEndpoint.tooMuchData=Ping o pong no pueden enviar más de 125 bytes +wsRemoteEndpoint.wrongState=El endpoint remoto estaba en estado [{0}] el cual es un estado no válido para el método llamado + +wsSession.closed=La sesión WebSocket [{0}] ha sido cerrada y ningún otro método (aparte de close()) puede ser llamado en una sesión cerrada\n +wsSession.created=La sesion WebSocket [{0}] fue creada\n +wsSession.doClose=Cerrando WebSocket sesión [{0}] +wsSession.duplicateHandlerText=Un manejador de mensaje de texto ya ha sido configurado +wsSession.instanceNew=Falló la registración de la instancia del dispoitivo final + +wsWebSocketContainer.missingAuthenticateHeader=Fallo al manejar el código de respuesta HTTP [{0}]. No existe la cabecera [{1}] en la respuesta +wsWebSocketContainer.pathNoHost=No se especificó ningún host en URI +wsWebSocketContainer.sessionCloseFail=La sesión con ID [{0}] no se cerró correctamente diff --git a/java/org/apache/tomcat/websocket/LocalStrings_fr.properties b/java/org/apache/tomcat/websocket/LocalStrings_fr.properties new file mode 100644 index 0000000..fc04ad7 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_fr.properties @@ -0,0 +1,149 @@ +# 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. + +asyncChannelGroup.createFail=Impossible de créer un AsynchronousChannelGroup dédié poour les clients Websockets ce qui est nécessaire pour éviter des fuites de mémoire dans un conteneur EE + +asyncChannelWrapperSecure.check.notOk=La négociation TLS a renvoyé un état inattendu [{0}] +asyncChannelWrapperSecure.check.unwrap=Des octets ont été écrits sur la sortie pendant la lecture +asyncChannelWrapperSecure.check.wrap=Des octets ont été consommés depuis l'entrée lors d'une écriture +asyncChannelWrapperSecure.closeFail=Impossible de fermer proprement le canal +asyncChannelWrapperSecure.concurrentRead=Les opérations de lecture concurrentes ne sont pas permises +asyncChannelWrapperSecure.concurrentWrite=Les opérations d'écriture concurrentes ne sont pas permises +asyncChannelWrapperSecure.eof=Fin de flux inattendue +asyncChannelWrapperSecure.statusUnwrap=Etat inattendu de SSLEngineResult après une opération unwrap() +asyncChannelWrapperSecure.statusWrap=Etat inattendu de SSLEngineResult après une opération wrap() +asyncChannelWrapperSecure.tooBig=Le résultat [{0}] est trop grand pour pouvoir être converti en Integer +asyncChannelWrapperSecure.unexpectedHandshakeState=Etat inattendu [{0}] lors de la négociation TLS +asyncChannelWrapperSecure.wrongStateRead=L'indicateur de lecture en cours était faux alors qu'il aurait dû vrai lors d'une tentative pour terminer une opération de lecture +asyncChannelWrapperSecure.wrongStateWrite=L'indicateur d'écriture en cours était faux alors qu'il aurait dû vrai lors d'une tentative pour terminer une opération d'écriture + +authenticator.nullPassword=Pas de mot de passe fourni pour l'authentification +authenticator.nullUserName=Pas de nom d'utilisateur fourni pour l'authentification +authenticator.realmMismatch=Le realm d''authentification fourni par l''utilisateur [{0}] ne correspond pas au realm dans la requête d''authentification recue par le serveur [{1}] + +backgroundProcessManager.processFailed=Un processus d'arrière-plan a échoué + +caseInsensitiveKeyMap.nullKey=Les clés nulles ne sont pas admises + +clientEndpointHolder.instanceCreationFailed=Echec de la création du WebSocketEndpoint +clientEndpointHolder.instanceRegistrationFailed=Echec de l'enregistrement de l'instance de l'Endpoint dans l'InstanceManager + +digestAuthenticator.algorithm=Impossible de générer l''abrégé (digest) pour la requête [{0}] + +futureToSendHandler.timeout=Le délai d''attente de l''opération est dépassé après avoir attendu [{0}] [{1}] pour qu''elle se termine + +perMessageDeflate.alreadyClosed=Le transformateur a été fermé et ne peut plus être utilisé +perMessageDeflate.deflateFailed=Impossible de décompresser une trame WebSocket compressée +perMessageDeflate.duplicateParameter=Double définition pour le paramètre d''extension [{0}] +perMessageDeflate.invalidState=Etat invalide +perMessageDeflate.invalidWindowSize=Une taille [{1}] de fenêtre invalide a été spécifiée pour [{0}], les valeurs valides sont les entiers de 8 à 15 inclus +perMessageDeflate.unknownParameter=Un paramètre d''extension inconnu [{0}] a été défini + +util.invalidMessageHandler=Le gestionnaire de messages fourni n'a pas de méthode onMessage(Object) +util.invalidType=Incapable de convertir la valeur [{0}] en le type [{1}]. Ce type n''est pas supporté. +util.invalidValue=La valeur [{0}] contient des délimitateurs +util.notToken=Un paramètre d''extension illégal a été spécifié avec le nom [{0}] et la valeur [{1}] +util.unknownDecoderType=Le Decoder de type [{0}] n''est pas reconnu + +wsFrame.alreadyResumed=Le message reçu a déjà été suspendu puis recommencé +wsFrame.alreadySuspended=La réception des messages a déjà été suspendue +wsFrame.bufferTooSmall=Le tampon de taille [{0}] est trop petit pour le message de taille [{1}] et les messages asynchrones ne sont pas supportés +wsFrame.byteToLongFail=trop d''octets fournis ([{0}]) pour une conversion vers "long" +wsFrame.closed=Nouvelle trame (frame) reçue après une trame de contrôle de fermeture +wsFrame.controlFragmented=Une trame de contrôle fragmentée a été reçue mais les trames de contrôle ne peuvent pas être fragmentées +wsFrame.controlNoFin=Une trame de contrôle qui a été envoyée n'avait pas le bit fin mis, alors qu'elles ne peuvent pas utiliser de trame de continuation +wsFrame.controlPayloadTooBig=Une trame de contrôle a été envoyée avec des données de taille [{0}] ce qui est supérieur aux 125 octets autorisés au maximum +wsFrame.illegalReadState=Etat en lecture inattendu [{0}] +wsFrame.invalidOpCode=Une trame a été envoyée avec un opCode non reconnu [{0}] +wsFrame.invalidUtf8=Une trame texte Websocket a été reçue et n'a pu 6etre traitée car elle contenait des séquence d'octets UTF-8 invalides +wsFrame.invalidUtf8Close=Une trame de fermeture Websocket a été reçue avec une cause qui contenait des séquences UTF-8 invalides +wsFrame.ioeTriggeredClose=Une IOException non récupérable est survenue donc la connection a été fermée +wsFrame.messageTooBig=Le message fait [{0}] octets mais le MessageHandler a une limite de [{1}] octets +wsFrame.noContinuation=Un nouveau message a été démarré quand une trame de continuation était attendue +wsFrame.notMasked=La trame du client n'a pas de masque alors que toutes les trames des clients doivent en avoir un +wsFrame.oneByteCloseCode=Le client a envoyé une trame de fermeture avec un octet de données ce qui est invalide +wsFrame.partialHeaderComplete=Une trame Websocket a été recue, fin [{0}], rsv [{1}], opCode [{2}], taille de données [{3}] +wsFrame.payloadMsbInvalid=Une trame WebSocket invalide a été reçue, le bit le plus significatif d'un bloc de 64 bits ne peut être mis +wsFrame.readFailed=La lecture asynchrone du client a échoué +wsFrame.sessionClosed=Les données du client ne peuvent pas être traitées car la session a déjà été fermée +wsFrame.suspendRequested=La suspension de la réception des messages a déjà été demandée +wsFrame.textMessageTooBig=Le message texte décodé était trop grand pour le tampon de sortie et la terminaison ne supporte pas les messages partiels +wsFrame.wrongRsv=La trame cliente (client frame) a les bits réservés d''un message dont l''opCode est [{1}] définis à [{0}], et ce n''est pas supporté par cette terminaison + +wsFrameClient.ioe=Echec lors de la lecture des données envoyées par le serveur + +wsHandshakeRequest.invalidUri=La chaîne de caractères [{0}] ne peut être utilisée pour construire un URL valide +wsHandshakeRequest.unknownScheme=Le schéma [{0}] de la requête n''est pas reconnu + +wsRemoteEndpoint.acquireTimeout=Le message en cours n'a pas été complètement envoyé dans le délai imparti +wsRemoteEndpoint.changeType=Quand un message fragmenté est envoyé, tous les fragments doivent être de même type +wsRemoteEndpoint.closed=Le message ne sera pas envoyé parce que la session WebSocket a été fermée +wsRemoteEndpoint.closedDuringMessage=Le reste du message ne sera pas envoyé parce que la session WebSocket est déjà fermée. +wsRemoteEndpoint.closedOutputStream=La méthode ne peut pas être appelée alors que l'OutputStream a été fermée +wsRemoteEndpoint.closedWriter=Cette méthode ne doit pas être appelée car le Writer a été fermé +wsRemoteEndpoint.encoderDestoryFailed=Echec de la destruction de l''encodeur de type [{0}] +wsRemoteEndpoint.encoderError=Erreur d''encodage [{0}] +wsRemoteEndpoint.flushOnCloseFailed=Le groupement de messages est toujours actif après fermeture de la session, impossible d'envoyer les messages restants +wsRemoteEndpoint.invalidEncoder=L''encodeur spécifié de type [{0}] n''a pu être instancié +wsRemoteEndpoint.noEncoder=Pas d''encodeur spécifié pour un objet de classe [{0}] +wsRemoteEndpoint.nullData=Argument nul invalide. +wsRemoteEndpoint.nullHandler=Argument null invalide pour le gestionnaire +wsRemoteEndpoint.sendInterrupt=Le thread actuel a été interrompu alors qu'il attendait qu'un envoi bloquant ne se termine +wsRemoteEndpoint.tooMuchData=Un ping ou pong ne peut pas envoyer plus de 125 octets +wsRemoteEndpoint.writeTimeout=Délai d'attente dépassé pour l'écriture bloquante +wsRemoteEndpoint.wrongState=La terminaison distante est dans l''état [{0}] ce qui est invalide pour la méthode appelée + +wsSession.closed=La session WebSocket [{0}] a été fermée et aucune méthode (à part close()) ne peut être appelée sur une session fermée +wsSession.created=Création de la session WebSocket [{0}] +wsSession.doClose=Fermeture de la session WebSocket [{0}] +wsSession.duplicateHandlerBinary=Un gestionnaire de message binaire a déjà été configuré +wsSession.duplicateHandlerPong=Un gestionnaire de messages pong a déjà été configuré +wsSession.duplicateHandlerText=Un gestionnaire de message texte a déjà été configuré +wsSession.flushFailOnClose=Impossible d'envoyer la file de messages lors de la fermeture de la session +wsSession.instanceCreateFailed=Echec de la création de l'instance de l'Endpoint +wsSession.instanceNew=L'enregistrement de l'instance de la terminaison a échoué +wsSession.invalidHandlerTypePong=Un gestionnaire de message pong doit implémenter MessageHandler.Whole +wsSession.messageFailed=Impossible d'écrire le message WebSocket complet car la connection a été fermée +wsSession.removeHandlerFailed=Impossible d''enlever le gestionnaire [{0}] car il n''était pas enregistré dans la session +wsSession.sendCloseFail=Impossible d''envoyer le message de fermeture pour la session [{0}] à la terminaison distante +wsSession.timeout=Le délai d''attente maximum de la session WebSocket [{0}] a été dépassé +wsSession.timeoutRead=Le délai d''inactivité en lecture de la session WebSocket [{0}] a expiré +wsSession.timeoutWrite=Le délai d''inactivité en écriture de la session WebSocket [{0}] a expiré +wsSession.unknownHandler=Impossible d''ajouter le gestionnaire de messages [{0}] pour le type non reconnu [{1}] +wsSession.unknownHandlerType=Incapable d''ajouter le gestionnaire de messages [{0}] puisqu''il est enveloppé (wrapped) comme le type non reconnu [{1}] + +wsWebSocketContainer.asynchronousSocketChannelFail=Impossible d'ouvrir une connection vers le serveur +wsWebSocketContainer.connect.entry=Connection à l''instance d''endpoint de type [{0}] à [{1}] +wsWebSocketContainer.connect.write=Ecriture de la requête d''upgrade HTTP depuis le tampon à partir de [{0}] avec une limite de [{1}] depuis l''adresse locale [{2}] +wsWebSocketContainer.defaultConfiguratorFail=Impossible de créer le configurateur par défaut +wsWebSocketContainer.failedAuthentication=Echec du traitement du code de réponse HTTP [{0}], l''en-tête d''authentification n''a pas été accepté par le serveur +wsWebSocketContainer.httpRequestFailed=La requête HTTP pour initier la connection WebSocket a échoué +wsWebSocketContainer.invalidExtensionParameters=Le serveur a répondu avec des paramètres d'extension que le client n'est pas capable de traiter +wsWebSocketContainer.invalidHeader=Impossible de traiter l''en-tête HTTP car deux-points n''est pas présents pour délimiter le nom et la valeur dans [{0}], l''en-tête a été sauté +wsWebSocketContainer.invalidStatus=La réponse HTTP du serveur [{0}] n''a pas permis une mise à niveau de HTTP vers WebSocket +wsWebSocketContainer.invalidSubProtocol=Le serveur WebSocket a renvoyé plusieurs valeurs pour l'en-tête Sec-WebSocket-Protocol +wsWebSocketContainer.maxBuffer=L'implémentation limites la valeur maximale d'un tampon à Integer.MAX_VALUE +wsWebSocketContainer.missingAnnotation=Impossible d''utiliser la classe POJO [{0}] car elle n''est pas annotée avec @ClientEndpoint +wsWebSocketContainer.missingAuthenticateHeader=Echec de traitement du code HTTP de réponse [{0}] : la réponse ne contient pas de header [{1}]. +wsWebSocketContainer.missingLocationHeader=Echec du traitement du code de réponse HTTP [{0}], l''en-tête location n''est pas présent dans la réponse +wsWebSocketContainer.pathNoHost=Aucun hôte n'est spécifié dans l'URI +wsWebSocketContainer.pathWrongScheme=Le schéma [{0}] n''est pas supporté, seuls sont supportés ws et wss +wsWebSocketContainer.proxyConnectFail=Impossible de se connecter au Proxy [{0}] configuré, le code HTTP de la réponse est [{0}] +wsWebSocketContainer.redirectThreshold=L''en-tête Location [{0}] est cyclique, le nombre de redirections [{1}] a été atteint sur le maximum [{2}] +wsWebSocketContainer.responseFail=L''upgrade de HTTP vers WebSocket a échouée mais des données partielles peuvent avoir été reçues: Status Code [{0}], En têtes HTTP [{1}] +wsWebSocketContainer.sessionCloseFail=La session avec ID [{0}] n''a pas été fermée proprement. +wsWebSocketContainer.shutdown=L'application web s'arrête +wsWebSocketContainer.sslEngineFail=Impossible de créer un SSLEngine pour supporter les connections TLS +wsWebSocketContainer.unsupportedAuthScheme=Impossible de gérer le code de réponse HTTP [{0}], un schéma authentification [{1}] non supporté a été retourné diff --git a/java/org/apache/tomcat/websocket/LocalStrings_ja.properties b/java/org/apache/tomcat/websocket/LocalStrings_ja.properties new file mode 100644 index 0000000..de552e3 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_ja.properties @@ -0,0 +1,149 @@ +# 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. + +asyncChannelGroup.createFail=JavaEEコンテナãªã©ã®è¤‡é›‘ãªã‚¯ãƒ©ã‚¹ãƒ­ãƒ¼ãƒ€ç’°å¢ƒã§ãƒ¡ãƒ¢ãƒªãƒªãƒ¼ã‚¯ã‚’防ããŸã‚ã«å¿…è¦ãªWebSocketクライアント専用ã®AsynchronousChannelGroupを作æˆã§ãã¾ã›ã‚“ + +asyncChannelWrapperSecure.check.notOk=TLSãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ãŒäºˆæœŸã—ãªã„ステータスを返ã—ã¾ã—㟠[{0}] +asyncChannelWrapperSecure.check.unwrap=読ã¿è¾¼ã¿ä¸­ã«ãƒã‚¤ãƒˆãŒå‡ºåŠ›ã«æ›¸ãè¾¼ã¾ã‚Œã¾ã—ãŸã€‚ +asyncChannelWrapperSecure.check.wrap=書ãè¾¼ã¿ä¸­ã«å…¥åŠ›ã‹ã‚‰ãƒã‚¤ãƒˆãŒæ¶ˆè²»ã•ã‚Œã¾ã—ãŸã€‚ +asyncChannelWrapperSecure.closeFail=ãƒãƒ£ãƒ³ãƒãƒ«ã‚’ãã‚Œã„ã«é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—㟠+asyncChannelWrapperSecure.concurrentRead=コンカレントãªèª­ã¿å–ã‚Šæ“作を行ã†ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +asyncChannelWrapperSecure.concurrentWrite=コンカレントãªæ›¸ãè¾¼ã¿æ“作を行ã†ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +asyncChannelWrapperSecure.eof=予期ã›ã¬ä½ç½®ã«ã‚¹ãƒˆãƒªãƒ¼ãƒ ã®çµ‚端を検出ã—ã¾ã—ãŸã€‚ +asyncChannelWrapperSecure.statusUnwrap=unwrap()æ“作後ã®SSLEngineResultã®äºˆæœŸã—ãªã„ステータス +asyncChannelWrapperSecure.statusWrap=wrap()æ“作後ã®SSLEngineResultã®äºˆæœŸã—ãªã„ステータス。 +asyncChannelWrapperSecure.tooBig=Integer ã¨ã—ã¦è§£é‡ˆã™ã‚‹ã«ã¯å¤§ãã™ãŽã‚‹çµæžœ [{0}] ã§ã™ã€‚ +asyncChannelWrapperSecure.unexpectedHandshakeState=TLSãƒãƒ³ãƒ‰ã‚·ã‚§ã‚¤ã‚¯ä¸­ã®äºˆæœŸã›ã¬çŠ¶æ…‹ [{0}] +asyncChannelWrapperSecure.wrongStateRead=読ã¿å–ã‚Šæ“作ã®å®Œäº†ä¸­ã«èª­ã¿å–り中をæ„味ã™ã‚‹ãƒ•ãƒ©ã‚°ãŒ false ã«ãªã£ã¦ã„ã‚‹ã“ã¨ã‚’検出ã—ã¾ã—㟠(true ã«ãªã£ã¦ã„ã‚‹ã¹ãã§ã™) +asyncChannelWrapperSecure.wrongStateWrite=書ãè¾¼ã¿æ“作を完了ã—よã†ã¨ã™ã‚‹ã¨ã€æ›¸ãè¾¼ã¿ãŒé€²è¡Œä¸­ã§ã‚ã‚‹ã“ã¨ã‚’示ã™ãƒ•ãƒ©ã‚°ãŒ false (true ã§ã‚ã£ãŸã¯ãšã§ã™) ã§ã‚ã‚‹ã“ã¨ãŒåˆ¤æ˜Žã—ã¾ã—㟠+ +authenticator.nullPassword=èªè¨¼ã«ä½¿ç”¨ã™ã‚‹ãƒ‘スワードãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ +authenticator.nullUserName=èªè¨¼ã«ä½¿ç”¨ã™ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼åãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ +authenticator.realmMismatch=ユーザãŒæŒ‡å®šã—ãŸèªè¨¼ãƒ¬ãƒ«ãƒ  [{0}] ã¯ã€ã‚µãƒ¼ãƒ [{1}] ã‹ã‚‰å—ä¿¡ã—ãŸèªè¨¼ãƒãƒ£ãƒ¬ãƒ³ã‚¸ã®ãƒ¬ãƒ«ãƒ ã¨ä¸€è‡´ã—ã¾ã›ã‚“ã§ã—㟠+ +backgroundProcessManager.processFailed=ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰å‡¦ç†ãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ + +caseInsensitiveKeyMap.nullKey=null 値ã¯ã‚­ãƒ¼ã«ä½¿ç”¨ã§ãã¾ã›ã‚“。 + +clientEndpointHolder.instanceCreationFailed=WebSocketEndpointã®ä½œæˆã«å¤±æ•—ã—ã¾ã—㟠+clientEndpointHolder.instanceRegistrationFailed=エンドãƒã‚¤ãƒ³ãƒˆã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’InstanceManagerã«ç™»éŒ²ã§ãã¾ã›ã‚“ã§ã—㟠+ +digestAuthenticator.algorithm=リクエストダイジェストを生æˆã§ãã¾ã›ã‚“ [{0}] + +futureToSendHandler.timeout=完了を [{0}] [{1}] 待機後ã«ã€æ“作ãŒã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã—㟠+ +perMessageDeflate.alreadyClosed=transformer ã¯ã‚¯ãƒ­ãƒ¼ã‚ºã•ã‚Œã¾ã—ãŸã€‚ã“れ以上使用ã•ã‚Œã¾ã›ã‚“ +perMessageDeflate.deflateFailed=圧縮ã•ã‚ŒãŸ WebSocket フレームを展開ã§ãã¾ã›ã‚“。 +perMessageDeflate.duplicateParameter=[{0}]拡張パラメータã®é‡è¤‡ã—ãŸå®šç¾© +perMessageDeflate.invalidState=ä¸æ­£ãªçŠ¶æ…‹ã§ã™ã€‚ +perMessageDeflate.invalidWindowSize=[{0}] ã®ã‚¦ã‚¤ãƒ³ãƒ‰ã‚¦ã‚µã‚¤ã‚ºã«ç„¡åŠ¹ãªå€¤ [{1}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ウインドウサイズ㯠8 以上 15 以下ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +perMessageDeflate.unknownParameter=未知ã®æ‹¡å¼µãƒ‘ラメーター [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ + +util.invalidMessageHandler=æä¾›ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒãƒ³ãƒ‰ãƒ©ã«onMessage(Object)メソッドãŒã‚ã‚Šã¾ã›ã‚“ +util.invalidType=値[{0}]をタイプ [{1}] ã«å¼·åˆ¶ã§ãã¾ã›ã‚“。ã“ã®ã‚¿ã‚¤ãƒ—ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。 +util.invalidValue=値 [{0}] ã¯åŒºåˆ‡ã‚Šæ–‡å­—ã‚’å«ã‚“ã§ã„ã¾ã™ +util.notToken=パラメーターå [{0}]ã€å€¤ [{1}] ã®ä¸æ­£ãªæ‹¡å¼µãƒ‘ラメーターãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +util.unknownDecoderType=デコーダタイプ [{0}] ã¯èªè­˜ã•ã‚Œã¾ã›ã‚“ + +wsFrame.alreadyResumed=メッセージã®å—ä¿¡ã¯æ—¢ã«å†é–‹ã•ã‚Œã¦ã„ã¾ã™ã€‚ +wsFrame.alreadySuspended=メッセージã®å—ä¿¡ã¯æ—¢ã«ä¸­æ–­ã•ã‚Œã¦ã„ã¾ã™ã€‚ +wsFrame.bufferTooSmall=éžåŒæœŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®éžã‚µãƒãƒ¼ãƒˆã¨ãƒãƒƒãƒ•ã‚¡ãƒ¼ãŒå°ã•ã™ãŽã¾ã™ã€‚ãƒãƒƒãƒ•ã‚¡ã‚µã‚¤ã‚ºï¼š[{0}]ã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚µã‚¤ã‚ºï¼š[{1}] +wsFrame.byteToLongFail=long ã¸å¤‰æ›ã™ã‚‹ãŸã‚ã«å·¨å¤§ãªãƒã‚¤ãƒˆåˆ— ([{0}]) ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ +wsFrame.closed=é–‰ã˜ãŸã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ«ãƒ•ãƒ¬ãƒ¼ãƒ ã§æ–°ã—ã„データフレームをå—ä¿¡ã—ã¾ã—㟠+wsFrame.controlFragmented=断片化ã•ã‚ŒãŸã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ«ãƒ•ãƒ¬ãƒ¼ãƒ ãŒå—ä¿¡ã•ã‚Œã¾ã—ãŸãŒã€ã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ«ãƒ•ãƒ¬ãƒ¼ãƒ ã¯æ–­ç‰‡åŒ–ã•ã‚Œã¾ã›ã‚“。 +wsFrame.controlNoFin=fin ビットãŒã‚ªãƒ•ã«ãªã£ã¦ã„るコントロールフレームをå—ä¿¡ã—ã¾ã—ãŸã€‚コントロールフレームを継続フレームã¨ã—ã¦ä½¿ç”¨ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +wsFrame.controlPayloadTooBig=コントロールフレーム㯠[{0}] ãƒã‚¤ãƒˆã®ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰ã§é€ä¿¡ã•ã‚Œã¾ã—ãŸãŒã€é€ä¿¡å¯èƒ½ãªæœ€å¤§å€¤ã® 125 ãƒã‚¤ãƒˆã‚’越ãˆã¦ã„ã¾ã™ã€‚ +wsFrame.illegalReadState=予期ã—ãªã„読ã¿å–り状態[{0}] +wsFrame.invalidOpCode=未知㮠opCode [{0}] ã® WebSocket フレームをå—ä¿¡ã—ã¾ã—ãŸã€‚ +wsFrame.invalidUtf8=無効ãªãƒã‚¤ãƒˆã‚·ãƒ¼ã‚±ãƒ³ã‚¹ãŒå«ã¾ã‚Œã¦ã„ãŸãŸã‚ã€UTF-8ã«ãƒ‡ã‚³ãƒ¼ãƒ‰ã§ããªã‹ã£ãŸWebSocketテキストフレームãŒå—ä¿¡ã•ã‚Œã¾ã—ãŸã€‚ +wsFrame.invalidUtf8Close=WebSocket ã¯ç„¡åŠ¹ãª UTF-8 ãƒã‚¤ãƒˆåˆ—ã‚’å«ã‚€ã“ã¨ã‚’原因ã¨ã™ã‚‹ã‚¯ãƒ­ãƒ¼ã‚ºãƒ•ãƒ¬ãƒ¼ãƒ ã‚’å—ä¿¡ã—ã¾ã—ãŸã€‚ +wsFrame.ioeTriggeredClose=回復ä¸èƒ½ãªIOException ãŒç™ºç”Ÿã—ãŸãŸã‚コãƒã‚¯ã‚·ãƒ§ãƒ³ã‚’切断ã—ã¾ã™ã€‚ +wsFrame.messageTooBig=メッセージ㯠[{0}] ãƒã‚¤ãƒˆã®é•·ã•ã§ã—ãŸãŒã€MessageHandlerã«ã¯ [{1}] ãƒã‚¤ãƒˆã®åˆ¶é™ãŒã‚ã‚Šã¾ã™ +wsFrame.noContinuation=continuation フレームãŒäºˆæƒ³ã•ã‚ŒãŸã¨ãã«æ–°ã—ã„メッセージãŒé–‹å§‹ã•ã‚Œã¾ã—ãŸã€‚ +wsFrame.notMasked=クライアントã®ãƒ•ãƒ¬ãƒ¼ãƒ ã¯ãƒžã‚¹ã‚¯ã•ã‚Œã¦ã„ã¾ã›ã‚“ãŒã€å…¨ã¦ã®ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã®ãƒ•ãƒ¬ãƒ¼ãƒ ã¯ãƒžã‚¹ã‚¯ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +wsFrame.oneByteCloseCode=クライアントã¯æœ‰åŠ¹ã§ã¯ãªã„å˜ä¸€ãƒã‚¤ãƒˆã®ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰ã‚’å«ã‚€ã‚¯ãƒ­ãƒ¼ã‚ºãƒ•ãƒ¬ãƒ¼ãƒ ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚ +wsFrame.partialHeaderComplete=WebSocket フレームをå—ä¿¡ã—ã¾ã—ãŸã€‚fin [{0}]ã€rsv [{1}]ã€OpCode [{2}]ã€ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰é•· [{3}] +wsFrame.payloadMsbInvalid=無効ãªWebSocketフレームをå—ä¿¡ã—ã¾ã—ãŸ-64ビットペイロードã®æœ€ä¸Šä½ãƒ“ットãŒä¸æ­£ã«è¨­å®šã•ã‚Œã¾ã—㟠+wsFrame.readFailed=éžåŒæœŸã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã®èª­ã¿å–ã‚Šã«å¤±æ•—ã—ã¾ã—㟠+wsFrame.sessionClosed=セッションãŒæ—¢ã«é–‰ã˜ã‚‰ã‚Œã¦ã„ã‚‹ãŸã‚ã€ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆãƒ‡ãƒ¼ã‚¿ã‚’処ç†ã§ãã¾ã›ã‚“。 +wsFrame.suspendRequested=ã™ã§ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®å—信中断をè¦æ±‚ã—ã¦ã„ã¾ã™ã€‚ +wsFrame.textMessageTooBig=デコードã•ã‚ŒãŸãƒ†ã‚­ã‚¹ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãŒå‡ºåŠ›ãƒãƒƒãƒ•ã‚¡ã«ã¨ã£ã¦å¤§ãã™ãŽã¾ã™ã€‚ã•ã‚‰ã«ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆãŒéƒ¨åˆ†ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 +wsFrame.wrongRsv=クライアントフレーム㯠opCode [{1}] ã§ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã™ã‚‹ãŸã‚ [{0}] ã®äºˆç´„ビットを設定ã—ã¾ã—ãŸãŒã€ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã¯å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“ + +wsFrameClient.ioe=サーãƒãƒ¼ã‹ã‚‰é€ä¿¡ã•ã‚ŒãŸãƒ‡ãƒ¼ã‚¿ã‚’読ã¿å–ã‚‹éš›ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ + +wsHandshakeRequest.invalidUri=文字列 [{0}] ã¯æ­£å¸¸ãª URI ã«å«ã‚ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。 +wsHandshakeRequest.unknownScheme=リクエストã®ã‚¹ã‚­ãƒ¼ãƒ  [{0}] ãŒèªè­˜ã•ã‚Œã¾ã›ã‚“ + +wsRemoteEndpoint.acquireTimeout=指定ã—ãŸæ™‚間内ã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +wsRemoteEndpoint.changeType=フラグメント化ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã™ã‚‹å ´åˆã€ã™ã¹ã¦ã®ãƒ•ãƒ©ã‚°ãƒ¡ãƒ³ãƒˆã¯åŒã˜ã‚¿ã‚¤ãƒ—ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。 +wsRemoteEndpoint.closed=WebSocket セッションã¯åˆ‡æ–­æ¸ˆã¿ã®ãŸã‚メッセージをé€ä¿¡ã—ã¾ã›ã‚“。 +wsRemoteEndpoint.closedDuringMessage=WebSocket セッションãŒåˆ‡æ–­ã•ã‚Œã¦ã„ã‚‹ãŸã‚残りã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã¯é€ä¿¡ã§ãã¾ã›ã‚“ +wsRemoteEndpoint.closedOutputStream=ã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ã¯ã€OutputStreamãŒé–‰ã˜ã‚‰ã‚ŒãŸã¨ãã«å‘¼ã³å‡ºã•ã‚Œãªã„å ´åˆãŒã‚ã‚Šã¾ã™ã€‚ +wsRemoteEndpoint.closedWriter=WriterãŒã‚¯ãƒ­ãƒ¼ã‚ºã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€ã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ã‚’呼ã³å‡ºã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +wsRemoteEndpoint.encoderDestoryFailed=タイプ [{0}] ã®ã‚¨ãƒ³ã‚³ãƒ¼ãƒ€ãƒ¼ã®ç ´æ£„ã«å¤±æ•—ã—ã¾ã—㟠+wsRemoteEndpoint.encoderError=エンコードエラー [{0}] +wsRemoteEndpoint.flushOnCloseFailed=セッションãŒé–‰ã˜ã‚‰ã‚ŒãŸå¾Œã«ã¾ã å¯èƒ½ã§ã‚ã£ãŸãƒãƒƒãƒãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã€‚残りã®ãƒãƒƒãƒãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’フラッシュã§ãã¾ã›ã‚“。 +wsRemoteEndpoint.invalidEncoder=指定ã•ã‚ŒãŸã‚¿ã‚¤ãƒ— [{0}] ã®ã‚¨ãƒ³ã‚³ãƒ¼ãƒ€ã‚’インスタンス化ã§ãã¾ã›ã‚“ã§ã—㟠+wsRemoteEndpoint.noEncoder=クラス [{0}] ã®ã‚ªãƒ–ジェクトã®ã‚¨ãƒ³ã‚³ãƒ¼ãƒ€ãƒ¼ãŒæœªæŒ‡å®šã§ã™ã€‚ +wsRemoteEndpoint.nullData=無効ãªNullデータ引数 +wsRemoteEndpoint.nullHandler=無効ãªnullãƒãƒ³ãƒ‰ãƒ©å¼•æ•° +wsRemoteEndpoint.sendInterrupt=åŒæœŸé€ä¿¡ã®å®Œäº†å¾…ã¡ã‚¹ãƒ¬ãƒƒãƒ‰ã«å‰²ã‚Šè¾¼ã¿ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +wsRemoteEndpoint.tooMuchData=Ping ãŠã‚ˆã³ Pong 㯠125 ãƒã‚¤ãƒˆä»¥ä¸Šé€ä¿¡ã§ãã¾ã›ã‚“。 +wsRemoteEndpoint.writeTimeout=ブロッキング書ãè¾¼ã¿ã®ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆ +wsRemoteEndpoint.wrongState=リモートエンドãƒã‚¤ãƒ³ãƒˆã®çŠ¶æ…‹ [{0}] ã¯å‘¼ã³å‡ºã—ãŸãƒ¡ã‚½ãƒƒãƒ‰ã«å¯¾ã—ã¦ç„¡åŠ¹ãªçŠ¶æ…‹ã§ã™ + +wsSession.closed=WebSocket セッション [{0}] を切断ã—ã¾ã—ãŸã€‚切断済ã¿ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã« close() 以外ã®ãƒ¡ã‚½ãƒƒãƒ‰å‘¼ã³å‡ºã—ã‚’ã™ã‚‹ã“ã¨ã¯ã‚ã‚Šã¾ã›ã‚“。 +wsSession.created=WebSocket セッション [{0}] を作æˆã—ã¾ã—ãŸã€‚ +wsSession.doClose=WebSocket セッション [{0}] を切断ã—ã¾ã™ã€‚ +wsSession.duplicateHandlerBinary=ãƒã‚¤ãƒŠãƒªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒãƒ³ãƒ‰ãƒ©ã¯æ—¢ã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ +wsSession.duplicateHandlerPong=pongメッセージãƒãƒ³ãƒ‰ãƒ©ã¯æ—¢ã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ã€‚ +wsSession.duplicateHandlerText=テキストメッセージãƒãƒ³ãƒ‰ãƒ©ã¯ã™ã§ã«æ§‹æˆã•ã‚Œã¦ã„ã¾ã™ã€‚ +wsSession.flushFailOnClose=セッション切断時ã«ãƒãƒƒãƒãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’フラッシュã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +wsSession.instanceCreateFailed=エンドãƒã‚¤ãƒ³ãƒˆã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ä½œæˆã«å¤±æ•—ã—ã¾ã—㟠+wsSession.instanceNew=エンドãƒã‚¤ãƒ³ãƒˆã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ç™»éŒ²ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +wsSession.invalidHandlerTypePong=pongメッセージãƒãƒ³ãƒ‰ãƒ©ã¯MessageHandler.Wholeを実装ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +wsSession.messageFailed=WebSocket コãƒã‚¯ã‚·ãƒ§ãƒ³ãŒåˆ‡æ–­ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€å®Œäº†ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’é€ä¿¡ã§ãã¾ã›ã‚“ +wsSession.removeHandlerFailed=セッションã«ç™»éŒ²ã•ã‚Œã¦ã„ãªã„ãŸã‚ãƒãƒ³ãƒ‰ãƒ©ãƒ¼ [{0}] を解除ã§ãã¾ã›ã‚“。 +wsSession.sendCloseFail=セッション [{0}] ã®ã‚¯ãƒ­ãƒ¼ã‚ºãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’リモートエンドãƒã‚¤ãƒ³ãƒˆã«é€ä¿¡ã§ãã¾ã›ã‚“ã§ã—㟠+wsSession.timeout=WebSocket セッション [{0}] タイムアウトãŒåˆ‡ã‚Œã¾ã—㟠+wsSession.timeoutRead=WebSocketセッション[{0}]読ã¿å–りアイドルタイムアウトã®æœŸé™ãŒåˆ‡ã‚Œã¾ã—㟠+wsSession.timeoutWrite=WebSocketセッション[{0}]書ãè¾¼ã¿ã‚¢ã‚¤ãƒ‰ãƒ«ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã®æœŸé™ãŒåˆ‡ã‚Œã¾ã—㟠+wsSession.unknownHandler=èªè­˜ã§ããªã„タイプ [{1}] ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒãƒ³ãƒ‰ãƒ© [{0}] を追加ã§ãã¾ã›ã‚“ +wsSession.unknownHandlerType=èªè­˜ã§ããªã„åž‹[{1}]ã¨ã—ã¦ãƒ©ãƒƒãƒ—ã•ã‚ŒãŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒãƒ³ãƒ‰ãƒ©[{0}]を追加ã§ãã¾ã›ã‚“ + +wsWebSocketContainer.asynchronousSocketChannelFail=サーãƒãƒ¼ã¸ã®æŽ¥ç¶šã‚’開始ã§ãã¾ã›ã‚“ +wsWebSocketContainer.connect.entry=タイプ [{0}] ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’[{1}]ã«æŽ¥ç¶šã—ã¦ã„ã¾ã™ +wsWebSocketContainer.connect.write=ローカルアドレス [{2}] ã‹ã‚‰ [{1}] ã®åˆ¶é™ã§ [{0}] ã‹ã‚‰å§‹ã¾ã‚‹ãƒãƒƒãƒ•ã‚¡ã‹ã‚‰HTTPアップグレードリクエストを書ã込んã§ã„ã¾ã™ +wsWebSocketContainer.defaultConfiguratorFail=既定ã®ã‚³ãƒ³ãƒ•ã‚£ã‚°ãƒ¬ãƒ¼ã‚¿ç”Ÿæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +wsWebSocketContainer.failedAuthentication=HTTP応答コード [{0}] を処ç†ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚èªè¨¼ãƒ˜ãƒƒãƒ€ãƒ¼ãŒã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦å—ã‘入れられã¾ã›ã‚“ã§ã—ãŸã€‚ +wsWebSocketContainer.httpRequestFailed=WebSocket接続を開始ã™ã‚‹HTTPリクエストãŒå¤±æ•—ã—ã¾ã—ãŸã€‚ +wsWebSocketContainer.invalidExtensionParameters=サーãƒãƒ¼ã¯ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã®è§£é‡ˆã§ããªã„拡張パラメーターã§å¿œç­”ã—ã¾ã—㟠+wsWebSocketContainer.invalidHeader=ヘッダーåã¨å€¤ã®åŒºåˆ‡ã‚Šæ–‡å­—(コロン)ãŒãªã„ HTTP ヘッダー [{0}] ã¯è§£é‡ˆã§ããªã„ãŸã‚無視ã—ã¾ã™ã€‚ +wsWebSocketContainer.invalidStatus=サーãƒãƒ¼[{0}]ã‹ã‚‰ã®HTTPレスãƒãƒ³ã‚¹ãŒWebSocketã¸ã®HTTPアップグレードを許å¯ã—ã¾ã›ã‚“ã§ã—ãŸã€‚ +wsWebSocketContainer.invalidSubProtocol=WebSocketサーãƒãƒ¼ã¯ã€Sec-WebSocket-Protocolヘッダーã«è¤‡æ•°ã®å€¤ã‚’è¿”ã—ã¾ã—ãŸã€‚ +wsWebSocketContainer.maxBuffer=ã“ã®å®Ÿè£…ã¯ãƒãƒƒãƒ•ã‚¡ã®æœ€å¤§ã‚µã‚¤ã‚ºã‚’Integer.MAX_VALUEã«åˆ¶é™ã—ã¾ã™ã€‚ +wsWebSocketContainer.missingAnnotation=POJOクラス [{0}] ã¯@ClientEndpointã§ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ã•ã‚Œã¦ã„ãªã„ãŸã‚使用ã§ãã¾ã›ã‚“ +wsWebSocketContainer.missingAuthenticateHeader=HTTP 応答コード [{0}] を処ç†ã§ãã¾ã›ã‚“。応答ヘッダー㫠[{1}] ヘッダーãŒã‚ã‚Šã¾ã›ã‚“。 +wsWebSocketContainer.missingLocationHeader=HTTP レスãƒãƒ³ã‚¹ã‚³ãƒ¼ãƒ‰ [{0}] を処ç†ã§ãã¾ã›ã‚“。レスãƒãƒ³ã‚¹ã« Location ヘッダーãŒã‚ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ +wsWebSocketContainer.pathNoHost=URIã«ãƒ›ã‚¹ãƒˆãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ +wsWebSocketContainer.pathWrongScheme=スキーマ [{0}] ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„るスキーマã¯wsã¨wssã§ã™ +wsWebSocketContainer.proxyConnectFail=設定ã•ã‚ŒãŸãƒ—ロキシ [{0}] ã«æŽ¥ç¶šã§ãã¾ã›ã‚“ã§ã—ãŸã€‚HTTPレスãƒãƒ³ã‚¹ã‚³ãƒ¼ãƒ‰ã¯ [{1}] ã§ã—㟠+wsWebSocketContainer.redirectThreshold=Locationヘッダー [{0}] ã®å¾ªç’°ã‚’検出ã€ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆå›žæ•° [{1}] ãŒä¸Šé™å€¤ [{2}] を超éŽã—ã¾ã—ãŸã€‚ +wsWebSocketContainer.responseFail=WebSocketã¸ã®HTTPアップグレードã¯å¤±æ•—ã—ã¾ã—ãŸãŒã€éƒ¨åˆ†çš„ãªãƒ‡ãƒ¼ã‚¿ãŒå—ä¿¡ã•ã‚ŒãŸå¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ï¼šã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã‚³ãƒ¼ãƒ‰ [{0}]ã€HTTPヘッダー [{1}] +wsWebSocketContainer.sessionCloseFail=ID [{0}] ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã¯æ­£å¸¸ã«åˆ‡æ–­ã—ã¾ã›ã‚“ã§ã—㟠+wsWebSocketContainer.shutdown=Webアプリケーションã¯åœæ­¢ä¸­ã§ã™ +wsWebSocketContainer.sslEngineFail=SSL/TLS 接続ã®ãŸã‚ã® SSL エンジンを作æˆã§ãã¾ã›ã‚“。 +wsWebSocketContainer.unsupportedAuthScheme=HTTPレスãƒãƒ³ã‚¹ã‚³ãƒ¼ãƒ‰ [{0}] を処ç†ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚サãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ãªã„èªè¨¼ã‚¹ã‚­ãƒ¼ãƒ  [{1}] ãŒãƒ¬ã‚¹ãƒãƒ³ã‚¹ã§è¿”ã•ã‚Œã¾ã—㟠diff --git a/java/org/apache/tomcat/websocket/LocalStrings_ko.properties b/java/org/apache/tomcat/websocket/LocalStrings_ko.properties new file mode 100644 index 0000000..a04d404 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_ko.properties @@ -0,0 +1,145 @@ +# 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. + +asyncChannelGroup.createFail=JavaEE 컨테ì´ë„ˆë“¤ê³¼ ê°™ì€ ë³µìž¡í•œ í´ëž˜ìŠ¤ë¡œë” 환경ì—ì„œ 메모리 누수를 방지하기 위해 필수ì ì´ë©° 웹소켓 í´ë¼ì´ì–¸íŠ¸ë“¤ì„ 위한, ì „ìš© AsynchronousChannelGroup를 ìƒì„±í•  수 없습니다. + +asyncChannelWrapperSecure.check.notOk=TLS handshakeê°€ 예기치 ì•Šì€ ìƒíƒœê°’ [{0}]ì„(를) 반환했습니다. +asyncChannelWrapperSecure.check.unwrap=ë°ì´í„°ë¥¼ ì½ëŠ” ë™ì•ˆ, ë°”ì´íŠ¸ë“¤ì´ 출력으로 쓰여졌습니다. +asyncChannelWrapperSecure.check.wrap=쓰기 ìž‘ì—…ì„ í•˜ëŠ” ë™ì•ˆ, 입력으로부터 ë°”ì´íŠ¸ë“¤ì´ 소비ë˜ì—ˆìŠµë‹ˆë‹¤. +asyncChannelWrapperSecure.closeFail=채ë„ì„ ê¹¨ë—하게 닫지 못했습니다. +asyncChannelWrapperSecure.concurrentRead=ë™ì‹œ ë°œìƒì ì¸ ì½ê¸° 오í¼ë ˆì´ì…˜ë“¤ì€ 허용ë˜ì§€ 않습니다. +asyncChannelWrapperSecure.concurrentWrite=ë™ì‹œì ì¸ 쓰기 오í¼ë ˆì´ì…˜ë“¤ì€ 허용ë˜ì§€ 않습니다. +asyncChannelWrapperSecure.eof=예기치 ì•Šì€ ìŠ¤íŠ¸ë¦¼ì˜ ë +asyncChannelWrapperSecure.statusUnwrap=unwrap() 오í¼ë ˆì´ì…˜ 후ì—, SSLEngineResultì˜ ì˜ˆê¸°ì¹˜ ì•Šì€ ìƒíƒœìž…니다. +asyncChannelWrapperSecure.statusWrap=wrap() 오í¼ë ˆì´ì…˜ 수행 ì´í›„, SSLEngineResultì˜ ì˜ˆê¸°ì¹˜ ì•Šì€ ìƒíƒœìž…니다. +asyncChannelWrapperSecure.tooBig=ê²°ê³¼ [{0}]ì´(ê°€) 너무 커서, 정수로서 í‘œí˜„ë  ìˆ˜ 없습니다. +asyncChannelWrapperSecure.unexpectedHandshakeState=TLS handshake ë„중 예기치 ì•Šì€ ìƒíƒœ [{0}] +asyncChannelWrapperSecure.wrongStateRead=ì½ê¸° 오í¼ë ˆì´ì…˜ì„ 완료하려 ì‹œë„í•  ë•Œì—, ì½ê¸° 진행 ì¤‘ìž„ì„ í‘œì‹œí•˜ëŠ” 플래그가 falseì¸ ê²ƒìœ¼ë¡œ (true였어만 í–ˆìŒì—ë„) ë°í˜€ì¡ŒìŠµë‹ˆë‹¤. +asyncChannelWrapperSecure.wrongStateWrite=쓰기 오í¼ë ˆì´ì…˜ì„ 완료하려 ì‹œë„í•  ë•Œ, 쓰기 진행 중ì´ë¼ëŠ” 플래그가 falseë¡œ (true였어야 함ì—ë„) ë°í˜€ì¡ŒìŠµë‹ˆë‹¤. + +authenticator.nullPassword=ì¸ì¦ì— 사용하기 위한 비밀번호가 제공ë˜ì§€ 않았습니다. +authenticator.nullUserName=ì¸ì¦ì— 사용하기 위한 사용ìžëª…ì´ ì œê³µë˜ì§€ 않았습니다. +authenticator.realmMismatch=사용ìžê°€ 제공한 ì¸ì¦ Realm [{0}](ì´)ê°€ 서버로부터 ë°›ì€ ì¸ì¦ 챌린지 Realm [{1}](와)ê³¼ ì¼ì¹˜í•˜ì§€ 않습니다. + +backgroundProcessManager.processFailed=백그ë¼ìš´ë“œ 프로세스가 실패했습니다. + +caseInsensitiveKeyMap.nullKey=ë„ì¸ í‚¤ë“¤ì€ í—ˆìš©ë˜ì§€ 않습니다. + +clientEndpointHolder.instanceCreationFailed=WebSocketEndpoint를 ìƒì„±í•˜ì§€ 못했습니다. +clientEndpointHolder.instanceRegistrationFailed=InstanceManager를 통해 Endpoint를 등ë¡í•˜ì§€ 못했습니다. + +futureToSendHandler.timeout=[{0}] [{1}]ì´(ê°€) 완료ë˜ê¸°ë¥¼ 기다린 후, ìž‘ì—… 제한 ì‹œê°„ì„ ì´ˆê³¼í–ˆìŠµë‹ˆë‹¤. + +perMessageDeflate.alreadyClosed=해당 transformerê°€ ì´ë¯¸ 닫혔으므로 ë”ì´ìƒ ì‚¬ìš©ë  ìˆ˜ 없습니다. +perMessageDeflate.deflateFailed=ì••ì¶•ëœ ì›¹ì†Œì¼“ í”„ë ˆìž„ì˜ ì••ì¶•ì„ í’€ì§€ 못했습니다. +perMessageDeflate.duplicateParameter=[{0}] 확장 파ë¼ë¯¸í„°ê°€ 중복 ì •ì˜ë˜ì–´ 있습니다. +perMessageDeflate.invalidState=유효하지 ì•Šì€ ìƒíƒœ +perMessageDeflate.invalidWindowSize=í¬ê¸°ê°€ [{1}]ì¸ ìœ íš¨í•˜ì§€ ì•Šì€ ìœˆë„ìš°ë“¤ì´ [{0}]ì„ ìœ„í•´ 지정ë˜ì—ˆìŠµë‹ˆë‹¤. 유효한 ê°’ë“¤ì˜ ë²”ìœ„ëŠ” 8ì—ì„œ 15ê¹Œì§€ì˜ ëª¨ë“  숫ìžë“¤ìž…니다. +perMessageDeflate.unknownParameter=ì•Œ 수 없는 확장 파ë¼ë¯¸í„° [{0}]ì€(는), ì •ì˜ë˜ì§€ 않았습니다. + +util.invalidMessageHandler=ì œê³µëœ ë©”ì‹œì§€ í•¸ë“¤ëŸ¬ì— onMessage(Object) 메소드가 없습니다. +util.invalidType=[{0}] ê°’ì„ íƒ€ìž… [{1}](으)ë¡œ ê°•ì œ 변환시킬 수 없습니다. 해당 íƒ€ìž…ì€ ì§€ì›ë˜ì§€ 않습니다. +util.notToken=허용ë˜ì§€ 않는 확장 파ë¼ë¯¸í„°ê°€ 지정ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ë¦„: [{0}], ê°’: [{1}]. +util.unknownDecoderType=해당 ë””ì½”ë” íƒ€ìž… [{0}]ì€(는) ì¸ì‹ë˜ì§€ 않습니다. + +wsFrame.alreadyResumed=메시지 ìˆ˜ì‹ ì´ ì´ë¯¸ 재개ë˜ì—ˆìŠµë‹ˆë‹¤. +wsFrame.alreadySuspended=메시지 ìˆ˜ì‹ ì´ ì´ë¯¸ ì¼ì‹œ 정지ë˜ì—ˆìŠµë‹ˆë‹¤. +wsFrame.bufferTooSmall=비ë™ê¸° 메시지를 지ì›í•  수 없습니다. 버í¼ê°€ 너무 작습니다. ë²„í¼ í¬ê¸°: [{0}], 메시지 í¬ê¸°: [{1}] +wsFrame.byteToLongFail=너무 ë§Žì€ ë°”ì´íŠ¸ë“¤([{0}])ì´ ì œê³µë˜ì–´, long으로 ë³€í™˜ë  ìˆ˜ 없었습니다. +wsFrame.closed=Control í”„ë ˆìž„ì„ ë‹«ì€ ì´í›„ì— ìƒˆë¡œìš´ í”„ë ˆìž„ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. +wsFrame.controlFragmented=단편화ëœ(fragmented) Control í”„ë ˆìž„ì„ ë°›ì•˜ì§€ë§Œ, Control í”„ë ˆìž„ì€ ë‹¨íŽ¸í™”ë  ìˆ˜ 없습니다. +wsFrame.controlNoFin=fin ë¹„íŠ¸ì…‹ì„ í¬í•¨í•˜ì§€ ì•Šì€ control í”„ë ˆìž„ì´ ì „ì†¡ë˜ì—ˆìŠµë‹ˆë‹¤. Control í”„ë ˆìž„ë“¤ì— continuation í”„ë ˆìž„ë“¤ì´ ì‚¬ìš©ë˜ëŠ” ê²ƒì´ í—ˆìš©ë˜ì§€ 않습니다. +wsFrame.controlPayloadTooBig=Control 프레임ì´, í¬ê¸°ê°€ [{0}]ì¸ payload와 함께 전송ë˜ì—ˆëŠ”ë°, ì´ëŠ” 최대 í—ˆìš©ì¹˜ì¸ 125ë°”ì´íŠ¸ë¥¼ 초과합니다. +wsFrame.illegalReadState=예기치 ì•Šì€ ì½ê¸° ìƒíƒœ: [{0}] +wsFrame.invalidOpCode=ì¸ì‹ë˜ì§€ 않는 opCode [{0}]와(ê³¼) 함께, 웹소켓 í”„ë ˆìž„ì´ ì „ì†¡ë˜ì—ˆìŠµë‹ˆë‹¤. +wsFrame.invalidUtf8=웹소켓 í…스트 í”„ë ˆìž„ì„ ë°›ì•˜ëŠ”ë°, 유효하지 ì•Šì€ ë°”ì´íŠ¸ 시퀀스를 í¬í•¨í•˜ê³  있기 때문ì—, UTF-8ë¡œ ë””ì½”ë”©ë  ìˆ˜ 없었습니다. +wsFrame.invalidUtf8Close=웹소켓 닫기 í”„ë ˆìž„ì„ ì ‘ìˆ˜í•˜ì˜€ëŠ”ë°, 닫기 사유는 유효하지 ì•Šì€ UTF-8 ë°”ì´íŠ¸ ì‹œí€€ìŠ¤ë“¤ì„ í¬í•¨í–ˆë‹¤ëŠ” 것입니다. +wsFrame.ioeTriggeredClose=ë³µêµ¬ë  ìˆ˜ 없는 IOExceptionì´ ë°œìƒí•˜ì—¬ ì—°ê²°ì´ ë‹«í˜”ìŠµë‹ˆë‹¤. +wsFrame.messageTooBig=메시지가 [{0}] ë°”ì´íŠ¸ì˜ 길ì´ë¡œ ë˜ì–´ 있으나, MessageHandler는 [{1}] ë°”ì´íŠ¸ì˜ ì œí•œê°’ì„ ê°€ì§€ê³  있습니다. +wsFrame.noContinuation=Continuation í”„ë ˆìž„ì´ ìš”êµ¬ë  ë•Œì—, 새로운 메시지가 시작ë˜ì—ˆìŠµë‹ˆë‹¤. +wsFrame.notMasked=í´ë¼ì´ì–¸íŠ¸ í”„ë ˆìž„ì´ ë§ˆìŠ¤í¬ ë˜ì–´ 있지 않습니다. 모든 í´ë¼ì´ì–¸íŠ¸ í”„ë ˆìž„ë“¤ì€ ë°˜ë“œì‹œ ë§ˆìŠ¤í¬ ë˜ì–´ì•¼ 합니다. +wsFrame.oneByteCloseCode=í´ë¼ì´ì–¸íŠ¸ê°€ ë‹¨ì¼ ë°”ì´íŠ¸ì˜ payload를 가진 닫기 í”„ë ˆìž„ì„ ë³´ëƒˆëŠ”ë°, ì´ëŠ” 유효하지 않습니다. +wsFrame.partialHeaderComplete=웹소켓 í”„ë ˆìž„ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. fin [{0}], rsv [{1}], OpCode [{2}], payload ê¸¸ì´ [{3}] +wsFrame.payloadMsbInvalid=유효하지 ì•Šì€ ì›¹ì†Œì¼“ í”„ë ˆìž„ì´ ì ‘ìˆ˜ë˜ì—ˆìŠµë‹ˆë‹¤ - 64 ë¹„íŠ¸ì˜ payloadì—, 허용ë˜ì§€ 않는 최ìƒìœ„ 비트가 설정ë˜ì—ˆìŠµë‹ˆë‹¤. +wsFrame.readFailed=비ë™ê¸° í´ë¼ì´ì–¸íŠ¸ì˜ ì½ê¸° 실패 +wsFrame.sessionClosed=해당 ì„¸ì…˜ì´ ì´ë¯¸ 닫혔기 때문ì—, í´ë¼ì´ì–¸íŠ¸ ë°ì´í„°ê°€ ì²˜ë¦¬ë  ìˆ˜ 없습니다. +wsFrame.suspendRequested=메시지 ìˆ˜ì‹ ì˜ ì¼ì‹œ 정지가 ì´ë¯¸ 요청ë˜ì—ˆìŠµë‹ˆë‹¤. +wsFrame.textMessageTooBig=ë””ì½”ë“œëœ í…스트 메시지가 출력 버í¼ì— 비해 너무 í¬ë©°, 해당 엔드í¬ì¸íŠ¸ëŠ” partial ë©”ì‹œì§€ë“¤ì„ ì§€ì›í•˜ì§€ 않습니다. +wsFrame.wrongRsv=í´ë¼ì´ì–¸íŠ¸ 프레임ì´, opCode [{1}]ì„(를) í¬í•¨í•œ 메시지를 위해, reserved ë¹„íŠ¸ë“¤ì„ [{0}](으)ë¡œ 설정했는ë°, ì´ëŠ” ì´ ì—”ë“œí¬ì¸íŠ¸ì— ì˜í•´ 지ì›ë˜ì§€ 않습니다. + +wsFrameClient.ioe=서버가 전송한 ë°ì´í„°ë¥¼ ì½ëŠ” 중 실패 + +wsHandshakeRequest.invalidUri=문ìžì—´ [{0}]ì€(는) 유효한 URI를 구성하는 ë° ì‚¬ìš©ë  ìˆ˜ 없습니다. +wsHandshakeRequest.unknownScheme=ìš”ì²­ì˜ ìŠ¤í‚´ [{0}]ì´(ê°€) ì¸ì‹ë˜ì§€ 않는 스킴입니다. + +wsRemoteEndpoint.acquireTimeout=ì§€ì •ëœ ì œí•œ 시간 ë‚´ì—, 현재 메시지가 완전히 전송ë˜ì§€ 않았습니다. +wsRemoteEndpoint.changeType=단편화ëœ(fragmented) 메시지를 전송할 ë•Œ, 모든 fragmentë“¤ì€ ë°˜ë“œì‹œ ë™ì¼í•œ 타입ì´ì–´ì•¼ 합니다. +wsRemoteEndpoint.closed=웹소켓 ì„¸ì…˜ì´ ì´ë¯¸ 닫혔기 때문ì—, 메시지가 전달ë˜ì§€ ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. +wsRemoteEndpoint.closedDuringMessage=웹소켓 ì„¸ì…˜ì´ ì´ë¯¸ 닫혔기 때문ì—, ë©”ì‹œì§€ì˜ ë‚˜ë¨¸ì§€ ë¶€ë¶„ì€ ì „ë‹¬ë˜ì§€ ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. +wsRemoteEndpoint.closedOutputStream=OutputStreamì´ ì´ë¯¸ 닫혀 있으므로, ì´ ë©”ì†Œë“œëŠ” í˜¸ì¶œë  ìˆ˜ 없습니다. +wsRemoteEndpoint.closedWriter=Writerê°€ ì´ë¯¸ 닫혔기 때문ì—, ì´ ë©”ì†Œë“œëŠ” í˜¸ì¶œë  ìˆ˜ 없습니다. +wsRemoteEndpoint.encoderDestoryFailed=[{0}] íƒ€ìž…ì˜ ì¸ì½”ë”를 소멸시키지 못했습니다. +wsRemoteEndpoint.flushOnCloseFailed=ì„¸ì…˜ì´ ì´ë¯¸ ì¢…ë£Œëœ ì´í›„ì—ë„, ë©”ì‹œì§€ë“¤ì´ ë°°ì¹˜(batch)ì— í¬í•¨ë˜ì–´ 있습니다. ë°°ì¹˜ì— ë‚¨ì•„ìžˆëŠ” ë©”ì‹œì§€ë“¤ì„ ë°°ì¶œí•  수 없습니다. +wsRemoteEndpoint.invalidEncoder=ì§€ì •ëœ íƒ€ìž… [{0}]ì˜ Encoderì˜ ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•  수 없었습니다. +wsRemoteEndpoint.noEncoder=í´ëž˜ìŠ¤ [{0}]ì˜ ê°ì²´ë¥¼ 위한 ì¸ì½”ë”ê°€ 지정ë˜ì§€ 않았습니다. +wsRemoteEndpoint.nullData=유효하지 ì•Šì€ ë„ ë°ì´í„° 아규먼트 +wsRemoteEndpoint.nullHandler=유효하지 ì•Šì€ ë„ í•¸ë“¤ëŸ¬ 아규먼트 +wsRemoteEndpoint.sendInterrupt=현재 쓰레드가, blocking ì „ì†¡ì´ ì™„ë£Œë˜ê¸°ë¥¼ ê¸°ë‹¤ë¦¬ë˜ ì¤‘ 중단ë˜ì—ˆìŠµë‹ˆë‹¤. +wsRemoteEndpoint.tooMuchData=Ping ë˜ëŠ” pongì€ 125 ë°”ì´íŠ¸ë¥¼ 초과한 ë°ì´í„°ë¥¼ 보낼 수 없습니다. +wsRemoteEndpoint.writeTimeout=Blocking 쓰기가 제한 시간 초과ë˜ì—ˆìŠµë‹ˆë‹¤. +wsRemoteEndpoint.wrongState=í˜¸ì¶œëœ ë©”ì†Œë“œì— ëŒ€í•´, ì›ê²© 엔드í¬ì¸íŠ¸ê°€ 유효하지 ì•Šì€ ìƒíƒœ [{0}]ì— ìžˆìŠµë‹ˆë‹¤. + +wsSession.closed=웹소켓 세션 [{0}]ì€(는) ì´ë¯¸ 닫혔으며, (close()를 제외한) ì–´ë–¤ ë©”ì†Œë“œë„ ë‹«ížŒ ì„¸ì…˜ì— í˜¸ì¶œë˜ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +wsSession.created=웹소켓 세션 [{0}]ì„(를) ìƒì„±í–ˆìŠµë‹ˆë‹¤. +wsSession.doClose=웹소켓 세션 [{0}]ì„(를) 닫습니다. +wsSession.duplicateHandlerBinary=ë°”ì´ë„ˆë¦¬ 메시지 핸들러가 ì´ë¯¸ 설정ë˜ì—ˆìŠµë‹ˆë‹¤. +wsSession.duplicateHandlerPong=Pong 메시지 핸들러가 ì´ë¯¸ 설정ë˜ì—ˆìŠµë‹ˆë‹¤. +wsSession.duplicateHandlerText=í…스트 메시지 핸들러가 ì´ë¯¸ 설정ë˜ì–´ 있습니다. +wsSession.flushFailOnClose=ì„¸ì…˜ì´ ë‹«íž ë•Œ, ë°°ì¹˜ì— ìŒ“ì¸ ë©”ì‹œì§€ë“¤ì„ ë°°ì¶œí•˜ì§€ 못했습니다. +wsSession.instanceCreateFailed=Endpoint 개체 ìƒì„± 실패 +wsSession.instanceNew=엔드í¬ì¸íŠ¸ ì¸ìŠ¤í„´ìŠ¤ ë“±ë¡ ì‹¤íŒ¨ +wsSession.invalidHandlerTypePong=Pong 메시지 핸들러는 반드시 MessageHandler.Wholeì„ êµ¬í˜„í•´ì•¼ 합니다. +wsSession.messageFailed=웹소켓 ì—°ê²°ì´ ì´ë¯¸ 닫혔기 때문ì—, 완전한 메시지를 쓸 수 없습니다. +wsSession.removeHandlerFailed=핸들러 [{0}]ì´(ê°€), ì´ ì„¸ì…˜ê³¼ 함께 등ë¡ë˜ì§€ 않았었기 때문ì—, ì œê±°ë  ìˆ˜ 없습니다. +wsSession.sendCloseFail=세션 [{0}]ì„(를) 위해, ì›ê²© 엔드í¬ì¸íŠ¸ë¡œ 세션 닫기 메시지를 보내지 못했습니다. +wsSession.timeout=웹소켓 세션 [{0}]ì´(ê°€) 제한 시간 초과로 만료ë˜ì—ˆìŠµë‹ˆë‹¤. +wsSession.timeoutRead=웹소켓 세션 [{0}]ì´(ê°€) ì½ê¸° 유휴 시간 타임아웃으로 만료ë˜ì—ˆìŠµë‹ˆë‹¤. +wsSession.timeoutWrite=웹소켓 세션 [{0}]ì´(ê°€) 쓰기 유휴 시간 타임아웃으로 만료ë˜ì—ˆìŠµë‹ˆë‹¤. +wsSession.unknownHandler=ì¸ì‹ë˜ì§€ 않는 타입 [{1}]ì„(를) 위한 것ì´ì—ˆê¸°ì—, 해당 메시지 핸들러 [{0}]ì„(를) 추가할 수 없습니다. +wsSession.unknownHandlerType=메시지 핸들러 [{0}]ì´(ê°€) ì¸ì‹ë˜ì§€ 않는 타입 [{1}](으)ë¡œ wrap ë˜ì–´ 있어, 추가할 수 없습니다. + +wsWebSocketContainer.asynchronousSocketChannelFail=ì„œë²„ì— ëŒ€í•œ ì—°ê²°ì„ ì—´ 수 없습니다. +wsWebSocketContainer.connect.entry=íƒ€ìž…ì´ [{0}]ì¸ ì—”ë“œí¬ì¸íŠ¸ ì¸ìŠ¤í„´ìŠ¤ë¥¼ [{1}]ì— ì—°ê²°í•©ë‹ˆë‹¤. +wsWebSocketContainer.connect.write=로컬 주소 [{2}](으)로부터, 시작 위치 [{0}], 최대 ê¸¸ì´ [{1}]ì˜ ë°ì´í„°ë¥¼ 버í¼ì— ì”니다. +wsWebSocketContainer.defaultConfiguratorFail=기본 Configurator를 ìƒì„±í•˜ì§€ 못했습니다. +wsWebSocketContainer.failedAuthentication=HTTP ì‘답 코드 [{0}]ì„(를) 처리하지 못했습니다. ì¸ì¦ í—¤ë”ê°€ ì„œë²„ì— ì˜í•´ 받아들여지지 않았습니다. +wsWebSocketContainer.httpRequestFailed=웹소켓 ì—°ê²°ì„ ì´ˆê¸°í™”í•˜ê¸° 위한 HTTP ìš”ì²­ì´ ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. +wsWebSocketContainer.invalidExtensionParameters=서버가, í´ë¼ì´ì–¸íŠ¸ê°€ 지ì›í•  수 없는 확장 파ë¼ë¯¸í„°ë“¤ê³¼ 함께 ì‘답했습니다. +wsWebSocketContainer.invalidHeader=[{0}] ë‚´ì—ì„œ, í—¤ë” ì´ë¦„ê³¼ í—¤ë” ê°’ì„ êµ¬ë¶„í•˜ê¸° 위한 콜론('':'')ì´ ì¡´ìž¬í•˜ì§€ 않기ì—, HTTP í—¤ë”를 파싱할 수 없습니다. 해당 í—¤ë”를 건너뛰었습니다. +wsWebSocketContainer.invalidStatus=서버 [{0}](으)ë¡œë¶€í„°ì˜ HTTP ì‘답ì€, 웹소켓으로 HTTP 업그레ì´ë“œë¥¼ 허용하지 않았습니다. +wsWebSocketContainer.invalidSubProtocol=웹소켓 서버가, 해당 Sec-WebSocket-Protocol í—¤ë”를 위해 여러 ê°’ë“¤ì„ ë°˜í™˜í–ˆìŠµë‹ˆë‹¤. +wsWebSocketContainer.maxBuffer=ì´ êµ¬í˜„ì€ ë²„í¼ì˜ 최대 í¬ê¸°ë¥¼ Integer.MAX_VALUEë¡œ 제한합니다. +wsWebSocketContainer.missingAnnotation=@ClientEndpointì— ì˜í•´ annotateë˜ì§€ 않았기ì—, POJO í´ëž˜ìŠ¤ [{0}]ì„(를) 사용할 수 없습니다. +wsWebSocketContainer.missingAuthenticateHeader=HTTP ì‘답 코드 [{0}]ì„(를) 처리하지 못했습니다. ì‘답 í—¤ë” [{1}] ê°€ 없습니다. +wsWebSocketContainer.missingLocationHeader=HTTP ì‘답 코드 [{0}]ì„(를) 처리하지 못했습니다. ì‘ë‹µì— Location í—¤ë”ê°€ 없습니다. +wsWebSocketContainer.pathNoHost=URI ë‚´ì— í˜¸ìŠ¤íŠ¸ê°€ 지정ë˜ì§€ 않았습니다. +wsWebSocketContainer.pathWrongScheme=스킴 [{0}]ì€(는) 지ì›ë˜ì§€ 않습니다. 지ì›ë˜ëŠ” ìŠ¤í‚´ë“¤ì€ ws와 wss입니다. +wsWebSocketContainer.proxyConnectFail=ì„¤ì •ëœ í”„ë¡ì‹œ [{0}](으)ë¡œ 연결하지 못했습니다. HTTP ì‘답 코드는 [{1}]ì´ì—ˆìŠµë‹ˆë‹¤. +wsWebSocketContainer.redirectThreshold=순환 Location í—¤ë” [{0}]ì´(ê°€) íƒì§€ë˜ì—ˆê³ , 최대 redirect íšŒìˆ˜ì— ë„달했습니다. 최대 [{2}]회 중 [{1}]회. +wsWebSocketContainer.responseFail=ì›¹ì†Œì¼“ìœ¼ë¡œì˜ HTTP 업그레ì´ë“œê°€ 실패했습니만, ì¼ë¶€ ë°ì´í„°ê°€ 접수ë˜ì—ˆì„ 수 있습니다: ìƒíƒœ 코드 [{0}], HTTP í—¤ë”들 [{1}] +wsWebSocketContainer.sessionCloseFail=IDê°€ [{0}]ì¸ ì„¸ì…˜ì´ ê¹¨ë—하게 닫히지 않았습니다. +wsWebSocketContainer.shutdown=웹 애플리케ì´ì…˜ì´ 중지ë˜ê³  있습니다. +wsWebSocketContainer.sslEngineFail=SSL/TLS ì—°ê²°ë“¤ì„ ì§€ì›í•˜ëŠ” SSLEngineì„ ìƒì„±í•  수 없습니다. +wsWebSocketContainer.unsupportedAuthScheme=HTTP ì‘답 코드 [{0}]ì„(를) 처리하지 못했습니다. 지ì›ë˜ì§€ 않는 ì¸ì¦ 스킴 [{1}]ì´(ê°€) ì‘답ì—ì„œ 반환ë˜ì—ˆìŠµë‹ˆë‹¤. diff --git a/java/org/apache/tomcat/websocket/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/websocket/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..6af6a27 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_pt_BR.properties @@ -0,0 +1,24 @@ +# 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. + +wsFrame.closed=Novo pacote recebido depois de um pacote de controle de fechamento +wsFrame.wrongRsv=O pacote do cliente configurou os bits reservados para [{0}] numa mensagem com opCode [{1}] que não é suportada pelo endpoint + +wsHandshakeRequest.invalidUri=A string [{0}] não pode ser usa + +wsRemoteEndpoint.closedDuringMessage=O aviso da mensagem não será enviado porquê a sessão do WebSocket foi fechada +wsRemoteEndpoint.wrongState=O endpoint remoto estava em estado [{0}] que é um estado inválido para o método chamado + +wsWebSocketContainer.sessionCloseFail=Sessão com ID [{0}] não fechou corretamente diff --git a/java/org/apache/tomcat/websocket/LocalStrings_ru.properties b/java/org/apache/tomcat/websocket/LocalStrings_ru.properties new file mode 100644 index 0000000..e27c7a6 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_ru.properties @@ -0,0 +1,40 @@ +# 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. + +backgroundProcessManager.processFailed=Ð’ фоновом процеÑÑе произошла ошибка + +caseInsensitiveKeyMap.nullKey=ПуÑтое значение ключей запрещено + +perMessageDeflate.invalidState=Ðекорректное ÑоÑтоÑние + +wsFrame.bufferTooSmall=Ðет поддержки аÑинхронных Ñообщений и буфере Ñлишком мал. Размер буфера: [{0}], Размер ÑообщениÑ:[{1}] +wsFrame.byteToLongFail=Слишком много байт ([{0}]) было предоÑтавлено Ð´Ð»Ñ Ð¿Ñ€ÐµÐ¾Ð±Ñ€Ð°Ð·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð² long +wsFrame.messageTooBig=Сообщение было длиной [{0}], но MessageHandler имеет предел равный [{1}] байт + +wsFrameClient.ioe=Ошибка Ñ‡Ñ‚ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… отправленных Ñервером + +wsHandshakeRequest.invalidUri=Строка [{0}] не может быть иÑпользована Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾Ð³Ð¾ URI +wsHandshakeRequest.unknownScheme=Схема [{0}] в запроÑе не раÑпознана + +wsRemoteEndpoint.acquireTimeout=Текущее Ñообщение не было отправлено целиком в указанный тайм-аут +wsRemoteEndpoint.changeType=При отправке фрагментированного ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð²Ñе фрагменты должны быть одного типа +wsRemoteEndpoint.closed=Сообщение не будет отправлено так как WebSocket ÑеÑÑÐ¸Ñ Ð±Ñ‹Ð»Ð° закрыта +wsRemoteEndpoint.closedDuringMessage=ОÑтаток ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð½Ðµ будет отправлен, так как ВебÑокет ÑеÑÑÐ¸Ñ Ð±Ñ‹Ð»Ð° закрыта + +wsSession.closed=WebSocket ÑеÑÑÐ¸Ñ [{0}] была закрыта и никакой метод (кроме как close()) может быть вызван Ð´Ð»Ñ Ð·Ð°ÐºÑ€Ñ‹Ñ‚Ð¾Ð¹ ÑеÑÑии +wsSession.doClose=Закрытие WebSocket ÑеÑÑии [{0}] + +wsWebSocketContainer.asynchronousSocketChannelFail=Ðе возможно открыть Ñоединение Ñ Ñервером +wsWebSocketContainer.sessionCloseFail=CеÑÑÐ¸Ñ Ñ ID [{0}] не была закрыта чиÑто diff --git a/java/org/apache/tomcat/websocket/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/websocket/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..f6202f1 --- /dev/null +++ b/java/org/apache/tomcat/websocket/LocalStrings_zh_CN.properties @@ -0,0 +1,145 @@ +# 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. + +asyncChannelGroup.createFail=无法为WebSocket客户端创建专用的异步通é“组,这是防止å¤æ‚类加载程åºçŽ¯å¢ƒï¼ˆå¦‚JavaEE容器)中内存泄æ¼æ‰€å¿…需的 + +asyncChannelWrapperSecure.check.notOk=TLSæ¡æ‰‹è¿”回æ„外状æ€[{0}] +asyncChannelWrapperSecure.check.unwrap=在读å–期间将字节写入输出 +asyncChannelWrapperSecure.check.wrap=在写入过程中从输入消耗了字节 +asyncChannelWrapperSecure.closeFail=干净的关闭通é“失败 +asyncChannelWrapperSecure.concurrentRead=ä¸å…许并å‘读å–æ“作 +asyncChannelWrapperSecure.concurrentWrite=ä¸å…许并å‘写æ“作 +asyncChannelWrapperSecure.eof=æ„外的æµç»“å°¾ +asyncChannelWrapperSecure.statusUnwrap=unwrap()æ“作åŽSSLEngineResult çš„æ„å¤–çŠ¶æ€ +asyncChannelWrapperSecure.statusWrap=wrap()æ“作åŽSSLEngineResultçš„æ„å¤–çŠ¶æ€ +asyncChannelWrapperSecure.tooBig=结果[{0}]太大,无法表示为整数 +asyncChannelWrapperSecure.unexpectedHandshakeState=TLSæ¡æ‰‹æœŸé—´å‡ºçŽ°æ„外状æ€[{0}] +asyncChannelWrapperSecure.wrongStateRead=å°è¯•å®Œæˆè¯»å–æ“作时,å‘现指示正在进行读å–的标志为false(应该为true) +asyncChannelWrapperSecure.wrongStateWrite=å°è¯•å®Œæˆå†™æ“作时,å‘现指示正在进行写æ“作的标志为false(应该为true) + +authenticator.nullPassword=没有æ供用于身份验è¯çš„å¯†ç  +authenticator.nullUserName=没有æ供用于身份验è¯çš„用户å +authenticator.realmMismatch=用户æ供的身份验è¯é¢†åŸŸ [{0}] 与从æœåŠ¡å™¨ [{1}] 收到的身份验è¯è´¨è¯¢ä¸­çš„领域ä¸åŒ¹é… + +backgroundProcessManager.processFailed=åŽå°è¿›ç¨‹å¤±è´¥ + +caseInsensitiveKeyMap.nullKey=ä¸å…许 Key 是 Null + +clientEndpointHolder.instanceCreationFailed=未能创建WebSocketEndpoint +clientEndpointHolder.instanceRegistrationFailed=无法å‘InstanceManager注册Endpoint实例 + +futureToSendHandler.timeout=等待[{0}][{1}]完æˆåŽæ“作超时。 + +perMessageDeflate.alreadyClosed=转æ¢å™¨å·²ç»å…³é—­ä¸”å¯èƒ½æ°¸è¿œä¸ä¼šä½¿ç”¨ +perMessageDeflate.deflateFailed=无法压缩这个WebSocket压缩结构 +perMessageDeflate.duplicateParameter=é‡å¤å®šä¹‰çš„扩展å‚æ•°[{0}] +perMessageDeflate.invalidState=æ— æ•ˆçŠ¶æ€ +perMessageDeflate.invalidWindowSize=为[{0}]指定了[{1}]大å°çš„无效窗å£ã€‚ 有效值是从8到15(包括8å’Œ15)的整数。 +perMessageDeflate.unknownParameter=定义了未知的扩展å‚æ•°[{0}] + +util.invalidMessageHandler=æ供的消æ¯å¤„ç†ç¨‹åºæ²¡æœ‰onMessage(对象)方法。 +util.invalidType=无法强制值[{0}]转为[{1}]类型(ä¸æ”¯æŒæ¬¡ç±»åž‹ï¼‰ã€‚ +util.notToken=一个éžæ³•çš„扩展å‚数被指定为å称[{0}]和值[{0}] +util.unknownDecoderType=无法识别该解ç å™¨ç±»åž‹[{0}] + +wsFrame.alreadyResumed=å·²æ¢å¤é‚®ä»¶æŽ¥æ”¶ +wsFrame.alreadySuspended=消æ¯æŽ¥æ”¶å·²æŒ‚起。 +wsFrame.bufferTooSmall=没有异步消æ¯æ”¯æŒï¼Œå¹¶ä¸”缓冲区太å°ã€‚缓冲区大å°ï¼š[{0}],消æ¯å¤§å°ï¼š[{1}] +wsFrame.byteToLongFail=æ供了太多字节([{0}]),转æ¢æˆä¸€ä¸ªé•¿çš„字节。 +wsFrame.closed=在一个关闭的控制帧åŽå—到了一个新的帧. +wsFrame.controlFragmented=接收到分段的控制帧,但控制帧å¯èƒ½ä¸è¢«åˆ†å‰²ã€‚ +wsFrame.controlNoFin=å‘é€ä¸€ä¸ªæ²¡æœ‰è®¾ç½®çš„控制帧。控制帧ä¸å…许使用连续帧。 +wsFrame.controlPayloadTooBig=以大于125字节的最大å…许值的大å°[{0}]的有效载è·å‘é€æŽ§åˆ¶å¸§ã€‚ +wsFrame.illegalReadState=æ„外的读状æ€[{0}] +wsFrame.invalidOpCode=用无法识别的æ“作ç [{0}]å‘é€äº†WebSocket帧 +wsFrame.invalidUtf8=接收到无法解ç ä¸ºUTF-8çš„WebSocket文本帧,因为它包å«æ— æ•ˆçš„字节åºåˆ— +wsFrame.invalidUtf8Close=接收到一个WebSocket关闭帧,其关闭原因包å«æ— æ•ˆçš„UTF-8字节åºåˆ— +wsFrame.ioeTriggeredClose=å‘生ä¸å¯æ¢å¤çš„IOException,因此连接已关闭。 +wsFrame.messageTooBig=消æ¯çš„长度为[{0}]个字节,但消æ¯å¤„ç†ç¨‹åºçš„é™åˆ¶ä¸º[{1}]个字节。 +wsFrame.noContinuation=当需è¦å»¶ç»­å¸§æ—¶å¯åŠ¨äº†æ–°æ¶ˆæ¯ã€‚ +wsFrame.notMasked=客户端帧未被å±è”½ï¼Œä½†å¿…é¡»å±è”½æ‰€æœ‰å®¢æˆ·ç«¯å¸§ +wsFrame.oneByteCloseCode=客户端å‘é€äº†ä¸€ä¸ªåŒ…å«æ— æ•ˆå•å­—节有效负载的关闭帧。 +wsFrame.partialHeaderComplete=接收到WebSocket帧. fin [{0}], rsv [{1}], OpCode [{2}], payload 长度 [{3}] +wsFrame.payloadMsbInvalid=接收到无效的WebSocket帧-éžæ³•è®¾ç½®äº†64ä½æœ‰æ•ˆè´Ÿè½½çš„æœ€é«˜æœ‰æ•ˆä½ +wsFrame.readFailed=异步客户端读å–失败 +wsFrame.sessionClosed=无法处ç†å®¢æˆ·ç«¯æ•°æ®ï¼Œå› ä¸ºä¼šè¯å·²è¢«å…³é—­ +wsFrame.suspendRequested=已请求暂åœæŽ¥æ”¶é‚®ä»¶ã€‚ +wsFrame.textMessageTooBig=解ç çš„文本消æ¯å¯¹äºŽè¾“出缓冲区太大,终结点ä¸æ”¯æŒéƒ¨åˆ†æ¶ˆæ¯ +wsFrame.wrongRsv=对于具有opCode [{1}]的消æ¯ï¼Œå®¢æˆ·ç«¯å¸§å°†ä¿ç•™ä½è®¾ç½®ä¸º[{0}],此端点ä¸æ”¯æŒ + +wsFrameClient.ioe=从å‘é€çš„æœåŠ¡å™¨ä¸Šè¯»å–æ•°æ®å¤±è´¥ + +wsHandshakeRequest.invalidUri=字符串 [{0}] ä¸èƒ½ç”¨æ¥ç»„æˆä¸€ä¸ªæœ‰æ•ˆçš„URI +wsHandshakeRequest.unknownScheme=请求中的计划[{0}]未识别 + +wsRemoteEndpoint.acquireTimeout=当å‰æ¶ˆæ¯æ²¡æœ‰åœ¨æŒ‡å®šçš„超时内完全å‘é€ +wsRemoteEndpoint.changeType=å‘é€åˆ†æ®µæ¶ˆæ¯æ—¶ï¼Œæ‰€æœ‰ç‰‡æ®µå¿…须是相åŒç±»åž‹çš„。 +wsRemoteEndpoint.closed=由于 WebSocket session 已关闭,消æ¯å°†ä¸ä¼šè¢«å‘é€ +wsRemoteEndpoint.closedDuringMessage=因为 WebSocket session 被关闭,消æ¯çš„剩余部分将ä¸ä¼šè¢«é€è¾¾ +wsRemoteEndpoint.closedOutputStream=由于OutputStream已关闭,ä¸åº”该调用此方法。 +wsRemoteEndpoint.closedWriter=此方法ä¸èƒ½è°ƒç”¨ï¼Œå› ä¸ºç¼–写器已关闭。 +wsRemoteEndpoint.encoderDestoryFailed=未能销æ¯[{0}]类型的编ç å™¨ +wsRemoteEndpoint.flushOnCloseFailed=会è¯å…³é—­åŽä»ç„¶å¯ç”¨æ‰¹å¤„ç†æ¶ˆæ¯ã€‚无法刷新剩余的批é‡æ¶ˆæ¯ +wsRemoteEndpoint.invalidEncoder=无法实例化类型为[{0}]的指定编ç å™¨ +wsRemoteEndpoint.noEncoder=没有为类 [{0}] 的对象指定编ç å™¨ +wsRemoteEndpoint.nullData=无效空的data å‚æ•° +wsRemoteEndpoint.nullHandler=无效的空处ç†ç¨‹åºå‚æ•° +wsRemoteEndpoint.sendInterrupt=当å‰çº¿ç¨‹åœ¨ç­‰å¾…阻塞å‘é€å®Œæˆæ—¶è¢«ä¸­æ–­ +wsRemoteEndpoint.tooMuchData=ping或pongä¸åº”该å‘é€è¶…过125字节 +wsRemoteEndpoint.writeTimeout=阻塞写入超时 +wsRemoteEndpoint.wrongState=远程 endpoint 处于 [{0}] 状æ€ï¼Œæ˜¯è¢«è°ƒç”¨æ–¹æ³•çš„æ— æ•ˆçŠ¶æ€ + +wsSession.closed=WebSocket会è¯[{0}]已关闭,并且在关闭的会è¯ä¸Šä¸èƒ½è°ƒç”¨ä»»ä½•æ–¹æ³•ï¼ˆé™¤äº†close()) +wsSession.created=创建WebSocket session [{0}]。 +wsSession.doClose=关闭 WebSocket session [{0}] +wsSession.duplicateHandlerBinary=å·²é…置二进制消æ¯å¤„ç†ç¨‹åº +wsSession.duplicateHandlerPong=å·²ç»é…置了pong消æ¯å¤„ç†å™¨ +wsSession.duplicateHandlerText=å·²é…置文本消æ¯å¤„ç†å™¨ +wsSession.flushFailOnClose=会è¯å…³é—­æ—¶åˆ·æ–°æ‰¹å¤„ç†é‚®ä»¶å¤±è´¥ +wsSession.instanceCreateFailed=Endpoint实例创建失败 +wsSession.instanceNew=endpoint 实例注册失败 +wsSession.invalidHandlerTypePong=一个pong消æ¯å¤„ç†ç¨‹åºå¿…须实现MessageHandler.Whole +wsSession.messageFailed=无法写入完整消æ¯ï¼Œå› ä¸ºWebSocket连接已关闭 +wsSession.removeHandlerFailed=无法删除处ç†ç¨‹åº[{0}],因为它未在此会è¯ä¸­æ³¨å†Œ +wsSession.sendCloseFail=给远程端点å‘é€å…³é—­æ¶ˆæ¯å¤±è´¥ï¼Œsession:[{0}] +wsSession.timeout=WebSocket会è¯[{0}]超时已过期 +wsSession.timeoutRead=WebSocket会è¯[{0}]读å–空闲超时过期 +wsSession.timeoutWrite=WebSocket会è¯[{0}]写入空闲超时过期 +wsSession.unknownHandler=无法添加消æ¯å¤„ç†ç¨‹åº[{0}],因为它是针对无法识别的类型[{1}] +wsSession.unknownHandlerType=无法添加消æ¯å¤„ç†ç¨‹åº[{0}],因为它被包装为无法识别的类型[{1}]。 + +wsWebSocketContainer.asynchronousSocketChannelFail=无法打开与æœåŠ¡å™¨çš„连接 +wsWebSocketContainer.connect.entry=连接[{0}]类型的终端实例至[{1}] +wsWebSocketContainer.connect.write=从本地地å€[{2}]以[{1}]çš„é™åˆ¶ä»Ž[{0}]开始从缓冲区写入HTTPå‡çº§è¯·æ±‚ +wsWebSocketContainer.defaultConfiguratorFail=无法创建默认é…置程åºã€‚ +wsWebSocketContainer.failedAuthentication=无法处ç†httpå“应代ç [{0}]。æœåŠ¡å™¨ä¸æŽ¥å—身份验è¯å¤´ã€‚ +wsWebSocketContainer.httpRequestFailed=å‘起与 [{0}] çš„ WebSocket 连接的 HTTP 请求失败 +wsWebSocketContainer.invalidExtensionParameters=æœåŠ¡å™¨ç”¨å®¢æˆ·ç«¯æ— æ³•æ”¯æŒçš„扩展å‚æ•°å“应 +wsWebSocketContainer.invalidHeader=无法分æžHTTP头,因为在[{0}]中没有冒å·æ¥åˆ†éš”头å称和头值。已跳过标题。 +wsWebSocketContainer.invalidStatus=æ¥è‡ªæœåŠ¡å™¨[{0}]çš„HTTPå“应ä¸å…许HTTPå‡çº§åˆ°WebSocket +wsWebSocketContainer.invalidSubProtocol=WebSocketæœåŠ¡å™¨ä¸ºSec-WebSocket-Protocol标头返回了多个值 +wsWebSocketContainer.maxBuffer=此实现将缓冲区的最大大å°é™åˆ¶ä¸ºInteger.MAX_VALUE +wsWebSocketContainer.missingAnnotation=无法使用POJOç±»[{0}],因为它未添加注解@ClientEndpoint +wsWebSocketContainer.missingAuthenticateHeader=无法处ç†HTTPå“应代ç [{0}]。 缺少 [{1}] 标头作为å“应 +wsWebSocketContainer.missingLocationHeader=处ç†HTTPå“åº”ç  [{0}] 失败。å“应头缺少Location +wsWebSocketContainer.pathNoHost=URI中未指定主机 +wsWebSocketContainer.pathWrongScheme=ä¸æ”¯æŒæ–¹æ¡ˆ[{0}]。支æŒçš„方案是wså’Œwss +wsWebSocketContainer.proxyConnectFail=失败连接到已é…ç½®çš„ä»£ç† [{0}]。HTTP å“应ç æ˜¯ [{1}] +wsWebSocketContainer.redirectThreshold=循环ä½ç½®å¤´[{0}]检测到/达到最大é‡å®šå‘æ•°[{1}]的最大值[{2}] +wsWebSocketContainer.responseFail=HTTPå‡çº§WebSocket失败,但是部分数æ®å·²è¢«æŽ¥æ”¶ï¼šçŠ¶æ€ç ï¼š[{0}],HTTP请求头[{1}] +wsWebSocketContainer.sessionCloseFail=ID 为 [{0}] çš„session 没有彻底关闭 +wsWebSocketContainer.shutdown=web应用程åºæ­£åœ¨åœæ­¢ +wsWebSocketContainer.sslEngineFail=无法创建SSLEngine以支æŒSSL/TLS连接 +wsWebSocketContainer.unsupportedAuthScheme=HTTPå“应ç å¤„ç†å¤±è´¥[{0}]. Unsupported Authentication 方案 [{1}] 返回到å“应 diff --git a/java/org/apache/tomcat/websocket/MessageHandlerResult.java b/java/org/apache/tomcat/websocket/MessageHandlerResult.java new file mode 100644 index 0000000..f989117 --- /dev/null +++ b/java/org/apache/tomcat/websocket/MessageHandlerResult.java @@ -0,0 +1,41 @@ +/* + * 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.tomcat.websocket; + +import jakarta.websocket.MessageHandler; + +public class MessageHandlerResult { + + private final MessageHandler handler; + private final MessageHandlerResultType type; + + + public MessageHandlerResult(MessageHandler handler, MessageHandlerResultType type) { + this.handler = handler; + this.type = type; + } + + + public MessageHandler getHandler() { + return handler; + } + + + public MessageHandlerResultType getType() { + return type; + } +} diff --git a/java/org/apache/tomcat/websocket/MessageHandlerResultType.java b/java/org/apache/tomcat/websocket/MessageHandlerResultType.java new file mode 100644 index 0000000..a4d857c --- /dev/null +++ b/java/org/apache/tomcat/websocket/MessageHandlerResultType.java @@ -0,0 +1,23 @@ +/* + * 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.tomcat.websocket; + +public enum MessageHandlerResultType { + BINARY, + TEXT, + PONG +} diff --git a/java/org/apache/tomcat/websocket/MessagePart.java b/java/org/apache/tomcat/websocket/MessagePart.java new file mode 100644 index 0000000..f7b5af9 --- /dev/null +++ b/java/org/apache/tomcat/websocket/MessagePart.java @@ -0,0 +1,81 @@ +/* + * 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.tomcat.websocket; + +import java.nio.ByteBuffer; + +import jakarta.websocket.SendHandler; + +class MessagePart { + private final boolean fin; + private final int rsv; + private final byte opCode; + private final ByteBuffer payload; + private final SendHandler intermediateHandler; + private volatile SendHandler endHandler; + private final long blockingWriteTimeoutExpiry; + + MessagePart(boolean fin, int rsv, byte opCode, ByteBuffer payload, SendHandler intermediateHandler, + SendHandler endHandler, long blockingWriteTimeoutExpiry) { + this.fin = fin; + this.rsv = rsv; + this.opCode = opCode; + this.payload = payload; + this.intermediateHandler = intermediateHandler; + this.endHandler = endHandler; + this.blockingWriteTimeoutExpiry = blockingWriteTimeoutExpiry; + } + + + public boolean isFin() { + return fin; + } + + + public int getRsv() { + return rsv; + } + + + public byte getOpCode() { + return opCode; + } + + + public ByteBuffer getPayload() { + return payload; + } + + + public SendHandler getIntermediateHandler() { + return intermediateHandler; + } + + + public SendHandler getEndHandler() { + return endHandler; + } + + public void setEndHandler(SendHandler endHandler) { + this.endHandler = endHandler; + } + + public long getBlockingWriteTimeoutExpiry() { + return blockingWriteTimeoutExpiry; + } +} + diff --git a/java/org/apache/tomcat/websocket/PerMessageDeflate.java b/java/org/apache/tomcat/websocket/PerMessageDeflate.java new file mode 100644 index 0000000..1868b2a --- /dev/null +++ b/java/org/apache/tomcat/websocket/PerMessageDeflate.java @@ -0,0 +1,491 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +import jakarta.websocket.Extension; +import jakarta.websocket.Extension.Parameter; +import jakarta.websocket.SendHandler; + +import org.apache.tomcat.util.res.StringManager; + +public class PerMessageDeflate implements Transformation { + + private static final StringManager sm = StringManager.getManager(PerMessageDeflate.class); + + private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover"; + private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover"; + private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits"; + private static final String CLIENT_MAX_WINDOW_BITS = "client_max_window_bits"; + + private static final int RSV_BITMASK = 0b100; + private static final byte[] EOM_BYTES = new byte[] { 0, 0, -1, -1 }; + + public static final String NAME = "permessage-deflate"; + + private final boolean serverContextTakeover; + private final int serverMaxWindowBits; + private final boolean clientContextTakeover; + private final int clientMaxWindowBits; + private final boolean isServer; + private final Inflater inflater = new Inflater(true); + private final ByteBuffer readBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE); + private final Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + private final byte[] EOM_BUFFER = new byte[EOM_BYTES.length + 1]; + + private volatile Transformation next; + private volatile boolean skipDecompression = false; + private volatile ByteBuffer writeBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE); + private volatile boolean firstCompressedFrameWritten = false; + // Flag to track if a message is completely empty + private volatile boolean emptyMessage = true; + + static PerMessageDeflate negotiate(List> preferences, boolean isServer) { + // Accept the first preference that the endpoint is able to support + for (List preference : preferences) { + boolean ok = true; + boolean serverContextTakeover = true; + int serverMaxWindowBits = -1; + boolean clientContextTakeover = true; + int clientMaxWindowBits = -1; + + for (Parameter param : preference) { + if (SERVER_NO_CONTEXT_TAKEOVER.equals(param.getName())) { + if (serverContextTakeover) { + serverContextTakeover = false; + } else { + // Duplicate definition + throw new IllegalArgumentException( + sm.getString("perMessageDeflate.duplicateParameter", SERVER_NO_CONTEXT_TAKEOVER)); + } + } else if (CLIENT_NO_CONTEXT_TAKEOVER.equals(param.getName())) { + if (clientContextTakeover) { + clientContextTakeover = false; + } else { + // Duplicate definition + throw new IllegalArgumentException( + sm.getString("perMessageDeflate.duplicateParameter", CLIENT_NO_CONTEXT_TAKEOVER)); + } + } else if (SERVER_MAX_WINDOW_BITS.equals(param.getName())) { + if (serverMaxWindowBits == -1) { + serverMaxWindowBits = Integer.parseInt(param.getValue()); + if (serverMaxWindowBits < 8 || serverMaxWindowBits > 15) { + throw new IllegalArgumentException(sm.getString("perMessageDeflate.invalidWindowSize", + SERVER_MAX_WINDOW_BITS, Integer.valueOf(serverMaxWindowBits))); + } + // Java SE API (as of Java 11) does not expose the API to + // control the Window size. It is effectively hard-coded + // to 15 + if (isServer && serverMaxWindowBits != 15) { + ok = false; + break; + // Note server window size is not an issue for the + // client since the client will assume 15 and if the + // server uses a smaller window everything will + // still work + } + } else { + // Duplicate definition + throw new IllegalArgumentException( + sm.getString("perMessageDeflate.duplicateParameter", SERVER_MAX_WINDOW_BITS)); + } + } else if (CLIENT_MAX_WINDOW_BITS.equals(param.getName())) { + if (clientMaxWindowBits == -1) { + if (param.getValue() == null) { + // Hint to server that the client supports this + // option. Java SE API (as of Java 11) does not + // expose the API to control the Window size. It is + // effectively hard-coded to 15 + clientMaxWindowBits = 15; + } else { + clientMaxWindowBits = Integer.parseInt(param.getValue()); + if (clientMaxWindowBits < 8 || clientMaxWindowBits > 15) { + throw new IllegalArgumentException(sm.getString("perMessageDeflate.invalidWindowSize", + CLIENT_MAX_WINDOW_BITS, Integer.valueOf(clientMaxWindowBits))); + } + } + // Java SE API (as of Java 11) does not expose the API to + // control the Window size. It is effectively hard-coded + // to 15 + if (!isServer && clientMaxWindowBits != 15) { + ok = false; + break; + // Note client window size is not an issue for the + // server since the server will assume 15 and if the + // client uses a smaller window everything will + // still work + } + } else { + // Duplicate definition + throw new IllegalArgumentException( + sm.getString("perMessageDeflate.duplicateParameter", CLIENT_MAX_WINDOW_BITS)); + } + } else { + // Unknown parameter + throw new IllegalArgumentException( + sm.getString("perMessageDeflate.unknownParameter", param.getName())); + } + } + if (ok) { + return new PerMessageDeflate(serverContextTakeover, serverMaxWindowBits, clientContextTakeover, + clientMaxWindowBits, isServer); + } + } + // Failed to negotiate agreeable terms + return null; + } + + + private PerMessageDeflate(boolean serverContextTakeover, int serverMaxWindowBits, boolean clientContextTakeover, + int clientMaxWindowBits, boolean isServer) { + this.serverContextTakeover = serverContextTakeover; + this.serverMaxWindowBits = serverMaxWindowBits; + this.clientContextTakeover = clientContextTakeover; + this.clientMaxWindowBits = clientMaxWindowBits; + this.isServer = isServer; + } + + + @Override + public TransformationResult getMoreData(byte opCode, boolean fin, int rsv, ByteBuffer dest) throws IOException { + // Control frames are never compressed and may appear in the middle of + // a WebSocket method. Pass them straight through. + if (Util.isControl(opCode)) { + return next.getMoreData(opCode, fin, rsv, dest); + } + + if (!Util.isContinuation(opCode)) { + // First frame in new message + skipDecompression = (rsv & RSV_BITMASK) == 0; + } + + // Pass uncompressed frames straight through. + if (skipDecompression) { + return next.getMoreData(opCode, fin, rsv, dest); + } + + int written; + boolean usedEomBytes = false; + + while (dest.remaining() > 0 || usedEomBytes) { + // Space available in destination. Try and fill it. + try { + written = inflater.inflate(dest.array(), dest.arrayOffset() + dest.position(), dest.remaining()); + } catch (DataFormatException e) { + throw new IOException(sm.getString("perMessageDeflate.deflateFailed"), e); + } catch (NullPointerException e) { + throw new IOException(sm.getString("perMessageDeflate.alreadyClosed"), e); + } + dest.position(dest.position() + written); + + if (inflater.needsInput() && !usedEomBytes) { + readBuffer.clear(); + TransformationResult nextResult = next.getMoreData(opCode, fin, (rsv ^ RSV_BITMASK), readBuffer); + inflater.setInput(readBuffer.array(), readBuffer.arrayOffset(), readBuffer.position()); + if (dest.hasRemaining()) { + if (TransformationResult.UNDERFLOW.equals(nextResult)) { + return nextResult; + } else if (TransformationResult.END_OF_FRAME.equals(nextResult) && readBuffer.position() == 0) { + if (fin) { + inflater.setInput(EOM_BYTES); + usedEomBytes = true; + } else { + return TransformationResult.END_OF_FRAME; + } + } + } else if (readBuffer.position() > 0) { + return TransformationResult.OVERFLOW; + } else if (fin) { + inflater.setInput(EOM_BYTES); + usedEomBytes = true; + } + } else if (written == 0) { + if (fin && (isServer && !clientContextTakeover || !isServer && !serverContextTakeover)) { + try { + inflater.reset(); + } catch (NullPointerException e) { + throw new IOException(sm.getString("perMessageDeflate.alreadyClosed"), e); + } + } + return TransformationResult.END_OF_FRAME; + } + } + + return TransformationResult.OVERFLOW; + } + + + @Override + public boolean validateRsv(int rsv, byte opCode) { + if (Util.isControl(opCode)) { + if ((rsv & RSV_BITMASK) != 0) { + return false; + } else { + if (next == null) { + return true; + } else { + return next.validateRsv(rsv, opCode); + } + } + } else { + int rsvNext = rsv; + if ((rsv & RSV_BITMASK) != 0) { + rsvNext = rsv ^ RSV_BITMASK; + } + if (next == null) { + return true; + } else { + return next.validateRsv(rsvNext, opCode); + } + } + } + + + @Override + public Extension getExtensionResponse() { + Extension result = new WsExtension(NAME); + + List params = result.getParameters(); + + if (!serverContextTakeover) { + params.add(new WsExtensionParameter(SERVER_NO_CONTEXT_TAKEOVER, null)); + } + if (serverMaxWindowBits != -1) { + params.add(new WsExtensionParameter(SERVER_MAX_WINDOW_BITS, Integer.toString(serverMaxWindowBits))); + } + if (!clientContextTakeover) { + params.add(new WsExtensionParameter(CLIENT_NO_CONTEXT_TAKEOVER, null)); + } + if (clientMaxWindowBits != -1) { + params.add(new WsExtensionParameter(CLIENT_MAX_WINDOW_BITS, Integer.toString(clientMaxWindowBits))); + } + + return result; + } + + + @Override + public void setNext(Transformation t) { + if (next == null) { + this.next = t; + } else { + next.setNext(t); + } + } + + + @Override + public boolean validateRsvBits(int i) { + if ((i & RSV_BITMASK) != 0) { + return false; + } + if (next == null) { + return true; + } else { + return next.validateRsvBits(i | RSV_BITMASK); + } + } + + + @Override + public List sendMessagePart(List uncompressedParts) throws IOException { + List allCompressedParts = new ArrayList<>(); + + for (MessagePart uncompressedPart : uncompressedParts) { + byte opCode = uncompressedPart.getOpCode(); + if (Util.isControl(opCode)) { + // Control messages can appear in the middle of other messages + // and must not be compressed. Pass it straight through. + allCompressedParts.add(uncompressedPart); + continue; + } + + if (uncompressedPart.getPayload().limit() != 0) { + emptyMessage = false; + } + if (emptyMessage && uncompressedPart.isFin()) { + // Zero length messages can't be compressed so pass the + // final (empty) part straight through. + allCompressedParts.add(uncompressedPart); + } else { + List compressedParts = new ArrayList<>(); + ByteBuffer uncompressedPayload = uncompressedPart.getPayload(); + SendHandler uncompressedIntermediateHandler = uncompressedPart.getIntermediateHandler(); + + if (uncompressedPayload.hasArray()) { + deflater.setInput(uncompressedPayload.array(), + uncompressedPayload.arrayOffset() + uncompressedPayload.position(), + uncompressedPayload.remaining()); + } else { + byte[] bytes = new byte[uncompressedPayload.remaining()]; + uncompressedPayload.get(bytes); + deflater.setInput(bytes, 0, bytes.length); + } + + int flush = (uncompressedPart.isFin() ? Deflater.SYNC_FLUSH : Deflater.NO_FLUSH); + boolean deflateRequired = true; + + while (deflateRequired) { + ByteBuffer compressedPayload = writeBuffer; + + try { + int written = deflater.deflate(compressedPayload.array(), + compressedPayload.arrayOffset() + compressedPayload.position(), + compressedPayload.remaining(), flush); + compressedPayload.position(compressedPayload.position() + written); + } catch (NullPointerException e) { + throw new IOException(sm.getString("perMessageDeflate.alreadyClosed"), e); + } + + if (!uncompressedPart.isFin() && compressedPayload.hasRemaining() && deflater.needsInput()) { + // This message part has been fully processed by the + // deflater. Fire the send handler for this message part + // and move on to the next message part. + break; + } + + // If this point is reached, a new compressed message part + // will be created... + MessagePart compressedPart; + + // .. and a new writeBuffer will be required. + writeBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE); + + // Flip the compressed payload ready for writing + compressedPayload.flip(); + + boolean fin = uncompressedPart.isFin(); + boolean full = compressedPayload.limit() == compressedPayload.capacity(); + boolean needsInput = deflater.needsInput(); + long blockingWriteTimeoutExpiry = uncompressedPart.getBlockingWriteTimeoutExpiry(); + + if (fin && !full && needsInput) { + // End of compressed message. Drop EOM bytes and output. + compressedPayload.limit(compressedPayload.limit() - EOM_BYTES.length); + compressedPart = new MessagePart(true, getRsv(uncompressedPart), opCode, compressedPayload, + uncompressedIntermediateHandler, uncompressedIntermediateHandler, + blockingWriteTimeoutExpiry); + deflateRequired = false; + startNewMessage(); + } else if (full && !needsInput) { + // Write buffer full and input message not fully read. + // Output and start new compressed part. + compressedPart = new MessagePart(false, getRsv(uncompressedPart), opCode, compressedPayload, + uncompressedIntermediateHandler, uncompressedIntermediateHandler, + blockingWriteTimeoutExpiry); + } else if (!fin && full && needsInput) { + // Write buffer full and input message not fully read. + // Output and get more data. + compressedPart = new MessagePart(false, getRsv(uncompressedPart), opCode, compressedPayload, + uncompressedIntermediateHandler, uncompressedIntermediateHandler, + blockingWriteTimeoutExpiry); + deflateRequired = false; + } else if (fin && full && needsInput) { + // Write buffer full. Input fully read. Deflater may be + // in one of four states: + // - output complete (just happened to align with end of + // buffer + // - in middle of EOM bytes + // - about to write EOM bytes + // - more data to write + int eomBufferWritten; + try { + eomBufferWritten = deflater.deflate(EOM_BUFFER, 0, EOM_BUFFER.length, Deflater.SYNC_FLUSH); + } catch (NullPointerException e) { + throw new IOException(sm.getString("perMessageDeflate.alreadyClosed"), e); + } + if (eomBufferWritten < EOM_BUFFER.length) { + // EOM has just been completed + compressedPayload.limit(compressedPayload.limit() - EOM_BYTES.length + eomBufferWritten); + compressedPart = new MessagePart(true, getRsv(uncompressedPart), opCode, compressedPayload, + uncompressedIntermediateHandler, uncompressedIntermediateHandler, + blockingWriteTimeoutExpiry); + deflateRequired = false; + startNewMessage(); + } else { + // More data to write + // Copy bytes to new write buffer + writeBuffer.put(EOM_BUFFER, 0, eomBufferWritten); + compressedPart = new MessagePart(false, getRsv(uncompressedPart), opCode, compressedPayload, + uncompressedIntermediateHandler, uncompressedIntermediateHandler, + blockingWriteTimeoutExpiry); + } + } else { + throw new IllegalStateException(sm.getString("perMessageDeflate.invalidState")); + } + + // Add the newly created compressed part to the set of parts + // to pass on to the next transformation. + compressedParts.add(compressedPart); + } + + SendHandler uncompressedEndHandler = uncompressedPart.getEndHandler(); + int size = compressedParts.size(); + if (size > 0) { + compressedParts.get(size - 1).setEndHandler(uncompressedEndHandler); + } + + allCompressedParts.addAll(compressedParts); + } + } + + if (next == null) { + return allCompressedParts; + } else { + return next.sendMessagePart(allCompressedParts); + } + } + + + private void startNewMessage() throws IOException { + firstCompressedFrameWritten = false; + emptyMessage = true; + if (isServer && !serverContextTakeover || !isServer && !clientContextTakeover) { + try { + deflater.reset(); + } catch (NullPointerException e) { + throw new IOException(sm.getString("perMessageDeflate.alreadyClosed"), e); + } + } + } + + + private int getRsv(MessagePart uncompressedMessagePart) { + int result = uncompressedMessagePart.getRsv(); + if (!firstCompressedFrameWritten) { + result += RSV_BITMASK; + firstCompressedFrameWritten = true; + } + return result; + } + + + @Override + public void close() { + // There will always be a next transformation + next.close(); + inflater.end(); + deflater.end(); + } +} diff --git a/java/org/apache/tomcat/websocket/PojoClassHolder.java b/java/org/apache/tomcat/websocket/PojoClassHolder.java new file mode 100644 index 0000000..61e338b --- /dev/null +++ b/java/org/apache/tomcat/websocket/PojoClassHolder.java @@ -0,0 +1,63 @@ +/* + * 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.tomcat.websocket; + +import javax.naming.NamingException; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; + +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.pojo.PojoEndpointClient; + +public class PojoClassHolder implements ClientEndpointHolder { + + private static final StringManager sm = StringManager.getManager(PojoClassHolder.class); + + private final Class pojoClazz; + private final ClientEndpointConfig clientEndpointConfig; + + + public PojoClassHolder(Class pojoClazz, ClientEndpointConfig clientEndpointConfig) { + this.pojoClazz = pojoClazz; + this.clientEndpointConfig = clientEndpointConfig; + } + + + @Override + public String getClassName() { + return pojoClazz.getName(); + } + + @Override + public Endpoint getInstance(InstanceManager instanceManager) throws DeploymentException { + try { + Object pojo; + if (instanceManager == null) { + pojo = pojoClazz.getConstructor().newInstance(); + } else { + pojo = instanceManager.newInstance(pojoClazz); + } + return new PojoEndpointClient(pojo, clientEndpointConfig.getDecoders(), instanceManager); + } catch (ReflectiveOperationException | SecurityException | NamingException e) { + throw new DeploymentException(sm.getString("clientEndpointHolder.instanceCreationFailed"), e); + } + } + +} diff --git a/java/org/apache/tomcat/websocket/PojoHolder.java b/java/org/apache/tomcat/websocket/PojoHolder.java new file mode 100644 index 0000000..8ff3f42 --- /dev/null +++ b/java/org/apache/tomcat/websocket/PojoHolder.java @@ -0,0 +1,60 @@ +/* + * 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.tomcat.websocket; + +import javax.naming.NamingException; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; + +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.pojo.PojoEndpointClient; + +public class PojoHolder implements ClientEndpointHolder { + + private static final StringManager sm = StringManager.getManager(PojoHolder.class); + + private final Object pojo; + private final ClientEndpointConfig clientEndpointConfig; + + + public PojoHolder(Object pojo, ClientEndpointConfig clientEndpointConfig) { + this.pojo = pojo; + this.clientEndpointConfig = clientEndpointConfig; + } + + + @Override + public String getClassName() { + return pojo.getClass().getName(); + } + + + @Override + public Endpoint getInstance(InstanceManager instanceManager) throws DeploymentException { + if (instanceManager != null) { + try { + instanceManager.newInstance(pojo); + } catch (ReflectiveOperationException | NamingException e) { + throw new DeploymentException(sm.getString("clientEndpointHolder.instanceRegistrationFailed"), e); + } + } + return new PojoEndpointClient(pojo, clientEndpointConfig.getDecoders(), instanceManager); + } +} diff --git a/java/org/apache/tomcat/websocket/ReadBufferOverflowException.java b/java/org/apache/tomcat/websocket/ReadBufferOverflowException.java new file mode 100644 index 0000000..5ad6f4f --- /dev/null +++ b/java/org/apache/tomcat/websocket/ReadBufferOverflowException.java @@ -0,0 +1,34 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; + +public class ReadBufferOverflowException extends IOException { + + private static final long serialVersionUID = 1L; + + private final int minBufferSize; + + public ReadBufferOverflowException(int minBufferSize) { + this.minBufferSize = minBufferSize; + } + + public int getMinBufferSize() { + return minBufferSize; + } +} diff --git a/java/org/apache/tomcat/websocket/Transformation.java b/java/org/apache/tomcat/websocket/Transformation.java new file mode 100644 index 0000000..7e60c56 --- /dev/null +++ b/java/org/apache/tomcat/websocket/Transformation.java @@ -0,0 +1,100 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import jakarta.websocket.Extension; + +/** + * The internal representation of the transformation that a WebSocket extension performs on a message. + */ +public interface Transformation { + + /** + * Sets the next transformation in the pipeline. + * + * @param t The next transformation + */ + void setNext(Transformation t); + + /** + * Validate that the RSV bit(s) required by this transformation are not being used by another extension. The + * implementation is expected to set any bits it requires before passing the set of in-use bits to the next + * transformation. + * + * @param i The RSV bits marked as in use so far as an int in the range zero to seven with RSV1 as the MSB and RSV3 + * as the LSB + * + * @return true if the combination of RSV bits used by the transformations in the pipeline do not + * conflict otherwise false + */ + boolean validateRsvBits(int i); + + /** + * Obtain the extension that describes the information to be returned to the client. + * + * @return The extension information that describes the parameters that have been agreed for this transformation + */ + Extension getExtensionResponse(); + + /** + * Obtain more input data. + * + * @param opCode The opcode for the frame currently being processed + * @param fin Is this the final frame in this WebSocket message? + * @param rsv The reserved bits for the frame currently being processed + * @param dest The buffer in which the data is to be written + * + * @return The result of trying to read more data from the transform + * + * @throws IOException If an I/O error occurs while reading data from the transform + */ + TransformationResult getMoreData(byte opCode, boolean fin, int rsv, ByteBuffer dest) throws IOException; + + /** + * Validates the RSV and opcode combination (assumed to have been extracted from a WebSocket Frame) for this + * extension. The implementation is expected to unset any RSV bits it has validated before passing the remaining RSV + * bits to the next transformation in the pipeline. + * + * @param rsv The RSV bits received as an int in the range zero to seven with RSV1 as the MSB and RSV3 as the LSB + * @param opCode The opCode received + * + * @return true if the RSV is valid otherwise false + */ + boolean validateRsv(int rsv, byte opCode); + + /** + * Takes the provided list of messages, transforms them, passes the transformed list on to the next transformation + * (if any) and then returns the resulting list of message parts after all of the transformations have been applied. + * + * @param messageParts The list of messages to be transformed + * + * @return The list of messages after this any any subsequent transformations have been applied. The size of the + * returned list may be bigger or smaller than the size of the input list + * + * @throws IOException If an error occurs during the transformation of the message parts + */ + List sendMessagePart(List messageParts) throws IOException; + + /** + * Clean-up any resources that were used by the transformation. + */ + void close(); +} diff --git a/java/org/apache/tomcat/websocket/TransformationFactory.java b/java/org/apache/tomcat/websocket/TransformationFactory.java new file mode 100644 index 0000000..3503717 --- /dev/null +++ b/java/org/apache/tomcat/websocket/TransformationFactory.java @@ -0,0 +1,41 @@ +/* + * 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.tomcat.websocket; + +import java.util.List; + +import jakarta.websocket.Extension; + +public class TransformationFactory { + + private static final TransformationFactory factory = new TransformationFactory(); + + private TransformationFactory() { + // Hide default constructor + } + + public static TransformationFactory getInstance() { + return factory; + } + + public Transformation create(String name, List> preferences, boolean isServer) { + if (PerMessageDeflate.NAME.equals(name)) { + return PerMessageDeflate.negotiate(preferences, isServer); + } + return null; + } +} diff --git a/java/org/apache/tomcat/websocket/TransformationResult.java b/java/org/apache/tomcat/websocket/TransformationResult.java new file mode 100644 index 0000000..edf282c --- /dev/null +++ b/java/org/apache/tomcat/websocket/TransformationResult.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.websocket; + +public enum TransformationResult { + /** + * The end of the available data was reached before the WebSocket frame was completely read. + */ + UNDERFLOW, + + /** + * The provided destination buffer was filled before all of the available data from the WebSocket frame could be + * processed. + */ + OVERFLOW, + + /** + * The end of the WebSocket frame was reached and all the data from that frame processed into the provided + * destination buffer. + */ + END_OF_FRAME +} diff --git a/java/org/apache/tomcat/websocket/Util.java b/java/org/apache/tomcat/websocket/Util.java new file mode 100644 index 0000000..00e47b4 --- /dev/null +++ b/java/org/apache/tomcat/websocket/Util.java @@ -0,0 +1,640 @@ +/* + * 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.tomcat.websocket; + +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +import javax.naming.NamingException; + +import jakarta.websocket.CloseReason.CloseCode; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.Decoder; +import jakarta.websocket.Decoder.Binary; +import jakarta.websocket.Decoder.BinaryStream; +import jakarta.websocket.Decoder.Text; +import jakarta.websocket.Decoder.TextStream; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Encoder; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Extension; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; + +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.pojo.PojoMessageHandlerPartialBinary; +import org.apache.tomcat.websocket.pojo.PojoMessageHandlerWholeBinary; +import org.apache.tomcat.websocket.pojo.PojoMessageHandlerWholeText; + +/** + * Utility class for internal use only within the {@link org.apache.tomcat.websocket} package. + */ +public class Util { + + private static final StringManager sm = StringManager.getManager(Util.class); + private static final Queue randoms = new ConcurrentLinkedQueue<>(); + + private Util() { + // Hide default constructor + } + + + static boolean isControl(byte opCode) { + return (opCode & 0x08) != 0; + } + + + static boolean isText(byte opCode) { + return opCode == Constants.OPCODE_TEXT; + } + + + static boolean isContinuation(byte opCode) { + return opCode == Constants.OPCODE_CONTINUATION; + } + + + static CloseCode getCloseCode(int code) { + if (code > 2999 && code < 5000) { + return CloseCodes.getCloseCode(code); + } + switch (code) { + case 1000: + return CloseCodes.NORMAL_CLOSURE; + case 1001: + return CloseCodes.GOING_AWAY; + case 1002: + return CloseCodes.PROTOCOL_ERROR; + case 1003: + return CloseCodes.CANNOT_ACCEPT; + case 1004: + // Should not be used in a close frame + // return CloseCodes.RESERVED; + return CloseCodes.PROTOCOL_ERROR; + case 1005: + // Should not be used in a close frame + // return CloseCodes.NO_STATUS_CODE; + return CloseCodes.PROTOCOL_ERROR; + case 1006: + // Should not be used in a close frame + // return CloseCodes.CLOSED_ABNORMALLY; + return CloseCodes.PROTOCOL_ERROR; + case 1007: + return CloseCodes.NOT_CONSISTENT; + case 1008: + return CloseCodes.VIOLATED_POLICY; + case 1009: + return CloseCodes.TOO_BIG; + case 1010: + return CloseCodes.NO_EXTENSION; + case 1011: + return CloseCodes.UNEXPECTED_CONDITION; + case 1012: + // Not in RFC6455 + // return CloseCodes.SERVICE_RESTART; + return CloseCodes.PROTOCOL_ERROR; + case 1013: + // Not in RFC6455 + // return CloseCodes.TRY_AGAIN_LATER; + return CloseCodes.PROTOCOL_ERROR; + case 1015: + // Should not be used in a close frame + // return CloseCodes.TLS_HANDSHAKE_FAILURE; + return CloseCodes.PROTOCOL_ERROR; + default: + return CloseCodes.PROTOCOL_ERROR; + } + } + + + static byte[] generateMask() { + // SecureRandom is not thread-safe so need to make sure only one thread + // uses it at a time. In theory, the pool could grow to the same size + // as the number of request processing threads. In reality it will be + // a lot smaller. + + // Get a SecureRandom from the pool + SecureRandom sr = randoms.poll(); + + // If one isn't available, generate a new one + if (sr == null) { + try { + sr = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + // Fall back to platform default + sr = new SecureRandom(); + } + } + + // Generate the mask + byte[] result = new byte[4]; + sr.nextBytes(result); + + // Put the SecureRandom back in the poll + randoms.add(sr); + + return result; + } + + + static Class getMessageType(MessageHandler listener) { + return getGenericType(MessageHandler.class, listener.getClass()).getClazz(); + } + + + private static Class getDecoderType(Class decoder) { + return getGenericType(Decoder.class, decoder).getClazz(); + } + + + static Class getEncoderType(Class encoder) { + return getGenericType(Encoder.class, encoder).getClazz(); + } + + + private static TypeResult getGenericType(Class type, Class clazz) { + + // Look to see if this class implements the interface of interest + + // Get all the interfaces + Type[] interfaces = clazz.getGenericInterfaces(); + for (Type iface : interfaces) { + // Only need to check interfaces that use generics + if (iface instanceof ParameterizedType) { + ParameterizedType pi = (ParameterizedType) iface; + // Look for the interface of interest + if (pi.getRawType() instanceof Class) { + if (type.isAssignableFrom((Class) pi.getRawType())) { + return getTypeParameter(clazz, pi.getActualTypeArguments()[0]); + } + } + } + } + + // Interface not found on this class. Look at the superclass. + @SuppressWarnings("unchecked") + Class superClazz = (Class) clazz.getSuperclass(); + if (superClazz == null) { + // Finished looking up the class hierarchy without finding anything + return null; + } + + TypeResult superClassTypeResult = getGenericType(type, superClazz); + int dimension = superClassTypeResult.getDimension(); + if (superClassTypeResult.getIndex() == -1 && dimension == 0) { + // Superclass implements interface and defines explicit type for + // the interface of interest + return superClassTypeResult; + } + + if (superClassTypeResult.getIndex() > -1) { + // Superclass implements interface and defines unknown type for + // the interface of interest + // Map that unknown type to the generic types defined in this class + ParameterizedType superClassType = (ParameterizedType) clazz.getGenericSuperclass(); + TypeResult result = getTypeParameter(clazz, + superClassType.getActualTypeArguments()[superClassTypeResult.getIndex()]); + result.incrementDimension(superClassTypeResult.getDimension()); + if (result.getClazz() != null && result.getDimension() > 0) { + superClassTypeResult = result; + } else { + return result; + } + } + + if (superClassTypeResult.getDimension() > 0) { + StringBuilder className = new StringBuilder(); + for (int i = 0; i < dimension; i++) { + className.append('['); + } + className.append('L'); + className.append(superClassTypeResult.getClazz().getCanonicalName()); + className.append(';'); + + Class arrayClazz; + try { + arrayClazz = Class.forName(className.toString()); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(e); + } + + return new TypeResult(arrayClazz, -1, 0); + } + + // Error will be logged further up the call stack + return null; + } + + + /* + * For a generic parameter, return either the Class used or if the type is unknown, the index for the type in + * definition of the class + */ + private static TypeResult getTypeParameter(Class clazz, Type argType) { + if (argType instanceof Class) { + return new TypeResult((Class) argType, -1, 0); + } else if (argType instanceof ParameterizedType) { + return new TypeResult((Class) ((ParameterizedType) argType).getRawType(), -1, 0); + } else if (argType instanceof GenericArrayType) { + Type arrayElementType = ((GenericArrayType) argType).getGenericComponentType(); + TypeResult result = getTypeParameter(clazz, arrayElementType); + result.incrementDimension(1); + return result; + } else { + TypeVariable[] tvs = clazz.getTypeParameters(); + for (int i = 0; i < tvs.length; i++) { + if (tvs[i].equals(argType)) { + return new TypeResult(null, i, 0); + } + } + return null; + } + } + + + public static boolean isPrimitive(Class clazz) { + if (clazz.isPrimitive()) { + return true; + } else if (clazz.equals(Boolean.class) || clazz.equals(Byte.class) || clazz.equals(Character.class) || + clazz.equals(Double.class) || clazz.equals(Float.class) || clazz.equals(Integer.class) || + clazz.equals(Long.class) || clazz.equals(Short.class)) { + return true; + } + return false; + } + + + public static Object coerceToType(Class type, String value) { + if (type.equals(String.class)) { + return value; + } else if (type.equals(boolean.class) || type.equals(Boolean.class)) { + return Boolean.valueOf(value); + } else if (type.equals(byte.class) || type.equals(Byte.class)) { + return Byte.valueOf(value); + } else if (type.equals(char.class) || type.equals(Character.class)) { + return Character.valueOf(value.charAt(0)); + } else if (type.equals(double.class) || type.equals(Double.class)) { + return Double.valueOf(value); + } else if (type.equals(float.class) || type.equals(Float.class)) { + return Float.valueOf(value); + } else if (type.equals(int.class) || type.equals(Integer.class)) { + return Integer.valueOf(value); + } else if (type.equals(long.class) || type.equals(Long.class)) { + return Long.valueOf(value); + } else if (type.equals(short.class) || type.equals(Short.class)) { + return Short.valueOf(value); + } else { + throw new IllegalArgumentException(sm.getString("util.invalidType", value, type.getName())); + } + } + + + /** + * Build the list of decoder entries from a set of decoder implementations. + * + * @param decoderClazzes Decoder implementation classes + * @param instanceManager Instance manager to use to create Decoder instances + * + * @return List of mappings from target type to associated decoder + * + * @throws DeploymentException If a provided decoder class is not valid + */ + public static List getDecoders(List> decoderClazzes, + InstanceManager instanceManager) throws DeploymentException { + + List result = new ArrayList<>(); + if (decoderClazzes != null) { + for (Class decoderClazz : decoderClazzes) { + // Need to instantiate decoder to ensure it is valid and that + // deployment can be failed if it is not + Decoder instance; + try { + if (instanceManager == null) { + instance = decoderClazz.getConstructor().newInstance(); + } else { + instance = (Decoder) instanceManager.newInstance(decoderClazz); + // Don't need this instance, so destroy it + instanceManager.destroyInstance(instance); + } + } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException + | NamingException e) { + throw new DeploymentException( + sm.getString("pojoMethodMapping.invalidDecoder", decoderClazz.getName()), e); + } + DecoderEntry entry = new DecoderEntry(getDecoderType(decoderClazz), decoderClazz); + result.add(entry); + } + } + + return result; + } + + + static Set getMessageHandlers(Class target, MessageHandler listener, + EndpointConfig endpointConfig, Session session) { + + // Will never be more than 2 types + Set results = new HashSet<>(2); + + // Simple cases - handlers already accepts one of the types expected by + // the frame handling code + if (String.class.isAssignableFrom(target)) { + MessageHandlerResult result = new MessageHandlerResult(listener, MessageHandlerResultType.TEXT); + results.add(result); + } else if (ByteBuffer.class.isAssignableFrom(target)) { + MessageHandlerResult result = new MessageHandlerResult(listener, MessageHandlerResultType.BINARY); + results.add(result); + } else if (PongMessage.class.isAssignableFrom(target)) { + MessageHandlerResult result = new MessageHandlerResult(listener, MessageHandlerResultType.PONG); + results.add(result); + // Handler needs wrapping and optional decoder to convert it to one of + // the types expected by the frame handling code + } else if (byte[].class.isAssignableFrom(target)) { + boolean whole = MessageHandler.Whole.class.isAssignableFrom(listener.getClass()); + MessageHandlerResult result = new MessageHandlerResult(whole + ? new PojoMessageHandlerWholeBinary(listener, getOnMessageMethod(listener), session, endpointConfig, + matchDecoders(target, endpointConfig, true, ((WsSession) session).getInstanceManager()), + new Object[1], 0, true, -1, false, -1) + : new PojoMessageHandlerPartialBinary(listener, getOnMessagePartialMethod(listener), session, + new Object[2], 0, true, 1, -1, -1), + MessageHandlerResultType.BINARY); + results.add(result); + } else if (InputStream.class.isAssignableFrom(target)) { + MessageHandlerResult result = new MessageHandlerResult( + new PojoMessageHandlerWholeBinary(listener, getOnMessageMethod(listener), session, endpointConfig, + matchDecoders(target, endpointConfig, true, ((WsSession) session).getInstanceManager()), + new Object[1], 0, true, -1, true, -1), + MessageHandlerResultType.BINARY); + results.add(result); + } else if (Reader.class.isAssignableFrom(target)) { + MessageHandlerResult result = new MessageHandlerResult( + new PojoMessageHandlerWholeText(listener, getOnMessageMethod(listener), session, endpointConfig, + matchDecoders(target, endpointConfig, false, ((WsSession) session).getInstanceManager()), + new Object[1], 0, true, -1, -1), + MessageHandlerResultType.TEXT); + results.add(result); + } else { + // Handler needs wrapping and requires decoder to convert it to one + // of the types expected by the frame handling code + DecoderMatch decoderMatch = matchDecoders(target, endpointConfig, + ((WsSession) session).getInstanceManager()); + Method m = getOnMessageMethod(listener); + if (decoderMatch.getBinaryDecoders().size() > 0) { + MessageHandlerResult result = new MessageHandlerResult( + new PojoMessageHandlerWholeBinary(listener, m, session, endpointConfig, + decoderMatch.getBinaryDecoders(), new Object[1], 0, false, -1, false, -1), + MessageHandlerResultType.BINARY); + results.add(result); + } + if (decoderMatch.getTextDecoders().size() > 0) { + MessageHandlerResult result = new MessageHandlerResult( + new PojoMessageHandlerWholeText(listener, m, session, endpointConfig, + decoderMatch.getTextDecoders(), new Object[1], 0, false, -1, -1), + MessageHandlerResultType.TEXT); + results.add(result); + } + } + + if (results.size() == 0) { + throw new IllegalArgumentException(sm.getString("wsSession.unknownHandler", listener, target)); + } + + return results; + } + + private static List> matchDecoders(Class target, EndpointConfig endpointConfig, + boolean binary, InstanceManager instanceManager) { + DecoderMatch decoderMatch = matchDecoders(target, endpointConfig, instanceManager); + if (binary) { + if (decoderMatch.getBinaryDecoders().size() > 0) { + return decoderMatch.getBinaryDecoders(); + } + } else if (decoderMatch.getTextDecoders().size() > 0) { + return decoderMatch.getTextDecoders(); + } + return null; + } + + private static DecoderMatch matchDecoders(Class target, EndpointConfig endpointConfig, + InstanceManager instanceManager) { + DecoderMatch decoderMatch; + try { + List> decoders = endpointConfig.getDecoders(); + List decoderEntries = getDecoders(decoders, instanceManager); + decoderMatch = new DecoderMatch(target, decoderEntries); + } catch (DeploymentException e) { + throw new IllegalArgumentException(e); + } + return decoderMatch; + } + + public static void parseExtensionHeader(List extensions, String header) { + // The relevant ABNF for the Sec-WebSocket-Extensions is as follows: + // extension-list = 1#extension + // extension = extension-token *( ";" extension-param ) + // extension-token = registered-token + // registered-token = token + // extension-param = token [ "=" (token | quoted-string) ] + // ; When using the quoted-string syntax variant, the value + // ; after quoted-string unescaping MUST conform to the + // ; 'token' ABNF. + // + // The limiting of parameter values to tokens or "quoted tokens" makes + // the parsing of the header significantly simpler and allows a number + // of short-cuts to be taken. + + // Step one, split the header into individual extensions using ',' as a + // separator + String unparsedExtensions[] = header.split(","); + for (String unparsedExtension : unparsedExtensions) { + // Step two, split the extension into the registered name and + // parameter/value pairs using ';' as a separator + String unparsedParameters[] = unparsedExtension.split(";"); + WsExtension extension = new WsExtension(unparsedParameters[0].trim()); + + for (int i = 1; i < unparsedParameters.length; i++) { + int equalsPos = unparsedParameters[i].indexOf('='); + String name; + String value; + if (equalsPos == -1) { + name = unparsedParameters[i].trim(); + value = null; + } else { + name = unparsedParameters[i].substring(0, equalsPos).trim(); + value = unparsedParameters[i].substring(equalsPos + 1).trim(); + int len = value.length(); + if (len > 1) { + if (value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') { + value = value.substring(1, value.length() - 1); + } + } + } + // Make sure value doesn't contain any of the delimiters since + // that would indicate something went wrong + if (containsDelims(name) || containsDelims(value)) { + throw new IllegalArgumentException(sm.getString("util.notToken", name, value)); + } + if (value != null && (value.indexOf(',') > -1 || value.indexOf(';') > -1 || value.indexOf('\"') > -1 || + value.indexOf('=') > -1)) { + throw new IllegalArgumentException(sm.getString("util.invalidValue", value)); + } + extension.addParameter(new WsExtensionParameter(name, value)); + } + extensions.add(extension); + } + } + + + private static boolean containsDelims(String input) { + if (input == null || input.length() == 0) { + return false; + } + for (char c : input.toCharArray()) { + switch (c) { + case ',': + case ';': + case '\"': + case '=': + return true; + default: + // NO_OP + } + + } + return false; + } + + private static Method getOnMessageMethod(MessageHandler listener) { + try { + return listener.getClass().getMethod("onMessage", Object.class); + } catch (NoSuchMethodException | SecurityException e) { + throw new IllegalArgumentException(sm.getString("util.invalidMessageHandler"), e); + } + } + + private static Method getOnMessagePartialMethod(MessageHandler listener) { + try { + return listener.getClass().getMethod("onMessage", Object.class, Boolean.TYPE); + } catch (NoSuchMethodException | SecurityException e) { + throw new IllegalArgumentException(sm.getString("util.invalidMessageHandler"), e); + } + } + + + public static class DecoderMatch { + + private final List> textDecoders = new ArrayList<>(); + private final List> binaryDecoders = new ArrayList<>(); + private final Class target; + + public DecoderMatch(Class target, List decoderEntries) { + this.target = target; + for (DecoderEntry decoderEntry : decoderEntries) { + if (decoderEntry.getClazz().isAssignableFrom(target)) { + if (Binary.class.isAssignableFrom(decoderEntry.getDecoderClazz())) { + binaryDecoders.add(decoderEntry.getDecoderClazz()); + // willDecode() method means this decoder may or may not + // decode a message so need to carry on checking for + // other matches + } else if (BinaryStream.class.isAssignableFrom(decoderEntry.getDecoderClazz())) { + binaryDecoders.add(decoderEntry.getDecoderClazz()); + // Stream decoders have to process the message so no + // more decoders can be matched + break; + } else if (Text.class.isAssignableFrom(decoderEntry.getDecoderClazz())) { + textDecoders.add(decoderEntry.getDecoderClazz()); + // willDecode() method means this decoder may or may not + // decode a message so need to carry on checking for + // other matches + } else if (TextStream.class.isAssignableFrom(decoderEntry.getDecoderClazz())) { + textDecoders.add(decoderEntry.getDecoderClazz()); + // Stream decoders have to process the message so no + // more decoders can be matched + break; + } else { + throw new IllegalArgumentException(sm.getString("util.unknownDecoderType")); + } + } + } + } + + + public List> getTextDecoders() { + return textDecoders; + } + + + public List> getBinaryDecoders() { + return binaryDecoders; + } + + + public Class getTarget() { + return target; + } + + + public boolean hasMatches() { + return (textDecoders.size() > 0) || (binaryDecoders.size() > 0); + } + } + + + private static class TypeResult { + private final Class clazz; + private final int index; + private int dimension; + + TypeResult(Class clazz, int index, int dimension) { + this.clazz = clazz; + this.index = index; + this.dimension = dimension; + } + + public Class getClazz() { + return clazz; + } + + public int getIndex() { + return index; + } + + public int getDimension() { + return dimension; + } + + public void incrementDimension(int inc) { + dimension += inc; + } + } +} diff --git a/java/org/apache/tomcat/websocket/WrappedMessageHandler.java b/java/org/apache/tomcat/websocket/WrappedMessageHandler.java new file mode 100644 index 0000000..fc5ee2e --- /dev/null +++ b/java/org/apache/tomcat/websocket/WrappedMessageHandler.java @@ -0,0 +1,25 @@ +/* + * 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.tomcat.websocket; + +import jakarta.websocket.MessageHandler; + +public interface WrappedMessageHandler { + long getMaxMessageSize(); + + MessageHandler getWrappedHandler(); +} diff --git a/java/org/apache/tomcat/websocket/WsContainerProvider.java b/java/org/apache/tomcat/websocket/WsContainerProvider.java new file mode 100644 index 0000000..0933791 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsContainerProvider.java @@ -0,0 +1,29 @@ +/* + * 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.tomcat.websocket; + +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.WebSocketContainer; + +@aQute.bnd.annotation.spi.ServiceProvider(value = ContainerProvider.class) +public class WsContainerProvider extends ContainerProvider { + + @Override + protected WebSocketContainer getContainer() { + return new WsWebSocketContainer(); + } +} diff --git a/java/org/apache/tomcat/websocket/WsExtension.java b/java/org/apache/tomcat/websocket/WsExtension.java new file mode 100644 index 0000000..6895619 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsExtension.java @@ -0,0 +1,46 @@ +/* + * 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.tomcat.websocket; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.websocket.Extension; + +public class WsExtension implements Extension { + + private final String name; + private final List parameters = new ArrayList<>(); + + WsExtension(String name) { + this.name = name; + } + + void addParameter(Parameter parameter) { + parameters.add(parameter); + } + + @Override + public String getName() { + return name; + } + + @Override + public List getParameters() { + return parameters; + } +} diff --git a/java/org/apache/tomcat/websocket/WsExtensionParameter.java b/java/org/apache/tomcat/websocket/WsExtensionParameter.java new file mode 100644 index 0000000..4bc464a --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsExtensionParameter.java @@ -0,0 +1,40 @@ +/* + * 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.tomcat.websocket; + +import jakarta.websocket.Extension.Parameter; + +public class WsExtensionParameter implements Parameter { + + private final String name; + private final String value; + + WsExtensionParameter(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } +} diff --git a/java/org/apache/tomcat/websocket/WsFrameBase.java b/java/org/apache/tomcat/websocket/WsFrameBase.java new file mode 100644 index 0000000..2b8bbb5 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsFrameBase.java @@ -0,0 +1,982 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.Extension; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.PongMessage; + +import org.apache.juli.logging.Log; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Takes the ServletInputStream, processes the WebSocket frames it contains and extracts the messages. WebSocket Pings + * received will be responded to automatically without any action required by the application. + */ +public abstract class WsFrameBase { + + private static final StringManager sm = StringManager.getManager(WsFrameBase.class); + + // Connection level attributes + protected final WsSession wsSession; + protected final ByteBuffer inputBuffer; + private final Transformation transformation; + + // Attributes for control messages + // Control messages can appear in the middle of other messages so need + // separate attributes + private final ByteBuffer controlBufferBinary = ByteBuffer.allocate(125); + private final CharBuffer controlBufferText = CharBuffer.allocate(125); + + // Attributes of the current message + private final CharsetDecoder utf8DecoderControl = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT); + private final CharsetDecoder utf8DecoderMessage = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT); + private boolean continuationExpected = false; + private boolean textMessage = false; + private ByteBuffer messageBufferBinary; + private CharBuffer messageBufferText; + // Cache the message handler in force when the message starts so it is used + // consistently for the entire message + private MessageHandler binaryMsgHandler = null; + private MessageHandler textMsgHandler = null; + + // Attributes of the current frame + private boolean fin = false; + private int rsv = 0; + private byte opCode = 0; + private final byte[] mask = new byte[4]; + private int maskIndex = 0; + private long payloadLength = 0; + private volatile long payloadWritten = 0; + + // Attributes tracking state + private volatile State state = State.NEW_FRAME; + private volatile boolean open = true; + + private static final AtomicReferenceFieldUpdater READ_STATE_UPDATER = + AtomicReferenceFieldUpdater.newUpdater(WsFrameBase.class, ReadState.class, "readState"); + private volatile ReadState readState = ReadState.WAITING; + + public WsFrameBase(WsSession wsSession, Transformation transformation) { + inputBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE); + inputBuffer.position(0).limit(0); + messageBufferBinary = ByteBuffer.allocate(wsSession.getMaxBinaryMessageBufferSize()); + messageBufferText = CharBuffer.allocate(wsSession.getMaxTextMessageBufferSize()); + wsSession.setWsFrame(this); + this.wsSession = wsSession; + Transformation finalTransformation; + if (isMasked()) { + finalTransformation = new UnmaskTransformation(); + } else { + finalTransformation = new NoopTransformation(); + } + if (transformation == null) { + this.transformation = finalTransformation; + } else { + transformation.setNext(finalTransformation); + this.transformation = transformation; + } + } + + + protected void processInputBuffer() throws IOException { + while (!isSuspended()) { + wsSession.updateLastActiveRead(); + if (state == State.NEW_FRAME) { + if (!processInitialHeader()) { + break; + } + // If a close frame has been received, no further data should + // have seen + if (!open) { + throw new IOException(sm.getString("wsFrame.closed")); + } + } + if (state == State.PARTIAL_HEADER) { + if (!processRemainingHeader()) { + break; + } + } + if (state == State.DATA) { + if (!processData()) { + break; + } + } + } + } + + + /** + * @return true if sufficient data was present to process all of the initial header + */ + private boolean processInitialHeader() throws IOException { + // Need at least two bytes of data to do this + if (inputBuffer.remaining() < 2) { + return false; + } + int b = inputBuffer.get(); + fin = (b & 0x80) != 0; + rsv = (b & 0x70) >>> 4; + opCode = (byte) (b & 0x0F); + if (!transformation.validateRsv(rsv, opCode)) { + throw new WsIOException(new CloseReason(CloseCodes.PROTOCOL_ERROR, + sm.getString("wsFrame.wrongRsv", Integer.valueOf(rsv), Integer.valueOf(opCode)))); + } + + if (Util.isControl(opCode)) { + if (!fin) { + throw new WsIOException( + new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.controlFragmented"))); + } + if (opCode != Constants.OPCODE_PING && opCode != Constants.OPCODE_PONG && + opCode != Constants.OPCODE_CLOSE) { + throw new WsIOException(new CloseReason(CloseCodes.PROTOCOL_ERROR, + sm.getString("wsFrame.invalidOpCode", Integer.valueOf(opCode)))); + } + } else { + if (continuationExpected) { + if (!Util.isContinuation(opCode)) { + throw new WsIOException( + new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.noContinuation"))); + } + } else { + try { + if (opCode == Constants.OPCODE_BINARY) { + // New binary message + textMessage = false; + int size = wsSession.getMaxBinaryMessageBufferSize(); + if (size != messageBufferBinary.capacity()) { + messageBufferBinary = ByteBuffer.allocate(size); + } + binaryMsgHandler = wsSession.getBinaryMessageHandler(); + textMsgHandler = null; + } else if (opCode == Constants.OPCODE_TEXT) { + // New text message + textMessage = true; + int size = wsSession.getMaxTextMessageBufferSize(); + if (size != messageBufferText.capacity()) { + messageBufferText = CharBuffer.allocate(size); + } + binaryMsgHandler = null; + textMsgHandler = wsSession.getTextMessageHandler(); + } else { + throw new WsIOException(new CloseReason(CloseCodes.PROTOCOL_ERROR, + sm.getString("wsFrame.invalidOpCode", Integer.valueOf(opCode)))); + } + } catch (IllegalStateException ise) { + // Thrown if the session is already closed + throw new WsIOException( + new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.sessionClosed"))); + } + } + continuationExpected = !fin; + } + b = inputBuffer.get(); + // Client data must be masked + if ((b & 0x80) == 0 && isMasked()) { + throw new WsIOException(new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.notMasked"))); + } + payloadLength = b & 0x7F; + state = State.PARTIAL_HEADER; + if (getLog().isTraceEnabled()) { + getLog().trace(sm.getString("wsFrame.partialHeaderComplete", Boolean.toString(fin), Integer.toString(rsv), + Integer.toString(opCode), Long.toString(payloadLength))); + } + return true; + } + + + protected abstract boolean isMasked(); + + protected abstract Log getLog(); + + + /** + * @return true if sufficient data was present to complete the processing of the header + */ + private boolean processRemainingHeader() throws IOException { + // Ignore the 2 bytes already read. 4 for the mask + int headerLength; + if (isMasked()) { + headerLength = 4; + } else { + headerLength = 0; + } + // Add additional bytes depending on length + if (payloadLength == 126) { + headerLength += 2; + } else if (payloadLength == 127) { + headerLength += 8; + } + if (inputBuffer.remaining() < headerLength) { + return false; + } + // Calculate new payload length if necessary + if (payloadLength == 126) { + payloadLength = byteArrayToLong(inputBuffer.array(), inputBuffer.arrayOffset() + inputBuffer.position(), 2); + inputBuffer.position(inputBuffer.position() + 2); + } else if (payloadLength == 127) { + payloadLength = byteArrayToLong(inputBuffer.array(), inputBuffer.arrayOffset() + inputBuffer.position(), 8); + // The most significant bit of those 8 bytes is required to be zero + // (see RFC 6455, section 5.2). If the most significant bit is set, + // the resulting payload length will be negative so test for that. + if (payloadLength < 0) { + throw new WsIOException( + new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.payloadMsbInvalid"))); + } + inputBuffer.position(inputBuffer.position() + 8); + } + if (Util.isControl(opCode)) { + if (payloadLength > 125) { + throw new WsIOException(new CloseReason(CloseCodes.PROTOCOL_ERROR, + sm.getString("wsFrame.controlPayloadTooBig", Long.valueOf(payloadLength)))); + } + if (!fin) { + throw new WsIOException( + new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.controlNoFin"))); + } + } + if (isMasked()) { + inputBuffer.get(mask, 0, 4); + } + state = State.DATA; + return true; + } + + + private boolean processData() throws IOException { + boolean result; + if (Util.isControl(opCode)) { + result = processDataControl(); + } else if (textMessage) { + if (textMsgHandler == null) { + result = swallowInput(); + } else { + result = processDataText(); + } + } else { + if (binaryMsgHandler == null) { + result = swallowInput(); + } else { + result = processDataBinary(); + } + } + if (result) { + updateStats(payloadLength); + } + checkRoomPayload(); + return result; + } + + + /** + * Hook for updating server side statistics. Called on every frame received. + * + * @param payloadLength Size of message payload + */ + protected void updateStats(long payloadLength) { + // NO-OP by default + } + + + private boolean processDataControl() throws IOException { + TransformationResult tr = transformation.getMoreData(opCode, fin, rsv, controlBufferBinary); + if (TransformationResult.UNDERFLOW.equals(tr)) { + return false; + } + // Control messages have fixed message size so + // TransformationResult.OVERFLOW is not possible here + + controlBufferBinary.flip(); + if (opCode == Constants.OPCODE_CLOSE) { + open = false; + String reason = null; + int code = CloseCodes.NORMAL_CLOSURE.getCode(); + if (controlBufferBinary.remaining() == 1) { + controlBufferBinary.clear(); + // Payload must be zero or 2+ bytes long + throw new WsIOException( + new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.oneByteCloseCode"))); + } + if (controlBufferBinary.remaining() > 1) { + code = controlBufferBinary.getShort(); + if (controlBufferBinary.remaining() > 0) { + CoderResult cr = utf8DecoderControl.decode(controlBufferBinary, controlBufferText, true); + if (cr.isError()) { + controlBufferBinary.clear(); + controlBufferText.clear(); + throw new WsIOException( + new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.invalidUtf8Close"))); + } + // There will be no overflow as the output buffer is big + // enough. There will be no underflow as all the data is + // passed to the decoder in a single call. + controlBufferText.flip(); + reason = controlBufferText.toString(); + } + } + wsSession.onClose(new CloseReason(Util.getCloseCode(code), reason)); + } else if (opCode == Constants.OPCODE_PING) { + if (wsSession.isOpen()) { + wsSession.getBasicRemote().sendPong(controlBufferBinary); + } + } else if (opCode == Constants.OPCODE_PONG) { + MessageHandler.Whole mhPong = wsSession.getPongMessageHandler(); + if (mhPong != null) { + try { + mhPong.onMessage(new WsPongMessage(controlBufferBinary)); + } catch (Throwable t) { + handleThrowableOnSend(t); + } finally { + controlBufferBinary.clear(); + } + } + } else { + // Should have caught this earlier but just in case... + controlBufferBinary.clear(); + throw new WsIOException(new CloseReason(CloseCodes.PROTOCOL_ERROR, + sm.getString("wsFrame.invalidOpCode", Integer.valueOf(opCode)))); + } + controlBufferBinary.clear(); + newFrame(); + return true; + } + + + @SuppressWarnings("unchecked") + protected void sendMessageText(boolean last) throws WsIOException { + if (textMsgHandler instanceof WrappedMessageHandler) { + long maxMessageSize = ((WrappedMessageHandler) textMsgHandler).getMaxMessageSize(); + if (maxMessageSize > -1 && messageBufferText.remaining() > maxMessageSize) { + throw new WsIOException(new CloseReason(CloseCodes.TOO_BIG, sm.getString("wsFrame.messageTooBig", + Long.valueOf(messageBufferText.remaining()), Long.valueOf(maxMessageSize)))); + } + } + + try { + if (textMsgHandler instanceof MessageHandler.Partial) { + ((MessageHandler.Partial) textMsgHandler).onMessage(messageBufferText.toString(), last); + } else { + // Caller ensures last == true if this branch is used + ((MessageHandler.Whole) textMsgHandler).onMessage(messageBufferText.toString()); + } + } catch (Throwable t) { + handleThrowableOnSend(t); + } finally { + messageBufferText.clear(); + } + } + + + private boolean processDataText() throws IOException { + // Copy the available data to the buffer + TransformationResult tr = transformation.getMoreData(opCode, fin, rsv, messageBufferBinary); + while (!TransformationResult.END_OF_FRAME.equals(tr)) { + // Frame not complete - we ran out of something + // Convert bytes to UTF-8 + messageBufferBinary.flip(); + while (true) { + CoderResult cr = utf8DecoderMessage.decode(messageBufferBinary, messageBufferText, false); + if (cr.isError()) { + throw new WsIOException( + new CloseReason(CloseCodes.NOT_CONSISTENT, sm.getString("wsFrame.invalidUtf8"))); + } else if (cr.isOverflow()) { + // Ran out of space in text buffer - flush it + if (usePartial()) { + messageBufferText.flip(); + sendMessageText(false); + messageBufferText.clear(); + } else { + throw new WsIOException( + new CloseReason(CloseCodes.TOO_BIG, sm.getString("wsFrame.textMessageTooBig"))); + } + } else if (cr.isUnderflow()) { + // Compact what we have to create as much space as possible + messageBufferBinary.compact(); + + // Need more input + // What did we run out of? + if (TransformationResult.OVERFLOW.equals(tr)) { + // Ran out of message buffer - exit inner loop and + // refill + break; + } else { + // TransformationResult.UNDERFLOW + // Ran out of input data - get some more + return false; + } + } + } + // Read more input data + tr = transformation.getMoreData(opCode, fin, rsv, messageBufferBinary); + } + + messageBufferBinary.flip(); + boolean last = false; + // Frame is fully received + // Convert bytes to UTF-8 + while (true) { + CoderResult cr = utf8DecoderMessage.decode(messageBufferBinary, messageBufferText, last); + if (cr.isError()) { + throw new WsIOException( + new CloseReason(CloseCodes.NOT_CONSISTENT, sm.getString("wsFrame.invalidUtf8"))); + } else if (cr.isOverflow()) { + // Ran out of space in text buffer - flush it + if (usePartial()) { + messageBufferText.flip(); + sendMessageText(false); + messageBufferText.clear(); + } else { + throw new WsIOException( + new CloseReason(CloseCodes.TOO_BIG, sm.getString("wsFrame.textMessageTooBig"))); + } + } else if (cr.isUnderflow() && !last) { + // End of frame and possible message as well. + + if (continuationExpected) { + // If partial messages are supported, send what we have + // managed to decode + if (usePartial()) { + messageBufferText.flip(); + sendMessageText(false); + messageBufferText.clear(); + } + messageBufferBinary.compact(); + newFrame(); + // Process next frame + return true; + } else { + // Make sure coder has flushed all output + last = true; + } + } else { + // End of message + messageBufferText.flip(); + sendMessageText(true); + + newMessage(); + return true; + } + } + } + + + private boolean processDataBinary() throws IOException { + // Copy the available data to the buffer + TransformationResult tr = transformation.getMoreData(opCode, fin, rsv, messageBufferBinary); + while (!TransformationResult.END_OF_FRAME.equals(tr)) { + // Frame not complete - what did we run out of? + if (TransformationResult.UNDERFLOW.equals(tr)) { + // Ran out of input data - get some more + return false; + } + + // Ran out of message buffer - flush it + if (!usePartial()) { + CloseReason cr = new CloseReason(CloseCodes.TOO_BIG, sm.getString("wsFrame.bufferTooSmall", + Integer.valueOf(messageBufferBinary.capacity()), Long.valueOf(payloadLength))); + throw new WsIOException(cr); + } + messageBufferBinary.flip(); + ByteBuffer copy = ByteBuffer.allocate(messageBufferBinary.limit()); + copy.put(messageBufferBinary); + copy.flip(); + sendMessageBinary(copy, false); + messageBufferBinary.clear(); + // Read more data + tr = transformation.getMoreData(opCode, fin, rsv, messageBufferBinary); + } + + // Frame is fully received + // Send the message if either: + // - partial messages are supported + // - the message is complete + if (usePartial() || !continuationExpected) { + messageBufferBinary.flip(); + ByteBuffer copy = ByteBuffer.allocate(messageBufferBinary.limit()); + copy.put(messageBufferBinary); + copy.flip(); + sendMessageBinary(copy, !continuationExpected); + messageBufferBinary.clear(); + } + + if (continuationExpected) { + // More data for this message expected, start a new frame + newFrame(); + } else { + // Message is complete, start a new message + newMessage(); + } + + return true; + } + + + private void handleThrowableOnSend(Throwable t) throws WsIOException { + ExceptionUtils.handleThrowable(t); + wsSession.getLocal().onError(wsSession, t); + CloseReason cr = new CloseReason(CloseCodes.CLOSED_ABNORMALLY, sm.getString("wsFrame.ioeTriggeredClose")); + throw new WsIOException(cr); + } + + + @SuppressWarnings("unchecked") + protected void sendMessageBinary(ByteBuffer msg, boolean last) throws WsIOException { + if (binaryMsgHandler instanceof WrappedMessageHandler) { + long maxMessageSize = ((WrappedMessageHandler) binaryMsgHandler).getMaxMessageSize(); + if (maxMessageSize > -1 && msg.remaining() > maxMessageSize) { + throw new WsIOException(new CloseReason(CloseCodes.TOO_BIG, sm.getString("wsFrame.messageTooBig", + Long.valueOf(msg.remaining()), Long.valueOf(maxMessageSize)))); + } + } + try { + if (binaryMsgHandler instanceof MessageHandler.Partial) { + ((MessageHandler.Partial) binaryMsgHandler).onMessage(msg, last); + } else { + // Caller ensures last == true if this branch is used + ((MessageHandler.Whole) binaryMsgHandler).onMessage(msg); + } + } catch (Throwable t) { + handleThrowableOnSend(t); + } + } + + + private void newMessage() { + messageBufferBinary.clear(); + messageBufferText.clear(); + utf8DecoderMessage.reset(); + continuationExpected = false; + newFrame(); + } + + + private void newFrame() { + if (inputBuffer.remaining() == 0) { + inputBuffer.position(0).limit(0); + } + + maskIndex = 0; + payloadWritten = 0; + state = State.NEW_FRAME; + + // These get reset in processInitialHeader() + // fin, rsv, opCode, payloadLength, mask + + checkRoomHeaders(); + } + + + private void checkRoomHeaders() { + // Is the start of the current frame too near the end of the input + // buffer? + if (inputBuffer.capacity() - inputBuffer.position() < 131) { + // Limit based on a control frame with a full payload + makeRoom(); + } + } + + + private void checkRoomPayload() { + if (inputBuffer.capacity() - inputBuffer.position() - payloadLength + payloadWritten < 0) { + makeRoom(); + } + } + + + private void makeRoom() { + inputBuffer.compact(); + inputBuffer.flip(); + } + + + private boolean usePartial() { + if (Util.isControl(opCode)) { + return false; + } else if (textMessage) { + return textMsgHandler instanceof MessageHandler.Partial; + } else { + // Must be binary + return binaryMsgHandler instanceof MessageHandler.Partial; + } + } + + + private boolean swallowInput() { + long toSkip = Math.min(payloadLength - payloadWritten, inputBuffer.remaining()); + inputBuffer.position(inputBuffer.position() + (int) toSkip); + payloadWritten += toSkip; + if (payloadWritten == payloadLength) { + if (continuationExpected) { + newFrame(); + } else { + newMessage(); + } + return true; + } else { + return false; + } + } + + + protected static long byteArrayToLong(byte[] b, int start, int len) throws IOException { + if (len > 8) { + throw new IOException(sm.getString("wsFrame.byteToLongFail", Long.valueOf(len))); + } + int shift = 0; + long result = 0; + for (int i = start + len - 1; i >= start; i--) { + result = result + ((b[i] & 0xFFL) << shift); + shift += 8; + } + return result; + } + + + protected boolean isOpen() { + return open; + } + + + protected Transformation getTransformation() { + return transformation; + } + + + private enum State { + NEW_FRAME, + PARTIAL_HEADER, + DATA + } + + + /** + *

    +     * WAITING            - not suspended
    +     *                      Server case: waiting for a notification that data is ready to be read from the socket, the
    +     *                                   socket is registered to the poller
    +     *                      Client case: data has been read from the socket and is waiting for data to be processed
    +     *
    +     * PROCESSING         - not suspended
    +     *                      Server case: reading from the socket and processing the data
    +     *                      Client case: processing the data if such has already been read and more data will be read
    +     *                                   from the socket
    +     *
    +     * SUSPENDING_WAIT    - suspended, a call to suspend() was made while in WAITING state. A call to resume() will do
    +     *                      nothing and will transition to WAITING state
    +     *
    +     * SUSPENDING_PROCESS - suspended, a call to suspend() was made while in PROCESSING state. A call to resume() will
    +     *                      do nothing and will transition to PROCESSING state
    +     *
    +     * SUSPENDED          - suspended
    +     *                      Server case: processing data finished (SUSPENDING_PROCESS) / a notification was received
    +     *                                   that data is ready to be read from the socket (SUSPENDING_WAIT), socket is not
    +     *                                   registered to the poller
    +     *                      Client case: processing data finished (SUSPENDING_PROCESS) / data has been read from the
    +     *                                   socket and is available for processing (SUSPENDING_WAIT)
    +     *                      A call to resume() will:
    +     *                      Server case: register the socket to the poller
    +     *                      Client case: resume data processing
    +     *
    +     * CLOSING            - not suspended, a close will be sent
    +     *
    +     *
    +     *     resume           data to be        resume
    +     *     no action        processed         no action
    +     *  |---------------| |---------------| |----------|
    +     *  |               v |               v v          |
    +     *  |  |----------WAITING«--------PROCESSING----|  |
    +     *  |  |             ^   processing             |  |
    +     *  |  |             |   finished               |  |
    +     *  |  |             |                          |  |
    +     *  | suspend        |                     suspend |
    +     *  |  |             |                          |  |
    +     *  |  |          resume                        |  |
    +     *  |  |    register socket to poller (server)  |  |
    +     *  |  |    resume data processing (client)     |  |
    +     *  |  |             |                          |  |
    +     *  |  v             |                          v  |
    +     * SUSPENDING_WAIT   |                  SUSPENDING_PROCESS
    +     *  |                |                             |
    +     *  | data available |        processing finished  |
    +     *  |-------------»SUSPENDED«----------------------|
    +     * 
    + */ + protected enum ReadState { + WAITING(false), + PROCESSING(false), + SUSPENDING_WAIT(true), + SUSPENDING_PROCESS(true), + SUSPENDED(true), + CLOSING(false); + + private final boolean isSuspended; + + ReadState(boolean isSuspended) { + this.isSuspended = isSuspended; + } + + public boolean isSuspended() { + return isSuspended; + } + } + + public void suspend() { + while (true) { + switch (readState) { + case WAITING: + if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.WAITING, ReadState.SUSPENDING_WAIT)) { + continue; + } + return; + case PROCESSING: + if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.PROCESSING, ReadState.SUSPENDING_PROCESS)) { + continue; + } + return; + case SUSPENDING_WAIT: + if (readState != ReadState.SUSPENDING_WAIT) { + continue; + } else { + if (getLog().isWarnEnabled()) { + getLog().warn(sm.getString("wsFrame.suspendRequested")); + } + } + return; + case SUSPENDING_PROCESS: + if (readState != ReadState.SUSPENDING_PROCESS) { + continue; + } else { + if (getLog().isWarnEnabled()) { + getLog().warn(sm.getString("wsFrame.suspendRequested")); + } + } + return; + case SUSPENDED: + if (readState != ReadState.SUSPENDED) { + continue; + } else { + if (getLog().isWarnEnabled()) { + getLog().warn(sm.getString("wsFrame.alreadySuspended")); + } + } + return; + case CLOSING: + return; + default: + throw new IllegalStateException(sm.getString("wsFrame.illegalReadState", state)); + } + } + } + + public void resume() { + while (true) { + switch (readState) { + case WAITING: + if (readState != ReadState.WAITING) { + continue; + } else { + if (getLog().isWarnEnabled()) { + getLog().warn(sm.getString("wsFrame.alreadyResumed")); + } + } + return; + case PROCESSING: + if (readState != ReadState.PROCESSING) { + continue; + } else { + if (getLog().isWarnEnabled()) { + getLog().warn(sm.getString("wsFrame.alreadyResumed")); + } + } + return; + case SUSPENDING_WAIT: + if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.SUSPENDING_WAIT, ReadState.WAITING)) { + continue; + } + return; + case SUSPENDING_PROCESS: + if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.SUSPENDING_PROCESS, ReadState.PROCESSING)) { + continue; + } + return; + case SUSPENDED: + if (!READ_STATE_UPDATER.compareAndSet(this, ReadState.SUSPENDED, ReadState.WAITING)) { + continue; + } + resumeProcessing(); + return; + case CLOSING: + return; + default: + throw new IllegalStateException(sm.getString("wsFrame.illegalReadState", state)); + } + } + } + + protected boolean isSuspended() { + return readState.isSuspended(); + } + + protected ReadState getReadState() { + return readState; + } + + protected void changeReadState(ReadState newState) { + READ_STATE_UPDATER.set(this, newState); + } + + protected boolean changeReadState(ReadState oldState, ReadState newState) { + return READ_STATE_UPDATER.compareAndSet(this, oldState, newState); + } + + /** + * This method will be invoked when the read operation is resumed. As the suspend of the read operation can be + * invoked at any time, when implementing this method one should consider that there might still be data remaining + * into the internal buffers that needs to be processed before reading again from the socket. + */ + protected abstract void resumeProcessing(); + + + private abstract static class TerminalTransformation implements Transformation { + + @Override + public boolean validateRsvBits(int i) { + // Terminal transformations don't use RSV bits and there is no next + // transformation so always return true. + return true; + } + + @Override + public Extension getExtensionResponse() { + // Return null since terminal transformations are not extensions + return null; + } + + @Override + public void setNext(Transformation t) { + // NO-OP since this is the terminal transformation + } + + /** + * {@inheritDoc} + *

    + * Anything other than a value of zero for rsv is invalid. + */ + @Override + public boolean validateRsv(int rsv, byte opCode) { + return rsv == 0; + } + + @Override + public void close() { + // NO-OP for the terminal transformations + } + } + + + /** + * For use by the client implementation that needs to obtain payload data without the need for unmasking. + */ + private final class NoopTransformation extends TerminalTransformation { + + @Override + public TransformationResult getMoreData(byte opCode, boolean fin, int rsv, ByteBuffer dest) { + // opCode is ignored as the transformation is the same for all + // opCodes + // rsv is ignored as it known to be zero at this point + long toWrite = Math.min(payloadLength - payloadWritten, inputBuffer.remaining()); + toWrite = Math.min(toWrite, dest.remaining()); + + int orgLimit = inputBuffer.limit(); + inputBuffer.limit(inputBuffer.position() + (int) toWrite); + dest.put(inputBuffer); + inputBuffer.limit(orgLimit); + payloadWritten += toWrite; + + if (payloadWritten == payloadLength) { + return TransformationResult.END_OF_FRAME; + } else if (inputBuffer.remaining() == 0) { + return TransformationResult.UNDERFLOW; + } else { + // !dest.hasRemaining() + return TransformationResult.OVERFLOW; + } + } + + + @Override + public List sendMessagePart(List messageParts) { + // TODO Masking should move to this method + // NO-OP send so simply return the message unchanged. + return messageParts; + } + } + + + /** + * For use by the server implementation that needs to obtain payload data and unmask it before any further + * processing. + */ + private final class UnmaskTransformation extends TerminalTransformation { + + @Override + public TransformationResult getMoreData(byte opCode, boolean fin, int rsv, ByteBuffer dest) { + // opCode is ignored as the transformation is the same for all + // opCodes + // rsv is ignored as it known to be zero at this point + while (payloadWritten < payloadLength && inputBuffer.remaining() > 0 && dest.hasRemaining()) { + byte b = (byte) ((inputBuffer.get() ^ mask[maskIndex]) & 0xFF); + maskIndex++; + if (maskIndex == 4) { + maskIndex = 0; + } + payloadWritten++; + dest.put(b); + } + if (payloadWritten == payloadLength) { + return TransformationResult.END_OF_FRAME; + } else if (inputBuffer.remaining() == 0) { + return TransformationResult.UNDERFLOW; + } else { + // !dest.hasRemaining() + return TransformationResult.OVERFLOW; + } + } + + @Override + public List sendMessagePart(List messageParts) { + // NO-OP send so simply return the message unchanged. + return messageParts; + } + } +} diff --git a/java/org/apache/tomcat/websocket/WsFrameClient.java b/java/org/apache/tomcat/websocket/WsFrameClient.java new file mode 100644 index 0000000..dd0c851 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsFrameClient.java @@ -0,0 +1,227 @@ +/* + * 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.tomcat.websocket; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCodes; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class WsFrameClient extends WsFrameBase { + + private final Log log = LogFactory.getLog(WsFrameClient.class); // must not be static + private static final StringManager sm = StringManager.getManager(WsFrameClient.class); + + private final AsyncChannelWrapper channel; + private final CompletionHandler handler; + // Not final as it may need to be re-sized + private volatile ByteBuffer response; + + public WsFrameClient(ByteBuffer response, AsyncChannelWrapper channel, WsSession wsSession, + Transformation transformation) { + super(wsSession, transformation); + this.response = response; + this.channel = channel; + this.handler = new WsFrameClientCompletionHandler(); + } + + + void startInputProcessing() { + try { + processSocketRead(); + } catch (IOException e) { + close(e); + } + } + + + private void processSocketRead() throws IOException { + while (true) { + switch (getReadState()) { + case WAITING: + if (!changeReadState(ReadState.WAITING, ReadState.PROCESSING)) { + continue; + } + while (response.hasRemaining()) { + if (isSuspended()) { + if (!changeReadState(ReadState.SUSPENDING_PROCESS, ReadState.SUSPENDED)) { + continue; + } + // There is still data available in the response buffer + // Return here so that the response buffer will not be + // cleared and there will be no data read from the + // socket. Thus when the read operation is resumed first + // the data left in the response buffer will be consumed + // and then a new socket read will be performed + return; + } + inputBuffer.mark(); + inputBuffer.position(inputBuffer.limit()).limit(inputBuffer.capacity()); + + int toCopy = Math.min(response.remaining(), inputBuffer.remaining()); + + // Copy remaining bytes read in HTTP phase to input buffer used by + // frame processing + + int orgLimit = response.limit(); + response.limit(response.position() + toCopy); + inputBuffer.put(response); + response.limit(orgLimit); + + inputBuffer.limit(inputBuffer.position()).reset(); + + // Process the data we have + processInputBuffer(); + } + response.clear(); + + // Get some more data + if (isOpen()) { + channel.read(response, null, handler); + } else { + changeReadState(ReadState.CLOSING); + } + return; + case SUSPENDING_WAIT: + if (!changeReadState(ReadState.SUSPENDING_WAIT, ReadState.SUSPENDED)) { + continue; + } + return; + default: + throw new IllegalStateException(sm.getString("wsFrameServer.illegalReadState", getReadState())); + } + } + } + + + /* + * Fatal error. Usually an I/O error. Try and send notifications. Make sure socket is closed. + */ + private void close(Throwable t) { + changeReadState(ReadState.CLOSING); + CloseReason cr; + if (t instanceof WsIOException) { + cr = ((WsIOException) t).getCloseReason(); + } else { + cr = new CloseReason(CloseCodes.CLOSED_ABNORMALLY, t.getMessage()); + } + + wsSession.doClose(cr, cr, true); + } + + + @Override + protected boolean isMasked() { + // Data is from the server so it is not masked + return false; + } + + + @Override + protected Log getLog() { + return log; + } + + private class WsFrameClientCompletionHandler implements CompletionHandler { + + @Override + public void completed(Integer result, Void attachment) { + if (result.intValue() == -1) { + // BZ 57762. A dropped connection will get reported as EOF + // rather than as an error so handle it here. + if (isOpen()) { + // No close frame was received + close(new EOFException()); + } + // No data to process + return; + } + response.flip(); + doResumeProcessing(true); + } + + @Override + public void failed(Throwable exc, Void attachment) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("wsFrame.readFailed"), exc); + } + if (exc instanceof ReadBufferOverflowException) { + // response will be empty if this exception is thrown + response = ByteBuffer.allocate(((ReadBufferOverflowException) exc).getMinBufferSize()); + response.flip(); + doResumeProcessing(false); + } else { + close(exc); + } + } + + private void doResumeProcessing(boolean checkOpenOnError) { + while (true) { + switch (getReadState()) { + case PROCESSING: + if (!changeReadState(ReadState.PROCESSING, ReadState.WAITING)) { + continue; + } + resumeProcessing(checkOpenOnError); + return; + case SUSPENDING_PROCESS: + if (!changeReadState(ReadState.SUSPENDING_PROCESS, ReadState.SUSPENDED)) { + continue; + } + return; + default: + throw new IllegalStateException(sm.getString("wsFrame.illegalReadState", getReadState())); + } + } + } + } + + + @Override + protected void resumeProcessing() { + resumeProcessing(true); + } + + private void resumeProcessing(boolean checkOpenOnError) { + try { + processSocketRead(); + } catch (IOException e) { + if (checkOpenOnError) { + // Only send a close message on an IOException if the client + // has not yet received a close control message from the server + // as the IOException may be in response to the client + // continuing to send a message after the server sent a close + // control message. + if (isOpen()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("wsFrameClient.ioe"), e); + } + close(e); + } + } else { + close(e); + } + } + } +} diff --git a/java/org/apache/tomcat/websocket/WsHandshakeResponse.java b/java/org/apache/tomcat/websocket/WsHandshakeResponse.java new file mode 100644 index 0000000..a35f04d --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsHandshakeResponse.java @@ -0,0 +1,56 @@ +/* + * 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.tomcat.websocket; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import jakarta.websocket.HandshakeResponse; + +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; + +/** + * Represents the response to a WebSocket handshake. + */ +public class WsHandshakeResponse implements HandshakeResponse { + + private final Map> headers = new CaseInsensitiveKeyMap<>(); + + + public WsHandshakeResponse() { + } + + + public WsHandshakeResponse(Map> headers) { + for (Entry> entry : headers.entrySet()) { + if (this.headers.containsKey(entry.getKey())) { + this.headers.get(entry.getKey()).addAll(entry.getValue()); + } else { + List values = new ArrayList<>(entry.getValue()); + this.headers.put(entry.getKey(), values); + } + } + } + + + @Override + public Map> getHeaders() { + return headers; + } +} diff --git a/java/org/apache/tomcat/websocket/WsIOException.java b/java/org/apache/tomcat/websocket/WsIOException.java new file mode 100644 index 0000000..d7fe6fb --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsIOException.java @@ -0,0 +1,40 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; + +import jakarta.websocket.CloseReason; + +/** + * Allows the WebSocket implementation to throw an {@link IOException} that includes a {@link CloseReason} specific to + * the error that can be passed back to the client. + */ +public class WsIOException extends IOException { + + private static final long serialVersionUID = 1L; + + private final CloseReason closeReason; + + public WsIOException(CloseReason closeReason) { + this.closeReason = closeReason; + } + + public CloseReason getCloseReason() { + return closeReason; + } +} diff --git a/java/org/apache/tomcat/websocket/WsPongMessage.java b/java/org/apache/tomcat/websocket/WsPongMessage.java new file mode 100644 index 0000000..6019fc5 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsPongMessage.java @@ -0,0 +1,39 @@ +/* + * 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.tomcat.websocket; + +import java.nio.ByteBuffer; + +import jakarta.websocket.PongMessage; + +public class WsPongMessage implements PongMessage { + + private final ByteBuffer applicationData; + + + public WsPongMessage(ByteBuffer applicationData) { + byte[] dst = new byte[applicationData.limit()]; + applicationData.get(dst); + this.applicationData = ByteBuffer.wrap(dst); + } + + + @Override + public ByteBuffer getApplicationData() { + return applicationData; + } +} diff --git a/java/org/apache/tomcat/websocket/WsRemoteEndpointAsync.java b/java/org/apache/tomcat/websocket/WsRemoteEndpointAsync.java new file mode 100644 index 0000000..a214026 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsRemoteEndpointAsync.java @@ -0,0 +1,78 @@ +/* + * 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.tomcat.websocket; + +import java.nio.ByteBuffer; +import java.util.concurrent.Future; + +import jakarta.websocket.RemoteEndpoint; +import jakarta.websocket.SendHandler; + +public class WsRemoteEndpointAsync extends WsRemoteEndpointBase implements RemoteEndpoint.Async { + + WsRemoteEndpointAsync(WsRemoteEndpointImplBase base) { + super(base); + } + + + @Override + public long getSendTimeout() { + return base.getSendTimeout(); + } + + + @Override + public void setSendTimeout(long timeout) { + base.setSendTimeout(timeout); + } + + + @Override + public void sendText(String text, SendHandler completion) { + base.sendStringByCompletion(text, completion); + } + + + @Override + public Future sendText(String text) { + return base.sendStringByFuture(text); + } + + + @Override + public Future sendBinary(ByteBuffer data) { + return base.sendBytesByFuture(data); + } + + + @Override + public void sendBinary(ByteBuffer data, SendHandler completion) { + base.sendBytesByCompletion(data, completion); + } + + + @Override + public Future sendObject(Object obj) { + return base.sendObjectByFuture(obj); + } + + + @Override + public void sendObject(Object obj, SendHandler completion) { + base.sendObjectByCompletion(obj, completion); + } +} diff --git a/java/org/apache/tomcat/websocket/WsRemoteEndpointBase.java b/java/org/apache/tomcat/websocket/WsRemoteEndpointBase.java new file mode 100644 index 0000000..e0a054d --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsRemoteEndpointBase.java @@ -0,0 +1,62 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jakarta.websocket.RemoteEndpoint; + +public abstract class WsRemoteEndpointBase implements RemoteEndpoint { + + protected final WsRemoteEndpointImplBase base; + + + WsRemoteEndpointBase(WsRemoteEndpointImplBase base) { + this.base = base; + } + + + @Override + public final void setBatchingAllowed(boolean batchingAllowed) throws IOException { + base.setBatchingAllowed(batchingAllowed); + } + + + @Override + public final boolean getBatchingAllowed() { + return base.getBatchingAllowed(); + } + + + @Override + public final void flushBatch() throws IOException { + base.flushBatch(); + } + + + @Override + public final void sendPing(ByteBuffer applicationData) throws IOException, IllegalArgumentException { + base.sendPing(applicationData); + } + + + @Override + public final void sendPong(ByteBuffer applicationData) throws IOException, IllegalArgumentException { + base.sendPong(applicationData); + } +} diff --git a/java/org/apache/tomcat/websocket/WsRemoteEndpointBasic.java b/java/org/apache/tomcat/websocket/WsRemoteEndpointBasic.java new file mode 100644 index 0000000..5bbd41f --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsRemoteEndpointBasic.java @@ -0,0 +1,74 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; + +import jakarta.websocket.EncodeException; +import jakarta.websocket.RemoteEndpoint; + +public class WsRemoteEndpointBasic extends WsRemoteEndpointBase implements RemoteEndpoint.Basic { + + WsRemoteEndpointBasic(WsRemoteEndpointImplBase base) { + super(base); + } + + + @Override + public void sendText(String text) throws IOException { + base.sendString(text); + } + + + @Override + public void sendBinary(ByteBuffer data) throws IOException { + base.sendBytes(data); + } + + + @Override + public void sendText(String fragment, boolean isLast) throws IOException { + base.sendPartialString(fragment, isLast); + } + + + @Override + public void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException { + base.sendPartialBytes(partialByte, isLast); + } + + + @Override + public OutputStream getSendStream() throws IOException { + return base.getSendStream(); + } + + + @Override + public Writer getSendWriter() throws IOException { + return base.getSendWriter(); + } + + + @Override + public void sendObject(Object o) throws IOException, EncodeException { + base.sendObject(o); + } +} diff --git a/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java b/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java new file mode 100644 index 0000000..7c28b06 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java @@ -0,0 +1,1283 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; + +import javax.naming.NamingException; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.EncodeException; +import jakarta.websocket.Encoder; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.RemoteEndpoint; +import jakarta.websocket.SendHandler; +import jakarta.websocket.SendResult; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.Utf8Encoder; +import org.apache.tomcat.util.res.StringManager; + +public abstract class WsRemoteEndpointImplBase implements RemoteEndpoint { + + protected static final StringManager sm = StringManager.getManager(WsRemoteEndpointImplBase.class); + + protected static final SendResult SENDRESULT_OK = new SendResult(); + + private final Log log = LogFactory.getLog(WsRemoteEndpointImplBase.class); // must not be static + + private final StateMachine stateMachine = new StateMachine(); + + private final IntermediateMessageHandler intermediateMessageHandler = new IntermediateMessageHandler(this); + + private Transformation transformation = null; + protected final Semaphore messagePartInProgress = new Semaphore(1); + private final Queue messagePartQueue = new ArrayDeque<>(); + private final Object messagePartLock = new Object(); + + // State + private volatile boolean closed = false; + private boolean fragmented = false; + private boolean nextFragmented = false; + private boolean text = false; + private boolean nextText = false; + + // Max size of WebSocket header is 14 bytes + private final ByteBuffer headerBuffer = ByteBuffer.allocate(14); + private final ByteBuffer outputBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE); + private final CharsetEncoder encoder = new Utf8Encoder(); + private final ByteBuffer encoderBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE); + private final AtomicBoolean batchingAllowed = new AtomicBoolean(false); + private volatile long sendTimeout = -1; + private WsSession wsSession; + private List encoderEntries = new ArrayList<>(); + + + protected void setTransformation(Transformation transformation) { + this.transformation = transformation; + } + + + public long getSendTimeout() { + return sendTimeout; + } + + + public void setSendTimeout(long timeout) { + this.sendTimeout = timeout; + } + + + @Override + public void setBatchingAllowed(boolean batchingAllowed) throws IOException { + boolean oldValue = this.batchingAllowed.getAndSet(batchingAllowed); + + if (oldValue && !batchingAllowed) { + flushBatch(); + } + } + + + @Override + public boolean getBatchingAllowed() { + return batchingAllowed.get(); + } + + + @Override + public void flushBatch() throws IOException { + sendMessageBlock(Constants.INTERNAL_OPCODE_FLUSH, null, true); + } + + + public void sendBytes(ByteBuffer data) throws IOException { + if (data == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData")); + } + stateMachine.binaryStart(); + sendMessageBlock(Constants.OPCODE_BINARY, data, true); + stateMachine.complete(true); + } + + + public Future sendBytesByFuture(ByteBuffer data) { + FutureToSendHandler f2sh = new FutureToSendHandler(wsSession); + sendBytesByCompletion(data, f2sh); + return f2sh; + } + + + public void sendBytesByCompletion(ByteBuffer data, SendHandler handler) { + if (data == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData")); + } + if (handler == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullHandler")); + } + StateUpdateSendHandler sush = new StateUpdateSendHandler(handler, stateMachine); + stateMachine.binaryStart(); + startMessage(Constants.OPCODE_BINARY, data, true, sush); + } + + + public void sendPartialBytes(ByteBuffer partialByte, boolean last) throws IOException { + if (partialByte == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData")); + } + stateMachine.binaryPartialStart(); + sendMessageBlock(Constants.OPCODE_BINARY, partialByte, last); + stateMachine.complete(last); + } + + + @Override + public void sendPing(ByteBuffer applicationData) throws IOException, IllegalArgumentException { + if (applicationData.remaining() > 125) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.tooMuchData")); + } + sendMessageBlock(Constants.OPCODE_PING, applicationData, true); + } + + + @Override + public void sendPong(ByteBuffer applicationData) throws IOException, IllegalArgumentException { + if (applicationData.remaining() > 125) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.tooMuchData")); + } + sendMessageBlock(Constants.OPCODE_PONG, applicationData, true); + } + + + public void sendString(String text) throws IOException { + if (text == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData")); + } + stateMachine.textStart(); + sendMessageBlock(CharBuffer.wrap(text), true); + } + + + public Future sendStringByFuture(String text) { + FutureToSendHandler f2sh = new FutureToSendHandler(wsSession); + sendStringByCompletion(text, f2sh); + return f2sh; + } + + + public void sendStringByCompletion(String text, SendHandler handler) { + if (text == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData")); + } + if (handler == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullHandler")); + } + stateMachine.textStart(); + TextMessageSendHandler tmsh = new TextMessageSendHandler(handler, CharBuffer.wrap(text), true, encoder, + encoderBuffer, this); + tmsh.write(); + // TextMessageSendHandler will update stateMachine when it completes + } + + + public void sendPartialString(String fragment, boolean isLast) throws IOException { + if (fragment == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData")); + } + stateMachine.textPartialStart(); + sendMessageBlock(CharBuffer.wrap(fragment), isLast); + } + + + public OutputStream getSendStream() { + stateMachine.streamStart(); + return new WsOutputStream(this); + } + + + public Writer getSendWriter() { + stateMachine.writeStart(); + return new WsWriter(this); + } + + + void sendMessageBlock(CharBuffer part, boolean last) throws IOException { + long timeoutExpiry = getTimeoutExpiry(); + boolean isDone = false; + while (!isDone) { + encoderBuffer.clear(); + CoderResult cr = encoder.encode(part, encoderBuffer, true); + if (cr.isError()) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.encoderError", cr)); + } + isDone = !cr.isOverflow(); + encoderBuffer.flip(); + sendMessageBlock(Constants.OPCODE_TEXT, encoderBuffer, last && isDone, timeoutExpiry); + } + stateMachine.complete(last); + } + + + void sendMessageBlock(byte opCode, ByteBuffer payload, boolean last) throws IOException { + sendMessageBlock(opCode, payload, last, getTimeoutExpiry()); + } + + + private long getTimeoutExpiry() { + // Get the timeout before we send the message. The message may + // trigger a session close and depending on timing the client + // session may close before we can read the timeout. + long timeout = getBlockingSendTimeout(); + if (timeout < 0) { + return Long.MAX_VALUE; + } else { + return System.currentTimeMillis() + timeout; + } + } + + + private void sendMessageBlock(byte opCode, ByteBuffer payload, boolean last, long timeoutExpiry) + throws IOException { + wsSession.updateLastActiveWrite(); + + BlockingSendHandler bsh = new BlockingSendHandler(); + + List messageParts = new ArrayList<>(); + messageParts.add(new MessagePart(last, 0, opCode, payload, bsh, bsh, timeoutExpiry)); + + messageParts = transformation.sendMessagePart(messageParts); + + // Some extensions/transformations may buffer messages so it is possible + // that no message parts will be returned. If this is the case simply + // return. + if (messageParts.size() == 0) { + return; + } + + try { + if (!acquireMessagePartInProgressSemaphore(opCode, timeoutExpiry)) { + String msg = sm.getString("wsRemoteEndpoint.acquireTimeout"); + wsSession.doClose(new CloseReason(CloseCodes.GOING_AWAY, msg), + new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg), true); + throw new SocketTimeoutException(msg); + } + } catch (InterruptedException e) { + String msg = sm.getString("wsRemoteEndpoint.sendInterrupt"); + wsSession.doClose(new CloseReason(CloseCodes.GOING_AWAY, msg), + new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg), true); + throw new IOException(msg, e); + } + + for (MessagePart mp : messageParts) { + try { + writeMessagePart(mp); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + messagePartInProgress.release(); + wsSession.doClose(new CloseReason(CloseCodes.GOING_AWAY, t.getMessage()), + new CloseReason(CloseCodes.CLOSED_ABNORMALLY, t.getMessage()), true); + throw t; + } + if (!bsh.getSendResult().isOK()) { + messagePartInProgress.release(); + Throwable t = bsh.getSendResult().getException(); + wsSession.doClose(new CloseReason(CloseCodes.GOING_AWAY, t.getMessage()), + new CloseReason(CloseCodes.CLOSED_ABNORMALLY, t.getMessage()), true); + throw new IOException(t); + } + // The BlockingSendHandler doesn't call end message so update the + // flags. + fragmented = nextFragmented; + text = nextText; + } + + if (payload != null) { + payload.clear(); + } + + endMessage(null, null); + } + + + /** + * Acquire the semaphore that allows a message part to be written. + * + * @param opCode The OPCODE for the message to be written + * @param timeoutExpiry The time when the attempt to acquire the semaphore should expire + * + * @return {@code true} if the semaphore is obtained, otherwise {@code false}. + * + * @throws InterruptedException If the wait for the semaphore is interrupted + */ + protected boolean acquireMessagePartInProgressSemaphore(byte opCode, long timeoutExpiry) + throws InterruptedException { + long timeout = timeoutExpiry - System.currentTimeMillis(); + return messagePartInProgress.tryAcquire(timeout, TimeUnit.MILLISECONDS); + } + + + void startMessage(byte opCode, ByteBuffer payload, boolean last, SendHandler handler) { + + wsSession.updateLastActiveWrite(); + + List messageParts = new ArrayList<>(); + messageParts.add(new MessagePart(last, 0, opCode, payload, intermediateMessageHandler, + new EndMessageHandler(this, handler), -1)); + + try { + messageParts = transformation.sendMessagePart(messageParts); + } catch (IOException ioe) { + handler.onResult(new SendResult(ioe)); + return; + } + + // Some extensions/transformations may buffer messages so it is possible + // that no message parts will be returned. If this is the case the + // trigger the supplied SendHandler + if (messageParts.size() == 0) { + handler.onResult(new SendResult()); + return; + } + + MessagePart mp = messageParts.remove(0); + + boolean doWrite = false; + synchronized (messagePartLock) { + if (Constants.OPCODE_CLOSE == mp.getOpCode() && getBatchingAllowed()) { + // Should not happen. To late to send batched messages now since + // the session has been closed. Complain loudly. + log.warn(sm.getString("wsRemoteEndpoint.flushOnCloseFailed")); + } + if (messagePartInProgress.tryAcquire()) { + doWrite = true; + } else { + // When a control message is sent while another message is being + // sent, the control message is queued. Chances are the + // subsequent data message part will end up queued while the + // control message is sent. The logic in this class (state + // machine, EndMessageHandler, TextMessageSendHandler) ensures + // that there will only ever be one data message part in the + // queue. There could be multiple control messages in the queue. + + // Add it to the queue + messagePartQueue.add(mp); + } + // Add any remaining messages to the queue + messagePartQueue.addAll(messageParts); + } + if (doWrite) { + // Actual write has to be outside sync block to avoid possible + // deadlock between messagePartLock and writeLock in + // o.a.coyote.http11.upgrade.AbstractServletOutputStream + writeMessagePart(mp); + } + } + + + void endMessage(SendHandler handler, SendResult result) { + boolean doWrite = false; + MessagePart mpNext = null; + synchronized (messagePartLock) { + + fragmented = nextFragmented; + text = nextText; + + mpNext = messagePartQueue.poll(); + if (mpNext == null) { + messagePartInProgress.release(); + } else if (!closed) { + // Session may have been closed unexpectedly in the middle of + // sending a fragmented message closing the endpoint. If this + // happens, clearly there is no point trying to send the rest of + // the message. + doWrite = true; + } + } + if (doWrite) { + // Actual write has to be outside sync block to avoid possible + // deadlock between messagePartLock and writeLock in + // o.a.coyote.http11.upgrade.AbstractServletOutputStream + writeMessagePart(mpNext); + } + + wsSession.updateLastActiveWrite(); + + // Some handlers, such as the IntermediateMessageHandler, do not have a + // nested handler so handler may be null. + if (handler != null) { + handler.onResult(result); + } + } + + + void writeMessagePart(MessagePart mp) { + if (closed) { + throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closed")); + } + + if (Constants.INTERNAL_OPCODE_FLUSH == mp.getOpCode()) { + nextFragmented = fragmented; + nextText = text; + outputBuffer.flip(); + SendHandler flushHandler = new OutputBufferFlushSendHandler(outputBuffer, mp.getEndHandler()); + doWrite(flushHandler, mp.getBlockingWriteTimeoutExpiry(), outputBuffer); + return; + } + + // Control messages may be sent in the middle of fragmented message + // so they have no effect on the fragmented or text flags + boolean first; + if (Util.isControl(mp.getOpCode())) { + nextFragmented = fragmented; + nextText = text; + if (mp.getOpCode() == Constants.OPCODE_CLOSE) { + closed = true; + } + first = true; + } else { + boolean isText = Util.isText(mp.getOpCode()); + + if (fragmented) { + // Currently fragmented + if (text != isText) { + throw new IllegalStateException(sm.getString("wsRemoteEndpoint.changeType")); + } + nextText = text; + nextFragmented = !mp.isFin(); + first = false; + } else { + // Wasn't fragmented. Might be now + if (mp.isFin()) { + nextFragmented = false; + } else { + nextFragmented = true; + nextText = isText; + } + first = true; + } + } + + byte[] mask; + + if (isMasked()) { + mask = Util.generateMask(); + } else { + mask = null; + } + + int payloadSize = mp.getPayload().remaining(); + headerBuffer.clear(); + writeHeader(headerBuffer, mp.isFin(), mp.getRsv(), mp.getOpCode(), isMasked(), mp.getPayload(), mask, first); + headerBuffer.flip(); + + if (getBatchingAllowed() || isMasked()) { + // Need to write via output buffer + OutputBufferSendHandler obsh = new OutputBufferSendHandler(mp.getEndHandler(), + mp.getBlockingWriteTimeoutExpiry(), headerBuffer, mp.getPayload(), mask, outputBuffer, + !getBatchingAllowed(), this); + obsh.write(); + } else { + // Can write directly + doWrite(mp.getEndHandler(), mp.getBlockingWriteTimeoutExpiry(), headerBuffer, mp.getPayload()); + } + + updateStats(payloadSize); + } + + + /** + * Hook for updating server side statistics. Called on every frame written (including when batching is enabled and + * the frames are buffered locally until the buffer is full or is flushed). + * + * @param payloadLength Size of message payload + */ + protected void updateStats(long payloadLength) { + // NO-OP by default + } + + + private long getBlockingSendTimeout() { + Object obj = wsSession.getUserProperties().get(Constants.BLOCKING_SEND_TIMEOUT_PROPERTY); + Long userTimeout = null; + if (obj instanceof Long) { + userTimeout = (Long) obj; + } + if (userTimeout == null) { + return Constants.DEFAULT_BLOCKING_SEND_TIMEOUT; + } else { + return userTimeout.longValue(); + } + } + + + /** + * Wraps the user provided handler so that the end point is notified when the message is complete. + */ + private static class EndMessageHandler implements SendHandler { + + private final WsRemoteEndpointImplBase endpoint; + private final SendHandler handler; + + EndMessageHandler(WsRemoteEndpointImplBase endpoint, SendHandler handler) { + this.endpoint = endpoint; + this.handler = handler; + } + + + @Override + public void onResult(SendResult result) { + endpoint.endMessage(handler, result); + } + } + + + /** + * If a transformation needs to split a {@link MessagePart} into multiple {@link MessagePart}s, it uses this handler + * as the end handler for each of the additional {@link MessagePart}s. This handler notifies this this class that + * the {@link MessagePart} has been processed and that the next {@link MessagePart} in the queue should be started. + * The final {@link MessagePart} will use the {@link EndMessageHandler} provided with the original + * {@link MessagePart}. + */ + private static class IntermediateMessageHandler implements SendHandler { + + private final WsRemoteEndpointImplBase endpoint; + + IntermediateMessageHandler(WsRemoteEndpointImplBase endpoint) { + this.endpoint = endpoint; + } + + + @Override + public void onResult(SendResult result) { + endpoint.endMessage(null, result); + } + } + + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void sendObject(Object obj) throws IOException, EncodeException { + if (obj == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData")); + } + /* + * Note that the implementation will convert primitives and their object equivalents by default but that users + * are free to specify their own encoders and decoders for this if they wish. + */ + Encoder encoder = findEncoder(obj); + if (encoder == null && Util.isPrimitive(obj.getClass())) { + String msg = obj.toString(); + sendString(msg); + return; + } + if (encoder == null && byte[].class.isAssignableFrom(obj.getClass())) { + ByteBuffer msg = ByteBuffer.wrap((byte[]) obj); + sendBytes(msg); + return; + } + + if (encoder instanceof Encoder.Text) { + String msg = ((Encoder.Text) encoder).encode(obj); + sendString(msg); + } else if (encoder instanceof Encoder.TextStream) { + try (Writer w = getSendWriter()) { + ((Encoder.TextStream) encoder).encode(obj, w); + } + } else if (encoder instanceof Encoder.Binary) { + ByteBuffer msg = ((Encoder.Binary) encoder).encode(obj); + sendBytes(msg); + } else if (encoder instanceof Encoder.BinaryStream) { + try (OutputStream os = getSendStream()) { + ((Encoder.BinaryStream) encoder).encode(obj, os); + } + } else { + throw new EncodeException(obj, sm.getString("wsRemoteEndpoint.noEncoder", obj.getClass())); + } + } + + + public Future sendObjectByFuture(Object obj) { + FutureToSendHandler f2sh = new FutureToSendHandler(wsSession); + sendObjectByCompletion(obj, f2sh); + return f2sh; + } + + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void sendObjectByCompletion(Object obj, SendHandler completion) { + + if (obj == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullData")); + } + if (completion == null) { + throw new IllegalArgumentException(sm.getString("wsRemoteEndpoint.nullHandler")); + } + + /* + * Note that the implementation will convert primitives and their object equivalents by default but that users + * are free to specify their own encoders and decoders for this if they wish. + */ + Encoder encoder = findEncoder(obj); + if (encoder == null && Util.isPrimitive(obj.getClass())) { + String msg = obj.toString(); + sendStringByCompletion(msg, completion); + return; + } + if (encoder == null && byte[].class.isAssignableFrom(obj.getClass())) { + ByteBuffer msg = ByteBuffer.wrap((byte[]) obj); + sendBytesByCompletion(msg, completion); + return; + } + + try { + if (encoder instanceof Encoder.Text) { + String msg = ((Encoder.Text) encoder).encode(obj); + sendStringByCompletion(msg, completion); + } else if (encoder instanceof Encoder.TextStream) { + try (Writer w = getSendWriter()) { + ((Encoder.TextStream) encoder).encode(obj, w); + } + completion.onResult(new SendResult()); + } else if (encoder instanceof Encoder.Binary) { + ByteBuffer msg = ((Encoder.Binary) encoder).encode(obj); + sendBytesByCompletion(msg, completion); + } else if (encoder instanceof Encoder.BinaryStream) { + try (OutputStream os = getSendStream()) { + ((Encoder.BinaryStream) encoder).encode(obj, os); + } + completion.onResult(new SendResult()); + } else { + throw new EncodeException(obj, sm.getString("wsRemoteEndpoint.noEncoder", obj.getClass())); + } + } catch (Exception e) { + SendResult sr = new SendResult(e); + completion.onResult(sr); + } + } + + + protected void setSession(WsSession wsSession) { + this.wsSession = wsSession; + } + + + protected void setEncoders(EndpointConfig endpointConfig) throws DeploymentException { + encoderEntries.clear(); + for (Class encoderClazz : endpointConfig.getEncoders()) { + Encoder instance; + InstanceManager instanceManager = wsSession.getInstanceManager(); + try { + if (instanceManager == null) { + instance = encoderClazz.getConstructor().newInstance(); + } else { + instance = (Encoder) instanceManager.newInstance(encoderClazz); + } + instance.init(endpointConfig); + } catch (ReflectiveOperationException | NamingException e) { + throw new DeploymentException(sm.getString("wsRemoteEndpoint.invalidEncoder", encoderClazz.getName()), + e); + } + EncoderEntry entry = new EncoderEntry(Util.getEncoderType(encoderClazz), instance); + encoderEntries.add(entry); + } + } + + + private Encoder findEncoder(Object obj) { + for (EncoderEntry entry : encoderEntries) { + if (entry.getClazz().isAssignableFrom(obj.getClass())) { + return entry.getEncoder(); + } + } + return null; + } + + + public final void close() { + InstanceManager instanceManager = wsSession.getInstanceManager(); + for (EncoderEntry entry : encoderEntries) { + entry.getEncoder().destroy(); + if (instanceManager != null) { + try { + instanceManager.destroyInstance(entry); + } catch (IllegalAccessException | InvocationTargetException e) { + log.warn(sm.getString("wsRemoteEndpoint.encoderDestoryFailed", encoder.getClass()), e); + } + } + } + // The transformation handles both input and output. It only needs to be + // closed once so it is closed here on the output side. + transformation.close(); + doClose(); + } + + + protected abstract void doWrite(SendHandler handler, long blockingWriteTimeoutExpiry, ByteBuffer... data); + + protected abstract boolean isMasked(); + + protected abstract void doClose(); + + protected abstract Lock getLock(); + + + private static void writeHeader(ByteBuffer headerBuffer, boolean fin, int rsv, byte opCode, boolean masked, + ByteBuffer payload, byte[] mask, boolean first) { + + byte b = 0; + + if (fin) { + // Set the fin bit + b -= 128; + } + + b += (rsv << 4); + + if (first) { + // This is the first fragment of this message + b += opCode; + } + // If not the first fragment, it is a continuation with opCode of zero + + headerBuffer.put(b); + + if (masked) { + b = (byte) 0x80; + } else { + b = 0; + } + + // Next write the mask && length length + if (payload.remaining() < 126) { + headerBuffer.put((byte) (payload.remaining() | b)); + } else if (payload.remaining() < 65536) { + headerBuffer.put((byte) (126 | b)); + headerBuffer.put((byte) (payload.remaining() >>> 8)); + headerBuffer.put((byte) (payload.remaining() & 0xFF)); + } else { + // Will never be more than 2^31-1 + headerBuffer.put((byte) (127 | b)); + headerBuffer.put((byte) 0); + headerBuffer.put((byte) 0); + headerBuffer.put((byte) 0); + headerBuffer.put((byte) 0); + headerBuffer.put((byte) (payload.remaining() >>> 24)); + headerBuffer.put((byte) (payload.remaining() >>> 16)); + headerBuffer.put((byte) (payload.remaining() >>> 8)); + headerBuffer.put((byte) (payload.remaining() & 0xFF)); + } + if (masked) { + headerBuffer.put(mask[0]); + headerBuffer.put(mask[1]); + headerBuffer.put(mask[2]); + headerBuffer.put(mask[3]); + } + } + + + private class TextMessageSendHandler implements SendHandler { + + private final SendHandler handler; + private final CharBuffer message; + private final boolean isLast; + private final CharsetEncoder encoder; + private final ByteBuffer buffer; + private final WsRemoteEndpointImplBase endpoint; + private volatile boolean isDone = false; + + TextMessageSendHandler(SendHandler handler, CharBuffer message, boolean isLast, CharsetEncoder encoder, + ByteBuffer encoderBuffer, WsRemoteEndpointImplBase endpoint) { + this.handler = handler; + this.message = message; + this.isLast = isLast; + this.encoder = encoder.reset(); + this.buffer = encoderBuffer; + this.endpoint = endpoint; + } + + public void write() { + buffer.clear(); + CoderResult cr = encoder.encode(message, buffer, true); + if (cr.isError()) { + throw new IllegalArgumentException(cr.toString()); + } + isDone = !cr.isOverflow(); + buffer.flip(); + endpoint.startMessage(Constants.OPCODE_TEXT, buffer, isDone && isLast, this); + } + + @Override + public void onResult(SendResult result) { + if (isDone) { + endpoint.stateMachine.complete(isLast); + handler.onResult(result); + } else if (!result.isOK()) { + handler.onResult(result); + } else if (closed) { + SendResult sr = new SendResult(new IOException(sm.getString("wsRemoteEndpoint.closedDuringMessage"))); + handler.onResult(sr); + } else { + write(); + } + } + } + + + /** + * Used to write data to the output buffer, flushing the buffer if it fills up. + */ + private static class OutputBufferSendHandler implements SendHandler { + + private final SendHandler handler; + private final long blockingWriteTimeoutExpiry; + private final ByteBuffer headerBuffer; + private final ByteBuffer payload; + private final byte[] mask; + private final ByteBuffer outputBuffer; + private final boolean flushRequired; + private final WsRemoteEndpointImplBase endpoint; + private volatile int maskIndex = 0; + + OutputBufferSendHandler(SendHandler completion, long blockingWriteTimeoutExpiry, ByteBuffer headerBuffer, + ByteBuffer payload, byte[] mask, ByteBuffer outputBuffer, boolean flushRequired, + WsRemoteEndpointImplBase endpoint) { + this.blockingWriteTimeoutExpiry = blockingWriteTimeoutExpiry; + this.handler = completion; + this.headerBuffer = headerBuffer; + this.payload = payload; + this.mask = mask; + this.outputBuffer = outputBuffer; + this.flushRequired = flushRequired; + this.endpoint = endpoint; + } + + public void write() { + // Write the header + while (headerBuffer.hasRemaining() && outputBuffer.hasRemaining()) { + outputBuffer.put(headerBuffer.get()); + } + if (headerBuffer.hasRemaining()) { + // Still more headers to write, need to flush + outputBuffer.flip(); + endpoint.doWrite(this, blockingWriteTimeoutExpiry, outputBuffer); + return; + } + + // Write the payload + int payloadLeft = payload.remaining(); + int payloadLimit = payload.limit(); + int outputSpace = outputBuffer.remaining(); + int toWrite = payloadLeft; + + if (payloadLeft > outputSpace) { + toWrite = outputSpace; + // Temporarily reduce the limit + payload.limit(payload.position() + toWrite); + } + + if (mask == null) { + // Use a bulk copy + outputBuffer.put(payload); + } else { + for (int i = 0; i < toWrite; i++) { + outputBuffer.put((byte) (payload.get() ^ (mask[maskIndex++] & 0xFF))); + if (maskIndex > 3) { + maskIndex = 0; + } + } + } + + if (payloadLeft > outputSpace) { + // Restore the original limit + payload.limit(payloadLimit); + // Still more data to write, need to flush + outputBuffer.flip(); + endpoint.doWrite(this, blockingWriteTimeoutExpiry, outputBuffer); + return; + } + + if (flushRequired) { + outputBuffer.flip(); + if (outputBuffer.remaining() == 0) { + handler.onResult(SENDRESULT_OK); + } else { + endpoint.doWrite(this, blockingWriteTimeoutExpiry, outputBuffer); + } + } else { + handler.onResult(SENDRESULT_OK); + } + } + + // ------------------------------------------------- SendHandler methods + @Override + public void onResult(SendResult result) { + if (result.isOK()) { + if (outputBuffer.hasRemaining()) { + endpoint.doWrite(this, blockingWriteTimeoutExpiry, outputBuffer); + } else { + outputBuffer.clear(); + write(); + } + } else { + handler.onResult(result); + } + } + } + + + /** + * Ensures that the output buffer is cleared after it has been flushed. + */ + private static class OutputBufferFlushSendHandler implements SendHandler { + + private final ByteBuffer outputBuffer; + private final SendHandler handler; + + OutputBufferFlushSendHandler(ByteBuffer outputBuffer, SendHandler handler) { + this.outputBuffer = outputBuffer; + this.handler = handler; + } + + @Override + public void onResult(SendResult result) { + if (result.isOK()) { + outputBuffer.clear(); + } + handler.onResult(result); + } + } + + + private static class WsOutputStream extends OutputStream { + + private final WsRemoteEndpointImplBase endpoint; + private final ByteBuffer buffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE); + private final Object closeLock = new Object(); + private volatile boolean closed = false; + private volatile boolean used = false; + + WsOutputStream(WsRemoteEndpointImplBase endpoint) { + this.endpoint = endpoint; + } + + @Override + public void write(int b) throws IOException { + if (closed) { + throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedOutputStream")); + } + + used = true; + if (buffer.remaining() == 0) { + flush(); + } + buffer.put((byte) b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (closed) { + throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedOutputStream")); + } + if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } + + used = true; + + if (len == 0) { + return; + } + + if (buffer.remaining() == 0) { + flush(); + } + int remaining = buffer.remaining(); + int written = 0; + + while (remaining < len - written) { + buffer.put(b, off + written, remaining); + written += remaining; + flush(); + remaining = buffer.remaining(); + } + buffer.put(b, off + written, len - written); + } + + @Override + public void flush() throws IOException { + if (closed) { + throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedOutputStream")); + } + + // Optimisation. If there is no data to flush then do not send an + // empty message. + if (buffer.position() > 0) { + doWrite(false); + } + } + + @Override + public void close() throws IOException { + synchronized (closeLock) { + if (closed) { + return; + } + closed = true; + } + + doWrite(true); + } + + private void doWrite(boolean last) throws IOException { + if (used) { + buffer.flip(); + endpoint.sendMessageBlock(Constants.OPCODE_BINARY, buffer, last); + } + endpoint.stateMachine.complete(last); + buffer.clear(); + } + } + + + private static class WsWriter extends Writer { + + private final WsRemoteEndpointImplBase endpoint; + private final CharBuffer buffer = CharBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE); + private final Object closeLock = new Object(); + private volatile boolean closed = false; + private volatile boolean used = false; + + WsWriter(WsRemoteEndpointImplBase endpoint) { + this.endpoint = endpoint; + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + if (closed) { + throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedWriter")); + } + if ((off < 0) || (off > cbuf.length) || (len < 0) || ((off + len) > cbuf.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } + + used = true; + + if (len == 0) { + return; + } + + if (buffer.remaining() == 0) { + flush(); + } + int remaining = buffer.remaining(); + int written = 0; + + while (remaining < len - written) { + buffer.put(cbuf, off + written, remaining); + written += remaining; + flush(); + remaining = buffer.remaining(); + } + buffer.put(cbuf, off + written, len - written); + } + + @Override + public void flush() throws IOException { + if (closed) { + throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedWriter")); + } + + if (buffer.position() > 0) { + doWrite(false); + } + } + + @Override + public void close() throws IOException { + synchronized (closeLock) { + if (closed) { + return; + } + closed = true; + } + + doWrite(true); + } + + private void doWrite(boolean last) throws IOException { + if (used) { + buffer.flip(); + endpoint.sendMessageBlock(buffer, last); + buffer.clear(); + } else { + endpoint.stateMachine.complete(last); + } + } + } + + + private static class EncoderEntry { + + private final Class clazz; + private final Encoder encoder; + + EncoderEntry(Class clazz, Encoder encoder) { + this.clazz = clazz; + this.encoder = encoder; + } + + public Class getClazz() { + return clazz; + } + + public Encoder getEncoder() { + return encoder; + } + } + + + private enum State { + OPEN, + STREAM_WRITING, + WRITER_WRITING, + BINARY_PARTIAL_WRITING, + BINARY_PARTIAL_READY, + BINARY_FULL_WRITING, + TEXT_PARTIAL_WRITING, + TEXT_PARTIAL_READY, + TEXT_FULL_WRITING + } + + + private static class StateMachine { + private State state = State.OPEN; + + public synchronized void streamStart() { + checkState(State.OPEN); + state = State.STREAM_WRITING; + } + + public synchronized void writeStart() { + checkState(State.OPEN); + state = State.WRITER_WRITING; + } + + public synchronized void binaryPartialStart() { + checkState(State.OPEN, State.BINARY_PARTIAL_READY); + state = State.BINARY_PARTIAL_WRITING; + } + + public synchronized void binaryStart() { + checkState(State.OPEN); + state = State.BINARY_FULL_WRITING; + } + + public synchronized void textPartialStart() { + checkState(State.OPEN, State.TEXT_PARTIAL_READY); + state = State.TEXT_PARTIAL_WRITING; + } + + public synchronized void textStart() { + checkState(State.OPEN); + state = State.TEXT_FULL_WRITING; + } + + public synchronized void complete(boolean last) { + if (last) { + checkState(State.TEXT_PARTIAL_WRITING, State.TEXT_FULL_WRITING, State.BINARY_PARTIAL_WRITING, + State.BINARY_FULL_WRITING, State.STREAM_WRITING, State.WRITER_WRITING); + state = State.OPEN; + } else { + checkState(State.TEXT_PARTIAL_WRITING, State.BINARY_PARTIAL_WRITING, State.STREAM_WRITING, + State.WRITER_WRITING); + if (state == State.TEXT_PARTIAL_WRITING) { + state = State.TEXT_PARTIAL_READY; + } else if (state == State.BINARY_PARTIAL_WRITING) { + state = State.BINARY_PARTIAL_READY; + } else if (state == State.WRITER_WRITING) { + // NO-OP. Leave state as is. + } else if (state == State.STREAM_WRITING) { + // NO-OP. Leave state as is. + } + } + } + + private void checkState(State... required) { + for (State state : required) { + if (this.state == state) { + return; + } + } + throw new IllegalStateException(sm.getString("wsRemoteEndpoint.wrongState", this.state)); + } + } + + + private static class StateUpdateSendHandler implements SendHandler { + + private final SendHandler handler; + private final StateMachine stateMachine; + + StateUpdateSendHandler(SendHandler handler, StateMachine stateMachine) { + this.handler = handler; + this.stateMachine = stateMachine; + } + + @Override + public void onResult(SendResult result) { + if (result.isOK()) { + stateMachine.complete(true); + } + handler.onResult(result); + } + } + + + private static class BlockingSendHandler implements SendHandler { + + private volatile SendResult sendResult = null; + + @Override + public void onResult(SendResult result) { + sendResult = result; + } + + public SendResult getSendResult() { + return sendResult; + } + } +} diff --git a/java/org/apache/tomcat/websocket/WsRemoteEndpointImplClient.java b/java/org/apache/tomcat/websocket/WsRemoteEndpointImplClient.java new file mode 100644 index 0000000..5f0cc67 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsRemoteEndpointImplClient.java @@ -0,0 +1,83 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReentrantLock; + +import jakarta.websocket.SendHandler; +import jakarta.websocket.SendResult; + +public class WsRemoteEndpointImplClient extends WsRemoteEndpointImplBase { + + private final AsyncChannelWrapper channel; + private final ReentrantLock lock = new ReentrantLock(); + + public WsRemoteEndpointImplClient(AsyncChannelWrapper channel) { + this.channel = channel; + } + + + @Override + protected boolean isMasked() { + return true; + } + + + @Override + protected void doWrite(SendHandler handler, long blockingWriteTimeoutExpiry, ByteBuffer... data) { + long timeout; + for (ByteBuffer byteBuffer : data) { + if (blockingWriteTimeoutExpiry == -1) { + timeout = getSendTimeout(); + if (timeout < 1) { + timeout = Long.MAX_VALUE; + } + } else { + timeout = blockingWriteTimeoutExpiry - System.currentTimeMillis(); + if (timeout < 0) { + SendResult sr = new SendResult(new IOException(sm.getString("wsRemoteEndpoint.writeTimeout"))); + handler.onResult(sr); + } + } + + try { + channel.write(byteBuffer).get(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + handler.onResult(new SendResult(e)); + return; + } + } + handler.onResult(SENDRESULT_OK); + } + + + @Override + protected void doClose() { + channel.close(); + } + + + @Override + protected ReentrantLock getLock() { + return lock; + } +} diff --git a/java/org/apache/tomcat/websocket/WsSession.java b/java/org/apache/tomcat/websocket/WsSession.java new file mode 100644 index 0000000..0c1b2d1 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsSession.java @@ -0,0 +1,1067 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import javax.naming.NamingException; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCode; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Extension; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.MessageHandler.Partial; +import jakarta.websocket.MessageHandler.Whole; +import jakarta.websocket.PongMessage; +import jakarta.websocket.RemoteEndpoint; +import jakarta.websocket.SendResult; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpointConfig; +import jakarta.websocket.server.ServerEndpointConfig.Configurator; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.pojo.PojoEndpointServer; +import org.apache.tomcat.websocket.server.DefaultServerEndpointConfigurator; + +public class WsSession implements Session { + + private final Log log = LogFactory.getLog(WsSession.class); // must not be static + private static final StringManager sm = StringManager.getManager(WsSession.class); + + // An ellipsis is a single character that looks like three periods in a row + // and is used to indicate a continuation. + private static final byte[] ELLIPSIS_BYTES = "\u2026".getBytes(StandardCharsets.UTF_8); + // An ellipsis is three bytes in UTF-8 + private static final int ELLIPSIS_BYTES_LEN = ELLIPSIS_BYTES.length; + + private static final boolean SEC_CONFIGURATOR_USES_IMPL_DEFAULT; + + private static AtomicLong ids = new AtomicLong(0); + + static { + // Use fake end point and path. They are never used, they just need to + // be sufficient to pass the validation tests. + ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(Object.class, "/"); + ServerEndpointConfig sec = builder.build(); + SEC_CONFIGURATOR_USES_IMPL_DEFAULT = sec.getConfigurator().getClass() + .equals(DefaultServerEndpointConfigurator.class); + } + + private final Endpoint localEndpoint; + private final WsRemoteEndpointImplBase wsRemoteEndpoint; + private final RemoteEndpoint.Async remoteEndpointAsync; + private final RemoteEndpoint.Basic remoteEndpointBasic; + private final ClassLoader applicationClassLoader; + private final WsWebSocketContainer webSocketContainer; + private final URI requestUri; + private final Map> requestParameterMap; + private final String queryString; + private final Principal userPrincipal; + private final EndpointConfig endpointConfig; + + private final List negotiatedExtensions; + private final String subProtocol; + private final Map pathParameters; + private final boolean secure; + private final String httpSessionId; + private final String id; + + // Expected to handle message types of only + private volatile MessageHandler textMessageHandler = null; + // Expected to handle message types of only + private volatile MessageHandler binaryMessageHandler = null; + private volatile MessageHandler.Whole pongMessageHandler = null; + private AtomicReference state = new AtomicReference<>(State.OPEN); + private final Map userProperties = new ConcurrentHashMap<>(); + private volatile int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE; + private volatile int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE; + private volatile long maxIdleTimeout = 0; + private volatile long lastActiveRead = System.currentTimeMillis(); + private volatile long lastActiveWrite = System.currentTimeMillis(); + private Map futures = new ConcurrentHashMap<>(); + private volatile Long sessionCloseTimeoutExpiry; + + + /** + * Creates a new WebSocket session for communication between the provided client and remote end points. The result + * of {@link Thread#getContextClassLoader()} at the time this constructor is called will be used when calling + * {@link Endpoint#onClose(Session, CloseReason)}. + * + * @param clientEndpointHolder The end point managed by this code + * @param wsRemoteEndpoint The other / remote end point + * @param wsWebSocketContainer The container that created this session + * @param negotiatedExtensions The agreed extensions to use for this session + * @param subProtocol The agreed sub-protocol to use for this session + * @param pathParameters The path parameters associated with the request that initiated this session or + * null if this is a client session + * @param secure Was this session initiated over a secure connection? + * @param clientEndpointConfig The configuration information for the client end point + * + * @throws DeploymentException if an invalid encode is specified + */ + public WsSession(ClientEndpointHolder clientEndpointHolder, WsRemoteEndpointImplBase wsRemoteEndpoint, + WsWebSocketContainer wsWebSocketContainer, List negotiatedExtensions, String subProtocol, + Map pathParameters, boolean secure, ClientEndpointConfig clientEndpointConfig) + throws DeploymentException { + this.wsRemoteEndpoint = wsRemoteEndpoint; + this.wsRemoteEndpoint.setSession(this); + this.remoteEndpointAsync = new WsRemoteEndpointAsync(wsRemoteEndpoint); + this.remoteEndpointBasic = new WsRemoteEndpointBasic(wsRemoteEndpoint); + this.webSocketContainer = wsWebSocketContainer; + applicationClassLoader = Thread.currentThread().getContextClassLoader(); + wsRemoteEndpoint.setSendTimeout(wsWebSocketContainer.getDefaultAsyncSendTimeout()); + this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize(); + this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize(); + this.maxIdleTimeout = webSocketContainer.getDefaultMaxSessionIdleTimeout(); + this.requestUri = null; + this.requestParameterMap = Collections.emptyMap(); + this.queryString = null; + this.userPrincipal = null; + this.httpSessionId = null; + this.negotiatedExtensions = negotiatedExtensions; + if (subProtocol == null) { + this.subProtocol = ""; + } else { + this.subProtocol = subProtocol; + } + this.pathParameters = pathParameters; + this.secure = secure; + this.wsRemoteEndpoint.setEncoders(clientEndpointConfig); + this.endpointConfig = clientEndpointConfig; + + this.userProperties.putAll(endpointConfig.getUserProperties()); + this.id = Long.toHexString(ids.getAndIncrement()); + + this.localEndpoint = clientEndpointHolder.getInstance(getInstanceManager()); + + if (log.isTraceEnabled()) { + log.trace(sm.getString("wsSession.created", id)); + } + } + + + /** + * Creates a new WebSocket session for communication between the provided server and remote end points. The result + * of {@link Thread#getContextClassLoader()} at the time this constructor is called will be used when calling + * {@link Endpoint#onClose(Session, CloseReason)}. + * + * @param wsRemoteEndpoint The other / remote end point + * @param wsWebSocketContainer The container that created this session + * @param requestUri The URI used to connect to this end point or null if this is a client + * session + * @param requestParameterMap The parameters associated with the request that initiated this session or + * null if this is a client session + * @param queryString The query string associated with the request that initiated this session or + * null if this is a client session + * @param userPrincipal The principal associated with the request that initiated this session or + * null if this is a client session + * @param httpSessionId The HTTP session ID associated with the request that initiated this session or + * null if this is a client session + * @param negotiatedExtensions The agreed extensions to use for this session + * @param subProtocol The agreed sub-protocol to use for this session + * @param pathParameters The path parameters associated with the request that initiated this session or + * null if this is a client session + * @param secure Was this session initiated over a secure connection? + * @param serverEndpointConfig The configuration information for the server end point + * + * @throws DeploymentException if an invalid encode is specified + */ + public WsSession(WsRemoteEndpointImplBase wsRemoteEndpoint, WsWebSocketContainer wsWebSocketContainer, + URI requestUri, Map> requestParameterMap, String queryString, Principal userPrincipal, + String httpSessionId, List negotiatedExtensions, String subProtocol, + Map pathParameters, boolean secure, ServerEndpointConfig serverEndpointConfig) + throws DeploymentException { + + this.wsRemoteEndpoint = wsRemoteEndpoint; + this.wsRemoteEndpoint.setSession(this); + this.remoteEndpointAsync = new WsRemoteEndpointAsync(wsRemoteEndpoint); + this.remoteEndpointBasic = new WsRemoteEndpointBasic(wsRemoteEndpoint); + this.webSocketContainer = wsWebSocketContainer; + applicationClassLoader = Thread.currentThread().getContextClassLoader(); + wsRemoteEndpoint.setSendTimeout(wsWebSocketContainer.getDefaultAsyncSendTimeout()); + this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize(); + this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize(); + this.maxIdleTimeout = webSocketContainer.getDefaultMaxSessionIdleTimeout(); + this.requestUri = requestUri; + if (requestParameterMap == null) { + this.requestParameterMap = Collections.emptyMap(); + } else { + this.requestParameterMap = requestParameterMap; + } + this.queryString = queryString; + this.userPrincipal = userPrincipal; + this.httpSessionId = httpSessionId; + this.negotiatedExtensions = negotiatedExtensions; + if (subProtocol == null) { + this.subProtocol = ""; + } else { + this.subProtocol = subProtocol; + } + this.pathParameters = pathParameters; + this.secure = secure; + this.wsRemoteEndpoint.setEncoders(serverEndpointConfig); + this.endpointConfig = serverEndpointConfig; + + this.userProperties.putAll(endpointConfig.getUserProperties()); + this.id = Long.toHexString(ids.getAndIncrement()); + + InstanceManager instanceManager = getInstanceManager(); + Configurator configurator = serverEndpointConfig.getConfigurator(); + Class clazz = serverEndpointConfig.getEndpointClass(); + + Object endpointInstance; + try { + if (instanceManager == null || !isDefaultConfigurator(configurator)) { + endpointInstance = configurator.getEndpointInstance(clazz); + if (instanceManager != null) { + try { + instanceManager.newInstance(endpointInstance); + } catch (ReflectiveOperationException | NamingException e) { + throw new DeploymentException(sm.getString("wsSession.instanceNew"), e); + } + } + } else { + endpointInstance = instanceManager.newInstance(clazz); + } + } catch (ReflectiveOperationException | NamingException e) { + throw new DeploymentException(sm.getString("wsSession.instanceCreateFailed"), e); + } + + if (endpointInstance instanceof Endpoint) { + this.localEndpoint = (Endpoint) endpointInstance; + } else { + this.localEndpoint = new PojoEndpointServer(pathParameters, endpointInstance); + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("wsSession.created", id)); + } + } + + + private boolean isDefaultConfigurator(Configurator configurator) { + if (configurator.getClass().equals(DefaultServerEndpointConfigurator.class)) { + return true; + } + if (SEC_CONFIGURATOR_USES_IMPL_DEFAULT && + configurator.getClass().equals(ServerEndpointConfig.Configurator.class)) { + return true; + } + return false; + } + + + public InstanceManager getInstanceManager() { + return webSocketContainer.getInstanceManager(applicationClassLoader); + } + + + @Override + public WebSocketContainer getContainer() { + checkState(); + return webSocketContainer; + } + + + @Override + public void addMessageHandler(MessageHandler listener) { + Class target = Util.getMessageType(listener); + doAddMessageHandler(target, listener); + } + + + @Override + public void addMessageHandler(Class clazz, Partial handler) throws IllegalStateException { + doAddMessageHandler(clazz, handler); + } + + + @Override + public void addMessageHandler(Class clazz, Whole handler) throws IllegalStateException { + doAddMessageHandler(clazz, handler); + } + + + @SuppressWarnings("unchecked") + private void doAddMessageHandler(Class target, MessageHandler listener) { + checkState(); + + // Message handlers that require decoders may map to text messages, + // binary messages, both or neither. + + // The frame processing code expects binary message handlers to + // accept ByteBuffer + + // Use the POJO message handler wrappers as they are designed to wrap + // arbitrary objects with MessageHandlers and can wrap MessageHandlers + // just as easily. + + Set mhResults = Util.getMessageHandlers(target, listener, endpointConfig, this); + + for (MessageHandlerResult mhResult : mhResults) { + switch (mhResult.getType()) { + case TEXT: { + if (textMessageHandler != null) { + throw new IllegalStateException(sm.getString("wsSession.duplicateHandlerText")); + } + textMessageHandler = mhResult.getHandler(); + break; + } + case BINARY: { + if (binaryMessageHandler != null) { + throw new IllegalStateException(sm.getString("wsSession.duplicateHandlerBinary")); + } + binaryMessageHandler = mhResult.getHandler(); + break; + } + case PONG: { + if (pongMessageHandler != null) { + throw new IllegalStateException(sm.getString("wsSession.duplicateHandlerPong")); + } + MessageHandler handler = mhResult.getHandler(); + if (handler instanceof MessageHandler.Whole) { + pongMessageHandler = (MessageHandler.Whole) handler; + } else { + throw new IllegalStateException(sm.getString("wsSession.invalidHandlerTypePong")); + } + + break; + } + default: { + throw new IllegalArgumentException( + sm.getString("wsSession.unknownHandlerType", listener, mhResult.getType())); + } + } + } + } + + + @Override + public Set getMessageHandlers() { + checkState(); + Set result = new HashSet<>(); + if (binaryMessageHandler != null) { + result.add(binaryMessageHandler); + } + if (textMessageHandler != null) { + result.add(textMessageHandler); + } + if (pongMessageHandler != null) { + result.add(pongMessageHandler); + } + return result; + } + + + @Override + public void removeMessageHandler(MessageHandler listener) { + checkState(); + if (listener == null) { + return; + } + + MessageHandler wrapped = null; + + if (listener instanceof WrappedMessageHandler) { + wrapped = ((WrappedMessageHandler) listener).getWrappedHandler(); + } + + if (wrapped == null) { + wrapped = listener; + } + + boolean removed = false; + if (wrapped.equals(textMessageHandler) || listener.equals(textMessageHandler)) { + textMessageHandler = null; + removed = true; + } + + if (wrapped.equals(binaryMessageHandler) || listener.equals(binaryMessageHandler)) { + binaryMessageHandler = null; + removed = true; + } + + if (wrapped.equals(pongMessageHandler) || listener.equals(pongMessageHandler)) { + pongMessageHandler = null; + removed = true; + } + + if (!removed) { + // ISE for now. Could swallow this silently / log this if the ISE + // becomes a problem + throw new IllegalStateException(sm.getString("wsSession.removeHandlerFailed", listener)); + } + } + + + @Override + public String getProtocolVersion() { + checkState(); + return Constants.WS_VERSION_HEADER_VALUE; + } + + + @Override + public String getNegotiatedSubprotocol() { + checkState(); + return subProtocol; + } + + + @Override + public List getNegotiatedExtensions() { + checkState(); + return negotiatedExtensions; + } + + + @Override + public boolean isSecure() { + checkState(); + return secure; + } + + + @Override + public boolean isOpen() { + return state.get() == State.OPEN || state.get() == State.OUTPUT_CLOSING || state.get() == State.CLOSING; + } + + + public boolean isClosed() { + return state.get() == State.CLOSED; + } + + + @Override + public long getMaxIdleTimeout() { + checkState(); + return maxIdleTimeout; + } + + + @Override + public void setMaxIdleTimeout(long timeout) { + checkState(); + this.maxIdleTimeout = timeout; + } + + + @Override + public void setMaxBinaryMessageBufferSize(int max) { + checkState(); + this.maxBinaryMessageBufferSize = max; + } + + + @Override + public int getMaxBinaryMessageBufferSize() { + checkState(); + return maxBinaryMessageBufferSize; + } + + + @Override + public void setMaxTextMessageBufferSize(int max) { + checkState(); + this.maxTextMessageBufferSize = max; + } + + + @Override + public int getMaxTextMessageBufferSize() { + checkState(); + return maxTextMessageBufferSize; + } + + + @Override + public Set getOpenSessions() { + checkState(); + return webSocketContainer.getOpenSessions(getSessionMapKey()); + } + + + @Override + public RemoteEndpoint.Async getAsyncRemote() { + checkState(); + return remoteEndpointAsync; + } + + + @Override + public RemoteEndpoint.Basic getBasicRemote() { + checkState(); + return remoteEndpointBasic; + } + + + @Override + public void close() throws IOException { + close(new CloseReason(CloseCodes.NORMAL_CLOSURE, "")); + } + + + @Override + public void close(CloseReason closeReason) throws IOException { + doClose(closeReason, closeReason); + } + + + /** + * WebSocket 1.0. Section 2.1.5. Need internal close method as spec requires that the local endpoint receives a 1006 + * on timeout. + * + * @param closeReasonMessage The close reason to pass to the remote endpoint + * @param closeReasonLocal The close reason to pass to the local endpoint + */ + public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal) { + doClose(closeReasonMessage, closeReasonLocal, false); + } + + + /** + * WebSocket 1.0. Section 2.1.5. Need internal close method as spec requires that the local endpoint receives a 1006 + * on timeout. + * + * @param closeReasonMessage The close reason to pass to the remote endpoint + * @param closeReasonLocal The close reason to pass to the local endpoint + * @param closeSocket Should the socket be closed immediately rather than waiting for the server to respond + */ + public void doClose(CloseReason closeReasonMessage, CloseReason closeReasonLocal, boolean closeSocket) { + + if (!state.compareAndSet(State.OPEN, State.OUTPUT_CLOSING)) { + // Close process has already been started. Don't start it again. + return; + } + + if (log.isTraceEnabled()) { + log.trace(sm.getString("wsSession.doClose", id)); + } + + // Flush any batched messages not yet sent. + try { + wsRemoteEndpoint.setBatchingAllowed(false); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("wsSession.flushFailOnClose"), t); + fireEndpointOnError(t); + } + + // Send the close message to the remote endpoint. + sendCloseMessage(closeReasonMessage); + fireEndpointOnClose(closeReasonLocal); + if (!state.compareAndSet(State.OUTPUT_CLOSING, State.OUTPUT_CLOSED) || closeSocket) { + /* + * A close message was received in another thread or this is handling an error condition. Either way, no + * further close message is expected to be received. Mark the session as fully closed... + */ + state.set(State.CLOSED); + // ... and close the network connection. + closeConnection(); + } else { + /* + * Set close timeout. If the client fails to send a close message response within the timeout, the session + * and the connection will be closed when the timeout expires. + */ + sessionCloseTimeoutExpiry = + Long.valueOf(System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(getSessionCloseTimeout())); + } + + // Fail any uncompleted messages. + IOException ioe = new IOException(sm.getString("wsSession.messageFailed")); + SendResult sr = new SendResult(ioe); + for (FutureToSendHandler f2sh : futures.keySet()) { + f2sh.onResult(sr); + } + } + + + /** + * Called when a close message is received. Should only ever happen once. Also called after a protocol error when + * the ProtocolHandler needs to force the closing of the connection. + * + * @param closeReason The reason contained within the received close message. + */ + public void onClose(CloseReason closeReason) { + if (state.compareAndSet(State.OPEN, State.CLOSING)) { + // Standard close. + + // Flush any batched messages not yet sent. + try { + wsRemoteEndpoint.setBatchingAllowed(false); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("wsSession.flushFailOnClose"), t); + fireEndpointOnError(t); + } + + // Send the close message response to the remote endpoint. + sendCloseMessage(closeReason); + fireEndpointOnClose(closeReason); + + // Mark the session as fully closed. + state.set(State.CLOSED); + + // Close the network connection. + closeConnection(); + } else if (state.compareAndSet(State.OUTPUT_CLOSING, State.CLOSING)) { + /* + * The local endpoint sent a close message the the same time as the remote endpoint. The local close is + * still being processed. Update the state so the the local close process will also close the network + * connection once it has finished sending a close message. + */ + } else if (state.compareAndSet(State.OUTPUT_CLOSED, State.CLOSED)) { + /* + * The local endpoint sent the first close message. The remote endpoint has now responded with its own close + * message so mark the session as fully closed and close the network connection. + */ + closeConnection(); + } + // CLOSING and CLOSED are NO-OPs + } + + + private void closeConnection() { + /* + * Close the network connection. + */ + wsRemoteEndpoint.close(); + /* + * Don't unregister the session until the connection is fully closed since webSocketContainer is responsible for + * tracking the session close timeout. + */ + webSocketContainer.unregisterSession(getSessionMapKey(), this); + } + + + /* + * Returns the session close timeout in milliseconds + */ + protected long getSessionCloseTimeout() { + long result = 0; + Object obj = userProperties.get(Constants.SESSION_CLOSE_TIMEOUT_PROPERTY); + if (obj instanceof Long) { + result = ((Long) obj).intValue(); + } + if (result <= 0) { + result = Constants.DEFAULT_SESSION_CLOSE_TIMEOUT; + } + return result; + } + + + protected void checkCloseTimeout() { + // Skip the check if no session close timeout has been set. + if (sessionCloseTimeoutExpiry != null) { + // Check if the timeout has expired. + if (System.nanoTime() - sessionCloseTimeoutExpiry.longValue() > 0) { + // Check if the session has been closed in another thread while the timeout was being processed. + if (state.compareAndSet(State.OUTPUT_CLOSED, State.CLOSED)) { + closeConnection(); + } + } + } + } + + + private void fireEndpointOnClose(CloseReason closeReason) { + + // Fire the onClose event + Throwable throwable = null; + InstanceManager instanceManager = getInstanceManager(); + Thread t = Thread.currentThread(); + ClassLoader cl = t.getContextClassLoader(); + t.setContextClassLoader(applicationClassLoader); + try { + localEndpoint.onClose(this, closeReason); + } catch (Throwable t1) { + ExceptionUtils.handleThrowable(t1); + throwable = t1; + } finally { + if (instanceManager != null) { + try { + instanceManager.destroyInstance(localEndpoint); + } catch (Throwable t2) { + ExceptionUtils.handleThrowable(t2); + if (throwable == null) { + throwable = t2; + } + } + } + t.setContextClassLoader(cl); + } + + if (throwable != null) { + fireEndpointOnError(throwable); + } + } + + + private void fireEndpointOnError(Throwable throwable) { + + // Fire the onError event + Thread t = Thread.currentThread(); + ClassLoader cl = t.getContextClassLoader(); + t.setContextClassLoader(applicationClassLoader); + try { + localEndpoint.onError(this, throwable); + } finally { + t.setContextClassLoader(cl); + } + } + + + private void sendCloseMessage(CloseReason closeReason) { + // 125 is maximum size for the payload of a control message + ByteBuffer msg = ByteBuffer.allocate(125); + CloseCode closeCode = closeReason.getCloseCode(); + // CLOSED_ABNORMALLY should not be put on the wire + if (closeCode == CloseCodes.CLOSED_ABNORMALLY) { + // PROTOCOL_ERROR is probably better than GOING_AWAY here + msg.putShort((short) CloseCodes.PROTOCOL_ERROR.getCode()); + } else { + msg.putShort((short) closeCode.getCode()); + } + + String reason = closeReason.getReasonPhrase(); + if (reason != null && reason.length() > 0) { + appendCloseReasonWithTruncation(msg, reason); + } + msg.flip(); + try { + wsRemoteEndpoint.sendMessageBlock(Constants.OPCODE_CLOSE, msg, true); + } catch (IOException | IllegalStateException e) { + // Failed to send close message. Close the socket and let the caller + // deal with the Exception + if (log.isDebugEnabled()) { + log.debug(sm.getString("wsSession.sendCloseFail", id), e); + } + closeConnection(); + // Failure to send a close message is not unexpected in the case of + // an abnormal closure (usually triggered by a failure to read/write + // from/to the client. In this case do not trigger the endpoint's + // error handling + if (closeCode != CloseCodes.CLOSED_ABNORMALLY) { + localEndpoint.onError(this, e); + } + } + } + + + private Object getSessionMapKey() { + if (endpointConfig instanceof ServerEndpointConfig) { + // Server + return ((ServerEndpointConfig) endpointConfig).getPath(); + } else { + // Client + return localEndpoint; + } + } + + /** + * Use protected so unit tests can access this method directly. + * + * @param msg The message + * @param reason The reason + */ + protected static void appendCloseReasonWithTruncation(ByteBuffer msg, String reason) { + // Once the close code has been added there are a maximum of 123 bytes + // left for the reason phrase. If it is truncated then care needs to be + // taken to ensure the bytes are not truncated in the middle of a + // multi-byte UTF-8 character. + byte[] reasonBytes = reason.getBytes(StandardCharsets.UTF_8); + + if (reasonBytes.length <= 123) { + // No need to truncate + msg.put(reasonBytes); + } else { + // Need to truncate + int remaining = 123 - ELLIPSIS_BYTES_LEN; + int pos = 0; + byte[] bytesNext = reason.substring(pos, pos + 1).getBytes(StandardCharsets.UTF_8); + while (remaining >= bytesNext.length) { + msg.put(bytesNext); + remaining -= bytesNext.length; + pos++; + bytesNext = reason.substring(pos, pos + 1).getBytes(StandardCharsets.UTF_8); + } + msg.put(ELLIPSIS_BYTES); + } + } + + + /** + * Make the session aware of a {@link FutureToSendHandler} that will need to be forcibly closed if the session + * closes before the {@link FutureToSendHandler} completes. + * + * @param f2sh The handler + */ + protected void registerFuture(FutureToSendHandler f2sh) { + // Ideally, this code should sync on stateLock so that the correct + // action is taken based on the current state of the connection. + // However, a sync on stateLock can't be used here as it will create the + // possibility of a dead-lock. See BZ 61183. + // Therefore, a slightly less efficient approach is used. + + // Always register the future. + futures.put(f2sh, f2sh); + + if (isOpen()) { + // The session is open. The future has been registered with the open + // session. Normal processing continues. + return; + } + + // The session is closing / closed. The future may or may not have been registered + // in time for it to be processed during session closure. + + if (f2sh.isDone()) { + // The future has completed. It is not known if the future was + // completed normally by the I/O layer or in error by doClose(). It + // doesn't matter which. There is nothing more to do here. + return; + } + + // The session is closing / closed. The Future had not completed when last checked. + // There is a small timing window that means the Future may have been + // completed since the last check. There is also the possibility that + // the Future was not registered in time to be cleaned up during session + // close. + // Attempt to complete the Future with an error result as this ensures + // that the Future completes and any client code waiting on it does not + // hang. It is slightly inefficient since the Future may have been + // completed in another thread or another thread may be about to + // complete the Future but knowing if this is the case requires the sync + // on stateLock (see above). + // Note: If multiple attempts are made to complete the Future, the + // second and subsequent attempts are ignored. + + IOException ioe = new IOException(sm.getString("wsSession.messageFailed")); + SendResult sr = new SendResult(ioe); + f2sh.onResult(sr); + } + + + /** + * Remove a {@link FutureToSendHandler} from the set of tracked instances. + * + * @param f2sh The handler + */ + protected void unregisterFuture(FutureToSendHandler f2sh) { + futures.remove(f2sh); + } + + + @Override + public URI getRequestURI() { + checkState(); + return requestUri; + } + + + @Override + public Map> getRequestParameterMap() { + checkState(); + return requestParameterMap; + } + + + @Override + public String getQueryString() { + checkState(); + return queryString; + } + + + @Override + public Principal getUserPrincipal() { + checkState(); + return getUserPrincipalInternal(); + } + + + public Principal getUserPrincipalInternal() { + return userPrincipal; + } + + + @Override + public Map getPathParameters() { + checkState(); + return pathParameters; + } + + + @Override + public String getId() { + return id; + } + + + @Override + public Map getUserProperties() { + checkState(); + return userProperties; + } + + + public Endpoint getLocal() { + return localEndpoint; + } + + + public String getHttpSessionId() { + return httpSessionId; + } + + + protected MessageHandler getTextMessageHandler() { + return textMessageHandler; + } + + + protected MessageHandler getBinaryMessageHandler() { + return binaryMessageHandler; + } + + + protected MessageHandler.Whole getPongMessageHandler() { + return pongMessageHandler; + } + + + protected void updateLastActiveRead() { + lastActiveRead = System.currentTimeMillis(); + } + + + protected void updateLastActiveWrite() { + lastActiveWrite = System.currentTimeMillis(); + } + + + protected void checkExpiration() { + // Local copies to ensure consistent behaviour during method execution + long timeout = maxIdleTimeout; + long timeoutRead = getMaxIdleTimeoutRead(); + long timeoutWrite = getMaxIdleTimeoutWrite(); + + long currentTime = System.currentTimeMillis(); + String key = null; + + if (timeoutRead > 0 && (currentTime - lastActiveRead) > timeoutRead) { + key = "wsSession.timeoutRead"; + } else if (timeoutWrite > 0 && (currentTime - lastActiveWrite) > timeoutWrite) { + key = "wsSession.timeoutWrite"; + } else if (timeout > 0 && (currentTime - lastActiveRead) > timeout && + (currentTime - lastActiveWrite) > timeout) { + key = "wsSession.timeout"; + } + + if (key != null) { + String msg = sm.getString(key, getId()); + if (log.isDebugEnabled()) { + log.debug(msg); + } + doClose(new CloseReason(CloseCodes.GOING_AWAY, msg), new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg)); + } + } + + + private long getMaxIdleTimeoutRead() { + Object timeout = userProperties.get(Constants.READ_IDLE_TIMEOUT_MS); + if (timeout instanceof Long) { + return ((Long) timeout).longValue(); + } + return 0; + } + + + private long getMaxIdleTimeoutWrite() { + Object timeout = userProperties.get(Constants.WRITE_IDLE_TIMEOUT_MS); + if (timeout instanceof Long) { + return ((Long) timeout).longValue(); + } + return 0; + } + + + private void checkState() { + if (isClosed()) { + /* + * As per RFC 6455, a WebSocket connection is considered to be closed once a peer has sent and received a + * WebSocket close frame. + */ + throw new IllegalStateException(sm.getString("wsSession.closed", id)); + } + } + + private enum State { + OPEN, + OUTPUT_CLOSING, + OUTPUT_CLOSED, + CLOSING, + CLOSED + } + + + private WsFrameBase wsFrame; + + void setWsFrame(WsFrameBase wsFrame) { + this.wsFrame = wsFrame; + } + + + /** + * Suspends the reading of the incoming messages. + */ + public void suspend() { + wsFrame.suspend(); + } + + + /** + * Resumes the reading of the incoming messages. + */ + public void resume() { + wsFrame.resume(); + } +} diff --git a/java/org/apache/tomcat/websocket/WsWebSocketContainer.java b/java/org/apache/tomcat/websocket/WsWebSocketContainer.java new file mode 100644 index 0000000..a5aa966 --- /dev/null +++ b/java/org/apache/tomcat/websocket/WsWebSocketContainer.java @@ -0,0 +1,1112 @@ +/* + * 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.tomcat.websocket; + +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannelGroup; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManagerFactory; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.Extension; +import jakarta.websocket.HandshakeResponse; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.InstanceManagerBindings; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.KeyStoreUtil; + +public class WsWebSocketContainer implements WebSocketContainer, BackgroundProcess { + + private static final StringManager sm = StringManager.getManager(WsWebSocketContainer.class); + private static final Random RANDOM = new Random(); + private static final byte[] CRLF = new byte[] { 13, 10 }; + + private static final byte[] GET_BYTES = "GET ".getBytes(StandardCharsets.ISO_8859_1); + private static final byte[] ROOT_URI_BYTES = "/".getBytes(StandardCharsets.ISO_8859_1); + private static final byte[] HTTP_VERSION_BYTES = " HTTP/1.1\r\n".getBytes(StandardCharsets.ISO_8859_1); + + private volatile AsynchronousChannelGroup asynchronousChannelGroup = null; + private final Object asynchronousChannelGroupLock = new Object(); + + private final Log log = LogFactory.getLog(WsWebSocketContainer.class); // must not be static + // Server side uses the endpoint path as the key + // Client side uses the client endpoint instance + private final Map> endpointSessionMap = new HashMap<>(); + private final Map sessions = new ConcurrentHashMap<>(); + private final Object endPointSessionMapLock = new Object(); + + private long defaultAsyncTimeout = -1; + private int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE; + private int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE; + private volatile long defaultMaxSessionIdleTimeout = 0; + private int backgroundProcessCount = 0; + private int processPeriod = Constants.DEFAULT_PROCESS_PERIOD; + + private InstanceManager instanceManager; + + protected InstanceManager getInstanceManager(ClassLoader classLoader) { + if (instanceManager != null) { + return instanceManager; + } + return InstanceManagerBindings.get(classLoader); + } + + protected void setInstanceManager(InstanceManager instanceManager) { + this.instanceManager = instanceManager; + } + + @Override + public Session connectToServer(Object pojo, URI path) throws DeploymentException { + ClientEndpointConfig config = createClientEndpointConfig(pojo.getClass()); + ClientEndpointHolder holder = new PojoHolder(pojo, config); + return connectToServerRecursive(holder, config, path, new HashSet<>()); + } + + + @Override + public Session connectToServer(Class annotatedEndpointClass, URI path) throws DeploymentException { + ClientEndpointConfig config = createClientEndpointConfig(annotatedEndpointClass); + ClientEndpointHolder holder = new PojoClassHolder(annotatedEndpointClass, config); + return connectToServerRecursive(holder, config, path, new HashSet<>()); + } + + + private ClientEndpointConfig createClientEndpointConfig(Class annotatedEndpointClass) + throws DeploymentException { + ClientEndpoint annotation = annotatedEndpointClass.getAnnotation(ClientEndpoint.class); + if (annotation == null) { + throw new DeploymentException( + sm.getString("wsWebSocketContainer.missingAnnotation", annotatedEndpointClass.getName())); + } + + Class configuratorClazz = annotation.configurator(); + + ClientEndpointConfig.Configurator configurator = null; + if (!ClientEndpointConfig.Configurator.class.equals(configuratorClazz)) { + try { + configurator = configuratorClazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.defaultConfiguratorFail"), e); + } + } + + ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create(); + // Avoid NPE when using RI API JAR - see BZ 56343 + if (configurator != null) { + builder.configurator(configurator); + } + ClientEndpointConfig config = builder.decoders(Arrays.asList(annotation.decoders())) + .encoders(Arrays.asList(annotation.encoders())) + .preferredSubprotocols(Arrays.asList(annotation.subprotocols())).build(); + + return config; + } + + + @Override + public Session connectToServer(Class clazz, ClientEndpointConfig clientEndpointConfiguration, + URI path) throws DeploymentException { + ClientEndpointHolder holder = new EndpointClassHolder(clazz); + return connectToServerRecursive(holder, clientEndpointConfiguration, path, new HashSet<>()); + } + + + @Override + public Session connectToServer(Endpoint endpoint, ClientEndpointConfig clientEndpointConfiguration, URI path) + throws DeploymentException { + ClientEndpointHolder holder = new EndpointHolder(endpoint); + return connectToServerRecursive(holder, clientEndpointConfiguration, path, new HashSet<>()); + } + + + private Session connectToServerRecursive(ClientEndpointHolder clientEndpointHolder, + ClientEndpointConfig clientEndpointConfiguration, URI path, Set redirectSet) + throws DeploymentException { + + if (log.isTraceEnabled()) { + log.trace(sm.getString("wsWebSocketContainer.connect.entry", clientEndpointHolder.getClassName(), path)); + } + + boolean secure = false; + ByteBuffer proxyConnect = null; + URI proxyPath; + + // Validate scheme (and build proxyPath) + String scheme = path.getScheme(); + if ("ws".equalsIgnoreCase(scheme)) { + proxyPath = URI.create("http" + path.toString().substring(2)); + } else if ("wss".equalsIgnoreCase(scheme)) { + proxyPath = URI.create("https" + path.toString().substring(3)); + secure = true; + } else { + throw new DeploymentException(sm.getString("wsWebSocketContainer.pathWrongScheme", scheme)); + } + + // Validate host + String host = path.getHost(); + if (host == null) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.pathNoHost")); + } + int port = path.getPort(); + + SocketAddress sa = null; + + // Check to see if a proxy is configured. Javadoc indicates return value + // will never be null + List proxies = ProxySelector.getDefault().select(proxyPath); + Proxy selectedProxy = null; + for (Proxy proxy : proxies) { + if (proxy.type().equals(Proxy.Type.HTTP)) { + sa = proxy.address(); + if (sa instanceof InetSocketAddress) { + InetSocketAddress inet = (InetSocketAddress) sa; + if (inet.isUnresolved()) { + sa = new InetSocketAddress(inet.getHostName(), inet.getPort()); + } + } + selectedProxy = proxy; + break; + } + } + + // If the port is not explicitly specified, compute it based on the + // scheme + if (port == -1) { + if ("ws".equalsIgnoreCase(scheme)) { + port = 80; + } else { + // Must be wss due to scheme validation above + port = 443; + } + } + + Map userProperties = clientEndpointConfiguration.getUserProperties(); + + // If sa is null, no proxy is configured so need to create sa + if (sa == null) { + sa = new InetSocketAddress(host, port); + } else { + proxyConnect = createProxyRequest(host, port, + (String) userProperties.get(Constants.PROXY_AUTHORIZATION_HEADER_NAME)); + } + + // Create the initial HTTP request to open the WebSocket connection + Map> reqHeaders = createRequestHeaders(host, port, secure, clientEndpointConfiguration); + clientEndpointConfiguration.getConfigurator().beforeRequest(reqHeaders); + if (Constants.DEFAULT_ORIGIN_HEADER_VALUE != null && !reqHeaders.containsKey(Constants.ORIGIN_HEADER_NAME)) { + List originValues = new ArrayList<>(1); + originValues.add(Constants.DEFAULT_ORIGIN_HEADER_VALUE); + reqHeaders.put(Constants.ORIGIN_HEADER_NAME, originValues); + } + ByteBuffer request = createRequest(path, reqHeaders); + + // Get the connection timeout + long timeout = Constants.IO_TIMEOUT_MS_DEFAULT; + String timeoutValue = (String) userProperties.get(Constants.IO_TIMEOUT_MS_PROPERTY); + if (timeoutValue != null) { + timeout = Long.valueOf(timeoutValue).intValue(); + } + + AsynchronousSocketChannel socketChannel; + try { + socketChannel = AsynchronousSocketChannel.open(getAsynchronousChannelGroup()); + } catch (IOException ioe) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.asynchronousSocketChannelFail"), ioe); + } + + // Set-up + // Same size as the WsFrame input buffer + ByteBuffer response = ByteBuffer.allocate(getDefaultMaxBinaryMessageBufferSize()); + String subProtocol; + boolean success = false; + List extensionsAgreed = new ArrayList<>(); + Transformation transformation = null; + AsyncChannelWrapper channel = null; + + try { + // Open the connection + Future fConnect = socketChannel.connect(sa); + + if (proxyConnect != null) { + fConnect.get(timeout, TimeUnit.MILLISECONDS); + // Proxy CONNECT is clear text + channel = new AsyncChannelWrapperNonSecure(socketChannel); + writeRequest(channel, proxyConnect, timeout); + HttpResponse httpResponse = processResponse(response, channel, timeout); + if (httpResponse.status == Constants.PROXY_AUTHENTICATION_REQUIRED) { + return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, path, + redirectSet, userProperties, request, httpResponse, AuthenticationType.PROXY); + } else if (httpResponse.getStatus() != 200) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.proxyConnectFail", selectedProxy, + Integer.toString(httpResponse.getStatus()))); + } + } + + if (secure) { + // Regardless of whether a non-secure wrapper was created for a + // proxy CONNECT, need to use TLS from this point on so wrap the + // original AsynchronousSocketChannel + SSLEngine sslEngine = createSSLEngine(clientEndpointConfiguration, host, port); + channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine); + } else if (channel == null) { + // Only need to wrap as this point if it wasn't wrapped to process a + // proxy CONNECT + channel = new AsyncChannelWrapperNonSecure(socketChannel); + } + + fConnect.get(timeout, TimeUnit.MILLISECONDS); + + Future fHandshake = channel.handshake(); + fHandshake.get(timeout, TimeUnit.MILLISECONDS); + + if (log.isTraceEnabled()) { + SocketAddress localAddress = null; + try { + localAddress = channel.getLocalAddress(); + } catch (IOException ioe) { + // Ignore + } + log.trace(sm.getString("wsWebSocketContainer.connect.write", Integer.valueOf(request.position()), + Integer.valueOf(request.limit()), localAddress)); + } + writeRequest(channel, request, timeout); + + HttpResponse httpResponse = processResponse(response, channel, timeout); + + // Check maximum permitted redirects + int maxRedirects = Constants.MAX_REDIRECTIONS_DEFAULT; + String maxRedirectsValue = (String) userProperties.get(Constants.MAX_REDIRECTIONS_PROPERTY); + if (maxRedirectsValue != null) { + maxRedirects = Integer.parseInt(maxRedirectsValue); + } + + if (httpResponse.status != 101) { + if (isRedirectStatus(httpResponse.status)) { + List locationHeader = httpResponse.getHandshakeResponse().getHeaders() + .get(Constants.LOCATION_HEADER_NAME); + + if (locationHeader == null || locationHeader.isEmpty() || locationHeader.get(0) == null || + locationHeader.get(0).isEmpty()) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.missingLocationHeader", + Integer.toString(httpResponse.status))); + } + + URI redirectLocation = URI.create(locationHeader.get(0)).normalize(); + + if (!redirectLocation.isAbsolute()) { + redirectLocation = path.resolve(redirectLocation); + } + + String redirectScheme = redirectLocation.getScheme().toLowerCase(); + + if (redirectScheme.startsWith("http")) { + redirectLocation = new URI(redirectScheme.replace("http", "ws"), redirectLocation.getUserInfo(), + redirectLocation.getHost(), redirectLocation.getPort(), redirectLocation.getPath(), + redirectLocation.getQuery(), redirectLocation.getFragment()); + } + + if (!redirectSet.add(redirectLocation) || redirectSet.size() > maxRedirects) { + throw new DeploymentException( + sm.getString("wsWebSocketContainer.redirectThreshold", redirectLocation, + Integer.toString(redirectSet.size()), Integer.toString(maxRedirects))); + } + + return connectToServerRecursive(clientEndpointHolder, clientEndpointConfiguration, redirectLocation, + redirectSet); + + } else if (httpResponse.status == Constants.UNAUTHORIZED) { + return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, path, + redirectSet, userProperties, request, httpResponse, AuthenticationType.WWW); + + } else { + throw new DeploymentException( + sm.getString("wsWebSocketContainer.invalidStatus", Integer.toString(httpResponse.status))); + } + } + HandshakeResponse handshakeResponse = httpResponse.getHandshakeResponse(); + clientEndpointConfiguration.getConfigurator().afterResponse(handshakeResponse); + + // Sub-protocol + List protocolHeaders = handshakeResponse.getHeaders().get(Constants.WS_PROTOCOL_HEADER_NAME); + if (protocolHeaders == null || protocolHeaders.size() == 0) { + subProtocol = null; + } else if (protocolHeaders.size() == 1) { + subProtocol = protocolHeaders.get(0); + } else { + throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidSubProtocol")); + } + + // Extensions + // Should normally only be one header but handle the case of + // multiple headers + List extHeaders = handshakeResponse.getHeaders().get(Constants.WS_EXTENSIONS_HEADER_NAME); + if (extHeaders != null) { + for (String extHeader : extHeaders) { + Util.parseExtensionHeader(extensionsAgreed, extHeader); + } + } + + // Build the transformations + TransformationFactory factory = TransformationFactory.getInstance(); + for (Extension extension : extensionsAgreed) { + List> wrapper = new ArrayList<>(1); + wrapper.add(extension.getParameters()); + Transformation t = factory.create(extension.getName(), wrapper, false); + if (t == null) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidExtensionParameters")); + } + if (transformation == null) { + transformation = t; + } else { + transformation.setNext(t); + } + } + + success = true; + } catch (ExecutionException | InterruptedException | SSLException | EOFException | TimeoutException + | URISyntaxException | AuthenticationException e) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.httpRequestFailed", path), e); + } finally { + if (!success) { + if (channel != null) { + channel.close(); + } else { + try { + socketChannel.close(); + } catch (IOException ioe) { + // Ignore + } + } + } + } + + // Switch to WebSocket + WsRemoteEndpointImplClient wsRemoteEndpointClient = new WsRemoteEndpointImplClient(channel); + + WsSession wsSession = new WsSession(clientEndpointHolder, wsRemoteEndpointClient, this, extensionsAgreed, + subProtocol, Collections.emptyMap(), secure, clientEndpointConfiguration); + + WsFrameClient wsFrameClient = new WsFrameClient(response, channel, wsSession, transformation); + // WsFrame adds the necessary final transformations. Copy the + // completed transformation chain to the remote end point. + wsRemoteEndpointClient.setTransformation(wsFrameClient.getTransformation()); + + wsSession.getLocal().onOpen(wsSession, clientEndpointConfiguration); + registerSession(wsSession.getLocal(), wsSession); + + /* + * It is possible that the server sent one or more messages as soon as the WebSocket connection was established. + * Depending on the exact timing of when those messages were sent they could be sat in the input buffer waiting + * to be read and will not trigger a "data available to read" event. Therefore, it is necessary to process the + * input buffer here. Note that this happens on the current thread which means that this thread will be used for + * any onMessage notifications. This is a special case. Subsequent "data available to read" events will be + * handled by threads from the AsyncChannelGroup's executor. + */ + wsFrameClient.startInputProcessing(); + + return wsSession; + } + + + private Session processAuthenticationChallenge(ClientEndpointHolder clientEndpointHolder, + ClientEndpointConfig clientEndpointConfiguration, URI path, Set redirectSet, + Map userProperties, ByteBuffer request, HttpResponse httpResponse, + AuthenticationType authenticationType) throws DeploymentException, AuthenticationException { + + if (userProperties.get(authenticationType.getAuthorizationHeaderName()) != null) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.failedAuthentication", + Integer.valueOf(httpResponse.status), authenticationType.getAuthorizationHeaderName())); + } + + List authenticateHeaders = httpResponse.getHandshakeResponse().getHeaders() + .get(authenticationType.getAuthenticateHeaderName()); + + if (authenticateHeaders == null || authenticateHeaders.isEmpty() || authenticateHeaders.get(0) == null || + authenticateHeaders.get(0).isEmpty()) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.missingAuthenticateHeader", + Integer.toString(httpResponse.status), authenticationType.getAuthenticateHeaderName())); + } + + String authScheme = authenticateHeaders.get(0).split("\\s+", 2)[0]; + + Authenticator auth = AuthenticatorFactory.getAuthenticator(authScheme); + + if (auth == null) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.unsupportedAuthScheme", + Integer.valueOf(httpResponse.status), authScheme)); + } + + String requestUri = new String(request.array(), StandardCharsets.ISO_8859_1).split("\\s", 3)[1]; + + userProperties.put(authenticationType.getAuthorizationHeaderName(), + auth.getAuthorization(requestUri, authenticateHeaders.get(0), + (String) userProperties.get(authenticationType.getUserNameProperty()), + (String) userProperties.get(authenticationType.getUserPasswordProperty()), + (String) userProperties.get(authenticationType.getUserRealmProperty()))); + + return connectToServerRecursive(clientEndpointHolder, clientEndpointConfiguration, path, redirectSet); + } + + + private static void writeRequest(AsyncChannelWrapper channel, ByteBuffer request, long timeout) + throws TimeoutException, InterruptedException, ExecutionException { + int toWrite = request.limit(); + + Future fWrite = channel.write(request); + Integer thisWrite = fWrite.get(timeout, TimeUnit.MILLISECONDS); + toWrite -= thisWrite.intValue(); + + while (toWrite > 0) { + fWrite = channel.write(request); + thisWrite = fWrite.get(timeout, TimeUnit.MILLISECONDS); + toWrite -= thisWrite.intValue(); + } + } + + + private static boolean isRedirectStatus(int httpResponseCode) { + + boolean isRedirect = false; + + switch (httpResponseCode) { + case Constants.MULTIPLE_CHOICES: + case Constants.MOVED_PERMANENTLY: + case Constants.FOUND: + case Constants.SEE_OTHER: + case Constants.USE_PROXY: + case Constants.TEMPORARY_REDIRECT: + isRedirect = true; + break; + default: + break; + } + + return isRedirect; + } + + + private static ByteBuffer createProxyRequest(String host, int port, String authorizationHeader) { + StringBuilder request = new StringBuilder(); + request.append("CONNECT "); + request.append(host); + request.append(':'); + request.append(port); + + request.append(" HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keepalive\r\nHost: "); + request.append(host); + request.append(':'); + request.append(port); + + if (authorizationHeader != null) { + request.append("\r\n"); + request.append(Constants.PROXY_AUTHORIZATION_HEADER_NAME); + request.append(':'); + request.append(authorizationHeader); + } + + request.append("\r\n\r\n"); + + byte[] bytes = request.toString().getBytes(StandardCharsets.ISO_8859_1); + return ByteBuffer.wrap(bytes); + } + + protected void registerSession(Object key, WsSession wsSession) { + + if (!wsSession.isOpen()) { + // The session was closed during onOpen. No need to register it. + return; + } + synchronized (endPointSessionMapLock) { + if (endpointSessionMap.size() == 0) { + BackgroundProcessManager.getInstance().register(this); + } + endpointSessionMap.computeIfAbsent(key, k -> new HashSet<>()).add(wsSession); + } + sessions.put(wsSession, wsSession); + } + + + protected void unregisterSession(Object key, WsSession wsSession) { + + synchronized (endPointSessionMapLock) { + Set wsSessions = endpointSessionMap.get(key); + if (wsSessions != null) { + wsSessions.remove(wsSession); + if (wsSessions.size() == 0) { + endpointSessionMap.remove(key); + } + } + if (endpointSessionMap.size() == 0) { + BackgroundProcessManager.getInstance().unregister(this); + } + } + sessions.remove(wsSession); + } + + + Set getOpenSessions(Object key) { + HashSet result = new HashSet<>(); + synchronized (endPointSessionMapLock) { + Set sessions = endpointSessionMap.get(key); + if (sessions != null) { + // Some sessions may be in the process of closing + for (WsSession session : sessions) { + if (session.isOpen()) { + result.add(session); + } + } + } + } + return result; + } + + private static Map> createRequestHeaders(String host, int port, boolean secure, + ClientEndpointConfig clientEndpointConfiguration) { + + Map> headers = new HashMap<>(); + List extensions = clientEndpointConfiguration.getExtensions(); + List subProtocols = clientEndpointConfiguration.getPreferredSubprotocols(); + Map userProperties = clientEndpointConfiguration.getUserProperties(); + + if (userProperties.get(Constants.AUTHORIZATION_HEADER_NAME) != null) { + List authValues = new ArrayList<>(1); + authValues.add((String) userProperties.get(Constants.AUTHORIZATION_HEADER_NAME)); + headers.put(Constants.AUTHORIZATION_HEADER_NAME, authValues); + } + + // Host header + List hostValues = new ArrayList<>(1); + if (port == 80 && !secure || port == 443 && secure) { + // Default ports. Do not include port in host header + hostValues.add(host); + } else { + hostValues.add(host + ':' + port); + } + + headers.put(Constants.HOST_HEADER_NAME, hostValues); + + // Upgrade header + List upgradeValues = new ArrayList<>(1); + upgradeValues.add(Constants.UPGRADE_HEADER_VALUE); + headers.put(Constants.UPGRADE_HEADER_NAME, upgradeValues); + + // Connection header + List connectionValues = new ArrayList<>(1); + connectionValues.add(Constants.CONNECTION_HEADER_VALUE); + headers.put(Constants.CONNECTION_HEADER_NAME, connectionValues); + + // WebSocket version header + List wsVersionValues = new ArrayList<>(1); + wsVersionValues.add(Constants.WS_VERSION_HEADER_VALUE); + headers.put(Constants.WS_VERSION_HEADER_NAME, wsVersionValues); + + // WebSocket key + List wsKeyValues = new ArrayList<>(1); + wsKeyValues.add(generateWsKeyValue()); + headers.put(Constants.WS_KEY_HEADER_NAME, wsKeyValues); + + // WebSocket sub-protocols + if (subProtocols != null && subProtocols.size() > 0) { + headers.put(Constants.WS_PROTOCOL_HEADER_NAME, subProtocols); + } + + // WebSocket extensions + if (extensions != null && extensions.size() > 0) { + headers.put(Constants.WS_EXTENSIONS_HEADER_NAME, generateExtensionHeaders(extensions)); + } + + return headers; + } + + + private static List generateExtensionHeaders(List extensions) { + List result = new ArrayList<>(extensions.size()); + for (Extension extension : extensions) { + StringBuilder header = new StringBuilder(); + header.append(extension.getName()); + for (Extension.Parameter param : extension.getParameters()) { + header.append(';'); + header.append(param.getName()); + String value = param.getValue(); + if (value != null && value.length() > 0) { + header.append('='); + header.append(value); + } + } + result.add(header.toString()); + } + return result; + } + + + private static String generateWsKeyValue() { + byte[] keyBytes = new byte[16]; + RANDOM.nextBytes(keyBytes); + return Base64.encodeBase64String(keyBytes); + } + + + private static ByteBuffer createRequest(URI uri, Map> reqHeaders) { + ByteBuffer result = ByteBuffer.allocate(4 * 1024); + + // Request line + result.put(GET_BYTES); + final String path = uri.getPath(); + if (null == path || path.isEmpty()) { + result.put(ROOT_URI_BYTES); + } else { + result.put(uri.getRawPath().getBytes(StandardCharsets.ISO_8859_1)); + } + String query = uri.getRawQuery(); + if (query != null) { + result.put((byte) '?'); + result.put(query.getBytes(StandardCharsets.ISO_8859_1)); + } + result.put(HTTP_VERSION_BYTES); + + // Headers + for (Entry> entry : reqHeaders.entrySet()) { + result = addHeader(result, entry.getKey(), entry.getValue()); + } + + // Terminating CRLF + result.put(CRLF); + + result.flip(); + + return result; + } + + + private static ByteBuffer addHeader(ByteBuffer result, String key, List values) { + if (values.isEmpty()) { + return result; + } + + result = putWithExpand(result, key.getBytes(StandardCharsets.ISO_8859_1)); + result = putWithExpand(result, ": ".getBytes(StandardCharsets.ISO_8859_1)); + result = putWithExpand(result, StringUtils.join(values).getBytes(StandardCharsets.ISO_8859_1)); + result = putWithExpand(result, CRLF); + + return result; + } + + + private static ByteBuffer putWithExpand(ByteBuffer input, byte[] bytes) { + if (bytes.length > input.remaining()) { + int newSize; + if (bytes.length > input.capacity()) { + newSize = 2 * bytes.length; + } else { + newSize = input.capacity() * 2; + } + ByteBuffer expanded = ByteBuffer.allocate(newSize); + input.flip(); + expanded.put(input); + input = expanded; + } + return input.put(bytes); + } + + + /** + * Process response, blocking until HTTP response has been fully received. + * + * @throws ExecutionException if there is an exception reading the response + * @throws InterruptedException if the thread is interrupted while reading the response + * @throws DeploymentException if the response status line is not correctly formatted + * @throws TimeoutException if the response was not read within the expected timeout + */ + private HttpResponse processResponse(ByteBuffer response, AsyncChannelWrapper channel, long timeout) + throws InterruptedException, ExecutionException, DeploymentException, EOFException, TimeoutException { + + Map> headers = new CaseInsensitiveKeyMap<>(); + + int status = 0; + boolean readStatus = false; + boolean readHeaders = false; + String line = null; + while (!readHeaders) { + // On entering loop buffer will be empty and at the start of a new + // loop the buffer will have been fully read. + response.clear(); + // Blocking read + Future read = channel.read(response); + Integer bytesRead; + try { + bytesRead = read.get(timeout, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + TimeoutException te = new TimeoutException( + sm.getString("wsWebSocketContainer.responseFail", Integer.toString(status), headers)); + te.initCause(e); + throw te; + } + if (bytesRead.intValue() == -1) { + throw new EOFException( + sm.getString("wsWebSocketContainer.responseFail", Integer.toString(status), headers)); + } + response.flip(); + while (response.hasRemaining() && !readHeaders) { + if (line == null) { + line = readLine(response); + } else { + line += readLine(response); + } + if ("\r\n".equals(line)) { + readHeaders = true; + } else if (line.endsWith("\r\n")) { + if (readStatus) { + parseHeaders(line, headers); + } else { + status = parseStatus(line); + readStatus = true; + } + line = null; + } + } + } + + return new HttpResponse(status, new WsHandshakeResponse(headers)); + } + + + private int parseStatus(String line) throws DeploymentException { + // This client only understands HTTP 1. + // RFC2616 is case specific + String[] parts = line.trim().split(" "); + // CONNECT for proxy may return a 1.0 response + if (parts.length < 2 || !("HTTP/1.0".equals(parts[0]) || "HTTP/1.1".equals(parts[0]))) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidStatus", line)); + } + try { + return Integer.parseInt(parts[1]); + } catch (NumberFormatException nfe) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.invalidStatus", line)); + } + } + + + private void parseHeaders(String line, Map> headers) { + // Treat headers as single values by default. + + int index = line.indexOf(':'); + if (index == -1) { + log.warn(sm.getString("wsWebSocketContainer.invalidHeader", line)); + return; + } + // Header names are case insensitive so always use lower case + String headerName = line.substring(0, index).trim().toLowerCase(Locale.ENGLISH); + // Multi-value headers are stored as a single header and the client is + // expected to handle splitting into individual values + String headerValue = line.substring(index + 1).trim(); + + List values = headers.computeIfAbsent(headerName, k -> new ArrayList<>(1)); + values.add(headerValue); + } + + private String readLine(ByteBuffer response) { + // All ISO-8859-1 + StringBuilder sb = new StringBuilder(); + + char c = 0; + while (response.hasRemaining()) { + c = (char) response.get(); + sb.append(c); + if (c == 10) { + break; + } + } + + return sb.toString(); + } + + + @SuppressWarnings("removal") + private SSLEngine createSSLEngine(ClientEndpointConfig clientEndpointConfig, String host, int port) + throws DeploymentException { + + Map userProperties = clientEndpointConfig.getUserProperties(); + try { + // See if a custom SSLContext has been provided + SSLContext sslContext = clientEndpointConfig.getSSLContext(); + + // If no SSLContext is found, try the pre WebSocket 2.1 Tomcat + // specific method + if (sslContext == null) { + sslContext = (SSLContext) userProperties.get(Constants.SSL_CONTEXT_PROPERTY); + } + + if (sslContext == null) { + // Create the SSL Context + sslContext = SSLContext.getInstance("TLS"); + + // Trust store + String sslTrustStoreValue = (String) userProperties.get(Constants.SSL_TRUSTSTORE_PROPERTY); + if (sslTrustStoreValue != null) { + String sslTrustStorePwdValue = (String) userProperties.get(Constants.SSL_TRUSTSTORE_PWD_PROPERTY); + if (sslTrustStorePwdValue == null) { + sslTrustStorePwdValue = Constants.SSL_TRUSTSTORE_PWD_DEFAULT; + } + + File keyStoreFile = new File(sslTrustStoreValue); + KeyStore ks = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(keyStoreFile)) { + KeyStoreUtil.load(ks, is, sslTrustStorePwdValue.toCharArray()); + } + + TrustManagerFactory tmf = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + + sslContext.init(null, tmf.getTrustManagers(), null); + } else { + sslContext.init(null, null, null); + } + } + + SSLEngine engine = sslContext.createSSLEngine(host, port); + + String sslProtocolsValue = (String) userProperties.get(Constants.SSL_PROTOCOLS_PROPERTY); + if (sslProtocolsValue != null) { + engine.setEnabledProtocols(sslProtocolsValue.split(",")); + } + + engine.setUseClientMode(true); + + // Enable host verification + // Start with current settings (returns a copy) + SSLParameters sslParams = engine.getSSLParameters(); + // Use HTTPS since WebSocket starts over HTTP(S) + sslParams.setEndpointIdentificationAlgorithm("HTTPS"); + // Write the parameters back + engine.setSSLParameters(sslParams); + + return engine; + } catch (Exception e) { + throw new DeploymentException(sm.getString("wsWebSocketContainer.sslEngineFail"), e); + } + } + + + @Override + public long getDefaultMaxSessionIdleTimeout() { + return defaultMaxSessionIdleTimeout; + } + + + @Override + public void setDefaultMaxSessionIdleTimeout(long timeout) { + this.defaultMaxSessionIdleTimeout = timeout; + } + + + @Override + public int getDefaultMaxBinaryMessageBufferSize() { + return maxBinaryMessageBufferSize; + } + + + @Override + public void setDefaultMaxBinaryMessageBufferSize(int max) { + maxBinaryMessageBufferSize = max; + } + + + @Override + public int getDefaultMaxTextMessageBufferSize() { + return maxTextMessageBufferSize; + } + + + @Override + public void setDefaultMaxTextMessageBufferSize(int max) { + maxTextMessageBufferSize = max; + } + + + /** + * {@inheritDoc} Currently, this implementation does not support any extensions. + */ + @Override + public Set getInstalledExtensions() { + return Collections.emptySet(); + } + + + /** + * {@inheritDoc} The default value for this implementation is -1. + */ + @Override + public long getDefaultAsyncSendTimeout() { + return defaultAsyncTimeout; + } + + + /** + * {@inheritDoc} The default value for this implementation is -1. + */ + @Override + public void setAsyncSendTimeout(long timeout) { + this.defaultAsyncTimeout = timeout; + } + + + /** + * Cleans up the resources still in use by WebSocket sessions created from this container. This includes closing + * sessions and cancelling {@link Future}s associated with blocking read/writes. + */ + public void destroy() { + CloseReason cr = new CloseReason(CloseCodes.GOING_AWAY, sm.getString("wsWebSocketContainer.shutdown")); + + for (WsSession session : sessions.keySet()) { + try { + session.close(cr); + } catch (IOException ioe) { + log.debug(sm.getString("wsWebSocketContainer.sessionCloseFail", session.getId()), ioe); + } + } + + // Only unregister with AsyncChannelGroupUtil if this instance + // registered with it + if (asynchronousChannelGroup != null) { + synchronized (asynchronousChannelGroupLock) { + if (asynchronousChannelGroup != null) { + AsyncChannelGroupUtil.unregister(); + asynchronousChannelGroup = null; + } + } + } + } + + + private AsynchronousChannelGroup getAsynchronousChannelGroup() { + // Use AsyncChannelGroupUtil to share a common group amongst all + // WebSocket clients + AsynchronousChannelGroup result = asynchronousChannelGroup; + if (result == null) { + synchronized (asynchronousChannelGroupLock) { + if (asynchronousChannelGroup == null) { + asynchronousChannelGroup = AsyncChannelGroupUtil.register(); + } + result = asynchronousChannelGroup; + } + } + return result; + } + + + // ----------------------------------------------- BackgroundProcess methods + + @Override + public void backgroundProcess() { + // This method gets called once a second. + backgroundProcessCount++; + if (backgroundProcessCount >= processPeriod) { + backgroundProcessCount = 0; + + // Check all registered sessions. + for (WsSession wsSession : sessions.keySet()) { + wsSession.checkExpiration(); + wsSession.checkCloseTimeout(); + } + } + + } + + + @Override + public void setProcessPeriod(int period) { + this.processPeriod = period; + } + + + /** + * {@inheritDoc} The default value is 10 which means session expirations are processed every 10 seconds. + */ + @Override + public int getProcessPeriod() { + return processPeriod; + } + + + private static class HttpResponse { + private final int status; + private final HandshakeResponse handshakeResponse; + + HttpResponse(int status, HandshakeResponse handshakeResponse) { + this.status = status; + this.handshakeResponse = handshakeResponse; + } + + + public int getStatus() { + return status; + } + + + public HandshakeResponse getHandshakeResponse() { + return handshakeResponse; + } + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/Constants.java b/java/org/apache/tomcat/websocket/pojo/Constants.java new file mode 100644 index 0000000..1eba531 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/Constants.java @@ -0,0 +1,29 @@ +/* + * 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.tomcat.websocket.pojo; + +/** + * Internal implementation constants. + */ +public class Constants { + + public static final String POJO_METHOD_MAPPING_KEY = "org.apache.tomcat.websocket.pojo.PojoEndpoint.methodMapping"; + + private Constants() { + // Hide default constructor + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties new file mode 100644 index 0000000..afefa8c --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties @@ -0,0 +1,45 @@ +# 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. + +pojoEndpointBase.closeSessionFail=Failed to close WebSocket session during error handling +pojoEndpointBase.onCloseFail=Failed to call onClose method of POJO end point for POJO of type [{0}] +pojoEndpointBase.onError=No error handling configured for [{0}] and the following error occurred +pojoEndpointBase.onErrorFail=Failed to call onError method of POJO end point for POJO of type [{0}] +pojoEndpointBase.onOpenFail=Failed to call onOpen method of POJO end point for POJO of type [{0}] + +pojoMessageHandlerWhole.decodeIoFail=IO error while decoding message +pojoMessageHandlerWhole.maxBufferSize=The maximum supported message size for this implementation is Integer.MAX_VALUE + +pojoMessageHandlerWholeBase.decodeDestoryFailed=Failed to destroy the decoder of type [{0}] + +pojoMethodMapping.decodePathParamFail=Failed to decode path parameter value [{0}] to expected type [{1}] +pojoMethodMapping.duplicateAnnotation=Duplicate annotations [{0}] present on class [{1}] +pojoMethodMapping.duplicateLastParam=Multiple boolean (last) parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.duplicateMessageParam=Multiple message parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.duplicatePongMessageParam=Multiple PongMessage parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.duplicateSessionParam=Multiple session parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.invalidDecoder=The specified decoder of type [{0}] could not be instantiated +pojoMethodMapping.methodNotPublic=The annotated method [{0}] is not public +pojoMethodMapping.noDecoder=No decoder was found for message parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.noPayload=No payload parameter present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.onErrorNoThrowable=No Throwable parameter was present on the method [{0}] of class [{1}] that was annotated with OnError +pojoMethodMapping.paramWithoutAnnotation=A parameter of type [{0}] was found on method[{1}] of class [{2}] that did not have a @PathParam annotation +pojoMethodMapping.partialInputStream=Invalid InputStream and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.partialObject=Invalid Object and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.partialPong=Invalid PongMessage and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.partialReader=Invalid Reader and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage +pojoMethodMapping.pongWithPayload=Invalid PongMessage and Message parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage + +pojoPathParam.wrongType=The type [{0}] is not permitted as a path parameter. Parameters annotated with @PathParam may only be Strings, Java primitives or a boxed version thereof. diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_cs.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_cs.properties new file mode 100644 index 0000000..f0bd452 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_cs.properties @@ -0,0 +1,18 @@ +# 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. + +pojoMethodMapping.duplicateAnnotation=Duplicitní anotace [{0}] přítomná na třídÄ› [{1}] +pojoMethodMapping.duplicatePongMessageParam=Více PongMessage parametrů pÅ™itomnýcn na metodÄ› [{0}] třídy [{1}], která byla oznaÄena anotací OnMessage +pojoMethodMapping.invalidDecoder=Nelze vytvoÅ™it instanci specifikovaného dekodéru typu [{0}] diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_de.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_de.properties new file mode 100644 index 0000000..2bdc86e --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_de.properties @@ -0,0 +1,20 @@ +# 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. + +pojoMethodMapping.duplicateAnnotation=Doppelte Annotation [{0}] an Klasse [{1}] gefunden +pojoMethodMapping.duplicateLastParam=Es sind mehrere Boolean (letzte) Parameter für die Methode [{0}] der Klasse [{1}], die mit OnMessage annotiert ist, vorhanden. +pojoMethodMapping.invalidDecoder=Der angegeben Dekoder vom Typ [{0}] konnte nicht instanziiert werden +pojoMethodMapping.methodNotPublic=Die annotierite Methode [{0}] ist nicht öffentlich +pojoMethodMapping.paramWithoutAnnotation=Es wurde ein Parameter des Typs [{0}] an der Methode [{1}] der Klasse [{2}] gefunden der nicht die Annotation @PathParam hat diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_es.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_es.properties new file mode 100644 index 0000000..0c385fb --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_es.properties @@ -0,0 +1,22 @@ +# 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. + +pojoEndpointBase.onCloseFail=Fallo al llamar el método onClose del endpoint POJO para el tipo POJO [{0}] +pojoEndpointBase.onOpenFail=Fallo al llamar el método onOpen del end point POJO para el tipo POJO [{0}]\n + +pojoMethodMapping.duplicateAnnotation=Anotaciones duplicadas [{0}] presente en la clase [{1}]\n +pojoMethodMapping.duplicatePongMessageParam=Varios parámetros de PongMessage estan presentes en el método [{0}] de la clase [{1}] que fue anotado con OnMessage +pojoMethodMapping.invalidDecoder=El decodificador especificado de tipo [{0}] no puede ser instanciado\n +pojoMethodMapping.onErrorNoThrowable=Parámetro no descartable estaba presente en el método [{0}] de clase [{1}] que fue apuntado con OnError diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_fr.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_fr.properties new file mode 100644 index 0000000..929f634 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_fr.properties @@ -0,0 +1,45 @@ +# 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. + +pojoEndpointBase.closeSessionFail=Echec de fermeture de la session WebSocket pendant le traitement d'erreur +pojoEndpointBase.onCloseFail=Echec lors de l''appel de la méthode onClose du point de terminaison POJO de type [{0}] +pojoEndpointBase.onError=Aucun gestionnaire d''erreur n''est configuré pour [{0}] et l''erreur suivante s''est produite +pojoEndpointBase.onErrorFail=Echec de l''appel de la méthode onError du point de terminaison POJO pour le type [{0}] +pojoEndpointBase.onOpenFail=Impossible d’appeler la méthode onOpen du point de terminaison POJO de type [{0}] + +pojoMessageHandlerWhole.decodeIoFail=Erreur d'IO lors du décodage du message +pojoMessageHandlerWhole.maxBufferSize=La taille maximale de message supportée par cette implémentation est Integer.MAX_VALUE + +pojoMessageHandlerWholeBase.decodeDestoryFailed=Echec de la destruction du décodeur de type [{0}] + +pojoMethodMapping.decodePathParamFail=Echec de décodage de la valeur de paramètre de chemin [{0}] vers le type attendu [{1}] +pojoMethodMapping.duplicateAnnotation=Annotations dupliquées [{0}] présentes pour la classe [{1}] +pojoMethodMapping.duplicateLastParam=Multiple (derniers) paramètres booléens présents sur la méthode [{0}] de classe [{1}], qui était annotée par OnMessage +pojoMethodMapping.duplicateMessageParam=De multiples paramètres de message sont présents sur la méthode [{0}] de la classe [{1}] qui a été annotée avec OnMessage +pojoMethodMapping.duplicatePongMessageParam=De multiples paramètres PongMessage sont présents sur la méthode [{0}] de la classe [{1}] qui a été annotée avec OnMessage +pojoMethodMapping.duplicateSessionParam=De multiples paramètres de session sont présents sur la méthode [{0}] de la classe [{1}] qui a été annotée avec OnMessage +pojoMethodMapping.invalidDecoder=Le décodeur de type [{0}] spécifié n''a pas pu être instantié +pojoMethodMapping.methodNotPublic=La méthode [{0}] annotée n''est pas publique +pojoMethodMapping.noDecoder=Aucun décodeur n''a été trouvé pour les paramètres de message présents sur la méthode [{0}] de la classe [{1}] qui a été annotée avec OnMessage +pojoMethodMapping.noPayload=Pas de paramètre de données présent sur la méthode [{0}] de la classe [{1}] qui a été annotée par OnMessage +pojoMethodMapping.onErrorNoThrowable=Aucun paramètre Throwable n''est présent sur la méthode [{0}] de la classe [{1}] qui est annotée par OnError +pojoMethodMapping.paramWithoutAnnotation=Un paramètre de type [{0}] a été trouvé sur la méthode [{1}] de la classe [{2}] qui n''avait pas d''annotation @PathParam +pojoMethodMapping.partialInputStream=L''InputStream et les paramètres booléens présents sur la méthode [{0}] de la classe [{1}] qui a été annotée par OnMessage sont invalides +pojoMethodMapping.partialObject=L''objet et la paramètres booléens présents sur la méthode [{0}] de la classe [{1}] qui a été annotée par OnMessage sont invalides +pojoMethodMapping.partialPong=Le PongMessage et les paramètres booléens présents sur la méthode [{0}] de la classe [{1}] qui a été annotée par OnMessage sont invalides +pojoMethodMapping.partialReader=Le Reader et les paramètres booléens présents sur la méthode [{0}] de la classe [{1}] qui a été annotée par OnMessage sont invalides +pojoMethodMapping.pongWithPayload=Le PongMessage et les paramètres de message présents sur la méthode [{0}] de la classe [{1}] qui a été annotée par OnMessage sont invalides + +pojoPathParam.wrongType=Le type [{0}] n''est pas autorisé en tant que paramètre de chemin, les paramètres annotés avec @PathParm doivent être uniquement des String, des primitives Java ou des versions encapsulées de celles ci diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_ja.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_ja.properties new file mode 100644 index 0000000..e1df88a --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_ja.properties @@ -0,0 +1,45 @@ +# 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. + +pojoEndpointBase.closeSessionFail=エラー処ç†ä¸­ã« WebSocket セッションを切断ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +pojoEndpointBase.onCloseFail=タイプ [{0}] ã®POJOã®POJOエンドãƒã‚¤ãƒ³ãƒˆã®onCloseメソッドã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—㟠+pojoEndpointBase.onError=[{0}]ã«å¯¾ã—ã¦ã‚¨ãƒ©ãƒ¼å‡¦ç†ãŒæ§‹æˆã•ã‚Œã¦ãŠã‚‰ãšã€æ¬¡ã®ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +pojoEndpointBase.onErrorFail=タイプ [{0}] ã®POJOã®POJOエンドãƒã‚¤ãƒ³ãƒˆã®onErrorメソッドã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—㟠+pojoEndpointBase.onOpenFail=タイプ [{0}] ã®POJOã®POJOエンドãƒã‚¤ãƒ³ãƒˆã®onOpenメソッドã®å‘¼ã³å‡ºã—ã«å¤±æ•—ã—ã¾ã—㟠+ +pojoMessageHandlerWhole.decodeIoFail=メッセージã®å¾©å·ä¸­ã«å…¥å‡ºåŠ›ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ +pojoMessageHandlerWhole.maxBufferSize=ã“ã®å®Ÿè£…ã§å¯¾å¿œå¯èƒ½ãªãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚µã‚¤ã‚ºã®ä¸Šé™å€¤ã¯ Integer.MAX_VALUE ã§ã™ã€‚ + +pojoMessageHandlerWholeBase.decodeDestoryFailed=タイプ [{0}] ã®ãƒ‡ã‚³ãƒ¼ãƒ€ãƒ¼ã®ç ´æ£„ã«å¤±æ•—ã—ã¾ã—㟠+ +pojoMethodMapping.decodePathParamFail=パスパラメータã®å€¤ [{0}] ã‚’ [{1}] åž‹ã¨ã—ã¦è§£é‡ˆã§ãã¾ã›ã‚“。 +pojoMethodMapping.duplicateAnnotation=クラス [{1}] ã«ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ [{0}] ãŒé‡è¤‡ã—ã¦ã„ã¾ã™ +pojoMethodMapping.duplicateLastParam=OnMessageã§ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ä»˜ã‘ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«è¤‡æ•°ã®booleanパラメータãŒå­˜åœ¨ã—ã¾ã™ +pojoMethodMapping.duplicateMessageParam=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«è¤‡æ•°ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ‘ラメータãŒå­˜åœ¨ã—ã¾ã™ +pojoMethodMapping.duplicatePongMessageParam=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«è¤‡æ•°ã®PongMessageパラメータãŒå­˜åœ¨ã—ã¾ã™ +pojoMethodMapping.duplicateSessionParam=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«è¤‡æ•°ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ãƒ‘ラメータãŒå­˜åœ¨ã—ã¾ã™ +pojoMethodMapping.invalidDecoder=指定ã•ã‚ŒãŸãƒ‡ã‚³ãƒ¼ãƒ€ãƒ¼ã‚¯ãƒ©ã‚¹ [{0}] をインスタンス化ã§ãã¾ã›ã‚“ã§ã—㟠+pojoMethodMapping.methodNotPublic=アノテーションã§ä¿®é£¾ã•ã‚ŒãŸãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ãŒpublicã§ã¯ã‚ã‚Šã¾ã›ã‚“ +pojoMethodMapping.noDecoder=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«å­˜åœ¨ã™ã‚‹ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãƒ‘ラメータã®ãƒ‡ã‚³ãƒ¼ãƒ€ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—㟠+pojoMethodMapping.noPayload=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«ã¯ãƒšã‚¤ãƒ­ãƒ¼ãƒ‰ã«å¯¾å¿œã™ã‚‹ãƒ‘ラメーターãŒã‚ã‚Šã¾ã›ã‚“。 +pojoMethodMapping.onErrorNoThrowable=OnError アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«ã€ThrowableパラメータãŒã‚ã‚Šã¾ã›ã‚“ã§ã—㟠+pojoMethodMapping.paramWithoutAnnotation=@PathParamアノテーションをæŒãŸãªã‹ã£ãŸã‚¯ãƒ©ã‚¹ [{2}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{1}] ã« [{0}] åž‹ã®ãƒ‘ラメータãŒè¦‹ã¤ã‹ã‚Šã¾ã—㟠+pojoMethodMapping.partialInputStream=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«å…¥åŠ›ã‚¹ãƒˆãƒªãƒ¼ãƒ ã¨ boolean ã®ä¸æ­£ãªãƒ‘ラメーターãŒå­˜åœ¨ã—ã¾ã™ +pojoMethodMapping.partialObject=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«ç„¡åŠ¹ãªã‚ªãƒ–ジェクトãŠã‚ˆã³boolean パラメータãŒã‚ã‚Šã¾ã™ +pojoMethodMapping.partialPong=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«å­˜åœ¨ã™ã‚‹PongMessageパラメータãŠã‚ˆã³booleanパラメータãŒç„¡åŠ¹ã§ã™ +pojoMethodMapping.partialReader=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã« Reader ãŠã‚ˆã³ boolean ã®ä¸æ­£ãªãƒ‘ラメーターãŒã‚ã‚Šã¾ã™ +pojoMethodMapping.pongWithPayload=OnMessage アノテーションã§ä¿®é£¾ã•ã‚ŒãŸã‚¯ãƒ©ã‚¹ [{1}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ [{0}] ã«ã€ç„¡åŠ¹ãªPongMessageãŠã‚ˆã³MessageパラメータãŒã‚ã‚Šã¾ã™ + +pojoPathParam.wrongType=タイプ [{0}] ã¯ãƒ‘スパラメータã¨ã—ã¦è¨±å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“。@PathParamã§ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ãŒä»˜ã‘られãŸãƒ‘ラメータã¯ã€æ–‡å­—列ã€Javaプリミティブã€ã¾ãŸã¯ãれらã®ãƒœãƒƒã‚¯ã‚¹ç‰ˆã®ã¿ã§ã™ã€‚ diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_ko.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_ko.properties new file mode 100644 index 0000000..b9f4243 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_ko.properties @@ -0,0 +1,45 @@ +# 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. + +pojoEndpointBase.closeSessionFail=오류 처리 중 웹소켓 ì„¸ì…˜ì„ ë‹«ì§€ 못했습니다. +pojoEndpointBase.onCloseFail=íƒ€ìž…ì´ [{0}]ì¸ POJO를 위한 POJO 엔드í¬ì¸íŠ¸ì˜ onClose 메소드를 호출하지 못했습니다. +pojoEndpointBase.onError=[{0}]ì„(를) 위한 오류 핸들ë§ì´ 설정ë˜ì§€ 않았으며, 다ìŒê³¼ ê°™ì€ ì˜¤ë¥˜ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. +pojoEndpointBase.onErrorFail=íƒ€ìž…ì´ [{0}]ì¸ POJO를 위한 POJO 엔드í¬ì¸íŠ¸ì˜ onError를 호출하지 못했습니다. +pojoEndpointBase.onOpenFail=íƒ€ìž…ì´ [{0}]ì¸ POJO를 위한, POJO 엔드í¬ì¸íŠ¸ì˜ onOpen 메소드를 호출하지 못했습니다. + +pojoMessageHandlerWhole.decodeIoFail=메시지를 디코딩하는 중 IO 오류 ë°œìƒ +pojoMessageHandlerWhole.maxBufferSize=ì´ êµ¬í˜„ì„ ìœ„í•´ 지ì›ë˜ëŠ” 최대 메시지 í¬ê¸°ëŠ” Integer.MAX_VALUE입니다. + +pojoMessageHandlerWholeBase.decodeDestoryFailed=[{0}] íƒ€ìž…ì˜ ë””ì½”ë”를 소멸시키지 못했습니다. + +pojoMethodMapping.decodePathParamFail=경로 파ë¼ë¯¸í„° ê°’ [{0}]ì„(를), 요구ë˜ëŠ” 타입 [{1}](으)ë¡œ 디코드하지 못했습니다. +pojoMethodMapping.duplicateAnnotation=ì¤‘ë³µëœ [{0}] annotationë“¤ì´ í´ëž˜ìŠ¤ [{1}]ì— ì¡´ìž¬í•©ë‹ˆë‹¤. +pojoMethodMapping.duplicateLastParam=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì—, 여러 ê°œì˜ boolean íƒ€ìž…ì˜ (마지막) 파ë¼ë¯¸í„°ë“¤ì´ 존재합니다. +pojoMethodMapping.duplicateMessageParam=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì— ì—¬ëŸ¬ ê°œì˜ ë©”ì‹œì§€ 파ë¼ë¯¸í„°ë“¤ì´ 존재합니다. +pojoMethodMapping.duplicatePongMessageParam=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì— ì—¬ëŸ¬ ê°œì˜ PongMessage 파ë¼ë¯¸í„°ë“¤ì´ 존재합니다. +pojoMethodMapping.duplicateSessionParam=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì— ì—¬ëŸ¬ ê°œì˜ ì„¸ì…˜ 파ë¼ë¯¸í„°ë“¤ì´ 존재합니다. +pojoMethodMapping.invalidDecoder=ì§€ì •ëœ íƒ€ìž… [{0}]ì˜ ë””ì½”ë”를 ìƒì„±í•  수 없었습니다. +pojoMethodMapping.methodNotPublic=Annotateëœ ë©”ì†Œë“œ [{0}]ì´(ê°€) public 메소드가 아닙니다. +pojoMethodMapping.noDecoder=í´ëž˜ìŠ¤ [{1}]ì—ì„œ, OnMessageë¡œ annotateëœ ë©”ì†Œë“œ [{0}]ì— ì¡´ìž¬í•˜ëŠ” 메시지 파ë¼ë¯¸í„°ë“¤ì„ 위한 디코ë”를 ì°¾ì„ ìˆ˜ 없습니다. +pojoMethodMapping.noPayload=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì—, payload 파ë¼ë¯¸í„°ê°€ 존재하지 않습니다. +pojoMethodMapping.onErrorNoThrowable=OnErrorë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì— Throwable 파ë¼ë¯¸í„°ê°€ 없습니다. +pojoMethodMapping.paramWithoutAnnotation=@PathParam으로 annotate ë˜ì§€ ì•Šì€ í´ëž˜ìŠ¤ [{2}]ì˜ ë©”ì†Œë“œ [{1}]ì—ì„œ, íƒ€ìž…ì´ [{0}]ì¸ íŒŒë¼ë¯¸í„°ê°€ 발견ë˜ì—ˆìŠµë‹ˆë‹¤. +pojoMethodMapping.partialInputStream=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì— ìœ íš¨í•˜ì§€ ì•Šì€ InputStreamê³¼ boolean 파ë¼ë¯¸í„°ë“¤ì´ 존재합니다. +pojoMethodMapping.partialObject=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì— ìœ íš¨í•˜ì§€ ì•Šì€ Object와 boolean 파ë¼ë¯¸í„°ë“¤ì´ 존재합니다. +pojoMethodMapping.partialPong=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì— ì¡´ìž¬í•˜ëŠ” PongMessage와 boolean 파ë¼ë¯¸í„°ë“¤ì€ 유효하지 않습니다. +pojoMethodMapping.partialReader=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì—, 유효하지 ì•Šì€ Reader와 boolean 파ë¼ë¯¸í„°ë“¤ì´ 존재합니다. +pojoMethodMapping.pongWithPayload=OnMessageë¡œ annotateëœ í´ëž˜ìŠ¤ [{1}]ì˜ ë©”ì†Œë“œ [{0}]ì— ì¡´ìž¬í•˜ëŠ” PongMessage와 Message 파ë¼ë¯¸í„°ë“¤ì€ 유효하지 않습니다. + +pojoPathParam.wrongType=타입 [{0}]ì€(는) 경로 파ë¼ë¯¸í„°ë¡œì„œ 허용ë˜ì§€ 않습니다. @PathParam으로 annotateëœ íŒŒë¼ë¯¸í„°ë“¤ì€ ì˜¤ì§ ë¬¸ìžì—´ë“¤, ë˜ëŠ” ìžë°” ì›ì‹œíƒ€ìž…들 ë˜ëŠ” ê·¸ê²ƒë“¤ì˜ ë°•ì‹±ëœ ë²„ì „ë“¤ì´ì–´ì•¼ 합니다. diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_pt_BR.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..8b2885e --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +pojoMethodMapping.invalidDecoder=O decodificador especificado de tipo [{0}] não pôde ser instanciado. diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_ru.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_ru.properties new file mode 100644 index 0000000..418615b --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_ru.properties @@ -0,0 +1,16 @@ +# 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. + +pojoMethodMapping.duplicateAnnotation=ÐÐ½Ð°Ð»Ð¾Ð³Ð¸Ñ‡Ð½Ð°Ñ Ð°Ð½Ð½Ð¾Ñ‚Ð°Ñ†Ð¸Ñ [{0}] ÑущеÑтвует в клаÑÑе [{1}] diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..02566f3 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings_zh_CN.properties @@ -0,0 +1,45 @@ +# 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. + +pojoEndpointBase.closeSessionFail=在错误处ç†æœŸé—´æ— æ³•å…³é—­WebSocketä¼šè¯ +pojoEndpointBase.onCloseFail=):无法为类型为[{0}]çš„POJO调用POJO端点的onClose方法 +pojoEndpointBase.onError=在错误å‘生åŽï¼Œæ²¡æœ‰ä¸º[{0}]é…ç½®é”™è¯¯å¤„ç† +pojoEndpointBase.onErrorFail=无法为类型为[{0}]çš„POJO调用POJO端点的onError方法 +pojoEndpointBase.onOpenFail=无法为类型为[{0}]çš„POJO调用POJO端点的onOpen方法 + +pojoMessageHandlerWhole.decodeIoFail=解ç æ¶ˆæ¯æ—¶å‡ºçŽ°IO错误 +pojoMessageHandlerWhole.maxBufferSize=此实现支æŒçš„最大消æ¯å¤§å°ä¸ºInteger.MAX_VALUE + +pojoMessageHandlerWholeBase.decodeDestoryFailed=未能销æ¯[{0}]类型的解ç å™¨ + +pojoMethodMapping.decodePathParamFail=未能将路径å‚数值[{0}]解ç ä¸ºé¢„期的类型[{1}] +pojoMethodMapping.duplicateAnnotation=ç±»[{1}]上存在的é‡å¤æ³¨é‡Š[{0}] +pojoMethodMapping.duplicateLastParam=用OnMessage注释的类[{1}]的方法[{0}]上存在多个布尔å‚数(最åŽçš„) +pojoMethodMapping.duplicateMessageParam=ç±»[{1}]的方法[{0}]上存在多个消æ¯å‚数,该方法已用OnMessage进行了注释。 +pojoMethodMapping.duplicatePongMessageParam=使用OnMessage注释的类[{1}]的方法[{0}]上存在多个PongMessageå‚æ•° +pojoMethodMapping.duplicateSessionParam=ç±»[{1}]的方法[{0}]上存在多个会è¯å‚数,该方法使用OnMessage进行了注释 +pojoMethodMapping.invalidDecoder=这个特定类型的解ç å™¨[{0}]无法被实例化 +pojoMethodMapping.methodNotPublic=注解方法[{0}]ä¸ä¸ºå…¬å…±æ–¹æ³• +pojoMethodMapping.noDecoder=在用onMessage注释的类[{1}]的方法[{0}]上找ä¸åˆ°æ¶ˆæ¯å‚数的解ç å™¨ã€‚ +pojoMethodMapping.noPayload=用OnMessage注释的类[{1}]的方法[{0}]上ä¸å­˜åœ¨æœ‰æ•ˆè´Ÿè½½å‚æ•° +pojoMethodMapping.onErrorNoThrowable=):ç±»[{1}]的方法[{0}]上ä¸å­˜åœ¨ç”¨OnError注释的抛出å‚æ•° +pojoMethodMapping.paramWithoutAnnotation=在类[{2}]的方法[{1}]上找到了类型为[{0}]çš„å‚数,该方法没有@PathParam 注释 +pojoMethodMapping.partialInputStream=ç±»[{1}]的方法[{0}]中存在无效的InputStreamå’Œbooleanå‚数,该方法使用OnMessage进行了批注 +pojoMethodMapping.partialObject=用onMessage注释的类[{1}]的方法[{0}]中存在无效的对象和布尔å‚æ•° +pojoMethodMapping.partialPong=用OnMessage注释的类[{1}]的方法[{0}]上存在无效的PongMessage和布尔å‚æ•° +pojoMethodMapping.partialReader=用OnMessage注释的类[{1}]的方法[{0}]中存在无效的读å–器和布尔å‚æ•° +pojoMethodMapping.pongWithPayload=ç±»[{1}]的方法[{0}]中存在无效的PongMessage 和消æ¯å‚数,该方法是用onMessage注释的 + +pojoPathParam.wrongType=ä¸å…许将类型[{0}]作为路径å‚数。用@PathParam注释的å‚æ•°åªèƒ½æ˜¯å­—符串ã€Java原语或盒装版本。 diff --git a/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java b/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java new file mode 100644 index 0000000..8067b72 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java @@ -0,0 +1,151 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.Set; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.Session; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +/** + * Base implementation (client and server have different concrete implementations) of the wrapper that converts a POJO + * instance into a WebSocket endpoint instance. + */ +public abstract class PojoEndpointBase extends Endpoint { + + private final Log log = LogFactory.getLog(PojoEndpointBase.class); // must not be static + private static final StringManager sm = StringManager.getManager(PojoEndpointBase.class); + + private Object pojo; + private final Map pathParameters; + private PojoMethodMapping methodMapping; + + + protected PojoEndpointBase(Map pathParameters) { + this.pathParameters = pathParameters; + } + + + protected final void doOnOpen(Session session, EndpointConfig config) { + PojoMethodMapping methodMapping = getMethodMapping(); + Object pojo = getPojo(); + + // Add message handlers before calling onOpen since that may trigger a + // message which in turn could trigger a response and/or close the + // session + for (MessageHandler mh : methodMapping.getMessageHandlers(pojo, pathParameters, session, config)) { + session.addMessageHandler(mh); + } + + if (methodMapping.getOnOpen() != null) { + try { + methodMapping.getOnOpen().invoke(pojo, methodMapping.getOnOpenArgs(pathParameters, session, config)); + + } catch (IllegalAccessException e) { + // Reflection related problems + log.error(sm.getString("pojoEndpointBase.onOpenFail", pojo.getClass().getName()), e); + handleOnOpenOrCloseError(session, e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + handleOnOpenOrCloseError(session, cause); + } catch (Throwable t) { + handleOnOpenOrCloseError(session, t); + } + } + } + + + private void handleOnOpenOrCloseError(Session session, Throwable t) { + // If really fatal - re-throw + ExceptionUtils.handleThrowable(t); + + // Trigger the error handler and close the session + onError(session, t); + try { + session.close(); + } catch (IOException ioe) { + log.warn(sm.getString("pojoEndpointBase.closeSessionFail"), ioe); + } + } + + @Override + public final void onClose(Session session, CloseReason closeReason) { + + if (methodMapping.getOnClose() != null) { + try { + methodMapping.getOnClose().invoke(pojo, + methodMapping.getOnCloseArgs(pathParameters, session, closeReason)); + } catch (Throwable t) { + log.error(sm.getString("pojoEndpointBase.onCloseFail", pojo.getClass().getName()), t); + handleOnOpenOrCloseError(session, t); + } + } + + // Trigger the destroy method for any associated decoders + Set messageHandlers = session.getMessageHandlers(); + for (MessageHandler messageHandler : messageHandlers) { + if (messageHandler instanceof PojoMessageHandlerWholeBase) { + ((PojoMessageHandlerWholeBase) messageHandler).onClose(); + } + } + } + + + @Override + public final void onError(Session session, Throwable throwable) { + + if (methodMapping.getOnError() == null) { + log.error(sm.getString("pojoEndpointBase.onError", pojo.getClass().getName()), throwable); + } else { + try { + methodMapping.getOnError().invoke(pojo, + methodMapping.getOnErrorArgs(pathParameters, session, throwable)); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("pojoEndpointBase.onErrorFail", pojo.getClass().getName()), t); + } + } + } + + protected Object getPojo() { + return pojo; + } + + protected void setPojo(Object pojo) { + this.pojo = pojo; + } + + + protected PojoMethodMapping getMethodMapping() { + return methodMapping; + } + + protected void setMethodMapping(PojoMethodMapping methodMapping) { + this.methodMapping = methodMapping; + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoEndpointClient.java b/java/org/apache/tomcat/websocket/pojo/PojoEndpointClient.java new file mode 100644 index 0000000..c4c770e --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoEndpointClient.java @@ -0,0 +1,46 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.util.Collections; +import java.util.List; + +import jakarta.websocket.Decoder; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; + +import org.apache.tomcat.InstanceManager; + +/** + * Wrapper class for instances of POJOs annotated with {@link jakarta.websocket.ClientEndpoint} so they appear as + * standard {@link jakarta.websocket.Endpoint} instances. + */ +public class PojoEndpointClient extends PojoEndpointBase { + + public PojoEndpointClient(Object pojo, List> decoders, InstanceManager instanceManager) + throws DeploymentException { + super(Collections.emptyMap()); + setPojo(pojo); + setMethodMapping(new PojoMethodMapping(pojo.getClass(), decoders, null, instanceManager)); + } + + @Override + public void onOpen(Session session, EndpointConfig config) { + doOnOpen(session, config); + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoEndpointServer.java b/java/org/apache/tomcat/websocket/pojo/PojoEndpointServer.java new file mode 100644 index 0000000..650a72b --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoEndpointServer.java @@ -0,0 +1,48 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.util.Map; + +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpointConfig; + +/** + * Wrapper class for instances of POJOs annotated with {@link jakarta.websocket.server.ServerEndpoint} so they appear as + * standard {@link jakarta.websocket.Endpoint} instances. + */ +public class PojoEndpointServer extends PojoEndpointBase { + + public PojoEndpointServer(Map pathParameters, Object pojo) { + super(pathParameters); + setPojo(pojo); + } + + + @Override + public void onOpen(Session session, EndpointConfig endpointConfig) { + + ServerEndpointConfig sec = (ServerEndpointConfig) endpointConfig; + + PojoMethodMapping methodMapping = (PojoMethodMapping) sec.getUserProperties() + .get(Constants.POJO_METHOD_MAPPING_KEY); + setMethodMapping(methodMapping); + + doOnOpen(session, endpointConfig); + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerBase.java b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerBase.java new file mode 100644 index 0000000..99e1a1e --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerBase.java @@ -0,0 +1,119 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +import jakarta.websocket.EncodeException; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.RemoteEndpoint; +import jakarta.websocket.Session; + +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.websocket.WrappedMessageHandler; + +/** + * Common implementation code for the POJO message handlers. + * + * @param The type of message to handle + */ +public abstract class PojoMessageHandlerBase implements WrappedMessageHandler { + + protected final Object pojo; + protected final Method method; + protected final Session session; + protected final Object[] params; + protected final int indexPayload; + protected final boolean convert; + protected final int indexSession; + protected final long maxMessageSize; + + public PojoMessageHandlerBase(Object pojo, Method method, Session session, Object[] params, int indexPayload, + boolean convert, int indexSession, long maxMessageSize) { + this.pojo = pojo; + this.method = method; + // TODO: The method should already be accessible here but the following + // code seems to be necessary in some as yet not fully understood cases. + try { + this.method.setAccessible(true); + } catch (Exception e) { + // It is better to make sure the method is accessible, but + // ignore exceptions and hope for the best + } + this.session = session; + this.params = params; + this.indexPayload = indexPayload; + this.convert = convert; + this.indexSession = indexSession; + this.maxMessageSize = maxMessageSize; + } + + + protected final void processResult(Object result) { + if (result == null) { + return; + } + + RemoteEndpoint.Basic remoteEndpoint = session.getBasicRemote(); + try { + if (result instanceof String) { + remoteEndpoint.sendText((String) result); + } else if (result instanceof ByteBuffer) { + remoteEndpoint.sendBinary((ByteBuffer) result); + } else if (result instanceof byte[]) { + remoteEndpoint.sendBinary(ByteBuffer.wrap((byte[]) result)); + } else { + remoteEndpoint.sendObject(result); + } + } catch (IOException | EncodeException ioe) { + throw new IllegalStateException(ioe); + } + } + + + /** + * Expose the POJO if it is a message handler so the Session is able to match requests to remove handlers if the + * original handler has been wrapped. + */ + @Override + public final MessageHandler getWrappedHandler() { + if (pojo instanceof MessageHandler) { + return (MessageHandler) pojo; + } else { + return null; + } + } + + + @Override + public final long getMaxMessageSize() { + return maxMessageSize; + } + + + protected final void handlePojoMethodException(Throwable t) { + t = ExceptionUtils.unwrapInvocationTargetException(t); + ExceptionUtils.handleThrowable(t); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new RuntimeException(t.getMessage(), t); + } + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBase.java b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBase.java new file mode 100644 index 0000000..1614fa9 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBase.java @@ -0,0 +1,73 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +import jakarta.websocket.DecodeException; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.Session; + +import org.apache.tomcat.websocket.WsSession; + +/** + * Common implementation code for the POJO partial message handlers. All the real work is done in this class and in the + * superclass. + * + * @param The type of message to handle + */ +public abstract class PojoMessageHandlerPartialBase extends PojoMessageHandlerBase + implements MessageHandler.Partial { + + private final int indexBoolean; + + public PojoMessageHandlerPartialBase(Object pojo, Method method, Session session, Object[] params, int indexPayload, + boolean convert, int indexBoolean, int indexSession, long maxMessageSize) { + super(pojo, method, session, params, indexPayload, convert, indexSession, maxMessageSize); + this.indexBoolean = indexBoolean; + } + + + @Override + public final void onMessage(T message, boolean last) { + if (params.length == 1 && params[0] instanceof DecodeException) { + ((WsSession) session).getLocal().onError(session, (DecodeException) params[0]); + return; + } + Object[] parameters = params.clone(); + if (indexBoolean != -1) { + parameters[indexBoolean] = Boolean.valueOf(last); + } + if (indexSession != -1) { + parameters[indexSession] = session; + } + if (convert) { + parameters[indexPayload] = ((ByteBuffer) message).array(); + } else { + parameters[indexPayload] = message; + } + Object result = null; + try { + result = method.invoke(pojo, parameters); + } catch (IllegalAccessException | InvocationTargetException e) { + handlePojoMethodException(e); + } + processResult(result); + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBinary.java b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBinary.java new file mode 100644 index 0000000..e4b3a0c --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBinary.java @@ -0,0 +1,33 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +import jakarta.websocket.Session; + +/** + * ByteBuffer specific concrete implementation for handling partial messages. + */ +public class PojoMessageHandlerPartialBinary extends PojoMessageHandlerPartialBase { + + public PojoMessageHandlerPartialBinary(Object pojo, Method method, Session session, Object[] params, + int indexPayload, boolean convert, int indexBoolean, int indexSession, long maxMessageSize) { + super(pojo, method, session, params, indexPayload, convert, indexBoolean, indexSession, maxMessageSize); + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialText.java b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialText.java new file mode 100644 index 0000000..2813e2c --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialText.java @@ -0,0 +1,32 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.lang.reflect.Method; + +import jakarta.websocket.Session; + +/** + * Text specific concrete implementation for handling partial messages. + */ +public class PojoMessageHandlerPartialText extends PojoMessageHandlerPartialBase { + + public PojoMessageHandlerPartialText(Object pojo, Method method, Session session, Object[] params, int indexPayload, + boolean convert, int indexBoolean, int indexSession, long maxMessageSize) { + super(pojo, method, session, params, indexPayload, convert, indexBoolean, indexSession, maxMessageSize); + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBase.java b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBase.java new file mode 100644 index 0000000..4ede463 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBase.java @@ -0,0 +1,132 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.naming.NamingException; + +import jakarta.websocket.DecodeException; +import jakarta.websocket.Decoder; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.Session; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.WsSession; + +/** + * Common implementation code for the POJO whole message handlers. All the real work is done in this class and in the + * superclass. + * + * @param The type of message to handle + */ +public abstract class PojoMessageHandlerWholeBase extends PojoMessageHandlerBase + implements MessageHandler.Whole { + + private final Log log = LogFactory.getLog(PojoMessageHandlerWholeBase.class); // must not be static + private static final StringManager sm = StringManager.getManager(PojoMessageHandlerWholeBase.class); + + protected final List decoders = new ArrayList<>(); + + public PojoMessageHandlerWholeBase(Object pojo, Method method, Session session, Object[] params, int indexPayload, + boolean convert, int indexSession, long maxMessageSize) { + super(pojo, method, session, params, indexPayload, convert, indexSession, maxMessageSize); + } + + + protected Decoder createDecoderInstance(Class clazz) + throws ReflectiveOperationException, NamingException { + InstanceManager instanceManager = ((WsSession) session).getInstanceManager(); + if (instanceManager == null) { + return clazz.getConstructor().newInstance(); + } else { + return (Decoder) instanceManager.newInstance(clazz); + } + } + + + @Override + public final void onMessage(T message) { + + if (params.length == 1 && params[0] instanceof DecodeException) { + ((WsSession) session).getLocal().onError(session, (DecodeException) params[0]); + return; + } + + // Can this message be decoded? + Object payload; + try { + payload = decode(message); + } catch (DecodeException de) { + ((WsSession) session).getLocal().onError(session, de); + return; + } + + if (payload == null) { + // Not decoded. Convert if required. + if (convert) { + payload = convert(message); + } else { + payload = message; + } + } + + Object[] parameters = params.clone(); + if (indexSession != -1) { + parameters[indexSession] = session; + } + parameters[indexPayload] = payload; + + Object result = null; + try { + result = method.invoke(pojo, parameters); + } catch (IllegalAccessException | InvocationTargetException e) { + handlePojoMethodException(e); + } + processResult(result); + } + + + protected void onClose() { + InstanceManager instanceManager = ((WsSession) session).getInstanceManager(); + + for (Decoder decoder : decoders) { + decoder.destroy(); + if (instanceManager != null) { + try { + instanceManager.destroyInstance(decoder); + } catch (IllegalAccessException | InvocationTargetException e) { + log.warn(sm.getString("pojoMessageHandlerWholeBase.decodeDestoryFailed", decoder.getClass()), e); + } + } + } + } + + + protected Object convert(T message) { + return message; + } + + + protected abstract Object decode(T message) throws DecodeException; +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBinary.java b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBinary.java new file mode 100644 index 0000000..0e70dfa --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBinary.java @@ -0,0 +1,113 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.util.List; + +import javax.naming.NamingException; + +import jakarta.websocket.DecodeException; +import jakarta.websocket.Decoder; +import jakarta.websocket.Decoder.Binary; +import jakarta.websocket.Decoder.BinaryStream; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; + +import org.apache.tomcat.util.res.StringManager; + +/** + * ByteBuffer specific concrete implementation for handling whole messages. + */ +public class PojoMessageHandlerWholeBinary extends PojoMessageHandlerWholeBase { + + private static final StringManager sm = StringManager.getManager(PojoMessageHandlerWholeBinary.class); + + private final boolean isForInputStream; + + public PojoMessageHandlerWholeBinary(Object pojo, Method method, Session session, EndpointConfig config, + List> decoderClazzes, Object[] params, int indexPayload, boolean convert, + int indexSession, boolean isForInputStream, long maxMessageSize) { + super(pojo, method, session, params, indexPayload, convert, indexSession, maxMessageSize); + + // Update binary text size handled by session + if (maxMessageSize > -1 && maxMessageSize > session.getMaxBinaryMessageBufferSize()) { + if (maxMessageSize > Integer.MAX_VALUE) { + throw new IllegalArgumentException(sm.getString("pojoMessageHandlerWhole.maxBufferSize")); + } + session.setMaxBinaryMessageBufferSize((int) maxMessageSize); + } + + try { + if (decoderClazzes != null) { + for (Class decoderClazz : decoderClazzes) { + if (Binary.class.isAssignableFrom(decoderClazz)) { + Binary decoder = (Binary) createDecoderInstance(decoderClazz); + decoder.init(config); + decoders.add(decoder); + } else if (BinaryStream.class.isAssignableFrom(decoderClazz)) { + BinaryStream decoder = (BinaryStream) createDecoderInstance(decoderClazz); + decoder.init(config); + decoders.add(decoder); + } else { + // Text decoder - ignore it + } + } + } + } catch (ReflectiveOperationException | NamingException e) { + throw new IllegalArgumentException(e); + } + this.isForInputStream = isForInputStream; + } + + + @Override + protected Object decode(ByteBuffer message) throws DecodeException { + for (Decoder decoder : decoders) { + if (decoder instanceof Binary) { + if (((Binary) decoder).willDecode(message)) { + return ((Binary) decoder).decode(message); + } + } else { + byte[] array = new byte[message.limit() - message.position()]; + message.get(array); + ByteArrayInputStream bais = new ByteArrayInputStream(array); + try { + return ((BinaryStream) decoder).decode(bais); + } catch (IOException ioe) { + throw new DecodeException(message, sm.getString("pojoMessageHandlerWhole.decodeIoFail"), ioe); + } + } + } + return null; + } + + + @Override + protected Object convert(ByteBuffer message) { + byte[] array = new byte[message.remaining()]; + message.get(array); + if (isForInputStream) { + return new ByteArrayInputStream(array); + } else { + return array; + } + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholePong.java b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholePong.java new file mode 100644 index 0000000..3586468 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholePong.java @@ -0,0 +1,45 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.lang.reflect.Method; + +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; + +/** + * PongMessage specific concrete implementation for handling whole messages. + */ +public class PojoMessageHandlerWholePong extends PojoMessageHandlerWholeBase { + + public PojoMessageHandlerWholePong(Object pojo, Method method, Session session, Object[] params, int indexPayload, + boolean convert, int indexSession) { + super(pojo, method, session, params, indexPayload, convert, indexSession, -1); + } + + @Override + protected Object decode(PongMessage message) { + // Never decoded + return null; + } + + + @Override + protected void onClose() { + // NO-OP + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java new file mode 100644 index 0000000..8342420 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java @@ -0,0 +1,119 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Method; +import java.util.List; + +import javax.naming.NamingException; + +import jakarta.websocket.DecodeException; +import jakarta.websocket.Decoder; +import jakarta.websocket.Decoder.Text; +import jakarta.websocket.Decoder.TextStream; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; + +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.Util; + + +/** + * Text specific concrete implementation for handling whole messages. + */ +public class PojoMessageHandlerWholeText extends PojoMessageHandlerWholeBase { + + private static final StringManager sm = StringManager.getManager(PojoMessageHandlerWholeText.class); + + private final Class primitiveType; + + public PojoMessageHandlerWholeText(Object pojo, Method method, Session session, EndpointConfig config, + List> decoderClazzes, Object[] params, int indexPayload, boolean convert, + int indexSession, long maxMessageSize) { + super(pojo, method, session, params, indexPayload, convert, indexSession, maxMessageSize); + + // Update max text size handled by session + if (maxMessageSize > -1 && maxMessageSize > session.getMaxTextMessageBufferSize()) { + if (maxMessageSize > Integer.MAX_VALUE) { + throw new IllegalArgumentException(sm.getString("pojoMessageHandlerWhole.maxBufferSize")); + } + session.setMaxTextMessageBufferSize((int) maxMessageSize); + } + + // Check for primitives + Class type = method.getParameterTypes()[indexPayload]; + if (Util.isPrimitive(type)) { + primitiveType = type; + return; + } else { + primitiveType = null; + } + + try { + if (decoderClazzes != null) { + for (Class decoderClazz : decoderClazzes) { + if (Text.class.isAssignableFrom(decoderClazz)) { + Text decoder = (Text) createDecoderInstance(decoderClazz); + decoder.init(config); + decoders.add(decoder); + } else if (TextStream.class.isAssignableFrom(decoderClazz)) { + TextStream decoder = (TextStream) createDecoderInstance(decoderClazz); + decoder.init(config); + decoders.add(decoder); + } else { + // Binary decoder - ignore it + } + } + } + } catch (ReflectiveOperationException | NamingException e) { + throw new IllegalArgumentException(e); + } + } + + + @Override + protected Object decode(String message) throws DecodeException { + // Handle primitives + if (primitiveType != null) { + return Util.coerceToType(primitiveType, message); + } + // Handle full decoders + for (Decoder decoder : decoders) { + if (decoder instanceof Text) { + if (((Text) decoder).willDecode(message)) { + return ((Text) decoder).decode(message); + } + } else { + StringReader r = new StringReader(message); + try { + return ((TextStream) decoder).decode(r); + } catch (IOException ioe) { + throw new DecodeException(message, sm.getString("pojoMessageHandlerWhole.decodeIoFail"), ioe); + } + } + } + return null; + } + + + @Override + protected Object convert(String message) { + return new StringReader(message); + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java b/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java new file mode 100644 index 0000000..467ae57 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java @@ -0,0 +1,663 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.io.InputStream; +import java.io.Reader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.DecodeException; +import jakarta.websocket.Decoder; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; +import jakarta.websocket.server.PathParam; + +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.DecoderEntry; +import org.apache.tomcat.websocket.Util; +import org.apache.tomcat.websocket.Util.DecoderMatch; + +/** + * For a POJO class annotated with {@link jakarta.websocket.server.ServerEndpoint}, an instance of this class creates + * and caches the method handler, method information and parameter information for the onXXX calls. + */ +public class PojoMethodMapping { + + private static final StringManager sm = StringManager.getManager(PojoMethodMapping.class); + + private final Method onOpen; + private final Method onClose; + private final Method onError; + private final PojoPathParam[] onOpenParams; + private final PojoPathParam[] onCloseParams; + private final PojoPathParam[] onErrorParams; + private final List onMessage = new ArrayList<>(); + private final String wsPath; + + + /** + * Create a method mapping for the given POJO + * + * @param clazzPojo POJO implementation class + * @param decoderClazzes Set of potential decoder classes + * @param wsPath Path at which the endpoint will be deployed + * @param instanceManager Instance manager to use to create Decoder instances + * + * @throws DeploymentException If the mapping cannot be completed + */ + public PojoMethodMapping(Class clazzPojo, List> decoderClazzes, String wsPath, + InstanceManager instanceManager) throws DeploymentException { + + this.wsPath = wsPath; + + List decoders = Util.getDecoders(decoderClazzes, instanceManager); + Method open = null; + Method close = null; + Method error = null; + Method[] clazzPojoMethods = null; + Class currentClazz = clazzPojo; + while (!currentClazz.equals(Object.class)) { + Method[] currentClazzMethods = currentClazz.getDeclaredMethods(); + if (currentClazz == clazzPojo) { + clazzPojoMethods = currentClazzMethods; + } + for (Method method : currentClazzMethods) { + if (method.isSynthetic()) { + // Skip all synthetic methods. + // They may have copies of annotations from methods we are + // interested in and they will use the wrong parameter type + // (they always use Object) so we can't used them here. + continue; + } + if (method.getAnnotation(OnOpen.class) != null) { + checkPublic(method); + if (open == null) { + open = method; + } else { + if (currentClazz == clazzPojo || !isMethodOverride(open, method)) { + // Duplicate annotation + throw new DeploymentException( + sm.getString("pojoMethodMapping.duplicateAnnotation", OnOpen.class, currentClazz)); + } + } + } else if (method.getAnnotation(OnClose.class) != null) { + checkPublic(method); + if (close == null) { + close = method; + } else { + if (currentClazz == clazzPojo || !isMethodOverride(close, method)) { + // Duplicate annotation + throw new DeploymentException( + sm.getString("pojoMethodMapping.duplicateAnnotation", OnClose.class, currentClazz)); + } + } + } else if (method.getAnnotation(OnError.class) != null) { + checkPublic(method); + if (error == null) { + error = method; + } else { + if (currentClazz == clazzPojo || !isMethodOverride(error, method)) { + // Duplicate annotation + throw new DeploymentException( + sm.getString("pojoMethodMapping.duplicateAnnotation", OnError.class, currentClazz)); + } + } + } else if (method.getAnnotation(OnMessage.class) != null) { + checkPublic(method); + MessageHandlerInfo messageHandler = new MessageHandlerInfo(method, decoders); + boolean found = false; + for (MessageHandlerInfo otherMessageHandler : onMessage) { + if (messageHandler.targetsSameWebSocketMessageType(otherMessageHandler)) { + found = true; + if (currentClazz == clazzPojo || + !isMethodOverride(messageHandler.m, otherMessageHandler.m)) { + // Duplicate annotation + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateAnnotation", + OnMessage.class, currentClazz)); + } + } + } + if (!found) { + onMessage.add(messageHandler); + } + } else { + // Method not annotated + } + } + currentClazz = currentClazz.getSuperclass(); + } + // If the methods are not on clazzPojo and they are overridden + // by a non annotated method in clazzPojo, they should be ignored + if (open != null && open.getDeclaringClass() != clazzPojo) { + if (isOverridenWithoutAnnotation(clazzPojoMethods, open, OnOpen.class)) { + open = null; + } + } + if (close != null && close.getDeclaringClass() != clazzPojo) { + if (isOverridenWithoutAnnotation(clazzPojoMethods, close, OnClose.class)) { + close = null; + } + } + if (error != null && error.getDeclaringClass() != clazzPojo) { + if (isOverridenWithoutAnnotation(clazzPojoMethods, error, OnError.class)) { + error = null; + } + } + List overriddenOnMessage = new ArrayList<>(); + for (MessageHandlerInfo messageHandler : onMessage) { + if (messageHandler.m.getDeclaringClass() != clazzPojo && + isOverridenWithoutAnnotation(clazzPojoMethods, messageHandler.m, OnMessage.class)) { + overriddenOnMessage.add(messageHandler); + } + } + for (MessageHandlerInfo messageHandler : overriddenOnMessage) { + onMessage.remove(messageHandler); + } + this.onOpen = open; + this.onClose = close; + this.onError = error; + onOpenParams = getPathParams(onOpen, MethodType.ON_OPEN); + onCloseParams = getPathParams(onClose, MethodType.ON_CLOSE); + onErrorParams = getPathParams(onError, MethodType.ON_ERROR); + } + + + private void checkPublic(Method m) throws DeploymentException { + if (!Modifier.isPublic(m.getModifiers())) { + throw new DeploymentException(sm.getString("pojoMethodMapping.methodNotPublic", m.getName())); + } + } + + + private boolean isMethodOverride(Method method1, Method method2) { + return method1.getName().equals(method2.getName()) && method1.getReturnType().equals(method2.getReturnType()) && + Arrays.equals(method1.getParameterTypes(), method2.getParameterTypes()); + } + + + private boolean isOverridenWithoutAnnotation(Method[] methods, Method superclazzMethod, + Class annotation) { + for (Method method : methods) { + if (isMethodOverride(method, superclazzMethod) && (method.getAnnotation(annotation) == null)) { + return true; + } + } + return false; + } + + + public String getWsPath() { + return wsPath; + } + + + public Method getOnOpen() { + return onOpen; + } + + + public Object[] getOnOpenArgs(Map pathParameters, Session session, EndpointConfig config) + throws DecodeException { + return buildArgs(onOpenParams, pathParameters, session, config, null, null); + } + + + public Method getOnClose() { + return onClose; + } + + + public Object[] getOnCloseArgs(Map pathParameters, Session session, CloseReason closeReason) + throws DecodeException { + return buildArgs(onCloseParams, pathParameters, session, null, null, closeReason); + } + + + public Method getOnError() { + return onError; + } + + + public Object[] getOnErrorArgs(Map pathParameters, Session session, Throwable throwable) + throws DecodeException { + return buildArgs(onErrorParams, pathParameters, session, null, throwable, null); + } + + + public boolean hasMessageHandlers() { + return !onMessage.isEmpty(); + } + + + public Set getMessageHandlers(Object pojo, Map pathParameters, Session session, + EndpointConfig config) { + Set result = new HashSet<>(); + for (MessageHandlerInfo messageMethod : onMessage) { + result.addAll(messageMethod.getMessageHandlers(pojo, pathParameters, session, config)); + } + return result; + } + + + private static PojoPathParam[] getPathParams(Method m, MethodType methodType) throws DeploymentException { + if (m == null) { + return new PojoPathParam[0]; + } + boolean foundThrowable = false; + Class[] types = m.getParameterTypes(); + Annotation[][] paramsAnnotations = m.getParameterAnnotations(); + PojoPathParam[] result = new PojoPathParam[types.length]; + for (int i = 0; i < types.length; i++) { + Class type = types[i]; + if (type.equals(Session.class)) { + result[i] = new PojoPathParam(type, null); + } else if (methodType == MethodType.ON_OPEN && type.equals(EndpointConfig.class)) { + result[i] = new PojoPathParam(type, null); + } else if (methodType == MethodType.ON_ERROR && type.equals(Throwable.class)) { + foundThrowable = true; + result[i] = new PojoPathParam(type, null); + } else if (methodType == MethodType.ON_CLOSE && type.equals(CloseReason.class)) { + result[i] = new PojoPathParam(type, null); + } else { + Annotation[] paramAnnotations = paramsAnnotations[i]; + for (Annotation paramAnnotation : paramAnnotations) { + if (paramAnnotation.annotationType().equals(PathParam.class)) { + result[i] = new PojoPathParam(type, ((PathParam) paramAnnotation).value()); + break; + } + } + // Parameters without annotations are not permitted + if (result[i] == null) { + throw new DeploymentException(sm.getString("pojoMethodMapping.paramWithoutAnnotation", type, + m.getName(), m.getClass().getName())); + } + } + } + if (methodType == MethodType.ON_ERROR && !foundThrowable) { + throw new DeploymentException( + sm.getString("pojoMethodMapping.onErrorNoThrowable", m.getName(), m.getDeclaringClass().getName())); + } + return result; + } + + + private static Object[] buildArgs(PojoPathParam[] pathParams, Map pathParameters, Session session, + EndpointConfig config, Throwable throwable, CloseReason closeReason) throws DecodeException { + Object[] result = new Object[pathParams.length]; + for (int i = 0; i < pathParams.length; i++) { + Class type = pathParams[i].getType(); + if (type.equals(Session.class)) { + result[i] = session; + } else if (type.equals(EndpointConfig.class)) { + result[i] = config; + } else if (type.equals(Throwable.class)) { + result[i] = throwable; + } else if (type.equals(CloseReason.class)) { + result[i] = closeReason; + } else { + String name = pathParams[i].getName(); + String value = pathParameters.get(name); + try { + result[i] = Util.coerceToType(type, value); + } catch (Exception e) { + throw new DecodeException(value, sm.getString("pojoMethodMapping.decodePathParamFail", value, type), + e); + } + } + } + return result; + } + + + private static class MessageHandlerInfo { + + private final Method m; + private int indexString = -1; + private int indexByteArray = -1; + private int indexByteBuffer = -1; + private int indexPong = -1; + private int indexBoolean = -1; + private int indexSession = -1; + private int indexInputStream = -1; + private int indexReader = -1; + private int indexPrimitive = -1; + private Map indexPathParams = new HashMap<>(); + private int indexPayload = -1; + private DecoderMatch decoderMatch = null; + private long maxMessageSize = -1; + + MessageHandlerInfo(Method m, List decoderEntries) throws DeploymentException { + + this.m = m; + + Class[] types = m.getParameterTypes(); + Annotation[][] paramsAnnotations = m.getParameterAnnotations(); + + for (int i = 0; i < types.length; i++) { + boolean paramFound = false; + Annotation[] paramAnnotations = paramsAnnotations[i]; + for (Annotation paramAnnotation : paramAnnotations) { + if (paramAnnotation.annotationType().equals(PathParam.class)) { + indexPathParams.put(Integer.valueOf(i), + new PojoPathParam(types[i], ((PathParam) paramAnnotation).value())); + paramFound = true; + break; + } + } + if (paramFound) { + continue; + } + if (String.class.isAssignableFrom(types[i])) { + if (indexString == -1) { + indexString = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", + m.getName(), m.getDeclaringClass().getName())); + } + } else if (Reader.class.isAssignableFrom(types[i])) { + if (indexReader == -1) { + indexReader = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", + m.getName(), m.getDeclaringClass().getName())); + } + } else if (boolean.class == types[i]) { + if (indexBoolean == -1) { + indexBoolean = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateLastParam", m.getName(), + m.getDeclaringClass().getName())); + } + } else if (ByteBuffer.class.isAssignableFrom(types[i])) { + if (indexByteBuffer == -1) { + indexByteBuffer = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", + m.getName(), m.getDeclaringClass().getName())); + } + } else if (byte[].class == types[i]) { + if (indexByteArray == -1) { + indexByteArray = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", + m.getName(), m.getDeclaringClass().getName())); + } + } else if (InputStream.class.isAssignableFrom(types[i])) { + if (indexInputStream == -1) { + indexInputStream = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", + m.getName(), m.getDeclaringClass().getName())); + } + } else if (Util.isPrimitive(types[i])) { + if (indexPrimitive == -1) { + indexPrimitive = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", + m.getName(), m.getDeclaringClass().getName())); + } + } else if (Session.class.isAssignableFrom(types[i])) { + if (indexSession == -1) { + indexSession = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateSessionParam", + m.getName(), m.getDeclaringClass().getName())); + } + } else if (PongMessage.class.isAssignableFrom(types[i])) { + if (indexPong == -1) { + indexPong = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicatePongMessageParam", + m.getName(), m.getDeclaringClass().getName())); + } + } else { + if (decoderMatch != null && decoderMatch.hasMatches()) { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", + m.getName(), m.getDeclaringClass().getName())); + } + decoderMatch = new DecoderMatch(types[i], decoderEntries); + + if (decoderMatch.hasMatches()) { + indexPayload = i; + } else { + throw new DeploymentException(sm.getString("pojoMethodMapping.noDecoder", m.getName(), + m.getDeclaringClass().getName())); + } + } + } + + // Additional checks required + if (indexString != -1) { + if (indexPayload != -1) { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", m.getName(), + m.getDeclaringClass().getName())); + } else { + indexPayload = indexString; + } + } + if (indexReader != -1) { + if (indexPayload != -1) { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", m.getName(), + m.getDeclaringClass().getName())); + } else { + indexPayload = indexReader; + } + } + if (indexByteArray != -1) { + if (indexPayload != -1) { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", m.getName(), + m.getDeclaringClass().getName())); + } else { + indexPayload = indexByteArray; + } + } + if (indexByteBuffer != -1) { + if (indexPayload != -1) { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", m.getName(), + m.getDeclaringClass().getName())); + } else { + indexPayload = indexByteBuffer; + } + } + if (indexInputStream != -1) { + if (indexPayload != -1) { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", m.getName(), + m.getDeclaringClass().getName())); + } else { + indexPayload = indexInputStream; + } + } + if (indexPrimitive != -1) { + if (indexPayload != -1) { + throw new DeploymentException(sm.getString("pojoMethodMapping.duplicateMessageParam", m.getName(), + m.getDeclaringClass().getName())); + } else { + indexPayload = indexPrimitive; + } + } + if (indexPong != -1) { + if (indexPayload != -1) { + throw new DeploymentException(sm.getString("pojoMethodMapping.pongWithPayload", m.getName(), + m.getDeclaringClass().getName())); + } else { + indexPayload = indexPong; + } + } + if (indexPayload == -1 && indexPrimitive == -1 && indexBoolean != -1) { + // The boolean we found is a payload, not a last flag + indexPayload = indexBoolean; + indexPrimitive = indexBoolean; + indexBoolean = -1; + } + if (indexPayload == -1) { + throw new DeploymentException( + sm.getString("pojoMethodMapping.noPayload", m.getName(), m.getDeclaringClass().getName())); + } + if (indexPong != -1 && indexBoolean != -1) { + throw new DeploymentException( + sm.getString("pojoMethodMapping.partialPong", m.getName(), m.getDeclaringClass().getName())); + } + if (indexReader != -1 && indexBoolean != -1) { + throw new DeploymentException( + sm.getString("pojoMethodMapping.partialReader", m.getName(), m.getDeclaringClass().getName())); + } + if (indexInputStream != -1 && indexBoolean != -1) { + throw new DeploymentException(sm.getString("pojoMethodMapping.partialInputStream", m.getName(), + m.getDeclaringClass().getName())); + } + if (decoderMatch != null && decoderMatch.hasMatches() && indexBoolean != -1) { + throw new DeploymentException( + sm.getString("pojoMethodMapping.partialObject", m.getName(), m.getDeclaringClass().getName())); + } + + maxMessageSize = m.getAnnotation(OnMessage.class).maxMessageSize(); + } + + + public boolean targetsSameWebSocketMessageType(MessageHandlerInfo otherHandler) { + if (otherHandler == null) { + return false; + } + + return isPong() && otherHandler.isPong() || isBinary() && otherHandler.isBinary() || + isText() && otherHandler.isText(); + } + + + private boolean isPong() { + return indexPong >= 0; + } + + + private boolean isText() { + return indexString >= 0 || indexPrimitive >= 0 || indexReader >= 0 || + (decoderMatch != null && decoderMatch.getTextDecoders().size() > 0); + } + + + private boolean isBinary() { + return indexByteArray >= 0 || indexByteBuffer >= 0 || indexInputStream >= 0 || + (decoderMatch != null && decoderMatch.getBinaryDecoders().size() > 0); + } + + + public Set getMessageHandlers(Object pojo, Map pathParameters, Session session, + EndpointConfig config) { + Object[] params = new Object[m.getParameterTypes().length]; + + for (Map.Entry entry : indexPathParams.entrySet()) { + PojoPathParam pathParam = entry.getValue(); + String valueString = pathParameters.get(pathParam.getName()); + Object value = null; + try { + value = Util.coerceToType(pathParam.getType(), valueString); + } catch (Exception e) { + DecodeException de = new DecodeException(valueString, + sm.getString("pojoMethodMapping.decodePathParamFail", valueString, pathParam.getType()), e); + params = new Object[] { de }; + break; + } + params[entry.getKey().intValue()] = value; + } + + Set results = new HashSet<>(2); + if (indexBoolean == -1) { + // Basic + if (indexString != -1 || indexPrimitive != -1) { + MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m, session, config, null, params, + indexPayload, false, indexSession, maxMessageSize); + results.add(mh); + } else if (indexReader != -1) { + MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m, session, config, null, params, + indexReader, true, indexSession, maxMessageSize); + results.add(mh); + } else if (indexByteArray != -1) { + MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo, m, session, config, null, params, + indexByteArray, true, indexSession, false, maxMessageSize); + results.add(mh); + } else if (indexByteBuffer != -1) { + MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo, m, session, config, null, params, + indexByteBuffer, false, indexSession, false, maxMessageSize); + results.add(mh); + } else if (indexInputStream != -1) { + MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo, m, session, config, null, params, + indexInputStream, true, indexSession, true, maxMessageSize); + results.add(mh); + } else if (decoderMatch != null && decoderMatch.hasMatches()) { + if (decoderMatch.getBinaryDecoders().size() > 0) { + MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo, m, session, config, + decoderMatch.getBinaryDecoders(), params, indexPayload, true, indexSession, true, + maxMessageSize); + results.add(mh); + } + if (decoderMatch.getTextDecoders().size() > 0) { + MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m, session, config, + decoderMatch.getTextDecoders(), params, indexPayload, true, indexSession, + maxMessageSize); + results.add(mh); + } + } else { + MessageHandler mh = new PojoMessageHandlerWholePong(pojo, m, session, params, indexPong, false, + indexSession); + results.add(mh); + } + } else { + // ASync + if (indexString != -1) { + MessageHandler mh = new PojoMessageHandlerPartialText(pojo, m, session, params, indexString, false, + indexBoolean, indexSession, maxMessageSize); + results.add(mh); + } else if (indexByteArray != -1) { + MessageHandler mh = new PojoMessageHandlerPartialBinary(pojo, m, session, params, indexByteArray, + true, indexBoolean, indexSession, maxMessageSize); + results.add(mh); + } else { + MessageHandler mh = new PojoMessageHandlerPartialBinary(pojo, m, session, params, indexByteBuffer, + false, indexBoolean, indexSession, maxMessageSize); + results.add(mh); + } + } + return results; + } + } + + + private enum MethodType { + ON_OPEN, + ON_CLOSE, + ON_ERROR + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/PojoPathParam.java b/java/org/apache/tomcat/websocket/pojo/PojoPathParam.java new file mode 100644 index 0000000..67a8eb3 --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/PojoPathParam.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat.websocket.pojo; + +import jakarta.websocket.DeploymentException; + +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.Util; + +/** + * Stores the parameter type and name for a parameter that needs to be passed to an onXxx method of + * {@link jakarta.websocket.Endpoint}. The name is only present for parameters annotated with + * {@link jakarta.websocket.server.PathParam}. For the {@link jakarta.websocket.Session} and {@link java.lang.Throwable} + * parameters, {@link #getName()} will always return null. + */ +public class PojoPathParam { + + private static final StringManager sm = StringManager.getManager(PojoPathParam.class); + + private final Class type; + private final String name; + + + public PojoPathParam(Class type, String name) throws DeploymentException { + if (name != null) { + // Annotated as @PathParam so validate type + validateType(type); + } + this.type = type; + this.name = name; + } + + + public Class getType() { + return type; + } + + + public String getName() { + return name; + } + + + private static void validateType(Class type) throws DeploymentException { + if (String.class == type) { + return; + } + if (Util.isPrimitive(type)) { + return; + } + throw new DeploymentException(sm.getString("pojoPathParam.wrongType", type.getName())); + } +} diff --git a/java/org/apache/tomcat/websocket/pojo/package-info.java b/java/org/apache/tomcat/websocket/pojo/package-info.java new file mode 100644 index 0000000..e23ad3d --- /dev/null +++ b/java/org/apache/tomcat/websocket/pojo/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ +/** + * This package provides the necessary plumbing to convert an annotated POJO into a WebSocket + * {@link jakarta.websocket.Endpoint}. + */ +package org.apache.tomcat.websocket.pojo; \ No newline at end of file diff --git a/java/org/apache/tomcat/websocket/server/Constants.java b/java/org/apache/tomcat/websocket/server/Constants.java new file mode 100644 index 0000000..5aa99bb --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/Constants.java @@ -0,0 +1,33 @@ +/* + * 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.tomcat.websocket.server; + +/** + * Internal implementation constants. + */ +public class Constants { + + public static final String BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM = "org.apache.tomcat.websocket.binaryBufferSize"; + public static final String TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM = "org.apache.tomcat.websocket.textBufferSize"; + + public static final String SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE = "jakarta.websocket.server.ServerContainer"; + + + private Constants() { + // Hide default constructor + } +} diff --git a/java/org/apache/tomcat/websocket/server/DefaultServerEndpointConfigurator.java b/java/org/apache/tomcat/websocket/server/DefaultServerEndpointConfigurator.java new file mode 100644 index 0000000..24ed9e5 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/DefaultServerEndpointConfigurator.java @@ -0,0 +1,84 @@ +/* + * 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.tomcat.websocket.server; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.websocket.Extension; +import jakarta.websocket.HandshakeResponse; +import jakarta.websocket.server.HandshakeRequest; +import jakarta.websocket.server.ServerEndpointConfig; + +@aQute.bnd.annotation.spi.ServiceProvider(value = ServerEndpointConfig.Configurator.class) +public class DefaultServerEndpointConfigurator extends ServerEndpointConfig.Configurator { + + @Override + public T getEndpointInstance(Class clazz) throws InstantiationException { + try { + return clazz.getConstructor().newInstance(); + } catch (InstantiationException e) { + throw e; + } catch (ReflectiveOperationException e) { + InstantiationException ie = new InstantiationException(); + ie.initCause(e); + throw ie; + } + } + + + @Override + public String getNegotiatedSubprotocol(List supported, List requested) { + + for (String request : requested) { + if (supported.contains(request)) { + return request; + } + } + return ""; + } + + + @Override + public List getNegotiatedExtensions(List installed, List requested) { + Set installedNames = new HashSet<>(); + for (Extension e : installed) { + installedNames.add(e.getName()); + } + List result = new ArrayList<>(); + for (Extension request : requested) { + if (installedNames.contains(request.getName())) { + result.add(request); + } + } + return result; + } + + + @Override + public boolean checkOrigin(String originHeaderValue) { + return true; + } + + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + // NO-OP + } + +} diff --git a/java/org/apache/tomcat/websocket/server/LocalStrings.properties b/java/org/apache/tomcat/websocket/server/LocalStrings.properties new file mode 100644 index 0000000..063b2c8 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/LocalStrings.properties @@ -0,0 +1,40 @@ +# 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. + +serverContainer.configuratorFail=Failed to create configurator of type [{0}] for POJO of type [{1}] +serverContainer.duplicatePaths=Multiple Endpoints may not be deployed to the same path [{0}] : existing endpoint was [{1}] and new endpoint is [{2}] +serverContainer.encoderFail=Unable to create encoder of type [{0}] +serverContainer.failedDeployment=Deployment of WebSocket Endpoints to the web application with path [{0}] in host [{1}] is not permitted due to the failure of a previous deployment +serverContainer.missingAnnotation=Cannot deploy POJO class [{0}] as it is not annotated with @ServerEndpoint +serverContainer.servletContextMissing=No ServletContext was specified + +upgradeUtil.incompatibleRsv=Extensions were specified that have incompatible RSV bit usage +upgradeUtil.pojoMapFail=Unable to complete method mapping for POJO class [{0}] + +uriTemplate.duplicateParameter=The parameter [{0}] appears more than once in the path which is not permitted +uriTemplate.emptySegment=The path [{0}] contains one or more empty segments which is not permitted +uriTemplate.invalidPath=The path [{0}] is not valid. +uriTemplate.invalidSegment=The segment [{0}] is not valid in the provided path [{1}] + +wsFrameServer.bytesRead=Read [{0}] bytes into input buffer ready for processing +wsFrameServer.illegalReadState=Unexpected read state [{0}] +wsFrameServer.onDataAvailable=Method entry + +wsHttpUpgradeHandler.closeOnError=Closing WebSocket connection due to an error +wsHttpUpgradeHandler.destroyFailed=Failed to close WebConnection while destroying the WebSocket HttpUpgradeHandler +wsHttpUpgradeHandler.noPreInit=The preInit() method must be called to configure the WebSocket HttpUpgradeHandler before the container calls init(). Usually, this means the Servlet that created the WsHttpUpgradeHandler instance should also call preInit() +wsHttpUpgradeHandler.serverStop=The server is stopping + +wsRemoteEndpointServer.closeFailed=Failed to close the ServletOutputStream connection cleanly diff --git a/java/org/apache/tomcat/websocket/server/LocalStrings_cs.properties b/java/org/apache/tomcat/websocket/server/LocalStrings_cs.properties new file mode 100644 index 0000000..b0bfde1 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/LocalStrings_cs.properties @@ -0,0 +1,16 @@ +# 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. + +wsFrameServer.bytesRead=NaÄtených [{0}] bytů do vstupního bufferu je pÅ™ipraveno na zpracování diff --git a/java/org/apache/tomcat/websocket/server/LocalStrings_de.properties b/java/org/apache/tomcat/websocket/server/LocalStrings_de.properties new file mode 100644 index 0000000..cd4a292 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/LocalStrings_de.properties @@ -0,0 +1,21 @@ +# 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. + +serverContainer.missingAnnotation=Die POJO Klasse [{0}] kann nicht deployed werden da sie nicht mit @ServerEndpoint annotiert ist. +serverContainer.servletContextMissing=Es wurde kein ServletContext angegeben + +upgradeUtil.incompatibleRsv=Es wurden Erweiterungen spezifiziert, die eine inkompatible RSV Bit Konstellation erzeugen + +uriTemplate.invalidPath=Der Pfad [{0}] ist nicht gültig. diff --git a/java/org/apache/tomcat/websocket/server/LocalStrings_es.properties b/java/org/apache/tomcat/websocket/server/LocalStrings_es.properties new file mode 100644 index 0000000..cd87615 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/LocalStrings_es.properties @@ -0,0 +1,21 @@ +# 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. + +serverContainer.configuratorFail=Fallo al crear configurador de tipo [{0}] para el POJO de tipo [{1}] +serverContainer.encoderFail=Imposible crear codificador de tipo [{0}] + +uriTemplate.invalidPath=El camino [{0}] no es válido.\n + +wsFrameServer.bytesRead=[{0}] leídos del bufer de entrada llistos para ser procesados diff --git a/java/org/apache/tomcat/websocket/server/LocalStrings_fr.properties b/java/org/apache/tomcat/websocket/server/LocalStrings_fr.properties new file mode 100644 index 0000000..ee86986 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/LocalStrings_fr.properties @@ -0,0 +1,40 @@ +# 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. + +serverContainer.configuratorFail=Echec de création du configurateur de type [{0}] pour le POJO de type [{1}] +serverContainer.duplicatePaths=Plusieurs terminaisons ne peuvent pas être déployés vers le même chemin [{0}] : la terminaison existante était [{1}] et la nouvelle est [{2}] +serverContainer.encoderFail=Impossible de créer un encodeur de type [{0}] +serverContainer.failedDeployment=Le déploiement de terminaisons WebSocket dans l''application web au chemin [{0}] dans l''hôte [{1}] n''est pas autorisé à cause de l''échec lors d''un précédent déploiement +serverContainer.missingAnnotation=Impossible de déployer la classe POJO [{0}] car elle n''a pas été annotée avec @ServerEndpoint +serverContainer.servletContextMissing=Aucun ServletContext n'a été spécifié + +upgradeUtil.incompatibleRsv=Des extensions qui ont été spécifiées ont une utilisation incompatible du bit RSV +upgradeUtil.pojoMapFail=Impossible de compléter le mappage des méthodes pour la classe POJO [{0}] + +uriTemplate.duplicateParameter=Le paramètre [{0}] apparaît plus d''une fois dans le chemin ce qui n''est pas permis +uriTemplate.emptySegment=Le chemin [{0}] contient un ou plusieurs segments vide ce qui n''est pas autorisé +uriTemplate.invalidPath=Le chemin [{0}] est invalide +uriTemplate.invalidSegment=Le segment [{0}] est invalide pour le chemin fourni [{1}] + +wsFrameServer.bytesRead=Lu [{0}] octets dans le buffer de réception prêts à être traités +wsFrameServer.illegalReadState=Etat de lecture inattendu [{0}] +wsFrameServer.onDataAvailable=Entrée de méthode + +wsHttpUpgradeHandler.closeOnError=Fermeture de la connection WebSocket à cause d'une erreur +wsHttpUpgradeHandler.destroyFailed=Echec de la fermeture de la WebConnection lors de la destruction du HttpUpgradeHandler de WebSocket +wsHttpUpgradeHandler.noPreInit=La méthode preInit() doit être appelée pour configurer le HttpUpgradeHandler de Websockets avant que le container n'appelle init(), cela veut habituellement dire que le Servlet qui a crée l'instance du WsHttpUpgradeHandler doit aussi appeler preInit() +wsHttpUpgradeHandler.serverStop=Le serveur est en train de s'arrêter + +wsRemoteEndpointServer.closeFailed=Impossible de fermer le ServletOutputStream proprement diff --git a/java/org/apache/tomcat/websocket/server/LocalStrings_ja.properties b/java/org/apache/tomcat/websocket/server/LocalStrings_ja.properties new file mode 100644 index 0000000..b219d63 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/LocalStrings_ja.properties @@ -0,0 +1,40 @@ +# 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. + +serverContainer.configuratorFail=POJO クラス [{1}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’構æˆã™ã‚‹ã‚¯ãƒ©ã‚¹ [{0}] ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’作æˆã§ãã¾ã›ã‚“。 +serverContainer.duplicatePaths=複数ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã‚’åŒã˜ãƒ‘ス [{0}] ã«é…å‚™ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“: 既存ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã¯ [{1}] ã§ã€æ–°ã—ã„エンドãƒã‚¤ãƒ³ãƒˆã¯ [{2}] ã§ã™ +serverContainer.encoderFail=タイプ [{0}] ã®ã‚¨ãƒ³ã‚³ãƒ¼ãƒ€ã‚’作æˆã§ãã¾ã›ã‚“ +serverContainer.failedDeployment=以å‰ã®é…å‚™ãŒå¤±æ•—ã—ãŸãŸã‚ã€ãƒ›ã‚¹ãƒˆ [{1}] 内ã®ãƒ‘ス [{0}] ã‚’æŒã¤Webアプリケーションã¸ã®WebSocketエンドãƒã‚¤ãƒ³ãƒˆã®é…å‚™ã¯è¨±å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“ +serverContainer.missingAnnotation=POJOクラス [{0}] ã¯@ServerEndpointã§ã‚¢ãƒŽãƒ†ãƒ¼ã‚·ãƒ§ãƒ³ä»˜ã‘ã•ã‚Œã¦ã„ãªã„ãŸã‚ã€é…å‚™ã§ãã¾ã›ã‚“ +serverContainer.servletContextMissing=ServletContextãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“ + +upgradeUtil.incompatibleRsv=互æ›æ€§ã®ãªã„RSVビットã®ä½¿ç”¨æ³•ã‚’æŒã¤æ‹¡å¼µãŒæŒ‡å®šã•ã‚Œã¾ã—㟠+upgradeUtil.pojoMapFail=POJOクラス [{0}] ã®ãƒ¡ã‚½ãƒƒãƒ‰ãƒžãƒƒãƒ”ングを完了ã§ãã¾ã›ã‚“ + +uriTemplate.duplicateParameter=パス内ã«ãƒ‘ラメーター [{0}] を複数回登場ã•ã›ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ +uriTemplate.emptySegment=パス [{0}] ã«ä¸€ã¤ä»¥ä¸Šã®ç©ºã‚»ã‚°ãƒ¡ãƒ³ãƒˆã‚’å«ã‚ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 +uriTemplate.invalidPath=[{0}] ã¯ç„¡åŠ¹ãªãƒ‘スã§ã™ã€‚ +uriTemplate.invalidSegment=パス [{1}] ã«å­˜åœ¨ã—ãªã„セグメント [{0}] ãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ + +wsFrameServer.bytesRead=入力ãƒãƒƒãƒ•ã‚¡ãƒ¼ã«èª­ã¿è¾¼ã‚“ã  [{0}] ãƒã‚¤ãƒˆã®ãƒ‡ãƒ¼ã‚¿ã¯å‡¦ç†å¯èƒ½ã§ã™ã€‚ +wsFrameServer.illegalReadState=予期ã—ãªã„読ã¿å–り状態[{0}] +wsFrameServer.onDataAvailable=メソッドエントリ + +wsHttpUpgradeHandler.closeOnError=エラーãŒç™ºç”Ÿã—ãŸãŸã‚ WebSocket コãƒã‚¯ã‚·ãƒ§ãƒ³ã‚’切断ã—ã¾ã™ã€‚ +wsHttpUpgradeHandler.destroyFailed=WebSocket HttpUpgradeHandlerを破棄ã—ã¦ã„ã‚‹é–“ã«WebConnectionã‚’é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ +wsHttpUpgradeHandler.noPreInit=コンテナãŒinit()を呼ã³å‡ºã™å‰ã«ã€preInit()メソッドを呼ã³å‡ºã™ã‚ˆã†ã«WebSocket HttpUpgradeHandlerを設定ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚通常ã€ã“ã‚Œã¯WsHttpUpgradeHandlerインスタンスを作æˆã—ãŸã‚µãƒ¼ãƒ–レットãŒpreInit()を呼ã³å‡ºã™å¿…è¦ãŒã‚ã‚‹ã“ã¨ã‚’æ„味ã—ã¾ã™ã€‚ +wsHttpUpgradeHandler.serverStop=サーãƒåœæ­¢ä¸­ + +wsRemoteEndpointServer.closeFailed=ServletOutputStreamコãƒã‚¯ã‚·ãƒ§ãƒ³ã‚’正常ã«é–‰ã˜ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—㟠diff --git a/java/org/apache/tomcat/websocket/server/LocalStrings_ko.properties b/java/org/apache/tomcat/websocket/server/LocalStrings_ko.properties new file mode 100644 index 0000000..d80cbbb --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/LocalStrings_ko.properties @@ -0,0 +1,40 @@ +# 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. + +serverContainer.configuratorFail=íƒ€ìž…ì´ [{1}]ì¸ POJO를 위한 타입 [{0}]ì˜ Configurator를 ìƒì„±í•˜ì§€ 못했습니다. +serverContainer.duplicatePaths=여러 ê°œì˜ ì—”ë“œí¬ì¸íŠ¸ë“¤ì´ ë™ì¼í•œ 경로 [{0}]ì— ë°°ì¹˜ë  ìˆ˜ 없습니다: 기존 엔드í¬ì¸íŠ¸ëŠ” [{1}]였으며 ì‹ ê·œ 엔드í¬ì¸íŠ¸ëŠ” [{2}]입니다. +serverContainer.encoderFail=íƒ€ìž…ì´ [{0}]ì¸ ì¸ì½”ë”를 ìƒì„±í•  수 없습니다. +serverContainer.failedDeployment=ì´ì „ ë°°ì¹˜ì˜ ì‹¤íŒ¨ë¡œ ì¸í•˜ì—¬, 호스트 [{1}] ë‚´ì— ê²½ë¡œ [{0}]ì˜ ì›¹ 애플리케ì´ì…˜ì— 대한 웹소켓 엔드í¬ì¸íŠ¸ë“¤ì˜ 배치가 허용ë˜ì§€ 않습니다. +serverContainer.missingAnnotation=í´ëž˜ìŠ¤ê°€ @ServerEndpointë¡œ annotateë˜ì–´ 있지 않기ì—, POJO í´ëž˜ìŠ¤ [{0}]ì„(를) 배치할 수 없습니다. +serverContainer.servletContextMissing=ì§€ì •ëœ ServletContextê°€ 없습니다. + +upgradeUtil.incompatibleRsv=호환ë˜ì§€ 않는 RSV 비트를 사용하여, Extensionë“¤ì´ ì§€ì •ë˜ì—ˆìŠµë‹ˆë‹¤. +upgradeUtil.pojoMapFail=POJO í´ëž˜ìŠ¤ [{0}](ì„)를 위한 메소드 ë§¤í•‘ì„ ì™„ë£Œí•  수 없습니다. + +uriTemplate.duplicateParameter=허용ë˜ì§€ 않는 경로ì—ì„œ, 파ë¼ë¯¸í„° [{0}]ì´(ê°€) ë‘번 ì´ìƒ 나타나고 있습니다. +uriTemplate.emptySegment=경로 [{0}]ì´(ê°€), 하나 ì´ìƒì˜ 허용ë˜ì§€ 않는 empty segmentë“¤ì„ í¬í•¨í•˜ê³  있습니다. +uriTemplate.invalidPath=경로 [{0}](ì€)는 유효하지 않습니다. +uriTemplate.invalidSegment=세그먼트 [{0}]ì€(는) ì œê³µëœ ê²½ë¡œ [{1}] ë‚´ì— ìœ íš¨í•˜ì§€ 않습니다. + +wsFrameServer.bytesRead=[{0}] ë°”ì´íŠ¸ë¥¼ ìž…ë ¥ 버í¼ì— ì½ì–´ 처리를 준비합니다. +wsFrameServer.illegalReadState=예기치 ì•Šì€ ì½ê¸° ìƒíƒœ [{0}] +wsFrameServer.onDataAvailable=메소드 엔트리 + +wsHttpUpgradeHandler.closeOnError=오류 ë°œìƒìœ¼ë¡œ ì¸í•˜ì—¬, 웹소켓 ì—°ê²°ì„ ë‹«ìŠµë‹ˆë‹¤. +wsHttpUpgradeHandler.destroyFailed=웹소켓 HttpUpgradeHandler를 소멸시키는 중, WebConnectionì„ ë‹«ì§€ 못했습니다. +wsHttpUpgradeHandler.noPreInit=컨테ì´ë„ˆê°€ init()ì„ í˜¸ì¶œí•˜ê¸° ì „ì— ì›¹ì†Œì¼“ HttpUpgradeHandler를 설정하기 위하여, preInit() 메소드가 반드시 호출ë˜ì–´ì•¼ë§Œ 합니다. í†µìƒ ì´ëŠ” WsHttpUpgradeHandler ì¸ìŠ¤í„´ìŠ¤ë¥¼ ìƒì„±í•œ ì„œë¸”ë¦¿ë„ preInit()ì„ í˜¸ì¶œí•´ì•¼ í•¨ì„ ì˜ë¯¸í•©ë‹ˆë‹¤. +wsHttpUpgradeHandler.serverStop=서버가 중지ë˜ê³  있는 중입니다. + +wsRemoteEndpointServer.closeFailed=해당 ServletOutputStreamì˜ ì—°ê²°ì„ ê¹¨ë—하게 닫지 못했습니다. diff --git a/java/org/apache/tomcat/websocket/server/LocalStrings_zh_CN.properties b/java/org/apache/tomcat/websocket/server/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..2ff2163 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/LocalStrings_zh_CN.properties @@ -0,0 +1,40 @@ +# 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. + +serverContainer.configuratorFail=无法为[{1}]类型的POJO创建类型[{0}]çš„é…ç½®ç¨‹åº +serverContainer.duplicatePaths=多个端点å¯èƒ½ä¸èƒ½å‘ä¸åˆ°åŒä¸€ä¸ªè·¯å¾„[{0}]:已ç»å­˜åœ¨çš„端点[{1}]和新的端点[{2}] +serverContainer.encoderFail=无法创建[{0}]类型的编ç å™¨ +serverContainer.failedDeployment=由于以å‰çš„部署失败,ä¸å…许将WebSocket终结点部署到主机[{1}]中路径为[{0}]çš„Webåº”ç”¨ç¨‹åº +serverContainer.missingAnnotation=无法部署POJOç±»[{0}],因为它没有用@ServerEndpoint进行注释 +serverContainer.servletContextMissing=没有指定ServletContext + +upgradeUtil.incompatibleRsv=指定扩展å具有ä¸å…¼å®¹çš„RSVä½ä½¿ç”¨ +upgradeUtil.pojoMapFail=æ— æ³•å®Œæˆ POJO ç±» [{0}] 的方法映射 + +uriTemplate.duplicateParameter=å‚æ•°[{0}]在ä¸å…许的路径中出现多次 +uriTemplate.emptySegment=路径[{0}]包å«ä¸€ä¸ªæˆ–多个ä¸å…许的空段 +uriTemplate.invalidPath=路径 [{0}] 无效。 +uriTemplate.invalidSegment=段[{0}]在æ供的路径[{1}]中无效 + +wsFrameServer.bytesRead=å°†[{0}]ä¸ªå­—èŠ‚è¯»å…¥è¾“å…¥ç¼“å†²åŒºï¼Œå‡†å¤‡è¿›è¡Œå¤„ç† +wsFrameServer.illegalReadState=æ„外的读å–状æ€[{0}] +wsFrameServer.onDataAvailable=进入方法 + +wsHttpUpgradeHandler.closeOnError=由于错误关闭WebSocket连接 +wsHttpUpgradeHandler.destroyFailed=销æ¯WebSocket HttpUpgradeHandler时无法关闭WebConnection +wsHttpUpgradeHandler.noPreInit=在容器调用init()之å‰ï¼Œå¿…须调用preinit()方法æ¥é…ç½®WebSocket HttpUpgradeHandler。通常,这æ„味ç€åˆ›å»ºWsHttpUpgradeHandler 实例的servlet也应该调用preinit() +wsHttpUpgradeHandler.serverStop=æœåŠ¡å™¨æ­£åœ¨åœæ­¢ + +wsRemoteEndpointServer.closeFailed=无法完全关闭ServletOutputStream 连接 diff --git a/java/org/apache/tomcat/websocket/server/UpgradeUtil.java b/java/org/apache/tomcat/websocket/server/UpgradeUtil.java new file mode 100644 index 0000000..4c388fe --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/UpgradeUtil.java @@ -0,0 +1,345 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.Extension; +import jakarta.websocket.HandshakeResponse; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; +import org.apache.tomcat.websocket.Constants; +import org.apache.tomcat.websocket.Transformation; +import org.apache.tomcat.websocket.TransformationFactory; +import org.apache.tomcat.websocket.Util; +import org.apache.tomcat.websocket.WsHandshakeResponse; +import org.apache.tomcat.websocket.pojo.PojoMethodMapping; + +public class UpgradeUtil { + + private static final StringManager sm = StringManager.getManager(UpgradeUtil.class.getPackage().getName()); + private static final byte[] WS_ACCEPT = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + .getBytes(StandardCharsets.ISO_8859_1); + + private UpgradeUtil() { + // Utility class. Hide default constructor. + } + + /** + * Checks to see if this is an HTTP request that includes a valid upgrade request to web socket. + *

    + * Note: RFC 2616 does not limit HTTP upgrade to GET requests but the Java WebSocket spec 1.0, section 8.2 implies + * such a limitation and RFC 6455 section 4.1 requires that a WebSocket Upgrade uses GET. + * + * @param request The request to check if it is an HTTP upgrade request for a WebSocket connection + * @param response The response associated with the request + * + * @return true if the request includes an HTTP Upgrade request for the WebSocket protocol, otherwise + * false + */ + public static boolean isWebSocketUpgradeRequest(ServletRequest request, ServletResponse response) { + + return ((request instanceof HttpServletRequest) && + (response instanceof HttpServletResponse) && headerContainsToken((HttpServletRequest) request, + Constants.UPGRADE_HEADER_NAME, Constants.UPGRADE_HEADER_VALUE) && + "GET".equals(((HttpServletRequest) request).getMethod())); + } + + + public static void doUpgrade(WsServerContainer sc, HttpServletRequest req, HttpServletResponse resp, + ServerEndpointConfig sec, Map pathParams) throws ServletException, IOException { + + // Validate the rest of the headers and reject the request if that + // validation fails + String key; + String subProtocol = null; + if (!headerContainsToken(req, Constants.CONNECTION_HEADER_NAME, Constants.CONNECTION_HEADER_VALUE)) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + if (!headerContainsToken(req, Constants.WS_VERSION_HEADER_NAME, Constants.WS_VERSION_HEADER_VALUE)) { + resp.setStatus(426); + resp.setHeader(Constants.WS_VERSION_HEADER_NAME, Constants.WS_VERSION_HEADER_VALUE); + return; + } + key = req.getHeader(Constants.WS_KEY_HEADER_NAME); + if (!validateKey(key)) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + + // Origin check + String origin = req.getHeader(Constants.ORIGIN_HEADER_NAME); + if (!sec.getConfigurator().checkOrigin(origin)) { + resp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + // Sub-protocols + List subProtocols = getTokensFromHeader(req, Constants.WS_PROTOCOL_HEADER_NAME); + subProtocol = sec.getConfigurator().getNegotiatedSubprotocol(sec.getSubprotocols(), subProtocols); + + // Extensions + // Should normally only be one header but handle the case of multiple + // headers + List extensionsRequested = new ArrayList<>(); + Enumeration extHeaders = req.getHeaders(Constants.WS_EXTENSIONS_HEADER_NAME); + while (extHeaders.hasMoreElements()) { + Util.parseExtensionHeader(extensionsRequested, extHeaders.nextElement()); + } + // Negotiation phase 1. By default this simply filters out the + // extensions that the server does not support but applications could + // use a custom configurator to do more than this. + List installedExtensions = null; + if (sec.getExtensions().size() == 0) { + installedExtensions = Constants.INSTALLED_EXTENSIONS; + } else { + installedExtensions = new ArrayList<>(); + installedExtensions.addAll(sec.getExtensions()); + installedExtensions.addAll(Constants.INSTALLED_EXTENSIONS); + } + List negotiatedExtensionsPhase1 = sec.getConfigurator().getNegotiatedExtensions(installedExtensions, + extensionsRequested); + + // Negotiation phase 2. Create the Transformations that will be applied + // to this connection. Note than an extension may be dropped at this + // point if the client has requested a configuration that the server is + // unable to support. + List transformations = createTransformations(negotiatedExtensionsPhase1); + + List negotiatedExtensionsPhase2; + if (transformations.isEmpty()) { + negotiatedExtensionsPhase2 = Collections.emptyList(); + } else { + negotiatedExtensionsPhase2 = new ArrayList<>(transformations.size()); + for (Transformation t : transformations) { + negotiatedExtensionsPhase2.add(t.getExtensionResponse()); + } + } + + // Build the transformation pipeline + Transformation transformation = null; + StringBuilder responseHeaderExtensions = new StringBuilder(); + boolean first = true; + for (Transformation t : transformations) { + if (first) { + first = false; + } else { + responseHeaderExtensions.append(','); + } + append(responseHeaderExtensions, t.getExtensionResponse()); + if (transformation == null) { + transformation = t; + } else { + transformation.setNext(t); + } + } + + // Now we have the full pipeline, validate the use of the RSV bits. + if (transformation != null && !transformation.validateRsvBits(0)) { + throw new ServletException(sm.getString("upgradeUtil.incompatibleRsv")); + } + + // If we got this far, all is good. Accept the connection. + resp.setHeader(Constants.UPGRADE_HEADER_NAME, Constants.UPGRADE_HEADER_VALUE); + resp.setHeader(Constants.CONNECTION_HEADER_NAME, Constants.CONNECTION_HEADER_VALUE); + resp.setHeader(HandshakeResponse.SEC_WEBSOCKET_ACCEPT, getWebSocketAccept(key)); + if (subProtocol != null && subProtocol.length() > 0) { + // RFC6455 4.2.2 explicitly states "" is not valid here + resp.setHeader(Constants.WS_PROTOCOL_HEADER_NAME, subProtocol); + } + if (!transformations.isEmpty()) { + resp.setHeader(Constants.WS_EXTENSIONS_HEADER_NAME, responseHeaderExtensions.toString()); + } + + // Add method mapping to user properties + if (!Endpoint.class.isAssignableFrom(sec.getEndpointClass()) && sec.getUserProperties() + .get(org.apache.tomcat.websocket.pojo.Constants.POJO_METHOD_MAPPING_KEY) == null) { + // This is a POJO endpoint and the application has called upgrade + // directly. Need to add the method mapping. + try { + PojoMethodMapping methodMapping = new PojoMethodMapping(sec.getEndpointClass(), sec.getDecoders(), + sec.getPath(), sc.getInstanceManager(Thread.currentThread().getContextClassLoader())); + if (methodMapping.getOnClose() != null || methodMapping.getOnOpen() != null || + methodMapping.getOnError() != null || methodMapping.hasMessageHandlers()) { + sec.getUserProperties().put(org.apache.tomcat.websocket.pojo.Constants.POJO_METHOD_MAPPING_KEY, + methodMapping); + } + } catch (DeploymentException e) { + throw new ServletException(sm.getString("upgradeUtil.pojoMapFail", sec.getEndpointClass().getName()), + e); + } + } + + WsPerSessionServerEndpointConfig perSessionServerEndpointConfig = new WsPerSessionServerEndpointConfig(sec); + + WsHandshakeRequest wsRequest = new WsHandshakeRequest(req, pathParams); + WsHandshakeResponse wsResponse = new WsHandshakeResponse(); + sec.getConfigurator().modifyHandshake(perSessionServerEndpointConfig, wsRequest, wsResponse); + wsRequest.finished(); + + // Add any additional headers + for (Entry> entry : wsResponse.getHeaders().entrySet()) { + for (String headerValue : entry.getValue()) { + resp.addHeader(entry.getKey(), headerValue); + } + } + + WsHttpUpgradeHandler wsHandler = req.upgrade(WsHttpUpgradeHandler.class); + wsHandler.preInit(perSessionServerEndpointConfig, sc, wsRequest, negotiatedExtensionsPhase2, subProtocol, + transformation, pathParams, req.isSecure()); + + } + + + /* + * Validate the key. It should be the base64 encoding of a random 16-byte value. 16-bytes are encoded in 24 base64 + * characters, the last two of which must be ==. + * + * The validation isn't perfect: + * + * - it doesn't check the final non-'=' character is valid in the context of the number of bits it is meant to be + * encoding. + * + * - it doesn't check that the value is random and changes for each connection. + * + * Given that this header is for the benefit of the client, not the server, this should be good enough. + */ + private static boolean validateKey(String key) { + if (key == null) { + return false; + } + + if (key.length() != 24) { + return false; + } + + char[] keyChars = key.toCharArray(); + if (keyChars[22] != '=' || keyChars[23] != '=') { + return false; + } + + for (int i = 0; i < 22; i++) { + if (!Base64.isInAlphabet(keyChars[i])) { + return false; + } + } + + return true; + } + + + private static List createTransformations(List negotiatedExtensions) { + + TransformationFactory factory = TransformationFactory.getInstance(); + + LinkedHashMap>> extensionPreferences = new LinkedHashMap<>(); + + // Result will likely be smaller than this + List result = new ArrayList<>(negotiatedExtensions.size()); + + for (Extension extension : negotiatedExtensions) { + extensionPreferences.computeIfAbsent(extension.getName(), k -> new ArrayList<>()) + .add(extension.getParameters()); + } + + for (Map.Entry>> entry : extensionPreferences.entrySet()) { + Transformation transformation = factory.create(entry.getKey(), entry.getValue(), true); + if (transformation != null) { + result.add(transformation); + } + } + return result; + } + + + private static void append(StringBuilder sb, Extension extension) { + if (extension == null || extension.getName() == null || extension.getName().length() == 0) { + return; + } + + sb.append(extension.getName()); + + for (Extension.Parameter p : extension.getParameters()) { + sb.append(';'); + sb.append(p.getName()); + if (p.getValue() != null) { + sb.append('='); + sb.append(p.getValue()); + } + } + } + + + /* + * This only works for tokens. Quoted strings need more sophisticated parsing. + */ + private static boolean headerContainsToken(HttpServletRequest req, String headerName, String target) { + Enumeration headers = req.getHeaders(headerName); + while (headers.hasMoreElements()) { + String header = headers.nextElement(); + String[] tokens = header.split(","); + for (String token : tokens) { + if (target.equalsIgnoreCase(token.trim())) { + return true; + } + } + } + return false; + } + + + /* + * This only works for tokens. Quoted strings need more sophisticated parsing. + */ + private static List getTokensFromHeader(HttpServletRequest req, String headerName) { + List result = new ArrayList<>(); + Enumeration headers = req.getHeaders(headerName); + while (headers.hasMoreElements()) { + String header = headers.nextElement(); + String[] tokens = header.split(","); + for (String token : tokens) { + result.add(token.trim()); + } + } + return result; + } + + + private static String getWebSocketAccept(String key) { + byte[] digest = ConcurrentMessageDigest.digestSHA1(key.getBytes(StandardCharsets.ISO_8859_1), WS_ACCEPT); + return Base64.encodeBase64String(digest); + } +} diff --git a/java/org/apache/tomcat/websocket/server/UriTemplate.java b/java/org/apache/tomcat/websocket/server/UriTemplate.java new file mode 100644 index 0000000..4681862 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/UriTemplate.java @@ -0,0 +1,169 @@ +/* + * 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.tomcat.websocket.server; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.websocket.DeploymentException; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Extracts path parameters from URIs used to create web socket connections using the URI template defined for the + * associated Endpoint. + */ +public class UriTemplate { + + private static final StringManager sm = StringManager.getManager(UriTemplate.class); + + private final String normalized; + private final List segments = new ArrayList<>(); + private final boolean hasParameters; + + + public UriTemplate(String path) throws DeploymentException { + + if (path == null || path.length() == 0 || !path.startsWith("/") || path.contains("/../") || + path.contains("/./") || path.contains("//")) { + throw new DeploymentException(sm.getString("uriTemplate.invalidPath", path)); + } + + StringBuilder normalized = new StringBuilder(path.length()); + Set paramNames = new HashSet<>(); + + // Include empty segments. + String[] segments = path.split("/", -1); + int paramCount = 0; + int segmentCount = 0; + + for (int i = 0; i < segments.length; i++) { + String segment = segments[i]; + if (segment.length() == 0) { + if (i == 0 || (i == segments.length - 1 && paramCount == 0)) { + // Ignore the first empty segment as the path must always + // start with '/' + // Ending with a '/' is also OK for instances used for + // matches but not for parameterised templates. + continue; + } else { + // As per EG discussion, all other empty segments are + // invalid + throw new DeploymentException(sm.getString("uriTemplate.emptySegment", path)); + } + } + normalized.append('/'); + int index = -1; + if (segment.startsWith("{") && segment.endsWith("}")) { + index = segmentCount; + segment = segment.substring(1, segment.length() - 1); + normalized.append('{'); + normalized.append(paramCount++); + normalized.append('}'); + if (!paramNames.add(segment)) { + throw new DeploymentException(sm.getString("uriTemplate.duplicateParameter", segment)); + } + } else { + if (segment.contains("{") || segment.contains("}")) { + throw new DeploymentException(sm.getString("uriTemplate.invalidSegment", segment, path)); + } + normalized.append(segment); + } + this.segments.add(new Segment(index, segment)); + segmentCount++; + } + + this.normalized = normalized.toString(); + this.hasParameters = paramCount > 0; + } + + + public Map match(UriTemplate candidate) { + + Map result = new HashMap<>(); + + // Should not happen but for safety + if (candidate.getSegmentCount() != getSegmentCount()) { + return null; + } + + Iterator targetSegments = segments.iterator(); + + for (Segment candidateSegment : candidate.getSegments()) { + Segment targetSegment = targetSegments.next(); + + if (targetSegment.getParameterIndex() == -1) { + // Not a parameter - values must match + if (!targetSegment.getValue().equals(candidateSegment.getValue())) { + // Not a match. Stop here + return null; + } + } else { + // Parameter + result.put(targetSegment.getValue(), candidateSegment.getValue()); + } + } + + return result; + } + + + public boolean hasParameters() { + return hasParameters; + } + + + public int getSegmentCount() { + return segments.size(); + } + + + public String getNormalizedPath() { + return normalized; + } + + + private List getSegments() { + return segments; + } + + + private static class Segment { + private final int parameterIndex; + private final String value; + + Segment(int parameterIndex, String value) { + this.parameterIndex = parameterIndex; + this.value = value; + } + + + public int getParameterIndex() { + return parameterIndex; + } + + + public String getValue() { + return value; + } + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsContextListener.java b/java/org/apache/tomcat/websocket/server/WsContextListener.java new file mode 100644 index 0000000..6c698dc --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsContextListener.java @@ -0,0 +1,49 @@ +/* + * 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.tomcat.websocket.server; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +/** + * In normal usage, this {@link ServletContextListener} does not need to be explicitly configured as the {@link WsSci} + * performs all the necessary bootstrap and installs this listener in the {@link ServletContext}. If the {@link WsSci} + * is disabled, this listener must be added manually to every {@link ServletContext} that uses WebSocket to bootstrap + * the {@link WsServerContainer} correctly. + */ +public class WsContextListener implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + ServletContext sc = sce.getServletContext(); + // Don't trigger WebSocket initialization if a WebSocket Server + // Container is already present + if (sc.getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE) == null) { + WsSci.init(sce.getServletContext(), false); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + ServletContext sc = sce.getServletContext(); + Object obj = sc.getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + if (obj instanceof WsServerContainer) { + ((WsServerContainer) obj).destroy(); + } + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsFilter.java b/java/org/apache/tomcat/websocket/server/WsFilter.java new file mode 100644 index 0000000..2245422 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsFilter.java @@ -0,0 +1,78 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Handles the initial HTTP connection for WebSocket connections. + */ +public class WsFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + private transient WsServerContainer sc; + + + @Override + public void init() throws ServletException { + sc = (WsServerContainer) getServletContext().getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + } + + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + // This filter only needs to handle WebSocket upgrade requests + if (!sc.areEndpointsRegistered() || !UpgradeUtil.isWebSocketUpgradeRequest(request, response)) { + chain.doFilter(request, response); + return; + } + + // HTTP request with an upgrade header for WebSocket present + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + // Check to see if this WebSocket implementation has a matching mapping + String path; + String pathInfo = req.getPathInfo(); + if (pathInfo == null) { + path = req.getServletPath(); + } else { + path = req.getServletPath() + pathInfo; + } + WsMappingResult mappingResult = sc.findMapping(path); + + if (mappingResult == null) { + // No endpoint registered for the requested path. Let the + // application handle it (it might redirect or forward for example) + chain.doFilter(request, response); + return; + } + + UpgradeUtil.doUpgrade(sc, req, resp, mappingResult.getConfig(), mappingResult.getPathParams()); + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsFrameServer.java b/java/org/apache/tomcat/websocket/server/WsFrameServer.java new file mode 100644 index 0000000..fb65a3a --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsFrameServer.java @@ -0,0 +1,204 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.coyote.http11.upgrade.UpgradeInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.Transformation; +import org.apache.tomcat.websocket.WsFrameBase; +import org.apache.tomcat.websocket.WsIOException; +import org.apache.tomcat.websocket.WsSession; + +public class WsFrameServer extends WsFrameBase { + + private final Log log = LogFactory.getLog(WsFrameServer.class); // must not be static + private static final StringManager sm = StringManager.getManager(WsFrameServer.class); + + private final SocketWrapperBase socketWrapper; + private final UpgradeInfo upgradeInfo; + private final ClassLoader applicationClassLoader; + + + public WsFrameServer(SocketWrapperBase socketWrapper, UpgradeInfo upgradeInfo, WsSession wsSession, + Transformation transformation, ClassLoader applicationClassLoader) { + super(wsSession, transformation); + this.socketWrapper = socketWrapper; + this.upgradeInfo = upgradeInfo; + this.applicationClassLoader = applicationClassLoader; + } + + + /** + * Called when there is data in the ServletInputStream to process. + * + * @throws IOException if an I/O error occurs while processing the available data + */ + private void onDataAvailable() throws IOException { + if (log.isTraceEnabled()) { + log.trace("wsFrameServer.onDataAvailable"); + } + if (isOpen() && inputBuffer.hasRemaining() && !isSuspended()) { + // There might be a data that was left in the buffer when + // the read has been suspended. + // Consume this data before reading from the socket. + processInputBuffer(); + } + + while (isOpen() && !isSuspended()) { + // Fill up the input buffer with as much data as we can + inputBuffer.mark(); + inputBuffer.position(inputBuffer.limit()).limit(inputBuffer.capacity()); + int read = socketWrapper.read(false, inputBuffer); + inputBuffer.limit(inputBuffer.position()).reset(); + // Some error conditions in NIO2 will trigger a return of zero and close the socket + if (read < 0 || socketWrapper.isClosed()) { + throw new EOFException(); + } else if (read == 0) { + return; + } + if (log.isTraceEnabled()) { + log.trace(sm.getString("wsFrameServer.bytesRead", Integer.toString(read))); + } + processInputBuffer(); + } + } + + + @Override + protected void updateStats(long payloadLength) { + upgradeInfo.addMsgsReceived(1); + upgradeInfo.addBytesReceived(payloadLength); + } + + + @Override + protected boolean isMasked() { + // Data is from the client so it should be masked + return true; + } + + + @Override + protected Transformation getTransformation() { + // Overridden to make it visible to other classes in this package + return super.getTransformation(); + } + + + @Override + protected boolean isOpen() { + // Overridden to make it visible to other classes in this package + return super.isOpen(); + } + + + @Override + protected Log getLog() { + return log; + } + + + @Override + protected void sendMessageText(boolean last) throws WsIOException { + Thread currentThread = Thread.currentThread(); + ClassLoader cl = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(applicationClassLoader); + super.sendMessageText(last); + } finally { + currentThread.setContextClassLoader(cl); + } + } + + + @Override + protected void sendMessageBinary(ByteBuffer msg, boolean last) throws WsIOException { + Thread currentThread = Thread.currentThread(); + ClassLoader cl = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(applicationClassLoader); + super.sendMessageBinary(msg, last); + } finally { + currentThread.setContextClassLoader(cl); + } + } + + + @Override + protected void resumeProcessing() { + socketWrapper.processSocket(SocketEvent.OPEN_READ, true); + } + + + SocketState notifyDataAvailable() throws IOException { + while (isOpen()) { + switch (getReadState()) { + case WAITING: + if (!changeReadState(ReadState.WAITING, ReadState.PROCESSING)) { + continue; + } + try { + return doOnDataAvailable(); + } catch (IOException e) { + changeReadState(ReadState.CLOSING); + throw e; + } + case SUSPENDING_WAIT: + if (!changeReadState(ReadState.SUSPENDING_WAIT, ReadState.SUSPENDED)) { + continue; + } + return SocketState.SUSPENDED; + default: + throw new IllegalStateException(sm.getString("wsFrameServer.illegalReadState", getReadState())); + } + } + + return SocketState.CLOSED; + } + + + private SocketState doOnDataAvailable() throws IOException { + onDataAvailable(); + while (isOpen()) { + switch (getReadState()) { + case PROCESSING: + if (!changeReadState(ReadState.PROCESSING, ReadState.WAITING)) { + continue; + } + return SocketState.UPGRADED; + case SUSPENDING_PROCESS: + if (!changeReadState(ReadState.SUSPENDING_PROCESS, ReadState.SUSPENDED)) { + continue; + } + return SocketState.SUSPENDED; + default: + throw new IllegalStateException(sm.getString("wsFrameServer.illegalReadState", getReadState())); + } + } + + return SocketState.CLOSED; + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsHandshakeRequest.java b/java/org/apache/tomcat/websocket/server/WsHandshakeRequest.java new file mode 100644 index 0000000..fb5bce9 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsHandshakeRequest.java @@ -0,0 +1,182 @@ +/* + * 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.tomcat.websocket.server; + +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.websocket.server.HandshakeRequest; + +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; +import org.apache.tomcat.util.res.StringManager; + +/** + * Represents the request that this session was opened under. + */ +public class WsHandshakeRequest implements HandshakeRequest { + + private static final StringManager sm = StringManager.getManager(WsHandshakeRequest.class); + + private final URI requestUri; + private final Map> parameterMap; + private final String queryString; + private final Principal userPrincipal; + private final Map> headers; + private final Object httpSession; + + private volatile HttpServletRequest request; + + + public WsHandshakeRequest(HttpServletRequest request, Map pathParams) { + + this.request = request; + + queryString = request.getQueryString(); + userPrincipal = request.getUserPrincipal(); + httpSession = request.getSession(false); + requestUri = buildRequestUri(request); + + // ParameterMap + Map originalParameters = request.getParameterMap(); + Map> newParameters = new HashMap<>(originalParameters.size()); + for (Entry entry : originalParameters.entrySet()) { + newParameters.put(entry.getKey(), Collections.unmodifiableList(Arrays.asList(entry.getValue()))); + } + for (Entry entry : pathParams.entrySet()) { + newParameters.put(entry.getKey(), Collections.singletonList(entry.getValue())); + } + parameterMap = Collections.unmodifiableMap(newParameters); + + // Headers + Map> newHeaders = new CaseInsensitiveKeyMap<>(); + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + + newHeaders.put(headerName, Collections.unmodifiableList(Collections.list(request.getHeaders(headerName)))); + } + + headers = Collections.unmodifiableMap(newHeaders); + } + + @Override + public URI getRequestURI() { + return requestUri; + } + + @Override + public Map> getParameterMap() { + return parameterMap; + } + + @Override + public String getQueryString() { + return queryString; + } + + @Override + public Principal getUserPrincipal() { + return userPrincipal; + } + + @Override + public Map> getHeaders() { + return headers; + } + + @Override + public boolean isUserInRole(String role) { + if (request == null) { + throw new IllegalStateException(); + } + + return request.isUserInRole(role); + } + + @Override + public Object getHttpSession() { + return httpSession; + } + + /** + * Called when the HandshakeRequest is no longer required. Since an instance of this class retains a reference to + * the current HttpServletRequest that reference needs to be cleared as the HttpServletRequest may be reused. There + * is no reason for instances of this class to be accessed once the handshake has been completed. + */ + void finished() { + request = null; + } + + + /* + * See RequestUtil.getRequestURL() + */ + private static URI buildRequestUri(HttpServletRequest req) { + + StringBuilder uri = new StringBuilder(); + String scheme = req.getScheme(); + int port = req.getServerPort(); + if (port < 0) { + // Work around java.net.URL bug + port = 80; + } + + if ("http".equals(scheme)) { + uri.append("ws"); + } else if ("https".equals(scheme)) { + uri.append("wss"); + } else if ("wss".equals(scheme) || "ws".equals(scheme)) { + uri.append(scheme); + } else { + // Should never happen + throw new IllegalArgumentException(sm.getString("wsHandshakeRequest.unknownScheme", scheme)); + } + + uri.append("://"); + uri.append(req.getServerName()); + + if ((scheme.equals("http") && (port != 80)) || (scheme.equals("ws") && (port != 80)) || + (scheme.equals("wss") && (port != 443)) || (scheme.equals("https") && (port != 443))) { + uri.append(':'); + uri.append(port); + } + + uri.append(req.getRequestURI()); + + if (req.getQueryString() != null) { + uri.append('?'); + uri.append(req.getQueryString()); + } + + try { + return new URI(uri.toString()); + } catch (URISyntaxException e) { + // Should never happen + throw new IllegalArgumentException(sm.getString("wsHandshakeRequest.invalidUri", uri.toString()), e); + } + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java b/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java new file mode 100644 index 0000000..93a717f --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java @@ -0,0 +1,265 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.EOFException; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.WebConnection; +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.Extension; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; +import org.apache.coyote.http11.upgrade.UpgradeInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.Transformation; +import org.apache.tomcat.websocket.WsIOException; +import org.apache.tomcat.websocket.WsSession; + +/** + * Servlet 3.1 HTTP upgrade handler for WebSocket connections. + */ +public class WsHttpUpgradeHandler implements InternalHttpUpgradeHandler { + + private final Log log = LogFactory.getLog(WsHttpUpgradeHandler.class); // must not be static + private static final StringManager sm = StringManager.getManager(WsHttpUpgradeHandler.class); + + private final ClassLoader applicationClassLoader; + + private SocketWrapperBase socketWrapper; + private UpgradeInfo upgradeInfo = new UpgradeInfo(); + + private Endpoint ep; + private ServerEndpointConfig serverEndpointConfig; + private WsServerContainer webSocketContainer; + private WsHandshakeRequest handshakeRequest; + private List negotiatedExtensions; + private String subProtocol; + private Transformation transformation; + private Map pathParameters; + private boolean secure; + private WebConnection connection; + + private WsRemoteEndpointImplServer wsRemoteEndpointServer; + private WsFrameServer wsFrame; + private WsSession wsSession; + + + public WsHttpUpgradeHandler() { + applicationClassLoader = Thread.currentThread().getContextClassLoader(); + } + + + @Override + public void setSocketWrapper(SocketWrapperBase socketWrapper) { + this.socketWrapper = socketWrapper; + } + + + public void preInit(ServerEndpointConfig serverEndpointConfig, WsServerContainer wsc, + WsHandshakeRequest handshakeRequest, List negotiatedExtensionsPhase2, String subProtocol, + Transformation transformation, Map pathParameters, boolean secure) { + this.serverEndpointConfig = serverEndpointConfig; + this.webSocketContainer = wsc; + this.handshakeRequest = handshakeRequest; + this.negotiatedExtensions = negotiatedExtensionsPhase2; + this.subProtocol = subProtocol; + this.transformation = transformation; + this.pathParameters = pathParameters; + this.secure = secure; + } + + + @Override + public void init(WebConnection connection) { + this.connection = connection; + if (serverEndpointConfig == null) { + throw new IllegalStateException(sm.getString("wsHttpUpgradeHandler.noPreInit")); + } + + String httpSessionId = null; + Object session = handshakeRequest.getHttpSession(); + if (session != null) { + httpSessionId = ((HttpSession) session).getId(); + } + + // Need to call onOpen using the web application's class loader + // Create the frame using the application's class loader so it can pick + // up application specific config from the ServerContainerImpl + Thread t = Thread.currentThread(); + ClassLoader cl = t.getContextClassLoader(); + t.setContextClassLoader(applicationClassLoader); + try { + wsRemoteEndpointServer = new WsRemoteEndpointImplServer(socketWrapper, upgradeInfo, webSocketContainer, + connection); + wsSession = new WsSession(wsRemoteEndpointServer, webSocketContainer, handshakeRequest.getRequestURI(), + handshakeRequest.getParameterMap(), handshakeRequest.getQueryString(), + handshakeRequest.getUserPrincipal(), httpSessionId, negotiatedExtensions, subProtocol, + pathParameters, secure, serverEndpointConfig); + ep = wsSession.getLocal(); + wsFrame = new WsFrameServer(socketWrapper, upgradeInfo, wsSession, transformation, applicationClassLoader); + // WsFrame adds the necessary final transformations. Copy the + // completed transformation chain to the remote end point. + wsRemoteEndpointServer.setTransformation(wsFrame.getTransformation()); + ep.onOpen(wsSession, serverEndpointConfig); + webSocketContainer.registerSession(serverEndpointConfig.getPath(), wsSession); + } catch (DeploymentException e) { + throw new IllegalArgumentException(e); + } finally { + t.setContextClassLoader(cl); + } + } + + + @Override + public UpgradeInfo getUpgradeInfo() { + return upgradeInfo; + } + + + @Override + public SocketState upgradeDispatch(SocketEvent status) { + switch (status) { + case OPEN_READ: + try { + return wsFrame.notifyDataAvailable(); + } catch (WsIOException ws) { + close(ws.getCloseReason()); + } catch (IOException ioe) { + onError(ioe); + CloseReason cr = new CloseReason(CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage()); + close(cr); + } + return SocketState.CLOSED; + case OPEN_WRITE: + wsRemoteEndpointServer.onWritePossible(false); + break; + case STOP: + CloseReason cr = new CloseReason(CloseCodes.GOING_AWAY, + sm.getString("wsHttpUpgradeHandler.serverStop")); + try { + wsSession.close(cr); + } catch (IOException ioe) { + onError(ioe); + cr = new CloseReason(CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage()); + close(cr); + return SocketState.CLOSED; + } + break; + case ERROR: + // Need to clear any in-progress writes before trying to send a close frame + wsRemoteEndpointServer.clearHandler(socketWrapper.getError(), false); + String msg = sm.getString("wsHttpUpgradeHandler.closeOnError"); + wsSession.doClose(new CloseReason(CloseCodes.GOING_AWAY, msg), + new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg)); + //$FALL-THROUGH$ + case DISCONNECT: + case TIMEOUT: + case CONNECT_FAIL: + return SocketState.CLOSED; + + } + + /* + * If a CLOSE frame has been received then wsFrame will be closed but need to keep the connection open until the + * CLOSE frame has been sent. Hence use the wsSession.isClosed() rather than wsFrame.isOpen() here. + */ + if (wsSession.isClosed()) { + return SocketState.CLOSED; + } else { + return SocketState.UPGRADED; + } + } + + + @Override + public void timeoutAsync(long now) { + // NO-OP + } + + + @Override + public void pause() { + // NO-OP + } + + + @Override + public void destroy() { + WebConnection connection = this.connection; + if (connection != null) { + this.connection = null; + try { + connection.close(); + } catch (Exception e) { + log.error(sm.getString("wsHttpUpgradeHandler.destroyFailed"), e); + } + } + } + + + private void onError(Throwable throwable) { + // Need to call onError using the web application's class loader + Thread t = Thread.currentThread(); + ClassLoader cl = t.getContextClassLoader(); + t.setContextClassLoader(applicationClassLoader); + try { + ep.onError(wsSession, throwable); + } finally { + t.setContextClassLoader(cl); + } + } + + + private void close(CloseReason cr) { + /* + * Any call to this method is a result of a problem reading from the client. At this point that state of the + * connection is unknown. First attempt to clear the handler for any in-flight message write (that probably + * failed). If using NIO2 is is possible that the original error occurred on a write but this method was called + * during a read. The in-progress write will block the sending of the close frame unless the handler is cleared + * (effectively signalling the write failed). + */ + wsRemoteEndpointServer.clearHandler(new EOFException(), true); + + /* + * Then: - send a close frame to the client - close the socket immediately. There is no point in waiting for a + * close frame from the client because there is no guarantee that we can recover from whatever messed up state + * the client put the connection into. + */ + wsSession.onClose(cr); + } + + + @Override + public void setSslSupport(SSLSupport sslSupport) { + // NO-OP. WebSocket has no requirement to access the TLS information + // associated with the underlying connection. + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsMappingResult.java b/java/org/apache/tomcat/websocket/server/WsMappingResult.java new file mode 100644 index 0000000..3729315 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsMappingResult.java @@ -0,0 +1,43 @@ +/* + * 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.tomcat.websocket.server; + +import java.util.Map; + +import jakarta.websocket.server.ServerEndpointConfig; + +class WsMappingResult { + + private final ServerEndpointConfig config; + private final Map pathParams; + + + WsMappingResult(ServerEndpointConfig config, Map pathParams) { + this.config = config; + this.pathParams = pathParams; + } + + + ServerEndpointConfig getConfig() { + return config; + } + + + Map getPathParams() { + return pathParams; + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsPerSessionServerEndpointConfig.java b/java/org/apache/tomcat/websocket/server/WsPerSessionServerEndpointConfig.java new file mode 100644 index 0000000..451efe4 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsPerSessionServerEndpointConfig.java @@ -0,0 +1,82 @@ +/* + * 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.tomcat.websocket.server; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.websocket.Decoder; +import jakarta.websocket.Encoder; +import jakarta.websocket.Extension; +import jakarta.websocket.server.ServerEndpointConfig; + +/** + * Wraps the provided {@link ServerEndpointConfig} and provides a per session view - the difference being that the map + * returned by {@link #getUserProperties()} is unique to this instance rather than shared with the wrapped + * {@link ServerEndpointConfig}. + */ +class WsPerSessionServerEndpointConfig implements ServerEndpointConfig { + + private final ServerEndpointConfig perEndpointConfig; + private final Map perSessionUserProperties = new ConcurrentHashMap<>(); + + WsPerSessionServerEndpointConfig(ServerEndpointConfig perEndpointConfig) { + this.perEndpointConfig = perEndpointConfig; + perSessionUserProperties.putAll(perEndpointConfig.getUserProperties()); + } + + @Override + public List> getEncoders() { + return perEndpointConfig.getEncoders(); + } + + @Override + public List> getDecoders() { + return perEndpointConfig.getDecoders(); + } + + @Override + public Map getUserProperties() { + return perSessionUserProperties; + } + + @Override + public Class getEndpointClass() { + return perEndpointConfig.getEndpointClass(); + } + + @Override + public String getPath() { + return perEndpointConfig.getPath(); + } + + @Override + public List getSubprotocols() { + return perEndpointConfig.getSubprotocols(); + } + + @Override + public List getExtensions() { + return perEndpointConfig.getExtensions(); + } + + @Override + public Configurator getConfigurator() { + return perEndpointConfig.getConfigurator(); + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsRemoteEndpointImplServer.java b/java/org/apache/tomcat/websocket/server/WsRemoteEndpointImplServer.java new file mode 100644 index 0000000..3be5d4f --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsRemoteEndpointImplServer.java @@ -0,0 +1,404 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.EOFException; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import jakarta.servlet.http.WebConnection; +import jakarta.websocket.SendHandler; +import jakarta.websocket.SendResult; + +import org.apache.coyote.http11.upgrade.UpgradeInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.net.SocketWrapperBase.BlockingMode; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.Constants; +import org.apache.tomcat.websocket.Transformation; +import org.apache.tomcat.websocket.WsRemoteEndpointImplBase; + +/** + * This is the server side {@link jakarta.websocket.RemoteEndpoint} implementation - i.e. what the server uses to send + * data to the client. + */ +public class WsRemoteEndpointImplServer extends WsRemoteEndpointImplBase { + + private static final StringManager sm = StringManager.getManager(WsRemoteEndpointImplServer.class); + private final Log log = LogFactory.getLog(WsRemoteEndpointImplServer.class); // must not be static + + private final SocketWrapperBase socketWrapper; + private final UpgradeInfo upgradeInfo; + private final WebConnection connection; + private final WsWriteTimeout wsWriteTimeout; + private volatile SendHandler handler = null; + private volatile ByteBuffer[] buffers = null; + + private volatile long timeoutExpiry = -1; + + public WsRemoteEndpointImplServer(SocketWrapperBase socketWrapper, UpgradeInfo upgradeInfo, + WsServerContainer serverContainer, WebConnection connection) { + this.socketWrapper = socketWrapper; + this.upgradeInfo = upgradeInfo; + this.connection = connection; + this.wsWriteTimeout = serverContainer.getTimeout(); + } + + + @Override + protected final boolean isMasked() { + return false; + } + + + /** + * {@inheritDoc} + *

    + * The close message is a special case. It needs to be blocking else implementing the clean-up that follows the + * sending of the close message gets a lot more complicated. On the server, this creates additional complications as + * a dead-lock may occur in the following scenario: + *

      + *
    1. Application thread writes message using non-blocking
    2. + *
    3. Write does not complete (write logic holds message pending lock)
    4. + *
    5. Socket is added to poller (or equivalent) for write + *
    6. Client sends close message
    7. + *
    8. Container processes received close message and tries to send close message in response
    9. + *
    10. Container holds socket lock and is blocked waiting for message pending lock
    11. + *
    12. Poller fires write possible event for socket
    13. + *
    14. Container tries to process write possible event but is blocked waiting for socket lock
    15. + *
    16. Processing of the WebSocket connection is dead-locked until the original message write times out
    17. + *
    + * The purpose of this method is to break the above dead-lock. It does this by returning control of the processor to + * the socket wrapper and releasing the socket lock while waiting for the pending message write to complete. + * Normally, that would be a terrible idea as it creates the possibility that the processor is returned to the pool + * more than once under various error conditions. In this instance it is safe because these are upgrade processors + * (isUpgrade() returns {@code true}) and upgrade processors are never pooled. + *

    + * TODO: Despite the complications it creates, it would be worth exploring the possibility of processing a received + * close frame in a non-blocking manner. + */ + @Override + protected boolean acquireMessagePartInProgressSemaphore(byte opCode, long timeoutExpiry) + throws InterruptedException { + + /* + * Special handling is required only when all of the following are true: + * - A close message is being sent + * - This thread currently holds the socketWrapper lock (i.e. the thread is current processing a socket event) + * + * Special handling is only possible if the socketWrapper lock is a ReentrantLock (it will be by default) + */ + if (socketWrapper.getLock() instanceof ReentrantLock) { + ReentrantLock reentrantLock = (ReentrantLock) socketWrapper.getLock(); + if (opCode == Constants.OPCODE_CLOSE && reentrantLock.isHeldByCurrentThread()) { + int socketWrapperLockCount = reentrantLock.getHoldCount(); + + while (!messagePartInProgress.tryAcquire()) { + if (timeoutExpiry < System.currentTimeMillis()) { + return false; + } + try { + // Release control of the processor + socketWrapper.setCurrentProcessor(connection); + // Release the per socket lock(s) + for (int i = 0; i < socketWrapperLockCount; i++) { + socketWrapper.getLock().unlock(); + } + // Provide opportunity for another thread to obtain the socketWrapper lock + Thread.yield(); + } finally { + // Re-obtain the per socket lock(s) + for (int i = 0; i < socketWrapperLockCount; i++) { + socketWrapper.getLock().lock(); + } + // Re-take control of the processor + socketWrapper.takeCurrentProcessor(); + } + } + + return true; + } + } + + // Skip special handling + return super.acquireMessagePartInProgressSemaphore(opCode, timeoutExpiry); + } + + + @Override + protected void doWrite(SendHandler handler, long blockingWriteTimeoutExpiry, ByteBuffer... buffers) { + if (socketWrapper.hasAsyncIO()) { + final boolean block = (blockingWriteTimeoutExpiry != -1); + long timeout = -1; + if (block) { + timeout = blockingWriteTimeoutExpiry - System.currentTimeMillis(); + if (timeout <= 0) { + SendResult sr = new SendResult(new SocketTimeoutException()); + handler.onResult(sr); + return; + } + } else { + this.handler = handler; + timeout = getSendTimeout(); + if (timeout > 0) { + // Register with timeout thread + timeoutExpiry = timeout + System.currentTimeMillis(); + wsWriteTimeout.register(this); + } + } + socketWrapper.write(block ? BlockingMode.BLOCK : BlockingMode.SEMI_BLOCK, timeout, TimeUnit.MILLISECONDS, + null, SocketWrapperBase.COMPLETE_WRITE_WITH_COMPLETION, new CompletionHandler() { + @Override + public void completed(Long result, Void attachment) { + if (block) { + long timeout = blockingWriteTimeoutExpiry - System.currentTimeMillis(); + if (timeout <= 0) { + failed(new SocketTimeoutException(), null); + } else { + handler.onResult(SENDRESULT_OK); + } + } else { + wsWriteTimeout.unregister(WsRemoteEndpointImplServer.this); + clearHandler(null, true); + } + } + + @Override + public void failed(Throwable exc, Void attachment) { + if (block) { + SendResult sr = new SendResult(exc); + handler.onResult(sr); + } else { + wsWriteTimeout.unregister(WsRemoteEndpointImplServer.this); + clearHandler(exc, true); + close(); + } + } + }, buffers); + } else { + if (blockingWriteTimeoutExpiry == -1) { + this.handler = handler; + this.buffers = buffers; + // This is definitely the same thread that triggered the write so a + // dispatch will be required. + onWritePossible(true); + } else { + // Blocking + try { + for (ByteBuffer buffer : buffers) { + long timeout = blockingWriteTimeoutExpiry - System.currentTimeMillis(); + if (timeout <= 0) { + SendResult sr = new SendResult(new SocketTimeoutException()); + handler.onResult(sr); + return; + } + socketWrapper.setWriteTimeout(timeout); + socketWrapper.write(true, buffer); + } + long timeout = blockingWriteTimeoutExpiry - System.currentTimeMillis(); + if (timeout <= 0) { + SendResult sr = new SendResult(new SocketTimeoutException()); + handler.onResult(sr); + return; + } + socketWrapper.setWriteTimeout(timeout); + socketWrapper.flush(true); + handler.onResult(SENDRESULT_OK); + } catch (IOException e) { + SendResult sr = new SendResult(e); + handler.onResult(sr); + } + } + } + } + + + @Override + protected void updateStats(long payloadLength) { + upgradeInfo.addMsgsSent(1); + upgradeInfo.addBytesSent(payloadLength); + } + + + public void onWritePossible(boolean useDispatch) { + // Note: Unused for async IO + ByteBuffer[] buffers = this.buffers; + if (buffers == null) { + // Servlet 3.1 will call the write listener once even if nothing + // was written + return; + } + boolean complete = false; + try { + socketWrapper.flush(false); + // If this is false there will be a call back when it is true + while (socketWrapper.isReadyForWrite()) { + complete = true; + for (ByteBuffer buffer : buffers) { + if (buffer.hasRemaining()) { + complete = false; + socketWrapper.write(false, buffer); + break; + } + } + if (complete) { + socketWrapper.flush(false); + complete = socketWrapper.isReadyForWrite(); + if (complete) { + wsWriteTimeout.unregister(this); + clearHandler(null, useDispatch); + } + break; + } + } + } catch (IOException | IllegalStateException e) { + wsWriteTimeout.unregister(this); + clearHandler(e, useDispatch); + close(); + } + + if (!complete) { + // Async write is in progress + long timeout = getSendTimeout(); + if (timeout > 0) { + // Register with timeout thread + timeoutExpiry = timeout + System.currentTimeMillis(); + wsWriteTimeout.register(this); + } + } + } + + + @Override + protected void doClose() { + if (handler != null) { + // close() can be triggered by a wide range of scenarios. It is far + // simpler just to always use a dispatch than it is to try and track + // whether or not this method was called by the same thread that + // triggered the write + clearHandler(new EOFException(), true); + } + try { + socketWrapper.close(); + } catch (Exception e) { + if (log.isInfoEnabled()) { + log.info(sm.getString("wsRemoteEndpointServer.closeFailed"), e); + } + } + wsWriteTimeout.unregister(this); + } + + + protected long getTimeoutExpiry() { + return timeoutExpiry; + } + + + /* + * Currently this is only called from the background thread so we could just call clearHandler() with useDispatch == + * false but the method parameter was added in case other callers started to use this method to make sure that those + * callers think through what the correct value of useDispatch is for them. + */ + protected void onTimeout(boolean useDispatch) { + if (handler != null) { + clearHandler(new SocketTimeoutException(), useDispatch); + } + close(); + } + + + @Override + protected void setTransformation(Transformation transformation) { + // Overridden purely so it is visible to other classes in this package + super.setTransformation(transformation); + } + + + /** + * @param t The throwable associated with any error that occurred + * @param useDispatch Should {@link SendHandler#onResult(SendResult)} be called from a new thread, keeping in mind + * the requirements of {@link jakarta.websocket.RemoteEndpoint.Async} + */ + void clearHandler(Throwable t, boolean useDispatch) { + // Setting the result marks this (partial) message as + // complete which means the next one may be sent which + // could update the value of the handler. Therefore, keep a + // local copy before signalling the end of the (partial) + // message. + SendHandler sh = handler; + handler = null; + buffers = null; + if (sh != null) { + if (useDispatch) { + OnResultRunnable r = new OnResultRunnable(sh, t); + try { + socketWrapper.execute(r); + } catch (RejectedExecutionException ree) { + // Can't use the executor so call the runnable directly. + // This may not be strictly specification compliant in all + // cases but during shutdown only close messages are going + // to be sent so there should not be the issue of nested + // calls leading to stack overflow as described in bug + // 55715. The issues with nested calls was the reason for + // the separate thread requirement in the specification. + r.run(); + } + } else { + if (t == null) { + sh.onResult(new SendResult()); + } else { + sh.onResult(new SendResult(t)); + } + } + } + } + + + @Override + protected Lock getLock() { + return socketWrapper.getLock(); + } + + + private static class OnResultRunnable implements Runnable { + + private final SendHandler sh; + private final Throwable t; + + private OnResultRunnable(SendHandler sh, Throwable t) { + this.sh = sh; + this.t = t; + } + + @Override + public void run() { + if (t == null) { + sh.onResult(new SendResult()); + } else { + sh.onResult(new SendResult(t)); + } + } + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsSci.java b/java/org/apache/tomcat/websocket/server/WsSci.java new file mode 100644 index 0000000..434602a --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsSci.java @@ -0,0 +1,143 @@ +/* + * 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.tomcat.websocket.server; + +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Set; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.HandlesTypes; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.server.ServerApplicationConfig; +import jakarta.websocket.server.ServerEndpoint; +import jakarta.websocket.server.ServerEndpointConfig; + +/** + * Registers an interest in any class that is annotated with {@link ServerEndpoint} so that Endpoint can be published + * via the WebSocket server. + */ +@HandlesTypes({ ServerEndpoint.class, ServerApplicationConfig.class, Endpoint.class }) +public class WsSci implements ServletContainerInitializer { + + @Override + public void onStartup(Set> clazzes, ServletContext ctx) throws ServletException { + + WsServerContainer sc = init(ctx, true); + + if (clazzes == null || clazzes.size() == 0) { + return; + } + + // Group the discovered classes by type + Set serverApplicationConfigs = new HashSet<>(); + Set> scannedEndpointClazzes = new HashSet<>(); + Set> scannedPojoEndpoints = new HashSet<>(); + + try { + // wsPackage is "jakarta.websocket." + String wsPackage = ContainerProvider.class.getName(); + wsPackage = wsPackage.substring(0, wsPackage.lastIndexOf('.') + 1); + for (Class clazz : clazzes) { + int modifiers = clazz.getModifiers(); + if (!Modifier.isPublic(modifiers) || Modifier.isAbstract(modifiers) || + Modifier.isInterface(modifiers) || !isExported(clazz)) { + // Non-public, abstract, interface or not in an exported + // package - skip it. + continue; + } + // Protect against scanning the WebSocket API JARs + if (clazz.getName().startsWith(wsPackage)) { + continue; + } + if (ServerApplicationConfig.class.isAssignableFrom(clazz)) { + serverApplicationConfigs.add((ServerApplicationConfig) clazz.getConstructor().newInstance()); + } + if (Endpoint.class.isAssignableFrom(clazz)) { + @SuppressWarnings("unchecked") + Class endpoint = (Class) clazz; + scannedEndpointClazzes.add(endpoint); + } + if (clazz.isAnnotationPresent(ServerEndpoint.class)) { + scannedPojoEndpoints.add(clazz); + } + } + } catch (ReflectiveOperationException e) { + throw new ServletException(e); + } + + // Filter the results + Set filteredEndpointConfigs = new HashSet<>(); + Set> filteredPojoEndpoints = new HashSet<>(); + + if (serverApplicationConfigs.isEmpty()) { + filteredPojoEndpoints.addAll(scannedPojoEndpoints); + } else { + for (ServerApplicationConfig config : serverApplicationConfigs) { + Set configFilteredEndpoints = config.getEndpointConfigs(scannedEndpointClazzes); + if (configFilteredEndpoints != null) { + filteredEndpointConfigs.addAll(configFilteredEndpoints); + } + Set> configFilteredPojos = config.getAnnotatedEndpointClasses(scannedPojoEndpoints); + if (configFilteredPojos != null) { + filteredPojoEndpoints.addAll(configFilteredPojos); + } + } + } + + try { + // Deploy endpoints + for (ServerEndpointConfig config : filteredEndpointConfigs) { + sc.addEndpoint(config); + } + // Deploy POJOs + for (Class clazz : filteredPojoEndpoints) { + sc.addEndpoint(clazz, true); + } + } catch (DeploymentException e) { + throw new ServletException(e); + } + } + + + private static boolean isExported(Class type) { + String packageName = type.getPackage().getName(); + Module module = type.getModule(); + return module.isExported(packageName); + } + + + static WsServerContainer init(ServletContext servletContext, boolean initBySciMechanism) { + + WsServerContainer sc = new WsServerContainer(servletContext); + + servletContext.setAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE, sc); + + servletContext.addListener(new WsSessionListener(sc)); + // Can't register the ContextListener again if the ContextListener is + // calling this method + if (initBySciMechanism) { + servletContext.addListener(new WsContextListener()); + } + + return sc; + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsServerContainer.java b/java/org/apache/tomcat/websocket/server/WsServerContainer.java new file mode 100644 index 0000000..b3b37ca --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsServerContainer.java @@ -0,0 +1,464 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; + +import javax.naming.NamingException; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Encoder; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpoint; +import jakarta.websocket.server.ServerEndpointConfig; +import jakarta.websocket.server.ServerEndpointConfig.Configurator; + +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.WsSession; +import org.apache.tomcat.websocket.WsWebSocketContainer; +import org.apache.tomcat.websocket.pojo.PojoMethodMapping; + +/** + * Provides a per class loader (i.e. per web application) instance of a ServerContainer. Web application wide defaults + * may be configured by setting the following servlet context initialisation parameters to the desired values. + *

      + *
    • {@link Constants#BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM}
    • + *
    • {@link Constants#TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM}
    • + *
    + */ +public class WsServerContainer extends WsWebSocketContainer implements ServerContainer { + + private static final StringManager sm = StringManager.getManager(WsServerContainer.class); + + private static final CloseReason AUTHENTICATED_HTTP_SESSION_CLOSED = new CloseReason(CloseCodes.VIOLATED_POLICY, + "This connection was established under an authenticated " + "HTTP session that has ended."); + + private final WsWriteTimeout wsWriteTimeout = new WsWriteTimeout(); + + private final ServletContext servletContext; + private final Map configExactMatchMap = new ConcurrentHashMap<>(); + private final Map> configTemplateMatchMap = new ConcurrentHashMap<>(); + private final Map> authenticatedSessions = new ConcurrentHashMap<>(); + private volatile boolean endpointsRegistered = false; + private volatile boolean deploymentFailed = false; + + WsServerContainer(ServletContext servletContext) { + + this.servletContext = servletContext; + setInstanceManager((InstanceManager) servletContext.getAttribute(InstanceManager.class.getName())); + + // Configure servlet context wide defaults + String value = servletContext.getInitParameter(Constants.BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM); + if (value != null) { + setDefaultMaxBinaryMessageBufferSize(Integer.parseInt(value)); + } + + value = servletContext.getInitParameter(Constants.TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM); + if (value != null) { + setDefaultMaxTextMessageBufferSize(Integer.parseInt(value)); + } + + FilterRegistration.Dynamic fr = servletContext.addFilter("Tomcat WebSocket (JSR356) Filter", new WsFilter()); + if (fr != null) { + fr.setAsyncSupported(true); + + EnumSet types = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD); + + fr.addMappingForUrlPatterns(types, true, "/*"); + } + } + + + /** + * Published the provided endpoint implementation at the specified path with the specified configuration. + * {@link #WsServerContainer(ServletContext)} must be called before calling this method. + * + * @param sec The configuration to use when creating endpoint instances + * + * @throws DeploymentException if the endpoint cannot be published as requested + */ + @Override + public void addEndpoint(ServerEndpointConfig sec) throws DeploymentException { + addEndpoint(sec, false); + } + + + void addEndpoint(ServerEndpointConfig sec, boolean fromAnnotatedPojo) throws DeploymentException { + + if (servletContext == null) { + throw new DeploymentException(sm.getString("serverContainer.servletContextMissing")); + } + + if (deploymentFailed) { + throw new DeploymentException(sm.getString("serverContainer.failedDeployment", + servletContext.getContextPath(), servletContext.getVirtualServerName())); + } + + try { + String path = sec.getPath(); + + // Add method mapping to user properties + PojoMethodMapping methodMapping = new PojoMethodMapping(sec.getEndpointClass(), sec.getDecoders(), path, + getInstanceManager(Thread.currentThread().getContextClassLoader())); + if (methodMapping.getOnClose() != null || methodMapping.getOnOpen() != null || + methodMapping.getOnError() != null || methodMapping.hasMessageHandlers()) { + sec.getUserProperties().put(org.apache.tomcat.websocket.pojo.Constants.POJO_METHOD_MAPPING_KEY, + methodMapping); + } + + UriTemplate uriTemplate = new UriTemplate(path); + if (uriTemplate.hasParameters()) { + Integer key = Integer.valueOf(uriTemplate.getSegmentCount()); + ConcurrentSkipListMap templateMatches = configTemplateMatchMap.get(key); + if (templateMatches == null) { + // Ensure that if concurrent threads execute this block they + // all end up using the same ConcurrentSkipListMap instance + templateMatches = new ConcurrentSkipListMap<>(); + configTemplateMatchMap.putIfAbsent(key, templateMatches); + templateMatches = configTemplateMatchMap.get(key); + } + TemplatePathMatch newMatch = new TemplatePathMatch(sec, uriTemplate, fromAnnotatedPojo); + TemplatePathMatch oldMatch = templateMatches.putIfAbsent(uriTemplate.getNormalizedPath(), newMatch); + if (oldMatch != null) { + // Note: This depends on Endpoint instances being added + // before POJOs in WsSci#onStartup() + if (oldMatch.isFromAnnotatedPojo() && !newMatch.isFromAnnotatedPojo() && + oldMatch.getConfig().getEndpointClass() == newMatch.getConfig().getEndpointClass()) { + // The WebSocket spec says to ignore the new match in this case + templateMatches.put(path, oldMatch); + } else { + // Duplicate uriTemplate; + throw new DeploymentException(sm.getString("serverContainer.duplicatePaths", path, + sec.getEndpointClass(), sec.getEndpointClass())); + } + } + } else { + // Exact match + ExactPathMatch newMatch = new ExactPathMatch(sec, fromAnnotatedPojo); + ExactPathMatch oldMatch = configExactMatchMap.put(path, newMatch); + if (oldMatch != null) { + // Note: This depends on Endpoint instances being added + // before POJOs in WsSci#onStartup() + if (oldMatch.isFromAnnotatedPojo() && !newMatch.isFromAnnotatedPojo() && + oldMatch.getConfig().getEndpointClass() == newMatch.getConfig().getEndpointClass()) { + // The WebSocket spec says to ignore the new match in this case + configExactMatchMap.put(path, oldMatch); + } else { + // Duplicate path mappings + throw new DeploymentException(sm.getString("serverContainer.duplicatePaths", path, + oldMatch.getConfig().getEndpointClass(), sec.getEndpointClass())); + } + } + } + + endpointsRegistered = true; + } catch (DeploymentException de) { + failDeployment(); + throw de; + } + } + + + /** + * Provides the equivalent of {@link #addEndpoint(ServerEndpointConfig)} for publishing plain old java objects + * (POJOs) that have been annotated as WebSocket endpoints. + * + * @param pojo The annotated POJO + */ + @Override + public void addEndpoint(Class pojo) throws DeploymentException { + addEndpoint(pojo, false); + } + + + void addEndpoint(Class pojo, boolean fromAnnotatedPojo) throws DeploymentException { + + if (deploymentFailed) { + throw new DeploymentException(sm.getString("serverContainer.failedDeployment", + servletContext.getContextPath(), servletContext.getVirtualServerName())); + } + + ServerEndpointConfig sec; + + try { + ServerEndpoint annotation = pojo.getAnnotation(ServerEndpoint.class); + if (annotation == null) { + throw new DeploymentException(sm.getString("serverContainer.missingAnnotation", pojo.getName())); + } + String path = annotation.value(); + + // Validate encoders + validateEncoders(annotation.encoders(), getInstanceManager(Thread.currentThread().getContextClassLoader())); + + // ServerEndpointConfig + Class configuratorClazz = annotation.configurator(); + Configurator configurator = null; + if (!configuratorClazz.equals(Configurator.class)) { + try { + configurator = annotation.configurator().getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new DeploymentException(sm.getString("serverContainer.configuratorFail", + annotation.configurator().getName(), pojo.getClass().getName()), e); + } + } + sec = ServerEndpointConfig.Builder.create(pojo, path).decoders(Arrays.asList(annotation.decoders())) + .encoders(Arrays.asList(annotation.encoders())) + .subprotocols(Arrays.asList(annotation.subprotocols())).configurator(configurator).build(); + } catch (DeploymentException de) { + failDeployment(); + throw de; + } + + addEndpoint(sec, fromAnnotatedPojo); + } + + + void failDeployment() { + deploymentFailed = true; + + // Clear all existing deployments + endpointsRegistered = false; + configExactMatchMap.clear(); + configTemplateMatchMap.clear(); + } + + + boolean areEndpointsRegistered() { + return endpointsRegistered; + } + + + @Override + public void upgradeHttpToWebSocket(Object httpServletRequest, Object httpServletResponse, ServerEndpointConfig sec, + Map pathParameters) throws IOException, DeploymentException { + try { + UpgradeUtil.doUpgrade(this, (HttpServletRequest) httpServletRequest, + (HttpServletResponse) httpServletResponse, sec, pathParameters); + } catch (ServletException e) { + throw new DeploymentException(e.getMessage(), e); + } + } + + + public WsMappingResult findMapping(String path) { + + // Check an exact match. Simple case as there are no templates. + ExactPathMatch match = configExactMatchMap.get(path); + if (match != null) { + return new WsMappingResult(match.getConfig(), Collections.emptyMap()); + } + + // No exact match. Need to look for template matches. + UriTemplate pathUriTemplate = null; + try { + pathUriTemplate = new UriTemplate(path); + } catch (DeploymentException e) { + // Path is not valid so can't be matched to a WebSocketEndpoint + return null; + } + + // Number of segments has to match + Integer key = Integer.valueOf(pathUriTemplate.getSegmentCount()); + ConcurrentSkipListMap templateMatches = configTemplateMatchMap.get(key); + + if (templateMatches == null) { + // No templates with an equal number of segments so there will be + // no matches + return null; + } + + // List is in alphabetical order of normalised templates. + // Correct match is the first one that matches. + ServerEndpointConfig sec = null; + Map pathParams = null; + for (TemplatePathMatch templateMatch : templateMatches.values()) { + pathParams = templateMatch.getUriTemplate().match(pathUriTemplate); + if (pathParams != null) { + sec = templateMatch.getConfig(); + break; + } + } + + if (sec == null) { + // No match + return null; + } + + return new WsMappingResult(sec, pathParams); + } + + + protected WsWriteTimeout getTimeout() { + return wsWriteTimeout; + } + + + /** + * {@inheritDoc} Overridden to make it visible to other classes in this package. + */ + @Override + protected InstanceManager getInstanceManager(ClassLoader classLoader) { + return super.getInstanceManager(classLoader); + } + + + /** + * {@inheritDoc} Overridden to make it visible to other classes in this package. + */ + @Override + protected void registerSession(Object key, WsSession wsSession) { + super.registerSession(key, wsSession); + if (wsSession.isOpen() && wsSession.getUserPrincipal() != null && wsSession.getHttpSessionId() != null) { + registerAuthenticatedSession(wsSession, wsSession.getHttpSessionId()); + } + } + + + /** + * {@inheritDoc} Overridden to make it visible to other classes in this package. + */ + @Override + protected void unregisterSession(Object key, WsSession wsSession) { + if (wsSession.getUserPrincipalInternal() != null && wsSession.getHttpSessionId() != null) { + unregisterAuthenticatedSession(wsSession, wsSession.getHttpSessionId()); + } + super.unregisterSession(key, wsSession); + } + + + private void registerAuthenticatedSession(WsSession wsSession, String httpSessionId) { + Set wsSessions = authenticatedSessions.get(httpSessionId); + if (wsSessions == null) { + wsSessions = ConcurrentHashMap.newKeySet(); + authenticatedSessions.putIfAbsent(httpSessionId, wsSessions); + wsSessions = authenticatedSessions.get(httpSessionId); + } + wsSessions.add(wsSession); + } + + + private void unregisterAuthenticatedSession(WsSession wsSession, String httpSessionId) { + Set wsSessions = authenticatedSessions.get(httpSessionId); + // wsSessions will be null if the HTTP session has ended + if (wsSessions != null) { + wsSessions.remove(wsSession); + } + } + + + public void closeAuthenticatedSession(String httpSessionId) { + Set wsSessions = authenticatedSessions.remove(httpSessionId); + + if (wsSessions != null && !wsSessions.isEmpty()) { + for (WsSession wsSession : wsSessions) { + try { + wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED); + } catch (IOException e) { + // Any IOExceptions during close will have been caught and the + // onError method called. + } + } + } + } + + + private static void validateEncoders(Class[] encoders, InstanceManager instanceManager) + throws DeploymentException { + + for (Class encoder : encoders) { + // Need to instantiate encoder to ensure it is valid and that + // deployment can be failed if it is not. The encoder is then + // discarded immediately. + Encoder instance; + try { + if (instanceManager == null) { + instance = encoder.getConstructor().newInstance(); + } else { + instance = (Encoder) instanceManager.newInstance(encoder); + instanceManager.destroyInstance(instance); + } + } catch (ReflectiveOperationException | NamingException e) { + throw new DeploymentException(sm.getString("serverContainer.encoderFail", encoder.getName()), e); + } + } + } + + + private static class TemplatePathMatch { + private final ServerEndpointConfig config; + private final UriTemplate uriTemplate; + private final boolean fromAnnotatedPojo; + + TemplatePathMatch(ServerEndpointConfig config, UriTemplate uriTemplate, boolean fromAnnotatedPojo) { + this.config = config; + this.uriTemplate = uriTemplate; + this.fromAnnotatedPojo = fromAnnotatedPojo; + } + + + public ServerEndpointConfig getConfig() { + return config; + } + + + public UriTemplate getUriTemplate() { + return uriTemplate; + } + + + public boolean isFromAnnotatedPojo() { + return fromAnnotatedPojo; + } + } + + + private static class ExactPathMatch { + private final ServerEndpointConfig config; + private final boolean fromAnnotatedPojo; + + ExactPathMatch(ServerEndpointConfig config, boolean fromAnnotatedPojo) { + this.config = config; + this.fromAnnotatedPojo = fromAnnotatedPojo; + } + + + public ServerEndpointConfig getConfig() { + return config; + } + + + public boolean isFromAnnotatedPojo() { + return fromAnnotatedPojo; + } + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsSessionListener.java b/java/org/apache/tomcat/websocket/server/WsSessionListener.java new file mode 100644 index 0000000..c3cd48f --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsSessionListener.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.websocket.server; + +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; + +public class WsSessionListener implements HttpSessionListener { + + private final WsServerContainer wsServerContainer; + + + public WsSessionListener(WsServerContainer wsServerContainer) { + this.wsServerContainer = wsServerContainer; + } + + + @Override + public void sessionDestroyed(HttpSessionEvent se) { + wsServerContainer.closeAuthenticatedSession(se.getSession().getId()); + } +} diff --git a/java/org/apache/tomcat/websocket/server/WsWriteTimeout.java b/java/org/apache/tomcat/websocket/server/WsWriteTimeout.java new file mode 100644 index 0000000..be5f23b --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/WsWriteTimeout.java @@ -0,0 +1,103 @@ +/* + * 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.tomcat.websocket.server; + +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.tomcat.websocket.BackgroundProcess; +import org.apache.tomcat.websocket.BackgroundProcessManager; + +/** + * Provides timeouts for asynchronous web socket writes. On the server side we only have access to + * {@link jakarta.servlet.ServletOutputStream} and {@link jakarta.servlet.ServletInputStream} so there is no way to set + * a timeout for writes to the client. + */ +public class WsWriteTimeout implements BackgroundProcess { + + /** + * Note: The comparator imposes orderings that are inconsistent with equals + */ + private final Set endpoints = new ConcurrentSkipListSet<>( + Comparator.comparingLong(WsRemoteEndpointImplServer::getTimeoutExpiry)); + private final AtomicInteger count = new AtomicInteger(0); + private int backgroundProcessCount = 0; + private volatile int processPeriod = 1; + + @Override + public void backgroundProcess() { + // This method gets called once a second. + backgroundProcessCount++; + + if (backgroundProcessCount >= processPeriod) { + backgroundProcessCount = 0; + + long now = System.currentTimeMillis(); + for (WsRemoteEndpointImplServer endpoint : endpoints) { + if (endpoint.getTimeoutExpiry() < now) { + // Background thread, not the thread that triggered the + // write so no need to use a dispatch + endpoint.onTimeout(false); + } else { + // Endpoints are ordered by timeout expiry so if this point + // is reached there is no need to check the remaining + // endpoints + break; + } + } + } + } + + + @Override + public void setProcessPeriod(int period) { + this.processPeriod = period; + } + + + /** + * {@inheritDoc} The default value is 1 which means asynchronous write timeouts are processed every 1 second. + */ + @Override + public int getProcessPeriod() { + return processPeriod; + } + + + public void register(WsRemoteEndpointImplServer endpoint) { + boolean result = endpoints.add(endpoint); + if (result) { + int newCount = count.incrementAndGet(); + if (newCount == 1) { + BackgroundProcessManager.getInstance().register(this); + } + } + } + + + public void unregister(WsRemoteEndpointImplServer endpoint) { + boolean result = endpoints.remove(endpoint); + if (result) { + int newCount = count.decrementAndGet(); + if (newCount == 0) { + BackgroundProcessManager.getInstance().unregister(this); + } + } + } +} diff --git a/java/org/apache/tomcat/websocket/server/package-info.java b/java/org/apache/tomcat/websocket/server/package-info.java new file mode 100644 index 0000000..36819c9 --- /dev/null +++ b/java/org/apache/tomcat/websocket/server/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ +/** + * Server-side specific implementation classes. These are in a separate package to make packaging a pure client JAR + * simpler. + */ +package org.apache.tomcat.websocket.server; \ No newline at end of file diff --git a/modules/cxf/.gitignore b/modules/cxf/.gitignore new file mode 100644 index 0000000..5f2dbe1 --- /dev/null +++ b/modules/cxf/.gitignore @@ -0,0 +1,12 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) +!/.mvn/wrapper/maven-wrapper.jar diff --git a/modules/cxf/pom.xml b/modules/cxf/pom.xml new file mode 100644 index 0000000..ac7ed8f --- /dev/null +++ b/modules/cxf/pom.xml @@ -0,0 +1,172 @@ + + + + 4.0.0 + + + org.apache + apache + 26 + + + org.apache.tomcat + tomcat-cxf + Apache CXF for Apache Tomcat CDI + Apache CXF packaged for Apache Tomcat CDI + + 3.5.3 + jar + + + 1.3 + 1.1.4 + 1.0 + 1.2.18 + + + + + org.apache.geronimo.specs + geronimo-jcdi_2.0_spec + ${geronimo-jcdi.version} + provided + + + javax.json + javax.json-api + ${javax.json-api.version} + + + javax.json.bind + javax.json.bind-api + ${javax.json.bind-api.version} + + + + org.apache.cxf + cxf-integration-cdi + ${project.version} + + + org.apache.cxf + cxf-rt-rs-client + ${project.version} + + + org.apache.cxf + cxf-rt-frontend-jaxrs + ${project.version} + + + org.apache.cxf + cxf-rt-rs-extension-providers + ${project.version} + + + org.apache.cxf + cxf-rt-rs-json-basic + ${project.version} + + + + org.apache.johnzon + johnzon-jsonb + ${johnzon.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + false + + + META-INF/cxf/bus-extensions.txt + + + ${mainClass} + + + + + + jakarta.activation:* + jakarta.annotation:jakarta.annotation-api + jakarta.el:* + jakarta.enterprise:jakarta.enterprise.cdi-api + jakarta.inject:* + jakarta.interceptor:* + + org.apache.geronimo.specs:geronimo-jta_1.1_spec + org.apache.ant:* + org.codehaus.woodstox:* + org.glassfish.jaxb:* + org.jvnet.staxex:* + com.fasterxml.woodstox:* + com.sun.istack:* + com.sun.xml.*:* + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + META-INF/LICENSE.txt + META-INF/LICENSE + META-INF/NOTICE.txt + META-INF/NOTICE + + module-info.class + META-INF/cxf/cxf.* + META-INF/cxf/cxf-servlet.* + META-INF/cxf/org.apache.cxf.bus.factory + META-INF/services/org.apache.cxf.bus.factory + META-INF/spring.* + + + + + + + + + + diff --git a/modules/cxf/src/main/java/tomcat/cxf/JsonBean.java b/modules/cxf/src/main/java/tomcat/cxf/JsonBean.java new file mode 100644 index 0000000..88ce138 --- /dev/null +++ b/modules/cxf/src/main/java/tomcat/cxf/JsonBean.java @@ -0,0 +1,28 @@ +/* + * 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 tomcat.cxf; + +import javax.enterprise.context.Dependent; +import javax.ws.rs.Produces; +import javax.ws.rs.ext.Provider; + +import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; + +@Produces("application/json") +@Provider +@Dependent +public class JsonBean extends JsonbJaxrsProvider {} diff --git a/modules/cxf/src/main/resources/META-INF/beans.xml b/modules/cxf/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..d483402 --- /dev/null +++ b/modules/cxf/src/main/resources/META-INF/beans.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/modules/cxf/src/main/resources/META-INF/web-fragment.xml b/modules/cxf/src/main/resources/META-INF/web-fragment.xml new file mode 100644 index 0000000..75ec8db --- /dev/null +++ b/modules/cxf/src/main/resources/META-INF/web-fragment.xml @@ -0,0 +1,39 @@ + + + + tomcat-cxf + + + + + + + ApacheTomcatCXFServlet + Apache Tomcat CXF Servlet + org.apache.cxf.cdi.CXFCdiServlet + 1 + + + ApacheTomcatCXFServlet + /api/* + + diff --git a/modules/jdbc-pool/LICENSE b/modules/jdbc-pool/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/modules/jdbc-pool/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. \ No newline at end of file diff --git a/modules/jdbc-pool/NOTICE b/modules/jdbc-pool/NOTICE new file mode 100644 index 0000000..b31eb1d --- /dev/null +++ b/modules/jdbc-pool/NOTICE @@ -0,0 +1,6 @@ +Apache Tomcat JDBC Pool +Copyright 2008-2024 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + diff --git a/modules/jdbc-pool/build.properties.default b/modules/jdbc-pool/build.properties.default new file mode 100644 index 0000000..e300ecb --- /dev/null +++ b/modules/jdbc-pool/build.properties.default @@ -0,0 +1,107 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +# build.properties.sample +# +# This is an example "build.properties" file, used to customize building +# Tomcat JDBC Pool for your local environment. It defines the location of all external +# modules that Tomcat JDBC Pool depends on. Copy this file to "build.properties" +# in the top-level source directory, and customize it as needed. +# ----------------------------------------------------------------------------- + +# ----- Version Control Flags ----- +version.major=1 +version.minor=1 +version.build=0 +version.patch=1 +version.suffix= + +# ----- Reproducible builds ----- +# Uncomment and set to current time for reproducible builds +#2021-03-18T06:00:00Z +#ant.tstamp.now=1616047200 + +# ----- Default Base Path for Dependent Packages ----- +# Please note this path must be absolute, not relative, +# as it is referenced with different working directory +# contexts by the various build scripts. +base.path=${basedir}/includes + +compile.source=1.8 +compile.target=1.8 +compile.release=8 +compile.debug=true + +# Do not pass -deprecation (-Xlint:deprecation) flag to javac +compile.deprecation=false + +# ----- Settings for Junit test database. + +# Common settings +testdb.username=root +testdb.password=password + +# H2 +testdb.url=jdbc:h2:~/.h2/test;QUERY_TIMEOUT=0;DB_CLOSE_ON_EXIT=TRUE;LOCK_TIMEOUT=50000;DEFAULT_LOCK_TIMEOUT=50000 +testdb.driverClassName=org.h2.Driver +testdb.validationQuery=SELECT 1 + +# MySQL +#testdb.url=jdbc:mysql://localhost:3306/mysql?autoReconnect=true +#testdb.driverClassName=com.mysql.jdbc.Driver +#testdb.validationQuery=SELECT 1 + +# Derby +#testdb.url=jdbc:derby:derbyDB;create=true +#testdb.driverClassName=org.apache.derby.jdbc.EmbeddedDriver +#testdb.validationQuery=VALUES 1 + +# JUnit Unit Test Suite +junit.version=4.13 +junit.home=${base.path}/junit-${junit.version} +junit.jar=${junit.home}/junit-${junit.version}.jar +junit.loc=https://repo.maven.apache.org/maven2/junit/junit/${junit.version}/junit-${junit.version}.jar + +# Hamcrest Library, used by JUnit +hamcrest.version=2.2 +hamcrest.home=${base.path}/hamcrest-${hamcrest.version} +hamcrest.jar=${hamcrest.home}/hamcrest-${hamcrest.version}.jar +hamcrest.loc=https://repo.maven.apache.org/maven2/org/hamcrest/hamcrest/${hamcrest.version}/hamcrest-${hamcrest.version}.jar + +mysql.home=${base.path}/mysql-connector-java-5.1.12 +mysql.jar=${mysql.home}/mysql-connector-java-5.1.12-bin.jar +mysql.loc=http://mysql.mirrors.hoobly.com/Downloads/Connector-J/mysql-connector-java-5.1.12.zip + +tomcat.version=8.0.14 +tomcat.home=${base.path}/apache-tomcat-${tomcat.version} +tomcat.dbcp.jar=${tomcat.home}/lib/tomcat-dbcp.jar +tomcat.juli.jar=${tomcat.home}/bin/tomcat-juli.jar +tomcat.loc=https://archive.apache.org/dist/tomcat/tomcat-8/v${tomcat.version}/bin/apache-tomcat-${tomcat.version}.zip + +tomcat.project.loc=https://svn.apache.org/repos/asf/tomcat/trunk/webapps/docs/project.xml +tomcat.project.dest=${base.path}/project.xml + +tomcat.xsl.loc=https://svn.apache.org/repos/asf/tomcat/trunk/webapps/docs/tomcat-docs.xsl +tomcat.xsl.dest=${base.path}/tomcat-docs.xsl + +derby.home=${base.path}/db-derby-10.5.1.1-bin +derby.loc=https://archive.apache.org/dist/db/derby/db-derby-10.5.1.1/db-derby-10.5.1.1-bin.tar.gz +derby.jar=${derby.home}/lib/derby.jar + +h2.version=2.1.210 +h2.home=${base.path}/h2-${h2.version} +h2.loc=https://repo.maven.apache.org/maven2/com/h2database/h2/${h2.version}/h2-${h2.version}.jar +h2.jar=${h2.home}/h2-${h2.version}.jar diff --git a/modules/jdbc-pool/build.xml b/modules/jdbc-pool/build.xml new file mode 100644 index 0000000..da4f801 --- /dev/null +++ b/modules/jdbc-pool/build.xml @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Creating test table for test purposes. + + + + + + + + + + + + + + + Performance and fairness tests. + + + + + + + + + + + + + + + + + Functional tests. + + + + + + + + + + + + + + + + + + + + + + + + + Testing:${test} + + + + + + + + + + + + + + + + + + diff --git a/modules/jdbc-pool/doc/changelog.xml b/modules/jdbc-pool/doc/changelog.xml new file mode 100644 index 0000000..2d7ddf8 --- /dev/null +++ b/modules/jdbc-pool/doc/changelog.xml @@ -0,0 +1,133 @@ + + + +]> + + + + &project; + + + Filip Hanik + Changelog + + + + +
    +

    + Starting with Apache Tomcat 7.0.19 in July 2011, Tomcat JDBC Connection Pool + is built and released as a component in official releases of Tomcat. + The changes are now listed in "jdbc-pool" sections of Apache Tomcat + changelog file. This changelog file is obsolete. +

    +
    + +
    + + + 1207712 Pool cleaner should be a global thread, not spawn one thread per connection pool. (fhanik) + 1073531 50805 Only initialize connections once when async (fhanik) + 1076380 50857 Correctly handle timeouts when the pool is busy when async (fhanik) + Added QueryTimeoutInterceptor to be able to configure timeouts on running queries automatically. + + +
    + +
    + + + 1069864 50759 Correctly set validation timestamp when using external validator.(fhanik) + + +
    + +
    + + + 1060998 50613 Fix concurrency issue around pool size calculation.(fhanik) + + +
    +
    + + + 1057743 Make sure passwords are masked.(fhanik) + + +
    +
    + + + 997321 Ensure threads borrowing connections do not + get stuck waiting for a new connection if a connection is released in + another thread. (markt) + 995432 Make interceptor class names, property names + and property values tolerant of whitespace by trimming the values before + use. (markt) + 995091 49831 Make sure pooled XAConnections are + closed when the connection pool shuts down. Patch provided by Daniel + Mikusa. (markt) + 995087 Code clean-up. Remove some unused code. (markt) + + 995083 Update to Tomcat 6.0.29 (for JULI). (markt) + + 992409 Code clean-up. Reduce sequences of three or more + blank lines to two blank lines. (markt) + 952811, 995095 48814 Add Validator + interface and allow users to configure a Validator class name. Patch + provided by Matt Passell. (markt) + 948073 Code clean-up. Remove unused imports. (markt) + + 943434 49224 Only try setting the username and + password if they are non-null. Patch provided by Matt Passell. (markt) + + 943032 49269 Set maxIdle to maxActive by + default to prevent warning on start when maxIdle > maxActive. Patch + provided by Matt Passell. (markt) + 940574 49241 Don't ignore the + suspectTimeout property. (fhanik) + 939320 Fix svn:keywords for property replacement. + (kkolinko) + 931550, 934651, 934677 Add a + statement cache. (fhanik) + 919076 Improve XA support. (fhanik) + 915940 48392 Add an interceptor to wrap + Statements and ResultSets to prevent access to the physical connection. + (fhanik) + 912026 Call setTransactionIsolation() before + anything else as some drivers require this to be the first call. (fhanik) + + 900017 Update Javadoc for XADataSource. (kkolinko) + + + +
    +
    + + + 720253 Document how to use interceptors + 717972 Added an interceptor that will clean up non closed statements when a connection is returned to the pool. (org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer) + 713763 Improve connection state handling + 713763 Improve connection state handling + + +
    + +
    diff --git a/modules/jdbc-pool/doc/jdbc-pool.xml b/modules/jdbc-pool/doc/jdbc-pool.xml new file mode 100644 index 0000000..f0b5e00 --- /dev/null +++ b/modules/jdbc-pool/doc/jdbc-pool.xml @@ -0,0 +1,993 @@ + + + +]> + + + &project; + + + Filip Hanik + The Tomcat JDBC Connection Pool + + + + +
    + +
    + +
    + +

    The JDBC Connection Pool org.apache.tomcat.jdbc.pool + is a replacement or an alternative to the Apache Commons DBCP + connection pool.

    + +

    So why do we need a new connection pool?

    + +

    Here are a few of the reasons:

    +
      +
    1. Commons DBCP 1.x is single threaded. In order to be thread safe + Commons locks the entire pool for short periods during both object + allocation and object return. Note that this does not apply to + Commons DBCP 2.x.
    2. +
    3. Commons DBCP 1.x can be slow. As the number of logical CPUs grows and + the number of concurrent threads attempting to borrow or return + objects increases, the performance suffers. For highly concurrent + systems the impact can be significant. Note that this does not apply + to Commons DBCP 2.x.
    4. +
    5. Commons DBCP is over 60 classes. tomcat-jdbc-pool core is 8 classes, + hence modifications for future requirement will require much less + changes. This is all you need to run the connection pool itself, the + rest is gravy.
    6. +
    7. Commons DBCP uses static interfaces. This means you have to use the + right version for a given JRE version or you may see + NoSuchMethodException exceptions.
    8. +
    9. It's not worth rewriting over 60 classes, when a connection pool can + be accomplished with a much simpler implementation.
    10. +
    11. Tomcat jdbc pool implements the ability retrieve a connection + asynchronously, without adding additional threads to the library + itself.
    12. +
    13. Tomcat jdbc pool is a Tomcat module, it depends on Tomcat JULI, a + simplified logging framework used in Tomcat.
    14. +
    15. Retrieve the underlying connection using the + javax.sql.PooledConnection interface.
    16. +
    17. Starvation proof. If a pool is empty, and threads are waiting for a + connection, when a connection is returned, the pool will awake the + correct thread waiting. Most pools will simply starve.
    18. +
    + +

    Features added over other connection pool implementations

    +
      +
    1. Support for highly concurrent environments and multi core/cpu systems.
    2. +
    3. Dynamic implementation of interface, will support java.sql and javax.sql interfaces for + your runtime environment (as long as your JDBC driver does the same), even when compiled with a lower version of the JDK.
    4. +
    5. Validation intervals - we don't have to validate every single time we use the connection, we can do this + when we borrow or return the connection, just not more frequent than an interval we can configure.
    6. +
    7. Run-Once query, a configurable query that will be run only once, when the connection to the database is established. + Very useful to setup session settings, that you want to exist during the entire time the connection is established.
    8. +
    9. Ability to configure custom interceptors. + This allows you to write custom interceptors to enhance the functionality. You can use interceptors to gather query stats, + cache session states, reconnect the connection upon failures, retry queries, cache query results, and so on. + Your options are endless and the interceptors are dynamic, not tied to a JDK version of a + java.sql/javax.sql interface.
    10. +
    11. High performance - we will show some differences in performance later on
    12. +
    13. Extremely simple, due to the very simplified implementation, the line count and source file count are very low, compare with c3p0 + that has over 200 source files(last time we checked), Tomcat jdbc has a core of 8 files, the connection pool itself is about half + that. As bugs may occur, they will be faster to track down, and easier to fix. Complexity reduction has been a focus from inception.
    14. +
    15. Asynchronous connection retrieval - you can queue your request for a connection and receive a Future<Connection> back.
    16. +
    17. Better idle connection handling. Instead of closing connections directly, it can still pool connections and sizes the idle pool with a smarter algorithm.
    18. +
    19. You can decide at what moment connections are considered abandoned, is it when the pool is full, or directly at a timeout + by specifying a pool usage threshold. +
    20. +
    21. The abandon connection timer will reset upon a statement/query activity. Allowing a connections that is in use for a long time to not timeout. + This is achieved using the ResetAbandonedTimer +
    22. +
    23. Close connections after they have been connected for a certain time. Age based close upon return to the pool. +
    24. +
    25. Get JMX notifications and log entries when connections are suspected for being abandoned. This is similar to + the removeAbandonedTimeout but it doesn't take any action, only reports the information. + This is achieved using the suspectTimeout attribute.
    26. +
    27. Connections can be retrieved from a java.sql.Driver, javax.sql.DataSource or javax.sql.XADataSource + This is achieved using the dataSource and dataSourceJNDI attributes.
    28. +
    29. XA connection support
    30. +
    + + +
    +
    +

    + Usage of the Tomcat connection pool has been made to be as simple as possible, for those of you that are familiar with commons-dbcp, the + transition will be very simple. Moving from other connection pools is also fairly straight forward. +

    + +

    The Tomcat connection pool offers a few additional features over what most other pools let you do:

    +
      +
    • initSQL - the ability to run an SQL statement exactly once, when the connection is created
    • +
    • validationInterval - in addition to running validations on connections, avoid running them too frequently.
    • +
    • jdbcInterceptors - flexible and pluggable interceptors to create any customizations around the pool, + the query execution and the result set handling. More on this in the advanced section.
    • +
    • fairQueue - Set the fair flag to true to achieve thread fairness or to use asynchronous connection retrieval
    • +
    +
    + +

    + The Tomcat Connection pool is configured as a resource described in The Tomcat JDBC documentation + With the only difference being that you have to specify the factory attribute and set the value to + org.apache.tomcat.jdbc.pool.DataSourceFactory +

    +
    + +

    + The connection pool only has another dependency, and that is on tomcat-juli.jar. + To configure the pool in a stand alone project using bean instantiation, the bean to instantiate is + org.apache.tomcat.jdbc.pool.DataSource. The same attributes (documented below) as you use to configure a connection + pool as a JNDI resource, are used to configure a data source as a bean. +

    +
    + +

    + The connection pool object exposes an MBean that can be registered. + In order for the connection pool object to create the MBean, the flag jmxEnabled has to be set to true. + This doesn't imply that the pool will be registered with an MBean server, merely that the MBean is created. + In a container like Tomcat, Tomcat itself registers the DataSource with the MBean server, the + org.apache.tomcat.jdbc.pool.DataSource object will then register the actual + connection pool MBean. + If you're running outside of a container, you can register the DataSource yourself under any object name you specify, + and it propagates the registration to the underlying pool. To do this you would call mBeanServer.registerMBean(dataSource.getPool().getJmxPool(),objectname). + Prior to this call, ensure that the pool has been created by calling dataSource.createPool(). +

    +
    + +
    +
    +

    To provide a very simple switch to and from commons-dbcp and tomcat-jdbc-pool, + Most attributes are the same and have the same meaning.

    + + + +

    factory is required, and the value should be org.apache.tomcat.jdbc.pool.DataSourceFactory

    +
    + +

    Type should always be javax.sql.DataSource or javax.sql.XADataSource

    +

    Depending on the type a org.apache.tomcat.jdbc.pool.DataSource or a org.apache.tomcat.jdbc.pool.XADataSource will be created.

    +
    +
    +
    + + +

    System properties are JVM wide, affect all pools created in the JVM

    + + +

    (boolean) Controls classloading of dynamic classes, such as + JDBC drivers, interceptors and validators. If set to + false, default value, the pool will first attempt + to load using the current loader (i.e. the class loader that + loaded the pool classes) and if class loading fails attempt to + load using the thread context loader. Set this value to + true, if you wish to remain backwards compatible + with Apache Tomcat 8.0.8 and earlier, and only attempt the + current loader. + If not set then the default value is false. +

    +
    +
    +
    + + +

    These attributes are shared between commons-dbcp and tomcat-jdbc-pool, in some cases default values are different.

    + + + +

    (boolean) The default auto-commit state of connections created by this pool. If not set, default is JDBC driver default (If not set then the setAutoCommit method will not be called.)

    +
    + + +

    (boolean) The default read-only state of connections created by this pool. If not set then the setReadOnly method will not be called. (Some drivers don't support read only mode, ex: Informix)

    +
    + + +

    (String) The default TransactionIsolation state of connections created by this pool. One of the following: (see javadoc )

    +
      +
    • NONE
    • +
    • READ_COMMITTED
    • +
    • READ_UNCOMMITTED
    • +
    • REPEATABLE_READ
    • +
    • SERIALIZABLE
    • +
    +

    If not set, the method will not be called and it defaults to the JDBC driver.

    +
    + + +

    (String) The default catalog of connections created by this pool.

    +
    + + +

    (String) The fully qualified Java class name of the JDBC driver to be used. The driver has to be accessible + from the same classloader as tomcat-jdbc.jar +

    +
    + + +

    (String) The connection username to be passed to our JDBC driver to establish a connection. + Note that method DataSource.getConnection(username,password) + by default will not use credentials passed into the method, + but will use the ones configured here. See alternateUsernameAllowed + property for more details. +

    +
    + + +

    (String) The connection password to be passed to our JDBC driver to establish a connection. + Note that method DataSource.getConnection(username,password) + by default will not use credentials passed into the method, + but will use the ones configured here. See alternateUsernameAllowed + property for more details. +

    +
    + + +

    (int) The maximum number of active connections that can be allocated from this pool at the same time. + The default value is 100

    +
    + + +

    (int) The maximum number of connections that should be kept in the pool at all times. + Default value is maxActive:100 + Idle connections are checked periodically (if enabled) and + connections that been idle for longer than minEvictableIdleTimeMillis + will be released. (also see testWhileIdle)

    +
    + + +

    + (int) The minimum number of established connections that should be kept in the pool at all times. + The connection pool can shrink below this number if validation queries fail. + Default value is derived from initialSize:10 (also see testWhileIdle) +

    +
    + + +

    (int)The initial number of connections that are created when the pool is started. + Default value is 10

    +
    + + +

    (int) The maximum number of milliseconds that the pool will wait (when there are no available connections) + for a connection to be returned before throwing an exception. + Default value is 30000 (30 seconds)

    +
    + + +

    (boolean) The indication of whether objects will be validated before being borrowed from the pool. + If the object fails to validate, it will be dropped from the pool, and we will attempt to borrow another. + In order to have a more efficient validation, see validationInterval. + Default value is false +

    +
    + + +

    (boolean) The indication of whether objects will be validated when a connection is first created. + If an object fails to validate, it will be throw SQLException. + Default value is false +

    +
    + + +

    (boolean) The indication of whether objects will be validated before being returned to the pool. + The default value is false. +

    +
    + + +

    (boolean) The indication of whether objects will be validated by the idle object evictor (if any). + If an object fails to validate, it will be dropped from the pool. + The default value is false and this property has to be set in order for the + pool cleaner/test thread is to run (also see timeBetweenEvictionRunsMillis) +

    +
    + + +

    (String) The SQL query that will be used to validate connections from this pool before returning them to the caller. + If specified, this query does not have to return any data, it just can't throw a SQLException. + The default value is null. + If not specified, connections will be validation by the isValid() method. + Example values are SELECT 1(mysql), select 1 from dual(oracle), SELECT 1(MS Sql Server) +

    +
    + + +

    (int) The timeout in seconds before a connection validation queries fail. This works by calling + java.sql.Statement.setQueryTimeout(seconds) on the statement that executes the validationQuery. + The pool itself doesn't timeout the query, it is still up to the JDBC driver to enforce query timeouts. + A value less than or equal to zero will disable this feature. + The default value is -1. +

    +
    + + +

    (String) The name of a class which implements the + org.apache.tomcat.jdbc.pool.Validator interface and + provides a no-arg constructor (may be implicit). If specified, the + class will be used to create a Validator instance which is then used + instead of any validation query to validate connections. The default + value is null. An example value is + com.mycompany.project.SimpleValidator. +

    +
    + + +

    (int) The number of milliseconds to sleep between runs of the idle connection validation/cleaner thread. + This value should not be set under 1 second. It dictates how often we check for idle, abandoned connections, and how often + we validate idle connections. This value will be overridden by maxAge if the latter is non-zero and lower. + The default value is 5000 (5 seconds).
    +

    +
    + + +

    (int) Property not used in tomcat-jdbc-pool.

    +
    + + +

    (int) The minimum amount of time an object may sit idle in the pool before it is eligible for eviction. + The default value is 60000 (60 seconds).

    +
    + + +

    (boolean) Property not used. Access can be achieved by calling unwrap on the pooled connection. + see javax.sql.DataSource interface, or call getConnection through reflection or + cast the object as javax.sql.PooledConnection

    +
    + + +

    (boolean) Flag to remove abandoned connections if they exceed the removeAbandonedTimeout. + If set to true a connection is considered abandoned and eligible for removal if it has been in use + longer than the removeAbandonedTimeout Setting this to true can recover db connections from + applications that fail to close a connection. See also logAbandoned + The default value is false.

    +
    + + +

    (int) Timeout in seconds before an abandoned(in use) connection can be removed. + The default value is 60 (60 seconds). The value should be set to the longest running query your applications + might have.

    +
    + + +

    (boolean) Flag to log stack traces for application code which abandoned a Connection. + Logging of abandoned Connections adds overhead for every Connection borrow because a stack trace has to be generated. + The default value is false.

    +
    + + +

    (String) The connection properties that will be sent to our JDBC driver when establishing new connections. + Format of the string must be [propertyName=property;]* + NOTE - The "user" and "password" properties will be passed explicitly, so they do not need to be included here. + The default value is null.

    +
    + + +

    (boolean) Property not used.

    +
    + + +

    (int) Property not used.

    +
    + +
    + +
    + + + + + + +

    (String) A custom query to be run when a connection is first created. + The default value is null.

    +
    + + +

    (String) A semicolon separated list of classnames extending + org.apache.tomcat.jdbc.pool.JdbcInterceptor class. + See Configuring JDBC interceptors + below for more detailed description of syntax and examples. +

    +

    + These interceptors will be inserted as an interceptor into the chain + of operations on a java.sql.Connection object. + The default value is null. +

    +

    + Predefined interceptors:
    + org.apache.tomcat.jdbc.pool.interceptor.
    ConnectionState
    + - keeps track of auto commit, read only, catalog and transaction isolation level.
    + org.apache.tomcat.jdbc.pool.interceptor.
    StatementFinalizer
    + - keeps track of opened statements, and closes them when the connection is returned to the pool. +

    +

    + More predefined interceptors are described in detail in the + JDBC Interceptors section. +

    +
    + + +

    (long) avoid excess validation, only run validation at most at this frequency - time in milliseconds. + If a connection is due for validation, but has been validated previously within this interval, it will not be validated again. + The default value is 3000 (3 seconds).

    +
    + + +

    (boolean) Register the pool with JMX or not. + The default value is true.

    +
    + + +

    (boolean) Set to true if you wish that calls to getConnection should be treated + fairly in a true FIFO fashion. This uses the org.apache.tomcat.jdbc.pool.FairBlockingQueue + implementation for the list of the idle connections. The default value is true. + This flag is required when you want to use asynchronous connection retrieval.
    + Setting this flag ensures that threads receive connections in the order they arrive.
    + During performance tests, there is a very large difference in how locks + and lock waiting is implemented. When fairQueue=true + there is a decision making process based on what operating system the system is running. + If the system is running on Linux (property os.name=Linux. + To disable this Linux specific behavior and still use the fair queue, simply add the property + org.apache.tomcat.jdbc.pool.FairBlockingQueue.ignoreOS=true to your system properties + before the connection pool classes are loaded. +

    +
    + + +

    (int) Connections that have been abandoned (timed out) won't get closed and reported up unless + the number of connections in use are above the percentage defined by abandonWhenPercentageFull. + The value should be between 0-100. + The default value is 0, which implies that connections are eligible for closure as soon + as removeAbandonedTimeout has been reached.

    +
    + + +

    (long) Time in milliseconds to keep a connection before recreating it. + When a connection is borrowed from the pool, the pool will check to see + if the now - time-when-connected > maxAge has been reached + , and if so, it reconnects before borrow it. When a connection is + returned to the pool, the pool will check to see if the + now - time-when-connected > maxAge has been reached, and + if so, it tries to reconnect. + When a connection is idle and timeBetweenEvictionRunsMillis is + greater than zero, the pool will periodically check to see if the + now - time-when-connected > maxAge has been reached, and + if so, it tries to reconnect. + Setting maxAge to a value lower than timeBetweenEvictionRunsMillis + will override it (so idle connection validation/cleaning will run more frequently). + The default value is 0, which implies that connections + will be left open and no age check will be done upon borrowing from the + pool, returning the connection to the pool or when checking idle connections.

    +
    + + +

    (boolean) Set to true if you wish the ProxyConnection class to use String.equals and set to false + when you wish to use == when comparing method names. This property does not apply to added interceptors as those are configured individually. + The default value is true. +

    +
    + +

    (int) Timeout value in seconds. Default value is 0.
    + Similar to to the removeAbandonedTimeout value but instead of treating the connection + as abandoned, and potentially closing the connection, this simply logs the warning if + logAbandoned is set to true. If this value is equal or less than 0, no suspect + checking will be performed. Suspect checking only takes place if the timeout value is larger than 0 and + the connection was not abandoned or if abandon check is disabled. If a connection is suspect a WARN message gets + logged and a JMX notification gets sent once. +

    +
    + +

    (boolean) If autoCommit==false then the pool can terminate the transaction by calling rollback on the connection as it is returned to the pool + Default value is false.
    +

    +
    + +

    (boolean) If autoCommit==false then the pool can complete the transaction by calling commit on the connection as it is returned to the pool + If rollbackOnReturn==true then this attribute is ignored. + Default value is false.
    +

    +
    + +

    (boolean) By default, the jdbc-pool will ignore the + DataSource.getConnection(username,password) + call, and simply return a previously pooled connection under the globally configured properties username and password, for performance reasons. +

    +

    + The pool can however be configured to allow use of different credentials + each time a connection is requested. To enable the functionality described in the + DataSource.getConnection(username,password) + call, simply set the property alternateUsernameAllowed + to true.
    + Should you request a connection with the credentials user1/password1 and the connection + was previously connected using different user2/password2, the connection will be closed, + and reopened with the requested credentials. This way, the pool size is still managed + on a global level, and not on a per schema level.
    + The default value is false.
    + This property was added as an enhancement to bug 50025. +

    +
    + +

    (javax.sql.DataSource) Inject a data source to the connection pool, and the pool will use the data source to retrieve connections instead of establishing them using the java.sql.Driver interface. + This is useful when you wish to pool XA connections or connections established using a data source instead of a connection string. Default value is null +

    +
    + +

    (String) The JNDI name for a data source to be looked up in JNDI and then used to establish connections to the database. See the dataSource attribute. Default value is null +

    +
    + +

    (boolean) Set this to true if you wish to put a facade on your connection so that it cannot be reused after it has been closed. This prevents a thread holding on to a + reference of a connection it has already called closed on, to execute queries on it. Default value is true. +

    +
    + +

    (boolean) Set this to true to log errors during the validation phase to the log file. If set to true, errors will be logged as SEVERE. Default value is false for backwards compatibility. +

    +
    + +

    (boolean) Set this to true to propagate the interrupt state for a thread that has been interrupted (not clearing the interrupt state). Default value is false for backwards compatibility. +

    +
    + +

    (boolean) Flag whether ignore error of connection creation while initializing the pool. + Set to true if you want to ignore error of connection creation while initializing the pool. + Set to false if you want to fail the initialization of the pool by throwing exception. + The default value is false. +

    +
    + +

    (boolean) Set this to true if you wish to wrap statements in order to + enable equals() and hashCode() methods to be + called on the closed statements if any statement proxy is set. + Default value is true. +

    +
    + +
    +
    +
    +
    + +

    To see an example of how to use an interceptor, take a look at + org.apache.tomcat.jdbc.pool.interceptor.ConnectionState. + This simple interceptor is a cache of three attributes, transaction isolation level, auto commit and read only state, + in order for the system to avoid not needed roundtrips to the database. +

    +

    Further interceptors will be added to the core of the pool as the need arises. Contributions are always welcome!

    +

    Interceptors are of course not limited to just java.sql.Connection but can be used to wrap any + of the results from a method invocation as well. You could build query performance analyzer that provides JMX notifications when a + query is running longer than the expected time.

    +
    + +

    Configuring JDBC interceptors is done using the jdbcInterceptors property. + The property contains a list of semicolon separated class names. If the + classname is not fully qualified it will be prefixed with the + org.apache.tomcat.jdbc.pool.interceptor. prefix. +

    +

    Example:
    + + jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.ConnectionState; + org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer" + +
    + is the same as +
    + jdbcInterceptors="ConnectionState;StatementFinalizer" +

    +

    + Interceptors can have properties as well. Properties for an interceptor + are specified within parentheses after the class name. Several properties + are separated by commas. +

    +

    Example:
    + + jdbcInterceptors="ConnectionState;StatementFinalizer(useEquals=true)" + +

    +

    + Extra whitespace characters around class names, property names and values + are ignored. +

    +
    + +

    Abstract base class for all interceptors, cannot be instantiated.

    + + +

    (boolean) Set to true if you wish the ProxyConnection class to use String.equals and set to false + when you wish to use == when comparing method names. + The default value is true. +

    +
    +
    +
    + +

    Caches the connection for the following attributes autoCommit, readOnly, + transactionIsolation and catalog. + It is a performance enhancement to avoid roundtrip to the database when getters are called or setters are called with an already set value. +

    + + +
    + +

    Keeps track of all statements created using createStatement, prepareStatement or prepareCall + and closes these statements when the connection is returned to the pool. +

    + + +

    (boolean as String) Enable tracing of unclosed statements. + When enabled and a connection is closed, and statements are not closed, + the interceptor will log all stack traces. + The default value is false. +

    +
    +
    +
    + +

    Caches PreparedStatement and/or CallableStatement + instances on a connection. +

    +

    The statements are cached per connection. + The count limit is counted globally for all connections that belong to + the same pool. Once the count reaches max, subsequent + statements are not returned to the cache and are closed immediately. +

    + + +

    (boolean as String) Enable caching of PreparedStatement + instances created using prepareStatement calls. + The default value is true. +

    +
    + +

    (boolean as String) Enable caching of CallableStatement + instances created using prepareCall calls. + The default value is false. +

    +
    + +

    (int as String) Limit on the count of cached statements across + the connection pool. + The default value is 50. +

    +
    +
    +
    + +

    See 48392. Interceptor to wrap statements and result sets in order to prevent access to the actual connection + using the methods ResultSet.getStatement().getConnection() and Statement.getConnection() +

    + + +
    + +

    Automatically calls java.sql.Statement.setQueryTimeout(seconds) when a new statement is created. + The pool itself doesn't timeout the query, it is still up to the JDBC driver to enforce query timeouts. +

    + + +

    (int as String) The number of seconds to set for the query timeout. + A value less than or equal to zero will disable this feature. + The default value is 1 seconds. +

    +
    +
    +
    + +

    Keeps track of query performance and issues log entries when queries exceed a time threshold of fail. + The log level used is WARN +

    + + +

    (int as String) The number of milliseconds a query has to exceed before issuing a log alert. + The default value is 1000 milliseconds. +

    +
    + +

    (int as String) The maximum number of queries to keep track of in order to preserve memory space. + A value less than or equal to 0 will disable this feature. + The default value is 1000. +

    +
    + +

    (boolean as String) Set to true if you wish to log slow queries. + The default value is true. +

    +
    + +

    (boolean as String) Set to true if you wish to log failed queries. + The default value is false. +

    +
    +
    +
    + +

    Extends the SlowQueryReport and in addition to log entries it issues JMX notification + for monitoring tools to react to. Inherits all the attributes from its parent class. + This class uses Tomcat's JMX engine so it won't work outside of the Tomcat container. + By default, JMX notifications are sent through the ConnectionPool mbean if it is enabled. + The SlowQueryReportJmx can also register an MBean if notifyPool=false +

    + + +

    (boolean as String) Set to false if you want JMX notifications to go to the SlowQueryReportJmx MBean + The default value is true. +

    +
    + +

    (String) Define a valid javax.management.ObjectName string that will be used to register this object with the platform mbean server + The default value is null and the object will be registered using + tomcat.jdbc:type=org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReportJmx,name=the-name-of-the-pool +

    +
    +
    +
    + +

    + The abandoned timer starts when a connection is checked out from the pool. + This means if you have a 30second timeout and run 10x10second queries using the connection + it will be marked abandoned and potentially reclaimed depending on the abandonWhenPercentageFull + attribute. + Using this interceptor it will reset the checkout timer every time you perform an operation on the connection or execute a + query successfully. +

    + + +
    +
    + +
    +

    Other examples of Tomcat configuration for JDBC usage can be found in the Tomcat documentation.

    + +

    Here is a simple example of how to create and use a data source.

    + +
    + +

    And here is an example on how to configure a resource for JNDI lookups

    +]]> + +
    + +

    The Tomcat JDBC connection pool supports asynchronous connection retrieval without adding additional threads to the + pool library. It does this by adding a method to the data source called Future<Connection> getConnectionAsync(). + In order to use the async retrieval, two conditions must be met: +

    +
      +
    1. You must configure the fairQueue property to be true.
    2. +
    3. You will have to cast the data source to org.apache.tomcat.jdbc.pool.DataSource
    4. +
    + An example of using the async feature is show below. + future = datasource.getConnectionAsync(); + while (!future.isDone()) { + System.out.println("Connection is not yet available. Do some background work"); + try { + Thread.sleep(100); //simulate work + }catch (InterruptedException x) { + Thread.currentThread().interrupt(); + } + } + con = future.get(); //should return instantly + Statement st = con.createStatement(); + ResultSet rs = st.executeQuery("select * from user");]]> + +
    + +

    Interceptors are a powerful way to enable, disable or modify functionality on a specific connection or its sub components. + There are many different use cases for when interceptors are useful. By default, and for performance reasons, the connection pool is stateless. + The only state the pool itself inserts are defaultAutoCommit, defaultReadOnly, defaultTransactionIsolation, defaultCatalog if + these are set. These 4 properties are only set upon connection creation. Should these properties be modified during the usage of the connection, + the pool itself will not reset them.

    +

    An interceptor has to extend the org.apache.tomcat.jdbc.pool.JdbcInterceptor class. This class is fairly simple, + You will need to have a no arg constructor

    + +

    + When a connection is borrowed from the pool, the interceptor can initialize or in some other way react to the event by implementing the +

    + +

    + method. This method gets called with two parameters, a reference to the connection pool itself ConnectionPool parent + and a reference to the underlying connection PooledConnection con. +

    +

    + When a method on the java.sql.Connection object is invoked, it will cause the +

    + +

    + method to get invoked. The Method method is the actual method invoked, and Object[] args are the arguments. + To look at a very simple example, where we demonstrate how to make the invocation to java.sql.Connection.close() a noop + if the connection has been closed +

    + +

    + There is an observation being made. It is the comparison of the method name. One way to do this would be to do + "close".equals(method.getName()). + Above we see a direct reference comparison between the method name and static final String reference. + According to the JVM spec, method names and static final String end up in a shared constant pool, so the reference comparison should work. + One could of course do this as well: +

    + +

    + The compare(String,Method) will use the useEquals flag on an interceptor and do either reference comparison or + a string value comparison when the useEquals=true flag is set. +

    +

    Pool start/stop
    + When the connection pool is started or closed, you can be notified. You will only be notified once per interceptor class + even though it is an instance method. and you will be notified using an interceptor currently not attached to a pool. +

    + +

    + When overriding these methods, don't forget to call super if you are extending a class other than JdbcInterceptor +

    +

    Configuring interceptors
    + Interceptors are configured using the jdbcInterceptors property or the setJdbcInterceptors method. + An interceptor can have properties, and would be configured like this +

    + + +

    Interceptor properties
    + Since interceptors can have properties, you need to be able to read the values of these properties within your + interceptor. Taking an example like the one above, you can override the setProperties method. +

    + properties) { + super.setProperties(properties); + final String myprop = "myprop"; + InterceptorProperty p1 = properties.get(myprop); + if (p1!=null) { + setMyprop(Long.parseLong(p1.getValue())); + } + }]]> + +
    + +

    Connection pools create wrappers around the actual connection in order to properly pool them. + We also create interceptors in these wrappers to be able to perform certain functions. + If there is a need to retrieve the actual connection, one can do so using the javax.sql.PooledConnection + interface. +

    + + +
    + +
    + +
    +

    We build the JDBC pool code with 1.6, but it is backwards compatible down to 1.5 for runtime environment. For unit test, we use 1.6 and higher

    +

    Other examples of Tomcat configuration for JDBC usage can be found in the Tomcat documentation.

    + +

    Building is pretty simple. The pool has a dependency on tomcat-juli.jar and in case you want the SlowQueryReportJmx

    + +

    + A build file can be found in the Tomcat source repository. +

    +

    + As a convenience, a build file is also included where a simple build command will generate all files needed. +

    + ant download (downloads dependencies) + ant build (compiles and generates .jar files) + ant dist (creates a release package) + ant test (runs tests, expects a test database to be setup) + +

    + The system is structured for a Maven build, but does generate release artifacts. Just the library itself. +

    +
    +
    + + +
    diff --git a/modules/jdbc-pool/doc/package.xsl b/modules/jdbc-pool/doc/package.xsl new file mode 100644 index 0000000..a987c80 --- /dev/null +++ b/modules/jdbc-pool/doc/package.xsl @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <xsl:value-of select="project/title"/> - <xsl:value-of select="properties/title"/> + + + +

    .

    + + + +
    + + + + + + + + + + + + + + + +
    + + + + + +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + +
    + + + + + +
    +
    + +
    +
    +
    + + + + + + +
    + + + + + + + + + + + + + + + + +
    + + + + + +
    + + +
    +              
    +            
    +
    + +
    + + + + + +
    +
    +
    + + + + + + + + + + + + + + +
    + Attribute + + Description +
    + + + + + + + + +
    +
    + + + + + + + + + + + + + + +
    + Property + + Description +
    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/modules/jdbc-pool/doc/project.xml b/modules/jdbc-pool/doc/project.xml new file mode 100644 index 0000000..912903d --- /dev/null +++ b/modules/jdbc-pool/doc/project.xml @@ -0,0 +1,31 @@ + + + + + Apache Tomcat JDBC Pool + + + The Apache Tomcat Servlet/JSP Container + + + + + + + diff --git a/modules/jdbc-pool/pom.xml b/modules/jdbc-pool/pom.xml new file mode 100644 index 0000000..986b90d --- /dev/null +++ b/modules/jdbc-pool/pom.xml @@ -0,0 +1,160 @@ + + + + 4.0.0 + + + org.apache + apache + 15 + + + org.apache.tomcat + tomcat-jdbc + 8.0.15-SNAPSHOT + + jar + + jdbc-pool + https://people.apache.org/~fhanik/jdbc-pool/ + + + UTF-8 + + + + + Development List + dev-subscribe@tomcat.apache.org + dev-unsubscribe@tomcat.apache.org + dev@tomcat.apache.org + + + Users List + users-subscribe@tomcat.apache.org + users-unsubscribe@tomcat.apache.org + users@tomcat.apache.org + + + + + scm:svn:https://svn.apache.org/repos/asf/tomcat/trunk/modules/jdbc-pool + scm:svn:https://svn.apache.org/repos/asf/tomcat/trunk/modules/jdbc-pool + https://svn.apache.org/viewvc/tomcat/trunk/modules/jdbc-pool + + + + + org.apache.tomcat + tomcat-juli + 10.1.0-M8 + + + junit + junit + 4.13.2 + test + + + org.apache.tomcat + tomcat-dbcp + 10.1.0-M8 + test + + + com.h2database + h2 + 2.2.220 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + true + true + true + true + + + + org.apache.maven.plugins + maven-jar-plugin + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.2 + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.10 + + true + true + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + diff --git a/modules/jdbc-pool/resources/MANIFEST.MF b/modules/jdbc-pool/resources/MANIFEST.MF new file mode 100644 index 0000000..6bf82b2 --- /dev/null +++ b/modules/jdbc-pool/resources/MANIFEST.MF @@ -0,0 +1,22 @@ +Manifest-Version: 1.0 +Export-Package: org.apache.tomcat.jdbc.naming;uses:="javax.naming,org.ap + ache.juli.logging,javax.naming.spi";version="@VERSION@",org.apache.tomc + at.jdbc.pool;uses:="org.apache.juli.logging,javax.sql,org.apache.tomcat + .jdbc.pool.jmx,javax.management,javax.naming,javax.naming.spi,org.apach + e.tomcat.jdbc.pool.interceptor";version="@VERSION@",org.apache.tomcat.j + dbc.pool.interceptor;uses:="org.apache.tomcat.jdbc.pool,org.apache.juli + .logging,javax.management.openmbean,javax.management";version="@VERSION@ + ",org.apache.tomcat.jdbc.pool.jmx;uses:="org.apache.tomcat.jdbc.pool,or + g.apache.juli.logging,javax.management";version="@VERSION@" +Bundle-Vendor: Apache Software Foundation +Bundle-Version: @VERSION@ +Bundle-Name: Apache Tomcat JDBC Connection Pool +Bundle-ManifestVersion: 2 +Bundle-SymbolicName: org.apache.tomcat.jdbc +Import-Package: + javax.management;version="0", + javax.management.openmbean;version="0", + javax.naming;version="0", + javax.naming.spi;version="0", + javax.sql;version="0", + org.apache.juli.logging;version="0" diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/naming/GenericNamingResourcesFactory.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/naming/GenericNamingResourcesFactory.java new file mode 100644 index 0000000..837fc56 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/naming/GenericNamingResourcesFactory.java @@ -0,0 +1,239 @@ +/* + * 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.tomcat.jdbc.naming; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.ClassLoaderUtil; + +/** + * Simple way of configuring generic resources by using reflection. + * Example usage: + *
    
    + * <Resource factory="org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory"
    + *              name="jdbc/test"
    + *              type="org.apache.derby.jdbc.ClientXADataSource"
    + *              databaseName="sample"
    + *              createDatabase="create"
    + *              serverName="localhost"
    + *              port="1527"/>
    + * 
    + * + */ +public class GenericNamingResourcesFactory implements ObjectFactory { + private static final Log log = LogFactory.getLog(GenericNamingResourcesFactory.class); + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception { + if ((obj == null) || !(obj instanceof Reference)) { + return null; + } + Reference ref = (Reference) obj; + Enumeration refs = ref.getAll(); + + String type = ref.getClassName(); + Object o = + ClassLoaderUtil.loadClass( + type, + GenericNamingResourcesFactory.class.getClassLoader(), + Thread.currentThread().getContextClassLoader()).getConstructor().newInstance(); + + while (refs.hasMoreElements()) { + RefAddr addr = refs.nextElement(); + String param = addr.getType(); + String value = null; + if (addr.getContent()!=null) { + value = addr.getContent().toString(); + } + if (setProperty(o, param, value)) { + + } else { + log.debug("Property not configured["+param+"]. No setter found on["+o+"]."); + } + } + return o; + } + + @SuppressWarnings("null") // setPropertyMethodVoid can't be null when used + private static boolean setProperty(Object o, String name, String value) { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: setProperty(" + + o.getClass() + " " + name + "=" + value + ")"); + } + + String setter = "set" + capitalize(name); + + try { + Method[] methods = o.getClass().getMethods(); + Method setPropertyMethodVoid = null; + Method setPropertyMethodBool = null; + + // First, the ideal case - a setFoo( String ) method + for (int i = 0; i < methods.length; i++) { + Class[] paramT = methods[i].getParameterTypes(); + if (setter.equals(methods[i].getName()) && paramT.length == 1 + && "java.lang.String".equals(paramT[0].getName())) { + + methods[i].invoke(o, new Object[] { value }); + return true; + } + } + + // Try a setFoo ( int ) or ( boolean ) + for (int i = 0; i < methods.length; i++) { + boolean ok = true; + if (setter.equals(methods[i].getName()) + && methods[i].getParameterTypes().length == 1) { + + // match - find the type and invoke it + Class paramType = methods[i].getParameterTypes()[0]; + Object[] params = new Object[1]; + + // Try a setFoo ( int ) + if ("java.lang.Integer".equals(paramType.getName()) + || "int".equals(paramType.getName())) { + try { + params[0] = Integer.valueOf(value); + } catch (NumberFormatException ex) { + ok = false; + } + // Try a setFoo ( long ) + }else if ("java.lang.Long".equals(paramType.getName()) + || "long".equals(paramType.getName())) { + try { + params[0] = Long.valueOf(value); + } catch (NumberFormatException ex) { + ok = false; + } + + // Try a setFoo ( boolean ) + } else if ("java.lang.Boolean".equals(paramType.getName()) + || "boolean".equals(paramType.getName())) { + params[0] = Boolean.valueOf(value); + + // Try a setFoo ( InetAddress ) + } else if ("java.net.InetAddress".equals(paramType + .getName())) { + try { + params[0] = InetAddress.getByName(value); + } catch (UnknownHostException exc) { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: Unable to resolve host name:" + value); + } + ok = false; + } + + // Unknown type + } else { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: Unknown type " + + paramType.getName()); + } + } + + if (ok) { + methods[i].invoke(o, params); + return true; + } + } + + // save "setProperty" for later + if ("setProperty".equals(methods[i].getName())) { + if (methods[i].getReturnType()==Boolean.TYPE){ + setPropertyMethodBool = methods[i]; + }else { + setPropertyMethodVoid = methods[i]; + } + + } + } + + // Ok, no setXXX found, try a setProperty("name", "value") + if (setPropertyMethodBool != null || setPropertyMethodVoid != null) { + Object[] params = new Object[2]; + params[0] = name; + params[1] = value; + if (setPropertyMethodBool != null) { + try { + return ((Boolean) setPropertyMethodBool.invoke(o, params)).booleanValue(); + }catch (IllegalArgumentException biae) { + //the boolean method had the wrong + //parameter types. lets try the other + if (setPropertyMethodVoid!=null) { + setPropertyMethodVoid.invoke(o, params); + return true; + }else { + throw biae; + } + } + } else { + setPropertyMethodVoid.invoke(o, params); + return true; + } + } + + } catch (IllegalArgumentException ex2) { + log.warn("IAE " + o + " " + name + " " + value, ex2); + } catch (SecurityException ex1) { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: SecurityException for " + + o.getClass() + " " + name + "=" + value + ")", ex1); + } + } catch (IllegalAccessException iae) { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: IllegalAccessException for " + + o.getClass() + " " + name + "=" + value + ")", iae); + } + } catch (InvocationTargetException ie) { + Throwable cause = ie.getCause(); + if (cause instanceof ThreadDeath) { + throw (ThreadDeath) cause; + } + if (cause instanceof VirtualMachineError) { + throw (VirtualMachineError) cause; + } + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: InvocationTargetException for " + + o.getClass() + " " + name + "=" + value + ")", ie); + } + } + return false; + } + + public static String capitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + char[] chars = name.toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + return new String(chars); + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ClassLoaderUtil.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ClassLoaderUtil.java new file mode 100644 index 0000000..c654f7f --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ClassLoaderUtil.java @@ -0,0 +1,61 @@ +/* + * 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.tomcat.jdbc.pool; + + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class ClassLoaderUtil { + private static final Log log = LogFactory.getLog(ClassLoaderUtil.class); + + private static final boolean onlyAttemptFirstLoader = + Boolean.getBoolean("org.apache.tomcat.jdbc.pool.onlyAttemptCurrentClassLoader"); + + public static Class loadClass(String className, ClassLoader... classLoaders) throws ClassNotFoundException { + ClassNotFoundException last = null; + StringBuilder errorMsg = null; + for (ClassLoader cl : classLoaders) { + try { + if (cl!=null) { + if (log.isDebugEnabled()) { + log.debug("Attempting to load class["+className+"] from "+cl); + } + return Class.forName(className, true, cl); + } else { + throw new ClassNotFoundException("Classloader is null"); + } + } catch (ClassNotFoundException x) { + last = x; + if (errorMsg==null) { + errorMsg = new StringBuilder(); + } else { + errorMsg.append(';'); + } + errorMsg.append("ClassLoader:"); + errorMsg.append(cl); + } + if (onlyAttemptFirstLoader) { + break; + } + } + throw new ClassNotFoundException("Unable to load class: "+className+" from "+errorMsg, last); + } + + + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java new file mode 100644 index 0000000..c5714ba --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java @@ -0,0 +1,1632 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import javax.sql.XAConnection; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Implementation of simple connection pool. + * The ConnectionPool uses a {@link PoolProperties} object for storing all the meta information about the connection pool. + * As the underlying implementation, the connection pool uses {@link java.util.concurrent.BlockingQueue} to store active and idle connections. + * A custom implementation of a fair {@link FairBlockingQueue} blocking queue is provided with the connection pool itself. + */ +public class ConnectionPool { + + /** + * Default domain for objects registering with an mbean server + */ + public static final String POOL_JMX_DOMAIN = "tomcat.jdbc"; + /** + * Prefix type for JMX registration + */ + public static final String POOL_JMX_TYPE_PREFIX = POOL_JMX_DOMAIN+":type="; + + /** + * Logger + */ + private static final Log log = LogFactory.getLog(ConnectionPool.class); + + //=============================================================================== + // INSTANCE/QUICK ACCESS VARIABLE + //=============================================================================== + /** + * Carries the size of the pool, instead of relying on a queue implementation + * that usually iterates over to get an exact count + */ + private AtomicInteger size = new AtomicInteger(0); + + /** + * All the information about the connection pool + * These are the properties the pool got instantiated with + */ + private PoolConfiguration poolProperties; + + /** + * Contains all the connections that are in use + * TODO - this shouldn't be a blocking queue, simply a list to hold our objects + */ + private BlockingQueue busy; + + /** + * Contains all the idle connections + */ + private BlockingQueue idle; + + /** + * The thread that is responsible for checking abandoned and idle threads + */ + private volatile PoolCleaner poolCleaner; + + /** + * Pool closed flag + */ + private volatile boolean closed = false; + + /** + * Since newProxyInstance performs the same operation, over and over + * again, it is much more optimized if we simply store the constructor ourselves. + */ + private Constructor proxyClassConstructor; + + /** + * Executor service used to cancel Futures + */ + private ThreadPoolExecutor cancellator = new ThreadPoolExecutor(0,1,1000,TimeUnit.MILLISECONDS,new LinkedBlockingQueue()); + + /** + * reference to the JMX mbean + */ + protected org.apache.tomcat.jdbc.pool.jmx.ConnectionPool jmxPool = null; + + /** + * counter to track how many threads are waiting for a connection + */ + private AtomicInteger waitcount = new AtomicInteger(0); + + private AtomicLong poolVersion = new AtomicLong(Long.MIN_VALUE); + + /** + * The counters for statistics of the pool. + */ + private final AtomicLong borrowedCount = new AtomicLong(0); + private final AtomicLong returnedCount = new AtomicLong(0); + private final AtomicLong createdCount = new AtomicLong(0); + private final AtomicLong releasedCount = new AtomicLong(0); + private final AtomicLong reconnectedCount = new AtomicLong(0); + private final AtomicLong removeAbandonedCount = new AtomicLong(0); + private final AtomicLong releasedIdleCount = new AtomicLong(0); + + //=============================================================================== + // PUBLIC METHODS + //=============================================================================== + + /** + * Instantiate a connection pool. This will create connections if initialSize is larger than 0. + * The {@link PoolProperties} should not be reused for another connection pool. + * @param prop PoolProperties - all the properties for this connection pool + * @throws SQLException Pool initialization error + */ + public ConnectionPool(PoolConfiguration prop) throws SQLException { + //setup quick access variables and pools + init(prop); + } + + + /** + * Retrieves a Connection future. If a connection is not available, one can block using future.get() + * until a connection has become available. + * If a connection is not retrieved, the Future must be cancelled in order for the connection to be returned + * to the pool. + * @return a Future containing a reference to the connection or the future connection + * @throws SQLException Cannot use asynchronous connect + */ + public Future getConnectionAsync() throws SQLException { + try { + PooledConnection pc = borrowConnection(0, null, null); + if (pc!=null) { + return new ConnectionFuture(pc); + } + }catch (SQLException x) { + if (x.getMessage().indexOf("NoWait")<0) { + throw x; + } + } + //we can only retrieve a future if the underlying queue supports it. + if (idle instanceof FairBlockingQueue) { + Future pcf = ((FairBlockingQueue)idle).pollAsync(); + return new ConnectionFuture(pcf); + } else if (idle instanceof MultiLockFairBlockingQueue) { + Future pcf = ((MultiLockFairBlockingQueue)idle).pollAsync(); + return new ConnectionFuture(pcf); + } else { + throw new SQLException("Connection pool is misconfigured, doesn't support async retrieval. Set the 'fair' property to 'true'"); + } + } + + /** + * Borrows a connection from the pool. If a connection is available (in the idle queue) or the pool has not reached + * {@link PoolProperties#maxActive maxActive} connections a connection is returned immediately. + * If no connection is available, the pool will attempt to fetch a connection for {@link PoolProperties#maxWait maxWait} milliseconds. + * @return Connection - a java.sql.Connection/javax.sql.PooledConnection reflection proxy, wrapping the underlying object. + * @throws SQLException - if the wait times out or a failure occurs creating a connection + */ + public Connection getConnection() throws SQLException { + //check out a connection + PooledConnection con = borrowConnection(-1,null,null); + return setupConnection(con); + } + + + /** + * Borrows a connection from the pool. If a connection is available (in the + * idle queue) or the pool has not reached {@link PoolProperties#maxActive + * maxActive} connections a connection is returned immediately. If no + * connection is available, the pool will attempt to fetch a connection for + * {@link PoolProperties#maxWait maxWait} milliseconds. + * @param username The user name to use for the connection + * @param password The password for the connection + * @return Connection - a java.sql.Connection/javax.sql.PooledConnection + * reflection proxy, wrapping the underlying object. + * @throws SQLException + * - if the wait times out or a failure occurs creating a + * connection + */ + public Connection getConnection(String username, String password) throws SQLException { + // check out a connection + PooledConnection con = borrowConnection(-1, username, password); + return setupConnection(con); + } + + /** + * Returns the name of this pool + * @return String - the name of the pool + */ + public String getName() { + return getPoolProperties().getPoolName(); + } + + /** + * Return the number of threads waiting for a connection + * @return number of threads waiting for a connection + */ + public int getWaitCount() { + return waitcount.get(); + } + + /** + * Returns the pool properties associated with this connection pool + * @return PoolProperties + * + */ + public PoolConfiguration getPoolProperties() { + return this.poolProperties; + } + + /** + * Returns the total size of this pool, this includes both busy and idle connections + * @return int - number of established connections to the database + */ + public int getSize() { + return size.get(); + } + + /** + * Returns the number of connections that are in use + * @return int - number of established connections that are being used by the application + */ + public int getActive() { + return busy.size(); + } + + /** + * Returns the number of idle connections + * @return int - number of established connections not being used + */ + public int getIdle() { + return idle.size(); + } + + /** + * Returns true if {@link #close close} has been called, and the connection pool is unusable + * @return boolean + */ + public boolean isClosed() { + return this.closed; + } + + //=============================================================================== + // PROTECTED METHODS + //=============================================================================== + + + /** + * configures a pooled connection as a proxy. + * This Proxy implements {@link java.sql.Connection} and {@link javax.sql.PooledConnection} interfaces. + * All calls on {@link java.sql.Connection} methods will be propagated down to the actual JDBC connection except for the + * {@link java.sql.Connection#close()} method. + * @param con a {@link PooledConnection} to wrap in a Proxy + * @return a {@link java.sql.Connection} object wrapping a pooled connection. + * @throws SQLException if an interceptor can't be configured, if the proxy can't be instantiated + */ + protected Connection setupConnection(PooledConnection con) throws SQLException { + //fetch previously cached interceptor proxy - one per connection + JdbcInterceptor handler = con.getHandler(); + if (handler==null) { + if (jmxPool != null) { + con.createMBean(); + } + //build the proxy handler + handler = new ProxyConnection(this,con,getPoolProperties().isUseEquals()); + //set up the interceptor chain + PoolProperties.InterceptorDefinition[] proxies = getPoolProperties().getJdbcInterceptorsAsArray(); + for (int i=proxies.length-1; i>=0; i--) { + try { + //create a new instance + JdbcInterceptor interceptor = proxies[i].getInterceptorClass().getConstructor().newInstance(); + //configure properties + interceptor.setProperties(proxies[i].getProperties()); + //setup the chain + interceptor.setNext(handler); + //call reset + interceptor.reset(this, con); + //configure the last one to be held by the connection + handler = interceptor; + }catch(Exception x) { + SQLException sx = new SQLException("Unable to instantiate interceptor chain."); + sx.initCause(x); + throw sx; + } + } + //cache handler for the next iteration + con.setHandler(handler); + } else { + JdbcInterceptor next = handler; + //we have a cached handler, reset it + while (next!=null) { + next.reset(this, con); + next = next.getNext(); + } + } + // setup statement proxy + if (getPoolProperties().getUseStatementFacade()) { + handler = new StatementFacade(handler); + } + try { + getProxyConstructor(con.getXAConnection() != null); + //create the proxy + //TODO possible optimization, keep track if this connection was returned properly, and don't generate a new facade + Connection connection = null; + if (getPoolProperties().getUseDisposableConnectionFacade() ) { + connection = (Connection)proxyClassConstructor.newInstance(new Object[] { new DisposableConnectionFacade(handler) }); + } else { + connection = (Connection)proxyClassConstructor.newInstance(new Object[] {handler}); + } + //return the connection + return connection; + }catch (Exception x) { + SQLException s = new SQLException(); + s.initCause(x); + throw s; + } + + } + + /** + * Creates and caches a {@link java.lang.reflect.Constructor} used to instantiate the proxy object. + * We cache this, since the creation of a constructor is fairly slow. + * @param xa Use a XA connection + * @return constructor used to instantiate the wrapper object + * @throws NoSuchMethodException Failed to get a constructor + */ + /* + * Neither the class nor the constructor are exposed outside of jdbc-pool. + * Given the comments in the jdbc-pool code regarding caching for + * performance, continue to use Proxy.getProxyClass(). This will need to be + * revisited if that method is marked for removal. + */ + @SuppressWarnings("deprecation") + public Constructor getProxyConstructor(boolean xa) throws NoSuchMethodException { + //cache the constructor + if (proxyClassConstructor == null ) { + Class proxyClass = xa ? + Proxy.getProxyClass(ConnectionPool.class.getClassLoader(), + new Class[] {Connection.class, javax.sql.PooledConnection.class, XAConnection.class}) : + Proxy.getProxyClass(ConnectionPool.class.getClassLoader(), + new Class[] {Connection.class, javax.sql.PooledConnection.class}); + proxyClassConstructor = proxyClass.getConstructor(new Class[] { InvocationHandler.class }); + } + return proxyClassConstructor; + } + + /** + * Closes the pool and all disconnects all idle connections + * Active connections will be closed upon the {@link java.sql.Connection#close close} method is called + * on the underlying connection instead of being returned to the pool + * @param force - true to even close the active connections + */ + protected void close(boolean force) { + //are we already closed + if (this.closed) { + return; + } + //prevent other threads from entering + this.closed = true; + //stop background thread + if (poolCleaner!=null) { + poolCleaner.stopRunning(); + } + + /* release all idle connections */ + BlockingQueue pool = (!idle.isEmpty())?idle:(force?busy:idle); + while (!pool.isEmpty()) { + try { + //retrieve the next connection + PooledConnection con = pool.poll(1000, TimeUnit.MILLISECONDS); + //close it and retrieve the next one, if one is available + while (con != null) { + //close the connection + if (pool==idle) { + release(con); + } else { + abandon(con); + } + if (!pool.isEmpty()) { + con = pool.poll(1000, TimeUnit.MILLISECONDS); + } else { + break; + } + } //while + } catch (InterruptedException ex) { + if (getPoolProperties().getPropagateInterruptState()) { + Thread.currentThread().interrupt(); + } + } + if (pool.isEmpty() && force && pool!=busy) { + pool = busy; + } + } + if (this.getPoolProperties().isJmxEnabled()) { + this.jmxPool = null; + } + PoolProperties.InterceptorDefinition[] proxies = getPoolProperties().getJdbcInterceptorsAsArray(); + for (int i=0; i(); + //busy = new FairBlockingQueue(); + //make space for 10 extra in case we flow over a bit + if (properties.isFairQueue()) { + idle = new FairBlockingQueue<>(); + //idle = new MultiLockFairBlockingQueue(); + //idle = new LinkedTransferQueue(); + //idle = new ArrayBlockingQueue(properties.getMaxActive(),false); + } else { + idle = new LinkedBlockingQueue<>(); + } + + initializePoolCleaner(properties); + + //create JMX MBean + if (this.getPoolProperties().isJmxEnabled()) { + createMBean(); + } + + //Parse and create an initial set of interceptors. Letting them know the pool has started. + //These interceptors will not get any connection. + PoolProperties.InterceptorDefinition[] proxies = getPoolProperties().getJdbcInterceptorsAsArray(); + for (int i=0; iproperties.getMaxActive()) { + log.warn("minIdle is larger than maxActive, setting minIdle to: "+properties.getMaxActive()); + properties.setMinIdle(properties.getMaxActive()); + } + if (properties.getMaxIdle()>properties.getMaxActive()) { + log.warn("maxIdle is larger than maxActive, setting maxIdle to: "+properties.getMaxActive()); + properties.setMaxIdle(properties.getMaxActive()); + } + if (properties.getMaxIdle()0 && properties.isPoolSweeperEnabled() && + properties.getTimeBetweenEvictionRunsMillis()>properties.getMaxAge()) { + log.warn("timeBetweenEvictionRunsMillis is larger than maxAge, setting timeBetweenEvictionRunsMillis to: " + properties.getMaxAge()); + properties.setTimeBetweenEvictionRunsMillis((int)properties.getMaxAge()); + } + } + + public void initializePoolCleaner(PoolConfiguration properties) { + //if the evictor thread is supposed to run, start it now + if (properties.isPoolSweeperEnabled()) { + poolCleaner = new PoolCleaner(this, properties.getTimeBetweenEvictionRunsMillis()); + poolCleaner.start(); + } //end if + } + + public void terminatePoolCleaner() { + if (poolCleaner!= null) { + poolCleaner.stopRunning(); + poolCleaner = null; + } + } + + +//=============================================================================== +// CONNECTION POOLING IMPL LOGIC +//=============================================================================== + + /** + * thread safe way to abandon a connection + * signals a connection to be abandoned. + * this will disconnect the connection, and log the stack trace if logAbandoned=true + * @param con PooledConnection + */ + protected void abandon(PooledConnection con) { + if (con == null) { + return; + } + try { + con.lock(); + String trace = con.getStackTrace(); + if (getPoolProperties().isLogAbandoned()) { + log.warn("Connection has been abandoned " + con + ":" + trace); + } + if (jmxPool!=null) { + jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_ABANDON, trace); + } + //release the connection + removeAbandonedCount.incrementAndGet(); + release(con); + } finally { + con.unlock(); + } + } + + /** + * Thread safe way to suspect a connection. Similar to + * {@link #abandon(PooledConnection)}, but instead of actually abandoning + * the connection, this will log a warning and set the suspect flag on the + * {@link PooledConnection} if logAbandoned=true + * + * @param con PooledConnection + */ + protected void suspect(PooledConnection con) { + if (con == null) { + return; + } + if (con.isSuspect()) { + return; + } + try { + con.lock(); + String trace = con.getStackTrace(); + if (getPoolProperties().isLogAbandoned()) { + log.warn("Connection has been marked suspect, possibly abandoned " + con + "["+(System.currentTimeMillis()-con.getTimestamp())+" ms.]:" + trace); + } + if (jmxPool!=null) { + jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.SUSPECT_ABANDONED_NOTIFICATION, trace); + } + con.setSuspect(true); + } finally { + con.unlock(); + } + } + + /** + * thread safe way to release a connection + * @param con PooledConnection + */ + protected void release(PooledConnection con) { + if (con == null) { + return; + } + try { + con.lock(); + if (con.release()) { + //counter only decremented once + size.addAndGet(-1); + con.setHandler(null); + } + releasedCount.incrementAndGet(); + } finally { + con.unlock(); + } + // we've asynchronously reduced the number of connections + // we could have threads stuck in idle.poll(timeout) that will never be + // notified + if (waitcount.get() > 0) { + if (!idle.offer(create(true))) { + log.warn("Failed to add a new connection to the pool after releasing a connection " + + "when at least one thread was waiting for a connection."); + } + } + } + + /** + * Thread safe way to retrieve a connection from the pool + * @param wait - time to wait, overrides the maxWait from the properties, + * set to -1 if you wish to use maxWait, 0 if you wish no wait time. + * @param username The user name to use for the connection + * @param password The password for the connection + * @return a connection + * @throws SQLException Failed to get a connection + */ + private PooledConnection borrowConnection(int wait, String username, String password) throws SQLException { + + if (isClosed()) { + throw new SQLException("Connection pool closed."); + } //end if + + //get the current time stamp + long now = System.currentTimeMillis(); + //see if there is one available immediately + PooledConnection con = idle.poll(); + + while (true) { + if (con!=null) { + //configure the connection and return it + PooledConnection result = borrowConnection(now, con, username, password); + borrowedCount.incrementAndGet(); + if (result!=null) { + return result; + } + } + + //if we get here, see if we need to create one + //this is not 100% accurate since it doesn't use a shared + //atomic variable - a connection can become idle while we are creating + //a new connection + if (size.get() < getPoolProperties().getMaxActive()) { + //atomic duplicate check + if (size.addAndGet(1) > getPoolProperties().getMaxActive()) { + //if we got here, two threads passed through the first if + size.decrementAndGet(); + } else { + //create a connection, we're below the limit + return createConnection(now, con, username, password); + } + } //end if + + //calculate wait time for this iteration + long maxWait = wait; + //if the passed in wait time is -1, means we should use the pool property value + if (wait==-1) { + maxWait = (getPoolProperties().getMaxWait()<=0)?Long.MAX_VALUE:getPoolProperties().getMaxWait(); + } + + long timetowait = Math.max(0, maxWait - (System.currentTimeMillis() - now)); + waitcount.incrementAndGet(); + try { + //retrieve an existing connection + con = idle.poll(timetowait, TimeUnit.MILLISECONDS); + } catch (InterruptedException ex) { + if (getPoolProperties().getPropagateInterruptState()) { + Thread.currentThread().interrupt(); + } + SQLException sx = new SQLException("Pool wait interrupted."); + sx.initCause(ex); + throw sx; + } finally { + waitcount.decrementAndGet(); + } + if (maxWait==0 && con == null) { //no wait, return one if we have one + if (jmxPool!=null) { + jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.POOL_EMPTY, "Pool empty - no wait."); + } + throw new PoolExhaustedException("[" + Thread.currentThread().getName()+"] " + + "NoWait: Pool empty. Unable to fetch a connection, none available["+busy.size()+" in use]."); + } + //we didn't get a connection, lets see if we timed out + if (con == null) { + if ((System.currentTimeMillis() - now) >= maxWait) { + if (jmxPool!=null) { + jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.POOL_EMPTY, "Pool empty - timeout."); + } + throw new PoolExhaustedException("[" + Thread.currentThread().getName()+"] " + + "Timeout: Pool empty. Unable to fetch a connection in " + (maxWait / 1000) + + " seconds, none available[size:"+size.get() +"; busy:"+busy.size()+"; idle:"+idle.size()+"; lastwait:"+timetowait+"]."); + } else { + //no timeout, lets try again + continue; + } + } + } //while + } + + /** + * Creates a JDBC connection and tries to connect to the database. + * @param now timestamp of when this was called + * @param notUsed Argument not used + * @param username The user name to use for the connection + * @param password The password for the connection + * @return a PooledConnection that has been connected + * @throws SQLException Failed to get a connection + */ + protected PooledConnection createConnection(long now, PooledConnection notUsed, String username, String password) throws SQLException { + //no connections where available we'll create one + PooledConnection con = create(false); + if (username!=null) { + con.getAttributes().put(PooledConnection.PROP_USER, username); + } + if (password!=null) { + con.getAttributes().put(PooledConnection.PROP_PASSWORD, password); + } + boolean error = false; + try { + //connect and validate the connection + con.lock(); + con.connect(); + if (con.validate(PooledConnection.VALIDATE_INIT)) { + //no need to lock a new one, its not contented + con.setTimestamp(now); + if (getPoolProperties().isLogAbandoned()) { + con.setStackTrace(getThreadDump()); + } + if (!busy.offer(con)) { + log.debug("Connection doesn't fit into busy array, connection will not be traceable."); + } + createdCount.incrementAndGet(); + return con; + } else { + //validation failed, make sure we disconnect + //and clean up + throw new SQLException("Validation Query Failed, enable logValidationErrors for more details."); + } //end if + } catch (Exception e) { + error = true; + if (log.isDebugEnabled()) { + log.debug("Unable to create a new JDBC connection.", e); + } + if (e instanceof SQLException) { + throw (SQLException)e; + } else { + SQLException ex = new SQLException(e.getMessage()); + ex.initCause(e); + throw ex; + } + } finally { + // con can never be null here + if (error ) { + release(con); + } + con.unlock(); + }//catch + } + + /** + * Validates and configures a previously idle connection + * @param now - timestamp + * @param con - the connection to validate and configure + * @param username The user name to use for the connection + * @param password The password for the connection + * @return a connection + * @throws SQLException if a validation error happens + */ + protected PooledConnection borrowConnection(long now, PooledConnection con, String username, String password) throws SQLException { + //we have a connection, lets set it up + + //flag to see if we need to nullify + boolean setToNull = false; + try { + con.lock(); + if (con.isReleased()) { + return null; + } + + //evaluate username/password change as well as max age functionality + boolean forceReconnect = con.shouldForceReconnect(username, password) || con.isMaxAgeExpired(); + + if (!con.isDiscarded() && !con.isInitialized()) { + //here it states that the connection not discarded, but the connection is null + //don't attempt a connect here. It will be done during the reconnect. + forceReconnect = true; + } + + if (!forceReconnect) { + if ((!con.isDiscarded()) && con.validate(PooledConnection.VALIDATE_BORROW)) { + //set the timestamp + con.setTimestamp(now); + if (getPoolProperties().isLogAbandoned()) { + //set the stack trace for this pool + con.setStackTrace(getThreadDump()); + } + if (!busy.offer(con)) { + log.debug("Connection doesn't fit into busy array, connection will not be traceable."); + } + return con; + } + } + //if we reached here, that means the connection + //is either has another principal, is discarded or validation failed. + //we will make one more attempt + //in order to guarantee that the thread that just acquired + //the connection shouldn't have to poll again. + try { + con.reconnect(); + reconnectedCount.incrementAndGet(); + int validationMode = isInitNewConnections() ? + PooledConnection.VALIDATE_INIT: + PooledConnection.VALIDATE_BORROW; + if (con.validate(validationMode)) { + //set the timestamp + con.setTimestamp(now); + if (getPoolProperties().isLogAbandoned()) { + //set the stack trace for this pool + con.setStackTrace(getThreadDump()); + } + if (!busy.offer(con)) { + log.debug("Connection doesn't fit into busy array, connection will not be traceable."); + } + return con; + } else { + //validation failed. + throw new SQLException("Failed to validate a newly established connection."); + } + } catch (Exception x) { + release(con); + setToNull = true; + if (x instanceof SQLException) { + throw (SQLException)x; + } else { + SQLException ex = new SQLException(x.getMessage()); + ex.initCause(x); + throw ex; + } + } + } finally { + con.unlock(); + if (setToNull) { + con = null; + } + } + } + + /** + * Returns whether new connections should be initialized by invoking + * {@link PooledConnection#validate(int)} with {@link PooledConnection#VALIDATE_INIT}. + * + * @return true if pool is either configured to test connections on connect or a non-NULL init + * SQL has been configured + */ + private boolean isInitNewConnections() { + return getPoolProperties().isTestOnConnect() || getPoolProperties().getInitSQL()!=null; + } + + /** + * Terminate the current transaction for the given connection. + * @param con The connection + * @return true if the connection TX termination succeeded + * otherwise false + */ + protected boolean terminateTransaction(PooledConnection con) { + try { + if (Boolean.FALSE.equals(con.getPoolProperties().getDefaultAutoCommit())) { + if (this.getPoolProperties().getRollbackOnReturn()) { + boolean autocommit = con.getConnection().getAutoCommit(); + if (!autocommit) { + con.getConnection().rollback(); + } + } else if (this.getPoolProperties().getCommitOnReturn()) { + boolean autocommit = con.getConnection().getAutoCommit(); + if (!autocommit) { + con.getConnection().commit(); + } + } + } + return true; + } catch (SQLException x) { + log.warn("Unable to terminate transaction, connection will be closed.",x); + return false; + } + + } + + /** + * Determines if a connection should be closed upon return to the pool. + * @param con - the connection + * @param action - the validation action that should be performed + * @return true if the connection should be closed + */ + protected boolean shouldClose(PooledConnection con, int action) { + if (con.getConnectionVersion() < getPoolVersion()) { + return true; + } + if (con.isDiscarded()) { + return true; + } + if (isClosed()) { + return true; + } + if (!con.validate(action)) { + return true; + } + if (!terminateTransaction(con)) { + return true; + } + return false; + } + + /** + * Checks whether this connection has {@link PooledConnection#isMaxAgeExpired() expired} and tries to reconnect if it has. + * @param con PooledConnection + * @return true if the connection was either not expired or expired but reconnecting succeeded, + * false if reconnecting failed (either because a new connection could not be established or + * validating the newly created connection failed) + * @see PooledConnection#isMaxAgeExpired() + */ + protected boolean reconnectIfExpired(PooledConnection con) { + if (con.isMaxAgeExpired()) { + try { + if (log.isDebugEnabled()) { + log.debug( "Connection ["+this+"] expired because of maxAge, trying to reconnect" ); + } + con.reconnect(); + reconnectedCount.incrementAndGet(); + if ( isInitNewConnections() && !con.validate( PooledConnection.VALIDATE_INIT)) { + return false; + } + } catch(Exception e) { + log.error("Failed to re-connect connection ["+this+"] that expired because of maxAge",e); + return false; + } + } + return true; + } + + /** + * Returns a connection to the pool + * If the pool is closed, the connection will be released + * If the connection is not part of the busy queue, it will be released. + * If {@link PoolProperties#testOnReturn} is set to true it will be validated + * @param con PooledConnection to be returned to the pool + */ + protected void returnConnection(PooledConnection con) { + if (isClosed()) { + //if the connection pool is closed + //close the connection instead of returning it + release(con); + return; + } //end if + + if (con != null) { + try { + returnedCount.incrementAndGet(); + con.lock(); + if (con.isSuspect()) { + if (poolProperties.isLogAbandoned() && log.isInfoEnabled()) { + log.info("Connection(" + con + ") that has been marked suspect was returned." + + " The processing time is " + (System.currentTimeMillis()-con.getTimestamp()) + " ms."); + } + if (jmxPool!=null) { + jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.SUSPECT_RETURNED_NOTIFICATION, + "Connection(" + con + ") that has been marked suspect was returned."); + } + } + if (busy.remove(con)) { + + if (!shouldClose(con,PooledConnection.VALIDATE_RETURN) && reconnectIfExpired(con)) { + con.clearWarnings(); + con.setStackTrace(null); + con.setTimestamp(System.currentTimeMillis()); + if (((idle.size()>=poolProperties.getMaxIdle()) && !poolProperties.isPoolSweeperEnabled()) || (!idle.offer(con))) { + if (log.isDebugEnabled()) { + log.debug("Connection ["+con+"] will be closed and not returned to the pool, idle["+idle.size()+"]>=maxIdle["+poolProperties.getMaxIdle()+"] idle.offer failed."); + } + release(con); + } + } else { + if (log.isDebugEnabled()) { + log.debug("Connection ["+con+"] will be closed and not returned to the pool."); + } + release(con); + } //end if + } else { + if (log.isDebugEnabled()) { + log.debug("Connection ["+con+"] will be closed and not returned to the pool, busy.remove failed."); + } + release(con); + } + } finally { + con.unlock(); + } + } //end if + } //checkIn + + /** + * Determines if a connection should be abandoned based on + * {@link PoolProperties#abandonWhenPercentageFull} setting. + * @return true if the connection should be abandoned + */ + protected boolean shouldAbandon() { + if (!poolProperties.isRemoveAbandoned()) { + return false; + } + if (poolProperties.getAbandonWhenPercentageFull()==0) { + return true; + } + float used = busy.size(); + float max = poolProperties.getMaxActive(); + float perc = poolProperties.getAbandonWhenPercentageFull(); + return (used/max*100f)>=perc; + } + + /** + * Iterates through all the busy connections and checks for connections that have timed out + */ + public void checkAbandoned() { + try { + if (busy.isEmpty()) { + return; + } + Iterator locked = busy.iterator(); + int sto = getPoolProperties().getSuspectTimeout(); + while (locked.hasNext()) { + PooledConnection con = locked.next(); + boolean setToNull = false; + try { + con.lock(); + //the con has been returned to the pool or released + //ignore it + if (idle.contains(con) || con.isReleased()) { + continue; + } + long time = con.getTimestamp(); + long now = System.currentTimeMillis(); + if (shouldAbandon() && (now - time) > con.getAbandonTimeout()) { + locked.remove(); + abandon(con); + setToNull = true; + } else if (sto > 0 && (now - time) > (sto * 1000L)) { + suspect(con); + } else { + //do nothing + } //end if + } finally { + con.unlock(); + if (setToNull) { + con = null; + } + } + } //while + } catch (ConcurrentModificationException e) { + log.debug("checkAbandoned failed." ,e); + } catch (Exception e) { + log.warn("checkAbandoned failed, it will be retried.",e); + } + } + + /** + * Iterates through the idle connections and resizes the idle pool based on parameters + * {@link PoolProperties#maxIdle}, {@link PoolProperties#minIdle}, {@link PoolProperties#minEvictableIdleTimeMillis} + */ + public void checkIdle() { + checkIdle(false); + } + + public void checkIdle(boolean ignoreMinSize) { + + try { + if (idle.isEmpty()) { + return; + } + long now = System.currentTimeMillis(); + Iterator unlocked = idle.iterator(); + while ( (ignoreMinSize || (idle.size()>=getPoolProperties().getMinIdle())) && unlocked.hasNext()) { + PooledConnection con = unlocked.next(); + boolean setToNull = false; + try { + con.lock(); + //the con been taken out, we can't clean it up + if (busy.contains(con)) { + continue; + } + long time = con.getTimestamp(); + if (shouldReleaseIdle(now, con, time)) { + releasedIdleCount.incrementAndGet(); + release(con); + unlocked.remove(); + setToNull = true; + } else { + //do nothing + } //end if + } finally { + con.unlock(); + if (setToNull) { + con = null; + } + } + } //while + } catch (ConcurrentModificationException e) { + log.debug("checkIdle failed." ,e); + } catch (Exception e) { + log.warn("checkIdle failed, it will be retried.",e); + } + + } + + + protected boolean shouldReleaseIdle(long now, PooledConnection con, long time) { + if (con.getConnectionVersion() < getPoolVersion()) { + return true; + } else { + return (con.getReleaseTime()>0) && ((now - time) > con.getReleaseTime()) && (getSize()>getPoolProperties().getMinIdle()); + } + } + + /** + * Forces a validation of all idle connections if {@link PoolProperties#testWhileIdle} is set. + */ + public void testAllIdle() { + testAllIdle(false); + } + + /** + * Forces a validation of all idle connections if {@link PoolProperties#testWhileIdle} is set. + * @param checkMaxAgeOnly whether to only check {@link PooledConnection#isMaxAgeExpired()} but + * not invoke {@link PooledConnection#validate(int)} + */ + public void testAllIdle(boolean checkMaxAgeOnly) { + try { + if (idle.isEmpty()) { + return; + } + Iterator unlocked = idle.iterator(); + while (unlocked.hasNext()) { + PooledConnection con = unlocked.next(); + try { + con.lock(); + //the con been taken out, we can't clean it up + if (busy.contains(con)) { + continue; + } + + boolean release; + if (checkMaxAgeOnly) { + release = !reconnectIfExpired(con); + } else { + release = !reconnectIfExpired(con) || !con.validate(PooledConnection.VALIDATE_IDLE); + } + if (release) { + releasedIdleCount.incrementAndGet(); + unlocked.remove(); + release(con); + } + } finally { + con.unlock(); + } + } //while + } catch (ConcurrentModificationException e) { + log.debug("testAllIdle failed." ,e); + } catch (Exception e) { + log.warn("testAllIdle failed, it will be retried.",e); + } + + } + + /** + * Creates a stack trace representing the existing thread's current state. + * @return a string object representing the current state. + * TODO investigate if we simply should store {@link java.lang.Thread#getStackTrace()} elements + */ + protected static String getThreadDump() { + Exception x = new Exception(); + x.fillInStackTrace(); + return getStackTrace(x); + } + + /** + * Convert an exception into a String + * @param x - the throwable + * @return a string representing the stack trace + */ + public static String getStackTrace(Throwable x) { + if (x == null) { + return null; + } else { + java.io.ByteArrayOutputStream bout = new java.io.ByteArrayOutputStream(); + java.io.PrintStream writer = new java.io.PrintStream(bout); + x.printStackTrace(writer); + String result = bout.toString(); + return (x.getMessage()!=null && x.getMessage().length()>0)? x.getMessage()+";"+result:result; + } //end if + } + + + /** + * Create a new pooled connection object. Not connected nor validated. + * @param incrementCounter true to increment the connection count + * @return a pooled connection object + */ + protected PooledConnection create(boolean incrementCounter) { + if (incrementCounter) { + size.incrementAndGet(); + } + PooledConnection con = new PooledConnection(getPoolProperties(), this); + return con; + } + + /** + * Purges all connections in the pool. + * For connections currently in use, these connections will be + * purged when returned on the pool. This call also + * purges connections that are idle and in the pool + * To only purge used/active connections see {@link #purgeOnReturn()} + */ + public void purge() { + purgeOnReturn(); + checkIdle(true); + } + + /** + * Purges connections when they are returned from the pool. + * This call does not purge idle connections until they are used. + * To purge idle connections see {@link #purge()} + */ + public void purgeOnReturn() { + poolVersion.incrementAndGet(); + } + + /** + * Hook to perform final actions on a pooled connection object once it has been disconnected and will be discarded + * @param con The connection + */ + protected void finalize(PooledConnection con) { + JdbcInterceptor handler = con.getHandler(); + while (handler!=null) { + handler.reset(null, null); + handler=handler.getNext(); + } + } + + /** + * Hook to perform final actions on a pooled connection object once it has been disconnected and will be discarded + * @param con The connection + * @param finalizing true if finalizing the connection + */ + protected void disconnectEvent(PooledConnection con, boolean finalizing) { + JdbcInterceptor handler = con.getHandler(); + while (handler!=null) { + handler.disconnected(this, con, finalizing); + handler=handler.getNext(); + } + } + + /** + * Return the object that is potentially registered in JMX for notifications + * @return the object implementing the {@link org.apache.tomcat.jdbc.pool.jmx.ConnectionPoolMBean} interface + */ + public org.apache.tomcat.jdbc.pool.jmx.ConnectionPool getJmxPool() { + return jmxPool; + } + + /** + * Create MBean object that can be registered. + */ + protected void createMBean() { + try { + jmxPool = new org.apache.tomcat.jdbc.pool.jmx.ConnectionPool(this); + } catch (Exception x) { + log.warn("Unable to start JMX integration for connection pool. Instance["+getName()+"] can't be monitored.",x); + } + } + + /** + * The total number of connections borrowed from this pool. + * @return the borrowed connection count + */ + public long getBorrowedCount() { + return borrowedCount.get(); + } + + /** + * The total number of connections returned to this pool. + * @return the returned connection count + */ + public long getReturnedCount() { + return returnedCount.get(); + } + + /** + * The total number of connections created by this pool. + * @return the created connection count + */ + public long getCreatedCount() { + return createdCount.get(); + } + + /** + * The total number of connections released from this pool. + * @return the released connection count + */ + public long getReleasedCount() { + return releasedCount.get(); + } + + /** + * The total number of connections reconnected by this pool. + * @return the reconnected connection count + */ + public long getReconnectedCount() { + return reconnectedCount.get(); + } + + /** + * The total number of connections released by remove abandoned. + * @return the PoolCleaner removed abandoned connection count + */ + public long getRemoveAbandonedCount() { + return removeAbandonedCount.get(); + } + + /** + * The total number of connections released by eviction. + * @return the PoolCleaner evicted idle connection count + */ + public long getReleasedIdleCount() { + return releasedIdleCount.get(); + } + + /** + * reset the statistics of this pool. + */ + public void resetStats() { + borrowedCount.set(0); + returnedCount.set(0); + createdCount.set(0); + releasedCount.set(0); + reconnectedCount.set(0); + removeAbandonedCount.set(0); + releasedIdleCount.set(0); + } + + /** + * Thread safe wrapper around a future for the regular queue + * This one retrieves the pooled connection object + * and performs the initialization according to + * interceptors and validation rules. + * This class is thread safe and is cancellable + * + */ + protected class ConnectionFuture implements Future, Runnable { + Future pcFuture = null; + AtomicBoolean configured = new AtomicBoolean(false); + CountDownLatch latch = new CountDownLatch(1); + volatile Connection result = null; + SQLException cause = null; + AtomicBoolean cancelled = new AtomicBoolean(false); + volatile PooledConnection pc = null; + public ConnectionFuture(Future pcf) { + this.pcFuture = pcf; + } + + public ConnectionFuture(PooledConnection pc) throws SQLException { + this.pc = pc; + result = ConnectionPool.this.setupConnection(pc); + configured.set(true); + } + /** + * {@inheritDoc} + */ + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (pc!=null) { + return false; + } else if ((!cancelled.get()) && cancelled.compareAndSet(false, true)) { + //cancel by retrieving the connection and returning it to the pool + ConnectionPool.this.cancellator.execute(this); + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public Connection get() throws InterruptedException, ExecutionException { + try { + return get(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + }catch (TimeoutException x) { + throw new ExecutionException(x); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Connection get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + PooledConnection pc = this.pc!=null?this.pc:pcFuture.get(timeout,unit); + if (pc!=null) { + if (result!=null) { + return result; + } + if (configured.compareAndSet(false, true)) { + try { + pc = borrowConnection(System.currentTimeMillis(),pc, null, null); + if (pc != null) { + result = ConnectionPool.this.setupConnection(pc); + } + } catch (SQLException x) { + cause = x; + } finally { + latch.countDown(); + } + } else { + //if we reach here, another thread is configuring the actual connection + latch.await(timeout,unit); //this shouldn't block for long + } + if (result==null) { + throw new ExecutionException(cause); + } + return result; + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isCancelled() { + return pc==null && (pcFuture.isCancelled() || cancelled.get()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isDone() { + return pc!=null || pcFuture.isDone(); + } + + /** + * run method to be executed when cancelled by an executor + */ + @Override + public void run() { + try { + Connection con = get(); //complete this future + if (con != null) { + con.close(); //return to the pool + } + }catch (ExecutionException ex) { + //we can ignore this + }catch (Exception x) { + log.error("Unable to cancel ConnectionFuture.",x); + } + } + + } + + + + private static volatile Timer poolCleanTimer = null; + private static Set cleaners = new HashSet<>(); + + private static synchronized void registerCleaner(PoolCleaner cleaner) { + unregisterCleaner(cleaner); + cleaners.add(cleaner); + if (poolCleanTimer == null) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(ConnectionPool.class.getClassLoader()); + // Create the timer thread in a PrivilegedAction so that a + // reference to the web application class loader is not created + // via Thread.inheritedAccessControlContext + PrivilegedAction pa = new PrivilegedNewTimer(); + poolCleanTimer = AccessController.doPrivileged(pa); + } finally { + Thread.currentThread().setContextClassLoader(loader); + } + } + poolCleanTimer.schedule(cleaner, cleaner.sleepTime,cleaner.sleepTime); + } + + private static synchronized void unregisterCleaner(PoolCleaner cleaner) { + boolean removed = cleaners.remove(cleaner); + if (removed) { + cleaner.cancel(); + if (poolCleanTimer != null) { + poolCleanTimer.purge(); + if (cleaners.isEmpty()) { + poolCleanTimer.cancel(); + poolCleanTimer = null; + } + } + } + } + + private static class PrivilegedNewTimer implements PrivilegedAction { + @Override + public Timer run() { + return new Timer("Tomcat JDBC Pool Cleaner["+ System.identityHashCode(ConnectionPool.class.getClassLoader()) + ":"+ + System.currentTimeMillis() + "]", true); + } + } + + // Testing use only + public static synchronized Set getPoolCleaners() { + return Collections.unmodifiableSet(cleaners); + } + + public long getPoolVersion() { + return poolVersion.get(); + } + + // Testing use only + public static synchronized Timer getPoolTimer() { + return poolCleanTimer; + } + + protected static class PoolCleaner extends TimerTask { + protected WeakReference pool; + protected long sleepTime; + + PoolCleaner(ConnectionPool pool, long sleepTime) { + this.pool = new WeakReference<>(pool); + this.sleepTime = sleepTime; + if (sleepTime <= 0) { + log.warn("Database connection pool evicter thread interval is set to 0, defaulting to 30 seconds"); + this.sleepTime = 1000 * 30; + } else if (sleepTime < 1000) { + log.warn("Database connection pool evicter thread interval is set to lower than 1 second."); + } + } + + @Override + public void run() { + ConnectionPool pool = this.pool.get(); + if (pool == null) { + stopRunning(); + } else if (!pool.isClosed()) { + try { + if (pool.getPoolProperties().isRemoveAbandoned() + || pool.getPoolProperties().getSuspectTimeout() > 0) { + pool.checkAbandoned(); + } + if (pool.getPoolProperties().getMinIdle() < pool.idle + .size()) { + pool.checkIdle(); + } + if (pool.getPoolProperties().isTestWhileIdle()) { + pool.testAllIdle(false); + } else if (pool.getPoolProperties().getMaxAge() > 0) { + pool.testAllIdle(true); + } + } catch (Exception x) { + log.error("", x); + } + } + } + + public void start() { + registerCleaner(this); + } + + public void stopRunning() { + unregisterCleaner(this); + } + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSource.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSource.java new file mode 100644 index 0000000..a8d672a --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSource.java @@ -0,0 +1,148 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.util.Hashtable; + +import javax.management.MBeanRegistration; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.jmx.JmxUtil; + + +/** + * A DataSource that can be instantiated through IoC and implements the DataSource interface + * since the DataSourceProxy is used as a generic proxy. + * The DataSource simply wraps a {@link ConnectionPool} in order to provide a standard interface to the user. + */ +public class DataSource extends DataSourceProxy implements javax.sql.DataSource,MBeanRegistration, org.apache.tomcat.jdbc.pool.jmx.ConnectionPoolMBean, javax.sql.ConnectionPoolDataSource { + private static final Log log = LogFactory.getLog(DataSource.class); + + /** + * Constructor for reflection only. A default set of pool properties will be created. + */ + public DataSource() { + super(); + } + + /** + * Constructs a DataSource object wrapping a connection + * @param poolProperties The pool properties + */ + public DataSource(PoolConfiguration poolProperties) { + super(poolProperties); + } + + + + + +//=============================================================================== +// JMX Operations - Register the actual pool itself under the tomcat.jdbc domain +//=============================================================================== + protected volatile ObjectName oname = null; + + /** + * Unregisters the underlying connection pool mbean.
    + * {@inheritDoc} + */ + @Override + public void postDeregister() { + if (oname!=null) { + unregisterJmx(); + } + } + + /** + * no-op
    + * {@inheritDoc} + */ + @Override + public void postRegister(Boolean registrationDone) { + // NOOP + } + + + /** + * no-op
    + * {@inheritDoc} + */ + @Override + public void preDeregister() throws Exception { + // NOOP + } + + /** + * If the connection pool MBean exists, it will be registered during this operation.
    + * {@inheritDoc} + */ + @Override + public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { + try { + if ( isJmxEnabled() ) { + this.oname = createObjectName(name); + if (oname!=null) { + registerJmx(); + } + } + }catch (MalformedObjectNameException x) { + log.error("Unable to create object name for JDBC pool.",x); + } + return name; + } + + /** + * Creates the ObjectName for the ConnectionPoolMBean object to be registered + * @param original the ObjectName for the DataSource + * @return the ObjectName for the ConnectionPoolMBean + * @throws MalformedObjectNameException Invalid object name + */ + public ObjectName createObjectName(ObjectName original) throws MalformedObjectNameException { + String domain = ConnectionPool.POOL_JMX_DOMAIN; + Hashtable properties = original.getKeyPropertyList(); + String origDomain = original.getDomain(); + properties.put("type", "ConnectionPool"); + properties.put("class", this.getClass().getName()); + if (original.getKeyProperty("path")!=null || properties.get("context")!=null) { + //this ensures that if the registration came from tomcat, we're not losing + //the unique domain, but putting that into as an engine attribute + properties.put("engine", origDomain); + } + ObjectName name = new ObjectName(domain,properties); + return name; + } + + /** + * Registers the ConnectionPoolMBean under a unique name based on the ObjectName for the DataSource + */ + protected void registerJmx() { + if (pool.getJmxPool()!=null) { + JmxUtil.registerJmx(oname, null, pool.getJmxPool()); + } + } + + /** + * + */ + protected void unregisterJmx() { + JmxUtil.unregisterJmx(oname); + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceFactory.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceFactory.java new file mode 100644 index 0000000..ee20304 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceFactory.java @@ -0,0 +1,598 @@ +/* + * 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.tomcat.jdbc.pool; + + +import java.sql.Connection; +import java.util.Hashtable; +import java.util.Properties; + +import javax.management.ObjectName; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; +import javax.sql.DataSource; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + *

    JNDI object factory that creates an instance of + * BasicDataSource that has been configured based on the + * RefAddr values of the specified Reference, + * which must match the names and data types of the + * BasicDataSource bean properties.

    + *
    + * Properties available for configuration:
    + * Commons DBCP properties
    + *
      + *
    1. initSQL - A query that gets executed once, right after the connection is established.
    2. + *
    3. testOnConnect - run validationQuery after connection has been established.
    4. + *
    5. validationInterval - avoid excess validation, only run validation at most at this frequency - time in milliseconds.
    6. + *
    7. jdbcInterceptors - a semicolon separated list of classnames extending {@link JdbcInterceptor} class.
    8. + *
    9. jmxEnabled - true of false, whether to register the pool with JMX.
    10. + *
    11. fairQueue - true of false, whether the pool should sacrifice a little bit of performance for true fairness.
    12. + *
    + * @author Craig R. McClanahan + * @author Dirk Verbeeck + */ +public class DataSourceFactory implements ObjectFactory { + private static final Log log = LogFactory.getLog(DataSourceFactory.class); + + protected static final String PROP_DEFAULTAUTOCOMMIT = "defaultAutoCommit"; + protected static final String PROP_DEFAULTREADONLY = "defaultReadOnly"; + protected static final String PROP_DEFAULTTRANSACTIONISOLATION = "defaultTransactionIsolation"; + protected static final String PROP_DEFAULTCATALOG = "defaultCatalog"; + + protected static final String PROP_DRIVERCLASSNAME = "driverClassName"; + protected static final String PROP_PASSWORD = "password"; + protected static final String PROP_URL = "url"; + protected static final String PROP_USERNAME = "username"; + + protected static final String PROP_MAXACTIVE = "maxActive"; + protected static final String PROP_MAXIDLE = "maxIdle"; + protected static final String PROP_MINIDLE = "minIdle"; + protected static final String PROP_INITIALSIZE = "initialSize"; + protected static final String PROP_MAXWAIT = "maxWait"; + protected static final String PROP_MAXAGE = "maxAge"; + + protected static final String PROP_TESTONBORROW = "testOnBorrow"; + protected static final String PROP_TESTONRETURN = "testOnReturn"; + protected static final String PROP_TESTWHILEIDLE = "testWhileIdle"; + protected static final String PROP_TESTONCONNECT = "testOnConnect"; + protected static final String PROP_VALIDATIONQUERY = "validationQuery"; + protected static final String PROP_VALIDATIONQUERY_TIMEOUT = "validationQueryTimeout"; + protected static final String PROP_VALIDATOR_CLASS_NAME = "validatorClassName"; + + protected static final String PROP_NUMTESTSPEREVICTIONRUN = "numTestsPerEvictionRun"; + protected static final String PROP_TIMEBETWEENEVICTIONRUNSMILLIS = "timeBetweenEvictionRunsMillis"; + protected static final String PROP_MINEVICTABLEIDLETIMEMILLIS = "minEvictableIdleTimeMillis"; + + protected static final String PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED = "accessToUnderlyingConnectionAllowed"; + + protected static final String PROP_REMOVEABANDONED = "removeAbandoned"; + protected static final String PROP_REMOVEABANDONEDTIMEOUT = "removeAbandonedTimeout"; + protected static final String PROP_LOGABANDONED = "logAbandoned"; + protected static final String PROP_ABANDONWHENPERCENTAGEFULL = "abandonWhenPercentageFull"; + + protected static final String PROP_POOLPREPAREDSTATEMENTS = "poolPreparedStatements"; + protected static final String PROP_MAXOPENPREPAREDSTATEMENTS = "maxOpenPreparedStatements"; + protected static final String PROP_CONNECTIONPROPERTIES = "connectionProperties"; + + protected static final String PROP_INITSQL = "initSQL"; + protected static final String PROP_INTERCEPTORS = "jdbcInterceptors"; + protected static final String PROP_VALIDATIONINTERVAL = "validationInterval"; + protected static final String PROP_JMX_ENABLED = "jmxEnabled"; + protected static final String PROP_FAIR_QUEUE = "fairQueue"; + + protected static final String PROP_USE_EQUALS = "useEquals"; + protected static final String PROP_USE_CON_LOCK = "useLock"; + + protected static final String PROP_DATASOURCE= "dataSource"; + protected static final String PROP_DATASOURCE_JNDI = "dataSourceJNDI"; + + protected static final String PROP_SUSPECT_TIMEOUT = "suspectTimeout"; + + protected static final String PROP_ALTERNATE_USERNAME_ALLOWED = "alternateUsernameAllowed"; + + protected static final String PROP_COMMITONRETURN = "commitOnReturn"; + protected static final String PROP_ROLLBACKONRETURN = "rollbackOnReturn"; + + protected static final String PROP_USEDISPOSABLECONNECTIONFACADE = "useDisposableConnectionFacade"; + + protected static final String PROP_LOGVALIDATIONERRORS = "logValidationErrors"; + + protected static final String PROP_PROPAGATEINTERRUPTSTATE = "propagateInterruptState"; + + protected static final String PROP_IGNOREEXCEPTIONONPRELOAD = "ignoreExceptionOnPreLoad"; + + protected static final String PROP_USESTATEMENTFACADE = "useStatementFacade"; + + public static final int UNKNOWN_TRANSACTIONISOLATION = -1; + + public static final String OBJECT_NAME = "object_name"; + + + protected static final String[] ALL_PROPERTIES = { + PROP_DEFAULTAUTOCOMMIT, + PROP_DEFAULTREADONLY, + PROP_DEFAULTTRANSACTIONISOLATION, + PROP_DEFAULTCATALOG, + PROP_DRIVERCLASSNAME, + PROP_MAXACTIVE, + PROP_MAXIDLE, + PROP_MINIDLE, + PROP_INITIALSIZE, + PROP_MAXWAIT, + PROP_TESTONBORROW, + PROP_TESTONRETURN, + PROP_TIMEBETWEENEVICTIONRUNSMILLIS, + PROP_NUMTESTSPEREVICTIONRUN, + PROP_MINEVICTABLEIDLETIMEMILLIS, + PROP_TESTWHILEIDLE, + PROP_TESTONCONNECT, + PROP_PASSWORD, + PROP_URL, + PROP_USERNAME, + PROP_VALIDATIONQUERY, + PROP_VALIDATIONQUERY_TIMEOUT, + PROP_VALIDATOR_CLASS_NAME, + PROP_VALIDATIONINTERVAL, + PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED, + PROP_REMOVEABANDONED, + PROP_REMOVEABANDONEDTIMEOUT, + PROP_LOGABANDONED, + PROP_POOLPREPAREDSTATEMENTS, + PROP_MAXOPENPREPAREDSTATEMENTS, + PROP_CONNECTIONPROPERTIES, + PROP_INITSQL, + PROP_INTERCEPTORS, + PROP_JMX_ENABLED, + PROP_FAIR_QUEUE, + PROP_USE_EQUALS, + OBJECT_NAME, + PROP_ABANDONWHENPERCENTAGEFULL, + PROP_MAXAGE, + PROP_USE_CON_LOCK, + PROP_DATASOURCE, + PROP_DATASOURCE_JNDI, + PROP_SUSPECT_TIMEOUT, + PROP_ALTERNATE_USERNAME_ALLOWED, + PROP_COMMITONRETURN, + PROP_ROLLBACKONRETURN, + PROP_USEDISPOSABLECONNECTIONFACADE, + PROP_LOGVALIDATIONERRORS, + PROP_PROPAGATEINTERRUPTSTATE, + PROP_IGNOREEXCEPTIONONPRELOAD, + PROP_USESTATEMENTFACADE + }; + + // -------------------------------------------------- ObjectFactory Methods + + /** + *

    Create and return a new BasicDataSource instance. If no + * instance can be created, return null instead.

    + * + * @param obj The possibly null object containing location or + * reference information that can be used in creating an object + * @param name The name of this object relative to nameCtx + * @param nameCtx The context relative to which the name + * parameter is specified, or null if name + * is relative to the default initial context + * @param environment The possibly null environment that is used in + * creating this object + * + * @exception Exception if an exception occurs creating the instance + */ + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) throws Exception { + + // We only know how to deal with javax.naming.References + // that specify a class name of "javax.sql.DataSource" + if ((obj == null) || !(obj instanceof Reference)) { + return null; + } + Reference ref = (Reference) obj; + boolean XA = false; + boolean ok = false; + if ("javax.sql.DataSource".equals(ref.getClassName())) { + ok = true; + } + if ("javax.sql.XADataSource".equals(ref.getClassName())) { + ok = true; + XA = true; + } + if (org.apache.tomcat.jdbc.pool.DataSource.class.getName().equals(ref.getClassName())) { + ok = true; + } + + if (!ok) { + log.warn(ref.getClassName()+" is not a valid class name/type for this JNDI factory."); + return null; + } + + + Properties properties = new Properties(); + for (int i = 0; i < ALL_PROPERTIES.length; i++) { + String propertyName = ALL_PROPERTIES[i]; + RefAddr ra = ref.get(propertyName); + if (ra != null) { + String propertyValue = ra.getContent().toString(); + properties.setProperty(propertyName, propertyValue); + } + } + + return createDataSource(properties,nameCtx,XA); + } + + public static PoolConfiguration parsePoolProperties(Properties properties) { + PoolConfiguration poolProperties = new PoolProperties(); + String value = null; + + value = properties.getProperty(PROP_DEFAULTAUTOCOMMIT); + if (value != null) { + poolProperties.setDefaultAutoCommit(Boolean.valueOf(value)); + } + + value = properties.getProperty(PROP_DEFAULTREADONLY); + if (value != null) { + poolProperties.setDefaultReadOnly(Boolean.valueOf(value)); + } + + value = properties.getProperty(PROP_DEFAULTTRANSACTIONISOLATION); + if (value != null) { + int level = UNKNOWN_TRANSACTIONISOLATION; + if ("NONE".equalsIgnoreCase(value)) { + level = Connection.TRANSACTION_NONE; + } else if ("READ_COMMITTED".equalsIgnoreCase(value)) { + level = Connection.TRANSACTION_READ_COMMITTED; + } else if ("READ_UNCOMMITTED".equalsIgnoreCase(value)) { + level = Connection.TRANSACTION_READ_UNCOMMITTED; + } else if ("REPEATABLE_READ".equalsIgnoreCase(value)) { + level = Connection.TRANSACTION_REPEATABLE_READ; + } else if ("SERIALIZABLE".equalsIgnoreCase(value)) { + level = Connection.TRANSACTION_SERIALIZABLE; + } else { + try { + level = Integer.parseInt(value); + } catch (NumberFormatException e) { + System.err.println("Could not parse defaultTransactionIsolation: " + value); + System.err.println("WARNING: defaultTransactionIsolation not set"); + System.err.println("using default value of database driver"); + level = UNKNOWN_TRANSACTIONISOLATION; + } + } + poolProperties.setDefaultTransactionIsolation(level); + } + + value = properties.getProperty(PROP_DEFAULTCATALOG); + if (value != null) { + poolProperties.setDefaultCatalog(value); + } + + value = properties.getProperty(PROP_DRIVERCLASSNAME); + if (value != null) { + poolProperties.setDriverClassName(value); + } + + value = properties.getProperty(PROP_MAXACTIVE); + if (value != null) { + poolProperties.setMaxActive(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_MAXIDLE); + if (value != null) { + poolProperties.setMaxIdle(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_MINIDLE); + if (value != null) { + poolProperties.setMinIdle(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_INITIALSIZE); + if (value != null) { + poolProperties.setInitialSize(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_MAXWAIT); + if (value != null) { + poolProperties.setMaxWait(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_TESTONBORROW); + if (value != null) { + poolProperties.setTestOnBorrow(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_TESTONRETURN); + if (value != null) { + poolProperties.setTestOnReturn(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_TESTONCONNECT); + if (value != null) { + poolProperties.setTestOnConnect(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_TIMEBETWEENEVICTIONRUNSMILLIS); + if (value != null) { + poolProperties.setTimeBetweenEvictionRunsMillis(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_NUMTESTSPEREVICTIONRUN); + if (value != null) { + poolProperties.setNumTestsPerEvictionRun(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_MINEVICTABLEIDLETIMEMILLIS); + if (value != null) { + poolProperties.setMinEvictableIdleTimeMillis(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_TESTWHILEIDLE); + if (value != null) { + poolProperties.setTestWhileIdle(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_PASSWORD); + if (value != null) { + poolProperties.setPassword(value); + } + + value = properties.getProperty(PROP_URL); + if (value != null) { + poolProperties.setUrl(value); + } + + value = properties.getProperty(PROP_USERNAME); + if (value != null) { + poolProperties.setUsername(value); + } + + value = properties.getProperty(PROP_VALIDATIONQUERY); + if (value != null) { + poolProperties.setValidationQuery(value); + } + + value = properties.getProperty(PROP_VALIDATIONQUERY_TIMEOUT); + if (value != null) { + poolProperties.setValidationQueryTimeout(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_VALIDATOR_CLASS_NAME); + if (value != null) { + poolProperties.setValidatorClassName(value); + } + + value = properties.getProperty(PROP_VALIDATIONINTERVAL); + if (value != null) { + poolProperties.setValidationInterval(Long.parseLong(value)); + } + + value = properties.getProperty(PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED); + if (value != null) { + poolProperties.setAccessToUnderlyingConnectionAllowed(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_REMOVEABANDONED); + if (value != null) { + poolProperties.setRemoveAbandoned(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_REMOVEABANDONEDTIMEOUT); + if (value != null) { + poolProperties.setRemoveAbandonedTimeout(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_LOGABANDONED); + if (value != null) { + poolProperties.setLogAbandoned(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_POOLPREPAREDSTATEMENTS); + if (value != null) { + log.warn(PROP_POOLPREPAREDSTATEMENTS + " is not a valid setting, it will have no effect."); + } + + value = properties.getProperty(PROP_MAXOPENPREPAREDSTATEMENTS); + if (value != null) { + log.warn(PROP_MAXOPENPREPAREDSTATEMENTS + " is not a valid setting, it will have no effect."); + } + + value = properties.getProperty(PROP_CONNECTIONPROPERTIES); + if (value != null) { + Properties p = getProperties(value); + poolProperties.setDbProperties(p); + } else { + poolProperties.setDbProperties(new Properties()); + } + + if (poolProperties.getUsername()!=null) { + poolProperties.getDbProperties().setProperty("user",poolProperties.getUsername()); + } + if (poolProperties.getPassword()!=null) { + poolProperties.getDbProperties().setProperty("password",poolProperties.getPassword()); + } + + value = properties.getProperty(PROP_INITSQL); + if (value != null) { + poolProperties.setInitSQL(value); + } + + value = properties.getProperty(PROP_INTERCEPTORS); + if (value != null) { + poolProperties.setJdbcInterceptors(value); + } + + value = properties.getProperty(PROP_JMX_ENABLED); + if (value != null) { + poolProperties.setJmxEnabled(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_FAIR_QUEUE); + if (value != null) { + poolProperties.setFairQueue(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_USE_EQUALS); + if (value != null) { + poolProperties.setUseEquals(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(OBJECT_NAME); + if (value != null) { + poolProperties.setName(ObjectName.quote(value)); + } + + value = properties.getProperty(PROP_ABANDONWHENPERCENTAGEFULL); + if (value != null) { + poolProperties.setAbandonWhenPercentageFull(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_MAXAGE); + if (value != null) { + poolProperties.setMaxAge(Long.parseLong(value)); + } + + value = properties.getProperty(PROP_USE_CON_LOCK); + if (value != null) { + poolProperties.setUseLock(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_DATASOURCE); + if (value != null) { + //this should never happen + throw new IllegalArgumentException("Can't set dataSource property as a string, this must be a javax.sql.DataSource object."); + + } + + value = properties.getProperty(PROP_DATASOURCE_JNDI); + if (value != null) { + poolProperties.setDataSourceJNDI(value); + } + + value = properties.getProperty(PROP_SUSPECT_TIMEOUT); + if (value != null) { + poolProperties.setSuspectTimeout(Integer.parseInt(value)); + } + + value = properties.getProperty(PROP_ALTERNATE_USERNAME_ALLOWED); + if (value != null) { + poolProperties.setAlternateUsernameAllowed(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_COMMITONRETURN); + if (value != null) { + poolProperties.setCommitOnReturn(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_ROLLBACKONRETURN); + if (value != null) { + poolProperties.setRollbackOnReturn(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_USEDISPOSABLECONNECTIONFACADE); + if (value != null) { + poolProperties.setUseDisposableConnectionFacade(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_LOGVALIDATIONERRORS); + if (value != null) { + poolProperties.setLogValidationErrors(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_PROPAGATEINTERRUPTSTATE); + if (value != null) { + poolProperties.setPropagateInterruptState(Boolean.parseBoolean(value)); + } + + value = properties.getProperty(PROP_IGNOREEXCEPTIONONPRELOAD); + if (value != null) { + poolProperties.setIgnoreExceptionOnPreLoad(Boolean.parseBoolean(value)); + } + value = properties.getProperty(PROP_USESTATEMENTFACADE); + if (value != null) { + poolProperties.setUseStatementFacade(Boolean.parseBoolean(value)); + } + return poolProperties; + } + + /** + * Creates and configures a {@link DataSource} instance based on the + * given properties. + * + * @param properties the datasource configuration properties + * @return the datasource + * @throws Exception if an error occurs creating the data source + */ + public DataSource createDataSource(Properties properties) throws Exception { + return createDataSource(properties,null,false); + } + public DataSource createDataSource(Properties properties,Context context, boolean XA) throws Exception { + PoolConfiguration poolProperties = parsePoolProperties(properties); + if (poolProperties.getDataSourceJNDI()!=null && poolProperties.getDataSource()==null) { + performJNDILookup(context, poolProperties); + } + org.apache.tomcat.jdbc.pool.DataSource dataSource = XA ? + new XADataSource(poolProperties) : + new org.apache.tomcat.jdbc.pool.DataSource(poolProperties); + //initialise the pool itself + dataSource.createPool(); + // Return the configured DataSource instance + return dataSource; + } + + public void performJNDILookup(Context context, PoolConfiguration poolProperties) { + Object jndiDS = null; + try { + if (context!=null) { + jndiDS = context.lookup(poolProperties.getDataSourceJNDI()); + } else { + log.warn("dataSourceJNDI property is configured, but local JNDI context is null."); + } + } catch (NamingException e) { + log.debug("The name \""+poolProperties.getDataSourceJNDI()+"\" cannot be found in the local context."); + } + if (jndiDS==null) { + try { + context = new InitialContext(); + jndiDS = context.lookup(poolProperties.getDataSourceJNDI()); + } catch (NamingException e) { + log.warn("The name \""+poolProperties.getDataSourceJNDI()+"\" cannot be found in the InitialContext."); + } + } + if (jndiDS!=null) { + poolProperties.setDataSource(jndiDS); + } + } + + /** + * Parse properties from the string. Format of the string must be [propertyName=property;]*. + * @param propText The properties string + * @return the properties + */ + protected static Properties getProperties(String propText) { + return PoolProperties.getProperties(propText,null); + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceProxy.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceProxy.java new file mode 100644 index 0000000..1981c74 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceProxy.java @@ -0,0 +1,1178 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Iterator; +import java.util.Properties; +import java.util.concurrent.Future; +import java.util.logging.Logger; + +import javax.sql.XAConnection; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorDefinition; + +/** + * The DataSource proxy lets us implements methods that don't exist in the current + * compiler JDK but might be methods that are part of a future JDK DataSource interface. + *
    + * It's a trick to work around compiler issues when implementing interfaces. For example, + * I could put in Java 6 methods of javax.sql.DataSource here, and compile it with JDK 1.5 + * and still be able to run under Java 6 without getting NoSuchMethodException. + */ + +public class DataSourceProxy implements PoolConfiguration { + private static final Log log = LogFactory.getLog(DataSourceProxy.class); + + protected volatile ConnectionPool pool = null; + + protected volatile PoolConfiguration poolProperties = null; + + public DataSourceProxy() { + this(new PoolProperties()); + } + + public DataSourceProxy(PoolConfiguration poolProperties) { + if (poolProperties == null) { + throw new NullPointerException("PoolConfiguration cannot be null."); + } + this.poolProperties = poolProperties; + } + + + @SuppressWarnings("unused") // Has to match signature in DataSource + public boolean isWrapperFor(Class iface) throws SQLException { + // we are not a wrapper of anything + return false; + } + + + @SuppressWarnings("unused") // Has to match signature in DataSource + public T unwrap(Class iface) throws SQLException { + //we can't unwrap anything + return null; + } + + /** + * Get a database connection. + * {@link javax.sql.DataSource#getConnection()} + * @param username The user name + * @param password The password + * @return the connection + * @throws SQLException Connection error + */ + public Connection getConnection(String username, String password) throws SQLException { + if (this.getPoolProperties().isAlternateUsernameAllowed()) { + if (pool == null) { + return createPool().getConnection(username,password); + } + return pool.getConnection(username,password); + } else { + return getConnection(); + } + } + + public PoolConfiguration getPoolProperties() { + return poolProperties; + } + + /** + * Sets up the connection pool, by creating a pooling driver. + * @return the connection pool + * @throws SQLException Error creating pool + */ + public ConnectionPool createPool() throws SQLException { + if (pool != null) { + return pool; + } else { + return pCreatePool(); + } + } + + /** + * Sets up the connection pool, by creating a pooling driver. + */ + private synchronized ConnectionPool pCreatePool() throws SQLException { + if (pool != null) { + return pool; + } else { + pool = new ConnectionPool(poolProperties); + return pool; + } + } + + /** + * Get a database connection. + * {@link javax.sql.DataSource#getConnection()} + * @return the connection + * @throws SQLException Connection error + */ + public Connection getConnection() throws SQLException { + if (pool == null) { + return createPool().getConnection(); + } + return pool.getConnection(); + } + + /** + * Invokes an sync operation to retrieve the connection. + * @return a Future containing a reference to the connection when it becomes available + * @throws SQLException Connection error + */ + public Future getConnectionAsync() throws SQLException { + if (pool == null) { + return createPool().getConnectionAsync(); + } + return pool.getConnectionAsync(); + } + + /** + * Get a database connection. + * {@link javax.sql.XADataSource#getXAConnection()} + * @return the connection + * @throws SQLException Connection error + */ + public XAConnection getXAConnection() throws SQLException { + Connection con = getConnection(); + if (con instanceof XAConnection) { + return (XAConnection)con; + } else { + try { + con.close(); + } catch (Exception ignore) { + // Ignore + } + throw new SQLException("Connection from pool does not implement javax.sql.XAConnection"); + } + } + + /** + * Get a database connection. + * {@link javax.sql.XADataSource#getXAConnection(String, String)} + * @param username The user name + * @param password The password + * @return the connection + * @throws SQLException Connection error + */ + public XAConnection getXAConnection(String username, String password) throws SQLException { + Connection con = getConnection(username, password); + if (con instanceof XAConnection) { + return (XAConnection)con; + } else { + try { + con.close(); + } catch (Exception ignore) { + // Ignore + } + throw new SQLException("Connection from pool does not implement javax.sql.XAConnection"); + } + } + + + /** + * Get a database connection. + * {@link javax.sql.DataSource#getConnection()} + * @return the connection + * @throws SQLException Connection error + */ + public javax.sql.PooledConnection getPooledConnection() throws SQLException { + return (javax.sql.PooledConnection) getConnection(); + } + + /** + * Get a database connection. + * {@link javax.sql.DataSource#getConnection()} + * @param username unused + * @param password unused + * @return the connection + * @throws SQLException Connection error + */ + public javax.sql.PooledConnection getPooledConnection(String username, + String password) throws SQLException { + return (javax.sql.PooledConnection) getConnection(); + } + + public ConnectionPool getPool() { + try { + return createPool(); + }catch (SQLException x) { + log.error("Error during connection pool creation.", x); + return null; + } + } + + + public void close() { + close(false); + } + public void close(boolean all) { + try { + if (pool != null) { + final ConnectionPool p = pool; + pool = null; + if (p!=null) { + p.close(all); + } + } + }catch (Exception x) { + log.warn("Error during connection pool closure.", x); + } + } + + public int getPoolSize() { + final ConnectionPool p = pool; + if (p == null) { + return 0; + } else { + return p.getSize(); + } + } + + + @Override + public String toString() { + return super.toString()+"{"+getPoolProperties()+"}"; + } + + +/*-----------------------------------------------------------------------*/ +// PROPERTIES WHEN NOT USED WITH FACTORY +/*------------------------------------------------------------------------*/ + + + @Override + public String getPoolName() { + return pool.getName(); + } + + + public void setPoolProperties(PoolConfiguration poolProperties) { + this.poolProperties = poolProperties; + } + + + @Override + public void setDriverClassName(String driverClassName) { + this.poolProperties.setDriverClassName(driverClassName); + } + + + @Override + public void setInitialSize(int initialSize) { + this.poolProperties.setInitialSize(initialSize); + } + + + @Override + public void setInitSQL(String initSQL) { + this.poolProperties.setInitSQL(initSQL); + } + + + @Override + public void setLogAbandoned(boolean logAbandoned) { + this.poolProperties.setLogAbandoned(logAbandoned); + } + + + @Override + public void setMaxActive(int maxActive) { + this.poolProperties.setMaxActive(maxActive); + } + + + @Override + public void setMaxIdle(int maxIdle) { + this.poolProperties.setMaxIdle(maxIdle); + } + + + @Override + public void setMaxWait(int maxWait) { + this.poolProperties.setMaxWait(maxWait); + } + + + @Override + public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) { + this.poolProperties.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + } + + + @Override + public void setMinIdle(int minIdle) { + this.poolProperties.setMinIdle(minIdle); + } + + + @Override + public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) { + this.poolProperties.setNumTestsPerEvictionRun(numTestsPerEvictionRun); + } + + + @Override + public void setPassword(String password) { + this.poolProperties.setPassword(password); + this.poolProperties.getDbProperties().setProperty("password",this.poolProperties.getPassword()); + } + + + @Override + public void setRemoveAbandoned(boolean removeAbandoned) { + this.poolProperties.setRemoveAbandoned(removeAbandoned); + } + + + @Override + public void setRemoveAbandonedTimeout(int removeAbandonedTimeout) { + this.poolProperties.setRemoveAbandonedTimeout(removeAbandonedTimeout); + } + + + @Override + public void setTestOnBorrow(boolean testOnBorrow) { + this.poolProperties.setTestOnBorrow(testOnBorrow); + } + + + @Override + public void setTestOnConnect(boolean testOnConnect) { + this.poolProperties.setTestOnConnect(testOnConnect); + } + + + @Override + public void setTestOnReturn(boolean testOnReturn) { + this.poolProperties.setTestOnReturn(testOnReturn); + } + + + @Override + public void setTestWhileIdle(boolean testWhileIdle) { + this.poolProperties.setTestWhileIdle(testWhileIdle); + } + + + @Override + public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) { + this.poolProperties.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + } + + + @Override + public void setUrl(String url) { + this.poolProperties.setUrl(url); + } + + + @Override + public void setUsername(String username) { + this.poolProperties.setUsername(username); + this.poolProperties.getDbProperties().setProperty("user",getPoolProperties().getUsername()); + } + + + @Override + public void setValidationInterval(long validationInterval) { + this.poolProperties.setValidationInterval(validationInterval); + } + + + @Override + public void setValidationQuery(String validationQuery) { + this.poolProperties.setValidationQuery(validationQuery); + } + + + @Override + public void setValidatorClassName(String className) { + this.poolProperties.setValidatorClassName(className); + } + + + @Override + public void setValidationQueryTimeout(int validationQueryTimeout) { + this.poolProperties.setValidationQueryTimeout(validationQueryTimeout); + } + + + @Override + public void setJdbcInterceptors(String interceptors) { + this.getPoolProperties().setJdbcInterceptors(interceptors); + } + + + @Override + public void setJmxEnabled(boolean enabled) { + this.getPoolProperties().setJmxEnabled(enabled); + } + + + @Override + public void setFairQueue(boolean fairQueue) { + this.getPoolProperties().setFairQueue(fairQueue); + } + + + @Override + public void setUseLock(boolean useLock) { + this.getPoolProperties().setUseLock(useLock); + } + + + @Override + public void setDefaultCatalog(String catalog) { + this.getPoolProperties().setDefaultCatalog(catalog); + } + + + @Override + public void setDefaultAutoCommit(Boolean autocommit) { + this.getPoolProperties().setDefaultAutoCommit(autocommit); + } + + + @Override + public void setDefaultTransactionIsolation(int defaultTransactionIsolation) { + this.getPoolProperties().setDefaultTransactionIsolation(defaultTransactionIsolation); + } + + + @Override + public void setConnectionProperties(String properties) { + try { + Properties prop = DataSourceFactory.getProperties(properties); + Iterator i = prop.keySet().iterator(); + while (i.hasNext()) { + String key = (String) i.next(); + String value = prop.getProperty(key); + getPoolProperties().getDbProperties().setProperty(key, value); + } + + } catch (Exception x) { + log.error("Unable to parse connection properties.", x); + throw new RuntimeException(x); + } + } + + + @Override + public void setUseEquals(boolean useEquals) { + this.getPoolProperties().setUseEquals(useEquals); + } + + /** + * no-op + * {@link javax.sql.DataSource#getParentLogger} + * @return no return value + * @throws SQLFeatureNotSupportedException Unsupported + */ + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * no-op + * {@link javax.sql.DataSource#getLogWriter} + * @return null + * @throws SQLException No exception + */ + public PrintWriter getLogWriter() throws SQLException { + return null; + } + + + /** + * no-op + * {@link javax.sql.DataSource#setLogWriter(PrintWriter)} + * @param out Ignored + * @throws SQLException No exception + */ + public void setLogWriter(PrintWriter out) throws SQLException { + // NOOP + } + + /** + * no-op + * {@link javax.sql.DataSource#getLoginTimeout} + * @return the timeout + */ + public int getLoginTimeout() { + if (poolProperties == null) { + return 0; + } else { + return poolProperties.getMaxWait() / 1000; + } + } + + /** + * {@link javax.sql.DataSource#setLoginTimeout(int)} + * @param i The timeout value + */ + public void setLoginTimeout(int i) { + if (poolProperties == null) { + return; + } else { + poolProperties.setMaxWait(1000 * i); + } + + } + + + + @Override + public int getSuspectTimeout() { + return getPoolProperties().getSuspectTimeout(); + } + + + @Override + public void setSuspectTimeout(int seconds) { + getPoolProperties().setSuspectTimeout(seconds); + } + + //=============================================================================== +// Expose JMX attributes through Tomcat's dynamic reflection +//=============================================================================== + /** + * If the pool has not been created, it will be created during this call. + * @return the number of established but idle connections + */ + public int getIdle() { + try { + return createPool().getIdle(); + }catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * {@link #getIdle()} + * @return the number of established but idle connections + */ + public int getNumIdle() { + return getIdle(); + } + + /** + * Forces an abandon check on the connection pool. + * If connections that have been abandoned exists, they will be closed during this run + */ + public void checkAbandoned() { + try { + createPool().checkAbandoned(); + }catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * Forces a check for resizing of the idle connections + */ + public void checkIdle() { + try { + createPool().checkIdle(); + }catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * @return number of connections in use by the application + */ + public int getActive() { + try { + return createPool().getActive(); + }catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * @return number of connections in use by the application + * {@link DataSource#getActive()} + */ + public int getNumActive() { + return getActive(); + } + + /** + * @return number of threads waiting for a connection + */ + public int getWaitCount() { + try { + return createPool().getWaitCount(); + }catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * @return the current size of the pool + */ + public int getSize() { + try { + return createPool().getSize(); + }catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * Performs a validation on idle connections + */ + public void testIdle() { + try { + createPool().testAllIdle(); + }catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * The total number of connections borrowed from this pool. + * @return the borrowed connection count + */ + public long getBorrowedCount() { + try { + return createPool().getBorrowedCount(); + } catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * The total number of connections returned to this pool. + * @return the returned connection count + */ + public long getReturnedCount() { + try { + return createPool().getReturnedCount(); + } catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * The total number of connections created by this pool. + * @return the created connection count + */ + public long getCreatedCount() { + try { + return createPool().getCreatedCount(); + } catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * The total number of connections released from this pool. + * @return the released connection count + */ + public long getReleasedCount() { + try { + return createPool().getReleasedCount(); + } catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * The total number of connections reconnected by this pool. + * @return the reconnected connection count + */ + public long getReconnectedCount() { + try { + return createPool().getReconnectedCount(); + } catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * The total number of connections released by remove abandoned. + * @return the PoolCleaner removed abandoned connection count + */ + public long getRemoveAbandonedCount() { + try { + return createPool().getRemoveAbandonedCount(); + } catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * The total number of connections released by eviction. + * @return the PoolCleaner evicted idle connection count + */ + public long getReleasedIdleCount() { + try { + return createPool().getReleasedIdleCount(); + } catch (SQLException x) { + throw new RuntimeException(x); + } + } + + /** + * reset the statistics of this pool. + */ + public void resetStats() { + try { + createPool().resetStats(); + } catch (SQLException x) { + throw new RuntimeException(x); + } + } + + //========================================================= + // PROPERTIES / CONFIGURATION + //========================================================= + + + @Override + public String getConnectionProperties() { + return getPoolProperties().getConnectionProperties(); + } + + + @Override + public Properties getDbProperties() { + return getPoolProperties().getDbProperties(); + } + + + @Override + public String getDefaultCatalog() { + return getPoolProperties().getDefaultCatalog(); + } + + + @Override + public int getDefaultTransactionIsolation() { + return getPoolProperties().getDefaultTransactionIsolation(); + } + + + @Override + public String getDriverClassName() { + return getPoolProperties().getDriverClassName(); + } + + + + @Override + public int getInitialSize() { + return getPoolProperties().getInitialSize(); + } + + + @Override + public String getInitSQL() { + return getPoolProperties().getInitSQL(); + } + + + @Override + public String getJdbcInterceptors() { + return getPoolProperties().getJdbcInterceptors(); + } + + + @Override + public int getMaxActive() { + return getPoolProperties().getMaxActive(); + } + + + @Override + public int getMaxIdle() { + return getPoolProperties().getMaxIdle(); + } + + + @Override + public int getMaxWait() { + return getPoolProperties().getMaxWait(); + } + + + @Override + public int getMinEvictableIdleTimeMillis() { + return getPoolProperties().getMinEvictableIdleTimeMillis(); + } + + + @Override + public int getMinIdle() { + return getPoolProperties().getMinIdle(); + } + + + @Override + public long getMaxAge() { + return getPoolProperties().getMaxAge(); + } + + + @Override + public String getName() { + return getPoolProperties().getName(); + } + + + @Override + public int getNumTestsPerEvictionRun() { + return getPoolProperties().getNumTestsPerEvictionRun(); + } + + /** + * @return DOES NOT RETURN THE PASSWORD, IT WOULD SHOW UP IN JMX + */ + @Override + public String getPassword() { + return "Password not available as DataSource/JMX operation."; + } + + + @Override + public int getRemoveAbandonedTimeout() { + return getPoolProperties().getRemoveAbandonedTimeout(); + } + + + + @Override + public int getTimeBetweenEvictionRunsMillis() { + return getPoolProperties().getTimeBetweenEvictionRunsMillis(); + } + + + @Override + public String getUrl() { + return getPoolProperties().getUrl(); + } + + + @Override + public String getUsername() { + return getPoolProperties().getUsername(); + } + + + @Override + public long getValidationInterval() { + return getPoolProperties().getValidationInterval(); + } + + + @Override + public String getValidationQuery() { + return getPoolProperties().getValidationQuery(); + } + + + @Override + public int getValidationQueryTimeout() { + return getPoolProperties().getValidationQueryTimeout(); + } + + + @Override + public String getValidatorClassName() { + return getPoolProperties().getValidatorClassName(); + } + + + @Override + public Validator getValidator() { + return getPoolProperties().getValidator(); + } + + @Override + public void setValidator(Validator validator) { + getPoolProperties().setValidator(validator); + } + + + + @Override + public boolean isAccessToUnderlyingConnectionAllowed() { + return getPoolProperties().isAccessToUnderlyingConnectionAllowed(); + } + + + @Override + public Boolean isDefaultAutoCommit() { + return getPoolProperties().isDefaultAutoCommit(); + } + + + @Override + public Boolean isDefaultReadOnly() { + return getPoolProperties().isDefaultReadOnly(); + } + + + @Override + public boolean isLogAbandoned() { + return getPoolProperties().isLogAbandoned(); + } + + + @Override + public boolean isPoolSweeperEnabled() { + return getPoolProperties().isPoolSweeperEnabled(); + } + + + @Override + public boolean isRemoveAbandoned() { + return getPoolProperties().isRemoveAbandoned(); + } + + + @Override + public int getAbandonWhenPercentageFull() { + return getPoolProperties().getAbandonWhenPercentageFull(); + } + + + @Override + public boolean isTestOnBorrow() { + return getPoolProperties().isTestOnBorrow(); + } + + + @Override + public boolean isTestOnConnect() { + return getPoolProperties().isTestOnConnect(); + } + + + @Override + public boolean isTestOnReturn() { + return getPoolProperties().isTestOnReturn(); + } + + + @Override + public boolean isTestWhileIdle() { + return getPoolProperties().isTestWhileIdle(); + } + + + + @Override + public Boolean getDefaultAutoCommit() { + return getPoolProperties().getDefaultAutoCommit(); + } + + + @Override + public Boolean getDefaultReadOnly() { + return getPoolProperties().getDefaultReadOnly(); + } + + + @Override + public InterceptorDefinition[] getJdbcInterceptorsAsArray() { + return getPoolProperties().getJdbcInterceptorsAsArray(); + } + + + @Override + public boolean getUseLock() { + return getPoolProperties().getUseLock(); + } + + + @Override + public boolean isFairQueue() { + return getPoolProperties().isFairQueue(); + } + + + @Override + public boolean isJmxEnabled() { + return getPoolProperties().isJmxEnabled(); + } + + + @Override + public boolean isUseEquals() { + return getPoolProperties().isUseEquals(); + } + + + @Override + public void setAbandonWhenPercentageFull(int percentage) { + getPoolProperties().setAbandonWhenPercentageFull(percentage); + } + + + @Override + public void setAccessToUnderlyingConnectionAllowed(boolean accessToUnderlyingConnectionAllowed) { + getPoolProperties().setAccessToUnderlyingConnectionAllowed(accessToUnderlyingConnectionAllowed); + } + + + @Override + public void setDbProperties(Properties dbProperties) { + getPoolProperties().setDbProperties(dbProperties); + } + + + @Override + public void setDefaultReadOnly(Boolean defaultReadOnly) { + getPoolProperties().setDefaultReadOnly(defaultReadOnly); + } + + + @Override + public void setMaxAge(long maxAge) { + getPoolProperties().setMaxAge(maxAge); + } + + + @Override + public void setName(String name) { + getPoolProperties().setName(name); + } + + @Override + public void setDataSource(Object ds) { + getPoolProperties().setDataSource(ds); + } + + @Override + public Object getDataSource() { + return getPoolProperties().getDataSource(); + } + + + @Override + public void setDataSourceJNDI(String jndiDS) { + getPoolProperties().setDataSourceJNDI(jndiDS); + } + + @Override + public String getDataSourceJNDI() { + return getPoolProperties().getDataSourceJNDI(); + } + + @Override + public boolean isAlternateUsernameAllowed() { + return getPoolProperties().isAlternateUsernameAllowed(); + } + + @Override + public void setAlternateUsernameAllowed(boolean alternateUsernameAllowed) { + getPoolProperties().setAlternateUsernameAllowed(alternateUsernameAllowed); + } + + @Override + public void setCommitOnReturn(boolean commitOnReturn) { + getPoolProperties().setCommitOnReturn(commitOnReturn); + } + + @Override + public boolean getCommitOnReturn() { + return getPoolProperties().getCommitOnReturn(); + } + + @Override + public void setRollbackOnReturn(boolean rollbackOnReturn) { + getPoolProperties().setRollbackOnReturn(rollbackOnReturn); + } + + @Override + public boolean getRollbackOnReturn() { + return getPoolProperties().getRollbackOnReturn(); + } + + @Override + public void setUseDisposableConnectionFacade(boolean useDisposableConnectionFacade) { + getPoolProperties().setUseDisposableConnectionFacade(useDisposableConnectionFacade); + } + + @Override + public boolean getUseDisposableConnectionFacade() { + return getPoolProperties().getUseDisposableConnectionFacade(); + } + + @Override + public void setLogValidationErrors(boolean logValidationErrors) { + getPoolProperties().setLogValidationErrors(logValidationErrors); + } + + @Override + public boolean getLogValidationErrors() { + return getPoolProperties().getLogValidationErrors(); + } + + @Override + public boolean getPropagateInterruptState() { + return getPoolProperties().getPropagateInterruptState(); + } + + @Override + public void setPropagateInterruptState(boolean propagateInterruptState) { + getPoolProperties().setPropagateInterruptState(propagateInterruptState); + } + + @Override + public boolean isIgnoreExceptionOnPreLoad() { + return getPoolProperties().isIgnoreExceptionOnPreLoad(); + } + + @Override + public void setIgnoreExceptionOnPreLoad(boolean ignoreExceptionOnPreLoad) { + getPoolProperties().setIgnoreExceptionOnPreLoad(ignoreExceptionOnPreLoad); + } + + @Override + public boolean getUseStatementFacade() { + return getPoolProperties().getUseStatementFacade(); + } + + @Override + public void setUseStatementFacade(boolean useStatementFacade) { + getPoolProperties().setUseStatementFacade(useStatementFacade); + } + + public void purge() { + try { + createPool().purge(); + }catch (SQLException x) { + log.error("Unable to purge pool.",x); + } + } + + public void purgeOnReturn() { + try { + createPool().purgeOnReturn(); + }catch (SQLException x) { + log.error("Unable to purge pool.",x); + } + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DisposableConnectionFacade.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DisposableConnectionFacade.java new file mode 100644 index 0000000..f6cea31 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DisposableConnectionFacade.java @@ -0,0 +1,98 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.SQLException; + +/** + * A DisposableConnectionFacade object is the top most interceptor that wraps an + * object of type {@link PooledConnection}. The DisposableConnectionFacade intercepts + * two methods: + *
      + *
    • {@link java.sql.Connection#close()} - returns the connection to the + * pool then breaks the link between cutoff and the next interceptor. + * May be called multiple times.
    • + *
    • {@link java.lang.Object#toString()} - returns a custom string for this + * object
    • + *
    + * By default method comparisons is done on a String reference level, unless the + * {@link PoolConfiguration#setUseEquals(boolean)} has been called with a + * true argument. + */ +public class DisposableConnectionFacade extends JdbcInterceptor { + protected DisposableConnectionFacade(JdbcInterceptor interceptor) { + setUseEquals(interceptor.isUseEquals()); + setNext(interceptor); + } + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + } + + + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return this==obj; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if (compare(EQUALS_VAL, method)) { + return Boolean.valueOf( + this.equals(Proxy.getInvocationHandler(args[0]))); + } else if (compare(HASHCODE_VAL, method)) { + return Integer.valueOf(this.hashCode()); + } else if (getNext()==null) { + if (compare(ISCLOSED_VAL, method)) { + return Boolean.TRUE; + } + else if (compare(CLOSE_VAL, method)) { + return null; + } + else if (compare(ISVALID_VAL, method)) { + return Boolean.FALSE; + } + } + + try { + return super.invoke(proxy, method, args); + } catch (NullPointerException e) { + if (getNext() == null) { + if (compare(TOSTRING_VAL, method)) { + return "DisposableConnectionFacade[null]"; + } + throw new SQLException( + "PooledConnection has already been closed."); + } + + throw e; + } finally { + if (compare(CLOSE_VAL, method)) { + setNext(null); + } + } + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/FairBlockingQueue.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/FairBlockingQueue.java new file mode 100644 index 0000000..6b23c1a --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/FairBlockingQueue.java @@ -0,0 +1,538 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A simple implementation of a blocking queue with fairness waiting. + * invocations to method poll(...) will get handed out in the order they were received. + * Locking is fine grained, a shared lock is only used during the first level of contention, waiting is done in a + * lock per thread basis so that order is guaranteed once the thread goes into a suspended monitor state. + *
    + * Not all of the methods of the {@link java.util.concurrent.BlockingQueue} are implemented. + * + * @param Type of element in the queue + */ + +public class FairBlockingQueue implements BlockingQueue { + + /** + * This little sucker is used to reorder the way to do + * {@link java.util.concurrent.locks.Lock#lock()}, + * {@link java.util.concurrent.locks.Lock#unlock()} + * and + * {@link java.util.concurrent.CountDownLatch#countDown()} + * during the {@link #poll(long, TimeUnit)} operation. + * On Linux, it performs much better if we count down while we hold the global + * lock, on Solaris its the other way around. + * Until we have tested other platforms we only check for Linux. + */ + static final boolean isLinux = "Linux".equals(System.getProperty("os.name")) && + (!Boolean.getBoolean(FairBlockingQueue.class.getName()+".ignoreOS")); + + /** + * Phase one entry lock in order to give out + * per-thread-locks for the waiting phase we have + * a phase one lock during the contention period. + */ + final ReentrantLock lock = new ReentrantLock(false); + + /** + * All the objects in the pool are stored in a simple linked list + */ + final LinkedList items; + + /** + * All threads waiting for an object are stored in a linked list + */ + final LinkedList> waiters; + + /** + * Creates a new fair blocking queue. + */ + public FairBlockingQueue() { + items = new LinkedList<>(); + waiters = new LinkedList<>(); + } + + //------------------------------------------------------------------ + // USED BY CONPOOL IMPLEMENTATION + //------------------------------------------------------------------ + /** + * Will always return true, queue is unbounded. + * {@inheritDoc} + */ + @Override + public boolean offer(E e) { + //during the offer, we will grab the main lock + final ReentrantLock lock = this.lock; + lock.lock(); + ExchangeCountDownLatch c = null; + try { + //check to see if threads are waiting for an object + if (!waiters.isEmpty()) { + //if threads are waiting grab the latch for that thread + c = waiters.poll(); + //give the object to the thread instead of adding it to the pool + c.setItem(e); + if (isLinux) { + c.countDown(); + } + } else { + //we always add first, so that the most recently used object will be given out + items.addFirst(e); + } + } finally { + lock.unlock(); + } + //if we exchanged an object with another thread, wake it up. + if (!isLinux && c!=null) { + c.countDown(); + } + //we have an unbounded queue, so always return true + return true; + } + + /** + * Will never timeout, as it invokes the {@link #offer(Object)} method. + * Once a lock has been acquired, the + * {@inheritDoc} + */ + @Override + public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { + return offer(e); + } + + /** + * Fair retrieval of an object in the queue. + * Objects are returned in the order the threads requested them. + * {@inheritDoc} + */ + @Override + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + E result = null; + final ReentrantLock lock = this.lock; + //acquire the global lock until we know what to do + lock.lock(); + try { + //check to see if we have objects + result = items.poll(); + if (result==null && timeout>0) { + //the queue is empty we will wait for an object + ExchangeCountDownLatch c = new ExchangeCountDownLatch<>(1); + //add to the bottom of the wait list + waiters.addLast(c); + //unlock the global lock + lock.unlock(); + boolean didtimeout = true; + InterruptedException interruptedException = null; + try { + //wait for the specified timeout + didtimeout = !c.await(timeout, unit); + } catch (InterruptedException ix) { + interruptedException = ix; + } + if (didtimeout) { + //if we timed out, or got interrupted + // remove ourselves from the waitlist + lock.lock(); + try { + waiters.remove(c); + } finally { + lock.unlock(); + } + } + //return the item we received, can be null if we timed out + result = c.getItem(); + if (null!=interruptedException) { + //we got interrupted + if ( null!=result) { + //we got a result - clear the interrupt status + //don't propagate cause we have removed a connection from pool + Thread.interrupted(); + } else { + throw interruptedException; + } + } + } else { + //we have an object, release + lock.unlock(); + } + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + return result; + } + + /** + * Request an item from the queue asynchronously + * @return - a future pending the result from the queue poll request + */ + public Future pollAsync() { + Future result = null; + final ReentrantLock lock = this.lock; + //grab the global lock + lock.lock(); + try { + //check to see if we have objects in the queue + E item = items.poll(); + if (item==null) { + //queue is empty, add ourselves as waiters + ExchangeCountDownLatch c = new ExchangeCountDownLatch<>(1); + waiters.addLast(c); + //return a future that will wait for the object + result = new ItemFuture<>(c); + } else { + //return a future with the item + result = new ItemFuture<>(item); + } + } finally { + lock.unlock(); + } + return result; + } + + @Override + public boolean remove(Object e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return items.remove(e); + } finally { + lock.unlock(); + } + } + + @Override + public int size() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return items.size(); + } finally { + lock.unlock(); + } + } + + @Override + public Iterator iterator() { + return new FairIterator(); + } + + @Override + public E poll() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return items.poll(); + } finally { + lock.unlock(); + } + } + + @Override + public boolean contains(Object e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + return items.contains(e); + } finally { + lock.unlock(); + } + } + + + //------------------------------------------------------------------ + // NOT USED BY CONPOOL IMPLEMENTATION + //------------------------------------------------------------------ + @Override + public boolean add(E e) { + return offer(e); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public int drainTo(Collection c, int maxElements) { + throw new UnsupportedOperationException("int drainTo(Collection c, int maxElements)"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + + @Override + public int drainTo(Collection c) { + return drainTo(c,Integer.MAX_VALUE); + } + + @Override + public void put(E e) throws InterruptedException { + offer(e); + } + + @Override + public int remainingCapacity() { + return Integer.MAX_VALUE - size(); + } + + @Override + public E take() throws InterruptedException { + return this.poll(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + } + + @Override + public boolean addAll(Collection c) { + Iterator i = c.iterator(); + while (i.hasNext()) { + E e = i.next(); + offer(e); + } + return true; + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public void clear() { + throw new UnsupportedOperationException("void clear()"); + + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException("boolean containsAll(Collection c)"); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException("boolean removeAll(Collection c)"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException("boolean retainAll(Collection c)"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public Object[] toArray() { + throw new UnsupportedOperationException("Object[] toArray()"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public T[] toArray(T[] a) { + throw new UnsupportedOperationException(" T[] toArray(T[] a)"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public E element() { + throw new UnsupportedOperationException("E element()"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public E peek() { + throw new UnsupportedOperationException("E peek()"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public E remove() { + throw new UnsupportedOperationException("E remove()"); + } + + + + //------------------------------------------------------------------ + // Non cancellable Future used to check and see if a connection has been made available + //------------------------------------------------------------------ + protected class ItemFuture implements Future { + protected volatile T item = null; + protected volatile ExchangeCountDownLatch latch = null; + protected volatile boolean canceled = false; + + public ItemFuture(T item) { + this.item = item; + } + + public ItemFuture(ExchangeCountDownLatch latch) { + this.latch = latch; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; //don't allow cancel for now + } + + @Override + public T get() throws InterruptedException, ExecutionException { + if (item!=null) { + return item; + } else if (latch!=null) { + latch.await(); + return latch.getItem(); + } else { + throw new ExecutionException("ItemFuture incorrectly instantiated. Bug in the code?", new Exception()); + } + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (item!=null) { + return item; + } else if (latch!=null) { + boolean timedout = !latch.await(timeout, unit); + if (timedout) { + throw new TimeoutException(); + } else { + return latch.getItem(); + } + } else { + throw new ExecutionException("ItemFuture incorrectly instantiated. Bug in the code?", new Exception()); + } + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return (item!=null || latch.getItem()!=null); + } + + } + + //------------------------------------------------------------------ + // Count down latch that can be used to exchange information + //------------------------------------------------------------------ + protected class ExchangeCountDownLatch extends CountDownLatch { + protected volatile T item; + public ExchangeCountDownLatch(int i) { + super(i); + } + public T getItem() { + return item; + } + public void setItem(T item) { + this.item = item; + } + } + + //------------------------------------------------------------------ + // Iterator safe from concurrent modification exceptions + //------------------------------------------------------------------ + protected class FairIterator implements Iterator { + E[] elements = null; + int index; + E element = null; + + @SuppressWarnings("unchecked") // Can't create arrays of generic types + public FairIterator() { + final ReentrantLock lock = FairBlockingQueue.this.lock; + lock.lock(); + try { + elements = (E[]) new Object[FairBlockingQueue.this.items.size()]; + FairBlockingQueue.this.items.toArray(elements); + index = 0; + } finally { + lock.unlock(); + } + } + @Override + public boolean hasNext() { + return index + * Interceptors can receive a set of properties. Each sub class is responsible for parsing the properties during runtime when they + * are needed or simply override the {@link #setProperties(Map)} method. + * Properties arrive in a key-value pair of Strings as they were received through the configuration. + * This method is called once per cached connection object when the object is first configured. + */ +public abstract class JdbcInterceptor implements InvocationHandler { + /** + * {@link java.sql.Connection#close()} method name + */ + public static final String CLOSE_VAL = "close"; + /** + * {@link Object#toString()} method name + */ + public static final String TOSTRING_VAL = "toString"; + /** + * {@link java.sql.Connection#isClosed()} method name + */ + public static final String ISCLOSED_VAL = "isClosed"; + /** + * {@link javax.sql.PooledConnection#getConnection()} method name + */ + public static final String GETCONNECTION_VAL = "getConnection"; + /** + * {@link java.sql.Wrapper#unwrap(Class)} method name + */ + public static final String UNWRAP_VAL = "unwrap"; + /** + * {@link java.sql.Wrapper#isWrapperFor(Class)} method name + */ + public static final String ISWRAPPERFOR_VAL = "isWrapperFor"; + + /** + * {@link java.sql.Connection#isValid(int)} method name + */ + public static final String ISVALID_VAL = "isValid"; + + /** + * {@link java.lang.Object#equals(Object)} + */ + public static final String EQUALS_VAL = "equals"; + + /** + * {@link java.lang.Object#hashCode()} + */ + public static final String HASHCODE_VAL = "hashCode"; + + /** + * Properties for this interceptor. + */ + protected Map properties = null; + + /** + * The next interceptor in the chain + */ + private volatile JdbcInterceptor next = null; + /** + * Property that decides how we do string comparison, default is to use + * {@link String#equals(Object)}. If set to false then the + * equality operator (==) is used. + */ + private boolean useEquals = true; + + /** + * Public constructor for instantiation through reflection + */ + public JdbcInterceptor() { + // NOOP + } + + /** + * Gets invoked each time an operation on {@link java.sql.Connection} is invoked. + * {@inheritDoc} + */ + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (getNext()!=null) { + return getNext().invoke(proxy,method,args); + } else { + throw new NullPointerException(); + } + } + + /** + * Returns the next interceptor in the chain + * @return the next interceptor in the chain + */ + public JdbcInterceptor getNext() { + return next; + } + + /** + * configures the next interceptor in the chain + * @param next The next chain item + */ + public void setNext(JdbcInterceptor next) { + this.next = next; + } + + /** + * Performs a string comparison, using references unless the useEquals property is set to true. + * @param name1 The first name + * @param name2 The second name + * @return true if name1 is equal to name2 based on {@link #useEquals} + */ + public boolean compare(String name1, String name2) { + if (isUseEquals()) { + return name1.equals(name2); + } else { + return name1==name2; + } + } + + /** + * Compares a method name (String) to a method (Method) + * {@link #compare(String,String)} + * Uses reference comparison unless the useEquals property is set to true + * @param methodName The method name + * @param method The method + * @return true if the name matches + */ + public boolean compare(String methodName, Method method) { + return compare(methodName, method.getName()); + } + + /** + * Gets called each time the connection is borrowed from the pool + * This means that if an interceptor holds a reference to the connection + * the interceptor can be reused for another connection. + *
    + * This method may be called with null as both arguments when we are closing down the connection. + * @param parent - the connection pool owning the connection + * @param con - the pooled connection + */ + public abstract void reset(ConnectionPool parent, PooledConnection con); + + /** + * Called when {@link java.sql.Connection#close()} is called on the underlying connection. + * This is to notify the interceptors, that the physical connection has been released. + * Implementation of this method should be thought through with care, as no actions should trigger an exception. + * @param parent - the connection pool that this connection belongs to + * @param con - the pooled connection that holds this connection + * @param finalizing - if this connection is finalizing. True means that the pooled connection will not reconnect the underlying connection + */ + public void disconnected(ConnectionPool parent, PooledConnection con, boolean finalizing) { + } + + + /** + * Returns the properties configured for this interceptor + * @return the configured properties for this interceptor + */ + public Map getProperties() { + return properties; + } + + /** + * Called during the creation of an interceptor + * The properties can be set during the configuration of an interceptor + * Override this method to perform type casts between string values and object properties + * @param properties The properties + */ + public void setProperties(Map properties) { + this.properties = properties; + final String useEquals = "useEquals"; + InterceptorProperty p = properties.get(useEquals); + if (p!=null) { + setUseEquals(Boolean.parseBoolean(p.getValue())); + } + } + + /** + * @return true if the compare method uses the Object.equals(Object) method + * false if comparison is done on a reference level + */ + public boolean isUseEquals() { + return useEquals; + } + + /** + * Set to true if string comparisons (for the {@link #compare(String, Method)} and {@link #compare(String, String)} methods) should use the Object.equals(Object) method + * The default is false + * @param useEquals true if equals will be used for comparisons + */ + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + /** + * This method is invoked by a connection pool when the pool is closed. + * Interceptor classes can override this method if they keep static + * variables or other tracking means around. + * This method is only invoked on a single instance of the interceptor, and not on every instance created. + * @param pool - the pool that is being closed. + */ + public void poolClosed(ConnectionPool pool) { + // NOOP + } + + /** + * This method is invoked by a connection pool when the pool is first started up, usually when the first connection is requested. + * Interceptor classes can override this method if they keep static + * variables or other tracking means around. + * This method is only invoked on a single instance of the interceptor, and not on every instance created. + * @param pool - the pool that is being closed. + */ + public void poolStarted(ConnectionPool pool) { + // NOOP + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/MultiLockFairBlockingQueue.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/MultiLockFairBlockingQueue.java new file mode 100644 index 0000000..30e94c6 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/MultiLockFairBlockingQueue.java @@ -0,0 +1,542 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +/** + * EXPERIMENTAL AND NOT YET COMPLETE! + * + * + * An implementation of a blocking queue with fairness waiting and lock dispersal to avoid contention. + * invocations to method poll(...) will get handed out in the order they were received. + * Locking is fine grained, a shared lock is only used during the first level of contention, waiting is done in a + * lock per thread basis so that order is guaranteed once the thread goes into a suspended monitor state. + *
    + * Not all of the methods of the {@link java.util.concurrent.BlockingQueue} are implemented. + * + * @param Type of element in the queue + */ + +public class MultiLockFairBlockingQueue implements BlockingQueue { + + final int LOCK_COUNT = Runtime.getRuntime().availableProcessors(); + + final AtomicInteger putQueue = new AtomicInteger(0); + final AtomicInteger pollQueue = new AtomicInteger(0); + + public int getNextPut() { + int idx = Math.abs(putQueue.incrementAndGet()) % LOCK_COUNT; + return idx; + } + + public int getNextPoll() { + int idx = Math.abs(pollQueue.incrementAndGet()) % LOCK_COUNT; + return idx; + } + /** + * Phase one entry lock in order to give out + * per-thread-locks for the waiting phase we have + * a phase one lock during the contention period. + */ + private final ReentrantLock[] locks = new ReentrantLock[LOCK_COUNT]; + + /** + * All the objects in the pool are stored in a simple linked list + */ + final LinkedList[] items; + + /** + * All threads waiting for an object are stored in a linked list + */ + final LinkedList>[] waiters; + + /** + * Creates a new fair blocking queue. + */ + @SuppressWarnings("unchecked") // Can create arrays of generic types + public MultiLockFairBlockingQueue() { + items = new LinkedList[LOCK_COUNT]; + waiters = new LinkedList[LOCK_COUNT]; + for (int i=0; i(); + waiters[i] = new LinkedList<>(); + locks[i] = new ReentrantLock(false); + } + } + + //------------------------------------------------------------------ + // USED BY CONPOOL IMPLEMENTATION + //------------------------------------------------------------------ + /** + * Will always return true, queue is unbounded. + * {@inheritDoc} + */ + @Override + public boolean offer(E e) { + int idx = getNextPut(); + //during the offer, we will grab the main lock + final ReentrantLock lock = this.locks[idx]; + lock.lock(); + ExchangeCountDownLatch c = null; + try { + //check to see if threads are waiting for an object + if (!waiters[idx].isEmpty()) { + //if threads are waiting grab the latch for that thread + c = waiters[idx].poll(); + //give the object to the thread instead of adding it to the pool + c.setItem(e); + } else { + //we always add first, so that the most recently used object will be given out + items[idx].addFirst(e); + } + } finally { + lock.unlock(); + } + //if we exchanged an object with another thread, wake it up. + if (c!=null) { + c.countDown(); + } + //we have an unbounded queue, so always return true + return true; + } + + /** + * Will never timeout, as it invokes the {@link #offer(Object)} method. + * Once a lock has been acquired, the + * {@inheritDoc} + */ + @Override + public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { + return offer(e); + } + + /** + * Fair retrieval of an object in the queue. + * Objects are returned in the order the threads requested them. + * {@inheritDoc} + */ + @Override + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + int idx = getNextPoll(); + E result = null; + final ReentrantLock lock = this.locks[idx]; + try { + //acquire the global lock until we know what to do + lock.lock(); + //check to see if we have objects + result = items[idx].poll(); + if (result==null && timeout>0) { + //the queue is empty we will wait for an object + ExchangeCountDownLatch c = new ExchangeCountDownLatch<>(1); + //add to the bottom of the wait list + waiters[idx].addLast(c); + //unlock the global lock + lock.unlock(); + //wait for the specified timeout + if (!c.await(timeout, unit)) { + //if we timed out, remove ourselves from the waitlist + lock.lock(); + waiters[idx].remove(c); + lock.unlock(); + } + //return the item we received, can be null if we timed out + result = c.getItem(); + } else { + //we have an object, release + lock.unlock(); + } + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + return result; + } + + /** + * Request an item from the queue asynchronously + * @return - a future pending the result from the queue poll request + */ + public Future pollAsync() { + int idx = getNextPoll(); + Future result = null; + final ReentrantLock lock = this.locks[idx]; + try { + //grab the global lock + lock.lock(); + //check to see if we have objects in the queue + E item = items[idx].poll(); + if (item==null) { + //queue is empty, add ourselves as waiters + ExchangeCountDownLatch c = new ExchangeCountDownLatch<>(1); + waiters[idx].addLast(c); + //return a future that will wait for the object + result = new ItemFuture<>(c); + } else { + //return a future with the item + result = new ItemFuture<>(item); + } + } finally { + lock.unlock(); + } + return result; + } + + @Override + public boolean remove(Object e) { + for (int idx=0; idx iterator() { + return new FairIterator(); + } + + @Override + public E poll() { + int idx = getNextPoll(); + final ReentrantLock lock = this.locks[idx]; + lock.lock(); + try { + return items[idx].poll(); + } finally { + lock.unlock(); + } + } + + @Override + public boolean contains(Object e) { + for (int idx=0; idx c, int maxElements) { + throw new UnsupportedOperationException("int drainTo(Collection c, int maxElements)"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public int drainTo(Collection c) { + return drainTo(c,Integer.MAX_VALUE); + } + + @Override + public void put(E e) throws InterruptedException { + offer(e); + } + + @Override + public int remainingCapacity() { + return Integer.MAX_VALUE - size(); + } + + @Override + public E take() throws InterruptedException { + return this.poll(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + } + + @Override + public boolean addAll(Collection c) { + Iterator i = c.iterator(); + while (i.hasNext()) { + E e = i.next(); + offer(e); + } + return true; + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public void clear() { + throw new UnsupportedOperationException("void clear()"); + + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException("boolean containsAll(Collection c)"); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException("boolean removeAll(Collection c)"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException("boolean retainAll(Collection c)"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public Object[] toArray() { + throw new UnsupportedOperationException("Object[] toArray()"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public T[] toArray(T[] a) { + throw new UnsupportedOperationException(" T[] toArray(T[] a)"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public E element() { + throw new UnsupportedOperationException("E element()"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public E peek() { + throw new UnsupportedOperationException("E peek()"); + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException - this operation is not supported + */ + @Override + public E remove() { + throw new UnsupportedOperationException("E remove()"); + } + + + + //------------------------------------------------------------------ + // Non cancellable Future used to check and see if a connection has been made available + //------------------------------------------------------------------ + protected class ItemFuture implements Future { + protected volatile T item = null; + protected volatile ExchangeCountDownLatch latch = null; + protected volatile boolean canceled = false; + + public ItemFuture(T item) { + this.item = item; + } + + public ItemFuture(ExchangeCountDownLatch latch) { + this.latch = latch; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; //don't allow cancel for now + } + + @Override + public T get() throws InterruptedException, ExecutionException { + if (item!=null) { + return item; + } else if (latch!=null) { + latch.await(); + return latch.getItem(); + } else { + throw new ExecutionException("ItemFuture incorrectly instantiated. Bug in the code?", new Exception()); + } + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (item!=null) { + return item; + } else if (latch!=null) { + boolean timedout = !latch.await(timeout, unit); + if (timedout) { + throw new TimeoutException(); + } else { + return latch.getItem(); + } + } else { + throw new ExecutionException("ItemFuture incorrectly instantiated. Bug in the code?", new Exception()); + } + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return (item!=null || latch.getItem()!=null); + } + + } + + //------------------------------------------------------------------ + // Count down latch that can be used to exchange information + //------------------------------------------------------------------ + protected class ExchangeCountDownLatch extends CountDownLatch { + protected volatile T item; + public ExchangeCountDownLatch(int i) { + super(i); + } + public T getItem() { + return item; + } + public void setItem(T item) { + this.item = item; + } + } + + //------------------------------------------------------------------ + // Iterator safe from concurrent modification exceptions + //------------------------------------------------------------------ + protected class FairIterator implements Iterator { + E[] elements = null; + int index; + E element = null; + + @SuppressWarnings("unchecked") // Can't create arrays of generic types + public FairIterator() { + ArrayList list = new ArrayList<>(MultiLockFairBlockingQueue.this.size()); + for (int idx=0; idxtrue if a fair queue is being used by the connection pool + * @return true if a fair waiting queue is being used + */ + boolean isFairQueue(); + + /** + * Set to true if you wish that calls to getConnection + * should be treated fairly in a true FIFO fashion. + * This uses the {@link FairBlockingQueue} implementation for the list of the idle connections. + * The default value is true. + * This flag is required when you want to use asynchronous connection retrieval. + * @param fairQueue true to use a fair queue + */ + void setFairQueue(boolean fairQueue); + + /** + * Property not used. Access is always allowed. + * Access can be achieved by calling unwrap on the pooled connection. see {@link javax.sql.DataSource} interface + * or call getConnection through reflection or cast the object as {@link javax.sql.PooledConnection} + * @return true + */ + boolean isAccessToUnderlyingConnectionAllowed(); + + /** + * No-op + * @param accessToUnderlyingConnectionAllowed parameter ignored + */ + void setAccessToUnderlyingConnectionAllowed(boolean accessToUnderlyingConnectionAllowed); + + /** + * The connection properties that will be sent to the JDBC driver when establishing new connections. + * Format of the string is [propertyName=property;]
    + * NOTE - The "user" and "password" properties will be passed explicitly, so they do not need to be included here. + * The default value is null. + * @return the connection properties + */ + String getConnectionProperties(); + + /** + * The properties that will be passed into {@link java.sql.Driver#connect(String, Properties)} method. + * Username and password do not need to be stored here, they will be passed into the properties right before the connection is established. + * @param connectionProperties properties - Format of the string is [propertyName=property;]* + * Example: prop1=value1;prop2=value2 + */ + void setConnectionProperties(String connectionProperties); + + /** + * Returns the database properties that are passed into the {@link java.sql.Driver#connect(String, Properties)} method. + * @return database properties that are passed into the {@link java.sql.Driver#connect(String, Properties)} method. + */ + Properties getDbProperties(); + + /** + * Overrides the database properties passed into the {@link java.sql.Driver#connect(String, Properties)} method. + * @param dbProperties The database properties + */ + void setDbProperties(Properties dbProperties); + + /** + * The default auto-commit state of connections created by this pool. + * If not set (null), default is JDBC driver default (If set to null then the {@link java.sql.Connection#setAutoCommit(boolean)} method will not be called.) + * @return the default auto commit setting, null is Driver default. + */ + Boolean isDefaultAutoCommit(); + + /** + * The default auto-commit state of connections created by this pool. + * If not set (null), default is JDBC driver default (If set to null then the {@link java.sql.Connection#setAutoCommit(boolean)} method will not be called.) + * @return the default auto commit setting, null is Driver default. + */ + Boolean getDefaultAutoCommit(); + + /** + * The default auto-commit state of connections created by this pool. + * If not set (null), default is JDBC driver default (If set to null then the {@link java.sql.Connection#setAutoCommit(boolean)} method will not be called.) + * @param defaultAutoCommit default auto commit setting, null is Driver default. + */ + void setDefaultAutoCommit(Boolean defaultAutoCommit); + + /** + * If non null, during connection creation the method {@link java.sql.Connection#setCatalog(String)} will be called with the set value. + * @return the default catalog, null if not set and accepting the driver default. + */ + String getDefaultCatalog(); + + /** + * If non null, during connection creation the method {@link java.sql.Connection#setCatalog(String)} will be called with the set value. + * @param defaultCatalog null if not set and accepting the driver default. + */ + void setDefaultCatalog(String defaultCatalog); + + /** + * If non null, during connection creation the method {@link java.sql.Connection#setReadOnly(boolean)} will be called with the set value. + * @return null if not set and accepting the driver default otherwise the read only value + */ + Boolean isDefaultReadOnly(); + + /** + * If non null, during connection creation the method {@link java.sql.Connection#setReadOnly(boolean)} will be called with the set value. + * @return null if not set and accepting the driver default otherwise the read only value + */ + Boolean getDefaultReadOnly(); + + /** + * If non null, during connection creation the method {@link java.sql.Connection#setReadOnly(boolean)} will be called with the set value. + * @param defaultReadOnly null if not set and accepting the driver default. + */ + void setDefaultReadOnly(Boolean defaultReadOnly); + + + /** + * Returns the default transaction isolation level. If set to {@link DataSourceFactory#UNKNOWN_TRANSACTIONISOLATION} the method + * {@link java.sql.Connection#setTransactionIsolation(int)} will not be called during connection creation. + * @return driver transaction isolation level, or -1 {@link DataSourceFactory#UNKNOWN_TRANSACTIONISOLATION} if not set. + */ + int getDefaultTransactionIsolation(); + + /** + * If set to {@link DataSourceFactory#UNKNOWN_TRANSACTIONISOLATION} the method + * {@link java.sql.Connection#setTransactionIsolation(int)} will not be called during connection creation. Otherwise the method + * will be called with the isolation level set by this property. + * @param defaultTransactionIsolation a value of {@link java.sql.Connection#TRANSACTION_NONE}, {@link java.sql.Connection#TRANSACTION_READ_COMMITTED}, + * {@link java.sql.Connection#TRANSACTION_READ_UNCOMMITTED}, {@link java.sql.Connection#TRANSACTION_REPEATABLE_READ}, + * {@link java.sql.Connection#TRANSACTION_SERIALIZABLE} or {@link DataSourceFactory#UNKNOWN_TRANSACTIONISOLATION} + * The last value will not be set on the connection. + */ + void setDefaultTransactionIsolation(int defaultTransactionIsolation); + + /** + * The fully qualified Java class name of the JDBC driver to be used. The driver has to be accessible from the same classloader as tomcat-jdbc.jar + * @return fully qualified JDBC driver name. + */ + String getDriverClassName(); + + /** + * The fully qualified Java class name of the JDBC driver to be used. The driver has to be accessible from the same classloader as tomcat-jdbc.jar + * @param driverClassName a fully qualified Java class name of a {@link java.sql.Driver} implementation. + */ + void setDriverClassName(String driverClassName); + + /** + * Returns the number of connections that will be established when the connection pool is started. + * Default value is 10 + * @return number of connections to be started when pool is started + */ + int getInitialSize(); + + /** + * Set the number of connections that will be established when the connection pool is started. + * Default value is 10. + * If this value exceeds {@link #setMaxActive(int)} it will automatically be lowered. + * @param initialSize the number of connections to be established. + * + */ + void setInitialSize(int initialSize); + + /** + * boolean flag to set if stack traces should be logged for application code which abandoned a Connection. + * Logging of abandoned Connections adds overhead for every Connection borrow because a stack trace has to be generated. + * The default value is false. + * @return true if the connection pool logs stack traces when connections are borrowed from the pool. + */ + boolean isLogAbandoned(); + + /** + * boolean flag to set if stack traces should be logged for application code which abandoned a Connection. + * Logging of abandoned Connections adds overhead for every Connection borrow because a stack trace has to be generated. + * The default value is false. + * @param logAbandoned set to true if stack traces should be recorded when {@link DataSource#getConnection()} is called. + */ + void setLogAbandoned(boolean logAbandoned); + + /** + * The maximum number of active connections that can be allocated from this pool at the same time. The default value is 100 + * @return the maximum number of connections used by this pool + */ + int getMaxActive(); + + /** + * The maximum number of active connections that can be allocated from this pool at the same time. The default value is 100 + * @param maxActive hard limit for number of managed connections by this pool + */ + void setMaxActive(int maxActive); + + + /** + * The maximum number of connections that should be kept in the idle pool if {@link #isPoolSweeperEnabled()} returns false. + * If the If {@link #isPoolSweeperEnabled()} returns true, then the idle pool can grow up to {@link #getMaxActive} + * and will be shrunk according to {@link #getMinEvictableIdleTimeMillis()} setting. + * Default value is maxActive:100 + * @return the maximum number of idle connections. + */ + int getMaxIdle(); + + /** + * The maximum number of connections that should be kept in the idle pool if {@link #isPoolSweeperEnabled()} returns false. + * If the If {@link #isPoolSweeperEnabled()} returns true, then the idle pool can grow up to {@link #getMaxActive} + * and will be shrunk according to {@link #getMinEvictableIdleTimeMillis()} setting. + * Default value is maxActive:100 + * @param maxIdle the maximum size of the idle pool + */ + void setMaxIdle(int maxIdle); + + /** + * The maximum number of milliseconds that the pool will wait (when there are no available connections and the + * {@link #getMaxActive} has been reached) for a connection to be returned + * before throwing an exception. Default value is 30000 (30 seconds) + * @return the number of milliseconds to wait for a connection to become available if the pool is maxed out. + */ + int getMaxWait(); + + /** + * The maximum number of milliseconds that the pool will wait (when there are no available connections and the + * {@link #getMaxActive} has been reached) for a connection to be returned + * before throwing an exception. Default value is 30000 (30 seconds) + * @param maxWait the maximum number of milliseconds to wait. + */ + void setMaxWait(int maxWait); + + /** + * The minimum amount of time an object must sit idle in the pool before it is eligible for eviction. + * The default value is 60000 (60 seconds). + * @return the minimum amount of idle time in milliseconds before a connection is considered idle and eligible for eviction. + */ + int getMinEvictableIdleTimeMillis(); + + /** + * The minimum amount of time an object must sit idle in the pool before it is eligible for eviction. + * The default value is 60000 (60 seconds). + * @param minEvictableIdleTimeMillis the number of milliseconds a connection must be idle to be eligible for eviction. + */ + void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis); + + /** + * The minimum number of established connections that should be kept in the pool at all times. + * The connection pool can shrink below this number if validation queries fail and connections get closed. + * Default value is derived from {@link #getInitialSize()} (also see {@link #setTestWhileIdle(boolean)} + * The idle pool will not shrink below this value during an eviction run, hence the number of actual connections + * can be between {@link #getMinIdle()} and somewhere between {@link #getMaxIdle()} and {@link #getMaxActive()} + * @return the minimum number of idle or established connections + */ + int getMinIdle(); + + /** + * The minimum number of established connections that should be kept in the pool at all times. + * The connection pool can shrink below this number if validation queries fail and connections get closed. + * Default value is derived from {@link #getInitialSize()} (also see {@link #setTestWhileIdle(boolean)} + * The idle pool will not shrink below this value during an eviction run, hence the number of actual connections + * can be between {@link #getMinIdle()} and somewhere between {@link #getMaxIdle()} and {@link #getMaxActive()} + * + * @param minIdle the minimum number of idle or established connections + */ + void setMinIdle(int minIdle); + + /** + * Returns the name of the connection pool. By default a JVM unique random name is assigned. + * @return the name of the pool, should be unique in a JVM + */ + String getName(); + + /** + * Sets the name of the connection pool + * @param name the name of the pool, should be unique in a runtime JVM + */ + void setName(String name); + + /** + * Property not used + * @return unknown value + */ + int getNumTestsPerEvictionRun(); + + /** + * Property not used + * @param numTestsPerEvictionRun parameter ignored. + */ + void setNumTestsPerEvictionRun(int numTestsPerEvictionRun); + + /** + * Returns the password used when establishing connections to the database. + * @return the password in string format + */ + String getPassword(); + + /** + * Sets the password to establish the connection with. + * The password will be included as a database property with the name 'password'. + * @param password The password + * @see #getDbProperties() + */ + void setPassword(String password); + + /** + * @see #getName() + * @return the pool name + */ + String getPoolName(); + + /** + * Returns the username used to establish the connection with + * @return the username used to establish the connection with + */ + String getUsername(); + + /** + * Sets the username used to establish the connection with + * It will also be a property called 'user' in the database properties. + * @param username The user name + * @see #getDbProperties() + */ + void setUsername(String username); + + + /** + * boolean flag to remove abandoned connections if they exceed the removeAbandonedTimeout. + * If set to true a connection is considered abandoned and eligible for removal if it has + * been in use longer than the {@link #getRemoveAbandonedTimeout()} and the condition for + * {@link #getAbandonWhenPercentageFull()} is met. + * Setting this to true can recover db connections from applications that fail to close a connection. + * See also {@link #isLogAbandoned()} The default value is false. + * @return true if abandoned connections can be closed and expelled out of the pool + */ + boolean isRemoveAbandoned(); + + /** + * boolean flag to remove abandoned connections if they exceed the removeAbandonedTimeout. + * If set to true a connection is considered abandoned and eligible for removal if it has + * been in use longer than the {@link #getRemoveAbandonedTimeout()} and the condition for + * {@link #getAbandonWhenPercentageFull()} is met. + * Setting this to true can recover db connections from applications that fail to close a connection. + * See also {@link #isLogAbandoned()} The default value is false. + * @param removeAbandoned set to true if abandoned connections can be closed and expelled out of the pool + */ + void setRemoveAbandoned(boolean removeAbandoned); + + /** + * The time in seconds before a connection can be considered abandoned. + * The timer can be reset upon queries using an interceptor. + * @param removeAbandonedTimeout the time in seconds before a used connection can be considered abandoned + * @see org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer + */ + void setRemoveAbandonedTimeout(int removeAbandonedTimeout); + + /** + * The time in seconds before a connection can be considered abandoned. + * The timer can be reset upon queries using an interceptor. + * @see org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer + * @return the time in seconds before a used connection can be considered abandoned + */ + int getRemoveAbandonedTimeout(); + + /** + * The indication of whether objects will be validated before being borrowed from the pool. + * If the object fails to validate, it will be dropped from the pool, and we will attempt to borrow another. + * NOTE - for a true value to have any effect, the validationQuery parameter must be set to a non-null string. + * Default value is false + * In order to have a more efficient validation, see {@link #setValidationInterval(long)} + * @return true if the connection is to be validated upon borrowing a connection from the pool + * @see #getValidationInterval() + */ + boolean isTestOnBorrow(); + + /** + * The indication of whether objects will be validated before being borrowed from the pool. + * If the object fails to validate, it will be dropped from the pool, and we will attempt to borrow another. + * NOTE - for a true value to have any effect, the validationQuery parameter must be set to a non-null string. + * Default value is false + * In order to have a more efficient validation, see {@link #setValidationInterval(long)} + * @param testOnBorrow set to true if validation should take place before a connection is handed out to the application + * @see #getValidationInterval() + */ + void setTestOnBorrow(boolean testOnBorrow); + + /** + * The indication of whether objects will be validated after being returned to the pool. + * If the object fails to validate, it will be dropped from the pool. + * NOTE - for a true value to have any effect, the validationQuery parameter must be set to a non-null string. + * Default value is false + * In order to have a more efficient validation, see {@link #setValidationInterval(long)} + * @return true if validation should take place after a connection is returned to the pool + * @see #getValidationInterval() + */ + boolean isTestOnReturn(); + + /** + * The indication of whether objects will be validated after being returned to the pool. + * If the object fails to validate, it will be dropped from the pool. + * NOTE - for a true value to have any effect, the validationQuery parameter must be set to a non-null string. + * Default value is false + * In order to have a more efficient validation, see {@link #setValidationInterval(long)} + * @param testOnReturn true if validation should take place after a connection is returned to the pool + * @see #getValidationInterval() + */ + void setTestOnReturn(boolean testOnReturn); + + + /** + * Set to true if query validation should take place while the connection is idle. + * @return true if validation should take place during idle checks + * @see #setTimeBetweenEvictionRunsMillis(int) + */ + boolean isTestWhileIdle(); + + /** + * Set to true if query validation should take place while the connection is idle. + * @param testWhileIdle true if validation should take place during idle checks + * @see #setTimeBetweenEvictionRunsMillis(int) + */ + void setTestWhileIdle(boolean testWhileIdle); + + /** + * The number of milliseconds to sleep between runs of the idle connection validation, abandoned cleaner + * and idle pool resizing. This value should not be set under 1 second. + * It dictates how often we check for idle, abandoned connections, and how often we validate idle connection and resize the idle pool. + * The default value is 5000 (5 seconds) + * @return the sleep time in between validations in milliseconds + */ + int getTimeBetweenEvictionRunsMillis(); + + /** + * The number of milliseconds to sleep between runs of the idle connection validation, abandoned cleaner + * and idle pool resizing. This value should not be set under 1 second. + * It dictates how often we check for idle, abandoned connections, and how often we validate idle connection and resize the idle pool. + * The default value is 5000 (5 seconds) + * @param timeBetweenEvictionRunsMillis the sleep time in between validations in milliseconds + */ + void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis); + + /** + * The URL used to connect to the database + * @return the configured URL for this connection pool + * @see java.sql.Driver#connect(String, Properties) + */ + String getUrl(); + + /** + * Sets the URL used to connect to the database + * @param url the configured URL for this connection pool + * @see java.sql.Driver#connect(String, Properties) + */ + void setUrl(String url); + + /** + * The SQL query that will be used to validate connections from this + * pool before returning them to the caller or pool. + * If specified, this query does not have to return any data, + * it just can't throw an SQLException. + * The default value is null. + * Example values are SELECT 1(mysql), + * select 1 from dual(oracle), + * SELECT 1(MS Sql Server) + * @return the query used for validation or null if no validation is performed + */ + String getValidationQuery(); + + /** + * The SQL query that will be used to validate connections from this + * pool before returning them to the caller or pool. + * If specified, this query does not have to return any data, + * it just can't throw an SQLException. + * The default value is null. + * Example values are SELECT 1(mysql), + * select 1 from dual(oracle), + * SELECT 1(MS Sql Server) + * @param validationQuery the query used for validation or null if no validation is performed + */ + void setValidationQuery(String validationQuery); + + /** + * The timeout in seconds before a connection validation queries fail. + * A value less than or equal to zero will disable this feature. Defaults to -1. + * @return the timeout value in seconds + */ + int getValidationQueryTimeout(); + + /** + * The timeout in seconds before a connection validation queries fail. + * A value less than or equal to zero will disable this feature. Defaults to -1. + * @param validationQueryTimeout The timeout value + */ + void setValidationQueryTimeout(int validationQueryTimeout); + + /** + * Return the name of the optional validator class - may be null. + * + * @return the name of the optional validator class - may be null + */ + String getValidatorClassName(); + + /** + * Set the name for an optional validator class which will be used in place of test queries. If set to + * null, standard validation will be used. + * + * @param className the name of the optional validator class + */ + void setValidatorClassName(String className); + + /** + * @return the optional validator object - may be null + */ + Validator getValidator(); + + /** + * Sets the validator object + * If this is a non null object, it will be used as a validator instead of the validationQuery + * If this is null, remove the usage of the validator. + * @param validator The validator object + */ + void setValidator(Validator validator); + + /** + * avoid excess validation, only run validation at most at this frequency - time in milliseconds. + * If a connection is due for validation, but has been validated previously + * within this interval, it will not be validated again. + * The default value is 3000 (3 seconds). + * @return the validation interval in milliseconds + */ + long getValidationInterval(); + + /** + * avoid excess validation, only run validation at most at this frequency - time in milliseconds. + * If a connection is due for validation, but has been validated previously + * within this interval, it will not be validated again. + * The default value is 3000 (3 seconds). + * @param validationInterval the validation interval in milliseconds + */ + void setValidationInterval(long validationInterval); + + /** + * A custom query to be run when a connection is first created. The default value is null. + * This query only runs once per connection, and that is when a new connection is established to the database. + * If this value is non null, it will replace the validation query during connection creation. + * @return the init SQL used to run against the DB or null if not set + */ + String getInitSQL(); + + /** + * A custom query to be run when a connection is first created. The default value is null. + * This query only runs once per connection, and that is when a new connection is established to the database. + * If this value is non null, it will replace the validation query during connection creation. + * @param initSQL the init SQL used to run against the DB or null if no query should be executed + */ + void setInitSQL(String initSQL); + + /** + * Returns true if we should run the validation query when connecting to the database for the first time on a connection. + * Normally this is always set to false, unless one wants to use the validationQuery as an init query. + * @return true if we should run the validation query upon connect + */ + boolean isTestOnConnect(); + + /** + * Set to true if we should run the validation query when connecting to the database for the first time on a connection. + * Normally this is always set to false, unless one wants to use the validationQuery as an init query. + * Setting an {@link #setInitSQL(String)} will override this setting, as the init SQL will be used instead of the validation query + * @param testOnConnect set to true if we should run the validation query upon connect + */ + void setTestOnConnect(boolean testOnConnect); + + /** + * A semicolon separated list of classnames extending {@link org.apache.tomcat.jdbc.pool.JdbcInterceptor} class. + * These interceptors will be inserted as an interceptor into the chain of operations on a java.sql.Connection object. + * Example interceptors are {@link org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer StatementFinalizer} to close all + * used statements during the session. + * {@link org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer ResetAbandonedTimer} resets the timer upon every operation + * on the connection or a statement. + * {@link org.apache.tomcat.jdbc.pool.interceptor.ConnectionState ConnectionState} caches the auto commit, read only and catalog settings to avoid round trips to the DB. + * The default value is null. + * @return the interceptors that are used for connections. + * Example format: 'ConnectionState(useEquals=true,fast=yes);ResetAbandonedTimer' + */ + String getJdbcInterceptors(); + + /** + * A semicolon separated list of classnames extending {@link org.apache.tomcat.jdbc.pool.JdbcInterceptor} class. + * These interceptors will be inserted as an interceptor into the chain of operations on a java.sql.Connection object. + * Example interceptors are {@link org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer StatementFinalizer} to close all + * used statements during the session. + * {@link org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer ResetAbandonedTimer} resets the timer upon every operation + * on the connection or a statement. + * {@link org.apache.tomcat.jdbc.pool.interceptor.ConnectionState ConnectionState} caches the auto commit, read only and catalog settings to avoid round trips to the DB. + * The default value is null. + * @param jdbcInterceptors the interceptors that are used for connections. + * Example format: 'ConnectionState(useEquals=true,fast=yes);ResetAbandonedTimer' + */ + void setJdbcInterceptors(String jdbcInterceptors); + + /** + * Returns the {@link #getJdbcInterceptors()} as an array of objects with properties and the classes. + * @return an array of interceptors that have been configured + */ + InterceptorDefinition[] getJdbcInterceptorsAsArray(); + + + /** + * If set to true, the connection pool creates a {@link org.apache.tomcat.jdbc.pool.jmx.ConnectionPoolMBean} object + * that can be registered with JMX to receive notifications and state about the pool. + * The ConnectionPool object doesn't register itself, as there is no way to keep a static non changing ObjectName across JVM restarts. + * @return true if the mbean object will be created upon startup. + */ + boolean isJmxEnabled(); + + /** + * If set to true, the connection pool creates a {@link org.apache.tomcat.jdbc.pool.jmx.ConnectionPoolMBean} object + * that can be registered with JMX to receive notifications and state about the pool. + * The ConnectionPool object doesn't register itself, as there is no way to keep a static non changing ObjectName across JVM restarts. + * @param jmxEnabled set to to if the mbean object should be created upon startup. + */ + void setJmxEnabled(boolean jmxEnabled); + + /** + * Returns true if the pool sweeper is enabled for the connection pool. + * The pool sweeper is enabled if any settings that require async intervention in the pool are turned on + * + * boolean result = getTimeBetweenEvictionRunsMillis()>0; + * result = result && (isRemoveAbandoned() && getRemoveAbandonedTimeout()>0); + * result = result || (isTestWhileIdle() && getValidationQuery()!=null); + * return result; + * + * + * @return true if a background thread is or will be enabled for this pool + */ + boolean isPoolSweeperEnabled(); + + /** + * Set to true if you wish the ProxyConnection class to use String.equals instead of + * == when comparing method names. + * This property does not apply to added interceptors as those are configured individually. + * The default value is false. + * @return true if pool uses {@link String#equals(Object)} instead of == when comparing method names on {@link java.sql.Connection} methods + */ + boolean isUseEquals(); + + /** + * Set to true if you wish the ProxyConnection class to use String.equals instead of + * == when comparing method names. + * This property does not apply to added interceptors as those are configured individually. + * The default value is false. + * @param useEquals set to true if the pool should use {@link String#equals(Object)} instead of == + * when comparing method names on {@link java.sql.Connection} methods + */ + void setUseEquals(boolean useEquals); + + /** + * Time in milliseconds to keep this connection before reconnecting. + * When a connection is idle, returned to the pool or borrowed from the pool, the pool will + * check to see if the ((now - time-when-connected) > maxAge) has been reached, and if so, + * it reconnects. Note that the age of idle connections will only be checked if + * {@link #getTimeBetweenEvictionRunsMillis()} returns a value greater than 0. + * The default value is 0, which implies that connections will be left open and no + * age checks will be done. + * This is a useful setting for database sessions that leak memory as it ensures that the session + * will have a finite life span. + * @return the time in milliseconds a connection will be open for when used + */ + long getMaxAge(); + + /** + * Time in milliseconds to keep this connection before reconnecting. + * When a connection is idle, returned to the pool or borrowed from the pool, the pool will + * check to see if the ((now - time-when-connected) > maxAge) has been reached, and if so, + * it reconnects. Note that the age of idle connections will only be checked if + * {@link #getTimeBetweenEvictionRunsMillis()} returns a value greater than 0. + * The default value is 0, which implies that connections will be left open and no + * age checks will be done. + * This is a useful setting for database sessions that leak memory as it ensures that the session + * will have a finite life span. + * @param maxAge the time in milliseconds a connection will be open for when used + */ + void setMaxAge(long maxAge); + + /** + * Return true if a lock should be used when operations are performed on the connection object. + * Should be set to false unless you plan to have a background thread of your own doing idle and abandon checking + * such as JMX clients. If the pool sweeper is enabled, then the lock will automatically be used regardless of this setting. + * @return true if a lock is used. + */ + boolean getUseLock(); + + /** + * Set to true if a lock should be used when operations are performed on the connection object. + * Should be set to false unless you plan to have a background thread of your own doing idle and abandon checking + * such as JMX clients. If the pool sweeper is enabled, then the lock will automatically be used regardless of this setting. + * @param useLock set to true if a lock should be used on connection operations + */ + void setUseLock(boolean useLock); + + /** + * Similar to {@link #setRemoveAbandonedTimeout(int)} but instead of treating the connection + * as abandoned, and potentially closing the connection, this simply logs the warning if + * {@link #isLogAbandoned()} returns true. If this value is equal or less than 0, no suspect + * checking will be performed. Suspect checking only takes place if the timeout value is larger than 0 and + * the connection was not abandoned or if abandon check is disabled. If a connection is suspect a WARN message gets + * logged and a JMX notification gets sent once. + * @param seconds - the amount of time in seconds that has to pass before a connection is marked suspect. + */ + void setSuspectTimeout(int seconds); + + /** + * Returns the time in seconds to pass before a connection is marked an abandoned suspect. + * Any value lesser than or equal to 0 means the check is disabled. + * @return Returns the time in seconds to pass before a connection is marked an abandoned suspect. + */ + int getSuspectTimeout(); + + /** + * Injects a datasource that will be used to retrieve/create connections. + * If a data source is set, the {@link PoolConfiguration#getUrl()} and {@link PoolConfiguration#getDriverClassName()} methods are ignored + * and not used by the pool. If the {@link PoolConfiguration#getUsername()} and {@link PoolConfiguration#getPassword()} + * values are set, the method {@link javax.sql.DataSource#getConnection(String, String)} method will be called instead of the + * {@link javax.sql.DataSource#getConnection()} method. + * If the data source implements {@link javax.sql.XADataSource} the methods + * {@link javax.sql.XADataSource#getXAConnection()} and {@link javax.sql.XADataSource#getXAConnection(String,String)} + * will be invoked. + * @param ds the {@link javax.sql.DataSource} to be used for creating connections to be pooled. + */ + void setDataSource(Object ds); + + /** + * Returns a datasource, if one exists that is being used to create connections. + * This method will return null if the pool is using a {@link java.sql.Driver} + * @return the {@link javax.sql.DataSource} to be used for creating connections to be pooled or null if a Driver is used. + */ + Object getDataSource(); + + /** + * Configure the connection pool to use a DataSource according to {@link PoolConfiguration#setDataSource(Object)} + * But instead of injecting the object, specify the JNDI location. + * After a successful JNDI look, the {@link PoolConfiguration#getDataSource()} will not return null. + * @param jndiDS -the JNDI string @TODO specify the rules here. + */ + void setDataSourceJNDI(String jndiDS); + + /** + * Returns the JNDI string configured for data source usage. + * @return the JNDI string or null if not set + */ + String getDataSourceJNDI(); + + /** + * Returns true if the call {@link DataSource#getConnection(String, String) getConnection(username,password)} is + * allowed. This is used for when the pool is used by an application accessing multiple schemas. + * There is a performance impact turning this option on. + * @return true if {@link DataSource#getConnection(String, String) getConnection(username,password)} is honored, false if it is ignored. + */ + boolean isAlternateUsernameAllowed(); + + /** + * Set to true if the call {@link DataSource#getConnection(String, String) getConnection(username,password)} is + * allowed and honored.. This is used for when the pool is used by an application accessing multiple schemas. + * There is a performance impact turning this option on, even when not used due to username checks. + * @param alternateUsernameAllowed - set true if {@link DataSource#getConnection(String, String) getConnection(username,password)} is honored, + * false if it is to be ignored. + */ + void setAlternateUsernameAllowed(boolean alternateUsernameAllowed); + /** + * Set to true if you want the connection pool to commit any pending transaction when a connection is returned. + * The default value is false, as this could result in committing data. + * This parameter is only looked at if the {@link #getDefaultAutoCommit()} returns false + * @param commitOnReturn set to true if the pool should call {@link java.sql.Connection#commit()} when a connection is returned to the pool. + * Default is false + */ + void setCommitOnReturn(boolean commitOnReturn); + + /** + * @see PoolConfiguration#setCommitOnReturn(boolean) + * @return true if the pool should commit when a connection is returned to it + */ + boolean getCommitOnReturn(); + + /** + * Set to true if you want the connection pool to rollback any pending transaction when a connection is returned. + * The default value is false, as this could result in committing data. + * This parameter is only looked at if the {@link #getDefaultAutoCommit()} returns false + * @param rollbackOnReturn set to true if the pool should call {@link java.sql.Connection#rollback()} when a connection is returned to the pool. + * Default is false + */ + void setRollbackOnReturn(boolean rollbackOnReturn); + + /** + * @see PoolConfiguration#setRollbackOnReturn(boolean) + * @return true if the pool should rollback when a connection is returned to it + */ + boolean getRollbackOnReturn(); + + /** + * If set to true, the connection will be wrapped with facade that will disallow the connection to be used after + * {@link java.sql.Connection#close()} is called. If set to true, after {@link java.sql.Connection#close()} all calls except + * {@link java.sql.Connection#close()} and {@link java.sql.Connection#isClosed()} will throw an exception. + * @param useDisposableConnectionFacade true to use a facade + */ + void setUseDisposableConnectionFacade(boolean useDisposableConnectionFacade); + /** + * Returns true if this connection pool is configured to use a connection facade to prevent re-use of connection after + * {@link java.sql.Connection#close()} has been invoked + * @return true if {@link java.sql.Connection#close()} has been invoked. + */ + boolean getUseDisposableConnectionFacade(); + + /** + * Set to true if you wish that errors from validation should be logged as error messages. + * @param logValidationErrors set to true to log validation errors + */ + void setLogValidationErrors(boolean logValidationErrors); + + /** + * Returns true if errors that happen during validation will be logged + * @return true if errors that happen during validation will be logged + */ + boolean getLogValidationErrors(); + + /** + * Returns true if the pool is configured to propagate interrupt state of a thread. + * A thread waiting for a connection, can have its wait interrupted, and by default + * will clear the interrupt flag and throw a {@link PoolExhaustedException} + * @return true if the pool is configured to propagate and not clear the thread interrupt state + */ + boolean getPropagateInterruptState(); + + /** + * Configure the pool to propagate interrupt state for interrupted threads waiting for a connection + * A thread waiting for a connection, can have its wait interrupted, and by default + * will clear the interrupt flag and throw a {@link PoolExhaustedException} + * If set to true, this behavior will change, while the {@link PoolExhaustedException} is still thrown, the threads interrupted state is still set. + * @param propagateInterruptState - set to true to not clear, but propagate, a threads interrupted state. + */ + void setPropagateInterruptState(boolean propagateInterruptState); + + /** + * Set to true if you want to ignore error of connection creation while initializing the pool. + * Set to false if you want to fail the initialization of the pool by throwing exception. + * @param ignoreExceptionOnPreLoad set to true if you want to ignore error of connection creation while initializing the pool. + */ + void setIgnoreExceptionOnPreLoad(boolean ignoreExceptionOnPreLoad); + + /** + * @return true to ignore exceptions + * @see PoolConfiguration#setIgnoreExceptionOnPreLoad(boolean) + */ + boolean isIgnoreExceptionOnPreLoad(); + + /** + * Set this to true if you wish to wrap statements in order to enable equals() and hashCode() + * methods to be called on the closed statements if any statement proxy is set. + * @param useStatementFacade set to true to wrap statements + */ + void setUseStatementFacade(boolean useStatementFacade); + + /** + * Returns true if this connection pool is configured to wrap statements in order + * to enable equals() and hashCode() methods to be called on the closed statements if any + * statement proxy is set. + * @return true if the statements are wrapped + */ + boolean getUseStatementFacade(); +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolExhaustedException.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolExhaustedException.java new file mode 100644 index 0000000..15f36ab --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolExhaustedException.java @@ -0,0 +1,56 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.sql.SQLException; + +public class PoolExhaustedException extends SQLException { + + private static final long serialVersionUID = 3501536931777262475L; + + public PoolExhaustedException() { + } + + public PoolExhaustedException(String reason) { + super(reason); + } + + public PoolExhaustedException(Throwable cause) { + super(cause); + } + + public PoolExhaustedException(String reason, String SQLState) { + super(reason, SQLState); + } + + public PoolExhaustedException(String reason, Throwable cause) { + super(reason, cause); + } + + public PoolExhaustedException(String reason, String SQLState, int vendorCode) { + super(reason, SQLState, vendorCode); + } + + public PoolExhaustedException(String reason, String sqlState, Throwable cause) { + super(reason, sqlState, cause); + } + + public PoolExhaustedException(String reason, String sqlState, int vendorCode, Throwable cause) { + super(reason, sqlState, vendorCode, cause); + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java new file mode 100644 index 0000000..44bddba --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java @@ -0,0 +1,1038 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class PoolProperties implements PoolConfiguration, Cloneable, Serializable { + + private static final long serialVersionUID = -8519283440854213745L; + private static final Log log = LogFactory.getLog(PoolProperties.class); + + public static final int DEFAULT_MAX_ACTIVE = 100; + + protected static final AtomicInteger poolCounter = new AtomicInteger(0); + private volatile Properties dbProperties = new Properties(); + private volatile String url = null; + private volatile String driverClassName = null; + private volatile Boolean defaultAutoCommit = null; + private volatile Boolean defaultReadOnly = null; + private volatile int defaultTransactionIsolation = DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION; + private volatile String defaultCatalog = null; + private volatile String connectionProperties; + private volatile int initialSize = 10; + private volatile int maxActive = DEFAULT_MAX_ACTIVE; + private volatile int maxIdle = maxActive; + private volatile int minIdle = initialSize; + private volatile int maxWait = 30000; + private volatile String validationQuery; + private volatile int validationQueryTimeout = -1; + private volatile String validatorClassName; + private transient volatile Validator validator; + private volatile boolean testOnBorrow = false; + private volatile boolean testOnReturn = false; + private volatile boolean testWhileIdle = false; + private volatile int timeBetweenEvictionRunsMillis = 5000; + private volatile int numTestsPerEvictionRun; + private volatile int minEvictableIdleTimeMillis = 60000; + private volatile boolean accessToUnderlyingConnectionAllowed = true; + private volatile boolean removeAbandoned = false; + private volatile int removeAbandonedTimeout = 60; + private volatile boolean logAbandoned = false; + private volatile String name = "Tomcat Connection Pool["+(poolCounter.addAndGet(1))+"-"+System.identityHashCode(PoolProperties.class)+"]"; + private volatile String password; + private volatile String username; + private volatile long validationInterval = 3000; + private volatile boolean jmxEnabled = true; + private volatile String initSQL; + private volatile boolean testOnConnect =false; + private volatile String jdbcInterceptors=null; + private volatile boolean fairQueue = true; + private volatile boolean useEquals = true; + private volatile int abandonWhenPercentageFull = 0; + private volatile long maxAge = 0; + private volatile boolean useLock = false; + private volatile InterceptorDefinition[] interceptors = null; + private volatile int suspectTimeout = 0; + private volatile Object dataSource = null; + private volatile String dataSourceJNDI = null; + private volatile boolean alternateUsernameAllowed = false; + private volatile boolean commitOnReturn = false; + private volatile boolean rollbackOnReturn = false; + private volatile boolean useDisposableConnectionFacade = true; + private volatile boolean logValidationErrors = false; + private volatile boolean propagateInterruptState = false; + private volatile boolean ignoreExceptionOnPreLoad = false; + private volatile boolean useStatementFacade = true; + + @Override + public void setAbandonWhenPercentageFull(int percentage) { + if (percentage<0) { + abandonWhenPercentageFull = 0; + } else if (percentage>100) { + abandonWhenPercentageFull = 100; + } else { + abandonWhenPercentageFull = percentage; + } + } + + @Override + public int getAbandonWhenPercentageFull() { + return abandonWhenPercentageFull; + } + + @Override + public boolean isFairQueue() { + return fairQueue; + } + + @Override + public void setFairQueue(boolean fairQueue) { + this.fairQueue = fairQueue; + } + + @Override + public boolean isAccessToUnderlyingConnectionAllowed() { + return accessToUnderlyingConnectionAllowed; + } + + + @Override + public String getConnectionProperties() { + return connectionProperties; + } + + + @Override + public Properties getDbProperties() { + return dbProperties; + } + + + @Override + public Boolean isDefaultAutoCommit() { + return defaultAutoCommit; + } + + + @Override + public String getDefaultCatalog() { + return defaultCatalog; + } + + + @Override + public Boolean isDefaultReadOnly() { + return defaultReadOnly; + } + + + @Override + public int getDefaultTransactionIsolation() { + return defaultTransactionIsolation; + } + + + @Override + public String getDriverClassName() { + return driverClassName; + } + + + @Override + public int getInitialSize() { + return initialSize; + } + + + @Override + public boolean isLogAbandoned() { + return logAbandoned; + } + + + @Override + public int getMaxActive() { + return maxActive; + } + + + @Override + public int getMaxIdle() { + return maxIdle; + } + + + @Override + public int getMaxWait() { + return maxWait; + } + + + @Override + public int getMinEvictableIdleTimeMillis() { + return minEvictableIdleTimeMillis; + } + + + @Override + public int getMinIdle() { + return minIdle; + } + + + @Override + public String getName() { + return name; + } + + + @Override + public int getNumTestsPerEvictionRun() { + return numTestsPerEvictionRun; + } + + + @Override + public String getPassword() { + return password; + } + + + @Override + public String getPoolName() { + return getName(); + } + + + @Override + public boolean isRemoveAbandoned() { + return removeAbandoned; + } + + + @Override + public int getRemoveAbandonedTimeout() { + return removeAbandonedTimeout; + } + + + @Override + public boolean isTestOnBorrow() { + return testOnBorrow; + } + + + @Override + public boolean isTestOnReturn() { + return testOnReturn; + } + + + @Override + public boolean isTestWhileIdle() { + return testWhileIdle; + } + + + @Override + public int getTimeBetweenEvictionRunsMillis() { + return timeBetweenEvictionRunsMillis; + } + + + @Override + public String getUrl() { + return url; + } + + + @Override + public String getUsername() { + return username; + } + + + @Override + public String getValidationQuery() { + return validationQuery; + } + + @Override + public int getValidationQueryTimeout() { + return validationQueryTimeout; + } + + @Override + public void setValidationQueryTimeout(int validationQueryTimeout) { + this.validationQueryTimeout = validationQueryTimeout; + } + + + @Override + public String getValidatorClassName() { + return validatorClassName; + } + + + @Override + public Validator getValidator() { + return validator; + } + + @Override + public void setValidator(Validator validator) { + this.validator = validator; + if (validator!=null) { + this.validatorClassName = validator.getClass().getName(); + } else { + this.validatorClassName = null; + } + } + + + + @Override + public long getValidationInterval() { + return validationInterval; + } + + + @Override + public String getInitSQL() { + return initSQL; + } + + + @Override + public boolean isTestOnConnect() { + return testOnConnect; + } + + + @Override + public String getJdbcInterceptors() { + return jdbcInterceptors; + } + + + @Override + public InterceptorDefinition[] getJdbcInterceptorsAsArray() { + if (interceptors == null) { + if (jdbcInterceptors==null) { + interceptors = new InterceptorDefinition[0]; + } else { + String[] interceptorValues = jdbcInterceptors.split(";"); + InterceptorDefinition[] definitions = new InterceptorDefinition[interceptorValues.length+1]; + //always add the trap interceptor to the mix + definitions[0] = new InterceptorDefinition(TrapException.class); + for (int i=0; i validatorClass = (Class)ClassLoaderUtil.loadClass( + className, + PoolProperties.class.getClassLoader(), + Thread.currentThread().getContextClassLoader() + ); + validator = validatorClass.getConstructor().newInstance(); + } catch (ClassNotFoundException e) { + log.warn("The class "+className+" cannot be found.", e); + } catch (ClassCastException e) { + log.warn("The class "+className+" does not implement the Validator interface.", e); + } catch (IllegalAccessException e) { + log.warn("The class "+className+" or its no-arg constructor are inaccessible.", e); + } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) { + log.warn("An object of class "+className+" cannot be instantiated. Make sure that "+ + "it includes an implicit or explicit no-arg constructor.", e); + } + } + + + @Override + public void setInitSQL(String initSQL) { + this.initSQL = initSQL!=null && initSQL.trim().length()>0 ? initSQL : null; + } + + + @Override + public void setTestOnConnect(boolean testOnConnect) { + this.testOnConnect = testOnConnect; + } + + + @Override + public void setJdbcInterceptors(String jdbcInterceptors) { + this.jdbcInterceptors = jdbcInterceptors; + this.interceptors = null; + } + + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("ConnectionPool["); + try { + String[] fields = DataSourceFactory.ALL_PROPERTIES; + for (String field: fields) { + final String[] prefix = new String[] {"get","is"}; + for (int j=0; j0; + boolean result = timer && (isRemoveAbandoned() && getRemoveAbandonedTimeout()>0); + result = result || (timer && getSuspectTimeout()>0); + result = result || (timer && isTestWhileIdle()); + result = result || (timer && getMinEvictableIdleTimeMillis()>0); + result = result || (timer && getMaxAge()>0); + return result; + } + + + public static class InterceptorDefinition implements Serializable { + private static final long serialVersionUID = 1L; + protected String className; + protected Map properties = new HashMap<>(); + protected volatile Class clazz = null; + public InterceptorDefinition(String className) { + this.className = className; + } + + public InterceptorDefinition(Class cl) { + this(cl.getName()); + clazz = cl; + } + + public String getClassName() { + return className; + } + public void addProperty(String name, String value) { + InterceptorProperty p = new InterceptorProperty(name,value); + addProperty(p); + } + + public void addProperty(InterceptorProperty p) { + properties.put(p.getName(), p); + } + + public Map getProperties() { + return properties; + } + + @SuppressWarnings("unchecked") + public Class getInterceptorClass() throws ClassNotFoundException { + if (clazz==null) { + if (getClassName().indexOf('.')<0) { + if (log.isDebugEnabled()) { + log.debug("Loading interceptor class:" + PKG_PREFIX + getClassName()); + } + clazz = ClassLoaderUtil.loadClass( + PKG_PREFIX + getClassName(), + PoolProperties.class.getClassLoader(), + Thread.currentThread().getContextClassLoader() + ); + } else { + if (log.isDebugEnabled()) { + log.debug("Loading interceptor class:"+getClassName()); + } + clazz = ClassLoaderUtil.loadClass( + getClassName(), + PoolProperties.class.getClassLoader(), + Thread.currentThread().getContextClassLoader() + ); + } + } + return (Class)clazz; + } + } + + public static class InterceptorProperty implements Serializable { + private static final long serialVersionUID = 1L; + String name; + String value; + public InterceptorProperty(String name, String value) { + assert(name!=null); + this.name = name; + this.value = value; + } + public String getName() { + return name; + } + public String getValue() { + return value; + } + + public boolean getValueAsBoolean(boolean def) { + if (value==null) { + return def; + } + if ("true".equals(value)) { + return true; + } + if ("false".equals(value)) { + return false; + } + return def; + } + + public int getValueAsInt(int def) { + if (value==null) { + return def; + } + try { + int v = Integer.parseInt(value); + return v; + }catch (NumberFormatException nfe) { + return def; + } + } + + public long getValueAsLong(long def) { + if (value==null) { + return def; + } + try { + return Long.parseLong(value); + }catch (NumberFormatException nfe) { + return def; + } + } + + public byte getValueAsByte(byte def) { + if (value==null) { + return def; + } + try { + return Byte.parseByte(value); + }catch (NumberFormatException nfe) { + return def; + } + } + + public short getValueAsShort(short def) { + if (value==null) { + return def; + } + try { + return Short.parseShort(value); + }catch (NumberFormatException nfe) { + return def; + } + } + + public float getValueAsFloat(float def) { + if (value==null) { + return def; + } + try { + return Float.parseFloat(value); + }catch (NumberFormatException nfe) { + return def; + } + } + + public double getValueAsDouble(double def) { + if (value==null) { + return def; + } + try { + return Double.parseDouble(value); + }catch (NumberFormatException nfe) { + return def; + } + } + + public char getValueAschar(char def) { + if (value==null) { + return def; + } + try { + return value.charAt(0); + }catch (StringIndexOutOfBoundsException nfe) { + return def; + } + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o==this) { + return true; + } + if (o instanceof InterceptorProperty) { + InterceptorProperty other = (InterceptorProperty)o; + return other.name.equals(this.name); + } + return false; + } + } + + + @Override + public boolean isUseEquals() { + return useEquals; + } + + + @Override + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + + @Override + public long getMaxAge() { + return maxAge; + } + + + @Override + public void setMaxAge(long maxAge) { + this.maxAge = maxAge; + } + + + @Override + public boolean getUseLock() { + return useLock; + } + + + @Override + public void setUseLock(boolean useLock) { + this.useLock = useLock; + } + + + @Override + public void setDataSource(Object ds) { + if (ds instanceof DataSourceProxy) { + throw new IllegalArgumentException("Layered pools are not allowed."); + } + this.dataSource = ds; + } + + @Override + public Object getDataSource() { + return dataSource; + } + + + @Override + public void setDataSourceJNDI(String jndiDS) { + this.dataSourceJNDI = jndiDS; + } + + @Override + public String getDataSourceJNDI() { + return this.dataSourceJNDI; + } + + + public static Properties getProperties(String propText, Properties props) { + if (props==null) { + props = new Properties(); + } + if (propText != null) { + try { + props.load(new ByteArrayInputStream(propText.replace(';', '\n').getBytes())); + }catch (IOException x) { + throw new RuntimeException(x); + } + } + return props; + } + + @Override + public boolean isAlternateUsernameAllowed() { + return alternateUsernameAllowed; + } + + @Override + public void setAlternateUsernameAllowed(boolean alternateUsernameAllowed) { + this.alternateUsernameAllowed = alternateUsernameAllowed; + } + + + @Override + public void setCommitOnReturn(boolean commitOnReturn) { + this.commitOnReturn = commitOnReturn; + } + + @Override + public boolean getCommitOnReturn() { + return this.commitOnReturn; + } + + @Override + public void setRollbackOnReturn(boolean rollbackOnReturn) { + this.rollbackOnReturn = rollbackOnReturn; + } + + @Override + public boolean getRollbackOnReturn() { + return this.rollbackOnReturn; + } + + @Override + public void setUseDisposableConnectionFacade(boolean useDisposableConnectionFacade) { + this.useDisposableConnectionFacade = useDisposableConnectionFacade; + } + + @Override + public boolean getUseDisposableConnectionFacade() { + return useDisposableConnectionFacade; + } + + @Override + public void setLogValidationErrors(boolean logValidationErrors) { + this.logValidationErrors = logValidationErrors; + } + + @Override + public boolean getLogValidationErrors() { + return this.logValidationErrors; + } + + @Override + public boolean getPropagateInterruptState() { + return propagateInterruptState; + } + + @Override + public void setPropagateInterruptState(boolean propagateInterruptState) { + this.propagateInterruptState = propagateInterruptState; + } + + @Override + public boolean isIgnoreExceptionOnPreLoad() { + return ignoreExceptionOnPreLoad; + } + + @Override + public void setIgnoreExceptionOnPreLoad(boolean ignoreExceptionOnPreLoad) { + this.ignoreExceptionOnPreLoad = ignoreExceptionOnPreLoad; + } + + @Override + public boolean getUseStatementFacade() { + return useStatementFacade; + } + + @Override + public void setUseStatementFacade(boolean useStatementFacade) { + this.useStatementFacade = useStatementFacade; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + // TODO Auto-generated method stub + return super.clone(); + } + + + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolUtilities.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolUtilities.java new file mode 100644 index 0000000..0da0d0b --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolUtilities.java @@ -0,0 +1,38 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.util.Properties; + +public class PoolUtilities { + + public static final String PROP_USER = "user"; + + public static final String PROP_PASSWORD = "password"; + + public static Properties clone(Properties p) { + Properties c = new Properties(); + c.putAll(p); + return c; + } + + public static Properties cloneWithoutPassword(Properties p) { + Properties result = clone(p); + result.remove(PROP_PASSWORD); + return result; + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java new file mode 100644 index 0000000..0326e0c --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java @@ -0,0 +1,908 @@ +/* + * 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.tomcat.jdbc.pool; + + +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.interceptor.ConnectionState; +import org.apache.tomcat.jdbc.pool.jmx.JmxUtil; + +/** + * Represents a pooled connection + * and holds a reference to the {@link java.sql.Connection} object + */ +public class PooledConnection implements PooledConnectionMBean { + /** + * Logger + */ + private static final Log log = LogFactory.getLog(PooledConnection.class); + + public static final String PROP_USER = PoolUtilities.PROP_USER; + + public static final String PROP_PASSWORD = PoolUtilities.PROP_PASSWORD; + + /** + * Validate when connection is borrowed flag + */ + public static final int VALIDATE_BORROW = 1; + /** + * Validate when connection is returned flag + */ + public static final int VALIDATE_RETURN = 2; + /** + * Validate when connection is idle flag + */ + public static final int VALIDATE_IDLE = 3; + /** + * Validate when connection is initialized flag + */ + public static final int VALIDATE_INIT = 4; + /** + * The properties for the connection pool + */ + protected PoolConfiguration poolProperties; + /** + * The underlying database connection + */ + private volatile java.sql.Connection connection; + + /** + * If using a XAConnection underneath. + */ + protected volatile javax.sql.XAConnection xaConnection; + /** + * When we track abandon traces, this string holds the thread dump + */ + private String abandonTrace = null; + /** + * Timestamp the connection was last 'touched' by the pool + */ + private volatile long timestamp; + /** + * Lock for this connection only + */ + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false); + /** + * Set to true if this connection has been discarded by the pool + */ + private volatile boolean discarded = false; + /** + * The Timestamp when the last time the connect() method was called successfully + */ + private volatile long lastConnected = -1; + /** + * timestamp to keep track of validation intervals + */ + private volatile long lastValidated = System.currentTimeMillis(); + /** + * The parent + */ + protected ConnectionPool parent; + + private HashMap attributes = new HashMap<>(); + + private volatile long connectionVersion=0; + + private static final AtomicLong connectionIndex = new AtomicLong(0); + + private ObjectName oname = null; + + /** + * Weak reference to cache the list of interceptors for this connection + * so that we don't create a new list of interceptors each time we borrow + * the connection + */ + private volatile JdbcInterceptor handler = null; + + private AtomicBoolean released = new AtomicBoolean(false); + + private volatile boolean suspect = false; + + private java.sql.Driver driver = null; + + /** + * Constructor + * @param prop - pool properties + * @param parent - the parent connection pool + */ + public PooledConnection(PoolConfiguration prop, ConnectionPool parent) { + poolProperties = prop; + this.parent = parent; + connectionVersion = parent.getPoolVersion(); + } + + @Override + public long getConnectionVersion() { + return connectionVersion; + } + + /** + * @deprecated use {@link #shouldForceReconnect(String, String)} + * method kept since it was public, to avoid changing interface. + * @param username The user name + * @param password The password + * @return trueif the pool does not need to reconnect + */ + @Deprecated + public boolean checkUser(String username, String password) { + return !shouldForceReconnect(username, password); + } + + /** + * Returns true if we must force reconnect based on credentials passed in. + * Returns false if {@link PoolConfiguration#isAlternateUsernameAllowed()} method returns false. + * Returns false if the username/password has not changed since this connection was connected + * @param username the username you wish to connect with, pass in null to accept the default username from {@link PoolConfiguration#getUsername()} + * @param password the password you wish to connect with, pass in null to accept the default username from {@link org.apache.tomcat.jdbc.pool.PoolConfiguration#getPassword()} + * @return true is the pool must reconnect + */ + public boolean shouldForceReconnect(String username, String password) { + + if (!getPoolProperties().isAlternateUsernameAllowed()) { + return false; + } + + if (username==null) { + username = poolProperties.getUsername(); + } + if (password==null) { + password = poolProperties.getPassword(); + } + + String storedUsr = (String)getAttributes().get(PROP_USER); + String storedPwd = (String)getAttributes().get(PROP_PASSWORD); + + boolean noChangeInCredentials = (username==null && storedUsr==null); + noChangeInCredentials = (noChangeInCredentials || (username!=null && username.equals(storedUsr))); + + noChangeInCredentials = noChangeInCredentials && ((password==null && storedPwd==null) || (password!=null && password.equals(storedPwd))); + + if (username==null) { + getAttributes().remove(PROP_USER); + } else { + getAttributes().put(PROP_USER, username); + } + if (password==null) { + getAttributes().remove(PROP_PASSWORD); + } else { + getAttributes().put(PROP_PASSWORD, password); + } + + return !noChangeInCredentials; + } + + /** + * Connects the underlying connection to the database. + * @throws SQLException if the method {@link #release()} has been called. + * @throws SQLException if driver instantiation fails + * @throws SQLException if a call to {@link java.sql.Driver#connect(String, java.util.Properties)} fails. + * @throws SQLException if default properties are configured and a call to + * {@link java.sql.Connection#setAutoCommit(boolean)}, {@link java.sql.Connection#setCatalog(String)}, + * {@link java.sql.Connection#setTransactionIsolation(int)} or {@link java.sql.Connection#setReadOnly(boolean)} fails. + */ + public void connect() throws SQLException { + if (released.get()) { + throw new SQLException("A connection once released, can't be reestablished."); + } + if (connection != null) { + try { + this.disconnect(false); + } catch (Exception x) { + log.debug("Unable to disconnect previous connection.", x); + } //catch + } //end if + //if (poolProperties.getDataSource()==null && poolProperties.getDataSourceJNDI()!=null) { + //TODO lookup JNDI name + //} + + if (poolProperties.getDataSource()!=null) { + connectUsingDataSource(); + } else { + connectUsingDriver(); + } + + //set up the default state, unless we expect the interceptor to do it + if (poolProperties.getJdbcInterceptors()==null || poolProperties.getJdbcInterceptors().indexOf(ConnectionState.class.getName())<0 || + poolProperties.getJdbcInterceptors().indexOf(ConnectionState.class.getSimpleName())<0) { + if (poolProperties.getDefaultTransactionIsolation()!=DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION) { + connection.setTransactionIsolation(poolProperties.getDefaultTransactionIsolation()); + } + if (poolProperties.getDefaultReadOnly()!=null) { + connection.setReadOnly(poolProperties.getDefaultReadOnly().booleanValue()); + } + if (poolProperties.getDefaultAutoCommit()!=null) { + connection.setAutoCommit(poolProperties.getDefaultAutoCommit().booleanValue()); + } + if (poolProperties.getDefaultCatalog()!=null) { + connection.setCatalog(poolProperties.getDefaultCatalog()); + } + } + this.discarded = false; + this.lastConnected = System.currentTimeMillis(); + } + + protected void connectUsingDataSource() throws SQLException { + String usr = null; + String pwd = null; + if (getAttributes().containsKey(PROP_USER)) { + usr = (String) getAttributes().get(PROP_USER); + } else { + usr = poolProperties.getUsername(); + getAttributes().put(PROP_USER, usr); + } + if (getAttributes().containsKey(PROP_PASSWORD)) { + pwd = (String) getAttributes().get(PROP_PASSWORD); + } else { + pwd = poolProperties.getPassword(); + getAttributes().put(PROP_PASSWORD, pwd); + } + if (poolProperties.getDataSource() instanceof javax.sql.XADataSource) { + javax.sql.XADataSource xds = (javax.sql.XADataSource)poolProperties.getDataSource(); + if (usr!=null && pwd!=null) { + xaConnection = xds.getXAConnection(usr, pwd); + connection = xaConnection.getConnection(); + } else { + xaConnection = xds.getXAConnection(); + connection = xaConnection.getConnection(); + } + } else if (poolProperties.getDataSource() instanceof javax.sql.DataSource){ + javax.sql.DataSource ds = (javax.sql.DataSource)poolProperties.getDataSource(); + if (usr!=null && pwd!=null) { + connection = ds.getConnection(usr, pwd); + } else { + connection = ds.getConnection(); + } + } else if (poolProperties.getDataSource() instanceof javax.sql.ConnectionPoolDataSource){ + javax.sql.ConnectionPoolDataSource ds = (javax.sql.ConnectionPoolDataSource)poolProperties.getDataSource(); + if (usr!=null && pwd!=null) { + connection = ds.getPooledConnection(usr, pwd).getConnection(); + } else { + connection = ds.getPooledConnection().getConnection(); + } + } else { + throw new SQLException("DataSource is of unknown class:"+(poolProperties.getDataSource()!=null?poolProperties.getDataSource().getClass():"null")); + } + } + protected void connectUsingDriver() throws SQLException { + + try { + if (driver==null) { + if (log.isDebugEnabled()) { + log.debug("Instantiating driver using class: "+poolProperties.getDriverClassName()+" [url="+poolProperties.getUrl()+"]"); + } + if (poolProperties.getDriverClassName()==null) { + //rely on DriverManager + log.warn("Not loading a JDBC driver as driverClassName property is null."); + } else { + driver = (java.sql.Driver) + ClassLoaderUtil.loadClass( + poolProperties.getDriverClassName(), + PooledConnection.class.getClassLoader(), + Thread.currentThread().getContextClassLoader() + ).getConstructor().newInstance(); + } + } + } catch (Exception cn) { + if (log.isDebugEnabled()) { + log.debug("Unable to instantiate JDBC driver.", cn); + } + SQLException ex = new SQLException(cn.getMessage()); + ex.initCause(cn); + throw ex; + } + String driverURL = poolProperties.getUrl(); + String usr = null; + String pwd = null; + if (getAttributes().containsKey(PROP_USER)) { + usr = (String) getAttributes().get(PROP_USER); + } else { + usr = poolProperties.getUsername(); + getAttributes().put(PROP_USER, usr); + } + if (getAttributes().containsKey(PROP_PASSWORD)) { + pwd = (String) getAttributes().get(PROP_PASSWORD); + } else { + pwd = poolProperties.getPassword(); + getAttributes().put(PROP_PASSWORD, pwd); + } + Properties properties = PoolUtilities.clone(poolProperties.getDbProperties()); + if (usr != null) { + properties.setProperty(PROP_USER, usr); + } + if (pwd != null) { + properties.setProperty(PROP_PASSWORD, pwd); + } + + try { + if (driver==null) { + connection = DriverManager.getConnection(driverURL, properties); + } else { + connection = driver.connect(driverURL, properties); + } + } catch (Exception x) { + if (log.isDebugEnabled()) { + log.debug("Unable to connect to database.", x); + } + if (parent.jmxPool!=null) { + parent.jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_CONNECT, + ConnectionPool.getStackTrace(x)); + } + if (x instanceof SQLException) { + throw (SQLException)x; + } else { + SQLException ex = new SQLException(x.getMessage()); + ex.initCause(x); + throw ex; + } + } + if (connection==null) { + throw new SQLException("Driver:"+driver+" returned null for URL:"+driverURL); + } + } + + /** + * @return true if connect() was called successfully and disconnect has not yet been called + */ + @Override + public boolean isInitialized() { + return connection!=null; + } + + /** + * Returns true if the connection has been connected more than + * {@link PoolConfiguration#getMaxAge()} milliseconds. false otherwise. + * @return Returns true if the connection has been connected more than + * {@link PoolConfiguration#getMaxAge()} milliseconds. false otherwise. + */ + @Override + public boolean isMaxAgeExpired() { + if (getPoolProperties().getMaxAge()>0 ) { + return (System.currentTimeMillis() - getLastConnected()) > getPoolProperties().getMaxAge(); + } else { + return false; + } + } + /** + * Issues a call to {@link #disconnect(boolean)} with the argument false followed by a call to + * {@link #connect()} + * @throws SQLException if the call to {@link #connect()} fails. + */ + public void reconnect() throws SQLException { + this.disconnect(false); + this.connect(); + } //reconnect + + /** + * Disconnects the connection. All exceptions are logged using debug level. + * @param finalize if set to true, a call to {@link ConnectionPool#finalize(PooledConnection)} is called. + */ + private void disconnect(boolean finalize) { + if (isDiscarded() && connection == null) { + return; + } + setDiscarded(true); + if (connection != null) { + try { + parent.disconnectEvent(this, finalize); + if (xaConnection == null) { + connection.close(); + } else { + xaConnection.close(); + } + }catch (Exception ignore) { + if (log.isDebugEnabled()) { + log.debug("Unable to close underlying SQL connection",ignore); + } + } + } + connection = null; + xaConnection = null; + lastConnected = -1; + if (finalize) { + parent.finalize(this); + } + } + + +//============================================================================ +// +//============================================================================ + + /** + * Returns abandon timeout in milliseconds + * @return abandon timeout in milliseconds + */ + public long getAbandonTimeout() { + if (poolProperties.getRemoveAbandonedTimeout() <= 0) { + return Long.MAX_VALUE; + } else { + return poolProperties.getRemoveAbandonedTimeout() * 1000L; + } //end if + } + + /** + * Returns true if the connection pool is configured + * to do validation for a certain action. + * @param action The validation action + */ + private boolean doValidate(int action) { + if (action == VALIDATE_BORROW && poolProperties.isTestOnBorrow()) { + return true; + } else if (action == VALIDATE_RETURN && poolProperties.isTestOnReturn()) { + return true; + } else if (action == VALIDATE_IDLE && poolProperties.isTestWhileIdle()) { + return true; + } else if (action == VALIDATE_INIT && poolProperties.isTestOnConnect()) { + return true; + } else if (action == VALIDATE_INIT && poolProperties.getInitSQL()!=null) { + return true; + } else { + return false; + } + } + + /** + * Returns true if the object is still valid. if not + * the pool will call the getExpiredAction() and follow up with one + * of the four expired methods + * @param validateAction The value + * @return true if the connection is valid + */ + public boolean validate(int validateAction) { + return validate(validateAction,null); + } + + /** + * Validates a connection. + * @param validateAction the action used. One of {@link #VALIDATE_BORROW}, {@link #VALIDATE_IDLE}, + * {@link #VALIDATE_INIT} or {@link #VALIDATE_RETURN} + * @param sql the SQL to be used during validation. If the {@link PoolConfiguration#setInitSQL(String)} has been called with a non null + * value and the action is {@link #VALIDATE_INIT} the init SQL will be used for validation. + * + * @return true if the connection was validated successfully. It returns true even if validation was not performed, such as when + * {@link PoolConfiguration#setValidationInterval(long)} has been called with a positive value. + *

    + * false if the validation failed. The caller should close the connection if false is returned since a session could have been left in + * an unknown state during initialization. + */ + public boolean validate(int validateAction,String sql) { + if (this.isDiscarded()) { + return false; + } + + if (!doValidate(validateAction)) { + //no validation required, no init sql and props not set + return true; + } + + //Don't bother validating if already have recently enough + long now = System.currentTimeMillis(); + if (validateAction!=VALIDATE_INIT && + poolProperties.getValidationInterval() > 0 && + (now - this.lastValidated) < + poolProperties.getValidationInterval()) { + return true; + } + + if (poolProperties.getValidator() != null) { + if (poolProperties.getValidator().validate(connection, validateAction)) { + this.lastValidated = now; + return true; + } else { + if (getPoolProperties().getLogValidationErrors()) { + log.error("Custom validation through "+poolProperties.getValidator()+" failed."); + } + return false; + } + } + + String query = sql; + + if (validateAction == VALIDATE_INIT && poolProperties.getInitSQL() != null) { + query = poolProperties.getInitSQL(); + } + + if (query == null) { + query = poolProperties.getValidationQuery(); + } + + if (query == null) { + boolean transactionCommitted = false; + int validationQueryTimeout = poolProperties.getValidationQueryTimeout(); + if (validationQueryTimeout < 0) { + validationQueryTimeout = 0; + } + try { + if (connection.isValid(validationQueryTimeout)) { + this.lastValidated = now; + transactionCommitted = silentlyCommitTransactionIfNeeded(); + return true; + } else { + if (getPoolProperties().getLogValidationErrors()) { + log.error("isValid() returned false."); + } + return false; + } + } catch (SQLException e) { + if (getPoolProperties().getLogValidationErrors()) { + log.error("isValid() failed.", e); + } else if (log.isDebugEnabled()) { + log.debug("isValid() failed.", e); + } + return false; + } finally { + if (!transactionCommitted) { + silentlyRollbackTransactionIfNeeded(); + } + } + } + + boolean transactionCommitted = false; + Statement stmt = null; + try { + stmt = connection.createStatement(); + + int validationQueryTimeout = poolProperties.getValidationQueryTimeout(); + if (validationQueryTimeout > 0) { + stmt.setQueryTimeout(validationQueryTimeout); + } + + stmt.execute(query); + stmt.close(); + this.lastValidated = now; + transactionCommitted = silentlyCommitTransactionIfNeeded(); + return true; + } catch (Exception ex) { + if (getPoolProperties().getLogValidationErrors()) { + log.error("SQL Validation error", ex); + } else if (log.isDebugEnabled()) { + log.debug("Unable to validate object:",ex); + } + if (stmt!=null) { + try { stmt.close();} catch (Exception ignore2){/*NOOP*/} + } + + } finally { + if (!transactionCommitted) { + silentlyRollbackTransactionIfNeeded(); + } + } + return false; + } //validate + + + private boolean silentlyCommitTransactionIfNeeded() { + try { + if (!connection.getAutoCommit()) { + connection.commit(); + } + return true; + } catch (SQLException e) { + log.debug("Failed to commit transaction", e); + } + return false; + } + + + private boolean silentlyRollbackTransactionIfNeeded() { + try { + if (!connection.getAutoCommit()) { + connection.rollback(); + } + return true; + } catch (SQLException e) { + log.debug("Failed to rollback transaction", e); + } + return false; + } + + + /** + * The time limit for how long the object + * can remain unused before it is released + * @return {@link PoolConfiguration#getMinEvictableIdleTimeMillis()} + */ + public long getReleaseTime() { + return this.poolProperties.getMinEvictableIdleTimeMillis(); + } + + /** + * This method is called if (Now - timeCheckedIn > getReleaseTime()) + * This method disconnects the connection, logs an error in debug mode if it happens + * then sets the {@link #released} flag to false. Any attempts to connect this cached object again + * will fail per {@link #connect()} + * The connection pool uses the atomic return value to decrement the pool size counter. + * @return true if this is the first time this method has been called. false if this method has been called before. + */ + public boolean release() { + try { + disconnect(true); + } catch (Exception x) { + if (log.isDebugEnabled()) { + log.debug("Unable to close SQL connection",x); + } + } + if (oname != null) { + JmxUtil.unregisterJmx(oname); + oname = null; + } + return released.compareAndSet(false, true); + + } + + /** + * The pool will set the stack trace when it is check out and + * checked in + * @param trace the stack trace for this connection + */ + + public void setStackTrace(String trace) { + abandonTrace = trace; + } + + /** + * Returns the stack trace from when this connection was borrowed. Can return null if no stack trace was set. + * @return the stack trace or null of no trace was set + */ + public String getStackTrace() { + return abandonTrace; + } + + /** + * Sets a timestamp on this connection. A timestamp usually means that some operation + * performed successfully. + * @param timestamp the timestamp as defined by {@link System#currentTimeMillis()} + */ + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + setSuspect(false); + } + + @Override + public boolean isSuspect() { + return suspect; + } + + public void setSuspect(boolean suspect) { + this.suspect = suspect; + } + + /** + * An interceptor can call this method with the value true, and the connection will be closed when it is returned to the pool. + * @param discarded - only valid value is true + * @throws IllegalStateException if this method is called with the value false and the value true has already been set. + */ + public void setDiscarded(boolean discarded) { + if (this.discarded && !discarded) { + throw new IllegalStateException("Unable to change the state once the connection has been discarded"); + } + this.discarded = discarded; + } + + /** + * Set the timestamp the connection was last validated. + * This flag is used to keep track when we are using a {@link PoolConfiguration#setValidationInterval(long) validation-interval}. + * @param lastValidated a timestamp as defined by {@link System#currentTimeMillis()} + */ + public void setLastValidated(long lastValidated) { + this.lastValidated = lastValidated; + } + + /** + * Sets the pool configuration for this connection and connection pool. + * Object is shared with the {@link ConnectionPool} + * @param poolProperties The pool properties + */ + public void setPoolProperties(PoolConfiguration poolProperties) { + this.poolProperties = poolProperties; + } + + /** + * Return the timestamps of last pool action. Timestamps are typically set when connections + * are borrowed from the pool. It is used to keep track of {@link PoolConfiguration#setRemoveAbandonedTimeout(int) abandon-timeouts}. + * This timestamp can also be reset by the {@link org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer#invoke(Object, java.lang.reflect.Method, Object[])} + * @return the timestamp of the last pool action as defined by {@link System#currentTimeMillis()} + */ + @Override + public long getTimestamp() { + return timestamp; + } + + /** + * Returns the discarded flag. + * @return the discarded flag. If the value is true, + * either {@link #disconnect(boolean)} has been called or it will be called when the connection is returned to the pool. + */ + @Override + public boolean isDiscarded() { + return discarded; + } + + /** + * Returns the timestamp of the last successful validation query execution. + * @return the timestamp of the last successful validation query execution as defined by {@link System#currentTimeMillis()} + */ + @Override + public long getLastValidated() { + return lastValidated; + } + + /** + * Returns the configuration for this connection and pool + * @return the configuration for this connection and pool + */ + public PoolConfiguration getPoolProperties() { + return poolProperties; + } + + /** + * Locks the connection only if either {@link PoolConfiguration#isPoolSweeperEnabled()} or + * {@link PoolConfiguration#getUseLock()} return true. The per connection lock ensures thread safety is + * multiple threads are performing operations on the connection. + * Otherwise this is a noop for performance + */ + public void lock() { + if (poolProperties.getUseLock() || this.poolProperties.isPoolSweeperEnabled()) { + //optimized, only use a lock when there is concurrency + lock.writeLock().lock(); + } + } + + /** + * Unlocks the connection only if the sweeper is enabled + * Otherwise this is a noop for performance + */ + public void unlock() { + if (poolProperties.getUseLock() || this.poolProperties.isPoolSweeperEnabled()) { + //optimized, only use a lock when there is concurrency + lock.writeLock().unlock(); + } + } + + /** + * Returns the underlying connection + * @return the underlying JDBC connection as it was returned from the JDBC driver + * @see javax.sql.PooledConnection#getConnection() + */ + public java.sql.Connection getConnection() { + return this.connection; + } + + /** + * Returns the underlying XA connection + * @return the underlying XA connection as it was returned from the Datasource + */ + public javax.sql.XAConnection getXAConnection() { + return this.xaConnection; + } + + + /** + * Returns the timestamp of when the connection was last connected to the database. + * ie, a successful call to {@link java.sql.Driver#connect(String, java.util.Properties)}. + * @return the timestamp when this connection was created as defined by {@link System#currentTimeMillis()} + */ + @Override + public long getLastConnected() { + return lastConnected; + } + + /** + * Returns the first handler in the interceptor chain + * @return the first interceptor for this connection + */ + public JdbcInterceptor getHandler() { + return handler; + } + + public void setHandler(JdbcInterceptor handler) { + if (this.handler!=null && this.handler!=handler) { + JdbcInterceptor interceptor = this.handler; + while (interceptor!=null) { + interceptor.reset(null, null); + interceptor = interceptor.getNext(); + }//while + }//end if + this.handler = handler; + } + + @Override + public String toString() { + return "PooledConnection["+(connection!=null?connection.toString():"null")+"]"; + } + + /** + * Returns true if this connection has been released and wont be reused. + * @return true if the method {@link #release()} has been called + */ + @Override + public boolean isReleased() { + return released.get(); + } + + public HashMap getAttributes() { + return attributes; + } + + public void createMBean() { + if (oname != null) { + return; + } + String keyprop = ",connections=PooledConnection["+connectionIndex.getAndIncrement()+"]"; + oname = JmxUtil.registerJmx(parent.getJmxPool().getObjectName(), keyprop, this); + } + + public ObjectName getObjectName() { + return oname; + } + + @Override + public void clearWarnings() { + try { + connection.clearWarnings(); + } catch (SQLException e) { + log.warn("Unable to clear Warnings, connection will be closed.", e); + } + } + + @Override + public boolean isClosed() throws SQLException { + return connection.isClosed(); + } + + @Override + public boolean getAutoCommit() throws SQLException { + return connection.getAutoCommit(); + } + + @Override + public String getCatalog() throws SQLException { + return connection.getCatalog(); + } + + @Override + public int getHoldability() throws SQLException { + return connection.getHoldability(); + } + + @Override + public boolean isReadOnly() throws SQLException { + return connection.isReadOnly(); + } + + @Override + public String getSchema() throws SQLException { + return connection.getSchema(); + } + + @Override + public int getTransactionIsolation() throws SQLException { + return connection.getTransactionIsolation(); + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnectionMBean.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnectionMBean.java new file mode 100644 index 0000000..16efe7e --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnectionMBean.java @@ -0,0 +1,42 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.sql.SQLException; + +public interface PooledConnectionMBean { + // PooledConnection + long getConnectionVersion(); + boolean isInitialized(); + boolean isMaxAgeExpired(); + boolean isSuspect(); + long getTimestamp(); + boolean isDiscarded(); + long getLastValidated(); + long getLastConnected(); + boolean isReleased(); + + // java.sql.Connection + void clearWarnings(); + boolean isClosed() throws SQLException; + boolean getAutoCommit() throws SQLException; + String getCatalog() throws SQLException; + int getHoldability() throws SQLException; + boolean isReadOnly() throws SQLException; + String getSchema() throws SQLException; + int getTransactionIsolation() throws SQLException; +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ProxyConnection.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ProxyConnection.java new file mode 100644 index 0000000..04fc32f --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ProxyConnection.java @@ -0,0 +1,161 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.SQLException; + +import javax.sql.XAConnection; +/** + * A ProxyConnection object is the bottom most interceptor that wraps an object of type + * {@link PooledConnection}. The ProxyConnection intercepts three methods: + *

      + *
    • {@link java.sql.Connection#close()} - returns the connection to the pool. May be called multiple times.
    • + *
    • {@link java.lang.Object#toString()} - returns a custom string for this object
    • + *
    • {@link javax.sql.PooledConnection#getConnection()} - returns the underlying connection
    • + *
    + * By default method comparisons is done on a String reference level, unless the {@link PoolConfiguration#setUseEquals(boolean)} has been called + * with a true argument. + */ +public class ProxyConnection extends JdbcInterceptor { + + protected PooledConnection connection = null; + + protected ConnectionPool pool = null; + + public PooledConnection getConnection() { + return connection; + } + + public void setConnection(PooledConnection connection) { + this.connection = connection; + } + + public ConnectionPool getPool() { + return pool; + } + + public void setPool(ConnectionPool pool) { + this.pool = pool; + } + + protected ProxyConnection(ConnectionPool parent, PooledConnection con, + boolean useEquals) { + pool = parent; + connection = con; + setUseEquals(useEquals); + } + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + this.pool = parent; + this.connection = con; + } + + public boolean isWrapperFor(Class iface) { + if (iface == XAConnection.class && connection.getXAConnection()!=null) { + return true; + } else { + return iface.isInstance(connection.getConnection()); + } + } + + + public Object unwrap(Class iface) throws SQLException { + if (iface == PooledConnection.class) { + return connection; + }else if (iface == XAConnection.class) { + return connection.getXAConnection(); + } else if (isWrapperFor(iface)) { + return connection.getConnection(); + } else { + throw new SQLException("Not a wrapper of "+iface.getName()); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (compare(ISCLOSED_VAL,method)) { + return Boolean.valueOf(isClosed()); + } + if (compare(CLOSE_VAL,method)) { + if (connection==null) + { + return null; //noop for already closed. + } + PooledConnection poolc = this.connection; + this.connection = null; + pool.returnConnection(poolc); + return null; + } else if (compare(TOSTRING_VAL,method)) { + return this.toString(); + } else if (compare(GETCONNECTION_VAL,method) && connection!=null) { + return connection.getConnection(); + } else if (method.getDeclaringClass().isAssignableFrom(XAConnection.class) && connection != null) { + try { + return method.invoke(connection.getXAConnection(),args); + }catch (Throwable t) { + if (t instanceof InvocationTargetException) { + throw t.getCause() != null ? t.getCause() : t; + } else { + throw t; + } + } + } + if (isClosed()) { + throw new SQLException("Connection has already been closed."); + } + if (compare(UNWRAP_VAL,method)) { + return unwrap((Class)args[0]); + } else if (compare(ISWRAPPERFOR_VAL,method)) { + return Boolean.valueOf(this.isWrapperFor((Class)args[0])); + } + try { + PooledConnection poolc = connection; + if (poolc!=null) { + return method.invoke(poolc.getConnection(),args); + } else { + throw new SQLException("Connection has already been closed."); + } + }catch (Throwable t) { + if (t instanceof InvocationTargetException) { + throw t.getCause() != null ? t.getCause() : t; + } else { + throw t; + } + } + } + + public boolean isClosed() { + return connection==null || connection.isDiscarded(); + } + + public PooledConnection getDelegateConnection() { + return connection; + } + + public ConnectionPool getParentPool() { + return pool; + } + + @Override + public String toString() { + return "ProxyConnection["+(connection!=null?connection.toString():"null")+"]"; + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/StatementFacade.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/StatementFacade.java new file mode 100644 index 0000000..4771b32 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/StatementFacade.java @@ -0,0 +1,157 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.interceptor.AbstractCreateStatementInterceptor; + +public class StatementFacade extends AbstractCreateStatementInterceptor { + + private static final Log logger = LogFactory.getLog(StatementFacade.class); + + protected StatementFacade(JdbcInterceptor interceptor) { + setUseEquals(interceptor.isUseEquals()); + setNext(interceptor); + } + + @Override + public void closeInvoked() { + // nothing to do + } + + /** + * Creates a statement interceptor to monitor query response times + */ + @Override + public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) { + try { + String name = method.getName(); + Constructor constructor = null; + String sql = null; + if (compare(CREATE_STATEMENT, name)) { + // createStatement + constructor = getConstructor(CREATE_STATEMENT_IDX, Statement.class); + } else if (compare(PREPARE_STATEMENT, name)) { + // prepareStatement + constructor = getConstructor(PREPARE_STATEMENT_IDX, PreparedStatement.class); + sql = (String)args[0]; + } else if (compare(PREPARE_CALL, name)) { + // prepareCall + constructor = getConstructor(PREPARE_CALL_IDX, CallableStatement.class); + sql = (String)args[0]; + } else { + // do nothing + return statement; + } + return constructor.newInstance(new Object[] { new StatementProxy(statement,sql) }); + } catch (Exception x) { + logger.warn("Unable to create statement proxy.", x); + } + return statement; + } + + /** + * Class to measure query execute time. + */ + protected class StatementProxy implements InvocationHandler { + protected boolean closed = false; + protected Object delegate; + protected final String query; + public StatementProxy(Object parent, String query) { + this.delegate = parent; + this.query = query; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (compare(TOSTRING_VAL,method)) { + return toString(); + } + if (compare(EQUALS_VAL, method)) { + return Boolean.valueOf( + this.equals(Proxy.getInvocationHandler(args[0]))); + } + if (compare(HASHCODE_VAL, method)) { + return Integer.valueOf(this.hashCode()); + } + if (compare(CLOSE_VAL, method)) { + if (delegate == null) { + return null; + } + } + if (compare(ISCLOSED_VAL, method)) { + if (delegate == null) { + return Boolean.TRUE; + } + } + if (delegate == null) { + throw new SQLException("Statement closed."); + } + Object result = null; + try { + //invoke next + result = method.invoke(delegate,args); + } catch (Throwable t) { + if (t instanceof InvocationTargetException && t.getCause() != null) { + throw t.getCause(); + } else { + throw t; + } + } + //perform close cleanup + if (compare(CLOSE_VAL, method)) { + delegate = null; + } + return result; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return this==obj; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(StatementProxy.class.getName()); + buf.append("[Proxy="); + buf.append(hashCode()); + buf.append("; Query="); + buf.append(query); + buf.append("; Delegate="); + buf.append(delegate); + buf.append(']'); + return buf.toString(); + } + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/TrapException.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/TrapException.java new file mode 100644 index 0000000..ade3881 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/TrapException.java @@ -0,0 +1,82 @@ +/* + * 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.tomcat.jdbc.pool; + + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.SQLException; +/** + * Interceptor that traps any unhandled exception types and throws an exception that has been declared by the method + * called, or throw an SQLException if it is declared. + * If the caught exception is not declared, and the method doesn't throw SQLException, then this interceptor will + * throw a RuntimeException + * + */ +public class TrapException extends JdbcInterceptor { + + + public TrapException() { + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + return super.invoke(proxy, method, args); + }catch (Exception t) { + Throwable exception = t; + if (t instanceof InvocationTargetException && t.getCause() != null) { + exception = t.getCause(); + if (exception instanceof Error) { + throw exception; + } + } + Class exceptionClass = exception.getClass(); + if (!isDeclaredException(method, exceptionClass)) { + if (isDeclaredException(method,SQLException.class)) { + SQLException sqlx = new SQLException("Uncaught underlying exception."); + sqlx.initCause(exception); + exception = sqlx; + } else { + RuntimeException rx = new RuntimeException("Uncaught underlying exception."); + rx.initCause(exception); + exception = rx; + } + } + throw exception; + } + + } + + public boolean isDeclaredException(Method m, Class clazz) { + for (Class cl : m.getExceptionTypes()) { + if (cl.equals(clazz) || cl.isAssignableFrom(clazz)) { + return true; + } + } + return false; + } + + /** + * no-op for this interceptor. no state is stored. + */ + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + // NOOP + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/Validator.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/Validator.java new file mode 100644 index 0000000..76fc9cb --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/Validator.java @@ -0,0 +1,37 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.sql.Connection; + +/** + * Interface to be implemented by custom validator classes. + * + * @author mpassell + */ +public interface Validator { + /** + * Validate a connection and return a boolean to indicate if it's valid. + * + * @param connection the Connection object to test + * @param validateAction the action used. One of {@link PooledConnection#VALIDATE_BORROW}, + * {@link PooledConnection#VALIDATE_IDLE}, {@link PooledConnection#VALIDATE_INIT} or + * {@link PooledConnection#VALIDATE_RETURN} + * @return true if the connection is valid + */ + boolean validate(Connection connection, int validateAction); +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XADataSource.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XADataSource.java new file mode 100644 index 0000000..dfd836c --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XADataSource.java @@ -0,0 +1,36 @@ +/* + * 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.tomcat.jdbc.pool; + +public class XADataSource extends DataSource implements javax.sql.XADataSource { + + /** + * Constructor for reflection only. A default set of pool properties will be created. + */ + public XADataSource() { + super(); + } + + /** + * Constructs a DataSource object wrapping a connection + * @param poolProperties The pool configuration + */ + public XADataSource(PoolConfiguration poolProperties) { + super(poolProperties); + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java new file mode 100644 index 0000000..9e64a93 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java @@ -0,0 +1,168 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.JdbcInterceptor; +import org.apache.tomcat.jdbc.pool.PooledConnection; + +/** + * Abstraction interceptor. This component intercepts all calls to create some type of SQL statement. + * By extending this class, one can intercept queries and update statements by overriding the {@link #createStatement(Object, Method, Object[], Object, long)} + * method. + */ +public abstract class AbstractCreateStatementInterceptor extends JdbcInterceptor { + protected static final String CREATE_STATEMENT = "createStatement"; + protected static final int CREATE_STATEMENT_IDX = 0; + protected static final String PREPARE_STATEMENT = "prepareStatement"; + protected static final int PREPARE_STATEMENT_IDX = 1; + protected static final String PREPARE_CALL = "prepareCall"; + protected static final int PREPARE_CALL_IDX = 2; + + protected static final String[] STATEMENT_TYPES = {CREATE_STATEMENT, PREPARE_STATEMENT, PREPARE_CALL}; + protected static final int STATEMENT_TYPE_COUNT = STATEMENT_TYPES.length; + + protected static final String EXECUTE = "execute"; + protected static final String EXECUTE_QUERY = "executeQuery"; + protected static final String EXECUTE_UPDATE = "executeUpdate"; + protected static final String EXECUTE_BATCH = "executeBatch"; + + protected static final String[] EXECUTE_TYPES = {EXECUTE, EXECUTE_QUERY, EXECUTE_UPDATE, EXECUTE_BATCH}; + + /** + * the constructors that are used to create statement proxies + */ + protected static final Constructor[] constructors = new Constructor[STATEMENT_TYPE_COUNT]; + + public AbstractCreateStatementInterceptor() { + super(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (compare(CLOSE_VAL,method)) { + closeInvoked(); + return super.invoke(proxy, method, args); + } else { + boolean process = false; + process = isStatement(method, process); + if (process) { + long start = System.currentTimeMillis(); + Object statement = super.invoke(proxy,method,args); + long delta = System.currentTimeMillis() - start; + return createStatement(proxy,method,args,statement, delta); + } else { + return super.invoke(proxy,method,args); + } + } + } + + /** + * Creates a constructor for a proxy class, if one doesn't already exist + * + * @param idx + * - the index of the constructor + * @param clazz + * - the interface that the proxy will implement + * @return - returns a constructor used to create new instances + * @throws NoSuchMethodException Constructor not found + */ + /* + * Neither the class nor the constructor are exposed outside of jdbc-pool. + * Given the comments in the jdbc-pool code regarding caching for + * performance, continue to use Proxy.getProxyClass(). This will need to be + * revisited if that method is marked for removal. + */ + @SuppressWarnings("deprecation") + protected Constructor getConstructor(int idx, Class clazz) throws NoSuchMethodException { + if (constructors[idx] == null) { + Class proxyClass = Proxy.getProxyClass(AbstractCreateStatementInterceptor.class.getClassLoader(), + new Class[] { clazz }); + constructors[idx] = proxyClass.getConstructor(new Class[] { InvocationHandler.class }); + } + return constructors[idx]; + } + + /** + * This method will be invoked after a successful statement creation. This method can choose to return a wrapper + * around the statement or return the statement itself. + * If this method returns a wrapper then it should return a wrapper object that implements one of the following interfaces. + * {@link java.sql.Statement}, {@link java.sql.PreparedStatement} or {@link java.sql.CallableStatement} + * @param proxy the actual proxy object + * @param method the method that was called. It will be one of the methods defined in {@link #STATEMENT_TYPES} + * @param args the arguments to the method + * @param statement the statement that the underlying connection created + * @param time Elapsed time + * @return a {@link java.sql.Statement} object + */ + public abstract Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time); + + /** + * Method invoked when the operation {@link java.sql.Connection#close()} is invoked. + */ + public abstract void closeInvoked(); + + /** + * Returns true if the method that is being invoked matches one of the statement types. + * + * @param method the method being invoked on the proxy + * @param process boolean result used for recursion + * @return returns true if the method name matched + */ + protected boolean isStatement(Method method, boolean process){ + return process(STATEMENT_TYPES, method, process); + } + + /** + * Returns true if the method that is being invoked matches one of the execute types. + * + * @param method the method being invoked on the proxy + * @param process boolean result used for recursion + * @return returns true if the method name matched + */ + protected boolean isExecute(Method method, boolean process){ + return process(EXECUTE_TYPES, method, process); + } + + /* + * Returns true if the method that is being invoked matches one of the method names passed in + * @param names list of method names that we want to intercept + * @param method the method being invoked on the proxy + * @param process boolean result used for recursion + * @return returns true if the method name matched + */ + protected boolean process(String[] names, Method method, boolean process) { + final String name = method.getName(); + for (int i=0; (!process) && i0)?(String)args[0]:query; + //if we do batch execution, then we name the query 'batch' + if (sql==null && compare(EXECUTE_BATCH,name)) { + sql = "batch"; + } + return sql; + } + + /** + * Invoked when a query execution, a call to execute/executeQuery or executeBatch succeeded and was within the timing threshold + * @param query the query that was executed and failed + * @param args the arguments to the execution + * @param name the name of the method used to execute {@link AbstractCreateStatementInterceptor#isExecute(Method, boolean)} + * @param start the time the query execution started + * @param delta the time the execution took + * @return - the SQL that was executed or the string "batch" if it was a batch execution + */ + protected String reportQuery(String query, Object[] args, final String name, long start, long delta) { + //extract the query string + String sql = (query==null && args!=null && args.length>0)?(String)args[0]:query; + //if we do batch execution, then we name the query 'batch' + if (sql==null && compare(EXECUTE_BATCH,name)) { + sql = "batch"; + } + return sql; + } + + /** + * Invoked when a query execution, a call to execute/executeQuery or executeBatch succeeded and was exceeded the timing threshold + * @param query the query that was executed and failed + * @param args the arguments to the execution + * @param name the name of the method used to execute {@link AbstractCreateStatementInterceptor#isExecute(Method, boolean)} + * @param start the time the query execution started + * @param delta the time the execution took + * @return - the SQL that was executed or the string "batch" if it was a batch execution + */ + protected String reportSlowQuery(String query, Object[] args, final String name, long start, long delta) { + //extract the query string + String sql = (query==null && args!=null && args.length>0)?(String)args[0]:query; + //if we do batch execution, then we name the query 'batch' + if (sql==null && compare(EXECUTE_BATCH,name)) { + sql = "batch"; + } + return sql; + } + + /** + * returns the query measure threshold. + * This value is in milliseconds. If the query is faster than this threshold than it won't be accounted for + * @return the threshold in milliseconds + */ + public long getThreshold() { + return threshold; + } + + /** + * Sets the query measurement threshold. The value is in milliseconds. + * If the query goes faster than this threshold it will not be recorded. + * @param threshold set to -1 to record every query. Value is in milliseconds. + */ + public void setThreshold(long threshold) { + this.threshold = threshold; + } + + /** + * Creates a statement interceptor to monitor query response times + */ + @Override + public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) { + try { + Object result = null; + String name = method.getName(); + String sql = null; + Constructor constructor = null; + if (compare(CREATE_STATEMENT,name)) { + //createStatement + constructor = getConstructor(CREATE_STATEMENT_IDX,Statement.class); + }else if (compare(PREPARE_STATEMENT,name)) { + //prepareStatement + sql = (String)args[0]; + constructor = getConstructor(PREPARE_STATEMENT_IDX,PreparedStatement.class); + if (sql!=null) { + prepareStatement(sql, time); + } + }else if (compare(PREPARE_CALL,name)) { + //prepareCall + sql = (String)args[0]; + constructor = getConstructor(PREPARE_CALL_IDX,CallableStatement.class); + prepareCall(sql,time); + }else { + //do nothing, might be a future unsupported method + //so we better bail out and let the system continue + return statement; + } + result = constructor.newInstance(new Object[] { new StatementProxy(statement,sql) }); + return result; + }catch (Exception x) { + log.warn("Unable to create statement proxy for slow query report.",x); + } + return statement; + } + + + /** + * Class to measure query execute time + * + */ + protected class StatementProxy implements InvocationHandler { + protected boolean closed = false; + protected Object delegate; + protected final String query; + public StatementProxy(Object parent, String query) { + this.delegate = parent; + this.query = query; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + //get the name of the method for comparison + final String name = method.getName(); + //was close invoked? + boolean close = compare(CLOSE_VAL, name); + //allow close to be called multiple times + if (close && closed) { + return null; + } + //are we calling isClosed? + if (compare(ISCLOSED_VAL, name)) { + return Boolean.valueOf(closed); + } + //if we are calling anything else, bail out + if (closed) { + throw new SQLException("Statement closed."); + } + boolean process = false; + //check to see if we are about to execute a query + process = isExecute( method, process); + //if we are executing, get the current time + long start = (process)?System.currentTimeMillis():0; + Object result = null; + try { + //execute the query + result = method.invoke(delegate,args); + }catch (Throwable t) { + reportFailedQuery(query,args,name,start,t); + if (t instanceof InvocationTargetException + && t.getCause() != null) { + throw t.getCause(); + } else { + throw t; + } + } + //measure the time + long delta = (process)?(System.currentTimeMillis()-start):Long.MIN_VALUE; + //see if we meet the requirements to measure + if (delta>threshold) { + try { + //report the slow query + reportSlowQuery(query, args, name, start, delta); + }catch (Exception t) { + if (log.isWarnEnabled()) { + log.warn("Unable to process slow query",t); + } + } + } else if (process) { + reportQuery(query, args, name, start, delta); + } + //perform close cleanup + if (close) { + closed=true; + delegate = null; + } + return result; + } + } + +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ConnectionState.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ConnectionState.java new file mode 100644 index 0000000..fdc64fb --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ConnectionState.java @@ -0,0 +1,183 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.lang.reflect.Method; +import java.sql.SQLException; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.DataSourceFactory; +import org.apache.tomcat.jdbc.pool.JdbcInterceptor; +import org.apache.tomcat.jdbc.pool.PoolConfiguration; +import org.apache.tomcat.jdbc.pool.PooledConnection; + +/** + * Interceptor that keep track of connection state to avoid roundtrips to the database. + * The {@link org.apache.tomcat.jdbc.pool.ConnectionPool} is optimized to do as little work as possible. + * The pool itself doesn't remember settings like {@link java.sql.Connection#setAutoCommit(boolean)}, + * {@link java.sql.Connection#setReadOnly(boolean)}, {@link java.sql.Connection#setCatalog(String)} or + * {@link java.sql.Connection#setTransactionIsolation(int)}. It relies on the application to remember how and when + * these settings have been applied. + * In the cases where the application code doesn't know or want to keep track of the state, this interceptor helps cache the + * state, and it also avoids roundtrips to the database asking for it. + * + */ + +public class ConnectionState extends JdbcInterceptor { + private static final Log log = LogFactory.getLog(ConnectionState.class); + + protected final String[] readState = {"getAutoCommit","getTransactionIsolation","isReadOnly","getCatalog"}; + protected final String[] writeState = {"setAutoCommit","setTransactionIsolation","setReadOnly","setCatalog"}; + + protected Boolean autoCommit = null; + protected Integer transactionIsolation = null; + protected Boolean readOnly = null; + protected String catalog = null; + + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + if (parent==null || con==null) { + //we are resetting, reset our defaults + autoCommit = null; + transactionIsolation = null; + readOnly = null; + catalog = null; + return; + } + PoolConfiguration poolProperties = parent.getPoolProperties(); + if (poolProperties.getDefaultTransactionIsolation()!=DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION) { + try { + if (transactionIsolation==null || transactionIsolation.intValue()!=poolProperties.getDefaultTransactionIsolation()) { + con.getConnection().setTransactionIsolation(poolProperties.getDefaultTransactionIsolation()); + transactionIsolation = Integer.valueOf(poolProperties.getDefaultTransactionIsolation()); + } + }catch (SQLException x) { + transactionIsolation = null; + log.error("Unable to reset transaction isolation state to connection.",x); + } + } + if (poolProperties.getDefaultReadOnly()!=null) { + try { + if (readOnly==null || readOnly.booleanValue()!=poolProperties.getDefaultReadOnly().booleanValue()) { + con.getConnection().setReadOnly(poolProperties.getDefaultReadOnly().booleanValue()); + readOnly = poolProperties.getDefaultReadOnly(); + } + }catch (SQLException x) { + readOnly = null; + log.error("Unable to reset readonly state to connection.",x); + } + } + if (poolProperties.getDefaultAutoCommit()!=null) { + try { + if (autoCommit==null || autoCommit.booleanValue()!=poolProperties.getDefaultAutoCommit().booleanValue()) { + con.getConnection().setAutoCommit(poolProperties.getDefaultAutoCommit().booleanValue()); + autoCommit = poolProperties.getDefaultAutoCommit(); + } + }catch (SQLException x) { + autoCommit = null; + log.error("Unable to reset autocommit state to connection.",x); + } + } + if (poolProperties.getDefaultCatalog()!=null) { + try { + if (catalog==null || (!catalog.equals(poolProperties.getDefaultCatalog()))) { + con.getConnection().setCatalog(poolProperties.getDefaultCatalog()); + catalog = poolProperties.getDefaultCatalog(); + } + }catch (SQLException x) { + catalog = null; + log.error("Unable to reset default catalog state to connection.",x); + } + } + + } + + + @Override + public void disconnected(ConnectionPool parent, PooledConnection con, boolean finalizing) { + //we are resetting, reset our defaults + autoCommit = null; + transactionIsolation = null; + readOnly = null; + catalog = null; + super.disconnected(parent, con, finalizing); + } + + + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String name = method.getName(); + boolean read = false; + int index = -1; + for (int i=0; (!read) && i properties) { + super.setProperties(properties); + InterceptorProperty p = properties.get("queryTimeout"); + if (p!=null) { + timeout = p.getValueAsInt(timeout); + } + } + + @Override + public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) { + if (statement instanceof Statement && timeout > 0) { + Statement s = (Statement)statement; + try { + s.setQueryTimeout(timeout); + }catch (SQLException x) { + log.warn("[QueryTimeoutInterceptor] Unable to set query timeout:"+x.getMessage(),x); + } + } + return statement; + } + + @Override + public void closeInvoked() { + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimer.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimer.java new file mode 100644 index 0000000..f9d66f4 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimer.java @@ -0,0 +1,106 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.lang.reflect.Method; + +import javax.management.ObjectName; + +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.PooledConnection; +import org.apache.tomcat.jdbc.pool.jmx.JmxUtil; + +/** + * Class that resets the abandoned timer on any activity on the + * Connection or any successful query executions. + * This interceptor is useful for when you have a {@link org.apache.tomcat.jdbc.pool.PoolConfiguration#setRemoveAbandonedTimeout(int)} + * that is fairly low, and you want to reset the abandoned time each time any operation on the connection is performed + * This is useful for batch processing programs that use connections for extensive amount of times. + * + */ +public class ResetAbandonedTimer extends AbstractQueryReport implements ResetAbandonedTimerMBean { + + private PooledConnection pcon; + + private ObjectName oname = null; + + public ResetAbandonedTimer() { + } + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + super.reset(parent, con); + if (con == null) { + this.pcon = null; + if (oname != null) { + JmxUtil.unregisterJmx(oname); + oname = null; + } + } else { + this.pcon = con; + if (oname == null) { + String keyprop = ",JdbcInterceptor=" + getClass().getSimpleName(); + oname = JmxUtil.registerJmx(pcon.getObjectName(), keyprop, this); + } + } + } + + @Override + public boolean resetTimer() { + boolean result = false; + if (pcon != null) { + pcon.setTimestamp(System.currentTimeMillis()); + result = true; + } + return result; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object result = super.invoke(proxy, method, args); + resetTimer(); + return result; + } + + @Override + protected void prepareCall(String query, long time) { + resetTimer(); + } + + @Override + protected void prepareStatement(String sql, long time) { + resetTimer(); + + } + + @Override + public void closeInvoked() { + resetTimer(); + } + + @Override + protected String reportQuery(String query, Object[] args, String name,long start, long delta) { + resetTimer(); + return super.reportQuery(query, args, name, start, delta); + } + + @Override + protected String reportSlowQuery(String query, Object[] args, String name,long start, long delta) { + resetTimer(); + return super.reportSlowQuery(query, args, name, start, delta); + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimerMBean.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimerMBean.java new file mode 100644 index 0000000..bbd2845 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimerMBean.java @@ -0,0 +1,21 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +public interface ResetAbandonedTimerMBean { + boolean resetTimer(); +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java new file mode 100644 index 0000000..84759bb --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java @@ -0,0 +1,503 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorProperty; +import org.apache.tomcat.jdbc.pool.PooledConnection; + +/** + * Slow query report interceptor. Tracks timing of query executions. + */ +public class SlowQueryReport extends AbstractQueryReport { + //logger + private static final Log log = LogFactory.getLog(SlowQueryReport.class); + + /** + * we will be keeping track of query stats on a per pool basis + */ + protected static final ConcurrentHashMap> perPoolStats = + new ConcurrentHashMap<>(); + /** + * the queries that are used for this interceptor. + */ + protected volatile ConcurrentHashMap queries = null; + /** + * Maximum number of queries we will be storing + */ + protected int maxQueries= 1000; //don't store more than this amount of queries + + /** + * Flag to enable disable logging of slow queries + */ + protected boolean logSlow = true; + + /** + * Flag to enable disable logging of failed queries + */ + protected boolean logFailed = false; + + /** + * Sort QueryStats by last invocation time + */ + protected final Comparator queryStatsComparator = new QueryStatsComparator(); + + /** + * Returns the query stats for a given pool + * @param poolname - the name of the pool we want to retrieve stats for + * @return a hash map containing statistics for 0 to maxQueries + */ + public static ConcurrentHashMap getPoolStats(String poolname) { + return perPoolStats.get(poolname); + } + + /** + * Creates a slow query report interceptor + */ + public SlowQueryReport() { + super(); + } + + public void setMaxQueries(int maxQueries) { + this.maxQueries = maxQueries; + } + + + @Override + protected String reportFailedQuery(String query, Object[] args, String name, long start, Throwable t) { + String sql = super.reportFailedQuery(query, args, name, start, t); + if (this.maxQueries > 0 ) { + long now = System.currentTimeMillis(); + long delta = now - start; + QueryStats qs = this.getQueryStats(sql); + if (qs != null) { + qs.failure(delta, now); + } + if (isLogFailed() && log.isWarnEnabled()) { + log.warn("Failed Query Report SQL="+sql+"; time="+delta+" ms;"); + } + } + return sql; + } + + @Override + protected String reportQuery(String query, Object[] args, final String name, long start, long delta) { + String sql = super.reportQuery(query, args, name, start, delta); + if (this.maxQueries > 0 ) { + QueryStats qs = this.getQueryStats(sql); + if (qs != null) { + qs.add(delta, start); + } + } + return sql; + } + + @Override + protected String reportSlowQuery(String query, Object[] args, String name, long start, long delta) { + String sql = super.reportSlowQuery(query, args, name, start, delta); + if (this.maxQueries > 0 ) { + QueryStats qs = this.getQueryStats(sql); + if (qs != null) { + qs.add(delta, start); + if (isLogSlow() && log.isWarnEnabled()) { + log.warn("Slow Query Report SQL="+sql+"; time="+delta+" ms;"); + } + } + } + return sql; + } + + /** + * invoked when the connection receives the close request + * Not used for now. + */ + @Override + public void closeInvoked() { + // NOOP + } + + @Override + public void prepareStatement(String sql, long time) { + if (this.maxQueries > 0 ) { + QueryStats qs = getQueryStats(sql); + if (qs != null) { + qs.prepare(time); + } + } + } + + @Override + public void prepareCall(String sql, long time) { + if (this.maxQueries > 0 ) { + QueryStats qs = getQueryStats(sql); + if (qs != null) { + qs.prepare(time); + } + } + } + + @Override + public void poolStarted(ConnectionPool pool) { + super.poolStarted(pool); + //see if we already created a map for this pool + queries = perPoolStats.get(pool.getName()); + if (queries==null) { + //create the map to hold our stats + //however TODO we need to improve the eviction + //selection + queries = new ConcurrentHashMap<>(); + if (perPoolStats.putIfAbsent(pool.getName(), queries)!=null) { + //there already was one + queries = perPoolStats.get(pool.getName()); + } + } + } + + @Override + public void poolClosed(ConnectionPool pool) { + perPoolStats.remove(pool.getName()); + super.poolClosed(pool); + } + + protected QueryStats getQueryStats(String sql) { + if (sql==null) { + sql = ""; + } + ConcurrentHashMap queries = SlowQueryReport.this.queries; + if (queries==null) { + if (log.isWarnEnabled()) { + log.warn("Connection has already been closed or abandoned"); + } + return null; + } + QueryStats qs = queries.get(sql); + if (qs == null) { + qs = new QueryStats(sql); + if (queries.putIfAbsent(sql,qs)!=null) { + qs = queries.get(sql); + } else { + //we added a new element, see if we need to remove the oldest + if (queries.size() > maxQueries) { + removeOldest(queries); + } + } + } + return qs; + } + + /** + * Sort QueryStats by last invocation time + * @param queries The queries map + */ + protected void removeOldest(ConcurrentHashMap queries) { + ArrayList list = new ArrayList<>(queries.values()); + Collections.sort(list, queryStatsComparator); + int removeIndex = 0; + while (queries.size() > maxQueries) { + String sql = list.get(removeIndex).getQuery(); + queries.remove(sql); + if (log.isDebugEnabled()) { + log.debug("Removing slow query, capacity reached:"+sql); + } + removeIndex++; + } + } + + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + super.reset(parent, con); + if (parent!=null) { + queries = perPoolStats.get(parent.getName()); + } else { + queries = null; + } + } + + + public boolean isLogSlow() { + return logSlow; + } + + public void setLogSlow(boolean logSlow) { + this.logSlow = logSlow; + } + + public boolean isLogFailed() { + return logFailed; + } + + public void setLogFailed(boolean logFailed) { + this.logFailed = logFailed; + } + + @Override + public void setProperties(Map properties) { + super.setProperties(properties); + final String threshold = "threshold"; + final String maxqueries= "maxQueries"; + final String logslow = "logSlow"; + final String logfailed = "logFailed"; + InterceptorProperty p1 = properties.get(threshold); + InterceptorProperty p2 = properties.get(maxqueries); + InterceptorProperty p3 = properties.get(logslow); + InterceptorProperty p4 = properties.get(logfailed); + if (p1!=null) { + setThreshold(Long.parseLong(p1.getValue())); + } + if (p2!=null) { + setMaxQueries(Integer.parseInt(p2.getValue())); + } + if (p3!=null) { + setLogSlow(Boolean.parseBoolean(p3.getValue())); + } + if (p4!=null) { + setLogFailed(Boolean.parseBoolean(p4.getValue())); + } + } + + + public static class QueryStats { + static final String[] FIELD_NAMES = new String[] { + "query", + "nrOfInvocations", + "maxInvocationTime", + "maxInvocationDate", + "minInvocationTime", + "minInvocationDate", + "totalInvocationTime", + "failures", + "prepareCount", + "prepareTime", + "lastInvocation" + }; + + static final String[] FIELD_DESCRIPTIONS = new String[] { + "The SQL query", + "The number of query invocations, a call to executeXXX", + "The longest time for this query in milliseconds", + "The time and date for when the longest query took place", + "The shortest time for this query in milliseconds", + "The time and date for when the shortest query took place", + "The total amount of milliseconds spent executing this query", + "The number of failures for this query", + "The number of times this query was prepared (prepareStatement/prepareCall)", + "The total number of milliseconds spent preparing this query", + "The date and time of the last invocation" + }; + + static final OpenType[] FIELD_TYPES = new OpenType[] { + SimpleType.STRING, + SimpleType.INTEGER, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.INTEGER, + SimpleType.LONG, + SimpleType.LONG + }; + + private final String query; + private volatile int nrOfInvocations; + private volatile long maxInvocationTime = Long.MIN_VALUE; + private volatile long maxInvocationDate; + private volatile long minInvocationTime = Long.MAX_VALUE; + private volatile long minInvocationDate; + private volatile long totalInvocationTime; + private volatile long failures; + private volatile int prepareCount; + private volatile long prepareTime; + private volatile long lastInvocation = 0; + + public static String[] getFieldNames() { + return FIELD_NAMES; + } + + public static String[] getFieldDescriptions() { + return FIELD_DESCRIPTIONS; + } + + public static OpenType[] getFieldTypes() { + return FIELD_TYPES; + } + + @Override + public String toString() { + SimpleDateFormat sdf = + new SimpleDateFormat("d MMM yyyy HH:mm:ss z", Locale.US); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + StringBuilder buf = new StringBuilder("QueryStats[query:"); + buf.append(query); + buf.append(", nrOfInvocations:"); + buf.append(nrOfInvocations); + buf.append(", maxInvocationTime:"); + buf.append(maxInvocationTime); + buf.append(", maxInvocationDate:"); + buf.append(sdf.format(new java.util.Date(maxInvocationDate))); + buf.append(", minInvocationTime:"); + buf.append(minInvocationTime); + buf.append(", minInvocationDate:"); + buf.append(sdf.format(new java.util.Date(minInvocationDate))); + buf.append(", totalInvocationTime:"); + buf.append(totalInvocationTime); + buf.append(", averageInvocationTime:"); + buf.append((float)totalInvocationTime / (float)nrOfInvocations); + buf.append(", failures:"); + buf.append(failures); + buf.append(", prepareCount:"); + buf.append(prepareCount); + buf.append(", prepareTime:"); + buf.append(prepareTime); + buf.append(']'); + return buf.toString(); + } + + public CompositeDataSupport getCompositeData(final CompositeType type) throws OpenDataException{ + Object[] values = new Object[] { + query, + Integer.valueOf(nrOfInvocations), + Long.valueOf(maxInvocationTime), + Long.valueOf(maxInvocationDate), + Long.valueOf(minInvocationTime), + Long.valueOf(minInvocationDate), + Long.valueOf(totalInvocationTime), + Long.valueOf(failures), + Integer.valueOf(prepareCount), + Long.valueOf(prepareTime), + Long.valueOf(lastInvocation) + }; + return new CompositeDataSupport(type,FIELD_NAMES,values); + } + + public QueryStats(String query) { + this.query = query; + } + + public synchronized void prepare(long invocationTime) { + prepareCount++; + prepareTime+=invocationTime; + + } + + public synchronized void add(long invocationTime, long now) { + maxInvocationTime = Math.max(invocationTime, maxInvocationTime); + if (maxInvocationTime == invocationTime) { + maxInvocationDate = now; + } + minInvocationTime = Math.min(invocationTime, minInvocationTime); + if (minInvocationTime==invocationTime) { + minInvocationDate = now; + } + nrOfInvocations++; + totalInvocationTime+=invocationTime; + lastInvocation = now; + } + + public synchronized void failure(long invocationTime, long now) { + add(invocationTime,now); + failures++; + + } + + public String getQuery() { + return query; + } + + public int getNrOfInvocations() { + return nrOfInvocations; + } + + public long getMaxInvocationTime() { + return maxInvocationTime; + } + + public long getMaxInvocationDate() { + return maxInvocationDate; + } + + public long getMinInvocationTime() { + return minInvocationTime; + } + + public long getMinInvocationDate() { + return minInvocationDate; + } + + public long getTotalInvocationTime() { + return totalInvocationTime; + } + + @Override + public int hashCode() { + return query.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof QueryStats) { + QueryStats qs = (QueryStats)other; + return qs.query.equals(this.query); + } + return false; + } + + public boolean isOlderThan(QueryStats other) { + return this.lastInvocation < other.lastInvocation; + } + } + + /** + * Compare QueryStats by their lastInvocation value. QueryStats that have + * never been updated, have a lastInvocation value of {@code 0} which should + * be handled as the newest possible invocation. + */ + // Public for unit tests + public static class QueryStatsComparator implements Comparator { + + @Override + public int compare(QueryStats stats1, QueryStats stats2) { + return Long.compare(handleZero(stats1.lastInvocation), + handleZero(stats2.lastInvocation)); + } + + private static long handleZero(long value) { + return value == 0 ? Long.MAX_VALUE : value; + } + + } + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReportJmx.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReportJmx.java new file mode 100644 index 0000000..b2531b3 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReportJmx.java @@ -0,0 +1,303 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import javax.management.ListenerNotFoundException; +import javax.management.MBeanNotificationInfo; +import javax.management.MalformedObjectNameException; +import javax.management.Notification; +import javax.management.NotificationBroadcasterSupport; +import javax.management.NotificationEmitter; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectName; +import javax.management.RuntimeOperationsException; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorProperty; +import org.apache.tomcat.jdbc.pool.PooledConnection; +import org.apache.tomcat.jdbc.pool.jmx.JmxUtil; +/** + * Publishes data to JMX and provides notifications + * when failures happen. + * + */ +public class SlowQueryReportJmx extends SlowQueryReport implements NotificationEmitter, SlowQueryReportJmxMBean{ + public static final String SLOW_QUERY_NOTIFICATION = "SLOW QUERY"; + public static final String FAILED_QUERY_NOTIFICATION = "FAILED QUERY"; + + public static final String objectNameAttribute = "objectName"; + + protected static volatile CompositeType SLOW_QUERY_TYPE; + + private static final Log log = LogFactory.getLog(SlowQueryReportJmx.class); + + + protected static final ConcurrentHashMap mbeans = + new ConcurrentHashMap<>(); + + + //==============================JMX STUFF======================== + protected volatile NotificationBroadcasterSupport notifier = new NotificationBroadcasterSupport(); + + @Override + public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws IllegalArgumentException { + notifier.addNotificationListener(listener, filter, handback); + } + + + @Override + public MBeanNotificationInfo[] getNotificationInfo() { + return notifier.getNotificationInfo(); + } + + @Override + public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException { + notifier.removeNotificationListener(listener); + + } + + @Override + public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws ListenerNotFoundException { + notifier.removeNotificationListener(listener, filter, handback); + + } + + + //==============================JMX STUFF======================== + + protected String poolName = null; + + protected static final AtomicLong notifySequence = new AtomicLong(0); + + protected boolean notifyPool = true; + + protected ConnectionPool pool = null; + + protected static CompositeType getCompositeType() { + if (SLOW_QUERY_TYPE==null) { + try { + SLOW_QUERY_TYPE = new CompositeType( + SlowQueryReportJmx.class.getName(), + "Composite data type for query statistics", + QueryStats.getFieldNames(), + QueryStats.getFieldDescriptions(), + QueryStats.getFieldTypes()); + }catch (OpenDataException x) { + log.warn("Unable to initialize composite data type for JMX stats and notifications.",x); + } + } + return SLOW_QUERY_TYPE; + } + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + super.reset(parent, con); + if (parent!=null) { + poolName = parent.getName(); + pool = parent; + registerJmx(); + } + } + + + @Override + public void poolClosed(ConnectionPool pool) { + this.poolName = pool.getName(); + deregisterJmx(); + super.poolClosed(pool); + } + + @Override + public void poolStarted(ConnectionPool pool) { + this.pool = pool; + super.poolStarted(pool); + this.poolName = pool.getName(); + } + + @Override + protected String reportFailedQuery(String query, Object[] args, String name, long start, Throwable t) { + query = super.reportFailedQuery(query, args, name, start, t); + if (isLogFailed()) { + notifyJmx(query,FAILED_QUERY_NOTIFICATION); + } + return query; + } + + protected void notifyJmx(String query, String type) { + try { + long sequence = notifySequence.incrementAndGet(); + + if (isNotifyPool()) { + if (this.pool!=null && this.pool.getJmxPool()!=null) { + this.pool.getJmxPool().notify(type, query); + } + } else { + if (notifier!=null) { + Notification notification = + new Notification(type, + this, + sequence, + System.currentTimeMillis(), + query); + + notifier.sendNotification(notification); + } + } + } catch (RuntimeOperationsException e) { + if (log.isDebugEnabled()) { + log.debug("Unable to send failed query notification.",e); + } + } + } + + @Override + protected String reportSlowQuery(String query, Object[] args, String name, long start, long delta) { + query = super.reportSlowQuery(query, args, name, start, delta); + if (isLogSlow()) { + notifyJmx(query,SLOW_QUERY_NOTIFICATION); + } + return query; + } + + /** + * JMX operation - return the names of all the pools + * @return - all the names of pools that we have stored data for + */ + public String[] getPoolNames() { + Set keys = perPoolStats.keySet(); + return keys.toArray(new String[0]); + } + + /** + * JMX operation - return the name of the pool + * @return the name of the pool, unique within the JVM + */ + public String getPoolName() { + return poolName; + } + + + public boolean isNotifyPool() { + return notifyPool; + } + + public void setNotifyPool(boolean notifyPool) { + this.notifyPool = notifyPool; + } + + /** + * JMX operation - remove all stats for this connection pool + */ + public void resetStats() { + ConcurrentHashMap queries = perPoolStats.get(poolName); + if (queries!=null) { + Iterator it = queries.keySet().iterator(); + while (it.hasNext()) { + it.remove(); + } + } + } + + /** + * JMX operation - returns all the queries we have collected. + * @return - the slow query report as composite data. + */ + @Override + public CompositeData[] getSlowQueriesCD() throws OpenDataException { + CompositeDataSupport[] result = null; + ConcurrentHashMap queries = perPoolStats.get(poolName); + if (queries!=null) { + Set> stats = queries.entrySet(); + if (stats!=null) { + result = new CompositeDataSupport[stats.size()]; + Iterator> it = stats.iterator(); + int pos = 0; + while (it.hasNext()) { + Map.Entry entry = it.next(); + QueryStats qs = entry.getValue(); + result[pos++] = qs.getCompositeData(getCompositeType()); + } + } + } + return result; + } + + protected void deregisterJmx() { + try { + if (mbeans.remove(poolName)!=null) { + ObjectName oname = getObjectName(getClass(),poolName); + JmxUtil.unregisterJmx(oname); + } + } catch (MalformedObjectNameException | RuntimeOperationsException e) { + log.warn("Jmx deregistration failed.",e); + } + + } + + + public ObjectName getObjectName(Class clazz, String poolName) throws MalformedObjectNameException { + ObjectName oname; + Map properties = getProperties(); + if (properties != null && properties.containsKey(objectNameAttribute)) { + oname = new ObjectName(properties.get(objectNameAttribute).getValue()); + } else { + oname = new ObjectName(ConnectionPool.POOL_JMX_TYPE_PREFIX+clazz.getName()+",name=" + poolName); + } + return oname; + } + + protected void registerJmx() { + try { + //only if we notify the pool itself + if (isNotifyPool()) { + + } else if (getCompositeType()!=null) { + ObjectName oname = getObjectName(getClass(),poolName); + if (mbeans.putIfAbsent(poolName, this)==null) { + JmxUtil.registerJmx(oname, null, this); + } + } else { + log.warn(SlowQueryReport.class.getName()+ "- No JMX support, composite type was not found."); + } + } catch (MalformedObjectNameException | RuntimeOperationsException e) { + log.error("Jmx registration failed, no JMX data will be exposed for the query stats.",e); + } + } + + @Override + public void setProperties(Map properties) { + super.setProperties(properties); + final String threshold = "notifyPool"; + InterceptorProperty p1 = properties.get(threshold); + if (p1!=null) { + this.setNotifyPool(Boolean.parseBoolean(p1.getValue())); + } + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReportJmxMBean.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReportJmxMBean.java new file mode 100644 index 0000000..7ae501b --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReportJmxMBean.java @@ -0,0 +1,23 @@ +/* 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.tomcat.jdbc.pool.interceptor; + +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.OpenDataException; + +public interface SlowQueryReportJmxMBean { + CompositeData[] getSlowQueriesCD() throws OpenDataException; +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java new file mode 100644 index 0000000..8b74b64 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java @@ -0,0 +1,402 @@ +/* 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.tomcat.jdbc.pool.interceptor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorProperty; +import org.apache.tomcat.jdbc.pool.PooledConnection; +import org.apache.tomcat.jdbc.pool.jmx.JmxUtil; + +/** + * Interceptor that caches {@code PreparedStatement} and/or + * {@code CallableStatement} instances on a connection. + */ +public class StatementCache extends StatementDecoratorInterceptor implements StatementCacheMBean { + private static final Log log = LogFactory.getLog(StatementCache.class); + protected static final String[] ALL_TYPES = new String[] {PREPARE_STATEMENT,PREPARE_CALL}; + protected static final String[] CALLABLE_TYPE = new String[] {PREPARE_CALL}; + protected static final String[] PREPARED_TYPE = new String[] {PREPARE_STATEMENT}; + protected static final String[] NO_TYPE = new String[] {}; + + protected static final String STATEMENT_CACHE_ATTR = StatementCache.class.getName() + ".cache"; + + /*begin properties for the statement cache*/ + private boolean cachePrepared = true; + private boolean cacheCallable = false; + private int maxCacheSize = 50; + private PooledConnection pcon; + private String[] types; + + private ObjectName oname = null; + + @Override + public boolean isCachePrepared() { + return cachePrepared; + } + + @Override + public boolean isCacheCallable() { + return cacheCallable; + } + + @Override + public int getMaxCacheSize() { + return maxCacheSize; + } + + public String[] getTypes() { + return types; + } + + @Override + public AtomicInteger getCacheSize() { + return cacheSize; + } + + @Override + public void setProperties(Map properties) { + super.setProperties(properties); + InterceptorProperty p = properties.get("prepared"); + if (p!=null) { + cachePrepared = p.getValueAsBoolean(cachePrepared); + } + p = properties.get("callable"); + if (p!=null) { + cacheCallable = p.getValueAsBoolean(cacheCallable); + } + p = properties.get("max"); + if (p!=null) { + maxCacheSize = p.getValueAsInt(maxCacheSize); + } + if (cachePrepared && cacheCallable) { + this.types = ALL_TYPES; + } else if (cachePrepared) { + this.types = PREPARED_TYPE; + } else if (cacheCallable) { + this.types = CALLABLE_TYPE; + } else { + this.types = NO_TYPE; + } + + } + /*end properties for the statement cache*/ + + /*begin the cache size*/ + private static ConcurrentHashMap cacheSizeMap = + new ConcurrentHashMap<>(); + + private AtomicInteger cacheSize; + + @Override + public void poolStarted(ConnectionPool pool) { + cacheSizeMap.putIfAbsent(pool, new AtomicInteger(0)); + super.poolStarted(pool); + } + + @Override + public void poolClosed(ConnectionPool pool) { + cacheSizeMap.remove(pool); + super.poolClosed(pool); + } + /*end the cache size*/ + + /*begin the actual statement cache*/ + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + super.reset(parent, con); + if (parent==null) { + cacheSize = null; + this.pcon = null; + if (oname != null) { + JmxUtil.unregisterJmx(oname); + oname = null; + } + } else { + cacheSize = cacheSizeMap.get(parent); + this.pcon = con; + if (!pcon.getAttributes().containsKey(STATEMENT_CACHE_ATTR)) { + ConcurrentHashMap cache = + new ConcurrentHashMap<>(); + pcon.getAttributes().put(STATEMENT_CACHE_ATTR,cache); + } + if (oname == null) { + String keyprop = ",JdbcInterceptor=" + getClass().getSimpleName(); + oname = JmxUtil.registerJmx(pcon.getObjectName(), keyprop, this); + } + } + } + + @Override + public void disconnected(ConnectionPool parent, PooledConnection con, boolean finalizing) { + @SuppressWarnings("unchecked") + ConcurrentHashMap statements = + (ConcurrentHashMap)con.getAttributes().get(STATEMENT_CACHE_ATTR); + + if (statements!=null) { + for (Map.Entry p : statements.entrySet()) { + closeStatement(p.getValue()); + } + statements.clear(); + } + + super.disconnected(parent, con, finalizing); + } + + public void closeStatement(CachedStatement st) { + if (st==null) { + return; + } + st.forceClose(); + } + + @Override + protected Object createDecorator(Object proxy, Method method, Object[] args, + Object statement, Constructor constructor, String sql) + throws InstantiationException, IllegalAccessException, InvocationTargetException { + boolean process = process(this.types, method, false); + if (process) { + Object result = null; + CachedStatement statementProxy = new CachedStatement((PreparedStatement)statement,sql); + result = constructor.newInstance(new Object[] { statementProxy }); + statementProxy.setActualProxy(result); + statementProxy.setConnection(proxy); + statementProxy.setConstructor(constructor); + statementProxy.setCacheKey(createCacheKey(method, args)); + return result; + } else { + return super.createDecorator(proxy, method, args, statement, constructor, sql); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + boolean process = process(this.types, method, false); + if (process && args.length>0 && args[0] instanceof String) { + CachedStatement statement = isCached(method, args); + if (statement!=null) { + //remove it from the cache since it is used + removeStatement(statement); + return statement.getActualProxy(); + } else { + return super.invoke(proxy, method, args); + } + } else { + return super.invoke(proxy,method,args); + } + } + + public CachedStatement isCached(Method method, Object[] args) { + ConcurrentHashMap cache = getCache(); + if (cache == null) { + return null; + } + CacheKey key = createCacheKey(method, args); + if (key == null) { + throw new IllegalArgumentException("Null key"); + } + return cache.get(key); + } + + public boolean cacheStatement(CachedStatement proxy) { + ConcurrentHashMap cache = getCache(); + if (cache == null) { + return false; + } + if (proxy.getCacheKey()==null) { + return false; + } else if (cache.containsKey(proxy.getCacheKey())) { + return false; + } else if (cacheSize.get()>=maxCacheSize) { + return false; + } else if (cacheSize.incrementAndGet()>maxCacheSize) { + cacheSize.decrementAndGet(); + return false; + } else { + //cache the statement + cache.put(proxy.getCacheKey(), proxy); + return true; + } + } + + public boolean removeStatement(CachedStatement proxy) { + ConcurrentHashMap cache = getCache(); + if (cache == null) { + return false; + } + if (cache.remove(proxy.getCacheKey()) != null) { + cacheSize.decrementAndGet(); + return true; + } else { + return false; + } + } + /*end the actual statement cache*/ + + protected ConcurrentHashMap getCache() { + PooledConnection pCon = this.pcon; + if (pCon == null) { + if (log.isWarnEnabled()) { + log.warn("Connection has already been closed or abandoned"); + } + return null; + } + @SuppressWarnings("unchecked") + ConcurrentHashMap cache = + (ConcurrentHashMap)pCon.getAttributes().get(STATEMENT_CACHE_ATTR); + return cache; + } + + @Override + public int getCacheSizePerConnection() { + ConcurrentHashMap cache = getCache(); + if (cache == null) { + return 0; + } + return cache.size(); + } + + protected class CachedStatement extends StatementDecoratorInterceptor.StatementProxy { + CacheKey key; + public CachedStatement(PreparedStatement parent, String sql) { + super(parent, sql); + } + + @Override + public void closeInvoked() { + //should we cache it + boolean shouldClose = true; + if (cacheSize.get() < maxCacheSize) { + //cache a proxy so that we don't reuse the facade + CachedStatement proxy = new CachedStatement(getDelegate(),getSql()); + proxy.setCacheKey(getCacheKey()); + try { + // clear Resultset + ResultSet result = getDelegate().getResultSet(); + if (result != null && !result.isClosed()) { + result.close(); + } + // clear parameter + getDelegate().clearParameters(); + + //create a new facade + Object actualProxy = getConstructor().newInstance(new Object[] { proxy }); + proxy.setActualProxy(actualProxy); + proxy.setConnection(getConnection()); + proxy.setConstructor(getConstructor()); + if (cacheStatement(proxy)) { + shouldClose = false; + } + } catch (RuntimeException | ReflectiveOperationException | SQLException x) { + removeStatement(proxy); + } + } + if (shouldClose) { + super.closeInvoked(); + } + closed = true; + delegate = null; + + } + + public void forceClose() { + removeStatement(this); + super.closeInvoked(); + } + + public CacheKey getCacheKey() { + return key; + } + + public void setCacheKey(CacheKey cacheKey) { + key = cacheKey; + } + + } + + protected CacheKey createCacheKey(Method method, Object[] args) { + return createCacheKey(method.getName(), args); + } + + protected CacheKey createCacheKey(String methodName, Object[] args) { + CacheKey key = null; + if (compare(PREPARE_STATEMENT, methodName)) { + key = new CacheKey(PREPARE_STATEMENT, args); + } else if (compare(PREPARE_CALL, methodName)) { + key = new CacheKey(PREPARE_CALL, args); + } + return key; + } + + + private static final class CacheKey { + private final String stmtType; + private final Object[] args; + private CacheKey(String type, Object[] methodArgs) { + stmtType = type; + args = methodArgs; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.deepHashCode(args); + result = prime * result + + ((stmtType == null) ? 0 : stmtType.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CacheKey other = (CacheKey) obj; + if (!Arrays.deepEquals(args, other.args)) { + return false; + } + if (stmtType == null) { + if (other.stmtType != null) { + return false; + } + } else if (!stmtType.equals(other.stmtType)) { + return false; + } + return true; + } + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCacheMBean.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCacheMBean.java new file mode 100644 index 0000000..f39904e --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCacheMBean.java @@ -0,0 +1,27 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.util.concurrent.atomic.AtomicInteger; + +public interface StatementCacheMBean { + boolean isCachePrepared(); + boolean isCacheCallable(); + int getMaxCacheSize(); + AtomicInteger getCacheSize(); + int getCacheSizePerConnection(); +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java new file mode 100644 index 0000000..6e53622 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java @@ -0,0 +1,306 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Implementation of JdbcInterceptor that proxies resultSets and statements. + * @author Guillermo Fernandes + */ +public class StatementDecoratorInterceptor extends AbstractCreateStatementInterceptor { + + private static final Log logger = LogFactory.getLog(StatementDecoratorInterceptor.class); + + protected static final String EXECUTE_QUERY = "executeQuery"; + protected static final String GET_GENERATED_KEYS = "getGeneratedKeys"; + protected static final String GET_RESULTSET = "getResultSet"; + + protected static final String[] RESULTSET_TYPES = {EXECUTE_QUERY, GET_GENERATED_KEYS, GET_RESULTSET}; + + /** + * the constructor to create the resultSet proxies + */ + protected static volatile Constructor resultSetConstructor = null; + + @Override + public void closeInvoked() { + // nothing to do + } + + /* + * Neither the class nor the constructor are exposed outside of jdbc-pool. + * Given the comments in the jdbc-pool code regarding caching for + * performance, continue to use Proxy.getProxyClass(). This will need to be + * revisited if that method is marked for removal. + */ + @SuppressWarnings("deprecation") + protected Constructor getResultSetConstructor() throws NoSuchMethodException { + if (resultSetConstructor == null) { + Class proxyClass = Proxy.getProxyClass(StatementDecoratorInterceptor.class.getClassLoader(), + new Class[] { ResultSet.class }); + resultSetConstructor = proxyClass.getConstructor(new Class[] { InvocationHandler.class }); + } + return resultSetConstructor; + } + + /** + * Creates a statement interceptor to monitor query response times + */ + @Override + public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) { + try { + String name = method.getName(); + Constructor constructor = null; + String sql = null; + if (compare(CREATE_STATEMENT, name)) { + // createStatement + constructor = getConstructor(CREATE_STATEMENT_IDX, Statement.class); + } else if (compare(PREPARE_STATEMENT, name)) { + // prepareStatement + constructor = getConstructor(PREPARE_STATEMENT_IDX, PreparedStatement.class); + sql = (String)args[0]; + } else if (compare(PREPARE_CALL, name)) { + // prepareCall + constructor = getConstructor(PREPARE_CALL_IDX, CallableStatement.class); + sql = (String)args[0]; + } else { + // do nothing, might be a future unsupported method + // so we better bail out and let the system continue + return statement; + } + return createDecorator(proxy, method, args, statement, constructor, sql); + } catch (Exception x) { + if (x instanceof InvocationTargetException) { + Throwable cause = x.getCause(); + if (cause instanceof ThreadDeath) { + throw (ThreadDeath) cause; + } + if (cause instanceof VirtualMachineError) { + throw (VirtualMachineError) cause; + } + } + logger.warn("Unable to create statement proxy for slow query report.", x); + } + return statement; + } + + /** + * Creates a proxy for a Statement. + * + * @param proxy The proxy object on which the method that triggered + * the creation of the statement was called. + * @param method The method that was called on the proxy + * @param args The arguments passed as part of the method call to + * the proxy + * @param statement The statement object that is to be proxied + * @param constructor The constructor for the desired proxy + * @param sql The sql of of the statement + * + * @return A new proxy for the Statement + * @throws InstantiationException Couldn't instantiate object + * @throws IllegalAccessException Inaccessible constructor + * @throws InvocationTargetException Exception thrown from constructor + */ + protected Object createDecorator(Object proxy, Method method, Object[] args, + Object statement, Constructor constructor, String sql) + throws InstantiationException, IllegalAccessException, InvocationTargetException { + Object result = null; + StatementProxy statementProxy = + new StatementProxy<>((Statement)statement,sql); + result = constructor.newInstance(new Object[] { statementProxy }); + statementProxy.setActualProxy(result); + statementProxy.setConnection(proxy); + statementProxy.setConstructor(constructor); + return result; + } + + protected boolean isExecuteQuery(String methodName) { + return EXECUTE_QUERY.equals(methodName); + } + + protected boolean isExecuteQuery(Method method) { + return isExecuteQuery(method.getName()); + } + + protected boolean isResultSet(Method method, boolean process) { + return process(RESULTSET_TYPES, method, process); + } + + /** + * Class to measure query execute time. + */ + protected class StatementProxy implements InvocationHandler { + + protected boolean closed = false; + protected T delegate; + private Object actualProxy; + private Object connection; + private String sql; + private Constructor constructor; + + public StatementProxy(T delegate, String sql) { + this.delegate = delegate; + this.sql = sql; + } + public T getDelegate() { + return this.delegate; + } + + public String getSql() { + return sql; + } + + public void setConnection(Object proxy) { + this.connection = proxy; + } + public Object getConnection() { + return this.connection; + } + + public void setActualProxy(Object proxy){ + this.actualProxy = proxy; + } + public Object getActualProxy() { + return this.actualProxy; + } + + + public Constructor getConstructor() { + return constructor; + } + public void setConstructor(Constructor constructor) { + this.constructor = constructor; + } + public void closeInvoked() { + if (getDelegate()!=null) { + try { + getDelegate().close(); + }catch (SQLException ignore) { + } + } + closed = true; + delegate = null; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (compare(TOSTRING_VAL,method)) { + return toString(); + } + // was close invoked? + boolean close = compare(CLOSE_VAL, method); + // allow close to be called multiple times + if (close && closed) { + return null; + } + // are we calling isClosed? + if (compare(ISCLOSED_VAL, method)) { + return Boolean.valueOf(closed); + } + // if we are calling anything else, bail out + if (closed) { + throw new SQLException("Statement closed."); + } + if (compare(GETCONNECTION_VAL,method)){ + return connection; + } + boolean process = false; + process = isResultSet(method, process); + // check to see if we are about to execute a query + // if we are executing, get the current time + Object result = null; + try { + // perform close cleanup + if (close) { + closeInvoked(); + } else { + // execute the query + result = method.invoke(delegate, args); + } + } catch (Throwable t) { + if (t instanceof InvocationTargetException + && t.getCause() != null) { + throw t.getCause(); + } else { + throw t; + } + } + if (process && result != null) { + Constructor cons = getResultSetConstructor(); + result = cons.newInstance(new Object[]{new ResultSetProxy(actualProxy, result)}); + } + return result; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(StatementProxy.class.getName()); + buf.append("[Proxy="); + buf.append(System.identityHashCode(this)); + buf.append("; Sql="); + buf.append(getSql()); + buf.append("; Delegate="); + buf.append(getDelegate()); + buf.append("; Connection="); + buf.append(getConnection()); + buf.append(']'); + return buf.toString(); + } + } + + protected static class ResultSetProxy implements InvocationHandler { + + private Object st; + private Object delegate; + + public ResultSetProxy(Object st, Object delegate) { + this.st = st; + this.delegate = delegate; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("getStatement")) { + return this.st; + } else { + try { + return method.invoke(this.delegate, args); + } catch (Throwable t) { + if (t instanceof InvocationTargetException + && t.getCause() != null) { + throw t.getCause(); + } else { + throw t; + } + } + } + } + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementFinalizer.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementFinalizer.java new file mode 100644 index 0000000..588b1f2 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementFinalizer.java @@ -0,0 +1,117 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.sql.Statement; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.PoolProperties; +import org.apache.tomcat.jdbc.pool.PooledConnection; + +/** + * Keeps track of statements associated with a connection and invokes close upon {@link java.sql.Connection#close()} + * Useful for applications that don't close the associated statements after being done with a connection. + * + */ +public class StatementFinalizer extends AbstractCreateStatementInterceptor { + private static final Log log = LogFactory.getLog(StatementFinalizer.class); + + protected List statements = new LinkedList<>(); + + private boolean logCreationStack = false; + + @Override + public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) { + try { + if (statement instanceof Statement) { + statements.add(new StatementEntry((Statement)statement)); + } + }catch (ClassCastException x) { + //ignore this one + } + return statement; + } + + @SuppressWarnings("null") // st is not null when used + @Override + public void closeInvoked() { + while (!statements.isEmpty()) { + StatementEntry ws = statements.remove(0); + Statement st = ws.getStatement(); + boolean shallClose = false; + try { + shallClose = st!=null && (!st.isClosed()); + if (shallClose) { + st.close(); + } + } catch (Exception ignore) { + if (log.isDebugEnabled()) { + log.debug("Unable to closed statement upon connection close.",ignore); + } + } finally { + if (logCreationStack && shallClose) { + log.warn("Statement created, but was not closed at:", ws.getAllocationStack()); + } + } + } + } + + @Override + public void setProperties(Map properties) { + super.setProperties(properties); + + PoolProperties.InterceptorProperty logProperty = properties.get("trace"); + if (null != logProperty) { + logCreationStack = logProperty.getValueAsBoolean(logCreationStack); + } + } + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + statements.clear(); + super.reset(parent, con); + } + + protected class StatementEntry { + private WeakReference statement; + private Throwable allocationStack; + + public StatementEntry(Statement statement) { + this.statement = new WeakReference<>(statement); + if (logCreationStack) { + this.allocationStack = new Throwable(); + } + } + + public Statement getStatement() { + return statement.get(); + } + + public Throwable getAllocationStack() { + return allocationStack; + } + } + + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/mbeans-descriptors.xml b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/mbeans-descriptors.xml new file mode 100644 index 0000000..4db1108 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/mbeans-descriptors.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + Slow query + + + + Failed query execution + + + + + \ No newline at end of file diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/ConnectionPool.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/ConnectionPool.java new file mode 100644 index 0000000..288162f --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/ConnectionPool.java @@ -0,0 +1,910 @@ +/* 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.tomcat.jdbc.pool.jmx; + +import java.util.Properties; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.management.MBeanNotificationInfo; +import javax.management.MBeanRegistration; +import javax.management.MBeanServer; +import javax.management.Notification; +import javax.management.NotificationBroadcasterSupport; +import javax.management.NotificationListener; +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.PoolConfiguration; +import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorDefinition; +import org.apache.tomcat.jdbc.pool.PoolUtilities; +import org.apache.tomcat.jdbc.pool.Validator; + +public class ConnectionPool extends NotificationBroadcasterSupport + implements ConnectionPoolMBean, MBeanRegistration { + + /** + * logger + */ + private static final Log log = LogFactory.getLog(ConnectionPool.class); + + /** + * the connection pool + */ + protected org.apache.tomcat.jdbc.pool.ConnectionPool pool = null; + /** + * sequence for JMX notifications + */ + protected AtomicInteger sequence = new AtomicInteger(0); + + /** + * Listeners that are local and interested in our notifications, no need for JMX + */ + protected ConcurrentLinkedQueue listeners = + new ConcurrentLinkedQueue<>(); + + /** + * the ObjectName of this pool. + */ + private ObjectName oname = null; + + public ConnectionPool(org.apache.tomcat.jdbc.pool.ConnectionPool pool) { + super(); + this.pool = pool; + } + + public org.apache.tomcat.jdbc.pool.ConnectionPool getPool() { + return pool; + } + + public PoolConfiguration getPoolProperties() { + return pool.getPoolProperties(); + } + + public ObjectName getObjectName() { + return oname; + } + + @Override + public ObjectName preRegister(MBeanServer server, ObjectName name) + throws Exception { + this.oname = name; + return name; + } + + @Override + public void postRegister(Boolean registrationDone) { + } + + @Override + public void preDeregister() throws Exception { + } + + @Override + public void postDeregister() { + } + + //================================================================= + // NOTIFICATION INFO + //================================================================= + public static final String NOTIFY_INIT = "INIT FAILED"; + public static final String NOTIFY_CONNECT = "CONNECTION FAILED"; + public static final String NOTIFY_ABANDON = "CONNECTION ABANDONED"; + public static final String SLOW_QUERY_NOTIFICATION = "SLOW QUERY"; + public static final String FAILED_QUERY_NOTIFICATION = "FAILED QUERY"; + public static final String SUSPECT_ABANDONED_NOTIFICATION = "SUSPECT CONNECTION ABANDONED"; + public static final String POOL_EMPTY = "POOL EMPTY"; + public static final String SUSPECT_RETURNED_NOTIFICATION = "SUSPECT CONNECTION RETURNED"; + + @Override + public MBeanNotificationInfo[] getNotificationInfo() { + MBeanNotificationInfo[] pres = super.getNotificationInfo(); + MBeanNotificationInfo[] loc = getDefaultNotificationInfo(); + MBeanNotificationInfo[] aug = new MBeanNotificationInfo[pres.length + loc.length]; + if (pres.length>0) { + System.arraycopy(pres, 0, aug, 0, pres.length); + } + if (loc.length >0) { + System.arraycopy(loc, 0, aug, pres.length, loc.length); + } + return aug; + } + + public static MBeanNotificationInfo[] getDefaultNotificationInfo() { + String[] types = new String[] {NOTIFY_INIT, NOTIFY_CONNECT, NOTIFY_ABANDON, SLOW_QUERY_NOTIFICATION, + FAILED_QUERY_NOTIFICATION, SUSPECT_ABANDONED_NOTIFICATION, POOL_EMPTY, SUSPECT_RETURNED_NOTIFICATION}; + String name = Notification.class.getName(); + String description = "A connection pool error condition was met."; + MBeanNotificationInfo info = new MBeanNotificationInfo(types, name, description); + return new MBeanNotificationInfo[] {info}; + } + + /** + * Return true if the notification was sent successfully, false otherwise. + * @param type The notification type + * @param message The message + * @return true if the notification succeeded + */ + public boolean notify(final String type, String message) { + try { + Notification n = new Notification( + type, + this, + sequence.incrementAndGet(), + System.currentTimeMillis(), + "["+type+"] "+message); + sendNotification(n); + for (NotificationListener listener : listeners) { + listener.handleNotification(n,this); + } + return true; + }catch (Exception x) { + if (log.isDebugEnabled()) { + log.debug("Notify failed. Type="+type+"; Message="+message,x); + } + return false; + } + + } + + public void addListener(NotificationListener list) { + listeners.add(list); + } + + public boolean removeListener(NotificationListener list) { + return listeners.remove(list); + } + + //================================================================= + // POOL STATS + //================================================================= + + @Override + public int getSize() { + return pool.getSize(); + } + + @Override + public int getIdle() { + return pool.getIdle(); + } + + @Override + public int getActive() { + return pool.getActive(); + } + + @Override + public int getNumIdle() { + return getIdle(); + } + + @Override + public int getNumActive() { + return getActive(); + } + + @Override + public int getWaitCount() { + return pool.getWaitCount(); + } + + @Override + public long getBorrowedCount() { + return pool.getBorrowedCount(); + } + + @Override + public long getReturnedCount() { + return pool.getReturnedCount(); + } + + @Override + public long getCreatedCount() { + return pool.getCreatedCount(); + } + + @Override + public long getReleasedCount() { + return pool.getReleasedCount(); + } + + @Override + public long getReconnectedCount() { + return pool.getReconnectedCount(); + } + + @Override + public long getRemoveAbandonedCount() { + return pool.getRemoveAbandonedCount(); + } + + @Override + public long getReleasedIdleCount() { + return pool.getReleasedIdleCount(); + } + + //================================================================= + // POOL OPERATIONS + //================================================================= + @Override + public void checkIdle() { + pool.checkIdle(); + } + + @Override + public void checkAbandoned() { + pool.checkAbandoned(); + } + + @Override + public void testIdle() { + pool.testAllIdle(); + } + + @Override + public void resetStats() { + pool.resetStats(); + } + + //================================================================= + // POOL PROPERTIES + //================================================================= + //========================================================= + // PROPERTIES / CONFIGURATION + //========================================================= + + + @Override + public String getConnectionProperties() { + return getPoolProperties().getConnectionProperties(); + } + + @Override + public Properties getDbProperties() { + return PoolUtilities.cloneWithoutPassword(getPoolProperties().getDbProperties()); + } + + @Override + public String getDefaultCatalog() { + return getPoolProperties().getDefaultCatalog(); + } + + @Override + public int getDefaultTransactionIsolation() { + return getPoolProperties().getDefaultTransactionIsolation(); + } + + @Override + public String getDriverClassName() { + return getPoolProperties().getDriverClassName(); + } + + + @Override + public int getInitialSize() { + return getPoolProperties().getInitialSize(); + } + + @Override + public String getInitSQL() { + return getPoolProperties().getInitSQL(); + } + + @Override + public String getJdbcInterceptors() { + return getPoolProperties().getJdbcInterceptors(); + } + + @Override + public int getMaxActive() { + return getPoolProperties().getMaxActive(); + } + + @Override + public int getMaxIdle() { + return getPoolProperties().getMaxIdle(); + } + + @Override + public int getMaxWait() { + return getPoolProperties().getMaxWait(); + } + + @Override + public int getMinEvictableIdleTimeMillis() { + return getPoolProperties().getMinEvictableIdleTimeMillis(); + } + + @Override + public int getMinIdle() { + return getPoolProperties().getMinIdle(); + } + + @Override + public long getMaxAge() { + return getPoolProperties().getMaxAge(); + } + + @Override + public String getName() { + return this.getPoolName(); + } + + @Override + public int getNumTestsPerEvictionRun() { + return getPoolProperties().getNumTestsPerEvictionRun(); + } + + /** + * @return DOES NOT RETURN THE PASSWORD, IT WOULD SHOW UP IN JMX + */ + @Override + public String getPassword() { + return "Password not available as DataSource/JMX operation."; + } + + @Override + public int getRemoveAbandonedTimeout() { + return getPoolProperties().getRemoveAbandonedTimeout(); + } + + + @Override + public int getTimeBetweenEvictionRunsMillis() { + return getPoolProperties().getTimeBetweenEvictionRunsMillis(); + } + + @Override + public String getUrl() { + return getPoolProperties().getUrl(); + } + + @Override + public String getUsername() { + return getPoolProperties().getUsername(); + } + + @Override + public long getValidationInterval() { + return getPoolProperties().getValidationInterval(); + } + + @Override + public String getValidationQuery() { + return getPoolProperties().getValidationQuery(); + } + + @Override + public int getValidationQueryTimeout() { + return getPoolProperties().getValidationQueryTimeout(); + } + + + @Override + public String getValidatorClassName() { + return getPoolProperties().getValidatorClassName(); + } + + + @Override + public Validator getValidator() { + return getPoolProperties().getValidator(); + } + + @Override + public boolean isAccessToUnderlyingConnectionAllowed() { + return getPoolProperties().isAccessToUnderlyingConnectionAllowed(); + } + + @Override + public Boolean isDefaultAutoCommit() { + return getPoolProperties().isDefaultAutoCommit(); + } + + @Override + public Boolean isDefaultReadOnly() { + return getPoolProperties().isDefaultReadOnly(); + } + + @Override + public boolean isLogAbandoned() { + return getPoolProperties().isLogAbandoned(); + } + + @Override + public boolean isPoolSweeperEnabled() { + return getPoolProperties().isPoolSweeperEnabled(); + } + + @Override + public boolean isRemoveAbandoned() { + return getPoolProperties().isRemoveAbandoned(); + } + + @Override + public int getAbandonWhenPercentageFull() { + return getPoolProperties().getAbandonWhenPercentageFull(); + } + + @Override + public boolean isTestOnBorrow() { + return getPoolProperties().isTestOnBorrow(); + } + + @Override + public boolean isTestOnConnect() { + return getPoolProperties().isTestOnConnect(); + } + + @Override + public boolean isTestOnReturn() { + return getPoolProperties().isTestOnReturn(); + } + + @Override + public boolean isTestWhileIdle() { + return getPoolProperties().isTestWhileIdle(); + } + + + @Override + public Boolean getDefaultAutoCommit() { + return getPoolProperties().getDefaultAutoCommit(); + } + + @Override + public Boolean getDefaultReadOnly() { + return getPoolProperties().getDefaultReadOnly(); + } + + @Override + public InterceptorDefinition[] getJdbcInterceptorsAsArray() { + return getPoolProperties().getJdbcInterceptorsAsArray(); + } + + @Override + public boolean getUseLock() { + return getPoolProperties().getUseLock(); + } + + @Override + public boolean isFairQueue() { + return getPoolProperties().isFairQueue(); + } + + @Override + public boolean isJmxEnabled() { + return getPoolProperties().isJmxEnabled(); + } + + @Override + public boolean isUseEquals() { + return getPoolProperties().isUseEquals(); + } + + @Override + public void setAbandonWhenPercentageFull(int percentage) { + getPoolProperties().setAbandonWhenPercentageFull(percentage); + } + + @Override + public void setAccessToUnderlyingConnectionAllowed(boolean accessToUnderlyingConnectionAllowed) { + getPoolProperties().setAccessToUnderlyingConnectionAllowed(accessToUnderlyingConnectionAllowed); + } + + @Override + public void setDbProperties(Properties dbProperties) { + getPoolProperties().setDbProperties(dbProperties); + } + + @Override + public void setDefaultReadOnly(Boolean defaultReadOnly) { + getPoolProperties().setDefaultReadOnly(defaultReadOnly); + } + + @Override + public void setMaxAge(long maxAge) { + boolean wasEnabled = getPoolProperties().isPoolSweeperEnabled(); + getPoolProperties().setMaxAge(maxAge); + //make sure the pool is properly configured + pool.checkPoolConfiguration(getPoolProperties()); + poolCleanerAttributeUpdated(wasEnabled); + } + + @Override + public void setName(String name) { + getPoolProperties().setName(name); + } + + @Override + public String getPoolName() { + return getPoolProperties().getName(); + } + + + @Override + public void setConnectionProperties(String connectionProperties) { + getPoolProperties().setConnectionProperties(connectionProperties); + + } + + @Override + public void setDefaultAutoCommit(Boolean defaultAutoCommit) { + getPoolProperties().setDefaultAutoCommit(defaultAutoCommit); + } + + @Override + public void setDefaultCatalog(String defaultCatalog) { + getPoolProperties().setDefaultCatalog(defaultCatalog); + } + + @Override + public void setDefaultTransactionIsolation(int defaultTransactionIsolation) { + getPoolProperties().setDefaultTransactionIsolation(defaultTransactionIsolation); + } + + @Override + public void setDriverClassName(String driverClassName) { + getPoolProperties().setDriverClassName(driverClassName); + } + + + @Override + public void setFairQueue(boolean fairQueue) { + // noop - this pool is already running + throw new UnsupportedOperationException(); + } + + + @Override + public void setInitialSize(int initialSize) { + // noop - this pool is already running + throw new UnsupportedOperationException(); + + } + + + @Override + public void setInitSQL(String initSQL) { + getPoolProperties().setInitSQL(initSQL); + + } + + + @Override + public void setJdbcInterceptors(String jdbcInterceptors) { + // noop - this pool is already running + throw new UnsupportedOperationException(); + } + + + @Override + public void setJmxEnabled(boolean jmxEnabled) { + // noop - this pool is already running and obviously jmx enabled + throw new UnsupportedOperationException(); + } + + + @Override + public void setLogAbandoned(boolean logAbandoned) { + getPoolProperties().setLogAbandoned(logAbandoned); + } + + + @Override + public void setMaxActive(int maxActive) { + getPoolProperties().setMaxActive(maxActive); + //make sure the pool is properly configured + pool.checkPoolConfiguration(getPoolProperties()); + } + + + @Override + public void setMaxIdle(int maxIdle) { + getPoolProperties().setMaxIdle(maxIdle); + //make sure the pool is properly configured + pool.checkPoolConfiguration(getPoolProperties()); + + } + + + @Override + public void setMaxWait(int maxWait) { + getPoolProperties().setMaxWait(maxWait); + } + + + @Override + public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) { + boolean wasEnabled = getPoolProperties().isPoolSweeperEnabled(); + getPoolProperties().setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + poolCleanerAttributeUpdated(wasEnabled); + } + + + @Override + public void setMinIdle(int minIdle) { + getPoolProperties().setMinIdle(minIdle); + //make sure the pool is properly configured + pool.checkPoolConfiguration(getPoolProperties()); + } + + + @Override + public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) { + getPoolProperties().setNumTestsPerEvictionRun(numTestsPerEvictionRun); + } + + + @Override + public void setPassword(String password) { + getPoolProperties().setPassword(password); + } + + + @Override + public void setRemoveAbandoned(boolean removeAbandoned) { + boolean wasEnabled = getPoolProperties().isPoolSweeperEnabled(); + getPoolProperties().setRemoveAbandoned(removeAbandoned); + poolCleanerAttributeUpdated(wasEnabled); + } + + + @Override + public void setRemoveAbandonedTimeout(int removeAbandonedTimeout) { + boolean wasEnabled = getPoolProperties().isPoolSweeperEnabled(); + getPoolProperties().setRemoveAbandonedTimeout(removeAbandonedTimeout); + poolCleanerAttributeUpdated(wasEnabled); + } + + + @Override + public void setTestOnBorrow(boolean testOnBorrow) { + getPoolProperties().setTestOnBorrow(testOnBorrow); + } + + + @Override + public void setTestOnConnect(boolean testOnConnect) { + getPoolProperties().setTestOnConnect(testOnConnect); + } + + + @Override + public void setTestOnReturn(boolean testOnReturn) { + getPoolProperties().setTestOnReturn(testOnReturn); + } + + + @Override + public void setTestWhileIdle(boolean testWhileIdle) { + boolean wasEnabled = getPoolProperties().isPoolSweeperEnabled(); + getPoolProperties().setTestWhileIdle(testWhileIdle); + poolCleanerAttributeUpdated(wasEnabled); + } + + + @Override + public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) { + boolean wasEnabled = getPoolProperties().isPoolSweeperEnabled(); + getPoolProperties().setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + //make sure the pool is properly configured + pool.checkPoolConfiguration(getPoolProperties()); + poolCleanerAttributeUpdated(wasEnabled); + } + + /** + * Starts/stops pool cleaner thread as necessary after its configuration properties + * were updated. + * + * This method must be called after configuration properties affecting the pool cleaner + * have been updated. + * + * @param wasEnabled whether the pool cleaner was enabled before the configuration change occurred. + */ + private void poolCleanerAttributeUpdated(boolean wasEnabled) { + boolean shouldBeEnabled = getPoolProperties().isPoolSweeperEnabled(); + //make sure pool cleaner starts/stops when it should + if (!wasEnabled && shouldBeEnabled) { + pool.initializePoolCleaner(getPoolProperties()); + } else if (wasEnabled) { + pool.terminatePoolCleaner(); + if (shouldBeEnabled) { + pool.initializePoolCleaner(getPoolProperties()); + } + } + } + + @Override + public void setUrl(String url) { + getPoolProperties().setUrl(url); + } + + + @Override + public void setUseEquals(boolean useEquals) { + getPoolProperties().setUseEquals(useEquals); + } + + + @Override + public void setUseLock(boolean useLock) { + getPoolProperties().setUseLock(useLock); + } + + + @Override + public void setUsername(String username) { + getPoolProperties().setUsername(username); + } + + + @Override + public void setValidationInterval(long validationInterval) { + getPoolProperties().setValidationInterval(validationInterval); + } + + + @Override + public void setValidationQuery(String validationQuery) { + getPoolProperties().setValidationQuery(validationQuery); + } + + @Override + public void setValidationQueryTimeout(int validationQueryTimeout) { + getPoolProperties().setValidationQueryTimeout(validationQueryTimeout); + } + + + @Override + public void setValidatorClassName(String className) { + getPoolProperties().setValidatorClassName(className); + } + + + @Override + public int getSuspectTimeout() { + return getPoolProperties().getSuspectTimeout(); + } + + + @Override + public void setSuspectTimeout(int seconds) { + getPoolProperties().setSuspectTimeout(seconds); + } + + @Override + public void setDataSource(Object ds) { + getPoolProperties().setDataSource(ds); + } + + @Override + public Object getDataSource() { + return getPoolProperties().getDataSource(); + } + + + @Override + public void setDataSourceJNDI(String jndiDS) { + getPoolProperties().setDataSourceJNDI(jndiDS); + } + + @Override + public String getDataSourceJNDI() { + return getPoolProperties().getDataSourceJNDI(); + } + + @Override + public boolean isAlternateUsernameAllowed() { + return getPoolProperties().isAlternateUsernameAllowed(); + } + + @Override + public void setAlternateUsernameAllowed(boolean alternateUsernameAllowed) { + getPoolProperties().setAlternateUsernameAllowed(alternateUsernameAllowed); + } + + @Override + public void setValidator(Validator validator) { + getPoolProperties().setValidator(validator); + } + + @Override + public void setCommitOnReturn(boolean commitOnReturn) { + getPoolProperties().setCommitOnReturn(commitOnReturn); + } + + @Override + public boolean getCommitOnReturn() { + return getPoolProperties().getCommitOnReturn(); + } + + @Override + public void setRollbackOnReturn(boolean rollbackOnReturn) { + getPoolProperties().setRollbackOnReturn(rollbackOnReturn); + } + + @Override + public boolean getRollbackOnReturn() { + return getPoolProperties().getRollbackOnReturn(); + } + + @Override + public void setUseDisposableConnectionFacade(boolean useDisposableConnectionFacade) { + getPoolProperties().setUseDisposableConnectionFacade(useDisposableConnectionFacade); + } + + @Override + public boolean getUseDisposableConnectionFacade() { + return getPoolProperties().getUseDisposableConnectionFacade(); + } + + @Override + public void setLogValidationErrors(boolean logValidationErrors) { + getPoolProperties().setLogValidationErrors(logValidationErrors); + } + + @Override + public boolean getLogValidationErrors() { + return getPoolProperties().getLogValidationErrors(); + } + + + @Override + public boolean getPropagateInterruptState() { + return getPoolProperties().getPropagateInterruptState(); + } + + @Override + public void setPropagateInterruptState(boolean propagateInterruptState) { + getPoolProperties().setPropagateInterruptState(propagateInterruptState); + } + + @Override + public boolean isIgnoreExceptionOnPreLoad() { + return getPoolProperties().isIgnoreExceptionOnPreLoad(); + } + + @Override + public void setIgnoreExceptionOnPreLoad(boolean ignoreExceptionOnPreLoad) { + // noop - this pool is already running + throw new UnsupportedOperationException(); + } + + @Override + public boolean getUseStatementFacade() { + return getPoolProperties().getUseStatementFacade(); + } + + @Override + public void setUseStatementFacade(boolean useStatementFacade) { + getPoolProperties().setUseStatementFacade(useStatementFacade); + } + + @Override + public void purge() { + pool.purge(); + + } + + @Override + public void purgeOnReturn() { + pool.purgeOnReturn(); + + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/ConnectionPoolMBean.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/ConnectionPoolMBean.java new file mode 100644 index 0000000..8117649 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/ConnectionPoolMBean.java @@ -0,0 +1,87 @@ +/* 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.tomcat.jdbc.pool.jmx; + +import org.apache.tomcat.jdbc.pool.PoolConfiguration; + +public interface ConnectionPoolMBean extends PoolConfiguration { + + //================================================================= + // POOL STATS + //================================================================= + + int getSize(); + + int getIdle(); + + int getActive(); + + int getNumIdle(); + + int getNumActive(); + + int getWaitCount(); + + long getBorrowedCount(); + + long getReturnedCount(); + + long getCreatedCount(); + + long getReleasedCount(); + + long getReconnectedCount(); + + long getRemoveAbandonedCount(); + + long getReleasedIdleCount(); + + //================================================================= + // POOL OPERATIONS + //================================================================= + void checkIdle(); + + void checkAbandoned(); + + void testIdle(); + + /** + * Purges all connections in the pool. + * For connections currently in use, these connections will be + * purged when returned on the pool. This call also + * purges connections that are idle and in the pool + * To only purge used/active connections see {@link #purgeOnReturn()} + */ + void purge(); + + /** + * Purges connections when they are returned from the pool. + * This call does not purge idle connections until they are used. + * To purge idle connections see {@link #purge()} + */ + void purgeOnReturn(); + + /** + * reset the statistics of this pool. + */ + void resetStats(); + + //================================================================= + // POOL NOTIFICATIONS + //================================================================= + + +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/JmxUtil.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/JmxUtil.java new file mode 100644 index 0000000..c93a3c4 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/jmx/JmxUtil.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat.jdbc.pool.jmx; + + +import java.lang.management.ManagementFactory; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class JmxUtil { + private static final Log log = LogFactory.getLog(JmxUtil.class); + + public static ObjectName registerJmx(ObjectName base, String keyprop, Object obj) { + ObjectName oname = null; + try { + oname = getObjectName(base, keyprop); + if (oname != null) { + ManagementFactory.getPlatformMBeanServer().registerMBean(obj, oname); + } + } catch (Exception e) { + log.error("Jmx registration failed.",e); + } + return oname; + } + + public static void unregisterJmx(ObjectName oname) { + if (oname ==null) { + return; + } + try { + ManagementFactory.getPlatformMBeanServer().unregisterMBean(oname); + } catch (Exception e) { + log.error("Jmx unregistration failed.",e); + } + } + + private static ObjectName getObjectName(ObjectName base, String keyprop) + throws MalformedObjectNameException { + if (base == null) { + return null; + } + StringBuilder OnameStr = new StringBuilder(base.toString()); + if (keyprop != null) { + OnameStr.append(keyprop); + } + ObjectName oname = new ObjectName(OnameStr.toString()); + return oname; + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/mbeans-descriptors.xml b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/mbeans-descriptors.xml new file mode 100644 index 0000000..677a1b4 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/mbeans-descriptors.xml @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug51582.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug51582.java new file mode 100644 index 0000000..a281aa7 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug51582.java @@ -0,0 +1,105 @@ +/* + * 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.tomcat.jdbc.bugs; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.PoolConfiguration; +import org.apache.tomcat.jdbc.test.DefaultProperties; + +public class Bug51582 { + + public static void main(String[] args) throws SQLException { + org.apache.tomcat.jdbc.pool.DataSource datasource = null; + PoolConfiguration p = new DefaultProperties(); + p.setJmxEnabled(true); + p.setTestOnBorrow(false); + p.setTestOnReturn(false); + p.setValidationInterval(1000); + p.setTimeBetweenEvictionRunsMillis(2000); + p.setMaxWait(2000); + p.setMinEvictableIdleTimeMillis(1000); + datasource = new org.apache.tomcat.jdbc.pool.DataSource(); + datasource.setPoolProperties(p); + datasource.setJdbcInterceptors("org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReportJmx(threshold=200)"); + ConnectionPool pool = datasource.createPool(); + Connection con = pool.getConnection(); + Statement st = con.createStatement(); + try { + st.execute("DROP ALIAS SLEEP"); + } catch (Exception ignore) { + // Ignore + } + st.execute("CREATE ALIAS SLEEP AS $$\nboolean sleep() {\n try {\n Thread.sleep(10000);\n return true; } catch (Exception x) {\n return false;\n }\n}\n$$;"); + st.close(); + con.close(); + int iter = 0; + while ((iter++) < 10) { + final Connection connection = pool.getConnection(); + final CallableStatement s = connection.prepareCall("{CALL SLEEP()}"); + List threadList = new ArrayList<>(); + for (int l = 0; l < 3; l++) { + final int i = l; + Thread thread = new Thread() { + + @Override + public void run() { + try { + if (i == 0) { + sleep(1000); + s.cancel(); + } else if (i == 1) { + // or use some other statement which will block + // for a longer time + long start = System.currentTimeMillis(); + System.out.println("[" + getName() + + "] Calling SP SLEEP"); + s.execute(); + System.out.println("[" + getName() + + "] Executed SP SLEEP [" + + (System.currentTimeMillis() - start) + + "]"); + } else { + sleep(1000); + connection.close(); + } + } catch (InterruptedException e) { + } catch (SQLException e) { + e.printStackTrace(); + } + } + }; + threadList.add(thread); + thread.start(); + } + for (Thread t : threadList) { + try { + t.join(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug53367.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug53367.java new file mode 100644 index 0000000..be0c6c4 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug53367.java @@ -0,0 +1,177 @@ +/* + * 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.tomcat.jdbc.bugs; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PoolExhaustedException; +import org.apache.tomcat.jdbc.pool.PoolProperties; +import org.apache.tomcat.jdbc.test.DefaultProperties; + +@RunWith(Parameterized.class) +public class Bug53367 { + + private boolean fairQueue; + + public Bug53367(boolean fair) { + this.fairQueue = fair; + } + + @Parameterized.Parameters + public static Collection parameters() { + return Arrays.asList(new Object[][]{ + new Object[] {Boolean.TRUE}, + new Object[] {Boolean.FALSE}, + }); + } + + @Test + public void testPool() throws SQLException, InterruptedException { + DriverManager.setLoginTimeout(1); + PoolProperties poolProperties = new DefaultProperties(); + int threadsCount = 3; + poolProperties.setMaxActive(threadsCount); + poolProperties.setMaxIdle(threadsCount); + poolProperties.setMinIdle(0); + poolProperties.setMaxWait(5000); + poolProperties.setInitialSize(0); + poolProperties.setRemoveAbandoned(true); + poolProperties.setRemoveAbandonedTimeout(300); + poolProperties.setRollbackOnReturn(true); + poolProperties.setFairQueue(fairQueue); + final DataSource ds = new DataSource(poolProperties); + + final CountDownLatch openedLatch = new CountDownLatch(threadsCount); + final CountDownLatch closedLatch = new CountDownLatch(threadsCount); + final CountDownLatch toCloseLatch = new CountDownLatch(1); + + for (int i = 0; i < threadsCount; i++) { + new Thread(new Runnable() { + @Override + public void run() { + try { + Connection connection = ds.getConnection(); + openedLatch.countDown(); + + toCloseLatch.await(); + connection.close(); + + closedLatch.countDown(); + + } catch (Exception e) { + System.err.println("Step 1:"+e.getMessage()); + } + } + }).start(); + } + + openedLatch.await(); + ConnectionPool pool = ds.getPool(); + //Now we have 3 initialized busy connections + Assert.assertEquals(0, pool.getIdle()); + Assert.assertEquals(threadsCount, pool.getActive()); + Assert.assertEquals(threadsCount, pool.getSize()); + + List threads = new ArrayList<>(); + for (int i = 0; i < threadsCount; i++) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + // Expected to fail + try (Connection c = ds.getConnection()) { + } catch (Exception e) { + System.err.println("Step 2:"+e.getMessage()); + } + } + }); + thread.start(); + threads.add(thread); + } + for (Thread thread : threads) { + thread.interrupt(); + } + for (Thread thread : threads) { + thread.join(); + } + //Still 3 active connections + Assert.assertEquals(0, pool.getIdle()); + Assert.assertEquals(threadsCount, pool.getActive()); + Assert.assertEquals(threadsCount, pool.getSize()); + + toCloseLatch.countDown(); + closedLatch.await(); + + //Here comes the bug! No more active connections and unable to establish new connections. + + Assert.assertEquals(threadsCount, pool.getIdle()); // <-- Should be threadsCount (3) here + Assert.assertEquals(0, pool.getActive()); + Assert.assertEquals(threadsCount, pool.getSize()); + + final AtomicInteger failedCount = new AtomicInteger(); + final ArrayBlockingQueue cons = new ArrayBlockingQueue<>(threadsCount); + threads.clear(); + for (int i = 0; i < threadsCount; i++) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + try { + cons.add(ds.getConnection()); + } catch (PoolExhaustedException e) { + failedCount.incrementAndGet(); + System.err.println("Step 3:"+e.getMessage()); + } catch (Exception e) { + System.err.println("Step 4:"+e.getMessage()); + throw new RuntimeException(e); + } + } + }); + thread.start(); + threads.add(thread); + } + + for (Thread thread : threads) { + thread.join(); + } + Assert.assertEquals(0, failedCount.get()); + + Assert.assertEquals(0, pool.getIdle()); + Assert.assertEquals(threadsCount, pool.getActive()); + Assert.assertEquals(threadsCount, pool.getSize()); + for (Connection con : cons) { + con.close(); + } + Assert.assertEquals(threadsCount, pool.getIdle()); + Assert.assertEquals(0, pool.getActive()); + Assert.assertEquals(threadsCount, pool.getSize()); + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54225.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54225.java new file mode 100644 index 0000000..be59026 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54225.java @@ -0,0 +1,64 @@ +/* + * 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.tomcat.jdbc.bugs; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PoolProperties; +import org.apache.tomcat.jdbc.test.DefaultProperties; + + +@RunWith(Parameterized.class) +public class Bug54225 { + + private String initSQL; + + public Bug54225(String initSQL) { + this.initSQL = initSQL; + } + + @Parameterized.Parameters + public static Collection parameters() { + return Arrays.asList(new Object[][]{ + new Object[] {""}, + new Object[] {null}, + }); + } + + @Test + public void testPool() throws SQLException { + PoolProperties poolProperties = new DefaultProperties(); + poolProperties.setMinIdle(0); + poolProperties.setInitialSize(0); + poolProperties.setMaxWait(5000); + poolProperties.setRemoveAbandoned(true); + poolProperties.setRemoveAbandonedTimeout(300); + poolProperties.setRollbackOnReturn(true); + poolProperties.setInitSQL(initSQL); + final DataSource ds = new DataSource(poolProperties); + ds.getConnection().close(); + Assert.assertNull(poolProperties.getInitSQL()); + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54227.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54227.java new file mode 100644 index 0000000..82a7504 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54227.java @@ -0,0 +1,65 @@ +/* + * 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.tomcat.jdbc.bugs; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.PooledConnection; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PoolProperties; +import org.apache.tomcat.jdbc.test.DefaultProperties; + +public class Bug54227 { + + + public Bug54227() { + } + + @Test + public void testPool() throws SQLException, InterruptedException { + PoolProperties poolProperties = new DefaultProperties(); + poolProperties.setMinIdle(0); + poolProperties.setInitialSize(0); + poolProperties.setMaxActive(1); + poolProperties.setMaxWait(5000); + poolProperties.setMaxAge(100); + poolProperties.setRemoveAbandoned(false); + + final DataSource ds = new DataSource(poolProperties); + Connection con; + Connection actual1; + Connection actual2; + + con = ds.getConnection(); + actual1 = ((PooledConnection)con).getConnection(); + con.close(); + con = ds.getConnection(); + actual2 = ((PooledConnection)con).getConnection(); + Assert.assertSame(actual1, actual2); + con.close(); + Thread.sleep(150); + con = ds.getConnection(); + actual2 = ((PooledConnection)con).getConnection(); + Assert.assertNotSame(actual1, actual2); + con.close(); + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54978.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54978.java new file mode 100644 index 0000000..4459a61 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/bugs/Bug54978.java @@ -0,0 +1,66 @@ +/* + * 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.tomcat.jdbc.bugs; + +import java.sql.SQLException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PoolProperties; +import org.apache.tomcat.jdbc.test.DefaultProperties; + +public class Bug54978 { + + @Test + public void testIllegalValidationQuery() { + PoolProperties poolProperties = new DefaultProperties(); + poolProperties.setMinIdle(0); + poolProperties.setInitialSize(1); + poolProperties.setMaxActive(1); + poolProperties.setMaxWait(5000); + poolProperties.setMaxAge(100); + poolProperties.setRemoveAbandoned(false); + poolProperties.setTestOnBorrow(true); + poolProperties.setTestOnConnect(false); + poolProperties.setValidationQuery("sdadsada"); + final DataSource ds = new DataSource(poolProperties); + try { + ds.getConnection().close(); + Assert.fail("Validation should have failed."); + }catch (SQLException x) { + } + } + + @Test + public void testIllegalValidationQueryWithLegalInit() throws SQLException { + PoolProperties poolProperties = new DefaultProperties(); + poolProperties.setMinIdle(0); + poolProperties.setInitialSize(1); + poolProperties.setMaxActive(1); + poolProperties.setMaxWait(5000); + poolProperties.setMaxAge(100); + poolProperties.setRemoveAbandoned(false); + poolProperties.setTestOnBorrow(true); + poolProperties.setTestOnConnect(false); + poolProperties.setValidationQuery("sdadsada"); + poolProperties.setInitSQL("SELECT 1"); + final DataSource ds = new DataSource(poolProperties); + ds.getConnection().close(); + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/PoolPropertiesTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/PoolPropertiesTest.java new file mode 100644 index 0000000..97f18fd --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/PoolPropertiesTest.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.jdbc.pool; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class PoolPropertiesTest { + private static final String DEFAULT_USER = "username_def"; + private static final String DEFAULT_PASSWD = "password_def"; + @Test + public void toStringOutputShouldHaveBalancedBrackets() { + PoolProperties properties = new PoolProperties(); + properties.setUsername(DEFAULT_USER); + properties.setPassword(DEFAULT_PASSWD); + properties.setAlternateUsernameAllowed(true); + properties.setInitialSize(0); + properties.setRemoveAbandoned(false); + properties.setTimeBetweenEvictionRunsMillis(-1); + + String asString = properties.toString(); + + List stack = new ArrayList<>(); + for (char c : asString.toCharArray()) { + switch (c) { + case '{': + case '(': + case '[': stack.add(Character.valueOf(c)); break; + case '}': Assert.assertEquals('{', stack.remove(stack.size() - 1).charValue()); break; + case ')': Assert.assertEquals('(', stack.remove(stack.size() - 1).charValue()); break; + case ']': Assert.assertEquals('[', stack.remove(stack.size() - 1).charValue()); break; + default: break; + } + } + Assert.assertEquals("All brackets should have been closed", 0, stack.size()); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/ShouldForceReconnectTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/ShouldForceReconnectTest.java new file mode 100644 index 0000000..db2865a --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/ShouldForceReconnectTest.java @@ -0,0 +1,140 @@ +/* + * 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.tomcat.jdbc.pool; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class ShouldForceReconnectTest { + + private ConnectionPool pool; + private PoolProperties properties; + + private static final String DEFAULT_USER = "username_def"; + private static final String DEFAULT_PASSWD = "password_def"; + private static final String ALT_USER = "username_alt"; + private static final String ALT_PASSWD = "password_alt"; + + @Before + public void setUp() throws Exception { + properties = new PoolProperties(); + properties.setUsername(DEFAULT_USER); + properties.setPassword(DEFAULT_PASSWD); + properties.setAlternateUsernameAllowed(true); + properties.setInitialSize(0); + properties.setRemoveAbandoned(false); + properties.setTimeBetweenEvictionRunsMillis(-1); + pool = new ConnectionPool(properties); + } + + @After + public void tearDown() throws Exception { + + + + + } + + @Test + public void testShouldForceReconnect() throws Exception { + PooledConnection con = new PooledConnection(properties, pool); + + //connection previously connect with default + configureDefault(con); + Assert.assertFalse(con.shouldForceReconnect(null, null)); + + configureDefault(con); + Assert.assertFalse(con.shouldForceReconnect(DEFAULT_USER, DEFAULT_PASSWD)); + + configureDefault(con); + Assert.assertFalse(con.shouldForceReconnect(null,DEFAULT_PASSWD)); + + configureDefault(con); + Assert.assertFalse(con.shouldForceReconnect(DEFAULT_USER, null)); + + configureDefault(con); + Assert.assertTrue(con.shouldForceReconnect(ALT_USER,ALT_PASSWD)); + + configureDefault(con); + Assert.assertTrue(con.shouldForceReconnect(null,ALT_PASSWD)); + + configureDefault(con); + Assert.assertTrue(con.shouldForceReconnect(ALT_USER,null)); + + //connection previously connect with alternate + configureAlt(con); + Assert.assertFalse(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + + configureAlt(con); + Assert.assertTrue(con.shouldForceReconnect(null, null)); + + configureAlt(con); + Assert.assertTrue(con.shouldForceReconnect(DEFAULT_USER, DEFAULT_PASSWD)); + + configureAlt(con); + Assert.assertTrue(con.shouldForceReconnect(null, DEFAULT_PASSWD)); + + configureAlt(con); + Assert.assertTrue(con.shouldForceReconnect(DEFAULT_USER, null)); + + configureAlt(con); + Assert.assertTrue(con.shouldForceReconnect(null,ALT_PASSWD)); + + configureAlt(con); + Assert.assertTrue(con.shouldForceReconnect(ALT_USER,null)); + + //test changes in username password + configureDefault(con); + Assert.assertFalse(con.shouldForceReconnect(null, null)); + Assert.assertTrue(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + Assert.assertFalse(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + Assert.assertTrue(con.shouldForceReconnect(null, null)); + + configureDefault(con); + Assert.assertFalse(con.shouldForceReconnect(DEFAULT_USER, DEFAULT_PASSWD)); + Assert.assertTrue(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + Assert.assertFalse(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + Assert.assertTrue(con.shouldForceReconnect(DEFAULT_USER, DEFAULT_PASSWD)); + + configureAlt(con); + Assert.assertFalse(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + Assert.assertFalse(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + Assert.assertTrue(con.shouldForceReconnect(DEFAULT_USER, DEFAULT_PASSWD)); + Assert.assertFalse(con.shouldForceReconnect(null, null)); + Assert.assertTrue(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + + + configureAlt(con); + Assert.assertTrue(con.shouldForceReconnect(DEFAULT_USER, DEFAULT_PASSWD)); + Assert.assertTrue(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + Assert.assertFalse(con.shouldForceReconnect(ALT_USER, ALT_PASSWD)); + Assert.assertTrue(con.shouldForceReconnect(DEFAULT_USER, DEFAULT_PASSWD)); + + } + + private void configureAlt(PooledConnection con) { + con.getAttributes().put(PooledConnection.PROP_USER, ALT_USER); + con.getAttributes().put(PooledConnection.PROP_PASSWORD, ALT_PASSWD); + } + + private void configureDefault(PooledConnection con) { + con.getAttributes().put(PooledConnection.PROP_USER, DEFAULT_USER); + con.getAttributes().put(PooledConnection.PROP_PASSWORD, DEFAULT_PASSWD); + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/InduceSlowQuery.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/InduceSlowQuery.java new file mode 100644 index 0000000..9ee8ad6 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/InduceSlowQuery.java @@ -0,0 +1,71 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.lang.reflect.Method; +import java.security.SecureRandom; + +public class InduceSlowQuery extends AbstractQueryReport { + public static final SecureRandom random = new SecureRandom(); + + public InduceSlowQuery() { + // TODO Auto-generated constructor stub + } + + public void doWait() { + try { + int b = random.nextInt(10); + if (b == 0) { + Thread.sleep(random.nextInt(2000)); + } + } catch (InterruptedException x) { + + } + + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // TODO Auto-generated method stub + Object result = super.invoke(proxy, method, args); + return result; + } + + @Override + protected void prepareCall(String query, long time) { + } + + @Override + protected void prepareStatement(String sql, long time) { + } + + @Override + public void closeInvoked() { + } + + @Override + protected String reportQuery(String query, Object[] args, String name, long start, long delta) { + doWait(); + return super.reportQuery(query, args, name, start, delta); + } + + @Override + protected String reportSlowQuery(String query, Object[] args, String name, long start, long delta) { + doWait(); + return super.reportSlowQuery(query, args, name, start, delta); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCounterInterceptor.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCounterInterceptor.java new file mode 100644 index 0000000..c0d39a8 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCounterInterceptor.java @@ -0,0 +1,65 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Statement; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Interceptor that counts opened Statements. Is used by tests. + */ +public class StatementCounterInterceptor extends StatementDecoratorInterceptor { + + private final AtomicInteger countOpen = new AtomicInteger(); + private final AtomicInteger countClosed = new AtomicInteger(); + + public int getActiveCount() { + return countOpen.get() - countClosed.get(); + } + + @Override + protected Object createDecorator(Object proxy, Method method, + Object[] args, Object statement, Constructor constructor, + String sql) throws InstantiationException, IllegalAccessException, + InvocationTargetException { + Object result; + StatementProxy statementProxy = new StatementProxy( + (Statement) statement, sql); + result = constructor.newInstance(new Object[] { statementProxy }); + statementProxy.setActualProxy(result); + statementProxy.setConnection(proxy); + statementProxy.setConstructor(constructor); + countOpen.incrementAndGet(); + return result; + } + + private class StatementProxy extends + StatementDecoratorInterceptor.StatementProxy { + StatementProxy(Statement delegate, String sql) { + super(delegate, sql); + } + + @Override + public void closeInvoked() { + countClosed.incrementAndGet(); + super.closeInvoked(); + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestInterceptor.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestInterceptor.java new file mode 100644 index 0000000..16fb3c7 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestInterceptor.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.jdbc.pool.interceptor; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.JdbcInterceptor; +import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorProperty; +import org.apache.tomcat.jdbc.pool.PooledConnection; + +public class TestInterceptor extends JdbcInterceptor { + public static boolean poolstarted = false; + public static boolean poolclosed = false; + public static final AtomicInteger instancecount = new AtomicInteger(0); + + @Override + public void poolClosed(ConnectionPool pool) { + super.poolClosed(pool); + poolclosed = true; + } + + @Override + public void poolStarted(ConnectionPool pool) { + super.poolStarted(pool); + poolstarted = true; + } + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + // NO-OP + } + + @Override + public void setProperties(Map properties) { + instancecount.incrementAndGet(); + super.setProperties(properties); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/AbandonPercentageTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/AbandonPercentageTest.java new file mode 100644 index 0000000..1e320df --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/AbandonPercentageTest.java @@ -0,0 +1,117 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer; + +public class AbandonPercentageTest extends DefaultTestCase { + + @Test + public void testDefaultAbandon() throws Exception { + this.datasource.setMaxActive(100); + this.datasource.setMaxIdle(100); + this.datasource.setInitialSize(0); + this.datasource.getPoolProperties().setAbandonWhenPercentageFull(0); + this.datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(100); + this.datasource.getPoolProperties().setRemoveAbandoned(true); + this.datasource.getPoolProperties().setRemoveAbandonedTimeout(1); + try (Connection con = datasource.getConnection()) { + Assert.assertEquals("Number of connections active/busy should be 1",1,datasource.getPool().getActive()); + Thread.sleep(2000); + Assert.assertEquals("Number of connections active/busy should be 0",0,datasource.getPool().getActive()); + } + } + + @Test + public void testMaxedOutAbandon() throws Exception { + int size = 100; + this.datasource.setMaxActive(size); + this.datasource.setMaxIdle(size); + this.datasource.setInitialSize(0); + this.datasource.getPoolProperties().setAbandonWhenPercentageFull(100); + this.datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(100); + this.datasource.getPoolProperties().setRemoveAbandoned(true); + this.datasource.getPoolProperties().setRemoveAbandonedTimeout(1); + try (Connection con = datasource.getConnection()) { + Assert.assertEquals("Number of connections active/busy should be 1",1,datasource.getPool().getActive()); + Thread.sleep(2000); + Assert.assertEquals("Number of connections active/busy should be 1",1,datasource.getPool().getActive()); + } + } + + @Test + public void testResetConnection() throws Exception { + int size = 1; + this.datasource.setMaxActive(size); + this.datasource.setMaxIdle(size); + this.datasource.setInitialSize(0); + this.datasource.getPoolProperties().setAbandonWhenPercentageFull(100); + this.datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(100); + this.datasource.getPoolProperties().setRemoveAbandoned(true); + this.datasource.getPoolProperties().setRemoveAbandonedTimeout(1); + this.datasource.getPoolProperties().setJdbcInterceptors(ResetAbandonedTimer.class.getName()); + try (Connection con = datasource.getConnection()) { + Assert.assertEquals("Number of connections active/busy should be 1",1,datasource.getPool().getActive()); + for (int i=0; i<20; i++) { + Thread.sleep(200); + // This call is here to ensure the pool thinks the connection + // is being used. + con.isClosed(); + } + Assert.assertEquals("Number of connections active/busy should be 1",1,datasource.getPool().getActive()); + } + } + + @Test + public void testHalfway() throws Exception { + int size = 100; + this.datasource.setMaxActive(size); + this.datasource.setMaxIdle(size); + this.datasource.setInitialSize(0); + this.datasource.getPoolProperties().setAbandonWhenPercentageFull(50); + this.datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(500); + this.datasource.getPoolProperties().setRemoveAbandoned(true); + this.datasource.getPoolProperties().setRemoveAbandonedTimeout(1); + Connection[] con = new Connection[size]; + con[0] = datasource.getConnection(); + Assert.assertEquals("Number of connections active/busy should be 1",1,datasource.getPool().getActive()); + for (int i=1; i<25; i++) { + con[i] = datasource.getConnection(); + } + Assert.assertEquals("Number of connections active/busy should be 25",25,datasource.getPool().getActive()); + Thread.sleep(2500); + Assert.assertEquals("Number of connections active/busy should be 25",25,datasource.getPool().getActive()); + this.datasource.getPoolProperties().setRemoveAbandonedTimeout(100); + for (int i=25; i> results = svc.invokeAll(Arrays.asList(runners)); + int failures = 0; + int total = 0; + for (int i=0; i { + String username; + String password; + volatile boolean done = false; + TestResult result = null; + boolean useuser = true; + + public TestRunner(String user, String pass, String guser, String gpass) { + username = user==null?guser : user; + password = pass==null?gpass : pass; + useuser = user!=null; + } + + @Override + public TestResult call() { + TestResult test = new TestResult(); + PooledConnection pcon = null; + for (int i=0; (!done) && (i cf = ((DataSourceProxy)datasource).getConnectionAsync(); + cf.get(5, TimeUnit.SECONDS); + }finally { + tearDown(); + } + } +} + diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/BorrowWaitTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/BorrowWaitTest.java new file mode 100644 index 0000000..98851ee --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/BorrowWaitTest.java @@ -0,0 +1,71 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.junit.Assert; +import org.junit.Test; + +public class BorrowWaitTest extends DefaultTestCase { + + @Test + public void testWaitTime() throws Exception { + + int wait = 10000; + this.datasource.setMaxActive(1); + this.datasource.setMaxWait(wait); + Connection con = datasource.getConnection(); + long start = System.currentTimeMillis(); + try { + Connection con2 = datasource.getConnection(); + Assert.assertFalse("This should not happen, connection should be unavailable.",true); + con2.close(); + }catch (SQLException x) { + long delta = System.currentTimeMillis() - start; + boolean inrange = Math.abs(wait-delta) <= 1000; + Assert.assertTrue("Connection should have been acquired within +/- 1 second, but was "+(wait-delta)+" ms.",inrange); + } + con.close(); + } + + public void testWaitTimeInfinite() throws Exception { + if(true){ + System.err.println("testWaitTimeInfinite() test is disabled."); + return;//this would lock up the test suite + } + /* + int wait = -1; + this.datasource.setMaxActive(1); + this.datasource.setMaxWait(wait); + Connection con = datasource.getConnection(); + long start = System.currentTimeMillis(); + try { + Connection con2 = datasource.getConnection(); + Assert.assertFalse("This should not happen, connection should be unavailable.",true); + }catch (SQLException x) { + long delta = System.currentTimeMillis(); + boolean inrange = Math.abs(wait-delta) < 1000; + Assert.assertTrue("Connection should have been acquired within +/- 1 second.",true); + } + con.close(); + */ + } + + +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Bug50571.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Bug50571.java new file mode 100644 index 0000000..7f8b173 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Bug50571.java @@ -0,0 +1,38 @@ +/* + * 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.tomcat.jdbc.test; + +import org.junit.Before; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.interceptor.ConnectionState; + +public class Bug50571 extends DefaultTestCase{ + + @Before + public void setUp() throws Exception { + this.datasource.setUrl("jdbc:h2:~/.h2/test;QUERY_TIMEOUT=0;DB_CLOSE_ON_EXIT=FALSE"); + this.datasource.setJdbcInterceptors(ConnectionState.class.getName()); + this.datasource.setDefaultTransactionIsolation(-55); + this.datasource.setInitialSize(1); + } + + @Test + public void testBug50571() throws Exception { + this.datasource.getConnection().close(); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Bug50805.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Bug50805.java new file mode 100644 index 0000000..d972e73 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/Bug50805.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.util.concurrent.Future; + +import org.junit.Assert; +import org.junit.Test; + +public class Bug50805 extends DefaultTestCase { + + @Test + public void test50805() throws Exception { + this.datasource.setInitialSize(0); + this.datasource.setMaxActive(10); + this.datasource.setMinIdle(1); + + Assert.assertEquals("Current size should be 0.", 0, this.datasource.getSize()); + + this.datasource.getConnection().close(); + + Assert.assertEquals("Current size should be 1.", 1, this.datasource.getSize()); + Assert.assertEquals("Idle size should be 1.", 1, this.datasource.getIdle()); + Assert.assertEquals("Busy size should be 0.", 0, this.datasource.getActive()); + + Future fc = this.datasource.getConnectionAsync(); + + Connection con = fc.get(); + + Assert.assertEquals("Current size should be 1.", 1, this.datasource.getSize()); + Assert.assertEquals("Idle size should be 0.", 0, this.datasource.getIdle()); + Assert.assertEquals("Busy size should be 1.", 1, this.datasource.getActive()); + + con.close(); + Assert.assertEquals("Current size should be 1.", 1, this.datasource.getSize()); + Assert.assertEquals("Idle size should be 1.", 1, this.datasource.getIdle()); + Assert.assertEquals("Busy size should be 0.", 0, this.datasource.getActive()); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/CheckOutThreadTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/CheckOutThreadTest.java new file mode 100644 index 0000000..0350a5e --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/CheckOutThreadTest.java @@ -0,0 +1,411 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.util.concurrent.CountDownLatch; + +import javax.sql.DataSource; + +import org.junit.Test; + +public class CheckOutThreadTest extends DefaultTestCase { + + CountDownLatch latch = null; + + @Test + public void testDBCPThreads10Connections10() throws Exception { + this.datasource.getPoolProperties().setMaxActive(10); + this.threadcount = 10; + this.transferProperties(); + this.tDatasource.getConnection().close(); + latch = new CountDownLatch(threadcount); + long start = System.currentTimeMillis(); + for (int i=0; i=ConnectCountTest.this.complete) { + break; + } + long start = System.nanoTime(); + Connection con = null; + try { + if (async) { + Future cf = ((DataSourceProxy)d).getConnectionAsync(); + con = cf.get(); + } else { + con = d.getConnection(); + } + long delta = System.nanoTime() - start; + totalwait += delta; + maxwait = Math.max(delta, maxwait); + minwait = Math.min(delta, minwait); + nroffetch++; + try { + if (ConnectCountTest.this.sleep>0) { + sleep(ConnectCountTest.this.sleep); + } + } catch (InterruptedException x) { + interrupted(); + } + } finally { + long cstart = System.nanoTime(); + if (con!=null) { + try {con.close();}catch(Exception x) {x.printStackTrace();} + } + long cdelta = System.nanoTime() - cstart; + totalcmax += cdelta; + cmax = Math.max(cdelta, cmax); + } + totalruntime+=(System.nanoTime()-start); + } + + } catch (RuntimeException | SQLException | ExecutionException | InterruptedException x) { + x.printStackTrace(); + } finally { + ConnectCountTest.this.latch.countDown(); + } + if (System.getProperty("print-thread-stats")!=null) { + System.out.println("["+getName()+"] "+ + "\n\tMax time to retrieve connection:"+maxwait/1000000f+" ms."+ + "\n\tTotal time to retrieve connection:"+totalwait/1000000f+" ms."+ + "\n\tAverage time to retrieve connection:"+totalwait/1000000f/nroffetch+" ms."+ + "\n\tMax time to close connection:"+cmax/1000000f+" ms."+ + "\n\tTotal time to close connection:"+totalcmax/1000000f+" ms."+ + "\n\tAverage time to close connection:"+totalcmax/1000000f/nroffetch+" ms."+ + "\n\tRun time:"+totalruntime/1000000f+" ms."+ + "\n\tNr of fetch:"+nroffetch); + } + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/CreateTestTable.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/CreateTestTable.java new file mode 100644 index 0000000..48a4bed --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/CreateTestTable.java @@ -0,0 +1,138 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Random; + +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer; + +public class CreateTestTable extends DefaultTestCase { + + public static volatile boolean recreate = Boolean.getBoolean("recreate"); + + @Test + public void testCreateTestTable() throws Exception { + Connection con = datasource.getConnection(); + Statement st = con.createStatement(); + try { + st.execute("create table test(id int not null, val1 varchar(255), val2 varchar(255), val3 varchar(255), val4 varchar(255))"); + } catch (Exception ignore) { + // Ignore + } + st.close(); + con.close(); + } + + public int testCheckData() throws Exception { + int count = 0; + String check = "select count (*) from test"; + Connection con = datasource.getConnection(); + Statement st = con.createStatement(); + try { + ResultSet rs = st.executeQuery(check); + + if (rs.next()) { + count = rs.getInt(1); + } + rs.close(); + st.close(); + System.out.println("Count:"+count); + }catch (Exception ignore) {} + con.close(); + return count; + } + + @Test + public void testPopulateData() throws Exception { + int count = 100000; + int actual = testCheckData(); + if (actual>=count) { + System.out.println("Test tables has "+actual+" rows of data. No need to populate."); + return; + } + + datasource.setJdbcInterceptors(ResetAbandonedTimer.class.getName()); + String insert = "insert into test values (?,?,?,?,?)"; + this.datasource.setRemoveAbandoned(false); + Connection con = datasource.getConnection(); + + boolean commit = con.getAutoCommit(); + con.setAutoCommit(false); + if (recreate) { + Statement st = con.createStatement(); + try { + st.execute("drop table test"); + } catch (Exception ignore) { + // Ignore + } + st.execute("create table test(id int not null, val1 varchar(255), val2 varchar(255), val3 varchar(255), val4 varchar(255))"); + st.close(); + } + + + PreparedStatement ps = con.prepareStatement(insert); + ps.setQueryTimeout(0); + for (int i=actual; i=0) { + //noop - ignore known missing properties + } else if (msg.indexOf("isMaxOpenPreparedStatements")>=0) { + //noop - ignore known missing properties + } else { + System.err.println("Missing property:"+x2.getMessage()); + } + } + } + if (get!=null) { + Object value = get.invoke(datasource.getPoolProperties(), new Object[0]); + if (value!=null) { + p.setProperty(dbcpProperty, value.toString()); + } + } + } + tDatasource = BasicDataSourceFactory.createDataSource(p); + }catch (Exception x) { + x.printStackTrace(); + } + } + + private String handleRenames(String oldName) { + if (RENAMED.containsKey(oldName)) { + return RENAMED.get(oldName); + } + return oldName; + } + + protected void transferPropertiesToC3P0() throws Exception { +// System.setProperty("com.mchange.v2.log.FallbackMLog.DEFAULT_CUTOFF_LEVEL", "WARNING"); +// MLog.getLogger().setLevel(MLevel.WARNING); +// MLog.getLogger("com").setLevel(MLevel.WARNING); +// //http://www.mchange.com/projects/c3p0/index.html#automaticTestTable +// ComboPooledDataSource c3p0 = new ComboPooledDataSource(); +// c3p0.setAcquireIncrement(1); +// c3p0.setAcquireRetryAttempts(2); +// c3p0.setAcquireRetryDelay(datasource.getPoolProperties().getMaxWait()); +// c3p0.setCheckoutTimeout(datasource.getPoolProperties().getMaxWait()); +// c3p0.setDebugUnreturnedConnectionStackTraces(datasource.getPoolProperties().isLogAbandoned()); +// c3p0.setIdleConnectionTestPeriod(datasource.getPoolProperties().getTimeBetweenEvictionRunsMillis()/1000); +// c3p0.setInitialPoolSize(datasource.getPoolProperties().getInitialSize()); +// c3p0.setMaxIdleTime(datasource.getPoolProperties().getMinEvictableIdleTimeMillis()/1000); +// c3p0.setMaxIdleTimeExcessConnections(datasource.getPoolProperties().getMaxIdle()); +// c3p0.setMaxPoolSize(datasource.getPoolProperties().getMaxActive()); +// c3p0.setMinPoolSize(datasource.getPoolProperties().getMinIdle()); +// c3p0.setPassword(datasource.getPoolProperties().getPassword()); +// c3p0.setPreferredTestQuery(datasource.getPoolProperties().getValidationQuery()); +// c3p0.setTestConnectionOnCheckin(datasource.getPoolProperties().isTestOnReturn()); +// c3p0.setTestConnectionOnCheckout(datasource.getPoolProperties().isTestOnBorrow()); +// c3p0.setUnreturnedConnectionTimeout(datasource.getPoolProperties().getRemoveAbandonedTimeout()); +// c3p0.setUser(datasource.getPoolProperties().getUsername()); +// c3p0.setUsesTraditionalReflectiveProxies(true); +// c3p0.setJdbcUrl(datasource.getPoolProperties().getUrl()); +// c3p0.setDriverClass(datasource.getPoolProperties().getDriverClassName()); +// this.c3p0Datasource = c3p0; + + /* + acquireIncrement + acquireRetryAttempts + acquireRetryDelay + autoCommitOnClose + automaticTestTable + breakAfterAcquireFailure + checkoutTimeout + connectionCustomizerClassName + connectionTesterClassName + debugUnreturnedConnectionStackTraces + factoryClassLocation + forceIgnoreUnresolvedTransactions + idleConnectionTestPeriod + initialPoolSize + maxAdministrativeTaskTime + maxConnectionAge + maxIdleTime + maxIdleTimeExcessConnections + maxPoolSize + maxStatements + maxStatementsPerConnection + minPoolSize + numHelperThreads + overrideDefaultUser + overrideDefaultPassword + password + preferredTestQuery + propertyCycle + testConnectionOnCheckin + testConnectionOnCheckout + unreturnedConnectionTimeout + user + usesTraditionalReflectiveProxies + */ + } + + + @After + public void tearDown() throws Exception { + try { + datasource.close(); + } catch (Exception ignore){ + // Ignore + } + try { + tDatasource.close(); + } catch (Exception ignore){ + // Ignore + } + //try {((ComboPooledDataSource)c3p0Datasource).close(true);}catch(Exception ignore){} + datasource = null; + tDatasource = null; + //c3p0Datasource = null; + System.gc(); + org.apache.tomcat.jdbc.test.driver.Driver.reset(); + } + + private static final String PROP_DEFAULTAUTOCOMMIT = "defaultAutoCommit"; + private static final String PROP_DEFAULTREADONLY = "defaultReadOnly"; + private static final String PROP_DEFAULTTRANSACTIONISOLATION = "defaultTransactionIsolation"; + private static final String PROP_DEFAULTCATALOG = "defaultCatalog"; + private static final String PROP_DRIVERCLASSNAME = "driverClassName"; + private static final String PROP_MAXACTIVE = "maxActive"; + private static final String PROP_MAXIDLE = "maxIdle"; + private static final String PROP_MINIDLE = "minIdle"; + private static final String PROP_INITIALSIZE = "initialSize"; + private static final String PROP_MAXWAIT = "maxWait"; + private static final String PROP_TESTONBORROW = "testOnBorrow"; + private static final String PROP_TESTONRETURN = "testOnReturn"; + private static final String PROP_TIMEBETWEENEVICTIONRUNSMILLIS = "timeBetweenEvictionRunsMillis"; + private static final String PROP_NUMTESTSPEREVICTIONRUN = "numTestsPerEvictionRun"; + private static final String PROP_MINEVICTABLEIDLETIMEMILLIS = "minEvictableIdleTimeMillis"; + private static final String PROP_TESTWHILEIDLE = "testWhileIdle"; + private static final String PROP_PASSWORD = "password"; + private static final String PROP_URL = "url"; + private static final String PROP_USERNAME = "username"; + private static final String PROP_VALIDATIONQUERY = "validationQuery"; + private static final String PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED = "accessToUnderlyingConnectionAllowed"; + private static final String PROP_REMOVEABANDONED = "removeAbandoned"; + private static final String PROP_REMOVEABANDONEDTIMEOUT = "removeAbandonedTimeout"; + private static final String PROP_LOGABANDONED = "logAbandoned"; + private static final String PROP_POOLPREPAREDSTATEMENTS = "poolPreparedStatements"; + private static final String PROP_MAXOPENPREPAREDSTATEMENTS = "maxOpenPreparedStatements"; + private static final String PROP_CONNECTIONPROPERTIES = "connectionProperties"; + + private static final String[] ALL_PROPERTIES = { + PROP_DEFAULTAUTOCOMMIT, + PROP_DEFAULTREADONLY, + PROP_DEFAULTTRANSACTIONISOLATION, + PROP_DEFAULTCATALOG, + PROP_DRIVERCLASSNAME, + PROP_MAXACTIVE, + PROP_MAXIDLE, + PROP_MINIDLE, + PROP_INITIALSIZE, + PROP_MAXWAIT, + PROP_TESTONBORROW, + PROP_TESTONRETURN, + PROP_TIMEBETWEENEVICTIONRUNSMILLIS, + PROP_NUMTESTSPEREVICTIONRUN, + PROP_MINEVICTABLEIDLETIMEMILLIS, + PROP_TESTWHILEIDLE, + PROP_PASSWORD, + PROP_URL, + PROP_USERNAME, + PROP_VALIDATIONQUERY, + PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED, + PROP_REMOVEABANDONED, + PROP_REMOVEABANDONEDTIMEOUT, + PROP_LOGABANDONED, + PROP_POOLPREPAREDSTATEMENTS, + PROP_MAXOPENPREPAREDSTATEMENTS, + PROP_CONNECTIONPROPERTIES + }; + + private static final Map RENAMED = new HashMap<>(); + + static { + RENAMED.put(PROP_MAXACTIVE, "maxTotal"); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/EqualsHashCodeTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/EqualsHashCodeTest.java new file mode 100644 index 0000000..2c97a00 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/EqualsHashCodeTest.java @@ -0,0 +1,74 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; + +import javax.sql.PooledConnection; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.tomcat.jdbc.test.driver.Driver; + +public class EqualsHashCodeTest extends DefaultTestCase{ + public static final String password = "password"; + public static final String username = "username"; + + @Before + public void setUp() throws Exception { + this.datasource.setDriverClassName(Driver.class.getName()); + this.datasource.setUrl("jdbc:tomcat:test"); + this.datasource.setPassword(password); + this.datasource.setMaxActive(1); + this.datasource.setMinIdle(datasource.getMaxActive()); + this.datasource.setMaxIdle(datasource.getMaxActive()); + this.datasource.setUsername(username); + this.datasource.getConnection().close(); + datasource.createPool(); + } + + @Test + public void testEquals() throws Exception { + Connection con1 = datasource.getConnection(); + Connection real1 = ((PooledConnection)con1).getConnection(); + Assert.assertEquals(con1, con1); + con1.close(); + Assert.assertEquals(con1, con1); + Connection con2 = datasource.getConnection(); + Connection real2 = ((PooledConnection)con2).getConnection(); + Assert.assertEquals(real1,real2); + Assert.assertEquals(con2, con2); + Assert.assertNotSame(con1, con2); + con2.close(); + Assert.assertEquals(con2, con2); + } + + @Test + public void testHashCode() throws Exception { + Connection con1 = datasource.getConnection(); + Assert.assertEquals(con1.hashCode(), con1.hashCode()); + con1.close(); + Assert.assertEquals(con1.hashCode(), con1.hashCode()); + Connection con2 = datasource.getConnection(); + Assert.assertEquals(con2.hashCode(), con2.hashCode()); + Assert.assertTrue(con1.hashCode() != con2.hashCode()); + con2.close(); + Assert.assertEquals(con2.hashCode(), con2.hashCode()); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/FairnessTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/FairnessTest.java new file mode 100644 index 0000000..e333104 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/FairnessTest.java @@ -0,0 +1,271 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.sql.DataSource; + +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.DataSourceProxy; + +public class FairnessTest extends DefaultTestCase { + + protected boolean run = true; + protected long sleep = Long.getLong("sleep", 10).longValue(); + protected long complete = Long.getLong("complete",20000).longValue(); + protected boolean printthread = Boolean.getBoolean("printthread"); + CountDownLatch latch = null; + protected void printThreadResults(TestThread[] threads, String name, int active, int expected) { + long minfetch = Long.MAX_VALUE, maxfetch = Long.MIN_VALUE, totalfetch = 0; + long maxwait = 0, minwait = Long.MAX_VALUE, totalwait = 0; + for (int i=0; i=FairnessTest.this.complete) { + break; + } + long start = System.nanoTime(); + Connection con = null; + try { + if (async) { + Future cf = ((DataSourceProxy)d).getConnectionAsync(); + con = cf.get(); + } else { + con = d.getConnection(); + } + long delta = System.nanoTime() - start; + totalwait += delta; + maxwait = Math.max(delta, maxwait); + minwait = Math.min(delta, minwait); + nroffetch++; + try { + if (FairnessTest.this.sleep>0) { + sleep(FairnessTest.this.sleep); + } + } catch (InterruptedException x) { + interrupted(); + } + } finally { + long cstart = System.nanoTime(); + if (con!=null) { + try {con.close();}catch(Exception x) {x.printStackTrace();} + } + long cdelta = System.nanoTime() - cstart; + totalcmax += cdelta; + cmax = Math.max(cdelta, cmax); + } + totalruntime+=(System.nanoTime()-start); + } + + } catch (RuntimeException | SQLException | ExecutionException | InterruptedException x) { + x.printStackTrace(); + } finally { + FairnessTest.this.latch.countDown(); + } + if (System.getProperty("print-thread-stats")!=null) { + System.out.println("["+getName()+"] "+ + "\n\tMax time to retrieve connection:"+maxwait/1000000f+" ms."+ + "\n\tTotal time to retrieve connection:"+totalwait/1000000f+" ms."+ + "\n\tAverage time to retrieve connection:"+totalwait/1000000f/nroffetch+" ms."+ + "\n\tMax time to close connection:"+cmax/1000000f+" ms."+ + "\n\tTotal time to close connection:"+totalcmax/1000000f+" ms."+ + "\n\tAverage time to close connection:"+totalcmax/1000000f/nroffetch+" ms."+ + "\n\tRun time:"+totalruntime/1000000f+" ms."+ + "\n\tNr of fetch:"+nroffetch); + } + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/JmxPasswordTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/JmxPasswordTest.java new file mode 100644 index 0000000..6c05c00 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/JmxPasswordTest.java @@ -0,0 +1,70 @@ +/* + * 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.tomcat.jdbc.test; + +import java.lang.management.ManagementFactory; +import java.util.Hashtable; +import java.util.Properties; + +import javax.management.JMX; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.PoolUtilities; +import org.apache.tomcat.jdbc.pool.jmx.ConnectionPoolMBean; +import org.apache.tomcat.jdbc.test.driver.Driver; + +public class JmxPasswordTest extends DefaultTestCase{ + public static final String password = "password"; + public static final String username = "username"; + public ObjectName oname = null; + + @Before + public void setUp() throws Exception { + this.datasource.setDriverClassName(Driver.class.getName()); + this.datasource.setUrl("jdbc:tomcat:test"); + this.datasource.setPassword(password); + this.datasource.setUsername(username); + this.datasource.getConnection().close(); + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + String domain = "tomcat.jdbc"; + Hashtable properties = new Hashtable<>(); + properties.put("type", "ConnectionPool"); + properties.put("class", this.getClass().getName()); + oname = new ObjectName(domain,properties); + ConnectionPool pool = datasource.createPool(); + org.apache.tomcat.jdbc.pool.jmx.ConnectionPool jmxPool = new org.apache.tomcat.jdbc.pool.jmx.ConnectionPool(pool); + mbs.registerMBean(jmxPool, oname); + + } + + @Test + public void testPassword() throws Exception { + Assert.assertEquals("Passwords should match when not using JMX.",password,datasource.getPoolProperties().getPassword()); + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + ConnectionPoolMBean mbean = JMX.newMBeanProxy(mbs, oname, ConnectionPoolMBean.class); + String jmxPassword = mbean.getPassword(); + Properties jmxProperties = mbean.getDbProperties(); + Assert.assertFalse("Passwords should not match.", password.equals(jmxPassword)); + Assert.assertFalse("Password property should be missing", jmxProperties.containsKey(PoolUtilities.PROP_PASSWORD)); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/MultipleCloseTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/MultipleCloseTest.java new file mode 100644 index 0000000..4180c51 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/MultipleCloseTest.java @@ -0,0 +1,71 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.test.driver.Driver; + +public class MultipleCloseTest extends DefaultTestCase { + + @Override + public org.apache.tomcat.jdbc.pool.DataSource createDefaultDataSource() { + org.apache.tomcat.jdbc.pool.DataSource ds = super.createDefaultDataSource(); + ds.getPoolProperties().setDriverClassName(Driver.class.getName()); + ds.getPoolProperties().setUrl(Driver.url); + ds.getPoolProperties().setInitialSize(0); + ds.getPoolProperties().setMaxIdle(1); + ds.getPoolProperties().setMinIdle(1); + ds.getPoolProperties().setMaxActive(1); + ds.getPoolProperties().setUseDisposableConnectionFacade(true); + return ds; + } + + @After + @Override + public void tearDown() throws Exception { + Driver.reset(); + super.tearDown(); + } + + @Test + public void testClosedConnectionsNotReused() throws Exception { + this.init(); + + Connection con1 = datasource.getConnection(); + + // A new connection is open + Assert.assertFalse(con1.isClosed()); + + con1.close(); + + // Confirm that a closed connection is closed + Assert.assertTrue(con1.isClosed()); + + // Open a new connection (This will re-use the previous pooled connection) + Connection con2 = datasource.getConnection(); + + // A connection, once closed, should stay closed + Assert.assertTrue(con1.isClosed()); + + con2.close(); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/PoolCleanerTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/PoolCleanerTest.java new file mode 100644 index 0000000..3ece41d --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/PoolCleanerTest.java @@ -0,0 +1,140 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.DataSource; + +public class PoolCleanerTest extends DefaultTestCase { + + private int countPoolCleanerThreads() { + Map threadmap = Thread.getAllStackTraces(); + int result = 0; + for (Thread t : threadmap.keySet()) { + if (t.getName().startsWith("Tomcat JDBC Pool Cleaner[")) { + result++; + } + } + return result; + } + + @Test + public void testPoolCleaner() throws Exception { + datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(2000); + datasource.getPoolProperties().setTestWhileIdle(true); + Assert.assertEquals("Pool cleaner should not be started yet.",0,ConnectionPool.getPoolCleaners().size() ); + Assert.assertNull("Pool timer should be null", ConnectionPool.getPoolTimer()); + Assert.assertEquals("Pool cleaner threads should not be present.",0, countPoolCleanerThreads()); + + datasource.getConnection().close(); + Assert.assertEquals("Pool cleaner should have 1 cleaner.",1,ConnectionPool.getPoolCleaners().size() ); + Assert.assertNotNull("Pool timer should not be null", ConnectionPool.getPoolTimer()); + Assert.assertEquals("Pool cleaner threads should be 1.",1, countPoolCleanerThreads()); + + datasource.close(); + Assert.assertEquals("Pool shutdown, no cleaners should be present.",0,ConnectionPool.getPoolCleaners().size() ); + Assert.assertNull("Pool timer should be null after shutdown", ConnectionPool.getPoolTimer()); + Assert.assertEquals("Pool cleaner threads should not be present after close.",0, countPoolCleanerThreads()); + + + } + + @Test + public void test2PoolCleaners() throws Exception { + datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(2000); + datasource.getPoolProperties().setTestWhileIdle(true); + + DataSource ds2 = new DataSource(datasource.getPoolProperties()); + + Assert.assertEquals("Pool cleaner should not be started yet.",0,ConnectionPool.getPoolCleaners().size() ); + Assert.assertNull("Pool timer should be null", ConnectionPool.getPoolTimer()); + Assert.assertEquals("Pool cleaner threads should not be present.",0, countPoolCleanerThreads()); + + datasource.getConnection().close(); + ds2.getConnection().close(); + Assert.assertEquals("Pool cleaner should have 2 cleaner.",2,ConnectionPool.getPoolCleaners().size() ); + Assert.assertNotNull("Pool timer should not be null", ConnectionPool.getPoolTimer()); + Assert.assertEquals("Pool cleaner threads should be 1.",1, countPoolCleanerThreads()); + + datasource.close(); + Assert.assertEquals("Pool cleaner should have 1 cleaner.",1,ConnectionPool.getPoolCleaners().size() ); + Assert.assertNotNull("Pool timer should not be null", ConnectionPool.getPoolTimer()); + + ds2.close(); + Assert.assertEquals("Pool shutdown, no cleaners should be present.",0,ConnectionPool.getPoolCleaners().size() ); + Assert.assertNull("Pool timer should be null after shutdown", ConnectionPool.getPoolTimer()); + Assert.assertEquals("Pool cleaner threads should not be present after close.",0, countPoolCleanerThreads()); + } + + @Test + public void testIdleTimeout() throws Exception { + datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(2000); + datasource.getPoolProperties().setTestWhileIdle(true); + datasource.getPoolProperties().setMaxIdle(0); + datasource.getPoolProperties().setInitialSize(0); + datasource.getPoolProperties().setMinIdle(0); + datasource.getPoolProperties().setMinEvictableIdleTimeMillis(1000); + datasource.getConnection().close(); + Assert.assertEquals("Pool should have 1 idle.", 1, datasource.getIdle()); + Thread.sleep(3000); + Assert.assertEquals("Pool should have 0 idle.", 0, datasource.getIdle()); + } + + @Test + public void testIdleReconnect() throws Exception { + // timeBetweenEvictionRunsMillis > maxAge to test that maxAge setting overrides it + datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(20000); + datasource.getPoolProperties().setMaxAge( 1000 ); + datasource.getPoolProperties().setTestWhileIdle(false); + datasource.getPoolProperties().setMaxIdle(1); + datasource.getPoolProperties().setInitialSize(0); + datasource.getPoolProperties().setMinIdle(1); + + Assert.assertEquals(0,datasource.getPool().getReconnectedCount()); + datasource.getConnection().close(); + Assert.assertEquals(0,datasource.getPool().getReconnectedCount()); + Assert.assertEquals("Pool should have 1 idle.", 1, datasource.getIdle()); + Thread.sleep(4000); + Assert.assertEquals("Pool should have 1 idle.", 1, datasource.getIdle()); + Assert.assertTrue(datasource.getPool().getReconnectedCount()>0); + } + + @Test + public void testReconnectOnReturn() throws Exception { + datasource.getPoolProperties().setMaxAge( 1500 ); + datasource.getPoolProperties().setTestWhileIdle(false); + datasource.getPoolProperties().setMaxIdle(1); + datasource.getPoolProperties().setInitialSize(0); + datasource.getPoolProperties().setMinIdle(1); + + Assert.assertEquals(0,datasource.getPool().getReconnectedCount()); + final Connection con = datasource.getConnection(); + Assert.assertEquals(0,datasource.getPool().getReconnectedCount()); + Assert.assertEquals("Pool should have 0 idle.", 0, datasource.getIdle()); + Thread.sleep(4000); + Assert.assertEquals(0,datasource.getPool().getReconnectedCount()); + con.close(); + Assert.assertEquals("Pool should have 1 idle.", 1, datasource.getIdle()); + Assert.assertTrue(datasource.getPool().getReconnectedCount()>0); + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/PoolPurgeTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/PoolPurgeTest.java new file mode 100644 index 0000000..380f7ab --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/PoolPurgeTest.java @@ -0,0 +1,86 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.test.driver.Driver; + +public class PoolPurgeTest extends DefaultTestCase { + + static final int expectedSize = 2; + + + @Override + public org.apache.tomcat.jdbc.pool.DataSource createDefaultDataSource() { + // TODO Auto-generated method stub + org.apache.tomcat.jdbc.pool.DataSource ds = super.createDefaultDataSource(); + ds.getPoolProperties().setDriverClassName(Driver.class.getName()); + ds.getPoolProperties().setUrl(Driver.url); + ds.getPoolProperties().setInitialSize(expectedSize); + ds.getPoolProperties().setMaxIdle(expectedSize); + ds.getPoolProperties().setMinIdle(expectedSize); + ds.getPoolProperties().setMaxActive(expectedSize); + ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(30000); + ds.getPoolProperties().setMaxAge(Long.MAX_VALUE); + return ds; + } + + + @Override + @After + public void tearDown() throws Exception { + Driver.reset(); + super.tearDown(); + } + + + @Test + public void testPoolPurge() throws Exception { + this.datasource.getConnection().close(); + Assert.assertEquals("Nr of connections should be "+expectedSize, expectedSize , datasource.getSize()); + this.datasource.purge(); + Assert.assertEquals("Nr of connections should be 0", 0 , datasource.getSize()); + } + + @Test + public void testPoolPurgeWithActive() throws Exception { + Connection con = datasource.getConnection(); + Assert.assertEquals("Nr of connections should be "+expectedSize, expectedSize , datasource.getSize()); + this.datasource.purge(); + Assert.assertEquals("Nr of connections should be "+(expectedSize-1), (expectedSize-1) , datasource.getSize()); + con.close(); + Assert.assertEquals("Nr of connections should be 0", 0 , datasource.getSize()); + } + + @Test + public void testPoolPurgeOnReturn() throws Exception { + init(); + Connection con = datasource.getConnection(); + Assert.assertEquals("Nr of connections should be "+expectedSize, expectedSize , datasource.getSize()); + this.datasource.purgeOnReturn(); + Assert.assertEquals("Nr of connections should be "+expectedSize, expectedSize , datasource.getSize()); + con.close(); + Assert.assertEquals("Nr of connections should be "+(expectedSize-1), (expectedSize-1) , datasource.getSize()); + tearDown(); + } +} + diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/SimplePOJOAsyncExample.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/SimplePOJOAsyncExample.java new file mode 100644 index 0000000..976a847 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/SimplePOJOAsyncExample.java @@ -0,0 +1,85 @@ +/* + * 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.tomcat.jdbc.test; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.concurrent.Future; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PoolConfiguration; +import org.apache.tomcat.jdbc.pool.PoolProperties; + +public class SimplePOJOAsyncExample { + + public static void main(String[] args) throws Exception { + PoolConfiguration p = new PoolProperties(); + p.setFairQueue(true); + p.setUrl("jdbc:mysql://localhost:3306/mysql?autoReconnect=true"); + p.setDriverClassName("com.mysql.jdbc.Driver"); + p.setUsername("root"); + p.setPassword("password"); + p.setJmxEnabled(true); + p.setTestWhileIdle(false); + p.setTestOnBorrow(true); + p.setValidationQuery("SELECT 1"); + p.setTestOnReturn(false); + p.setValidationInterval(30000); + p.setTimeBetweenEvictionRunsMillis(30000); + p.setMaxActive(100); + p.setInitialSize(10); + p.setMaxWait(10000); + p.setRemoveAbandonedTimeout(60); + p.setMinEvictableIdleTimeMillis(30000); + p.setMinIdle(10); + p.setLogAbandoned(true); + p.setRemoveAbandoned(true); + p.setJdbcInterceptors("org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer"); + DataSource datasource = new DataSource(); + datasource.setPoolProperties(p); + + Connection con = null; + try { + Future future = datasource.getConnectionAsync(); + while (!future.isDone()) { + System.out.println("Connection is not yet available. Do some background work"); + try { + Thread.sleep(100); //simulate work + } catch (InterruptedException x) { + Thread.interrupted(); + } + } + con = future.get(); //should return instantly + Statement st = con.createStatement(); + ResultSet rs = st.executeQuery("select * from user"); + int cnt = 1; + while (rs.next()) { + System.out.println((cnt++)+". Host:" +rs.getString("Host")+" User:"+rs.getString("User")+" Password:"+rs.getString("Password")); + } + rs.close(); + st.close(); + } finally { + if (con!=null) { + try { + con.close(); + } catch (Exception ignore) { + // Ignore + } + } + } + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/SimplePOJOExample.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/SimplePOJOExample.java new file mode 100644 index 0000000..381dacb --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/SimplePOJOExample.java @@ -0,0 +1,75 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PoolConfiguration; +import org.apache.tomcat.jdbc.pool.PoolProperties; + +public class SimplePOJOExample { + + public static void main(String[] args) throws Exception { + PoolConfiguration p = new PoolProperties(); + p.setUrl("jdbc:mysql://localhost:3306/mysql?autoReconnect=true"); + p.setDriverClassName("com.mysql.jdbc.Driver"); + p.setUsername("root"); + p.setPassword("password"); + p.setJmxEnabled(true); + p.setTestWhileIdle(false); + p.setTestOnBorrow(true); + p.setValidationQuery("SELECT 1"); + p.setTestOnReturn(false); + p.setValidationInterval(30000); + p.setTimeBetweenEvictionRunsMillis(30000); + p.setMaxActive(100); + p.setInitialSize(10); + p.setMaxWait(10000); + p.setRemoveAbandonedTimeout(60); + p.setMinEvictableIdleTimeMillis(30000); + p.setMinIdle(10); + p.setJdbcInterceptors("org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer"); + p.setLogAbandoned(true); + p.setRemoveAbandoned(true); + DataSource datasource = new DataSource(); + datasource.setPoolProperties(p); + + Connection con = null; + try { + con = datasource.getConnection(); + Statement st = con.createStatement(); + ResultSet rs = st.executeQuery("select * from user"); + int cnt = 1; + while (rs.next()) { + System.out.println((cnt++)+". Host:" +rs.getString("Host")+" User:"+rs.getString("User")+" Password:"+rs.getString("Password")); + } + rs.close(); + st.close(); + } finally { + if (con!=null) { + try { + con.close(); + } catch (Exception ignore) { + // Ignore + } + } + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/StarvationTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/StarvationTest.java new file mode 100644 index 0000000..3dd074d --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/StarvationTest.java @@ -0,0 +1,114 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.junit.Assert; +import org.junit.Test; + +/** + * If a connection is abandoned and closed, + * then that should free up a spot in the pool, and other threads + * that are waiting should not time out and throw an error but be + * able to acquire a connection, since one was just released. + */ +public class StarvationTest extends DefaultTestCase { + + private void config() { + datasource.getPoolProperties().setMaxActive(1); + datasource.getPoolProperties().setMaxIdle(1); + datasource.getPoolProperties().setInitialSize(1); + datasource.getPoolProperties().setRemoveAbandoned(true); + datasource.getPoolProperties().setRemoveAbandonedTimeout(5); + datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(500); + datasource.getPoolProperties().setMaxWait(10000); + datasource.getPoolProperties().setLogAbandoned(true); + } + +// @Test +// public void testDBCPConnectionStarvation() throws Exception { +// config(); +// this.transferProperties(); +// this.tDatasource.getConnection().close(); +// javax.sql.DataSource datasource = this.tDatasource; +// Connection con1 = datasource.getConnection(); +// Connection con2 = null; +// try { +// con2 = datasource.getConnection(); +// try { +// con2.setCatalog("mysql");//make sure connection is valid +// }catch (SQLException x) { +// Assert.assertFalse("2nd Connection is not valid:"+x.getMessage(),true); +// } +// Assert.assertTrue("Connection 1 should be closed.",con1.isClosed()); //first connection should be closed +// }catch (Exception x) { +// Assert.assertFalse("Connection got starved:"+x.getMessage(),true); +// }finally { +// if (con2!=null) con2.close(); +// } +// } + + @Test + public void testConnectionStarvation() throws Exception { + config(); + Connection con1 = datasource.getConnection(); + Connection con2 = null; + try { + con2 = datasource.getConnection(); + try { + con2.setCatalog("mysql");//make sure connection is valid + }catch (SQLException x) { + Assert.assertFalse("2nd Connection is not valid:"+x.getMessage(),true); + } + Assert.assertTrue("Connection 1 should be closed.",con1.isClosed()); //first connection should be closed + }catch (Exception x) { + Assert.assertFalse("Connection got starved:"+x.getMessage(),true); + }finally { + if (con2!=null) { + con2.close(); + } + } + con1.close(); + } + + @Test + public void testFairConnectionStarvation() throws Exception { + init(); + config(); + datasource.getPoolProperties().setFairQueue(true); + Connection con1 = datasource.getConnection(); + Connection con2 = null; + try { + con2 = datasource.getConnection(); + try { + con2.setCatalog("mysql");//make sure connection is valid + }catch (SQLException x) { + Assert.assertFalse("2nd Connection is not valid:"+x.getMessage(),true); + } + Assert.assertTrue("Connection 1 should be closed.",con1.isClosed()); //first connection should be closed + }catch (Exception x) { + Assert.assertFalse("Connection got starved:"+x.getMessage(),true); + }finally { + if (con2!=null) { + con2.close(); + } + } + con1.close(); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/StatementFinalizerTest.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/StatementFinalizerTest.java new file mode 100644 index 0000000..9ceff5f --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/StatementFinalizerTest.java @@ -0,0 +1,56 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.Statement; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer; + +public class StatementFinalizerTest extends DefaultTestCase { + + @Test + public void testStatementFinalization() throws Exception { + datasource.setJdbcInterceptors(StatementFinalizer.class.getName()); + Connection con = datasource.getConnection(); + Statement st = con.createStatement(); + Assert.assertFalse("Statement should not be closed.",st.isClosed()); + con.close(); + Assert.assertTrue("Statement should be closed.",st.isClosed()); + } + + + @Test + public void testStatementFinalizationForMultiple() throws Exception { + datasource.setJdbcInterceptors(StatementFinalizer.class.getName()); + Connection con = datasource.getConnection(); + Statement[] statements = new Statement[1000]; + for (int i = 0; i < statements.length; i++) { + statements[i] = con.createStatement(); + } + for (Statement st : statements) { + Assert.assertFalse("Statement should not be closed.", st.isClosed()); + } + con.close(); + for (Statement st : statements) { + Assert.assertTrue("Statement should be closed.", st.isClosed()); + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestAsyncQueue.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestAsyncQueue.java new file mode 100644 index 0000000..87687ff --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestAsyncQueue.java @@ -0,0 +1,91 @@ +/* + * 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.tomcat.jdbc.test; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.FairBlockingQueue; + +public class TestAsyncQueue { + + protected FairBlockingQueue queue = null; + + @Before + public void setUp() throws Exception { + this.queue = new FairBlockingQueue<>(); + } + + @After + public void tearDown() throws Exception { + this.queue = null; + } + + + @Test + public void testAsyncPoll1() throws Exception { + Object item = new Object(); + queue.offer(item); + Future future = queue.pollAsync(); + Assert.assertEquals(future.get(),item); + } + + + @Test + public void testAsyncPoll2() throws Exception { + Object item = new Object(); + OfferThread thread = new OfferThread(item,5000); + thread.start(); + Future future = queue.pollAsync(); + try { + future.get(2000, TimeUnit.MILLISECONDS); + Assert.assertFalse("Request should have timed out",true); + }catch (TimeoutException x) { + Assert.assertTrue("Request timed out properly",true); + }catch (Exception x) { + Assert.assertTrue("Request threw an error",false); + x.printStackTrace(); + } + Assert.assertEquals(future.get(),item); + } + + protected class OfferThread extends Thread { + Object item = null; + long delay = 5000; + public OfferThread(Object i, long d) { + this.item = i; + this.delay = d; + this.setDaemon(false); + this.setName(TestAsyncQueue.class.getName()+"-OfferThread"); + } + @Override + public void run() { + try { + sleep(delay); + } catch (Exception ignore){ + // Ignore + } + TestAsyncQueue.this.queue.offer(item); + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestConcurrency.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestConcurrency.java new file mode 100644 index 0000000..db09dc9 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestConcurrency.java @@ -0,0 +1,216 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.test.driver.Driver; + +public class TestConcurrency extends DefaultTestCase { + + public static final boolean debug = Boolean.getBoolean("jdbc.debug"); + + protected volatile DataSource ds = null; + + @Before + public void setUp() throws Exception { + ds = createDefaultDataSource(); + ds.getPoolProperties().setDriverClassName(Driver.class.getName()); + ds.getPoolProperties().setUrl(Driver.url); + ds.getPoolProperties().setInitialSize(0); + ds.getPoolProperties().setMaxIdle(0); + ds.getPoolProperties().setMinIdle(0); + ds.getPoolProperties().setMaxActive(10); + ds.getPoolProperties().setRemoveAbandoned(true); + ds.getPoolProperties().setLogAbandoned(true); + ds.getPoolProperties().setTestWhileIdle(true); + ds.getPoolProperties().setMinEvictableIdleTimeMillis(750); + ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(25); + ds.setFairQueue(true); + } + + @Override + @After + public void tearDown() throws Exception { + ds.close(true); + Driver.reset(); + super.tearDown(); + } + + @Test + public void testSimple() throws Exception { + ds.getConnection().close(); + final int iter = 1000 * 10; + final AtomicInteger loopcount = new AtomicInteger(0); + final Runnable run = new Runnable() { + @Override + public void run() { + try { + while (loopcount.incrementAndGet() < iter) { + Connection con = ds.getConnection(); + Thread.sleep(10); + con.close(); + } + }catch (Exception x) { + loopcount.set(iter); //stops the test + x.printStackTrace(); + } + } + }; + Thread[] threads = new Thread[20]; + for (int i=0; i secondProps = interceptorDefs[2].getProperties(); + Assert.assertNotNull(secondProps); + Assert.assertEquals(secondProps.size(), 2); + Assert.assertNotNull(secondProps.get("parm1")); + Assert.assertEquals(secondProps.get("parm1").getValue(), "value1"); + Assert.assertNotNull(secondProps.get("parm2")); + Assert.assertEquals(secondProps.get("parm2").getValue(), "value2"); + } + + @Test + public void testWhitespace() throws Exception { + String interceptorConfig = "FirstInterceptor ; \n" + + "SecondInterceptor (parm1 = value1 , parm2= value2 ) ; \n\n" + + "\t org.cyb.ThirdInterceptor(parm1=value1); \n" + + "EmptyParmValInterceptor(parm1= )"; + PoolProperties props = new PoolProperties(); + props.setJdbcInterceptors(interceptorConfig); + InterceptorDefinition[] interceptorDefs = props.getJdbcInterceptorsAsArray(); + Assert.assertNotNull(interceptorDefs); + + // 5 items because parser automatically inserts TrapException interceptor to front of list + Assert.assertEquals(interceptorDefs.length, 5); + Assert.assertEquals(interceptorDefs[0].getClassName(), TrapException.class.getName()); + + Assert.assertNotNull(interceptorDefs[1]); + Assert.assertEquals(interceptorDefs[1].getClassName(), "FirstInterceptor"); + Assert.assertNotNull(interceptorDefs[2]); + Assert.assertEquals(interceptorDefs[2].getClassName(), "SecondInterceptor"); + Assert.assertNotNull(interceptorDefs[3]); + Assert.assertEquals(interceptorDefs[3].getClassName(), "org.cyb.ThirdInterceptor"); + + Map secondProps = interceptorDefs[2].getProperties(); + Assert.assertNotNull(secondProps); + Assert.assertEquals(secondProps.size(), 2); + Assert.assertNotNull(secondProps.get("parm1")); + Assert.assertEquals(secondProps.get("parm1").getValue(), "value1"); + Assert.assertNotNull(secondProps.get("parm2")); + Assert.assertEquals(secondProps.get("parm2").getValue(), "value2"); // Bug 54395 + + Map thirdProps = interceptorDefs[3].getProperties(); + Assert.assertNotNull(thirdProps); + Assert.assertEquals(thirdProps.size(), 1); + Assert.assertNotNull(thirdProps.get("parm1")); + Assert.assertEquals(thirdProps.get("parm1").getValue(), "value1"); + + Map emptyParmValProps = interceptorDefs[4].getProperties(); + Assert.assertNotNull(emptyParmValProps); + Assert.assertEquals(emptyParmValProps.size(), 1); + Assert.assertNotNull(emptyParmValProps.get("parm1")); + Assert.assertEquals(emptyParmValProps.get("parm1").getValue(), ""); + } + + /* + * Some of these should probably be handled more cleanly by the parser, but a few known + * exception scenarios are presented here just to document current behavior. In many cases + * failure in parsing will just be propagated to a definition that will fail later + * when instantiated. Should we be failing faster (and with more detail)? + */ + @Test + public void testExceptionOrNot() throws Exception { + PoolProperties props = null; + + String[] exceptionInducingConfigs = { + "EmptyParmsInterceptor()", + "WhitespaceParmsInterceptor( )" + }; + for (String badConfig : exceptionInducingConfigs) { + props = new PoolProperties(); + props.setJdbcInterceptors(badConfig); + try { + props.getJdbcInterceptorsAsArray(); + Assert.fail("Expected exception."); + } catch (Exception e) { + // Expected + } + } + + String[] noExceptionButInvalidConfigs = { + "MalformedParmsInterceptor(= )", + "MalformedParmsInterceptor( =)", + "MalformedParmsInterceptor(", + "MalformedParmsInterceptor( ", + "MalformedParmsInterceptor)", + "MalformedParmsInterceptor) ", + "MalformedParmsInterceptor )" + }; + for (String badConfig : noExceptionButInvalidConfigs) { + props = new PoolProperties(); + props.setJdbcInterceptors(badConfig); + try { + props.getJdbcInterceptorsAsArray(); + } catch (Exception e) { + Assert.fail("Unexpected exception."); + } + } + } + + @Test + public void textExtraSemicolonBehavior() { + + // This one DOES get an extra/empty definition + PoolProperties props = new PoolProperties(); + props.setJdbcInterceptors(";EmptyLeadingSemiInterceptor"); + InterceptorDefinition[] jiDefs = props.getJdbcInterceptorsAsArray(); + Assert.assertNotNull(jiDefs); + Assert.assertEquals(jiDefs.length, 3); + + // This one does NOT get an extra/empty definition (no trailing whitespace) + props = new PoolProperties(); + props.setJdbcInterceptors("EmptyTrailingSemiInterceptor;"); + jiDefs = props.getJdbcInterceptorsAsArray(); + Assert.assertNotNull(jiDefs); + Assert.assertEquals(jiDefs.length, 2); + + // This one DOES get an extra/empty definition (with trailing whitespace) + props = new PoolProperties(); + props.setJdbcInterceptors("EmptyTrailingSemiInterceptor; "); + jiDefs = props.getJdbcInterceptorsAsArray(); + Assert.assertNotNull(jiDefs); + Assert.assertEquals(jiDefs.length, 3); + } +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestQueryTimeoutInterceptor.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestQueryTimeoutInterceptor.java new file mode 100644 index 0000000..7c5694b --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestQueryTimeoutInterceptor.java @@ -0,0 +1,51 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.Statement; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.interceptor.QueryTimeoutInterceptor; +import org.apache.tomcat.jdbc.test.driver.Driver; + +public class TestQueryTimeoutInterceptor extends DefaultTestCase { + + @Test + public void testTimeout() throws Exception { + int timeout = 10; + int withoutuser =10; + int withuser = withoutuser; + this.datasource.setMaxActive(withuser+withoutuser); + this.datasource.setJdbcInterceptors(QueryTimeoutInterceptor.class.getName()+"(queryTimeout="+timeout+")"); + this.datasource.setDriverClassName(Driver.class.getName()); + this.datasource.setUrl("jdbc:tomcat:test"); + Connection con = this.datasource.getConnection(); + Statement st = con.createStatement(); + Assert.assertEquals(st.getClass().getName(),timeout,st.getQueryTimeout()); + st.close(); + st = con.prepareStatement(""); + Assert.assertEquals(st.getClass().getName(),timeout,st.getQueryTimeout()); + st.close(); + st = con.prepareCall(""); + Assert.assertEquals(st.getClass().getName(),timeout,st.getQueryTimeout()); + st.close(); + con.close(); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSizePreservation.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSizePreservation.java new file mode 100644 index 0000000..2c2e350 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSizePreservation.java @@ -0,0 +1,126 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PoolConfiguration; +import org.apache.tomcat.jdbc.test.driver.Driver; + +/** + * https://bz.apache.org/bugzilla/show_bug.cgi?id=50613 + */ +public class TestSizePreservation { + + protected volatile DataSource ds = null; + + private void initSimplePoolProperties() { + PoolConfiguration p = new DefaultProperties(); + ds = new DataSource(); + ds.setPoolProperties(p); + + ds.getPoolProperties().setDriverClassName(Driver.class.getName()); + ds.getPoolProperties().setUrl(Driver.url); + ds.getPoolProperties().setFairQueue(true); + ds.getPoolProperties().setJmxEnabled(false); + ds.getPoolProperties().setTestWhileIdle(true); + ds.getPoolProperties().setTestOnBorrow(false); + ds.getPoolProperties().setTestOnReturn(false); + ds.getPoolProperties().setValidationInterval(30000); + ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(30000); + ds.getPoolProperties().setInitialSize(100); + ds.getPoolProperties().setMaxActive(100); + ds.getPoolProperties().setMinIdle(0); + ds.getPoolProperties().setMaxIdle(0); + ds.getPoolProperties().setMaxWait(10000); + ds.getPoolProperties().setRemoveAbandonedTimeout(10); + ds.getPoolProperties().setMinEvictableIdleTimeMillis(10000); + ds.getPoolProperties().setLogAbandoned(false); + ds.getPoolProperties().setRemoveAbandoned(false); + ds.getPoolProperties().setUseLock(true); + } + + private void initEvictingPool() { + initSimplePoolProperties(); + ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(25); + ds.getPoolProperties().setMinEvictableIdleTimeMillis(750); + ds.getPoolProperties().setRemoveAbandoned(true); + ds.getPoolProperties().setRemoveAbandonedTimeout(1); + } + + @Test + public void testSimple() throws Exception { + initSimplePoolProperties(); + common(); + ds.close(true); + Driver.reset(); + } + + @Test + public void testEvicting() throws Exception { + initEvictingPool(); + common(); + ds.close(true); + Driver.reset(); + } + + private void common() throws Exception { + ds.getConnection().close(); + final int iterations = 1000; + final AtomicInteger loopcount = new AtomicInteger(0); + final Runnable run = new Runnable() { + @Override + public void run() { + try { + while (loopcount.incrementAndGet() < iterations) { + Connection c = ds.getConnection(); + Thread.sleep(1000); + c.close(); + } + } catch (Exception x) { + x.printStackTrace(); + } + } + }; + Thread[] threads = new Thread[200]; + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(run); + } + for (int i = 0; i < threads.length; i++) { + threads[i].start(); + } + try { + while (loopcount.get() < iterations) { + Thread.sleep(250); + } + } catch (Exception x) { + loopcount.set(iterations); // stops the test + x.printStackTrace(); + } + for (int i = 0; i < threads.length; i++) { + threads[i].join(); + } + System.out.println("Pool size:"+ds.getPool().getSize()); + Assert.assertTrue("Size validity check: ", ds.getPool().getSize() >= 0); + } + +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSlowQueryComparator.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSlowQueryComparator.java new file mode 100644 index 0000000..458c3d2 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSlowQueryComparator.java @@ -0,0 +1,121 @@ +/* + * 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.tomcat.jdbc.test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport.QueryStats; + +public class TestSlowQueryComparator { + + @Test + public void testBug58489() throws Exception { + + long[] testData = { 0, 0, 0, 1444225382010l, 0, 1444225382011l, 0, + 1444225382012l, 0, 1444225382056l, 0, 1444225382014l, 0, + 1444225382015l, 0, 1444225382016l, 0, 0, 1444225382017l, 0, + 1444225678350l, 0, 1444225680397l, 0, 1444225382018l, + 1444225382019l, 1444225382020l, 0, 1444225382021l, 0, + 1444225382022l, 1444225382023l + + }; + + List stats = new ArrayList<>(); + + for (int i = 0; i < testData.length; i++) { + QueryStats qs = new QueryStats(String.valueOf(i)); + qs.add(0, testData[i]); + stats.add(qs); + } + + try { + Collections.sort(stats, createComparator()); + } catch (IllegalArgumentException e) { + Assert.fail(e.getMessage()); + } + } + + @Test + public void testEqualQueryStatsWithNoLastInvocation() throws Exception { + Comparator queryStatsComparator = createComparator(); + QueryStats q1 = new QueryStats("abc"); + Assert.assertEquals(0, queryStatsComparator.compare(q1, q1)); + } + + @Test + public void testEqualQueryStatsWithLastInvocation() throws Exception { + Comparator queryStatsComparator = createComparator(); + QueryStats q1 = new QueryStats("abc"); + q1.add(0, 100); + Assert.assertEquals(0, queryStatsComparator.compare(q1, q1)); + } + + @Test + public void testQueryStatsOneWithLastInvocation() throws Exception { + Comparator queryStatsComparator = createComparator(); + QueryStats q1 = new QueryStats("abc"); + QueryStats q2 = new QueryStats("def"); + q2.add(0, 100); + Assert.assertEquals(1, queryStatsComparator.compare(q1, q2)); + Assert.assertEquals(-1, queryStatsComparator.compare(q2, q1)); + } + + @Test + public void testQueryStatsBothWithSameLastInvocation() throws Exception { + Comparator queryStatsComparator = createComparator(); + QueryStats q1 = new QueryStats("abc"); + QueryStats q2 = new QueryStats("def"); + q1.add(0, 100); + q2.add(0, 100); + Assert.assertEquals(0, queryStatsComparator.compare(q1, q2)); + Assert.assertEquals(0, queryStatsComparator.compare(q2, q1)); + } + + @Test + public void testQueryStatsBothWithSomeLastInvocation() throws Exception { + Comparator queryStatsComparator = createComparator(); + QueryStats q1 = new QueryStats("abc"); + QueryStats q2 = new QueryStats("abc"); + q1.add(0, 100); + q2.add(0, 150); + Assert.assertEquals(-1, queryStatsComparator.compare(q1, q2)); + Assert.assertEquals(1, queryStatsComparator.compare(q2, q1)); + } + + private Comparator createComparator() + throws ClassNotFoundException, InstantiationException, + IllegalAccessException, InvocationTargetException, + SecurityException, NoSuchMethodException { + Class comparatorClass = Class + .forName("org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport$QueryStatsComparator"); + Constructor comparatorConstructor = comparatorClass.getConstructor(); + comparatorConstructor.setAccessible(true); + @SuppressWarnings("unchecked") + Comparator queryStatsComparator = (Comparator) comparatorConstructor + .newInstance(); + return queryStatsComparator; + } + +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSlowQueryReport.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSlowQueryReport.java new file mode 100644 index 0000000..411511f --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSlowQueryReport.java @@ -0,0 +1,336 @@ +/* + * 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.tomcat.jdbc.test; + +import java.lang.management.ManagementFactory; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import javax.management.AttributeChangeNotification; +import javax.management.Notification; +import javax.management.NotificationListener; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport; +import org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReportJmx; + +public class TestSlowQueryReport extends DefaultTestCase { + public static final String superSlowSql = "select count(1) from test where val1 like 'ewq%eq' and val2 = 'ew%rre' and val3 = 'sda%da' and val4 = 'dad%ada'"; + public static final String failedSql = "select 1 from non_existent"; + @Before + public void setUp() throws SQLException { + DriverManager.registerDriver(new MockDriver()); + + // use our mock driver + this.datasource.setDriverClassName(MockDriver.class.getName()); + this.datasource.setUrl(MockDriver.url); + + // Required to trigger validation query's execution + this.datasource.setInitialSize(1); + this.datasource.setTestOnBorrow(true); + this.datasource.setValidationInterval(-1); + this.datasource.setValidationQuery("SELECT 1"); + this.datasource.setMaxActive(1); + this.datasource.setJdbcInterceptors(SlowQueryReportJmx.class.getName()+"(threshold=50,notifyPool=false)"); + } + + @Test + public void testSlowSql() throws Exception { + int count = 3; + this.datasource.setMaxActive(1); + this.datasource.setJdbcInterceptors(SlowQueryReport.class.getName()+"(threshold=50)"); + Connection con = this.datasource.getConnection(); + for (int i=0; i map = SlowQueryReport.getPoolStats(datasource.getPool().getName()); + Assert.assertNotNull(map); + Assert.assertEquals(1,map.size()); + String key = map.keySet().iterator().next(); + SlowQueryReport.QueryStats stats = map.get(key); + System.out.println("Stats:"+stats); + + for (int i=0; i map = SlowQueryReport.getPoolStats(datasource.getPool().getName()); + Assert.assertNotNull(map); + Assert.assertEquals(1,map.size()); + String key = map.keySet().iterator().next(); + SlowQueryReport.QueryStats stats = map.get(key); + System.out.println("Stats:"+stats); + ClientListener listener = new ClientListener(); + ConnectionPool pool = datasource.getPool(); + ManagementFactory.getPlatformMBeanServer().addNotificationListener( + new SlowQueryReportJmx().getObjectName(SlowQueryReportJmx.class, pool.getName()), + listener, + null, + null); + + for (int i=0; i map = SlowQueryReport.getPoolStats(datasource.getPool().getName()); + Assert.assertNotNull(map); + Assert.assertEquals(1,map.size()); + ConnectionPool pool = datasource.getPool(); + con.close(); + tearDown(); + Assert.assertNull(SlowQueryReport.getPoolStats(pool.getName())); + } + + @Test + public void testFailedSql() throws Exception { + int count = 3; + Connection con = this.datasource.getConnection(); + for (int i=0; i map = SlowQueryReport.getPoolStats(datasource.getPool().getName()); + Assert.assertNotNull(map); + Assert.assertEquals(1,map.size()); + ConnectionPool pool = datasource.getPool(); + String key = map.keySet().iterator().next(); + SlowQueryReport.QueryStats stats = map.get(key); + System.out.println("Stats:"+stats); + con.close(); + tearDown(); + Assert.assertNull(SlowQueryReport.getPoolStats(pool.getName())); + } + + + public static class ClientListener implements NotificationListener { + AtomicInteger notificationCount = new AtomicInteger(0); + @Override + public void handleNotification(Notification notification, + Object handback) { + notificationCount.incrementAndGet(); + System.out.println("\nReceived notification:"); + System.out.println("\tClassName: " + notification.getClass().getName()); + System.out.println("\tSource: " + notification.getSource()); + System.out.println("\tType: " + notification.getType()); + System.out.println("\tMessage: " + notification.getMessage()); + if (notification instanceof AttributeChangeNotification) { + AttributeChangeNotification acn = + (AttributeChangeNotification) notification; + System.out.println("\tAttributeName: " + acn.getAttributeName()); + System.out.println("\tAttributeType: " + acn.getAttributeType()); + System.out.println("\tNewValue: " + acn.getNewValue()); + System.out.println("\tOldValue: " + acn.getOldValue()); + } + } + } + + /** + * Mock Driver, Connection and Statement implementations use to verify setQueryTimeout was called. + */ + public static class MockDriver implements java.sql.Driver { + public static final String url = "jdbc:tomcat:mock"; + + public MockDriver() { + } + + @Override + public boolean acceptsURL(String url) throws SQLException { + return url!=null && url.equals(MockDriver.url); + } + + @Override + public Connection connect(String url, Properties info) throws SQLException { + return new MockConnection(info); + } + + @Override + public int getMajorVersion() { + return 0; + } + + @Override + public int getMinorVersion() { + return 0; + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return null; + } + + @Override + public boolean jdbcCompliant() { + return false; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } + } + + public static class MockConnection extends org.apache.tomcat.jdbc.test.driver.Connection { + public MockConnection(Properties info) { + super(info); + } + + @Override + public Statement createStatement() throws SQLException { + return new MockStatement(false); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return new MockStatement(sql.equals(superSlowSql)); + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + return new MockStatement(sql.equals(superSlowSql)); + } + } + + public static class MockStatement extends org.apache.tomcat.jdbc.test.driver.Statement { + boolean slow = false; + + public MockStatement(boolean slow) { + this.slow = slow; + } + + @Override + public boolean execute(String sql) throws SQLException { + if (failedSql.equals(sql)) { + throw new SQLException("Invalid SQL:"+sql); + } + if (slow || superSlowSql.equals(sql)) { + try { + Thread.sleep(200); + }catch (Exception x) { + } + } + return super.execute(sql); + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + if (failedSql.equals(sql)) { + throw new SQLException("Invalid SQL:"+sql); + } + if (slow || superSlowSql.equals(sql)) { + try { + Thread.sleep(200); + }catch (Exception x) { + } + } + return super.executeQuery(sql); + } + + @Override + public ResultSet executeQuery() throws SQLException { + if (slow) { + try { + Thread.sleep(200); + }catch (Exception x) { + } + } + return super.executeQuery(); + } + } + + +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestStatementCache.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestStatementCache.java new file mode 100644 index 0000000..b7c3e30 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestStatementCache.java @@ -0,0 +1,209 @@ +/* + * 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.tomcat.jdbc.test; + +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.PreparedStatement; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.JdbcInterceptor; +import org.apache.tomcat.jdbc.pool.interceptor.StatementCache; +import org.apache.tomcat.jdbc.pool.interceptor.StatementCounterInterceptor; + +public class TestStatementCache extends DefaultTestCase { + + private static volatile TestStatementCacheInterceptor interceptor = null; + + + @Override + @After + public void tearDown() throws Exception { + interceptor = null; + super.tearDown(); + } + + + private void config(boolean cachePrepared, boolean cacheCallable, int max) { + datasource.getPoolProperties().setJdbcInterceptors(TestStatementCacheInterceptor.class.getName()+ + "(prepared="+cachePrepared+",callable="+cacheCallable+",max="+max+")"); + } + + @Test + public void testIsCacheEnabled() throws Exception { + config(true,true,50); + datasource.getConnection().close(); + Assert.assertNotNull("Interceptor was not created.", interceptor); + } + + @Test + public void testCacheProperties() throws Exception { + config(true,true,50); + datasource.getConnection().close(); + Assert.assertTrue(interceptor.isCacheCallable()); + Assert.assertTrue(interceptor.isCachePrepared()); + Assert.assertEquals(50,interceptor.getMaxCacheSize()); + } + + @Test + public void testCacheProperties2() throws Exception { + config(false,false,100); + datasource.getConnection().close(); + Assert.assertFalse(interceptor.isCacheCallable()); + Assert.assertFalse(interceptor.isCachePrepared()); + Assert.assertEquals(100,interceptor.getMaxCacheSize()); + } + + @Test + public void testPreparedStatementCache() throws Exception { + config(true,false,100); + Connection con = datasource.getConnection(); + PreparedStatement ps1 = con.prepareStatement("select 1"); + PreparedStatement ps2 = con.prepareStatement("select 1"); + Assert.assertEquals(0,interceptor.getCacheSize().get()); + ps1.close(); + Assert.assertTrue(ps1.isClosed()); + Assert.assertEquals(1,interceptor.getCacheSize().get()); + PreparedStatement ps3 = con.prepareStatement("select 1"); + Assert.assertEquals(0,interceptor.getCacheSize().get()); + ps2.close(); + Assert.assertTrue(ps2.isClosed()); + ps3.close(); + Assert.assertTrue(ps3.isClosed()); + Assert.assertEquals(1,interceptor.getCacheSize().get()); + con.close(); + } + + @Test + public void testPreparedStatementCache2() throws Exception { + init(); + config(false,false,100); + Connection con = datasource.getConnection(); + PreparedStatement ps1 = con.prepareStatement("select 1"); + PreparedStatement ps2 = con.prepareStatement("select 1"); + Assert.assertEquals(0,interceptor.getCacheSize().get()); + ps1.close(); + Assert.assertTrue(ps1.isClosed()); + Assert.assertEquals(0,interceptor.getCacheSize().get()); + PreparedStatement ps3 = con.prepareStatement("select 1"); + Assert.assertEquals(0,interceptor.getCacheSize().get()); + ps2.close(); + Assert.assertTrue(ps2.isClosed()); + ps3.close(); + Assert.assertTrue(ps3.isClosed()); + Assert.assertEquals(0,interceptor.getCacheSize().get()); + con.close(); + } + + @Test + public void testStatementClose1() throws Exception { + init(); + datasource.setJdbcInterceptors( + TestStatementCacheInterceptor.class.getName() + + "(prepared=true,callable=false,max=1);" + + StatementCounterInterceptor.class.getName()); + Connection con = datasource.getConnection(); + StatementCounterInterceptor counter = findInterceptor(con, StatementCounterInterceptor.class); + PreparedStatement ps1, ps2; + + ps1 = con.prepareStatement("select 1"); + Assert.assertEquals(1, counter.getActiveCount()); + ps1.close(); + Assert.assertEquals("Statement goes into cache, not closed", 1, counter.getActiveCount()); + + ps1 = con.prepareStatement("select 1"); + Assert.assertEquals("Reusing statement from cache", 1, counter.getActiveCount()); + ps2 = con.prepareStatement("select 1"); + Assert.assertEquals("Reusing statement from cache", 2, counter.getActiveCount()); + + ps2.close(); + Assert.assertEquals("Statement goes into cache, not closed", 2, counter.getActiveCount()); + ps1.close(); + // Cache has "max=1". The following tests BZ 54732. + Assert.assertEquals("Statement does not go into cache, closed", 1, counter.getActiveCount()); + + con.close(); + Assert.assertEquals("Connection returned to the pool. Statement is in cache", 1, counter.getActiveCount()); + + datasource.close(); + Assert.assertEquals("Pool cleared. All statements in cache are closed", 0, counter.getActiveCount()); + } + + @Test + public void testStatementClose2() throws Exception { + init(); + datasource.setJdbcInterceptors( + TestStatementCacheInterceptor.class.getName() + + "(prepared=false,callable=false,max=10);" + + StatementCounterInterceptor.class.getName()); + Connection con = datasource.getConnection(); + StatementCounterInterceptor counter = findInterceptor(con, StatementCounterInterceptor.class); + PreparedStatement ps1 = con.prepareStatement("select 1"); + Assert.assertEquals(1, counter.getActiveCount()); + ps1.close(); + Assert.assertEquals("Statement is not pooled, closes immediately", 0, counter.getActiveCount()); + } + + @Test + public void testMaxCacheSize() throws Exception { + init(); + config(true,false,100); + Connection con1 = datasource.getConnection(); + Connection con2 = datasource.getConnection(); + for (int i=0; i<120; i++) { + Connection con = (i%2==0)?con1:con2; + PreparedStatement ps = con.prepareStatement("select "+i); + ps.close(); + } + Assert.assertEquals(100,interceptor.getCacheSize().get()); + con1.close(); + con2.close(); + } + + + public static class TestStatementCacheInterceptor extends StatementCache { + public TestStatementCacheInterceptor() { + interceptor = this; + } + } + + /** + * Helper method that finds interceptor instance in interceptor chain of a + * proxied class. + * + * @param proxy + * Proxy class + * @param clazz + * Interceptor class that we are looking for + * @return Instance of clazz + */ + private static T findInterceptor(Object proxy, + Class clazz) { + JdbcInterceptor interceptor = (JdbcInterceptor) Proxy + .getInvocationHandler(proxy); + while (interceptor != null) { + if (clazz.isInstance(interceptor)) { + return clazz.cast(interceptor); + } + interceptor = interceptor.getNext(); + } + return null; + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSuspectTimeout.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSuspectTimeout.java new file mode 100644 index 0000000..9a9f06e --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestSuspectTimeout.java @@ -0,0 +1,46 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.PooledConnection; + +public class TestSuspectTimeout extends DefaultTestCase { + + @Test + public void testSuspect() throws Exception { + this.datasource.setMaxActive(100); + this.datasource.setMaxIdle(100); + this.datasource.setInitialSize(0); + this.datasource.getPoolProperties().setAbandonWhenPercentageFull(0); + this.datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(100); + this.datasource.getPoolProperties().setRemoveAbandoned(true); + this.datasource.getPoolProperties().setRemoveAbandonedTimeout(100); + this.datasource.getPoolProperties().setSuspectTimeout(1); + this.datasource.getPoolProperties().setLogAbandoned(true); + Connection con = datasource.getConnection(); + Assert.assertEquals("Number of connections active/busy should be 1",1,datasource.getPool().getActive()); + Thread.sleep(3000); + PooledConnection pcon = con.unwrap(PooledConnection.class); + Assert.assertTrue("Connection should be marked suspect",pcon.isSuspect()); + con.close(); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestTimeout.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestTimeout.java new file mode 100644 index 0000000..aec85b6 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestTimeout.java @@ -0,0 +1,94 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +public class TestTimeout extends DefaultTestCase { + + @Test + public void testCheckoutTimeout() throws Exception { + Set cons = new HashSet<>(); + try { + this.datasource.getPoolProperties().setTestWhileIdle(true); + this.datasource.getPoolProperties().setTestOnBorrow(false); + this.datasource.getPoolProperties().setTestOnReturn(false); + this.datasource.getPoolProperties().setValidationInterval(30000); + this.datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(1000); + this.datasource.getPoolProperties().setMaxActive(20); + this.datasource.getPoolProperties().setMaxWait(3000); + this.datasource.getPoolProperties().setRemoveAbandonedTimeout(5); + this.datasource.getPoolProperties().setMinEvictableIdleTimeMillis(5000); + this.datasource.getPoolProperties().setMinIdle(5); + this.datasource.getPoolProperties().setLogAbandoned(true); + System.out.println("About to test connection pool:"+datasource); + for (int i = 0; i < 21; i++) { + long now = System.currentTimeMillis(); + cons.add(this.datasource.getConnection()); + long delta = System.currentTimeMillis()-now; + System.out.println("Got connection #"+i+" in "+delta+" ms."); + } + Assert.fail(); + } catch ( Exception x ) { + // Expected on 21st checkout + }finally { + Thread.sleep(2000); + } + for (Connection c : cons) { + c.close(); + } + } + + @Test + public void testCheckoutTimeoutFair() throws Exception { + Set cons = new HashSet<>(); + try { + this.datasource.getPoolProperties().setFairQueue(true); + this.datasource.getPoolProperties().setTestWhileIdle(true); + this.datasource.getPoolProperties().setTestOnBorrow(false); + this.datasource.getPoolProperties().setTestOnReturn(false); + this.datasource.getPoolProperties().setValidationInterval(30000); + this.datasource.getPoolProperties().setTimeBetweenEvictionRunsMillis(1000); + this.datasource.getPoolProperties().setMaxActive(20); + this.datasource.getPoolProperties().setMaxWait(3000); + this.datasource.getPoolProperties().setRemoveAbandonedTimeout(5); + this.datasource.getPoolProperties().setMinEvictableIdleTimeMillis(5000); + this.datasource.getPoolProperties().setMinIdle(5); + this.datasource.getPoolProperties().setLogAbandoned(true); + System.out.println("About to test connection pool:"+datasource); + for (int i = 0; i < 21; i++) { + long now = System.currentTimeMillis(); + cons.add(this.datasource.getConnection()); + long delta = System.currentTimeMillis()-now; + System.out.println("Got connection #"+i+" in "+delta+" ms."); + } + Assert.fail(); + } catch ( Exception x ) { + // Expected on 21st checkout + }finally { + Thread.sleep(2000); + } + for (Connection c : cons) { + c.close(); + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestValidation.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestValidation.java new file mode 100644 index 0000000..d996173 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestValidation.java @@ -0,0 +1,647 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Savepoint; +import java.sql.Statement; +import java.util.Properties; +import java.util.logging.Logger; + +import javax.sql.PooledConnection; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TestValidation extends DefaultTestCase { + + public static final Boolean WITHAUTOCOMMIT = Boolean.TRUE; + public static final Boolean NOAUTOCOMMIT = Boolean.FALSE; + + public static final String WITHVALIDATIONQUERY = "SELECT 1"; + public static final String NOVALIDATIONQUERY = null; + + @Before + public void setUp() throws SQLException { + DriverManager.registerDriver(new MockDriver()); + + // use our mock driver + datasource.setDriverClassName(MockDriver.class.getName()); + datasource.setUrl(MockDriver.getUrlWithValidationOutcomes(ValidationOutcome.SUCCESS)); + + // Required to trigger validation query's execution + datasource.setInitialSize(1); + datasource.setMinIdle(1); + datasource.setMaxIdle(1); + datasource.setMaxActive(2); + // Validation interval is disabled by default to ensure validation occurs every time + datasource.setValidationInterval(-1); + // No validation query by default + datasource.setValidationQuery(null); + } + + @After + public void cleanup() throws SQLException { + datasource = createDefaultDataSource(); + DriverManager.deregisterDriver(new MockDriver()); + } + + private PooledConnection getPooledConnection() throws SQLException { + return (PooledConnection) datasource.getConnection(); + } + + private static MockConnection getMock(PooledConnection pooledConnection) throws SQLException { + return (MockConnection) pooledConnection.getConnection(); + } + + /* -------------------------------- * + * Validation onConnect * + * -------------------------------- */ + + private void checkOnConnectValidationWithOutcome(ValidationOutcome validationOutcomes, String validationQuery, Boolean autoCommit) throws SQLException { + datasource.setUrl(MockDriver.getUrlWithValidationOutcomes(validationOutcomes)); + datasource.getPoolProperties().setTestOnConnect(true); + datasource.getPoolProperties().setValidationQuery(validationQuery); + datasource.getPoolProperties().setDefaultAutoCommit(autoCommit); + PooledConnection cxn1 = getPooledConnection(); + MockConnection mockCxn1 = getMock(cxn1); + Assert.assertFalse("No transaction must be running after connection is obtained", mockCxn1.isRunningTransaction()); + } + + /* ------- No validation query ----- */ + + @Test + public void testOnConnectValidationWithoutValidationQueryDoesNotOccurWhenDisabled() throws SQLException { + datasource.setUrl(MockDriver.getUrlWithValidationOutcomes(ValidationOutcome.FAILURE)); + datasource.getPoolProperties().setTestOnConnect(false); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + PooledConnection cxn = getPooledConnection(); + Assert.assertFalse("No transaction must be running after connection is obtained", getMock(cxn).isRunningTransaction()); + } + + @Test + public void testOnConnectValidationSuccessWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.SUCCESS, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test(expected=SQLException.class) + public void testOnConnectFailureThenSuccessWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.FAILURE, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test(expected=SQLException.class) + public void testOnConnectExceptionThenSuccessWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.EXCEPTION, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnConnectValidationSuccessWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.SUCCESS, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test(expected=SQLException.class) + public void testOnConnectFailureThenSuccessWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.FAILURE, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test(expected=SQLException.class) + public void testOnConnectExceptionThenSuccessWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.EXCEPTION, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + /* ------- With validation query ----- */ + + @Test + public void testOnConnectValidationWithValidationSQLDoesNotOccurWhenDisabled() throws SQLException { + this.datasource.setUrl(MockDriver.getUrlWithValidationOutcomes(ValidationOutcome.FAILURE)); + datasource.getPoolProperties().setTestOnConnect(false); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + datasource.getPoolProperties().setValidationQuery("SELECT 1"); + PooledConnection cxn = getPooledConnection(); + Assert.assertFalse("No transaction must be running after connection is obtained", getMock(cxn).isRunningTransaction()); + } + + @Test + public void testOnConnectValidationSuccessWithValidationQueryAndAutoCommitEnabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.SUCCESS, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test(expected=SQLException.class) + public void testOnConnectFailureThenSuccessWithValidationQueryAndAutoCommitEnabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.FAILURE, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test(expected=SQLException.class) + public void testOnConnectExceptionThenSuccessWithValidationQueryAndAutoCommitEnabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.EXCEPTION, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnConnectValidationSuccessWithValidationQueryAndAutoCommitDisabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.SUCCESS, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test(expected=SQLException.class) + public void testOnConnectFailureThenSuccessWithValidationQueryAndAutoCommitDisabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.FAILURE, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test(expected=SQLException.class) + public void testOnConnectExceptionThenSuccessWithValidationQueryAndAutoCommitDisabled() throws SQLException { + checkOnConnectValidationWithOutcome(ValidationOutcome.EXCEPTION, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + /* -------------------------------- * + * Validation onBorrow * + * -------------------------------- */ + + private void obtainCxnWithValidationOutcomeAndAttemptAgain(ValidationOutcome validationOutcome, String validationQuery, Boolean autoCommit) throws SQLException { + datasource.getPoolProperties().setValidationQuery(validationQuery); + datasource.getPoolProperties().setDefaultAutoCommit(autoCommit); + + PooledConnection cxn1 = getPooledConnection(); + MockConnection mockCxn1 = getMock(cxn1); + Assert.assertFalse("No transaction must be running after connection is obtained", mockCxn1.isRunningTransaction()); + + // Discard connection and set next validation outcome to provided outcome value + mockCxn1.setValidationOutcome(validationOutcome); + cxn1.close(); + + PooledConnection cxn2 = getPooledConnection(); + MockConnection mockCxn2 = getMock(cxn2); + Assert.assertFalse("No transaction must be running after connection is obtained", mockCxn2.isRunningTransaction()); + if (validationOutcome == ValidationOutcome.SUCCESS) { + Assert.assertEquals("Connection with successful validation is reused", mockCxn1, mockCxn2); + } else { + Assert.assertNotEquals("Connection with failed validation must not be returned again", mockCxn1, mockCxn2); + } + } + + private void obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome validationOutcome, String validationQuery, Boolean autoCommit) throws SQLException { + datasource.getPoolProperties().setTestOnBorrow(true); + obtainCxnWithValidationOutcomeAndAttemptAgain(validationOutcome, validationQuery, autoCommit); + } + + /* ------- No validation query ----- */ + + @Test + public void testOnBorrowValidationWithoutValidationQueryDoesNotOccurWhenDisabled() throws SQLException { + datasource.getPoolProperties().setTestOnBorrow(false); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + PooledConnection cxn = getPooledConnection(); + Assert.assertFalse("No transaction must be running after connection is obtained", getMock(cxn).isRunningTransaction()); + } + + @Test + public void testOnBorrowValidationSuccessWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.SUCCESS, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationFailureWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.FAILURE, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationExceptionWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.EXCEPTION, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationSuccessWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.SUCCESS, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationFailureWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.FAILURE, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationExceptionWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.EXCEPTION, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + /* ------- With validation query ----- */ + + @Test + public void testOnBorrowValidationWithValidationQueryDoesNotOccurWhenDisabled() throws SQLException { + datasource.getPoolProperties().setTestOnBorrow(false); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + datasource.getPoolProperties().setValidationQuery("SELECT 1"); + PooledConnection cxn = getPooledConnection(); + Assert.assertFalse("No transaction must be running after connection is obtained", getMock(cxn).isRunningTransaction()); + } + + @Test + public void testOnBorrowValidationSuccessWithValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.SUCCESS, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationFailureWithValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.FAILURE, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationExceptionWithValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.EXCEPTION, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationSuccessWithValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.SUCCESS, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationFailureWithValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.FAILURE, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testOnBorrowValidationExceptionWithValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnBorrowValidationOutcomeAndAttemptAgain(ValidationOutcome.EXCEPTION, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + /* -------------------------------- * + * Validation onReturn * + * -------------------------------- */ + + private void obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome validationOutcome, String validationQuery, Boolean autoCommit) throws SQLException { + datasource.getPoolProperties().setTestOnReturn(true); + obtainCxnWithValidationOutcomeAndAttemptAgain(validationOutcome, validationQuery, autoCommit); + } + + /* ------- No validation query ----- */ + + @Test + public void testOnReturnValidationWithoutValidationQueryDoesNotOccurWhenDisabled() throws SQLException { + datasource.getPoolProperties().setTestOnReturn(false); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + PooledConnection cxn = getPooledConnection(); + Assert.assertFalse("No transaction must be running after connection is obtained", getMock(cxn).isRunningTransaction()); + } + + @Test + public void testOnReturnValidationSuccessWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.SUCCESS, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationFailureWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.FAILURE, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationExceptionWithoutValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.EXCEPTION, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationSuccessWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + datasource.getPoolProperties().setTestOnReturn(true); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.SUCCESS, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationFailureWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + datasource.getPoolProperties().setTestOnReturn(true); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.FAILURE, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationExceptionWithoutValidationQueryAndAutoCommitDisabled() throws SQLException { + datasource.getPoolProperties().setTestOnReturn(true); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.EXCEPTION, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + /* ------- With validation query ----- */ + + @Test + public void testOnReturnValidationWithValidationQueryDoesNotOccurWhenDisabled() throws SQLException { + datasource.getPoolProperties().setTestOnReturn(false); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + datasource.getPoolProperties().setValidationQuery("SELECT 1"); + PooledConnection cxn = getPooledConnection(); + Assert.assertFalse("No transaction must be running after connection is obtained", getMock(cxn).isRunningTransaction()); + } + + @Test + public void testOnReturnValidationSuccessWithValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.SUCCESS, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationFailureWithValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.FAILURE, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationExceptionWithValidationQueryAndAutoCommitEnabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.EXCEPTION, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationSuccessWithValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.SUCCESS, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationFailureWithValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.FAILURE, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testOnReturnValidationExceptionWithValidationQueryAndAutoCommitDisabled() throws SQLException { + obtainCxnWithOnReturnValidationOutcomeAndAttemptAgain(ValidationOutcome.EXCEPTION, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + /* -------------------------------- * + * Validation whileIdle * + * -------------------------------- */ + + private void obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome validationOutcome, String validationQuery, Boolean autoCommit) + throws SQLException, InterruptedException { + datasource.getPoolProperties().setTestWhileIdle(true); + datasource.getPoolProperties().setValidationInterval(1); + datasource.getPoolProperties().setDefaultAutoCommit(autoCommit); + datasource.getPoolProperties().setValidationQuery(validationQuery); + datasource.setUrl(MockDriver.getUrlWithValidationOutcomes(validationOutcome)); + + PooledConnection cxn1 = getPooledConnection(); + MockConnection mockCxn1 = getMock(cxn1); + Assert.assertFalse("No transaction must be running after connection is obtained", mockCxn1.isRunningTransaction()); + + cxn1.close(); + Assert.assertEquals("Pool must contain 1 idle connection at this time", datasource.getIdle(), 1); + + Thread.sleep(1200); // Nasty - instrument PooledConnection to drive time measurement instead of hard-coded System.currentTimeMillis() + datasource.testIdle(); + + if (validationOutcome == ValidationOutcome.SUCCESS) { + Assert.assertEquals("Pool must contain 1 idle connection at this time", datasource.getIdle(), 1); + } else { + Assert.assertEquals("Pool must not contain any idle connection at this time", datasource.getIdle(), 0); + } + } + + /* ------- No validation query ----- */ + + @Test + public void testWhileIdleReturnValidationWithoutValidationQueryDoesNotOccurWhenDisabled() throws SQLException, InterruptedException { + datasource.setUrl(MockDriver.getUrlWithValidationOutcomes(ValidationOutcome.FAILURE)); + datasource.getPoolProperties().setTestWhileIdle(false); + datasource.getPoolProperties().setDefaultAutoCommit(Boolean.FALSE); + datasource.getPoolProperties().setValidationInterval(1); + + PooledConnection cxn = getPooledConnection(); + cxn.close(); + Assert.assertEquals("Pool must contain 1 idle connection at this time", datasource.getIdle(), 1); + + Thread.sleep(1200); // Nasty - instrument PooledConnection to drive time measurement instead of hard-coded System.currentTimeMillis() + datasource.testIdle(); + Assert.assertEquals("Pool must contain 1 idle connection at this time", datasource.getIdle(), 1); + } + + @Test + public void testWhileIdleValidationSuccessWithoutValidationQueryAndAutoCommitEnabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.SUCCESS, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationFailureWithoutValidationQueryAndAutoCommitEnabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.FAILURE, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationExceptionWithoutValidationQueryAndAutoCommitEnabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.EXCEPTION, NOVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationSuccessWithoutValidationQueryAndAutoCommitDisabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.SUCCESS, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationFailureWithoutValidationQueryAndAutoCommitDisabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.FAILURE, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationExceptionWithoutValidationQueryAndAutoCommitDisabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.EXCEPTION, NOVALIDATIONQUERY, NOAUTOCOMMIT); + } + + /* ------- With validation query ----- */ + + @Test + public void testWhileIdleValidationSuccessWithValidationQueryAndAutoCommitEnabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.SUCCESS, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationFailureWithValidationQueryAndAutoCommitEnabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.FAILURE, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationExceptionWithValidationQueryAndAutoCommitEnabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.EXCEPTION, WITHVALIDATIONQUERY, WITHAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationSuccessWithValidationQueryAndAutoCommitDisabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.SUCCESS, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationFailureWithValidationQueryAndAutoCommitDisabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.FAILURE, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + @Test + public void testWhileIdleValidationExceptionWithValidationQueryAndAutoCommitDisabled() throws SQLException, InterruptedException { + obtainThenReleaseCxnAndAssessIdleValidationWithOutcome(ValidationOutcome.EXCEPTION, WITHVALIDATIONQUERY, NOAUTOCOMMIT); + } + + /* ------- Helper mock classes ----- */ + + public enum ValidationOutcome { + SUCCESS, // Validation returns true + FAILURE, // Validation returns false + EXCEPTION // Validation throws an unexpected exception + } + + /** + * Mock Driver, Connection and Statement implementations used to control validation outcome and verify transaction status. + */ + public static class MockDriver implements java.sql.Driver { + public static final String url = "jdbc:tomcat:mock"; + + public static String getUrlWithValidationOutcomes(ValidationOutcome validationOutcome) { + return url + "?" + validationOutcome; + } + + private ValidationOutcome getValidationOutcomeFromUrl(String url) { + String outcomesAsString = url.substring(url.lastIndexOf("?")+1); + return ValidationOutcome.valueOf(outcomesAsString); + } + + public MockDriver() { + } + + @Override + public boolean acceptsURL(String url) throws SQLException { + return url!=null && url.startsWith(MockDriver.url); + } + + @Override + public Connection connect(String url, Properties info) throws SQLException { + return new MockConnection(info, getValidationOutcomeFromUrl(url)); + } + + @Override + public int getMajorVersion() { + return 0; + } + + @Override + public int getMinorVersion() { + return 0; + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return null; + } + + @Override + public boolean jdbcCompliant() { + return false; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } + } + + public static class MockConnection extends org.apache.tomcat.jdbc.test.driver.Connection { + private boolean autoCommit = false; + private boolean runningTransaction = false; + private ValidationOutcome validationOutcome; + + public MockConnection(Properties info, ValidationOutcome validationOutcome) { + super(info); + this.validationOutcome = validationOutcome; + } + + public boolean isRunningTransaction() { + return runningTransaction; + } + + protected void statementExecuted() { + this.runningTransaction = !autoCommit; + } + + protected void transactionCleared() { + this.runningTransaction = false; + } + + protected void setValidationOutcome(ValidationOutcome validationOutcome) { + this.validationOutcome = validationOutcome; + } + + protected ValidationOutcome getValidationOutcome() { + return validationOutcome; + } + + @Override + public boolean getAutoCommit() throws SQLException { + return autoCommit; + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + this.autoCommit = autoCommit; + } + + @Override + public Statement createStatement() throws SQLException { + return new MockStatement(this); + } + + @Override + public void commit() throws SQLException { + super.commit(); + transactionCleared(); + } + + @Override + public void rollback() throws SQLException { + super.rollback(); + transactionCleared(); + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + super.rollback(savepoint); + transactionCleared(); + } + + @Override + public boolean isValid(int timeout) throws SQLException { + statementExecuted(); + switch (validationOutcome) { + case SUCCESS: { return true; } + case FAILURE: { return false; } + case EXCEPTION: { throw new SQLException("Unexpected error generated in test"); } + default: { return true; } + } + } + } + + public static class MockStatement extends org.apache.tomcat.jdbc.test.driver.Statement { + + private MockConnection connection; + + public MockStatement(MockConnection connection) { + super(); + this.connection = connection; + } + + @Override + public boolean execute(String sql) throws SQLException { + if (connection.getValidationOutcome()==ValidationOutcome.SUCCESS) { + return false; + } else { + throw new SQLException("Simulated validation query failure"); + } + } + } + +} \ No newline at end of file diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestValidationQueryTimeout.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestValidationQueryTimeout.java new file mode 100644 index 0000000..6382d64 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TestValidationQueryTimeout.java @@ -0,0 +1,260 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLTimeoutException; +import java.sql.Statement; +import java.util.Properties; +import java.util.logging.Logger; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.tomcat.jdbc.pool.interceptor.QueryTimeoutInterceptor; + +public class TestValidationQueryTimeout extends DefaultTestCase { + + private static final int TIMEOUT = 10; + private static boolean isTimeoutSet; + private static final String longQuery = "select * from test as A, test as B, test as C, test as D, test as E"; + + @Before + public void setUp() throws SQLException { + DriverManager.registerDriver(new MockDriver()); + + // use our mock driver + this.datasource.setDriverClassName(MockDriver.class.getName()); + this.datasource.setUrl(MockDriver.url); + + // Required to trigger validation query's execution + this.datasource.setInitialSize(1); + this.datasource.setTestOnBorrow(true); + this.datasource.setValidationInterval(-1); + this.datasource.setValidationQuery("SELECT 1"); + this.datasource.setValidationQueryTimeout(TIMEOUT); + + isTimeoutSet = false; + } + + @Override + @After + public void tearDown() throws SQLException { + DriverManager.deregisterDriver(new MockDriver()); + } + + @Test + public void testValidationQueryTimeoutEnabled() throws Exception { + // because testOnBorrow is true, this triggers the validation query + Connection con = this.datasource.getConnection(); + Assert.assertTrue(isTimeoutSet); + con.close(); + } + + @Test + public void testValidationQueryTimeoutDisabled() throws Exception { + this.datasource.setValidationQueryTimeout(-1); + + // because testOnBorrow is true, this triggers the validation query + Connection con = this.datasource.getConnection(); + Assert.assertFalse(isTimeoutSet); + con.close(); + } + + @Test + public void testValidationQueryTimeoutWithQueryTimeoutInterceptor() throws Exception { + int interceptorTimeout = 30; + this.datasource.setJdbcInterceptors( + QueryTimeoutInterceptor.class.getName()+ + "(queryTimeout="+ interceptorTimeout +")"); + + // because testOnBorrow is true, this triggers the validation query + Connection con = this.datasource.getConnection(); + Assert.assertTrue(isTimeoutSet); + + // now create a statement, make sure the query timeout is set by the interceptor + Statement st = con.createStatement(); + Assert.assertEquals(interceptorTimeout, st.getQueryTimeout()); + st.close(); + st = con.prepareStatement(""); + Assert.assertEquals(interceptorTimeout, st.getQueryTimeout()); + st.close(); + st = con.prepareCall(""); + Assert.assertEquals(interceptorTimeout, st.getQueryTimeout()); + st.close(); + con.close(); + + // pull another connection and check it + isTimeoutSet = false; + Connection con2 = this.datasource.getConnection(); + Assert.assertTrue(isTimeoutSet); + con2.close(); + } + + // this test depends on the execution time of the validation query + // specifically, it needs to run for longer than 1 second to pass + // if this fails + @Test(expected=SQLException.class) + public void testValidationQueryTimeoutOnConnection() throws Exception { + // use our mock driver + // Required to trigger validation query's execution + this.datasource.setTestOnConnect(true); + this.datasource.setValidationInterval(-1); + this.datasource.setValidationQuery(longQuery); + this.datasource.setValidationQueryTimeout(1); + + this.datasource.getConnection(); + } + + @Test(expected=SQLException.class) + public void testValidationInvalidOnConnection() throws Exception { + // use a real driver cause we have an invalid query to validate + this.datasource.setDriverClassName("org.h2.Driver"); + this.datasource.setUrl("jdbc:h2:~/.h2/test;QUERY_TIMEOUT=0;DB_CLOSE_ON_EXIT=FALSE"); + + // Required to trigger validation query's execution + this.datasource.setTestOnBorrow(true); + this.datasource.setInitialSize(1); + this.datasource.setTestOnConnect(true); + this.datasource.setValidationInterval(-1); + this.datasource.setValidationQuery("SELEC"); // 'SELECT' and 'SELECT 1' are valid queries + this.datasource.setValidationQueryTimeout(1); + + this.datasource.getConnection(); + } + + @Test + public void testLongValidationQueryTime() throws Exception { + // use our mock driver + Connection con = this.datasource.getConnection(); + Statement stmt = null; + long start = 0, end = 0; + try { + stmt = con.createStatement(); + // set the query timeout to 2 sec + // this keeps this test from slowing things down too much + stmt.setQueryTimeout(2); + // assert that our long query takes longer than one second to run + // this is a requirement for other tests to run properly + start = System.currentTimeMillis(); + stmt.execute(longQuery); + } catch (SQLTimeoutException ex) { + + } catch (SQLException x) { + Assert.fail("We should have got a timeout exception."); + } finally { + end = System.currentTimeMillis(); + + if (stmt != null) { stmt.close(); } + if (con != null) { con.close(); } + + Assert.assertTrue(start != 0 && end != 0); + //we're faking it + //Assert.assertTrue((end - start) > 1000); + } + } + + @Test(expected = SQLException.class) + public void testValidationQueryTimeoutOnBorrow() throws Exception { + // Required to trigger validation query's execution + this.datasource.setTestOnBorrow(true); + this.datasource.setValidationInterval(-1); + this.datasource.setValidationQuery(longQuery); + this.datasource.setValidationQueryTimeout(1); + // assert that even though the validation query we don't get a connection + this.datasource.getConnection(); + } + + /** + * Mock Driver, Connection and Statement implementations use to verify setQueryTimeout was called. + */ + public static class MockDriver implements java.sql.Driver { + public static final String url = "jdbc:tomcat:mock"; + + public MockDriver() { + } + + @Override + public boolean acceptsURL(String url) throws SQLException { + return url!=null && url.equals(MockDriver.url); + } + + @Override + public Connection connect(String url, Properties info) throws SQLException { + return new MockConnection(info); + } + + @Override + public int getMajorVersion() { + return 0; + } + + @Override + public int getMinorVersion() { + return 0; + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return null; + } + + @Override + public boolean jdbcCompliant() { + return false; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } + } + + public static class MockConnection extends org.apache.tomcat.jdbc.test.driver.Connection { + public MockConnection(Properties info) { + super(info); + } + + @Override + public Statement createStatement() throws SQLException { + return new MockStatement(); + } + } + + public static class MockStatement extends org.apache.tomcat.jdbc.test.driver.Statement { + @Override + public void setQueryTimeout(int seconds) throws SQLException { + super.setQueryTimeout(seconds); + isTimeoutSet = true; + } + + @Override + public boolean execute(String sql) throws SQLException { + if (longQuery.equals(sql)) { + throw new SQLTimeoutException(); + } else { + return super.execute(sql); + } + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TwoDataSources.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TwoDataSources.java new file mode 100644 index 0000000..2c508ab --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/TwoDataSources.java @@ -0,0 +1,61 @@ +/* + * 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.tomcat.jdbc.test; + +import java.sql.Connection; + +import org.junit.Assert; +import org.junit.Test; + +public class TwoDataSources extends DefaultTestCase { + + @Test + public void testTwoDataSources() throws Exception { + org.apache.tomcat.jdbc.pool.DataSource d1 = this.createDefaultDataSource(); + org.apache.tomcat.jdbc.pool.DataSource d2 = this.createDefaultDataSource(); + d1.setRemoveAbandoned(true); + d1.setRemoveAbandonedTimeout(2); + d1.setTimeBetweenEvictionRunsMillis(1000); + d2.setRemoveAbandoned(false); + Connection c1 = d1.getConnection(); + Connection c2 = d2.getConnection(); + Thread.sleep(5000); + try { + c1.createStatement(); + Assert.assertTrue("Connection should have been abandoned.",false); + }catch (Exception x) { + Assert.assertTrue("This is correct, c1 is abandoned",true); + } + + try { + c2.createStatement(); + Assert.assertTrue("Connection should not have been abandoned.",true); + }catch (Exception x) { + Assert.assertTrue("Connection c2 should be working",false); + } + try { + Assert.assertTrue("Connection should have been closed.",c1.isClosed()); + }catch (Exception x) { + Assert.assertTrue("This is correct, c1 is closed",true); + } + try { + Assert.assertFalse("Connection c2 should not have been closed.",c2.isClosed()); + }catch (Exception x) { + Assert.assertTrue("Connection c2 should be working",false); + } + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Connection.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Connection.java new file mode 100644 index 0000000..2c614e7 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Connection.java @@ -0,0 +1,320 @@ +/* + * 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.tomcat.jdbc.test.driver; + +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +import org.apache.tomcat.jdbc.pool.PooledConnection; + +public class Connection implements java.sql.Connection { + Properties info; + + public Connection(Properties info) { + this.info = info; + } + + public String getUsername() { + return info.getProperty(PooledConnection.PROP_USER); + } + + public String getPassword() { + return info.getProperty(PooledConnection.PROP_PASSWORD); + } + + @Override + public void clearWarnings() throws SQLException { + } + + @Override + public void close() throws SQLException { + Driver.disconnectCount.incrementAndGet(); + } + + @Override + public void commit() throws SQLException { + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return null; + } + + @Override + public Blob createBlob() throws SQLException { + return null; + } + + @Override + public Clob createClob() throws SQLException { + return null; + } + + @Override + public NClob createNClob() throws SQLException { + return null; + } + + @Override + public SQLXML createSQLXML() throws SQLException { + return null; + } + + @Override + public Statement createStatement() throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return null; + } + + @Override + public boolean getAutoCommit() throws SQLException { + return false; + } + + @Override + public String getCatalog() throws SQLException { + return null; + } + + @Override + public Properties getClientInfo() throws SQLException { + return null; + } + + @Override + public String getClientInfo(String name) throws SQLException { + return null; + } + + @Override + public int getHoldability() throws SQLException { + return 0; + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + return null; + } + + @Override + public int getTransactionIsolation() throws SQLException { + return 0; + } + + @Override + public Map> getTypeMap() throws SQLException { + return null; + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public boolean isClosed() throws SQLException { + return false; + } + + @Override + public boolean isReadOnly() throws SQLException { + return false; + } + + @Override + public boolean isValid(int timeout) throws SQLException { + return false; + } + + @Override + public String nativeSQL(String sql) throws SQLException { + return null; + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return new org.apache.tomcat.jdbc.test.driver.Statement(); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + } + + @Override + public void rollback() throws SQLException { + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + } + + @Override + public void setCatalog(String catalog) throws SQLException { + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + } + + @Override + public void setHoldability(int holdability) throws SQLException { + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + } + + @Override + public Savepoint setSavepoint() throws SQLException { + return null; + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + return null; + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + @Override + public T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public void setSchema(String schema) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public String getSchema() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void abort(Executor executor) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public int getNetworkTimeout() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + + +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Driver.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Driver.java new file mode 100644 index 0000000..7e0bb8d --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Driver.java @@ -0,0 +1,88 @@ +/* + * 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.tomcat.jdbc.test.driver; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +public class Driver implements java.sql.Driver { + public static final String url = "jdbc:tomcat:test"; + public static final AtomicInteger connectCount = new AtomicInteger(0); + public static final AtomicInteger disconnectCount = new AtomicInteger(0); + + public static void reset() { + connectCount.set(0); + disconnectCount.set(0); + } + + static { + try { + DriverManager.registerDriver(new Driver()); + }catch (Exception x) { + x.printStackTrace(); + throw new RuntimeException(x); + } + } + + public Driver() { + } + + @Override + public boolean acceptsURL(String url) throws SQLException { + return url!=null && url.equals(Driver.url); + } + + @Override + public Connection connect(String url, Properties info) throws SQLException { + connectCount.addAndGet(1); + return new org.apache.tomcat.jdbc.test.driver.Connection(info); + } + + @Override + public int getMajorVersion() { + return 0; + } + + @Override + public int getMinorVersion() { + return 0; + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return null; + } + + @Override + public boolean jdbcCompliant() { + return false; + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + // TODO Auto-generated method stub + return null; + } + + +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/ResultSet.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/ResultSet.java new file mode 100644 index 0000000..71d26b8 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/ResultSet.java @@ -0,0 +1,1223 @@ +/* + * 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.tomcat.jdbc.test.driver; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +public class ResultSet implements java.sql.ResultSet { + boolean hasNext = true; + + @Override + public boolean absolute(int row) throws SQLException { + return false; + } + + @Override + public void afterLast() throws SQLException { + } + + @Override + public void beforeFirst() throws SQLException { + } + + @Override + public void cancelRowUpdates() throws SQLException { + } + + @Override + public void clearWarnings() throws SQLException { + } + @Override + public void close() throws SQLException { + } + + @Override + public void deleteRow() throws SQLException { + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + return 0; + } + + @Override + public boolean first() throws SQLException { + return hasNext; + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + return null; + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + return null; + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) + throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) + throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getConcurrency() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public String getCursorName() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getFetchDirection() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getFetchSize() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getHoldability() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getInt(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getInt(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public long getLong(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public long getLong(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getNString(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getNString(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getObject(int columnIndex, Map> map) + throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getObject(String columnLabel, Map> map) + throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getRow() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public short getShort(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public short getShort(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public Statement getStatement() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getString(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getString(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) + throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) + throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getType() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SQLWarning getWarnings() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void insertRow() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public boolean isAfterLast() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isBeforeFirst() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isClosed() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isFirst() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isLast() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean last() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void moveToCurrentRow() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void moveToInsertRow() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public boolean next() throws SQLException { + boolean next = hasNext; + hasNext = false; + // TODO Auto-generated method stub + return next; + } + + @Override + public boolean previous() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void refreshRow() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public boolean relative(int rows) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean rowDeleted() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean rowInserted() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean rowUpdated() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setFetchSize(int rows) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, + long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, + long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBoolean(String columnLabel, boolean x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, + int length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, + long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateClob(String columnLabel, Reader reader) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, + long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNClob(int columnIndex, NClob clob) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNClob(String columnLabel, NClob clob) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNClob(String columnLabel, Reader reader) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNString(int columnIndex, String string) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNString(String columnLabel, String string) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateRow() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) + throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public boolean wasNull() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public T unwrap(Class iface) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Statement.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Statement.java new file mode 100644 index 0000000..c37bb08 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Statement.java @@ -0,0 +1,1320 @@ +/* + * 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.tomcat.jdbc.test.driver; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +public class Statement implements CallableStatement { + int timeout=-1; + @Override + public Array getArray(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Array getArray(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public BigDecimal getBigDecimal(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Blob getBlob(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Blob getBlob(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean getBoolean(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean getBoolean(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public byte getByte(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public byte getByte(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public byte[] getBytes(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public byte[] getBytes(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Reader getCharacterStream(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Reader getCharacterStream(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Clob getClob(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Clob getClob(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Date getDate(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Date getDate(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Date getDate(int parameterIndex, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Date getDate(String parameterName, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public double getDouble(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public double getDouble(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public float getFloat(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public float getFloat(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getInt(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getInt(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public long getLong(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public long getLong(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public Reader getNCharacterStream(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Reader getNCharacterStream(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public NClob getNClob(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public NClob getNClob(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getNString(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getNString(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getObject(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getObject(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getObject(int parameterIndex, Map> map) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Object getObject(String parameterName, Map> map) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Ref getRef(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Ref getRef(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public RowId getRowId(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public RowId getRowId(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SQLXML getSQLXML(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SQLXML getSQLXML(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public short getShort(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public short getShort(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public String getString(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getString(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Time getTime(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Time getTime(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Time getTime(int parameterIndex, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Time getTime(String parameterName, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Timestamp getTimestamp(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Timestamp getTimestamp(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public URL getURL(int parameterIndex) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public URL getURL(String parameterName) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBlob(String parameterName, Blob x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBlob(String parameterName, InputStream inputStream) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBoolean(String parameterName, boolean x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setByte(String parameterName, byte x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBytes(String parameterName, byte[] x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setClob(String parameterName, Clob x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setClob(String parameterName, Reader reader) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setClob(String parameterName, Reader reader, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setDate(String parameterName, Date x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setDate(String parameterName, Date x, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setDouble(String parameterName, double x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setFloat(String parameterName, float x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setInt(String parameterName, int x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setLong(String parameterName, long x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNCharacterStream(String parameterName, Reader value) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNClob(String parameterName, NClob value) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNClob(String parameterName, Reader reader) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNClob(String parameterName, Reader reader, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNString(String parameterName, String value) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNull(String parameterName, int sqlType) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setObject(String parameterName, Object x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setRowId(String parameterName, RowId x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setShort(String parameterName, short x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setString(String parameterName, String x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setTime(String parameterName, Time x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setTime(String parameterName, Time x, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setTimestamp(String parameterName, Timestamp x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setURL(String parameterName, URL val) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public boolean wasNull() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void addBatch() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void clearParameters() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public boolean execute() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public ResultSet executeQuery() throws SQLException { + // TODO Auto-generated method stub + return new org.apache.tomcat.jdbc.test.driver.ResultSet(); + } + + @Override + public int executeUpdate() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void addBatch(String sql) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void cancel() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void clearBatch() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void clearWarnings() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void close() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public boolean execute(String sql) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public int[] executeBatch() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + // TODO Auto-generated method stub + return new org.apache.tomcat.jdbc.test.driver.ResultSet(); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public Connection getConnection() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getFetchDirection() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getFetchSize() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + // TODO Auto-generated method stub + return new org.apache.tomcat.jdbc.test.driver.ResultSet(); + } + + @Override + public int getMaxFieldSize() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getMaxRows() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean getMoreResults() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public int getQueryTimeout() throws SQLException { + // TODO Auto-generated method stub + return timeout; + } + + @Override + public ResultSet getResultSet() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getResultSetConcurrency() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getResultSetHoldability() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getResultSetType() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getUpdateCount() throws SQLException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public SQLWarning getWarnings() throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isClosed() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isPoolable() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void setCursorName(String name) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setFetchSize(int rows) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setMaxFieldSize(int max) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setMaxRows(int max) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + // TODO Auto-generated method stub + this.timeout = seconds; + + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public T unwrap(Class iface) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void closeOnCompletion() throws SQLException { + // TODO Auto-generated method stub + + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + // TODO Auto-generated method stub + return false; + } + + @Override + public T getObject(int parameterIndex, Class type) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + @Override + public T getObject(String parameterName, Class type) throws SQLException { + // TODO Auto-generated method stub + return null; + } + + + +} diff --git a/modules/owb/.gitignore b/modules/owb/.gitignore new file mode 100644 index 0000000..6c3c891 --- /dev/null +++ b/modules/owb/.gitignore @@ -0,0 +1,13 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) +!/.mvn/wrapper/maven-wrapper.jar +/bin/ diff --git a/modules/owb/pom.xml b/modules/owb/pom.xml new file mode 100644 index 0000000..d08a6b6 --- /dev/null +++ b/modules/owb/pom.xml @@ -0,0 +1,149 @@ + + + + 4.0.0 + + + org.apache + apache + 30 + + + org.apache.tomcat + tomcat-owb + Apache Tomcat CDI 4 support + Apache Tomcat CDI 4 support using Apache OpenWebBeans + + 4.0.2 + jar + + + 2.0.1 + 2.1.0 + 4.0.1 + 2.1.1 + 10.1.19 + + + + + jakarta.inject + jakarta.inject-api + ${jakarta-inject.version} + + + jakarta.interceptor + jakarta.interceptor-api + ${jakarta-interceptor.version} + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${jakarta-enterprise-cdi.version} + + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation-api.version} + provided + + + + org.apache.openwebbeans + openwebbeans-spi + ${project.version} + + + org.apache.openwebbeans + openwebbeans-impl + ${project.version} + + + org.apache.openwebbeans + openwebbeans-web + ${project.version} + + + org.apache.openwebbeans + openwebbeans-el22 + ${project.version} + + + + org.apache.tomcat + tomcat-catalina + ${tomcat.version} + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + false + + + + + + + + + + jakarta.el:* + + + + + *:* + + META-INF/DEPENDENCIES + META-INF/LICENSE.txt + META-INF/LICENSE + META-INF/MANIFEST.MF + META-INF/NOTICE.txt + META-INF/NOTICE + + module-info.class + + + + + + + + + + + diff --git a/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansContextLifecycleListener.java b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansContextLifecycleListener.java new file mode 100644 index 0000000..e926cd7 --- /dev/null +++ b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansContextLifecycleListener.java @@ -0,0 +1,143 @@ +/* + * 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.webbeans.web.tomcat; + +import java.util.LinkedList; + +import org.apache.catalina.Context; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Valve; +import org.apache.tomcat.InstanceManager; +import org.apache.webbeans.servlet.WebBeansConfigurationListener; + + +/** + * Context lifecycle listener. + */ +public class OpenWebBeansContextLifecycleListener implements LifecycleListener { + + /** + * Add security valve. + */ + protected boolean addSecurityValve = true; + + /** + * @return true to add the security valve + */ + public boolean getAddSecurityValve() { + return addSecurityValve; + } + + /** + * Configure if a security valve will be added + * @param addSecurityValve the addSecurityValve to set + */ + public void setAddSecurityValve(boolean addSecurityValve) { + this.addSecurityValve = addSecurityValve; + } + + /** + * Start without a beans.xml file. + */ + protected boolean startWithoutBeansXml = true; + + /** + * @return the startWithoutBeansXml + */ + public boolean getStartWithoutBeansXml() { + return startWithoutBeansXml; + } + + /** + * @param startWithoutBeansXml the startWithoutBeansXml to set + */ + public void setStartWithoutBeansXml(boolean startWithoutBeansXml) { + this.startWithoutBeansXml = startWithoutBeansXml; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getSource() instanceof Context) { + Context context = (Context) event.getSource(); + if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { + if (getStartWithoutBeansXml() + || context.getResources().getResource("/WEB-INF/beans.xml").exists() + || context.getResources().getResource("/WEB-INF/classes/META-INF/beans.xml").exists()) { + // Registering ELResolver with JSP container + System.setProperty("org.apache.webbeans.application.jsp", "true"); + // Add Listeners + String[] oldListeners = context.findApplicationListeners(); + LinkedList listeners = new LinkedList<>(); + listeners.addFirst(WebBeansConfigurationListener.class.getName()); + for (String listener : oldListeners) { + listeners.add(listener); + context.removeApplicationListener(listener); + } + for (String listener : listeners) { + context.addApplicationListener(listener); + } + Pipeline pipeline = context.getPipeline(); + // Add to the corresponding pipeline to get a notification once configure is done + if (pipeline instanceof Lifecycle) { + boolean contextLifecycleListenerFound = false; + for (LifecycleListener listener : ((Lifecycle) pipeline).findLifecycleListeners()) { + if (listener instanceof OpenWebBeansContextLifecycleListener) { + contextLifecycleListenerFound = true; + } + } + if (!contextLifecycleListenerFound) { + ((Lifecycle) pipeline).addLifecycleListener(this); + } + } + if (getAddSecurityValve()) { + // Add security valve + boolean securityValveFound = false; + for (Valve valve : pipeline.getValves()) { + if (valve instanceof OpenWebBeansSecurityValve) { + securityValveFound = true; + } + } + if (!securityValveFound) { + pipeline.addValve(new OpenWebBeansSecurityValve()); + } + } + } + } + } else if (event.getSource() instanceof Pipeline && event.getType().equals(Lifecycle.START_EVENT)) { + // This notification occurs once the configuration is fully done, including naming resources setup + // Otherwise, the instance manager is not ready for creation + Pipeline pipeline = (Pipeline) event.getSource(); + if (pipeline.getContainer() instanceof Context) { + Context context = (Context) pipeline.getContainer(); + if (!(context.getInstanceManager() instanceof OpenWebBeansInstanceManager)) { + InstanceManager processor = context.getInstanceManager(); + if (processor == null) { + processor = context.createInstanceManager(); + } + InstanceManager custom = new OpenWebBeansInstanceManager(context.getLoader().getClassLoader(), processor); + context.setInstanceManager(custom); + } + } + } + } + +} diff --git a/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansInstanceManager.java b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansInstanceManager.java new file mode 100644 index 0000000..d1b39d7 --- /dev/null +++ b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansInstanceManager.java @@ -0,0 +1,142 @@ +/* + * 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.webbeans.web.tomcat; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.naming.NamingException; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.Producer; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.util.res.StringManager; +import org.apache.webbeans.config.WebBeansContext; +import org.apache.webbeans.container.BeanManagerImpl; +import org.apache.webbeans.inject.OWBInjector; + +public class OpenWebBeansInstanceManager implements InstanceManager { + + private static final Log log = LogFactory.getLog(OpenWebBeansInstanceManager.class); + private static final StringManager sm = StringManager.getManager(OpenWebBeansInstanceManager.class); + + private final ClassLoader loader; + private final InstanceManager instanceManager; + private final Map instances = new ConcurrentHashMap<>(); + private static final class Instance { + private final Object object; + private final CreationalContext context; + private Instance(Object object, CreationalContext context) { + this.object = object; + this.context = context; + } + } + + public OpenWebBeansInstanceManager(ClassLoader loader, InstanceManager instanceManager) { + this.loader = loader; + this.instanceManager = instanceManager; + } + + @SuppressWarnings("unchecked") + @Override + public void destroyInstance(Object object) + throws IllegalAccessException, InvocationTargetException { + Instance injectorInstance = instances.get(object); + if (injectorInstance != null) { + try { + ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + try { + BeanManagerImpl beanManager = WebBeansContext.currentInstance().getBeanManagerImpl(); + @SuppressWarnings("rawtypes") + Producer producer = beanManager.getProducerForJavaEeComponent(injectorInstance.object.getClass()); + if (producer != null) { + producer.dispose(injectorInstance.object); + } else if (injectorInstance.context != null) { + injectorInstance.context.release(); + } + } finally { + Thread.currentThread().setContextClassLoader(oldLoader); + } + } catch (Exception e) { + log.error(sm.getString("instanceManager.destroyError", object), e); + } + } + this.instanceManager.destroyInstance(object); + } + + @Override + public Object newInstance(Class aClass) throws IllegalAccessException, + InvocationTargetException, NamingException, InstantiationException, + IllegalArgumentException, NoSuchMethodException, SecurityException { + Object object = this.instanceManager.newInstance(aClass); + inject(object); + return object; + } + + @Override + public Object newInstance(String str) + throws IllegalAccessException, InvocationTargetException, + NamingException, InstantiationException, ClassNotFoundException, + IllegalArgumentException, NoSuchMethodException, SecurityException { + Object object = this.instanceManager.newInstance(str); + inject(object); + return object; + } + + @Override + public void newInstance(Object object) throws IllegalAccessException, + InvocationTargetException, NamingException { + inject(object); + } + + @Override + public Object newInstance(String str, ClassLoader cl) + throws IllegalAccessException, InvocationTargetException, + NamingException, InstantiationException, ClassNotFoundException, + IllegalArgumentException, NoSuchMethodException, SecurityException { + Object object = this.instanceManager.newInstance(str, cl); + inject(object); + return object; + } + + private void inject(Object object) { + try { + ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + CreationalContext context = null; + try { + BeanManager beanManager = WebBeansContext.currentInstance().getBeanManagerImpl(); + context = beanManager.createCreationalContext(null); + OWBInjector.inject(beanManager, object, context); + } finally { + Thread.currentThread().setContextClassLoader(oldLoader); + } + instances.put(object, new Instance(object, context)); + } catch (Exception e) { + log.error(sm.getString("instanceManager.injectError", object), e); + } + } + +} diff --git a/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansListener.java b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansListener.java new file mode 100644 index 0000000..33d8276 --- /dev/null +++ b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansListener.java @@ -0,0 +1,81 @@ +/* + * 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.webbeans.web.tomcat; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.core.FrameworkListener; + +/** + * This listener must be declared in server.xml as a Server listener to be active. + * It will add OpenWebBeansContextLifecycleListener on all contexts. + */ +public class OpenWebBeansListener extends FrameworkListener { + + public OpenWebBeansListener() { + // Try loading a class from OpenWebBeans to make sure it is available + new org.apache.webbeans.exception.WebBeansConfigurationException(""); + } + + @Override + protected LifecycleListener createLifecycleListener(Context context) { + OpenWebBeansContextLifecycleListener listener = new OpenWebBeansContextLifecycleListener(); + listener.setAddSecurityValve(getAddSecurityValve()); + listener.setStartWithoutBeansXml(getStartWithoutBeansXml()); + return listener; + } + + /** + * Add security valve. + */ + protected boolean addSecurityValve = true; + + /** + * @return true to add the security valve + */ + public boolean getAddSecurityValve() { + return addSecurityValve; + } + + /** + * Configure if a security valve will be added + * @param addSecurityValve the addSecurityValve to set + */ + public void setAddSecurityValve(boolean addSecurityValve) { + this.addSecurityValve = addSecurityValve; + } + + /** + * Start without a beans.xml file. + */ + protected boolean startWithoutBeansXml = true; + + /** + * @return the startWithoutBeansXml + */ + public boolean getStartWithoutBeansXml() { + return startWithoutBeansXml; + } + + /** + * @param startWithoutBeansXml the startWithoutBeansXml to set + */ + public void setStartWithoutBeansXml(boolean startWithoutBeansXml) { + this.startWithoutBeansXml = startWithoutBeansXml; + } + +} diff --git a/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansSecurityFilter.java b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansSecurityFilter.java new file mode 100644 index 0000000..2601f09 --- /dev/null +++ b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansSecurityFilter.java @@ -0,0 +1,67 @@ +/* + * 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.webbeans.web.tomcat; + +import java.io.IOException; +import java.security.Principal; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; + + +/** + * Filter which sets the UserPrincipal into a ThreadLocal + * to make it injectable via a CDI Producer. This is an alternative + * to the valve to allow configuration at the webapp level as well. + * The filter name should usually be OwbSecurityFilter, mapped on + * REQUEST with *. + */ +public class OpenWebBeansSecurityFilter implements Filter { + + private static ThreadLocal principal = new ThreadLocal<>(); + + public static Principal getPrincipal() { + return principal.get(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + if (request instanceof HttpServletRequest) { + Principal p = ((HttpServletRequest) request).getUserPrincipal(); + if (p != null) { + principal.set(p); + } + } + + // continue with the request + chain.doFilter(request, response); + } finally { + if (principal.get() != null) { + principal.remove(); + } + } + } + +} diff --git a/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansSecurityValve.java b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansSecurityValve.java new file mode 100644 index 0000000..5d944e2 --- /dev/null +++ b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/OpenWebBeansSecurityValve.java @@ -0,0 +1,59 @@ +/* + * 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.webbeans.web.tomcat; + +import java.io.IOException; +import java.security.Principal; + +import jakarta.servlet.ServletException; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; + + +/** + * Valve which sets the UserPrincipal into a ThreadLocal + * to make it injectable via a CDI Producer. + */ +public class OpenWebBeansSecurityValve extends ValveBase { + + private static ThreadLocal principal = new ThreadLocal<>(); + + public static Principal getPrincipal() { + return principal.get(); + } + + @Override + public void invoke(Request request, Response response) + throws IOException, ServletException { + Principal p = request.getUserPrincipal(); + try { + if (p != null) { + principal.set(p); + } + getNext().invoke(request, response); + } finally { + if (p != null) { + principal.remove(); + } + } + } + +} diff --git a/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/TomcatPlugin.java b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/TomcatPlugin.java new file mode 100644 index 0000000..383b391 --- /dev/null +++ b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/TomcatPlugin.java @@ -0,0 +1,102 @@ +/* + * 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.webbeans.web.tomcat; + +import java.util.EventListener; + +import jakarta.servlet.Filter; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContextAttributeListener; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletRequestAttributeListener; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.http.HttpSessionActivationListener; +import jakarta.servlet.http.HttpSessionAttributeListener; +import jakarta.servlet.http.HttpSessionBindingListener; +import jakarta.servlet.http.HttpSessionListener; + +import org.apache.tomcat.util.res.StringManager; +import org.apache.webbeans.config.WebBeansContext; +import org.apache.webbeans.exception.WebBeansConfigurationException; +import org.apache.webbeans.spi.SecurityService; +import org.apache.webbeans.spi.plugins.AbstractOwbPlugin; + +/** + * Tomcat plugin for OpenWebBeans. + */ +public class TomcatPlugin extends AbstractOwbPlugin { + + private static final StringManager sm = StringManager.getManager(TomcatPlugin.class); + + /** + * Security service implementation. + */ + private final TomcatSecurityService securityService = + (TomcatSecurityService) WebBeansContext.getInstance().getSecurityService(); + + @Override + public T getSupportedService(Class serviceClass) { + if (serviceClass.equals(SecurityService.class)) { + return serviceClass.cast(this.securityService); + } + return null; + } + + @Override + public void isManagedBean(Class clazz) { + if (isServletSpecClass(clazz)) { + throw new WebBeansConfigurationException(sm.getString("plugin.notManagedBean", clazz.getName())); + } + } + + @Override + public boolean supportsJavaEeComponentInjections(Class clazz) { + if (isServletSpecClass(clazz)) { + return true; + } + return false; + } + + private boolean isServletSpecClass(Class clazz) { + if (Servlet.class.isAssignableFrom(clazz) + || Filter.class.isAssignableFrom(clazz)) { + return true; + } + if (EventListener.class.isAssignableFrom(clazz)) { + return ServletContextListener.class.isAssignableFrom(clazz) + || ServletContextAttributeListener.class.isAssignableFrom(clazz) + || HttpSessionActivationListener.class.isAssignableFrom(clazz) + || HttpSessionAttributeListener.class.isAssignableFrom(clazz) + || HttpSessionBindingListener.class.isAssignableFrom(clazz) + || HttpSessionListener.class.isAssignableFrom(clazz) + || ServletRequestListener.class.isAssignableFrom(clazz) + || ServletRequestAttributeListener.class.isAssignableFrom(clazz); + } + return false; + } + + @Override + public boolean supportService(Class serviceClass) { + if (serviceClass.equals(SecurityService.class)) { + return true; + } + return false; + } + +} diff --git a/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/TomcatSecurityService.java b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/TomcatSecurityService.java new file mode 100644 index 0000000..8f290d4 --- /dev/null +++ b/modules/owb/src/main/java/org/apache/webbeans/web/tomcat/TomcatSecurityService.java @@ -0,0 +1,104 @@ +/* + * 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.webbeans.web.tomcat; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.security.Principal; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import javax.security.auth.Subject; + +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.webbeans.config.WebBeansContext; +import org.apache.webbeans.corespi.security.SimpleSecurityService; + +public class TomcatSecurityService extends SimpleSecurityService { + private final boolean useWrapper; + private final Principal proxy; + + public TomcatSecurityService(final WebBeansContext context) { + useWrapper = "true".equalsIgnoreCase(context.getOpenWebBeansConfiguration() + .getProperty("org.apache.webbeans.component.PrincipalBean.proxy", "true")); + final ClassLoader loader = SimpleSecurityService.class.getClassLoader(); + final Class[] apiToProxy = Stream.concat( + Stream.of(Principal.class), + Stream.of(context.getOpenWebBeansConfiguration() + .getProperty("org.apache.webbeans.component.PrincipalBean.proxyApis", "org.eclipse.microprofile.jwt.JsonWebToken").split(",")) + .map(String::trim) + .filter(it -> !it.isEmpty()) + .map(it -> { + try { // if MP JWT-Auth is available + return loader.loadClass(it.trim()); + } catch (final NoClassDefFoundError | ClassNotFoundException e) { + return null; + } + })).filter(Objects::nonNull).toArray(Class[]::new); + proxy = apiToProxy.length == 1 ? new TomcatSecurityServicePrincipal() : Principal.class.cast( + Proxy.newProxyInstance(loader, apiToProxy, (proxy, method, args) -> { + try { + return method.invoke(getCurrentPrincipal(), args); + } catch (final InvocationTargetException ite) { + throw ite.getTargetException(); + } + })); + + } + + @Override // reason of that class + public Principal getCurrentPrincipal() { + return useWrapper ? proxy : getUserPrincipal(); + } + + // ensure it is contextual + private static class TomcatSecurityServicePrincipal implements Principal { + @Override + public String getName() { + return unwrap().getName(); + } + + @Override + public boolean implies(final Subject subject) { + return unwrap().implies(subject); + } + + private Principal unwrap() { + return getUserPrincipal(); + } + } + + @SuppressWarnings("unchecked") + private static Principal getUserPrincipal() { + final BeanManager beanManager = CDI.current().getBeanManager(); + final HttpServletRequest request = HttpServletRequest.class.cast( + beanManager.getReference( + beanManager.resolve(beanManager.getBeans(HttpServletRequest.class)), HttpServletRequest.class, + beanManager.createCreationalContext(null))); + final Object supplier = request.getAttribute(Principal.class.getName() + ".supplier"); + if (supplier != null) { + return ((Supplier) supplier).get(); + } + return request.getUserPrincipal(); + } +} diff --git a/modules/owb/src/main/resources/META-INF/openwebbeans/openwebbeans.properties b/modules/owb/src/main/resources/META-INF/openwebbeans/openwebbeans.properties new file mode 100644 index 0000000..c152225 --- /dev/null +++ b/modules/owb/src/main/resources/META-INF/openwebbeans/openwebbeans.properties @@ -0,0 +1,20 @@ +#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. + +configuration.ordinal=20 + +org.apache.webbeans.spi.SecurityService=org.apache.webbeans.web.tomcat.TomcatSecurityService diff --git a/modules/owb/src/main/resources/META-INF/services/org.apache.webbeans.spi.plugins.OpenWebBeansPlugin b/modules/owb/src/main/resources/META-INF/services/org.apache.webbeans.spi.plugins.OpenWebBeansPlugin new file mode 100644 index 0000000..9b7d288 --- /dev/null +++ b/modules/owb/src/main/resources/META-INF/services/org.apache.webbeans.spi.plugins.OpenWebBeansPlugin @@ -0,0 +1,17 @@ +# 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. +org.apache.webbeans.web.tomcat.TomcatPlugin \ No newline at end of file diff --git a/modules/owb/src/main/resources/org/apache/webbeans/web/tomcat/LocalStrings.properties b/modules/owb/src/main/resources/org/apache/webbeans/web/tomcat/LocalStrings.properties new file mode 100644 index 0000000..33ec8f7 --- /dev/null +++ b/modules/owb/src/main/resources/org/apache/webbeans/web/tomcat/LocalStrings.properties @@ -0,0 +1,19 @@ +# 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. + +instanceManager.destroyError=An error occurred destroying the injector for instance [{0}] +instanceManager.injectError=An error occurred injecting the dependencies for instance [{0}] + +plugin.notManagedBean=Given class [{0}] is not a managed bean diff --git a/modules/stuffed/Dockerfile b/modules/stuffed/Dockerfile new file mode 100644 index 0000000..0ab94d3 --- /dev/null +++ b/modules/stuffed/Dockerfile @@ -0,0 +1,61 @@ +# 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. + +FROM eclipse-temurin:11-jre +VOLUME /tmp + +USER root +RUN mkdir -m 777 -p /deployments + +ADD target/tomcat-stuffed-1.0.jar /deployments/app.jar +ADD conf /deployments/conf +ADD webapps /deployments/webapps +# COPY *.war /deployments/ +RUN chmod 777 /deployments/conf +RUN chmod 777 /deployments/webapps + +WORKDIR /deployments + +ARG namespace=tomcat +ENV KUBERNETES_NAMESPACE=$namespace +ARG port=8080 +EXPOSE $port + +ENV JAVA_OPTS="-Dcatalina.base=. -Djava.security.egd=file:/dev/urandom" + +# Add JULI logging configuration +ENV JAVA_OPTS="${JAVA_OPTS} -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.util.logging.config.file=conf/logging.properties" +# OpenSSL integration for Java 17 +#ENV JAVA_OPTS="${JAVA_OPTS} --enable-native-access=ALL-UNNAMED --add-modules jdk.incubator.foreign" + +RUN sh -c 'touch app.jar' + +RUN mkdir -p /opt + +# Optional: Add Jolokia agent for JMX monitoring and management +# RUN mkdir /opt/jolokia && wget https://repo.maven.apache.org/maven2/org/jolokia/jolokia-jvm/1.7.2/jolokia-jvm-1.7.2.jar -O /opt/jolokia/jolokia.jar +# ARG jolokiaport=8778 +# ENV JAVA_OPTS="-javaagent:/opt/jolokia/jolokia.jar=host=*,port=$jolokiaport,protocol=https,authIgnoreCerts=true ${JAVA_OPTS}" +# EXPOSE $jolokiaport + +# Optional: Add Prometheus agent for JMX monitoring +# RUN mkdir /opt/prometheus && wget https://repo.maven.apache.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.20.0/jmx_prometheus_javaagent-0.20.0.jar -O /opt/prometheus/prometheus.jar && wget https://raw.githubusercontent.com/prometheus/jmx_exporter/master/example_configs/tomcat.yml -O conf/prometheus.yaml +# ARG prometheusport=9404 +# ENV JAVA_OPTS="-javaagent:/opt/prometheus/prometheus.jar=$prometheusport:conf/prometheus.yaml ${JAVA_OPTS}" +# EXPOSE $prometheusport + +ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar app.jar" ] +# ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar app.jar --war myrootwebapp.war --path /mydemo --war demo-1.0.war" ] diff --git a/modules/stuffed/DockerfileGraal b/modules/stuffed/DockerfileGraal new file mode 100644 index 0000000..19e2353 --- /dev/null +++ b/modules/stuffed/DockerfileGraal @@ -0,0 +1,41 @@ +# 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. + +FROM busybox:glibc + +VOLUME /tmp + +USER root +RUN mkdir -m 777 -p /deployments + +ADD tomcat-stuffed-1.0 /deployments/app +ADD conf /deployments/conf +ADD webapps /deployments/webapps + +WORKDIR /deployments + +ARG namespace=tomcat +ENV KUBERNETES_NAMESPACE=$namespace +ARG port=8080 +EXPOSE $port + +ENV JAVA_OPTS="-Dcatalina.base=. -Djava.security.egd=file:/dev/urandom" +ENV CATALINA_OPTS="--catalina -useGeneratedCode" + +# Add JULI logging configuration +ENV JAVA_OPTS="${JAVA_OPTS} -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.util.logging.config.file=conf/logging.properties" + +ENTRYPOINT [ "sh", "-c", "/deployments/app $JAVA_OPTS $CATALINA_OPTS" ] diff --git a/modules/stuffed/README.md b/modules/stuffed/README.md new file mode 100644 index 0000000..d13f959 --- /dev/null +++ b/modules/stuffed/README.md @@ -0,0 +1,88 @@ + + +# Apache Tomcat distribution for container platforms + +## Configuration + +Configuration is located in `conf/server.xml`, `conf/web.xml`, `conf/logging.properties`, all other configuration files, resources and context files are located in `conf`, identical to standalone Tomcat. + +## Building + +### Maven build + +Update Tomcat version number in the `pom.xml`, customize Tomcat components in the dependencies to keep the ones needed (only the main `tomcat-catalina` is mandatory). Custom Tomcat components sources can be added to the usual Maven build path and will be included in the package that is built. +``` +mvn clean; mvn package +``` + +### Docker build + +``` +docker build -t apache/tomcat-stuffed:1.0 -f ./Dockerfile . +``` +Docker build arguments include `namespace` (default is `tomcat`) and `port` which should match the Tomcat port in `server.xml` (default is `8080`). Other ports that need to be exposed can be added in the `Dockerfile` as needed. Webapps should be added to the `webapps` folder where they will be auto deployed by the host if using the defaults. Otherwise, the `Dockerfile` command line can be edited like below to include the necessary resources and command line arguments to run a single or multiple hardcoded web applications. + +## Running + +Add a webapp as folder mywebapp (for this example, or specify another path), or a path from which a configured Host will auto deploy +``` +--path: Specify a path the webapp will use +--war: Add the specified path (directory or war) as a webapp (if no path has been specified, it will be the root webapp) +``` + +The JULI logging manager configuration is optional but makes logging more readable and configurable: +`-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.util.logging.config.file=conf/logging.properties` +The default JULI configuration uses `catalina.base`, so specifying the system property with `-Dcatalina.base=.` is also useful. + +### Command line example with a single root webapp + +``` +java -Dcatalina.base=. -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.util.logging.config.file=conf/logging.properties -jar target/tomcat-stuffed-1.0.jar --war myrootwebapp +``` + +### Command line example with three webapps + +``` +java -Dcatalina.base=. -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.util.logging.config.file=conf/logging.properties -jar target/tomcat-stuffed-1.0.jar --war myrootwebapp --path /path1 --war mywebapp1 --path /path2 --war mywebapp2 +``` + +## Cloud + +### Deployment + +An example `tomcat.yaml` is included which uses the Docker image. It uses the health check valve which can be added in `conf/server.xml`, or a similar service responding to requests on `/health`. It also declares the optional Jolokia and Prometheus ports for monitoring. + +### Cluster + +If using the Kubernetes cloud clustering membership provider, the pod needs to have the permission to view other pods. For example with Openshift, this is done with: +``` +oc policy add-role-to-user view system:serviceaccount:$(oc project -q):default -n $(oc project -q) +``` + +## Native Image + +The Tomcat documentation includes information on using the native-image tool (docs/graal.html). + +Running in a container is possible, an example `DockerfileGraal` is given. To use a native image in a container that is not identical to the build platform, +the `native-image` call will need to use the additional `--static` parameter to statically link base libraries (this will then require zlib and glibc +static libraries). Due to TLS using dynamic libraries (SunEC for JSSE and tomcat-native for OpenSSL), TLS support is not available with static linking. +If TLS support is needed, the native image must instead be built on a platform identical to the target platform. diff --git a/modules/stuffed/conf/.gitignore b/modules/stuffed/conf/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/modules/stuffed/conf/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/modules/stuffed/pom.xml b/modules/stuffed/pom.xml new file mode 100644 index 0000000..66d99c3 --- /dev/null +++ b/modules/stuffed/pom.xml @@ -0,0 +1,128 @@ + + + + 4.0.0 + + org.apache.tomcat + tomcat-stuffed + 1.0 + jar + + + UTF-8 + org.apache.catalina.startup.Tomcat + 10.1.18 + + + + + org.apache.tomcat + tomcat-catalina + ${tomcat.version} + + + + org.apache.tomcat + tomcat-jasper + ${tomcat.version} + + + + org.apache.tomcat + tomcat-catalina-ha + ${tomcat.version} + + + + org.apache.tomcat + tomcat-websocket + ${tomcat.version} + + + + org.apache.tomcat + tomcat-dbcp + ${tomcat.version} + + + + org.apache.tomcat + tomcat-storeconfig + ${tomcat.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 11 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + false + + + *:* + + module-info.class + + + + + + + + META-INF/services/jakarta.servlet.ServletContainerInitializer + + + ${mainClass} + + + + .RSA + .SF + META-INF/MANIFEST.MF + META-INF/web-fragment.xml + + + + + + + + + + diff --git a/modules/stuffed/tomcat-jni.json b/modules/stuffed/tomcat-jni.json new file mode 100644 index 0000000..a72c482 --- /dev/null +++ b/modules/stuffed/tomcat-jni.json @@ -0,0 +1,7 @@ +[ +{ "name":"org.apache.tomcat.jni.Error" }, +{ "name":"org.apache.tomcat.jni.FileInfo" }, +{ "name":"org.apache.tomcat.jni.Sockaddr" }, +{ "name":"org.apache.tomcat.jni.SSL", "methods":[{"name":"newSSL","parameterTypes":["long","boolean"]}] }, +{ "name":"java.lang.String", "methods":[{"name":"","parameterTypes":["byte[]"]},{"name":"getBytes","parameterTypes":[]}] } +] diff --git a/modules/stuffed/tomcat-reflection.json b/modules/stuffed/tomcat-reflection.json new file mode 100644 index 0000000..29566cb --- /dev/null +++ b/modules/stuffed/tomcat-reflection.json @@ -0,0 +1,51 @@ +[ +{ "name":"java.lang.Object" }, +{ "name":"jakarta.servlet.http.HttpServlet" }, +{ "name":"org.apache.catalina.AccessLog" }, +{ "name":"org.apache.catalina.AsyncDispatcher" }, +{ "name":"org.apache.catalina.Authenticator" }, +{ "name":"org.apache.catalina.Cluster" }, +{ "name":"org.apache.catalina.Container" }, +{ "name":"org.apache.catalina.Contained" }, +{ "name":"org.apache.catalina.Context" }, +{ "name":"org.apache.catalina.CredentialHandler" }, +{ "name":"org.apache.catalina.DistributedManager" }, +{ "name":"org.apache.catalina.Engine" }, +{ "name":"org.apache.catalina.Executor" }, +{ "name":"org.apache.catalina.Group" }, +{ "name":"org.apache.catalina.Host" }, +{ "name":"org.apache.catalina.JmxEnabled" }, +{ "name":"org.apache.catalina.Lifecycle" }, +{ "name":"org.apache.catalina.LifecycleListener" }, +{ "name":"org.apache.catalina.Loader" }, +{ "name":"org.apache.catalina.Manager" }, +{ "name":"org.apache.catalina.Pipeline" }, +{ "name":"org.apache.catalina.Realm" }, +{ "name":"org.apache.catalina.Role" }, +{ "name":"org.apache.catalina.Server" }, +{ "name":"org.apache.catalina.Service" }, +{ "name":"org.apache.catalina.Session" }, +{ "name":"org.apache.catalina.SessionIdGenerator" }, +{ "name":"org.apache.catalina.SessionListener" }, +{ "name":"org.apache.catalina.Store" }, +{ "name":"org.apache.catalina.StoreManager" }, +{ "name":"org.apache.catalina.User" }, +{ "name":"org.apache.catalina.UserDatabase" }, +{ "name":"org.apache.catalina.Valve" }, +{ "name":"org.apache.catalina.WebResource" }, +{ "name":"org.apache.catalina.WebResourceRoot" }, +{ "name":"org.apache.catalina.WebResourceSet" }, +{ "name":"org.apache.catalina.Wrapper" }, +{ "name":"org.apache.catalina.tribes.Channel" }, +{ "name":"org.apache.catalina.tribes.MembershipService" }, +{ "name":"org.apache.coyote.UpgradeProtocol" }, +{ "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.el.ExpressionFactoryImpl", "methods" : [{"name":"","parameterTypes":[]}] }, +{ "name":"org.apache.naming.factory.EjbFactory", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.naming.factory.ResourceEnvFactory", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.naming.factory.ResourceFactory", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.naming.factory.TransactionFactory", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.tomcat.websocket.server.WsHttpUpgradeHandler", "methods" : [{"name": "","parameterTypes":[] }] } +] diff --git a/modules/stuffed/tomcat-resource.json b/modules/stuffed/tomcat-resource.json new file mode 100644 index 0000000..0c41918 --- /dev/null +++ b/modules/stuffed/tomcat-resource.json @@ -0,0 +1,85 @@ +{ + "bundles":[ + {"name":"jakarta.el.LocalStrings"}, + {"name":"jakarta.servlet.LocalStrings"}, + {"name":"jakarta.servlet.http.LocalStrings"}, + {"name":"org.apache.catalina.authenticator.LocalStrings"}, + {"name":"org.apache.catalina.authenticator.jaspic.LocalStrings"}, + {"name":"org.apache.catalina.connector.LocalStrings"}, + {"name":"org.apache.catalina.core.LocalStrings"}, + {"name":"org.apache.catalina.deploy.LocalStrings"}, + {"name":"org.apache.catalina.filters.LocalStrings"}, + {"name":"org.apache.catalina.ha.authenticator.LocalStrings"}, + {"name":"org.apache.catalina.ha.backend.LocalStrings"}, + {"name":"org.apache.catalina.ha.context.LocalStrings"}, + {"name":"org.apache.catalina.ha.deploy.LocalStrings"}, + {"name":"org.apache.catalina.ha.session.LocalStrings"}, + {"name":"org.apache.catalina.ha.tcp.LocalStrings"}, + {"name":"org.apache.catalina.loader.LocalStrings"}, + {"name":"org.apache.catalina.manager.LocalStrings"}, + {"name":"org.apache.catalina.manager.host.LocalStrings"}, + {"name":"org.apache.catalina.mapper.LocalStrings"}, + {"name":"org.apache.catalina.mbeans.LocalStrings"}, + {"name":"org.apache.catalina.realm.LocalStrings"}, + {"name":"org.apache.catalina.security.LocalStrings"}, + {"name":"org.apache.catalina.servlets.LocalStrings"}, + {"name":"org.apache.catalina.session.LocalStrings"}, + {"name":"org.apache.catalina.startup.LocalStrings"}, + {"name":"org.apache.catalina.storeconfig.LocalStrings"}, + {"name":"org.apache.catalina.tribes.group.LocalStrings"}, + {"name":"org.apache.catalina.tribes.group.interceptors.LocalStrings"}, + {"name":"org.apache.catalina.tribes.io.LocalStrings"}, + {"name":"org.apache.catalina.tribes.jmx.LocalStrings"}, + {"name":"org.apache.catalina.tribes.membership.LocalStrings"}, + {"name":"org.apache.catalina.tribes.membership.cloud.LocalStrings"}, + {"name":"org.apache.catalina.tribes.tipis.LocalStrings"}, + {"name":"org.apache.catalina.tribes.transport.LocalStrings"}, + {"name":"org.apache.catalina.tribes.transport.nio.LocalStrings"}, + {"name":"org.apache.catalina.tribes.util.LocalStrings"}, + {"name":"org.apache.catalina.users.LocalStrings"}, + {"name":"org.apache.catalina.util.LocalStrings"}, + {"name":"org.apache.catalina.valves.LocalStrings"}, + {"name":"org.apache.catalina.valves.rewrite.LocalStrings"}, + {"name":"org.apache.catalina.webresources.LocalStrings"}, + {"name":"org.apache.coyote.LocalStrings"}, + {"name":"org.apache.coyote.http11.LocalStrings"}, + {"name":"org.apache.coyote.http11.filters.LocalStrings"}, + {"name":"org.apache.coyote.http11.upgrade.LocalStrings"}, + {"name":"org.apache.coyote.http2.LocalStrings"}, + {"name":"org.apache.el.LocalStrings"}, + {"name":"org.apache.jasper.resources.LocalStrings"}, + {"name":"org.apache.naming.LocalStrings"}, + {"name":"org.apache.naming.factory.LocalStrings"}, + {"name":"org.apache.naming.factory.webservices.LocalStrings"}, + {"name":"org.apache.tomcat.dbcp.dbcp2.LocalStrings"}, + {"name":"org.apache.tomcat.util.LocalStrings"}, + {"name":"org.apache.tomcat.util.buf.LocalStrings"}, + {"name":"org.apache.tomcat.util.codec.binary.LocalStrings"}, + {"name":"org.apache.tomcat.util.compat.LocalStrings"}, + {"name":"org.apache.tomcat.util.descriptor.LocalStrings"}, + {"name":"org.apache.tomcat.util.descriptor.tld.LocalStrings"}, + {"name":"org.apache.tomcat.util.descriptor.web.LocalStrings"}, + {"name":"org.apache.tomcat.util.digester.LocalStrings"}, + {"name":"org.apache.tomcat.util.http.LocalStrings"}, + {"name":"org.apache.tomcat.util.http.parser.LocalStrings"}, + {"name":"org.apache.tomcat.util.json.LocalStrings"}, + {"name":"org.apache.tomcat.util.modeler.LocalStrings"}, + {"name":"org.apache.tomcat.util.net.LocalStrings"}, + {"name":"org.apache.tomcat.util.scan.LocalStrings"}, + {"name":"org.apache.tomcat.util.security.LocalStrings"}, + {"name":"org.apache.tomcat.util.threads.LocalStrings"}, + {"name":"org.apache.tomcat.websocket.LocalStrings"}, + {"name":"org.apache.tomcat.websocket.pojo.LocalStrings"}, + {"name":"org.apache.tomcat.websocket.server.LocalStrings"} + ], + "resources":[ + {"pattern":"^org/apache/tomcat/.*mbeans-descriptors\\.xml$"}, + {"pattern":"^org/apache/catalina/.*mbeans-descriptors\\.xml$"}, + {"pattern":"^org/apache/coyote/.*mbeans-descriptors\\.xml$"}, + {"pattern":"^org/apache/catalina/.*\\.properties$"}, + {"pattern":"^jakarta/servlet/resources/.*"}, + {"pattern":"^org/apache/tomcat/.*\\.dtd$"}, + {"pattern":"^org/apache/jasper/.*mbeans-descriptors\\.xml$"}, + {"pattern":"^jakarta/servlet/jsp/resources/.*"} + ] +} diff --git a/modules/stuffed/tomcat.yaml b/modules/stuffed/tomcat.yaml new file mode 100644 index 0000000..a85f4ad --- /dev/null +++ b/modules/stuffed/tomcat.yaml @@ -0,0 +1,51 @@ +# 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. + +kind: Deployment +apiVersion: extensions/v1beta1 +metadata: + name: tomcat + creationTimestamp: + labels: + run: tomcat +spec: + replicas: 1 + selector: + matchLabels: + run: tomcat + template: + metadata: + creationTimestamp: + labels: + run: tomcat + spec: + containers: + - name: tomcat + image: apache/tomcat-stuffed:1.0 + ports: + - containerPort: 8080 + - containerPort: 8778 + name: jolokia + - containerPort: 9404 + name: prometheus + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + resources: {} + strategy: {} +status: {} diff --git a/modules/stuffed/webapp-jspc.ant.xml b/modules/stuffed/webapp-jspc.ant.xml new file mode 100644 index 0000000..cffafcf --- /dev/null +++ b/modules/stuffed/webapp-jspc.ant.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JARs from /WEB-INF/lib need to be made available to Maven as dependencies. + Classes from /WEB-INF/classes will be packaged to the shaded JAR, but they will not be available during compilation. If needed during that step, they need to be packaged as JARs and made available to Maven as dependencies. + + + + + + + + + + + + + + + diff --git a/modules/stuffed/webapps/.gitignore b/modules/stuffed/webapps/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/modules/stuffed/webapps/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/res/META-INF/annotations-api.jar.manifest b/res/META-INF/annotations-api.jar.manifest new file mode 100644 index 0000000..7ad1a8d --- /dev/null +++ b/res/META-INF/annotations-api.jar.manifest @@ -0,0 +1,11 @@ +Manifest-version: 1.0 +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ + +Name: jakarta/annotation/ +Specification-Title: Jakarta Annotations +Specification-Version: @annotation.spec.version@ +Specification-Vendor: Eclipse Foundation +Implementation-Title: jakarta.annotation +Implementation-Version: @annotation.spec.version@@annotation.revision@ +Implementation-Vendor: Apache Software Foundation diff --git a/res/META-INF/bootstrap.jar.manifest b/res/META-INF/bootstrap.jar.manifest new file mode 100644 index 0000000..74d5425 --- /dev/null +++ b/res/META-INF/bootstrap.jar.manifest @@ -0,0 +1,11 @@ +Manifest-Version: 1.0 +Main-Class: org.apache.catalina.startup.Bootstrap +Class-Path: commons-daemon.jar +Specification-Title: Apache Tomcat Bootstrap +Specification-Version: @VERSION_MAJOR_MINOR@ +Specification-Vendor: Apache Software Foundation +Implementation-Title: Apache Tomcat Bootstrap +Implementation-Version: @VERSION@ +Implementation-Vendor: Apache Software Foundation +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ \ No newline at end of file diff --git a/res/META-INF/default.license b/res/META-INF/default.license new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/res/META-INF/default.license @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/res/META-INF/default.manifest b/res/META-INF/default.manifest new file mode 100644 index 0000000..8937c41 --- /dev/null +++ b/res/META-INF/default.manifest @@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Specification-Title: Apache Tomcat +Specification-Version: @VERSION_MAJOR_MINOR@ +Specification-Vendor: Apache Software Foundation +Implementation-Title: Apache Tomcat +Implementation-Version: @VERSION@ +Implementation-Vendor: Apache Software Foundation +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ diff --git a/res/META-INF/default.notice b/res/META-INF/default.notice new file mode 100644 index 0000000..9b66b20 --- /dev/null +++ b/res/META-INF/default.notice @@ -0,0 +1,5 @@ +Apache Tomcat +Copyright 1999-@YEAR@ The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/res/META-INF/default/.gitignore b/res/META-INF/default/.gitignore new file mode 100644 index 0000000..0073da0 --- /dev/null +++ b/res/META-INF/default/.gitignore @@ -0,0 +1,24 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +# Git ignores empty directories and the unit tests require this directory to +# be present for the welcome file tests to pass. The presence of this file +# doesn't break the unit tests. +# +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/res/META-INF/el-api.jar.manifest b/res/META-INF/el-api.jar.manifest new file mode 100644 index 0000000..0383e37 --- /dev/null +++ b/res/META-INF/el-api.jar.manifest @@ -0,0 +1,11 @@ +Manifest-version: 1.0 +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ + +Name: jakarta/el/ +Specification-Title: Jakarta Expression Language +Specification-Version: @el.spec.version@ +Specification-Vendor: Eclipse Foundation +Implementation-Title: jakarta.annotation +Implementation-Version: @el.spec.version@@el.revision@ +Implementation-Vendor: Apache Software Foundation diff --git a/res/META-INF/jasper-el.jar/services/jakarta.el.ExpressionFactory b/res/META-INF/jasper-el.jar/services/jakarta.el.ExpressionFactory new file mode 100644 index 0000000..8076bd2 --- /dev/null +++ b/res/META-INF/jasper-el.jar/services/jakarta.el.ExpressionFactory @@ -0,0 +1,16 @@ +# 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. + +org.apache.el.ExpressionFactoryImpl \ No newline at end of file diff --git a/res/META-INF/jasper-el.jar/web-fragment.xml b/res/META-INF/jasper-el.jar/web-fragment.xml new file mode 100644 index 0000000..1e7d6c5 --- /dev/null +++ b/res/META-INF/jasper-el.jar/web-fragment.xml @@ -0,0 +1,26 @@ + + + + org_apache_jasper_el + + \ No newline at end of file diff --git a/res/META-INF/jasper.jar/services/jakarta.servlet.ServletContainerInitializer b/res/META-INF/jasper.jar/services/jakarta.servlet.ServletContainerInitializer new file mode 100644 index 0000000..2eb8f6d --- /dev/null +++ b/res/META-INF/jasper.jar/services/jakarta.servlet.ServletContainerInitializer @@ -0,0 +1,16 @@ +# 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. + +org.apache.jasper.servlet.JasperInitializer \ No newline at end of file diff --git a/res/META-INF/jasper.jar/web-fragment.xml b/res/META-INF/jasper.jar/web-fragment.xml new file mode 100644 index 0000000..a255c86 --- /dev/null +++ b/res/META-INF/jasper.jar/web-fragment.xml @@ -0,0 +1,26 @@ + + + + org_apache_jasper + + \ No newline at end of file diff --git a/res/META-INF/jaspic-api.jar.manifest b/res/META-INF/jaspic-api.jar.manifest new file mode 100644 index 0000000..5cc43a1 --- /dev/null +++ b/res/META-INF/jaspic-api.jar.manifest @@ -0,0 +1,11 @@ +Manifest-version: 1.0 +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ + +Name: jakarta/security/auth/message +Specification-Title: Jakarta Authentication SPI for Containers +Specification-Version: @jaspic.spec.version@ +Specification-Vendor: Eclipse Foundation +Implementation-Title: jakarta.security.auth.message +Implementation-Version: @jaspic.spec.version@@jaspic.revision@ +Implementation-Vendor: Apache Software Foundation diff --git a/res/META-INF/jsp-api.jar.manifest b/res/META-INF/jsp-api.jar.manifest new file mode 100644 index 0000000..da97c06 --- /dev/null +++ b/res/META-INF/jsp-api.jar.manifest @@ -0,0 +1,11 @@ +Manifest-version: 1.0 +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ + +Name: jakarta/servlet/jsp/ +Specification-Title: Jakarta Server Pages +Specification-Version: @jsp.spec.version@ +Specification-Vendor: Eclipse Foundation +Implementation-Title: jakarta.servlet.jsp +Implementation-Version: @jsp.spec.version@@jsp.revision@ +Implementation-Vendor: Apache Software Foundation diff --git a/res/META-INF/servlet-api.jar.license b/res/META-INF/servlet-api.jar.license new file mode 100644 index 0000000..2b4876a --- /dev/null +++ b/res/META-INF/servlet-api.jar.license @@ -0,0 +1,858 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + + + +APACHE TOMCAT SUBCOMPONENTS: + +Apache Tomcat includes a number of subcomponents with separate copyright notices +and license terms. Your use of these subcomponents is subject to the terms and +conditions of the following licenses. + + +For the following XML Schemas for Java EE Deployment Descriptors: + - javaee_5.xsd + - javaee_web_services_1_2.xsd + - javaee_web_services_client_1_2.xsd + - javaee_6.xsd + - javaee_web_services_1_3.xsd + - javaee_web_services_client_1_3.xsd + - jsp_2_2.xsd + - web-app_3_0.xsd + - web-common_3_0.xsd + - web-fragment_3_0.xsd + - javaee_7.xsd + - javaee_web_services_1_4.xsd + - javaee_web_services_client_1_4.xsd + - jsp_2_3.xsd + - web-app_3_1.xsd + - web-common_3_1.xsd + - web-fragment_3_1.xsd + - javaee_8.xsd + - web-app_4_0.xsd + - web-common_4_0.xsd + - web-fragment_4_0.xsd + +COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 + +1. Definitions. + + 1.1. Contributor. means each individual or entity that creates or contributes + to the creation of Modifications. + + 1.2. Contributor Version. means the combination of the Original Software, + prior Modifications used by a Contributor (if any), and the + Modifications made by that particular Contributor. + + 1.3. Covered Software. means (a) the Original Software, or (b) Modifications, + or (c) the combination of files containing Original Software with files + containing Modifications, in each case including portions thereof. + + 1.4. Executable. means the Covered Software in any form other than Source + Code. + + 1.5. Initial Developer. means the individual or entity that first makes + Original Software available under this License. + + 1.6. Larger Work. means a work which combines Covered Software or portions + thereof with code not governed by the terms of this License. + + 1.7. License. means this document. + + 1.8. Licensable. means having the right to grant, to the maximum extent + possible, whether at the time of the initial grant or subsequently + acquired, any and all of the rights conveyed herein. + + 1.9. Modifications. means the Source Code and Executable form of any of the + following: + + A. Any file that results from an addition to, deletion from or + modification of the contents of a file containing Original Software + or previous Modifications; + + B. Any new file that contains any part of the Original Software or + previous Modification; or + + C. Any new file that is contributed or otherwise made available under + the terms of this License. + + 1.10. Original Software. means the Source Code and Executable form of + computer software code that is originally released under this License. + + 1.11. Patent Claims. means any patent claim(s), now owned or hereafter + acquired, including without limitation, method, process, and apparatus + claims, in any patent Licensable by grantor. + + 1.12. Source Code. means (a) the common form of computer software code in + which modifications are made and (b) associated documentation included + in or with such code. + + 1.13. You. (or .Your.) means an individual or a legal entity exercising + rights under, and complying with all of the terms of, this License. For + legal entities, .You. includes any entity which controls, is controlled + by, or is under common control with You. For purposes of this + definition, .control. means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + +2. License Grants. + + 2.1. The Initial Developer Grant. + + Conditioned upon Your compliance with Section 3.1 below and subject to + third party intellectual property claims, the Initial Developer hereby + grants You a world-wide, royalty-free, non-exclusive license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by Initial Developer, to use, reproduce, modify, display, + perform, sublicense and distribute the Original Software (or + portions thereof), with or without Modifications, and/or as part of + a Larger Work; and + + (b) under Patent Claims infringed by the making, using or selling of + Original Software, to make, have made, use, practice, sell, and + offer for sale, and/or otherwise dispose of the Original Software + (or portions thereof). + + (c) The licenses granted in Sections 2.1(a) and (b) are effective on the + date Initial Developer first distributes or otherwise makes the + Original Software available to a third party under the terms of this + License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is granted: + (1) for code that You delete from the Original Software, or (2) for + infringements caused by: (i) the modification of the Original + Software, or (ii) the combination of the Original Software with + other software or devices. + + 2.2. Contributor Grant. + + Conditioned upon Your compliance with Section 3.1 below and subject to third + party intellectual property claims, each Contributor hereby grants You a + world-wide, royalty-free, non-exclusive license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by Contributor to use, reproduce, modify, display, + perform, sublicense and distribute the Modifications created by such + Contributor (or portions thereof), either on an unmodified basis, + with other Modifications, as Covered Software and/or as part of a + Larger Work; and + + (b) under Patent Claims infringed by the making, using, or selling of + Modifications made by that Contributor either alone and/or in + combination with its Contributor Version (or portions of such + combination), to make, use, sell, offer for sale, have made, and/or + otherwise dispose of: (1) Modifications made by that Contributor (or + portions thereof); and (2) the combination of Modifications made by + that Contributor with its Contributor Version (or portions of such + combination). + + (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on + the date Contributor first distributes or otherwise makes the + Modifications available to a third party. + + (d) Notwithstanding Section 2.2(b) above, no patent license is granted: + (1) for any code that Contributor has deleted from the Contributor + Version; (2) for infringements caused by: (i) third party + modifications of Contributor Version, or (ii) the combination of + Modifications made by that Contributor with other software (except + as part of the Contributor Version) or other devices; or (3) under + Patent Claims infringed by Covered Software in the absence of + Modifications made by that Contributor. + +3. Distribution Obligations. + + 3.1. Availability of Source Code. + Any Covered Software that You distribute or otherwise make available in + Executable form must also be made available in Source Code form and that + Source Code form must be distributed only under the terms of this License. + You must include a copy of this License with every copy of the Source Code + form of the Covered Software You distribute or otherwise make available. + You must inform recipients of any such Covered Software in Executable form + as to how they can obtain such Covered Software in Source Code form in a + reasonable manner on or through a medium customarily used for software + exchange. + + 3.2. Modifications. + The Modifications that You create or to which You contribute are governed + by the terms of this License. You represent that You believe Your + Modifications are Your original creation(s) and/or You have sufficient + rights to grant the rights conveyed by this License. + + 3.3. Required Notices. + You must include a notice in each of Your Modifications that identifies + You as the Contributor of the Modification. You may not remove or alter + any copyright, patent or trademark notices contained within the Covered + Software, or any notices of licensing or any descriptive text giving + attribution to any Contributor or the Initial Developer. + + 3.4. Application of Additional Terms. + You may not offer or impose any terms on any Covered Software in Source + Code form that alters or restricts the applicable version of this License + or the recipients. rights hereunder. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability obligations to + one or more recipients of Covered Software. However, you may do so only on + Your own behalf, and not on behalf of the Initial Developer or any + Contributor. You must make it absolutely clear that any such warranty, + support, indemnity or liability obligation is offered by You alone, and + You hereby agree to indemnify the Initial Developer and every Contributor + for any liability incurred by the Initial Developer or such Contributor as + a result of warranty, support, indemnity or liability terms You offer. + + 3.5. Distribution of Executable Versions. + You may distribute the Executable form of the Covered Software under the + terms of this License or under the terms of a license of Your choice, + which may contain terms different from this License, provided that You are + in compliance with the terms of this License and that the license for the + Executable form does not attempt to limit or alter the recipient.s rights + in the Source Code form from the rights set forth in this License. If You + distribute the Covered Software in Executable form under a different + license, You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial Developer + or Contributor. You hereby agree to indemnify the Initial Developer and + every Contributor for any liability incurred by the Initial Developer or + such Contributor as a result of any such terms You offer. + + 3.6. Larger Works. + You may create a Larger Work by combining Covered Software with other code + not governed by the terms of this License and distribute the Larger Work + as a single product. In such a case, You must make sure the requirements + of this License are fulfilled for the Covered Software. + +4. Versions of the License. + + 4.1. New Versions. + Sun Microsystems, Inc. is the initial license steward and may publish + revised and/or new versions of this License from time to time. Each + version will be given a distinguishing version number. Except as provided + in Section 4.3, no one other than the license steward has the right to + modify this License. + + 4.2. Effect of New Versions. + You may always continue to use, distribute or otherwise make the Covered + Software available under the terms of the version of the License under + which You originally received the Covered Software. If the Initial + Developer includes a notice in the Original Software prohibiting it from + being distributed or otherwise made available under any subsequent version + of the License, You must distribute and make the Covered Software + available under the terms of the version of the License under which You + originally received the Covered Software. Otherwise, You may also choose + to use, distribute or otherwise make the Covered Software available under + the terms of any subsequent version of the License published by the + license steward. + + 4.3. Modified Versions. + When You are an Initial Developer and You want to create a new license for + Your Original Software, You may create and use a modified version of this + License if You: (a) rename the license and remove any references to the + name of the license steward (except to note that the license differs from + this License); and (b) otherwise make it clear that the license contains + terms which differ from this License. + +5. DISCLAIMER OF WARRANTY. + + COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN .AS IS. BASIS, WITHOUT + WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT + LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, + MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK + AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD + ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL + DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY + SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN + ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED + HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +6. TERMINATION. + + 6.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to + cure such breach within 30 days of becoming aware of the breach. + Provisions which, by their nature, must remain in effect beyond the + termination of this License shall survive. + + 6.2. If You assert a patent infringement claim (excluding declaratory + judgment actions) against Initial Developer or a Contributor (the + Initial Developer or Contributor against whom You assert such claim + is referred to as .Participant.) alleging that the Participant + Software (meaning the Contributor Version where the Participant is a + Contributor or the Original Software where the Participant is the + Initial Developer) directly or indirectly infringes any patent, then + any and all rights granted directly or indirectly to You by such + Participant, the Initial Developer (if the Initial Developer is not + the Participant) and all Contributors under Sections 2.1 and/or 2.2 + of this License shall, upon 60 days notice from Participant terminate + prospectively and automatically at the expiration of such 60 day + notice period, unless if within such 60 day period You withdraw Your + claim with respect to the Participant Software against such + Participant either unilaterally or pursuant to a written agreement + with Participant. + + 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end + user licenses that have been validly granted by You or any + distributor hereunder prior to termination (excluding licenses + granted to You by any distributor) shall survive termination. + +7. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING + NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY + OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF + ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, + INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT + LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, + COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR + LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF + SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR + DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY.S NEGLIGENCE TO THE EXTENT + APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS + EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +8. U.S. GOVERNMENT END USERS. + + The Covered Software is a .commercial item,. as that term is defined in 48 + C.F.R. 2.101 (Oct. 1995), consisting of .commercial computer software. (as + that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and commercial + computer software documentation. as such terms are used in 48 C.F.R. 12.212 + (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 + through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered + Software with only those rights set forth herein. This U.S. Government Rights + clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or + provision that addresses Government rights in computer software under this + License. + +9. MISCELLANEOUS. + + This License represents the complete agreement concerning subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. This License shall be governed by the law of the jurisdiction + specified in a notice contained within the Original Software (except to the + extent applicable law, if any, provides otherwise), excluding such + jurisdiction's conflict-of-law provisions. Any litigation relating to this + License shall be subject to the jurisdiction of the courts located in the + jurisdiction and venue specified in a notice contained within the Original + Software, with the losing party responsible for costs, including, without + limitation, court costs and reasonable attorneys. fees and expenses. The + application of the United Nations Convention on Contracts for the + International Sale of Goods is expressly excluded. Any law or regulation + which provides that the language of a contract shall be construed against + the drafter shall not apply to this License. You agree that You alone are + responsible for compliance with the United States export administration + regulations (and the export control laws and regulation of any other + countries) when You use, distribute or otherwise make available any Covered + Software. + +10. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is responsible + for claims and damages arising, directly or indirectly, out of its + utilization of rights under this License and You agree to work with Initial + Developer and Contributors to distribute such responsibility on an equitable + basis. Nothing herein is intended or shall be deemed to constitute any + admission of liability. + + NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION + LICENSE (CDDL) + + The code released under the CDDL shall be governed by the laws of the State + of California (excluding conflict-of-law provisions). Any litigation relating + to this License shall be subject to the jurisdiction of the Federal Courts of + the Northern District of California and the state courts of the State of + California, with venue lying in Santa Clara County, California. + + +For the following Jakarta EE Schemas: +- jakartaee_9.xsd +- jakartaee_10.xsd +- jakarta_web-services_2_0.xsd +- jakarta_web-services_client_2_0.xsd +- jsp_3_0.xsd +- jsp_3_1.xsd +- web-app_5_0.xsd +- web-app_6_0.xsd +- web-commonn_5_0.xsd +- web-commonn_6_0.xsd +- web-fragment_5_0.xsd +- web-fragment_6_0.xsd +- web-jsptaglibrary_3_0.xsd +- web-jsptaglibrary_3_1.xsd + +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/res/META-INF/servlet-api.jar.manifest b/res/META-INF/servlet-api.jar.manifest new file mode 100644 index 0000000..d61b4a3 --- /dev/null +++ b/res/META-INF/servlet-api.jar.manifest @@ -0,0 +1,11 @@ +Manifest-version: 1.0 +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ + +Name: jakarta/servlet/ +Specification-Title: Jakarta Servlet +Specification-Version: @servlet.spec.version@ +Specification-Vendor: Eclipse Foundation +Implementation-Title: jakarta.servlet +Implementation-Version: @servlet.spec.version@@servlet.revision@ +Implementation-Vendor: Apache Software Foundation diff --git a/res/META-INF/servlet-api.jar.notice b/res/META-INF/servlet-api.jar.notice new file mode 100644 index 0000000..cf779f3 --- /dev/null +++ b/res/META-INF/servlet-api.jar.notice @@ -0,0 +1,31 @@ +Apache Tomcat +Copyright 1999-@YEAR@ The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +The original XML Schemas for Java EE Deployment Descriptors: + - javaee_5.xsd + - javaee_web_services_1_2.xsd + - javaee_web_services_client_1_2.xsd + - javaee_6.xsd + - javaee_web_services_1_3.xsd + - javaee_web_services_client_1_3.xsd + - jsp_2_2.xsd + - web-app_3_0.xsd + - web-common_3_0.xsd + - web-fragment_3_0.xsd + - javaee_7.xsd + - javaee_web_services_1_4.xsd + - javaee_web_services_client_1_4.xsd + - jsp_2_3.xsd + - web-app_3_1.xsd + - web-common_3_1.xsd + - web-fragment_3_1.xsd + - javaee_8.xsd + - web-app_4_0.xsd + - web-common_4_0.xsd + - web-fragment_4_0.xsd + +may be obtained from: +http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/index.html diff --git a/res/META-INF/tomcat-websocket.jar/services/jakarta.servlet.ServletContainerInitializer b/res/META-INF/tomcat-websocket.jar/services/jakarta.servlet.ServletContainerInitializer new file mode 100644 index 0000000..c850c01 --- /dev/null +++ b/res/META-INF/tomcat-websocket.jar/services/jakarta.servlet.ServletContainerInitializer @@ -0,0 +1,16 @@ +# 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. + +org.apache.tomcat.websocket.server.WsSci \ No newline at end of file diff --git a/res/META-INF/tomcat-websocket.jar/services/jakarta.websocket.ContainerProvider b/res/META-INF/tomcat-websocket.jar/services/jakarta.websocket.ContainerProvider new file mode 100644 index 0000000..abdaee2 --- /dev/null +++ b/res/META-INF/tomcat-websocket.jar/services/jakarta.websocket.ContainerProvider @@ -0,0 +1,16 @@ +# 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. + +org.apache.tomcat.websocket.WsContainerProvider \ No newline at end of file diff --git a/res/META-INF/tomcat-websocket.jar/services/jakarta.websocket.server.ServerEndpointConfig$Configurator b/res/META-INF/tomcat-websocket.jar/services/jakarta.websocket.server.ServerEndpointConfig$Configurator new file mode 100644 index 0000000..6453734 --- /dev/null +++ b/res/META-INF/tomcat-websocket.jar/services/jakarta.websocket.server.ServerEndpointConfig$Configurator @@ -0,0 +1,16 @@ +# 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. + +org.apache.tomcat.websocket.server.DefaultServerEndpointConfigurator \ No newline at end of file diff --git a/res/META-INF/tomcat-websocket.jar/web-fragment.xml b/res/META-INF/tomcat-websocket.jar/web-fragment.xml new file mode 100644 index 0000000..378fe7d --- /dev/null +++ b/res/META-INF/tomcat-websocket.jar/web-fragment.xml @@ -0,0 +1,26 @@ + + + + org_apache_tomcat_websocket + + \ No newline at end of file diff --git a/res/META-INF/websocket-api.jar.manifest b/res/META-INF/websocket-api.jar.manifest new file mode 100644 index 0000000..08b95b2 --- /dev/null +++ b/res/META-INF/websocket-api.jar.manifest @@ -0,0 +1,11 @@ +Manifest-version: 1.0 +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ + +Name: jakarta/websocket/ +Specification-Title: Jakarta WebSocket +Specification-Version: @websocket.spec.version@@websocket.revision@ +Specification-Vendor: Eclipse Foundation +Implementation-Title: jakarta.websocket +Implementation-Version: @version@ +Implementation-Vendor: Apache Software Foundation diff --git a/res/META-INF/websocket-client-api.jar.manifest b/res/META-INF/websocket-client-api.jar.manifest new file mode 100644 index 0000000..233756e --- /dev/null +++ b/res/META-INF/websocket-client-api.jar.manifest @@ -0,0 +1,11 @@ +Manifest-version: 1.0 +X-Compile-Source-JDK: @source.jdk@ +X-Compile-Target-JDK: @target.jdk@ + +Name: jakarta/websocket/client +Specification-Title: Jakarta WebSocket Client +Specification-Version: @websocket.spec.version@@websocket.revision@ +Specification-Vendor: Eclipse Foundation +Implementation-Title: jakarta.websocket.client +Implementation-Version: @version@ +Implementation-Vendor: Apache Software Foundation diff --git a/res/bnd/annotations-api.jar.tmp.bnd b/res/bnd/annotations-api.jar.tmp.bnd new file mode 100644 index 0000000..0b0ff92 --- /dev/null +++ b/res/bnd/annotations-api.jar.tmp.bnd @@ -0,0 +1,43 @@ +# 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. + +-include: build-defaults.bnd, spec-defaults.bnd + +Bundle-Name: tomcat-annotations-api +Bundle-SymbolicName: org.apache.tomcat-annotations-api +Export-Package: \ + jakarta.annotation.*;version=${annotation.spec.version} + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaAnnotations;\ + version:Version=${annotation.spec.version};\ + uses:='${packages;NAMED;jakarta.annotation.*}' + +-namesection: jakarta/annotation*/;\ + Specification-Title=Jakarta Annotations;\ + Specification-Version=${annotation.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.annotation;\ + Implementation-Version=${annotation.spec.version}${annotation.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + jakarta.annotation;\ + access=32;\ + version=${annotation.spec.version} +-jpms-module-info-options: \ + jakarta.annotation;\ + substitute=annotations-api \ No newline at end of file diff --git a/res/bnd/build-defaults.bnd b/res/bnd/build-defaults.bnd new file mode 100644 index 0000000..b804587 --- /dev/null +++ b/res/bnd/build-defaults.bnd @@ -0,0 +1,39 @@ +# 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. + +Bundle-Version: ${version_cleanup;${version}} +Bundle-License: https://www.apache.org/licenses/LICENSE-2.0.txt + +Specification-Title: Apache Tomcat +Specification-Version: ${version.major.minor} +Specification-Vendor: Apache Software Foundation +Implementation-Title: Apache Tomcat +Implementation-Version: ${version} +Implementation-Vendor: Apache Software Foundation + +X-Compile-Source-JDK: ${compile.release} +X-Compile-Target-JDK: ${compile.release} + +-includeresource.notice: META-INF/NOTICE=${tomcat.output}/manifests/default.notice +-includeresource.license: META-INF/LICENSE=${tomcat.output}/manifests/default.license + +-noclassforname: true + +-reproducible: true +-noextraheaders: true + +-removeheaders: DSTAMP,TODAY,TSTAMP + +module.name: org.apache.${replace;${Bundle-Name};-;.} \ No newline at end of file diff --git a/res/bnd/catalina-ha.jar.tmp.bnd b/res/bnd/catalina-ha.jar.tmp.bnd new file mode 100644 index 0000000..278e6a8 --- /dev/null +++ b/res/bnd/catalina-ha.jar.tmp.bnd @@ -0,0 +1,35 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-catalina-ha +Bundle-SymbolicName: org.apache.tomcat-catalina-ha +Export-Package: \ + org.apache.catalina.ha,\ + org.apache.catalina.ha.authenticator,\ + org.apache.catalina.ha.backend,\ + org.apache.catalina.ha.context,\ + org.apache.catalina.ha.deploy,\ + org.apache.catalina.ha.session,\ + org.apache.catalina.ha.tcp + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=catalina-ha \ No newline at end of file diff --git a/res/bnd/catalina-ssi.jar.tmp.bnd b/res/bnd/catalina-ssi.jar.tmp.bnd new file mode 100644 index 0000000..a8b0c3b --- /dev/null +++ b/res/bnd/catalina-ssi.jar.tmp.bnd @@ -0,0 +1,29 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-ssi +Bundle-SymbolicName: org.apache.tomcat-ssi +Export-Package: \ + org.apache.catalina.ssi + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=catalina-ssi \ No newline at end of file diff --git a/res/bnd/catalina-storeconfig.jar.tmp.bnd b/res/bnd/catalina-storeconfig.jar.tmp.bnd new file mode 100644 index 0000000..c47666a --- /dev/null +++ b/res/bnd/catalina-storeconfig.jar.tmp.bnd @@ -0,0 +1,28 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-storeconfig +Bundle-SymbolicName: org.apache.tomcat-storeconfig +Export-Package: org.apache.catalina.storeconfig + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=catalina-storeconfig \ No newline at end of file diff --git a/res/bnd/catalina-tribes.jar.tmp.bnd b/res/bnd/catalina-tribes.jar.tmp.bnd new file mode 100644 index 0000000..5b45cd8 --- /dev/null +++ b/res/bnd/catalina-tribes.jar.tmp.bnd @@ -0,0 +1,41 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-tribes +Bundle-SymbolicName: org.apache.tomcat-tribes +Export-Package: \ + org.apache.catalina.tribes,\ + org.apache.catalina.tribes.group,\ + org.apache.catalina.tribes.group.interceptors,\ + org.apache.catalina.tribes.io,\ + org.apache.catalina.tribes.jmx,\ + org.apache.catalina.tribes.membership,\ + org.apache.catalina.tribes.tipis,\ + org.apache.catalina.tribes.transport,\ + org.apache.catalina.tribes.transport.nio,\ + org.apache.catalina.tribes.util + +-includepackage: \ + org.apache.catalina.tribes.membership.cloud + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=catalina-tribes \ No newline at end of file diff --git a/res/bnd/catalina.jar.tmp.bnd b/res/bnd/catalina.jar.tmp.bnd new file mode 100644 index 0000000..8720b37 --- /dev/null +++ b/res/bnd/catalina.jar.tmp.bnd @@ -0,0 +1,68 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-catalina +Bundle-SymbolicName: org.apache.tomcat-catalina +Export-Package: \ + org.apache.catalina.authenticator,\ + org.apache.catalina.authenticator.jaspic,\ + org.apache.catalina.connector,\ + org.apache.catalina.core,\ + org.apache.catalina.deploy,\ + org.apache.catalina.filters,\ + org.apache.catalina.loader,\ + org.apache.catalina.manager,\ + org.apache.catalina.manager.host,\ + org.apache.catalina.mapper,\ + org.apache.catalina.mbeans,\ + org.apache.catalina.realm,\ + org.apache.catalina.security,\ + org.apache.catalina.servlets,\ + org.apache.catalina.session,\ + org.apache.catalina.startup,\ + org.apache.catalina.users,\ + org.apache.catalina.util,\ + org.apache.catalina.valves,\ + org.apache.catalina.valves.rewrite,\ + org.apache.catalina.webresources,\ + org.apache.naming,\ + org.apache.naming.factory,\ + org.apache.naming.java,\ + org.apache.catalina.webresources.war,\ + org.apache.catalina.manager.util,\ + org.apache.catalina + +-includepackage: \ + org.apache.naming.factory.webservices + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version};\ + modules='\ + jakarta.ejb,\ + jakarta.mail,\ + jakarta.persistence,\ + jakarta.xml.ws,\ + java.xml.ws' +-jpms-module-info-options: \ + ${module.name};substitute=catalina,\ + jakarta.ejb;static=true,\ + jakarta.mail;static=true,\ + jakarta.persistence;static=true,\ + jakarta.xml.ws;static=true,\ + java.xml.ws;static=true \ No newline at end of file diff --git a/res/bnd/el-api.jar.tmp.bnd b/res/bnd/el-api.jar.tmp.bnd new file mode 100644 index 0000000..23451d4 --- /dev/null +++ b/res/bnd/el-api.jar.tmp.bnd @@ -0,0 +1,51 @@ +# 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. + +-include: build-defaults.bnd, spec-defaults.bnd + +Bundle-Name: tomcat-el-api +Bundle-SymbolicName: org.apache.tomcat-el-api +Export-Package: jakarta.el;version=${el.spec.version} + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaExpressionLanguage;\ + version:Version=${el.spec.version};\ + uses:='${packages;NAMED;jakarta.el.*}' + +Require-Capability: \ + osgi.extender;\ + filter:="(&(osgi.extender=osgi.serviceloader.processor)(version>=1.0.0)(!(version>=2.0.0)))",\ + osgi.serviceloader;\ + filter:="(osgi.serviceloader=jakarta.el.ExpressionFactory)";\ + osgi.serviceloader="jakarta.el.ExpressionFactory",\ + osgi.ee;\ + filter:="(&(osgi.ee=JavaSE)(version=1.8))" + +-namesection: jakarta/el*/;\ + Specification-Title=Jakarta Expression Language;\ + Specification-Version=${el.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.el;\ + Implementation-Version=${el.spec.version}${el.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + jakarta.el;\ + access=32;\ + version=${el.spec.version} +-jpms-module-info-options: \ + jakarta.el;\ + substitute=el-api \ No newline at end of file diff --git a/res/bnd/jasper-el.jar.tmp.bnd b/res/bnd/jasper-el.jar.tmp.bnd new file mode 100644 index 0000000..94536a4 --- /dev/null +++ b/res/bnd/jasper-el.jar.tmp.bnd @@ -0,0 +1,37 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-jasper-el +Bundle-SymbolicName: org.apache.tomcat-jasper-el +Export-Package: \ + org.apache.el,\ + org.apache.el.lang,\ + org.apache.el.parser + +-includepackage: \ + org.apache.el.stream,\ + org.apache.el.util + +-includeresource.meta-inf: /META-INF/=${tomcat.output}/manifests/jasper-el.jar/ + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=jasper-el \ No newline at end of file diff --git a/res/bnd/jasper.jar.tmp.bnd b/res/bnd/jasper.jar.tmp.bnd new file mode 100644 index 0000000..9747933 --- /dev/null +++ b/res/bnd/jasper.jar.tmp.bnd @@ -0,0 +1,45 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-jasper +Bundle-SymbolicName: org.apache.tomcat-jasper +Export-Package: \ + org.apache.jasper,\ + org.apache.jasper.compiler,\ + org.apache.jasper.compiler.tagplugin,\ + org.apache.jasper.el,\ + org.apache.jasper.optimizations,\ + org.apache.jasper.runtime,\ + org.apache.jasper.security,\ + org.apache.jasper.servlet,\ + org.apache.jasper.tagplugins.jstl,\ + org.apache.jasper.tagplugins.jstl.core,\ + org.apache.jasper.util + +-includepackage: \ + org.apache.jasper.resources + +-includeresource.meta-inf: /META-INF/=${tomcat.output}/manifests/jasper.jar/ + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version};\ + modules='ecj' +-jpms-module-info-options: \ + ${module.name};\ + substitute=jasper \ No newline at end of file diff --git a/res/bnd/jaspic-api.jar.tmp.bnd b/res/bnd/jaspic-api.jar.tmp.bnd new file mode 100644 index 0000000..088f536 --- /dev/null +++ b/res/bnd/jaspic-api.jar.tmp.bnd @@ -0,0 +1,43 @@ +# 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. + +-include: build-defaults.bnd, spec-defaults.bnd + +Bundle-Name: tomcat-jaspic-api +Bundle-SymbolicName: org.apache.tomcat-jaspic-api +Export-Package: \ + jakarta.security.auth.message.*;version=${jaspic.spec.version} + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaAuthentication;\ + version:Version=${jaspic.spec.version};\ + uses:='${packages;NAMED;jakarta.security.auth.message.*}' + +-namesection: jakarta/security/auth/message*/;\ + Specification-Title=Jakarta Authentication SPI for Containers;\ + Specification-Version=${jaspic.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.security.auth.message;\ + Implementation-Version=${jaspic.spec.version}${jaspic.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + jakarta.security.auth.message;\ + access=32;\ + version=${jaspic.spec.version} +-jpms-module-info-options: \ + jakarta.security.auth.message;\ + substitute=jaspic-api \ No newline at end of file diff --git a/res/bnd/jsp-api.jar.tmp.bnd b/res/bnd/jsp-api.jar.tmp.bnd new file mode 100644 index 0000000..8d7e5fc --- /dev/null +++ b/res/bnd/jsp-api.jar.tmp.bnd @@ -0,0 +1,43 @@ +# 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. + +-include: build-defaults.bnd, spec-defaults.bnd + +Bundle-Name: tomcat-jsp-api +Bundle-SymbolicName: org.apache.tomcat-jsp-api +Export-Package: \ + jakarta.servlet.jsp.*;version=${jsp.spec.version} + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaServerPages;\ + version:Version=${jsp.spec.version};\ + uses:='${packages;NAMED;jakarta.servlet.jsp.*}' + +-namesection: jakarta/servlet/jsp*/;\ + Specification-Title=Jakarta Server Pages;\ + Specification-Version=${jsp.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.servlet.jsp;\ + Implementation-Version=${jsp.spec.version}${jsp.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + jakarta.servlet.jsp;\ + access=32;\ + version=${jsp.spec.version} +-jpms-module-info-options: \ + jakarta.servlet.jsp;\ + substitute=jsp-api \ No newline at end of file diff --git a/res/bnd/servlet-api.jar.tmp.bnd b/res/bnd/servlet-api.jar.tmp.bnd new file mode 100644 index 0000000..09e542c --- /dev/null +++ b/res/bnd/servlet-api.jar.tmp.bnd @@ -0,0 +1,47 @@ +# 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. + +-include: build-defaults.bnd, spec-defaults.bnd + +Bundle-Name: tomcat-servlet-api +Bundle-SymbolicName: org.apache.tomcat-servlet-api +Export-Package: \ + !jakarta.servlet.jsp.*,\ + jakarta.servlet.*;version=${servlet.spec.version} + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaServlet;\ + version:Version=${servlet.spec.version};\ + uses:='${packages;NAMED;jakarta.servlet.*;NAMED;!jakarta.servlet.jsp.*}' + +-includeresource.notice2: META-INF/NOTICE=${tomcat.output}/manifests/servlet-api.jar.notice +-includeresource.license2: META-INF/LICENSE=${tomcat.output}/manifests/servlet-api.jar.license + +-namesection: jakarta/servlet*/;\ + Specification-Title=Jakarta Servlet;\ + Specification-Version=${servlet.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.servlet;\ + Implementation-Version=${servlet.spec.version}${servlet.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + jakarta.servlet;\ + access=32;\ + version=${servlet.spec.version} +-jpms-module-info-options: \ + jakarta.servlet;\ + substitute=servlet-api \ No newline at end of file diff --git a/res/bnd/spec-defaults.bnd b/res/bnd/spec-defaults.bnd new file mode 100644 index 0000000..1303ae2 --- /dev/null +++ b/res/bnd/spec-defaults.bnd @@ -0,0 +1,21 @@ +# 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. + +Specification-Title: +Specification-Version: +Specification-Vendor: +Implementation-Title: +Implementation-Version: +Implementation-Vendor: diff --git a/res/bnd/tomcat-api.jar.tmp.bnd b/res/bnd/tomcat-api.jar.tmp.bnd new file mode 100644 index 0000000..8a624b8 --- /dev/null +++ b/res/bnd/tomcat-api.jar.tmp.bnd @@ -0,0 +1,28 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-api +Bundle-SymbolicName: org.apache.tomcat-api +Export-Package: org.apache.tomcat + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/tomcat-coyote.jar.tmp.bnd b/res/bnd/tomcat-coyote.jar.tmp.bnd new file mode 100644 index 0000000..9900a31 --- /dev/null +++ b/res/bnd/tomcat-coyote.jar.tmp.bnd @@ -0,0 +1,55 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-coyote +Bundle-SymbolicName: org.apache.tomcat-coyote +Export-Package: \ + org.apache.coyote,\ + org.apache.coyote.ajp,\ + org.apache.coyote.http11,\ + org.apache.coyote.http11.filters,\ + org.apache.coyote.http11.upgrade,\ + org.apache.coyote.http2,\ + org.apache.tomcat.util.bcel.classfile,\ + org.apache.tomcat.util.http,\ + org.apache.tomcat.util.http.fileupload,\ + org.apache.tomcat.util.http.fileupload.disk,\ + org.apache.tomcat.util.http.fileupload.servlet,\ + org.apache.tomcat.util.http.fileupload.util,\ + org.apache.tomcat.util.http.parser,\ + org.apache.tomcat.util.log,\ + org.apache.tomcat.util.modeler,\ + org.apache.tomcat.util.modeler.modules,\ + org.apache.tomcat.util.net,\ + org.apache.tomcat.util.net.jsse,\ + org.apache.tomcat.util.net.openssl,\ + org.apache.tomcat.util.net.openssl.ciphers,\ + org.apache.tomcat.util.net.openssl.panama,\ + org.apache.tomcat.util.openssl + +-includepackage: \ + org.apache.tomcat.util.bcel,\ + org.apache.tomcat.util.http.fileupload.impl,\ + org.apache.tomcat.util.http.fileupload.util.mime + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/tomcat-dbcp.jar.tmp.bnd b/res/bnd/tomcat-dbcp.jar.tmp.bnd new file mode 100644 index 0000000..a6f973d --- /dev/null +++ b/res/bnd/tomcat-dbcp.jar.tmp.bnd @@ -0,0 +1,34 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-dbcp +Bundle-SymbolicName: org.apache.tomcat-dbcp +Export-Package: \ + org.apache.tomcat.dbcp.dbcp2.cpdsadapter,\ + org.apache.tomcat.dbcp.dbcp2.datasources,\ + org.apache.tomcat.dbcp.dbcp2.managed,\ + org.apache.tomcat.dbcp.dbcp2,\ + org.apache.tomcat.dbcp.pool2,\ + org.apache.tomcat.dbcp.pool2.impl + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/tomcat-embed-core.jar.tmp.bnd b/res/bnd/tomcat-embed-core.jar.tmp.bnd new file mode 100644 index 0000000..067a86f --- /dev/null +++ b/res/bnd/tomcat-embed-core.jar.tmp.bnd @@ -0,0 +1,144 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-embed-core +Bundle-SymbolicName: org.apache.tomcat-embed-core +Export-Package: \ + !jakarta.servlet.jsp.*,\ + jakarta.security.auth.message.*;version=${jaspic.spec.version},\ + jakarta.servlet.*;version=${servlet.spec.version},\ + org.apache.catalina,\ + org.apache.catalina.authenticator,\ + org.apache.catalina.authenticator.jaspic,\ + org.apache.catalina.connector,\ + org.apache.catalina.core,\ + org.apache.catalina.deploy,\ + org.apache.catalina.filters,\ + org.apache.catalina.loader,\ + org.apache.catalina.manager,\ + org.apache.catalina.manager.host,\ + org.apache.catalina.manager.util,\ + org.apache.catalina.mapper,\ + org.apache.catalina.mbeans,\ + org.apache.catalina.realm,\ + org.apache.catalina.security,\ + org.apache.catalina.servlets,\ + org.apache.catalina.session,\ + org.apache.catalina.startup,\ + org.apache.catalina.users,\ + org.apache.catalina.util,\ + org.apache.catalina.valves,\ + org.apache.catalina.valves.rewrite,\ + org.apache.catalina.webresources,\ + org.apache.catalina.webresources.war,\ + org.apache.coyote,\ + org.apache.coyote.ajp,\ + org.apache.coyote.http11,\ + org.apache.coyote.http11.filters,\ + org.apache.coyote.http11.upgrade,\ + org.apache.coyote.http2,\ + org.apache.juli,\ + org.apache.juli.logging,\ + org.apache.naming,\ + org.apache.naming.factory,\ + org.apache.naming.java,\ + org.apache.tomcat,\ + org.apache.tomcat.jni,\ + org.apache.tomcat.util,\ + org.apache.tomcat.util.bcel.classfile,\ + org.apache.tomcat.util.buf,\ + org.apache.tomcat.util.codec.binary,\ + org.apache.tomcat.util.collections,\ + org.apache.tomcat.util.compat,\ + org.apache.tomcat.util.descriptor,\ + org.apache.tomcat.util.descriptor.tagplugin,\ + org.apache.tomcat.util.descriptor.web,\ + org.apache.tomcat.util.digester,\ + org.apache.tomcat.util.file,\ + org.apache.tomcat.util.http,\ + org.apache.tomcat.util.http.fileupload,\ + org.apache.tomcat.util.http.fileupload.disk,\ + org.apache.tomcat.util.http.fileupload.impl,\ + org.apache.tomcat.util.http.fileupload.servlet,\ + org.apache.tomcat.util.http.fileupload.util,\ + org.apache.tomcat.util.http.parser,\ + org.apache.tomcat.util.log,\ + org.apache.tomcat.util.modeler,\ + org.apache.tomcat.util.modeler.modules,\ + org.apache.tomcat.util.net,\ + org.apache.tomcat.util.net.openssl,\ + org.apache.tomcat.util.net.openssl.ciphers,\ + org.apache.tomcat.util.net.openssl.panama,\ + org.apache.tomcat.util.openssl,\ + org.apache.tomcat.util.res,\ + org.apache.tomcat.util.scan,\ + org.apache.tomcat.util.security,\ + org.apache.tomcat.util.threads + +-includepackage: \ + org.apache.naming.factory.webservices,\ + org.apache.tomcat.util.bcel,\ + org.apache.tomcat.util.http.fileupload.util.mime,\ + org.apache.tomcat.util.json,\ + org.apache.tomcat.util.net.jsse + +-includeresource.notice2: META-INF/NOTICE=${tomcat.output}/manifests/servlet-api.jar.notice +-includeresource.license2: META-INF/LICENSE=${tomcat.output}/manifests/servlet-api.jar.license + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaAuthentication;\ + version:Version=${jaspic.spec.version};\ + uses:='${packages;NAMED;jakarta.security.auth.message.*}',\ + osgi.contract;\ + osgi.contract=JakartaServlet;\ + version:Version=${servlet.spec.version};\ + uses:='${packages;NAMED;jakarta.servlet.*;NAMED;!jakarta.servlet.jsp.*}' + +-namesection: \ + jakarta/security/auth/message*/;\ + Specification-Title=Jakarta Authentication SPI for Containers;\ + Specification-Version=${jaspic.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.security.auth.message;\ + Implementation-Version=${jaspic.spec.version}${jaspic.revision};\ + Implementation-Vendor=Apache Software Foundation,\ + jakarta/servlet*/;\ + Specification-Title=Jakarta Servlet;\ + Specification-Version=${servlet.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.servlet;\ + Implementation-Version=${servlet.spec.version}${servlet.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version};\ + modules='\ + jakarta.ejb,\ + jakarta.mail,\ + jakarta.persistence,\ + jakarta.xml.ws,\ + java.xml.ws' +-jpms-module-info-options: \ + ${module.name};substitute=tomcat-embed-core,\ + jakarta.ejb;static=true,\ + jakarta.mail;static=true,\ + jakarta.persistence;static=true,\ + jakarta.xml.ws;static=true,\ + java.xml.ws;static=true \ No newline at end of file diff --git a/res/bnd/tomcat-embed-el.jar.tmp.bnd b/res/bnd/tomcat-embed-el.jar.tmp.bnd new file mode 100644 index 0000000..86fd7a6 --- /dev/null +++ b/res/bnd/tomcat-embed-el.jar.tmp.bnd @@ -0,0 +1,61 @@ +# 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.q + +-include: build-defaults.bnd + +Bundle-Name: tomcat-embed-jasper-el +Bundle-SymbolicName: org.apache.tomcat-embed-jasper-el +Export-Package: \ + jakarta.el;version=${el.spec.version},\ + org.apache.el,\ + org.apache.el.lang,\ + org.apache.el.parser + +-includepackage: \ + org.apache.el.stream,\ + org.apache.el.util + +-includeresource.meta-inf: /META-INF/=${tomcat.output}/manifests/jasper-el.jar/ + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaExpressionLanguage;\ + version:Version=${el.spec.version};\ + uses:='${packages;NAMED;jakarta.el.*}' + +Require-Capability: \ + osgi.extender;\ + filter:="(&(osgi.extender=osgi.serviceloader.processor)(version>=1.0.0)(!(version>=2.0.0)))",\ + osgi.serviceloader;\ + filter:="(osgi.serviceloader=jakarta.el.ExpressionFactory)";\ + osgi.serviceloader="jakarta.el.ExpressionFactory",\ + osgi.ee;\ + filter:="(&(osgi.ee=JavaSE)(version=1.8))" + +-namesection: jakarta/el*/;\ + Specification-Title=Jakarta Expression Language;\ + Specification-Version=${el.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.annotation;\ + Implementation-Version=${el.spec.version}${el.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + org.apache.tomcat.embed.el;\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + org.apache.tomcat.embed.el;\ + substitute=tomcat-embed-el \ No newline at end of file diff --git a/res/bnd/tomcat-embed-jasper.jar.tmp.bnd b/res/bnd/tomcat-embed-jasper.jar.tmp.bnd new file mode 100644 index 0000000..ca0953e --- /dev/null +++ b/res/bnd/tomcat-embed-jasper.jar.tmp.bnd @@ -0,0 +1,60 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-embed-jasper +Bundle-SymbolicName: org.apache.tomcat-embed-jasper +Export-Package: \ + jakarta.servlet.jsp.*;version=${jsp.spec.version},\ + org.apache.jasper,\ + org.apache.jasper.compiler,\ + org.apache.jasper.compiler.tagplugin,\ + org.apache.jasper.el,\ + org.apache.jasper.optimizations,\ + org.apache.jasper.runtime,\ + org.apache.jasper.security,\ + org.apache.jasper.servlet,\ + org.apache.jasper.tagplugins.jstl,\ + org.apache.jasper.tagplugins.jstl.core,\ + org.apache.jasper.util,\ + org.apache.tomcat.util.descriptor.tld + +-includepackage: \ + org.apache.jasper.resources + +-includeresource.meta-inf: /META-INF/=${tomcat.output}/manifests/jasper.jar/ + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaServerPages;\ + version:Version=${jsp.spec.version};\ + uses:='${packages;NAMED;jakarta.servlet.jsp.*}' + +-namesection: jakarta/servlet/jsp*/;\ + Specification-Title=Jakarta Server Pages;\ + Specification-Version=${jsp.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.servlet.jsp;\ + Implementation-Version=${jsp.spec.version}${jsp.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/tomcat-embed-websocket.jar.tmp.bnd b/res/bnd/tomcat-embed-websocket.jar.tmp.bnd new file mode 100644 index 0000000..e3b938d --- /dev/null +++ b/res/bnd/tomcat-embed-websocket.jar.tmp.bnd @@ -0,0 +1,62 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-embed-websocket +Bundle-SymbolicName: org.apache.tomcat-embed-websocket +Export-Package: \ + jakarta.websocket.*;version=${websocket.spec.version},\ + org.apache.tomcat.websocket,\ + org.apache.tomcat.websocket.server + +-includepackage: \ + org.apache.tomcat.websocket.pojo + +-includeresource.meta-inf: /META-INF/=${tomcat.output}/manifests/tomcat-websocket.jar/ + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaWebSocket;\ + version:Version=${websocket.spec.version};\ + uses:='${packages;NAMED;jakarta.websocket.*}' + +Require-Capability: \ + osgi.extender;\ + filter:="(&(osgi.extender=osgi.serviceloader.processor)(version>=1.0.0)(!(version>=2.0.0)))",\ + osgi.serviceloader;\ + filter:="(osgi.serviceloader=jakarta.websocket.ContainerProvider)";\ + osgi.serviceloader="jakarta.websocket.ContainerProvider",\ + osgi.serviceloader;\ + filter:="(osgi.serviceloader=jakarta.websocket.server.ServerEndpointConfig$Configurator)";\ + osgi.serviceloader="jakarta.websocket.server.ServerEndpointConfig$Configurator",\ + osgi.ee;\ + filter:="(&(osgi.ee=JavaSE)(version=1.8))" + +-namesection: jakarta/websocket*/;\ + Specification-Title=Jakarta WebSocket;\ + Specification-Version=${websocket.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.websocket;\ + Implementation-Version=${websocket.spec.version}${websocket.revision};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/tomcat-jni.jar.tmp.bnd b/res/bnd/tomcat-jni.jar.tmp.bnd new file mode 100644 index 0000000..ec12bc8 --- /dev/null +++ b/res/bnd/tomcat-jni.jar.tmp.bnd @@ -0,0 +1,28 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-jni +Bundle-SymbolicName: org.apache.tomcat-jni +Export-Package: org.apache.tomcat.jni + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/tomcat-juli.jar.tmp.bnd b/res/bnd/tomcat-juli.jar.tmp.bnd new file mode 100644 index 0000000..14383c4 --- /dev/null +++ b/res/bnd/tomcat-juli.jar.tmp.bnd @@ -0,0 +1,30 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-juli +Bundle-SymbolicName: org.apache.tomcat-juli +Export-Package: \ + org.apache.juli,\ + org.apache.juli.logging + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/tomcat-util-scan.jar.tmp.bnd b/res/bnd/tomcat-util-scan.jar.tmp.bnd new file mode 100644 index 0000000..1a376c7 --- /dev/null +++ b/res/bnd/tomcat-util-scan.jar.tmp.bnd @@ -0,0 +1,34 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-util-scan +Bundle-SymbolicName: org.apache.tomcat-util-scan +Export-Package: \ + org.apache.tomcat.util.descriptor,\ + org.apache.tomcat.util.descriptor.tagplugin,\ + org.apache.tomcat.util.descriptor.tld,\ + org.apache.tomcat.util.descriptor.web,\ + org.apache.tomcat.util.digester,\ + org.apache.tomcat.util.scan + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/tomcat-util.jar.tmp.bnd b/res/bnd/tomcat-util.jar.tmp.bnd new file mode 100644 index 0000000..f687924 --- /dev/null +++ b/res/bnd/tomcat-util.jar.tmp.bnd @@ -0,0 +1,40 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-util +Bundle-SymbolicName: org.apache.tomcat-util +Export-Package: \ + org.apache.tomcat.util,\ + org.apache.tomcat.util.buf,\ + org.apache.tomcat.util.codec.binary,\ + org.apache.tomcat.util.collections,\ + org.apache.tomcat.util.compat,\ + org.apache.tomcat.util.file,\ + org.apache.tomcat.util.res,\ + org.apache.tomcat.util.security,\ + org.apache.tomcat.util.threads + +-includepackage: \ + org.apache.tomcat.util.json + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} diff --git a/res/bnd/tomcat-websocket.jar.tmp.bnd b/res/bnd/tomcat-websocket.jar.tmp.bnd new file mode 100644 index 0000000..5429d0c --- /dev/null +++ b/res/bnd/tomcat-websocket.jar.tmp.bnd @@ -0,0 +1,35 @@ +# 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. + +-include: build-defaults.bnd + +Bundle-Name: tomcat-websocket +Bundle-SymbolicName: org.apache.tomcat-websocket +Export-Package: \ + org.apache.tomcat.websocket,\ + org.apache.tomcat.websocket.server + +-includepackage: \ + org.apache.tomcat.websocket.pojo + +-includeresource.meta-inf: /META-INF/=${tomcat.output}/manifests/tomcat-websocket.jar/ + +-jpms-module-info: \ + ${module.name};\ + access=32;\ + version=${Bundle-Version} +-jpms-module-info-options: \ + ${module.name};\ + substitute=${Bundle-Name} \ No newline at end of file diff --git a/res/bnd/websocket-api.jar.tmp.bnd b/res/bnd/websocket-api.jar.tmp.bnd new file mode 100644 index 0000000..0184c99 --- /dev/null +++ b/res/bnd/websocket-api.jar.tmp.bnd @@ -0,0 +1,56 @@ +# 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. + +-include: build-defaults.bnd, spec-defaults.bnd + +Bundle-Name: tomcat-websocket-api +Bundle-SymbolicName: org.apache.tomcat-websocket-api +Export-Package: \ + jakarta.websocket.server.*;version=${websocket.spec.version} + +Provide-Capability: \ + osgi.contract;\ + osgi.contract=JakartaWebSocket;\ + version:Version=${websocket.spec.version};\ + uses:='${packages;NAMED;jakarta.websocket.*}' + +Require-Capability: \ + osgi.extender;\ + filter:="(&(osgi.extender=osgi.serviceloader.processor)(version>=1.0.0)(!(version>=2.0.0)))",\ + osgi.serviceloader;\ + filter:="(osgi.serviceloader=jakarta.websocket.server.ServerEndpointConfig$Configurator)";\ + osgi.serviceloader="jakarta.websocket.server.ServerEndpointConfig$Configurator",\ + osgi.ee;\ + filter:="(&(osgi.ee=JavaSE)(version=1.8))" + +-namesection: jakarta/websocket/server*/;\ + Specification-Title=Jakarta WebSocket;\ + Specification-Version=${websocket.spec.version}${websocket.revision};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.websocket.server;\ + Implementation-Version=${version};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + jakarta.websocket;\ + access=32;\ + modules=jakarta.websocket.client;\ + version=${websocket.spec.version} +-jpms-module-info-options: \ + jakarta.websocket;\ + substitute=websocket-api,\ + jakarta.websocket.client;\ + transitive=true;\ + static=false \ No newline at end of file diff --git a/res/bnd/websocket-client-api.jar.tmp.bnd b/res/bnd/websocket-client-api.jar.tmp.bnd new file mode 100644 index 0000000..492e495 --- /dev/null +++ b/res/bnd/websocket-client-api.jar.tmp.bnd @@ -0,0 +1,46 @@ +# 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. + +-include: build-defaults.bnd, spec-defaults.bnd + +Bundle-Name: tomcat-websocket-client-api +Bundle-SymbolicName: org.apache.tomcat-websocket-client-api +Export-Package: \ + jakarta.websocket;version=${websocket.spec.version} + +Require-Capability: \ + osgi.extender;\ + filter:="(&(osgi.extender=osgi.serviceloader.processor)(version>=1.0.0)(!(version>=2.0.0)))",\ + osgi.serviceloader;\ + filter:="(osgi.serviceloader=jakarta.websocket.ContainerProvider)";\ + osgi.serviceloader="jakarta.websocket.ContainerProvider",\ + osgi.ee;\ + filter:="(&(osgi.ee=JavaSE)(version=1.8))" + +-namesection: jakarta/websocket*/;\ + Specification-Title=Jakarta WebSocket Client;\ + Specification-Version=${websocket.spec.version}${websocket.revision};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.websocket;\ + Implementation-Version=${version};\ + Implementation-Vendor=Apache Software Foundation + +-jpms-module-info: \ + jakarta.websocket.client;\ + access=32;\ + version=${websocket.spec.version} +-jpms-module-info-options: \ + jakarta.websocket.client;\ + substitute=websocket-client-api \ No newline at end of file diff --git a/res/checkstyle/checkstyle.xml b/res/checkstyle/checkstyle.xml new file mode 100644 index 0000000..a6de189 --- /dev/null +++ b/res/checkstyle/checkstyle.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/checkstyle/header-al2.txt b/res/checkstyle/header-al2.txt new file mode 100644 index 0000000..7234793 --- /dev/null +++ b/res/checkstyle/header-al2.txt @@ -0,0 +1,19 @@ +^<\?xml.*>$ +^@echo off$ +^#! +^\W*$ +^(rem)?\W*Licensed to the Apache Software Foundation \(ASF\) under one or more$ +^(rem)?\W*contributor license agreements\. See the NOTICE file distributed with$ +^(rem)?\W*this work for additional information regarding copyright ownership\.$ +^(rem)?\W*The ASF licenses this file to You under the Apache License, Version 2\.0$ +^(rem)?\W*\(the "License"\); you may not use this file except in compliance with$ +^(rem)?\W*the License\. You may obtain a copy of the License at$ +^(rem)?\W*$ +^(rem)?\W*http://www.apache.org/licenses/LICENSE-2\.0$ +^(rem)?\W*$ +^(rem)?\W*Unless required by applicable law or agreed to in writing, software$ +^(rem)?\W*distributed under the License is distributed on an "AS IS" BASIS,$ +^(rem)?\W*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.$ +^(rem)?\W*See the License for the specific language governing permissions and$ +^(rem)?\W*limitations under the License\.$ +^(rem)?\W*$ \ No newline at end of file diff --git a/res/checkstyle/jakarta-checkstyle.xml b/res/checkstyle/jakarta-checkstyle.xml new file mode 100644 index 0000000..0f93885 --- /dev/null +++ b/res/checkstyle/jakarta-checkstyle.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/checkstyle/jakarta-import-control.xml b/res/checkstyle/jakarta-import-control.xml new file mode 100644 index 0000000..9034aa9 --- /dev/null +++ b/res/checkstyle/jakarta-import-control.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/checkstyle/org-checkstyle.xml b/res/checkstyle/org-checkstyle.xml new file mode 100644 index 0000000..a4f8ae0 --- /dev/null +++ b/res/checkstyle/org-checkstyle.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/checkstyle/org-import-control.xml b/res/checkstyle/org-import-control.xml new file mode 100644 index 0000000..b6d8773 --- /dev/null +++ b/res/checkstyle/org-import-control.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/checkstyle/test-checkstyle.xml b/res/checkstyle/test-checkstyle.xml new file mode 100644 index 0000000..6b2ef6b --- /dev/null +++ b/res/checkstyle/test-checkstyle.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/deployer/build.xml b/res/deployer/build.xml new file mode 100644 index 0000000..2ff91fe --- /dev/null +++ b/res/deployer/build.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/graal/README.md b/res/graal/README.md new file mode 100644 index 0000000..717c761 --- /dev/null +++ b/res/graal/README.md @@ -0,0 +1,40 @@ + + +Introduction +=== + +GraalVM is a polyglot virtual machine. In addition to that, it supports Ahead of Time, +AOT, compilation of Java applications into native executable files via its +[native-image`](https://github.com/oracle/graal/tree/master/substratevm) compiler. + +Reflection Directives +=== + +This directory contains directives to the compiler on what classes use reflection. +These are currently stored in a file called `tomcat-reflection.json` in the `META-INF/native-image/groupId/artifactId` +location. + +This directory also contains resource directives, so that resource files normally included in a JAR file +also get compiled into the executable image. +These are currently stored in a file called `tomcat-resource.json` in the `META-INF/native-image/groupId/artifactId` +location. + diff --git a/res/graal/build-tomcat-native-image.sh b/res/graal/build-tomcat-native-image.sh new file mode 100755 index 0000000..81370ee --- /dev/null +++ b/res/graal/build-tomcat-native-image.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# 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. + +set -e + +CURDIR=`pwd` + +# resolve links - $0 may be a softlink +PRG="$0" +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +# directory of this script +PRGDIR=`dirname "$PRG"` + +cd $PRGDIR/../.. +ant clean && ant && ant embed && ant test-compile + +mkdir -p output/graal +cd output/testclasses +jar cvfM ../graal/tomcat-embedded-sample.jar org/apache/catalina/startup/EmbeddedTomcat*class + +cd ../graal + +native-image \ +--verbose \ +--no-server \ +-H:EnableURLProtocols=http \ +--report-unsupported-elements-at-runtime \ +--initialize-at-run-time=org.apache,jakarta.servlet \ +-H:TraceClassInitialization=org.* \ +-H:+PrintClassInitialization \ +-H:+PrintAnalysisCallTree \ +-H:Name=tc-graal-image \ +-H:+ReportExceptionStackTraces \ +--allow-incomplete-classpath \ +--no-fallback \ +-cp ../embed/tomcat-embed-programmatic.jar:tomcat-embedded-sample.jar \ +org.apache.catalina.startup.EmbeddedTomcat + +cd $CURDIR \ No newline at end of file diff --git a/res/graal/graal-measure.sh b/res/graal/graal-measure.sh new file mode 100755 index 0000000..2f2094e --- /dev/null +++ b/res/graal/graal-measure.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# 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. + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +CURDIR=`pwd` + +# resolve links - $0 may be a softlink +PRG="$0" +while [ -h "$PRG" ]; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`/"$link" + fi +done + +# directory of this script +PRGDIR=`dirname "$PRG"` + +EXECUTABLE=${PRGDIR}/../../output/graal/tc-graal-image + +./${EXECUTABLE} "$@" 2>&1 & +PID=$! +sleep 2 +RESPONSE=`curl -s localhost:8080/` +if [[ "$RESPONSE" == 'OK: http://localhost:8080/[1]' ]]; then + printf "\n${GREEN}SUCCESS${NC}: the servlet is working\n" +else + printf "\n${RED}FAILURE${NC}: the HTTP response of the application does not contain the expected value\n" +fi + +RSS=`ps -o rss ${PID} | tail -n1` +RSS=`bc <<< "scale=1; ${RSS}/1024"` +echo "RSS memory: ${RSS}M" +SIZE=`wc -c <"${EXECUTABLE}"`/1024 +SIZE=`bc <<< "scale=1; ${SIZE}/1024"` +echo "Image size: ${SIZE}M" + +kill -9 $PID \ No newline at end of file diff --git a/res/graal/tomcat-embed-core/native-image/native-image.properties b/res/graal/tomcat-embed-core/native-image/native-image.properties new file mode 100644 index 0000000..8944701 --- /dev/null +++ b/res/graal/tomcat-embed-core/native-image/native-image.properties @@ -0,0 +1,16 @@ +# 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. + +Args = -H:ReflectionConfigurationResources=${.}/tomcat-reflection.json -H:ResourceConfigurationResources=${.}/tomcat-resource.json diff --git a/res/graal/tomcat-embed-core/native-image/tomcat-jni.json b/res/graal/tomcat-embed-core/native-image/tomcat-jni.json new file mode 100644 index 0000000..4479fb8 --- /dev/null +++ b/res/graal/tomcat-embed-core/native-image/tomcat-jni.json @@ -0,0 +1,12 @@ +[ + { "name":"org.apache.tomcat.jni.Buffer", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true}, + { "name":"org.apache.tomcat.jni.FileInfo", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true}, + { "name":"org.apache.tomcat.jni.Library", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true}, + { "name":"org.apache.tomcat.jni.Pool", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true}, + { "name":"org.apache.tomcat.jni.Sockaddr", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true}, + { "name":"org.apache.tomcat.jni.SSL", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true}, + { "name":"org.apache.tomcat.jni.SSLConf", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true}, + { "name":"org.apache.tomcat.jni.SSLContext", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true}, + { "name":"java.lang.String", "allDeclaredConstructors" : true,"allPublicConstructors" : true,"allDeclaredMethods" : true,"allPublicMethods" : true,"allDeclaredClasses" : true,"allPublicClasses" : true,"allPublicFields" : true,"allDeclaredFields" : true} + +] diff --git a/res/graal/tomcat-embed-core/native-image/tomcat-reflection.json b/res/graal/tomcat-embed-core/native-image/tomcat-reflection.json new file mode 100644 index 0000000..31f01ed --- /dev/null +++ b/res/graal/tomcat-embed-core/native-image/tomcat-reflection.json @@ -0,0 +1,66 @@ +[ +{ "name":"jakarta.servlet.http.HttpServlet" }, +{ "name":"org.apache.catalina.AccessLog" }, +{ "name":"org.apache.catalina.AsyncDispatcher" }, +{ "name":"org.apache.catalina.Authenticator" }, +{ "name":"org.apache.catalina.Cluster" }, +{ "name":"org.apache.catalina.Container" }, +{ "name":"org.apache.catalina.Contained" }, +{ "name":"org.apache.catalina.Context" }, +{ "name":"org.apache.catalina.CredentialHandler" }, +{ "name":"org.apache.catalina.DistributedManager" }, +{ "name":"org.apache.catalina.Engine" }, +{ "name":"org.apache.catalina.Executor" }, +{ "name":"org.apache.catalina.Group" }, +{ "name":"org.apache.catalina.Host" }, +{ "name":"org.apache.catalina.JmxEnabled" }, +{ "name":"org.apache.catalina.Lifecycle" }, +{ "name":"org.apache.catalina.LifecycleListener" }, +{ "name":"org.apache.catalina.Loader" }, +{ "name":"org.apache.catalina.Manager" }, +{ "name":"org.apache.catalina.Pipeline" }, +{ "name":"org.apache.catalina.Realm" }, +{ "name":"org.apache.catalina.Role" }, +{ "name":"org.apache.catalina.Server" }, +{ "name":"org.apache.catalina.Service" }, +{ "name":"org.apache.catalina.Session" }, +{ "name":"org.apache.catalina.SessionIdGenerator" }, +{ "name":"org.apache.catalina.SessionListener" }, +{ "name":"org.apache.catalina.Store" }, +{ "name":"org.apache.catalina.StoreManager" }, +{ "name":"org.apache.catalina.User" }, +{ "name":"org.apache.catalina.UserDatabase" }, +{ "name":"org.apache.catalina.Valve" }, +{ "name":"org.apache.catalina.WebResource" }, +{ "name":"org.apache.catalina.WebResourceRoot" }, +{ "name":"org.apache.catalina.WebResourceSet" }, +{ "name":"org.apache.catalina.Wrapper" }, +{ "name":"org.apache.catalina.authenticator.jaspic.AuthConfigFactoryImpl", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.catalina.core.StandardContext", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.catalina.loader.ParallelWebappClassLoader", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true}, +{ "name":"org.apache.catalina.servlets.DefaultServlet", "allDeclaredFields":true, "allDeclaredMethods":true }, +{ "name":"org.apache.catalina.valves.ErrorReportValve", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.coyote.AbstractProtocol", "methods": [{"name": "getLocalPort","parameterTypes": []},{"name": "getPort","parameterTypes": []}, {"name": "setPortOffset","parameterTypes": ["int"]}, {"name": "getPortOffset","parameterTypes": []}, {"name": "setPort","parameterTypes": ["int"]}]}, +{ "name":"org.apache.coyote.http11.AbstractHttp11Protocol", "methods": [{"name": "setSecure","parameterTypes": ["boolean"]}, {"name": "setMaxSavePostSize","parameterTypes": ["int"]}]}, +{ "name":"org.apache.coyote.http11.Http11NioProtocol", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.coyote.http11.Http11Nio2Protocol", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.coyote.UpgradeProtocol" }, +{ "name":"org.apache.coyote.Request", "methods":[{"name":"","parameterTypes":[] }] }, +{ "name":"org.apache.coyote.RequestGroupInfo", "methods":[{"name":"","parameterTypes":[] }] }, +{ "name":"org.apache.coyote.RequestInfo", "allPublicMethods":true }, +{ "name":"org.apache.coyote.Response", "methods":[{"name":"","parameterTypes":[] }]}, +{ "name":"org.apache.juli.AsyncFileHandler", "methods":[{"name":"","parameterTypes":[] }] }, +{ "name":"org.apache.naming.factory.EjbFactory", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.naming.factory.ResourceEnvFactory", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.naming.factory.ResourceFactory", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.naming.factory.TransactionFactory", "methods" : [{"name": "","parameterTypes":[]}] }, +{ "name":"org.apache.tomcat.util.net.AbstractEndpoint", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true}, +{ "name":"org.apache.tomcat.util.net.AbstractJsseEndpoint", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true}, +{ "name":"org.apache.tomcat.util.net.NioEndpoint", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true}, +{ "name":"org.apache.tomcat.util.buf.StringCache", "allPublicMethods":true }, +{ "name":"org.apache.tomcat.util.net.openssl.OpenSSLImplementation", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true}, +{ "name":"org.apache.tomcat.util.net.SSLHostConfig", "allPublicMethods":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ "name":"org.apache.tomcat.util.net.SSLHostConfigCertificate", "allPublicMethods":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ "name":"org.apache.tomcat.util.net.SocketProperties", "allPublicMethods":true }, +{ "name":"org.apache.tomcat.util.net.openssl.OpenSSLConf", "methods":[{"name":"","parameterTypes":[] }] } +] diff --git a/res/graal/tomcat-embed-core/native-image/tomcat-resource.json b/res/graal/tomcat-embed-core/native-image/tomcat-resource.json new file mode 100644 index 0000000..f10d8f2 --- /dev/null +++ b/res/graal/tomcat-embed-core/native-image/tomcat-resource.json @@ -0,0 +1,57 @@ +{ + "bundles":[ + {"name":"jakarta.servlet.LocalStrings"}, + {"name":"jakarta.servlet.http.LocalStrings"}, + {"name":"org.apache.catalina.authenticator.LocalStrings"}, + {"name":"org.apache.catalina.authenticator.jaspic.LocalStrings"}, + {"name":"org.apache.catalina.connector.LocalStrings"}, + {"name":"org.apache.catalina.core.LocalStrings"}, + {"name":"org.apache.catalina.deploy.LocalStrings"}, + {"name":"org.apache.catalina.filters.LocalStrings"}, + {"name":"org.apache.catalina.loader.LocalStrings"}, + {"name":"org.apache.catalina.manager.LocalStrings"}, + {"name":"org.apache.catalina.manager.host.LocalStrings"}, + {"name":"org.apache.catalina.mapper.LocalStrings"}, + {"name":"org.apache.catalina.mbeans.LocalStrings"}, + {"name":"org.apache.catalina.realm.LocalStrings"}, + {"name":"org.apache.catalina.security.LocalStrings"}, + {"name":"org.apache.catalina.servlets.LocalStrings"}, + {"name":"org.apache.catalina.session.LocalStrings"}, + {"name":"org.apache.catalina.startup.LocalStrings"}, + {"name":"org.apache.catalina.users.LocalStrings"}, + {"name":"org.apache.catalina.util.LocalStrings"}, + {"name":"org.apache.catalina.valves.LocalStrings"}, + {"name":"org.apache.catalina.valves.rewrite.LocalStrings"}, + {"name":"org.apache.catalina.webresources.LocalStrings"}, + {"name":"org.apache.coyote.LocalStrings"}, + {"name":"org.apache.coyote.http11.LocalStrings"}, + {"name":"org.apache.coyote.http11.filters.LocalStrings"}, + {"name":"org.apache.coyote.http11.upgrade.LocalStrings"}, + {"name":"org.apache.coyote.http2.LocalStrings"}, + {"name":"org.apache.naming.LocalStrings"}, + {"name":"org.apache.naming.factory.LocalStrings"}, + {"name":"org.apache.naming.factory.webservices.LocalStrings"}, + {"name":"org.apache.tomcat.util.LocalStrings"}, + {"name":"org.apache.tomcat.util.buf.LocalStrings"}, + {"name":"org.apache.tomcat.util.codec.binary.LocalStrings"}, + {"name":"org.apache.tomcat.util.compat.LocalStrings"}, + {"name":"org.apache.tomcat.util.descriptor.LocalStrings"}, + {"name":"org.apache.tomcat.util.descriptor.web.LocalStrings"}, + {"name":"org.apache.tomcat.util.digester.LocalStrings"}, + {"name":"org.apache.tomcat.util.http.LocalStrings"}, + {"name":"org.apache.tomcat.util.http.parser.LocalStrings"}, + {"name":"org.apache.tomcat.util.json.LocalStrings"}, + {"name":"org.apache.tomcat.util.modeler.LocalStrings"}, + {"name":"org.apache.tomcat.util.net.LocalStrings"}, + {"name":"org.apache.tomcat.util.scan.LocalStrings"}, + {"name":"org.apache.tomcat.util.security.LocalStrings"}, + {"name":"org.apache.tomcat.util.threads.LocalStrings"} + ], + "resources":[ + {"pattern":"^org/apache/tomcat/.*mbeans-descriptors\\.xml$"}, + {"pattern":"^org/apache/catalina/.*mbeans-descriptors\\.xml$"}, + {"pattern":"^org/apache/coyote/.*mbeans-descriptors\\.xml$"}, + {"pattern":"^org/apache/catalina/.*\\.properties$"}, + {"pattern":"^org/apache/tomcat/.*\\.dtd$"} + ] +} diff --git a/res/graal/tomcat-embed-el/native-image/native-image.properties b/res/graal/tomcat-embed-el/native-image/native-image.properties new file mode 100644 index 0000000..29b501f --- /dev/null +++ b/res/graal/tomcat-embed-el/native-image/native-image.properties @@ -0,0 +1,16 @@ +# 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. + +Args = -H:ReflectionConfigurationResources=${.}/tomcat-reflection.json -H:ResourceConfigurationResources=${.}/tomcat-resource.json \ No newline at end of file diff --git a/res/graal/tomcat-embed-el/native-image/tomcat-reflection.json b/res/graal/tomcat-embed-el/native-image/tomcat-reflection.json new file mode 100644 index 0000000..24f67f2 --- /dev/null +++ b/res/graal/tomcat-embed-el/native-image/tomcat-reflection.json @@ -0,0 +1,3 @@ +[ +{ "name":"org.apache.el.ExpressionFactoryImpl", "methods" : [{"name":"","parameterTypes":[]}] } +] diff --git a/res/graal/tomcat-embed-el/native-image/tomcat-resource.json b/res/graal/tomcat-embed-el/native-image/tomcat-resource.json new file mode 100644 index 0000000..e942924 --- /dev/null +++ b/res/graal/tomcat-embed-el/native-image/tomcat-resource.json @@ -0,0 +1,6 @@ +{ + "bundles":[ + {"name":"jakarta.el.LocalStrings"}, + {"name":"org.apache.el.LocalStrings"} + ] +} diff --git a/res/graal/tomcat-embed-jasper/native-image/native-image.properties b/res/graal/tomcat-embed-jasper/native-image/native-image.properties new file mode 100644 index 0000000..29b501f --- /dev/null +++ b/res/graal/tomcat-embed-jasper/native-image/native-image.properties @@ -0,0 +1,16 @@ +# 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. + +Args = -H:ReflectionConfigurationResources=${.}/tomcat-reflection.json -H:ResourceConfigurationResources=${.}/tomcat-resource.json \ No newline at end of file diff --git a/res/graal/tomcat-embed-jasper/native-image/tomcat-reflection.json b/res/graal/tomcat-embed-jasper/native-image/tomcat-reflection.json new file mode 100644 index 0000000..0d4f101 --- /dev/null +++ b/res/graal/tomcat-embed-jasper/native-image/tomcat-reflection.json @@ -0,0 +1,2 @@ +[ +] diff --git a/res/graal/tomcat-embed-jasper/native-image/tomcat-resource.json b/res/graal/tomcat-embed-jasper/native-image/tomcat-resource.json new file mode 100644 index 0000000..fa339f8 --- /dev/null +++ b/res/graal/tomcat-embed-jasper/native-image/tomcat-resource.json @@ -0,0 +1,9 @@ +{ + "bundles":[ + {"name":"org.apache.jasper.resources.LocalStrings"} + ], + "resources":[ + {"pattern":"^org/apache/jasper/.*mbeans-descriptors\\.xml$"}, + {"pattern":"^jakarta/servlet/jsp/resources/.*"} + ] +} diff --git a/res/graal/tomcat-embed-programmatic/native-image/native-image.properties b/res/graal/tomcat-embed-programmatic/native-image/native-image.properties new file mode 100644 index 0000000..29b501f --- /dev/null +++ b/res/graal/tomcat-embed-programmatic/native-image/native-image.properties @@ -0,0 +1,16 @@ +# 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. + +Args = -H:ReflectionConfigurationResources=${.}/tomcat-reflection.json -H:ResourceConfigurationResources=${.}/tomcat-resource.json \ No newline at end of file diff --git a/res/graal/tomcat-embed-programmatic/native-image/tomcat-reflection.json b/res/graal/tomcat-embed-programmatic/native-image/tomcat-reflection.json new file mode 100644 index 0000000..0d4f101 --- /dev/null +++ b/res/graal/tomcat-embed-programmatic/native-image/tomcat-reflection.json @@ -0,0 +1,2 @@ +[ +] diff --git a/res/graal/tomcat-embed-programmatic/native-image/tomcat-resource.json b/res/graal/tomcat-embed-programmatic/native-image/tomcat-resource.json new file mode 100644 index 0000000..ea7243d --- /dev/null +++ b/res/graal/tomcat-embed-programmatic/native-image/tomcat-resource.json @@ -0,0 +1,42 @@ +{ + "bundles":[ + {"name":"jakarta.servlet.LocalStrings"}, + {"name":"jakarta.servlet.http.LocalStrings"}, + {"name":"org.apache.catalina.authenticator.LocalStrings"}, + {"name":"org.apache.catalina.authenticator.jaspic.LocalStrings"}, + {"name":"org.apache.catalina.connector.LocalStrings"}, + {"name":"org.apache.catalina.core.LocalStrings"}, + {"name":"org.apache.catalina.deploy.LocalStrings"}, + {"name":"org.apache.catalina.loader.LocalStrings"}, + {"name":"org.apache.catalina.mapper.LocalStrings"}, + {"name":"org.apache.catalina.realm.LocalStrings"}, + {"name":"org.apache.catalina.security.LocalStrings"}, + {"name":"org.apache.catalina.session.LocalStrings"}, + {"name":"org.apache.catalina.startup.LocalStrings"}, + {"name":"org.apache.catalina.util.LocalStrings"}, + {"name":"org.apache.catalina.valves.LocalStrings"}, + {"name":"org.apache.catalina.webresources.LocalStrings"}, + {"name":"org.apache.coyote.LocalStrings"}, + {"name":"org.apache.coyote.http11.LocalStrings"}, + {"name":"org.apache.coyote.http11.filters.LocalStrings"}, + {"name":"org.apache.coyote.http11.upgrade.LocalStrings"}, + {"name":"org.apache.coyote.http2.LocalStrings"}, + {"name":"org.apache.naming.LocalStrings"}, + {"name":"org.apache.naming.factory.LocalStrings"}, + {"name":"org.apache.tomcat.util.LocalStrings"}, + {"name":"org.apache.tomcat.util.buf.LocalStrings"}, + {"name":"org.apache.tomcat.util.codec.binary.LocalStrings"}, + {"name":"org.apache.tomcat.util.compat.LocalStrings"}, + {"name":"org.apache.tomcat.util.http.LocalStrings"}, + {"name":"org.apache.tomcat.util.http.parser.LocalStrings"}, + {"name":"org.apache.tomcat.util.json.LocalStrings"}, + {"name":"org.apache.tomcat.util.modeler.LocalStrings"}, + {"name":"org.apache.tomcat.util.net.LocalStrings"}, + {"name":"org.apache.tomcat.util.scan.LocalStrings"}, + {"name":"org.apache.tomcat.util.security.LocalStrings"}, + {"name":"org.apache.tomcat.util.threads.LocalStrings"} + ], + "resources":[ + {"pattern":"^org/apache/catalina/.*\\.properties$"} + ] +} diff --git a/res/graal/tomcat-embed-websocket/native-image/native-image.properties b/res/graal/tomcat-embed-websocket/native-image/native-image.properties new file mode 100644 index 0000000..29b501f --- /dev/null +++ b/res/graal/tomcat-embed-websocket/native-image/native-image.properties @@ -0,0 +1,16 @@ +# 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. + +Args = -H:ReflectionConfigurationResources=${.}/tomcat-reflection.json -H:ResourceConfigurationResources=${.}/tomcat-resource.json \ No newline at end of file diff --git a/res/graal/tomcat-embed-websocket/native-image/tomcat-reflection.json b/res/graal/tomcat-embed-websocket/native-image/tomcat-reflection.json new file mode 100644 index 0000000..3df91d6 --- /dev/null +++ b/res/graal/tomcat-embed-websocket/native-image/tomcat-reflection.json @@ -0,0 +1,8 @@ +[ +{ "name":"org.apache.tomcat.websocket.server.WsHttpUpgradeHandler", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true }, +{ "name":"org.apache.tomcat.websocket.pojo.PojoEndpointBase", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true }, +{ "name":"org.apache.tomcat.websocket.pojo.PojoEndpointServer", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true }, +{ "name":"org.apache.tomcat.websocket.server.WsContextListener", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true }, +{ "name":"org.apache.tomcat.websocket.server.WsFilter", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true }, +{ "name":"org.apache.tomcat.websocket.server.WsHttpUpgradeHandler", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true } +] diff --git a/res/graal/tomcat-embed-websocket/native-image/tomcat-resource.json b/res/graal/tomcat-embed-websocket/native-image/tomcat-resource.json new file mode 100644 index 0000000..b158949 --- /dev/null +++ b/res/graal/tomcat-embed-websocket/native-image/tomcat-resource.json @@ -0,0 +1,7 @@ +{ + "bundles":[ + {"name":"org.apache.tomcat.websocket.LocalStrings"}, + {"name":"org.apache.tomcat.websocket.pojo.LocalStrings"}, + {"name":"org.apache.tomcat.websocket.server.LocalStrings"} + ] +} diff --git a/res/ide-support/coding-style.txt b/res/ide-support/coding-style.txt new file mode 100644 index 0000000..0348f40 --- /dev/null +++ b/res/ide-support/coding-style.txt @@ -0,0 +1,30 @@ +================================================================================ + 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. +================================================================================ + +Best practice, fit in with the style that is already there. However, things have +evolved over time so there are some variations across the project; use your +judgement. + +There is a validate build target that performs checks using tools like checkstyle. +This is off by default but can be enabled by setting "execute.validate=true" in +your build.properties file. + +This directory contains support files for various IDEs that can help configure +your development environment. + +Some guidelines: +* Do not rely on Java's autoboxing - perform conversions explicitly diff --git a/res/ide-support/eclipse/clean-up-asf-tomcat.xml b/res/ide-support/eclipse/clean-up-asf-tomcat.xml new file mode 100644 index 0000000..b250c87 --- /dev/null +++ b/res/ide-support/eclipse/clean-up-asf-tomcat.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ide-support/eclipse/eclipse.classpath b/res/ide-support/eclipse/eclipse.classpath new file mode 100644 index 0000000..fcad3f3 --- /dev/null +++ b/res/ide-support/eclipse/eclipse.classpath @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ide-support/eclipse/eclipse.project b/res/ide-support/eclipse/eclipse.project new file mode 100644 index 0000000..8139e41 --- /dev/null +++ b/res/ide-support/eclipse/eclipse.project @@ -0,0 +1,33 @@ + + + + tomcat-10.1.x + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/res/ide-support/eclipse/formatting-asf-tomcat.xml b/res/ide-support/eclipse/formatting-asf-tomcat.xml new file mode 100644 index 0000000..c58449c --- /dev/null +++ b/res/ide-support/eclipse/formatting-asf-tomcat.xml @@ -0,0 +1,416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ide-support/eclipse/java-compiler-errors-warnings.txt b/res/ide-support/eclipse/java-compiler-errors-warnings.txt new file mode 100644 index 0000000..3c1ce23 --- /dev/null +++ b/res/ide-support/eclipse/java-compiler-errors-warnings.txt @@ -0,0 +1,132 @@ +================================================================================ + 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. +================================================================================ + +# Java -> Compiler -> Errors/Warnings +======================================= + +The following settings are for Eclipse 4.15 +W = Warning +I = Ignore +E = Error + +Code style + - Non-static access to static member - W + - Indirect access to static member - I + - Unqualified access to instance field - I + - Access to a non-accessible member... - I + - Parameter assignment - I + - Non-externalized strings - I + - Undocumented empty block - I + - Resource not managed via try-with-resource (1.7 or higher) - I + - Method with a constructor name - W + - Method can be static - I + - Method can potentially be static - I + +Potential programming problems + - All - W + except the following: + + - Incomplete 'switch' cases on enum - W + [ ] Signal even if 'default' case exists + - Unlikely argument type for... - W + [ ] Perfprm strict... + - 'switch' is missing 'default' case - I + - Potential resource leak - I + +Name shadowing and conflicts + - Field declaration hides another... - I + - Local variable declaration hides.. - I + - Type parameter hides another type - W + - Method does not override... - W + - Interface method conflicts... - W + +Deprecated and restricted API + - Deprecated API - W + ([ ] on all additional check boxes) + - Deprecated API marked for removal - W + - Forbidden references - E + - Discouraged reference - W + +Modules + - All - W + +Unnecessary code + - All - W + ([x] on all additional check boxes) + except the following: + + - Value of exception parameter is... - I + - Unnecessary else - I + +Generic types + - All - W + [ ] Ignore unavoidable generic type problems + +Annotations + - All - W + ([x] on all additional check boxes) + +Null analysis + - Null pointer access - W + - Potential null pointer access - W + - Redundant null check - W + [ ] Include 'assert' in null analysis + [ ] Enable annotation-based null analysis + +Note: The list of codes supported in @SuppressWarnings annotation in +Eclipse IDE is documented here: + + 3.6: http://help.eclipse.org/helios/topic/org.eclipse.jdt.doc.isv/guide/jdt_api_compile.htm + 3.7: http://help.eclipse.org/indigo/topic/org.eclipse.jdt.doc.user/tasks/task-suppress_warnings.htm + 4.2: http://help.eclipse.org/juno/topic/org.eclipse.jdt.doc.user/tasks/task-suppress_warnings.htm + + +# Java -> Compiler -> Javadoc +============================= + +Enable 'Process Javadoc comments' + - Malformed Javadoc comments - W + - Only consider members visible as - Public + [X] Validate tag arguments + [ ] Report non-visible references + [X] Report deprecated references + - Missing tag descriptions - Validate all standard tags + - Missing Javadoc tags - W + - Only consider members visible as - Public + [X] - Ignore in overriding and implementing methods + [ ] - Ignore method type parameters + - Missing Javadoc comments - I + + +# Java -> Code Style -> Organize Imports +========================================== + +The following configuration of "Organize Imports" setting can be +recommended for a quick setup. The order is enforced by checkstyle, so for +the authoritative definition look at in +res/checkstyle/checkstyle.xml + + - java + - javax + - async + - jsp2 + - [static] org.junit + - org.junit + - [static] org + - org + - com + - util diff --git a/res/ide-support/eclipse/org.eclipse.jdt.core.prefs.properties b/res/ide-support/eclipse/org.eclipse.jdt.core.prefs.properties new file mode 100644 index 0000000..0e28ec4 --- /dev/null +++ b/res/ide-support/eclipse/org.eclipse.jdt.core.prefs.properties @@ -0,0 +1,20 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=@BUILD_JAVA_VERSION@ +org.eclipse.jdt.core.compiler.compliance=@MIN_JAVA_VERSION@ +org.eclipse.jdt.core.compiler.source=@BUILD_JAVA_VERSION@ diff --git a/res/ide-support/eclipse/start-tomcat.launch b/res/ide-support/eclipse/start-tomcat.launch new file mode 100644 index 0000000..b0f3eb5 --- /dev/null +++ b/res/ide-support/eclipse/start-tomcat.launch @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/res/ide-support/eclipse/stop-tomcat.launch b/res/ide-support/eclipse/stop-tomcat.launch new file mode 100644 index 0000000..1c7bcd3 --- /dev/null +++ b/res/ide-support/eclipse/stop-tomcat.launch @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/res/ide-support/idea/ant.xml b/res/ide-support/idea/ant.xml new file mode 100644 index 0000000..ac834d1 --- /dev/null +++ b/res/ide-support/idea/ant.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/codeStyles/Project.xml b/res/ide-support/idea/codeStyles/Project.xml new file mode 100644 index 0000000..adf710b --- /dev/null +++ b/res/ide-support/idea/codeStyles/Project.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/codeStyles/codeStyleConfig.xml b/res/ide-support/idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..c81df23 --- /dev/null +++ b/res/ide-support/idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/compiler.xml b/res/ide-support/idea/compiler.xml new file mode 100644 index 0000000..c65738a --- /dev/null +++ b/res/ide-support/idea/compiler.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/copyright/Tomcat.xml b/res/ide-support/idea/copyright/Tomcat.xml new file mode 100644 index 0000000..d9e4548 --- /dev/null +++ b/res/ide-support/idea/copyright/Tomcat.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/copyright/profiles_settings.xml b/res/ide-support/idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..ebc13c1 --- /dev/null +++ b/res/ide-support/idea/copyright/profiles_settings.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/externalDependencies.xml b/res/ide-support/idea/externalDependencies.xml new file mode 100644 index 0000000..11f0751 --- /dev/null +++ b/res/ide-support/idea/externalDependencies.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/inspectionProfiles/Project_Default.xml b/res/ide-support/idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..dd01fa3 --- /dev/null +++ b/res/ide-support/idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/misc.xml b/res/ide-support/idea/misc.xml new file mode 100644 index 0000000..0f6a7a2 --- /dev/null +++ b/res/ide-support/idea/misc.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/modules.xml b/res/ide-support/idea/modules.xml new file mode 100644 index 0000000..03c9e26 --- /dev/null +++ b/res/ide-support/idea/modules.xml @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/ide-support/idea/tomcat.iml b/res/ide-support/idea/tomcat.iml new file mode 100644 index 0000000..48543cc --- /dev/null +++ b/res/ide-support/idea/tomcat.iml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ide-support/idea/workspace.xml b/res/ide-support/idea/workspace.xml new file mode 100644 index 0000000..bb47d31 --- /dev/null +++ b/res/ide-support/idea/workspace.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/ide-support/netbeans/README.txt b/res/ide-support/netbeans/README.txt new file mode 100644 index 0000000..8873b49 --- /dev/null +++ b/res/ide-support/netbeans/README.txt @@ -0,0 +1,143 @@ +================================================================================ + 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. +================================================================================ + + + Building and Debugging Apache Tomcat under NetBeans + + +Unlike other IDE's, NetBeans is a pure java swing application. It uses +Apache Ant to build its projects, and works directly with the class files +and jars created by the standard Apache Ant build.xml files. This strength +is also its weakness when working with complex projects such as Tomcat that +already have their own build.xml files, but which do not use the NetBeans +templates. + +Any of complex Ant project can still be managed under NetBeans by +defining it to be something called a Free-Form Project. However, because +the build.xml does not conform to all the NetBeans naming and structural +conventions, a certain amount of manual customisation is required to +achieve a useful level of integration. + + +1. NetBeans can open a Tomcat source tree as a Free-Form Project, which + will allow you to edit, build, and debug Tomcat and its unit tests + within the workbench. Even with NetBeans 7.1, integration of a project + as complex as Tomcat requires significant configuration. The + configuration involves dealing with several quirky aspects of the way + NetBeans manages this kind of project. Before you try to open Tomcat + as a NetBeans project, you are strongly recommended to successfully + build and run the tests using Apache Ant from a command prompt! + (see BUILDING.txt in the Tomcat source root directory). + +2. Once Tomcat has been built, you can install the default NetBeans + project configuration by running the following build target: + + ant ide-netbeans + + This uses the Tomcat build.xml to create a new directory called + nbproject (this name is reserved by NetBeans). The nbproject directory + will then be primed with a self-consistent set of default definitions + sufficient to open Tomcat as a Free-Form Project under NetBeans. + + Note: if you ever open the Project Properties from the NetBeans context + menu, even without making any changes, there is a significant risk + that NetBeans will modify one or more of these files. You can + restore the Tomcat default files at any time by running this target: + + ant ide-netbeans-replace + + Only the default files will be overwritten, so any other content + such as your own local files, and the NetBeans private directory, + will not be affected. + +3. NetBeans needs to know where to find the directory where you keep the + Tomcat dependencies jars. If you have successfully built Tomcat from + a command prompt, you will probably have assigned the base.path + property in your build.properties file. + + Warning: The support for Tomcat in NetBeans will detect and use this + property. However, if you have left it to default, you MUST + still define this path in the nb-tomcat-project.properties file! + + Note: The current support for Tomcat in NetBeans does not include the + components in the modules directory (e.g. tomcat-lite). + +4. Start NetBeans... once it has initialised and scanned your other open + projects, just open an existing project and select the location of + Tomcat. NetBeans will recognise it as a Free-Form project, then read and + validate the nbproject/project.xml file. This file defines how to relate + targets in build.xml to NetBeans project-related actions. It also tells + NetBeans what classpaths to use for validation and code completion of + the various source directories. + + Warning: do not be tempted to casually click the properties menu item + for the Tomcat project. NetBeans might change the contents + of these files. (The NetBeans New Project wizard also + automatically creates a Free-Form project.xml which carries + this same warning). + + Note: the Tomcat project should open successfully and, after the source + packages have been scanned, they should not be flagged with any + syntax errors (except in some of the jsp bug unit tests). + +5. Verify your work by running the NetBeans project Clean action. It + should complete successfully. Next, run the Build action (which calls + the Tomcat deploy build target) and confirm that it successfully + compiles the Tomcat source files and creates the jars. + +6. Next, navigate down to one of the test files and select the compile + action. This will compile only your chosen file, although the compiler + will find there is nothing to do unless you have deliberately changed it. + + Note: if you have changed any of the Tomcat source files, they will be + recompiled first. However, any changes to test files will not be compiled + unless you select those file and explicitly compile them. If you have any + doubts about dependencies between unit test classes, you can use the + compileAllTests project action and any files that have been changed + will be detected and compiled. + +7. You can run an individual unit test class by selecting it and choosing + the "run selected file" NetBeans action. As the test runs, NetBeans + should open a unit test results pane that shows you the progress and + final outcome of the test (Note: this feature does not currently work). + +8. Next, open the source of the unit test that ran successfully in step 7. + Set a breakpoint in one of the test cases, then request NetBeans to + debug that class. The class will start running, and then will stop as + it hits your breakpoint. You should be able to display variables, then + navigate the call stack to open the source files of each method. You + should also be able to step through the code. Use the continue icon + to resume execution. When the test completes, you should see the same + jUnit test result panel as in step 7 (Note: this feature does not + currently work). + +9. You can also use your Tomcat NetBeans Free-Form project to debug an + external Tomcat instance that is executing on the same, or a different + machine. (Obviously, the external instance must be running the same + version of the source code!) + + The external Tomcat instance must be started with its jvm enabled for + debugging by adding extra arguments to JAVA_OPTS, e.g. + -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n + + To debug the external Tomcat instance under NetBeans, select the + "attach debugger" choice from the debug menu. Accept the default + JPDA debugger with the SocketAttach connector and the dt_socket + transport. Specify the hostname and port where the Tomcat jvm is + listening. Your NetBeans workbench should then connect to the external + Tomcat and display the running threads. You can now set breakpoints and + begin debugging. diff --git a/res/ide-support/netbeans/nb-tomcat-build.properties b/res/ide-support/netbeans/nb-tomcat-build.properties new file mode 100644 index 0000000..165bc2c --- /dev/null +++ b/res/ide-support/netbeans/nb-tomcat-build.properties @@ -0,0 +1,50 @@ +#================================================================================ +# 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. +#================================================================================ + +# NetBeans will automatically set the project basedir "." correctly to +# the parent directory of the nbproject subdirectory +# +# the Tomcat build.properties file defines the confusingly-named "base.path" +# property, which is where the Tomcat dependencies are downloaded + + +# the following properties will be generated by build.xml, but are also needed +# by the NetBeans targets which do not have access to those properties +# as-yet not created by the main Ant build. + +tomcat.output=./output +tomcat.classes=${tomcat.output}/classes +tomcat.build=${tomcat.output}/build +test.classes=${tomcat.output}/testclasses +test.temp=${tomcat.output}/test-tmp + +nb-test.io-method=org.apache.coyote.http11.Http11NioProtocol + +# it is not possible to retrieve the classpaths from the build to +# use in the NetBeans targets, so they must be explicitly declared + +nb-test.classpath=${test.classes}:${tomcat.build}/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@OBJENESIS_JAR@:@CGLIB_JAR@:@HAMCREST_JAR@:@ECJ_JAR@:@UNBOUNDID_JAR@:${tomcat.classes} + +# Extra properties used by the Tomcat project additional NetBeans targets. + +# configure JUnit test results so that NetBeans can intercept and display them +# +nb-junit.formatter=-Dorg.apache.juli.formatter=java.util.logging.SimpleFormatter +nb-junit.formatter.type=plain +nb-junit.formatter.usefile=true +nb-junit.formatter.extension=.txt + diff --git a/res/ide-support/netbeans/nb-tomcat-project.properties b/res/ide-support/netbeans/nb-tomcat-project.properties new file mode 100644 index 0000000..e1217fc --- /dev/null +++ b/res/ide-support/netbeans/nb-tomcat-project.properties @@ -0,0 +1,33 @@ +#================================================================================ +# 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. +#================================================================================ + +# NetBeans will automatically set the project basedir "." correctly to +# the parent directory of the nbproject subdirectory +# +# The Tomcat build.properties file defines the confusingly-named "base.path" +# property, which is where the Tomcat dependencies are downloaded. +# If you have followed the Tomcat recommendation and explicitly coded +# a path, it will be used by NetBeans. However, if you have left it to +# default you MUST explicitly code it here, e.g. +#base.path=/home/yourname/sandboxApache/tomcat8-dependencies + +# NetBeans syntax checking and code completion of the main source files +# needs all the Ant libraries to be on the classpath. This is achieved +# by the standard build.xml, but needs to be duplicated for NetBeans. +# +ant.includes=${ant.home}/lib/ant.jar:${ant.home}/lib/ant-apache-bcel.jar:${ant.home}/lib/ant-launcher.jar:${ant.home}/lib/ant-apache-oro.jar:${ant.home}/lib/ant-apache-regexp.jar:${ant.home}/lib/ant-jmf.jar:${ant.home}/lib/ant-jsch.jar:${ant.home}/lib/ant-apache-bsf.jar:${ant.home}/lib/ant-apache-log4j.jar:${ant.home}/lib/ant-apache-resolver.jar:${ant.home}/lib/ant-apache-xalan2.jar:${ant.home}/lib/ant-commons-net.jar:${ant.home}/lib/ant-javamail.jar:${ant.home}/lib/ant-junit.jar:${ant.home}/lib/ant-swing.jar:${ant.home}/lib/ant-testutil.jar:${ant.home}/lib/junit.jar:${ant.home}/lib/ant-commons-logging.jar + diff --git a/res/ide-support/netbeans/nb-tomcat.xml b/res/ide-support/netbeans/nb-tomcat.xml new file mode 100644 index 0000000..85ae7fe --- /dev/null +++ b/res/ide-support/netbeans/nb-tomcat.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This target can only run under NetBeans. + Property 'files' must be set in project.xml. + + + + + + + + + + + + + + + + + + + + This target can only run under NetBeans. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ide-support/netbeans/project.xml b/res/ide-support/netbeans/project.xml new file mode 100644 index 0000000..eda4386 --- /dev/null +++ b/res/ide-support/netbeans/project.xml @@ -0,0 +1,197 @@ + + + + + org.netbeans.modules.ant.freeform + + + + Tomcat @VERSION_MAJOR_MINOR@ + + + ./build.properties + + + ./nbproject/nb-tomcat-project.properties + + + + + java + java + + + + java + webapps/docs/appdev/sample/src + + + + java + modules/tomcat-lite/test + + + + java + test + + + + + + + deploy + + + + clean + + + + javadoc + + + + test + + + + clean + deploy + + + + -compile-all-tests + + + + compile-selected-files-in-test + + files + test + \.java$ + relative-path + + , + + + + + + test-selected-file + + test.entry + test + \.java$ + java-name + + + + + + + + debug-selected-file-in-test + + test.entry + test + \.java$ + java-name + + + + + + + + + + + java + + + + webapps/docs/appdev/sample/src + + + + test + + + build.xml + + + + + + + + + + + + + + + + java + @JAXRPC_JAR@:@WSDL4J_JAR@:@JDT_JAR@:@BND_JAR@:@MIGRATION_JAR@:${ant.includes}/ + @BUILD_JAVA_VERSION@ + + + webapps/docs/appdev/sample/src + output/classes + @BUILD_JAVA_VERSION@ + + + test + + output/classes:output/testclasses:output/build/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@OBJENESIS_JAR@:@CGLIB_JAR@:@HAMCREST_JAR@:@UNBOUNDID_JAR@ + @BUILD_JAVA_VERSION@ + + + + diff --git a/res/install-win/INSTALLLICENSE b/res/install-win/INSTALLLICENSE new file mode 100644 index 0000000..4c3277f --- /dev/null +++ b/res/install-win/INSTALLLICENSE @@ -0,0 +1,1143 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + + + +APACHE TOMCAT SUBCOMPONENTS: + +Apache Tomcat includes a number of subcomponents with separate copyright notices +and license terms. Your use of these subcomponents is subject to the terms and +conditions of the following licenses. + + +For the Eclipse JDT Core Batch Compiler (ecj-x.x.x.jar) component and the +following Jakarta EE Schemas: +- jakartaee_9.xsd +- jakartaee_10.xsd +- jakarta_web-services_2_0.xsd +- jakarta_web-services_client_2_0.xsd +- jsp_3_0.xsd +- jsp_3_1.xsd +- web-app_5_0.xsd +- web-app_6_0.xsd +- web-commonn_5_0.xsd +- web-commonn_6_0.xsd +- web-fragment_5_0.xsd +- web-fragment_6_0.xsd +- web-jsptaglibrary_3_0.xsd +- web-jsptaglibrary_3_1.xsd + +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. + + +For the Windows Installer component: + + * All NSIS source code, plug-ins, documentation, examples, header files and + graphics, with the exception of the compression modules and where + otherwise noted, are licensed under the zlib/libpng license. + * The zlib compression module for NSIS is licensed under the zlib/libpng + license. + * The bzip2 compression module for NSIS is licensed under the bzip2 license. + * The lzma compression module for NSIS is licensed under the Common Public + License version 1.0. + +zlib/libpng license + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +bzip2 license + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + 3. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 4. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. + +Julian Seward, Cambridge, UK. + +jseward@acm.org +Common Public License version 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation +distributed under this Agreement, and b) in the case of each subsequent +Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are +distributed by that particular Contributor. A Contribution 'originates' from a +Contributor if it was added to the Program by such Contributor itself or anyone +acting on such Contributor's behalf. Contributions do not include additions to +the Program which: (i) are separate modules of software distributed in +conjunction with the Program under their own license agreement, and (ii) are not +derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free copyright license to +reproduce, prepare derivative works of, publicly display, publicly perform, +distribute and sublicense the Contribution of such Contributor, if any, and such +derivative works, in source code and object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed +Patents to make, use, sell, offer to sell, import and otherwise transfer the +Contribution of such Contributor, if any, in source code and object code form. +This patent license shall apply to the combination of the Contribution and the +Program if, at the time the Contribution is added by the Contributor, such +addition of the Contribution causes such combination to be covered by the +Licensed Patents. The patent license shall not apply to any other combinations +which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the licenses to +its Contributions set forth herein, no assurances are provided by any +Contributor that the Program does not infringe the patent or other intellectual +property rights of any other entity. Each Contributor disclaims any liability to +Recipient for claims brought by any other entity based on infringement of +intellectual property rights or otherwise. As a condition to exercising the +rights and licenses granted hereunder, each Recipient hereby assumes sole +responsibility to secure any other intellectual property rights needed, if any. +For example, if a third party patent license is required to allow Recipient to +distribute the Program, it is Recipient's responsibility to acquire that license +before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient copyright +rights in its Contribution, if any, to grant the copyright license set forth in +this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its +own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties and +conditions, express and implied, including warranties or conditions of title and +non-infringement, and implied warranties or conditions of merchantability and +fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and consequential +damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are offered by +that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such Contributor, +and informs licensees how to obtain it in a reasonable manner on or through a +medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the +Program. + +Each Contributor must identify itself as the originator of its Contribution, if +any, in a manner that reasonably allows subsequent Recipients to identify the +originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, if +a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, damages +and costs (collectively "Losses") arising from claims, lawsuits and other legal +actions brought by a third party against the Indemnified Contributor to the +extent caused by the acts or omissions of such Commercial Contributor in +connection with its distribution of the Program in a commercial product +offering. The obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property infringement. In order +to qualify, an Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial Contributor to +control, and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may participate in +any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If that +Commercial Contributor then makes performance claims, or offers warranties +related to Product X, those performance claims and warranties are such +Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a court +requires any other Contributor to pay any damages as a result, the Commercial +Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each +Recipient is solely responsible for determining the appropriateness of using and +distributing the Program and assumes all risks associated with its exercise of +rights under this Agreement, including but not limited to the risks and costs of +program errors, compliance with applicable laws, damage to or loss of data, +programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS +GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable +law, it shall not affect the validity or enforceability of the remainder of the +terms of this Agreement, and without further action by the parties hereto, such +provision shall be reformed to the minimum extent necessary to make such +provision valid and enforceable. + +If Recipient institutes patent litigation against a Contributor with respect to +a patent applicable to software (including a cross-claim or counterclaim in a +lawsuit), then any patent licenses granted by that Contributor to such Recipient +under this Agreement shall terminate as of the date such litigation is filed. In +addition, if Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or hardware) +infringes such Recipient's patent(s), then such Recipient's rights granted under +Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue and +survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to time. +No one other than the Agreement Steward has the right to modify this Agreement. +IBM is the initial Agreement Steward. IBM may assign the responsibility to serve +as the Agreement Steward to a suitable separate entity. Each new version of the +Agreement will be given a distinguishing version number. The Program (including +Contributions) may always be distributed subject to the version of the Agreement +under which it was received. In addition, after a new version of the Agreement +is published, Contributor may elect to distribute the Program (including its +Contributions) under the new version. Except as expressly stated in Sections +2(a) and 2(b) above, Recipient receives no rights or licenses to the +intellectual property of any Contributor under this Agreement, whether +expressly, by implication, estoppel or otherwise. All rights in the Program not +expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial in +any resulting litigation. + +Special exception for LZMA compression module + +Igor Pavlov and Amir Szekely, the authors of the LZMA compression module for +NSIS, expressly permit you to statically or dynamically link your code (or bind +by name) to the files from the LZMA compression module for NSIS without +subjecting your linked code to the terms of the Common Public license version +1.0. Any modifications or additions to files from the LZMA compression module +for NSIS, however, are subject to the terms of the Common Public License version +1.0. + + +For the following XML Schemas for Java EE Deployment Descriptors: + - javaee_5.xsd + - javaee_web_services_1_2.xsd + - javaee_web_services_client_1_2.xsd + - javaee_6.xsd + - javaee_web_services_1_3.xsd + - javaee_web_services_client_1_3.xsd + - jsp_2_2.xsd + - web-app_3_0.xsd + - web-common_3_0.xsd + - web-fragment_3_0.xsd + - javaee_7.xsd + - javaee_web_services_1_4.xsd + - javaee_web_services_client_1_4.xsd + - jsp_2_3.xsd + - web-app_3_1.xsd + - web-common_3_1.xsd + - web-fragment_3_1.xsd + - javaee_8.xsd + - web-app_4_0.xsd + - web-common_4_0.xsd + - web-fragment_4_0.xsd + +COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 + +1. Definitions. + + 1.1. Contributor. means each individual or entity that creates or contributes + to the creation of Modifications. + + 1.2. Contributor Version. means the combination of the Original Software, + prior Modifications used by a Contributor (if any), and the + Modifications made by that particular Contributor. + + 1.3. Covered Software. means (a) the Original Software, or (b) Modifications, + or (c) the combination of files containing Original Software with files + containing Modifications, in each case including portions thereof. + + 1.4. Executable. means the Covered Software in any form other than Source + Code. + + 1.5. Initial Developer. means the individual or entity that first makes + Original Software available under this License. + + 1.6. Larger Work. means a work which combines Covered Software or portions + thereof with code not governed by the terms of this License. + + 1.7. License. means this document. + + 1.8. Licensable. means having the right to grant, to the maximum extent + possible, whether at the time of the initial grant or subsequently + acquired, any and all of the rights conveyed herein. + + 1.9. Modifications. means the Source Code and Executable form of any of the + following: + + A. Any file that results from an addition to, deletion from or + modification of the contents of a file containing Original Software + or previous Modifications; + + B. Any new file that contains any part of the Original Software or + previous Modification; or + + C. Any new file that is contributed or otherwise made available under + the terms of this License. + + 1.10. Original Software. means the Source Code and Executable form of + computer software code that is originally released under this License. + + 1.11. Patent Claims. means any patent claim(s), now owned or hereafter + acquired, including without limitation, method, process, and apparatus + claims, in any patent Licensable by grantor. + + 1.12. Source Code. means (a) the common form of computer software code in + which modifications are made and (b) associated documentation included + in or with such code. + + 1.13. You. (or .Your.) means an individual or a legal entity exercising + rights under, and complying with all of the terms of, this License. For + legal entities, .You. includes any entity which controls, is controlled + by, or is under common control with You. For purposes of this + definition, .control. means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + +2. License Grants. + + 2.1. The Initial Developer Grant. + + Conditioned upon Your compliance with Section 3.1 below and subject to + third party intellectual property claims, the Initial Developer hereby + grants You a world-wide, royalty-free, non-exclusive license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by Initial Developer, to use, reproduce, modify, display, + perform, sublicense and distribute the Original Software (or + portions thereof), with or without Modifications, and/or as part of + a Larger Work; and + + (b) under Patent Claims infringed by the making, using or selling of + Original Software, to make, have made, use, practice, sell, and + offer for sale, and/or otherwise dispose of the Original Software + (or portions thereof). + + (c) The licenses granted in Sections 2.1(a) and (b) are effective on the + date Initial Developer first distributes or otherwise makes the + Original Software available to a third party under the terms of this + License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is granted: + (1) for code that You delete from the Original Software, or (2) for + infringements caused by: (i) the modification of the Original + Software, or (ii) the combination of the Original Software with + other software or devices. + + 2.2. Contributor Grant. + + Conditioned upon Your compliance with Section 3.1 below and subject to third + party intellectual property claims, each Contributor hereby grants You a + world-wide, royalty-free, non-exclusive license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by Contributor to use, reproduce, modify, display, + perform, sublicense and distribute the Modifications created by such + Contributor (or portions thereof), either on an unmodified basis, + with other Modifications, as Covered Software and/or as part of a + Larger Work; and + + (b) under Patent Claims infringed by the making, using, or selling of + Modifications made by that Contributor either alone and/or in + combination with its Contributor Version (or portions of such + combination), to make, use, sell, offer for sale, have made, and/or + otherwise dispose of: (1) Modifications made by that Contributor (or + portions thereof); and (2) the combination of Modifications made by + that Contributor with its Contributor Version (or portions of such + combination). + + (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on + the date Contributor first distributes or otherwise makes the + Modifications available to a third party. + + (d) Notwithstanding Section 2.2(b) above, no patent license is granted: + (1) for any code that Contributor has deleted from the Contributor + Version; (2) for infringements caused by: (i) third party + modifications of Contributor Version, or (ii) the combination of + Modifications made by that Contributor with other software (except + as part of the Contributor Version) or other devices; or (3) under + Patent Claims infringed by Covered Software in the absence of + Modifications made by that Contributor. + +3. Distribution Obligations. + + 3.1. Availability of Source Code. + Any Covered Software that You distribute or otherwise make available in + Executable form must also be made available in Source Code form and that + Source Code form must be distributed only under the terms of this License. + You must include a copy of this License with every copy of the Source Code + form of the Covered Software You distribute or otherwise make available. + You must inform recipients of any such Covered Software in Executable form + as to how they can obtain such Covered Software in Source Code form in a + reasonable manner on or through a medium customarily used for software + exchange. + + 3.2. Modifications. + The Modifications that You create or to which You contribute are governed + by the terms of this License. You represent that You believe Your + Modifications are Your original creation(s) and/or You have sufficient + rights to grant the rights conveyed by this License. + + 3.3. Required Notices. + You must include a notice in each of Your Modifications that identifies + You as the Contributor of the Modification. You may not remove or alter + any copyright, patent or trademark notices contained within the Covered + Software, or any notices of licensing or any descriptive text giving + attribution to any Contributor or the Initial Developer. + + 3.4. Application of Additional Terms. + You may not offer or impose any terms on any Covered Software in Source + Code form that alters or restricts the applicable version of this License + or the recipients. rights hereunder. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability obligations to + one or more recipients of Covered Software. However, you may do so only on + Your own behalf, and not on behalf of the Initial Developer or any + Contributor. You must make it absolutely clear that any such warranty, + support, indemnity or liability obligation is offered by You alone, and + You hereby agree to indemnify the Initial Developer and every Contributor + for any liability incurred by the Initial Developer or such Contributor as + a result of warranty, support, indemnity or liability terms You offer. + + 3.5. Distribution of Executable Versions. + You may distribute the Executable form of the Covered Software under the + terms of this License or under the terms of a license of Your choice, + which may contain terms different from this License, provided that You are + in compliance with the terms of this License and that the license for the + Executable form does not attempt to limit or alter the recipient.s rights + in the Source Code form from the rights set forth in this License. If You + distribute the Covered Software in Executable form under a different + license, You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial Developer + or Contributor. You hereby agree to indemnify the Initial Developer and + every Contributor for any liability incurred by the Initial Developer or + such Contributor as a result of any such terms You offer. + + 3.6. Larger Works. + You may create a Larger Work by combining Covered Software with other code + not governed by the terms of this License and distribute the Larger Work + as a single product. In such a case, You must make sure the requirements + of this License are fulfilled for the Covered Software. + +4. Versions of the License. + + 4.1. New Versions. + Sun Microsystems, Inc. is the initial license steward and may publish + revised and/or new versions of this License from time to time. Each + version will be given a distinguishing version number. Except as provided + in Section 4.3, no one other than the license steward has the right to + modify this License. + + 4.2. Effect of New Versions. + You may always continue to use, distribute or otherwise make the Covered + Software available under the terms of the version of the License under + which You originally received the Covered Software. If the Initial + Developer includes a notice in the Original Software prohibiting it from + being distributed or otherwise made available under any subsequent version + of the License, You must distribute and make the Covered Software + available under the terms of the version of the License under which You + originally received the Covered Software. Otherwise, You may also choose + to use, distribute or otherwise make the Covered Software available under + the terms of any subsequent version of the License published by the + license steward. + + 4.3. Modified Versions. + When You are an Initial Developer and You want to create a new license for + Your Original Software, You may create and use a modified version of this + License if You: (a) rename the license and remove any references to the + name of the license steward (except to note that the license differs from + this License); and (b) otherwise make it clear that the license contains + terms which differ from this License. + +5. DISCLAIMER OF WARRANTY. + + COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN .AS IS. BASIS, WITHOUT + WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT + LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, + MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK + AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD + ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL + DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY + SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN + ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED + HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +6. TERMINATION. + + 6.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to + cure such breach within 30 days of becoming aware of the breach. + Provisions which, by their nature, must remain in effect beyond the + termination of this License shall survive. + + 6.2. If You assert a patent infringement claim (excluding declaratory + judgment actions) against Initial Developer or a Contributor (the + Initial Developer or Contributor against whom You assert such claim + is referred to as .Participant.) alleging that the Participant + Software (meaning the Contributor Version where the Participant is a + Contributor or the Original Software where the Participant is the + Initial Developer) directly or indirectly infringes any patent, then + any and all rights granted directly or indirectly to You by such + Participant, the Initial Developer (if the Initial Developer is not + the Participant) and all Contributors under Sections 2.1 and/or 2.2 + of this License shall, upon 60 days notice from Participant terminate + prospectively and automatically at the expiration of such 60 day + notice period, unless if within such 60 day period You withdraw Your + claim with respect to the Participant Software against such + Participant either unilaterally or pursuant to a written agreement + with Participant. + + 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end + user licenses that have been validly granted by You or any + distributor hereunder prior to termination (excluding licenses + granted to You by any distributor) shall survive termination. + +7. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING + NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY + OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF + ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, + INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT + LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, + COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR + LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF + SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR + DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY.S NEGLIGENCE TO THE EXTENT + APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS + EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +8. U.S. GOVERNMENT END USERS. + + The Covered Software is a .commercial item,. as that term is defined in 48 + C.F.R. 2.101 (Oct. 1995), consisting of .commercial computer software. (as + that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and commercial + computer software documentation. as such terms are used in 48 C.F.R. 12.212 + (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 + through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered + Software with only those rights set forth herein. This U.S. Government Rights + clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or + provision that addresses Government rights in computer software under this + License. + +9. MISCELLANEOUS. + + This License represents the complete agreement concerning subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. This License shall be governed by the law of the jurisdiction + specified in a notice contained within the Original Software (except to the + extent applicable law, if any, provides otherwise), excluding such + jurisdiction's conflict-of-law provisions. Any litigation relating to this + License shall be subject to the jurisdiction of the courts located in the + jurisdiction and venue specified in a notice contained within the Original + Software, with the losing party responsible for costs, including, without + limitation, court costs and reasonable attorneys. fees and expenses. The + application of the United Nations Convention on Contracts for the + International Sale of Goods is expressly excluded. Any law or regulation + which provides that the language of a contract shall be construed against + the drafter shall not apply to this License. You agree that You alone are + responsible for compliance with the United States export administration + regulations (and the export control laws and regulation of any other + countries) when You use, distribute or otherwise make available any Covered + Software. + +10. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is responsible + for claims and damages arising, directly or indirectly, out of its + utilization of rights under this License and You agree to work with Initial + Developer and Contributors to distribute such responsibility on an equitable + basis. Nothing herein is intended or shall be deemed to constitute any + admission of liability. + + NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION + LICENSE (CDDL) + + The code released under the CDDL shall be governed by the laws of the State + of California (excluding conflict-of-law provisions). Any litigation relating + to this License shall be subject to the jurisdiction of the Federal Courts of + the Northern District of California and the state courts of the State of + California, with venue lying in Santa Clara County, California. + diff --git a/res/install-win/Uninstall.exe.sig b/res/install-win/Uninstall.exe.sig new file mode 100644 index 0000000000000000000000000000000000000000..bcaa8f5f46b5372130f73d0c81712e04c80cf8e6 GIT binary patch literal 10202 zcmdU#cT`kK*yia#&LBvXCW9c{h9)PQAfRLr5s8vBk|ha^1W_c4fPhWT5+o==f*=S2 zN=}lK1VMr%fo&Wab-vl1IcN9GIm;GYjs3fUnB;eyd9 z&Q<`$`5FuY!ASvP9DI`yC6EOK3Wi`~0lqi{JUAdI2qgjrLD7zYJ#hAy8d3ls0>VN= z)4>5)faYKNLNC(omnDJJF0WCTiE$pr80~(!#LgxX^As_7;aO_J9D9$^XQ4J>iC1{d zBy8np{9k7{7Eo3=n=2b?LrBB=m%|s(C}wE@g&hH)pzdf07z75BkvyS!xcC0T!rG|D zG~PuT{gdzfaVH=m%y=!}gaAGcF(!_N1XS!yGE=z7x>caD@^ zfk#s3Gavf9#LWQHCVi$T^|J1p#}14IWw$rl$$Msv2e2izNVS;Wq*7)1zj~T?jr#gk zmtyN!=ccl1_e5J)fw&K3@??Ays7>6v^KKFjx27);(SL^ZzlAZHabM_KF;Z)b-DNg? zf>_3WW3+OEu6FrtcV{8uqH(FW1b^EnfYh`J3g)NOJge+pHPb8BM+b?VzB~2FXTyPbI6c`+Z zx}JB82Kh3p^wmD2hqxPhSq_WC<&y+2*5eK8D`z&%=b^1WW^&f1sf$&R>z&O%Otn<@hj&RNX(wi_pQamh^I=8Va+PA->XcL zr{(c)h%4_gbyyZ0Zkf0O#d_z*@aT2q4s%%>{3 zLG|1+G*_A6vyxe;PI$&Zs{VcYF|tBw0P27<3d`xmTQMCd_yx`TIXTQJn+|QH0Le*z z(vyHt%+d@2ZsXu#lmZ(Y7X*Sj0CoV|X&C^8&{(^R z%FOg+HM;(GA^+(z_^}w3#SKt68443r8ju9U(V}SK5X5OD7kj~f8^#_W zKM4ba5&(Dr4h|IaFV?T?3!^R&6bOXnjINh6xVj)}jLvYQm#U;5+^N5YTbERr7RtL4 zY?Mb=!?GJnXQRWini=NlP)B!Wi}JEJ|Ai{iwXNfTm%C{@8!qE)wOWuiwfE=8_b=#n z?``cA0^UjY%Z&QEKgh{Hkz-ztcS%z!PwYrh&%uFEdUDG5E!E^p)F5+1Dyv>YiSx%-#qZG?>=>EAB9B- znJ|DM9k7x@Jv7Nhvz7#w#3us!v_3KzUEE0Iwa^-hN$iKLjKL5-Ua5tQVW@33ns`>1l0$Ltv*)gk!OPW&a1G;?+To;ym zODo(Lm~bp2;<$T2n*+-7e-dxU^KOF_l*6 zsop*sjC74HAaR*JO2$P$UGYjx>cjRP` zN)Q(%fByAr>b%5_mB4Y9GsCF!tugiJ@fqoK*Zl=1efzi#tw-cMdTtlF3S8y7CN8Hj z*on4qZ?@;hyUJQWt-SO+MLeS)GFiW#F4-fGm~4PYIXxqn{J{h(d0TgwYQY(5rUiOgKB@*=rmUot(%CR9vj;{k0CtCAejxl0` z5m5}Ue^KOW>R|`fV18F_k0gHg2ez=t)HbKz6!~u@`>%?8vh`tPhY|qi%13XXv z$H=rDVDm3!8hP3a0EY;jitGPLrhlxP6OmR#d>3iClhH6iF%k`6BpQtf5jc(WzZL1f z2C)ZVCqZCP%>E6)!@;o%BmzOf0O227){sMcT+mE+%w7cqQ4q7z~r>}4yIENGGbK88gY3ydi{UmMDHbgjQaAVLRbpI}l z9RauMtfYu{YSvJ>$w-8U%B8N$)9t2mjR3K*cRjL^QS>};aXT}3T%aD#>s=7sDQCQD zQkkzFZJElOIvHL#cle;zoFmjerY^&IH z-mSZ2uJpBE7stKP69nWIbf$>TTUy{E}dNz%%5(aVf=}_Z%lL$EXvP&pF!r!Y^X` z<~PnJUT=3^7pYIcnt)NOa~_yjQKwl}f4tft&K**cfaY0!gsc+pr%HgA-^3>+s0$pX zJgU$9uz78`7P^lb7np$FTgU1&Y}$LzdIyVe)&tBjyl&wko6Q}HugEihhb}Xi^jkvw z*Q+`_Oac218zek)0|$Bh@>G`>C`lMcMMUY+c)qR<_s{})=gdfDhE4o-_(vz(6 z)TlGBF80~*uT$nEh4`QiQ`JO{zpH_3YMmmT1(hfk*9H(2ECCj=-9g-U7CE=DnX zDe$E|<(;BwqI=i!Iab+ORv_}=%@V_=g>`hgstSr+Ny~bitnSP;!$lfOicPB9 znYwp{2KM-p^RcKth#b-3#5MW0Ezt|iw>NF&WI#yVvWmqj3rr|x76)GLz^BHFW@*qv zjjW1_&(la9GYT9MaDwQoY^^Tz)XCGukOfATNZ=2)J`vDwI<_^@xv$(9yE;{FFzK|+ zvG<_AY)_PIeyJ;Z`&I9OQ>-b%pf*;4D@gdfC6{rDYu9}acAT=HK7+4Rv?{ANCyfL* z78h3ta!MdP(PhH7U+om(opaB&cvmx{q_OE4O?Nb^k$$PoR4-xM7fdY~#8o5~veH*Y zto?phao!z>SOH=^&Sc-XV(4b7Dnk+NFpj;P9{}M^fq~jdAdRw(hB3 zANA@EyexfD(fDnkhHEexEAw@eaC4o;cg%E)qO`u244q!03P7Uzv6;F`O+GQ6Hquv5 zO7Z!A<;nvAeDFJaNg`eHQ*K?yaO>(naLZa`a=B7s*O)xy;K~P4@o@w;=zY zbBo~*Zei>Q02c@QgjYQ+te93J({C~tArPzNe#946OQ22XnUi`?p9 z+0oCB>fVmEomqx`=-0R%mJ}Y|SXq~*MB_g;6hKE#OVeqZ7^wU8?Tp7l^DMow+^%%o zv4@ycYF}da<&Rv;#vEyn^pyJPjs`hp4x>JJgnVQuF)`RA&iy=jFRe~&N>pvcu|_!M zV2)W^|8@H(+b`+TqjE0<+tfwR#3XKi?9y=)p2R48NraA zbtR(H$wP!USca~P;}gxRDBKdcJCDL(DChNe?Q4hJLIzCtM6d3Mgx|+c>md9Dr#t4O zH4wWs^(r}s0B3;1sw!q)%@~t3D|ho{y2TWYN@ga9FwHdT((|w$M^-F_`8eCnqpJ_a zWfZcT7pbGQ?jswN?p}#kEs(!B6R}Rk)wTMCMi=)yKUp&op1Gu8-^ zT|wk(!fJ}>gYl-X6FanHYw#>xW=PFCMCUN!xGii9Xhj_s2P-k}U6UwJhOCAtY?Yfj z)>}_=n1n2~73UqrF)o2`w0K?hk04{Ru5hGUa-bOr3B^@E@6!p{)?g z81(Q)p|s9(*2(UtgL6F#hG``DWBf`PkG&<%CtHo(q|$55izUzL%rCS3NX8w0?xvYd zuU@jyt$Ojt%W)c!46vovBnkd_%Nd&mo1+`A2Y6r%jgRBXvor!Xx;t>Q0qgc4*^zH| z@Y&)rQFJ4=)k6~PhU2s}!3PRU#HRKsN4Jccagrq0Tlj91(pzfHv<*lbkI41j-G1Nt zRq29}f_uFC(MPg0`k=-a^IDH86bHSy#q1`%u0NiU=egYfsA~=H>=Mdc^_X?0J>+wF zCbPNoa{nB9#OyiWC+yGpU)i=9Yo9QbRT)pLfA;W$Ct4_8O}Wg*U&!OYN9&cX{jKBnZ0q&1sCM)%spObgXt|9uC$V}}h46>B#(0^| z87l-|$ob4CJ6L$yzi?49D{K1WSoTW1+FyYkE@#2NZS3YHwZQ&1Bl~HgWmT?GF06IFH&5%= z&9i`KjQ7td1NtZZnV{MjXIvevioOz}{BJWJMZ}NY+8ZBZrT_NFl@feH5kVJrQ6yI*r!=hTHH zvFctWpSQma8XJ+H7atduz%>%n+wygVUquIycbp;ee^z^xxqRCq-+F+hU1B*2C3KC8 zy2EBeAgQcpNc=_Vr4YO7vxm)|_Eh%I3W{CD7nLtn_1}3{DcUf!!mwSL_Ck(#LQTc* zoV9asWM7OMjs7EDNjJ)=KZ$0}Z=lTu-K3kL8hXUtH?77x2~Nw^Fp2 zqj#-L+_U4wxwM8nx4v#XIXYYv6(C<6K!ywlP`G}W100J}iVc4oon-EcVw&|=t&ZF0 z{mYaA{==;2JIgR9HllyBOaOr?ikz|x1%kpUF^(|dkJB$nFbMMfMCs({=$A?`C7!&6 zy{V_Eiv^q!pvSCuC@Iky4Hp{^Q#T8kx{H;mqm8$zn~jqrob0<$L9r=u1p&CP@dDVwtyj>P0cVL+D?{kCu4wJ za&mVx|Kt1U0Gg8v2`R~cT%_aVU}owD(@>Q=4FdV6AQ*j3|Me+-KV$+$Vh))=;b0Uf z5UZaeg|+7uf7-}SS6O=AbS>^6=@V~!nAihJvwO8ZBWErj5;RF>ukl~;D~)2tT6v?L z@R6i(#elra*L!-qD^uROi259DN@()==a~pbg_$o0oJ&~G#w*ZxRGsFEKE^7Q3oU&Vn@_=g-QC)eASvT z?3tBVW-a7PRUrG4hu!?Z;odi?AfX0N!5<_9gE3F!=(kksZ*#Gq#M)6G9o3_Awmfh$ ze>d?3H9xJj;|)k*CJe@2!&ni3z-gMo4RD?!0}P_3|ACCZo}2Q^X=wis5dVz<2>}8t zPQHW*8dwAb<^$SjO|*K5>M4l+$qH0NoOl{PERCOx#26YUeg?+Qz*vp;|1&7!?+<}~ z_>2E8Xkgsw&u~KAP$2N`za*}~X+I2cHBN8zUlI48@73-5VK*Zs#s&TDunLnCd;%?yOYUsOfn(Pr2!f|qGWSRaviU$4X1G2+zU?Y9&o(%0WRYlQ;$5_`tK>Mq zBE~#bWAtr+kt2+vQuANLY>URjYhuy34uTN2^+SvkoEt zAb-M+!pGN&*zyJKC!6e{=1)Ure#n@6fx)x!RseR$Thw_!GR4xOg2Vyslom7=whOb1 ziZwT`e#F9?_6hMsM4R)1YvJ1`OyJ&594;qKZZ~Q(hegYxgw9c_C|#=?)#AZ1#pu%F zk3@&agVeGhR|t6fJ{8-%;Gx|InM|SX&ALIl2%MwoYH$9a_$25a3?ijc1cIZmmoWfd z0E0k)-+#zgFd6pWu>O}9OVEMI0NF2Zk`Td9W-#raFLU5S;6%SPLji_gN`!C{%qIWm ziz!rq`p=F~xGeOdL`=#1Jhoi#^H_j+-Yk<;z$S>cb<320>9Tpn8&;#_!C+WZkI63f zIXbc2W7yGt4ByDo=f@7&&Bn4pQ#TcU+nHdP2StGb)VMx9iomAlm9>WW6kl&?4EmxT zWHRkxIqtC(G;uG}qaA+JDYh8eAYbv&e}znGO`)FFxnKT`GuSIaOzJi2zEg!`K7D|& zSb7zTHoCALd9{sYhFW& zlmpcdlbvo1Jkq)qJ@xvN)DQ{oUWCZx^pcu_BI7?8|T#ogB zZ@K@T=epB=jB(|%PWR&mmOul-XztLdo);uM8uk9hrB4UTj_0KVeaNhWTM*1)0xQdF z;LjlvI!bEbu%*b#JO0Y6yfO|{i1=PhJ-Bq;PFbf-F;il>{Aa$>x@eZT=-Q^?p{`6) zhUGq-*!6g%vM$Sg%C+#I2BQeF8se}5VXGBeO!bZhguS4ULG75psg~UBsLt9tnBRdJaY=C0ny>KY@9L z==7zbATR(0Vfgu^ao{KjF{TRtWOjT0Cmsl9UjK~~b2_gvGX-sf*>#=1k%XDVn6s(V z7p*{;Iec2c$A*()+!O?WfD6L~;GzgYeSrUGD-_NGg)MZ~yb@ZXQE9;mkC&|*;Xgh; zTdO-BUNRJI6kmPWE95WxGAIg+#X_$pwmS~9D|r8WLqP(P;8AdukS=Z@Ys}fW&@({Mdq|G@RC(tx2o6I;hVm%lL)Df0G) z-~K>!$o?uvn&hi<-0rD3!XEe81_Qk=Dq7Pa#(j@^PRX~aEL&t!t(DJeJ0QNG zG1pu3N%tF?ZM%|;XKVdRMt*Ev>UikF^%mxywm||{-HLD}O&OEOAfDjo8N8jNojD^S z0gNG8?3UXoT#J!lrJdntP>0tC-~9N9`BKRoAAOcqu*W5T(TZle_9b`k%p^ViBT}zm z9BGZIkX43K6wUjRJFfBx{+d(lKACGzoZh<2b*zu1d6d!LF_f=Z?A3I>bQXWn8{IYR z#A+<{Ha!(YA)%3~Ia!Doke1_HB9^#hY-OIPEmO{4aufQ%tw-=qNP@mTFO@jv_o9c% W^y>1espxc28d@=4rEd<)IQm~@5xSTF literal 0 HcmV?d00001 diff --git a/res/install-win/header.bmp b/res/install-win/header.bmp new file mode 100644 index 0000000000000000000000000000000000000000..0c05ce1eebdabe40b150597b1b1894e2c7a06f19 GIT binary patch literal 10198 zcmd6szi#8o6^BoN01{Zr8bMosZ#I}ZbcO-nD5MRD2lR!y=$&FBY&C^B~iaQ=ggTu{_XGo^-FQ(*FP8e6|FzR zZ{TOLb=TWph{S*JxC_&h5#dja@fFWB#ab)&-&TJ7=im8K0tN6!+=7KpBoT;#MpA%* z7A$lkg+L4m@S-r#f`xAU-IzLo0t~cZp%bAgZ43z%V4wvH9U=u{NT2`%Em-J8_>(S% z1PU0;^x3NX-ug-#6pHxh&P zya0W)V4)Ke3&fB>0R~#I(0R8dF?9k37-+#l=UrOF)Cm+|palz^cQ-#`OacWMXu(3~ zL!phS6DYty3l=&bCN`!{pa26cSm;~`nJ%VIpa26cSm?yke=9L0P=J9J^wIgyieu^o z3NX-uh0cdv5>qEofPoe)bUq}Dm^y(1476aO^Wkb@>I4cf(1L}|hjJTJCs2Ta7A$l= z%xz4aKmi6?`SCx49QmB#ejN<|)hcEA& zs@m;ciDWHZp`Y0o>q1plHHX8YX)2j+^JaSqcRU=TKYp1mwN+NTTQqekX)u?M{V%(x z`_@+Ow6D?B(VuiLRDDs~?A6B6Q_bz+Qg4&wcRamrJwOHp4zn)uGwTarTaDzFF6Bma;k2a$eqf6Nbdi zZ}Q2_Mb&{X-%5YGt|7ydO}ca>q4S!#w!9bZv9d3fJT3b)=KR~sKjlgFc|ir zY`2?v(73yvc`K`eD7DtIOZu@D>@{rUp*dVqB6r=UPS%ZYhYhiwj$w89KHD>9UX-

    2>QRYwxdzBYZ5#v1k-hDd6Jg)tdq%>%yINSCbiBQSWe zVaU$aT-vW)zkK3zTAD0W*k9FTQ^arlSNrW*Ge&w>Hkv|1DK*A+(PZT7-&sc&$$}88 zcOpj}Y-8CD{pOszP|Nn!VJ-?uHPOG5?X{fg)k?aCT=vbO;fH1=a_#$vk~|WuG^;D# z9!)yQnHp>9LYVrc*-5rbU5VtOtR81NuA6Axy)J8|xTl-FAdHDFAA^X=#jg{t<&3X% zNt6E%S?lRiVPw~Q@>t~z_@b%<34(Gv`%G?JQ5WfzL~zkjzp4vkNq#y-^L5Qdk#DUQ zU)&C1GuPGf;Ue;?Rr0xK%Knv{s>;^XMV5+Y!qRN=>6NTIc~Di9ODgt8nmwn!K9 zq?52Flb3WgA0+pxcs{*qu@!acrs2$}bZU@SrdMVfUwn{qlBL4F>O7k#zE091 z0JpjpI+;6Yp71GDJ&jOPS1TsbxQZ{MW!LvFV+=n z>DtUE74|y2@0 zTDopW)h3mw+N+OTcyV4M_f=V{uF9oWCU)AVvMh7SkBbAL)pXtNeR`a!Wt;+7_;_1A z=dAPh`M%8Le!OzqtFNewNO8)-(nnGsPAADj@$>gHN<+MddNp11%}=?g=Y{t-7E~r=2^)M52)!b;F%`0XgpBQxR2@drMOs_20h->L`$?;0h?1dVQ zHIPePYOwo9ea3y$6kaYJxt1;#IWr+Q%^s_{H2VCJlpqmLp~k7FKkb)}Tuaw*bD*=( z#&OigAMoF)tg2ETD_4XU*zWWo%WkcvYqJ>ZY-x|@&E>FAT{*ce@_u~I!^-t}x!hSz z7aPc#CtbSNpqkXC;!>&?G=%8}Pd|+-QdF81|K$uA?zgsJhzOaXBE@)a6c1)AYNrq+v}l zt*SOMnkXadKVH@n!ER+Zq#FCNsxIz%eq#I|-NBM%vm}1@3oqv}w|%Ta()Y@$$+BM9 z+PcQ>4#)f)T<9*HLykwrNxhg|uc#jv3G8d>3i?8Qoas^veVnZe^>z7ebyZ;?lFr;& z0Fs>f%6IZ(uv4g=A2}+KbeI3FKAlJ-`Sa=H<38i$*V$_tBJL~i<+RIa4Jl5A zebqb>jPpXRy4tbf{4 zy_dc8$J5!oex6?SIw(th1}mxwl&_ zjGMl{t#J3352yOyJagNxJa|d`{WJc10{`93f1f;W$&&My{20=bCI5-z zfAf(`%6}z;&BYhIQUUbn?9QKO@G`klsJO5S7&cEn_mm(-r zh+_;r4CkzJP6KC31NS@Mf$XyLscxY>BJ9uKcI)SE`_$)dyY;#w`^Y!b9I||-ns@Fx zKja$tKJl%=6Cpl-@_X~~ck-q;zn&ao5@CIg?;mm&?~mr+|9mfdVL(h+QhLNd4B7(x z#y7u#L%@BG@B72F=dOtcPMPmZmcHawOJBNt*$cxZFFQYvTf?q=aJXc!W;Y`5K?Fi? za{00seD7p9Z~nu7zjA3rEb{JgwbJ#BQ;A%7tQ|b_fX_T&@>NU4m`eK@2&Miuo&dVL+)~yCgJt_7=-Ol+x z2*N4zO{GhFUUKP$FTV2Pm&53dZ@K|b{DxOw8@)zoV~a7$zz78W+uD2Y! zcF$$Ymtu8F_9op<4cWPMLKkPtjq)MMqGat=%c&I=R-!Nsp}?gVH0&ayVQQ$#`#L*j zn(LFVOtxH;D^Qs2&e=^cr^>KJd)v2cyy=hL5Y74YpMBxBKc&o;jVpO1F1;Y;;8w3( z{+@T-`1#vDO#!U4Zq@R2S6z(xZhX@lDY<^t#TSKH!}V|}^ndHeK1`j_59ZsxbrW}& z?YPvK^oeU;f_LL@`lc^;i+$pnmGfWuvI>0SEA!LWys2Ae@j0i=cWAIQFBH1${QvRi zUsF})=kLW%+&J=!%R#dkRj!)tH~#!<&02~Z+*)RR2aQ;Ddl4-?t=fO zKD)L~&Yvu`IeBvX>=gN2jpu5|%;Yz|{#6>)T&k2Q*W)rL$Lp7iL@Sn^&yCbnw!3CG z#`~lw2Lu}qmYx5eciyBjYGqsVlQ+NnMyjEb2yyFD2d>GLn*M zheN)fqOaU_yY8iiaL0S2>GQj{PN0gZjcfk$?r&2dGSPau0&Ycx+44BW)yfNv$3T~g z3YUxU7+_om%El{)qht8~>P;(sI}5#!!T7u2SXJPZ`Q|kACDr4?-|{9**!<@2@efn%-jgTW;HNP#SHQ+RDtfNO5IIzmvq;-#HmHVd!`qcNIF@cI88@Hj ziNe+0_Gf?U%rRbyE9>GYftQJD#btm)a7%%41pxh{*HJ(%`tEnW_1W7#efQmWQ@H${ z46j_eE^OP(2^4T0@(>!t|h{CN%igH@y~} zVi;||{Zsg|{0jy*ZH z<()%kS|zCf*SYi+5qfe9GW%28u?zPM-{nc>`ot%kCB3O``}b_sec8U@wI|3#Kb*z1 zKvba6b8z=oPT@qC1UT-i>y-CHbA%IhS+-1I}_j<{_u3l6V(C z+LR~e;T9{FzZ}<69-EW@T#kle9L%px1sU*dCV3UBt|Tr5=TxMC90%{ zP<*Q)x-Td4$Hj5XfT&%z#Pk$?>zn`Z#y9d(iXI!`6NIfVJq>GMNGNTD11v4)Cnazr zicu>S=I#-kC}5hOr`yLV9HOdJpqvuTQB^slDv_@oT?ASAw4Dpa>Gb~w!OY3Gyy=Fh zNg&M|vwNY8(N2BY-Sc3)|6gq=$N7*Bk=?oaiQSDb6Y{%(ycbB*0T;bWO?Q zx4nEH4KV(MYvyRgnTlA(Up60=GRZk*z9EuPJVD9%vL+9HE}ofU z!ZMPBYas{wHP@e?=ebg4*fuoBE@qZTqbDqUaKXi8GJH=!IFMH9?lT)gld zl_~EH3efQ_I%{rCdC`;k{-ZKQKt0`dhq;XNfFKRmA8))k5q$J{xMNr$ZU*1C zMH!4p_oEDeax_d(dLvXAFD@@Y^IHu>4f!wLZ^a)W=%y4vUD4iVATpk@1=D!GGs@;W zL%-nlbAeOlTV_yubRp3BlUv^H1~X1oQxYw+jhBEF zdNTMUCr&BKVp!_cNA}@e7=&Wns=P+y+Q^Etxqfr*{Jnuuu>q$?Rpri$jpLm7J6U?V z;nyiZJVTZY$+_e(3M6x~<;rr~Z+r9WxjV)FSGj_QGO1qNqBu^;u`MrXa6zc}meQy* zP3EjuU%MA<${TNasI&a>M&r=yUwgF8m?zLgDe9A< zhyqg_gdTYjBErvq`cthQ=kevCuVpWxOM2-gFAtY|!AoK&@Z~Y#qHzEUf8jHqlDY|g zxLS;yoSbI}j8S=+ia`Tz0T6n$R3GlnJApR?Rwt}4<6oQ^fAGzT-1qB${#C&g?z8O_ zCYbK!7F0<}WZMy%$*B^b#HdbiQ2aM%iEpaJ&@wlbHW;wyBD9)(UPs|ULI32*7;-N8 zl#~y??Y7&d)~}?VjFHn5je3_qln}LAc0Qq0n|Cjm(i}3A3y7dgdgIDG%!KCAal9}B zMbttK5u|@q)IMx+Nw`<5X^Z%#^5GPC-iq^*9w^gIF#YzAefi>v7zX)1@J)SGhtuSn zmbft?FMSE)9#SjuF*$T7wfmiWzG;u8TZTo6I|HhdfAQyE`TS=-_3gXAMW!rGs7-nF zON>!WbrL^~#|1C?pbW^kB*QOUFi>9llCRwP`M6!dg8$)oB4`ODPTurKe<&AN4#%BU zQn!nfnpgZuig7h=9Zp?-bv_0Yz?6Y`PK;9+Uij#Tph0b6D?Nk~TGs_qL24O4;SAk~ zj;!XPD8^CRhz&mUf%muqgj-aV#!{hZ$o%{t-|_(!2DsRdzo1@{_Vgq;vuR7)$o;#w zsV3`IUW$RJ3C&{5SR|vX^3CkxP+qn41yq>hxQ`Ei@MfGh(?EXs)dT`jYVw|U{*jE! zy~zK{op(@YMsH6X-Yd$jnJkED9>?jM{$xDgITUexl7p6f*-^4>aXNf+vd+(f1~H^x zCQ59B7z8W>4sl9(%8OxwL`M2hP&uUmlih>6wo^V_$OSJ2ELo~8V~=p>Z+<;vDAH+4<-JZ(4W@z?e%qad^M3 z5H%^C)d6LzVF>fG8D7Z|tpqeuZKRlceDvhxW- zWVy1{D?GpMBfIG5Bp2sgPSjnZo_fo*ctvGOX4I{Kt48UL9MUr^4IulGmImUvjX}T| zBhvE|TH@jj`emf0j1qEjLZ(ni`%01CwCme(HS$#xji2bb-L*Bwzd59%vJN?8k`b@? z=9DwF{=|1bWxmy5do?a(Mc}=#Uukx;&dIR>>O;9g3PlG%a-1S^7;Y6Ps5MM;7pX7K z688eSr9^dzTNESVpCT3aOlFu|L&ya=-qEUdDGboz$hm3UWvP5y>1ZOCYIAF@5Cux7 zkYdX(3~qE0Uy6%}ZyCOggA3}s&hT|I=}o@3>l zUd`_NS#oA}X+(|NW+q7t`cg5uCEexp_|~~xs%d!f=%cDVC{qHVa#r(BA9c#P{L|Ke zw9V;fjBnu*ugeQrTK$4}rz@sBb-#Gpt;#1VqEM|=lZLS3)Z=KWA*U47PT7AqS8Jn` zyql|GIc`_NBt37suaBM+?k5eLF}@WHl&(y4$bLZVCw2v2JbQZT3fyqEAe3X2?sI*M z-W(s5LJW6Yxg8rBbC+|6?xP0I7~j-Gw#?%UGWmV1;_029gKv}POI10iQBO|sS-u(q z1otKml{v!Foh`!}(wM7sPPjiba3=Y7{jg==GMN%w~YEtyMD`NX5kB zY1B|{^aUkw9mPW3Ed?}AhY&~0z!C40)427yQx>m*GsZVIlZ`zBDnl;5DrdGN;BrCn zt%_+&zIUm!_*Tv-Rlpa?=VrNkHED)B`o5}Ux8%8;Go^tu#&;et8E@t@RrLk0<7yyC z26uJx*u|T=&qpw`;MKYXbXjk^1#*sGBP!1!3%S@i`OIqI%<=tK6o;I)I?QFSaN3vE ziM`icF!Qu;>MNi270%WZe|Y)M78M`kbGk=ORVCz-lB)vV%_W_Qkn^*g%XH;j@|-LU zoa1{CnPg(5ot(26Ia8I(=lm?IOwJiE=WF)L<4^w~Jo>}Scb5;Dznj1KoC=@_@k^@m zNA3DXqY5l0-xonxO!>3B%nvEw8GVnm-i)A5~SxiH^ zWhz}&e!V*S*L3#>g6~sx9@Q(=yO>aBkYI}n1@9u{9LYi_%Z5prY@LH3aFuh#D}SGK z`78eOB9qE@x83V=mz>#K6AI*NOIBmE+?#{C$hXq0NpdVBsK8>jI%_c{zdvRE8uMK# zDBDT4WQ&2dm8Gk#(CKE9lc_@Pb;E|wSa*>&?O#ZI3e04;tBO!e0-*D|Oopm0+`Vh069SkkQEna(FRgyfa@@i&2iriI>b1Hp$x6=O@#H*5j<-30k_|7`A#t~$doa0Oz zEDPDp84>08VpO3fcDtEXj&|=m=v={9?!C2Jp<xi+QzWMOTK&yY^2qDu=Rlk+b0|*)WX~Mj?BDm-l@~-N^Y>i}G%z2Wv*!<7if6 zC1>T2r*<)>9Dju~d-MmL@2pwN(muPk%_!rI0U9PSzsbmgd8&lPl;ITq8N|~|`F6S& zIg7bzE@N4(%ZO*$8bgPcEmRt|=CjZX;yJzt>rt(su2QldyRx)z!&tC+5M)aW*|a9W zLI%r7Rhiz+>?$+o+)uLaUI}VKZ>C#^kegp=CM!*{^=UnKO^|bJM})2^^U?3We82c_ zHPTzRs<-h~W>%c^s~qbZ8%WNEkp9<*t0ye?ewjr4<+b0ZCY74jNo8df62h zy^PPr*J*@f=EK;%iSM%ZrryG2F8pkCszg0WJxwS*T{W|6yLDnMS!qV!y0xmU|7?zI ztW&bG*{H1Vi<~Lpe&Ksmgv9$~l9(_m%Iea=nkE8b-_ALTf>K$?HpzJzRyLkt~hldYhe%fkvnX<80mR#wr5x zuAwncmCz6I2w3lr!c~2|ODiPj=2UW4f;CfVEGsiB*_zJ->e)=Pn(ZjF+REAPk}HC{ z*i>9qGwN!i%9HQfTBzDp?Jh=9UG$Y7ar<*=gD*pXD)c(sz7dzTbCPN$<|d@jYE`Q{PLJ6?OXkuO(rASrnYhy0PryA6|Z8c zOH@^Ck-KMx?|d^qoobaqcWWh70i4u?1`)J%j@23(FB8=Ht(skRWjya@t5DH~PvkUYJnR4V_9a_V^`ma2Cww-NS+1dIfhiR=~DUiGRm$*1_@&avP_ix|V=}*|l-bSv)jVN%$KB?wLBIyRfb9zWCkT*seTEY1V45&a+Xlk&KniXPb5@$Mle3 zBrKCtQIsh+Z&25z(OC8h%zU&f_w()ZCFIj|OSaVmS5?$)aPw%eWVgIhN~JQ9t<1_> zqwZec(>0iK#@pVASXexElmz8&`7_RU_ZB(FHMcOn$)gn2tJRFV3}K^mO;*{|wzlcb zg9)j~skkz48NxAq=3>Y_d{-y4>vM#)Z|SP!DpGHD*}5eNYNv%t3m%QK;;cu6tlv>& zW0+o>^_tlbWFg}+O15Jb(|Py)kX4O* zbh-1c$!yIEaar7;SA=AEOjO3KBG3@`;_9Oawg2`BDwMCw{n+(gU5{M3s@`r-Cc9Dr zyOo-xJCQ9}${j;Chbo)dmRYFG@7?jxoGQc{uD;~-(&BG4zQS%-C1h1tuJk5Nj;Gv4 zv85^tc1@fCFclRo86?wX-o#!8QixW>X1HsXtCQWI^ z+N_J`h`&X;4(J?R0@>H9?x7nCZdK8I=h|0fSIxFM5VEqQL~^XnuCB8&Dm&%f#+>rg zD=!-Id83?kOS54zqg{?C)?6BC^=17@M;E{CT*tRY$ai<$dWBKV3~jWkK+cM)vwX%2yz2d}t8z<;mkZ*$us}TJsg< z-MHMQlweyoh?nnM3^~5xmE{#t>Yk+jI|jjg z2zhed99&>i^e2ZIvk~H8KSaI*L<0BBaV<^ z$dJHHM##?FLZ*C-BNxU(oGrV-w{kPtW88LyI5o&Mv%{wLxiPtsx$#}qRJyb0K&X^H zWD*U7q>vi3)efw+w;||dg-r3Jqg~mwndAFU@V&a#=vJ@W9Je%sp4DQ|8_H93F7;MA z&*rEs)iq@{bOs=UA;~EG@E^6L za;ha;mJ2s+;Ct299_bK@1f6M zG-S7Mt4?|lwQb-Y9c@=|SDrxXTX$viqnNB&D_uf%Kb66>YtweEZbW>@IS3U`<9ElP z+>i_fXG|%VSo&`A+u1yM?R!xSY2{dz6x}YD#u%2$3pkkI;4EpnO!x9ZEmCC z&O#S-t5JeAoV%ozU9!n4Yud`$^kTqlWMC?m%vc7`h$GmM3tC7P!={{^;9E(Bx^Q!K zT~xR6T`RYpD~jv}4Bk!YokfEyU06+(`f9TE0!?uUmN3%@wi!aUrDdBxGa*XyHiKo^ z{2@3q3%S5)$oW#1^L3m_RkNIHp5@$FYak0*UDBSEX{`$M?!ZgeOtxTU*-W`XhYAx? zk&`LUun9k-D4{}Jd>iS8?=IQ)+=4qMlZJo!OC^|Vbyac|skiuE4B1wS1Z!>Jt=lOH zkKH0vHo!>Av9+=Zk+Yd4X9zMYKvV4sqYT*@mrd#-uk7*`sybJ#>o&jAkM(9XRvOOA z4_}_5b1Y@6Ro2X)kD*+&WP}D>wVz59=naHr%3Ne-FAlUO*@!O@@;czBK@W$wT2GQ6 zCTc*RU{H_F#LXzUE6-hFZA~?N7gc8E%CBf^t4OG9*2TKAx_3=UB`lIQicN1^aY|{+ z#8sp*r!4-~Rh_--)>T5qdRJ+cRsr2Q&xXj9+iDhM)-imKcjV?ujAih^8X8Te1am8l zf!m-*eA0zm4~kB19-0zx`^Fah*!gvXAh@eXR=aMqpLHumLiXG!6g`Go+aRQJFc`xY z%5t`1WngvGWCkf8e6XCrhQ<952`2$fQ@ zzL2|QqhdQ4tWq&8JK1uTQEluj^io1*hR^cpaeM~Pa!s-@stLK7mMfiDS5{rcy0tC1 zy~X1MuWF_Fzy=dkq^xq)P2 zZoM*G13!7LWw!xp8?CjVmfK-lkeZ}B)Q2|hxvl8Tw2gtCkY%mPgzREnZEa?lKsFhc zGdEd2RYqqvt&B9WYLRKn#+8vu+_K;{S1ikFj@#lky448QMx`aGPCB$!Eo9{jaoHst zP{MCN zyEd~>(AJ=g5l$85*~wL>luqgTW9sQcxq)uA^-u|w_N-M2)_FEWHju3*3)-gG8rc@l z7})Vu%$Oz9m60=tcDr`rrZ@8q_e&I+*w0Jw`MeZ74w&zx+q*UhRYlzZKf7wD8f+Cv zu!6GIkOw}yb=X<;l~pR6Y1gK;V8a`QQNkjbgJYMhn09_8RK>ijV%|llPVDZEs=V{v zUTeTYuf{5^W`!Gc1pD%AGTX7TZwB2wn2?H`43#j;;0bYIX-X@4!5XRs_7ZkTl!)!{ z_lUU`anFJQcMW%1*Q!U>Pq5mV8*hzdHJNObn00DhSD{E{C)>}0ZEUNL61o6o=6p0G zh`e>`>QhS9>7#bJoeov^^=>7MYLxS>2W!ScrNc})+P3DiIqc3W+u5es(8xm=l*JbV z#8Zd@?p?$I-IQl62Z$GRO|q$8(vYp`oAUK|pCh;!dZUBT@dBj`!Zt;&2`3zc@d zJ{z4{zX)0N*(x@b1$3QK?qf={7j4Q1>w$W&FV(ZmdF;e@#L;-)GSmS^zIrgGq*@#Zu9E#Y5a9`?)WmU zTW`&)%-nFxi}dA{&a*i(eN(p5{`!e+9jZ@#8-+gXpuS<7H;xW{0P{jx13SB8g@ZjRBBFrKq~ zw9E9i=`{DKCQB#mR@}4O48W%`_)*+f=CGM$IJ1t>#T^DfUnC5?UJA|a)Vm%X-V7l^7x!L| zzGM{KJ|Dr?&2YO02xHu~#{bnMKh~qjMp@ZgSE|f#OZ#$2d0SadrnOyV>4G;M@!1P$z&t=v9=cU z=vz$^tikNC9b0FnC66N87L|ds`GTcjpA&_mi4`9+m`ibBvltT-`)BMblNiX zKo;oXo>p_H%evJvM7bS#XC}2vxeSCw5Gva((w7U5-lbZN$IRaw(vO4MRo24Nlp1ja zV0Mf>#J37*t21l3xq8dDUnK|%ON_OJ25x?E*EVo=HGEn%_|Y(JY*f~_ul3u|TC9h#vo)0s<-=3UD>W;dR8@KyBaQScx!c;^{iNABQF19iy+ucneyz&s|77Rh^cY$_X178S%HxBa$&pd^?uC0S^Q z869KvatxZi`S&Gr4DKAt&!Ow@$8{i-{_#K$#h0TlE6S>qCZ*M^ZBw2tA{*Ioy_92F zM=T0gOu1u~2xZICI^&dM6(N@?AB8v!n`eY2-pQUjpNyQZTfv?ArizCT>=wTFo(zbC z;{tT~{}Ajtdh&boo7_8Dzhd*cORcFI?u})|>F_eWexFUyS>n!2wox9~dj!@XA|F{% zwMamB&Tcxjv~^-+_6a4pBf<;f5JD)ivJ^ON_X$h)}_uA2p(F;-@1A_Y}u1-yGZGs=>SY zlm9NBTV>{_)?X&oOu~2hIl9PZoBafG_9eY!W0D}dq=BPq9ky)T8(70f4t$LZ!bsp| zCQgmx`^G)bz^Iz!+DfJ^72oU*5N}7;dFC_ai*@AcfGTJi{-Iqj;^3ac6*=f z2Fr58WMg2z%Ap*&ONMexQl=_<*^o!@x@+rXH|?IA>Sm708Fl5HzhkB!_il^2tP6Bh zWa9A!g4;s&;&SBK1eUV{vRA^0FfG>Y%SRy&=!#Vws2~gwZzFP3F2pnF8F5i=o+Ccd zj<7b|aZcNn6ym;a7jD&NfE9isLT3U`q zCe}3H51C#UWM_Rzwki?2K3nh8Y#X_4wlmz(l{O8w6ECvb~I zTnvlq9=`oF#MjcwE1)8q+horzxT)4U#xhr3tY-wrq&)W=5CRT{*Rpu~o+Yt{5n*4F55yHoGZCuQJ@(wb%>@CR^4r-+EKq zx*6_Fv*a;OdK9DCHzVKoq#NbSc5gkPJjL#+e78}#$;eVg4q!r8g>SZunvX(Uj@+P& z@0j>Zv5G8MQQr093nShRjmNlQi@1a!FqYR|*m&`=^FB+m=WhHrp1Y9kfLj;x1LXS| z?(*1Ow4UYKt73$E{i@5@@{ES_j$Y-x)01n$VXuo6pc99zyMmYmIVKM1HEt-w+v{T} zO8R0evNbMitShEXc33FAbBF9VJO}Z}E`Y$H zyldHauY{=lLZDZaS8)B3IKzZZHOV3J_cfEM_ z=cT0sX4D@qZ}HQ(~zUA`BBJHiq^Lk!l8cWzi6KZwMOgZB4= z@A*5v__;T~;WZ}??^U|)$iCx84j^B5>;QT}5wjbcDVtDMo9rqOq+Aq@&@kPaL7qEX zXz_3h3ve;t6*}x1FAEi;T^zdwa?alE(@r_RSN2USp|0oLrd@LjQ_ho|LC2R5>8kB| z2Ia#Gxhll5f+^oR(U{DyU-@ruMY;IyrChT={ejAGlw!UCvhm;Lo2;TdlRe;DOgCh= zrFAmfF4^2;*mDQmo34(S1gx2r+*nrF@T7kCp1XtY!Cy0G(q=F~{=Lm56oGHO>AGI6Zt+ z5f@&_E+1@IcPZ<8zHs|z1M2TJ(>={_cmypC=p{HI)V|7mqo9g!y&UaG(73gaAbV7B zXDix=g)qWl8<$ELg;#}RMpN>hom-T;hZd9L`%0+b&bzL#LH9$;AH&& zHqr{#k&o<~yX8G^z2!Y`|G<0Sam&r`Bq1mvD|*9oGgQ*+ya#T(M1(_1PvN6o5rU03 zAiMR}+2q*XaAaYKo`rm{Illjd{5;6tJIC&yI=cy7&`mi)2HlF1kaZ};@ft%4PkkOa z9V3n)NA3s{!4bIw6+pN1ZaEn_0VGF0mBWw?i8?gBT6 zi|_2Yfg8_F6cuY+*Tx_>Q<9S#u4G}!wFl-HD#3pn?ielE5MNIPaXdGQfO59*y>0U9 zt&`3AhbADAx9E#T^cuwyf}3p**?JexZpx;$y)Bq*BO*atVN}u9OKO$5Z}+Sdhq7Fi z9KT!L+Pl4cQ)f3vf}1HJs{%PAjyuRY6y>4V{F3wlOFJJVOR=?A!kSyBVnij3TZY1- zi`51y*geN8cHwr5ic%sLXg_)**z8#4XCoXh-A(&qtu$0<9WGpaoA)lcX0>vBnGotFR`djV}=@TVi1OwoCv(RG=7W1jA%u9E&@*?%la{Pf2@s&E1#8 zMYvR?_O(z6t`9}5Gim2Jf ztM2&1XTJD_&);$T7w-Pfcj!j`<#+D6^Y+g|`4?~h?45UfUfWwg{*gDl_5`f+td}Q# zc$}o;xijC=`w-bo&?K@m+_I?17UDK`7jEH_>8ZdHb_4!)Zr!(QE~oo~bw1bEY(VcL zKG)N7g}WOoq0)vZCzH{YW`ITETR!a{g8<#WX#vE?F3GB*lm)%G5Y3L<{##%- zd{=ODg80s!o2O?K+*FI_kH|!xyNr#*SYY`pVnJccPW<(?{Va9xPl|E0`bKLDrG$g zd>V1NX12g=uN{16xb3!e8t40^Ah_8oCecg858Pxd-7b``Us2F41Q)hr7okB~2Ty8& z`2YOwcV9r6oqw&FX2r?R?Lqa`iQE#gCP$4_{Q zBX72^J$?wRNjP%Fb5~_hu z9kv84Cyyn#dmZ)w?$%+~0F8(=DlScMmG4~_s00_N1Qm1UB5@b-Ib_dIGIDn3&7)qr zp=eB?_fX!x9<6_wgRLpIOF{@IwTN&$BaYnl;wdz!*VBq}M-~Ib%LJ#Lb;Gy!g+Jat zhG068iJW(j=WZ6WU(Pr;V}GPF26P1ieM($4eM;sgc|D15UR4YQzUI1X^jjW;P)b>r z$r?apc?GuCqcv-?2X1@VXUk62i+Cj2CA@Cm??WRVU7n0u7Ajb)-GlpfE|{uQy1V&y zmb=xfA4|Q0cxZRe`WZIsnR4k$af`Gk#MurUc?WR}Ve3G7?Z^w_V<>N8mEm75cM-Q2 zhjoGH2%?L4jHR>__(pmOqlU)XF}KILc|_#4eO=wgo|_sp{e$%@;5$}zr^SFQ8^lRN zIdhZ1?YGtT&8zRKB}q*EZoELpQl;8j?5e(uVm)sN8uDRP4no zUv9)b5usRxtz4B}#nI_3$#z$dQ*bwkCof(FDxf9I8h={;AsWq;3&`xnBj`wLutfEe zA-FY<(|&HMa^AqrZDNwLEQryK8|S^@N`8O?(?lkofyZIL^-Zs31n|Z;zm}L=aMOv5 z0TVv0MCZDEV<{A|Wo_4UTZ0JUIg-yt*BNfvgDr4pQ^|xGZaHjOx^Z&JTZ2W$#SB?U zAnr=?fZ#*>$&Yks|L#R63iqUZeN~{Fk;WGy2yvr=xD= zx4!xHkACEXC^0&eGx0sN=%(b4KRG60rhV+#UoYGyyBlPiY@=Yl#frHZCTUS#`QU*) zz3G|*dwNq2jI>&r-t%`5o7=cGEssos^=I6@9c?72^g?94i0h!jCfiCCmYhbssu**cdAh7d19D@Gha zlsi_zc{PYD7B54G=38DIxP^GdclP2&T;YS5G@_Lkw+mkq>r{&8ZX%O*5WdYZs z;GVdO(e9|_#FY${;J-Z;?+;m#!)Qt2y}`UZ1}D*AGx6Ix?=IYq0S`k0G?R$3c+2#vtA}@)EHSkrSolKt)kX*M$SOlHwD&RQc~N;^LbvKDzPW zWw>yOdYu*Aek{6ha|7K;_x1ya+a|Hhm9U!$ojY#7{jS@;@ZInHB|v}oFTZowonK%S zJhorF{fl?p{R(O+0rfCO@C2LlwMuaLF}?c&UhR-dhpPJ z-gM2u1HI|sp?yv~9!{ffWWFmQKaik525~9Dd?O4YK4Lz|aO)L167k_dl|huEaJ6q03Lr<4{`&{Wt4)-SYLd;~tw>@|8-42f+$aC9xa#tDa z%JBR5@62RCaK+6v`}Z9z>Cl1wL#eoJJ64gp^ydxx6?aDK!psI7)FGVLlZ_ z+b_egs2=bwxLx8D!tS;zntrE#JcZ2QsK!UH+4q_gNBEAtI?-%tBwNza1KVHs>SH&& z=33l#v$CF7+gyB{((gXQjq_HA(u&S*yB*5y=|swk71r9#Slefd5|J&Wkj)kX*c~dV zAJ$=lk6d%e>6*ib#;3}=SA6GP*JsGD6B@O>Qu}hXQQH>u!EBUVxE_9O4Z0(irr()@ zav_dIY#rOnln3H%pb}wc`~9%6emicM2gS{|T{s|(L0qo38>r|bC4yOqhyT_-Tk)OY zR%Bw&E$8j0%Fh`BZksLjqpmS`Gr5|_bj#FQLha~2m$#ChV6Bv=&D*waBp?yr$UC=> zgd1%(o+C}!C@g8-s_pOy7P7&xUqvXr8+iraQ z2jBmm53;-a zPpMn`x`+ywzUfda3gv?awn2-Y@T8e0G|zlz#L;l+)zlvdaI7LOdt&!S43W;JA%&EZFl2r zhO3J^9Ew96NrRCglK^(O;fh5aVypZpbM#0~hmRiVO)g&vzL)ZSeFWoP1O-cKf`=fZ zEgDwWOUa;*K|FhLISZ**e#D4JZ@%^A*ag0YxE)jF#q)5A{I>&@rf=5HYlcs3{}GOc zsM5;>v*1mi6)BUN3RDEA9Wl@Xx3)$+{I_=OynR9s+~T{8nfMJ7Rbjw{e=4^u37p%! z3eQc|SxmwgkiYSbZ$R$bZhR9%B}%V=vee83exedtgxbur|c-?hxyy5!Szc#jq z_HJdIBoCDcYZRKNB*nLwmJMW&Vxb0Z#o6}Rw(G#s2V)|JcY4#4Yhzi+&Hx##Uj8`R z@HYbAUZxkHNzc^xbxkbfdP;8NmO&qsHWHo3sAAiJwkqVsr6?vo+pg!(FnLCN5R{kx z4~~4#4(!s5i(|E&+oniizPS(>4I_o~=3i5pnb^K%BMG-{zLA3ytxHMUV5xY_vK@RU z2+AFT)$ zHoI+GSAu$w^4!gE#G!-o*@i~!8YT&*E&FEVF~}Y{b}i}fwIkBlbGfW<Z}1-B-o#dm}Fa0m|H^5Qth z47x%T_|74U`1bII5YLoDFKfi*sRj1Jd@J11MfI9s%U%71#XF^Z*MayXhD{i~FT!lYjWS>)=~q3GX;GrLd$5NaVBiuB_meJxq3E zsx}}I;zOoosG&G-_zw5hjA|bQjtU)+%2*qYma(35olBUsEoD z>Pb+~RgWmLpvRq)GeEpE#^Rt{N@Lq9vl&zw6{zq!CG8&e-*(}K8@`9&#(z(5h*Y|m zf?Hd?{6Sp4G6yO>h zM93pEf?Lfo-%SU!?S1-z&+8VG7e}RmI0+?N?Ff!i8oqbT?%qB_(i@n!@s6#Vx6e*t zZW4ZtS2t9zhpTM|#qGlF#WUZ4y`&4bgOlvJ1MY6vT?V{) z)Zkk)IMEj_k0yK*X1N=QBY*o%Z@%F*$4UH~fQB2wuf1l!dy~7iHy`e3`5uBB&7xnE zUBO)iBqnyZYjblWoGjSE-M+Xmu7oNYuYiEks`>%vF!-3zxO6Yim>s(Fc& z;6!1!)&_z&la%lsok#{oM*p#5Hp1>Qz*J(m_G1QVggeJ1=DWC-aNBf@Mv*#_7wmH!spJ8;``)7$afTQ_f;o!Yu()7+L#Tc;;x zH*Zv$ncASll(y3BYAvV(nKbyLpdI9 z3^0IrIN(08Z*Mp4+uvmFKd_GkcdMy2A@{S%qPRcpR%3S=m zI$gW2141sq?x5RR!WdC5xaHJEd44;!h&SV*-C=TpF8?jW;k$$=JIaSBw)2^|3T~4< zH!}&`v(pnZn>TKm+CZ9~T)(BH=}qfMnnisY0BGqd~k~{wn7^n+6rK{>jVRz2Pw=Uh!@lb@_ERHE}7I|)bcFefW4*g76!~54AJ@o!}zw5&vd>;!=-u<>4j~?Em zp_1sDXuxF0Yz#TYtXMPmo;|l?Zo(2)Y%3-yeD}bOg+LsjNqhG0R@%L1mlC@nu;Jj| zz5Dj<+rNMRfde@61N#qRWO|y6I52<1z(g7KD=s{HO#o$g6s9gofnz65ESN@Jr^@$b zd!r+cigLO0p%|vD>&mQGzC8M!?cA+9ozh{bao?_edv}sD+>93N+CIB;+sux+EpuC^ zx6W>!o0*!O-ZZm$V$0OV>B$Y7H?5zVSU0(G?WPTDlqS}%ZqwBh>#qj%hV_Z7H*Z=^ zk}uC8N}yG6SC}vWuW%?Vg{ZdKh1dQY=bicXX@*AsZwM@oy@nwZv1!5$1Rcv+_oZ<{X5IToig53CdQ&qDY)glGu+XgFH86Zclel;8qZCE zeR~DBnBKK}r_;_|J3`*oAa2U{?k6g55EtLDVmCR!tB}oZ#b1}^Dktyi-0|qEOm~}W zB3YGmKD&<20X@g%j>t3O@xCgE=Rl>)w>@`;dwYRI{_ArE2QHVP(w2I*xg#=(S$Fp-!)>>oyYyz_i^Fo>3QO#s zao&PEFD40-w0%l~Y~m2$7TMc(Yy<0U+qTWk&28PbZFX)>X^zYtJGKM*?%liavk$Z)H$a^gh=G*y^H5FWcj4Z% zdpmsZ+_4q7w{M$KOaj?krZ?fXr#4MYPHddmuzth3wd>ceAwl-qH7nPwzKXQ=>J?Rj z^+Neo6UmD=Avpe9Lx(22VAgfvAl|reeZjEpE}jD4mFE`RBHM6#z*{j%Y*}p3&f|Rf z&d;~~N!A8lA2D~C2q;4+8ceyjL?a9fMp&}HZ|+?~fr`07TySFp2a6KKH@!wWkqWXK zxJ~xfty^YibAobk7v=KexNxFyhu|5P%-k5PRjevNrgQRjkh`Sj0y;Y8bQjLo8IsF* zyeL=K({Ov-P>9E9X&FdWbcc_?MeO1AwsXfWoWS;N+tJq6*}2)7*)7vE)0?NKCO1!R zg6vHjHwf-^YgVsaeKl##)vH#oT6y)#tCZHPx^nf(D@X_=a2Mio;qu}dQHl6uieC;i zA2E!$x1G0F;J`M!@IX|2cV;vNdc-G^*vi8@I1%5(Iy@`7sNg2*j{%dC=uqOhW5A@O zh)l|N0wO+{oBYaMv3^j0@J-B3G_rf==9BZ^YZn!s)W1m(!3onh?XPc!W9}F#5!r@Y zd=CxZ?vEiq^NsT+AmJ~3b|HIuW@hu2EmPCeY&UN~o|(o{v$IeR;ycu-29 zF?Y=3?%XW6%c9wnj5I0iR-aOlN#ff>fsN-TGLgd$xXbTWJS*Ado0tT+VOV5OZr(h( zdD>}e8b>rcLkY?eloO2O`db%Hh$8+>PnR%qjI{+7^H~VSSEzZb;SO(|FM+eVm+ICb z-$-)4opF!u1b=1T$1a>G+;E5YF7pF|+in}Uaof0p@Y`E9PZg&JAs4 zV{XAc6gVCT_cP?bckGb!mgffRO;ejm6O&U)++GaV3qjOV%Nm^nJMqm%8@Eb);N}Rwna0-Dqfg;@Hs>gK&x` zx^*%F?)LX)8V%H1c*X=h6i9rcS+VF%wjE0RcZ_qFp>D_?3QOAWG_*_jV+}WsybT_A z0Xe}moyKjuN^i0_Z|2h?T%6dlanmGe117>ylT+X>FFrfRI~W(vSPH$&7zmGPo-%|Y zXav)V>#qlB{Z(YQv^8ViI_uhF$B!HZvSX}eVWl65)h#5}yL8iF+`ZgauJ?HP<7!O> z#*V3HzB!#oMHfM!EqaQ$OWcTIeg^vwh29{Wq<)Lyx(MxdY=i9Ctuu1lxPpmE^tB1c zjoaoAu-C3$ef8>91lg-rt%}W6SFO0}N|L-a!c|vB*uCQ!>yodkI zkQnXm`~aSqglJo9yNOH!=oaqMp$xb+U@`S9xZ-I~ zd)i|k^CORV*ux(F(1$+sVGm*R@P|F(p$~Z&e{3Jle-C{)haUOxhd=xw4}0)KANnJY zer$q!^<C|1C|e&Ym5f&vv>IFA7?eA^Dj3K=MD zMp1Bha)AO!+XWF9ULq_)0*2B9sWKs6nCAUJ;wf@cPR|5t>&SI$GmW5XIEMmN8bvlw z5h?D)tSzJdjQ#Hc?OnX_G>S@pguj`Y8Nod{ISJV)4z1y~*REZA)fFqA`fE>n)MFm= zfFtB1~ z`s0U1HrwuZ01bCRx%r_X5#+&Z_VTJ-`m&cj|F?hpH~vHU`_13_joxE3mG={(5=w6X}moLTg20p-3ScxSL2I)^})dVt<)kjb0pyiKTcPV`a>Kb!{sz zo*yvNEWYzDa^`ys+>lM;Ufi7iv|>5lb($Z*XN&C3^u;HMwI?>{kD={#Yc5}L1!Vt? z$39lz!ttYj{O>;TX}|e@{qBW7^ZW~c?)eu!>G`ktN56C7&;0HMKll6#p7i`zmYi3z z`LZvA}S8J!@!LPa9;H6qADsxo~;j3h|n6du|<2pRy+` z;nSG@;aU3)!J(dI`c}7MZnilrY36atmyja(E(3zh8ocY>@BGr2@1mpm<~O}wpUG3e z&6Ytxr6IUWG(te4893JUBmbjcc+$XAgu}pd$hCwR?u5T4$5A5Cz&Cq6DVU+J?VE3dDQiHs5yJ4cs`({RcDFj4bCA!*PLl{~W4dt!Ng|&NSS5 zZ%}7NI10NJlgMoo#R+c6X2^(vBZfyAB&CzxcEst0Pi@?^$#CPd38|m*)Tch?(U2XU z`aw&UJmM#Q3UL43j<-Mk_$Pkt*saev@yTag_wlEl_}J4=d@|B?ACKMRpFsYXm+ySw zBOc8uPk7?bur7twDY2-K6|9)~C<38D8-9YOpzkAq?o#kdjR0`8uD0$LL$%$X>$8!H5G zq_|Te|FlLRdVvui#iB*1t)8)5pI*4#o8(1QkuA5)coA?jHVWCy184{pvgt>bV1)6% z_*=1J#nYeu^hZ7Bv33)W`pLh8!~T^WZ+-glTc2@V12lX;{lv#&d)>#Mdi}?M9=JjL z$!o585F&E?oNiCYv?1S|*O$hHRx+(2)@@bA@uFF#uf#Qivc3#N%JPZs&S3 z-&KgRq~TqQV9T&c#di~yl%K@Iw|cchp1XX(pZEla4dQa%nQwXSvYlgf&TC$M{L_E> zDI7MlJQ_XJ&)8n~+Utl+h`B*rF*jSyi>Y-TLhErHB&l69N$n(ZMJ+Y1}qs)k zCgvsxDJl3a!)x(#$#C9Bf^gpLkNF#q#bZD6v488S2XB!V!L`Io^Ta2^7`!e|e3HIA zx_;t6{27>rZ~Je#@En3GL}7fxe9ME{bDM9)Bo4TNQgDN5ft$g~Jk5IfH+7S|w&9g_M7=Ob)yNB#llW^NPAQ9ZW+19S#uwvz^ zr#}7Zk9o{vP#1r|O`J_g{j1yGhR<%WE)XjY!E=YVM%Bb8*M0n1Cq9XHf5P)#^pHo$ z>!0__PyR)`_15Xl;j=~d>{R}xCWtHHB8|BC7UFWOcHyxGt25nG4Wu;ji4a%#V&`qR zJtio=^CCfRtg28aDyR5_aS|;LRB*y4GKe1tMgHe^zbl~I?a8lv>8`il^kzDj>QILB zJ}{WavF8@t8Z!~o3Qm}+0B+_h*z%o_-}(=~9f9GJ^Pc$pS3vetuKOff#uCpw9xqT- zk73}OB2WB>Kev8j^M;L6*m8Xt4&OKtlRk3c8VCo<)`cGe8eBMF2{Iu*1YFyj47d*+ z#&ZKVq{1uUmbDu(!_5Fqm154jIC8Ml&?JCEH>s?G;m*M~4-bE;fP6XI?6Bpn<*?gm zEv?Uvw+9VmFch-4OmCgqJUhX(SFDfPL~msMx`{&eGk!#5pSR?}=Ph~k zkNwn>e)qzExASedWTK9M_*l^`K}b>TuN`MMyfp>>_2xhPrx#2;{wcrth)4eoY(aY9 z10VRj=lpwC8g85Ar)!b)V;{Eqy{5K!L$L8zQ^2Wl;bl@Be_9Qs&>K`_0jOe?D5Y9A zJC5%ttwrALn71CFF5kkMb+9b&jx;wFi>QL~nK!)Q)e1$#G;s)jeDmbCKl{@Y$B$^$ zJXiq=j}dav-)1&@hCnUk4=JaQmm;s9&suz{KtRn|3IaWcFw_7sa3;cx7koPh1nssp z=%x`j1|?X#Q5J3l*{lSZAsU!rvO5wvG5-W{v3O}mc<C*B#UGlhp^^9j;$7?hk zOnC9@KZX~_@%;EN{3{Cl$dCL8u9`k2d{5xT3*YW#LICb9BIB6L3~x#|F{1`s88Wal z+@J}df?KPSgf%p?(nSfxl^VX8ODQV@%HkIyn)u_H$~Wcd>i%)lF4puuucjc*5y~tlzZ!vdjPPfB8!fdB8&jCR^g{pZlFxJZ0xw2{shnJ@Xju`BnuX zXcgU}*eCtx6%TkMqu|9;hw9_Ofbz#b{_(&2JHO4+6jq80X#JRtZBClfW-GqKGq$PA zckRN<7q@Z2@F`!+R?&wCQpWMEGvg^~CoIMHP~@G)1R=K#8TNM}n_n-2cjH^8Sfe+#>ykT;Rhy<*|jg|PsqbQoo zWNd+u7}`d8Q;`jM4!DU)G*J>$r)4qeZ~xZsJm)#j`S8{9SU%C7gxx z=E2|z;g36J#6da#aXbEa0Qj@RCJs^GQK1gc=YZ@PdXTfgO`@?hnS|Sh?BD-`m;L-d z{>Kk^(1T6rqksHw|LlJ(K3kzk8Q`PiNca)2#Opr(%;TTHcMy~OvzM+o?-7py>0kIK z|MZzmD?IzzzxR8;xBQi_+BUOg+qP-QW~DekW77vD7%^j_1Gw2$LT1Q;B+7|q;fEx| z#W&mVp{)yV*lPz;0?v{~@lD_z{mZWNR%9Z?J)r~NLvR=8%^Rd~-sMLOZ+!i0wDwhK z1NtXF_L1Y)9*PMa{X0hO3Rq_21vfDXV0&QMgU8|9We>R$+NSUUv|agANTikyYXA5qBsk_xsmQAzJ++fcxXdD zf0QY{J|-YJv)ha1DWxHVBFxUpXVXTo*4XH8J^w%9W$8F-P?fIB-}(nX_b*;Csqt(g zY@9TzV;iqNdg>(_3D1pi?2}J=>Bk{Br$Q!?;%(m z3eA{a>z~0uptDQ9K3^!r;aiB0ci|AI;c0g?g}8|Ay6};Fa~kIeZnj*m=DT&?O@+2i z5mmH->@a%;t`7T2WxVl+AR(@dXvm=gQ-^JzVL-FeDtFq zJsxWxed$YID*p|ydQ{}XA(%hiMKW6zpd@$1-}CrsKwaZAZQn5>Y!Yp$$X>O2&C{Oo z%pd*npYYuu`lv_#Z%=sAKfiF(Qx4q{-N!OV!|U!D$370qMB8Gz;rsZlIPYJ)e8+CH=w|Xt(-EcXhBLR^srzea%7Q7(K5&YKTPR6k;n=jJp!?|57mSV|SYI^$!4U;O;1=~nV| z#)MA!3L`-a&nslpFrw{}g72nhS@@385`9+2ADQo)%2C)IeM)__vIHmi@59#}25?5s z*z(T)iJ$lhAjUcU(%QoWj?X;G>owv?%=W+kpTF=xx<3zi!0NT@Kzw@hECf`1!#!{W z ze-yyeM-((XG9z!TzGLOE<+Cq()y2Q~FaOnp9>NG|={~YbP* z6(;Q$3mkUqn~~x>`jjQUfA^^e-&dRvGiot} z@-JSwDIEBDOP=@a|9A7;R^~NgYGZ1K$b=*>9xfb4VhAmUJ3ySyVhmBYQF0J%&T!k? zcM@$=^0G@W|LLE4+!&hkA)W#LUi6|DsedWNt6St71=HmfL3#c(;K)r$)RPC&SgRpT zD6)Rj^Z(=j_Ya@�Q+`H4Eo4Nb>i8^|$}+^bI&{2!`f(#YK0#$Yl%ePdyg%?N2-Y zU*qV}PY{;;(ze zJnF)I(AisXq8f0hSfSu%D+z~KR6NF_n0||Q-N%D?w9V5gT)blbx#3#@Z*wMbe1o<4#+it0Y)QV+jd0XMV;^igm`kyMI4;FH zMly~lP|mg!%2q(bjz954PTuAjCo=31I8=`~d>i7EoKsOC&jmVgAV8*zthT05kZ?!u zs&whnQG%oUq=-VZzlBc1X{VhwWXKR5JbGC>fWnK-jShk4?etz?&8Q=13A0Zw4&&z9 zO~U;?feYpkeIn>P63pgcYj$Px6Uvlv?`v<(d8C$)IGmPv)$fl=(+a_7u!97!&t&%BzE54)KDB`B# z4sqa#@*V3^^7k@>XovDPSTEu}GI*&GIq8a0aIt9^e< z{rdIERU&+Odw5|&0B~?rCzmw`KEBc=jq475g2COEi;Z2AYGIwZr9GRHrP<6EPr2|iUaC}SQ*J&07bZYN;buOvxe(c`m$2zL8^^B)kB=rRqZ53{5yGUvP6e;H03KcOc#& z-})S0yzTQjz9rtlcW~z;eM7#Ri+S6a4k=dZT;eq-cmLG#W#&%;+pRnkTR&2je@((e zTVm$h@vUJz&on#AI${Uw^7!!41Pb|yOO-39L3=E?#kR7xm8o(zzdc6$jyvyxG8@Sw zeC8|)5(qB7MJbnL%CsP&W(+1z#yt~5@WSkw_J~)kSdqCcqeqQlnmI4dNMUet2>lN) zrHg>pK0xat`@u1$gH22`<-n3xZCuJ}_ULhAMvuGsrdt};ug`f23d2j{iKm`jt5Z^w z@lOIYU)&^l@z#!b5)?XnYx@kDlZ~x2h>C2kaqWOp8@In;@S}~VJPX;aGPiYDv=#2r z`;#uH$Kis>8JIL+EZq~F{QZD;$hs3Y@PiA8 z22#W~+xS4CI+`ZLZYv)V;`#JjWV7WFMf0t<@$c&Sju&qax0IVLh)cHJ7q(QY zob1f~d-weG1M%`-UR?3Ki<#tQ2*R`)hX@p?KsvD${nBSNXiG0j zK>#{TM90ba;lDB`mc?^Vo)-95Xr_6%+0j)I>E)bXSHRtS&wV6mc&0sE!q zUx#=QH=tBh)i6;_Ne164q%Aj&dg0a=+1KX{#Orbf6(Qgko zJ6Sooi?bFj&d#n`vnCwVE*|i>3+Qt4eVb4~txf~1qnVIct!kBtV{QHpk_2#&B&?k} z5*s&}+%|;z+nuTyJ5A&4dXU+4v(zpTsk* zqryJJ=<%-H$e4dT@ZhzbuCqv=sCM6xcPwA?yqeeazjFN3jAvQ$Dqq&Zqn0{iw!|FB z)^^re+$`_WI%8w2Or&}fI~_7Mx6RntU}R?L%GKQ%yX=h#0j~_)7UZ79iHp9Z8Ez6E zfm?Gk2oKOiwz^WDa?aA$#WI&t6yNbKJce&8gchcwU?}qRJCty5Q9xQQrBJ@frjj|a zLnLZW!mu|-m3Q78aCy!|VL=zk7vCOkoLPEW3QN5H=UZF6A$s}j(5Jaeazb+w%w52I zn{2yO6VJE0m455ZNfd7OA)5<+1qo)_AHNWO^oj|~+B4R(h>;8HaaO2tp@E-}Smms< z0Lc1n6Pbr>eA7fWFu}LB+@c%VHgDcMPc8*@{;h~-{L;!f4IhbqYm18Wf;xD3m*C>g z8_=w`*1NOSBlQ72_%w*|to3s}(y7Hnp1dN~OA2*x$xo_%6M>b?1CD7AHrR zm}U0PAWS*KlF AeGBzC@cJ>eiYk0)d$PUl_J|8^7|+Pgg7TYg8=6qe1~rd6?X1=d*Pvv$aQMcQm#coB z_`pLmg%+zI;e_CLyhIqanHtn@82s{1qzl~VMlz>insQDy)jXo&u+B^An89gq(UGIA zr&*6-i+vwBAgNxxdX*|wA||A`5b$yCIJR`zD)rkD^mJKJ+gza9#Xxdxbf#U#&h}Z> zm89WD5O1(nYnS2RPTSZjeM6h{4eb_eXtm(2)@g6GS+Krk`kQSRzSV5jE9I-6Bg50E zenab=aP(WJgR#k3WF*d-J3EzXF_B8@*h_0J0w&6ZxYzGs#KZM9p#nB^dUp{v8!+)M z+~hKDxW2GJc;HgPjKTu=&VxG)3o8C%rvMjm)Oo?)ARLg_xq~f-T8w^2%!z%6Rj<8F zDy+jgV494pufP6!%chO=7&++qi}2^rIJ|9b#)9Q`OT6h;s%NP&og$Q5paZvSu8>0x zC33PCEzWJ;s)cor5)#g+-_DIwXv|`JyOi~eOFI=$IOUX67*v;pBc+};`DV6o3EWjG zpQ*=ttYdbh+-#Xu#N)*`h|^?L=+ZHVf9aN!~zS11Ev1Ev~h9OiXB0uQ6Q|JFU4Y15DYTf$wH0fYB!6B#~tT zu$Lb3DBqS)WD-s<)8Y#1gT+WVUCd}N^EdepfDDK0y@aFU;#(HQ{0U8}E``UcTZ@dz z>$TXLeowQ66UjDP$hPFKS(8YY(ecMteDe)AZFmcgy$_MkhV|8Nyq-52uLYm1kdpN* zWAaS$;C7zdBksl9YqoX-w>gurVa2)1D0ht-H5?*|r|}5(2;HcqSf?L)1r)q7i!={n*2go_^MuPSWgbP{<`Xsl#`>)@>ho?C}wg4S)Rc z$ANnk63&PpbI?a(R_Jk3Z1oA-A8n%x2jz| zfMfpBduFswezR@LnzqSrHcwf5Veh*&0#9BcQ6IBWL5YE&mK~Fg7+a+#2UDhDTA4!8 ztx7M!fPQEp?qVqc-#$<%2{&fkXm`AXqoG2MZDA5_e24lSwJ3rA&Zpmw@4(sQHz(eo z$ec*L)w2vvF6N@9J^R$sy?fq{3bsT1@V-6oP^C>>>=?dddX@^Cpx@%#2L#!%G0RB6 zyD+}FomxDQo#VFb(32z9fr?{U>)kgC0-ERAWD~H!l-Fo^qf=#|N@N!84L96?XI7*F z!vKQAhTd_4G+0hgFD^aHgMxW+N5qosxfJ$t5oBCD^$PeqMOIGN?BdE*WJaBwZuT+ z=6qsKZJbM3u(aWz%9mvhD^)$Rl2JH^FsyFisx3$Pwunlo--YoVbtwQw6KpcZN65Dj zH~%d$FM{u=co)I94!gw}sfRxOw-;>fkZbkk+BH_F#6?alV6?zypy?FGmb=8an_Fql zB+56=+pvxltU9X`S{j3uUHDv9){DB#z z%bsNH8e56XVY#A3%e(KrkEFZ1?zrQ)6KrhKx^vv?DGXrD_m2y@CmdCx_`P@CH}cWP zA)D=}M@J5SWW?=5@3>{i&F$K>xx9XZTD58cv=eE)T-aH}RWsS{jH<4`IIG3X*U)K4 zEpWSZb`a0m5oA7*mhEPRag?%S4|8!6>rJ}mWt*;~^(LJmX|-^Dn}zGzq`rw}w@O)8 z|KU`9WxiO6(kC8uY$<9EoK^YEhaVY^Ga>(M>;wvt(y0Dym)g4 z*1qYw#_ZgLLo7-R-yU%^(LxbU!UYmr>8AK*D+w3ha40OTXBg(9;68lw5+3jkzzBjTRjbo-!CGiQ2*nDBv`SrbLAN_gl|6}H zI#A+-Vked>LzO0;Xgz^l`28$!oc+1bX;GoUNUzEqsEkcZnI z=E#mJcMRO{jcmKNrcKJKR>^Ctcj#vauKR2pa;I0W^57#(PqpNDoXL2C@` z4d$CUcy4+Vvh9@HkIh*@aS%7Y!*#*sPfXehmvEyz5=beKZ?h=*Gs2aN0 zZjrj?;_kPfczQJnwfo`iw|7_V9|yW#qrh#DRISH3!4IEn0NKmnI*u)I+{tw>ui2^J zB?BI2hChsQK4IQ~{IvBH)u3G6N6nIEpA&^c?*rt%|T< zv}dWvpuJ*#@s8&k#5un#&BJ5%s@_p$blPahMxSV*S-_{tGPfeYpSgBH}Jm1PT zLAmpZw?=cE?S^k59)+8I@f}aLux9Ho**RIvw>k9;tH{BnMyoE0IY5c=K|2%2R#sv|4qLXK1F*Q`3>@44AjH`MDPH zFtd?muo|hk+ub0PtF_?t+o0C~q(FB$*=>m8^MNfwVlwZtYl7@Q54X8(`ji-MY>Deo z0(Zc7$~t^@>y&jZlULzS+9a>0kVC1H>~B)}j4F>{s9epAF+@!!O`Av~&%~urX_~)^ zwct-cJm_UQ3HRsW7`{E?vM5FLGQEV$rGR+g5uBt37RBkeEK1-nLW?4LL0od^r=gyfKYWppQaMtVy^TDJ8S}dVf7+4x#xDj1OHIzqK2nj((u$m8q*fB&VT~ea7+yr(@W(~+gz9(t+IE) zC3QmJnA~)%5j(b@waYZv`Dp_^+uVp#<CZ%bF*|TwSHmWuxiZJj zoCIM(_>Ss#W;laEnynP?0{JdbymPE$88{P(x4D$uY!El@bF%Kg??KnME6y-vgZpL% zlq}lW(IsG!Mq9B)U}I`r)fXlVHIvM@3APx2PF&-+@JiwbDo)?A?c!bL84OmP8Qg%P z(`+ZnPP{1r1)z*KxnPL&SX*ufVz#ENc{ zGAG;`*q2GR`iY#0dZ`7P6QexLIWgs)WB$aBKL(|!easO#Z^n+D+duyB!?$;A=Ud5~ zaE1bI@g02gxUjdxTgn~4?QDshNrBx;V>!GS*-pHLwTBzB)r|7&Y&gGT=un5a<@+}t zztl!afC&~C@4&anc+}|Fztw`(+yzyrELIpXw%Btat$ogR1{BdksvLqN$_nwG(ZZhf z2W}53H(R2kh!GsHrd!Ftdpnm;!eD!A2RG*;lp8C8Eonaf=~`D_U9x0JJAIjrDpsl1 zxhIvWFm%|xHV<{Y&FUa6o(?7Xj5beshLtHK(Zq61s5I+Ja(as*ml8~<3uA>Kj#rU* zvsEBLe5<2bP%qPl0;S&$Bpb^VU~Fw_K`5-&#l$T~LMYO26J_~t{E1tLVd}WYO}~Sv zN|;t9sn4h1`OP=)6r43ebK;zdn~mlWf9~mJ&p)&L*=LrMRYHjHi$k9=0>9$Yqy(B0-|zP3L}yPz4N-?0W~F$JE8f6r#M=iQFcd8 zA%{d^FIB|t0o=}(n0~uBH)-6)w`4nggV}C|+`)R``nEXl9`BOS}?!hnwQSka zA)Zja`gv_r-@0h%7>f-iB%X491Lh-}s5|J*3;-xdIgefheCOYIm!VTEfvJiv^0*D3 zUBu)6z+J&7$rC7; zq0VLX9veLoHv-nwnP7GtvlT3Vii$P!)C_TvEzr?#8W%%69IF@Q9&zcnx8R7k*Kco8 z?6e=J-(JFPj67zbC(eSa-x%c^70=Ue?x02aF3_ClG=YA{@NFqF3enG=ZV(@~S@a>z z1^>$fKK|q*1y6*y^1R_2b3#cpVJ*G|cMRV?EEsx|V6-xZZxip_ToSx!T$Y-Y8{*E$ zWKm45X7%d&nU6p9Y?noySgzW+&8NQzd8U-=>N)p3<&l~9jR1y74bgT zc`fYmf8h3bxY6&{S(|LKf`;E+hZ4<(Z#)S>z{U^FJGFXk7u^=yiN}{KU+=a@x-MIP zX8qR2doimcO;5T)g$j4vbFYoyq$JOtXLa~($4Zu4HGWW*W@|^FYio!{a;>6>7s7Xt zjzW|$CQ#`8iRT;GV)=HSA}ZlU@r_Fo;y%(oJsapt4rp(eT8v}HE%p(5#_FVC5(z0cXe3*B6AWCw}#(Q?$()a!FNkH z{Ejm*{Z4+1p7BKk?x%#Bz8wQh01E9oF z$6VF9lU*PT)3!&MiSaqyX1pk-nTAtUSY^a`MFucrZj7lz&kZ-xencZ8x^WY&U7# z>6==lZ4PovEMT&x#jID(Y2Aeq6b^11wLSIxy4U5b>9_T({@cDDu=VSto&Oo|_P2v} z{ipYak88EPj^kVBv62ZTn_hDr6VEJ@Gj*ZWGjMzpE1`MH#kUZLZ$msk-+*q2yMa7y z;X4NLc>VTKLiF29IQkuxaCzov0cEe>BH0FdCX7f>=h7z(#`qJx3fpM#o3~NUpxz$+ zwyO2Uw~04RV>(0wKlsM zQTm7mi+wa#LOJXbq!|kj#k7t~cLxn7^CKHzF>4iQ1ci$ez zx8mGn1u|0s@oquxRPx3vM`oRVZe9BxcEBc{RJppz_LeXDZ~to0)^Gdn{AR$8{|?ys zpMh`xcfjud4&432fOmczxcA3Fdw=Ns?3Qv>Y6e*Z35m5Xym;dDIjI@8rXel54n&Ma z0dy|l?J1X*lT|0>CO#;C;`N(eCgS~<^NnnKixQ6SxSIlnkI9LR6L0Yy<4@+#vemVr z9Jrl?&xGlZKVp5?!|;uMEBvXi%IR+XTW>lSemFXtA?R6#IVVwbQrLjJ__kD7V%`L{ zJ=|fmJ2WQ*1dVK&6N$I{2`hM$Hw)abFyi4yC_Bj%bP7%uPdKW4ox0THgJv6NaZH)S2d++u;Hyfz)3NxaXFQg^A7! zcI6@heGpg0;w2|58UJE(OH-8iHG2NiieEOxOzUV0wI50m2fhXh#+n z6yGM;!~~CwZEAL@r))VA2I*@SFModhW|N+TOJh{B9kT9{ zRCih-edow=-b%QI%Vy)AI?9!ZuhzUvyBy{|28kj^&UgGH#kU61me)J(cwqEO@hEVdlaGtSM5xMzxY|vO?(!l?kVqpt{L2CfniM zA7jo5wu_UvaZ1tvZH#k!#VWN;l})N6az9z4c2FPul+Nz@6Xp+4U`kJL&E3y=>=U z?GQ)82krlPz`h@b?EAUS4N2}Gw_%|3&OLwj{A7M35Km3d2wiv(F$8f7DGKpa!)cWm$^j&DpAqw#^Y zN}jXp?QPDAs6QEOF)gH&5Eewd1HOG^FakHhK^g9-Kk?=}8upgw_FLKRi1`lu3AcE2 zg0xsAPMkO8HXnY7PqHX&;bQCME~KV2qZ(&cw}~zImeLi^s59UpQd3+etph!0o23~p z-qB!m)(KT=wV1ya1l?$k`4ihuU1QD!u@4-4p3^vTta)Uh{ozm!(Ljw>woBhiD9~)T z(x*~4HXXINv@J8q2e!=e7NcfkxchDXYT&j*kWIh@@gB6}KS{g3>;F#RNj%&ztv2w$ zFE{P~Wzhb^1NZ$fXz#CAJU-(nEB1v~Nj&Gg^9h6lH`QpMoInbvX2nwEQoMw#A_{z~ zfw(M+Ka)rFJ1@g1(qoY;NQY7j0nR(6LM zwO3a%fDaBjxb=Y$Z|S!pJ|NCp-0@#*w<-6Y z@8wB6*6a*^_ZQlr13wSh_sfuXf3>UoeoWf?bHlqvIHO_*`W4NZu~Y^_Zz@Rc!-eoo z^Ac`Co?pc!;qiR?flQbwf^T!eaeOQC5luwN7eeuTM}vb=zHM+PEC`|VLVi*fKh-gM=5M-@l7*^XL}&&RUR?QnswkrzGq> zed<);hH3f(ix#FK;aTY!i__C`GBR@)FY48;YYEdYTEZFiS`jxywwq3Uq0D)g@Wa!p zUpv?$fVgv49@`Yx8E%O0m~G?vE`Rk;=(iJZ#RP$y6c?O#%gnc^$V07?wrOv+oc-!) zwJ&9W-Oe?|5}J>kjoU`Ku_c7M=~MRI@$Des-uWHT62yDZo}Xg)7TmTy;6R7+cYk32 z*1f+Z?f?0L8wc{r#0?qa?0kI}5NCKf*@EJM5mzh)mtynkBQC`o6;=Q7e6vFf2kOzm zpgLAR2Y2W`R8c(?|u9C>UKtXAlv(sXty%TcY$!@QvAi+5_ZsTyF}tA%a(A>Y`2BI z9p7YD_3Yl=;gHy}d2^V?nSi)$Qw{O#jC2|rzhrUN(#2VK-a6DR-D}h4%Tzk!?Dl<_ zQDzR;*_Q@mvys>^?}{QM7Cp57sM``)w5ZD;67S$y5x%X!TIME3D_dr4YM1dQ>47+J z%XV~d6SX$DH+|l3>!Cgj;%xnL@U|~--h+02J8+jdlY#GiH)M~gvYd(ZI}h%6f5D&J ze85zE(EeY!7q2pKFWt?b&NsyE@L_cGzI*RqkiM8fA2(x%h1!Ba&3rDL&Zd)awn6DO zSKvyB7sfZ_BI2|xVwGf#z`Tq%iT0;Txnyxud&KzAcQHwU{_bxiU7T zQsNEPFb&`6H-{VIot2TEvp79BD}%Ot@nY7boi=e?Evqo0iE8#+JESa6P$oM_9CuMX zr`ZO2U@d-se9#{c-kivGQ-I0}^thOb&0SdV)+PNOL{{AgO1QN5E&Vnf8nEdrx|Nu3 z#&K{ahhF7-na0~Q7xRiZn!6oN*bw8%%=Smk{hE3z0%2g;&4J(96 zd@GiMgvYyZ8FA`vTItPsR^nY`Ym00cg`fcXh#RpygGE9q={|@@0 z1x;)4h@RI4b#RuKUj*%VFZ>>>nPy52-#JI1^`4Q1PUL zhi}?{c+kNgha9lGcuL>RpPy8Ww&LurdwRpbeQeuK9Me_~W-kf;2@!wW-(^=>7 zW5z_OxU(}eJm1T5vX|#%Kbf1gJU8d5rAzL-{r0kM$!gQ@VhLw;=u3_@(;5`$ zFfGT6yL=V43XNa((7Xz@F0C_UILJD8u89=Hu4Ipf??MuYF-;S@%nP9tj zeP_z;;3l?h^1Sy~qt?;P_NIg8wgd_SYNG4TaGb417GL5?AK84z8gO?+1_k$Bi;kI|A(+ie=@N=(p*5U{sRc{fQ?w z8$Q)e7YW6WI`$Y!*l7(;^xL~|1yWdZ0pUHB4i~L9^bdoO0*dyhd5}dSe!&)@DE89(HrA z+B~<=@GWPxxdBYeRH^0cP2g?V=U}j+fmQ(4>dY%okUSGgVAj8Tn)?=(^VsD6@do#% ze`6tVCWN|eSjP=jBHn}E`L5%fU#=W(<&93O z^LOiCg7~|?^xx+wcUq2+V^q{Dt=_1Wm2F|A^K;G--z@Qripzpa!jW%Jb`TDqGuw=K zxVnS4D01F^hHr%ku^ygp3j-F-w}ONC8=VMbz6;}9!}2~jhy}Ng>&n*?A#oD^>o33f zP@$N&?W?c8iusoR_TsJnWr6zb#hV>2#x#=@A_?EVb1Aap`9{Alx#SXl8?r_zU=7?@ z5eo}C^No1N=r?@Ro?4QNehcfTm*zaPBwJewD%Yr0+aNZM6HdLb5p&V6nDTt(#vSY} z>^7H`+UP3lYc77<&}R_^H|}F1jle%N@0gP`VXRoqp7$_*WkMbSkdQ=1LOwUn9kO(`tAIJ zo+TMnI1_C5MO~Bh39B}0)#HtS!Z+S}z53zmVfd|Hp z9Sh%T@J$15Bs_{ZXLy=Amoa?9lHzhoA&ki)gvO|Ti*Ho?i1-dN2VL;n^NpjYDF%tt z_n9a95aJtWqW+}@FO!pJVY`EnZi@tv=8 zS)_i$H#6J84hwFpt~s}jKan|s@)*9CWoKc&X#h#xq zDfjksy4@B;Ai2-=7TCI-#ADr8uma%4sRL&Tt-NWcg)f+XTOQR{%v~7#wo4Cu=UYZ| zNDplG`1Iq;R^*U7u586-kIfLfxa!9DjO0alhm)#S?=bI4Zom26Uvc5~0H@+Ny=yl8 zh9&EGjLv)PlsbmLp>?lWjoaSbU4nbvK8w~$zdhx)I*<5v%a};W+5XS@j!CZaeA`6) zP`{6mZ*RWYG0I)qAV@KV{tIVIFe=}C`!(@z!X}!tBmMUMOPQ0uitqT~31wjQW3Wy( zC4=Jm2JvYBa%t9L@hw}z7NCvwW!cXy%YA0q(x-E?o?WtJ;hZ^58aCz!cc=xww(Tb- zoZIUzJPElgw#9b4P|4pPo?ois>D(JUlqzslYPj4fn-9S;fHSGn^Dc_El|F{a633#K zkW}3Ek6|e`=ZlQ5<4d1#%+bej#8N`2)54eNS=zAW_HX)<+x_;p7VzHvy#n4ildiA7 zU$sd~d&k5C1iO98GnMOH&NV9kUz&6e!E$rk76K<)g7da+C1F82nwXgzmaet8KDy+w z$CP3=P>G{i7uJ?B;pwGHmEr^N@tBjZ9kyM%bdmWE>PpbpWZl3Jab){{#J5Gal||_N z$zR2{4-N`$@om+r$c=PKk~W*eRf8b23b-ZSY`uP~HZ0~_DlUuS#XH)$jPWOh>32Ne zgOkV^vJ072@U2jw^c&WJTYM`%xP;&!{Yxj|;@e9&l*2bR*2tyvB$k50&i=zg4*tXz-079FW1@b5v7ujX+V^9ZXLi_P z(43%3mSia`joABi6bqls!=NxtIdmzrW{PjNF?^G1`d9grSiY^NSy1wcB8%R9%YVzF zDRBMnEc z_QH}SJ$iO6U$(3bW;;`HQjLo)x#J1y{n5F+?7`$x6)N%mW?dxSfrKYqIpukj8{2J& z12?M)XR|gRS>~+Vx0!@klDr9fQ|p!Inlnf^s$)_Zo=_XEtfkYwba3hFWZiVNHk9c@l#j z6(@cY=(=5{_i2xG{nQJ)4YaS3n9!n4TPCT@TaZewH0!`1*1qI zM(KB8PDreB@o%45^%wFT)5UDqupw}B0?lG&2b-DZ_)bgr{yWMyA#fx-)NewBLFB}D zG}-ZFW6EjP^~_~}bYc5etrdlYL@p#hRc>+>Z|yC$A!=~LSz_rbHLT7clkKe!s`Qm@ zEb?}Kx7#o1e)~CHho0B< zK6K*?y9aLj+{Bv<*zMmdPjKL#p9bvxk(8-{dk+uT{za{}U9@b_iD%cm;{GwmxJtRq zNo#!PDA0E_yP&uW7aHpa4i9+OMuK{-Ine0t5smL2)AX(p4elOMf7tMPca5&wV+i)2 z?@;{cW4d+i-lg*m-Midy?e(2$_uqd%13g~6J?Jrf8zdp(81erf@EyPn>&bI8KP{$P z>94w-!$SNI>o+>Cn4q=@xz8CC*)e=8c!F89$!YkLMT{p|LRD0}y?%@Dh=d#8l5IE^ z&D!yBbM>igL!9x>`LkxWYuW0k;w21aJ7<{GnM3z$=-BPtR;+fXSKvpCH12Ae0Pv^OnYI_wr?2INkY6yg5C3-4OQ-S!#ewa zZt?h3mpx0Dw0MKNN8+{->7J|KuhyWIP9~>abQwm4xC%@wz=E7(RN!9z+diqg@I~~E z=&j<#F)3VCm)9QR?!Er{>&>E&7w#+yTeZN2hs(gm=(qIo|3&>~ad_#f~mN65F#3Bm8Wb!}E+sbh|TZ19zSlW-Elw-C?C%*b^T9wNTjOSs3H z9pAwEnViK>lUp_7PnD!=DdZF7%7IYV}|m?mX+l)4H0sj zI-Z+J73X%p^}?>VU(ofo+Be>IUbkDq^zMW+F8tf&xBa8uEf3ba<)O=Odx&lQTOaEF z(%Xbh(C>k}zoA>%&+-I+Bvr82s)J`W4Cb3vZQA076Xmh9lechaui0(DHh=GrYmD(H4FokW#W_$E&WT zTY0@n`1R6n@$KPe2fht))}?od!*>+%-{e~kneG2+f1*;~-kjJ64fz((QLLlT1 zxUnemCozLN@%_utdk6m(rr*#UBjH>{zxg2%8?sQa*HCUQB~HHs3C~E6=NrUn;+ri@ z`(|Y4<}A<7crqt**`h@}H+OMb&Z33c84G7kpVXp73ml6}wKcdy18(W#*b>aQA}82% zT(}KzF51po+KgFR1&H)b^&d$_woyu=&R3G4bFsA zLGlDqZuo9+=SVxW7=bEx@|BOzy2sqrA}1!~ zWL7c8x&K$Tj9>Cd1fJ~hA^S~hF+LK-;W0_cRCK^k>iK4JlCQTM) z&MZSbhVKaC=5LG8Z^iNcFYqn@9pxK;LRj$YufBoi@4x^4qmMpf@X~MD`Q(#N5cp{S z(slQO!Ape*qi`44zm$GQdzs#!MEO=03Vh?gVT+t3s<5Nqnt}<_Sc!}CQ~Z1l%@m|qwT?e+S>1SGsz!s%z6 zHlK1S3sN+TKH9~UghM%e8{$^gaE8P?zl6u~?Mjd2)!G!_i1+_#{f2LyT;#vm!Z*_{ ziFOn7es$%`${$pGPz5z@{-qndlz!Xbj@gC3jPIy;D^SQSJzV^RUwzD|QD(s<;Vxm& z^X+$_9KO9vS+Y1|X;vn1JIZa)haEIKFeF)-kiCq@WM(c&OIyO(C@mGNX&DQXvr9DOa8TEfmW94v$?}rn@q)toMdckwP?eYQ=h@d zYe2N+6Z38OeZyyncmGY6Z32jdO_H{KWvSk~oH^NasB8tBdS@?g{@B?d+jX~?OV(hG z>k{~riF*{`8FNvI36~AI8?2FhWSgM40^wjSEw>m8fs}o}5P3YSQFD9YV#S8tb1&tR znP4C2w@X0*Yxt(BA=@b51zD6}h!C7Kbol=Y-xeZt@e{&=9`UVPHmDOxWYA{Lv(%1@ ze=9iX|zH)T^f` zGCYaG<4&(qz18*9E^IOV6>S)Z4J~_8C4$_2w|+Wsv+<2Z!CF|@d#meN zx@;<9f=th8aOZGnE^|^PbQ;-4q4COiXmq;%u+et)ZY12q+k|}BNORT0c;tw>JqBZe zn1s(OTkzXSHCyX?x}Z4wbUH0cNXp^9pL(r+_q=O*xMMG|d9xP$(DN)6h3r_%kHw#W zwGfZd@2E=&qbUCaz6%@QvH1-y%T}j@JnIt~pLZF$(|r6}h>LIZ`#;}k@yo-+C@eXa z?z5hnPeg9JCq=@kyS#Pr?F8@10+Ya(99z(BgO68(58DGs;RV|yoli;dZm1@<4TmW&wTXJ&}{S8QkY$A zaC^RiJCARAna-jJakli|KwSJBx&MYt4b=D)!XqBv$v^qh!)?~S5We-{a49qpr+?}5 z1mA!E{gto0{QQdN7?!7UmD-Chy};Ed-#-5RzvG+f^A*aM=l3XIzC4Koq`$&9;EHdy zF^FTDX*O3)lO9||1)AwTLbiOiwme1;Mz+%#$qLB^WtdK$J&SN5jU7+95a+=>g#i4- zC&pggv164p&ak)&uvp`g$oRa(o$Zaw*7e@<<$z7*yiL5fe8yDEL0fE+Wq%?kM7~|B zH(`^T_WVre5(z)QLuXtBN^GKg(0YgP3Zn%!?mD5uDFh<>Z~wwjc4PIT9E*Z8QODGj zd)uLABWBg=(222nPCdsQb*y4RLvFssMtPX4LZ+r^PcQK({h!@6pf&RoG z{LLyq8lNw8e5WA3qxplX>{GOUyZLDfmIT|FC<@kG7F_Y~znt%7OP0jwH@e;?sA-?T zs+N*)>u81&9)q}$MaAKsyRaXwrOA}DB{o9KO1JJKvW*_o1UF=hX14Pfou*&KPdjsl zD3^qTIDGRIp3b`va^X0cGJf3P{sSmHbo5ariyc*>+zB?@vHD=KqmH7H*lmnb1cI$gjxA8Uxv}%I)f`e) zo3!S^dJL2+o;dhjOQYrLtuDKeW9Ak&^utbvjU1_~yC14hIY!Q+)i=#S1t;aMwfk z-$y2sq?{zGgrka|)B2ir(-t!oP1)W9H-C*kVIBjKZ>BQfOa||^VuChix$mbM?XM?O zp!x6_vKa`lK`AF)kW0DZv6-AZu<54IHpPL7^ezzo-cJ}C8|y*T5B_Me!h^pI-2W?b zuB33nk!34Z!i6K}bTP>_l77pdpx^Lqh4Y<$TQ76=^r%Jg@#X&s-#!J!^UVPf_1`)p z6u~#g9G{FPb7C$fWj67X4?hfsctW=G3Pm`|^TscUe@FTD79|=Y^!c$z=)dFn7UGa? zXa0nQVRzn1dUq6YQ69h@@-50Gu`#TBvb`+s)+e<{(i}##UPHB)7wKC8Fy@a$KyR-*wuH_7yUOJauXW9u!KSC!7^sa zRGl6F9poZ|N$@dTmKy$*4^g%h6h#O+Q8RZ%D?hg>G zAxzEy5^_=QuktVphrjd71vez28ob?|!-jeNj{0v4q0BJEWB3O2BjKCE4FQ3L#|Jh4 z^0((Vi0^2AERLrLzIE7{?VfAq+jf#IMELT`mvG@M#6xAaEn7F|wJlq=ZQr_e+ZOo~ z;HEx<_&$>S!9UD5`RK&O2@aBm!O8Tr(@vx3TLj|L<^uWlcY$W0+@bB3=bk@v77dy` z*`l0VxIKB+>;-e@aWEkHJe{||=a{osC?+e@rp9Gw_2_DKEXX-w0*;O2Y@#F;HI6?c zXHc^l=wHG&C1LR=lsQh?`(K-(<0cWaLllpkT8@oA>ki;9Onvts=ElPOiK_WrI%t?3 z4+kw<5SrnhEr}?EP*84J7YnDj>1{)p%E^TM{Xe&#w1|(&w1>9s+oRu1iphj{V`MpIol_mli^C>yv9M zxHZJ%#rrShTjtwsZKAPnxbREeKqkLOfCvcJn#?N!}mY%(0%v+DNMYm%K#hJAvj1X>_*EOv_Q(%FI~dmx0sUwJHGFd{TdT7J7=%%oV}KU z$k#4eOWyX`e{Y1bsej9Z>~kYqOg58o3T&;$%w=%5>ff7OpS!M$o3L};(skD_UB!d1 zTe_B8uq({2zeP3Nn1JlYecXi3pW05%I;ObA30dz2{igXTn4WL+JCJbaPyPVk=xh=F z%UJ!^vVc*4f<=)_F$*4<-%zxE>zFfJ66W%f4z_O(#o6q_!EE`lsq-LSA#jFxh!R3M zaMP4Os5N)~&+tv>p0da~pAH&00KSDds!4a#_c+BjD91JUrV>!lW=xvgzgO?N7uG3P zu3U6V3qQ6lN0%r8H+3$!phN36!-n1t+%$`Vr>4l6&%=$)C~h0{IrPY-9P3 z*7xB)4>v16SY)xF5IEh~ybxhne^C1E_zwID=H&YyzH@jVCK6}GPh5jcv|-+(M-X@r_rxW9V&sNahif5KEJa z8|W4x$=8((i8sE1p1Jg`u56|la1p4)+(l_P3XLl==$wf~X7tv|<$`Z^#*G>-K$#B) z-&d!-(0S=vWuBOdZ??j)mh)|4?{E9=`ftsqtIhuqA|2@~Gl= zwQ(C_BWE>f&6cat#x2$wx2RCHHlLP(Atw6}-lm<^xD8L?!8NXIV_1`(r8vyHzx3UD zsQ%rLRXm%zC^k5%W1Af&V%iW-IKC63-$=O3iS&E!Tsm(S0+-8$CBhYi=3?rfAlvwp zDBp%?BzWTIUutr(tM3!3U-c*X_7)}Z-;wzDx8Ht?|ITN@o&Uy;1GoGM<2z=-L%x0g z62yz@VupNch{pzZxPLkPoIbVCtHIi_)@EzajBtD35QJ~Ob zg7k;e7E;v8X`6<-V=&HlRd1x5S~qKsnL)Wd+)CuiOE=HTLdCoH=%!~e2~E0NyjV%1 zgiKMZ-L!r8=eJ|Q2~^X$q%bVOlO)Ou?!kipO#B3wOk_jVPEI(fQn3=nd#^f(s~)(Y z(29!{h7rK17sqO2Z}@94eo0uy?jpy~5@n5P8r~frPs5_{`lOY+0&YLIn3P$)ar2{( zEyWk)Gk5OX+2%ORw-j&%zlFH`iCL70|E7QG*SwYy(BOO#d@Hhdqixt+~v~+*+Ems9afwT5aW3%^qNIhdvzso8aI8DzH%pf+Ct^R#Aiv|4rr~ zMKs^}f%tcSO8M-yBC*g3f|n%S#o<9l$}Rg$*NyTJYj};e0nohv=j*fHAcc^)fsZy;jMeTwx5AazUJwtpKkesk@yLGBjMtk zt@PWgcoF&?Gq@wfe;?oKU!vcVa2pLO+<*J}u{MMm`tRh~Go;@Z8Js)ACSTImOrFEa zfIohZ3%7#bp=L)(KMdb$z)hozX%@vLS{10@@maDkExuzUyfD52-CGoJe{xAKRjG&= zo^je~9!FR_w{~qD%On4Ifcy|ZmtD~oIUh581mTgoZfz3}bUf?+1=*pxNT&`RrP5qj++_VN+6s8s5 zL^U?otY+12%eBHZkU^z!d(bbinb>1UO!a@Mn{*Ffr&O7fub%f5V>bvkPqC*H3f%iM zf^7%Sz8?ncCEevmW-+UHCnpf*{L^Qkr)w#Mk^B?j_WCWpfm?jLmG^?+pqxp#&8v^> z2QQ-%{@ebNY0^%`zOUMgET%$Zu<+lBtEP&Pp&;>VJXpT1NH(M{~;+tETMc=z;Pm;5vP--80 zzz-JPs>-H+q(|J}60<|e!UyiT8#lu6r0Ysrk`@lc*H%t+?&taid4+fT1Eo zk2U`!{S`MtP8D#IJjjj(Pp~MK`EAp7l6C||><5!#MI#Z#*>NTscbxY$$4TwBU7X7P z*q;T{_E~Mi0}n{Q!P=BN^e4bQbH z{f_6`cQj-8mUvs?<47%8W1Ujp5x#vmMQ}g4Y^j7CzSXFmiI77%b_MY!q8G2n;@H#7 zj$1udKWNa!^!IWruRyJmwk=xHo0QKc2q>&kZqcmDhOR!ARmoCYo|8LhaFV7Hm#=gt zCd5MBTfQ>5NsHb5DUHk%5NE{4LWJAD=<((U2st$e38&Z!QfhAb)@v8N#)mnpQA=CD zVNX!h3azC{=Ka>ZjjJ>ZzHr=#B-^9hIiz@E`Kq-qyuM$(VUJ#Z=i{B9-eiZ6TM(lE zE?Y#?Mo)1^Z-0AceR9F=sDN)em!52|-=1$FK6jR_oa!Xp5Q1!N3*Y_q|0JJ;A2HujZm-|q?h$`_+0sDHbF%qhk3Re` z^(3(4^gE-Cs0-!sAAaz`sgoyDP$m8_5bp#HnP1G9j@fHQCEdgb_BD0`$Alv#+thzk z6Wl5X*QH%(QJ7Exi}Rh}@`moKq8iJI|n$&G^`)!cSCxI>*- zHOv3r@LBg&|2nQ*1rD>O4@|JUSvs90rMT1-EmTtPwnw+(;d=&M%Fvdu0=l*7lZ|gI3V{@7QKaAAoCM-6f8s1k09zQo1H!z{Wu&;hM?5ei zTAw?HZ$tdg@$LP$xs>ENt5?0M&SkVy>Gls(rA>T8wW272MR9YDvUN*jOw9v7U9eS1ww~6Tu-z{%d5Dz$v-9;_gk$$+IQR6gSNBo)VDZ_0C$pD802RL5cJ14 zhLCo!q_aIov&>sREr#!W!NDWsTjM>zT{PeFC+N4g;8Fc{t2pG#BFxUpaQP?3b3wtAUtfLg z73sJ5wph56aP)hTn}``>Q6%1MJ=T6FhHOcAJl{p=ca(3uxZq~T?*~F|g=9@)5puW9 zSM{;!^xg1Lzc)YWz2W0N8$Y@6nJpBhC*0S4#Wss7Zu^qHDS!fN+b0(C%Fv9T z0e7=B1vN7sJj~^~@EOeXySi}tFgnus47?W~rpXl~oms5s_( z^QQF_MHbm#etw8pAxpd-Jb2J+IaTfu4(?PYo` z7rs&P)_y$JLfmid0SI|6B{r8rGsxx_W^SRhY~ZKe0iff-ppQ1O4%T5?8a~m4A8YPn z-Zrw+IpalJS9^;^PQb`$UjOetZ+-&7w7%;;ruAR*lWEhYOa^W>&o|;7_AkS3 zrCYAdsd&LFH~ew^#<+OCIU00i90}hR2zQ2wiJ15&SrqcCeEllOwvJ|4i13-Gp7eM!f#j<*Ue#zNXMe#RVEyKY zeb#-@Yt27)e2TKPM0*^_wk_)%7 zdY5h`i{kl~en(YYLLSezXC0(zHV<*sgU`(ss`wLkN3O)J-{f9b$`DQ(qXGVc)Qb*`HCoGV0_$l4F^O%WABF+mqr z9a*=Pu{P7c!O97l33>CA{_FqUYwbt8V6W95=%C_#`|2~icA)WX4~uW~8@?w_oM@C! zvF24=e52wa-)=!}r`#^*B+q<1vlG(ZxH`YzUj2`>Yi^! zQFMe!zu{Z@jfBHDz|9YuzQs5^>VePGp{Vdlh#(rux1a;;th* zc1%#Gobe`tZe|A2fMF4&}-%19xM0te0fjLSNC;$ zWk0O$L+t zU)dipy>fT&m-gbqX}w;0r}xU;+|}!~{e510zvr6wdawIuzqKF1C3Mo`wCoZ9Pe^3C zB*IJ(hNk$8$2z*BPNW@O93jq@1g8!Mx4qHJ`+B_ePWPAhHo7T@pPYN{x#Px-8$W)$ zA@2E}I?*h6(R|OD;uJHla~b73FE|*ZV-}C0p@cx@=>W*(*iZdz;=DSs1*-=tai7WgpT zrPmbPY*}Z*;g@j2iRG!9siAmNQ0q#5+h;1qJzm}gZ}_bD=0j|y-<$ztyK(%zU;ihcft;#tEBAF@xx3p-dwRXRtHDhVx)-#;Vs9 zRQddiLyYnSA(W7BB-~qYiFaXq`~GD_#vejUv&W4R-^ETmp$rjg1}3q{5C&d7*ZvE>QEptp4Nq@wH!Zv4)I~Qe zd#mRQJG;NIqx*`td%n24*9*JxDsT^c=r>y1Z_UTu-~1Ts-1GH!i1qY*iPjhJ|h4QU|pCi$4 zx$r38NcdbAMG4G_qdaSII*}6^fkMjtU`vehy}`_e&0986@khBRAg-}`6Y_96 zk69EKKVd87K9XLhDR|taXuf4jz}oM4MfWOhOo!nSyV1D_-STI)@Ette9WcDIjSc1I zsDFvRo=~PN@o7ti-DFWYJVy6*|LpbJKLP9N)aU=!sQ@pSZZ~q(y8y zOv>o-;+}3VztjEIeH;e(Z!_PB_lA!MeO!(|cBAaPxJ6hJ5?z$=bH;0ka1Ywb3WHzu zQP6O^8!_E_2*fc+SEas0-P&uCSF{*2ufZ)3Q@pCji#vP2`d)XO_sYFAj;n4j?rb>Z z9~?Z4;p0z6j~)%*QgQgEO_?;&0x44`hW?xKCYq-cNO-({7sj`be51vJR(zXSJIjrB z=jpduaEHIQC^|BD6%Lem3ogXfzm!E0--LT4;b9a!SX!LndUCA@p%5iR!c~z_bJfz* zQZOgJmnp2It}126rHg{^j~lEz-RD1CVWc&qyk0nuoFfSp~Bg@$xe+Ys5671u1Z_kan|xHhmC3cz{EP&_hHc=<`8jQoKU`EtKqY{Kew~j8wcpl z5`XOW@-Ey_gTeRn`drZOF=NIAe2*VHe!@8OCle&reuQnnH~)xt-CY>pF^C68RU{EbeEUQ&qTntN9!{rsxmINDG3l6y%Kp83$e1OU zP>3VmF?{E_@GuH4@h%#1^WRRtv%;is&$^fJc*>m-FF?FQ$_bMb-p)6oDHc8V?EFcx z_4Fv;4wA$ZPdt$^MT>tkr+EEm$S}tDl56`hQ$3hEWI=Ab7^u!~*p}4oqmM0r=GkXm zc4dub9WTG>f%-%K(SCZ)Wj%**7;s_QvMpTi9!Y^uq9{%89n<)}F-`9s+k9+F{o5WV z$)tAc>(6S~jMt%z7BA1csPW?LP-FJ!vSpjxKgL3nufIzdlRf}l%!W7Jrz2B*3-NJ| zZ}i*qJ9)w=(jAmci~Vj#Jzr-gu98H zdUbEXd2c>}T#9uut(2C1RzJ$K`t_9`eekbOKK|FTrP(+W60Ka!#5PoBW3y`Y8_u7w zu3D;PS-Ewm0{xD-a32WwaBIh_c(kJ_#C3~@TRW0)_>Mu`cQn0_`<*D_MtRup^f>xk zwDTt^_k52YWASe;WlueWP9e@1{k8%5bst~YrMHEyg|Y}`>loKRaPO9haPZn|C{p4c zV$TY%CKiX?5+zHq)QO(0EqB|FJ=^_n`X21My2-(f?!gJimMPzQ#O$80zfVWg5MR0b zZv*eL_e@BrUAs1LbHSgC9ruL%Netg~E@?1B7OHjO;`lDuoVc0qU>W6G>I~u%>wNm{ zB|O^2EShg9kGgR9HpIhGOow<7KcNaCu|oVe13l_)igLP`EZR*ubd;N|Peh5~TY*CP zlXwXSbW!dRkMeC=p1-dtxMTQ^xf)?QW`L8pN&%sj7{q@s=scdILeG-hh4Im6@dV7?lsJfHzSahA=p-{TK$aEPG!AJyW5dz|H3F+ z_S|jmUwewXaF^~0dz^Yjx8*T3Dg<#vk?uOSiH>p_`Ez z@4`Lb(O3#tgS%OjoGgV4QSLy(op{FpE$Go~*J7BCUKQWAjWQk8?C9;${oi~HO!Iip z_merfR+lv~@%ZwmUXx+t^91a00;B%FLcQ^6?RmJos?m z-n~2DaAVglH&U(hqBRfzA<+N|~CF7WNmH*iP$ zmq*Tb)Su*;lSt&-`xE1v$e{Dz1@k=xOHRi#>}85?A)eREoHmwqjb%}EY_YW+SLD;F zxOId%<1k}XFkA2>dDfe2*9h;GFR!4N=}Z4GR}Blkz?$*xT?%k3kb)8S{v@7nZ&70S zmWrExb8!;x5w{z-grM6#7k3q-;=PJgyda}u#7Ein`%#Fs@9!#lhX?ZjZ3D!YWTBcM zgc5hoe3fYmr2aK>sT~qyF%+{UC3WV^nX_llrnEfMAFa$~YAP#Jrl+T8xC?3@c(ct>4|^gAEoQt!ytGi_wY=7{Izix=iqisn_Y#^YYZE%AJxae8Gy zAzHG!qYxub(H02ivTyG$3MiUj zA3prv`v>W0!uQITUIcfl%i5&O)OnO^^L*pKy#@CYu6gzTk6sw!-i2#NhTbff zNqDNJ9!DiypqqqiUS>GlQyAa;T16r55>a9hFM@A*6+tWnw#37YEAf2CpZWxPl)dk1xjqz@5ZcG()s)e*HLjp+$EwE#DMm#p6f{YhF?h~EQ2Cp$R$6t+BR*ByGo2*A44ByN% z(o%24sLyG36{Q!f&}MjnCqpVkylT~|%ml-2b3wMbKsJ|nPvZHG7XoylKrI*L=!?0W zI3pf8Jifu4mS<7M2jwDUQOw+ga~(Kn;sfE*Z=2q%E@l|QWE6Al>Q%xzKjH!L zx)HufN`Y^pC>AGleN8Ml4bZ(;@qrYbE7?Z*j#nf7T3*Y8j;4#K#PBVnA{8$Zbh9p= z!sx!gv4|bGk8g!No)_n!8#Q{Qs}E89%)d3bDwzT%1z)OFt44kYX(6Si!D-7qs4@Ubcp$&7oLB<=3Fuk57}&K zVYd=nGBwYf%(8vYH)PWSQxGvH2A;FWvdZF{Scv!raR64684=Y9)5aRyx{14^`dzSu z=O5_NNyLsbpKIm3lo^#-Q;Cy&e&`cPwMX3V2z^a@FbmFDW`4e-9Zm7gHp(|aL;o{G zf13iuDX+V!1lmI4;)NmZ<0`-xg*$q+F!3&o<05Y7ypfris#U!!U36LP7VU#*vx8jX z%`OX3;JLvXwT3*lPGk#(+y4<_5SMc6Zy5cPvM;_RDE$2`oRyK*ym@msJ;Gw(_F+QG zFX_~a(IP(B<(FM%bp~hJRAO!6+a}D%Jna!h(q<{ zv5y$v{NwpH#NEVt+-`YVH29 zVnCqmFJ8Y3{`w|66c$Nde0jsN?^+Y+zY9&VErmk}hsIhKYD8N#$>K3;fY zG0IOWcZ#lFf9HSK^$jq24DL56SkN$ZH`Te_(}9>}p{V=vS4ts^CK z6691xhAm;({K(ixr#|sW0&q*oJ>rsZZM{VyiXONW)g_D+wUP@r&}U4ZKXcN&nHUut zrPpvhMx3!8zK_za*pfHvT2R_b`b}z~T)13{_?9`b?xsWB8*vMVyVP+1GkD2~`KkO* z82w$@53mODxQGh3L|KoKZ9yE}76xwgb{-$SdW8Ett7S{-zS;#ls~v5r%zw)*w+LuZ zmd_U1ifzl2=vznO_J0t+6Z03#$TpYa|FC|?&YX^Kn_cB{Jo#O4P^X@%Rg!~GXE`nI za_8+s381OSH)Q8yx<<9h6ULM)S9Z$8Cu-J!bxWo4=38}qq?>Fd-lnQ?VH0n@Wlm!J zcc9<#d&r&T<`&Pi---D{MgNSRY*TKAc-%lT zaPwQ}=YVoT0#vB#cvXjJkvcvpx;}o~XoA8ZF3M}vs4mq*>1x!d*6qg5Ndx+H>)OTi zJHI)J@{M$gZ<+6?^Nzx8j>CmbKsRc>&7bhU5cm2W<(p@E374Z+IV{c;cD{(LZaja+ zc;kC|05^!E+%6_~m{~lI@@c8_ZR!JLD;(aZ_RrMV&Ng;7BO^POt$*7vJHlN z+|=X35RduUxT;^&|Hk_Hqm+Ym%+&z$aF{cy*}u6T!!eJKxuw9X@h{6qVjWtDkvXYc zx$=GY-3PUjXHaHeKzo$g(YMn!devjCotVE;JlrBX%D4UTL-E$H!%xF0cf`oy7hPOO zpOcSz+Nr1WMUyJ?d~?68gD8@R={OhF#pk%TI72;Ua@2y;%N ze9MpI>33lguF?EhzFj#L>&fQld+v#tG$$711bxJN3!FpL#4VvjmR`6gnhzibTR$c(2v`U}PUcph>88|On@ zJeRo|x|eIyo#^vZPZdtJ=fZA>l%OKt4G;0$Nh- z;r3+f7Jro;|Hc0Z+_-$F-24PIehVoMTgAIT9BsPgrXi$u5lCs@zCC;cH;AL$z&KM~w!l|#5e+`RO};Rv+!TVxl&H#d0|*K>4y zu;t}AV(fJzd`A(-D$&cd6Uy{SKDHgrrvmht$O+>1bL+ zB}AORCQTA96_-olPj3MP;s#E|iO+<`?2 z11aJ=nwP?#Qs+mHIDd4Sb$VPMD4y?l75D1pe|RxLGOcdmY7}d~FU|zF-RLY$4BXKk zXJL{YcYA?mCKqR7ZxXuyyq(-!nC32hZ2Khc;y*t-e&2t`|F69>@w23;(suu^jxx@S z(9PES-q7?0^a98#A~+zTASei^IO2laC@3iVuH(4Q2h2kyIv<;!3Cg64no$BQ4h|BGLVD%QsxJ z@GBq=)>X)dZh)(%GtK9luol_0*UiaB;|DhJlAJa}yu9`iht?bN32upt0)c)L6p>w1 z$|1u<36Ftb5I5!Ko3Rw#B+=jre1i?*sI)|x34BL4Zx|L-a-wb}h$}haueYMY zqv8$NEQRkQ`%bV7h?{S|#&mGc2v-(98z>y!zF8%_ObQp`F$5lWG*72}4xXP2v!_gNIE#Tky)upxl{>k`ra!MZb^i2?;NJ3vunrx}Fxp6A$7$PMtx*YYjAz zQs#xz|5fr7d|!9sS8u+GNJDw_k>jRMc}HRVvBPU0{PF|pZhq;$d+-7eKf0GpTAZ3>PXz5#^Delz8~6D!vuA9LOfdj=hYAjM$uiRgjRIBdlx~MYe3W#uVu7 zeskNSzokOew(d~=2xzW9EzPZK8qTKNQpqmKx4b8~W3;3Un8?HK`Izr`D!lY5#Wx{S z_X)mPlz*#dDe<;)*Un|>S!TYSc}v0@@fLF8Tgt8E8M3v2s}m5zJ@5_U)4j})@Ua<% z@ngfQBSOLp-{^Pd`)5D>A#gwa)PK^Fl5Yjyw2;>R$Jf91n_vHm0dj_ra6ycSe=7c7 z4}4col}T4h>~Q7mf>;t2FwQ!lp{*!0a~st?rXqf%oYR z?-VCp`j?rOow3GVKz2sl(jcY4pDYsZoR);!NlWw>s6TycNRQ^m6}|=cV&yj9f;;QC z$;KEJZzU&ld@~9%FWyi-$M@BTce9x9oTW&=S;x7O5Lb#KzKysZTD$Jl(NJzbdK`B& zPsHr?qTj&%shf1`K2nr8r*A253Xc?xTrEaXorg7Lh`jSO{O?dnD}+X;a!=9@Wj_4Ku<9a-v(w-9u&G!{62cwmG<` zZ&(5_3p}#`_J##~J4F%SLcAtGLy7|5LR@z=bqcDwwbC(PtiGc#D32wkh66W$bv*dy zPp-ww@at=U3gzLZam0tISLR!Y*URXj;&GAnh)S1>6W2cWqm-k6`QHpf@`3W?awIV2 z-wedrPVdDs{TOrKA5v`!bCnv~;@vkepXA|zl#^7lHN$&Mxp6WMS1%~ik}_J7GjGAo zI`=7?%mluTxZzgMaujZ7-T~CKpfYbazcX5n#-%=)Q^i3+AL$e$f+c=GG%A- zGH032Y=d&%v_19R{`gZhartO-a(gr1Fg*i(7%jnXa-%#cvWs$;Nz0@qx|O3xX6O5q zN=t~Hf+)VZN-N>Ea%aAMo!4bQGLy2@Lg@5we z$eG~##%rUNR9$0*;+wIQnx&Mfvl{sp-E=G)#0j~uQ0y}z=4RB$Tw*&vo-&&K<16|%B-YIroBc=YkPCCov#o}ka{EEz z9m-8wGPuhcfiru!Bkdme4#gRANL_E0nPk2VH;7lhOMR7 zZaOvm;bTn=uj|P~rHmL70QeT-*h1WtgE$M33r)%+Dx~A{PyHD2jD_q5poY28O$&2l zjP6Wf-fe21T%N0h-PhSAW*P&m{jVZ^*GNthy-GbzG7M6c`^R;(8@Sc2RG%_Z$LLVj zTk+B+i8pIyCKm6UnTYSvsZ=OqBi_(eZ!DInf@H|eJxhUF({AqR3_VNW2JA_``9enA zW8oT|QGU|!9e?jQO-hl3$A~yj)S=(;CdD$1=KAB@$mDjWZffF!^0KRVCB70BNqEtN z5fxYG246=#9Y0xlVmoovj%|~MxtW4YcXZz=-jHY^d(`~U)Ros1U z9;W29FN558Nf$k>B@+^FI+Vk$oTA)iC@?OeX*3YTOJ))mUMO10!k~~e6xq)=F7@b#CIM@abJ^qWC@pw*EKUzu8mwJ5Z9YH_3WKS z49h?`?PsBv3FyjF>?t{}Uj9lJK1;+k_*(93s=KL>NXR8ZLR^$9ma$GTu2H}1E^{EG zVa1zMsO9oAB|d+5g|_Q6YN!b`&4!xkhRYD&f?G^mBkal9fXSfT>I)6ZUGL+RFtus7 zlatE#e7AB`yd{q4cTP^4VL?W_fe;#n<(zoW^No0un{3ekl%j}z@vU>he9vX!^N;p4 zd|!LA45XC4<|nTyiTE{FGif$@nItOZNoht@7z&N&Om8^BheTm$D`wK9Im8FP73P&7E{wpX;c109YPTuwS9lQrbYBfuPe6OErg>fY zPF=|c?tyQn1jeku47aBT5(2@k0l{IOU<n^weTnNQ+!u3gdmVUb%{TfAiyxMPM%2uHb( zqduiPpW!AP+;Vn@lB+JU3y~^gCdAaroySZJ_p*G$(j4CcZ~i$3Cnx4xJxky|1HP@| zvn(Ycu6o?SXbywor;c2Ayu2f&42M7UlOL8zCQJ9+bvy0(?&FV@S+cX?lc%2iF;lNt zVnSJ&eT8>$Q%X_dz9x&Z6xO&{LauS~*@#LRoi*Y)Y(`u{u1j;)Z$n_RDY4z~WU5BX zR}OK%=jZ_!6uO2s+>mHpNw4!-a8Jl~47L{8=KV_XM+Yn)^0Uas|59#`mWXeulK7T* zqufQd;*Op&ZzZVayA|&tEg?Cvc!QSudsS7_RKc8?^l(eBBR}CkEkYjrE#Xd7bS$Yj zi0jAn!>q-39fyZ z&JyL~+ucq4+axM8#C7+0m`qEwP;m+QOs)n23(ALF#aP%dG1U+(-jU=~KqTI2tO!xAJ!8yQC#EqWoo)GH%y6)&?>!l5($@Xduh4XZr0sW?fE z7+MkHz7Wseqf&B}S-LXJ>o+c9h)tuDsank6`}b+CgDXZQW6GqKa^sy6@$G~p&gUT; zkx%3zt(|RS=wV99_9)y+OQhUNOXifjyd4|O%F3+{W#${KwKSPGIB9f_7n{^~o0QB% z)ERO3hRmwmWmvGuOoaG`V+VH_cJa-AClAM@*_bz54v`VBaF_ffPr9N`QL~eWS;r}O z&0+Y4VEF#=_rJ|epC5f+@Bd!<_uFrxo#ZOw`+@sDPp30pSy*0qRl6pgifaN^lc+Sg z3X)?566Ja5P!dkwA_FDU5`oW`@`ycVym?aK_wji8tXE+2ka1Y4`j* zs+^gC(=0(4?&&ClfNZ^#YR304=53i4(y$$T8*zQC^C0*hCe4cPCQAYIhVMArP8^Cv zoaNLZ@%{IuCCKJX<#Bp|e&e-Qbt(^XzI59y*ZV{tE^YNMu<62Q6(16nSvoH7qcIX2 z){!-qyFX>%Hm zzx>6|IK^Wo7V8`1ZO}2I6vKrT@g`SM7B0}$;}qhoMS1XD5s!Wn@e|^TsB}xXE9#~k zxbp%!bI=q~H{vpS>=u+{?icLVf7JYF!?Ztj<&AZ{It|yW+UhcCN8XTgv{r|*fm>b4 z7$+H?7jAMAr6qP-0#Ul~Vh-*;Ah}R(1$M3nf zJoK50kdv{n5MWa-#93D$2GHk{mULrCFGDj}J&|Ww;*-ZeVI!Y)Dm3n@t>IRqXd?#e ze0n<}@g*)2?&02~=X0YMGrnUSaPkhN^r)?ACy?nb!H~6D`b0AU9p)=osxW)?0pFWA6K@sxc`}kor8@`8^ zMPTU_DE0Yyp&#jr{*ou}SsdmV8gaD25)SCjRWJ(WEopgNe}{)*1-I6^QY-wT zL2D2ZD^^5!y4yW9qjj4nNf2Tf)sZ-}EVx`EC%0Z=PY}H7VrZ=9?@WA^-Zr_xdQ>l1HJbQWIdv0jFQE|csy4OHt&l2#jTvJJPfmO7AalbPUB zyVsg+xMy)0gIhxg8E)m=dOV;R5ad!XxGmnwOoFH(GZES58v?~-7H`wr@_p>UHb{r> zM#7js3HWB=mxQ<~!5>}=6)zo4C*o0E5I=dv=RW)C??3VFA3pK;4`co9AASGZ zjHNKB@`ES7#~Lqit}{T6Q{tpA6I86?Q{7D=ZfPlz2q9O>sE)s)HPa{FPfMa%-)%6= z`7D$xS1}pcFzAA5Mp${8D>8)PhJWTV)11p8yZNYRJ@1e+kl9a{9zsKNuCvIlCvX`U z$*oq}rYR*jXMVD4bzpM`C-a@t67E{WsP25uO&n_|hZ!b@+k7kSR%WsQZt*SH7!&lc zpy1Bp4Xff?a3k3u4cMXIl5l;Xwf2^Z=PV^&BhkGng{&y*Q3l~7OG$|eRd;t&jnTQQ zel&Slh<90w@|cFz{58#bn>R*OSo&PW8k2jRjUZ*jBUjNf-0YOXRT_qLMYzN*p!qwX z7IGUcb|sOZFefl=4&!?<%tS8<4@blBF)^CM`XjJ5${BI$Pr27+?5Li@?qPpAWh3&5 zg}@iUrs)iC$fIX8GzzE8woXN}w8bSTS!VC8Cf0!^7o zD0jIt!z~)f6rzS3#Lc(BuK9_xaGeC1Z|ph5O9G@*N)oQ_=E*Wv7;--F9o0R4ScBpf z?$SJlgjk}hQRWyzwnhvwqJNqAcCMmCe0EJtcYkobLoz&sAJ^wA}JsEmLa~q zHbV{HLP~2hC!-;9RW|(-8Efj6H&tg66dFc5Z_;C$JjtdRSZc`u&Xkwbr5!8>8uM-91$VPa zp;>$nMkC%F!M~DG&bCj*Y*?yg#7DXzFu-j;VBi_^t*f8-&SYD;=lnnPCYzzah+2Bn zgW=Xio46BwEs?mEMSRDAxA-m;Dd{!bkY?)o%tVxCJiu=bh z;wP@$uQSiXg;4&hS6?c9%8^wWJfS?EQ^RJ`75})YDF-0~ILnkP!_zgG(Y5 zv)i0R(Qzd~kI<$obx>ZD)H+fQ9EK)mFBBSU8KThZrZ9uv+n&Jm_w+)pi`B;gN>yQP~#XRa3g5 z60WWd+=NejNB-I6+)hgbx9?NV#Jh}{C=IuiyAkgJsx;u8?p6v&u+}2D zCEmiA^^EUP#FdNd`#PKuS1zuTsgdwg$KpP!6BYRW>5rdyn?KQg`t*PQ*RNlH?WJG6 z{xS;&v#hu|58r?K)1Ts9Kd-+`2@8)w$VaAp26}lsd6tfATqWE+o2eo#Csgx7t}E|M z$Q5HpU6fnMgO6M5jXmhm$l7ewWf*=jY8ZwRBbVFeWXSuB+8O9(2ac0DZYrZIPgUzd zuxW)GD8ppynl_yLvwmbId*;=Gb-7@U^d>O=K*Xy#iIe(K0Rro2Su7%My$QwuR}z2#JSrs3}l z{L#gvF*J{bz6HbKWGMgrn>U%J5w{WXxSfqq;iT4? zY~2}%?nj+0H6vT%ExwzCr1p7=Y|na=?Qz+I+K8FBbA-!(H~o=KU?D&&-z zKw}o~xHVZuyMycl+r=}g&9XuNJG_4z3!jO7*4F$+!lmNzV`UhUwN)JBnu0i0{Nz>9 zUHIA0pF+IJQm(lwx)IS?zPij2J8|#_-_xJ&Ub_9(8#&jZoP~$&8Byse*F`X-VwhDt z6Y}!-rZRG0QYGYB#R;Jms~D}E3+_3@bI(kp5T(avtYs)oPwRS{(17ZRMrM7CF5ay( zW}V&qk!FlENl35OCflhtCo^OVWjr8*b;i0l@Vc2~bmM@1PGa#^;8) zx9)@N@`MF^({cP844;1YYhQhUGxd{K@7L_Y$G`d2!gj3SJ06KR5zn*f?6;>dLRY;(f)w%`6+lH&&0hWM4;NJ&AbXTQ!64YmV)qqSW}ru>*hp;~xU=@{eaR zUw`$L*Is}1oxi+I&r*XYWasfPa(Nyi3~q6yH(&{LA#S(!LQVjxmJg$c23@QR?rFrw zJD?5dV3esE#V!|`_!S!YyV&|xF3i7<-OL-waF^@SOs)HDWJuyyao1*?~HhsaAhev9EbKb0%NA)LOf;{?mKmK zH+&26?|#;|;hmU@fsNs=V=3nxL@5Fch%Mhc8H8%dJ4?~*D^2n$)!l<0ms>o(xdYc)f_g>|8C2mmr$$uj4ckKbl#tuMLR{&J!r-+t z^3l9JhZ%bU@t|BwMn`tTS+DCE&1G@VJFmp<>ciPcRjXQ$Vy!k#X?FDza2K*!+mj2j zZTPOTJzR1TCMi^nV|iG?wt>~tpz%KBQ1o%A(7!w<@=oytv+KB@q!@pQJ|TjW3V*P z=1mLgl)5jbw)wg@^(_zT+3@e0X&JfZ*glPA_ktaM(C|niTW~wowus2v_P@wxJtf;7 zEivC!xy!?53*cU%a)Y>-6s1bL#kccqft~msI+~Jq_8vuiDp5Icc;{7IP!8?U?C=2n z==MQrWwR!@#B|Ye{~=Z-@1@l$fX2!ABKHuFU#rTTOrAm3vniJ zHY%=;l;dr%X zY~xb#o#A#A=HQ-J?jCN7wEBq1g6i|8&c1H z)5+4j5jjb&s%$kOVa@R0Y)IR1>-^7(P%nLDesV)L9>&jx+q&bNL>Jx~J7p%C&ppfv zWO^wACymQ+&k!GbUiiv`^)-TqZ3g#nuSVi6z8mod@eP;nxkRLE9f^42JFB>}6jAP< z^htK|$SzV8oq{?YuddItg^)9D$cV~kZ@c*yKYQxc*IxPM&wl!ayFUZmA>k+DO+|Re zDlWd2s|4Rek|!aTuZ_3_M(fdZg~%Ck^DQHBFnf?WgJOU(+!SilhBnK*+(?MMelZp{M2rBEC>kcnlRb){cf;s+P>J`M+My};{@GBi zw05;+G_4EP$H+F;%0}!)8B!3t+Rg`YUtranWZm+u`#5FFYd>0&dz09GT4K6n817ll zJbxcve5+3h$&2{r<52O!H)|p8>8#tVB+2+ zMuasDb!W{M<$_zX$hucyNdUabBk}O_K1Fq?@;l9@&$<}vEj1a7qh6yd*}=1d$Eceao4Q!eP}ln^N&|)l8%4tv4;xLOAmbMPP&`F_}PCJ zEngxFhi?|HVlxK~gDOTGZ)WG#q`ZLMQ0{2SXb>L^^?QE=I)lgw7M+4!*zl_%{F=ML!;QH}@dh<2+1-c|0zUgbS6!|{=%u_$6J*?617oYtlCoXeT zzH;wfe9DXz9Kp~^ti|{69u;eOk9gK{3%Msk^q`yatmQf0BEUcd-visI762UFL1MXoQLgFpW z7Ccf(aB81gV8EpGDGfJ<_>OL6=~J3s@r`zG*uV2aN%*LW&m>%fhWp}~haew}JStfBEwb*PVR*<(J60zxVCO7&N^0=sqTe|K;ts7%oK1#rKJ$ zyaa1s43U->zQq9fkdEWfCLIW-g2M4DN|kTh~tqg3GM0wC+$w&dvRe@+6nxRzEU^ zjOV9x8*TXr}VXTr3+OUD7Ra4MxH50 zVEWOOo^nKsNE>S#W&$I#V!K7EXs5}uk-1zInN*FW5>&tOy|x z({SeyK(;bTk?pReG@F>VaBIpd%AFpg%xQ_pMi~va&{_aD;@gnjz+L#>d2vQO^KHaA zN)_^QUz1ZulG}+`NRk_oAZm3Y7 z(t>TW-!t49T4cKN4A?s_-mrhiMM_bu;(70Uet>vReKO(#J)VIpi3)2#r@EZtJXj3G z{+m)kbUZCz2L~}2O9@Bt_~x;F@U2sSMtORLzCCMSV~BD`rK)%tS5d%NTb~xoxhXg3 z!d+{_B%_t{0l~@g?o7z4+&&Z$Ov`3^o5*44ND zAc>xvOp^h7*&c=4qa_RAc1CcP${k?uDEW88cax|XaY?u-pGs7ee=Ap^86f`HqYu9J z>WiIzHpL3-O2&_~|H5Y0-oRC5FE&8F@4Nf;AkXA#k5)1HrT z4DaH}WJ{ZfV20ZXiQ&#^2_mRPX~}zr8&S?_H)1`cf)3ASm-h7lJxRz$A;nXTUL(LAG){(!ke>4(;?nChm=8)&zsX{^#~ud}1( z95`rpICi*eBLlb<3z4n$;+#Z?gLNibX^F5FEtwyQx9Bo#QdkQvhAB0-LH5oIy=>UG zV-t(EYvNLRoQ;ZeARSl3js8gtBaT6}$WmBTS9dpsc#wVhPR0$dKD<+{)i`K$tAYD) z@Et=)1$2pl);Li9i+g{v}FWW(f08% z8k$ul)}vTAOtUGS9>p5Z$X&`t)QN1c)@3uu9_~~aYX!){RIkQ*I(XYIqtV`OBA>p3WZ&7a* z?-BQ6zl_T$Y>itX_Eb>62F-4yis; z-oIQ%j>uT#Ajv>OIjq~c2zd;X7mo|^2J~hakw7+~wU~ub1`rxJK!=hBAT~vtjSL@l z&u+6rv)Sz5e5%b{2TJm0Q)g|?sdblO%MT_SBM+yp5CPa|c4w1K%^a=gDs1R%T-5HrdKd47W6!wG&|RZMZQS@iwbL?~t4b zZq^&%TZp@;QodvIzK;>-$EuK*aq*-3x5i7bhDk6O+>8Kg4SrYU7T|?=%1Kw$=U|PH zldgz$tsBJgsr_rj<$Y705znc)k{0JJec)$gB+w}w#mI%3$OTOIySc=;yQDGvTcgHE zwdcn}+|rCSc6Ls7TG)efn{2yXhOeuO;cl1~+$P(-NduD;68IKc@NKvovaQ@sPNdW( z8L&6(*>=Gc-<%El=7K~;h$~S!ymvEvW2?qg_vcUkSUiI=e|hI~x8MA^J8t5p&!7JA zd&)uofB*PfJ%a?_z-=vu@9aBl`?T@3LD$7JuA#bbTog!=n~UWzPfPL6;g>y^w+yMv zvY_|yHJDCq_Fq&9`;UG!0<=!Zf;E+Qi)3W1m1NI(BaMwXy_P4X+443{o+LX>ma=9` zv(BJ5IXsMG**4!RhZ|is*%%V;%6Ef!oChVFvG^}Q+!7AZ*jdH@>sN1XL4G&V_aY^oEZX_j28j!a~(3 zf~#BD0L{U3NmE-hjo}lxQENFNYj%*WvDaa3)ezZ;3#o_}>?}Rei7b65g{&dl2?<7{ z+?H*Oc>QI{NfyA3gbT$i-pV)v+5vIFjdlm$lJExcnx!nv#eHrC;^m>`5bu4_>7=?C z67S#q*Bj*JF|I<7^B^x&*-C54%OiC;{qEg&+`?weuqjK!cXp-iQ7xYf$?#ZIAU@n| z!^nKgCh5!yXM2>JN!AVhz&!}Akn#*`w(Ed>KCRKYE>TfP?|o`l?|j(^WJP=#27 z^}&5xQE?U}A+NpsJh%flZ41!j?)|oJfAi75m)F|EH~JkXIuA<^^T-+rMtL!<~&$(4%k|aLiDK-4mI)6rE|U(aFhgb@@h9A61pYTxXi& z2C8Xns`YswV7@NTY(VM;#-o`z+GdSWSh8;!o8B51kB7{%_+*fsRnMR zB~oqH8+LC!pGDiEo}IkJ_ciP?or`D0;hQWRzM=fc{!8C_=N%9y$v(7y2bUrJq0Jaq zIk@BJKmB1zT~3pf2i)ZzC-;M#tH^8N$26E~{gBgHYlsTNQBjaALJFSOW_=WNW8v}O z%*`BI1K$R{8pHe~cAld)F+F!mr1N3!_~|%Jd9G5nd$kUThw(nOS8I`Ntnrn+XG5EK zRerYHjkOHPkRla<8{|m=)w+>w5U&Zz!ki=vXoh>1mh8M3GL@4^xdpeBddBzOE$6eu z_SP6k=@IuK+}E_>O4LMUPl203mA#RrusrvRr)vIv+s#Z)Cq-e5JVp->Z2!|Ac=cxd zi$}{Md?{2)@Q8(F*0Uw#6Y&Y0xlO0CO&!#Z{ zXm5+)rrNddQPMNFQ9VdHJRZrG3S_5cXU_awZZNai@Xe4w4i>h_7S`5mVeOoo>$w)+ zKk3H-m7j<)iT6yofjixV7vIiF3^&5PVa9h##M#vnZoWB6{ekjG8$W5jW$+lqp*``8 zr|L@8@4xr%!tl~>fA#voz2)8doVEM5|K}5rmqEkRzkKFtIReDto3$L2y*3l_bicQu zJVzx*OZJ?W7sPd+*@0)s#TV`d6pNya9U!jWYl*))-Rvr_I({6Vi7iSXH?VrB>j?Y~G+1suw?YWeu9=dEB&s^Sp`;EL5 zg-)1mbCR@Nc}3(cc2EZ0-fZ-vp`3W+@L-#DBaY!*MVDy}xj=>qF!v&+Azpge|NM*HLo`vHQqNrfbI4QXt~0k!N^X_Ov}zbcOWc4glSZ^t=zq4quhp@gf^D3 zL2fIz$(A9t)>;1;C+oN{>2IMN*k1DTE!@M8JNcJy<$mWB;&Q0`n>{YOiAoMwrkq$hx&~eE4O2NN z7;Y=NOE64W<~!To;e9o7m1M(rPCU%C&SAGYV?9o`Cy>lvE%CFQpUG~-NSbZ3myvDX z!5JRI$y$j$my?t^f1%vN2w+1t#P)Dk{qFfD7iVu#Zp3wTt@A0RdK}z+VJuu+_H0JK zyt#g+(S-2AMNXnT{8=cN=0=-Ph8W-|RdBgh35f|l-v_a1(%(OgT z>$5%F8F^nh*1>z4U7~uDZv@=K=(QW7mI`jx0$L5v5-{hwj&J-dxJ`C8gm_POhC39k zEUj!8@pf90`JRG%ith$)A@7f@q`NnP#52p8CdfTV2(|vfx{xm#N^6%M7 zY4>dV^b#Y6F=%+8jEk4Kwc^_@bE0Ckz`ugKLA+}D{3MnfNNc$bMSYZYNuc1sc8^Wv zS=|MhnX_T^DY4xc-g$xHlP!Lyd4({4?`=_og^G)I{Yz^QY*%fm6BF4+PyIDj%jPhC)uT8cMyF14KJA%Yu`Ox3b z@e}Bj$Z))S!*B>7Z8k7z)6mb1LDw^{`DDoD*i+|*514#+c)~ zLA>ERQ{Lt(42pZ0efPx&cU}CMo3DlAQ11F?8Vcz3&l3mG`c%@cHI0wF{J{1rx|ge% zZw!%bUuV!W;=Kqts4{$$*|B%{dp-=h5qHTbXB28=#){j}+;4i%));QS@QX&-wyPn7 znqxHeR|9it#Rv~#S)I;(UWR?4!_#!g$GX2n3 zIvEB%`xo1|W0-P9NJHinI)pj8z1_eux;aPH@ZfX9rAFVHLN?X;>(~9BwPZf)Fvp`6 zbD_>vk*zjnJGa_yX$(A;z1lc@6l-E+vSl<_1Gk2Z)hkoNot3+X+jkX>*_3SWJQ*H@wK)-xv=r`4ZbzNT$?%BjEP=5dWm@0Vpzu%@# zCnGA{;AC{=$llG|;EaKkeV6EF5U$ZlpPgmUjkvWuyWSybC~tztW{z>T#X(*hno*ZZ}3v^AneCJHUCElmsUE061&IJp^%ippp7paSxse+utdWd?tiUsAvGt0Ov zd(fWE9?y}$c7$v=ilZ^Eozb!WXa>bFVPx`D?h^0jlCv~?z}}#GxiHr@+vBZ1vPb?| zpxJV4cD4QAkWGvhz>Rp9SjX_1q}uC-?8vr@T5D~a%|-zhB@-@ywr4M z7*qjk9jND9Kgn;4@5^>>+)L#`!Y`nrm+iO^(2?%Hmxs~5|K0!K2E>6fh8Tz==fys- zE2gZ#cjPVfvghBAC9j$C>|fd0`zr*!jkhOn4fB{Y9NEq=*=7gZi;Xh{y*Zj|qH=FG zM%%hHB%+Uew)_0I(Ks;sw%mPN!PwvMHxWDnXtHIP>|kJ5&10b@$R2LFuLSOlWyo<^ z7$#v6*+zQ9_DlYSMWw)P!(ZcAUVLeggd6cV)wT!Rd$z~8c&y9YvEch%Mp9xB#s3&V z@pc#>&W(NU1O@aZQImvE3Aw~0vz{Z-1WB6?p$&(5Ss)sKn$1k1KFTH8ZocTvybpFR zx4qa{wh3;mn{Q*(UD==5+12@Sx!_k&L+0mF!&E+4YW!V#a0;UXG}348-mSAVB zMYd!cxTA+wa*~;Hr{qKk0hIxhVX`rdWq?XrQt}Sq_A=vp+r=N(B0aZ+&k_|4jSpE) zlaBM-^*q}aod8svMVA=XAdX)i`SQICd%yhR3$K>tr5BzL`=w_oWQ~4bvF9S(5!rbf zKdO>Y{vPL%Aupe4IqudnJg#rbn}B(au&vRgz&^#x5toIQl*ZvQf^5WsW(W zM&}A+s};FvvUA&U2%C0`4LMMHF-Lasu*5}ciH_8VzEdn+C(?X3*@)AUCMU^hNhK;F zuJpkPNsD+%ON=fgo9Yd=#J5!3aQ8wE-#(M_8Kyez-xi2ttM-@igax_yj!QTxqhVzX z@v_}93oLAg5qDooxrX=g2nK5}Mm##8L*GS)9NmP$h|6%~Y~(Qa_F~){lFpd>j?pt^ zTKkVyqp!I^@0)3F$gtDoOg8ORoQ!Ra7?3Gs_13k2OX9SbXb*@KN&7M-kt%?GxPcq?Y0v(1Y(C7cjH} z&~!R=Vd73FhUObGYlW6`>qj1G_?FA$N3q_xn9WhpiHgHE#k!)~huz@NA3@GEP|D zY+tO?fBgOLt~+&Bnc4Qg&1qiJB%d^YVF8TOu3Ls*MxmCW=-x7i1>6(dK@n(eR}ablep=OjQ9 z*v#i77HlxXHg(D-=tsgdwkUVrfg!9}D=``2TYLCBI)sl^e_-ftx*o6|Cm#<+Y~5J0 zl#(-T+w{VV&j;KAb&Q{A*n~IxFlfl%ojQDACw)-oDjGRzD3|ZddZ?7TvkP&a75kNvS!<6k+2bY8AWBRvp-A0?B!rb1*6%E;9dW39wyU2DP05H=w47Zt< zk+H_kWJ|d%+cFGQCR@s#Ic(r=$gX^Ex$rzK+O~>k%5@;ExipL-4&T&ZH=~DzZ*uWn zTQ}Z!_w57I40wCYyBI~zpZw1s(9vY#cumB2)(76?DLW3seA~#B%g&TzH=whb1Dpeo z4Gk)pV#Q#M8W}0xMiT5C z7i?pVv4Cu0ZN5eJlxz#}Mz#fBrqL=aTtzQykQUDy%(sj5LFSvIa8Ml`zI()}hsJJn z!YNmQ?_d7>C(1^?`1#vEebY((Ej1>@KK8XQOVs%jfqS=K9GN-;qUDyS`k;1MBe z8XuW*jO^LQ>DtUu84a6*Dx*fd45G(0-C-QPF+@oN8HG*BvYQJ~=A&i5-tS<1ZK&pK zHcepDZMSDAu|0=%?miAD4&442SqUTw7q{N##e?R zHpjWVudvCM;pW@Ox)6e`3*$gGPR>5Z&SX0}GKgcdIMv3mY-8kvTd9bEv0P#Zvz~9m zE%61^jJRP2Y?cj|Zv3b$e_%u3)KOM~xat(AV3}UaNy&%_OuziXFKH6`T^`9?+PCc! z@O|q|*Qu|0+s&sq-Qk-%L6`5CIjHenc5CBtZ8o?!VH;(=iG2@8Ut-#tk6eDHhEI&< ziwln5e56gzI>PJ=a@D<|80_i#62%BJ8plTu-= zrOu*PNLRk65ZBjytOjxEx7Si}su%-0TVn78`@i0NRS<*tkAL`s7&cj2iqm)N#yfAj zQF8v7n@*Ch05=}FY)6bL=0!<)tcMv327M|3O`IIRx#*U0bZZz8WLr#Bj64ybF*?a_ z0Btr$p>$!hGn@WsS;I7El(X%oG5uJ>eD1gOzMAr?-R?lhSdfYcam#kq>@vaz9ujQ9 z-IHzcwtg>`ZCJG~3vQ@piTtElw4c5p#95ni{|tuUc2(G9vI~g{)uoX3eEs3tj|64N z7V9(&X@BC|kCpsn2`|7mcQ;vl($5grBg~0!+-hHAOvR&*Tve@P?ml=Ok<>gH130BGM>)i=6`cXr3<`_HIKigHIJ$J3Uua3J$J(*qIkX`wn z*CR*JSj(6tBwII1{>=BXa2q*8Xe=|OS-A!GBEH!>A0;EMpV6;o#CKi#@g1AbS3g&X zU$%P-67D}%4Bw&OTP{FF-um;~Z2tPqH)y&09QF>z4DsT@QRgnpdQZ7ZD(ef!(K8>t zEwJW@U&!|;tWnrp7Q9(-Z)m0(F6pb3OZo;@#LM%6*6&fKRk)aJN5_%MvCDAJ$Sz$; z33e~r0w&{x&3znUX0m0NOjFv!E!B?v+lz&qy|MLWebYxY=sv43s7heH`CqnOQs>}O z5+jD2&)>Iu^W)$8#$VokTWXDv@7}!eCqMjdNluo&a{m|b1?dWNYY%YCbKC5(x9l%F zQEttw69#Q69*#|pT&7&*E}7yWv(nfmY}^>c-l|`X;lbPSZ_R(oFHY^&RK}b6{^Fe7 zjq$nd7}-nMnQ1#VdmGziV>B^h(_}Be8n~x4TYPH`+)Q&_mhVh9&;qkC7S-c?XV6W# z5O2;THpMqpLBF>vKS9U~@gTlu>%YXQdFdy3VdO10T=l8zPwd-r{?3x1>m&|GY}rJQ zlca?qb33T_U8cNoI-5;wn#i%KxT<^><6#_q3Usj&56(5qSYk5;<;u&=Yx{Hl_4K=Q zb#~rWj5-XGZp!55_s+5R^5pF7YFMRw-fa0_%cNAhpOEkob;L9-#ab%tr}TDqdUQ1o(NGn!Ezb=$Qiq@65f@78}q zxp4$o^A-yXTqD2OS*AS8Mz(VdWII$g`fyshW}v4LF&73v^W4lfOKvm6F=6+96waZX z(khp`&W+KxJ(t);7`-R)Glk+ma3%pNl!NZ6xP|v zcD~3K&Dd5VMa^piZKEN(VOqeLA`GKtNd-dejG`c(f}2m5=R2#o5AC09SXccFH-;`f zY7AM5E>GWh>>)BUA3P%@tMEt9}>IJ$jg@TKQ%tjVEc5Q|v5^#gyZ2!<~(K zH#7|_5@3SYgjt&n6B#CdGz!zXEp**5s7c<$9&PC;*4b~iEg}Zm1`$z!Y-p3%Nv&8n za2spOwhaR#Lm(L~V9U1HTOr(w_?|i~^aDPh{M+VRhAR!atZoUvk`LJY`silU zG;KD`*XGP!X&5^@Hrv_xA{&SuCIM}}M`arljCEFSm;soKkh#iYiakoUSoKHQn9NTW z^PMSoRcyGTRC>onF_t34X`AF+b7l3m2!_n@h4Pj?!TQ`{_cexmoxR`KO@H5k91SxB zP@b_E<>vh;6ubSADensxl;p}U0NTyBLmH!Tqc$_y3WUOh-NcK{QL+iWFtEXn_VRq^y|e9KHS4Zct}cA585+yf>HFBj_kZo72g$#`{?)HU*K_mv z@Xbf|PVcpbZ#J{jvHQR@k;|J7vyGmQrbKLw7B0YU*^jKv%kHV49<7Exa$81?-24T0 z4>Z1&x0?_&F>0`8Q?brK3-L^LBioRZnZ_1f89dQ!zVk=Dy$s*^3!_Ig+%{A{Bfj(E ze+~Phc;5=*z5U`koI)cRFE5S%xbt#N%(AB%4>tbwra^BIR}?91j3&C!K*=Z*aMP4G ze6zVMmuJ62UyY_#3pSg^Hk*ydylI!($=S$pAt6DVboq7QOtuVTEyGMp_>7A6F(WPl zq01wtH3uusnbi9+kRmn zP6=2GZj3u_z3w~TdgO)YfAJy{34iciUI)H!$0cex;++log|!iH_?C+)Yy!ZhV$-nR zL^KC`v3*8Jna=zz7rqB0A7`<>xE21i@xv6^*{fNnd)on$(E~k7wv-Meynv0AF~cpQ z&XneU|Fhy7755QgS~iAitV%O0@Yip=!fX=;OzJ--g)g1{zjyxk_?4F<-n4V)=JMFY z@S-jJ(o=44Hn=MuD$KFLwiMauW88$_o68!riM!>oIt@qCsa?!s|0zGz7e>i&*v4?h z+!mL}aQR}xKFOlcIM7VveC!;8u!k3$nQRQjZdtO0ve!a|wH8Bl_Qu)rZN#@+@KIF` z{ia%>+z@}^#|zx`f#Mn8#d_zhKc75yIVW^XJS<}@BrPU88}_UXE{@JvZ0H&~n!q@m zQ&Bf1%?>Va481QmMrr1kaaf1R>ns1XIV_toMV^;Drhz=!sxWr0d9%q!_CPiopxLw! z?Nwhe?R*(rcw-p`MyyJsaA&nP*3v<3_HY}mv!Q$y_*O2ybz{sUXU#%gft%(2d+*dZ zx1}3{5*~EWv&KJv@<;Hk$u!}hi`zNnnR40ovhu$s4BqS`=aL-Y%tUYZFrC{HYS>i3 zb?$-YqukQQwl^BX4#b$tv**=Xu`tS_Tx$xIWP5G6We95^nIc<2|6}ouesA7*-qub3 z6rf{l{1|+{@a)rNsC()0Z$5hUk$p6eCh`3I=kC1q?RVZPcQKc?l%W-6>Ka3o*W|(M zN7?3EhOVL6_EptJ6GCk^@n+Mk$j}Z=VT0HEwAuXV8Exiv(Nb-WdO(HtQt!z*S2NeI zsCAA@j*g-xrU^7g##$)nwfL57HwrDX&kf&F>!w2WguCEl!S|+rx@6;dN>_jzo&M4H zzq4i2KX1S2W7{tLC@o~Y^Wu*^`qc-}a_(Q=aNSj;G>|QK_@uWsG)DICTmXiV78Pb4 zl0BNY^no`YXG9e{i}k5*Ej|t#)^gLAIyjmm}TSqkOnfM#I(z zzuDBso3urC)2O_erP}_fG?%R4Z@7 zG~!LkrA5f09Dyd=e(CvN1m71%2b3NsYXtlLFMh7jfBMF2t}JlJfYHvNypES=&>Ohf zlW(9+shU**QSi3S4GdaGUIGWNZSMG!WN(be5ZEtltZMkDLU)bt1}m z@wsOr2|4}l5Ko)o2&R6^?Z1#QkQbB8}x>9HtkJ&T!?FJ@3T1r zeRxTt14%du?P$n-0vMnLMuxB1Eq}CHcJmX9H{Fma!zX;>sO(SM+1sO7XR;MFYqkxO zJqk~++@rg_x58E9n_9xRPQ+->#*a{;`|i0@BCWMXPKq6QIsbnCX%{PXfp0RdFFeuk1A;idmY@+UunsPQ5W7L1TLP71ncxEN#{R;6Q$gsrY=)=6? z+zK^KC2nS8WT(k*jZ5w7?0++jL9iSq2hPSy$o_Ef4b3rfQr%OF+++j8lQ@TDX_GC3@24#74 zd{w?OS|1ACA`CYQBbR48lWljY(3>_4ca8-yVlzhyn`o-UWMf!*h4Sdy`>d|9YWi)m zseQC<<40n|aMQXcKu(ue|goRed=_w8(%@f?UcrAY-Z15+vC|F9uAvh z;}RP=%(mfM%M?IOd1ffvlMoMbYu&BJ1p4`y*>0-gP4D9tHHMqtd&VVlpfCx@KAqakjdH_B4Gr*l?a}MTllHTm&t{IoDA0tJ@}Adog7_-)O&bE@jUNeQxyw@w zBwXn@ZD!Cg+FyE~azT8hj4HB;EtkbUWwwpHnf;jkEALYx1Db6Z_tEHP4i?)}Bu$k! zTzb=qL}Pe|#+W*Ooz*mqfwHN2Abm5{E)80nV;caCZLD=uhE4fRZnS&lVS9cmGzzHV z7y!(_`j3)as7-lsjgQt4h2n!yHV(;qnF}e-Xp` zU}PfC$j%38?5W?%yXAds(?;y3$=S{jxUsRy%d)5*a2-Ar+xR z83QJD1k*&Ad;xy+dQp5~cS}G^x7&-8^8>0tEC7HRiv`2q} zV>adbm8Q_ORk=3ZMt`ajt+O{8+wN_g>2gDi(FaH+d7pD`uX28(&ZQbpvT^qvw>c3(p^Q0x4uJdi)dwp)5*)QuF%+g>-($(!EiAHi5nzBiY&1gzD+ zjE>_4AA9Zfm&rw#aH2xmd}$d}*~ZQnenj~U-YLDzxTW7LqmEM{d&=($i8dV`MUKrT zgpTx7xp&BvENf@b-?N>6Wiicp&uqK$HkxMkn{tu;W}^v2-poPi?Y!qXw^xnt+LH6@ z4IMT6?mKQ(lKn}>Ny@lkq~@FcDY6mGp*=s66TTa`m-11M=xBUoqp|g^92?t>kpnCU zC@~E#@ovpW-po{CyPP4mvspAw=lP}0uXxixCqqqeon`o=rd*r21tUAhK9u2g_PGle z7-zcwf$@UJ<8=njK;Tlv+?uProJ-u=?7fC%6u103+1K1_~LU<$1ut1rMo_R3(Zz~jRE?! zQwcX~G8XqQ->3E~w4>`xfNZZbShD@mVx#|JJ|D8}J$=B@AM$S5@K-QKn?~W{eHw2Q znx^E#zPW0A%QYx;@LF;YjAMuQ>23~hA!DHf zEGP%EeovKT{%lX}Ou6^T##A}` zzqWDzJ)eE}!TVW0cl*t3#tz7~&WDZh+=IHOKHymzst@btIGKt3k;(zfJ4`|89Y&4j zTbbHrZGV>|+g`abTA$oT+l^c7tESxhe|U{m)9;|UJT^~?67DEA_%cDF+{G;Y%#y2= zTRTfHKmQD~@;M)vZ*<+Mqt_ijeAVH-Y_66obIfwsRNnjwWHcpg3YV4RHMQsB;~2{P zbr)jfa_s!kl-;*QC5=x;H>bY)AuV5JzM(wyyYwJ|l{rI5Atgd-ur*E#~R{pJY?&(<==|=r7zrtTR8`G+kO>YOl3xoHWk{uf8Hmv zwGvxD+UyCkP08qHU*XZ+DD>0(>I+wv|8?Vbf=63Kjg2d zE#8!&FHVtnHs!fWI!-QWs*x*`cf-zm`lAK=d!MyutQy~0wu#MFaNl^IsD_gImal^^Od_=DD?6c3(t$g$=_XX_lo`(L_a&eAvw)^A1w@>r8vfUqFC3&CT_P6|z zOPa5{+*m{LEx@@ zmnR*7yNnqA{rcnU$;qnS-uh6$_W@LL)%X_KXf})BCKo637R_Vp*;qJ#tArP%Y%LG+ zd;o1;|B3&9Jg{nf<1!Fu4d1NEG)TdDj}K4l@SKhx+nE^;-TQEPmWR|lUK&y0)_4E^ z@&0;C-)|4B8sECLUJ_^xp?qZ9rjIZ-0fqH=9=@sYVy5`in}FVkg}eFuivgeTMw)z->XRE){Vfu%*6b4>0S50vL0A9zE^id zuiVi*+jDo>#;@;Ps`8?+p zSm%44@3riKb-vefbkDhbp7RQ<^S#dZTK2#?-)lL#=UhI|c?H(_Ugvu)dtjaKwH)1Z zE}!ST0_%LQ^Szcmu+H~dj_x^^&vRaZb-vg6UdtX>=X)(j_ngb;Ij_Jv-|Kv@We=?L zy_TbU&gJu*S74p*b-vfK2iEys%h5gO@_EiHu+H~7-)q?e>wK@}=$>==Jm(eI@c#fH C*!i#k literal 0 HcmV?d00001 diff --git a/res/install-win/tomcat-installer.exe.sig b/res/install-win/tomcat-installer.exe.sig new file mode 100644 index 0000000000000000000000000000000000000000..eedcad1e1703ae3da720159e81b98a222bc15021 GIT binary patch literal 10202 zcmdU#cT`kK*yia#l+Z-UG#LcpHbHU*$r43Gf=JFeN^B%55|tnz8A*a<2@(WR5D<_g zNR$kMWJF-Q9T|1L*_}CO_sluVKTTELs;=9&>iO0Cyca;S4`bn82vZ8%CjeoCGm-3_ z0FwPB7z9EP0R&jMH^Y=b77!>Hf{6k6Vc~IMfuJCy5Eul_bOh{y^S{&(0k{wlMkX{H z9E1UA{iQEd`aO2TyF9H2NyO=Zpk+ILaLoR7#=5)aUsdJ<uEj z;4EA%547crG2iYbHSO#=Vn^uLB>^O6G=PM9WJ16oFc?Poi1Pm7yNin(AOBst$r2AVnAONb%)utZd{gT;1Wyj%Eld)Lo#e z&ym8syu5hKQB7v3CLS{<2Lubigl>Y6()`&&#?8&c)xsREEDzUqa&m{u$pG}{3HbN` zVE_Sq|1&sGz>h#601-ZZK@mX_1K`&G?KBtxhCil3`R6orT|L~~QPcQ72Sot@31<5B z?m?Is5G0rg167X)MuI^Y%?q(^){_mNj8wz{CcuV&t*wBS=MAI8R@CVAwi(U>3H@Tv z)SjoeY3>lT0}R_Vxxy5ydasfk=!z?Rwz`N1KA8+-ifa>TGrY+qN9-acVR6pISCKM&BjZrF z3yyIhKSs6pjZbMH9!B1lW1t)=Mxf~}-R#$(dA#3WkkP2->p zG0Ba2nC2>?qsx_o33tj*%|ps;jnSkXu4U)+tT^nhM%%E0Njm&n7JWxcO!l(?JhqhG zPd2>CbQgEW3M=U5X~3wh3Ccl&L;)n|52qq01D8_3D`{ z3>=CF-~d=yP}IK|zwR%Txy=LtbBbcVY7DMZLbm zo&6HPH}!6X@%z4hIr&F&jH?e_vy`e*y3;iZupp#f?DFpy1DCEp*=*)Lm%qFvAilN_ z=?s4|pe9Wvc-huQx4bS(@BUiuki^oniDTN)9WMhz-Fr?OS(*~xJaMh>Ja+0Dher$E zqy%_N)4j+ZFe*B>Srh*7 z_(s2B3FS-m$8NQt!pGN&j5%hXCjv;Urzk;w_(PDfFA3GM==zN+mK<|fUNFA#>rZwL zCCLAU68@pa{CxaB)mRAN=i@t5W3%7Y_&h+4deSH;5S}e6ZZs8m5MqD`x&cDUch&$u z6J13CxVDar03dP+uA?NwCwK|2YvW*{<8JEUY~yH!M#4YI@vAUFEal2p30GyUtDUTn z_N_Rf!X&M%;en9;47F^B%<{J(hX#AZS@PtvCb<#Y)?fX%(<09WrZZ#^@iS_qJ4vOrUQ-t$5>+#0g=Y`Gv0@94s>H zvdx%jmde_iJ6hY9l-2LT2e_{Z`fxqjR=C;79Id}UVLF*`BGPhL0pzKTEGMhzOAbOJ zPAX#tja5BKv=ZLj!xy^|#q^|=4d;*yMgs9SVQw&=Gn>wzk=$!@uaKh0^;|19y6iD~ zGe;j6#DPJ0NB48@ve9c}fTZbiM)jcQ^8rG;6#N1Hs-9UjL}vfpdQxS;LO)u|a>4AJmBmgyDbveoI1)Q*xm^6DYuP zc$86grks)orcT-i==3&I9(8IO&8l)9IY$u`aoT*dY3-xKUY5N1di319;f)c8@UM5^ ztO5wDo*I&ePVJg%x9IS3kWVO@3ibNP-J(HEti2Cxq!hi5T;0h=PK!07xP6O5d*n>k zZ`KrP#93x=XG}+x%pc!tG-nI9k8eu%d*_VjxACOWL!;hczYtbow0z2;8@V|Y<>G1a z;q9ht4)fO*cBK5%6n2_o7J9bU+Q|#6dl}DsQi5~z!iw<}-b}SeXD%im9p$IHmbG`b zw%WRky&?_5ZLfLU6CTdeppm2e+{-fVi)-4lDp@bJ-nB+UBG+l2Mab^*-&>UkPIB&OXb)ur z9_QY-9yQ!u6=1=hQPmjqmfJZ$LjU1#V91JETZ1s`+P8?5*0{I95J`1s7W2yJSsZ@> zf3=`%BzAJCDO(+43|kxBiUy?lYLy^3>o#ogZp9JKX4G^#Zi_|1*W@5&!EgyJs-;*) zZv~#L$J{fNZPaf&J|`$U%ksq>y;`B&wy=)NR#!t3D`{I#!kWmg87)zgT66?fXoPYr z12GA2Bmg`iU}g zw3S&=u`-LuF{jue87qXQ*4FCcK$ARmJS;e-Tnu-l^AVpx+o|nM-Mh-I3F|XehSN@~ zY=`%TDh`EV3oE^GyDtWhoDxh03>y;^I6^L6u;ef)ck8{&#)?%D^4@TdoJwu|*0eGI z*3!}%UO_p8E3V>_&x`$19D0v@i?QW?eo|Db`-L5cBna!aN2R2V@Vi{8X)Co2&&`zZK z^>OdMU=>MeMUywdnr@*mX2$C`qs(<%-!f7!2~+u5()M@@D*!1Pr)C;z4MhYvIx>Fx z5{i{yYu4`Z;ey}Vi{tANpK)2lUN2PVW}M<=GYaUv{suE_pB;TGio zb8gZ8!7Y>>0bpZcqPYe66JWnF>wA=eVwNbgyhJpP=jWJ4(Ud{GX)tx(fs(*Bcg=#k z<$j{;GmNe>dw`vkBqLV-vFdR~jvw2QP@|}klEZ@`DdipZ28v(>&EvGDn$T&_03nxd zrhS8=*uLEa+qqSE|B$9nL~2x2YfV#@5@o=|Xb?3q6=jcUO0eGEn>o+L_D?h>atD%$ zr=B7b8Shj2R6cU7ny_U(&{rCwJ{e(`I*#r44Eso1e$#N9;K}Fd$gC!j8R07*92+jB zAI&rB7`*ISw*8VVIWAYn|5`(sEIwuTW3R5mm=TzaL7LwWGW(#SjnTtRyZiEJy8vx= zo@#WDlcx}Os1$WC+cM>gSnP7Szy}d{OXL zHe1iK-3(iKT~>IKNVjN4BO0dXeFep=HWagpelQDDzHB=&9Y-X;d0fh5-jw23W}&03 z|0Kdehgtl95JKQ5v;+_U0u^`tVR|Y3OK8CX`ahs`{wK6h!IT?1m_l=j{~wu@qoWW` z7jpkbiKK2N^K{?ik@Zb|1G>ay!CThme!dE&|#dsfD&e<&5oZNUh%mt@yO-iiF(+u9~>&DIpth+*FKYRJuGSRL2mHQ?z_%C zrHjT29uGZEKEkqSLR#w#L}j(Bs5*iCz1PnwhGQW<*CyMc3l1!=B+$~@N<_PHvT z(cF1;Xg>3UStZXh=I5e4mR-8WN3<2SCR3ZAJ^c|W7K&HXRakgSxRPEx{&alXO_J}k zw!qQ*hSm3(+yam0CLPVGu!TTzM6}gfJwMjS)@j%N&h9LH6mail2c8v8)3TYtQCM|IVkqx>uQ|vs1|ltV(qME*0$(iR7khm|~A6~<+R>OD8%N_`hJ&VXtaa?c+ z5FGR`BRT!EeyB)J?QA~(B9i;3DY^eelDsV()M!E1(e!NmMX+JV^GmUF~T|()8on5`3QB#|#khQW` z&T4EC$q0Ms_I>*J4{g%IkBweu^00cdreDDHy~CMYbIm7aoK26be7m4Y2!Ax$Q$z2$ zc3VxlaAEMGgN)%loBroq?XPb4rG53DO`!J7w(%I${3K8E0#1-oDF5zYUfB!BfU%>h zFZltrZvFGdRvW7NK8mdnApys78Mcp;21y$H(eI3%JiDl zI%Y0+a153%%3(N9zD>@ciiE7Zj}zKZ*WP|}ld)Y-as30a>OH=f5exdWHRF~kR`iokw&V@CXuyf12OXXL2a_RkZ*M|W~KEg^SU2PK*d{Z97m#hpT5B3T==QRYy zF`8baU9k57O?;4F5SGtrRWh zY20d34(+(HE^o-tZ*H1QkB^nc2FaHN!DONUB(^^)fn#w-v9WLC(~P~b44(qj>l43n z|7FPl|6$bgon@$ujqsl=;}bv?h0a)p1VIs`C`b6*kL;H?7zFvAQ9`GVeyIeL;>cUr zn|hhLS|I2E8q|)565*3+y4rY}x?8|CT&+wUZG27LZJZntu>3ES3>t?UhdQ zN5a-ML*iaP-`U;XTzTtK3VN#a@U#n+xdL^ zk#Bl?r6;X)yNGhPx!|Dke$26ECR4VP($`Q|^Vc;d?$xerY@TR9oX=+_H7Ed8m&}Jo`8{A98-ZQ2mM` zcI5=cTY=lN7`2(QtxE!KGCuXrTB{+F=MRo#w03F}X0#=CBPyLJBV_z!;@L{)eIGoG zeoI5F4ntBMd~KxBNbNk>Vou+>6$~@5Hdu2_c8Bd&FV2uZ(g4I;5O1 zH1J7mmAQl`Ljm@s5VQTBL*zG!5WyBN{vRX+gHcc9?RBxiG7}?+Zwv77WT>Ls=03-&vT#39z3b0}P^|`GJhT=1qC!GU@rKLcfFpsYsw|2e3@-zR~7 z_>2E;(BQE-vRt2 zH$9MTFwZ*tbvLb7AQx9TBD=$W@M+~8NbEf9luF#Fz~GDh;;IzHjoijT-6*tkCvYZF zM`^odKe(Y!e1AI;oUj><7dpF^dq^~#&jY$R$MG@A$4zK~Wn|3QBE{^cZ|UNJlH)Lw z2;)qJAypyuffL_{)ewqgt%jVV<_%HJ*f}c(cGxV&ShY{04P3$mU`sQ1u7s{%Q|!_t*yV9A>taseOJI(U zgz~)#pDj?~QZKAnl=9~ubWf=cFeBpQt1{HnV&qM?r8)-WnyhsUPgR{XpNsU{S3Iqi z)0X>`zUL9^?K&UBmP_QAnrfD`&npUE6A2bBJSpC=BU+B5b83yd!f|Ut-%~aENr@7b z&PY42*Wl{SBXV$`-#Te+I&)BFXCK_DNT|!FYUeT`vq@yV+8)Cl_SN@P>NU5T1Vrx@ zP1%v$_Ol|etfN}C$scWB9yRlqiH{5p{q)cZzzlnXyxv@@PBEB0<^!B&LId~KRZGZvQTN45c~FGP8cEeH|5ZMg(0bUG44iK6N7N{eQsNju48wK7Jjl`lEVu( zGM4g)YMsG^Ev?cl9IS%k6KxQYvUb{Emt=8daJuNiSIk5SB+$8uMPAfBgmj9l|sC zrGCm2cCtN9|DkmGJ}7s)d}ow|(emS4yD&fQpot-V`;yKVFKleTEsQ|v*T*Td79_Mz z4oXvJJt=K#+viaR>LcOkxF5A z@w8a=wh_8mkJO7q9It+xs->@@ur7~3a-pk;d?ZQVUD?*(=F;MQO=+DC^CIF+vH3(} zes26)sLsgiRho@k`Cp#89w-`=d#gxoC0|j{I)D8+b@wMPh)rO@-Ld#;4b_Nc+v~DT zafFH_Y08R|ajwI7EzxyP!m*MD$?De}gQPI1+qE1~sexsg9Sbc=IYZV9b^82nIKx5a zYFS7K96&;7e?Dm}1QJ4ks=`IDZm<8u13|6pzj0#D)-~#Mlxc%Hb)CJDgj&R?+|=2N zRv^?GJ}cm2B8X6KiU1&hxP;(C2n+HX0K7k2p$H}@yt=$<4}%-ulBy~;lyN_zBiil2 zt~9&>U-Rt*_5Kxm-v6;i!5G|_8*Rl=i=uh{Ihy$i(d5G*tXqeEh3^Vg#2YAmMw`ka zYzMup^zYDO(FoI#rPe&EJcCP9g293Bi#Q4B(|Ih_KSOE#ox+as)G{n8ly zMv_=-Q7nOk>dq)(9-A9cL89Eo{Oyuf9puIP%lLEeYqWJZ^FHeP7Mqqg6RHsOU%Jo3 z?n%WlbaAOuqpE`8DLBXV^72LBnH2qef`lzGKxOO;X+JX8r5Mw82p2Ohd2>3{N$i=fuoy$HL{> z5WO(eSpgVbsH~A*))xXxwB{F;UId0R79^=N&v3rfP5c->z$c}t*f??JUB6?>g)ipA z&PJ7l*|F(j)O + diff --git a/res/install-win/tomcat-users_2.xml b/res/install-win/tomcat-users_2.xml new file mode 100644 index 0000000..f85002a --- /dev/null +++ b/res/install-win/tomcat-users_2.xml @@ -0,0 +1,35 @@ + + + + + diff --git a/res/install-win/tomcat.ico b/res/install-win/tomcat.ico new file mode 100644 index 0000000000000000000000000000000000000000..6c5bd2c63b38b326cf01f7f9fc61f7b21b5bc5ae GIT binary patch literal 21630 zcmeHP2V7KF);}l+mZdpS12dp7GgxTSG#Vo@ z(O42=Ta6`lqX=qx-EY&s#3b&TWN?4G%lE(U%{<2eO*5PP@_8NJyXD+-&pY?LbI-l! zJc%r*Hw_#(kXRaLLDYb|p-9WDh>oL9DEA~9V@))2BoF_252AH_Sef-E+HOO%;vtiA zKcW%#M9$7U{IXs|=SH!z0lq(x(|D8eFrqhRfDSaX9wYH*L6*1@z06(t3y;lMp(etQ z)N1~QE3)+R%P-^38alZjb#}5G{_u}gv5O%MKmqB7@ou9+Nv~DgRCQG8wVE3Pv}&zZ zp}H}kL!nV<0VmhuAL>lIZRIqvAL)oQuBrjsw5;x~@VsQ4v+ck&6$ zNAkpc3CWPh*!1nU-_rH#*XiSrKc@HIdylSOyGE_8t#sy<%XH@4c{+Oh1RXqljCSrl zNKfy2k)D3~X=>W`Jk_@Bp^9e?lJ2QpRQ=?$RQANPw6bvv)irOXinNYqaGF5@*wko z1OHz*z}|gt^K|FKdwZLg2lqt{lsi$+{*Mp#_U`9MZ3Z)z!sWG~~}cSs4pSACKwNMm+N9ut8k82U%G5 zb{zW1=!b2&{=KX`AEJRnMh+eRhy~!G_o2JSIND7ZGm$SdV%&QZS-IUVvpOUWp3fS} zX3zUebS&$8r=E4-2JysNcpnm`=SL=fupbEwFdkmLc0Dw@(Cf=Dzr1zpmQeiUlTV=I zp=wLl*75M$cJE7DwH^Tf`Okl#){$=9xPdNLuDuHF4`mG9izVY<|N0jX4=}6(B{iG! z3rnD4TXycnf&rcGQ+Ev77)l#!3qPDdKUN|rW!J{@^VjX2({LJ!8#;SN{RL=yEDBho z%-}!x;DdMGc}G>dVcxp^QyVYg=T&_KO9ED$s04Zd0CxTT?|(-n@?a*`z4VG& ztfE-(uy0rL?s!1u`fJb1p>kG}qLduAlY z^2DFf?o)GW2o*^8+wSAB5*h41BlC=Q;EyibXjMj7X85U1%L0XYSAYyw)@v1H-3c2QgR*j`UXCspPSdI9%B+7U2Cz|RZkM$- zKxlKjtVYL9_3bi70U5W;tcb+$o2DNu8|V4=a%bzwNc?%?&w_ZnU2TbiF`3!z(Au2c z4mC)xzy3OBx1Tz7ijEySM*H{gr=2@@QcKIzw0ZMpTEBihRn^o``RZ!YRaR2Dwvx)q zwN$#Qf@J4DrNk?@C=GUce0v8aUjB~KE?lSdv!7GOsgEh?+*cHR`4+{teov9D9Tag% zPjP3jQ^tu8Dd)(WlzZqZ$zE)woaZl4?(Q>`b>goiJ^Thq_g*6DbEhchnIn|ba)`3G z9VY2h2PkX9PLeimr_9=|R8+p2N=jE!`no45y|RHaGOT zek}e*HC`d7=F2psu8s1PrIcNurc^~4rOHc5QoNqx3M(mg#Yze#TuAoZ0>aX7?Mk3O+p0rNw9^&rsK-y@#s!X8P==zg41 zv>KS=;qNbwWd*cbB)HjPlajB@VO=fz#zwgZ1O)qJbH+J)KC1;DVSSb>m5DZe2S}nk z`~!V`gS{5;M03yI+s8dCV&S3;4|j2JV1U1WfH)!xyA_tWcIuxc4hj-S1o{U1BL%pp zPD?OQ%D4B8n;jS+juNw0|A0BkqlY_8=O3DDx9mp`Mfiy4`TGa^2M3Cm*$p2weE3X` z0Ir=eu#ZA4_6T(Mb9YM}JL1uCV;zPpwZapyb_>qtu?$ss`ee`Z%3CpE_{3!A5#!UP z*b1@so;Yq}sduJwQHVJ8(P5KPos(T0MlFHa#M*lpZ6=F8F>}2ZI1G1m96Elo!-yFe zmK%TI?R`wo=vj{poxEU@bF!1eBplsw?N$T9>K@TKQasZ`(}y|OKW^{fIBD3>sfKp4 z8Q7=yVx=-42%{YwoF+`NALQ`Zc!yA9J2rY&mi=&QNEG8Re9#2Zv~j~99yushXlE^a z3UzRCoj9gw+5~48hwj>^xF{2?d-v&M6Rn&+rJMc(6mo;LC@uVRH|@O_8;wbeXdGK| z&u1LFoBOZd{sZrKH#GfMtHxopk@)k(p9S&zUNr>cH%O-T-u=z<4|9KUnqvE7;9uX+ zP*-1HQ&)$xW7DVmsd;Q%pMCaO7-Ufun-`TfFbj!G(Q%1hwMQjo8?e#+sW^Ff?5p?f z+m|TKpTBySd*hkFwR>Rd?cBYa$B%7uR9TZ}-HDc#7Bl?VRBzd`C5lUJZEa?)D8l^S zvu97*isF#+ryk#M1=ePca^-~!7x+^HVSZtC2dvu6S7wXR3C&n&mZF8_$D|w7yuH9 zM1cS2KmYmeyYJ$OGV&CW^3^yq#|g{CjaOk5gS{|I(MteEM!=6e=!srYfpZyH&Dc~h zTa`=c>FJo4mTk{qR{=wM!HUX&b$dZA3~t!t_&GMWhiF=sCk)W@4zr#80%E&l^&=2jn(q%BWd-Ly0#r@g)X$Kc<# zZ5wt7*suU67z>&sR#eVzJY$L(_<G@R$@Hn1z$FaG>MEF-93|p3;xARnVt6p~KTIPwr z%J!T~yz~!j6|pP2cI}!VMW*;6-Z-bgiIqSAwyg5vQfzVpSMQRjo3InY`5iHxNCnaw-n@=G$Rx8M=5o5kj?>p3H^eeuN?Yu2n0(3)nT zxOVT}EmIUntLuZypN?IwPHfkM=Q-OB-NUJYLW zcZZJxUJm#-G^|2K-0%v9gdqTbv}%WYK@+wm%%fSi~%0VRWjCr z!=r+GgvX<IrORNvl`sRfgy41i}t$I7`sUS`VL(!bZd3nWLpst=0gU zTBA}}>kaiC9KmWWX}|_}e>4g;2hZ1op>l1_77o z_3%+)20*P=4=#fdD$ZZT;RXrJ=hF!vlUxNC5yK-Sc&1bx)_S=KJlCd?5g}rWtj*xz zMbm?+Y8$h91^>2nkbxfK05VP#z!CnrjMa=^Tr%)Ms5i;nLFezGp6Qgkr`skI5A&e> z-{Qla>x>OItQrT2LSm=VNRT1={`>E_csm&T1jCqKd+jyOZwGsijvhTqF!~54AN+PL zv~lA`YHV)ie0SJ?(5h9d2qrASD(C!k$!*_J`YT^k#EVW$NT7HyUrl~?p?}8efGg?1Y4Z4cb+D%^)Hj`nU^SM>wc1M zKLLLoV)8ccC26z4Usu;c(&|m{)or3QT?3_7Hc=|>De%`Nm#yP`b@OWeLO6>cpPD0t zy(0y!-brC)PjLRZ*$t;T-`teOi!{Cdtid;T)!>_JqX~_d>9OW^dIYh0!|K~;PVI3@ zD$!AVQ3Zu7YY7`3k`$~X$%@q^$tfdd6tj&+e10)yWMuGuxLhUta0L{WsicKzITV_b zM@!-pC^9*VBIA=OBqf)E6QvXz7f)fa@XN(5gKsX8{1?WPIBF@0agU9QqlltLOp?B?t16&9(} z^^OVfO}Qt$Mepncw|R3VF-iR`Fkk6z0r9stBo+~Qc}1pv6pQ}qnAtw_Jac<9Zyexz z$-P{So;&K932D;u%MByWYCyhZwy%$mcM6<K7*qn?1)@EcTftD>kldoV@kOpB-4x-@+oD zNj7M)ulVb4X^7If4I#17->^ugpTNsg;lOl2DfxQ4PPE{Hl zmj?QA>yzv4FN^UA3Gv7cpY7=__Voc~mc+j9;Rz-hlcR@q$XX`xMGA=V@Dq!9SO)Hy zP_VT8Hn;ib1w2Kmycw8pc(|_s?d9WHxL}ZIWWjCz8u9l^k4*9Q@$&I;jrS4YeFAeQ zi)?L0!}1N>G(Im;x;6F79Vd46^mH=;FLWAWizFJ%Jg8hU;O`rWlFf2?+8nX3mp6EV zN1o@MGC^c#XKQOWNaU~-E>)Hc@Oj)L^@z&#^mUhb`+B|c`X?jy+^1lCNq2YZ}Hi$sH_GnN?Ww=&Yts*rm0UYMhDva_`xKHX{f z=;6U*9%3v?Kfj{Yj0CMoS^eme+Tr|RYdXkf~)1t*rkJ?UD-lm@{d4`fzL|)N~ zD8}nvrOx)YB9X(4A-GH*^Cvrz3ws9)_{plMKi(<}PNehL`_+5uP|;v#kwXwB%+}s% z#%Pfv6H_BRSw^NuEnHZt(`g2BuVX(aNX|4D=SjAMnMByxiN>-wPk;yV9z88A`}bj5 z0BNA3$aaY6;fZ#mm)P0b*@|pMqasjk0?&a7mxxg!`w5TQ*$$bZ8z!>*(_@2w0Dj;o zyL_Eiou5*!%T3BJ(pgP<#VSXtZjwz0PCtJ6i_`Mh^RgFnHgI*w6% zL!_=e)Rdp*;AFW{7iH92tZ@Jo8E$udUWIMB z@<}dJgeZYugA_#SoH=u5$&w{Fn?e8(!Y}Yc5J`k$4U0tQ62fJ|G75**vzVBGsQ9nG z`s&`4;OR`$y@7?+n>TMF-XbJ4EI1((hk2u$+i`3s$z3VQD3mOYN5};`&Eyi0gJ4jL zZ?!du-K505O;U?rlzK@FlIHN1Ya=V04+Sd0AVPIp&8TMKC|(x zUvLDx&Nxhk1Do%{(}O%{m?I8CzzrgDN=r-45DG3?J^y<-r8Hoc148Z3TLTlFy zf{kGF_|6ZI$ke{IM@KhZ1u=+5Li|c#Ow!)Hd(FBC^q?LA7NN;Gh}4-i*B>9En?a%o zZaX-zfXbmmhk}04hCs2dV5q^zZCkf)T^t)XZ_(1Qm75UNG`9H~LZdKE2;4yo7N!hB z(zUNCJ!oCNe0g};CY-d+6$hBcIU$IF9XT6QMTH?Oz!V{3U+_^~b8|BeSjCcP#B3pc z3sGN?$w|%ah^Yc+a270%T;eigHV(B-`wDq8dJsJ4>pu@Me;#$mXS#cWV;BVq(q*KD z-T81pNF8K}4~Q(0(KD?+3EY7gVX5k^vMD*t_+ zK!a(wrj?8_-qrqN10S^u`CUe{X0m6@O4XE51CSMzzm<@)T5nxvAmL9+$ zR0UC7I6{Yh;8hlXg^mJwp{q6{yJI zK+uvjxR8Q%JK#72B~U5h#dUby;ERjMD-TUCfTIciI(*`cnF5Q_Pks~#9T^G{ta`KbD%vgSDD?k11y5$4*o_6QpWkOGitXcm#&2YR$X0vZ~j1; z3sH||#0W+3u(-IGIoXK(7c9lQ4|*_nm@;=Caq8L6!4?pcdHNHaawDD+!F(*9ue@fK zr#D=pXvI`w;xL0|g58xrSn2S6yfE}VGM|0+8J^DB#RZmL@R~hL70Al`g2N!Rm?{2; zSi`ilU-GXQ$N*b1kG;p!g8)ZRX!_z{tgiHcd-&S0tNh%JFf73^W70A*=7mV0KQT4z zg=+7BkieUqQc;)n#2(qPce9VYC41@Z_<~gkwZu2>5O1(3{lYi!oSH_ucclm8BGwSm z(sv^OF`ak^`7laT7?^o*og(rTWMC|~wKMCsf}YUWB*b4rfN_n@DT3PeEyM$F zQ}XHmMUd{|Rz2p3g`%GS8Z!tFJDm1V1*kzxD&8OgNw@T%74M*{t1F_t5v&PTz|;X( znb+AQ!45IC4`DsRB@fSkTzdyXb+a3u0Z9pGufvDVv=Fys;F^v=-liRRxw4PH4|=k< z9VodR72^fS7 zjM(gFk7Edoh-C|eAu0koFrn@H?4$1>LI58=qX{bru4o0Xn3(N40~rvI+-<%DfFGX$ zS|G<1q@DXhw)G$)#SuV^K|xzm*}9%RdmAw1a!u0VkUSoYjW1cD2r+(&xK z%gYhX4^f#L7y`8TiW3_yuXzDb*F%R5M~z=dcvxIAYFGpV5#B5Wi=&H1!{XlfbRX%# z#!$NLSYol3$GLLlN(dNUNqpmun04&?5HN$CGM07rzfy6DtsvWunHsV8k)9V{d@(_( zVP9oq5%KpolXfEm4EGf)Rsd|o!w(~B1)m8Ct@n{0=puxXz@?4%@dr;NphH%#UJYQ; zkfE0@UHUcX!JC+!osC_m5R@;Ba<>dZ`LT7v*9Zc@edLdT^Os~E&~tC@J>bs+{@j}b Z_ip9o=XZF$ZAu$O{K1`Z;MeEN{{f@AXfgl* literal 0 HcmV?d00001 diff --git a/res/install-win/tomcat.nsi b/res/install-win/tomcat.nsi new file mode 100644 index 0000000..905472b --- /dev/null +++ b/res/install-win/tomcat.nsi @@ -0,0 +1,1388 @@ +; 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. + +; Tomcat script for Nullsoft Installer + +Unicode true + +!ifdef UNINSTALLONLY + OutFile "tempinstaller.exe" +!else + OutFile tomcat-installer.exe +!endif + + ;Compression options + CRCCheck on + SetCompressor /SOLID lzma + + Name "Apache Tomcat" + + ;Product information + VIAddVersionKey ProductName "Apache Tomcat" + VIAddVersionKey CompanyName "Apache Software Foundation" + VIAddVersionKey LegalCopyright "Copyright (c) 1999-@YEAR@ The Apache Software Foundation" + VIAddVersionKey FileDescription "Apache Tomcat Installer" + VIAddVersionKey FileVersion "2.0" + VIAddVersionKey ProductVersion "@VERSION@" + VIAddVersionKey Comments "tomcat.apache.org" + VIAddVersionKey InternalName "apache-tomcat-@VERSION@.exe" + VIProductVersion @VERSION_NUMBER@ + +!include "MUI2.nsh" +!include "nsDialogs.nsh" +!include "StrFunc.nsh" +!include "LogicLib.nsh" +!include "FileFunc.nsh" +!include "TextFunc.nsh" +${StrRep} +${Using:StrFunc} StrLoc + +Var JavaHome +Var JavaExe +Var JvmDll +Var Arch +Var ResetInstDir +Var TomcatPortShutdown +Var TomcatPortHttp +Var TomcatMenuEntriesEnable +Var TomcatShortcutAllUsers +Var TomcatServiceName +Var TomcatServiceNameAlreadyInstalled +Var TomcatServiceDefaultName +Var TomcatServiceFileName +Var TomcatServiceManagerFileName +Var TomcatAdminEnable +Var TomcatAdminUsername +Var TomcatAdminPassword +Var TomcatAdminRoles + +Var TomcatAdminEnableConfigured +Var TomcatAdminRolesConfigured +Var TomcatMenuEntriesEnableConfigured + +; Variables that store handles of dialog controls +Var CtlJavaHome +Var CtlTomcatPortShutdown +Var CtlTomcatPortHttp +Var CtlTomcatServiceName +Var CtlTomcatShortcutAllUsers +Var CtlTomcatAdminUsername +Var CtlTomcatAdminPassword +Var CtlTomcatAdminRoles + +; Handle of the service-install.log file +; It is opened in "Core" section and closed in "-post" +Var ServiceInstallLog + +;-------------------------------- +;Configuration + + !define MUI_HEADERIMAGE + !define MUI_HEADERIMAGE_RIGHT + !define MUI_HEADERIMAGE_BITMAP header.bmp + !define MUI_WELCOMEFINISHPAGE_BITMAP side_left.bmp + !define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\webapps\ROOT\RELEASE-NOTES.txt" + !define MUI_FINISHPAGE_RUN + !define MUI_FINISHPAGE_RUN_FUNCTION "startService" + !define MUI_FINISHPAGE_NOREBOOTSUPPORT + + !define MUI_ABORTWARNING + + !define MUI_ICON tomcat.ico + !define MUI_UNICON tomcat.ico + + ;Install Page order + !insertmacro MUI_PAGE_WELCOME + ; Show file named "INSTALLLICENSE" + !insertmacro MUI_PAGE_LICENSE INSTALLLICENSE + ; Use custom onLeave function with COMPONENTS page + !define MUI_PAGE_CUSTOMFUNCTION_LEAVE pageComponentsLeave + !insertmacro MUI_PAGE_COMPONENTS + Page custom pageConfiguration pageConfigurationLeave "$(TEXT_CONF_PAGETITLE)" + Page custom pageChooseJVM pageChooseJVMLeave "$(TEXT_JVM_PAGETITLE)" + !define MUI_PAGE_CUSTOMFUNCTION_LEAVE pageDirectoryLeave + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_INSTFILES + Page custom CheckUserType + !insertmacro MUI_PAGE_FINISH + + !ifdef UNINSTALLONLY + ;Uninstall Page order + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + !endif + + ;Language + !insertmacro MUI_LANGUAGE English + + ;Install Options pages + LangString TEXT_JVM_TITLE ${LANG_ENGLISH} "Java Virtual Machine" + LangString TEXT_JVM_SUBTITLE ${LANG_ENGLISH} "Java Virtual Machine path selection." + LangString TEXT_JVM_PAGETITLE ${LANG_ENGLISH} ": Java Virtual Machine path selection" + + LangString TEXT_INSTDIR_NOT_EMPTY ${LANG_ENGLISH} "The specified installation directory is not empty. Do you wish to continue?" + LangString TEXT_CONF_TITLE ${LANG_ENGLISH} "Configuration" + LangString TEXT_CONF_SUBTITLE ${LANG_ENGLISH} "Tomcat basic configuration." + LangString TEXT_CONF_PAGETITLE ${LANG_ENGLISH} ": Configuration Options" + + LangString TEXT_JVM_LABEL1 ${LANG_ENGLISH} "Please select the path of a Java @MIN_JAVA_VERSION@ or later JRE installed on your system." + LangString TEXT_CONF_LABEL_PORT_SHUTDOWN ${LANG_ENGLISH} "Server Shutdown Port" + LangString TEXT_CONF_LABEL_PORT_HTTP ${LANG_ENGLISH} "HTTP/1.1 Connector Port" + LangString TEXT_CONF_LABEL_SERVICE_NAME ${LANG_ENGLISH} "Windows Service Name" + LangString TEXT_CONF_LABEL_SHORTCUT_ALL_USERS ${LANG_ENGLISH} "Create shortcuts for all users" + LangString TEXT_CONF_LABEL_ADMIN ${LANG_ENGLISH} "Tomcat Administrator Login (optional)" + LangString TEXT_CONF_LABEL_ADMINUSERNAME ${LANG_ENGLISH} "User Name" + LangString TEXT_CONF_LABEL_ADMINPASSWORD ${LANG_ENGLISH} "Password" + LangString TEXT_CONF_LABEL_ADMINROLES ${LANG_ENGLISH} "Roles" + + ;Component-selection page + LangString DESC_SecTomcat ${LANG_ENGLISH} "Install the Tomcat Servlet container as a Windows service." + LangString DESC_SecTomcatCore ${LANG_ENGLISH} "Install the Tomcat Servlet container core and create the Windows service." + LangString DESC_SecTomcatService ${LANG_ENGLISH} "Automatically start the Tomcat service when the computer is started." + LangString DESC_SecTomcatNative ${LANG_ENGLISH} "Install APR based Tomcat native .dll to enable the OpenSSL based TLS implementation for HTTP connectors." + LangString DESC_SecMenu ${LANG_ENGLISH} "Create a Start Menu program group for Tomcat." + LangString DESC_SecDocs ${LANG_ENGLISH} "Install the Tomcat documentation bundle. This includes documentation on the servlet container and its configuration options, on the Jasper JSP page compiler, as well as on the native webserver connectors." + LangString DESC_SecManager ${LANG_ENGLISH} "Install the Tomcat Manager administrative web application." + LangString DESC_SecHostManager ${LANG_ENGLISH} "Install the Tomcat Host Manager administrative web application." + LangString DESC_SecExamples ${LANG_ENGLISH} "Install the Servlet and JSP examples web application." + + ;Install types + InstType Normal + InstType Minimum + InstType Full + + ReserveFile System.dll + ReserveFile nsDialogs.dll + ReserveFile tomcat-users_1.xml + ReserveFile tomcat-users_2.xml + +;-------------------------------- +;Installer Sections + +SubSection "Tomcat" SecTomcat + +Section "Core" SecTomcatCore + + SectionIn 1 2 3 RO + + ${If} ${Silent} + Call checkJava + ${EndIf} + + SetOutPath $INSTDIR + File tomcat.ico + File LICENSE + File NOTICE + File RELEASE-NOTES + SetOutPath $INSTDIR\lib + File /r lib\*.* + ; Note: just calling 'SetOutPath' will create the empty folders for us + SetOutPath $INSTDIR\logs + SetOutPath $INSTDIR\work + SetOutPath $INSTDIR\temp + SetOutPath $INSTDIR\bin + File bin\bootstrap.jar + File bin\tomcat-juli.jar + File bin\*.bat + SetOutPath $INSTDIR\conf + File conf\*.* + SetOutPath $INSTDIR\webapps\ROOT + File /r webapps\ROOT\*.* + + Call configure + + DetailPrint "Using Jvm: $JavaHome" + + StrCpy $R0 $TomcatServiceName + StrCpy $TomcatServiceFileName $R0.exe + StrCpy $TomcatServiceManagerFileName $R0w.exe + + SetOutPath $INSTDIR\bin + File /oname=$TomcatServiceManagerFileName bin\tomcat@VERSION_MAJOR@w.exe + + ; Get the current platform x86 / AMD64 / IA64 + ${If} $Arch == "x86" + File /oname=$TomcatServiceFileName bin\tomcat@VERSION_MAJOR@.exe + ${ElseIf} $Arch == "x64" + File /oname=$TomcatServiceFileName bin\x64\tomcat@VERSION_MAJOR@.exe + ${EndIf} + + FileOpen $ServiceInstallLog "$INSTDIR\logs\service-install.log" a + FileSeek $ServiceInstallLog 0 END + + InstallRetry: + FileWrite $ServiceInstallLog '"$INSTDIR\bin\$TomcatServiceFileName" //IS//$TomcatServiceName --DisplayName "Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" --Description "Apache Tomcat @VERSION@ Server - https://tomcat.apache.org/" --LogPath "$INSTDIR\logs" --Install "$INSTDIR\bin\$TomcatServiceFileName" --Jvm "$JvmDll" --StartPath "$INSTDIR" --StopPath "$INSTDIR"' + FileWrite $ServiceInstallLog "$\r$\n" + ClearErrors + DetailPrint "Installing $TomcatServiceName service" + nsExec::ExecToStack '"$INSTDIR\bin\$TomcatServiceFileName" //IS//$TomcatServiceName --DisplayName "Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" --Description "Apache Tomcat @VERSION@ Server - https://tomcat.apache.org/" --LogPath "$INSTDIR\logs" --Install "$INSTDIR\bin\$TomcatServiceFileName" --Jvm "$JvmDll" --StartPath "$INSTDIR" --StopPath "$INSTDIR"' + Pop $0 + Pop $1 + StrCmp $0 "0" InstallOk + FileWrite $ServiceInstallLog "Install failed: $0 $1$\r$\n" + MessageBox MB_ABORTRETRYIGNORE|MB_ICONSTOP \ + "Failed to install $TomcatServiceName service.$\r$\nCheck your settings and permissions.$\r$\nIgnore and continue anyway (not recommended)?" \ + /SD IDIGNORE IDIGNORE InstallOk IDRETRY InstallRetry + Quit + InstallOk: + ClearErrors + + ; Will be closed in "-post" section + ; FileClose $ServiceInstallLog +SectionEnd + +Section "Service Startup" SecTomcatService + + SectionIn 3 + + ${If} $ServiceInstallLog != "" + FileWrite $ServiceInstallLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --Startup auto' + FileWrite $ServiceInstallLog "$\r$\n" + ${EndIf} + DetailPrint "Configuring $TomcatServiceName service" + nsExec::ExecToLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --Startup auto' + + ClearErrors + +SectionEnd + +Section "Native" SecTomcatNative + + SectionIn 3 + + SetOutPath $INSTDIR\bin + + ${If} $Arch == "x86" + File bin\tcnative-2.dll + ${ElseIf} $Arch == "x64" + File /oname=tcnative-2.dll bin\x64\tcnative-2.dll + ${EndIf} + + ClearErrors + +SectionEnd + +SubSectionEnd + +Section "Start Menu Items" SecMenu + + SectionIn 1 2 3 + +SectionEnd + +Section "Documentation" SecDocs + + SectionIn 1 3 + SetOutPath $INSTDIR\webapps\docs + File /r webapps\docs\*.* + +SectionEnd + +Section "Manager" SecManager + + SectionIn 1 3 + + SetOverwrite on + SetOutPath $INSTDIR\webapps\manager + File /r webapps\manager\*.* + +SectionEnd + +Section "Host Manager" SecHostManager + + SectionIn 3 + + SetOverwrite on + SetOutPath $INSTDIR\webapps\host-manager + File /r webapps\host-manager\*.* + +SectionEnd + +Section "Examples" SecExamples + + SectionIn 3 + + SetOverwrite on + SetOutPath $INSTDIR\webapps\examples + File /r webapps\examples\*.* + +SectionEnd + +Section -post + ${If} $ServiceInstallLog != "" + FileWrite $ServiceInstallLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --Classpath "$INSTDIR\bin\bootstrap.jar;$INSTDIR\bin\tomcat-juli.jar" --StartClass org.apache.catalina.startup.Bootstrap --StopClass org.apache.catalina.startup.Bootstrap --StartParams start --StopParams stop --StartMode jvm --StopMode jvm' + FileWrite $ServiceInstallLog "$\r$\n" + FileWrite $ServiceInstallLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --JvmOptions "-Dcatalina.home=$INSTDIR#-Dcatalina.base=$INSTDIR#-Djava.io.tmpdir=$INSTDIR\temp#-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager#-Djava.util.logging.config.file=$INSTDIR\conf\logging.properties"' + FileWrite $ServiceInstallLog "$\r$\n" + FileWrite $ServiceInstallLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --JvmOptions9 "--add-opens=java.base/java.lang=ALL-UNNAMED#--add-opens=java.base/java.io=ALL-UNNAMED#--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED"' + FileWrite $ServiceInstallLog "$\r$\n" + FileWrite $ServiceInstallLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --StdOutput auto --StdError auto --JvmMs 128 --JvmMx 256' + FileWrite $ServiceInstallLog "$\r$\n" + FileClose $ServiceInstallLog + ${EndIf} + + DetailPrint "Configuring $TomcatServiceName service" + nsExec::ExecToLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --Classpath "$INSTDIR\bin\bootstrap.jar;$INSTDIR\bin\tomcat-juli.jar" --StartClass org.apache.catalina.startup.Bootstrap --StopClass org.apache.catalina.startup.Bootstrap --StartParams start --StopParams stop --StartMode jvm --StopMode jvm' + nsExec::ExecToLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --JvmOptions "-Dcatalina.home=$INSTDIR#-Dcatalina.base=$INSTDIR#-Djava.io.tmpdir=$INSTDIR\temp#-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager#-Djava.util.logging.config.file=$INSTDIR\conf\logging.properties"' + nsExec::ExecToLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --JvmOptions9 "--add-opens=java.base/java.lang=ALL-UNNAMED#--add-opens=java.base/java.io=ALL-UNNAMED#--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED"' + nsExec::ExecToLog '"$INSTDIR\bin\$TomcatServiceFileName" //US//$TomcatServiceName --StdOutput auto --StdError auto --JvmMs 128 --JvmMx 256' + + ${If} $TomcatShortcutAllUsers == "1" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Run" "ApacheTomcatMonitor@VERSION_MAJOR_MINOR@_$TomcatServiceName" '"$INSTDIR\bin\$TomcatServiceManagerFileName" //MS//$TomcatServiceName' + ${Else} + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "ApacheTomcatMonitor@VERSION_MAJOR_MINOR@_$TomcatServiceName" '"$INSTDIR\bin\$TomcatServiceManagerFileName" //MS//$TomcatServiceName' + ${EndIf} + + ${If} $TomcatMenuEntriesEnable == "1" + Call createShortcuts + ${EndIf} + + !ifndef UNINSTALLONLY + SetOutPath $INSTDIR + ; this packages the signed uninstaller + File Uninstall.exe + !endif + + WriteRegStr HKLM "SOFTWARE\Apache Software Foundation\Tomcat\@VERSION_MAJOR_MINOR@\$TomcatServiceName" "InstallPath" $INSTDIR + WriteRegStr HKLM "SOFTWARE\Apache Software Foundation\Tomcat\@VERSION_MAJOR_MINOR@\$TomcatServiceName" "Version" @VERSION@ + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" \ + "DisplayName" "Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName (remove only)" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" \ + "DisplayVersion" @VERSION@ + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" \ + "DisplayIcon" "$\"$INSTDIR\tomcat.ico$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" \ + "Publisher" "The Apache Software Foundation" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" \ + "UninstallString" "$\"$INSTDIR\Uninstall.exe$\" -ServiceName=$\"$TomcatServiceName$\"" + + ; Configure file permissions + ; S-1-5-19 LocalService + ; S-1-5-32-544 Local Administrators group + ; S-1-5-18 Local System + ; S-1-5-11 Authenticated users + ; + ; Grant admins, LocalService and Local System full control full control + nsExec::ExecToStack 'icacls "$INSTDIR" /inheritance:r /grant *S-1-5-19:(OI)(CI)(F) /grant *S-1-5-32-544:(OI)(CI)(F) /grant *S-1-5-18:(OI)(CI)(F)' + Pop $0 + Pop $1 + StrCmp $0 "0" SetGroupPermissionsOk + FileWrite $ServiceInstallLog "Install failed (setting file permissions): $0 $1$\r$\n" + MessageBox MB_YESNO|MB_ICONSTOP \ + "Failed to set file permissions.$\r$\nCheck your settings and permissions.$\r$\nIgnore and continue anyway (not recommended)?" \ + /SD IDNO IDYES SetGroupPermissionsOk + Quit + SetGroupPermissionsOk: + ClearErrors + + ; Make the icon readable to all authenticated users so it appears correctly in the uninstall UI + nsExec::ExecToStack 'icacls "$INSTDIR\tomcat.ico" /inheritance:e /grant *S-1-5-11:(R)' + Pop $0 + Pop $1 + StrCmp $0 "0" SetIconPermissionsOk + FileWrite $ServiceInstallLog "Install failed (setting file permissions for icon): $0 $1$\r$\n" + MessageBox MB_YESNO|MB_ICONSTOP \ + "Failed to set icon file permissions.$\r$\nCheck your settings and permissions.$\r$\nIgnore and continue anyway (not recommended)?" \ + /SD IDNO IDYES SetIconPermissionsOk + Quit + SetIconPermissionsOk: + ClearErrors + + ; Make the uninstaller readable and executable to all authenticated users so the user that installed Tomcat can also uninstall it + nsExec::ExecToStack 'icacls "$INSTDIR\Uninstall.exe" /inheritance:e /grant *S-1-5-11:(RX)' + Pop $0 + Pop $1 + StrCmp $0 "0" SetUninstallerPermissionsOk + FileWrite $ServiceInstallLog "Install failed (setting file permissions for uninstaller): $0 $1$\r$\n" + MessageBox MB_YESNO|MB_ICONSTOP \ + "Failed to set uninstaller file permissions.$\r$\nCheck your settings and permissions.$\r$\nIgnore and continue anyway (not recommended)?" \ + /SD IDNO IDYES SetUninstallerPermissionsOk + Quit + SetUninstallerPermissionsOk: + ClearErrors + +SectionEnd + +!define ReadFromConfigIni "!insertmacro ReadFromConfigIni" +!macro ReadFromConfigIni Return_Variable Key_Name Config_File + Push "${Config_File}" + Push "${Return_Variable}" + Push "${Key_Name}" + Call ReadFromConfigIni + IfErrors +2 + StrCpy ${Return_Variable} $1 +!macroend + +Function ReadFromConfigIni + ClearErrors + ; Stack: + Pop $0 ; Stack: + Pop $1 ; Stack: + Pop $2 ; Stack: -empty- + + ${ConfigRead} $2 '$0=' $1 ; +FunctionEnd + +Function .onInit + !ifdef UNINSTALLONLY + ; If UNINSTALLONLY is defined, then we aren't supposed to do anything except write out + ; the installer. This is better than processing a command line option as it means + ; this entire code path is not present in the final (real) installer. + WriteUninstaller "$EXEDIR\Uninstall.exe" + Quit + !endif + + ${GetParameters} $R0 + ClearErrors + + ${GetOptions} "$R0" "/?" $R1 + ${IfNot} ${Errors} + MessageBox MB_OK|MB_ICONINFORMATION 'Available options:$\r$\n\ + /S - Silent install.$\r$\n\ + /C=config.ini - specify full path of config file to override default values.$\r$\n\ + /D=INSTDIR - Specify installation directory.' + Abort + ${EndIf} + ClearErrors + + StrCpy $ResetInstDir "$INSTDIR" + + ;Initialize default values + StrCpy $JavaHome "" + StrCpy $TomcatPortShutdown "-1" + StrCpy $TomcatPortHttp "8080" + StrCpy $TomcatMenuEntriesEnable "0" + StrCpy $TomcatShortcutAllUsers "0" + StrCpy $TomcatServiceDefaultName "Tomcat@VERSION_MAJOR@" + StrCpy $TomcatServiceName $TomcatServiceDefaultName + StrCpy $TomcatServiceFileName "Tomcat@VERSION_MAJOR@.exe" + StrCpy $TomcatServiceManagerFileName "Tomcat@VERSION_MAJOR@w.exe" + StrCpy $TomcatAdminEnable "0" + StrCpy $TomcatAdminUsername "" + StrCpy $TomcatAdminPassword "" + StrCpy $TomcatAdminRoles "" + + ;override default values in case config file was passed in + ${GetOptions} "$R0" "/C=" $R2 + ${IfNot} ${Errors} + ${ReadFromConfigIni} $JavaHome "JavaHome" $R2 + ${ReadFromConfigIni} $TomcatPortShutdown "TomcatPortShutdown" $R2 + ${ReadFromConfigIni} $TomcatPortHttp "TomcatPortHttp" $R2 + ${ReadFromConfigIni} $TomcatMenuEntriesEnable "TomcatMenuEntriesEnable" $R2 + ${ReadFromConfigIni} $TomcatShortcutAllUsers "TomcatShortcutAllUsers" $R2 + ${ReadFromConfigIni} $TomcatServiceDefaultName "TomcatServiceDefaultName" $R2 + ${ReadFromConfigIni} $TomcatServiceName "TomcatServiceName" $R2 + ${ReadFromConfigIni} $TomcatServiceFileName "TomcatServiceFileName" $R2 + ${ReadFromConfigIni} $TomcatServiceManagerFileName "TomcatServiceManagerFileName" $R2 + ${ReadFromConfigIni} $TomcatAdminEnable "TomcatAdminEnable" $R2 + ${ReadFromConfigIni} $TomcatAdminUsername "TomcatAdminUsername" $R2 + ${ReadFromConfigIni} $TomcatAdminPassword "TomcatAdminPassword" $R2 + ${ReadFromConfigIni} $TomcatAdminRoles "TomcatAdminRoles" $R2 + ${EndIf} + ClearErrors + + ;take a copy of the default / configured values + StrCpy $TomcatAdminEnableConfigured $TomcatAdminEnable + StrCpy $TomcatAdminRolesConfigured $TomcatAdminRoles + StrCpy $TomcatMenuEntriesEnableConfigured $TomcatMenuEntriesEnable + +FunctionEnd + +Function pageChooseJVM + !insertmacro MUI_HEADER_TEXT "$(TEXT_JVM_TITLE)" "$(TEXT_JVM_SUBTITLE)" + ${If} $JavaHome == "" + Call findJavaHome + Pop $JavaHome + ${EndIf} + + nsDialogs::Create 1018 + Pop $R0 + + ${NSD_CreateLabel} 0 5u 100% 25u "$(TEXT_JVM_LABEL1)" + Pop $R0 + ${NSD_CreateDirRequest} 0 65u 280u 13u "$JavaHome" + Pop $CtlJavaHome + ${NSD_CreateBrowseButton} 282u 65u 15u 13u "..." + Pop $R0 + ${NSD_OnClick} $R0 pageChooseJVM_onDirBrowse + + ${NSD_SetFocus} $CtlJavaHome + nsDialogs::Show +FunctionEnd + +; onClick function for button next to DirRequest control +Function pageChooseJVM_onDirBrowse + ; R0 is HWND of the button, it is on top of the stack + Pop $R0 + + ${NSD_GetText} $CtlJavaHome $R1 + nsDialogs::SelectFolderDialog "" "$R1" + Pop $R1 + + ${If} $R1 != "error" + ${NSD_SetText} $CtlJavaHome $R1 + ${EndIf} +FunctionEnd + +Function pageChooseJVMLeave + ${NSD_GetText} $CtlJavaHome $JavaHome + ${If} $JavaHome == "" + Abort + ${EndIf} + + Call checkJava +FunctionEnd + +; onLeave function for the COMPONENTS page +; It updates options based on what components were selected. +; +Function pageComponentsLeave + StrCpy $TomcatAdminEnable $TomcatAdminEnableConfigured + StrCpy $TomcatAdminRoles $TomcatAdminRolesConfigured + StrCpy $TomcatMenuEntriesEnable $TomcatMenuEntriesEnableConfigured + + SectionGetFlags ${SecManager} $0 + IntOp $0 $0 & ${SF_SELECTED} + ${If} $0 <> 0 + StrCpy $TomcatAdminEnable "1" + ${If} $TomcatAdminRoles != "" + ${StrLoc} $0 $TomcatAdminRoles "manager-gui" 0 + ${If} $0 == "" + StrCpy $TomcatAdminRoles "$TomcatAdminRoles,manager-gui" + ${EndIf} + ${Else} + StrCpy $TomcatAdminRoles "manager-gui" + ${EndIf} + ${EndIf} + + SectionGetFlags ${SecHostManager} $0 + IntOp $0 $0 & ${SF_SELECTED} + ${If} $0 <> 0 + StrCpy $TomcatAdminEnable "1" + ${If} $TomcatAdminRoles != "" + ${StrLoc} $0 $TomcatAdminRoles "admin-gui" 0 + ${If} $0 == "" + StrCpy $TomcatAdminRoles "$TomcatAdminRoles,admin-gui" + ${EndIf} + ${Else} + StrCpy $TomcatAdminRoles "admin-gui" + ${EndIf} + ${EndIf} + + SectionGetFlags ${SecMenu} $0 + IntOp $0 $0 & ${SF_SELECTED} + ${If} $0 <> 0 + StrCpy $TomcatMenuEntriesEnable "1" + ${EndIf} +FunctionEnd + +Function pageDirectoryLeave + ${DirState} "$INSTDIR" $0 + ${If} $0 == 1 ;folder is full. (other values: 0: empty, -1: not found) + ;query selection + MessageBox MB_OKCANCEL|MB_ICONQUESTION "$(TEXT_INSTDIR_NOT_EMPTY)" /SD IDOK IDCANCEL notok + Goto ok + notok: + Abort + ok: + ${EndIf} +FunctionEnd + +Function pageConfiguration + !insertmacro MUI_HEADER_TEXT "$(TEXT_CONF_TITLE)" "$(TEXT_CONF_SUBTITLE)" + + nsDialogs::Create 1018 + Pop $R0 + + ${NSD_CreateLabel} 0 2u 100u 14u "$(TEXT_CONF_LABEL_PORT_SHUTDOWN)" + Pop $R0 + + ${NSD_CreateText} 150u 0 50u 12u "$TomcatPortShutdown" + Pop $CtlTomcatPortShutdown + ${NSD_SetTextLimit} $CtlTomcatPortShutdown 5 + + ${NSD_CreateLabel} 0 19u 100u 14u "$(TEXT_CONF_LABEL_PORT_HTTP)" + Pop $R0 + + ${NSD_CreateText} 150u 17u 50u 12u "$TomcatPortHttp" + Pop $CtlTomcatPortHttp + ${NSD_SetTextLimit} $CtlTomcatPortHttp 5 + + ${NSD_CreateLabel} 0 57u 140u 14u "$(TEXT_CONF_LABEL_SERVICE_NAME)" + Pop $R0 + + ${NSD_CreateText} 150u 55u 140u 12u "$TomcatServiceName" + Pop $CtlTomcatServiceName + + ${If} $TomcatMenuEntriesEnable == "1" + ${NSD_CreateLabel} 0 75u 100u 14u "$(TEXT_CONF_LABEL_SHORTCUT_ALL_USERS)" + Pop $R0 + ${NSD_CreateCheckBox} 150u 74u 10u 10u "$TomcatShortcutAllUsers" + Pop $CtlTomcatShortcutAllUsers + ${EndIf} + + ${If} $TomcatAdminEnable == "1" + ${NSD_CreateLabel} 0 93u 90u 28u "$(TEXT_CONF_LABEL_ADMIN)" + Pop $R0 + ${NSD_CreateLabel} 100u 93u 40u 14u "$(TEXT_CONF_LABEL_ADMINUSERNAME)" + Pop $R0 + ${NSD_CreateText} 150u 91u 110u 12u "$TomcatAdminUsername" + Pop $CtlTomcatAdminUsername + ${NSD_CreateLabel} 100u 110u 40u 12u "$(TEXT_CONF_LABEL_ADMINPASSWORD)" + Pop $R0 + ${NSD_CreatePassword} 150u 108u 110u 12u "$TomcatAdminPassword" + Pop $CtlTomcatAdminPassword + ${NSD_CreateLabel} 100u 127u 40u 14u "$(TEXT_CONF_LABEL_ADMINROLES)" + Pop $R0 + ${NSD_CreateText} 150u 125u 110u 12u "$TomcatAdminRoles" + Pop $CtlTomcatAdminRoles + ${EndIf} + + ${NSD_SetFocus} $CtlTomcatPortShutdown + nsDialogs::Show +FunctionEnd + +Function pageConfigurationLeave + ${NSD_GetText} $CtlTomcatPortShutdown $TomcatPortShutdown + ${NSD_GetText} $CtlTomcatPortHttp $TomcatPortHttp + ${NSD_GetText} $CtlTomcatServiceName $TomcatServiceName + ${If} $TomcatMenuEntriesEnable == "1" + ${NSD_GetState} $CtlTomcatShortcutAllUsers $TomcatShortcutAllUsers + ${EndIf} + ${If} $TomcatAdminEnable == "1" + ${NSD_GetText} $CtlTomcatAdminUsername $TomcatAdminUsername + ${NSD_GetText} $CtlTomcatAdminPassword $TomcatAdminPassword + ${NSD_GetText} $CtlTomcatAdminRoles $TomcatAdminRoles + ${EndIf} + + ${If} $TomcatPortShutdown == "" + MessageBox MB_ICONEXCLAMATION|MB_OK 'The shutdown port may not be empty' + Abort "Config not right" + Goto exit + ${EndIf} + + ${If} $TomcatPortHttp == "" + MessageBox MB_ICONEXCLAMATION|MB_OK 'The HTTP port may not be empty' + Abort "Config not right" + Goto exit + ${EndIf} + + ${If} $TomcatServiceName == "" + MessageBox MB_ICONEXCLAMATION|MB_OK 'The Service Name may not be empty' + Abort "Config not right" + Goto exit + ${EndIf} + + ReadRegStr $TomcatServiceNameAlreadyInstalled HKLM "SYSTEM\CurrentControlSet\Services\$TomcatServiceName" \ + "DisplayName" + ${If} $TomcatServiceNameAlreadyInstalled != "" + MessageBox MB_ICONEXCLAMATION|MB_OK 'A service with the given Service Name is already installed on this machine. \ + Please choose another Service Name' + Abort "Config not right" + Goto exit + ${EndIf} + + Push $TomcatServiceName + Call validateServiceName + Pop $0 + + IntCmp $0 1 exit + MessageBox MB_ICONEXCLAMATION|MB_OK 'The Service Name may not contain a space or any of the following characters: <>:"/\:|?*' + Abort "Config not right" + exit: +FunctionEnd + +; Validates that a service name does not use any of the invalid +; characters: <>:"/\:|?* +; Note that space is also not permitted although it will be once +; Tomcat is using Daemon 1.0.6 or later +; +; Put the proposed service name on the stack +; If the name is valid, a 1 will be left on the stack +; If the name is invalid, a 0 will be left on the stack +Function validateServiceName + Pop $0 + StrLen $1 $0 + StrCpy $3 '<>:"/\:|?* ' + StrLen $4 $3 + + loopInput: + IntOp $1 $1 - 1 + IntCmp $1 -1 valid + loopTestChars: + IntOp $4 $4 - 1 + IntCmp $4 -1 loopTestCharsDone + StrCpy $2 $0 1 $1 + StrCpy $5 $3 1 $4 + StrCmp $2 $5 invalid loopTestChars + loopTestCharsDone: + StrLen $4 $3 + Goto loopInput + + invalid: + Push 0 + Goto exit + + valid: + Push 1 + exit: +FunctionEnd + +;-------------------------------- +;Descriptions +!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${SecTomcat} $(DESC_SecTomcat) + !insertmacro MUI_DESCRIPTION_TEXT ${SecTomcatCore} $(DESC_SecTomcatCore) + !insertmacro MUI_DESCRIPTION_TEXT ${SecTomcatService} $(DESC_SecTomcatService) + !insertmacro MUI_DESCRIPTION_TEXT ${SecTomcatNative} $(DESC_SecTomcatNative) + !insertmacro MUI_DESCRIPTION_TEXT ${SecMenu} $(DESC_SecMenu) + !insertmacro MUI_DESCRIPTION_TEXT ${SecDocs} $(DESC_SecDocs) + !insertmacro MUI_DESCRIPTION_TEXT ${SecManager} $(DESC_SecManager) + !insertmacro MUI_DESCRIPTION_TEXT ${SecHostManager} $(DESC_SecHostManager) + !insertmacro MUI_DESCRIPTION_TEXT ${SecExamples} $(DESC_SecExamples) +!insertmacro MUI_FUNCTION_DESCRIPTION_END + + +; ===================== +; CheckUserType Function +; ===================== +; +; Check the user type, and warn if it's not an administrator. +; Taken from Examples/UserInfo that ships with NSIS. +Function CheckUserType + ClearErrors + UserInfo::GetName + IfErrors Win9x + Pop $0 + UserInfo::GetAccountType + Pop $1 + StrCmp $1 "Admin" 0 +3 + ; This is OK, do nothing + Goto done + + MessageBox MB_OK|MB_ICONEXCLAMATION 'Note: the current user is not an administrator. \ + To run Tomcat as a Windows service, you must be an administrator. \ + You can still run Tomcat from the command-line as this type of user.' + Goto done + + Win9x: + # This one means you don't need to care about admin or + # not admin because Windows 9x doesn't either + MessageBox MB_OK "Error! This DLL can't run under Windows 9x!" + + done: +FunctionEnd + +; ================== +; checkJava Function +; ================== +; +; Checks that a valid JVM has been specified or a suitable default is available +; Sets $JavaHome, $JavaExe and $JvmDll accordingly +; Determines if the JVM is 32-bit or 64-bit and sets $Arch accordingly. For +; 64-bit JVMs, also determines if it is x64 or ia64 +Function checkJava + + ${If} $JavaHome == "" + ; E.g. if a silent install + Call findJavaHome + Pop $JavaHome + ${EndIf} + + ${If} $JavaHome == "" + ${OrIfNot} ${FileExists} "$JavaHome\bin\java.exe" + IfSilent +2 + MessageBox MB_OK|MB_ICONSTOP "No Java Virtual Machine found in folder:$\r$\n$JavaHome" + DetailPrint "No Java Virtual Machine found in folder:$\r$\n$JavaHome" + Quit + ${EndIf} + + StrCpy "$JavaExe" "$JavaHome\bin\java.exe" + + ; Need path to jvm.dll to configure the service - uses $JavaHome + Call findJVMPath + Pop $5 + ${If} $5 == "" + IfSilent +2 + MessageBox MB_OK|MB_ICONSTOP "No Java Virtual Machine found in folder:$\r$\n$5" + DetailPrint "No Java Virtual Machine found in folder:$\r$\n$5" + Quit + ${EndIf} + + StrCpy "$JvmDll" $5 + + ; Read PE header of JvmDll to check for architecture + ; 1. Jump to 0x3c and read offset of PE header + ; 2. Jump to offset. Read PE header signature. It must be 'PE'\0\0 (50 45 00 00). + ; 3. The next word gives the machine type. + ; 0x014c: x86 + ; 0x8664: x64 + ; 0x0200: i64 + ClearErrors + FileOpen $R1 "$JvmDll" r + IfErrors WrongPEHeader + + FileSeek $R1 0x3c SET + FileReadByte $R1 $R2 + FileReadByte $R1 $R3 + IntOp $R3 $R3 << 8 + IntOp $R2 $R2 + $R3 + + FileSeek $R1 $R2 SET + FileReadByte $R1 $R2 + IntCmp $R2 0x50 +1 WrongPEHeader WrongPEHeader + FileReadByte $R1 $R2 + IntCmp $R2 0x45 +1 WrongPEHeader WrongPEHeader + FileReadByte $R1 $R2 + IntCmp $R2 0 +1 WrongPEHeader WrongPEHeader + FileReadByte $R1 $R2 + IntCmp $R2 0 +1 WrongPEHeader WrongPEHeader + + FileReadByte $R1 $R2 + FileReadByte $R1 $R3 + IntOp $R3 $R3 << 8 + IntOp $R2 $R2 + $R3 + + IntCmp $R2 0x014c +1 +4 +4 + StrCpy "$Arch" "x86" + SetRegView 32 + Goto DonePEHeader + + IntCmp $R2 0x8664 +1 +4 +4 + StrCpy "$Arch" "x64" + SetRegView 64 + Goto DonePEHeader + + IntCmp $R2 0x0200 +1 +4 +4 + StrCpy "$Arch" "i64" + SetRegView 64 + Goto DonePEHeader + +WrongPEHeader: + IfSilent +2 + MessageBox MB_OK|MB_ICONEXCLAMATION 'Cannot read PE header from "$JvmDll"$\r$\nWill assume that the architecture is x86.' + DetailPrint 'Cannot read PE header from "$JvmDll". Assuming the architecture is x86.' + SetRegView 32 + StrCpy "$Arch" "x86" + +DonePEHeader: + FileClose $R1 + + DetailPrint 'Architecture: "$Arch"' + + StrCpy $INSTDIR "$ResetInstDir" + + ; The default varies depending on 32-bit or 64-bit + ${If} "$INSTDIR" == "" + ${If} $Arch == "x86" + ${If} $TomcatServiceName == $TomcatServiceDefaultName + StrCpy $INSTDIR "$PROGRAMFILES32\Apache Software Foundation\Tomcat @VERSION_MAJOR_MINOR@" + ${Else} + StrCpy $INSTDIR "$PROGRAMFILES32\Apache Software Foundation\Tomcat @VERSION_MAJOR_MINOR@_$TomcatServiceName" + ${EndIf} + ${Else} + ${If} $TomcatServiceName == $TomcatServiceDefaultName + StrCpy $INSTDIR "$PROGRAMFILES64\Apache Software Foundation\Tomcat @VERSION_MAJOR_MINOR@" + ${Else} + StrCpy $INSTDIR "$PROGRAMFILES64\Apache Software Foundation\Tomcat @VERSION_MAJOR_MINOR@_$TomcatServiceName" + ${EndIf} + ${EndIf} + ${EndIf} + +FunctionEnd + + +; ===================== +; findJavaHome Function +; ===================== +; +; Find the JAVA_HOME used on the system, and put the result on the top of the +; stack +; Will return an empty string if the path cannot be determined +; +Function findJavaHome + + ClearErrors + StrCpy $1 "" + + ; Use the 64-bit registry first on 64-bit machines + ExpandEnvStrings $0 "%PROGRAMW6432%" + ${If} $0 != "%PROGRAMW6432%" + SetRegView 64 + ReadRegStr $2 HKLM "SOFTWARE\JavaSoft\JRE" "CurrentVersion" + ReadRegStr $1 HKLM "SOFTWARE\JavaSoft\JRE\$2" "JavaHome" + ReadRegStr $3 HKLM "SOFTWARE\JavaSoft\JRE\$2" "RuntimeLib" + + IfErrors 0 +2 + StrCpy $1 "" + ClearErrors + + ${If} $1 == "" + ReadRegStr $2 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment" "CurrentVersion" + ReadRegStr $1 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment\$2" "JavaHome" + ReadRegStr $3 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment\$2" "RuntimeLib" + + IfErrors 0 +2 + StrCpy $1 "" + ClearErrors + ${EndIf} + ${EndIf} + + ; If no 64-bit Java was found, look for 32-bit Java + ${If} $1 == "" + SetRegView 32 + ReadRegStr $2 HKLM "SOFTWARE\JavaSoft\JRE" "CurrentVersion" + ReadRegStr $1 HKLM "SOFTWARE\JavaSoft\JRE\$2" "JavaHome" + ReadRegStr $3 HKLM "SOFTWARE\JavaSoft\JRE\$2" "RuntimeLib" + + IfErrors 0 +2 + StrCpy $1 "" + ClearErrors + + ${If} $1 == "" + ReadRegStr $2 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment" "CurrentVersion" + ReadRegStr $1 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment\$2" "JavaHome" + ReadRegStr $3 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment\$2" "RuntimeLib" + + IfErrors 0 +2 + StrCpy $1 "" + ClearErrors + ${EndIf} + + ; If using 64-bit, go back to using 64-bit registry + ${If} $0 != "%PROGRAMW6432%" + SetRegView 64 + ${EndIf} + ${EndIf} + + ; If no 32-bit Java (JRE) found, look for 64-bit Java JDK + ${If} $1 == "" + ${AndIf} $0 != "%PROGRAMW6432%" + ReadRegStr $2 HKLM "SOFTWARE\JavaSoft\JDK" "CurrentVersion" + ReadRegStr $1 HKLM "SOFTWARE\JavaSoft\JDK\$2" "JavaHome" + ; "RuntimeLib" is not available here + + IfErrors 0 +2 + StrCpy $1 "" + ClearErrors + ${EndIf} + + ; If nothing found, try environment variable JAVA_HOME + ${If} $1 == "" + ExpandEnvStrings $1 "%JAVA_HOME%" + ${If} $1 == "%JAVA_HOME%" + StrCpy $1 "" + ${EndIf} + ClearErrors + ${EndIf} + + ; Put the result in the stack + Push $1 + +FunctionEnd + + +; ==================== +; FindJVMPath Function +; ==================== +; +; Find the full JVM path, and put the result on top of the stack +; Implicit argument: $JavaHome +; Will return an empty string if the path cannot be determined +; +Function findJVMPath + + ClearErrors + + ;Step one: Is this a JRE path (Program Files\Java\XXX) + StrCpy $1 "$JavaHome" + + StrCpy $2 "$1\bin\hotspot\jvm.dll" + IfFileExists "$2" FoundJvmDll + StrCpy $2 "$1\bin\server\jvm.dll" + IfFileExists "$2" FoundJvmDll + StrCpy $2 "$1\bin\client\jvm.dll" + IfFileExists "$2" FoundJvmDll + StrCpy $2 "$1\bin\classic\jvm.dll" + IfFileExists "$2" FoundJvmDll + + ;Step two: Is this a JDK path (Program Files\XXX\jre) + StrCpy $1 "$JavaHome\jre" + + StrCpy $2 "$1\bin\hotspot\jvm.dll" + IfFileExists "$2" FoundJvmDll + StrCpy $2 "$1\bin\server\jvm.dll" + IfFileExists "$2" FoundJvmDll + StrCpy $2 "$1\bin\client\jvm.dll" + IfFileExists "$2" FoundJvmDll + StrCpy $2 "$1\bin\classic\jvm.dll" + IfFileExists "$2" FoundJvmDll + + ClearErrors + + ;Step three: Read defaults from registry + ReadRegStr $1 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment" "CurrentVersion" + ReadRegStr $2 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment\$1" "RuntimeLib" + IfErrors 0 FoundJvmDll + + ;not found + StrCpy $2 "" + + FoundJvmDll: + ClearErrors + + ; Put the result in the stack + Push $2 + +FunctionEnd + + +; ================== +; Configure Function +; ================== +; +; Writes server.xml and tomcat-users.xml +; +Function configure + ; Build final server.xml + DetailPrint "Creating server.xml.new" + + FileOpen $R1 "$INSTDIR\conf\server.xml" r + FileOpen $R2 "$INSTDIR\conf\server.xml.new" w + + SERVER_XML_LOOP: + FileRead $R1 $R3 + IfErrors SERVER_XML_LEAVELOOP + ${StrRep} $R4 $R3 "8005" "$TomcatPortShutdown" + ${StrRep} $R3 $R4 "8080" "$TomcatPortHttp" + FileWrite $R2 $R3 + Goto SERVER_XML_LOOP + SERVER_XML_LEAVELOOP: + + FileClose $R1 + FileClose $R2 + + ; Replace server.xml with server.xml.new + Delete "$INSTDIR\conf\server.xml" + FileOpen $R9 "$INSTDIR\conf\server.xml" w + Push "$INSTDIR\conf\server.xml.new" + Call copyFile + FileClose $R9 + Delete "$INSTDIR\conf\server.xml.new" + + DetailPrint 'Server shutdown listener configured on port "$TomcatPortShutdown"' + DetailPrint 'HTTP/1.1 Connector configured on port "$TomcatPortHttp"' + DetailPrint "server.xml written" + + StrCpy $R5 '' + + ${If} $TomcatAdminEnable == "1" + ${AndIf} "$TomcatAdminUsername" != "" + ${AndIf} "$TomcatAdminPassword" != "" + ${AndIf} "$TomcatAdminRoles" != "" + ; Escape XML + Push $TomcatAdminUsername + Call xmlEscape + Pop $R1 + Push $TomcatAdminPassword + Call xmlEscape + Pop $R2 + Push $TomcatAdminRoles + Call xmlEscape + Pop $R3 + StrCpy $R5 '$\r$\n' + DetailPrint 'Admin user added: "$TomcatAdminUsername"' + ${EndIf} + + + ; Extract these fragments to $PLUGINSDIR. That is a temporary directory, + ; that is automatically deleted when the installer exits. + InitPluginsDir + SetOutPath $PLUGINSDIR + File tomcat-users_1.xml + File tomcat-users_2.xml + + ; Build final tomcat-users.xml + Delete "$INSTDIR\conf\tomcat-users.xml" + DetailPrint "Writing tomcat-users.xml" + FileOpen $R9 "$INSTDIR\conf\tomcat-users.xml" w + ; File will be written using current windows codepage + System::Call 'Kernel32::GetACP() i .r18' + ${If} $R8 == "932" + ; Special case where Java uses non-standard name for character set + FileWrite $R9 "$\r$\n" + ${Else} + FileWrite $R9 "$\r$\n" + ${EndIf} + Push "$PLUGINSDIR\tomcat-users_1.xml" + Call copyFile + FileWrite $R9 $R5 + Push "$PLUGINSDIR\tomcat-users_2.xml" + Call copyFile + + FileClose $R9 + DetailPrint "tomcat-users.xml written" + + Delete "$PLUGINSDIR\tomcat-users_1.xml" + Delete "$PLUGINSDIR\tomcat-users_2.xml" +FunctionEnd + + +Function xmlEscape + Pop $0 + ${StrRep} $0 $0 "&" "&" + ${StrRep} $0 $0 "$\"" """ + ${StrRep} $0 $0 "<" "<" + ${StrRep} $0 $0 ">" ">" + Push $0 +FunctionEnd + + +; ================= +; CopyFile Function +; ================= +; +; Copy specified file contents to $R9 +; +Function copyFile + + ClearErrors + + Pop $0 + + FileOpen $1 $0 r + + NoError: + + FileRead $1 $2 + IfErrors EOF 0 + FileWrite $R9 $2 + + IfErrors 0 NoError + + EOF: + + FileClose $1 + + ClearErrors + +FunctionEnd + + +; ================= +; createShortcuts Function +; ================= +Function createShortcuts + + ${If} $TomcatShortcutAllUsers == ${BST_CHECKED} + SetShellVarContext all + ${EndIf} + + SetOutPath "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" + + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Tomcat Home Page.lnk" \ + "https://tomcat.apache.org/" + + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Welcome.lnk" \ + "http://127.0.0.1:$TomcatPortHttp/" + + ${If} ${SectionIsSelected} ${SecManager} + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Tomcat Manager.lnk" \ + "http://127.0.0.1:$TomcatPortHttp/manager/html" + ${EndIf} + + ${If} ${SectionIsSelected} ${SecHostManager} + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Tomcat Host Manager.lnk" \ + "http://127.0.0.1:$TomcatPortHttp/host-manager/html" + ${EndIf} + + ${If} ${SectionIsSelected} ${SecDocs} + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Tomcat Documentation.lnk" \ + "$INSTDIR\webapps\docs\index.html" + ${EndIf} + + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Uninstall Tomcat @VERSION_MAJOR_MINOR@.lnk" \ + "$INSTDIR\Uninstall.exe" '-ServiceName="$TomcatServiceName"' + + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Tomcat @VERSION_MAJOR_MINOR@ Program Directory.lnk" \ + "$INSTDIR" + + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Monitor Tomcat.lnk" \ + "$INSTDIR\bin\$TomcatServiceManagerFileName" \ + '//MS//$TomcatServiceName' \ + "$INSTDIR\tomcat.ico" 0 SW_SHOWNORMAL + + CreateShortCut "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName\Configure Tomcat.lnk" \ + "$INSTDIR\bin\$TomcatServiceManagerFileName" \ + '//ES//$TomcatServiceName' \ + "$INSTDIR\tomcat.ico" 0 SW_SHOWNORMAL + + ${If} $TomcatShortcutAllUsers == ${BST_CHECKED} + SetShellVarContext current + ${EndIf} + +FunctionEnd + +; ================= +; startService Function +; +; Using a function allows the service name to be varied +; ================= +Function startService + ExecShell "" "$INSTDIR\bin\$TomcatServiceManagerFileName" "//MR//$TomcatServiceName" +FunctionEnd + + +;-------------------------------- +;Uninstaller Section + +!ifdef UNINSTALLONLY + Section Uninstall + + ${If} $TomcatServiceName == "" + MessageBox MB_ICONSTOP|MB_OK \ + "No service name specified to uninstall. This will be provided automatically if you uninstall via \ + Add/Remove Programs or the shortcut on the Start menu. Alternatively, call the installer from \ + the command line with -ServiceName=$\"$\"." + Quit + ${EndIf} + + Delete "$INSTDIR\Uninstall.exe" + + ; Stop Tomcat service monitor if running + DetailPrint "Stopping $TomcatServiceName service monitor" + nsExec::ExecToLog '"$INSTDIR\bin\$TomcatServiceManagerFileName" //MQ//$TomcatServiceName' + ; Delete Tomcat service + DetailPrint "Uninstalling $TomcatServiceName service" + nsExec::ExecToLog '"$INSTDIR\bin\$TomcatServiceFileName" //DS//$TomcatServiceName --LogPath "$INSTDIR\logs"' + ClearErrors + + ; Don't know if 32-bit or 64-bit registry was used so, for now, remove both + SetRegView 32 + DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" + DeleteRegKey HKLM "SOFTWARE\Apache Software Foundation\Tomcat\@VERSION_MAJOR_MINOR@\$TomcatServiceName" + DeleteRegValue HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "ApacheTomcatMonitor@VERSION_MAJOR_MINOR@_$TomcatServiceName" + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "ApacheTomcatMonitor@VERSION_MAJOR_MINOR@_$TomcatServiceName" + SetRegView 64 + DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" + DeleteRegKey HKLM "SOFTWARE\Apache Software Foundation\Tomcat\@VERSION_MAJOR_MINOR@\$TomcatServiceName" + DeleteRegValue HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "ApacheTomcatMonitor@VERSION_MAJOR_MINOR@_$TomcatServiceName" + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "ApacheTomcatMonitor@VERSION_MAJOR_MINOR@_$TomcatServiceName" + + ; Don't know if short-cuts were created for all users, one user or not at all so, for now, remove both + SetShellVarContext all + RMDir /r "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" + SetShellVarContext current + RMDir /r "$SMPROGRAMS\Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName" + + ; Before files are removed using recursive deletes, remove any symbolic + ; links in the installation directory and the directory structure below it + ; to ensure the recursive deletes don't result in any nasty surprises. + Push "$INSTDIR" + Call un.RemoveSymlinks + + Delete "$INSTDIR\tomcat.ico" + Delete "$INSTDIR\LICENSE" + Delete "$INSTDIR\NOTICE" + RMDir /r "$INSTDIR\bin" + RMDir /r "$INSTDIR\lib" + Delete "$INSTDIR\conf\*.dtd" + RMDir "$INSTDIR\logs" + RMDir /r "$INSTDIR\webapps\docs" + RMDir /r "$INSTDIR\webapps\examples" + RMDir /r "$INSTDIR\work" + RMDir /r "$INSTDIR\temp" + RMDir "$INSTDIR" + + IfSilent Removed 0 + + ; if $INSTDIR was removed, skip these next ones + IfFileExists "$INSTDIR" 0 Removed + MessageBox MB_YESNO|MB_ICONQUESTION \ + "Remove all files in your Apache Tomcat @VERSION_MAJOR_MINOR@ $TomcatServiceName directory? (If you have anything \ + you created that you want to keep, click No)" IDNO Removed + ; these would be skipped if the user hits no + RMDir /r "$INSTDIR\webapps" + RMDir /r "$INSTDIR\logs" + RMDir /r "$INSTDIR\conf" + Delete "$INSTDIR\*.*" + RMDir /r "$INSTDIR" + Sleep 500 + IfFileExists "$INSTDIR" 0 Removed + MessageBox MB_OK|MB_ICONEXCLAMATION \ + "Note: $INSTDIR could not be removed." + Removed: + + SectionEnd + + ; ================= + ; uninstall init function + ; + ; Read the command line parameter and set up the service name variables so the + ; uninstaller knows which service it is working with + ; ================= + Function un.onInit + ${GetParameters} $R0 + ${GetOptions} $R0 "-ServiceName=" $R1 + StrCpy $TomcatServiceName $R1 + StrCpy $TomcatServiceFileName $R1.exe + StrCpy $TomcatServiceManagerFileName $R1w.exe + FunctionEnd + + ; ================= + ; Removes symbolic links from the path found on top of the stack. + ; The path is removed from the stack as a result of calling this function. + ; ================= + Function un.RemoveSymlinks + Pop $0 + ${GetFileAttributes} "$0" "REPARSE_POINT" $3 + ; DetailPrint "Processing directory [$0] [$3]" + FindFirst $1 $2 $0\*.* + ; DetailPrint "Search [$1] found [$2]" + StrCmp $3 "1" RemoveSymlinks-delete + RemoveSymlinks-loop: + ; DetailPrint "Search [$1] processing [$0\$2]" + StrCmp $2 "" RemoveSymlinks-exit + StrCmp $2 "." RemoveSymlinks-skip + StrCmp $2 ".." RemoveSymlinks-skip + IfFileExists $0\$2\*.* RemoveSymlinks-directory + RemoveSymlinks-skip: + ; DetailPrint "Search [$1] ignoring file [$0\$2]" + FindNext $1 $2 + StrCmp $2 "" RemoveSymlinks-exit + goto RemoveSymlinks-loop + RemoveSymlinks-directory: + ; DetailPrint "Search [$1] found directory [$0\$2]" + Push $0 + Push $1 + Push $0\$2 + Call un.RemoveSymlinks + Pop $1 + Pop $0 + ; DetailPrint "Search [$1] restored for [$0]" + FindNext $1 $2 + goto RemoveSymlinks-loop + RemoveSymlinks-delete: + ; DetailPrint "Deleting symlink [$0]" + SetFileAttributes "$0" "NORMAL" + System::Call "kernel32::RemoveDirectoryW(w `$0`) i.n" + RemoveSymlinks-exit: + ; DetailPrint "Search [$1] closed" + FindClose $1 + FunctionEnd + +!endif + +;eof diff --git a/res/maven/README.txt b/res/maven/README.txt new file mode 100644 index 0000000..4152c0e --- /dev/null +++ b/res/maven/README.txt @@ -0,0 +1,39 @@ +================================================================================ + 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. +================================================================================ + +General preparations before any publishing: +1 - Generate a standard Tomcat release. + This will generate a mvn.properties.release file as part of the tag. It + should include the property settings required to complete the release. +2 - Should any of the properties need to be overridden, create a + mvn.properties and override as necessary. + +To publish a snapshot do the following: +1 - ant -f mvn-pub.xml deploy-snapshot + This populates + https://repository.apache.org/content/repositories/snapshots/org/apache/tomcat/ + +To release do the following: +1 - ant -f mvn-pub.xml deploy-release + that step creates a staging area in + https://repository.apache.org/index.html#stagingRepositories +2 - check the upload and then close the repository +3 - include the repository in the VOTE thread +4 - in https://repository.apache.org/index.html#stagingRepositories release it + +To install in your maven repo: +1 - ant -f mvn-pub.xml generic-install diff --git a/res/maven/mvn-pub.xml b/res/maven/mvn-pub.xml new file mode 100644 index 0000000..80e4fa2 --- /dev/null +++ b/res/maven/mvn-pub.xml @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GPG is required, but was not found at ${gpg.exec} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/maven/mvn.properties.default b/res/maven/mvn.properties.default new file mode 100644 index 0000000..0e3eb78 --- /dev/null +++ b/res/maven/mvn.properties.default @@ -0,0 +1,65 @@ +# 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. +# +# To create custom properties, simply create +# the file mvn.properties +# in this directory +# no need to change this file +# + +# Authentication +# Note: You will be prompted for your GPG passphrase and LDAP password when +# running this script +asf.ldap.username= +gpg.exec=C:/software/GNU/GnuPG/gpg.exe +# Set this property to use the user name and password from the Maven +# settings.xml file rather than from asf.ldap.username and prompting for the +# associated password +# maven.auth.useSettings=Anything + +# ASF Snapshot Repository (hosted on Nexus) +maven.snapshot.repo.url=https://repository.apache.org/content/repositories/snapshots +maven.snapshot.repo.repositoryId=apache.snapshots.https + +# ASF Release Repository (hosted on Nexus) +# Note: Also used for staging releases prior to voting +maven.asf.release.repo.url=https://repository.apache.org/service/local/staging/deploy/maven2 +maven.asf.release.repo.repositoryId=apache.releases.https + +# Release version info +maven.asf.release.deploy.version=10.1.23 + +#Where do we load the libraries from +tomcat.lib.path=../../output/build/lib +tomcat.bin.path=../../output/build/bin +tomcat.release.path=../../output/release +tomcat.src.path=../../output/src-jars +tomcat.embed.path=../../output/embed +tomcat.embed.src.path=../../output/embed-src-jars + +#Where do we find the POM files +tomcat.pom.path=../../res/maven + +# ----- Default Base Path for Dependent Packages ----- +# Please note this path must be absolute, not relative, +# as it is referenced with different working directory +# contexts by the various build scripts. +base.path=${user.home}/tomcat-build-libs + +# ----- Maven Ant Tasks ----- +maven-resolver-ant-tasks.version=1.3.0 +maven-resolver-ant-tasks.home=${base.path}/maven-resolver-ant-tasks-${maven-resolver-ant-tasks.version} +maven-resolver-ant-tasks.loc=https://repo1.maven.org/maven2/org/apache/maven/resolver/maven-resolver-ant-tasks/${maven-resolver-ant-tasks.version}/maven-resolver-ant-tasks-${maven-resolver-ant-tasks.version}-uber.jar +maven-resolver-ant-tasks.jar=${maven-resolver-ant-tasks.home}/maven-resolver-ant-tasks-${maven-resolver-ant-tasks.version}-uber.jar diff --git a/res/maven/mvn.properties.release b/res/maven/mvn.properties.release new file mode 100644 index 0000000..eb2c607 --- /dev/null +++ b/res/maven/mvn.properties.release @@ -0,0 +1,27 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + +# This file was auto-generated by the pre-release Ant target. + +# Remove "-dev" from the version since this is not a development release. +maven.asf.release.deploy.version=10.1.23 + +# Re-use the same GPG executable. +gpg.exec=/opt/homebrew/bin/gpg + +# Set the user name to use to upload the artefacts to Nexus. +asf.ldap.username=schultz diff --git a/res/maven/tomcat-annotations-api.pom b/res/maven/tomcat-annotations-api.pom new file mode 100644 index 0000000..ce92a6d --- /dev/null +++ b/res/maven/tomcat-annotations-api.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-annotations-api + @MAVEN.DEPLOY.VERSION@ + Annotations Package + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + \ No newline at end of file diff --git a/res/maven/tomcat-api.pom b/res/maven/tomcat-api.pom new file mode 100644 index 0000000..66774f4 --- /dev/null +++ b/res/maven/tomcat-api.pom @@ -0,0 +1,43 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-api + @MAVEN.DEPLOY.VERSION@ + Definition of interfaces shared by Catalina and Jasper + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-servlet-api + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-catalina-ant.pom b/res/maven/tomcat-catalina-ant.pom new file mode 100644 index 0000000..d192418 --- /dev/null +++ b/res/maven/tomcat-catalina-ant.pom @@ -0,0 +1,49 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-catalina-ant + @MAVEN.DEPLOY.VERSION@ + Tomcat Ant tasks for remote management + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-util + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util-scan + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-catalina-ha.pom b/res/maven/tomcat-catalina-ha.pom new file mode 100644 index 0000000..c9052d0 --- /dev/null +++ b/res/maven/tomcat-catalina-ha.pom @@ -0,0 +1,79 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-catalina-ha + @MAVEN.DEPLOY.VERSION@ + Tomcat High Availability Implementation + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-coyote + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-servlet-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-tribes + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-catalina + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util-scan + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-catalina.pom b/res/maven/tomcat-catalina.pom new file mode 100644 index 0000000..eb1a43d --- /dev/null +++ b/res/maven/tomcat-catalina.pom @@ -0,0 +1,97 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-catalina + @MAVEN.DEPLOY.VERSION@ + Tomcat Servlet Engine Core Classes and Standard implementations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-servlet-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-jsp-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-annotations-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-jni + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-coyote + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util-scan + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-jaspic-api + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-coyote.pom b/res/maven/tomcat-coyote.pom new file mode 100644 index 0000000..58b40c5 --- /dev/null +++ b/res/maven/tomcat-coyote.pom @@ -0,0 +1,61 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-coyote + @MAVEN.DEPLOY.VERSION@ + Tomcat Connectors and HTTP parser + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-servlet-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-jni + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-dbcp.pom b/res/maven/tomcat-dbcp.pom new file mode 100644 index 0000000..9331bcd --- /dev/null +++ b/res/maven/tomcat-dbcp.pom @@ -0,0 +1,43 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-dbcp + @MAVEN.DEPLOY.VERSION@ + Tomcat Database Connection Pooling package + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-el-api.pom b/res/maven/tomcat-el-api.pom new file mode 100644 index 0000000..338d6df --- /dev/null +++ b/res/maven/tomcat-el-api.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-el-api + @MAVEN.DEPLOY.VERSION@ + Expression language package + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-embed-core.pom b/res/maven/tomcat-embed-core.pom new file mode 100644 index 0000000..8ecb0bb --- /dev/null +++ b/res/maven/tomcat-embed-core.pom @@ -0,0 +1,43 @@ + + + + 4.0.0 + org.apache.tomcat.embed + tomcat-embed-core + @MAVEN.DEPLOY.VERSION@ + Core Tomcat implementation + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-annotations-api + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-embed-el.pom b/res/maven/tomcat-embed-el.pom new file mode 100644 index 0000000..390a457 --- /dev/null +++ b/res/maven/tomcat-embed-el.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat.embed + tomcat-embed-el + @MAVEN.DEPLOY.VERSION@ + Core Tomcat implementation + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-embed-jasper.pom b/res/maven/tomcat-embed-jasper.pom new file mode 100644 index 0000000..72a85db --- /dev/null +++ b/res/maven/tomcat-embed-jasper.pom @@ -0,0 +1,54 @@ + + + + 4.0.0 + org.apache.tomcat.embed + tomcat-embed-jasper + @MAVEN.DEPLOY.VERSION@ + Core Tomcat implementation + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat.embed + tomcat-embed-core + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat.embed + tomcat-embed-el + @MAVEN.DEPLOY.VERSION@ + compile + + + org.eclipse.jdt + ecj + 3.33.0 + + + diff --git a/res/maven/tomcat-embed-programmatic.pom b/res/maven/tomcat-embed-programmatic.pom new file mode 100644 index 0000000..58add74 --- /dev/null +++ b/res/maven/tomcat-embed-programmatic.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat.experimental + tomcat-embed-programmatic + @MAVEN.DEPLOY.VERSION@ + Experimental Minimal Tomcat for Programmatic Use + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-embed-websocket.pom b/res/maven/tomcat-embed-websocket.pom new file mode 100644 index 0000000..a9f6070 --- /dev/null +++ b/res/maven/tomcat-embed-websocket.pom @@ -0,0 +1,49 @@ + + + + 4.0.0 + org.apache.tomcat.embed + tomcat-embed-websocket + @MAVEN.DEPLOY.VERSION@ + Core Tomcat implementation + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-websocket-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat.embed + tomcat-embed-core + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-i18n-cs.pom b/res/maven/tomcat-i18n-cs.pom new file mode 100644 index 0000000..b9aa30c --- /dev/null +++ b/res/maven/tomcat-i18n-cs.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-cs + @MAVEN.DEPLOY.VERSION@ + Czech translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-i18n-de.pom b/res/maven/tomcat-i18n-de.pom new file mode 100644 index 0000000..b7ed71f --- /dev/null +++ b/res/maven/tomcat-i18n-de.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-de + @MAVEN.DEPLOY.VERSION@ + German translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-i18n-es.pom b/res/maven/tomcat-i18n-es.pom new file mode 100644 index 0000000..4464334 --- /dev/null +++ b/res/maven/tomcat-i18n-es.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-es + @MAVEN.DEPLOY.VERSION@ + Spanish translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-i18n-fr.pom b/res/maven/tomcat-i18n-fr.pom new file mode 100644 index 0000000..953b56b --- /dev/null +++ b/res/maven/tomcat-i18n-fr.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-fr + @MAVEN.DEPLOY.VERSION@ + French translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-i18n-ja.pom b/res/maven/tomcat-i18n-ja.pom new file mode 100644 index 0000000..f31a7fd --- /dev/null +++ b/res/maven/tomcat-i18n-ja.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-ja + @MAVEN.DEPLOY.VERSION@ + Japanese translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-i18n-ko.pom b/res/maven/tomcat-i18n-ko.pom new file mode 100644 index 0000000..2d7fd2e --- /dev/null +++ b/res/maven/tomcat-i18n-ko.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-ko + @MAVEN.DEPLOY.VERSION@ + Korean translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-i18n-pt-BR.pom b/res/maven/tomcat-i18n-pt-BR.pom new file mode 100644 index 0000000..f8d61e9 --- /dev/null +++ b/res/maven/tomcat-i18n-pt-BR.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-pt-BR + @MAVEN.DEPLOY.VERSION@ + Brazilian Portuguese translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-i18n-ru.pom b/res/maven/tomcat-i18n-ru.pom new file mode 100644 index 0000000..b09662e --- /dev/null +++ b/res/maven/tomcat-i18n-ru.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-ru + @MAVEN.DEPLOY.VERSION@ + Russian translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-i18n-zh-CN.pom b/res/maven/tomcat-i18n-zh-CN.pom new file mode 100644 index 0000000..de53ab5 --- /dev/null +++ b/res/maven/tomcat-i18n-zh-CN.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-i18n-zh-CN + @MAVEN.DEPLOY.VERSION@ + Simplified Chinese translations + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-jasper-el.pom b/res/maven/tomcat-jasper-el.pom new file mode 100644 index 0000000..ec04f91 --- /dev/null +++ b/res/maven/tomcat-jasper-el.pom @@ -0,0 +1,44 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-jasper-el + @MAVEN.DEPLOY.VERSION@ + Jasper Expression Language Impl + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-el-api + @MAVEN.DEPLOY.VERSION@ + compile + + + + diff --git a/res/maven/tomcat-jasper.pom b/res/maven/tomcat-jasper.pom new file mode 100644 index 0000000..264cb80 --- /dev/null +++ b/res/maven/tomcat-jasper.pom @@ -0,0 +1,84 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-jasper + @MAVEN.DEPLOY.VERSION@ + Tomcats JSP Parser + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-servlet-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-jsp-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-el-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.eclipse.jdt + ecj + 3.33.0 + + + org.apache.tomcat + tomcat-jasper-el + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util-scan + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-jaspic-api.pom b/res/maven/tomcat-jaspic-api.pom new file mode 100644 index 0000000..fa23a3a --- /dev/null +++ b/res/maven/tomcat-jaspic-api.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-jaspic-api + @MAVEN.DEPLOY.VERSION@ + jakarta.security.auth.message package + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-jdbc.pom b/res/maven/tomcat-jdbc.pom new file mode 100644 index 0000000..35dc946 --- /dev/null +++ b/res/maven/tomcat-jdbc.pom @@ -0,0 +1,43 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-jdbc + @MAVEN.DEPLOY.VERSION@ + Tomcat JDBC Pool Package + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-jni.pom b/res/maven/tomcat-jni.pom new file mode 100644 index 0000000..df32ede --- /dev/null +++ b/res/maven/tomcat-jni.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-jni + @MAVEN.DEPLOY.VERSION@ + Interface code to the native connector + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-jsp-api.pom b/res/maven/tomcat-jsp-api.pom new file mode 100644 index 0000000..4fed11b --- /dev/null +++ b/res/maven/tomcat-jsp-api.pom @@ -0,0 +1,49 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-jsp-api + @MAVEN.DEPLOY.VERSION@ + JSP package + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-el-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-servlet-api + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-juli.pom b/res/maven/tomcat-juli.pom new file mode 100644 index 0000000..ec05176 --- /dev/null +++ b/res/maven/tomcat-juli.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + Tomcat Core Logging Package + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-servlet-api.pom b/res/maven/tomcat-servlet-api.pom new file mode 100644 index 0000000..67d11b2 --- /dev/null +++ b/res/maven/tomcat-servlet-api.pom @@ -0,0 +1,83 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-servlet-api + @MAVEN.DEPLOY.VERSION@ + jakarta.servlet package + https://tomcat.apache.org/ + + + + Apache License, Version 2.0 and + Common Development And Distribution License (CDDL) Version 1.0 and + Eclipse Public License - v 2.0 + + + http://www.apache.org/licenses/LICENSE-2.0.txt and + http://www.opensource.org/licenses/cddl1.txt and + https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt + + repo + + The Apache License, version 2.0 applies to all files apart from + javaee_5.xsd, + javaee_6.xsd, + javaee_7.xsd, + javaee_8.xsd, + javaee_web_services_1_2.xsd, + javaee_web_services_client_1_2.xsd, + javaee_web_services_1_3.xsd, + javaee_web_services_client_1_3.xsd, + javaee_web_services_1_4.xsd, + javaee_web_services_client_1_4.xsd, + jsp_2_2.xsd, + jsp_2_3.xsd, + web-app_3_0.xsd, + web-common_3_0.xsd, + web-fragment_3_0.xsd, + web-app_3_1.xsd, + web-common_3_1.xsd, + web-fragment_3_1.xsd, + web-app_4_0.xsd, + web-common_4_0.xsd, + web-fragment_4_0.xsd, + to which the CDDL version 1.0 applies and + jakartaee_9.xsd + jakartaee_10.xsd + jakarta_web-services_2_0.xsd + jakarta_web-services_client_2_0.xsd + jsp_3_0.xsd + jsp_3_1.xsd + web-app_5_0.xsd + web-app_6_0.xsd + web-commonn_5_0.xsd + web-commonn_6_0.xsd + web-fragment_5_0.xsd + web-fragment_6_0.xsd + web-jsptaglibrary_3_0.xsd + web-jsptaglibrary_3_1.xsd + to which the EPLv2 applies. + + + + diff --git a/res/maven/tomcat-ssi.pom b/res/maven/tomcat-ssi.pom new file mode 100644 index 0000000..13a2a3c --- /dev/null +++ b/res/maven/tomcat-ssi.pom @@ -0,0 +1,43 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-ssi + @MAVEN.DEPLOY.VERSION@ + Server-side Includes module + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-storeconfig.pom b/res/maven/tomcat-storeconfig.pom new file mode 100644 index 0000000..e1ae04c --- /dev/null +++ b/res/maven/tomcat-storeconfig.pom @@ -0,0 +1,73 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-storeconfig + @MAVEN.DEPLOY.VERSION@ + Tomcat storeconfig component + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-coyote + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-tribes + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-catalina + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-catalina-ha + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-tribes.pom b/res/maven/tomcat-tribes.pom new file mode 100644 index 0000000..7e51344 --- /dev/null +++ b/res/maven/tomcat-tribes.pom @@ -0,0 +1,43 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-tribes + @MAVEN.DEPLOY.VERSION@ + Tomcat Group Communication Package + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-util-scan.pom b/res/maven/tomcat-util-scan.pom new file mode 100644 index 0000000..3737466 --- /dev/null +++ b/res/maven/tomcat-util-scan.pom @@ -0,0 +1,58 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-util-scan + @MAVEN.DEPLOY.VERSION@ + + Common code shared by Catalina and Jasper for scanning JARS and processing + XML descriptors + + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-util + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-api + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-util.pom b/res/maven/tomcat-util.pom new file mode 100644 index 0000000..80d3e39 --- /dev/null +++ b/res/maven/tomcat-util.pom @@ -0,0 +1,43 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-util + @MAVEN.DEPLOY.VERSION@ + Common code shared by multiple Tomcat components + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-websocket-api.pom b/res/maven/tomcat-websocket-api.pom new file mode 100644 index 0000000..27206ac --- /dev/null +++ b/res/maven/tomcat-websocket-api.pom @@ -0,0 +1,43 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-websocket-api + @MAVEN.DEPLOY.VERSION@ + WebSocket API + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-websocket-client-api + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat-websocket-client-api.pom b/res/maven/tomcat-websocket-client-api.pom new file mode 100644 index 0000000..1e84dfd --- /dev/null +++ b/res/maven/tomcat-websocket-client-api.pom @@ -0,0 +1,35 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-websocket-client-api + @MAVEN.DEPLOY.VERSION@ + WebSocket Client API + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/maven/tomcat-websocket.pom b/res/maven/tomcat-websocket.pom new file mode 100644 index 0000000..47c53bf --- /dev/null +++ b/res/maven/tomcat-websocket.pom @@ -0,0 +1,61 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat-websocket + @MAVEN.DEPLOY.VERSION@ + Tomcat WebSocket implementation + https://tomcat.apache.org/ + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.apache.tomcat + tomcat-servlet-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-websocket-api + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-juli + @MAVEN.DEPLOY.VERSION@ + compile + + + org.apache.tomcat + tomcat-util + @MAVEN.DEPLOY.VERSION@ + compile + + + diff --git a/res/maven/tomcat.pom b/res/maven/tomcat.pom new file mode 100644 index 0000000..67db3e2 --- /dev/null +++ b/res/maven/tomcat.pom @@ -0,0 +1,36 @@ + + + + 4.0.0 + org.apache.tomcat + tomcat + @MAVEN.DEPLOY.VERSION@ + Binary distribution of Apache Tomcat + https://tomcat.apache.org/ + pom + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/res/openssl/README.md b/res/openssl/README.md new file mode 100644 index 0000000..5fd1eba --- /dev/null +++ b/res/openssl/README.md @@ -0,0 +1,42 @@ +# OpenSSL support for Apache Tomcat + +## Building + +The OpenSSL API support classes can be built using jextract from Java 22+. + +jextract is now available in its own standalone repository. Clone +`https://github.com/openjdk/jextract` in some location and +checkout the branch that supports Java 22. Please refer to the +instructions from the repository for building. It should be the +`panama` branch. + +This step is only useful to be able to use additional native APIs from OpenSSL +or stdlib. + +Find include paths using `gcc -xc -E -v -`, on Fedora it is +`/usr/lib/gcc/x86_64-redhat-linux/12/include`. Edit `openssl-tomcat.conf` +accordingly to set the appropriate path. + +``` +export JEXTRACT_HOME=/jextract/build/jextract +$JEXTRACT_HOME/bin/jextract @openssl-tomcat.conf openssl.h +``` +Note: The build path for the JDK will be different on other platforms. + +The code included was generated using OpenSSL 3.0. As long as things remain +API compatible, the generated code will still work. + +The `openssl-tomcat.conf` will generate a trimmed down OpenSSL API. When +developing new features, the full API can be generated instead using: +``` +$JEXTRACT_HOME/bin/jextract --source -t org.apache.tomcat.util.openssl -lssl -I /usr/lib/gcc/x86_64-redhat-linux/12/include openssl.h --output src/main/java +``` + +The `openssl.conf` file lists all the API calls and constants that can be +generated using jextract, as a reference to what is available. Some macros are +not supported and have to be reproduced in code. + +Before committing updated generated files, they need to have the license header +added. The `addlicense.sh` script can do that and process all Java source files +in the `src/main/java/org/apache/tomcat/util/openssl` directory. + diff --git a/res/openssl/addlicense.sh b/res/openssl/addlicense.sh new file mode 100755 index 0000000..4ab5e38 --- /dev/null +++ b/res/openssl/addlicense.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# 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. + +for generated in ../../java/org/apache/tomcat/util/openssl/*.java; do + cat license.header $generated >> $generated.$$ + mv $generated.$$ $generated + echo Updated $generated +done diff --git a/res/openssl/license.header b/res/openssl/license.header new file mode 100644 index 0000000..4b326ae --- /dev/null +++ b/res/openssl/license.header @@ -0,0 +1,17 @@ +/* + * 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. + */ + diff --git a/res/openssl/openssl-tomcat.conf b/res/openssl/openssl-tomcat.conf new file mode 100644 index 0000000..66ebe76 --- /dev/null +++ b/res/openssl/openssl-tomcat.conf @@ -0,0 +1,369 @@ +# 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. + +-t org.apache.tomcat.util.openssl +-lssl +# Configure include path +-I /usr/lib/gcc/x86_64-redhat-linux/12/include +--output ../../java + +#### Extracted from: /usr/include/openssl/asn1.h + +--include-function ASN1_STRING_get0_data # header: /usr/include/openssl/asn1.h +--include-function ASN1_STRING_length # header: /usr/include/openssl/asn1.h + +#### Extracted from: /usr/include/openssl/bio.h + +--include-function BIO_ctrl # header: /usr/include/openssl/bio.h +--include-function BIO_ctrl_pending # header: /usr/include/openssl/bio.h +--include-function BIO_free # header: /usr/include/openssl/bio.h +--include-function BIO_new # header: /usr/include/openssl/bio.h +--include-function BIO_new_bio_pair # header: /usr/include/openssl/bio.h +--include-function BIO_new_file # header: /usr/include/openssl/bio.h +--include-function BIO_read # header: /usr/include/openssl/bio.h +--include-function BIO_s_bio # header: /usr/include/openssl/bio.h +--include-function BIO_s_file # header: /usr/include/openssl/bio.h +--include-function BIO_s_mem # header: /usr/include/openssl/bio.h +--include-function BIO_write # header: /usr/include/openssl/bio.h +--include-constant BIO_CLOSE # header: /usr/include/openssl/bio.h +--include-constant BIO_CTRL_RESET # header: /usr/include/openssl/bio.h +--include-constant BIO_C_SET_FILENAME # header: /usr/include/openssl/bio.h +--include-constant BIO_FP_READ # header: /usr/include/openssl/bio.h + +#### Extracted from: /usr/include/openssl/bn.h + +--include-function BN_get_rfc2409_prime_1024 # header: /usr/include/openssl/bn.h +--include-function BN_get_rfc2409_prime_768 # header: /usr/include/openssl/bn.h +--include-function BN_get_rfc3526_prime_1536 # header: /usr/include/openssl/bn.h +--include-function BN_get_rfc3526_prime_2048 # header: /usr/include/openssl/bn.h +--include-function BN_get_rfc3526_prime_3072 # header: /usr/include/openssl/bn.h +--include-function BN_get_rfc3526_prime_4096 # header: /usr/include/openssl/bn.h +--include-function BN_get_rfc3526_prime_6144 # header: /usr/include/openssl/bn.h +--include-function BN_get_rfc3526_prime_8192 # header: /usr/include/openssl/bn.h +--include-function BN_new # header: /usr/include/openssl/bn.h +--include-function BN_set_word # header: /usr/include/openssl/bn.h + +#### Extracted from: /usr/include/openssl/crypto.h + +--include-function CRYPTO_free # header: /usr/include/openssl/crypto.h +--include-function OpenSSL_version # header: /usr/include/openssl/crypto.h +--include-function OpenSSL_version_num # header: /usr/include/openssl/crypto.h +--include-constant OPENSSL_INIT_ENGINE_ALL_BUILTIN # deprecated header: /usr/include/openssl/crypto.h + +#### Extracted from: /usr/include/openssl/dh.h + +--include-function DH_free # deprecated header: /usr/include/openssl/dh.h +--include-function DH_new # deprecated header: /usr/include/openssl/dh.h +--include-function DH_set0_pqg # deprecated header: /usr/include/openssl/dh.h + +#### Extracted from: /usr/include/openssl/ec.h + +--include-function d2i_ECPKParameters # header: /usr/include/openssl/ec.h +--include-function EC_GROUP_free # header: /usr/include/openssl/ec.h +--include-function EC_GROUP_get_curve_name # header: /usr/include/openssl/ec.h +--include-function EC_KEY_free # deprecated header: /usr/include/openssl/ec.h +--include-function EC_KEY_new_by_curve_name # deprecated header: /usr/include/openssl/ec.h + +#### Extracted from: /usr/include/openssl/engine.h + +--include-function ENGINE_by_id # deprecated header: /usr/include/openssl/engine.h +--include-function ENGINE_ctrl_cmd_string # deprecated header: /usr/include/openssl/engine.h +--include-function ENGINE_free # deprecated header: /usr/include/openssl/engine.h +--include-function ENGINE_load_private_key # deprecated header: /usr/include/openssl/engine.h +--include-function ENGINE_register_all_complete # deprecated header: /usr/include/openssl/engine.h +--include-function ENGINE_set_default # deprecated header: /usr/include/openssl/engine.h +--include-constant ENGINE_METHOD_ALL # deprecated header: /usr/include/openssl/engine.h + +#### Extracted from: /usr/include/openssl/err.h + +--include-function ERR_clear_error # header: /usr/include/openssl/err.h +--include-function ERR_error_string # header: /usr/include/openssl/err.h +--include-function ERR_get_error # header: /usr/include/openssl/err.h +--include-function ERR_peek_last_error # header: /usr/include/openssl/err.h +--include-constant ERR_REASON_MASK # header: /usr/include/openssl/err.h + +#### Extracted from: /usr/include/openssl/evp.h + +--include-function EVP_MD_fetch # header: /usr/include/openssl/evp.h +--include-function EVP_MD_free # header: /usr/include/openssl/evp.h +--include-function EVP_MD_get0_provider # header: /usr/include/openssl/evp.h +--include-function EVP_PKEY_get_base_id # header: /usr/include/openssl/evp.h +--include-function EVP_PKEY_get_bits # header: /usr/include/openssl/evp.h +--include-function EVP_PKEY_free # header: /usr/include/openssl/evp.h +--include-constant EVP_PKEY_DSA # header: /usr/include/openssl/evp.h +--include-constant EVP_PKEY_NONE # header: /usr/include/openssl/evp.h +--include-constant EVP_PKEY_RSA # header: /usr/include/openssl/evp.h + +#### Extracted from: /usr/include/openssl/obj_mac.h + +--include-constant NID_info_access # header: /usr/include/openssl/obj_mac.h + +#### Extracted from: /usr/include/openssl/ocsp.h + +--include-function OCSP_BASICRESP_free # header: /usr/include/openssl/ocsp.h +--include-function OCSP_CERTID_free # header: /usr/include/openssl/ocsp.h +--include-function OCSP_REQUEST_free # header: /usr/include/openssl/ocsp.h +--include-function OCSP_REQUEST_new # header: /usr/include/openssl/ocsp.h +--include-function OCSP_RESPONSE_free # header: /usr/include/openssl/ocsp.h +--include-function OCSP_cert_to_id # header: /usr/include/openssl/ocsp.h +--include-function OCSP_request_add0_id # header: /usr/include/openssl/ocsp.h +--include-function OCSP_response_get1_basic # header: /usr/include/openssl/ocsp.h +--include-function OCSP_response_status # header: /usr/include/openssl/ocsp.h +--include-function OCSP_resp_find # header: /usr/include/openssl/ocsp.h +--include-function OCSP_resp_get0 # header: /usr/include/openssl/ocsp.h +--include-function OCSP_single_get0_status # header: /usr/include/openssl/ocsp.h +--include-function d2i_OCSP_RESPONSE # header: /usr/include/openssl/ocsp.h +--include-function i2d_OCSP_REQUEST # header: /usr/include/openssl/ocsp.h +--include-constant OCSP_RESPONSE_STATUS_SUCCESSFUL # header: /usr/include/openssl/ocsp.h +--include-constant V_OCSP_CERTSTATUS_GOOD # header: /usr/include/openssl/ocsp.h +--include-constant V_OCSP_CERTSTATUS_REVOKED # header: /usr/include/openssl/ocsp.h +--include-constant V_OCSP_CERTSTATUS_UNKNOWN # header: /usr/include/openssl/ocsp.h + +#### Extracted from: /usr/include/openssl/opensslconf-x86_64.h + +--include-constant OPENSSL_API_COMPAT # header: /usr/include/openssl/opensslconf-x86_64.h +--include-constant OPENSSL_FILE # header: /usr/include/openssl/opensslconf-x86_64.h +--include-constant OPENSSL_LINE # header: /usr/include/openssl/opensslconf-x86_64.h +--include-constant OPENSSL_MIN_API # header: /usr/include/openssl/opensslconf-x86_64.h + +#### Extracted from: /usr/include/openssl/pem.h + +--include-function PEM_ASN1_read_bio # header: /usr/include/openssl/pem.h +--include-function PEM_read_bio_DHparams # deprecated header: /usr/include/openssl/pem.h +--include-function PEM_read_bio_ECPKParameters # deprecated header: /usr/include/openssl/pem.h +--include-function PEM_read_bio_Parameters # header: /usr/include/openssl/pem.h +--include-function PEM_read_bio_PrivateKey # header: /usr/include/openssl/pem.h +--include-function PEM_read_bio_X509_AUX # header: /usr/include/openssl/pem.h +--include-constant PEM_STRING_ECPARAMETERS # header: /usr/include/openssl/pem.h + +#### Extracted from: /usr/include/openssl/pemerr.h + +--include-constant PEM_R_NO_START_LINE # header: /usr/include/openssl/pemerr.h + +#### Extracted from: /usr/include/openssl/pkcs12.h + +--include-function PKCS12_free # header: /usr/include/openssl/pkcs12.h +--include-function PKCS12_parse # header: /usr/include/openssl/pkcs12.h +--include-function PKCS12_verify_mac # header: /usr/include/openssl/pkcs12.h +--include-function d2i_PKCS12_bio # header: /usr/include/openssl/pkcs12.h + +#### Extracted from: /usr/include/openssl/provider.h + +--include-function OSSL_PROVIDER_get0_name # header: /usr/include/openssl/provider.h + +#### Extracted from: /usr/include/openssl/rand.h + +--include-function RAND_load_file # header: /usr/include/openssl/rand.h +--include-function RAND_seed # header: /usr/include/openssl/rand.h + +#### Extracted from: /usr/include/openssl/ssl.h + +--include-function OPENSSL_init_ssl # header: /usr/include/openssl/ssl.h +--include-function SSL_CIPHER_get_auth_nid # header: /usr/include/openssl/ssl.h +--include-function SSL_CIPHER_get_kx_nid # header: /usr/include/openssl/ssl.h +--include-function SSL_CIPHER_get_name # header: /usr/include/openssl/ssl.h +--include-function SSL_CONF_CTX_finish # header: /usr/include/openssl/ssl.h +--include-function SSL_CONF_CTX_free # header: /usr/include/openssl/ssl.h +--include-function SSL_CONF_CTX_new # header: /usr/include/openssl/ssl.h +--include-function SSL_CONF_CTX_set_flags # header: /usr/include/openssl/ssl.h +--include-function SSL_CONF_CTX_set_ssl_ctx # header: /usr/include/openssl/ssl.h +--include-function SSL_CONF_cmd # header: /usr/include/openssl/ssl.h +--include-function SSL_CONF_cmd_value_type # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_add_client_CA # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_check_private_key # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_clear_options # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_ctrl # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_free # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_get_cert_store # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_get_ciphers # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_get_client_CA_list # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_get_options # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_get_timeout # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_load_verify_locations # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_new # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_alpn_select_cb # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_cert_verify_callback # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_cipher_list # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_ciphersuites # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_client_CA_list # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_default_passwd_cb # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_default_verify_paths # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_options # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_session_id_context # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_timeout # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_tmp_dh_callback # deprecated header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set_verify # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_set0_tmp_dh_pkey # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_use_certificate # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_use_certificate_chain_file # header: /usr/include/openssl/ssl.h +--include-function SSL_CTX_use_PrivateKey # header: /usr/include/openssl/ssl.h +--include-function SSL_SESSION_get_id # header: /usr/include/openssl/ssl.h +--include-function SSL_SESSION_get_time # header: /usr/include/openssl/ssl.h +--include-function SSL_add_file_cert_subjects_to_stack # header: /usr/include/openssl/ssl.h +--include-function SSL_do_handshake # header: /usr/include/openssl/ssl.h +--include-function SSL_free # header: /usr/include/openssl/ssl.h +--include-function SSL_get_ciphers # header: /usr/include/openssl/ssl.h +--include-function SSL_get_current_cipher # header: /usr/include/openssl/ssl.h +--include-function SSL_get_ex_data_X509_STORE_CTX_idx # header: /usr/include/openssl/ssl.h +--include-function SSL_get_options # header: /usr/include/openssl/ssl.h +--include-function SSL_get_peer_cert_chain # header: /usr/include/openssl/ssl.h +--include-function SSL_get_privatekey # header: /usr/include/openssl/ssl.h +--include-function SSL_get_session # header: /usr/include/openssl/ssl.h +--include-function SSL_get_shutdown # header: /usr/include/openssl/ssl.h +--include-function SSL_get_version # header: /usr/include/openssl/ssl.h +--include-function SSL_get0_alpn_selected # header: /usr/include/openssl/ssl.h +--include-function SSL_get1_peer_certificate # header: /usr/include/openssl/ssl.h +--include-function SSL_in_init # header: /usr/include/openssl/ssl.h +--include-function SSL_load_client_CA_file # header: /usr/include/openssl/ssl.h +--include-function SSL_new # header: /usr/include/openssl/ssl.h +--include-function SSL_pending # header: /usr/include/openssl/ssl.h +--include-function SSL_read # header: /usr/include/openssl/ssl.h +--include-function SSL_renegotiate # header: /usr/include/openssl/ssl.h +--include-function SSL_renegotiate_pending # header: /usr/include/openssl/ssl.h +--include-function SSL_set_accept_state # header: /usr/include/openssl/ssl.h +--include-function SSL_set_bio # header: /usr/include/openssl/ssl.h +--include-function SSL_set_cipher_list # header: /usr/include/openssl/ssl.h +--include-function SSL_set_connect_state # header: /usr/include/openssl/ssl.h +--include-function SSL_set_info_callback # header: /usr/include/openssl/ssl.h +--include-function SSL_set_options # header: /usr/include/openssl/ssl.h +--include-function SSL_set_verify # header: /usr/include/openssl/ssl.h +--include-function SSL_set_verify_result # header: /usr/include/openssl/ssl.h +--include-function SSL_shutdown # header: /usr/include/openssl/ssl.h +--include-function SSL_verify_client_post_handshake # header: /usr/include/openssl/ssl.h +--include-function SSL_write # header: /usr/include/openssl/ssl.h +--include-function TLS_server_method # header: /usr/include/openssl/ssl.h +--include-constant SSL_CB_HANDSHAKE_DONE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CONF_FLAG_CERTIFICATE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CONF_FLAG_FILE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CONF_FLAG_SERVER # header: /usr/include/openssl/ssl.h +--include-constant SSL_CONF_FLAG_SHOW_ERRORS # header: /usr/include/openssl/ssl.h +--include-constant SSL_CONF_TYPE_DIR # header: /usr/include/openssl/ssl.h +--include-constant SSL_CONF_TYPE_FILE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CONF_TYPE_UNKNOWN # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_CHAIN_CERT # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_GET_SESS_CACHE_MODE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_GET_SESS_CACHE_SIZE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_ACCEPT # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_ACCEPT_GOOD # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_ACCEPT_RENEGOTIATE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_CACHE_FULL # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_CB_HIT # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_CONNECT # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_CONNECT_GOOD # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_CONNECT_RENEGOTIATE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_HIT # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_MISSES # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_NUMBER # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SESS_TIMEOUTS # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_DH_AUTO # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_GROUPS # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_MAX_PROTO_VERSION # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_MIN_PROTO_VERSION # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_SESS_CACHE_MODE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_SESS_CACHE_SIZE # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_TLSEXT_TICKET_KEYS # header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_TMP_DH # deprecated header: /usr/include/openssl/ssl.h +--include-constant SSL_CTRL_SET_TMP_ECDH # deprecated header: /usr/include/openssl/ssl.h +--include-constant SSL_ERROR_NONE # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_ALL # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_CIPHER_SERVER_PREFERENCE # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_COMPRESSION # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_SSLv2 # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_SSLv3 # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_TICKET # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_TLSv1 # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_TLSv1_1 # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_TLSv1_2 # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_NO_TLSv1_3 # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_SINGLE_DH_USE # header: /usr/include/openssl/ssl.h +--include-constant SSL_OP_SINGLE_ECDH_USE # header: /usr/include/openssl/ssl.h +--include-constant SSL_RECEIVED_SHUTDOWN # header: /usr/include/openssl/ssl.h +--include-constant SSL_SENT_SHUTDOWN # header: /usr/include/openssl/ssl.h +--include-constant SSL_SESS_CACHE_OFF # header: /usr/include/openssl/ssl.h +--include-constant SSL_SESS_CACHE_SERVER # header: /usr/include/openssl/ssl.h +--include-constant SSL_VERIFY_FAIL_IF_NO_PEER_CERT # header: /usr/include/openssl/ssl.h +--include-constant SSL_VERIFY_NONE # header: /usr/include/openssl/ssl.h +--include-constant SSL_VERIFY_PEER # header: /usr/include/openssl/ssl.h + +#### Extracted from: /usr/include/openssl/ssl2.h + +--include-constant SSL2_VERSION # header: /usr/include/openssl/ssl2.h + +#### Extracted from: /usr/include/openssl/ssl3.h + +--include-constant SSL3_VERSION # header: /usr/include/openssl/ssl3.h + +#### Extracted from: /usr/include/openssl/tls1.h + +--include-constant SSL_TLSEXT_ERR_NOACK # header: /usr/include/openssl/tls1.h +--include-constant SSL_TLSEXT_ERR_OK # header: /usr/include/openssl/tls1.h +--include-constant TLS1_1_VERSION # header: /usr/include/openssl/tls1.h +--include-constant TLS1_2_VERSION # header: /usr/include/openssl/tls1.h +--include-constant TLS1_3_VERSION # header: /usr/include/openssl/tls1.h +--include-constant TLS1_VERSION # header: /usr/include/openssl/tls1.h + +#### Extracted from: /usr/include/openssl/stack.h + +--include-function OPENSSL_sk_num # header: /usr/include/openssl/stack.h +--include-function OPENSSL_sk_value # header: /usr/include/openssl/stack.h + +#### Extracted from: /usr/include/openssl/types.h + +--include-typedef pem_password_cb + +#### Extracted from: /usr/include/openssl/x509.h + +--include-function i2d_X509 # header: /usr/include/openssl/x509.h +--include-function d2i_X509 # header: /usr/include/openssl/x509.h +--include-function d2i_X509_bio # header: /usr/include/openssl/x509.h +--include-function X509_EXTENSION_get_data # header: /usr/include/openssl/x509.h +--include-function X509_free # header: /usr/include/openssl/x509.h +--include-function X509_get_ext # header: /usr/include/openssl/x509.h +--include-function X509_get_ext_by_NID # header: /usr/include/openssl/x509.h +--include-constant X509_FILETYPE_PEM # header: /usr/include/openssl/x509.h + +#### Extracted from: /usr/include/openssl/x509v3.h + +--include-function X509_check_issued # header: /usr/include/openssl/x509v3.h + +#### Extracted from: /usr/include/openssl/x509_vfy.h + +--include-function X509_LOOKUP_ctrl # header: /usr/include/openssl/x509_vfy.h +--include-function X509_LOOKUP_file # header: /usr/include/openssl/x509_vfy.h +--include-function X509_LOOKUP_hash_dir # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_add_lookup # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_CTX_get_current_cert # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_CTX_get_error # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_CTX_get_error_depth # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_CTX_get_ex_data # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_CTX_get0_current_issuer # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_CTX_get0_untrusted # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_CTX_set_error # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_set_flags # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_L_ADD_DIR # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_L_FILE_LOAD # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_APPLICATION_VERIFICATION # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_CERT_UNTRUSTED # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_CRL_HAS_EXPIRED # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_FLAG_CRL_CHECK # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_FLAG_CRL_CHECK_ALL # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_OK # header: /usr/include/openssl/x509_vfy.h + diff --git a/res/openssl/openssl.h b/res/openssl/openssl.h new file mode 100644 index 0000000..e31fad9 --- /dev/null +++ b/res/openssl/openssl.h @@ -0,0 +1,31 @@ +/* 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/res/rat/rat-excludes.txt b/res/rat/rat-excludes.txt new file mode 100644 index 0000000..f4dc296 --- /dev/null +++ b/res/rat/rat-excludes.txt @@ -0,0 +1,256 @@ + 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. + + + This is excludes file for Apache RAT tool run by ASF Buildbot, + https://creadur.apache.org/rat/ + + The following files are excluded: + + - log files and other temporary files generated during testing + + - *.html files in documentation are generated from XML sources + + - *.sha512 files are generated and cannot contain license + + - *.manifest, MANIFEST.MF JAR manifest files cannot contain license + + - Files in API documentation (javadoc) that are generated + + - test files, such as trivial textual files containing only "OK' string, + files in unusual encodings or compressed files are also excluded. + + - JSON files (RFC7159) are data and cannot contain comments + + - local build configuration files + + - file fragments that are combined during the build process and therefore can + not contain a license header in every fragment + + - JavaEE XML schemas that are CDDL licensed + + - Checkstyle configuration file that defines how to check for the presence of + ALv2 headers + + - files used simply to ensure directories are not empty + + - *.bmp, *.dia files are binary + (*.gif, *.jpg are also binary, but are automatically detected by RAT as + ones, so no explicit configuration is needed) + + - Markdown files for display in their GitHub UI + + - Temporary cache files used by Checkstyle + + - Temporary files used by buildbot + + +output/build/logs/* +output/test-tmp/** + +output/build/webapps/docs/**/*.html +output/dist/webapps/docs/**/*.html +output/deployer/deployer-howto.html + +**/*.sha512 + +**/MANIFEST.MF +**/*.manifest + +output/dist/webapps/docs/*/copy.svg +output/dist/webapps/docs/*/element-list +output/dist/webapps/docs/*/jquery/** +output/dist/webapps/docs/*/jquery-ui.overrides.css +output/dist/webapps/docs/*/legal/** +output/dist/webapps/docs/*/member-search-index.js +output/dist/webapps/docs/*/module-search-index.js +output/dist/webapps/docs/*/package-search-index.js +output/dist/webapps/docs/*/resource-files/** +output/dist/webapps/docs/*/script.js +output/dist/webapps/docs/*/script-dir/** +output/dist/webapps/docs/*/script-files/** +output/dist/webapps/docs/*/search.js +output/dist/webapps/docs/*/search-page.js +output/dist/webapps/docs/*/tag-search-index.js +output/dist/webapps/docs/*/type-search-index.js + +output/dist/src/test/org/apache/tomcat/util/net/jsse/key-password +output/dist/src/test/org/apache/tomcat/util/net/key-password +output/dist/src/test/org/apache/tomcat/util/net/keystore-password +output/dist/src/test/webapp/404.html +output/dist/src/test/webapp/index.html.br +output/dist/src/test/webapp/bug49nnn/bug49464-cp1252.txt +output/dist/src/test/webapp/bug49nnn/bug49464-ibm850.txt +output/dist/src/test/webapp/bug49nnn/bug49464-iso-8859-1.txt +output/dist/src/test/webapp/bug49nnn/bug49464-utf-8-bom.txt +output/dist/src/test/webapp/bug49nnn/bug49464-utf-8.txt +output/dist/src/test/webapp/bug53257/*.txt +output/dist/src/test/webapp/bug53257/foo bar/foobar.txt +output/dist/src/test/webapp/bug66609/a&a.txt +output/dist/src/test/webapp/bug66609/b'b.txt +output/dist/src/test/webapp-fragments/WEB-INF/classes/*.txt +output/dist/src/test/webresources/dir1/d1/d1-f1.txt +output/dist/src/test/webresources/dir1/d2/d2-f1.txt +output/dist/src/test/webresources/dir1/*.txt +test/org/apache/tomcat/util/net/jsse/key-password +test/org/apache/tomcat/util/net/key-password +test/org/apache/tomcat/util/net/keystore-password +test/webapp/404.html +test/webapp/index.html.br +test/webapp/bug49nnn/bug49464-cp1252.txt +test/webapp/bug49nnn/bug49464-ibm850.txt +test/webapp/bug49nnn/bug49464-iso-8859-1.txt +test/webapp/bug49nnn/bug49464-utf-8-bom.txt +test/webapp/bug49nnn/bug49464-utf-8.txt +test/webapp/bug53257/*.txt +test/webapp/bug53257/foo bar/foobar.txt +test/webapp/bug66609/a&a.txt +test/webapp/bug66609/b'b.txt +test/webapp-fragments/WEB-INF/classes/*.txt +test/webresources/dir1/d1/d1-f1.txt +test/webresources/dir1/d2/d2-f1.txt +test/webresources/dir1/*.txt + + +**/*.json + +build.properties +res/maven/mvn.properties + +output/dist/src/res/install-win/tomcat-users_2.xml +output/dist/tomcat-users_2.xml +res/install-win/tomcat-users_2.xml + +java/jakarta/servlet/resources/javaee_5.xsd +java/jakarta/servlet/resources/javaee_6.xsd +java/jakarta/servlet/resources/javaee_7.xsd +java/jakarta/servlet/resources/javaee_8.xsd +java/jakarta/servlet/resources/jakartaee_9.xsd +java/jakarta/servlet/resources/jakartaee_10.xsd +java/jakarta/servlet/resources/javaee_web_services_1_2.xsd +java/jakarta/servlet/resources/javaee_web_services_1_3.xsd +java/jakarta/servlet/resources/javaee_web_services_1_4.xsd +java/jakarta/servlet/resources/jakartaee_web_services_2_0.xsd +java/jakarta/servlet/resources/javaee_web_services_client_1_2.xsd +java/jakarta/servlet/resources/javaee_web_services_client_1_3.xsd +java/jakarta/servlet/resources/javaee_web_services_client_1_4.xsd +java/jakarta/servlet/resources/jakartaee_web_services_client_2_0.xsd +java/jakarta/servlet/resources/jsp_2_2.xsd +java/jakarta/servlet/resources/jsp_2_3.xsd +java/jakarta/servlet/resources/jsp_3_0.xsd +java/jakarta/servlet/resources/jsp_3_1.xsd +java/jakarta/servlet/resources/web-app_3_0.xsd +java/jakarta/servlet/resources/web-app_3_1.xsd +java/jakarta/servlet/resources/web-app_4_0.xsd +java/jakarta/servlet/resources/web-app_5_0.xsd +java/jakarta/servlet/resources/web-app_6_0.xsd +java/jakarta/servlet/resources/web-common_3_0.xsd +java/jakarta/servlet/resources/web-common_3_1.xsd +java/jakarta/servlet/resources/web-common_4_0.xsd +java/jakarta/servlet/resources/web-common_5_0.xsd +java/jakarta/servlet/resources/web-common_6_0.xsd +java/jakarta/servlet/resources/web-fragment_3_0.xsd +java/jakarta/servlet/resources/web-fragment_3_1.xsd +java/jakarta/servlet/resources/web-fragment_4_0.xsd +java/jakarta/servlet/resources/web-fragment_5_0.xsd +java/jakarta/servlet/resources/web-fragment_6_0.xsd +java/jakarta/servlet/resources/web-jsptaglibrary_3_0.xsd +java/jakarta/servlet/resources/web-jsptaglibrary_3_1.xsd +output/classes/jakarta/servlet/resources/javaee_5.xsd +output/classes/jakarta/servlet/resources/javaee_6.xsd +output/classes/jakarta/servlet/resources/javaee_7.xsd +output/classes/jakarta/servlet/resources/javaee_8.xsd +output/classes/jakarta/servlet/resources/jakartaee_9.xsd +output/classes/jakarta/servlet/resources/jakartaee_10.xsd +output/classes/jakarta/servlet/resources/javaee_web_services_1_2.xsd +output/classes/jakarta/servlet/resources/javaee_web_services_1_3.xsd +output/classes/jakarta/servlet/resources/javaee_web_services_1_4.xsd +output/classes/jakarta/servlet/resources/jakartaee_web_services_2_0.xsd +output/classes/jakarta/servlet/resources/javaee_web_services_client_1_2.xsd +output/classes/jakarta/servlet/resources/javaee_web_services_client_1_3.xsd +output/classes/jakarta/servlet/resources/javaee_web_services_client_1_4.xsd +output/classes/jakarta/servlet/resources/jakartaee_web_services_client_1_4.xsd +output/classes/jakarta/servlet/resources/jakartaee_web_services_client_2_0.xsd +output/classes/jakarta/servlet/resources/jsp_2_2.xsd +output/classes/jakarta/servlet/resources/jsp_2_3.xsd +output/classes/jakarta/servlet/resources/jsp_3_0.xsd +output/classes/jakarta/servlet/resources/jsp_3_1.xsd +output/classes/jakarta/servlet/resources/web-app_3_0.xsd +output/classes/jakarta/servlet/resources/web-app_3_1.xsd +output/classes/jakarta/servlet/resources/web-app_4_0.xsd +output/classes/jakarta/servlet/resources/web-app_5_0.xsd +output/classes/jakarta/servlet/resources/web-app_6_0.xsd +output/classes/jakarta/servlet/resources/web-common_3_0.xsd +output/classes/jakarta/servlet/resources/web-common_3_1.xsd +output/classes/jakarta/servlet/resources/web-common_4_0.xsd +output/classes/jakarta/servlet/resources/web-common_5_0.xsd +output/classes/jakarta/servlet/resources/web-common_6_0.xsd +output/classes/jakarta/servlet/resources/web-fragment_3_0.xsd +output/classes/jakarta/servlet/resources/web-fragment_3_1.xsd +output/classes/jakarta/servlet/resources/web-fragment_4_0.xsd +output/classes/jakarta/servlet/resources/web-fragment_5_0.xsd +output/classes/jakarta/servlet/resources/web-fragment_6_0.xsd +output/classes/jakarta/servlet/resources/web-jsptaglibrary_3_0.xsd +output/classes/jakarta/servlet/resources/web-jsptaglibrary_3_1.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_5.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_6.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_7.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_8.xsd +output/dist/src/java/jakarta/servlet/resources/jakartaee_9.xsd +output/dist/src/java/jakarta/servlet/resources/jakartaee_10.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_web_services_1_2.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_web_services_1_3.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_web_services_1_4.xsd +output/dist/src/java/jakarta/servlet/resources/jakartaee_web_services_2_0.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_web_services_client_1_2.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_web_services_client_1_3.xsd +output/dist/src/java/jakarta/servlet/resources/javaee_web_services_client_1_4.xsd +output/dist/src/java/jakarta/servlet/resources/jakartaee_web_services_client_2_0.xsd +output/dist/src/java/jakarta/servlet/resources/jsp_2_2.xsd +output/dist/src/java/jakarta/servlet/resources/jsp_2_3.xsd +output/dist/src/java/jakarta/servlet/resources/jsp_3_0.xsd +output/dist/src/java/jakarta/servlet/resources/jsp_3_1.xsd +output/dist/src/java/jakarta/servlet/resources/web-app_3_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-app_3_1.xsd +output/dist/src/java/jakarta/servlet/resources/web-app_4_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-app_5_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-app_6_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-common_3_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-common_3_1.xsd +output/dist/src/java/jakarta/servlet/resources/web-common_4_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-common_5_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-common_6_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-fragment_3_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-fragment_3_1.xsd +output/dist/src/java/jakarta/servlet/resources/web-fragment_4_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-fragment_5_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-fragment_6_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-jsptaglibrary_3_0.xsd +output/dist/src/java/jakarta/servlet/resources/web-jsptaglibrary_3_1.xsd + +output/dist/src/res/checkstyle/header-al2.txt +res/checkstyle/header-al2.txt + +output/dist/temp/safeToDelete.tmp + + +**/*.bmp +**/*.dia + +**/*.md + +output/res/checkstyle/* + +tomcat-10.1.x/** diff --git a/res/scripts/check-mime.pl b/res/scripts/check-mime.pl new file mode 100755 index 0000000..23a6b6a --- /dev/null +++ b/res/scripts/check-mime.pl @@ -0,0 +1,471 @@ +#!/usr/bin/perl + +# 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. + +# ----------------------------------------------------------------------------- +# Merge the MIME type definitions contained in the +# file mime.types from the httpd project into Tomcat web.xml. +# ----------------------------------------------------------------------------- + +# The script uses two mime type lists to describe +# the merging between httpd and Tomcat mime types. +# +# - %TOMCAT_ONLY: Additional extensions for Tomcat that do not exist in httpd +# - %TOMCAT_KEEP: Mime type differences for common extensions where we stick to +# the Tomcat definition + +# The script checks consistency between Tomcat and httpd according +# to the lists TOMCAT_ONLY and TOMCAT_KEEP and generates a new web.xml: +# +# A) Additional extensions in Tomcat which are not part of TOMCAT_ONLY +# are logged. They will be removed in the generated new web.xml. +# If you want to keep them, add them to TOMCAT_ONLY and run the +# script again. If you want to remove them, commit the generated +# new web.xml. +# B) Mime type differences for the same extension between httpd +# and Tomcat that are not part of TOMCAT_KEEP are logged. +# They will be overwritten with the httpd definition in the generated +# new web.xml. If you want to keep their Tomcat definition, add them +# to TOMCAT_KEEP and run the script again. If you want to use the +# definitions from httpd, commit the generated new web.xml. +# C) Additional extensions in httpd are logged. The script outputs a +# merged web.xml, which already includes all those additional +# extensions. If you want to keep them, commit the generated +# new web.xml. +# D) If the extensions are not sorted alphabetically, a message is logged. +# The generated web.xml will always be sorted alphabetically. +# If you want to keep the alphabetical sort order, commit the generated +# new web.xml. + +use strict; +use locale; +use POSIX qw(locale_h); +use Getopt::Std; + +################### BEGIN VARIABLES WHICH MUST BE MAINTAINED ##################### + +# Script version, printed via getopts with "--version" +our $VERSION = '1.1'; + +# Locale used via LC_COLLATE when sorting extensions +my $LOCALE = 'en.UTF-8'; + +# Mime types that are part of the Tomcat +# configuration, but missing from httpd + +my %TOMCAT_ONLY = qw( + abs audio/x-mpeg + aim application/x-aim + anx application/annodex + art image/x-jg + avx video/x-rad-screenplay + axa audio/annodex + axv video/annodex + body text/html + dib image/bmp + dv video/x-dv + gz application/x-gzip + htc text/x-component + jsf text/plain + jspf text/plain + m4b audio/mp4 + m4r audio/mp4 + mp1 audio/mpeg + mpa audio/mpeg + mac image/x-macpaint + mpega audio/x-mpeg + mpv2 video/mpeg2 + pict image/pict + pnt image/x-macpaint + qti image/x-quicktime + qtif image/x-quicktime + shtml text/x-server-parsed-html + ulw audio/basic + z application/x-compress +); + +# Mime types, that are defined differently +# in Tomcat than in httpd + +my %TOMCAT_KEEP = qw( + cdf application/x-cdf + class application/java + exe application/octet-stream + flac audio/flac + m4v video/mp4 + mif application/x-mif + pct image/pict + pic image/pict + pls audio/x-scpls +); + +################### END VARIABLES WHICH MUST BE MAINTAINED ##################### + +# Global data variables +# Mime type definitions from httpd +my %httpd; +# Mime type definitions from Tomcat +my %tomcat; +# Comments found when parsing mime type definitions +my %tomcat_comments; +# Is the whole mime type commented out? +my %tomcat_commented; +# List of extensions found in the original order +my @tomcat_extensions; +# Text in web.xml before and after the mime-type definitions +my $tomcat_pre; my $tomcat_post; + + +# Helper variables +my $i; +my $line; +my $mimetype; +my @extensions; +my $extension; +my $type; +my $comment; +my $commented; +my $msg; +my $previous; +my $current; +# File handles +my $mimetypes_fh; +my $webxml_fh; +my $output_fh; + + +# Usage/Help +sub HELP_MESSAGE { + my $fh = shift; + print $fh "Usage:: $0 -m MIMEFILE -i INPUTFILE -o OUTPUTFILE\n"; + print $fh " MIMEFILE: path to mime.types from the httpd project\n"; + print $fh " INPUTFILE: path to existing web.xml, which will be checked\n"; + print $fh " OUTPUTFILE: path to the new (generated) web.xml. Any existing\n"; + print $fh " file will be overwritten.\n"; +} + + +# Parse arguments: +# -m: mime.types file (httpd) to use +# -i: input web.xml file to check +# -o: output web.xml file (gets generated and overwritten) + +$Getopt::Std::STANDARD_HELP_VERSION = 1; +our ($opt_m, $opt_i, $opt_o); +getopts('m:i:o:'); + + +# Check whether mandatory arguments are given +if ($opt_m eq '' || $opt_i eq '' || $opt_o eq '') { + HELP_MESSAGE(*STDOUT); + exit 1; +} + + +# Switch locale for alphabetical ordering +setlocale(LC_COLLATE, $LOCALE); + +# Check whether TOMCAT_ONLY and TOMCAT_KEEP are disjoint +for $extension (sort keys %TOMCAT_ONLY) { + if (exists($TOMCAT_KEEP{$extension})) { + push(@extensions, ($extension)); + } +} +if (@extensions > 0) { + print STDERR "FATAL Lists TOMCAT_ONLY and TOMCAT_KEEP must be disjoint.\n"; + print STDERR "FATAL Common entries are: " . join(', ', @extensions) . " - Aborting!\n"; + exit 6; +} + +# Read and parse httpd mime.types, build up hash extension->mime-type +open($mimetypes_fh, '<', $opt_m) or die "Could not open file '$opt_m' for read - Aborting!"; +while (<$mimetypes_fh>) { + chomp($_); + $line = $_; + $line =~ s/#.*//; + $line =~ s/^\s+//; + if ($line ne '') { + ($mimetype, @extensions) = split(/\s+/, $line); + if (@extensions > 0) { + for $extension (@extensions) { + $httpd{$extension} = $mimetype; + } + } else { + print STDERR "WARN mime.types line ignored: $_\n"; + } + } +} +close($mimetypes_fh); + +# Read and parse web.xml, build up hash extension->mime-type +# and store the file parts form before and after mime mappings. +open($webxml_fh, '<', $opt_i) or die "Could not open file '$opt_i' for read - Aborting!"; + +# Skip and record all lines before the first mime type definition. +# Because of comment handling we need to read one line ahead. +$line = ''; +while (<$webxml_fh>) { + if ($_ !~ //) { + $tomcat_pre .= $line; + } else { + last; + } + $line = $_; +} + +$commented = 0; +# If the previous line was start of a comment +# set marker, else add it to pre. +if ($line =~ /^\s*\s*$/) { + $comment = $1; + $_ = <$webxml_fh>; + chomp($_); + } + if ($_ =~ /^\s*([^<]*)<\/extension>\s*$/ ) { + $extension = $1; + $extension =~ s/^\s+//; + $extension =~ s/\s+$//; + } else { + print STDERR "ERROR Parse error in Tomcat mime-mapping line $.\n"; + print STDERR "ERROR Expected ...', got '$_' - Aborting!\n"; + close($webxml_fh); + exit 2; + } + $_ = <$webxml_fh>; + chomp($_); + if ($_ =~ /^\s*([^<]*)<\/mime-type>\s*$/ ) { + $type = $1; + $type =~ s/^\s+//; + $type =~ s/\s+$//; + if (exists($tomcat{$extension}) && $tomcat{$extension} ne $type) { + print STDERR "WARN MIME mapping redefinition detected!\n"; + print STDERR "WARN Kept '$extension' -> '$tomcat{$extension}'\n"; + print STDERR "WARN Ignored '$extension' -> '$type'\n"; + } else { + $tomcat{$extension} = $type; + if ($comment ne '') { + $tomcat_comments{$extension} = $comment; + } + if ($commented) { + $tomcat_commented{$extension} = 1; + } + push(@tomcat_extensions, $extension); + } + } else { + print STDERR "ERROR Parse error in Tomcat mime-mapping line $.\n"; + print STDERR "ERROR Expected ...', got '$_' - Aborting!\n"; + close($webxml_fh); + exit 3; + } + $_ = <$webxml_fh>; + chomp($_); + if ($_ !~ /^\s*<\/mime-mapping>\s*$/) { + print STDERR "ERROR Parse error in Tomcat mime-mapping line $.\n"; + print STDERR "ERROR Expected '', got '$_' - Aborting!\n"; + close($webxml_fh); + exit 4; + } + $_ = <$webxml_fh>; + # Check for comment closure + if ($commented && $_ =~ /^[^<]*-->\s*$/) { + $commented = 0; + $_ = <$webxml_fh>; + } + # Check for comment opening + if ($_ =~ /^\s*\n"; + } + print $output_fh " $extension\n"; + print $output_fh " $httpd{$extension}\n"; + print $output_fh " \n"; + if (exists($tomcat_commented{$extension})) { + print $output_fh " -->\n"; + } +} +print $output_fh $tomcat_post; +close($output_fh); +print "New file '$opt_o' has been written.\n"; diff --git a/res/spotbugs/filter-false-positives.xml b/res/spotbugs/filter-false-positives.xml new file mode 100644 index 0000000..d9c9a7e --- /dev/null +++ b/res/spotbugs/filter-false-positives.xml @@ -0,0 +1,2662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/welcome.bin.html b/res/welcome.bin.html new file mode 100644 index 0000000..095c800 --- /dev/null +++ b/res/welcome.bin.html @@ -0,0 +1,86 @@ + + + + + +Apache Tomcat @VERSION@ + + + + +

    Apache Tomcat @VERSION@

    +

    Useful references:

    +
    + +

    NOTE: The tar files in this distribution use GNU tar extensions, +and must be untarred with a GNU compatible version of tar. The version +of tar on Solaris and Mac OS X will not work with +these files.

    + +

    Tomcat @VERSION_MAJOR_MINOR@ requires Java @MIN_JAVA_VERSION@ or later. Read the +RELEASE-NOTES and the RUNNING.txt file in the distribution for more details. +

    + +

    Packaging Details (or "What Should I Download?")

    +
    +
    bin/
    +
    +
    apache-tomcat-[version].zip or .tar.gz
    +
    Base distribution. These distributions do not include the Windows + service wrapper nor the compiled APR/native library for Windows.
    +
    apache-tomcat-[version].exe
    +
    32-bit/64-bit Windows installer for Tomcat.   Please note that while this + distribution includes the vast majority of the base distribution, some of the + command-line scripts for launching Tomcat are not included. This distribution + is intended for those users planning to launch Tomcat through the Windows + shortcuts or services.
    +
    apache-tomcat-[version]-windows-x86.zip
    +
    32-bit Windows specific distribution that includes the Windows service + wrapper and the compiled APR/native library for use with 32-bit JVMs on both + 32 and 64 bit Windows platforms.
    +
    apache-tomcat-[version]-windows-x64.zip
    +
    64-bit Windows specific distribution that includes the Windows service + wrapper and the compiled APR/native library for use with 64-bit JVMs on + x64 Windows platforms.
    +
    apache-tomcat-[version]-deployer.zip or .tar.gz
    +
    The standalone + + Tomcat Web Application Deployer.
    +
    apache-tomcat-[version]-fulldocs.tar.gz
    +
    The Tomcat documentation bundle, including complete javadocs.
    +
    +
    src/
    +
    +
    apache-tomcat-[version].zip or .tar.gz
    +
    The source code. See + building instructions.
    +
    +
    + +

    Thank you for using Tomcat! +

    +

    The Apache Tomcat Project
    https://tomcat.apache.org/

    + diff --git a/res/welcome.main.html b/res/welcome.main.html new file mode 100644 index 0000000..ff22030 --- /dev/null +++ b/res/welcome.main.html @@ -0,0 +1,86 @@ + + + + + +Apache Tomcat @VERSION@ + + + + +

    Apache Tomcat @VERSION@

    +

    Useful references:

    + + +

    NOTE: The tar files in this distribution use GNU tar extensions, +and must be untarred with a GNU compatible version of tar. The version +of tar on Solaris and Mac OS X will not work with +these files.

    + +

    Tomcat @VERSION_MAJOR_MINOR@ requires Java @MIN_JAVA_VERSION@ or later. Read the +RELEASE-NOTES and the RUNNING.txt file in the distribution for more details. +

    + +

    Packaging Details (or "What Should I Download?")

    +
    +
    bin/
    +
    +
    apache-tomcat-[version].zip or .tar.gz
    +
    Base distribution. These distributions do not include the Windows + service wrapper nor the compiled APR/native library for Windows.
    +
    apache-tomcat-[version].exe
    +
    32-bit/64-bit Windows installer for Tomcat.   Please note that while this + distribution includes the vast majority of the base distribution, some of the + command-line scripts for launching Tomcat are not included. This distribution + is intended for those users planning to launch Tomcat through the Windows + shortcuts or services.
    +
    apache-tomcat-[version]-windows-x86.zip
    +
    32-bit Windows specific distribution that includes the Windows service + wrapper and the compiled APR/native library for use with 32-bit JVMs on both + 32 and 64 bit Windows platforms.
    +
    apache-tomcat-[version]-windows-x64.zip
    +
    64-bit Windows specific distribution that includes the Windows service + wrapper and the compiled APR/native library for use with 64-bit JVMs on + x64 Windows platforms.
    +
    apache-tomcat-[version]-deployer.zip or .tar.gz
    +
    The standalone + + Tomcat Web Application Deployer.
    +
    apache-tomcat-[version]-fulldocs.tar.gz
    +
    The Tomcat documentation bundle, including complete javadocs.
    +
    +
    src/
    +
    +
    apache-tomcat-[version].zip or .tar.gz
    +
    The source code. See + building instructions.
    +
    +
    + +

    Thank you for using Tomcat! +

    +

    The Apache Tomcat Project
    https://tomcat.apache.org/

    + diff --git a/test/META-INF/services/jakarta.servlet.ServletContainerInitializer b/test/META-INF/services/jakarta.servlet.ServletContainerInitializer new file mode 100644 index 0000000..6cb967c --- /dev/null +++ b/test/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -0,0 +1,17 @@ +# 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. + +# SCIs that should be added when running tests +org.apache.jasper.servlet.JasperInitializer \ No newline at end of file diff --git a/test/conf/TesterRewriteMapB.txt b/test/conf/TesterRewriteMapB.txt new file mode 100644 index 0000000..3817ad7 --- /dev/null +++ b/test/conf/TesterRewriteMapB.txt @@ -0,0 +1,24 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +## +## map.txt -- rewriting map +## + +a aa + +aa aaaa +b bb diff --git a/test/conf/TesterRewriteMapC.txt b/test/conf/TesterRewriteMapC.txt new file mode 100644 index 0000000..8590998 --- /dev/null +++ b/test/conf/TesterRewriteMapC.txt @@ -0,0 +1,24 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +## +## map.txt -- rewriting map +## + +a aa|aa|aa|aa + +b bb|aa|bb|aa +aa aaaa|aaaa diff --git a/test/conf/jaspic-test-01.xml b/test/conf/jaspic-test-01.xml new file mode 100644 index 0000000..8e0adbd --- /dev/null +++ b/test/conf/jaspic-test-01.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/test/conf/jaspic-test-02.xml b/test/conf/jaspic-test-02.xml new file mode 100644 index 0000000..3f26d56 --- /dev/null +++ b/test/conf/jaspic-test-02.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/test/conf/jaspic-test-04.xml b/test/conf/jaspic-test-04.xml new file mode 100644 index 0000000..4ac64d7 --- /dev/null +++ b/test/conf/jaspic-test-04.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/test/deployment/broken.war b/test/deployment/broken.war new file mode 100644 index 0000000000000000000000000000000000000000..09b67885b41c59459c45a2b465a8fd3634a1bf03 GIT binary patch literal 1507 zcmWIWW@Zs#U|`^2$o5_9v}?OlNf?lq0>oTEoSB!BTA`OwlAEJ@#+$1_L4@@}g@f?<0ji}lf4#ma4|c#^OVWPbD~`R zWdfyRR?Dz;2O6E5(i|{ppXa}+lZxvZ0-&}tFib7m>IAe94mg1BoLXF*nV;vJpP!wX>X(?C zYO56P7!>d9?-$}45u&7^Z-?6r96-avU7g@gfP~X5pi6aTr=MTI#K7>1g@J(|NmY4j z63F)65a0aS4g!1KPu0&T@={?gTjZ6QWnJ`n;i6p;Tz)qS;+5O(tFF!K= z9Q)qi&+yE~b)g}%^1&RzG*R!DQ&03}&T!f>dFzQ8iY^v2Rm+zeM~S zD^Vo=dfT}!F~Nv+0^TBS>p(m7=w68z&00=XFidkSGM2Mo|n&i!fnC_-Ste|o0o(g`F6n7I_CP-S1~7dO}?mY z$CJbme~a-@q`{T|b+hBI_f;)_pXh&8^>*_FwyaAxogeERJtYwOC^U>UUFm3Ce>B9p3IhR8oKe?uUDfeu% zMnJaI)&5!9QXi`L*R0Z*zqT{;pwy=Gkx$lapCf&g_iPEnuFvxl;;O!6r`S5Ki&ym! zQaUhORX|m)W`=5@(FTS7i=Q4pO62U{{QGz0o}N76h&zYBD(&l>c2Q~B3%<0ETz0M> z&oWF1=5{^2MnrRi$xd0dv?n>+J#1NbtLa@6T~>2Ac#_(uSx-53E?KScW?IM!uXSrz z&f-WDTlPTJJa6`w`MXw3e1F^e(t}&eS2Et!UZ@`T{Gj=_Gfc7F4KaK#gEh8TJAdKW z<-6}rrXTCx%(FX<-{e@xw8qyH%^viKXrM2fHxzPJvi@R%l0b31PuZT zU=q0y;DU-VGDt9-Q?Y%s)&Cr0LiE+E$TY%iY?&HnwgAxVIv|GUU1XzAJ=jyK6?Tph zM*9}K^KAoK1u+^|K>#xvcT2Z1`o>;{FAuUZXF(KhW z!ijIOueW6H4)&CoFrDd0jKiFn(mzq2Kl8iy-{O9{uPVRawnXt{+U`Jl$>mv;;7L7Nqn;r?ge)+tw{E{AS9UXyf z=Q@wxbBL5a!ImhPl6vxIrRqvi)hf}QmS*P!wmB7M6&9w|%`2{}D@$7ypOm+1ZP1S7 zfIXY{$9H7AbWCd67u#U2JlU3cok2k3E_X(k1uY!nAw3fvn;y>TdOLgVzvmLdi`K`v zE)z^k{;YT=hf!gj^EZx77v-;gI~T%ZahzjuZTH=u+Ya^~6V*C#F0MoTEoSB!BTA`OwlAEJ@#+$1_L4@@}g@f?<0ji}lf4#ma4|c#^OVWPbD~`R zWdfyRR?Dz;2O6E5(i|{ppXa}+lZxvZ0-&}tFt~16>jbnA4mg1BoLXF*nV;vJpP!wX>X(?C zYO56P7!>d9?-$}45u&7^Zx`Ur$Yc*T2wNDa04)Fk1u%&mB3w{0Mg|Fnb1JrPw)&rA zOo+aE6`4kujm?KJvju==*8wryBgjU7Slc^OG2k2{jP@;d=i3If3Su;_P=Of@a`sYW rqmjZ0*%novWriI + \ No newline at end of file diff --git a/test/deployment/contextCopyXMLFalse.war b/test/deployment/contextCopyXMLFalse.war new file mode 100644 index 0000000000000000000000000000000000000000..c231b32d1ba1a58fd15b71ed9e324835319d8435 GIT binary patch literal 733 zcmWIWW@Zs#;Nak3Sdnnli2(_4GO#fCx`sIFdiuHP|2xINz|0Wf&CUT*!30$nfK#&w zPz7AGucM!*n`>~0p0C?y-!rFuymj?1@_OrPojY@WbCAIm;|EWR^t^m^Jbf>gu43Vw z@mlhT^epX+AL6QN&zERfJQo*zBCa}(VQTiLqK`$4U`Ny{-j8<%THyl32uJ9|qWS>E z5y|;^C8-r9dKI}jn+wmK*Y;ei5vbv(s|R#M@wxNo&-r}yKI6MJl&hSltNhz{?!sP< zz#xxv`YU=qeBBu8<$XreFEr@F_QywC&X6zFH?u-uYkolUk)yVM9Hr0P9z jCV+wy*=wNSL;xp7j$BAbkNS%fJ8tyN<$0 literal 0 HcmV?d00001 diff --git a/test/deployment/contextCopyXMLTrue.war b/test/deployment/contextCopyXMLTrue.war new file mode 100644 index 0000000000000000000000000000000000000000..c55039b9f35f906fa83e5a7f906d8919754a62f1 GIT binary patch literal 732 zcmWIWW@Zs#;Nak3IFNAEi2(_4GO#fCx`sIFdiuHP|2xINz|0Wf&CUT*!30$nfK#&w zPz7AGucM!*n`>~0p0C?y-!rFuymj?1@_OrPojY@WbCAIm;|EWR^t^m^Jbf>gu43Vw z@mlhT^epX+AL6QN&zERfJQo*zBCa}(VQTiLqK`$4U`Ny{-j8<%THyl32uJKj^8tz@ zlJoOQQY%XIDspo+7oIz>?YUMXP{U7G59o;EbLY>W^ZDw1#&>HdS2<5t`M2-fg}oes zK_2JySM+@Nx-nEsQ_D9r=)(5LOknHPIiAn61zK&5VtuynT9g3f0?KCQrKDEqWt0Fd zJ*jc>?8e7Wg90{qp7LMGvbo_6)9!-f3av|yJh~#%`ss>F>yf6WIcJ_6nRMolP3xyK zlXzP9)U-bNvuDzkE}^|$cVt?(oS741EB%+L!}C|b;+!uBmqB7=*LJ6pFrZy2VAn7* zi7=o>C@i)>5sC`nF$9X<0B=;S$YBf$Ndy3S9j+BAa*<5{#RYOOp_*_A*#xARK{f#t goXB1S1t$VHG9m|VfHx}}NR9;vzXR#>Kv@O`07w(TUH||9 literal 0 HcmV?d00001 diff --git a/test/deployment/contextUnpackWARFalse.war b/test/deployment/contextUnpackWARFalse.war new file mode 100644 index 0000000000000000000000000000000000000000..40b6630ff00c4494b59338318fad4c534b30e3c7 GIT binary patch literal 583 zcmWIWW@Zs#U|`^2$o5_9v}?OlNf?lq0>oTEoSB!BTA`OwlAEJ@#+$1_L4@@}g@f?<0ji}lf4#ma4|c#^OVWPbD~`R zWdfyRR?Dz;2O6E5(i|{ppXa}+lZxvZ0-&}tFt~16>jbnA4mg1Z$mwup$@zIDsTC!96}dS!&JeajXPtc+21e3H6lbwLEkRGn~}*LY&5n|Q2|;B0t#Rf zIgGfVVvGzD4ChpA-)!|i$Cwa(^(r!rFdLhnVP*>e&8`DtxYv-4{;;-preeT3Mi}i| z?9R6hXcfe0T;T*W8szMy$VMZD8nP|((+y{qgq&mSnl@V)WIBuv@MdKLYG+^uLRm&& J3;;0$0|1i#lNA5} literal 0 HcmV?d00001 diff --git a/test/deployment/contextUnpackWARTrue.war b/test/deployment/contextUnpackWARTrue.war new file mode 100644 index 0000000000000000000000000000000000000000..9017b197cbd709b755435ab0892ef61f385787db GIT binary patch literal 582 zcmWIWW@Zs#U|`^2$o5_9v}?OlNf?lq0>oTEoSB!BTA`OwlAEJ@#+$1_L4@@}g@f?<0ji}lf4#ma4|c#^OVWPbD~`R zWdfyRR?Dz;2O6E5(i|{ppXa}+lZxvZ0-&}tFt~16>jbnA4mg1!tW?-$}45u&7^Zx`Ur$Yc*T8C#&J0Br;T1u%&m zL|jlYMg|Fnb1JrPw)&rAOo+aE6`4kujm^t2vju==*8wryXUIl>Slc^OG2k2{jP@;d z=i3If3Su;_V1gM9a`sYWqmcp)*_Nb_{%cA?&M|gPn=K479YzOuv$6rTGcW_8EF%Mh JFc32^006Vdk^2Au literal 0 HcmV?d00001 diff --git a/test/deployment/dir with spaces/context.jar b/test/deployment/dir with spaces/context.jar new file mode 100644 index 0000000000000000000000000000000000000000..105d9f9b6521dc15fe766d2fbd7d7e8ccfa75aac GIT binary patch literal 489 zcmWIWW@Zs#U|`^2xLwfb7NEVQ+n14n!IX)Cfg32AoS#>cT2Z1`o>;{FAuUZXF(KhW z!ijIOueW6H4)&CoFrDd0jKiFn(mzq2Kl8iy-{O9{uPVRawnXt{+U`Jl$>mv;;7L7Nqn;r?ge)+tw{E{AS9UXyf z=Q@wxbBL5a!ImhPl6vxIrRqvi)hf}QmS*P!wmB7M6&9w|%`2{}D@$7ypOm+1ZP1S7 zfIXY{$9H7AbWCd67u#U2JlU3cok2k3E_X(k1uY!nAw3fvn;y>TdOLgVzvmLdi`K`v zE)z^k{;YT=hf!gj^EZx77v-;gI~T%ZahzjuZTH=u+Ya^~6V*C#F0MoTEoSB!BTA`OwlAEJ@#+$1_L4@@}g@f?<0ji}lf4#ma4|c#^OVWPbD~`R zWdfyRR?Dz;2O6E5(i|{ppXa}+lZxvZ0-&}tFt~16>jbnA4mg1BoLXF*nV;vJpP!wX>X(?C zYO56P7!>d9?-$}45u&7^Zx`Ur$Yc*T2wNDa04)Fk1u%&mB3w{0Mg|Fnb1JrPw)&rA zOo+aE6`4kujm?KJvju==*8wryBgjU7Slc^OG2k2{jP@;d=i3If3Su;_P=Of@a`sYW rqmjZ0*%novWriI + \ No newline at end of file diff --git a/test/deployment/dirContext/index.html b/test/deployment/dirContext/index.html new file mode 100644 index 0000000..0753b0a --- /dev/null +++ b/test/deployment/dirContext/index.html @@ -0,0 +1,22 @@ + + + + +

    Directory based web application with a context.xml file.

    + + \ No newline at end of file diff --git a/test/deployment/dirNoContext/index.html b/test/deployment/dirNoContext/index.html new file mode 100644 index 0000000..9b3b5a4 --- /dev/null +++ b/test/deployment/dirNoContext/index.html @@ -0,0 +1,22 @@ + + + + +

    Directory based web application with no context.xml file.

    + + \ No newline at end of file diff --git a/test/deployment/noContext.war b/test/deployment/noContext.war new file mode 100644 index 0000000000000000000000000000000000000000..ef951bc3e97e9c7b920972593bb0dd18996a4fd3 GIT binary patch literal 240 zcmWIWW@Zs#U|`^2$o5_9v}?OlNf?lq0>oTEoSB!BTA`OwlAEJ@#+$1_L4@@}g@f?<0ji}lf4#ma4|c#^OVWPbD~`R zWdfyRR?Dz;2O6E5(i|{ppXa}+lZxvZ0=yZS>=|&`uL5)d2q=I_WY=&(#TXeR7|yBK ezS-)3jxizn>Q!Vqz?+o~q>&K_V}NuW*c1TaqeTw@ literal 0 HcmV?d00001 diff --git a/test/jakarta/el/TestArrayELResolver.java b/test/jakarta/el/TestArrayELResolver.java new file mode 100644 index 0000000..49f0e1f --- /dev/null +++ b/test/jakarta/el/TestArrayELResolver.java @@ -0,0 +1,522 @@ +/* + * 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 jakarta.el; + +import org.junit.Assert; +import org.junit.Test; + +public class TestArrayELResolver { + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetType01() { + ArrayELResolver resolver = new ArrayELResolver(); + resolver.getType(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is not an array. + */ + @Test + public void testGetType02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.GET_TYPE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetType03() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + Class result = resolver.getType(context, base, Integer.valueOf(0)); + + Assert.assertEquals(base.getClass().getComponentType(), result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that the key is out of bounds and exception will be thrown. + */ + @Test(expected = PropertyNotFoundException.class) + public void testGetType04() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.getType(context, base, Integer.valueOf(1)); + } + + /** + * Tests that a result is returned even when a coercion cannot be performed. + */ + @Test + public void testGetType05() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + Class result = resolver.getType(context, base, "index"); + + Assert.assertEquals(base.getClass().getComponentType(), result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a null result is returned when the base is null. + */ + @Test + public void testGetType06() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Class result = resolver.getType(context, null, "index"); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetValue01() { + ArrayELResolver resolver = new ArrayELResolver(); + resolver.getValue(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is not an array. + */ + @Test + public void testGetValue02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.GET_VALUE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetValue03() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + Object result = resolver.getValue(context, base, Integer.valueOf(0)); + + Assert.assertEquals("element", result); + Assert.assertTrue(context.isPropertyResolved()); + } + + @Test + public void testGetValueCoercion01() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + Object result = resolver.getValue(context, base, Character.valueOf((char) 0)); + + Assert.assertEquals("element", result); + Assert.assertTrue(context.isPropertyResolved()); + } + + @Test + public void testGetValueCoercion02a() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + Object result = resolver.getValue(context, base, Boolean.FALSE); + + Assert.assertEquals("element", result); + Assert.assertTrue(context.isPropertyResolved()); + } + + @Test + public void testGetValueCoercion02b() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + Object result = resolver.getValue(context, base, Boolean.TRUE); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + @Test + public void testGetValueCoercion03() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + Object result = resolver.getValue(context, base, "0"); + + Assert.assertEquals("element", result); + Assert.assertTrue(context.isPropertyResolved()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetValueCoercion04() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.getValue(context, base, new Object()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetValueCoercion05() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.getValue(context, base, null); + } + + /** + * Tests a coercion cannot be performed as the key is not integer. + */ + @Test(expected = IllegalArgumentException.class) + public void testGetValueCoercion06() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.getValue(context, base, "key"); + } + + /** + * Tests that if the key is out of bounds null will be returned. + */ + @Test + public void testGetValue05() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + Object result = resolver.getValue(context, base, Integer.valueOf(1)); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + + result = resolver.getValue(context, base, Integer.valueOf(-1)); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testSetValue01() { + ArrayELResolver resolver = new ArrayELResolver(); + resolver.setValue(null, new Object(), new Object(), new Object()); + } + + /** + * Tests that a valid property is not set if base is not an array. + */ + @Test + public void testSetValue02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.SET_VALUE, false); + } + + /** + * Tests that an exception is thrown when readOnly is true. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue03() { + ArrayELResolver resolver = new ArrayELResolver(true); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.setValue(context, new String[] {}, new Object(), new Object()); + } + + /** + * Tests that a valid property is set. + */ + @Test + public void testSetValue04() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.setValue(context, base, Integer.valueOf(0), "new-element"); + + Assert.assertEquals("new-element", resolver.getValue(context, base, Integer.valueOf(0))); + Assert.assertTrue(context.isPropertyResolved()); + + resolver.setValue(context, base, Integer.valueOf(0), null); + + Assert.assertEquals(null, resolver.getValue(context, base, Integer.valueOf(0))); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests a coercion cannot be performed as the key is not integer. + */ + @Test(expected = IllegalArgumentException.class) + public void testSetValue05() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.setValue(context, base, "key", "new-element"); + } + + /** + * Tests that the key is out of bounds and exception will be thrown. + */ + @Test(expected = PropertyNotFoundException.class) + public void testSetValue06() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.setValue(context, base, Integer.valueOf(1), "new-element"); + } + + /** + * Tests that an exception will be thrown if the value is not from the corresponding type. + */ + @Test(expected = ClassCastException.class) + public void testSetValue07() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.setValue(context, base, Integer.valueOf(0), Integer.valueOf(1)); + } + + /** + * Tests setting arrays of primitives. https://bz.apache.org/bugzilla/show_bug.cgi?id=55691 + */ + @Test + public void testSetValue08() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + int[] base = new int[] { 1, 2, 3 }; + resolver.setValue(context, base, Integer.valueOf(1), Integer.valueOf(4)); + + Assert.assertEquals(Integer.valueOf(base[1]), Integer.valueOf(4)); + } + + /* + * Null base should be a NO-OP rather than an exception + */ + @Test + public void testSetValue09() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.setValue(context, null, Integer.valueOf(1), Integer.valueOf(4)); + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testIsReadOnly01() { + ArrayELResolver resolver = new ArrayELResolver(); + resolver.isReadOnly(null, new Object(), new Object()); + } + + /** + * Tests that the propertyResolved is false if base is not an array. + */ + @Test + public void testIsReadOnly02() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, new Object(), new Object()); + + Assert.assertFalse(result); + Assert.assertFalse(context.isPropertyResolved()); + + resolver = new ArrayELResolver(true); + + result = resolver.isReadOnly(context, new Object(), new Object()); + + Assert.assertTrue(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that if the ArrayELResolver is constructed with readOnly the method will return always true, otherwise + * false. + */ + @Test + public void testIsReadOnly03() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + boolean result = resolver.isReadOnly(context, base, Integer.valueOf(0)); + + Assert.assertFalse(result); + Assert.assertTrue(context.isPropertyResolved()); + + resolver = new ArrayELResolver(true); + + result = resolver.isReadOnly(context, base, Integer.valueOf(0)); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that the key is out of bounds and exception will be thrown. + */ + @Test(expected = PropertyNotFoundException.class) + public void testIsReadOnly04() { + doTestIsReadOutOfBounds(1); + } + + @Test(expected = PropertyNotFoundException.class) + public void testIsReadOnly05() { + doTestIsReadOutOfBounds(-1); + } + + private void doTestIsReadOutOfBounds(int index) { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + resolver.isReadOnly(context, base, Integer.valueOf(index)); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a result is returned even when a coercion cannot be performed. + */ + @Test + public void testIsReadOnly06() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + String[] base = new String[] { "element" }; + boolean result = resolver.isReadOnly(context, base, "key"); + + Assert.assertFalse(result); + Assert.assertTrue(context.isPropertyResolved()); + + resolver = new ArrayELResolver(true); + + result = resolver.isReadOnly(context, base, "key"); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + @Test + public void testIsReadOnly07() { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, null, null); + + Assert.assertFalse(result); + Assert.assertFalse(context.isPropertyResolved()); + + resolver = new ArrayELResolver(true); + + result = resolver.isReadOnly(context, null, null); + + Assert.assertTrue(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + private void doNegativeTest(Object base, Object trigger, MethodUnderTest method, boolean checkResult) { + ArrayELResolver resolver = new ArrayELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = null; + switch (method) { + case GET_VALUE: { + result = resolver.getValue(context, base, trigger); + break; + } + case SET_VALUE: { + resolver.setValue(context, base, trigger, new Object()); + break; + } + case GET_TYPE: { + result = resolver.getType(context, base, trigger); + break; + } + default: { + // Should never happen + Assert.fail("Missing case for method"); + } + } + + if (checkResult) { + Assert.assertNull(result); + } + Assert.assertFalse(context.isPropertyResolved()); + } + + private enum MethodUnderTest { + GET_VALUE, + SET_VALUE, + GET_TYPE + } + + + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + @Test + public void testGetFeatureDescriptors() { + // Should always return null + ArrayELResolver resolver = new ArrayELResolver(); + Assert.assertNull(resolver.getFeatureDescriptors(null, null)); + } + + + @Test + public void testGetCommonPropertyType01() { + // null base, null response + ArrayELResolver resolver = new ArrayELResolver(); + Assert.assertNull(resolver.getCommonPropertyType(null, null)); + } + + + @Test + public void testGetCommonPropertyType02() { + // non-array base, null response + ArrayELResolver resolver = new ArrayELResolver(); + Assert.assertNull(resolver.getCommonPropertyType(null, new Object())); + } + + + @Test + public void testGetCommonPropertyType03() { + // array base, Integer response + ArrayELResolver resolver = new ArrayELResolver(); + Class clazz = resolver.getCommonPropertyType(null, new Object[] {}); + Assert.assertNotNull(clazz); + Assert.assertEquals(clazz, Integer.class); + } +} diff --git a/test/jakarta/el/TestBeanELResolver.java b/test/jakarta/el/TestBeanELResolver.java new file mode 100644 index 0000000..fd73060 --- /dev/null +++ b/test/jakarta/el/TestBeanELResolver.java @@ -0,0 +1,1066 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.Iterator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.el.ELContextImpl; + +public class TestBeanELResolver { + + private static final String METHOD01_NAME = "toString"; + private static final String METHOD02_NAME = ""; + private static final String METHOD03_NAME = "nonExistingMethod"; + private static final String BEAN_NAME = "test"; + private static final String PROPERTY01_NAME = "valueA"; + private static final String PROPERTY02_NAME = "valueB"; + private static final String PROPERTY03_NAME = "name"; + private static final String PROPERTY04_NAME = "valueC"; + private static final String PROPERTY_VALUE = "test1"; + + @Test + public void testBug53421() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + Bean bean = new Bean(); + + ValueExpression varBean = factory.createValueExpression(bean, Bean.class); + context.getVariableMapper().setVariable("bean", varBean); + + + ValueExpression ve = factory.createValueExpression(context, "${bean.valueA}", String.class); + Exception e = null; + try { + ve.getValue(context); + } catch (PropertyNotFoundException pnfe) { + e = pnfe; + } + assertThat("Wrong exception type", e, instanceOf(PropertyNotFoundException.class)); + String type = Bean.class.getName(); + @SuppressWarnings("null") // Not possible due to test above + String msg = e.getMessage(); + Assert.assertTrue("No reference to type [" + type + "] where property cannot be found in [" + msg + "]", + msg.contains(type)); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetType01() { + BeanELResolver resolver = new BeanELResolver(); + resolver.getType(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is null. + */ + @Test + public void testGetType02() { + doNegativeTest(null, new Object(), MethodUnderTest.GET_TYPE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetType03() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Class result = resolver.getType(context, new Bean(), PROPERTY01_NAME); + + // Property is read-only so should return null + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that an exception will be thrown when the property does not exist. + */ + @Test(expected = PropertyNotFoundException.class) + public void testGetType04() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.getType(context, new Bean(), PROPERTY02_NAME); + } + + /** + * Tests that an exception will be thrown when a coercion cannot be performed. + */ + @Test(expected = PropertyNotFoundException.class) + public void testGetType05() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.getType(context, new Bean(), new Object()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetValue01() { + BeanELResolver resolver = new BeanELResolver(); + resolver.getValue(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is null. + */ + @Test + public void testGetValue02() { + doNegativeTest(null, new Object(), MethodUnderTest.GET_VALUE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetValue03() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getValue(context, new TesterBean(BEAN_NAME), PROPERTY03_NAME); + + Assert.assertEquals(BEAN_NAME, result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that an exception will be thrown when the property does not exist. + */ + @Test(expected = PropertyNotFoundException.class) + public void testGetValue04() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.getValue(context, new Bean(), PROPERTY02_NAME); + } + + /** + * Tests that an exception will be thrown when a coercion cannot be performed. + */ + @Test(expected = PropertyNotFoundException.class) + public void testGetValue05() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.getValue(context, new Bean(), new Object()); + } + + /** + * Tests that an exception will be thrown when the property is not readable. + */ + @Test(expected = PropertyNotFoundException.class) + public void testGetValue06() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.getValue(context, new Bean(), PROPERTY01_NAME); + } + + /** + * Tests that getter method throws exception which should be propagated. + */ + @Test(expected = ELException.class) + public void testGetValue07() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.getValue(context, new TesterBean(BEAN_NAME), PROPERTY01_NAME); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testSetValue01() { + BeanELResolver resolver = new BeanELResolver(); + resolver.setValue(null, new Object(), new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is null. + */ + @Test + public void testSetValue02() { + doNegativeTest(null, new Object(), MethodUnderTest.SET_VALUE, true); + } + + /** + * Tests that an exception is thrown when readOnly is true. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue03() { + BeanELResolver resolver = new BeanELResolver(true); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.setValue(context, new Bean(), new Object(), new Object()); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testSetValue04() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + TesterBean bean = new TesterBean(BEAN_NAME); + resolver.setValue(context, bean, PROPERTY03_NAME, PROPERTY_VALUE); + + Assert.assertEquals(PROPERTY_VALUE, resolver.getValue(context, bean, PROPERTY03_NAME)); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that an exception will be thrown when a coercion cannot be performed. + */ + @Test(expected = PropertyNotFoundException.class) + public void testSetValue05() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.setValue(context, new Bean(), new Object(), PROPERTY_VALUE); + } + + /** + * Tests that an exception will be thrown when the property does not exist. + */ + @Test(expected = PropertyNotFoundException.class) + public void testSetValue06() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.setValue(context, new Bean(), PROPERTY02_NAME, PROPERTY_VALUE); + } + + /** + * Tests that an exception will be thrown when the property does not have setter method. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue07() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.setValue(context, new TesterBean(BEAN_NAME), PROPERTY01_NAME, PROPERTY_VALUE); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testIsReadOnly01() { + BeanELResolver resolver = new BeanELResolver(); + resolver.isReadOnly(null, new Object(), new Object()); + } + + /** + * Tests that the propertyResolved is false if base is null. + */ + @Test + public void testIsReadOnly02() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.isReadOnly(context, null, new Object()); + + Assert.assertFalse(context.isPropertyResolved()); + + resolver = new BeanELResolver(true); + + resolver.isReadOnly(context, null, new Object()); + + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that if the BeanELResolver is constructed with readOnly the method will return always true. + */ + @Test + public void testIsReadOnly03() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, new TesterBean(BEAN_NAME), PROPERTY03_NAME); + + Assert.assertFalse(result); + Assert.assertTrue(context.isPropertyResolved()); + + resolver = new BeanELResolver(true); + + result = resolver.isReadOnly(context, new TesterBean(BEAN_NAME), PROPERTY03_NAME); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that an exception is thrown when a coercion cannot be performed. + */ + @Test(expected = PropertyNotFoundException.class) + public void testIsReadOnly04() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.isReadOnly(context, new TesterBean(BEAN_NAME), Integer.valueOf(0)); + } + + /** + * Tests that an exception will be thrown when the property does not exist. + */ + @Test(expected = PropertyNotFoundException.class) + public void testIsReadOnly05() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.isReadOnly(context, new Bean(), PROPERTY02_NAME); + } + + /** + * Tests that true will be returned when the property does not have setter method. + */ + @Test + public void testIsReadOnly06() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, new TesterBean(BEAN_NAME), PROPERTY01_NAME); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a valid FeatureDescriptors are not returned if base is not Map. + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + @Test + public void testGetFeatureDescriptors01() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Iterator result = resolver.getFeatureDescriptors(context, null); + + Assert.assertNull(result); + } + + /** + * Tests that a valid FeatureDescriptors are returned. + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + @Test + public void testGetFeatureDescriptors02() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Iterator result = resolver.getFeatureDescriptors(context, new Bean()); + + while (result.hasNext()) { + PropertyDescriptor featureDescriptor = (PropertyDescriptor) result.next(); + Assert.assertEquals(featureDescriptor.getPropertyType(), featureDescriptor.getValue(ELResolver.TYPE)); + Assert.assertEquals(Boolean.TRUE, featureDescriptor.getValue(ELResolver.RESOLVABLE_AT_DESIGN_TIME)); + } + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testInvoke01() { + BeanELResolver resolver = new BeanELResolver(); + resolver.invoke(null, new Object(), new Object(), new Class[0], new Object[0]); + } + + /** + * Tests that a valid property is not resolved if base is null. + */ + @Test + public void testInvoke02() { + doNegativeTest(null, new Object(), MethodUnderTest.INVOKE, true); + } + + /** + * Tests a method invocation. + */ + @Test + public void testInvoke03() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), METHOD01_NAME, new Class[] {}, + new Object[] {}); + + Assert.assertEquals(BEAN_NAME, result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that the method name cannot be coerced to String. + */ + @Test + public void testInvoke04() { + doNegativeTest(new Bean(), null, MethodUnderTest.INVOKE, true); + } + + /** + * Tests that a call to <init> as a method name will throw an exception. + */ + @Test(expected = MethodNotFoundException.class) + public void testInvoke05() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.invoke(context, new TesterBean(BEAN_NAME), METHOD02_NAME, new Class[] {}, new Object[] {}); + } + + /** + * Tests that a call to a non existing method will throw an exception. + */ + @Test(expected = MethodNotFoundException.class) + public void testInvoke06() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.invoke(context, new TesterBean(BEAN_NAME), METHOD03_NAME, new Class[] {}, new Object[] {}); + } + + @Test + public void testInvokeVarargsCoerce01() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", new Class[] {}, + new String[] {}); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce02() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", null, null); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce03() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", null, new String[] {}); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce04() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", new Class[] {}, null); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce05() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", new Class[] { null }, + new String[] { null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce06() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", null, + new String[] { null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce07() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", new Class[] { null }, + null); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce08() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class }, new String[] { "true" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce09() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class }, new Object[] { "true", null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce10() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String[].class }, new Object[] { "true", null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce11() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class }, new Object[] { "10" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce12() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String[].class }, new String[] { "10" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce13() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class }, new String[] { "10", "11" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce14() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class }, new String[] { "true", null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargsCoerce15() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class }, new Object[] { "true", new ArrayList<>() }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce16() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class, String.class }, new Object[] { "10", "11", "12" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargsCoerce17() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class }, new Object[] { "10", "11", "12" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargsCoerce18() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class, String.class, String.class }, + new Object[] { "10", "11", "12" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce19() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class, String.class, String.class }, + new Object[] { "true", "10", "11", "12" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargsCoerce20() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class, String.class }, new Object[] { "true", "10", "11", "12" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargsCoerce21() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class, String.class, String.class, String.class }, + new Object[] { "true", "10", "11", "12" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargsCoerce22() { + BeanELResolver resolver = new BeanELResolver(); + StandardELContext context = new StandardELContext(ELManager.getExpressionFactory()); + context.addELResolver(new StringToLongNeverFailResolver()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class, String.class }, new Object[] { "AA", "BB", "CC" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargsCoerce23() { + BeanELResolver resolver = new BeanELResolver(); + StandardELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { String.class, String.class, String.class }, new Object[] { "AA", "BB", "CC" }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs01() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", new Class[] {}, + new Object[] {}); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs02() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", null, null); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs03() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", null, new Object[] {}); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs04() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", new Class[] {}, null); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs05() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", new Class[] { null }, + new Object[] { null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs06() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", null, + new Object[] { null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs07() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", new Class[] { null }, + null); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs08() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Boolean.class }, new Object[] { Boolean.TRUE }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs09() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Boolean.class, Integer.class }, new Object[] { Boolean.TRUE, null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs10() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Boolean.class, Integer[].class }, new Object[] { Boolean.TRUE, null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs11() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Integer.class }, new Object[] { Integer.valueOf(10) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs12() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Integer[].class }, new Object[] { Integer.valueOf(10) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs13() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Integer.class, Integer.class }, + new Object[] { Integer.valueOf(10), Integer.valueOf(11) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + // Note: The coercion rules are that a null of any type can be coerced to a + // null of *any* other type so this works. + @Test + public void testInvokeVarargs14() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Boolean.class, ArrayList.class }, new Object[] { Boolean.TRUE, null }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargs15() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Boolean.class, ArrayList.class }, new Object[] { Boolean.TRUE, new ArrayList<>() }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs16() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Integer.class, Integer.class, Integer.class }, + new Object[] { Integer.valueOf(10), Integer.valueOf(11), Integer.valueOf(12) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargs17() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Integer.class, Integer.class }, + new Object[] { Integer.valueOf(10), Integer.valueOf(11), Integer.valueOf(12) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargs18() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Integer.class, Integer.class, Integer.class, Integer.class }, + new Object[] { Integer.valueOf(10), Integer.valueOf(11), Integer.valueOf(12) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test + public void testInvokeVarargs19() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Boolean.class, Integer.class, Integer.class, Integer.class }, + new Object[] { Boolean.TRUE, Integer.valueOf(10), Integer.valueOf(11), Integer.valueOf(12) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargs20() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Boolean.class, Integer.class, Integer.class }, + new Object[] { Boolean.TRUE, Integer.valueOf(10), Integer.valueOf(11), Integer.valueOf(12) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + @Test(expected = MethodNotFoundException.class) + public void testInvokeVarargs21() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new TesterBean(BEAN_NAME), "getNameVarargs", + new Class[] { Boolean.class, Integer.class, Integer.class, Integer.class, Integer.class }, + new Object[] { Boolean.TRUE, Integer.valueOf(10), Integer.valueOf(11), Integer.valueOf(12) }); + + Assert.assertEquals(BEAN_NAME, result); + } + + /** + * Tests that a valid property implemented by a default method is resolved. + */ + @Test + public void testGetDefaultValue() { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getValue(context, new Bean(), PROPERTY04_NAME); + + Assert.assertEquals("Default", result); + Assert.assertTrue(context.isPropertyResolved()); + } + + + private static class Bean implements MyInterface { + + @SuppressWarnings("unused") + public void setValueA(String valueA) { + // NOOP + } + } + + + public interface MyInterface { + default String getValueC() { + return "Default"; + } + } + + + private void doNegativeTest(Object base, Object trigger, MethodUnderTest method, boolean checkResult) { + BeanELResolver resolver = new BeanELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = null; + switch (method) { + case GET_VALUE: { + result = resolver.getValue(context, base, trigger); + break; + } + case SET_VALUE: { + resolver.setValue(context, base, trigger, new Object()); + break; + } + case GET_TYPE: { + result = resolver.getType(context, base, trigger); + break; + } + case INVOKE: { + result = resolver.invoke(context, base, trigger, new Class[0], new Object[0]); + break; + } + default: { + // Should never happen + Assert.fail("Missing case for method"); + } + } + + if (checkResult) { + Assert.assertNull(result); + } + Assert.assertFalse(context.isPropertyResolved()); + } + + private enum MethodUnderTest { + GET_VALUE, + SET_VALUE, + GET_TYPE, + INVOKE + } + + + /* + * Custom resolver that will always convert a string to an integer. If the provided string is not a valid integer, + * zero will be returned. + */ + private static class StringToLongNeverFailResolver extends ELResolver { + + @Override + public Object getValue(ELContext context, Object base, Object property) { + return null; + } + + @Override + public Class getType(ELContext context, Object base, Object property) { + return null; + } + + @Override + public void setValue(ELContext context, Object base, Object property, Object value) { + throw new PropertyNotWritableException(); + } + + @Override + public boolean isReadOnly(ELContext context, Object base, Object property) { + return true; + } + + @Override + public Class getCommonPropertyType(ELContext context, Object base) { + return null; + } + + @Override + public T convertToType(ELContext context, Object obj, Class type) { + if (Integer.class.equals(type) && obj instanceof String) { + context.setPropertyResolved(true); + Integer result; + try { + result = Integer.valueOf((String) obj); + } catch (NumberFormatException e) { + result = Integer.valueOf(0); + } + @SuppressWarnings("unchecked") + T t = (T) result; + return t; + } + return super.convertToType(context, obj, type); + } + } +} diff --git a/test/jakarta/el/TestBeanELResolverVarargsInvocation.java b/test/jakarta/el/TestBeanELResolverVarargsInvocation.java new file mode 100644 index 0000000..89f913f --- /dev/null +++ b/test/jakarta/el/TestBeanELResolverVarargsInvocation.java @@ -0,0 +1,118 @@ +/* + * 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 jakarta.el; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TestBeanELResolverVarargsInvocation { + public static class Foo { + + public String joinDelimited(String delim, String... strings) { + StringBuilder result = new StringBuilder(); + if (strings != null) { + for (String s : strings) { + if (delim != null && result.length() > 0) { + result.append(delim); + } + result.append(s); + } + } + return result.toString(); + } + + public String join(String... strings) { + return joinDelimited(null, strings); + } + } + + private Foo foo; + private ELContext elContext; + private BeanELResolver beanELResolver; + + @Before + public void setup() { + foo = new Foo(); + beanELResolver = new BeanELResolver(); + elContext = new ELContext() { + private VariableMapper variableMapper = new VariableMapper() { + private Map vars = new HashMap<>(); + + @Override + public ValueExpression setVariable(String arg0, ValueExpression arg1) { + if (arg1 == null) { + return vars.remove(arg0); + } else { + return vars.put(arg0, arg1); + } + } + + @Override + public ValueExpression resolveVariable(String arg0) { + return vars.get(arg0); + } + }; + private FunctionMapper functionMapper = new FunctionMapper() { + + @Override + public Method resolveFunction(String arg0, String arg1) { + return null; + } + }; + + @Override + public VariableMapper getVariableMapper() { + return variableMapper; + } + + @Override + public FunctionMapper getFunctionMapper() { + return functionMapper; + } + + @Override + public ELResolver getELResolver() { + return beanELResolver; + } + }; + } + + /** + * Tests varargs that come after an opening argument. + */ + @Test + public void testJoinDelimited() { + Assert.assertEquals(foo.joinDelimited("-", "foo", "bar", "baz"), beanELResolver.invoke(elContext, foo, + "joinDelimited", null, new Object[] { "-", "foo", "bar", "baz" })); + } + + /** + * Tests varargs that constitute a method's only parameters, as well as bogus results due to improper matching of + * ANY vararg method, and depending on the order in which reflected methods are encountered. + */ + @Test + public void testJoin() { + Assert.assertEquals(foo.join("foo", "bar", "baz"), + beanELResolver.invoke(elContext, foo, "join", null, new Object[] { "foo", "bar", "baz" })); + } + +} \ No newline at end of file diff --git a/test/jakarta/el/TestBeanNameELResolver.java b/test/jakarta/el/TestBeanNameELResolver.java new file mode 100644 index 0000000..1e4e286 --- /dev/null +++ b/test/jakarta/el/TestBeanNameELResolver.java @@ -0,0 +1,586 @@ +/* + * 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 jakarta.el; + +import org.junit.Assert; +import org.junit.Test; + +public class TestBeanNameELResolver { + + private static final String BEAN01_NAME = "bean01"; + private static final TesterBean BEAN01 = new TesterBean(BEAN01_NAME); + private static final String BEAN02_NAME = "bean02"; + private static final TesterBean BEAN02 = new TesterBean(BEAN02_NAME); + private static final String BEAN99_NAME = "bean99"; + private static final TesterBean BEAN99 = new TesterBean(BEAN99_NAME); + + /** + * Creates the resolver that is used for the test. All the tests use a resolver with the same configuration. + */ + private BeanNameELResolver createBeanNameELResolver() { + return createBeanNameELResolver(true); + } + + private BeanNameELResolver createBeanNameELResolver(boolean allowCreate) { + + TesterBeanNameResolver beanNameResolver = new TesterBeanNameResolver(); + beanNameResolver.setBeanValue(BEAN01_NAME, BEAN01); + beanNameResolver.setBeanValue(BEAN02_NAME, BEAN02); + beanNameResolver.setAllowCreate(allowCreate); + + BeanNameELResolver beanNameELResolver = new BeanNameELResolver(beanNameResolver); + return beanNameELResolver; + } + + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetValue01() { + BeanNameELResolver resolver = createBeanNameELResolver(); + resolver.getValue(null, new Object(), new Object()); + } + + + /** + * Tests that a valid bean is resolved. + */ + @Test + public void testGetValue02() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getValue(context, null, BEAN01_NAME); + + Assert.assertEquals(BEAN01, result); + Assert.assertTrue(context.isPropertyResolved()); + } + + + /** + * Tests that a valid bean is not resolved if base is non-null. + */ + @Test + public void testGetValue03() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getValue(context, new Object(), BEAN01_NAME); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Tests that a valid bean is not resolved if property is not a String even if it can be coerced to a valid bean + * name. + */ + @Test + public void testGetValue04() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object property = new Object() { + @Override + public String toString() { + return BEAN01_NAME; + } + }; + + Object result = resolver.getValue(context, null, property); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Beans that don't exist shouldn't return anything + */ + @Test + public void testGetValue05() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getValue(context, null, BEAN99_NAME); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Exception during resolution should be wrapped and re-thrown. + */ + @Test + public void testGetValue06() { + doThrowableTest(TesterBeanNameResolver.EXCEPTION_TRIGGER_NAME, MethodUnderTest.GET_VALUE); + } + + + /** + * Throwable during resolution should be wrapped and re-thrown. + */ + @Test + public void testGetValue07() { + doThrowableTest(TesterBeanNameResolver.THROWABLE_TRIGGER_NAME, MethodUnderTest.GET_VALUE); + } + + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testSetValue01() { + BeanNameELResolver resolver = createBeanNameELResolver(); + resolver.setValue(null, new Object(), new Object(), new Object()); + } + + + /** + * Test replace with create enabled. + */ + @Test + public void testSetValue02() { + doSetValueCreateReplaceTest(true, BEAN01_NAME); + } + + + /** + * Test replace with create disabled. + */ + @Test + public void testSetValue03() { + doSetValueCreateReplaceTest(false, BEAN01_NAME); + } + + + /** + * Test create with create enabled. + */ + @Test + public void testSetValue04() { + doSetValueCreateReplaceTest(true, BEAN99_NAME); + } + + + /** + * Test create with create disabled. + */ + @Test + public void testSetValue05() { + doSetValueCreateReplaceTest(false, BEAN99_NAME); + } + + + /** + * Test replacing a read-only bean with create enabled. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue06() { + doSetValueCreateReplaceTest(true, TesterBeanNameResolver.READ_ONLY_NAME); + } + + + /** + * Test replacing a read-only bean with create disable. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue07() { + doSetValueCreateReplaceTest(false, TesterBeanNameResolver.READ_ONLY_NAME); + } + + + /** + * Exception during resolution should be wrapped and re-thrown. + */ + @Test + public void testSetValue08() { + doThrowableTest(TesterBeanNameResolver.EXCEPTION_TRIGGER_NAME, MethodUnderTest.SET_VALUE); + } + + + /** + * Throwable during resolution should be wrapped and re-thrown. + */ + @Test + public void testSetValue09() { + doThrowableTest(TesterBeanNameResolver.THROWABLE_TRIGGER_NAME, MethodUnderTest.SET_VALUE); + } + + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetType01() { + BeanNameELResolver resolver = createBeanNameELResolver(); + resolver.getType(null, new Object(), new Object()); + } + + + /** + * Tests that a valid bean is resolved. + */ + @Test + public void testGetType02() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Class result = resolver.getType(context, null, BEAN01_NAME); + + Assert.assertEquals(BEAN01.getClass(), result); + Assert.assertTrue(context.isPropertyResolved()); + } + + + /** + * Tests that a valid bean is not resolved if base is non-null. + */ + @Test + public void testGetType03() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Class result = resolver.getType(context, new Object(), BEAN01_NAME); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Tests that a valid bean is not resolved if property is not a String even if it can be coerced to a valid bean + * name. + */ + @Test + public void testGetType04() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object property = new Object() { + @Override + public String toString() { + return BEAN01_NAME; + } + }; + + Class result = resolver.getType(context, null, property); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Beans that don't exist shouldn't return anything + */ + @Test + public void testGetType05() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Class result = resolver.getType(context, null, BEAN99_NAME); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Exception during resolution should be wrapped and re-thrown. + */ + @Test + public void testGetType06() { + doThrowableTest(TesterBeanNameResolver.EXCEPTION_TRIGGER_NAME, MethodUnderTest.GET_TYPE); + } + + + /** + * Throwable during resolution should be wrapped and re-thrown. + */ + @Test + public void testGetType07() { + doThrowableTest(TesterBeanNameResolver.THROWABLE_TRIGGER_NAME, MethodUnderTest.GET_TYPE); + } + + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testIsReadOnly01() { + BeanNameELResolver resolver = createBeanNameELResolver(); + resolver.isReadOnly(null, new Object(), new Object()); + } + + + /** + * Tests that a writable bean is reported as writable. + */ + @Test + public void testIsReadOnly02() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, null, BEAN01_NAME); + + Assert.assertFalse(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + + /** + * Tests that a read-only bean is reported as not writable. + */ + @Test + public void testIsReadOnly03() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, null, TesterBeanNameResolver.READ_ONLY_NAME); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + + /** + * Tests that a valid bean is not resolved if base is non-null. + */ + @Test + public void testIsReadOnly04() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.isReadOnly(context, new Object(), BEAN01_NAME); + + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Tests that a valid bean is not resolved if property is not a String even if it can be coerced to a valid bean + * name. + */ + @Test + public void testIsReadOnly05() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object property = new Object() { + @Override + public String toString() { + return BEAN01_NAME; + } + }; + + resolver.isReadOnly(context, null, property); + + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Beans that don't exist should not resolve + */ + @Test + public void testIsReadOnly06() { + + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.isReadOnly(context, null, BEAN99_NAME); + + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Exception during resolution should be wrapped and re-thrown. + */ + @Test + public void testIsReadOnly07() { + doThrowableTest(TesterBeanNameResolver.EXCEPTION_TRIGGER_NAME, MethodUnderTest.IS_READ_ONLY); + } + + + /** + * Throwable during resolution should be wrapped and re-thrown. + */ + @Test + public void testIsReadOnly08() { + doThrowableTest(TesterBeanNameResolver.THROWABLE_TRIGGER_NAME, MethodUnderTest.IS_READ_ONLY); + } + + + /** + * Confirm it returns null for 'valid' input. + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + public void testGetFeatureDescriptors01() { + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getFeatureDescriptors(context, null); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Confirm it returns null for invalid input. + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + public void testGetFeatureDescriptors02() { + BeanNameELResolver resolver = createBeanNameELResolver(); + + Object result = resolver.getFeatureDescriptors(null, new Object()); + + Assert.assertNull(result); + } + + + /** + * Confirm it returns String.class for 'valid' input. + */ + public void testGetCommonPropertyType01() { + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getCommonPropertyType(context, null); + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + + /** + * Confirm it returns String.class for invalid input. + */ + public void testGetCommonPropertyType02() { + BeanNameELResolver resolver = createBeanNameELResolver(); + + Object result = resolver.getCommonPropertyType(null, new Object()); + + Assert.assertNull(result); + } + + + private void doThrowableTest(String trigger, MethodUnderTest method) { + BeanNameELResolver resolver = createBeanNameELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + ELException elException = null; + try { + switch (method) { + case GET_VALUE: { + resolver.getValue(context, null, trigger); + break; + } + case SET_VALUE: { + resolver.setValue(context, null, trigger, new Object()); + break; + } + case GET_TYPE: { + resolver.getType(context, null, trigger); + break; + } + case IS_READ_ONLY: { + resolver.isReadOnly(context, null, trigger); + break; + } + default: { + // Should never happen + Assert.fail("Missing case for method"); + } + } + + } catch (ELException e) { + elException = e; + } + + Assert.assertFalse(context.isPropertyResolved()); + Assert.assertNotNull(elException); + + Throwable cause = elException.getCause(); + Assert.assertNotNull(cause); + } + + + /** + * Tests adding/replacing beans beans + */ + private void doSetValueCreateReplaceTest(boolean canCreate, String beanName) { + BeanNameELResolver resolver = createBeanNameELResolver(canCreate); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + // Get bean one to be sure it has been replaced when testing replace + Object bean = resolver.getValue(context, null, BEAN01_NAME); + + Assert.assertTrue(context.isPropertyResolved()); + Assert.assertEquals(BEAN01, bean); + + // Reset context + context.setPropertyResolved(false); + + // Replace BEAN01 + resolver.setValue(context, null, beanName, BEAN99); + if (canCreate || BEAN01_NAME.equals(beanName)) { + Assert.assertTrue(context.isPropertyResolved()); + + // Obtain BEAN01 again + context.setPropertyResolved(false); + bean = resolver.getValue(context, null, beanName); + + Assert.assertTrue(context.isPropertyResolved()); + Assert.assertEquals(BEAN99, bean); + } else { + Assert.assertFalse(context.isPropertyResolved()); + + // Obtain BEAN01 again + context.setPropertyResolved(false); + bean = resolver.getValue(context, null, BEAN01_NAME); + + Assert.assertTrue(context.isPropertyResolved()); + Assert.assertEquals(BEAN01, bean); + } + } + + + private enum MethodUnderTest { + GET_VALUE, + SET_VALUE, + GET_TYPE, + IS_READ_ONLY + } +} diff --git a/test/jakarta/el/TestCompositeELResolver.java b/test/jakarta/el/TestCompositeELResolver.java new file mode 100644 index 0000000..eb6c582 --- /dev/null +++ b/test/jakarta/el/TestCompositeELResolver.java @@ -0,0 +1,37 @@ +/* + * 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 jakarta.el; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestCompositeELResolver extends TomcatBaseTest { + + @Test + public void testBug50408() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug50408.jsp", new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } +} diff --git a/test/jakarta/el/TestELContext.java b/test/jakarta/el/TestELContext.java new file mode 100644 index 0000000..a66638d --- /dev/null +++ b/test/jakarta/el/TestELContext.java @@ -0,0 +1,169 @@ +/* + * 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 jakarta.el; + +import java.util.List; + +import jakarta.el.TesterEvaluationListener.Pair; + +import org.junit.Assert; +import org.junit.Test; + +public class TestELContext { + + /** + * Tests that a null key results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetContext() { + ELContext elContext = new TesterELContext(); + elContext.getContext(null); + } + + /** + * Tests that a null key results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testPutContext01() { + ELContext elContext = new TesterELContext(); + elContext.putContext(null, new Object()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testPutContext02() { + ELContext elContext = new TesterELContext(); + elContext.putContext(Object.class, null); + } + + /** + * Tests that the context object will be added to the map with context objects. The key is used as unique identifier + * of the context object in the map. + */ + @Test + public void testPutContext03() { + ELContext elContext = new TesterELContext(); + Assert.assertNull(elContext.getContext(String.class)); + elContext.putContext(String.class, "test"); + Assert.assertEquals("test", elContext.getContext(String.class)); + elContext.putContext(String.class, "test1"); + Assert.assertEquals("test1", elContext.getContext(String.class)); + } + + /** + * Tests that propertyResolved will be set to true and the corresponding listeners will be notified. + */ + @Test + public void testSetPropertyResolved() { + ELContext elContext = new TesterELContext(); + + TesterEvaluationListener listener = new TesterEvaluationListener(); + elContext.addEvaluationListener(listener); + + TesterBean bean = new TesterBean("test"); + + elContext.setPropertyResolved(bean, "name"); + + Assert.assertTrue(elContext.isPropertyResolved()); + + List events = listener.getResolvedProperties(); + Assert.assertEquals(1, events.size()); + Pair p = events.get(0); + Assert.assertEquals(bean, p.getBase()); + Assert.assertEquals("name", p.getProperty()); + } + + /** + * Tests that the corresponding listeners will be notified. + */ + @Test + public void testNotifyBeforeEvaluation() { + ELContext elContext = new TesterELContext(); + + TesterEvaluationListener listener = new TesterEvaluationListener(); + elContext.addEvaluationListener(listener); + + elContext.notifyBeforeEvaluation("before"); + + List events = listener.getBeforeEvaluationExpressions(); + Assert.assertEquals(1, events.size()); + Assert.assertEquals("before", events.get(0)); + } + + /** + * Tests that the corresponding listeners will be notified. + */ + @Test + public void testNotifyAfterEvaluation() { + ELContext elContext = new TesterELContext(); + + TesterEvaluationListener listener = new TesterEvaluationListener(); + elContext.addEvaluationListener(listener); + + elContext.notifyAfterEvaluation("after"); + + List events = listener.getAfterEvaluationExpressions(); + Assert.assertEquals(1, events.size()); + Assert.assertEquals("after", events.get(0)); + } + + /** + * Tests not compatible object and type. + */ + @Test(expected = ELException.class) + public void testConvertToType01() { + ELContext elContext = new TesterELContext(); + elContext.convertToType("test", Integer.class); + } + + /** + * Tests that if there is no ELResolver the standard coercions will be invoked. + */ + @Test + public void testConvertToType02() { + ELContext elContext = new TesterELContext(); + boolean originalPropertyResolved = false; + elContext.setPropertyResolved(originalPropertyResolved); + + Object result = elContext.convertToType("test", String.class); + Assert.assertEquals("test", result); + + Assert.assertTrue(originalPropertyResolved == elContext.isPropertyResolved()); + } + + /** + * Tests that if there is ELResolver it will handle the conversion. If this resolver cannot return a result the + * standard coercions will be invoked. + */ + @Test + public void testConvertToType03() { + ELContext elContext = new TesterELContext(new TesterELResolverOne()); + + boolean originalPropertyResolved = false; + elContext.setPropertyResolved(originalPropertyResolved); + + Object result = elContext.convertToType("1", String.class); + Assert.assertEquals("ONE", result); + Assert.assertTrue(originalPropertyResolved == elContext.isPropertyResolved()); + + result = elContext.convertToType("test", String.class); + Assert.assertEquals("test", result); + Assert.assertTrue(originalPropertyResolved == elContext.isPropertyResolved()); + } +} diff --git a/test/jakarta/el/TestELProcessor.java b/test/jakarta/el/TestELProcessor.java new file mode 100644 index 0000000..1dd04c0 --- /dev/null +++ b/test/jakarta/el/TestELProcessor.java @@ -0,0 +1,207 @@ +/* + * 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 jakarta.el; + +import org.junit.Assert; +import org.junit.Test; + +public class TestELProcessor { + + @Test + public void testDefineBean01() { + ELProcessor elp = new ELProcessor(); + elp.defineBean("bean01", new TesterBean("name01")); + Assert.assertEquals("name01", elp.eval("bean01.name")); + } + + + @Test(expected = ELException.class) + public void testEval01() { + ELProcessor elp = new ELProcessor(); + elp.eval("${1+1}"); + } + + + @Test(expected = ELException.class) + public void testEval02() { + ELProcessor elp = new ELProcessor(); + elp.eval("#{1+1}"); + } + + + @Test + public void testEval03() { + ELProcessor elp = new ELProcessor(); + // Note \ is escaped as \\ in Java source code + String result = elp.eval("'\\\\'"); + Assert.assertEquals("\\", result); + } + + + @Test + public void testDefineFunctionMethod01() throws Exception { + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "toBoolean", Boolean.class.getMethod("valueOf", String.class)); + Assert.assertEquals(Boolean.valueOf(true), elp.eval("fn:toBoolean(true)")); + } + + + @Test + public void testDefineFunctionName01() throws Exception { + ELProcessor elp = new ELProcessor(); + // java.lang should be automatically imported so no need for full class + // name + elp.defineFunction("fn", "toBoolean", "Boolean", "valueOf"); + Assert.assertEquals(Boolean.valueOf(true), elp.eval("fn:toBoolean(true)")); + } + + + @Test + public void testDefineFunctionName02() throws Exception { + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "test", "java.lang.Integer", "Integer valueOf(int)"); + Assert.assertEquals(Integer.valueOf(1), elp.eval("fn:test(1)")); + } + + + @Test + public void testDefineFunctionName03() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "test", "jakarta.el.TesterFunctions", "void doIt()"); + elp.eval("fn:test()"); + Assert.assertEquals("A", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName04() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "test", "jakarta.el.TesterFunctions", "void doIt(int)"); + elp.eval("fn:test(5)"); + Assert.assertEquals("B", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName05() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "test", "jakarta.el.TesterFunctions", "void doIt(Integer)"); + elp.eval("fn:test(null)"); + Assert.assertEquals("C", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName06() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "jakarta.el.TesterFunctions", "void doIt(int)"); + elp.eval("doIt(5)"); + Assert.assertEquals("B", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName07() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "", "jakarta.el.TesterFunctions", "void doIt(int)"); + elp.eval("fn:doIt(5)"); + Assert.assertEquals("B", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName08() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "", "jakarta.el.TesterFunctions", "void doIt(int[])"); + elp.eval("fn:doIt([5].stream().toArray())"); + Assert.assertEquals("D", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName09() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "", "jakarta.el.TesterFunctions", "void doIt(int[][])"); + elp.eval("fn:doIt([[5].stream().toArray()].stream().toArray())"); + Assert.assertEquals("E", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName10() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "test1", "java.lang.Integer", "Integer valueOf(int)"); + elp.defineFunction("fn", "test2", "jakarta.el.TesterFunctions", "void doIt(Integer[])"); + elp.eval("fn:test2([fn:test1(1), fn:test1(2)].stream().toArray())"); + Assert.assertEquals("F", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName11() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "test1", "java.lang.Integer", "Integer valueOf(int)"); + elp.defineFunction("fn", "test2", "jakarta.el.TesterFunctions", "void doIt(Integer[][])"); + elp.eval("fn:test2([[fn:test1(1), fn:test1(2)].stream().toArray()].stream().toArray())"); + Assert.assertEquals("G", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName12() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "test", "jakarta.el.TesterFunctions", "void doIt(long...)"); + elp.eval("fn:test(1,2)"); + Assert.assertEquals("H", TesterFunctions.getCallList()); + } + + + @Test + public void testDefineFunctionName13() throws Exception { + TesterFunctions.resetCallList(); + ELProcessor elp = new ELProcessor(); + elp.defineFunction("fn", "test", "jakarta.el.TesterFunctions", "void doIt(Object...)"); + elp.eval("fn:test(null, null)"); + Assert.assertEquals("I", TesterFunctions.getCallList()); + } + + + @Test + public void testPrimitiveArray01() { + ELProcessor elp = new ELProcessor(); + TesterBean bean01 = new TesterBean("bean01"); + elp.defineBean("bean01", bean01); + elp.defineBean("bean02", new TesterBean("bean02")); + + Integer[] result = elp.eval("bean02.setValueC(bean01.valueB);bean02.valueC"); + + Assert.assertEquals(bean01.getValueB().length, result.length); + for (int i = 0; i < result.length; i++) { + Assert.assertEquals(bean01.getValueB()[i], result[i].intValue()); + } + } +} diff --git a/test/jakarta/el/TestELResolver.java b/test/jakarta/el/TestELResolver.java new file mode 100644 index 0000000..4b83910 --- /dev/null +++ b/test/jakarta/el/TestELResolver.java @@ -0,0 +1,129 @@ +/* + * 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 jakarta.el; + +import org.junit.Assert; +import org.junit.Test; + +public class TestELResolver { + + @Test + public void testConvertToType01() { + ELContext context = new TesterELContext(); + + ValueExpression ve = ELManager.getExpressionFactory().createValueExpression(context, "1", String.class); + + String result = (String) ve.getValue(context); + + Assert.assertEquals("1", result); + } + + + @Test + public void testConvertToType02() { + ELContext context = new TesterELContext(new TesterELResolverOne()); + + ValueExpression ve = ELManager.getExpressionFactory().createValueExpression(context, "1", String.class); + + String result = (String) ve.getValue(context); + + Assert.assertEquals("ONE", result); + } + + + @Test + public void testConvertToType03() { + ELContext context = new TesterELContext(new TesterELResolverOne()); + + ValueExpression ve = ELManager.getExpressionFactory().createValueExpression(context, "2", String.class); + + String result = (String) ve.getValue(context); + + Assert.assertEquals("2", result); + } + + + @Test + public void testConvertToType04() { + CompositeELResolver resolver = new CompositeELResolver(); + ELContext context = new TesterELContext(resolver); + + ValueExpression ve = ELManager.getExpressionFactory().createValueExpression(context, "2", String.class); + + String result = (String) ve.getValue(context); + + Assert.assertEquals("2", result); + } + + + @Test + public void testConvertToType05() { + CompositeELResolver resolver = new CompositeELResolver(); + resolver.add(new TesterELResolverOne()); + resolver.add(new TesterELResolverTwo()); + ELContext context = new TesterELContext(resolver); + + ValueExpression ve = ELManager.getExpressionFactory().createValueExpression(context, "1", String.class); + + String result = (String) ve.getValue(context); + + Assert.assertEquals("ONE", result); + } + + + @Test + public void testConvertToType06() { + CompositeELResolver resolver = new CompositeELResolver(); + resolver.add(new TesterELResolverOne()); + resolver.add(new TesterELResolverTwo()); + ELContext context = new TesterELContext(resolver); + + ValueExpression ve = ELManager.getExpressionFactory().createValueExpression(context, "2", String.class); + + String result = (String) ve.getValue(context); + + Assert.assertEquals("TWO", result); + } + + + @Test + public void testConvertToType07() { + CompositeELResolver resolver = new CompositeELResolver(); + resolver.add(new TesterELResolverOne()); + resolver.add(new TesterELResolverTwo()); + ELContext context = new TesterELContext(resolver); + + ValueExpression ve = ELManager.getExpressionFactory().createValueExpression(context, "3", String.class); + + String result = (String) ve.getValue(context); + + Assert.assertEquals("3", result); + } + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=57802 + @Test + public void testDefaultConvertToType() { + ELContext context = new TesterELContext(new StaticFieldELResolver()); + + ValueExpression ve = ELManager.getExpressionFactory().createValueExpression(context, "${!Boolean.FALSE}", + Boolean.class); + + Boolean result = (Boolean) ve.getValue(context); + + Assert.assertEquals(Boolean.TRUE, result); + } +} diff --git a/test/jakarta/el/TestEvaluationListener.java b/test/jakarta/el/TestEvaluationListener.java new file mode 100644 index 0000000..ef0acc1 --- /dev/null +++ b/test/jakarta/el/TestEvaluationListener.java @@ -0,0 +1,132 @@ +/* + * 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 jakarta.el; + +import java.util.List; + +import jakarta.el.TesterEvaluationListener.Pair; + +import org.junit.Assert; +import org.junit.Test; + +public class TestEvaluationListener { + + + @Test + public void testPropertyResolved01() { + ELContext context = new TesterELContext(); + ELResolver resolver = new BeanELResolver(); + TesterBean bean = new TesterBean("test"); + TesterEvaluationListener listener = new TesterEvaluationListener(); + + context.addEvaluationListener(listener); + + Object result = resolver.getValue(context, bean, "name"); + + Assert.assertTrue(context.isPropertyResolved()); + Assert.assertEquals("test", result); + List events = listener.getResolvedProperties(); + + Assert.assertEquals(1, events.size()); + Pair p = events.get(0); + Assert.assertEquals(bean, p.getBase()); + Assert.assertEquals("name", p.getProperty()); + } + + + @Test + public void testPropertyResolved02() { + ELContext context = new TesterELContext(); + ELResolver resolver = new BeanELResolver(); + TesterBean bean = new TesterBean("test"); + TesterEvaluationListener listener = new TesterEvaluationListener(); + + context.addEvaluationListener(listener); + + Exception exception = null; + try { + resolver.getValue(context, bean, "foo"); + } catch (PropertyNotFoundException e) { + exception = e; + } + + Assert.assertNotNull(exception); + + // Still expect the property to be resolved and the listener to fire + // since the vent is at the time of resolution. The EL spec could be a + // lot clear on this. + Assert.assertTrue(context.isPropertyResolved()); + List events = listener.getResolvedProperties(); + + Assert.assertEquals(1, events.size()); + Pair p = events.get(0); + Assert.assertEquals(bean, p.getBase()); + Assert.assertEquals("foo", p.getProperty()); + } + + + @Test + public void testEvaluation01() { + ExpressionFactory factory = ELManager.getExpressionFactory(); + ELContext context = new TesterELContext(); + String expression = "${1 + 1}"; + ValueExpression ve = factory.createValueExpression(context, expression, int.class); + + TesterEvaluationListener listener = new TesterEvaluationListener(); + context.addEvaluationListener(listener); + + Object result = ve.getValue(context); + + // Check the result + Assert.assertEquals(Integer.valueOf(2), result); + + List before = listener.getBeforeEvaluationExpressions(); + Assert.assertEquals(1, before.size()); + Assert.assertEquals(expression, before.get(0)); + + List after = listener.getAfterEvaluationExpressions(); + Assert.assertEquals(1, after.size()); + Assert.assertEquals(expression, after.get(0)); + } + + + @Test + public void testEvaluation02() { + ExpressionFactory factory = ELManager.getExpressionFactory(); + ELContext context = new TesterELContext(new CompositeELResolver()); + String expression = "${foo.bar + 1}"; + ValueExpression ve = factory.createValueExpression(context, expression, int.class); + + TesterEvaluationListener listener = new TesterEvaluationListener(); + context.addEvaluationListener(listener); + + Exception e = null; + try { + ve.getValue(context); + } catch (PropertyNotFoundException pnfe) { + e = pnfe; + } + Assert.assertNotNull(e); + + List before = listener.getBeforeEvaluationExpressions(); + Assert.assertEquals(1, before.size()); + Assert.assertEquals(expression, before.get(0)); + + List after = listener.getAfterEvaluationExpressions(); + Assert.assertEquals(0, after.size()); + } +} diff --git a/test/jakarta/el/TestImportHandler.java b/test/jakarta/el/TestImportHandler.java new file mode 100644 index 0000000..db4bbf5 --- /dev/null +++ b/test/jakarta/el/TestImportHandler.java @@ -0,0 +1,268 @@ +/* + * 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 jakarta.el; + +import java.util.ArrayList; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.res.StringManager; + +public class TestImportHandler { + + /** + * java.lang should be imported by default + */ + @Test + public void testResolveClass01() { + ImportHandler handler = new ImportHandler(); + + Class result = handler.resolveClass("String"); + + Assert.assertEquals(String.class, result); + } + + + /** + * Resolve an unknown class + */ + @Test + public void testResolveClass02() { + ImportHandler handler = new ImportHandler(); + + Class result = handler.resolveClass("Foo"); + + Assert.assertNull(result); + } + + + /** + * Conflict on resolution. + */ + @Test + public void testResolveClass03() { + ImportHandler handler = new ImportHandler(); + + handler.importPackage("org.apache.tomcat.util"); + handler.importPackage("org.apache.jasper.runtime"); + + for (int i = 1; i <= 3; i++) { + try { + Class clazz = handler.resolveClass("ExceptionUtils"); + Assert.fail("Expected ELException but got [" + clazz.getName() + "] on iteration " + i); + } catch (ELException ex) { + // Expected + } + } + } + + + /** + * Multiple package imports with a single match. https://bz.apache.org/bugzilla/show_bug.cgi?id=57113 + */ + @Test + public void testResolveClass04() { + ImportHandler handler = new ImportHandler(); + + handler.importPackage("java.util"); + handler.importPackage("java.net"); + + Class clazz = handler.resolveClass("ArrayList"); + + Assert.assertEquals(ArrayList.class, clazz); + } + + + /** + * Attempting to resolve something that isn't a simple class name + * https://bz.apache.org/bugzilla/show_bug.cgi?id=57132 + */ + @Test + public void testResolveClass05() { + ImportHandler handler = new ImportHandler(); + + handler.importPackage("java.nio"); + + Class clazz = handler.resolveClass("charset.StandardCharsets"); + + Assert.assertNull(clazz); + } + + /** + * Attempting to resolve something that isn't a simple class name + * https://bz.apache.org/bugzilla/show_bug.cgi?id=57132 + */ + @Test + public void testResolveClass06() { + ImportHandler handler = new ImportHandler(); + + handler.importPackage("java.nio"); + + Class clazz = handler.resolveClass(null); + + Assert.assertNull(clazz); + } + + /** + * Import a valid class. + */ + @Test + public void testImportClass01() { + ImportHandler handler = new ImportHandler(); + + handler.importClass("org.apache.tomcat.util.res.StringManager"); + + Class result = handler.resolveClass("StringManager"); + + Assert.assertEquals(StringManager.class, result); + } + + + /** + * Import an invalid class. + */ + @Test + public void testImportClass02() { + ImportHandler handler = new ImportHandler(); + handler.importClass("org.apache.tomcat.util.res.StringManagerX"); + Class result = handler.resolveClass("StringManagerX"); + Assert.assertNull(result); + } + + + /** + * Import conflicting classes + */ + @Test + public void testImportClass03() { + ImportHandler handler = new ImportHandler(); + + handler.importClass("org.apache.tomcat.util.ExceptionUtils"); + for (int i = 1; i <= 3; i++) { + try { + handler.importClass("org.apache.jasper.util.ExceptionUtils"); + Assert.fail("Expected ELException but got none on iteration " + i); + } catch (ELException ex) { + // Expected + } + } + } + + + /** + * Import duplicate classes (i.e. the same class twice). + */ + @Test + public void testImportClass04() { + ImportHandler handler = new ImportHandler(); + + handler.importClass("org.apache.tomcat.util.res.StringManager"); + handler.importClass("org.apache.tomcat.util.res.StringManager"); + + Class result = handler.resolveClass("StringManager"); + + Assert.assertEquals(StringManager.class, result); + } + + + /** + * Import an invalid package. + */ + @Test + public void testImportPackage01_57574() { + ImportHandler handler = new ImportHandler(); + + handler.importPackage("org.apache.tomcat.foo"); + + // No exception is expected + } + + + /** + * Import a valid static field. + */ + @Test + public void testImportStatic01() { + ImportHandler handler = new ImportHandler(); + + handler.importStatic("org.apache.tomcat.util.scan.Constants.Package"); + + Class result = handler.resolveStatic("Package"); + + Assert.assertEquals(org.apache.tomcat.util.scan.Constants.class, result); + } + + + /** + * Import an invalid static field - does not exist. + */ + @Test(expected = ELException.class) + public void testImportStatic02() { + ImportHandler handler = new ImportHandler(); + + handler.importStatic("org.apache.tomcat.util.buf.Constants.PackageXX"); + } + + + /** + * Import an invalid static field - non-public. + */ + @Test + public void testImportStatic03() { + ImportHandler handler = new ImportHandler(); + + handler.importStatic("org.apache.tomcat.util.buf.Ascii.toLower"); + + Class result = handler.resolveStatic("toLower"); + + Assert.assertEquals(org.apache.tomcat.util.buf.Ascii.class, result); + } + + + /** + * Import an invalid static field - conflict. + */ + @Test + public void testImportStatic04() { + ImportHandler handler = new ImportHandler(); + + handler.importStatic("org.apache.tomcat.util.scan.Constants.Package"); + for (int i = 1; i <= 3; i++) { + try { + handler.importStatic("org.apache.tomcat.util.threads.Constants.Package"); + Assert.fail("Expected ELException but got none on iteration " + i); + } catch (ELException ex) { + // Expected + } + } + } + + + /** + * Package imports with conflicts due to non-public classes should not conflict. + */ + @Test + public void testBug57135() { + ImportHandler importHandler = new ImportHandler(); + + importHandler.importPackage("util.a"); + importHandler.importPackage("util.b"); + + importHandler.resolveClass("Foo"); + } +} diff --git a/test/jakarta/el/TestImportHandlerStandardPackages.java b/test/jakarta/el/TestImportHandlerStandardPackages.java new file mode 100644 index 0000000..b93b06e --- /dev/null +++ b/test/jakarta/el/TestImportHandlerStandardPackages.java @@ -0,0 +1,131 @@ +/* + * 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 jakarta.el; + +import java.io.File; +import java.lang.module.ModuleFinder; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URL; +import java.util.Enumeration; +import java.util.Map; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +public class TestImportHandlerStandardPackages { + + @Test + public void testClassListsAreComplete() throws Exception { + // Use reflection to get hold of the internal Map + Class clazz = ImportHandler.class; + Field f = clazz.getDeclaredField("standardPackages"); + f.setAccessible(true); + Object obj = f.get(null); + + @SuppressWarnings("unchecked") + Map> standardPackageName = (Map>) obj; + + for (Map.Entry> entry : standardPackageName.entrySet()) { + checkPackageClassList(entry.getKey(), entry.getValue()); + } + } + + + private void checkPackageClassList(String packageName, Set classNames) throws Exception { + + if ("java.lang".equals(packageName)) { + // The intention is that this test will catch new classes when the + // tests are run on a newer JRE. + // The latest version of the JRE where this test is known to pass is + // - OpenJDK 19 EA 22 + ModuleFinder.ofSystem().find("java.base").get().open().list().filter(c -> (c.startsWith("java/lang/"))) + .filter(c -> c.lastIndexOf('/') == 9) // Exclude sub-packages + .filter(c -> c.endsWith(".class")) // Exclude non-class resources + .map(c -> c.substring(10, c.length() - 6)) // Extract class name + .map(c -> { + try { + return Class.forName("java.lang." + c, false, + TesterImportHandlerPerformance.class.getClassLoader()); // Get the class object + } catch (ClassNotFoundException e) { + throw new RuntimeException(c); + } + }).filter(c -> null != c).filter(c -> Modifier.isPublic(c.getModifiers())) // Exclude non-public + // classes + .map(c -> c.getName().substring(10)) // Back to the class name + .map(c -> c.replace('$', '.')).filter(c -> !classNames.contains(c)) // Skip classes already listed + .filter(c -> !c.startsWith("FdLibm.")) // Skip public inner class + .filter(c -> !c.startsWith("LiveStackFrame.")) // Skip public inner class + .filter(c -> !c.startsWith("WeakPairMap.")) // Skip public inner class + .forEach(c -> Assert.fail("java.lang." + c)); // Should have in list + } else { + // When this test runs, the class loader will be loading resources + // from a directory for each of these packages. + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + String path = packageName.replace('.', '/'); + Enumeration resources = cl.getResources(path); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + URI uri = resource.toURI(); + // Gump includes some JARs on classpath - skip them + if (!"file".equals(uri.getScheme())) { + continue; + } + File dir = new File(uri); + + String[] files = dir.list(); + Assert.assertNotNull(files); + for (String file : files) { + if (!file.endsWith(".class")) { + // Skip non-class resources + continue; + } + if (file.startsWith("Test") || file.endsWith("BaseTest.class")) { + // Skip test resources + continue; + } + if (file.matches(".*\\$[0-9]?\\.class")) { + // Skip anonymous inner classes + continue; + } + String name = file.substring(0, file.length() - 6); + name = name.replace('$', '.'); + if (classNames.contains(name)) { + // Skip classes already known + continue; + } + File f = new File(dir, file); + if (!f.isFile()) { + // Skip directories + continue; + } + Class clazz = Class.forName(packageName + "." + name.replace(".", "$")); + if (!Modifier.isPublic(clazz.getModifiers())) { + // Skip non-public classes + continue; + } + + // There should be nothing left unless we missed something + Assert.fail(packageName + "." + name); + } + } + } + } +} diff --git a/test/jakarta/el/TestListELResolver.java b/test/jakarta/el/TestListELResolver.java new file mode 100644 index 0000000..f8fa5e6 --- /dev/null +++ b/test/jakarta/el/TestListELResolver.java @@ -0,0 +1,366 @@ +/* + * 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 jakarta.el; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class TestListELResolver { + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetValue01() { + ListELResolver resolver = new ListELResolver(); + resolver.getValue(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is not List. + */ + @Test + public void testGetValue02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.GET_VALUE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetValue03() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + Object result = resolver.getValue(context, list, Integer.valueOf(0)); + + Assert.assertEquals("key", result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests a coercion cannot be performed as the key is not integer. + */ + @Test(expected = IllegalArgumentException.class) + public void testGetValue04() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + resolver.getValue(context, list, "key"); + } + + /** + * Tests that the key is out of bounds and null will be returned. + */ + @Test + public void testGetValue05() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + Object result = resolver.getValue(context, list, Integer.valueOf(1)); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + + result = resolver.getValue(context, list, Integer.valueOf(-1)); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetType01() { + ListELResolver resolver = new ListELResolver(); + resolver.getType(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is not List. + */ + @Test + public void testGetType02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.GET_TYPE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetType03() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + Class result = resolver.getType(context, list, Integer.valueOf(0)); + + Assert.assertEquals(Object.class, result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that the key is out of bounds and exception will be thrown. + */ + @Test(expected = PropertyNotFoundException.class) + public void testGetType04() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + resolver.getType(context, list, Integer.valueOf(1)); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testSetValue01() { + ListELResolver resolver = new ListELResolver(); + resolver.setValue(null, new Object(), new Object(), new Object()); + } + + /** + * Tests that a valid property is not set if base is not List. + */ + @Test + public void testSetValue02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.SET_VALUE, false); + } + + /** + * Tests that an exception is thrown when readOnly is true. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue03() { + ListELResolver resolver = new ListELResolver(true); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.setValue(context, new ArrayList<>(), new Object(), new Object()); + } + + /** + * Tests that a valid property is set. + */ + @Test + public void testSetValue04() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("value"); + resolver.setValue(context, list, Integer.valueOf(0), "value"); + + Assert.assertEquals("value", resolver.getValue(context, list, Integer.valueOf(0))); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that an exception is thrown when the list is not modifiable. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue05() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = Collections.unmodifiableList(new ArrayList<>()); + resolver.setValue(context, list, Integer.valueOf(0), "value"); + } + + /** + * Tests a coercion cannot be performed as the key is not integer. + */ + @Test(expected = IllegalArgumentException.class) + public void testSetValue06() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + resolver.setValue(context, list, "key", "value"); + } + + /** + * Tests that the key is out of bounds and exception will be thrown. + */ + @Test(expected = PropertyNotFoundException.class) + public void testSetValue07() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + resolver.setValue(context, list, Integer.valueOf(1), "value"); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testIsReadOnly01() { + ListELResolver resolver = new ListELResolver(); + resolver.isReadOnly(null, new Object(), new Object()); + } + + /** + * Tests that the propertyResolved is false if base is not List. + */ + @Test + public void testIsReadOnly02() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, new Object(), new Object()); + + Assert.assertFalse(result); + Assert.assertFalse(context.isPropertyResolved()); + + resolver = new ListELResolver(true); + + result = resolver.isReadOnly(context, new Object(), new Object()); + + Assert.assertTrue(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that if the ListELResolver is constructed with readOnly the method will return always true, otherwise + * false. + */ + @Test + public void testIsReadOnly03() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + boolean result = resolver.isReadOnly(context, list, Integer.valueOf(0)); + + Assert.assertFalse(result); + Assert.assertTrue(context.isPropertyResolved()); + + resolver = new ListELResolver(true); + + result = resolver.isReadOnly(context, list, Integer.valueOf(0)); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that the readOnly is true always when the map is not modifiable. + */ + @Test + public void testIsReadOnly04() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + List unmodifiableList = Collections.unmodifiableList(list); + boolean result = resolver.isReadOnly(context, unmodifiableList, Integer.valueOf(0)); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that the key is out of bounds and exception will be thrown. + */ + @Test(expected = PropertyNotFoundException.class) + public void testIsReadOnly05() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + resolver.isReadOnly(context, list, Integer.valueOf(1)); + } + + /** + * Tests that a result is returned even when a coercion cannot be performed. + */ + @Test + public void testIsReadOnly06() { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + List list = new ArrayList<>(); + list.add("key"); + boolean result = resolver.isReadOnly(context, list, "key"); + + Assert.assertFalse(result); + Assert.assertTrue(context.isPropertyResolved()); + + resolver = new ListELResolver(true); + + result = resolver.isReadOnly(context, list, "key"); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + private void doNegativeTest(Object base, Object trigger, MethodUnderTest method, boolean checkResult) { + ListELResolver resolver = new ListELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = null; + switch (method) { + case GET_VALUE: { + result = resolver.getValue(context, base, trigger); + break; + } + case SET_VALUE: { + resolver.setValue(context, base, trigger, new Object()); + break; + } + case GET_TYPE: { + result = resolver.getType(context, base, trigger); + break; + } + default: { + // Should never happen + Assert.fail("Missing case for method"); + } + } + + if (checkResult) { + Assert.assertNull(result); + } + Assert.assertFalse(context.isPropertyResolved()); + } + + private enum MethodUnderTest { + GET_VALUE, + SET_VALUE, + GET_TYPE + } + +} diff --git a/test/jakarta/el/TestMapELResolver.java b/test/jakarta/el/TestMapELResolver.java new file mode 100644 index 0000000..483baf1 --- /dev/null +++ b/test/jakarta/el/TestMapELResolver.java @@ -0,0 +1,295 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class TestMapELResolver { + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetType01() { + MapELResolver mapELResolver = new MapELResolver(); + mapELResolver.getType(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is not Map. + */ + @Test + public void testGetType02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.GET_TYPE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetType03() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Class result = mapELResolver.getType(context, new HashMap<>(), "test"); + + Assert.assertEquals(Object.class, result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetValue01() { + MapELResolver mapELResolver = new MapELResolver(); + mapELResolver.getValue(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is not Map. + */ + @Test + public void testGetValue02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.GET_VALUE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetValue03() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Map map = new HashMap<>(); + map.put("key", "value"); + Object result = mapELResolver.getValue(context, map, "key"); + + Assert.assertEquals("value", result); + Assert.assertTrue(context.isPropertyResolved()); + + result = mapELResolver.getValue(context, map, "unknown-key"); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testSetValue01() { + MapELResolver mapELResolver = new MapELResolver(); + mapELResolver.setValue(null, new Object(), new Object(), new Object()); + } + + /** + * Tests that a valid property is not set if base is not Map. + */ + @Test + public void testSetValue02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.SET_VALUE, false); + } + + /** + * Tests that an exception is thrown when readOnly is true. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue03() { + MapELResolver mapELResolver = new MapELResolver(true); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + mapELResolver.setValue(context, new HashMap<>(), new Object(), new Object()); + } + + /** + * Tests that a valid property is set. + */ + @Test + public void testSetValue04() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Map map = new HashMap<>(); + mapELResolver.setValue(context, map, "key", "value"); + + Assert.assertEquals("value", mapELResolver.getValue(context, map, "key")); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that an exception is thrown when the map is not modifiable. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue05() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Map map = Collections.unmodifiableMap(new HashMap<>()); + mapELResolver.setValue(context, map, "key", "value"); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testIsReadOnly01() { + MapELResolver mapELResolver = new MapELResolver(); + mapELResolver.isReadOnly(null, new Object(), new Object()); + } + + /** + * Tests that the propertyResolved is false if base is not Map. + */ + @Test + public void testIsReadOnly02() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = mapELResolver.isReadOnly(context, new Object(), new Object()); + + Assert.assertFalse(result); + Assert.assertFalse(context.isPropertyResolved()); + + mapELResolver = new MapELResolver(true); + + result = mapELResolver.isReadOnly(context, new Object(), new Object()); + + Assert.assertTrue(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that if the MapELResolver is constructed with readOnly the method will return always true, otherwise false. + */ + @Test + public void testIsReadOnly03() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = mapELResolver.isReadOnly(context, new HashMap<>(), new Object()); + + Assert.assertFalse(result); + Assert.assertTrue(context.isPropertyResolved()); + + mapELResolver = new MapELResolver(true); + + result = mapELResolver.isReadOnly(context, new HashMap<>(), new Object()); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that the readOnly is true always when the map is not modifiable. + */ + @Test + public void testIsReadOnly04() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Map map = Collections.unmodifiableMap(new HashMap<>()); + boolean result = mapELResolver.isReadOnly(context, map, new Object()); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a valid FeatureDescriptors are not returned if base is not Map. + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + @Test + public void testGetFeatureDescriptors01() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Iterator result = mapELResolver.getFeatureDescriptors(context, new Object()); + + Assert.assertNull(result); + } + + /** + * Tests that a valid FeatureDescriptors are returned. + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + @Test + public void testGetFeatureDescriptors02() { + MapELResolver mapELResolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Map map = new HashMap<>(); + map.put("key", "value"); + Iterator result = mapELResolver.getFeatureDescriptors(context, map); + + while (result.hasNext()) { + FeatureDescriptor featureDescriptor = result.next(); + Assert.assertEquals("key", featureDescriptor.getDisplayName()); + Assert.assertEquals("key", featureDescriptor.getName()); + Assert.assertEquals("", featureDescriptor.getShortDescription()); + Assert.assertFalse(featureDescriptor.isExpert()); + Assert.assertFalse(featureDescriptor.isHidden()); + Assert.assertTrue(featureDescriptor.isPreferred()); + Assert.assertEquals("key".getClass(), featureDescriptor.getValue(ELResolver.TYPE)); + Assert.assertEquals(Boolean.TRUE, featureDescriptor.getValue(ELResolver.RESOLVABLE_AT_DESIGN_TIME)); + } + } + + private void doNegativeTest(Object base, Object trigger, MethodUnderTest method, boolean checkResult) { + MapELResolver resolver = new MapELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = null; + switch (method) { + case GET_VALUE: { + result = resolver.getValue(context, base, trigger); + break; + } + case SET_VALUE: { + resolver.setValue(context, base, trigger, new Object()); + break; + } + case GET_TYPE: { + result = resolver.getType(context, base, trigger); + break; + } + default: { + // Should never happen + Assert.fail("Missing case for method"); + } + } + + if (checkResult) { + Assert.assertNull(result); + } + Assert.assertFalse(context.isPropertyResolved()); + } + + private enum MethodUnderTest { + GET_VALUE, + SET_VALUE, + GET_TYPE + } +} diff --git a/test/jakarta/el/TestMethodReference.java b/test/jakarta/el/TestMethodReference.java new file mode 100644 index 0000000..c9b524f --- /dev/null +++ b/test/jakarta/el/TestMethodReference.java @@ -0,0 +1,68 @@ +/* + * 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 jakarta.el; + +import java.beans.BeanProperty; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.el.ELContextImpl; + +public class TestMethodReference { + + @Test + public void testGetAnnotationInfo01() { + // None + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBean bean = new TesterBean("myBean"); + + ValueExpression var = factory.createValueExpression(bean, TesterBean.class); + context.getVariableMapper().setVariable("bean", var); + + MethodExpression me = factory.createMethodExpression(context, "${bean.getName()}", String.class, null); + + MethodReference result = me.getMethodReference(context); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.getAnnotations()); + Assert.assertEquals(0, result.getAnnotations().length); + } + + @Test + public void testGetAnnotationInfo02() { + // @BeanProperty with defaults + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBean bean = new TesterBean("myBean"); + + ValueExpression var = factory.createValueExpression(bean, TesterBean.class); + context.getVariableMapper().setVariable("bean", var); + + MethodExpression me = factory.createMethodExpression(context, "${bean.getValueD()}", String.class, null); + + MethodReference result = me.getMethodReference(context); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.getAnnotations()); + Assert.assertEquals(1, result.getAnnotations().length); + Assert.assertEquals(BeanProperty.class, result.getAnnotations()[0].annotationType()); + } +} diff --git a/test/jakarta/el/TestResourceBundleELResolver.java b/test/jakarta/el/TestResourceBundleELResolver.java new file mode 100644 index 0000000..eaca94f --- /dev/null +++ b/test/jakarta/el/TestResourceBundleELResolver.java @@ -0,0 +1,303 @@ +/* + * 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 jakarta.el; + +import java.beans.FeatureDescriptor; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.ListResourceBundle; +import java.util.ResourceBundle; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.el.ELContextImpl; + +public class TestResourceBundleELResolver { + + @Test + public void bug53001() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + ResourceBundle rb = new TesterResourceBundle(); + + ValueExpression var = factory.createValueExpression(rb, ResourceBundle.class); + context.getVariableMapper().setVariable("rb", var); + + ValueExpression ve = factory.createValueExpression(context, "${rb.keys}", String.class); + + MethodExpression me = factory.createMethodExpression(context, "${rb.getKeys()}", Enumeration.class, null); + + // Ensure we are specification compliant + String result1 = (String) ve.getValue(context); + Assert.assertEquals("???keys???", result1); + + // Check that the method expression does return the keys + Object result2 = me.invoke(context, null); + @SuppressWarnings("unchecked") + Enumeration e = (Enumeration) result2; + + Assert.assertTrue(e.hasMoreElements()); + String element = e.nextElement(); + if ("key1".equals(element)) { + Assert.assertEquals("key1", element); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key2", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + } else { + Assert.assertEquals("key2", element); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key1", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + } + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetValue01() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + resolver.getValue(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is not ResourceBundle. + */ + @Test + public void testGetValue02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.GET_VALUE, true); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetValue03() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + ResourceBundle resourceBundle = new TesterResourceBundle(); + Object result = resolver.getValue(context, resourceBundle, "key1"); + + Assert.assertEquals("value1", result); + Assert.assertTrue(context.isPropertyResolved()); + + result = resolver.getValue(context, resourceBundle, "unknown-key"); + + Assert.assertEquals("???unknown-key???", result); + Assert.assertTrue(context.isPropertyResolved()); + + result = resolver.getValue(context, resourceBundle, null); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetType01() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + resolver.getType(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is not resolved if base is not ResourceBundle. + */ + @Test + public void testGetType02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.GET_TYPE, true); + } + + /** + * Tests that null will be returned when base is ResourceBundle. Checks that the propertyResolved is true. + */ + @Test + public void testGetType03() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + ResourceBundle resourceBundle = new TesterResourceBundle(); + Class result = resolver.getType(context, resourceBundle, "key1"); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testSetValue01() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + resolver.setValue(null, new Object(), new Object(), new Object()); + } + + /** + * Tests that a valid property is not set if base is not ResourceBundle. + */ + @Test + public void testSetValue02() { + doNegativeTest(new Object(), new Object(), MethodUnderTest.SET_VALUE, false); + } + + /** + * Tests that an exception is thrown because the resolver is read only. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue03() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + ResourceBundle resourceBundle = new TesterResourceBundle(); + resolver.setValue(context, resourceBundle, new Object(), new Object()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testIsReadOnly01() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + resolver.isReadOnly(null, new Object(), new Object()); + } + + /** + * Tests that the propertyResolved is false and readOnly is false if base is not ResourceBundle. + */ + @Test + public void testIsReadOnly02() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, new Object(), new Object()); + + Assert.assertFalse(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that the readOnly is true always when the base is ResourceBundle. + */ + @Test + public void testIsReadOnly03() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + ResourceBundle resourceBundle = new TesterResourceBundle(); + boolean result = resolver.isReadOnly(context, resourceBundle, new Object()); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a valid FeatureDescriptors are not returned if base is not ResourceBundle. + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + @Test + public void testGetFeatureDescriptors01() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Iterator result = resolver.getFeatureDescriptors(context, new Object()); + + Assert.assertNull(result); + } + + /** + * Tests that a valid FeatureDescriptors are returned. + */ + @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") + @Test + public void testGetFeatureDescriptors02() { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + ResourceBundle resourceBundle = new TesterResourceBundle(new Object[][] { { "key", "value" } }); + Iterator result = resolver.getFeatureDescriptors(context, resourceBundle); + + while (result.hasNext()) { + FeatureDescriptor featureDescriptor = result.next(); + Assert.assertEquals("key", featureDescriptor.getDisplayName()); + Assert.assertEquals("key", featureDescriptor.getName()); + Assert.assertEquals("", featureDescriptor.getShortDescription()); + Assert.assertFalse(featureDescriptor.isExpert()); + Assert.assertFalse(featureDescriptor.isHidden()); + Assert.assertTrue(featureDescriptor.isPreferred()); + Assert.assertEquals(String.class, featureDescriptor.getValue(ELResolver.TYPE)); + Assert.assertEquals(Boolean.TRUE, featureDescriptor.getValue(ELResolver.RESOLVABLE_AT_DESIGN_TIME)); + } + } + + private static class TesterResourceBundle extends ListResourceBundle { + + TesterResourceBundle() { + this(new Object[][] { { "key1", "value1" }, { "key2", "value2" } }); + } + + TesterResourceBundle(Object[][] contents) { + this.contents = contents; + } + + @Override + protected Object[][] getContents() { + return contents; + } + + private Object[][] contents; + } + + private void doNegativeTest(Object base, Object trigger, MethodUnderTest method, boolean checkResult) { + ResourceBundleELResolver resolver = new ResourceBundleELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = null; + switch (method) { + case GET_VALUE: { + result = resolver.getValue(context, base, trigger); + break; + } + case SET_VALUE: { + resolver.setValue(context, base, trigger, new Object()); + break; + } + case GET_TYPE: { + result = resolver.getType(context, base, trigger); + break; + } + default: { + // Should never happen + Assert.fail("Missing case for method"); + } + } + + if (checkResult) { + Assert.assertNull(result); + } + Assert.assertFalse(context.isPropertyResolved()); + } + + private enum MethodUnderTest { + GET_VALUE, + SET_VALUE, + GET_TYPE + } +} diff --git a/test/jakarta/el/TestStaticFieldELResolver.java b/test/jakarta/el/TestStaticFieldELResolver.java new file mode 100644 index 0000000..d55ebd7 --- /dev/null +++ b/test/jakarta/el/TestStaticFieldELResolver.java @@ -0,0 +1,459 @@ +/* + * 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 jakarta.el; + +import org.junit.Assert; +import org.junit.Test; + +public class TestStaticFieldELResolver { + + private static final String PROPERTY01_NAME = "publicStaticString"; + private static final String PROPERTY01_VALUE = "publicStaticStringNewValue"; + private static final String PROPERTY02_NAME = "nonExistingString"; + private static final String PROPERTY03_NAME = "publicString"; + private static final String PROPERTY04_NAME = "privateStaticString"; + private static final String PROPERTY05_NAME = "privateString"; + private static final String METHOD01_NAME = ""; + private static final String METHOD02_NAME = "getPublicStaticString"; + private static final String METHOD03_NAME = "setPrivateString"; + private static final String METHOD04_NAME = "printPublicStaticString"; + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetValue01() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + resolver.getValue(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetValue02() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getValue(context, new ELClass(TesterClass.class), PROPERTY01_NAME); + + Assert.assertEquals(PROPERTY01_NAME, result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a valid property is not resolved if base is not ELCLass. + */ + @Test + public void testGetValue03() { + doNegativeTest(new Object(), PROPERTY01_NAME, MethodUnderTest.GET_VALUE); + } + + /** + * Tests that non String property is not resolved. + */ + @Test + public void testGetValue04() { + doNegativeTest(new ELClass(TesterClass.class), new Object(), MethodUnderTest.GET_VALUE); + } + + /** + * Property doesn't exist + */ + @Test + public void testGetValue05() { + doThrowableTest(PROPERTY02_NAME, MethodUnderTest.GET_VALUE, true); + } + + /** + * Property is not static + */ + @Test + public void testGetValue06() { + doThrowableTest(PROPERTY03_NAME, MethodUnderTest.GET_VALUE, false); + } + + /** + * Property is not public + */ + @Test + public void testGetValue07() { + doThrowableTest(PROPERTY04_NAME, MethodUnderTest.GET_VALUE, true); + } + + /** + * Property is neither public nor static + */ + @Test + public void testGetValue08() { + doThrowableTest(PROPERTY05_NAME, MethodUnderTest.GET_VALUE, true); + } + + /** + * Tests that a valid property of Enum is resolved. + */ + @Test + public void testGetValue09() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.getValue(context, new ELClass(MethodUnderTest.class), + MethodUnderTest.GET_TYPE.toString()); + + Assert.assertEquals(MethodUnderTest.GET_TYPE, result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testSetValue01() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + resolver.setValue(null, new Object(), new Object(), new Object()); + } + + /** + * Tests that cannot write to a static field. + */ + @Test(expected = PropertyNotWritableException.class) + public void testSetValue02() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + resolver.setValue(context, new ELClass(TesterClass.class), PROPERTY01_NAME, PROPERTY01_VALUE); + } + + /** + * Tests that the operation is not invoked if base is not ELCLass. + */ + @Test + public void testSetValue03() { + doNegativeTest(new Object(), PROPERTY01_NAME, MethodUnderTest.SET_VALUE); + } + + /** + * Tests that the operation is no invoked when the property is not String. + */ + @Test + public void testSetValue04() { + doNegativeTest(new ELClass(TesterClass.class), new Object(), MethodUnderTest.SET_VALUE); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testIsReadOnly01() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + resolver.isReadOnly(null, new Object(), new Object()); + } + + /** + * Tests that the propertyResolved is true when base is ELCLass and the property is String. + */ + @Test + public void testIsReadOnly02() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, new ELClass(TesterClass.class), PROPERTY01_NAME); + + Assert.assertTrue(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that the propertyResolved is false if base is not ELCLass. + */ + @Test + public void testIsReadOnly03() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, new Object(), PROPERTY01_NAME); + + Assert.assertTrue(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that the propertyResolved is false when the property is not String. + */ + @Test + public void testIsReadOnly04() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + boolean result = resolver.isReadOnly(context, new ELClass(TesterClass.class), new Object()); + + Assert.assertTrue(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testGetType01() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + resolver.getType(null, new Object(), new Object()); + } + + /** + * Tests that a valid property is resolved. + */ + @Test + public void testGetType02() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Class result = resolver.getType(context, new ELClass(TesterClass.class), PROPERTY01_NAME); + + // Resolver is read-only so this should return null + Assert.assertNull(result); + } + + /** + * Tests that a valid property is not resolved if base is not ELCLass. + */ + @Test + public void testGetType03() { + doNegativeTest(new Object(), PROPERTY01_NAME, MethodUnderTest.GET_TYPE); + } + + /** + * Tests that non String property is not resolved. + */ + @Test + public void testGetType04() { + doNegativeTest(new ELClass(TesterClass.class), new Object(), MethodUnderTest.GET_TYPE); + } + + /** + * Property doesn't exist + */ + @Test + public void testGetType05() { + doThrowableTest(PROPERTY02_NAME, MethodUnderTest.GET_TYPE, true); + } + + /** + * Property is not static + */ + @Test + public void testGetType06() { + doThrowableTest(PROPERTY03_NAME, MethodUnderTest.GET_TYPE, false); + } + + /** + * Property is not public + */ + @Test + public void testGetType07() { + doThrowableTest(PROPERTY04_NAME, MethodUnderTest.GET_TYPE, true); + } + + /** + * Property is neither public nor static + */ + @Test + public void testGetType08() { + doThrowableTest(PROPERTY05_NAME, MethodUnderTest.GET_TYPE, true); + } + + /** + * Tests that a valid property of Enum is resolved. + */ + @Test + public void testGetType09() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Class result = resolver.getType(context, new ELClass(MethodUnderTest.class), + MethodUnderTest.GET_TYPE.toString()); + + // Resolver is read-only so this should return null + Assert.assertNull(result); + } + + /** + * Tests that a null context results in an NPE as per EL Javadoc. + */ + @Test(expected = NullPointerException.class) + public void testInvoke01() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + resolver.invoke(null, new Object(), new Object(), new Class[] {}, new Object[] {}); + } + + /** + * Tests a constructor invocation. + */ + @Test + public void testInvoke02() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new ELClass(TesterClass.class), METHOD01_NAME, null, null); + + Assert.assertEquals(TesterClass.class, result.getClass()); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests a method invocation. + */ + @Test + public void testInvoke03() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new ELClass(TesterClass.class), METHOD02_NAME, new Class[] {}, + new Object[] {}); + + Assert.assertEquals(PROPERTY01_NAME, result); + Assert.assertTrue(context.isPropertyResolved()); + } + + /** + * Tests that a valid method is not resolved if base is not ELCLass. + */ + @Test + public void testInvoke04() { + doNegativeTest(new Object(), METHOD02_NAME, MethodUnderTest.INVOKE); + } + + /** + * Tests that non String method name is not resolved. + */ + @Test + public void testInvoke05() { + doNegativeTest(new ELClass(TesterClass.class), new Object(), MethodUnderTest.INVOKE); + } + + /** + * Tests that a private constructor invocation will fail. + */ + @Test + public void testInvoke06() { + doThrowableTest(METHOD01_NAME, MethodUnderTest.INVOKE, false); + } + + /** + * Tests that a non static method invocation will fail. + */ + @Test + public void testInvoke07() { + doThrowableTest(METHOD03_NAME, MethodUnderTest.INVOKE, false); + } + + /** + * Tests a void method invocation. + */ + @Test + public void testInvoke08() { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = resolver.invoke(context, new ELClass(TesterClass.class), METHOD04_NAME, new Class[] {}, + new Object[] {}); + + Assert.assertNull(result); + Assert.assertTrue(context.isPropertyResolved()); + } + + private void doNegativeTest(Object elClass, Object trigger, MethodUnderTest method) { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + Object result = null; + switch (method) { + case GET_VALUE: { + result = resolver.getValue(context, elClass, trigger); + break; + } + case SET_VALUE: { + resolver.setValue(context, elClass, trigger, PROPERTY01_VALUE); + result = resolver.getValue(context, elClass, trigger); + break; + } + case GET_TYPE: { + result = resolver.getType(context, elClass, trigger); + break; + } + case INVOKE: { + result = resolver.invoke(context, elClass, trigger, new Class[] { String.class }, + new Object[] { "test" }); + break; + } + default: { + // Should never happen + Assert.fail("Missing case for method"); + } + } + + Assert.assertNull(result); + Assert.assertFalse(context.isPropertyResolved()); + } + + private void doThrowableTest(String trigger, MethodUnderTest method, boolean checkCause) { + StaticFieldELResolver resolver = new StaticFieldELResolver(); + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + ELException exception = null; + try { + switch (method) { + case GET_VALUE: { + resolver.getValue(context, new ELClass(TesterClass.class), trigger); + break; + } + case GET_TYPE: { + resolver.getType(context, new ELClass(TesterClass.class), trigger); + break; + } + case INVOKE: { + resolver.invoke(context, new ELClass(TesterClass.class), trigger, new Class[] { String.class }, + new Object[] { "test" }); + break; + } + default: { + // Should never happen + Assert.fail("Missing case for method"); + } + } + + } catch (PropertyNotFoundException | MethodNotFoundException e) { + exception = e; + } + + Assert.assertTrue(context.isPropertyResolved()); + Assert.assertNotNull(exception); + + if (checkCause) { + // Can't be null due to assertion above + Throwable cause = exception.getCause(); + Assert.assertNotNull(cause); + } + } + + private enum MethodUnderTest { + GET_VALUE, + SET_VALUE, + GET_TYPE, + INVOKE + } +} diff --git a/test/jakarta/el/TestUtil.java b/test/jakarta/el/TestUtil.java new file mode 100644 index 0000000..3ed53f5 --- /dev/null +++ b/test/jakarta/el/TestUtil.java @@ -0,0 +1,56 @@ +/* + * 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 jakarta.el; + +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; + +public class TestUtil { + + @Test + public void test01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("sb", new StringBuilder()); + Assert.assertEquals("a", processor.eval("sb.append('a'); sb.toString()")); + } + + + @Test + public void test02() { + ELProcessor processor = new ELProcessor(); + processor.getELManager().importClass("java.util.Date"); + Date result = processor.eval("Date(86400)"); + Assert.assertEquals(86400, result.getTime()); + } + + + @Test + public void testBug56425a() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("string", "a-b-c-d"); + Assert.assertEquals("a_b_c_d", processor.eval("string.replace(\"-\",\"_\")")); + } + + @Test + public void testBug56425b() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("string", "Not used. Any value is fine here"); + Assert.assertEquals("5", processor.eval("string.valueOf(5)")); + } +} diff --git a/test/jakarta/el/TesterBean.java b/test/jakarta/el/TesterBean.java new file mode 100644 index 0000000..25d46c0 --- /dev/null +++ b/test/jakarta/el/TesterBean.java @@ -0,0 +1,72 @@ +/* + * 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 jakarta.el; + +import java.beans.BeanProperty; + +public class TesterBean { + + private String name; + private Integer[] valueC; + + public TesterBean(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getNameVarargs(@SuppressWarnings("unused") Integer... someNumbers) { + return name; + } + + public String getNameVarargs(@SuppressWarnings("unused") Boolean someBoolean, + @SuppressWarnings("unused") Integer... someNumbers) { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return getName(); + } + + public String getValueA() throws Exception { + throw new Exception(); + } + + public int[] getValueB() { + return new int[] { 1, 2, 3, 4, 5 }; + } + + public void setValueC(Integer[] values) { + valueC = values; + } + + public Integer[] getValueC() { + return valueC; + } + + @BeanProperty + public String getValueD() { + return ""; + } +} diff --git a/test/jakarta/el/TesterBeanNameResolver.java b/test/jakarta/el/TesterBeanNameResolver.java new file mode 100644 index 0000000..e04fd44 --- /dev/null +++ b/test/jakarta/el/TesterBeanNameResolver.java @@ -0,0 +1,82 @@ +/* + * 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 jakarta.el; + +import java.util.HashMap; +import java.util.Map; + +public class TesterBeanNameResolver extends BeanNameResolver { + + public static final String EXCEPTION_TRIGGER_NAME = "exception"; + public static final String THROWABLE_TRIGGER_NAME = "throwable"; + public static final String READ_ONLY_NAME = "readonly"; + + private Map beans = new HashMap<>(); + private boolean allowCreate = true; + + + public TesterBeanNameResolver() { + beans.put(EXCEPTION_TRIGGER_NAME, new Object()); + beans.put(THROWABLE_TRIGGER_NAME, new Object()); + beans.put(READ_ONLY_NAME, new Object()); + } + + @Override + public void setBeanValue(String beanName, Object value) throws PropertyNotWritableException { + checkTriggers(beanName); + if (allowCreate || beans.containsKey(beanName)) { + beans.put(beanName, value); + } + } + + @Override + public boolean isNameResolved(String beanName) { + return beans.containsKey(beanName); + } + + @Override + public Object getBean(String beanName) { + checkTriggers(beanName); + return beans.get(beanName); + } + + @Override + public boolean canCreateBean(String beanName) { + checkTriggers(beanName); + return allowCreate; + } + + + public void setAllowCreate(boolean allowCreate) { + this.allowCreate = allowCreate; + } + + @Override + public boolean isReadOnly(String beanName) { + checkTriggers(beanName); + return READ_ONLY_NAME.equals(beanName); + } + + private void checkTriggers(String beanName) { + if (EXCEPTION_TRIGGER_NAME.equals(beanName)) { + throw new RuntimeException(); + } + if (THROWABLE_TRIGGER_NAME.equals(beanName)) { + throw new Error(); + } + } +} diff --git a/test/jakarta/el/TesterClass.java b/test/jakarta/el/TesterClass.java new file mode 100644 index 0000000..7057aee --- /dev/null +++ b/test/jakarta/el/TesterClass.java @@ -0,0 +1,47 @@ +/* + * 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 jakarta.el; + +public class TesterClass { + + public static final String publicStaticString = "publicStaticString"; + public String publicString = "publicString"; + @SuppressWarnings("unused") // Used in TestStaticFieldELResolver + private static String privateStaticString = "privateStaticString"; + @SuppressWarnings("unused") // Used in TestStaticFieldELResolver + private String privateString = "privateString"; + + public TesterClass() { + } + + @SuppressWarnings("unused") // Used in TestStaticFieldELResolver + private TesterClass(String privateString) { + this.privateString = privateString; + } + + public static String getPublicStaticString() { + return publicStaticString; + } + + public static void printPublicStaticString() { + System.out.println(publicStaticString); + } + + public void setPrivateString(String privateString) { + this.privateString = privateString; + } +} diff --git a/test/jakarta/el/TesterCompositeELResolverPerformance.java b/test/jakarta/el/TesterCompositeELResolverPerformance.java new file mode 100644 index 0000000..e974b97 --- /dev/null +++ b/test/jakarta/el/TesterCompositeELResolverPerformance.java @@ -0,0 +1,52 @@ +/* + * 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 jakarta.el; + +import org.junit.Test; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterCompositeELResolverPerformance { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=68119 + */ + @Test + public void testConvertToType() throws Exception { + ELManager manager = new ELManager(); + ELContext context = manager.getELContext(); + ELResolver resolver = context.getELResolver(); + + // Warm-up + doConversion(context, resolver); + + long start = System.nanoTime(); + doConversion(context, resolver); + long duration = System.nanoTime() - start; + + System.out.println("convertToType performance test complete in " + duration + "ns"); + } + + + private void doConversion(ELContext context, ELResolver resolver) { + for (int i = 0; i < 10000000; i++) { + resolver.convertToType(context, "This is a String", String.class); + } + } +} diff --git a/test/jakarta/el/TesterELContext.java b/test/jakarta/el/TesterELContext.java new file mode 100644 index 0000000..7478cfb --- /dev/null +++ b/test/jakarta/el/TesterELContext.java @@ -0,0 +1,45 @@ +/* + * 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 jakarta.el; + +public class TesterELContext extends ELContext { + + private final ELResolver resolver; + + public TesterELContext() { + this(null); + } + + public TesterELContext(ELResolver resolver) { + this.resolver = resolver; + } + + @Override + public ELResolver getELResolver() { + return resolver; + } + + @Override + public FunctionMapper getFunctionMapper() { + return null; + } + + @Override + public VariableMapper getVariableMapper() { + return null; + } +} diff --git a/test/jakarta/el/TesterELResolverOne.java b/test/jakarta/el/TesterELResolverOne.java new file mode 100644 index 0000000..478fe28 --- /dev/null +++ b/test/jakarta/el/TesterELResolverOne.java @@ -0,0 +1,31 @@ +/* + * 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 jakarta.el; + +public class TesterELResolverOne extends TypeConverter { + + @Override + public T convertToType(ELContext context, Object obj, Class type) { + if ("1".equals(obj) && type == String.class) { + context.setPropertyResolved(obj, type); + @SuppressWarnings("unchecked") + T result = (T) "ONE"; + return result; + } + return null; + } +} diff --git a/test/jakarta/el/TesterELResolverTwo.java b/test/jakarta/el/TesterELResolverTwo.java new file mode 100644 index 0000000..c9a9c96 --- /dev/null +++ b/test/jakarta/el/TesterELResolverTwo.java @@ -0,0 +1,31 @@ +/* + * 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 jakarta.el; + +public class TesterELResolverTwo extends TypeConverter { + + @Override + public T convertToType(ELContext context, Object obj, Class type) { + if ("2".equals(obj) && type == String.class) { + context.setPropertyResolved(obj, type); + @SuppressWarnings("unchecked") + T result = (T) "TWO"; + return result; + } + return null; + } +} diff --git a/test/jakarta/el/TesterEvaluationListener.java b/test/jakarta/el/TesterEvaluationListener.java new file mode 100644 index 0000000..ba60b97 --- /dev/null +++ b/test/jakarta/el/TesterEvaluationListener.java @@ -0,0 +1,79 @@ +/* + * 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 jakarta.el; + +import java.util.ArrayList; +import java.util.List; + +public class TesterEvaluationListener extends EvaluationListener { + + private final List resolvedProperties = new ArrayList<>(); + private final List beforeEvaluationExpressions = new ArrayList<>(); + private final List afterEvaluationExpressions = new ArrayList<>(); + + + @Override + public void propertyResolved(ELContext context, Object base, Object property) { + resolvedProperties.add(new Pair(base, property)); + } + + + @Override + public void beforeEvaluation(ELContext context, String expression) { + beforeEvaluationExpressions.add(expression); + } + + + @Override + public void afterEvaluation(ELContext context, String expression) { + afterEvaluationExpressions.add(expression); + } + + + public List getResolvedProperties() { + return resolvedProperties; + } + + + public List getBeforeEvaluationExpressions() { + return beforeEvaluationExpressions; + } + + + public List getAfterEvaluationExpressions() { + return afterEvaluationExpressions; + } + + + public static class Pair { + private final Object base; + private final Object property; + + public Pair(Object base, Object property) { + this.base = base; + this.property = property; + } + + public Object getBase() { + return base; + } + + public Object getProperty() { + return property; + } + } +} diff --git a/test/jakarta/el/TesterFunctions.java b/test/jakarta/el/TesterFunctions.java new file mode 100644 index 0000000..c14ad2b --- /dev/null +++ b/test/jakarta/el/TesterFunctions.java @@ -0,0 +1,66 @@ +/* + * 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 jakarta.el; + +public class TesterFunctions { + + private static StringBuilder calls = new StringBuilder(); + + public static String getCallList() { + return calls.toString(); + } + + public static void resetCallList() { + calls = new StringBuilder(); + } + + public static void doIt() { + calls.append('A'); + } + + public static void doIt(@SuppressWarnings("unused") int a) { + calls.append('B'); + } + + public static void doIt(@SuppressWarnings("unused") Integer a) { + calls.append('C'); + } + + public static void doIt(@SuppressWarnings("unused") int[] a) { + calls.append('D'); + } + + public static void doIt(@SuppressWarnings("unused") int[][] a) { + calls.append('E'); + } + + public static void doIt(@SuppressWarnings("unused") Integer[] a) { + calls.append('F'); + } + + public static void doIt(@SuppressWarnings("unused") Integer[][] a) { + calls.append('G'); + } + + public static void doIt(@SuppressWarnings("unused") long... a) { + calls.append('H'); + } + + public static void doIt(@SuppressWarnings("unused") Object... a) { + calls.append('I'); + } +} diff --git a/test/jakarta/el/TesterImportHandlerPerformance.java b/test/jakarta/el/TesterImportHandlerPerformance.java new file mode 100644 index 0000000..77e39e7 --- /dev/null +++ b/test/jakarta/el/TesterImportHandlerPerformance.java @@ -0,0 +1,49 @@ +/* + * 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 jakarta.el; + +import org.junit.Test; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterImportHandlerPerformance { + + /* + * This test is looking at the cost of looking up a class when the standard JSP package imports are present: - + * java.lang - jakarta.servlet - jakarta.servlet.http - jakarta.servlet.jsp + * + * Before optimisation, this test took ~4.6s on markt's desktop After optimisation, this test took ~0.05s on markt's + * desktop + */ + @Test + public void testBug62453() throws Exception { + long totalTime = 0; + for (int i = 0; i < 100000; i++) { + ImportHandler ih = new ImportHandler(); + ih.importPackage("jakarta.servlet"); + ih.importPackage("jakarta.servlet.http"); + ih.importPackage("jakarta.servlet.jsp"); + long start = System.nanoTime(); + ih.resolveClass("unknown"); + long end = System.nanoTime(); + totalTime += (end - start); + } + System.out.println("Time taken: " + totalTime + "ns"); + } +} diff --git a/test/jakarta/servlet/TestSessionCookieConfig.java b/test/jakarta/servlet/TestSessionCookieConfig.java new file mode 100644 index 0000000..737c702 --- /dev/null +++ b/test/jakarta/servlet/TestSessionCookieConfig.java @@ -0,0 +1,56 @@ +/* + * 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 jakarta.servlet; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestSessionCookieConfig extends TomcatBaseTest { + + /* + * Not strictly testing the SessionCookieConfig class + */ + @Test + public void testCustomAttribute() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk responseBody = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + + int statusCode = + getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49196.jsp", responseBody, responseHeaders); + + Assert.assertEquals(HttpServletResponse.SC_OK, statusCode); + Assert.assertTrue(responseBody.toString().contains("OK")); + Assert.assertTrue(responseHeaders.containsKey("Set-Cookie")); + + List setCookieHeaders = responseHeaders.get("Set-Cookie"); + Assert.assertEquals(1, setCookieHeaders.size()); + + String setCookieHeader = setCookieHeaders.get(0); + Assert.assertTrue(setCookieHeader.contains("; aaa=bbb")); + } +} diff --git a/test/jakarta/servlet/annotation/TestServletSecurity.java b/test/jakarta/servlet/annotation/TestServletSecurity.java new file mode 100644 index 0000000..4782922 --- /dev/null +++ b/test/jakarta/servlet/annotation/TestServletSecurity.java @@ -0,0 +1,106 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestServletSecurity extends TomcatBaseTest { + + @Test + public void testFooThenFooBar() throws Exception { + doTestFooAndFooBar(true); + } + + + @Test + public void testFooBarThenFoo() throws Exception { + doTestFooAndFooBar(false); + } + + + public void doTestFooAndFooBar(boolean fooFirst) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "Foo", Foo.class.getName()); + ctx.addServletMappingDecoded("/foo/*", "Foo"); + + Tomcat.addServlet(ctx, "FooBar", FooBar.class.getName()); + ctx.addServletMappingDecoded("/foo/bar/*", "FooBar"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + + if (fooFirst) { + rc = getUrl("http://localhost:" + getPort() + "/foo", bc, null, null); + } else { + rc = getUrl("http://localhost:" + getPort() + "/foo/bar", bc, null, null); + } + + bc.recycle(); + Assert.assertEquals(403, rc); + + if (fooFirst) { + rc = getUrl("http://localhost:" + getPort() + "/foo/bar", bc, null, null); + } else { + rc = getUrl("http://localhost:" + getPort() + "/foo", bc, null, null); + } + + Assert.assertEquals(403, rc); + } + + + @ServletSecurity(@HttpConstraint(ServletSecurity.EmptyRoleSemantic.DENY)) + public static class Foo extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.getWriter().print("OK: Foo"); + } + } + + + public static class FooBar extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.getWriter().print("OK: FooBar"); + } + } +} diff --git a/test/jakarta/servlet/annotation/TestServletSecurityMappings.java b/test/jakarta/servlet/annotation/TestServletSecurityMappings.java new file mode 100644 index 0000000..60440ea --- /dev/null +++ b/test/jakarta/servlet/annotation/TestServletSecurityMappings.java @@ -0,0 +1,203 @@ +/* + * 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 jakarta.servlet.annotation; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +@RunWith(Parameterized.class) +public class TestServletSecurityMappings extends TomcatBaseTest { + + @Parameters(name = "{0}, {1}, {2}, {3}") + public static Collection inputs() { + List result = new ArrayList<>(); + result.add(new Object[] { Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE }); + result.add(new Object[] { Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.TRUE }); + result.add(new Object[] { Boolean.FALSE, Boolean.FALSE, Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { Boolean.FALSE, Boolean.FALSE, Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE }); + result.add(new Object[] { Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE }); + result.add(new Object[] { Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { Boolean.TRUE, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE }); + result.add(new Object[] { Boolean.TRUE, Boolean.FALSE, Boolean.FALSE, Boolean.TRUE }); + result.add(new Object[] { Boolean.TRUE, Boolean.FALSE, Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { Boolean.TRUE, Boolean.FALSE, Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { Boolean.TRUE, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE }); + result.add(new Object[] { Boolean.TRUE, Boolean.TRUE, Boolean.FALSE, Boolean.TRUE }); + result.add(new Object[] { Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE }); + return result; + } + + @Parameter(0) + public boolean redirectContextRoot; + + @Parameter(1) + public boolean secureRoot; + + @Parameter(2) + public boolean secureDefault; + + @Parameter(3) + public boolean secureFoo; + + + @Test + public void doTestSecurityAnnotationsAddServlet() throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("/test", null); + ctx.setMapperContextRootRedirectEnabled(redirectContextRoot); + + ServletContainerInitializer sci = new SCI(secureRoot, secureDefault, secureFoo); + ctx.addServletContainerInitializer(sci, null); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + + // Foo + rc = getUrl("http://localhost:" + getPort() + "/test/foo", bc, false); + if (secureFoo || secureDefault) { + Assert.assertEquals(403, rc); + } else { + Assert.assertEquals(200, rc); + } + bc.recycle(); + + // Default + rc = getUrl("http://localhost:" + getPort() + "/test/something", bc, false); + if (secureDefault) { + Assert.assertEquals(403, rc); + } else { + Assert.assertEquals(200, rc); + } + bc.recycle(); + + // Root + rc = getUrl("http://localhost:" + getPort() + "/test", bc, false); + if (redirectContextRoot) { + Assert.assertEquals(302, rc); + } else { + if (secureRoot || secureDefault) { + Assert.assertEquals(403, rc); + } else { + Assert.assertEquals(200, rc); + } + } + } + + + public static class SCI implements ServletContainerInitializer { + + private final boolean secureRoot; + private final boolean secureDefault; + private final boolean secureFoo; + + public SCI(boolean secureRoot, boolean secureDefault, boolean secureFoo) { + this.secureRoot = secureRoot; + this.secureDefault = secureDefault; + this.secureFoo = secureFoo; + } + + @Override + public void onStartup(Set> c, ServletContext ctx) throws ServletException { + + ServletRegistration.Dynamic sr; + if (secureRoot) { + sr = ctx.addServlet("Root", SecureRoot.class.getName()); + } else { + sr = ctx.addServlet("Root", Ok.class.getName()); + } + sr.addMapping(""); + + if (secureDefault) { + sr = ctx.addServlet("Default", SecureDefault.class.getName()); + } else { + sr = ctx.addServlet("Default", Ok.class.getName()); + } + sr.addMapping("/"); + + if (secureFoo) { + sr = ctx.addServlet("Foo", SecureFoo.class.getName()); + } else { + sr = ctx.addServlet("Foo", Ok.class.getName()); + } + sr.addMapping("/foo"); + } + } + + + @ServletSecurity(@HttpConstraint(ServletSecurity.EmptyRoleSemantic.DENY)) + public static class SecureRoot extends Ok { + + private static final long serialVersionUID = 1L; + } + + + @ServletSecurity(@HttpConstraint(ServletSecurity.EmptyRoleSemantic.DENY)) + public static class SecureDefault extends Ok { + + private static final long serialVersionUID = 1L; + } + + + @ServletSecurity(@HttpConstraint(ServletSecurity.EmptyRoleSemantic.DENY)) + public static class SecureFoo extends Ok { + + private static final long serialVersionUID = 1L; + } + + + public static class Ok extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.getWriter().print("OK"); + } + } +} diff --git a/test/jakarta/servlet/http/HttpServletDoHeadBaseTest.java b/test/jakarta/servlet/http/HttpServletDoHeadBaseTest.java new file mode 100644 index 0000000..4fce606 --- /dev/null +++ b/test/jakarta/servlet/http/HttpServletDoHeadBaseTest.java @@ -0,0 +1,373 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.OutputBuffer; +import org.apache.catalina.connector.Response; +import org.apache.catalina.connector.ResponseFacade; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http2.Http2TestBase; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; +import org.apache.tomcat.util.compat.JreCompat; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +public class HttpServletDoHeadBaseTest extends Http2TestBase { + + // Tomcat has a minimum output buffer size of 8 * 1024. + // (8 * 1024) /16 = 512 + + private static final String VALID = "** valid data **"; + private static final String INVALID = "* invalid data *"; + + protected static final Integer BUFFERS[] = new Integer[] { Integer.valueOf (16), Integer.valueOf(8 * 1024), Integer.valueOf(16 * 1024) }; + + protected static final Integer COUNTS[] = new Integer[] { Integer.valueOf(0), Integer.valueOf(1), + Integer.valueOf(511), Integer.valueOf(512), Integer.valueOf(513), + Integer.valueOf(1023), Integer.valueOf(1024), Integer.valueOf(1025) }; + + @Parameter(2) + public boolean useLegacy; + @Parameter(3) + public int bufferSize; + @Parameter(4) + public boolean useWriter; + @Parameter(5) + public int invalidWriteCount; + @Parameter(6) + public ResetType resetType; + @Parameter(7) + public int validWriteCount; + @Parameter(8) + public boolean explicitFlush; + + @Test + public void testDoHead() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + configureAndStartWebApplication(); + + Map> getHeaders = new CaseInsensitiveKeyMap<>(); + String path = "http://localhost:" + getPort() + "/test"; + ByteChunk out = new ByteChunk(); + + int rc = getUrl(path, out, getHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + out.recycle(); + + Map> headHeaders = new HashMap<>(); + rc = headUrl(path, out, headHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // Headers should be the same (apart from Date) + Assert.assertEquals(getHeaders.size(), headHeaders.size()); + for (Map.Entry> getHeader : getHeaders.entrySet()) { + String headerName = getHeader.getKey(); + if ("date".equalsIgnoreCase(headerName)) { + continue; + } + Assert.assertTrue(headerName, headHeaders.containsKey(headerName)); + List getValues = getHeader.getValue(); + List headValues = headHeaders.get(headerName); + Assert.assertEquals(getValues.size(), headValues.size()); + for (String value : getValues) { + Assert.assertTrue(headValues.contains(value)); + } + } + + tomcat.stop(); + } + + + @Test + public void testDoHeadHttp2() throws Exception { + StringBuilder debug = new StringBuilder(); + try { + http2Connect(); + + // Get request + byte[] frameHeaderGet = new byte[9]; + ByteBuffer headersPayloadGet = ByteBuffer.allocate(128); + buildGetRequest(frameHeaderGet, headersPayloadGet, null, 3, "/test"); + writeFrame(frameHeaderGet, headersPayloadGet); + + // Want the headers frame for stream 3 + parser.readFrame(); + while (!output.getTrace().startsWith("3-HeadersStart\n")) { + debug.append(output.getTrace()); + output.clearTrace(); + parser.readFrame(); + } + String traceGet = output.getTrace(); + debug.append(output.getTrace()); + output.clearTrace(); + + // Head request + byte[] frameHeaderHead = new byte[9]; + ByteBuffer headersPayloadHead = ByteBuffer.allocate(128); + buildHeadRequest(frameHeaderHead, headersPayloadHead, 5, "/test"); + writeFrame(frameHeaderHead, headersPayloadHead); + + // Want the headers frame for stream 5 + parser.readFrame(); + while (!output.getTrace().startsWith("5-HeadersStart\n")) { + debug.append(output.getTrace()); + output.clearTrace(); + parser.readFrame(); + } + String traceHead = output.getTrace(); + debug.append(output.getTrace()); + + String[] getHeaders = traceGet.split("\n"); + String[] headHeaders = traceHead.split("\n"); + + int i = 0; + for (; i < getHeaders.length; i++) { + // Headers should be the same, ignoring the first character which is the steam ID + Assert.assertEquals(getHeaders[i] + "\n" + traceGet + traceHead, '3', getHeaders[i].charAt(0)); + Assert.assertEquals(headHeaders[i] + "\n" + traceGet + traceHead, '5', headHeaders[i].charAt(0)); + Assert.assertEquals(traceGet + traceHead, getHeaders[i].substring(1), headHeaders[i].substring(1)); + } + + // Stream 5 should have one more trace entry + Assert.assertEquals("5-EndOfStream", headHeaders[i]); + } catch (Exception t) { + System.out.println(debug.toString()); + throw t; + } + } + + + @Override + @SuppressWarnings("removal") + protected void configureAndStartWebApplication() throws LifecycleException { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + StandardContext ctxt = (StandardContext) getProgrammaticRootContext(); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + HeadTestServlet s = new HeadTestServlet(bufferSize, useWriter, invalidWriteCount, resetType, validWriteCount, explicitFlush); + Wrapper w = Tomcat.addServlet(ctxt, "HeadTestServlet", s); + if (useLegacy) { + w.addInitParameter(HttpServlet.LEGACY_DO_HEAD, "true"); + } + ctxt.addServletMappingDecoded("/test", "HeadTestServlet"); + + tomcat.start(); + } + + + private static class HeadTestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final int bufferSize; + private final boolean useWriter; + private final int invalidWriteCount; + private final ResetType resetType; + private final int validWriteCount; + private final boolean explicitFlush; + + HeadTestServlet(int bufferSize, boolean useWriter, int invalidWriteCount, ResetType resetType, + int validWriteCount, boolean explicitFlush) { + this.bufferSize = bufferSize; + this.useWriter = useWriter; + this.invalidWriteCount = invalidWriteCount; + this.resetType = resetType; + this.validWriteCount = validWriteCount; + this.explicitFlush = explicitFlush; + } + + @SuppressWarnings("removal") + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + int originalBufferSize = resp.getBufferSize(); + int adjustedBufferSize = bufferSize; + boolean resetBufferSize = false; + + if (Boolean.parseBoolean(getServletConfig().getInitParameter(LEGACY_DO_HEAD)) && + JreCompat.isJre19Available() && "HEAD".equals(req.getMethod()) && useWriter && + resetType != ResetType.NONE) { + /* + * Using legacy HEAD handling with a Writer on Java 19+. + * HttpServlet wraps the response. The test is sensitive to + * buffer sizes. The size of the buffer HttpServlet uses varies + * with Java version. For the tests to pass the number of + * characters that can be written before the response is + * committed needs to be the same. + * + * Internally, the Tomcat response buffer defaults to 8192 + * bytes. When a Writer is used, an additional 8192 byte buffer + * is used for char->byte. + * + * With Java <19, the char->byte buffer used by HttpServlet + * processing HEAD requests in legacy mode is created as a 8192 + * byte buffer which is consistent with the buffer Tomcat uses + * internally. + * + * With Java 19+, the char->byte buffer used by HttpServlet + * processing HEAD requests in legacy mode is created as a 512 + * byte buffer. + * + * If the response isn't reset, none of this matters as it is + * just the size of the response buffer and the size of the + * response that determines if chunked encoding is used. + * However, if the response is reset then things get interesting + * as the amount of response data that can be written before + * committing the response is the combination of the char->byte + * buffer and the response buffer. Because the char->byte buffer + * size in legacy mode varies with Java version, the size of the + * response buffer needs to be adjusted to compensate so that + * both GET and HEAD can write the same amount of data before + * committing the response. To make matters worse, to obtain the + * correct behaviour the response buffer size needs to be reset + * back to 8192 after the reset. + * + * This is why the legacy mode is problematic and the new + * approach of the container handling HEAD is better. + */ + + // Response buffer is always no smaller than originalBufferSize + if (adjustedBufferSize < originalBufferSize) { + adjustedBufferSize = originalBufferSize; + } + + // Adjust for smaller initial char -> byte buffer in Java 19+ + // originalBufferSize initial size in Java <19 + // 512 initial size in Java 19+ + adjustedBufferSize = adjustedBufferSize + originalBufferSize - 512; + + resetBufferSize = true; + } + + resp.setBufferSize(adjustedBufferSize); + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + PrintWriter pw = null; + OutputStream os = null; + // Do this rather than repeated calls to getWriter() / + // getOutputStream() to ensure that HEAD handling doesn't rely on + // replacing the OutputStream / PrintWriter (an earlier + // implementation did rely on this) + if (useWriter) { + pw = resp.getWriter(); + } else { + os = resp.getOutputStream(); + } + + for (int i = 0; i < invalidWriteCount; i++) { + write(INVALID, pw, os); + } + + try { + switch (resetType) { + case NONE: { + break; + } + case BUFFER: { + resp.resetBuffer(); + if (resetBufferSize) { + resetResponseBuffer(resp, originalBufferSize); + } + break; + } + case FULL: { + resp.reset(); + if (resetBufferSize) { + resetResponseBuffer(resp, originalBufferSize); + } + break; + } + } + } catch (IllegalStateException ise) { + write("\nIllegalStateException\n", pw, os); + } + + for (int i = 0; i < validWriteCount; i++) { + write(VALID, pw, os); + } + + if (explicitFlush) { + resp.flushBuffer(); + } + } + + private void write(String msg, PrintWriter pw, OutputStream os) throws IOException { + if (useWriter) { + pw.print(msg); + } else { + os.write(msg.getBytes(StandardCharsets.UTF_8)); + } + } + + private void resetResponseBuffer(HttpServletResponse resp, int size) throws ServletException { + // This bypasses various checks but that is OK in this case. + try { + ResponseFacade responseFacade = (ResponseFacade) ((HttpServletResponseWrapper) resp).getResponse(); + + Field responseField = ResponseFacade.class.getDeclaredField("response"); + responseField.setAccessible(true); + Response response = (Response) responseField.get(responseFacade); + + Field outputBufferField = Response.class.getDeclaredField("outputBuffer"); + outputBufferField.setAccessible(true); + OutputBuffer outputBuffer = (OutputBuffer) outputBufferField.get(response); + + Field byteBufferField = OutputBuffer.class.getDeclaredField("bb"); + byteBufferField.setAccessible(true); + byteBufferField.set(outputBuffer, ByteBuffer.allocate(Math.max(size, bufferSize))); + } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { + throw new ServletException(e); + } + } + } + + + enum ResetType { + NONE, + BUFFER, + FULL + } +} diff --git a/test/jakarta/servlet/http/TestCookie.java b/test/jakarta/servlet/http/TestCookie.java new file mode 100644 index 0000000..27268be --- /dev/null +++ b/test/jakarta/servlet/http/TestCookie.java @@ -0,0 +1,214 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.BitSet; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Basic tests for Cookie in default configuration. + */ +public class TestCookie { + public static final BitSet CHAR; // + public static final BitSet CTL; // + public static final BitSet SEPARATORS; + public static final BitSet TOKEN; // 1* + + static { + CHAR = new BitSet(256); + CHAR.set(0, 128); + + CTL = new BitSet(256); + CTL.set(0, 32); + CTL.set(127); + + SEPARATORS = new BitSet(256); + for (char ch : "()<>@,;:\\\"/[]?={} \t".toCharArray()) { + SEPARATORS.set(ch); + } + + TOKEN = new BitSet(256); + TOKEN.or(CHAR); // any CHAR + TOKEN.andNot(CTL); // except CTLs + TOKEN.andNot(SEPARATORS); // or separators + } + + @SuppressWarnings("removal") + @Test + public void testDefaults() { + Cookie cookie = new Cookie("foo", null); + Assert.assertEquals("foo", cookie.getName()); + Assert.assertNull(cookie.getValue()); + Assert.assertEquals(0, cookie.getVersion()); + Assert.assertEquals(-1, cookie.getMaxAge()); + } + + @SuppressWarnings("removal") + @Test + public void testInitialValue() { + Cookie cookie = new Cookie("foo", "bar"); + Assert.assertEquals("foo", cookie.getName()); + Assert.assertEquals("bar", cookie.getValue()); + Assert.assertEquals(0, cookie.getVersion()); + } + + @Test + public void defaultImpliesNetscape() { + Cookie cookie = new Cookie("$Foo", null); + Assert.assertEquals("$Foo", cookie.getName()); + } + + @Test + public void tokenVersion() { + Cookie cookie = new Cookie("Version", null); + Assert.assertEquals("Version", cookie.getName()); + } + + @Test + public void attributeVersion() { + Cookie cookie = new Cookie("Comment", null); + Assert.assertEquals("Comment", cookie.getName()); + } + + @Test + public void attributeDiscard() { + Cookie cookie = new Cookie("Discard", null); + Assert.assertEquals("Discard", cookie.getName()); + } + + @Test + public void attributeExpires() { + Cookie cookie = new Cookie("Expires", null); + Assert.assertEquals("Expires", cookie.getName()); + } + + @Test + public void attributeMaxAge() { + Cookie cookie = new Cookie("Max-Age", null); + Assert.assertEquals("Max-Age", cookie.getName()); + } + + @Test + public void attributeDomain() { + Cookie cookie = new Cookie("Domain", null); + Assert.assertEquals("Domain", cookie.getName()); + } + + @Test + public void attributePath() { + Cookie cookie = new Cookie("Path", null); + Assert.assertEquals("Path", cookie.getName()); + } + + @Test + public void attributeSecure() { + Cookie cookie = new Cookie("Secure", null); + Assert.assertEquals("Secure", cookie.getName()); + } + + @Test + public void attributeHttpOnly() { + Cookie cookie = new Cookie("HttpOnly", null); + Assert.assertEquals("HttpOnly", cookie.getName()); + } + + @Test + public void testGetAttributes01() { + Cookie cookie = new Cookie("name", "value"); + Assert.assertEquals(0, cookie.getAttributes().size()); + } + + @Test + public void testMaxAge01() { + Cookie cookie = new Cookie("name", "value"); + Assert.assertEquals(-1, cookie.getMaxAge()); + + for (int value : new int[] { Integer.MIN_VALUE, -2, -1, 0, 1, 2, Integer.MAX_VALUE}) { + cookie.setMaxAge(value); + Assert.assertEquals(value, cookie.getMaxAge()); + } + } + + @Test + public void testAttribute01() { + Cookie cookie = new Cookie("name", "value"); + cookie.setAttribute("aaa", "bbb"); + Assert.assertEquals("bbb", cookie.getAttribute("aAa")); + cookie.setAttribute("aaa", ""); + Assert.assertEquals("", cookie.getAttribute("aAa")); + cookie.setAttribute("aaa", null); + Assert.assertNull(cookie.getAttribute("aAa")); + } + + @Test(expected = IllegalArgumentException.class) + public void testAttributeInvalid01() { + Cookie cookie = new Cookie("name", "value"); + cookie.setAttribute("a> resHeaders= new HashMap<>(); + int rc = headUrl("http://localhost:" + getPort() + "/", new ByteChunk(), + resHeaders); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(LargeBodyServlet.RESPONSE_LENGTH, + resHeaders.get("Content-Length").get(0)); + } + + + private static class LargeBodyServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String RESPONSE_LENGTH = "12345678901"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setHeader("content-length", RESPONSE_LENGTH); + } + } + + + /* + * Verifies that the same Content-Length is returned for both GET and HEAD + * operations when a Servlet includes content from another Servlet + */ + @Test + public void testBug57602() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + + Bug57602ServletOuter outer = new Bug57602ServletOuter(); + Tomcat.addServlet(ctx, "Bug57602ServletOuter", outer); + ctx.addServletMappingDecoded("/outer", "Bug57602ServletOuter"); + + Bug57602ServletInner inner = new Bug57602ServletInner(); + Tomcat.addServlet(ctx, "Bug57602ServletInner", inner); + ctx.addServletMappingDecoded("/inner", "Bug57602ServletInner"); + + tomcat.start(); + + Map> resHeaders= new CaseInsensitiveKeyMap<>(); + String path = "http://localhost:" + getPort() + "/outer"; + ByteChunk out = new ByteChunk(); + + int rc = getUrl(path, out, resHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + String length = getSingleHeader("Content-Length", resHeaders); + Assert.assertEquals(Long.parseLong(length), out.getLength()); + out.recycle(); + + rc = headUrl(path, out, resHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(0, out.getLength()); + Assert.assertEquals(length, resHeaders.get("Content-Length").get(0)); + + tomcat.stop(); + } + + + @Test + public void testHeadWithChunking() throws Exception { + doTestHead(new ChunkingServlet()); + } + + + @Test + public void testHeadWithResetBufferWriter() throws Exception { + doTestHead(new ResetBufferServlet(true)); + } + + + @Test + public void testHeadWithResetBufferStream() throws Exception { + doTestHead(new ResetBufferServlet(false)); + } + + + @Test + public void testHeadWithResetWriter() throws Exception { + doTestHead(new ResetServlet(true)); + } + + + @Test + public void testHeadWithResetStream() throws Exception { + doTestHead(new ResetServlet(false)); + } + + + @Test + public void testHeadWithNonBlocking() throws Exception { + // Less than buffer size + doTestHead(new NonBlockingWriteServlet(4 * 1024)); + } + + + private void doTestHead(Servlet servlet) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + + Wrapper w = Tomcat.addServlet(ctx, "TestServlet", servlet); + // Not all need/use this but it is simpler to set it for all + w.setAsyncSupported(true); + ctx.addServletMappingDecoded("/test", "TestServlet"); + + tomcat.start(); + + Map> getHeaders = new CaseInsensitiveKeyMap<>(); + String path = "http://localhost:" + getPort() + "/test"; + ByteChunk out = new ByteChunk(); + + int rc = getUrl(path, out, getHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + out.recycle(); + + Map> headHeaders = new HashMap<>(); + rc = headUrl(path, out, headHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // Headers should be the same (apart from Date) + Assert.assertEquals(getHeaders.size(), headHeaders.size()); + for (Map.Entry> getHeader : getHeaders.entrySet()) { + String headerName = getHeader.getKey(); + if ("date".equalsIgnoreCase(headerName)) { + continue; + } + Assert.assertTrue(headerName, headHeaders.containsKey(headerName)); + List getValues = getHeader.getValue(); + List headValues = headHeaders.get(headerName); + Assert.assertEquals(getValues.size(), headValues.size()); + for (String value : getValues) { + Assert.assertTrue(headValues.contains(value)); + } + } + + tomcat.stop(); + } + + + @Test + public void testDoOptions() throws Exception { + doTestDoOptions(new OptionsServlet(), "GET, HEAD, OPTIONS"); + } + + + @Test + public void testDoOptionsSub() throws Exception { + doTestDoOptions(new OptionsServletSub(), "GET, HEAD, POST, OPTIONS"); + } + + + private void doTestDoOptions(Servlet servlet, String expectedAllow) throws Exception{ + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + + // Map the test Servlet + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + Map> resHeaders= new HashMap<>(); + int rc = methodUrl("http://localhost:" + getPort() + "/", new ByteChunk(), + DEFAULT_CLIENT_TIMEOUT_MS, null, resHeaders, "OPTIONS"); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(expectedAllow, resHeaders.get("Allow").get(0)); + } + + + @Test + public void testUnimplementedMethodHttp09() throws Exception { + doTestUnimplementedMethod("0.9"); + } + + + @Test + public void testUnimplementedMethodHttp10() throws Exception { + doTestUnimplementedMethod("1.0"); + } + + + @Test + public void testUnimplementedMethodHttp11() throws Exception { + doTestUnimplementedMethod("1.1"); + } + + + /* + * See org.apache.coyote.http2.TestHttpServlet for the HTTP/2 version of + * this test. It was placed in that package because it needed access to + * package private classes. + */ + + + private void doTestUnimplementedMethod(String httpVersion) { + StringBuilder request = new StringBuilder("PUT /test"); + boolean isHttp09 = "0.9".equals(httpVersion); + boolean isHttp10 = "1.0".equals(httpVersion); + + if (!isHttp09) { + request.append(" HTTP/"); + request.append(httpVersion); + } + request.append(SimpleHttpClient.CRLF); + + request.append("Host: localhost:8080"); + request.append(SimpleHttpClient.CRLF); + + request.append("Connection: close"); + request.append(SimpleHttpClient.CRLF); + + request.append(SimpleHttpClient.CRLF); + + Client client = new Client(request.toString(), "0.9".equals(httpVersion)); + + client.doRequest(); + + if (isHttp09) { + Assert.assertTrue( client.getResponseBody(), client.getResponseBody().contains(" 400 ")); + } else if (isHttp10) { + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + } else { + Assert.assertTrue(client.getResponseLine(), client.isResponse405()); + } + } + + + @Test + public void testTrace() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.getConnector().setAllowTrace(true); + + // No file system docBase required + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + + // Map the test Servlet + Tomcat.addServlet(ctx, "servlet", new SimpleServlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + TraceClient client = new TraceClient(); + client.setPort(getPort()); + client.setRequest(new String[] { + "TRACE / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + "X-aaa: a1, a2" + SimpleHttpClient.CRLF + + "X-aaa: a3" + SimpleHttpClient.CRLF + + "Cookie: c1-v1" + SimpleHttpClient.CRLF + + "Authorization: not-a-real-credential" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF}); + client.setUseContentLength(true); + + client.connect(); + client.sendRequest(); + client.readResponse(true); + + String body = client.getResponseBody(); + + System.out.println(body); + + Assert.assertTrue(client.getResponseLine(), client.isResponse200()); + // Far from perfect but good enough + body = body.toLowerCase(Locale.ENGLISH); + Assert.assertTrue(body.contains("a1")); + Assert.assertTrue(body.contains("a2")); + Assert.assertTrue(body.contains("a3")); + // Sensitive headers (cookies, WWW-Authenticate) must not be reflected + // (since RFC 7231) + Assert.assertFalse(body.contains("cookie")); + Assert.assertFalse(body.contains("authorization")); + + client.disconnect(); + } + + + private static final class TraceClient extends SimpleHttpClient { + + @Override + public boolean isResponseBodyOK() { + return true; + } + } + + + private class Client extends SimpleHttpClient { + + Client(String request, boolean isHttp09) { + setRequest(new String[] {request}); + setUseHttp09(isHttp09); + } + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "TesterServlet", new TesterServlet()); + root.addServletMappingDecoded("/test", "TesterServlet"); + + try { + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + setRequestPause(20); + + // Open connection + connect(); + + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + e.printStackTrace(); + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + return false; + } + } + + + private static class Bug57602ServletOuter extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + pw.println("Header"); + req.getRequestDispatcher("/inner").include(req, resp); + pw.println("Footer"); + } + } + + + private static class Bug57602ServletInner extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + pw.println("Included"); + } + } + + + private static class ChunkingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + // Trigger chunking + pw.write(new char[8192 * 16]); + pw.println("Data"); + } + } + + + private static class ResetBufferServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean useWriter; + + ResetBufferServlet(boolean useWriter) { + this.useWriter = useWriter; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + if (useWriter) { + PrintWriter pw = resp.getWriter(); + pw.write(new char[4 * 1024]); + resp.resetBuffer(); + pw.write(new char[4 * 1024]); + } else { + ServletOutputStream sos = resp.getOutputStream(); + sos.write(new byte [4 * 1024]); + resp.resetBuffer(); + sos.write(new byte [4 * 1024]); + } + } + } + + + private static class ResetServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean useWriter; + + ResetServlet(boolean useWriter) { + this.useWriter = useWriter; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + if (useWriter) { + PrintWriter pw = resp.getWriter(); + resp.addHeader("aaa", "bbb"); + pw.write(new char[4 * 1024]); + resp.resetBuffer(); + resp.addHeader("ccc", "ddd"); + pw.write(new char[4 * 1024]); + } else { + ServletOutputStream sos = resp.getOutputStream(); + resp.addHeader("aaa", "bbb"); + sos.write(new byte [4 * 1024]); + resp.resetBuffer(); + resp.addHeader("ccc", "ddd"); + sos.write(new byte [4 * 1024]); + } + } + } + + + private static class NonBlockingWriteServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final int bytesToWrite; + + NonBlockingWriteServlet(int bytesToWrite) { + this.bytesToWrite = bytesToWrite; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + AsyncContext ac = req.startAsync(req, resp); + ac.setTimeout(3000); + WriteListener wListener = new NonBlockingWriteListener(ac, bytesToWrite); + resp.getOutputStream().setWriteListener(wListener); + } + + private static class NonBlockingWriteListener implements WriteListener { + + private final AsyncContext ac; + private final ServletOutputStream sos; + private int bytesToWrite; + + NonBlockingWriteListener(AsyncContext ac, int bytesToWrite) throws IOException { + this.ac = ac; + this.sos = ac.getResponse().getOutputStream(); + this.bytesToWrite = bytesToWrite; + } + + @Override + public void onWritePossible() throws IOException { + do { + // Write up to 1k a time + int bytesThisTime = Math.min(bytesToWrite, 1024); + sos.write(new byte[bytesThisTime]); + bytesToWrite -= bytesThisTime; + } while (sos.isReady() && bytesToWrite > 0); + + if (sos.isReady() && bytesToWrite == 0) { + ac.complete(); + } + } + + @Override + public void onError(Throwable throwable) { + throwable.printStackTrace(); + } + } + } + + + private static class OptionsServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + pw.print("OK"); + } + } + + + private static class OptionsServletSub extends OptionsServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + doGet(req, resp); + } + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite0.java b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite0.java new file mode 100644 index 0000000..6f33a5a --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite0.java @@ -0,0 +1,57 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestHttpServletDoHeadValidWrite0 extends HttpServletDoHeadBaseTest { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4} {5} {6} {7} {8}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + for (Boolean l : booleans) { + for (Integer buf : BUFFERS) { + for (Boolean w : booleans) { + for (Integer c1 : COUNTS) { + for (ResetType rt : ResetType.values()) { + for (Boolean f : booleans) { + parameterSets.add(new Object[] { + base[0], base[1], + l, buf, w, c1, rt, Integer.valueOf(0), f }); + } + } + } + } + } + } + } + return parameterSets; + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1.java b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1.java new file mode 100644 index 0000000..24b0257 --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1.java @@ -0,0 +1,57 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestHttpServletDoHeadValidWrite1 extends HttpServletDoHeadBaseTest { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4} {5} {6} {7} {8}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + for (Boolean l : booleans) { + for (Integer buf : BUFFERS) { + for (Boolean w : booleans) { + for (Integer c1 : COUNTS) { + for (ResetType rt : ResetType.values()) { + for (Boolean f : booleans) { + parameterSets.add(new Object[] { + base[0], base[1], + l, buf, w, c1, rt, Integer.valueOf(1), f }); + } + } + } + } + } + } + } + return parameterSets; + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1023.java b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1023.java new file mode 100644 index 0000000..725a444 --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1023.java @@ -0,0 +1,57 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestHttpServletDoHeadValidWrite1023 extends HttpServletDoHeadBaseTest { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4} {5} {6} {7} {8}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + for (Boolean l : booleans) { + for (Integer buf : BUFFERS) { + for (Boolean w : booleans) { + for (Integer c1 : COUNTS) { + for (ResetType rt : ResetType.values()) { + for (Boolean f : booleans) { + parameterSets.add(new Object[] { + base[0], base[1], + l, buf, w, c1, rt, Integer.valueOf(1023), f }); + } + } + } + } + } + } + } + return parameterSets; + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1024.java b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1024.java new file mode 100644 index 0000000..5900baa --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1024.java @@ -0,0 +1,57 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestHttpServletDoHeadValidWrite1024 extends HttpServletDoHeadBaseTest { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4} {5} {6} {7} {8}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + for (Boolean l : booleans) { + for (Integer buf : BUFFERS) { + for (Boolean w : booleans) { + for (Integer c1 : COUNTS) { + for (ResetType rt : ResetType.values()) { + for (Boolean f : booleans) { + parameterSets.add(new Object[] { + base[0], base[1], + l, buf, w, c1, rt, Integer.valueOf(1024), f }); + } + } + } + } + } + } + } + return parameterSets; + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1025.java b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1025.java new file mode 100644 index 0000000..d72211a --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite1025.java @@ -0,0 +1,57 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestHttpServletDoHeadValidWrite1025 extends HttpServletDoHeadBaseTest { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4} {5} {6} {7} {8}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + for (Boolean l : booleans) { + for (Integer buf : BUFFERS) { + for (Boolean w : booleans) { + for (Integer c1 : COUNTS) { + for (ResetType rt : ResetType.values()) { + for (Boolean f : booleans) { + parameterSets.add(new Object[] { + base[0], base[1], + l, buf, w, c1, rt, Integer.valueOf(1025), f }); + } + } + } + } + } + } + } + return parameterSets; + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite511.java b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite511.java new file mode 100644 index 0000000..2027472 --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite511.java @@ -0,0 +1,57 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestHttpServletDoHeadValidWrite511 extends HttpServletDoHeadBaseTest { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4} {5} {6} {7} {8}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + for (Boolean l : booleans) { + for (Integer buf : BUFFERS) { + for (Boolean w : booleans) { + for (Integer c1 : COUNTS) { + for (ResetType rt : ResetType.values()) { + for (Boolean f : booleans) { + parameterSets.add(new Object[] { + base[0], base[1], + l, buf, w, c1, rt, Integer.valueOf(511), f }); + } + } + } + } + } + } + } + return parameterSets; + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite512.java b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite512.java new file mode 100644 index 0000000..13ee6c4 --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite512.java @@ -0,0 +1,57 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestHttpServletDoHeadValidWrite512 extends HttpServletDoHeadBaseTest { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4} {5} {6} {7} {8}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + for (Boolean l : booleans) { + for (Integer buf : BUFFERS) { + for (Boolean w : booleans) { + for (Integer c1 : COUNTS) { + for (ResetType rt : ResetType.values()) { + for (Boolean f : booleans) { + parameterSets.add(new Object[] { + base[0], base[1], + l, buf, w, c1, rt, Integer.valueOf(512), f }); + } + } + } + } + } + } + } + return parameterSets; + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite513.java b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite513.java new file mode 100644 index 0000000..0867fe7 --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletDoHeadValidWrite513.java @@ -0,0 +1,57 @@ +/* + * 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 jakarta.servlet.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestHttpServletDoHeadValidWrite513 extends HttpServletDoHeadBaseTest { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4} {5} {6} {7} {8}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + for (Boolean l : booleans) { + for (Integer buf : BUFFERS) { + for (Boolean w : booleans) { + for (Integer c1 : COUNTS) { + for (ResetType rt : ResetType.values()) { + for (Boolean f : booleans) { + parameterSets.add(new Object[] { + base[0], base[1], + l, buf, w, c1, rt, Integer.valueOf(513), f }); + } + } + } + } + } + } + } + return parameterSets; + } +} diff --git a/test/jakarta/servlet/http/TestHttpServletResponseSendError.java b/test/jakarta/servlet/http/TestHttpServletResponseSendError.java new file mode 100644 index 0000000..1b777d1 --- /dev/null +++ b/test/jakarta/servlet/http/TestHttpServletResponseSendError.java @@ -0,0 +1,366 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.ErrorPage; + +/** + * These tests evolved out of a discussion in the Jakarta Servlet project + * regarding the intended behaviour in various error scenarios. Async requests + * and/or async error pages added additional complexity. + */ +@RunWith(Parameterized.class) +public class TestHttpServletResponseSendError extends TomcatBaseTest { + + /* + * Implementation notes: + * Original Request + * - async + * - error in original thread / new thread + * - error before / after startAsync + * - error before / after complete / dispatch + * Error page + * - sync + * - async + * - complete + * - dispatch + */ + + private enum AsyncErrorPoint { + /* + * Thread A is the container thread the processes the original request. + * Thread B is the async thread (may or may not be a container thread) + * that is started by the async processing. + */ + THREAD_A_BEFORE_START_ASYNC, + THREAD_A_AFTER_START_ASYNC, + THREAD_A_AFTER_START_RUNNABLE, + THREAD_B_BEFORE_COMPLETE + /* + * If the error is triggered after Thread B completes async processing + * there is essentially a race condition between thread B making the + * change and the container checking to see if the error flag has been + * set. We can't easily control the execution order here so we don't + * test it. + */ + } + + + @Parameterized.Parameters(name = "{index}: async[{0}], throw[{1}], dispatch[{2}], errorPoint[{3}], useStart[{4}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + for (Boolean async : booleans) { + for (Boolean throwException : booleans) { + if (async.booleanValue()) { + for (Boolean useDispatch : booleans) { + for (AsyncErrorPoint errorPoint : AsyncErrorPoint.values()) { + for (Boolean useStart : booleans) { + if (throwException.booleanValue() && !useStart.booleanValue() && + errorPoint == AsyncErrorPoint.THREAD_B_BEFORE_COMPLETE) { + // Skip this combination as exceptions that occur on application + // managed threads are not visible to the container. + continue; + } + parameterSets.add(new Object[] { async, throwException, useDispatch, + errorPoint, useStart} ); + } + } + } + } else { + // Ignore the async specific parameters + parameterSets.add(new Object[] { async, throwException, Boolean.FALSE, + AsyncErrorPoint.THREAD_A_AFTER_START_ASYNC, Boolean.FALSE} ); + } + } + } + + return parameterSets; + } + + + @Parameter(0) + public boolean async; + @Parameter(1) + public boolean throwException; + @Parameter(2) + public boolean useDispatch; + @Parameter(3) + public AsyncErrorPoint errorPoint; + @Parameter(4) + public boolean useStart; + + + @Test + public void testSendError() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + if (async) { + Wrapper w = Tomcat.addServlet(ctx, "target", + new TesterAsyncServlet(throwException, useDispatch, errorPoint, useStart)); + w.setAsyncSupported(true); + } else { + Tomcat.addServlet(ctx, "target", new TesterServlet(throwException)); + } + ctx.addServletMappingDecoded("/target", "target"); + Tomcat.addServlet(ctx, "dispatch", new TesterDispatchServlet()); + ctx.addServletMappingDecoded("/dispatch", "dispatch"); + + Tomcat.addServlet(ctx, "error599", new ErrorServletStatic599()); + ctx.addServletMappingDecoded("/error599", "error599"); + Tomcat.addServlet(ctx, "errorException", new ErrorServletStaticException()); + ctx.addServletMappingDecoded("/errorException", "errorException"); + + ErrorPage ep1 = new ErrorPage(); + ep1.setErrorCode(599); + ep1.setLocation("/error599"); + ctx.addErrorPage(ep1); + + ErrorPage ep2 = new ErrorPage(); + ep2.setExceptionType(SendErrorException.class.getName()); + ep2.setLocation("/errorException"); + ctx.addErrorPage(ep2); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + + rc = getUrl("http://localhost:" + getPort() + "/target", bc, null, null); + + String body = bc.toString(); + + if (throwException) { + Assert.assertEquals(500, rc); + Assert.assertEquals("FAIL-Exception", body); + } else { + Assert.assertEquals(599, rc); + Assert.assertEquals("FAIL-599", body); + } + } + + + public static class TesterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean throwException; + + public TesterServlet(boolean throwException) { + this.throwException = throwException; + } + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (throwException) { + throw new SendErrorException(); + } else { + // Custom 5xx code so we can detect if the correct error is + // reported + resp.sendError(599); + } + } + } + + + public static class TesterAsyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean throwException; + private final boolean useDispatch; + private final AsyncErrorPoint errorPoint; + private final boolean useStart; + + public TesterAsyncServlet(boolean throwException, boolean useDispatch, AsyncErrorPoint errorPoint, + boolean useStart) { + this.throwException = throwException; + this.useDispatch = useDispatch; + this.errorPoint = errorPoint; + this.useStart = useStart; + } + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (errorPoint == AsyncErrorPoint.THREAD_A_BEFORE_START_ASYNC) { + doError(resp); + } + + AsyncContext ac = req.startAsync(); + ac.setTimeout(2000); + + if (errorPoint == AsyncErrorPoint.THREAD_A_AFTER_START_ASYNC) { + doError(resp); + } + + AsyncRunnable r = new AsyncRunnable(ac, throwException, useDispatch, errorPoint); + + if (useStart) { + ac.start(r); + } else { + Thread t = new Thread(r); + t.start(); + } + + if (errorPoint == AsyncErrorPoint.THREAD_A_AFTER_START_RUNNABLE) { + doError(resp); + } + } + + + private void doError(HttpServletResponse resp) throws IOException { + if (throwException) { + throw new SendErrorException(); + } else { + resp.sendError(599); + } + } + } + + + public static class AsyncRunnable implements Runnable { + + private final AsyncContext ac; + private final boolean throwException; + private final boolean useDispatch; + private final AsyncErrorPoint errorPoint; + + public AsyncRunnable(AsyncContext ac, boolean throwException, boolean useDispatch, + AsyncErrorPoint errorPoint) { + this.ac = ac; + this.throwException = throwException; + this.useDispatch = useDispatch; + this.errorPoint = errorPoint; + } + + @Override + public void run() { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + +/* + if (errorPoint == AsyncErrorPoint.THREAD_B_AFTER_COMPLETE) { + if (useDispatch) { + ac.complete(); + } else { + ac.dispatch("/dispatch"); + } + } + */ + if (throwException) { + throw new SendErrorException(); + } else { + // Custom 5xx code so we can detect if the correct error is + // reported + try { + ((HttpServletResponse) ac.getResponse()).sendError(599); + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (errorPoint == AsyncErrorPoint.THREAD_B_BEFORE_COMPLETE) { + if (useDispatch) { + ac.dispatch("/dispatch"); + } else { + ac.complete(); + } + } + } + } + + + public static class TesterDispatchServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write("DISPATCH"); + } + } + + + public static class SendErrorException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + } + + + public static class ErrorServletStatic599 extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write("FAIL-599"); + } + } + + + public static class ErrorServletStaticException extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write("FAIL-Exception"); + } + } + +} diff --git a/test/jakarta/servlet/http/TesterHttpServletPerformance.java b/test/jakarta/servlet/http/TesterHttpServletPerformance.java new file mode 100644 index 0000000..aeb5cc4 --- /dev/null +++ b/test/jakarta/servlet/http/TesterHttpServletPerformance.java @@ -0,0 +1,70 @@ +/* + * 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 jakarta.servlet.http; + +import java.io.IOException; + +import jakarta.servlet.ServletException; + +import org.junit.Test; + +import org.apache.catalina.connector.RequestFacade; +import org.apache.catalina.filters.TesterHttpServletResponse; + + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterHttpServletPerformance { + + @Test + public void testDoOptions() throws IOException, ServletException{ + TesterServlet testerServlet = new TesterServlet(); + TesterRequest testerRequest = new TesterRequest(false); + TesterHttpServletResponse testerResponse = new TesterHttpServletResponse(); + + long start = System.nanoTime(); + for (int i = 0; i < 10000000; i++) { + testerServlet.doOptions(testerRequest, testerResponse); + } + long end = System.nanoTime(); + + System.out.println("doOptions()" + (end - start) + "ns"); + } + + + private static class TesterServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + } + + + private static class TesterRequest extends RequestFacade { + + private final boolean allowTrace; + + TesterRequest(boolean allowTrace) { + super(null); + this.allowTrace = allowTrace; + } + + @Override + public boolean getAllowTrace() { + return allowTrace; + } + } +} diff --git a/test/jakarta/servlet/jsp/TestPageContext.java b/test/jakarta/servlet/jsp/TestPageContext.java new file mode 100644 index 0000000..116125d --- /dev/null +++ b/test/jakarta/servlet/jsp/TestPageContext.java @@ -0,0 +1,37 @@ +/* + * 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 jakarta.servlet.jsp; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestPageContext extends TomcatBaseTest { + + @Test + public void testBug49196() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + + "/test/bug49nnn/bug49196.jsp"); + + String result = res.toString(); + Assert.assertTrue(result.contains("OK")); + } +} diff --git a/test/jakarta/servlet/jsp/TesterPageContext.java b/test/jakarta/servlet/jsp/TesterPageContext.java new file mode 100644 index 0000000..59814ca --- /dev/null +++ b/test/jakarta/servlet/jsp/TesterPageContext.java @@ -0,0 +1,195 @@ +/* + * 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 jakarta.servlet.jsp; + +import java.io.IOException; +import java.util.Enumeration; + +import jakarta.el.ELContext; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpSession; + +public class TesterPageContext extends PageContext { + + @Override + public void initialize(Servlet servlet, ServletRequest request, + ServletResponse response, String errorPageURL, + boolean needsSession, int bufferSize, boolean autoFlush) + throws IOException, IllegalStateException, IllegalArgumentException { + // NO-OP + } + + @Override + public void release() { + // NO-OP + } + + @Override + public HttpSession getSession() { + // NO-OP + return null; + } + + @Override + public Object getPage() { + // NO-OP + return null; + } + + @Override + public ServletRequest getRequest() { + // NO-OP + return null; + } + + @Override + public ServletResponse getResponse() { + // NO-OP + return null; + } + + @Override + public Exception getException() { + // NO-OP + return null; + } + + @Override + public ServletConfig getServletConfig() { + // NO-OP + return null; + } + + @Override + public ServletContext getServletContext() { + // NO-OP + return null; + } + + @Override + public void forward(String relativeUrlPath) throws ServletException, + IOException { + // NO-OP + + } + + @Override + public void include(String relativeUrlPath) throws ServletException, + IOException { + // NO-OP + } + + @Override + public void include(String relativeUrlPath, boolean flush) + throws ServletException, IOException { + // NO-OP + } + + @Override + public void handlePageException(Exception e) throws ServletException, + IOException { + // NO-OP + } + + @Override + public void handlePageException(Throwable t) throws ServletException, + IOException { + // NO-OP + } + + @Override + public void setAttribute(String name, Object value) { + // NO-OP + } + + @Override + public void setAttribute(String name, Object value, int scope) { + // NO-OP + } + + @Override + public Object getAttribute(String name) { + // NO-OP + return null; + } + + @Override + public Object getAttribute(String name, int scope) { + // NO-OP + return null; + } + + @Override + public Object findAttribute(String name) { + // NO-OP + return null; + } + + @Override + public void removeAttribute(String name) { + // NO-OP + } + + @Override + public void removeAttribute(String name, int scope) { + // NO-OP + } + + @Override + public int getAttributesScope(String name) { + // NO-OP + return 0; + } + + @Override + public Enumeration getAttributeNamesInScope(int scope) { + // NO-OP + return null; + } + + @Override + public JspWriter getOut() { + // NO-OP + return null; + } + + @Override + @Deprecated + public jakarta.servlet.jsp.el.ExpressionEvaluator getExpressionEvaluator() { + // NO-OP + return null; + } + + @Override + public ELContext getELContext() { + // NO-OP + return null; + } + + @Override + @Deprecated + public jakarta.servlet.jsp.el.VariableResolver getVariableResolver() { + // NO-OP + return null; + } + +} diff --git a/test/jakarta/servlet/jsp/el/TestImportELResolver.java b/test/jakarta/servlet/jsp/el/TestImportELResolver.java new file mode 100644 index 0000000..652f04d --- /dev/null +++ b/test/jakarta/servlet/jsp/el/TestImportELResolver.java @@ -0,0 +1,55 @@ +/* + * 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 jakarta.servlet.jsp.el; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestImportELResolver extends TomcatBaseTest { + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=66441 + @Test + public void testImportStaticFields() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug66441.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.contains("EL - Long min value is -9223372036854775808")); + Assert.assertTrue(result.contains("JSP - Long min value is -9223372036854775808")); + } + + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=66582 + @Test + public void testImportStaticFieldFromInterface() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug66582.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + String result = res.toString(); + Assert.assertFalse(result, result.contains("data")); + } +} diff --git a/test/jakarta/servlet/jsp/el/TestScopedAttributeELResolver.java b/test/jakarta/servlet/jsp/el/TestScopedAttributeELResolver.java new file mode 100644 index 0000000..2bb9102 --- /dev/null +++ b/test/jakarta/servlet/jsp/el/TestScopedAttributeELResolver.java @@ -0,0 +1,39 @@ +/* + * 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 jakarta.servlet.jsp.el; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestScopedAttributeELResolver extends TomcatBaseTest { + + @Test + public void testBug49196() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + + "/test/bug6nnnn/bug62453.jsp"); + + String result = res.toString(); + Assert.assertTrue(result, result.contains("
    foo: OK
    ")); + Assert.assertTrue(result, result.contains("
    bar:
    ")); + Assert.assertTrue(result, result.contains("
    baz:
    ")); + } +} diff --git a/test/jakarta/servlet/jsp/el/TesterScopedAttributeELResolverPerformance.java b/test/jakarta/servlet/jsp/el/TesterScopedAttributeELResolverPerformance.java new file mode 100644 index 0000000..3b37ef1 --- /dev/null +++ b/test/jakarta/servlet/jsp/el/TesterScopedAttributeELResolverPerformance.java @@ -0,0 +1,51 @@ +/* + * 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 jakarta.servlet.jsp.el; + +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ELResolver; +import jakarta.el.StandardELContext; +import jakarta.servlet.jsp.JspContext; +import jakarta.servlet.jsp.TesterPageContext; + +import org.junit.Test; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterScopedAttributeELResolverPerformance { + + /* + * With the caching of NotFound responses this test takes ~20ms. Without the + * caching it takes ~6s. + */ + @Test + public void testGetValuePerformance() throws Exception { + + ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + + context.putContext(JspContext.class, new TesterPageContext()); + + ELResolver resolver = new ScopedAttributeELResolver(); + + for (int i = 0; i < 100000; i++) { + resolver.getValue(context, null, "unknown"); + } + } +} diff --git a/test/jakarta/servlet/resources/TestSchemaValidation.java b/test/jakarta/servlet/resources/TestSchemaValidation.java new file mode 100644 index 0000000..dd070fb --- /dev/null +++ b/test/jakarta/servlet/resources/TestSchemaValidation.java @@ -0,0 +1,175 @@ +/* + * 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 jakarta.servlet.resources; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.descriptor.DigesterFactory; +import org.apache.tomcat.util.descriptor.XmlErrorHandler; +import org.apache.tomcat.util.descriptor.XmlIdentifiers; +import org.apache.tomcat.util.descriptor.web.WebRuleSet; +import org.apache.tomcat.util.descriptor.web.WebXml; +import org.apache.tomcat.util.digester.Digester; + +public class TestSchemaValidation { + + @Test + public void testWebapp() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp/WEB-INF/web.xml")); + Assert.assertEquals("6.0", desc.getVersion()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + @Test + public void testWebapp_2_2() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-2.2/WEB-INF/web.xml")); + Assert.assertEquals("2.2", desc.getVersion()); + Assert.assertEquals(XmlIdentifiers.WEB_22_PUBLIC, desc.getPublicId()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + @Test + public void testWebapp_2_3() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-2.3/WEB-INF/web.xml")); + Assert.assertEquals("2.3", desc.getVersion()); + Assert.assertEquals(XmlIdentifiers.WEB_23_PUBLIC, desc.getPublicId()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + @Test + public void testWebapp_2_4() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-2.4/WEB-INF/web.xml")); + Assert.assertEquals("2.4", desc.getVersion()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + @Test + public void testWebapp_2_5() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-2.5/WEB-INF/web.xml")); + Assert.assertEquals("2.5", desc.getVersion()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + @Test + public void testWebapp_3_0() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-3.0/WEB-INF/web.xml")); + Assert.assertEquals("3.0", desc.getVersion()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + @Test + public void testWebapp_3_1() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-3.1/WEB-INF/web.xml")); + Assert.assertEquals("3.1", desc.getVersion()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + @Test + public void testWebapp_4_0() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-4.0/WEB-INF/web.xml")); + Assert.assertEquals("4.0", desc.getVersion()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + @Test + public void testWebapp_5_0() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-5.0/WEB-INF/web.xml")); + Assert.assertEquals("5.0", desc.getVersion()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } + + + @Test + public void testWebapp_6_0() throws Exception { + XmlErrorHandler handler = new XmlErrorHandler(); + Digester digester = DigesterFactory.newDigester( + true, true, new WebRuleSet(false), true); + digester.setErrorHandler(handler); + digester.push(new WebXml()); + WebXml desc = (WebXml) digester.parse( + new File("test/webapp-6.0/WEB-INF/web.xml")); + Assert.assertEquals("6.0", desc.getVersion()); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + } +} diff --git a/test/jakarta/websocket/TesterContainerProviderPerformance.java b/test/jakarta/websocket/TesterContainerProviderPerformance.java new file mode 100644 index 0000000..0e2c718 --- /dev/null +++ b/test/jakarta/websocket/TesterContainerProviderPerformance.java @@ -0,0 +1,59 @@ +/* + * 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 jakarta.websocket; + +import java.util.function.IntConsumer; +import java.util.function.Supplier; + +import org.junit.Test; + +import org.apache.tomcat.unittest.TesterThreadedPerformance; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterContainerProviderPerformance { + + @Test + public void testGetWebSocketContainer() throws Exception { + for (int i = 1; i < 9; i++) { + TesterThreadedPerformance test = + new TesterThreadedPerformance(i, 250000, new TestInstanceSupplier()); + long duration = test.doTest(); + System.out.println(i + " threads completed in " + duration + "ns"); + } + } + + + private static class TestInstanceSupplier implements Supplier { + + @Override + public IntConsumer get() { + return new TestInstance(); + } + } + + + private static class TestInstance implements IntConsumer { + + @Override + public void accept(int value) { + ContainerProvider.getWebSocketContainer(); + } + } +} diff --git a/test/org/apache/catalina/ant/TestDeployTask.java b/test/org/apache/catalina/ant/TestDeployTask.java new file mode 100644 index 0000000..0441d01 --- /dev/null +++ b/test/org/apache/catalina/ant/TestDeployTask.java @@ -0,0 +1,124 @@ +/* + * 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.catalina.ant; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tools.ant.BuildException; + +public class TestDeployTask extends TomcatBaseTest { + + @Test + public void bug58086a() { + DeployTask deployTask = new DeployTask() { + + @Override + public void execute(String command, InputStream istream, String contentType, long contentLength) + throws BuildException { + Assert.assertEquals("/deploy?path=somepath", command); + Assert.assertEquals("application/octet-stream", contentType); + try { + istream.close(); + } catch (IOException e) { + } + } + + }; + + setDefaults(deployTask); + + testExecute(deployTask, "file:./test/deployment/context.war"); + testExecute(deployTask, new File("test/deployment/context.war").toURI().toString()); + testExecute(deployTask, new File("test/deployment/context.war").getAbsolutePath()); + testExecute(deployTask, "jar:" + new File("test/deployment/context.jar").toURI().toString() + "!/context.war"); + testExecute(deployTask, new File("test/deployment/dir with spaces/context.war").toURI().toString()); + testExecute(deployTask, new File("test/deployment/dir with spaces/context.war").getAbsolutePath()); + testExecute(deployTask, "jar:" + new File("test/deployment/dir with spaces/context.jar").toURI().toString() + + "!/context.war"); + testExecute(deployTask, "file:./test/deployment/dir%20with%20spaces/context.war"); + } + + @Test(expected = BuildException.class) + public void bug58086b() { + DeployTask deployTask = new DeployTask(); + setDefaults(deployTask); + testExecute(deployTask, "scheme:./test/deployment/context.war"); + } + + @Test(expected = BuildException.class) + public void bug58086c() { + DeployTask deployTask = new DeployTask(); + setDefaults(deployTask); + testExecute(deployTask, "sc:./test/deployment/context.war"); + } + + @Test(expected = BuildException.class) + public void bug58086d() { + DeployTask deployTask = new DeployTask(); + setDefaults(deployTask); + testExecute(deployTask, "file:./test/deployment/dir with spaces/context.war"); + } + + + @Test + public void bug58086e() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File root = new File("test/deployment"); + tomcat.addWebapp("", root.getAbsolutePath()); + + tomcat.start(); + + DeployTask deployTask = new DeployTask() { + + @Override + public void execute(String command, InputStream istream, String contentType, long contentLength) + throws BuildException { + Assert.assertEquals("/deploy?path=somepath", command); + Assert.assertEquals("application/octet-stream", contentType); + try { + istream.close(); + } catch (IOException e) { + } + } + + }; + + setDefaults(deployTask); + + testExecute(deployTask, "http://localhost:" + getPort() + "/context.war"); + } + + private void setDefaults(DeployTask deployTask) { + deployTask.setUrl("someurl"); + deployTask.setUsername("someuser"); + deployTask.setPassword("somepassword"); + deployTask.setPath("somepath"); + } + + private void testExecute(DeployTask deployTask, String war) { + deployTask.setWar(war); + deployTask.execute(); + } +} diff --git a/test/org/apache/catalina/authenticator/ResponseDescriptor.java b/test/org/apache/catalina/authenticator/ResponseDescriptor.java new file mode 100644 index 0000000..d79a42c --- /dev/null +++ b/test/org/apache/catalina/authenticator/ResponseDescriptor.java @@ -0,0 +1,59 @@ +/* + * 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.catalina.authenticator; + +import java.util.List; +import java.util.Map; + +/** + * This class incorporates test response data + */ +class ResponseDescriptor { + private Map> headers; + private String body; + private int responseCode; + + + public Map> getHeaders() { + return headers; + } + + + public void setHeaders(Map> headers) { + this.headers = headers; + } + + + public String getBody() { + return body; + } + + + public void setBody(String body) { + this.body = body; + } + + + public int getResponseCode() { + return responseCode; + } + + + public void setResponseCode(int responseCode) { + this.responseCode = responseCode; + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/authenticator/TestAuthInfoResponseHeaders.java b/test/org/apache/catalina/authenticator/TestAuthInfoResponseHeaders.java new file mode 100644 index 0000000..bfebfc7 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestAuthInfoResponseHeaders.java @@ -0,0 +1,170 @@ +/* + * 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.catalina.authenticator; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.valves.RemoteIpValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +public class TestAuthInfoResponseHeaders extends TomcatBaseTest { + + private static String USER = "user"; + private static String PWD = "pwd"; + private static String ROLE = "role"; + private static String URI = "/protected"; + private static String CONTEXT_PATH = "/foo"; + private static String CLIENT_AUTH_HEADER = "authorization"; + + /* + * Encapsulate the logic to generate an HTTP header + * for BASIC Authentication. + * Note: only used internally, so no need to validate arguments. + */ + private static final class BasicCredentials { + + private final String method; + private final String username; + private final String password; + private final String credentials; + + private BasicCredentials(String aMethod, + String aUsername, String aPassword) { + method = aMethod; + username = aUsername; + password = aPassword; + String userCredentials = username + ":" + password; + byte[] credentialsBytes = + userCredentials.getBytes(StandardCharsets.ISO_8859_1); + String base64auth = Base64.encodeBase64String(credentialsBytes); + credentials= method + " " + base64auth; + } + + private String getCredentials() { + return credentials; + } + } + + @Test + public void testNoHeaders() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false); + } + + @Test + public void testWithHeaders() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, true); + } + + public void doTest(String user, String pwd, String uri, boolean expectResponseAuthHeaders) + throws Exception { + + if (expectResponseAuthHeaders) { + BasicAuthenticator auth = + (BasicAuthenticator) getTomcatInstance().getHost().findChild( + CONTEXT_PATH).getPipeline().getFirst(); + auth.setSendAuthInfoResponseHeaders(true); + } + getTomcatInstance().start(); + + Map> reqHeaders = new HashMap<>(); + + List auth = new ArrayList<>(); + auth.add(new BasicCredentials("Basic", user, pwd).getCredentials()); + reqHeaders.put(CLIENT_AUTH_HEADER, auth); + + List forwardedFor = new ArrayList<>(); + forwardedFor.add("192.168.0.10"); + List forwardedHost = new ArrayList<>(); + forwardedHost.add("localhost"); + reqHeaders.put("X-Forwarded-For", forwardedFor); + reqHeaders.put("X-Forwarded-Host", forwardedHost); + + Map> respHeaders = new HashMap<>(); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + uri, bc, reqHeaders, + respHeaders); + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + + if (expectResponseAuthHeaders) { + List remoteUsers = respHeaders.get("remote-user"); + Assert.assertNotNull(remoteUsers); + Assert.assertEquals(USER, remoteUsers.get(0)); + List authTypes = respHeaders.get("auth-type"); + Assert.assertNotNull(authTypes); + Assert.assertEquals(HttpServletRequest.BASIC_AUTH, authTypes.get(0)); + } else { + Assert.assertFalse(respHeaders.containsKey("remote-user")); + Assert.assertFalse(respHeaders.containsKey("auth-type")); + } + + bc.recycle(); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + // Configure a context with digest auth and a single protected resource + Tomcat tomcat = getTomcatInstance(); + tomcat.getHost().getPipeline().addValve(new RemoteIpValve()); + + // No file system docBase required + Context ctxt = tomcat.addContext(CONTEXT_PATH, null); + + // Add protected servlet + Tomcat.addServlet(ctxt, "TesterServlet", new TesterServlet()); + ctxt.addServletMappingDecoded(URI, "TesterServlet"); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(URI); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + ctxt.addConstraint(sc); + + // Configure the Realm + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser(USER, PWD); + realm.addUserRole(USER, ROLE); + ctxt.setRealm(realm); + + // Configure the authenticator + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod(HttpServletRequest.BASIC_AUTH); + ctxt.setLoginConfig(lc); + ctxt.getPipeline().addValve(new BasicAuthenticator()); + } +} diff --git a/test/org/apache/catalina/authenticator/TestAuthenticatorBaseCorsPreflight.java b/test/org/apache/catalina/authenticator/TestAuthenticatorBaseCorsPreflight.java new file mode 100644 index 0000000..b0a68dd --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestAuthenticatorBaseCorsPreflight.java @@ -0,0 +1,177 @@ +/* + * 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.catalina.authenticator; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.Realm; +import org.apache.catalina.authenticator.AuthenticatorBase.AllowCorsPreflight; +import org.apache.catalina.filters.AddDefaultCharsetFilter; +import org.apache.catalina.filters.CorsFilter; +import org.apache.catalina.realm.NullRealm; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +@RunWith(Parameterized.class) +public class TestAuthenticatorBaseCorsPreflight extends TomcatBaseTest { + + private static final String ALLOWED_ORIGIN = "http://example.com"; + private static final String EMPTY_ORIGIN = ""; + private static final String INVALID_ORIGIN = "http://%20"; + private static final String SAME_ORIGIN = "http://localhost"; + private static final String ALLOWED_METHOD = "GET"; + private static final String BLOCKED_METHOD = "POST"; + private static final String EMPTY_METHOD = ""; + + @Parameterized.Parameters(name = "{index}: input[{0}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { AllowCorsPreflight.NEVER, "/*", "OPTIONS", null, null, Boolean.FALSE }); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", null, null, Boolean.FALSE }); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", ALLOWED_ORIGIN, ALLOWED_METHOD, Boolean.TRUE }); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", EMPTY_ORIGIN, ALLOWED_METHOD, Boolean.FALSE}); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", INVALID_ORIGIN, ALLOWED_METHOD, Boolean.FALSE }); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", SAME_ORIGIN, ALLOWED_METHOD, Boolean.FALSE }); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "GET", ALLOWED_ORIGIN, ALLOWED_METHOD, Boolean.FALSE }); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", ALLOWED_ORIGIN, BLOCKED_METHOD, Boolean.FALSE }); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", ALLOWED_ORIGIN, EMPTY_METHOD, Boolean.FALSE}); + parameterSets.add(new Object[] { AllowCorsPreflight.ALWAYS, "/*", "OPTIONS", ALLOWED_ORIGIN, null, Boolean.FALSE}); + parameterSets.add(new Object[] { AllowCorsPreflight.FILTER, "/*", "OPTIONS", ALLOWED_ORIGIN, ALLOWED_METHOD, Boolean.TRUE }); + parameterSets.add(new Object[] { AllowCorsPreflight.FILTER, "/x", "OPTIONS", ALLOWED_ORIGIN, ALLOWED_METHOD, Boolean.FALSE }); + + return parameterSets; + } + + @Parameter(0) + public AllowCorsPreflight allowCorsPreflight; + @Parameter(1) + public String filterMapping; + @Parameter(2) + public String method; + @Parameter(3) + public String origin; + @Parameter(4) + public String accessControl; + @Parameter(5) + public boolean allow; + + + @BeforeClass + public static void init() { + // So the test can set the origin header + System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + } + + + @Test + public void test() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctx = tomcat.addContext("", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + LoginConfig loginConfig = new LoginConfig(); + loginConfig.setAuthMethod("BASIC"); + ctx.setLoginConfig(loginConfig); + + BasicAuthenticator basicAuth = new BasicAuthenticator(); + basicAuth.setAllowCorsPreflight(allowCorsPreflight.toString()); + ctx.getPipeline().addValve(basicAuth); + + Realm realm = new NullRealm(); + ctx.setRealm(realm); + + SecurityCollection securityCollection = new SecurityCollection(); + securityCollection.addPattern("/*"); + SecurityConstraint constraint = new SecurityConstraint(); + constraint.setAuthConstraint(true); + constraint.addCollection(securityCollection); + ctx.addConstraint(constraint); + + // For code coverage + FilterDef otherFilter = new FilterDef(); + otherFilter.setFilterName("other"); + otherFilter.setFilterClass(AddDefaultCharsetFilter.class.getName()); + FilterMap otherMap = new FilterMap(); + otherMap.setFilterName("other"); + otherMap.addURLPatternDecoded("/other"); + ctx.addFilterDef(otherFilter); + ctx.addFilterMap(otherMap); + + FilterDef corsFilter = new FilterDef(); + corsFilter.setFilterName("cors"); + corsFilter.setFilterClass(CorsFilter.class.getName()); + corsFilter.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_ORIGINS, ALLOWED_ORIGIN); + corsFilter.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_METHODS, ALLOWED_METHOD); + FilterMap corsFilterMap = new FilterMap(); + corsFilterMap.setFilterName("cors"); + corsFilterMap.addURLPatternDecoded(filterMapping); + ctx.addFilterDef(corsFilter); + ctx.addFilterMap(corsFilterMap); + + tomcat.start(); + + Map> reqHead = new HashMap<>(); + if (origin != null) { + List values = new ArrayList<>(); + if (SAME_ORIGIN.equals(origin)) { + values.add(origin + ":" + getPort()); + } else { + values.add(origin); + } + reqHead.put(CorsFilter.REQUEST_HEADER_ORIGIN, values); + } + if (accessControl != null) { + List values = new ArrayList<>(); + values.add(accessControl); + reqHead.put(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, values); + } + + ByteChunk out = new ByteChunk(); + int rc = methodUrl("http://localhost:" + getPort() + "/target", out, 300000, reqHead, null, method, false); + + if (allow) { + Assert.assertEquals(200, rc); + } else { + Assert.assertEquals(403, rc); + } + } +} diff --git a/test/org/apache/catalina/authenticator/TestBasicAuthParser.java b/test/org/apache/catalina/authenticator/TestBasicAuthParser.java new file mode 100644 index 0000000..03d91ba --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestBasicAuthParser.java @@ -0,0 +1,536 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Test the BasicAuthenticator's BasicCredentials inner class and the + * associated Base64 decoder. + */ +public class TestBasicAuthParser { + + private static final String NICE_METHOD = "Basic"; + private static final String USER_NAME = "userid"; + private static final String PASSWORD = "secret"; + + /* + * test cases with good BASIC Auth credentials - Base64 strings + * can have zero, one or two trailing pad characters + */ + @Test + public void testGoodCredentials() throws Exception { + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + @Test + public void testGoodCredentialsNoPassword() throws Exception { + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, null); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertNull(credentials.getPassword()); + } + + @Test + public void testGoodCrib() throws Exception { + final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA=="; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + @Test + public void testGoodCribUserOnly() throws Exception { + final String BASE64_CRIB = "dXNlcmlk"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertNull(credentials.getPassword()); + } + + @Test + public void testGoodCribOnePad() throws Exception { + final String PASSWORD1 = "secrets"; + final String BASE64_CRIB = "dXNlcmlkOnNlY3JldHM="; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD1, credentials.getPassword()); + } + + /* + * Line breaks are not permitted inside the base64 encoded value. + */ + @Test(expected=IllegalArgumentException.class) + public void testLineWrap() throws Exception { + final String BASE64_CRIB = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldY" + + "WVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0" + + "\n" + "NTY3ODkrL0FBQUFCQkJCQ0NDQ0REREQ="; + final BasicAuthHeader AUTH_HEADER = new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + @SuppressWarnings("unused") + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + } + + /* + * RFC 2045 says the Base64 encoded string should be represented + * as lines of no more than 76 characters. However, RFC 2617 + * says a base64-user-pass token is not limited to 76 char/line. + */ + @Test + public void testGoodCribBase64Big() throws Exception { + // Our decoder accepts a long token without complaint. + final String USER_LONG = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz0123456789+/AAAABBBBCCCC" + + "DDDD"; // 80 characters + final String BASE64_CRIB = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldY" + + "WVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0" + + "NTY3ODkrL0FBQUFCQkJCQ0NDQ0REREQ="; // no new line + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_LONG, credentials.getUsername()); + } + + + /* + * verify the parser follows RFC2617 by treating the auth-scheme + * token as case-insensitive. + */ + @Test + public void testAuthMethodCaseBasic() throws Exception { + final String METHOD = "bAsIc"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(METHOD, USER_NAME, PASSWORD); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + /* + * Confirm the Basic parser rejects an invalid authentication method. + */ + @Test(expected = IllegalArgumentException.class) + public void testAuthMethodBadMethod() throws Exception { + final String METHOD = "BadMethod"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(METHOD, USER_NAME, PASSWORD); + @SuppressWarnings("unused") + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials(AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + } + + /* + * Confirm the Basic parser allows exactly one space after the authentication method. + */ + @Test(expected=IllegalArgumentException.class) + public void testAuthMethodExtraLeadingSpace() throws Exception { + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD + " ", USER_NAME, PASSWORD); + @SuppressWarnings("unused") + final BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials(AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + } + + + /* + * invalid decoded credentials cases + */ + @Test + public void testWrongPassword() throws Exception { + final String PWD_WRONG = "wrong"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, PWD_WRONG); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertNotSame(PASSWORD, credentials.getPassword()); + } + + @Test + public void testMissingUsername() throws Exception { + final String EMPTY_USER_NAME = ""; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, EMPTY_USER_NAME, PASSWORD); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(EMPTY_USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + @Test + public void testShortUsername() throws Exception { + final String SHORT_USER_NAME = "a"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, SHORT_USER_NAME, PASSWORD); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(SHORT_USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + @Test + public void testShortPassword() throws Exception { + final String SHORT_PASSWORD = "a"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, SHORT_PASSWORD); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(SHORT_PASSWORD, credentials.getPassword()); + } + + @Test + public void testPasswordHasSpaceEmbedded() throws Exception { + final String PASSWORD_SPACE = "abc def"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_SPACE); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD_SPACE, credentials.getPassword()); + } + + @Test + public void testPasswordHasColonEmbedded() throws Exception { + final String PASSWORD_COLON = "abc:def"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD_COLON, credentials.getPassword()); + } + + @Test + public void testPasswordHasColonLeading() throws Exception { + final String PASSWORD_COLON = ":abcdef"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD_COLON, credentials.getPassword()); + } + + @Test + public void testPasswordHasColonTrailing() throws Exception { + final String PASSWORD_COLON = "abcdef:"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD_COLON); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD_COLON, credentials.getPassword()); + } + + /* + * Confirm the Basic parser does not tolerate excess white space after the base64 blob. + */ + @Test(expected=IllegalArgumentException.class) + public void testAuthMethodExtraTrailingSpace() throws Exception { + final BasicAuthHeader AUTH_HEADER = new BasicAuthHeader(NICE_METHOD, USER_NAME, PASSWORD, " "); + @SuppressWarnings("unused") + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials(AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + } + + /* + * Confirm the Basic parser does not tolerate excess white space around the username inside the base64 blob. + */ + @Test + public void testUserExtraSpace() throws Exception { + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, " " + USER_NAME + " ", PASSWORD); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertNotEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(USER_NAME, credentials.getUsername().trim()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + /* + * Confirm the Basic parser tolerates excess white space around the user name inside the base64 blob when + * trimCredentials is enabled. + */ + @Test + public void testUserExtraSpaceWithTrimCredentials() throws Exception { + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, " " + USER_NAME + " ", PASSWORD); + @SuppressWarnings("deprecation") + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + /* + * Confirm the Basic parser does not tolerate excess white space around the password within the base64 blob. + */ + @Test + public void testPasswordExtraSpace() throws Exception { + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, " " + PASSWORD + " "); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertNotEquals(PASSWORD, credentials.getPassword()); + Assert.assertEquals(PASSWORD, credentials.getPassword().trim()); + } + + /* + * Confirm the Basic parser tolerates excess white space around the password inside the base64 blob when + * trimCredentials is enabled. + */ + @Test + public void testPasswordExtraSpaceWithTrimCredentials() throws Exception { + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, USER_NAME, " " + PASSWORD + " "); + @SuppressWarnings("deprecation") + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8, true); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + + /* + * invalid base64 string tests + * + * Refer to + * - RFC 7617 (Basic Auth) + * - RFC 4648 (base 64) + */ + + /* + * non-trailing "=" is illegal and will be rejected by the parser + */ + @Test(expected = IllegalArgumentException.class) + public void testBadBase64InlineEquals() throws Exception { + final String BASE64_CRIB = "dXNlcmlkOnNlY3J=dAo="; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + @SuppressWarnings("unused") // Exception will be thrown. + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + } + + /* + * "-" is not a legal base64 character. The RFC says it must be + * ignored by the decoder. This will scramble the decoded string + * and eventually result in an IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void testBadBase64Char() throws Exception { + final String BASE64_CRIB = "dXNlcmlkOnNl-3JldHM="; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + @SuppressWarnings("unused") + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + } + + /* + * "-" is not a legal base64 character. + */ + @Test(expected=IllegalArgumentException.class) + public void testBadBase64LastChar() throws Exception { + final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA-="; + final BasicAuthHeader AUTH_HEADER = new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + @SuppressWarnings("unused") + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials(AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + } + + /* + * The trailing third "=" is illegal. + */ + @Test(expected=IllegalArgumentException.class) + public void testBadBase64TooManyEquals() throws Exception { + final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA==="; + final BasicAuthHeader AUTH_HEADER = new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + @SuppressWarnings("unused") + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials(AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + } + + /* + * there should be a multiple of 4 encoded characters. However, + * the RFC says the decoder should pad the input string with + * zero bits out to the next boundary. An error will not be detected + * unless the payload has been damaged in some way - this + * particular crib has no damage. + */ + @Test + public void testBadBase64BadLength() throws Exception { + final String BASE64_CRIB = "dXNlcmlkOnNlY3JldA"; + final BasicAuthHeader AUTH_HEADER = + new BasicAuthHeader(NICE_METHOD, BASE64_CRIB); + BasicAuthenticator.BasicCredentials credentials = + new BasicAuthenticator.BasicCredentials( + AUTH_HEADER.getHeader(), StandardCharsets.UTF_8); + Assert.assertEquals(USER_NAME, credentials.getUsername()); + Assert.assertEquals(PASSWORD, credentials.getPassword()); + } + + + /* + * Encapsulate the logic to generate an HTTP header + * for BASIC Authentication. + * Note: only used internally, so no need to validate arguments. + */ + private static final class BasicAuthHeader { + + private static final byte[] HEADER = + "authorization: ".getBytes(StandardCharsets.ISO_8859_1); + private ByteChunk authHeader; + private int initialOffset = 0; + + /* + * This method creates a valid base64 blob + */ + private BasicAuthHeader(String method, String username, + String password) { + this(method, username, password, null); + } + + /* + * This method creates valid base64 blobs with optional trailing data + */ + private BasicAuthHeader(String method, String username, + String password, String extraBlob) { + prefix(method); + + String userCredentials = + ((password == null) || (password.length() < 1)) + ? username + : username + ":" + password; + byte[] credentialsBytes = + userCredentials.getBytes(StandardCharsets.ISO_8859_1); + String base64auth = Base64.getEncoder().encodeToString(credentialsBytes); + byte[] base64Bytes = + base64auth.getBytes(StandardCharsets.ISO_8859_1); + + byte[] extraBytes = + ((extraBlob == null) || (extraBlob.length() < 1)) + ? null : + extraBlob.getBytes(StandardCharsets.ISO_8859_1); + + try { + authHeader.append(base64Bytes, 0, base64Bytes.length); + if (extraBytes != null) { + authHeader.append(extraBytes, 0, extraBytes.length); + } + } + catch (IOException ioe) { + throw new IllegalStateException("unable to extend ByteChunk:" + + ioe.getMessage()); + } + // emulate tomcat server - offset points to method in header + authHeader.setOffset(initialOffset); + } + + /* + * This method allows injection of cribbed base64 blobs, + * without any validation of the contents + */ + private BasicAuthHeader(String method, String fakeBase64) { + prefix(method); + + byte[] fakeBytes = fakeBase64.getBytes(StandardCharsets.ISO_8859_1); + + try { + authHeader.append(fakeBytes, 0, fakeBytes.length); + } + catch (IOException ioe) { + throw new IllegalStateException("unable to extend ByteChunk:" + + ioe.getMessage()); + } + // emulate tomcat server - offset points to method in header + authHeader.setOffset(initialOffset); + } + + /* + * construct the common authorization header + */ + private void prefix(String method) { + authHeader = new ByteChunk(); + authHeader.setBytes(HEADER, 0, HEADER.length); + initialOffset = HEADER.length; + + String methodX = method + " "; + byte[] methodBytes = methodX.getBytes(StandardCharsets.ISO_8859_1); + + try { + authHeader.append(methodBytes, 0, methodBytes.length); + } + catch (IOException ioe) { + throw new IllegalStateException("unable to extend ByteChunk:" + + ioe.getMessage()); + } + } + + private ByteChunk getHeader() { + return authHeader; + } + } +} diff --git a/test/org/apache/catalina/authenticator/TestDigestAuthenticator.java b/test/org/apache/catalina/authenticator/TestDigestAuthenticator.java new file mode 100644 index 0000000..60cf849 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestDigestAuthenticator.java @@ -0,0 +1,388 @@ +/* + * 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.catalina.authenticator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.unittest.TesterRequest; +import org.apache.tomcat.unittest.TesterServletContext; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; + +public class TestDigestAuthenticator extends TomcatBaseTest { + + private static String USER = "user"; + private static String PWD = "pwd"; + private static String ROLE = "role"; + private static String URI = "/protected"; + private static String QUERY = "?foo=bar"; + private static String CONTEXT_PATH = "/foo"; + private static String CLIENT_AUTH_HEADER = "authorization"; + private static String REALM = "TestRealm"; + private static String CNONCE = "cnonce"; + private static String NC1 = "00000001"; + private static String NC2 = "00000002"; + private static String QOP = "auth"; + + + @Test + public void bug54521() throws LifecycleException { + DigestAuthenticator digestAuthenticator = new DigestAuthenticator(); + TesterContext context = new TesterContext(); + context.setServletContext(new TesterServletContext()); + digestAuthenticator.setContainer(context); + digestAuthenticator.start(); + Request request = new TesterRequest(); + final int count = 1000; + + Set nonces = new HashSet<>(); + + for (int i = 0; i < count; i++) { + nonces.add(digestAuthenticator.generateNonce(request)); + } + + Assert.assertEquals(count, nonces.size()); + } + + + @Test + public void testAllValid() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + NC1, NC2, CNONCE, QOP, true, true); + } + + @Test + public void testValidNoQop() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + null, null, null, null, true, true); + } + + @Test + public void testValidQuery() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI + QUERY, false, true, REALM, true, + true, NC1, NC2, CNONCE, QOP, true, true); + } + + @Test + public void testInvalidUriFail() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, true, true, REALM, true, true, + NC1, NC2, CNONCE, QOP, false, false); + } + + @Test + public void testInvalidUriPass() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, true, false, REALM, true, true, + NC1, NC2, CNONCE, QOP, true, true); + } + + @Test + public void testInvalidRealm() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, "null", true, true, + NC1, NC2, CNONCE, QOP, false, false); + } + + @Test + public void testInvalidNonce() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, false, true, + NC1, NC2, CNONCE, QOP, false, true); + } + + @Test + public void testInvalidOpaque() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, false, + NC1, NC2, CNONCE, QOP, false, true); + } + + @Test + public void testInvalidNc1() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + "null", null, CNONCE, QOP, false, false); + } + + @Test + public void testInvalidQop() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + NC1, NC2, CNONCE, "null", false, false); + } + + @Test + public void testInvalidQopCombo1() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + NC1, NC2, CNONCE, null, false, false); + } + + @Test + public void testInvalidQopCombo2() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + NC1, NC2, null, QOP, false, false); + } + + @Test + public void testInvalidQopCombo3() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + NC1, NC2, null, null, false, false); + } + + @Test + public void testInvalidQopCombo4() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + null, null, CNONCE, QOP, false, false); + } + + @Test + public void testInvalidQopCombo5() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + null, null, CNONCE, null, false, false); + } + + @Test + public void testInvalidQopCombo6() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + null, null, null, QOP, false, false); + } + + @Test + public void testReplay() throws Exception { + doTest(USER, PWD, CONTEXT_PATH + URI, false, true, REALM, true, true, + NC1, NC1, CNONCE, QOP, true, false); + } + + public void doTest(String user, String pwd, String uri, boolean breakUri, + boolean validateUri, String realm, boolean useServerNonce, + boolean useServerOpaque, String nc1, String nc2, String cnonce, + String qop, boolean req2expect200, boolean req3expect200) + throws Exception { + + if (!validateUri) { + DigestAuthenticator auth = + (DigestAuthenticator) getTomcatInstance().getHost().findChild( + CONTEXT_PATH).getPipeline().getFirst(); + auth.setValidateUri(false); + } + getTomcatInstance().start(); + + String digestUri; + if (breakUri) { + digestUri = "/broken" + uri; + } else { + digestUri = uri; + } + List auth = new ArrayList<>(); + auth.add(buildDigestResponse(user, pwd, digestUri, realm, "null", + "null", nc1, cnonce, qop)); + Map> reqHeaders = new HashMap<>(); + reqHeaders.put(CLIENT_AUTH_HEADER, auth); + + Map> respHeaders = new HashMap<>(); + + // The first request will fail - but we need to extract the nonce + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + uri, bc, reqHeaders, + respHeaders); + Assert.assertEquals(401, rc); + Assert.assertTrue(bc.getLength() > 0); + bc.recycle(); + + // Second request should succeed (if we use the server nonce) + auth.clear(); + if (useServerNonce) { + if (useServerOpaque) { + auth.add(buildDigestResponse(user, pwd, digestUri, realm, + getNonce(respHeaders), getOpaque(respHeaders), nc1, + cnonce, qop)); + } else { + auth.add(buildDigestResponse(user, pwd, digestUri, realm, + getNonce(respHeaders), "null", nc1, cnonce, qop)); + } + } else { + auth.add(buildDigestResponse(user, pwd, digestUri, realm, + "null", getOpaque(respHeaders), nc1, cnonce, QOP)); + } + rc = getUrl("http://localhost:" + getPort() + uri, bc, reqHeaders, + null); + + if (req2expect200) { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + } else { + Assert.assertEquals(401, rc); + Assert.assertTrue(bc.getLength() > 0); + } + + // Third request should succeed if we increment nc + auth.clear(); + bc.recycle(); + auth.add(buildDigestResponse(user, pwd, digestUri, realm, + getNonce(respHeaders), getOpaque(respHeaders), nc2, cnonce, + qop)); + rc = getUrl("http://localhost:" + getPort() + uri, bc, reqHeaders, + null); + + if (req3expect200) { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + } else { + Assert.assertEquals(401, rc); + Assert.assertTrue(bc.getLength() > 0); + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + // Configure a context with digest auth and a single protected resource + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctxt = tomcat.addContext(CONTEXT_PATH, null); + + // Add protected servlet + Tomcat.addServlet(ctxt, "TesterServlet", new TesterServlet()); + ctxt.addServletMappingDecoded(URI, "TesterServlet"); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(URI); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + ctxt.addConstraint(sc); + + // Configure the Realm + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser(USER, PWD); + realm.addUserRole(USER, ROLE); + ctxt.setRealm(realm); + + // Configure the authenticator + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("DIGEST"); + lc.setRealmName(REALM); + ctxt.setLoginConfig(lc); + ctxt.getPipeline().addValve(new DigestAuthenticator()); + } + + protected static String getNonce(Map> respHeaders) { + List authHeaders = + respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME); + // Assume there is only one + String authHeader = authHeaders.iterator().next(); + + int start = authHeader.indexOf("nonce=\"") + 7; + int end = authHeader.indexOf('\"', start); + return authHeader.substring(start, end); + } + + protected static String getOpaque(Map> respHeaders) { + List authHeaders = + respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME); + // Assume there is only one + String authHeader = authHeaders.iterator().next(); + + int start = authHeader.indexOf("opaque=\"") + 8; + int end = authHeader.indexOf('\"', start); + return authHeader.substring(start, end); + } + + /* + * Notes from RFC2617 + * H(data) = MD5(data) + * KD(secret, data) = H(concat(secret, ":", data)) + * A1 = unq(username-value) ":" unq(realm-value) ":" passwd + * A2 = Method ":" digest-uri-value + * request-digest = <"> < KD ( H(A1), unq(nonce-value) + ":" nc-value + ":" unq(cnonce-value) + ":" unq(qop-value) + ":" H(A2) + ) <"> + */ + private static String buildDigestResponse(String user, String pwd, + String uri, String realm, String nonce, String opaque, String nc, + String cnonce, String qop) { + + String a1 = user + ":" + realm + ":" + pwd; + String a2 = "GET:" + uri; + + String digestA1 = digest(a1); + String digestA2 = digest(a2); + + String response; + if (qop == null) { + response = digestA1 + ":" + nonce + ":" + digestA2; + } else { + response = digestA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + + qop + ":" + digestA2; + } + + String md5response = digest(response); + + StringBuilder auth = new StringBuilder(); + auth.append("Digest username=\""); + auth.append(user); + auth.append("\", realm=\""); + auth.append(realm); + auth.append("\", nonce=\""); + auth.append(nonce); + auth.append("\", uri=\""); + auth.append(uri); + auth.append("\", opaque=\""); + auth.append(opaque); + auth.append("\", response=\""); + auth.append(md5response); + auth.append("\""); + if (qop != null) { + auth.append(", qop="); + auth.append(qop); + auth.append(""); + } + if (nc != null) { + auth.append(", nc="); + auth.append(nc); + } + if (cnonce != null) { + auth.append(", cnonce=\""); + auth.append(cnonce); + auth.append("\""); + } + + return auth.toString(); + } + + private static String digest(String input) { + return HexUtils.toHexString(ConcurrentMessageDigest.digestMD5(input.getBytes())); + } +} diff --git a/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java b/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java new file mode 100644 index 0000000..8534552 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java @@ -0,0 +1,279 @@ +/* + * 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.catalina.authenticator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.authenticator.DigestAuthenticator.AuthDigest; +import org.apache.catalina.realm.LockOutRealm; +import org.apache.catalina.realm.MessageDigestCredentialHandler; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; + +@RunWith(Parameterized.class) +public class TestDigestAuthenticatorAlgorithms extends TomcatBaseTest { + + private static final String USER = "user"; + private static final String PASSWORD = "password"; + + private static final String URI = "/protected"; + + private static String REALM_NAME = "TestRealm"; + private static String CNONCE = "cnonce"; + + private static final List> ALGORITHM_PERMUTATIONS = new ArrayList<>(); + static { + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5, AuthDigest.SHA_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5, AuthDigest.SHA_256, AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5, AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5, AuthDigest.SHA_512_256, AuthDigest.SHA_256)); + + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256, AuthDigest.MD5)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256, AuthDigest.MD5, AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256, AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256, AuthDigest.SHA_512_256, AuthDigest.MD5)); + + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256, AuthDigest.MD5)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256, AuthDigest.MD5, AuthDigest.SHA_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256, AuthDigest.SHA_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256, AuthDigest.SHA_256, AuthDigest.MD5)); + } + + @Parameterized.Parameters(name = "{index}: Algorithms[{0}], Algorithm[{1}], PwdDigest[{2}], AuthExpected[{3}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + for (List algorithmPermutation : ALGORITHM_PERMUTATIONS) { + StringBuilder algorithms = new StringBuilder(); + StringUtils.join(algorithmPermutation, ',', (x) -> x.getRfcName(), algorithms); + for (AuthDigest algorithm : AuthDigest.values()) { + boolean authExpected = algorithmPermutation.contains(algorithm); + for (Boolean digestPassword : booleans) { + String user; + if (digestPassword.booleanValue()) { + user = USER + "-" + algorithm; + } else { + user = USER; + } + parameterSets.add(new Object[] { algorithms.toString(), algorithm, digestPassword, user, Boolean.valueOf(authExpected) }); + } + } + } + + return parameterSets; + } + + @Parameter(0) + public String serverAlgorithms; + + @Parameter(1) + public AuthDigest clientAlgorithm; + + @Parameter(2) + public boolean digestPassword; + + @Parameter(3) + public String user; + + @Parameter(4) + public boolean authExpected; + + + @Test + public void testDigestAuthentication() throws Exception { + // Make sure client algorithm is available for digests + ConcurrentMessageDigest.init(clientAlgorithm.getJavaName()); + + // Configure a context with digest authentication and a single protected resource + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctxt = getProgrammaticRootContext(); + + // Add protected servlet + Tomcat.addServlet(ctxt, "TesterServlet", new TesterServlet()); + ctxt.addServletMappingDecoded(URI, "TesterServlet"); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(URI); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole("role"); + sc.addCollection(collection); + ctxt.addConstraint(sc); + + // Configure the Realm + TesterMapRealm realm = new TesterMapRealm(); + String password; + if (digestPassword) { + MessageDigestCredentialHandler mdch = new MessageDigestCredentialHandler(); + mdch.setAlgorithm(clientAlgorithm.getJavaName()); + mdch.setSaltLength(0); + realm.setCredentialHandler(mdch); + password = mdch.mutate(user + ":" + REALM_NAME + ":" + PASSWORD); + } else { + password = PASSWORD; + } + realm.addUser(user, password); + realm.addUserRole(user, "role"); + + LockOutRealm lockOutRealm = new LockOutRealm(); + lockOutRealm.addRealm(realm); + ctxt.setRealm(lockOutRealm); + + // Configure the authenticator + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("DIGEST"); + lc.setRealmName(REALM_NAME); + ctxt.setLoginConfig(lc); + DigestAuthenticator digestAuthenticator = new DigestAuthenticator(); + digestAuthenticator.setAlgorithms(serverAlgorithms); + ctxt.getPipeline().addValve(digestAuthenticator); + + tomcat.start(); + + // The first request will always fail - but we need the challenge + Map> respHeaders = new HashMap<>(); + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + URI, bc, respHeaders); + Assert.assertEquals(401, rc); + Assert.assertTrue(bc.getLength() > 0); + bc.recycle(); + + // Second request will succeed depending on client and server algorithms + List auth = new ArrayList<>(); + auth.add(buildDigestResponse(user, PASSWORD, URI, REALM_NAME, clientAlgorithm, + respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME), "00000001", CNONCE, DigestAuthenticator.QOP)); + Map> reqHeaders = new HashMap<>(); + reqHeaders.put("authorization", auth); + rc = getUrl("http://localhost:" + getPort() + URI, bc, reqHeaders, null); + + if (authExpected) { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + } else { + Assert.assertEquals(401, rc); + } + } + + + protected static String getNonce(String authHeader) { + int start = authHeader.indexOf("nonce=\"") + 7; + int end = authHeader.indexOf('\"', start); + return authHeader.substring(start, end); + } + + + protected static String getOpaque(String authHeader) { + int start = authHeader.indexOf("opaque=\"") + 8; + int end = authHeader.indexOf('\"', start); + return authHeader.substring(start, end); + } + + + private static String buildDigestResponse(String user, String pwd, String uri, String realm, AuthDigest algorithm, + List authHeaders, String nc, String cnonce, String qop) { + + // Find auth header with correct algorithm + String nonce = null; + String opaque = null; + for (String authHeader : authHeaders) { + nonce = getNonce(authHeader); + opaque = getOpaque(authHeader); + if (authHeader.contains("algorithm=" + algorithm.getRfcName())) { + break; + } + } + if (nonce == null || opaque == null) { + Assert.fail(); + } + + String a1 = user + ":" + realm + ":" + pwd; + String a2 = "GET:" + uri; + + String digestA1 = digest(algorithm.getJavaName(), a1); + String digestA2 = digest(algorithm.getJavaName(), a2); + + String response; + if (qop == null) { + response = digestA1 + ":" + nonce + ":" + digestA2; + } else { + response = digestA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + digestA2; + } + + String digestResponse = digest(algorithm.getJavaName(), response); + + StringBuilder auth = new StringBuilder(); + auth.append("Digest username=\""); + auth.append(user); + auth.append("\", realm=\""); + auth.append(realm); + auth.append("\", algorithm="); + auth.append(algorithm.getRfcName()); + auth.append(", nonce=\""); + auth.append(nonce); + auth.append("\", uri=\""); + auth.append(uri); + auth.append("\", opaque=\""); + auth.append(opaque); + auth.append("\", response=\""); + auth.append(digestResponse); + auth.append("\""); + if (qop != null) { + auth.append(", qop="); + auth.append(qop); + auth.append(""); + } + if (nc != null) { + auth.append(", nc="); + auth.append(nc); + } + if (cnonce != null) { + auth.append(", cnonce=\""); + auth.append(cnonce); + auth.append("\""); + } + + return auth.toString(); + } + + private static String digest(String algorithm, String input) { + return HexUtils.toHexString(ConcurrentMessageDigest.digest(algorithm, input.getBytes())); + } +} diff --git a/test/org/apache/catalina/authenticator/TestFormAuthenticatorA.java b/test/org/apache/catalina/authenticator/TestFormAuthenticatorA.java new file mode 100644 index 0000000..2671b3e --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestFormAuthenticatorA.java @@ -0,0 +1,711 @@ +/* + * 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.catalina.authenticator; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.StringTokenizer; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Valve; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.websocket.server.WsContextListener; + +/* + * Test FORM authentication for sessions that do and do not use cookies. + * + * 1. A client that can accept and respond to a Set-Cookie for JSESSIONID + * will be able to maintain its authenticated session, no matter whether + * the session ID is changed once, many times, or not at all. + * + * 2. A client that cannot accept cookies will only be able to maintain a + * persistent session IF the server sends the correct (current) jsessionid + * as a path parameter appended to ALL urls within its response. That is + * achievable with servlets, jsps, jstl (all of which which can ask for an + * encoded url to be inserted into the dynamic web page). It cannot work + * with static HTML. + * note: this test class uses the Tomcat sample jsps, which conform. + * + * 3. Therefore, any webapp that MIGHT need to authenticate a client that + * does not accept cookies MUST generate EVERY protected resource url + * dynamically (so that it will include the current session ID). + * + * 4. Any webapp that cannot satisfy case 3 MUST turn off + * changeSessionIdOnAuthentication for its Context and thus degrade the + * session fixation protection for ALL of its clients. + * note from MarkT: Not sure I agree with this. If the URLs aren't + * being encoded, then the session is going to break regardless of + * whether or not the session ID changes. + * + * Unlike a "proper browser", this unit test class does a quite lot of + * screen-scraping and cheating of headers and urls (not very elegant, + * but it makes no claims to generality). + * + */ +public class TestFormAuthenticatorA extends TomcatBaseTest { + + // these should really be singletons to be type-safe, + // we are in a unit test and don't need to paranoid. + protected static final boolean USE_100_CONTINUE = true; + protected static final boolean NO_100_CONTINUE = !USE_100_CONTINUE; + + protected static final boolean CLIENT_USE_COOKIES = true; + protected static final boolean CLIENT_NO_COOKIES = !CLIENT_USE_COOKIES; + + protected static final boolean CLIENT_USE_HTTP_11 = true; + protected static final boolean CLIENT_USE_HTTP_10 = !CLIENT_USE_HTTP_11; + + protected static final boolean SERVER_USE_COOKIES = true; + protected static final boolean SERVER_NO_COOKIES = !SERVER_USE_COOKIES; + + protected static final boolean SERVER_CHANGE_SESSID = true; + protected static final boolean SERVER_FREEZE_SESSID = !SERVER_CHANGE_SESSID; + + // minimum session timeout + private static final int SHORT_SESSION_TIMEOUT_SECS = 1; + private static final long TIMEOUT_DELAY_MSECS = ((SHORT_SESSION_TIMEOUT_SECS + 10) * 1000); + + private FormAuthClient client; + + // first, a set of tests where the server uses a cookie to carry + // the current session ID during and after authentication, and + // the client is prepared to return cookies with each request + + @Test + public void testGetWithCookies() throws Exception { + doTest("GET", "GET", NO_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + + + + // next, a set of tests where the server Context is configured to never + // use cookies and the session ID is only carried as a url path parameter + + // Bug 53584 + @Test + public void testGetNoServerCookies() throws Exception { + doTest("GET", "GET", NO_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID); + } + + + + + // next, a set of tests where the server Context uses cookies, + // but the client refuses to return them and tries to use + // the session ID if carried as a url path parameter + + @Test + public void testGetNoClientCookies() throws Exception { + doTest("GET", "GET", NO_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + + // finally, a set of tests to explore quirky situations + // but there is not need to replicate all the scenarios above. + + @Test + public void testNoChangedSessidWithCookies() throws Exception { + doTest("GET", "GET", NO_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_USE_COOKIES, + SERVER_FREEZE_SESSID); + } + + @Test + public void testNoChangedSessidWithoutCookies() throws Exception { + doTest("GET", "GET", NO_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_USE_COOKIES, + SERVER_FREEZE_SESSID); + } + + @Test + public void testTimeoutWithoutCookies() throws Exception { + String protectedUri = doTest("GET", "GET", NO_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_USE_COOKIES, + SERVER_FREEZE_SESSID); + + // Force session to expire one second from now + Context context = (Context) getTomcatInstance().getHost().findChildren()[0]; + forceSessionMaxInactiveInterval(context, SHORT_SESSION_TIMEOUT_SECS); + + // wait long enough for my session to expire + Thread.sleep(TIMEOUT_DELAY_MSECS); + + // then try to continue using the expired session to get the + // protected resource once more. + // should get login challenge or timeout status 408 + doTestProtected("GET", protectedUri, NO_100_CONTINUE, + FormAuthClient.LOGIN_REQUIRED, 1); + } + + // HTTP 1.0 test + @Test + public void testGetWithCookiesHttp10() throws Exception { + doTest("GET", "GET", NO_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID, + CLIENT_USE_HTTP_10); + } + + + @Test + public void testSelectedMethods() throws Exception { + + FormAuthClientSelectedMethods client = + new FormAuthClientSelectedMethods(true, true, true, true); + + // First request for protected resource gets the login page + client.doResourceRequest("PUT", true, "/test?" + + SelectedMethodsServlet.PARAM + "=" + + SelectedMethodsServlet.VALUE, null); + Assert.assertTrue(client.getResponseLine(), client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + String originalSessionId = client.getSessionId(); + client.reset(); + + // Second request replies to the login challenge + client.doResourceRequest("POST", true, "/test/j_security_check", + FormAuthClientBase.LOGIN_REPLY); + Assert.assertTrue("login failed " + client.getResponseLine(), + client.isResponse303()); + Assert.assertTrue(client.isResponseBodyOK()); + String redirectUri = client.getRedirectUri(); + client.reset(); + + // Third request - the login was successful so + // follow the redirect to the protected resource + client.doResourceRequest("GET", true, redirectUri, null); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + String newSessionId = client.getSessionId(); + + Assert.assertTrue(!originalSessionId.equals(newSessionId)); + client.reset(); + } + + + /* + * Choreograph the steps of the test dialogue with the server + * 1. while not authenticated, try to access a protected resource + * 2. respond to the login challenge with good credentials + * 3. after successful login, follow the redirect to the original page + * 4. repeatedly access the protected resource to demonstrate + * persistence of the authenticated session + * + * @param resourceMethod HTTP method for accessing the protected resource + * @param redirectMethod HTTP method for the login FORM reply + * @param useContinue whether the HTTP client should expect a 100 Continue + * @param clientShouldUseCookies whether the client should send cookies + * @param serverWillUseCookies whether the server should send cookies + * + */ + private String doTest(String resourceMethod, String redirectMethod, + boolean useContinue, boolean clientShouldUseCookies, + boolean serverWillUseCookies, boolean serverWillChangeSessid) + throws Exception { + return doTest(resourceMethod, redirectMethod, useContinue, + clientShouldUseCookies, serverWillUseCookies, + serverWillChangeSessid, true); + } + + private String doTest(String resourceMethod, String redirectMethod, + boolean useContinue, boolean clientShouldUseCookies, + boolean serverWillUseCookies, boolean serverWillChangeSessid, + boolean clientShouldUseHttp11) throws Exception { + + client = new FormAuthClient(clientShouldUseCookies, + clientShouldUseHttp11, serverWillUseCookies, + serverWillChangeSessid); + + // First request for protected resource gets the login page + client.setUseContinue(useContinue); + client.doResourceRequest(resourceMethod, false, null, null); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + String loginUri = client.extractBodyUri( + FormAuthClient.LOGIN_PARAM_TAG, + FormAuthClient.LOGIN_RESOURCE); + String originalSessionId = null; + if (serverWillUseCookies && clientShouldUseCookies) { + originalSessionId = client.getSessionId(); + } else { + originalSessionId = client.extractPathSessionId(loginUri); + } + client.reset(); + + // Second request replies to the login challenge + client.setUseContinue(useContinue); + client.doLoginRequest(loginUri); + if (clientShouldUseHttp11) { + Assert.assertTrue("login failed " + client.getResponseLine(), + client.isResponse303()); + } else { + Assert.assertTrue("login failed " + client.getResponseLine(), + client.isResponse302()); + } + Assert.assertTrue(client.isResponseBodyOK()); + String redirectUri = client.getRedirectUri(); + client.reset(); + + // Third request - the login was successful so + // follow the redirect to the protected resource + client.doResourceRequest(redirectMethod, true, redirectUri, null); + if ("POST".equals(redirectMethod)) { + client.setUseContinue(useContinue); + } + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + String protectedUri = client.extractBodyUri( + FormAuthClient.RESOURCE_PARAM_TAG, + FormAuthClient.PROTECTED_RESOURCE); + String newSessionId = null; + if (serverWillUseCookies && clientShouldUseCookies) { + newSessionId = client.getSessionId(); + } else { + newSessionId = client.extractPathSessionId(protectedUri); + } + boolean sessionIdIsChanged = !(originalSessionId.equals(newSessionId)); + Assert.assertTrue(sessionIdIsChanged == serverWillChangeSessid); + client.reset(); + + // Subsequent requests - keep accessing the protected resource + doTestProtected(resourceMethod, protectedUri, useContinue, + FormAuthClient.LOGIN_SUCCESSFUL, 5); + + return protectedUri; // in case more requests will be issued + } + + /* + * Repeatedly access the protected resource after the client has + * successfully logged-in to the webapp. The current session attributes + * will be used and cannot be changed. + * 3. after successful login, follow the redirect to the original page + * 4. repeatedly access the protected resource to demonstrate + * persistence of the authenticated session + * + * @param resourceMethod HTTP method for accessing the protected resource + * @param protectedUri to access (with or without sessionid) + * @param useContinue whether the HTTP client should expect a 100 Continue + * @param clientShouldUseCookies whether the client should send cookies + * @param serverWillUseCookies whether the server should send cookies + * + */ + private void doTestProtected(String resourceMethod, String protectedUri, + boolean useContinue, int phase, int repeatCount) + throws Exception { + + // Subsequent requests - keep accessing the protected resource + for (int i = 0; i < repeatCount; i++) { + client.setUseContinue(useContinue); + client.doResourceRequest(resourceMethod, false, protectedUri, null); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK(phase)); + client.reset(); + } + } + + /* + * Encapsulate the logic needed to run a suitably-configured tomcat + * instance, send it an HTTP request and process the server response + */ + private abstract static class FormAuthClientBase extends SimpleHttpClient { + + protected static final String LOGIN_PARAM_TAG = "action="; + protected static final String LOGIN_RESOURCE = "j_security_check"; + protected static final String LOGIN_REPLY = + "j_username=tomcat&j_password=tomcat"; + + protected static final String PROTECTED_RELATIVE_PATH = + "/examples/jsp/security/protected/"; + protected static final String PROTECTED_RESOURCE = "index.jsp"; + private static final String PROTECTED_RESOURCE_URL = + PROTECTED_RELATIVE_PATH + PROTECTED_RESOURCE; + protected static final String RESOURCE_PARAM_TAG = "href="; + private static final char PARAM_DELIM = '?'; + + // primitive tracking of the test phases to verify the HTML body + protected static final int LOGIN_REQUIRED = 1; + protected static final int REDIRECTING = 2; + protected static final int LOGIN_SUCCESSFUL = 3; + private int requestCount = 0; + + // todo: forgot this change and making it up again! + protected final String SESSION_PARAMETER_START = + SESSION_PARAMETER_NAME + "="; + + protected boolean clientShouldUseHttp11; + + protected void doLoginRequest(String loginUri) throws Exception { + + doResourceRequest("POST", true, + PROTECTED_RELATIVE_PATH + loginUri, LOGIN_REPLY); + } + + /* + * Prepare the resource request HTTP headers and issue the request. + * Three kinds of uri are supported: + * 1. fully qualified uri. + * 2. minimal uri without webapp path. + * 3. null - use the default protected resource + * Cookies are sent if available and supported by the test. Otherwise, the + * caller is expected to have provided a session id as a path parameter. + */ + protected void doResourceRequest(String method, boolean isFullQualUri, + String resourceUri, String requestTail) throws Exception { + + // build the HTTP request while assembling the uri + StringBuilder requestHead = new StringBuilder(128); + requestHead.append(method).append(' '); + if (isFullQualUri) { + requestHead.append(resourceUri); + } else { + if (resourceUri == null) { + // the default relative url + requestHead.append(PROTECTED_RESOURCE_URL); + } else { + requestHead.append(PROTECTED_RELATIVE_PATH) + .append(resourceUri); + } + if ("GET".equals(method)) { + requestHead.append("?role=bar"); + } + } + if (clientShouldUseHttp11) { + requestHead.append(" HTTP/1.1").append(CRLF); + } else { + requestHead.append(" HTTP/1.0").append(CRLF); + } + + // next, add the constant http headers + requestHead.append("Host: localhost").append(CRLF); + requestHead.append("Connection: close").append(CRLF); + + // then any optional http headers + if (getUseContinue()) { + requestHead.append("Expect: 100-continue").append(CRLF); + } + if (getUseCookies()) { + String sessionId = getSessionId(); + if (sessionId != null) { + requestHead.append("Cookie: ") + .append(SESSION_COOKIE_NAME) + .append('=').append(sessionId).append(CRLF); + } + } + + // finally, for posts only, deal with the request content + if ("POST".equals(method)) { + if (requestTail == null) { + requestTail = "role=bar"; + } + requestHead.append( + "Content-Type: application/x-www-form-urlencoded") + .append(CRLF); + // calculate post data length + String len = Integer.toString(requestTail.length()); + requestHead.append("Content-length: ").append(len).append(CRLF); + } + + // always put an empty line after the headers + requestHead.append(CRLF); + + String request[] = new String[2]; + request[0] = requestHead.toString(); + request[1] = requestTail; + doRequest(request); + } + + private void doRequest(String request[]) throws Exception { + setRequest(request); + connect(); + processRequest(); + disconnect(); + requestCount++; + } + + /* + * verify the server response HTML body is the page we expect, + * based on the dialogue position within doTest. + */ + @Override + public boolean isResponseBodyOK() { + return isResponseBodyOK(requestCount); + } + + /* + * verify the server response HTML body is the page we expect, + * based on the dialogue position given by the caller. + */ + public boolean isResponseBodyOK(int testPhase) { + switch (testPhase) { + case LOGIN_REQUIRED: + // First request should return in the login page + assertContains(getResponseBody(), + "Login Page for Examples"); + return true; + case REDIRECTING: + // Second request should result in redirect without a body + return true; + default: + // Subsequent requests should return in the protected page. + // Our role parameter should be appear in the page. + String body = getResponseBody(); + assertContains(body, + "Protected Page for Examples"); + assertContains(body, + " elements = getResponseBodyUriElements(); + String fullPath = null; + for (String element : elements) { + int ix = element.indexOf(paramTag); + if (ix > -1) { + ix += paramTag.length(); + char delim = element.charAt(ix); + int iy = element.indexOf(resource, ix); + if (iy > -1) { + int lastCharIx = element.indexOf(delim, iy); + fullPath = element.substring(iy, lastCharIx); + // remove any trailing parameters + int paramDelim = fullPath.indexOf(PARAM_DELIM); + if (paramDelim > -1) { + fullPath = fullPath.substring(0, paramDelim); + } + break; + } + } + } + return fullPath; + } + + /* + * extract the session id path element (if it exists in the given url) + */ + protected String extractPathSessionId(String url) { + String sessionId = null; + int iStart = url.indexOf(SESSION_PARAMETER_START); + if (iStart > -1) { + iStart += SESSION_PARAMETER_START.length(); + String remainder = url.substring(iStart); + StringTokenizer parser = new StringTokenizer(remainder, + SESSION_PATH_PARAMETER_TAILS); + if (parser.hasMoreElements()) { + sessionId = parser.nextToken(); + } else { + sessionId = url.substring(iStart); + } + } + return sessionId; + } + + private void assertContains(String body, String expected) { + if (!body.contains(expected)) { + Assert.fail("Response number " + requestCount + + ": body check failure.\n" + + "Expected to contain substring: [" + expected + + "]\nActual: [" + body + "]"); + } + } + } + + + private class FormAuthClient extends FormAuthClientBase { + private FormAuthClient(boolean clientShouldUseCookies, + boolean clientShouldUseHttp11, + boolean serverShouldUseCookies, + boolean serverShouldChangeSessid) throws Exception { + + this.clientShouldUseHttp11 = clientShouldUseHttp11; + + Tomcat tomcat = getTomcatInstance(); + File appDir = new File(System.getProperty("tomcat.test.basedir"), "webapps/examples"); + Context ctx = tomcat.addWebapp(null, "/examples", + appDir.getAbsolutePath()); + setUseCookies(clientShouldUseCookies); + ctx.setCookies(serverShouldUseCookies); + ctx.addApplicationListener(WsContextListener.class.getName()); + + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser("tomcat", "tomcat"); + realm.addUserRole("tomcat", "tomcat"); + ctx.setRealm(realm); + + tomcat.start(); + + // Valve pipeline is only established after tomcat starts + Valve[] valves = ctx.getPipeline().getValves(); + for (Valve valve : valves) { + if (valve instanceof AuthenticatorBase) { + ((AuthenticatorBase)valve) + .setChangeSessionIdOnAuthentication( + serverShouldChangeSessid); + break; + } + } + + // Port only known after Tomcat starts + setPort(getPort()); + } + } + + + /** + * Encapsulate the logic needed to run a suitably-configured Tomcat + * instance, send it an HTTP request and process the server response when + * the protected resource is only protected for some HTTP methods. The use + * case of particular interest is when GET and POST are not protected since + * those are the methods used by the login form and the redirect and if + * those methods are not protected the authenticator may not process the + * associated requests. + */ + private class FormAuthClientSelectedMethods extends FormAuthClientBase { + + private FormAuthClientSelectedMethods(boolean clientShouldUseCookies, + boolean clientShouldUseHttp11, + boolean serverShouldUseCookies, + boolean serverShouldChangeSessid) throws Exception { + + this.clientShouldUseHttp11 = clientShouldUseHttp11; + + Tomcat tomcat = getTomcatInstance(); + + Context ctx = tomcat.addContext( + "", System.getProperty("java.io.tmpdir")); + Tomcat.addServlet(ctx, "SelectedMethods", + new SelectedMethodsServlet()); + ctx.addServletMappingDecoded("/test", "SelectedMethods"); + // Login servlet just needs to respond "OK". Client will handle + // creating a valid response. No need for a form. + Tomcat.addServlet(ctx, "Login", + new TesterServlet()); + ctx.addServletMappingDecoded("/login", "Login"); + + // Configure the security constraints + SecurityConstraint constraint = new SecurityConstraint(); + SecurityCollection collection = new SecurityCollection(); + collection.setName("Protect PUT"); + collection.addMethod("PUT"); + collection.addPatternDecoded("/test"); + constraint.addCollection(collection); + constraint.addAuthRole("tomcat"); + ctx.addConstraint(constraint); + + // Configure authentication + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("FORM"); + lc.setLoginPage("/login"); + ctx.setLoginConfig(lc); + ctx.getPipeline().addValve(new FormAuthenticator()); + + setUseCookies(clientShouldUseCookies); + ctx.setCookies(serverShouldUseCookies); + + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser("tomcat", "tomcat"); + realm.addUserRole("tomcat", "tomcat"); + ctx.setRealm(realm); + + tomcat.start(); + + // Valve pipeline is only established after tomcat starts + Valve[] valves = ctx.getPipeline().getValves(); + for (Valve valve : valves) { + if (valve instanceof AuthenticatorBase) { + ((AuthenticatorBase)valve) + .setChangeSessionIdOnAuthentication( + serverShouldChangeSessid); + break; + } + } + + // Port only known after Tomcat starts + setPort(getPort()); + } + + @Override + public boolean isResponseBodyOK() { + if (isResponse303()) { + return true; + } + Assert.assertTrue(getResponseBody(), getResponseBody().contains("OK")); + Assert.assertFalse(getResponseBody().contains("FAIL")); + return true; + } + } + + + private static final class SelectedMethodsServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + public static final String PARAM = "TestParam"; + public static final String VALUE = "TestValue"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain;charset=UTF-8"); + + if (VALUE.equals(req.getParameter(PARAM)) && + req.isUserInRole("tomcat")) { + resp.getWriter().print("OK"); + } else { + resp.getWriter().print("FAIL"); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Same as GET for this test case + doGet(req, resp); + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Same as GET for this test case + doGet(req, resp); + } + } +} diff --git a/test/org/apache/catalina/authenticator/TestFormAuthenticatorB.java b/test/org/apache/catalina/authenticator/TestFormAuthenticatorB.java new file mode 100644 index 0000000..a8dff99 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestFormAuthenticatorB.java @@ -0,0 +1,520 @@ +/* + * 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.catalina.authenticator; + +import java.io.File; +import java.util.List; +import java.util.StringTokenizer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Valve; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.server.WsContextListener; + +/* + * Test FORM authentication for sessions that do and do not use cookies. + * + * 1. A client that can accept and respond to a Set-Cookie for JSESSIONID + * will be able to maintain its authenticated session, no matter whether + * the session ID is changed once, many times, or not at all. + * + * 2. A client that cannot accept cookies will only be able to maintain a + * persistent session IF the server sends the correct (current) jsessionid + * as a path parameter appended to ALL urls within its response. That is + * achievable with servlets, jsps, jstl (all of which which can ask for an + * encoded url to be inserted into the dynamic web page). It cannot work + * with static HTML. + * note: this test class uses the Tomcat sample jsps, which conform. + * + * 3. Therefore, any webapp that MIGHT need to authenticate a client that + * does not accept cookies MUST generate EVERY protected resource url + * dynamically (so that it will include the current session ID). + * + * 4. Any webapp that cannot satisfy case 3 MUST turn off + * changeSessionIdOnAuthentication for its Context and thus degrade the + * session fixation protection for ALL of its clients. + * note from MarkT: Not sure I agree with this. If the URLs aren't + * being encoded, then the session is going to break regardless of + * whether or not the session ID changes. + * + * Unlike a "proper browser", this unit test class does a quite lot of + * screen-scraping and cheating of headers and urls (not very elegant, + * but it makes no claims to generality). + * + */ +public class TestFormAuthenticatorB extends TomcatBaseTest { + + // these should really be singletons to be type-safe, + // we are in a unit test and don't need to paranoid. + protected static final boolean USE_100_CONTINUE = true; + protected static final boolean NO_100_CONTINUE = !USE_100_CONTINUE; + + protected static final boolean CLIENT_USE_COOKIES = true; + protected static final boolean CLIENT_NO_COOKIES = !CLIENT_USE_COOKIES; + + protected static final boolean CLIENT_USE_HTTP_11 = true; + protected static final boolean CLIENT_USE_HTTP_10 = !CLIENT_USE_HTTP_11; + + protected static final boolean SERVER_USE_COOKIES = true; + protected static final boolean SERVER_NO_COOKIES = !SERVER_USE_COOKIES; + + protected static final boolean SERVER_CHANGE_SESSID = true; + protected static final boolean SERVER_FREEZE_SESSID = !SERVER_CHANGE_SESSID; + + private FormAuthClient client; + + // first, a set of tests where the server uses a cookie to carry + // the current session ID during and after authentication, and + // the client is prepared to return cookies with each request + + @Test + public void testPostNoContinueWithCookies() throws Exception { + doTest("POST", "GET", NO_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + // Bug 49779 + @Test + public void testPostNoContinuePostRedirectWithCookies() throws Exception { + doTest("POST", "POST", NO_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + + // next, a set of tests where the server Context is configured to never + // use cookies and the session ID is only carried as a url path parameter + + @Test + public void testPostNoContinueNoServerCookies() throws Exception { + doTest("POST", "GET", NO_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID); + } + + // variant of Bug 49779 + @Test + public void testPostNoContinuePostRedirectNoServerCookies() + throws Exception { + doTest("POST", "POST", NO_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID); + } + + + + + // next, a set of tests where the server Context uses cookies, + // but the client refuses to return them and tries to use + // the session ID if carried as a url path parameter + + @Test + public void testPostNoContinueNoClientCookies() throws Exception { + doTest("POST", "GET", NO_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + // variant of Bug 49779 + @Test + public void testPostNoContinuePostRedirectNoClientCookies() + throws Exception { + doTest("POST", "POST", NO_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + + + + // finally, a set of tests to explore quirky situations + // but there is not need to replicate all the scenarios above. + + /* + * Choreograph the steps of the test dialogue with the server + * 1. while not authenticated, try to access a protected resource + * 2. respond to the login challenge with good credentials + * 3. after successful login, follow the redirect to the original page + * 4. repeatedly access the protected resource to demonstrate + * persistence of the authenticated session + * + * @param resourceMethod HTTP method for accessing the protected resource + * @param redirectMethod HTTP method for the login FORM reply + * @param useContinue whether the HTTP client should expect a 100 Continue + * @param clientShouldUseCookies whether the client should send cookies + * @param serverWillUseCookies whether the server should send cookies + * + */ + private String doTest(String resourceMethod, String redirectMethod, + boolean useContinue, boolean clientShouldUseCookies, + boolean serverWillUseCookies, boolean serverWillChangeSessid) + throws Exception { + return doTest(resourceMethod, redirectMethod, useContinue, + clientShouldUseCookies, serverWillUseCookies, + serverWillChangeSessid, true); + } + + private String doTest(String resourceMethod, String redirectMethod, + boolean useContinue, boolean clientShouldUseCookies, + boolean serverWillUseCookies, boolean serverWillChangeSessid, + boolean clientShouldUseHttp11) throws Exception { + + client = new FormAuthClient(clientShouldUseCookies, + clientShouldUseHttp11, serverWillUseCookies, + serverWillChangeSessid); + + // First request for protected resource gets the login page + client.setUseContinue(useContinue); + client.doResourceRequest(resourceMethod, false, null, null); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + String loginUri = client.extractBodyUri( + FormAuthClient.LOGIN_PARAM_TAG, + FormAuthClient.LOGIN_RESOURCE); + String originalSessionId = null; + if (serverWillUseCookies && clientShouldUseCookies) { + originalSessionId = client.getSessionId(); + } else { + originalSessionId = client.extractPathSessionId(loginUri); + } + client.reset(); + + // Second request replies to the login challenge + client.setUseContinue(useContinue); + client.doLoginRequest(loginUri); + if (clientShouldUseHttp11) { + Assert.assertTrue("login failed " + client.getResponseLine(), + client.isResponse303()); + } else { + Assert.assertTrue("login failed " + client.getResponseLine(), + client.isResponse302()); + } + Assert.assertTrue(client.isResponseBodyOK()); + String redirectUri = client.getRedirectUri(); + client.reset(); + + // Third request - the login was successful so + // follow the redirect to the protected resource + client.doResourceRequest(redirectMethod, true, redirectUri, null); + if ("POST".equals(redirectMethod)) { + client.setUseContinue(useContinue); + } + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + String protectedUri = client.extractBodyUri( + FormAuthClient.RESOURCE_PARAM_TAG, + FormAuthClient.PROTECTED_RESOURCE); + String newSessionId = null; + if (serverWillUseCookies && clientShouldUseCookies) { + newSessionId = client.getSessionId(); + } else { + newSessionId = client.extractPathSessionId(protectedUri); + } + boolean sessionIdIsChanged = !(originalSessionId.equals(newSessionId)); + Assert.assertTrue(sessionIdIsChanged == serverWillChangeSessid); + client.reset(); + + // Subsequent requests - keep accessing the protected resource + doTestProtected(resourceMethod, protectedUri, useContinue, + FormAuthClient.LOGIN_SUCCESSFUL, 5); + + return protectedUri; // in case more requests will be issued + } + + /* + * Repeatedly access the protected resource after the client has + * successfully logged-in to the webapp. The current session attributes + * will be used and cannot be changed. + * 3. after successful login, follow the redirect to the original page + * 4. repeatedly access the protected resource to demonstrate + * persistence of the authenticated session + * + * @param resourceMethod HTTP method for accessing the protected resource + * @param protectedUri to access (with or without sessionid) + * @param useContinue whether the HTTP client should expect a 100 Continue + * @param clientShouldUseCookies whether the client should send cookies + * @param serverWillUseCookies whether the server should send cookies + * + */ + private void doTestProtected(String resourceMethod, String protectedUri, + boolean useContinue, int phase, int repeatCount) + throws Exception { + + // Subsequent requests - keep accessing the protected resource + for (int i = 0; i < repeatCount; i++) { + client.setUseContinue(useContinue); + client.doResourceRequest(resourceMethod, false, protectedUri, null); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK(phase)); + client.reset(); + } + } + + /* + * Encapsulate the logic needed to run a suitably-configured tomcat + * instance, send it an HTTP request and process the server response + */ + private abstract static class FormAuthClientBase extends SimpleHttpClient { + + protected static final String LOGIN_PARAM_TAG = "action="; + protected static final String LOGIN_RESOURCE = "j_security_check"; + protected static final String LOGIN_REPLY = + "j_username=tomcat&j_password=tomcat"; + + protected static final String PROTECTED_RELATIVE_PATH = + "/examples/jsp/security/protected/"; + protected static final String PROTECTED_RESOURCE = "index.jsp"; + private static final String PROTECTED_RESOURCE_URL = + PROTECTED_RELATIVE_PATH + PROTECTED_RESOURCE; + protected static final String RESOURCE_PARAM_TAG = "href="; + private static final char PARAM_DELIM = '?'; + + // primitive tracking of the test phases to verify the HTML body + protected static final int LOGIN_REQUIRED = 1; + protected static final int REDIRECTING = 2; + protected static final int LOGIN_SUCCESSFUL = 3; + private int requestCount = 0; + + // todo: forgot this change and making it up again! + protected final String SESSION_PARAMETER_START = + SESSION_PARAMETER_NAME + "="; + + protected boolean clientShouldUseHttp11; + + protected void doLoginRequest(String loginUri) throws Exception { + + doResourceRequest("POST", true, + PROTECTED_RELATIVE_PATH + loginUri, LOGIN_REPLY); + } + + /* + * Prepare the resource request HTTP headers and issue the request. + * Three kinds of uri are supported: + * 1. fully qualified uri. + * 2. minimal uri without webapp path. + * 3. null - use the default protected resource + * Cookies are sent if available and supported by the test. Otherwise, the + * caller is expected to have provided a session id as a path parameter. + */ + protected void doResourceRequest(String method, boolean isFullQualUri, + String resourceUri, String requestTail) throws Exception { + + // build the HTTP request while assembling the uri + StringBuilder requestHead = new StringBuilder(128); + requestHead.append(method).append(' '); + if (isFullQualUri) { + requestHead.append(resourceUri); + } else { + if (resourceUri == null) { + // the default relative url + requestHead.append(PROTECTED_RESOURCE_URL); + } else { + requestHead.append(PROTECTED_RELATIVE_PATH) + .append(resourceUri); + } + if ("GET".equals(method)) { + requestHead.append("?role=bar"); + } + } + if (clientShouldUseHttp11) { + requestHead.append(" HTTP/1.1").append(CRLF); + } else { + requestHead.append(" HTTP/1.0").append(CRLF); + } + + // next, add the constant http headers + requestHead.append("Host: localhost").append(CRLF); + requestHead.append("Connection: close").append(CRLF); + + // then any optional http headers + if (getUseContinue()) { + requestHead.append("Expect: 100-continue").append(CRLF); + } + if (getUseCookies()) { + String sessionId = getSessionId(); + if (sessionId != null) { + requestHead.append("Cookie: ") + .append(SESSION_COOKIE_NAME) + .append('=').append(sessionId).append(CRLF); + } + } + + // finally, for posts only, deal with the request content + if ("POST".equals(method)) { + if (requestTail == null) { + requestTail = "role=bar"; + } + requestHead.append( + "Content-Type: application/x-www-form-urlencoded") + .append(CRLF); + // calculate post data length + String len = Integer.toString(requestTail.length()); + requestHead.append("Content-length: ").append(len).append(CRLF); + } + + // always put an empty line after the headers + requestHead.append(CRLF); + + String request[] = new String[2]; + request[0] = requestHead.toString(); + request[1] = requestTail; + doRequest(request); + } + + private void doRequest(String request[]) throws Exception { + setRequest(request); + connect(); + processRequest(); + disconnect(); + requestCount++; + } + + /* + * verify the server response HTML body is the page we expect, + * based on the dialogue position within doTest. + */ + @Override + public boolean isResponseBodyOK() { + return isResponseBodyOK(requestCount); + } + + /* + * verify the server response HTML body is the page we expect, + * based on the dialogue position given by the caller. + */ + public boolean isResponseBodyOK(int testPhase) { + switch (testPhase) { + case LOGIN_REQUIRED: + // First request should return in the login page + assertContains(getResponseBody(), + "Login Page for Examples"); + return true; + case REDIRECTING: + // Second request should result in redirect without a body + return true; + default: + // Subsequent requests should return in the protected page. + // Our role parameter should be appear in the page. + String body = getResponseBody(); + assertContains(body, + "Protected Page for Examples"); + assertContains(body, + " elements = getResponseBodyUriElements(); + String fullPath = null; + for (String element : elements) { + int ix = element.indexOf(paramTag); + if (ix > -1) { + ix += paramTag.length(); + char delim = element.charAt(ix); + int iy = element.indexOf(resource, ix); + if (iy > -1) { + int lastCharIx = element.indexOf(delim, iy); + fullPath = element.substring(iy, lastCharIx); + // remove any trailing parameters + int paramDelim = fullPath.indexOf(PARAM_DELIM); + if (paramDelim > -1) { + fullPath = fullPath.substring(0, paramDelim); + } + break; + } + } + } + return fullPath; + } + + /* + * extract the session id path element (if it exists in the given url) + */ + protected String extractPathSessionId(String url) { + String sessionId = null; + int iStart = url.indexOf(SESSION_PARAMETER_START); + if (iStart > -1) { + iStart += SESSION_PARAMETER_START.length(); + String remainder = url.substring(iStart); + StringTokenizer parser = new StringTokenizer(remainder, + SESSION_PATH_PARAMETER_TAILS); + if (parser.hasMoreElements()) { + sessionId = parser.nextToken(); + } else { + sessionId = url.substring(iStart); + } + } + return sessionId; + } + + private void assertContains(String body, String expected) { + if (!body.contains(expected)) { + Assert.fail("Response number " + requestCount + + ": body check failure.\n" + + "Expected to contain substring: [" + expected + + "]\nActual: [" + body + "]"); + } + } + } + + + private class FormAuthClient extends FormAuthClientBase { + private FormAuthClient(boolean clientShouldUseCookies, + boolean clientShouldUseHttp11, + boolean serverShouldUseCookies, + boolean serverShouldChangeSessid) throws Exception { + + this.clientShouldUseHttp11 = clientShouldUseHttp11; + + Tomcat tomcat = getTomcatInstance(); + File appDir = new File(System.getProperty("tomcat.test.basedir"), "webapps/examples"); + Context ctx = tomcat.addWebapp(null, "/examples", + appDir.getAbsolutePath()); + setUseCookies(clientShouldUseCookies); + ctx.setCookies(serverShouldUseCookies); + ctx.addApplicationListener(WsContextListener.class.getName()); + + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser("tomcat", "tomcat"); + realm.addUserRole("tomcat", "tomcat"); + ctx.setRealm(realm); + + tomcat.start(); + + // Valve pipeline is only established after tomcat starts + Valve[] valves = ctx.getPipeline().getValves(); + for (Valve valve : valves) { + if (valve instanceof AuthenticatorBase) { + ((AuthenticatorBase)valve) + .setChangeSessionIdOnAuthentication( + serverShouldChangeSessid); + break; + } + } + + // Port only known after Tomcat starts + setPort(getPort()); + } + } +} diff --git a/test/org/apache/catalina/authenticator/TestFormAuthenticatorC.java b/test/org/apache/catalina/authenticator/TestFormAuthenticatorC.java new file mode 100644 index 0000000..78ab635 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestFormAuthenticatorC.java @@ -0,0 +1,522 @@ +/* + * 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.catalina.authenticator; + +import java.io.File; +import java.util.List; +import java.util.StringTokenizer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Valve; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.server.WsContextListener; + +/* + * Test FORM authentication for sessions that do and do not use cookies. + * + * 1. A client that can accept and respond to a Set-Cookie for JSESSIONID + * will be able to maintain its authenticated session, no matter whether + * the session ID is changed once, many times, or not at all. + * + * 2. A client that cannot accept cookies will only be able to maintain a + * persistent session IF the server sends the correct (current) jsessionid + * as a path parameter appended to ALL urls within its response. That is + * achievable with servlets, jsps, jstl (all of which which can ask for an + * encoded url to be inserted into the dynamic web page). It cannot work + * with static HTML. + * note: this test class uses the Tomcat sample jsps, which conform. + * + * 3. Therefore, any webapp that MIGHT need to authenticate a client that + * does not accept cookies MUST generate EVERY protected resource url + * dynamically (so that it will include the current session ID). + * + * 4. Any webapp that cannot satisfy case 3 MUST turn off + * changeSessionIdOnAuthentication for its Context and thus degrade the + * session fixation protection for ALL of its clients. + * note from MarkT: Not sure I agree with this. If the URLs aren't + * being encoded, then the session is going to break regardless of + * whether or not the session ID changes. + * + * Unlike a "proper browser", this unit test class does a quite lot of + * screen-scraping and cheating of headers and urls (not very elegant, + * but it makes no claims to generality). + * + */ +public class TestFormAuthenticatorC extends TomcatBaseTest { + + // these should really be singletons to be type-safe, + // we are in a unit test and don't need to paranoid. + protected static final boolean USE_100_CONTINUE = true; + protected static final boolean NO_100_CONTINUE = !USE_100_CONTINUE; + + protected static final boolean CLIENT_USE_COOKIES = true; + protected static final boolean CLIENT_NO_COOKIES = !CLIENT_USE_COOKIES; + + protected static final boolean CLIENT_USE_HTTP_11 = true; + protected static final boolean CLIENT_USE_HTTP_10 = !CLIENT_USE_HTTP_11; + + protected static final boolean SERVER_USE_COOKIES = true; + protected static final boolean SERVER_NO_COOKIES = !SERVER_USE_COOKIES; + + protected static final boolean SERVER_CHANGE_SESSID = true; + protected static final boolean SERVER_FREEZE_SESSID = !SERVER_CHANGE_SESSID; + + private FormAuthClient client; + + // first, a set of tests where the server uses a cookie to carry + // the current session ID during and after authentication, and + // the client is prepared to return cookies with each request + + @Test + public void testPostWithContinueAndCookies() throws Exception { + doTest("POST", "GET", USE_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + // Bug 49779 + @Test + public void testPostWithContinuePostRedirectWithCookies() throws Exception { + doTest("POST", "POST", USE_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + + // next, a set of tests where the server Context is configured to never + // use cookies and the session ID is only carried as a url path parameter + + @Test + public void testPostWithContinueNoServerCookies() throws Exception { + doTest("POST", "GET", USE_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID); + } + + // variant of Bug 49779 + @Test + public void testPostWithContinuePostRedirectNoServerCookies() + throws Exception { + doTest("POST", "POST", USE_100_CONTINUE, + CLIENT_USE_COOKIES, SERVER_NO_COOKIES, SERVER_CHANGE_SESSID); + } + + + // next, a set of tests where the server Context uses cookies, + // but the client refuses to return them and tries to use + // the session ID if carried as a url path parameter + + @Test + public void testGetNoClientCookies() throws Exception { + doTest("GET", "GET", NO_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + @Test + public void testPostWithContinueNoClientCookies() throws Exception { + doTest("POST", "GET", USE_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + // variant of Bug 49779 + @Test + public void testPostWithContinuePostRedirectNoClientCookies() + throws Exception { + doTest("POST", "POST", USE_100_CONTINUE, + CLIENT_NO_COOKIES, SERVER_USE_COOKIES, SERVER_CHANGE_SESSID); + } + + + // finally, a set of tests to explore quirky situations + // but there is not need to replicate all the scenarios above. + + /* + * Choreograph the steps of the test dialogue with the server + * 1. while not authenticated, try to access a protected resource + * 2. respond to the login challenge with good credentials + * 3. after successful login, follow the redirect to the original page + * 4. repeatedly access the protected resource to demonstrate + * persistence of the authenticated session + * + * @param resourceMethod HTTP method for accessing the protected resource + * @param redirectMethod HTTP method for the login FORM reply + * @param useContinue whether the HTTP client should expect a 100 Continue + * @param clientShouldUseCookies whether the client should send cookies + * @param serverWillUseCookies whether the server should send cookies + * + */ + private String doTest(String resourceMethod, String redirectMethod, + boolean useContinue, boolean clientShouldUseCookies, + boolean serverWillUseCookies, boolean serverWillChangeSessid) + throws Exception { + return doTest(resourceMethod, redirectMethod, useContinue, + clientShouldUseCookies, serverWillUseCookies, + serverWillChangeSessid, true); + } + + private String doTest(String resourceMethod, String redirectMethod, + boolean useContinue, boolean clientShouldUseCookies, + boolean serverWillUseCookies, boolean serverWillChangeSessid, + boolean clientShouldUseHttp11) throws Exception { + + client = new FormAuthClient(clientShouldUseCookies, + clientShouldUseHttp11, serverWillUseCookies, + serverWillChangeSessid); + + // First request for protected resource gets the login page + client.setUseContinue(useContinue); + client.doResourceRequest(resourceMethod, false, null, null); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + String loginUri = client.extractBodyUri( + FormAuthClient.LOGIN_PARAM_TAG, + FormAuthClient.LOGIN_RESOURCE); + String originalSessionId = null; + if (serverWillUseCookies && clientShouldUseCookies) { + originalSessionId = client.getSessionId(); + } else { + originalSessionId = client.extractPathSessionId(loginUri); + } + client.reset(); + + // Second request replies to the login challenge + client.setUseContinue(useContinue); + client.doLoginRequest(loginUri); + if (clientShouldUseHttp11) { + Assert.assertTrue("login failed " + client.getResponseLine(), + client.isResponse303()); + } else { + Assert.assertTrue("login failed " + client.getResponseLine(), + client.isResponse302()); + } + Assert.assertTrue(client.isResponseBodyOK()); + String redirectUri = client.getRedirectUri(); + client.reset(); + + // Third request - the login was successful so + // follow the redirect to the protected resource + client.doResourceRequest(redirectMethod, true, redirectUri, null); + if ("POST".equals(redirectMethod)) { + client.setUseContinue(useContinue); + } + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + String protectedUri = client.extractBodyUri( + FormAuthClient.RESOURCE_PARAM_TAG, + FormAuthClient.PROTECTED_RESOURCE); + String newSessionId = null; + if (serverWillUseCookies && clientShouldUseCookies) { + newSessionId = client.getSessionId(); + } else { + newSessionId = client.extractPathSessionId(protectedUri); + } + boolean sessionIdIsChanged = !(originalSessionId.equals(newSessionId)); + Assert.assertTrue(sessionIdIsChanged == serverWillChangeSessid); + client.reset(); + + // Subsequent requests - keep accessing the protected resource + doTestProtected(resourceMethod, protectedUri, useContinue, + FormAuthClient.LOGIN_SUCCESSFUL, 5); + + return protectedUri; // in case more requests will be issued + } + + /* + * Repeatedly access the protected resource after the client has + * successfully logged-in to the webapp. The current session attributes + * will be used and cannot be changed. + * 3. after successful login, follow the redirect to the original page + * 4. repeatedly access the protected resource to demonstrate + * persistence of the authenticated session + * + * @param resourceMethod HTTP method for accessing the protected resource + * @param protectedUri to access (with or without sessionid) + * @param useContinue whether the HTTP client should expect a 100 Continue + * @param clientShouldUseCookies whether the client should send cookies + * @param serverWillUseCookies whether the server should send cookies + * + */ + private void doTestProtected(String resourceMethod, String protectedUri, + boolean useContinue, int phase, int repeatCount) + throws Exception { + + // Subsequent requests - keep accessing the protected resource + for (int i = 0; i < repeatCount; i++) { + client.setUseContinue(useContinue); + client.doResourceRequest(resourceMethod, false, protectedUri, null); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK(phase)); + client.reset(); + } + } + + /* + * Encapsulate the logic needed to run a suitably-configured tomcat + * instance, send it an HTTP request and process the server response + */ + private abstract static class FormAuthClientBase extends SimpleHttpClient { + + protected static final String LOGIN_PARAM_TAG = "action="; + protected static final String LOGIN_RESOURCE = "j_security_check"; + protected static final String LOGIN_REPLY = + "j_username=tomcat&j_password=tomcat"; + + protected static final String PROTECTED_RELATIVE_PATH = + "/examples/jsp/security/protected/"; + protected static final String PROTECTED_RESOURCE = "index.jsp"; + private static final String PROTECTED_RESOURCE_URL = + PROTECTED_RELATIVE_PATH + PROTECTED_RESOURCE; + protected static final String RESOURCE_PARAM_TAG = "href="; + private static final char PARAM_DELIM = '?'; + + // primitive tracking of the test phases to verify the HTML body + protected static final int LOGIN_REQUIRED = 1; + protected static final int REDIRECTING = 2; + protected static final int LOGIN_SUCCESSFUL = 3; + private int requestCount = 0; + + // todo: forgot this change and making it up again! + protected final String SESSION_PARAMETER_START = + SESSION_PARAMETER_NAME + "="; + + protected boolean clientShouldUseHttp11; + + protected void doLoginRequest(String loginUri) throws Exception { + + doResourceRequest("POST", true, + PROTECTED_RELATIVE_PATH + loginUri, LOGIN_REPLY); + } + + /* + * Prepare the resource request HTTP headers and issue the request. + * Three kinds of uri are supported: + * 1. fully qualified uri. + * 2. minimal uri without webapp path. + * 3. null - use the default protected resource + * Cookies are sent if available and supported by the test. Otherwise, the + * caller is expected to have provided a session id as a path parameter. + */ + protected void doResourceRequest(String method, boolean isFullQualUri, + String resourceUri, String requestTail) throws Exception { + + // build the HTTP request while assembling the uri + StringBuilder requestHead = new StringBuilder(128); + requestHead.append(method).append(' '); + if (isFullQualUri) { + requestHead.append(resourceUri); + } else { + if (resourceUri == null) { + // the default relative url + requestHead.append(PROTECTED_RESOURCE_URL); + } else { + requestHead.append(PROTECTED_RELATIVE_PATH) + .append(resourceUri); + } + if ("GET".equals(method)) { + requestHead.append("?role=bar"); + } + } + if (clientShouldUseHttp11) { + requestHead.append(" HTTP/1.1").append(CRLF); + } else { + requestHead.append(" HTTP/1.0").append(CRLF); + } + + // next, add the constant http headers + requestHead.append("Host: localhost").append(CRLF); + requestHead.append("Connection: close").append(CRLF); + + // then any optional http headers + if (getUseContinue()) { + requestHead.append("Expect: 100-continue").append(CRLF); + } + if (getUseCookies()) { + String sessionId = getSessionId(); + if (sessionId != null) { + requestHead.append("Cookie: ") + .append(SESSION_COOKIE_NAME) + .append('=').append(sessionId).append(CRLF); + } + } + + // finally, for posts only, deal with the request content + if ("POST".equals(method)) { + if (requestTail == null) { + requestTail = "role=bar"; + } + requestHead.append( + "Content-Type: application/x-www-form-urlencoded") + .append(CRLF); + // calculate post data length + String len = Integer.toString(requestTail.length()); + requestHead.append("Content-length: ").append(len).append(CRLF); + } + + // always put an empty line after the headers + requestHead.append(CRLF); + + String request[] = new String[2]; + request[0] = requestHead.toString(); + request[1] = requestTail; + doRequest(request); + } + + private void doRequest(String request[]) throws Exception { + setRequest(request); + connect(); + processRequest(); + disconnect(); + requestCount++; + } + + /* + * verify the server response HTML body is the page we expect, + * based on the dialogue position within doTest. + */ + @Override + public boolean isResponseBodyOK() { + return isResponseBodyOK(requestCount); + } + + /* + * verify the server response HTML body is the page we expect, + * based on the dialogue position given by the caller. + */ + public boolean isResponseBodyOK(int testPhase) { + switch (testPhase) { + case LOGIN_REQUIRED: + // First request should return in the login page + assertContains(getResponseBody(), + "Login Page for Examples"); + return true; + case REDIRECTING: + // Second request should result in redirect without a body + return true; + default: + // Subsequent requests should return in the protected page. + // Our role parameter should be appear in the page. + String body = getResponseBody(); + assertContains(body, + "Protected Page for Examples"); + assertContains(body, + " elements = getResponseBodyUriElements(); + String fullPath = null; + for (String element : elements) { + int ix = element.indexOf(paramTag); + if (ix > -1) { + ix += paramTag.length(); + char delim = element.charAt(ix); + int iy = element.indexOf(resource, ix); + if (iy > -1) { + int lastCharIx = element.indexOf(delim, iy); + fullPath = element.substring(iy, lastCharIx); + // remove any trailing parameters + int paramDelim = fullPath.indexOf(PARAM_DELIM); + if (paramDelim > -1) { + fullPath = fullPath.substring(0, paramDelim); + } + break; + } + } + } + return fullPath; + } + + /* + * extract the session id path element (if it exists in the given url) + */ + protected String extractPathSessionId(String url) { + String sessionId = null; + int iStart = url.indexOf(SESSION_PARAMETER_START); + if (iStart > -1) { + iStart += SESSION_PARAMETER_START.length(); + String remainder = url.substring(iStart); + StringTokenizer parser = new StringTokenizer(remainder, + SESSION_PATH_PARAMETER_TAILS); + if (parser.hasMoreElements()) { + sessionId = parser.nextToken(); + } else { + sessionId = url.substring(iStart); + } + } + return sessionId; + } + + private void assertContains(String body, String expected) { + if (!body.contains(expected)) { + Assert.fail("Response number " + requestCount + + ": body check failure.\n" + + "Expected to contain substring: [" + expected + + "]\nActual: [" + body + "]"); + } + } + } + + + private class FormAuthClient extends FormAuthClientBase { + private FormAuthClient(boolean clientShouldUseCookies, + boolean clientShouldUseHttp11, + boolean serverShouldUseCookies, + boolean serverShouldChangeSessid) throws Exception { + + this.clientShouldUseHttp11 = clientShouldUseHttp11; + + Tomcat tomcat = getTomcatInstance(); + File appDir = new File(System.getProperty("tomcat.test.basedir"), "webapps/examples"); + Context ctx = tomcat.addWebapp(null, "/examples", + appDir.getAbsolutePath()); + setUseCookies(clientShouldUseCookies); + ctx.setCookies(serverShouldUseCookies); + ctx.addApplicationListener(WsContextListener.class.getName()); + + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser("tomcat", "tomcat"); + realm.addUserRole("tomcat", "tomcat"); + ctx.setRealm(realm); + + tomcat.start(); + + // Valve pipeline is only established after tomcat starts + Valve[] valves = ctx.getPipeline().getValves(); + for (Valve valve : valves) { + if (valve instanceof AuthenticatorBase) { + ((AuthenticatorBase)valve) + .setChangeSessionIdOnAuthentication( + serverShouldChangeSessid); + break; + } + } + + // Port only known after Tomcat starts + setPort(getPort()); + } + } +} diff --git a/test/org/apache/catalina/authenticator/TestJaspicCallbackHandlerInAuthenticator.java b/test/org/apache/catalina/authenticator/TestJaspicCallbackHandlerInAuthenticator.java new file mode 100644 index 0000000..57aa8b3 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestJaspicCallbackHandlerInAuthenticator.java @@ -0,0 +1,187 @@ +/** + * 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.catalina.authenticator; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.Principal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.callback.CallerPrincipalCallback; +import jakarta.security.auth.message.callback.GroupPrincipalCallback; +import jakarta.security.auth.message.callback.PasswordValidationCallback; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Contained; +import org.apache.catalina.Container; +import org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl; +import org.apache.catalina.connector.Request; +import org.apache.catalina.core.ContainerBase; +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.catalina.realm.RealmBase; + +public class TestJaspicCallbackHandlerInAuthenticator { + + @Test + public void testCustomCallbackHandlerCreation() throws Exception { + testCallbackHandlerCreation("org.apache.catalina.authenticator.TesterCallbackHandlerImpl", + TesterCallbackHandlerImpl.class); + } + + + @Test + public void testDefaultCallbackHandlerCreation() throws Exception { + testCallbackHandlerCreation(null, CallbackHandlerImpl.class); + } + + + private void testCallbackHandlerCreation(String callbackHandlerImplClassName, Class callbackHandlerImplClass) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException { + CallbackHandler callbackHandler = createCallbackHandler(callbackHandlerImplClassName); + Assert.assertTrue(callbackHandlerImplClass.isInstance(callbackHandler)); + } + + + @Test + public void testCallerPrincipalCallback() throws Exception { + CallbackHandler callbackHandler = createCallbackHandler(null); + Subject clientSubject = new Subject(); + CallerPrincipalCallback cpc1 = new CallerPrincipalCallback(clientSubject, "name1"); + callbackHandler.handle(new Callback[] { cpc1 }); + CallerPrincipalCallback cpc2 = new CallerPrincipalCallback(clientSubject, new Principal() { + @Override + public String getName() { + return "name2"; + } + }); + callbackHandler.handle(new Callback[] { cpc2 }); + Set credentials = clientSubject.getPrivateCredentials(); + Assert.assertTrue(credentials.size() == 2); + Set names = new HashSet<>(Arrays.asList(new String[] { "name1", "name2" })); + for (Object o : credentials) { + names.remove(((GenericPrincipal) o).getName()); + } + Assert.assertTrue(names.isEmpty()); + } + + @Test + public void testGroupPrincipalCallback() throws Exception { + CallbackHandler callbackHandler = createCallbackHandler(null); + Subject clientSubject = new Subject(); + CallerPrincipalCallback cpc = new CallerPrincipalCallback(clientSubject, "name"); + GroupPrincipalCallback gpc = new GroupPrincipalCallback(clientSubject, + new String[] { "group1", "group2" }); + callbackHandler.handle(new Callback[] { cpc, gpc }); + Set credentials = clientSubject.getPrivateCredentials(); + Assert.assertTrue(credentials.size() == 1); + GenericPrincipal gp = (GenericPrincipal) credentials.iterator().next(); + Assert.assertEquals("name", gp.getName()); + Assert.assertTrue(gp.hasRole("group1")); + Assert.assertTrue(gp.hasRole("group2")); + } + + @Test + public void testPasswordValidationCallback() throws Exception { + CallbackHandler callbackHandler = createCallbackHandler(null); + Container container = new TestContainer(); + container.setRealm(new TestRealm()); + ((Contained) callbackHandler).setContainer(container); + Subject clientSubject = new Subject(); + PasswordValidationCallback pvc1 = new PasswordValidationCallback(clientSubject, "name1", + "password".toCharArray()); + callbackHandler.handle(new Callback[] { pvc1 }); + Assert.assertTrue(pvc1.getResult()); + PasswordValidationCallback pvc2 = new PasswordValidationCallback(clientSubject, "name2", + "invalid".toCharArray()); + callbackHandler.handle(new Callback[] { pvc2 }); + Assert.assertFalse(pvc2.getResult()); + Set credentials = clientSubject.getPrivateCredentials(); + Assert.assertTrue(credentials.size() == 1); + GenericPrincipal gp = (GenericPrincipal) credentials.iterator().next(); + Assert.assertEquals("name1", gp.getName()); + } + + + private CallbackHandler createCallbackHandler(String callbackHandlerImplClassName) throws NoSuchMethodException, + SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + TestAuthenticator authenticator = new TestAuthenticator(); + if (callbackHandlerImplClassName != null) { + authenticator.setJaspicCallbackHandlerClass(callbackHandlerImplClassName); + } + Method createCallbackHandlerMethod = AuthenticatorBase.class.getDeclaredMethod("createCallbackHandler"); + createCallbackHandlerMethod.setAccessible(true); + return (CallbackHandler) createCallbackHandlerMethod.invoke(authenticator); + } + + + private static class TestAuthenticator extends AuthenticatorBase { + + @Override + protected boolean doAuthenticate(Request request, HttpServletResponse response) + throws IOException { + return false; + } + + @Override + protected String getAuthMethod() { + return null; + } + + } + + + private static class TestContainer extends ContainerBase { + + @Override + protected String getObjectNameKeyProperties() { + return null; + } + } + + + private static class TestRealm extends RealmBase { + + @Override + public Principal authenticate(String username, String password) { + if (getPassword(username).equals(password)) { + return getPrincipal(username); + } + return null; + } + + @Override + protected String getPassword(String username) { + return "password"; + } + + @Override + protected Principal getPrincipal(String username) { + return new GenericPrincipal(username); + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/authenticator/TestNonLoginAndBasicAuthenticator.java b/test/org/apache/catalina/authenticator/TestNonLoginAndBasicAuthenticator.java new file mode 100644 index 0000000..5475355 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestNonLoginAndBasicAuthenticator.java @@ -0,0 +1,606 @@ +/* + * 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.catalina.authenticator; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.session.ManagerBase; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +/** + * Test BasicAuthenticator and NonLoginAuthenticator when a + * SingleSignOn Valve is not active. + * + *

    + * In the absence of SSO support, these two authenticator classes + * both have quite simple behaviour. By testing them together, we + * can make sure they operate independently and confirm that no + * SSO logic has been accidentally triggered. + * + *

    + * r1495169 refactored BasicAuthenticator by creating an inner class + * called BasicCredentials. All edge cases associated with strangely + * encoded Base64 credentials are tested thoroughly by TestBasicAuthParser. + * Therefore, TestNonLoginAndBasicAuthenticator only needs to examine + * a sufficient set of test cases to verify the interface between + * BasicAuthenticator and BasicCredentials, which it does by running + * each test under a separate tomcat instance. + */ +public class TestNonLoginAndBasicAuthenticator extends TomcatBaseTest { + + protected static final boolean USE_COOKIES = true; + protected static final boolean NO_COOKIES = !USE_COOKIES; + + private static final String USER = "user"; + private static final String PWD = "pwd"; + private static final String ROLE = "role"; + private static final String NICE_METHOD = "Basic"; + + private static final String HTTP_PREFIX = "http://localhost:"; + private static final String CONTEXT_PATH_NOLOGIN = "/nologin"; + private static final String CONTEXT_PATH_LOGIN = "/login"; + private static final String URI_PROTECTED = "/protected"; + private static final String URI_PUBLIC = "/anyoneCanAccess"; + + private static final int SHORT_SESSION_TIMEOUT_SECS = 1; + private static final int MANAGER_SCAN_INTERVAL_SECS = 2; + private static final int MANAGER_EXPIRE_SESSIONS_FAST = 1; + private static final int EXTRA_DELAY_SECS = 5; + private static final long TIMEOUT_DELAY_MSECS = + ((SHORT_SESSION_TIMEOUT_SECS + + (MANAGER_SCAN_INTERVAL_SECS * MANAGER_EXPIRE_SESSIONS_FAST) + + EXTRA_DELAY_SECS) * 1000); + + private static final String CLIENT_AUTH_HEADER = "authorization"; + private static final String SERVER_AUTH_HEADER = "WWW-Authenticate"; + private static final String SERVER_COOKIE_HEADER = "Set-Cookie"; + private static final String CLIENT_COOKIE_HEADER = "Cookie"; + + private static final BasicCredentials NO_CREDENTIALS = null; + private static final BasicCredentials GOOD_CREDENTIALS = + new BasicCredentials(NICE_METHOD, USER, PWD); + private static final BasicCredentials STRANGE_CREDENTIALS = + new BasicCredentials("bAsIc", USER, PWD); + private static final BasicCredentials BAD_CREDENTIALS = + new BasicCredentials(NICE_METHOD, USER, "wrong"); + private static final BasicCredentials BAD_METHOD = + new BasicCredentials("BadMethod", USER, PWD); + + private Tomcat tomcat; + private Context basicContext; + private Context nonloginContext; + private List cookies; + + /* + * Try to access an unprotected resource in a webapp that + * does not have a login method defined. + * This should be permitted. + */ + @Test + public void testAcceptPublicNonLogin() throws Exception { + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, NO_COOKIES, + HttpServletResponse.SC_OK); + } + + /* + * Try to access a protected resource in a webapp that + * does not have a login method defined. + * This should be rejected with SC_FORBIDDEN 403 status. + */ + @Test + public void testRejectProtectedNonLogin() throws Exception { + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, NO_COOKIES, + HttpServletResponse.SC_FORBIDDEN); + } + + /* + * Try to access an unprotected resource in a webapp that + * has a BASIC login method defined. + * This should be permitted without a challenge. + */ + @Test + public void testAcceptPublicBasic() throws Exception { + doTestBasic(CONTEXT_PATH_LOGIN + URI_PUBLIC, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + } + + /* + * Try to access a protected resource in a webapp that + * has a BASIC login method defined. The access will be + * challenged with 401 SC_UNAUTHORIZED, and then be permitted + * once authenticated. + */ + @Test + public void testAcceptProtectedBasic() throws Exception { + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + } + + /* + * This is the same as testAcceptProtectedBasic (above), except + * using an invalid password. + */ + @Test + public void testAuthMethodBadCredentials() throws Exception { + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, BAD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + } + + /* + * This is the same as testAcceptProtectedBasic (above), except + * to verify the server follows RFC2617 by treating the auth-scheme + * token as case-insensitive. + */ + @Test + public void testAuthMethodCaseBasic() throws Exception { + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, STRANGE_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + } + + /* + * This is the same as testAcceptProtectedBasic (above), except + * using an invalid authentication method. + * + * Note: the container ensures the Basic login method is called. + * BasicAuthenticator does not find the expected authentication + * header method, and so does not extract any credentials. + * + * The request is rejected with 401 SC_UNAUTHORIZED status. RFC2616 + * says the response body should identify the auth-schemes that are + * acceptable for the container. + */ + @Test + public void testAuthMethodBadMethod() throws Exception { + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, BAD_METHOD, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + } + + /* + * The default behaviour of BASIC authentication does NOT create + * a session on the server. Verify that the client is required to + * send a valid authenticate header with every request to access + * protected resources. + */ + @Test + public void testBasicLoginWithoutSession() throws Exception { + + // this section is identical to testAuthMethodCaseBasic + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + + // next, try to access the protected resource while not providing + // credentials. This confirms the server has not retained any state + // data which might allow it to authenticate the client. Expect + // to be challenged with 401 SC_UNAUTHORIZED. + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + + // finally, provide credentials to confirm the resource + // can still be accessed with an authentication header. + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + } + + /* + * Test the optional behaviour of BASIC authentication to create + * a session on the server. The server will return a session cookie. + * + * 1. try to access a protected resource without credentials, so + * get Unauthorized status. + * 2. try to access a protected resource when providing credentials, + * so get OK status and a server session cookie. + * 3. access the protected resource once more using a session cookie. + * 4. repeat using the session cookie. + * + * Note: The FormAuthenticator is a two-step process and is protected + * from session fixation attacks by the default AuthenticatorBase + * changeSessionIdOnAuthentication setting of true. However, + * BasicAuthenticator is a one-step process and so the + * AuthenticatorBase does not reissue the sessionId. + */ + @Test + public void testBasicLoginSessionPersistence() throws Exception { + + setAlwaysUseSession(); + + // this section is identical to testAuthMethodCaseBasic + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + + // confirm the session is not recognised by the server alone + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + + // now provide the harvested session cookie for authentication + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + USE_COOKIES, HttpServletResponse.SC_OK); + + // finally, do it again with the cookie to be sure + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + USE_COOKIES, HttpServletResponse.SC_OK); + } + + /* + * Verify the timeout mechanism works for BASIC sessions. This test + * follows the flow of testBasicLoginSessionPersistence (above). + */ + @Test + public void testBasicLoginSessionTimeout() throws Exception { + + setAlwaysUseSession(); + setRapidSessionTimeout(); + + // this section is identical to testAuthMethodCaseBasic + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + + // now provide the harvested session cookie for authentication + List originalCookies = cookies; + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + USE_COOKIES, HttpServletResponse.SC_OK); + + // Force session to expire one second from now + forceSessionMaxInactiveInterval( + (Context) getTomcatInstance().getHost().findChild(CONTEXT_PATH_LOGIN), + SHORT_SESSION_TIMEOUT_SECS); + + // allow the session to time out and lose authentication + Thread.sleep(TIMEOUT_DELAY_MSECS); + + // provide the harvested session cookie for authentication + // to confirm it has expired + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + USE_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + + // finally, do BASIC reauthentication and get another session + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + + // slightly paranoid verification + boolean sameCookies = originalCookies.equals(cookies); + Assert.assertTrue(!sameCookies); + } + + /* + * Logon to access a protected resource in a webapp that uses + * BASIC authentication. Then try to access a protected resource + * in a different webapp which does not have a login method. + * This should be rejected with SC_FORBIDDEN 403 status, confirming + * there has been no cross-authentication between the webapps. + */ + @Test + public void testBasicLoginRejectProtected() throws Exception { + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + NO_COOKIES, HttpServletResponse.SC_FORBIDDEN); + } + + /* + * Try to use the session cookie from the BASIC webapp to request + * access to the webapp that does not have a login method. (This + * is equivalent to Single Signon, but without the Valve.) + * + * Verify there is no cross-authentication when using similar logic + * to testBasicLoginRejectProtected (above). + * + * This should be rejected with SC_FORBIDDEN 403 status. + */ + @Test + public void testBasicLoginRejectProtectedWithSession() throws Exception { + + setAlwaysUseSession(); + + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, NO_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, GOOD_CREDENTIALS, + NO_COOKIES, HttpServletResponse.SC_OK); + + // use the session cookie harvested with the other webapp + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + USE_COOKIES, HttpServletResponse.SC_FORBIDDEN); + } + + + private void doTestNonLogin(String uri, boolean useCookie, + int expectedRC) throws Exception { + + Map> reqHeaders = new HashMap<>(); + Map> respHeaders = new HashMap<>(); + + if (useCookie) { + addCookies(reqHeaders); + } + + ByteChunk bc = new ByteChunk(); + int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, + respHeaders); + + if (expectedRC != HttpServletResponse.SC_OK) { + Assert.assertEquals(expectedRC, rc); + Assert.assertTrue(bc.getLength() > 0); + } else { + Assert.assertEquals("OK", bc.toString()); + } + } + + private void doTestBasic(String uri, BasicCredentials credentials, + boolean useCookie, int expectedRC) throws Exception { + + Map> reqHeaders = new HashMap<>(); + Map> respHeaders = new HashMap<>(); + + if (useCookie) { + addCookies(reqHeaders); + } else { + if (credentials != null) { + List auth = new ArrayList<>(); + auth.add(credentials.getCredentials()); + reqHeaders.put(CLIENT_AUTH_HEADER, auth); + } + } + + ByteChunk bc = new ByteChunk(); + int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, + respHeaders); + + if (expectedRC != HttpServletResponse.SC_OK) { + Assert.assertEquals(expectedRC, rc); + Assert.assertTrue(bc.getLength() > 0); + if (expectedRC == HttpServletResponse.SC_UNAUTHORIZED) { + // The server should identify the acceptable method(s) + boolean methodFound = false; + List authHeaders = respHeaders.get(SERVER_AUTH_HEADER); + for (String authHeader : authHeaders) { + if (authHeader.contains(NICE_METHOD)) { + methodFound = true; + break; + } + } + Assert.assertTrue(methodFound); + } + } else { + Assert.assertEquals("OK", bc.toString()); + List newCookies = respHeaders.get(SERVER_COOKIE_HEADER); + if (newCookies != null) { + // harvest cookies whenever the server sends some new ones + saveCookies(respHeaders); + } + } + } + + + /* + * setup two webapps for every test + * + * note: the super class tearDown method will stop tomcat + */ + @Override + public void setUp() throws Exception { + + super.setUp(); + + // create a tomcat server using the default in-memory Realm + tomcat = getTomcatInstance(); + + // add the test user and role to the Realm + tomcat.addUser(USER, PWD); + tomcat.addRole(USER, ROLE); + + // setup both NonLogin and Login webapps + setUpNonLogin(); + setUpLogin(); + + tomcat.start(); + } + + + private void setUpNonLogin() throws Exception { + + // Must have a real docBase for webapps - just use temp + nonloginContext = tomcat.addContext(CONTEXT_PATH_NOLOGIN, + System.getProperty("java.io.tmpdir")); + + // Add protected servlet to the context + Tomcat.addServlet(nonloginContext, "TesterServlet1", new TesterServlet()); + nonloginContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet1"); + + SecurityCollection collection1 = new SecurityCollection(); + collection1.addPatternDecoded(URI_PROTECTED); + SecurityConstraint sc1 = new SecurityConstraint(); + sc1.addAuthRole(ROLE); + sc1.addCollection(collection1); + nonloginContext.addConstraint(sc1); + + // Add unprotected servlet to the context + Tomcat.addServlet(nonloginContext, "TesterServlet2", new TesterServlet()); + nonloginContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet2"); + + SecurityCollection collection2 = new SecurityCollection(); + collection2.addPatternDecoded(URI_PUBLIC); + SecurityConstraint sc2 = new SecurityConstraint(); + // do not add a role - which signals access permitted without one + sc2.addCollection(collection2); + nonloginContext.addConstraint(sc2); + + // Configure the authenticator and inherit the Realm from Engine + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("NONE"); + nonloginContext.setLoginConfig(lc); + AuthenticatorBase nonloginAuthenticator = new NonLoginAuthenticator(); + nonloginContext.getPipeline().addValve(nonloginAuthenticator); + } + + private void setUpLogin() throws Exception { + + // Must have a real docBase for webapps - just use temp + basicContext = tomcat.addContext(CONTEXT_PATH_LOGIN, + System.getProperty("java.io.tmpdir")); + + // Add protected servlet to the context + Tomcat.addServlet(basicContext, "TesterServlet3", new TesterServlet()); + basicContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet3"); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(URI_PROTECTED); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + basicContext.addConstraint(sc); + + // Add unprotected servlet to the context + Tomcat.addServlet(basicContext, "TesterServlet4", new TesterServlet()); + basicContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet4"); + + SecurityCollection collection2 = new SecurityCollection(); + collection2.addPatternDecoded(URI_PUBLIC); + SecurityConstraint sc2 = new SecurityConstraint(); + // do not add a role - which signals access permitted without one + sc2.addCollection(collection2); + basicContext.addConstraint(sc2); + + // Configure the authenticator and inherit the Realm from Engine + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("BASIC"); + basicContext.setLoginConfig(lc); + AuthenticatorBase basicAuthenticator = new BasicAuthenticator(); + basicContext.getPipeline().addValve(basicAuthenticator); + } + + /* + * Force non-default behaviour for both Authenticators + */ + private void setAlwaysUseSession() { + + ((AuthenticatorBase)basicContext.getAuthenticator()) + .setAlwaysUseSession(true); + ((AuthenticatorBase)nonloginContext.getAuthenticator()) + .setAlwaysUseSession(true); + } + + /* + * Force rapid timeout scanning for the Basic Authentication webapp + * The StandardManager default service cycle time is 10 seconds, + * with a session expiry scan every 6 cycles. + */ + private void setRapidSessionTimeout() { + basicContext.getParent().getParent().setBackgroundProcessorDelay( + MANAGER_SCAN_INTERVAL_SECS); + ((ManagerBase) basicContext.getManager()) + .setProcessExpiresFrequency(MANAGER_EXPIRE_SESSIONS_FAST); + } + /* + * Encapsulate the logic to generate an HTTP header + * for BASIC Authentication. + * Note: only used internally, so no need to validate arguments. + */ + private static final class BasicCredentials { + + private final String method; + private final String username; + private final String password; + private final String credentials; + + private BasicCredentials(String aMethod, + String aUsername, String aPassword) { + method = aMethod; + username = aUsername; + password = aPassword; + String userCredentials = username + ":" + password; + byte[] credentialsBytes = + userCredentials.getBytes(StandardCharsets.ISO_8859_1); + String base64auth = Base64.encodeBase64String(credentialsBytes); + credentials= method + " " + base64auth; + } + + private String getCredentials() { + return credentials; + } + } + + /* + * extract and save the server cookies from the incoming response + */ + protected void saveCookies(Map> respHeaders) { + // we only save the Cookie values, not header prefix + List cookieHeaders = respHeaders.get(SERVER_COOKIE_HEADER); + if (cookieHeaders == null) { + cookies = null; + } else { + cookies = new ArrayList<>(cookieHeaders.size()); + for (String cookieHeader : cookieHeaders) { + cookies.add(cookieHeader.substring(0, cookieHeader.indexOf(';'))); + } + } + } + + /* + * add all saved cookies to the outgoing request + */ + protected void addCookies(Map> reqHeaders) { + if ((cookies != null) && (cookies.size() > 0)) { + StringBuilder cookieHeader = new StringBuilder(); + boolean first = true; + for (String cookie : cookies) { + if (!first) { + cookieHeader.append(';'); + } else { + first = false; + } + cookieHeader.append(cookie); + } + List cookieHeaderList = new ArrayList<>(1); + cookieHeaderList.add(cookieHeader.toString()); + reqHeaders.put(CLIENT_COOKIE_HEADER, cookieHeaderList); + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java b/test/org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java new file mode 100644 index 0000000..c034775 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java @@ -0,0 +1,679 @@ +/* + * 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.catalina.authenticator; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Session; +import org.apache.catalina.session.ManagerBase; +import org.apache.catalina.startup.TesterServletEncodeUrl; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +/** + * Test BasicAuthenticator and NonLoginAuthenticator when a + * SingleSignOn Valve is active. + * + *

    + * In the absence of SSO support, a webapp using NonLoginAuthenticator + * simply cannot access protected resources. These tests exercise the + * the way successfully authenticating a different webapp under the + * BasicAuthenticator triggers the additional SSO logic for both webapps. + * + *

    + * The two Authenticators are thoroughly exercised by two other unit test + * classes: TestBasicAuthParser and TestNonLoginAndBasicAuthenticator. + * This class mainly examines the way the Single SignOn Valve interacts with + * two webapps when the second cannot be authenticated directly, but needs + * to inherit its authentication via the other. + * + *

    + * When the server and client can both use cookies, the authentication + * is preserved through the exchange of a JSSOSESSIONID cookie, which + * is different to the individual and unique JSESSIONID cookies assigned + * separately to the two webapp sessions. + * + *

    + * The other situation examined is where the server returns authentication + * cookies, but the client is configured to ignore them. The Tomcat + * documentation clearly states that SSO requires the client to + * support cookies, so access to resources in other webapp containers + * receives no SSO assistance. + */ +public class TestSSOnonLoginAndBasicAuthenticator extends TomcatBaseTest { + + protected static final boolean USE_COOKIES = true; + protected static final boolean NO_COOKIES = !USE_COOKIES; + + private static final String USER = "user"; + private static final String PWD = "pwd"; + private static final String ROLE = "role"; + private static final String NICE_METHOD = "Basic"; + + private static final String HTTP_PREFIX = "http://localhost:"; + private static final String CONTEXT_PATH_NOLOGIN = "/nologin"; + private static final String CONTEXT_PATH_LOGIN = "/login"; + private static final String URI_PROTECTED = "/protected"; + private static final String URI_PUBLIC = "/anyoneCanAccess"; + + // session expiry in web.xml is defined in minutes + private static final int SHORT_SESSION_TIMEOUT_MINS = 1; + private static final int LONG_SESSION_TIMEOUT_MINS = 2; + + // we don't change the expiry scan interval - just the iteration count + private static final int MANAGER_SCAN_INTERVAL_SECS = 10; + private static final int MANAGER_EXPIRE_SESSIONS_FAST = 1; + + // now compute some delays - beware of the units! + private static final int EXTRA_DELAY_SECS = 5; + private static final int TIMEOUT_WAIT_SECS = EXTRA_DELAY_SECS + + (MANAGER_SCAN_INTERVAL_SECS * MANAGER_EXPIRE_SESSIONS_FAST) * 5; + + private static final String CLIENT_AUTH_HEADER = "authorization"; + private static final String SERVER_AUTH_HEADER = "WWW-Authenticate"; + private static final String SERVER_COOKIE_HEADER = "Set-Cookie"; + private static final String CLIENT_COOKIE_HEADER = "Cookie"; + private static final String ENCODE_SESSION_PARAM = "jsessionid"; + private static final String ENCODE_SSOSESSION_PARAM = "jssosessionid"; + + private static final + TestSSOnonLoginAndBasicAuthenticator.BasicCredentials + NO_CREDENTIALS = null; + private static final + TestSSOnonLoginAndBasicAuthenticator.BasicCredentials + GOOD_CREDENTIALS = + new TestSSOnonLoginAndBasicAuthenticator.BasicCredentials( + NICE_METHOD, USER, PWD); + + private Tomcat tomcat; + private Context basicContext; + private Context nonloginContext; + private List cookies; + private String encodedURL; + + /* + * Run some checks without an established SSO session + * to make sure the test environment is correct. + */ + @Test + public void testEssentialEnvironment() throws Exception { + + // should be permitted to access an unprotected resource. + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, + USE_COOKIES, HttpServletResponse.SC_OK); + + // should not be permitted to access a protected resource + // with the two Authenticators used in the remaining tests. + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + USE_COOKIES, HttpServletResponse.SC_FORBIDDEN); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + public void testEssentialEnvironmentWithoutCookies() throws Exception { + + // should be permitted to access an unprotected resource. + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, + NO_COOKIES, HttpServletResponse.SC_OK); + + // should not be permitted to access a protected resource + // with the two Authenticators used in the remaining tests. + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + NO_COOKIES, HttpServletResponse.SC_FORBIDDEN); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, NO_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + } + + /* + * Logon to access a protected resource using BASIC authentication, + * which will establish an SSO session. + * Wait until the SSO session times-out, then try to re-access + * the resource. This should be rejected with SC_FORBIDDEN 401 status. + * + * Note: this test should run for ~10 seconds. + */ + @Test + public void testBasicAccessAndSessionTimeout() throws Exception { + + setRapidSessionTimeoutDetection(); + + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + GOOD_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_OK); + + // verify the SSOID exists as a cookie + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + GOOD_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_OK); + + // make the session time out and lose authentication + doImminentSessionTimeout(basicContext); + + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + } + + + /* + * Logon to access a protected resource using BASIC authentication, + * which will establish an SSO session. + * Immediately try to access a protected resource in the NonLogin + * webapp while providing the SSO session cookie received from the + * first webapp. This should be successful with SC_OK 200 status. + */ + @Test + public void testBasicLoginThenAcceptWithCookies() throws Exception { + + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, NO_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + GOOD_CREDENTIALS, USE_COOKIES, HttpServletResponse.SC_OK); + + // send the cookie which proves we have an authenticated SSO session + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + USE_COOKIES, HttpServletResponse.SC_OK); + } + + /* + * Logon to access a protected resource using BASIC authentication, + * which will establish an SSO session. + * Immediately try to access a protected resource in the NonLogin + * webapp, but without sending the SSO session cookie. + * This should be rejected with SC_FORBIDDEN 403 status. + */ + @Test + public void testBasicLoginThenRejectWithoutCookie() throws Exception { + + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + GOOD_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_OK); + + // fail to send the authentication cookie to the other webapp. + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + NO_COOKIES, HttpServletResponse.SC_FORBIDDEN); + } + + /* + * Logon to access a protected resource using BASIC authentication, + * which will establish an SSO session. + * Then try to access a protected resource in the NonLogin + * webapp by sending the JSESSIONID from the redirect header. + * The access request should be rejected because the Basic webapp's + * sessionID is not valid for any other container. + */ + @Test + public void testBasicAccessThenAcceptAuthWithUri() throws Exception { + + setAlwaysUseSession(); + + // first, fail to access the protected resource without credentials + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, NO_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + + // now, access the protected resource with good credentials + // to establish the session + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + GOOD_CREDENTIALS, NO_COOKIES, + HttpServletResponse.SC_OK); + + // next, access it again to harvest the session id url parameter + String forwardParam = "?nextUrl=" + CONTEXT_PATH_LOGIN + URI_PROTECTED; + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED + forwardParam, + GOOD_CREDENTIALS, NO_COOKIES, + HttpServletResponse.SC_OK); + + // verify the sessionID was encoded in the absolute URL + String firstEncodedURL = encodedURL; + Assert.assertTrue(firstEncodedURL.contains(ENCODE_SESSION_PARAM)); + + // access the protected resource with the encoded url (with session id) + doTestBasic(firstEncodedURL + forwardParam, + NO_CREDENTIALS, NO_COOKIES, + HttpServletResponse.SC_OK); + + // verify the sessionID has not changed + // verify the SSO sessionID was not encoded + String secondEncodedURL = encodedURL; + Assert.assertEquals(firstEncodedURL, secondEncodedURL); + Assert.assertFalse(firstEncodedURL.contains(ENCODE_SSOSESSION_PARAM)); + + // extract the first container's session ID + int ix = secondEncodedURL.indexOf(ENCODE_SESSION_PARAM); + String sessionId = secondEncodedURL.substring(ix); + + // expect to fail using that sessionID in a different container + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED + ";" + sessionId, + NO_COOKIES, HttpServletResponse.SC_FORBIDDEN); + } + + /* + * Logon to access a protected resource using BASIC authentication, + * which will establish an SSO session. + * Immediately try to access a protected resource in the NonLogin + * webapp while providing the SSO session cookie received from the + * first webapp. This should be successful with SC_OK 200 status. + * + * Then, wait long enough for the BASIC session to expire. (The SSO + * session should remain active because the NonLogin session has + * not yet expired). + * Try to access the protected resource again, before the SSO session + * has expired. This should be successful with SC_OK 200 status. + * + * Finally, wait for the non-login session to expire and try again.. + * This should be rejected with SC_FORBIDDEN 403 status. + * + * (see bugfix https://bz.apache.org/bugzilla/show_bug.cgi?id=52303) + * + * Note: this test should run for ~20 seconds. + */ + @Test + public void testBasicExpiredAcceptProtectedWithCookies() throws Exception { + + setRapidSessionTimeoutDetection(); + + // begin with a repeat of testBasicLoginAcceptProtectedWithCookies + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + GOOD_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_OK); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + USE_COOKIES, HttpServletResponse.SC_OK); + + // wait long enough for the BASIC session to expire, + // but not long enough for the NonLogin session expiry. + doImminentSessionTimeout(basicContext); + + // this successful NonLogin access should replenish the + // the individual session expiry time and keep the SSO session alive + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + USE_COOKIES, HttpServletResponse.SC_OK); + + // wait long enough for the NonLogin session to expire, + // which will also tear down the SSO session at the same time. + doImminentSessionTimeout(nonloginContext); + + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, USE_COOKIES, + HttpServletResponse.SC_FORBIDDEN); + doTestBasic(CONTEXT_PATH_LOGIN + URI_PROTECTED, + NO_CREDENTIALS, USE_COOKIES, + HttpServletResponse.SC_UNAUTHORIZED); + + } + + + public void doTestNonLogin(String uri, boolean useCookie, + int expectedRC) throws Exception { + + Map> reqHeaders = new HashMap<>(); + Map> respHeaders = new HashMap<>(); + + if (useCookie && (cookies != null)) { + addCookies(reqHeaders); + } + + ByteChunk bc = new ByteChunk(); + int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, + respHeaders); + + if (expectedRC != HttpServletResponse.SC_OK) { + Assert.assertEquals(expectedRC, rc); + Assert.assertTrue(bc.getLength() > 0); + } else { + Assert.assertEquals("OK", bc.toString()); + } +} + + private void doTestBasic(String uri, + TestSSOnonLoginAndBasicAuthenticator.BasicCredentials credentials, + boolean useCookie, int expectedRC) throws Exception { + + Map> reqHeaders = new HashMap<>(); + Map> respHeaders = new HashMap<>(); + + if (useCookie && (cookies != null)) { + addCookies(reqHeaders); + } else { + if (credentials != null) { + List auth = new ArrayList<>(); + auth.add(credentials.getCredentials()); + reqHeaders.put(CLIENT_AUTH_HEADER, auth); + } + } + + ByteChunk bc = new ByteChunk(); + int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, + respHeaders); + + Assert.assertEquals("Unexpected Return Code", expectedRC, rc); + if (expectedRC != HttpServletResponse.SC_OK) { + Assert.assertTrue(bc.getLength() > 0); + if (expectedRC == HttpServletResponse.SC_UNAUTHORIZED) { + // The server should identify the acceptable method(s) + boolean methodFound = false; + List authHeaders = respHeaders.get(SERVER_AUTH_HEADER); + for (String authHeader : authHeaders) { + if (authHeader.contains(NICE_METHOD)) { + methodFound = true; + break; + } + } + Assert.assertTrue(methodFound); + } + } else { + String thePage = bc.toString(); + Assert.assertNotNull(thePage); + Assert.assertTrue(thePage.startsWith("OK")); + if (useCookie) { + List newCookies = respHeaders.get(SERVER_COOKIE_HEADER); + if (newCookies != null) { + // harvest cookies whenever the server sends some new ones + cookies = newCookies; + } + } else { + encodedURL = ""; + final String start = ""; + int iStart = thePage.indexOf(start); + int iEnd = 0; + if (iStart > -1) { + iStart += start.length(); + iEnd = thePage.indexOf(end, iStart); + if (iEnd > -1) { + encodedURL = thePage.substring(iStart, iEnd); + } + } + } + } + } + + + + + /* + * setup two webapps for every test + * + * note: the super class tearDown method will stop tomcat + */ + @Override + public void setUp() throws Exception { + + super.setUp(); + + // create a tomcat server using the default in-memory Realm + tomcat = getTomcatInstance(); + + // associate the SingeSignOn Valve before the Contexts + SingleSignOn sso = new SingleSignOn(); + tomcat.getHost().getPipeline().addValve(sso); + + // add the test user and role to the Realm + tomcat.addUser(USER, PWD); + tomcat.addRole(USER, ROLE); + + // setup both NonLogin and Login webapps + setUpNonLogin(); + setUpLogin(); + + tomcat.start(); + } + + @Override + public void tearDown() throws Exception { + + tomcat.stop(); + } + + private void setUpNonLogin() throws Exception { + + // Must have a real docBase for webapps - just use temp + nonloginContext = tomcat.addContext(CONTEXT_PATH_NOLOGIN, + System.getProperty("java.io.tmpdir")); + nonloginContext.setSessionTimeout(LONG_SESSION_TIMEOUT_MINS); + + // Add protected servlet to the context + Tomcat.addServlet(nonloginContext, "TesterServlet1", + new TesterServletEncodeUrl()); + nonloginContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet1"); + + SecurityCollection collection1 = new SecurityCollection(); + collection1.addPatternDecoded(URI_PROTECTED); + SecurityConstraint sc1 = new SecurityConstraint(); + sc1.addAuthRole(ROLE); + sc1.addCollection(collection1); + nonloginContext.addConstraint(sc1); + + // Add unprotected servlet to the context + Tomcat.addServlet(nonloginContext, "TesterServlet2", + new TesterServletEncodeUrl()); + nonloginContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet2"); + + SecurityCollection collection2 = new SecurityCollection(); + collection2.addPatternDecoded(URI_PUBLIC); + SecurityConstraint sc2 = new SecurityConstraint(); + // do not add a role - which signals access permitted without one + sc2.addCollection(collection2); + nonloginContext.addConstraint(sc2); + + // Configure the authenticator and inherit the Realm from Engine + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("NONE"); + nonloginContext.setLoginConfig(lc); + AuthenticatorBase nonloginAuthenticator = new NonLoginAuthenticator(); + nonloginContext.getPipeline().addValve(nonloginAuthenticator); + } + + private void setUpLogin() throws Exception { + + // Must have a real docBase for webapps - just use temp + basicContext = tomcat.addContext(CONTEXT_PATH_LOGIN, + System.getProperty("java.io.tmpdir")); + basicContext.setSessionTimeout(SHORT_SESSION_TIMEOUT_MINS); + + // Add protected servlet to the context + Tomcat.addServlet(basicContext, "TesterServlet3", + new TesterServletEncodeUrl()); + basicContext.addServletMappingDecoded(URI_PROTECTED, "TesterServlet3"); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(URI_PROTECTED); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + basicContext.addConstraint(sc); + + // Add unprotected servlet to the context + Tomcat.addServlet(basicContext, "TesterServlet4", + new TesterServletEncodeUrl()); + basicContext.addServletMappingDecoded(URI_PUBLIC, "TesterServlet4"); + SecurityCollection collection2 = new SecurityCollection(); + collection2.addPatternDecoded(URI_PUBLIC); + SecurityConstraint sc2 = new SecurityConstraint(); + // do not add a role - which signals access permitted without one + sc2.addCollection(collection2); + basicContext.addConstraint(sc2); + + // Configure the authenticator and inherit the Realm from Engine + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("BASIC"); + basicContext.setLoginConfig(lc); + AuthenticatorBase basicAuthenticator = new BasicAuthenticator(); + basicContext.getPipeline().addValve(basicAuthenticator); + } + + /* + * extract and save the server cookies from the incoming response + */ + protected void saveCookies(Map> respHeaders) { + // we only save the Cookie values, not header prefix + List cookieHeaders = respHeaders.get(SERVER_COOKIE_HEADER); + if (cookieHeaders == null) { + cookies = null; + } else { + cookies = new ArrayList<>(cookieHeaders.size()); + for (String cookieHeader : cookieHeaders) { + cookies.add(cookieHeader.substring(0, cookieHeader.indexOf(';'))); + } + } + } + + /* + * add all saved cookies to the outgoing request + */ + protected void addCookies(Map> reqHeaders) { + if ((cookies != null) && (cookies.size() > 0)) { + StringBuilder cookieHeader = new StringBuilder(); + boolean first = true; + for (String cookie : cookies) { + if (!first) { + cookieHeader.append(';'); + } else { + first = false; + } + cookieHeader.append(cookie); + } + List cookieHeaderList = new ArrayList<>(1); + cookieHeaderList.add(cookieHeader.toString()); + reqHeaders.put(CLIENT_COOKIE_HEADER, cookieHeaderList); + } + } + + /* + * Force non-default behaviour for both Authenticators. + * The session id will not be regenerated after authentication, + * which is less secure but needed for browsers that will not + * handle cookies. + */ + private void setAlwaysUseSession() { + + ((AuthenticatorBase) basicContext.getAuthenticator()) + .setAlwaysUseSession(true); + ((AuthenticatorBase) nonloginContext.getAuthenticator()) + .setAlwaysUseSession(true); + } + + /* + * Force faster timeout for an active Container than can + * be defined in web.xml. By getting to the active Session we + * can choose seconds instead of minutes. + * Note: shamelessly cloned from ManagerBase - beware of synch issues + * on the underlying sessions. + */ + private void doImminentSessionTimeout(Context activeContext) { + + ManagerBase manager = (ManagerBase) activeContext.getManager(); + Session[] sessions = manager.findSessions(); + for (Session session : sessions) { + if (session != null && session.isValid()) { + session.setMaxInactiveInterval(EXTRA_DELAY_SECS); + // leave it to be expired by the manager + } + } + + + try { + Thread.sleep(EXTRA_DELAY_SECS * 1000); + } catch (InterruptedException ie) { + // ignored + } + + // Paranoid verification that active sessions have now gone + int count = 0; + sessions = manager.findSessions(); + while (sessions.length != 0 && count < TIMEOUT_WAIT_SECS) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Ignore + } + sessions = manager.findSessions(); + count++; + } + + sessions = manager.findSessions(); + Assert.assertTrue(sessions.length == 0); + } + + /* + * Force rapid timeout scanning for both webapps + * The StandardManager default service cycle time is 10 seconds, + * with a session expiry scan every 6 cycles. + */ + private void setRapidSessionTimeoutDetection() { + + ((ManagerBase) basicContext.getManager()) + .setProcessExpiresFrequency(MANAGER_EXPIRE_SESSIONS_FAST); + ((ManagerBase) nonloginContext.getManager()) + .setProcessExpiresFrequency(MANAGER_EXPIRE_SESSIONS_FAST); + } + + /* + * Encapsulate the logic to generate an HTTP header + * for BASIC Authentication. + * Note: only used internally, so no need to validate arguments. + */ + private static final class BasicCredentials { + + private final String method; + private final String username; + private final String password; + private final String credentials; + + private BasicCredentials(String aMethod, + String aUsername, String aPassword) { + method = aMethod; + username = aUsername; + password = aPassword; + String userCredentials = username + ":" + password; + byte[] credentialsBytes = + userCredentials.getBytes(StandardCharsets.ISO_8859_1); + String base64auth = Base64.encodeBase64String(credentialsBytes); + credentials= method + " " + base64auth; + } + + private String getCredentials() { + return credentials; + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/authenticator/TestSSOnonLoginAndDigestAuthenticator.java b/test/org/apache/catalina/authenticator/TestSSOnonLoginAndDigestAuthenticator.java new file mode 100644 index 0000000..0a51b08 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestSSOnonLoginAndDigestAuthenticator.java @@ -0,0 +1,501 @@ +/* + * 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.catalina.authenticator; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; + +/** + * Test DigestAuthenticator and NonLoginAuthenticator when a + * SingleSignOn Valve is active. + * + *

    + * In the absence of SSO support, a webapp using NonLoginAuthenticator + * simply cannot access protected resources. These tests exercise the + * the way successfully authenticating a different webapp under the + * DigestAuthenticator triggers the additional SSO logic for both webapps. + * + *

    + * Note: these tests are intended to exercise the SSO logic of the + * Authenticator, but not to comprehensively test all of its logic paths. + * That is the responsibility of the non-SSO test suite. + */ +public class TestSSOnonLoginAndDigestAuthenticator extends TomcatBaseTest { + + private static final String USER = "user"; + private static final String PWD = "pwd"; + private static final String ROLE = "role"; + + private static final String HTTP_PREFIX = "http://localhost:"; + private static final String CONTEXT_PATH_NOLOGIN = "/nologin"; + private static final String CONTEXT_PATH_DIGEST = "/digest"; + private static final String URI_PROTECTED = "/protected"; + private static final String URI_PUBLIC = "/anyoneCanAccess"; + + private static final int SHORT_TIMEOUT_SECS = 4; + private static final long SHORT_TIMEOUT_DELAY_MSECS = + ((SHORT_TIMEOUT_SECS + 3) * 1000); + private static final int LONG_TIMEOUT_SECS = 10; + private static final long LONG_TIMEOUT_DELAY_MSECS = + ((LONG_TIMEOUT_SECS + 2) * 1000); + + private static final String CLIENT_AUTH_HEADER = "authorization"; + private static final String OPAQUE = "opaque"; + private static final String NONCE = "nonce"; + private static final String REALM = "realm"; + private static final String CNONCE = "cnonce"; + + private static String NC1 = "00000001"; + private static String NC2 = "00000002"; + private static String QOP = "auth"; + + private static String SERVER_COOKIES = "Set-Cookie"; + private static String BROWSER_COOKIES = "Cookie"; + + private List cookies; + + /* + * Try to access an unprotected resource without an + * established SSO session. + * This should be permitted. + */ + @Test + public void testAcceptPublicNonLogin() throws Exception { + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PUBLIC, + true, false, 200); + } + + /* + * Try to access a protected resource without an established + * SSO session. + * This should be rejected with SC_FORBIDDEN 403 status. + */ + @Test + public void testRejectProtectedNonLogin() throws Exception { + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + false, true, 403); + } + + /* + * Logon to access a protected resource using DIGEST authentication, + * which will establish an SSO session. + * Wait until the SSO session times-out, then try to re-access + * the resource. + * This should be rejected with SC_FORBIDDEN 401 status, which + * will then be followed by successful re-authentication. + */ + @Test + public void testDigestLoginSessionTimeout() throws Exception { + doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED, + true, 401, true, true, NC1, CNONCE, QOP, true); + // wait long enough for my session to expire + Thread.sleep(LONG_TIMEOUT_DELAY_MSECS); + // must change the client nonce to succeed + doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED, + true, 401, true, true, NC2, CNONCE, QOP, true); + } + + /* + * Logon to access a protected resource using DIGEST authentication, + * which will establish an SSO session. + * Immediately try to access a protected resource in the NonLogin + * webapp, but without sending the SSO session cookie. + * This should be rejected with SC_FORBIDDEN 403 status. + */ + @Test + public void testDigestLoginRejectProtectedWithoutCookies() throws Exception { + doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED, + true, 401, true, true, NC1, CNONCE, QOP, true); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + false, true, 403); + } + + /* + * Logon to access a protected resource using DIGEST authentication, + * which will establish an SSO session. + * Immediately try to access a protected resource in the NonLogin + * webapp while sending the SSO session cookie provided by the + * first webapp. + * This should be successful with SC_OK 200 status. + */ + @Test + public void testDigestLoginAcceptProtectedWithCookies() throws Exception { + doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED, + true, 401, true, true, NC1, CNONCE, QOP, true); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + true, false, 200); + } + + /* + * Logon to access a protected resource using DIGEST authentication, + * which will establish an SSO session. + * Immediately try to access a protected resource in the NonLogin + * webapp while sending the SSO session cookie provided by the + * first webapp. + * This should be successful with SC_OK 200 status. + * + * Then, wait long enough for the DIGEST session to expire. (The SSO + * session should remain active because the NonLogin session has + * not yet expired). + * + * Try to access the protected resource again, before the SSO session + * has expired. + * This should be successful with SC_OK 200 status. + * + * Finally, wait for the non-login session to expire and try again.. + * This should be rejected with SC_FORBIDDEN 403 status. + * + * (see bugfix https://bz.apache.org/bugzilla/show_bug.cgi?id=52303) + */ + @Test + public void testDigestExpiredAcceptProtectedWithCookies() throws Exception { + doTestDigest(USER, PWD, CONTEXT_PATH_DIGEST + URI_PROTECTED, + true, 401, true, true, NC1, CNONCE, QOP, true); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + true, false, 200); + + // wait long enough for the BASIC session to expire, + // but not long enough for NonLogin session expiry + Thread.sleep(SHORT_TIMEOUT_DELAY_MSECS); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + true, false, 200); + + // wait long enough for my NonLogin session to expire + // and tear down the SSO session at the same time. + Thread.sleep(LONG_TIMEOUT_DELAY_MSECS); + doTestNonLogin(CONTEXT_PATH_NOLOGIN + URI_PROTECTED, + false, true, 403); + } + + + public void doTestNonLogin(String uri, boolean addCookies, + boolean expectedReject, int expectedRC) + throws Exception { + + Map> reqHeaders = new HashMap<>(); + Map> respHeaders = new HashMap<>(); + + ByteChunk bc = new ByteChunk(); + if (addCookies) { + addCookies(reqHeaders); + } + int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, + respHeaders); + + if (expectedReject) { + Assert.assertEquals(expectedRC, rc); + Assert.assertTrue(bc.getLength() > 0); + } else { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + saveCookies(respHeaders); + } +} + + public void doTestDigest(String user, String pwd, String uri, + boolean expectedReject1, int expectedRC1, + boolean useServerNonce, boolean useServerOpaque, + String nc1, String cnonce, + String qop, boolean req2expect200) + throws Exception { + + String digestUri= uri; + + List auth = new ArrayList<>(); + Map> reqHeaders1 = new HashMap<>(); + Map> respHeaders1 = new HashMap<>(); + + // the first access attempt should be challenged + auth.add(buildDigestResponse(user, pwd, digestUri, REALM, "null", + "null", nc1, cnonce, qop)); + reqHeaders1.put(CLIENT_AUTH_HEADER, auth); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders1, + respHeaders1); + + if (expectedReject1) { + Assert.assertEquals(expectedRC1, rc); + Assert.assertTrue(bc.getLength() > 0); + } else { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + saveCookies(respHeaders1); + return; + } + + // Second request should succeed (if we use the server nonce) + Map> reqHeaders2 = new HashMap<>(); + Map> respHeaders2 = new HashMap<>(); + + auth.clear(); + if (useServerNonce) { + if (useServerOpaque) { + auth.add(buildDigestResponse(user, pwd, digestUri, + getAuthToken(respHeaders1, REALM), + getAuthToken(respHeaders1, NONCE), + getAuthToken(respHeaders1, OPAQUE), + nc1, cnonce, qop)); + } else { + auth.add(buildDigestResponse(user, pwd, digestUri, + getAuthToken(respHeaders1, REALM), + getAuthToken(respHeaders1, NONCE), + "null", nc1, cnonce, qop)); + } + } else { + auth.add(buildDigestResponse(user, pwd, digestUri, + getAuthToken(respHeaders2, REALM), + "null", getAuthToken(respHeaders1, OPAQUE), + nc1, cnonce, QOP)); + } + reqHeaders2.put(CLIENT_AUTH_HEADER, auth); + + bc.recycle(); + rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders2, + respHeaders2); + + if (req2expect200) { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + saveCookies(respHeaders2); + } else { + Assert.assertEquals(401, rc); + Assert.assertTrue((bc.getLength() > 0)); + } + } + + + @Override + public void setUp() throws Exception { + super.setUp(); + + // create a tomcat server using the default in-memory Realm + Tomcat tomcat = getTomcatInstance(); + + // associate the SingeSignOn Valve before the Contexts + SingleSignOn sso = new SingleSignOn(); + tomcat.getHost().getPipeline().addValve(sso); + + // add the test user and role to the Realm + tomcat.addUser(USER, PWD); + tomcat.addRole(USER, ROLE); + + // setup both NonLogin, Login and digest webapps + setUpNonLogin(tomcat); + setUpDigest(tomcat); + + tomcat.start(); + } + + private void setUpNonLogin(Tomcat tomcat) throws Exception { + + // Must have a real docBase for webapps - just use temp + Context ctxt = tomcat.addContext(CONTEXT_PATH_NOLOGIN, + System.getProperty("java.io.tmpdir")); + ctxt.setSessionTimeout(LONG_TIMEOUT_SECS); + + // Add protected servlet + Tomcat.addServlet(ctxt, "TesterServlet1", new TesterServlet()); + ctxt.addServletMappingDecoded(URI_PROTECTED, "TesterServlet1"); + SecurityCollection collection1 = new SecurityCollection(); + collection1.addPatternDecoded(URI_PROTECTED); + SecurityConstraint sc1 = new SecurityConstraint(); + sc1.addAuthRole(ROLE); + sc1.addCollection(collection1); + ctxt.addConstraint(sc1); + + // Add unprotected servlet + Tomcat.addServlet(ctxt, "TesterServlet2", new TesterServlet()); + ctxt.addServletMappingDecoded(URI_PUBLIC, "TesterServlet2"); + SecurityCollection collection2 = new SecurityCollection(); + collection2.addPatternDecoded(URI_PUBLIC); + SecurityConstraint sc2 = new SecurityConstraint(); + // do not add a role - which signals access permitted without one + sc2.addCollection(collection2); + ctxt.addConstraint(sc2); + + // Configure the appropriate authenticator + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("NONE"); + ctxt.setLoginConfig(lc); + ctxt.getPipeline().addValve(new NonLoginAuthenticator()); + } + + private void setUpDigest(Tomcat tomcat) throws Exception { + + // Must have a real docBase for webapps - just use temp + Context ctxt = tomcat.addContext(CONTEXT_PATH_DIGEST, + System.getProperty("java.io.tmpdir")); + ctxt.setSessionTimeout(SHORT_TIMEOUT_SECS); + + // Add protected servlet + Tomcat.addServlet(ctxt, "TesterServlet3", new TesterServlet()); + ctxt.addServletMappingDecoded(URI_PROTECTED, "TesterServlet3"); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(URI_PROTECTED); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + ctxt.addConstraint(sc); + + // Configure the appropriate authenticator + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("DIGEST"); + ctxt.setLoginConfig(lc); + ctxt.getPipeline().addValve(new DigestAuthenticator()); + } + + protected static String getAuthToken( + Map> respHeaders, String token) { + + final String AUTH_PREFIX = "=\""; + final String AUTH_SUFFIX = "\""; + List authHeaders = + respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME); + + // Assume there is only one + String authHeader = authHeaders.get(0); + String searchFor = token + AUTH_PREFIX; + int start = authHeader.indexOf(searchFor) + searchFor.length(); + int end = authHeader.indexOf(AUTH_SUFFIX, start); + return authHeader.substring(start, end); + } + + /* + * Notes from RFC2617 + * H(data) = MD5(data) + * KD(secret, data) = H(concat(secret, ":", data)) + * A1 = unq(username-value) ":" unq(realm-value) ":" passwd + * A2 = Method ":" digest-uri-value + * request-digest = <"> < KD ( H(A1), unq(nonce-value) + ":" nc-value + ":" unq(cnonce-value) + ":" unq(qop-value) + ":" H(A2) + ) <"> + */ + private static String buildDigestResponse(String user, String pwd, + String uri, String realm, String nonce, String opaque, String nc, + String cnonce, String qop) { + + String a1 = user + ":" + realm + ":" + pwd; + String a2 = "GET:" + uri; + + String digestA1 = digest(a1); + String digestA2 = digest(a2); + + String response; + if (qop == null) { + response = digestA1 + ":" + nonce + ":" + digestA2; + } else { + response = digestA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + + qop + ":" + digestA2; + } + + String md5response = digest(response); + + StringBuilder auth = new StringBuilder(); + auth.append("Digest username=\""); + auth.append(user); + auth.append("\", realm=\""); + auth.append(realm); + auth.append("\", nonce=\""); + auth.append(nonce); + auth.append("\", uri=\""); + auth.append(uri); + auth.append("\", opaque=\""); + auth.append(opaque); + auth.append("\", response=\""); + auth.append(md5response); + auth.append("\""); + if (qop != null) { + auth.append(", qop="); + auth.append(qop); + } + if (nc != null) { + auth.append(", nc="); + auth.append(nc); + } + if (cnonce != null) { + auth.append(", cnonce=\""); + auth.append(cnonce); + auth.append("\""); + } + + return auth.toString(); + } + + private static String digest(String input) { + return HexUtils.toHexString(ConcurrentMessageDigest.digestMD5( + input.getBytes(StandardCharsets.UTF_8))); + } + + /* + * extract and save the server cookies from the incoming response + */ + protected void saveCookies(Map> respHeaders) { + + // we only save the Cookie values, not header prefix + List cookieHeaders = respHeaders.get(SERVER_COOKIES); + if (cookieHeaders == null) { + cookies = null; + } else { + cookies = new ArrayList<>(cookieHeaders.size()); + for (String cookieHeader : cookieHeaders) { + cookies.add(cookieHeader.substring(0, cookieHeader.indexOf(';'))); + } + } + } + + /* + * add all saved cookies to the outgoing request + */ + protected void addCookies(Map> reqHeaders) { + if ((cookies != null) && (cookies.size() > 0)) { + StringBuilder cookieHeader = new StringBuilder(); + boolean first = true; + for (String cookie : cookies) { + if (!first) { + cookieHeader.append(';'); + } else { + first = false; + } + cookieHeader.append(cookie); + } + List cookieHeaderList = new ArrayList<>(1); + cookieHeaderList.add(cookieHeader.toString()); + reqHeaders.put(BROWSER_COOKIES, cookieHeaderList); + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/authenticator/TesterCallbackHandlerImpl.java b/test/org/apache/catalina/authenticator/TesterCallbackHandlerImpl.java new file mode 100644 index 0000000..b989363 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TesterCallbackHandlerImpl.java @@ -0,0 +1,36 @@ +/** + * 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.catalina.authenticator; + +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; + +public class TesterCallbackHandlerImpl implements CallbackHandler { + + public TesterCallbackHandlerImpl() { + // Default constructor required by reflection + } + + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + // don't have to do anything; needed only for instantiation + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java b/test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java new file mode 100644 index 0000000..1d95b0b --- /dev/null +++ b/test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java @@ -0,0 +1,298 @@ +/* + * 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.catalina.authenticator; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Request; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.core.StandardService; +import org.apache.catalina.filters.TesterHttpServletResponse; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterDigestAuthenticatorPerformance { + + private static String USER = "user"; + private static String PWD = "pwd"; + private static String ROLE = "role"; + private static String METHOD = "GET"; + private static String URI = "/protected"; + private static String CONTEXT_PATH = "/foo"; + private static String CLIENT_AUTH_HEADER = "authorization"; + private static String REALM = "TestRealm"; + private static String QOP = "auth"; + + private static final AtomicInteger nonceCount = new AtomicInteger(0); + + private DigestAuthenticator authenticator = new DigestAuthenticator(); + + + @Test + public void testSimple() throws Exception { + doTest(4, 1000000); + } + + public void doTest(int threadCount, int requestCount) throws Exception { + + TesterRunnable runnables[] = new TesterRunnable[threadCount]; + Thread threads[] = new Thread[threadCount]; + + String nonce = authenticator.generateNonce(new TesterDigestRequest()); + + // Create the runnables & threads + for (int i = 0; i < threadCount; i++) { + runnables[i] = + new TesterRunnable(authenticator, nonce, requestCount); + threads[i] = new Thread(runnables[i]); + } + + long start = System.currentTimeMillis(); + + // Start the threads + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + + // Wait for the threads to finish + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + double wallTime = System.currentTimeMillis() - start; + + // Gather the results... + double totalTime = 0; + int totalSuccess = 0; + for (int i = 0; i < threadCount; i++) { + System.out.println("Thread: " + i + " Success: " + + runnables[i].getSuccess()); + totalSuccess = totalSuccess + runnables[i].getSuccess(); + totalTime = totalTime + runnables[i].getTime(); + } + + System.out.println("Average time per request (user): " + + totalTime/(threadCount * requestCount)); + System.out.println("Average time per request (wall): " + + wallTime/(threadCount * requestCount)); + + Assert.assertEquals(((long)requestCount) * threadCount, totalSuccess); + } + + @Before + public void setUp() throws Exception { + + ConcurrentMessageDigest.init("MD5"); + + // Configure the Realm + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser(USER, PWD); + realm.addUserRole(USER, ROLE); + + // Add the Realm to the Context + Context context = new StandardContext(); + context.setName(CONTEXT_PATH); + context.setRealm(realm); + + Host host = new StandardHost(); + context.setParent(host); + + Engine engine = new StandardEngine(); + host.setParent(engine); + + Service service = new StandardService(); + engine.setService(service); + + // Configure the Login config + LoginConfig config = new LoginConfig(); + config.setRealmName(REALM); + context.setLoginConfig(config); + + // Make the Context and Realm visible to the Authenticator + authenticator.setContainer(context); + authenticator.setNonceCountWindowSize(8 * 1024); + + authenticator.start(); + } + + + private static class TesterRunnable implements Runnable { + + private String nonce; + private int requestCount; + + private int success = 0; + private long time = 0; + + private TesterDigestRequest request; + private HttpServletResponse response; + private DigestAuthenticator authenticator; + + private static final String A1 = USER + ":" + REALM + ":" + PWD; + private static final String A2 = METHOD + ":" + CONTEXT_PATH + URI; + + private static final String DIGEST_A1 = HexUtils.toHexString( + ConcurrentMessageDigest.digest("MD5", A1.getBytes(StandardCharsets.UTF_8))); + private static final String DIGEST_A2 = HexUtils.toHexString( + ConcurrentMessageDigest.digest("MD5", A2.getBytes(StandardCharsets.UTF_8))); + + + + // All init code should be in here. run() needs to be quick + TesterRunnable(DigestAuthenticator authenticator, + String nonce, int requestCount) throws Exception { + this.authenticator = authenticator; + this.nonce = nonce; + this.requestCount = requestCount; + + request = new TesterDigestRequest(); + request.getMappingData().context = authenticator.context; + + response = new TesterHttpServletResponse(); + } + + @Override + public void run() { + long start = System.currentTimeMillis(); + for (int i = 0; i < requestCount; i++) { + try { + request.setAuthHeader(buildDigestResponse(nonce)); + if (authenticator.authenticate(request, response)) { + success++; + } + // Clear out authenticated user ready for next iteration + request.setUserPrincipal(null); + } catch (IOException ioe) { + // Ignore + } + } + time = System.currentTimeMillis() - start; + } + + public int getSuccess() { + return success; + } + + public long getTime() { + return time; + } + + private String buildDigestResponse(String nonce) { + + String ncString = String.format("%1$08x", + Integer.valueOf(nonceCount.incrementAndGet())); + String cnonce = "cnonce"; + + String response = DIGEST_A1 + ":" + nonce + ":" + ncString + ":" + + cnonce + ":" + QOP + ":" + DIGEST_A2; + + String md5response = HexUtils.toHexString(ConcurrentMessageDigest.digest( + "MD5", response.getBytes(StandardCharsets.UTF_8))); + + StringBuilder auth = new StringBuilder(); + auth.append("Digest username=\""); + auth.append(USER); + auth.append("\", realm=\""); + auth.append(REALM); + auth.append("\", nonce=\""); + auth.append(nonce); + auth.append("\", uri=\""); + auth.append(CONTEXT_PATH + URI); + auth.append("\", opaque=\""); + auth.append(authenticator.getOpaque()); + auth.append("\", response=\""); + auth.append(md5response); + auth.append("\""); + auth.append(", qop="); + auth.append(QOP); + auth.append(", nc="); + auth.append(ncString); + auth.append(", cnonce=\""); + auth.append(cnonce); + auth.append("\""); + + return auth.toString(); + } + } + + + private static class TesterDigestRequest extends Request { + + private String authHeader = null; + + TesterDigestRequest() { + super(null); + } + + @Override + public String getRemoteAddr() { + return "127.0.0.1"; + } + + public void setAuthHeader(String authHeader) { + this.authHeader = authHeader; + } + + @Override + public String getHeader(String name) { + if (CLIENT_AUTH_HEADER.equalsIgnoreCase(name)) { + return authHeader; + } else { + return super.getHeader(name); + } + } + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getQueryString() { + return null; + } + + @Override + public String getRequestURI() { + return CONTEXT_PATH + URI; + } + + @Override + public org.apache.coyote.Request getCoyoteRequest() { + return new org.apache.coyote.Request(); + } + } +} diff --git a/test/org/apache/catalina/authenticator/jaspic/TestAuthConfigFactoryImpl.java b/test/org/apache/catalina/authenticator/jaspic/TestAuthConfigFactoryImpl.java new file mode 100644 index 0000000..9f4fe6b --- /dev/null +++ b/test/org/apache/catalina/authenticator/jaspic/TestAuthConfigFactoryImpl.java @@ -0,0 +1,446 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.security.auth.message.config.AuthConfigFactory; +import jakarta.security.auth.message.config.AuthConfigProvider; +import jakarta.security.auth.message.config.RegistrationListener; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Globals; + +public class TestAuthConfigFactoryImpl { + + private String oldCatalinaBase; + private static final File TEST_CONFIG_FILE = new File("test/conf/jaspic-providers.xml"); + + @Test + public void testRegistrationNullLayer() { + doTestRegistration(null, "AC_1", ":AC_1"); + } + + + @Test + public void testRegistrationNullAppContext() { + doTestRegistration("L_1", null, "L_1:"); + } + + + @Test + public void testRegistrationNullLayerAndNullAppContext() { + doTestRegistration(null, null, ":"); + } + + + @Test + public void testSearchNoMatch01() { + doTestSearchOrder("foo", "bar", 1); + } + + + @Test + public void testSearchNoMatch02() { + doTestSearchOrder(null, "bar", 1); + } + + + @Test + public void testSearchNoMatch03() { + doTestSearchOrder("foo", null, 1); + } + + + @Test + public void testSearchNoMatch04() { + doTestSearchOrder(null, null, 1); + } + + + @Test + public void testSearchOnlyAppContextMatch01() { + doTestSearchOrder("foo", "AC_1", 2); + } + + + @Test + public void testSearchOnlyAppContextMatch02() { + doTestSearchOrder(null, "AC_1", 2); + } + + + @Test + public void testSearchOnlyAppContextMatch03() { + doTestSearchOrder("L_2", "AC_1", 2); + } + + + @Test + public void testSearchOnlyLayerMatch01() { + doTestSearchOrder("L_1", "bar", 3); + } + + + @Test + public void testSearchOnlyLayerMatch02() { + doTestSearchOrder("L_1", null, 3); + } + + + @Test + public void testSearchOnlyLayerMatch03() { + doTestSearchOrder("L_1", "AC_2", 3); + } + + + @Test + public void testSearchBothMatch() { + doTestSearchOrder("L_2", "AC_2", 4); + } + + + private void doTestSearchOrder(String layer, String appContext, int expected) { + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp1, null, null, "1"); + AuthConfigProvider acp2 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp2, null, "AC_1", "2"); + AuthConfigProvider acp3 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp3, "L_1", null, "3"); + AuthConfigProvider acp4 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp4, "L_2", "AC_2", "4"); + + AuthConfigProvider searchResult = factory.getConfigProvider(layer, appContext, null); + int searchIndex; + if (searchResult == acp1) { + searchIndex = 1; + } else if (searchResult == acp2) { + searchIndex = 2; + } else if (searchResult == acp3) { + searchIndex = 3; + } else if (searchResult == acp4) { + searchIndex = 4; + } else { + searchIndex = -1; + } + Assert.assertEquals(expected, searchIndex); + } + + + private void doTestRegistration(String layer, String appContext, String expectedRegId) { + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null); + SimpleRegistrationListener listener = new SimpleRegistrationListener(layer, appContext); + + String regId = factory.registerConfigProvider(acp1, layer, appContext, null); + Assert.assertEquals(expectedRegId, regId); + + factory.getConfigProvider(layer, appContext, listener); + factory.removeRegistration(regId); + Assert.assertTrue(listener.wasCorrectlyCalled()); + + listener.reset(); + factory.registerConfigProvider(acp1, layer, appContext, null); + factory.getConfigProvider(layer, appContext, listener); + // Replace it + AuthConfigProvider acp2 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp2, layer, appContext, null); + Assert.assertTrue(listener.wasCorrectlyCalled()); + } + + + @Test + public void testRegistrationInsertExact01() { + doTestRegistrationInsert("L_3", "AC_2", "L_3", "AC_2"); + } + + + @Test + public void testRegistrationInsertExact02() { + doTestRegistrationInsert("L_2", "AC_3", "L_2", "AC_3"); + } + + + @Test + public void testRegistrationInsertExact03() { + doTestRegistrationInsert("L_4", "AC_4", "L_4", "AC_4"); + } + + + @Test + public void testRegistrationInsertAppContext01() { + doTestRegistrationInsert(null, "AC_3", "L_2", "AC_3"); + } + + + @Test + public void testRegistrationInsertAppContext02() { + doTestRegistrationInsert(null, "AC_4", "L_4", "AC_4"); + } + + + @Test + public void testRegistrationInsertLayer01() { + doTestRegistrationInsert("L_4", null, "L_4", "AC_4"); + } + + + private void doTestRegistrationInsert(String newLayer, String newAppContext, + String expectedListenerLayer, String expectedListenerAppContext) { + // Set up + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp1, "L_1", "AC_1", null); + AuthConfigProvider acp2 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp2, null, "AC_2", null); + AuthConfigProvider acp3 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp3, "L_2", null, null); + AuthConfigProvider acp4 = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acp4, null, null, null); + + SimpleRegistrationListener listener1 = new SimpleRegistrationListener("L_1", "AC_1"); + factory.getConfigProvider("L_1", "AC_1", listener1); + SimpleRegistrationListener listener2 = new SimpleRegistrationListener("L_3", "AC_2"); + factory.getConfigProvider("L_3", "AC_2", listener2); + SimpleRegistrationListener listener3 = new SimpleRegistrationListener("L_2", "AC_3"); + factory.getConfigProvider("L_2", "AC_3", listener3); + SimpleRegistrationListener listener4 = new SimpleRegistrationListener("L_4", "AC_4"); + factory.getConfigProvider("L_4", "AC_4", listener4); + + List listeners = new ArrayList<>(); + listeners.add(listener1); + listeners.add(listener2); + listeners.add(listener3); + listeners.add(listener4); + + // Register a new provider that will impact some existing registrations + AuthConfigProvider acpNew = new SimpleAuthConfigProvider(null, null); + factory.registerConfigProvider(acpNew, newLayer, newAppContext, null); + + // Check to see if the expected listener fired. + for (SimpleRegistrationListener listener : listeners) { + if (listener.wasCalled()) { + Assert.assertEquals(listener.layer, expectedListenerLayer); + Assert.assertEquals(listener.appContext, expectedListenerAppContext); + Assert.assertTrue(listener.wasCorrectlyCalled()); + } else { + Assert.assertFalse((listener.layer.equals(expectedListenerLayer) && + listener.appContext.equals(expectedListenerAppContext))); + } + } + } + + + @Test + public void testDetachListenerNonexistingRegistration() { + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null); + String registrationId = factory.registerConfigProvider(acp1, "L_1", "AC_1", null); + + SimpleRegistrationListener listener1 = new SimpleRegistrationListener("L_1", "AC_1"); + factory.getConfigProvider("L_1", "AC_1", listener1); + + factory.removeRegistration(registrationId); + String[] registrationIds = factory.detachListener(listener1, "L_1", "AC_1"); + Assert.assertTrue(registrationIds.length == 0); + } + + + @Test + public void testDetachListener() { + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null); + String registrationId = factory.registerConfigProvider(acp1, "L_1", "AC_1", null); + + SimpleRegistrationListener listener1 = new SimpleRegistrationListener("L_1", "AC_1"); + factory.getConfigProvider("L_1", "AC_1", listener1); + + String[] registrationIds = factory.detachListener(listener1, "L_1", "AC_1"); + Assert.assertTrue(registrationIds.length == 1); + Assert.assertEquals(registrationId, registrationIds[0]); + } + + + @Test + public void testRegistrationNullListener() { + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null); + String registrationId = factory.registerConfigProvider(acp1, "L_1", "AC_1", null); + + factory.getConfigProvider("L_1", "AC_1", null); + + boolean result = factory.removeRegistration(registrationId); + Assert.assertTrue(result); + } + + + @Test + public void testAllRegistrationIds() { + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + AuthConfigProvider acp1 = new SimpleAuthConfigProvider(null, null); + String registrationId1 = factory.registerConfigProvider(acp1, "L_1", "AC_1", null); + AuthConfigProvider acp2 = new SimpleAuthConfigProvider(null, null); + String registrationId2 = factory.registerConfigProvider(acp2, "L_2", "AC_2", null); + + String[] registrationIds = factory.getRegistrationIDs(null); + Assert.assertTrue(registrationIds.length == 2); + Set ids = new HashSet<>(Arrays.asList(registrationIds)); + Assert.assertTrue(ids.contains(registrationId1)); + Assert.assertTrue(ids.contains(registrationId2)); + } + + + @Before + public void setUp() { + // set CATALINA_BASE to test so that the file with persistent providers will be written in test/conf folder + oldCatalinaBase = System.getProperty(Globals.CATALINA_BASE_PROP); + System.setProperty(Globals.CATALINA_BASE_PROP, "test"); + + if (TEST_CONFIG_FILE.exists()) { + if (!TEST_CONFIG_FILE.delete()) { + Assert.fail("Failed to delete " + TEST_CONFIG_FILE); + } + } + } + + + @After + public void cleanUp() { + if (oldCatalinaBase != null ) { + System.setProperty(Globals.CATALINA_BASE_PROP, oldCatalinaBase); + } else { + System.clearProperty(Globals.CATALINA_BASE_PROP); + } + + if (TEST_CONFIG_FILE.exists()) { + if (!TEST_CONFIG_FILE.delete()) { + Assert.fail("Failed to delete " + TEST_CONFIG_FILE); + } + } + } + + + @Test + public void testRemovePersistentRegistration() { + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + factory.registerConfigProvider( + SimpleAuthConfigProvider.class.getName(), null, "L_1", "AC_1", null); + String registrationId2 = factory.registerConfigProvider( + SimpleAuthConfigProvider.class.getName(), null, "L_2", "AC_2", null); + + factory.removeRegistration(registrationId2); + factory.refresh(); + + String[] registrationIds = factory.getRegistrationIDs(null); + for (String registrationId : registrationIds) { + Assert.assertNotEquals(registrationId2, registrationId); + } + } + + + @Test + public void testRegistrationNullClassName() { + doTestNullClassName(false, "L_1", "AC_1"); + } + + + @Test + public void testRegistrationNullClassOverrideExisting() { + doTestNullClassName(true, "L_1", "AC_1"); + } + + + @Test + public void testRegistrationNullClassNullLayerNullAppContext() { + doTestNullClassName(false, null, null); + } + + + private void doTestNullClassName(boolean shouldOverrideExistingProvider, String layer, String appContext) { + AuthConfigFactory factory = new AuthConfigFactoryImpl(); + if (shouldOverrideExistingProvider) { + factory.registerConfigProvider(SimpleAuthConfigProvider.class.getName(), null, layer, appContext, null); + } + String registrationId = factory.registerConfigProvider(null, null, layer, appContext, null); + factory.refresh(); + + String[] registrationIds = factory.getRegistrationIDs(null); + Set ids = new HashSet<>(Arrays.asList(registrationIds)); + Assert.assertTrue(ids.contains(registrationId)); + AuthConfigProvider provider = factory.getConfigProvider(layer, appContext, null); + Assert.assertNull(provider); + } + + + private static class SimpleRegistrationListener implements RegistrationListener { + + private final String layer; + private final String appContext; + + private boolean called = false; + private String layerNotified; + private String appContextNotified; + + SimpleRegistrationListener(String layer, String appContext) { + this.layer = layer; + this.appContext = appContext; + } + + @Override + public void notify(String layer, String appContext) { + called = true; + layerNotified = layer; + appContextNotified = appContext; + } + + + public boolean wasCalled() { + return called; + } + + + public boolean wasCorrectlyCalled() { + return called && areTheSame(layer, layerNotified) && + areTheSame(appContext, appContextNotified); + } + + + public void reset() { + called = false; + layerNotified = null; + appContextNotified = null; + } + + + private static boolean areTheSame(String a, String b) { + if (a == null) { + return b == null; + } + return a.equals(b); + } + } +} diff --git a/test/org/apache/catalina/authenticator/jaspic/TestPersistentProviderRegistrations.java b/test/org/apache/catalina/authenticator/jaspic/TestPersistentProviderRegistrations.java new file mode 100644 index 0000000..a767af2 --- /dev/null +++ b/test/org/apache/catalina/authenticator/jaspic/TestPersistentProviderRegistrations.java @@ -0,0 +1,135 @@ + /** + * 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.catalina.authenticator.jaspic; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations.Provider; +import org.apache.catalina.authenticator.jaspic.PersistentProviderRegistrations.Providers; + +public class TestPersistentProviderRegistrations { + + @Test + public void testLoadEmpty() { + File f = new File("test/conf/jaspic-test-01.xml"); + Providers result = PersistentProviderRegistrations.loadProviders(f); + Assert.assertEquals(0, result.getProviders().size()); + } + + + @Test + public void testLoadSimple() { + File f = new File("test/conf/jaspic-test-02.xml"); + Providers result = PersistentProviderRegistrations.loadProviders(f); + validateSimple(result); + } + + + private void validateSimple(Providers providers) { + Assert.assertEquals(1, providers.getProviders().size()); + Provider p = providers.getProviders().get(0); + Assert.assertEquals("a", p.getClassName()); + Assert.assertEquals("b", p.getLayer()); + Assert.assertEquals("c", p.getAppContext()); + Assert.assertEquals("d", p.getDescription()); + + Assert.assertEquals(2, p.getProperties().size()); + Assert.assertEquals("f", p.getProperties().get("e")); + Assert.assertEquals("h", p.getProperties().get("g")); + } + + + @Test + public void testSaveSimple() { + File f = new File("test/conf/jaspic-test-03.xml"); + if (f.exists()) { + Assert.assertTrue(f.delete()); + } + + // Create a config and write it out + Providers start = new Providers(); + Provider p = new Provider(); + p.setClassName("a"); + p.setLayer("b"); + p.setAppContext("c"); + p.setDescription("d"); + p.addProperty("e", "f"); + p.addProperty("g", "h"); + start.addProvider(p); + PersistentProviderRegistrations.writeProviders(start, f); + + // Read it back + Providers end = PersistentProviderRegistrations.loadProviders(f); + + validateSimple(end); + + if (f.exists()) { + Assert.assertTrue("Failed to clean up [" + f + "]", f.delete()); + } + } + + + @Test + public void testLoadProviderWithoutLayerAndAC() { + File f = new File("test/conf/jaspic-test-04.xml"); + Providers providers = PersistentProviderRegistrations.loadProviders(f); + validateNoLayerAndAC(providers); + } + + + private void validateNoLayerAndAC(Providers providers) { + Assert.assertEquals(1, providers.getProviders().size()); + Provider p = providers.getProviders().get(0); + Assert.assertEquals("a", p.getClassName()); + Assert.assertNull(p.getLayer()); + Assert.assertNull(p.getAppContext()); + Assert.assertEquals("d", p.getDescription()); + } + + + @Test + public void testSaveProviderWithoutLayerAndAC() { + File f = new File("test/conf/jaspic-test-05.xml"); + if (f.exists()) { + Assert.assertTrue(f.delete()); + } + + // Create a config and write it out + Providers initialProviders = new Providers(); + Provider p = new Provider(); + p.setClassName("a"); + p.setDescription("d"); + initialProviders.addProvider(p); + PersistentProviderRegistrations.writeProviders(initialProviders, f); + + // Read it back + Providers loadedProviders = PersistentProviderRegistrations.loadProviders(f); + + try { + validateNoLayerAndAC(loadedProviders); + } finally { + if (f.exists()) { + if (!f.delete()) { + Assert.fail("Failed to delete " + f); + } + } + } + } +} diff --git a/test/org/apache/catalina/authenticator/jaspic/TestSimpleServerAuthConfig.java b/test/org/apache/catalina/authenticator/jaspic/TestSimpleServerAuthConfig.java new file mode 100644 index 0000000..660002f --- /dev/null +++ b/test/org/apache/catalina/authenticator/jaspic/TestSimpleServerAuthConfig.java @@ -0,0 +1,74 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.MessageInfo; +import jakarta.security.auth.message.config.ServerAuthConfig; +import jakarta.security.auth.message.config.ServerAuthContext; + +import org.junit.Assert; +import org.junit.Test; + +public class TestSimpleServerAuthConfig { + + private static final String SERVER_AUTH_MODULE_KEY_PREFIX = + "org.apache.catalina.authenticator.jaspic.ServerAuthModule."; + + private static final Map CONFIG_PROPERTIES; + static { + CONFIG_PROPERTIES = new HashMap<>(); + CONFIG_PROPERTIES.put(SERVER_AUTH_MODULE_KEY_PREFIX + "1", + TesterServerAuthModuleA.class.getName()); + } + + @Test + public void testConfigOnServerAuthConfig() throws Exception { + ServerAuthConfig serverAuthConfig = + new SimpleServerAuthConfig(null, null, null, CONFIG_PROPERTIES); + ServerAuthContext serverAuthContext = serverAuthConfig.getAuthContext(null, null, null); + + validateServerAuthContext(serverAuthContext); + } + + + @Test + public void testConfigOnGetAuthContext() throws Exception { + ServerAuthConfig serverAuthConfig = new SimpleServerAuthConfig(null, null, null, null); + ServerAuthContext serverAuthContext = + serverAuthConfig.getAuthContext(null, null, CONFIG_PROPERTIES); + + validateServerAuthContext(serverAuthContext); + } + + + @Test(expected=AuthException.class) + public void testConfigNone() throws Exception { + ServerAuthConfig serverAuthConfig = new SimpleServerAuthConfig(null, null, null, null); + serverAuthConfig.getAuthContext(null, null, null); + } + + + private void validateServerAuthContext(ServerAuthContext serverAuthContext) throws Exception { + MessageInfo msgInfo = new TesterMessageInfo(); + serverAuthContext.cleanSubject(msgInfo, null); + Assert.assertEquals("init()-cleanSubject()-", msgInfo.getMap().get("trace")); + } +} diff --git a/test/org/apache/catalina/authenticator/jaspic/TesterMessageInfo.java b/test/org/apache/catalina/authenticator/jaspic/TesterMessageInfo.java new file mode 100644 index 0000000..eab3383 --- /dev/null +++ b/test/org/apache/catalina/authenticator/jaspic/TesterMessageInfo.java @@ -0,0 +1,54 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.security.auth.message.MessageInfo; + +public class TesterMessageInfo implements MessageInfo { + + private Object requestMessage; + private Object responseMessage; + private final Map map = new HashMap<>(); + + @Override + public Object getRequestMessage() { + return requestMessage; + } + + @Override + public Object getResponseMessage() { + return responseMessage; + } + + @Override + public void setRequestMessage(Object request) { + requestMessage = request; + } + + @Override + public void setResponseMessage(Object response) { + responseMessage = response; + } + + @Override + public Map getMap() { + return map; + } +} diff --git a/test/org/apache/catalina/authenticator/jaspic/TesterServerAuthModuleA.java b/test/org/apache/catalina/authenticator/jaspic/TesterServerAuthModuleA.java new file mode 100644 index 0000000..3cc19ff --- /dev/null +++ b/test/org/apache/catalina/authenticator/jaspic/TesterServerAuthModuleA.java @@ -0,0 +1,63 @@ +/** + * 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.catalina.authenticator.jaspic; + +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; + +import jakarta.security.auth.message.AuthException; +import jakarta.security.auth.message.AuthStatus; +import jakarta.security.auth.message.MessageInfo; +import jakarta.security.auth.message.MessagePolicy; +import jakarta.security.auth.message.module.ServerAuthModule; + +public class TesterServerAuthModuleA implements ServerAuthModule { + + private StringBuilder trace = new StringBuilder("init()-"); + + @Override + public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, + Subject serviceSubject) throws AuthException { + return null; + } + + @Override + public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) + throws AuthException { + return null; + } + + @Override + public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException { + trace.append("cleanSubject()-"); + messageInfo.getMap().put("trace", trace.toString()); + } + + @Override + public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, + CallbackHandler handler, @SuppressWarnings("rawtypes") Map options) + throws AuthException { + // NO-OP + } + + @Override + public Class[] getSupportedMessageTypes() { + return null; + } +} diff --git a/test/org/apache/catalina/connector/TestClientReadTimeout.java b/test/org/apache/catalina/connector/TestClientReadTimeout.java new file mode 100644 index 0000000..8c25542 --- /dev/null +++ b/test/org/apache/catalina/connector/TestClientReadTimeout.java @@ -0,0 +1,129 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + + +public class TestClientReadTimeout extends TomcatBaseTest { + + static Tomcat tomcat; + + @Test + public void testTimeoutGets408() throws IOException, LifecycleException { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(null); + + Tomcat.addServlet(ctx, "TestServlet", new SyncServlet()); + ctx.addServletMappingDecoded("/*", "TestServlet"); + + tomcat.start(); + + try (Socket socket = new Socket("localhost", getPort())) { + String request = "GET /async HTTP/1.1\r\nHost: localhost\r\ncontent-length: 101\r\n\r\n"; + + OutputStream os = socket.getOutputStream(); + os.write(request.getBytes(StandardCharsets.UTF_8)); + InputStream is = socket.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + String opening = null; + try { + opening = reader.readLine(); + } catch (SocketException e) { + // Handled below. An exception here means opening will be null + } + if (tomcat.getConnector().getProtocolHandlerClassName().contains("Nio2")) { + Assert.assertNull("NIO2 unexpectedly returned a response", opening); + } else { + Assert.assertNotNull("Didn't get back a response", opening); + StringBuilder sb = new StringBuilder(opening); + + try { + Assert.assertTrue( + "expected status code " + HttpServletResponse.SC_REQUEST_TIMEOUT + " but got " + opening, + opening.startsWith("HTTP/1.1 " + HttpServletResponse.SC_REQUEST_TIMEOUT)); + boolean connectionClose = false; + while (reader.ready()) { + String line = reader.readLine(); + if (line == null) { + break; + } + + sb.append("\n").append(line); + if ("connection: close".equalsIgnoreCase(line)) { + connectionClose = true; + } + + Assert.assertFalse(line.contains("Exception Report")); + Assert.assertFalse(line.contains("Status Report")); + } + + Assert.assertTrue("No 'Connection: close' header seen", connectionClose); + } catch (Throwable t) { + Assert.fail("Response:\n" + sb); + t.printStackTrace(); + } + } + } + } + + + static final class SyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + while (req.getInputStream().read() != -1) { + // NO-OP - Any data read is ignored + } + resp.setStatus(200); + resp.flushBuffer(); + } catch (ClientAbortException e) { + // resp.sendError(408); + } + } + } + +} \ No newline at end of file diff --git a/test/org/apache/catalina/connector/TestConnector.java b/test/org/apache/catalina/connector/TestConnector.java new file mode 100644 index 0000000..8d58994 --- /dev/null +++ b/test/org/apache/catalina/connector/TestConnector.java @@ -0,0 +1,220 @@ +/* + * 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.catalina.connector; + +import java.io.File; +import java.net.SocketTimeoutException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.Servlet; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Wrapper; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.servlets.WebdavServlet; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Test cases for {@link Connector}. + */ +public class TestConnector extends TomcatBaseTest { + + @Test + public void testStop() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Wrapper w = + Tomcat.addServlet(root, "tester", new TesterServlet()); + w.setAsyncSupported(true); + root.addServletMappingDecoded("/", "tester"); + + Connector connector = tomcat.getConnector(); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/", bc, null, null); + + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + + rc = -1; + bc.recycle(); + + connector.stop(); + + try { + rc = getUrl("http://localhost:" + getPort() + "/", bc, 1000, + null, null); + } catch (SocketTimeoutException ste) { + // May also see this with NIO + // Make sure the test passes if we do + rc = 503; + } + Assert.assertEquals(503, rc); + } + + + @Test + public void testPort() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Connector connector1 = tomcat.getConnector(); + connector1.setPort(0); + + Connector connector2 = new Connector(); + connector2.setPort(0); + + tomcat.getService().addConnector(connector2); + + tomcat.start(); + + int localPort1 = connector1.getLocalPort(); + int localPort2 = connector2.getLocalPort(); + + Assert.assertTrue(localPort1 > 0); + Assert.assertTrue(localPort2 > 0); + } + + + @Test(expected=LifecycleException.class) + public void testInvalidProtocolThrows() throws Exception { + doTestInvalidProtocol(true); + } + + + @Test + public void testInvalidProtocolNoThrows() throws Exception { + doTestInvalidProtocol(false); + } + + + private void doTestInvalidProtocol(boolean throwOnFailure) throws Exception { + Connector c = new Connector("foo.Bar"); + c.setThrowOnFailure(throwOnFailure); + + c.start(); + } + + + @Test(expected=LifecycleException.class) + public void testDuplicatePortThrows() throws Exception { + doTestDuplicatePort(true); + } + + + @Test + public void testDuplicatePortNoThrows() throws Exception { + doTestDuplicatePort(false); + } + + + private void doTestDuplicatePort(boolean throwOnFailure) throws Exception { + Connector c1 = new Connector(); + c1.setThrowOnFailure(throwOnFailure); + c1.setPort(0); + c1.start(); + + Connector c2 = new Connector(); + c2.setThrowOnFailure(throwOnFailure); + c2.setPort(c1.getLocalPort()); + + c2.start(); + } + + + @Test + public void testTraceAllowedDefault() throws Exception { + doTestTrace(new DefaultServlet(), true); + } + + + @Test + public void testTraceNotAllowedDefault() throws Exception { + doTestTrace(new DefaultServlet(), false); + } + + + @Test + public void testTraceAllowedWebDav() throws Exception { + doTestTrace(new WebdavServlet(), true); + } + + + @Test + public void testTraceNotAllowedWebDav() throws Exception { + doTestTrace(new WebdavServlet(), false); + } + + + @Test + public void testTraceAllowedCustom() throws Exception { + doTestTrace(new TesterServlet(), true); + } + + + @Test + public void testTraceNotAllowedCustom() throws Exception { + doTestTrace(new TesterServlet(), false); + } + + + private void doTestTrace(Servlet servlet, boolean allowTrace) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context root = tomcat.addContext("", appDir.getAbsolutePath()); + Tomcat.addServlet(root, "default", servlet); + root.addServletMappingDecoded("/", "default"); + + Connector connector = tomcat.getConnector(); + connector.setAllowTrace(allowTrace); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + Map> respHeaders = new HashMap<>(); + int rc = methodUrl("http://localhost:" + getPort() + "/index.html", + bc, 30000, null, respHeaders, "OPTIONS"); + + Assert.assertEquals(200, rc); + + boolean foundTrace = false; + for (String header : respHeaders.get("Allow")) { + if (header.contains("TRACE")) { + foundTrace = true; + break; + } + } + + if (allowTrace) { + Assert.assertTrue(foundTrace); + } else { + Assert.assertFalse(foundTrace); + } + } +} diff --git a/test/org/apache/catalina/connector/TestCoyoteAdapter.java b/test/org/apache/catalina/connector/TestCoyoteAdapter.java new file mode 100644 index 0000000..0aa87ea --- /dev/null +++ b/test/org/apache/catalina/connector/TestCoyoteAdapter.java @@ -0,0 +1,427 @@ +/* + * 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.catalina.connector; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; + +public class TestCoyoteAdapter extends TomcatBaseTest { + + public static final String TEXT_8K; + public static final byte[] BYTES_8K; + + static { + StringBuilder sb = new StringBuilder(8192); + for (int i = 0; i < 512; i++) { + sb.append("0123456789ABCDEF"); + } + TEXT_8K = sb.toString(); + BYTES_8K = TEXT_8K.getBytes(StandardCharsets.UTF_8); + } + @Test + public void testPathParmsRootNone() throws Exception { + pathParamTest("/", "none"); + } + + @Test + public void testPathParmsFooNone() throws Exception { + pathParamTest("/foo", "none"); + } + + @Test + public void testPathParmsRootSessionOnly() throws Exception { + pathParamTest("/;jsessionid=1234", "1234"); + } + + @Test + public void testPathParmsFooSessionOnly() throws Exception { + pathParamTest("/foo;jsessionid=1234", "1234"); + } + + @Test + public void testPathParmsFooSessionDummy() throws Exception { + pathParamTest("/foo;jsessionid=1234;dummy", "1234"); + } + + @Test + public void testPathParmsFooSessionDummyValue() throws Exception { + pathParamTest("/foo;jsessionid=1234;dummy=5678", "1234"); + } + + @Test + public void testPathParmsFooSessionValue() throws Exception { + pathParamTest("/foo;jsessionid=1234;=5678", "1234"); + } + + @Test + public void testPathParmsFooSessionBar() throws Exception { + pathParamTest("/foo;jsessionid=1234/bar", "1234"); + } + + @Test + public void testPathParamsRedirect() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // Must have a real docBase. Don't use java.io.tmpdir as it may not be + // writable. + File docBase = new File(getTemporaryDirectory(), "testCoyoteAdapter"); + addDeleteOnTearDown(docBase); + if (!docBase.mkdirs() && !docBase.isDirectory()) { + Assert.fail("Failed to create: [" + docBase.toString() + "]"); + } + + // Create the folder that will trigger the redirect + File foo = new File(docBase, "foo"); + addDeleteOnTearDown(foo); + if (!foo.mkdirs() && !foo.isDirectory()) { + Assert.fail("Unable to create foo directory in docBase"); + } + + Context ctx = tomcat.addContext("", docBase.getAbsolutePath()); + + Tomcat.addServlet(ctx, "servlet", new PathParamServlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + testPath("/", "none"); + testPath("/;jsessionid=1234", "1234"); + testPath("/foo;jsessionid=1234", "1234"); + testPath("/foo;jsessionid=1234;dummy", "1234"); + testPath("/foo;jsessionid=1234;dummy=5678", "1234"); + testPath("/foo;jsessionid=1234;=5678", "1234"); + testPath("/foo;jsessionid=1234/bar", "1234"); + } + + private void pathParamTest(String path, String expected) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new PathParamServlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + path); + Assert.assertEquals(expected, res.toString()); + } + + private void testPath(String path, String expected) throws Exception { + ByteChunk res = getUrl("http://localhost:" + getPort() + path); + Assert.assertEquals(expected, res.toString()); + } + + private static class PathParamServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + String sessionId = req.getRequestedSessionId(); + if (sessionId == null) { + sessionId = "none"; + } + pw.write(sessionId); + } + } + + @Test + public void testPathParamExtRootNoParam() throws Exception { + pathParamExtensionTest("/testapp/blah.txt", "none"); + } + + @Test + public void testPathParamExtLevel1NoParam() throws Exception { + pathParamExtensionTest("/testapp/blah/blah.txt", "none"); + } + + @Test + public void testPathParamExtLevel1WithParam() throws Exception { + pathParamExtensionTest("/testapp/blah;x=y/blah.txt", "none"); + } + + private void pathParamExtensionTest(String path, String expected) + throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("/testapp", null); + + Tomcat.addServlet(ctx, "servlet", new PathParamServlet()); + ctx.addServletMappingDecoded("*.txt", "servlet"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + path); + Assert.assertEquals(expected, res.toString()); + } + + @Test + public void testBug54602a() throws Exception { + // No UTF-8 + doTestUriDecoding("/foo", "UTF-8", "/foo"); + } + + @Test + public void testBug54602b() throws Exception { + // Valid UTF-8 + doTestUriDecoding("/foo%c4%87", "UTF-8", "/foo\u0107"); + } + + @Test + public void testBug54602c() throws Exception { + // Partial UTF-8 + doTestUriDecoding("/foo%c4", "UTF-8", null); + } + + @Test + public void testBug54602d() throws Exception { + // Invalid UTF-8 + doTestUriDecoding("/foo%ff", "UTF-8", null); + } + + @Test + public void testBug54602e() throws Exception { + // Invalid UTF-8 + doTestUriDecoding("/foo%ed%a0%80", "UTF-8", null); + } + + private void doTestUriDecoding(String path, String encoding, + String expectedPathInfo) throws Exception{ + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + tomcat.getConnector().setURIEncoding(encoding); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + PathInfoServlet servlet = new PathInfoServlet(); + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/*", "servlet"); + + tomcat.start(); + + int rc = getUrl("http://localhost:" + getPort() + path, + new ByteChunk(), null); + + if (expectedPathInfo == null) { + // Invalid URI + Assert.assertEquals(HttpServletResponse.SC_BAD_REQUEST, rc); + } else { + // Valid URI + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(expectedPathInfo, servlet.getPathInfo()); + } + } + + private static class PathInfoServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private volatile String pathInfo = null; + + public String getPathInfo() { + return pathInfo; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + // Not thread safe. Concurrent requests to this servlet will + // over-write all the results but the last processed. + pathInfo = req.getPathInfo(); + } + } + + + @Test + public void testBug54928() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncServlet servlet = new AsyncServlet(); + Wrapper w = Tomcat.addServlet(ctx, "async", servlet); + w.setAsyncSupported(true); + ctx.addServletMappingDecoded("/async", "async"); + + tomcat.start(); + + SimpleHttpClient client = new SimpleHttpClient() { + @Override + public boolean isResponseBodyOK() { + return true; + } + }; + + String request = "GET /async HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: a" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + client.setPort(getPort()); + client.setRequest(new String[] {request}); + + client.connect(); + client.sendRequest(); + + for (int i = 0; i < 10; i++) { + String line = client.readLine(); + if (line != null && line.length() > 20) { + log.info(line.subSequence(0, 20) + "..."); + } + } + + client.disconnect(); + + // Wait for server thread to stop + Thread t = servlet.getThread(); + long startTime = System.nanoTime(); + t.join(5000); + long endTime = System.nanoTime(); + log.info("Waited for servlet thread to stop for " + + (endTime - startTime) / 1000000 + " ms"); + + Assert.assertTrue(servlet.isCompleted()); + } + + @Test + public void testNormalize01() { + doTestNormalize("/foo/../bar", "/bar"); + } + + @Test + public void testNormalize02() { + doTestNormalize("/foo/.", "/foo"); + } + + private void doTestNormalize(String input, String expected) { + MessageBytes mb = MessageBytes.newInstance(); + byte[] b = input.getBytes(StandardCharsets.UTF_8); + // Need to allow an extra byte in case '/' is appended during processing + byte[] b2 = new byte[b.length + 1]; + System.arraycopy(b, 0, b2, 0, b.length); + mb.setBytes(b2, 0, b.length); + + boolean result = CoyoteAdapter.normalize(mb, false); + mb.toString(); + + if (expected == null) { + Assert.assertFalse(result); + } else { + Assert.assertTrue(result); + Assert.assertEquals(expected, mb.toString()); + } + } + + + private class AsyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + // This is a hack that won't work generally as servlets are expected to + // handle more than one request. + private Thread t; + private volatile boolean completed = false; + + public Thread getThread() { + return t; + } + + public boolean isCompleted() { + return completed; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + final OutputStream os = resp.getOutputStream(); + + final AsyncContext asyncCtxt = req.startAsync(); + asyncCtxt.setTimeout(3000); + + t = new Thread(new Runnable() { + + @Override + public void run() { + for (int i = 0; i < 20; i++) { + try { + // Some tests depend on this write failing (e.g. + // because the client has gone away). In some cases + // there may be a large (ish) buffer to fill before + // the write fails. + for (int j = 0 ; j < 8; j++) { + os.write(BYTES_8K); + } + os.flush(); + Thread.sleep(1000); + } catch (Exception e) { + log.info("Exception caught " + e); + try { + // Note if request times out before this + // exception is thrown and the complete call + // below is made, the complete call below will + // fail since the timeout will have completed + // the request. + asyncCtxt.complete(); + break; + } finally { + completed = true; + } + } + } + } + }); + t.setName("testBug54928"); + t.start(); + } + } +} diff --git a/test/org/apache/catalina/connector/TestCoyoteAdapterCanonicalization.java b/test/org/apache/catalina/connector/TestCoyoteAdapterCanonicalization.java new file mode 100644 index 0000000..7d0e71d --- /dev/null +++ b/test/org/apache/catalina/connector/TestCoyoteAdapterCanonicalization.java @@ -0,0 +1,235 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import static org.apache.catalina.startup.SimpleHttpClient.CRLF; +import org.apache.catalina.Context; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +/* + * Tests for the canonicalization clarifications in Servlet 6.0 + */ +@RunWith(Parameterized.class) +public class TestCoyoteAdapterCanonicalization extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{index}: requestURI[{0}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + // This should be consistent with the table in the Servlet specification + parameterSets.add(new Object[] { "foo/bar", "/foo/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar;jsessionid=1234", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/", "/foo/bar/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/;jsessionid=1234", "/foo/bar/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo;/bar;", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo;/bar;/;", "/foo/bar/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo%00/bar/", "/foo\000/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo%7Fbar", "/foo\177bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo%2Fbar", "/foo%2Fbar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo%2Fb%25r", "/foo%2Fb%25r", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/b%25r", "/foo/b%r", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo\\bar", "/foo\\bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo%5Cbar", "/foo\\bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo;%2F/bar", "/foo/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/./bar", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/././bar", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/./foo/bar", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/%2e/bar", "/foo/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/.;/bar", "/foo/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/%2e;/bar", "/foo/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/.%2Fbar", "/foo/.%2Fbar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/.%5Cbar", "/foo/.\\bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar/.", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/./", "/foo/bar/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/.;", "/foo/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/./;", "/foo/bar/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/.bar", "/foo/.bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/../bar", "/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/../../bar", "/../bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/../foo/bar", "/../foo/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/%2e%2E/bar", "/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/%2e%2e/%2E%2E/bar", "/../bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/./../bar", "/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/..;/bar", "/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/%2e%2E;/bar", "/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/..%2Fbar", "/foo/..%2Fbar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/..%5Cbar", "/foo/..\\bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar/..", "/foo", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/../", "/foo/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/..;", "/foo", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/../;", "/foo/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/..bar", "/foo/..bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/.../bar", "/foo/.../bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo//bar", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "//foo//bar//", "/foo/bar/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/;/foo;/;/bar/;/;", "/foo/bar/", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo//../bar", "/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/;/../bar", "/bar", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo%E2%82%ACbar", "/foo\u20acbar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo%20bar", "/foo bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo%E2%82", "/foo%E2%82", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo%E2%82bar", "/foo%E2%82bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo%-1/bar", "/foo%-1/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo%XX/bar", "/foo%XX/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo%/bar", "/foo%/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar%0", "/foo/bar%0", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/good%20/bad%/%20mix%", "/good /bad%/%20mix%", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar?q", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar#f", "/foo/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar?q#f", "/foo/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar/?q", "/foo/bar/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar/#f", "/foo/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar/?q#f", "/foo/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar;?q", "/foo/bar", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/foo/bar;#f", "/foo/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/foo/bar;?q#f", "/foo/bar", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/", "/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "//", "/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/;/", "/", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/.", "/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/..", "/..", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/./", "/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "/../", "/../", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "foo/bar/", "/foo/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "./foo/bar/", "/foo/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "%2e/foo/bar/", "/foo/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "../foo/bar/", "/../foo/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { ".%2e/foo/bar/", "/../foo/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { ";/foo/bar/", "/foo/bar/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/#f", "/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "#f", "/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { "/?q", "/", Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { "?q", "/", Boolean.TRUE, Boolean.TRUE }); + + return parameterSets; + } + + @Parameter(0) + public String requestURI; + + @Parameter(1) + public String canonicalizedURI; + + @Parameter(2) + public boolean badRequestServlet; + + @Parameter(3) + public boolean badRequestTomcat; + + + @Test + public void testCanonicalizationSpecification() throws Exception { + doTestCanonicalization(true, badRequestServlet); + } + + @Test + public void testCanonicalizationTomcat() throws Exception { + doTestCanonicalization(false, badRequestTomcat); + } + + + private void doTestCanonicalization(boolean specCompliant, boolean expectBadRequest) throws Exception { + Tomcat tomcat = getTomcatInstance(); + // ROOT web application so context path is "" + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "EchoServletPath", new EchoServletPath()); + // Map as default servlet so servlet path should be URI less context + // path. Since the content path is "" the servlet path should be the + // URI. + root.addServletMappingDecoded("/", "EchoServletPath"); + + if (specCompliant) { + // Enabled options for stricter checking + tomcat.getConnector().setRejectSuspiciousURIs(true); + } + + tomcat.start(); + + Client client = new Client(tomcat.getConnector().getLocalPort(), canonicalizedURI); + client.setRequest(new String[] { + "GET " + requestURI + " HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + CRLF + }); + client.setResponseBodyEncoding(StandardCharsets.UTF_8); + + client.connect(); + client.processRequest(); + + // Expected response + String line = client.getResponseLine(); + String body = client.getResponseBody(); + if (expectBadRequest) { + Assert.assertTrue(line + CRLF + body, line.startsWith("HTTP/1.1 " + HttpServletResponse.SC_BAD_REQUEST)); + } else { + Assert.assertTrue(line + CRLF + body, line.startsWith("HTTP/1.1 " + HttpServletResponse.SC_OK)); + Assert.assertEquals(line + CRLF + body, canonicalizedURI, body); + } + } + + + public class EchoServletPath extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write(req.getServletPath()); + } + } + + + private static final class Client extends SimpleHttpClient { + + private final String expected; + + Client(int port, String expected) { + this.expected = expected; + setPort(port); + setRequestPause(0); + setUseContentLength(true); + } + + @Override + public boolean isResponseBodyOK() { + return expected.equals(getResponseBody()); + } + } +} diff --git a/test/org/apache/catalina/connector/TestCoyoteAdapterRequestFuzzing.java b/test/org/apache/catalina/connector/TestCoyoteAdapterRequestFuzzing.java new file mode 100644 index 0000000..f100cf8 --- /dev/null +++ b/test/org/apache/catalina/connector/TestCoyoteAdapterRequestFuzzing.java @@ -0,0 +1,167 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import static org.apache.catalina.startup.SimpleHttpClient.CRLF; +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterData; + +/* + * Various requests, usually originating from fuzzing, that have triggered an + * incorrect response from Tomcat - usually a 500 response rather than a 400 + * response. + */ +@RunWith(Parameterized.class) +public class TestCoyoteAdapterRequestFuzzing extends TomcatBaseTest { + + private static final String VALUE_16K = TesterData.string('x', 16 * 1024); + // Default max header count is 100 + private static final String HEADER_150 = TesterData.string("X-Tomcat-Test: a" + CRLF, 150); + // Default max header count is 200 (need to keep under maxHeaderCount as well) + private static final String COOKIE_250 = TesterData.string("Cookie: a=b;c=d;e=f;g=h" + CRLF, 75); + + @Parameterized.Parameters(name = "{index}: requestline[{0}], expected[{2}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { "GET /00 HTTP/1.1", + "Host: lÿ#" + CRLF, + "400" } ); + parameterSets.add(new Object[] { "GET *; HTTP/1.1", + "Host: localhost" + CRLF, + "400" } ); + parameterSets.add(new Object[] { "GET /02 HTTP/1.1", + "Host: localhost" + CRLF + + "Content-Length: \u00A0" + CRLF, + "400" } ); + parameterSets.add(new Object[] { "GET /03 HTTP/1.1", + "Content-Length: 1" + CRLF + + "Content-Length: 1" + CRLF, + "400" } ); + parameterSets.add(new Object[] { "GET /04 HTTP/1.1", + "Transfer-Encoding: " + VALUE_16K + CRLF, + "400" } ); + parameterSets.add(new Object[] { "GET /05 HTTP/1.1", + "Expect: " + VALUE_16K + CRLF, + "400" } ); + parameterSets.add(new Object[] { "GET /06 HTTP/1.1", + "Connection: " + VALUE_16K + CRLF, + "400" } ); + parameterSets.add(new Object[] { "GET /07 HTTP/1.1", + "User-Agent: " + VALUE_16K + CRLF, + "400" } ); + parameterSets.add(new Object[] { "GET /08 HTTP/1.1", + HEADER_150, + "400" } ); + parameterSets.add(new Object[] { "GET http://host/09 HTTP/1.0", + HEADER_150, + "400" } ); + parameterSets.add(new Object[] { "GET /10 HTTP/1.1", + "Host: localhost" + CRLF + + COOKIE_250, + "400" } ); + + return parameterSets; + } + + @Parameter(0) + public String requestLine; + + @Parameter(1) + public String headers; + + @Parameter(2) + public String expected; + + + @Test + public void doTest() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Assert.assertTrue(tomcat.getConnector().setProperty("restrictedUserAgents", "value-not-important")); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Tomcat.addServlet(ctxt, "default", DefaultServlet.class.getName()); + ctxt.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] {requestLine + CRLF, headers + CRLF}); + + client.connect(); + try { + client.processRequest(); + } catch (SocketException e) { + // Likely connection reset. Response line won't be available. + return; + } + + // Expected response + String line = client.getResponseLine(); + Assert.assertTrue(line + CRLF + client.getResponseBody(), line.startsWith("HTTP/1.1 " + expected + " ")); + } + + + private static final class Client extends SimpleHttpClient { + + Client(int port) { + setPort(port); + setRequestPause(0); + } + + @Override + protected OutputStream createOutputStream(Socket socket) throws IOException { + // Override the default implementation so we can create a large + // enough buffer to hold the entire request. + // The default implementation uses the 8k buffer in the + // StreamEncoder. Since some requests are larger than this, those + // requests will be sent in several parts. If the first part is + // sufficient for Tomcat to determine the request is invalid, Tomcat + // will close the connection, causing the write of the remaining + // parts to fail which in turn causes the test to fail. + return new BufferedOutputStream(super.createOutputStream(socket), 32 * 1024); + } + + @Override + public boolean isResponseBodyOK() { + // Response body varies. It is the response code that is of interest + // in these tests. + return true; + } + } +} diff --git a/test/org/apache/catalina/connector/TestCoyoteInputStream.java b/test/org/apache/catalina/connector/TestCoyoteInputStream.java new file mode 100644 index 0000000..dced9fb --- /dev/null +++ b/test/org/apache/catalina/connector/TestCoyoteInputStream.java @@ -0,0 +1,72 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestCoyoteInputStream extends TomcatBaseTest { + + @Test + public void testReadWithByteBuffer() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "testServlet", new TestServlet()); + root.addServletMappingDecoded("/", "testServlet"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + String requestBody = "HelloWorld"; + int rc = postUrl(requestBody.getBytes(StandardCharsets.UTF_8), + "http://localhost:" + getPort() + "/", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(requestBody.equals(bc.toString())); + } + + private static final class TestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + CoyoteInputStream is = (CoyoteInputStream) req.getInputStream(); + ByteBuffer buffer = ByteBuffer.allocate(256); + is.read(buffer); + CoyoteOutputStream os = (CoyoteOutputStream) resp.getOutputStream(); + os.write(buffer); + } + + } + +} diff --git a/test/org/apache/catalina/connector/TestCoyoteOutputStream.java b/test/org/apache/catalina/connector/TestCoyoteOutputStream.java new file mode 100644 index 0000000..d808db7 --- /dev/null +++ b/test/org/apache/catalina/connector/TestCoyoteOutputStream.java @@ -0,0 +1,293 @@ +/* + * 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.catalina.connector; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel.MapMode; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestCoyoteOutputStream extends TomcatBaseTest { + + @Test + public void testNonBlockingWriteNoneBlockingWriteNoneContainerThread() throws Exception { + doNonBlockingTest(0, 0, true); + } + + @Test + public void testNonBlockingWriteOnceBlockingWriteNoneContainerThread() throws Exception { + doNonBlockingTest(1, 0, true); + } + + @Test + public void testNonBlockingWriteTwiceBlockingWriteNoneContainerThread() throws Exception { + doNonBlockingTest(2, 0, true); + } + + @Test + public void testNonBlockingWriteNoneBlockingWriteOnceContainerThread() throws Exception { + doNonBlockingTest(0, 1, true); + } + + @Test + public void testNonBlockingWriteOnceBlockingWriteOnceContainerThread() throws Exception { + doNonBlockingTest(1, 1, true); + } + + @Test + public void testNonBlockingWriteTwiceBlockingWriteOnceContainerThread() throws Exception { + doNonBlockingTest(2, 1, true); + } + + @Test + public void testNonBlockingWriteNoneBlockingWriteNoneNonContainerThread() throws Exception { + doNonBlockingTest(0, 0, false); + } + + @Test + public void testNonBlockingWriteOnceBlockingWriteNoneNonContainerThread() throws Exception { + doNonBlockingTest(1, 0, false); + } + + @Test + public void testNonBlockingWriteTwiceBlockingWriteNoneNonContainerThread() throws Exception { + doNonBlockingTest(2, 0, false); + } + + @Test + public void testNonBlockingWriteNoneBlockingWriteOnceNonContainerThread() throws Exception { + doNonBlockingTest(0, 1, false); + } + + @Test + public void testNonBlockingWriteOnceBlockingWriteOnceNonContainerThread() throws Exception { + doNonBlockingTest(1, 1, false); + } + + @Test + public void testNonBlockingWriteTwiceBlockingWriteOnceNonContainerThread() throws Exception { + doNonBlockingTest(2, 1, false); + } + + @Test + public void testWriteWithByteBuffer() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "testServlet", new TestServlet()); + root.addServletMappingDecoded("/", "testServlet"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/", bc, null, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + File file = new File("test/org/apache/catalina/connector/test_content.txt"); + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + ByteChunk expected = new ByteChunk(); + expected.append(raf.getChannel().map(MapMode.READ_ONLY, 0, file.length())); + Assert.assertTrue(expected.equals(bc)); + } + } + + private void doNonBlockingTest(int asyncWriteTarget, int syncWriteTarget, + boolean useContainerThreadToSetListener) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Wrapper w = Tomcat.addServlet(root, "nbWrite", + new NonBlockingWriteServlet(asyncWriteTarget, useContainerThreadToSetListener)); + w.setAsyncSupported(true); + root.addServletMappingDecoded("/nbWrite", "nbWrite"); + Tomcat.addServlet(root, "write", + new BlockingWriteServlet(asyncWriteTarget, syncWriteTarget)); + w.setAsyncSupported(true); + root.addServletMappingDecoded("/write", "write"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + // Extend timeout to 5 mins for debugging + int rc = getUrl("http://localhost:" + getPort() + "/nbWrite", bc, + 300000, null, null); + + int totalCount = asyncWriteTarget + syncWriteTarget; + StringBuilder sb = new StringBuilder(totalCount * 16); + for (int i = 0; i < totalCount; i++) { + sb.append("OK - " + i + System.lineSeparator()); + } + String expected = null; + if (sb.length() > 0) { + expected = sb.toString(); + } + Assert.assertEquals(200, rc); + Assert.assertEquals(expected, bc.toString()); + } + + private static final class NonBlockingWriteServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final int asyncWriteTarget; + private final AtomicInteger asyncWriteCount = new AtomicInteger(0); + private final boolean useContainerThreadToSetListener; + + NonBlockingWriteServlet(int asyncWriteTarget, + boolean useContainerThreadToSetListener) { + this.asyncWriteTarget = asyncWriteTarget; + this.useContainerThreadToSetListener = useContainerThreadToSetListener; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + ServletOutputStream sos = resp.getOutputStream(); + + + AsyncContext asyncCtxt = req.startAsync(); + asyncCtxt.setTimeout(5); + Runnable task = new AsyncTask(asyncCtxt, sos); + if (useContainerThreadToSetListener) { + asyncCtxt.start(task); + } else { + Thread t = new Thread(task); + t.start(); + } + } + + private void doAsyncWrite(AsyncContext asyncCtxt, + ServletOutputStream sos) throws IOException { + while (sos.isReady()) { + int next = asyncWriteCount.getAndIncrement(); + if (next < asyncWriteTarget) { + sos.write( + ("OK - " + next + System.lineSeparator()).getBytes( + StandardCharsets.UTF_8)); + sos.flush(); + } else { + asyncCtxt.dispatch("/write"); + break; + } + } + } + + private class AsyncTask implements Runnable { + + private final AsyncContext asyncCtxt; + private final ServletOutputStream sos; + + AsyncTask(AsyncContext asyncCtxt, ServletOutputStream sos) { + this.asyncCtxt = asyncCtxt; + this.sos = sos; + } + + @Override + public void run() { + sos.setWriteListener(new MyWriteListener(asyncCtxt, sos)); + } + } + + private final class MyWriteListener implements WriteListener { + + private final AsyncContext asyncCtxt; + private final ServletOutputStream sos; + + MyWriteListener(AsyncContext asyncCtxt, + ServletOutputStream sos) { + this.asyncCtxt = asyncCtxt; + this.sos = sos; + } + + @Override + public void onWritePossible() throws IOException { + doAsyncWrite(asyncCtxt, sos); + } + + @Override + public void onError(Throwable throwable) { + // Not expected. + throwable.printStackTrace(); + } + } + } + + private static final class BlockingWriteServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final int start; + private final int len; + + BlockingWriteServlet(int start, int len) { + this.start = start; + this.len = len; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + ServletOutputStream sos = resp.getOutputStream(); + + for (int i = start; i < start + len; i++) { + sos.write(("OK - " + i + System.lineSeparator()).getBytes( + StandardCharsets.UTF_8)); + } + } + } + + private static final class TestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + CoyoteOutputStream os = (CoyoteOutputStream) resp.getOutputStream(); + File file = new File("test/org/apache/catalina/connector/test_content.txt"); + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + os.write(raf.getChannel().map(MapMode.READ_ONLY, 0, file.length())); + } + } + + } +} diff --git a/test/org/apache/catalina/connector/TestInputBuffer.java b/test/org/apache/catalina/connector/TestInputBuffer.java new file mode 100644 index 0000000..d345bc3 --- /dev/null +++ b/test/org/apache/catalina/connector/TestInputBuffer.java @@ -0,0 +1,160 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.TestUtf8; +import org.apache.tomcat.util.buf.TestUtf8.Utf8TestCase; + +public class TestInputBuffer extends TomcatBaseTest { + + @Test + public void testUtf8Body() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Echo", new Utf8Echo()); + root.addServletMappingDecoded("/test", "Echo"); + + tomcat.start(); + + for (Utf8TestCase testCase : TestUtf8.TEST_CASES) { + String expected = null; + if (testCase.invalidIndex == -1) { + expected = testCase.outputReplaced; + } + doUtf8BodyTest(testCase.description, testCase.input, expected); + } + } + + + @Test + public void testBug60400() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Bug60400Servlet", new Bug60400Servlet()); + root.addServletMappingDecoded("/", "Bug60400Servlet"); + + Assert.assertTrue(tomcat.getConnector().setProperty("socket.appReadBufSize", "9000")); + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + byte[] requestBody = new byte[9500]; + Arrays.fill(requestBody, (byte) 1); + int rc = postUrl(requestBody, "http://localhost:" + getPort() + "/", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(requestBody.length, bc.getLength()); + } + + + private void doUtf8BodyTest(String description, int[] input, + String expected) throws Exception { + + byte[] bytes = new byte[input.length]; + for (int i = 0; i < input.length; i++) { + bytes[i] = (byte) input[i]; + } + + ByteChunk bc = new ByteChunk(); + int rc = postUrl(bytes, "http://localhost:" + getPort() + "/test", bc, + null); + + if (expected == null) { + Assert.assertEquals(description, HttpServletResponse.SC_OK, rc); + Assert.assertEquals(description, "FAILED", bc.toString()); + } else if (expected.length() == 0) { + Assert.assertNull(description, bc.toString()); + } else { + bc.setCharset(StandardCharsets.UTF_8); + Assert.assertEquals(description, expected, bc.toString()); + } + } + + + private static class Utf8Echo extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Should use POST + resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.setCharacterEncoding("UTF-8"); + Reader r = req.getReader(); + + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/plain"); + Writer w = resp.getWriter(); + + try { + // Copy one character at a time + int c = r.read(); + while (c != -1) { + w.write(c); + c = r.read(); + } + w.close(); + } catch (MalformedInputException mie) { + resp.resetBuffer(); + w.write("FAILED"); + } + } + } + + + private static class Bug60400Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + StringBuilder builder = new StringBuilder(); + try (BufferedReader reader = req.getReader()) { + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + } + resp.getWriter().print(builder); + } + } +} diff --git a/test/org/apache/catalina/connector/TestKeepAliveCount.java b/test/org/apache/catalina/connector/TestKeepAliveCount.java new file mode 100644 index 0000000..81f8a66 --- /dev/null +++ b/test/org/apache/catalina/connector/TestKeepAliveCount.java @@ -0,0 +1,143 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestKeepAliveCount extends TomcatBaseTest { + + @Test + public void testHttp10() throws Exception { + TestKeepAliveClient client = new TestKeepAliveClient(); + client.doHttp10Request(); + } + + @Test + public void testHttp11() throws Exception { + TestKeepAliveClient client = new TestKeepAliveClient(); + client.doHttp11Request(); + } + + + private class TestKeepAliveClient extends SimpleHttpClient { + + + private boolean init; + + private synchronized void init() { + if (init) { + return; + } + + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Simple", new SimpleServlet()); + root.addServletMappingDecoded("/test", "Simple"); + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "5")); + Assert.assertTrue(tomcat.getConnector().setProperty("connectionTimeout", "20000")); + Assert.assertTrue(tomcat.getConnector().setProperty("keepAliveTimeout", "50000")); + init = true; + } + + private void doHttp10Request() throws Exception { + Tomcat tomcat = getTomcatInstance(); + init(); + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + + // Open connection + connect(); + + // Send request in two parts + String[] request = new String[1]; + request[0] = + "GET /test HTTP/1.0" + CRLF + CRLF; + setRequest(request); + processRequest(false); // blocks until response has been read + boolean passed = (this.readLine()==null); + // Close the connection + disconnect(); + reset(); + tomcat.stop(); + Assert.assertTrue(passed); + } + + private void doHttp11Request() throws Exception { + Tomcat tomcat = getTomcatInstance(); + init(); + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + + // Open connection + connect(); + + // Send request in two parts + String[] request = new String[1]; + request[0] = + "GET /test HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Keep-Alive" + CRLF+ + "Keep-Alive: 300"+ CRLF+ CRLF; + + setRequest(request); + + for (int i=0; i<5; i++) { + processRequest(false); // blocks until response has been read + Assert.assertTrue(getResponseLine()!=null && getResponseLine().startsWith("HTTP/1.1 200 ")); + } + boolean passed = (this.readLine()==null); + // Close the connection + disconnect(); + reset(); + tomcat.stop(); + Assert.assertTrue(passed); + } + + @Override + public boolean isResponseBodyOK() { + return true; + } + + } + + + private static class SimpleServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentLength(0); + resp.flushBuffer(); + } + + } + +} diff --git a/test/org/apache/catalina/connector/TestMaxConnections.java b/test/org/apache/catalina/connector/TestMaxConnections.java new file mode 100644 index 0000000..ae3d658 --- /dev/null +++ b/test/org/apache/catalina/connector/TestMaxConnections.java @@ -0,0 +1,159 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestMaxConnections extends TomcatBaseTest { + private static final int MAX_CONNECTIONS = 3; + public static final int soTimeout = 5000; + public static final int connectTimeout = 1000; + + @Test + public void testConnector() throws Exception { + init(); + ConnectThread[] t = new ConnectThread[10]; + for (int i=0; i maxConnections) { + maxConnections = currentConnections; + } + } + + private static synchronized void decrement() { + currentConnections--; + } + + + public static synchronized int getMaxConnections() { + return maxConnections; + } + } +} diff --git a/test/org/apache/catalina/connector/TestOutputBuffer.java b/test/org/apache/catalina/connector/TestOutputBuffer.java new file mode 100644 index 0000000..e510f4d --- /dev/null +++ b/test/org/apache/catalina/connector/TestOutputBuffer.java @@ -0,0 +1,214 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestOutputBuffer extends TomcatBaseTest{ + + /* + * Expect that the buffered results are slightly slower since Tomcat now has + * an internal buffer so an extra one just adds overhead. + * + * @see "https://bz.apache.org/bugzilla/show_bug.cgi?id=52328" + */ + @Test + public void testWriteSpeed() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + + for (int i = 1; i <= WritingServlet.EXPECTED_CONTENT_LENGTH; i*=10) { + WritingServlet servlet = new WritingServlet(i); + Tomcat.addServlet(root, "servlet" + i, servlet); + root.addServletMappingDecoded("/servlet" + i, "servlet" + i); + } + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + + for (int i = 1; i <= WritingServlet.EXPECTED_CONTENT_LENGTH; i*=10) { + int rc = getUrl("http://localhost:" + getPort() + + "/servlet" + i, bc, null, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals( + WritingServlet.EXPECTED_CONTENT_LENGTH, bc.getLength()); + + bc.recycle(); + + rc = getUrl("http://localhost:" + getPort() + + "/servlet" + i + "?useBuffer=y", bc, null, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals( + WritingServlet.EXPECTED_CONTENT_LENGTH, bc.getLength()); + + bc.recycle(); + } + } + + @Test + public void testBug52577() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + + Bug52577Servlet bug52577 = new Bug52577Servlet(); + Tomcat.addServlet(root, "bug52577", bug52577); + root.addServletMappingDecoded("/", "bug52577"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/", bc, null, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals("OK", bc.toString()); + } + + private static class WritingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + protected static final int EXPECTED_CONTENT_LENGTH = 100000; + + private final String writeString; + private final int writeCount; + + WritingServlet(int writeLength) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < writeLength; i++) { + sb.append('x'); + } + writeString = sb.toString(); + writeCount = EXPECTED_CONTENT_LENGTH / writeLength; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("ISO-8859-1"); + + Writer w = resp.getWriter(); + + // Wrap with a buffer if necessary + String useBufferStr = req.getParameter("useBuffer"); + if (useBufferStr != null) { + w = new BufferedWriter(w); + } + + long start = System.nanoTime(); + for (int i = 0; i < writeCount; i++) { + w.write(writeString); + } + if (useBufferStr != null) { + w.flush(); + } + long lastRunNano = System.nanoTime() - start; + + System.out.println("Write length: " + writeString.length() + + ", Buffered: " + (useBufferStr == null ? "n" : "y") + + ", Time: " + lastRunNano + "ns"); + } + } + + private static class Bug52577Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + Writer w = resp.getWriter(); + w.write("OK"); + resp.resetBuffer(); + w.write("OK"); + } + } + + + @Test + public void testUtf8SurrogateBody() throws Exception { + // Create test data. This is carefully constructed to trigger the edge + // case. Small variations may cause the test to miss the edge case. + StringBuffer sb = new StringBuffer(); + sb.append('a'); + + for (int i = 0x10000; i < 0x11000; i++) { + char[] chars = Character.toChars(i); + sb.append(chars); + } + String data = sb.toString(); + + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Test", new Utf8WriteChars(data)); + root.addServletMappingDecoded("/test", "Test"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + getUrl("http://localhost:" + getPort() + "/test", bc, null); + + bc.setCharset(StandardCharsets.UTF_8); + Assert.assertEquals(data, bc.toString()); + } + + + private static class Utf8WriteChars extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final char[] chars; + + Utf8WriteChars(String data) { + chars = data.toCharArray(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/plain"); + Writer w = resp.getWriter(); + + for (char aChar : chars) { + w.write(aChar); + } + } + } + +} diff --git a/test/org/apache/catalina/connector/TestRequest.java b/test/org/apache/catalina/connector/TestRequest.java new file mode 100644 index 0000000..95ef702 --- /dev/null +++ b/test/org/apache/catalina/connector/TestRequest.java @@ -0,0 +1,966 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.authenticator.BasicAuthenticator; +import org.apache.catalina.filters.FailedRequestFilter; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterRequest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.EncodedSolidusHandling; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.LoginConfig; + +/** + * Test case for {@link Request}. + */ +public class TestRequest extends TomcatBaseTest { + + /** + * Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=37794 + * POST parameters are not returned from a call to + * any of the {@link HttpServletRequest} getParameterXXX() methods if the + * request is chunked. + */ + @Test + public void testBug37794() { + Bug37794Client client = new Bug37794Client(true); + + // Edge cases around zero + client.doRequest(-1, false); // Unlimited + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + client.reset(); + client.doRequest(0, false); // 0 bytes - too small should fail + Assert.assertTrue(client.isResponse413()); + client.reset(); + client.doRequest(1, false); // 1 byte - too small should fail + Assert.assertTrue(client.isResponse413()); + + // Edge cases around actual content length + client.reset(); + client.doRequest(6, false); // Too small should fail + Assert.assertTrue(client.isResponse413()); + client.reset(); + client.doRequest(7, false); // Just enough should pass + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + client.reset(); + client.doRequest(8, false); // 1 extra - should pass + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + + // Much larger + client.reset(); + client.doRequest(8096, false); // Plenty of space - should pass + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + + // Check for case insensitivity + client.reset(); + client.doRequest(8096, true); // Plenty of space - should pass + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + /** + * Additional test for failed requests handling when no FailedRequestFilter + * is defined. + */ + @Test + public void testBug37794withoutFilter() { + Bug37794Client client = new Bug37794Client(false); + + // Edge cases around actual content length + client.reset(); + client.doRequest(6, false); // Too small should fail + // Response code will be OK, but parameters list will be empty + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("", client.getResponseBody()); + } + + private static class Bug37794Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + /** + * Only interested in the parameters and values for POST requests. + */ + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Just echo the parameters and values back as plain text + resp.setContentType("text/plain"); + + PrintWriter out = resp.getWriter(); + + // Assume one value per attribute + Enumeration names = req.getParameterNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + out.println(name + "=" + req.getParameter(name)); + } + } + } + + /** + * Bug 37794 test client. + */ + private class Bug37794Client extends SimpleHttpClient { + + private final boolean createFilter; + + private boolean init; + + Bug37794Client(boolean createFilter) { + this.createFilter = createFilter; + } + + private synchronized void init() throws Exception { + if (init) { + return; + } + + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Bug37794", new Bug37794Servlet()); + root.addServletMappingDecoded("/test", "Bug37794"); + + if (createFilter) { + FilterDef failedRequestFilter = new FilterDef(); + failedRequestFilter.setFilterName("failedRequestFilter"); + failedRequestFilter.setFilterClass( + FailedRequestFilter.class.getName()); + FilterMap failedRequestFilterMap = new FilterMap(); + failedRequestFilterMap.setFilterName("failedRequestFilter"); + failedRequestFilterMap.addURLPatternDecoded("/*"); + root.addFilterDef(failedRequestFilter); + root.addFilterMap(failedRequestFilterMap); + } + + tomcat.start(); + + setPort(tomcat.getConnector().getLocalPort()); + + init = true; + } + + private Exception doRequest(int postLimit, boolean ucChunkedHead) { + Tomcat tomcat = getTomcatInstance(); + + try { + init(); + tomcat.getConnector().setMaxPostSize(postLimit); + + // Open connection + connect(); + + // Send request in two parts + String[] request = new String[2]; + if (ucChunkedHead) { + request[0] = + "POST http://localhost:8080/test HTTP/1.1" + CRLF + + "Host: localhost:8080" + CRLF + + "content-type: application/x-www-form-urlencoded" + CRLF + + "Transfer-Encoding: CHUNKED" + CRLF + + "Connection: close" + CRLF + + CRLF + + "3" + CRLF + + "a=1" + CRLF; + } else { + request[0] = + "POST http://localhost:8080/test HTTP/1.1" + CRLF + + "Host: localhost:8080" + CRLF + + "content-type: application/x-www-form-urlencoded" + CRLF + + "Transfer-Encoding: chunked" + CRLF + + "Connection: close" + CRLF + + CRLF + + "3" + CRLF + + "a=1" + CRLF; + } + request[1] = + "4" + CRLF + + "&b=2" + CRLF + + "0" + CRLF + + CRLF; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("a=1")) { + return false; + } + if (!getResponseBody().contains("b=2")) { + return false; + } + return true; + } + + } + + /* + * Test case for + * bug + * 38118. + */ + @Test + public void testBug38113() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add the Servlet + Tomcat.addServlet(ctx, "servlet", new EchoQueryStringServlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + // No query string + ByteChunk res = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("QueryString=null", res.toString()); + + // Query string + res = getUrl("http://localhost:" + getPort() + "/?a=b"); + Assert.assertEquals("QueryString=a=b", res.toString()); + + // Empty string + res = getUrl("http://localhost:" + getPort() + "/?"); + Assert.assertEquals("QueryString=", res.toString()); + } + + private static final class EchoQueryStringServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + pw.print("QueryString=" + req.getQueryString()); + } + } + + /* + * Test case for {@link Request#login(String, String)} and + * {@link Request#logout()}. + */ + @Test + public void testLoginLogout() throws Exception{ + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + LoginConfig config = new LoginConfig(); + config.setAuthMethod("BASIC"); + ctx.setLoginConfig(config); + ctx.getPipeline().addValve(new BasicAuthenticator()); + + Tomcat.addServlet(ctx, "servlet", new LoginLogoutServlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser(LoginLogoutServlet.USER, LoginLogoutServlet.PWD); + ctx.setRealm(realm); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals(LoginLogoutServlet.OK, res.toString()); + } + + private static final class LoginLogoutServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + private static final String USER = "user"; + private static final String PWD = "pwd"; + private static final String OK = "OK"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + req.login(USER, PWD); + + if (!req.getRemoteUser().equals(USER)) { + throw new ServletException(); + } + if (!req.getUserPrincipal().getName().equals(USER)) { + throw new ServletException(); + } + + req.logout(); + + if (req.getRemoteUser() != null) { + throw new ServletException(); + } + if (req.getUserPrincipal() != null) { + throw new ServletException(); + } + + resp.getWriter().write(OK); + } + + } + + @Test + public void testBug49424NoChunking() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", + System.getProperty("java.io.tmpdir")); + Tomcat.addServlet(root, "Bug37794", new Bug37794Servlet()); + root.addServletMappingDecoded("/", "Bug37794"); + tomcat.start(); + + HttpURLConnection conn = getConnection("http://localhost:" + getPort() + "/"); + InputStream is = conn.getInputStream(); + Assert.assertNotNull(is); + } + + @Test + public void testBug49424WithChunking() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", + System.getProperty("java.io.tmpdir")); + Tomcat.addServlet(root, "Bug37794", new Bug37794Servlet()); + root.addServletMappingDecoded("/", "Bug37794"); + tomcat.start(); + + HttpURLConnection conn = getConnection("http://localhost:" + getPort() + "/"); + conn.setChunkedStreamingMode(8 * 1024); + InputStream is = conn.getInputStream(); + Assert.assertNotNull(is); + } + + /** + * Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=48692 + * PUT requests should be able to fetch request parameters coming from + * the request body (when properly configured using the new parseBodyMethod + * setting). + */ + @Test + public void testBug48692() { + Bug48692Client client = new Bug48692Client(); + + // Make sure GET works properly + client.doRequest("GET", "foo=bar", null, null, false); + + Assert.assertTrue("Non-200 response for GET request", + client.isResponse200()); + Assert.assertEquals("Incorrect response for GET request", + "foo=bar", + client.getResponseBody()); + + client.reset(); + + // + // Make sure POST works properly + // + // POST with separate GET and POST parameters + client.doRequest("POST", "foo=bar", "application/x-www-form-urlencoded", "bar=baz", true); + + Assert.assertTrue("Non-200 response for POST request", + client.isResponse200()); + Assert.assertEquals("Incorrect response for POST request", + "bar=baz,foo=bar", + client.getResponseBody()); + + client.reset(); + + // POST with overlapping GET and POST parameters + client.doRequest("POST", "foo=bar&bar=foo", "application/x-www-form-urlencoded", "bar=baz&foo=baz", true); + + Assert.assertTrue("Non-200 response for POST request", + client.isResponse200()); + Assert.assertEquals("Incorrect response for POST request", + "bar=baz,bar=foo,foo=bar,foo=baz", + client.getResponseBody()); + + client.reset(); + + // PUT without POST-style parsing + client.doRequest("PUT", "foo=bar&bar=foo", "application/x-www-form-urlencoded", "bar=baz&foo=baz", false); + + Assert.assertTrue("Non-200 response for PUT/noparse request", + client.isResponse200()); + Assert.assertEquals("Incorrect response for PUT request", + "bar=foo,foo=bar", + client.getResponseBody()); + + client.reset(); + + // PUT with POST-style parsing + client.doRequest("PUT", "foo=bar&bar=foo", "application/x-www-form-urlencoded", "bar=baz&foo=baz", true); + + Assert.assertTrue("Non-200 response for PUT request", + client.isResponse200()); + Assert.assertEquals("Incorrect response for PUT/parse request", + "bar=baz,bar=foo,foo=bar,foo=baz", + client.getResponseBody()); + + client.reset(); + + /* + private Exception doRequest(String method, + String queryString, + String contentType, + String requestBody, + boolean allowBody) { + */ + } + + @Test + public void testBug54984() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", + System.getProperty("java.io.tmpdir")); + root.setAllowCasualMultipartParsing(true); + Tomcat.addServlet(root, "Bug54984", new Bug54984Servlet()); + root.addServletMappingDecoded("/", "Bug54984"); + tomcat.start(); + + HttpURLConnection conn = getConnection("http://localhost:" + getPort() + + "/parseParametersBeforeParseParts"); + + prepareRequestBug54984(conn); + + checkResponseBug54984(conn); + + conn.disconnect(); + + conn = getConnection("http://localhost:" + getPort() + "/"); + + prepareRequestBug54984(conn); + + checkResponseBug54984(conn); + + conn.disconnect(); + } + + /** + * + */ + private static class EchoParametersServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + /** + * Only interested in the parameters and values for requests. + * Note: echos parameters in alphabetical order. + */ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Just echo the parameters and values back as plain text + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + PrintWriter out = resp.getWriter(); + + TreeMap parameters = + new TreeMap<>(req.getParameterMap()); + + boolean first = true; + + for(String name: parameters.keySet()) { + String[] values = req.getParameterValues(name); + + Arrays.sort(values); + + for (String value : values) { + if (first) { + first = false; + } else { + out.print(","); + } + + out.print(name + "=" + value); + } + } + } + } + + /** + * Bug 48692 test client: test for allowing PUT request bodies. + */ + private class Bug48692Client extends SimpleHttpClient { + + private boolean init; + + private synchronized void init() throws Exception { + if (init) { + return; + } + + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "EchoParameters", new EchoParametersServlet()); + root.addServletMappingDecoded("/echo", "EchoParameters"); + tomcat.start(); + + setPort(tomcat.getConnector().getLocalPort()); + + init = true; + } + + private Exception doRequest(String method, + String queryString, + String contentType, + String requestBody, + boolean allowBody) { + Tomcat tomcat = getTomcatInstance(); + + try { + init(); + if(allowBody) { + tomcat.getConnector().setParseBodyMethods(method); + } + else { + tomcat.getConnector().setParseBodyMethods(""); // never parse + } + + // Open connection + connect(); + + // Re-encode the request body so that bytes = characters + if(null != requestBody) { + requestBody = new String(requestBody.getBytes("UTF-8"), "ASCII"); + } + + // Send specified request body using method + String[] request = { + ( + method + " http://localhost:" + getPort() + "/echo" + + (null == queryString ? "" : ("?" + queryString)) + + " HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + (null == contentType ? "" + : ("Content-Type: " + contentType + CRLF)) + + "Connection: close" + CRLF + + (null == requestBody ? "" : "Content-Length: " + requestBody.length() + CRLF) + + CRLF + + (null == requestBody ? "" : requestBody) + ) + }; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + return false; // Don't care + } + } + + private HttpURLConnection getConnection(String query) throws IOException { + URL postURL; + postURL = new URL(query); + HttpURLConnection conn = (HttpURLConnection) postURL.openConnection(); + conn.setRequestMethod("POST"); + + conn.setDoInput(true); + conn.setDoOutput(true); + conn.setUseCaches(false); + conn.setAllowUserInteraction(false); + + return conn; + } + + private static class Bug54984Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.setCharacterEncoding("UTF-8"); + + if (req.getRequestURI().endsWith("parseParametersBeforeParseParts")) { + req.getParameterNames(); + } + + req.getPart("part"); + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + resp.getWriter().println("Part " + req.getParameter("part")); + } + } + + private void prepareRequestBug54984(HttpURLConnection conn) + throws Exception { + String boundary = "-----" + System.currentTimeMillis(); + conn.setRequestProperty("Content-Type", + "multipart/form-data; boundary=" + boundary); + + try (OutputStreamWriter osw = new OutputStreamWriter(conn.getOutputStream(), "UTF-8"); + PrintWriter writer = new PrintWriter(osw, true)) { + writer.append("--" + boundary).append("\r\n"); + writer.append("Content-Disposition: form-data; name=\"part\"\r\n"); + writer.append("Content-Type: text/plain; charset=UTF-8\r\n"); + writer.append("\r\n"); + writer.append("äö").append("\r\n"); + writer.flush(); + + writer.append("\r\n"); + writer.flush(); + + writer.append("--" + boundary + "--").append("\r\n"); + } + } + + private void checkResponseBug54984(HttpURLConnection conn) + throws Exception { + List response = new ArrayList<>(); + int status = conn.getResponseCode(); + if (status == HttpURLConnection.HTTP_OK) { + try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), "UTF-8"); + BufferedReader reader = new BufferedReader(isr)) { + String line = null; + while ((line = reader.readLine()) != null) { + response.add(line); + } + Assert.assertTrue(response.contains("Part äö")); + } + } else { + Assert.fail("OK status was expected: " + status); + } + } + + @Test + public void testBug56501a() throws Exception { + doBug56501("/path", "/path", "/path"); + } + + @Test + public void testBug56501b() throws Exception { + doBug56501("/path", "/path/", "/path"); + } + + @Test + public void testBug56501c() throws Exception { + doBug56501("/path", "/path/xxx", "/path"); + } + + @Test + public void testBug56501d() throws Exception { + doBug56501("", "", ""); + } + + @Test + public void testBug56501e() throws Exception { + doBug56501("", "/", ""); + } + + @Test + public void testBug56501f() throws Exception { + doBug56501("", "/xxx", ""); + } + + @Test + public void testBug56501g() throws Exception { + doBug56501("/path/abc", "/path/abc", "/path/abc"); + } + + @Test + public void testBug56501h() throws Exception { + doBug56501("/path/abc", "/path/abc/", "/path/abc"); + } + + @Test + public void testBug56501i() throws Exception { + doBug56501("/path/abc", "/path/abc/xxx", "/path/abc"); + } + + @Test + public void testBug56501j() throws Exception { + doBug56501("/pa_th/abc", "/pa%5Fth/abc", "/pa%5Fth/abc"); + } + + @Test + public void testBug56501k() throws Exception { + doBug56501("/pa_th/abc", "/pa%5Fth/abc/", "/pa%5Fth/abc"); + } + + @Test + public void testBug56501l() throws Exception { + doBug56501("/pa_th/abc", "/pa%5Fth/abc/xxx", "/pa%5Fth/abc"); + } + + @Test + public void testBug56501m() throws Exception { + doBug56501("/pa_th/abc", "/pa_th/abc", "/pa_th/abc"); + } + + @Test + public void testBug56501n() throws Exception { + doBug56501("/pa_th/abc", "/pa_th/abc/", "/pa_th/abc"); + } + + @Test + public void testBug56501o() throws Exception { + doBug56501("/pa_th/abc", "/pa_th/abc/xxx", "/pa_th/abc"); + } + + @Test + public void testBug56501p() throws Exception { + doBug56501("/path/abc", "/path;a=b/abc/xxx", "/path;a=b/abc"); + } + + @Test + public void testBug56501q() throws Exception { + doBug56501("/path/abc", "/path/abc;a=b/xxx", "/path/abc;a=b"); + } + + @Test + public void testBug56501r() throws Exception { + doBug56501("/path/abc", "/path/abc/xxx;a=b", "/path/abc"); + } + + @Test + public void testBug56501s() throws Exception { + doBug56501("/path/abc", "/.;a=b/path/abc/xxx", "/.;a=b/path/abc"); + } + + @Test + public void testBug57215a() throws Exception { + doBug56501("/path", "//path", "//path"); + } + + @Test + public void testBug57215b() throws Exception { + doBug56501("/path", "//path/", "//path"); + } + + @Test + public void testBug57215c() throws Exception { + doBug56501("/path", "/%2Fpath", "/%2Fpath", EncodedSolidusHandling.DECODE); + } + + @Test + public void testBug57215d() throws Exception { + doBug56501("/path", "/%2Fpath%2F", "/%2Fpath", EncodedSolidusHandling.DECODE); + } + + @Test + public void testBug57215e() throws Exception { + doBug56501("/path", "/foo/../path", "/foo/../path"); + } + + @Test + public void testBug57215f() throws Exception { + doBug56501("/path", "/foo/..%2fpath", "/foo/..%2fpath", EncodedSolidusHandling.DECODE); + } + + private void doBug56501(String deployPath, String requestPath, String expected) throws Exception { + doBug56501(deployPath, requestPath, expected, EncodedSolidusHandling.REJECT); + } + + + private void doBug56501(String deployPath, String requestPath, String expected, + EncodedSolidusHandling encodedSolidusHandling) throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + tomcat.getConnector().setEncodedSolidusHandling(encodedSolidusHandling.getValue()); + + // No file system docBase required + Context ctx = tomcat.addContext(deployPath, null); + ctx.setAllowMultipleLeadingForwardSlashInPath(true); + + Tomcat.addServlet(ctx, "servlet", new Bug56501Servlet()); + ctx.addServletMappingDecoded("/*", "servlet"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + requestPath); + String resultPath = res.toString(); + if (resultPath == null) { + resultPath = ""; + } + Assert.assertEquals(expected, resultPath); + } + + private static class Bug56501Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print(req.getContextPath()); + } + } + + @Test + public void getLocaleMultipleHeaders01() throws Exception { + TesterRequest req = new TesterRequest(); + + req.addHeader("accept-language", "en;q=0.5"); + req.addHeader("accept-language", "en-gb"); + + Locale actual = req.getLocale(); + Locale expected = Locale.forLanguageTag("en-gb"); + + Assert.assertEquals(expected, actual); + } + + /* + * Reverse header order of getLocaleMultipleHeaders01() and make sure the + * result is the same. + */ + @Test + public void getLocaleMultipleHeaders02() throws Exception { + TesterRequest req = new TesterRequest(); + + req.addHeader("accept-language", "en-gb"); + req.addHeader("accept-language", "en;q=0.5"); + + Locale actual = req.getLocale(); + Locale expected = Locale.forLanguageTag("en-gb"); + + Assert.assertEquals(expected, actual); + } + + + @Test + public void testGetReaderValidEncoding() throws Exception { + doTestGetReader("ISO-8859-1", true); + } + + + @Test + public void testGetReaderInvalidEbcoding() throws Exception { + doTestGetReader("X-Invalid", false); + } + + + private void doTestGetReader(String userAgentCharacterEncoding, boolean expect200) + throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new Bug61264GetReaderServlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + Charset charset = StandardCharsets.ISO_8859_1; + try { + charset = Charset.forName(userAgentCharacterEncoding); + } catch (UnsupportedCharsetException e) { + // Ignore - use default set above + } + byte[] body = "Test".getBytes(charset); + ByteChunk bc = new ByteChunk(); + Map> reqHeaders = new HashMap<>(); + reqHeaders.put("Content-Type", + Arrays.asList(new String[] {"text/plain;charset=" + userAgentCharacterEncoding})); + + int rc = postUrl(body, "http://localhost:" + getPort() + "/", bc, reqHeaders, null); + + if (expect200) { + Assert.assertEquals(200, rc); + } else { + Assert.assertEquals(500, rc); + } + } + + + private static class Bug61264GetReaderServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // This is intended for POST requests + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Container will handle any errors + req.getReader(); + } + } +} diff --git a/test/org/apache/catalina/connector/TestResponse.java b/test/org/apache/catalina/connector/TestResponse.java new file mode 100644 index 0000000..39e483f --- /dev/null +++ b/test/org/apache/catalina/connector/TestResponse.java @@ -0,0 +1,1004 @@ +/* + * 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.catalina.connector; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.unittest.TesterRequest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; + +/** + * Test case for {@link Request}. + */ +public class TestResponse extends TomcatBaseTest { + + @Test + public void testBug49598() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new Bug49598Servlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + Map> headers = new CaseInsensitiveKeyMap<>(); + getUrl("http://localhost:" + getPort() + "/", new ByteChunk(), headers); + + // Check for headers without a name + for (Map.Entry> header : headers.entrySet()) { + if (header.getKey() == null) { + // Expected if this is the response line + List values = header.getValue(); + if (values.size() == 1 && + values.get(0).startsWith("HTTP/1.1")) { + continue; + } + Assert.fail("Null header name detected for value " + values); + } + } + + // Check for exactly one Set-Cookie header + int count = 0; + for (String headerName : headers.keySet()) { + if ("Set-Cookie".equals(headerName)) { + count ++; + } + } + Assert.assertEquals(1, count); + } + + private static final class Bug49598Servlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + HttpSession session = req.getSession(true); + session.invalidate(); + req.getSession(true); + } + + } + + + /* + * Tests an issue noticed during the investigation of BZ 52811. + */ + @Test + public void testCharset() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new CharsetServlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + + Assert.assertEquals("OK", bc.toString()); + } + + private static final class CharsetServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + PrintWriter pw = resp.getWriter(); + resp.setHeader("Content-Type", "text/plain;charset=UTF-8"); + + // Should be ISO-8859-1 because getWriter() was called before + // setHeader() + if (resp.getCharacterEncoding().equals("ISO-8859-1")) { + pw.print("OK"); + } else { + pw.print("FAIL: " + resp.getCharacterEncoding()); + } + } + + } + + + @Test + public void testBug52811() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new Bug52811Servlet()); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + + Assert.assertEquals("OK", bc.toString()); + } + + + @Test + public void testBug53062a() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("./bar.html"); + + Assert.assertEquals("http://localhost:8080/level1/level2/bar.html", + result); + } + + + @Test + public void testBug53062b() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("."); + + Assert.assertEquals("http://localhost:8080/level1/level2/", result); + } + + + @Test + public void testBug53062c() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute(".."); + + Assert.assertEquals("http://localhost:8080/level1/", result); + } + + + @Test + public void testBug53062d() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute(".././.."); + + Assert.assertEquals("http://localhost:8080/", result); + } + + + @Test(expected=IllegalArgumentException.class) + public void testBug53062e() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + resp.toAbsolute("../../.."); + } + + + @Test + public void testBug53062f() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("bar.html"); + + Assert.assertEquals( + "http://localhost:8080/level1/level2/bar.html", result); + } + + + @Test + public void testBug53062g() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("bar.html?x=/../"); + + Assert.assertEquals( + "http://localhost:8080/level1/level2/bar.html?x=/../", result); + } + + + @Test + public void testBug53062h() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("bar.html?x=/../../"); + + Assert.assertEquals( + "http://localhost:8080/level1/level2/bar.html?x=/../../", + result); + } + + + @Test + public void testBug53062i() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("./.?x=/../../"); + + Assert.assertEquals( + "http://localhost:8080/level1/level2/?x=/../../", result); + } + + + @Test + public void testBug53062j() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("./..?x=/../../"); + + Assert.assertEquals("http://localhost:8080/level1/?x=/../../", result); + } + + + @Test + public void testBug53062k() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("./..?x=/../.."); + + Assert.assertEquals( + "http://localhost:8080/level1/?x=/../..", + result); + } + + + @Test + public void testBug53062l() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("bar.html#/../"); + + Assert.assertEquals( + "http://localhost:8080/level1/level2/bar.html#/../", result); + } + + + @Test + public void testBug53062m() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("bar.html#/../../"); + + Assert.assertEquals( + "http://localhost:8080/level1/level2/bar.html#/../../", result); + } + + + @Test + public void testBug53062n() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("./.#/../../"); + + Assert.assertEquals( + "http://localhost:8080/level1/level2/#/../../", result); + } + + + @Test + public void testBug53062o() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("./..#/../../"); + + Assert.assertEquals("http://localhost:8080/level1/#/../../", result); + } + + + @Test + public void testBug53062p() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.toAbsolute("./..#/../.."); + + Assert.assertEquals("http://localhost:8080/level1/#/../..", result); + } + + + private void doTestEncodeURL(String location, String expected) { + Request req = new TesterRequest(true); + req.setRequestedSessionId("1234"); + req.setRequestedSessionURL(true); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.encodeURL(location); + Assert.assertEquals(expected, result); + } + + + @Test + public void testEncodeURL01() throws Exception { + doTestEncodeURL("./bar.html", "./bar.html;jsessionid=1234"); + } + + + @Test + public void testEncodeURL02() throws Exception { + doTestEncodeURL(".", ".;jsessionid=1234"); + } + + + @Test + public void testEncodeURL03() throws Exception { + doTestEncodeURL("..", "..;jsessionid=1234"); + } + + + @Test + public void testEncodeURL04() throws Exception { + doTestEncodeURL(".././..", ".././..;jsessionid=1234"); + } + + + public void testEncodeURL05() throws Exception { + doTestEncodeURL("../../..", "../../.."); + } + + + @Test + public void testEncodeURL06() throws Exception { + doTestEncodeURL("bar.html", "bar.html;jsessionid=1234"); + } + + + @Test + public void testEncodeURL07() throws Exception { + doTestEncodeURL("bar.html?x=/../", "bar.html;jsessionid=1234?x=/../"); + } + + + @Test + public void testEncodeURL08() throws Exception { + doTestEncodeURL("bar.html?x=/../../", "bar.html;jsessionid=1234?x=/../../"); + } + + + @Test + public void testEncodeURL09() throws Exception { + doTestEncodeURL("./.?x=/../../", "./.;jsessionid=1234?x=/../../"); + } + + + @Test + public void testEncodeURL10() throws Exception { + doTestEncodeURL("./..?x=/../../", "./..;jsessionid=1234?x=/../../"); + } + + + @Test + public void testEncodeURL11() throws Exception { + doTestEncodeURL("./..?x=/../..", "./..;jsessionid=1234?x=/../.."); + } + + + @Test + public void testEncodeURL12() throws Exception { + doTestEncodeURL("bar.html#/../", "bar.html;jsessionid=1234#/../"); + } + + + @Test + public void testEncodeURL13() throws Exception { + doTestEncodeURL("bar.html#/../../", "bar.html;jsessionid=1234#/../../"); + } + + + @Test + public void testEncodeURL14() throws Exception { + doTestEncodeURL("./.#/../../", "./.;jsessionid=1234#/../../"); + } + + + @Test + public void testEncodeURL15() throws Exception { + doTestEncodeURL("./..#/../../", "./..;jsessionid=1234#/../../"); + } + + + @Test + public void testEncodeURL16() throws Exception { + doTestEncodeURL("./..#/../..", "./..;jsessionid=1234#/../.."); + } + + + private void doTestEncodeRedirectURL(String location, String expected) { + Request req = new TesterRequest(true); + req.setRequestedSessionId("1234"); + req.setRequestedSessionURL(true); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.encodeRedirectURL(location); + Assert.assertEquals(expected, result); + } + + + @Test + public void testEncodeRedirectURL01() throws Exception { + doTestEncodeRedirectURL("./bar.html", "./bar.html;jsessionid=1234"); + } + + + @Test + public void testEncodeRedirectURL02() throws Exception { + doTestEncodeRedirectURL(".", ".;jsessionid=1234"); + } + + + @Test + public void testEncodeRedirectURL03() throws Exception { + doTestEncodeRedirectURL("..", "..;jsessionid=1234"); + } + + + @Test + public void testEncodeRedirectURL04() throws Exception { + doTestEncodeRedirectURL(".././..", ".././..;jsessionid=1234"); + } + + + @Test(expected=IllegalArgumentException.class) + public void testEncodeRedirectURL05() throws Exception { + doTestEncodeRedirectURL("../../..", "throws IAE"); + } + + + @Test + public void testEncodeRedirectURL06() throws Exception { + doTestEncodeRedirectURL("bar.html", "bar.html;jsessionid=1234"); + } + + + @Test + public void testEncodeRedirectURL07() throws Exception { + doTestEncodeRedirectURL("bar.html?x=/../", "bar.html;jsessionid=1234?x=/../"); + } + + + @Test + public void testEncodeRedirectURL08() throws Exception { + doTestEncodeRedirectURL("bar.html?x=/../../", "bar.html;jsessionid=1234?x=/../../"); + } + + + @Test + public void testEncodeRedirectURL09() throws Exception { + doTestEncodeRedirectURL("./.?x=/../../", "./.;jsessionid=1234?x=/../../"); + } + + + @Test + public void testEncodeRedirectURL10() throws Exception { + doTestEncodeRedirectURL("./..?x=/../../", "./..;jsessionid=1234?x=/../../"); + } + + + @Test + public void testEncodeRedirectURL11() throws Exception { + doTestEncodeRedirectURL("./..?x=/../..", "./..;jsessionid=1234?x=/../.."); + } + + + @Test + public void testEncodeRedirectURL12() throws Exception { + doTestEncodeRedirectURL("bar.html#/../", "bar.html;jsessionid=1234#/../"); + } + + + @Test + public void testEncodeRedirectURL13() throws Exception { + doTestEncodeRedirectURL("bar.html#/../../", "bar.html;jsessionid=1234#/../../"); + } + + + @Test + public void testEncodeRedirectURL14() throws Exception { + doTestEncodeRedirectURL("./.#/../../", "./.;jsessionid=1234#/../../"); + } + + + @Test + public void testEncodeRedirectURL15() throws Exception { + doTestEncodeRedirectURL("./..#/../../", "./..;jsessionid=1234#/../../"); + } + + + @Test + public void testEncodeRedirectURL16() throws Exception { + doTestEncodeURL("./..#/../..", "./..;jsessionid=1234#/../.."); + } + + + @Test + public void testSendRedirect01() throws Exception { + doTestSendRedirect("../foo", "../foo"); + } + + + @Test + public void testSendRedirect02() throws Exception { + doTestSendRedirect("../foo bar", "../foo bar"); + } + + + @Test + public void testSendRedirect03() throws Exception { + doTestSendRedirect("../foo%20bar", "../foo%20bar"); + } + + + private void doTestSendRedirect(String input, String expectedLocation) throws Exception { + // Set-up. + // Note: Not sufficient for testing relative -> absolute + Connector connector = new Connector(); + org.apache.coyote.Response cResponse = new org.apache.coyote.Response(); + Response response = new Response(); + response.setCoyoteResponse(cResponse); + Request request = new Request(connector); + org.apache.coyote.Request cRequest = new org.apache.coyote.Request(); + request.setCoyoteRequest(cRequest); + Context context = new TesterContext(); + request.getMappingData().context = context; + response.setRequest(request); + // Do test + response.sendRedirect(input); + String location = response.getHeader("Location"); + Assert.assertEquals(expectedLocation, location); + } + + + @Test + public void testBug53469a() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.encodeURL("../bar.html"); + + Assert.assertEquals("../bar.html", result); + } + + + @Test + public void testBug53469b() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + String result = resp.encodeURL("../../../../bar.html"); + + Assert.assertEquals("../../../../bar.html", result); + } + + + private static final String ISO_8859_1 = StandardCharsets.ISO_8859_1.name(); + private static final String UTF_8 = StandardCharsets.UTF_8.name(); + private static final String UNKNOWN = "unknown"; + private static final String TEXT = "text/plain"; + private static final String TEXT_ISO_8859_1 = TEXT + ";charset=" + ISO_8859_1; + private static final String TEXT_UTF_8 = TEXT + ";charset=" + UTF_8; + private static final String TEXT_UNKNOWN = TEXT + ";charset=" + UNKNOWN; + private static final Locale UNDETERMINED = Locale.forLanguageTag("xxx"); + + @Test + public void testSetCharacterEncoding01() { + Response response = setupResponse(); + + // Check default + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetCharacterEncoding02() { + Response response = setupResponse(); + + // Check multiple calls + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetCharacterEncoding03() throws IOException { + Response response = setupResponse(); + + // Check after getWriter() + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.getWriter(); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetCharacterEncoding04() throws IOException { + Response response = setupResponse(); + + // Check after commit + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.flushBuffer(); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetCharacterEncoding05() { + Response response = setupResponse(); + + // Check calling with null + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setCharacterEncoding(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test(expected = UnsupportedEncodingException.class) + public void testSetCharacterEncoding06() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set and writer + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UNKNOWN); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getWriter(); + } + + + @Test + public void testSetCharacterEncoding07() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UNKNOWN); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getOutputStream(); + } + + + @Test + public void testSetCharacterEncoding08() { + Response response = setupResponse(); + + // Check multiple calls with different methods + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetContentType01() { + Response response = setupResponse(); + + // Check multiple calls + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetContentType02() throws IOException { + Response response = setupResponse(); + + // Check after getWriter() + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.getWriter(); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetContentType03() throws IOException { + Response response = setupResponse(); + + // Check after commit + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.flushBuffer(); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetContentType04() { + Response response = setupResponse(); + + // Check calling with null + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setContentType(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test(expected = UnsupportedEncodingException.class) + public void testSetContentType05() throws IOException { + Response response = setupResponse(); + response.getContext().addLocaleEncodingMappingParameter(Locale.UK.toLanguageTag(), UNKNOWN); + + // Check calling with an unknown character set and writer + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UNKNOWN); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getWriter(); + } + + + @Test + public void testSetContentType06() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setContentType(TEXT_UNKNOWN); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getOutputStream(); + } + + + @Test + public void testSetLocale01() { + Response response = setupResponse(); + + // Check multiple calls + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetLocale02() throws IOException { + Response response = setupResponse(); + + // Check after getWriter() + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.getWriter(); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetLocale03() throws IOException { + Response response = setupResponse(); + + // Check after commit + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.flushBuffer(); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + } + + + @Test + public void testSetLocale04() { + Response response = setupResponse(); + + // Check calling with null + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setLocale(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test(expected = UnsupportedEncodingException.class) + public void testSetLocale05() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set and writer + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(UNDETERMINED); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getWriter(); + } + + + @Test + public void testSetLocale06() throws IOException { + Response response = setupResponse(); + + // Check calling with an unknown character set + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + response.setLocale(UNDETERMINED); + Assert.assertEquals(UNKNOWN, response.getCharacterEncoding()); + response.getOutputStream(); + } + + + @Test + public void testSetLocale07() { + Response response = setupResponse(); + + // Check setLocale() is over-ridden by setCharacterEncoding + + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + + // setLocale doesn't change previous value + response.setCharacterEncoding(UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + + // Reset + response.setCharacterEncoding(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + + // setLocale is over-ridden by setCharacterEncoding + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setCharacterEncoding(ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + @Test + public void testSetLocale08() { + Response response = setupResponse(); + + // Check setLocale() is over-ridden by setContentType + + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + + // setLocale doesn't change previous value + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setLocale(Locale.ENGLISH); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + + // Reset + response.setContentType(null); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + + // setLocale is over-ridden by setContentTpe + response.setLocale(Locale.CHINESE); + Assert.assertEquals(UTF_8, response.getCharacterEncoding()); + response.setContentType(TEXT_ISO_8859_1); + Assert.assertEquals(ISO_8859_1, response.getCharacterEncoding()); + } + + + private Response setupResponse() { + Connector connector = new Connector(); + org.apache.coyote.Response cResponse = new org.apache.coyote.Response(); + Response response = new Response(); + response.setCoyoteResponse(cResponse); + Request request = new Request(connector); + org.apache.coyote.Request cRequest = new org.apache.coyote.Request(); + request.setCoyoteRequest(cRequest); + Context context = new TesterContext(); + request.getMappingData().context = context; + response.setRequest(request); + context.addLocaleEncodingMappingParameter(Locale.ENGLISH.getLanguage(), ISO_8859_1); + context.addLocaleEncodingMappingParameter(Locale.CHINESE.getLanguage(), UTF_8); + context.addLocaleEncodingMappingParameter(UNDETERMINED.toLanguageTag(), UNKNOWN); + return response; + } + + + private static final class Bug52811Servlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("multipart/related;" + + "boundary=1_4F50BD36_CDF8C28;" + + "Start=\"<31671603.smil>\";" + + "Type=\"application/smil;charset=UTF-8\""); + + // Should be ISO-8859-1 because the charset in the above is part + // of the Type parameter + PrintWriter pw = resp.getWriter(); + if (resp.getCharacterEncoding().equals("ISO-8859-1")) { + pw.print("OK"); + } else { + pw.print("FAIL: " + resp.getCharacterEncoding()); + } + } + + } +} diff --git a/test/org/apache/catalina/connector/TestResponsePerformance.java b/test/org/apache/catalina/connector/TestResponsePerformance.java new file mode 100644 index 0000000..3648296 --- /dev/null +++ b/test/org/apache/catalina/connector/TestResponsePerformance.java @@ -0,0 +1,86 @@ +/* + * 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.catalina.connector; + +import java.net.URI; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.LoggingBaseTest; +import org.apache.tomcat.unittest.TesterRequest; + +/* + * This is a relative performance test so it remains part of the standard test run. If the test fails then we need to + * looking at why and possibly disable/remove the homebrew approach for some OS/Java combinations. + */ +public class TestResponsePerformance extends LoggingBaseTest { + + private static final int ITERATIONS = 1000000; + + @Test + public void testToAbsolutePerformance() throws Exception { + Request req = new TesterRequest(); + Response resp = new Response(); + resp.setRequest(req); + + // Warm up + doHomebrew(resp); + doUri(); + + // Note: With Java 11 the 'homebrew' approach is consistently 3-4 times faster on both MacOS (Intel) and Linux + // With Java 22 EA the 'homebrew' approach is consistently a little over 2x faster on MacOS (M1) + + // To allow for timing differences between runs, a "best of n" approach + // is taken for this test + final int bestOf = 5; + final int winTarget = (bestOf + 1) / 2; + int homebrewWin = 0; + int count = 0; + + while (count < bestOf && homebrewWin < winTarget) { + long homebrew = doHomebrew(resp); + long uri = doUri(); + log.info("Current 'home-brew': " + homebrew + "ms, Using URI: " + uri + "ms"); + if (homebrew < uri) { + homebrewWin++; + } + count++; + } + Assert.assertTrue(homebrewWin == winTarget); + } + + + private long doHomebrew(Response resp) { + long start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) { + resp.toAbsolute("bar.html"); + } + return System.currentTimeMillis() - start; + } + + + private long doUri() { + long start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) { + URI base = URI.create( + "http://localhost:8080/level1/level2/foo.html"); + base.resolve(URI.create("bar.html")).toASCIIString(); + } + return System.currentTimeMillis() - start; + } +} diff --git a/test/org/apache/catalina/connector/TestSendFile.java b/test/org/apache/catalina/connector/TestSendFile.java new file mode 100644 index 0000000..ead45b4 --- /dev/null +++ b/test/org/apache/catalina/connector/TestSendFile.java @@ -0,0 +1,248 @@ +/* + * 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.catalina.connector; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestSendFile extends TomcatBaseTest { + + public static final int ITERATIONS = 10; + public static final int EXPECTED_CONTENT_LENGTH = 100000; + + @Test + public void testSendFile() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + + File[] files = new File[ITERATIONS]; + for (int i = 0; i < ITERATIONS; i++) { + files[i] = generateFile(TEMP_DIR, "-" + i, EXPECTED_CONTENT_LENGTH * (i + 1)); + addDeleteOnTearDown(files[i]); + } + + for (int i = 0; i < ITERATIONS; i++) { + WritingServlet servlet = new WritingServlet(files[i]); + Tomcat.addServlet(root, "servlet" + i, servlet); + root.addServletMappingDecoded("/servlet" + i, "servlet" + i); + } + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + Map> respHeaders = new HashMap<>(); + for (int i = 0; i < ITERATIONS; i++) { + long start = System.currentTimeMillis(); + int rc = getUrl("http://localhost:" + getPort() + "/servlet" + i, bc, null, + respHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + System.out.println("Client received " + bc.getLength() + " bytes in " + + (System.currentTimeMillis() - start) + " ms."); + Assert.assertEquals("Expected [" + EXPECTED_CONTENT_LENGTH * (i + 1L) + + "], was [" + bc.getLength() + "]", + EXPECTED_CONTENT_LENGTH * (i + 1L), bc.getLength()); + + bc.recycle(); + } + } + + public File generateFile(String dir, String suffix, int size) throws IOException { + String name = "testSendFile-" + System.currentTimeMillis() + suffix + ".txt"; + File f = new File(dir, name); + try (FileWriter fw = new FileWriter(f, false); BufferedWriter w = new BufferedWriter(fw)) { + int defSize = 8192; + while (size > 0) { + int bytes = Math.min(size, defSize); + char[] b = new char[bytes]; + Arrays.fill(b, 'X'); + w.write(b); + size = size - bytes; + } + w.flush(); + } + System.out.println( + "Created file:" + f.getAbsolutePath() + " with " + f.length() + " bytes."); + return f; + + } + + + private static class WritingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final File f; + + WritingServlet(File f) { + this.f = f; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("'application/octet-stream"); + resp.setCharacterEncoding("ISO-8859-1"); + resp.setContentLengthLong(f.length()); + if (Boolean.TRUE.equals(req.getAttribute(Globals.SENDFILE_SUPPORTED_ATTR))) { + req.setAttribute(Globals.SENDFILE_FILENAME_ATTR, f.getAbsolutePath()); + req.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0)); + req.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(f.length())); + } else { + byte[] c = new byte[8192]; + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(f))) { + int len = 0; + int written = 0; + long start = System.currentTimeMillis(); + do { + len = in.read(c); + if (len > 0) { + resp.getOutputStream().write(c, 0, len); + written += len; + } + } while (len > 0); + System.out.println("Server Wrote " + written + " bytes in " + + (System.currentTimeMillis() - start) + " ms."); + } + } + } + } + + + @Test + public void testBug60409() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = tomcat.addContext("", TEMP_DIR); + File file = generateFile(TEMP_DIR, "", EXPECTED_CONTENT_LENGTH); + Tomcat.addServlet(ctx, "test", new Bug60409Servlet(file)); + ctx.addServletMappingDecoded("/", "test"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + getUrl("http://localhost:" + getPort() + "/test/?" + Globals.SENDFILE_SUPPORTED_ATTR + + "=true", bc, null); + + CountDownLatch latch = new CountDownLatch(2); + List throwables = new CopyOnWriteArrayList<>(); + new Thread( + new RequestExecutor("http://localhost:" + getPort() + "/test/", latch, throwables)) + .start(); + new Thread( + new RequestExecutor("http://localhost:" + getPort() + "/test/", latch, throwables)) + .start(); + + latch.await(3000, TimeUnit.MILLISECONDS); + + if (throwables.size() > 0) { + Assert.fail("[" + throwables.size() + "] throwables observed"); + } + } + + private static final class Bug60409Servlet extends HttpServlet { + private static final long serialVersionUID = 1L; + private final File file; + + Bug60409Servlet(File file) { + this.file = file; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (Boolean.valueOf(req.getParameter(Globals.SENDFILE_SUPPORTED_ATTR)).booleanValue()) { + resp.setContentType("'application/octet-stream"); + resp.setCharacterEncoding("ISO-8859-1"); + resp.setContentLengthLong(file.length()); + req.setAttribute(Globals.SENDFILE_FILENAME_ATTR, file.getAbsolutePath()); + req.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0)); + req.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(file.length())); + if (!file.delete()) { + throw new ServletException("Failed to delete [" + file + "]"); + } + } else { + byte[] c = new byte[1024]; + Random rd = new Random(); + rd.nextBytes(c); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + resp.getOutputStream().write(c); + } + } + + } + + private static final class RequestExecutor implements Runnable { + private final String url; + private final CountDownLatch latch; + private final List exceptions; + + RequestExecutor(String url, CountDownLatch latch, List exceptions) { + this.url = url; + this.latch = latch; + this.exceptions = exceptions; + } + + @Override + public void run() { + try { + ByteChunk result = new ByteChunk(); + int rc = getUrl(url, result, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(1024, result.getLength()); + } catch (Throwable e) { + e.printStackTrace(); + exceptions.add(e); + } finally { + latch.countDown(); + } + } + + } +} diff --git a/test/org/apache/catalina/connector/TesterRequestPerformance.java b/test/org/apache/catalina/connector/TesterRequestPerformance.java new file mode 100644 index 0000000..42ec455 --- /dev/null +++ b/test/org/apache/catalina/connector/TesterRequestPerformance.java @@ -0,0 +1,47 @@ +/* + * 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.catalina.connector; + +import org.junit.Test; + +import org.apache.tomcat.unittest.TesterRequest; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterRequestPerformance { + + @Test + public void localeParsePerformance() throws Exception { + TesterRequest req = new TesterRequest(); + req.addHeader("accept-encoding", "en-gb,en"); + + long start = System.nanoTime(); + + // Takes about 0.3s on a quad core 2.7Ghz 2013 MacBook + for (int i = 0; i < 10000000; i++) { + req.parseLocales(); + req.localesParsed = false; + req.locales.clear(); + } + + long time = System.nanoTime() - start; + + System.out.println(time); + } +} diff --git a/test/org/apache/catalina/connector/test_content.txt b/test/org/apache/catalina/connector/test_content.txt new file mode 100644 index 0000000..e49c4e8 --- /dev/null +++ b/test/org/apache/catalina/connector/test_content.txt @@ -0,0 +1,19 @@ +# 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. + +# This is a test file for the WebappServiceLoader +# It contains comment lines and blank lines + +test content diff --git a/test/org/apache/catalina/core/TestApplicationContext.java b/test/org/apache/catalina/core/TestApplicationContext.java new file mode 100644 index 0000000..2a32814 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationContext.java @@ -0,0 +1,331 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; + +import jakarta.servlet.Filter; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.descriptor.JspPropertyGroupDescriptor; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.Tomcat.FixContextListener; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestApplicationContext extends TomcatBaseTest { + + @Test + public void testBug53257() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + + "/test/bug53257/index.jsp"); + + String result = res.toString(); + String[] lines = result.split("\n"); + for (String line : lines) { + if (line.startsWith("FAIL")) { + Assert.fail(line); + } + } + } + + + @Test + public void testBug53467() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug53467%5D.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(res.toString().contains("

    OK

    ")); + } + + + @Test(expected = IllegalArgumentException.class) + public void testAddFilterWithFilterNameNull() throws LifecycleException { + getServletContext().addFilter(null, (Filter) null); + } + + + @Test(expected = IllegalArgumentException.class) + public void testAddFilterWithFilterNameEmptyString() throws LifecycleException { + getServletContext().addFilter("", (Filter) null); + } + + + @Test(expected = IllegalArgumentException.class) + public void testAddServletWithServletNameNull() throws LifecycleException { + getServletContext().addServlet(null, (Servlet) null); + } + + + @Test(expected = IllegalArgumentException.class) + public void testAddServletWithServletNameEmptyString() throws LifecycleException { + getServletContext().addServlet("", (Servlet) null); + } + + + @Test + public void testGetJspConfigDescriptor() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, false); + + StandardContext standardContext = + (StandardContext) tomcat.getHost().findChildren()[0]; + + ServletContext servletContext = standardContext.getServletContext(); + + Assert.assertNull(servletContext.getJspConfigDescriptor()); + + tomcat.start(); + + Assert.assertNotNull(servletContext.getJspConfigDescriptor()); + } + + @Test + public void testJspPropertyGroupsAreIsolated() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, false); + + StandardContext standardContext = + (StandardContext) tomcat.getHost().findChildren()[0]; + + ServletContext servletContext = standardContext.getServletContext(); + + Assert.assertNull(servletContext.getJspConfigDescriptor()); + + tomcat.start(); + + JspConfigDescriptor jspConfigDescriptor = + servletContext.getJspConfigDescriptor(); + Collection propertyGroups = + jspConfigDescriptor.getJspPropertyGroups(); + Assert.assertFalse(propertyGroups.isEmpty()); + propertyGroups.clear(); + + jspConfigDescriptor = servletContext.getJspConfigDescriptor(); + propertyGroups = jspConfigDescriptor.getJspPropertyGroups(); + Assert.assertFalse(propertyGroups.isEmpty()); + } + + + private ServletContext getServletContext() throws LifecycleException { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, false); + + StandardContext standardContext = + (StandardContext) tomcat.getHost().findChildren()[0]; + + return standardContext.getServletContext(); + } + + + @Test(expected = IllegalStateException.class) + public void testSetInitParameter() throws Exception { + getTomcatInstance().start(); + getServletContext().setInitParameter("name", "value"); + } + + + /* + * Cross-context requests with parallel deployment + */ + @Test + public void testBug57190() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context foo1 = new StandardContext(); + foo1.setName("/foo##1"); + foo1.setPath("/foo"); + foo1.setWebappVersion("1"); + foo1.addLifecycleListener(new FixContextListener()); + foo1.addLifecycleListener(new SetIdListener("foo1")); + tomcat.getHost().addChild(foo1); + + Context foo2 = new StandardContext(); + foo2.setName("/foo##2"); + foo2.setPath("/foo"); + foo2.setWebappVersion("2"); + foo2.addLifecycleListener(new FixContextListener()); + foo2.addLifecycleListener(new SetIdListener("foo2")); + tomcat.getHost().addChild(foo2); + + Context bar = tomcat.addContext("/bar", null); + bar.addLifecycleListener(new SetIdListener("bar")); + + Context ctx = getProgrammaticRootContext(); + ctx.addLifecycleListener(new SetIdListener("ROOT")); + ctx.setCrossContext(true); + + Tomcat.addServlet(ctx, "Bug57190Servlet", new Bug57190Servlet()); + ctx.addServletMappingDecoded("/", "Bug57190Servlet"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/"); + String body = res.toString(); + + Assert.assertTrue(body, body.contains("01-bar")); + Assert.assertTrue(body, body.contains("02-foo2")); + Assert.assertTrue(body, body.contains("03-foo1")); + Assert.assertTrue(body, body.contains("04-foo2")); + Assert.assertTrue(body, body.contains("05-foo2")); + Assert.assertTrue(body, body.contains("06-ROOT")); + Assert.assertTrue(body, body.contains("07-ROOT")); + Assert.assertTrue(body, body.contains("08-foo2")); + Assert.assertTrue(body, body.contains("09-ROOT")); + } + + + private static class Bug57190Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + ServletContext sc = req.getServletContext(); + pw.println("01-" + sc.getContext("/bar").getInitParameter("id")); + pw.println("02-" + sc.getContext("/foo").getInitParameter("id")); + pw.println("03-" + sc.getContext("/foo##1").getInitParameter("id")); + pw.println("04-" + sc.getContext("/foo##2").getInitParameter("id")); + pw.println("05-" + sc.getContext("/foo##3").getInitParameter("id")); + pw.println("06-" + sc.getContext("/unknown").getInitParameter("id")); + pw.println("07-" + sc.getContext("/").getInitParameter("id")); + pw.println("08-" + sc.getContext("/foo/bar").getInitParameter("id")); + pw.println("09-" + sc.getContext("/football").getInitParameter("id")); + } + } + + + private static class SetIdListener implements LifecycleListener { + + private final String id; + + SetIdListener(String id) { + this.id = id; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (Lifecycle.CONFIGURE_START_EVENT.equals(event.getType())) { + ((Context) event.getSource()).getServletContext().setInitParameter("id", id); + } + } + } + + + /* + * The expectation is that you can set a context attribute on + * ServletContextB from ServletContextA and then access that attribute via + * a cross-context dispatch to ServletContextB. + */ + @Test + public void testCrossContextSetAttribute() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx2 = tomcat.addContext("/second", null); + GetAttributeServlet getAttributeServlet = new GetAttributeServlet(); + Tomcat.addServlet(ctx2, "getAttributeServlet", getAttributeServlet); + ctx2.addServletMappingDecoded("/test", "getAttributeServlet"); + + // No file system docBase required + Context ctx1 = tomcat.addContext("/first", null); + ctx1.setCrossContext(true); + SetAttributeServlet setAttributeServlet = new SetAttributeServlet("/test", "/second"); + Tomcat.addServlet(ctx1, "setAttributeServlet", setAttributeServlet); + ctx1.addServletMappingDecoded("/test", "setAttributeServlet"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/first/test", bc, null); + + Assert.assertEquals(200, rc); + Assert.assertEquals("01-PASS", bc.toString()); + } + + + private static class SetAttributeServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String ATTRIBUTE_NAME = "setAttributeTest"; + private static final String ATTRIBUTE_VALUE = "abcde"; + + private final String targetContextPath; + private final String targetPath; + + SetAttributeServlet(String targetPath, String targetContextPath) { + this.targetPath = targetPath; + this.targetContextPath = targetContextPath; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + ServletContext sc; + if (targetContextPath == null) { + sc = req.getServletContext(); + } else { + sc = req.getServletContext().getContext(targetContextPath); + } + sc.setAttribute(ATTRIBUTE_NAME, ATTRIBUTE_VALUE); + sc.getRequestDispatcher(targetPath).forward(req, resp); + } + } + + + private static class GetAttributeServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + String value = (String) req.getServletContext().getAttribute( + SetAttributeServlet.ATTRIBUTE_NAME); + if (SetAttributeServlet.ATTRIBUTE_VALUE.equals(value)) { + pw.print("01-PASS"); + } else { + pw.print("01-FAIL"); + } + } + } +} diff --git a/test/org/apache/catalina/core/TestApplicationContextFacadeSecurityManager.java b/test/org/apache/catalina/core/TestApplicationContextFacadeSecurityManager.java new file mode 100644 index 0000000..fc5a5d5 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationContextFacadeSecurityManager.java @@ -0,0 +1,148 @@ +/* + * 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.catalina.core; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.security.SecurityUtil; +import org.apache.tomcat.util.security.SecurityManagerBaseTest; +import org.easymock.EasyMock; +import org.easymock.IExpectationSetters; +import org.easymock.internal.LastControl; + +@RunWith(Parameterized.class) +public final class TestApplicationContextFacadeSecurityManager extends SecurityManagerBaseTest { + + /** + * @return {@link Collection} of non-static, non-object, public {@link + * Method}s in {@link ApplicationContextFacade} to be run with the the Java + * 2 {@link SecurityManager} been enabled. + */ + @Parameterized.Parameters(name = "{index}: method={0}") + public static Collection publicApplicationContextFacadeMethods() { + return Arrays.stream(ApplicationContextFacade.class.getMethods()) + .filter(method -> !Modifier.isStatic(method.getModifiers())) + .filter(method -> { + try { + Object.class.getMethod(method.getName(), method.getParameterTypes()); + return false; + } catch (final NoSuchMethodException e) { + return true; + } + }) + .collect(Collectors.toList()); + } + + + private static Object[] getDefaultParams(final Method method) { + final int paramsCount = method.getParameterCount(); + final Object[] params = new Object[paramsCount]; + final Class[] paramTypes = method.getParameterTypes(); + for (int i = 0; i < params.length; i++) { + params[i] = getDefaultValue(paramTypes[i]); + } + return params; + } + + + @SuppressWarnings("unchecked") + private static T getDefaultValue(final Class clazz) { + return !isVoid(clazz) ? (T) Array.get(Array.newInstance(clazz, 1), 0) : null; + } + + + private static boolean isVoid(Class clazz) { + return void.class.equals(clazz) || Void.class.equals(clazz); + } + + + @Parameter(0) + public Method methodToTest; + + + /** + * Test for + * Bug + * 64735 which confirms that {@link ApplicationContextFacade} behaves + * correctly when the Java 2 {@link SecurityManager} has been enabled. + * + * @throws NoSuchMethodException Should never happen + * @throws IllegalAccessException Should never happen + * @throws InvocationTargetException Should never happen + */ + @Test + public void testBug64735() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + Assert.assertTrue(SecurityUtil.isPackageProtectionEnabled()); + + // Mock the ApplicationContext that we provide to the ApplicationContextFacade. + final ApplicationContext mockAppContext = EasyMock.createMock(ApplicationContext.class); + final Method expectedAppContextMethod = + ApplicationContext.class.getMethod( + methodToTest.getName(), + methodToTest.getParameterTypes()); + + // Expect that only the provided method which is being tested will be called exactly once. + final IExpectationSetters expectationSetters; + if (isVoid(expectedAppContextMethod.getReturnType())) { + expectedAppContextMethod.invoke(mockAppContext, getDefaultParams(methodToTest)); + expectationSetters = EasyMock.expectLastCall(); + } else { + expectationSetters = + EasyMock.expect(expectedAppContextMethod.invoke( + mockAppContext, getDefaultParams(methodToTest))); + } + expectationSetters + .andAnswer(() -> { + Assert.assertEquals( + expectedAppContextMethod, + LastControl.getCurrentInvocation().getMethod()); + return getDefaultValue(expectedAppContextMethod.getReturnType()); + }).once(); + EasyMock.replay(mockAppContext); + EasyMock.verifyUnexpectedCalls(mockAppContext); + + // Invoke the method on ApplicationContextFacade. Fail if any unexpected exceptions are + // thrown. + try { + methodToTest.invoke( + new ApplicationContextFacade(mockAppContext), + getDefaultParams(methodToTest)); + } catch (final IllegalAccessException | InvocationTargetException e) { + throw new AssertionError( + "Failed to call " + + methodToTest + + " with SecurityManager enabled.", + e); + } + + // Verify that the method called through to the wrapped ApplicationContext correctly. + EasyMock.verifyRecording(); + } +} diff --git a/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java b/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java new file mode 100644 index 0000000..ff19ec1 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcher.java @@ -0,0 +1,524 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.util.URLEncoder; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.UDecoder; + +@RunWith(value = Parameterized.class) +public class TestApplicationContextGetRequestDispatcher extends TomcatBaseTest { + + private final boolean useAsync; + + public TestApplicationContextGetRequestDispatcher(boolean useAsync) { + this.useAsync = useAsync; + } + + @Parameters(name = "{index}: useAsync[{0}]") + public static Collection data() { + return Arrays.asList(new Object[][]{ + {Boolean.TRUE}, + {Boolean.FALSE} + }); + } + + @Test + public void testGetRequestDispatcherNullPath01() throws Exception { + doTestGetRequestDispatcher(true, "/start", null, null, "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherNullPath02() throws Exception { + doTestGetRequestDispatcher(false, "/start", null, null, "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherOutsideContextRoot01() throws Exception { + doTestGetRequestDispatcher( + true, "/start", null, "../outside", "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherOutsideContextRoot02() throws Exception { + doTestGetRequestDispatcher( + false, "/start", null, "../outside", "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherEncodedTraversal() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", null, "%2E%2E/target", "/target", DispatcherServlet.NULL); + } + + + @Test + public void testGetRequestDispatcherTraversal01() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", null, "../target", "/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcherTraversal02() throws Exception { + doTestGetRequestDispatcher( + false, "/prefix/start", null, "../target", "/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcherTraversal03() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", null, "../target?a=b", "/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcherTraversal04() throws Exception { + doTestGetRequestDispatcher( + false, "/prefix/start", null, "../target?a=b", "/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcherTraversal05() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", "a=b", "../target", "/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcherTraversal06() throws Exception { + doTestGetRequestDispatcher( + false, "/prefix/start", "a=b", "../target", "/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher01() throws Exception { + doTestGetRequestDispatcher( + true, "/prefix/start", null, "target", "/prefix/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher02() throws Exception { + doTestGetRequestDispatcher( + false, "/prefix/start", null, "target", "/prefix/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher03() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "target?a=b", "/prefix/target", + TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher04() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "target?a=b", "/prefix/target", + TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher05() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "target", "/prefix/target", + TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher06() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "target", "/prefix/target", + TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher11() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", null, "target", + "/aa%3Fbb%3Dcc/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher12() throws Exception { + // Expected to fail because when the RD processes this as unencoded it + // sees /aa?bb=cc/target which it thinks is a query string. This is why + // Tomcat encodes by default. + doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", null, "target", + "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher13() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", null, "target?a=b", + "/aa%3Fbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher14() throws Exception { + // Expected to fail because when the RD processes this as unencoded it + // sees /aa?bb=cc/target which it thinks is a query string. This is why + // Tomcat encodes by default. + doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", null, "target?a=b", + "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher15() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Fbb%3Dcc/start", "a=b", "target", + "/aa%3Fbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher16() throws Exception { + // Expected to fail because when the RD processes this as unencoded it + // sees /aa?bb=cc/target which it thinks is a query string. This is why + // Tomcat encodes by default. + doTestGetRequestDispatcher(false, "/aa%3Fbb%3Dcc/start", "a=b", "target", + "/aa%3Fbb%3Dcc/target", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher21() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", null, "target", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher22() throws Exception { + doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", null, "target", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher23() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", null, "target?a=b", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher24() throws Exception { + doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", null, "target?a=b", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher25() throws Exception { + doTestGetRequestDispatcher(true, "/aa%3Dbb%3Dcc/start", "a=b", "target", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher26() throws Exception { + doTestGetRequestDispatcher(false, "/aa%3Dbb%3Dcc/start", "a=b", "target", + "/aa%3Dbb%3Dcc/target", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher31() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc", + "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher32() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc", + "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher33() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b", + "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher34() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b", + "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher35() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "aa%3Fbb%3Dcc", + "/prefix/aa%3Fbb%3Dcc", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher36() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "aa%3Fbb%3Dcc", + "/prefix/aa%3Fbb%3Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher41() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc", + "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher42() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc", + "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher43() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b", + "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher44() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa%3Fbb%3Dcc?a=b", + "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher45() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", "a=b", "aa%3Fbb%3Dcc", + "/prefix/aa%253Fbb%253Dcc", Default404Servlet.DEFAULT_404); + } + + + @Test + public void testGetRequestDispatcher46() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", "a=b", "aa%3Fbb%3Dcc", + "/prefix/aa%253Fbb%253Dcc", TargetServlet.OK + "a=b"); + } + + + @Test + public void testGetRequestDispatcher47() throws Exception { + doTestGetRequestDispatcher(true, "/prefix/start", null, "aa+bb", + "/prefix/aa+bb", TargetServlet.OK); + } + + + @Test + public void testGetRequestDispatcher48() throws Exception { + doTestGetRequestDispatcher(false, "/prefix/start", null, "aa+bb", + "/prefix/aa+bb", TargetServlet.OK); + } + + + private void doTestGetRequestDispatcher(boolean useEncodedDispatchPaths, String startPath, + String startQueryString, String dispatchPath, String targetPath, String expectedBody) + throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("/test\u6771\u4eac", null); + ctx.setDispatchersUseEncodedPaths(useEncodedDispatchPaths); + + // Add a default servlet to return 404 for not found resources + Tomcat.addServlet(ctx, "Default", new Default404Servlet()); + ctx.addServletMappingDecoded("/", "Default"); + + // Add a target servlet to dispatch to + Tomcat.addServlet(ctx, "target", new TargetServlet()); + ctx.addServletMappingDecoded( + UDecoder.URLDecode(targetPath, StandardCharsets.UTF_8), "target"); + + if (useAsync) { + Wrapper w = Tomcat.addServlet( + ctx, "rd", new AsyncDispatcherServlet(dispatchPath, useEncodedDispatchPaths)); + w.setAsyncSupported(true); + } else { + Tomcat.addServlet(ctx, "rd", new DispatcherServlet(dispatchPath)); + } + ctx.addServletMappingDecoded(UDecoder.URLDecode(startPath, StandardCharsets.UTF_8), "rd"); + + tomcat.start(); + + StringBuilder url = new StringBuilder("http://localhost:"); + url.append(getPort()); + url.append("/test%E6%9D%B1%E4%BA%AC"); + url.append(startPath); + if (startQueryString != null) { + url.append('?'); + url.append(startQueryString); + } + + ByteChunk bc = getUrl(url.toString()); + String body = bc.toString(); + + Assert.assertEquals(expectedBody, body); + } + + + static class Default404Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String DEFAULT_404 = "DEFAULT-404"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().print(DEFAULT_404); + resp.setStatus(404); + } + } + + + private static class DispatcherServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String NULL = "RD-NULL"; + + private final String dispatchPath; + + DispatcherServlet(String dispatchPath) { + this.dispatchPath = dispatchPath; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + RequestDispatcher rd = req.getRequestDispatcher(dispatchPath); + if (rd == null) { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().print(NULL); + } else { + rd.forward(req, resp); + } + } + } + + + private static class TargetServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String OK = "OK"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + String contextPath = req.getContextPath(); + if ("/test%E6%9D%B1%E4%BA%AC".equals(contextPath)) { + resp.getWriter().print(OK); + } else { + resp.getWriter().print("FAIL - ContextPath"); + } + String qs = req.getQueryString(); + if (qs != null) { + resp.getWriter().print(qs); + } + } + } + + + private static class AsyncDispatcherServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String NULL = "RD-NULL"; + + private final String dispatchPath; + private final boolean encodePath; + + AsyncDispatcherServlet(String dispatchPath, boolean encodePath) { + this.dispatchPath = dispatchPath; + this.encodePath = encodePath; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + AsyncContext ac = req.startAsync(); + // Quick and dirty. Sufficient for this test but ignores lots of + // edge cases. + String target = null; + if (dispatchPath != null) { + target = req.getServletPath(); + int lastSlash = target.lastIndexOf('/'); + target = target.substring(0, lastSlash + 1); + if (encodePath) { + target = URLEncoder.DEFAULT.encode(target, StandardCharsets.UTF_8); + } + target += dispatchPath; + } + try { + ac.dispatch(target); + } catch (UnsupportedOperationException uoe) { + ac.complete(); + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().print(NULL); + } + } + } +} diff --git a/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherB.java b/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherB.java new file mode 100644 index 0000000..401fd89 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherB.java @@ -0,0 +1,585 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.MappingMatch; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +@RunWith(value = Parameterized.class) +public class TestApplicationContextGetRequestDispatcherB extends TomcatBaseTest { + + @Parameters(name = "{index}: startMapping[{0}], startUri[{1}], dispatcherType[{2}], " + + "targetMapping[{3}], targetUri[{4}], useEncodedDispatchPaths[{5}], " + + "expectedRequestURI[{6}], expectedContextPath[{7}], expectedServletPath[{8}], " + + "expectedPathInfo[{9}], expectedQueryString[{10}], expectedMappingMatch[{11}, " + + "expectedMappingPattern[{12}], expectedMappingMatchValue[{13}], " + + "expectedMappingServletName[{14}], " + + "expectedDispatcherRequestURI[{15}], expectedDispatcherContextPath[{16}], " + + "expectedDispatcherServletPath[{17}], expectedDispatcherPathInfo[{18}], " + + "expectedDispatcherQueryString[{19}], expectedDispatcherMappingMatch[{20}]," + + "expectedDispatcherMappingPattern[{21}], expectedDispatcherMappingMatchValue[{22}]," + + "expectedDispatcherMappingServletName[{23}]," + + "expectedBody") + public static Collection data() { + return Arrays.asList(new Object[][]{ + // Simple dispatch for each type + { "/start", "/start", DispatcherType.INCLUDE, "/target", "/target", Boolean.TRUE, + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "OK"}, + { "/start", "/start", DispatcherType.FORWARD, "/target", "/target", Boolean.TRUE, + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start", DispatcherType.ASYNC, "/target", "/target", Boolean.TRUE, + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Simple dispatch with query strings + { "/start", "/start?abcde=fghij", DispatcherType.INCLUDE, "/target", "/target?zyxwv=utsrq", Boolean.TRUE, + "/test/start", "/test", "/start", null, "abcde=fghij", + MappingMatch.EXACT, "/start", "start", "rd", + "/test/target", "/test", "/target", null, "zyxwv=utsrq", + MappingMatch.EXACT, "/target", "target", "target", + "OK"}, + { "/start", "/start?abcde=fghij", DispatcherType.FORWARD, "/target", "/target?zyxwv=utsrq", Boolean.TRUE, + "/test/target", "/test", "/target", null, "zyxwv=utsrq", + MappingMatch.EXACT, "/target", "target", "target", + "/test/start", "/test", "/start", null, "abcde=fghij", + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start?abcde=fghij", DispatcherType.ASYNC, "/target", "/target?zyxwv=utsrq", Boolean.TRUE, + "/test/target", "/test", "/target", null, "zyxwv=utsrq", + MappingMatch.EXACT, "/target", "target", "target", + "/test/start", "/test", "/start", null, "abcde=fghij", + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Simple dispatch with trailing path parameters at start + { "/start", "/start;abcde=fghij", DispatcherType.INCLUDE, "/target", "/target", Boolean.TRUE, + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "OK"}, + { "/start", "/start;abcde=fghij", DispatcherType.FORWARD, "/target", "/target", Boolean.TRUE, + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start;abcde=fghij", DispatcherType.ASYNC, "/target", "/target", Boolean.TRUE, + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Simple dispatch with path parameters at start + { "/start", ";abcde=fghij/start", DispatcherType.INCLUDE, "/target", "/target", Boolean.TRUE, + "/test;abcde=fghij/start", "/test;abcde=fghij", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "OK"}, + { "/start", ";abcde=fghij/start", DispatcherType.FORWARD, "/target", "/target", Boolean.TRUE, + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test;abcde=fghij/start", "/test;abcde=fghij", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", ";abcde=fghij/start", DispatcherType.ASYNC, "/target", "/target", Boolean.TRUE, + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test;abcde=fghij/start", "/test;abcde=fghij", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Simple dispatch with path parameters on dispatch + { "/start", "/start", DispatcherType.INCLUDE, "/target", "/target;abcde=fghij", Boolean.TRUE, + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/target;abcde=fghij", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "OK"}, + { "/start", "/start", DispatcherType.FORWARD, "/target", "/target;abcde=fghij", Boolean.TRUE, + "/test/target;abcde=fghij", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start", DispatcherType.ASYNC, "/target", "/target;abcde=fghij", Boolean.TRUE, + "/test/target;abcde=fghij", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Simple dispatch with multiple path parameters on start and dispatch + { "/start", "/start;abcde=fghij", DispatcherType.INCLUDE, "/target", ";klmno=pqrst/target;uvwxy=z0123", Boolean.TRUE, + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/;klmno=pqrst/target;uvwxy=z0123", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "OK"}, + { "/start", "/start;abcde=fghij", DispatcherType.FORWARD, "/target", ";klmno=pqrst/target;uvwxy=z0123", Boolean.TRUE, + "/test/;klmno=pqrst/target;uvwxy=z0123", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start;abcde=fghij", DispatcherType.ASYNC, "/target", ";klmno=pqrst/target;uvwxy=z0123", Boolean.TRUE, + "/test/;klmno=pqrst/target;uvwxy=z0123", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "ASYNC-IAE"}, + // Simple dispatch with directory traversal + { "/start/*", "/start/foo", DispatcherType.INCLUDE, "/target", "../target", Boolean.TRUE, + "/test/start/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "/test/start/../target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "OK"}, + { "/start/*", "/start/foo", DispatcherType.FORWARD, "/target", "../target", Boolean.TRUE, + "/test/start/../target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "OK"}, + { "/start/*", "/start/foo", DispatcherType.ASYNC, "/target", "../target", Boolean.TRUE, + "/test/start/../target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "ASYNC-IAE"}, + // Simple dispatch with directory traversal and path parameters + // Note comments in Request.getRequestDispatcher(String) that + // explain why the path parameter abcde=fghij is not present on the + // dispatched requestURI + { "/start/*", "/start;abcde=fghij/foo", DispatcherType.INCLUDE, "/target", "../target;klmno=pqrst", Boolean.TRUE, + "/test/start;abcde=fghij/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "/test/start/../target;klmno=pqrst", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "OK"}, + { "/start/*", "/start;abcde=fghij/foo", DispatcherType.FORWARD, "/target", "../target;klmno=pqrst", Boolean.TRUE, + "/test/start/../target;klmno=pqrst", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start;abcde=fghij/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "OK"}, + { "/start/*", "/start;abcde=fghij/foo", DispatcherType.ASYNC, "/target", "../target;klmno=pqrst", Boolean.TRUE, + "/test/start;abcde=fghij/../target;klmno=pqrst", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start;abcde=fghij/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "ASYNC-IAE"}, + // Simple dispatch with invalid directory traversal + { "/start/*", "/start/foo", DispatcherType.INCLUDE, "/target", "../../target", Boolean.TRUE, + "/test/start/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "/test/start/../target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "RD-NULL"}, + { "/start/*", "/start/foo", DispatcherType.FORWARD, "/target", "../../target", Boolean.TRUE, + "/test/start/../target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "RD-NULL"}, + { "/start/*", "/start/foo", DispatcherType.ASYNC, "/target", "../../target", Boolean.TRUE, + "/test/start/../target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start/foo", "/test", "/start", "/foo", null, + MappingMatch.PATH, "/start/*", "foo", "rd", + "ASYNC-IAE"}, + // Simple dispatch with invalid target + { "/start", "/start", DispatcherType.INCLUDE, "/target", "/does-not-exist", Boolean.TRUE, + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "RD-NULL"}, + { "/start", "/start", DispatcherType.FORWARD, "/target", "/does-not-exist", Boolean.TRUE, + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "RD-NULL"}, + { "/start", "/start", DispatcherType.ASYNC, "/target", "/does-not-exist", Boolean.TRUE, + "/test/target", "/test", "/target", null, null, + MappingMatch.EXACT, "/target", "target", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "ASYNC-RD-NULL"}, + // Welcome files + { "/start", "/start", DispatcherType.INCLUDE, "*.html", "/", Boolean.TRUE, + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "OK"}, + { "/start", "/start", DispatcherType.FORWARD, "*.html", "/", Boolean.TRUE, + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start", DispatcherType.ASYNC, "*.html", "/", Boolean.TRUE, + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Welcome files with query strings + { "/start", "/start?abcde=fghij", DispatcherType.INCLUDE, "*.html", "/?zyxwv=utsrq", Boolean.TRUE, + "/test/start", "/test", "/start", null, "abcde=fghij", + MappingMatch.EXACT, "/start", "start", "rd", + "/test/", "/test", "/index.html", null, "zyxwv=utsrq", + MappingMatch.EXTENSION, "*.html", "index", "target", + "OK"}, + { "/start", "/start?abcde=fghij", DispatcherType.FORWARD, "*.html", "/?zyxwv=utsrq", Boolean.TRUE, + "/test/", "/test", "/index.html", null, "zyxwv=utsrq", + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test/start", "/test", "/start", null, "abcde=fghij", + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start?abcde=fghij", DispatcherType.ASYNC, "*.html", "/?zyxwv=utsrq", Boolean.TRUE, + "/test/", "/test", "/index.html", null, "zyxwv=utsrq", + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test/start", "/test", "/start", null, "abcde=fghij", + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Welcome files with trailing path parameters at start + { "/start", "/start;abcde=fghij", DispatcherType.INCLUDE, "*.html", "/", Boolean.TRUE, + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "OK"}, + { "/start", "/start;abcde=fghij", DispatcherType.FORWARD, "*.html", "/", Boolean.TRUE, + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start;abcde=fghij", DispatcherType.ASYNC, "*.html", "/", Boolean.TRUE, + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test/start;abcde=fghij", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Welcome files with path parameters at start + { "/start", ";abcde=fghij/start", DispatcherType.INCLUDE, "*.html", "/", Boolean.TRUE, + "/test;abcde=fghij/start", "/test;abcde=fghij", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "OK"}, + { "/start", ";abcde=fghij/start", DispatcherType.FORWARD, "*.html", "/", Boolean.TRUE, + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test;abcde=fghij/start", "/test;abcde=fghij", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", ";abcde=fghij/start", DispatcherType.ASYNC, "*.html", "/", Boolean.TRUE, + "/test/", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test;abcde=fghij/start", "/test;abcde=fghij", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + // Welcome files with trailing path parameters on dispatch + { "/start", "/start", DispatcherType.INCLUDE, "*.html", "/;abcde=fghij", Boolean.TRUE, + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "/test/;abcde=fghij", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "OK"}, + { "/start", "/start", DispatcherType.FORWARD, "*.html", "/;abcde=fghij", Boolean.TRUE, + "/test/;abcde=fghij", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + { "/start", "/start", DispatcherType.ASYNC, "*.html", "/;abcde=fghij", Boolean.TRUE, + "/test/;abcde=fghij", "/test", "/index.html", null, null, + MappingMatch.EXTENSION, "*.html", "index", "target", + "/test/start", "/test", "/start", null, null, + MappingMatch.EXACT, "/start", "start", "rd", + "OK"}, + }); + } + + // Inputs + private final String startMapping; + private final String startUri; + private final DispatcherType dispatcherType; + private final String targetMapping; + private final String targetUri; + private final boolean useEncodedDispatchPaths; + // Outputs + private final String expectedRequestURI; + private final String expectedContextPath; + private final String expectedServletPath; + private final String expectedPathInfo; + private final String expectedQueryString; + private final MappingMatch expectedMappingMatch; + private final String expectedMappingPattern; + private final String expectedMappingMatchValue; + private final String expectedMappingServletName; + private final String expectedDispatcherRequestURI; + private final String expectedDispatcherContextPath; + private final String expectedDispatcherServletPath; + private final String expectedDispatcherPathInfo; + private final String expectedDispatcherQueryString; + private final MappingMatch expectedDispatcherMappingMatch; + private final String expectedDispatcherMappingPattern; + private final String expectedDispatcherMappingMatchValue; + private final String expectedDispatcherMappingServletName; + private final String expectedBody; + + + public TestApplicationContextGetRequestDispatcherB(String startMapping, String startUri, + DispatcherType dispatcherType, String targetMapping, String targetUri, + boolean useEncodedDispatchPaths, + String expectedRequestURI, String expectedContextPath, String expectedServletPath, + String expectedPathInfo, String expectedQueryString, MappingMatch expectedMappingMatch, + String expectedMappingPattern, String expectedMappingMatchValue, + String expectedMappingServletName, + String expectedDispatcherRequestURI, String expectedDispatcherContextPath, + String expectedDispatcherServletPath, String expectedDispatcherPathInfo, + String expectedDispatcherQueryString, MappingMatch expectedDispatcherMappingMatch, + String expectedDispatcherMappingPattern, String expectedDispatcherMappingMatchValue, + String expectedDispatcherMappingServletName, + String expectedBody) { + this.startMapping = startMapping; + this.startUri = startUri; + this.dispatcherType = dispatcherType; + this.targetMapping = targetMapping; + this.targetUri = targetUri; + this.useEncodedDispatchPaths = useEncodedDispatchPaths; + this.expectedRequestURI = expectedRequestURI; + this.expectedContextPath = expectedContextPath; + this.expectedServletPath = expectedServletPath; + this.expectedPathInfo = expectedPathInfo; + this.expectedQueryString = expectedQueryString; + this.expectedMappingMatch = expectedMappingMatch; + this.expectedMappingPattern = expectedMappingPattern; + this.expectedMappingMatchValue = expectedMappingMatchValue; + this.expectedMappingServletName = expectedMappingServletName; + this.expectedDispatcherRequestURI = expectedDispatcherRequestURI; + this.expectedDispatcherContextPath = expectedDispatcherContextPath; + this.expectedDispatcherServletPath = expectedDispatcherServletPath; + this.expectedDispatcherPathInfo = expectedDispatcherPathInfo; + this.expectedDispatcherQueryString = expectedDispatcherQueryString; + this.expectedDispatcherMappingMatch = expectedDispatcherMappingMatch; + this.expectedDispatcherMappingPattern = expectedDispatcherMappingPattern; + this.expectedDispatcherMappingMatchValue = expectedDispatcherMappingMatchValue; + this.expectedDispatcherMappingServletName = expectedDispatcherMappingServletName; + this.expectedBody = expectedBody; + } + + + @Test + public void doTest() throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("/test", null); + ctx.setDispatchersUseEncodedPaths(useEncodedDispatchPaths); + ctx.addWelcomeFile("index.html"); + + // Add a target servlet to dispatch to + Tomcat.addServlet(ctx, "target", new Target()); + ctx.addServletMappingDecoded(targetMapping, "target"); + + Wrapper w = Tomcat.addServlet(ctx, "rd", new Dispatch()); + w.setAsyncSupported(true); + ctx.addServletMappingDecoded(startMapping, "rd"); + + tomcat.start(); + + StringBuilder url = new StringBuilder("http://localhost:"); + url.append(getPort()); + url.append("/test"); + url.append(startUri); + + ByteChunk bc = getUrl(url.toString()); + String body = bc.toString(); + + Assert.assertEquals(expectedBody, body); + } + + + private class Dispatch extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (dispatcherType == DispatcherType.INCLUDE) { + RequestDispatcher rd = req.getRequestDispatcher(targetUri); + if (rd == null) { + writeResponse(resp, "RD-NULL"); + } else { + rd.include(req, resp); + } + } else if (dispatcherType == DispatcherType.FORWARD) { + RequestDispatcher rd = req.getRequestDispatcher(targetUri); + if (rd == null) { + writeResponse(resp, "RD-NULL"); + } else { + rd.forward(req, resp); + } + } else if (dispatcherType == DispatcherType.ASYNC) { + AsyncContext ac = req.startAsync(); + try { + ac.dispatch(targetUri); + } catch (IllegalArgumentException iae) { + // targetUri is invalid? + if (!targetUri.startsWith("/")) { + // That'll do it. + ac.complete(); + writeResponse(resp, "ASYNC-IAE"); + } else { + // Not expected. Rethrow. + throw iae; + } + } catch (UnsupportedOperationException uoe) { + // While a custom context implementation could cause this, + // if this occurs during this unit test the cause will be an + // invalid (unmapped) target path which returned a null + // dispatcher + ac.complete(); + writeResponse(resp, "ASYNC-RD-NULL"); + } + } else { + // Unexpected dispatch type for this test + throw new ServletException("Unknown dispatch type: " + dispatcherType); + } + } + + + private void writeResponse(HttpServletResponse resp, String message) throws IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + pw.print(message); + } + } + + + private class Target extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + Assert.assertEquals(expectedRequestURI, req.getRequestURI()); + Assert.assertEquals(expectedContextPath, req.getContextPath()); + Assert.assertEquals(expectedServletPath, req.getServletPath()); + Assert.assertEquals(expectedPathInfo, req.getPathInfo()); + Assert.assertEquals(expectedQueryString, req.getQueryString()); + HttpServletMapping mapping = req.getHttpServletMapping(); + Assert.assertEquals(expectedMappingMatch, mapping.getMappingMatch()); + Assert.assertEquals(expectedMappingPattern, mapping.getPattern()); + Assert.assertEquals(expectedMappingMatchValue, mapping.getMatchValue()); + Assert.assertEquals(expectedMappingServletName, mapping.getServletName()); + + for (DispatcherType type : DispatcherType.values()) { + if (type == dispatcherType) { + String name = dispatcherType.name().toLowerCase(Locale.ENGLISH); + Assert.assertEquals(expectedDispatcherRequestURI, + req.getAttribute("jakarta.servlet." + name + ".request_uri")); + Assert.assertEquals(expectedDispatcherContextPath, + req.getAttribute("jakarta.servlet." + name + ".context_path")); + Assert.assertEquals(expectedDispatcherServletPath, + req.getAttribute("jakarta.servlet." + name + ".servlet_path")); + Assert.assertEquals(expectedDispatcherPathInfo, + req.getAttribute("jakarta.servlet." + name + ".path_info")); + Assert.assertEquals(expectedDispatcherQueryString, + req.getAttribute("jakarta.servlet." + name + ".query_string")); + HttpServletMapping dispatcherMapping = + (HttpServletMapping) req.getAttribute( + "jakarta.servlet." + name + ".mapping"); + Assert.assertNotNull(dispatcherMapping); + Assert.assertEquals(expectedDispatcherMappingMatch, + dispatcherMapping.getMappingMatch()); + Assert.assertEquals(expectedDispatcherMappingPattern, + dispatcherMapping.getPattern()); + Assert.assertEquals(expectedDispatcherMappingMatchValue, + dispatcherMapping.getMatchValue()); + Assert.assertEquals(expectedDispatcherMappingServletName, + dispatcherMapping.getServletName()); + } else if (type == DispatcherType.ERROR || type == DispatcherType.REQUEST) { + // Skip - not tested + } else { + assertAllNull(req, type.name().toLowerCase(Locale.ENGLISH)); + } + } + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + pw.print("OK"); + } + } + + + private void assertAllNull(HttpServletRequest req, String type) { + Assert.assertNull(req.getAttribute("jakarta.servlet." + type + ".request_uri")); + Assert.assertNull(req.getAttribute("jakarta.servlet." + type + ".context_path")); + Assert.assertNull(req.getAttribute("jakarta.servlet." + type + ".servlet_path")); + Assert.assertNull(req.getAttribute("jakarta.servlet." + type + ".path_info")); + Assert.assertNull(req.getAttribute("jakarta.servlet." + type + ".query_string")); + Assert.assertNull(req.getAttribute("jakarta.servlet." + type + ".mapping")); + } +} diff --git a/test/org/apache/catalina/core/TestApplicationContextStripPathParams.java b/test/org/apache/catalina/core/TestApplicationContextStripPathParams.java new file mode 100644 index 0000000..2faea49 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationContextStripPathParams.java @@ -0,0 +1,66 @@ +/* + * 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.catalina.core; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.startup.TomcatBaseTest; + +@RunWith(value = Parameterized.class) +public class TestApplicationContextStripPathParams extends TomcatBaseTest { + + private final String input; + private final String expectedOutput; + + public TestApplicationContextStripPathParams(String input, String expectedOutput) { + this.input = input; + this.expectedOutput = expectedOutput; + } + + @Parameters(name = "{index}: input[{0}]") + public static Collection data() { + return Arrays.asList(new Object[][]{ + { "/foo", "/foo"}, + { "/foo/", "/foo/"}, + { "/foo/bar", "/foo/bar"}, + { "/foo;", "/foo"}, + { "/foo;/", "/foo/"}, + { "/foo;/bar", "/foo/bar"}, + { "/foo;a=1", "/foo"}, + { "/foo;a=1/", "/foo/"}, + { "/foo;a=1/bar", "/foo/bar"}, + // Arguably not valid but does the right thing anyway + { ";/foo", "/foo"}, + { ";a=1/foo", "/foo"}, + { ";/foo/bar", "/foo/bar"}, + { ";/foo;a=1/bar", "/foo/bar"}, + }); + } + + @Test + public void testStringPathParams() { + String output = ApplicationContext.stripPathParams(input); + Assert.assertEquals(expectedOutput, output); + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/core/TestApplicationFilterConfig.java b/test/org/apache/catalina/core/TestApplicationFilterConfig.java new file mode 100644 index 0000000..18d3191 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationFilterConfig.java @@ -0,0 +1,68 @@ +/* + * 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.catalina.core; + +import java.util.Set; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.filters.AddDefaultCharsetFilter; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.modeler.Registry; + +public class TestApplicationFilterConfig extends TomcatBaseTest { + + @Test + public void testBug54170() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "HelloWorld", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "HelloWorld"); + + // Add a filter with a name that should be escaped if used in a JMX + // object name + FilterDef filterDef = new FilterDef(); + filterDef.setFilterClass(AddDefaultCharsetFilter.class.getName()); + filterDef.setFilterName("bug54170*"); + ctx.addFilterDef(filterDef); + + tomcat.start(); + + final MBeanServer mbeanServer = + Registry.getRegistry(null, null).getMBeanServer(); + + // There should be one Servlet MBean registered + Set servlets = mbeanServer.queryNames( + new ObjectName("Tomcat:j2eeType=Servlet,*"), null); + Assert.assertEquals(1, servlets.size()); + + // There should be one Filter MBean registered + Set filters = mbeanServer.queryNames( + new ObjectName("Tomcat:j2eeType=Filter,*"), null); + Assert.assertEquals(1, filters.size()); + } +} diff --git a/test/org/apache/catalina/core/TestApplicationHttpRequest.java b/test/org/apache/catalina/core/TestApplicationHttpRequest.java new file mode 100644 index 0000000..a233a8e --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationHttpRequest.java @@ -0,0 +1,357 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestApplicationHttpRequest extends TomcatBaseTest { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=58836 + */ + @Test + public void testForwardQueryString01() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b" }); + doQueryStringTest(null, "a=b", expected); + } + + + @Test + public void testForwardQueryString02() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b", "c" }); + doQueryStringTest(null, "a=b&a=c", expected); + } + + + @Test + public void testForwardQueryString03() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b" }); + expected.put("c", new String[] { "d" }); + doQueryStringTest(null, "a=b&c=d", expected); + } + + + @Test + public void testForwardQueryString04() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b", "e" }); + expected.put("c", new String[] { "d" }); + doQueryStringTest(null, "a=b&c=d&a=e", expected); + } + + + @Test + public void testForwardQueryString05() throws Exception { + // Parameters with no value are assigned a value of the empty string + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b", "e" }); + expected.put("c", new String[] { "" }); + doQueryStringTest(null, "a=b&c&a=e", expected); + } + + + @Test + public void testOriginalQueryString01() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b" }); + doQueryStringTest("a=b", null, expected); + } + + + @Test + public void testOriginalQueryString02() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b", "c" }); + doQueryStringTest("a=b&a=c", null, expected); + } + + + @Test + public void testOriginalQueryString03() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b" }); + expected.put("c", new String[] { "d" }); + doQueryStringTest("a=b&c=d", null, expected); + } + + + @Test + public void testOriginalQueryString04() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b", "e" }); + expected.put("c", new String[] { "d" }); + doQueryStringTest("a=b&c=d&a=e", null, expected); + } + + + @Test + public void testOriginalQueryString05() throws Exception { + // Parameters with no value are assigned a value of the empty string + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b", "e" }); + expected.put("c", new String[] { "" }); + doQueryStringTest("a=b&c&a=e", null, expected); + } + + + @Test + public void testMergeQueryString01() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "z", "b" }); + doQueryStringTest("a=b", "a=z", expected); + } + + + @Test + public void testMergeQueryString02() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "z", "b", "e" }); + expected.put("c", new String[] { "" }); + doQueryStringTest("a=b&c&a=e", "a=z", expected); + } + + + @Test + public void testMergeQueryString03() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b", "e" }); + expected.put("c", new String[] { "z", "" }); + doQueryStringTest("a=b&c&a=e", "c=z", expected); + } + + + @Test + public void testMergeQueryString04() throws Exception { + Map expected = new HashMap<>(); + expected.put("a", new String[] { "", "b", "e" }); + expected.put("c", new String[] { "" }); + doQueryStringTest("a=b&c&a=e", "a", expected); + } + + @Test + public void testMergeQueryString05() throws Exception { + // https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%81%D1%82 + // "Test" = "Test" + String test = "\u0422\u0435\u0441\u0442"; + String query = test + "=%D0%A2%D0%B5%D1%81%D1%82"; + + Map expected = new HashMap<>(); + expected.put("a", new String[] { "b" }); + expected.put(test, new String[] { test }); + doQueryStringTest("a=b", query, expected); + } + + + private void doQueryStringTest(String originalQueryString, String forwardQueryString, + Map expected) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + if (forwardQueryString == null) { + Tomcat.addServlet(ctx, "forward", new ForwardServlet("/display")); + } else { + Tomcat.addServlet(ctx, "forward", new ForwardServlet("/display?" + forwardQueryString)); + } + ctx.addServletMappingDecoded("/forward", "forward"); + + Tomcat.addServlet(ctx, "display", new DisplayParameterServlet(expected)); + ctx.addServletMappingDecoded("/display", "display"); + + tomcat.start(); + + ByteChunk response = new ByteChunk(); + StringBuilder target = new StringBuilder("http://localhost:"); + target.append(getPort()); + target.append("/forward"); + if (originalQueryString != null) { + target.append('?'); + target.append(originalQueryString); + } + int rc = getUrl(target.toString(), response, null); + + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", response.toString()); + } + + + @Test + public void testParameterImmutability() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "forward", new ForwardServlet("/modify")); + ctx.addServletMappingDecoded("/forward", "forward"); + + Tomcat.addServlet(ctx, "modify", new ModifyParameterServlet()); + ctx.addServletMappingDecoded("/modify", "modify"); + + tomcat.start(); + + ByteChunk response = new ByteChunk(); + StringBuilder target = new StringBuilder("http://localhost:"); + target.append(getPort()); + target.append("/forward"); + int rc = getUrl(target.toString(), response, null); + + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", response.toString()); + } + + + private static class ForwardServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final String target; + + ForwardServlet(String target) { + this.target = target; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.setCharacterEncoding("UTF-8"); + req.getRequestDispatcher(target).forward(req, resp); + } + } + + + private static class DisplayParameterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private Map expected; + + DisplayParameterServlet(Map expected) { + this.expected = expected; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.setCharacterEncoding("UTF-8"); + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter w = resp.getWriter(); + Map actual = req.getParameterMap(); + + boolean ok = true; + for (Entry entry : actual.entrySet()) { + String[] expectedValue = expected.get(entry.getKey()); + if (expectedValue == null || + expectedValue.length != entry.getValue().length) { + ok = false; + break; + } + for (int i = 0; i < expectedValue.length; i++) { + if (!expectedValue[i].equals(entry.getValue()[i])) { + ok = false; + break; + } + } + if (!ok) { + break; + } + } + + if (ok) { + w.print("OK"); + return; + } + boolean firstParam = true; + for (Entry param : actual.entrySet()) { + if (firstParam) { + firstParam = false; + } else { + w.print(';'); + } + w.print(param.getKey()); + w.print(':'); + boolean firstValue = true; + for (String value : param.getValue()) { + if (firstValue) { + firstValue = false; + } else { + w.print(','); + } + w.print('('); + w.print(value); + w.print(')'); + } + } + } + } + + + private static class ModifyParameterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + // Suppress warnings generated because the code is trying to put the + // wrong type of values into the Map + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + Map map = req.getParameterMap(); + + boolean insertWorks; + try { + map.put("test", new Integer[] { Integer.valueOf(0) }); + insertWorks = true; + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + insertWorks = false; + } + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + if (insertWorks) { + pw.print("FAIL"); + } else { + pw.print("OK"); + } + } + } +} diff --git a/test/org/apache/catalina/core/TestApplicationMapping.java b/test/org/apache/catalina/core/TestApplicationMapping.java new file mode 100644 index 0000000..39fcb99 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationMapping.java @@ -0,0 +1,375 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestApplicationMapping extends TomcatBaseTest { + + @Test + public void testContextNonRootMappingContextRoot() throws Exception { + doTestMapping("/dummy", "", "", "", "CONTEXT_ROOT"); + } + + @Test + public void testContextNonRootMappingDefault() throws Exception { + doTestMapping("/dummy", "/", "/foo", "", "DEFAULT"); + } + + @Test + public void testContextNonRootMappingExtension() throws Exception { + doTestMapping("/dummy", "*.test", "/foo/bar.test", "foo/bar", "EXTENSION"); + } + + @Test + public void testContextNonRootMappingExact() throws Exception { + doTestMapping("/dummy", "/foo/bar", "/foo/bar", "foo/bar", "EXACT"); + } + + @Test + public void testContextNonRootMappingPathNone() throws Exception { + doTestMapping("/dummy", "/foo/bar/*", "/foo/bar", null, "PATH"); + } + + @Test + public void testContextNonRootMappingPathSeparatorOnly() throws Exception { + doTestMapping("/dummy", "/foo/bar/*", "/foo/bar/", "", "PATH"); + } + + @Test + public void testContextNonRootMappingPath() throws Exception { + doTestMapping("/dummy", "/foo/bar/*", "/foo/bar/foo2", "foo2", "PATH"); + } + + @Test + public void testContextRootMappingContextRoot() throws Exception { + doTestMapping("", "", "", "", "CONTEXT_ROOT"); + } + + @Test + public void testContextRootMappingDefault() throws Exception { + doTestMapping("", "/", "/foo", "", "DEFAULT"); + } + + @Test + public void testContextRootMappingExtension() throws Exception { + doTestMapping("", "*.test", "/foo/bar.test", "foo/bar", "EXTENSION"); + } + + @Test + public void testContextRootMappingExact() throws Exception { + doTestMapping("", "/foo/bar", "/foo/bar", "foo/bar", "EXACT"); + } + + @Test + public void testContextRootMappingPath() throws Exception { + doTestMapping("", "/foo/bar/*", "/foo/bar/foo2", "foo2", "PATH"); + } + + private void doTestMapping(String contextPath, String mapping, String requestPath, + String matchValue, String matchType) throws Exception { + doTestMappingDirect(contextPath, mapping, requestPath, matchValue, matchType); + tearDown(); + setUp(); + doTestMappingInclude(contextPath, mapping, requestPath, matchValue, matchType); + tearDown(); + setUp(); + doTestMappingNamedInclude(contextPath, mapping, requestPath, matchValue, matchType); + tearDown(); + setUp(); + doTestMappingForward(contextPath, mapping, requestPath, matchValue, matchType); + tearDown(); + setUp(); + doTestMappingNamedForward(contextPath, mapping, requestPath, matchValue, matchType); + tearDown(); + setUp(); + doTestMappingAsync(contextPath, mapping, requestPath, matchValue, matchType); + } + + private void doTestMappingDirect(String contextPath, String mapping, String requestPath, + String matchValue, String matchType) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext(contextPath, null); + + Tomcat.addServlet(ctx, "Mapping", new MappingServlet()); + ctx.addServletMappingDecoded(mapping, "Mapping"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + contextPath + requestPath); + String body = bc.toString(); + + Assert.assertTrue(body, body.contains("MatchValue=[" + matchValue + "]")); + Assert.assertTrue(body, body.contains("Pattern=[" + mapping + "]")); + Assert.assertTrue(body, body.contains("MatchType=[" + matchType + "]")); + Assert.assertTrue(body, body.contains("ServletName=[Mapping]")); + } + + private void doTestMappingInclude(String contextPath, String mapping, String requestPath, + String matchValue, String matchType) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext(contextPath, null); + + Tomcat.addServlet(ctx, "Include", new IncludeServlet()); + ctx.addServletMappingDecoded(mapping, "Include"); + Tomcat.addServlet(ctx, "Mapping", new MappingServlet()); + ctx.addServletMappingDecoded("/mapping", "Mapping"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + contextPath + requestPath); + String body = bc.toString(); + + Assert.assertTrue(body, body.contains("MatchValue=[" + matchValue + "]")); + Assert.assertTrue(body, body.contains("Pattern=[" + mapping + "]")); + Assert.assertTrue(body, body.contains("MatchType=[" + matchType + "]")); + Assert.assertTrue(body, body.contains("ServletName=[Include]")); + + Assert.assertTrue(body, body.contains("IncludeMatchValue=[mapping]")); + Assert.assertTrue(body, body.contains("IncludePattern=[/mapping]")); + Assert.assertTrue(body, body.contains("IncludeMatchType=[EXACT]")); + Assert.assertTrue(body, body.contains("IncludeServletName=[Mapping]")); + } + + private void doTestMappingNamedInclude(String contextPath, String mapping, String requestPath, + String matchValue, String matchType) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext(contextPath, null); + + Tomcat.addServlet(ctx, "Include", new NamedIncludeServlet()); + ctx.addServletMappingDecoded(mapping, "Include"); + Tomcat.addServlet(ctx, "Mapping", new MappingServlet()); + ctx.addServletMappingDecoded("/mapping", "Mapping"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + contextPath + requestPath); + String body = bc.toString(); + + Assert.assertTrue(body, body.contains("MatchValue=[" + matchValue + "]")); + Assert.assertTrue(body, body.contains("Pattern=[" + mapping + "]")); + Assert.assertTrue(body, body.contains("MatchType=[" + matchType + "]")); + Assert.assertTrue(body, body.contains("ServletName=[Include]")); + } + + private void doTestMappingForward(String contextPath, String mapping, String requestPath, + String matchValue, String matchType) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext(contextPath, null); + + Tomcat.addServlet(ctx, "Forward", new ForwardServlet()); + ctx.addServletMappingDecoded(mapping, "Forward"); + Tomcat.addServlet(ctx, "Mapping", new MappingServlet()); + ctx.addServletMappingDecoded("/mapping", "Mapping"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + contextPath + requestPath); + String body = bc.toString(); + + Assert.assertTrue(body, body.contains("MatchValue=[mapping]")); + Assert.assertTrue(body, body.contains("Pattern=[/mapping]")); + Assert.assertTrue(body, body.contains("MatchType=[EXACT]")); + Assert.assertTrue(body, body.contains("ServletName=[Mapping]")); + + Assert.assertTrue(body, body.contains("ForwardMatchValue=[" + matchValue + "]")); + Assert.assertTrue(body, body.contains("ForwardPattern=[" + mapping + "]")); + Assert.assertTrue(body, body.contains("ForwardMatchType=[" + matchType + "]")); + Assert.assertTrue(body, body.contains("ForwardServletName=[Forward]")); + } + + private void doTestMappingNamedForward(String contextPath, String mapping, String requestPath, + String matchValue, String matchType) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext(contextPath, null); + + Tomcat.addServlet(ctx, "Forward", new NamedForwardServlet()); + ctx.addServletMappingDecoded(mapping, "Forward"); + Tomcat.addServlet(ctx, "Mapping", new MappingServlet()); + ctx.addServletMappingDecoded("/mapping", "Mapping"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + contextPath + requestPath); + String body = bc.toString(); + + Assert.assertTrue(body, body.contains("MatchValue=[" + matchValue + "]")); + Assert.assertTrue(body, body.contains("Pattern=[" + mapping + "]")); + Assert.assertTrue(body, body.contains("MatchType=[" + matchType + "]")); + Assert.assertTrue(body, body.contains("ServletName=[Forward]")); + } + + private void doTestMappingAsync(String contextPath, String mapping, String requestPath, + String matchValue, String matchType) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext(contextPath, null); + + Wrapper w = Tomcat.addServlet(ctx, "Async", new AsyncServlet()); + w.setAsyncSupported(true); + ctx.addServletMappingDecoded(mapping, "Async"); + Tomcat.addServlet(ctx, "Mapping", new MappingServlet()); + ctx.addServletMappingDecoded("/mapping", "Mapping"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + contextPath + requestPath); + String body = bc.toString(); + + Assert.assertTrue(body, body.contains("MatchValue=[mapping]")); + Assert.assertTrue(body, body.contains("Pattern=[/mapping]")); + Assert.assertTrue(body, body.contains("MatchType=[EXACT]")); + Assert.assertTrue(body, body.contains("ServletName=[Mapping]")); + + Assert.assertTrue(body, body.contains("AsyncMatchValue=[" + matchValue + "]")); + Assert.assertTrue(body, body.contains("AsyncPattern=[" + mapping + "]")); + Assert.assertTrue(body, body.contains("AsyncMatchType=[" + matchType + "]")); + Assert.assertTrue(body, body.contains("AsyncServletName=[Async]")); + } + + + private static class IncludeServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + RequestDispatcher rd = req.getRequestDispatcher("/mapping"); + rd.include(req, resp); + } + } + + + private static class NamedIncludeServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + RequestDispatcher rd = req.getServletContext().getNamedDispatcher("Mapping"); + rd.include(req, resp); + } + } + + + private static class NamedForwardServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + RequestDispatcher rd = req.getServletContext().getNamedDispatcher("Mapping"); + rd.forward(req, resp); + } + } + + + private static class ForwardServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + RequestDispatcher rd = req.getRequestDispatcher("/mapping"); + rd.forward(req, resp); + } + } + + + private static class AsyncServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + AsyncContext ac = req.startAsync(); + ac.dispatch("/mapping"); + } + } + + + private static class MappingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain;charset=UTF-8"); + PrintWriter pw = resp.getWriter(); + HttpServletMapping mapping = req.getHttpServletMapping(); + pw.println("MatchValue=[" + mapping.getMatchValue() + "]"); + pw.println("Pattern=[" + mapping.getPattern() + "]"); + pw.println("MatchType=[" + mapping.getMappingMatch() + "]"); + pw.println("ServletName=[" + mapping.getServletName() + "]"); + HttpServletMapping includeMapping = + (HttpServletMapping) req.getAttribute(RequestDispatcher.INCLUDE_MAPPING); + if (includeMapping != null) { + pw.println("IncludeMatchValue=[" + includeMapping.getMatchValue() + "]"); + pw.println("IncludePattern=[" + includeMapping.getPattern() + "]"); + pw.println("IncludeMatchType=[" + includeMapping.getMappingMatch() + "]"); + pw.println("IncludeServletName=[" + includeMapping.getServletName() + "]"); + + } + HttpServletMapping forwardMapping = + (HttpServletMapping) req.getAttribute(RequestDispatcher.FORWARD_MAPPING); + if (forwardMapping != null) { + pw.println("ForwardMatchValue=[" + forwardMapping.getMatchValue() + "]"); + pw.println("ForwardPattern=[" + forwardMapping.getPattern() + "]"); + pw.println("ForwardMatchType=[" + forwardMapping.getMappingMatch() + "]"); + pw.println("ForwardServletName=[" + forwardMapping.getServletName() + "]"); + } + HttpServletMapping asyncMapping = + (HttpServletMapping) req.getAttribute(AsyncContext.ASYNC_MAPPING); + if (asyncMapping != null) { + pw.println("AsyncMatchValue=[" + asyncMapping.getMatchValue() + "]"); + pw.println("AsyncPattern=[" + asyncMapping.getPattern() + "]"); + pw.println("AsyncMatchType=[" + asyncMapping.getMappingMatch() + "]"); + pw.println("AsyncServletName=[" + asyncMapping.getServletName() + "]"); + } + } + } +} diff --git a/test/org/apache/catalina/core/TestApplicationPushBuilder.java b/test/org/apache/catalina/core/TestApplicationPushBuilder.java new file mode 100644 index 0000000..e939084 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationPushBuilder.java @@ -0,0 +1,72 @@ +/* + * 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.catalina.core; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +public class TestApplicationPushBuilder { + + @Test + public void test01() { + doTest("foo", StandardCharsets.UTF_8, "foo"); + } + + @Test + public void test02() { + doTest("/foo", StandardCharsets.UTF_8, "/foo"); + } + + @Test + public void test03() { + doTest("%20foo", StandardCharsets.UTF_8, " foo"); + } + + @Test + public void test04() { + doTest("fo%20o", StandardCharsets.UTF_8, "fo o"); + } + + @Test + public void test05() { + doTest("foo%20", StandardCharsets.UTF_8, "foo "); + } + + @Test + public void test06() { + doTest("%21foo", StandardCharsets.UTF_8, "!foo"); + } + + @Test + public void test07() { + doTest("fo%21o", StandardCharsets.UTF_8, "fo!o"); + } + + @Test + public void test08() { + doTest("foo%21", StandardCharsets.UTF_8, "foo!"); + } + + + private void doTest(String input, Charset charset, String expected) { + String result = ApplicationPushBuilder.decode(input, charset); + Assert.assertEquals(expected, result); + } +} diff --git a/test/org/apache/catalina/core/TestApplicationSessionCookieConfig.java b/test/org/apache/catalina/core/TestApplicationSessionCookieConfig.java new file mode 100644 index 0000000..4203901 --- /dev/null +++ b/test/org/apache/catalina/core/TestApplicationSessionCookieConfig.java @@ -0,0 +1,139 @@ +/* + * 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.catalina.core; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.LifecycleState; + +public class TestApplicationSessionCookieConfig { + private ApplicationSessionCookieConfig applicationSessionCookieConfig; + private final CustomContext context = new CustomContext(); + + @Before + public void setUp() throws Exception { + applicationSessionCookieConfig = new ApplicationSessionCookieConfig( + context); + } + + @Test + public void testSetCommentInitPhase() { + context.setState(LifecycleState.STARTING_PREP); + applicationSessionCookieConfig.setComment("test"); + Assert.assertNull(applicationSessionCookieConfig.getComment()); + } + + @Test(expected = IllegalStateException.class) + public void testSetCommentNotInitPhase() { + context.setState(LifecycleState.STARTED); + applicationSessionCookieConfig.setComment("test"); + } + + @Test + public void testSetDomainInitPhase() { + context.setState(LifecycleState.STARTING_PREP); + applicationSessionCookieConfig.setDomain("test"); + Assert.assertTrue(applicationSessionCookieConfig.getDomain().equals("test")); + } + + @Test(expected = IllegalStateException.class) + public void testSetDomainNotInitPhase() { + context.setState(LifecycleState.STARTED); + applicationSessionCookieConfig.setDomain("test"); + } + + @Test + public void testSetHttpOnlyInitPhase() { + context.setState(LifecycleState.STARTING_PREP); + applicationSessionCookieConfig.setHttpOnly(true); + Assert.assertTrue(applicationSessionCookieConfig.isHttpOnly()); + } + + @Test(expected = IllegalStateException.class) + public void testSetHttpOnlyNotInitPhase() { + context.setState(LifecycleState.STARTED); + applicationSessionCookieConfig.setHttpOnly(true); + } + + @Test + public void testSetMaxAgeInitPhase() { + context.setState(LifecycleState.STARTING_PREP); + applicationSessionCookieConfig.setMaxAge(1); + Assert.assertTrue(applicationSessionCookieConfig.getMaxAge() == 1); + } + + @Test(expected = IllegalStateException.class) + public void testSetMaxAgeNotInitPhase() { + context.setState(LifecycleState.STARTED); + applicationSessionCookieConfig.setMaxAge(1); + } + + @Test + public void testSetNameInitPhase() { + context.setState(LifecycleState.STARTING_PREP); + applicationSessionCookieConfig.setName("test"); + Assert.assertTrue(applicationSessionCookieConfig.getName().equals("test")); + } + + @Test(expected = IllegalStateException.class) + public void testSetNameNotInitPhase() { + context.setState(LifecycleState.STARTED); + applicationSessionCookieConfig.setName("test"); + } + + @Test + public void testSetPathInitPhase() { + context.setState(LifecycleState.STARTING_PREP); + applicationSessionCookieConfig.setPath("test"); + Assert.assertTrue(applicationSessionCookieConfig.getPath().equals("test")); + } + + @Test(expected = IllegalStateException.class) + public void testSetPathNotInitPhase() { + context.setState(LifecycleState.STARTED); + applicationSessionCookieConfig.setPath("test"); + } + + @Test + public void testSetSecureInitPhase() { + context.setState(LifecycleState.STARTING_PREP); + applicationSessionCookieConfig.setSecure(true); + Assert.assertTrue(applicationSessionCookieConfig.isSecure()); + } + + @Test(expected = IllegalStateException.class) + public void testSetSecureNotInitPhase() { + context.setState(LifecycleState.STARTED); + applicationSessionCookieConfig.setSecure(true); + } + + private static class CustomContext extends StandardContext { + private volatile LifecycleState state; + + @Override + public LifecycleState getState() { + return state; + } + + @Override + public synchronized void setState(LifecycleState state) { + this.state = state; + } + } +} diff --git a/test/org/apache/catalina/core/TestAsyncContextImpl.java b/test/org/apache/catalina/core/TestAsyncContextImpl.java new file mode 100644 index 0000000..fa89a1b --- /dev/null +++ b/test/org/apache/catalina/core/TestAsyncContextImpl.java @@ -0,0 +1,3218 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.LogManager; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.GenericServlet; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.ServletRequestWrapper; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.ServletResponseWrapper; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.connector.TestCoyoteAdapter; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.valves.TesterAccessLogValve; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.EncodedSolidusHandling; +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.easymock.EasyMock; + +public class TestAsyncContextImpl extends TomcatBaseTest { + + // Time for a request to process (need to allow for threads to start etc.) + private static final long REQUEST_TIME = 1500; + // Timeout thread (where used) checks for timeout every second + private static final long TIMEOUT_MARGIN = 1000; + // Default timeout for these tests + private static final long TIMEOUT = 5000; + + private static StringBuilder tracker; + + public static synchronized void resetTracker() { + tracker = new StringBuilder(); + } + + public static synchronized void track(String trace) { + tracker.append(trace); + } + + public static synchronized String getTrack() { + return tracker.toString(); + } + + @Test + public void testBug49528() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Bug49528Servlet servlet = new Bug49528Servlet(); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/", "servlet"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + // Call the servlet once + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("OK", bc.toString()); + + // Give the async thread a chance to finish (but not too long) + int counter = 0; + while (!servlet.isDone() && counter < 10) { + Thread.sleep(1000); + counter++; + } + + Assert.assertEquals("1false2true3true4true5false", servlet.getResult()); + + // Check the access log + alv.validateAccessLog(1, 200, Bug49528Servlet.THREAD_SLEEP_TIME, + Bug49528Servlet.THREAD_SLEEP_TIME + REQUEST_TIME); + } + + @Test + public void testBug49567() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Bug49567Servlet servlet = new Bug49567Servlet(); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/", "servlet"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + // Call the servlet once + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("OK", bc.toString()); + + // Give the async thread a chance to finish (but not too long) + int counter = 0; + while (!servlet.isDone() && counter < 20) { + Thread.sleep(1000); + counter++; + } + + Assert.assertEquals("1false2true3true4true5false", servlet.getResult()); + + // Check the access log + alv.validateAccessLog(1, 200, Bug49567Servlet.THREAD_SLEEP_TIME, + Bug49567Servlet.THREAD_SLEEP_TIME + REQUEST_TIME); + } + + @Test + public void testAsyncStartNoComplete() throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // Minimise pauses during test + Assert.assertTrue(tomcat.getConnector().setProperty("connectionTimeout", "3000")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncStartNoCompleteServlet servlet = + new AsyncStartNoCompleteServlet(); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/", "servlet"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + // Call the servlet the first time + getUrl("http://localhost:" + getPort() + "/?echo=run1"); + Assert.assertEquals("OK-run1", getTrack()); + resetTracker(); + + // Call the servlet the second time with a request parameter + getUrl("http://localhost:" + getPort() + "/?echo=run2"); + Assert.assertEquals("OK-run2", getTrack()); + + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + + // Check the access log + alv.validateAccessLog(2, 500, + AsyncStartNoCompleteServlet.ASYNC_TIMEOUT, + AsyncStartNoCompleteServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + + REQUEST_TIME); + } + + @Test + public void testAsyncStartWithComplete() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncStartWithCompleteServlet servlet = + new AsyncStartWithCompleteServlet(); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/", "servlet"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + // Call the servlet once + ByteChunk bc = new ByteChunk(); + Map> headers = new CaseInsensitiveKeyMap<>(); + getUrl("http://localhost:" + getPort() + "/", bc, headers); + + Assert.assertEquals("OK", bc.toString()); + String contentLength = getSingleHeader("Content-Length", headers); + Assert.assertEquals("2", contentLength); + + // Check the access log + alv.validateAccessLog(1, 200, 0, REQUEST_TIME); + } + + /* + * NOTE: This servlet is only intended to be used in single-threaded tests. + */ + private static class Bug49528Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private volatile boolean done = false; + + private StringBuilder result; + + public static final long THREAD_SLEEP_TIME = 1000; + + public String getResult() { + return result.toString(); + } + + public boolean isDone() { + return done; + } + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + + result = new StringBuilder(); + result.append('1'); + result.append(req.isAsyncStarted()); + req.startAsync().setTimeout(10000); + result.append('2'); + result.append(req.isAsyncStarted()); + + req.getAsyncContext().start(new Runnable() { + @Override + public void run() { + try { + result.append('3'); + result.append(req.isAsyncStarted()); + Thread.sleep(THREAD_SLEEP_TIME); + result.append('4'); + result.append(req.isAsyncStarted()); + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + req.getAsyncContext().complete(); + result.append('5'); + try { + // Once complete() has been called on a + // non-container thread it is not safe to + // continue to use the request object as it + // may be recycled at any point. Normally + // there is enough time for this call to + // complete but not always. If this call + // fails in Tomcat an ISE will result so + // handle this here with a hack. What we are + // really checking here is that it does not + // return true. + result.append(req.isAsyncStarted()); + } catch (IllegalStateException npe) { + result.append("false"); + } catch (Throwable t) { + // Additional debugging for intermittent test failure + result.append(t.getClass().getName()); + t.printStackTrace(); + } + done = true; + } catch (InterruptedException | IOException e) { + result.append(e); + } + } + }); + // Pointless method call so there is somewhere to put a break point + // when debugging + req.getMethod(); + } + } + + /* + * NOTE: This servlet is only intended to be used in single-threaded tests. + */ + private static class Bug49567Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private volatile boolean done = false; + + private volatile StringBuilder result; + + public static final long THREAD_SLEEP_TIME = 1000; + + public String getResult() { + return result.toString(); + } + + public boolean isDone() { + return done; + } + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + + result = new StringBuilder(); + result.append('1'); + result.append(req.isAsyncStarted()); + req.startAsync(); + result.append('2'); + result.append(req.isAsyncStarted()); + + req.getAsyncContext().start(new Runnable() { + @Override + public void run() { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + result.append('3'); + result.append(req.isAsyncStarted()); + Thread.sleep(THREAD_SLEEP_TIME); + result.append('4'); + result.append(req.isAsyncStarted()); + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + req.getAsyncContext().complete(); + result.append('5'); + try { + // Once complete() has been called on a + // non-container thread it is not safe to + // continue to use the request object as it + // may be recycled at any point. Normally + // there is enough time for this call to + // complete but not always. If this call + // fails in Tomcat an ISE will result so + // handle this here with a hack. What we are + // really checking here is that it does not + // return true. + result.append(req.isAsyncStarted()); + } catch (IllegalStateException ise) { + result.append("false"); + } + done = true; + } catch (InterruptedException | IOException e) { + result.append(e); + } + } + }); + t.start(); + } + }); + // Pointless method call so there is somewhere to put a break point + // when debugging + req.getMethod(); + } + } + + private static class AsyncStartNoCompleteServlet extends HttpServlet { + + public static final long ASYNC_TIMEOUT = 1000; + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + + String echo = req.getParameter("echo"); + AsyncContext actxt = req.startAsync(); + track("OK"); + if (echo != null) { + track("-" + echo); + } + // Speed up the test by reducing the timeout + actxt.setTimeout(ASYNC_TIMEOUT); + } + } + + private static class AsyncStartWithCompleteServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + + AsyncContext actxt = req.startAsync(); + actxt.setTimeout(3000); + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + actxt.complete(); + } + } + + @Test + public void testTimeoutListenerCompleteNoDispatch() throws Exception { + // Should work + doTestTimeout(Boolean.TRUE, null); + } + + @Test + public void testTimeoutListenerNoCompleteNoDispatch() throws Exception { + // Should trigger an error - must do one or other + doTestTimeout(Boolean.FALSE, null); + } + + @Test + public void testTimeoutListenerCompleteNonAsyncDispatch() throws Exception { + // Should trigger an error - can't do both + doTestTimeout(Boolean.TRUE, Boolean.FALSE); + } + + @Test + public void testTimeoutListenerNoCompleteNonAsyncDispatch() + throws Exception { + // Should work + doTestTimeout(Boolean.FALSE, Boolean.FALSE); + } + + @Test + public void testTimeoutListenerCompleteAsyncDispatch() throws Exception { + // Should trigger an error - can't do both + doTestTimeout(Boolean.TRUE, Boolean.TRUE); + } + + @Test + public void testTimeoutListenerNoCompleteAsyncDispatch() throws Exception { + // Should work + doTestTimeout(Boolean.FALSE, Boolean.TRUE); + } + + @Test + public void testTimeoutNoListener() throws Exception { + // Should work + doTestTimeout(null, null); + } + + private void doTestTimeout(Boolean completeOnTimeout, Boolean asyncDispatch) + throws Exception { + + resetTracker(); + + String dispatchUrl = null; + if (asyncDispatch != null) { + if (asyncDispatch.booleanValue()) { + dispatchUrl = "/async"; + } else { + dispatchUrl = "/nonasync"; + } + } + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + TimeoutServlet timeout = new TimeoutServlet(completeOnTimeout, dispatchUrl); + + Wrapper wrapper = Tomcat.addServlet(ctx, "time", timeout); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/start", "time"); + + if (asyncDispatch != null) { + if (asyncDispatch.booleanValue()) { + AsyncStartRunnable asyncStartRunnable = + new AsyncStartRunnable(); + Wrapper async = + Tomcat.addServlet(ctx, "async", asyncStartRunnable); + async.setAsyncSupported(true); + ctx.addServletMappingDecoded(dispatchUrl, "async"); + } else { + NonAsyncServlet nonAsync = new NonAsyncServlet(); + Tomcat.addServlet(ctx, "nonasync", nonAsync); + ctx.addServletMappingDecoded(dispatchUrl, "nonasync"); + } + } + + ctx.addApplicationListener(TrackingRequestListener.class.getName()); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + TesterAccessLogValve alvGlobal = new TesterAccessLogValve(); + tomcat.getHost().getPipeline().addValve(alvGlobal); + + tomcat.start(); + try { + getUrl("http://localhost:" + getPort() + "/start"); + } catch (IOException ioe) { + // Ignore - expected for some error conditions + } + StringBuilder expected = new StringBuilder("requestInitialized-"); + expected.append("TimeoutServletGet-"); + if (completeOnTimeout == null) { + expected.append("requestDestroyed"); + } else if (completeOnTimeout.booleanValue()) { + expected.append("onTimeout-"); + expected.append("onComplete-"); + expected.append("requestDestroyed"); + } else { + expected.append("onTimeout-"); + if (asyncDispatch != null) { + if (asyncDispatch.booleanValue()) { + expected.append("onStartAsync-Runnable-"); + } else { + expected.append("NonAsyncServletGet-"); + } + } + expected.append("onComplete-"); + expected.append("requestDestroyed"); + } + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + String expectedTrack = expected.toString(); + int count = 0; + while (!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count ++; + } + Assert.assertEquals(expectedTrack, getTrack()); + + // Check the access log + if (completeOnTimeout == null || + (!completeOnTimeout.booleanValue() && asyncDispatch == null)) { + alvGlobal.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT, + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + + REQUEST_TIME); + alv.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT, + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + + REQUEST_TIME); + } else { + long timeoutDelay = TimeoutServlet.ASYNC_TIMEOUT; + if (asyncDispatch != null && asyncDispatch.booleanValue() && + !completeOnTimeout.booleanValue()) { + // The async dispatch includes a sleep + timeoutDelay += AsyncStartRunnable.THREAD_SLEEP_TIME; + } + alvGlobal.validateAccessLog(1, 200, timeoutDelay, + timeoutDelay + TIMEOUT_MARGIN + REQUEST_TIME); + alv.validateAccessLog(1, 200, timeoutDelay, + timeoutDelay + TIMEOUT_MARGIN + REQUEST_TIME); + } + + Assert.assertTrue(timeout.isAsyncStartedCorrect()); + } + + private static class TimeoutServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + private final Boolean completeOnTimeout; + private final transient TrackingListener trackingListener; + + public static final long ASYNC_TIMEOUT = 100; + + TimeoutServlet(Boolean completeOnTimeout, String dispatchUrl) { + this.completeOnTimeout = completeOnTimeout; + if (completeOnTimeout == null) { + this.trackingListener = null; + } else { + this.trackingListener = new TrackingListener(false, completeOnTimeout.booleanValue(), dispatchUrl); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (req.isAsyncSupported()) { + track("TimeoutServletGet-"); + final AsyncContext ac = req.startAsync(); + ac.setTimeout(ASYNC_TIMEOUT); + + if (completeOnTimeout != null) { + ac.addListener(trackingListener); + } + } else { + resp.getWriter().print("FAIL: Async unsupported"); + } + } + + public boolean isAsyncStartedCorrect() { + if (trackingListener == null) { + return true; + } + return trackingListener.isAsyncStartedCorrect(); + } + } + + @Test + public void testDispatchSingle() throws Exception { + doTestDispatch(1, false); + } + + @Test + public void testDispatchDouble() throws Exception { + doTestDispatch(2, false); + } + + @Test + public void testDispatchMultiple() throws Exception { + doTestDispatch(5, false); + } + + @Test + public void testDispatchWithThreadSingle() throws Exception { + doTestDispatch(1, true); + } + + @Test + public void testDispatchWithThreadDouble() throws Exception { + doTestDispatch(2, true); + } + + @Test + public void testDispatchWithThreadMultiple() throws Exception { + doTestDispatch(5, true); + } + + private void doTestDispatch(int iter, boolean useThread) throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + DispatchingServlet dispatch = new DispatchingServlet(false, false); + Wrapper wrapper = Tomcat.addServlet(ctx, "dispatch", dispatch); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/stage1", "dispatch"); + + NonAsyncServlet nonasync = new NonAsyncServlet(); + Wrapper wrapper2 = Tomcat.addServlet(ctx, "nonasync", nonasync); + wrapper2.setAsyncSupported(true); + ctx.addServletMappingDecoded("/stage2", "nonasync"); + + ctx.addApplicationListener(TrackingRequestListener.class.getName()); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/stage1?iter="); + url.append(iter); + if (useThread) { + url.append("&useThread=y"); + } + getUrl(url.toString()); + + StringBuilder expected = new StringBuilder("requestInitialized-"); + int loop = iter; + while (loop > 0) { + expected.append("DispatchingServletGet-"); + loop--; + } + expected.append("NonAsyncServletGet-"); + expected.append("requestDestroyed"); + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + String expectedTrack = expected.toString(); + int count = 0; + while (!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count ++; + } + Assert.assertEquals(expectedTrack, getTrack()); + Assert.assertTrue(dispatch.isAsyncStartedCorrect()); + + // Check the access log + alv.validateAccessLog(1, 200, 0, REQUEST_TIME); + } + + private static class DispatchingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String ITER_PARAM = "iter"; + private static final String DISPATCH_CHECK = "check"; + private final transient TrackingListener trackingListener; + + DispatchingServlet(boolean addTrackingListener, + boolean completeOnError) { + if (addTrackingListener) { + trackingListener = new TrackingListener(completeOnError, true, null); + } else { + trackingListener = null; + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if ("y".equals(req.getParameter(DISPATCH_CHECK))) { + if (req.getDispatcherType() != DispatcherType.ASYNC) { + track("WrongDispatcherType-"); + } + } + track("DispatchingServletGet-"); + final int iter = Integer.parseInt(req.getParameter(ITER_PARAM)) - 1; + final AsyncContext ctxt = req.startAsync(); + if (trackingListener != null) { + ctxt.addListener(trackingListener); + } + Runnable run = new Runnable() { + @Override + public void run() { + if (iter > 0) { + ctxt.dispatch("/stage1?" + ITER_PARAM + "=" + iter + + "&" + DISPATCH_CHECK + "=y"); + } else { + ctxt.dispatch("/stage2"); + } + } + }; + if ("y".equals(req.getParameter("useThread"))) { + new Thread(run).start(); + } else { + run.run(); + } + } + + public boolean isAsyncStartedCorrect() { + if (trackingListener == null) { + return true; + } + return trackingListener.isAsyncStartedCorrect(); + } + } + + private static class NonAsyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + track("NonAsyncServletGet-"); + } + } + + @Test + public void testListeners() throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + TrackingServlet tracking = new TrackingServlet(); + Wrapper wrapper = Tomcat.addServlet(ctx, "tracking", tracking); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/stage1", "tracking"); + + TimeoutServlet timeout = new TimeoutServlet(Boolean.TRUE, null); + Wrapper wrapper2 = Tomcat.addServlet(ctx, "timeout", timeout); + wrapper2.setAsyncSupported(true); + ctx.addServletMappingDecoded("/stage2", "timeout"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/stage1"); + + getUrl(url.toString()); + + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + String expectedTrack = "DispatchingServletGet-DispatchingServletGet-" + + "onStartAsync-TimeoutServletGet-onStartAsync-onTimeout-" + + "onComplete-"; + int count = 0; + while (!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count ++; + } + Assert.assertEquals(expectedTrack, getTrack()); + + // Check the access log + alv.validateAccessLog(1, 200, TimeoutServlet.ASYNC_TIMEOUT, + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + REQUEST_TIME); + } + + private static class TrackingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static volatile boolean first = true; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + track("DispatchingServletGet-"); + resp.flushBuffer(); + + final boolean first = TrackingServlet.first; + TrackingServlet.first = false; + + final AsyncContext ctxt = req.startAsync(); + TrackingListener listener = new TrackingListener(false, true, null); + ctxt.addListener(listener); + ctxt.setTimeout(3000); + + Runnable run = new Runnable() { + @Override + public void run() { + if (first) { + ctxt.dispatch("/stage1"); + } else { + ctxt.dispatch("/stage2"); + } + } + }; + if ("y".equals(req.getParameter("useThread"))) { + new Thread(run).start(); + } else { + run.run(); + } + } + } + + public static class TrackingListener implements AsyncListener { + + private final boolean completeOnError; + private final boolean completeOnTimeout; + private final String dispatchUrl; + // Assumes listener is fired after container thread that initiated async + // has exited. + private boolean asyncStartedCorrect = true; + + public TrackingListener(boolean completeOnError, + boolean completeOnTimeout, String dispatchUrl) { + this.completeOnError = completeOnError; + this.completeOnTimeout = completeOnTimeout; + this.dispatchUrl = dispatchUrl; + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + track("onComplete-"); + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + boolean expectedAsyncStarted = true; + + track("onTimeout-"); + if (completeOnTimeout){ + event.getAsyncContext().complete(); + expectedAsyncStarted = false; + } + if (dispatchUrl != null) { + event.getAsyncContext().dispatch(dispatchUrl); + expectedAsyncStarted = false; + } + + ServletRequest req = event.getSuppliedRequest(); + asyncStartedCorrect = (expectedAsyncStarted == req.isAsyncStarted()); + } + + @Override + public void onError(AsyncEvent event) throws IOException { + boolean expectedAsyncStarted = true; + + track("onError-"); + if (completeOnError) { + event.getAsyncContext().complete(); + expectedAsyncStarted = false; + } + + ServletRequest req = event.getSuppliedRequest(); + asyncStartedCorrect = (expectedAsyncStarted == req.isAsyncStarted()); + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + track("onStartAsync-"); + } + + public boolean isAsyncStartedCorrect() { + return asyncStartedCorrect; + } + } + + private static class StickyTrackingListener extends TrackingListener { + + StickyTrackingListener(boolean completeOnError, + boolean completeOnTimeout, String dispatchUrl) { + super(completeOnError, completeOnTimeout, dispatchUrl); + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + super.onStartAsync(event); + // Re-add this listener to the new AsyncContext + event.getAsyncContext().addListener(this); + } + } + + public static class TrackingRequestListener + implements ServletRequestListener { + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + track("requestDestroyed"); + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + track("requestInitialized-"); + } + } + + @Test + public void testDispatchErrorSingle() throws Exception { + doTestDispatchError(1, false, false); + } + + @Test + public void testDispatchErrorDouble() throws Exception { + doTestDispatchError(2, false, false); + } + + @Test + public void testDispatchErrorMultiple() throws Exception { + doTestDispatchError(5, false, false); + } + + @Test + public void testDispatchErrorWithThreadSingle() throws Exception { + doTestDispatchError(1, true, false); + } + + @Test + public void testDispatchErrorWithThreadDouble() throws Exception { + doTestDispatchError(2, true, false); + } + + @Test + public void testDispatchErrorWithThreadMultiple() throws Exception { + doTestDispatchError(5, true, false); + } + + @Test + public void testDispatchErrorSingleThenComplete() throws Exception { + doTestDispatchError(1, false, true); + } + + @Test + public void testDispatchErrorDoubleThenComplete() throws Exception { + doTestDispatchError(2, false, true); + } + + @Test + public void testDispatchErrorMultipleThenComplete() throws Exception { + doTestDispatchError(5, false, true); + } + + @Test + public void testDispatchErrorWithThreadSingleThenComplete() + throws Exception { + doTestDispatchError(1, true, true); + } + + @Test + public void testDispatchErrorWithThreadDoubleThenComplete() + throws Exception { + doTestDispatchError(2, true, true); + } + + @Test + public void testDispatchErrorWithThreadMultipleThenComplete() + throws Exception { + doTestDispatchError(5, true, true); + } + + private void doTestDispatchError(int iter, boolean useThread, + boolean completeOnError) + throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + DispatchingServlet dispatch = + new DispatchingServlet(true, completeOnError); + Wrapper wrapper = Tomcat.addServlet(ctx, "dispatch", dispatch); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/stage1", "dispatch"); + + ErrorServlet error = new ErrorServlet(); + Tomcat.addServlet(ctx, "error", error); + ctx.addServletMappingDecoded("/stage2", "error"); + + ctx.addApplicationListener(TrackingRequestListener.class.getName()); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/stage1?iter="); + url.append(iter); + if (useThread) { + url.append("&useThread=y"); + } + getUrl(url.toString()); + + StringBuilder expected = new StringBuilder("requestInitialized-"); + int loop = iter; + while (loop > 0) { + expected.append("DispatchingServletGet-"); + if (loop != iter) { + expected.append("onStartAsync-"); + } + loop--; + } + expected.append("ErrorServletGet-onError-onComplete-requestDestroyed"); + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + String expectedTrack = expected.toString(); + int count = 0; + while (!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count ++; + } + Assert.assertEquals(expectedTrack, getTrack()); + Assert.assertTrue(dispatch.isAsyncStartedCorrect()); + + // Check the access log + alv.validateAccessLog(1, 500, 0, REQUEST_TIME); + } + + private static class ErrorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + track("ErrorServletGet-"); + try { + // Give the original thread a chance to exit the + // ErrorReportValve before we throw this exception + Thread.sleep(500); + } catch (InterruptedException e) { + throw new ServletException(e); + } + throw new ServletException("Oops."); + } + } + + @Test + public void testBug50352() throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncStartRunnable servlet = new AsyncStartRunnable(); + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/", "servlet"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + getUrl("http://localhost:" + getPort() + "/"); + + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + String expectedTrack = "Runnable-onComplete-"; + int count = 0; + while (!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count ++; + } + Assert.assertEquals(expectedTrack, getTrack()); + + // Check the access log + alv.validateAccessLog(1, 200, AsyncStartRunnable.THREAD_SLEEP_TIME, + AsyncStartRunnable.THREAD_SLEEP_TIME + REQUEST_TIME); + } + + private static final class AsyncStartRunnable extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public static final long THREAD_SLEEP_TIME = 3000; + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) + throws ServletException, IOException { + + final AsyncContext asyncContext = + request.startAsync(request, response); + + asyncContext.addListener(new TrackingListener(false, false, null)); + + asyncContext.start(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(THREAD_SLEEP_TIME); + track("Runnable-"); + asyncContext.complete(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + } + + @Test + public void testBug50753() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Bug50753Servlet servlet = new Bug50753Servlet(); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/", "servlet"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + // Call the servlet once + Map> headers = new CaseInsensitiveKeyMap<>(); + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/", bc, headers); + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + String testHeader = getSingleHeader("A", headers); + Assert.assertEquals("xyz",testHeader); + + // Check the access log + alv.validateAccessLog(1, 200, Bug50753Servlet.THREAD_SLEEP_TIME, + Bug50753Servlet.THREAD_SLEEP_TIME + REQUEST_TIME); + } + + private static class Bug50753Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public static final long THREAD_SLEEP_TIME = 5000; + + @Override + protected void doGet(HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + final AsyncContext ctx = req.startAsync(); + ctx.start(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(THREAD_SLEEP_TIME); + resp.setHeader("A", "xyz"); + resp.setContentType("text/plain"); + resp.setContentLength("OK".getBytes().length); + resp.getWriter().print("OK"); + ctx.complete(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + } + + @Test + public void testErrorHandling() throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + ErrorServlet error = new ErrorServlet(); + Tomcat.addServlet(ctx, "error", error); + ctx.addServletMappingDecoded("/error", "error"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/error"); + + int rc = getUrl(url.toString(), new ByteChunk(), null); + + Assert.assertEquals(500, rc); + + // Without this test may complete before access log has a chance to log + // the request + Thread.sleep(REQUEST_TIME); + + // Check the access log + alv.validateAccessLog(1, 500, 0, REQUEST_TIME); + } + + @Test + public void testCommitOnComplete() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncStatusServlet asyncStatusServlet = + new AsyncStatusServlet(HttpServletResponse.SC_BAD_REQUEST); + Wrapper wrapper = + Tomcat.addServlet(ctx, "asyncStatusServlet", asyncStatusServlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/asyncStatusServlet", "asyncStatusServlet"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/asyncStatusServlet"); + + int rc = getUrl(url.toString(), new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_BAD_REQUEST, rc); + + // Without this test may complete before access log has a chance to log + // the request + Thread.sleep(REQUEST_TIME); + + // Check the access log + alv.validateAccessLog(1, HttpServletResponse.SC_BAD_REQUEST, 0, + REQUEST_TIME); + + } + + private static class AsyncStatusServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private int status; + + AsyncStatusServlet(int status) { + this.status = status; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + AsyncContext actxt = req.startAsync(); + resp.setStatus(status); + actxt.complete(); + } + } + + @Test + public void testBug51197a() throws Exception { + doTestBug51197(false, false); + } + + @Test + public void testBug51197b() throws Exception { + doTestBug51197(true, false); + } + + @Test + public void testBug51197c() throws Exception { + doTestBug51197(false, true); + } + + @Test + public void testBug51197d() throws Exception { + doTestBug51197(true, true); + } + + private void doTestBug51197(boolean threaded, boolean customError) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncErrorServlet asyncErrorServlet = + new AsyncErrorServlet(HttpServletResponse.SC_BAD_REQUEST, threaded); + Wrapper wrapper = + Tomcat.addServlet(ctx, "asyncErrorServlet", asyncErrorServlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/asyncErrorServlet", "asyncErrorServlet"); + + if (customError) { + CustomErrorServlet customErrorServlet = new CustomErrorServlet(); + Tomcat.addServlet(ctx, "customErrorServlet", customErrorServlet); + ctx.addServletMappingDecoded("/customErrorServlet", "customErrorServlet"); + + ErrorPage ep = new ErrorPage(); + ep.setLocation("/customErrorServlet"); + + ctx.addErrorPage(ep); + } + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/asyncErrorServlet"); + + ByteChunk res = new ByteChunk(); + int rc = getUrl(url.toString(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_BAD_REQUEST, rc); + + // SRV 10.9.2 - Handling an error is entirely the application's + // responsibility when an error occurs on an application thread. + // Calling sendError() followed by complete() and expecting the standard + // error page mechanism to kick in could be viewed as handling the error + String responseBody = res.toString(); + Assert.assertNotNull(responseBody); + Assert.assertTrue(responseBody.length() > 0); + if (customError) { + Assert.assertTrue(responseBody, responseBody.contains(CustomErrorServlet.ERROR_MESSAGE)); + } else { + Assert.assertTrue(responseBody, responseBody.contains(AsyncErrorServlet.ERROR_MESSAGE)); + } + + // Without this test may complete before access log has a chance to log + // the request + Thread.sleep(REQUEST_TIME); + + // Check the access log + alv.validateAccessLog(1, HttpServletResponse.SC_BAD_REQUEST, 0, + REQUEST_TIME); + } + + private static class CustomErrorServlet extends GenericServlet { + + private static final long serialVersionUID = 1L; + + public static final String ERROR_MESSAGE = "Custom error page"; + + @Override + public void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + res.getWriter().print(ERROR_MESSAGE); + } + + } + + + private static class AsyncErrorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public static final String ERROR_MESSAGE = "It broke."; + + private int status; + private boolean threaded; + + AsyncErrorServlet(int status, boolean threaded) { + this.status = status; + this.threaded = threaded; + } + + @Override + protected void doGet(HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + + final AsyncContext actxt = req.startAsync(); + actxt.setTimeout(TIMEOUT); + if (threaded) { + actxt.start(new Runnable() { + @Override + public void run() { + try { + resp.sendError(status, ERROR_MESSAGE); + actxt.complete(); + } catch (IOException e) { + // Ignore + } + } + }); + } else { + resp.sendError(status, ERROR_MESSAGE); + actxt.complete(); + } + } + } + + @Test + public void testBug53337() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Wrapper a = Tomcat.addServlet(ctx, "ServletA", new Bug53337ServletA()); + a.setAsyncSupported(true); + Wrapper b = Tomcat.addServlet(ctx, "ServletB", new Bug53337ServletB()); + b.setAsyncSupported(true); + Tomcat.addServlet(ctx, "ServletC", new Bug53337ServletC()); + ctx.addServletMappingDecoded("/ServletA", "ServletA"); + ctx.addServletMappingDecoded("/ServletB", "ServletB"); + ctx.addServletMappingDecoded("/ServletC", "ServletC"); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/ServletA"); + + ByteChunk body = new ByteChunk(); + int rc = getUrl(url.toString(), body, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals("OK", body.toString()); + } + + private static class Bug53337ServletA extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + RequestDispatcher rd = req.getRequestDispatcher("/ServletB"); + rd.forward(req, resp); + } + } + + private static class Bug53337ServletB extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse resp) + throws ServletException, IOException { + + final AsyncContext async = req.startAsync(); + // Just for debugging + async.setTimeout(100000); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.submit(new Runnable() { + + @Override + public void run() { + async.dispatch("/ServletC"); + } + }); + executor.shutdown(); + } + } + + private static class Bug53337ServletC extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + } + } + + @Test + public void testBug53843() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Bug53843ServletA servletA = new Bug53843ServletA(); + Wrapper a = Tomcat.addServlet(ctx, "ServletA", servletA); + a.setAsyncSupported(true); + Tomcat.addServlet(ctx, "ServletB", new Bug53843ServletB()); + + ctx.addServletMappingDecoded("/ServletA", "ServletA"); + ctx.addServletMappingDecoded("/ServletB", "ServletB"); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/ServletA"); + + ByteChunk body = new ByteChunk(); + int rc = getUrl(url.toString(), body, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals("OK", body.toString()); + Assert.assertTrue(servletA.isAsyncWhenExpected()); + } + + private static class Bug53843ServletA extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private boolean isAsyncWhenExpected = true; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + // Should not be async at this point + isAsyncWhenExpected = isAsyncWhenExpected && !req.isAsyncStarted(); + + final AsyncContext async = req.startAsync(); + + // Should be async at this point + isAsyncWhenExpected = isAsyncWhenExpected && req.isAsyncStarted(); + + async.start(new Runnable() { + + @Override + public void run() { + // This should be delayed until the original container + // thread exists + async.dispatch("/ServletB"); + } + }); + + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new ServletException(e); + } + + // Should be async at this point + isAsyncWhenExpected = isAsyncWhenExpected && req.isAsyncStarted(); + } + + public boolean isAsyncWhenExpected() { + return isAsyncWhenExpected; + } + } + + private static class Bug53843ServletB extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + } + } + + @Test + public void testTimeoutErrorDispatchNone() throws Exception { + doTestTimeoutErrorDispatch(null, null); + } + + @Test + public void testTimeoutErrorDispatchNonAsync() throws Exception { + doTestTimeoutErrorDispatch(Boolean.FALSE, null); + } + + @Test + public void testTimeoutErrorDispatchAsyncStart() throws Exception { + doTestTimeoutErrorDispatch( + Boolean.TRUE, ErrorPageAsyncMode.NO_COMPLETE); + } + + @Test + public void testTimeoutErrorDispatchAsyncComplete() throws Exception { + doTestTimeoutErrorDispatch(Boolean.TRUE, ErrorPageAsyncMode.COMPLETE); + } + + @Test + public void testTimeoutErrorDispatchAsyncDispatch() throws Exception { + doTestTimeoutErrorDispatch(Boolean.TRUE, ErrorPageAsyncMode.DISPATCH); + } + + private void doTestTimeoutErrorDispatch(Boolean asyncError, + ErrorPageAsyncMode mode) throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + TimeoutServlet timeout = new TimeoutServlet(null, null); + Wrapper w1 = Tomcat.addServlet(ctx, "time", timeout); + w1.setAsyncSupported(true); + ctx.addServletMappingDecoded("/async", "time"); + + NonAsyncServlet nonAsync = new NonAsyncServlet(); + Wrapper w2 = Tomcat.addServlet(ctx, "nonAsync", nonAsync); + w2.setAsyncSupported(true); + ctx.addServletMappingDecoded("/error/nonasync", "nonAsync"); + + AsyncErrorPage asyncErrorPage = new AsyncErrorPage(mode); + Wrapper w3 = Tomcat.addServlet(ctx, "asyncErrorPage", asyncErrorPage); + w3.setAsyncSupported(true); + ctx.addServletMappingDecoded("/error/async", "asyncErrorPage"); + + if (asyncError != null) { + ErrorPage ep = new ErrorPage(); + ep.setErrorCode(500); + if (asyncError.booleanValue()) { + ep.setLocation("/error/async"); + } else { + ep.setLocation("/error/nonasync"); + } + + ctx.addErrorPage(ep); + } + + ctx.addApplicationListener(TrackingRequestListener.class.getName()); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + TesterAccessLogValve alvGlobal = new TesterAccessLogValve(); + tomcat.getHost().getPipeline().addValve(alvGlobal); + + tomcat.start(); + ByteChunk res = new ByteChunk(); + try { + getUrl("http://localhost:" + getPort() + "/async", res, null); + } catch (IOException ioe) { + // Ignore - expected for some error conditions + } + + StringBuilder expected = new StringBuilder(); + expected.append("requestInitialized-TimeoutServletGet-"); + if (asyncError != null) { + if (asyncError.booleanValue()) { + expected.append("AsyncErrorPageGet-"); + if (mode == ErrorPageAsyncMode.NO_COMPLETE){ + expected.append("NoOp-"); + } else if (mode == ErrorPageAsyncMode.COMPLETE) { + expected.append("Complete-"); + } else if (mode == ErrorPageAsyncMode.DISPATCH) { + expected.append("Dispatch-NonAsyncServletGet-"); + } + } else { + expected.append("NonAsyncServletGet-"); + } + } + expected.append("requestDestroyed"); + + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + String expectedTrack = expected.toString(); + int count = 0; + while (!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count ++; + } + Assert.assertEquals(expectedTrack, getTrack()); + + // Check the access log + alvGlobal.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT, + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + + REQUEST_TIME); + alv.validateAccessLog(1, 500, TimeoutServlet.ASYNC_TIMEOUT, + TimeoutServlet.ASYNC_TIMEOUT + TIMEOUT_MARGIN + + REQUEST_TIME); + } + + private enum ErrorPageAsyncMode { + NO_COMPLETE, + COMPLETE, + DISPATCH + } + + private static class AsyncErrorPage extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final ErrorPageAsyncMode mode; + + AsyncErrorPage(ErrorPageAsyncMode mode) { + this.mode = mode; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + track("AsyncErrorPageGet-"); + + final AsyncContext ctxt = req.getAsyncContext(); + + switch(mode) { + case COMPLETE: + track("Complete-"); + ctxt.complete(); + break; + case DISPATCH: + track("Dispatch-"); + ctxt.dispatch("/error/nonasync"); + break; + case NO_COMPLETE: + track("NoOp-"); + break; + default: + // Impossible + break; + } + } + } + + @Test + public void testBug54178() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Bug54178ServletA bug54178ServletA = new Bug54178ServletA(); + Wrapper wrapper = + Tomcat.addServlet(ctx, "bug54178ServletA", bug54178ServletA); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/bug54178ServletA", "bug54178ServletA"); + + Bug54178ServletB bug54178ServletB = new Bug54178ServletB(); + Tomcat.addServlet(ctx, "bug54178ServletB", bug54178ServletB); + ctx.addServletMappingDecoded("/bug54178ServletB", "bug54178ServletB"); + + tomcat.start(); + + ByteChunk body = new ByteChunk(); + int rc = -1; + + try { + rc = getUrl("http://localhost:" + getPort() + "/bug54178ServletA?" + + Bug54178ServletA.PARAM_NAME + "=bar", + body, null); + } catch (IOException ioe) { + // This may happen if test fails. Output the exception in case it is + // useful and let asserts handle the failure + ioe.printStackTrace(); + } + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + body.recycle(); + + rc = getUrl("http://localhost:" + getPort() + "/bug54178ServletB", + body, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals("OK", body.toString()); + } + + private static class Bug54178ServletA extends HttpServlet { + + public static final String PARAM_NAME = "foo"; + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + req.getParameter(PARAM_NAME); + AsyncContext actxt = req.startAsync(); + actxt.addListener(new Bug54178AsyncListener()); + actxt.complete(); + } + } + + private static class Bug54178ServletB extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + String result = req.getParameter(Bug54178ServletA.PARAM_NAME); + if (result == null) { + pw.write("OK"); + } else { + pw.write("FAIL"); + } + } + } + + private static class Bug54178AsyncListener implements AsyncListener { + + @Override + public void onComplete(AsyncEvent event) throws IOException { + throw new RuntimeException("Testing Bug54178"); + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onError(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + // NO-OP + } + } + + + @Test + public void testBug59219a() throws Exception{ + doTestBug59219("", "doGet-onError-onComplete-"); + } + + + @Test + public void testBug59219b() throws Exception{ + doTestBug59219("?loops=3", "doGet-doGet-onStartAsync-doGet-onStartAsync-onError-onComplete-"); + } + + + private void doTestBug59219(String queryString, String expectedTrack) throws Exception { + resetTracker(); + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + Bug59219Servlet bug59219Servlet = new Bug59219Servlet(); + Wrapper w = tomcat.addServlet("", "async", bug59219Servlet); + w.setAsyncSupported(true); + ctx.addServletMappingDecoded("/async", "async"); + + tomcat.start(); + + getUrl("http://localhost:" + getPort() + "/async" + queryString); + + // Wait up to 5s for the response + int count = 0; + while(!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count++; + } + + Assert.assertEquals(expectedTrack, getTrack()); + } + + + private static class Bug59219Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final transient TrackingListener trackingListener = new TrackingListener(true, false, "/async"); + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + track("doGet-"); + AsyncContext ctx = req.startAsync(); + ctx.setTimeout(3000); + ctx.addListener(trackingListener); + + String loopsParam = req.getParameter("loops"); + Integer loopsAttr = (Integer) req.getAttribute("loops"); + + int loops = 0; + if (loopsAttr != null) { + loops = loopsAttr.intValue(); + } else if (loopsParam != null) { + loops = Integer.parseInt(loopsParam); + } + + if (loops > 1) { + loops--; + req.setAttribute("loops", Integer.valueOf(loops)); + ctx.dispatch(); + } else { + throw new ServletException(); + } + } + } + + @Test + public void testForbiddenDispatching() throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + NonAsyncServlet nonAsyncServlet = new NonAsyncServlet(); + Wrapper wrapper = Tomcat.addServlet(ctx, "nonAsyncServlet", + nonAsyncServlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/target", "nonAsyncServlet"); + + DispatchingGenericServlet forbiddenDispatchingServlet = new DispatchingGenericServlet(); + Wrapper wrapper1 = Tomcat.addServlet(ctx, + "forbiddenDispatchingServlet", forbiddenDispatchingServlet); + wrapper1.setAsyncSupported(true); + ctx.addServletMappingDecoded("/forbiddenDispatchingServlet", + "forbiddenDispatchingServlet"); + + tomcat.start(); + + try { + getUrl("http://localhost:" + getPort() + + "/forbiddenDispatchingServlet"); + } catch (IOException ioe) { + // This may happen if test fails. Output the exception in case it is + // useful and let asserts handle the failure + ioe.printStackTrace(); + } + + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + String expectedTrack = "OKNonAsyncServletGet-"; + int count = 0; + while (!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count ++; + } + Assert.assertEquals(expectedTrack, getTrack()); + } + + private static class DispatchingGenericServlet extends GenericServlet { + + private static final long serialVersionUID = 1L; + private static final String CUSTOM_REQ_RESP = "crr"; + private static final String EMPTY_DISPATCH = "empty"; + + @Override + public void service(ServletRequest req, ServletResponse resp) + throws ServletException, IOException { + if (DispatcherType.ASYNC != req.getDispatcherType()) { + AsyncContext asyncContext; + if ("y".equals(req.getParameter(CUSTOM_REQ_RESP))) { + asyncContext = req.startAsync( + new ServletRequestWrapper(req), + new ServletResponseWrapper(resp)); + } else { + asyncContext = req.startAsync(); + } + if ("y".equals(req.getParameter(EMPTY_DISPATCH))) { + asyncContext.dispatch(); + } else { + asyncContext.dispatch("/target"); + } + try { + asyncContext.dispatch("/nonExistingServlet"); + track("FAIL"); + } catch (IllegalStateException e) { + track("OK"); + } + } else { + track("DispatchingGenericServletGet-"); + } + } + } + + + @Test + public void testGetRequestISE() throws Exception { + doTestAsyncISE(true); + } + + + @Test + public void testGetResponseISE() throws Exception { + doTestAsyncISE(false); + } + + + private void doTestAsyncISE(boolean useGetRequest) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncISEServlet servlet = new AsyncISEServlet(); + + Wrapper w = Tomcat.addServlet(ctx, "AsyncISEServlet", servlet); + w.setAsyncSupported(true); + ctx.addServletMappingDecoded("/test", "AsyncISEServlet"); + + tomcat.start(); + + ByteChunk response = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() +"/test", response, + null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + boolean hasIse = false; + try { + if (useGetRequest) { + servlet.getAsyncContext().getRequest(); + } else { + servlet.getAsyncContext().getResponse(); + } + } catch (IllegalStateException ise) { + hasIse = true; + } + + Assert.assertTrue(hasIse); + } + + + /** + * Accessing the AsyncContext in this way is an ugly hack that should never + * be used in a real application since it is not thread safe. That said, it + * is this sort of hack that the ISE is meant to be preventing. + * + */ + private static class AsyncISEServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private transient AsyncContext asyncContext; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain;UTF-8"); + + asyncContext = req.startAsync(); + // This will commit the response + asyncContext.complete(); + } + + public AsyncContext getAsyncContext() { + return asyncContext; + } + } + + + @Test + public void testDispatchWithCustomRequestResponse() throws Exception { + prepareApplicationWithGenericServlet(""); + + StringBuilder expected = new StringBuilder(); + expected.append("OK"); + expected.append("CustomGenericServletGet-"); + requestApplicationWithGenericServlet("/dispatch?crr=y", expected); + + expected = new StringBuilder(); + expected.append("OK"); + expected.append("DispatchingGenericServletGet-"); + requestApplicationWithGenericServlet("/dispatch?crr=y&empty=y", + expected); + } + + private static class CustomGenericServlet extends GenericServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + if (req instanceof ServletRequestWrapper + && res instanceof ServletResponseWrapper) { + track("CustomGenericServletGet-"); + } + } + + } + + @Test + public void testEmptyDispatch() throws Exception { + prepareApplicationWithGenericServlet("/fo o"); + StringBuilder expected = new StringBuilder(); + expected.append("OK"); + expected.append("DispatchingGenericServletGet-"); + requestApplicationWithGenericServlet("/fo%20o/dispatch?empty=y", + expected); + requestApplicationWithGenericServlet("//fo%20o/dispatch?empty=y", + expected); + requestApplicationWithGenericServlet("/./fo%20o/dispatch?empty=y", + expected); + requestApplicationWithGenericServlet("/fo%20o//dispatch?empty=y", + expected); + requestApplicationWithGenericServlet("/fo%20o/./dispatch?empty=y", + expected); + requestApplicationWithGenericServlet("/fo%20o/c/../dispatch?empty=y", + expected); + } + + @Test + public void testEmptyDispatchWithCustomRequestResponse() throws Exception { + prepareApplicationWithGenericServlet("/fo o"); + StringBuilder expected = new StringBuilder(); + expected.append("OK"); + expected.append("DispatchingGenericServletGet-"); + requestApplicationWithGenericServlet("/fo%20o/dispatch?crr=y&empty=y", + expected); + requestApplicationWithGenericServlet("//fo%20o/dispatch?crr=y&empty=y", + expected); + requestApplicationWithGenericServlet( + "/./fo%20o/dispatch?crr=y&empty=y", expected); + requestApplicationWithGenericServlet("/fo%20o//dispatch?crr=y&empty=y", + expected); + requestApplicationWithGenericServlet( + "/fo%20o/./dispatch?crr=y&empty=y", expected); + requestApplicationWithGenericServlet( + "/fo%20o/c/../dispatch?crr=y&empty=y", expected); + } + + private void prepareApplicationWithGenericServlet(String contextPath) + throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext(contextPath, null); + ctx.setAllowMultipleLeadingForwardSlashInPath(true); + + DispatchingGenericServlet dispatch = new DispatchingGenericServlet(); + Wrapper wrapper = Tomcat.addServlet(ctx, "dispatch", dispatch); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/dispatch", "dispatch"); + + CustomGenericServlet customGeneric = new CustomGenericServlet(); + Wrapper wrapper2 = Tomcat.addServlet(ctx, "customGeneric", + customGeneric); + wrapper2.setAsyncSupported(true); + ctx.addServletMappingDecoded("/target", "customGeneric"); + + tomcat.start(); + } + + private void requestApplicationWithGenericServlet(String path, + StringBuilder expectedContent) throws Exception { + resetTracker(); + getUrl("http://localhost:" + getPort() + path); + + // Request may complete before listener has finished processing so wait + // up to 5 seconds for the right response + String expectedTrack = expectedContent.toString(); + int count = 0; + while (!expectedTrack.equals(getTrack()) && count < 100) { + Thread.sleep(50); + count ++; + } + Assert.assertEquals(expectedTrack, getTrack()); + } + + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=57326 + @Test + public void testAsyncContextListenerClearing() throws Exception { + resetTracker(); + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Servlet stage1 = new DispatchingServletTracking("/stage2", true); + Wrapper wrapper1 = Tomcat.addServlet(ctx, "stage1", stage1); + wrapper1.setAsyncSupported(true); + ctx.addServletMappingDecoded("/stage1", "stage1"); + + Servlet stage2 = new DispatchingServletTracking("/stage3", false); + Wrapper wrapper2 = Tomcat.addServlet(ctx, "stage2", stage2); + wrapper2.setAsyncSupported(true); + ctx.addServletMappingDecoded("/stage2", "stage2"); + + Servlet stage3 = new NonAsyncServlet(); + Tomcat.addServlet(ctx, "stage3", stage3); + ctx.addServletMappingDecoded("/stage3", "stage3"); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + tomcat.start(); + + getUrl("http://localhost:" + getPort()+ "/stage1"); + + Assert.assertEquals("doGet-startAsync-doGet-startAsync-onStartAsync-NonAsyncServletGet-onComplete-", getTrack()); + + // Check the access log + alv.validateAccessLog(1, 200, 0, REQUEST_TIME); + } + + private static class DispatchingServletTracking extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final String target; + private final boolean addTrackingListener; + + DispatchingServletTracking(String target, boolean addTrackingListener) { + this.target = target; + this.addTrackingListener = addTrackingListener; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + track("doGet-startAsync-"); + AsyncContext ac = req.startAsync(); + if (addTrackingListener) { + ac.addListener(new StickyTrackingListener(false, false, null)); + } + ac.dispatch(target); + } + } + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=57559 + @Test + public void testAsyncRequestURI_24() throws Exception { + // '$' is permitted in a path + doTestAsyncRequestURI("/foo/$/bar"); + } + + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=60722 + @Test + public void testAsyncRequestURI_25() throws Exception { + doTestAsyncRequestURI("/foo/%25/bar"); + } + + + private void doTestAsyncRequestURI(String uri) throws Exception{ + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Servlet servlet = new AsyncRequestUriServlet(); + Wrapper wrapper1 = Tomcat.addServlet(ctx, "bug57559", servlet); + wrapper1.setAsyncSupported(true); + ctx.addServletMappingDecoded("/", "bug57559"); + + tomcat.start(); + + ByteChunk body = getUrl("http://localhost:" + getPort() + uri); + + Assert.assertEquals(uri, body.toString()); + } + + private static class AsyncRequestUriServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (DispatcherType.ASYNC.equals(req.getDispatcherType())) { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write(req.getRequestURI()); + } else { + req.startAsync().dispatch(); + } + } + } + + @Test + public void testDispatchFromOtherContainerThread() throws Exception { + resetTracker(); + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + NonAsyncServlet nonAsyncServlet = new NonAsyncServlet(); + Tomcat.addServlet(ctx, "nonAsyncServlet", nonAsyncServlet); + ctx.addServletMappingDecoded("/target", "nonAsyncServlet"); + + AsyncStashServlet asyncStashServlet = new AsyncStashServlet(); + Wrapper w1 = Tomcat.addServlet(ctx, "asyncStashServlet", asyncStashServlet); + w1.setAsyncSupported(true); + ctx.addServletMappingDecoded("/asyncStashServlet", "asyncStashServlet"); + + AsyncRetrieveServlet asyncRetrieveServlet = new AsyncRetrieveServlet(); + Wrapper w2 = Tomcat.addServlet(ctx, "asyncRetrieveServlet", asyncRetrieveServlet); + w2.setAsyncSupported(true); + ctx.addServletMappingDecoded("/asyncRetrieveServlet", "asyncRetrieveServlet"); + + tomcat.start(); + + // First request in separate thread because the response won't be + // written until after the second request has been made. + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + getUrl("http://localhost:" + getPort() + "/asyncStashServlet"); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + t.start(); + + // Wait for first request to get as far as it can + int count = 0; + while (count < 100 && getTrack() != null && + !getTrack().startsWith("AsyncStashServletGet-")) { + count++; + Thread.sleep(100); + } + + getUrl("http://localhost:" + getPort() + "/asyncRetrieveServlet"); + + // Wait for second request to release first and allow it to complete + String expectedTrack = "AsyncStashServletGet-AsyncRetrieveServletGet-NonAsyncServletGet-"; + count = 0; + while (count < 100 && !getTrack().equals(expectedTrack)) { + count++; + Thread.sleep(100); + } + + Assert.assertEquals(expectedTrack, getTrack()); + } + + private static class AsyncStashServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String DEFAULT_KEY = "DEFAULT"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + String key = req.getParameter("key"); + if (key == null) { + key = DEFAULT_KEY; + } + + req.getServletContext().setAttribute(key, req.startAsync()); + track("AsyncStashServletGet-"); + } + } + + private static class AsyncRetrieveServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + private static final String DEFAULT_KEY = "DEFAULT"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + String key = req.getParameter("key"); + if (key == null) { + key = DEFAULT_KEY; + } + + AsyncContext ac = (AsyncContext) req.getServletContext().getAttribute(key); + if (ac == null) { + track("FAIL:nullAsyncContext-"); + } else { + track("AsyncRetrieveServletGet-"); + ac.dispatch("/target"); + } + } + } + + + /* + * See https://bz.apache.org/bugzilla/show_bug.cgi?id=58751 comment 1 + */ + @Test + public void testTimeoutDispatchCustomErrorPage() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context context = getProgrammaticRootContext(); + tomcat.addServlet("", "timeout", Bug58751AsyncServlet.class.getName()) + .setAsyncSupported(true); + CustomErrorServlet customErrorServlet = new CustomErrorServlet(); + Tomcat.addServlet(context, "customErrorServlet", customErrorServlet); + context.addServletMappingDecoded("/timeout", "timeout"); + context.addServletMappingDecoded("/error", "customErrorServlet"); + ErrorPage errorPage = new ErrorPage(); + errorPage.setLocation("/error"); + context.addErrorPage(errorPage); + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/timeout", responseBody, null); + + Assert.assertEquals(503, rc); + Assert.assertEquals(CustomErrorServlet.ERROR_MESSAGE, responseBody.toString()); + } + + + public static class Bug58751AsyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (req.getAttribute("timeout") != null) { + resp.sendError(503); + } else { + final AsyncContext context = req.startAsync(); + context.setTimeout(5000); + context.addListener(new AsyncListener() { + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + HttpServletResponse response = (HttpServletResponse) event + .getSuppliedResponse(); + if (!response.isCommitted()) { + event.getSuppliedRequest() + .setAttribute("timeout", Boolean.TRUE); + context.dispatch(); + } + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + } + + @Override + public void onError(AsyncEvent event) throws IOException { + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + } + }); + } + } + + } + + + @Test + public void testAsyncListenerSupplyRequestResponse() { + final ServletRequest servletRequest = EasyMock.createMock(ServletRequest.class); + final ServletResponse servletResponse = EasyMock.createMock(ServletResponse.class); + final AsyncListener listener = new AsyncListener() { + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + checkRequestResponse(event); + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + checkRequestResponse(event); + } + + @Override + public void onError(AsyncEvent event) throws IOException { + checkRequestResponse(event); + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + checkRequestResponse(event); + } + + private void checkRequestResponse(AsyncEvent event) { + Assert.assertEquals(servletRequest, event.getSuppliedRequest()); + Assert.assertEquals(servletResponse, event.getSuppliedResponse()); + } + }; + final Context context = new TesterContext(); + final Response response = new Response(); + final Request request = new Request(null); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.getMappingData().context = context; + final AsyncContextImpl ac = new AsyncContextImpl(request); + ac.addListener(listener, servletRequest, servletResponse); + ac.setStarted(context, request, response, true); + ac.addListener(listener, servletRequest, servletResponse); + ac.setErrorState(new Exception(), true); + ac.fireOnComplete(); + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=59317 + */ + @Test + public void testAsyncDispatchUrlWithSpaces() throws Exception { + doTestDispatchWithSpaces(true); + } + + + @Test + public void testForwardDispatchUrlWithSpaces() throws Exception { + doTestDispatchWithSpaces(false); + } + + + private void doTestDispatchWithSpaces(boolean async) throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context context = getProgrammaticRootContext(); + if (async) { + Servlet s = new AsyncDispatchUrlWithSpacesServlet(); + Wrapper w = Tomcat.addServlet(context, "space", s); + w.setAsyncSupported(true); + } else { + Tomcat.addServlet(context, "space", new ForwardDispatchUrlWithSpacesServlet()); + } + context.addServletMappingDecoded("/space/*", "space"); + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/sp%61ce/foo%20bar", responseBody, null); + + Assert.assertEquals(200, rc); + } + + + private static class AsyncDispatchUrlWithSpacesServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + Integer countObj = (Integer) req.getAttribute("count"); + int count = 0; + if (countObj != null) { + count = countObj.intValue(); + } + count++; + req.setAttribute("count", Integer.valueOf(count)); + + String encodedUri = req.getRequestURI(); + + try { + // Just here to trigger the error + @SuppressWarnings("unused") + URI u = new URI(encodedUri); + } catch (URISyntaxException e) { + throw new ServletException(e); + } + + if (count > 3) { + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + } else { + AsyncContext ac = req.startAsync(); + ac.dispatch(encodedUri); + } + } + } + + + private static class ForwardDispatchUrlWithSpacesServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + Integer countObj = (Integer) req.getAttribute("count"); + int count = 0; + if (countObj != null) { + count = countObj.intValue(); + } + count++; + req.setAttribute("count", Integer.valueOf(count)); + + String encodedUri = req.getRequestURI(); + + try { + // Just here to trigger the error + @SuppressWarnings("unused") + URI u = new URI(req.getRequestURI()); + } catch (URISyntaxException e) { + throw new ServletException(e); + } + + if (count > 3) { + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + } else { + RequestDispatcher rd = req.getRequestDispatcher(encodedUri); + rd.forward(req, resp); + } + } + } + + + @Test + public void testBug61185() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + tomcat.getConnector().setEncodedSolidusHandling(EncodedSolidusHandling.DECODE.getValue()); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + EncodedDispatchServlet encodedDispatchServlet = new EncodedDispatchServlet(); + Wrapper wrapper = Tomcat.addServlet(ctx, "encodedDispatchServlet", encodedDispatchServlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/*", "encodedDispatchServlet"); + + tomcat.start(); + + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + EncodedDispatchServlet.ENCODED_URI, body, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals("OK", body.toString()); + } + + + private static final class EncodedDispatchServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final String ENCODED_URI = "/foo/vv%2F1234/add/2"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (DispatcherType.ASYNC == req.getDispatcherType()) { + if (ENCODED_URI.equals(req.getRequestURI())) { + resp.getWriter().print("OK"); + } else { + resp.getWriter().print("FAIL"); + } + } else { + AsyncContext ac = req.startAsync(); + ac.dispatch(); + } + } + + } + + + @Test + public void testAsyncIoEnd00() throws Exception { + doTestAsyncIoEnd(false, false); + } + + + @Test + public void testAsyncIoEnd01() throws Exception { + doTestAsyncIoEnd(false, true); + } + + + @Test + public void testAsyncIoEnd02() throws Exception { + doTestAsyncIoEnd(true, false); + } + + + @Test + public void testAsyncIoEnd03() throws Exception { + doTestAsyncIoEnd(true, true); + } + + + private void doTestAsyncIoEnd(boolean useThread, boolean useComplete) throws Exception { + LogManager.getLogManager().getLogger("org.apache.coyote").setLevel(Level.ALL); + try { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncIoEndServlet asyncIoEndServlet = new AsyncIoEndServlet(useThread, useComplete); + Wrapper wrapper = Tomcat.addServlet(ctx, "asyncIoEndServlet", asyncIoEndServlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/asyncIoEndServlet", "asyncIoEndServlet"); + + SimpleServlet simpleServlet = new SimpleServlet(); + Tomcat.addServlet(ctx, "simpleServlet", simpleServlet); + ctx.addServletMappingDecoded("/simpleServlet", "simpleServlet"); + + tomcat.start(); + + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/asyncIoEndServlet", body, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals("OK", body.toString()); + + Assert.assertFalse(asyncIoEndServlet.getInvalidStateDetected()); + } finally { + LogManager.getLogManager().getLogger("org.apache.coyote").setLevel(Level.INFO); + } + } + + + private static class AsyncIoEndServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean useThread; + private final boolean useComplete; + private transient AsyncIoEndWriteListener asyncIoEndWriteListener; + + AsyncIoEndServlet(boolean useThread, boolean useComplete) { + this.useThread = useThread; + this.useComplete = useComplete; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (useComplete) { + // Write expected body here + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getOutputStream().write("OK".getBytes(StandardCharsets.UTF_8)); + } + AsyncContext ac = req.startAsync(); + ServletOutputStream sos = resp.getOutputStream(); + asyncIoEndWriteListener = new AsyncIoEndWriteListener(ac, useThread, useComplete); + sos.setWriteListener(asyncIoEndWriteListener); + } + + public boolean getInvalidStateDetected() { + if (asyncIoEndWriteListener != null) { + return asyncIoEndWriteListener.getInvalidStateDetected(); + } + return false; + } + } + + + private static class AsyncIoEndWriteListener implements WriteListener { + + private final AsyncContext ac; + private final boolean useThread; + private final boolean useComplete; + private boolean invalidStateDetected = false; + + AsyncIoEndWriteListener(AsyncContext ac, boolean useThread, + boolean useComplete) { + this.ac = ac; + this.useThread = useThread; + this.useComplete = useComplete; + } + + + @Override + public void onWritePossible() throws IOException { + if (useThread) { + (new Thread() { + @Override + public void run() { + doOnWritePossible(); + } + }).start(); + } else { + doOnWritePossible(); + } + } + + + public void doOnWritePossible() { + // Hack to avoid ISE if we try getting the request after complete/dispatch + ServletRequest req = ac.getRequest(); + if (useComplete) { + ac.complete(); + } else { + ac.dispatch("/simpleServlet"); + } + if (!useThread && req.isAsyncStarted()) { + invalidStateDetected = true; + } + } + + @Override + public void onError(Throwable throwable) { + throw new RuntimeException(throwable); + } + + + public boolean getInvalidStateDetected() { + return invalidStateDetected; + } + } + + + private static class SimpleServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + // Write expected body here + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getOutputStream().write("OK".getBytes(StandardCharsets.UTF_8)); + } + } + + + /* + * Tests an error on an async thread before the container thread that called + * startAsync() has returned to the container. + * + * Required sequence is: + * - enter Servlet's service() method + * - startAsync() + * - start async thread + * - close client connection + * - write on async thread -> I/O error + * - exit Servlet's service() method + * + * This test makes extensive use of instance fields in the Servlet that + * would normally be considered very poor practice. It is only safe in this + * test as the Servlet only processes a single request. + */ + @Test + public void testBug63816() throws Exception { + CountDownLatch doGetLatch = new CountDownLatch(1); + CountDownLatch clientCloseLatch = new CountDownLatch(1); + CountDownLatch threadCompleteLatch = new CountDownLatch(1); + + AtomicBoolean ise = new AtomicBoolean(true); + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Bug63816Servlet bug63816Servlet = new Bug63816Servlet(doGetLatch, clientCloseLatch, threadCompleteLatch, ise); + Wrapper wrapper = Tomcat.addServlet(ctx, "bug63816Servlet", bug63816Servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/*", "bug63816Servlet"); + + tomcat.start(); + + Bug63816Client client = new Bug63816Client(); + client.setPort(getPort()); + client.setRequest(new String[] { "GET / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF}); + client.connect(); + client.sendRequest(); + + // Wait for async to start + doGetLatch.await(); + + client.disconnect(); + + clientCloseLatch.countDown(); + + threadCompleteLatch.await(); + + Assert.assertFalse(ise.get()); + } + + + private static final class Bug63816Client extends SimpleHttpClient { + + @Override + public boolean isResponseBodyOK() { + return true; + } + } + + + private static final class Bug63816Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final transient CountDownLatch doGetLatch; + private final transient CountDownLatch clientCloseLatch; + private final transient CountDownLatch threadCompleteLatch; + private final AtomicBoolean ise; + + Bug63816Servlet(CountDownLatch doGetLatch, CountDownLatch clientCloseLatch, + CountDownLatch threadCompleteLatch, AtomicBoolean ise) { + this.doGetLatch = doGetLatch; + this.clientCloseLatch = clientCloseLatch; + this.threadCompleteLatch = threadCompleteLatch; + this.ise = ise; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + doGetLatch.countDown(); + + AsyncContext ac = req.startAsync(); + Thread t = new Bug63816Thread(ac, clientCloseLatch, threadCompleteLatch, ise); + t.start(); + + try { + threadCompleteLatch.await(); + } catch (InterruptedException e) { + // Ignore + } + } + } + + + private static final class Bug63816Thread extends Thread { + + private final AsyncContext ac; + private final CountDownLatch clientCloseLatch; + private final CountDownLatch threadCompleteLatch; + private final AtomicBoolean ise; + + Bug63816Thread(AsyncContext ac, CountDownLatch clientCloseLatch, CountDownLatch threadCompleteLatch, + AtomicBoolean ise) { + this.ac = ac; + this.clientCloseLatch = clientCloseLatch; + this.threadCompleteLatch = threadCompleteLatch; + this.ise = ise; + } + + @Override + public void run() { + try { + // Wait for client to close connection + clientCloseLatch.await(); + + try { + ServletResponse resp = ac.getResponse(); + resp.setContentType("text/plain"); + for (int i = 0; i < 4; i++) { + resp.getWriter().write(TestCoyoteAdapter.TEXT_8K); + resp.flushBuffer(); + } + } catch (IOException e) { + // Ignore + } + ise.set(false); + } catch (InterruptedException e) { + // Ignore + } finally { + threadCompleteLatch.countDown(); + } + } + } + + + @Test + public void testCanceledPostChunked() throws Exception { + doTestCanceledPost(new String[] { + "POST / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + SimpleHttpClient.CRLF + + "Transfer-Encoding: Chunked" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "10" + SimpleHttpClient.CRLF + + "This is 16 bytes" + SimpleHttpClient.CRLF + }); + } + + + @Test + public void testCanceledPostNoChunking() throws Exception { + doTestCanceledPost(new String[] { + "POST / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + SimpleHttpClient.CRLF + + "Content-Length: 100" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "This is 16 bytes" + }); + } + + + /* + * Tests an error on an async thread when the client closes the connection + * before fully writing the request body. + * + * Required sequence is: + * - enter Servlet's service() method + * - startAsync() + * - start async thread + * - read partial body + * - close client connection + * - read on async thread -> I/O error + * - exit Servlet's service() method + * + * This test makes extensive use of instance fields in the Servlet that + * would normally be considered very poor practice. It is only safe in this + * test as the Servlet only processes a single request. + */ + private void doTestCanceledPost(String[] request) throws Exception { + CountDownLatch partialReadLatch = new CountDownLatch(1); + CountDownLatch clientCloseLatch = new CountDownLatch(1); + CountDownLatch threadCompleteLatch = new CountDownLatch(1); + + AtomicBoolean testFailed = new AtomicBoolean(true); + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + PostServlet postServlet = new PostServlet(partialReadLatch, clientCloseLatch, threadCompleteLatch, testFailed); + Wrapper wrapper = Tomcat.addServlet(ctx, "postServlet", postServlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/*", "postServlet"); + + tomcat.start(); + + PostClient client = new PostClient(); + client.setPort(getPort()); + client.setRequest(request); + client.connect(); + client.sendRequest(); + + // Wait server to read partial request body + partialReadLatch.await(); + + client.disconnect(); + + clientCloseLatch.countDown(); + + threadCompleteLatch.await(); + + Assert.assertFalse(testFailed.get()); + } + + + private static final class PostClient extends SimpleHttpClient { + + @Override + public boolean isResponseBodyOK() { + return true; + } + } + + + private static final class PostServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final transient CountDownLatch partialReadLatch; + private final transient CountDownLatch clientCloseLatch; + private final transient CountDownLatch threadCompleteLatch; + private final AtomicBoolean testFailed; + + PostServlet(CountDownLatch doPostLatch, CountDownLatch clientCloseLatch, + CountDownLatch threadCompleteLatch, AtomicBoolean testFailed) { + this.partialReadLatch = doPostLatch; + this.clientCloseLatch = clientCloseLatch; + this.threadCompleteLatch = threadCompleteLatch; + this.testFailed = testFailed; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + AsyncContext ac = req.startAsync(); + Thread t = new PostServletThread(ac, partialReadLatch, clientCloseLatch, threadCompleteLatch, testFailed); + t.start(); + + try { + threadCompleteLatch.await(); + } catch (InterruptedException e) { + // Ignore + } + } + } + + + private static final class PostServletThread extends Thread { + + private final AsyncContext ac; + private final CountDownLatch partialReadLatch; + private final CountDownLatch clientCloseLatch; + private final CountDownLatch threadCompleteLatch; + private final AtomicBoolean testFailed; + + PostServletThread(AsyncContext ac, CountDownLatch partialReadLatch, CountDownLatch clientCloseLatch, + CountDownLatch threadCompleteLatch, AtomicBoolean testFailed) { + this.ac = ac; + this.partialReadLatch = partialReadLatch; + this.clientCloseLatch = clientCloseLatch; + this.threadCompleteLatch = threadCompleteLatch; + this.testFailed = testFailed; + } + + @Override + public void run() { + try { + int bytesRead = 0; + byte[] buffer = new byte[32]; + InputStream is = null; + + try { + is = ac.getRequest().getInputStream(); + + // Read the partial request body + while (bytesRead < 16) { + int read = is.read(buffer); + if (read == -1) { + // Error condition + return; + } + bytesRead += read; + } + } catch (IOException ioe) { + // Error condition + return; + } finally { + partialReadLatch.countDown(); + } + + // Wait for client to close connection + clientCloseLatch.await(); + + // Read again + try { + is.read(); + } catch (IOException e) { + e.printStackTrace(); + // Required. Clear the error marker. + testFailed.set(false); + } + } catch (InterruptedException e) { + // Ignore + } finally { + threadCompleteLatch.countDown(); + } + } + } +} diff --git a/test/org/apache/catalina/core/TestAsyncContextImplDispatch.java b/test/org/apache/catalina/core/TestAsyncContextImplDispatch.java new file mode 100644 index 0000000..db65c8b --- /dev/null +++ b/test/org/apache/catalina/core/TestAsyncContextImplDispatch.java @@ -0,0 +1,151 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Written for the specific test case of async Servlet, dispatches to sync + * Servlet that then tries to call startAsync() but covers all combinations + * for completeness. + */ +@RunWith(Parameterized.class) +public class TestAsyncContextImplDispatch extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{index}: tgt-sup [{0}], dis-sup [{1}], dis-st [{2}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + for (Boolean targetAsyncSupported : booleans) { + for (Boolean dispatchAsyncSupported : booleans) { + for (Boolean dispatchAsyncStart : booleans) { + Boolean allowed = Boolean.valueOf(!dispatchAsyncStart.booleanValue() || + targetAsyncSupported.booleanValue() && dispatchAsyncSupported.booleanValue() && + dispatchAsyncStart.booleanValue()); + + parameterSets.add(new Object[] { targetAsyncSupported, dispatchAsyncSupported, dispatchAsyncStart, allowed} ); + } + } + } + + return parameterSets; + } + + @Parameter(0) + public boolean targetAsyncSupported; + @Parameter(1) + public boolean dispatchAsyncSupported; + @Parameter(2) + public boolean dispatchAsyncStart; + @Parameter(3) + public boolean allowed; + + + @Test + public void testSendError() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Wrapper w1 = Tomcat.addServlet(ctx, "target", new TesterServlet()); + w1.setAsyncSupported(targetAsyncSupported); + ctx.addServletMappingDecoded("/target", "target"); + + Wrapper w2 = Tomcat.addServlet(ctx, "dispatch", new TesterDispatchServlet(dispatchAsyncStart)); + w2.setAsyncSupported(dispatchAsyncSupported); + ctx.addServletMappingDecoded("/dispatch", "dispatch"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + + rc = getUrl("http://localhost:" + getPort() + "/target", bc, null, null); + + String body = bc.toString(); + + if (allowed) { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", body); + } else { + Assert.assertEquals(500, rc); + } + } + + + public static class TesterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + req.getRequestDispatcher("/dispatch").forward(req, resp); + } + } + + + public static class TesterDispatchServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean start; + + public TesterDispatchServlet(boolean start) { + this.start = start; + } + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (start) { + AsyncContext ac = req.startAsync(); + ac.complete(); + } + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write("OK"); + } + } +} diff --git a/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java b/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java new file mode 100644 index 0000000..dfdc33b --- /dev/null +++ b/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java @@ -0,0 +1,182 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestAsyncContextImplListenerOnComplete extends TomcatBaseTest { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=68227 + */ + @Test + public void testAfterNetworkErrorThenDispatch() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Latch to track that complete has been called + CountDownLatch completeLatch = new CountDownLatch(1); + + Wrapper servletWrapper = tomcat.addServlet("", "repro-servlet", new ReproServlet(completeLatch)); + servletWrapper.addMapping("/repro"); + servletWrapper.setAsyncSupported(true); + servletWrapper.setLoadOnStartup(1); + + ctx.addServletMappingDecoded("/", "repro-servlet"); + + tomcat.start(); + Thread.sleep(2000); + + triggerBrokenPipe(getPort()); + + Assert.assertTrue(completeLatch.await(30, TimeUnit.SECONDS)); + } + + + private void triggerBrokenPipe(int port) throws IOException, InterruptedException { + try (Socket socket = new Socket()) { + socket.setReceiveBufferSize(1); + socket.connect(new InetSocketAddress("localhost", port)); + + try (var writer = new OutputStreamWriter(socket.getOutputStream())) { + writer.write("GET /repro" + SimpleHttpClient.CRLF + + "Accept: text/event-stream" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF); + writer.flush(); + } + Thread.sleep(1_000); + } + } + + + private static class ReproServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private final EventSource eventSource = new EventSource(); + + private final CountDownLatch completeLatch; + + ReproServlet(CountDownLatch completeLatch) { + this.completeLatch = completeLatch; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + resp.setContentType("text/event-stream"); + resp.setCharacterEncoding("UTF-8"); + resp.setStatus(200); + AsyncContext context = req.startAsync(); + context.addListener(new ReproAsyncListener()); + eventSource.add(context); + } + + private class ReproAsyncListener implements AsyncListener { + @Override + public void onComplete(AsyncEvent event) { + try { + eventSource.remove(event.getAsyncContext()); + } finally { + completeLatch.countDown(); + } + } + + @Override + public void onTimeout(AsyncEvent event) { + // Not expected + new AssertionError("onTimeout").printStackTrace(); + } + + @Override + public void onError(AsyncEvent event) { + event.getAsyncContext().dispatch(); // Mirroring Spring's behaviour + } + + @Override + public void onStartAsync(AsyncEvent event) { + // NO-OP + } + } + } + + + private static class EventSource { + + private final Set contexts = new HashSet<>(); + + private EventSource() { + Runnable r = () -> { + while (true) { + try { + Thread.sleep(2000); + send("PING"); + } catch (InterruptedException e) { + System.out.println("Failed to sleep: " + e); + } + } + }; + Thread t = new Thread(r); + t.start(); + } + + public void send(String message) { + for (AsyncContext context : contexts) { + try { + PrintWriter writer = context.getResponse().getWriter(); + writer.write("event: " + message + "\n\n"); + writer.flush(); + } catch (Exception e) { + System.out.println("Failed to send event: " + e); + } + } + } + + public void add(AsyncContext context) { + contexts.add(context); + } + + public void remove(AsyncContext context) { + contexts.remove(context); + } + } +} diff --git a/test/org/apache/catalina/core/TestAsyncContextStateChanges.java b/test/org/apache/catalina/core/TestAsyncContextStateChanges.java new file mode 100644 index 0000000..0d8c7dc --- /dev/null +++ b/test/org/apache/catalina/core/TestAsyncContextStateChanges.java @@ -0,0 +1,377 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.TestCoyoteAdapter; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +/* + * Derived from a test for https://bz.apache.org/bugzilla/show_bug.cgi?id=63816 + * Expanded to cover https://bz.apache.org/bugzilla/show_bug.cgi?id=63817 and + * additional scenarios. + */ +@RunWith(Parameterized.class) +public class TestAsyncContextStateChanges extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{index}: end [{0}], timing [{1}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + for (AsyncEnd asyncEnd : AsyncEnd.values()) { + for (EndTiming endTiming : EndTiming.values()) { + parameterSets.add(new Object[] { asyncEnd, endTiming }); + } + } + return parameterSets; + } + + @Parameter(0) + public AsyncEnd asyncEnd; + + @Parameter(1) + public EndTiming endTiming; + + private ServletRequest servletRequest = null; + private AsyncContext asyncContext = null; + private AtomicBoolean failed = new AtomicBoolean(); + private CountDownLatch servletStartLatch; + private CountDownLatch threadCompleteLatch; + private CountDownLatch clientDisconnectLatch; + private CountDownLatch endLatch; + private boolean dispatch; + + @Test + public void testAsync() throws Exception { + dispatch = false; + servletRequest = null; + asyncContext = null; + + // Initialise tracking fields + failed.set(true); + servletStartLatch = new CountDownLatch(1); + threadCompleteLatch = new CountDownLatch(1); + clientDisconnectLatch = new CountDownLatch(1); + endLatch = new CountDownLatch(1); + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + AsyncServlet bug63816Servlet = new AsyncServlet(); + Wrapper wrapper = Tomcat.addServlet(ctx, "bug63816Servlet", bug63816Servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/*", "bug63816Servlet"); + + tomcat.start(); + + Client client = new Client(); + client.setPort(getPort()); + client.setRequest(new String[] { "GET / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF}); + client.connect(); + client.sendRequest(); + + // Wait for Servlet to start processing request + servletStartLatch.await(); + + if (asyncEnd.isError()) { + client.disconnect(); + clientDisconnectLatch.countDown(); + try { + endLatch.await(); + } catch (InterruptedException e) { + // Ignore + } + } else { + client.setUseContentLength(true); + client.readResponse(true); + } + + Assert.assertFalse(failed.get()); + } + + + private static final class Client extends SimpleHttpClient { + + @Override + public boolean isResponseBodyOK() { + return true; + } + } + + + private final class AsyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + servletStartLatch.countDown(); + + if (dispatch) { + return; + } + + if (!asyncEnd.isError()) { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.setContentLength(2); + resp.getWriter().print("OK"); + } + + servletRequest = req; + asyncContext = req.startAsync(); + asyncContext.addListener(new Listener()); + if (!asyncEnd.isError()) { + // Use a short timeout so the test does not pause for too long + // waiting for the timeout to be triggered. + asyncContext.setTimeout(1000); + } + Thread t = new NonContainerThread(); + + switch (endTiming) { + case INLINE: { + t.run(); + break; + } + case THREAD_COMPLETES_AFTER_SERVLET_EXIT: { + t.start(); + break; + } + case THREAD_COMPLETES_BEFORE_SERVLET_EXIT: { + t.start(); + try { + threadCompleteLatch.await(); + } catch (InterruptedException e) { + // Ignore + } + endLatch.countDown(); + break; + } + } + } + } + + + private final class NonContainerThread extends Thread { + + @Override + public void run() { + if (endTiming == EndTiming.THREAD_COMPLETES_AFTER_SERVLET_EXIT) { + try { + /* + * As much as I dislike it, I don't see any easy way around + * this hack. The thread is started as the Servlet exits but + * we need to wait for the post processing to complete for + * the test to work as intended. In real-world applications + * this does mean that there is a real chance of an ISE. We + * may need to increase this delay for some CI systems. + */ + sleep(1000); + } catch (InterruptedException e) { + // Ignore + } + } + + // Trigger the error if necessary + if (asyncEnd.isError()) { + try { + clientDisconnectLatch.await(); + } catch (InterruptedException e) { + // Ignore + } + try { + ServletResponse resp = asyncContext.getResponse(); + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + OutputStream os = resp.getOutputStream(); + resp.setContentType("text/plain"); + // Needs to be large enough to fill buffers and trigger + // the IOE for the test to work as expected. + for (int i = 0; i < 64; i++) { + os.write(TestCoyoteAdapter.TEXT_8K.getBytes(StandardCharsets.UTF_8)); + } + } catch (IOException e) { + // Expected + } + } + + if (endTiming != EndTiming.THREAD_COMPLETES_AFTER_SERVLET_EXIT) { + try { + switch (asyncEnd) { + case COMPLETE: + case ERROR_COMPLETE: { + asyncContext.complete(); + break; + } + case DISPATCH: + case ERROR_DISPATCH: { + dispatch = true; + asyncContext.dispatch(); + break; + } + case NONE: + case ERROR_NONE: { + break; + } + } + + // The request must stay in async mode until doGet() exists + if (servletRequest.isAsyncStarted()) { + failed.set(false); + } + } finally { + if (endTiming == EndTiming.THREAD_COMPLETES_BEFORE_SERVLET_EXIT) { + threadCompleteLatch.countDown(); + } + } + } + } + } + + + private class Listener implements AsyncListener { + + @Override + public void onComplete(AsyncEvent event) throws IOException { + if (endTiming == EndTiming.INLINE) { + endLatch.countDown(); + } + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + // Need to handle timeouts for THREAD_COMPLETES_AFTER_SERVLET_EXIT in the listener to + // avoid concurrency issues. + if (endTiming == EndTiming.THREAD_COMPLETES_AFTER_SERVLET_EXIT) { + switch (asyncEnd) { + case COMPLETE: { + asyncContext.complete(); + break; + } + case DISPATCH: { + dispatch = true; + asyncContext.dispatch(); + break; + } + default: + // NO-OP + } + } + if (servletRequest.isAsyncStarted() == asyncEnd.isNone()) { + failed.set(false); + } + endLatch.countDown(); + } + + @Override + public void onError(AsyncEvent event) throws IOException { + // Need to handle errors for THREAD_COMPLETES_AFTER_SERVLET_EXIT in the listener to + // avoid concurrency issues. + if (endTiming == EndTiming.THREAD_COMPLETES_AFTER_SERVLET_EXIT) { + switch (asyncEnd) { + case ERROR_COMPLETE: { + asyncContext.complete(); + break; + } + case ERROR_DISPATCH: { + dispatch = true; + asyncContext.dispatch(); + break; + } + default: + // NO-OP + } + if (servletRequest.isAsyncStarted() == asyncEnd.isNone()) { + failed.set(false); + } + endLatch.countDown(); + } + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + // NO-OP + } + } + + + public enum AsyncEnd { + + NONE ( true, false), + COMPLETE (false, false), + DISPATCH (false, false), + ERROR_NONE ( true, true), + ERROR_COMPLETE(false, true), + ERROR_DISPATCH(false, true); + + final boolean none; + final boolean error; + + AsyncEnd(boolean none, boolean error) { + this.none = none; + this.error = error; + } + + public boolean isNone() { + return none; + } + + public boolean isError() { + return error; + } + } + + + public enum EndTiming { + INLINE, + THREAD_COMPLETES_BEFORE_SERVLET_EXIT, + THREAD_COMPLETES_AFTER_SERVLET_EXIT + } +} diff --git a/test/org/apache/catalina/core/TestContextNamingInfoListener.java b/test/org/apache/catalina/core/TestContextNamingInfoListener.java new file mode 100644 index 0000000..65f1d47 --- /dev/null +++ b/test/org/apache/catalina/core/TestContextNamingInfoListener.java @@ -0,0 +1,119 @@ +/* + * 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.catalina.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.util.ContextName; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; + +@RunWith(Parameterized.class) +public class TestContextNamingInfoListener extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{index}: contextPath[{0}], webappVersion[{1}], displayName[{2}], emptyOnRoot[{3}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { "", "", null, Boolean.FALSE, "/", "/", "ROOT" }); + parameterSets.add(new Object[] { "", "42", null, Boolean.FALSE, "/", "/", "ROOT##42" }); + parameterSets.add(new Object[] { "", "", null, Boolean.TRUE, "", "", "" }); + parameterSets.add(new Object[] { "", "42", null, Boolean.TRUE, "", "", "##42" }); + for (Boolean b: Arrays.asList(Boolean.FALSE, Boolean.TRUE)) { + parameterSets.add(new Object[] { "/foo", "", null, b, "/foo", "/foo", "/foo" }); + parameterSets.add(new Object[] { "/foo", "", "My Foo Webapp", b, "/foo", "/foo", "/foo" }); + parameterSets.add(new Object[] { "/foo", "42", "My Foo Webapp", b, "/foo", "/foo", "/foo##42" }); + parameterSets.add(new Object[] { "/foo/bar", "", null, b, "/foo/bar", "/foo/bar", "/foo/bar" }); + parameterSets.add(new Object[] { "/foo/bar", "", "My Foobar Webapp", b, "/foo/bar", "/foo/bar", "/foo/bar" }); + parameterSets.add(new Object[] { "/foo/bar", "42", "My Foobar Webapp", b, "/foo/bar", "/foo/bar", "/foo/bar##42" }); + parameterSets.add(new Object[] { "/\u0444\u0443/\u0431\u0430\u0440", "", "\u041C\u043E\u0439 \u0424\u0443\u0431\u0430\u0440 \u0412\u0435\u0431\u0430\u043F\u043F", b, "/\u0444\u0443/\u0431\u0430\u0440", "/%D1%84%D1%83/%D0%B1%D0%B0%D1%80", "/\u0444\u0443/\u0431\u0430\u0440" }); + parameterSets.add(new Object[] { "/\u0444\u0443/\u0431\u0430\u0440", "42", "\u041C\u043E\u0439 \u0424\u0443\u0431\u0430\u0440 \u0412\u0435\u0431\u0430\u043F\u043F", b, "/\u0444\u0443/\u0431\u0430\u0440", "/%D1%84%D1%83/%D0%B1%D0%B0%D1%80", "/\u0444\u0443/\u0431\u0430\u0440##42" }); + } + + return parameterSets; + } + + @Parameter(0) + public String contextPath; + @Parameter(1) + public String webappVersion; + @Parameter(2) + public String displayName; + @Parameter(3) + public boolean emptyOnRoot; + @Parameter(4) + public String expectedContextPath; + @Parameter(5) + public String expectedEncodedContextPath; + @Parameter(6) + public String expectedName; + + @Test + public void testListener() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + ContextName cn = new ContextName(contextPath, webappVersion); + Context ctx = tomcat.addContext(cn.getPath(), null); + ctx.setName(cn.getName()); + ctx.setWebappVersion(cn.getVersion()); + ctx.setDisplayName(displayName); + + // Enable JNDI - it is disabled by default + tomcat.enableNaming(); + + ContextNamingInfoListener listener = new ContextNamingInfoListener(); + listener.setEmptyOnRoot(emptyOnRoot); + + ctx.addLifecycleListener(listener); + + tomcat.start(); + + Assert.assertEquals(LifecycleState.STARTED, ctx.getState()); + + NamingResourcesImpl namingResources = ctx.getNamingResources(); + ContextEnvironment pathEnv = namingResources.findEnvironment("context/path"); + ContextEnvironment encodedPathEnv = namingResources.findEnvironment("context/encodedPath"); + ContextEnvironment webappVersionEnv = namingResources.findEnvironment("context/webappVersion"); + ContextEnvironment nameEnv = namingResources.findEnvironment("context/name"); + ContextEnvironment baseNameEnv = namingResources.findEnvironment("context/baseName"); + ContextEnvironment displayNameEnv = namingResources.findEnvironment("context/displayName"); + + Assert.assertEquals(expectedContextPath, pathEnv.getValue()); + Assert.assertEquals(expectedEncodedContextPath, encodedPathEnv.getValue()); + Assert.assertEquals(ctx.getWebappVersion(), webappVersionEnv.getValue()); + Assert.assertEquals(expectedName, nameEnv.getValue()); + Assert.assertEquals(ctx.getBaseName(), baseNameEnv.getValue()); + Assert.assertEquals(ctx.getDisplayName(), displayNameEnv.getValue()); + + tomcat.stop(); + } + +} diff --git a/test/org/apache/catalina/core/TestDefaultInstanceManager.java b/test/org/apache/catalina/core/TestDefaultInstanceManager.java new file mode 100644 index 0000000..46bec32 --- /dev/null +++ b/test/org/apache/catalina/core/TestDefaultInstanceManager.java @@ -0,0 +1,90 @@ +/* + * 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.catalina.core; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.servlet.JasperInitializer; + + +public class TestDefaultInstanceManager extends TomcatBaseTest { + + @Test + public void testClassUnloading() throws Exception { + + DefaultInstanceManager instanceManager = doClassUnloadingPrep(); + + // Request a JSP page (that doesn't load any tag libraries etc.) + // This page does use @PostConstruct to ensure that the cache does not + // retain strong references + getUrl("http://localhost:" + getPort() + "/test/annotations.jsp"); + // Request a second JSP (again, no tag libraries etc.) + getUrl("http://localhost:" + getPort() + "/test/bug36923.jsp"); + + // Check the number of classes in the cache + int count = instanceManager.getAnnotationCacheSize(); + + // Request a third JSP (again, no tag libraries etc.) + getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug51544.jsp"); + + // Force a GC to clear out unloaded class (first JSP) + System.gc(); + + // Spin a while until GC happens or we wait too long + int loop = 0; + while (loop < 10) { + instanceManager.backgroundProcess(); + if (instanceManager.getAnnotationCacheSize() == count) { + break; + } + Thread.sleep(100); + loop++; + } + + // First JSP should be unloaded and replaced by third (second left + // alone) so no change in overall count + Assert.assertEquals(count, instanceManager.getAnnotationCacheSize()); + } + + private DefaultInstanceManager doClassUnloadingPrep() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // Create the context (don't use addWebapp as we want to modify the + // JSP Servlet settings). + File appDir = new File("test/webapp"); + StandardContext ctxt = (StandardContext) tomcat.addContext( + null, "/test", appDir.getAbsolutePath()); + + ctxt.addServletContainerInitializer(new JasperInitializer(), null); + + // Configure the defaults and then tweak the JSP servlet settings + // Note: Min value for maxLoadedJsps is 2 + Tomcat.initWebappDefaults(ctxt); + Wrapper w = (Wrapper) ctxt.findChild("jsp"); + w.addInitParameter("maxLoadedJsps", "2"); + + tomcat.start(); + + return (DefaultInstanceManager) ctxt.getInstanceManager(); + } +} diff --git a/test/org/apache/catalina/core/TestNamingContextListener.java b/test/org/apache/catalina/core/TestNamingContextListener.java new file mode 100644 index 0000000..25ba844 --- /dev/null +++ b/test/org/apache/catalina/core/TestNamingContextListener.java @@ -0,0 +1,174 @@ +/* + * 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.catalina.core; + +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; + +public class TestNamingContextListener extends TomcatBaseTest { + + private static final String BUG49132_NAME = "TestName"; + private static final String BUG49132_VALUE = "Test Value"; + + private static final String BUG54096_NameA = "envA"; + private static final String BUG54096_ValueA = "valueA"; + private static final String BUG54096_NameB = "envB"; + private static final String BUG54096_ValueB = "B"; + + /* + * Test JNDI is available to ServletContextListeners. + */ + @Test + public void testBug49132() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Enable JNDI - it is disabled by default + tomcat.enableNaming(); + + ContextEnvironment environment = new ContextEnvironment(); + environment.setType(BUG49132_VALUE.getClass().getName()); + environment.setName(BUG49132_NAME); + environment.setValue(BUG49132_VALUE); + ctx.getNamingResources().addEnvironment(environment); + + ctx.addApplicationListener(Bug49132Listener.class.getName()); + + tomcat.start(); + + Assert.assertEquals(LifecycleState.STARTED, ctx.getState()); + } + + public static final class Bug49132Listener implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + javax.naming.Context initCtx; + try { + initCtx = new InitialContext(); + javax.naming.Context envCtx = + (javax.naming.Context) initCtx.lookup("java:comp/env"); + String value = (String) envCtx.lookup(BUG49132_NAME); + if (!BUG49132_VALUE.equals(value)) { + throw new RuntimeException(); + } + } catch (NamingException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testBug54096() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Enable JNDI - it is disabled by default + tomcat.enableNaming(); + + ContextEnvironment environmentA = new ContextEnvironment(); + environmentA.setType(Bug54096EnvA.class.getName()); + environmentA.setName(BUG54096_NameA); + environmentA.setValue(BUG54096_ValueA); + ctx.getNamingResources().addEnvironment(environmentA); + + ContextEnvironment environmentB = new ContextEnvironment(); + environmentB.setType(Bug54096EnvB.class.getName()); + environmentB.setName(BUG54096_NameB); + environmentB.setValue(BUG54096_ValueB); + ctx.getNamingResources().addEnvironment(environmentB); + + ctx.addApplicationListener(Bug54096Listener.class.getName()); + + tomcat.start(); + + Assert.assertEquals(LifecycleState.STARTED, ctx.getState()); + } + + public static class Bug54096EnvA { + + private final String value; + + public Bug54096EnvA(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + public static class Bug54096EnvB { + + private final char value; + + public Bug54096EnvB(char value) { + this.value = value; + } + + public char getValue() { + return value; + } + } + + public static final class Bug54096Listener implements + ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + javax.naming.Context initCtx; + try { + initCtx = new InitialContext(); + javax.naming.Context envCtx = + (javax.naming.Context) initCtx.lookup("java:comp/env"); + + // Validate entry A + Bug54096EnvA valueA = + (Bug54096EnvA) envCtx.lookup(BUG54096_NameA); + if (!BUG54096_ValueA.equals(valueA.getValue())) { + throw new RuntimeException(); + } + + // Validate entry B + Bug54096EnvB valueB = + (Bug54096EnvB) envCtx.lookup(BUG54096_NameB); + if (BUG54096_ValueB.charAt(0) != valueB.getValue()) { + throw new RuntimeException(); + } + + } catch (NamingException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/test/org/apache/catalina/core/TestPropertiesRoleMappingListener.java b/test/org/apache/catalina/core/TestPropertiesRoleMappingListener.java new file mode 100644 index 0000000..d37e750 --- /dev/null +++ b/test/org/apache/catalina/core/TestPropertiesRoleMappingListener.java @@ -0,0 +1,169 @@ +/* + * 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.catalina.core; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.authenticator.BasicAuthenticator; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +public class TestPropertiesRoleMappingListener extends TomcatBaseTest { + + @Test(expected = NullPointerException.class) + public void testNullRoleMappingFile() throws Exception { + PropertiesRoleMappingListener listener = new PropertiesRoleMappingListener(); + listener.setRoleMappingFile(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyRoleMappingFile() throws Exception { + PropertiesRoleMappingListener listener = new PropertiesRoleMappingListener(); + listener.setRoleMappingFile(""); + } + + @Test(expected = LifecycleException.class) + public void testNotFoundRoleMappingFile() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + PropertiesRoleMappingListener listener = new PropertiesRoleMappingListener(); + ctx.addLifecycleListener(listener); + + try { + tomcat.start(); + } finally { + tomcat.stop(); + } + } + + @Test + public void testFileFromServletContext() throws Exception { + doTest("webapp:/WEB-INF/role-mapping.properties", null); + } + + @Test + public void testFileFromServletContextWithKeyPrefix() throws Exception { + doTest("webapp:/WEB-INF/prefixed-role-mapping.properties", "app-roles."); + } + + @Test + public void testFileFromClasspath() throws Exception { + doTest("classpath:/com/example/role-mapping.properties", null); + } + + @Test + public void testFileFromClasspathWithKeyPrefix() throws Exception { + doTest("classpath:/com/example/prefixed-role-mapping.properties", "app-roles."); + } + + @Test + public void testFileFromFile() throws Exception { + File appDir = new File("test/webapp-role-mapping"); + File file = new File(appDir, "WEB-INF/role-mapping.properties"); + doTest(file.getAbsoluteFile().toURI().toASCIIString(), null); + } + + @Test + public void testFileFromFileWithKeyPrefix() throws Exception { + File appDir = new File("test/webapp-role-mapping"); + File file = new File(appDir, "WEB-INF/prefixed-role-mapping.properties"); + doTest(file.getAbsoluteFile().toURI().toASCIIString(), "app-roles."); + } + + private void doTest(String roleMappingFile, String keyPrefix) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-role-mapping"); + Context ctx = tomcat.addContext("", appDir.getAbsolutePath()); + + PropertiesRoleMappingListener listener = new PropertiesRoleMappingListener(); + listener.setRoleMappingFile(roleMappingFile); + listener.setKeyPrefix(keyPrefix); + ctx.addLifecycleListener(listener); + + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + LoginConfig loginConfig = new LoginConfig(); + loginConfig.setAuthMethod(HttpServletRequest.BASIC_AUTH); + ctx.setLoginConfig(loginConfig); + ctx.getPipeline().addValve(new BasicAuthenticator()); + + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser("foo", "bar"); + // role 'admin' + realm.addUserRole("foo", "de25f8f5-e534-4980-9351-e316384b1127"); + realm.addUser("waldo", "fred"); + // role 'user' + realm.addUserRole("waldo", "13f6b886-cba8-4b5b-9a1b-06a6fe533356"); + // role 'supervisor' + realm.addUserRole("waldo", "45071e9a-13ef-11ee-89dc-20677cd45840"); + ctx.setRealm(realm); + + for (String role : Arrays.asList("admin", "user", "unmapped")) { + SecurityCollection securityCollection = new SecurityCollection(); + securityCollection.addPattern("/" + role + ".txt"); + SecurityConstraint constraint = new SecurityConstraint(); + constraint.addAuthRole(role); + constraint.addCollection(securityCollection); + ctx.addConstraint(constraint); + ctx.addSecurityRole(role); + } + + tomcat.start(); + + testRequest("foo:bar", "/admin.txt", 200); + testRequest("waldo:fred", "/user.txt", 200); + testRequest("waldo:fred", "/unmapped.txt", 403); + testRequest("bar:baz", "/user.txt", 401); + } + + private void testRequest(String credentials, String path, int statusCode) throws IOException { + ByteChunk out = new ByteChunk(); + Map> reqHead = new HashMap<>(); + List head = new ArrayList<>(); + head.add(HttpServletRequest.BASIC_AUTH + " " + + Base64.encodeBase64String(credentials.getBytes(StandardCharsets.ISO_8859_1))); + reqHead.put("Authorization", head); + int rc = getUrl("http://localhost:" + getPort() + path, out, reqHead, null); + Assert.assertEquals(statusCode, rc); + } + +} diff --git a/test/org/apache/catalina/core/TestStandardContext.java b/test/org/apache/catalina/core/TestStandardContext.java new file mode 100644 index 0000000..aa92e22 --- /dev/null +++ b/test/org/apache/catalina/core/TestStandardContext.java @@ -0,0 +1,1069 @@ +/* + * 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.catalina.core; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.HttpConstraintElement; +import jakarta.servlet.HttpMethodConstraintElement; +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.annotation.HttpConstraint; +import jakarta.servlet.annotation.HttpMethodConstraint; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.ServletSecurity.TransportGuarantee; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Wrapper; +import org.apache.catalina.authenticator.BasicAuthenticator; +import org.apache.catalina.loader.WebappLoader; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.servlet.JasperInitializer; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.LoginConfig; + + +public class TestStandardContext extends TomcatBaseTest { + + private static final String REQUEST = + "GET / HTTP/1.1\r\n" + + "Host: anything\r\n" + + "Connection: close\r\n" + + "\r\n"; + + @Test + public void testBug46243() throws Exception { + // This tests that if a Filter init() fails then the web application + // is not put into service. (BZ 46243) + // This also tests that if the cause of the failure is gone, + // the context can be started without a need to redeploy it. + + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + File docBase = new File(tomcat.getHost().getAppBaseFile(), "ROOT"); + if (!docBase.mkdirs() && !docBase.isDirectory()) { + Assert.fail("Unable to create docBase"); + } + + Context root = tomcat.addContext("", "ROOT"); + configureTest46243Context(root, true); + tomcat.start(); + + // Configure the client + Bug46243Client client = + new Bug46243Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { REQUEST }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse404()); + + // Context failed to start. This checks that automatic transition + // from FAILED to STOPPED state was successful. + Assert.assertEquals(LifecycleState.STOPPED, root.getState()); + + // Prepare context for the second attempt + // Configuration was cleared on stop() thanks to + // StandardContext.resetContext(), so we need to configure it again + // from scratch. + configureTest46243Context(root, false); + root.start(); + // The same request is processed successfully + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals(Bug46243Filter.class.getName() + + HelloWorldServlet.RESPONSE_TEXT, client.getResponseBody()); + } + + private static void configureTest46243Context(Context context, boolean fail) { + // Add a test filter that fails + FilterDef filterDef = new FilterDef(); + filterDef.setFilterClass(Bug46243Filter.class.getName()); + filterDef.setFilterName("Bug46243"); + filterDef.addInitParameter("fail", Boolean.toString(fail)); + context.addFilterDef(filterDef); + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName("Bug46243"); + filterMap.addURLPatternDecoded("*"); + context.addFilterMap(filterMap); + + // Add a test servlet so there is something to generate a response if + // it works (although it shouldn't) + Tomcat.addServlet(context, "Bug46243", new HelloWorldServlet()); + context.addServletMappingDecoded("/", "Bug46243"); + } + + private static final class Bug46243Client extends SimpleHttpClient { + + Bug46243Client(int port) { + setPort(port); + } + + @Override + public boolean isResponseBodyOK() { + // Don't care about the body in this test + return true; + } + } + + public static final class Bug46243Filter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + PrintWriter out = response.getWriter(); + out.print(getClass().getName()); + chain.doFilter(request, response); + } + + @Override + public void init() throws ServletException { + boolean fail = getInitParameter("fail").equals("true"); + if (fail) { + throw new ServletException("Init fail (test)", new ClassNotFoundException()); + } + } + } + + @Test + public void testWebappLoaderStartFail() throws Exception { + // Test that if WebappLoader start() fails and if the cause of + // the failure is gone, the context can be started without + // a need to redeploy it. + + // Set up a container + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + // To not start Context automatically, as we have to configure it first + ((ContainerBase) tomcat.getHost()).setStartChildren(false); + + FailingWebappLoader loader = new FailingWebappLoader(); + File root = new File("test/webapp"); + Context context = tomcat.addWebapp("", root.getAbsolutePath()); + context.setLoader(loader); + + try { + context.start(); + Assert.fail(); + } catch (LifecycleException ex) { + // As expected + } + Assert.assertEquals(LifecycleState.FAILED, context.getState()); + + // The second attempt + loader.setFail(false); + context.start(); + Assert.assertEquals(LifecycleState.STARTED, context.getState()); + + // Using a test from testBug49922() to check that the webapp is running + ByteChunk result = getUrl("http://localhost:" + getPort() + + "/bug49922/target"); + Assert.assertEquals("Target", result.toString()); + } + + @Test + public void testWebappListenerConfigureFail() throws Exception { + // Test that if LifecycleListener on webapp fails during + // configure_start event and if the cause of the failure is gone, + // the context can be started without a need to redeploy it. + + // Set up a container + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + // To not start Context automatically, as we have to configure it first + ((ContainerBase) tomcat.getHost()).setStartChildren(false); + + FailingLifecycleListener listener = new FailingLifecycleListener(); + File root = new File("test/webapp"); + Context context = tomcat.addWebapp("", root.getAbsolutePath()); + context.addLifecycleListener(listener); + + try { + context.start(); + Assert.fail(); + } catch (LifecycleException ex) { + // As expected + } + Assert.assertEquals(LifecycleState.FAILED, context.getState()); + + // The second attempt + listener.setFail(false); + context.start(); + Assert.assertEquals(LifecycleState.STARTED, context.getState()); + + // Using a test from testBug49922() to check that the webapp is running + ByteChunk result = getUrl("http://localhost:" + getPort() + + "/bug49922/target"); + Assert.assertEquals("Target", result.toString()); + } + + private static class FailingWebappLoader extends WebappLoader { + private boolean fail = true; + protected void setFail(boolean fail) { + this.fail = fail; + } + @Override + protected void startInternal() throws LifecycleException { + if (fail) { + throw new RuntimeException("Start fail (test)"); + } + super.startInternal(); + } + } + + private static class FailingLifecycleListener implements LifecycleListener { + private static final String failEvent = Lifecycle.CONFIGURE_START_EVENT; + private boolean fail = true; + protected void setFail(boolean fail) { + this.fail = fail; + } + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (fail && event.getType().equals(failEvent)) { + throw new RuntimeException(failEvent + " fail (test)"); + } + } + } + + @Test + public void testBug49922() throws Exception { + // Test that filter mapping works. Test that the same filter is + // called only once, even if is selected by several mapping + // url-patterns or by both a url-pattern and a servlet-name. + + getTomcatInstanceTestWebapp(false, true); + + ByteChunk result = new ByteChunk(); + + // Check filter and servlet aren't called + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug49922/foo", result, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + Assert.assertTrue(result.getLength() > 0); + + // Check extension mapping works + result = getUrl("http://localhost:" + getPort() + "/test/foo.do"); + Assert.assertEquals("FilterServlet", result.toString()); + + // Check path mapping works + result = getUrl("http://localhost:" + getPort() + "/test/bug49922/servlet"); + Assert.assertEquals("FilterServlet", result.toString()); + + // Check servlet name mapping works + result = getUrl("http://localhost:" + getPort() + "/test/foo.od"); + Assert.assertEquals("FilterServlet", result.toString()); + + // Check filter is only called once + result = getUrl("http://localhost:" + getPort() + + "/test/bug49922/servlet/foo.do"); + Assert.assertEquals("FilterServlet", result.toString()); + result = getUrl("http://localhost:" + getPort() + + "/test/bug49922/servlet/foo.od"); + Assert.assertEquals("FilterServlet", result.toString()); + + // Check dispatcher mapping + result = getUrl("http://localhost:" + getPort() + + "/test/bug49922/target"); + Assert.assertEquals("Target", result.toString()); + result = getUrl("http://localhost:" + getPort() + + "/test/bug49922/forward"); + Assert.assertEquals("FilterTarget", result.toString()); + result = getUrl("http://localhost:" + getPort() + + "/test/bug49922/include"); + Assert.assertEquals("IncludeFilterTarget", result.toString()); + } + + + public static final class Bug49922Filter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + response.setContentType("text/plain"); + response.getWriter().print("Filter"); + chain.doFilter(request, response); + } + } + + public static final class Bug49922ForwardServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.getRequestDispatcher("/bug49922/target").forward(req, resp); + } + + } + + public static final class Bug49922IncludeServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("Include"); + req.getRequestDispatcher("/bug49922/target").include(req, resp); + } + + } + + public static final class Bug49922TargetServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("Target"); + } + + } + + public static final class Bug49922Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("Servlet"); + } + + } + + @Test + public void testBug50015() throws Exception { + // Test that configuring servlet security constraints programmatically + // does work. + + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Setup realm + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser("tomcat", "tomcat"); + realm.addUserRole("tomcat", "tomcat"); + ctx.setRealm(realm); + + // Configure app for BASIC auth + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("BASIC"); + ctx.setLoginConfig(lc); + ctx.getPipeline().addValve(new BasicAuthenticator()); + + // Add ServletContainerInitializer + ServletContainerInitializer sci = new Bug50015SCI(); + ctx.addServletContainerInitializer(sci, null); + + // Start the context + tomcat.start(); + + // Request the first servlet + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/bug50015", + bc, null); + + // Check for a 401 + Assert.assertNotSame("OK", bc.toString()); + Assert.assertEquals(401, rc); + } + + public static final class Bug50015SCI + implements ServletContainerInitializer { + + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + // Register and map servlet + Servlet s = new TesterServlet(); + ServletRegistration.Dynamic sr = ctx.addServlet("bug50015", s); + sr.addMapping("/bug50015"); + + // Limit access to users in the Tomcat role + HttpConstraintElement hce = new HttpConstraintElement( + TransportGuarantee.NONE, "tomcat"); + ServletSecurityElement sse = new ServletSecurityElement(hce); + sr.setServletSecurity(sse); + } + } + + @Test + public void testDenyUncoveredHttpMethodsSCITrue() throws Exception { + doTestDenyUncoveredHttpMethodsSCI(true); + } + + @Test + public void testDenyUncoveredHttpMethodsSCIFalse() throws Exception { + doTestDenyUncoveredHttpMethodsSCI(false); + } + + private void doTestDenyUncoveredHttpMethodsSCI(boolean enableDeny) + throws Exception { + // Test that denying uncovered HTTP methods when adding servlet security + // constraints programmatically does work. + + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + ctx.setDenyUncoveredHttpMethods(enableDeny); + + // Setup realm + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser("tomcat", "tomcat"); + realm.addUserRole("tomcat", "tomcat"); + ctx.setRealm(realm); + + // Configure app for BASIC auth + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("BASIC"); + ctx.setLoginConfig(lc); + ctx.getPipeline().addValve(new BasicAuthenticator()); + + // Add ServletContainerInitializer + ServletContainerInitializer sci = new DenyUncoveredHttpMethodsSCI(); + ctx.addServletContainerInitializer(sci, null); + + // Start the context + tomcat.start(); + + // Request the first servlet + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test", + bc, null); + + // Check for a 401 + if (enableDeny) { + // Should be default error page + Assert.assertTrue(bc.toString().contains("403")); + Assert.assertEquals(403, rc); + } else { + Assert.assertEquals("OK", bc.toString()); + Assert.assertEquals(200, rc); + } + } + + public static final class DenyUncoveredHttpMethodsSCI + implements ServletContainerInitializer { + + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + // Register and map servlet + Servlet s = new TesterServlet(); + ServletRegistration.Dynamic sr = ctx.addServlet("test", s); + sr.addMapping("/test"); + + // Add a constraint with uncovered methods + HttpConstraintElement hce = new HttpConstraintElement( + TransportGuarantee.NONE, "tomcat"); + HttpMethodConstraintElement hmce = + new HttpMethodConstraintElement("POST", hce); + Set hmces = new HashSet<>(); + hmces.add(hmce); + ServletSecurityElement sse = new ServletSecurityElement(hmces); + sr.setServletSecurity(sse); + } + } + + @Test + public void testBug51376a() throws Exception { + doTestBug51376(false); + } + + @Test + public void testBug51376b() throws Exception { + doTestBug51376(true); + } + + private void doTestBug51376(boolean loadOnStartUp) throws Exception { + // Test that for a servlet that was added programmatically its + // loadOnStartup property is honored and its init() and destroy() + // methods are called. + + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add ServletContainerInitializer + Bug51376SCI sci = new Bug51376SCI(loadOnStartUp); + ctx.addServletContainerInitializer(sci, null); + + // Start the context + tomcat.start(); + + // Stop the context + ctx.stop(); + + // Make sure that init() and destroy() were called correctly + Assert.assertTrue(sci.getServlet().isOk()); + Assert.assertTrue(loadOnStartUp == sci.getServlet().isInitCalled()); + } + + public static final class Bug51376SCI + implements ServletContainerInitializer { + + private Bug51376Servlet s = null; + private boolean loadOnStartUp; + + public Bug51376SCI(boolean loadOnStartUp) { + this.loadOnStartUp = loadOnStartUp; + } + + private Bug51376Servlet getServlet() { + return s; + } + + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + // Register and map servlet + s = new Bug51376Servlet(); + ServletRegistration.Dynamic sr = ctx.addServlet("bug51376", s); + sr.addMapping("/bug51376"); + if (loadOnStartUp) { + sr.setLoadOnStartup(1); + } + } + } + + public static final class Bug51376Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private Boolean initOk = null; + private Boolean destroyOk = null; + + @Override + public void init() { + if (initOk == null && destroyOk == null) { + initOk = Boolean.TRUE; + } else { + initOk = Boolean.FALSE; + } + } + + @Override + public void destroy() { + if (initOk.booleanValue() && destroyOk == null) { + destroyOk = Boolean.TRUE; + } else { + destroyOk = Boolean.FALSE; + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().write("OK"); + } + + protected boolean isOk() { + if (initOk != null && initOk.booleanValue() && destroyOk != null && + destroyOk.booleanValue()) { + return true; + } else if (initOk == null && destroyOk == null) { + return true; + } else { + return false; + } + } + + protected boolean isInitCalled() { + return initOk != null && initOk.booleanValue(); + } + } + + /** + * Test case for bug 49711: HttpServletRequest.getParts does not work + * in a filter. + */ + @Test + public void testBug49711() { + Bug49711Client client = new Bug49711Client(); + + // Make sure non-multipart works properly + client.doRequest("/regular", false, false); + + // Servlet attempts to read parts which will trigger an ISE + Assert.assertTrue(client.isResponse500()); + + client.reset(); + + // Make sure regular multipart works properly + client.doRequest("/multipart", false, true); // send multipart request + + Assert.assertEquals("Regular multipart doesn't work", + "parts=1", + client.getResponseBody()); + + client.reset(); + + // Make casual multipart request to "regular" servlet w/o config + // We expect an error + client.doRequest("/regular", false, true); // send multipart request + + // Servlet attempts to read parts which will trigger an ISE + Assert.assertTrue(client.isResponse500()); + + client.reset(); + + // Make casual multipart request to "regular" servlet w/config + // We expect that the server /will/ parse the parts, even though + // there is no @MultipartConfig + client.doRequest("/regular", true, true); // send multipart request + + Assert.assertEquals("Incorrect response for configured casual multipart request", + "parts=1", + client.getResponseBody()); + + client.reset(); + } + + private static class Bug49711Servlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Just echo the parameters and values back as plain text + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + PrintWriter out = resp.getWriter(); + + out.println("parts=" + (null == req.getParts() + ? "null" + : Integer.valueOf(req.getParts().size()))); + } + } + + @MultipartConfig + private static class Bug49711Servlet_multipart extends Bug49711Servlet { + private static final long serialVersionUID = 1L; + } + + /** + * Bug 49711 test client: test for casual getParts calls. + */ + private class Bug49711Client extends SimpleHttpClient { + + private boolean init; + private Context context; + + private synchronized void init() throws Exception { + if (init) { + return; + } + + Tomcat tomcat = getTomcatInstance(); + context = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(context, "regular", new Bug49711Servlet()); + Wrapper w = Tomcat.addServlet(context, "multipart", new Bug49711Servlet_multipart()); + + // Tomcat.addServlet does not respect annotations, so we have + // to set our own MultipartConfigElement. + w.setMultipartConfigElement(new MultipartConfigElement("")); + + context.addServletMappingDecoded("/regular", "regular"); + context.addServletMappingDecoded("/multipart", "multipart"); + tomcat.start(); + + setPort(tomcat.getConnector().getLocalPort()); + + init = true; + } + + private Exception doRequest(String uri, + boolean allowCasualMultipart, + boolean makeMultipartRequest) { + try { + init(); + + context.setAllowCasualMultipartParsing(allowCasualMultipart); + + // Open connection + connect(); + + // Send specified request body using method + String[] request; + + if(makeMultipartRequest) { + String boundary = "--simpleboundary"; + + String content = "--" + boundary + CRLF + + "Content-Disposition: form-data; name=\"name\"" + CRLF + CRLF + + "value" + CRLF + + "--" + boundary + "--" + CRLF; + + // Re-encode the content so that bytes = characters + content = new String(content.getBytes("UTF-8"), "ASCII"); + + request = new String[] { + "POST http://localhost:" + getPort() + uri + " HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Connection: close" + CRLF + + "Content-Type: multipart/form-data; boundary=" + boundary + CRLF + + "Content-Length: " + content.length() + CRLF + + CRLF + + content + + CRLF + }; + } else { + request = new String[] { + "GET http://localhost:" + getPort() + uri + " HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Connection: close" + CRLF + + CRLF + }; + } + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + + return null; + } + + @Override + public boolean isResponseBodyOK() { + return false; // Don't care + } + } + + @Test(expected = IllegalArgumentException.class) + public void testAddPostConstructMethodNullClassName() { + new StandardContext().addPostConstructMethod(null, ""); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddPostConstructMethodNullMethodName() { + new StandardContext().addPostConstructMethod("", null); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddPostConstructMethodConflicts() { + StandardContext standardContext = new StandardContext(); + standardContext.addPostConstructMethod("a", "a"); + standardContext.addPostConstructMethod("a", "b"); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddPreDestroyMethodNullClassName() { + new StandardContext().addPreDestroyMethod(null, ""); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddPreDestroyMethodNullMethodName() { + new StandardContext().addPreDestroyMethod("", null); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddPreDestroyMethodConflicts() { + StandardContext standardContext = new StandardContext(); + standardContext.addPreDestroyMethod("a", "a"); + standardContext.addPreDestroyMethod("a", "b"); + } + + @Test + public void testTldListener() throws Exception { + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + File docBase = new File("test/webapp-3.0"); + Context ctx = tomcat.addContext("", docBase.getAbsolutePath()); + ctx.addServletContainerInitializer(new JasperInitializer(), null); + + // Start the context + tomcat.start(); + + // Stop the context + ctx.stop(); + + String log = TesterTldListener.getLog(); + Assert.assertTrue(log, log.contains("PASS-01")); + Assert.assertTrue(log, log.contains("PASS-02")); + Assert.assertFalse(log, log.contains("FAIL")); + } + + @Test + public void testFlagFailCtxIfServletStartFails() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File docBase = new File(System.getProperty("java.io.tmpdir")); + StandardContext context = (StandardContext) tomcat.addContext("", + docBase.getAbsolutePath()); + + // first we test the flag itself, which can be set on the Host and + // Context + Assert.assertFalse(context.getComputedFailCtxIfServletStartFails()); + + StandardHost host = (StandardHost) tomcat.getHost(); + host.setFailCtxIfServletStartFails(true); + Assert.assertTrue(context.getComputedFailCtxIfServletStartFails()); + context.setFailCtxIfServletStartFails(Boolean.FALSE); + Assert.assertFalse("flag on Context should override Host config", + context.getComputedFailCtxIfServletStartFails()); + + // second, we test the actual effect of the flag on the startup + Wrapper servlet = Tomcat.addServlet(context, "myservlet", + new FailingStartupServlet()); + servlet.setLoadOnStartup(1); + + tomcat.start(); + Assert.assertTrue("flag false should not fail deployment", context.getState() + .isAvailable()); + + tomcat.stop(); + Assert.assertFalse(context.getState().isAvailable()); + + host.removeChild(context); + context = (StandardContext) tomcat.addContext("", + docBase.getAbsolutePath()); + servlet = Tomcat.addServlet(context, "myservlet", + new FailingStartupServlet()); + servlet.setLoadOnStartup(1); + tomcat.start(); + Assert.assertFalse("flag true should fail deployment", context.getState() + .isAvailable()); + } + + private static class FailingStartupServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void init() throws ServletException { + throw new ServletException("failing on purpose"); + } + + } + + @Test + public void testBug56085() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, true); + + String realPath = ((Context) tomcat.getHost().findChildren()[0]).getRealPath("\\"); + + Assert.assertNull(realPath); + } + + /* + * Check real path for directories ends with File.separator for consistency + * with previous major versions. + */ + @Test + public void testBug57556a() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, true); + Context testContext = ((Context) tomcat.getHost().findChildren()[0]); + + File f = new File(testContext.getDocBase()); + if (!f.isAbsolute()) { + f = new File(((Host) testContext.getParent()).getAppBaseFile(), f.getPath()); + } + String base = f.getCanonicalPath(); + + + doTestBug57556(testContext, "", base + File.separatorChar); + doTestBug57556(testContext, "/", base + File.separatorChar); + doTestBug57556(testContext, "/jsp", base + File.separatorChar+ "jsp"); + doTestBug57556(testContext, "/jsp/", base + File.separatorChar+ "jsp" + File.separatorChar); + doTestBug57556(testContext, "/index.html", base + File.separatorChar + "index.html"); + doTestBug57556(testContext, "/foo", base + File.separatorChar + "foo"); + doTestBug57556(testContext, "/foo/", base + File.separatorChar + "foo" + File.separatorChar); + } + + @Test + public void testBug57556b() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File docBase = new File("/"); + Context testContext = tomcat.addContext("", docBase.getAbsolutePath()); + tomcat.start(); + + File f = new File(testContext.getDocBase()); + if (!f.isAbsolute()) { + f = new File(((Host) testContext.getParent()).getAppBaseFile(), f.getPath()); + } + String base = f.getCanonicalPath(); + + doTestBug57556(testContext, "", base); + doTestBug57556(testContext, "/", base); + } + + private void doTestBug57556(Context testContext, String path, String expected) throws Exception { + String realPath = testContext.getRealPath(path); + Assert.assertNotNull(realPath); + Assert.assertEquals(expected, realPath); + } + + @Test + public void testBug56903() { + Context context = new StandardContext(); + + context.setResourceOnlyServlets("a,b,c"); + MatcherAssert.assertThat(Arrays.asList(context.getResourceOnlyServlets().split(",")), + CoreMatchers.hasItems("a", "b", "c")); + } + + @Test + public void testSetPath() { + testSetPath("", ""); + testSetPath("/foo", "/foo"); + testSetPath("/foo/bar", "/foo/bar"); + testSetPath(null, ""); + testSetPath("/", ""); + testSetPath("foo", "/foo"); + testSetPath("/foo/bar/", "/foo/bar"); + testSetPath("foo/bar/", "/foo/bar"); + } + + private void testSetPath(String value, String expectedValue) { + StandardContext context = new StandardContext(); + context.setPath(value); + Assert.assertEquals(expectedValue, context.getPath()); + } + + + @Test + public void testUncoveredMethods() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("/test", null); + ctx.setDenyUncoveredHttpMethods(true); + + ServletContainerInitializer sci = new SCI(); + ctx.addServletContainerInitializer(sci, null); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + + rc = getUrl("http://localhost:" + getPort() + "/test/foo", bc, false); + + Assert.assertEquals(403, rc); + } + + + public static class SCI implements ServletContainerInitializer { + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + ServletRegistration.Dynamic sr = ctx.addServlet("Foo", Foo.class.getName()); + sr.addMapping("/foo"); + } + } + + + @ServletSecurity(value=@HttpConstraint(ServletSecurity.EmptyRoleSemantic.DENY), + httpMethodConstraints=@HttpMethodConstraint("POST")) + public static class Foo extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.getWriter().print("OK"); + } + } + + + @Test + public void testNamingContextName() throws Exception { + Engine engine = new StandardEngine(); + engine.setName("engine"); + + Host host = new StandardHost(); + host.setName("host"); + host.setParent(engine); + + Context context = new StandardContext(); + context.setName("context"); + context.setParent(host); + + Method m = StandardContext.class.getDeclaredMethod("getNamingContextName"); + m.setAccessible(true); + String result = (String) m.invoke(context); + + Assert.assertEquals("/engine/hostcontext", result); + } +} diff --git a/test/org/apache/catalina/core/TestStandardContextAliases.java b/test/org/apache/catalina/core/TestStandardContextAliases.java new file mode 100644 index 0000000..e22ccfa --- /dev/null +++ b/test/org/apache/catalina/core/TestStandardContextAliases.java @@ -0,0 +1,117 @@ +/* + * 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.catalina.core; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Set; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.webresources.StandardRoot; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestStandardContextAliases extends TomcatBaseTest { + + @Test + public void testDirContextAliases() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + File lib = new File("webapps/examples/WEB-INF/lib"); + ctx.setResources(new StandardRoot(ctx)); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/lib", + lib.getAbsolutePath(), null, "/"); + + + Tomcat.addServlet(ctx, "test", new TestServlet()); + ctx.addServletMappingDecoded("/", "test"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/"); + + String result = res.toString(); + if (result == null) { + result = ""; + } + + Assert.assertTrue(result.contains("00-PASS")); + Assert.assertTrue(result.contains("01-PASS")); + Assert.assertTrue(result.contains("02-PASS")); + } + + + /** + * Looks for the JSTL JARs in WEB-INF/lib. + */ + public static class TestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + + ServletContext context = getServletContext(); + + // Check resources individually + URL url = context.getResource("/WEB-INF/lib/taglibs-standard-spec-1.2.5-migrated-0.0.1.jar"); + if (url != null) { + resp.getWriter().write("00-PASS\n"); + } + + url = context.getResource("/WEB-INF/lib/taglibs-standard-impl-1.2.5-migrated-0.0.1.jar"); + if (url != null) { + resp.getWriter().write("01-PASS\n"); + } + + // Check a directory listing + Set libs = context.getResourcePaths("/WEB-INF/lib"); + if (libs == null) { + return; + } + + if (!libs.contains("/WEB-INF/lib/taglibs-standard-spec-1.2.5-migrated-0.0.1.jar")) { + return; + } + if (!libs.contains("/WEB-INF/lib/taglibs-standard-impl-1.2.5-migrated-0.0.1.jar")) { + return; + } + + resp.getWriter().write("02-PASS\n"); + } + + } +} diff --git a/test/org/apache/catalina/core/TestStandardContextResources.java b/test/org/apache/catalina/core/TestStandardContextResources.java new file mode 100644 index 0000000..b412c17 --- /dev/null +++ b/test/org/apache/catalina/core/TestStandardContextResources.java @@ -0,0 +1,280 @@ +/* + * 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.catalina.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.List; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Constants; +import org.apache.catalina.startup.ContextConfig; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.util.IOTools; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.WebXml; + +public class TestStandardContextResources extends TomcatBaseTest { + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + // BZ 49218: The test fails if JreMemoryLeakPreventionListener is not + // present. The listener affects the JVM, and thus not only the current, + // but also the subsequent tests that are run in the same JVM. So it is + // fair to add it in every test. + tomcat.getServer().addLifecycleListener( + new JreMemoryLeakPreventionListener()); + } + + @Test + public void testResources() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + // app dir is relative to server home + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + assertPageContains("/test/resourceA.jsp", + "

    resourceA.jsp in the web application

    "); + assertPageContains("/test/resourceB.jsp", + "

    resourceB.jsp in resources.jar

    "); + assertPageContains("/test/folder/resourceC.jsp", + "

    resourceC.jsp in the web application

    "); + assertPageContains("/test/folder/resourceD.jsp", + "

    resourceD.jsp in resources.jar

    "); + assertPageContains("/test/folder/resourceE.jsp", + "

    resourceE.jsp in the web application

    "); + assertPageContains("/test/resourceG.jsp", + "

    resourceG.jsp in WEB-INF/classes

    ", 404); + + // For BZ 54391. Relative ordering is specified in resources2.jar. + // It is not absolute-ordering, so there may be other jars in the list + @SuppressWarnings("unchecked") + List orderedLibs = (List) ctx.getServletContext() + .getAttribute(ServletContext.ORDERED_LIBS); + if (orderedLibs.size() > 2) { + log.warn("testResources(): orderedLibs: " + orderedLibs); + } + int index = orderedLibs.indexOf("resources.jar"); + int index2 = orderedLibs.indexOf("resources2.jar"); + Assert.assertTrue(orderedLibs.toString(), index >= 0 && index2 >= 0 + && index < index2); + } + + @Test + public void testResourcesWebInfClasses() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // app dir is relative to server home + File appDir = new File("test/webapp-fragments"); + + // Need to cast to be able to set StandardContext specific attribute + StandardContext ctxt = (StandardContext) + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + ctxt.setAddWebinfClassesResources(true); + + tomcat.start(); + + assertPageContains("/test/resourceA.jsp", + "

    resourceA.jsp in the web application

    "); + assertPageContains("/test/resourceB.jsp", + "

    resourceB.jsp in resources.jar

    "); + assertPageContains("/test/folder/resourceC.jsp", + "

    resourceC.jsp in the web application

    "); + assertPageContains("/test/folder/resourceD.jsp", + "

    resourceD.jsp in resources.jar

    "); + assertPageContains("/test/folder/resourceE.jsp", + "

    resourceE.jsp in the web application

    "); + assertPageContains("/test/resourceG.jsp", + "

    resourceG.jsp in WEB-INF/classes

    "); + } + + @Test + public void testResourcesAbsoluteOrdering() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File appDir = new File("test/webapp-fragments"); + + AbsoluteOrderContextConfig absoluteOrderConfig = new AbsoluteOrderContextConfig(); + + // app dir is relative to server home + StandardContext ctx = (StandardContext) tomcat.addWebapp(null, "/test", + appDir.getAbsolutePath(), absoluteOrderConfig); + + Tomcat.addServlet(ctx, "getresource", new GetResourceServlet()); + ctx.addServletMappingDecoded("/getresource", "getresource"); + + tomcat.start(); + assertPageContains("/test/getresource?path=/resourceF.jsp", + "

    resourceF.jsp in resources2.jar

    "); + assertPageContains("/test/getresource?path=/resourceB.jsp", + "

    resourceB.jsp in resources.jar

    "); + + // Check ordering, for BZ 54391 + Assert.assertEquals(Arrays.asList("resources.jar", "resources2.jar"), ctx + .getServletContext().getAttribute(ServletContext.ORDERED_LIBS)); + + tomcat.getHost().removeChild(ctx); + tomcat.getHost().stop(); + + // change ordering + absoluteOrderConfig.swap(); + + ctx = (StandardContext) tomcat.addWebapp(null, "/test", + appDir.getAbsolutePath(), absoluteOrderConfig); + Tomcat.addServlet(ctx, "getresource", new GetResourceServlet()); + ctx.addServletMappingDecoded("/getresource", "getresource"); + + tomcat.getHost().start(); + + assertPageContains("/test/getresource?path=/resourceF.jsp", + "

    resourceF.jsp in resources2.jar

    "); + assertPageContains("/test/getresource?path=/resourceB.jsp", + "

    resourceB.jsp in resources2.jar

    "); + + // Check ordering, for BZ 54391 + Assert.assertEquals(Arrays.asList("resources2.jar", "resources.jar"), ctx + .getServletContext().getAttribute(ServletContext.ORDERED_LIBS)); + } + + + public static class AbsoluteOrderContextConfig extends ContextConfig { + + private boolean swap = false; + + public AbsoluteOrderContextConfig() { + super(); + // Prevent it from looking (if it finds one - it'll have dup error) + setDefaultWebXml(Constants.NoDefaultWebXml); + } + + @Override + protected WebXml createWebXml() { + WebXml wxml = new WebXml(); + if (swap) { + wxml.addAbsoluteOrdering("resources2"); + wxml.addAbsoluteOrdering("resources"); + } else { + wxml.addAbsoluteOrdering("resources"); + wxml.addAbsoluteOrdering("resources2"); + } + return wxml; + } + + protected void swap() { + swap = !swap; + } + } + + + @Test + public void testResources2() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + // app dir is relative to server home + StandardContext ctx = (StandardContext) tomcat.addWebapp(null, "/test", + appDir.getAbsolutePath()); + skipTldsForResourceJars(ctx); + + Tomcat.addServlet(ctx, "getresource", new GetResourceServlet()); + ctx.addServletMappingDecoded("/getresource", "getresource"); + + tomcat.start(); + + assertPageContains("/test/getresource?path=/resourceF.jsp", + "

    resourceF.jsp in resources2.jar

    "); + assertPageContains("/test/getresource?path=/resourceA.jsp", + "

    resourceA.jsp in the web application

    "); + assertPageContains("/test/getresource?path=/resourceB.jsp", + "

    resourceB.jsp in resources.jar

    "); + assertPageContains("/test/getresource?path=/folder/resourceC.jsp", + "

    resourceC.jsp in the web application

    "); + assertPageContains("/test/getresource?path=/folder/resourceD.jsp", + "

    resourceD.jsp in resources.jar

    "); + assertPageContains("/test/getresource?path=/folder/resourceE.jsp", + "

    resourceE.jsp in the web application

    "); + } + + /** + * A servlet that prints the requested resource. The path to the requested + * resource is passed as a parameter, path. + */ + public static class GetResourceServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + + ServletContext context = getServletContext(); + + // Check resources individually + URL url = context.getResource(req.getParameter("path")); + if (url == null) { + resp.getWriter().println("Not found"); + return; + } + + try (InputStream input = url.openStream(); + OutputStream output = resp.getOutputStream()) { + IOTools.flow(input, output); + } + } + } + + private void assertPageContains(String pageUrl, String expectedBody) + throws IOException { + + assertPageContains(pageUrl, expectedBody, 200); + } + + private void assertPageContains(String pageUrl, String expectedBody, + int expectedStatus) throws IOException { + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + pageUrl, res, null); + + Assert.assertEquals(expectedStatus, sc); + + if (expectedStatus == 200) { + String result = res.toString(); + Assert.assertTrue(result, result.indexOf(expectedBody) > 0); + } + } +} diff --git a/test/org/apache/catalina/core/TestStandardContextValve.java b/test/org/apache/catalina/core/TestStandardContextValve.java new file mode 100644 index 0000000..2057c05 --- /dev/null +++ b/test/org/apache/catalina/core/TestStandardContextValve.java @@ -0,0 +1,262 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.util.Locale; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Response; +import org.apache.catalina.startup.ExpectationClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.ErrorPage; + +public class TestStandardContextValve extends TomcatBaseTest { + + @Test + public void testBug51653a() throws Exception { + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Traces order of events across multiple components + StringBuilder trace = new StringBuilder(); + + //Add the error page + Tomcat.addServlet(ctx, "errorPage", new Bug51653ErrorPage(trace)); + ctx.addServletMappingDecoded("/error", "errorPage"); + // And the handling for 404 responses + ErrorPage errorPage = new ErrorPage(); + errorPage.setErrorCode(Response.SC_NOT_FOUND); + errorPage.setLocation("/error"); + ctx.addErrorPage(errorPage); + + // Add the request listener + Bug51653RequestListener reqListener = + new Bug51653RequestListener(trace); + ((StandardContext) ctx).addApplicationEventListener(reqListener); + + tomcat.start(); + + // Request a page that does not exist + int rc = getUrl("http://localhost:" + getPort() + "/invalid", + new ByteChunk(), null); + + // Need to allow time (but not too long in case the test fails) for + // ServletRequestListener to complete + int i = 40; + while (i > 0) { + if (trace.toString().endsWith("Destroy")) { + break; + } + Thread.sleep(250); + i--; + } + + Assert.assertEquals(Response.SC_NOT_FOUND, rc); + Assert.assertEquals("InitErrorDestroy", trace.toString()); + } + + + @Test + public void testBug51653b() throws Exception { + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Traces order of events across multiple components + StringBuilder trace = new StringBuilder(); + + // Add the page that generates the error + Tomcat.addServlet(ctx, "test", new Bug51653ErrorTrigger()); + ctx.addServletMappingDecoded("/test", "test"); + + // Add the error page + Tomcat.addServlet(ctx, "errorPage", new Bug51653ErrorPage(trace)); + ctx.addServletMappingDecoded("/error", "errorPage"); + // And the handling for 404 responses + ErrorPage errorPage = new ErrorPage(); + errorPage.setErrorCode(Response.SC_NOT_FOUND); + errorPage.setLocation("/error"); + ctx.addErrorPage(errorPage); + + // Add the request listener + Bug51653RequestListener reqListener = + new Bug51653RequestListener(trace); + ((StandardContext) ctx).addApplicationEventListener(reqListener); + + tomcat.start(); + + // Request a page that does not exist + int rc = getUrl("http://localhost:" + getPort() + "/test", + new ByteChunk(), null); + + // Need to allow time (but not too long in case the test fails) for + // ServletRequestListener to complete + int i = 40; + while (i > 0) { + if (trace.toString().endsWith("Destroy")) { + break; + } + Thread.sleep(250); + i--; + } + + Assert.assertEquals(Response.SC_NOT_FOUND, rc); + Assert.assertEquals("InitErrorDestroy", trace.toString()); + } + + + private static class Bug51653ErrorTrigger extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.sendError(Response.SC_NOT_FOUND); + } + } + + + private static class Bug51653ErrorPage extends HttpServlet { + private static final long serialVersionUID = 1L; + + private StringBuilder sb; + + Bug51653ErrorPage(StringBuilder sb) { + this.sb = sb; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + sb.append("Error"); + + resp.setContentType("text/plain"); + resp.getWriter().write("Error"); + } + } + + + private static class Bug51653RequestListener + implements ServletRequestListener { + + private StringBuilder sb; + + Bug51653RequestListener(StringBuilder sb) { + this.sb = sb; + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + sb.append("Init"); + } + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + sb.append("Destroy"); + } + + } + + + @Test + public void test100ContinueDefault() throws Exception { + // The default setting is IMMEDIATELY + // This test verifies that we get proper 100 Continue responses + // when the continueResponseTiming property is not set + test100Continue(); + } + + + @Test + public void test100ContinueSentImmediately() throws Exception { + final Tomcat tomcat = getTomcatInstance(); + + final Connector connector = tomcat.getConnector(); + connector.setProperty("continueResponseTiming", "immediately"); + + test100Continue(); + } + + + @Test + public void test100ContinueSentOnRequestContentRead() throws Exception { + final Tomcat tomcat = getTomcatInstance(); + + final Connector connector = tomcat.getConnector(); + final String policyString = ContinueResponseTiming.ON_REQUEST_BODY_READ.toString().toLowerCase(Locale.ENGLISH); + connector.setProperty("continueResponseTiming", policyString); + + test100Continue(); + } + + + private void test100Continue() throws Exception { + // Makes a request that expects a 100 Continue response and verifies + // that the 100 Continue response is received. This does not check + // that the correct ContinueResponseTiming was used, just + // that a 100 Continue response is received. The unit tests for + // Request verify that the various settings are correctly implemented. + + final Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + final Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "echo", new EchoBodyServlet()); + ctx.addServletMappingDecoded("/echo", "echo"); + + tomcat.start(); + + final ExpectationClient client = new ExpectationClient(); + + client.setPort(tomcat.getConnector().getLocalPort()); + // Expected content doesn't end with a CR-LF so if it isn't chunked make + // sure the content length is used as reading it line-by-line will fail + // since there is no "line". + client.setUseContentLength(true); + + client.connect(); + + client.doRequestHeaders(); + + Assert.assertTrue(client.isResponse100()); + + client.doRequestBody(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + } +} diff --git a/test/org/apache/catalina/core/TestStandardHostValve.java b/test/org/apache/catalina/core/TestStandardHostValve.java new file mode 100644 index 0000000..2f0799c --- /dev/null +++ b/test/org/apache/catalina/core/TestStandardHostValve.java @@ -0,0 +1,260 @@ +/* + * 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.catalina.core; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Response; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.ErrorPage; + +public class TestStandardHostValve extends TomcatBaseTest { + + @Test + public void testErrorPageHandling400() throws Exception { + doTestErrorPageHandling(400, "", "/400"); + } + + + @Test + public void testErrorPageHandling400WithException() throws Exception { + doTestErrorPageHandling(400, "java.lang.IllegalStateException", "/400"); + } + + + @Test + public void testErrorPageHandling500() throws Exception { + doTestErrorPageHandling(500, "", "/500"); + } + + + @Test + public void testErrorPageHandlingDefault() throws Exception { + doTestErrorPageHandling(501, "", "/default"); + } + + + private void doTestErrorPageHandling(int error, String exception, String report) + throws Exception { + + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add the error page + Tomcat.addServlet(ctx, "error", new ErrorServlet()); + ctx.addServletMappingDecoded("/error", "error"); + + // Add the error handling page + Tomcat.addServlet(ctx, "report", new ReportServlet()); + ctx.addServletMappingDecoded("/report/*", "report"); + + // Add the handling for 400 responses + ErrorPage errorPage400 = new ErrorPage(); + errorPage400.setErrorCode(Response.SC_BAD_REQUEST); + errorPage400.setLocation("/report/400"); + ctx.addErrorPage(errorPage400); + + // And the handling for 500 responses + ErrorPage errorPage500 = new ErrorPage(); + errorPage500.setErrorCode(Response.SC_INTERNAL_SERVER_ERROR); + errorPage500.setLocation("/report/500"); + ctx.addErrorPage(errorPage500); + + // And the default error handling + ErrorPage errorPageDefault = new ErrorPage(); + errorPageDefault.setLocation("/report/default"); + ctx.addErrorPage(errorPageDefault); + + tomcat.start(); + + // Request a page that triggers an error + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/error?errorCode=" + error + "&exception=" + exception, + bc, null); + + Assert.assertEquals(error, rc); + Assert.assertEquals(report, bc.toString()); + } + + + @Test(expected=IllegalArgumentException.class) + public void testInvalidErrorPage() throws Exception { + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add a broken error page configuration + ErrorPage errorPage500 = new ErrorPage(); + errorPage500.setErrorCode("java.lang.Exception"); + errorPage500.setLocation("/report/500"); + ctx.addErrorPage(errorPage500); + } + + + @Test + public void testSRLAfterError() throws Exception { + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add the error page + Tomcat.addServlet(ctx, "error", new ErrorServlet()); + ctx.addServletMappingDecoded("/error", "error"); + + final List result = new ArrayList<>(); + + // Add the request listener + ServletRequestListener servletRequestListener = new ServletRequestListener() { + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + result.add("Visit requestDestroyed"); + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + result.add("Visit requestInitialized"); + } + + }; + ((StandardContext) ctx).addApplicationEventListener(servletRequestListener); + + tomcat.start(); + + // Request a page that triggers an error + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/error?errorCode=400", bc, null); + + Assert.assertEquals(400, rc); + Assert.assertTrue(result.contains("Visit requestInitialized")); + Assert.assertTrue(result.contains("Visit requestDestroyed")); + } + + + @Test + public void testIncompleteResponse() throws Exception { + // Set up a container + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add the error page + Tomcat.addServlet(ctx, "error", new ExceptionServlet()); + ctx.addServletMappingDecoded("/error", "error"); + + // Add the error handling page + Tomcat.addServlet(ctx, "report", new ReportServlet()); + ctx.addServletMappingDecoded("/report/*", "report"); + + // Add the handling for 500 responses + ErrorPage errorPage500 = new ErrorPage(); + errorPage500.setErrorCode(Response.SC_INTERNAL_SERVER_ERROR); + errorPage500.setLocation("/report/500"); + ctx.addErrorPage(errorPage500); + + // Add the default error handling + ErrorPage errorPageDefault = new ErrorPage(); + errorPageDefault.setLocation("/report/default"); + ctx.addErrorPage(errorPageDefault); + + tomcat.start(); + + // Request a page that triggers an error + ByteChunk bc = new ByteChunk(); + Throwable t = null; + try { + getUrl("http://localhost:" + getPort() + "/error", bc, null); + System.out.println(bc.toString()); + } catch (IOException ioe) { + t = ioe; + } + Assert.assertNotNull(t); + } + + + private static class ErrorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + int error = Integer.parseInt(req.getParameter("errorCode")); + resp.sendError(error); + + Throwable t = null; + String exception = req.getParameter("exception"); + if (exception != null && exception.length() > 0) { + try { + t = (Throwable) Class.forName(exception).getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + // Should never happen but in case it does... + t = new IllegalArgumentException(); + } + req.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + } + } + } + + + private static class ExceptionServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.flushBuffer(); + throw new IOException(); + } + } + + + private static class ReportServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String pathInfo = req.getPathInfo(); + resp.setContentType("text/plain"); + resp.getWriter().print(pathInfo); + } + } +} diff --git a/test/org/apache/catalina/core/TestStandardService.java b/test/org/apache/catalina/core/TestStandardService.java new file mode 100644 index 0000000..ba1e954 --- /dev/null +++ b/test/org/apache/catalina/core/TestStandardService.java @@ -0,0 +1,58 @@ +/* + * 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.catalina.core; + +import java.net.InetAddress; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestStandardService extends TomcatBaseTest { + + @Test(expected = IllegalArgumentException.class) + public void testAddInvalidConnectorThrow() throws Exception { + doTestAddInvalidConnector(true); + } + + + @Test + public void testAddInvalidConnectorNoThrow() throws Exception { + doTestAddInvalidConnector(false); + } + + + private void doTestAddInvalidConnector(boolean throwOnFailure) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + Connector connector = tomcat.getConnector(); + + tomcat.start(); + + Connector c2 = new Connector("HTTP/1.1"); + c2.setThrowOnFailure(throwOnFailure); + + Assert.assertTrue(c2.setProperty("address", ((InetAddress) connector.getProperty("address")).getHostAddress())); + c2.setPort(connector.getLocalPort()); + + tomcat.getService().addConnector(c2); + } +} diff --git a/test/org/apache/catalina/core/TestStandardWrapper.java b/test/org/apache/catalina/core/TestStandardWrapper.java new file mode 100644 index 0000000..7338529 --- /dev/null +++ b/test/org/apache/catalina/core/TestStandardWrapper.java @@ -0,0 +1,485 @@ +/* + * 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.catalina.core; + +import java.io.File; +import java.io.IOException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.annotation.HttpConstraint; +import jakarta.servlet.annotation.HttpMethodConstraint; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.authenticator.BasicAuthenticator; +import org.apache.catalina.realm.MessageDigestCredentialHandler; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.LoginConfig; + +public class TestStandardWrapper extends TomcatBaseTest { + + @Test + public void testSecurityAnnotationsSimple() throws Exception { + doTest(DenyAllServlet.class.getName(), false, false, false, false); + } + + @Test + public void testSecurityAnnotationsSubclass1() throws Exception { + doTest(SubclassDenyAllServlet.class.getName(), + false, false, false,false); + } + + @Test + public void testSecurityAnnotationsSubclass2() throws Exception { + doTest(SubclassAllowAllServlet.class.getName(), + false, false, true, false); + } + + @Test + public void testSecurityAnnotationsMethods1() throws Exception { + doTest(MethodConstraintServlet.class.getName(), + false, false, false, false); + } + + @Test + public void testSecurityAnnotationsMethods2() throws Exception { + doTest(MethodConstraintServlet.class.getName(), + true, false, true, false); + } + + @Test + public void testSecurityAnnotationsRole1() throws Exception { + doTest(RoleAllowServlet.class.getName(), false, true, true, false); + } + + @Test + public void testSecurityAnnotationsRole2() throws Exception { + doTest(RoleDenyServlet.class.getName(), false, true, false, false); + } + + @Test + public void testSecurityAnnotationsUncoveredGet01() throws Exception { + // Use a POST with role - should be allowed + doTest(UncoveredGetServlet.class.getName(), true, true, true, false); + } + + @Test + public void testSecurityAnnotationsUncoveredGet02() throws Exception { + // Use a POST with role - should be allowed + doTest(UncoveredGetServlet.class.getName(), true, true, true, true); + } + + @Test + public void testSecurityAnnotationsUncoveredGet03() throws Exception { + // Use a POST no role - should be blocked + doTest(UncoveredGetServlet.class.getName(), true, false, false, false); + } + + @Test + public void testSecurityAnnotationsUncoveredGet04() throws Exception { + // Use a POST no role - should be blocked + doTest(UncoveredGetServlet.class.getName(), true, false, false, true); + } + + @Test + public void testSecurityAnnotationsUncoveredGet05() throws Exception { + // Use a GET with role - should be allowed as denyUncovered is false + doTest(UncoveredGetServlet.class.getName(), false, true, true, false); + } + + @Test + public void testSecurityAnnotationsUncoveredGet06() throws Exception { + // Use a GET with role - should be blocked as denyUncovered is true + doTest(UncoveredGetServlet.class.getName(), false, true, false, true); + } + + @Test + public void testSecurityAnnotationsUncoveredGet07() throws Exception { + // Use a GET no role - should be allowed as denyUncovered is false + doTest(UncoveredGetServlet.class.getName(), false, false, true, false); + } + + @Test + public void testSecurityAnnotationsUncoveredGet08() throws Exception { + // Use a GET no role - should be blocked as denyUncovered is true + doTest(UncoveredGetServlet.class.getName(), true, false, false, true); + } + + @Test + public void testSecurityAnnotationsWebXmlPriority() throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + Context ctx = tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + skipTldsForResourceJars(ctx); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + rc = getUrl("http://localhost:" + getPort() + + "/testStandardWrapper/securityAnnotationsWebXmlPriority", + bc, null, null); + + Assert.assertTrue(bc.getLength() > 0); + Assert.assertEquals(403, rc); + } + + @Test + public void testSecurityAnnotationsMetaDataPriority() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk bc = new ByteChunk(); + int rc; + rc = getUrl("http://localhost:" + getPort() + + "/test/testStandardWrapper/securityAnnotationsMetaDataPriority", + bc, null, null); + + Assert.assertEquals("OK", bc.toString()); + Assert.assertEquals(200, rc); + } + + @Test + public void testSecurityAnnotationsAddServlet1() throws Exception { + doTestSecurityAnnotationsAddServlet(false); + } + + @Test + public void testSecurityAnnotationsAddServlet2() throws Exception { + doTestSecurityAnnotationsAddServlet(true); + } + + @Test + public void testSecurityAnnotationsNoWebXmlConstraints() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-servletsecurity-a"); + tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + rc = getUrl("http://localhost:" + getPort() + "/", + bc, null, null); + + Assert.assertTrue(bc.getLength() > 0); + Assert.assertEquals(403, rc); + } + + @Test + public void testSecurityAnnotationsNoWebXmlLoginConfig() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-servletsecurity-b"); + tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + rc = getUrl("http://localhost:" + getPort() + "/protected.jsp", + bc, null, null); + + Assert.assertTrue(bc.getLength() > 0); + Assert.assertEquals(403, rc); + + bc.recycle(); + + rc = getUrl("http://localhost:" + getPort() + "/unprotected.jsp", + bc, null, null); + + Assert.assertEquals(200, rc); + Assert.assertTrue(bc.toString().contains("00-OK")); + } + + @Test + public void testRoleMappingInEngine() throws Exception { + doTestRoleMapping("engine"); + } + + @Test + public void testRoleMappingInHost() throws Exception { + doTestRoleMapping("host"); + } + + @Test + public void testRoleMappingInContext() throws Exception { + doTestRoleMapping("context"); + } + + private void doTestRoleMapping(String realmContainer) + throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addRoleMapping("testRole", "very-complex-role-name"); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", RoleAllowServlet.class.getName()); + ctx.addServletMappingDecoded("/", "servlet"); + + ctx.setLoginConfig(new LoginConfig("BASIC", null, null, null)); + ctx.getPipeline().addValve(new BasicAuthenticator()); + + TesterMapRealm realm = new TesterMapRealm(); + MessageDigestCredentialHandler ch = new MessageDigestCredentialHandler(); + ch.setAlgorithm("SHA"); + realm.setCredentialHandler(ch); + + /* Attach the realm to the appropriate container, but role mapping must + * always succeed because it is evaluated at context level. + */ + if (realmContainer.equals("engine")) { + tomcat.getEngine().setRealm(realm); + } else if (realmContainer.equals("host")) { + tomcat.getHost().setRealm(realm); + } else if (realmContainer.equals("context")) { + ctx.setRealm(realm); + } else { + throw new IllegalArgumentException("realmContainer is invalid"); + } + + realm.addUser("testUser", ch.mutate("testPwd")); + realm.addUserRole("testUser", "testRole1"); + realm.addUserRole("testUser", "very-complex-role-name"); + realm.addUserRole("testUser", "another-very-complex-role-name"); + + tomcat.start(); + + Principal p = realm.authenticate("testUser", "testPwd"); + + Assert.assertNotNull(p); + Assert.assertEquals("testUser", p.getName()); + // This one is mapped + Assert.assertTrue(realm.hasRole(wrapper, p, "testRole")); + Assert.assertTrue(realm.hasRole(wrapper, p, "testRole1")); + Assert.assertFalse(realm.hasRole(wrapper, p, "testRole2")); + Assert.assertTrue(realm.hasRole(wrapper, p, "very-complex-role-name")); + Assert.assertTrue(realm.hasRole(wrapper, p, "another-very-complex-role-name")); + + // This now tests RealmBase#hasResourcePermission() because we need a wrapper + // to be passed from an authenticator + ByteChunk bc = new ByteChunk(); + Map> reqHeaders = new HashMap<>(); + List authHeaders = new ArrayList<>(); + // testUser, testPwd + authHeaders.add("Basic dGVzdFVzZXI6dGVzdFB3ZA=="); + reqHeaders.put("Authorization", authHeaders); + + int rc = getUrl("http://localhost:" + getPort() + "/", bc, reqHeaders, + null); + + Assert.assertEquals("OK", bc.toString()); + Assert.assertEquals(200, rc); + } + + private void doTestSecurityAnnotationsAddServlet(boolean useCreateServlet) + throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Servlet s = new DenyAllServlet(); + ServletContainerInitializer sci = new SCI(s, useCreateServlet); + ctx.addServletContainerInitializer(sci, null); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc; + rc = getUrl("http://localhost:" + getPort() + "/", bc, null, null); + + if (useCreateServlet) { + Assert.assertTrue(bc.getLength() > 0); + Assert.assertEquals(403, rc); + } else { + Assert.assertEquals("OK", bc.toString()); + Assert.assertEquals(200, rc); + } + } + + private void doTest(String servletClassName, boolean usePost, + boolean useRole, boolean expect200, boolean denyUncovered) + throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + ctx.setDenyUncoveredHttpMethods(denyUncovered); + + Wrapper wrapper = Tomcat.addServlet(ctx, "servlet", servletClassName); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/", "servlet"); + + if (useRole) { + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser("testUser", "testPwd"); + realm.addUserRole("testUser", "testRole"); + ctx.setRealm(realm); + + ctx.setLoginConfig(new LoginConfig("BASIC", null, null, null)); + ctx.getPipeline().addValve(new BasicAuthenticator()); + } + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + Map> reqHeaders = null; + if (useRole) { + reqHeaders = new HashMap<>(); + List authHeaders = new ArrayList<>(); + // testUser, testPwd + authHeaders.add("Basic dGVzdFVzZXI6dGVzdFB3ZA=="); + reqHeaders.put("Authorization", authHeaders); + } + + int rc; + if (usePost) { + rc = postUrl(null, "http://localhost:" + getPort() + "/", bc, + reqHeaders, null); + } else { + rc = getUrl("http://localhost:" + getPort() + "/", bc, reqHeaders, + null); + } + + if (expect200) { + Assert.assertEquals("OK", bc.toString()); + Assert.assertEquals(200, rc); + } else { + Assert.assertTrue(bc.getLength() > 0); + Assert.assertEquals(403, rc); + } + } + + public static class TestServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + doGet(req, resp); + } + } + + @ServletSecurity(@HttpConstraint(EmptyRoleSemantic.DENY)) + public static class DenyAllServlet extends TestServlet { + private static final long serialVersionUID = 1L; + } + + public static class SubclassDenyAllServlet extends DenyAllServlet { + private static final long serialVersionUID = 1L; + } + + @ServletSecurity(@HttpConstraint(EmptyRoleSemantic.PERMIT)) + public static class SubclassAllowAllServlet extends DenyAllServlet { + private static final long serialVersionUID = 1L; + } + + @ServletSecurity(value= @HttpConstraint(EmptyRoleSemantic.PERMIT), + httpMethodConstraints = { + @HttpMethodConstraint(value="GET", + emptyRoleSemantic = EmptyRoleSemantic.DENY) + } + ) + public static class MethodConstraintServlet extends TestServlet { + private static final long serialVersionUID = 1L; + } + + @ServletSecurity(httpMethodConstraints = { + @HttpMethodConstraint(value="POST",rolesAllowed = "testRole") + } + ) + public static class UncoveredGetServlet extends TestServlet { + private static final long serialVersionUID = 1L; + } + + @ServletSecurity(@HttpConstraint(rolesAllowed = "testRole")) + public static class RoleAllowServlet extends TestServlet { + private static final long serialVersionUID = 1L; + } + + @ServletSecurity(@HttpConstraint(rolesAllowed = "otherRole")) + public static class RoleDenyServlet extends TestServlet { + private static final long serialVersionUID = 1L; + } + + public static class SCI implements ServletContainerInitializer { + + private Servlet servlet; + private boolean createServlet; + + public SCI(Servlet servlet, boolean createServlet) { + this.servlet = servlet; + this.createServlet = createServlet; + } + + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + Servlet s; + + if (createServlet) { + s = ctx.createServlet(servlet.getClass()); + } else { + s = servlet; + } + ServletRegistration.Dynamic r = ctx.addServlet("servlet", s); + r.addMapping("/"); + } + } +} diff --git a/test/org/apache/catalina/core/TestSwallowAbortedUploads.java b/test/org/apache/catalina/core/TestSwallowAbortedUploads.java new file mode 100644 index 0000000..385bfba --- /dev/null +++ b/test/org/apache/catalina/core/TestSwallowAbortedUploads.java @@ -0,0 +1,485 @@ +/* + * 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.catalina.core; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Writer; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; + +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Part; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class TestSwallowAbortedUploads extends TomcatBaseTest { + + private static Log log = LogFactory.getLog(TestSwallowAbortedUploads.class); + + /* + * Test whether size limited uploads correctly handle connection draining. + */ + public Exception doAbortedUploadTest(AbortedUploadClient client, boolean limited, + boolean swallow) { + Exception ex = client.doRequest(limited, swallow); + if (log.isDebugEnabled()) { + log.debug("Response line: " + client.getResponseLine()); + log.debug("Response headers: " + client.getResponseHeaders()); + log.debug("Response body: " + client.getResponseBody()); + if (ex != null) { + log.debug("Exception in client: ", ex); + } + + } + return ex; + } + + /* + * Test whether aborted POST correctly handle connection draining. + */ + public Exception doAbortedPOSTTest(AbortedPOSTClient client, int status, + boolean swallow) { + Exception ex = client.doRequest(status, swallow); + if (log.isDebugEnabled()) { + log.debug("Response line: " + client.getResponseLine()); + log.debug("Response headers: " + client.getResponseHeaders()); + log.debug("Response body: " + client.getResponseBody()); + if (ex != null) { + log.info("Exception in client: ", ex); + } + + } + return ex; + } + + @Test + public void testAbortedUploadUnlimitedSwallow() { + log.info("Unlimited, swallow enabled"); + AbortedUploadClient client = new AbortedUploadClient(); + Exception ex = doAbortedUploadTest(client, false, true); + Assert.assertNull("Unlimited upload with swallow enabled generates client exception", + ex); + Assert.assertTrue("Unlimited upload with swallow enabled returns error status code", + client.isResponse200()); + client.reset(); + } + + @Test + public void testAbortedUploadUnlimitedNoSwallow() { + log.info("Unlimited, swallow disabled"); + AbortedUploadClient client = new AbortedUploadClient(); + Exception ex = doAbortedUploadTest(client, false, false); + Assert.assertNull("Unlimited upload with swallow disabled generates client exception", + ex); + Assert.assertTrue("Unlimited upload with swallow disabled returns error status code", + client.isResponse200()); + client.reset(); + } + + @Test + public void testAbortedUploadLimitedSwallow() { + log.info("Limited, swallow enabled"); + AbortedUploadClient client = new AbortedUploadClient(); + Exception ex = doAbortedUploadTest(client, true, true); + Assert.assertNull("Limited upload with swallow enabled generates client exception", + ex); + Assert.assertTrue("Limited upload with swallow enabled returns non-500 status code", + client.isResponse500()); + client.reset(); + } + + @Test + public void testAbortedUploadLimitedNoSwallow() { + log.info("Limited, swallow disabled"); + AbortedUploadClient client = new AbortedUploadClient(); + Exception ex = doAbortedUploadTest(client, true, false); + assertThat("Limited upload with swallow disabled does not generate client exception", + ex, instanceOf(java.net.SocketException.class)); + client.reset(); + } + + @Test + public void testAbortedPOSTOKSwallow() { + log.info("Aborted (OK), swallow enabled"); + AbortedPOSTClient client = new AbortedPOSTClient(); + Exception ex = doAbortedPOSTTest(client, HttpServletResponse.SC_OK, true); + Assert.assertNull("Unlimited upload with swallow enabled generates client exception", + ex); + Assert.assertTrue("Unlimited upload with swallow enabled returns error status code", + client.isResponse200()); + client.reset(); + } + + @Test + public void testAbortedPOSTOKNoSwallow() { + log.info("Aborted (OK), swallow disabled"); + AbortedPOSTClient client = new AbortedPOSTClient(); + Exception ex = doAbortedPOSTTest(client, HttpServletResponse.SC_OK, false); + Assert.assertNull("Unlimited upload with swallow disabled generates client exception", + ex); + Assert.assertTrue("Unlimited upload with swallow disabled returns error status code", + client.isResponse200()); + client.reset(); + } + + @Test + public void testAbortedPOST413Swallow() { + log.info("Aborted (413), swallow enabled"); + AbortedPOSTClient client = new AbortedPOSTClient(); + Exception ex = doAbortedPOSTTest(client, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, true); + Assert.assertNull("Limited upload with swallow enabled generates client exception", + ex); + Assert.assertTrue("Limited upload with swallow enabled returns error status code", + client.isResponse413()); + client.reset(); + } + + @Test + public void testAbortedPOST413NoSwallow() { + log.info("Aborted (413), swallow disabled"); + AbortedPOSTClient client = new AbortedPOSTClient(); + Exception ex = doAbortedPOSTTest(client, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, false); + assertThat("Limited upload with swallow disabled does not generate client exception", + ex, instanceOf(java.net.SocketException.class)); + client.reset(); + } + + @MultipartConfig + private static class AbortedUploadServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + PrintWriter out = resp.getWriter(); + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + StringBuilder sb = new StringBuilder(); + try { + Collection c = req.getParts(); + if (c == null) { + log.debug("Count: -1"); + sb.append("Count: -1\n"); + } else { + log.debug("Count: " + c.size()); + sb.append("Count: " + c.size() + "\n"); + for (Part p : c) { + log.debug("Name: " + p.getName() + ", Size: " + + p.getSize()); + sb.append("Name: " + p.getName() + ", Size: " + + p.getSize() + "\n"); + } + } + } catch (IllegalStateException ex) { + log.debug("IllegalStateException during getParts()"); + sb.append("IllegalStateException during getParts()\n"); + resp.setStatus(500); + } catch (Throwable ex) { + log.error("Exception during getParts()", ex); + sb.append(ex); + resp.setStatus(500); + } + out.print(sb.toString()); + resp.flushBuffer(); + } + + } + + /** + * Test no connection draining when upload too large + */ + private class AbortedUploadClient extends SimpleHttpClient { + + private static final String URI = "/uploadAborted"; + private static final String servletName = "uploadAborted"; + private static final int limitSize = 100; + private static final int hugeSize = 10000000; + + private Context context; + + private synchronized void init(boolean limited, boolean swallow) + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + context = tomcat.addContext("", TEMP_DIR); + Wrapper w; + w = Tomcat.addServlet(context, servletName, + new AbortedUploadServlet()); + // Tomcat.addServlet does not respect annotations, so we have + // to set our own MultipartConfigElement. + // Choose upload file size limit. + if (limited) { + w.setMultipartConfigElement(new MultipartConfigElement("", + limitSize, -1, -1)); + } else { + w.setMultipartConfigElement(new MultipartConfigElement("")); + } + context.addServletMappingDecoded(URI, servletName); + context.setSwallowAbortedUploads(swallow); + + Connector c = tomcat.getConnector(); + c.setMaxPostSize(2 * hugeSize); + Assert.assertTrue(c.setProperty("maxSwallowSize", Integer.toString(hugeSize))); + + tomcat.start(); + setPort(c.getLocalPort()); + } + + private Exception doRequest(boolean limited, boolean swallow) { + char body[] = new char[hugeSize]; + Arrays.fill(body, 'X'); + + try { + init(limited, swallow); + + // Open connection + connect(); + + // Send specified request body using method + String[] request; + + String boundary = "--simpleboundary"; + StringBuilder sb = new StringBuilder(); + + sb.append("--"); + sb.append(boundary); + sb.append(CRLF); + sb.append("Content-Disposition: form-data; name=\"part\""); + sb.append(CRLF); + sb.append(CRLF); + sb.append(body); + sb.append(CRLF); + sb.append("--"); + sb.append(boundary); + sb.append("--"); + sb.append(CRLF); + + // Re-encode the content so that bytes = characters + String content = new String(sb.toString().getBytes("UTF-8"), + "ASCII"); + + request = new String[] { "POST http://localhost:" + getPort() + URI + " HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Connection: close" + CRLF + + "Content-Type: multipart/form-data; boundary=" + boundary + CRLF + + "Content-Length: " + content.length() + CRLF + + CRLF + + content + CRLF }; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + return false; // Don't care + } + } + + private static class AbortedPOSTServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final int status; + + AbortedPOSTServlet(int status) { + this.status = status; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.setStatus(status); + PrintWriter out = resp.getWriter(); + out.print("OK"); + resp.flushBuffer(); + } + + } + + /** + * Test no connection draining when upload too large + */ + private class AbortedPOSTClient extends SimpleHttpClient { + + private static final String URI = "/uploadAborted"; + private static final String servletName = "uploadAborted"; + private static final int hugeSize = 10000000; + + private Context context; + + private synchronized void init(int status, boolean swallow) + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + context = tomcat.addContext("", TEMP_DIR); + AbortedPOSTServlet servlet = new AbortedPOSTServlet(status); + Tomcat.addServlet(context, servletName, servlet); + context.addServletMappingDecoded(URI, servletName); + context.setSwallowAbortedUploads(swallow); + + tomcat.start(); + + Connector c = tomcat.getConnector(); + c.setMaxPostSize(2 * hugeSize); + Assert.assertTrue(c.setProperty("maxSwallowSize", Integer.toString(hugeSize))); + + setPort(c.getLocalPort()); + } + + private Exception doRequest(int status, boolean swallow) { + char body[] = new char[hugeSize]; + Arrays.fill(body, 'X'); + + try { + init(status, swallow); + + // Open connection + connect(); + + // Send specified request body using method + String[] request; + + String content = new String(body); + + request = new String[] { "POST http://localhost:" + getPort() + URI + " HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Connection: close" + CRLF + + "Content-Length: " + content.length() + CRLF + + CRLF + + content + CRLF }; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + return false; // Don't care + } + } + + + @Test + public void testChunkedPUTLimit() throws Exception { + doTestChunkedPUT(true); + } + + + @Test + public void testChunkedPUTNoLimit() throws Exception { + doTestChunkedPUT(false); + } + + + public void doTestChunkedPUT(boolean limit) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + tomcat.addContext("", TEMP_DIR); + // No need for target to exist. + + if (!limit) { + Assert.assertTrue(tomcat.getConnector().setProperty("maxSwallowSize", "-1")); + } + + tomcat.start(); + + Exception writeEx = null; + Exception readEx = null; + String responseLine = null; + + try (Socket conn = new Socket("localhost", getPort())) { + Writer writer = new OutputStreamWriter( + conn.getOutputStream(), StandardCharsets.US_ASCII); + writer.write("PUT /does-not-exist HTTP/1.1\r\n"); + writer.write("Host: any\r\n"); + writer.write("Transfer-encoding: chunked\r\n"); + writer.write("\r\n"); + + // Smarter than the typical client. Attempts to read the response + // even if the request is not fully written. + try { + // Write (or try to write) 16 MiB + for (int i = 0; i < 1024 * 1024; i++) { + writer.write("10\r\n"); + writer.write("0123456789ABCDEF\r\n"); + } + } catch (Exception e) { + writeEx = e; + } + + try { + BufferedReader reader = new BufferedReader(new InputStreamReader( + conn.getInputStream(), StandardCharsets.US_ASCII)); + + responseLine = reader.readLine(); + } catch (IOException e) { + readEx = e; + } + } + + if (limit) { + Assert.assertNotNull(writeEx); + } else { + Assert.assertNull(writeEx); + Assert.assertNull(readEx); + Assert.assertNotNull(responseLine); + Assert.assertTrue(responseLine.contains("404")); + } + } +} diff --git a/test/org/apache/catalina/core/TesterApplicationHttpRequestPerformance.java b/test/org/apache/catalina/core/TesterApplicationHttpRequestPerformance.java new file mode 100644 index 0000000..03d5932 --- /dev/null +++ b/test/org/apache/catalina/core/TesterApplicationHttpRequestPerformance.java @@ -0,0 +1,52 @@ +/* + * 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.catalina.core; + +import org.junit.Test; + +import org.apache.catalina.connector.Request; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterApplicationHttpRequestPerformance { + + @Test + public void testGetAttribute() { + org.apache.coyote.Request coyoteRequest = new org.apache.coyote.Request(); + Request request = new Request(null); + request.setCoyoteRequest(coyoteRequest); + ApplicationHttpRequest applicationHttpRequest = new ApplicationHttpRequest(request, null ,false); + + // Warm-up + doTestGetAttribute(applicationHttpRequest); + + long start = System.nanoTime(); + doTestGetAttribute(applicationHttpRequest); + long duration = System.nanoTime() - start; + + System.out.println(duration + "ns"); + } + + + private void doTestGetAttribute(ApplicationHttpRequest request) { + for (int i = 0; i < 100000000; i++) { + request.getAttribute("Unknown"); + } + } +} diff --git a/test/org/apache/catalina/core/TesterDefaultInstanceManagerPerformance.java b/test/org/apache/catalina/core/TesterDefaultInstanceManagerPerformance.java new file mode 100644 index 0000000..cbea78d --- /dev/null +++ b/test/org/apache/catalina/core/TesterDefaultInstanceManagerPerformance.java @@ -0,0 +1,97 @@ +/* + * 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.catalina.core; + +import java.lang.reflect.InvocationTargetException; + +import javax.naming.NamingException; + +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.InstanceManager; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterDefaultInstanceManagerPerformance extends TomcatBaseTest { + + @Test + public void testConcurrency() throws Exception { + // Create a populated InstanceManager + Tomcat tomcat = getTomcatInstance(); + Context ctx = tomcat.addContext(null, "", null); + + tomcat.start(); + + InstanceManager im = ctx.getInstanceManager(); + + for (int i = 1; i < 9; i++) { + doTestConcurrency(im, i); + } + } + + + private void doTestConcurrency(InstanceManager im, int threadCount) throws Exception { + long start = System.nanoTime(); + + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(new InstanceManagerRunnable(im)); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + + long duration = System.nanoTime() - start; + + System.out.println(threadCount + " threads completed in " + duration + "ns"); + } + + + private static class InstanceManagerRunnable implements Runnable { + + private final InstanceManager im; + + private InstanceManagerRunnable(InstanceManager im) { + this.im = im; + } + + @Override + public void run() { + try { + Object test = new DefaultServlet(); + for (int i = 0; i < 200000; i++) { + im.newInstance(test); + im.destroyInstance(test); + } + } catch (NamingException | IllegalAccessException | InvocationTargetException ne) { + ne.printStackTrace(); + } + } + } +} diff --git a/test/org/apache/catalina/core/TesterTldListener.java b/test/org/apache/catalina/core/TesterTldListener.java new file mode 100644 index 0000000..37c6dbf --- /dev/null +++ b/test/org/apache/catalina/core/TesterTldListener.java @@ -0,0 +1,59 @@ +/* + * 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.catalina.core; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +public class TesterTldListener implements ServletContextListener { + + private static StringBuilder log = new StringBuilder(); + + public static String getLog() { + return log.toString(); + } + + private ServletContext servletContext; + + @Override + public void contextInitialized(ServletContextEvent sce) { + + ServletContext sc = sce.getServletContext(); + servletContext = sc; + + // Try and use one of the Servlet 3.0 methods that should be blocked + try { + sc.getServletRegistrations(); + log.append("FAIL-01"); + } catch (UnsupportedOperationException uoe) { + log.append("PASS-01"); + } catch (Exception e) { + log.append("FAIL-02"); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + // Bug 57446. Same ServletContext should be presented as at init + if (servletContext == sce.getServletContext()) { + log.append("PASS-02"); + } else { + log.append("FAIL-03"); + } + } +} diff --git a/test/org/apache/catalina/filters/TestAddCharSetFilter.java b/test/org/apache/catalina/filters/TestAddCharSetFilter.java new file mode 100644 index 0000000..f662357 --- /dev/null +++ b/test/org/apache/catalina/filters/TestAddCharSetFilter.java @@ -0,0 +1,179 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; + +public class TestAddCharSetFilter extends TomcatBaseTest { + + @Test + public void testNoneSpecifiedMode1() throws Exception { + doTest(null, "ISO-8859-1"); + } + + @Test + public void testNoneSpecifiedMode2() throws Exception { + doTest(null, "ISO-8859-2", 2, true); + } + + @Test + public void testNoneSpecifiedMode3() throws Exception { + doTest(null, "ISO-8859-3", 3, true); + } + + @Test + public void testDefault() throws Exception { + doTest("default", "ISO-8859-1"); + } + + @Test + public void testDefaultMixedCase() throws Exception { + doTest("dEfAuLt", "ISO-8859-1"); + } + + @Test + public void testSystem() throws Exception { + doTest("system", Charset.defaultCharset().name()); + } + + @Test + public void testSystemMixedCase() throws Exception { + doTest("SyStEm", Charset.defaultCharset().name()); + } + + @Test + public void testUTF8() throws Exception { + doTest("utf-8", "utf-8"); + } + + + private void doTest(String encoding, String expected) throws Exception { + doTest(encoding, expected, 1, true); + tearDown(); + setUp(); + doTest(encoding, expected, 1, false); + } + + private void doTest(String encoding, String expected, int mode, boolean useSetContentType) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add the Servlet + CharsetServlet servlet = new CharsetServlet(mode, useSetContentType); + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/", "servlet"); + + // Add the Filter + FilterDef filterDef = new FilterDef(); + filterDef.setFilterClass(AddDefaultCharsetFilter.class.getName()); + filterDef.setFilterName("filter"); + if (encoding != null) { + filterDef.addInitParameter("encoding", encoding); + } + ctx.addFilterDef(filterDef); + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName("filter"); + filterMap.addServletName("servlet"); + ctx.addFilterMap(filterMap); + + tomcat.start(); + + Map> headers = new HashMap<>(); + getUrl("http://localhost:" + getPort() + "/", new ByteChunk(), headers); + + String ct = getSingleHeader("Content-Type", headers).toLowerCase(Locale.ENGLISH); + Assert.assertEquals("text/plain;charset=" + expected.toLowerCase(Locale.ENGLISH), ct); + } + + private static class CharsetServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + private static final String OUTPUT = "OK"; + + private final int mode; + private final boolean useSetContentType; + + CharsetServlet(int mode, boolean useSetContentType) { + this.mode = mode; + this.useSetContentType = useSetContentType; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + String value; + switch (mode) { + case 1: + value = "text/plain"; + if (useSetContentType) { + resp.setContentType(value); + } else { + resp.setHeader("Content-Type", value); + } + break; + case 2: + value = "text/plain;charset=ISO-8859-2"; + if (useSetContentType) { + resp.setContentType(value); + } else { + resp.setHeader("Content-Type", value); + } + break; + case 3: + if (useSetContentType) { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("ISO-8859-3"); + } else { + resp.setHeader("Content-Type", "text/plain;charset=ISO-8859-3"); + } + break; + default: + value = "text/plain;charset=ISO-8859-4"; + if (useSetContentType) { + resp.setContentType(value); + } else { + resp.setHeader("Content-Type", value); + } + break; + } + + resp.getWriter().print(OUTPUT); + } + } +} diff --git a/test/org/apache/catalina/filters/TestCorsFilter.java b/test/org/apache/catalina/filters/TestCorsFilter.java new file mode 100644 index 0000000..c5f68b1 --- /dev/null +++ b/test/org/apache/catalina/filters/TestCorsFilter.java @@ -0,0 +1,1315 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.http.RequestUtil; + +public class TestCorsFilter { + private FilterChain filterChain = new TesterFilterChain(); + + /* + * Tests if a GET request is treated as simple request. + * + * @See http://www.w3.org/TR/cors/#simple-method + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterSimpleGET() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals("*")); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.SIMPLE.name().toLowerCase(Locale.ENGLISH))); + } + + /* + * Tests if a POST request is treated as simple request. + * + * @See http://www.w3.org/TR/cors/#simple-method + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterSimplePOST() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setContentType("text/plain"); + request.setMethod("POST"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals("*")); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.SIMPLE.name().toLowerCase(Locale.ENGLISH))); + } + + /* + * Tests if a HEAD request is treated as simple request. + * + * @See http://www.w3.org/TR/cors/#simple-method + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterSimpleHEAD() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("HEAD"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals("*")); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.SIMPLE.name().toLowerCase(Locale.ENGLISH))); + } + + /* + * Test the presence of specific origin in response, when '*' is not used. + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterSimpleSpecificHeader() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("POST"); + request.setContentType("text/plain"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.SIMPLE.name().toLowerCase(Locale.ENGLISH))); + } + + /* + * Tests the that supports credentials may not be enabled with any origin, '*'. + * + * @throws ServletException + */ + @Test(expected = ServletException.class) + public void testDoFilterSimpleAnyOriginAndSupportsCredentials() throws ServletException { + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigAnyOriginAndSupportsCredentials()); + } + + /* + * Tests the presence of the origin (and not '*') in the response, when supports credentials is enabled alongwith + * any origin, '*'. + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterSimpleAnyOriginAndSupportsCredentialsDisabled() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigAnyOriginAndSupportsCredentialsDisabled()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN) + .equals(TesterFilterConfigs.ANY_ORIGIN)); + Assert.assertNull(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS)); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.SIMPLE.name().toLowerCase(Locale.ENGLISH))); + } + + /* + * Tests the presence of exposed headers in response, if configured. + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterSimpleWithExposedHeaders() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("POST"); + request.setContentType("text/plain"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigWithExposedHeaders()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals("*")); + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS) + .equals(TesterFilterConfigs.EXPOSED_HEADERS)); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.SIMPLE.name().toLowerCase(Locale.ENGLISH))); + } + + /* + * Checks if an OPTIONS request is processed as pre-flight. + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterPreflight() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase(Locale.ENGLISH))); + Assert.assertTrue( + request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals("Content-Type")); + } + + /* + * Checks if an OPTIONS request is processed as pre-flight where any origin is enabled. + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterPreflightAnyOrigin() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase(Locale.ENGLISH))); + Assert.assertTrue( + request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals("Content-Type")); + } + + /* + * Checks if an OPTIONS request is processed as pre-flight. + * + * @throws IOException + * + * @throws ServletException + */ + @Test + public void testDoFilterPreflightInvalidOrigin() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://www.example.com"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_FORBIDDEN); + } + + @Test + public void testDoFilterPreflightNegativeMaxAge() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfigNegativeMaxAge()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertNull(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE)); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase(Locale.ENGLISH))); + Assert.assertTrue( + request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals("Content-Type")); + } + + @Test + public void testDoFilterPreflightWithCredentials() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSecureFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue( + response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS).equals("true")); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase(Locale.ENGLISH))); + Assert.assertTrue( + request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals("Content-Type")); + } + + @Test + public void testDoFilterPreflightWithoutCredentialsAndSpecificOrigin() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigSpecificOriginAndSupportsCredentialsDisabled()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertNull(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS)); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase(Locale.ENGLISH))); + Assert.assertTrue( + request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals("Content-Type")); + } + + /* + * Negative test, when a CORS request arrives, with no origin header. + */ + @Test + public void testDoFilterNoOrigin() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + + request.setMethod("POST"); + request.setContentType("text/plain"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.NOT_CORS, requestType); + + corsFilter.doFilter(request, response, filterChain); + + Assert.assertFalse( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + } + + /* + * Negative test, when a non-CORS request arrives, with an origin header. + */ + @Test + public void testDoFilterSameHostWithOrigin01() throws IOException, ServletException { + doTestDoFilterSameHostWithOrigin01("http://localhost:8080", "http", "localhost", 8080, false); + } + + @Test + public void testDoFilterSameHostWithOrigin02() throws IOException, ServletException { + doTestDoFilterSameHostWithOrigin01("http://localhost:8080", "https", "localhost", 8080, true); + } + + @Test + public void testDoFilterSameHostWithOrigin03() throws IOException, ServletException { + doTestDoFilterSameHostWithOrigin01("http://localhost:8080", "http", "localhost", 8081, true); + } + + @Test + public void testDoFilterSameHostWithOrigin04() throws IOException, ServletException { + doTestDoFilterSameHostWithOrigin01("http://localhost:8080", "http", "foo.dev.local", 8080, true); + } + + @Test + public void testDoFilterSameHostWithOrigin05() throws IOException, ServletException { + doTestDoFilterSameHostWithOrigin01("https://localhost:8443", "https", "localhost", 8443, false); + } + + @Test + public void testDoFilterSameHostWithOrigin06() throws IOException, ServletException { + doTestDoFilterSameHostWithOrigin01("https://localhost", "https", "localhost", 443, false); + } + + @Test + public void testDoFilterSameHostWithOrigin07() throws IOException, ServletException { + doTestDoFilterSameHostWithOrigin01("http://localhost", "http", "localhost", 80, false); + } + + private void doTestDoFilterSameHostWithOrigin01(String origin, String scheme, String host, int port, boolean isCors) + throws IOException, ServletException { + + TesterHttpServletRequest request = new TesterHttpServletRequest(); + + request.setMethod("POST"); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, origin); + request.setScheme(scheme); + request.setServerName(host); + request.setServerPort(port); + request.setContentType("text/plain"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + if (isCors) { + Assert.assertNotEquals(CorsFilter.CORSRequestType.NOT_CORS, requestType); + } else { + Assert.assertEquals(CorsFilter.CORSRequestType.NOT_CORS, requestType); + } + + corsFilter.doFilter(request, response, filterChain); + + if (isCors) { + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + } else { + Assert.assertFalse( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + } + } + + @Test + public void testDoFilterInvalidCORSOriginNotAllowed() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "www.google.com"); + request.setMethod("POST"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * A CORS request arrives with a "null" origin which is allowed by default. + */ + @Test + public void testDoFilterNullOriginAllowedByDefault() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + + request.setMethod("POST"); + request.setContentType("text/plain"); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "null"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.SIMPLE, requestType); + + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + } + + /* + * A CORS request arrives with a "null" origin which is explicitly allowed by configuration. + */ + @Test + public void testDoFilterNullOriginAllowedByConfiguration() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + + request.setMethod("POST"); + request.setContentType("text/plain"); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "null"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigSpecificOriginNullAllowed()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.SIMPLE, requestType); + + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + } + + @Test(expected = ServletException.class) + public void testDoFilterNullRequestNullResponse() throws IOException, ServletException { + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(null, null, filterChain); + } + + @Test(expected = ServletException.class) + public void testDoFilterNullRequestResponse() throws IOException, ServletException { + TesterHttpServletResponse response = new TesterHttpServletResponse(); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(null, response, filterChain); + } + + @Test(expected = ServletException.class) + public void testDoFilterRequestNullResponse() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, null, filterChain); + } + + @Test + public void testInitDefaultFilterConfig() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(new FilterConfig() { + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return null; + } + + @Override + public String getInitParameter(String name) { + return null; + } + + @Override + public String getFilterName() { + return null; + } + }); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertNull(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN)); + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN) + .equals(TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE) + .equals(CorsFilter.CORSRequestType.SIMPLE.name().toLowerCase(Locale.ENGLISH))); + } + + @Test(expected = ServletException.class) + public void testInitInvalidFilterConfig() throws ServletException { + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigInvalidMaxPreflightAge()); + // If we don't get an exception at this point, then all mocked objects + // worked as expected. + } + + /* + * Tests if a non-simple request is given to simple request handler. + * + * @throws IOException + * + * @throws ServletException + */ + @Test(expected = IllegalArgumentException.class) + public void testNotSimple() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.handleSimpleCORS(request, response, filterChain); + } + + /* + * When a non-preflight request is given to a pre-flight request handler. + * + * @throws IOException + * + * @throws ServletException + */ + @Test(expected = IllegalArgumentException.class) + public void testNotPreflight() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.handlePreflightCORS(request, response, filterChain); + } + + @Test(expected = IllegalArgumentException.class) + public void testDecorateCORSPropertiesNullRequestNullCORSRequestType() { + CorsFilter.decorateCORSProperties(null, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testDecorateCORSPropertiesNullRequestValidCORSRequestType() { + CorsFilter.decorateCORSProperties(null, CorsFilter.CORSRequestType.SIMPLE); + } + + @Test(expected = IllegalArgumentException.class) + public void testDecorateCORSPropertiesValidRequestNullRequestType() { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + CorsFilter.decorateCORSProperties(request, null); + } + + @Test + public void testDecorateCORSPropertiesCORSRequestTypeNotCORS() { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + CorsFilter.decorateCORSProperties(request, CorsFilter.CORSRequestType.NOT_CORS); + Assert.assertFalse( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + } + + @Test + public void testDecorateCORSPropertiesCORSRequestTypeInvalidCORS() { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + CorsFilter.decorateCORSProperties(request, CorsFilter.CORSRequestType.INVALID_CORS); + Assert.assertNull(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)); + } + + @Test + public void testCheckSimpleRequestTypeAnyOrigin() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://www.w3.org"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.SIMPLE, requestType); + } + + /* + * Happy path test, when a valid CORS Simple request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckSimpleRequestType() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.SIMPLE, requestType); + } + + /* + * Happy path test, when a valid CORS Simple request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckActualRequestType() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setMethod("PUT"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.ACTUAL, requestType); + } + + /* + * Happy path test, when a valid CORS Simple request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckActualRequestTypeMethodPOSTNotSimpleHeaders() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setMethod("POST"); + request.setContentType("application/json"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.ACTUAL, requestType); + } + + /* + * Happy path test, when a valid CORS Pre-flight request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckPreFlightRequestType() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type"); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.PRE_FLIGHT, requestType); + } + + /* + * when a valid CORS Pre-flight request arrives, with no Access-Control-Request-Method + */ + @Test + public void testCheckPreFlightRequestTypeNoACRM() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.ACTUAL, requestType); + } + + /* + * when a valid CORS Pre-flight request arrives, with empty Access-Control-Request-Method + */ + @Test + public void testCheckPreFlightRequestTypeEmptyACRM() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, ""); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.INVALID_CORS, requestType); + } + + /* + * Happy path test, when a valid CORS Pre-flight request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckPreFlightRequestTypeNoHeaders() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.PRE_FLIGHT, requestType); + } + + /* + * Section 6.2.3 + * + * @throws ServletException + * + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeInvalidRequestMethod() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "POLITE"); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * Section Section 6.2.5 + * + * @throws ServletException + * + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeUnsupportedRequestMethod() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "TRACE"); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * Section Section 6.2.6 + * + * @throws ServletException + * + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeUnsupportedRequestHeaders() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "X-ANSWER"); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSecureFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * Section Section 6.2.7 + * + * @throws ServletException + * + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeAnyOriginNoWithCredentials() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, "Origin"); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigAnyOriginAndSupportsCredentialsDisabled()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals("*")); + Assert.assertNull(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + + @Test + public void testCheckPreFlightRequestTypeOriginNotAllowed() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "www.ebay.com"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSecureFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * Happy path test, when a valid CORS Pre-flight request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckPreFlightRequestTypeEmptyHeaders() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader(CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, ""); + request.setMethod("OPTIONS"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.PRE_FLIGHT, requestType); + } + + /* + * Negative test, when a CORS request arrives, with an empty origin. + * + * @throws ServletException + */ + @Test + public void testCheckNotCORSRequestTypeEmptyOrigin() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, ""); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.INVALID_CORS, requestType); + } + + /* + * Tests for failure, when a different domain is used, that's not in the allowed list of origins. + * + * @throws ServletException + * + * @throws IOException + */ + @Test + public void testCheckInvalidOrigin() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "www.example.com"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * Tests for failure, when the 'null' origin is used, and it's not in the list of allowed origins. + */ + @Test + public void testCheckNullOriginNotAllowed() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "null"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * Tests for failure, when a different sub-domain is used, that's not in the allowed list of origins. + * + * @throws ServletException + * + * @throws IOException + */ + @Test + public void testCheckInvalidOriginNotAllowedSubdomain() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://commons.apache.org"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * PUT is not an allowed request method. + * + * @throws ServletException + * + * @throws IOException + */ + @Test + public void testCheckInvalidRequestMethod() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://tomcat.apache.org"); + request.setMethod("PUT"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * When requestMethod is null + * + * @throws ServletException + */ + @Test + public void testCheckNullRequestMethod() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://tomcat.apache.org"); + request.setMethod(null); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.INVALID_CORS, requestType); + } + + /* + * "http://tomcat.apache.org" is an allowed origin and "https://tomcat.apache.org" is not, because scheme doesn't + * match + * + * @throws ServletException + */ + @Test + public void testCheckForSchemeVariance() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "https://tomcat.apache.org"); + request.setMethod("POST"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.INVALID_CORS, requestType); + } + + /* + * "http://tomcat.apache.org" is an allowed origin and "http://tomcat.apache.org:8080" is not, because ports doesn't + * match + * + * @throws ServletException + * + * @throws IOException + */ + @Test + public void testCheckForPortVariance() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://tomcat.apache.org:8080"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + /* + * Tests for failure, when an invalid {@link HttpServletRequest} is encountered. + */ + @Test(expected = IllegalArgumentException.class) + public void testCheckRequestTypeNull() { + HttpServletRequest request = null; + CorsFilter corsFilter = new CorsFilter(); + corsFilter.checkRequestType(request); + } + + @Test + public void testJoin() { + Set elements = new LinkedHashSet<>(); + String separator = ","; + elements.add("world"); + elements.add("peace"); + String join = CorsFilter.join(elements, separator); + Assert.assertTrue("world,peace".equals(join)); + } + + @Test + public void testJoinSingleElement() { + Set elements = new LinkedHashSet<>(); + String separator = ","; + elements.add("world"); + String join = CorsFilter.join(elements, separator); + Assert.assertTrue("world".equals(join)); + } + + @Test + public void testJoinSepNull() { + Set elements = new LinkedHashSet<>(); + String separator = null; + elements.add("world"); + elements.add("peace"); + String join = CorsFilter.join(elements, separator); + Assert.assertTrue("world,peace".equals(join)); + } + + @Test + public void testJoinElementsNull() { + Set elements = null; + String separator = ","; + String join = CorsFilter.join(elements, separator); + + Assert.assertNull(join); + } + + @Test + public void testJoinOneNullElement() { + Set elements = new LinkedHashSet<>(); + String separator = ","; + elements.add(null); + elements.add("peace"); + String join = CorsFilter.join(elements, separator); + Assert.assertTrue(",peace".equals(join)); + } + + @Test + public void testJoinAllNullElements() { + Set elements = new LinkedHashSet<>(); + String separator = ","; + elements.add(null); + elements.add(null); + String join = CorsFilter.join(elements, separator); + Assert.assertTrue("".equals(join)); + } + + @Test + public void testJoinAllEmptyElements() { + Set elements = new LinkedHashSet<>(); + String separator = ","; + elements.add(""); + elements.add(""); + String join = CorsFilter.join(elements, separator); + Assert.assertTrue("".equals(join)); + } + + @Test + public void testJoinPipeSeparator() { + Set elements = new LinkedHashSet<>(); + String separator = "|"; + elements.add("world"); + elements.add("peace"); + String join = CorsFilter.join(elements, separator); + Assert.assertTrue("world|peace".equals(join)); + } + + @Test + public void testWithFilterConfig() throws ServletException { + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + Assert.assertTrue(corsFilter.getAllowedHttpHeaders().size() == 6); + Assert.assertTrue(corsFilter.getAllowedHttpMethods().size() == 4); + Assert.assertTrue(corsFilter.getAllowedOrigins().size() == 0); + Assert.assertTrue(corsFilter.isAnyOriginAllowed()); + Assert.assertTrue(corsFilter.getExposedHeaders().size() == 0); + Assert.assertFalse(corsFilter.isSupportsCredentials()); + Assert.assertTrue(corsFilter.getPreflightMaxAge() == 1800); + } + + @Test(expected = ServletException.class) + public void testWithFilterConfigInvalidPreflightAge() throws ServletException { + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigInvalidMaxPreflightAge()); + } + + @Test + public void testWithStringParserEmpty() throws ServletException { + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getEmptyFilterConfig()); + Assert.assertTrue(corsFilter.getAllowedHttpHeaders().size() == 0); + Assert.assertTrue(corsFilter.getAllowedHttpMethods().size() == 0); + Assert.assertTrue(corsFilter.getAllowedOrigins().size() == 0); + Assert.assertTrue(corsFilter.getExposedHeaders().size() == 0); + Assert.assertFalse(corsFilter.isSupportsCredentials()); + Assert.assertTrue(corsFilter.getPreflightMaxAge() == 0); + } + + /* + * If an init param is null, it's default value will be used. + * + * @throws ServletException + */ + @Test + public void testWithStringParserNull() throws ServletException { + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getNullFilterConfig()); + Assert.assertTrue(corsFilter.getAllowedHttpHeaders().size() == 6); + Assert.assertTrue(corsFilter.getAllowedHttpMethods().size() == 4); + Assert.assertTrue(corsFilter.getAllowedOrigins().size() == 0); + Assert.assertFalse(corsFilter.isAnyOriginAllowed()); + Assert.assertTrue(corsFilter.getExposedHeaders().size() == 0); + Assert.assertFalse(corsFilter.isSupportsCredentials()); + Assert.assertTrue(corsFilter.getPreflightMaxAge() == 1800); + } + + @Test + public void testValidOrigin() { + Assert.assertTrue(RequestUtil.isValidOrigin("http://www.w3.org")); + } + + @Test + public void testInValidOriginCRLF() { + Assert.assertFalse(RequestUtil.isValidOrigin("http://www.w3.org\r\n")); + } + + @Test + public void testInValidOriginEncodedCRLF1() { + Assert.assertFalse(RequestUtil.isValidOrigin("http://www.w3.org%0d%0a")); + } + + @Test + public void testInValidOriginEncodedCRLF2() { + Assert.assertFalse(RequestUtil.isValidOrigin("http://www.w3.org%0D%0A")); + } + + @Test + public void testInValidOriginEncodedCRLF3() { + Assert.assertFalse(RequestUtil.isValidOrigin("http://www.w3.org%0%0d%0ad%0%0d%0aa")); + } + + @Test + public void testCheckInvalidCRLF1() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://www.w3.org\r\n"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.INVALID_CORS, requestType); + } + + @Test + public void testCheckInvalidCRLF2() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://www.w3.org\r\n"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.INVALID_CORS, requestType); + } + + @Test + public void testCheckInvalidCRLF3() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://www.w3.org%0d%0a"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.INVALID_CORS, requestType); + } + + @Test + public void testCheckInvalidCRLF4() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "http://www.w3.org%0D%0A"); + request.setMethod("GET"); + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.INVALID_CORS, requestType); + } + + @Test + public void testDecorateRequestDisabled() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getFilterConfigDecorateRequestDisabled()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader(CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals("*")); + Assert.assertNull(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)); + Assert.assertNull(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN)); + Assert.assertNull(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS)); + Assert.assertNull(request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE)); + } + + /* + * A CORS request arrives with a "null" origin which is allowed by default. + */ + @Test + public void testContentTypeWithParameter() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + + request.setMethod("POST"); + request.setContentType("text/plain;charset=UTF-8"); + request.setHeader(CorsFilter.REQUEST_HEADER_ORIGIN, "null"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CorsFilter corsFilter = new CorsFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CorsFilter.CORSRequestType requestType = corsFilter.checkRequestType(request); + Assert.assertEquals(CorsFilter.CORSRequestType.SIMPLE, requestType); + + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue( + ((Boolean) request.getAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)).booleanValue()); + } +} diff --git a/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java b/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java new file mode 100644 index 0000000..9b5634b --- /dev/null +++ b/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java @@ -0,0 +1,215 @@ +/* + * 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.catalina.filters; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.function.Predicate; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.filters.CsrfPreventionFilter.LruCache; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterServletContext; + +public class TestCsrfPreventionFilter extends TomcatBaseTest { + + private static final String RESULT_NONCE = Constants.CSRF_NONCE_SESSION_ATTR_NAME + "=TESTNONCE"; + + private final HttpServletResponse wrapper = new CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(), + Constants.CSRF_NONCE_SESSION_ATTR_NAME, "TESTNONCE", null); + + @Test + public void testAddNonceNoQueryNoAnchor() throws Exception { + Assert.assertEquals("/test?" + RESULT_NONCE, wrapper.encodeRedirectURL("/test")); + } + + @Test + public void testAddNonceQueryNoAnchor() throws Exception { + Assert.assertEquals("/test?a=b&" + RESULT_NONCE, wrapper.encodeRedirectURL("/test?a=b")); + } + + @Test + public void testAddNonceNoQueryAnchor() throws Exception { + Assert.assertEquals("/test?" + RESULT_NONCE + "#c", wrapper.encodeRedirectURL("/test#c")); + } + + @Test + public void testAddNonceQueryAnchor() throws Exception { + Assert.assertEquals("/test?a=b&" + RESULT_NONCE + "#c", wrapper.encodeRedirectURL("/test?a=b#c")); + } + + @Test + public void testLruCacheSerializable() throws Exception { + LruCache cache = new LruCache<>(5); + cache.add("key1"); + cache.add("key2"); + cache.add("key3"); + cache.add("key4"); + cache.add("key5"); + cache.add("key6"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(cache); + + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + @SuppressWarnings("unchecked") + LruCache cache2 = (LruCache) ois.readObject(); + + cache2.add("key7"); + Assert.assertFalse(cache2.contains("key1")); + Assert.assertFalse(cache2.contains("key2")); + Assert.assertTrue(cache2.contains("key3")); + Assert.assertTrue(cache2.contains("key4")); + Assert.assertTrue(cache2.contains("key5")); + Assert.assertTrue(cache2.contains("key6")); + Assert.assertTrue(cache2.contains("key7")); + } + + @Test + public void testLruCacheSerializablePerformance() throws Exception { + for (int i = 0; i < 10000; i++) { + testLruCacheSerializable(); + } + } + + + @Test + public void testNoNonceBuilders() { + Assert.assertEquals(CsrfPreventionFilter.PrefixPredicate.class, CsrfPreventionFilter.createNoNoncePredicate(null, "/images/*").getClass()); + Assert.assertEquals(CsrfPreventionFilter.SuffixPredicate.class, CsrfPreventionFilter.createNoNoncePredicate(null, "*.png").getClass()); + Assert.assertEquals(CsrfPreventionFilter.PatternPredicate.class, CsrfPreventionFilter.createNoNoncePredicate(null, "/^(/images/.*|.*\\.png)$/").getClass()); + + Collection> chain = CsrfPreventionFilter.createNoNoncePredicates(null, "*.png,/js/*,*.jpg,/images/*,mime:*/png,mime:image/*"); + + Assert.assertEquals(6, chain.size()); + Iterator> items = chain.iterator(); + + Assert.assertEquals(CsrfPreventionFilter.SuffixPredicate.class, items.next().getClass()); + Assert.assertEquals(CsrfPreventionFilter.PrefixPredicate.class, items.next().getClass()); + Assert.assertEquals(CsrfPreventionFilter.SuffixPredicate.class, items.next().getClass()); + Assert.assertEquals(CsrfPreventionFilter.PrefixPredicate.class, items.next().getClass()); + Predicate item = items.next(); + Assert.assertEquals(CsrfPreventionFilter.MimePredicate.class, item.getClass()); + Assert.assertEquals(CsrfPreventionFilter.SuffixPredicate.class, ((CsrfPreventionFilter.MimePredicate)item).getPredicate().getClass()); + + item = items.next(); + Assert.assertEquals(CsrfPreventionFilter.MimePredicate.class, item.getClass()); + Assert.assertEquals(CsrfPreventionFilter.PrefixPredicate.class, ((CsrfPreventionFilter.MimePredicate)item).getPredicate().getClass()); + } + + @Test + public void testNoNoncePatternMatchers() { + String[] urls = { "/images/home.png" }; + Predicate prefix = new CsrfPreventionFilter.PrefixPredicate("/images/"); + Predicate suffix = new CsrfPreventionFilter.SuffixPredicate(".png"); + Predicate regex = new CsrfPreventionFilter.PatternPredicate("^(/images/.*|.*\\.png)$"); + + for(String url : urls) { + Assert.assertTrue("Prefix match fails", prefix.test(url)); + Assert.assertTrue("Suffix match fails", suffix.test(url)); + Assert.assertTrue("Pattern match fails", regex.test(url)); + } + + ArrayList> chain = new ArrayList<>(); + chain.add(prefix); + chain.add(suffix); + chain.add(regex); + + HttpServletResponse response = new CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(), + Constants.CSRF_NONCE_SESSION_ATTR_NAME, "TESTNONCE", chain); + + // These URLs should include nonces + Assert.assertEquals("/foo?" + RESULT_NONCE, response.encodeURL("/foo")); + Assert.assertEquals("/foo/images?" + RESULT_NONCE, response.encodeURL("/foo/images")); + Assert.assertEquals("/foo/images/home.jpg?" + RESULT_NONCE, response.encodeURL("/foo/images/home.jpg")); + + // These URLs should not + Assert.assertEquals("/images/home.png", response.encodeURL("/images/home.png")); + Assert.assertEquals("/images/home.jpg", response.encodeURL("/images/home.jpg")); + Assert.assertEquals("/home.png", response.encodeURL("/home.png")); + Assert.assertEquals("/home.png", response.encodeURL("/home.png")); + } + + @Test + public void testNoNonceMimeMatcher() { + MimeTypeServletContext context = new MimeTypeServletContext(); + Predicate mime = new CsrfPreventionFilter.MimePredicate(context, new CsrfPreventionFilter.PrefixPredicate("image/")); + + context.setMimeType("image/png"); + Assert.assertTrue("MIME match fails", mime.test("/images/home.png")); + + context.setMimeType("text/plain"); + Assert.assertFalse("MIME match succeeds where it should fail", mime.test("/test.txt")); + + Collection> chain = Collections.singleton(mime); + HttpServletResponse response = new CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(), + Constants.CSRF_NONCE_SESSION_ATTR_NAME, "TESTNONCE", chain); + + // These URLs should include nonces + Assert.assertEquals("/foo?" + RESULT_NONCE, response.encodeURL("/foo")); + Assert.assertEquals("/foo/images?" + RESULT_NONCE, response.encodeURL("/foo/images")); + Assert.assertEquals("/foo/images/home.jpg?" + RESULT_NONCE, response.encodeURL("/foo/images/home.jpg")); + Assert.assertEquals("/images/home.png?" + RESULT_NONCE, response.encodeURL("/images/home.png")); + Assert.assertEquals("/images/home.jpg?" + RESULT_NONCE, response.encodeURL("/images/home.jpg")); + Assert.assertEquals("/home.png?" + RESULT_NONCE, response.encodeURL("/home.png")); + + context.setMimeType("image/png"); + // These URLs should not + Assert.assertEquals("/images/home.png", response.encodeURL("/images/home.png")); + Assert.assertEquals("/images/home.jpg", response.encodeURL("/images/home.jpg")); + Assert.assertEquals("/home.png", response.encodeURL("/home.png")); + Assert.assertEquals("/foo", response.encodeURL("/foo")); + Assert.assertEquals("/foo/home.png", response.encodeURL("/foo/home.png")); + Assert.assertEquals("/foo/images/home.jpg", response.encodeURL("/foo/images/home.jpg")); + } + + private static class MimeTypeServletContext extends TesterServletContext { + private String mimeType; + public void setMimeType(String type) { + mimeType = type; + } + + @Override + public String getMimeType(String url) { + return mimeType; + } + } + private static class NonEncodingResponse extends TesterHttpServletResponse { + + @Override + public String encodeRedirectURL(String url) { + return url; + } + + @Override + public String encodeURL(String url) { + return url; + } + } +} diff --git a/test/org/apache/catalina/filters/TestCsrfPreventionFilter2.java b/test/org/apache/catalina/filters/TestCsrfPreventionFilter2.java new file mode 100644 index 0000000..9560c44 --- /dev/null +++ b/test/org/apache/catalina/filters/TestCsrfPreventionFilter2.java @@ -0,0 +1,89 @@ +/* + * 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.catalina.filters; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.filters.CsrfPreventionFilter.LruCache; + +public class TestCsrfPreventionFilter2 { + + /* + * When this test fails, it tends to enter a long running loop but it will eventually finish (after ~70s on a 8-core + * Windows box). + */ + @Test + public void testLruCacheConcurrency() throws Exception { + int threadCount = 2; + long iterationCount = 100000L; + + Assert.assertTrue(threadCount > 1); + + LruCache cache = new LruCache<>(threadCount - 1); + + LruTestThread[] threads = new LruTestThread[threadCount]; + for (int i = 0; i < threadCount; i++) { + threads[i] = new LruTestThread(cache, iterationCount); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + + for (int i = 0; i < threadCount; i++) { + Assert.assertTrue(threads[i].getResult()); + } + + } + + private static class LruTestThread extends Thread { + private final LruCache cache; + private long iterationCount = 0; + private volatile boolean result = false; + + LruTestThread(LruCache cache, long iterationCount) { + this.cache = cache; + this.iterationCount = iterationCount; + } + + public boolean getResult() { + return result; + } + + @Override + public void run() { + String test = getName(); + try { + for (long i = 0; i < iterationCount; i++) { + cache.add(test + i); + if (!cache.contains(test + i)) { + // Expected + } + } + } catch (Exception e) { + e.printStackTrace(); + return; + } + result = true; + } + } +} diff --git a/test/org/apache/catalina/filters/TestExpiresFilter.java b/test/org/apache/catalina/filters/TestExpiresFilter.java new file mode 100644 index 0000000..38acb2e --- /dev/null +++ b/test/org/apache/catalina/filters/TestExpiresFilter.java @@ -0,0 +1,524 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.StringTokenizer; +import java.util.TimeZone; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.filters.ExpiresFilter.Duration; +import org.apache.catalina.filters.ExpiresFilter.DurationUnit; +import org.apache.catalina.filters.ExpiresFilter.ExpiresConfiguration; +import org.apache.catalina.filters.ExpiresFilter.StartingPoint; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.http.FastHttpDateFormat; + +public class TestExpiresFilter extends TomcatBaseTest { + public static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + + @Test + public void testConfiguration() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("ExpiresDefault", "access plus 1 month"); + filterDef.addInitParameter("ExpiresByType text/html", "access plus 1 month 15 days 2 hours"); + filterDef.addInitParameter("ExpiresByType image/gif", "modification plus 5 hours 3 minutes"); + filterDef.addInitParameter("ExpiresByType image/jpg", "A10000"); + filterDef.addInitParameter("ExpiresByType video/mpeg", "M20000"); + filterDef.addInitParameter("ExpiresExcludedResponseStatusCodes", "304, 503"); + + ExpiresFilter expiresFilter = new ExpiresFilter(); + + filterDef.setFilter(expiresFilter); + filterDef.setFilterClass(ExpiresFilter.class.getName()); + filterDef.setFilterName(ExpiresFilter.class.getName()); + + root.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(ExpiresFilter.class.getName()); + filterMap.addURLPatternDecoded("*"); + + tomcat.start(); + try { + // VERIFY EXCLUDED RESPONSE STATUS CODES + int[] excludedResponseStatusCodes = expiresFilter.getExcludedResponseStatusCodesAsInts(); + Assert.assertEquals(2, excludedResponseStatusCodes.length); + Assert.assertEquals(304, excludedResponseStatusCodes[0]); + Assert.assertEquals(503, excludedResponseStatusCodes[1]); + + // VERIFY DEFAULT CONFIGURATION + ExpiresConfiguration expiresConfigurationDefault = expiresFilter.getDefaultExpiresConfiguration(); + Assert.assertEquals(StartingPoint.ACCESS_TIME, expiresConfigurationDefault.getStartingPoint()); + Assert.assertEquals(1, expiresConfigurationDefault.getDurations().size()); + Assert.assertEquals(DurationUnit.MONTH, expiresConfigurationDefault.getDurations().get(0).getUnit()); + Assert.assertEquals(1, expiresConfigurationDefault.getDurations().get(0).getAmount()); + + // VERIFY TEXT/HTML + ExpiresConfiguration expiresConfigurationTextHtml = expiresFilter.getExpiresConfigurationByContentType() + .get("text/html"); + Assert.assertEquals(StartingPoint.ACCESS_TIME, expiresConfigurationTextHtml.getStartingPoint()); + + Assert.assertEquals(3, expiresConfigurationTextHtml.getDurations().size()); + + Duration oneMonth = expiresConfigurationTextHtml.getDurations().get(0); + Assert.assertEquals(DurationUnit.MONTH, oneMonth.getUnit()); + Assert.assertEquals(1, oneMonth.getAmount()); + + Duration fifteenDays = expiresConfigurationTextHtml.getDurations().get(1); + Assert.assertEquals(DurationUnit.DAY, fifteenDays.getUnit()); + Assert.assertEquals(15, fifteenDays.getAmount()); + + Duration twoHours = expiresConfigurationTextHtml.getDurations().get(2); + Assert.assertEquals(DurationUnit.HOUR, twoHours.getUnit()); + Assert.assertEquals(2, twoHours.getAmount()); + + // VERIFY IMAGE/GIF + ExpiresConfiguration expiresConfigurationImageGif = expiresFilter.getExpiresConfigurationByContentType() + .get("image/gif"); + Assert.assertEquals(StartingPoint.LAST_MODIFICATION_TIME, expiresConfigurationImageGif.getStartingPoint()); + + Assert.assertEquals(2, expiresConfigurationImageGif.getDurations().size()); + + Duration fiveHours = expiresConfigurationImageGif.getDurations().get(0); + Assert.assertEquals(DurationUnit.HOUR, fiveHours.getUnit()); + Assert.assertEquals(5, fiveHours.getAmount()); + + Duration threeMinutes = expiresConfigurationImageGif.getDurations().get(1); + Assert.assertEquals(DurationUnit.MINUTE, threeMinutes.getUnit()); + Assert.assertEquals(3, threeMinutes.getAmount()); + + // VERIFY IMAGE/JPG + ExpiresConfiguration expiresConfigurationImageJpg = expiresFilter.getExpiresConfigurationByContentType() + .get("image/jpg"); + Assert.assertEquals(StartingPoint.ACCESS_TIME, expiresConfigurationImageJpg.getStartingPoint()); + + Assert.assertEquals(1, expiresConfigurationImageJpg.getDurations().size()); + + Duration tenThousandSeconds = expiresConfigurationImageJpg.getDurations().get(0); + Assert.assertEquals(DurationUnit.SECOND, tenThousandSeconds.getUnit()); + Assert.assertEquals(10000, tenThousandSeconds.getAmount()); + + // VERIFY VIDEO/MPEG + ExpiresConfiguration expiresConfiguration = expiresFilter.getExpiresConfigurationByContentType() + .get("video/mpeg"); + Assert.assertEquals(StartingPoint.LAST_MODIFICATION_TIME, expiresConfiguration.getStartingPoint()); + + Assert.assertEquals(1, expiresConfiguration.getDurations().size()); + + Duration twentyThousandSeconds = expiresConfiguration.getDurations().get(0); + Assert.assertEquals(DurationUnit.SECOND, twentyThousandSeconds.getUnit()); + Assert.assertEquals(20000, twentyThousandSeconds.getAmount()); + } finally { + tomcat.stop(); + } + } + + /* + * Test that a resource with empty content is also processed + */ + @Test + public void testEmptyContent() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("text/plain"); + // no content is written in the response + } + }; + + validate(servlet, Integer.valueOf(7 * 60)); + } + + @Test + public void testParseExpiresConfigurationCombinedDuration() { + ExpiresFilter expiresFilter = new ExpiresFilter(); + ExpiresConfiguration actualConfiguration = expiresFilter + .parseExpiresConfiguration("access plus 1 month 15 days 2 hours"); + + Assert.assertEquals(StartingPoint.ACCESS_TIME, actualConfiguration.getStartingPoint()); + + Assert.assertEquals(3, actualConfiguration.getDurations().size()); + + } + + @Test + public void testParseExpiresConfigurationMonoDuration() { + ExpiresFilter expiresFilter = new ExpiresFilter(); + ExpiresConfiguration actualConfiguration = expiresFilter.parseExpiresConfiguration("access plus 2 hours"); + + Assert.assertEquals(StartingPoint.ACCESS_TIME, actualConfiguration.getStartingPoint()); + + Assert.assertEquals(1, actualConfiguration.getDurations().size()); + Assert.assertEquals(2, actualConfiguration.getDurations().get(0).getAmount()); + Assert.assertEquals(DurationUnit.HOUR, actualConfiguration.getDurations().get(0).getUnit()); + + } + + @Test + public void testSkipBecauseCacheControlMaxAgeIsDefined() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("text/xml; charset=utf-8"); + response.addHeader("Cache-Control", "private, max-age=232"); + response.getWriter().print("Hello world"); + } + }; + + validate(servlet, Integer.valueOf(232)); + } + + @Test + public void testExcludedResponseStatusCode() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + response.addHeader("ETag", "W/\"1934-1269208821000\""); + response.addDateHeader("Date", System.currentTimeMillis()); + } + }; + + validate(servlet, null, HttpServletResponse.SC_NOT_MODIFIED); + } + + @Test + public void testNullContentType() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType(null); + } + }; + + validate(servlet, Integer.valueOf(1 * 60)); + } + + @Test + public void testSkipBecauseExpiresIsDefined() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("text/xml; charset=utf-8"); + response.addDateHeader("Expires", System.currentTimeMillis()); + response.getWriter().print("Hello world"); + } + }; + + validate(servlet, null); + } + + @Test + public void testUseContentTypeExpiresConfiguration() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("text/xml; charset=utf-8"); + response.getWriter().print("Hello world"); + } + }; + + validate(servlet, Integer.valueOf(3 * 60)); + } + + @Test + public void testUseContentTypeWithoutCharsetExpiresConfiguration() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("text/xml; charset=iso-8859-1"); + response.getWriter().print("Hello world"); + } + }; + + validate(servlet, Integer.valueOf(5 * 60)); + } + + @Test + public void testUseDefaultConfiguration1() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("image/jpeg"); + response.getWriter().print("Hello world"); + } + }; + + validate(servlet, Integer.valueOf(1 * 60)); + } + + @Test + public void testUseDefaultConfiguration2() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("image/jpeg"); + response.addHeader("Cache-Control", "private"); + + response.getWriter().print("Hello world"); + } + }; + + validate(servlet, Integer.valueOf(1 * 60)); + } + + @Test + public void testUseMajorTypeExpiresConfiguration() throws Exception { + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("text/json; charset=iso-8859-1"); + response.getWriter().print("Hello world"); + } + }; + + validate(servlet, Integer.valueOf(7 * 60)); + } + + protected void validate(HttpServlet servlet, Integer expectedMaxAgeInSeconds) throws Exception { + validate(servlet, expectedMaxAgeInSeconds, HttpServletResponse.SC_OK); + } + + protected void validate(HttpServlet servlet, Integer expectedMaxAgeInSeconds, int expectedResponseStatusCode) + throws Exception { + + // SETUP + + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("ExpiresDefault", "access plus 1 minute"); + filterDef.addInitParameter("ExpiresByType text/xml;charset=utf-8", "access plus 3 minutes"); + filterDef.addInitParameter("ExpiresByType text/xml", "access plus 5 minutes"); + filterDef.addInitParameter("ExpiresByType text", "access plus 7 minutes"); + filterDef.addInitParameter("ExpiresExcludedResponseStatusCodes", "304, 503"); + + filterDef.setFilterClass(ExpiresFilter.class.getName()); + filterDef.setFilterName(ExpiresFilter.class.getName()); + + root.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(ExpiresFilter.class.getName()); + filterMap.addURLPatternDecoded("*"); + root.addFilterMap(filterMap); + + Tomcat.addServlet(root, servlet.getClass().getName(), servlet); + root.addServletMappingDecoded("/test", servlet.getClass().getName()); + + tomcat.start(); + + try { + Calendar.getInstance(TimeZone.getTimeZone("GMT")); + long timeBeforeInMillis = System.currentTimeMillis(); + + // TEST + ByteChunk bc = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + int rc = getUrl("http://localhost:" + getPort() + "/test", bc, responseHeaders); + + // VALIDATE + Assert.assertEquals(expectedResponseStatusCode, rc); + + StringBuilder msg = new StringBuilder(); + for (Entry> field : responseHeaders.entrySet()) { + for (String value : field.getValue()) { + msg.append((field.getKey() == null ? "" : field.getKey() + ": ") + value + "\n"); + } + } + System.out.println(msg); + + Integer actualMaxAgeInSeconds; + + String cacheControlHeader = getSingleHeader("Cache-Control", responseHeaders); + + if (cacheControlHeader == null) { + actualMaxAgeInSeconds = null; + } else { + actualMaxAgeInSeconds = null; + StringTokenizer cacheControlTokenizer = new StringTokenizer(cacheControlHeader, ","); + while (cacheControlTokenizer.hasMoreTokens() && actualMaxAgeInSeconds == null) { + String cacheDirective = cacheControlTokenizer.nextToken(); + StringTokenizer cacheDirectiveTokenizer = new StringTokenizer(cacheDirective, "="); + if (cacheDirectiveTokenizer.countTokens() == 2) { + String key = cacheDirectiveTokenizer.nextToken().trim(); + String value = cacheDirectiveTokenizer.nextToken().trim(); + if (key.equalsIgnoreCase("max-age")) { + actualMaxAgeInSeconds = Integer.valueOf(value); + } + } + } + } + + if (expectedMaxAgeInSeconds == null) { + Assert.assertNull("actualMaxAgeInSeconds '" + actualMaxAgeInSeconds + "' should be null", + actualMaxAgeInSeconds); + return; + } + + Assert.assertNotNull(actualMaxAgeInSeconds); + + String contentType = getSingleHeader("Content-Type", responseHeaders); + + int deltaInSeconds = Math.abs(actualMaxAgeInSeconds.intValue() - expectedMaxAgeInSeconds.intValue()); + Assert.assertTrue("actualMaxAgeInSeconds: " + actualMaxAgeInSeconds + ", expectedMaxAgeInSeconds: " + + expectedMaxAgeInSeconds + ", request time: " + timeBeforeInMillis + " for content type " + + contentType, deltaInSeconds < 3); + + } finally { + tomcat.stop(); + } + } + + @Test + public void testIntsToCommaDelimitedString() { + String actual = ExpiresFilter.intsToCommaDelimitedString(new int[] { 500, 503 }); + String expected = "500, 503"; + + Assert.assertEquals(expected, actual); + } + + + /* + * Tests Expires filter with: - per content type expires - no default - Default servlet returning 304s (without + * content-type) + */ + @Test + public void testBug63909() throws Exception { + + Tomcat tomcat = getTomcatInstanceTestWebapp(false, false); + Context ctxt = (Context) tomcat.getHost().findChild("/test"); + + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("ExpiresByType text/xml;charset=utf-8", "access plus 3 minutes"); + filterDef.addInitParameter("ExpiresByType text/xml", "access plus 5 minutes"); + filterDef.addInitParameter("ExpiresByType text", "access plus 7 minutes"); + filterDef.addInitParameter("ExpiresExcludedResponseStatusCodes", ""); + + filterDef.setFilterClass(ExpiresFilter.class.getName()); + filterDef.setFilterName(ExpiresFilter.class.getName()); + + ctxt.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(ExpiresFilter.class.getName()); + filterMap.addURLPatternDecoded("*"); + ctxt.addFilterMap(filterMap); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + Map> requestHeaders = new CaseInsensitiveKeyMap<>(); + List ifModifiedSinceValues = new ArrayList<>(); + ifModifiedSinceValues.add(FastHttpDateFormat.getCurrentDate()); + requestHeaders.put("If-Modified-Since", ifModifiedSinceValues); + Map> responseHeaders = new CaseInsensitiveKeyMap<>(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug69303.txt", bc, requestHeaders, + responseHeaders); + + Assert.assertEquals(HttpServletResponse.SC_NOT_MODIFIED, rc); + + StringBuilder msg = new StringBuilder(); + for (Entry> field : responseHeaders.entrySet()) { + for (String value : field.getValue()) { + msg.append((field.getKey() == null ? "" : field.getKey() + ": ") + value + "\n"); + } + } + System.out.println(msg); + + Integer actualMaxAgeInSeconds; + + String cacheControlHeader = getSingleHeader("Cache-Control", responseHeaders); + + if (cacheControlHeader == null) { + actualMaxAgeInSeconds = null; + } else { + actualMaxAgeInSeconds = null; + StringTokenizer cacheControlTokenizer = new StringTokenizer(cacheControlHeader, ","); + while (cacheControlTokenizer.hasMoreTokens() && actualMaxAgeInSeconds == null) { + String cacheDirective = cacheControlTokenizer.nextToken(); + StringTokenizer cacheDirectiveTokenizer = new StringTokenizer(cacheDirective, "="); + if (cacheDirectiveTokenizer.countTokens() == 2) { + String key = cacheDirectiveTokenizer.nextToken().trim(); + String value = cacheDirectiveTokenizer.nextToken().trim(); + if (key.equalsIgnoreCase("max-age")) { + actualMaxAgeInSeconds = Integer.valueOf(value); + } + } + } + } + + Assert.assertNotNull(actualMaxAgeInSeconds); + Assert.assertTrue(Math.abs(actualMaxAgeInSeconds.intValue() - 420) < 3); + } +} diff --git a/test/org/apache/catalina/filters/TestRateLimitFilter.java b/test/org/apache/catalina/filters/TestRateLimitFilter.java new file mode 100644 index 0000000..0d91828 --- /dev/null +++ b/test/org/apache/catalina/filters/TestRateLimitFilter.java @@ -0,0 +1,168 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.time.Instant; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.filters.TestRemoteIpFilter.MockFilterChain; +import org.apache.catalina.filters.TestRemoteIpFilter.MockHttpServletRequest; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterResponse; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; + +public class TestRateLimitFilter extends TomcatBaseTest { + + @Test + public void testRateLimitWith4Clients() throws Exception { + + int bucketRequests = 40; + int bucketDuration = 4; + + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter(RateLimitFilter.PARAM_BUCKET_REQUESTS, String.valueOf(bucketRequests)); + filterDef.addInitParameter(RateLimitFilter.PARAM_BUCKET_DURATION, String.valueOf(bucketDuration)); + + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + tomcat.start(); + + MockFilterChain filterChain = new MockFilterChain(); + RateLimitFilter rateLimitFilter = testRateLimitFilter(filterDef, root); + + int allowedRequests = (int) Math.round(rateLimitFilter.bucketCounter.getRatio() * bucketRequests); + + long sleepTime = rateLimitFilter.bucketCounter.getMillisUntilNextBucket(); + System.out.printf("Sleeping %d millis for the next time bucket to start\n", Long.valueOf(sleepTime)); + Thread.sleep(sleepTime); + + TestClient tc1 = new TestClient(rateLimitFilter, filterChain, "10.20.20.5", 200, 5); + TestClient tc2 = new TestClient(rateLimitFilter, filterChain, "10.20.20.10", 200, 10); + + TestClient tc3 = new TestClient(rateLimitFilter, filterChain, "10.20.20.20", 200, 20); + TestClient tc4 = new TestClient(rateLimitFilter, filterChain, "10.20.20.40", 200, 40); + + // Sleep for up to 10s for clients to complete + int count = 0; + while (count < 100 && (tc1.results[24] == 0 || tc2.results[49] == 0 || tc3.results[allowedRequests - 1] == 0 || + tc3.results[allowedRequests] == 0 || tc4.results[allowedRequests - 1] == 0 || + tc4.results[allowedRequests] == 0)) { + Thread.sleep(100); + count++; + } + + Assert.assertEquals(200, tc1.results[24]); // only 25 requests made, all allowed + + Assert.assertEquals(200, tc2.results[49]); // only 25 requests made, all allowed + + Assert.assertEquals(200, tc3.results[allowedRequests - 1]); // first allowedRequests allowed + Assert.assertEquals(429, tc3.results[allowedRequests]); // subsequent requests dropped + + Assert.assertEquals(200, tc4.results[allowedRequests - 1]); // first allowedRequests allowed + Assert.assertEquals(429, tc4.results[allowedRequests]); // subsequent requests dropped + } + + private RateLimitFilter testRateLimitFilter(FilterDef filterDef, Context root) throws ServletException { + + RateLimitFilter rateLimitFilter = new RateLimitFilter(); + filterDef.setFilterClass(RateLimitFilter.class.getName()); + filterDef.setFilter(rateLimitFilter); + filterDef.setFilterName(RateLimitFilter.class.getName()); + root.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(RateLimitFilter.class.getName()); + filterMap.addURLPatternDecoded("*"); + root.addFilterMap(filterMap); + + FilterConfig filterConfig = TesterFilterConfigs.generateFilterConfig(filterDef); + + rateLimitFilter.init(filterConfig); + + return rateLimitFilter; + } + + static class TestClient extends Thread { + RateLimitFilter filter; + FilterChain filterChain; + String ip; + + int requests; + int sleep; + + int[] results; + + TestClient(RateLimitFilter filter, FilterChain filterChain, String ip, int requests, int rps) { + this.filter = filter; + this.filterChain = filterChain; + this.ip = ip; + this.requests = requests; + this.sleep = 1000 / rps; + this.results = new int[requests]; + super.setDaemon(true); + super.start(); + } + + @Override + public void run() { + try { + for (int i = 0; i < requests; i++) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr(ip); + TesterResponse response = new TesterResponseWithStatus(); + response.setRequest(request); + filter.doFilter(request, response, filterChain); + results[i] = response.getStatus(); + System.out.printf("%s %s: %s %d\n", ip, Instant.now(), Integer.valueOf(i + 1), + Integer.valueOf(response.getStatus())); + sleep(sleep); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + static class TesterResponseWithStatus extends TesterResponse { + + int status = 200; + String message = "OK"; + + @Override + public void sendError(int status, String message) throws IOException { + this.status = status; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + } + +} diff --git a/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java b/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java new file mode 100644 index 0000000..9047517 --- /dev/null +++ b/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java @@ -0,0 +1,150 @@ +/* + * 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.catalina.filters; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Request; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterResponse; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; + +public class TestRemoteCIDRFilter extends TomcatBaseTest { + + @Test + public void testAllowOnly() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + tomcat.start(); + + TestRemoteIpFilter.MockFilterChain filterChain = new TestRemoteIpFilter.MockFilterChain(); + + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("allow", "192.168.10.0/24, 192.168.20.0/24"); + Filter filter = createTestFilter(filterDef, RemoteCIDRFilter.class, root, "*"); + + String ipAddr; + Request request; + TesterResponse response; + int expected; + + for (int i = 0; i < 256; i++) { + for (int j = 0; j < 256; j += 11) { + ipAddr = String.format("192.168.%s.%s", Integer.valueOf(i), Integer.valueOf(j)); + request = new TestRemoteIpFilter.MockHttpServletRequest(ipAddr); + response = new TestRateLimitFilter.TesterResponseWithStatus(); + expected = (i == 10 || i == 20) ? HttpServletResponse.SC_OK : HttpServletResponse.SC_FORBIDDEN; + filter.doFilter(request, response, filterChain); + Assert.assertEquals(expected, response.getStatus()); + } + } + } + + @Test + public void testDenyOnly() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + tomcat.start(); + + TestRemoteIpFilter.MockFilterChain filterChain = new TestRemoteIpFilter.MockFilterChain(); + + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("deny", "192.168.10.0/24, 192.168.20.0/24"); + Filter filter = createTestFilter(filterDef, RemoteCIDRFilter.class, root, "*"); + + String ipAddr; + Request request; + TesterResponse response; + int expected; + + for (int i = 0; i < 256; i++) { + for (int j = 0; j < 256; j += 11) { + ipAddr = String.format("192.168.%s.%s", Integer.valueOf(i), Integer.valueOf(j)); + request = new TestRemoteIpFilter.MockHttpServletRequest(ipAddr); + response = new TestRateLimitFilter.TesterResponseWithStatus(); + expected = (i != 10 && i != 20) ? HttpServletResponse.SC_OK : HttpServletResponse.SC_FORBIDDEN; + filter.doFilter(request, response, filterChain); + Assert.assertEquals(expected, response.getStatus()); + } + } + } + + @Test + public void testAllowDeny() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + tomcat.start(); + + TestRemoteIpFilter.MockFilterChain filterChain = new TestRemoteIpFilter.MockFilterChain(); + + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("allow", "10.10.0.0/16"); + filterDef.addInitParameter("deny", "10.10.10.0/24, 10.10.20.0/24"); + Filter filter = createTestFilter(filterDef, RemoteCIDRFilter.class, root, "*"); + + String ipAddr; + Request request; + TesterResponse response; + int expected; + + for (int i = 0; i < 256; i++) { + for (int j = 0; j < 256; j += 11) { + ipAddr = String.format("10.10.%s.%s", Integer.valueOf(i), Integer.valueOf(j)); + request = new TestRemoteIpFilter.MockHttpServletRequest(ipAddr); + response = new TestRateLimitFilter.TesterResponseWithStatus(); + expected = (i != 10 && i != 20) ? HttpServletResponse.SC_OK : HttpServletResponse.SC_FORBIDDEN; + filter.doFilter(request, response, filterChain); + Assert.assertEquals(expected, response.getStatus()); + } + } + } + + private Filter createTestFilter(FilterDef filterDef, Class testFilterClass, Context root, String urlPattern) + throws ServletException { + + RemoteCIDRFilter remoteCIDRFilter = new RemoteCIDRFilter(); + + filterDef.setFilterClass(testFilterClass.getName()); + filterDef.setFilterName(testFilterClass.getName()); + filterDef.setFilter(remoteCIDRFilter); + root.addFilterDef(filterDef); + + root.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(testFilterClass.getName()); + filterMap.addURLPatternDecoded(urlPattern); + root.addFilterMap(filterMap); + + FilterConfig filterConfig = TesterFilterConfigs.generateFilterConfig(filterDef); + + remoteCIDRFilter.init(filterConfig); + + return remoteCIDRFilter; + } + +} diff --git a/test/org/apache/catalina/filters/TestRemoteIpFilter.java b/test/org/apache/catalina/filters/TestRemoteIpFilter.java new file mode 100644 index 0000000..daa252b --- /dev/null +++ b/test/org/apache/catalina/filters/TestRemoteIpFilter.java @@ -0,0 +1,871 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Context; +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.unittest.TesterResponse; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; + +public class TestRemoteIpFilter extends TomcatBaseTest { + + /** + * Mock {@link FilterChain} to keep a handle on the passed {@link ServletRequest} and (@link ServletResponse}. + */ + public static class MockFilterChain implements FilterChain { + private HttpServletRequest request; + private HttpServletResponse response; + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + this.request = (HttpServletRequest) request; + this.response = (HttpServletResponse) response; + } + + public HttpServletRequest getRequest() { + return request; + } + + public HttpServletResponse getResponse() { + return response; + } + } + + public static class MockHttpServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public String remoteAddr; + public String remoteHost; + public String scheme; + public String serverName; + public int serverPort; + public boolean isSecure; + + @Override + public void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + this.isSecure = request.isSecure(); + this.remoteAddr = request.getRemoteAddr(); + this.remoteHost = request.getRemoteHost(); + this.scheme = request.getScheme(); + this.serverName = request.getServerName(); + this.serverPort = request.getServerPort(); + PrintWriter writer = response.getWriter(); + + writer.println("request.remoteAddr=" + request.getRemoteAddr()); + writer.println("request.remoteHost=" + request.getRemoteHost()); + writer.println("request.secure=" + request.isSecure()); + writer.println("request.scheme=" + request.getScheme()); + writer.println("request.serverName=" + request.getServerName()); + writer.println("request.serverPort=" + request.getServerPort()); + + writer.println(); + for (Enumeration headers = request.getHeaderNames(); headers.hasMoreElements();) { + String name = headers.nextElement().toString(); + writer.println("request.header['" + name + "']=" + Collections.list(request.getHeaders(name))); + } + } + } + + /** + * Enhanced {@link Request} to ease testing. + */ + public static class MockHttpServletRequest extends Request { + public MockHttpServletRequest() { + super(new Connector()); + setCoyoteRequest(new org.apache.coyote.Request()); + } + + public MockHttpServletRequest(String ipAddress) { + this(); + this.setRemoteAddr(ipAddress); + } + + public void setHeader(String name, String value) { + getCoyoteRequest().getMimeHeaders().setValue(name).setString(value); + } + + public void addHeader(String name, String value) { + getCoyoteRequest().getMimeHeaders().addValue(name).setString(value); + } + + public void setScheme(String scheme) { + getCoyoteRequest().scheme().setString(scheme); + } + + @Override + public String getServerName() { + return "localhost"; + } + + @Override + public Context getContext() { + // Lazt init + if (super.getContext() == null) { + getMappingData().context = new TesterContext(); + } + return super.getContext(); + } + } + + public static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + + @Test + public void testCommaDelimitedListToStringArray() { + List elements = Arrays.asList("element1", "element2", "element3"); + String actual = StringUtils.join(elements); + Assert.assertEquals("element1,element2,element3", actual); + } + + @Test + public void testCommaDelimitedListToStringArrayEmptyList() { + List elements = new ArrayList<>(); + String actual = StringUtils.join(elements); + Assert.assertEquals("", actual); + } + + @Test + public void testCommaDelimitedListToStringArrayNullList() { + String actual = StringUtils.join((String[]) null); + Assert.assertEquals("", actual); + } + + @Test + public void testHeaderNamesCaseInsensitivity() { + RemoteIpFilter.XForwardedRequest request = new RemoteIpFilter.XForwardedRequest(new MockHttpServletRequest()); + request.setHeader("myheader", "lower Case"); + request.setHeader("MYHEADER", "UPPER CASE"); + request.setHeader("MyHeader", "Camel Case"); + Assert.assertEquals(1, request.headers.size()); + Assert.assertEquals("Camel Case", request.getHeader("myheader")); + } + + @Test + public void testIncomingRequestIsSecuredButProtocolHeaderSaysItIsNotWithCustomValues() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("protocolHeader", "x-forwarded-proto"); + filterDef.addInitParameter("remoteIpHeader", "x-my-forwarded-for"); + filterDef.addInitParameter("httpServerPort", "8080"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.0.10"); + request.setSecure(true); + request.setScheme("https"); + request.setHeader("x-my-forwarded-for", "140.211.11.130"); + request.setHeader("x-forwarded-proto", "http"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + boolean actualSecure = actualRequest.isSecure(); + Assert.assertFalse("request must be unsecured as header x-forwarded-proto said it is http", actualSecure); + + String actualScheme = actualRequest.getScheme(); + Assert.assertEquals("scheme must be http as header x-forwarded-proto said it is http", "http", actualScheme); + + int actualServerPort = actualRequest.getServerPort(); + Assert.assertEquals("wrong http server port", 8080, actualServerPort); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + } + + @Test + public void testIncomingRequestIsSecuredButProtocolHeaderSaysItIsNotWithDefaultValues() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("protocolHeader", "x-forwarded-proto"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.0.10"); + request.setSecure(true); + request.setScheme("https"); + request.setHeader("x-forwarded-for", "140.211.11.130"); + request.setHeader("x-forwarded-proto", "http"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + boolean actualSecure = actualRequest.isSecure(); + Assert.assertFalse("request must be unsecured as header x-forwarded-proto said it is http", actualSecure); + + String actualScheme = actualRequest.getScheme(); + Assert.assertEquals("scheme must be http as header x-forwarded-proto said it is http", "http", actualScheme); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + } + + @Test + public void testInvokeAllowedRemoteAddrWithNullRemoteIpHeader() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", "192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = request.getHeader("x-forwarded-for"); + Assert.assertNull("x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = request.getHeader("x-forwarded-by"); + Assert.assertNull("x-forwarded-by must be null", actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "192.168.0.10", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "remote-host-original-value", actualRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreInternal() throws Exception { + + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", "192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.addHeader("x-forwarded-for", "140.211.11.130, 192.168.0.10, 192.168.0.11"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = actualRequest.getHeader("x-forwarded-for"); + Assert.assertNull("all proxies are internal, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertNull("all proxies are internal, x-forwarded-by must be null", actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrusted() throws Exception { + + // PREPARE + RemoteIpFilter remoteIpFilter = new RemoteIpFilter(); + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", "192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + filterDef.setFilter(remoteIpFilter); + MockHttpServletRequest request = new MockHttpServletRequest(); + + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.setHeader("x-forwarded-for", "140.211.11.130, proxy1, proxy2"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = actualRequest.getHeader("x-forwarded-for"); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2", + actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrustedEmptyInternal() throws Exception { + + // PREPARE + RemoteIpFilter remoteIpFilter = new RemoteIpFilter(); + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", ""); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + filterDef.setFilter(remoteIpFilter); + MockHttpServletRequest request = new MockHttpServletRequest(); + + request.setRemoteAddr("proxy3"); + request.setRemoteHost("remote-host-original-value"); + request.setHeader("x-forwarded-for", "140.211.11.130, proxy1, proxy2"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = actualRequest.getHeader("x-forwarded-for"); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2,proxy3", + actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrustedUnusedInternal() throws Exception { + + // PREPARE + RemoteIpFilter remoteIpFilter = new RemoteIpFilter(); + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + filterDef.setFilter(remoteIpFilter); + MockHttpServletRequest request = new MockHttpServletRequest(); + + request.setRemoteAddr("proxy3"); + request.setRemoteHost("remote-host-original-value"); + request.setHeader("x-forwarded-for", "140.211.11.130, proxy1, proxy2"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = actualRequest.getHeader("x-forwarded-for"); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2,proxy3", + actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrustedAndRemoteAddrMatchRegexp() throws Exception { + + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", "127\\.0\\.0\\.1|192\\.168\\..*|another-internal-proxy"); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.addHeader("x-forwarded-for", "140.211.11.130"); + request.addHeader("x-forwarded-for", "proxy1"); + request.addHeader("x-forwarded-for", "proxy2"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = actualRequest.getHeader("x-forwarded-for"); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2", + actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrustedOrInternal() throws Exception { + + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", "192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.setHeader("x-forwarded-for", "140.211.11.130, proxy1, proxy2, 192.168.0.10, 192.168.0.11"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = actualRequest.getHeader("x-forwarded-for"); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2", + actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + } + + @Test + public void testInvokeNotAllowedRemoteAddr() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", "192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + + request.setRemoteAddr("not-allowed-internal-proxy"); + request.setRemoteHost("not-allowed-internal-proxy-host"); + request.setHeader("x-forwarded-for", "140.211.11.130, proxy1, proxy2"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = actualRequest.getHeader("x-forwarded-for"); + Assert.assertEquals("x-forwarded-for must be unchanged", "140.211.11.130, proxy1, proxy2", actualXForwardedFor); + + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertNull("x-forwarded-by must be null", actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "not-allowed-internal-proxy", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "not-allowed-internal-proxy-host", actualRemoteHost); + } + + @Test + public void testInvokeUntrustedProxyInTheChain() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", "192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + filterDef.addInitParameter("trustedProxies", "proxy1|proxy2|proxy3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.setHeader("x-forwarded-for", "140.211.11.130, proxy1, untrusted-proxy, proxy2"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedFor = actualRequest.getHeader("x-forwarded-for"); + Assert.assertEquals("ip/host before untrusted-proxy must appear in x-forwarded-for", "140.211.11.130,proxy1", + actualXForwardedFor); + + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertEquals("ip/host after untrusted-proxy must appear in x-forwarded-by", "proxy2", + actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "untrusted-proxy", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "untrusted-proxy", actualRemoteHost); + } + + @Test + public void testInvokeXforwardedHost() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("hostHeader", "x-forwarded-host"); + filterDef.addInitParameter("portHeader", "x-forwarded-port"); + filterDef.addInitParameter("protocolHeader", "x-forwarded-proto"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + // protocol + request.setSecure(false); + request.setServerPort(8080); + request.setScheme("http"); + // host and port + request.getCoyoteRequest().serverName().setString("10.0.0.1"); + request.setHeader("x-forwarded-host", "example.com"); + request.setHeader("x-forwarded-port", "8443"); + request.setHeader("x-forwarded-proto", "https"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + // protocol + String actualServerName = actualRequest.getServerName(); + Assert.assertEquals("postInvoke serverName", "example.com", actualServerName); + + String actualScheme = actualRequest.getScheme(); + Assert.assertEquals("postInvoke scheme", "https", actualScheme); + + int actualServerPort = actualRequest.getServerPort(); + Assert.assertEquals("postInvoke serverPort", 8443, actualServerPort); + + boolean actualSecure = actualRequest.isSecure(); + Assert.assertTrue("postInvoke secure", actualSecure); + } + + @Test + public void testInvokeXforwardedHostAndPort() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("hostHeader", "x-forwarded-host"); + filterDef.addInitParameter("portHeader", "x-forwarded-port"); + filterDef.addInitParameter("protocolHeader", "x-forwarded-proto"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + // protocol + request.setSecure(false); + request.setServerPort(8080); + request.setScheme("http"); + // host and port + request.getCoyoteRequest().serverName().setString("10.0.0.1"); + request.setHeader("x-forwarded-host", "example.com:8443"); + request.setHeader("x-forwarded-proto", "https"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + // protocol + String actualServerName = actualRequest.getServerName(); + Assert.assertEquals("postInvoke serverName", "example.com", actualServerName); + + String actualScheme = actualRequest.getScheme(); + Assert.assertEquals("postInvoke scheme", "https", actualScheme); + + int actualServerPort = actualRequest.getServerPort(); + Assert.assertEquals("postInvoke serverPort", 443, actualServerPort); + + boolean actualSecure = actualRequest.isSecure(); + Assert.assertTrue("postInvoke secure", actualSecure); + } + + @Test + public void testListToCommaDelimitedString() { + String[] actual = StringUtils.splitCommaSeparated("element1, element2, element3"); + String[] expected = new String[] { "element1", "element2", "element3" }; + Assert.assertEquals(expected.length, actual.length); + for (int i = 0; i < actual.length; i++) { + Assert.assertEquals(expected[i], actual[i]); + } + } + + @Test + public void testListToCommaDelimitedStringMixedSpaceChars() { + String[] actual = StringUtils.splitCommaSeparated("element1 , element2,\t element3"); + String[] expected = new String[] { "element1", "element2", "element3" }; + Assert.assertEquals(expected.length, actual.length); + for (int i = 0; i < actual.length; i++) { + Assert.assertEquals(expected[i], actual[i]); + } + } + + private MockFilterChain testRemoteIpFilter(FilterDef filterDef, Request request) + throws LifecycleException, IOException, ServletException { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + + RemoteIpFilter remoteIpFilter = new RemoteIpFilter(); + filterDef.setFilterClass(RemoteIpFilter.class.getName()); + filterDef.setFilter(remoteIpFilter); + filterDef.setFilterName(RemoteIpFilter.class.getName()); + root.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(RemoteIpFilter.class.getName()); + filterMap.addURLPatternDecoded("*"); + root.addFilterMap(filterMap); + + getTomcatInstance().start(); + + MockFilterChain filterChain = new MockFilterChain(); + + // TEST + TesterResponse response = new TesterResponse(); + response.setRequest(request); + remoteIpFilter.doFilter(request, response, filterChain); + return filterChain; + } + + @Test + public void testRequestAttributesForAccessLog() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("protocolHeader", "x-forwarded-proto"); + filterDef.addInitParameter("remoteIpHeader", "x-my-forwarded-for"); + filterDef.addInitParameter("httpServerPort", "8080"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.0.10"); + request.setHeader("x-my-forwarded-for", "140.211.11.130"); + request.setHeader("x-forwarded-proto", "http"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + Assert.assertEquals("org.apache.catalina.AccessLog.ServerPort", Integer.valueOf(8080), + actualRequest.getAttribute(AccessLog.SERVER_PORT_ATTRIBUTE)); + + Assert.assertEquals("org.apache.catalina.AccessLog.RemoteAddr", "140.211.11.130", + actualRequest.getAttribute(AccessLog.REMOTE_ADDR_ATTRIBUTE)); + + Assert.assertEquals("org.apache.catalina.AccessLog.RemoteHost", "140.211.11.130", + actualRequest.getAttribute(AccessLog.REMOTE_HOST_ATTRIBUTE)); + } + + @Test + public void testRequestForwarded() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("protocolHeader", "x-forwarded-proto"); + filterDef.addInitParameter("remoteIpHeader", "x-my-forwarded-for"); + filterDef.addInitParameter("httpServerPort", "8080"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.0.10"); + request.setHeader("x-my-forwarded-for", "140.211.11.130"); + request.setHeader("x-forwarded-proto", "http"); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + Assert.assertEquals("org.apache.tomcat.request.forwarded", Boolean.TRUE, + actualRequest.getAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE)); + } + + /* + * Test {@link RemoteIpFilter} in Tomcat standalone server + */ + @Test + public void testWithTomcatServer() throws Exception { + + // mostly default configuration : enable "x-forwarded-proto" + Map remoteIpFilterParameter = new HashMap<>(); + remoteIpFilterParameter.put("protocolHeader", "x-forwarded-proto"); + + // SETUP + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + + FilterDef filterDef = new FilterDef(); + filterDef.getParameterMap().putAll(remoteIpFilterParameter); + filterDef.setFilterClass(RemoteIpFilter.class.getName()); + filterDef.setFilterName(RemoteIpFilter.class.getName()); + + root.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(RemoteIpFilter.class.getName()); + filterMap.addURLPatternDecoded("*"); + root.addFilterMap(filterMap); + + MockHttpServlet mockServlet = new MockHttpServlet(); + + Tomcat.addServlet(root, mockServlet.getClass().getName(), mockServlet); + root.addServletMappingDecoded("/test", mockServlet.getClass().getName()); + + getTomcatInstance().start(); + + // TEST + HttpURLConnection httpURLConnection = (HttpURLConnection) new URL( + "http://localhost:" + tomcat.getConnector().getLocalPort() + "/test").openConnection(); + String expectedRemoteAddr = "my-remote-addr"; + httpURLConnection.addRequestProperty("x-forwarded-for", expectedRemoteAddr); + httpURLConnection.addRequestProperty("x-forwarded-proto", "https"); + + // VALIDATE + Assert.assertEquals(HttpURLConnection.HTTP_OK, httpURLConnection.getResponseCode()); + + // VALIDATE X-FORWARDED-FOR + Assert.assertEquals(expectedRemoteAddr, mockServlet.remoteAddr); + Assert.assertEquals(expectedRemoteAddr, mockServlet.remoteHost); + + // VALIDATE X-FORWARDED-PROTO + Assert.assertTrue(mockServlet.isSecure); + Assert.assertEquals("https", mockServlet.scheme); + Assert.assertEquals(443, mockServlet.serverPort); + } + + @Test + public void testJSessionIdSecureAttributeMissing() throws Exception { + + // mostly default configuration : enable "x-forwarded-proto" + Map remoteIpFilterParameter = new HashMap<>(); + remoteIpFilterParameter.put("protocolHeader", "x-forwarded-proto"); + + // SETUP + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + + FilterDef filterDef = new FilterDef(); + filterDef.getParameterMap().putAll(remoteIpFilterParameter); + filterDef.setFilterClass(RemoteIpFilter.class.getName()); + filterDef.setFilterName(RemoteIpFilter.class.getName()); + + root.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(RemoteIpFilter.class.getName()); + filterMap.addURLPatternDecoded("*"); + root.addFilterMap(filterMap); + + Bug66471Servlet bug66471Servlet = new Bug66471Servlet(); + + Tomcat.addServlet(root, bug66471Servlet.getClass().getName(), bug66471Servlet); + root.addServletMappingDecoded("/test", bug66471Servlet.getClass().getName()); + + getTomcatInstance().start(); + + Map> resHeaders = new HashMap<>(); + Map> reqHeaders = new HashMap<>(); + String expectedRemoteAddr = "my-remote-addr"; + List forwardedFor = new ArrayList<>(1); + forwardedFor.add(expectedRemoteAddr); + List forwardedProto = new ArrayList<>(1); + forwardedProto.add("https"); + reqHeaders.put("x-forwarded-for", forwardedFor); + reqHeaders.put("x-forwarded-proto", forwardedProto); + + getUrl("http://localhost:" + tomcat.getConnector().getLocalPort() + "/test", null, reqHeaders, resHeaders); + String setCookie = resHeaders.get("Set-Cookie").get(0); + Assert.assertTrue(setCookie.contains("Secure")); + Assert.assertTrue(bug66471Servlet.isSecure.booleanValue()); + } + + public static class Bug66471Servlet extends HttpServlet { + private static final long serialVersionUID = 1L; + public Boolean isSecure; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.getSession(); + isSecure = (Boolean) req.getAttribute(Globals.REMOTE_IP_FILTER_SECURE); + } + } + + @Test + public void testInternalProxies() throws Exception { + RemoteIpFilter remoteIpFilter = new RemoteIpFilter(); + Pattern internalProxiesPattern = remoteIpFilter.getInternalProxies(); + + doTestPattern(internalProxiesPattern, "8.8.8.8", false); + doTestPattern(internalProxiesPattern, "100.62.0.0", false); + doTestPattern(internalProxiesPattern, "100.63.255.255", false); + doTestPattern(internalProxiesPattern, "100.64.0.0", true); + doTestPattern(internalProxiesPattern, "100.65.0.0", true); + doTestPattern(internalProxiesPattern, "100.68.0.0", true); + doTestPattern(internalProxiesPattern, "100.72.0.0", true); + doTestPattern(internalProxiesPattern, "100.88.0.0", true); + doTestPattern(internalProxiesPattern, "100.95.0.0", true); + doTestPattern(internalProxiesPattern, "100.102.0.0", true); + doTestPattern(internalProxiesPattern, "100.110.0.0", true); + doTestPattern(internalProxiesPattern, "100.126.0.0", true); + doTestPattern(internalProxiesPattern, "100.127.255.255", true); + doTestPattern(internalProxiesPattern, "100.128.0.0", false); + doTestPattern(internalProxiesPattern, "100.130.0.0", false); + } + + private void doTestPattern(Pattern pattern, String input, boolean expectedMatch) { + boolean match = pattern.matcher(input).matches(); + Assert.assertEquals(input, Boolean.valueOf(expectedMatch), Boolean.valueOf(match)); + } +} diff --git a/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java b/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java new file mode 100644 index 0000000..9ee91c6 --- /dev/null +++ b/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java @@ -0,0 +1,342 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.easymock.EasyMock; + +public class TestRestCsrfPreventionFilter { + + private static final String NONCE = "nonce"; + + private static final String INVALID_NONCE = "invalid-nonce"; + + private static final String GET_METHOD = "GET"; + + private static final String POST_METHOD = "POST"; + + public static final String ACCEPTED_PATH1 = "/accepted/index1.jsp"; + + public static final String ACCEPTED_PATH2 = "/accepted/index2.jsp"; + + public static final String ACCEPTED_PATHS = ACCEPTED_PATH1 + "," + ACCEPTED_PATH2; + + private RestCsrfPreventionFilter filter; + + private TesterRequest request; + + private TesterResponse response; + + private TesterFilterChain filterChain; + + private HttpSession session; + + @Before + public void setUp() { + filter = new RestCsrfPreventionFilter() { + @Override + protected String generateNonce(HttpServletRequest request) { + return NONCE; + } + }; + request = new TesterRequest(); + response = new TesterResponse(); + filterChain = new TesterFilterChain(); + session = EasyMock.createMock(HttpSession.class); + } + + @Test + public void testGetRequestNoSessionNoNonce() throws Exception { + setRequestExpectations(GET_METHOD, null, null); + filter.doFilter(request, response, filterChain); + verifyContinueChain(); + } + + @Test + public void testPostRequestNoSessionNoNonce() throws Exception { + setRequestExpectations(POST_METHOD, null, null); + filter.doFilter(request, response, filterChain); + verifyDenyResponse(HttpServletResponse.SC_FORBIDDEN); + } + + @Test + public void testPostRequestSessionNoNonce1() throws Exception { + setRequestExpectations(POST_METHOD, session, null); + testPostRequestHeaderScenarios(null, true); + } + + @Test + public void testPostRequestSessionNoNonce2() throws Exception { + setRequestExpectations(POST_METHOD, session, null); + testPostRequestHeaderScenarios(NONCE, true); + } + + @Test + public void testPostRequestSessionInvalidNonce() throws Exception { + setRequestExpectations(POST_METHOD, session, INVALID_NONCE); + testPostRequestHeaderScenarios(NONCE, true); + } + + @Test + public void testPostRequestSessionValidNonce() throws Exception { + setRequestExpectations(POST_METHOD, session, NONCE); + testPostRequestHeaderScenarios(NONCE, false); + } + + @Test + public void testGetFetchRequestSessionNoNonce() throws Exception { + setRequestExpectations(GET_METHOD, session, Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE); + EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME)).andReturn(null); + session.setAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME, NONCE); + EasyMock.expectLastCall(); + EasyMock.replay(session); + filter.doFilter(request, response, filterChain); + verifyContinueChainNonceAvailable(); + EasyMock.verify(session); + } + + @Test + public void testPostFetchRequestSessionNoNonce() throws Exception { + setRequestExpectations(POST_METHOD, session, Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE); + testPostRequestHeaderScenarios(null, true); + } + + @Test + public void testGetFetchRequestSessionNonce() throws Exception { + setRequestExpectations(GET_METHOD, session, Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE); + EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME)).andReturn(NONCE); + EasyMock.replay(session); + filter.doFilter(request, response, filterChain); + verifyContinueChainNonceAvailable(); + EasyMock.verify(session); + } + + @Test + public void testPostFetchRequestSessionNonce() throws Exception { + setRequestExpectations(POST_METHOD, session, Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE); + testPostRequestHeaderScenarios(NONCE, true); + } + + @Test + public void testPostRequestCustomDenyStatus() throws Exception { + setRequestExpectations(POST_METHOD, null, null); + filter.setDenyStatus(HttpServletResponse.SC_BAD_REQUEST); + filter.doFilter(request, response, filterChain); + verifyDenyResponse(HttpServletResponse.SC_BAD_REQUEST); + } + + @Test + public void testPostRequestValidNonceAsParameterValidPath1() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { NONCE }, ACCEPTED_PATH1); + testPostRequestParamsScenarios(NONCE, false, true); + } + + @Test + public void testPostRequestValidNonceAsParameterValidPath2() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { NONCE }, ACCEPTED_PATH2); + testPostRequestParamsScenarios(NONCE, false, true); + } + + @Test + public void testPostRequestInvalidNonceAsParameterValidPath() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { INVALID_NONCE }, ACCEPTED_PATH1); + testPostRequestParamsScenarios(NONCE, true, true); + } + + @Test + public void testPostRequestValidNonceAsParameterInvalidPath() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { NONCE }, ACCEPTED_PATH1 + "blah"); + testPostRequestParamsScenarios(NONCE, true, true); + } + + @Test + public void testPostRequestValidNonceAsParameterNoPath() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { NONCE }, ACCEPTED_PATH1); + testPostRequestParamsScenarios(NONCE, true, false); + } + + @Test + public void testPostRequestValidNonceAsParameterNoNonceInSession() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { NONCE }, ACCEPTED_PATH1); + testPostRequestParamsScenarios(null, true, true); + } + + @Test + public void testPostRequestValidNonceAsParameterInvalidNonceAsHeader() throws Exception { + setRequestExpectations(POST_METHOD, session, INVALID_NONCE, new String[] { NONCE }, ACCEPTED_PATH1); + testPostRequestParamsScenarios(NONCE, true, true); + } + + @Test + public void testPostRequestNoNonceAsParameterAndHeaderValidPath() throws Exception { + setRequestExpectations(POST_METHOD, session, null, null, ACCEPTED_PATH1); + testPostRequestParamsScenarios(NONCE, true, true); + } + + @Test + public void testPostRequestMultipleValidNoncesAsParameterValidPath() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { NONCE, NONCE }, ACCEPTED_PATH1); + testPostRequestParamsScenarios(NONCE, false, true); + } + + @Test + public void testPostRequestMultipleNoncesAsParameterValidPath() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { NONCE, INVALID_NONCE }, ACCEPTED_PATH1); + testPostRequestParamsScenarios(NONCE, true, true); + } + + @Test + public void testPostRequestMultipleInvalidNoncesAsParameterValidPath() throws Exception { + setRequestExpectations(POST_METHOD, session, null, new String[] { INVALID_NONCE, INVALID_NONCE }, + ACCEPTED_PATH1); + testPostRequestParamsScenarios(NONCE, true, true); + } + + @Test + public void testGETRequestFetchNonceAsParameter() throws Exception { + setRequestExpectations(GET_METHOD, null, null, new String[] { Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE }, + ACCEPTED_PATH1); + filter.setPathsAcceptingParams(ACCEPTED_PATHS); + filter.doFilter(request, response, filterChain); + verifyContinueChainNonceNotAvailable(); + } + + private void testPostRequestHeaderScenarios(String sessionAttr, boolean denyResponse) throws Exception { + testPostRequestParamsScenarios(sessionAttr, denyResponse, false); + } + + private void testPostRequestParamsScenarios(String sessionAttr, boolean denyResponse, boolean configurePaths) + throws Exception { + EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME)).andReturn(sessionAttr); + EasyMock.replay(session); + if (configurePaths) { + filter.setPathsAcceptingParams(ACCEPTED_PATHS); + } + filter.doFilter(request, response, filterChain); + if (denyResponse) { + verifyDenyResponse(HttpServletResponse.SC_FORBIDDEN); + } else { + verifyContinueChain(); + } + EasyMock.verify(session); + } + + private void setRequestExpectations(String method, HttpSession session, String headerValue) { + setRequestExpectations(method, session, headerValue, null, null); + } + + private void setRequestExpectations(String method, HttpSession session, String headerValue, String[] paramValues, + String servletPath) { + request.setMethod(method); + request.setSession(session); + request.setHeader(Constants.CSRF_REST_NONCE_HEADER_NAME, headerValue); + request.setParameterValues(paramValues); + request.setServletPath(servletPath); + } + + private void verifyContinueChain() { + Assert.assertTrue(filterChain.isVisited()); + } + + private void verifyContinueChainNonceAvailable() { + Assert.assertTrue(NONCE.equals(response.getHeader(Constants.CSRF_REST_NONCE_HEADER_NAME))); + verifyContinueChain(); + } + + private void verifyContinueChainNonceNotAvailable() { + Assert.assertNull(response.getHeader(Constants.CSRF_REST_NONCE_HEADER_NAME)); + verifyContinueChain(); + } + + private void verifyDenyResponse(int statusCode) { + Assert.assertTrue(Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE + .equals(response.getHeader(Constants.CSRF_REST_NONCE_HEADER_NAME))); + Assert.assertTrue(statusCode == response.getStatus()); + Assert.assertTrue(!filterChain.isVisited()); + } + + private static class TesterFilterChain implements FilterChain { + private boolean visited = false; + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + visited = true; + } + + boolean isVisited() { + return visited; + } + } + + private static class TesterRequest extends TesterHttpServletRequest { + private HttpSession session; + private String[] paramValues; + private String servletPath; + + void setSession(HttpSession session) { + this.session = session; + } + + @Override + public HttpSession getSession(boolean create) { + return session; + } + + void setParameterValues(String[] paramValues) { + this.paramValues = paramValues; + } + + @Override + public String[] getParameterValues(String name) { + return paramValues; + } + + void setServletPath(String servletPath) { + this.servletPath = servletPath; + } + + @Override + public String getServletPath() { + return servletPath; + } + + @Override + public String getPathInfo() { + return ""; + } + } + + private static class TesterResponse extends TesterHttpServletResponse { + @Override + public void sendError(int status, String message) throws IOException { + setStatus(status); + } + } +} diff --git a/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter2.java b/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter2.java new file mode 100644 index 0000000..eb74714 --- /dev/null +++ b/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter2.java @@ -0,0 +1,352 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.authenticator.AuthenticatorBase; +import org.apache.catalina.authenticator.BasicAuthenticator; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +public class TestRestCsrfPreventionFilter2 extends TomcatBaseTest { + private static final boolean USE_COOKIES = true; + private static final boolean NO_COOKIES = !USE_COOKIES; + + private static final String METHOD_GET = "GET"; + private static final String METHOD_POST = "POST"; + + private static final String HTTP_PREFIX = "http://localhost:"; + private static final String CONTEXT_PATH_LOGIN = ""; + private static final String URI_PROTECTED = "/services/*"; + private static final String URI_CSRF_PROTECTED = "/services/customers/*"; + private static final String LIST_CUSTOMERS = "/services/customers/"; + private static final String REMOVE_CUSTOMER = "/services/customers/removeCustomer"; + private static final String ADD_CUSTOMER = "/services/customers/addCustomer"; + private static final String REMOVE_ALL_CUSTOMERS = "/services/customers/removeAllCustomers"; + private static final String FILTER_INIT_PARAM = "pathsAcceptingParams"; + private static final String SERVLET_NAME = "TesterServlet"; + private static final String FILTER_NAME = "Csrf"; + + private static final String CUSTOMERS_LIST_RESPONSE = "Customers list"; + private static final String CUSTOMER_REMOVED_RESPONSE = "Customer removed"; + private static final String CUSTOMER_ADDED_RESPONSE = "Customer added"; + + private static final String INVALID_NONCE_1 = "invalid_nonce"; + private static final String INVALID_NONCE_2 = ""; + + private static final String USER = "user"; + private static final String PWD = "pwd"; + private static final String ROLE = "role"; + private static final String METHOD = "BASIC"; + private static final BasicCredentials CREDENTIALS = new BasicCredentials(METHOD, USER, PWD); + + private static final String CLIENT_AUTH_HEADER = "authorization"; + private static final String SERVER_COOKIE_HEADER = "Set-Cookie"; + private static final String CLIENT_COOKIE_HEADER = "Cookie"; + + private static final int SHORT_SESSION_TIMEOUT_MINS = 1; + + private Tomcat tomcat; + private Context context; + private List cookies = new ArrayList<>(); + private String validNonce; + + @Override + public void setUp() throws Exception { + super.setUp(); + + tomcat = getTomcatInstance(); + + tomcat.addUser(USER, PWD); + tomcat.addRole(USER, ROLE); + + setUpApplication(); + + tomcat.start(); + } + + @Test + public void testRestCsrfProtectionWithHeader() throws Exception { + testClearGet(); + testClearPost(); + testGetFirstFetch(); + testValidPost(); + testInvalidPost(); + testGetSecondFetch(); + } + + @Test + public void testRestCsrfProtectionWithRequestParams() throws Exception { + testGetFirstFetch(); + testValidPostWithRequestParams(); + testInvalidPostWithRequestParams(); + } + + private void testClearGet() throws Exception { + doTest(METHOD_GET, LIST_CUSTOMERS, CREDENTIALS, null, NO_COOKIES, HttpServletResponse.SC_OK, + CUSTOMERS_LIST_RESPONSE, null, false, null); + } + + private void testClearPost() throws Exception { + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, null, NO_COOKIES, HttpServletResponse.SC_FORBIDDEN, null, + null, true, Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + } + + private void testGetFirstFetch() throws Exception { + doTest(METHOD_GET, LIST_CUSTOMERS, CREDENTIALS, null, NO_COOKIES, HttpServletResponse.SC_OK, + CUSTOMERS_LIST_RESPONSE, Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE, true, null); + } + + private void testValidPost() throws Exception { + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, null, USE_COOKIES, HttpServletResponse.SC_OK, + CUSTOMER_REMOVED_RESPONSE, validNonce, false, null); + } + + private void testInvalidPost() throws Exception { + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, null, USE_COOKIES, HttpServletResponse.SC_FORBIDDEN, null, + Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE, true, Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, null, USE_COOKIES, HttpServletResponse.SC_FORBIDDEN, null, + INVALID_NONCE_1, true, Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, null, USE_COOKIES, HttpServletResponse.SC_FORBIDDEN, null, + INVALID_NONCE_2, true, Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, null, USE_COOKIES, HttpServletResponse.SC_FORBIDDEN, null, + null, true, Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + } + + private void testGetSecondFetch() throws Exception { + doTest(METHOD_GET, LIST_CUSTOMERS, CREDENTIALS, null, USE_COOKIES, HttpServletResponse.SC_OK, + CUSTOMERS_LIST_RESPONSE, Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE, true, validNonce); + } + + private void testValidPostWithRequestParams() throws Exception { + String validBody = Constants.CSRF_REST_NONCE_HEADER_NAME + "=" + validNonce; + String invalidbody = Constants.CSRF_REST_NONCE_HEADER_NAME + "=" + INVALID_NONCE_1; + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, validBody.getBytes(StandardCharsets.ISO_8859_1), USE_COOKIES, + HttpServletResponse.SC_OK, CUSTOMER_REMOVED_RESPONSE, null, false, null); + doTest(METHOD_POST, ADD_CUSTOMER, CREDENTIALS, validBody.getBytes(StandardCharsets.ISO_8859_1), USE_COOKIES, + HttpServletResponse.SC_OK, CUSTOMER_ADDED_RESPONSE, null, false, null); + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, invalidbody.getBytes(StandardCharsets.ISO_8859_1), + USE_COOKIES, HttpServletResponse.SC_OK, CUSTOMER_REMOVED_RESPONSE, validNonce, false, null); + } + + private void testInvalidPostWithRequestParams() throws Exception { + String validBody = Constants.CSRF_REST_NONCE_HEADER_NAME + "=" + validNonce; + String invalidbody1 = Constants.CSRF_REST_NONCE_HEADER_NAME + "=" + INVALID_NONCE_1; + String invalidbody2 = Constants.CSRF_REST_NONCE_HEADER_NAME + "=" + + Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE; + doTest(METHOD_POST, REMOVE_ALL_CUSTOMERS, CREDENTIALS, validBody.getBytes(StandardCharsets.ISO_8859_1), + USE_COOKIES, HttpServletResponse.SC_FORBIDDEN, null, null, true, + Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, invalidbody1.getBytes(StandardCharsets.ISO_8859_1), + USE_COOKIES, HttpServletResponse.SC_FORBIDDEN, null, null, true, + Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + doTest(METHOD_POST, REMOVE_CUSTOMER, CREDENTIALS, invalidbody2.getBytes(StandardCharsets.ISO_8859_1), + USE_COOKIES, HttpServletResponse.SC_FORBIDDEN, null, null, true, + Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE); + } + + private void doTest(String method, String uri, BasicCredentials credentials, byte[] body, boolean useCookie, + int expectedRC, String expectedResponse, String nonce, boolean expectCsrfRH, String expectedCsrfRHV) + throws Exception { + Map> reqHeaders = new HashMap<>(); + Map> respHeaders = new HashMap<>(); + + addNonce(reqHeaders, nonce, n -> Objects.nonNull(n)); + + if (useCookie) { + addCookies(reqHeaders, l -> Objects.nonNull(l) && l.size() > 0); + } + + addCredentials(reqHeaders, credentials, c -> Objects.nonNull(c)); + + ByteChunk bc = new ByteChunk(); + int rc; + if (METHOD_GET.equals(method)) { + rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, respHeaders); + } else { + rc = postUrl(body, HTTP_PREFIX + getPort() + uri, bc, reqHeaders, respHeaders); + } + + Assert.assertEquals(expectedRC, rc); + + if (expectedRC == HttpServletResponse.SC_OK) { + Assert.assertEquals(expectedResponse, bc.toString()); + List newCookies = respHeaders.get(SERVER_COOKIE_HEADER); + saveCookies(newCookies, l -> Objects.nonNull(l) && l.size() > 0); + } + + if (!expectCsrfRH) { + Assert.assertNull(respHeaders.get(Constants.CSRF_REST_NONCE_HEADER_NAME)); + } else { + List respHeaderValue = respHeaders.get(Constants.CSRF_REST_NONCE_HEADER_NAME); + Assert.assertNotNull(respHeaderValue); + if (Objects.nonNull(expectedCsrfRHV)) { + Assert.assertTrue(respHeaderValue.contains(expectedCsrfRHV)); + } else { + validNonce = respHeaderValue.get(0); + } + } + } + + private void saveCookies(List newCookies, Predicate> tester) { + if (tester.test(newCookies)) { + newCookies.forEach(h -> cookies.add(h.substring(0, h.indexOf(';')))); + } + } + + private void addCookies(Map> reqHeaders, Predicate> tester) { + if (tester.test(cookies)) { + StringBuilder cookieHeader = new StringBuilder(); + boolean first = true; + for (String cookie : cookies) { + if (!first) { + cookieHeader.append(';'); + } else { + first = false; + } + cookieHeader.append(cookie); + } + addRequestHeader(reqHeaders, CLIENT_COOKIE_HEADER, cookieHeader.toString()); + } + } + + private void addNonce(Map> reqHeaders, String nonce, Predicate tester) { + if (tester.test(nonce)) { + addRequestHeader(reqHeaders, Constants.CSRF_REST_NONCE_HEADER_NAME, nonce); + } + } + + private void addCredentials(Map> reqHeaders, BasicCredentials credentials, + Predicate tester) { + if (tester.test(credentials)) { + addRequestHeader(reqHeaders, CLIENT_AUTH_HEADER, credentials.getCredentials()); + } + } + + private void addRequestHeader(Map> reqHeaders, String key, String value) { + List valueList = new ArrayList<>(1); + valueList.add(value); + reqHeaders.put(key, valueList); + } + + private void setUpApplication() throws Exception { + context = tomcat.addContext(CONTEXT_PATH_LOGIN, System.getProperty("java.io.tmpdir")); + context.setSessionTimeout(SHORT_SESSION_TIMEOUT_MINS); + + Tomcat.addServlet(context, SERVLET_NAME, new TesterServlet()); + context.addServletMappingDecoded(URI_PROTECTED, SERVLET_NAME); + + FilterDef filterDef = new FilterDef(); + filterDef.setFilterName(FILTER_NAME); + filterDef.setFilterClass(RestCsrfPreventionFilter.class.getCanonicalName()); + filterDef.addInitParameter(FILTER_INIT_PARAM, REMOVE_CUSTOMER + "," + ADD_CUSTOMER); + context.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName(FILTER_NAME); + filterMap.addURLPatternDecoded(URI_CSRF_PROTECTED); + context.addFilterMap(filterMap); + + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(URI_PROTECTED); + + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + context.addConstraint(sc); + + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod(METHOD); + context.setLoginConfig(lc); + + AuthenticatorBase basicAuthenticator = new BasicAuthenticator(); + context.getPipeline().addValve(basicAuthenticator); + } + + private static final class BasicCredentials { + private final String method; + private final String username; + private final String password; + private final String credentials; + + private BasicCredentials(String aMethod, String aUsername, String aPassword) { + method = aMethod; + username = aUsername; + password = aPassword; + String userCredentials = username + ":" + password; + byte[] credentialsBytes = userCredentials.getBytes(StandardCharsets.ISO_8859_1); + String base64auth = Base64.encodeBase64String(credentialsBytes); + credentials = method + " " + base64auth; + } + + private String getCredentials() { + return credentials; + } + } + + private static class TesterServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (Objects.equals(LIST_CUSTOMERS, getRequestedPath(req))) { + resp.getWriter().print(CUSTOMERS_LIST_RESPONSE); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (Objects.equals(REMOVE_CUSTOMER, getRequestedPath(req))) { + resp.getWriter().print(CUSTOMER_REMOVED_RESPONSE); + } else if (Objects.equals(ADD_CUSTOMER, getRequestedPath(req))) { + resp.getWriter().print(CUSTOMER_ADDED_RESPONSE); + } + } + + private String getRequestedPath(HttpServletRequest request) { + String path = request.getServletPath(); + if (Objects.nonNull(request.getPathInfo())) { + path = path + request.getPathInfo(); + } + return path; + } + } +} diff --git a/test/org/apache/catalina/filters/TesterFilterChain.java b/test/org/apache/catalina/filters/TesterFilterChain.java new file mode 100644 index 0000000..165711d --- /dev/null +++ b/test/org/apache/catalina/filters/TesterFilterChain.java @@ -0,0 +1,32 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +public class TesterFilterChain implements FilterChain { + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + // NoOp + } +} diff --git a/test/org/apache/catalina/filters/TesterFilterConfigs.java b/test/org/apache/catalina/filters/TesterFilterConfigs.java new file mode 100644 index 0000000..c902240 --- /dev/null +++ b/test/org/apache/catalina/filters/TesterFilterConfigs.java @@ -0,0 +1,278 @@ +/* + * 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.catalina.filters; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; + +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; + +import org.apache.tomcat.unittest.TesterServletContext; +import org.apache.tomcat.util.descriptor.web.FilterDef; + +public class TesterFilterConfigs { + public static final String HTTPS_WWW_APACHE_ORG = "https://www.apache.org"; + public static final String HTTP_TOMCAT_APACHE_ORG = "http://tomcat.apache.org"; + public static final String EXPOSED_HEADERS = "X-CUSTOM-HEADER"; + /** + * Any origin + */ + public static final String ANY_ORIGIN = "*"; + + public static final TesterServletContext mockServletContext = new TesterServletContext(); + + // Default config for the test is to allow any origin + public static FilterConfig getDefaultFilterConfig() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = ANY_ORIGIN; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CorsFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getFilterConfigAnyOriginAndSupportsCredentials() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS + ",PUT"; + final String allowedOrigins = ANY_ORIGIN; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = "true"; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getFilterConfigAnyOriginAndSupportsCredentialsDisabled() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS + ",PUT"; + final String allowedOrigins = ANY_ORIGIN; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = "false"; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getFilterConfigSpecificOriginAndSupportsCredentialsDisabled() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS + ",PUT"; + final String allowedOrigins = HTTP_TOMCAT_APACHE_ORG + "," + HTTPS_WWW_APACHE_ORG; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = "false"; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getFilterConfigSpecificOriginNullAllowed() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = HTTP_TOMCAT_APACHE_ORG + ",null"; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CorsFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getFilterConfigWithExposedHeaders() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = ANY_ORIGIN; + final String exposedHeaders = EXPOSED_HEADERS; + final String supportCredentials = CorsFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getSecureFilterConfig() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS + ",PUT"; + final String allowedOrigins = HTTPS_WWW_APACHE_ORG; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = "true"; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getNullFilterConfig() { + return generateFilterConfig(null, null, null, null, null, null, null); + } + + public static FilterConfig getSpecificOriginFilterConfig() { + final String allowedOrigins = HTTPS_WWW_APACHE_ORG + "," + HTTP_TOMCAT_APACHE_ORG; + + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS + ",PUT"; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CorsFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getSpecificOriginFilterConfigNegativeMaxAge() { + final String allowedOrigins = HTTPS_WWW_APACHE_ORG + "," + HTTP_TOMCAT_APACHE_ORG; + + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS + ",PUT"; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CorsFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = "-1"; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getFilterConfigInvalidMaxPreflightAge() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = CorsFilter.DEFAULT_ALLOWED_ORIGINS; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CorsFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = "abc"; + final String decorateRequest = CorsFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getEmptyFilterConfig() { + final String allowedHttpHeaders = ""; + final String allowedHttpMethods = ""; + final String allowedOrigins = ""; + final String exposedHeaders = ""; + final String supportCredentials = ""; + final String preflightMaxAge = ""; + final String decorateRequest = ""; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig getFilterConfigDecorateRequestDisabled() { + final String allowedHttpHeaders = CorsFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CorsFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = ANY_ORIGIN; + final String exposedHeaders = CorsFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CorsFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CorsFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String decorateRequest = "false"; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, allowedOrigins, exposedHeaders, + supportCredentials, preflightMaxAge, decorateRequest); + } + + public static FilterConfig generateFilterConfig(FilterDef filterDef) { + + TesterServletContext mockServletContext = new TesterServletContext(); + Map parameters = filterDef.getParameterMap(); + + FilterConfig filterConfig = new FilterConfig() { + + @Override + public String getFilterName() { + return filterDef.getFilterName(); + } + + @Override + public ServletContext getServletContext() { + return mockServletContext; + } + + @Override + public String getInitParameter(String name) { + + return parameters.get(name); + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(parameters.keySet()); + } + }; + + return filterConfig; + } + + private static FilterConfig generateFilterConfig(final String allowedHttpHeaders, final String allowedHttpMethods, + final String allowedOrigins, final String exposedHeaders, final String supportCredentials, + final String preflightMaxAge, final String decorateRequest) { + FilterConfig filterConfig = new FilterConfig() { + + @Override + public String getFilterName() { + return "cors-filter"; + } + + @Override + public ServletContext getServletContext() { + return mockServletContext; + } + + @Override + public String getInitParameter(String name) { + if (CorsFilter.PARAM_CORS_ALLOWED_HEADERS.equalsIgnoreCase(name)) { + return allowedHttpHeaders; + } else if (CorsFilter.PARAM_CORS_ALLOWED_METHODS.equalsIgnoreCase(name)) { + return allowedHttpMethods; + } else if (CorsFilter.PARAM_CORS_ALLOWED_ORIGINS.equalsIgnoreCase(name)) { + return allowedOrigins; + } else if (CorsFilter.PARAM_CORS_EXPOSED_HEADERS.equalsIgnoreCase(name)) { + return exposedHeaders; + } else if (CorsFilter.PARAM_CORS_SUPPORT_CREDENTIALS.equalsIgnoreCase(name)) { + return supportCredentials; + } else if (CorsFilter.PARAM_CORS_PREFLIGHT_MAXAGE.equalsIgnoreCase(name)) { + return preflightMaxAge; + } else if (CorsFilter.PARAM_CORS_REQUEST_DECORATE.equalsIgnoreCase(name)) { + return decorateRequest; + } + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return null; + } + }; + + return filterConfig; + } +} diff --git a/test/org/apache/catalina/filters/TesterHttpServletRequest.java b/test/org/apache/catalina/filters/TesterHttpServletRequest.java new file mode 100644 index 0000000..449963e --- /dev/null +++ b/test/org/apache/catalina/filters/TesterHttpServletRequest.java @@ -0,0 +1,463 @@ +/* + * 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.catalina.filters; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConnection; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletMapping; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.Part; +import jakarta.servlet.http.PushBuilder; + +public class TesterHttpServletRequest implements HttpServletRequest { + + private Map attributes = new HashMap<>(); + private Map> headers = new HashMap<>(); + private String method; + private String scheme; + private String serverName; + private int serverPort; + private String contentType; + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + @Override + public String getCharacterEncoding() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setCharacterEncoding(String env) throws UnsupportedEncodingException { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getContentLength() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getContentType() { + return this.contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getParameter(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getParameterNames() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String[] getParameterValues(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Map getParameterMap() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getProtocol() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getScheme() { + return scheme; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + @Override + public String getServerName() { + return serverName; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + + @Override + public int getServerPort() { + return serverPort; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + @Override + public BufferedReader getReader() throws IOException { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRemoteAddr() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRemoteHost() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setAttribute(String name, Object o) { + attributes.put(name, o); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + + @Override + public Locale getLocale() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getLocales() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isSecure() { + throw new RuntimeException("Not implemented"); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getRemotePort() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getLocalName() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getLocalAddr() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getLocalPort() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getAuthType() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Cookie[] getCookies() { + throw new RuntimeException("Not implemented"); + } + + @Override + public long getDateHeader(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getHeader(String name) { + List list = headers.get(name); + if (list != null) { + return list.get(0); + // return CorsFilter.join(new HashSet<>(list), ","); + } + return null; + } + + public void setHeader(String name, String value) { + List values = new ArrayList<>(); + values.add(value); + headers.put(name, values); + } + + @Override + public Enumeration getHeaders(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public int getIntHeader(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public HttpServletMapping getHttpServletMapping() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + @Override + public String getPathInfo() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getPathTranslated() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getContextPath() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getQueryString() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRemoteUser() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isUserInRole(String role) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Principal getUserPrincipal() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRequestedSessionId() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRequestURI() { + throw new RuntimeException("Not implemented"); + } + + @Override + public StringBuffer getRequestURL() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getServletPath() { + throw new RuntimeException("Not implemented"); + } + + @Override + public HttpSession getSession(boolean create) { + throw new RuntimeException("Not implemented"); + } + + @Override + public HttpSession getSession() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isRequestedSessionIdValid() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isRequestedSessionIdFromURL() { + throw new RuntimeException("Not implemented"); + } + + @Override + public long getContentLengthLong() { + throw new RuntimeException("Not implemented"); + } + + @Override + public ServletContext getServletContext() { + throw new RuntimeException("Not implemented"); + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + throw new RuntimeException("Not implemented"); + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isAsyncStarted() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isAsyncSupported() { + throw new RuntimeException("Not implemented"); + } + + @Override + public AsyncContext getAsyncContext() { + throw new RuntimeException("Not implemented"); + } + + @Override + public DispatcherType getDispatcherType() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String changeSessionId() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public void login(String username, String password) throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public void logout() throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public Collection getParts() throws IOException, ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public T upgrade(Class httpUpgradeHandlerClass) + throws IOException, ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public PushBuilder newPushBuilder() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isTrailerFieldsReady() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Map getTrailerFields() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRequestId() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getProtocolRequestId() { + throw new RuntimeException("Not implemented"); + } + + @Override + public ServletConnection getServletConnection() { + throw new RuntimeException("Not implemented"); + } +} diff --git a/test/org/apache/catalina/filters/TesterHttpServletResponse.java b/test/org/apache/catalina/filters/TesterHttpServletResponse.java new file mode 100644 index 0000000..5f8d52d --- /dev/null +++ b/test/org/apache/catalina/filters/TesterHttpServletResponse.java @@ -0,0 +1,376 @@ +/* + * 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.catalina.filters; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; + +/** + * Mock HttpServletResponse + */ +public class TesterHttpServletResponse implements HttpServletResponse { + + private PrintWriter pw; + private List headerNames = new ArrayList<>(); + private List headerValues = new ArrayList<>(); + private int status; + + public TesterHttpServletResponse() { + // NOOP + } + + + @Override + public PrintWriter getWriter() throws IOException { + if (pw == null) { + pw = new PrintWriter(new StringWriter()); + } + return pw; + } + + + @Override + public String getHeader(String name) { + int index = headerNames.indexOf(name); + if (index != -1) { + return headerValues.get(index); + } + return null; + } + + + @Override + public void setHeader(String name, String value) { + int index = headerNames.indexOf(name); + if (index != -1) { + headerValues.set(index, value); + } else { + headerNames.add(name); + headerValues.add(value); + } + } + + + @Override + public void addHeader(String name, String value) { + headerNames.add(name); + headerValues.add(value); + } + + + @Override + public int getStatus() { + return status; + } + + + @Override + public void setStatus(int status) { + this.status = status; + } + + + public void setAppCommitted(@SuppressWarnings("unused") boolean appCommitted) { + /* NOOP */} + + public boolean isAppCommitted() { + return false; + } + + public Connector getConnector() { + return null; + } + + public void setConnector(@SuppressWarnings("unused") Connector connector) { + // NOOP + } + + public int getContentCount() { + return -1; + } + + public Context getContext() { + return null; + } + + public void setContext(@SuppressWarnings("unused") Context context) { + // NOOP + } + + public boolean getIncluded() { + return false; + } + + public void setIncluded(@SuppressWarnings("unused") boolean included) { + // NOOP + } + + public Request getRequest() { + return null; + } + + public void setRequest(@SuppressWarnings("unused") Request request) { + // NOOP + } + + public ServletResponse getResponse() { + return null; + } + + public OutputStream getStream() { + return null; + } + + public void setStream(@SuppressWarnings("unused") OutputStream stream) { + // NOOP + } + + public void setSuspended(@SuppressWarnings("unused") boolean suspended) { + // NOOP + } + + public boolean isSuspended() { + return false; + } + + public void setError() { + /* NOOP */} + + public boolean isError() { + return false; + } + + /** + * @return Always null + * + * @throws IOException Never happens + */ + public ServletOutputStream createOutputStream() throws IOException { + return null; + } + + /** + * @throws IOException Never happens + */ + public void finishResponse() throws IOException { + /* NOOP */} + + public int getContentLength() { + return -1; + } + + @Override + public String getContentType() { + return null; + } + + public PrintWriter getReporter() { + return null; + } + + public void recycle() { + /* NOOP */ + } + + /** + * @param b Unused + * + * @throws IOException Never happens + */ + public void write(int b) throws IOException { + // NOOP + } + + /** + * @param b Unused + * + * @throws IOException Never happens + */ + public void write(byte b[]) throws IOException { + // NOOP + } + + /** + * @param b Unused + * @param off Unused + * @param len Unused + * + * @throws IOException Never happens + */ + public void write(byte b[], int off, int len) throws IOException { + // NOOP + } + + @Override + public void flushBuffer() throws IOException { + /* NOOP */} + + @Override + public int getBufferSize() { + return -1; + } + + @Override + public String getCharacterEncoding() { + return null; + } + + @Override + public void setCharacterEncoding(String charEncoding) { + /* NOOP */} + + @Override + public ServletOutputStream getOutputStream() throws IOException { + return null; + } + + @Override + public Locale getLocale() { + return null; + } + + @Override + public boolean isCommitted() { + return false; + } + + @Override + public void reset() { + /* NOOP */} + + @Override + public void resetBuffer() { + /* NOOP */} + + @Override + public void setBufferSize(int size) { + /* NOOP */} + + @Override + public void setContentLength(int length) { + /* NOOP */} + + @Override + public void setContentType(String type) { + /* NOOP */} + + @Override + public void setLocale(Locale locale) { + /* NOOP */} + + @Override + public Collection getHeaderNames() { + return null; + } + + @Override + public Collection getHeaders(String name) { + return Collections.emptyList(); + } + + public String getMessage() { + return null; + } + + public void reset(@SuppressWarnings("unused") int status, @SuppressWarnings("unused") String message) { + /* NOOP */} + + @Override + public void addCookie(Cookie cookie) { + /* NOOP */} + + @Override + public void addDateHeader(String name, long value) { + /* NOOP */} + + @Override + public void addIntHeader(String name, int value) { + /* NOOP */} + + @Override + public boolean containsHeader(String name) { + return false; + } + + @Override + public String encodeRedirectURL(String url) { + return null; + } + + @Override + public String encodeURL(String url) { + return null; + } + + /** + * @throws IOException Never happens + */ + public void sendAcknowledgement() throws IOException { + /* NOOP */} + + @Override + public void sendError(int status) throws IOException { + /* NOOP */} + + @Override + public void sendError(int status, String message) throws IOException { + // NOOP + } + + @Override + public void sendRedirect(String location) throws IOException { + /* NOOP */} + + @Override + public void setDateHeader(String name, long value) { + /* NOOP */} + + @Override + public void setIntHeader(String name, int value) { + /* NOOP */} + + @Override + public void setContentLengthLong(long length) { + /* NOOP */} + + @Override + public void setTrailerFields(Supplier> supplier) { + /* NOOP */ } + + @Override + public Supplier> getTrailerFields() { + return null; + } +} diff --git a/test/org/apache/catalina/ha/context/TestReplicatedContext.java b/test/org/apache/catalina/ha/context/TestReplicatedContext.java new file mode 100644 index 0000000..3230df0 --- /dev/null +++ b/test/org/apache/catalina/ha/context/TestReplicatedContext.java @@ -0,0 +1,73 @@ +/* + * 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.catalina.ha.context; + +import java.io.File; +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestReplicatedContext extends TomcatBaseTest { + + @Test + public void testBug57425() throws LifecycleException, IOException { + Tomcat tomcat = getTomcatInstance(); + Host host = tomcat.getHost(); + if (host instanceof StandardHost) { + ((StandardHost) host).setContextClass(ReplicatedContext.class.getName()); + } + + File root = new File("test/webapp"); + Context context = tomcat.addWebapp(host, "", root.getAbsolutePath()); + + Tomcat.addServlet(context, "test", new AccessContextServlet()); + context.addServletMappingDecoded("/access", "test"); + + tomcat.start(); + + ByteChunk result = getUrl("http://localhost:" + getPort() + "/access"); + + Assert.assertEquals("OK", result.toString()); + + } + + private static class AccessContextServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + getServletContext().setAttribute("NULL", null); + resp.getWriter().print("OK"); + } + } +} diff --git a/test/org/apache/catalina/ha/session/TestDeltaRequest.java b/test/org/apache/catalina/ha/session/TestDeltaRequest.java new file mode 100644 index 0000000..bc795af --- /dev/null +++ b/test/org/apache/catalina/ha/session/TestDeltaRequest.java @@ -0,0 +1,73 @@ +/* + * 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.catalina.ha.session; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Manager; +import org.apache.catalina.session.StandardManager; +import org.apache.tomcat.unittest.TesterContext; + +public class TestDeltaRequest { + + /* + * Mostly interested in whether the attributes transfer correctly as the AttributeInfo class does not have a public + * no-arg constructor. This shouldn't be necessary for a package private class (CheckStyle flags the use of a public + * modifier as redundant) but SpotBugs complains as the class implements Externalizable. The purpose of this test is + * to confirm that the SpotBugs warning is a false positive. + */ + @Test + public void testSerialization() throws Exception { + // Create original request + DeltaRequest original = new DeltaRequest(); + original.setSessionId("1234"); + original.setAttribute("A", "One"); + original.setAttribute("B", "Two"); + + // Seralize original request + byte[] bytes; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(original); + bytes = baos.toByteArray(); + } + + // Deserialize original request + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais); + DeltaRequest copyRequest = (DeltaRequest) ois.readObject(); + + // Apply DeltaRequest so we can test that the attributes transferred correctly + DeltaSession copySession = new DeltaSession(); + Manager manager = new StandardManager(); + manager.setContext(new TesterContext()); + copySession.setManager(manager); + copySession.setId("1234", false); + copySession.setValid(true); + copyRequest.execute(copySession, false); + + // Test for the attributes + Assert.assertEquals("One", copySession.getAttribute("A")); + Assert.assertEquals("Two", copySession.getAttribute("B")); + } +} diff --git a/test/org/apache/catalina/loader/EchoTag.java b/test/org/apache/catalina/loader/EchoTag.java new file mode 100644 index 0000000..2667511 --- /dev/null +++ b/test/org/apache/catalina/loader/EchoTag.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.loader; + +import java.io.IOException; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.TagSupport; + +public class EchoTag extends TagSupport { + private static final long serialVersionUID = 1L; + + private String echo = null; + + public void setEcho(String echo) { + this.echo = echo; + } + + public String getEcho() { + return echo; + } + + @Override + public int doStartTag() throws JspException { + try { + pageContext.getOut().print("

    " + echo + "

    "); + } catch (IOException e) { + throw new JspException(e); + } + return super.doStartTag(); + } +} diff --git a/test/org/apache/catalina/loader/MyAnnotatedServlet.java b/test/org/apache/catalina/loader/MyAnnotatedServlet.java new file mode 100644 index 0000000..68e472a --- /dev/null +++ b/test/org/apache/catalina/loader/MyAnnotatedServlet.java @@ -0,0 +1,40 @@ +/* + * 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.catalina.loader; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet(value = "/annotatedServlet") +public class MyAnnotatedServlet extends HttpServlet { + + static final String MESSAGE = "This is generated by an annotated servlet"; + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("test/plain"); + resp.getWriter().println(MESSAGE); + } + +} diff --git a/test/org/apache/catalina/loader/TestVirtualContext.java b/test/org/apache/catalina/loader/TestVirtualContext.java new file mode 100644 index 0000000..a377d74 --- /dev/null +++ b/test/org/apache/catalina/loader/TestVirtualContext.java @@ -0,0 +1,338 @@ +/* + * 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.catalina.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.core.JreMemoryLeakPreventionListener; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.util.IOTools; +import org.apache.catalina.webresources.StandardRoot; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.scan.StandardJarScanner; + +public class TestVirtualContext extends TomcatBaseTest { + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + // BZ 49218: The test fails if JreMemoryLeakPreventionListener is not + // present. The listener affects the JVM, and thus not only the current, + // but also the subsequent tests that are run in the same JVM. So it is + // fair to add it in every test. + tomcat.getServer().addLifecycleListener( + new JreMemoryLeakPreventionListener()); + } + + @Test + public void testVirtualClassLoader() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-virtual-webapp/src/main/webapp-a"); + // app dir is relative to server home + StandardContext ctx = (StandardContext) tomcat.addWebapp(null, "/test", + appDir.getAbsolutePath()); + + ctx.setResources(new StandardRoot(ctx)); + File f1 = new File("test/webapp-virtual-webapp/target/classes"); + File f2 = new File("test/webapp-virtual-library/target/WEB-INF"); + File f3 = new File( + "test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/classes"); + File f4 = new File( + "test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes"); + File f5 = new File("test/webapp-virtual-webapp/src/main/misc"); + File f6 = new File("test/webapp-virtual-webapp/src/main/webapp-b"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/classes", + f1.getAbsolutePath(), null, "/"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF", + f2.getAbsolutePath(), null, "/"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/classes", + f3.getAbsolutePath(), null, "/"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/classes", + f4.getAbsolutePath(), null, "/"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/other", + f5.getAbsolutePath(), null, "/"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/", + f6.getAbsolutePath(), null, "/"); + + StandardJarScanner jarScanner = new StandardJarScanner(); + jarScanner.setScanAllDirectories(true); + ctx.setJarScanner(jarScanner); + ctx.setAddWebinfClassesResources(true); + + tomcat.start(); + + assertPageContains("/test/classpathGetResourceAsStream.jsp?path=nonexistent", + "resourceAInWebInfClasses=true", 404); + + assertPageContains( + "/test/classpathGetResourceAsStream.jsp?path=rsrc/resourceA.properties", + "resourceAInWebInfClasses=true"); + assertPageContains( + "/test/classpathGetResourceUrlThenGetStream.jsp?path=rsrc/resourceA.properties", + "resourceAInWebInfClasses=true"); + + assertPageContains( + "/test/classpathGetResourceAsStream.jsp?path=rsrc/resourceB.properties", + "resourceBInTargetClasses=true"); + assertPageContains( + "/test/classpathGetResourceUrlThenGetStream.jsp?path=rsrc/resourceB.properties", + "resourceBInTargetClasses=true"); + + assertPageContains( + "/test/classpathGetResourceAsStream.jsp?path=rsrc/resourceC.properties", + "resourceCInDependentLibraryTargetClasses=true"); + assertPageContains( + "/test/classpathGetResourceUrlThenGetStream.jsp?path=rsrc/resourceC.properties", + "resourceCInDependentLibraryTargetClasses=true"); + + assertPageContains( + "/test/classpathGetResourceAsStream.jsp?path=rsrc/resourceD.properties", + "resourceDInPackagedJarInWebInfLib=true"); + assertPageContains( + "/test/classpathGetResourceUrlThenGetStream.jsp?path=rsrc/resourceD.properties", + "resourceDInPackagedJarInWebInfLib=true"); + + assertPageContains( + "/test/classpathGetResourceAsStream.jsp?path=rsrc/resourceG.properties", + "resourceGInWebInfClasses=true"); + assertPageContains( + "/test/classpathGetResourceUrlThenGetStream.jsp?path=rsrc/resourceG.properties", + "resourceGInWebInfClasses=true"); + + // test listing all possible paths for a classpath resource + String allUrls = + getUrl( + "http://localhost:" + getPort() + + "/test/classpathGetResources.jsp?path=rsrc/").toString(); + Assert.assertTrue( + allUrls, + allUrls.indexOf("/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/classes/rsrc") > 0); + Assert.assertTrue( + allUrls, + allUrls.indexOf("/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc") > 0); + Assert.assertTrue( + allUrls, + allUrls.indexOf("/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/lib/rsrc.jar!/rsrc") > 0); + Assert.assertTrue( + allUrls, + allUrls.indexOf("/test/webapp-virtual-webapp/target/classes/rsrc") > 0); + Assert.assertTrue( + allUrls, + allUrls.indexOf("/test/webapp-virtual-library/target/WEB-INF/classes/rsrc") > 0); + + // check that there's no duplicate in the URLs + String[] allUrlsArray = allUrls.split("\\s+"); + Assert.assertEquals(new HashSet<>(Arrays.asList(allUrlsArray)).size(), + allUrlsArray.length); + + String allRsrsc2ClasspathUrls = + getUrl( + "http://localhost:" + getPort() + + "/test/classpathGetResources.jsp?path=rsrc-2/").toString(); + Assert.assertTrue( + allRsrsc2ClasspathUrls, + allRsrsc2ClasspathUrls.indexOf("/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc-2") > 0); + + // tests context.getRealPath + + // the following fails because getRealPath always return a non-null path + // even if there's no such resource + // assertPageContains("/test/contextGetRealPath.jsp?path=nonexistent", + // "resourceAInWebInfClasses=true", 404); + + // Real paths depend on the OS and this test has to work on all + // platforms so use File to convert the path to a platform specific form + File f = new File( + "test/webapp-virtual-webapp/src/main/webapp-a/rsrc/resourceF.properties"); + assertPageContains( + "/test/contextGetRealPath.jsp?path=/rsrc/resourceF.properties", + f.getPath()); + + // tests context.getResource then the content + + assertPageContains("/test/contextGetResource.jsp?path=/nonexistent", + "resourceAInWebInfClasses=true", 404); + assertPageContains( + "/test/contextGetResource.jsp?path=/WEB-INF/classes/rsrc/resourceA.properties", + "resourceAInWebInfClasses=true"); + assertPageContains( + "/test/contextGetResource.jsp?path=/WEB-INF/classes/rsrc/resourceG.properties", + "resourceGInWebInfClasses=true"); + assertPageContains( + "/test/contextGetResource.jsp?path=/rsrc/resourceE.properties", + "resourceEInDependentLibraryTargetClasses=true"); + assertPageContains( + "/test/contextGetResource.jsp?path=/other/resourceI.properties", + "resourceIInWebapp=true"); + assertPageContains( + "/test/contextGetResource.jsp?path=/rsrc-2/resourceJ.properties", + "resourceJInWebapp=true"); + + String allRsrcPaths = + getUrl( + "http://localhost:" + getPort() + + "/test/contextGetResourcePaths.jsp?path=/rsrc/").toString(); + Assert.assertTrue( + allRsrcPaths, + allRsrcPaths.indexOf("/rsrc/resourceF.properties") > 0); + Assert.assertTrue( + allRsrcPaths, + allRsrcPaths.indexOf("/rsrc/resourceE.properties") > 0); + Assert.assertTrue( + allRsrcPaths, + allRsrcPaths.indexOf("/rsrc/resourceH.properties") > 0); + + // check that there's no duplicate in the URLs + String[] allRsrcPathsArray = allRsrcPaths.split("\\s+"); + Assert.assertEquals(new HashSet<>(Arrays.asList(allRsrcPathsArray)).size(), + allRsrcPathsArray.length); + + String allRsrc2Paths = + getUrl( + "http://localhost:" + getPort() + + "/test/contextGetResourcePaths.jsp?path=/rsrc-2/").toString(); + Assert.assertTrue( + allRsrc2Paths, + allRsrc2Paths.indexOf("/rsrc-2/resourceJ.properties") > 0); + + assertPageContains( + "/test/testTlds.jsp", + "worldA"); + assertPageContains( + "/test/testTlds.jsp", + "worldB"); + assertPageContains( + "/test/testTlds.jsp", + "worldC"); + assertPageContains( + "/test/testTlds.jsp", + "worldD"); + } + + @Test + public void testAdditionalWebInfClassesPaths() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-virtual-webapp/src/main/webapp-a"); + // app dir is relative to server home + StandardContext ctx = (StandardContext) tomcat.addWebapp(null, "/test", + appDir.getAbsolutePath()); + File tempFile = File.createTempFile("virtualWebInfClasses", null); + + File additionWebInfClasses = new File(tempFile.getAbsolutePath() + ".dir"); + Assert.assertTrue(additionWebInfClasses.mkdirs()); + File targetPackageForAnnotatedClass = + new File(additionWebInfClasses, + MyAnnotatedServlet.class.getPackage().getName().replace('.', '/')); + Assert.assertTrue(targetPackageForAnnotatedClass.mkdirs()); + try (InputStream annotatedServletClassInputStream = this.getClass().getResourceAsStream( + MyAnnotatedServlet.class.getSimpleName() + ".class"); + FileOutputStream annotatedServletClassOutputStream = new FileOutputStream(new File( + targetPackageForAnnotatedClass, MyAnnotatedServlet.class.getSimpleName() + + ".class"))) { + IOTools.flow(annotatedServletClassInputStream, annotatedServletClassOutputStream); + } + + ctx.setResources(new StandardRoot(ctx)); + File f1 = new File("test/webapp-virtual-webapp/target/classes"); + File f2 = new File("test/webapp-virtual-library/target/WEB-INF/classes"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/classes", + f1.getAbsolutePath(), null, "/"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/classes", + f2.getAbsolutePath(), null, "/"); + + tomcat.start(); + // first test that without the setting on StandardContext the annotated + // servlet is not detected + assertPageContains("/test/annotatedServlet", MyAnnotatedServlet.MESSAGE, 404); + + tomcat.stop(); + + // then test that if we configure StandardContext with the additional + // path, the servlet is detected + ctx.setResources(new StandardRoot(ctx)); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/classes", + f1.getAbsolutePath(), null, "/"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/classes", + f2.getAbsolutePath(), null, "/"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/classes", + additionWebInfClasses.getAbsolutePath(), null, "/"); + + tomcat.start(); + assertPageContains("/test/annotatedServlet", MyAnnotatedServlet.MESSAGE); + tomcat.stop(); + ExpandWar.delete(additionWebInfClasses); + Assert.assertTrue("Failed to clean up [" + tempFile + "]", tempFile.delete()); + } + + private void assertPageContains(String pageUrl, String expectedBody) + throws IOException { + + assertPageContains(pageUrl, expectedBody, 200); + } + + private void assertPageContains(String pageUrl, String expectedBody, + int expectedStatus) throws IOException { + ByteChunk res = new ByteChunk(); + // Note: With a read timeout of 3s the ASF CI buildbot was consistently + // seeing failures with this test. The failures were due to the + // JSP initialisation taking longer than the read timeout. The + // root cause of this is the frequent poor IO performance of the + // VM running the buildbot instance. Increasing this to 10s should + // avoid these failures. + // With the additional of Travis CI, failures continued to + // observed with a 10s timeout. It was therefore increased to 20s + // and then 30s. + int sc = getUrl("http://localhost:" + getPort() + pageUrl, res, 30000, + null, null); + + Assert.assertEquals(expectedStatus, sc); + + if (expectedStatus == 200) { + String result = res.toString(); + Assert.assertTrue(result, result.contains(expectedBody)); + } + } +} diff --git a/test/org/apache/catalina/loader/TestVirtualWebappLoader.java b/test/org/apache/catalina/loader/TestVirtualWebappLoader.java new file mode 100644 index 0000000..8a158a9 --- /dev/null +++ b/test/org/apache/catalina/loader/TestVirtualWebappLoader.java @@ -0,0 +1,108 @@ +/* + * 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.catalina.loader; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.webresources.StandardRoot; + +public class TestVirtualWebappLoader extends TomcatBaseTest { + + @Test + public void testModified() throws Exception { + WebappLoader loader = new WebappLoader(); + Assert.assertNull(loader.getClassLoader()); + Assert.assertFalse(loader.modified()); + } + + @Test + public void testLoaderInstance() throws Exception { + WebappLoader loader = new WebappLoader(); + Assert.assertNull(loader.getClassLoader()); + WebappClassLoader cl = new WebappClassLoader(); + loader.setLoaderInstance(cl); + Assert.assertSame(cl, loader.getClassLoader()); + Assert.assertEquals(WebappClassLoader.class.getName(), loader.getLoaderClass()); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + StandardContext ctx = (StandardContext) tomcat.addContext("", + appDir.getAbsolutePath()); + + loader.setContext(ctx); + ctx.setLoader(loader); + + ctx.setResources(new StandardRoot(ctx)); + ctx.resourcesStart(); + + loader.start(); + Assert.assertSame(cl, loader.getClassLoader()); + Assert.assertEquals(WebappClassLoader.class.getName(), loader.getLoaderClass()); + loader.stop(); + Assert.assertNull(loader.getClassLoader()); + Assert.assertEquals(WebappClassLoader.class.getName(), loader.getLoaderClass()); + } + + @Test + public void testStartInternal() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + StandardContext ctx = (StandardContext) tomcat.addContext("", + appDir.getAbsolutePath()); + + + WebappLoader loader = new WebappLoader(); + + loader.setContext(ctx); + ctx.setLoader(loader); + + ctx.setResources(new StandardRoot(ctx)); + ctx.resourcesStart(); + + File f1 = new File("test/webapp-fragments/WEB-INF/lib"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/lib", + f1.getAbsolutePath(), null, "/"); + + loader.start(); + String[] repos = loader.getLoaderRepositories(); + Assert.assertEquals(4,repos.length); + loader.stop(); + + repos = loader.getLoaderRepositories(); + Assert.assertEquals(0, repos.length); + + // no leak + loader.start(); + repos = loader.getLoaderRepositories(); + Assert.assertEquals(4,repos.length); + + // clear loader + ctx.setLoader(null); + // see tearDown()! + tomcat.start(); + } +} diff --git a/test/org/apache/catalina/loader/TestWebappClassLoader.java b/test/org/apache/catalina/loader/TestWebappClassLoader.java new file mode 100644 index 0000000..11b53ab --- /dev/null +++ b/test/org/apache/catalina/loader/TestWebappClassLoader.java @@ -0,0 +1,175 @@ +/* + * 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.catalina.loader; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestWebappClassLoader extends TomcatBaseTest { + + @Test + public void testGetURLs() throws Exception { + File f = new File("test/webresources/war-url-connection.war"); + + String[] expected = new String[2]; + String warUrl = f.toURI().toURL().toExternalForm(); + expected[0] = "war:" + warUrl + "*/WEB-INF/classes/"; + expected[1] = "war:" + warUrl + "*/WEB-INF/lib/test.jar"; + + Tomcat tomcat = getTomcatInstance(); + + StandardContext ctx = + (StandardContext)tomcat.addContext("", f.getAbsolutePath()); + + tomcat.start(); + + ClassLoader cl = ctx.getLoader().getClassLoader(); + + try (URLClassLoader ucl = (URLClassLoader) cl) { + URL[] urls = ucl.getURLs(); + Assert.assertEquals(expected.length, urls.length); + String[] actual = new String[urls.length]; + for (int i = 0; i < urls.length; i++) { + actual[i] = urls[i].toExternalForm(); + } + Assert.assertArrayEquals(expected, actual); + } + } + + @Test + public void testFilter() throws IOException { + + String[] classSuffixes = new String[]{ + "", + "some.package.Example" + }; + + String[] resourceSuffixes = new String[]{ + "", + "some/path/test.properties", + "some/path/test" + }; + + String[] prefixes = new String[]{ + "", + "resources", + "WEB-INF", + "WEB-INF.classes", + "WEB-INF.lib", + "org", + "org.apache", + "jakarta", + "javax", + "com.mycorp" + }; + + String[] prefixesPermit = new String[]{ + "org.apache.tomcat.jdbc", + "jakarta.servlet.jsp.jstl", + }; + + String[] prefixesDeny = new String[]{ + "org.apache.catalina", + "org.apache.coyote", + "org.apache.el", + "org.apache.jasper", + "org.apache.juli", + "org.apache.naming", + "org.apache.tomcat", + "jakarta.annotation", + "jakarta.el", + "jakarta.servlet", + "jakarta.websocket", + "jakarta.security.auth.message" + }; + + try (WebappClassLoader loader = new WebappClassLoader()) { + String name; + + for (String prefix : prefixes) { + for (String suffix : classSuffixes) { + name = prefix + "." + suffix; + Assert.assertTrue("Class '" + name + "' failed permit filter", + !loader.filter(name, true)); + if (prefix.equals("")) { + name = suffix; + Assert.assertTrue("Class '" + name + "' failed permit filter", + !loader.filter(name, true)); + } + if (suffix.equals("")) { + name = prefix; + Assert.assertTrue("Class '" + name + "' failed permit filter", + !loader.filter(name, true)); + } + } + prefix = prefix.replace('.', '/'); + for (String suffix : resourceSuffixes) { + name = prefix + "/" + suffix; + Assert.assertTrue("Resource '" + name + "' failed permit filter", + !loader.filter(name, false)); + if (prefix.equals("")) { + name = suffix; + Assert.assertTrue("Resource '" + name + "' failed permit filter", + !loader.filter(name, false)); + } + if (suffix.equals("")) { + name = prefix; + Assert.assertTrue("Resource '" + name + "' failed permit filter", + !loader.filter(name, false)); + } + } + } + + for (String prefix : prefixesPermit) { + for (String suffix : classSuffixes) { + name = prefix + "." + suffix; + Assert.assertTrue("Class '" + name + "' failed permit filter", + !loader.filter(name, true)); + } + prefix = prefix.replace('.', '/'); + for (String suffix : resourceSuffixes) { + name = prefix + "/" + suffix; + Assert.assertTrue("Resource '" + name + "' failed permit filter", + !loader.filter(name, false)); + } + } + + for (String prefix : prefixesDeny) { + for (String suffix : classSuffixes) { + name = prefix + "." + suffix; + Assert.assertTrue("Class '" + name + "' failed deny filter", + loader.filter(name, true)); + } + prefix = prefix.replace('.', '/'); + for (String suffix : resourceSuffixes) { + name = prefix + "/" + suffix; + Assert.assertTrue("Resource '" + name + "' failed deny filter", + loader.filter(name, false)); + } + } + } + } +} diff --git a/test/org/apache/catalina/loader/TestWebappClassLoaderExecutorMemoryLeak.java b/test/org/apache/catalina/loader/TestWebappClassLoaderExecutorMemoryLeak.java new file mode 100644 index 0000000..fc01786 --- /dev/null +++ b/test/org/apache/catalina/loader/TestWebappClassLoaderExecutorMemoryLeak.java @@ -0,0 +1,130 @@ +/* + * 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.catalina.loader; + +import java.io.IOException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestWebappClassLoaderExecutorMemoryLeak extends TomcatBaseTest { + + @Test + public void testTimerThreadLeak() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + if (ctx instanceof StandardContext) { + ((StandardContext) ctx).setClearReferencesStopThreads(true); + } + + ExecutorServlet executorServlet = new ExecutorServlet(); + Tomcat.addServlet(ctx, "taskServlet", executorServlet); + ctx.addServletMappingDecoded("/", "taskServlet"); + + tomcat.start(); + + // This will trigger the timer & thread creation + getUrl("http://localhost:" + getPort() + "/"); + + // Stop the context + ctx.stop(); + + // Should be shutdown once the stop() method above exists + Assert.assertTrue(executorServlet.tpe.isShutdown()); + + // The time taken to shutdown the executor can vary between systems. Try + // to avoid false test failures due to timing issues. Give the executor + // up to 10 seconds to close down. + int count = 0; + while (count < 100 && !executorServlet.tpe.isTerminated()) { + count++; + Thread.sleep(100); + } + + // If the executor has not terminated, there is a thread/memory leak + Assert.assertTrue(executorServlet.tpe.isTerminated()); + } + + static class ExecutorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + int nTasks = 5; + long n = 1000L; + int tpSize = 10; + + public transient volatile ThreadPoolExecutor tpe; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.getWriter().println( + "The current thread served " + this + " servlet"); + tpe = new ThreadPoolExecutor(tpSize, tpSize, 50000L, + TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); + + Task[] tasks = new Task[nTasks]; + for (int i = 0; i < nTasks; i++) { + tasks[i] = new Task("Task " + i); + tpe.execute(tasks[i]); + } + resp.getWriter().println("Started " + nTasks + + " never ending tasks using the ThreadPoolExecutor"); + resp.getWriter().flush(); + } + + static class Task implements Runnable { + + String _id; + + Task(String id) { + this._id = id; + } + + @Override + public void run() { + Thread currentThread = Thread.currentThread(); + try { + while (!currentThread.isInterrupted()) { + Thread.sleep(20000); + System.out.println( + currentThread.getClass() + " [" + currentThread.getName() + "] executing " + this._id); + } + } catch (InterruptedException e) { + System.out.println(currentThread.getClass() + " [" + currentThread.getName() + "] EXITING"); + } + } + } + } +} diff --git a/test/org/apache/catalina/loader/TestWebappClassLoaderMemoryLeak.java b/test/org/apache/catalina/loader/TestWebappClassLoaderMemoryLeak.java new file mode 100644 index 0000000..e9ccbd4 --- /dev/null +++ b/test/org/apache/catalina/loader/TestWebappClassLoaderMemoryLeak.java @@ -0,0 +1,121 @@ +/* + * 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.catalina.loader; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestWebappClassLoaderMemoryLeak extends TomcatBaseTest { + + @Test + public void testTimerThreadLeak() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + if (ctx instanceof StandardContext) { + ((StandardContext) ctx).setClearReferencesStopTimerThreads(true); + } + + Tomcat.addServlet(ctx, "taskServlet", new TaskServlet()); + ctx.addServletMappingDecoded("/", "taskServlet"); + + tomcat.start(); + + // This will trigger the timer & thread creation + getUrl("http://localhost:" + getPort() + "/"); + + // Stop the context + ctx.stop(); + + Thread[] threads = getThreads(); + for (Thread thread : threads) { + if (thread != null && thread.isAlive() && + TaskServlet.TIMER_THREAD_NAME.equals(thread.getName())) { + thread.join(5000); + if (thread.isAlive()) { + Assert.fail("Timer thread still running"); + } + } + } + } + + /* + * Get the set of current threads as an array. + * Copied from WebappClassLoaderBase + */ + private Thread[] getThreads() { + // Get the current thread group + ThreadGroup tg = Thread.currentThread( ).getThreadGroup( ); + // Find the root thread group + while (tg.getParent() != null) { + tg = tg.getParent(); + } + + int threadCountGuess = tg.activeCount() + 50; + Thread[] threads = new Thread[threadCountGuess]; + int threadCountActual = tg.enumerate(threads); + // Make sure we don't miss any threads + while (threadCountActual == threadCountGuess) { + threadCountGuess *=2; + threads = new Thread[threadCountGuess]; + // Note tg.enumerate(Thread[]) silently ignores any threads that + // can't fit into the array + threadCountActual = tg.enumerate(threads); + } + + return threads; + } + + private static final class TaskServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String TIMER_THREAD_NAME = "leaked-thread"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + Timer timer = new Timer(TIMER_THREAD_NAME); + timer.schedule(new LocalTask(), 0, 10000); + } + + } + + private static final class LocalTask extends TimerTask { + + @Override + public void run() { + // Doesn't actually need to do anything. + } + + } +} diff --git a/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java b/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java new file mode 100644 index 0000000..1912264 --- /dev/null +++ b/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java @@ -0,0 +1,411 @@ +/* + * 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.catalina.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Method; +import java.security.ProtectionDomain; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestWebappClassLoaderWeaving extends TomcatBaseTest { + + private static final String PACKAGE_PREFIX = "org/apache/catalina/loader"; + + private static String WEBAPP_DOC_BASE; + + @BeforeClass + public static void setUpClass() throws Exception { + + String webappDocBase = "test/tmpTestWebappClassLoaderWeaving"; + File webappDocBaseFile = new File(webappDocBase); + WEBAPP_DOC_BASE = webappDocBaseFile.getCanonicalPath(); + File classes = new File(webappDocBaseFile, "/WEB-INF/classes/" + PACKAGE_PREFIX); + Assert.assertTrue("Failed to create [" + classes + "]", classes.mkdirs()); + + copyResource(PACKAGE_PREFIX + "/TesterNeverWeavedClass.class", + new File(classes, "TesterNeverWeavedClass.class")); + copyResource(PACKAGE_PREFIX + "/TesterUnweavedClass.class", + new File(classes, "TesterUnweavedClass.class")); + + } + + @AfterClass + public static void tearDownClass() throws Exception { + ExpandWar.delete(new File(WEBAPP_DOC_BASE)); + } + + + private Tomcat tomcat; + private Context context; + private WebappClassLoaderBase loader; + + @Before + @Override + public void setUp() throws Exception { + + super.setUp(); + + this.tomcat = getTomcatInstance(); + this.context = this.tomcat.addContext("/weaving", WEBAPP_DOC_BASE); + this.tomcat.start(); + + ClassLoader loader = this.context.getLoader().getClassLoader(); + Assert.assertNotNull("The class loader should not be null.", loader); + Assert.assertTrue("The class loader is not correct.", loader instanceof WebappClassLoaderBase); + + this.loader = (WebappClassLoaderBase) loader; + + } + + @After + @Override + public void tearDown() throws Exception { + + try { + this.loader = null; + + this.context.stop(); + this.tomcat.getHost().removeChild(this.context); + this.context = null; + } finally { + super.tearDown(); + } + + } + + @Test + public void testNoWeaving() throws Exception { + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testAddingNullTransformerThrowsException() throws Exception { + + try { + this.loader.addTransformer(null); + Assert.fail("Expected exception IllegalArgumentException, got no exception."); + } catch (IllegalArgumentException ignore) { + // good + } + + // class loading should still work, no weaving + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testAddedTransformerInstrumentsClass1() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testAddedTransformerInstrumentsClass2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testTransformersExecuteInOrderAdded1() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testTransformersExecuteInOrderAdded2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments1() throws Exception { + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_1); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_2); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments3() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_1); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testCopiedClassLoaderExcludesResourcesAndTransformers() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + Assert.assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + Assert.assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + WebappClassLoaderBase copiedLoader = (WebappClassLoaderBase) this.loader.copyWithoutTransformers(); + + result = invokeDoMethodOnClass(copiedLoader, "TesterNeverWeavedClass"); + Assert.assertEquals("The third result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(copiedLoader, "TesterUnweavedClass"); + Assert.assertEquals("The fourth result is not correct.", "Hello, Unweaved World!", result); + + Assert.assertEquals("getClearReferencesHttpClientKeepAliveThread did not match.", + Boolean.valueOf(this.loader.getClearReferencesHttpClientKeepAliveThread()), + Boolean.valueOf(copiedLoader.getClearReferencesHttpClientKeepAliveThread())); + Assert.assertEquals("getClearReferencesLogFactoryRelease did not match.", + Boolean.valueOf(this.loader.getClearReferencesLogFactoryRelease()), + Boolean.valueOf(copiedLoader.getClearReferencesLogFactoryRelease())); + Assert.assertEquals("getClearReferencesStopThreads did not match.", + Boolean.valueOf(this.loader.getClearReferencesStopThreads()), + Boolean.valueOf(copiedLoader.getClearReferencesStopThreads())); + Assert.assertEquals("getClearReferencesStopTimerThreads did not match.", + Boolean.valueOf(this.loader.getClearReferencesStopTimerThreads()), + Boolean.valueOf(copiedLoader.getClearReferencesStopTimerThreads())); + Assert.assertEquals("getContextName did not match.", this.loader.getContextName(), + copiedLoader.getContextName()); + Assert.assertEquals("getDelegate did not match.", + Boolean.valueOf(this.loader.getDelegate()), + Boolean.valueOf(copiedLoader.getDelegate())); + Assert.assertEquals("getURLs did not match.", this.loader.getURLs().length, + copiedLoader.getURLs().length); + Assert.assertSame("getParent did not match.", this.loader.getParent(), copiedLoader.getParent()); + + } + + private static void copyResource(String name, File file) throws Exception { + ClassLoader cl = TestWebappClassLoaderWeaving.class.getClassLoader(); + try (InputStream is = cl.getResourceAsStream(name)) { + if (is == null) { + throw new IOException("Resource " + name + " not found on classpath."); + } + + try (FileOutputStream os = new FileOutputStream(file)) { + for (int b = is.read(); b >= 0; b = is.read()) { + os.write(b); + } + } + } + } + + private static String invokeDoMethodOnClass(WebappClassLoaderBase loader, String className) + throws Exception { + + Class c = loader.findClass("org.apache.catalina.loader." + className); + Assert.assertNotNull("The loaded class should not be null.", c); + + Method m = c.getMethod("doMethod"); + + Object o = c.getConstructor().newInstance(); + return (String) m.invoke(o); + + } + + private static class ReplacementTransformer implements ClassFileTransformer { + + private static final String CLASS_TO_WEAVE = PACKAGE_PREFIX + "/TesterUnweavedClass"; + + private final byte[] replacement; + + ReplacementTransformer(byte[] replacement) { + this.replacement = replacement; + } + + @Override + public byte[] transform(ClassLoader loader, String className, Class x, + ProtectionDomain y, byte[] b) { + + if (CLASS_TO_WEAVE.equals(className)) { + return this.replacement; + } + + return null; + + } + + } + + /** + * Compiled version of org.apache.catalina.loader.TesterUnweavedClass, except that + * the doMethod method returns "Hello, Weaver #1!". Compiled with Oracle Java 1.6.0_51. + */ + private static final byte[] WEAVED_REPLACEMENT_1 = new byte[] { + -54, -2, -70, -66, 0, 0, 0, 50, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 1, + 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, + 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 8, 100, + 111, 77, 101, 116, 104, 111, 100, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, + 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, + 105, 108, 101, 1, 0, 24, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, 101, + 100, 67, 108, 97, 115, 115, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 17, 72, 101, + 108, 108, 111, 44, 32, 87, 101, 97, 118, 101, 114, 32, 35, 49, 33, 1, 0, 46, 111, 114, + 103, 47, 97, 112, 97, 99, 104, 101, 47, 99, 97, 116, 97, 108, 105, 110, 97, 47, 108, + 111, 97, 100, 101, 114, 47, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, + 101, 100, 67, 108, 97, 115, 115, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, + 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, + 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, + 0, 6, 0, 1, 0, 0, 0, 19, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, + 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 22, 0, 1, 0, 11, 0, 0, 0, + 2, 0, 12 + }; + + /** + * Compiled version of org.apache.catalina.loader.TesterUnweavedClass, except that + * the doMethod method returns "Hello, Weaver #2!". Compiled with Oracle Java 1.6.0_51. + */ + private static final byte[] WEAVED_REPLACEMENT_2 = new byte[] { + -54, -2, -70, -66, 0, 0, 0, 50, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 1, + 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, + 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 8, 100, + 111, 77, 101, 116, 104, 111, 100, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, + 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, + 105, 108, 101, 1, 0, 24, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, 101, + 100, 67, 108, 97, 115, 115, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 17, 72, 101, + 108, 108, 111, 44, 32, 87, 101, 97, 118, 101, 114, 32, 35, 50, 33, 1, 0, 46, 111, 114, + 103, 47, 97, 112, 97, 99, 104, 101, 47, 99, 97, 116, 97, 108, 105, 110, 97, 47, 108, + 111, 97, 100, 101, 114, 47, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, + 101, 100, 67, 108, 97, 115, 115, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, + 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, + 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, + 0, 6, 0, 1, 0, 0, 0, 19, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, + 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 22, 0, 1, 0, 11, 0, 0, 0, + 2, 0, 12 + }; + + /* + * The WEAVED_REPLACEMENT_1 and WEAVED_REPLACEMENT_2 field contents are generated using the + * following code. To regenerate them, alter the TesterUnweavedClass code as desired, recompile, + * and run this main method. + */ + public static void main(String... arguments) throws Exception { + ClassLoader cl = TestWebappClassLoaderWeaving.class.getClassLoader(); + try (InputStream input = cl.getResourceAsStream( + "org/apache/catalina/loader/TesterUnweavedClass.class")) { + + StringBuilder builder = new StringBuilder(); + builder.append(" "); + + System.out.println(" private static final byte[] WEAVED_REPLACEMENT_1 = new byte[] {"); + for (int i = 0, b = input.read(); b >= 0; i++, b = input.read()) { + String value = "" + ((byte)b); + if (builder.length() + value.length() > 97) { + builder.append(','); + System.out.println(builder.toString()); + builder = new StringBuilder(); + builder.append(" ").append(value); + } else { + if (i > 0) { + builder.append(", "); + } + builder.append(value); + } + } + System.out.println(builder.toString()); + } + System.out.println(" }"); + } +} diff --git a/test/org/apache/catalina/loader/TesterNeverWeavedClass.java b/test/org/apache/catalina/loader/TesterNeverWeavedClass.java new file mode 100644 index 0000000..1db74a5 --- /dev/null +++ b/test/org/apache/catalina/loader/TesterNeverWeavedClass.java @@ -0,0 +1,24 @@ +/* + * 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.catalina.loader; + +public class TesterNeverWeavedClass { + + public String doMethod() { + return "This will never be weaved."; + } +} diff --git a/test/org/apache/catalina/loader/TesterUnweavedClass.java b/test/org/apache/catalina/loader/TesterUnweavedClass.java new file mode 100644 index 0000000..9936eed --- /dev/null +++ b/test/org/apache/catalina/loader/TesterUnweavedClass.java @@ -0,0 +1,24 @@ +/* + * 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.catalina.loader; + +public class TesterUnweavedClass { + + public String doMethod() { + return "Hello, Unweaved World!"; + } +} diff --git a/test/org/apache/catalina/loader/TesterWebappClassLoaderThreadLocalMemoryLeak.java b/test/org/apache/catalina/loader/TesterWebappClassLoaderThreadLocalMemoryLeak.java new file mode 100644 index 0000000..c833e12 --- /dev/null +++ b/test/org/apache/catalina/loader/TesterWebappClassLoaderThreadLocalMemoryLeak.java @@ -0,0 +1,200 @@ +/* + * 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.catalina.loader; + +import java.io.InputStream; +import java.util.concurrent.Executor; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.core.JreMemoryLeakPreventionListener; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterLogValidationFilter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.threads.ThreadPoolExecutor; + +/* + * These unit tests are ignored by default as they are not reliable. They have + * been failing regularly on Gump for some time and have recently started to + * fail regularly on markt's laptop. + * + * The problem is that the ThreadLocal Maps are affected by GC. If GC occurs at + * the wrong point, the leaking ThreadLocal will be cleaned up and the test will + * fail. It is not possible to force the test to pass without effectively + * changing the nature of the test so it no longer tests detection of leaks via + * ThreadLocals. + * + * The test has been left in place since it will work reasonably reliably on + * most systems (just not all and particularly some of the ASF's CI systems) and + * still may be useful if a bug is reported in this area in the future. + */ +public class TesterWebappClassLoaderThreadLocalMemoryLeak extends TomcatBaseTest { + + @Test + public void testThreadLocalLeak1() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // Need to make sure we see a leak for the right reasons + tomcat.getServer().addLifecycleListener( + new JreMemoryLeakPreventionListener()); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "leakServlet1", + "org.apache.tomcat.unittest.TesterLeakingServlet1"); + ctx.addServletMappingDecoded("/leak1", "leakServlet1"); + + tomcat.start(); + + Executor executor = tomcat.getConnector().getProtocolHandler().getExecutor(); + ((ThreadPoolExecutor) executor).setThreadRenewalDelay(-1); + + // Configure logging filter to check leak message appears + TesterLogValidationFilter f = TesterLogValidationFilter.add(null, + "The web application [ROOT] created a ThreadLocal with key of", null, + "org.apache.catalina.loader.WebappClassLoaderBase"); + + // Need to force loading of all web application classes via the web + // application class loader + loadClass("TesterCounter", + (WebappClassLoaderBase) ctx.getLoader().getClassLoader()); + loadClass("TesterLeakingServlet1", + (WebappClassLoaderBase) ctx.getLoader().getClassLoader()); + + // This will trigger the ThreadLocal creation + int rc = getUrl("http://localhost:" + getPort() + "/leak1", + new ByteChunk(), null); + + // Make sure request is OK + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // Destroy the context + ctx.stop(); + tomcat.getHost().removeChild(ctx); + ctx = null; + + // Make sure we have a memory leak + String[] leaks = ((StandardHost) tomcat.getHost()) + .findReloadedContextMemoryLeaks(); + Assert.assertNotNull(leaks); + Assert.assertTrue(leaks.length > 0); + + // Make sure the message was logged + Assert.assertEquals(1, f.getMessageCount()); + } + + + @Test + public void testThreadLocalLeak2() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // Need to make sure we see a leak for the right reasons + tomcat.getServer().addLifecycleListener( + new JreMemoryLeakPreventionListener()); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "leakServlet2", + "org.apache.tomcat.unittest.TesterLeakingServlet2"); + ctx.addServletMappingDecoded("/leak2", "leakServlet2"); + + tomcat.start(); + + Executor executor = tomcat.getConnector().getProtocolHandler().getExecutor(); + ((ThreadPoolExecutor) executor).setThreadRenewalDelay(-1); + + // Configure logging filter to check leak message appears + TesterLogValidationFilter f = TesterLogValidationFilter.add(null, + "The web application [ROOT] created a ThreadLocal with key of", null, + "org.apache.catalina.loader.WebappClassLoaderBase"); + + // Need to force loading of all web application classes via the web + // application class loader + loadClass("TesterCounter", + (WebappClassLoaderBase) ctx.getLoader().getClassLoader()); + loadClass("TesterThreadScopedHolder", + (WebappClassLoaderBase) ctx.getLoader().getClassLoader()); + loadClass("TesterLeakingServlet2", + (WebappClassLoaderBase) ctx.getLoader().getClassLoader()); + + // This will trigger the ThreadLocal creation + int rc = getUrl("http://localhost:" + getPort() + "/leak2", + new ByteChunk(), null); + + // Make sure request is OK + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // Destroy the context + ctx.stop(); + tomcat.getHost().removeChild(ctx); + ctx = null; + + // Make sure we have a memory leak + String[] leaks = ((StandardHost) tomcat.getHost()) + .findReloadedContextMemoryLeaks(); + Assert.assertNotNull(leaks); + Assert.assertTrue(leaks.length > 0); + + // Make sure the message was logged + Assert.assertEquals(1, f.getMessageCount()); + } + + + /** + * Utility method to ensure that classes are loaded by the + * WebappClassLoader. We can't just create classes since they will be loaded + * by the current class loader rather than the WebappClassLoader. This would + * mean that no leak occurred making the test for a leak rather pointless + * So, we load the bytes via the current class loader but define the class + * with the WebappClassLoader. + * + * This method assumes that all classes are in the current package. + */ + private void loadClass(String name, WebappClassLoaderBase cl) throws Exception { + try (InputStream is = cl.getResourceAsStream( + "org/apache/tomcat/unittest/" + name + ".class")) { + // We know roughly how big the class will be (~ 1K) so allow 2k as a + // starting point + byte[] classBytes = new byte[2048]; + int offset = 0; + int read = is.read(classBytes, offset, classBytes.length-offset); + while (read > -1) { + offset += read; + if (offset == classBytes.length) { + // Buffer full - double size + byte[] tmp = new byte[classBytes.length * 2]; + System.arraycopy(classBytes, 0, tmp, 0, classBytes.length); + classBytes = tmp; + } + read = is.read(classBytes, offset, classBytes.length-offset); + } + Class lpClass = cl.doDefineClass( + "org.apache.tomcat.unittest." + name, classBytes, 0, + offset, cl.getClass().getProtectionDomain()); + // Make sure we can create an instance + lpClass.getConstructor().newInstance(); + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/manager/TestStatusTransformer.java b/test/org/apache/catalina/manager/TestStatusTransformer.java new file mode 100644 index 0000000..3cd772f --- /dev/null +++ b/test/org/apache/catalina/manager/TestStatusTransformer.java @@ -0,0 +1,81 @@ +/* + * 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.catalina.manager; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import static org.apache.catalina.startup.SimpleHttpClient.CRLF; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.json.JSONParser; + +public class TestStatusTransformer extends TomcatBaseTest { + + @Test + public void testJSON() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // Add default servlet to make some requests + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + ctxt.setPrivileged(true); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", + "org.apache.catalina.servlets.DefaultServlet"); + defaultServlet.addInitParameter("fileEncoding", "ISO-8859-1"); + ctxt.addServletMappingDecoded("/", "default"); + Tomcat.addServlet(ctxt, "status", "org.apache.catalina.manager.StatusManagerServlet"); + ctxt.addServletMappingDecoded("/status/*", "status"); + ctxt.addMimeMapping("html", "text/html"); + Context ctxt2 = tomcat.addContext("/test", null); + Tomcat.addServlet(ctxt2, "status", "org.apache.catalina.manager.StatusManagerServlet"); + ctxt.addServletMappingDecoded("/somepath/*", "status"); + tomcat.start(); + + SimpleHttpClient client = new SimpleHttpClient() { + @Override + public boolean isResponseBodyOK() { + return true; + } + }; + client.setPort(getPort()); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + CRLF }); + client.connect(); + client.processRequest(true); + + client.setRequest(new String[] { + "GET /status/all?JSON=true HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + CRLF }); + client.connect(); + client.processRequest(true); + String json = client.getResponseBody(); + System.out.println(json); + JSONParser parser = new JSONParser(json); + String result = parser.parse().toString(); + Assert.assertTrue(result.contains("name=localhost/")); + } + +} diff --git a/test/org/apache/catalina/mapper/TestMapper.java b/test/org/apache/catalina/mapper/TestMapper.java new file mode 100644 index 0000000..03386b1 --- /dev/null +++ b/test/org/apache/catalina/mapper/TestMapper.java @@ -0,0 +1,549 @@ +/* + * 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.catalina.mapper; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.core.StandardWrapper; +import org.apache.catalina.startup.LoggingBaseTest; +import org.apache.tomcat.util.buf.MessageBytes; + +public class TestMapper extends LoggingBaseTest { + + protected Mapper mapper; + + private HashMap hostMap = new HashMap<>(); + + private synchronized Host createHost(String name) { + Host host = hostMap.get(name); + if (host == null) { + host = new StandardHost(); + host.setName(name); + hostMap.put(name, host); + } + return host; + } + + private Context createContext(String name) { + Context context = new StandardContext(); + context.setName(name); + return context; + } + + private Wrapper createWrapper(String name) { + Wrapper wrapper = new StandardWrapper(); + wrapper.setName(name); + return wrapper; + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + mapper = new Mapper(); + + mapper.addHost("sjbjdvwsbvhrb", new String[0], createHost("blah1")); + mapper.addHost("sjbjdvwsbvhr/", new String[0], createHost("blah1")); + mapper.addHost("wekhfewuifweuibf", new String[0], createHost("blah2")); + mapper.addHost("ylwrehirkuewh", new String[0], createHost("blah3")); + mapper.addHost("iohgeoihro", new String[0], createHost("blah4")); + mapper.addHost("fwehoihoihwfeo", new String[0], createHost("blah5")); + mapper.addHost("owefojiwefoi", new String[0], createHost("blah6")); + mapper.addHost("iowejoiejfoiew", new String[0], createHost("blah7")); + mapper.addHost("ohewoihfewoih", new String[0], createHost("blah8")); + mapper.addHost("fewohfoweoih", new String[0], createHost("blah9")); + mapper.addHost("ttthtiuhwoih", new String[0], createHost("blah10")); + mapper.addHost("lkwefjwojweffewoih", new String[0], createHost("blah11")); + mapper.addHost("zzzuyopjvewpovewjhfewoih", new String[0], createHost("blah12")); + mapper.addHost("xxxxgqwiwoih", new String[0], createHost("blah13")); + mapper.addHost("qwigqwiwoih", new String[0], createHost("blah14")); + mapper.addHost("qwerty.net", new String[0], createHost("blah15")); + mapper.addHost("*.net", new String[0], createHost("blah16")); + mapper.addHost("zzz.com", new String[0], createHost("blah17")); + mapper.addHostAlias("iowejoiejfoiew", "iowejoiejfoiew_alias"); + + mapper.setDefaultHostName("ylwrehirkuewh"); + + String[] welcomes = new String[2]; + welcomes[0] = "boo/baba"; + welcomes[1] = "bobou"; + + Host host = createHost("blah7"); + mapper.addContextVersion("iowejoiejfoiew", host, "", "0", createContext("context0"), new String[0], null, null); + mapper.addContextVersion("iowejoiejfoiew", host, "/foo", "0", createContext("context1"), new String[0], null, + null); + mapper.addContextVersion("iowejoiejfoiew", host, "/foo/bar", "0", createContext("context2"), welcomes, null, + null); + + mapper.addWrappers("iowejoiejfoiew", "/foo", "0", Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("context1-defaultWrapper"), false, false) })); + mapper.addWrappers("iowejoiejfoiew", "/foo/bar", "0", + Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/fo/*", createWrapper("wrapper0"), false, false), + new WrapperMappingInfo("/", createWrapper("wrapper1"), false, false), + new WrapperMappingInfo("/blh", createWrapper("wrapper2"), false, false), + new WrapperMappingInfo("*.jsp", createWrapper("wrapper3"), false, false), + new WrapperMappingInfo("/blah/bou/*", createWrapper("wrapper4"), false, false), + new WrapperMappingInfo("/blah/bobou/*", createWrapper("wrapper5"), false, false), + new WrapperMappingInfo("*.htm", createWrapper("wrapper6"), false, false) })); + + mapper.addContextVersion("iowejoiejfoiew", host, "/foo/bar/bla", "0", createContext("context3"), new String[0], + null, Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/bobou/*", createWrapper("wrapper7"), false, false) })); + + host = createHost("blah16"); + mapper.addContextVersion("*.net", host, "", "0", createContext("context4"), new String[0], null, null); + mapper.addWrappers("*.net", "", "0", Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("context4-defaultWrapper"), false, false) })); + } + + @Test + public void testAddHost() throws Exception { + // Try to add duplicates + // Duplicate Host name + mapper.addHost("iowejoiejfoiew", new String[0], createHost("blah17")); + // Alias conflicting with existing Host + mapper.addHostAlias("iowejoiejfoiew", "qwigqwiwoih"); + // Alias conflicting with existing Alias + mapper.addHostAlias("sjbjdvwsbvhrb", "iowejoiejfoiew_alias"); + // Redundancy. Alias name = Host name. No error here. + mapper.addHostAlias("qwigqwiwoih", "qwigqwiwoih"); + // Redundancy. Duplicate Alias for the same Host name. No error here. + mapper.addHostAlias("iowejoiejfoiew", "iowejoiejfoiew_alias"); + mapper.addHostAlias("iowejoiejfoiew", "iowejoiejfoiew_alias"); + + // Check we have the right number + // (added 17 including one host alias. Three duplicates do not increase the count.) + Assert.assertEquals(19, mapper.hosts.length); + + // Make sure adding a duplicate *does not* overwrite + final int iowPos = 4; + Assert.assertEquals("blah7", mapper.hosts[iowPos].object.getName()); + + final int qwigPos = 10; + Assert.assertEquals("blah14", mapper.hosts[qwigPos].object.getName()); + + // Check for alphabetical order of host names + String previous; + String current = mapper.hosts[0].name; + for (int i = 1; i < mapper.hosts.length; i++) { + previous = current; + current = mapper.hosts[i].name; + Assert.assertTrue(previous.compareTo(current) < 0); + } + + // Check that host alias has the same data + Mapper.MappedHost host = mapper.hosts[iowPos]; + Mapper.MappedHost alias = mapper.hosts[iowPos + 1]; + Assert.assertEquals("iowejoiejfoiew", host.name); + Assert.assertEquals("iowejoiejfoiew_alias", alias.name); + Assert.assertFalse(host.isAlias()); + Assert.assertTrue(alias.isAlias()); + Assert.assertEquals(host.object, alias.object); + + // Test addContextVersion() followed by addHost() + Host hostZ = createHost("zzzz"); + Context contextZ = createContext("contextZ"); + + Assert.assertEquals(19, mapper.hosts.length); + mapper.addContextVersion("zzzz", hostZ, "/", "", contextZ, null, null, null); + Assert.assertEquals(20, mapper.hosts.length); + + mapper.addHost("zzzz", new String[] { "zzzz_alias1", "zzzz_alias2" }, hostZ); + Assert.assertEquals(22, mapper.hosts.length); + + Assert.assertEquals("zzzz", mapper.hosts[19].name); + Assert.assertEquals("zzzz_alias1", mapper.hosts[20].name); + Assert.assertEquals("zzzz_alias2", mapper.hosts[21].name); + Assert.assertEquals(2, mapper.hosts[19].getAliases().size()); + Assert.assertSame(contextZ, mapper.hosts[19].contextList.contexts[0].versions[0].object); + Assert.assertSame(contextZ, mapper.hosts[21].contextList.contexts[0].versions[0].object); + } + + @Test + public void testRemoveHost() { + Assert.assertEquals(19, mapper.hosts.length); + mapper.removeHostAlias("iowejoiejfoiew"); + mapper.removeHost("iowejoiejfoiew_alias"); + Assert.assertEquals(19, mapper.hosts.length); // No change + mapper.removeHostAlias("iowejoiejfoiew_alias"); + Assert.assertEquals(18, mapper.hosts.length); // Removed + + mapper.addHostAlias("iowejoiejfoiew", "iowejoiejfoiew_alias"); + Assert.assertEquals(19, mapper.hosts.length); + + final int iowPos = 4; + Mapper.MappedHost hostMapping = mapper.hosts[iowPos]; + Mapper.MappedHost aliasMapping = mapper.hosts[iowPos + 1]; + Assert.assertEquals("iowejoiejfoiew_alias", aliasMapping.name); + Assert.assertTrue(aliasMapping.isAlias()); + Assert.assertSame(hostMapping.object, aliasMapping.object); + + Assert.assertEquals("iowejoiejfoiew", hostMapping.getRealHostName()); + Assert.assertEquals("iowejoiejfoiew", aliasMapping.getRealHostName()); + Assert.assertSame(hostMapping, hostMapping.getRealHost()); + Assert.assertSame(hostMapping, aliasMapping.getRealHost()); + + mapper.removeHost("iowejoiejfoiew"); + Assert.assertEquals(17, mapper.hosts.length); // Both host and alias removed + for (Mapper.MappedHost host : mapper.hosts) { + Assert.assertTrue(host.name, !host.name.startsWith("iowejoiejfoiew")); + } + } + + @Test + public void testMap() throws Exception { + MappingData mappingData = new MappingData(); + MessageBytes host = MessageBytes.newInstance(); + host.setString("iowejoiejfoiew"); + MessageBytes wildcard = MessageBytes.newInstance(); + wildcard.setString("foo.net"); + MessageBytes alias = MessageBytes.newInstance(); + alias.setString("iowejoiejfoiew_alias"); + MessageBytes uri = MessageBytes.newInstance(); + uri.setString("/foo/bar/blah/bobou/foo"); + uri.toChars(); + uri.getCharChunk().setLimit(-1); + + mapper.map(host, uri, null, mappingData); + Assert.assertEquals("blah7", mappingData.host.getName()); + Assert.assertEquals("context2", mappingData.context.getName()); + Assert.assertEquals("wrapper5", mappingData.wrapper.getName()); + Assert.assertEquals("/blah/bobou", mappingData.wrapperPath.toString()); + Assert.assertEquals("/foo", mappingData.pathInfo.toString()); + Assert.assertTrue(mappingData.redirectPath.isNull()); + + mappingData.recycle(); + uri.recycle(); + uri.setString("/foo/bar/bla/bobou/foo"); + uri.toChars(); + uri.getCharChunk().setLimit(-1); + mapper.map(host, uri, null, mappingData); + Assert.assertEquals("blah7", mappingData.host.getName()); + Assert.assertEquals("context3", mappingData.context.getName()); + Assert.assertEquals("wrapper7", mappingData.wrapper.getName()); + Assert.assertEquals("/bobou", mappingData.wrapperPath.toString()); + Assert.assertEquals("/foo", mappingData.pathInfo.toString()); + Assert.assertTrue(mappingData.redirectPath.isNull()); + + mappingData.recycle(); + uri.recycle(); + uri.setString("/foo/bar/bla/bobou/foo"); + uri.toChars(); + uri.getCharChunk().setLimit(-1); + mapper.map(wildcard, uri, null, mappingData); + Assert.assertEquals("blah16", mappingData.host.getName()); + Assert.assertEquals("context4", mappingData.context.getName()); + Assert.assertEquals("context4-defaultWrapper", mappingData.wrapper.getName()); + Assert.assertEquals("/foo/bar/bla/bobou/foo", mappingData.wrapperPath.toString()); + Assert.assertTrue(mappingData.pathInfo.isNull()); + Assert.assertTrue(mappingData.redirectPath.isNull()); + + mappingData.recycle(); + uri.setString("/foo/bar/bla/bobou/foo"); + uri.toChars(); + uri.getCharChunk().setLimit(-1); + mapper.map(alias, uri, null, mappingData); + Assert.assertEquals("blah7", mappingData.host.getName()); + Assert.assertEquals("context3", mappingData.context.getName()); + Assert.assertEquals("wrapper7", mappingData.wrapper.getName()); + Assert.assertEquals("/bobou", mappingData.wrapperPath.toString()); + Assert.assertEquals("/foo", mappingData.pathInfo.toString()); + Assert.assertTrue(mappingData.redirectPath.isNull()); + } + + @Test + public void testAddRemoveContextVersion() throws Exception { + final String hostName = "iowejoiejfoiew"; + final int iowPos = 4; + final String contextPath = "/foo/bar"; + final int contextPos = 2; + + MappingData mappingData = new MappingData(); + MessageBytes hostMB = MessageBytes.newInstance(); + MessageBytes uriMB = MessageBytes.newInstance(); + hostMB.setString(hostName); + uriMB.setString("/foo/bar/blah/bobou/foo"); + + // Verifying configuration created by setUp() + Mapper.MappedHost mappedHost = mapper.hosts[iowPos]; + Assert.assertEquals(hostName, mappedHost.name); + Mapper.MappedContext mappedContext = mappedHost.contextList.contexts[contextPos]; + Assert.assertEquals(contextPath, mappedContext.name); + Assert.assertEquals(1, mappedContext.versions.length); + Assert.assertEquals("0", mappedContext.versions[0].name); + Host oldHost = mappedHost.object; + Context oldContext = mappedContext.versions[0].object; + Assert.assertEquals("context2", oldContext.getName()); + + Context oldContext1 = mappedHost.contextList.contexts[contextPos - 1].versions[0].object; + Assert.assertEquals("context1", oldContext1.getName()); + + mappingData.recycle(); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals("blah7", mappingData.host.getName()); + Assert.assertEquals("context2", mappingData.context.getName()); + Assert.assertEquals("wrapper5", mappingData.wrapper.getName()); + mappingData.recycle(); + mapper.map(oldContext, uriMB, mappingData); + Assert.assertEquals("wrapper5", mappingData.wrapper.getName()); + + Context newContext = createContext("newContext"); + mapper.addContextVersion(hostName, oldHost, contextPath, "1", newContext, null, null, + Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("newContext-default"), false, false) })); + + Assert.assertEquals(2, mappedContext.versions.length); + Assert.assertEquals("0", mappedContext.versions[0].name); + Assert.assertEquals("1", mappedContext.versions[1].name); + mappingData.recycle(); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals("newContext", mappingData.context.getName()); + Assert.assertEquals("newContext-default", mappingData.wrapper.getName()); + mappingData.recycle(); + mapper.map(newContext, uriMB, mappingData); + Assert.assertEquals("newContext-default", mappingData.wrapper.getName()); + + mapper.removeContextVersion(oldContext, hostName, contextPath, "0"); + + Assert.assertEquals(1, mappedContext.versions.length); + Assert.assertEquals("1", mappedContext.versions[0].name); + mappingData.recycle(); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals("newContext", mappingData.context.getName()); + Assert.assertEquals("newContext-default", mappingData.wrapper.getName()); + mappingData.recycle(); + mapper.map(newContext, uriMB, mappingData); + Assert.assertEquals("newContext-default", mappingData.wrapper.getName()); + + mapper.removeContextVersion(oldContext, hostName, contextPath, "1"); + + Assert.assertNotSame(mappedContext, mappedHost.contextList.contexts[contextPos]); + Assert.assertEquals("/foo/bar/bla", mappedHost.contextList.contexts[contextPos].name); + mappingData.recycle(); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals("context1", mappingData.context.getName()); + Assert.assertEquals("context1-defaultWrapper", mappingData.wrapper.getName()); + mappingData.recycle(); + mapper.map(oldContext1, uriMB, mappingData); + Assert.assertEquals("context1-defaultWrapper", mappingData.wrapper.getName()); + + mapper.addContextVersion(hostName, oldHost, contextPath, "0", newContext, null, null, + Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("newContext-defaultWrapper2"), false, false) })); + mappedContext = mappedHost.contextList.contexts[contextPos]; + + Assert.assertEquals(contextPath, mappedContext.name); + Assert.assertEquals(1, mappedContext.versions.length); + Assert.assertEquals("0", mappedContext.versions[0].name); + mappingData.recycle(); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals("newContext", mappingData.context.getName()); + Assert.assertEquals("newContext-defaultWrapper2", mappingData.wrapper.getName()); + mappingData.recycle(); + mapper.map(newContext, uriMB, mappingData); + Assert.assertEquals("newContext-defaultWrapper2", mappingData.wrapper.getName()); + } + + @Test + public void testReloadContextVersion() throws Exception { + final String hostName = "iowejoiejfoiew"; + final int iowPos = 4; + final String contextPath = "/foo/bar"; + final int contextPos = 2; + + MappingData mappingData = new MappingData(); + MessageBytes hostMB = MessageBytes.newInstance(); + MessageBytes uriMB = MessageBytes.newInstance(); + hostMB.setString(hostName); + uriMB.setString("/foo/bar/blah/bobou/foo"); + + // Verifying configuration created by setUp() + Mapper.MappedHost mappedHost = mapper.hosts[iowPos]; + Assert.assertEquals(hostName, mappedHost.name); + Mapper.MappedContext mappedContext = mappedHost.contextList.contexts[contextPos]; + Assert.assertEquals(contextPath, mappedContext.name); + Assert.assertEquals(1, mappedContext.versions.length); + Assert.assertEquals("0", mappedContext.versions[0].name); + Host oldHost = mappedHost.object; + Context oldContext = mappedContext.versions[0].object; + Assert.assertEquals("context2", oldContext.getName()); + + Context oldContext1 = mappedHost.contextList.contexts[contextPos - 1].versions[0].object; + Assert.assertEquals("context1", oldContext1.getName()); + + mappingData.recycle(); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals("blah7", mappingData.host.getName()); + Assert.assertEquals("context2", mappingData.context.getName()); + Assert.assertEquals("wrapper5", mappingData.wrapper.getName()); + mappingData.recycle(); + mapper.map(oldContext, uriMB, mappingData); + Assert.assertEquals("wrapper5", mappingData.wrapper.getName()); + + // Mark context as paused + // This is what happens when context reload starts + mapper.pauseContextVersion(oldContext, hostName, contextPath, "0"); + + mappingData.recycle(); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals("blah7", mappingData.host.getName()); + Assert.assertEquals("context2", mappingData.context.getName()); + // Wrapper is not mapped for incoming requests if context is paused + Assert.assertNull(mappingData.wrapper); + mappingData.recycle(); + mapper.map(oldContext, uriMB, mappingData); + // Wrapper is mapped for mapping method used by forward or include dispatch + Assert.assertEquals("wrapper5", mappingData.wrapper.getName()); + + // Re-add the same context, but different list of wrappers + // This is what happens when context reload completes + mapper.addContextVersion(hostName, oldHost, contextPath, "0", oldContext, null, null, + Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("newDefaultWrapper"), false, false) })); + + mappedContext = mappedHost.contextList.contexts[contextPos]; + Assert.assertEquals(contextPath, mappedContext.name); + Assert.assertEquals(1, mappedContext.versions.length); + Assert.assertEquals("0", mappedContext.versions[0].name); + + mappingData.recycle(); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals("blah7", mappingData.host.getName()); + Assert.assertEquals("context2", mappingData.context.getName()); + Assert.assertEquals("newDefaultWrapper", mappingData.wrapper.getName()); + mappingData.recycle(); + mapper.map(oldContext, uriMB, mappingData); + Assert.assertEquals("newDefaultWrapper", mappingData.wrapper.getName()); + } + + @Test + public void testContextListConcurrencyBug56653() throws Exception { + final Host host = createHost("localhost"); + final Context contextRoot = createContext("ROOT"); + final Context context1 = createContext("foo"); + final Context context2 = createContext("foo#bar"); + final Context context3 = createContext("foo#bar#bla"); + final Context context4 = createContext("foo#bar#bla#baz"); + + mapper.addHost("localhost", new String[] { "alias" }, host); + mapper.setDefaultHostName("localhost"); + + mapper.addContextVersion("localhost", host, "", "0", contextRoot, new String[0], null, null); + mapper.addContextVersion("localhost", host, "/foo", "0", context1, new String[0], null, null); + mapper.addContextVersion("localhost", host, "/foo/bar", "0", context2, new String[0], null, null); + mapper.addContextVersion("localhost", host, "/foo/bar/bla", "0", context3, new String[0], null, null); + mapper.addContextVersion("localhost", host, "/foo/bar/bla/baz", "0", context4, new String[0], null, null); + + final AtomicBoolean running = new AtomicBoolean(true); + Thread t = new Thread() { + @Override + public void run() { + for (int i = 0; i < 100000; i++) { + mapper.removeContextVersion(context4, "localhost", "/foo/bar/bla/baz", "0"); + mapper.addContextVersion("localhost", host, "/foo/bar/bla/baz", "0", context4, new String[0], null, + null); + } + running.set(false); + } + }; + + MappingData mappingData = new MappingData(); + MessageBytes hostMB = MessageBytes.newInstance(); + hostMB.setString("localhost"); + MessageBytes aliasMB = MessageBytes.newInstance(); + aliasMB.setString("alias"); + MessageBytes uriMB = MessageBytes.newInstance(); + char[] uri = "/foo/bar/bla/bobou/foo".toCharArray(); + uriMB.setChars(uri, 0, uri.length); + + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals(context3, mappingData.context); + + mappingData.recycle(); + uriMB.setChars(uri, 0, uri.length); + mapper.map(aliasMB, uriMB, null, mappingData); + Assert.assertEquals(context3, mappingData.context); + + t.start(); + while (running.get()) { + mappingData.recycle(); + uriMB.setChars(uri, 0, uri.length); + mapper.map(hostMB, uriMB, null, mappingData); + Assert.assertEquals(context3, mappingData.context); + + mappingData.recycle(); + uriMB.setChars(uri, 0, uri.length); + mapper.map(aliasMB, uriMB, null, mappingData); + Assert.assertEquals(context3, mappingData.context); + } + } + + + @Test + public void testCompareIgnoreCase() throws Exception { + + Mapper mapper = new Mapper(); + + mapper.addHost("aaa", new String[0], createHost("a3")); + mapper.addHost("aaaa", new String[0], createHost("a4")); + mapper.addHost("aaaaa", new String[0], createHost("a5")); + mapper.addHost("aaaaaa", new String[0], createHost("a6")); + mapper.addHost("aaaaaaa", new String[0], createHost("a7")); + + mapper.setDefaultHostName("aaa"); + + mapper.addContextVersion("aaa", createHost("a3"), "", "0", createContext("c3"), new String[0], null, null); + mapper.addContextVersion("aaaa", createHost("a4"), "", "0", createContext("c4"), new String[0], null, null); + mapper.addContextVersion("aaaaa", createHost("a5"), "", "0", createContext("c5"), new String[0], null, null); + mapper.addContextVersion("aaaaaa", createHost("a6"), "", "0", createContext("c6"), new String[0], null, null); + mapper.addContextVersion("aaaaaaa", createHost("a7"), "", "0", createContext("c7"), new String[0], null, null); + + mapper.addWrappers("aaa", "", "0", Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("c3-default"), false, false) })); + mapper.addWrappers("aaaa", "", "0", Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("c4-default"), false, false) })); + mapper.addWrappers("aaaaa", "", "0", Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("c5-default"), false, false) })); + mapper.addWrappers("aaaaaa", "", "0", Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("c6-default"), false, false) })); + mapper.addWrappers("aaaaaaa", "", "0", Arrays.asList(new WrapperMappingInfo[] { + new WrapperMappingInfo("/", createWrapper("c7-default"), false, false) })); + + MappingData mappingData = new MappingData(); + MessageBytes hostMB = MessageBytes.newInstance(); + hostMB.setString("aaaa"); + MessageBytes uriMB = MessageBytes.newInstance(); + char[] uri = "/index.html".toCharArray(); + uriMB.setChars(uri, 0, uri.length); + + mapper.map(hostMB, uriMB, null, mappingData); + + Assert.assertEquals("a4", mappingData.host.getName()); + } +} diff --git a/test/org/apache/catalina/mapper/TestMapperListener.java b/test/org/apache/catalina/mapper/TestMapperListener.java new file mode 100644 index 0000000..f5be04e --- /dev/null +++ b/test/org/apache/catalina/mapper/TestMapperListener.java @@ -0,0 +1,117 @@ +/* + * 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.catalina.mapper; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Container; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.websocket.server.WsContextListener; + +public class TestMapperListener extends TomcatBaseTest { + + @Test + public void testTomcatRestartListenerCount_Bug56717() throws IOException, LifecycleException { + // The test runs Tomcat twice, tests that it has started successfully, + // and compares the counts of listeners registered on containers + // after the first and the second starts. + // Sample request is from TestTomcat#testSingleWebapp() + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + // app dir is relative to server home + Context ctxt = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + tomcat.start(); + + ByteChunk res; + String text; + res = getUrl("http://localhost:" + getPort() + "/examples/servlets/servlet/HelloWorldExample"); + text = res.toString(); + Assert.assertTrue(text, text.contains("")); + + List listenersFirst = new ArrayList<>(); + populateListenersInfo(listenersFirst, tomcat.getEngine()); + + tomcat.stop(); + tomcat.start(); + + res = getUrl("http://localhost:" + getPort() + "/examples/servlets/servlet/HelloWorldExample"); + text = res.toString(); + Assert.assertTrue(text, text.contains("")); + + List listenersSecond = new ArrayList<>(); + populateListenersInfo(listenersSecond, tomcat.getEngine()); + + Assert.assertEquals(listenersFirst.size(), listenersSecond.size()); + for (int i = 0, len = listenersFirst.size(); i < len; i++) { + ListenersInfo a = listenersFirst.get(i); + ListenersInfo b = listenersSecond.get(i); + boolean equal = a.container.getClass() == b.container.getClass() && + a.containerListeners.length == b.containerListeners.length && + a.lifecycleListeners.length == b.lifecycleListeners.length; + if (!equal) { + Assert.fail("The lists of listeners differ:\n" + a + "\n" + b); + } + } + } + + private static class ListenersInfo { + public final Container container; + public final ContainerListener[] containerListeners; + public final LifecycleListener[] lifecycleListeners; + + ListenersInfo(Container container, ContainerListener[] containerListeners, + LifecycleListener[] lifecycleListeners) { + this.container = container; + this.containerListeners = containerListeners; + this.lifecycleListeners = lifecycleListeners; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("[container: \"").append(container).append("\"\n containerListeners.length: ") + .append(containerListeners.length).append(", lifecycleListeners.length: ") + .append(lifecycleListeners.length).append("\n containerListeners: ") + .append(Arrays.asList(containerListeners)).append("\n lifecycleListeners: ") + .append(Arrays.asList(lifecycleListeners)).append("\n]"); + return buf.toString(); + } + } + + private static void populateListenersInfo(List list, Container container) { + list.add(new ListenersInfo(container, container.findContainerListeners(), container.findLifecycleListeners())); + for (Container child : container.findChildren()) { + populateListenersInfo(list, child); + } + } +} diff --git a/test/org/apache/catalina/mapper/TestMapperPerformance.java b/test/org/apache/catalina/mapper/TestMapperPerformance.java new file mode 100644 index 0000000..4b4feb8 --- /dev/null +++ b/test/org/apache/catalina/mapper/TestMapperPerformance.java @@ -0,0 +1,73 @@ +/* + * 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.catalina.mapper; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.MessageBytes; + +/* + * This is an absolute performance test with an upper limit. Therefore it is executed as part of a standard test run. + */ +public class TestMapperPerformance extends TestMapper { + + @Test + public void testPerformance() throws Exception { + String[] requestedHostNames = new String[] { "xxxxxxxxxxx", "iowejoiejfoiew", "iowejoiejfoiex", "owefojiwefoi", + "owefojiwefoix", "qwerty.net", "foo.net", "zzz.com", "abc.com" }; + + for (String requestedHostName : requestedHostNames) { + testPerformance(requestedHostName); + } + } + + private void testPerformance(String requestedHostName) throws Exception { + // Takes ~1s on markt's laptop. If this takes more than 5s something + // probably needs looking at. If this fails repeatedly then we may need + // to increase this limit. + final long maxTime = 5000; + long time = testPerformanceImpl(requestedHostName); + log.info("Host [" + requestedHostName + "], Time [" + time + "]ms"); + if (time >= maxTime) { + // Rerun to reject occasional failures, e.g. because of gc + log.warn("testPerformance() test completed in " + time + " ms"); + time = testPerformanceImpl(requestedHostName); + log.warn("testPerformance() test rerun completed in " + time + " ms"); + } + Assert.assertTrue(String.valueOf(time), time < maxTime); + } + + private long testPerformanceImpl(String requestedHostName) throws Exception { + MappingData mappingData = new MappingData(); + MessageBytes host = MessageBytes.newInstance(); + host.setString(requestedHostName); + MessageBytes uri = MessageBytes.newInstance(); + uri.setString("/foo/bar/blah/bobou/foo"); + uri.toChars(); + uri.getCharChunk().setLimit(-1); + + long start = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + mappingData.recycle(); + mapper.map(host, uri, null, mappingData); + } + long time = System.currentTimeMillis() - start; + return time; + } + +} diff --git a/test/org/apache/catalina/mapper/TestMapperWebapps.java b/test/org/apache/catalina/mapper/TestMapperWebapps.java new file mode 100644 index 0000000..91262d6 --- /dev/null +++ b/test/org/apache/catalina/mapper/TestMapperWebapps.java @@ -0,0 +1,270 @@ +/* + * 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.catalina.mapper; + +import java.io.File; +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.valves.RemoteAddrValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.websocket.server.WsContextListener; + +/** + * Mapper tests that use real web applications on a running Tomcat. + */ +public class TestMapperWebapps extends TomcatBaseTest { + + @Test + public void testContextRoot_Bug53339() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + // No file system docBase required + Context ctx = tomcat.addContext("", null); + + Tomcat.addServlet(ctx, "Bug53356", new Bug53356Servlet()); + ctx.addServletMappingDecoded("", "Bug53356"); + + tomcat.start(); + + ByteChunk body = getUrl("http://localhost:" + getPort()); + + Assert.assertEquals("OK", body.toString()); + } + + private static class Bug53356Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Confirm behaviour as per Servlet 12.2 + boolean pass = "/".equals(req.getPathInfo()); + if (pass) { + pass = "".equals(req.getServletPath()); + } + if (pass) { + pass = "".equals(req.getContextPath()); + } + + resp.setContentType("text/plain"); + if (pass) { + resp.getWriter().write("OK"); + } else { + resp.getWriter().write("FAIL"); + } + } + } + + @Test + public void testContextReload_Bug56658_Bug56882() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + // app dir is relative to server home + org.apache.catalina.Context ctxt = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + tomcat.start(); + + // The tests are from TestTomcat#testSingleWebapp(), #testJsps() + // We reload the context and verify that the pages are still accessible + ByteChunk res; + String text; + + res = getUrl("http://localhost:" + getPort() + "/examples/servlets/servlet/HelloWorldExample"); + text = res.toString(); + Assert.assertTrue(text, text.contains("")); + + res = getUrl("http://localhost:" + getPort() + "/examples/jsp/jsp2/el/basic-arithmetic.jsp"); + text = res.toString(); + Assert.assertTrue(text, text.contains("${(1==2) ? 3 : 4}")); + + res = getUrl("http://localhost:" + getPort() + "/examples/index.html"); + text = res.toString(); + Assert.assertTrue(text, text.contains("Apache Tomcat Examples")); + + long timeA = System.currentTimeMillis(); + res = getUrl("http://localhost:" + getPort() + "/examples/jsp/include/include.jsp"); + String timestamp = findCommonPrefix(timeA, System.currentTimeMillis()); + text = res.toString(); + Assert.assertTrue(text, + text.contains("In place evaluation of another JSP which gives you the current time: " + timestamp)); + Assert.assertTrue(text, text.contains("To get the current time in ms")); + Assert.assertTrue(text, text.contains("by including the output of another JSP: " + timestamp)); + Assert.assertTrue(text, text.contains(":-)")); + + res = getUrl("http://localhost:" + getPort() + "/examples/jsp/forward/forward.jsp"); + text = res.toString(); + Assert.assertTrue(text, text.contains("VM Memory usage")); + + ctxt.reload(); + + res = getUrl("http://localhost:" + getPort() + "/examples/servlets/servlet/HelloWorldExample"); + text = res.toString(); + Assert.assertTrue(text, text.contains("")); + + res = getUrl("http://localhost:" + getPort() + "/examples/jsp/jsp2/el/basic-arithmetic.jsp"); + text = res.toString(); + Assert.assertTrue(text, text.contains("${(1==2) ? 3 : 4}")); + + res = getUrl("http://localhost:" + getPort() + "/examples/index.html"); + text = res.toString(); + Assert.assertTrue(text, text.contains("Apache Tomcat Examples")); + + timeA = System.currentTimeMillis(); + res = getUrl("http://localhost:" + getPort() + "/examples/jsp/include/include.jsp"); + timestamp = findCommonPrefix(timeA, System.currentTimeMillis()); + text = res.toString(); + Assert.assertTrue(text, + text.contains("In place evaluation of another JSP which gives you the current time: " + timestamp)); + Assert.assertTrue(text, text.contains("To get the current time in ms")); + Assert.assertTrue(text, text.contains("by including the output of another JSP: " + timestamp)); + Assert.assertTrue(text, text.contains(":-)")); + + res = getUrl("http://localhost:" + getPort() + "/examples/jsp/forward/forward.jsp"); + text = res.toString(); + Assert.assertTrue(text, text.contains("VM Memory usage")); + } + + @Test + public void testWelcomeFileNotStrict() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + + StandardContext ctxt = (StandardContext) tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + ctxt.setReplaceWelcomeFiles(true); + ctxt.addWelcomeFile("index.jsp"); + // Mapping for *.do is defined in web.xml + ctxt.addWelcomeFile("index.do"); + + tomcat.start(); + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/welcome-files", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(bc.toString().contains("JSP")); + + rc = getUrl("http://localhost:" + getPort() + "/test/welcome-files/sub", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(bc.toString().contains("Servlet")); + } + + @Test + public void testWelcomeFileStrict() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + + StandardContext ctxt = (StandardContext) tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + ctxt.setReplaceWelcomeFiles(true); + ctxt.addWelcomeFile("index.jsp"); + // Mapping for *.do is defined in web.xml + ctxt.addWelcomeFile("index.do"); + + // Simulate STRICT_SERVLET_COMPLIANCE + ctxt.setResourceOnlyServlets(""); + + tomcat.start(); + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/welcome-files", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(bc.toString().contains("JSP")); + + rc = getUrl("http://localhost:" + getPort() + "/test/welcome-files/sub", bc, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + } + + @Test + public void testRedirect() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // Use standard test webapp as ROOT + File rootDir = new File("test/webapp"); + org.apache.catalina.Context root = tomcat.addWebapp(null, "", rootDir.getAbsolutePath()); + + // Add a security constraint + SecurityConstraint constraint = new SecurityConstraint(); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded("/welcome-files/*"); + collection.addPatternDecoded("/welcome-files"); + constraint.addCollection(collection); + constraint.addAuthRole("foo"); + root.addConstraint(constraint); + + // Also make examples available + File examplesDir = new File(getBuildDirectory(), "webapps/examples"); + org.apache.catalina.Context examples = tomcat.addWebapp(null, "/examples", examplesDir.getAbsolutePath()); + examples.setMapperContextRootRedirectEnabled(false); + // Then block access to the examples to test redirection + RemoteAddrValve rav = new RemoteAddrValve(); + rav.setDeny(".*"); + rav.setDenyStatus(404); + examples.getPipeline().addValve(rav); + + tomcat.start(); + + // Redirects within a web application + doRedirectTest("/welcome-files", 401); + doRedirectTest("/welcome-files/", 401); + + doRedirectTest("/jsp", 302); + doRedirectTest("/jsp/", 404); + + doRedirectTest("/WEB-INF", 404); + doRedirectTest("/WEB-INF/", 404); + + // Redirects between web applications + doRedirectTest("/examples", 404); + doRedirectTest("/examples/", 404); + } + + + private void doRedirectTest(String path, int expected) throws IOException { + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + path, bc, false); + Assert.assertEquals(expected, rc); + } + + + /** + * Prepare a string to search in messages that contain a timestamp, when it is known that the timestamp was printed + * between {@code timeA} and {@code timeB}. + */ + private static String findCommonPrefix(long timeA, long timeB) { + while ((timeA != timeB) && timeA > 0) { + timeA /= 10; + timeB /= 10; + } + return String.valueOf(timeA); + } +} diff --git a/test/org/apache/catalina/mbeans/TestRegistration.java b/test/org/apache/catalina/mbeans/TestRegistration.java new file mode 100644 index 0000000..cd7a982 --- /dev/null +++ b/test/org/apache/catalina/mbeans/TestRegistration.java @@ -0,0 +1,261 @@ +/* + * 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.catalina.mbeans; + +import java.io.File; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Realm; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.realm.CombinedRealm; +import org.apache.catalina.realm.NullRealm; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.modeler.Registry; + +/** + * General tests around the process of registration and de-registration that + * don't necessarily apply to one specific Tomcat class. + * + */ +public class TestRegistration extends TomcatBaseTest { + + private static final String contextName = "/foo"; + + private static final String ADDRESS; + + static { + String address; + try { + address = InetAddress.getByName("localhost").getHostAddress(); + } catch (UnknownHostException e) { + address = "INIT_FAILED"; + } + ADDRESS = address; + } + + + private static String[] basicMBeanNames() { + return new String[] { + "Tomcat:type=Engine", + "Tomcat:type=Realm,realmPath=/realm0", + "Tomcat:type=Mapper", + "Tomcat:type=MBeanFactory", + "Tomcat:type=NamingResources", + "Tomcat:type=Server", + "Tomcat:type=Service", + "Tomcat:type=StringCache", + "Tomcat:type=UtilityExecutor", + "Tomcat:type=Valve,name=StandardEngineValve", + }; + } + + private static String[] hostMBeanNames(String host) { + return new String[] { + "Tomcat:type=Host,host=" + host, + "Tomcat:type=Valve,host=" + host + ",name=ErrorReportValve", + "Tomcat:type=Valve,host=" + host + ",name=StandardHostValve", + }; + } + + private String[] optionalMBeanNames(String host) { + if (isAccessLogEnabled()) { + return new String[] { + "Tomcat:type=Valve,host=" + host + ",name=AccessLogValve", + }; + } else { + return new String[] { }; + } + } + + private static String[] requestMBeanNames(String port, String type) { + return new String[] { + "Tomcat:type=RequestProcessor,worker=" + + ObjectName.quote("http-" + type + "-" + ADDRESS + "-" + port) + + ",name=HttpRequest1", + }; + } + + private static String[] contextMBeanNames(String host, String context) { + return new String[] { + "Tomcat:j2eeType=WebModule,name=//" + host + context + + ",J2EEApplication=none,J2EEServer=none", + "Tomcat:type=Loader,host=" + host + ",context=" + context, + "Tomcat:type=Manager,host=" + host + ",context=" + context, + "Tomcat:type=NamingResources,host=" + host + ",context=" + context, + "Tomcat:type=Valve,host=" + host + ",context=" + context + + ",name=NonLoginAuthenticator", + "Tomcat:type=Valve,host=" + host + ",context=" + context + + ",name=StandardContextValve", + "Tomcat:type=ParallelWebappClassLoader,host=" + host + ",context=" + context, + "Tomcat:type=WebResourceRoot,host=" + host + ",context=" + context, + "Tomcat:type=WebResourceRoot,host=" + host + ",context=" + context + + ",name=Cache", + "Tomcat:type=Realm,realmPath=/realm0,host=" + host + + ",context=" + context, + "Tomcat:type=Realm,realmPath=/realm0/realm0,host=" + host + + ",context=" + context + }; + } + + private static String[] connectorMBeanNames(String port, String type) { + return new String[] { + "Tomcat:type=Connector,port=" + port + ",address=" + + ObjectName.quote(ADDRESS), + "Tomcat:type=GlobalRequestProcessor,name=" + + ObjectName.quote("http-" + type + "-" + ADDRESS + "-" + port), + "Tomcat:type=ProtocolHandler,port=" + port + ",address=" + + ObjectName.quote(ADDRESS), + "Tomcat:type=ThreadPool,name=" + + ObjectName.quote("http-" + type + "-" + ADDRESS + "-" + port), + "Tomcat:type=SocketProperties,name=" + + ObjectName.quote("http-" + type + "-" + ADDRESS + "-" + port), + }; + } + + /* + * Test verifying that Tomcat correctly de-registers the MBeans it has + * registered. + * @author Marc Guillemot + */ + @Test + public void testMBeanDeregistration() throws Exception { + final MBeanServer mbeanServer = Registry.getRegistry(null, null).getMBeanServer(); + // Verify there are no Catalina or Tomcat MBeans + Set onames = mbeanServer.queryNames(new ObjectName("Catalina:*"), null); + log.info(MBeanDumper.dumpBeans(mbeanServer, onames)); + Assert.assertEquals("Unexpected: " + onames, 0, onames.size()); + onames = mbeanServer.queryNames(new ObjectName("Tomcat:*"), null); + log.info(MBeanDumper.dumpBeans(mbeanServer, onames)); + Assert.assertEquals("Unexpected: " + onames, 0, onames.size()); + + final Tomcat tomcat = getTomcatInstance(); + final File contextDir = new File(getTemporaryDirectory(), "webappFoo"); + addDeleteOnTearDown(contextDir); + if (!contextDir.mkdirs() && !contextDir.isDirectory()) { + Assert.fail("Failed to create: [" + contextDir.toString() + "]"); + } + Context ctx = tomcat.addContext(contextName, contextDir.getAbsolutePath()); + + CombinedRealm combinedRealm = new CombinedRealm(); + Realm nullRealm = new NullRealm(); + combinedRealm.addRealm(nullRealm); + ctx.setRealm(combinedRealm); + + tomcat.start(); + + getUrl("http://localhost:" + getPort()); + + // Verify there are no Catalina MBeans + onames = mbeanServer.queryNames(new ObjectName("Catalina:*"), null); + log.info(MBeanDumper.dumpBeans(mbeanServer, onames)); + Assert.assertEquals("Found: " + onames, 0, onames.size()); + + // Verify there are the correct Tomcat MBeans + onames = mbeanServer.queryNames(new ObjectName("Tomcat:*"), null); + ArrayList found = new ArrayList<>(onames.size()); + for (ObjectName on: onames) { + found.add(on.toString()); + } + + // Create the list of expected MBean names + String protocol = tomcat.getConnector().getProtocolHandlerClassName(); + if (protocol.indexOf("Nio2") > 0) { + protocol = "nio2"; + } else { + protocol = "nio"; + } + String index = tomcat.getConnector().getProperty("nameIndex").toString(); + ArrayList expected = new ArrayList<>(Arrays.asList(basicMBeanNames())); + expected.addAll(Arrays.asList(hostMBeanNames("localhost"))); + expected.addAll(Arrays.asList(contextMBeanNames("localhost", contextName))); + expected.addAll(Arrays.asList(connectorMBeanNames("auto-" + index, protocol))); + expected.addAll(Arrays.asList(optionalMBeanNames("localhost"))); + expected.addAll(Arrays.asList(requestMBeanNames( + "auto-" + index + "-" + getPort(), protocol))); + + // Did we find all expected MBeans? + ArrayList missing = new ArrayList<>(expected); + missing.removeAll(found); + Assert.assertTrue("Missing Tomcat MBeans: " + missing, missing.isEmpty()); + + // Did we find any unexpected MBeans? + List additional = found; + additional.removeAll(expected); + Assert.assertTrue("Unexpected Tomcat MBeans: " + additional, additional.isEmpty()); + + // Check a known attribute + String connectorName = Arrays.asList(connectorMBeanNames("auto-" + index, protocol)).get(0); + // This should normally return "http", but any non null non exception is good enough + Assert.assertNotNull(mbeanServer.getAttribute(new ObjectName(connectorName), "scheme")); + + tomcat.stop(); + + // There should still be some Tomcat MBeans + onames = mbeanServer.queryNames(new ObjectName("Tomcat:*"), null); + Assert.assertTrue("No Tomcat MBeans", onames.size() > 0); + + // add a new host + StandardHost host = new StandardHost(); + host.setName("otherhost"); + tomcat.getEngine().addChild(host); + + final File contextDir2 = new File(getTemporaryDirectory(), "webappFoo2"); + addDeleteOnTearDown(contextDir2); + if (!contextDir2.mkdirs() && !contextDir2.isDirectory()) { + Assert.fail("Failed to create: [" + contextDir2.toString() + "]"); + } + tomcat.addContext(host, contextName + "2", contextDir2.getAbsolutePath()); + + tomcat.start(); + tomcat.stop(); + tomcat.destroy(); + + // There should be no Catalina MBeans and no Tomcat MBeans + onames = mbeanServer.queryNames(new ObjectName("Catalina:*"), null); + log.info(MBeanDumper.dumpBeans(mbeanServer, onames)); + Assert.assertEquals("Remaining: " + onames, 0, onames.size()); + onames = mbeanServer.queryNames(new ObjectName("Tomcat:*"), null); + log.info(MBeanDumper.dumpBeans(mbeanServer, onames)); + Assert.assertEquals("Remaining: " + onames, 0, onames.size()); + } + + /* + * Confirm that, as far as ObjectName is concerned, the order of the key + * properties is not significant. + */ + @Test + public void testNames() throws MalformedObjectNameException { + ObjectName on1 = new ObjectName("test:foo=a,bar=b"); + ObjectName on2 = new ObjectName("test:bar=b,foo=a"); + + Assert.assertTrue(on1.equals(on2)); + } +} diff --git a/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java b/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java new file mode 100644 index 0000000..c86a500 --- /dev/null +++ b/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java @@ -0,0 +1,1700 @@ +/* + * 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.catalina.nonblocking; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.Socket; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.SocketFactory; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.AsyncContextImpl; +import org.apache.catalina.startup.BytesStreamer; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.valves.TesterAccessLogValve; +import org.apache.coyote.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestNonBlockingAPI extends TomcatBaseTest { + + private static final Log log = LogFactory.getLog(TestNonBlockingAPI.class); + + private static final int CHUNK_SIZE = 1024 * 1024; + private static final int WRITE_SIZE = CHUNK_SIZE * 10; + private static final byte[] DATA = new byte[WRITE_SIZE]; + private static final int WRITE_PAUSE_MS = 500; + + private static final Field CTX_REQUEST_FIELD; + + static { + // Use this sequence for padding to make it easier to spot errors + byte[] padding = new byte[] {'z', 'y', 'x', 'w', 'v', 'u', 't', 's', + 'r', 'q', 'p', 'o', 'n', 'm', 'l', 'k'}; + int blockSize = padding.length; + + for (int i = 0; i < WRITE_SIZE / blockSize; i++) { + String hex = String.format("%01X", Integer.valueOf(i)); + int hexSize = hex.length(); + int padSize = blockSize - hexSize; + + System.arraycopy(padding, 0, DATA, i * blockSize, padSize); + System.arraycopy( + hex.getBytes(), 0, DATA, i * blockSize + padSize, hexSize); + } + + Field f = null; + try { + f = AsyncContextImpl.class.getDeclaredField("request"); + f.setAccessible(true); + } catch (NoSuchFieldException | SecurityException e) { + Assert.fail(); + } + CTX_REQUEST_FIELD = f; + } + + + @Test + public void testNonBlockingRead() throws Exception { + doTestNonBlockingRead(false, false); + } + + + @Test + public void testNonBlockingReadAsync() throws Exception { + doTestNonBlockingRead(false, true); + } + + + @Test(expected=IOException.class) + public void testNonBlockingReadIgnoreIsReady() throws Exception { + doTestNonBlockingRead(true, false); + } + + + private void doTestNonBlockingRead(boolean ignoreIsReady, boolean async) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + NBReadServlet servlet = new NBReadServlet(ignoreIsReady, async); + String servletName = NBReadServlet.class.getName(); + Tomcat.addServlet(ctx, servletName, servlet); + ctx.addServletMappingDecoded("/", servletName); + + tomcat.start(); + + Map> reqHeaders = new HashMap<>(); + int rc = postUrl(true, new DataWriter(async ? 0 : 500, async ? 2000000 : 5), + "http://localhost:" + getPort() + "/", new ByteChunk(), reqHeaders, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + if (async) { + Assert.assertEquals(2000000 * 8, servlet.listener.body.length()); + TestAsyncReadListener listener = (TestAsyncReadListener) servlet.listener; + Assert.assertTrue(Math.abs(listener.containerThreadCount.get() - listener.notReadyCount.get()) <= 1); + Assert.assertEquals(listener.isReadyCount.get(), listener.nonContainerThreadCount.get()); + } else { + Assert.assertEquals(5 * 8, servlet.listener.body.length()); + } + } + + + @Test + public void testNonBlockingWrite() throws Exception { + testNonBlockingWriteInternal(false); + } + + @Test + public void testNonBlockingWriteWithKeepAlive() throws Exception { + testNonBlockingWriteInternal(true); + } + + private void testNonBlockingWriteInternal(boolean keepAlive) throws Exception { + AtomicBoolean asyncContextIsComplete = new AtomicBoolean(false); + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + NBWriteServlet servlet = new NBWriteServlet(asyncContextIsComplete); + String servletName = NBWriteServlet.class.getName(); + Tomcat.addServlet(ctx, servletName, servlet); + ctx.addServletMappingDecoded("/", servletName); + // Note: Low values of socket.txBufSize can trigger very poor + // performance. Set it just low enough to ensure that the + // non-blocking write servlet will see isReady() == false + Assert.assertTrue(tomcat.getConnector().setProperty("socket.txBufSize", "1048576")); + tomcat.start(); + + SocketFactory factory = SocketFactory.getDefault(); + Socket s = factory.createSocket("localhost", getPort()); + + InputStream is = s.getInputStream(); + byte[] buffer = new byte[8192]; + + ByteChunk result = new ByteChunk(); + + OutputStream os = s.getOutputStream(); + if (keepAlive) { + os.write(("OPTIONS * HTTP/1.1\r\n" + + "Host: localhost:" + getPort() + "\r\n" + + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + os.flush(); + // Make sure the entire response has been read. + int read = is.read(buffer); + // The response should end with CRLFCRLF + Assert.assertEquals(buffer[read - 4], '\r'); + Assert.assertEquals(buffer[read - 3], '\n'); + Assert.assertEquals(buffer[read - 2], '\r'); + Assert.assertEquals(buffer[read - 1], '\n'); + } + os.write(("GET / HTTP/1.1\r\n" + + "Host: localhost:" + getPort() + "\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + os.flush(); + + int read = 0; + int readSinceLastPause = 0; + while (read != -1) { + read = is.read(buffer); + if (readSinceLastPause == 0) { + log.info("Reading data"); + } + if (read > 0) { + result.append(buffer, 0, read); + } + readSinceLastPause += read; + if (readSinceLastPause > WRITE_SIZE / 16) { + log.info("Read " + readSinceLastPause + " bytes, pause 500ms"); + readSinceLastPause = 0; + Thread.sleep(500); + } + } + + os.close(); + is.close(); + s.close(); + + // Validate the result. + // Response line + String resultString = result.toString(); + log.info("Client read " + resultString.length() + " bytes"); + int lineStart = 0; + int lineEnd = resultString.indexOf('\n', 0); + String line = resultString.substring(lineStart, lineEnd + 1); + Assert.assertEquals("HTTP/1.1 200 \r\n", line); + + // Check headers - looking to see if response is chunked (it should be) + boolean chunked = false; + while (line.length() > 2) { + lineStart = lineEnd + 1; + lineEnd = resultString.indexOf('\n', lineStart); + line = resultString.substring(lineStart, lineEnd + 1); + if (line.startsWith("Transfer-Encoding:")) { + Assert.assertEquals("Transfer-Encoding: chunked\r\n", line); + chunked = true; + } + } + Assert.assertTrue(chunked); + + // Now check body size + int totalBodyRead = 0; + int chunkSize = -1; + + while (chunkSize != 0) { + // Chunk size in hex + lineStart = lineEnd + 1; + lineEnd = resultString.indexOf('\n', lineStart); + line = resultString.substring(lineStart, lineEnd + 1); + Assert.assertTrue(line.endsWith("\r\n")); + line = line.substring(0, line.length() - 2); + log.info("[" + line + "]"); + chunkSize = Integer.parseInt(line, 16); + + // Read the chunk + lineStart = lineEnd + 1; + lineEnd = resultString.indexOf('\n', lineStart); + log.info("Start : " + lineStart + ", End: " + lineEnd); + if (lineEnd > lineStart) { + line = resultString.substring(lineStart, lineEnd + 1); + } else { + line = resultString.substring(lineStart); + } + if (line.length() > 40) { + log.info(line.substring(0, 32)); + } else { + log.info(line); + } + if (chunkSize + 2 != line.length()) { + log.error("Chunk wrong length. Was " + line.length() + + " Expected " + (chunkSize + 2)); + + byte[] resultBytes = resultString.getBytes(); + + // Find error + boolean found = false; + for (int i = totalBodyRead; i < (totalBodyRead + line.length()); i++) { + if (DATA[i] != resultBytes[lineStart + i - totalBodyRead]) { + int dataStart = i - 64; + if (dataStart < 0) { + dataStart = 0; + } + int dataEnd = i + 64; + if (dataEnd > DATA.length) { + dataEnd = DATA.length; + } + int resultStart = lineStart + i - totalBodyRead - 64; + if (resultStart < 0) { + resultStart = 0; + } + int resultEnd = lineStart + i - totalBodyRead + 64; + if (resultEnd > resultString.length()) { + resultEnd = resultString.length(); + } + log.error("Mismatch tx: " + new String( + DATA, dataStart, dataEnd - dataStart)); + log.error("Mismatch rx: " + + resultString.substring(resultStart, resultEnd)); + found = true; + break; + } + } + if (!found) { + log.error("No mismatch. Data truncated"); + } + } + + Assert.assertTrue(line, line.endsWith("\r\n")); + Assert.assertEquals(chunkSize + 2, line.length()); + + totalBodyRead += chunkSize; + } + + Assert.assertEquals(WRITE_SIZE, totalBodyRead); + Assert.assertTrue("AsyncContext should have been completed.", asyncContextIsComplete.get()); + } + + + @Test + public void testNonBlockingWriteError01ListenerComplete() throws Exception { + doTestNonBlockingWriteError01NoListenerComplete(true); + } + + + @Test + public void testNonBlockingWriteError01NoListenerComplete() throws Exception { + doTestNonBlockingWriteError01NoListenerComplete(false); + } + + + private void doTestNonBlockingWriteError01NoListenerComplete(boolean listenerCompletesOnError) throws Exception { + AtomicBoolean asyncContextIsComplete = new AtomicBoolean(false); + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + TesterAccessLogValve alv = new TesterAccessLogValve(); + ctx.getPipeline().addValve(alv); + + // Some CI platforms appear to have particularly large write buffers + // and appear to ignore the socket.txBufSize below. Therefore, configure + // configure the Servlet to keep writing until an error is encountered. + NBWriteServlet servlet = new NBWriteServlet(asyncContextIsComplete, true, listenerCompletesOnError); + String servletName = NBWriteServlet.class.getName(); + Tomcat.addServlet(ctx, servletName, servlet); + ctx.addServletMappingDecoded("/", servletName); + // Note: Low values of socket.txBufSize can trigger very poor + // performance. Set it just low enough to ensure that the + // non-blocking write servlet will see isReady() == false + Assert.assertTrue(tomcat.getConnector().setProperty("socket.txBufSize", "524228")); + tomcat.start(); + + SocketFactory factory = SocketFactory.getDefault(); + Socket s = factory.createSocket("localhost", getPort()); + + ByteChunk result = new ByteChunk(); + OutputStream os = s.getOutputStream(); + os.write(("GET / HTTP/1.1\r\n" + + "Host: localhost:" + getPort() + "\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + os.flush(); + + InputStream is = s.getInputStream(); + byte[] buffer = new byte[8192]; + + int read = 0; + int readSinceLastPause = 0; + int readTotal = 0; + while (read != -1 && readTotal < WRITE_SIZE / 32) { + long start = System.currentTimeMillis(); + read = is.read(buffer); + long end = System.currentTimeMillis(); + log.info("Client read [" + read + "] bytes in [" + (end - start) + + "] ms"); + if (read > 0) { + result.append(buffer, 0, read); + } + readSinceLastPause += read; + readTotal += read; + if (readSinceLastPause > WRITE_SIZE / 64) { + readSinceLastPause = 0; + Thread.sleep(WRITE_PAUSE_MS); + } + } + + os.close(); + is.close(); + s.close(); + + String resultString = result.toString(); + log.info("Client read " + resultString.length() + " bytes"); + int lineStart = 0; + int lineEnd = resultString.indexOf('\n', 0); + String line = resultString.substring(lineStart, lineEnd + 1); + Assert.assertEquals("HTTP/1.1 200 \r\n", line); + + // Listeners are invoked and access valve entries created on a different + // thread so give that thread a chance to complete its work. + int count = 0; + while (count < 100 && !servlet.wlistener.onErrorInvoked) { + Thread.sleep(100); + count ++; + } + + while (count < 100 && !asyncContextIsComplete.get()) { + Thread.sleep(100); + count ++; + } + + while (count < 100 && alv.getEntryCount() < 1) { + Thread.sleep(100); + count ++; + } + + Assert.assertTrue("Error listener should have been invoked.", servlet.wlistener.onErrorInvoked); + Assert.assertTrue("Async context should have been completed.", asyncContextIsComplete.get()); + + // TODO Figure out why non-blocking writes with the NIO connector appear + // to be slower on Linux + alv.validateAccessLog(1, 500, WRITE_PAUSE_MS, + WRITE_PAUSE_MS + 30 * 1000); + } + + + @Test + public void testBug55438NonBlockingReadWriteEmptyRead() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + NBReadWriteServlet servlet = new NBReadWriteServlet(); + String servletName = NBReadWriteServlet.class.getName(); + Tomcat.addServlet(ctx, servletName, servlet); + ctx.addServletMappingDecoded("/", servletName); + + tomcat.start(); + + Map> resHeaders = new HashMap<>(); + int rc = postUrl(false, new BytesStreamer() { + @Override + public byte[] next() { + return new byte[] {}; + } + + @Override + public int getLength() { + return 0; + } + + @Override + public int available() { + return 0; + } + }, "http://localhost:" + + getPort() + "/", new ByteChunk(), resHeaders, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + + + public static class DataWriter implements BytesStreamer { + int max = 5; + int count = 0; + long delay = 0; + byte[] b = "WANTMORE".getBytes(StandardCharsets.ISO_8859_1); + byte[] f = "FINISHED".getBytes(StandardCharsets.ISO_8859_1); + + public DataWriter(long delay, int max) { + this.delay = delay; + this.max = max; + } + + @Override + public int getLength() { + return b.length * max; + } + + @Override + public int available() { + if (count < max) { + return b.length; + } else { + return 0; + } + } + + @Override + public byte[] next() { + if (count < max) { + if (count > 0) { + try { + if (delay > 0) { + Thread.sleep(delay); + } + } catch (Exception x) { + } + } + count++; + if (count < max) { + return b; + } else { + return f; + } + } else { + return null; + } + } + + } + + @WebServlet(asyncSupported = true) + public static class NBReadServlet extends TesterServlet { + private static final long serialVersionUID = 1L; + private final boolean async; + private final boolean ignoreIsReady; + transient TestReadListener listener; + + public NBReadServlet(boolean ignoreIsReady, boolean async) { + this.async = async; + this.ignoreIsReady = ignoreIsReady; + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // step 1 - start async + AsyncContext actx = req.startAsync(); + actx.setTimeout(Long.MAX_VALUE); + actx.addListener(new AsyncListener() { + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + log.info("onTimeout"); + + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + log.info("onStartAsync"); + + } + + @Override + public void onError(AsyncEvent event) throws IOException { + log.info("AsyncListener.onError"); + + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + log.info("onComplete"); + + } + }); + // step 2 - notify on read + ServletInputStream in = req.getInputStream(); + if (async) { + listener = new TestAsyncReadListener(actx, false, ignoreIsReady); + } else { + listener = new TestReadListener(actx, false, ignoreIsReady); + } + in.setReadListener(listener); + } + } + + @WebServlet(asyncSupported = true) + public static class NBWriteServlet extends TesterServlet { + private static final long serialVersionUID = 1L; + private final AtomicBoolean asyncContextIsComplete; + private final boolean unlimited; + private final boolean listenerCompletesOnError; + public transient volatile TestWriteListener wlistener; + + public NBWriteServlet(AtomicBoolean asyncContextIsComplete) { + this(asyncContextIsComplete, false, true); + } + + + public NBWriteServlet(AtomicBoolean asyncContextIsComplete, boolean unlimited, boolean listenerCompletesOnError) { + this.asyncContextIsComplete = asyncContextIsComplete; + this.unlimited = unlimited; + this.listenerCompletesOnError = listenerCompletesOnError; + } + + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // step 1 - start async + AsyncContext actx = req.startAsync(); + actx.setTimeout(Long.MAX_VALUE); + actx.addListener(new AsyncListener() { + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + log.info("onTimeout"); + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + log.info("onStartAsync"); + } + + @Override + public void onError(AsyncEvent event) throws IOException { + log.info("AsyncListener.onError"); + if (listenerCompletesOnError) { + event.getAsyncContext().complete(); + } + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + log.info("onComplete"); + asyncContextIsComplete.set(true); + } + }); + // step 2 - notify on read + ServletOutputStream out = resp.getOutputStream(); + resp.setBufferSize(200 * 1024); + wlistener = new TestWriteListener(actx, unlimited); + out.setWriteListener(wlistener); + } + + + } + + @WebServlet(asyncSupported = true) + public static class NBReadWriteServlet extends TesterServlet { + private static final long serialVersionUID = 1L; + public transient volatile TestReadWriteListener rwlistener; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // step 1 - start async + AsyncContext actx = req.startAsync(); + actx.setTimeout(Long.MAX_VALUE); + + // step 2 - notify on read + ServletInputStream in = req.getInputStream(); + rwlistener = new TestReadWriteListener(actx); + in.setReadListener(rwlistener); + } + } + + private static class TestReadListener implements ReadListener { + protected final AsyncContext ctx; + protected final boolean usingNonBlockingWrite; + protected final boolean ignoreIsReady; + protected final StringBuilder body = new StringBuilder(); + + + TestReadListener(AsyncContext ctx, + boolean usingNonBlockingWrite, + boolean ignoreIsReady) { + this.ctx = ctx; + this.usingNonBlockingWrite = usingNonBlockingWrite; + this.ignoreIsReady = ignoreIsReady; + } + + @Override + public void onDataAvailable() throws IOException { + ServletInputStream in = ctx.getRequest().getInputStream(); + String s = ""; + byte[] b = new byte[8192]; + int read = 0; + do { + read = in.read(b); + if (read == -1) { + break; + } + s += new String(b, 0, read); + } while (ignoreIsReady || in.isReady()); + log.info(s); + body.append(s); + } + + @Override + public void onAllDataRead() { + log.info("onAllDataRead totalData=" + body.toString().length()); + // If non-blocking writes are being used, don't write here as it + // will inject unexpected data into the write output. + if (!usingNonBlockingWrite) { + String msg; + if (body.toString().endsWith("FINISHED")) { + msg = "OK"; + } else { + msg = "FAILED"; + } + try { + ctx.getResponse().getOutputStream().print(msg); + } catch (IOException ioe) { + // Ignore + } + ctx.complete(); + } + } + + @Override + public void onError(Throwable throwable) { + log.info("ReadListener.onError totalData=" + body.toString().length()); + throwable.printStackTrace(); + } + } + + private static class TestAsyncReadListener extends TestReadListener { + + AtomicInteger isReadyCount = new AtomicInteger(0); + AtomicInteger notReadyCount = new AtomicInteger(0); + AtomicInteger containerThreadCount = new AtomicInteger(0); + AtomicInteger nonContainerThreadCount = new AtomicInteger(0); + + TestAsyncReadListener(AsyncContext ctx, + boolean usingNonBlockingWrite, boolean ignoreIsReady) { + super(ctx, usingNonBlockingWrite, ignoreIsReady); + } + + @Override + public void onDataAvailable() throws IOException { + Request coyoteRequest; + try { + coyoteRequest = ((org.apache.catalina.connector.Request) CTX_REQUEST_FIELD.get(ctx)).getCoyoteRequest(); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + + if (coyoteRequest.isRequestThread()) { + containerThreadCount.incrementAndGet(); + } else { + nonContainerThreadCount.incrementAndGet(); + } + new Thread() { + @Override + public void run() { + try { + ServletInputStream in = ctx.getRequest().getInputStream(); + byte[] b = new byte[1024]; + int read = in.read(b); + if (read == -1) { + return; + } + body.append(new String(b, 0, read)); + boolean isReady = ignoreIsReady || in.isReady(); + if (isReady) { + isReadyCount.incrementAndGet(); + } else { + notReadyCount.incrementAndGet(); + } + if (isReady) { + onDataAvailable(); + } + } catch (IOException e) { + onError(e); + } + } + }.start(); + } + + @Override + public void onAllDataRead() { + super.onAllDataRead(); + log.info("isReadyCount=" + isReadyCount + " notReadyCount=" + notReadyCount + + " containerThreadCount=" + containerThreadCount + + " nonContainerThreadCount=" + nonContainerThreadCount); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + log.info("isReadyCount=" + isReadyCount + " notReadyCount=" + notReadyCount + + " containerThreadCount=" + containerThreadCount + + " nonContainerThreadCount=" + nonContainerThreadCount); + } + } + + private static class TestWriteListener implements WriteListener { + AsyncContext ctx; + private final boolean unlimited; + int written = 0; + public volatile boolean onErrorInvoked = false; + + TestWriteListener(AsyncContext ctx, boolean unlimited) { + this.ctx = ctx; + this.unlimited = unlimited; + } + + @Override + public void onWritePossible() throws IOException { + long start = System.currentTimeMillis(); + int before = written; + while ((written < WRITE_SIZE || unlimited) && + ctx.getResponse().getOutputStream().isReady()) { + ctx.getResponse().getOutputStream().write( + DATA, written, CHUNK_SIZE); + written += CHUNK_SIZE; + } + if (written == WRITE_SIZE) { + // Clear the output buffer else data may be lost when + // calling complete + ctx.getResponse().flushBuffer(); + } + log.info("Write took: " + (System.currentTimeMillis() - start) + + " ms. Bytes before=" + before + " after=" + written); + // only call complete if we have emptied the buffer + if (ctx.getResponse().getOutputStream().isReady() && + written == WRITE_SIZE) { + // it is illegal to call complete + // if there is a write in progress + ctx.complete(); + } + } + + @Override + public void onError(Throwable throwable) { + log.info("WriteListener.onError"); + throwable.printStackTrace(); + onErrorInvoked = true; + } + + } + + private static class TestReadWriteListener implements ReadListener { + AsyncContext ctx; + private final StringBuilder body = new StringBuilder(); + + TestReadWriteListener(AsyncContext ctx) { + this.ctx = ctx; + } + + @Override + public void onDataAvailable() throws IOException { + ServletInputStream in = ctx.getRequest().getInputStream(); + String s = ""; + byte[] b = new byte[8192]; + int read = 0; + do { + read = in.read(b); + if (read == -1) { + break; + } + s += new String(b, 0, read); + } while (in.isReady()); + log.info("Read [" + s + "]"); + body.append(s); + } + + @Override + public void onAllDataRead() throws IOException { + log.info("onAllDataRead"); + ServletOutputStream output = ctx.getResponse().getOutputStream(); + output.setWriteListener(new WriteListener() { + @Override + public void onWritePossible() throws IOException { + ServletOutputStream output = ctx.getResponse().getOutputStream(); + if (output.isReady()) { + log.info("Writing [" + body.toString() + "]"); + output.write(body.toString().getBytes("utf-8")); + } + ctx.complete(); + } + + @Override + public void onError(Throwable throwable) { + log.info("ReadWriteListener.onError"); + throwable.printStackTrace(); + } + }); + } + + @Override + public void onError(Throwable throwable) { + log.info("ReadListener.onError"); + throwable.printStackTrace(); + } + + } + + public static int postUrlWithDisconnect(boolean stream, BytesStreamer streamer, String path, + Map> reqHead, Map> resHead) throws IOException { + + URL url = new URL(path); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setReadTimeout(1000000); + if (reqHead != null) { + for (Map.Entry> entry : reqHead.entrySet()) { + StringBuilder valueList = new StringBuilder(); + for (String value : entry.getValue()) { + if (valueList.length() > 0) { + valueList.append(','); + } + valueList.append(value); + } + connection.setRequestProperty(entry.getKey(), valueList.toString()); + } + } + if (streamer != null && stream) { + if (streamer.getLength() > 0) { + connection.setFixedLengthStreamingMode(streamer.getLength()); + } else { + connection.setChunkedStreamingMode(1024); + } + } + + connection.connect(); + + // Write the request body + try (OutputStream os = connection.getOutputStream()) { + while (streamer != null && streamer.available() > 0) { + byte[] next = streamer.next(); + os.write(next); + os.flush(); + } + } + + int rc = connection.getResponseCode(); + if (resHead != null) { + Map> head = connection.getHeaderFields(); + resHead.putAll(head); + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + + } + if (rc == HttpServletResponse.SC_OK) { + connection.getInputStream().close(); + connection.disconnect(); + } + return rc; + } + + + @Test + public void testDelayedNBWrite() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + CountDownLatch latch1 = new CountDownLatch(1); + DelayedNBWriteServlet servlet = new DelayedNBWriteServlet(latch1); + String servletName = DelayedNBWriteServlet.class.getName(); + Tomcat.addServlet(ctx, servletName, servlet); + ctx.addServletMappingDecoded("/", servletName); + + tomcat.start(); + + CountDownLatch latch2 = new CountDownLatch(2); + List exceptions = new ArrayList<>(); + + Thread t = new Thread( + new RequestExecutor("http://localhost:" + getPort() + "/", latch2, exceptions)); + t.start(); + + latch1.await(3000, TimeUnit.MILLISECONDS); + + Thread t1 = new Thread(new RequestExecutor( + "http://localhost:" + getPort() + "/?notify=true", latch2, exceptions)); + t1.start(); + + latch2.await(3000, TimeUnit.MILLISECONDS); + + if (exceptions.size() > 0) { + Assert.fail(); + } + } + + @Test + public void testDelayedNBReadWrite() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + CountDownLatch latch1 = new CountDownLatch(2); + DelayedNBReadWriteServlet servlet = new DelayedNBReadWriteServlet(latch1); + String servletName = DelayedNBReadWriteServlet.class.getName(); + Tomcat.addServlet(ctx, servletName, servlet); + ctx.addServletMappingDecoded("/", servletName); + + tomcat.start(); + + CountDownLatch latch2 = new CountDownLatch(1); + List exceptions = new ArrayList<>(); + + Thread t = new Thread( + new RequestPostExecutor("http://localhost:" + getPort() + "/", latch2, exceptions)); + t.start(); + + latch1.await(3000, TimeUnit.MILLISECONDS); + latch2.await(3000, TimeUnit.MILLISECONDS); + + if (exceptions.size() > 0) { + Assert.fail(); + } + } + + private static final class RequestExecutor implements Runnable { + private final String url; + private final CountDownLatch latch; + private final List exceptions; + + RequestExecutor(String url, CountDownLatch latch, List exceptions) { + this.url = url; + this.latch = latch; + this.exceptions = exceptions; + } + + @Override + public void run() { + try { + ByteChunk result = new ByteChunk(); + int rc = getUrl(url, result, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(result.toString().contains("OK")); + } catch (Throwable e) { + e.printStackTrace(); + exceptions.add(e); + } finally { + latch.countDown(); + } + } + + } + + private static final class RequestPostExecutor implements Runnable { + private final String url; + private final CountDownLatch latch; + private final List exceptions; + + RequestPostExecutor(String url, CountDownLatch latch, List exceptions) { + this.url = url; + this.latch = latch; + this.exceptions = exceptions; + } + + @Override + public void run() { + try { + ByteChunk result = new ByteChunk(); + int rc = postUrl("body".getBytes("utf-8"), url, result, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(result.toString().contains("OK")); + } catch (Throwable e) { + e.printStackTrace(); + exceptions.add(e); + } finally { + latch.countDown(); + } + } + + } + + @WebServlet(asyncSupported = true) + private static final class DelayedNBWriteServlet extends TesterServlet { + private static final long serialVersionUID = 1L; + private final Set emitters = new HashSet<>(); + private final transient CountDownLatch latch; + + DelayedNBWriteServlet(CountDownLatch latch) { + this.latch = latch; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + boolean notify = Boolean.parseBoolean(request.getParameter("notify")); + AsyncContext ctx = request.startAsync(); + ctx.setTimeout(1000); + if (!notify) { + emitters.add(new Emitter(ctx)); + latch.countDown(); + } else { + for (Emitter e : emitters) { + e.emit(); + } + response.getOutputStream().println("OK"); + response.getOutputStream().flush(); + ctx.complete(); + } + } + + } + + @WebServlet(asyncSupported = true) + private static final class DelayedNBReadWriteServlet extends TesterServlet { + private static final long serialVersionUID = 1L; + private final transient CountDownLatch latch; + + DelayedNBReadWriteServlet(CountDownLatch latch) { + this.latch = latch; + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + final AsyncContext ctx = request.startAsync(); + ctx.setTimeout(1000); + + Thread readWriteListener = new Thread(new ReadWriteListener(latch, ctx)); + readWriteListener.start(); + } + } + + private static final class ReadWriteListener implements Runnable { + private final transient CountDownLatch latch; + private final transient AsyncContext ctx; + + ReadWriteListener(CountDownLatch latch, AsyncContext ctx){ + this.latch = latch; + this.ctx = ctx; + } + + @Override + public void run() { + try { + setListeners(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void setListeners() throws IOException { + final ServletInputStream is = ctx.getRequest().getInputStream(); + final ServletOutputStream os = ctx.getResponse().getOutputStream(); + + is.setReadListener(new ReadListener() { + @Override + public void onDataAvailable() { + + try { + byte buffer[] = new byte[1 * 4]; + while (is.isReady() && !is.isFinished()) { + @SuppressWarnings("unused") + int ignore = is.read(buffer); + } + String body = new String(buffer, StandardCharsets.UTF_8); + Assert.assertTrue(body.equals("body")); + + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + @Override + public void onAllDataRead() { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + } + }); + + os.setWriteListener(new WriteListener() { + private boolean written = false; + + @Override + public void onWritePossible() throws IOException { + ServletOutputStream out = ctx.getResponse().getOutputStream(); + if (out.isReady() && !written) { + out.println("OK"); + written = true; + } + if (out.isReady() && written) { + out.flush(); + if (out.isReady()) { + ctx.complete(); + latch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + } + + }); + } + + } + + + private static final class Emitter implements Serializable { + + private static final long serialVersionUID = 1L; + + private final transient AsyncContext ctx; + + Emitter(AsyncContext ctx) { + this.ctx = ctx; + } + + void emit() throws IOException { + ctx.getResponse().getOutputStream().setWriteListener(new WriteListener() { + private boolean written = false; + + @Override + public void onWritePossible() throws IOException { + ServletOutputStream out = ctx.getResponse().getOutputStream(); + if (out.isReady() && !written) { + out.println("OK"); + written = true; + } + if (out.isReady() && written) { + out.flush(); + if (out.isReady()) { + ctx.complete(); + } + } + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + } + + }); + } + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=61932 + */ + @Test + public void testNonBlockingReadWithDispatch() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + NBReadWithDispatchServlet servlet = new NBReadWithDispatchServlet(); + String servletName = NBReadWithDispatchServlet.class.getName(); + Tomcat.addServlet(ctx, servletName, servlet); + ctx.addServletMappingDecoded("/", servletName); + + tomcat.start(); + + Map> resHeaders = new HashMap<>(); + int rc = postUrl(true, new DataWriter(500, 5), "http://localhost:" + + getPort() + "/", new ByteChunk(), resHeaders, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + + + @WebServlet(asyncSupported = true) + private static final class NBReadWithDispatchServlet extends TesterServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + final CountDownLatch latch = new CountDownLatch(1); + + // Dispatch to "/error" will end up here + if (req.getDispatcherType().equals(DispatcherType.ASYNC)) { + // Return without writing anything. This will generate the + // expected 200 response. + return; + } + + final AsyncContext asyncCtx = req.startAsync(); + final ServletInputStream is = req.getInputStream(); + is.setReadListener(new ReadListener() { + + @Override + public void onDataAvailable() { + + try { + byte buffer[] = new byte[1 * 1024]; + while (is.isReady() && !is.isFinished()) { + is.read(buffer); + } + + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + @Override + public void onAllDataRead() { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + } + }); + + new Thread(() -> { + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + asyncCtx.dispatch("/error"); + }).start(); + } + } + + + @Test + public void testCanceledPostChunked() throws Exception { + doTestCanceledPost(new String[] { + "POST / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + SimpleHttpClient.CRLF + + "Transfer-Encoding: Chunked" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "10" + SimpleHttpClient.CRLF + + "This is 16 bytes" + SimpleHttpClient.CRLF + }); + } + + + @Test + public void testCanceledPostNoChunking() throws Exception { + doTestCanceledPost(new String[] { + "POST / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + SimpleHttpClient.CRLF + + "Content-Length: 100" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "This is 16 bytes" + }); + } + + + /* + * Tests an error on an non-blocking read when the client closes the + * connection before fully writing the request body. + * + * Required sequence is: + * - enter Servlet's service() method + * - startAsync() + * - configure non-blocking read + * - read partial body + * - close client connection + * - error is triggered + * - exit Servlet's service() method + * + * This test makes extensive use of instance fields in the Servlet that + * would normally be considered very poor practice. It is only safe in this + * test as the Servlet only processes a single request. + */ + private void doTestCanceledPost(String[] request) throws Exception { + + CountDownLatch partialReadLatch = new CountDownLatch(1); + CountDownLatch completeLatch = new CountDownLatch(1); + + AtomicBoolean testFailed = new AtomicBoolean(true); + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + PostServlet postServlet = new PostServlet(partialReadLatch, completeLatch, testFailed); + Wrapper wrapper = Tomcat.addServlet(ctx, "postServlet", postServlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/*", "postServlet"); + + tomcat.start(); + + ResponseOKClient client = new ResponseOKClient(); + client.setPort(getPort()); + client.setRequest(request); + client.connect(); + client.sendRequest(); + + // Wait server to read partial request body + partialReadLatch.await(); + + client.disconnect(); + + completeLatch.await(); + + Assert.assertFalse(testFailed.get()); + } + + + private static final class ResponseOKClient extends SimpleHttpClient { + + @Override + public boolean isResponseBodyOK() { + return true; + } + } + + + private static final class PostServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final transient CountDownLatch partialReadLatch; + private final transient CountDownLatch completeLatch; + private final AtomicBoolean testFailed; + + PostServlet(CountDownLatch doPostLatch, CountDownLatch completeLatch, AtomicBoolean testFailed) { + this.partialReadLatch = doPostLatch; + this.completeLatch = completeLatch; + this.testFailed = testFailed; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + AsyncContext ac = req.startAsync(); + ac.setTimeout(-1); + CanceledPostAsyncListener asyncListener = new CanceledPostAsyncListener(completeLatch); + ac.addListener(asyncListener); + + CanceledPostReadListener readListener = new CanceledPostReadListener(ac, partialReadLatch, testFailed); + req.getInputStream().setReadListener(readListener); + } + } + + + private static final class CanceledPostAsyncListener implements AsyncListener { + + private final transient CountDownLatch completeLatch; + + CanceledPostAsyncListener(CountDownLatch completeLatch) { + this.completeLatch = completeLatch; + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + System.out.println("complete"); + completeLatch.countDown(); + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + System.out.println("onTimeout"); + } + + @Override + public void onError(AsyncEvent event) throws IOException { + System.out.println("onError-async"); + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + System.out.println("onStartAsync"); + } + } + + private static final class CanceledPostReadListener implements ReadListener { + + private final AsyncContext ac; + private final CountDownLatch partialReadLatch; + private final AtomicBoolean testFailed; + private int totalRead = 0; + + CanceledPostReadListener(AsyncContext ac, CountDownLatch partialReadLatch, AtomicBoolean testFailed) { + this.ac = ac; + this.partialReadLatch = partialReadLatch; + this.testFailed = testFailed; + } + + @Override + public void onDataAvailable() throws IOException { + ServletInputStream sis = ac.getRequest().getInputStream(); + boolean isReady; + + byte[] buffer = new byte[32]; + do { + if (partialReadLatch.getCount() == 0) { + System.out.println("debug"); + } + int bytesRead = sis.read(buffer); + + if (bytesRead == -1) { + return; + } + totalRead += bytesRead; + isReady = sis.isReady(); + System.out.println("Read [" + bytesRead + + "], buffer [" + new String(buffer, 0, bytesRead, StandardCharsets.UTF_8) + + "], total read [" + totalRead + + "], isReady [" + isReady + "]"); + } while (isReady); + if (totalRead == 16) { + partialReadLatch.countDown(); + } + } + + @Override + public void onAllDataRead() throws IOException { + ac.complete(); + } + + @Override + public void onError(Throwable throwable) { + throwable.printStackTrace(); + // This is the expected behaviour so clear the failed flag. + testFailed.set(false); + ac.complete(); + } + } + + + @Test + public void testNonBlockingWriteError02NoSwallow() throws Exception { + doTestNonBlockingWriteError02(false); + } + + + @Test + public void testNonBlockingWriteError02Swallow() throws Exception { + doTestNonBlockingWriteError02(true); + } + + + /* + * Tests client disconnect in the following scenario: + * - async with non-blocking IO + * - response has been committed + * - no data in buffers + * - client disconnects + * - server attempts a write + */ + private void doTestNonBlockingWriteError02(boolean swallowIoException) throws Exception { + CountDownLatch responseCommitLatch = new CountDownLatch(1); + CountDownLatch clientCloseLatch = new CountDownLatch(1); + CountDownLatch asyncCompleteLatch = new CountDownLatch(1); + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + NBWriteServlet02 writeServlet = + new NBWriteServlet02(responseCommitLatch, clientCloseLatch, asyncCompleteLatch, swallowIoException); + Wrapper wrapper = Tomcat.addServlet(ctx, "writeServlet", writeServlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/*", "writeServlet"); + + tomcat.start(); + + ResponseOKClient client = new ResponseOKClient(); + client.setPort(getPort()); + client.setRequest(new String[] { + "GET / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + }); + client.connect(); + client.sendRequest(); + + responseCommitLatch.await(); + + client.disconnect(); + clientCloseLatch.countDown(); + + Assert.assertTrue("Failed to complete async processing", asyncCompleteLatch.await(60, TimeUnit.SECONDS)); + } + + + private static class NBWriteServlet02 extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final transient CountDownLatch responseCommitLatch; + private final transient CountDownLatch clientCloseLatch; + private final transient CountDownLatch asyncCompleteLatch; + private final boolean swallowIoException; + + NBWriteServlet02(CountDownLatch responseCommitLatch, CountDownLatch clientCloseLatch, + CountDownLatch asyncCompleteLatch, boolean swallowIoException) { + this.responseCommitLatch = responseCommitLatch; + this.clientCloseLatch = clientCloseLatch; + this.asyncCompleteLatch = asyncCompleteLatch; + this.swallowIoException = swallowIoException; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + AsyncContext ac = req.startAsync(); + ac.addListener(new TestAsyncListener02(asyncCompleteLatch)); + ac.setTimeout(5000); + + WriteListener writeListener = + new TestWriteListener02(ac, responseCommitLatch, clientCloseLatch, swallowIoException); + resp.getOutputStream().setWriteListener(writeListener); + } + } + + + private static class TestAsyncListener02 implements AsyncListener { + + private final CountDownLatch asyncCompleteLatch; + + TestAsyncListener02(CountDownLatch asyncCompleteLatch) { + this.asyncCompleteLatch = asyncCompleteLatch; + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + asyncCompleteLatch.countDown(); + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onError(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + // NO-OP + } + + } + + private static class TestWriteListener02 implements WriteListener { + + private final AsyncContext ac; + private final CountDownLatch responseCommitLatch; + private final CountDownLatch clientCloseLatch; + private final boolean swallowIoException; + private volatile AtomicInteger stage = new AtomicInteger(0); + + TestWriteListener02(AsyncContext ac, CountDownLatch responseCommitLatch, + CountDownLatch clientCloseLatch, boolean swallowIoException) { + this.ac = ac; + this.responseCommitLatch = responseCommitLatch; + this.clientCloseLatch = clientCloseLatch; + this.swallowIoException = swallowIoException; + } + + @Override + public void onWritePossible() throws IOException { + try { + ServletOutputStream sos = ac.getResponse().getOutputStream(); + do { + if (stage.get() == 0) { + // Commit the response + ac.getResponse().flushBuffer(); + responseCommitLatch.countDown(); + stage.incrementAndGet(); + } else if (stage.get() == 1) { + // Wait for the client to drop the connection + try { + clientCloseLatch.await(); + } catch (InterruptedException e) { + // Ignore + } + sos.print("TEST"); + stage.incrementAndGet(); + } else if (stage.get() == 2) { + // This should trigger an error as the client closed the + // socket + sos.flush(); + // Additional writes are required to trigger the error + // on solaris + sos.print("MORE"); + log.info("Additional server write after client close to trigger exception"); + } + } while (sos.isReady()); + } catch (IOException ioe) { + if (!swallowIoException) { + throw ioe; + } + } + } + + @Override + public void onError(Throwable throwable) { + // NO-OP + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/nonblocking/TesterAjpNonBlockingClient.java b/test/org/apache/catalina/nonblocking/TesterAjpNonBlockingClient.java new file mode 100644 index 0000000..fce3a9c --- /dev/null +++ b/test/org/apache/catalina/nonblocking/TesterAjpNonBlockingClient.java @@ -0,0 +1,105 @@ +/* + * 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.catalina.nonblocking; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.net.SocketFactory; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.nonblocking.TestNonBlockingAPI.DataWriter; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * This is not a standard set of unit tests. This is a set of test clients for + * AJP support of Servlet 3.1 non-blocking IO. It assumes that there is an httpd + * instance listening on localhost:80 that is redirecting all traffic to a + * default Tomcat instance of version 8 or above that includes the examples + * web application. + */ +public class TesterAjpNonBlockingClient extends TomcatBaseTest { + + @Test + public void doTestAJPNonBlockingRead() throws Exception { + + Map> resHeaders = new HashMap<>(); + ByteChunk out = new ByteChunk(); + int rc = postUrl(true, new DataWriter(2000, 5), "http://localhost" + + "/examples/servlets/nonblocking/bytecounter", + out, resHeaders, null); + + System.out.println(out.toString()); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + + + @Test + public void testNonBlockingWrite() throws Exception { + + SocketFactory factory = SocketFactory.getDefault(); + Socket s = factory.createSocket("localhost", 80); + + ByteChunk result = new ByteChunk(); + OutputStream os = s.getOutputStream(); + os.write(("GET /examples/servlets/nonblocking/numberwriter HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n").getBytes(StandardCharsets.ISO_8859_1)); + os.flush(); + + InputStream is = s.getInputStream(); + byte[] buffer = new byte[8192]; + + int read = 0; + int readSinceLastPause = 0; + while (read != -1) { + read = is.read(buffer); + if (read > 0) { + result.append(buffer, 0, read); + } + readSinceLastPause += read; + if (readSinceLastPause > 40000) { + readSinceLastPause = 0; + Thread.sleep(500); + } + } + + os.close(); + is.close(); + s.close(); + + // Validate the result + String resultString = result.toString(); + log.info("Client read " + resultString.length() + " bytes"); + + System.out.println(resultString); + + Assert.assertTrue(resultString.contains("00000000000000010000")); + } +} diff --git a/test/org/apache/catalina/realm/TestGenericPrincipal.java b/test/org/apache/catalina/realm/TestGenericPrincipal.java new file mode 100644 index 0000000..495372c --- /dev/null +++ b/test/org/apache/catalina/realm/TestGenericPrincipal.java @@ -0,0 +1,87 @@ +/* + * 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.catalina.realm; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class TestGenericPrincipal { + + private static final String USER = "user"; + private static final List ROLES = Collections.unmodifiableList( + Arrays.asList(new String[] { "ROLE1", "ROLE2" })); + private static final TesterPrincipal PRINCIPAL = new TesterPrincipal("Principal"); + private static final TesterPrincipalNonSerializable PRINCIPAL_NON_SERIALIZABLE = + new TesterPrincipalNonSerializable("PrincipalNonSerializable"); + + @Test + public void testSerialize01() throws ClassNotFoundException, IOException { + GenericPrincipal gpIn = new GenericPrincipal(USER, ROLES); + doTest(gpIn); + } + + @Test + public void testSerialize02() throws ClassNotFoundException, IOException { + GenericPrincipal gpIn = new GenericPrincipal(USER, ROLES, PRINCIPAL); + doTest(gpIn); + } + + @Test + public void testSerialize03() throws ClassNotFoundException, IOException { + GenericPrincipal gpIn = new GenericPrincipal(USER, ROLES, PRINCIPAL_NON_SERIALIZABLE); + doTest(gpIn); + } + + private void doTest(GenericPrincipal gpIn) + throws ClassNotFoundException, IOException { + GenericPrincipal gpOut = serializeAndDeserialize(gpIn); + + Assert.assertNull(gpOut.getGssCredential()); + Assert.assertEquals(gpIn.getName(), gpOut.getName()); + Assert.assertArrayEquals(gpIn.getRoles(), gpOut.getRoles()); + if (gpIn == gpIn.getUserPrincipal()) { + Assert.assertEquals(gpOut, gpOut.getUserPrincipal()); + } else if (gpIn.getUserPrincipal() instanceof Serializable) { + Assert.assertEquals(gpIn.getUserPrincipal(), gpOut.getUserPrincipal()); + } else { + Assert.assertEquals(gpOut, gpOut.getUserPrincipal()); + } + } + + private GenericPrincipal serializeAndDeserialize(GenericPrincipal gpIn) + throws IOException, ClassNotFoundException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(gpIn); + + byte[] data = bos.toByteArray(); + + ByteArrayInputStream bis = new ByteArrayInputStream(data); + ObjectInputStream ois = new ObjectInputStream(bis); + return (GenericPrincipal) ois.readObject(); + } +} diff --git a/test/org/apache/catalina/realm/TestJNDIRealm.java b/test/org/apache/catalina/realm/TestJNDIRealm.java new file mode 100644 index 0000000..16d28d3 --- /dev/null +++ b/test/org/apache/catalina/realm/TestJNDIRealm.java @@ -0,0 +1,200 @@ +/* + * 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.catalina.realm; + +import java.lang.reflect.Field; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.naming.NameParserImpl; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.util.buf.HexUtils; +import org.easymock.EasyMock; + +public class TestJNDIRealm { + + private static final String ALGORITHM = "MD5"; + + private static final String USER = "test-user"; + private static final String PASSWORD = "test-password"; + private static final String REALM = "test-realm"; + + private static final String NONCE = "test-nonce"; + // Not digested but doesn't matter for the purposes of the test + private static final String DIGEST_A2 = "method:request-uri"; + public static final String USER_PASSWORD_ATTR = "test-pwd"; + + private static MessageDigest md5Helper; + + @BeforeClass + public static void setupClass() throws Exception { + md5Helper = MessageDigest.getInstance(ALGORITHM); + } + + @Test + public void testAuthenticateWithoutUserPassword() throws Exception { + // GIVEN + JNDIRealm realm = buildRealm(PASSWORD); + + // WHEN + String expectedResponse = + HexUtils.toHexString(md5Helper.digest((digestA1() + ":" + NONCE + ":" + DIGEST_A2).getBytes())); + Principal principal = + realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2, ALGORITHM); + + // THEN + Assert.assertNull(principal); + } + + @Test + public void testAuthenticateWithUserPassword() throws Exception { + // GIVEN + JNDIRealm realm = buildRealm(PASSWORD); + realm.setUserPassword(USER_PASSWORD_ATTR); + + // WHEN + String expectedResponse = + HexUtils.toHexString(md5Helper.digest((digestA1() + ":" + NONCE + ":" + DIGEST_A2).getBytes())); + Principal principal = + realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2, ALGORITHM); + + // THEN + assertThat(principal, instanceOf(GenericPrincipal.class)); + Assert.assertEquals(USER, principal.getName()); + } + + @Test + public void testAuthenticateWithUserPasswordAndCredentialHandler() throws Exception { + // GIVEN + JNDIRealm realm = buildRealm(digestA1()); + realm.setCredentialHandler(buildCredentialHandler()); + realm.setUserPassword(USER_PASSWORD_ATTR); + + // WHEN + String expectedResponse = + HexUtils.toHexString(md5Helper.digest((digestA1() + ":" + NONCE + ":" + DIGEST_A2).getBytes())); + Principal principal = + realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2, ALGORITHM); + + // THEN + assertThat(principal, instanceOf(GenericPrincipal.class)); + Assert.assertEquals(USER, principal.getName()); + } + + @Test + public void testErrorRealm() throws Exception { + Context context = new TesterContext(); + JNDIRealm realm = new JNDIRealm(); + realm.setContainer(context); + realm.setUserSearch(""); + // Connect to something that will fail + realm.setConnectionURL("ldap://127.0.0.1:12345"); + realm.start(); + + final CountDownLatch latch = new CountDownLatch(3); + (new Thread(() -> { realm.authenticate("foo", "bar"); latch.countDown(); })).start(); + (new Thread(() -> { realm.authenticate("foo", "bar"); latch.countDown(); })).start(); + (new Thread(() -> { realm.authenticate("foo", "bar"); latch.countDown(); })).start(); + + Assert.assertTrue(latch.await(30, TimeUnit.SECONDS)); + } + + + private JNDIRealm buildRealm(String password) throws NamingException, + NoSuchFieldException, IllegalAccessException, LifecycleException { + Context context = new TesterContext(); + JNDIRealm realm = new JNDIRealm(); + realm.setContainer(context); + realm.setUserSearch(""); + + // Usually everything is created in create() but that's not the case here + Field field = JNDIRealm.class.getDeclaredField("singleConnection"); + field.setAccessible(true); + Field field2 = JNDIRealm.JNDIConnection.class.getDeclaredField("context"); + field2.setAccessible(true); + field2.set(field.get(realm), mockDirContext(mockSearchResults(password))); + + realm.start(); + + return realm; + } + + private MessageDigestCredentialHandler buildCredentialHandler() + throws NoSuchAlgorithmException { + MessageDigestCredentialHandler credentialHandler = new MessageDigestCredentialHandler(); + credentialHandler.setAlgorithm(ALGORITHM); + return credentialHandler; + } + + private NamingEnumeration mockSearchResults(String password) + throws NamingException { + NamingEnumeration searchResults = + EasyMock.createNiceMock(NamingEnumeration.class); + EasyMock.expect(Boolean.valueOf(searchResults.hasMore())) + .andReturn(Boolean.TRUE) + .andReturn(Boolean.FALSE) + .andReturn(Boolean.TRUE) + .andReturn(Boolean.FALSE); + EasyMock.expect(searchResults.next()) + .andReturn(new SearchResult("ANY RESULT", "", + new BasicAttributes(USER_PASSWORD_ATTR, password))) + .times(2); + EasyMock.replay(searchResults); + return searchResults; + } + + private DirContext mockDirContext(NamingEnumeration namingEnumeration) + throws NamingException { + DirContext dirContext = EasyMock.createNiceMock(InitialDirContext.class); + EasyMock.expect(dirContext.search(EasyMock.anyString(), EasyMock.anyString(), + EasyMock.anyObject(SearchControls.class))) + .andReturn(namingEnumeration) + .times(2); + EasyMock.expect(dirContext.getNameParser("")) + .andReturn(new NameParserImpl()).times(2); + EasyMock.expect(dirContext.getNameInNamespace()) + .andReturn("ANY NAME") + .times(2); + EasyMock.replay(dirContext); + return dirContext; + } + + private String digestA1() { + String a1 = USER + ":" + REALM + ":" + PASSWORD; + return HexUtils.toHexString(md5Helper.digest(a1.getBytes())); + } +} diff --git a/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java b/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java new file mode 100644 index 0000000..677bcc5 --- /dev/null +++ b/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java @@ -0,0 +1,86 @@ +/* + * 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.catalina.realm; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestJNDIRealmAttributeValueEscape { + + @Parameterized.Parameters(name = "{index}: in[{0}], out[{1}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + // No escaping required + parameterSets.add(new String[] { "none", "none" }); + // Simple cases (same order as RFC 4512 section 2) + // Each appearing at the beginning, middle and ent + parameterSets.add(new String[] { " test", "\\20test" }); + parameterSets.add(new String[] { "te st", "te st" }); + parameterSets.add(new String[] { "test ", "test\\20" }); + parameterSets.add(new String[] { "#test", "\\23test" }); + parameterSets.add(new String[] { "te#st", "te#st" }); + parameterSets.add(new String[] { "test#", "test#" }); + parameterSets.add(new String[] { "\"test", "\\22test" }); + parameterSets.add(new String[] { "te\"st", "te\\22st" }); + parameterSets.add(new String[] { "test\"", "test\\22" }); + parameterSets.add(new String[] { "+test", "\\2Btest" }); + parameterSets.add(new String[] { "te+st", "te\\2Bst" }); + parameterSets.add(new String[] { "test+", "test\\2B" }); + parameterSets.add(new String[] { ",test", "\\2Ctest" }); + parameterSets.add(new String[] { "te,st", "te\\2Cst" }); + parameterSets.add(new String[] { "test,", "test\\2C" }); + parameterSets.add(new String[] { ";test", "\\3Btest" }); + parameterSets.add(new String[] { "te;st", "te\\3Bst" }); + parameterSets.add(new String[] { "test;", "test\\3B" }); + parameterSets.add(new String[] { "test", "\\3Etest" }); + parameterSets.add(new String[] { "te>st", "te\\3Est" }); + parameterSets.add(new String[] { "test>", "test\\3E" }); + parameterSets.add(new String[] { "\\test", "\\5Ctest" }); + parameterSets.add(new String[] { "te\\st", "te\\5Cst" }); + parameterSets.add(new String[] { "test\\", "test\\5C" }); + parameterSets.add(new String[] { "\u0000test", "\\00test" }); + parameterSets.add(new String[] { "te\u0000st", "te\\00st" }); + parameterSets.add(new String[] { "test\u0000", "test\\00" }); + return parameterSets; + } + + + @Parameter(0) + public String in; + @Parameter(1) + public String out; + + private JNDIRealm realm = new JNDIRealm(); + + @Test + public void testConvertToHexEscape() throws Exception { + String result = realm.doAttributeValueEscaping(in); + Assert.assertEquals(out, result); + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/realm/TestJNDIRealmConvertToHexEscape.java b/test/org/apache/catalina/realm/TestJNDIRealmConvertToHexEscape.java new file mode 100644 index 0000000..8c610a3 --- /dev/null +++ b/test/org/apache/catalina/realm/TestJNDIRealmConvertToHexEscape.java @@ -0,0 +1,70 @@ +/* + * 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.catalina.realm; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestJNDIRealmConvertToHexEscape { + + @Parameterized.Parameters(name = "{index}: in[{0}], out[{1}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new String[] { "none", "none" }); + parameterSets.add(new String[] { "\\", "\\" }); + parameterSets.add(new String[] { "\\\\", "\\5C" }); + parameterSets.add(new String[] { "\\5C", "\\5C" }); + parameterSets.add(new String[] { "\\ ", "\\20" }); + parameterSets.add(new String[] { "\\20", "\\20" }); + parameterSets.add(new String[] { "\\ foo", "\\20foo" }); + parameterSets.add(new String[] { "\\20foo", "\\20foo" }); + parameterSets.add(new String[] { "\\ foo", "\\20 foo" }); + parameterSets.add(new String[] { "\\20 foo", "\\20 foo" }); + parameterSets.add(new String[] { "\\ \\ foo", "\\20\\20foo" }); + parameterSets.add(new String[] { "\\20\\20foo", "\\20\\20foo" }); + parameterSets.add(new String[] { "foo\\ ", "foo\\20" }); + parameterSets.add(new String[] { "foo\\20", "foo\\20" }); + parameterSets.add(new String[] { "foo \\ ", "foo \\20" }); + parameterSets.add(new String[] { "foo \\20", "foo \\20" }); + parameterSets.add(new String[] { "foo\\ \\ ", "foo\\20\\20" }); + parameterSets.add(new String[] { "foo\\20\\20", "foo\\20\\20" }); + + return parameterSets; + } + + + @Parameter(0) + public String in; + @Parameter(1) + public String out; + + + @Test + public void testConvertToHexEscape() throws Exception { + String result = JNDIRealm.convertToHexEscape(in); + Assert.assertEquals(out, result); + } +} diff --git a/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java b/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java new file mode 100644 index 0000000..cdbc252 --- /dev/null +++ b/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java @@ -0,0 +1,315 @@ +/* + * 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.catalina.realm; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.juli.logging.LogFactory; + +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.listener.InMemoryListenerConfig; +import com.unboundid.ldap.sdk.AddRequest; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPResult; +import com.unboundid.ldap.sdk.ResultCode; + +@RunWith(Parameterized.class) +public class TestJNDIRealmIntegration { + + private static final String USER_PATTERN = "cn={0},ou=people,dc=example,dc=com"; + private static final String USER_SEARCH = "cn={0}"; + private static final String USER_BASE = "ou=people,dc=example,dc=com"; + private static final String ROLE_SEARCH_A = "member={0}"; + private static final String ROLE_SEARCH_B = "member=cn={1},ou=people,dc=example,dc=com"; + private static final String ROLE_SEARCH_C = "member=cn={2},ou=people,dc=example,dc=com"; + private static final String ROLE_BASE = "ou=people,dc=example,dc=com"; + + private static InMemoryDirectoryServer ldapServer; + + @Parameterized.Parameters(name = "{index}: user[{5}], pwd[{6}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + for (String userRoleAttribute : new String[] { "cn", null }) { + for (String roleSearch : new String[] { ROLE_SEARCH_A, ROLE_SEARCH_B, ROLE_SEARCH_C }) { + if (userRoleAttribute != null) { + addUsers(USER_PATTERN, null, null, roleSearch, ROLE_BASE, userRoleAttribute, parameterSets, 1); + addUsers(USER_PATTERN, null, null, roleSearch, ROLE_BASE, userRoleAttribute, parameterSets, 4); + addUsers(null, USER_SEARCH, USER_BASE, roleSearch, ROLE_BASE, userRoleAttribute, parameterSets, 1); + addUsers(null, USER_SEARCH, USER_BASE, roleSearch, ROLE_BASE, userRoleAttribute, parameterSets, 4); + } + } + parameterSets.add(new Object[] { "cn={0},ou=s\\;ub,ou=people,dc=example,dc=com", null, null, ROLE_SEARCH_A, + "{3},ou=people,dc=example,dc=com", "testsub", "test", new String[] { "TestGroup4" }, + userRoleAttribute, Integer.valueOf(1) }); + parameterSets.add(new Object[] { "cn={0},ou=s\\;ub,ou=people,dc=example,dc=com", null, null, ROLE_SEARCH_A, + "{3},ou=people,dc=example,dc=com", "testsub", "test", new String[] { "TestGroup4" }, + userRoleAttribute, Integer.valueOf(4) }); + } + return parameterSets; + } + + + private static void addUsers(String userPattern, String userSearch, String userBase, String roleSearch, + String roleBase, String userRoleAttribute, List parameterSets, int poolSize) { + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "test", "test", new String[] {"TestGroup"}, userRoleAttribute, Integer.valueOf(poolSize) }); + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "t;", "test", new String[] {"TestGroup"}, userRoleAttribute, Integer.valueOf(poolSize) }); + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "t*", "test", new String[] {"TestGroup"}, userRoleAttribute, Integer.valueOf(poolSize) }); + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "t=", "test", new String[] {"TestGroup*3"}, userRoleAttribute, Integer.valueOf(poolSize) }); + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "norole", "test", new String[0], userRoleAttribute, Integer.valueOf(poolSize) }); + // Bug 65373 + parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, + "<>+=\"#;,rrr", "<>+=\"#;,rrr", new String[0], userRoleAttribute, Integer.valueOf(poolSize) }); + } + + + @Parameter(0) + public String realmConfigUserPattern; + @Parameter(1) + public String realmConfigUserSearch; + @Parameter(2) + public String realmConfigUserBase; + @Parameter(3) + public String realmConfigRoleSearch; + @Parameter(4) + public String realmConfigRoleBase; + @Parameter(5) + public String username; + @Parameter(6) + public String credentials; + @Parameter(7) + public String[] groups; + @Parameter(8) + public String realmConfigUserRoleAttribute; + @Parameter(9) + public int poolSize; + + @Test + public void testAuthenication() throws Exception { + JNDIRealm realm = new JNDIRealm(); + realm.containerLog = LogFactory.getLog(TestJNDIRealmIntegration.class); + + realm.setConnectionURL("ldap://localhost:" + ldapServer.getListenPort()); + realm.setUserPattern(realmConfigUserPattern); + realm.setUserSearch(realmConfigUserSearch); + realm.setUserBase(realmConfigUserBase); + realm.setUserRoleAttribute(realmConfigUserRoleAttribute); + realm.setRoleName("cn"); + realm.setRoleBase(realmConfigRoleBase); + realm.setRoleSearch(realmConfigRoleSearch); + realm.setRoleNested(true); + realm.setConnectionPoolSize(poolSize); + + // If using pooling, simply try more to see what happens + for (int i = 0; i < poolSize; i++) { + GenericPrincipal p = (GenericPrincipal) realm.authenticate(username, credentials); + + Assert.assertNotNull(p); + Assert.assertEquals(username, p.name); + + Set actualGroups = new HashSet<>(Arrays.asList(p.getRoles())); + Set expectedGroups = new HashSet<>(Arrays.asList(groups)); + + Assert.assertEquals(expectedGroups.size(), actualGroups.size()); + Set tmp = new HashSet<>(); + tmp.addAll(expectedGroups); + tmp.removeAll(actualGroups); + Assert.assertEquals(0, tmp.size()); + } + } + + + @BeforeClass + public static void createLDAP() throws Exception { + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com"); + InetAddress localhost = InetAddress.getByName("localhost"); + InMemoryListenerConfig listenerConfig = + new InMemoryListenerConfig("localListener", localhost, 0, null, null, null); + config.setListenerConfigs(listenerConfig); + config.addAdditionalBindCredentials("cn=admin", "password"); + ldapServer = new InMemoryDirectoryServer(config); + + ldapServer.startListening(); + + try (LDAPConnection conn = ldapServer.getConnection()) { + + // Note: Only the DNs need attribute value escaping + AddRequest addBase = new AddRequest( + "dn: dc=example,dc=com", + "objectClass: top", + "objectClass: domain", + "dc: example"); + LDAPResult result = conn.processOperation(addBase); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addPeople = new AddRequest( + "dn: ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: organizationalUnit"); + result = conn.processOperation(addPeople); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTest = new AddRequest( + "dn: cn=test,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: test", + "sn: Test", + "userPassword: test"); + result = conn.processOperation(addUserTest); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTestSemicolon = new AddRequest( + "dn: cn=t\\;,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: t;", + "sn: Tsemicolon", + "userPassword: test"); + result = conn.processOperation(addUserTestSemicolon); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTestAsterisk = new AddRequest( + "dn: cn=t*,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: t*", + "sn: Tasterisk", + "userPassword: test"); + result = conn.processOperation(addUserTestAsterisk); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserTestEquals = new AddRequest( + "dn: cn=t\\=,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: t=", + "sn: Tequals", + "userPassword: test"); + result = conn.processOperation(addUserTestEquals); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addUserNoRole = new AddRequest( + "dn: cn=norole,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: person", + "objectClass: organizationalPerson", + "cn: norole", + "sn: No Role", + "userPassword: test"); + result = conn.processOperation(addUserNoRole); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addGroupTest = new AddRequest( + "dn: cn=TestGroup,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: groupOfNames", + "cn: TestGroup", + "member: cn=test,ou=people,dc=example,dc=com", + "member: cn=t\\;,ou=people,dc=example,dc=com", + "member: cn=t\\*,ou=people,dc=example,dc=com"); + result = conn.processOperation(addGroupTest); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + + AddRequest addGroupTest2 = new AddRequest( + "dn: cn=Test\\Group*3,ou=people,dc=example,dc=com", + "objectClass: top", + "objectClass: groupOfNames", + "cn: Test>Group*3", + "member: cn=Test\\+=\"#;,rrr", + "sn: Bug 65373", + "userPassword: <>+=\"#;,rrr"); + result = conn.processOperation(addUserBug65373); + Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); + } + } + + + @AfterClass + public static void destroyLDAP() { + ldapServer.shutDown(true); + } +} diff --git a/test/org/apache/catalina/realm/TestMemoryRealm.java b/test/org/apache/catalina/realm/TestMemoryRealm.java new file mode 100644 index 0000000..567277a --- /dev/null +++ b/test/org/apache/catalina/realm/TestMemoryRealm.java @@ -0,0 +1,38 @@ +/* + * 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.catalina.realm; + +import java.security.Principal; + +import org.junit.Assert; +import org.junit.Test; + +public class TestMemoryRealm { + + /** + * Unknown user triggers NPE. + */ + @Test + public void testBug56246() { + MemoryRealm memoryRealm = new MemoryRealm(); + memoryRealm.setCredentialHandler(new MessageDigestCredentialHandler()); + + Principal p = memoryRealm.authenticate("foo", "bar"); + + Assert.assertNull(p); + } +} diff --git a/test/org/apache/catalina/realm/TestMessageDigestCredentialHandler.java b/test/org/apache/catalina/realm/TestMessageDigestCredentialHandler.java new file mode 100644 index 0000000..2020da3 --- /dev/null +++ b/test/org/apache/catalina/realm/TestMessageDigestCredentialHandler.java @@ -0,0 +1,61 @@ +/* + * 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.catalina.realm; + +import java.security.NoSuchAlgorithmException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.security.ConcurrentMessageDigest; + +public class TestMessageDigestCredentialHandler { + + private static final String[] DIGESTS = new String[] {"MD5", "SHA-1", "SHA-512"}; + + private static final String PWD = "password"; + + static { + try { + ConcurrentMessageDigest.init("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + @Test + public void testGeneral() throws Exception { + for (String digest : DIGESTS) { + for (int saltLength = 0; saltLength < 20; saltLength++) { + for (int iterations = 1; iterations < 100; iterations += 10) { + doTest(digest, saltLength, iterations); + } + } + } + } + + private void doTest(String digest, int saltLength, int iterations) throws NoSuchAlgorithmException { + MessageDigestCredentialHandler mdch = new MessageDigestCredentialHandler(); + MessageDigestCredentialHandler verifier = new MessageDigestCredentialHandler(); + mdch.setAlgorithm(digest); + mdch.setIterations(iterations); + mdch.setSaltLength(saltLength); + verifier.setAlgorithm(digest); + String storedCredential = mdch.mutate(PWD); + Assert.assertTrue(verifier.matches(PWD, storedCredential)); + } +} diff --git a/test/org/apache/catalina/realm/TestRealmBase.java b/test/org/apache/catalina/realm/TestRealmBase.java new file mode 100644 index 0000000..3d1fde0 --- /dev/null +++ b/test/org/apache/catalina/realm/TestRealmBase.java @@ -0,0 +1,792 @@ +/* + * 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.catalina.realm; + +import java.io.IOException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.annotation.ServletSecurity; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.unittest.TesterRequest; +import org.apache.tomcat.unittest.TesterResponse; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +public class TestRealmBase { + + private static final String USER1 = "user1"; + private static final String USER2 = "user2"; + private static final String USER99 = "user99"; + private static final String PWD = "password"; + public static final String ROLE1 = "role1"; + private static final String ROLE2 = "role2"; + private static final String ROLE3 = "role3"; + private static final String ROLE99 = "role99"; + + // All digested passwords are the digested form of "password" + private static final String PWD_MD5 = "5f4dcc3b5aa765d61d8327deb882cf99"; + private static final String PWD_SHA = "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8"; + private static final String PWD_MD5_PREFIX = + "{MD5}X03MO1qnZdYdgyfeuILPmQ=="; + private static final String PWD_SHA_PREFIX = + "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="; + // Salt added to "password" is "salttoprotectpassword" + private static final String PWD_SSHA_PREFIX = + "{SSHA}oFLhvfQVqFykEWu8v1pPE6nN0QRzYWx0dG9wcm90ZWN0cGFzc3dvcmQ="; + + @Test + public void testDigestMD5() throws Exception { + doTestDigestDigestPasswords(PWD, "MD5", PWD_MD5); + } + + @Test + public void testDigestSHA() throws Exception { + doTestDigestDigestPasswords(PWD, "SHA", PWD_SHA); + } + + @Test + public void testDigestMD5Prefix() throws Exception { + doTestDigestDigestPasswords(PWD, "MD5", PWD_MD5_PREFIX); + } + + @Test + public void testDigestSHAPrefix() throws Exception { + doTestDigestDigestPasswords(PWD, "SHA", PWD_SHA_PREFIX); + } + + @Test + public void testDigestSSHAPrefix() throws Exception { + doTestDigestDigestPasswords(PWD, "SHA", PWD_SSHA_PREFIX); + } + + private void doTestDigestDigestPasswords(String password, + String digest, String digestedPassword) throws Exception { + Context context = new TesterContext(); + TesterMapRealm realm = new TesterMapRealm(); + realm.setContainer(context); + MessageDigestCredentialHandler ch = new MessageDigestCredentialHandler(); + ch.setAlgorithm(digest); + realm.setCredentialHandler(ch); + realm.start(); + + realm.addUser(USER1, digestedPassword); + + Principal p = realm.authenticate(USER1, password); + + Assert.assertNotNull(p); + Assert.assertEquals(USER1, p.getName()); + } + + @Test + public void testUserWithSingleRole() throws IOException { + List userRoles = new ArrayList<>(); + List constraintRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + // Configure this test + userRoles.add(ROLE1); + constraintRoles.add(ROLE1); + applicationRoles.add(ROLE1); + + doRoleTest(userRoles, constraintRoles, applicationRoles, true); + } + + + @Test + public void testUserWithNoRoles() throws IOException { + List userRoles = new ArrayList<>(); + List constraintRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + // Configure this test + constraintRoles.add(ROLE1); + applicationRoles.add(ROLE1); + + doRoleTest(userRoles, constraintRoles, applicationRoles, false); + } + + + @Test + public void testUserWithSingleRoleAndAllRoles() throws IOException { + List userRoles = new ArrayList<>(); + List constraintRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + // Configure this test + userRoles.add(ROLE1); + applicationRoles.add(ROLE1); + constraintRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + + doRoleTest(userRoles, constraintRoles, applicationRoles, true); + } + + + @Test + public void testUserWithoutNoRolesAndAllRoles() throws IOException { + List userRoles = new ArrayList<>(); + List constraintRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + // Configure this test + constraintRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + applicationRoles.add(ROLE1); + + doRoleTest(userRoles, constraintRoles, applicationRoles, false); + } + + + @Test + public void testAllRolesWithNoAppRole() throws IOException { + List userRoles = new ArrayList<>(); + List constraintRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + // Configure this test + userRoles.add(ROLE1); + constraintRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + + doRoleTest(userRoles, constraintRoles, applicationRoles, false); + } + + + @Test + public void testAllAuthenticatedUsers() throws IOException { + List userRoles = new ArrayList<>(); + List constraintRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + // Configure this test + constraintRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + + doRoleTest(userRoles, constraintRoles, applicationRoles, true); + } + + + @Test + public void testAllAuthenticatedUsersAsAppRoleNoUser() throws IOException { + List userRoles = new ArrayList<>(); + List constraintRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + // Configure this test + userRoles.add(ROLE1); + constraintRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + applicationRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + + doRoleTest(userRoles, constraintRoles, applicationRoles, false); + } + + + @Test + public void testAllAuthenticatedUsersAsAppRoleWithUser() + throws IOException { + List userRoles = new ArrayList<>(); + List constraintRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + // Configure this test + userRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + constraintRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + applicationRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + + doRoleTest(userRoles, constraintRoles, applicationRoles, true); + } + + + @Test + public void testNoAuthConstraint() throws IOException { + // No auth constraint == allow access for all + List applicationRoles = new ArrayList<>(); + + doRoleTest(null, null, applicationRoles, true); + } + + + /* + * The combining constraints tests are based on the scenarios described in + * section + */ + + @Test + public void testCombineConstraints01() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // User role is in first constraint + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE1); + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(ROLE2); + applicationRoles.add(ROLE1); + applicationRoles.add(ROLE2); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints02() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // User role is in last constraint + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE2); + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(ROLE2); + applicationRoles.add(ROLE1); + applicationRoles.add(ROLE2); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints03() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // User role is not in any constraint + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE3); + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(ROLE2); + applicationRoles.add(ROLE1); + applicationRoles.add(ROLE2); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, false); + } + + + @Test + public void testCombineConstraints04() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // * is any app role + // User role is not in any constraint + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE99); + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + applicationRoles.add(ROLE2); + applicationRoles.add(ROLE3); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, false); + } + + + @Test + public void testCombineConstraints05() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // * is any app role + // User role is a non-app constraint role + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE1); + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + applicationRoles.add(ROLE2); + applicationRoles.add(ROLE3); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints06() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // * is any app role + // User role is an app role + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE2); + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + applicationRoles.add(ROLE2); + applicationRoles.add(ROLE3); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints07() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // * is any app role + // User has no role + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + applicationRoles.add(ROLE2); + applicationRoles.add(ROLE3); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, false); + } + + + @Test + public void testCombineConstraints08() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // ** is any authenticated user + // User has no role + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + applicationRoles.add(ROLE2); + applicationRoles.add(ROLE3); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints09() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // ** is any authenticated user + // User has constraint role + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE1); + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + applicationRoles.add(ROLE2); + applicationRoles.add(ROLE3); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints10() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // ** is any authenticated user + // User has app role + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE2); + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + applicationRoles.add(ROLE2); + applicationRoles.add(ROLE3); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints11() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // ** is any authenticated user + // User is not authenticated + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + constraintOneRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + applicationRoles.add(ROLE2); + applicationRoles.add(ROLE3); + + doRoleTest(null, constraintOneRoles, constraintTwoRoles, + applicationRoles, false); + } + + + @Test + public void testCombineConstraints12() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // Constraint without role or implied role permits unauthenticated users + // User is not authenticated + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + constraintTwoRoles.add(ROLE1); + applicationRoles.add(ROLE1); + + doRoleTest(null, null, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints13() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // Constraint without role or implied role permits unauthenticated users + // User is not authenticated + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + applicationRoles.add(ROLE1); + + doRoleTest(null, null, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints14() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // Constraint without role or implied role permits unauthenticated users + // User is not authenticated + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + applicationRoles.add(ROLE1); + + doRoleTest(null, null, constraintTwoRoles, + applicationRoles, true); + } + + + @Test + public void testCombineConstraints15() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // Constraint with empty auth section prevents all access + // User has matching constraint role + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE1); + constraintTwoRoles.add(ROLE1); + applicationRoles.add(ROLE1); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, false); + } + + + @Test + public void testCombineConstraints16() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // Constraint with empty auth section prevents all access + // User has matching role + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_ROLES); + applicationRoles.add(ROLE1); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, false); + } + + + @Test + public void testCombineConstraints17() throws IOException { + // Allowed roles should be the union of the roles in the constraints + // Constraint with empty auth section prevents all access + // User matches all authenticated users + List userRoles = new ArrayList<>(); + List constraintOneRoles = new ArrayList<>(); + List constraintTwoRoles = new ArrayList<>(); + List applicationRoles = new ArrayList<>(); + + userRoles.add(ROLE1); + constraintTwoRoles.add(SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS); + applicationRoles.add(ROLE1); + + doRoleTest(userRoles, constraintOneRoles, constraintTwoRoles, + applicationRoles, false); + } + + + /** + * @param userRoles null tests unauthenticated access + * otherwise access is tested with an authenticated + * user with the listed roles + * @param constraintRoles null is equivalent to no auth + * constraint whereas an empty list is equivalent + * to an auth constraint that defines no roles. + */ + private void doRoleTest(List userRoles, + List constraintRoles, List applicationRoles, + boolean expected) throws IOException { + + List constraintTwoRoles = new ArrayList<>(); + constraintTwoRoles.add(ROLE99); + doRoleTest(userRoles, constraintRoles, constraintTwoRoles, + applicationRoles, expected); + } + + + private void doRoleTest(List userRoles, + List constraintOneRoles, List constraintTwoRoles, + List applicationRoles, boolean expected) + throws IOException { + + TesterMapRealm mapRealm = new TesterMapRealm(); + + // Configure the security constraints for the resource + SecurityConstraint constraintOne = new SecurityConstraint(); + if (constraintOneRoles != null) { + constraintOne.setAuthConstraint(true); + for (String constraintRole : constraintOneRoles) { + constraintOne.addAuthRole(constraintRole); + if (applicationRoles.contains( + SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS)) { + constraintOne.treatAllAuthenticatedUsersAsApplicationRole(); + } + } + } + SecurityConstraint constraintTwo = new SecurityConstraint(); + if (constraintTwoRoles != null) { + constraintTwo.setAuthConstraint(true); + for (String constraintRole : constraintTwoRoles) { + constraintTwo.addAuthRole(constraintRole); + if (applicationRoles.contains( + SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS)) { + constraintTwo.treatAllAuthenticatedUsersAsApplicationRole(); + } + } + } + SecurityConstraint[] constraints = + new SecurityConstraint[] { constraintOne, constraintTwo }; + + // Set up the mock request and response + Request request = new Request(null); + Response response = new TesterResponse(); + Context context = new TesterContext(); + for (String applicationRole : applicationRoles) { + context.addSecurityRole(applicationRole); + } + request.getMappingData().context = context; + + // Set up an authenticated user + // Configure the users in the Realm + if (userRoles != null) { + GenericPrincipal gp = new GenericPrincipal(USER1, userRoles); + request.setUserPrincipal(gp); + } + + // Check if user meets constraints + boolean result = mapRealm.hasResourcePermission( + request, response, constraints, null); + + Assert.assertEquals(Boolean.valueOf(expected), Boolean.valueOf(result)); + } + + + /* + * This test case covers the special case in section 13.4.1 of the Servlet + * 3.1 specification for {@link jakarta.servlet.annotation.HttpConstraint}. + */ + @Test + public void testHttpConstraint() throws IOException { + // Get the annotation from the test case + Class clazz = TesterServletSecurity01.class; + ServletSecurity servletSecurity = + clazz.getAnnotation(ServletSecurity.class); + + // Convert the annotation into constraints + ServletSecurityElement servletSecurityElement = + new ServletSecurityElement(servletSecurity); + SecurityConstraint[] constraints = + SecurityConstraint.createConstraints( + servletSecurityElement, "/*"); + + // Create a separate constraint that covers DELETE + SecurityConstraint deleteConstraint = new SecurityConstraint(); + deleteConstraint.addAuthRole(ROLE1); + SecurityCollection deleteCollection = new SecurityCollection(); + deleteCollection.addMethod("DELETE"); + deleteCollection.addPatternDecoded("/*"); + deleteConstraint.addCollection(deleteCollection); + + TesterMapRealm mapRealm = new TesterMapRealm(); + + // Set up the mock request and response + TesterRequest request = new TesterRequest(); + Response response = new TesterResponse(); + Context context = request.getContext(); + context.addSecurityRole(ROLE1); + context.addSecurityRole(ROLE2); + request.getMappingData().context = context; + + // Create the principals + List userRoles1 = new ArrayList<>(); + userRoles1.add(ROLE1); + GenericPrincipal gp1 = new GenericPrincipal(USER1, userRoles1); + + List userRoles2 = new ArrayList<>(); + userRoles2.add(ROLE2); + GenericPrincipal gp2 = new GenericPrincipal(USER2, userRoles2); + + List userRoles99 = new ArrayList<>(); + GenericPrincipal gp99 = new GenericPrincipal(USER99, userRoles99); + + // Add the constraints to the context + for (SecurityConstraint constraint : constraints) { + context.addConstraint(constraint); + } + context.addConstraint(deleteConstraint); + + // All users should be able to perform a GET + request.setMethod("GET"); + + SecurityConstraint[] constraintsGet = + mapRealm.findSecurityConstraints(request, context); + + request.setUserPrincipal(null); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsGet, null)); + request.setUserPrincipal(gp1); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsGet, null)); + request.setUserPrincipal(gp2); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsGet, null)); + request.setUserPrincipal(gp99); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsGet, null)); + + // Only user1 should be able to perform a POST as only that user has + // role1. + request.setMethod("POST"); + + SecurityConstraint[] constraintsPost = + mapRealm.findSecurityConstraints(request, context); + + request.setUserPrincipal(null); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsPost, null)); + request.setUserPrincipal(gp1); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsPost, null)); + request.setUserPrincipal(gp2); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsPost, null)); + request.setUserPrincipal(gp99); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsPost, null)); + + // Only users with application roles (role1 or role2 so user1 or user2) + // should be able to perform a PUT. + request.setMethod("PUT"); + + SecurityConstraint[] constraintsPut = + mapRealm.findSecurityConstraints(request, context); + + request.setUserPrincipal(null); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsPut, null)); + request.setUserPrincipal(gp1); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsPut, null)); + request.setUserPrincipal(gp2); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsPut, null)); + request.setUserPrincipal(gp99); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsPut, null)); + + // Any authenticated user should be able to perform a TRACE. + request.setMethod("TRACE"); + + SecurityConstraint[] constraintsTrace = + mapRealm.findSecurityConstraints(request, context); + + request.setUserPrincipal(null); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsTrace, null)); + request.setUserPrincipal(gp1); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsTrace, null)); + request.setUserPrincipal(gp2); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsTrace, null)); + request.setUserPrincipal(gp99); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsTrace, null)); + + // Only user1 should be able to perform a DELETE as only that user has + // role1. + request.setMethod("DELETE"); + + SecurityConstraint[] constraintsDelete = + mapRealm.findSecurityConstraints(request, context); + + request.setUserPrincipal(null); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsDelete, null)); + request.setUserPrincipal(gp1); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsDelete, null)); + request.setUserPrincipal(gp2); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsDelete, null)); + request.setUserPrincipal(gp99); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsDelete, null)); + } +} diff --git a/test/org/apache/catalina/realm/TestSecretKeyCredentialHandler.java b/test/org/apache/catalina/realm/TestSecretKeyCredentialHandler.java new file mode 100644 index 0000000..a48d4c4 --- /dev/null +++ b/test/org/apache/catalina/realm/TestSecretKeyCredentialHandler.java @@ -0,0 +1,84 @@ +/* + * 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.catalina.realm; + +import java.security.NoSuchAlgorithmException; + +import org.junit.Assert; +import org.junit.Test; + +public class TestSecretKeyCredentialHandler { + + private static final String[] ALGORITHMS = { "PBKDF2WithHmacSHA1", "PBEWithMD5AndDES" }; + private static final String[] PASSWORDS = { "password", "$!&#%!%@$#@*^$%&%%#!!*%$%&#@!^" }; + private static final int[] KEYLENGTHS = { 8, 111, 256 }; + private static final int[] SALTLENGTHS = { 1, 7, 12, 20 }; + private static final int[] ITERATIONS = { 1, 2111, 10000 }; + + @Test + public void testGeneral() throws Exception { + for (String digest : ALGORITHMS) { + for (String password : PASSWORDS) { + for (int saltLength : SALTLENGTHS) { + for (int iterations : ITERATIONS) { + for (int keyLength : KEYLENGTHS) { + doTest(password, digest, saltLength, iterations, keyLength, true); + } + } + } + } + } + } + + @Test + public void testZeroSalt() throws NoSuchAlgorithmException { + doTest(PASSWORDS[0], ALGORITHMS[0], 0, ITERATIONS[0], KEYLENGTHS[0], false); + } + + @Test + public void testZeroIterations() throws NoSuchAlgorithmException { + doTest(PASSWORDS[0], ALGORITHMS[0], SALTLENGTHS[0], 0, KEYLENGTHS[0], false); + } + + @Test + public void testZeroKeyLength() throws NoSuchAlgorithmException { + doTest(PASSWORDS[0], ALGORITHMS[0], SALTLENGTHS[0], ITERATIONS[0], 0, false); + } + + private void doTest(String password, String digest, int saltLength, int iterations, + int keyLength, boolean expectMatch) throws NoSuchAlgorithmException { + SecretKeyCredentialHandler pbech = new SecretKeyCredentialHandler(); + SecretKeyCredentialHandler verifier = new SecretKeyCredentialHandler(); + pbech.setAlgorithm(digest); + pbech.setIterations(iterations); + pbech.setSaltLength(saltLength); + pbech.setKeyLength(keyLength); + verifier.setAlgorithm(digest); + String storedCredential = pbech.mutate(password); + if (expectMatch) { + Assert.assertTrue( + "[" + digest + "] [" + saltLength + "] [" + iterations + "] [" + keyLength + "] [" + + password + "] [" + storedCredential + "]", + verifier.matches(password, storedCredential)); + } else { + Assert.assertFalse( + "[" + digest + "] [" + saltLength + "] [" + iterations + "] [" + keyLength + "] [" + + password + "] [" + storedCredential + "]", + verifier.matches(password, storedCredential)); + } + } +} diff --git a/test/org/apache/catalina/realm/TesterPrincipal.java b/test/org/apache/catalina/realm/TesterPrincipal.java new file mode 100644 index 0000000..94e03bc --- /dev/null +++ b/test/org/apache/catalina/realm/TesterPrincipal.java @@ -0,0 +1,66 @@ +/* + * 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.catalina.realm; + +import java.io.Serializable; +import java.security.Principal; + +public class TesterPrincipal implements Principal, Serializable { + + private static final long serialVersionUID = 1L; + + private final String name; + + public TesterPrincipal(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TesterPrincipal other = (TesterPrincipal) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } +} diff --git a/test/org/apache/catalina/realm/TesterPrincipalNonSerializable.java b/test/org/apache/catalina/realm/TesterPrincipalNonSerializable.java new file mode 100644 index 0000000..02b0cc0 --- /dev/null +++ b/test/org/apache/catalina/realm/TesterPrincipalNonSerializable.java @@ -0,0 +1,63 @@ +/* + * 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.catalina.realm; + +import java.security.Principal; + +public class TesterPrincipalNonSerializable implements Principal { + + private final String name; + + public TesterPrincipalNonSerializable(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TesterPrincipalNonSerializable other = (TesterPrincipalNonSerializable) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } +} diff --git a/test/org/apache/catalina/realm/TesterServletSecurity01.java b/test/org/apache/catalina/realm/TesterServletSecurity01.java new file mode 100644 index 0000000..1a1f9cd --- /dev/null +++ b/test/org/apache/catalina/realm/TesterServletSecurity01.java @@ -0,0 +1,35 @@ +/* + * 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.catalina.realm; + +import jakarta.servlet.annotation.HttpConstraint; +import jakarta.servlet.annotation.HttpMethodConstraint; +import jakarta.servlet.annotation.ServletSecurity; + +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +@ServletSecurity(value=@HttpConstraint, + httpMethodConstraints={ + @HttpMethodConstraint(value="POST", + rolesAllowed=TestRealmBase.ROLE1), + @HttpMethodConstraint(value="PUT", + rolesAllowed=SecurityConstraint.ROLE_ALL_ROLES), + @HttpMethodConstraint(value="TRACE", + rolesAllowed=SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS)}) +public class TesterServletSecurity01 { + // Class is NO-OP. It is only used to 'host' the annotation. +} diff --git a/test/org/apache/catalina/security/TestSecurityClassLoad.java b/test/org/apache/catalina/security/TestSecurityClassLoad.java new file mode 100644 index 0000000..a318486 --- /dev/null +++ b/test/org/apache/catalina/security/TestSecurityClassLoad.java @@ -0,0 +1,27 @@ +/* + * 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.catalina.security; + +import org.junit.Test; + +public class TestSecurityClassLoad { + + @Test + public void testLoad() throws Exception { + SecurityClassLoad.securityClassLoad(Thread.currentThread().getContextClassLoader(), false); + } +} diff --git a/test/org/apache/catalina/servlets/DefaultServletEncodingBaseTest.java b/test/org/apache/catalina/servlets/DefaultServletEncodingBaseTest.java new file mode 100644 index 0000000..740ff5c --- /dev/null +++ b/test/org/apache/catalina/servlets/DefaultServletEncodingBaseTest.java @@ -0,0 +1,294 @@ +/* + * 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.catalina.servlets; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.servlets.DefaultServlet.BomConfig; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.http.parser.MediaType; + +/* + * Note: This test is split using two base classes. This is because, as a single + * test, it takes so long to run it dominates the time taken to run the + * tests when running tests using multiple threads. For example, on a + * system with 12 cores, the tests take ~5 minutes per connector with this + * test as a single test and ~3.5 minutes per connector with this test + * split in two. + */ +@RunWith(Parameterized.class) +public abstract class DefaultServletEncodingBaseTest extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{index}: contextEnc[{0}], fileEnc[{1}], target[{2}]," + + " useInclude[{3}], outputEnc[{4}], callSetCharacterEnc[{5}], useWriter[{6}]") + public static Collection parameters() { + + String[] encodings = new String[] { + "utf-8", "ibm850", "cp1252", "iso-8859-1" }; + + String[] targetFiles = new String[] { + "cp1252", "ibm850", "iso-8859-1", "utf-8-bom", "utf-8" }; + + List parameterSets = new ArrayList<>(); + + for (String contextResponseEncoding : encodings) { + for (String fileEncoding : encodings) { + for (String targetFile : targetFiles) { + for (Boolean useInclude : booleans) { + if (useInclude.booleanValue()) { + for (String outputEncoding : encodings) { + for (Boolean callSetCharacterEncoding : booleans) { + for (Boolean useWriter : booleans) { + parameterSets.add(new Object[] { contextResponseEncoding, + fileEncoding, targetFile, + useInclude, outputEncoding, + callSetCharacterEncoding, useWriter }); + } + } + } + } else { + /* + * Not using include so ignore outputEncoding, + * callSetCharacterEncoding and useWriter + * + * Tests that do not use include are always expected to + * pass. + */ + String encoding = targetFile; + if (encoding.endsWith("-bom")) { + encoding = encoding.substring(0, encoding.length() - 4); + } + parameterSets.add(new Object[] { contextResponseEncoding, fileEncoding, + targetFile, useInclude, encoding, Boolean.FALSE, + Boolean.FALSE }); + } + } + } + } + } + + return parameterSets; + } + + + private static boolean getExpected(String fileEncoding, BomConfig bomConfig, String targetFile, + String outputEncoding, boolean callSetCharacterEncoding, boolean useWriter) { + if (targetFile.endsWith("-bom") && !bomConfig.stripBom) { + /* + * If the target file contains a BOM and the BOM is not stripped the + * test should always fail because BOM will be kept untouched + * and may result in a different character in the output + * + * We could differentiate iso-8859-1 and cp1252 but we would need logic here and in + * the asserts and that would make the logic in the test as complex as the code + * under test. + */ + return false; + } else if (useWriter || callSetCharacterEncoding) { + /* + * Using a writer or setting the output character encoding means the + * response will specify a character set. These cases therefore + * reduce to can the file be read with the correct encoding. + * (Assuming any BOM is always skipped in the included output.) + */ + if (targetFile.endsWith("-bom") && bomConfig.useBomEncoding || + targetFile.startsWith(fileEncoding) || + targetFile.equals("cp1252") && fileEncoding.equals("iso-8859-1") || + targetFile.equals("iso-8859-1") && fileEncoding.equals("cp1252")) { + return true; + } else { + return false; + } + } else if (!(targetFile.startsWith(outputEncoding) || + targetFile.equals("cp1252") && outputEncoding.equals("iso-8859-1") || + targetFile.equals("iso-8859-1") && outputEncoding.equals("cp1252"))) { + /* + * The non-writer use cases read the target file as bytes. These + * cases therefore reduce to can the bytes from the target file be + * included in the output without corruption? The character used in + * the tests has been chosen so that, apart from iso-8859-1 and + * cp1252, the bytes vary by character set. + * (Assuming any BOM is always skipped in the included output.) + */ + return false; + } else { + return true; + } + } + + + @Parameter(0) + public String contextResponseEncoding; + @Parameter(1) + public String fileEncoding; + @Parameter(2) + public String targetFile; + @Parameter(3) + public boolean useInclude; + @Parameter(4) + public String outputEncoding; + @Parameter(5) + public boolean callSetCharacterEncoding; + @Parameter(6) + public boolean useWriter; + + + protected abstract BomConfig getUseBom(); + + + @Test + public void testEncoding() throws Exception { + + boolean expectedPass = getExpected(fileEncoding, getUseBom(), targetFile, outputEncoding, + callSetCharacterEncoding, useWriter); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + ctxt.setResponseCharacterEncoding(contextResponseEncoding); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", DefaultServlet.class.getName()); + defaultServlet.addInitParameter("fileEncoding", fileEncoding); + defaultServlet.addInitParameter("useBomIfPresent", getUseBom().configurationValue); + + ctxt.addServletMappingDecoded("/", "default"); + + if (useInclude) { + Tomcat.addServlet(ctxt, "include", new EncodingServlet( + outputEncoding, callSetCharacterEncoding, targetFile, useWriter)); + ctxt.addServletMappingDecoded("/include", "include"); + } + + tomcat.start(); + + final ByteChunk res = new ByteChunk(); + Map> headers = new HashMap<>(); + + String target; + if (useInclude) { + target = "http://localhost:" + getPort() + "/include"; + } else { + target = "http://localhost:" + getPort() + "/bug49nnn/bug49464-" + targetFile + ".txt"; + } + int rc = getUrl(target, res, headers); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + String contentType = getSingleHeader("Content-Type", headers); + if (contentType != null) { + MediaType mediaType = MediaType.parseMediaType(new StringReader(contentType)); + String charset = mediaType.getCharset(); + if (charset == null) { + res.setCharset(B2CConverter.getCharset(outputEncoding)); + } else { + res.setCharset(B2CConverter.getCharset(charset)); + } + } else { + res.setCharset(B2CConverter.getCharset(outputEncoding)); + } + String body = res.toString(); + /* + * Remove BOM before checking content if DefaultServlet is configured to + * remove BOM. + */ + if (!useInclude && targetFile.endsWith("-bom") && getUseBom().stripBom) { + body = body.substring(1); + } + + if (expectedPass) { + if (useInclude) { + Assert.assertEquals("\u00bd-\u00bd-\u00bd", body); + } else { + Assert.assertEquals("\u00bd", body); + } + } else { + if (useInclude) { + Assert.assertNotEquals("\u00bd-\u00bd-\u00bd", body); + } else { + Assert.assertNotEquals("\u00bd", body); + } + } + } + + + private static class EncodingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final String outputEncoding; + private final boolean callSetCharacterEncoding; + private final String includeTarget; + private final boolean useWriter; + + EncodingServlet(String outputEncoding, boolean callSetCharacterEncoding, + String includeTarget, boolean useWriter) { + this.outputEncoding = outputEncoding; + this.callSetCharacterEncoding = callSetCharacterEncoding; + this.includeTarget = includeTarget; + this.useWriter = useWriter; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + if (callSetCharacterEncoding) { + resp.setCharacterEncoding(outputEncoding); + } + if (useWriter) { + PrintWriter pw = resp.getWriter(); + pw.print("\u00bd-"); + } else { + resp.getOutputStream().write("\u00bd-".getBytes(outputEncoding)); + } + resp.flushBuffer(); + RequestDispatcher rd = + req.getRequestDispatcher("/bug49nnn/bug49464-" + includeTarget + ".txt"); + rd.include(req, resp); + if (useWriter) { + PrintWriter pw = resp.getWriter(); + pw.print("-\u00bd"); + } else { + resp.getOutputStream().write("-\u00bd".getBytes(outputEncoding)); + } + } + } +} diff --git a/test/org/apache/catalina/servlets/ServletOptionsBaseTest.java b/test/org/apache/catalina/servlets/ServletOptionsBaseTest.java new file mode 100644 index 0000000..d23bbb8 --- /dev/null +++ b/test/org/apache/catalina/servlets/ServletOptionsBaseTest.java @@ -0,0 +1,164 @@ +/* + * 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.catalina.servlets; + + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import jakarta.servlet.Servlet; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runners.Parameterized.Parameter; + +import static org.apache.catalina.startup.SimpleHttpClient.CRLF; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.scan.StandardJarScanner; + +public abstract class ServletOptionsBaseTest extends TomcatBaseTest { + + protected static final String COLLECTION_NAME = "collection"; + protected static final String FILE_NAME = "file"; + protected static final String UNKNOWN_NAME = "unknown"; + + @Parameter(0) + public boolean listings; + + @Parameter(1) + public boolean readonly; + + @Parameter(2) + public boolean trace; + + @Parameter(3) + public String url; + + @Parameter(4) + public String method; + + + /* + * Check that methods returned by OPTIONS are consistent with the return + * http status code. + * Method not present in options response -> 405 expected + * Method present in options response -> anything other than 405 expected + */ + @Test + public void testOptions() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + tomcat.getConnector().setAllowTrace(trace); + + File docBase = new File(getTemporaryDirectory(), "webdav"); + File collection = new File(docBase, COLLECTION_NAME); + Assert.assertTrue(collection.mkdirs()); + File file = new File(docBase, FILE_NAME); + Assert.assertTrue(file.createNewFile()); + + addDeleteOnTearDown(docBase); + + // app dir is relative to server home + org.apache.catalina.Context ctx = + tomcat.addWebapp(null, "/webdav", docBase.getAbsolutePath()); + + Wrapper w = Tomcat.addServlet(ctx, "servlet", createServlet()); + w.addInitParameter("listings", Boolean.toString(listings)); + w.addInitParameter("readonly", Boolean.toString(readonly)); + + ctx.addServletMappingDecoded("/*", "servlet"); + + // Disable class path scanning - it slows the tests down by almost an order of magnitude + ((StandardJarScanner) ctx.getJarScanner()).setScanClassPath(false); + + tomcat.start(); + + OptionsHttpClient client = new OptionsHttpClient(); + client.setPort(getPort()); + client.setRequest(new String[] { + "OPTIONS /webdav/" + url + " HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Connection: close" + CRLF + + CRLF }); + + client.connect(); + client.processRequest(); + + Assert.assertTrue(client.getResponseLine(), client.isResponse200()); + Set allowed = client.getAllowedMethods(); + + client.disconnect(); + client.reset(); + + client.setRequest(new String[] { + method + " /webdav/" + url + " HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Connection: close" + CRLF + + CRLF }); + + client.connect(); + client.processRequest(); + + String msg = "Listings[" + listings + "], readonly [" + readonly + + "], trace[ " + trace + "], url[" + url + "], method[" + method + "]"; + + Assert.assertNotNull(client.getResponseLine()); + + if (allowed.contains(method)) { + Assert.assertFalse(msg, client.isResponse405()); + } else { + Assert.assertTrue(msg, client.isResponse405()); + allowed = client.getAllowedMethods(); + Assert.assertFalse(msg, allowed.contains(method)); + } + } + + + protected abstract Servlet createServlet(); + + + private static class OptionsHttpClient extends SimpleHttpClient { + + @Override + public boolean isResponseBodyOK() { + return true; + } + + public Set getAllowedMethods() { + String valueList = null; + for (String header : getResponseHeaders()) { + if (header.startsWith("Allow:")) { + valueList = header.substring(6).trim(); + break; + } + } + Assert.assertNotNull(valueList); + String[] values = valueList.split(","); + for (int i = 0; i < values.length; i++) { + values[i] = values[i].trim(); + } + Set allowed = new HashSet<>(Arrays.asList(values)); + + return allowed; + } + } +} diff --git a/test/org/apache/catalina/servlets/TestCGIServletCmdLineArguments.java b/test/org/apache/catalina/servlets/TestCGIServletCmdLineArguments.java new file mode 100644 index 0000000..37e241f --- /dev/null +++ b/test/org/apache/catalina/servlets/TestCGIServletCmdLineArguments.java @@ -0,0 +1,102 @@ +/* + * 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.catalina.servlets; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.tomcat.util.compat.JrePlatform; + +@RunWith(Parameterized.class) +public class TestCGIServletCmdLineArguments { + + private static final Pattern defaultDecodedPatternWindows; + + static { + /* + * There are various ways to handle the platform specific behaviour + * here. This was chosen as it is simple and the tests are run on + * Windows as part of every release cycle. + */ + defaultDecodedPatternWindows = Pattern.compile("[\\w\\Q-.\\/:\\E]+"); + + if (JrePlatform.IS_WINDOWS) { + Pattern p = null; + try { + CGIServlet cgiServlet = new CGIServlet(); + Field f = CGIServlet.class.getDeclaredField("cmdLineArgumentsDecodedPattern"); + f.setAccessible(true); + p = (Pattern) f.get(cgiServlet); + } catch (IllegalAccessException | NoSuchFieldException | SecurityException e) { + } + + Assert.assertNotNull(p); + Assert.assertEquals(defaultDecodedPatternWindows.toString(), p.toString()); + } + } + + @Parameterized.Parameters(name = "{index}: argument[{0}], allowed[{1}]") + public static Collection parameters() { + List params = new ArrayList<>(); + params.add(new Object[] { "", Boolean.FALSE } ); + params.add(new Object[] { "<", Boolean.FALSE } ); + params.add(new Object[] { "\"", Boolean.FALSE } ); + params.add(new Object[] { "'", Boolean.FALSE } ); + params.add(new Object[] { "|", Boolean.FALSE } ); + params.add(new Object[] { ">", Boolean.FALSE } ); + + params.add(new Object[] { ".", Boolean.TRUE } ); + params.add(new Object[] { "-p", Boolean.TRUE } ); + params.add(new Object[] { "--p", Boolean.TRUE } ); + params.add(new Object[] { "/p", Boolean.TRUE } ); + params.add(new Object[] { "abc", Boolean.TRUE } ); + params.add(new Object[] { "a_b_c", Boolean.TRUE } ); + params.add(new Object[] { "123", Boolean.TRUE } ); + params.add(new Object[] { "file.txt", Boolean.TRUE } ); + params.add(new Object[] { "C:\\some\\path\\file.txt", Boolean.TRUE } ); + + params.add(new Object[] { "file.txt file2.txt", Boolean.FALSE } ); + params.add(new Object[] { "C:\\some\\path with space\\file.txt", Boolean.FALSE } ); + params.add(new Object[] { "\"C:\\some\\quoted path with space\\file.txt\"", Boolean.FALSE } ); + + return params; + } + + @Parameter(0) + public String argument; + + @Parameter(1) + public Boolean allowed; + + @Test + public void test() { + if (allowed.booleanValue()) { + Assert.assertTrue(defaultDecodedPatternWindows.matcher(argument).matches()); + } else { + Assert.assertFalse(defaultDecodedPatternWindows.matcher(argument).matches()); + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/servlets/TestDefaultServlet.java b/test/org/apache/catalina/servlets/TestDefaultServlet.java new file mode 100644 index 0000000..c8321ce --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServlet.java @@ -0,0 +1,674 @@ +/* + * 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.catalina.servlets; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import static org.apache.catalina.startup.SimpleHttpClient.CRLF; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.websocket.server.WsContextListener; + +public class TestDefaultServlet extends TomcatBaseTest { + + /* + * Test attempting to access special paths (WEB-INF/META-INF) using + * DefaultServlet. + */ + @Test + public void testGetSpecials() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + String contextPath = "/examples"; + + File appDir = new File(getBuildDirectory(), "webapps" + contextPath); + // app dir is relative to server home + tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + + tomcat.start(); + + final ByteChunk res = new ByteChunk(); + + int rc =getUrl("http://localhost:" + getPort() + contextPath + + "/WEB-INF/web.xml", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/WEB-INF/doesntexistanywhere", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/WEB-INF/", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/META-INF/MANIFEST.MF", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/META-INF/doesntexistanywhere", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + } + + @Test + public void testDefaultCompression() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + ((AbstractHttp11Protocol) tomcat.getConnector().getProtocolHandler()).setCompression("force"); + + File appDir = new File("test/webapp"); + + // app dir is relative to server home + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", + "org.apache.catalina.servlets.DefaultServlet"); + defaultServlet.addInitParameter("fileEncoding", "ISO-8859-1"); + ctxt.addServletMappingDecoded("/", "default"); + + ctxt.addMimeMapping("html", "text/html"); + + tomcat.start(); + + TestCompressedClient gzipClient = new TestCompressedClient(getPort()); + + gzipClient.reset(); + gzipClient.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: gzip" + CRLF + CRLF }); + gzipClient.connect(); + gzipClient.processRequest(); + Assert.assertTrue(gzipClient.isResponse200()); + List responseHeaders = gzipClient.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Encoding: gzip")); + for (String header : responseHeaders) { + Assert.assertFalse(header.startsWith("Content-Length: ")); + } + } + + /* + * Verify serving of gzipped resources from context root. + */ + @Test + public void testGzippedFile() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + + File gzipIndex = new File(appDir, "index.html.gz"); + long gzipSize = gzipIndex.length(); + + File index = new File(appDir, "index.html"); + long indexSize = index.length(); + + // app dir is relative to server home + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", + "org.apache.catalina.servlets.DefaultServlet"); + defaultServlet.addInitParameter("gzip", "true"); + defaultServlet.addInitParameter("fileEncoding", "ISO-8859-1"); + ctxt.addServletMappingDecoded("/", "default"); + + ctxt.addMimeMapping("html", "text/html"); + + tomcat.start(); + + TestCompressedClient gzipClient = new TestCompressedClient(getPort()); + + gzipClient.reset(); + gzipClient.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: gzip, br" + CRLF + CRLF }); + gzipClient.connect(); + gzipClient.processRequest(); + Assert.assertTrue(gzipClient.isResponse200()); + List responseHeaders = gzipClient.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Encoding: gzip")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + gzipSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + + gzipClient.reset(); + gzipClient.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF+ CRLF }); + gzipClient.connect(); + gzipClient.processRequest(); + Assert.assertTrue(gzipClient.isResponse200()); + responseHeaders = gzipClient.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Type: text/html")); + Assert.assertFalse(responseHeaders.contains("Content-Encoding: gzip")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + indexSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + } + + /* + * Verify serving of brotli compressed resources from context root. + */ + @Test + public void testBrotliCompressedFile() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + + long brSize = new File(appDir, "index.html.br").length(); + long indexSize = new File(appDir, "index.html").length(); + + // app dir is relative to server home + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", + "org.apache.catalina.servlets.DefaultServlet"); + defaultServlet.addInitParameter("precompressed", "true"); + defaultServlet.addInitParameter("fileEncoding", "ISO-8859-1"); + + ctxt.addServletMappingDecoded("/", "default"); + ctxt.addMimeMapping("html", "text/html"); + + tomcat.start(); + + TestCompressedClient client = new TestCompressedClient(getPort()); + + client.reset(); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: br, gzip" + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + List responseHeaders = client.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Encoding: br")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + brSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + + client.reset(); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF+ CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + responseHeaders = client.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Type: text/html")); + Assert.assertFalse(responseHeaders.contains("Content-Encoding")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + indexSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + } + + /* + * Verify serving of custom compressed resources from context root. + */ + @Test + public void testCustomCompressedFile() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + + long brSize = new File(appDir, "index.html.br").length(); + long gzSize = new File(appDir, "index.html.gz").length(); + + // app dir is relative to server home + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", + DefaultServlet.class.getName()); + defaultServlet.addInitParameter("precompressed", "gzip=.gz,custom=.br"); + + ctxt.addServletMappingDecoded("/", "default"); + ctxt.addMimeMapping("html", "text/html"); + + tomcat.start(); + + TestCompressedClient client = new TestCompressedClient(getPort()); + + client.reset(); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: br, gzip ; q = 0.5 , custom" + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + List responseHeaders = client.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Encoding: custom")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + brSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + + client.reset(); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: br;q=1,gzip,custom" + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + responseHeaders = client.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Encoding: gzip")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + gzSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + } + + /* + * Verify that "*" and "identity" values are handled correctly in accept-encoding header. + */ + @Test + public void testIdentityAndStarAcceptEncodings() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + + long brSize = new File(appDir, "index.html.br").length(); + long indexSize = new File(appDir, "index.html").length(); + + // app dir is relative to server home + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", + DefaultServlet.class.getName()); + defaultServlet.addInitParameter("precompressed", "br=.br,gzip=.gz"); + defaultServlet.addInitParameter("fileEncoding", "ISO-8859-1"); + + ctxt.addServletMappingDecoded("/", "default"); + ctxt.addMimeMapping("html", "text/html"); + + tomcat.start(); + + TestCompressedClient client = new TestCompressedClient(getPort()); + + client.reset(); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: gzip;q=0.9,*" + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + List responseHeaders = client.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Encoding: br")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + brSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + + client.reset(); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: gzip;q=0.9,br;q=0,identity," + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + responseHeaders = client.getResponseHeaders(); + Assert.assertFalse(responseHeaders.contains("Content-Encoding")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + indexSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + } + + /* + * Verify preferring of brotli in default configuration for actual Firefox and Chrome requests. + */ + @Test + public void testBrotliPreference() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + + long brSize = new File(appDir, "index.html.br").length(); + + // app dir is relative to server home + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", + DefaultServlet.class.getName()); + defaultServlet.addInitParameter("precompressed", "true"); + + ctxt.addServletMappingDecoded("/", "default"); + ctxt.addMimeMapping("html", "text/html"); + + tomcat.start(); + + TestCompressedClient client = new TestCompressedClient(getPort()); + + // Firefox 45 Accept-Encoding + client.reset(); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: gzip, deflate, br" + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + List responseHeaders = client.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Encoding: br")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + brSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + + // Chrome 50 Accept-Encoding + client.reset(); + client.setRequest(new String[] { + "GET /index.html HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: Close" + CRLF + + "Accept-Encoding: gzip, deflate, sdch, br" + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + responseHeaders = client.getResponseHeaders(); + Assert.assertTrue(responseHeaders.contains("Content-Encoding: br")); + Assert.assertTrue(responseHeaders.contains("Content-Length: " + brSize)); + Assert.assertTrue(responseHeaders.contains("vary: accept-encoding")); + } + + /* + * Test https://bz.apache.org/bugzilla/show_bug.cgi?id=50026 + * Verify serving of resources from context root with subpath mapping. + */ + @Test + public void testGetWithSubpathmount() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + String contextPath = "/examples"; + + File appDir = new File(getBuildDirectory(), "webapps" + contextPath); + // app dir is relative to server home + Context ctx = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + ctx.addApplicationListener(WsContextListener.class.getName()); + + // Override the default servlet with our own mappings + Tomcat.addServlet(ctx, "default2", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default2"); + ctx.addServletMappingDecoded("/servlets/*", "default2"); + ctx.addServletMappingDecoded("/static/*", "default2"); + + tomcat.start(); + + final ByteChunk res = new ByteChunk(); + + // Make sure DefaultServlet isn't exposing special directories + // by remounting the webapp under a sub-path + + int rc =getUrl("http://localhost:" + getPort() + contextPath + + "/static/WEB-INF/web.xml", res, null); + + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/static/WEB-INF/doesntexistanywhere", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/static/WEB-INF/", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/static/META-INF/MANIFEST.MF", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/static/META-INF/doesntexistanywhere", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + // Make sure DefaultServlet is serving resources relative to the + // context root regardless of where the it is mapped + + final ByteChunk rootResource = new ByteChunk(); + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/index.html", rootResource, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + final ByteChunk subpathResource = new ByteChunk(); + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/servlets/index.html", subpathResource, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + Assert.assertFalse(rootResource.toString().equals(subpathResource.toString())); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/static/index.html", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + } + + /* + * Test https://bz.apache.org/bugzilla/show_bug.cgi?id=50413 Serving a + * custom error page + */ + @Test + public void testCustomErrorPage() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + + // app dir is relative to server home + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", + DefaultServlet.class.getName()); + defaultServlet.addInitParameter("fileEncoding", "ISO-8859-1"); + + ctxt.addServletMappingDecoded("/", "default"); + ctxt.addMimeMapping("html", "text/html"); + ErrorPage ep = new ErrorPage(); + ep.setErrorCode(404); + ep.setLocation("/404.html"); + ctxt.addErrorPage(ep); + + tomcat.start(); + + TestCustomErrorClient client = + new TestCustomErrorClient(tomcat.getConnector().getLocalPort()); + + client.reset(); + client.setRequest(new String[] { + "GET /MyApp/missing HTTP/1.0" +CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse404()); + Assert.assertEquals("It is 404.html", client.getResponseBody()); + + SimpleDateFormat format = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + String tomorrow = format.format(new Date(System.currentTimeMillis() + + 24 * 60 * 60 * 1000)); + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=50413 + // + client.reset(); + client.setRequest(new String[] { + "GET /MyApp/missing HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: close" + CRLF + + "If-Modified-Since: " + tomorrow + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse404()); + Assert.assertEquals("It is 404.html", client.getResponseBody()); + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=50413#c6 + // + client.reset(); + client.setRequest(new String[] { + "GET /MyApp/missing HTTP/1.1" + CRLF + + "Host: localhost" + CRLF + + "Connection: close" + CRLF + + "Range: bytes=0-100" + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse404()); + Assert.assertEquals("It is 404.html", client.getResponseBody()); + } + + /* + * Test what happens if a custom 404 page is configured, + * but its file is actually missing. + */ + @Test + public void testCustomErrorPageMissing() throws Exception { + File appDir = new File(getTemporaryDirectory(), "MyApp"); + File webInf = new File(appDir, "WEB-INF"); + addDeleteOnTearDown(appDir); + if (!webInf.mkdirs() && !webInf.isDirectory()) { + Assert.fail("Unable to create directory [" + webInf + "]"); + } + + File webxml = new File(appDir, "WEB-INF/web.xml"); + try (FileOutputStream fos = new FileOutputStream(webxml); + Writer w = new OutputStreamWriter(fos, "UTF-8")) { + w.write("\n" + + "\n" + + "\n404\n" + + "/404-absent.html\n\n" + + "\n"); + } + + Tomcat tomcat = getTomcatInstance(); + String contextPath = "/MyApp"; + tomcat.addWebapp(null, contextPath, appDir.getAbsolutePath()); + tomcat.start(); + + TestCustomErrorClient client = + new TestCustomErrorClient(tomcat.getConnector().getLocalPort()); + + client.reset(); + client.setRequest(new String[] { + "GET /MyApp/missing HTTP/1.0" + CRLF + CRLF }); + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse404()); + } + + /* + * Verifies that the same Content-Length is returned for both GET and HEAD + * operations when a static resource served by the DefaultServlet is + * included. + */ + @Test + public void testBug57601() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, true); + + Map> resHeaders= new HashMap<>(); + String path = "http://localhost:" + getPort() + "/test/bug5nnnn/bug57601.jsp"; + ByteChunk out = new ByteChunk(); + + int rc = getUrl(path, out, resHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + String length = resHeaders.get("Content-Length").get(0); + Assert.assertEquals(Long.parseLong(length), out.getLength()); + out.recycle(); + + rc = headUrl(path, out, resHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(0, out.getLength()); + Assert.assertEquals(length, resHeaders.get("Content-Length").get(0)); + + tomcat.stop(); + } + + public static int getUrl(String path, ByteChunk out, + Map> resHead) throws IOException { + out.recycle(); + return TomcatBaseTest.getUrl(path, out, resHead); + } + + private static class TestCustomErrorClient extends SimpleHttpClient { + + TestCustomErrorClient(int port) { + setPort(port); + } + + @Override + public boolean isResponseBodyOK() { + return true; + } + } + + private static class TestCompressedClient extends SimpleHttpClient { + + TestCompressedClient(int port) { + setPort(port); + } + + @Override + public boolean isResponseBodyOK() { + return true; + } + } + + /* + * Bug 66609 + */ + @Test + public void testXmlDirectoryListing() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", new DefaultServlet()); + defaultServlet.addInitParameter("listings", "true"); + defaultServlet.addInitParameter("localXsltFile", "_listing.xslt"); + + ctxt.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + Map> resHeaders= new HashMap<>(); + String path = "http://localhost:" + getPort() + "/bug66609/"; + ByteChunk out = new ByteChunk(); + + int rc = getUrl(path, out, resHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } +} diff --git a/test/org/apache/catalina/servlets/TestDefaultServletEncodingPassThroughBom.java b/test/org/apache/catalina/servlets/TestDefaultServletEncodingPassThroughBom.java new file mode 100644 index 0000000..48006cd --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServletEncodingPassThroughBom.java @@ -0,0 +1,27 @@ +/* + * 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.catalina.servlets; + +import org.apache.catalina.servlets.DefaultServlet.BomConfig; + +public class TestDefaultServletEncodingPassThroughBom extends DefaultServletEncodingBaseTest { + + @Override + protected BomConfig getUseBom() { + return BomConfig.PASS_THROUGH; + } +} diff --git a/test/org/apache/catalina/servlets/TestDefaultServletEncodingWithBom.java b/test/org/apache/catalina/servlets/TestDefaultServletEncodingWithBom.java new file mode 100644 index 0000000..dccbfd3 --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServletEncodingWithBom.java @@ -0,0 +1,27 @@ +/* + * 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.catalina.servlets; + +import org.apache.catalina.servlets.DefaultServlet.BomConfig; + +public class TestDefaultServletEncodingWithBom extends DefaultServletEncodingBaseTest { + + @Override + protected BomConfig getUseBom() { + return BomConfig.TRUE; + } +} diff --git a/test/org/apache/catalina/servlets/TestDefaultServletEncodingWithoutBom.java b/test/org/apache/catalina/servlets/TestDefaultServletEncodingWithoutBom.java new file mode 100644 index 0000000..0d6bae0 --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServletEncodingWithoutBom.java @@ -0,0 +1,27 @@ +/* + * 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.catalina.servlets; + +import org.apache.catalina.servlets.DefaultServlet.BomConfig; + +public class TestDefaultServletEncodingWithoutBom extends DefaultServletEncodingBaseTest { + + @Override + protected BomConfig getUseBom() { + return BomConfig.FALSE; + } +} diff --git a/test/org/apache/catalina/servlets/TestDefaultServletIfMatchRequests.java b/test/org/apache/catalina/servlets/TestDefaultServletIfMatchRequests.java new file mode 100644 index 0000000..0e6665e --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServletIfMatchRequests.java @@ -0,0 +1,204 @@ +/* + * 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.catalina.servlets; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.WebResource; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +@RunWith(Parameterized.class) +public class TestDefaultServletIfMatchRequests extends TomcatBaseTest { + + private static final Integer RC_200 = Integer.valueOf(200); + private static final Integer RC_304 = Integer.valueOf(304); + private static final Integer RC_400 = Integer.valueOf(400); + private static final Integer RC_412 = Integer.valueOf(412); + + private static final String[] CONCAT = new String[] { ",", " ,", ", ", " , " }; + private static String resourceETagStrong; + private static String resourceETagWeak; + + @Parameterized.Parameters(name = "{index} resource-strong [{0}], matchHeader [{1}]") + public static Collection parameters() { + + // Get the length of the file used for this test + // It varies by platform due to line-endings + File index = new File("test/webapp/index.html"); + resourceETagStrong = "\"" + index.length() + "-" + index.lastModified() + "\""; + resourceETagWeak = "W/" + resourceETagStrong; + + String otherETagStrong = "\"123456789\""; + String otherETagWeak = "W/\"123456789\""; + + List parameterSets = new ArrayList<>(); + + for (Boolean resourceWithStrongETag : booleans) { + // No match header + parameterSets.add(new Object[] { resourceWithStrongETag, null, RC_200, RC_200 }); + + // match header is invalid + parameterSets.add(new Object[] { resourceWithStrongETag, "", RC_400, RC_400 }); + parameterSets.add(new Object[] { resourceWithStrongETag, "W", RC_400, RC_400 }); + parameterSets.add(new Object[] { resourceWithStrongETag, "W/", RC_400, RC_400 }); + parameterSets.add(new Object[] { resourceWithStrongETag, "w/" + resourceETagStrong, RC_400, RC_400 }); + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagStrong + " x", RC_400, RC_400 }); + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagStrong + "x", RC_400, RC_400 }); + for (String concat : CONCAT) { + parameterSets.add(new Object[] { resourceWithStrongETag, concat + resourceETagStrong, RC_400, RC_400 }); + parameterSets.add(new Object[] { resourceWithStrongETag, concat + resourceETagWeak, RC_400, RC_400 }); + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagStrong + concat, RC_400, RC_400 }); + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagWeak + concat, RC_400, RC_400 }); + } + + // match header always matches resource (leading and trailing space should be ignored) + parameterSets.add(new Object[] { resourceWithStrongETag, "*", RC_200, RC_304 }); + parameterSets.add(new Object[] { resourceWithStrongETag, " *", RC_200, RC_304 }); + parameterSets.add(new Object[] { resourceWithStrongETag, "* ", RC_200, RC_304 }); + + // match header never matches resource + parameterSets.add(new Object[] { resourceWithStrongETag, otherETagStrong, RC_412, RC_200 }); + parameterSets.add(new Object[] { resourceWithStrongETag, otherETagWeak, RC_412, RC_200 }); + + // match header includes weak tag + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagWeak, RC_412, RC_304 }); + for (String concat : CONCAT) { + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagWeak + concat + otherETagWeak, + RC_412, RC_304 }); + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagWeak + concat + otherETagStrong, + RC_412, RC_304 }); + parameterSets.add(new Object[] { resourceWithStrongETag, otherETagWeak + concat + resourceETagWeak, + RC_412, RC_304 }); + parameterSets.add(new Object[] { resourceWithStrongETag, otherETagStrong + concat + resourceETagWeak, + RC_412, RC_304 }); + } + + // match header includes strong entity tag + // If-Match result depends on whether the resource has a strong entity tag + Integer rcIfMatch; + if (resourceWithStrongETag.booleanValue()) { + rcIfMatch = RC_200; + } else { + rcIfMatch = RC_412; + } + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagStrong, rcIfMatch, RC_304 }); + for (String concat : CONCAT) { + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagStrong + concat + otherETagWeak, + rcIfMatch, RC_304 }); + parameterSets.add(new Object[] { resourceWithStrongETag, resourceETagStrong + concat + otherETagStrong, + rcIfMatch, RC_304 }); + parameterSets.add(new Object[] { resourceWithStrongETag, otherETagWeak + concat + resourceETagStrong, + rcIfMatch, RC_304 }); + parameterSets.add(new Object[] { resourceWithStrongETag, otherETagStrong + concat + resourceETagStrong, + rcIfMatch, RC_304 }); + } + } + + return parameterSets; + } + + @Parameter(0) + public boolean resourceHasStrongETag; + + @Parameter(1) + public String matchHeader; + + @Parameter(2) + public int ifMatchResponseCode; + + @Parameter(3) + public int ifNoneMatchResponseCode; + + @Test + public void testIfMatch() throws Exception { + doMatchTest("If-Match", ifMatchResponseCode, false); + } + + + @Test + public void testIfNoneMatch() throws Exception { + doMatchTest("If-None-Match", ifNoneMatchResponseCode, true); + } + + + private void doMatchTest(String headerName, int responseCodeExpected, boolean responseHasEtag) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + + if (resourceHasStrongETag) { + Tomcat.addServlet(ctxt, "default", DefaultWithStrongETag.class.getName()); + } else { + Tomcat.addServlet(ctxt, "default", DefaultServlet.class.getName()); + } + ctxt.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + // Set up parameters + String path = "http://localhost:" + getPort() + "/index.html"; + ByteChunk responseBody = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + + Map> requestHeaders = null; + if (matchHeader != null) { + List values = Collections.singletonList(matchHeader); + requestHeaders = Collections.singletonMap(headerName, values); + } + + int rc = getUrl(path, responseBody, requestHeaders, responseHeaders); + + // Check the result + Assert.assertEquals(responseCodeExpected, rc); + + // If-None-Match should have a real resource ETag in successful response + if (responseHasEtag && (rc == 200 || rc == 304)) { + System.out.println(responseHeaders); + String responseEtag = responseHeaders.get("ETag").get(0); + Assert.assertEquals(resourceHasStrongETag ? resourceETagStrong : resourceETagWeak, responseEtag); + } + } + + + public static class DefaultWithStrongETag extends DefaultServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected String generateETag(WebResource resource) { + String weakETag = super.generateETag(resource); + // Make it a strong ETag + return weakETag.substring(2); + } + } +} diff --git a/test/org/apache/catalina/servlets/TestDefaultServletOptions.java b/test/org/apache/catalina/servlets/TestDefaultServletOptions.java new file mode 100644 index 0000000..b5fe61a --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServletOptions.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.servlets; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.Servlet; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TestDefaultServletOptions extends ServletOptionsBaseTest { + + @Parameters + public static Collection inputs() { + String[] urls = new String[] { COLLECTION_NAME, FILE_NAME, UNKNOWN_NAME }; + String[] methods = new String[] { "GET", "POST", "HEAD", "TRACE", "PUT", "DELETE" }; + + List result = new ArrayList<>(); + + for (Boolean listingsValue : booleans) { + for (Boolean readOnlyValue : booleans) { + for (Boolean traceValue : booleans) { + for (String url : urls) { + for (String method : methods) { + result.add(new Object[] { + listingsValue, readOnlyValue, traceValue, url, method } ); + } + } + } + } + + } + return result; + } + + + @Override + protected Servlet createServlet() { + return new DefaultServlet(); + } +} diff --git a/test/org/apache/catalina/servlets/TestDefaultServletPut.java b/test/org/apache/catalina/servlets/TestDefaultServletPut.java new file mode 100644 index 0000000..7426297 --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServletPut.java @@ -0,0 +1,198 @@ +/* + * 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.catalina.servlets; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import static org.apache.catalina.startup.SimpleHttpClient.CRLF; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +@RunWith(Parameterized.class) +public class TestDefaultServletPut extends TomcatBaseTest { + + private static final String START_TEXT= "Starting text"; + private static final String START_LEN = Integer.toString(START_TEXT.length()); + private static final String PATCH_TEXT= "Ending *"; + private static final String PATCH_LEN = Integer.toString(PATCH_TEXT.length()); + private static final String END_TEXT= "Ending * text"; + + @Parameterized.Parameters(name = "{index} rangeHeader [{0}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + // Valid partial PUT + parameterSets.add(new Object[] { + "Content-Range: bytes 0-" + PATCH_LEN + "/" + START_LEN + CRLF, Boolean.TRUE, END_TEXT, Boolean.TRUE }); + // Full PUT + parameterSets.add(new Object[] { + "", null, PATCH_TEXT, Boolean.TRUE }); + // Invalid range + parameterSets.add(new Object[] { + "Content-Range: apples 0-" + PATCH_LEN + "/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes00-" + PATCH_LEN + "/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes0-" + PATCH_LEN + "/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes=0-" + PATCH_LEN + "/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes@0-" + PATCH_LEN + "/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes 9-7/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes -7/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes 9-/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes 9-X/" + START_LEN + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes 0-5/" + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + parameterSets.add(new Object[] { + "Content-Range: bytes 0-5/0x5" + CRLF, Boolean.FALSE, START_TEXT, Boolean.TRUE }); + // Valid partial PUT but partial PUT is disabled + parameterSets.add(new Object[] { + "Content-Range: bytes 0-" + PATCH_LEN + "/" + START_LEN + CRLF, Boolean.TRUE, START_TEXT, Boolean.FALSE }); + + return parameterSets; + } + + + private File tempDocBase; + + @Parameter(0) + public String contentRangeHeader; + + @Parameter(1) + public Boolean contentRangeHeaderValid; + + @Parameter(2) + public String expectedEndText; + + @Parameter(3) + public boolean allowPartialPut; + + @Override + public void setUp() throws Exception { + super.setUp(); + tempDocBase = Files.createTempDirectory(getTemporaryDirectory().toPath(), "put").toFile(); + } + + + /* + * Replaces the text at the start of START_TEXT with PATCH_TEXT. + */ + @Test + public void testPut() throws Exception { + // Configure a web app with a read/write default servlet + Tomcat tomcat = getTomcatInstance(); + Context ctxt = tomcat.addContext("", tempDocBase.getAbsolutePath()); + + Wrapper w = Tomcat.addServlet(ctxt, "default", DefaultServlet.class.getName()); + w.addInitParameter("readonly", "false"); + w.addInitParameter("allowPartialPut", Boolean.toString(allowPartialPut)); + ctxt.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + // Disable caching + ctxt.getResources().setCachingAllowed(false); + + // Full PUT + PutClient putClient = new PutClient(getPort()); + + putClient.setRequest(new String[] { + "PUT /test.txt HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Content-Length: " + START_LEN + CRLF + + CRLF + + START_TEXT + }); + putClient.connect(); + putClient.processRequest(false); + Assert.assertTrue(putClient.isResponse201()); + putClient.disconnect(); + + putClient.reset(); + + // Partial PUT + putClient.connect(); + putClient.setRequest(new String[] { + "PUT /test.txt HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + contentRangeHeader + + "Content-Length: " + PATCH_LEN + CRLF + + CRLF + + PATCH_TEXT + }); + putClient.processRequest(false); + if (contentRangeHeaderValid == null) { + // Not present (so will do a full PUT, replacing the existing) + Assert.assertTrue(putClient.isResponse204()); + } else if (contentRangeHeaderValid.booleanValue() && allowPartialPut) { + // Valid + Assert.assertTrue(putClient.isResponse204()); + } else { + // Not valid + Assert.assertTrue(putClient.isResponse400()); + } + + // Check for the final resource + String path = "http://localhost:" + getPort() + "/test.txt"; + ByteChunk responseBody = new ByteChunk(); + + int rc = getUrl(path, responseBody, null); + + Assert.assertEquals(200, rc); + Assert.assertEquals(expectedEndText, responseBody.toString()); + } + + + @Override + public void tearDown() { + ExpandWar.deleteDir(tempDocBase, false); + } + + + private static class PutClient extends SimpleHttpClient { + + PutClient(int port) { + setPort(port); + } + + + @Override + public boolean isResponseBodyOK() { + return false; + } + } +} diff --git a/test/org/apache/catalina/servlets/TestDefaultServletRangeRequests.java b/test/org/apache/catalina/servlets/TestDefaultServletRangeRequests.java new file mode 100644 index 0000000..cd69501 --- /dev/null +++ b/test/org/apache/catalina/servlets/TestDefaultServletRangeRequests.java @@ -0,0 +1,176 @@ +/* + * 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.catalina.servlets; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.http.FastHttpDateFormat; + +@RunWith(Parameterized.class) +public class TestDefaultServletRangeRequests extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{index} rangeHeader [{0}], ifRangeHeader [{1}]") + public static Collection parameters() { + + // Get the length of the file used for this test + // It varies by platform due to line-endings + File index = new File("test/webapp/index.html"); + long len = index.length(); + String strLen = Long.toString(len); + String lastModified = FastHttpDateFormat.formatDate(index.lastModified()); + + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { "", null, Integer.valueOf(200), strLen, "" }); + // Invalid + parameterSets.add(new Object[] { "bytes", null, Integer.valueOf(416), "", "*/" + len }); + parameterSets.add(new Object[] { "bytes=", null, Integer.valueOf(416), "", "*/" + len }); + // Invalid with unknown type + parameterSets.add(new Object[] { "unknown", null, Integer.valueOf(416), "", "*/" + len }); + parameterSets.add(new Object[] { "unknown=", null, Integer.valueOf(416), "", "*/" + len }); + // Invalid ranges + parameterSets.add(new Object[] { "bytes=-", null, Integer.valueOf(416), "", "*/" + len }); + parameterSets.add(new Object[] { "bytes=10-b", null, Integer.valueOf(416), "", "*/" + len }); + parameterSets.add(new Object[] { "bytes=b-10", null, Integer.valueOf(416), "", "*/" + len }); + // Invalid ranges (out of range) + parameterSets.add(new Object[] { "bytes=1000-2000", null, Integer.valueOf(416), "", "*/" + len }); + // Invalid no equals + parameterSets.add(new Object[] { "bytes 1-10", null, Integer.valueOf(416), "", "*/" + len }); + parameterSets.add(new Object[] { "bytes1-10", null, Integer.valueOf(416), "", "*/" + len }); + parameterSets.add(new Object[] { "bytes10-", null, Integer.valueOf(416), "", "*/" + len }); + parameterSets.add(new Object[] { "bytes-10", null, Integer.valueOf(416), "", "*/" + len }); + // Unknown types + parameterSets.add(new Object[] { "unknown=1-2", null, Integer.valueOf(200), strLen, "" }); + parameterSets.add(new Object[] { "bytesX=1-2", null, Integer.valueOf(200), strLen, "" }); + parameterSets.add(new Object[] { "Xbytes=1-2", null, Integer.valueOf(200), strLen, "" }); + // Valid range + parameterSets.add(new Object[] { + "bytes=0-9", null, Integer.valueOf(206), "10", "0-9/" + len }); + parameterSets.add(new Object[] { + "bytes=-100", null, Integer.valueOf(206), "100", (len - 100) + "-" + (len - 1) + "/" + len }); + parameterSets.add(new Object[] { + "bytes=100-", null, Integer.valueOf(206), "" + (len - 100), "100-" + (len - 1) + "/" + len }); + // Valid range (too much) + parameterSets.add(new Object[] { + "bytes=0-1000", null, Integer.valueOf(206), strLen, "0-" + (len - 1) + "/" + len }); + parameterSets.add(new Object[] { + "bytes=-1000", null, Integer.valueOf(206), strLen, "0-" + (len - 1) + "/" + len }); + + /* If-Range tests */ + // Valid + parameterSets.add(new Object[] { + "bytes=0-9", lastModified, Integer.valueOf(206), "10", "0-9/" + len }); + // Nonsense date (return whole entity) + parameterSets.add(new Object[] { + "bytes=0-9", "a-b-c", Integer.valueOf(200), strLen, ""}); + // Different date (return whole entity) + parameterSets.add(new Object[] { + "bytes=0-9", FastHttpDateFormat.formatDate(1000), Integer.valueOf(200), strLen, ""}); + + return parameterSets; + } + + @Parameter(0) + public String rangeHeader; + @Parameter(1) + public String ifRangeHeader; + @Parameter(2) + public int responseCodeExpected; + @Parameter(3) + public String contentLengthExpected; + @Parameter(4) + public String responseRangeExpected; + + @Test + public void testRange() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctxt, "default", DefaultServlet.class.getName()); + ctxt.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + // Set up parameters + String path = "http://localhost:" + getPort() + "/index.html"; + ByteChunk responseBody = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + + Map> requestHeaders = buildRangeHeader(rangeHeader); + if (ifRangeHeader != null) { + List values = new ArrayList<>(1); + values.add(ifRangeHeader); + requestHeaders.put("If-Range", values); + } + + int rc = getUrl(path, responseBody, requestHeaders, responseHeaders); + + // Check the result + Assert.assertEquals(responseCodeExpected, rc); + + if (contentLengthExpected.length() > 0) { + String contentLength = responseHeaders.get("Content-Length").get(0); + Assert.assertEquals(contentLengthExpected, contentLength); + } + + if (responseRangeExpected.length() > 0) { + String responseRange = null; + List headerValues = responseHeaders.get("Content-Range"); + if (headerValues != null && headerValues.size() == 1) { + responseRange = headerValues.get(0); + } + Assert.assertEquals("bytes " + responseRangeExpected, responseRange); + } + } + + + private static Map> buildRangeHeader(String... headerValues) { + Map> requestHeaders = new HashMap<>(); + List values = new ArrayList<>(); + for (String headerValue : headerValues) { + if (headerValue.length() > 0) { + values.add(headerValue); + } + } + + if (values.size() == 0) { + return null; + } + + requestHeaders.put("range", values); + + return requestHeaders; + } +} diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java new file mode 100644 index 0000000..9e2eb1f --- /dev/null +++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java @@ -0,0 +1,198 @@ +/* + * 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.catalina.servlets; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.SAXParserFactory; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.websocket.server.WsContextListener; +import org.xml.sax.InputSource; + +public class TestWebdavServlet extends TomcatBaseTest { + + /* + * Test attempting to access special paths (WEB-INF/META-INF) using WebdavServlet + */ + @Test + public void testGetSpecials() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + String contextPath = "/examples"; + + File appDir = new File(getBuildDirectory(), "webapps" + contextPath); + // app dir is relative to server home + Context ctx = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctx, "webdav", new WebdavServlet()); + ctx.addServletMappingDecoded("/*", "webdav"); + + tomcat.start(); + + final ByteChunk res = new ByteChunk(); + + int rc =getUrl("http://localhost:" + getPort() + contextPath + + "/WEB-INF/web.xml", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/WEB-INF/doesntexistanywhere", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/WEB-INF/", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/META-INF/MANIFEST.MF", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/META-INF/doesntexistanywhere", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + } + + /* + * Test https://bz.apache.org/bugzilla/show_bug.cgi?id=50026 + * Verify protection of special paths with re-mount of web app resource root. + */ + @Test + public void testGetWithSubpathmount() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + String contextPath = "/examples"; + + File appDir = new File(getBuildDirectory(), "webapps" + contextPath); + // app dir is relative to server home + Context ctx = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctx, "webdav", new WebdavServlet()); + ctx.addServletMappingDecoded("/webdav/*", "webdav"); + ctx.addApplicationListener(WsContextListener.class.getName()); + + tomcat.start(); + + final ByteChunk res = new ByteChunk(); + + // Make sure WebdavServlet isn't exposing special directories + // by remounting the webapp under a sub-path + + int rc =getUrl("http://localhost:" + getPort() + contextPath + + "/webdav/WEB-INF/web.xml", res, null); + + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/webdav/WEB-INF/doesntexistanywhere", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/webdav/WEB-INF/", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/webdav/META-INF/MANIFEST.MF", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/webdav/META-INF/doesntexistanywhere", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + // Make sure WebdavServlet is serving resources + // relative to the map/mount point + final ByteChunk rootResource = new ByteChunk(); + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/index.html", rootResource, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + final ByteChunk subpathResource = new ByteChunk(); + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/webdav/index.html", subpathResource, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + Assert.assertEquals(rootResource.toString(), subpathResource.toString()); + + rc =getUrl("http://localhost:" + getPort() + contextPath + + "/webdav/static/index.html", res, null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + } + + public static int getUrl(String path, ByteChunk out, + Map> resHead) throws IOException { + out.recycle(); + return TomcatBaseTest.getUrl(path, out, resHead); + } + + /* + * Bug 66609 + */ + @Test + public void testDirectoryListing() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + + Wrapper defaultServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet()); + defaultServlet.addInitParameter("listings", "true"); + + ctxt.addServletMappingDecoded("/*", "webdav"); + ctxt.addMimeMapping("html", "text/html"); + + tomcat.start(); + + Client client = new Client(); + client.setPort(getPort()); + client.setRequest(new String[] { "PROPFIND /bug66609/ HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF}); + client.connect(); + client.sendRequest(); + + client.setUseContentLength(true); + client.readResponse(true); + + // This will throw an exception if the XML is not valid + SAXParserFactory.newInstance().newSAXParser().getXMLReader().parse(new InputSource(new StringReader(client.getResponseBody()))); + } + + + private static final class Client extends SimpleHttpClient { + + @Override + public boolean isResponseBodyOK() { + return true; + } + } +} diff --git a/test/org/apache/catalina/servlets/TestWebdavServletOptionCollection.java b/test/org/apache/catalina/servlets/TestWebdavServletOptionCollection.java new file mode 100644 index 0000000..c57408c --- /dev/null +++ b/test/org/apache/catalina/servlets/TestWebdavServletOptionCollection.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.servlets; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.Servlet; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestWebdavServletOptionCollection extends ServletOptionsBaseTest { + + @Parameters + public static Collection inputs() { + String[] methods = new String[] { "GET", "POST", "HEAD", "TRACE", "PUT", "DELETE", + "MKCOL", "LOCK", "UNLOCK", "COPY", "MOVE", "PROPFIND", "PROPPATCH" }; + + List result = new ArrayList<>(); + + for (Boolean listingsValue : booleans) { + for (Boolean readOnlyValue : booleans) { + for (Boolean traceValue : booleans) { + for (String method : methods) { + result.add(new Object[] { + listingsValue, readOnlyValue, traceValue, COLLECTION_NAME, method } ); + } + } + } + + } + return result; + } + + + @Override + protected Servlet createServlet() { + return new WebdavServlet(); + } +} diff --git a/test/org/apache/catalina/servlets/TestWebdavServletOptionsFile.java b/test/org/apache/catalina/servlets/TestWebdavServletOptionsFile.java new file mode 100644 index 0000000..7085c7b --- /dev/null +++ b/test/org/apache/catalina/servlets/TestWebdavServletOptionsFile.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.servlets; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.Servlet; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestWebdavServletOptionsFile extends ServletOptionsBaseTest { + + @Parameters + public static Collection inputs() { + String[] methods = new String[] { "GET", "POST", "HEAD", "TRACE", "PUT", "DELETE", + "MKCOL", "LOCK", "UNLOCK", "COPY", "MOVE", "PROPFIND", "PROPPATCH" }; + + List result = new ArrayList<>(); + + for (Boolean listingsValue : booleans) { + for (Boolean readOnlyValue : booleans) { + for (Boolean traceValue : booleans) { + for (String method : methods) { + result.add(new Object[] { + listingsValue, readOnlyValue, traceValue, FILE_NAME, method } ); + } + } + } + + } + return result; + } + + + @Override + protected Servlet createServlet() { + return new WebdavServlet(); + } +} diff --git a/test/org/apache/catalina/servlets/TestWebdavServletOptionsUnknown.java b/test/org/apache/catalina/servlets/TestWebdavServletOptionsUnknown.java new file mode 100644 index 0000000..f5ca12c --- /dev/null +++ b/test/org/apache/catalina/servlets/TestWebdavServletOptionsUnknown.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.servlets; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.Servlet; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/* + * Split into multiple tests as a single test takes so long it impacts the time + * of an entire test run. + */ +@RunWith(Parameterized.class) +public class TestWebdavServletOptionsUnknown extends ServletOptionsBaseTest { + + @Parameters + public static Collection inputs() { + String[] methods = new String[] { "GET", "POST", "HEAD", "TRACE", "PUT", "DELETE", + "MKCOL", "LOCK", "UNLOCK", "COPY", "MOVE", "PROPFIND", "PROPPATCH" }; + + List result = new ArrayList<>(); + + for (Boolean listingsValue : booleans) { + for (Boolean readOnlyValue : booleans) { + for (Boolean traceValue : booleans) { + for (String method : methods) { + result.add(new Object[] { + listingsValue, readOnlyValue, traceValue, UNKNOWN_NAME, method } ); + } + } + } + + } + return result; + } + + + @Override + protected Servlet createServlet() { + return new WebdavServlet(); + } +} diff --git a/test/org/apache/catalina/session/Benchmarks.java b/test/org/apache/catalina/session/Benchmarks.java new file mode 100644 index 0000000..f114308 --- /dev/null +++ b/test/org/apache/catalina/session/Benchmarks.java @@ -0,0 +1,361 @@ +/* + * 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.catalina.session; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.security.SecureRandom; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Session; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; + +/** + * Named Benchmarks so it is not automatically executed as part of the unit + * tests. + */ +public class Benchmarks { + + /* + * Results on markt's 4-core Windows dev box + * 1 thread - ~1,400ms + * 2 threads - ~2,100ms + * 4 threads - ~3,100ms + * 16 threads - ~14,700ms + * + * Results on markt's 2-core OSX dev box + * 1 thread - ~1,400ms + * 2 threads - ~1,700ms + * 4 threads - ~3,500ms + * 16 threads - ~14,465ms + */ + @Test + public void testManagerBaseGenerateSessionId() throws Exception { + doTestManagerBaseGenerateSessionId(1, 1000000); + doTestManagerBaseGenerateSessionId(1, 1000000); + doTestManagerBaseGenerateSessionId(1, 1000000); + doTestManagerBaseGenerateSessionId(2, 1000000); + doTestManagerBaseGenerateSessionId(2, 1000000); + doTestManagerBaseGenerateSessionId(2, 1000000); + doTestManagerBaseGenerateSessionId(4, 1000000); + doTestManagerBaseGenerateSessionId(4, 1000000); + doTestManagerBaseGenerateSessionId(4, 1000000); + doTestManagerBaseGenerateSessionId(16, 1000000); + // Reduce iterations as context switching will slow things down + doTestManagerBaseGenerateSessionId(100, 100000); + doTestManagerBaseGenerateSessionId(400, 10000); + } + + + private void doTestManagerBaseGenerateSessionId(int threadCount, + int iterCount) throws Exception { + + // Create a default session manager + StandardManager mgr = new StandardManager(); + try { + mgr.startInternal(); + } catch (LifecycleException e) { + // Ignore - this is expected + } + mgr.generateSessionId(); + while (mgr.sessionCreationTiming.size() < + ManagerBase.TIMING_STATS_CACHE_SIZE) { + mgr.sessionCreationTiming.add(null); + } + while (mgr.sessionExpirationTiming.size() < + ManagerBase.TIMING_STATS_CACHE_SIZE) { + mgr.sessionExpirationTiming.add(null); + } + + + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread( + new TestThreadGenerateSessionId(mgr, iterCount)); + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + for (int i = 0; i < threadCount; i++) { + try { + threads[i].join(); + } catch (InterruptedException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + } + long end = System.currentTimeMillis(); + + StringBuilder result = new StringBuilder(); + result.append("Threads: "); + result.append(threadCount); + result.append(", Time(ms): "); + result.append(end-start); + System.out.println(result.toString()); + } + + + private static final class TestThreadGenerateSessionId implements Runnable { + + private ManagerBase mgr; + private int count; + + TestThreadGenerateSessionId(ManagerBase mgr, int count) { + this.mgr = mgr; + this.count = count; + } + + @Override + public void run() { + for (int i = 0; i < count; i++) { + mgr.generateSessionId(); + } + } + } + + + /* + * Results on markt's 4-core Windows dev box + * 1 thread - ~3,800ms + * 2 threads - ~6,700ms + * 4 threads - ~11,000ms + * 16 threads - ~43,500ms + * + * Results on markt's 2-core OSX dev box + * 1 thread - ~4,100ms + * 2 threads - ~5,700ms + * 4 threads - ~11,700ms + * 16 threads - ~45,600ms + */ + @Test + public void testManagerBaseCreateSession() throws LifecycleException { + doTestManagerBaseCreateSession(1, 100000); + doTestManagerBaseCreateSession(2, 1000000); + doTestManagerBaseCreateSession(4, 1000000); + doTestManagerBaseCreateSession(16, 1000000); + // Reduce iterations as context switching will slow things down + doTestManagerBaseCreateSession(100, 100000); + doTestManagerBaseCreateSession(400, 10000); + } + + + private void doTestManagerBaseCreateSession(int threadCount, + int iterCount) throws LifecycleException { + + // Create a default session manager + StandardManager mgr = new StandardManager(); + mgr.setPathname(null); + Host host = new StandardHost(); + host.setName("unittest"); + Context context = new StandardContext(); + context.setPath(""); + context.setParent(host); + mgr.setContext(context); + mgr.start(); + mgr.generateSessionId(); + while (mgr.sessionCreationTiming.size() < + ManagerBase.TIMING_STATS_CACHE_SIZE) { + mgr.sessionCreationTiming.add(null); + } + while (mgr.sessionExpirationTiming.size() < + ManagerBase.TIMING_STATS_CACHE_SIZE) { + mgr.sessionExpirationTiming.add(null); + } + + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread( + new TestThreadCreateSession(mgr, iterCount)); + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + for (int i = 0; i < threadCount; i++) { + try { + threads[i].join(); + } catch (InterruptedException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + } + long end = System.currentTimeMillis(); + + StringBuilder result = new StringBuilder(); + result.append("Threads: "); + result.append(threadCount); + result.append(", Time(ms): "); + result.append(end-start); + System.out.println(result.toString()); + } + + private static final class TestThreadCreateSession implements Runnable { + + private ManagerBase mgr; + private int count; + + TestThreadCreateSession(ManagerBase mgr, int count) { + this.mgr = mgr; + this.count = count; + } + + @Override + public void run() { + for (int i = 0; i < count; i++) { + Session session = mgr.createSession(mgr.generateSessionId()); + session.expire(); + } + } + } + + + /* + * SecureRandom vs. reading /dev/urandom. Very different performance noted + * on some platforms. + * + * Results on markt's 4-core Windows dev box + * SecureRandom /dev/urandom + * 1 thread - ~766ms N/A + * 2 threads - ~843ms N/A + * 4 threads - ~766ms N/A + * + * Results on markt's 2-core OSX dev box + * SecureRandom /dev/urandom + * 1 thread - ~759ms ~3,500ms + * 2 threads - ~725ms ~5,200ms + * 4 threads - ~1,265ms ~10,500ms + */ + @Test + public void testSecureRandomVsDevURandom() throws Exception { + doTestSecureRandomVsDevURandom(1, 1000000); + doTestSecureRandomVsDevURandom(2, 1000000); + doTestSecureRandomVsDevURandom(4, 1000000); + } + + private void doTestSecureRandomVsDevURandom(int threadCount, int iterCount) + throws Exception { + doTestSecureRandomVsDevURandomInner(threadCount, iterCount, true); + doTestSecureRandomVsDevURandomInner(threadCount, iterCount, false); + } + + private void doTestSecureRandomVsDevURandomInner(int threadCount, + int iterCount, boolean useSecureRandom) throws Exception { + + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + if (useSecureRandom) { + threads[i] = new Thread(new TestThreadSecureRandom(iterCount)); + } else { + threads[i] = new Thread(new TestThreadDevUrandom(iterCount)); + } + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + for (int i = 0; i < threadCount; i++) { + try { + threads[i].join(); + } catch (InterruptedException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + } + long end = System.currentTimeMillis(); + + StringBuilder result = new StringBuilder(); + if (useSecureRandom) { + result.append("SecureRandom "); + } else { + result.append("/dev/urandom "); + } + result.append("Threads: "); + result.append(threadCount); + result.append(", Time(ms): "); + result.append(end-start); + System.out.println(result.toString()); + } + + private static final class TestThreadSecureRandom implements Runnable { + + private SecureRandom secureRandom; + private byte[] bytes = new byte[16]; + private int count; + + TestThreadSecureRandom(int iterCount) throws Exception { + this.count = iterCount; + this.secureRandom = SecureRandom.getInstance("SHA1PRNG"); + } + + @Override + public void run() { + for (int i = 0; i < count; i++) { + secureRandom.nextBytes(bytes); + } + } + + } + + private static final class TestThreadDevUrandom implements Runnable { + + private InputStream is; + private byte[] bytes = new byte[16]; + private int count; + + TestThreadDevUrandom(int iterCount) { + try { + is = new FileInputStream("/dev/urandom"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + this.count = iterCount; + } + + @Override + public void run() { + try { + int read = 0; + for (int i = 0; i < count; i++) { + read = is.read(bytes); + if (read < bytes.length) { + throw new IOException("Only read " + read + " bytes"); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/test/org/apache/catalina/session/FileStoreTest.java b/test/org/apache/catalina/session/FileStoreTest.java new file mode 100644 index 0000000..4b80b38 --- /dev/null +++ b/test/org/apache/catalina/session/FileStoreTest.java @@ -0,0 +1,100 @@ +/* + * 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.catalina.session; + +import java.io.File; +import java.io.IOException; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.Manager; +import org.apache.catalina.startup.ExpandWar; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.unittest.TesterServletContext; + +public class FileStoreTest { + + private static final String SESS_TEMPPATH = "SESS_TEMP"; + private static final File dir = new File(SESS_TEMPPATH); + private static FileStore fileStore; + private static File file1 = new File(SESS_TEMPPATH + "/tmp1.session"); + private static File file2 = new File(SESS_TEMPPATH + "/tmp2.session"); + private static Manager manager = new StandardManager(); + + + @BeforeClass + public static void setup() { + TesterContext testerContext = new TesterContext(); + testerContext.setServletContext(new TesterServletContext()); + manager.setContext(testerContext); + fileStore = new FileStore(); + fileStore.setManager(manager); + } + + + @AfterClass + public static void cleanup() { + ExpandWar.delete(dir); + } + + + @Before + public void beforeEachTest() throws IOException { + fileStore.setDirectory(SESS_TEMPPATH); + if (!dir.mkdir()) { + Assert.fail(); + } + if (!file1.createNewFile()) { + Assert.fail(); + } + if (!file2.createNewFile()) { + Assert.fail(); + } + } + + + @Test + public void getSize() throws Exception { + Assert.assertEquals(2, fileStore.getSize()); + } + + + @Test + public void clear() throws Exception { + fileStore.clear(); + Assert.assertEquals(0, fileStore.getSize()); + } + + + @Test + public void keys() throws Exception { + Assert.assertArrayEquals(new String[]{"tmp1", "tmp2"}, fileStore.keys()); + fileStore.clear(); + Assert.assertArrayEquals(new String[]{}, fileStore.keys()); + } + + + @Test + public void removeTest() throws Exception { + fileStore.remove("tmp1"); + Assert.assertEquals(1, fileStore.getSize()); + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/session/TestPersistentManager.java b/test/org/apache/catalina/session/TestPersistentManager.java new file mode 100644 index 0000000..b7ef69d --- /dev/null +++ b/test/org/apache/catalina/session/TestPersistentManager.java @@ -0,0 +1,159 @@ +/* + * 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.catalina.session; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.Store; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.RequestFacade; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.unittest.TesterHost; +import org.easymock.EasyMock; +import org.easymock.IAnswer; + +public class TestPersistentManager { + + @Test + public void testMinIdleSwap() throws Exception { + PersistentManager manager = new PersistentManager(); + manager.setStore(new TesterStore()); + + Host host = new TesterHost(); + Context context = new TesterContext(); + context.setParent(host); + + manager.setContext(context); + + manager.setMaxActiveSessions(2); + manager.setMinIdleSwap(0); + + manager.start(); + + // Create the maximum number of sessions + manager.createSession(null); + manager.createSession(null); + + // Given the minIdleSwap settings, this should swap one out to get below + // the limit + manager.processPersistenceChecks(); + Assert.assertEquals(1, manager.getActiveSessions()); + Assert.assertEquals(2, manager.getActiveSessionsFull()); + + manager.createSession(null); + Assert.assertEquals(2, manager.getActiveSessions()); + Assert.assertEquals(3, manager.getActiveSessionsFull()); + } + + @Test + public void testBug62175() throws Exception { + PersistentManager manager = new PersistentManager(); + AtomicInteger sessionExpireCounter = new AtomicInteger(); + + Store mockStore = EasyMock.createNiceMock(Store.class); + EasyMock.expect(mockStore.load(EasyMock.anyString())).andAnswer(new IAnswer() { + + @Override + public Session answer() throws Throwable { + return timedOutSession(manager, sessionExpireCounter); + } + }).anyTimes(); + + EasyMock.replay(mockStore); + + manager.setStore(mockStore); + + Host host = new TesterHost(); + + RequestCachingSessionListener requestCachingSessionListener = new RequestCachingSessionListener(); + + Context context = new TesterContext() { + + @Override + public Object[] getApplicationLifecycleListeners() { + return new Object[] { requestCachingSessionListener }; + } + + @Override + public Manager getManager() { + return manager; + } + }; + context.setParent(host); + + Connector connector = EasyMock.createNiceMock(Connector.class); + Request req = new Request(connector) { + @Override + public Context getContext() { + return context; + } + }; + req.setRequestedSessionId("invalidSession"); + HttpServletRequest request = new RequestFacade(req); + EasyMock.replay(connector); + requestCachingSessionListener.request = request; + + manager.setContext(context); + + manager.start(); + + Assert.assertNull(request.getSession(false)); + Assert.assertEquals(1, sessionExpireCounter.get()); + + } + + private static class RequestCachingSessionListener implements HttpSessionListener { + + private HttpServletRequest request; + + @Override + public void sessionDestroyed(HttpSessionEvent se) { + request.getSession(false); + } + } + + private StandardSession timedOutSession(PersistentManager manager, AtomicInteger counter) { + StandardSession timedOutSession = new StandardSession(manager) { + private static final long serialVersionUID = -5910605558747844210L; + + @Override + public void expire() { + counter.incrementAndGet(); + super.expire(); + } + }; + timedOutSession.isValid = true; + timedOutSession.expiring = false; + timedOutSession.maxInactiveInterval = 1; + timedOutSession.lastAccessedTime = 0; + timedOutSession.id = "invalidSession"; + return timedOutSession; + } +} + diff --git a/test/org/apache/catalina/session/TestPersistentManagerIntegration.java b/test/org/apache/catalina/session/TestPersistentManagerIntegration.java new file mode 100644 index 0000000..bd4fac3 --- /dev/null +++ b/test/org/apache/catalina/session/TestPersistentManagerIntegration.java @@ -0,0 +1,220 @@ +/* + * 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.catalina.session; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Session; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.valves.PersistentValve; + +public class TestPersistentManagerIntegration extends TomcatBaseTest { + + /** + * Wait enough for the system clock to update its value. On some systems + * (e.g. old Windows) the clock granularity is tens of milliseconds. + */ + private void waitForClockUpdate() throws InterruptedException { + long startTime = System.currentTimeMillis(); + int waitTime = 1; + do { + Thread.sleep(waitTime); + waitTime *= 10; + } while (System.currentTimeMillis() == startTime); + } + + /** + * Wait while session access counter has a positive value. + */ + private void waitWhileSessionIsActive(StandardSession session) + throws InterruptedException { + long maxWaitTime = System.currentTimeMillis() + 60000; + AtomicInteger accessCount = session.accessCount; + while (accessCount.get() > 0) { + // Wait until o.a.c.connector.Request.recycle() completes, + // as it updates lastAccessedTime. + Assert.assertTrue(System.currentTimeMillis() < maxWaitTime); + Thread.sleep(200); + } + } + + @Test + public void noSessionCreate_57637() throws IOException, LifecycleException { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + ctx.setDistributable(true); + + Tomcat.addServlet(ctx, "DummyServlet", new DummyServlet()); + ctx.addServletMappingDecoded("/dummy", "DummyServlet"); + + PersistentManager manager = new PersistentManager(); + TesterStore store = new TesterStore(); + + manager.setStore(store); + manager.setMaxIdleBackup(0); + manager.setSessionActivityCheck(true); + ctx.setManager(manager); + ctx.addValve(new PersistentValve()); + tomcat.start(); + Assert.assertEquals(manager.getActiveSessions(), 0); + Assert.assertTrue("No sessions managed", manager.getSessionIdsFull().isEmpty()); + Assert.assertEquals( + "NO_SESSION", + getUrl( + "http://localhost:" + getPort() + + "/dummy?no_create_session=true").toString()); + Assert.assertEquals(manager.getActiveSessions(), 0); + Assert.assertTrue("No sessions where created", manager.getSessionIdsFull().isEmpty()); + } + + @Test + public void testCreateSessionAndPassivate() throws IOException, LifecycleException, ClassNotFoundException { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + ctx.setDistributable(true); + + Tomcat.addServlet(ctx, "DummyServlet", new DummyServlet()); + ctx.addServletMappingDecoded("/dummy", "DummyServlet"); + + PersistentManager manager = new PersistentManager(); + TesterStore store = new TesterStore(); + + manager.setStore(store); + manager.setMaxIdleBackup(0); + manager.setSessionActivityCheck(true); + ctx.setManager(manager); + ctx.addValve(new PersistentValve()); + tomcat.start(); + Assert.assertEquals("No active sessions", manager.getActiveSessions(), 0); + Assert.assertTrue("No sessions managed", manager.getSessionIdsFull().isEmpty()); + String sessionId = getUrl( + "http://localhost:" + getPort() + + "/dummy?no_create_session=false").toString(); + Assert.assertNotNull("Session is stored", store.load(sessionId)); + Assert.assertEquals("All sessions are passivated", manager.getActiveSessions(), 0); + Assert.assertTrue("One session was created", !manager.getSessionIdsFull().isEmpty()); + } + + @Test + public void backsUpOnce_56698() throws IOException, LifecycleException, + InterruptedException { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.setDistributable(true); + + Tomcat.addServlet(ctx, "DummyServlet", new DummyServlet()); + ctx.addServletMappingDecoded("/dummy", "DummyServlet"); + + PersistentManager manager = new PersistentManager(); + TesterStore store = new TesterStore(); + + manager.setStore(store); + manager.setMaxIdleBackup(0); + manager.setSessionActivityCheck(true); + ctx.setManager(manager); + tomcat.start(); + String sessionId = getUrl("http://localhost:" + getPort() + "/dummy") + .toString(); + + // Note: PersistenceManager.findSession() silently updates + // session.lastAccessedTime, so call it only once before other work. + Session session = manager.findSession(sessionId); + + // Wait until request processing ends, as Request.recycle() updates + // session.lastAccessedTime via session.endAccess(). + waitWhileSessionIsActive((StandardSession) session); + + long lastAccessedTime = session.getLastAccessedTimeInternal(); + + // Session should be idle at least for 0 second (maxIdleBackup) + // to be eligible for persistence, thus no need to wait. + + // Waiting a bit, to catch changes in last accessed time of a session + waitForClockUpdate(); + + manager.processPersistenceChecks(); + Assert.assertEquals(Arrays.asList(sessionId), store.getSavedIds()); + Assert.assertEquals(lastAccessedTime, session.getLastAccessedTimeInternal()); + + // session was not accessed, so no save will be performed + waitForClockUpdate(); + manager.processPersistenceChecks(); + Assert.assertEquals(Arrays.asList(sessionId), store.getSavedIds()); + Assert.assertEquals(lastAccessedTime, session.getLastAccessedTimeInternal()); + + // access session + session.access(); + session.endAccess(); + + // session was accessed, so it will be saved once again + manager.processPersistenceChecks(); + Assert.assertEquals(Arrays.asList(sessionId, sessionId), + store.getSavedIds()); + + // session was not accessed, so once again no save will happen + manager.processPersistenceChecks(); + Assert.assertEquals(Arrays.asList(sessionId, sessionId), + store.getSavedIds()); + } + + private static class DummyServlet extends HttpServlet { + + private static final long serialVersionUID = -3696433049266123995L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + boolean createSession = !Boolean.parseBoolean(req + .getParameter("no_create_session")); + HttpSession session = req.getSession(createSession); + if (session == null) { + resp.getWriter().print("NO_SESSION"); + } else { + String id = session.getId(); + resp.getWriter().print(id); + } + } + + } +} diff --git a/test/org/apache/catalina/session/TestStandardSession.java b/test/org/apache/catalina/session/TestStandardSession.java new file mode 100644 index 0000000..8fbea24 --- /dev/null +++ b/test/org/apache/catalina/session/TestStandardSession.java @@ -0,0 +1,154 @@ +/* + * 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.catalina.session; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Manager; +import org.apache.catalina.core.StandardContext; + +public class TestStandardSession { + + private static final Manager TEST_MANAGER; + + static { + TEST_MANAGER = new StandardManager(); + TEST_MANAGER.setContext(new StandardContext()); + } + + + @Test + public void testSerializationEmpty() throws Exception { + + StandardSession s1 = new StandardSession(TEST_MANAGER); + s1.setValid(true); + StandardSession s2 = serializeThenDeserialize(s1); + + validateSame(s1, s2, 0); + } + + + @Test + public void testSerializationSimple01() throws Exception { + + StandardSession s1 = new StandardSession(TEST_MANAGER); + s1.setValid(true); + s1.setAttribute("attr01", "value01"); + + StandardSession s2 = serializeThenDeserialize(s1); + + validateSame(s1, s2, 1); + } + + + @Test + public void testSerializationSimple02() throws Exception { + + StandardSession s1 = new StandardSession(TEST_MANAGER); + s1.setValid(true); + s1.setAttribute("attr01", new NonSerializable()); + + StandardSession s2 = serializeThenDeserialize(s1); + + validateSame(s1, s2, 0); + } + + + @Test + public void testSerializationSimple03() throws Exception { + + StandardSession s1 = new StandardSession(TEST_MANAGER); + s1.setValid(true); + s1.setAttribute("attr01", "value01"); + s1.setAttribute("attr02", new NonSerializable()); + + StandardSession s2 = serializeThenDeserialize(s1); + + validateSame(s1, s2, 1); + } + + + /* + * See Bug 58284 + */ + @Test + public void serializeSkipsNonSerializableAttributes() throws Exception { + final String nonSerializableKey = "nonSerializable"; + final String nestedNonSerializableKey = "nestedNonSerializable"; + final String serializableKey = "serializable"; + final Object serializableValue = "foo"; + + StandardSession s1 = new StandardSession(TEST_MANAGER); + s1.setValid(true); + Map value = new HashMap<>(); + value.put("key", new NonSerializable()); + s1.setAttribute(nestedNonSerializableKey, value); + s1.setAttribute(serializableKey, serializableValue); + s1.setAttribute(nonSerializableKey, new NonSerializable()); + + StandardSession s2 = serializeThenDeserialize(s1); + + Assert.assertNull(s2.getAttribute(nestedNonSerializableKey)); + Assert.assertNull(s2.getAttribute(nonSerializableKey)); + Assert.assertEquals(serializableValue, s2.getAttribute(serializableKey)); + } + + + private StandardSession serializeThenDeserialize(StandardSession source) + throws IOException, ClassNotFoundException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + source.writeObjectData(oos); + + StandardSession dest = new StandardSession(TEST_MANAGER); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + dest.readObjectData(ois); + + return dest; + } + + + private void validateSame(StandardSession s1, StandardSession s2, int expectedCount) { + int count = 0; + Enumeration names = s1.getAttributeNames(); + while (names.hasMoreElements()) { + count ++; + String name = names.nextElement(); + Object v1 = s1.getAttribute(name); + Object v2 = s2.getAttribute(name); + + Assert.assertEquals(v1, v2); + } + + Assert.assertEquals(expectedCount, count); + } + + + private static class NonSerializable { + } +} diff --git a/test/org/apache/catalina/session/TestStandardSessionIntegration.java b/test/org/apache/catalina/session/TestStandardSessionIntegration.java new file mode 100644 index 0000000..1e83e78 --- /dev/null +++ b/test/org/apache/catalina/session/TestStandardSessionIntegration.java @@ -0,0 +1,107 @@ +/* + * 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.catalina.session; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.ha.tcp.SimpleTcpCluster; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.tribes.group.GroupChannel; +import org.apache.catalina.tribes.transport.ReceiverBase; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestStandardSessionIntegration extends TomcatBaseTest { + + /* + * Test session.invalidate() in a clustered environment. + */ + @Test + public void testBug56578a() throws Exception { + doTestInvalidate(true); + } + + @Test + public void testBug56578b() throws Exception { + doTestInvalidate(false); + } + + private void doTestInvalidate(boolean useClustering) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "bug56578", new Bug56578Servlet()); + ctx.addServletMappingDecoded("/bug56578", "bug56578"); + + if (useClustering) { + SimpleTcpCluster cluster = new SimpleTcpCluster(); + ((ReceiverBase) ((GroupChannel) cluster.getChannel()).getChannelReceiver()).setHost("localhost"); + tomcat.getEngine().setCluster(cluster); + ctx.setDistributable(true); + ctx.setManager(ctx.getCluster().createManager("")); + } + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/bug56578"); + Assert.assertEquals("PASS", res.toString()); + } + + private static class Bug56578Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + + HttpSession session = req.getSession(true); + session.invalidate(); + + // Ugly but the easiest way to test of the session is valid or not + boolean result; + try { + session.getCreationTime(); + result = false; + } catch (IllegalStateException ise) { + result = true; + } + + if (result) { + pw.print("PASS"); + } else { + pw.print("FAIL"); + } + } + } +} diff --git a/test/org/apache/catalina/session/TesterStore.java b/test/org/apache/catalina/session/TesterStore.java new file mode 100644 index 0000000..00bc2ef --- /dev/null +++ b/test/org/apache/catalina/session/TesterStore.java @@ -0,0 +1,90 @@ +/* + * 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.catalina.session; + +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.Store; + +class TesterStore implements Store { + + private Manager manager; + private Map sessions = new HashMap<>(); + private List savedIds = new ArrayList<>(); + + List getSavedIds() { + return savedIds; + } + + @Override + public Manager getManager() { + return this.manager; + } + + @Override + public void setManager(Manager manager) { + this.manager = manager; + } + + @Override + public int getSize() throws IOException { + return savedIds.size(); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + } + + @Override + public String[] keys() throws IOException { + return new ArrayList<>(sessions.keySet()).toArray(new String[0]); + } + + @Override + public Session load(String id) throws ClassNotFoundException, + IOException { + return sessions.get(id); + } + + @Override + public void remove(String id) throws IOException { + sessions.remove(id); + } + + @Override + public void clear() throws IOException { + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + } + + @Override + public void save(Session session) throws IOException { + sessions.put(session.getId(), session); + savedIds.add(session.getId()); + } + +} + diff --git a/test/org/apache/catalina/ssi/TestExpressionParseTree.java b/test/org/apache/catalina/ssi/TestExpressionParseTree.java new file mode 100644 index 0000000..50c3856 --- /dev/null +++ b/test/org/apache/catalina/ssi/TestExpressionParseTree.java @@ -0,0 +1,191 @@ +/* + * 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.catalina.ssi; + +import java.io.IOException; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class TestExpressionParseTree { + + private static final long LAST_MODIFIED = 60 * 60 * 24 * 1000; + + + @Test + public void testSimple1() throws Exception { + SSIMediator mediator = + new SSIMediator(new TesterSSIExternalResolver(), LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("a = a", mediator); + Assert.assertTrue(ept.evaluateTree()); + } + + + @Test + public void testSimple2() throws Exception { + SSIMediator mediator = + new SSIMediator(new TesterSSIExternalResolver(), LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("a = b", mediator); + Assert.assertFalse(ept.evaluateTree()); + } + + + @Test + public void testSimple3() throws Exception { + SSIMediator mediator = + new SSIMediator(new TesterSSIExternalResolver(), LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("a = /a/", mediator); + Assert.assertTrue(ept.evaluateTree()); + } + + + @Test + public void testSimple4() throws Exception { + SSIMediator mediator = + new SSIMediator(new TesterSSIExternalResolver(), LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("a = /b/", mediator); + Assert.assertFalse(ept.evaluateTree()); + } + + + @Test + public void testSimple5() throws Exception { + SSIExternalResolver r = new TesterSSIExternalResolver(); + r.setVariableValue("QUERY_STRING", "a"); + SSIMediator mediator = new SSIMediator(r, LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("$QUERY_STRING = a", mediator); + Assert.assertTrue(ept.evaluateTree()); + } + + + @Test + public void testSimple6() throws Exception { + SSIExternalResolver r = new TesterSSIExternalResolver(); + r.setVariableValue("QUERY_STRING", "a"); + SSIMediator mediator = new SSIMediator(r, LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("$QUERY_STRING = b", mediator); + Assert.assertFalse(ept.evaluateTree()); + } + + + @Test + public void testSimple7() throws Exception { + SSIExternalResolver r = new TesterSSIExternalResolver(); + r.setVariableValue("QUERY_STRING", "a"); + SSIMediator mediator = new SSIMediator(r, LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("$QUERY_STRING = /a/", mediator); + Assert.assertTrue(ept.evaluateTree()); + } + + + @Test + public void testSimple8() throws Exception { + SSIExternalResolver r = new TesterSSIExternalResolver(); + r.setVariableValue("QUERY_STRING", "a"); + SSIMediator mediator = new SSIMediator(r, LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("$QUERY_STRING = /b/", mediator); + Assert.assertFalse(ept.evaluateTree()); + } + + + @Test + public void testBug55176a() throws Exception { + SSIExternalResolver r = new TesterSSIExternalResolver(); + r.setVariableValue("QUERY_STRING", "a="); + SSIMediator mediator = new SSIMediator(r, LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("$QUERY_STRING = /a=/", mediator); + Assert.assertTrue(ept.evaluateTree()); + } + + + @Test + public void testBug55176b() throws Exception { + SSIExternalResolver r = new TesterSSIExternalResolver(); + r.setVariableValue("QUERY_STRING", "a"); + SSIMediator mediator = new SSIMediator(r, LAST_MODIFIED); + ExpressionParseTree ept = + new ExpressionParseTree("$QUERY_STRING = /a=/", mediator); + Assert.assertFalse(ept.evaluateTree()); + } + + + /** + * Minimal implementation that provides the bare essentials require for the + * unit tests. + */ + private static class TesterSSIExternalResolver + implements SSIExternalResolver { + + private Map variables = new HashMap<>(); + + @Override + public void addVariableNames(Collection variableNames) { + // NO-OP + } + + @Override + public String getVariableValue(String name) { + return variables.get(name); + } + + @Override + public void setVariableValue(String name, String value) { + variables.put(name, value); + } + + @Override + public Date getCurrentDate() { + return null; + } + + @Override + public long getFileSize(String path, boolean virtual) + throws IOException { + return 0; + } + + @Override + public long getFileLastModified(String path, boolean virtual) + throws IOException { + return 0; + } + + @Override + public String getFileText(String path, boolean virtual) + throws IOException { + return null; + } + + @Override + public void log(String message, Throwable throwable) { + // NO-OP + } + } +} diff --git a/test/org/apache/catalina/ssi/TestRegExpCapture.java b/test/org/apache/catalina/ssi/TestRegExpCapture.java new file mode 100644 index 0000000..d190cf2 --- /dev/null +++ b/test/org/apache/catalina/ssi/TestRegExpCapture.java @@ -0,0 +1,96 @@ +/* + * 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.catalina.ssi; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; + +@RunWith(Parameterized.class) +public class TestRegExpCapture extends TomcatBaseTest { + + @Parameters(name = "{index}: [{0}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + // d is always the empty string + // Neither a nor b are set, c is empty + parameterSets.add(new Object[] { "", "

    a(none)b(none)cd

    " }); + // a is set, b is not, c is empty + parameterSets.add(new Object[] { "?a=1", "

    a1b(none)cd

    " }); + // a is not set, b is set, c is the same as b + parameterSets.add(new Object[] { "?b=1", "

    a(none)b1c1d

    " }); + + return parameterSets; + } + + private final String queryString; + private final String expectedInBody; + + + public TestRegExpCapture(String queryString, String expectedInBody) { + this.queryString = queryString; + this.expectedInBody = expectedInBody; + } + + @Test + public void testBug53387() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + // SSI requires a privileged Context + ctx.setPrivileged(true); + + FilterDef ssiFilter = new FilterDef(); + ssiFilter.setFilterName("ssiFilter"); + ssiFilter.setFilterClass(SSIFilter.class.getName()); + FilterMap ssiFilterMap = new FilterMap(); + ssiFilterMap.setFilterName("ssiFilter"); + ssiFilterMap.addURLPatternDecoded("*.shtml"); + ctx.addFilterDef(ssiFilter); + ctx.addFilterMap(ssiFilterMap); + + ctx.addMimeMapping("shtml", "text/x-server-parsed-html"); + + tomcat.start(); + + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug53387.shtml" + queryString, body, null); + + Assert.assertEquals(200, rc); + + String text = body.toString(); + Assert.assertTrue(text, text.contains(expectedInBody)); + } +} diff --git a/test/org/apache/catalina/startup/BytesStreamer.java b/test/org/apache/catalina/startup/BytesStreamer.java new file mode 100644 index 0000000..7e063af --- /dev/null +++ b/test/org/apache/catalina/startup/BytesStreamer.java @@ -0,0 +1,40 @@ +/* + * 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.catalina.startup; + +/** + * Used by {@link TomcatBaseTest} + */ +public interface BytesStreamer { + /** + * Get the length of the content about to be streamed. + * + * @return the length if known, else -1 and chucked encoding should be used + */ + int getLength(); + + /** + * @return the number of bytes available in next chunk + */ + int available(); + + /** + * @return returns the next byte to write if {@link #available()} returns + * > 0 + */ + byte[] next(); +} diff --git a/test/org/apache/catalina/startup/DuplicateMappingParamFilter.java b/test/org/apache/catalina/startup/DuplicateMappingParamFilter.java new file mode 100644 index 0000000..cda928d --- /dev/null +++ b/test/org/apache/catalina/startup/DuplicateMappingParamFilter.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; + +/** + * Test Mock with wrong Annotation! + * + * @author Peter Rossbach + * + */ +@WebFilter(value = "/param", filterName="paramDFilter", + urlPatterns = { "/param1" , "/param2" }) +public class DuplicateMappingParamFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws ServletException, IOException { + chain.doFilter(req, res); + } +} + diff --git a/test/org/apache/catalina/startup/DuplicateMappingParamServlet.java b/test/org/apache/catalina/startup/DuplicateMappingParamServlet.java new file mode 100644 index 0000000..797cd5f --- /dev/null +++ b/test/org/apache/catalina/startup/DuplicateMappingParamServlet.java @@ -0,0 +1,48 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebInitParam; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Test Mock with wrong Annotation! + * + * @author Peter Rossbach + */ +@WebServlet(value = "/annotation/overwrite", urlPatterns ="/param2", name = "param", initParams = { + @WebInitParam(name = "foo", value = "Hello"), + @WebInitParam(name = "bar", value = "World!") }) +public class DuplicateMappingParamServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse res) + throws IOException, ServletException { + PrintWriter out = res.getWriter(); + out.print("

    " + getInitParameter("foo") + " " + + getInitParameter("bar") + "

    "); + } +} diff --git a/test/org/apache/catalina/startup/EmbeddedTomcat.java b/test/org/apache/catalina/startup/EmbeddedTomcat.java new file mode 100644 index 0000000..d19496f --- /dev/null +++ b/test/org/apache/catalina/startup/EmbeddedTomcat.java @@ -0,0 +1,97 @@ +/* + * 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.catalina.startup; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.LogManager; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Ignore; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.modeler.Registry; +import org.apache.tomcat.util.scan.StandardJarScanFilter; +import org.apache.tomcat.util.scan.StandardJarScanner; + +@Ignore +public class EmbeddedTomcat { + + private static void resetLogging() { + final String loggingConfig = "handlers = java.util.logging.ConsoleHandler\n" + + ".handlers = java.util.logging.ConsoleHandler\n" + + "java.util.logging.ConsoleHandler.level = FINE\n" + + "java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter\n" + + "java.util.logging.ConsoleHandler.encoding = UTF-8\n"; + try { + InputStream is = new ByteArrayInputStream(loggingConfig.getBytes(StandardCharsets.UTF_8)); + LogManager.getLogManager().readConfiguration(is); + LogFactory.getLog(EmbeddedTomcat.class).info("Logger configured to System.out"); + } catch (SecurityException | IOException e) { + // Ignore, the VM default will be used + } + } + + public static void main(String... args) throws Exception { + Registry.disableRegistry(); + Tomcat tomcat = new Tomcat(); + resetLogging(); + tomcat.setPort(8080); + Connector connector = tomcat.getConnector(); + connector.setProperty("bindOnInit", "false"); + // No file system docBase required + Context ctx = tomcat.addContext("", null); + skipTldsForResourceJars(ctx); + CounterServlet counterServlet = new CounterServlet(); + Tomcat.addServlet(ctx, "counterServlet", counterServlet); + ctx.addServletMappingDecoded("/", "counterServlet"); + //ctx.addApplicationListener(new WsContextListener()); + + tomcat.start(); + Thread.sleep(60*1000); + } + + public static void skipTldsForResourceJars(Context context) { + StandardJarScanner scanner = (StandardJarScanner) context.getJarScanner(); + StandardJarScanFilter filter = (StandardJarScanFilter) scanner.getJarScanFilter(); + filter.setTldSkip(filter.getTldSkip() + ",resources*.jar"); + } + + private static class CounterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private AtomicInteger callCount = new AtomicInteger(0); + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.getSession(true); + resp.setContentType("text/plain"); + resp.getWriter().print("OK: " + req.getRequestURL() + "[" + callCount.incrementAndGet()+ "]"); + } + } +} diff --git a/test/org/apache/catalina/startup/ExpectationClient.java b/test/org/apache/catalina/startup/ExpectationClient.java new file mode 100644 index 0000000..ef7722b --- /dev/null +++ b/test/org/apache/catalina/startup/ExpectationClient.java @@ -0,0 +1,51 @@ +/* + * 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.catalina.startup; + +/** + * Simple http client used for testing requests with "Expect" headers. + */ +public class ExpectationClient extends SimpleHttpClient { + + private static final String BODY = "foo=bar"; + + public void doRequestHeaders() throws Exception { + StringBuilder requestHeaders = new StringBuilder(); + requestHeaders.append("POST /echo HTTP/1.1").append(CRLF); + requestHeaders.append("Host: localhost").append(CRLF); + requestHeaders.append("Expect: 100-continue").append(CRLF); + requestHeaders.append("Content-Type: application/x-www-form-urlencoded").append(CRLF); + String len = Integer.toString(BODY.length()); + requestHeaders.append("Content-length: ").append(len).append(CRLF); + requestHeaders.append(CRLF); + + setRequest(new String[] {requestHeaders.toString()}); + + processRequest(false); + } + + public void doRequestBody() throws Exception { + setRequest(new String[] { BODY }); + + processRequest(true); + } + + @Override + public boolean isResponseBodyOK() { + return BODY.equals(getResponseBody()); + } +} diff --git a/test/org/apache/catalina/startup/FastNonSecureRandom.java b/test/org/apache/catalina/startup/FastNonSecureRandom.java new file mode 100644 index 0000000..c8c28e0 --- /dev/null +++ b/test/org/apache/catalina/startup/FastNonSecureRandom.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.startup; + +import java.security.SecureRandom; +import java.util.Random; + +public class FastNonSecureRandom extends SecureRandom { + + private static final long serialVersionUID = 1L; + + private final Random random = new Random(); + + @Override + public String getAlgorithm() { + return "INSECURE"; + } + + @Override + public synchronized void setSeed(byte[] seed) { + // Not implemented + } + + @Override + public synchronized void setSeed(long seed) { + // The super class constructor calls this method earlier than our + // fields are initialized. Ignore the call. + if (random == null) { + return; + } + random.setSeed(seed); + } + + @Override + public synchronized void nextBytes(byte[] bytes) { + random.nextBytes(bytes); + } + + @Override + public byte[] generateSeed(int numBytes) { + byte[] value = new byte[numBytes]; + nextBytes(value); + return value; + } + +} \ No newline at end of file diff --git a/test/org/apache/catalina/startup/LoggingBaseTest.java b/test/org/apache/catalina/startup/LoggingBaseTest.java new file mode 100644 index 0000000..bf0ea40 --- /dev/null +++ b/test/org/apache/catalina/startup/LoggingBaseTest.java @@ -0,0 +1,158 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.LogManager; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestName; + +import org.apache.juli.ClassLoaderLogManager; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Base class that provides logging support for test cases that respects the + * standard conf/logging.properties configuration file. + * + *

    + * It also provides support for cleaning up temporary files after shutdown. See + * {@link #addDeleteOnTearDown(File)}. + * + *

    + * Note that the logging configuration uses + * ${catalina.base} value and thus we take care about that property + * even if the tests do not use Tomcat. + */ +public abstract class LoggingBaseTest { + + private static List deleteOnClassTearDown = new ArrayList<>(); + + protected Log log; + + private static File tempDir; + + private List deleteOnTearDown = new ArrayList<>(); + + /** + * Provides name of the currently executing test method. + */ + @Rule + public final TestName testName = new TestName(); + + /* + * Helper method that returns the directory where Tomcat build resides. It + * is used to access resources that are part of default Tomcat deployment. + * E.g. the examples webapp. + */ + public static File getBuildDirectory() { + return new File(System.getProperty("tomcat.test.tomcatbuild", + "output/build")); + } + + /* + * Helper method that returns the path of the temporary directory used by + * the test runs. The directory is configured during {@link #setUp()}. + * + *

    + * It is used as ${catalina.base} for the instance of Tomcat + * that is being started, but can be used to store other temporary files as + * well. Its work and webapps subdirectories are + * deleted at {@link #tearDown()}. If you have other files or directories + * that have to be deleted on cleanup, register them with + * {@link #addDeleteOnTearDown(File)}. + */ + public File getTemporaryDirectory() { + return tempDir; + } + + /** + * Schedule the given file or directory to be deleted during after-test + * cleanup. + * + * @param file + * File or directory + */ + public void addDeleteOnTearDown(File file) { + deleteOnTearDown.add(file); + } + + @BeforeClass + public static void setUpPerTestClass() throws Exception { + // Create catalina.base directory + File tempBase = new File(System.getProperty("tomcat.test.temp", "output/tmp")); + if (!tempBase.mkdirs() && !tempBase.isDirectory()) { + Assert.fail("Unable to create base temporary directory for tests"); + } + Path tempBasePath = FileSystems.getDefault().getPath(tempBase.getAbsolutePath()); + tempDir = Files.createTempDirectory(tempBasePath, "test").toFile(); + + System.setProperty(Constants.CATALINA_BASE_PROP, tempDir.getAbsolutePath()); + + // Configure logging + System.setProperty("java.util.logging.manager", + "org.apache.juli.ClassLoaderLogManager"); + System.setProperty("java.util.logging.config.file", + new File(System.getProperty("tomcat.test.basedir"), + "conf/logging.properties").toString()); + + // tempDir contains log files which will be open until JULI shuts down + deleteOnClassTearDown.add(tempDir); + } + + @Before + public void setUp() throws Exception { + log = LogFactory.getLog(getClass()); + log.info("Starting test case [" + testName.getMethodName() + "]"); + } + + @After + public void tearDown() throws Exception { + boolean deleted = true; + for (File file : deleteOnTearDown) { + deleted = deleted & ExpandWar.delete(file); + } + deleteOnTearDown.clear(); + + Assert.assertTrue("Failed to delete at least one file", deleted); + } + + @AfterClass + public static void tearDownPerTestClass() throws Exception { + LogManager logManager = LogManager.getLogManager(); + if (logManager instanceof ClassLoaderLogManager) { + ((ClassLoaderLogManager) logManager).shutdown(); + } else { + logManager.reset(); + } + for (File file : deleteOnClassTearDown) { + ExpandWar.delete(file); + } + deleteOnClassTearDown.clear(); + } +} diff --git a/test/org/apache/catalina/startup/NoMappingParamServlet.java b/test/org/apache/catalina/startup/NoMappingParamServlet.java new file mode 100644 index 0000000..64008b3 --- /dev/null +++ b/test/org/apache/catalina/startup/NoMappingParamServlet.java @@ -0,0 +1,43 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebInitParam; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet(name = "param1", initParams = { + @WebInitParam(name = "foo", value = "Hello"), + @WebInitParam(name = "bar", value = "World!") }) +public class NoMappingParamServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse res) + throws IOException, ServletException { + PrintWriter out = res.getWriter(); + out.print("

    " + getInitParameter("foo") + " " + + getInitParameter("bar") + "

    "); + } +} diff --git a/test/org/apache/catalina/startup/ParamFilter.java b/test/org/apache/catalina/startup/ParamFilter.java new file mode 100644 index 0000000..8431cec --- /dev/null +++ b/test/org/apache/catalina/startup/ParamFilter.java @@ -0,0 +1,49 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.annotation.WebInitParam; + +/** + * Test Mock to check Filter Annotations + * @author Peter Rossbach + */ +@WebFilter(value = "/param", filterName = "paramFilter", + dispatcherTypes = { DispatcherType.ERROR, DispatcherType.ASYNC }, + initParams = { @WebInitParam(name = "message", value = "Servlet says: ") }) +public class ParamFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + @Override + public void doFilter(ServletRequest req, ServletResponse res, + FilterChain chain) throws ServletException, IOException { + PrintWriter out = res.getWriter(); + out.print(getInitParameter("message")); + chain.doFilter(req, res); + } +} diff --git a/test/org/apache/catalina/startup/ParamServlet.java b/test/org/apache/catalina/startup/ParamServlet.java new file mode 100644 index 0000000..3af16c7 --- /dev/null +++ b/test/org/apache/catalina/startup/ParamServlet.java @@ -0,0 +1,43 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebInitParam; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet(value = "/annotation/overwrite", name = "param", initParams = { + @WebInitParam(name = "foo", value = "Hello"), + @WebInitParam(name = "bar", value = "World!") }, displayName = "param", description = "param", largeIcon = "paramLarge.png", smallIcon = "paramSmall.png", loadOnStartup = 0, asyncSupported = false) +public class ParamServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse res) + throws IOException, ServletException { + PrintWriter out = res.getWriter(); + out.print("

    " + getInitParameter("foo") + " " + + getInitParameter("bar") + "

    "); + } +} diff --git a/test/org/apache/catalina/startup/SimpleHttpClient.java b/test/org/apache/catalina/startup/SimpleHttpClient.java new file mode 100644 index 0000000..bc47ada --- /dev/null +++ b/test/org/apache/catalina/startup/SimpleHttpClient.java @@ -0,0 +1,505 @@ +/* + * 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.catalina.startup; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.junit.Assert; + +/** + * Simple client for unit testing. It isn't robust, it isn't secure and + * should not be used as the basis for production code. Its only purpose + * is to do the bare minimum for the unit tests. + */ +public abstract class SimpleHttpClient { + public static final String TEMP_DIR = + System.getProperty("java.io.tmpdir"); + + public static final String CR = "\r"; + public static final String LF = "\n"; + public static final String CRLF = CR + LF; + + public static final String INFO_100 = "HTTP/1.1 100 "; + public static final String OK_200 = "HTTP/1.1 200 "; + public static final String CREATED_201 = "HTTP/1.1 201 "; + public static final String NOCONTENT_204 = "HTTP/1.1 204 "; + public static final String REDIRECT_302 = "HTTP/1.1 302 "; + public static final String REDIRECT_303 = "HTTP/1.1 303 "; + public static final String FAIL_400 = "HTTP/1.1 400 "; + public static final String FORBIDDEN_403 = "HTTP/1.1 403 "; + public static final String FAIL_404 = "HTTP/1.1 404 "; + public static final String FAIL_405 = "HTTP/1.1 405 "; + public static final String TIMEOUT_408 = "HTTP/1.1 408 "; + public static final String FAIL_413 = "HTTP/1.1 413 "; + public static final String FAIL_417 = "HTTP/1.1 417 "; + public static final String FAIL_50X = "HTTP/1.1 50"; + public static final String FAIL_500 = "HTTP/1.1 500 "; + public static final String FAIL_501 = "HTTP/1.1 501 "; + + private static final String CONTENT_LENGTH_HEADER_PREFIX = + "Content-Length: "; + + protected static final String SESSION_COOKIE_NAME = "JSESSIONID"; + protected static final String SESSION_PARAMETER_NAME = + SESSION_COOKIE_NAME.toLowerCase(Locale.ENGLISH); + + private static final String COOKIE_HEADER_PREFIX = "Set-Cookie: "; + private static final String SESSION_COOKIE_HEADER_PREFIX = + COOKIE_HEADER_PREFIX + SESSION_COOKIE_NAME + "="; + + private static final String REDIRECT_HEADER_PREFIX = "Location: "; + protected static final String SESSION_PATH_PARAMETER_PREFIX = + SESSION_PARAMETER_NAME + "="; + protected static final String SESSION_PATH_PARAMETER_TAILS = CRLF + ";?"; + + private static final String ELEMENT_HEAD = "<"; + private static final String ELEMENT_TAIL = ">"; + private static final String RESOURCE_TAG = "a"; + private static final String LOGIN_TAG = "form"; + + private Socket socket; + private Writer writer; + private BufferedReader reader; + private int port = 8080; + + private String[] request; + private boolean useContinue = false; + private boolean useCookies = true; + private boolean useHttp09 = false; + private int requestPause = 1000; + private Charset requestBodyEncoding = StandardCharsets.ISO_8859_1; + + private String responseLine; + private List responseHeaders = new ArrayList<>(); + private String sessionId; + private boolean useContentLength; + private int contentLength; + private String redirectUri; + + private String responseBody; + private List bodyUriElements = null; + private Charset responseBodyEncoding = StandardCharsets.ISO_8859_1; + + public void setPort(int thePort) { + port = thePort; + } + + public void setRequest(String[] theRequest) { + request = theRequest; + } + + /* + * Expect the server to reply with 100 Continue interim response + */ + public void setUseContinue(boolean theUseContinueFlag) { + useContinue = theUseContinueFlag; + } + + public boolean getUseContinue() { + return useContinue; + } + + public void setUseCookies(boolean theUseCookiesFlag) { + useCookies = theUseCookiesFlag; + } + + public boolean getUseCookies() { + return useCookies; + } + + public void setUseHttp09(boolean theUseHttp09Flag) { + useHttp09 = theUseHttp09Flag; + } + + public void setRequestPause(int theRequestPause) { + requestPause = theRequestPause; + } + + public String getResponseLine() { + return responseLine; + } + + public List getResponseHeaders() { + return responseHeaders; + } + + public void setResponseBodyEncoding(Charset charset) { + responseBodyEncoding = charset; + } + + public String getResponseBody() { + return responseBody; + } + + /** + * Return opening tags of HTML elements that were extracted by the + * {@link #extractUriElements()} method. + * + *

    + * Note, that {@link #extractUriElements()} method has to be called + * explicitly. + * + * @return List of HTML tags, accumulated by {@link #extractUriElements()} + * method, or {@code null} if the method has not been called yet. + */ + public List getResponseBodyUriElements() { + return bodyUriElements; + } + + public void setUseContentLength(boolean b) { + useContentLength = b; + } + + public void setSessionId(String theSessionId) { + sessionId = theSessionId; + } + + public String getSessionId() { + return sessionId; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void connect(int connectTimeout, int soTimeout) + throws UnknownHostException, IOException { + SocketAddress addr = new InetSocketAddress("localhost", port); + socket = new Socket(); + socket.setSoTimeout(soTimeout); + socket.connect(addr,connectTimeout); + OutputStream os = createOutputStream(socket); + writer = new OutputStreamWriter(os, requestBodyEncoding); + InputStream is = socket.getInputStream(); + Reader r = new InputStreamReader(is, responseBodyEncoding); + reader = new BufferedReader(r); + } + public void connect() throws UnknownHostException, IOException { + connect(10000, 10000); + } + + protected OutputStream createOutputStream(Socket socket) throws IOException { + return socket.getOutputStream(); + } + + public void processRequest() throws IOException, InterruptedException { + processRequest(true); + } + + public void processRequest(boolean wantBody) + throws IOException, InterruptedException { + sendRequest(); + + readResponse(wantBody); + + } + + /* + * Send the component parts of the request + * (be tolerant and simply skip null entries) + */ + public void sendRequest() throws InterruptedException, IOException { + boolean first = true; + for (String requestPart : request) { + if (requestPart != null) { + if (first) { + first = false; + } else { + Thread.sleep(requestPause); + } + writer.write(requestPart); + writer.flush(); + } + } + } + + public void readResponse(boolean wantBody) throws IOException { + // clear any residual data before starting on this response + responseHeaders.clear(); + responseBody = null; + if (bodyUriElements != null) { + bodyUriElements.clear(); + } + + // HTTP 0.9 has neither response line nor headers + if (!useHttp09) { + // Read the response status line + responseLine = readLine(); + + // Is a 100 continue response expected? + if (useContinue) { + if (isResponse100()) { + // Skip the blank after the 100 Continue response + readLine(); + // Now get the final response + responseLine = readLine(); + } else { + throw new IOException("No 100 Continue response"); + } + } + + // Put the headers into a map, and process interesting ones + processHeaders(); + } + + // Read the body, if requested and if one exists + processBody(wantBody); + + if (isResponse408()) { + // the session has timed out + sessionId = null; + } + } + + /* + * Accumulate the response headers into a map, and extract + * interesting details at the same time + */ + private void processHeaders() throws IOException { + // Reset response fields + redirectUri = null; + contentLength = -1; + + String line = readLine(); + while ((line != null) && (line.length() > 0)) { + responseHeaders.add(line); + if (line.startsWith(CONTENT_LENGTH_HEADER_PREFIX)) { + contentLength = Integer.parseInt(line.substring(16)); + } + else if (line.startsWith(COOKIE_HEADER_PREFIX)) { + if (useCookies) { + String temp = line.substring( + SESSION_COOKIE_HEADER_PREFIX.length()); + temp = temp.substring(0, temp.indexOf(';')); + setSessionId(temp); + } + } + else if (line.startsWith(REDIRECT_HEADER_PREFIX)) { + redirectUri = line.substring(REDIRECT_HEADER_PREFIX.length()); + } + line = readLine(); + } + } + + /* + * Read the body of the server response. Save its contents and + * search it for uri-elements only if requested + */ + private void processBody(boolean wantBody) throws IOException { + // Read the body, if one exists + StringBuilder builder = new StringBuilder(); + if (wantBody) { + if (useContentLength && (contentLength > -1)) { + char[] body = new char[contentLength]; + int read = reader.read(body); + builder.append(body, 0 , read); + Assert.assertEquals(contentLength, builder.toString().getBytes(responseBodyEncoding).length); + } else { + // not using content length, so just read it line by line + String line = null; + try { + while ((line = readLine()) != null) { + builder.append(line); + } + } catch (SocketException e) { + // Ignore + // May see a SocketException if the request hasn't been + // fully read when the connection is closed as that may + // trigger a TCP reset. + } + } + } + responseBody = builder.toString(); + } + + /** + * Scan the response body for opening tags of certain HTML elements + * (<a>, <form>). If any are found, then accumulate them. + * + *

    + * Note: This method has the following limitations: a) It assumes that the + * response is HTML. b) It searches for lowercase tags only. + * + * @see #getResponseBodyUriElements() + */ + public void extractUriElements() { + bodyUriElements = new ArrayList<>(); + if (responseBody.length() > 0) { + int ix = 0; + while ((ix = extractUriElement(responseBody, ix, RESOURCE_TAG)) > 0){ + // loop + } + ix = 0; + while ((ix = extractUriElement(responseBody, ix, LOGIN_TAG)) > 0){ + // loop + } + } + } + + /* + * Scan an HTML body for a given HTML uri element, starting from the + * given index into the source string. If any are found, simply + * accumulate them as literal strings, including angle brackets. + * note: nested elements will not be collected. + * + * @param body HTTP body of the response + * @param startIx offset into the body to resume the scan (for iteration) + * @param elementName to scan for (only one at a time) + * @returns the index into the body to continue the scan (for iteration) + */ + private int extractUriElement(String body, int startIx, String elementName) { + String detector = ELEMENT_HEAD + elementName + " "; + int iStart = body.indexOf(detector, startIx); + if (iStart > -1) { + int iEnd = body.indexOf(ELEMENT_TAIL, iStart); + if (iEnd < 0) { + throw new IllegalArgumentException( + "Response body check failure.\n" + + "element [" + detector + "] is not terminated with [" + + ELEMENT_TAIL + "]\nActual: [" + body + "]"); + } + String element = body.substring(iStart, iEnd); + bodyUriElements.add(element); + iStart += element.length(); + } + return iStart; + } + + public String readLine() throws IOException { + return reader.readLine(); + } + + public void disconnect() throws IOException { + writer.close(); + reader.close(); + socket.close(); + } + + public void reset() { + socket = null; + writer = null; + reader = null; + + request = null; + requestPause = 1000; + + useContinue = false; + + resetResponse(); + } + + public void resetResponse() { + responseLine = null; + responseHeaders = new ArrayList<>(); + responseBody = null; + } + + public boolean responseLineStartsWith(String expected) { + String line = getResponseLine(); + if (line == null) { + return false; + } + return line.startsWith(expected); + } + + public boolean isResponse100() { + return responseLineStartsWith(INFO_100); + } + + public boolean isResponse200() { + return responseLineStartsWith(OK_200); + } + + public boolean isResponse201() { + return responseLineStartsWith(CREATED_201); + } + + public boolean isResponse204() { + return responseLineStartsWith(NOCONTENT_204); + } + + public boolean isResponse302() { + return responseLineStartsWith(REDIRECT_302); + } + + public boolean isResponse303() { + return responseLineStartsWith(REDIRECT_303); + } + + public boolean isResponse400() { + return responseLineStartsWith(FAIL_400); + } + + public boolean isResponse403() { + return responseLineStartsWith(FORBIDDEN_403); + } + + public boolean isResponse404() { + return responseLineStartsWith(FAIL_404); + } + + public boolean isResponse405() { + return responseLineStartsWith(FAIL_405); + } + + public boolean isResponse408() { + return responseLineStartsWith(TIMEOUT_408); + } + + public boolean isResponse413() { + return responseLineStartsWith(FAIL_413); + } + + public boolean isResponse417() { + return responseLineStartsWith(FAIL_417); + } + + public boolean isResponse50x() { + return responseLineStartsWith(FAIL_50X); + } + + public boolean isResponse500() { + return responseLineStartsWith(FAIL_500); + } + + public boolean isResponse501() { + return responseLineStartsWith(FAIL_501); + } + + public Socket getSocket() { + return socket; + } + + public abstract boolean isResponseBodyOK(); +} diff --git a/test/org/apache/catalina/startup/TestBootstrap.java b/test/org/apache/catalina/startup/TestBootstrap.java new file mode 100644 index 0000000..476ad79 --- /dev/null +++ b/test/org/apache/catalina/startup/TestBootstrap.java @@ -0,0 +1,180 @@ +/* + * 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.catalina.startup; + +import org.junit.Assert; +import org.junit.Test; + + +public class TestBootstrap { + + @Test + public void testEmptyNonQuoted() { + doTest(""); + } + + @Test + public void testOneNonQuoted() { + doTest("a", "a"); + } + + @Test + public void testTwoNonQuoted01() { + doTest("a,b", "a", "b"); + } + + @Test + public void testTwoNonQuoted02() { + doTest("a,,b", "a", "b"); + } + + @Test + public void testTwoNonQuoted03() { + doTest(",a,b", "a", "b"); + } + + @Test + public void testTwoNonQuoted04() { + doTest("a,b,", "a", "b"); + } + + @Test + public void testThreeNonQuoted() { + doTest("a,b,c", "a", "b", "c"); + } + + @Test + public void testEmptyQuoted() { + doTest("\"\""); + } + + @Test + public void testOneQuoted01() { + doTest("\"a\"", "a"); + } + + @Test + public void testOneQuoted02() { + doTest("\"aaa\"", "aaa"); + } + + @Test + public void testOneQuoted03() { + doTest("\"a,aa\"", "a,aa"); + } + + @Test + public void testOneQuoted04() { + doTest("\",aaa\"", ",aaa"); + } + + @Test + public void testOneQuoted05() { + doTest("\"aaa,\"", "aaa,"); + } + + @Test + public void testTwoQuoted01() { + doTest("\"aaa\",bbb", "aaa", "bbb"); + } + + @Test + public void testTwoQuoted02() { + doTest("\"a,aa\",bbb", "a,aa", "bbb"); + } + + @Test + public void testTwoQuoted03() { + doTest("\"aaa\",\"bbb\"", "aaa", "bbb"); + } + + @Test + public void testTwoQuoted04() { + doTest("\"aaa\",\",bbb\"", "aaa", ",bbb"); + } + + @Test + public void testTwoQuoted05() { + doTest("aaa,\"bbb,\"", "aaa", "bbb,"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes01() { + doTest("\"", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes02() { + doTest("\"aaa", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes03() { + doTest("aaa\"", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes04() { + doTest("a\"a", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes05() { + doTest("b,\"", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes06() { + doTest("b,\"aaa", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes07() { + doTest("b,aaa\"", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes08() { + doTest("b,a\"a", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes09() { + doTest("\",b", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes10() { + doTest("\"aaa,b", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes11() { + doTest("aaa\",b", "ignored"); + } + + @Test(expected=IllegalArgumentException.class) + public void testUnbalancedQuotes12() { + doTest("a\"a,b", "ignored"); + } + + private void doTest(String input, String... expected) { + String[] result = Bootstrap.getPaths(input); + + Assert.assertArrayEquals(expected, result); + } +} diff --git a/test/org/apache/catalina/startup/TestContextConfig.java b/test/org/apache/catalina/startup/TestContextConfig.java new file mode 100644 index 0000000..4b6b748 --- /dev/null +++ b/test/org/apache/catalina/startup/TestContextConfig.java @@ -0,0 +1,206 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardContext; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestContextConfig extends TomcatBaseTest { + + @Test + public void testOverrideWithSCIDefaultName() throws Exception { + doTestOverrideDefaultServletWithSCI("default"); + } + + @Test + public void testOverrideWithSCIDefaultMapping() throws Exception { + doTestOverrideDefaultServletWithSCI("anything"); + } + + private void doTestOverrideDefaultServletWithSCI(String servletName) + throws Exception{ + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + StandardContext ctxt = (StandardContext) tomcat.addContext(null, + "/test", appDir.getAbsolutePath()); + ctxt.setDefaultWebXml(new File("conf/web.xml").getAbsolutePath()); + ctxt.addLifecycleListener(new ContextConfig()); + + ctxt.addServletContainerInitializer( + new CustomDefaultServletSCI(servletName), null); + + tomcat.start(); + + assertPageContains("/test", "OK - Custom default Servlet"); + } + + @Test + public void testBug51396() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + // app dir is relative to server home + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + skipTldsForResourceJars(ctx); + + tomcat.start(); + + assertPageContains("/test/bug51396.jsp", "

    OK

    "); + } + + @Test + public void testBug53574() throws Exception { + getTomcatInstanceTestWebapp(false, true); + assertPageContains("/test/bug53574", "OK"); + } + + @Test + public void testBug54262() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments-empty-absolute-ordering"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + assertPageContains("/test/resourceA.jsp", + "resourceA.jsp in resources.jar"); + assertPageContains("/test/resources/HelloWorldExample", + null, HttpServletResponse.SC_NOT_FOUND); + } + + @Test + public void testBug54379() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + Context context = + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + Tomcat.addServlet(context, "TestServlet", + "org.apache.catalina.startup.TesterServletWithLifeCycleMethods"); + context.addServletMappingDecoded("/testServlet", "TestServlet"); + + tomcat.enableNaming(); + + tomcat.start(); + + assertPageContains("/test/testServlet", "postConstruct1()"); + } + + @Test + public void testBug54448and54450() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + Context context = tomcat.addWebapp(null, "/test", + appDir.getAbsolutePath()); + + Tomcat.addServlet(context, "TestServlet", + "org.apache.catalina.startup.TesterServletWithAnnotations"); + context.addServletMappingDecoded("/testServlet", "TestServlet"); + + tomcat.enableNaming(); + + tomcat.start(); + + assertPageContains("/test/testServlet", + "envEntry1: 0 envEntry2: 2 envEntry3: 33 envEntry4: 0 envEntry5: 55 envEntry6: 66"); + } + + @Test + public void testBug55210() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + assertPageContains("/test/TesterServlet1", "OK"); + assertPageContains("/test/TesterServlet2", "OK"); + } + + private static class CustomDefaultServletSCI + implements ServletContainerInitializer { + + private String servletName; + + CustomDefaultServletSCI(String servletName) { + this.servletName = servletName; + } + + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + Servlet s = new CustomDefaultServlet(); + ServletRegistration.Dynamic r = ctx.addServlet(servletName, s); + r.addMapping("/"); + } + + } + + private static class CustomDefaultServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("OK - Custom default Servlet"); + } + } + + private void assertPageContains(String pageUrl, String expectedBody) + throws IOException { + assertPageContains(pageUrl, expectedBody, HttpServletResponse.SC_OK); + } + + private void assertPageContains(String pageUrl, String expectedBody, + int expectedStatus) throws IOException { + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + pageUrl, res, null); + + Assert.assertEquals(expectedStatus, sc); + + if (expectedStatus == HttpServletResponse.SC_OK) { + String result = res.toString(); + Assert.assertTrue(result, result.contains(expectedBody)); + } + } + +} diff --git a/test/org/apache/catalina/startup/TestContextConfigAnnotation.java b/test/org/apache/catalina/startup/TestContextConfigAnnotation.java new file mode 100644 index 0000000..f395e7d --- /dev/null +++ b/test/org/apache/catalina/startup/TestContextConfigAnnotation.java @@ -0,0 +1,362 @@ +/* + * 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.catalina.startup; + +import java.beans.PropertyChangeListener; +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Loader; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.ContextConfig.JavaClassCacheEntry; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.ServletDef; +import org.apache.tomcat.util.descriptor.web.WebXml; + +/** + * Check Servlet 3.0 Spec 8.2.3.3: Override annotation parameter from web.xml or + * fragment. + * + * @author Peter Rossbach + */ +public class TestContextConfigAnnotation { + + @Test + public void testAnnotation() throws Exception { + WebXml webxml = new WebXml(); + Map javaClassCache = new HashMap<>(); + ContextConfig config = new ContextConfig(); + File pFile = paramClassResource( + "org/apache/catalina/startup/ParamServlet"); + Assert.assertTrue(pFile.exists()); + config.processAnnotationsFile(pFile, webxml, false, javaClassCache); + ServletDef servletDef = webxml.getServlets().get("param"); + Assert.assertNotNull(servletDef); + Assert.assertEquals("Hello", servletDef.getParameterMap().get("foo")); + Assert.assertEquals("World!", servletDef.getParameterMap().get("bar")); + Assert.assertEquals("param", webxml.getServletMappings().get( + "/annotation/overwrite")); + + Assert.assertEquals("param", servletDef.getDescription()); + Assert.assertEquals("param", servletDef.getDisplayName()); + Assert.assertEquals("paramLarge.png", servletDef.getLargeIcon()); + Assert.assertEquals("paramSmall.png", servletDef.getSmallIcon()); + Assert.assertEquals(Boolean.FALSE, servletDef.getAsyncSupported()); + Assert.assertEquals(Integer.valueOf(0), servletDef.getLoadOnStartup()); + Assert.assertNull(servletDef.getEnabled()); + Assert.assertNull(servletDef.getJspFile()); + + } + + @Test + public void testOverwriteAnnotation() throws Exception { + WebXml webxml = new WebXml(); + Map javaClassCache = new HashMap<>(); + ServletDef servletDef = new ServletDef(); + servletDef.setServletName("param"); + servletDef.setServletClass("org.apache.catalina.startup.ParamServlet"); + servletDef.addInitParameter("foo", "tomcat"); + servletDef.setDescription("Description"); + servletDef.setDisplayName("DisplayName"); + servletDef.setLargeIcon("LargeIcon"); + servletDef.setSmallIcon("SmallIcon"); + servletDef.setAsyncSupported("true"); + servletDef.setLoadOnStartup("1"); + + webxml.addServlet(servletDef); + webxml.addServletMapping("/param", "param"); + ContextConfig config = new ContextConfig(); + File pFile = paramClassResource( + "org/apache/catalina/startup/ParamServlet"); + Assert.assertTrue(pFile.exists()); + config.processAnnotationsFile(pFile, webxml, false, javaClassCache); + + Assert.assertEquals(servletDef, webxml.getServlets().get("param")); + + Assert.assertEquals("tomcat", servletDef.getParameterMap().get("foo")); + Assert.assertEquals("param", webxml.getServletMappings().get("/param")); + // annotation mapping not added s. Servlet Spec 3.0 (Nov 2009) + // 8.2.3.3.iv page 81 + Assert.assertNull(webxml.getServletMappings().get("/annotation/overwrite")); + + Assert.assertEquals("Description", servletDef.getDescription()); + Assert.assertEquals("DisplayName", servletDef.getDisplayName()); + Assert.assertEquals("LargeIcon", servletDef.getLargeIcon()); + Assert.assertEquals("SmallIcon", servletDef.getSmallIcon()); + Assert.assertEquals(Boolean.TRUE, servletDef.getAsyncSupported()); + Assert.assertEquals(Integer.valueOf(1), servletDef.getLoadOnStartup()); + Assert.assertNull(servletDef.getEnabled()); + Assert.assertNull(servletDef.getJspFile()); + } + + @Test + public void testNoMapping() throws Exception { + WebXml webxml = new WebXml(); + Map javaClassCache = new HashMap<>(); + ContextConfig config = new ContextConfig(); + File pFile = paramClassResource( + "org/apache/catalina/startup/NoMappingParamServlet"); + Assert.assertTrue(pFile.exists()); + config.processAnnotationsFile(pFile, webxml, false, javaClassCache); + ServletDef servletDef = webxml.getServlets().get("param1"); + Assert.assertNull(servletDef); + + webxml.addServletMapping("/param", "param1"); + config.processAnnotationsFile(pFile, webxml, false, javaClassCache); + servletDef = webxml.getServlets().get("param1"); + Assert.assertNull(servletDef); + + } + + @Test + public void testSetupWebXMLNoMapping() throws Exception { + WebXml webxml = new WebXml(); + Map javaClassCache = new HashMap<>(); + ServletDef servletDef = new ServletDef(); + servletDef.setServletName("param1"); + servletDef.setServletClass( + "org.apache.catalina.startup.NoMappingParamServlet"); + servletDef.addInitParameter("foo", "tomcat"); + + webxml.addServlet(servletDef); + webxml.addServletMapping("/param", "param1"); + ContextConfig config = new ContextConfig(); + File pFile = paramClassResource( + "org/apache/catalina/startup/NoMappingParamServlet"); + Assert.assertTrue(pFile.exists()); + config.processAnnotationsFile(pFile, webxml, false, javaClassCache); + Assert.assertEquals("tomcat", servletDef.getParameterMap().get("foo")); + Assert.assertEquals("World!", servletDef.getParameterMap().get("bar")); + ServletDef servletDef1 = webxml.getServlets().get("param1"); + Assert.assertNotNull(servletDef1); + Assert.assertEquals(servletDef, servletDef1); + } + + @Test + public void testDuplicateMapping() throws Exception { + WebXml webxml = new WebXml(); + Map javaClassCache = new HashMap<>(); + ContextConfig config = new ContextConfig(); + File pFile = paramClassResource( + "org/apache/catalina/startup/DuplicateMappingParamServlet"); + Assert.assertTrue(pFile.exists()); + try { + config.processAnnotationsFile(pFile, webxml, false, javaClassCache); + Assert.fail(); + } catch (IllegalArgumentException ex) { + // ignore + } + ServletDef servletDef = webxml.getServlets().get("param"); + Assert.assertNull(servletDef); + } + + @Test + public void testFilterMapping() throws Exception { + WebXml webxml = new WebXml(); + Map javaClassCache = new HashMap<>(); + ContextConfig config = new ContextConfig(); + File sFile = paramClassResource( + "org/apache/catalina/startup/ParamServlet"); + config.processAnnotationsFile(sFile, webxml, false, javaClassCache); + File fFile = paramClassResource( + "org/apache/catalina/startup/ParamFilter"); + config.processAnnotationsFile(fFile, webxml, false, javaClassCache); + FilterDef fdef = webxml.getFilters().get("paramFilter"); + Assert.assertNotNull(fdef); + Assert.assertEquals("Servlet says: ",fdef.getParameterMap().get("message")); + } + + @Test + public void testOverwriteFilterMapping() throws Exception { + WebXml webxml = new WebXml(); + Map javaClassCache = new HashMap<>(); + FilterDef filterDef = new FilterDef(); + filterDef.setFilterName("paramFilter"); + filterDef.setFilterClass("org.apache.catalina.startup.ParamFilter"); + filterDef.addInitParameter("message", "tomcat"); + filterDef.setDescription("Description"); + filterDef.setDisplayName("DisplayName"); + filterDef.setLargeIcon("LargeIcon"); + filterDef.setSmallIcon("SmallIcon"); + filterDef.setAsyncSupported("true"); + + + webxml.addFilter(filterDef); + FilterMap filterMap = new FilterMap(); + filterMap.addURLPatternDecoded("/param1"); + filterMap.setFilterName("paramFilter"); + webxml.addFilterMapping(filterMap); + + ContextConfig config = new ContextConfig(); + File sFile = paramClassResource( + "org/apache/catalina/startup/ParamServlet"); + config.processAnnotationsFile(sFile, webxml, false, javaClassCache); + File fFile = paramClassResource( + "org/apache/catalina/startup/ParamFilter"); + config.processAnnotationsFile(fFile, webxml, false, javaClassCache); + FilterDef fdef = webxml.getFilters().get("paramFilter"); + Assert.assertNotNull(fdef); + Assert.assertEquals(filterDef,fdef); + Assert.assertEquals("tomcat",fdef.getParameterMap().get("message")); + Set filterMappings = webxml.getFilterMappings(); + Assert.assertTrue(filterMappings.contains(filterMap)); + // annotation mapping not added s. Servlet Spec 3.0 (Nov 2009) + // 8.2.3.3.vi page 81 + String[] urlPatterns = filterMap.getURLPatterns(); + Assert.assertNotNull(urlPatterns); + Assert.assertEquals(1,urlPatterns.length); + Assert.assertEquals("/param1",urlPatterns[0]); + + // check simple Parameter + Assert.assertEquals("Description", fdef.getDescription()); + Assert.assertEquals("DisplayName", fdef.getDisplayName()); + Assert.assertEquals("LargeIcon", fdef.getLargeIcon()); + Assert.assertEquals("SmallIcon", fdef.getSmallIcon()); + // FIXME: Strange why servletDef is Boolean and FilterDef is String? + Assert.assertEquals("true", fdef.getAsyncSupported()); + + String[] dis = filterMap.getDispatcherNames(); + Assert.assertEquals(2, dis.length); + Assert.assertEquals(DispatcherType.ERROR.toString(),dis[0]); + Assert.assertEquals(DispatcherType.ASYNC.toString(),dis[1]); + + } + + @Test + public void testDuplicateFilterMapping() throws Exception { + WebXml webxml = new WebXml(); + Map javaClassCache = new HashMap<>(); + ContextConfig config = new ContextConfig(); + File pFile = paramClassResource( + "org/apache/catalina/startup/DuplicateMappingParamFilter"); + Assert.assertTrue(pFile.exists()); + try { + config.processAnnotationsFile(pFile, webxml, false, javaClassCache); + Assert.fail(); + } catch (IllegalArgumentException ex) { + // ignore + } + FilterDef filterDef = webxml.getFilters().get("paramD"); + Assert.assertNull(filterDef); + } + + @Test + public void testCheckHandleTypes() throws Exception { + Map javaClassCache = new HashMap<>(); + ContextConfig config = new ContextConfig(); + config.handlesTypesAnnotations = true; + config.handlesTypesNonAnnotations = true; + + // Need a Context, Loader and ClassLoader for checkHandleTypes + StandardContext context = new StandardContext(); + context.setLoader(new TesterLoader()); + config.context = context; + + // Add an SCI that has no interest in any type + SCI sciNone = new SCI(); + config.initializerClassMap.put(sciNone, new HashSet<>()); + + // Add an SCI with an interest in Servlets + SCI sciServlet = new SCI(); + config.initializerClassMap.put(sciServlet, new HashSet<>()); + config.typeInitializerMap.put(Servlet.class, new HashSet<>()); + config.typeInitializerMap.get(Servlet.class).add(sciServlet); + + // Add an SCI with an interest in Objects - i.e. everything + SCI sciObject = new SCI(); + config.initializerClassMap.put(sciObject, new HashSet<>()); + config.typeInitializerMap.put(Object.class, new HashSet<>()); + config.typeInitializerMap.get(Object.class).add(sciObject); + + // Scan Servlet, Filter, Servlet, Listener + WebXml ignore = new WebXml(); + File file = paramClassResource( + "org/apache/catalina/startup/ParamServlet"); + config.processAnnotationsFile(file, ignore, false, javaClassCache); + file = paramClassResource("org/apache/catalina/startup/ParamFilter"); + config.processAnnotationsFile(file, ignore, false, javaClassCache); + file = paramClassResource("org/apache/catalina/startup/TesterServlet"); + config.processAnnotationsFile(file, ignore, false, javaClassCache); + file = paramClassResource("org/apache/catalina/startup/TestListener"); + config.processAnnotationsFile(file, ignore, false, javaClassCache); + + // Check right number of classes were noted to be handled + Assert.assertEquals(0, config.initializerClassMap.get(sciNone).size()); + Assert.assertEquals(2, config.initializerClassMap.get(sciServlet).size()); + Assert.assertEquals(4, config.initializerClassMap.get(sciObject).size()); + } + + private static final class SCI implements ServletContainerInitializer { + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + // NO-OP. Just need a class that implements SCI. + } + } + + private static final class TesterLoader implements Loader { + + @Override + public void backgroundProcess() {} + @Override + public ClassLoader getClassLoader() { + return this.getClass().getClassLoader(); + } + @Override + public Context getContext() { return null; } + @Override + public void setContext(Context context) {} + @Override + public boolean getDelegate() { return false; } + @Override + public void setDelegate(boolean delegate) {} + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + } + @Override + public boolean modified() { return false; } + @Override + public void removePropertyChangeListener(PropertyChangeListener l) {} + } + + /* + * Find compiled test class + */ + private File paramClassResource(String className) throws URISyntaxException { + URL url = getClass().getClassLoader().getResource(className + ".class"); + Assert.assertNotNull(url); + + File file = new File(url.toURI()); + return file; + } +} diff --git a/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentA.java b/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentA.java new file mode 100644 index 0000000..60f9dd7 --- /dev/null +++ b/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentA.java @@ -0,0 +1,482 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.util.ContextName; + +/** + * The purpose of this class is to test the automatic deployment features of the + * {@link HostConfig} implementation. + */ +public class TestHostConfigAutomaticDeploymentA extends TomcatBaseTest { + + private static final ContextName APP_NAME = new ContextName("myapp", false); + private static final File XML_SOURCE = + new File("test/deployment/context.xml"); + private static final File WAR_XML_SOURCE = + new File("test/deployment/context.war"); + private static final File DIR_XML_SOURCE = + new File("test/deployment/dirContext"); + private static final File DIR_SOURCE = + new File("test/deployment/dirNoContext"); + + private static final int XML = 1; + private static final int EXT = 2; + private static final int WAR = 3; + private static final int DIR = 4; + + private static final String XML_COOKIE_NAME = "XML_CONTEXT"; + private static final String WAR_COOKIE_NAME = "WAR_CONTEXT"; + private static final String DIR_COOKIE_NAME = "DIR_CONTEXT"; + // private static final String DEFAULT_COOKIE_NAME = "JSESSIONID"; + + private File external; + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + external = new File(getTemporaryDirectory(), "external"); + if (!external.exists() && !external.mkdir()) { + Assert.fail("Unable to create external for test"); + } + + // Disable background thread + tomcat.getEngine().setBackgroundProcessorDelay(-1); + + // Enable deployer + tomcat.getHost().addLifecycleListener(new HostConfig()); + + // Disable deployment on start up + tomcat.getHost().setDeployOnStartup(false); + + // Clean-up after test + addDeleteOnTearDown(new File(tomcat.basedir, "/conf")); + addDeleteOnTearDown(external); + } + + + /* + * Expected behaviour for the deletion of files. + * + * Artifacts present Artifact Artifacts remaining + * XML WAR EXT DIR Removed XML WAR EXT DIR Notes + * N N N Y DIR - - - N + * N Y N N WAR - N - - + * N Y N Y DIR - Y - R 1 + * N Y N Y WAR - N - N + * Y N N N XML N - - - + * Y N N Y DIR N - - N + * Y N N Y XML R - - Y 2 + * Y N Y N EXT Y - N - + * Y N Y N XML N - Y - + * Y N Y Y DIR R - Y R 1,2 + * Y N Y Y EXT Y - N N + * Y N Y Y XML N - Y N + * Y Y N N WAR N N - - + * Y Y N N XML N N - - + * Y Y N Y DIR R Y - R 1,2 + * Y Y N Y WAR N N - - + * Y Y N Y XML R Y - Y + * + * Notes: 1. The DIR will be re-created since unpackWARs is true. + * 2. The XML will be extracted from the WAR/DIR if deployXML and + * copyXML are true. + */ + @Test + public void testDeleteDirRemoveDir() throws Exception { + doTestDelete(false, false, false, false, true, DIR, false, false, false, + null); + } + + @Test + public void testDeleteWarRemoveWar() throws Exception { + doTestDelete(false, false, false, true, false, WAR, false, false, false, + null); + } + + @Test + public void testDeleteWarDirRemoveDir() throws Exception { + doTestDelete(false, false, false, true, true, DIR, false, true, true, + WAR_COOKIE_NAME); + } + + @Test + public void testDeleteWarDirRemoveWar() throws Exception { + doTestDelete(false, false, false, true, true, WAR, false, false, false, + null); + } + + @Test + public void testDeleteXmlRemoveXml() throws Exception { + doTestDelete(true, false, false, false, false, XML, false, false, false, + null); + } + + @Test + public void testDeleteXmlDirRemoveDir() throws Exception { + doTestDelete(true, false, false, false, true, DIR, false, false, false, + null); + } + + @Test + public void testDeleteXmlDirRemoveXml() throws Exception { + doTestDelete(true, false, false, false, true, XML, false, false, true, + DIR_COOKIE_NAME); + } + + @Test + public void testDeleteXmlDirRemoveXmlCopyXml() throws Exception { + ((StandardHost) getTomcatInstance().getHost()).setCopyXML(true); + doTestDelete(true, false, false, false, true, XML, true, false, true, + DIR_COOKIE_NAME); + } + + @Test + public void testDeleteXmlExtwarRemoveExt() throws Exception { + doTestDelete(true, true, false, false, false, EXT, true, false, false, + XML_COOKIE_NAME); + } + + @Test + public void testDeleteXmlExtdirRemoveExt() throws Exception { + doTestDelete(true, false, true, false, false, EXT, true, false, false, + XML_COOKIE_NAME); + } + + @Test + public void testDeleteXmlExtwarRemoveXml() throws Exception { + doTestDelete(true, true, false, false, false, XML, false, false, false, + null); + } + + @Test + public void testDeleteXmlExtdirRemoveXml() throws Exception { + doTestDelete(true, false, true, false, false, XML, false, false, false, + null); + } + + @Test + public void testDeleteXmlExtwarDirRemoveDir() throws Exception { + doTestDelete(true, true, false, false, true, DIR, true, false, true, + XML_COOKIE_NAME); + } + + @Test + public void testDeleteXmlExtwarDirRemoveExt() throws Exception { + doTestDelete(true, true, false, false, true, EXT, true, false, false, + XML_COOKIE_NAME); + } + + @Test + public void testDeleteXmlExtwarDirRemoveXml() throws Exception { + doTestDelete(true, true, false, false, true, XML, false, false, false, + null); + } + + @Test + public void testDeleteXmlWarRemoveWar() throws Exception { + doTestDelete(true, false, false, true, false, WAR, false, false, false, + null); + } + + @Test + public void testDeleteXmlWarRemoveXml() throws Exception { + doTestDelete(true, false, false, true, false, XML, false, true, false, + WAR_COOKIE_NAME); + } + + @Test + public void testDeleteXmlWarRemoveXmlCopyXml() throws Exception { + ((StandardHost) getTomcatInstance().getHost()).setCopyXML(true); + doTestDelete(true, false, false, true, false, XML, true, true, false, + WAR_COOKIE_NAME); + } + + @Test + public void testDeleteXmlWarDirRemoveDir() throws Exception { + doTestDelete(true, false, false, true, true, DIR, false, true, true, + WAR_COOKIE_NAME); + } + + @Test + public void testDeleteXmlWarDirRemoveDirCopyXml() throws Exception { + ((StandardHost) getTomcatInstance().getHost()).setCopyXML(true); + doTestDelete(true, false, false, true, true, DIR, true, true, true, + WAR_COOKIE_NAME); + } + + @Test + public void testDeleteXmlWarDirRemoveWar() throws Exception { + doTestDelete(true, false, false, true, true, WAR, false, false, false, + null); + } + + @Test + public void testDeleteXmlWarDirRemoveWarCopyXml() throws Exception { + ((StandardHost) getTomcatInstance().getHost()).setCopyXML(true); + doTestDelete(true, false, false, true, true, WAR, false, false, false, + null); + } + + @Test + public void testDeleteXmlWarDirRemoveXml() throws Exception { + doTestDelete(true, false, false, true, true, XML, false, true, true, + DIR_COOKIE_NAME); + } + + @Test + public void testDeleteXmlWarDirRemoveXmlCopyXml() throws Exception { + ((StandardHost) getTomcatInstance().getHost()).setCopyXML(true); + doTestDelete(true, false, false, true, true, XML, true, true, true, + WAR_COOKIE_NAME); + } + + private void doTestDelete(boolean startXml, boolean startExternalWar, + boolean startExternalDir, boolean startWar, boolean startDir, + int toDelete, boolean resultXml, boolean resultWar, + boolean resultDir, String resultCookieName) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + + // Init + File xml = null; + File ext = null; + File war = null; + File dir = null; + + if (startXml && !startExternalWar && !startExternalDir) { + xml = createXmlInConfigBaseForAppbase(); + } + if (startExternalWar) { + ext = createWar(WAR_XML_SOURCE, false); + xml = createXmlInConfigBaseForExternal(ext); + } + if (startExternalDir) { + ext = createDirInExternal(true); + xml = createXmlInConfigBaseForExternal(ext); + } + if (startWar) { + war = createWar(WAR_XML_SOURCE, true); + } + if (startDir) { + dir = createDirInAppbase(true); + } + + if ((startWar || startExternalWar) && !startDir) { + host.setUnpackWARs(false); + } + + // Deploy the files we copied + tomcat.start(); + host.backgroundProcess(); + + // Remove the specified file + switch (toDelete) { + case XML: + ExpandWar.delete(xml); + break; + case EXT: + ExpandWar.delete(ext); + break; + case WAR: + ExpandWar.delete(war); + break; + case DIR: + ExpandWar.delete(dir); + break; + default: + Assert.fail(); + } + + // Trigger an auto-deployment cycle + host.backgroundProcess(); + + Context ctxt = (Context) host.findChild(APP_NAME.getName()); + + // Check the results + // External WAR and DIR should only be deleted if the test is testing + // behaviour when the external resource is deleted + if (toDelete == EXT) { + if (ext == null) { + Assert.fail(); + } else { + Assert.assertFalse(ext.exists()); + } + } else { + if (startExternalWar) { + if (ext == null) { + Assert.fail(); + } else { + Assert.assertTrue(ext.isFile()); + } + } + if (startExternalDir) { + if (ext == null) { + Assert.fail(); + } else { + Assert.assertTrue(ext.isDirectory()); + } + } + } + + if (resultXml) { + if (xml == null) { + Assert.fail(); + } else { + Assert.assertTrue(xml.isFile()); + } + } + if (resultWar) { + if (war == null) { + Assert.fail(); + } else { + Assert.assertTrue(war.isFile()); + } + } + if (resultDir) { + if (dir == null) { + Assert.fail(); + } else { + Assert.assertTrue(dir.isDirectory()); + } + } + + if (!resultXml && (startExternalWar || startExternalDir)) { + Assert.assertNull(ctxt); + } + if (!resultWar && !resultDir) { + if (resultXml) { + Assert.assertNotNull(ctxt); + Assert.assertEquals(LifecycleState.FAILED, ctxt.getState()); + } else { + Assert.assertNull(ctxt); + } + } + + if (ctxt != null) { + Assert.assertEquals(resultCookieName, ctxt.getSessionCookieName()); + } + } + + + private File createDirInAppbase(boolean withXml) throws IOException { + File dir = new File(getTomcatInstance().getHost().getAppBaseFile(), + APP_NAME.getBaseName()); + if (withXml) { + recursiveCopy(DIR_XML_SOURCE.toPath(), dir.toPath()); + } else { + recursiveCopy(DIR_SOURCE.toPath(), dir.toPath()); + } + return dir; + } + + private File createDirInExternal(boolean withXml) throws IOException { + File ext = new File(external, "external" + ".war"); + if (withXml) { + recursiveCopy(DIR_XML_SOURCE.toPath(), ext.toPath()); + } else { + recursiveCopy(DIR_SOURCE.toPath(), ext.toPath()); + } + return ext; + } + + private File createWar(File src, boolean useAppbase) throws IOException { + File dest; + if (useAppbase) { + dest = new File(getTomcatInstance().getHost().getAppBaseFile(), + APP_NAME.getBaseName() + ".war"); + } else { + dest = new File(external, "external" + ".war"); + } + Files.copy(src.toPath(), dest.toPath()); + // Make sure that HostConfig thinks the WAR has been modified. + Assert.assertTrue("Failed to set last modified for [" + dest + "]", dest.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return dest; + } + + private File createXmlInConfigBaseForAppbase() throws IOException { + File xml = getXmlInConfigBaseForAppbase(); + File parent = xml.getParentFile(); + if (!parent.isDirectory()) { + Assert.assertTrue(parent.mkdirs()); + } + Files.copy(XML_SOURCE.toPath(), xml.toPath()); + // Make sure that HostConfig thinks the xml has been modified. + Assert.assertTrue("Failed to set last modified for [" + xml + "]", xml.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return xml; + } + + private File getXmlInConfigBaseForAppbase() { + Host host = getTomcatInstance().getHost(); + return new File(host.getConfigBaseFile(), APP_NAME + ".xml"); + } + + private File createXmlInConfigBaseForExternal(File ext) throws IOException { + return createXmlInConfigBaseForExternal(ext, false); + } + + private File createXmlInConfigBaseForExternal(File ext, boolean antiLocking) + throws IOException { + File xml = new File(getTomcatInstance().getHost().getConfigBaseFile(), + APP_NAME + ".xml"); + File parent = xml.getParentFile(); + if (!parent.isDirectory()) { + Assert.assertTrue(parent.mkdirs()); + } + + try (FileOutputStream fos = new FileOutputStream(xml)) { + StringBuilder context = new StringBuilder(); + context.append(""); + fos.write(context.toString().getBytes(StandardCharsets.ISO_8859_1)); + } + // Make sure that HostConfig thinks the xml has been modified. + Assert.assertTrue("Failed to set last modified for [" + xml + "]", xml.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return xml; + } + + public static class TesterContext extends StandardContext { + // No functional change + } +} diff --git a/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentB.java b/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentB.java new file mode 100644 index 0000000..b892b19 --- /dev/null +++ b/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentB.java @@ -0,0 +1,687 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.util.ContextName; + +/** + * The purpose of this class is to test the automatic deployment features of the + * {@link HostConfig} implementation. + */ +public class TestHostConfigAutomaticDeploymentB extends TomcatBaseTest { + + private static final ContextName APP_NAME = new ContextName("myapp", false); + private static final File XML_SOURCE = + new File("test/deployment/context.xml"); + private static final File WAR_XML_SOURCE = + new File("test/deployment/context.war"); + private static final File WAR_SOURCE = + new File("test/deployment/noContext.war"); + private static final File DIR_XML_SOURCE = + new File("test/deployment/dirContext"); + private static final File DIR_SOURCE = + new File("test/deployment/dirNoContext"); + + private static final String XML_COOKIE_NAME = "XML_CONTEXT"; + private static final String WAR_COOKIE_NAME = "WAR_CONTEXT"; + private static final String DIR_COOKIE_NAME = "DIR_CONTEXT"; + // private static final String DEFAULT_COOKIE_NAME = "JSESSIONID"; + + private File external; + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + external = new File(getTemporaryDirectory(), "external"); + if (!external.exists() && !external.mkdir()) { + Assert.fail("Unable to create external for test"); + } + + // Disable background thread + tomcat.getEngine().setBackgroundProcessorDelay(-1); + + // Enable deployer + tomcat.getHost().addLifecycleListener(new HostConfig()); + + // Disable deployment on start up + tomcat.getHost().setDeployOnStartup(false); + + // Clean-up after test + addDeleteOnTearDown(new File(tomcat.basedir, "/conf")); + addDeleteOnTearDown(external); + } + + + /* + * Expected behaviour for deployment of an XML file. + * deployXML copyXML unpackWARs XML WAR DIR + * Y/N Y/N Y/N Y N N + * + * Note: Context will fail to start because no valid docBase is present. + */ + @Test + public void testDeploymentXmlFFF() throws Exception { + createXmlInConfigBaseForAppbase(); + doTestDeployment(false, false, false, + LifecycleState.FAILED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlFFT() throws Exception { + createXmlInConfigBaseForAppbase(); + doTestDeployment(false, false, true, + LifecycleState.FAILED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlFTF() throws Exception { + createXmlInConfigBaseForAppbase(); + doTestDeployment(false, true, false, + LifecycleState.FAILED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlFTT() throws Exception { + createXmlInConfigBaseForAppbase(); + doTestDeployment(false, true, true, + LifecycleState.FAILED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlTFF() throws Exception { + createXmlInConfigBaseForAppbase(); + doTestDeployment(true, false, false, + LifecycleState.FAILED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlTFT() throws Exception { + createXmlInConfigBaseForAppbase(); + doTestDeployment(true, false, true, + LifecycleState.FAILED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlTTF() throws Exception { + createXmlInConfigBaseForAppbase(); + doTestDeployment(true, true, false, + LifecycleState.FAILED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlTTT() throws Exception { + createXmlInConfigBaseForAppbase(); + doTestDeployment(true, true, true, + LifecycleState.FAILED, XML_COOKIE_NAME, true, false, false); + } + + + /* + * Expected behaviour for deployment of an XML file that points to an + * external WAR. + * deployXML copyXML unpackWARs XML WAR DIR + * Y/N Y/N Y Y N Y + * Y/N Y/N N Y N N + * + * Notes: No WAR file is present in the appBase because it is an external + * WAR. + * Any context.xml file embedded in the external WAR file is ignored. + */ + @Test + public void testDeploymentXmlExternalWarXmlFFF() throws Exception { + File war = createWar(WAR_XML_SOURCE, false); + createXmlInConfigBaseForExternal(war); + doTestDeployment(false, false, false, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalWarXmlFFT() throws Exception { + File war = createWar(WAR_XML_SOURCE, false); + createXmlInConfigBaseForExternal(war); + doTestDeployment(false, false, true, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, true); + } + + @Test + public void testDeploymentXmlExternalWarXmlFTF() throws Exception { + File war = createWar(WAR_XML_SOURCE, false); + createXmlInConfigBaseForExternal(war); + doTestDeployment(false, true, false, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalWarXmlFTT() throws Exception { + File war = createWar(WAR_XML_SOURCE, false); + createXmlInConfigBaseForExternal(war); + doTestDeployment(false, true, true, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, true); + } + + @Test + public void testDeploymentXmlExternalWarXmlTFF() throws Exception { + File war = createWar(WAR_XML_SOURCE, false); + createXmlInConfigBaseForExternal(war); + doTestDeployment(true, false, false, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalWarXmlTFT() throws Exception { + File war = createWar(WAR_XML_SOURCE, false); + createXmlInConfigBaseForExternal(war); + doTestDeployment(true, false, true, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, true); + } + + @Test + public void testDeploymentXmlExternalWarXmlTTF() throws Exception { + File war = createWar(WAR_XML_SOURCE, false); + createXmlInConfigBaseForExternal(war); + doTestDeployment(true, true, false, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalWarXmlTTT() throws Exception { + File war = createWar(WAR_XML_SOURCE, false); + createXmlInConfigBaseForExternal(war); + doTestDeployment(true, true, true, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, true); + } + + + /* + * Expected behaviour for deployment of an XML file that points to an + * external DIR. + * deployXML copyXML unpackWARs XML WAR DIR + * Y/N Y/N Y/N Y N N + * + * Notes: Any context.xml file embedded in the external DIR file is ignored. + */ + @Test + public void testDeploymentXmlExternalDirXmlFFF() throws Exception { + File dir = createDirInExternal(true); + createXmlInConfigBaseForExternal(dir); + doTestDeployment(false, false, false, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalDirXmlFFT() throws Exception { + File dir = createDirInExternal(true); + createXmlInConfigBaseForExternal(dir); + doTestDeployment(false, false, true, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalDirXmlFTF() throws Exception { + File dir = createDirInExternal(true); + createXmlInConfigBaseForExternal(dir); + doTestDeployment(false, true, false, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalDirXmlFTT() throws Exception { + File dir = createDirInExternal(true); + createXmlInConfigBaseForExternal(dir); + doTestDeployment(false, true, true, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalDirXmlTFF() throws Exception { + File dir = createDirInExternal(true); + createXmlInConfigBaseForExternal(dir); + doTestDeployment(true, false, false, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalDirXmlTFT() throws Exception { + File dir = createDirInExternal(true); + createXmlInConfigBaseForExternal(dir); + doTestDeployment(true, false, true, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalDirXmlTTF() throws Exception { + File dir = createDirInExternal(true); + createXmlInConfigBaseForExternal(dir); + doTestDeployment(true, true, false, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + @Test + public void testDeploymentXmlExternalDirXmlTTT() throws Exception { + File dir = createDirInExternal(true); + createXmlInConfigBaseForExternal(dir); + doTestDeployment(true, true, true, + LifecycleState.STARTED, XML_COOKIE_NAME, true, false, false); + } + + + /* + * Expected behaviour for deployment of a WAR with an embedded XML file. + * deployXML copyXML unpackWARs XML WAR DIR + * N Y/N N N Y N + * N Y/N Y N Y Y + * Y N N N Y N + * Y N Y N Y Y + * Y Y N Y Y N + * Y Y Y Y Y Y + */ + @Test + public void testDeploymentWarXmlFFF() throws Exception { + createWar(WAR_XML_SOURCE, true); + doTestDeployment(false, false, false, + LifecycleState.FAILED, null, false, true, false); + } + + @Test + public void testDeploymentWarXmlFFT() throws Exception { + createWar(WAR_XML_SOURCE, true); + doTestDeployment(false, false, true, + LifecycleState.FAILED, null, false, true, true); + } + + @Test + public void testDeploymentWarXmlFTF() throws Exception { + createWar(WAR_XML_SOURCE, true); + doTestDeployment(false, true, false, + LifecycleState.FAILED, null, false, true, false); + } + + @Test + public void testDeploymentWarXmlFTT() throws Exception { + createWar(WAR_XML_SOURCE, true); + doTestDeployment(false, true, true, + LifecycleState.FAILED, null, false, true, true); + } + + @Test + public void testDeploymentWarXmlTFF() throws Exception { + createWar(WAR_XML_SOURCE, true); + doTestDeployment(true, false, false, + LifecycleState.STARTED, WAR_COOKIE_NAME, false, true, false); + } + + @Test + public void testDeploymentWarXmlTFT() throws Exception { + createWar(WAR_XML_SOURCE, true); + doTestDeployment(true, false, true, + LifecycleState.STARTED, WAR_COOKIE_NAME, false, true, true); + } + + @Test + public void testDeploymentWarXmlTTF() throws Exception { + createWar(WAR_XML_SOURCE, true); + doTestDeployment(true, true, false, + LifecycleState.STARTED, WAR_COOKIE_NAME, true, true, false); + } + + @Test + public void testDeploymentWarXmlTTT() throws Exception { + createWar(WAR_XML_SOURCE, true); + doTestDeployment(true, true, true, + LifecycleState.STARTED, WAR_COOKIE_NAME, true, true, true); + } + + + /* + * Expected behaviour for deployment of a WAR without an embedded XML file. + * deployXML copyXML unpackWARs XML WAR DIR + * Y/N Y/N N N Y N + * Y/N Y/N Y N Y Y + */ + @Test + public void testDeploymentWarFFF() throws Exception { + createWar(WAR_SOURCE, true); + doTestDeployment(false, false, false, + LifecycleState.STARTED, null, false, true, false); + } + + @Test + public void testDeploymentWarFFT() throws Exception { + createWar(WAR_SOURCE, true); + doTestDeployment(false, false, true, + LifecycleState.STARTED, null, false, true, true); + } + + @Test + public void testDeploymentWarFTF() throws Exception { + createWar(WAR_SOURCE, true); + doTestDeployment(false, true, false, + LifecycleState.STARTED, null, false, true, false); + } + + @Test + public void testDeploymentWarFTT() throws Exception { + createWar(WAR_SOURCE, true); + doTestDeployment(false, true, true, + LifecycleState.STARTED, null, false, true, true); + } + + @Test + public void testDeploymentWarTFF() throws Exception { + createWar(WAR_SOURCE, true); + doTestDeployment(true, false, false, + LifecycleState.STARTED, null, false, true, false); + } + + @Test + public void testDeploymentWarTFT() throws Exception { + createWar(WAR_SOURCE, true); + doTestDeployment(true, false, true, + LifecycleState.STARTED, null, false, true, true); + } + + @Test + public void testDeploymentWarTTF() throws Exception { + createWar(WAR_SOURCE, true); + doTestDeployment(true, true, false, + LifecycleState.STARTED, null, false, true, false); + } + + @Test + public void testDeploymentWarTTT() throws Exception { + createWar(WAR_SOURCE, true); + doTestDeployment(true, true, true, + LifecycleState.STARTED, null, false, true, true); + } + + + /* + * Expected behaviour for deployment of a DIR with an embedded XML file. + * deployXML copyXML unpackWARs XML WAR DIR + * N Y/N Y/N N N Y + * Y N Y/N N N Y + * Y Y Y/N Y N Y + */ + @Test + public void testDeploymentDirXmlFFF() throws Exception { + createDirInAppbase(true); + doTestDeployment(false, false, false, + LifecycleState.FAILED, null, false, false, true); + } + + @Test + public void testDeploymentDirXmlFFT() throws Exception { + createDirInAppbase(true); + doTestDeployment(false, false, true, + LifecycleState.FAILED, null, false, false, true); + } + + @Test + public void testDeploymentDirXmlFTF() throws Exception { + createDirInAppbase(true); + doTestDeployment(false, true, false, + LifecycleState.FAILED, null, false, false, true); + } + + @Test + public void testDeploymentDirXmlFTT() throws Exception { + createDirInAppbase(true); + doTestDeployment(false, true, true, + LifecycleState.FAILED, null, false, false, true); + } + + @Test + public void testDeploymentDirXmlTFF() throws Exception { + createDirInAppbase(true); + doTestDeployment(true, false, false, + LifecycleState.STARTED, DIR_COOKIE_NAME, false, false, true); + } + + @Test + public void testDeploymentDirXmlTFT() throws Exception { + createDirInAppbase(true); + doTestDeployment(true, false, true, + LifecycleState.STARTED, DIR_COOKIE_NAME, false, false, true); + } + + @Test + public void testDeploymentDirXmlTTF() throws Exception { + createDirInAppbase(true); + doTestDeployment(true, true, false, + LifecycleState.STARTED, DIR_COOKIE_NAME, true, false, true); + } + + @Test + public void testDeploymentDirXmlTTT() throws Exception { + createDirInAppbase(true); + doTestDeployment(true, true, true, + LifecycleState.STARTED, DIR_COOKIE_NAME, true, false, true); + } + + + /* + * Expected behaviour for deployment of a DIR without an embedded XML file. + * deployXML copyXML unpackWARs XML WAR DIR + * Y/N Y/N Y/N N N Y + */ + @Test + public void testDeploymentDirFFF() throws Exception { + createDirInAppbase(false); + doTestDeployment(false, false, false, + LifecycleState.STARTED, null, false, false, true); + } + + @Test + public void testDeploymentDirFFT() throws Exception { + createDirInAppbase(false); + doTestDeployment(false, false, true, + LifecycleState.STARTED, null, false, false, true); + } + + @Test + public void testDeploymentDirFTF() throws Exception { + createDirInAppbase(false); + doTestDeployment(false, true, false, + LifecycleState.STARTED, null, false, false, true); + } + + @Test + public void testDeploymentDirFTT() throws Exception { + createDirInAppbase(false); + doTestDeployment(false, true, true, + LifecycleState.STARTED, null, false, false, true); + } + + @Test + public void testDeploymentDirTFF() throws Exception { + createDirInAppbase(false); + doTestDeployment(true, false, false, + LifecycleState.STARTED, null, false, false, true); + } + + @Test + public void testDeploymentDirTFT() throws Exception { + createDirInAppbase(false); + doTestDeployment(true, false, true, + LifecycleState.STARTED, null, false, false, true); + } + + @Test + public void testDeploymentDirTTF() throws Exception { + createDirInAppbase(false); + doTestDeployment(true, true, false, + LifecycleState.STARTED, null, false, false, true); + } + + @Test + public void testDeploymentDirTTT() throws Exception { + createDirInAppbase(false); + doTestDeployment(true, true, true, + LifecycleState.STARTED, null, false, false, true); + } + + private void doTestDeployment(boolean deployXML, boolean copyXML, + boolean unpackWARs, LifecycleState resultState, String cookieName, + boolean resultXml, boolean resultWar, boolean resultDir) + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // Start the instance + tomcat.start(); + + // Set the attributes + StandardHost host = (StandardHost) tomcat.getHost(); + host.setDeployXML(deployXML); + host.setCopyXML(copyXML); + host.setUnpackWARs(unpackWARs); + + // Trigger automatic deployment + host.backgroundProcess(); + + // Test the results + Context ctxt = (Context) tomcat.getHost().findChild(APP_NAME.getPath()); + if (resultState == null) { + Assert.assertNull(ctxt); + } else { + Assert.assertNotNull(ctxt); + Assert.assertEquals(resultState, ctxt.getState()); + Assert.assertEquals(cookieName, ctxt.getSessionCookieName()); + } + + File xml = new File( + host.getConfigBaseFile(), APP_NAME.getBaseName() + ".xml"); + Assert.assertEquals( + Boolean.valueOf(resultXml), Boolean.valueOf(xml.isFile())); + + File war = new File( + host.getAppBaseFile(), APP_NAME.getBaseName() + ".war"); + Assert.assertEquals( + Boolean.valueOf(resultWar), Boolean.valueOf(war.isFile())); + + File dir = new File(host.getAppBase(), APP_NAME.getBaseName()); + Assert.assertEquals( + Boolean.valueOf(resultDir), Boolean.valueOf(dir.isDirectory())); + } + + + private File createDirInAppbase(boolean withXml) throws IOException { + File dir = new File(getTomcatInstance().getHost().getAppBaseFile(), + APP_NAME.getBaseName()); + if (withXml) { + recursiveCopy(DIR_XML_SOURCE.toPath(), dir.toPath()); + } else { + recursiveCopy(DIR_SOURCE.toPath(), dir.toPath()); + } + return dir; + } + + private File createDirInExternal(boolean withXml) throws IOException { + File ext = new File(external, "external" + ".war"); + if (withXml) { + recursiveCopy(DIR_XML_SOURCE.toPath(), ext.toPath()); + } else { + recursiveCopy(DIR_SOURCE.toPath(), ext.toPath()); + } + return ext; + } + + private File createWar(File src, boolean useAppbase) throws IOException { + File dest; + if (useAppbase) { + dest = new File(getTomcatInstance().getHost().getAppBaseFile(), + APP_NAME.getBaseName() + ".war"); + } else { + dest = new File(external, "external" + ".war"); + } + Files.copy(src.toPath(), dest.toPath()); + // Make sure that HostConfig thinks the WAR has been modified. + Assert.assertTrue("Failed to set last modified for [" + dest + "]", dest.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return dest; + } + + private File createXmlInConfigBaseForAppbase() throws IOException { + File xml = getXmlInConfigBaseForAppbase(); + File parent = xml.getParentFile(); + if (!parent.isDirectory()) { + Assert.assertTrue(parent.mkdirs()); + } + Files.copy(XML_SOURCE.toPath(), xml.toPath()); + // Make sure that HostConfig thinks the xml has been modified. + Assert.assertTrue("Failed to set last modified for [" + xml + "]", xml.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return xml; + } + + private File getXmlInConfigBaseForAppbase() { + Host host = getTomcatInstance().getHost(); + return new File(host.getConfigBaseFile(), APP_NAME + ".xml"); + } + + private File createXmlInConfigBaseForExternal(File ext) throws IOException { + return createXmlInConfigBaseForExternal(ext, false); + } + + private File createXmlInConfigBaseForExternal(File ext, boolean antiLocking) + throws IOException { + File xml = new File(getTomcatInstance().getHost().getConfigBaseFile(), + APP_NAME + ".xml"); + File parent = xml.getParentFile(); + if (!parent.isDirectory()) { + Assert.assertTrue(parent.mkdirs()); + } + + try (FileOutputStream fos = new FileOutputStream(xml)) { + StringBuilder context = new StringBuilder(); + context.append(""); + fos.write(context.toString().getBytes(StandardCharsets.ISO_8859_1)); + } + // Make sure that HostConfig thinks the xml has been modified. + Assert.assertTrue("Failed to set last modified for [" + xml + "]", xml.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return xml; + } + + public static class TesterContext extends StandardContext { + // No functional change + } +} diff --git a/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentC.java b/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentC.java new file mode 100644 index 0000000..0b2189f --- /dev/null +++ b/test/org/apache/catalina/startup/TestHostConfigAutomaticDeploymentC.java @@ -0,0 +1,1162 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.util.ContextName; + +/** + * The purpose of this class is to test the automatic deployment features of the + * {@link HostConfig} implementation. + */ +public class TestHostConfigAutomaticDeploymentC extends TomcatBaseTest { + + private static final ContextName APP_NAME = new ContextName("myapp", false); + private static final File XML_SOURCE = + new File("test/deployment/context.xml"); + private static final File WAR_XML_SOURCE = + new File("test/deployment/context.war"); + private static final File WAR_XML_COPYXML_FALSE_SOURCE = + new File("test/deployment/contextCopyXMLFalse.war"); + private static final File WAR_XML_COPYXML_TRUE_SOURCE = + new File("test/deployment/contextCopyXMLTrue.war"); + private static final File WAR_XML_UNPACKWAR_FALSE_SOURCE = + new File("test/deployment/contextUnpackWARFalse.war"); + private static final File WAR_XML_UNPACKWAR_TRUE_SOURCE = + new File("test/deployment/contextUnpackWARTrue.war"); + private static final File WAR_SOURCE = + new File("test/deployment/noContext.war"); + private static final File WAR_BROKEN_SOURCE = + new File("test/deployment/broken.war"); + private static final File DIR_XML_SOURCE = + new File("test/deployment/dirContext"); + private static final File DIR_XML_SOURCE_META_INF = + new File("test/deployment/dirContext/META-INF"); + private static final File DIR_SOURCE = + new File("test/deployment/dirNoContext"); + + private static final int XML = 1; + private static final int EXT = 2; + private static final int WAR = 3; + private static final int DIR = 4; + private static final int DIR_XML = 5; + + private static final int NONE = 1; + private static final int RELOAD = 2; + private static final int REDEPLOY = 3; + + private static final String XML_COOKIE_NAME = "XML_CONTEXT"; + private static final String WAR_COOKIE_NAME = "WAR_CONTEXT"; + private static final String DIR_COOKIE_NAME = "DIR_CONTEXT"; + // private static final String DEFAULT_COOKIE_NAME = "JSESSIONID"; + + private File external; + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + external = new File(getTemporaryDirectory(), "external"); + if (!external.exists() && !external.mkdir()) { + Assert.fail("Unable to create external for test"); + } + + // Disable background thread + tomcat.getEngine().setBackgroundProcessorDelay(-1); + + // Enable deployer + tomcat.getHost().addLifecycleListener(new HostConfig()); + + // Disable deployment on start up + tomcat.getHost().setDeployOnStartup(false); + + // Clean-up after test + addDeleteOnTearDown(new File(tomcat.basedir, "/conf")); + addDeleteOnTearDown(external); + } + + + /* + * Expected behaviour for modification of files. + * + * Artifacts present Artifact Artifacts remaining + * XML WAR EXT DIR Modified XML WAR EXT DIR Action + * N N N Y DIR - - - M None + * N Y N N WAR - M - - Redeploy + * N Y N Y DIR - Y - M None + * N Y N Y WAR - M - R Redeploy + * Y N N N XML M - - - Redeploy + * Y N N Y DIR Y - - M None + * Y N N Y XML M - - Y Redeploy + * Y N Y N EXT Y - M - Reload if WAR + * Y N Y N XML M - Y - Redeploy + * Y N Y Y DIR Y - Y M None + * Y N Y Y EXT Y - M R Reload + * Y N Y Y XML M - Y Y Redeploy + * Y Y N N WAR Y M - - Reload + * Y Y N N XML M Y - - Redeploy + * Y Y N Y DIR Y Y - M None + * Y Y N Y WAR Y M - - Reload + * Y Y N Y XML M Y - Y Redeploy + */ + @Test + public void testModifyDirUpdateDir() throws Exception { + doTestModify(false, false, false, false, true, DIR, + false, false, true, DIR_COOKIE_NAME, NONE); + } + + @Test + public void testModifyWarUpdateWar() throws Exception { + doTestModify(false, false, false, true, false, WAR, + false, true, false, WAR_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testModifyWarDirUpdateDir() throws Exception { + // DIR_COOKIE_NAME since Tomcat is going to assume DIR is expanded WAR + doTestModify(false, false, false, true, true, DIR, + false, true, true, DIR_COOKIE_NAME, NONE); + } + + @Test + public void testModifyWarDirUpdateWar() throws Exception { + doTestModify(false, false, false, true, true, WAR, + false, true, true, WAR_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testModifyXmlUpdateXml() throws Exception { + doTestModify(true, false, false, false, false, XML, + true, false, false, XML_COOKIE_NAME, REDEPLOY, + LifecycleState.FAILED); + } + + @Test + public void testModifyXmlDirUpdateDir() throws Exception { + doTestModify(true, false, false, false, true, DIR, + true, false, true, XML_COOKIE_NAME, NONE); + } + + @Test + public void testModifyXmlDirUpdateXml() throws Exception { + doTestModify(true, false, false, false, true, XML, + true, false, true, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testModifyXmlExtwarUpdateExtwar() throws Exception { + doTestModify(true, true, false, false, false, EXT, + true, false, false, XML_COOKIE_NAME, RELOAD); + } + + @Test + public void testModifyXmlExtdirUpdateExtdir() throws Exception { + doTestModify(true, false, true, false, false, EXT, + true, false, false, XML_COOKIE_NAME, NONE); + } + + @Test + public void testModifyXmlExtwarUpdateXml() throws Exception { + doTestModify(true, true, false, false, false, XML, + true, false, false, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testModifyXmlExtdirUpdateXml() throws Exception { + doTestModify(true, false, true, false, false, XML, + true, false, false, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testModifyXmlExtwarDirUpdateDir() throws Exception { + doTestModify(true, true, false, false, true, DIR, + true, false, false, XML_COOKIE_NAME, NONE); + } + + @Test + public void testModifyXmlExtwarDirUpdateExt() throws Exception { + doTestModify(true, true, false, false, true, EXT, + true, false, true, XML_COOKIE_NAME, RELOAD); + } + + @Test + public void testModifyXmlExtwarDirUpdateXml() throws Exception { + doTestModify(true, true, false, false, true, XML, + true, false, false, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testModifyXmlWarUpdateWar() throws Exception { + doTestModify(true, false, false, true, false, WAR, + true, true, false, XML_COOKIE_NAME, RELOAD); + } + + @Test + public void testModifyXmlWarUpdateXml() throws Exception { + doTestModify(true, false, false, true, false, XML, + true, true, false, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testModifyXmlWarDirUpdateDir() throws Exception { + doTestModify(true, false, false, true, true, DIR, + true, true, true, XML_COOKIE_NAME, NONE); + } + + @Test + public void testModifyXmlWarDirUpdateWar() throws Exception { + doTestModify(true, false, false, true, true, WAR, + true, true, true, XML_COOKIE_NAME, RELOAD); + } + + @Test + public void testModifyXmlWarDirUpdateXml() throws Exception { + doTestModify(true, false, false, true, true, XML, + true, true, true, XML_COOKIE_NAME, REDEPLOY); + } + + private void doTestModify(boolean startXml, boolean startExternalWar, + boolean startExternalDir, boolean startWar, boolean startDir, + int toModify, boolean resultXml, boolean resultWar, + boolean resultDir, String resultCookieName, int resultAction) + throws Exception { + doTestModify(startXml, startExternalWar, startExternalDir, startWar, + startDir, toModify, resultXml, resultWar, resultDir, + resultCookieName, resultAction, LifecycleState.STARTED); + } + + private void doTestModify(boolean startXml, boolean startExternalWar, + boolean startExternalDir, boolean startWar, boolean startDir, + int toModify, boolean resultXml, boolean resultWar, + boolean resultDir, String resultCookieName, int resultAction, + LifecycleState resultState) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + + // Init + File xml = null; + File ext = null; + File war = null; + File dir = null; + + long testStartTime = System.currentTimeMillis(); + + if (startXml && !startExternalWar && !startExternalDir) { + xml = createXmlInConfigBaseForAppbase(); + } + if (startExternalWar) { + ext = createWar(WAR_XML_SOURCE, false); + xml = createXmlInConfigBaseForExternal(ext); + } + if (startExternalDir) { + ext = createDirInAppbase(true); + xml = createXmlInConfigBaseForExternal(ext); + } + if (startWar) { + war = createWar(WAR_XML_SOURCE, true); + } + if (startDir) { + dir = createDirInAppbase(true); + } + + if ((startWar || startExternalWar) && !startDir) { + host.setUnpackWARs(false); + } + + // Deploy the files we copied + tomcat.start(); + host.backgroundProcess(); + + // Update the last modified time. Make sure that the OS reports a change + // in modification time that HostConfig can detect. Change is made + // relative to test start time to ensure new modification times are + // sufficiently different. + switch (toModify) { + case XML: + if (xml == null) { + Assert.fail(); + } else { + Assert.assertTrue("Failed to set last modified for [" + xml + "]", xml.setLastModified( + testStartTime - 10 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + } + break; + case EXT: + if (ext == null) { + Assert.fail(); + } else { + Assert.assertTrue("Failed to set last modified for [" + ext + "]", ext.setLastModified( + testStartTime - 10 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + } + break; + case WAR: + if (war == null) { + Assert.fail(); + } else { + Assert.assertTrue("Failed to set last modified for [" + war + "]", war.setLastModified( + testStartTime - 10 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + } + break; + case DIR: + if (dir == null) { + Assert.fail(); + } else { + Assert.assertTrue("Failed to set last modified for [" + dir + "]", dir.setLastModified( + testStartTime - 10 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + } + break; + default: + Assert.fail(); + } + + Context oldContext = (Context) host.findChild(APP_NAME.getName()); + StateTracker tracker = new StateTracker(); + oldContext.addLifecycleListener(tracker); + + // Trigger an auto-deployment cycle + host.backgroundProcess(); + + Context newContext = (Context) host.findChild(APP_NAME.getName()); + + // Check the results + if (resultXml) { + if (xml == null) { + Assert.fail(); + } else { + Assert.assertTrue(xml.isFile()); + } + } + if (resultWar) { + if (war == null) { + Assert.fail(); + } else { + Assert.assertTrue(war.isFile()); + } + } + if (resultDir) { + if (dir == null) { + Assert.fail(); + } else { + Assert.assertTrue(dir.isDirectory()); + } + } + + if (!resultXml && (startExternalWar || startExternalDir)) { + Assert.assertNull(newContext); + } + if (!resultWar && !resultDir) { + if (resultXml) { + Assert.assertNotNull(newContext); + if (!startExternalWar && !startExternalDir) { + Assert.assertEquals(LifecycleState.FAILED, + newContext.getState()); + } else { + Assert.assertEquals(LifecycleState.STARTED, + newContext.getState()); + } + } else { + Assert.assertNull(newContext); + } + } + + if (newContext != null) { + Assert.assertEquals(resultCookieName, + newContext.getSessionCookieName()); + Assert.assertEquals(resultState, newContext.getState()); + } + + if (resultAction == NONE) { + Assert.assertSame(oldContext, newContext); + Assert.assertEquals("", tracker.getHistory()); + } else if (resultAction == RELOAD) { + Assert.assertSame(oldContext, newContext); + Assert.assertEquals("stopstart", tracker.getHistory()); + } else if (resultAction == REDEPLOY) { + Assert.assertNotSame(oldContext, newContext); + // No init or start as that will be in a new context object + Assert.assertEquals("stopafter_destroy", tracker.getHistory()); + } else { + Assert.fail(); + } + } + + + /* + * Expected behaviour for the addition of files. + * + * Artifacts present copyXML deployXML Artifact Artifacts remaining + * XML WAR EXT DIR Added XML WAR EXT DIR Action + * N Y N N N Y DIR - Y - A None + * N N N Y N Y WAR - A - R Redeploy + * Y N N N N Y DIR Y - - A None + * N N N Y N Y XML A - - Y Redeploy + * Y N N N N Y WAR Y A - - Reload + * N Y N N N Y XML A Y - - Redeploy + * Y Y N N N Y DIR Y Y - A None + * Y N N Y N Y WAR Y A - N Reload + * N Y N Y N Y XML A Y - Y Redeploy + * Y N Y N N Y DIR Y - Y A None + * Y N Y N N Y WAR Y A Y - None + * N N N Y N Y EXT A - A R Redeploy + * N Y N N N Y EXT A Y A - Redeploy + * + * N N N Y Y/N N DIR+XML - - - Y Redeploy (failed) + * N N N Y Y Y DIR+XML A - - Y Redeploy + * N N N Y N Y DIR+XML - - - Y Redeploy + * + * Addition of a file is treated as if the added file has been modified + * with the following additional actions: + * - If a WAR is added, any DIR is removed and may be recreated depending on + * unpackWARs. + * - If an XML file is added that refers to an external docBase any WAR or + * DIR in the appBase will be removed. The DIR may be recreated if the + * external resource is a WAR and unpackWARs is true. + * - If a DIR is added when a WAR already exists and unpackWARs is false, + * the DIR will be ignored but a warning will be logged when the DIR is + * first detected. If the WAR is removed, the DIR will be left and may be + * deployed via automatic deployment. + * - If a WAR is added when an external WAR already exists for the same + * context, the WAR will be treated the same way as a DIR is treated in + * the previous bullet point. + */ + @Test + public void testAdditionWarAddDir() throws Exception { + doTestAddition(false, false, false, true, false, DIR, + false, true, true, WAR_COOKIE_NAME, NONE); + } + + @Test + public void testAdditionDirAddWar() throws Exception { + doTestAddition(false, false, false, false, true, WAR, + false, true, true, WAR_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testAdditionXmlAddDir() throws Exception { + doTestAddition(true, false, false, false, false, DIR, + true, false, true, XML_COOKIE_NAME, NONE); + } + + @Test + public void testAdditionDirAddXml() throws Exception { + doTestAddition(false, false, false, false, true, XML, + true, false, true, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testAdditionXmlAddWar() throws Exception { + doTestAddition(true, false, false, false, false, WAR, + true, true, false, XML_COOKIE_NAME, RELOAD); + } + + @Test + public void testAdditionWarAddXml() throws Exception { + doTestAddition(false, false, false, true, false, XML, + true, true, false, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testAdditionXmlWarAddDir() throws Exception { + doTestAddition(true, false, false, true, false, DIR, + true, true, true, XML_COOKIE_NAME, NONE); + } + + @Test + public void testAdditionXmlDirAddWar() throws Exception { + doTestAddition(true, false, false, false, true, WAR, + true, true, false, XML_COOKIE_NAME, RELOAD); + } + + @Test + public void testAdditionWarDirAddXml() throws Exception { + doTestAddition(false, false, false, true, true, XML, + true, true, true, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testAdditionXmlExtwarAddDir() throws Exception { + doTestAddition(true, true, false, false, false, DIR, + true, false, true, XML_COOKIE_NAME, NONE); + } + + @Test + public void testAdditionXmlExtdirAddDir() throws Exception { + doTestAddition(true, false, true, false, false, DIR, + true, false, true, XML_COOKIE_NAME, NONE); + } + + @Test + public void testAdditionXmlExtwarAddWar() throws Exception { + doTestAddition(true, true, false, false, false, WAR, + true, true, false, XML_COOKIE_NAME, NONE); + } + + @Test + public void testAdditionXmlExtdirAddWar() throws Exception { + doTestAddition(true, false, true, false, false, WAR, + true, true, false, XML_COOKIE_NAME, NONE); + } + + @Test + public void testAdditionDirAddXmlExtwar() throws Exception { + doTestAddition(false, false, false, false, true, EXT, + true, false, true, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testAdditionWarAddXmlExtwar() throws Exception { + doTestAddition(false, false, false, true, false, EXT, + true, true, false, XML_COOKIE_NAME, REDEPLOY); + } + + @Test + public void testAdditionDirAddDirXmlTF() throws Exception { + doTestAddition(false, false, false, false, true, true, false, DIR_XML, + false, false, true, null, REDEPLOY, LifecycleState.FAILED); + } + + @Test + public void testAdditionDirAddDirXmlFF() throws Exception { + doTestAddition(false, false, false, false, true, false, false, DIR_XML, + false, false, true, null, REDEPLOY, LifecycleState.FAILED); + } + + @Test + public void testAdditionDirAddDirXmlTT() throws Exception { + doTestAddition(false, false, false, false, true, true, true, DIR_XML, + true, false, true, DIR_COOKIE_NAME, REDEPLOY, + LifecycleState.STARTED); + } + + @Test + public void testAdditionDirAddDirXmlFT() throws Exception { + doTestAddition(false, false, false, false, true, false, true, DIR_XML, + false, false, true, DIR_COOKIE_NAME, REDEPLOY, + LifecycleState.STARTED); + } + + private void doTestAddition(boolean startXml, boolean startExternalWar, + boolean startExternalDir, boolean startWar, boolean startDir, + int toAdd, boolean resultXml, boolean resultWar, + boolean resultDir, String resultCookieName, int resultAction) + throws Exception { + + doTestAddition(startXml, startExternalWar, startExternalDir, startWar, + startDir, false, true, toAdd, resultXml, resultWar, resultDir, + resultCookieName, resultAction, LifecycleState.STARTED); + } + + private void doTestAddition(boolean startXml, boolean startExternalWar, + boolean startExternalDir, boolean startWar, boolean startDir, + boolean copyXML, boolean deployXML, int toAdd, boolean resultXml, + boolean resultWar, boolean resultDir, String resultCookieName, + int resultAction, LifecycleState state) + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + + // Init + File xml = null; + File ext = null; + File war = null; + File dir = null; + + if (startXml && !startExternalWar && !startExternalDir) { + xml = createXmlInConfigBaseForAppbase(); + } + if (startExternalWar) { + ext = createWar(WAR_XML_SOURCE, false); + xml = createXmlInConfigBaseForExternal(ext); + } + if (startExternalDir) { + ext = createDirInExternal(true); + xml = createXmlInConfigBaseForExternal(ext); + } + if (startWar) { + war = createWar(WAR_XML_SOURCE, true); + } + if (startDir) { + dir = createDirInAppbase(toAdd != DIR_XML); + } + + if ((startWar || startExternalWar) && !startDir) { + host.setUnpackWARs(false); + } + + host.setCopyXML(copyXML); + host.setDeployXML(deployXML); + + // Deploy the files we copied + tomcat.start(); + host.backgroundProcess(); + + // Change the specified file + switch (toAdd) { + case XML: + if (xml == null) { + xml = createXmlInConfigBaseForAppbase(); + } else { + Assert.fail(); + } + break; + case EXT: + if (ext == null && xml == null) { + ext = createWar(WAR_XML_SOURCE, false); + xml = createXmlInConfigBaseForExternal(ext); + } else { + Assert.fail(); + } + break; + case WAR: + if (war == null) { + war = createWar(WAR_XML_SOURCE, true); + } else { + Assert.fail(); + } + break; + case DIR: + if (dir == null) { + dir = createDirInAppbase(true); + } else { + Assert.fail(); + } + break; + case DIR_XML: + dir = createDirXmlInAppbase(); + xml = getXmlInConfigBaseForAppbase(); + break; + default: + Assert.fail(); + } + + Context oldContext = (Context) host.findChild(APP_NAME.getName()); + StateTracker tracker = new StateTracker(); + oldContext.addLifecycleListener(tracker); + + // Trigger an auto-deployment cycle + host.backgroundProcess(); + + Context newContext = (Context) host.findChild(APP_NAME.getName()); + + // Check the results + if (resultXml) { + if (xml == null) { + Assert.fail(); + } else { + Assert.assertTrue(xml.isFile()); + } + } + if (resultWar) { + if (war == null) { + Assert.fail(); + } else { + Assert.assertTrue(war.isFile()); + } + } + if (resultDir) { + if (dir == null) { + Assert.fail(); + } else { + Assert.assertTrue(dir.isDirectory()); + } + } + + if (!resultXml && (startExternalWar || startExternalDir)) { + Assert.assertNull(newContext); + } + if (!resultWar && !resultDir) { + if (resultXml) { + Assert.assertNotNull(newContext); + if (!startExternalWar && !startExternalDir) { + Assert.assertEquals(LifecycleState.FAILED, + newContext.getState()); + } else { + Assert.assertEquals(LifecycleState.STARTED, + newContext.getState()); + } + } else { + Assert.assertNull(newContext); + } + } + + if (newContext != null) { + Assert.assertEquals(resultCookieName, + newContext.getSessionCookieName()); + } + + if (resultAction == NONE) { + Assert.assertSame(oldContext, newContext); + Assert.assertEquals("", tracker.getHistory()); + } else if (resultAction == RELOAD) { + Assert.assertSame(oldContext, newContext); + Assert.assertEquals("stopstart", tracker.getHistory()); + } else if (resultAction == REDEPLOY) { + if (newContext == null) { + Assert.fail(); + } else { + Assert.assertEquals(state, newContext.getState()); + } + Assert.assertNotSame(oldContext, newContext); + // No init or start as that will be in a new context object + Assert.assertEquals("stopafter_destroy", tracker.getHistory()); + } else { + Assert.fail(); + } + } + + + /* + * Test context unpackWAR setting. + * If context.getUnpackWAR != Host.getUnpackWARs the Host wins. + */ + @Test + public void testUnpackWARFFF() throws Exception { + doTestUnpackWAR(false, false, false, false); + } + + @Test + public void testUnpackWARFFT() throws Exception { + doTestUnpackWAR(false, false, true, false); + } + + @Test + public void testUnpackWARFTF() throws Exception { + doTestUnpackWAR(false, true, false, false); + } + + @Test + public void testUnpackWARFTT() throws Exception { + doTestUnpackWAR(false, true, true, false); + } + + @Test + public void testUnpackWARTFF() throws Exception { + doTestUnpackWAR(true, false, false, false); + } + + @Test + public void testUnpackWARTFT() throws Exception { + // External WAR - therefore XML in WAR will be ignored + doTestUnpackWAR(true, false, true, true); + } + + @Test + public void testUnpackWARTTF() throws Exception { + doTestUnpackWAR(true, true, false, true); + } + + @Test + public void testUnpackWARTTT() throws Exception { + doTestUnpackWAR(true, true, true, true); + } + + private void doTestUnpackWAR(boolean unpackWARs, boolean unpackWAR, + boolean external, boolean resultDir) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + + host.setUnpackWARs(unpackWARs); + + tomcat.start(); + + File war; + if (unpackWAR) { + war = createWar(WAR_XML_UNPACKWAR_TRUE_SOURCE, !external); + } else { + war = createWar(WAR_XML_UNPACKWAR_FALSE_SOURCE, !external); + } + if (external) { + createXmlInConfigBaseForExternal(war); + } + + host.backgroundProcess(); + + File dir = new File(host.getAppBase(), APP_NAME.getBaseName()); + Assert.assertEquals( + Boolean.valueOf(resultDir), Boolean.valueOf(dir.isDirectory())); + } + + + @Test + public void testBrokenAppWithAntiLockingF() throws Exception { + testBrokenAppWithAntiLocking(false); + } + + @Test + public void testBrokenAppWithAntiLockingT() throws Exception { + testBrokenAppWithAntiLocking(true); + } + + private void testBrokenAppWithAntiLocking(boolean unpackWARs) + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + + host.setUnpackWARs(unpackWARs); + + File war = createWar(WAR_BROKEN_SOURCE, false); + createXmlInConfigBaseForExternal(war, true); + + File dir = new File(host.getAppBaseFile(), APP_NAME.getBaseName()); + + tomcat.start(); + + // Simulate deploy on start-up + tomcat.getHost().backgroundProcess(); + + Assert.assertTrue(war.isFile()); + if (unpackWARs) { + Assert.assertTrue(dir.isDirectory()); + } + } + + private File createDirInAppbase(boolean withXml) throws IOException { + File dir = new File(getTomcatInstance().getHost().getAppBaseFile(), + APP_NAME.getBaseName()); + if (withXml) { + recursiveCopy(DIR_XML_SOURCE.toPath(), dir.toPath()); + } else { + recursiveCopy(DIR_SOURCE.toPath(), dir.toPath()); + } + return dir; + } + + private File createDirXmlInAppbase() throws IOException { + File dir = new File(getTomcatInstance().getHost().getAppBaseFile(), + APP_NAME.getBaseName() + "/META-INF"); + recursiveCopy(DIR_XML_SOURCE_META_INF.toPath(), dir.toPath()); + return dir; + } + + private File createDirInExternal(boolean withXml) throws IOException { + File ext = new File(external, "external" + ".war"); + if (withXml) { + recursiveCopy(DIR_XML_SOURCE.toPath(), ext.toPath()); + } else { + recursiveCopy(DIR_SOURCE.toPath(), ext.toPath()); + } + return ext; + } + + private File createWar(File src, boolean useAppbase) throws IOException { + File dest; + if (useAppbase) { + dest = new File(getTomcatInstance().getHost().getAppBaseFile(), + APP_NAME.getBaseName() + ".war"); + } else { + dest = new File(external, "external" + ".war"); + } + Files.copy(src.toPath(), dest.toPath()); + // Make sure that HostConfig thinks the WAR has been modified. + Assert.assertTrue("Failed to set last modified for [" + dest + "]", dest.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return dest; + } + + private File createXmlInConfigBaseForAppbase() throws IOException { + File xml = getXmlInConfigBaseForAppbase(); + File parent = xml.getParentFile(); + if (!parent.isDirectory()) { + Assert.assertTrue(parent.mkdirs()); + } + Files.copy(XML_SOURCE.toPath(), xml.toPath()); + // Make sure that HostConfig thinks the xml has been modified. + Assert.assertTrue("Failed to set last modified for [" + xml + "]", xml.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return xml; + } + + private File getXmlInConfigBaseForAppbase() { + Host host = getTomcatInstance().getHost(); + return new File(host.getConfigBaseFile(), APP_NAME + ".xml"); + } + + private File createXmlInConfigBaseForExternal(File ext) throws IOException { + return createXmlInConfigBaseForExternal(ext, false); + } + + private File createXmlInConfigBaseForExternal(File ext, boolean antiLocking) + throws IOException { + File xml = new File(getTomcatInstance().getHost().getConfigBaseFile(), + APP_NAME + ".xml"); + File parent = xml.getParentFile(); + if (!parent.isDirectory()) { + Assert.assertTrue(parent.mkdirs()); + } + + try (FileOutputStream fos = new FileOutputStream(xml)) { + StringBuilder context = new StringBuilder(); + context.append(""); + fos.write(context.toString().getBytes(StandardCharsets.ISO_8859_1)); + } + // Make sure that HostConfig thinks the xml has been modified. + Assert.assertTrue("Failed to set last modified for [" + xml + "]", xml.setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return xml; + } + + private static class StateTracker implements LifecycleListener { + + private StringBuilder stateHistory = new StringBuilder(); + + @Override + public void lifecycleEvent(LifecycleEvent event) { + + String type = event.getType(); + + if (type.equals(Lifecycle.START_EVENT) || + type.equals(Lifecycle.STOP_EVENT) || + type.equals(Lifecycle.AFTER_DESTROY_EVENT)) { + stateHistory.append(type); + } + } + + + public String getHistory() { + return stateHistory.toString(); + } + } + + + /* + * Test context copyXML setting. + * If context.copyXML != Host.copyXML the Host wins. + * For external WARs, a context.xml must always already exist + */ + @Test + public void testCopyXMLFFF() throws Exception { + doTestCopyXML(false, false, false, false); + } + + @Test + public void testCopyXMLFFT() throws Exception { + doTestCopyXML(false, false, true, true); + } + + @Test + public void testCopyXMLFTF() throws Exception { + doTestCopyXML(false, true, false, true); + } + + @Test + public void testCopyXMLFTT() throws Exception { + doTestCopyXML(false, true, true, true); + } + + @Test + public void testCopyXMLTFF() throws Exception { + doTestCopyXML(true, false, false, true); + } + + @Test + public void testCopyXMLTFT() throws Exception { + doTestCopyXML(true, false, true, true); + } + + @Test + public void testCopyXMLTTF() throws Exception { + doTestCopyXML(true, true, false, true); + } + + @Test + public void testCopyXMLTTT() throws Exception { + doTestCopyXML(true, true, true, true); + } + + private void doTestCopyXML(boolean copyXmlHost, boolean copyXmlWar, + boolean external, boolean resultXml) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + + host.setCopyXML(copyXmlHost); + + tomcat.start(); + + File war; + if (copyXmlWar) { + war = createWar(WAR_XML_COPYXML_TRUE_SOURCE, !external); + } else { + war = createWar(WAR_XML_COPYXML_FALSE_SOURCE, !external); + } + if (external) { + createXmlInConfigBaseForExternal(war); + } + + host.backgroundProcess(); + + File xml = new File(host.getConfigBaseFile(), + APP_NAME.getBaseName() + ".xml"); + Assert.assertEquals( + Boolean.valueOf(resultXml), Boolean.valueOf(xml.isFile())); + + Context context = (Context) host.findChild(APP_NAME.getName()); + if (external) { + Assert.assertEquals(XML_COOKIE_NAME, + context.getSessionCookieName()); + } else { + Assert.assertEquals(WAR_COOKIE_NAME, + context.getSessionCookieName()); + } + } + + + @Test + public void testSetContextClassName() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + Host host = tomcat.getHost(); + if (host instanceof StandardHost) { + StandardHost standardHost = (StandardHost) host; + standardHost.setContextClass(TesterContext.class.getName()); + } + + // Copy the WAR file + File war = new File(host.getAppBaseFile(), + APP_NAME.getBaseName() + ".war"); + Files.copy(WAR_XML_SOURCE.toPath(), war.toPath()); + + // Deploy the copied war + tomcat.start(); + host.backgroundProcess(); + + // Check the Context class + Context ctxt = (Context) host.findChild(APP_NAME.getName()); + + assertThat(ctxt, instanceOf(TesterContext.class)); + } + + + public static class TesterContext extends StandardContext { + // No functional change + } + + + @Test + public void testUpdateWarOfflineNoContextFF() throws Exception { + doTestUpdateWarOffline(WAR_SOURCE, false, false); + } + + + @Test + public void testUpdateWarOfflineNoContextTF() throws Exception { + doTestUpdateWarOffline(WAR_SOURCE, true, false); + } + + + @Test + public void testUpdateWarOfflineNoContextFT() throws Exception { + doTestUpdateWarOffline(WAR_SOURCE, false, true); + } + + + @Test + public void testUpdateWarOfflineNoContextTT() throws Exception { + doTestUpdateWarOffline(WAR_SOURCE, true, true); + } + + + @Test + public void testUpdateWarOfflineContextFF() throws Exception { + doTestUpdateWarOffline(WAR_XML_SOURCE, false, false); + } + + + @Test + public void testUpdateWarOfflineContextTF() throws Exception { + doTestUpdateWarOffline(WAR_XML_SOURCE, true, false); + } + + + @Test + public void testUpdateWarOfflineContextFT() throws Exception { + doTestUpdateWarOffline(WAR_XML_SOURCE, false, true); + } + + + @Test + public void testUpdateWarOfflineContextTT() throws Exception { + doTestUpdateWarOffline(WAR_XML_SOURCE, true, true); + } + + + private void doTestUpdateWarOffline(File srcWar, boolean deployOnStartUp, boolean autoDeploy) + throws Exception { + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + host.setDeployOnStartup(deployOnStartUp); + + File war = createWar(srcWar, true); + // Make the WAR appear to have been created earlier + Assert.assertTrue("Failed to set last modified for [" + war + "]", war.setLastModified( + war.lastModified() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + + tomcat.addWebapp(APP_NAME.getPath(), war.getAbsolutePath()); + tomcat.start(); + + // Get the last modified timestamp for the expanded dir + File dir = new File(host.getAppBase(), APP_NAME.getBaseName()); + // Make the DIR appear to have been created earlier + long lastModified = war.lastModified() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS; + Assert.assertTrue("Failed to set last modified for [" + dir + "]", + dir.setLastModified(lastModified)); + + host.stop(); + Assert.assertTrue("Failed to set last modified for [" + war + "]", + war.setLastModified(System.currentTimeMillis())); + host.start(); + if (autoDeploy) { + host.backgroundProcess(); + } + + long newLastModified = dir.lastModified(); + + Assert.assertNotEquals("Timestamp hasn't changed", lastModified, newLastModified); + } +} diff --git a/test/org/apache/catalina/startup/TestListener.java b/test/org/apache/catalina/startup/TestListener.java new file mode 100644 index 0000000..5c2c049 --- /dev/null +++ b/test/org/apache/catalina/startup/TestListener.java @@ -0,0 +1,109 @@ +/* + * 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.catalina.startup; + +import java.util.Set; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; + +public class TestListener extends TomcatBaseTest { + + /* + * Check that a ServletContainerInitializer can install a + * {@link ServletContextListener} and that it gets initialized. + * @throws Exception + */ + @Test + public void testServletContainerInitializer() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context context = getProgrammaticRootContext(); + + context.addServletContainerInitializer(new SCI(), null); + tomcat.start(); + Assert.assertTrue(SCL.initialized); + } + + /* + * Check that a {@link ServletContextListener} cannot install a + * {@link ServletContainerInitializer}. + * @throws Exception + */ + @Test + public void testServletContextListener() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context context = getProgrammaticRootContext(); + + // SCL2 pretends to be in web.xml, and tries to install a + // ServletContainerInitializer. + context.addApplicationListener(SCL2.class.getName()); + tomcat.start(); + + //check that the ServletContainerInitializer wasn't initialized. + Assert.assertFalse(SCL3.initialized); + } + + public static class SCI implements ServletContainerInitializer { + + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + ctx.addListener(new SCL()); + } + } + + public static class SCL implements ServletContextListener { + + static boolean initialized = false; + + @Override + public void contextInitialized(ServletContextEvent sce) { + initialized = true; + } + } + + public static class SCL2 implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + ServletContext sc = sce.getServletContext(); + sc.addListener(SCL3.class.getName()); + } + } + + public static class SCL3 implements ServletContextListener { + + static boolean initialized = false; + + @Override + public void contextInitialized(ServletContextEvent sce) { + initialized = true; + } + } +} diff --git a/test/org/apache/catalina/startup/TestMultipartConfig.java b/test/org/apache/catalina/startup/TestMultipartConfig.java new file mode 100644 index 0000000..3483ec3 --- /dev/null +++ b/test/org/apache/catalina/startup/TestMultipartConfig.java @@ -0,0 +1,187 @@ +/* + * 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.catalina.startup; + +import java.lang.reflect.Method; + +import jakarta.servlet.MultipartConfigElement; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.core.StandardService; +import org.apache.catalina.core.StandardWrapper; +import org.apache.tomcat.util.descriptor.web.MultipartDef; +import org.apache.tomcat.util.descriptor.web.ServletDef; +import org.apache.tomcat.util.descriptor.web.WebXml; + +public class TestMultipartConfig { + @Test + public void testNoMultipartConfig() throws Exception { + StandardWrapper servlet = config(null); + + MultipartConfigElement mce = servlet.getMultipartConfigElement(); + + Assert.assertNull(mce); + } + + @Test + public void testDefaultMultipartConfig() throws Exception { + MultipartDef multipartDef = new MultipartDef(); + // Do not set any attributes on multipartDef: expect defaults + + StandardWrapper servlet = config(multipartDef); + MultipartConfigElement mce = servlet.getMultipartConfigElement(); + + Assert.assertNotNull(mce); + Assert.assertEquals("", mce.getLocation()); + Assert.assertEquals(-1, mce.getMaxFileSize()); + Assert.assertEquals(-1, mce.getMaxRequestSize()); + Assert.assertEquals(0, mce.getFileSizeThreshold()); + } + + @Test + public void testPartialMultipartConfigMaxFileSize() throws Exception { + MultipartDef multipartDef = new MultipartDef(); + multipartDef.setMaxFileSize("1024"); + + StandardWrapper servlet = config(multipartDef); + MultipartConfigElement mce = servlet.getMultipartConfigElement(); + + Assert.assertNotNull(mce); + Assert.assertEquals("", mce.getLocation()); + Assert.assertEquals(1024, mce.getMaxFileSize()); + Assert.assertEquals(-1, mce.getMaxRequestSize()); + Assert.assertEquals(0, mce.getFileSizeThreshold()); + } + + @Test + public void testPartialMultipartConfigMaxRequestSize() throws Exception { + MultipartDef multipartDef = new MultipartDef(); + multipartDef.setMaxRequestSize("10240"); + + StandardWrapper servlet = config(multipartDef); + MultipartConfigElement mce = servlet.getMultipartConfigElement(); + + Assert.assertNotNull(mce); + Assert.assertEquals("", mce.getLocation()); + Assert.assertEquals(-1, mce.getMaxFileSize()); + Assert.assertEquals(10240, mce.getMaxRequestSize()); + Assert.assertEquals(0, mce.getFileSizeThreshold()); + } + + @Test + public void testPartialMultipartConfigFileSizeThreshold() throws Exception { + MultipartDef multipartDef = new MultipartDef(); + multipartDef.setFileSizeThreshold("24"); + + StandardWrapper servlet = config(multipartDef); + MultipartConfigElement mce = servlet.getMultipartConfigElement(); + + Assert.assertNotNull(mce); + Assert.assertEquals("", mce.getLocation()); + Assert.assertEquals(-1, mce.getMaxFileSize()); + Assert.assertEquals(-1, mce.getMaxRequestSize()); + Assert.assertEquals(24, mce.getFileSizeThreshold()); + } + + @Test + public void testCompleteMultipartConfig() throws Exception { + MultipartDef multipartDef = new MultipartDef(); + multipartDef.setMaxFileSize("1024"); + multipartDef.setMaxRequestSize("10240"); + multipartDef.setFileSizeThreshold("24"); + multipartDef.setLocation("/tmp/foo"); + + StandardWrapper servlet = config(multipartDef); + + MultipartConfigElement mce = servlet.getMultipartConfigElement(); + + Assert.assertNotNull(mce); + Assert.assertEquals("/tmp/foo", mce.getLocation()); + Assert.assertEquals(1024, mce.getMaxFileSize()); + Assert.assertEquals(10240, mce.getMaxRequestSize()); + Assert.assertEquals(24, mce.getFileSizeThreshold()); + } + + private StandardWrapper config(MultipartDef multipartDef) throws Exception { + MyContextConfig config = new MyContextConfig(); + + WebXml webxml = new WebXml(); + + ServletDef servletDef = new ServletDef(); + servletDef.setServletName("test"); + servletDef.setServletClass("org.apache.catalina.startup.ParamServlet"); + servletDef.setMultipartDef(multipartDef); + webxml.addServlet(servletDef); + + Method m = ContextConfig.class.getDeclaredMethod("configureContext", WebXml.class); + + // Force our way in + m.setAccessible(true); + + m.invoke(config, webxml); + + StandardWrapper servlet = (StandardWrapper)config.getContext().findChild("test"); + + return servlet; + } + + private static class MyContextConfig extends ContextConfig { + MyContextConfig() { + CustomContext context = new CustomContext(); + super.context = context; + context.setConfigured(false); + context.setState(LifecycleState.STARTING_PREP); + context.setName("test"); + + Connector connector = new Connector(); + StandardService service = new StandardService(); + service.addConnector(connector); + StandardEngine engine = new StandardEngine(); + engine.setService(service); + Container parent = new StandardHost(); + parent.setParent(engine); + super.context.setParent(parent); + } + public Context getContext() { + return super.context; + } + } + + private static class CustomContext extends StandardContext { + private volatile LifecycleState state; + + @Override + public synchronized LifecycleState getState() { + return state; + } + + @Override + public synchronized void setState(LifecycleState state) { + this.state = state; + } + } + +} diff --git a/test/org/apache/catalina/startup/TestTomcat.java b/test/org/apache/catalina/startup/TestTomcat.java new file mode 100644 index 0000000..e620cc5 --- /dev/null +++ b/test/org/apache/catalina/startup/TestTomcat.java @@ -0,0 +1,643 @@ +/* + * 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.catalina.startup; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.net.URLConnection; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.authenticator.AuthenticatorBase; +import org.apache.catalina.connector.Request; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.ha.context.ReplicatedContext; +import org.apache.tomcat.util.MultiThrowable; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextResourceLink; +import org.apache.tomcat.websocket.server.WsContextListener; + +public class TestTomcat extends TomcatBaseTest { + + /** + * Simple servlet to test in-line registration. + */ + public static class HelloWorld extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse res) + throws IOException { + res.getWriter().write("Hello world"); + } + } + + /** + * Simple servlet to test the default session manager. + */ + public static class HelloWorldSession extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse res) + throws IOException { + req.getSession(true); + res.getWriter().write("Hello world"); + } + } + + /** + * Simple servlet to test JNDI + */ + public static class HelloWorldJndi extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final String JNDI_ENV_NAME = "test"; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse res) + throws IOException { + + String name = null; + + try { + javax.naming.Context initCtx = new InitialContext(); + javax.naming.Context envCtx = + (javax.naming.Context) initCtx.lookup("java:comp/env"); + name = (String) envCtx.lookup(JNDI_ENV_NAME); + } catch (NamingException e) { + throw new IOException(e); + } + + res.getWriter().write("Hello, " + name); + } + } + + /** + * Servlet that tries to obtain a URL for WEB-INF/web.xml + */ + public static class GetResource extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse res) + throws IOException { + URL url = req.getServletContext().getResource("/WEB-INF/web.xml"); + + res.getWriter().write("The URL obtained for /WEB-INF/web.xml was "); + if (url == null) { + res.getWriter().write("null"); + } else { + res.getWriter().write(url.toString() + "\n"); + res.getWriter().write("The first 20 characters of that resource are:\n"); + + // Read some content from the resource + URLConnection conn = url.openConnection(); + + char cbuf[] = new char[20]; + int read = 0; + try (InputStream is = conn.getInputStream(); + Reader reader = new InputStreamReader(is)) { + while (read < 20) { + int len = reader.read(cbuf, read, cbuf.length - read); + res.getWriter().write(cbuf, read, len); + read = read + len; + } + } + } + } + } + + /** + * Simple servlet to test initialization of servlet instances. + */ + private static class InitCount extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private AtomicInteger callCount = new AtomicInteger(0); + + @Override + public void init() throws ServletException { + super.init(); + callCount.incrementAndGet(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + } + + public int getCallCount() { + return callCount.intValue(); + } + } + + + /* + * Start tomcat with a single context and one + * servlet - all programmatic, no server.xml or + * web.xml used. + * + * @throws Exception + */ + @Test + public void testProgrammatic() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "myServlet", new HelloWorld()); + ctx.addServletMappingDecoded("/", "myServlet"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("Hello world", res.toString()); + } + + @Test + public void testSingleWebapp() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + // app dir is relative to server home + Context ctxt = tomcat.addWebapp( + null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + + "/examples/servlets/servlet/HelloWorldExample"); + String text = res.toString(); + Assert.assertTrue(text, text.indexOf("
    ") > 0); + } + + @Test + public void testJsps() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + // app dir is relative to server home + Context ctxt = tomcat.addWebapp( + null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + + "/examples/jsp/jsp2/el/basic-arithmetic.jsp"); + String text = res.toString(); + Assert.assertTrue(text, text.indexOf("${(1==2) ? 3 : 4}") > 0); + } + + @Test + public void testSession() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "myServlet", new HelloWorldSession()); + ctx.addServletMappingDecoded("/", "myServlet"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("Hello world", res.toString()); + } + + @Test + public void testLaunchTime() throws Exception { + Tomcat tomcat = getTomcatInstance(); + long t0 = System.currentTimeMillis(); + tomcat.addContext(null, "", "."); + tomcat.start(); + log.info("Tomcat started in [" + (System.currentTimeMillis() - t0) + + "] ms"); + } + + + /* + * Test for enabling JNDI. + */ + @Test + public void testEnableNaming() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Enable JNDI - it is disabled by default + tomcat.enableNaming(); + + ContextEnvironment environment = new ContextEnvironment(); + environment.setType("java.lang.String"); + environment.setName(HelloWorldJndi.JNDI_ENV_NAME); + environment.setValue("Tomcat User"); + ctx.getNamingResources().addEnvironment(environment); + + Tomcat.addServlet(ctx, "jndiServlet", new HelloWorldJndi()); + ctx.addServletMappingDecoded("/", "jndiServlet"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("Hello, Tomcat User", res.toString()); + } + + /* + * Test for enabling JNDI and using global resources. + */ + @Test + public void testEnableNamingGlobal() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Enable JNDI - it is disabled by default + tomcat.enableNaming(); + + ContextEnvironment environment = new ContextEnvironment(); + environment.setType("java.lang.String"); + environment.setName("globalTest"); + environment.setValue("Tomcat User"); + tomcat.getServer().getGlobalNamingResources().addEnvironment(environment); + + ContextResourceLink link = new ContextResourceLink(); + link.setGlobal("globalTest"); + link.setName(HelloWorldJndi.JNDI_ENV_NAME); + link.setType("java.lang.String"); + ctx.getNamingResources().addResourceLink(link); + + Tomcat.addServlet(ctx, "jndiServlet", new HelloWorldJndi()); + ctx.addServletMappingDecoded("/", "jndiServlet"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("Hello, Tomcat User", res.toString()); + } + + + /* + * Test for https://bz.apache.org/bugzilla/show_bug.cgi?id=47866 + */ + @Test + public void testGetResource() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + String contextPath = "/examples"; + + File appDir = new File(getBuildDirectory(), "webapps" + contextPath); + // app dir is relative to server home + Context ctx = + tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + ctx.addApplicationListener(WsContextListener.class.getName()); + + Tomcat.addServlet(ctx, "testGetResource", new GetResource()); + ctx.addServletMappingDecoded("/testGetResource", "testGetResource"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + + int rc =getUrl("http://localhost:" + getPort() + contextPath + + "/testGetResource", res, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(res.toString().contains(" webXmlMimeMappings = webXmlDefaultFragment.getMimeMappings(); + + Set embeddedExtensions = new HashSet<>(Arrays.asList(ctx.findMimeMappings())); + + // Find entries present in conf/web.xml that are missing in embedded + Set missingInEmbedded = new HashSet<>(webXmlMimeMappings.keySet()); + missingInEmbedded.removeAll(embeddedExtensions); + if (missingInEmbedded.size() > 0) { + for (String missingExtension : missingInEmbedded) { + System.out.println("Missing in embedded: [" + missingExtension + + "]-[" + webXmlMimeMappings.get(missingExtension) + "]"); + } + Assert.fail("Embedded is missing [" + missingInEmbedded.size() + "] entries compared to conf/web.xml"); + } + + // Find entries present in embedded that are missing in conf/web.xml + Set missingInWebXml = new HashSet<>(embeddedExtensions); + missingInWebXml.removeAll(webXmlMimeMappings.keySet()); + if (missingInWebXml.size() > 0) { + for (String missingExtension : missingInWebXml) { + System.out.println("Missing in embedded: [" + missingExtension + + "]-[" + ctx.findMimeMapping(missingExtension) + "]"); + } + Assert.fail("Embedded is missing [" + missingInWebXml.size() + "] entries compared to conf/web.xml"); + } + } +} diff --git a/test/org/apache/catalina/startup/TestTomcatStandalone.java b/test/org/apache/catalina/startup/TestTomcatStandalone.java new file mode 100644 index 0000000..ebb763a --- /dev/null +++ b/test/org/apache/catalina/startup/TestTomcatStandalone.java @@ -0,0 +1,107 @@ +/* + * 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.catalina.startup; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.TestTomcat.HelloWorld; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestTomcatStandalone extends LoggingBaseTest { + + protected class ServerXml extends CatalinaBaseConfigurationSource { + public ServerXml() { + super(getTemporaryDirectory(), null); + } + + private static final String SERVER_XML = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + " \n" + + " \n" + + " \n" + "\n" + + " \n" + "\n" + + " \n" + + " \n" + + "\n" + + " \n" + + " \n" + + " \n" + "\n" + + " \n" + + "\n" + + " \n" + + "\n" + " \n" + " \n" + + " \n" + ""; + + @Override + public Resource getServerXml() throws IOException { + Resource resource; + try { + resource = new Resource(new ByteArrayInputStream(SERVER_XML.getBytes(StandardCharsets.ISO_8859_1)), + new URI("file:server.xml")); + } catch (URISyntaxException e) { + throw new IOException(e); + } + return resource; + } + } + + @Test + public void testServerXml() throws Exception { + Tomcat tomcat = new Tomcat(); + tomcat.init(new ServerXml()); + + // No file system docBase required + Context ctx = tomcat.addContext("", null); + + Tomcat.addServlet(ctx, "myServlet", new HelloWorld()); + ctx.addServletMappingDecoded("/", "myServlet"); + + tomcat.start(); + + ByteChunk res = TomcatBaseTest.getUrl("http://localhost:" + tomcat.getConnector().getLocalPort() + "/"); + Assert.assertEquals("Hello world", res.toString()); + + tomcat.stop(); + tomcat.destroy(); + } + +} diff --git a/test/org/apache/catalina/startup/TestWebappServiceLoader.java b/test/org/apache/catalina/startup/TestWebappServiceLoader.java new file mode 100644 index 0000000..df8126f --- /dev/null +++ b/test/org/apache/catalina/startup/TestWebappServiceLoader.java @@ -0,0 +1,231 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.webresources.StandardRoot; +import org.apache.tomcat.unittest.TesterContext; +import org.easymock.EasyMock; +import org.easymock.IMocksControl; + +public class TestWebappServiceLoader { + private static final String CONFIG_FILE = + "META-INF/services/jakarta.servlet.ServletContainerInitializer"; + private IMocksControl control; + private ClassLoader cl; + private ClassLoader parent; + private Context context; + private ServletContext servletContext; + private WebappServiceLoader loader; + + @Before + public void init() { + control = EasyMock.createStrictControl(); + parent = control.createMock(ClassLoader.class); + cl = EasyMock.createMockBuilder(ClassLoader.class) + .withConstructor(parent) + .addMockedMethod("loadClass", String.class) + .createMock(control); + servletContext = control.createMock(ServletContext.class); + EasyMock.expect(servletContext.getClassLoader()).andStubReturn(cl); + context = new ExtendedTesterContext(servletContext, parent); + } + + @Test + public void testNoInitializersFound() throws IOException { + loader = new WebappServiceLoader<>(context); + EasyMock.expect(cl.getResources(CONFIG_FILE)) + .andReturn(Collections.emptyEnumeration()); + EasyMock.expect(servletContext.getAttribute(ServletContext.ORDERED_LIBS)) + .andReturn(null); + EasyMock.expect(cl.getResources(CONFIG_FILE)) + .andReturn(Collections.emptyEnumeration()); + control.replay(); + Assert.assertTrue(loader.load(ServletContainerInitializer.class).isEmpty()); + control.verify(); + } + + @Test + @SuppressWarnings("unchecked") + public void testInitializerFromClasspath() throws IOException { + URL url = new URL("file://test"); + loader = EasyMock.createMockBuilder(WebappServiceLoader.class) + .addMockedMethod("parseConfigFile", LinkedHashSet.class, URL.class) + .withConstructor(context).createMock(control); + EasyMock.expect(cl.getResources(CONFIG_FILE)) + .andReturn(Collections.enumeration(Collections.singleton(url))); + loader.parseConfigFile(EasyMock.isA(LinkedHashSet.class), EasyMock.same(url)); + EasyMock.expect(servletContext.getAttribute(ServletContext.ORDERED_LIBS)) + .andReturn(null); + EasyMock.expect(cl.getResources(CONFIG_FILE)) + .andReturn(Collections.enumeration(Collections.singleton(url))); + control.replay(); + Assert.assertTrue(loader.load(ServletContainerInitializer.class).isEmpty()); + control.verify(); + } + + @Test + @SuppressWarnings("unchecked") + public void testWithOrdering() throws IOException { + URL url1 = new URL("file://jar1.jar"); + URL sci1 = new URL("jar:file://jar1.jar!/" + CONFIG_FILE); + URL url2 = new URL("file://dir/"); + URL sci2 = new URL("file://dir/" + CONFIG_FILE); + loader = EasyMock.createMockBuilder(WebappServiceLoader.class) + .addMockedMethod("parseConfigFile", LinkedHashSet.class, URL.class) + .withConstructor(context).createMock(control); + List jars = Arrays.asList("jar1.jar", "dir/"); + EasyMock.expect(parent.getResources(CONFIG_FILE)) + .andReturn(Collections.emptyEnumeration()); + EasyMock.expect(servletContext.getAttribute(ServletContext.ORDERED_LIBS)) + .andReturn(jars); + EasyMock.expect(servletContext.getResource("/WEB-INF/classes/" + CONFIG_FILE)) + .andReturn(null); + EasyMock.expect(servletContext.getResource("/WEB-INF/lib/jar1.jar")) + .andReturn(url1); + loader.parseConfigFile(EasyMock.isA(LinkedHashSet.class), EasyMock.eq(sci1)); + EasyMock.expect(servletContext.getResource("/WEB-INF/lib/dir/")) + .andReturn(url2); + loader.parseConfigFile(EasyMock.isA(LinkedHashSet.class), EasyMock.eq(sci2)); + + control.replay(); + Assert.assertTrue(loader.load(ServletContainerInitializer.class).isEmpty()); + control.verify(); + } + + @Test + public void testParseConfigFile() throws IOException { + LinkedHashSet found = new LinkedHashSet<>(); + loader = new WebappServiceLoader<>(context); + loader.parseConfigFile(found, getClass().getResource("service-config.txt")); + Assert.assertEquals(Collections.singleton("provider1"), found); + } + + @Test + public void testLoadServices() throws Exception { + Class sci = TesterServletContainerInitializer1.class; + loader = new WebappServiceLoader<>(context); + cl.loadClass(sci.getName()); + EasyMock.expectLastCall() + .andReturn(sci); + LinkedHashSet names = new LinkedHashSet<>(); + names.add(sci.getName()); + control.replay(); + Collection initializers = + loader.loadServices(ServletContainerInitializer.class, names); + Assert.assertEquals(1, initializers.size()); + Assert.assertTrue(sci.isInstance(initializers.iterator().next())); + control.verify(); + } + + @Test + public void testServiceIsNotExpectedType() throws Exception { + Class sci = Object.class; + loader = new WebappServiceLoader<>(context); + cl.loadClass(sci.getName()); + EasyMock.expectLastCall() + .andReturn(sci); + LinkedHashSet names = new LinkedHashSet<>(); + names.add(sci.getName()); + control.replay(); + try { + loader.loadServices(ServletContainerInitializer.class, names); + } catch (IOException e) { + assertThat(e.getCause(), instanceOf(ClassCastException.class)); + } finally { + control.verify(); + } + } + + @Test + public void testServiceCannotBeConstructed() throws Exception { + Class sci = Integer.class; + loader = new WebappServiceLoader<>(context); + cl.loadClass(sci.getName()); + EasyMock.expectLastCall() + .andReturn(sci); + LinkedHashSet names = new LinkedHashSet<>(); + names.add(sci.getName()); + control.replay(); + try { + loader.loadServices(ServletContainerInitializer.class, names); + } catch (IOException e) { + assertThat(e.getCause(), instanceOf(ReflectiveOperationException.class)); + } finally { + control.verify(); + } + } + + private static class ExtendedTesterContext extends TesterContext { + private final ServletContext servletContext; + private final ClassLoader parent; + private final WebResourceRoot resources; + + ExtendedTesterContext(ServletContext servletContext, ClassLoader parent) { + this.servletContext = servletContext; + this.parent = parent; + // Empty resources - any non-null returns will be mocked on the + // ServletContext + this.resources = new StandardRoot(this); + try { + this.resources.start(); + } catch (LifecycleException e) { + throw new IllegalStateException(e); + } + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public String getContainerSciFilter() { + return ""; + } + + @Override + public ClassLoader getParentClassLoader() { + return parent; + } + + @Override + public WebResourceRoot getResources() { + return resources; + } + } +} diff --git a/test/org/apache/catalina/startup/TesterMapRealm.java b/test/org/apache/catalina/startup/TesterMapRealm.java new file mode 100644 index 0000000..c1ee7f3 --- /dev/null +++ b/test/org/apache/catalina/startup/TesterMapRealm.java @@ -0,0 +1,55 @@ +/* + * 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.catalina.startup; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.catalina.realm.RealmBase; + +/** + * Simple Realm that uses a configurable {@link Map} to link user names and + * passwords. + */ +public final class TesterMapRealm extends RealmBase { + private Map users = new HashMap<>(); + private Map> roles = new HashMap<>(); + + public void addUser(String username, String password) { + users.put(username, password); + } + + public void addUserRole(String username, String role) { + roles.computeIfAbsent(username, k -> new ArrayList<>()).add(role); + } + + @Override + protected String getPassword(String username) { + return users.get(username); + } + + @Override + protected Principal getPrincipal(String username) { + return new GenericPrincipal(username, + roles.get(username)); + } + +} \ No newline at end of file diff --git a/test/org/apache/catalina/startup/TesterServlet.java b/test/org/apache/catalina/startup/TesterServlet.java new file mode 100644 index 0000000..219c08a --- /dev/null +++ b/test/org/apache/catalina/startup/TesterServlet.java @@ -0,0 +1,56 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class TesterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean explicitClose; + + + public TesterServlet() { + this(false); + } + + + public TesterServlet(boolean explicitClose) { + this.explicitClose = explicitClose; + } + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + PrintWriter out = resp.getWriter(); + out.print("OK"); + + if (explicitClose) { + resp.setHeader("Connection", "close"); + } + } +} diff --git a/test/org/apache/catalina/startup/TesterServletContainerInitializer1.java b/test/org/apache/catalina/startup/TesterServletContainerInitializer1.java new file mode 100644 index 0000000..d397dcc --- /dev/null +++ b/test/org/apache/catalina/startup/TesterServletContainerInitializer1.java @@ -0,0 +1,38 @@ +/* + * 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.catalina.startup; + +import java.util.Set; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; + +public class TesterServletContainerInitializer1 implements + ServletContainerInitializer { + + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + Servlet s = new TesterServlet(); + ServletRegistration.Dynamic r = ctx.addServlet("TesterServlet1", s); + r.addMapping("/TesterServlet1"); + } + +} diff --git a/test/org/apache/catalina/startup/TesterServletContainerInitializer2.java b/test/org/apache/catalina/startup/TesterServletContainerInitializer2.java new file mode 100644 index 0000000..658ce11 --- /dev/null +++ b/test/org/apache/catalina/startup/TesterServletContainerInitializer2.java @@ -0,0 +1,38 @@ +/* + * 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.catalina.startup; + +import java.util.Set; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; + +public class TesterServletContainerInitializer2 implements + ServletContainerInitializer { + + @Override + public void onStartup(Set> c, ServletContext ctx) + throws ServletException { + Servlet s = new TesterServlet(); + ServletRegistration.Dynamic r = ctx.addServlet("TesterServlet2", s); + r.addMapping("/TesterServlet2"); + } + +} diff --git a/test/org/apache/catalina/startup/TesterServletEncodeUrl.java b/test/org/apache/catalina/startup/TesterServletEncodeUrl.java new file mode 100644 index 0000000..810b747 --- /dev/null +++ b/test/org/apache/catalina/startup/TesterServletEncodeUrl.java @@ -0,0 +1,59 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * A test servlet that will always encode the url in case the client requires + * session persistence but is not configured to support cookies. + */ +public class TesterServletEncodeUrl extends HttpServlet { + + private static final long serialVersionUID = 1L; + + /** + * Almost minimal processing for a servlet. + *

    + * The request parameter nextUrl specifies the url to which the + * caller would like to go next. If supplied, put an encoded url into the + * returned HTML page as a hyperlink. + */ + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + PrintWriter out = resp.getWriter(); + out.print("OK"); + + String param = req.getParameter("nextUrl"); + if (param!=null) { + // append an encoded url to carry the sessionids + String targetUrl = resp.encodeURL(param); + out.print(". You want to go here next."); + } + } +} diff --git a/test/org/apache/catalina/startup/TesterServletWithAnnotations.java b/test/org/apache/catalina/startup/TesterServletWithAnnotations.java new file mode 100644 index 0000000..6b9aa2a --- /dev/null +++ b/test/org/apache/catalina/startup/TesterServletWithAnnotations.java @@ -0,0 +1,75 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; + +import jakarta.annotation.Resource; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class TesterServletWithAnnotations extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Resource + private int envEntry1; + + private int envEntry2; + + private int envEntry3; + + private int envEntry4; + + @Resource(name = "envEntry5") + private int envEntry5; + + private int envEntry6; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("envEntry1: " + envEntry1); + resp.getWriter().print(" envEntry2: " + envEntry2); + resp.getWriter().print(" envEntry3: " + envEntry3); + resp.getWriter().print(" envEntry4: " + envEntry4); + resp.getWriter().print(" envEntry5: " + envEntry5); + resp.getWriter().print(" envEntry6: " + envEntry6); + } + + public void setEnvEntry2(int envEntry2) { + this.envEntry2 = envEntry2; + } + + @Resource + public void setEnvEntry3(int envEntry3) { + this.envEntry3 = envEntry3; + } + + @Resource + public void setEnvEntry4(int envEntry4) { + this.envEntry4 = envEntry4; + } + + @Resource(name = "envEntry6") + public void setEnvEntry6(int envEntry6) { + this.envEntry6 = envEntry6; + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/startup/TesterServletWithLifeCycleMethods.java b/test/org/apache/catalina/startup/TesterServletWithLifeCycleMethods.java new file mode 100644 index 0000000..96a25c5 --- /dev/null +++ b/test/org/apache/catalina/startup/TesterServletWithLifeCycleMethods.java @@ -0,0 +1,58 @@ +/* + * 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.catalina.startup; + +import java.io.IOException; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class TesterServletWithLifeCycleMethods extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private String result; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print(result); + } + + @PostConstruct + protected void postConstruct() { + result = "postConstruct()"; + } + + @PreDestroy + protected void preDestroy() { + result = "preDestroy()"; + } + + protected void postConstruct1() { + result = "postConstruct1()"; + } + + protected void preDestroy1() { + result = "preDestroy1()"; + } +} diff --git a/test/org/apache/catalina/startup/TomcatBaseTest.java b/test/org/apache/catalina/startup/TomcatBaseTest.java new file mode 100644 index 0000000..91e654a --- /dev/null +++ b/test/org/apache/catalina/startup/TomcatBaseTest.java @@ -0,0 +1,935 @@ +/* + * 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.catalina.startup; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Manager; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.Session; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.session.ManagerBase; +import org.apache.catalina.session.StandardManager; +import org.apache.catalina.util.IOTools; +import org.apache.catalina.valves.AccessLogValve; +import org.apache.catalina.webresources.StandardRoot; +import org.apache.coyote.http11.Http11NioProtocol; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; +import org.apache.tomcat.util.net.TesterSupport; +import org.apache.tomcat.util.scan.StandardJarScanFilter; +import org.apache.tomcat.util.scan.StandardJarScanner; + +/** + * Base test case that provides a Tomcat instance for each test - mainly so we + * don't have to keep writing the cleanup code. + */ +public abstract class TomcatBaseTest extends LoggingBaseTest { + + /* + * Ensures APR Library.initialize() and Library.terminate() don't interfere + * with the calls from the Lifecycle listener and trigger a JVM crash + */ + @SuppressWarnings("unused") + private static final boolean ignored = TesterSupport.OPENSSL_AVAILABLE; + + // Used by parameterized tests. Defined here to reduce duplication. + protected static final Boolean[] booleans = new Boolean[] { Boolean.FALSE, Boolean.TRUE }; + + protected static final int DEFAULT_CLIENT_TIMEOUT_MS = 300_000; + + public static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + + private Tomcat tomcat; + private boolean accessLogEnabled = false; + + /** + * Make the Tomcat instance available to sub-classes. + * + * @return A Tomcat instance without any pre-configured web applications + */ + public Tomcat getTomcatInstance() { + return tomcat; + } + + /** + * Make the Tomcat instance preconfigured with test/webapp available to + * sub-classes. + * @param addJstl Should JSTL support be added to the test webapp + * @param start Should the Tomcat instance be started + * + * @return A Tomcat instance pre-configured with the web application located + * at test/webapp + * + * @throws LifecycleException If a problem occurs while starting the + * instance + */ + public Tomcat getTomcatInstanceTestWebapp(boolean addJstl, boolean start) + throws LifecycleException { + File appDir = new File("test/webapp"); + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + StandardJarScanner scanner = (StandardJarScanner) ctx.getJarScanner(); + StandardJarScanFilter filter = (StandardJarScanFilter) scanner.getJarScanFilter(); + filter.setTldSkip(filter.getTldSkip() + ",testclasses"); + filter.setPluggabilitySkip(filter.getPluggabilitySkip() + ",testclasses"); + + if (addJstl) { + File lib = new File("webapps/examples/WEB-INF/lib"); + ctx.setResources(new StandardRoot(ctx)); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/lib", + lib.getAbsolutePath(), null, "/"); + } + + if (start) { + tomcat.start(); + } + return tomcat; + } + + + public Context getProgrammaticRootContext() { + // No file system docBase required + Context ctx = tomcat.addContext("", null); + // Disable class path scanning - it slows the tests down by almost an order of magnitude + ((StandardJarScanner) ctx.getJarScanner()).setScanClassPath(false); + return ctx; + } + + + /* + * Sub-classes need to know port so they can connect + */ + public int getPort() { + return tomcat.getConnector().getLocalPort(); + } + + /* + * Sub-classes may want to check, whether an AccessLogValve is active + */ + public boolean isAccessLogEnabled() { + return accessLogEnabled; + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + // Trigger loading of catalina.properties + CatalinaProperties.getProperty("foo"); + + File appBase = new File(getTemporaryDirectory(), "webapps"); + if (!appBase.exists() && !appBase.mkdir()) { + Assert.fail("Unable to create appBase for test"); + } + + tomcat = new TomcatWithFastSessionIDs(); + + String protocol = getProtocol(); + Connector connector = new Connector(protocol); + // Listen only on localhost + Assert.assertTrue(connector.setProperty("address", InetAddress.getByName("localhost").getHostAddress())); + // Use random free port + connector.setPort(0); + // By default, a connector failure means a failed test + connector.setThrowOnFailure(true); + // Mainly set to reduce timeouts during async tests + Assert.assertTrue(connector.setProperty("connectionTimeout", "3000")); + tomcat.getService().addConnector(connector); + tomcat.setConnector(connector); + + File catalinaBase = getTemporaryDirectory(); + tomcat.setBaseDir(catalinaBase.getAbsolutePath()); + tomcat.getHost().setAppBase(appBase.getAbsolutePath()); + + accessLogEnabled = Boolean.getBoolean("tomcat.test.accesslog"); + if (accessLogEnabled) { + String accessLogDirectory = System + .getProperty("tomcat.test.reports"); + if (accessLogDirectory == null) { + accessLogDirectory = new File(getBuildDirectory(), "logs") + .toString(); + } + AccessLogValve alv = new AccessLogValve(); + alv.setDirectory(accessLogDirectory); + alv.setPattern("%h %l %u %t \"%r\" %s %b %I %D"); + tomcat.getHost().getPipeline().addValve(alv); + } + + // Cannot delete the whole tempDir, because logs are there, + // but delete known subdirectories of it. + addDeleteOnTearDown(new File(catalinaBase, "webapps")); + addDeleteOnTearDown(new File(catalinaBase, "work")); + } + + protected String getProtocol() { + // Has a protocol been specified + String protocol = System.getProperty("tomcat.test.protocol"); + + // Use NIO by default starting with Tomcat 8 + if (protocol == null) { + protocol = Http11NioProtocol.class.getName(); + } + + return protocol; + } + + @After + @Override + public void tearDown() throws Exception { + try { + // Some tests may call tomcat.destroy(), some tests may just call + // tomcat.stop(), some not call either method. Make sure that stop() + // & destroy() are called as necessary. + if (tomcat.server != null + && tomcat.server.getState() != LifecycleState.DESTROYED) { + if (tomcat.server.getState() != LifecycleState.STOPPED) { + tomcat.stop(); + } + tomcat.destroy(); + } + } finally { + super.tearDown(); + } + } + + /** + * Simple Hello World servlet for use by test cases + */ + public static final class HelloWorldServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public static final String RESPONSE_TEXT = + "

    Hello World

    "; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + PrintWriter out = resp.getWriter(); + out.print(RESPONSE_TEXT); + } + } + + + public static final class RequestDescriptor { + + private final Map requestInfo = new HashMap<>(); + private final Map contextInitParameters = new HashMap<>(); + private final Map contextAttributes = new HashMap<>(); + private final Map headers = new CaseInsensitiveKeyMap<>(); + private final Map attributes = new HashMap<>(); + private final Map params = new HashMap<>(); + private final Map sessionAttributes = new HashMap<>(); + + public Map getRequestInfo() { + return requestInfo; + } + + public Map getContextInitParameters() { + return contextInitParameters; + } + + public Map getContextAttributes() { + return contextAttributes; + } + + public Map getHeaders() { + return headers; + } + + public Map getAttributes() { + return attributes; + } + + public Map getParams() { + return params; + } + + public Map getSessionAttributes() { + return sessionAttributes; + } + + public String getRequestInfo(String name) { + return requestInfo.get(name); + } + + public void putRequestInfo(String name, String value) { + requestInfo.put(name, value); + } + + public String getContextInitParameter(String name) { + return contextInitParameters.get(name); + } + + public void putContextInitParameter(String name, String value) { + contextInitParameters.put(name, value); + } + + public String getContextAttribute(String name) { + return contextAttributes.get(name); + } + + public void putContextAttribute(String name, String value) { + contextAttributes.put(name, value); + } + + public String getHeader(String name) { + return headers.get(name); + } + + public void putHeader(String name, String value) { + headers.put(name, value); + } + + public String getAttribute(String name) { + return attributes.get(name); + } + + public void putAttribute(String name, String value) { + attributes.put(name, value); + } + + public String getParam(String name) { + return params.get(name); + } + + public void putParam(String name, String value) { + params.put(name, value); + } + + public String getSessionAttribute(String name) { + return sessionAttributes.get(name); + } + + public void putSessionAttribute(String name, String value) { + sessionAttributes.put(name, value); + } + + public void compare (RequestDescriptor request) { + Map base; + Map cmp; + base = request.getRequestInfo(); + cmp = this.getRequestInfo(); + for (String name: base.keySet()) { + Assert.assertEquals("Request info " + name, base.get(name), cmp.get(name)); + } + base = request.getContextInitParameters(); + cmp = this.getContextInitParameters(); + for (String name: base.keySet()) { + Assert.assertEquals("Context parameter " + name, base.get(name), cmp.get(name)); + } + base = request.getContextAttributes(); + cmp = this.getContextAttributes(); + for (String name: base.keySet()) { + Assert.assertEquals("Context attribute " + name, base.get(name), cmp.get(name)); + } + base = request.getHeaders(); + cmp = this.getHeaders(); + for (String name: base.keySet()) { + Assert.assertEquals("Header " + name, base.get(name), cmp.get(name)); + } + base = request.getAttributes(); + cmp = this.getAttributes(); + for (String name: base.keySet()) { + Assert.assertEquals("Attribute " + name, base.get(name), cmp.get(name)); + } + base = request.getParams(); + cmp = this.getParams(); + for (String name: base.keySet()) { + Assert.assertEquals("Param " + name, base.get(name), cmp.get(name)); + } + base = request.getSessionAttributes(); + cmp = this.getSessionAttributes(); + for (String name: base.keySet()) { + Assert.assertEquals("Session attribute " + name, base.get(name), cmp.get(name)); + } + } + } + + + public static final class SnoopResult { + + public static RequestDescriptor parse(String body) { + + int n; + int m; + String key; + String value; + String name; + + RequestDescriptor request = new RequestDescriptor(); + + for (String line: body.split(System.lineSeparator())) { + n = line.indexOf(": "); + if (n > 0) { + key = line.substring(0, n); + value = line.substring(n + 2); + m = key.indexOf(':'); + if (m > 0) { + name = key.substring(m + 1); + key = key.substring(0, m); + if (key.equals("CONTEXT-PARAM")) { + request.putContextInitParameter(name, value); + } else if (key.equals("CONTEXT-ATTRIBUTE")) { + request.putContextAttribute(name, value); + } else if (key.equals("HEADER")) { + request.putHeader(name, value); + } else if (key.equals("ATTRIBUTE")) { + request.putAttribute(name, value); + } else if (key.equals("PARAM")) { + request.putParam(name, value); + } else if (key.equals("SESSION-ATTRIBUTE")) { + request.putSessionAttribute(name, value); + } else { + request.putRequestInfo(key + ":" + name, value); + } + } else { + request.putRequestInfo(key, value); + } + } + } + + return request; + } + } + + /** + * Simple servlet that dumps request information. Tests using this should + * note that additional information may be added to in the future and should + * therefore test return values using SnoopResult. + */ + public static final class SnoopServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void service(HttpServletRequest request, + HttpServletResponse response) + throws ServletException, IOException { + + String name; + StringBuilder value; + Object attribute; + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + ServletContext ctx = this.getServletContext(); + HttpSession session = request.getSession(false); + PrintWriter out = response.getWriter(); + + out.println("CONTEXT-NAME: " + ctx.getServletContextName()); + out.println("CONTEXT-PATH: " + ctx.getContextPath()); + out.println("CONTEXT-MAJOR-VERSION: " + ctx.getMajorVersion()); + out.println("CONTEXT-MINOR-VERSION: " + ctx.getMinorVersion()); + out.println("CONTEXT-SERVER-INFO: " + ctx.getServerInfo()); + for (Enumeration e = ctx.getInitParameterNames(); + e.hasMoreElements();) { + name = e.nextElement(); + out.println("CONTEXT-INIT-PARAM:" + name + ": " + + ctx.getInitParameter(name)); + } + for (Enumeration e = ctx.getAttributeNames(); + e.hasMoreElements();) { + name = e.nextElement(); + out.println("CONTEXT-ATTRIBUTE:" + name + ": " + + ctx.getAttribute(name)); + } + out.println("REQUEST-CONTEXT-PATH: " + request.getContextPath()); + out.println("REQUEST-SERVER-NAME: " + request.getServerName()); + out.println("REQUEST-SERVER-PORT: " + request.getServerPort()); + out.println("REQUEST-LOCAL-NAME: " + request.getLocalName()); + out.println("REQUEST-LOCAL-ADDR: " + request.getLocalAddr()); + out.println("REQUEST-LOCAL-PORT: " + request.getLocalPort()); + out.println("REQUEST-REMOTE-HOST: " + request.getRemoteHost()); + out.println("REQUEST-REMOTE-ADDR: " + request.getRemoteAddr()); + out.println("REQUEST-REMOTE-PORT: " + request.getRemotePort()); + out.println("REQUEST-PROTOCOL: " + request.getProtocol()); + out.println("REQUEST-SCHEME: " + request.getScheme()); + out.println("REQUEST-IS-SECURE: " + request.isSecure()); + out.println("REQUEST-URI: " + request.getRequestURI()); + out.println("REQUEST-URL: " + request.getRequestURL()); + out.println("REQUEST-SERVLET-PATH: " + request.getServletPath()); + out.println("REQUEST-METHOD: " + request.getMethod()); + out.println("REQUEST-PATH-INFO: " + request.getPathInfo()); + out.println("REQUEST-PATH-TRANSLATED: " + + request.getPathTranslated()); + out.println("REQUEST-QUERY-STRING: " + request.getQueryString()); + out.println("REQUEST-REMOTE-USER: " + request.getRemoteUser()); + out.println("REQUEST-AUTH-TYPE: " + request.getAuthType()); + out.println("REQUEST-USER-PRINCIPAL: " + + request.getUserPrincipal()); + out.println("REQUEST-CHARACTER-ENCODING: " + + request.getCharacterEncoding()); + out.println("REQUEST-CONTENT-LENGTH: " + + request.getContentLengthLong()); + out.println("REQUEST-CONTENT-TYPE: " + request.getContentType()); + out.println("REQUEST-LOCALE: " + request.getLocale()); + + for (Enumeration e = request.getHeaderNames(); + e.hasMoreElements();) { + name = e.nextElement(); + value = new StringBuilder(); + for (Enumeration h = request.getHeaders(name); + h.hasMoreElements();) { + value.append(h.nextElement()); + if (h.hasMoreElements()) { + value.append(';'); + } + } + out.println("HEADER:" + name + ": " + value); + } + + for (Enumeration e = request.getAttributeNames(); + e.hasMoreElements();) { + name = e.nextElement(); + attribute = request.getAttribute(name); + out.println("ATTRIBUTE:" + name + ": " + + (attribute != null ? attribute : "(null)")); + } + + for (Enumeration e = request.getParameterNames(); + e.hasMoreElements();) { + name = e.nextElement(); + value = new StringBuilder(); + String values[] = request.getParameterValues(name); + int m = values.length; + for (int j = 0; j < m; j++) { + value.append(values[j]); + if (j < m - 1) { + value.append(';'); + } + } + out.println("PARAM/" + name + ": " + value); + } + + out.println("SESSION-REQUESTED-ID: " + + request.getRequestedSessionId()); + out.println("SESSION-REQUESTED-ID-COOKIE: " + + request.isRequestedSessionIdFromCookie()); + out.println("SESSION-REQUESTED-ID-URL: " + + request.isRequestedSessionIdFromURL()); + out.println("SESSION-REQUESTED-ID-VALID: " + + request.isRequestedSessionIdValid()); + + if (session != null) { + out.println("SESSION-ID: " + session.getId()); + out.println("SESSION-CREATION-TIME: " + + session.getCreationTime()); + out.println("SESSION-LAST-ACCESSED-TIME: " + + session.getLastAccessedTime()); + out.println("SESSION-MAX-INACTIVE-INTERVAL: " + + session.getMaxInactiveInterval()); + out.println("SESSION-IS-NEW: " + session.isNew()); + for (Enumeration e = session.getAttributeNames(); + e.hasMoreElements();) { + name = e.nextElement(); + attribute = session.getAttribute(name); + out.println("SESSION-ATTRIBUTE:" + name + ": " + + (attribute != null ? attribute : "(null)")); + } + } + + int bodySize = 0; + if ("PUT".equalsIgnoreCase(request.getMethod())) { + InputStream is = request.getInputStream(); + int read = 0; + byte[] buffer = new byte[8192]; + while (read != -1) { + read = is.read(buffer); + if (read > -1) { + bodySize += read; + } + } + } + out.println("REQUEST-BODY-SIZE: " + bodySize); + } + } + + + /** + * Servlet that simply echos the request body back as the response body. + */ + public static class EchoBodyServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // NO-OP - No body to echo + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Beware of clients that try to send the whole request body before + // reading any of the response. They may cause this test to lock up. + try (InputStream is = req.getInputStream(); + OutputStream os = resp.getOutputStream()) { + IOTools.flow(is, os); + } + } + } + + + /* + * Wrapper for getting the response. + */ + public static ByteChunk getUrl(String path) throws IOException { + ByteChunk out = new ByteChunk(); + getUrl(path, out, null); + return out; + } + + public static int getUrl(String path, ByteChunk out, Map> resHead) + throws IOException { + return getUrl(path, out, null, resHead); + } + + public static int getUrl(String path, ByteChunk out, boolean followRedirects) + throws IOException { + return methodUrl(path, out, DEFAULT_CLIENT_TIMEOUT_MS, null, null, "GET", followRedirects); + } + + public static int headUrl(String path, ByteChunk out, Map> resHead) + throws IOException { + return methodUrl(path, out, DEFAULT_CLIENT_TIMEOUT_MS, null, resHead, "HEAD"); + } + + public static int getUrl(String path, ByteChunk out, Map> reqHead, + Map> resHead) throws IOException { + return getUrl(path, out, DEFAULT_CLIENT_TIMEOUT_MS, reqHead, resHead); + } + + public static int getUrl(String path, ByteChunk out, int readTimeout, + Map> reqHead, Map> resHead) + throws IOException { + return methodUrl(path, out, readTimeout, reqHead, resHead, "GET"); + } + + public static int methodUrl(String path, ByteChunk out, int readTimeout, + Map> reqHead, Map> resHead, String method) + throws IOException { + return methodUrl(path, out, readTimeout, reqHead, resHead, method, true); + } + + public static int methodUrl(String path, ByteChunk out, int readTimeout, + Map> reqHead, Map> resHead, String method, + boolean followRedirects) throws IOException { + + URL url = new URL(path); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setUseCaches(false); + connection.setReadTimeout(readTimeout); + connection.setRequestMethod(method); + connection.setInstanceFollowRedirects(followRedirects); + if (reqHead != null) { + for (Map.Entry> entry : reqHead.entrySet()) { + StringBuilder valueList = new StringBuilder(); + for (String value : entry.getValue()) { + if (valueList.length() > 0) { + valueList.append(','); + } + valueList.append(value); + } + connection.setRequestProperty(entry.getKey(), + valueList.toString()); + } + } + connection.connect(); + int rc = connection.getResponseCode(); + if (resHead != null) { + // Skip the entry with null key that is used for the response line + // that some Map implementations may not accept. + for (Map.Entry> entry : connection.getHeaderFields().entrySet()) { + if (entry.getKey() != null) { + resHead.put(entry.getKey(), entry.getValue()); + } + } + } + InputStream is; + if (rc < 400) { + is = connection.getInputStream(); + } else { + is = connection.getErrorStream(); + } + if (is != null) { + try (BufferedInputStream bis = new BufferedInputStream(is)) { + byte[] buf = new byte[2048]; + int rd = 0; + while((rd = bis.read(buf)) > 0) { + out.append(buf, 0, rd); + } + } + } + return rc; + } + + public static ByteChunk postUrl(byte[] body, String path) + throws IOException { + ByteChunk out = new ByteChunk(); + postUrl(body, path, out, null); + return out; + } + + public static int postUrl(byte[] body, String path, ByteChunk out, + Map> resHead) throws IOException { + return postUrl(body, path, out, null, resHead); + } + + public static int postUrl(final byte[] body, String path, ByteChunk out, + Map> reqHead, + Map> resHead) throws IOException { + BytesStreamer s = new BytesStreamer() { + boolean done = false; + @Override + public byte[] next() { + done = true; + return body; + + } + + @Override + public int getLength() { + return body!=null?body.length:0; + } + + @Override + public int available() { + if (done) { + return 0; + } else { + return getLength(); + } + } + }; + return postUrl(false,s,path,out,reqHead,resHead); + } + + + public static int postUrl(boolean stream, BytesStreamer streamer, String path, ByteChunk out, + Map> reqHead, + Map> resHead) throws IOException { + + URL url = new URL(path); + HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setReadTimeout(1000000); + if (reqHead != null) { + for (Map.Entry> entry : reqHead.entrySet()) { + StringBuilder valueList = new StringBuilder(); + for (String value : entry.getValue()) { + if (valueList.length() > 0) { + valueList.append(','); + } + valueList.append(value); + } + connection.setRequestProperty(entry.getKey(), + valueList.toString()); + } + } + if (streamer != null && stream) { + if (streamer.getLength()>0) { + connection.setFixedLengthStreamingMode(streamer.getLength()); + } else { + connection.setChunkedStreamingMode(1024); + } + } + + connection.connect(); + + // Write the request body + try (OutputStream os = connection.getOutputStream()) { + while (streamer != null && streamer.available() > 0) { + byte[] next = streamer.next(); + os.write(next); + os.flush(); + } + } + + int rc = connection.getResponseCode(); + if (resHead != null) { + Map> head = connection.getHeaderFields(); + resHead.putAll(head); + } + InputStream is; + if (rc < 400) { + is = connection.getInputStream(); + } else { + is = connection.getErrorStream(); + } + + try (BufferedInputStream bis = new BufferedInputStream(is)) { + byte[] buf = new byte[2048]; + int rd = 0; + while((rd = bis.read(buf)) > 0) { + out.append(buf, 0, rd); + } + } + return rc; + } + + protected static String getStatusCode(String statusLine) { + if (statusLine == null || statusLine.length() < 12) { + return statusLine; + } else { + return statusLine.substring(9, 12); + } + } + + protected static String getSingleHeader(String header, Map> headers) { + // Assume headers is never null + + // Assume that either: + // a) is correct since HTTP headers are case insensitive but most Map + // implementations are case-sensitive; or + // b) CaseInsensitiveKeyMap or similar is used + List headerValues = headers.get(header); + + // Looking for a single header. No matches are OK + if (headerValues == null) { + return null; + } + + // Found a single header - return the header value + if (headerValues.size() == 1) { + return headerValues.get(0); + } + + // More than one header value is an error + throw new IllegalStateException("Found multiple headers for [" + header + "]"); + } + + private static class TomcatWithFastSessionIDs extends Tomcat { + + @Override + public void start() throws LifecycleException { + // Use fast, insecure session ID generation for all tests + Server server = getServer(); + for (Service service : server.findServices()) { + Container e = service.getContainer(); + for (Container h : e.findChildren()) { + for (Container c : h.findChildren()) { + Manager m = ((Context) c).getManager(); + if (m == null) { + m = new StandardManager(); + ((Context) c).setManager(m); + } + if (m instanceof ManagerBase) { + ((ManagerBase) m).setSecureRandomClass( + "org.apache.catalina.startup.FastNonSecureRandom"); + } + } + } + } + super.start(); + } + } + + + public static void recursiveCopy(final Path src, final Path dest) + throws IOException { + + Files.walkFileTree(src, new FileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, + BasicFileAttributes attrs) throws IOException { + Files.copy(dir, dest.resolve(src.relativize(dir))); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) throws IOException { + Path destPath = dest.resolve(src.relativize(file)); + Files.copy(file, destPath); + // Make sure that HostConfig thinks all newly copied files have + // been modified. + Assert.assertTrue("Failed to set last modified for [" + destPath + "]", + destPath.toFile().setLastModified( + System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS)); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException ioe) + throws IOException { + throw ioe; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException ioe) + throws IOException { + // NO-OP + return FileVisitResult.CONTINUE; + }}); + } + + + public static void skipTldsForResourceJars(Context context) { + StandardJarScanner scanner = (StandardJarScanner) context.getJarScanner(); + StandardJarScanFilter filter = (StandardJarScanFilter) scanner.getJarScanFilter(); + filter.setTldSkip(filter.getTldSkip() + ",resources*.jar"); + } + + + public static void forceSessionMaxInactiveInterval(Context context, int newIntervalSecs) { + Session[] sessions = context.getManager().findSessions(); + for (Session session : sessions) { + session.setMaxInactiveInterval(newIntervalSecs); + } + } +} diff --git a/test/org/apache/catalina/startup/service-config.txt b/test/org/apache/catalina/startup/service-config.txt new file mode 100644 index 0000000..a2081b0 --- /dev/null +++ b/test/org/apache/catalina/startup/service-config.txt @@ -0,0 +1,20 @@ +# 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. + +# This is a test file for the WebappServiceLoader +# It contains comment lines and blank lines + +provider1 # This comment should be ignored +provider1 # provider 1 should only be returned once diff --git a/test/org/apache/catalina/startup/web-1lifecyclecallback.xml b/test/org/apache/catalina/startup/web-1lifecyclecallback.xml new file mode 100644 index 0000000..bc80573 --- /dev/null +++ b/test/org/apache/catalina/startup/web-1lifecyclecallback.xml @@ -0,0 +1,32 @@ + + + + + test.TestServlet + postConstruct + + + test.TestServlet + preDestroy + + \ No newline at end of file diff --git a/test/org/apache/catalina/startup/web-1ordering.xml b/test/org/apache/catalina/startup/web-1ordering.xml new file mode 100644 index 0000000..6db953c --- /dev/null +++ b/test/org/apache/catalina/startup/web-1ordering.xml @@ -0,0 +1,27 @@ + + + + + bar + + \ No newline at end of file diff --git a/test/org/apache/catalina/startup/web-2lifecyclecallback.xml b/test/org/apache/catalina/startup/web-2lifecyclecallback.xml new file mode 100644 index 0000000..c0dfef3 --- /dev/null +++ b/test/org/apache/catalina/startup/web-2lifecyclecallback.xml @@ -0,0 +1,32 @@ + + + + + test.TestServlet + postConstruct1 + + + test.TestServlet + postConstruct2 + + \ No newline at end of file diff --git a/test/org/apache/catalina/startup/web-2ordering.xml b/test/org/apache/catalina/startup/web-2ordering.xml new file mode 100644 index 0000000..b6ce716 --- /dev/null +++ b/test/org/apache/catalina/startup/web-2ordering.xml @@ -0,0 +1,30 @@ + + + + + foo + + + bar + + \ No newline at end of file diff --git a/test/org/apache/catalina/startup/web-fragment-1name.xml b/test/org/apache/catalina/startup/web-fragment-1name.xml new file mode 100644 index 0000000..0742453 --- /dev/null +++ b/test/org/apache/catalina/startup/web-fragment-1name.xml @@ -0,0 +1,25 @@ + + + + name1 + \ No newline at end of file diff --git a/test/org/apache/catalina/startup/web-fragment-1ordering.xml b/test/org/apache/catalina/startup/web-fragment-1ordering.xml new file mode 100644 index 0000000..b609887 --- /dev/null +++ b/test/org/apache/catalina/startup/web-fragment-1ordering.xml @@ -0,0 +1,29 @@ + + + + + + bar + + + \ No newline at end of file diff --git a/test/org/apache/catalina/startup/web-fragment-2name.xml b/test/org/apache/catalina/startup/web-fragment-2name.xml new file mode 100644 index 0000000..ffcdd68 --- /dev/null +++ b/test/org/apache/catalina/startup/web-fragment-2name.xml @@ -0,0 +1,26 @@ + + + + name1 + name2 + \ No newline at end of file diff --git a/test/org/apache/catalina/startup/web-fragment-2ordering.xml b/test/org/apache/catalina/startup/web-fragment-2ordering.xml new file mode 100644 index 0000000..acd10e9 --- /dev/null +++ b/test/org/apache/catalina/startup/web-fragment-2ordering.xml @@ -0,0 +1,34 @@ + + + + + + foo + + + + + bar + + + \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/TesterMulticast.java b/test/org/apache/catalina/tribes/TesterMulticast.java new file mode 100644 index 0000000..c0f1c95 --- /dev/null +++ b/test/org/apache/catalina/tribes/TesterMulticast.java @@ -0,0 +1,137 @@ +/* + * 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.catalina.tribes; + +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.UnknownHostException; + +import org.apache.catalina.tribes.util.JreCompat; + +/** + * A simple multicast test that replicates the core elements of Tomcat's + * multicast membership. If this works then multicast membership should work. + * Useful notes for various operating systems follow.

    + * OSX + *

      + *
    • The firewall blocks multicast between processes on the local machine so + * you will need to disable the OSX firewall before the test below will + * work.
    • + *
    + * Windows Server 2008 + *
      + *
    • This works out of the box
    • + *
    + */ +public class TesterMulticast { + + private static final String ADDRESS = "228.0.0.4"; + private static final int PORT = 56565; + private static final InetAddress INET_ADDRESS; + + static { + InetAddress result = null; + try { + result = InetAddress.getByName(ADDRESS); + } catch (UnknownHostException e) { + // deal with later + } + INET_ADDRESS = result; + } + + + public static void main(String[] args) throws Exception { + // Start Rx Thread + Rx rx = new Rx(); + Thread rxThread = new Thread(rx); + rxThread.setDaemon(true); + rxThread.start(); + + // Start Tx Thread + Tx tx = new Tx(); + Thread txThread = new Thread(tx); + txThread.setDaemon(true); + txThread.start(); + + + Thread.sleep(10000); + + tx.stop(); + rx.stop(); + } + + private static class Rx implements Runnable { + + private volatile boolean run = true; + + @Override + public void run() { + try (MulticastSocket s = new MulticastSocket(PORT)) { + JreCompat.getInstance().setSocketoptionIpMulticastLoop(s, true); + s.joinGroup(new InetSocketAddress(INET_ADDRESS, 0), null); + DatagramPacket p = new DatagramPacket(new byte[4], 4); + p.setAddress(INET_ADDRESS); + p.setPort(PORT); + while (run) { + s.receive(p); + String d = new String (p.getData()); + System.out.println("Rx: " + d); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void stop() { + run = false; + } + } + + private static class Tx implements Runnable { + + private volatile boolean run = true; + + @Override + public void run() { + try (MulticastSocket s = new MulticastSocket(PORT)) { + JreCompat.getInstance().setSocketoptionIpMulticastLoop(s, true); + s.joinGroup(new InetSocketAddress(INET_ADDRESS, 0), null); + DatagramPacket p = new DatagramPacket(new byte[4], 4); + p.setAddress(INET_ADDRESS); + p.setPort(PORT); + long counter = 0; + String msg; + while (run) { + msg = String.format("%04d", Long.valueOf(counter)); + p.setData(msg.getBytes()); + System.out.println("Tx: " + msg); + s.send(p); + counter++; + Thread.sleep(500); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void stop() { + run = false; + } + } +} diff --git a/test/org/apache/catalina/tribes/TesterUtil.java b/test/org/apache/catalina/tribes/TesterUtil.java new file mode 100644 index 0000000..54a8db3 --- /dev/null +++ b/test/org/apache/catalina/tribes/TesterUtil.java @@ -0,0 +1,52 @@ +/* + * 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.catalina.tribes; + +import org.apache.catalina.tribes.group.interceptors.DomainFilterInterceptor; +import org.apache.catalina.tribes.util.UUIDGenerator; + +/** + * Utility methods for use by multiple tests. + */ +public class TesterUtil { + + private TesterUtil() { + // Hide default constructor + } + + + /* + * Configures a set of channels to use a random domain. Use to ensure that + * multiple instance of the test suite do not interfere when running on the + * same machine. This may happen in a CI system or when a developer is + * running tests for multiple branches in parallel. + */ + public static void addRandomDomain(ManagedChannel[] channels) { + if (channels == null) { + return; + } + + byte[] domain = UUIDGenerator.randomUUID(false); + + for (ManagedChannel channel : channels) { + channel.getMembershipService().setDomain(domain); + DomainFilterInterceptor filter = new DomainFilterInterceptor(); + filter.setDomain(domain); + channel.addInterceptor(filter); + } + } +} diff --git a/test/org/apache/catalina/tribes/demos/ChannelCreator.java b/test/org/apache/catalina/tribes/demos/ChannelCreator.java new file mode 100644 index 0000000..77009ca --- /dev/null +++ b/test/org/apache/catalina/tribes/demos/ChannelCreator.java @@ -0,0 +1,238 @@ +/* + * 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.catalina.tribes.demos; + +import java.util.ArrayList; +import java.util.Properties; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.GroupChannel; +import org.apache.catalina.tribes.group.interceptors.DomainFilterInterceptor; +import org.apache.catalina.tribes.group.interceptors.FragmentationInterceptor; +import org.apache.catalina.tribes.group.interceptors.GzipInterceptor; +import org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor; +import org.apache.catalina.tribes.group.interceptors.OrderInterceptor; +import org.apache.catalina.tribes.group.interceptors.StaticMembershipInterceptor; +import org.apache.catalina.tribes.group.interceptors.TcpFailureDetector; +import org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor; +import org.apache.catalina.tribes.membership.McastService; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.transport.Constants; +import org.apache.catalina.tribes.transport.MultiPointSender; +import org.apache.catalina.tribes.transport.ReceiverBase; +import org.apache.catalina.tribes.transport.ReplicationTransmitter; + +public class ChannelCreator { + + public static StringBuilder usage() { + StringBuilder buf = new StringBuilder(); + buf.append("\n\t\t[-bind tcpbindaddress]").append("\n\t\t[-tcpselto tcpselectortimeout]") + .append("\n\t\t[-tcpthreads tcpthreadcount]").append("\n\t\t[-port tcplistenport]") + .append("\n\t\t[-autobind tcpbindtryrange]").append("\n\t\t[-ackto acktimeout]") + .append("\n\t\t[-receiver org.apache.catalina.tribes.transport.nio.NioReceiver|org.apache.catalina.tribes.transport.bio.BioReceiver|]") + .append("\n\t\t[-transport org.apache.catalina.tribes.transport.nio.PooledParallelSender|org.apache.catalina.tribes.transport.bio.PooledMultiSender]") + .append("\n\t\t[-transport.xxx transport specific property]").append("\n\t\t[-maddr multicastaddr]") + .append("\n\t\t[-mport multicastport]").append("\n\t\t[-mbind multicastbindaddr]") + .append("\n\t\t[-mfreq multicastfrequency]").append("\n\t\t[-mdrop multicastdroptime]") + .append("\n\t\t[-gzip]") + .append("\n\t\t[-static hostname:port (-static localhost:9999 -static 127.0.0.1:8888 can be repeated)]") + .append("\n\t\t[-order]").append("\n\t\t[-ordersize maxorderqueuesize]").append("\n\t\t[-frag]") + .append("\n\t\t[-fragsize maxmsgsize]").append("\n\t\t[-throughput]").append("\n\t\t[-failuredetect]") + .append("\n\t\t[-async]").append("\n\t\t[-asyncsize maxqueuesizeinkilobytes]"); + return buf; + + } + + public static Channel createChannel(String[] args) throws Exception { + String bind = "auto"; + int port = 4001; + String mbind = null; + boolean gzip = false; + int tcpseltimeout = 5000; + int tcpthreadcount = 4; + int acktimeout = 15000; + String mcastaddr = "228.0.0.5"; + int mcastport = 45565; + long mcastfreq = 500; + long mcastdrop = 2000; + boolean order = false; + int ordersize = Integer.MAX_VALUE; + boolean frag = false; + int fragsize = 1024; + int autoBind = 10; + ArrayList staticMembers = new ArrayList<>(); + Properties transportProperties = new Properties(); + String transport = "org.apache.catalina.tribes.transport.nio.PooledParallelSender"; + String receiver = "org.apache.catalina.tribes.transport.nio.NioReceiver"; + boolean async = false; + int asyncsize = 1024 * 1024 * 50; // 50 MiB + boolean throughput = false; + boolean failuredetect = false; + + for (int i = 0; i < args.length; i++) { + if ("-bind".equals(args[i])) { + bind = args[++i]; + } else if ("-port".equals(args[i])) { + port = Integer.parseInt(args[++i]); + } else if ("-autobind".equals(args[i])) { + autoBind = Integer.parseInt(args[++i]); + } else if ("-tcpselto".equals(args[i])) { + tcpseltimeout = Integer.parseInt(args[++i]); + } else if ("-tcpthreads".equals(args[i])) { + tcpthreadcount = Integer.parseInt(args[++i]); + } else if ("-gzip".equals(args[i])) { + gzip = true; + } else if ("-async".equals(args[i])) { + async = true; + } else if ("-failuredetect".equals(args[i])) { + failuredetect = true; + } else if ("-asyncsize".equals(args[i])) { + asyncsize = Integer.parseInt(args[++i]); + System.out.println("Setting MessageDispatchInterceptor.maxQueueSize=" + asyncsize); + } else if ("-static".equals(args[i])) { + String d = args[++i]; + String h = d.substring(0, d.indexOf(':')); + String p = d.substring(h.length() + 1); + Member m = new MemberImpl(h, Integer.parseInt(p), 2000); + staticMembers.add(m); + } else if ("-throughput".equals(args[i])) { + throughput = true; + } else if ("-order".equals(args[i])) { + order = true; + } else if ("-ordersize".equals(args[i])) { + ordersize = Integer.parseInt(args[++i]); + System.out.println("Setting OrderInterceptor.maxQueue=" + ordersize); + } else if ("-frag".equals(args[i])) { + frag = true; + } else if ("-fragsize".equals(args[i])) { + fragsize = Integer.parseInt(args[++i]); + System.out.println("Setting FragmentationInterceptor.maxSize=" + fragsize); + } else if ("-ackto".equals(args[i])) { + acktimeout = Integer.parseInt(args[++i]); + } else if ("-transport".equals(args[i])) { + transport = args[++i]; + } else if (args[i] != null && args[i].startsWith("transport.")) { + String key = args[i]; + String val = args[++i]; + transportProperties.setProperty(key, val); + } else if ("-receiver".equals(args[i])) { + receiver = args[++i]; + } else if ("-maddr".equals(args[i])) { + mcastaddr = args[++i]; + } else if ("-mport".equals(args[i])) { + mcastport = Integer.parseInt(args[++i]); + } else if ("-mfreq".equals(args[i])) { + mcastfreq = Long.parseLong(args[++i]); + } else if ("-mdrop".equals(args[i])) { + mcastdrop = Long.parseLong(args[++i]); + } else if ("-mbind".equals(args[i])) { + mbind = args[++i]; + } + } + + System.out.println("Creating receiver class=" + receiver); + Class cl = Class.forName(receiver, true, ChannelCreator.class.getClassLoader()); + ReceiverBase rx = (ReceiverBase) cl.getConstructor().newInstance(); + rx.setAddress(bind); + rx.setPort(port); + rx.setSelectorTimeout(tcpseltimeout); + rx.setMaxThreads(tcpthreadcount); + rx.setMinThreads(tcpthreadcount); + rx.getBind(); + rx.setRxBufSize(Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE); + rx.setTxBufSize(Constants.DEFAULT_CLUSTER_ACK_BUFFER_SIZE); + rx.setAutoBind(autoBind); + + + ReplicationTransmitter ps = new ReplicationTransmitter(); + System.out.println("Creating transport class=" + transport); + MultiPointSender sender = (MultiPointSender) Class + .forName(transport, true, ChannelCreator.class.getClassLoader()).getConstructor().newInstance(); + sender.setTimeout(acktimeout); + sender.setMaxRetryAttempts(2); + sender.setRxBufSize(Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE); + sender.setTxBufSize(Constants.DEFAULT_CLUSTER_ACK_BUFFER_SIZE); + + for (Object o : transportProperties.keySet()) { + String key = (String) o; + IntrospectionUtils.setProperty(sender, key, transportProperties.getProperty(key)); + } + IntrospectionUtils.clear(); + ps.setTransport(sender); + + McastService service = new McastService(); + service.setAddress(mcastaddr); + if (mbind != null) { + service.setMcastBindAddress(mbind); + } + service.setFrequency(mcastfreq); + service.setMcastDropTime(mcastdrop); + service.setPort(mcastport); + + ManagedChannel channel = new GroupChannel(); + channel.setChannelReceiver(rx); + channel.setChannelSender(ps); + channel.setMembershipService(service); + + if (throughput) { + channel.addInterceptor(new ThroughputInterceptor()); + } + if (gzip) { + channel.addInterceptor(new GzipInterceptor()); + } + if (frag) { + FragmentationInterceptor fi = new FragmentationInterceptor(); + fi.setMaxSize(fragsize); + channel.addInterceptor(fi); + } + if (order) { + OrderInterceptor oi = new OrderInterceptor(); + oi.setMaxQueue(ordersize); + channel.addInterceptor(oi); + } + + if (async) { + MessageDispatchInterceptor mi = new MessageDispatchInterceptor(); + mi.setMaxQueueSize(asyncsize); + channel.addInterceptor(mi); + System.out.println("Added MessageDispatchInterceptor"); + } + + if (failuredetect) { + TcpFailureDetector tcpfi = new TcpFailureDetector(); + channel.addInterceptor(tcpfi); + } + if (staticMembers.size() > 0) { + StaticMembershipInterceptor smi = new StaticMembershipInterceptor(); + for (Member staticMember : staticMembers) { + smi.addStaticMember(staticMember); + } + channel.addInterceptor(smi); + } + + + byte[] domain = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; + channel.getMembershipService().setDomain(domain); + DomainFilterInterceptor filter = new DomainFilterInterceptor(); + filter.setDomain(domain); + channel.addInterceptor(filter); + return channel; + } + +} \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/demos/CoordinationDemo.java b/test/org/apache/catalina/tribes/demos/CoordinationDemo.java new file mode 100644 index 0000000..ad958a0 --- /dev/null +++ b/test/org/apache/catalina/tribes/demos/CoordinationDemo.java @@ -0,0 +1,438 @@ +/* + * 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.catalina.tribes.demos; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.StringTokenizer; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.GroupChannel; +import org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor; +import org.apache.catalina.tribes.group.interceptors.NonBlockingCoordinator; +import org.apache.catalina.tribes.group.interceptors.TcpFailureDetector; +import org.apache.catalina.tribes.transport.ReceiverBase; +import org.apache.catalina.tribes.util.Arrays; + + +public class CoordinationDemo { + static int CHANNEL_COUNT = 5; + static int SCREEN_WIDTH = 120; + static long SLEEP_TIME = 10; + static int CLEAR_SCREEN = 30; + static boolean MULTI_THREAD = false; + static boolean[] VIEW_EVENTS = new boolean[255]; + StringBuilder statusLine = new StringBuilder(); + Status[] status = null; + BufferedReader reader = null; + + /** + * Construct and show the application. + */ + public CoordinationDemo() { + // Default constructor + } + + public void init() { + reader = new BufferedReader(new InputStreamReader(System.in)); + status = new Status[CHANNEL_COUNT]; + } + + + public void clearScreen() { + StringBuilder buf = new StringBuilder(700); + for (int i = 0; i < CLEAR_SCREEN; i++) { + buf.append("\n"); + } + System.out.println(buf); + } + + public void printMenuOptions() { + System.out.println("Commands:"); + System.out.println("\tstart [member id]"); + System.out.println("\tstop [member id]"); + System.out.println("\tprint (refresh)"); + System.out.println("\tquit"); + System.out.print("Enter command:"); + } + + public synchronized void printScreen() { + clearScreen(); + System.out.println(" ###." + getHeader()); + for (int i = 0; i < status.length; i++) { + System.out.print(leftfill(String.valueOf(i + 1) + ".", 5, " ")); + if (status[i] != null) { + System.out.print(status[i].getStatusLine()); + } + } + System.out.println("\n\n"); + System.out.println("Overall status:" + statusLine); + printMenuOptions(); + + } + + public String getHeader() { + // member - 30 + // running- 10 + // coord - 30 + // view-id - 24 + // view count - 8 + + StringBuilder buf = new StringBuilder(); + buf.append(leftfill("Member", 30, " ")); + buf.append(leftfill("Running", 10, " ")); + buf.append(leftfill("Coord", 30, " ")); + buf.append(leftfill("View-id(short)", 24, " ")); + buf.append(leftfill("Count", 8, " ")); + buf.append("\n"); + + buf.append(rightfill("===" + new java.sql.Timestamp(System.currentTimeMillis()).toString(), SCREEN_WIDTH, "=")); + buf.append("\n"); + return buf.toString(); + } + + public String[] tokenize(String line) { + StringTokenizer tz = new StringTokenizer(line, " "); + String[] result = new String[tz.countTokens()]; + for (int i = 0; i < result.length; i++) { + result[i] = tz.nextToken(); + } + return result; + } + + public void waitForInput() throws IOException { + for (int i = 0; i < status.length; i++) { + status[i] = new Status(this); + } + printScreen(); + String l = reader.readLine(); + String[] args; + if (l == null) { + args = new String[] {}; + } else { + args = tokenize(l); + } + while (args.length >= 1 && (!"quit".equalsIgnoreCase(args[0]))) { + if ("start".equalsIgnoreCase(args[0])) { + cmdStart(args); + } else if ("stop".equalsIgnoreCase(args[0])) { + cmdStop(args); + + } + printScreen(); + l = reader.readLine(); + if (l != null) { + args = tokenize(l); + } + } + for (Status value : status) { + value.stop(); + } + } + + private void cmdStop(String[] args) { + if (args.length == 1) { + setSystemStatus("System shutting down..."); + Thread[] t = new Thread[CHANNEL_COUNT]; + for (int i = 0; i < status.length; i++) { + final int j = i; + t[j] = new Thread() { + @Override + public void run() { + status[j].stop(); + } + }; + } + for (int i = 0; i < status.length; i++) { + if (MULTI_THREAD) { + t[i].start(); + } else { + t[i].run(); + } + } + setSystemStatus("System stopped."); + } else { + int index = -1; + try { + index = Integer.parseInt(args[1]) - 1; + } catch (Exception x) { + setSystemStatus("Invalid index:" + args[1]); + } + if (index >= 0) { + setSystemStatus("Stopping member:" + (index + 1)); + status[index].stop(); + setSystemStatus("Member stopped:" + (index + 1)); + } + } + } + + private void cmdStart(String[] args) { + if (args.length == 1) { + setSystemStatus("System starting up..."); + Thread[] t = new Thread[CHANNEL_COUNT]; + for (int i = 0; i < status.length; i++) { + final int j = i; + t[j] = new Thread() { + @Override + public void run() { + status[j].start(); + } + }; + } + for (int i = 0; i < status.length; i++) { + if (MULTI_THREAD) { + t[i].start(); + } else { + t[i].run(); + } + } + setSystemStatus("System started."); + } else { + int index = -1; + try { + index = Integer.parseInt(args[1]) - 1; + } catch (Exception x) { + setSystemStatus("Invalid index:" + args[1]); + } + if (index >= 0) { + setSystemStatus("Starting member:" + (index + 1)); + status[index].start(); + setSystemStatus("Member started:" + (index + 1)); + } + } + } + + public void setSystemStatus(String status) { + statusLine.delete(0, statusLine.length()); + statusLine.append(status); + } + + + public static void setEvents(String events) { + java.util.Arrays.fill(VIEW_EVENTS, false); + StringTokenizer t = new StringTokenizer(events, ","); + while (t.hasMoreTokens()) { + int idx = Integer.parseInt(t.nextToken()); + VIEW_EVENTS[idx] = true; + } + } + + public static void run(String[] args, CoordinationDemo demo) throws Exception { + usage(); + java.util.Arrays.fill(VIEW_EVENTS, true); + + for (int i = 0; i < args.length; i++) { + if ("-c".equals(args[i])) { + CHANNEL_COUNT = Integer.parseInt(args[++i]); + } else if ("-t".equals(args[i])) { + MULTI_THREAD = Boolean.parseBoolean(args[++i]); + } else if ("-s".equals(args[i])) { + SLEEP_TIME = Long.parseLong(args[++i]); + } else if ("-sc".equals(args[i])) { + CLEAR_SCREEN = Integer.parseInt(args[++i]); + } else if ("-p".equals(args[i])) { + setEvents(args[++i]); + } else if ("-h".equals(args[i])) { + System.exit(0); + } + } + demo.init(); + demo.waitForInput(); + } + + private static void usage() { + System.out.println("Usage:"); + System.out.println( + "\tjava org.apache.catalina.tribes.demos.CoordinationDemo -c channel-count(int) -t multi-thread(true|false) -s sleep-time(ms) -sc clear-screen(int) -p view_events_csv(1,2,5,7)"); + System.out.println("Example:"); + System.out + .println("\tjava o.a.c.t.d.CoordinationDemo -> starts demo single threaded start/stop with 5 channels"); + System.out.println( + "\tjava o.a.c.t.d.CoordinationDemo -c 10 -> starts demo single threaded start/stop with 10 channels"); + System.out.println( + "\tjava o.a.c.t.d.CoordinationDemo -c 7 -t true -s 1000 -sc 50-> starts demo multi threaded start/stop with 7 channels and 1 second sleep time between events and 50 lines to clear screen"); + System.out.println( + "\tjava o.a.c.t.d.CoordinationDemo -t true -p 12 -> starts demo multi threaded start/stop with 5 channels and only prints the EVT_CONF_RX event"); + System.out.println(); + } + + public static void main(String[] args) throws Exception { + CoordinationDemo demo = new CoordinationDemo(); + run(args, demo); + } + + public static String leftfill(String value, int length, String ch) { + return fill(value, length, ch, true); + } + + public static String rightfill(String value, int length, String ch) { + return fill(value, length, ch, false); + } + + public static String fill(String value, int length, String ch, boolean left) { + StringBuilder buf = new StringBuilder(); + if (!left) { + buf.append(value.trim()); + } + for (int i = value.trim().length(); i < length; i++) { + buf.append(ch); + } + if (left) { + buf.append(value.trim()); + } + return buf.toString(); + } + + + public static class Status { + public CoordinationDemo parent; + public GroupChannel channel; + NonBlockingCoordinator interceptor = null; + public String status; + public Exception error; + public String startstatus = "new"; + + public Status(CoordinationDemo parent) { + this.parent = parent; + } + + public String getStatusLine() { + // member - 30 + // running- 10 + // coord - 30 + // view-id - 24 + // view count - 8 + StringBuilder buf = new StringBuilder(); + String local = ""; + String coord = ""; + String viewId = ""; + String count = "0"; + if (channel != null) { + Member lm = channel.getLocalMember(false); + local = lm != null ? lm.getName() : ""; + coord = interceptor != null && interceptor.getCoordinator() != null + ? interceptor.getCoordinator().getName() + : ""; + if (interceptor != null) { + viewId = getByteString( + interceptor.getViewId() != null ? interceptor.getViewId().getBytes() : new byte[0]); + count = String.valueOf(interceptor.getView().length); + } + } + buf.append(leftfill(local, 30, " ")); + buf.append(leftfill(startstatus, 10, " ")); + buf.append(leftfill(coord, 30, " ")); + buf.append(leftfill(viewId, 24, " ")); + buf.append(leftfill(count, 8, " ")); + buf.append("\n"); + buf.append("Status:" + status); + buf.append("\n"); + return buf.toString(); + } + + public String getByteString(byte[] b) { + if (b == null) { + return "{}"; + } + return Arrays.toString(b, 0, Math.min(b.length, 4)); + } + + public void start() { + try { + if (channel == null) { + channel = createChannel(); + startstatus = "starting"; + channel.start(Channel.DEFAULT); + startstatus = "running"; + } else { + status = "Channel already started."; + } + } catch (Exception x) { + synchronized (System.err) { + System.err.println("Start failed:"); + StackTraceElement[] els = x.getStackTrace(); + for (StackTraceElement el : els) { + System.err.println(el.toString()); + } + } + status = "Start failed:" + x.getMessage(); + error = x; + startstatus = "failed"; + try { + channel.stop(Channel.DEFAULT); + } catch (Exception ignore) { + // Ignore + } + channel = null; + interceptor = null; + } + } + + public void stop() { + try { + if (channel != null) { + channel.stop(Channel.DEFAULT); + status = "Channel Stopped"; + } else { + status = "Channel Already Stopped"; + } + } catch (Exception x) { + synchronized (System.err) { + System.err.println("Stop failed:"); + StackTraceElement[] els = x.getStackTrace(); + for (StackTraceElement el : els) { + System.err.println(el.toString()); + } + } + + status = "Stop failed:" + x.getMessage(); + error = x; + } finally { + startstatus = "stopped"; + channel = null; + interceptor = null; + } + } + + public GroupChannel createChannel() { + channel = new GroupChannel(); + ((ReceiverBase) channel.getChannelReceiver()).setAutoBind(100); + interceptor = new NonBlockingCoordinator() { + @Override + public void fireInterceptorEvent(InterceptorEvent event) { + status = event.getEventTypeDesc(); + int type = event.getEventType(); + boolean display = VIEW_EVENTS[type]; + if (display) { + parent.printScreen(); + } + try { + Thread.sleep(SLEEP_TIME); + } catch (Exception x) { + // Ignore + } + } + }; + channel.addInterceptor(interceptor); + channel.addInterceptor(new TcpFailureDetector()); + channel.addInterceptor(new MessageDispatchInterceptor()); + return channel; + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/demos/EchoRpcTest.java b/test/org/apache/catalina/tribes/demos/EchoRpcTest.java new file mode 100644 index 0000000..d028a43 --- /dev/null +++ b/test/org/apache/catalina/tribes/demos/EchoRpcTest.java @@ -0,0 +1,208 @@ +/* + * 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.catalina.tribes.demos; + +import java.io.Serializable; +import java.nio.charset.StandardCharsets; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.Response; +import org.apache.catalina.tribes.group.RpcCallback; +import org.apache.catalina.tribes.group.RpcChannel; + +public class EchoRpcTest implements RpcCallback, Runnable { + + Channel channel; + int count; + String message; + long pause; + RpcChannel rpc; + int options; + long timeout; + String name; + + public EchoRpcTest(Channel channel, String name, int count, String message, long pause, int options, long timeout) { + this.channel = channel; + this.count = count; + this.message = message; + this.pause = pause; + this.options = options; + this.rpc = new RpcChannel(name.getBytes(StandardCharsets.UTF_8), channel, this); + this.timeout = timeout; + this.name = name; + } + + /** + * If the reply has already been sent to the requesting thread, the rpc callback can handle any data that comes in + * after the fact. + * + * @param msg Serializable + * @param sender Member + */ + @Override + public void leftOver(Serializable msg, Member sender) { + System.out.println("Received a left over message from [" + sender.getName() + "] with data [" + msg + "]"); + } + + /** + * @param msg Serializable + * @param sender Member + * + * @return Serializable - null if no reply should be sent + */ + @Override + public Serializable replyRequest(Serializable msg, Member sender) { + System.out.println("Received a reply request message from [" + sender.getName() + "] with data [" + msg + "]"); + return "Reply(" + name + "):" + msg; + } + + @Override + public void run() { + long counter = 0; + while (counter < count) { + String msg = message + " cnt=" + (++counter); + try { + System.out.println("Sending [" + msg + "]"); + long start = System.currentTimeMillis(); + Response[] resp = rpc.send(channel.getMembers(), msg, options, Channel.SEND_OPTIONS_DEFAULT, timeout); + System.out.println("Send of [" + msg + "] completed. Nr of responses=" + resp.length + " Time:" + + (System.currentTimeMillis() - start) + " ms."); + for (Response response : resp) { + System.out.println("Received a response message from [" + response.getSource().getName() + + "] with data [" + response.getMessage() + "]"); + } + Thread.sleep(pause); + } catch (Exception x) { + // Ignore + } + } + } + + public static void usage() { + System.out.println("Tribes RPC tester."); + System.out.println("Usage:\n\t" + "java EchoRpcTest [options]\n\t" + "Options:\n\t\t" + + "[-mode all|first|majority] \n\t\t" + "[-debug] \n\t\t" + "[-count messagecount] \n\t\t" + + "[-timeout timeoutinms] \n\t\t" + "[-stats statinterval] \n\t\t" + + "[-pause nrofsecondstopausebetweensends] \n\t\t" + "[-message message] \n\t\t" + + "[-name rpcname] \n\t\t" + "[-break (halts execution on exception)]\n" + "\tChannel options:" + + ChannelCreator.usage() + "\n\n" + "Example:\n\t" + "java EchoRpcTest -port 4004\n\t" + + "java EchoRpcTest -bind 192.168.0.45 -port 4005\n\t" + + "java EchoRpcTest -bind 192.168.0.45 -port 4005 -mbind 192.168.0.45 -count 100 -stats 10\n"); + } + + public static void main(String[] args) throws Exception { + long pause = 3000; + int count = 1000000; + int stats = 10000; + String name = "EchoRpcId"; + int options = RpcChannel.ALL_REPLY; + long timeout = 15000; + String message = "EchoRpcMessage"; + if (args.length == 0) { + usage(); + System.exit(1); + } + for (int i = 0; i < args.length; i++) { + if ("-threads".equals(args[i])) { + // Not used + } else if ("-count".equals(args[i])) { + count = Integer.parseInt(args[++i]); + System.out.println("Sending " + count + " messages."); + } else if ("-pause".equals(args[i])) { + pause = Long.parseLong(args[++i]) * 1000; + } else if ("-break".equals(args[i])) { + // Not used + } else if ("-stats".equals(args[i])) { + stats = Integer.parseInt(args[++i]); + System.out.println("Stats every " + stats + " message"); + } else if ("-timeout".equals(args[i])) { + timeout = Long.parseLong(args[++i]); + } else if ("-message".equals(args[i])) { + message = args[++i]; + } else if ("-name".equals(args[i])) { + name = args[++i]; + } else if ("-mode".equals(args[i])) { + if ("all".equals(args[++i])) { + options = RpcChannel.ALL_REPLY; + } else if ("first".equals(args[i])) { + options = RpcChannel.FIRST_REPLY; + } else if ("majority".equals(args[i])) { + options = RpcChannel.MAJORITY_REPLY; + } + } else if ("-debug".equals(args[i])) { + // Not used + } else if ("-help".equals(args[i])) { + usage(); + System.exit(1); + } + } + + + ManagedChannel channel = (ManagedChannel) ChannelCreator.createChannel(args); + EchoRpcTest test = new EchoRpcTest(channel, name, count, message, pause, options, timeout); + channel.start(Channel.DEFAULT); + Runtime.getRuntime().addShutdownHook(new Shutdown(channel)); + test.run(); + + System.out.println("System test complete, sleeping to let threads finish."); + Thread.sleep(60 * 1000 * 60); + } + + public static class Shutdown extends Thread { + ManagedChannel channel = null; + + public Shutdown(ManagedChannel channel) { + this.channel = channel; + } + + @Override + public void run() { + System.out.println("Shutting down..."); + SystemExit exit = new SystemExit(5000); + exit.setDaemon(true); + exit.start(); + try { + channel.stop(Channel.DEFAULT); + + } catch (Exception x) { + x.printStackTrace(); + } + System.out.println("Channel stopped."); + } + } + + public static class SystemExit extends Thread { + private long delay; + + public SystemExit(long delay) { + this.delay = delay; + } + + @Override + public void run() { + try { + sleep(delay); + } catch (Exception x) { + x.printStackTrace(); + } + System.exit(0); + + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/demos/IntrospectionUtils.java b/test/org/apache/catalina/tribes/demos/IntrospectionUtils.java new file mode 100644 index 0000000..56cc59e --- /dev/null +++ b/test/org/apache/catalina/tribes/demos/IntrospectionUtils.java @@ -0,0 +1,212 @@ +/* + * 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.catalina.tribes.demos; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Utils for introspection and reflection + */ +public final class IntrospectionUtils { + + + private static final Log log = LogFactory.getLog(IntrospectionUtils.class); + + /* + * Find a method with the right name If found, call the method ( if param is int or boolean we'll convert value to + * the right type before) - that means you can have setDebug(1). + */ + @SuppressWarnings("null") + public static boolean setProperty(Object o, String name, String value) { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: setProperty(" + o.getClass() + " " + name + "=" + value + ")"); + } + + String setter = "set" + capitalize(name); + + try { + Method methods[] = findMethods(o.getClass()); + Method setPropertyMethodVoid = null; + Method setPropertyMethodBool = null; + + // First, the ideal case - a setFoo( String ) method + for (Method item : methods) { + Class paramT[] = item.getParameterTypes(); + if (setter.equals(item.getName()) && paramT.length == 1 && + "java.lang.String".equals(paramT[0].getName())) { + + item.invoke(o, new Object[] { value }); + return true; + } + } + + // Try a setFoo ( int ) or ( boolean ) + for (Method method : methods) { + boolean ok = true; + if (setter.equals(method.getName()) && method.getParameterTypes().length == 1) { + + // match - find the type and invoke it + Class paramType = method.getParameterTypes()[0]; + Object params[] = new Object[1]; + + // Try a setFoo ( int ) + if ("java.lang.Integer".equals(paramType.getName()) || "int".equals(paramType.getName())) { + try { + params[0] = Integer.valueOf(value); + } catch (NumberFormatException ex) { + ok = false; + } + // Try a setFoo ( long ) + } else if ("java.lang.Long".equals(paramType.getName()) || "long".equals(paramType.getName())) { + try { + params[0] = Long.valueOf(value); + } catch (NumberFormatException ex) { + ok = false; + } + + // Try a setFoo ( boolean ) + } else if ("java.lang.Boolean".equals(paramType.getName()) || + "boolean".equals(paramType.getName())) { + params[0] = Boolean.valueOf(value); + + // Try a setFoo ( InetAddress ) + } else if ("java.net.InetAddress".equals(paramType.getName())) { + try { + params[0] = InetAddress.getByName(value); + } catch (UnknownHostException exc) { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: Unable to resolve host name:" + value); + } + ok = false; + } + + // Unknown type + } else { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: Unknown type " + paramType.getName()); + } + } + + if (ok) { + method.invoke(o, params); + return true; + } + } + + // save "setProperty" for later + if ("setProperty".equals(method.getName())) { + if (method.getReturnType() == Boolean.TYPE) { + setPropertyMethodBool = method; + } else { + setPropertyMethodVoid = method; + } + + } + } + + // Ok, no setXXX found, try a setProperty("name", "value") + if (setPropertyMethodBool != null || setPropertyMethodVoid != null) { + Object params[] = new Object[2]; + params[0] = name; + params[1] = value; + if (setPropertyMethodBool != null) { + try { + return ((Boolean) setPropertyMethodBool.invoke(o, params)).booleanValue(); + } catch (IllegalArgumentException biae) { + // the boolean method had the wrong + // parameter types. lets try the other + if (setPropertyMethodVoid != null) { + setPropertyMethodVoid.invoke(o, params); + return true; + } else { + throw biae; + } + } + } else { + setPropertyMethodVoid.invoke(o, params); + return true; + } + } + + } catch (IllegalArgumentException ex2) { + log.warn("IAE " + o + " " + name + " " + value, ex2); + } catch (SecurityException ex1) { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: SecurityException for " + o.getClass() + " " + name + "=" + value + ")", + ex1); + } + } catch (IllegalAccessException iae) { + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: IllegalAccessException for " + o.getClass() + " " + name + "=" + value + + ")", iae); + } + } catch (InvocationTargetException ie) { + Throwable cause = ie.getCause(); + if (cause instanceof ThreadDeath) { + throw (ThreadDeath) cause; + } + if (cause instanceof VirtualMachineError) { + throw (VirtualMachineError) cause; + } + if (log.isDebugEnabled()) { + log.debug("IntrospectionUtils: InvocationTargetException for " + o.getClass() + " " + name + "=" + + value + ")", ie); + } + } + return false; + } + + /* + * Reverse of Introspector.decapitalize + */ + public static String capitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + char chars[] = name.toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + return new String(chars); + } + + // -------------------- other utils -------------------- + + public static void clear() { + objectMethods.clear(); + } + + static Map, Method[]> objectMethods = new ConcurrentHashMap<>(); + + public static Method[] findMethods(Class c) { + Method methods[] = objectMethods.get(c); + if (methods != null) { + return methods; + } + + methods = c.getMethods(); + objectMethods.put(c, methods); + return methods; + } + +} diff --git a/test/org/apache/catalina/tribes/demos/LoadTest.java b/test/org/apache/catalina/tribes/demos/LoadTest.java new file mode 100644 index 0000000..293eea1 --- /dev/null +++ b/test/org/apache/catalina/tribes/demos/LoadTest.java @@ -0,0 +1,410 @@ +/* + * 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.catalina.tribes.demos; + +import java.io.Serializable; +import java.util.Random; + +import org.apache.catalina.tribes.ByteMessage; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class LoadTest implements MembershipListener, ChannelListener, Runnable { + private static final Log log = LogFactory.getLog(LoadTest.class); + public static int size = 24000; + public static final Object mutex = new Object(); + public boolean doRun = true; + + public long bytesReceived = 0; + public float mBytesReceived = 0; + public int messagesReceived = 0; + public boolean send = true; + public boolean debug = false; + public int msgCount = 100; + ManagedChannel channel = null; + public int statsInterval = 10000; + public long pause = 0; + public boolean breakonChannelException = false; + public boolean async = false; + public long receiveStart = 0; + public int channelOptions = Channel.SEND_OPTIONS_DEFAULT; + + static int messageSize = 0; + + public static long messagesSent = 0; + public static long messageStartSendTime = 0; + public static long messageEndSendTime = 0; + public static int threadCount = 0; + + public static synchronized void startTest() { + threadCount++; + if (messageStartSendTime == 0) { + messageStartSendTime = System.currentTimeMillis(); + } + } + + public static synchronized void endTest() { + threadCount--; + if (messageEndSendTime == 0 && threadCount == 0) { + messageEndSendTime = System.currentTimeMillis(); + } + } + + + public static synchronized long addSendStats(long count) { + messagesSent += count; + return 0l; + } + + private static void printSendStats(long counter, int messageSize) { + float cnt = counter; + float size = messageSize; + float time = (System.currentTimeMillis() - messageStartSendTime) / 1000f; + log.info("****SEND STATS-" + Thread.currentThread().getName() + "*****" + "\n\tMessage count:" + counter + + "\n\tTotal bytes :" + (long) (size * cnt) + "\n\tTotal seconds:" + (time) + "\n\tBytes/second :" + + (size * cnt / time) + "\n\tMBytes/second:" + (size * cnt / time / 1024f / 1024f)); + } + + + public LoadTest(ManagedChannel channel, boolean send, int msgCount, boolean debug, long pause, int stats, + boolean breakOnEx) { + this.channel = channel; + this.send = send; + this.msgCount = msgCount; + this.debug = debug; + this.pause = pause; + this.statsInterval = stats; + this.breakonChannelException = breakOnEx; + } + + + @Override + public void run() { + + long counter = 0; + long total = 0; + LoadMessage msg = new LoadMessage(); + + try { + startTest(); + while (total < msgCount) { + if (channel.getMembers().length == 0 || (!send)) { + synchronized (mutex) { + try { + mutex.wait(); + } catch (InterruptedException x) { + log.info("Thread interrupted from wait"); + } + } + } else { + try { + // msg.setMsgNr((int)++total); + counter++; + if (debug) { + printArray(msg.getMessage()); + } + channel.send(channel.getMembers(), msg, channelOptions); + if (pause > 0) { + if (debug) { + System.out.println("Pausing sender for " + pause + " ms."); + } + Thread.sleep(pause); + } + } catch (ChannelException x) { + if (debug) { + log.error("Unable to send message:" + x.getMessage(), x); + } + log.error("Unable to send message:" + x.getMessage()); + ChannelException.FaultyMember[] faulty = x.getFaultyMembers(); + for (ChannelException.FaultyMember faultyMember : faulty) { + log.error("Faulty: " + faultyMember); + } + --counter; + if (this.breakonChannelException) { + throw x; + } + } + } + if ((counter % statsInterval) == 0 && (counter > 0)) { + // add to the global counter + counter = addSendStats(counter); + // print from the global counter + // printSendStats(LoadTest.messagesSent, LoadTest.messageSize, LoadTest.messageSendTime); + printSendStats(messagesSent, messageSize); + + } + + } + } catch (Exception x) { + log.error("Captured error while sending:" + x.getMessage()); + if (debug) { + log.error("", x); + } + printSendStats(messagesSent, messageSize); + } + endTest(); + } + + + /** + * memberAdded + * + * @param member Member TODO Implement this org.apache.catalina.tribes.MembershipListener method + */ + @Override + public void memberAdded(Member member) { + log.info("Member added:" + member); + synchronized (mutex) { + mutex.notifyAll(); + } + } + + /** + * memberDisappeared + * + * @param member Member TODO Implement this org.apache.catalina.tribes.MembershipListener method + */ + @Override + public void memberDisappeared(Member member) { + log.info("Member disappeared:" + member); + } + + @Override + public boolean accept(Serializable msg, Member mbr) { + return (msg instanceof LoadMessage) || (msg instanceof ByteMessage); + } + + @Override + public void messageReceived(Serializable msg, Member mbr) { + if (receiveStart == 0) { + receiveStart = System.currentTimeMillis(); + } + if (debug) { + if (msg instanceof LoadMessage) { + printArray(((LoadMessage) msg).getMessage()); + } + } + + if (msg instanceof ByteMessage && !(msg instanceof LoadMessage)) { + LoadMessage tmp = new LoadMessage(); + tmp.setMessage(((ByteMessage) msg).getMessage()); + msg = tmp; + tmp = null; + } + + + bytesReceived += ((LoadMessage) msg).getMessage().length; + mBytesReceived += (((LoadMessage) msg).getMessage().length) / 1024f / 1024f; + messagesReceived++; + if ((messagesReceived % statsInterval) == 0 || (messagesReceived == msgCount)) { + float bytes = (((LoadMessage) msg).getMessage().length * messagesReceived); + float seconds = (System.currentTimeMillis() - receiveStart) / 1000f; + log.info("****RECEIVE STATS-" + Thread.currentThread().getName() + "*****" + "\n\tMessage count :" + + (long) messagesReceived + "\n\tMessage/sec :" + messagesReceived / seconds + + "\n\tTotal bytes :" + (long) bytes + "\n\tTotal mbytes :" + (long) mBytesReceived + + "\n\tTime since 1st:" + seconds + " seconds" + "\n\tBytes/second :" + (bytes / seconds) + + "\n\tMBytes/second :" + (mBytesReceived / seconds) + "\n"); + + } + } + + + public static void printArray(byte[] data) { + System.out.print("{"); + for (byte datum : data) { + System.out.print(datum); + System.out.print(","); + } + System.out.println("} size:" + data.length); + } + + + public static class LoadMessage extends ByteMessage { + + public static byte[] outdata = new byte[size]; + public static final Random r = new Random(); + + public static int getMessageSize(LoadMessage msg) { + return msg.getMessage().length; + } + + static { + r.nextBytes(outdata); + } + + protected byte[] message = getMessage(); + + public LoadMessage() { + // Default constructor + } + + @Override + public byte[] getMessage() { + if (message == null) { + message = outdata; + } + return message; + } + + @Override + public void setMessage(byte[] data) { + this.message = data; + } + } + + public static void usage() { + System.out.println("Tribes Load tester."); + System.out.println("The load tester can be used in sender or received mode or both"); + System.out.println( + "Usage:\n\t" + "java LoadTest [options]\n\t" + "Options:\n\t\t" + "[-mode receive|send|both] \n\t\t" + + "[-startoptions startflags (default is Channel.DEFAULT) ] \n\t\t" + "[-debug] \n\t\t" + + "[-count messagecount] \n\t\t" + "[-stats statinterval] \n\t\t" + + "[-pause nrofsecondstopausebetweensends] \n\t\t" + "[-threads numberofsenderthreads] \n\t\t" + + "[-size messagesize] \n\t\t" + "[-sendoptions channeloptions] \n\t\t" + + "[-break (halts execution on exception)]\n" + + "[-shutdown (issues a channel.stop() command after send is completed)]\n" + + "\tChannel options:" + ChannelCreator.usage() + "\n\n" + "Example:\n\t" + + "java LoadTest -port 4004\n\t" + "java LoadTest -bind 192.168.0.45 -port 4005\n\t" + + "java LoadTest -bind 192.168.0.45 -port 4005 -mbind 192.168.0.45 -count 100 -stats 10\n"); + } + + public static void main(String[] args) throws Exception { + boolean send = true; + boolean debug = false; + long pause = 0; + int count = 1000000; + int stats = 10000; + boolean breakOnEx = false; + int threads = 1; + boolean shutdown = false; + int startoptions = Channel.DEFAULT; + int channelOptions = Channel.SEND_OPTIONS_DEFAULT; + if (args.length == 0) { + args = new String[] { "-help" }; + } + for (int i = 0; i < args.length; i++) { + if ("-threads".equals(args[i])) { + threads = Integer.parseInt(args[++i]); + } else if ("-count".equals(args[i])) { + count = Integer.parseInt(args[++i]); + System.out.println("Sending " + count + " messages."); + } else if ("-pause".equals(args[i])) { + pause = Long.parseLong(args[++i]) * 1000; + } else if ("-break".equals(args[i])) { + breakOnEx = true; + } else if ("-shutdown".equals(args[i])) { + shutdown = true; + } else if ("-stats".equals(args[i])) { + stats = Integer.parseInt(args[++i]); + System.out.println("Stats every " + stats + " message"); + } else if ("-sendoptions".equals(args[i])) { + channelOptions = Integer.parseInt(args[++i]); + System.out.println("Setting send options to " + channelOptions); + } else if ("-startoptions".equals(args[i])) { + startoptions = Integer.parseInt(args[++i]); + System.out.println("Setting start options to " + startoptions); + } else if ("-size".equals(args[i])) { + size = Integer.parseInt(args[++i]) - 4; + System.out.println("Message size will be:" + (size + 4) + " bytes"); + } else if ("-mode".equals(args[i])) { + if ("receive".equals(args[++i])) { + send = false; + } + } else if ("-debug".equals(args[i])) { + debug = true; + } else if ("-help".equals(args[i])) { + usage(); + System.exit(1); + } + } + + ManagedChannel channel = (ManagedChannel) ChannelCreator.createChannel(args); + + LoadTest test = new LoadTest(channel, send, count, debug, pause, stats, breakOnEx); + test.channelOptions = channelOptions; + LoadMessage msg = new LoadMessage(); + + messageSize = LoadMessage.getMessageSize(msg); + channel.addChannelListener(test); + channel.addMembershipListener(test); + channel.start(startoptions); + Runtime.getRuntime().addShutdownHook(new Shutdown(channel)); + while (threads > 1) { + Thread t = new Thread(test); + t.setDaemon(true); + t.start(); + threads--; + test = new LoadTest(channel, send, count, debug, pause, stats, breakOnEx); + test.channelOptions = channelOptions; + } + test.run(); + if (shutdown && send) { + channel.stop(Channel.DEFAULT); + } + System.out.println("System test complete, sleeping to let threads finish."); + Thread.sleep(60 * 1000 * 60); + } + + public static class Shutdown extends Thread { + ManagedChannel channel = null; + + public Shutdown(ManagedChannel channel) { + this.channel = channel; + } + + @Override + public void run() { + System.out.println("Shutting down..."); + SystemExit exit = new SystemExit(5000); + exit.setDaemon(true); + exit.start(); + try { + channel.stop(Channel.DEFAULT); + + } catch (Exception x) { + x.printStackTrace(); + } + System.out.println("Channel stopped."); + } + } + + public static class SystemExit extends Thread { + private long delay; + + public SystemExit(long delay) { + this.delay = delay; + } + + @Override + public void run() { + try { + sleep(delay); + } catch (Exception x) { + x.printStackTrace(); + } + System.exit(0); + + } + } + +} \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/demos/MapDemo.java b/test/org/apache/catalina/tribes/demos/MapDemo.java new file mode 100644 index 0000000..564d211 --- /dev/null +++ b/test/org/apache/catalina/tribes/demos/MapDemo.java @@ -0,0 +1,557 @@ +/* + * 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.catalina.tribes.demos; + +import java.awt.Color; +import java.awt.Component; +import java.awt.ComponentOrientation; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.Serializable; +import java.util.Random; + +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableColumn; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.tipis.LazyReplicatedMap; + +/** + * Example of how the lazy replicated map works, also shows how the BackupManager works in a Tomcat cluster. + */ +public class MapDemo implements ChannelListener, MembershipListener { + + /** + * The Map containing the replicated data + */ + protected LazyReplicatedMap map; + + /** + * Table to be displayed in Swing + */ + protected SimpleTableDemo table; + + /** + * Constructs a map demo object. + * + * @param channel - the Tribes channel object to be used for communication + * @param mapName - the name of this map + */ + public MapDemo(Channel channel, String mapName) { + // instantiate the replicated map + map = new LazyReplicatedMap<>(null, channel, 5000, mapName, null); + // create a gui, name it with the member name of this JVM + table = SimpleTableDemo.createAndShowGUI(map, channel.getLocalMember(false).getName()); + // add ourself as a listener for messages + channel.addChannelListener(this); + // add ourself as a listener for memberships + channel.addMembershipListener(this); + // initialize the map by receiving a fake message + this.messageReceived(null, null); + } + + /** + * Decides if the messageReceived should be invoked will always return false since we rely on the lazy map to do all + * the messaging for us + */ + @Override + public boolean accept(Serializable msg, Member source) { + // simple refresh the table model + table.dataModel.getValueAt(-1, -1); + return false; + } + + /** + * Invoked if accept returns true. No op for now + * + * @param msg - the message received + * @param source - the sending member + */ + @Override + public void messageReceived(Serializable msg, Member source) { + // NOOP + } + + /** + * Invoked when a member is added to the group + */ + @Override + public void memberAdded(Member member) { + // NOOP + } + + /** + * Invoked when a member leaves the group + */ + @Override + public void memberDisappeared(Member member) { + // just refresh the table model + table.dataModel.getValueAt(-1, -1); + } + + /** + * Prints usage + */ + public static void usage() { + System.out.println("Tribes MapDemo."); + System.out.println("Usage:\n\t" + "java MapDemo [channel options] mapName\n\t" + "\tChannel options:" + + ChannelCreator.usage()); + } + + @SuppressWarnings("unused") + public static void main(String[] args) throws Exception { + long start = System.currentTimeMillis(); + // create a channel object + ManagedChannel channel = (ManagedChannel) ChannelCreator.createChannel(args); + // define a map name, unless one is defined as a parameters + String mapName = "MapDemo"; + if (args.length > 0 && (!args[args.length - 1].startsWith("-"))) { + mapName = args[args.length - 1]; + } + // start the channel + channel.start(Channel.DEFAULT); + // listen for shutdown + Runtime.getRuntime().addShutdownHook(new Shutdown(channel)); + // create a map demo object + new MapDemo(channel, mapName); + + // put the main thread to sleep until we are done + System.out.println("System test complete, time to start=" + (System.currentTimeMillis() - start) + + " ms. Sleeping to let threads finish."); + Thread.sleep(60 * 1000 * 60); + } + + /** + * Listens for shutdown events, and stops this instance + */ + public static class Shutdown extends Thread { + // the channel running in this demo + ManagedChannel channel = null; + + public Shutdown(ManagedChannel channel) { + this.channel = channel; + } + + + @Override + public void run() { + System.out.println("Shutting down..."); + // create an exit thread that forces a shutdown if the JVM won't exit cleanly + SystemExit exit = new SystemExit(5000); + exit.setDaemon(true); + exit.start(); + try { + // stop the channel + channel.stop(Channel.DEFAULT); + } catch (Exception x) { + x.printStackTrace(); + } + System.out.println("Channel stopped."); + } + } + + public static class SystemExit extends Thread { + private long delay; + + public SystemExit(long delay) { + this.delay = delay; + } + + @Override + public void run() { + try { + sleep(delay); + } catch (Exception x) { + x.printStackTrace(); + } + System.exit(0); + } + } + + public static class SimpleTableDemo extends JPanel implements ActionListener { + + private static final long serialVersionUID = 1L; + + private static int WIDTH = 550; + + private LazyReplicatedMap map; + private boolean DEBUG = false; + AbstractTableModel dataModel = new AbstractTableModel() { + + + private static final long serialVersionUID = 1L; + String[] columnNames = { "Rownum", "Key", "Value", "Primary Node", "Backup Node", "isPrimary", "isProxy", + "isBackup" }; + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public int getRowCount() { + return map.sizeFull() + 1; + } + + public StringBuilder getMemberNames(Member[] members) { + StringBuilder buf = new StringBuilder(); + if (members != null) { + for (Member member : members) { + buf.append(member.getName()); + buf.append("; "); + } + } + return buf; + } + + @Override + public Object getValueAt(int row, int col) { + if (row == -1) { + update(); + return ""; + } + if (row == 0) { + return columnNames[col]; + } + Object[] keys = map.keySetFull().toArray(); + String key = (String) keys[row - 1]; + LazyReplicatedMap.MapEntry entry = map.getInternal(key); + switch (col) { + case 0: + return String.valueOf(row); + case 1: + return entry.getKey(); + case 2: + return entry.getValue(); + case 3: + return entry.getPrimary() != null ? entry.getPrimary().getName() : "null"; + case 4: + return getMemberNames(entry.getBackupNodes()); + case 5: + return Boolean.valueOf(entry.isPrimary()); + case 6: + return Boolean.valueOf(entry.isProxy()); + case 7: + return Boolean.valueOf(entry.isBackup()); + default: + return ""; + } + + } + + public void update() { + fireTableDataChanged(); + } + }; + + JTextField txtAddKey = new JTextField(20); + JTextField txtAddValue = new JTextField(20); + JTextField txtRemoveKey = new JTextField(20); + JTextField txtChangeKey = new JTextField(20); + JTextField txtChangeValue = new JTextField(20); + + JTable table = null; + + public SimpleTableDemo(LazyReplicatedMap map) { + super(); + this.map = map; + + this.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); + + // final JTable table = new JTable(data, columnNames); + table = new JTable(dataModel); + + table.setPreferredScrollableViewportSize(new Dimension(WIDTH, 150)); + for (int i = 0; i < table.getColumnCount(); i++) { + TableColumn tm = table.getColumnModel().getColumn(i); + tm.setCellRenderer(new ColorRenderer()); + } + + + if (DEBUG) { + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + printDebugData(table); + } + }); + } + + // setLayout(new GridLayout(5, 0)); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + // Create the scroll pane and add the table to it. + JScrollPane scrollPane = new JScrollPane(table); + + // Add the scroll pane to this panel. + add(scrollPane); + + // create a add value button + JPanel addpanel = new JPanel(); + addpanel.setPreferredSize(new Dimension(WIDTH, 30)); + addpanel.add(createButton("Add", "add")); + addpanel.add(txtAddKey); + addpanel.add(txtAddValue); + addpanel.setMaximumSize(new Dimension(WIDTH, 30)); + add(addpanel); + + // create a remove value button + JPanel removepanel = new JPanel(); + removepanel.setPreferredSize(new Dimension(WIDTH, 30)); + removepanel.add(createButton("Remove", "remove")); + removepanel.add(txtRemoveKey); + removepanel.setMaximumSize(new Dimension(WIDTH, 30)); + add(removepanel); + + // create a change value button + JPanel changepanel = new JPanel(); + changepanel.add(createButton("Change", "change")); + changepanel.add(txtChangeKey); + changepanel.add(txtChangeValue); + changepanel.setPreferredSize(new Dimension(WIDTH, 30)); + changepanel.setMaximumSize(new Dimension(WIDTH, 30)); + add(changepanel); + + + // create sync button + JPanel syncpanel = new JPanel(); + syncpanel.add(createButton("Synchronize", "sync")); + syncpanel.add(createButton("Replicate", "replicate")); + syncpanel.add(createButton("Random", "random")); + syncpanel.setPreferredSize(new Dimension(WIDTH, 30)); + syncpanel.setMaximumSize(new Dimension(WIDTH, 30)); + add(syncpanel); + + + } + + public JButton createButton(String text, String command) { + JButton button = new JButton(text); + button.setActionCommand(command); + button.addActionListener(this); + return button; + } + + @Override + public void actionPerformed(ActionEvent e) { + System.out.println(e.getActionCommand()); + if ("add".equals(e.getActionCommand())) { + System.out.println("Add key:" + txtAddKey.getText() + " value:" + txtAddValue.getText()); + map.put(txtAddKey.getText(), new StringBuilder(txtAddValue.getText())); + } + if ("change".equals(e.getActionCommand())) { + System.out.println("Change key:" + txtChangeKey.getText() + " value:" + txtChangeValue.getText()); + StringBuilder buf = map.get(txtChangeKey.getText()); + if (buf != null) { + buf.delete(0, buf.length()); + buf.append(txtChangeValue.getText()); + map.replicate(txtChangeKey.getText(), true); + } else { + buf = new StringBuilder(); + buf.append(txtChangeValue.getText()); + map.put(txtChangeKey.getText(), buf); + } + } + if ("remove".equals(e.getActionCommand())) { + System.out.println("Remove key:" + txtRemoveKey.getText()); + map.remove(txtRemoveKey.getText()); + } + if ("sync".equals(e.getActionCommand())) { + System.out.println("Syncing from another node."); + map.transferState(); + } + if ("random".equals(e.getActionCommand())) { + Thread t = new Thread() { + @Override + public void run() { + for (int i = 0; i < 5; i++) { + String key = random(5, 0, 0, true, true, null); + map.put(key, new StringBuilder(key)); + dataModel.fireTableDataChanged(); + table.paint(table.getGraphics()); + try { + sleep(500); + } catch (InterruptedException x) { + interrupted(); + } + } + } + }; + t.start(); + } + + if ("replicate".equals(e.getActionCommand())) { + System.out.println("Replicating out to the other nodes."); + map.replicate(true); + } + dataModel.getValueAt(-1, -1); + } + + public static final Random random = new Random(); + + public static String random(int count, int start, int end, boolean letters, boolean numbers, char[] chars) { + if (count == 0) { + return ""; + } else if (count < 0) { + throw new IllegalArgumentException("Requested random string length " + count + " is less than 0."); + } + if ((start == 0) && (end == 0)) { + end = 'z' + 1; + start = ' '; + if (!letters && !numbers) { + start = 0; + end = Integer.MAX_VALUE; + } + } + + char[] buffer = new char[count]; + int gap = end - start; + + while (count-- != 0) { + char ch; + if (chars == null) { + ch = (char) (random.nextInt(gap) + start); + } else { + ch = chars[random.nextInt(gap) + start]; + } + if ((letters && Character.isLetter(ch)) || (numbers && Character.isDigit(ch)) || + (!letters && !numbers)) { + if (ch >= 56320 && ch <= 57343) { + if (count == 0) { + count++; + } else { + // low surrogate, insert high surrogate after putting it in + buffer[count] = ch; + count--; + buffer[count] = (char) (55296 + random.nextInt(128)); + } + } else if (ch >= 55296 && ch <= 56191) { + if (count == 0) { + count++; + } else { + // high surrogate, insert low surrogate before putting it in + buffer[count] = (char) (56320 + random.nextInt(128)); + count--; + buffer[count] = ch; + } + } else if (ch >= 56192 && ch <= 56319) { + // private high surrogate, no effing clue, so skip it + count++; + } else { + buffer[count] = ch; + } + } else { + count++; + } + } + return new String(buffer); + } + + private void printDebugData(JTable table) { + int numRows = table.getRowCount(); + int numCols = table.getColumnCount(); + javax.swing.table.TableModel model = table.getModel(); + + System.out.println("Value of data: "); + for (int i = 0; i < numRows; i++) { + System.out.print(" row " + i + ":"); + for (int j = 0; j < numCols; j++) { + System.out.print(" " + model.getValueAt(i, j)); + } + System.out.println(); + } + System.out.println("--------------------------"); + } + + /* + * Create the GUI and show it. For thread safety, this method should be invoked from the event-dispatching + * thread. + */ + public static SimpleTableDemo createAndShowGUI(LazyReplicatedMap map, String title) { + // Make sure we have nice window decorations. + JFrame.setDefaultLookAndFeelDecorated(true); + + // Create and set up the window. + JFrame frame = new JFrame("SimpleTableDemo - " + title); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + // Create and set up the content pane. + SimpleTableDemo newContentPane = new SimpleTableDemo(map); + newContentPane.setOpaque(true); // content panes must be opaque + frame.setContentPane(newContentPane); + + // Display the window. + frame.setSize(450, 250); + newContentPane.setSize(450, 300); + frame.pack(); + frame.setVisible(true); + return newContentPane; + } + } + + static class ColorRenderer extends DefaultTableCellRenderer { + + private static final long serialVersionUID = 1L; + + ColorRenderer() { + super(); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { + Component cell = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + cell.setBackground(Color.WHITE); + if (row > 0) { + Color color = null; + boolean primary = ((Boolean) table.getValueAt(row, 5)).booleanValue(); + boolean proxy = ((Boolean) table.getValueAt(row, 6)).booleanValue(); + boolean backup = ((Boolean) table.getValueAt(row, 7)).booleanValue(); + if (primary) { + color = Color.GREEN; + } else if (proxy) { + color = Color.RED; + } else if (backup) { + color = Color.BLUE; + } + if (color != null) { + cell.setBackground(color); + } + } + return cell; + } + + + } + + +} diff --git a/test/org/apache/catalina/tribes/demos/MembersWithProperties.java b/test/org/apache/catalina/tribes/demos/MembersWithProperties.java new file mode 100644 index 0000000..7966bb6 --- /dev/null +++ b/test/org/apache/catalina/tribes/demos/MembersWithProperties.java @@ -0,0 +1,122 @@ +/* + * 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.catalina.tribes.demos; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Properties; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.catalina.tribes.util.UUIDGenerator; + +public class MembersWithProperties implements MembershipListener { + static Thread main; + + public MembersWithProperties(Channel channel, Properties props) throws IOException { + channel.addMembershipListener(this); + ManagedChannel mchannel = (ManagedChannel) channel; + mchannel.getMembershipService().setPayload(getPayload(props)); + } + + byte[] getPayload(Properties props) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + props.store(bout, ""); + return bout.toByteArray(); + } + + Properties getProperties(byte[] payload) throws IOException { + ByteArrayInputStream bin = new ByteArrayInputStream(payload); + Properties props = new Properties(); + props.load(bin); + return props; + } + + @Override + public void memberAdded(Member member) { + try { + System.out.println("Received member added:" + member); + System.out.println("Payload[" + member + "] :"); + getProperties(member.getPayload()).store(System.out, ""); + } catch (Exception x) { + x.printStackTrace(); + } + } + + @Override + public void memberDisappeared(Member member) { + try { + System.out.println("Received member disappeared:" + member); + System.out.println("Payload[" + member + "] :"); + getProperties(member.getPayload()).store(System.out, ""); + } catch (Exception x) { + x.printStackTrace(); + } + } + + public static void usage() { + System.out.println("Tribes Member Properties demo."); + System.out.println("Usage:\n\t" + "java MemberWithProperties \n\t" + "Channel options:" + + ChannelCreator.usage() + "\n\n" + "Example:\n\t" + "java MembersWithProperties -port 4004\n\t" + + "java MembersWithProperties -bind 192.168.0.45 -port 4005\n\t" + + "java MembersWithProperties -bind 192.168.0.45 -port 4005 -mbind 192.168.0.45 -count 100 -stats 10\n"); + } + + @SuppressWarnings("unused") + public static void main(String[] args) throws Exception { + if (args.length == 0) { + usage(); + } + main = Thread.currentThread(); + ManagedChannel channel = (ManagedChannel) ChannelCreator.createChannel(args); + Properties props = new Properties(); + props.setProperty("mydomainkey", "mydomainvalue"); + props.setProperty("someotherkey", Arrays.toString(UUIDGenerator.randomUUID(true))); + new MembersWithProperties(channel, props); + channel.start(Channel.DEFAULT); + Runtime.getRuntime().addShutdownHook(new Shutdown(channel)); + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException ix) { + Thread.sleep(5000);// allow everything to shutdown + } + } + + public static class Shutdown extends Thread { + ManagedChannel channel = null; + + public Shutdown(ManagedChannel channel) { + this.channel = channel; + } + + @Override + public void run() { + System.out.println("Shutting down..."); + try { + channel.stop(Channel.DEFAULT); + } catch (Exception x) { + x.printStackTrace(); + } + System.out.println("Channel stopped."); + main.interrupt(); + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/group/TestGroupChannelMemberArrival.java b/test/org/apache/catalina/tribes/group/TestGroupChannelMemberArrival.java new file mode 100644 index 0000000..6163361 --- /dev/null +++ b/test/org/apache/catalina/tribes/group/TestGroupChannelMemberArrival.java @@ -0,0 +1,196 @@ +/* + * 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.catalina.tribes.group; + +import java.util.ArrayList; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.TesterUtil; +import org.apache.catalina.tribes.transport.ReceiverBase; + +public class TestGroupChannelMemberArrival { + private static int count = 10; + private ManagedChannel[] channels = new ManagedChannel[count]; + private TestMbrListener[] listeners = new TestMbrListener[count]; + + @Before + public void setUp() throws Exception { + for (int i = 0; i < channels.length; i++) { + channels[i] = new GroupChannel(); + ((ReceiverBase) channels[i].getChannelReceiver()).setHost("localhost"); + channels[i].getMembershipService().setPayload( ("Channel-" + (i + 1)).getBytes("ASCII")); + listeners[i] = new TestMbrListener( ("Listener-" + (i + 1))); + channels[i].addMembershipListener(listeners[i]); + } + TesterUtil.addRandomDomain(channels); + } + + @Test + public void testMemberArrival() throws Exception { + //purpose of this test is to make sure that we have received all the members + //that we can expect before the start method returns + Thread[] threads = new Thread[channels.length]; + for (int i=0; i= 0; i--) { + TestMbrListener listener = listeners[i]; + synchronized (listener.members) { + if (channels.length - 1 != listener.members.size()) { + arrivalLengthErrors.append("Checking member arrival length for ["); + arrivalLengthErrors.append(listener.name); + arrivalLengthErrors.append("]. Was ["); + arrivalLengthErrors.append(listener.members.size()); + arrivalLengthErrors.append("] but should have been ["); + arrivalLengthErrors.append(channels.length - 1); + arrivalLengthErrors.append(']'); + arrivalLengthErrors.append('\n'); + } + } + } + // Note if this fails for all listeners check multicast is working with + // org.apache.catalina.tribes.TesterMulticast + Assert.assertTrue(arrivalLengthErrors.toString(), arrivalLengthErrors.length() == 0); + System.out.println(System.currentTimeMillis() + + " Members arrival counts checked."); + } + + @After + public void tearDown() throws Exception { + + for (ManagedChannel channel : channels) { + try { + channel.stop(Channel.DEFAULT); + } catch (Exception ignore) { + // Ignore + } + } + } + + public static class TestMbrListener + implements MembershipListener { + public String name = null; + public TestMbrListener(String name) { + this.name = name; + } + + public ArrayList members = new ArrayList<>(1); + + @Override + public void memberAdded(Member member) { + String msg; + int count; + synchronized (members) { + if (!members.contains(member)) { + members.add(member); + msg = "member added"; + } else { + msg = "member added called, but member is already in the list"; + } + count = members.size(); + } + report(msg, member, count); + } + + @Override + public void memberDisappeared(Member member) { + String msg; + int count; + synchronized (members) { + if (members.contains(member)) { + members.remove(member); + msg = "member disappeared"; + } else { + msg = "member disappeared called, but there is no such member in the list"; + } + count = members.size(); + } + report(msg, member, count); + } + + private void report(String event, Member member, int count) { + StringBuilder message = new StringBuilder(100); + message.append(System.currentTimeMillis()); + message.append(' '); + message.append(name); + message.append(':'); + message.append(event); + message.append(", has "); + message.append(count); + message.append(" members now. Member:["); + message.append("host: "); + appendByteArrayToString(message, member.getHost()); + message.append(", port: "); + message.append(member.getPort()); + message.append(", id: "); + appendByteArrayToString(message, member.getUniqueId()); + message.append(", payload: "); + try { + message.append(new String(member.getPayload(), "ASCII")); + } catch (Exception x) { + message.append("unknown"); + } + Thread t = Thread.currentThread(); + message.append("]; Thread:").append(t.getName()).append(", hash:") + .append(t.hashCode()); + System.out.println(message); + } + + private void appendByteArrayToString(StringBuilder sb, byte[] input) { + if (input == null) { + sb.append("null"); + return; + } + for (int i = 0; i < input.length; i++) { + if (i > 0) { + sb.append('.'); + } + sb.append(input[i] & 0xFF); + } + } + } + +} diff --git a/test/org/apache/catalina/tribes/group/TestGroupChannelOptionFlag.java b/test/org/apache/catalina/tribes/group/TestGroupChannelOptionFlag.java new file mode 100644 index 0000000..37701b5 --- /dev/null +++ b/test/org/apache/catalina/tribes/group/TestGroupChannelOptionFlag.java @@ -0,0 +1,108 @@ +/* + * 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.catalina.tribes.group; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.tomcat.util.res.StringManager; + +public class TestGroupChannelOptionFlag { + private final StringManager sm = StringManager.getManager(TestGroupChannelOptionFlag.class); + private GroupChannel channel = null; + + @Before + public void setUp() throws Exception { + channel = new GroupChannel(); + } + + @After + public void tearDown() throws Exception { + if (channel != null) { + try { + channel.stop(Channel.DEFAULT); + } catch (Exception ignore) { + // Ignore + } + } + channel = null; + } + + @Test + public void testOptionConflict() throws Exception { + String errorMsgRegx = getTestOptionErrorMsgRegx(); + + boolean error = false; + channel.setOptionCheck(true); + ChannelInterceptor i = new TestInterceptor(); + i.setOptionFlag(128); + channel.addInterceptor(i); + i = new TestInterceptor(); + i.setOptionFlag(128); + channel.addInterceptor(i); + try { + channel.start(Channel.DEFAULT); + } catch (ChannelException x) { + if (x.getMessage().matches(errorMsgRegx)) { + error = true; + } + } + Assert.assertTrue(error); + } + + @Test + public void testOptionNoConflict() throws Exception { + String errorMsgRegx = getTestOptionErrorMsgRegx(); + + boolean error = false; + channel.setOptionCheck(true); + ChannelInterceptor i = new TestInterceptor(); + i.setOptionFlag(128); + channel.addInterceptor(i); + i = new TestInterceptor(); + i.setOptionFlag(64); + channel.addInterceptor(i); + i = new TestInterceptor(); + i.setOptionFlag(256); + channel.addInterceptor(i); + try { + channel.start(Channel.DEFAULT); + } catch (ChannelException x) { + if (x.getMessage().matches(errorMsgRegx)) { + error = true; + } + } + Assert.assertFalse(error); + } + + private String getTestOptionErrorMsgRegx() { + String errorMsgRegx = sm.getString("groupChannel.optionFlag.conflict", ".+").replace("[", "\\["); + errorMsgRegx += "; No faulty members identified."; + return errorMsgRegx; + } + + public static class TestInterceptor extends ChannelInterceptorBase { + // Just use base class + } + + +} diff --git a/test/org/apache/catalina/tribes/group/TestGroupChannelSenderConnections.java b/test/org/apache/catalina/tribes/group/TestGroupChannelSenderConnections.java new file mode 100644 index 0000000..9e4ce21 --- /dev/null +++ b/test/org/apache/catalina/tribes/group/TestGroupChannelSenderConnections.java @@ -0,0 +1,186 @@ +/* + * 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.catalina.tribes.group; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.startup.LoggingBaseTest; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.TesterUtil; +import org.apache.catalina.tribes.transport.ReceiverBase; +import org.apache.catalina.tribes.transport.ReplicationTransmitter; + +public class TestGroupChannelSenderConnections extends LoggingBaseTest { + private static final int count = 2; + private ManagedChannel[] channels = new ManagedChannel[count]; + private TestMsgListener[] listeners = new TestMsgListener[count]; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + for (int i = 0; i < channels.length; i++) { + channels[i] = new GroupChannel(); + ((ReceiverBase) channels[i].getChannelReceiver()).setHost("localhost"); + channels[i].getMembershipService().setPayload( ("Channel-" + (i + 1)).getBytes("ASCII")); + listeners[i] = new TestMsgListener( ("Listener-" + (i + 1))); + channels[i].addChannelListener(listeners[i]); + } + TesterUtil.addRandomDomain(channels); + for (ManagedChannel channel : channels) { + channel.start(Channel.SND_RX_SEQ | Channel.SND_TX_SEQ); + } + } + + public void sendMessages(long delay, long sleep) throws Exception { + resetMessageCounters(); + Member local = channels[0].getLocalMember(true); + Member dest = channels[1].getLocalMember(true); + int n = 3; + log.info("Sending " + n + " messages from [" + local.getName() + + "] to [" + dest.getName() + "] with delay of " + delay + + " ms between them."); + for (int i = 0; i < n; i++) { + channels[0].send(new Member[] { dest }, new TestMsg(), 0); + boolean last = (i == n - 1); + if (!last && delay > 0) { + Thread.sleep(delay); + } + } + log.info("Messages sent. Waiting no more than " + (sleep / 1000) + + " seconds for them to be received"); + long startTime = System.currentTimeMillis(); + int countReceived; + while ((countReceived = getReceivedMessageCount()) != n) { + long time = System.currentTimeMillis(); + if ((time - startTime) > sleep) { + Assert.fail("Only " + countReceived + " out of " + n + + " messages have been received in " + (sleep / 1000) + + " seconds"); + break; + } + Thread.sleep(100); + } + } + + @Test + public void testConnectionLinger() throws Exception { + sendMessages(0,15000); + } + + @Test + public void testKeepAliveCount() throws Exception { + log.info("Setting keep alive count to 0"); + for (ManagedChannel channel : channels) { + ReplicationTransmitter t = (ReplicationTransmitter)channel.getChannelSender(); + t.getTransport().setKeepAliveCount(0); + } + sendMessages(1000,15000); + } + + @Test + public void testKeepAliveTime() throws Exception { + log.info("Setting keep alive count to 1 second"); + for (ManagedChannel channel : channels) { + ReplicationTransmitter t = (ReplicationTransmitter)channel.getChannelSender(); + t.getTransport().setKeepAliveTime(1000); + } + sendMessages(2000,15000); + } + + @After + @Override + public void tearDown() throws Exception { + try { + for (ManagedChannel channel : channels) { + channel.stop(Channel.DEFAULT); + } + } finally { + super.tearDown(); + } + } + + private void resetMessageCounters() { + for (TestMsgListener listener: listeners) { + listener.reset(); + } + } + + private int getReceivedMessageCount() { + int count = 0; + for (TestMsgListener listener: listeners) { + count += listener.getReceivedCount(); + } + return count; + } + + // Test message. The message size is random. + public static class TestMsg implements Serializable { + private static final long serialVersionUID = 1L; + private static Random r = new Random(); + private HashMap> map = new HashMap<>(); + public TestMsg() { + int size = Math.abs(r.nextInt() % 200); + for (int i=0; i list = new ArrayList<>(length); + map.put(Integer.valueOf(i),list); + } + } + } + + public class TestMsgListener implements ChannelListener { + private final String name; + private final AtomicInteger counter = new AtomicInteger(); + public TestMsgListener(String name) { + this.name = name; + } + + public void reset() { + counter.set(0); + } + + public int getReceivedCount() { + return counter.get(); + } + + @Override + public void messageReceived(Serializable msg, Member sender) { + counter.incrementAndGet(); + log.info("["+name+"] Received message:"+msg+" from " + sender.getName()); + } + + @Override + public boolean accept(Serializable msg, Member sender) { + return true; + } + + } + +} diff --git a/test/org/apache/catalina/tribes/group/TestGroupChannelStartStop.java b/test/org/apache/catalina/tribes/group/TestGroupChannelStartStop.java new file mode 100644 index 0000000..584b86e --- /dev/null +++ b/test/org/apache/catalina/tribes/group/TestGroupChannelStartStop.java @@ -0,0 +1,155 @@ +/* + * 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.catalina.tribes.group; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.transport.ReceiverBase; + +public class TestGroupChannelStartStop { + private GroupChannel channel = null; + private int udpPort = 45543; + + @Before + public void setUp() throws Exception { + channel = new GroupChannel(); + ((ReceiverBase) channel.getChannelReceiver()).setHost("localhost"); + } + + @After + public void tearDown() throws Exception { + try { + channel.stop(Channel.DEFAULT); + } catch (Exception ignore) { + // Ignore + } + } + + @Test + public void testDoubleFullStart() throws Exception { + int count = 0; + try { + channel.start(Channel.DEFAULT); + count++; + } catch ( Exception x){x.printStackTrace();} + try { + channel.start(Channel.DEFAULT); + count++; + } catch ( Exception x){x.printStackTrace();} + Assert.assertEquals(count,2); + channel.stop(Channel.DEFAULT); + } + + @Test + public void testScrap() throws Exception { + System.out.println(channel.getChannelReceiver().getClass()); + ((ReceiverBase)channel.getChannelReceiver()).setMaxThreads(1); + } + + @Test + public void testDoublePartialStart() throws Exception { + //try to double start the RX + int count = 0; + try { + channel.start(Channel.SND_RX_SEQ); + channel.start(Channel.MBR_RX_SEQ); + count++; + } catch ( Exception x){x.printStackTrace();} + try { + channel.start(Channel.MBR_RX_SEQ); + count++; + } catch ( Exception x){ + // expected + } + Assert.assertEquals(count,1); + channel.stop(Channel.DEFAULT); + //double the membership sender + count = 0; + try { + channel.start(Channel.SND_RX_SEQ); + channel.start(Channel.MBR_TX_SEQ); + count++; + } catch ( Exception x){x.printStackTrace();} + try { + channel.start(Channel.MBR_TX_SEQ); + count++; + } catch ( Exception x){ + // expected + } + Assert.assertEquals(count,1); + channel.stop(Channel.DEFAULT); + + count = 0; + try { + channel.start(Channel.SND_RX_SEQ); + count++; + } catch ( Exception x){x.printStackTrace();} + try { + channel.start(Channel.SND_RX_SEQ); + count++; + } catch ( Exception x){ + // expected + } + Assert.assertEquals(count,1); + channel.stop(Channel.DEFAULT); + + count = 0; + try { + channel.start(Channel.SND_TX_SEQ); + count++; + } catch ( Exception x){x.printStackTrace();} + try { + channel.start(Channel.SND_TX_SEQ); + count++; + } catch ( Exception x){ + // expected + } + Assert.assertEquals(count,1); + channel.stop(Channel.DEFAULT); + } + + @Test + public void testFalseOption() throws Exception { + int flag = 0xFFF0;//should get ignored by the underlying components + int count = 0; + try { + channel.start(flag); + count++; + } catch ( Exception x){x.printStackTrace();} + try { + channel.start(flag); + count++; + } catch ( Exception x){ + // expected + } + Assert.assertEquals(count,2); + channel.stop(Channel.DEFAULT); + } + + @Test + public void testUdpReceiverStart() throws Exception { + ReceiverBase rb = (ReceiverBase)channel.getChannelReceiver(); + rb.setUdpPort(udpPort); + channel.start(Channel.DEFAULT); + Thread.sleep(1000); + channel.stop(Channel.DEFAULT); + } +} diff --git a/test/org/apache/catalina/tribes/group/interceptors/EncryptionInterceptorBaseTest.java b/test/org/apache/catalina/tribes/group/interceptors/EncryptionInterceptorBaseTest.java new file mode 100644 index 0000000..c3db40d --- /dev/null +++ b/test/org/apache/catalina/tribes/group/interceptors/EncryptionInterceptorBaseTest.java @@ -0,0 +1,185 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.io.File; +import java.security.Security; +import java.util.ArrayList; +import java.util.Collection; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; + +public class EncryptionInterceptorBaseTest { + + protected static final String MESSAGE_FILE = "message.bin"; + + protected static final String encryptionKey128 = "cafebabedeadbeefbeefcafecafebabe"; + protected static final String encryptionKey192 = "cafebabedeadbeefbeefcafecafebabedeadbeefbeefcafe"; + protected static final String encryptionKey256 = "cafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeef"; + + protected EncryptInterceptor src; + protected EncryptInterceptor dest; + + + @BeforeClass + public static void setupClass() { + Security.setProperty("jdk.tls.disabledAlgorithms", ""); + Security.setProperty("crypto.policy", "unlimited"); + } + + + @AfterClass + public static void cleanup() { + File f = new File(MESSAGE_FILE); + if (f.isFile()) { + Assert.assertTrue(f.delete()); + } + } + + + @Before + public void setup() { + src = new EncryptInterceptor(); + src.setEncryptionKey(encryptionKey128); + + dest = new EncryptInterceptor(); + dest.setEncryptionKey(encryptionKey128); + + src.setNext(new PipedInterceptor(dest)); + dest.setPrevious(new ValueCaptureInterceptor()); + } + + + /** + * Actually go through the interceptor's send/receive message methods. + * + * @param input The clear text message to sent + * @param src The interceptor to use to encrypt the message + * @param dest The interceptor to use to decrypt the message + * + * @return The clear text message received + */ + protected static String roundTrip(String input, EncryptInterceptor src, EncryptInterceptor dest) throws Exception { + byte[] bytes = input.getBytes("UTF-8"); + + bytes = roundTrip(bytes, src, dest); + + return new String(bytes, "UTF-8"); + } + + + /** + * Actually go through the interceptor's send/receive message methods. + * + * @param input The clear text message to sent + * @param src The interceptor to use to encrypt the message + * @param dest The interceptor to use to decrypt the message + * + * @return The clear text message received + */ + protected static byte[] roundTrip(byte[] input, EncryptInterceptor src, EncryptInterceptor dest) throws Exception { + ChannelData msg = new ChannelData(false); + msg.setMessage(new XByteBuffer(input, false)); + src.sendMessage(null, msg, null); + + return ((ValueCaptureInterceptor) dest.getPrevious()).getValue(); + } + + + /** + * Interceptor that delivers directly to a destination. + */ + protected static class PipedInterceptor extends ChannelInterceptorBase { + private ChannelInterceptor dest; + + PipedInterceptor(ChannelInterceptor dest) { + if (null == dest) { + throw new IllegalArgumentException("Destination must not be null"); + } + + this.dest = dest; + } + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) + throws ChannelException { + dest.messageReceived(msg); + } + } + + /** + * Interceptor that simply captures the latest message sent to or received by it. + */ + protected static class ValueCaptureInterceptor extends ChannelInterceptorBase { + private byte[] value; + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) + throws ChannelException { + value = msg.getMessage().getBytes(); + } + + @Override + public void messageReceived(ChannelMessage msg) { + value = msg.getMessage().getBytes(); + } + + public byte[] getValue() { + return value; + } + } + + /** + * Interceptor that simply captures all messages sent to or received by it. + */ + protected static class ValuesCaptureInterceptor extends ChannelInterceptorBase { + private ArrayList messages = new ArrayList<>(); + + @Override + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) + throws ChannelException { + synchronized (messages) { + messages.add(msg.getMessage().getBytes()); + } + } + + @Override + public void messageReceived(ChannelMessage msg) { + synchronized (messages) { + messages.add(msg.getMessage().getBytes()); + } + } + + @SuppressWarnings("unchecked") + public Collection getValues() { + return (Collection) messages.clone(); + } + } + +} diff --git a/test/org/apache/catalina/tribes/group/interceptors/TestDomainFilterInterceptor.java b/test/org/apache/catalina/tribes/group/interceptors/TestDomainFilterInterceptor.java new file mode 100644 index 0000000..075ce54 --- /dev/null +++ b/test/org/apache/catalina/tribes/group/interceptors/TestDomainFilterInterceptor.java @@ -0,0 +1,134 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.ArrayList; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.group.GroupChannel; +import org.apache.catalina.tribes.util.UUIDGenerator; + +public class TestDomainFilterInterceptor { + private static int count = 10; + private ManagedChannel[] channels = new ManagedChannel[count]; + private TestMbrListener[] listeners = new TestMbrListener[count]; + + @Before + public void setUp() throws Exception { + for (int i = 0; i < channels.length; i++) { + channels[i] = new GroupChannel(); + channels[i].getMembershipService().setPayload( ("Channel-" + (i + 1)).getBytes("ASCII")); + listeners[i] = new TestMbrListener( ("Listener-" + (i + 1))); + channels[i].addMembershipListener(listeners[i]); + DomainFilterInterceptor filter = new DomainFilterInterceptor(); + filter.setDomain(UUIDGenerator.randomUUID(false)); + channels[i].addInterceptor(filter); + } + } + + public void clear() { + for (int i = 0; i < channels.length; i++) { + listeners[i].members.clear(); + } + } + + @Test + public void testMemberArrival() throws Exception { + //purpose of this test is to make sure that we have received all the members + //that we can expect before the start method returns + Thread[] threads = new Thread[channels.length]; + for (int i=0; i=0; i-- ) { + Assert.assertEquals("Checking member arrival length",0,listeners[i].members.size()); + } + } + + @After + public void tearDown() throws Exception { + + for (ManagedChannel channel : channels) { + try { + channel.stop(Channel.DEFAULT); + } catch (Exception ignore) { + // Ignore + } + } + } + + public static class TestMbrListener + implements MembershipListener { + public String name = null; + public TestMbrListener(String name) { + this.name = name; + } + + public ArrayList members = new ArrayList<>(); + @Override + public void memberAdded(Member member) { + if (!members.contains(member)) { + members.add(member); + try { + System.out.println(name + ":member added[" + new String(member.getPayload(), "ASCII") + "; Thread:"+Thread.currentThread().getName()+"]"); + } catch (Exception x) { + System.out.println(name + ":member added[unknown]"); + } + } + } + + @Override + public void memberDisappeared(Member member) { + if (members.contains(member)) { + members.remove(member); + try { + System.out.println(name + ":member disappeared[" + new String(member.getPayload(), "ASCII") + "; Thread:"+Thread.currentThread().getName()+"]"); + } catch (Exception x) { + System.out.println(name + ":member disappeared[unknown]"); + } + } + } + + } + +} diff --git a/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java b/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java new file mode 100644 index 0000000..5815d27 --- /dev/null +++ b/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java @@ -0,0 +1,377 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.Collection; + +import javax.crypto.Cipher; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.core.IsEqual; +import org.hamcrest.core.IsNot; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; + +/** + * Tests the EncryptInterceptor. + * + * Many of the tests in this class use strings as input and output, even + * though the interceptor actually operates on byte arrays. This is done + * for readability for the tests and their outputs. + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TestEncryptInterceptor extends EncryptionInterceptorBaseTest { + + @Test + public void testBasic() throws Exception { + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Basic roundtrip failed", + testInput, + roundTrip(testInput, src, dest)); + } + + @Test + public void testMultipleMessages() throws Exception { + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Basic roundtrip failed", + testInput, + roundTrip(testInput, src, dest)); + + Assert.assertEquals("Second roundtrip failed", + testInput, + roundTrip(testInput, src, dest)); + + Assert.assertEquals("Third roundtrip failed", + testInput, + roundTrip(testInput, src, dest)); + + Assert.assertEquals("Fourth roundtrip failed", + testInput, + roundTrip(testInput, src, dest)); + + Assert.assertEquals("Fifth roundtrip failed", + testInput, + roundTrip(testInput, src, dest)); + } + + @Test + public void testTinyPayload() throws Exception { + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "x"; + + Assert.assertEquals("Tiny payload roundtrip failed", + testInput, + roundTrip(testInput, src, dest)); + } + + @Test + public void testLargePayload() throws Exception { + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + byte[] bytes = new byte[1024*1024]; + + Assert.assertArrayEquals("Huge payload roundtrip failed", + bytes, + roundTrip(bytes, src, dest)); + } + + @Test + public void testCustomProvider() throws Exception { + src.setProviderName("SunJCE"); // Explicitly set the provider name + dest.setProviderName("SunJCE"); + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Failed to set custom provider name", + testInput, + roundTrip(testInput, src, dest)); + } + + @Test + public void test192BitKey() throws Exception { + Assume.assumeTrue("Skipping test192BitKey because the JVM does not support it", + 192 <= Cipher.getMaxAllowedKeyLength("AES")); + + src.setEncryptionKey(encryptionKey192); + dest.setEncryptionKey(encryptionKey192); + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Failed to set custom provider name", + testInput, + roundTrip(testInput, src, dest)); + } + + @Test + public void test256BitKey() throws Exception { + Assume.assumeTrue("Skipping test256BitKey because the JVM does not support it", + 256 <= Cipher.getMaxAllowedKeyLength("AES")); + + src.setEncryptionKey(encryptionKey256); + dest.setEncryptionKey(encryptionKey256); + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Failed to set custom provider name", + testInput, + roundTrip(testInput, src, dest)); + } + + @Test + public void testOFB() throws Exception { + src.setEncryptionAlgorithm("AES/OFB/PKCS5Padding"); + src.start(Channel.SND_TX_SEQ); + dest.setEncryptionAlgorithm("AES/OFB/PKCS5Padding"); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Failed in OFB mode", + testInput, + roundTrip(testInput, src, dest)); + } + + @Test + public void testCFB() throws Exception { + src.setEncryptionAlgorithm("AES/CFB/PKCS5Padding"); + src.start(Channel.SND_TX_SEQ); + dest.setEncryptionAlgorithm("AES/CFB/PKCS5Padding"); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Failed in CFB mode", + testInput, + roundTrip(testInput, src, dest)); + } + + @Test + public void testGCM() throws Exception { + src.setEncryptionAlgorithm("AES/GCM/NoPadding"); + src.start(Channel.SND_TX_SEQ); + dest.setEncryptionAlgorithm("AES/GCM/NoPadding"); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Failed in GCM mode", + testInput, + roundTrip(testInput, src, dest)); + } + + /* + * ECB mode isn't supported because it's insecure. + */ + @Test + public void testECB() throws Exception { + try { + src.setEncryptionAlgorithm("AES/ECB/PKCS5Padding"); + src.start(Channel.SND_TX_SEQ); + + // start() should trigger IllegalArgumentException + Assert.fail("ECB mode is not being refused"); + } catch (IllegalArgumentException iae) { + // Expected + } + } + + @Test + public void testViaFile() throws Exception { + src.start(Channel.SND_TX_SEQ); + src.setNext(new ValueCaptureInterceptor()); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + ChannelData msg = new ChannelData(false); + msg.setMessage(new XByteBuffer(testInput.getBytes("UTF-8"), false)); + src.sendMessage(null, msg, null); + + byte[] bytes = ((ValueCaptureInterceptor)src.getNext()).getValue(); + + try (FileOutputStream out = new FileOutputStream(MESSAGE_FILE)) { + out.write(bytes); + } + + dest.start(Channel.SND_TX_SEQ); + + bytes = new byte[8192]; + int read; + + try (FileInputStream in = new FileInputStream(MESSAGE_FILE)) { + read = in.read(bytes); + } + + msg = new ChannelData(false); + XByteBuffer xbb = new XByteBuffer(read, false); + xbb.append(bytes, 0, read); + msg.setMessage(xbb); + + dest.messageReceived(msg); + } + + @Test + public void testMessageUniqueness() throws Exception { + src.start(Channel.SND_TX_SEQ); + src.setNext(new ValueCaptureInterceptor()); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + ChannelData msg = new ChannelData(false); + msg.setMessage(new XByteBuffer(testInput.getBytes("UTF-8"), false)); + src.sendMessage(null, msg, null); + + byte[] cipherText1 = ((ValueCaptureInterceptor)src.getNext()).getValue(); + + msg.setMessage(new XByteBuffer(testInput.getBytes("UTF-8"), false)); + src.sendMessage(null, msg, null); + + byte[] cipherText2 = ((ValueCaptureInterceptor)src.getNext()).getValue(); + + MatcherAssert.assertThat("Two identical cleartexts encrypt to the same ciphertext", + cipherText1, IsNot.not(IsEqual.equalTo(cipherText2))); + } + + @Test + public void testPickup() throws Exception { + File file = new File(MESSAGE_FILE); + if(!file.exists()) { + System.err.println("File message.bin does not exist. Skipping test."); + return; + } + + dest.start(Channel.SND_TX_SEQ); + + byte[] bytes = new byte[8192]; + int read; + + try (FileInputStream in = new FileInputStream(file)) { + read = in.read(bytes); + } + + ChannelData msg = new ChannelData(false); + XByteBuffer xbb = new XByteBuffer(read, false); + xbb.append(bytes, 0, read); + msg.setMessage(xbb); + + dest.messageReceived(msg); + } + + /* + * This test isn't guaranteed to catch any multithreaded issues, but it + * gives a good exercise. + */ + @Test + public void testMultithreaded() throws Exception { + String inputValue = "A test string to fight over."; + final byte[] bytes = inputValue.getBytes("UTF-8"); + int numThreads = 100; + final int messagesPerThread = 10; + + dest.setPrevious(new ValuesCaptureInterceptor()); + + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + Runnable job = new Runnable() { + @Override + public void run() { + try { + ChannelData msg = new ChannelData(false); + XByteBuffer xbb = new XByteBuffer(1024, false); + xbb.append(bytes, 0, bytes.length); + msg.setMessage(xbb); + + for(int i=0; i messages = ((ValuesCaptureInterceptor)dest.getPrevious()).getValues(); + + Assert.assertEquals("Did not receive all expected messages", + numThreads * messagesPerThread, messages.size()); + + for(byte[] message : messages) { + Assert.assertArrayEquals("Message is corrupted", message, bytes); + } + } + + @Test + public void testTcpFailureDetectorDetection() { + src.setPrevious(new TcpFailureDetector()); + + try { + src.start(Channel.SND_TX_SEQ); + Assert.fail("EncryptInterceptor should detect TcpFailureDetector and throw an error"); + } catch (EncryptInterceptor.ChannelConfigException cce) { + // Expected behavior + } catch (AssertionError ae) { + // This is the junit assertion being thrown + throw ae; + } catch (Throwable t) { + Assert.fail("EncryptionInterceptor should throw ChannelConfigException, not " + t.getClass().getName()); + } + } +} diff --git a/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorLargeHeap.java b/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorLargeHeap.java new file mode 100644 index 0000000..81d0206 --- /dev/null +++ b/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorLargeHeap.java @@ -0,0 +1,47 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import org.junit.Assert; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import org.apache.catalina.tribes.Channel; + +/** + * Tests the EncryptInterceptor using very large inputs. + * + * Many of the tests in this class use strings as input and output, even + * though the interceptor actually operates on byte arrays. This is done + * for readability for the tests and their outputs. + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TestEncryptInterceptorLargeHeap extends EncryptionInterceptorBaseTest { + + @Test + public void testHugePayload() throws Exception { + src.start(Channel.SND_TX_SEQ); + dest.start(Channel.SND_TX_SEQ); + + byte[] bytes = new byte[1024*1024*1024]; + + Assert.assertArrayEquals("Huge payload roundtrip failed", + bytes, + roundTrip(bytes, src, dest)); + } +} diff --git a/test/org/apache/catalina/tribes/group/interceptors/TestGzipInterceptor.java b/test/org/apache/catalina/tribes/group/interceptors/TestGzipInterceptor.java new file mode 100644 index 0000000..dbcea24 --- /dev/null +++ b/test/org/apache/catalina/tribes/group/interceptors/TestGzipInterceptor.java @@ -0,0 +1,58 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TestGzipInterceptor { + + @Parameters(name = "{index}: bufferSize[{0}]") + public static Collection inputs() { + List result = new ArrayList<>(); + result.add(new Object[] { Integer.valueOf(GzipInterceptor.DEFAULT_BUFFER_SIZE / 2) }); + result.add(new Object[] { Integer.valueOf(GzipInterceptor.DEFAULT_BUFFER_SIZE - 1) }); + result.add(new Object[] { Integer.valueOf(GzipInterceptor.DEFAULT_BUFFER_SIZE) }); + result.add(new Object[] { Integer.valueOf(GzipInterceptor.DEFAULT_BUFFER_SIZE + 1) }); + result.add(new Object[] { Integer.valueOf(GzipInterceptor.DEFAULT_BUFFER_SIZE * 2) }); + result.add(new Object[] { Integer.valueOf(GzipInterceptor.DEFAULT_BUFFER_SIZE * 4) }); + result.add(new Object[] { Integer.valueOf(GzipInterceptor.DEFAULT_BUFFER_SIZE * 10 + 1000) }); + return result; + } + + @Parameter(0) + public int bufferSize; + + @Test + public void testCompressDecompress() throws Exception { + byte[] data = new byte[bufferSize]; + Arrays.fill(data, (byte)1); + byte[] compress = GzipInterceptor.compress(data); + byte[] result = GzipInterceptor.decompress(compress); + Assert.assertTrue(Arrays.equals(data, result)); + } +} diff --git a/test/org/apache/catalina/tribes/group/interceptors/TestNonBlockingCoordinator.java b/test/org/apache/catalina/tribes/group/interceptors/TestNonBlockingCoordinator.java new file mode 100644 index 0000000..1ad80db --- /dev/null +++ b/test/org/apache/catalina/tribes/group/interceptors/TestNonBlockingCoordinator.java @@ -0,0 +1,185 @@ +/* + * 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.catalina.tribes.group.interceptors; + +import java.util.logging.Level; +import java.util.logging.LogManager; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.TesterUtil; +import org.apache.catalina.tribes.group.GroupChannel; +import org.apache.catalina.tribes.transport.ReceiverBase; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class TestNonBlockingCoordinator { + + private static final Log log = LogFactory.getLog(TestNonBlockingCoordinator.class); + + private static final int CHANNEL_COUNT = 10; + + private GroupChannel[] channels = null; + private NonBlockingCoordinator[] coordinators = null; + + @Before + public void setUp() throws Exception { + LogManager.getLogManager().getLogger( + "org.apache.catalina.tribes.group.interceptors.TestNonBlockingCoordinator").setLevel(Level.ALL); + try { + log.info("Setup"); + channels = new GroupChannel[CHANNEL_COUNT]; + coordinators = new NonBlockingCoordinator[CHANNEL_COUNT]; + Thread[] threads = new Thread[CHANNEL_COUNT]; + for ( int i=0; i 60000) { + Assert.fail("Cluster took more than 60s to start"); + } + Thread.sleep(50); + m = channels[i].getMembers(); + } + } + } + + @Test + public void testOrder1() throws Exception { + Member[] dest = channels[0].getMembers(); + final AtomicInteger value = new AtomicInteger(0); + for ( int i=0; i<100; i++ ) { + channels[0].send(dest,Integer.valueOf(value.getAndAdd(1)),0); + } + Thread.sleep(5000); + for (TestListener testListener : test) { + Assert.assertFalse(testListener.fail); + } + } + + @Test + public void testOrder2() throws Exception { + final Member[] dest = channels[0].getMembers(); + final AtomicInteger value = new AtomicInteger(0); + final Queue exceptionQueue = new ConcurrentLinkedQueue<>(); + Runnable run = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 100; i++) { + try { + synchronized (channels[0]) { + channels[0].send(dest, Integer.valueOf(value.getAndAdd(1)), 0); + } + }catch ( Exception x ) { + exceptionQueue.add(x); + } + } + } + }; + Thread[] threads = new Thread[5]; + for (int i=0;i 0) { + Assert.assertEquals("Expecting member count to not be equal",mbrlist1.members.size()+1,mbrlist2.members.size()); + count--; + } + channel1.stop(Channel.DEFAULT); + channel2.stop(Channel.DEFAULT); + } + + @Test + public void testTcpMcastFail() throws Exception { + System.out.println("testTcpMcastFail()"); + clear(); + channel1.start(Channel.DEFAULT); + channel2.start(Channel.DEFAULT); + //Thread.sleep(1000); + Assert.assertEquals("Expecting member count to be equal",mbrlist1.members.size(),mbrlist2.members.size()); + channel2.stop(Channel.MBR_TX_SEQ); + ByteMessage msg = new ByteMessage(new byte[1024]); + try { + Thread.sleep(5000); + Assert.assertEquals("Expecting member count to be equal",mbrlist1.members.size(),mbrlist2.members.size()); + channel1.send(channel1.getMembers(), msg, 0); + } catch ( ChannelException x ) { + Assert.fail("Message send should have succeeded."); + } + channel1.stop(Channel.DEFAULT); + channel2.stop(Channel.DEFAULT); + } + + @After + public void tearDown() throws Exception { + tcpFailureDetector1 = null; + tcpFailureDetector2 = null; + try { + channel1.stop(Channel.DEFAULT); + } catch (Exception ignore) { + // Ignore + } + channel1 = null; + try { + channel2.stop(Channel.DEFAULT); + } catch (Exception ignore) { + // Ignore + } + channel2 = null; + } + + public static class TestMbrListener implements MembershipListener { + public String name = null; + public TestMbrListener(String name) { + this.name = name; + } + public ArrayList members = new ArrayList<>(); + @Override + public void memberAdded(Member member) { + if ( !members.contains(member) ) { + members.add(member); + try{ + System.out.println(name + ":member added[" + new String(member.getPayload(), "ASCII") + "]"); + }catch ( Exception x ) { + System.out.println(name + ":member added[unknown]"); + } + } + } + + @Override + public void memberDisappeared(Member member) { + if ( members.contains(member) ) { + members.remove(member); + try{ + System.out.println(name + ":member disappeared[" + new String(member.getPayload(), "ASCII") + "]"); + }catch ( Exception x ) { + System.out.println(name + ":member disappeared[unknown]"); + } + } + } + + } + +} diff --git a/test/org/apache/catalina/tribes/io/TestChannelData.java b/test/org/apache/catalina/tribes/io/TestChannelData.java new file mode 100644 index 0000000..9f6a396 --- /dev/null +++ b/test/org/apache/catalina/tribes/io/TestChannelData.java @@ -0,0 +1,33 @@ +/* + * 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.catalina.tribes.io; + +import org.junit.Assert; +import org.junit.Test; + +public class TestChannelData { + + @Test + public void testClone() { + ChannelData original = new ChannelData(); + ChannelData clone = original.clone(); + + Assert.assertFalse(original == clone); + Assert.assertTrue(original.getClass() == clone.getClass()); + Assert.assertTrue(original.equals(clone)); + } +} diff --git a/test/org/apache/catalina/tribes/io/TestXByteBuffer.java b/test/org/apache/catalina/tribes/io/TestXByteBuffer.java new file mode 100644 index 0000000..cf7ff86 --- /dev/null +++ b/test/org/apache/catalina/tribes/io/TestXByteBuffer.java @@ -0,0 +1,41 @@ +/* + * 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.catalina.tribes.io; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import org.junit.Assert; +import org.junit.Test; + +public class TestXByteBuffer { + + @Test + public void testEmptyArray() throws Exception { + Object obj = XByteBuffer.deserialize(new byte[0]); + Assert.assertNull(obj); + } + + @Test + public void testSerializationString() throws Exception { + String test = "This is as test."; + byte[] msg = XByteBuffer.serialize(test); + Object obj = XByteBuffer.deserialize(msg); + assertThat(obj, instanceOf(String.class)); + Assert.assertEquals(test, obj); + } +} diff --git a/test/org/apache/catalina/tribes/membership/TestMemberImplSerialization.java b/test/org/apache/catalina/tribes/membership/TestMemberImplSerialization.java new file mode 100644 index 0000000..d0a05c5 --- /dev/null +++ b/test/org/apache/catalina/tribes/membership/TestMemberImplSerialization.java @@ -0,0 +1,107 @@ +/* + * 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.catalina.tribes.membership; + +import java.util.Arrays; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.tribes.Member; + +public class TestMemberImplSerialization { + private MemberImpl m1, m2, p1,p2; + private byte[] payload = null; + private int udpPort = 3445; + + @Before + public void setUp() throws Exception { + payload = new byte[333]; + Arrays.fill(payload,(byte)1); + m1 = new MemberImpl("localhost",3333,1,payload); + m2 = new MemberImpl("localhost",3333,1); + payload = new byte[333]; + Arrays.fill(payload,(byte)2); + p1 = new MemberImpl("127.0.0.1",3333,1,payload); + p2 = new MemberImpl("localhost",3331,1,payload); + m1.setDomain(new byte[] {1,2,3,4,5,6,7,8,9}); + m2.setDomain(new byte[] {1,2,3,4,5,6,7,8,9}); + m1.setCommand(new byte[] {1,2,4,5,6,7,8,9}); + m2.setCommand(new byte[] {1,2,4,5,6,7,8,9}); + m1.setUdpPort(udpPort); + m2.setUdpPort(m1.getUdpPort()); + } + + @Test + public void testCompare() throws Exception { + Assert.assertTrue(m1.equals(m2)); + Assert.assertTrue(m2.equals(m1)); + Assert.assertTrue(p1.equals(m2)); + Assert.assertFalse(m1.equals(p2)); + Assert.assertFalse(m1.equals(p2)); + Assert.assertFalse(m2.equals(p2)); + Assert.assertFalse(p1.equals(p2)); + } + + @Test + public void testUdpPort() throws Exception { + byte[] md1 = m1.getData(); + byte[] md2 = m2.getData(); + + Member a1 = MemberImpl.getMember(md1); + Member a2 = MemberImpl.getMember(md2); + + Assert.assertTrue(a1.getUdpPort()==a2.getUdpPort()); + Assert.assertTrue(a1.getUdpPort()==udpPort); + } + + @Test + public void testSerializationOne() throws Exception { + Member m = m1; + byte[] md1 = m.getData(false,true); + byte[] mda1 = m.getData(false,false); + Assert.assertTrue(Arrays.equals(md1,mda1)); + Assert.assertTrue(md1==mda1); + mda1 = m.getData(true,true); + Member ma1 = MemberImpl.getMember(mda1); + Assert.assertTrue(compareMembers(m,ma1)); + mda1 = p1.getData(false); + Assert.assertFalse(Arrays.equals(md1,mda1)); + ma1 = MemberImpl.getMember(mda1); + Assert.assertTrue(compareMembers(p1,ma1)); + + md1 = m.getData(true,true); + Thread.sleep(50); + mda1 = m.getData(true,true); + Member a1 = MemberImpl.getMember(md1); + Member a2 = MemberImpl.getMember(mda1); + Assert.assertTrue(a1.equals(a2)); + Assert.assertFalse(Arrays.equals(md1,mda1)); + + + } + + public boolean compareMembers(Member impl1, Member impl2) { + boolean result = true; + result = result && Arrays.equals(impl1.getHost(),impl2.getHost()); + result = result && Arrays.equals(impl1.getPayload(),impl2.getPayload()); + result = result && Arrays.equals(impl1.getUniqueId(),impl2.getUniqueId()); + result = result && impl1.getPort() == impl2.getPort(); + return result; + } +} diff --git a/test/org/apache/catalina/tribes/membership/TestMembership.java b/test/org/apache/catalina/tribes/membership/TestMembership.java new file mode 100644 index 0000000..687740c --- /dev/null +++ b/test/org/apache/catalina/tribes/membership/TestMembership.java @@ -0,0 +1,68 @@ +/* + * 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.catalina.tribes.membership; + +import java.lang.reflect.Field; +import java.util.Iterator; +import java.util.Map.Entry; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.membership.Membership.MbrEntry; + +public class TestMembership { + + @Test + public void testClone() throws Exception { + Member m1 = new MemberImpl("localhost", 1, 1000); + Member m2 = new MemberImpl("localhost", 2, 1000); + Member m3 = new MemberImpl("localhost", 3, 1000); + + Membership original = new Membership(m1); + original.addMember(m2); + original.addMember(m3); + + Membership clone = original.clone(); + + Assert.assertFalse(original == clone); + Assert.assertTrue(original.getClass() == clone.getClass()); + + Assert.assertTrue(original.local == clone.local); + + Assert.assertFalse(original.map == clone.map); + Assert.assertTrue(original.map.size() == clone.map.size()); + Iterator> cloneEntries = clone.map.entrySet().iterator(); + for (Entry originalEntry : original.map.entrySet()) { + Entry cloneEntry = cloneEntries.next(); + Assert.assertTrue(originalEntry.getKey() == cloneEntry.getKey()); + Assert.assertTrue(originalEntry.getValue() == cloneEntry.getValue()); + } + + Assert.assertTrue(original.memberComparator == clone.memberComparator); + Assert.assertFalse(original.members == clone.members); + Assert.assertArrayEquals(original.members, clone.members); + + // Need to use reflection to access lock since it is a private field + Field f = Membership.class.getDeclaredField("membersLock"); + f.setAccessible(true); + Object originalLock = f.get(original); + Object cloneLock = f.get(clone); + Assert.assertFalse(originalLock == cloneLock); + } +} diff --git a/test/org/apache/catalina/tribes/membership/cloud/TestKubernetesJson.java b/test/org/apache/catalina/tribes/membership/cloud/TestKubernetesJson.java new file mode 100644 index 0000000..46bb55c --- /dev/null +++ b/test/org/apache/catalina/tribes/membership/cloud/TestKubernetesJson.java @@ -0,0 +1,286 @@ +/* + * 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.catalina.tribes.membership.cloud; + +import java.io.StringReader; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.tribes.membership.MemberImpl; + +public class TestKubernetesJson extends KubernetesMembershipProvider { + + private static final String OPENSHIFT_JSON_POD_LIST = "{\n" + + " \"kind\": \"PodList\",\n" + " \"apiVersion\": \"v1\",\n" + + " \"metadata\": {\n" + + " \"selfLink\": \"/api/v1/namespaces/tomcat-in-the-cloud/pods\",\n" + + " \"resourceVersion\": \"1120453180\"\n" + " },\n" + + " \"items\": [\n" + " {\n" + " \"metadata\": {\n" + + " \"name\": \"tomcat-in-the-cloud-5bc6dc7cf8-ndpdm\",\n" + + " \"generateName\": \"tomcat-in-the-cloud-5bc6dc7cf8-\",\n" + + " \"namespace\": \"tomcat-in-the-cloud\",\n" + + " \"selfLink\": \"/api/v1/namespaces/tomcat-in-the-cloud/pods/tomcat-in-the-cloud-5bc6dc7cf8-ndpdm\",\n" + + " \"uid\": \"29cc7dec-cc8d-11e8-943e-02ec8e61afcf\",\n" + + " \"resourceVersion\": \"1120373053\",\n" + + " \"creationTimestamp\": \"2018-10-10T13:05:29Z\",\n" + + " \"labels\": {\n" + + " \"pod-template-hash\": \"1672873794\",\n" + + " \"run\": \"tomcat-in-the-cloud\"\n" + " },\n" + + " \"annotations\": {\n" + + " \"kubernetes.io/limit-ranger\": \"LimitRanger plugin set: cpu, memory request for container tomcat-in-the-cloud; cpu, memory limit for container tomcat-in-the-cloud\",\n" + + " \"openshift.io/scc\": \"restricted\"\n" + + " },\n" + " \"ownerReferences\": [\n" + + " {\n" + " \"apiVersion\": \"apps/v1\",\n" + + " \"kind\": \"ReplicaSet\",\n" + + " \"name\": \"tomcat-in-the-cloud-5bc6dc7cf8\",\n" + + " \"uid\": \"12c4fa68-cc8d-11e8-943e-02ec8e61afcf\",\n" + + " \"controller\": true,\n" + + " \"blockOwnerDeletion\": true\n" + " }\n" + + " ]\n" + " },\n" + " \"spec\": {\n" + + " \"volumes\": [\n" + " {\n" + + " \"name\": \"default-token-n87wf\",\n" + + " \"secret\": {\n" + + " \"secretName\": \"default-token-n87wf\",\n" + + " \"defaultMode\": 420\n" + " }\n" + + " }\n" + " ],\n" + " \"containers\": [\n" + + " {\n" + + " \"name\": \"tomcat-in-the-cloud\",\n" + + " \"image\": \"docker.io/jfclere/test\",\n" + + " \"ports\": [\n" + " {\n" + + " \"containerPort\": 8080,\n" + + " \"protocol\": \"TCP\"\n" + " }\n" + + " ],\n" + " \"resources\": {\n" + + " \"limits\": {\n" + + " \"cpu\": \"1\",\n" + + " \"memory\": \"512Mi\"\n" + " },\n" + + " \"requests\": {\n" + + " \"cpu\": \"20m\",\n" + + " \"memory\": \"256Mi\"\n" + " }\n" + + " },\n" + " \"volumeMounts\": [\n" + + " {\n" + + " \"name\": \"default-token-n87wf\",\n" + + " \"readOnly\": true,\n" + + " \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\"\n" + + " }\n" + " ],\n" + + " \"terminationMessagePath\": \"/dev/termination-log\",\n" + + " \"terminationMessagePolicy\": \"File\",\n" + + " \"imagePullPolicy\": \"Always\",\n" + + " \"securityContext\": {\n" + + " \"capabilities\": {\n" + + " \"drop\": [\n" + " \"KILL\",\n" + + " \"MKNOD\",\n" + + " \"NET_RAW\",\n" + + " \"SETGID\",\n" + + " \"SETUID\"\n" + " ]\n" + + " },\n" + " \"runAsUser\": 1119270000\n" + + " }\n" + " }\n" + " ],\n" + + " \"restartPolicy\": \"Always\",\n" + + " \"terminationGracePeriodSeconds\": 30,\n" + + " \"dnsPolicy\": \"ClusterFirst\",\n" + + " \"nodeSelector\": {\n" + + " \"type\": \"compute\"\n" + " },\n" + + " \"serviceAccountName\": \"default\",\n" + + " \"serviceAccount\": \"default\",\n" + + " \"nodeName\": \"ip-172-31-18-231.ca-central-1.compute.internal\",\n" + + " \"securityContext\": {\n" + + " \"seLinuxOptions\": {\n" + + " \"level\": \"s0:c345,c295\"\n" + " },\n" + + " \"fsGroup\": 1119270000\n" + " },\n" + + " \"imagePullSecrets\": [\n" + " {\n" + + " \"name\": \"default-dockercfg-lx7wg\"\n" + + " }\n" + " ],\n" + + " \"schedulerName\": \"default-scheduler\",\n" + + " \"tolerations\": [\n" + " {\n" + + " \"key\": \"node.kubernetes.io/memory-pressure\",\n" + + " \"operator\": \"Exists\",\n" + + " \"effect\": \"NoSchedule\"\n" + " }\n" + + " ],\n" + " \"priority\": 0\n" + " },\n" + + " \"status\": {\n" + " \"phase\": \"Running\",\n" + + " \"conditions\": [\n" + " {\n" + + " \"type\": \"Initialized\",\n" + + " \"status\": \"True\",\n" + + " \"lastProbeTime\": null,\n" + + " \"lastTransitionTime\": \"2018-10-10T13:05:30Z\"\n" + + " },\n" + " {\n" + + " \"type\": \"Ready\",\n" + + " \"status\": \"True\",\n" + + " \"lastProbeTime\": null,\n" + + " \"lastTransitionTime\": \"2018-10-10T13:06:30Z\"\n" + + " },\n" + " {\n" + + " \"type\": \"ContainersReady\",\n" + + " \"status\": \"True\",\n" + + " \"lastProbeTime\": null,\n" + + " \"lastTransitionTime\": null\n" + " },\n" + + " {\n" + " \"type\": \"PodScheduled\",\n" + + " \"status\": \"True\",\n" + + " \"lastProbeTime\": null,\n" + + " \"lastTransitionTime\": \"2018-10-10T13:05:30Z\"\n" + + " }\n" + " ],\n" + + " \"hostIP\": \"172.31.18.231\",\n" + + " \"podIP\": \"10.130.140.153\",\n" + + " \"startTime\": \"2018-10-10T13:05:30Z\",\n" + + " \"containerStatuses\": [\n" + " {\n" + + " \"name\": \"tomcat-in-the-cloud\",\n" + + " \"state\": {\n" + " \"running\": {\n" + + " \"startedAt\": \"2018-10-10T13:06:29Z\"\n" + + " }\n" + " },\n" + + " \"lastState\": {\n" + " \n" + + " },\n" + " \"ready\": true,\n" + + " \"restartCount\": 0,\n" + + " \"image\": \"docker.io/jfclere/test:latest\",\n" + + " \"imageID\": \"docker.io/jfclere/test@sha256:d36155492516915ecf448fe62769f61b5f9ab795bc8ae1e72a199ee6485cbea2\",\n" + + " \"containerID\": \"cri-o://a173e05e66730efd5e5df6e9b2681b5c37de44b81cc22f2455077b3008a5284c\"\n" + + " }\n" + " ],\n" + + " \"qosClass\": \"Burstable\"\n" + " }\n" + " },\n" + + " {\n" + " \"metadata\": {\n" + + " \"name\": \"tomcat-in-the-cloud-5bc6dc7cf8-w454k\",\n" + + " \"generateName\": \"tomcat-in-the-cloud-5bc6dc7cf8-\",\n" + + " \"namespace\": \"tomcat-in-the-cloud\",\n" + + " \"selfLink\": \"/api/v1/namespaces/tomcat-in-the-cloud/pods/tomcat-in-the-cloud-5bc6dc7cf8-w454k\",\n" + + " \"uid\": \"12cd7ae2-cc8d-11e8-943e-02ec8e61afcf\",\n" + + " \"resourceVersion\": \"1120369623\",\n" + + " \"creationTimestamp\": \"2018-10-10T13:04:51Z\",\n" + + " \"labels\": {\n" + + " \"pod-template-hash\": \"1672873794\",\n" + + " \"run\": \"tomcat-in-the-cloud\"\n" + " },\n" + + " \"annotations\": {\n" + + " \"kubernetes.io/limit-ranger\": \"LimitRanger plugin set: cpu, memory request for container tomcat-in-the-cloud; cpu, memory limit for container tomcat-in-the-cloud\",\n" + + " \"openshift.io/scc\": \"restricted\"\n" + + " },\n" + " \"ownerReferences\": [\n" + + " {\n" + " \"apiVersion\": \"apps/v1\",\n" + + " \"kind\": \"ReplicaSet\",\n" + + " \"name\": \"tomcat-in-the-cloud-5bc6dc7cf8\",\n" + + " \"uid\": \"12c4fa68-cc8d-11e8-943e-02ec8e61afcf\",\n" + + " \"controller\": true,\n" + + " \"blockOwnerDeletion\": true\n" + " }\n" + + " ]\n" + " },\n" + " \"spec\": {\n" + + " \"volumes\": [\n" + " {\n" + + " \"name\": \"default-token-n87wf\",\n" + + " \"secret\": {\n" + + " \"secretName\": \"default-token-n87wf\",\n" + + " \"defaultMode\": 420\n" + " }\n" + + " }\n" + " ],\n" + " \"containers\": [\n" + + " {\n" + + " \"name\": \"tomcat-in-the-cloud\",\n" + + " \"image\": \"docker.io/jfclere/test\",\n" + + " \"ports\": [\n" + " {\n" + + " \"containerPort\": 8080,\n" + + " \"protocol\": \"TCP\"\n" + " }\n" + + " ],\n" + " \"resources\": {\n" + + " \"limits\": {\n" + + " \"cpu\": \"1\",\n" + + " \"memory\": \"512Mi\"\n" + " },\n" + + " \"requests\": {\n" + + " \"cpu\": \"20m\",\n" + + " \"memory\": \"256Mi\"\n" + " }\n" + + " },\n" + " \"volumeMounts\": [\n" + + " {\n" + + " \"name\": \"default-token-n87wf\",\n" + + " \"readOnly\": true,\n" + + " \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\"\n" + + " }\n" + " ],\n" + + " \"terminationMessagePath\": \"/dev/termination-log\",\n" + + " \"terminationMessagePolicy\": \"File\",\n" + + " \"imagePullPolicy\": \"Always\",\n" + + " \"securityContext\": {\n" + + " \"capabilities\": {\n" + + " \"drop\": [\n" + " \"KILL\",\n" + + " \"MKNOD\",\n" + + " \"NET_RAW\",\n" + + " \"SETGID\",\n" + + " \"SETUID\"\n" + " ]\n" + + " },\n" + " \"runAsUser\": 1119270000\n" + + " }\n" + " }\n" + " ],\n" + + " \"restartPolicy\": \"Always\",\n" + + " \"terminationGracePeriodSeconds\": 30,\n" + + " \"dnsPolicy\": \"ClusterFirst\",\n" + + " \"nodeSelector\": {\n" + + " \"type\": \"compute\"\n" + " },\n" + + " \"serviceAccountName\": \"default\",\n" + + " \"serviceAccount\": \"default\",\n" + + " \"nodeName\": \"ip-172-31-23-191.ca-central-1.compute.internal\",\n" + + " \"securityContext\": {\n" + + " \"seLinuxOptions\": {\n" + + " \"level\": \"s0:c345,c295\"\n" + " },\n" + + " \"fsGroup\": 1119270000\n" + " },\n" + + " \"imagePullSecrets\": [\n" + " {\n" + + " \"name\": \"default-dockercfg-lx7wg\"\n" + + " }\n" + " ],\n" + + " \"schedulerName\": \"default-scheduler\",\n" + + " \"tolerations\": [\n" + " {\n" + + " \"key\": \"node.kubernetes.io/memory-pressure\",\n" + + " \"operator\": \"Exists\",\n" + + " \"effect\": \"NoSchedule\"\n" + " }\n" + + " ],\n" + " \"priority\": 0\n" + " },\n" + + " \"status\": {\n" + " \"phase\": \"Running\",\n" + + " \"conditions\": [\n" + " {\n" + + " \"type\": \"Initialized\",\n" + + " \"status\": \"True\",\n" + + " \"lastProbeTime\": null,\n" + + " \"lastTransitionTime\": \"2018-10-10T13:04:51Z\"\n" + + " },\n" + " {\n" + + " \"type\": \"Ready\",\n" + + " \"status\": \"True\",\n" + + " \"lastProbeTime\": null,\n" + + " \"lastTransitionTime\": \"2018-10-10T13:05:15Z\"\n" + + " },\n" + " {\n" + + " \"type\": \"ContainersReady\",\n" + + " \"status\": \"True\",\n" + + " \"lastProbeTime\": null,\n" + + " \"lastTransitionTime\": null\n" + " },\n" + + " {\n" + " \"type\": \"PodScheduled\",\n" + + " \"status\": \"True\",\n" + + " \"lastProbeTime\": null,\n" + + " \"lastTransitionTime\": \"2018-10-10T13:04:51Z\"\n" + + " }\n" + " ],\n" + + " \"hostIP\": \"172.31.23.191\",\n" + + " \"podIP\": \"10.129.123.39\",\n" + + " \"startTime\": \"2018-10-10T13:04:51Z\",\n" + + " \"containerStatuses\": [\n" + " {\n" + + " \"name\": \"tomcat-in-the-cloud\",\n" + + " \"state\": {\n" + " \"running\": {\n" + + " \"startedAt\": \"2018-10-10T13:05:14Z\"\n" + + " }\n" + " },\n" + + " \"lastState\": {\n" + " \n" + + " },\n" + " \"ready\": true,\n" + + " \"restartCount\": 0,\n" + + " \"image\": \"docker.io/jfclere/test:latest\",\n" + + " \"imageID\": \"docker.io/jfclere/test@sha256:d36155492516915ecf448fe62769f61b5f9ab795bc8ae1e72a199ee6485cbea2\",\n" + + " \"containerID\": \"cri-o://401533326c8224ef5a3f40234dd26c1030925d659757e0b26c09ff1e728c78f1\"\n" + + " }\n" + " ],\n" + + " \"qosClass\": \"Burstable\"\n" + " }\n" + " }\n" + + " ]\n" + "}"; + + @Test + public void testJson() throws Exception { + startTime = Instant.now(); + + List members = new ArrayList<>(); + parsePods(new StringReader(OPENSHIFT_JSON_POD_LIST), members); + + Assert.assertTrue(members.size() == 2); + Assert.assertTrue("10.130.140.153".equals(members.get(0).getHostname())); + Assert.assertTrue( + "tcp://10.130.140.153:0".equals(members.get(0).getName())); + + members.clear(); + } +} diff --git a/test/org/apache/catalina/tribes/test/NioSenderTest.java b/test/org/apache/catalina/tribes/test/NioSenderTest.java new file mode 100644 index 0000000..43c7112 --- /dev/null +++ b/test/org/apache/catalina/tribes/test/NioSenderTest.java @@ -0,0 +1,110 @@ +/* + * 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.catalina.tribes.test; + +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.Iterator; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.transport.nio.NioSender; + +public class NioSenderTest { + private Selector selector = null; + private int counter = 0; + Member mbr; + private static int testOptions = Channel.SEND_OPTIONS_DEFAULT; + public NioSenderTest() { + // Default constructor + } + + public synchronized int inc() { + return ++counter; + } + + public synchronized ChannelData getMessage(Member mbr) { + String msg = "Thread-"+Thread.currentThread().getName()+" Message:"+inc(); + ChannelData data = new ChannelData(true); + data.setMessage(new XByteBuffer(msg.getBytes(),false)); + data.setAddress(mbr); + + return data; + } + + public void init() throws Exception { + selector = Selector.open(); + mbr = new MemberImpl("localhost",4444,0); + NioSender sender = new NioSender(); + sender.setDestination(mbr); + sender.setDirectBuffer(true); + sender.setSelector(selector); + sender.setMessage(XByteBuffer.createDataPackage(getMessage(mbr))); + sender.connect(); + } + + public void run() { + while (true) { + + int selectedKeys = 0; + try { + selectedKeys = selector.select(100); + // if ( selectedKeys == 0 ) { + // System.out.println("No registered interests. Sleeping for a second."); + // Thread.sleep(100); + } catch (Exception e) { + e.printStackTrace(); + continue; + } + + if (selectedKeys == 0) { + continue; + } + + Iterator it = selector.selectedKeys().iterator(); + while (it.hasNext()) { + SelectionKey sk = it.next(); + it.remove(); + try { + int readyOps = sk.readyOps(); + sk.interestOps(sk.interestOps() & ~readyOps); + NioSender sender = (NioSender) sk.attachment(); + if ( sender.process(sk, (testOptions&Channel.SEND_OPTIONS_USE_ACK)==Channel.SEND_OPTIONS_USE_ACK) ) { + System.out.println("Message completed for handler:"+sender); + Thread.sleep(2000); + sender.reset(); + sender.setMessage(XByteBuffer.createDataPackage(getMessage(mbr))); + } + + + } catch (Throwable t) { + t.printStackTrace(); + return; + } + } + } + } + + public static void main(String[] args) throws Exception { + NioSenderTest sender = new NioSenderTest(); + sender.init(); + sender.run(); + } +} diff --git a/test/org/apache/catalina/tribes/test/TribesTestSuite.java b/test/org/apache/catalina/tribes/test/TribesTestSuite.java new file mode 100644 index 0000000..a626300 --- /dev/null +++ b/test/org/apache/catalina/tribes/test/TribesTestSuite.java @@ -0,0 +1,53 @@ +/* + * 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.catalina.tribes.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +import org.apache.catalina.tribes.group.TestGroupChannelMemberArrival; +import org.apache.catalina.tribes.group.TestGroupChannelOptionFlag; +import org.apache.catalina.tribes.group.TestGroupChannelSenderConnections; +import org.apache.catalina.tribes.group.TestGroupChannelStartStop; +import org.apache.catalina.tribes.group.interceptors.TestDomainFilterInterceptor; +import org.apache.catalina.tribes.group.interceptors.TestNonBlockingCoordinator; +import org.apache.catalina.tribes.group.interceptors.TestOrderInterceptor; +import org.apache.catalina.tribes.group.interceptors.TestTcpFailureDetector; +import org.apache.catalina.tribes.io.TestXByteBuffer; +import org.apache.catalina.tribes.membership.TestMemberImplSerialization; +import org.apache.catalina.tribes.test.channel.TestDataIntegrity; +import org.apache.catalina.tribes.test.channel.TestMulticastPackages; +import org.apache.catalina.tribes.test.channel.TestRemoteProcessException; +import org.apache.catalina.tribes.test.channel.TestUdpPackages; + +@RunWith(Suite.class) +@SuiteClasses({ + // o.a.catalina.tribes.test.channel + TestGroupChannelStartStop.class, TestGroupChannelOptionFlag.class, + TestDataIntegrity.class, TestMulticastPackages.class, + TestRemoteProcessException.class, TestUdpPackages.class, + // o.a.catalina.tribes.test.interceptors + TestNonBlockingCoordinator.class, TestOrderInterceptor.class, + // o.a.catalina.tribes.test.io + TestGroupChannelSenderConnections.class, TestXByteBuffer.class, + // o.a.catalina.tribes.test.membership + TestMemberImplSerialization.class, TestDomainFilterInterceptor.class, + TestGroupChannelMemberArrival.class, TestTcpFailureDetector.class }) +public class TribesTestSuite { + +} diff --git a/test/org/apache/catalina/tribes/test/channel/TestChannelConfig.java b/test/org/apache/catalina/tribes/test/channel/TestChannelConfig.java new file mode 100644 index 0000000..32f17d3 --- /dev/null +++ b/test/org/apache/catalina/tribes/test/channel/TestChannelConfig.java @@ -0,0 +1,70 @@ +/* + * 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.catalina.tribes.test.channel; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.ha.session.BackupManager; +import org.apache.catalina.ha.tcp.SimpleTcpCluster; +import org.apache.catalina.tribes.Channel; + + +public class TestChannelConfig { + + @Test + public void testIntInput() { + + SimpleTcpCluster cluster = new SimpleTcpCluster(); + cluster.setChannelSendOptions(Channel.SEND_OPTIONS_ASYNCHRONOUS | Channel.SEND_OPTIONS_MULTICAST); + Assert.assertEquals(Channel.SEND_OPTIONS_ASYNCHRONOUS | Channel.SEND_OPTIONS_MULTICAST, cluster.getChannelSendOptions()); + } + + @Test + public void testStringInputSimple() { + + SimpleTcpCluster cluster = new SimpleTcpCluster(); + cluster.setChannelSendOptions("multicast"); + Assert.assertEquals(Channel.SEND_OPTIONS_MULTICAST, cluster.getChannelSendOptions()); + } + + @Test + public void testStringInputCompound() { + + SimpleTcpCluster cluster = new SimpleTcpCluster(); + cluster.setChannelSendOptions("async, multicast"); + Assert.assertEquals(Channel.SEND_OPTIONS_ASYNCHRONOUS | Channel.SEND_OPTIONS_MULTICAST, cluster.getChannelSendOptions()); + } + + @Test + public void testStringRepresentationOfIntValue() { + + String options = "multicast, async"; + SimpleTcpCluster cluster = new SimpleTcpCluster(); + cluster.setChannelSendOptions(options); + Assert.assertEquals(options, cluster.getChannelSendOptionsName()); + } + + @Test + public void testStringInputForMapSendOptions() { + + BackupManager manager = new BackupManager(); + manager.setMapSendOptions("async, multicast"); + Assert.assertEquals(Channel.SEND_OPTIONS_ASYNCHRONOUS | Channel.SEND_OPTIONS_MULTICAST, manager.getMapSendOptions()); + } + +} \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/test/channel/TestDataIntegrity.java b/test/org/apache/catalina/tribes/test/channel/TestDataIntegrity.java new file mode 100644 index 0000000..3da336f --- /dev/null +++ b/test/org/apache/catalina/tribes/test/channel/TestDataIntegrity.java @@ -0,0 +1,219 @@ +/* + * 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.catalina.tribes.test.channel; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Random; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.TesterUtil; +import org.apache.catalina.tribes.group.GroupChannel; +import org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor; + +public class TestDataIntegrity { + private int msgCount = 500; + private int threadCount = 20; + private GroupChannel channel1; + private GroupChannel channel2; + private Listener listener1; + + @Before + public void setUp() throws Exception { + channel1 = new GroupChannel(); + channel1.addInterceptor(new MessageDispatchInterceptor()); + channel2 = new GroupChannel(); + channel2.addInterceptor(new MessageDispatchInterceptor()); + listener1 = new Listener(); + channel2.addChannelListener(listener1); + TesterUtil.addRandomDomain(new ManagedChannel[] {channel1, channel2}); + channel1.start(Channel.DEFAULT); + channel2.start(Channel.DEFAULT); + } + + @After + public void tearDown() throws Exception { + channel1.stop(Channel.DEFAULT); + channel2.stop(Channel.DEFAULT); + } + + @Test + public void testDataSendNO_ACK() throws Exception { + System.err.println("Starting NO_ACK"); + Thread[] threads = new Thread[threadCount]; + for (int x=0; x=0 && nr0 && d.data.length>=4) { + //populate number + d.hasNr = true; + XByteBuffer.toBytes(number,d.data, 0); + } + return d; + } + + public int getNumber() { + if (!hasNr) { + return -1; + } + return XByteBuffer.toInt(this.data, 0); + } + + public static boolean verify(Data d) { + boolean result = (d.length == d.data.length); + for ( int i=(d.hasNr?4:0); result && (i=0 && nr0 && d.data.length>=4) { + //populate number + d.hasNr = true; + XByteBuffer.toBytes(number,d.data, 0); + } + return d; + } + + public int getNumber() { + if (!hasNr) { + return -1; + } + return XByteBuffer.toInt(this.data, 0); + } + + public static boolean verify(Data d) { + boolean result = (d.length == d.data.length); + for ( int i=(d.hasNr?4:0); result && (i it = selector.selectedKeys().iterator(); + while (it.hasNext()) { + SelectionKey sk = it.next(); + it.remove(); + try { + int readyOps = sk.readyOps(); + sk.interestOps(sk.interestOps() & ~readyOps); + if (sender.process(sk, false)) { + total = total.add(bytes); + sender.reset(); + sender.setMessage(buf); + mb += ( (double) len) / 1024 / 1024; + if ( ( (++count) % 10000) == 0) { + long time = System.currentTimeMillis(); + double seconds = ( (double) (time - start)) / 1000; + System.out.println("Throughput " + df.format(mb / seconds) + " MiB/s, total "+mb+" MiB total "+total+" bytes."); + } + } + + } catch (Throwable t) { + t.printStackTrace(); + return; + } + } + selector.selectedKeys().clear(); + } + System.out.println("Complete, sleeping 15 seconds"); + Thread.sleep(15000); + } +} diff --git a/test/org/apache/catalina/tribes/test/transport/SocketNioValidateSend.java b/test/org/apache/catalina/tribes/test/transport/SocketNioValidateSend.java new file mode 100644 index 0000000..a3107b7 --- /dev/null +++ b/test/org/apache/catalina/tribes/test/transport/SocketNioValidateSend.java @@ -0,0 +1,102 @@ +/* + * 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.catalina.tribes.test.transport; + +import java.math.BigDecimal; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.Iterator; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.transport.nio.NioSender; + +public class SocketNioValidateSend { + + public static void main(String[] args) throws Exception { + Selector selector = Selector.open(); + Member mbr = new MemberImpl("localhost", 9999, 0); + byte seq = 0; + byte[] buf = new byte[50000]; + Arrays.fill(buf,seq); + int len = buf.length; + BigDecimal total = new BigDecimal((double)0); + BigDecimal bytes = new BigDecimal((double)len); + NioSender sender = new NioSender(); + sender.setDestination(mbr); + sender.setDirectBuffer(true); + sender.setSelector(selector); + sender.connect(); + sender.setMessage(buf); + System.out.println("Writing to 9999"); + long start = 0; + double mb = 0; + boolean first = true; + int count = 0; + + DecimalFormat df = new DecimalFormat("##.00"); + while (count<100000) { + if (first) { + first = false; + start = System.currentTimeMillis(); + } + sender.setMessage(buf); + int selectedKeys = 0; + try { + selectedKeys = selector.select(0); + } catch (Exception e) { + e.printStackTrace(); + continue; + } + + if (selectedKeys == 0) { + continue; + } + + Iterator it = selector.selectedKeys().iterator(); + while (it.hasNext()) { + SelectionKey sk = it.next(); + it.remove(); + try { + int readyOps = sk.readyOps(); + sk.interestOps(sk.interestOps() & ~readyOps); + if (sender.process(sk, false)) { + total = total.add(bytes); + sender.reset(); + seq++; + Arrays.fill(buf,seq); + sender.setMessage(buf); + mb += ( (double) len) / 1024 / 1024; + if ( ( (++count) % 10000) == 0) { + long time = System.currentTimeMillis(); + double seconds = ( (double) (time - start)) / 1000; + System.out.println("Throughput " + df.format(mb / seconds) + " MiB/s, total "+mb+" MiB total "+total+" bytes."); + } + } + + } catch (Throwable t) { + t.printStackTrace(); + return; + } + } + } + System.out.println("Complete, sleeping 15 seconds"); + Thread.sleep(15000); + } +} diff --git a/test/org/apache/catalina/tribes/test/transport/SocketReceive.java b/test/org/apache/catalina/tribes/test/transport/SocketReceive.java new file mode 100644 index 0000000..fbc1cdd --- /dev/null +++ b/test/org/apache/catalina/tribes/test/transport/SocketReceive.java @@ -0,0 +1,90 @@ +/* + * 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.catalina.tribes.test.transport; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.ServerSocket; +import java.net.Socket; +import java.text.DecimalFormat; + +import org.apache.catalina.tribes.transport.Constants; + +public class SocketReceive { + static long start = 0; + static double mb = 0; + static byte[] buf = new byte[8192 * 4]; + static boolean first = true; + static int count = 0; + static DecimalFormat df = new DecimalFormat("##.00"); + static BigDecimal total = new BigDecimal(0); + static BigDecimal bytes = new BigDecimal(32871); + + + public static void main(String[] args) throws Exception { + + try (ServerSocket srvSocket = new ServerSocket(9999)) { + System.out.println("Listening on 9999"); + Socket socket = srvSocket.accept(); + socket.setReceiveBufferSize(Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE); + InputStream in = socket.getInputStream(); + Thread t = new Thread() { + @Override + public void run() { + while ( true ) { + try { + sleep(1000); + printStats(start, mb, count, df, total); + }catch ( Exception x ) { + // Ignore + } + } + } + }; + t.setDaemon(true); + t.start(); + + while ( true ) { + if ( first ) { + first = false; start = System.currentTimeMillis(); + } + int len = in.read(buf); + if ( len == -1 ) { + printStats(start, mb, count, df, total); + System.exit(1); + } + if ( bytes.intValue() != len ) { + bytes = new BigDecimal((double)len); + } + total = total.add(bytes); + mb += ( (double) len) / 1024 / 1024; + if ( ((++count) % 10000) == 0 ) { + printStats(start, mb, count, df, total); + } + } + } + } + + private static void printStats(long start, double mb, int count, + DecimalFormat df, BigDecimal total) { + long time = System.currentTimeMillis(); + double seconds = ((double)(time-start))/1000; + System.out.println("Throughput " + df.format(mb/seconds) + + " MiB/s messages " + count + ", total " + mb + + " MiB total " + total + " bytes."); + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/test/transport/SocketSend.java b/test/org/apache/catalina/tribes/test/transport/SocketSend.java new file mode 100644 index 0000000..0cdb5ac --- /dev/null +++ b/test/org/apache/catalina/tribes/test/transport/SocketSend.java @@ -0,0 +1,74 @@ +/* + * 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.catalina.tribes.test.transport; + +import java.io.OutputStream; +import java.math.BigDecimal; +import java.net.Socket; +import java.text.DecimalFormat; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.membership.MemberImpl; + +public class SocketSend { + + public static void main(String[] args) throws Exception { + + + Member mbr = new MemberImpl("localhost", 9999, 0); + ChannelData data = new ChannelData(); + data.setOptions(Channel.SEND_OPTIONS_BYTE_MESSAGE); + data.setAddress(mbr); + byte[] buf = new byte[8192 * 4]; + data.setMessage(new XByteBuffer(buf,false)); + buf = XByteBuffer.createDataPackage(data); + int len = buf.length; + System.out.println("Message size:"+len+" bytes"); + BigDecimal total = new BigDecimal((double)0); + BigDecimal bytes = new BigDecimal((double)len); + try (Socket socket = new Socket("localhost",9999)) { + System.out.println("Writing to 9999"); + OutputStream out = socket.getOutputStream(); + long start = 0; + double mb = 0; + boolean first = true; + int count = 0; + DecimalFormat df = new DecimalFormat("##.00"); + while ( count<1000000 ) { + if ( first ) { + first = false; start = System.currentTimeMillis(); + } + out.write(buf,0,buf.length); + mb += ( (double) buf.length) / 1024 / 1024; + total = total.add(bytes); + if ( ((++count) % 10000) == 0 ) { + long time = System.currentTimeMillis(); + double seconds = ((double)(time-start))/1000; + System.out.println("Throughput " + df.format(mb/seconds) + + " MiB/s messages " + count + ", total " + mb + + " MiB, total " + total + " bytes."); + } + } + out.flush(); + System.out.println("Complete, sleeping 5 seconds"); + Thread.sleep(5000); + } + } +} diff --git a/test/org/apache/catalina/tribes/test/transport/SocketTribesReceive.java b/test/org/apache/catalina/tribes/test/transport/SocketTribesReceive.java new file mode 100644 index 0000000..0a2b54e --- /dev/null +++ b/test/org/apache/catalina/tribes/test/transport/SocketTribesReceive.java @@ -0,0 +1,103 @@ +/* + * 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.catalina.tribes.test.transport; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.ServerSocket; +import java.net.Socket; +import java.text.DecimalFormat; + +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.transport.Constants; + +public class SocketTribesReceive { + static long start = 0; + static double mb = 0; + //static byte[] buf = new byte[32871]; + static byte[] buf = new byte[32871]; + static boolean first = true; + static int count = 0; + static DecimalFormat df = new DecimalFormat("##.00"); + static BigDecimal total = new BigDecimal((double)0); + static BigDecimal bytes = new BigDecimal((double)32871); + + + public static void main(String[] args) throws Exception { + int size = Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE; + if (args.length > 0 ) { + try { + size = Integer.parseInt(args[0]); + } catch (Exception e){ + /* Ignore */ + } + } + XByteBuffer xbuf = new XByteBuffer(Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE, true); + try (ServerSocket srvSocket = new ServerSocket(9999)) { + System.out.println("Listening on 9999"); + Socket socket = srvSocket.accept(); + socket.setReceiveBufferSize(size); + InputStream in = socket.getInputStream(); + Thread t = new Thread() { + @Override + public void run() { + while ( true ) { + try { + sleep(1000); + printStats(start, mb, count, df, total); + }catch ( Exception x ) { /* Ignore */ } + } + } + }; + t.setDaemon(true); + t.start(); + + while ( true ) { + if ( first ) { + first = false; start = System.currentTimeMillis(); + } + int len = in.read(buf); + if ( len == -1 ) { + printStats(start, mb, count, df, total); + System.exit(1); + } + xbuf.append(buf,0,len); + if ( bytes.intValue() != len ) { + bytes = new BigDecimal((double)len); + } + total = total.add(bytes); + while ( xbuf.countPackages(true) > 0 ) { + xbuf.extractPackage(true); + count++; + } + mb += ( (double) len) / 1024 / 1024; + if ( ((count) % 10000) == 0 ) { + printStats(start, mb, count, df, total); + } + } + } + } + + private static void printStats(long start, double mb, int count, + DecimalFormat df, BigDecimal total) { + long time = System.currentTimeMillis(); + double seconds = ((double)(time-start))/1000; + System.out.println("Throughput " + df.format(mb/seconds) + + " MiB/s messages " + count + ", total " + mb + + " MiB total " + total + " bytes."); + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/tribes/test/transport/SocketValidateReceive.java b/test/org/apache/catalina/tribes/test/transport/SocketValidateReceive.java new file mode 100644 index 0000000..45433f0 --- /dev/null +++ b/test/org/apache/catalina/tribes/test/transport/SocketValidateReceive.java @@ -0,0 +1,120 @@ +/* + * 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.catalina.tribes.test.transport; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.ServerSocket; +import java.net.Socket; +import java.text.DecimalFormat; + +import org.apache.catalina.tribes.transport.Constants; + +public class SocketValidateReceive { + static long start = 0; + static double mb = 0; + static byte[] buf = new byte[8192 * 4]; + static boolean first = true; + static int count = 0; + static DecimalFormat df = new DecimalFormat("##.00"); + static BigDecimal total = new BigDecimal(0); + static BigDecimal bytes = new BigDecimal(32871); + + + public static void main(String[] args) throws Exception { + int size = Constants.DEFAULT_CLUSTER_MSG_BUFFER_SIZE; + if (args.length > 0 ) { + try {size=Integer.parseInt(args[0]);}catch(Exception x){ /* Ignore */ } + } + + try(ServerSocket srvSocket = new ServerSocket(9999)) { + System.out.println("Listening on 9999"); + Socket socket = srvSocket.accept(); + socket.setReceiveBufferSize(size); + InputStream in = socket.getInputStream(); + MyDataReader reader = new MyDataReader(50000); + Thread t = new Thread() { + @Override + public void run() { + while ( true ) { + try { + sleep(1000); + printStats(start, mb, count, df, total); + }catch ( Exception x ) { /* Ignore */ } + } + } + }; + t.setDaemon(true); + t.start(); + + while ( true ) { + if ( first ) { + first = false; start = System.currentTimeMillis(); + } + int len = in.read(buf); + if ( len == -1 ) { + printStats(start, mb, count, df, total); + System.exit(1); + } + count += reader.append(buf,0,len); + + if ( bytes.intValue() != len ) { + bytes = new BigDecimal((double)len); + } + total = total.add(bytes); + mb += ( (double) len) / 1024 / 1024; + if ( ((count) % 10000) == 0 ) { + printStats(start, mb, count, df, total); + } + } + } + } + + private static void printStats(long start, double mb, int count, + DecimalFormat df, BigDecimal total) { + long time = System.currentTimeMillis(); + double seconds = ((double)(time-start))/1000; + System.out.println("Throughput " + df.format(mb/seconds) + + " MiB/s messages " + count + ", total " + mb + + " MiB total " + total + " bytes."); + } + + public static class MyDataReader { + int length = 10; + int cur = 0; + byte seq = 0; + public MyDataReader(int len) { + length = len; + } + + public int append(byte[] b, int off, int len) throws Exception { + int packages = 0; + for ( int i=off; i users = db.getUsers(); + Assert.assertFalse("Some users found", users.hasNext()); + + User tomcatUser = db.createUser("tomcat", "password", "A new user"); + Role adminRole = db.createRole("admin", "Admin role"); + Role managerRole = db.createRole("manager", "Manager role"); + Role userRole = db.createRole("user", "User role"); + tomcatUser.addRole(adminRole); + tomcatUser.addRole(userRole); + db.save(); + + users = db.getUsers(); + Assert.assertTrue("No users found", users.hasNext()); + tomcatUser = users.next(); + Assert.assertTrue("Wrong user", tomcatUser.getUsername().equals("tomcat")); + Assert.assertTrue("Wrong password", tomcatUser.getPassword().equals("password")); + // Cannot save the user full name + Assert.assertNull("Wrong user fullname", tomcatUser.getFullName()); + adminRole = db.findRole("admin"); + Assert.assertNotNull("No admin role", adminRole); + Assert.assertTrue("No role for user", tomcatUser.isInRole(adminRole)); + // Manager role cannot be saved, but remains valid in memory + managerRole = db.findRole("manager"); + Assert.assertFalse("Unexpected role for user", tomcatUser.isInRole(managerRole)); + + db.close(); + + } + + @Test + public void testUserDatabase() + throws Exception { + + db = new DerbyUserDatabase("full"); + db.setReadonly(false); + db.setUserTable("users"); + db.setUserNameCol("user_name"); + db.setUserCredCol("user_pass"); + db.setUserRoleTable("user_roles"); + db.setUserGroupTable("user_groups"); + db.setRoleTable("roles"); + db.setRoleNameCol("role_name"); + db.setGroupTable("groups"); + db.setGroupNameCol("group_name"); + db.setGroupRoleTable("group_roles"); + // Not setting the description or full name since it allows checking persistence, + // as any modification is kept in memory until save() + db.open(); + // First create the DB tables + Connection connection = db.getConnection(); + for (String sql: FULL_SCHEMA.split(";")) { + try (Statement statement = connection.createStatement()) { + statement.execute(sql); + } + } + + Iterator users = db.getUsers(); + Assert.assertFalse("Some users found", users.hasNext()); + + User tomcatUser = db.createUser("tomcat", "password", "A new user"); + User randomUser = db.createUser("random", "password", "Another new user"); + Role adminRole = db.createRole("admin", "Admin role"); + Role managerRole = db.createRole("manager", "Manager role"); + Role userRole = db.createRole("user", "User role"); + Group userGroup = db.createGroup("users", "All users"); + userGroup.addRole(userRole); + tomcatUser.addRole(adminRole); + tomcatUser.addGroup(userGroup); + randomUser.addGroup(userGroup); + db.save(); + + users = db.getUsers(); + Assert.assertTrue("No users found", users.hasNext()); + tomcatUser = users.next(); + if (!tomcatUser.getUsername().equals("tomcat")) { + tomcatUser = users.next(); + } + Assert.assertTrue("Wrong user", tomcatUser.getUsername().equals("tomcat")); + Assert.assertTrue("Wrong password", tomcatUser.getPassword().equals("password")); + // Cannot save the user full name + Assert.assertNull("Wrong user fullname", tomcatUser.getFullName()); + adminRole = db.findRole("admin"); + Assert.assertNotNull("No admin role", adminRole); + Assert.assertNull("Wrong admin role", adminRole.getDescription()); + Assert.assertTrue("No role for user", tomcatUser.isInRole(adminRole)); + managerRole = db.findRole("manager"); + Assert.assertFalse("Unexpected role for user", tomcatUser.isInRole(managerRole)); + userRole = db.findRole("user"); + userGroup = db.findGroup("users"); + Assert.assertNull("Wrong users group", userGroup.getDescription()); + Assert.assertTrue("No role for group", userGroup.isInRole(userRole)); + randomUser = db.findUser("random"); + Assert.assertTrue("No group for user", randomUser.isInGroup(userGroup)); + + db.close(); + + } +} diff --git a/test/org/apache/catalina/users/MemoryUserDatabaseTests.java b/test/org/apache/catalina/users/MemoryUserDatabaseTests.java new file mode 100644 index 0000000..fa97f93 --- /dev/null +++ b/test/org/apache/catalina/users/MemoryUserDatabaseTests.java @@ -0,0 +1,259 @@ +/* + * 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.catalina.users; + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.security.Principal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.Group; +import org.apache.catalina.Role; +import org.apache.catalina.User; +import org.apache.catalina.realm.GenericPrincipal; +import org.apache.catalina.realm.UserDatabaseRealm; + +public class MemoryUserDatabaseTests { + private static File TEST_FILE = new File(System.getProperty("java.io.tmpdir"), "tomcat-users.xml"); + + private static MemoryUserDatabase db; + + @BeforeClass + public static void createSampleDB() + throws Exception { + + try(BufferedWriter out = new BufferedWriter(new FileWriter(TEST_FILE))) { + out.write("" + + "" + + "" + + "" + + "" + + ""); + } + + db = new MemoryUserDatabase(); + db.setPathname(TEST_FILE.toURI().toURL().toString()); + db.open(); + } + + @AfterClass + public static void cleanup() { + Assert.assertTrue(TEST_FILE.delete()); + } + + @Test + public void testLoadUserDatabase() + throws Exception { + assertPrincipalNames(new String[] { "testrole", "otherrole"}, db.getRoles()); + assertPrincipalNames(new String[] { "testgroup", "othergroup"}, db.getGroups()); + + Iterator users = db.getUsers(); + + Assert.assertTrue("No users found", users.hasNext()); + + User user = users.next(); + + Assert.assertEquals("admin", user.getName()); + Assert.assertNull(user.getFullName()); + Assert.assertEquals("sekr3t", user.getPassword()); + + assertPrincipalNames(new String[] { "testrole", "otherrole"}, user.getRoles()); + assertPrincipalNames(new String[] { "testgroup", "othergroup"}, user.getGroups()); + } + + public void testReloadUserDatabase() + throws Exception { + // Change the database on the disk and reload + + try(BufferedWriter out = new BufferedWriter(new FileWriter(TEST_FILE))) { + out.write("" + + "" + + "" + + "" + + "" + + ""); + + db.open(); + } + + assertPrincipalNames(new String[] { "foo", "bar"}, db.getRoles()); + assertPrincipalNames(new String[] { "bar", "foo"}, db.getGroups()); + + Iterator users = db.getUsers(); + + Assert.assertTrue("No users found", users.hasNext()); + + User user = users.next(); + + Assert.assertEquals("root", user.getName()); + Assert.assertNull(user.getFullName()); + Assert.assertEquals("sup3Rsekr3t", user.getPassword()); + + assertPrincipalNames(new String[] { "foo", "bar"}, user.getRoles()); + assertPrincipalNames(new String[] { "bar", "foo"}, user.getGroups()); + } + + @Test + public void testMultithreadedMutateUserDatabase() + throws Exception { + // Generate lots of concurrent load on the user database + Runnable job = new Runnable() { + @Override + public void run() { + for(int i=0; i<10; ++i) { + db.createUser("newUser-" + Thread.currentThread().getName() + "-" + i, "x", null); + } + } + }; + + int numThreads = 100; + Thread[] threads = new Thread[numThreads + 1]; + for(int i=0; i users = db.getUsers(); + for(; users.hasNext();) { + User user = users.next(); + if(user.getUsername().startsWith("newUser")) { + db.removeUser(user); + } + } + + users = db.getUsers(); + + Assert.assertTrue("No users found", users.hasNext()); + + User user = users.next(); + + Assert.assertEquals("admin", user.getName()); + Assert.assertNull(user.getFullName()); + Assert.assertEquals("sekr3t", user.getPassword()); + + assertPrincipalNames(new String[] { "testrole", "otherrole"}, user.getRoles()); + assertPrincipalNames(new String[] { "testgroup", "othergroup"}, user.getGroups()); + } + + @Test + public void testSerializePrincipal() + throws Exception { + User user = db.findUser("admin"); + GenericPrincipal gpIn = new UserDatabaseRealm.UserDatabasePrincipal(user, db); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(gpIn); + + byte[] data = bos.toByteArray(); + + ByteArrayInputStream bis = new ByteArrayInputStream(data); + ObjectInputStream ois = new ObjectInputStream(bis); + GenericPrincipal gpOut = (GenericPrincipal) ois.readObject(); + + Assert.assertEquals("admin", gpOut.getName()); + assertPrincipalNames(gpOut.getRoles(), user.getRoles()); + } + + private void assertPrincipalNames(String[] expectedNames, Iterator i) { + HashSet names = new HashSet<>(Arrays.asList(expectedNames)); + + int j=0; + while(i.hasNext()) { + Assert.assertTrue(names.contains(i.next().getName())); + j++; + } + + Assert.assertEquals(expectedNames.length, j); + } + + @Test + public void testDataEscaping() throws Exception { + File file = File.createTempFile("tomcat-users", ".xml"); + file.deleteOnExit(); + + MemoryUserDatabase mud = new MemoryUserDatabase(); + Role role = mud.createRole("role\"name", "descr&iption"); + Group group = mud.createGroup("grou parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] {"/foo/bar", "/bar", ""}); + parameterSets.add(new Object[] {"C:\\foo\\bar", "/bar", ""}); + parameterSets.add(new Object[] {"/foo/bar.war", "/bar", ""}); + parameterSets.add(new Object[] {"C:\\foo\\bar.war", "/bar", ""}); + parameterSets.add(new Object[] {"/foo/bar.xml", "/bar", ""}); + parameterSets.add(new Object[] {"C:\\foo\\bar.xml", "/bar", ""}); + parameterSets.add(new Object[] {"/foo/bar////", "/bar", ""}); + parameterSets.add(new Object[] {"C:\\foo\\bar\\\\", "/bar", ""}); + parameterSets.add(new Object[] {"/foo/bar##4", "/bar", "4"}); + parameterSets.add(new Object[] {"C:\\foo\\bar##4", "/bar", "4"}); + parameterSets.add(new Object[] {"/foo/bar#foo##4", "/bar/foo", "4"}); + parameterSets.add(new Object[] {"C:\\foo\\bar#foo##4", "/bar/foo", "4"}); + parameterSets.add(new Object[] {"/foo/ROOT", "", ""}); + parameterSets.add(new Object[] {"C:\\foo\\ROOT", "", ""}); + + return parameterSets; + } + + @Parameter(0) + public String path; + + @Parameter(1) + public String expectedPath; + + @Parameter(2) + public String expectedVersion; + + + @Test + public void testContextNameExtractFromPath() throws Exception { + ContextName cn = ContextName.extractFromPath(path); + Assert.assertEquals(expectedPath, cn.getPath()); + Assert.assertEquals(expectedVersion, cn.getVersion()); + } +} diff --git a/test/org/apache/catalina/util/TestNetMask.java b/test/org/apache/catalina/util/TestNetMask.java new file mode 100644 index 0000000..9ba3717 --- /dev/null +++ b/test/org/apache/catalina/util/TestNetMask.java @@ -0,0 +1,176 @@ +/* + * 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.catalina.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public final class TestNetMask { + + @Parameter(0) + public String mask; + + @Parameter(1) + public String input; + + @Parameter(2) + public Boolean valid; + + @Parameter(3) + public Boolean matches; + + + @Parameters(name = "{index}: mask [{0}], input [{1}]") + public static Collection inputs() { + List result = new ArrayList<>(); + + // Invalid IPv4 netmasks + result.add(new Object[] { "260.1.1.1", null, Boolean.FALSE, null }); + result.add(new Object[] { "1.2.3.4/foo", null, Boolean.FALSE, null }); + result.add(new Object[] { "1.2.3.4/-1", null, Boolean.FALSE, null }); + result.add(new Object[] { "1.2.3.4/33", null, Boolean.FALSE, null }); + + // Invalid IPv6 netmasks + result.add(new Object[] { "fffff::/71", null, Boolean.FALSE, null }); + result.add(new Object[] { "ae31::27:ef2:1/foo", null, Boolean.FALSE, null }); + result.add(new Object[] { "ae31::27:ef2:1/-1", null, Boolean.FALSE, null }); + result.add(new Object[] { "ae31::27:ef2:1/129", null, Boolean.FALSE, null }); + + // Invalid port regex suffix after ";" + result.add(new Object[] { "1.2.3.4;[", null, Boolean.FALSE, null }); + + // IPv4 + result.add(new Object[] { "1.2.3.4", "1.2.3.4", Boolean.TRUE, Boolean.TRUE }); + + result.add(new Object[] { "1.2.3.4/32", "1.2.3.3", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "1.2.3.4/32", "1.2.3.4", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "1.2.3.4/32", "1.2.3.5", Boolean.TRUE, Boolean.FALSE }); + + result.add(new Object[] { "1.2.3.4/31", "1.2.3.3", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "1.2.3.4/31", "1.2.3.4", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "1.2.3.4/31", "1.2.3.5", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "1.2.3.4/31", "1.2.3.6", Boolean.TRUE, Boolean.FALSE }); + + result.add(new Object[] { "10.0.0.0/22", "9.255.255.255", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "10.0.0.0/22", "10.0.0.0", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "10.0.0.0/22", "10.0.3.255", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "10.0.0.0/22", "10.0.4.0", Boolean.TRUE, Boolean.FALSE }); + + // IPv6 + result.add(new Object[] { "::5:1", "::5:1", Boolean.TRUE, Boolean.TRUE }); + + result.add(new Object[] { "::5:1/128", "::4:ffff", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "::5:1/128", "::5:1", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "::5:1/128", "::5:2", Boolean.TRUE, Boolean.FALSE }); + + result.add(new Object[] { "::5:1/127", "::4:ffff", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "::5:1/127", "::5:0", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "::5:1/127", "::5:1", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "::5:1/127", "::5:2", Boolean.TRUE, Boolean.FALSE }); + + result.add(new Object[] { "a::5:1/42", "9:ffff:ffff:ffff:ffff:ffff:ffff:ffff", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "a::5:1/42", "a::0", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "a::5:1/42", "a:0:3f:ffff:ffff:ffff:ffff:ffff", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "a::5:1/42", "a:0:40::", Boolean.TRUE, Boolean.FALSE }); + + // Mixed + result.add(new Object[] { "10.0.0.0/22", "::1", Boolean.TRUE, Boolean.FALSE }); + + // port + result.add(new Object[] { "1.2.3.4;8080", "1.2.3.4", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "1.2.3.4", "1.2.3.4;8080", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "1.2.3.4;", "1.2.3.4;8080", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "1.2.3.4;8080", "1.2.3.4;8080", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "1.2.3.4;8080", "1.2.3.4;8009", Boolean.TRUE, Boolean.FALSE }); + result.add(new Object[] { "1.2.3.4;.*", "1.2.3.4;8080", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "1.2.3.4;8\\d+", "1.2.3.4;8080", Boolean.TRUE, Boolean.TRUE }); + result.add(new Object[] { "1.2.3.4;8\\d+", "1.2.3.4;9090", Boolean.TRUE, Boolean.FALSE }); + + return result; + } + + + @Test + public void testNetMask() { + Exception exception = null; + NetMask netMask = null; + try { + netMask = new NetMask(mask); + } catch (Exception e) { + exception = e; + } + + if (valid.booleanValue()) { + Assert.assertNull(exception); + Assert.assertNotNull(netMask); + } else { + Assert.assertNotNull(exception); + Assert.assertEquals(IllegalArgumentException.class.getName(), exception.getClass().getName()); + return; + } + + final int portIdx = input.indexOf(";"); + final boolean usePort = portIdx >= 0 || mask.indexOf(";") >= 0; + final int port; + final String nonPortPart; + + if (portIdx == -1) { + port = -1; + nonPortPart = input; + } else { + port = Integer.parseInt(input.substring(portIdx + 1)); + nonPortPart = input.substring(0, portIdx); + } + + InetAddress inetAddress = null; + try { + inetAddress = InetAddress.getByName(nonPortPart); + } catch (UnknownHostException e) { + e.printStackTrace(); + Assert.fail(); + } + + if (usePort) { + Assert.assertEquals(matches, Boolean.valueOf(netMask.matches(inetAddress, port))); + } else { + Assert.assertEquals(matches, Boolean.valueOf(netMask.matches(inetAddress))); + } + + Assert.assertEquals(mask, netMask.toString()); + + NetMask nm1, nm2, nm3; + nm1 = new NetMask("192.168.0.0/24"); + nm2 = new NetMask("192.168.0.0/24"); + nm3 = new NetMask("192.168.1.0/24"); + + Assert.assertEquals(nm1, nm2); + Assert.assertEquals(nm1.hashCode(), nm2.hashCode()); + + Assert.assertNotEquals(nm1, nm3); + } +} diff --git a/test/org/apache/catalina/util/TestNetMaskSet.java b/test/org/apache/catalina/util/TestNetMaskSet.java new file mode 100644 index 0000000..f97538c --- /dev/null +++ b/test/org/apache/catalina/util/TestNetMaskSet.java @@ -0,0 +1,62 @@ +/* + * 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.catalina.util; + +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class TestNetMaskSet { + + @Test + public void testNetMaskSet() throws UnknownHostException { + + NetMaskSet nms = new NetMaskSet(); + nms.addAll("192.168.0.0/24, 192.168.1.0/27, 192.168.2.2, 10.0.0.0/8"); + + Assert.assertTrue(nms.contains("192.168.0.5")); + Assert.assertTrue(nms.contains("192.168.0.255")); + + Assert.assertTrue(nms.contains("192.168.1.0")); + Assert.assertTrue(nms.contains("192.168.1.1")); + Assert.assertTrue(nms.contains("192.168.1.31")); + Assert.assertFalse(nms.contains("192.168.1.32")); + + Assert.assertTrue(nms.contains("192.168.2.2")); + Assert.assertFalse(nms.contains("192.168.2.1")); + Assert.assertFalse(nms.contains("192.168.2.3")); + + Assert.assertTrue(nms.contains("10.10.10.10")); + Assert.assertTrue(nms.contains("10.20.30.40")); + Assert.assertFalse(nms.contains("9.10.10.10")); + Assert.assertFalse(nms.contains("11.10.10.10")); + + String s = nms.toString(); + Assert.assertTrue(s.indexOf('[') == -1); + Assert.assertTrue(s.indexOf(']') == -1); + + List list = Arrays.asList(s.split("\\s*,\\s*")); + Assert.assertTrue(list.contains("192.168.0.0/24")); + Assert.assertTrue(list.contains("192.168.1.0/27")); + Assert.assertTrue(list.contains("192.168.2.2")); + Assert.assertTrue(list.contains("10.0.0.0/8")); + } +} diff --git a/test/org/apache/catalina/util/TestParameterMap.java b/test/org/apache/catalina/util/TestParameterMap.java new file mode 100644 index 0000000..87ead7f --- /dev/null +++ b/test/org/apache/catalina/util/TestParameterMap.java @@ -0,0 +1,310 @@ +/* + * 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.catalina.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TestParameterMap { + + private static final String[] TEST_PARAM_VALUES_1 = { "value1" }; + private static final String[] TEST_PARAM_VALUES_2 = { "value2" }; + private static final String[] TEST_PARAM_VALUES_2_UPDATED = { "value2-updated" }; + private static final String[] TEST_PARAM_VALUES_3 = { "value3" }; + private static final String[] TEST_PARAM_VALUES_REPLACED = { "replaced" }; + + private Map paramMap; + + @Before + public void setUp() { + paramMap = new ParameterMap<>(); + + paramMap.put("param1", TEST_PARAM_VALUES_1); + paramMap.put("param2", TEST_PARAM_VALUES_2); + paramMap.put("param3", TEST_PARAM_VALUES_3); + + Assert.assertTrue(paramMap.containsKey("param1")); + Assert.assertArrayEquals(TEST_PARAM_VALUES_1, paramMap.get("param1")); + Assert.assertTrue(paramMap.containsKey("param2")); + Assert.assertArrayEquals(TEST_PARAM_VALUES_2, paramMap.get("param2")); + Assert.assertTrue(paramMap.containsKey("param3")); + Assert.assertArrayEquals(TEST_PARAM_VALUES_3, paramMap.get("param3")); + + final Set keySet = paramMap.keySet(); + Assert.assertTrue(keySet.contains("param1")); + Assert.assertTrue(keySet.contains("param2")); + Assert.assertTrue(keySet.contains("param3")); + + paramMap.put("param2", TEST_PARAM_VALUES_2_UPDATED); + paramMap.remove("param3"); + + Assert.assertTrue(paramMap.containsKey("param1")); + Assert.assertArrayEquals(TEST_PARAM_VALUES_1, paramMap.get("param1")); + Assert.assertTrue(paramMap.containsKey("param2")); + Assert.assertArrayEquals(TEST_PARAM_VALUES_2_UPDATED, paramMap.get("param2")); + Assert.assertFalse(paramMap.containsKey("param3")); + Assert.assertNull(paramMap.get("param3")); + + Assert.assertTrue(keySet.contains("param1")); + Assert.assertTrue(keySet.contains("param2")); + Assert.assertFalse(keySet.contains("param3")); + } + + @After + public void tearDown() { + Assert.assertTrue(paramMap.containsKey("param1")); + Assert.assertArrayEquals(TEST_PARAM_VALUES_1, paramMap.get("param1")); + Assert.assertTrue(paramMap.containsKey("param2")); + Assert.assertArrayEquals(TEST_PARAM_VALUES_2_UPDATED, paramMap.get("param2")); + Assert.assertFalse(paramMap.containsKey("param3")); + Assert.assertNull(paramMap.get("param3")); + } + + @Test + public void testMapImmutabilityAfterLocked() { + ((ParameterMap) paramMap).setLocked(true); + + try { + String[] updatedParamValues22 = new String[] { "value2-updated-2" }; + paramMap.put("param2", updatedParamValues22); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + + try { + String[] updatedParamValues22 = new String[] { "value2-updated-2" }; + paramMap.putIfAbsent("param22", updatedParamValues22); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + + try { + final Map additionalParams = new HashMap<>(); + additionalParams.put("param4", new String[] { "value4" }); + paramMap.putAll(additionalParams); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + + try { + paramMap.merge("param2", new String[] { "value2-merged" }, (a, b) -> (b)); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + + try { + paramMap.remove("param2"); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + + try { + paramMap.remove("param2", TEST_PARAM_VALUES_2_UPDATED); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + + try { + paramMap.replace("param2", new String[] { "value2-replaced" }); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + + try { + paramMap.replace("param2", TEST_PARAM_VALUES_2_UPDATED, new String[] { "value2-replaced" }); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + + try { + paramMap.replaceAll((a, b) -> TEST_PARAM_VALUES_REPLACED); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + paramMap.clear(); + Assert.fail("ParameterMap is not locked."); + } catch (IllegalStateException expectedException) { + } + } + + @Test + public void testKeySetImmutabilityAfterLocked() { + ((ParameterMap) paramMap).setLocked(true); + + final Set keySet = paramMap.keySet(); + + try { + keySet.add("param4"); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + keySet.remove("param2"); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + keySet.removeIf((a) -> "param2".equals(a)); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + keySet.removeAll(Arrays.asList("param1", "param2")); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + keySet.retainAll(Collections.emptyList()); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + keySet.clear(); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + } + + @Test + public void testValuesImmutabilityAfterLocked() { + ((ParameterMap) paramMap).setLocked(true); + + final Collection valuesCol = paramMap.values(); + + try { + valuesCol.add(new String[] { "value4" }); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + List list = new ArrayList<>(); + list.add(new String[] { "value4" }); + valuesCol.addAll(list); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + valuesCol.remove(TEST_PARAM_VALUES_1); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + valuesCol.removeIf((a) -> true); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + List list = new ArrayList<>(); + list.add(TEST_PARAM_VALUES_1); + valuesCol.removeAll(list); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + valuesCol.retainAll(Collections.emptyList()); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + valuesCol.clear(); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + } + + @Test + public void testEntrySetImmutabilityAfterLocked() { + ((ParameterMap) paramMap).setLocked(true); + + final Set> entrySet = paramMap.entrySet(); + + try { + final Map anotherParamsMap = new HashMap<>(); + anotherParamsMap.put("param4", new String[] { "value4" }); + Map.Entry anotherEntry = anotherParamsMap.entrySet().iterator().next(); + entrySet.add(anotherEntry); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + final Map anotherParamsMap = new HashMap<>(); + anotherParamsMap.put("param4", new String[] { "value4" }); + anotherParamsMap.put("param5", new String[] { "value5" }); + entrySet.addAll(anotherParamsMap.entrySet()); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + final Map.Entry entry = entrySet.iterator().next(); + entrySet.remove(entry); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + entrySet.removeIf((a) -> true); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + Set> anotherEntrySet = new HashSet<>(entrySet); + entrySet.removeAll(anotherEntrySet); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + entrySet.retainAll(Collections.emptySet()); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + + try { + entrySet.clear(); + Assert.fail("ParameterMap is not locked."); + } catch (UnsupportedOperationException expectedException) { + } + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/util/TestServerInfo.java b/test/org/apache/catalina/util/TestServerInfo.java new file mode 100644 index 0000000..3a61701 --- /dev/null +++ b/test/org/apache/catalina/util/TestServerInfo.java @@ -0,0 +1,30 @@ +/* + * 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.catalina.util; + +import org.junit.Test; + +public class TestServerInfo { + + /** + * Test that prints the server version info. + */ + @Test + public void testServerInfo() { + ServerInfo.main(new String[0]); + } +} diff --git a/test/org/apache/catalina/util/TestTimeBucketCounter.java b/test/org/apache/catalina/util/TestTimeBucketCounter.java new file mode 100644 index 0000000..43f505e --- /dev/null +++ b/test/org/apache/catalina/util/TestTimeBucketCounter.java @@ -0,0 +1,78 @@ +/* + * 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.catalina.util; + +import org.junit.Assert; +import org.junit.Test; + +public class TestTimeBucketCounter { + + static final double DELTA = 0.001; + + @Test + public void testNextPowerOf2() { + Assert.assertEquals(128, TimeBucketCounter.nextPowerOf2(100)); + Assert.assertEquals(128, TimeBucketCounter.nextPowerOf2(127)); + Assert.assertEquals(128, TimeBucketCounter.nextPowerOf2(128)); + Assert.assertEquals(256, TimeBucketCounter.nextPowerOf2(250)); + Assert.assertEquals(1024, TimeBucketCounter.nextPowerOf2(1000)); + Assert.assertEquals(1024, TimeBucketCounter.nextPowerOf2(1023)); + Assert.assertEquals(2048, TimeBucketCounter.nextPowerOf2(1025)); + } + + @Test + public void testCalcRatioToNextPowerOf2() { + Assert.assertEquals(256 / 256d, TimeBucketCounter.ratioToPowerOf2(256), DELTA); + Assert.assertEquals(256 / 200d, TimeBucketCounter.ratioToPowerOf2(200), DELTA); + Assert.assertEquals(65_536 / 60_000d, TimeBucketCounter.ratioToPowerOf2(60_000), DELTA); + } + + @Test + public void testTimeBucketCounter() { + TimeBucketCounter tbc = new TimeBucketCounter(60, new java.util.concurrent.ScheduledThreadPoolExecutor(1)); + Assert.assertEquals(16, tbc.getNumBits()); + Assert.assertEquals(1.092, tbc.getRatio(), DELTA); + } + + @Test + public void testGetMillisUntilNextBucket() throws InterruptedException { + long millis; + int tb1, tb2; + + TimeBucketCounter tbc = new TimeBucketCounter(2, new java.util.concurrent.ScheduledThreadPoolExecutor(1)); + tb1 = tbc.getCurrentBucketPrefix(); + millis = tbc.getMillisUntilNextBucket(); + + // sleep millis and get bucket + Thread.sleep(millis); + tb2 = tbc.getCurrentBucketPrefix(); + + // ensure the new time bucket is one more than the previous one + Assert.assertEquals(1, tb2 - tb1); + + tb1 = tb2; + millis = tbc.getMillisUntilNextBucket(); + + // sleep again + Thread.sleep(millis); + tb2 = tbc.getCurrentBucketPrefix(); + + // ensure again + Assert.assertEquals(1, tb2 - tb1); + } +} diff --git a/test/org/apache/catalina/util/TestURLEncoder.java b/test/org/apache/catalina/util/TestURLEncoder.java new file mode 100644 index 0000000..dbef0fb --- /dev/null +++ b/test/org/apache/catalina/util/TestURLEncoder.java @@ -0,0 +1,56 @@ +/* + * 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.catalina.util; + +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +public class TestURLEncoder { + + private static final String SPACE = " "; + private static final String DOLLAR = "$"; + private static final String AMPERSAND = "&"; + private static final String AMPERSAND_ENCODED = "%26"; + + @Test + public void testClone() { + URLEncoder original = new URLEncoder(); + URLEncoder clone = (URLEncoder) original.clone(); + + // Ensure encode as space is not shared + original.setEncodeSpaceAsPlus(true); + Assert.assertNotEquals(original.encode(SPACE, StandardCharsets.UTF_8), + clone.encode(SPACE, StandardCharsets.UTF_8)); + + // Ensure safe characters is not shared + original.addSafeCharacter('$'); + Assert.assertNotEquals(original.encode(DOLLAR, StandardCharsets.UTF_8), + clone.encode(DOLLAR, StandardCharsets.UTF_8)); + } + + + @Test + public void testRemoveSafeCharacter() { + URLEncoder xml = (URLEncoder) URLEncoder.DEFAULT.clone(); + // This should not encode '&' + Assert.assertEquals(AMPERSAND, xml.encode(AMPERSAND, StandardCharsets.UTF_8)); + xml.removeSafeCharacter('&'); + Assert.assertEquals(AMPERSAND_ENCODED, xml.encode(AMPERSAND, StandardCharsets.UTF_8)); + } +} diff --git a/test/org/apache/catalina/valves/Benchmarks.java b/test/org/apache/catalina/valves/Benchmarks.java new file mode 100644 index 0000000..431c73b --- /dev/null +++ b/test/org/apache/catalina/valves/Benchmarks.java @@ -0,0 +1,489 @@ +/* + * 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.catalina.valves; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.Test; + +/** + * Some simple micro-benchmarks to help determine best approach for thread safety in valves, particularly the + * {@link AccessLogValve}. Implemented as JUnit tests to make the simple to execute but does not used Test* as the class + * name to avoid being included in the automated unit tests. + */ +public class Benchmarks { + @Test + public void testAccessLogGetDate() throws Exception { + // Is it better to use a sync or a thread local here? + BenchmarkTest benchmark = new BenchmarkTest(); + Runnable[] tests = new Runnable[] { new GetDateBenchmarkTest_Sync(), new GetDateBenchmarkTest_Local(), + new GetDateBenchmarkTest_LocalMutableLong(), new GetDateBenchmarkTest_LocalStruct() }; + benchmark.doTest(5, tests); + } + + private static class GetDateBenchmarkTest_Sync implements Runnable { + + @Override + public String toString() { + return "Syncs"; + } + + private volatile long currentMillis = 0; + private volatile Date currentDate = null; + + @Override + public void run() { + getCurrentDate(); + } + + public Date getCurrentDate() { + long systime = System.currentTimeMillis(); + if ((systime - currentMillis) > 1000) { + synchronized (this) { + if ((systime - currentMillis) > 1000) { + currentDate = new Date(systime); + currentMillis = systime; + } + } + } + return currentDate; + } + } + + private static class GetDateBenchmarkTest_Local implements Runnable { + + @Override + public String toString() { + return "ThreadLocals"; + } + + private ThreadLocal currentMillisLocal = new ThreadLocal<>() { + @Override + protected Long initialValue() { + return Long.valueOf(0); + } + }; + + private ThreadLocal currentDateLocal = new ThreadLocal<>(); + + @Override + public void run() { + getCurrentDate(); + } + + public Date getCurrentDate() { + long systime = System.currentTimeMillis(); + if ((systime - currentMillisLocal.get().longValue()) > 1000) { + currentDateLocal.set(new Date(systime)); + currentMillisLocal.set(Long.valueOf(systime)); + } + return currentDateLocal.get(); + } + } + + private static class GetDateBenchmarkTest_LocalMutableLong implements Runnable { + + @Override + public String toString() { + return "ThreadLocals with a mutable Long"; + } + + private static class MutableLong { + long value = 0; + } + + private ThreadLocal currentMillisLocal = new ThreadLocal<>() { + @Override + protected MutableLong initialValue() { + return new MutableLong(); + } + }; + + private ThreadLocal currentDateLocal = new ThreadLocal<>(); + + @Override + public void run() { + getCurrentDate(); + } + + public Date getCurrentDate() { + long systime = System.currentTimeMillis(); + if ((systime - currentMillisLocal.get().value) > 1000) { + currentDateLocal.set(new Date(systime)); + currentMillisLocal.get().value = systime; + } + return currentDateLocal.get(); + } + } + + private static class GetDateBenchmarkTest_LocalStruct implements Runnable { + + @Override + public String toString() { + return "single ThreadLocal"; + } + + // note, that we can avoid (long -> Long) conversion + private static class Struct { + public long currentMillis = 0; + public Date currentDate; + } + + private ThreadLocal currentStruct = new ThreadLocal<>() { + @Override + protected Struct initialValue() { + return new Struct(); + } + }; + + @Override + public void run() { + getCurrentDate(); + } + + public Date getCurrentDate() { + Struct struct = currentStruct.get(); + long systime = System.currentTimeMillis(); + if ((systime - struct.currentMillis) > 1000) { + struct.currentDate = new Date(systime); + struct.currentMillis = systime; + } + return struct.currentDate; + } + } + + @Test + public void testAccessLogTimeDateElement() throws Exception { + // Is it better to use a sync or a thread local here? + BenchmarkTest benchmark = new BenchmarkTest(); + Runnable[] tests = new Runnable[] { new TimeDateElementBenchmarkTest_Sync(), + new TimeDateElementBenchmarkTest_Local(), new TimeDateElementBenchmarkTest_LocalStruct(), + new TimeDateElementBenchmarkTest_LocalStruct_SBuilder() }; + benchmark.doTest(5, tests); + } + + private abstract static class TimeDateElementBenchmarkTestBase { + protected static final String months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", + "Nov", "Dec" }; + + protected String lookup(String month) { + int index; + try { + index = Integer.parseInt(month) - 1; + } catch (Throwable t) { + index = 0; // cannot happen, in theory + } + return months[index]; + } + } + + private static class TimeDateElementBenchmarkTest_Sync extends TimeDateElementBenchmarkTestBase + implements Runnable { + + @Override + public String toString() { + return "Syncs"; + } + + private volatile Date currentDate = new Date(); + private volatile String currentDateString = null; + private SimpleDateFormat dayFormatter = new SimpleDateFormat("dd"); + private SimpleDateFormat monthFormatter = new SimpleDateFormat("MM"); + private SimpleDateFormat yearFormatter = new SimpleDateFormat("yyyy"); + private SimpleDateFormat timeFormatter = new SimpleDateFormat("hh:mm:ss"); + + @Override + public void run() { + printDate(); + } + + public String printDate() { + StringBuilder buf = new StringBuilder(); + Date date = getDateSync(); + if (currentDate != date) { + synchronized (this) { + if (currentDate != date) { + StringBuilder current = new StringBuilder(32); + current.append('['); + current.append(dayFormatter.format(date)); // Day + current.append('/'); + current.append(lookup(monthFormatter.format(date))); // Month + current.append('/'); + current.append(yearFormatter.format(date)); // Year + current.append(':'); + current.append(timeFormatter.format(date)); // Time + current.append(']'); + currentDateString = current.toString(); + currentDate = date; + } + } + } + buf.append(currentDateString); + return buf.toString(); + } + + private Date getDateSync() { + long systime = System.currentTimeMillis(); + if ((systime - currentDate.getTime()) > 1000) { + synchronized (this) { + if ((systime - currentDate.getTime()) > 1000) { + currentDate.setTime(systime); + } + } + } + return currentDate; + } + } + + private static class TimeDateElementBenchmarkTest_Local extends TimeDateElementBenchmarkTestBase + implements Runnable { + + @Override + public String toString() { + return "ThreadLocals"; + } + + private ThreadLocal currentDateStringLocal = new ThreadLocal<>(); + + private ThreadLocal currentDateLocal = new ThreadLocal<>() { + @Override + protected Date initialValue() { + return new Date(); + } + }; + private ThreadLocal dayFormatterLocal = new ThreadLocal<>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("dd"); + } + }; + private ThreadLocal monthFormatterLocal = new ThreadLocal<>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("MM"); + } + }; + private ThreadLocal yearFormatterLocal = new ThreadLocal<>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("yyyy"); + } + }; + private ThreadLocal timeFormatterLocal = new ThreadLocal<>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("hh:mm:ss"); + } + }; + + @Override + public void run() { + printDate(); + } + + public String printDate() { + getDateLocal(); + if (currentDateStringLocal.get() == null) { + StringBuilder current = new StringBuilder(32); + current.append('['); + current.append(dayFormatterLocal.get().format(currentDateLocal.get())); // Day + current.append('/'); + current.append(lookup(monthFormatterLocal.get().format(currentDateLocal.get()))); // Month + current.append('/'); + current.append(yearFormatterLocal.get().format(currentDateLocal.get())); // Year + current.append(':'); + current.append(timeFormatterLocal.get().format(currentDateLocal.get())); // Time + current.append(']'); + currentDateStringLocal.set(current.toString()); + } + return currentDateStringLocal.get(); + } + + private Date getDateLocal() { + long systime = System.currentTimeMillis(); + if ((systime - currentDateLocal.get().getTime()) > 1000) { + currentDateLocal.get().setTime(systime); + currentDateStringLocal.set(null); + } + return currentDateLocal.get(); + } + } + + private static class TimeDateElementBenchmarkTest_LocalStruct extends TimeDateElementBenchmarkTestBase + implements Runnable { + + @Override + public String toString() { + return "single ThreadLocal"; + } + + private static class Struct { + public String currentDateString; + public Date currentDate = new Date(); + public SimpleDateFormat dayFormatter = new SimpleDateFormat("dd"); + public SimpleDateFormat monthFormatter = new SimpleDateFormat("MM"); + public SimpleDateFormat yearFormatter = new SimpleDateFormat("yyyy"); + public SimpleDateFormat timeFormatter = new SimpleDateFormat("hh:mm:ss"); + } + + private ThreadLocal structLocal = new ThreadLocal<>() { + @Override + protected Struct initialValue() { + return new Struct(); + } + }; + + @Override + public void run() { + printDate(); + } + + public String printDate() { + getDateLocal(); + Struct struct = structLocal.get(); + if (struct.currentDateString == null) { + StringBuilder current = new StringBuilder(32); + current.append('['); + current.append(struct.dayFormatter.format(struct.currentDate)); // Day + current.append('/'); + current.append(lookup(struct.monthFormatter.format(struct.currentDate))); // Month + current.append('/'); + current.append(struct.yearFormatter.format(struct.currentDate)); // Year + current.append(':'); + current.append(struct.timeFormatter.format(struct.currentDate)); // Time + current.append(']'); + struct.currentDateString = current.toString(); + } + return struct.currentDateString; + } + + private Date getDateLocal() { + Struct struct = structLocal.get(); + long systime = System.currentTimeMillis(); + if ((systime - struct.currentDate.getTime()) > 1000) { + struct.currentDate.setTime(systime); + struct.currentDateString = null; + } + return struct.currentDate; + } + } + + private static class TimeDateElementBenchmarkTest_LocalStruct_SBuilder extends TimeDateElementBenchmarkTestBase + implements Runnable { + + @Override + public String toString() { + return "single ThreadLocal, with StringBuilder"; + } + + private static class Struct { + public String currentDateString; + public Date currentDate = new Date(); + public SimpleDateFormat dayFormatter = new SimpleDateFormat("dd"); + public SimpleDateFormat monthFormatter = new SimpleDateFormat("MM"); + public SimpleDateFormat yearFormatter = new SimpleDateFormat("yyyy"); + public SimpleDateFormat timeFormatter = new SimpleDateFormat("hh:mm:ss"); + } + + private ThreadLocal structLocal = new ThreadLocal<>() { + @Override + protected Struct initialValue() { + return new Struct(); + } + }; + + @Override + public void run() { + printDate(); + } + + public String printDate() { + getDateLocal(); + Struct struct = structLocal.get(); + if (struct.currentDateString == null) { + StringBuilder current = new StringBuilder(32); + current.append('['); + current.append(struct.dayFormatter.format(struct.currentDate)); // Day + current.append('/'); + current.append(lookup(struct.monthFormatter.format(struct.currentDate))); // Month + current.append('/'); + current.append(struct.yearFormatter.format(struct.currentDate)); // Year + current.append(':'); + current.append(struct.timeFormatter.format(struct.currentDate)); // Time + current.append(']'); + struct.currentDateString = current.toString(); + } + return struct.currentDateString; + } + + private Date getDateLocal() { + Struct struct = structLocal.get(); + long systime = System.currentTimeMillis(); + if ((systime - struct.currentDate.getTime()) > 1000) { + struct.currentDate.setTime(systime); + struct.currentDateString = null; + } + return struct.currentDate; + } + } + + private static class BenchmarkTest { + public void doTest(int threadCount, Runnable[] tests) throws Exception { + for (int iterations = 1000000; iterations < 10000001; iterations += 1000000) { + for (Runnable test : tests) { + doTestInternal(threadCount, iterations, test); + } + } + } + + private void doTestInternal(int threadCount, int iterations, Runnable test) throws Exception { + long start = System.currentTimeMillis(); + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(new TestThread(iterations, test)); + } + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + long end = System.currentTimeMillis(); + + System.out.println(test.getClass().getSimpleName() + ": " + threadCount + " threads and " + iterations + + " iterations using " + test + " took " + (end - start) + "ms"); + } + } + + private static class TestThread implements Runnable { + private int count; + private Runnable test; + + TestThread(int count, Runnable test) { + this.count = count; + this.test = test; + } + + @Override + public void run() { + for (int i = 0; i < count; i++) { + test.run(); + } + } + } +} diff --git a/test/org/apache/catalina/valves/TestAbstractAccessLogValveEscape.java b/test/org/apache/catalina/valves/TestAbstractAccessLogValveEscape.java new file mode 100644 index 0000000..d6f5bc4 --- /dev/null +++ b/test/org/apache/catalina/valves/TestAbstractAccessLogValveEscape.java @@ -0,0 +1,73 @@ +/* + * 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.catalina.valves; + +import java.io.CharArrayWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + + +@RunWith(Parameterized.class) +public class TestAbstractAccessLogValveEscape { + + @Parameters(name = "{index}: [{0}]") + public static Collection parameters() { + + List parameters = new ArrayList<>(); + + parameters.add(new String[] { null, "-" }); + parameters.add(new String[] { "", "-" }); + parameters.add(new String[] { "ok", "ok" }); + parameters.add(new String[] { "o\fk", "o\\fk" }); + parameters.add(new String[] { "o\nk", "o\\nk" }); + parameters.add(new String[] { "o\rk", "o\\rk" }); + parameters.add(new String[] { "o\tk", "o\\tk" }); + parameters.add(new String[] { "o\"k", "o\\\"k" }); + parameters.add(new String[] { "o\\k", "o\\\\k" }); + parameters.add(new String[] { "o\u0002k", "o\\u0002k" }); + parameters.add(new String[] { "o\u007fk", "o\\u007fk" }); + parameters.add(new String[] { "o\u0080k", "o\\u0080k" }); + parameters.add(new String[] { "o\u00ffk", "o\\u00ffk" }); + parameters.add(new String[] { "o\u8765k", "o\\u8765k" }); + parameters.add(new String[] { "12345\u0002\u00036\t789\"", "12345\\u0002\\u00036\\t789\\\"" }); + parameters.add(new String[] { "\u0002\u00036\t789\"12345", "\\u0002\\u00036\\t789\\\"12345" }); + + return parameters; + } + + @Parameter(0) + public String input; + + @Parameter(1) + public String expected; + + + @Test + public void testEscape() { + CharArrayWriter actual = new CharArrayWriter(); + AbstractAccessLogValve.escapeAndAppend(input, actual); + Assert.assertEquals(expected, actual.toString()); + } +} diff --git a/test/org/apache/catalina/valves/TestAccessLogValve.java b/test/org/apache/catalina/valves/TestAccessLogValve.java new file mode 100644 index 0000000..74d207b --- /dev/null +++ b/test/org/apache/catalina/valves/TestAccessLogValve.java @@ -0,0 +1,341 @@ +/* + * 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.catalina.valves; + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.json.JSONParser; +import org.apache.tomcat.util.json.ParseException; + + +@RunWith(Parameterized.class) +public class TestAccessLogValve extends TomcatBaseTest { + + private static Log log = LogFactory.getLog(TestAccessLogValve.class); + + // Requests can return in the client before log() has been called + private static final long SLEEP = 2; + private static final long SLEEP_MAX = 1000; + + private static final String TEXT_TYPE = "text"; + private static final String JSON_TYPE = "json"; + + private static final String RESPONSE = "OK\n"; + private static final String BYTES = Integer.toString(RESPONSE.length()); + + private static final String REQUEST_HEADER = "myRequestHeader"; + private static final String REQUEST_HEADER_VALUE = "1 2 3 4 5 6 7 8 9"; + private static final String REQUEST_HEADER_VALUE_ENCODED = "1 2 3 4 5 6 7 8 9"; + + private static final String RESPONSE_HEADER = "myResponseHeader"; + private static final String RESPONSE_HEADER_VALUE = "864\u00e4\u00f6\u00fc642"; + private static final String RESPONSE_HEADER_VALUE_ENCODED = "864\\\\u00e4\\\\u00f6\\\\u00fc642"; + + private static final String REQUEST_ATTRIBUTE = "myRequestAttribute"; + private static final String REQUEST_ATTRIBUTE_VALUE = "987\u00e4\u00f6\u00fc654"; + private static final String REQUEST_ATTRIBUTE_VALUE_ENCODED = "987\\\\u00e4\\\\u00f6\\\\u00fc654"; + + private static final String SESSION_ATTRIBUTE = "mySessionAttribute"; + private static final String SESSION_ATTRIBUTE_VALUE = "123\u00e4\u00f6\u00fc456"; + private static final String SESSION_ATTRIBUTE_VALUE_ENCODED = "123\\\\u00e4\\\\u00f6\\\\u00fc456"; + + private static final String DATE_PATTERN = "\\[\\d\\d/[A-Z][a-z][a-z]/\\d\\d\\d\\d:\\d\\d:\\d\\d:\\d\\d [-+]\\d\\d\\d\\d\\]"; + private static final String LOCAL_IP_PATTERN = "(127\\.0\\.\\d\\.\\d+|\\[::1\\])"; + private static final String IP_PATTERN = "(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|([0-9a-fA-F]){1,4}(:([0-9a-fA-F]){1,4}){7})"; + private static final String UA_PATTERN = "[^\"]+"; + + @Parameterized.Parameters(name = "{index}: Name[{0}], Type[{1}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] {"pct-a", TEXT_TYPE, "/", "%a", LOCAL_IP_PATTERN}); + parameterSets.add(new Object[] {"pct-a", JSON_TYPE, "/", "%a", "\\{\"remoteAddr\":\"" + LOCAL_IP_PATTERN + "\"\\}"}); + parameterSets.add(new Object[] {"pct-A", TEXT_TYPE, "/", "%A", IP_PATTERN}); + parameterSets.add(new Object[] {"pct-A", JSON_TYPE, "/", "%A", "\\{\"localAddr\":\"" + IP_PATTERN + "\"\\}"}); + parameterSets.add(new Object[] {"pct-b", TEXT_TYPE, "/", "%b", BYTES}); + parameterSets.add(new Object[] {"pct-b", JSON_TYPE, "/", "%b", "\\{\"size\":\"" + BYTES + "\"\\}"}); + parameterSets.add(new Object[] {"pct-B", TEXT_TYPE, "/", "%B", BYTES}); + parameterSets.add(new Object[] {"pct-B", JSON_TYPE, "/", "%B", "\\{\"byteSentNC\":\"" + BYTES + "\"\\}"}); + parameterSets.add(new Object[] {"pct-D", TEXT_TYPE, "/", "%D", "\\d+"}); + parameterSets.add(new Object[] {"pct-D", JSON_TYPE, "/", "%D", "\\{\"elapsedTime\":\"\\d+\"\\}"}); + parameterSets.add(new Object[] {"pct-F", TEXT_TYPE, "/", "%F", "\\d+"}); + parameterSets.add(new Object[] {"pct-F", JSON_TYPE, "/", "%F", "\\{\"firstByteTime\":\"\\d+\"\\}"}); + parameterSets.add(new Object[] {"pct-h", TEXT_TYPE, "/", "%h", LOCAL_IP_PATTERN}); + parameterSets.add(new Object[] {"pct-h", JSON_TYPE, "/", "%h", "\\{\"host\":\"" + LOCAL_IP_PATTERN + "\"\\}"}); + parameterSets.add(new Object[] {"pct-H", TEXT_TYPE, "/", "%H", "HTTP/1.1"}); + parameterSets.add(new Object[] {"pct-H", JSON_TYPE, "/", "%H", "\\{\"protocol\":\"HTTP/1.1\"\\}"}); + parameterSets.add(new Object[] {"pct-I", TEXT_TYPE, "/", "%I", "http-nio2?-" + LOCAL_IP_PATTERN + "-auto-\\d+-exec-\\d+"}); + parameterSets.add(new Object[] {"pct-I", JSON_TYPE, "/", "%I", "\\{\"threadName\":\"http-nio2?-" + LOCAL_IP_PATTERN + "-auto-\\d+-exec-\\d+\"\\}"}); + parameterSets.add(new Object[] {"pct-l", TEXT_TYPE, "/", "%l", "-"}); + parameterSets.add(new Object[] {"pct-l", JSON_TYPE, "/", "%l", "\\{\"logicalUserName\":\"-\"\\}"}); + parameterSets.add(new Object[] {"pct-m", TEXT_TYPE, "/", "%m", "GET"}); + parameterSets.add(new Object[] {"pct-m", JSON_TYPE, "/", "%m", "\\{\"method\":\"GET\"\\}"}); + parameterSets.add(new Object[] {"pct-p", TEXT_TYPE, "/", "%p", "\\d+"}); + parameterSets.add(new Object[] {"pct-p", JSON_TYPE, "/", "%p", "\\{\"port\":\"\\d+\"\\}"}); + parameterSets.add(new Object[] {"pct-q", TEXT_TYPE, "/?data=123", "%q", "\\?data=123"}); + parameterSets.add(new Object[] {"pct-q", JSON_TYPE, "/?data=123", "%q", "\\{\"query\":\"\\?data=123\"\\}"}); + parameterSets.add(new Object[] {"pct-r", TEXT_TYPE, "/", "%r", "GET / HTTP/1.1"}); + parameterSets.add(new Object[] {"pct-r", JSON_TYPE, "/", "%r", "\\{\"request\":\"GET / HTTP/1.1\"\\}"}); + parameterSets.add(new Object[] {"pct-s", TEXT_TYPE, "/", "%s", "200"}); + parameterSets.add(new Object[] {"pct-s", JSON_TYPE, "/", "%s", "\\{\"statusCode\":\"200\"\\}"}); + parameterSets.add(new Object[] {"pct-S", TEXT_TYPE, "/", "%S", "[A-F0-9]{32}"}); + parameterSets.add(new Object[] {"pct-S", JSON_TYPE, "/", "%S", "\\{\"sessionId\":\"[A-F0-9]{32}\"\\}"}); + parameterSets.add(new Object[] {"pct-t", TEXT_TYPE, "/", "%t", DATE_PATTERN}); + parameterSets.add(new Object[] {"pct-t", JSON_TYPE, "/", "%t", "\\{\"time\":\"" + DATE_PATTERN + "\"\\}"}); + parameterSets.add(new Object[] {"pct-T", TEXT_TYPE, "/", "%T", "\\d+"}); + parameterSets.add(new Object[] {"pct-T", JSON_TYPE, "/", "%T", "\\{\"elapsedTimeS\":\"\\d+\"\\}"}); + parameterSets.add(new Object[] {"pct-u", TEXT_TYPE, "/", "%u", "-"}); + parameterSets.add(new Object[] {"pct-u", JSON_TYPE, "/", "%u", "\\{\"user\":\"-\"\\}"}); + parameterSets.add(new Object[] {"pct-U", TEXT_TYPE, "/", "%U", "/"}); + parameterSets.add(new Object[] {"pct-U", JSON_TYPE, "/", "%U", "\\{\"path\":\"/\"\\}"}); + parameterSets.add(new Object[] {"pct-v", TEXT_TYPE, "/", "%v", "localhost"}); + parameterSets.add(new Object[] {"pct-v", JSON_TYPE, "/", "%v", "\\{\"localServerName\":\"localhost\"\\}"}); + parameterSets.add(new Object[] {"pct-X", TEXT_TYPE, "/", "%X", "\\+"}); + parameterSets.add(new Object[] {"pct-X", JSON_TYPE, "/", "%X", "\\{\"connectionStatus\":\"\\+\"\\}"}); + parameterSets.add(new Object[] {"pct-a-remote", TEXT_TYPE, "/", "%{remote}a", LOCAL_IP_PATTERN}); + parameterSets.add(new Object[] {"pct-a-remote", JSON_TYPE, "/", "%{remote}a", "\\{\"remoteAddr-remote\":\"" + LOCAL_IP_PATTERN + "\"\\}"}); + parameterSets.add(new Object[] {"pct-a-peer", TEXT_TYPE, "/", "%{peer}a", LOCAL_IP_PATTERN}); + parameterSets.add(new Object[] {"pct-a-peer", JSON_TYPE, "/", "%{peer}a", "\\{\"remoteAddr-peer\":\"" + LOCAL_IP_PATTERN + "\"\\}"}); + parameterSets.add(new Object[] {"pct-p-local", TEXT_TYPE, "/", "%{local}p", "\\d+"}); + parameterSets.add(new Object[] {"pct-p-local", JSON_TYPE, "/", "%{local}p", "\\{\"port-local\":\"\\d+\"\\}"}); + parameterSets.add(new Object[] {"pct-p-remote", TEXT_TYPE, "/", "%{remote}p", "\\d+"}); + parameterSets.add(new Object[] {"pct-p-remote", JSON_TYPE, "/", "%{remote}p", "\\{\"port-remote\":\"\\d+\"\\}"}); + parameterSets.add(new Object[] {"pct-t-sec", TEXT_TYPE, "/", "%{sec}t", "\\d{10}"}); + parameterSets.add(new Object[] {"pct-t-sec", JSON_TYPE, "/", "%{sec}t", "\\{\"time-sec\":\"\\d{10}\"\\}"}); + parameterSets.add(new Object[] {"pct-t-msec", TEXT_TYPE, "/", "%{msec}t", "\\d{13}"}); + parameterSets.add(new Object[] {"pct-t-msec", JSON_TYPE, "/", "%{msec}t", "\\{\"time-msec\":\"\\d{13}\"\\}"}); + parameterSets.add(new Object[] {"pct-t-msec_frac", TEXT_TYPE, "/", "%{msec_frac}t", "\\d{3}"}); + parameterSets.add(new Object[] {"pct-t-msec_frac", JSON_TYPE, "/", "%{msec_frac}t", "\\{\"time-msec_frac\":\"\\d{3}\"\\}"}); + parameterSets.add(new Object[] {"pct-t-begin:sec", TEXT_TYPE, "/", "%{begin:sec}t", "\\d{10}"}); + parameterSets.add(new Object[] {"pct-t-begin:sec", JSON_TYPE, "/", "%{begin:sec}t", "\\{\"time-begin:sec\":\"\\d{10}\"\\}"}); + parameterSets.add(new Object[] {"pct-t-end:sec", TEXT_TYPE, "/", "%{end:sec}t", "\\d{10}"}); + parameterSets.add(new Object[] {"pct-t-end:sec", JSON_TYPE, "/", "%{end:sec}t", "\\{\"time-end:sec\":\"\\d{10}\"\\}"}); + parameterSets.add(new Object[] {"pct-t-begin:umlaut_time_S", TEXT_TYPE, "/", "%{begin:'\u00c4'HH '\u00e4' mm '\u00f6' ss '\u00fc' SSS'\u00dc'}t", "\\\\u00c4\\d\\d \\\\u00e4 \\d\\d \\\\u00f6 \\d\\d \\\\u00fc \\d\\d\\d\\\\u00dc"}); + parameterSets.add(new Object[] {"pct-t-begin:umlaut_time_S", JSON_TYPE, "/", "%{begin:'\u00c4'HH '\u00e4' mm '\u00f6' ss '\u00fc' SSS'\u00dc'}t", "\\{\"time-begin:'\u00c4'HH '\u00e4' mm '\u00f6' ss '\u00fc' SSS'\u00dc'\":\"\\\\u00c4\\d\\d \\\\u00e4 \\d\\d \\\\u00f6 \\d\\d \\\\u00fc \\d\\d\\d\\\\u00dc\"\\}"}); + parameterSets.add(new Object[] {"common", TEXT_TYPE, "/", "common", + LOCAL_IP_PATTERN + " - - " + DATE_PATTERN + " \"GET / HTTP/1.1\" 200 " + BYTES}); + parameterSets.add(new Object[] {"common", JSON_TYPE, "/", "common", + "\\{\"host\":\"" + LOCAL_IP_PATTERN + "\",\"logicalUserName\":\"-\",\"user\":\"-\",\"time\":\"" + DATE_PATTERN + + "\",\"request\":\"GET / HTTP/1.1\",\"statusCode\":\"200\",\"size\":\"" + BYTES + "\"\\}"}); + parameterSets.add(new Object[] {"combined", TEXT_TYPE, "/", "combined", + LOCAL_IP_PATTERN + " - - " + DATE_PATTERN + " \"GET / HTTP/1.1\" 200 " + BYTES + " \"-\" \"" + UA_PATTERN + "\""}); + parameterSets.add(new Object[] {"combined", JSON_TYPE, "/", "combined", + "\\{\"host\":\"" + LOCAL_IP_PATTERN + "\",\"logicalUserName\":\"-\",\"user\":\"-\",\"time\":\"" + DATE_PATTERN + + "\",\"request\":\"GET / HTTP/1.1\",\"statusCode\":\"200\",\"size\":\"" + BYTES + "\"" + + ",\"requestHeaders\": \\{\"Referer\":\"-\",\"User-Agent\":\"" + UA_PATTERN + "\"\\}\\}"}); + parameterSets.add(new Object[] {"verbatim-text", TEXT_TYPE, "/", "123\u00e4\u00f6%s\u00fc%b%D\u00dc456", "123\u00e4\u00f6200\u00fc" + BYTES + "\\d+\u00dc456"}); + parameterSets.add(new Object[] {"verbatim-text", JSON_TYPE, "/", "123\u00e4\u00f6%s\u00fc%b%D\u00dc456", "\\{\"statusCode\":\"200\",\"size\":\"" + BYTES + "\",\"elapsedTime\":\"\\d+\"\\}"}); + parameterSets.add(new Object[] {"merged-cookies", TEXT_TYPE, "/", "%{Cookie}i", "COOKIE-1_1=1_1; COOKIE-1_2=1_2; COOKIE-1_3=1_3,COOKIE-2_1=2_1;COOKIE-2_2=2_2;COOKIE-2_3=2_3"}); + parameterSets.add(new Object[] {"merged-cookies", JSON_TYPE, "/", "%{Cookie}i", "\\{\"requestHeaders\": \\{\"Cookie\":\"COOKIE-1_1=1_1; COOKIE-1_2=1_2; COOKIE-1_3=1_3,COOKIE-2_1=2_1;COOKIE-2_2=2_2;COOKIE-2_3=2_3\"\\}\\}"}); + parameterSets.add(new Object[] {"cookie", TEXT_TYPE, "/", "%{COOKIE-2_2}c", "2_2"}); + parameterSets.add(new Object[] {"cookie", JSON_TYPE, "/", "%{COOKIE-2_2}c", "\\{\"cookies\": \\{\"COOKIE-2_2\":\"2_2\"\\}\\}"}); + parameterSets.add(new Object[] {"request-header", TEXT_TYPE, "/", "%{" + REQUEST_HEADER + "}i", REQUEST_HEADER_VALUE_ENCODED}); + parameterSets.add(new Object[] {"request-header", JSON_TYPE, "/", "%{" + REQUEST_HEADER + "}i", "\\{\"requestHeaders\": \\{\"" + REQUEST_HEADER + "\":\"" + REQUEST_HEADER_VALUE_ENCODED + "\"\\}\\}"}); + parameterSets.add(new Object[] {"response-header", TEXT_TYPE, "/", "%{" + RESPONSE_HEADER + "}o", RESPONSE_HEADER_VALUE_ENCODED}); + parameterSets.add(new Object[] {"response-header", JSON_TYPE, "/", "%{" + RESPONSE_HEADER + "}o", "\\{\"responseHeaders\": \\{\"" + RESPONSE_HEADER + "\":\"" + RESPONSE_HEADER_VALUE_ENCODED + "\"\\}\\}"}); + parameterSets.add(new Object[] {"request-attribute", TEXT_TYPE, "/", "%{" + REQUEST_ATTRIBUTE + "}r", REQUEST_ATTRIBUTE_VALUE_ENCODED}); + parameterSets.add(new Object[] {"request-attribute", JSON_TYPE, "/", "%{" + REQUEST_ATTRIBUTE + "}r", "\\{\"requestAttributes\": \\{\"" + REQUEST_ATTRIBUTE + "\":\"" + REQUEST_ATTRIBUTE_VALUE_ENCODED + "\"\\}\\}"}); + parameterSets.add(new Object[] {"session-attribute", TEXT_TYPE, "/", "%{" + SESSION_ATTRIBUTE + "}s", SESSION_ATTRIBUTE_VALUE_ENCODED}); + parameterSets.add(new Object[] {"session-attribute", JSON_TYPE, "/", "%{" + SESSION_ATTRIBUTE + "}s", "\\{\"sessionAttributes\": \\{\"" + SESSION_ATTRIBUTE + "\":\"" + SESSION_ATTRIBUTE_VALUE_ENCODED + "\"\\}\\}"}); + + return parameterSets; + } + + @Parameter(0) + public String name; + + @Parameter(1) + public String type; + + @Parameter(2) + public String path; + + @Parameter(3) + public String logPattern; + + @Parameter(4) + public String resultMatch; + + /** + * Extend AbstractAccessLogValve to retrieve log output. + */ + public final class TesterAccessLogValve extends AbstractAccessLogValve { + + private CharArrayWriter writer; + + public TesterAccessLogValve(CharArrayWriter writer) { + this.writer = writer; + } + + /** + * Log the specified message to the log file, switching files if + * the date has changed since the previous log call. + * + * @param message Message to be logged + */ + @Override + public void log(CharArrayWriter message) { + try { + message.writeTo(writer); + } catch (IOException ex) { + log.error("Could not write to writer", ex); + } + } + } + + /** + * Extend JsonAccessLogValve to retrieve log output. + */ + public final class TesterJsonAccessLogValve extends JsonAccessLogValve { + + private CharArrayWriter writer; + + public TesterJsonAccessLogValve(CharArrayWriter writer) { + this.writer = writer; + } + + /** + * Log the specified message to the log file, switching files if + * the date has changed since the previous log call. + * + * @param message Message to be logged + */ + @Override + public void log(CharArrayWriter message) { + try { + message.writeTo(writer); + } catch (IOException ex) { + log.error("Could not write to writer", ex); + } + } + } + + private static class TesterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.setAttribute(REQUEST_ATTRIBUTE, REQUEST_ATTRIBUTE_VALUE); + HttpSession session = req.getSession(); + session.setAttribute(SESSION_ATTRIBUTE, SESSION_ATTRIBUTE_VALUE); + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.setHeader(RESPONSE_HEADER, RESPONSE_HEADER_VALUE); + PrintWriter pw = resp.getWriter(); + pw.print(RESPONSE); + } + } + + @Test + public void test() throws LifecycleException, IOException { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + + // Map the test Servlet + TesterServlet servlet = new TesterServlet(); + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/", "servlet"); + + CharArrayWriter writer = new CharArrayWriter(); + if (TEXT_TYPE.equals(type)) { + TesterAccessLogValve valve = new TesterAccessLogValve(writer); + valve.setPattern(logPattern); + tomcat.getHost().getPipeline().addValve(valve); + } else if (JSON_TYPE.equals(type)) { + TesterJsonAccessLogValve valve = new TesterJsonAccessLogValve(writer); + valve.setPattern(logPattern); + tomcat.getHost().getPipeline().addValve(valve); + } else { + log.error("Unknown AccessLogValve type " + type); + Assert.fail("Unknown AccessLogValve type " + type); + } + + tomcat.start(); + + String url = "http://localhost:" + getPort() + path; + ByteChunk out = new ByteChunk(); + Map> reqHead = new HashMap<>(); + Map> resHead = new HashMap<>(); + List cookieHeaders = new ArrayList<>(); + cookieHeaders.add("COOKIE-1_1=1_1; COOKIE-1_2=1_2; COOKIE-1_3=1_3"); + cookieHeaders.add("COOKIE-2_1=2_1;COOKIE-2_2=2_2;COOKIE-2_3=2_3"); + reqHead.put("Cookie", cookieHeaders); + List testHeaders = new ArrayList<>(); + testHeaders.add(REQUEST_HEADER_VALUE); + reqHead.put(REQUEST_HEADER, testHeaders); + int status = getUrl(url, out, reqHead, resHead); + Assert.assertEquals(HttpServletResponse.SC_OK, status); + long startWait = System.currentTimeMillis(); + String result = writer.toString(); + while ("".equals(result) && System.currentTimeMillis() - startWait < SLEEP_MAX) { + try { + Thread.sleep(SLEEP); + } catch (InterruptedException ex) { + log.error("Exception during sleep", ex); + } + result = writer.toString(); + } + Assert.assertFalse("Access log line empty after " + (System.currentTimeMillis() - startWait) + " milliseconds", "".equals(result)); + boolean matches = Pattern.matches(resultMatch, result); + if (!matches) { + log.error("Resulting log line '" + result + "' does not match '" + resultMatch + "'"); + } + Assert.assertTrue("Resulting log line '" + result + "' does not match '" + resultMatch + "'", matches); + + if (JSON_TYPE.equals(type)) { + JSONParser parser = new JSONParser(result); + try { + parser.parse(); + } catch (ParseException ex) { + log.error("Exception during Json result parsing", ex); + Assert.fail("Could not parse Json result"); + } + } + } + +} diff --git a/test/org/apache/catalina/valves/TestAccessLogValveDateFormatCache.java b/test/org/apache/catalina/valves/TestAccessLogValveDateFormatCache.java new file mode 100644 index 0000000..171f5cf --- /dev/null +++ b/test/org/apache/catalina/valves/TestAccessLogValveDateFormatCache.java @@ -0,0 +1,91 @@ +/* + * 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.catalina.valves; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAccessLogValveDateFormatCache { + + // Note that there is a similar test: + // org.apache.juli.TestDateFormatCache.testBug54044() + @Test + public void testBug54044() throws Exception { + + final int cacheSize = 10; + + SimpleDateFormat sdf = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss Z]", Locale.US); + sdf.setTimeZone(TimeZone.getDefault()); + + AccessLogValve.DateFormatCache dfc = new AccessLogValve.DateFormatCache(cacheSize, Locale.US, null); + + // Create an array to hold the expected values + String[] expected = new String[cacheSize]; + + // Fill the cache & populate the expected values + for (int secs = 0; secs < (cacheSize); secs++) { + dfc.getFormat(secs * 1000); + expected[secs] = generateExpected(sdf, secs); + } + Assert.assertArrayEquals(expected, dfc.cLFCache.cache); + + + // Cause the cache to roll-around by one and then confirm + dfc.getFormat(cacheSize * 1000); + expected[0] = generateExpected(sdf, cacheSize); + Assert.assertArrayEquals(expected, dfc.cLFCache.cache); + + // Jump 2 ahead and then confirm (skipped value should be null) + dfc.getFormat((cacheSize + 2) * 1000); + expected[1] = null; + expected[2] = generateExpected(sdf, cacheSize + 2); + Assert.assertArrayEquals(expected, dfc.cLFCache.cache); + + // Back 1 to fill in the gap + dfc.getFormat((cacheSize + 1) * 1000); + expected[1] = generateExpected(sdf, cacheSize + 1); + Assert.assertArrayEquals(expected, dfc.cLFCache.cache); + + // Return to 1 and confirm skipped value is null + dfc.getFormat(1 * 1000); + expected[1] = generateExpected(sdf, 1); + expected[2] = null; + Assert.assertArrayEquals(expected, dfc.cLFCache.cache); + + // Go back one further + dfc.getFormat(0); + expected[0] = generateExpected(sdf, 0); + Assert.assertArrayEquals(expected, dfc.cLFCache.cache); + + // Jump ahead far enough that the entire cache will need to be cleared + dfc.getFormat(42 * 1000); + for (int i = 0; i < cacheSize; i++) { + expected[i] = null; + } + expected[0] = generateExpected(sdf, 42); + Assert.assertArrayEquals(expected, dfc.cLFCache.cache); + } + + private String generateExpected(SimpleDateFormat sdf, long secs) { + return sdf.format(new Date(secs * 1000)); + } +} diff --git a/test/org/apache/catalina/valves/TestCrawlerSessionManagerValve.java b/test/org/apache/catalina/valves/TestCrawlerSessionManagerValve.java new file mode 100644 index 0000000..9c535ca --- /dev/null +++ b/test/org/apache/catalina/valves/TestCrawlerSessionManagerValve.java @@ -0,0 +1,214 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionBindingListener; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Host; +import org.apache.catalina.Manager; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.session.StandardManager; +import org.apache.catalina.session.StandardSession; +import org.easymock.EasyMock; +import org.easymock.IExpectationSetters; + +public class TestCrawlerSessionManagerValve { + + private static final Manager TEST_MANAGER; + + static { + TEST_MANAGER = new StandardManager(); + TEST_MANAGER.setContext(new StandardContext()); + } + + + @Test + public void testCrawlerIpsPositive() throws Exception { + CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); + valve.setCrawlerIps("216\\.58\\.206\\.174"); + valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); + valve.setNext(EasyMock.createMock(Valve.class)); + HttpSession session = createSessionExpectations(valve, true); + Request request = createRequestExpectations("216.58.206.174", session, true); + + EasyMock.replay(request, session); + + valve.invoke(request, EasyMock.createMock(Response.class)); + + EasyMock.verify(request, session); + } + + @Test + public void testCrawlerIpsNegative() throws Exception { + CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); + valve.setCrawlerIps("216\\.58\\.206\\.174"); + valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); + valve.setNext(EasyMock.createMock(Valve.class)); + HttpSession session = createSessionExpectations(valve, false); + Request request = createRequestExpectations("127.0.0.1", session, false); + + EasyMock.replay(request, session); + + valve.invoke(request, EasyMock.createMock(Response.class)); + + EasyMock.verify(request, session); + } + + @Test + public void testCrawlerMultipleHostsHostAware() throws Exception { + CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); + valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); + valve.setHostAware(true); + valve.setContextAware(true); + valve.setNext(EasyMock.createMock(Valve.class)); + + verifyCrawlingLocalhost(valve, "localhost"); + verifyCrawlingLocalhost(valve, "example.invalid"); + } + + @Test + public void testCrawlerMultipleContextsContextAware() throws Exception { + CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); + valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); + valve.setHostAware(true); + valve.setContextAware(true); + valve.setNext(EasyMock.createMock(Valve.class)); + + verifyCrawlingContext(valve, "/examples"); + verifyCrawlingContext(valve, null); + } + + @Test + public void testCrawlersSessionIdIsRemovedAfterSessionExpiry() throws IOException, ServletException { + CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); + valve.setCrawlerIps("216\\.58\\.206\\.174"); + valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); + valve.setNext(EasyMock.createMock(Valve.class)); + valve.setSessionInactiveInterval(0); + StandardSession session = new StandardSession(TEST_MANAGER); + session.setId("id"); + session.setValid(true); + + Request request = createRequestExpectations("216.58.206.174", session, true); + + EasyMock.replay(request); + + valve.invoke(request, EasyMock.createMock(Response.class)); + + EasyMock.verify(request); + + MatcherAssert.assertThat(valve.getClientIpSessionId().values(), CoreMatchers.hasItem("id")); + + session.expire(); + + Assert.assertEquals(0, valve.getClientIpSessionId().values().size()); + } + + + private void verifyCrawlingLocalhost(CrawlerSessionManagerValve valve, String hostname) + throws IOException, ServletException { + HttpSession session = createSessionExpectations(valve, true); + Request request = createRequestExpectations("127.0.0.1", session, true, hostname, "/examples", "tomcatBot 1.0"); + + EasyMock.replay(request, session); + + valve.invoke(request, EasyMock.createMock(Response.class)); + + EasyMock.verify(request, session); + } + + + private void verifyCrawlingContext(CrawlerSessionManagerValve valve, String contextPath) + throws IOException, ServletException { + HttpSession session = createSessionExpectations(valve, true); + Request request = createRequestExpectations("127.0.0.1", session, true, "localhost", contextPath, + "tomcatBot 1.0"); + + EasyMock.replay(request, session); + + valve.invoke(request, EasyMock.createMock(Response.class)); + + EasyMock.verify(request, session); + } + + + private HttpSession createSessionExpectations(CrawlerSessionManagerValve valve, boolean isBot) { + HttpSession session = EasyMock.createMock(HttpSession.class); + if (isBot) { + EasyMock.expect(session.getId()).andReturn("id").times(2); + session.setAttribute(EasyMock.eq(valve.getClass().getName()), + EasyMock.anyObject(HttpSessionBindingListener.class)); + EasyMock.expectLastCall(); + session.setMaxInactiveInterval(60); + EasyMock.expectLastCall(); + } + return session; + } + + + private Request createRequestExpectations(String ip, HttpSession session, boolean isBot) { + return createRequestExpectations(ip, session, isBot, "localhost", "/examples", "something 1.0"); + } + + private Request createRequestExpectations(String ip, HttpSession session, boolean isBot, String hostname, + String contextPath, String userAgent) { + Request request = EasyMock.createMock(Request.class); + EasyMock.expect(request.getRemoteAddr()).andReturn(ip); + EasyMock.expect(request.getHost()).andReturn(simpleHostWithName(hostname)); + EasyMock.expect(request.getContext()).andReturn(simpleContextWithName(contextPath)); + IExpectationSetters setter = EasyMock.expect(request.getSession(false)).andReturn(null); + if (isBot) { + setter.andReturn(session); + } + EasyMock.expect(request.getHeaders("user-agent")) + .andAnswer(() -> Collections.enumeration(Arrays.asList(userAgent))); + return request; + } + + private Host simpleHostWithName(String hostname) { + Host host = EasyMock.createMock(Host.class); + EasyMock.expect(host.getName()).andReturn(hostname); + EasyMock.replay(host); + return host; + } + + private Context simpleContextWithName(String contextPath) { + if (contextPath == null) { + return null; + } + Context context = EasyMock.createMock(Context.class); + EasyMock.expect(context.getName()).andReturn(contextPath); + EasyMock.replay(context); + return context; + } +} diff --git a/test/org/apache/catalina/valves/TestErrorReportValve.java b/test/org/apache/catalina/valves/TestErrorReportValve.java new file mode 100644 index 0000000..d4bdee4 --- /dev/null +++ b/test/org/apache/catalina/valves/TestErrorReportValve.java @@ -0,0 +1,266 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.res.StringManager; + +public class TestErrorReportValve extends TomcatBaseTest { + + private static final StringManager sm = StringManager.getManager(TestErrorReportValve.class); + + @Test + public void testBug53071() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "errorServlet", new ErrorServlet()); + ctx.addServletMappingDecoded("/", "errorServlet"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + res.setCharset(StandardCharsets.UTF_8); + getUrl("http://localhost:" + getPort(), res, null); + Assert.assertTrue(res.toString().contains( + "

    " + sm.getString("errorReportValve.message") + " " + ErrorServlet.ERROR_TEXT + "

    ")); + } + + + private static final class ErrorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final String ERROR_TEXT = "The wheels fell off."; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.setAttribute(RequestDispatcher.ERROR_EXCEPTION, new Throwable(ERROR_TEXT)); + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + + @Test + public void testBug54220DoNotSetNotFound() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "bug54220", new Bug54220Servlet(false)); + ctx.addServletMappingDecoded("/", "bug54220"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertNull(res.toString()); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + + + @Test + public void testBug54220SetNotFound() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "bug54220", new Bug54220Servlet(true)); + ctx.addServletMappingDecoded("/", "bug54220"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertNull(res.toString()); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + } + + + private static final class Bug54220Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private boolean setNotFound; + + private Bug54220Servlet(boolean setNotFound) { + this.setNotFound = setNotFound; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + if (setNotFound) { + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } + } + + + /* + * Custom error/status codes should not result in a blank response. + */ + @Test + public void testBug54536() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "bug54536", new Bug54536Servlet()); + ctx.addServletMappingDecoded("/", "bug54536"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(Bug54536Servlet.ERROR_STATUS, rc); + String body = res.toString(); + Assert.assertNotNull(body); + Assert.assertTrue(body, body.contains(Bug54536Servlet.ERROR_MESSAGE)); + } + + + private static final class Bug54536Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final int ERROR_STATUS = 999; + private static final String ERROR_MESSAGE = "The sky is falling"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.sendError(ERROR_STATUS, ERROR_MESSAGE); + } + } + + @Test + public void testBug56042() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Bug56042Servlet bug56042Servlet = new Bug56042Servlet(); + Wrapper wrapper = Tomcat.addServlet(ctx, "bug56042Servlet", bug56042Servlet); + wrapper.setAsyncSupported(true); + ctx.addServletMappingDecoded("/bug56042Servlet", "bug56042Servlet"); + + tomcat.start(); + + StringBuilder url = new StringBuilder(48); + url.append("http://localhost:"); + url.append(getPort()); + url.append("/bug56042Servlet"); + + ByteChunk res = new ByteChunk(); + int rc = getUrl(url.toString(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_BAD_REQUEST, rc); + } + + private static class Bug56042Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Only set the status on the first call (the dispatch will trigger + // another call to this Servlet) + if (resp.getStatus() != HttpServletResponse.SC_BAD_REQUEST) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + AsyncContext ac = req.startAsync(); + ac.dispatch(); + } + } + } + + private static final class ExceptionServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void service(ServletRequest request, ServletResponse response) throws IOException { + throw new RuntimeException(); + } + } + + + private static final class ErrorPageServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void service(ServletRequest request, ServletResponse response) throws IOException { + response.getWriter().print("OK"); + } + } + + + @Test + public void testErrorPageServlet() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "exception", new ExceptionServlet()); + ctx.addServletMappingDecoded("/exception", "exception"); + Tomcat.addServlet(ctx, "erropage", new ErrorPageServlet()); + ctx.addServletMappingDecoded("/erropage", "erropage"); + ErrorPage errorPage = new ErrorPage(); + errorPage.setErrorCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + errorPage.setLocation("/erropage"); + ctx.addErrorPage(errorPage); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/exception", res, null); + + Assert.assertEquals(res.toString(), "OK"); + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + +} diff --git a/test/org/apache/catalina/valves/TestExtendedAccessLogValve.java b/test/org/apache/catalina/valves/TestExtendedAccessLogValve.java new file mode 100644 index 0000000..fb3377e --- /dev/null +++ b/test/org/apache/catalina/valves/TestExtendedAccessLogValve.java @@ -0,0 +1,63 @@ +/* + * 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.catalina.valves; + +import org.junit.Assert; +import org.junit.Test; + +public class TestExtendedAccessLogValve { + + @Test + public void alpha() { + Assert.assertEquals("\"foo\"", ExtendedAccessLogValve.wrap("foo")); + } + + @Test + public void testNull() { + Assert.assertEquals("-", ExtendedAccessLogValve.wrap(null)); + } + + @Test + public void empty() { + Assert.assertEquals("\"\"", ExtendedAccessLogValve.wrap("")); + } + + @Test + public void singleQuoteMiddle() { + Assert.assertEquals("\"foo'bar\"", ExtendedAccessLogValve.wrap("foo'bar")); + } + + @Test + public void doubleQuoteMiddle() { + Assert.assertEquals("\"foo\"\"bar\"", ExtendedAccessLogValve.wrap("foo\"bar")); + } + + @Test + public void doubleQuoteStart() { + Assert.assertEquals("\"\"\"foobar\"", ExtendedAccessLogValve.wrap("\"foobar")); + } + + @Test + public void doubleQuoteEnd() { + Assert.assertEquals("\"foobar\"\"\"", ExtendedAccessLogValve.wrap("foobar\"")); + } + + @Test + public void doubleQuote() { + Assert.assertEquals("\"\"\"\"", ExtendedAccessLogValve.wrap("\"")); + } +} diff --git a/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java b/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java new file mode 100644 index 0000000..b2b1fa0 --- /dev/null +++ b/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java @@ -0,0 +1,341 @@ +/* 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.catalina.valves; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.http.Cookie; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.core.StandardPipeline; +import org.apache.tomcat.util.descriptor.web.Constants; +import org.easymock.EasyMock; +import org.easymock.IMocksControl; + +@RunWith(Parameterized.class) +public class TestLoadBalancerDrainingValve { + + @Parameters(name = "{index}: activation[{0}], validSessionID[{1}], expectInvokeNext[{2}], enableIgnore[{3}], " + + "queryString[{4}]") + public static Collection parameters() { + + String[] jkActivations = new String[] { "ACT", "DIS" }; + Boolean[] booleans = new Boolean[] { Boolean.TRUE, Boolean.FALSE }; + String[] queryStrings = new String[] { null, "foo=bar" }; + + List parameterSets = new ArrayList<>(); + for (String jkActivation : jkActivations) { + for (Boolean validSessionId : booleans) { + for (Boolean enableIgnore : booleans) { + Boolean expectInvokeNext = Boolean.valueOf( + "ACT".equals(jkActivation) || enableIgnore.booleanValue() || validSessionId.booleanValue()); + for (String queryString : queryStrings) { + for (Boolean secureRequest : booleans) { + for (Boolean secureSessionConfig : booleans) { + parameterSets.add(new Object[] { jkActivation, validSessionId, expectInvokeNext, + enableIgnore, queryString, secureRequest, secureSessionConfig }); + } + } + } + } + } + } + return parameterSets; + } + + + @Parameter(0) + public String jkActivation; + + @Parameter(1) + public boolean validSessionId; + + @Parameter(2) + public boolean expectInvokeNext; + + @Parameter(3) + public boolean enableIgnore; + + @Parameter(4) + public String queryString; + + @Parameter(5) + public Boolean secureRequest; + + @Parameter(6) + public boolean secureSessionConfig; + + + @Test + public void runValve() throws Exception { + IMocksControl control = EasyMock.createControl(); + ServletContext servletContext = control.createMock(ServletContext.class); + Context ctx = control.createMock(Context.class); + Request request = control.createMock(Request.class); + Response response = control.createMock(Response.class); + + String sessionCookieName = "JSESSIONID"; + String sessionId = "cafebabe"; + String requestURI = "/test/path"; + SessionCookieConfig cookieConfig = new CookieConfig(); + cookieConfig.setDomain("example.com"); + cookieConfig.setName(sessionCookieName); + cookieConfig.setPath("/"); + cookieConfig.setSecure(secureSessionConfig); + + // Valve.init requires all of this stuff + EasyMock.expect(ctx.getMBeanKeyProperties()).andStubReturn(""); + EasyMock.expect(ctx.getName()).andStubReturn(""); + EasyMock.expect(ctx.getPipeline()).andStubReturn(new StandardPipeline()); + EasyMock.expect(ctx.getDomain()).andStubReturn("foo"); + EasyMock.expect(ctx.getLogger()) + .andStubReturn(org.apache.juli.logging.LogFactory.getLog(LoadBalancerDrainingValve.class)); + EasyMock.expect(ctx.getServletContext()).andStubReturn(servletContext); + + // Set up the actual test + EasyMock.expect(request.getAttribute(LoadBalancerDrainingValve.ATTRIBUTE_KEY_JK_LB_ACTIVATION)) + .andStubReturn(jkActivation); + EasyMock.expect(Boolean.valueOf(request.isRequestedSessionIdValid())) + .andStubReturn(Boolean.valueOf(validSessionId)); + + ArrayList cookies = new ArrayList<>(); + if (enableIgnore) { + cookies.add(new Cookie("ignore", "true")); + } + + if (!validSessionId && jkActivation.equals("DIS")) { + MyCookie cookie = new MyCookie(cookieConfig.getName(), sessionId); + cookie.setPath(cookieConfig.getPath()); + cookie.setValue(sessionId); + + cookies.add(cookie); + + EasyMock.expect(request.getRequestedSessionId()).andStubReturn(sessionId); + EasyMock.expect(request.getRequestURI()).andStubReturn(requestURI); + EasyMock.expect(request.getCookies()).andStubReturn(cookies.toArray(new Cookie[0])); + EasyMock.expect(request.getContext()).andStubReturn(ctx); + EasyMock.expect(ctx.getSessionCookieName()).andStubReturn(sessionCookieName); + EasyMock.expect(servletContext.getSessionCookieConfig()).andStubReturn(cookieConfig); + EasyMock.expect(request.getQueryString()).andStubReturn(queryString); + EasyMock.expect(ctx.getSessionCookiePath()).andStubReturn("/"); + + if (!enableIgnore) { + EasyMock.expect(Boolean.valueOf(ctx.getSessionCookiePathUsesTrailingSlash())) + .andStubReturn(Boolean.TRUE); + EasyMock.expect(request.getQueryString()).andStubReturn(queryString); + // Response will have cookie deleted + MyCookie expectedCookie = new MyCookie(cookieConfig.getName(), ""); + expectedCookie.setPath(cookieConfig.getPath()); + expectedCookie.setMaxAge(0); + + EasyMock.expect(Boolean.valueOf(request.isSecure())).andReturn(secureRequest); + + // These two lines just mean EasyMock.expect(response.addCookie) but for a void method + response.addCookie(expectedCookie); + EasyMock.expect(ctx.getSessionCookieName()).andReturn(sessionCookieName); // Indirect call + String expectedRequestURI = requestURI; + if (null != queryString) { + expectedRequestURI = expectedRequestURI + '?' + queryString; + } + response.setHeader("Location", expectedRequestURI); + response.setStatus(307); + } + } + + Valve next = control.createMock(Valve.class); + + if (expectInvokeNext) { + // Expect the "next" Valve to fire + // Next 2 lines are basically EasyMock.expect(next.invoke(req,res)) but for a void method + next.invoke(request, response); + EasyMock.expectLastCall(); + } + + // Get set to actually test + control.replay(); + + LoadBalancerDrainingValve valve = new LoadBalancerDrainingValve(); + valve.setContainer(ctx); + valve.init(); + valve.setNext(next); + valve.setIgnoreCookieName("ignore"); + valve.setIgnoreCookieValue("true"); + + valve.invoke(request, response); + + control.verify(); + } + + + private static class CookieConfig implements SessionCookieConfig { + + private String name; + + private final Map attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getDomain() { + return attributes.get(Constants.COOKIE_DOMAIN_ATTR); + } + + @Override + public void setDomain(String domain) { + attributes.put(Constants.COOKIE_DOMAIN_ATTR, domain); + } + + @Override + public String getPath() { + return attributes.get(Constants.COOKIE_PATH_ATTR); + } + + @Override + public void setPath(String path) { + attributes.put(Constants.COOKIE_PATH_ATTR, path); + } + + @Override + public String getComment() { + return null; + } + + @Override + public void setComment(String comment) { + // NO-OP + } + + @Override + public boolean isHttpOnly() { + String httpOnly = getAttribute(Constants.COOKIE_HTTP_ONLY_ATTR); + if (httpOnly == null) { + return false; + } + return Boolean.parseBoolean(httpOnly); + } + + @Override + public void setHttpOnly(boolean httpOnly) { + setAttribute(Constants.COOKIE_HTTP_ONLY_ATTR, Boolean.toString(httpOnly)); + } + + @Override + public boolean isSecure() { + String secure = getAttribute(Constants.COOKIE_SECURE_ATTR); + if (secure == null) { + return false; + } + return Boolean.parseBoolean(secure); + } + + @Override + public void setSecure(boolean secure) { + setAttribute(Constants.COOKIE_SECURE_ATTR, Boolean.toString(secure)); + } + + @Override + public int getMaxAge() { + String maxAge = getAttribute(Constants.COOKIE_MAX_AGE_ATTR); + if (maxAge == null) { + return -1; + } + return Integer.parseInt(maxAge); + } + + @Override + public void setMaxAge(int maxAge) { + setAttribute(Constants.COOKIE_MAX_AGE_ATTR, Integer.toString(maxAge)); + } + + @Override + public void setAttribute(String name, String value) { + attributes.put(name, value); + } + + @Override + public String getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Map getAttributes() { + return Collections.unmodifiableMap(attributes); + } + } + + + // A Cookie subclass that knows how to compare itself to other Cookie objects + private static class MyCookie extends Cookie { + private static final long serialVersionUID = 1L; + + MyCookie(String name, String value) { + super(name, value); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof MyCookie)) { + return false; + } + + MyCookie mc = (MyCookie) o; + return mc.getName().equals(this.getName()) && mc.getPath().equals(this.getPath()) && + mc.getValue().equals(this.getValue()) && mc.getMaxAge() == this.getMaxAge(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getMaxAge(); + result = prime * result + ((getName() == null) ? 0 : getName().hashCode()); + result = prime * result + ((getPath() == null) ? 0 : getPath().hashCode()); + result = prime * result + ((getValue() == null) ? 0 : getValue().hashCode()); + return result; + } + + + @Override + public String toString() { + return "Cookie { name=" + getName() + ", value=" + getValue() + ", path=" + getPath() + ", maxAge=" + + getMaxAge() + " }"; + } + } +} diff --git a/test/org/apache/catalina/valves/TestPatternTokenizer.java b/test/org/apache/catalina/valves/TestPatternTokenizer.java new file mode 100644 index 0000000..1b19637 --- /dev/null +++ b/test/org/apache/catalina/valves/TestPatternTokenizer.java @@ -0,0 +1,32 @@ +/* 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.catalina.valves; + +import java.io.IOException; + +import org.junit.Test; + +import org.apache.catalina.valves.ExtendedAccessLogValve.PatternTokenizer; + +public class TestPatternTokenizer { + + @Test(expected = IOException.class) + public void doUnexpectedParenthesis() throws IOException { + String input = "a)aa)"; + PatternTokenizer tokenizer = new PatternTokenizer(input); + tokenizer.getToken(); + } +} diff --git a/test/org/apache/catalina/valves/TestPersistentValve.java b/test/org/apache/catalina/valves/TestPersistentValve.java new file mode 100644 index 0000000..b50c17b --- /dev/null +++ b/test/org/apache/catalina/valves/TestPersistentValve.java @@ -0,0 +1,97 @@ +/* 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.catalina.valves; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.tomcat.unittest.TesterRequest; +import org.apache.tomcat.unittest.TesterResponse; + +public class TestPersistentValve { + + @Test + public void testSemaphore() throws Exception { + // Create the test objects + PersistentValve pv = new PersistentValve(); + Request request = new TesterRequest(); + Response response = new TesterResponse(); + TesterValve testerValve = new TesterValve(); + + // Configure the test objects + request.setRequestedSessionId("1234"); + + // Plumb the test objects together + pv.setContainer(request.getContext()); + pv.setNext(testerValve); + + // Call the necessary lifecycle methods + pv.init(); + + // Run the test + Thread[] threads = new Thread[5]; + + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + try { + pv.invoke(request, response); + } catch (IOException | ServletException e) { + throw new RuntimeException(e); + } + }); + } + + for (int i = 0; i < threads.length; i++) { + threads[i].start(); + } + + for (int i = 0; i < threads.length; i++) { + threads[i].join(); + } + + Assert.assertEquals(1, testerValve.getMaximumConcurrency()); + } + + + private static class TesterValve extends ValveBase { + + private static AtomicInteger maximumConcurrency = new AtomicInteger(); + private static AtomicInteger concurrency = new AtomicInteger(); + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + int c = concurrency.incrementAndGet(); + maximumConcurrency.getAndUpdate((v) -> c > v ? c : v); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Ignore + } + concurrency.decrementAndGet(); + } + + public int getMaximumConcurrency() { + return maximumConcurrency.get(); + } + } +} diff --git a/test/org/apache/catalina/valves/TestRemoteIpValve.java b/test/org/apache/catalina/valves/TestRemoteIpValve.java new file mode 100644 index 0000000..70bd230 --- /dev/null +++ b/test/org/apache/catalina/valves/TestRemoteIpValve.java @@ -0,0 +1,1226 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Globals; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.tomcat.util.buf.StringUtils; + +/** + * {@link RemoteIpValve} Tests + */ +public class TestRemoteIpValve { + + static class RemoteAddrAndHostTrackerValve extends ValveBase { + private String remoteAddr; + private String remoteHost; + private String scheme; + private boolean secure; + private String serverName; + private int serverPort; + private String forwardedFor; + private String forwardedBy; + + public String getRemoteAddr() { + return remoteAddr; + } + + public String getRemoteHost() { + return remoteHost; + } + + public String getScheme() { + return scheme; + } + + public String getServerName() { + return serverName; + } + + public int getServerPort() { + return serverPort; + } + + public boolean isSecure() { + return secure; + } + + public String getForwardedFor() { + return forwardedFor; + } + + public String getForwardedBy() { + return forwardedBy; + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + this.remoteHost = request.getRemoteHost(); + this.remoteAddr = request.getRemoteAddr(); + this.scheme = request.getScheme(); + this.secure = request.isSecure(); + this.serverName = request.getServerName(); + this.serverPort = request.getServerPort(); + this.forwardedFor = request.getHeader("x-forwarded-for"); + this.forwardedBy = request.getHeader("x-forwarded-by"); + } + } + + public static class MockRequest extends Request { + + public MockRequest() { + super(new Connector()); + } + + @Override + public void setAttribute(String name, Object value) { + getCoyoteRequest().getAttributes().put(name, value); + } + + @Override + public Object getAttribute(String name) { + return getCoyoteRequest().getAttribute(name); + } + } + + @Test + public void testInvokeAllowedRemoteAddrWithNullRemoteIpHeader() throws Exception { + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies("192\\.168\\.0\\.10, 192\\.168\\.0\\.11"); + remoteIpValve.setTrustedProxies("proxy1, proxy2, proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = request.getHeader("x-forwarded-for"); + Assert.assertNull("x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = request.getHeader("x-forwarded-by"); + Assert.assertNull("x-forwarded-by must be null", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "192.168.0.10", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "remote-host-original-value", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + + } + + @Test + public void testInvokeAllProxiesAreTrusted() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies("192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + remoteIpValve.setTrustedProxies("proxy1|proxy2|proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for") + .setString("140.211.11.130, proxy1, proxy2"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2", + actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrustedEmptyInternal() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies(""); + remoteIpValve.setTrustedProxies("proxy1|proxy2|proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("proxy3"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for") + .setString("140.211.11.130, proxy1, proxy2"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2,proxy3", + actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "proxy3", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrustedUnusedInternal() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setTrustedProxies("proxy1|proxy2|proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("proxy3"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for") + .setString("140.211.11.130, proxy1, proxy2"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2,proxy3", + actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "proxy3", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrustedOrInternal() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies("192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + remoteIpValve.setTrustedProxies("proxy1|proxy2|proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for") + .setString("140.211.11.130, proxy1, proxy2, 192.168.0.10, 192.168.0.11"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2", + actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreInternal() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies("192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + remoteIpValve.setTrustedProxies("proxy1|proxy2|proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for") + .setString("140.211.11.130, 192.168.0.10, 192.168.0.11"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("all proxies are internal, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = request.getHeader("x-forwarded-by"); + Assert.assertNull("all proxies are internal, x-forwarded-by must be null", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + } + + @Test + public void testInvokeAllProxiesAreTrustedAndRemoteAddrMatchRegexp() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies("127\\.0\\.0\\.1|192\\.168\\..*|another-internal-proxy"); + remoteIpValve.setTrustedProxies("proxy1|proxy2|proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("proxy1"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("proxy2"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1,proxy2", + actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + } + + @Test + public void test172dash12InternalProxies() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies( + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("172.16.0.5"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("209.244.0.3"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-proto").setString("https"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "209.244.0.3", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "209.244.0.3", actualRemoteHost); + + String actualPostInvokeRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "209.244.0.3", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + + boolean isSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertTrue("request from internal proxy should be marked secure", isSecure); + + String scheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("Scheme should be marked to https.", "https", scheme); + + request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("172.25.250.250"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("209.244.0.3"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-proto").setString("https"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor); + + actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "209.244.0.3", actualRemoteAddr); + + actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "209.244.0.3", actualRemoteHost); + + actualPostInvokeRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "209.244.0.3", actualPostInvokeRemoteAddr); + + actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + + isSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertTrue("request from internal proxy should be marked secure", isSecure); + + scheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("Scheme should be marked to https.", "https", scheme); + + + } + + + @Test + public void testInvokeXforwardedProtoSaysHttpsForIncomingHttpRequest() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130"); + // protocol + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-proto").setString("https"); + request.setSecure(false); + request.setServerPort(8080); + request.getCoyoteRequest().scheme().setString("http"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + // client ip + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("no intermediate non-trusted proxy, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = request.getHeader("x-forwarded-by"); + Assert.assertNull("no intermediate trusted proxy", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteHost); + + // protocol + String actualScheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("x-forwarded-proto says https", "https", actualScheme); + + int actualServerPort = remoteAddrAndHostTrackerValve.getServerPort(); + Assert.assertEquals("x-forwarded-proto says https", 443, actualServerPort); + + boolean actualSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertTrue("x-forwarded-proto says https", actualSecure); + + boolean actualPostInvokeSecure = request.isSecure(); + Assert.assertFalse("postInvoke secure", actualPostInvokeSecure); + + int actualPostInvokeServerPort = request.getServerPort(); + Assert.assertEquals("postInvoke serverPort", 8080, actualPostInvokeServerPort); + + String actualPostInvokeScheme = request.getScheme(); + Assert.assertEquals("postInvoke scheme", "http", actualPostInvokeScheme); + } + + @Test + public void testInvokeXforwardedProtoIsNullForIncomingHttpRequest() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130"); + // protocol + // null "x-forwarded-proto" + request.setSecure(false); + request.setServerPort(8080); + request.getCoyoteRequest().scheme().setString("http"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + // client ip + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("no intermediate non-trusted proxy, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = request.getHeader("x-forwarded-by"); + Assert.assertNull("no intermediate trusted proxy", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteHost); + + // protocol + String actualScheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("x-forwarded-proto is null", "http", actualScheme); + + int actualServerPort = remoteAddrAndHostTrackerValve.getServerPort(); + Assert.assertEquals("x-forwarded-proto is null", 8080, actualServerPort); + + boolean actualSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertFalse("x-forwarded-proto is null", actualSecure); + + boolean actualPostInvokeSecure = request.isSecure(); + Assert.assertFalse("postInvoke secure", actualPostInvokeSecure); + + int actualPostInvokeServerPort = request.getServerPort(); + Assert.assertEquals("postInvoke serverPort", 8080, actualPostInvokeServerPort); + + String actualPostInvokeScheme = request.getScheme(); + Assert.assertEquals("postInvoke scheme", "http", actualPostInvokeScheme); + } + + @Test + public void testInvokeXforwardedProtoSaysHttpForIncomingHttpsRequest() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130"); + // protocol + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-proto").setString("http"); + request.setSecure(true); + request.setServerPort(8443); + request.getCoyoteRequest().scheme().setString("https"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + // client ip + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("no intermediate non-trusted proxy, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertNull("no intermediate trusted proxy", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteHost); + + // protocol + String actualScheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("x-forwarded-proto says http", "http", actualScheme); + + int actualServerPort = remoteAddrAndHostTrackerValve.getServerPort(); + Assert.assertEquals("x-forwarded-proto says http", 80, actualServerPort); + + boolean actualSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertFalse("x-forwarded-proto says http", actualSecure); + + boolean actualPostInvokeSecure = request.isSecure(); + Assert.assertTrue("postInvoke secure", actualPostInvokeSecure); + + int actualPostInvokeServerPort = request.getServerPort(); + Assert.assertEquals("postInvoke serverPort", 8443, actualPostInvokeServerPort); + + String actualPostInvokeScheme = request.getScheme(); + Assert.assertEquals("postInvoke scheme", "https", actualPostInvokeScheme); + } + + @Test + public void testInvokeXforwardedProtoSaysMultipleHttpsForwardsForIncomingHttpsRequest() throws Exception { + performXForwardedProtoWithMultipleForwardsTest("https,https", true, true); + } + + @Test + public void testInvokeXforwardedProtoSaysMultipleForwardsWithFirstBeingHttpForIncomingHttpsRequest() + throws Exception { + performXForwardedProtoWithMultipleForwardsTest("http,https", true, false); + } + + @Test + public void testInvokeXforwardedProtoSaysMultipleForwardsWithLastBeingHttpForIncomingHttpRequest() + throws Exception { + performXForwardedProtoWithMultipleForwardsTest("https,http", false, false); + } + + @Test + public void testInvokeXforwardedProtoSaysMultipleForwardsWithMiddleBeingHttpForIncomingHttpsRequest() + throws Exception { + performXForwardedProtoWithMultipleForwardsTest("https,http,https", true, false); + } + + @Test + public void testInvokeXforwardedProtoSaysMultipleHttpForwardsForIncomingHttpRequest() throws Exception { + performXForwardedProtoWithMultipleForwardsTest("http,http", false, false); + } + + @Test + public void testInvokeXforwardedProtoSaysInvalidValueForIncomingHttpRequest() throws Exception { + performXForwardedProtoWithMultipleForwardsTest(",", false, false); + } + + private void performXForwardedProtoWithMultipleForwardsTest(String incomingHeaderValue, boolean arrivesAsSecure, + boolean shouldBeSecure) throws Exception { + + // PREPARE + String incomingScheme = arrivesAsSecure ? "https" : "http"; + String expectedScheme = shouldBeSecure ? "https" : "http"; + int incomingServerPort = arrivesAsSecure ? 8443 : 8080; + int expectedServerPort = shouldBeSecure ? 443 : 80; + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130"); + // protocol + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-proto").setString(incomingHeaderValue); + request.setSecure(arrivesAsSecure); + request.setServerPort(incomingServerPort); + request.getCoyoteRequest().scheme().setString(incomingScheme); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + // client ip + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("no intermediate non-trusted proxy, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertNull("no intermediate trusted proxy", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteHost); + + // protocol + String actualScheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("x-forwarded-proto says " + expectedScheme, expectedScheme, actualScheme); + + int actualServerPort = remoteAddrAndHostTrackerValve.getServerPort(); + Assert.assertEquals("x-forwarded-proto says " + expectedScheme, expectedServerPort, actualServerPort); + + boolean actualSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertEquals("x-forwarded-proto says " + expectedScheme, Boolean.valueOf(shouldBeSecure), + Boolean.valueOf(actualSecure)); + + boolean actualPostInvokeSecure = request.isSecure(); + Assert.assertEquals("postInvoke secure", Boolean.valueOf(arrivesAsSecure), + Boolean.valueOf(actualPostInvokeSecure)); + + int actualPostInvokeServerPort = request.getServerPort(); + Assert.assertEquals("postInvoke serverPort", incomingServerPort, actualPostInvokeServerPort); + + String actualPostInvokeScheme = request.getScheme(); + Assert.assertEquals("postInvoke scheme", incomingScheme, actualPostInvokeScheme); + } + + @Test + public void testInvokeXforwardedProtoIsNullForIncomingHttpsRequest() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130"); + // protocol + // Don't declare "x-forwarded-proto" + request.setSecure(true); + request.setServerPort(8443); + request.getCoyoteRequest().scheme().setString("https"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + // client ip + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertNull("no intermediate non-trusted proxy, x-forwarded-for must be null", actualXForwardedFor); + + String actualXForwardedBy = request.getHeader("x-forwarded-by"); + Assert.assertNull("no intermediate trusted proxy", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "140.211.11.130", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteHost); + + // protocol + String actualScheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("x-forwarded-proto is null", "https", actualScheme); + + int actualServerPort = remoteAddrAndHostTrackerValve.getServerPort(); + Assert.assertEquals("x-forwarded-proto is null", 8443, actualServerPort); + + boolean actualSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertTrue("x-forwarded-proto is null", actualSecure); + + boolean actualPostInvokeSecure = request.isSecure(); + Assert.assertTrue("postInvoke secure", actualPostInvokeSecure); + + int actualPostInvokeServerPort = request.getServerPort(); + Assert.assertEquals("postInvoke serverPort", 8443, actualPostInvokeServerPort); + + String actualPostInvokeScheme = request.getScheme(); + Assert.assertEquals("postInvoke scheme", "https", actualPostInvokeScheme); + } + + @Test + public void testInvokeXforwardedHost() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setHostHeader("x-forwarded-host"); + remoteIpValve.setPortHeader("x-forwarded-port"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + // protocol + request.setSecure(false); + request.setServerPort(8080); + request.getCoyoteRequest().scheme().setString("http"); + // host and port + request.getCoyoteRequest().serverName().setString("10.0.0.1"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-host").setString("example.com:8443"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-port").setString("8443"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-proto").setString("https"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + // protocol + String actualServerName = remoteAddrAndHostTrackerValve.getServerName(); + Assert.assertEquals("tracked serverName", "example.com", actualServerName); + + String actualScheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("tracked scheme", "https", actualScheme); + + int actualServerPort = remoteAddrAndHostTrackerValve.getServerPort(); + Assert.assertEquals("tracked serverPort", 8443, actualServerPort); + + boolean actualSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertTrue("tracked secure", actualSecure); + + String actualPostInvokeServerName = request.getServerName(); + Assert.assertEquals("postInvoke serverName", "10.0.0.1", actualPostInvokeServerName); + + boolean actualPostInvokeSecure = request.isSecure(); + Assert.assertFalse("postInvoke secure", actualPostInvokeSecure); + + int actualPostInvokeServerPort = request.getServerPort(); + Assert.assertEquals("postInvoke serverPort", 8080, actualPostInvokeServerPort); + + String actualPostInvokeScheme = request.getScheme(); + Assert.assertEquals("postInvoke scheme", "http", actualPostInvokeScheme); + } + + @Test + public void testInvokeXforwardedHostAndPort() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setHostHeader("x-forwarded-host"); + remoteIpValve.setPortHeader("x-forwarded-port"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + // protocol + request.setSecure(false); + request.setServerPort(8080); + request.getCoyoteRequest().scheme().setString("http"); + // host and port + request.getCoyoteRequest().serverName().setString("10.0.0.1"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-host").setString("example.com"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-port").setString("8443"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-proto").setString("https"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + // protocol + String actualServerName = remoteAddrAndHostTrackerValve.getServerName(); + Assert.assertEquals("tracked serverName", "example.com", actualServerName); + + String actualScheme = remoteAddrAndHostTrackerValve.getScheme(); + Assert.assertEquals("tracked scheme", "https", actualScheme); + + int actualServerPort = remoteAddrAndHostTrackerValve.getServerPort(); + Assert.assertEquals("tracked serverPort", 8443, actualServerPort); + + boolean actualSecure = remoteAddrAndHostTrackerValve.isSecure(); + Assert.assertTrue("tracked secure", actualSecure); + + String actualPostInvokeServerName = request.getServerName(); + Assert.assertEquals("postInvoke serverName", "10.0.0.1", actualPostInvokeServerName); + + boolean actualPostInvokeSecure = request.isSecure(); + Assert.assertFalse("postInvoke secure", actualPostInvokeSecure); + + int actualPostInvokeServerPort = request.getServerPort(); + Assert.assertEquals("postInvoke serverPort", 8080, actualPostInvokeServerPort); + + String actualPostInvokeScheme = request.getScheme(); + Assert.assertEquals("postInvoke scheme", "http", actualPostInvokeScheme); + } + + @Test + public void testInvokeNotAllowedRemoteAddr() throws Exception { + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies("192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + remoteIpValve.setTrustedProxies("proxy1|proxy2|proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("not-allowed-internal-proxy"); + request.setRemoteHost("not-allowed-internal-proxy-host"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for") + .setString("140.211.11.130, proxy1, proxy2"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = request.getHeader("x-forwarded-for"); + Assert.assertEquals("x-forwarded-for must be unchanged", "140.211.11.130, proxy1, proxy2", actualXForwardedFor); + + String actualXForwardedBy = request.getHeader("x-forwarded-by"); + Assert.assertNull("x-forwarded-by must be null", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "not-allowed-internal-proxy", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "not-allowed-internal-proxy-host", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "not-allowed-internal-proxy", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "not-allowed-internal-proxy-host", actualPostInvokeRemoteHost); + } + + @Test + public void testInvokeUntrustedProxyInTheChain() throws Exception { + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies("192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + remoteIpValve.setTrustedProxies("proxy1|proxy2|proxy3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for") + .setString("140.211.11.130, proxy1, untrusted-proxy, proxy2"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedFor = remoteAddrAndHostTrackerValve.getForwardedFor(); + Assert.assertEquals("ip/host before untrusted-proxy must appear in x-forwarded-for", "140.211.11.130,proxy1", + actualXForwardedFor); + + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertEquals("ip/host after untrusted-proxy must appear in x-forwarded-by", "proxy2", + actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "untrusted-proxy", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "untrusted-proxy", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + } + + @Test + public void testCommaDelimitedListToStringArray() { + String[] actual = StringUtils.splitCommaSeparated("element1, element2, element3"); + String[] expected = new String[] { "element1", "element2", "element3" }; + assertArrayEquals(expected, actual); + } + + @Test + public void testCommaDelimitedListToStringArrayMixedSpaceChars() { + String[] actual = StringUtils.splitCommaSeparated("element1 , element2,\t element3"); + String[] expected = new String[] { "element1", "element2", "element3" }; + assertArrayEquals(expected, actual); + } + + @Test + public void testRequestAttributesForAccessLog() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130"); + // protocol + request.setServerPort(8080); + request.getCoyoteRequest().scheme().setString("http"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + Assert.assertEquals("org.apache.catalina.AccessLog.ServerPort", Integer.valueOf(8080), + request.getAttribute(AccessLog.SERVER_PORT_ATTRIBUTE)); + + Assert.assertEquals("org.apache.catalina.AccessLog.RemoteAddr", "140.211.11.130", + request.getAttribute(AccessLog.REMOTE_ADDR_ATTRIBUTE)); + + Assert.assertEquals("org.apache.catalina.AccessLog.RemoteHost", "140.211.11.130", + request.getAttribute(AccessLog.REMOTE_HOST_ATTRIBUTE)); + } + + @Test + public void testRequestForwarded() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130"); + // protocol + request.setServerPort(8080); + request.getCoyoteRequest().scheme().setString("http"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + Assert.assertEquals("org.apache.tomcat.request.forwarded", Boolean.TRUE, + request.getAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE)); + } + + @Test + public void testRequestForwardedForWithPortNumber() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130:1234"); + // protocol + request.setServerPort(8080); + request.getCoyoteRequest().scheme().setString("http"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + + Assert.assertEquals("140.211.11.130:1234", remoteAddrAndHostTrackerValve.getRemoteAddr()); + } + + @Test + public void testRequestForwardedForWithProxyPortNumber() throws Exception { + + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + // remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + // remoteIpValve.setProtocolHeader("x-forwarded-proto"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(); + request.setCoyoteRequest(new org.apache.coyote.Request()); + // client ip + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("192.168.0.10"); + // Trust c.d + remoteIpValve.setTrustedProxies("foo\\.bar:123"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for") + .setString("140.211.11.130:1234, foo.bar:123"); + // protocol + request.setServerPort(8080); + request.getCoyoteRequest().scheme().setString("http"); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + + Assert.assertEquals("140.211.11.130:1234", remoteAddrAndHostTrackerValve.getRemoteAddr()); + } + + private void assertArrayEquals(String[] expected, String[] actual) { + if (expected == null) { + Assert.assertNull(actual); + return; + } + Assert.assertNotNull(actual); + Assert.assertEquals(expected.length, actual.length); + List e = new ArrayList<>(Arrays.asList(expected)); + List a = new ArrayList<>(Arrays.asList(actual)); + + for (String entry : e) { + Assert.assertTrue(a.remove(entry)); + } + Assert.assertTrue(a.isEmpty()); + } + + @Test + public void testInternalProxies() throws Exception { + RemoteIpValve remoteIpValve = new RemoteIpValve(); + Pattern internalProxiesPattern = Pattern.compile(remoteIpValve.getInternalProxies()); + + doTestPattern(internalProxiesPattern, "8.8.8.8", false); + doTestPattern(internalProxiesPattern, "100.62.0.0", false); + doTestPattern(internalProxiesPattern, "100.63.255.255", false); + doTestPattern(internalProxiesPattern, "100.64.0.0", true); + doTestPattern(internalProxiesPattern, "100.65.0.0", true); + doTestPattern(internalProxiesPattern, "100.68.0.0", true); + doTestPattern(internalProxiesPattern, "100.72.0.0", true); + doTestPattern(internalProxiesPattern, "100.88.0.0", true); + doTestPattern(internalProxiesPattern, "100.95.0.0", true); + doTestPattern(internalProxiesPattern, "100.102.0.0", true); + doTestPattern(internalProxiesPattern, "100.110.0.0", true); + doTestPattern(internalProxiesPattern, "100.126.0.0", true); + doTestPattern(internalProxiesPattern, "100.127.255.255", true); + doTestPattern(internalProxiesPattern, "100.128.0.0", false); + doTestPattern(internalProxiesPattern, "100.130.0.0", false); + } + + private void doTestPattern(Pattern pattern, String input, boolean expectedMatch) { + boolean match = pattern.matcher(input).matches(); + Assert.assertEquals(input, Boolean.valueOf(expectedMatch), Boolean.valueOf(match)); + } +} diff --git a/test/org/apache/catalina/valves/TestRequestFilterValve.java b/test/org/apache/catalina/valves/TestRequestFilterValve.java new file mode 100644 index 0000000..2deb7db --- /dev/null +++ b/test/org/apache/catalina/valves/TestRequestFilterValve.java @@ -0,0 +1,396 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; + +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.core.StandardContext; + +/** + * {@link RequestFilterValve} Tests + */ +public class TestRequestFilterValve { + + private static final int OK = 200; + private static final int FORBIDDEN = 403; + private static final int CUSTOM = 499; + + private static final String ADDR_ALLOW_PAT = "127\\.\\d*\\.\\d*\\.\\d*"; + private static final String ADDR_DENY_PAT = "\\d*\\.\\d*\\.\\d*\\.1"; + private static final String ADDR_ONLY_ALLOW = "127.0.0.2"; + private static final String ADDR_ONLY_DENY = "192.168.0.1"; + private static final String ADDR_ALLOW_AND_DENY = "127.0.0.1"; + private static final String ADDR_NO_ALLOW_NO_DENY = "192.168.0.2"; + + private static final String HOST_ALLOW_PAT = "www\\.example\\.[a-zA-Z0-9-]*"; + private static final String HOST_DENY_PAT = ".*\\.org"; + private static final String HOST_ONLY_ALLOW = "www.example.com"; + private static final String HOST_ONLY_DENY = "host.example.org"; + private static final String HOST_ALLOW_AND_DENY = "www.example.org"; + private static final String HOST_NO_ALLOW_NO_DENY = "host.example.com"; + + private static final String CIDR_ALLOW_PROP = "127.0.0.0/16"; + private static final String CIDR_DENY_PROP = "192.168.0.0/24,127.0.0.0/24"; + private static final String CIDR_ONLY_ALLOW = "127.0.1.1"; + private static final String CIDR_ONLY_DENY = "192.168.0.1"; + private static final String CIDR_ALLOW_AND_DENY = "127.0.0.1"; + private static final String CIDR_NO_ALLOW_NO_DENY = "192.168.1.1"; + + private static final String CIDR6_ALLOW_PROP = "::/96"; + private static final String CIDR6_DENY_PROP = "::f:0:0/112,::/112"; + private static final String CIDR6_ONLY_ALLOW = "0:0:0:0:0:0:148f:1"; + private static final String CIDR6_ONLY_DENY = "0:0:0:0:0:F:0:a"; + private static final String CIDR6_ALLOW_AND_DENY = "0:0:0:0:0:0:0:fA8"; + private static final String CIDR6_NO_ALLOW_NO_DENY = "1:0:0:0:0:0:0:1"; + + private static final int PORT = 8080; + private static final String ADDR_OTHER = "1.2.3.4"; + private static final String PORT_MATCH_PATTERN = ";\\d*"; + private static final String PORT_NO_MATCH_PATTERN = ";8081"; + + + static class TerminatingValve extends ValveBase { + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + } + } + + public static class MockResponse extends Response { + private int status = OK; + + @Override + public void sendError(int status) throws IOException { + this.status = status; + } + + @Override + public int getStatus() { + return status; + } + } + + private void twoTests(String allow, String deny, boolean denyStatus, boolean addConnectorPort, boolean auth, + String property, String type, boolean allowed) { + oneTest(allow, deny, denyStatus, addConnectorPort, false, auth, property, type, allowed); + if (!type.equals("Host")) { + oneTest(allow, deny, denyStatus, addConnectorPort, true, auth, property, type, allowed); + } + } + + private void oneTest(String allow, String deny, boolean denyStatus, boolean addConnectorPort, + boolean usePeerAddress, boolean auth, String property, String type, boolean allowed) { + // PREPARE + RequestFilterValve valve = null; + Connector connector = new Connector(); + Context context = new StandardContext(); + Request request = new Request(connector); + Response response = new MockResponse(); + StringBuilder msg = new StringBuilder(); + int expected = allowed ? OK : FORBIDDEN; + + connector.setPort(PORT); + request.getMappingData().context = context; + request.setCoyoteRequest(new org.apache.coyote.Request()); + + Assert.assertNotNull("Invalid test with null type", type); + + request.setCoyoteRequest(new org.apache.coyote.Request()); + + if (property != null) { + if (type.equals("Addr")) { + valve = new RemoteAddrValve(); + if (usePeerAddress) { + request.setRemoteAddr(ADDR_OTHER); + request.getCoyoteRequest().peerAddr().setString(property); + ((RemoteAddrValve) valve).setUsePeerAddress(true); + msg.append(" peer='" + property + "'"); + } else { + request.setRemoteAddr(property); + request.getCoyoteRequest().peerAddr().setString(ADDR_OTHER); + msg.append(" ip='" + property + "'"); + } + } else if (type.equals("Host")) { + valve = new RemoteHostValve(); + request.setRemoteHost(property); + msg.append(" host='" + property + "'"); + } else if (type.equals("CIDR")) { + valve = new RemoteCIDRValve(); + if (usePeerAddress) { + request.setRemoteAddr(ADDR_OTHER); + request.getCoyoteRequest().peerAddr().setString(property); + ((RemoteCIDRValve) valve).setUsePeerAddress(true); + msg.append(" peer='" + property + "'"); + } else { + request.setRemoteAddr(property); + request.getCoyoteRequest().peerAddr().setString(ADDR_OTHER); + msg.append(" ip='" + property + "'"); + } + } + } + Assert.assertNotNull("Invalid test type" + type, valve); + valve.setNext(new TerminatingValve()); + + if (allow != null) { + valve.setAllow(allow); + msg.append(" allow='" + allow + "'"); + } + if (deny != null) { + valve.setDeny(deny); + msg.append(" deny='" + deny + "'"); + } + if (denyStatus) { + valve.setDenyStatus(CUSTOM); + msg.append(" denyStatus='" + CUSTOM + "'"); + if (!allowed) { + expected = CUSTOM; + } + } + if (addConnectorPort) { + if (valve instanceof RemoteAddrValve) { + ((RemoteAddrValve) valve).setAddConnectorPort(true); + } else if (valve instanceof RemoteHostValve) { + ((RemoteHostValve) valve).setAddConnectorPort(true); + } else if (valve instanceof RemoteCIDRValve) { + ((RemoteCIDRValve) valve).setAddConnectorPort(true); + } else { + Assert.fail("Can only set 'addConnectorPort' for RemoteAddrValve, RemoteHostValve and RemoteCIDRValve"); + } + msg.append(" addConnectorPort='true'"); + } + if (auth) { + context.setPreemptiveAuthentication(true); + valve.setInvalidAuthenticationWhenDeny(true); + msg.append(" auth='true'"); + } + + // TEST + try { + valve.invoke(request, response); + } catch (IOException | ServletException ex) { + // Ignore + } + + // VERIFY + if (!allowed && auth) { + Assert.assertEquals(msg.toString(), OK, response.getStatus()); + Assert.assertEquals(msg.toString(), "invalid", request.getHeader("authorization")); + } else { + Assert.assertEquals(msg.toString(), expected, response.getStatus()); + } + } + + private void standardTests(String allow_pat, String deny_pat, String OnlyAllow, String OnlyDeny, + String AllowAndDeny, String NoAllowNoDeny, boolean auth, String type) { + String apat; + String dpat; + + // Test without ports + apat = allow_pat; + dpat = deny_pat; + twoTests(null, null, false, false, auth, AllowAndDeny, type, false); + twoTests(null, null, true, false, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, false, auth, AllowAndDeny, type, true); + twoTests(apat, null, false, false, auth, NoAllowNoDeny, type, false); + twoTests(apat, null, true, false, auth, AllowAndDeny, type, true); + twoTests(apat, null, true, false, auth, NoAllowNoDeny, type, false); + twoTests(null, dpat, false, false, auth, AllowAndDeny, type, false); + twoTests(null, dpat, false, false, auth, NoAllowNoDeny, type, true); + twoTests(null, dpat, true, false, auth, AllowAndDeny, type, false); + twoTests(null, dpat, true, false, auth, NoAllowNoDeny, type, true); + twoTests(apat, dpat, false, false, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, false, false, auth, OnlyAllow, type, true); + twoTests(apat, dpat, false, false, auth, OnlyDeny, type, false); + twoTests(apat, dpat, false, false, auth, AllowAndDeny, type, false); + twoTests(apat, dpat, true, false, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, true, false, auth, OnlyAllow, type, true); + twoTests(apat, dpat, true, false, auth, OnlyDeny, type, false); + twoTests(apat, dpat, true, false, auth, AllowAndDeny, type, false); + + // Test with port in pattern but forgotten "addConnectorPort" + apat = allow_pat + PORT_MATCH_PATTERN; + dpat = deny_pat + PORT_MATCH_PATTERN; + twoTests(null, null, false, false, auth, AllowAndDeny, type, false); + twoTests(null, null, true, false, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, false, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, false, auth, NoAllowNoDeny, type, false); + twoTests(apat, null, true, false, auth, AllowAndDeny, type, false); + twoTests(apat, null, true, false, auth, NoAllowNoDeny, type, false); + twoTests(null, dpat, false, false, auth, AllowAndDeny, type, true); + twoTests(null, dpat, false, false, auth, NoAllowNoDeny, type, true); + twoTests(null, dpat, true, false, auth, AllowAndDeny, type, true); + twoTests(null, dpat, true, false, auth, NoAllowNoDeny, type, true); + twoTests(apat, dpat, false, false, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, false, false, auth, OnlyAllow, type, false); + twoTests(apat, dpat, false, false, auth, OnlyDeny, type, false); + twoTests(apat, dpat, false, false, auth, AllowAndDeny, type, false); + twoTests(apat, dpat, true, false, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, true, false, auth, OnlyAllow, type, false); + twoTests(apat, dpat, true, false, auth, OnlyDeny, type, false); + twoTests(apat, dpat, true, false, auth, AllowAndDeny, type, false); + + // Test with "addConnectorPort" but port not in pattern + apat = allow_pat; + dpat = deny_pat; + twoTests(null, null, false, true, auth, AllowAndDeny, type, false); + twoTests(null, null, true, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, null, true, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, true, true, auth, NoAllowNoDeny, type, false); + twoTests(null, dpat, false, true, auth, AllowAndDeny, type, true); + twoTests(null, dpat, false, true, auth, NoAllowNoDeny, type, true); + twoTests(null, dpat, true, true, auth, AllowAndDeny, type, true); + twoTests(null, dpat, true, true, auth, NoAllowNoDeny, type, true); + twoTests(apat, dpat, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, false, true, auth, OnlyAllow, type, false); + twoTests(apat, dpat, false, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, false, true, auth, AllowAndDeny, type, false); + twoTests(apat, dpat, true, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, true, true, auth, OnlyAllow, type, false); + twoTests(apat, dpat, true, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, true, true, auth, AllowAndDeny, type, false); + + // Test "addConnectorPort" and with port matching in both patterns + apat = allow_pat + PORT_MATCH_PATTERN; + dpat = deny_pat + PORT_MATCH_PATTERN; + twoTests(null, null, false, true, auth, AllowAndDeny, type, false); + twoTests(null, null, true, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, true, auth, AllowAndDeny, type, true); + twoTests(apat, null, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, null, true, true, auth, AllowAndDeny, type, true); + twoTests(apat, null, true, true, auth, NoAllowNoDeny, type, false); + twoTests(null, dpat, false, true, auth, AllowAndDeny, type, false); + twoTests(null, dpat, false, true, auth, NoAllowNoDeny, type, true); + twoTests(null, dpat, true, true, auth, AllowAndDeny, type, false); + twoTests(null, dpat, true, true, auth, NoAllowNoDeny, type, true); + twoTests(apat, dpat, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, false, true, auth, OnlyAllow, type, true); + twoTests(apat, dpat, false, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, false, true, auth, AllowAndDeny, type, false); + twoTests(apat, dpat, true, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, true, true, auth, OnlyAllow, type, true); + twoTests(apat, dpat, true, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, true, true, auth, AllowAndDeny, type, false); + + // Test "addConnectorPort" and with port not matching in both patterns + apat = allow_pat + PORT_NO_MATCH_PATTERN; + dpat = deny_pat + PORT_NO_MATCH_PATTERN; + twoTests(null, null, false, true, auth, AllowAndDeny, type, false); + twoTests(null, null, true, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, null, true, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, true, true, auth, NoAllowNoDeny, type, false); + twoTests(null, dpat, false, true, auth, AllowAndDeny, type, true); + twoTests(null, dpat, false, true, auth, NoAllowNoDeny, type, true); + twoTests(null, dpat, true, true, auth, AllowAndDeny, type, true); + twoTests(null, dpat, true, true, auth, NoAllowNoDeny, type, true); + twoTests(apat, dpat, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, false, true, auth, OnlyAllow, type, false); + twoTests(apat, dpat, false, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, false, true, auth, AllowAndDeny, type, false); + twoTests(apat, dpat, true, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, true, true, auth, OnlyAllow, type, false); + twoTests(apat, dpat, true, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, true, true, auth, AllowAndDeny, type, false); + + // Test "addConnectorPort" and with port matching only in allow + apat = allow_pat + PORT_MATCH_PATTERN; + dpat = deny_pat + PORT_NO_MATCH_PATTERN; + twoTests(null, null, false, true, auth, AllowAndDeny, type, false); + twoTests(null, null, true, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, true, auth, AllowAndDeny, type, true); + twoTests(apat, null, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, null, true, true, auth, AllowAndDeny, type, true); + twoTests(apat, null, true, true, auth, NoAllowNoDeny, type, false); + twoTests(null, dpat, false, true, auth, AllowAndDeny, type, true); + twoTests(null, dpat, false, true, auth, NoAllowNoDeny, type, true); + twoTests(null, dpat, true, true, auth, AllowAndDeny, type, true); + twoTests(null, dpat, true, true, auth, NoAllowNoDeny, type, true); + twoTests(apat, dpat, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, false, true, auth, OnlyAllow, type, true); + twoTests(apat, dpat, false, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, false, true, auth, AllowAndDeny, type, true); + twoTests(apat, dpat, true, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, true, true, auth, OnlyAllow, type, true); + twoTests(apat, dpat, true, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, true, true, auth, AllowAndDeny, type, true); + + // Test "addConnectorPort" and with port matching only in deny + apat = allow_pat + PORT_NO_MATCH_PATTERN; + dpat = deny_pat + PORT_MATCH_PATTERN; + twoTests(null, null, false, true, auth, AllowAndDeny, type, false); + twoTests(null, null, true, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, null, true, true, auth, AllowAndDeny, type, false); + twoTests(apat, null, true, true, auth, NoAllowNoDeny, type, false); + twoTests(null, dpat, false, true, auth, AllowAndDeny, type, false); + twoTests(null, dpat, false, true, auth, NoAllowNoDeny, type, true); + twoTests(null, dpat, true, true, auth, AllowAndDeny, type, false); + twoTests(null, dpat, true, true, auth, NoAllowNoDeny, type, true); + twoTests(apat, dpat, false, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, false, true, auth, OnlyAllow, type, false); + twoTests(apat, dpat, false, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, false, true, auth, AllowAndDeny, type, false); + twoTests(apat, dpat, true, true, auth, NoAllowNoDeny, type, false); + twoTests(apat, dpat, true, true, auth, OnlyAllow, type, false); + twoTests(apat, dpat, true, true, auth, OnlyDeny, type, false); + twoTests(apat, dpat, true, true, auth, AllowAndDeny, type, false); + } + + @Test + public void testRemoteAddrValveIPv4() { + standardTests(ADDR_ALLOW_PAT, ADDR_DENY_PAT, ADDR_ONLY_ALLOW, ADDR_ONLY_DENY, ADDR_ALLOW_AND_DENY, + ADDR_NO_ALLOW_NO_DENY, false, "Addr"); + standardTests(ADDR_ALLOW_PAT, ADDR_DENY_PAT, ADDR_ONLY_ALLOW, ADDR_ONLY_DENY, ADDR_ALLOW_AND_DENY, + ADDR_NO_ALLOW_NO_DENY, true, "Addr"); + } + + @Test + public void testRemoteHostValve() { + standardTests(HOST_ALLOW_PAT, HOST_DENY_PAT, HOST_ONLY_ALLOW, HOST_ONLY_DENY, HOST_ALLOW_AND_DENY, + HOST_NO_ALLOW_NO_DENY, false, "Host"); + standardTests(HOST_ALLOW_PAT, HOST_DENY_PAT, HOST_ONLY_ALLOW, HOST_ONLY_DENY, HOST_ALLOW_AND_DENY, + HOST_NO_ALLOW_NO_DENY, true, "Host"); + } + + @Test + public void testRemoteCIDRValve() { + standardTests(CIDR_ALLOW_PROP, CIDR_DENY_PROP, CIDR_ONLY_ALLOW, CIDR_ONLY_DENY, CIDR_ALLOW_AND_DENY, + CIDR_NO_ALLOW_NO_DENY, false, "CIDR"); + standardTests(CIDR_ALLOW_PROP, CIDR_DENY_PROP, CIDR_ONLY_ALLOW, CIDR_ONLY_DENY, CIDR_ALLOW_AND_DENY, + CIDR_NO_ALLOW_NO_DENY, true, "CIDR"); + } + + @Test + public void testRemoteCIDR6Valve() { + standardTests(CIDR6_ALLOW_PROP, CIDR6_DENY_PROP, CIDR6_ONLY_ALLOW, CIDR6_ONLY_DENY, CIDR6_ALLOW_AND_DENY, + CIDR6_NO_ALLOW_NO_DENY, false, "CIDR"); + standardTests(CIDR6_ALLOW_PROP, CIDR6_DENY_PROP, CIDR6_ONLY_ALLOW, CIDR6_ONLY_DENY, CIDR6_ALLOW_AND_DENY, + CIDR6_NO_ALLOW_NO_DENY, true, "CIDR"); + } +} diff --git a/test/org/apache/catalina/valves/TestSSLValve.java b/test/org/apache/catalina/valves/TestSSLValve.java new file mode 100644 index 0000000..3d8df7b --- /dev/null +++ b/test/org/apache/catalina/valves/TestSSLValve.java @@ -0,0 +1,356 @@ +/* + * 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.catalina.valves; + +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.logging.Level; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Globals; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.tomcat.unittest.TesterLogValidationFilter; +import org.apache.tomcat.util.buf.UEncoder; +import org.easymock.EasyMock; + +public class TestSSLValve { + + public static class MockRequest extends Request { + + public MockRequest() { + super(EasyMock.createMock(Connector.class)); + setCoyoteRequest(new org.apache.coyote.Request()); + } + + @Override + public void setAttribute(String name, Object value) { + getCoyoteRequest().getAttributes().put(name, value); + } + + @Override + public Object getAttribute(String name) { + return getCoyoteRequest().getAttribute(name); + } + + public void setHeader(String header, String value) { + getCoyoteRequest().getMimeHeaders().setValue(header).setString(value); + } + + public void addHeader(String header, String value) { + getCoyoteRequest().getMimeHeaders().addValue(header).setString(value); + } + } + + private static final String[] CERTIFICATE_LINES = new String[] { "-----BEGIN CERTIFICATE-----", + "MIIFXTCCA0WgAwIBAgIJANFf3YTJgYifMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV", + "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX", + "aWRnaXRzIFB0eSBMdGQwHhcNMTcwNTI2MjEzNjM3WhcNMTgwNTI2MjEzNjM3WjBF", + "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50", + "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC", + "CgKCAgEA2ykNBanZz4cVITNpZcWNVmErUzqgSNrK361mj9vEdB1UkHatwal9jVrR", + "QvFgfiZ8Gl+/85t0ebJhJ+rIr1ww6JE7v2s2MThENj95K5EwZOmgvw+CBlBYsFIz", + "8BtjlVYy+v7RaGPXfjrFkexQP9UIaiIIog2ClDZirRvb+QxS930/YW5Qo+X6EX6W", + "/m/HvlorD25U4ni2FQ0y+EMO2e1jD88cAAMoP5f+Mf6NBK8I6yUeaSuMq7WqtHGV", + "e4F1WOg5z9J5c/M69rB0iQr5NUQwZ1mPYf5Kr0P6+TLh8DJphbVvmHJyT3bgofeV", + "JYl/kdjiXS5P/jwY9tfmhu04tsyzopWRUFCcj5zCiqZYaMn0wtDn08KaAh9oOlg8", + "Z6mJ9i5EybkLm63W7z7LxuM+qnYzq4wKkKdx8hbpASwPqzJkJeXFL/LzhKdZuHiR", + "clgPVYnm98URwhObh073dKguG/gkhcnpXcVBBVdVTJZYGBvTpQh0afXd9bcBwOzY", + "t4MDpGiQB2fLzBOEZhQ37kUcWPmZw5bNPxhx4yE96Md0rx/Gu4ipAHuqLemb1SL5", + "uWNesVmgY3OXaIamQIm9BCwkf8mMvoYdAT+lukTUZLtJ6s2w+Oxnl10tmb+6sTXy", + "UB3WcBTp/o3YjAyJPnM1Wq6nVNQ4W2+NbV5purGAP09sumxeJj8CAwEAAaNQME4w", + "HQYDVR0OBBYEFCGOYMvymUG2ZZT+lK4LvwEvx731MB8GA1UdIwQYMBaAFCGOYMvy", + "mUG2ZZT+lK4LvwEvx731MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB", + "AG6m4nDYCompUtRVude1qulwwAaYEMHyIIsfymI8uAE7d2o4bGjVpAUOdH/VWSOp", + "Rzx0oK6K9cHyiBlKHw5zSZdqcRi++tDX3P9Iy5tXO//zkhMEnSpk6RF2+9JXtyhx", + "Gma4yAET1yES+ybiFT21uZrGCC9r69rWG8JRZshc4RVWGwZsd0zrATVqfY0mZurm", + "xLgU4UOvkTczjlrLiklwwU68M1DLcILJ5FZGTWeTJ/q1wpIn9isK2siAW/VOcbuG", + "xdbGladnIFv+iQfuZG0yjcuMsBFsQiXi6ONM8GM+dr+61V63/1s73jYcOToEsTMM", + "3bHeVffoSkhZvOGTRCI6QhK9wqnIKhAYqu+NbV4OphfE3gOaK+T1cASXUtSQPXoa", + "sEoIVmbQsWRBhWvYShVqvINsH/hAT3Cf/+SslprtQUqiyt2ljdgrRFZdoyB3S7ky", + "KWoZRvHRj2cKU65LVYwx6U1A8SGmViz4aHMSai0wwKzOVv9MGHeRaVhlMmMsbdfu", + "wKoKJv0xYoVwEh1rB8TH8PjbL+6eFLeZXYVZnH71d5JHCghZ8W0a11ZuYkscSQWk", + "yoTBqEpJloWksrypqp3iL4PAL5+KkB2zp66+MVAg8LcEDFJggBBJCtv4SCWV7ZOB", + "WLu8gep+XCwSn0Wb6D3eFs4DoIiMvQ6g2rS/pk7o5eWj", "-----END CERTIFICATE-----" }; + + private SSLValve valve = new SSLValve(); + + private MockRequest mockRequest = new MockRequest(); + private Valve mockNext = EasyMock.createMock(Valve.class); + + + @Before + public void setUp() throws Exception { + valve.setNext(mockNext); + mockNext.invoke(mockRequest, null); + EasyMock.replay(mockNext); + } + + + @Test + public void testSslHeader() { + final String headerName = "myheader"; + final String headerValue = "BASE64_HEADER_VALUE"; + mockRequest.setHeader(headerName, headerValue); + + Assert.assertEquals(headerValue, valve.mygetHeader(mockRequest, headerName)); + } + + + @Test + public void testSslHeaderNull() { + final String headerName = "myheader"; + mockRequest.setHeader(headerName, null); + + Assert.assertNull(valve.mygetHeader(mockRequest, headerName)); + } + + + @Test + public void testSslHeaderNullModHeader() { + final String headerName = "myheader"; + final String nullModHeaderValue = "(null)"; + mockRequest.setHeader(headerName, nullModHeaderValue); + + Assert.assertNull(valve.mygetHeader(mockRequest, nullModHeaderValue)); + } + + + @Test + public void testSslHeaderNullName() throws Exception { + Assert.assertNull(valve.mygetHeader(mockRequest, null)); + } + + + @Test + public void testSslHeaderMultiples() throws Exception { + final String headerName = "myheader"; + final String headerValue = "BASE64_HEADER_VALUE"; + mockRequest.addHeader(headerName, headerValue); + mockRequest.addHeader(headerName, "anyway won't be found"); + + Assert.assertEquals(headerValue, valve.mygetHeader(mockRequest, headerName)); + } + + + @Test + public void testSslClientCertHeaderSingleSpace() throws Exception { + String singleSpaced = certificateSingleLine(" "); + mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced); + + valve.invoke(mockRequest, null); + + assertCertificateParsed(); + } + + + @Test + public void testSslClientCertHeaderMultiSpace() throws Exception { + String singleSpaced = certificateSingleLine(" "); + mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced); + + valve.invoke(mockRequest, null); + + assertCertificateParsed(); + } + + + @Test + public void testSslClientCertHeaderTab() throws Exception { + String singleSpaced = certificateSingleLine("\t"); + mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced); + + valve.invoke(mockRequest, null); + + assertCertificateParsed(); + } + + + @Test + public void testSslClientCertHeaderEscaped() throws Exception { + String cert = certificateEscaped(); + mockRequest.setHeader(valve.getSslClientEscapedCertHeader(), cert); + + valve.invoke(mockRequest, null); + + assertCertificateParsed(); + } + + + @Test + public void testSslClientCertNull() throws Exception { + TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "", null, + "org.apache.catalina.valves.SSLValve"); + + valve.invoke(mockRequest, null); + + EasyMock.verify(mockNext); + Assert.assertNull(mockRequest.getAttribute(Globals.CERTIFICATES_ATTR)); + Assert.assertEquals(0, f.getMessageCount()); + } + + + @Test + public void testSslClientCertShorter() throws Exception { + mockRequest.setHeader(valve.getSslClientCertHeader(), "shorter than hell"); + + TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "", null, + "org.apache.catalina.valves.SSLValve"); + + valve.invoke(mockRequest, null); + + EasyMock.verify(mockNext); + Assert.assertNull(mockRequest.getAttribute(Globals.CERTIFICATES_ATTR)); + Assert.assertEquals(0, f.getMessageCount()); + } + + + @Test + public void testSslClientCertIgnoredBegin() throws Exception { + String[] linesBegin = Arrays.copyOf(CERTIFICATE_LINES, CERTIFICATE_LINES.length); + linesBegin[0] = "3fisjcme3kdsakasdfsadkafsd3"; + String begin = certificateSingleLine(linesBegin, " "); + mockRequest.setHeader(valve.getSslClientCertHeader(), begin); + + valve.invoke(mockRequest, null); + + assertCertificateParsed(); + } + + + @Test + public void testSslClientCertBadFormat() throws Exception { + String[] linesDeleted = Arrays.copyOf(CERTIFICATE_LINES, CERTIFICATE_LINES.length / 2); + String deleted = certificateSingleLine(linesDeleted, " "); + mockRequest.setHeader(valve.getSslClientCertHeader(), deleted); + + TesterLogValidationFilter f = TesterLogValidationFilter.add(Level.WARNING, null, + "java.security.cert.CertificateException", "org.apache.catalina.valves.SSLValve"); + + valve.invoke(mockRequest, null); + + EasyMock.verify(mockNext); + Assert.assertNull(mockRequest.getAttribute(Globals.CERTIFICATES_ATTR)); + Assert.assertEquals(1, f.getMessageCount()); + } + + + @Test + public void testClientCertProviderNotFound() throws Exception { + EasyMock.expect(mockRequest.getConnector().getProperty("clientCertProvider")).andStubReturn("wontBeFound"); + EasyMock.replay(mockRequest.getConnector()); + mockRequest.setHeader(valve.getSslClientCertHeader(), certificateSingleLine(" ")); + + TesterLogValidationFilter f = TesterLogValidationFilter.add(Level.SEVERE, null, + "java.security.NoSuchProviderException", "org.apache.catalina.valves.SSLValve"); + + valve.invoke(mockRequest, null); + + Assert.assertNull(mockRequest.getAttribute(Globals.CERTIFICATES_ATTR)); + Assert.assertEquals(1, f.getMessageCount()); + } + + + @Test + public void testSslCipherHeaderPresent() throws Exception { + String cipher = "ciphered-with"; + mockRequest.setHeader(valve.getSslCipherHeader(), cipher); + + valve.invoke(mockRequest, null); + + Assert.assertEquals(cipher, mockRequest.getAttribute(Globals.CIPHER_SUITE_ATTR)); + } + + + @Test + public void testSslSessionIdHeaderPresent() throws Exception { + String session = "ssl-session"; + mockRequest.setHeader(valve.getSslSessionIdHeader(), session); + + valve.invoke(mockRequest, null); + + Assert.assertEquals(session, mockRequest.getAttribute(Globals.SSL_SESSION_ID_ATTR)); + } + + + @Test + public void testSslCipherUserKeySizeHeaderPresent() throws Exception { + Integer keySize = Integer.valueOf(452); + mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(), String.valueOf(keySize)); + + valve.invoke(mockRequest, null); + + Assert.assertEquals(keySize, mockRequest.getAttribute(Globals.KEY_SIZE_ATTR)); + } + + + @Test(expected = NumberFormatException.class) + public void testSslCipherUserKeySizeHeaderBadFormat() throws Exception { + mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(), "not-an-integer"); + + try { + valve.invoke(mockRequest, null); + } catch (NumberFormatException e) { + Assert.assertNull(mockRequest.getAttribute(Globals.KEY_SIZE_ATTR)); + throw e; + } + } + + + private static String certificateSingleLine(String[] lines, String separator) { + StringBuilder singleSpaced = new StringBuilder(); + for (String current : lines) { + singleSpaced.append(current).append(separator); + } + singleSpaced.deleteCharAt(singleSpaced.length() - 1); + return singleSpaced.toString(); + } + + + private static String certificateSingleLine(String separator) { + return certificateSingleLine(CERTIFICATE_LINES, separator); + } + + + private static String certificateEscaped() throws Exception { + String cert = certificateSingleLine(CERTIFICATE_LINES, "\n"); + String escaped = new UEncoder(UEncoder.SafeCharsSet.DEFAULT).encodeURL(cert, 0, cert.length()).toString(); + Assert.assertTrue(escaped, escaped.contains("%0a")); // newline is escaped + Assert.assertTrue(escaped, escaped.contains("%20")); // space is escaped + Assert.assertTrue(escaped, escaped.contains("%2b")); // + is escaped + Assert.assertTrue(escaped, escaped.contains("%2f")); // / is escaped + return escaped; + } + + + private void assertCertificateParsed() throws Exception { + TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "", null, + "org.apache.catalina.valves.SSLValve"); + + EasyMock.verify(mockNext); + + X509Certificate[] certificates = (X509Certificate[]) mockRequest.getAttribute(Globals.CERTIFICATES_ATTR); + Assert.assertNotNull(certificates); + Assert.assertEquals(1, certificates.length); + Assert.assertNotNull(certificates[0]); + Assert.assertEquals(0, f.getMessageCount()); + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java b/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java new file mode 100644 index 0000000..ae54e19 --- /dev/null +++ b/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java @@ -0,0 +1,158 @@ +/* + * 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.catalina.valves; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestStuckThreadDetectionValve extends TomcatBaseTest { + private StandardContext context; + private Tomcat tomcat; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + tomcat = getTomcatInstance(); + File docBase = new File(System.getProperty("java.io.tmpdir")); + context = (StandardContext) tomcat.addContext("", docBase.getAbsolutePath()); + } + + @Test + public void testDetection() throws Exception { + // second, we test the actual effect of the flag on the startup + StickingServlet stickingServlet = new StickingServlet(8000L); + Wrapper servlet = Tomcat.addServlet(context, "myservlet", stickingServlet); + servlet.addMapping("/myservlet"); + + StuckThreadDetectionValve valve = new StuckThreadDetectionValve(); + valve.setThreshold(2); + context.addValve(valve); + context.setBackgroundProcessorDelay(1); + tomcat.start(); + + Assert.assertEquals(0, valve.getStuckThreadIds().length); + + final ByteChunk result = new ByteChunk(); + Thread asyncThread = new Thread() { + @Override + public void run() { + try { + getUrl("http://localhost:" + getPort() + "/myservlet", result, null); + } catch (IOException e) { + e.printStackTrace(); + } + } + + }; + asyncThread.start(); + try { + Thread.sleep(500L); + Assert.assertEquals(0, valve.getStuckThreadIds().length); + + Thread.sleep(5000L); + Assert.assertEquals(1, valve.getStuckThreadIds().length); + } finally { + asyncThread.join(20000); + // check that we did not reach the join timeout + Assert.assertFalse(asyncThread.isAlive()); + } + Assert.assertFalse(stickingServlet.wasInterrupted); + Assert.assertTrue(result.toString().startsWith("OK")); + } + + @Test + public void testInterruption() throws Exception { + // second, we test the actual effect of the flag on the startup + StickingServlet stickingServlet = new StickingServlet(TimeUnit.SECONDS.toMillis(20L)); + Wrapper servlet = Tomcat.addServlet(context, "myservlet", stickingServlet); + servlet.addMapping("/myservlet"); + + StuckThreadDetectionValve valve = new StuckThreadDetectionValve(); + valve.setThreshold(2); + valve.setInterruptThreadThreshold(5); + context.addValve(valve); + context.setBackgroundProcessorDelay(1); + tomcat.start(); + + Assert.assertEquals(0, valve.getStuckThreadIds().length); + + final ByteChunk result = new ByteChunk(); + Thread asyncThread = new Thread() { + @Override + public void run() { + try { + getUrl("http://localhost:" + getPort() + "/myservlet", result, null); + } catch (IOException e) { + e.printStackTrace(); + } + } + + }; + asyncThread.start(); + try { + Thread.sleep(4000L); + Assert.assertEquals(1, valve.getStuckThreadIds().length); + + } finally { + asyncThread.join(20000); + // check that we did not reach the join timeout + Assert.assertFalse(asyncThread.isAlive()); + } + Assert.assertTrue(stickingServlet.wasInterrupted); + Assert.assertEquals(0, valve.getStuckThreadIds().length); + Assert.assertTrue(result.toString().startsWith("OK")); + } + + private static class StickingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private final long delay; + boolean wasInterrupted = false; + + StickingServlet(long delay) { + this.delay = delay; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + wasInterrupted = true; + } + resp.setContentType("text/plain"); + resp.getWriter().println("OK"); + } + + } +} diff --git a/test/org/apache/catalina/valves/TesterAccessLogValve.java b/test/org/apache/catalina/valves/TesterAccessLogValve.java new file mode 100644 index 0000000..4ef6710 --- /dev/null +++ b/test/org/apache/catalina/valves/TesterAccessLogValve.java @@ -0,0 +1,123 @@ +/* + * 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.catalina.valves; + +import java.io.IOException; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.ServletException; + +import org.junit.Assert; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; + +public class TesterAccessLogValve extends ValveBase implements AccessLog { + + private static final boolean RELAX_TIMING = Boolean.getBoolean("tomcat.test.relaxTiming"); + + // Timing tests need an error margin to prevent failures. + private static final long ERROR_MARGIN = RELAX_TIMING ? 2000 : 100; + + private final Queue entries = new ConcurrentLinkedQueue<>(); + + public TesterAccessLogValve() { + // Async requests are supported + super(true); + } + + @Override + public void log(Request request, Response response, long time) { + entries.add(new Entry(request.getRequestURI(), response.getStatus(), TimeUnit.NANOSECONDS.toMillis(time))); + } + + @Override + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + // NOOP - test code + } + + @Override + public boolean getRequestAttributesEnabled() { + // Always false - test code + return false; + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + // Just invoke next - access logging happens via log() method + getNext().invoke(request, response); + } + + public int getEntryCount() { + return entries.size(); + } + + public void validateAccessLog(int count, int status, long minTime, long maxTime) throws Exception { + + // Wait (but not too long) until all expected entries appear (access log + // entry will be made after response has been returned to user) + for (int i = 0; i < 10 && entries.size() < count; i++) { + Thread.sleep(100); + } + + StringBuilder entriesLog = new StringBuilder(); + for (Entry entry : entries) { + entriesLog.append(entry.toString()); + entriesLog.append(System.lineSeparator()); + } + Assert.assertEquals(entriesLog.toString(), count, entries.size()); + for (Entry entry : entries) { + Assert.assertEquals(status, entry.getStatus()); + Assert.assertTrue(entry.toString() + " duration is not >= " + (minTime - ERROR_MARGIN), + entry.getTime() >= minTime - ERROR_MARGIN); + Assert.assertTrue(entry.toString() + " duration is not < " + (maxTime + ERROR_MARGIN), + entry.getTime() < maxTime + ERROR_MARGIN); + } + } + + public static class Entry { + private final String uri; + private final int status; + private final long time; + + public Entry(String uri, int status, long time) { + this.uri = uri; + this.status = status; + this.time = time; + } + + public String getUri() { + return uri; + } + + public int getStatus() { + return status; + } + + public long getTime() { + return time; + } + + @Override + public String toString() { + return "Uri: " + uri + ", Status: " + status + ", Time: " + time; + } + } +} diff --git a/test/org/apache/catalina/valves/rewrite/TestQuotedStringTokenizer.java b/test/org/apache/catalina/valves/rewrite/TestQuotedStringTokenizer.java new file mode 100644 index 0000000..2104155 --- /dev/null +++ b/test/org/apache/catalina/valves/rewrite/TestQuotedStringTokenizer.java @@ -0,0 +1,71 @@ +/* + * 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.catalina.valves.rewrite; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TestQuotedStringTokenizer { + + private String inputText; + private List tokens; + + @Parameters(name = "{index}: tokenize({0}) = {1}") + public static Collection data() { + return Arrays.asList(new Object[][] { { null, Collections.emptyList() }, { "", Collections.emptyList() }, + { " \t\r\n", Collections.emptyList() }, { "simple", Arrays.asList("simple") }, + { "more than one word", Arrays.asList("more", "than", "one", "word") }, + { "\"quoted text\"", Arrays.asList("quoted text") }, + { " mixed \t\"words with\\\"\" escapes", Arrays.asList("mixed", "words with\"", "escapes") }, + { "# comment", Collections.emptyList() }, + { "Something # and then a comment", Arrays.asList("Something") }, + { "\"Quoted with a #\" which is not a comment", + Arrays.asList("Quoted with a #", "which", "is", "not", "a", "comment") } }); + } + + public TestQuotedStringTokenizer(String inputText, List tokens) { + this.inputText = inputText; + this.tokens = tokens; + } + + @Test + public void testTokenize() { + QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(inputText); + List result = new ArrayList<>(); + int count = tokens.size(); + while (tokenizer.hasMoreTokens()) { + MatcherAssert.assertThat(Integer.valueOf(tokenizer.countTokens()), CoreMatchers.is(Integer.valueOf(count))); + result.add(tokenizer.nextToken()); + count--; + } + MatcherAssert.assertThat(Integer.valueOf(tokenizer.countTokens()), CoreMatchers.is(Integer.valueOf(0))); + MatcherAssert.assertThat(tokens, CoreMatchers.is(result)); + } + +} diff --git a/test/org/apache/catalina/valves/rewrite/TestResolverSSL.java b/test/org/apache/catalina/valves/rewrite/TestResolverSSL.java new file mode 100644 index 0000000..6b7de8e --- /dev/null +++ b/test/org/apache/catalina/valves/rewrite/TestResolverSSL.java @@ -0,0 +1,182 @@ +/* + * 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.catalina.valves.rewrite; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.ServletException; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Container; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.core.AprLifecycleListener; +import org.apache.catalina.core.StandardServer; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.valves.ValveBase; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.TesterSupport; + +@RunWith(Parameterized.class) +public class TestResolverSSL extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation" }); + parameterSets.add( + new Object[] { "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + + @Test + public void testSslEnv() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Container root = tomcat.getHost().findChild(""); + root.getPipeline().addValve(new ResolverTestValve()); + + // Enable session caching so the SSL Session is available when using OpenSSL + SSLHostConfig sslHostConfig = tomcat.getConnector().findSslHostConfigs()[0]; + sslHostConfig.setSessionCacheSize(20 * 1024); + + tomcat.start(); + ByteChunk res = getUrl("https://localhost:" + getPort() + "/protected"); + // Just look a bit at the result + System.out.println(res.toString()); + Assert.assertTrue(res.toString().indexOf("OK") > 0); + } + + //@formatter:off + // List from https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#envvars + private static final String[] keys = { + "HTTPS", + "SSL_PROTOCOL", + "SSL_SESSION_ID", + "SSL_SESSION_RESUMED", + "SSL_SECURE_RENEG", + "SSL_CIPHER", + "SSL_CIPHER_EXPORT", + "SSL_CIPHER_USEKEYSIZE", + "SSL_CIPHER_ALGKEYSIZE", + "SSL_COMPRESS_METHOD", + "SSL_VERSION_INTERFACE", + "SSL_VERSION_LIBRARY", + "SSL_CLIENT_M_VERSION", + "SSL_CLIENT_M_SERIAL", + "SSL_CLIENT_S_DN", + "SSL_CLIENT_S_DN_CN", // CN component + "SSL_CLIENT_S_DN_O", // O component + "SSL_CLIENT_S_DN_C", // C component + "SSL_CLIENT_SAN_Email_0", + "SSL_CLIENT_SAN_DNS_0", + "SSL_CLIENT_SAN_OTHER_msUPN_0", + "SSL_CLIENT_I_DN", + "SSL_CLIENT_I_DN_CN", // CN component + "SSL_CLIENT_I_DN_O", // O component + "SSL_CLIENT_I_DN_C", // C component + "SSL_CLIENT_V_START", + "SSL_CLIENT_V_END", + "SSL_CLIENT_V_REMAIN", + "SSL_CLIENT_A_SIG", + "SSL_CLIENT_A_KEY", + "SSL_CLIENT_CERT", + "SSL_CLIENT_CERT_CHAIN_0", + "SSL_CLIENT_CERT_RFC4523_CEA", + "SSL_CLIENT_VERIFY", + "SSL_SERVER_M_VERSION", + "SSL_SERVER_M_SERIAL", + "SSL_SERVER_S_DN", + "SSL_SERVER_SAN_Email_0", + "SSL_SERVER_SAN_DNS_0", + "SSL_SERVER_SAN_OTHER_dnsSRV_0", + "SSL_SERVER_S_DN_CN", // CN component + "SSL_SERVER_S_DN_O", // O component + "SSL_SERVER_S_DN_C", // C component + "SSL_SERVER_I_DN", + "SSL_SERVER_I_DN_CN", // CN component + "SSL_SERVER_I_DN_O", // O component + "SSL_SERVER_I_DN_C", // C component + "SSL_SERVER_V_START", + "SSL_SERVER_V_END", + "SSL_SERVER_A_SIG", + "SSL_SERVER_A_KEY", + "SSL_SERVER_CERT", + "SSL_SRP_USER", + "SSL_SRP_USERINFO", + "SSL_TLS_SNI" }; + //@formatter:on + + public static class ResolverTestValve extends ValveBase { + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + PrintWriter writer = response.getWriter(); + Resolver resolver = new ResolverImpl(request); + for (String key : keys) { + resolve(key, resolver, writer); + } + writer.println("OK"); + } + + private void resolve(String key, Resolver resolver, PrintWriter writer) { + writer.println("[" + key + "] " + resolver.resolveSsl(key)); + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.configureClientCertContext(tomcat); + + TesterSupport.configureClientSsl(); + + Assert.assertTrue(tomcat.getConnector().setProperty("sslImplementationName", sslImplementationName)); + + if (needApr) { + AprLifecycleListener listener = new AprLifecycleListener(); + Assume.assumeTrue(AprLifecycleListener.isAprAvailable()); + StandardServer server = (StandardServer) tomcat.getServer(); + server.addLifecycleListener(listener); + } + } +} diff --git a/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java b/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java new file mode 100644 index 0000000..464a960 --- /dev/null +++ b/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java @@ -0,0 +1,905 @@ +/* + * 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.catalina.valves.rewrite; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.valves.ValveBase; +import org.apache.tomcat.util.buf.ByteChunk; + +/* + * Implementation note: + * + * A number of these tests involve the rewrite valve returning an HTTP Location + * header that include un-encoded UTF-8 bytes. How the HTTP client handles these + * depends on the default character encoding configured for the JVM running the + * test. The tests expect the client to be configured with UTF-8 as the default + * encoding. Use of any other encoding is likely to lead to test failures. + */ +public class TestRewriteValve extends TomcatBaseTest { + + @Test + public void testNoRewrite() throws Exception { + doTestRewrite("", "/a/%255A", "/a/%255A"); + } + + @Test + public void testBackslashPercentSign() throws Exception { + doTestRewrite("RewriteRule ^(.*) /a/\\%5A", "/", "/a/%255A"); + } + + @Test + public void testNoopRewrite() throws Exception { + doTestRewrite("RewriteRule ^(.*) $1", "/a/%255A", "/a/%255A"); + } + + @Test + public void testNoopValveSkipRewrite() throws Exception { + doTestRewrite("RewriteRule ^(.*) $1 [VS]", "/a/%255A", "/a/%255A", null, null, true); + } + + @Test + public void testPathRewrite() throws Exception { + doTestRewrite("RewriteRule ^/b(.*) /a$1", "/b/%255A", "/a/%255A"); + } + + @Test + public void testNonNormalizedPathRewrite() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /b/../a/$1", "/b/%255A", "/b/../a/%255A"); + } + + // BZ 57863 + @Test + public void testRewriteMap01() throws Exception { + doTestRewrite("RewriteMap mapa org.apache.catalina.valves.rewrite.TesterRewriteMapA\n" + + "RewriteRule /b/(.*).html$ /c/${mapa:$1}", "/b/a.html", "/c/aa"); + } + + @Test + public void testRewriteMap02() throws Exception { + doTestRewrite("RewriteMap mapa org.apache.catalina.valves.rewrite.TesterRewriteMapA\n" + + "RewriteRule /b/(.*).html$ /c/${mapa:$1|dd}", "/b/x.html", "/c/dd"); + } + + // BZ 62667 + @Test + public void testRewriteMap03() throws Exception { + doTestRewrite("RewriteMap mapa org.apache.catalina.valves.rewrite.TesterRewriteMapA\n" + + "RewriteRule /b/(.*).html$ /c/${mapa:$1|d$1d}", "/b/x.html", "/c/dxd"); + } + + @Test + public void testRewriteMap04() throws Exception { + doTestRewrite("RewriteMap mapa org.apache.catalina.valves.rewrite.TesterRewriteMapA\n" + + "RewriteRule /b/(.*).html$ /c/${mapa:a$1|dd}", "/b/a.html", "/c/aaaa"); + } + + @Test + public void testRewriteMap05() throws Exception { + doTestRewrite("RewriteMap mapa org.apache.catalina.valves.rewrite.TesterRewriteMapA\n" + + "RewriteRule /b/.* /c/${mapa:a}", "/b/a.html", "/c/aa"); + } + + @Test + public void testRewriteMap06() throws Exception { + doTestRewrite("RewriteMap mapa org.apache.catalina.valves.rewrite.TesterRewriteMapA\n" + + "RewriteRule /b/.* /c/${mapa:${mapa:a}}", "/b/a.html", "/c/aaaa"); + } + + @Test + public void testRewriteMap07() throws Exception { + doTestRewrite("RewriteMap mapa org.apache.catalina.valves.rewrite.TesterRewriteMapA foo bar\n" + + "RewriteRule /b/.* /c/${mapa:${mapa:a}}", "/b/a.html", "/c/aaaa"); + } + + @Test + public void testRewriteMap08() throws Exception { + doTestRewrite("RewriteMap lc int:tolower\n" + "RewriteRule ^(.*) ${lc:$1}", "/C/AaA", "/c/aaa"); + } + + @Test + public void testRewriteMap09() throws Exception { + doTestRewrite("RewriteMap lc int:toupper\n" + "RewriteRule ^(.*) ${lc:$1}", "/w/aAa", "/W/AAA"); + } + + @Test + public void testRewriteMap10() throws Exception { + doTestRewrite("RewriteMap lc int:escape\n" + "RewriteRule ^(.*) ${lc:$1}", "/c/a%20aa", "/c/a%2520aa"); + } + + @Test + public void testRewriteMap11() throws Exception { + doTestRewrite("RewriteMap lc int:unescape\n" + "RewriteRule ^(.*) ${lc:$1}", "/c/a%2520aa", "/c/a%20aa"); + } + + + private static String getTestConfDirectory() { + File f = new File("test/conf"); + return f.getAbsolutePath() + File.separator; + } + + + @Test + public void testRewriteMap12() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1}", "/b/a.html", "/c/aa"); + } + + @Test + public void testRewriteMap13() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1|dd}", "/b/x.html", "/c/dd"); + } + + // BZ 62667 + @Test + public void testRewriteMap14() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1|d$1d}", "/b/x.html", "/c/dxd"); + } + + @Test + public void testRewriteMap15() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:a$1|dd}", "/b/a.html", "/c/aaaa"); + } + + @Test + public void testRewriteMap16() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt\n" + + "RewriteRule /b/.* /c/${mapb:a}", "/b/a.html", "/c/aa"); + } + + @Test + public void testRewriteMap17() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt\n" + + "RewriteRule /b/.* /c/${mapb:${mapb:a}}", "/b/a.html", "/c/aaaa"); + } + + @Test + public void testRewriteMap18() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt\n" + + "RewriteRule /b/.* /c/${mapb:${mapb:a}}", "/b/a.html", "/c/aaaa"); + } + + @Test(expected = IllegalArgumentException.class) + public void testRewriteMap19() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt first\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1}", "/b/aa.html", "/c/aaaa"); + } + + @Test(expected = IllegalArgumentException.class) + public void testRewriteMap20() throws Exception { + doTestRewrite("RewriteMap mapb txt:" + getTestConfDirectory() + "TesterRewriteMapB.txt first second\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1}", "/b/aa.html", "/c/aaaa"); + } + + @Test + public void testRewriteMap21() throws Exception { + doTestRewrite("RewriteMap mapb rnd:" + getTestConfDirectory() + "TesterRewriteMapC.txt\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1}", "/b/a.html", "/c/aa"); + } + + // This test should succeed 50% of the runs as it depends on a random choice + public void testRewriteMap22() throws Exception { + doTestRewrite("RewriteMap mapb rnd:" + getTestConfDirectory() + "TesterRewriteMapC.txt\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1}", "/b/b.html", "/c/bb"); + } + + @Test + public void testRewriteMap23() throws Exception { + doTestRewrite("RewriteMap mapb rnd:" + getTestConfDirectory() + "TesterRewriteMapC.txt\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1}", "/b/aa.html", "/c/aaaa"); + } + + @Test(expected = IllegalArgumentException.class) + public void testRewriteMap24() throws Exception { + doTestRewrite("RewriteMap mapb rnd:" + getTestConfDirectory() + "TesterRewriteMapC.txt first\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1}", "/b/aa.html", "/c/aaaa"); + } + + @Test(expected = IllegalArgumentException.class) + public void testRewriteMap25() throws Exception { + doTestRewrite("RewriteMap mapb rnd:" + getTestConfDirectory() + "TesterRewriteMapC.txt first second\n" + + "RewriteRule /b/(.*).html$ /c/${mapb:$1}", "/b/aa.html", "/c/aaaa"); + } + + @Test + public void testRewriteServerVar() throws Exception { + doTestRewrite("RewriteRule /b/(.*).html$ /c%{SERVLET_PATH}", "/b/x.html", "/c/b/x.html"); + } + + @Test + public void testRewriteEnvVarAndServerVar() throws Exception { + System.setProperty("some_variable", "something"); + doTestRewrite("RewriteRule /b/(.*).html$ /c/%{ENV:some_variable}%{SERVLET_PATH}", "/b/x.html", + "/c/something/b/x.html"); + } + + @Test + public void testRewriteServerVarAndEnvVar() throws Exception { + System.setProperty("some_variable", "something"); + doTestRewrite("RewriteRule /b/(.*).html$ /c%{SERVLET_PATH}/%{ENV:some_variable}", "/b/x.html", + "/c/b/x.html/something"); + } + + @Test + public void testRewriteMissingCurlyBraceOnVar() throws Exception { + try { + doTestRewrite("RewriteRule /b/(.*).html$ /c%_{SERVLET_PATH}", "/b/x.html", "/c"); + Assert.fail("IAE expected."); + } catch (IllegalArgumentException e) { + // expected as %_{ is invalid + } + } + + @Test + public void testRewriteMissingCurlyBraceOnMapper() throws Exception { + try { + doTestRewrite("RewriteRule /b/(.*).html$ /c$_{SERVLET_PATH}", "/b/x.html", "/c"); + Assert.fail("IAE expected."); + } catch (IllegalArgumentException e) { + // expected as $_{ is invalid + } + } + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=60013 + public void testRewriteWithEncoding02() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*)$ /c/?param=$1 [L]", "/b/%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95", "/c/", + "param=\u5728\u7EBF\u6D4B\u8BD5"); + } + + @Test + public void testNonAsciiPath() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /c/$1", "/b/%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95", + "/c/%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95"); + } + + @Test + public void testNonAsciiPathRedirect() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /c/$1 [R]", "/b/%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95", + "/c/%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95"); + } + + @Test + public void testQueryString() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /c?$1", "/b/id=1", "/c", "id=1"); + } + + @Test + public void testQueryStringRemove() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /c/$1?", "/b/d?=1", "/c/d", null); + } + + @Test + public void testQueryStringRemove02() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /c/$1 [QSD]", "/b/d?=1", "/c/d", null); + } + + @Test + public void testNonAsciiQueryString() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /c?$1", "/b/id=%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95", "/c", + "id=%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95"); + } + + + @Test + public void testNonAsciiQueryStringAndPath() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/$1?$2", "/b/%E5%9C%A8%E7%BA%BF/id=%E6%B5%8B%E8%AF%95", + "/c/%E5%9C%A8%E7%BA%BF", "id=%E6%B5%8B%E8%AF%95"); + } + + + @Test + public void testNonAsciiQueryStringAndRedirect() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /c?$1 [R]", "/b/id=%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95", "/c", + "id=%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95"); + } + + + @Test + public void testNonAsciiQueryStringAndPathAndRedirect() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/$1?$2 [R]", "/b/%E5%9C%A8%E7%BA%BF/id=%E6%B5%8B%E8%AF%95", + "/c/%E5%9C%A8%E7%BA%BF", "id=%E6%B5%8B%E8%AF%95"); + } + + + @Test + public void testNonAsciiQueryStringWithB() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*)/id=(.*) /c?filename=$1&id=$2 [B]", + "/b/file01/id=%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95", "/c", + "filename=file01&id=%25E5%259C%25A8%25E7%25BA%25BF%25E6%25B5%258B%25E8%25AF%2595"); + } + + + @Test + public void testNonAsciiQueryStringAndPathAndRedirectWithB() throws Exception { + // Note the double encoding of the result (httpd produces the same result) + doTestRewrite("RewriteRule ^/b/(.*)/(.*)/id=(.*) /c/$1?filename=$2&id=$3 [B,R]", + "/b/%E5%9C%A8%E7%BA%BF/file01/id=%E6%B5%8B%E8%AF%95", "/c/%25E5%259C%25A8%25E7%25BA%25BF", + "filename=file01&id=%25E6%25B5%258B%25E8%25AF%2595"); + } + + + @Test + public void testUtf8WithBothQsFlagsNone() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/%C2%A1%C2%A1", + "id=%C2%A1"); + } + + + @Test + public void testUtf8WithBothQsFlagsB() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", + "/c/%C2%A1%25C2%25A1", "id=%25C2%25A1"); + } + + + @Test + public void testUtf8WithBothQsFlagsR() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", + "/c/%C2%A1%C2%A1", "id=%C2%A1"); + } + + + @Test + public void testUtf8WithBothQsFlagsRB() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", + "/c/%C2%A1%25C2%25A1", "id=%25C2%25A1"); + } + + + @Test + public void testUtf8WithBothQsFlagsRNE() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null); + } + + + @Test + public void testUtf8WithBothQsFlagsRBNE() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null); + } + + + @Test + public void testUtf8WithBothQsFlagsBQSA() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B,QSA]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", + "/c/%C2%A1%25C2%25A1", "id=%25C2%25A1&di=%C2%AE"); + } + + + @Test + public void testUtf8WithBothQsFlagsRQSA() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,QSA]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", + "/c/%C2%A1%C2%A1", "id=%C2%A1&di=%C2%AE"); + } + + + @Test + public void testUtf8WithBothQsFlagsRBQSA() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,QSA]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", + "/c/%C2%A1%25C2%25A1", "id=%25C2%25A1&di=%C2%AE"); + } + + + @Test + public void testUtf8WithBothQsFlagsRNEQSA() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE,QSA]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null); + } + + + @Test + public void testUtf8WithBothQsFlagsRBNEQSA() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE,QSA]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null); + } + + + @Test + public void testUtf8WithOriginalQsFlagsNone() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1", "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%C2%A1", "id=%C2%A1"); + } + + + @Test + public void testUtf8WithOriginalQsFlagsB() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [B]", "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%25C2%25A1", + "id=%C2%A1"); + } + + + @Test + public void testUtf8WithOriginalQsFlagsR() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R]", "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%C2%A1", "id=%C2%A1"); + } + + + @Test + public void testUtf8WithOriginalQsFlagsRB() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B]", "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%25C2%25A1", + "id=%C2%A1"); + } + + + @Test + public void testUtf8WithOriginalQsFlagsRNE() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,NE]", "/b/%C2%A1?id=%C2%A1", null); + } + + + @Test + public void testUtf8WithOriginalQsFlagsRBNE() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B,NE]", "/b/%C2%A1?id=%C2%A1", null); + } + + + @Test + public void testUtf8WithRewriteQsFlagsNone() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2", "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%C2%A1", + "id=%C2%A1"); + } + + + @Test + public void testUtf8WithRewriteQsFlagsB() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B]", "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%25C2%25A1", + "id=%25C2%25A1"); + } + + + @Test + public void testUtf8WithRewriteQsFlagsR() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R]", "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%C2%A1", + "id=%C2%A1"); + } + + + @Test + public void testUtf8WithBothQsFlagsQSA() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [QSA]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE", + "/c/%C2%A1%C2%A1", "id=%C2%A1&di=%C2%AE"); + } + + + @Test + public void testUtf8WithRewriteQsFlagsRB() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B]", "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%25C2%25A1", + "id=%25C2%25A1"); + } + + + @Test + public void testUtf8WithRewriteQsFlagsRNE() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE]", "/b/%C2%A1/id=%C2%A1", null); + } + + + @Test + public void testUtf8WithRewriteQsFlagsRBNE() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE]", "/b/%C2%A1/id=%C2%A1", null); + } + + + @Test + public void testUtf8WithRewriteQsFlagsQSA() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [QSA]", "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%C2%A1", + "id=%C2%A1"); + } + + + @Test + public void testUtf8FlagsNone() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1", "/b/%C2%A1", "/c/%C2%A1%C2%A1"); + } + + + @Test + public void testUtf8FlagsB() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [B]", "/b/%C2%A1", "/c/%C2%A1%25C2%25A1"); + } + + + @Test + public void testUtf8FlagsR() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R]", "/b/%C2%A1", "/c/%C2%A1%C2%A1"); + } + + + @Test + public void testUtf8FlagsRB() throws Exception { + // Note %C2%A1 == \u00A1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B]", "/b/%C2%A1", "/c/%C2%A1%25C2%25A1"); + } + + + @Test + public void testUtf8FlagsRNE() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,NE]", "/b/%C2%A1", null); + } + + + @Test + public void testUtf8FlagsRBNE() throws Exception { + // Note %C2%A1 == \u00A1 + // Failing to escape the redirect means UTF-8 bytes in the Location + // header which will be treated as if they are ISO-8859-1 + doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B,NE]", "/b/%C2%A1", null); + } + + + @Test + public void testFlagsNC() throws Exception { + // https://bz.apache.org/bugzilla/show_bug.cgi?id=60116 + doTestRewrite("RewriteCond %{QUERY_STRING} a=([a-z]*) [NC]\n" + "RewriteRule .* - [E=X-Test:%1]", "/c?a=aAa", + "/c", null, "aAa"); + } + + + @Test + public void testHostRewrite() throws Exception { + // Based on report from users list that ':' was encoded and breaking + // the redirect + doTestRewrite("RewriteRule ^/b(.*) http://%{HTTP_HOST}:%{SERVER_PORT}/a$1 [R]", "/b/%255A", "/a/%255A"); + } + + + @Test + public void testDefaultRedirect() throws Exception { + doTestRedirect("RewriteRule ^/from/a$ /to/b [R]", "/redirect/from/a", "/redirect/to/b", 302); + } + + + @Test + public void testTempRedirect() throws Exception { + doTestRedirect("RewriteRule ^/from/a$ /to/b [R=temp]", "/redirect/from/a", "/redirect/to/b", 302); + } + + + @Test + public void testPermanentRedirect() throws Exception { + // Disable the following of redirects for this test only + boolean originalValue = HttpURLConnection.getFollowRedirects(); + HttpURLConnection.setFollowRedirects(false); + try { + doTestRedirect("RewriteRule ^/from/a$ /to/b [R=permanent]", "/redirect/from/a", "/redirect/to/b", 301); + } finally { + HttpURLConnection.setFollowRedirects(originalValue); + } + } + + + @Test + public void testSeeotherRedirect() throws Exception { + // Disable the following of redirects for this test only + boolean originalValue = HttpURLConnection.getFollowRedirects(); + HttpURLConnection.setFollowRedirects(false); + try { + doTestRedirect("RewriteRule ^/from/a$ /to/b [R=seeother]", "/redirect/from/a", "/redirect/to/b", 303); + } finally { + HttpURLConnection.setFollowRedirects(originalValue); + } + } + + + @Test + public void test307Redirect() throws Exception { + // Disable the following of redirects for this test only + boolean originalValue = HttpURLConnection.getFollowRedirects(); + HttpURLConnection.setFollowRedirects(false); + try { + doTestRedirect("RewriteRule ^/from/a$ /to/b [R=307]", "/redirect/from/a", "/redirect/to/b", 307); + } finally { + HttpURLConnection.setFollowRedirects(originalValue); + } + } + + + @Test + public void testBackReferenceRewrite() throws Exception { + doTestRewrite("RewriteRule ^/b/(rest)?$ /c/$1", "/b/rest", "/c/rest"); + } + + + @Test + public void testEmptyBackReferenceRewrite() throws Exception { + doTestRewrite("RewriteRule ^/b/(rest)?$ /c/$1", "/b/", "/c/"); + } + + + @Test + public void testNegativePattern01() throws Exception { + doTestRewrite("RewriteRule !^/b/.* /c/", "/b", "/c/"); + } + + + @Test + public void testNegativePattern02() throws Exception { + doTestRewrite("RewriteRule !^/b/.* /c/", "/d/e/f", "/c/"); + } + + + @Test + public void testNegativePattern03() throws Exception { + doTestRewrite("RewriteRule !^/c/.* /b/", "/c/", "/c/"); + } + + + @Test + public void testNegativePattern04() throws Exception { + doTestRewrite("RewriteRule !^/c/.* /b/", "/c/d", "/c/d"); + } + + @Test + public void testMultiLine001() throws Exception { + doTestRewrite("RewriteRule /dummy /anotherDummy [L]\nRewriteRule ^/a /c [L]", "/a", "/c"); + } + + @Test + public void testMultiLine002() throws Exception { + doTestRewrite("RewriteRule /dummy /a\nRewriteRule /a /c [L]", "/dummy", "/c"); + } + + private void doTestRewrite(String config, String request, String expectedURI) throws Exception { + doTestRewrite(config, request, expectedURI, null); + } + + + private void doTestRewrite(String config, String request, String expectedURI, String expectedQueryString) + throws Exception { + doTestRewrite(config, request, expectedURI, expectedQueryString, null); + } + + + private void doTestRewrite(String config, String request, String expectedURI, String expectedQueryString, + String expectedAttributeValue) throws Exception { + doTestRewrite(config, request, expectedURI, expectedQueryString, expectedAttributeValue, false); + } + + private void doTestRewrite(String config, String request, String expectedURI, String expectedQueryString, + String expectedAttributeValue, boolean valveSkip) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + RewriteValve rewriteValve = new RewriteValve(); + ctx.getPipeline().addValve(rewriteValve); + if (valveSkip) { + ctx.getPipeline().addValve(new ValveBase() { + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + throw new IllegalStateException(); + } + }); + } + + rewriteValve.setConfiguration(config); + + Tomcat.addServlet(ctx, "snoop", new SnoopServlet()); + ctx.addServletMappingDecoded("/a/%5A", "snoop"); + ctx.addServletMappingDecoded("/c/*", "snoop"); + ctx.addServletMappingDecoded("/W/*", "snoop"); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + request, res, null); + res.setCharset(StandardCharsets.UTF_8); + + if (expectedURI == null) { + // Rewrite is expected to fail. Probably because invalid characters + // were written into the request target + Assert.assertEquals(400, rc); + } else { + String body = res.toString(); + RequestDescriptor requestDesc = SnoopResult.parse(body); + String requestURI = requestDesc.getRequestInfo("REQUEST-URI"); + Assert.assertEquals(expectedURI, requestURI); + + if (expectedQueryString != null) { + String queryString = requestDesc.getRequestInfo("REQUEST-QUERY-STRING"); + Assert.assertEquals(expectedQueryString, queryString); + } + + if (expectedAttributeValue != null) { + String attributeValue = requestDesc.getAttribute("X-Test"); + Assert.assertEquals(expectedAttributeValue, attributeValue); + } + } + } + + private void doTestRedirect(String config, String request, String expectedURI, int expectedStatusCode) + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("redirect", null); + + RewriteValve rewriteValve = new RewriteValve(); + ctx.getPipeline().addValve(rewriteValve); + + rewriteValve.setConfiguration(config); + + Tomcat.addServlet(ctx, "tester", new TesterServlet()); + ctx.addServletMappingDecoded("/from/a", "tester"); + ctx.addServletMappingDecoded("/to/b", "tester"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + int rc = methodUrl("http://localhost:" + getPort() + request, res, DEFAULT_CLIENT_TIMEOUT_MS, null, resHead, + "GET", false); + res.setCharset(StandardCharsets.UTF_8); + + if (expectedURI == null) { + // Rewrite is expected to fail. Probably because invalid characters + // were written into the request target + Assert.assertEquals(400, rc); + } else { + List locations = resHead.get("Location"); + Assert.assertFalse(locations.isEmpty()); + String redirectURI = locations.get(0); + Assert.assertEquals(expectedURI, redirectURI); + Assert.assertEquals(expectedStatusCode, rc); + } + } + + + @Test + public void testCookie() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("redirect", null); + + CookieTestValve cookieTestValve = new CookieTestValve(); + tomcat.getHost().getPipeline().addValve(cookieTestValve); + RewriteValve rewriteValve = new RewriteValve(); + tomcat.getHost().getPipeline().addValve(rewriteValve); + + rewriteValve.setConfiguration("RewriteRule ^/source/(.*) /redirect/$1"); + + Tomcat.addServlet(ctx, "cookieTest", new CookieTestServlet()); + + ctx.addServletMappingDecoded("/", "cookieTest"); + + tomcat.start(); + + Map> reqHead = new HashMap<>(); + reqHead.put("cookie", List.of("test=data")); + ByteChunk res = new ByteChunk(); + int rc = methodUrl("http://localhost:" + getPort() + "/source/cookieTest", res, DEFAULT_CLIENT_TIMEOUT_MS, + reqHead, null, "GET", false); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + res.setCharset(StandardCharsets.UTF_8); + Assert.assertEquals("PASS", res.toString()); + } + + + public static class CookieTestValve extends ValveBase { + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("test".equals(cookie.getName())) { + request.setAttribute("cookieTest", cookie.getValue()); + break; + } + } + } + getNext().invoke(request, response); + } + } + + + public static class CookieTestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + PrintWriter pw = resp.getWriter(); + if (req.getAttribute("cookieTest") != null) { + pw.print("PASS"); + } else { + pw.print("FAIL"); + } + } + } +} diff --git a/test/org/apache/catalina/valves/rewrite/TesterRewriteMapA.java b/test/org/apache/catalina/valves/rewrite/TesterRewriteMapA.java new file mode 100644 index 0000000..434e341 --- /dev/null +++ b/test/org/apache/catalina/valves/rewrite/TesterRewriteMapA.java @@ -0,0 +1,46 @@ +/* + * 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.catalina.valves.rewrite; + +import java.util.HashMap; +import java.util.Map; + +public class TesterRewriteMapA implements RewriteMap { + + private static final Map map = new HashMap<>(); + + static { + map.put("a", "aa"); + map.put("aa", "aaaa"); + map.put("b", "bb"); + } + + @Override + public String setParameters(String params) { + throw new IllegalStateException(); + } + + @Override + public void setParameters(String... params) { + // NO-OP + } + + @Override + public String lookup(String key) { + return map.get(key); + } +} diff --git a/test/org/apache/catalina/webresources/AbstractTestFileResourceSet.java b/test/org/apache/catalina/webresources/AbstractTestFileResourceSet.java new file mode 100644 index 0000000..d61c15b --- /dev/null +++ b/test/org/apache/catalina/webresources/AbstractTestFileResourceSet.java @@ -0,0 +1,82 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import org.junit.Test; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; + +public abstract class AbstractTestFileResourceSet extends AbstractTestResourceSet { + + private final boolean readOnly; + + protected AbstractTestFileResourceSet(boolean readOnly) { + this.readOnly = readOnly; + } + + protected abstract File getDir2(); + + @Override + public WebResourceRoot getWebResourceRoot() { + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = new DirResourceSet(root, "/", getBaseDir().getAbsolutePath(), "/"); + webResourceSet.setReadOnly(readOnly); + root.setMainResources(webResourceSet); + + WebResourceSet f1 = new FileResourceSet(root, "/f1.txt", + "test/webresources/dir1/f1.txt", "/"); + f1.setReadOnly(readOnly); + root.addPreResources(f1); + + WebResourceSet f2 = new FileResourceSet(root, "/f2.txt", + "test/webresources/dir1/f2.txt", "/"); + f2.setReadOnly(readOnly); + root.addPreResources(f2); + + WebResourceSet d1f1 = new FileResourceSet(root, "/d1/d1-f1.txt", + "test/webresources/dir1/d1/d1-f1.txt", "/"); + d1f1.setReadOnly(readOnly); + root.addPreResources(d1f1); + + WebResourceSet d2f1 = new FileResourceSet(root, "/d2/d2-f1.txt", + "test/webresources/dir1/d2/d2-f1.txt", "/"); + d2f1.setReadOnly(readOnly); + root.addPreResources(d2f1); + + return root; + } + + @Override + protected boolean isWritable() { + return !readOnly; + } + + @Override + public File getBaseDir() { + return getDir2(); + } + + @Override + @Test + public void testNoArgConstructor() { + @SuppressWarnings("unused") + Object obj = new FileResourceSet(); + } +} diff --git a/test/org/apache/catalina/webresources/AbstractTestResourceSet.java b/test/org/apache/catalina/webresources/AbstractTestResourceSet.java new file mode 100644 index 0000000..ce4c31b --- /dev/null +++ b/test/org/apache/catalina/webresources/AbstractTestResourceSet.java @@ -0,0 +1,552 @@ +/* + * 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.catalina.webresources; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.Manifest; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; + +public abstract class AbstractTestResourceSet { + + protected WebResourceRoot resourceRoot; + + protected abstract WebResourceRoot getWebResourceRoot(); + protected abstract boolean isWritable(); + + public String getMount() { + return ""; + } + + public abstract File getBaseDir(); + + @Before + public final void setup() throws LifecycleException { + resourceRoot = getWebResourceRoot(); + resourceRoot.start(); + } + + @After + public final void teardown() throws LifecycleException { + resourceRoot.stop(); + resourceRoot.destroy(); + } + + @Test(expected = IllegalArgumentException.class) + public final void testGetResourceEmpty() { + resourceRoot.getResource(""); + } + + //------------------------------------------------------------ getResource() + + @Test + public final void testGetResourceRoot() { + doTestGetResourceRoot(true); + } + + @Test + public final void testGetResourceRootNoSlash() { + doTestGetResourceRoot(false); + } + + + private void doTestGetResourceRoot(boolean slash) { + String mount = getMount(); + if (!slash && mount.length() == 0) { + return; + } + mount = mount + (slash ? "/" : ""); + + WebResource webResource = resourceRoot.getResource(mount); + + Assert.assertTrue(webResource.isDirectory()); + String expected; + if (getMount().length() > 0) { + expected = getMount().substring(1); + } else { + expected = ""; + } + Assert.assertEquals(expected, webResource.getName()); + Assert.assertEquals(mount + (!slash ? "/" : ""), webResource.getWebappPath()); + } + + @Test + public final void testGetResourceDirA() { + WebResource webResource = resourceRoot.getResource(getMount() + "/d1"); + Assert.assertTrue(webResource.isDirectory()); + Assert.assertEquals("d1", webResource.getName()); + Assert.assertEquals(getMount() + "/d1/", webResource.getWebappPath()); + Assert.assertEquals(-1, webResource.getContentLength()); + Assert.assertNull(webResource.getContent()); + Assert.assertNull(webResource.getInputStream()); + } + + @Test + public final void testGetResourceDirB() { + WebResource webResource = resourceRoot.getResource(getMount() + "/d1/"); + Assert.assertTrue(webResource.isDirectory()); + Assert.assertEquals("d1", webResource.getName()); + Assert.assertEquals(getMount() + "/d1/", webResource.getWebappPath()); + Assert.assertEquals(-1, webResource.getContentLength()); + Assert.assertNull(webResource.getContent()); + Assert.assertNull(webResource.getInputStream()); + } + + @Test + public final void testGetResourceFile() { + WebResource webResource = + resourceRoot.getResource(getMount() + "/d1/d1-f1.txt"); + Assert.assertTrue(webResource.isFile()); + Assert.assertEquals("d1-f1.txt", webResource.getName()); + Assert.assertEquals( + getMount() + "/d1/d1-f1.txt", webResource.getWebappPath()); + Assert.assertEquals(0, webResource.getContentLength()); + Assert.assertEquals(0, webResource.getContent().length); + Assert.assertNotNull(webResource.getInputStream()); + } + + @Test + public final void testGetResourceFileWithTrailingSlash() { + WebResource webResource = + resourceRoot.getResource(getMount() + "/d1/d1-f1.txt/"); + Assert.assertFalse(webResource.exists()); + } + + @Test + public final void testGetResourceCaseSensitive() { + WebResource webResource = + resourceRoot.getResource(getMount() + "/d1/d1-F1.txt"); + Assert.assertFalse(webResource.exists()); + } + + @Test + public final void testGetResourceTraversal() { + WebResource webResource = null; + try { + webResource = resourceRoot.getResource(getMount() + "/../"); + } catch (IllegalArgumentException iae) { + // Expected if mount point is zero length + Assert.assertTrue(getMount().length() == 0); + return; + } + + Assert.assertFalse(webResource.exists()); + } + + + //------------------------------------------------------------------- list() + + @Test(expected = IllegalArgumentException.class) + public final void testListEmpty() { + resourceRoot.list(""); + } + + @Test + public final void testListRoot() { + doTestListRoot(true); + } + + @Test + public final void testListRootNoSlash() { + doTestListRoot(false); + } + + + private void doTestListRoot(boolean slash) { + String mount = getMount(); + if (!slash && mount.length() == 0) { + return; + } + + String[] results = resourceRoot.list(mount + (slash ? "/" : "")); + + Set expected = new HashSet<>(); + expected.add("d1"); + expected.add("d2"); + expected.add("f1.txt"); + expected.add("f2.txt"); + + // Directories created by Subversion 1.6 and earlier clients + Set optional = new HashSet<>(); + optional.add(".svn"); + // Files visible in some tests only + optional.add(getMount() + ".ignore-me.txt"); + optional.add("META-INF"); + + for (String result : results) { + Assert.assertTrue(result, + expected.remove(result) || optional.remove(result)); + } + Assert.assertEquals(0, expected.size()); + } + + @Test + public final void testListDirA() { + String[] results = resourceRoot.list(getMount() + "/d1"); + + Set expected = new HashSet<>(); + expected.add("d1-f1.txt"); + + // Directories created by Subversion 1.6 and earlier clients + Set optional = new HashSet<>(); + optional.add(".svn"); + // Files visible in some tests only + optional.add(".ignore-me.txt"); + + for (String result : results) { + Assert.assertTrue(result, + expected.remove(result) || optional.remove(result)); + } + Assert.assertEquals(0, expected.size()); + } + + @Test + public final void testListDirB() { + String[] results = resourceRoot.list(getMount() + "/d1/"); + + Set expected = new HashSet<>(); + expected.add("d1-f1.txt"); + + // Directories created by Subversion 1.6 and earlier clients + Set optional = new HashSet<>(); + optional.add(".svn"); + // Files visible in some tests only + optional.add(".ignore-me.txt"); + + for (String result : results) { + Assert.assertTrue(result, + expected.remove(result) || optional.remove(result)); + } + Assert.assertEquals(0, expected.size()); + } + + @Test + public final void testListFile() { + String[] results = resourceRoot.list(getMount() + "/d1/d1-f1.txt"); + + Assert.assertNotNull(results); + Assert.assertEquals(0, results.length); + } + + //-------------------------------------------------------- listWebAppPaths() + + @Test(expected = IllegalArgumentException.class) + public final void testListWebAppPathsEmpty() { + resourceRoot.listWebAppPaths(""); + } + + @Test + public final void testListWebAppPathsRoot() { + doTestListWebAppPathsRoot(true); + } + + @Test + public final void testListWebAppPathsRootNoSlash() { + doTestListWebAppPathsRoot(false); + } + + + private void doTestListWebAppPathsRoot(boolean slash) { + String mount = getMount(); + if (!slash && mount.length() == 0) { + return; + } + + Set results = resourceRoot.listWebAppPaths(mount + (slash ? "/" : "")); + + Set expected = new HashSet<>(); + expected.add(getMount() + "/d1/"); + expected.add(getMount() + "/d2/"); + expected.add(getMount() + "/f1.txt"); + expected.add(getMount() + "/f2.txt"); + + // Directories created by Subversion 1.6 and earlier clients + Set optional = new HashSet<>(); + optional.add(getMount() + "/.svn/"); + // Files visible in some tests only + optional.add(getMount() + "/.ignore-me.txt"); + // Files visible in some configurations only + optional.add(getMount() + "/META-INF/"); + + for (String result : results) { + Assert.assertTrue(result, + expected.remove(result) || optional.remove(result)); + } + Assert.assertEquals(0, expected.size()); + } + + @Test + public final void testListWebAppPathsDirA() { + Set results = resourceRoot.listWebAppPaths(getMount() + "/d1"); + + Set expected = new HashSet<>(); + expected.add(getMount() + "/d1/d1-f1.txt"); + + // Directories created by Subversion 1.6 and earlier clients + Set optional = new HashSet<>(); + optional.add(getMount() + "/d1/.svn/"); + // Files visible in some tests only + optional.add(getMount() + "/d1/.ignore-me.txt"); + + for (String result : results) { + Assert.assertTrue(result, + expected.remove(result) || optional.remove(result)); + } + Assert.assertEquals(0, expected.size()); + } + + @Test + public final void testListWebAppPathsDirB() { + Set results = resourceRoot.listWebAppPaths(getMount() + "/d1/"); + + Set expected = new HashSet<>(); + expected.add(getMount() + "/d1/d1-f1.txt"); + + // Directories created by Subversion 1.6 and earlier clients + Set optional = new HashSet<>(); + optional.add(getMount() + "/d1/.svn/"); + // Files visible in some tests only + optional.add(getMount() + "/d1/.ignore-me.txt"); + + for (String result : results) { + Assert.assertTrue(result, + expected.remove(result) || optional.remove(result)); + } + Assert.assertEquals(0, expected.size()); + } + + @Test + public final void testListWebAppPathsFile() { + Set results = + resourceRoot.listWebAppPaths(getMount() + "/d1/d1-f1.txt"); + + Assert.assertNull(results); + } + + //------------------------------------------------------------------ mkdir() + + @Test(expected = IllegalArgumentException.class) + public final void testMkdirEmpty() { + resourceRoot.mkdir(""); + } + + @Test + public final void testMkdirRoot() { + Assert.assertFalse(resourceRoot.mkdir(getMount() + "/")); + } + + @Test + public final void testMkdirDirA() { + WebResource d1 = resourceRoot.getResource(getMount() + "/d1"); + if (d1.exists()) { + Assert.assertFalse(resourceRoot.mkdir(getMount() + "/d1")); + } else if (d1.isVirtual()) { + Assert.assertTrue(resourceRoot.mkdir(getMount() + "/d1")); + File file = new File(getBaseDir(), "d1"); + Assert.assertTrue(file.isDirectory()); + Assert.assertTrue(file.delete()); + } else { + Assert.fail("Unhandled condition in unit test"); + } + } + + @Test + public final void testMkdirDirB() { + WebResource d1 = resourceRoot.getResource(getMount() + "/d1/"); + if (d1.exists()) { + Assert.assertFalse(resourceRoot.mkdir(getMount() + "/d1/")); + } else if (d1.isVirtual()) { + Assert.assertTrue(resourceRoot.mkdir(getMount() + "/d1/")); + File file = new File(getBaseDir(), "d1"); + Assert.assertTrue(file.isDirectory()); + Assert.assertTrue(file.delete()); + } else { + Assert.fail("Unhandled condition in unit test"); + } + } + + @Test + public final void testMkdirFile() { + Assert.assertFalse(resourceRoot.mkdir(getMount() + "/d1/d1-f1.txt")); + } + + @Test + public final void testMkdirNew() { + String newDirName = getNewDirName(); + if (isWritable()) { + Assert.assertTrue(resourceRoot.mkdir(getMount() + "/" + newDirName)); + + File file = new File(getBaseDir(), newDirName); + Assert.assertTrue(file.isDirectory()); + Assert.assertTrue(file.delete()); + } else { + Assert.assertFalse(resourceRoot.mkdir(getMount() + "/" + newDirName)); + } + } + + protected abstract String getNewDirName(); + + //------------------------------------------------------------------ write() + + @Test(expected = IllegalArgumentException.class) + public final void testWriteEmpty() { + InputStream is = new ByteArrayInputStream("test".getBytes()); + resourceRoot.write("", is, false); + } + + @Test + public final void testWriteRoot() { + InputStream is = new ByteArrayInputStream("test".getBytes()); + Assert.assertFalse(resourceRoot.write(getMount() + "/", is, false)); + } + + @Test + public final void testWriteDirA() { + WebResource d1 = resourceRoot.getResource(getMount() + "/d1"); + InputStream is = new ByteArrayInputStream("test".getBytes()); + if (d1.exists()) { + Assert.assertFalse(resourceRoot.write(getMount() + "/d1", is, false)); + } else if (d1.isVirtual()) { + Assert.assertTrue(resourceRoot.write( + getMount() + "/d1", is, false)); + File file = new File(getBaseDir(), "d1"); + Assert.assertTrue(file.exists()); + Assert.assertTrue(file.delete()); + } else { + Assert.fail("Unhandled condition in unit test"); + } + } + + @Test + public final void testWriteDirB() { + WebResource d1 = resourceRoot.getResource(getMount() + "/d1/"); + InputStream is = new ByteArrayInputStream("test".getBytes()); + if (d1.exists() || d1.isVirtual()) { + Assert.assertFalse(resourceRoot.write(getMount() + "/d1/", is, false)); + } else { + Assert.fail("Unhandled condition in unit test"); + } + } + + @Test + public final void testWriteFile() { + InputStream is = new ByteArrayInputStream("test".getBytes()); + Assert.assertFalse(resourceRoot.write( + getMount() + "/d1/d1-f1.txt", is, false)); + } + + @Test(expected = NullPointerException.class) + public final void testWriteNull() { + resourceRoot.write(getMount() + "/" + getNewFileNameNull(), null, false); + } + + protected abstract String getNewFileNameNull(); + + @Test + public final void testWrite() { + String newFileName = getNewFileName(); + InputStream is = new ByteArrayInputStream("test".getBytes()); + if (isWritable()) { + Assert.assertTrue(resourceRoot.write( + getMount() + "/" + newFileName, is, false)); + File file = new File(getBaseDir(), newFileName); + Assert.assertTrue(file.exists()); + Assert.assertTrue(file.delete()); + } else { + Assert.assertFalse(resourceRoot.write( + getMount() + "/" + newFileName, is, false)); + } + } + + @Test + public final void testWriteWithTrailingSlash() { + String newFileName = getNewFileName() + "/"; + InputStream is = new ByteArrayInputStream("test".getBytes()); + Assert.assertFalse(resourceRoot.write( + getMount() + "/" + newFileName, is, false)); + } + + protected abstract String getNewFileName(); + + // ------------------------------------------------------ getCanonicalPath() + + @Test + public final void testGetCanonicalPathExists() { + WebResource exists = + resourceRoot.getResource(getMount() + "/d1/d1-f1.txt"); + String existsCanonicalPath = exists.getCanonicalPath(); + + URL existsUrl = exists.getURL(); + if ("file".equals(existsUrl.getProtocol())) { + // Should have a canonical path + Assert.assertNotNull(existsCanonicalPath); + } else { + Assert.assertNull(existsCanonicalPath); + } + } + + @Test + public final void testGetCanonicalPathDoesNotExist() { + WebResource exists = + resourceRoot.getResource(getMount() + "/d1/d1-f1.txt"); + WebResource doesNotExist = + resourceRoot.getResource(getMount() + "/d1/dummy.txt"); + String doesNotExistCanonicalPath = doesNotExist.getCanonicalPath(); + + URL existsUrl = exists.getURL(); + if ("file".equals(existsUrl.getProtocol())) { + // Should be possible to construct a canonical path for a resource + // that doesn't exist given that a resource that does exist in the + // same directory has a URL with the file protocol + Assert.assertNotNull(doesNotExistCanonicalPath); + } else { + Assert.assertNull(doesNotExistCanonicalPath); + } + } + + + // ----------------------------------------------------------- getManifest() + + @Test + public final void testGetManifest() { + WebResource exists = resourceRoot.getResource(getMount() + "/d1/d1-f1.txt"); + boolean manifestExists = resourceRoot.getResource("/META-INF/MANIFEST.MF").exists(); + Manifest m = exists.getManifest(); + if (getMount().equals("") && manifestExists) { + Assert.assertNotNull(m); + } else { + Assert.assertNull(m); + } + } + + + // ------------------------------------------------------------ constructors + + public abstract void testNoArgConstructor(); +} diff --git a/test/org/apache/catalina/webresources/AbstractTestResourceSetMount.java b/test/org/apache/catalina/webresources/AbstractTestResourceSetMount.java new file mode 100644 index 0000000..3b84d67 --- /dev/null +++ b/test/org/apache/catalina/webresources/AbstractTestResourceSetMount.java @@ -0,0 +1,75 @@ +/* + * 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.catalina.webresources; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.WebResource; + +public abstract class AbstractTestResourceSetMount + extends AbstractTestResourceSet { + + @Override + public final String getMount() { + return "/mount"; + } + + @Test + public final void testGetResourceAbove() { + WebResource webResource = resourceRoot.getResource("/"); + Assert.assertFalse(webResource.exists()); + } + + @Test + public final void testListAbove() { + String[] results = resourceRoot.list("/"); + + Assert.assertNotNull(results); + Assert.assertEquals(1, results.length); + Assert.assertEquals(getMount().substring(1), results[0]); + } + + @Test + public final void testListWebAppPathsAbove() { + Set results = resourceRoot.listWebAppPaths("/"); + + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + Assert.assertTrue(results.contains(getMount() + "/")); + } + + @Test + public void testMkdirAbove() { + Assert.assertFalse(resourceRoot.mkdir("/")); + } + + @Test + public void testWriteAbove() { + InputStream is = new ByteArrayInputStream("test".getBytes()); + Assert.assertFalse(resourceRoot.write("/", is, false)); + } + + @Override + public void testNoArgConstructor() { + // NO-OP + } +} diff --git a/test/org/apache/catalina/webresources/TestAbstractArchiveResource.java b/test/org/apache/catalina/webresources/TestAbstractArchiveResource.java new file mode 100644 index 0000000..9c59fd8 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestAbstractArchiveResource.java @@ -0,0 +1,77 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.WebResource; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestAbstractArchiveResource extends TomcatBaseTest { + + @Test + public void testNestedJarGetURL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File docBase = new File("test/webresources/war-url-connection.war"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + skipTldsForResourceJars(ctx); + + ((StandardHost) tomcat.getHost()).setUnpackWARs(false); + + tomcat.start(); + + WebResource webResource = + ctx.getResources().getClassLoaderResource("/META-INF/resources/index.html"); + + StringBuilder expectedURL = new StringBuilder("jar:war:"); + expectedURL.append(docBase.getCanonicalFile().toURI().toURL().toString()); + expectedURL.append("*/WEB-INF/lib/test.jar!/META-INF/resources/index.html"); + + Assert.assertEquals(expectedURL.toString(), webResource.getURL().toString()); + } + + + @Test + public void testJarGetURL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File docBase = new File("test/webapp"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + skipTldsForResourceJars(ctx); + + ((StandardHost) tomcat.getHost()).setUnpackWARs(false); + + tomcat.start(); + + WebResource webResource = + ctx.getResources().getClassLoaderResource("/META-INF/tags/echo.tag"); + + StringBuilder expectedURL = new StringBuilder("jar:"); + expectedURL.append(docBase.getCanonicalFile().toURI().toURL().toString()); + expectedURL.append("WEB-INF/lib/test-lib.jar!/META-INF/tags/echo.tag"); + + Assert.assertEquals(expectedURL.toString(), webResource.getURL().toString()); + } + +} diff --git a/test/org/apache/catalina/webresources/TestAbstractArchiveResourceSet.java b/test/org/apache/catalina/webresources/TestAbstractArchiveResourceSet.java new file mode 100644 index 0000000..ba5f742 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestAbstractArchiveResourceSet.java @@ -0,0 +1,115 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.lang.reflect.Field; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; + +public class TestAbstractArchiveResourceSet { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=65586 + */ + @Test + public void testBloomFilterWithDirectoryVerifyCleanup() throws Exception { + WebResourceRoot root = new TesterWebResourceRoot(); + + root.setArchiveIndexStrategy(WebResourceRoot.ArchiveIndexStrategy.BLOOM.name()); + + File file = new File("webapps/examples/WEB-INF/lib/taglibs-standard-impl-1.2.5-migrated-0.0.1.jar"); + + JarResourceSet jarResourceSet = new JarResourceSet(root, "/WEB-INF/classes", file.getAbsolutePath(), "/"); + jarResourceSet.getArchiveEntries(false); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + WebResource r1 = jarResourceSet.getResource("/WEB-INF/classes/org/"); + Assert.assertTrue(r1.isDirectory()); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + WebResource r2 = jarResourceSet.getResource("/WEB-INF/classes/org"); + Assert.assertTrue(r2.isDirectory()); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + jarResourceSet.gc(); + Assert.assertNotNull(getJarContents(jarResourceSet)); + } + + @Test + public void testPurgedBloomFilterWithDirectoryVerifyCleanup() throws Exception { + WebResourceRoot root = new TesterWebResourceRoot(); + + root.setArchiveIndexStrategy(WebResourceRoot.ArchiveIndexStrategy.PURGED.name()); + + File file = new File("webapps/examples/WEB-INF/lib/taglibs-standard-impl-1.2.5-migrated-0.0.1.jar"); + + JarResourceSet jarResourceSet = new JarResourceSet(root, "/WEB-INF/classes", file.getAbsolutePath(), "/"); + jarResourceSet.getArchiveEntries(false); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + WebResource r1 = jarResourceSet.getResource("/WEB-INF/classes/org/"); + Assert.assertTrue(r1.isDirectory()); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + WebResource r2 = jarResourceSet.getResource("/WEB-INF/classes/org"); + Assert.assertTrue(r2.isDirectory()); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + jarResourceSet.gc(); + Assert.assertNull(getJarContents(jarResourceSet)); + } + + @Deprecated + @Test + public void testBloomFilterWithSimpleArchiveIndexing() throws Exception { + WebResourceRoot root = new TesterWebResourceRoot(); + + root.setArchiveIndexStrategy(WebResourceRoot.ArchiveIndexStrategy.SIMPLE.name()); + root.getContext().setUseBloomFilterForArchives(true); + + File file = new File("webapps/examples/WEB-INF/lib/taglibs-standard-impl-1.2.5-migrated-0.0.1.jar"); + + JarResourceSet jarResourceSet = new JarResourceSet(root, "/WEB-INF/classes", file.getAbsolutePath(), "/"); + jarResourceSet.getArchiveEntries(false); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + WebResource r1 = jarResourceSet.getResource("/WEB-INF/classes/org/"); + Assert.assertTrue(r1.isDirectory()); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + WebResource r2 = jarResourceSet.getResource("/WEB-INF/classes/org"); + Assert.assertTrue(r2.isDirectory()); + Assert.assertNotNull(getJarContents(jarResourceSet)); + + jarResourceSet.gc(); + Assert.assertNull(getJarContents(jarResourceSet)); + } + + private JarContents getJarContents(Object target) + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + Field field = AbstractArchiveResourceSet.class.getDeclaredField("jarContents"); + field.setAccessible(true); + JarContents contents = (JarContents) field.get(target); + + return contents; + } +} diff --git a/test/org/apache/catalina/webresources/TestCachedResource.java b/test/org/apache/catalina/webresources/TestCachedResource.java new file mode 100644 index 0000000..7e890c7 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestCachedResource.java @@ -0,0 +1,134 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestCachedResource extends TomcatBaseTest { + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=63872 + @Test + public void testUrlFileFromDirectory() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + File docBase = new File("test/webresources/dir1"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + tomcat.start(); + + WebResourceRoot root = ctx.getResources(); + + URL d1 = root.getResource("/d1").getURL(); + + URL d1f1 = new URL(d1, "d1-f1.txt"); + + try (InputStream is = d1f1.openStream()) { + Assert.assertNotNull(is); + } + } + + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=63970 + @Test + public void testCachedJarUrlConnection() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + File docBase = new File("test/webresources/war-url-connection.war"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + tomcat.start(); + + WebResourceRoot root = ctx.getResources(); + + // WAR contains a resources JAR so this should return a JAR URL + URL webinf = root.getResource("/index.html").getURL(); + + Assert.assertEquals("jar", webinf.getProtocol()); + JarURLConnection jarConn = null; + try { + jarConn = (JarURLConnection) webinf.openConnection(); + } catch (ClassCastException e) { + // Ignore + } + Assert.assertNotNull(jarConn); + } + + + @Test + public void testDirectoryListingsPackedWar() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + File docBase = new File("test/webresources/war-url-connection.war"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + ((StandardHost) tomcat.getHost()).setUnpackWARs(false); + tomcat.start(); + + WebResourceRoot root = ctx.getResources(); + + URL d1 = root.getResource("/").getURL(); + + try (InputStream is = d1.openStream()) { + Assert.assertNotNull(is); + } + } + + + @Test + public void testDirectoryListingsWar() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + File docBase = new File("test/webresources/war-url-connection.war"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + tomcat.start(); + + WebResourceRoot root = ctx.getResources(); + + URL d1 = root.getResource("/").getURL(); + + try (InputStream is = d1.openStream()) { + Assert.assertNotNull(is); + } + } + + + @Test + public void testDirectoryListingsDir() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + File docBase = new File("test/webresources/dir1"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + tomcat.start(); + + WebResourceRoot root = ctx.getResources(); + + URL d1 = root.getResource("/d1").getURL(); + + try (InputStream is = d1.openStream()) { + Assert.assertNotNull(is); + } + } +} diff --git a/test/org/apache/catalina/webresources/TestClasspathUrlStreamHandler.java b/test/org/apache/catalina/webresources/TestClasspathUrlStreamHandler.java new file mode 100644 index 0000000..f1bb097 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestClasspathUrlStreamHandler.java @@ -0,0 +1,44 @@ +/* + * 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.catalina.webresources; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Properties; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestClasspathUrlStreamHandler { + + @BeforeClass + public static void setup() { + TomcatURLStreamHandlerFactory.getInstance(); + } + + @Test + public void testClasspathURL01() throws IOException { + URL u = new URL("classpath:/org/apache/catalina/webresources/LocalStrings.properties"); + InputStream is = u.openStream(); + Properties p = new Properties(); + p.load(is); + String msg = (String) p.get("dirResourceSet.writeNpe"); + Assert.assertEquals("The input stream may not be null", msg); + } +} diff --git a/test/org/apache/catalina/webresources/TestDirResourceSet.java b/test/org/apache/catalina/webresources/TestDirResourceSet.java new file mode 100644 index 0000000..f636c18 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestDirResourceSet.java @@ -0,0 +1,93 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestDirResourceSet extends AbstractTestResourceSet { + + private static Path tempDir; + private static File dir1; + + @BeforeClass + public static void before() throws IOException { + tempDir = Files.createTempDirectory("test", new FileAttribute[0]); + dir1 = new File(tempDir.toFile(), "dir1"); + TomcatBaseTest.recursiveCopy(new File("test/webresources/dir1").toPath(), dir1.toPath()); + } + + @AfterClass + public static void after() { + ExpandWar.delete(tempDir.toFile()); + } + + + @Override + public WebResourceRoot getWebResourceRoot() { + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = + new DirResourceSet(root, "/", getBaseDir().getAbsolutePath(), "/"); + webResourceSet.setReadOnly(false); + root.setMainResources(webResourceSet); + return root; + } + + @Override + protected boolean isWritable() { + return true; + } + + @Override + public File getBaseDir() { + return dir1; + } + + @Override + @Test + public void testNoArgConstructor() { + @SuppressWarnings("unused") + Object obj = new DirResourceSet(); + } + + @Override + protected String getNewDirName() { + return "test-dir-01"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-01"; + } + + @Override + protected String getNewFileName() { + return "test-file-01"; + } +} diff --git a/test/org/apache/catalina/webresources/TestDirResourceSetInternal.java b/test/org/apache/catalina/webresources/TestDirResourceSetInternal.java new file mode 100644 index 0000000..ffb7a6d --- /dev/null +++ b/test/org/apache/catalina/webresources/TestDirResourceSetInternal.java @@ -0,0 +1,89 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestDirResourceSetInternal extends AbstractTestResourceSet { + + private static Path tempDir; + private static File dir1; + + @BeforeClass + public static void before() throws IOException { + tempDir = Files.createTempDirectory("test", new FileAttribute[0]); + dir1 = new File(tempDir.toFile(), "dir1"); + TomcatBaseTest.recursiveCopy(new File("test/webresources/dir1").toPath(), dir1.toPath()); + } + + @AfterClass + public static void after() { + ExpandWar.delete(tempDir.toFile()); + } + + + @Override + public WebResourceRoot getWebResourceRoot() { + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = + new DirResourceSet(root, "/", tempDir.toAbsolutePath().toString(), "/dir1"); + root.setMainResources(webResourceSet); + return root; + } + + @Override + protected boolean isWritable() { + return true; + } + + @Override + public File getBaseDir() { + return dir1; + } + + @Override + public void testNoArgConstructor() { + // NO-OP. Tested in TestDirResource + } + + @Override + protected String getNewDirName() { + return "test-dir-02"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-02"; + } + + @Override + protected String getNewFileName() { + return "test-file-02"; + } +} diff --git a/test/org/apache/catalina/webresources/TestDirResourceSetMount.java b/test/org/apache/catalina/webresources/TestDirResourceSetMount.java new file mode 100644 index 0000000..9282669 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestDirResourceSetMount.java @@ -0,0 +1,85 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestDirResourceSetMount extends AbstractTestResourceSetMount { + + private static Path tempDir; + private static File dir1; + + @BeforeClass + public static void before() throws IOException { + tempDir = Files.createTempDirectory("test", new FileAttribute[0]); + dir1 = new File(tempDir.toFile(), "dir1"); + TomcatBaseTest.recursiveCopy(new File("test/webresources/dir1").toPath(), dir1.toPath()); + } + + @AfterClass + public static void after() { + ExpandWar.delete(tempDir.toFile()); + } + + + @Override + public WebResourceRoot getWebResourceRoot() { + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = + new DirResourceSet(new TesterWebResourceRoot(), getMount(), + getBaseDir().getAbsolutePath(), "/"); + root.setMainResources(webResourceSet); + return root; + } + + @Override + protected boolean isWritable() { + return true; + } + + @Override + public File getBaseDir() { + return dir1; + } + + @Override + protected String getNewDirName() { + return "test-dir-03"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-03"; + } + + @Override + protected String getNewFileName() { + return "test-file-03"; + } +} diff --git a/test/org/apache/catalina/webresources/TestDirResourceSetReadOnly.java b/test/org/apache/catalina/webresources/TestDirResourceSetReadOnly.java new file mode 100644 index 0000000..02ba3b2 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestDirResourceSetReadOnly.java @@ -0,0 +1,65 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; + +public class TestDirResourceSetReadOnly extends AbstractTestResourceSet { + + @Override + public WebResourceRoot getWebResourceRoot() { + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = + new DirResourceSet(root, "/", getBaseDir().getAbsolutePath(), "/"); + webResourceSet.setReadOnly(true); + root.setMainResources(webResourceSet); + return root; + } + + @Override + protected boolean isWritable() { + return false; + } + + @Override + public File getBaseDir() { + return new File("test/webresources/dir1"); + } + + @Override + public void testNoArgConstructor() { + // NO-OP. Tested in TestDirResource + } + + @Override + protected String getNewDirName() { + return "test-dir-04"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-04"; + } + + @Override + protected String getNewFileName() { + return "test-file-04"; + } +} diff --git a/test/org/apache/catalina/webresources/TestDirResourceSetVirtual.java b/test/org/apache/catalina/webresources/TestDirResourceSetVirtual.java new file mode 100644 index 0000000..02c7736 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestDirResourceSetVirtual.java @@ -0,0 +1,107 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestDirResourceSetVirtual extends AbstractTestResourceSet { + + private static Path tempDir; + private static File dir1; + + @BeforeClass + public static void before() throws IOException { + tempDir = Files.createTempDirectory("test", new FileAttribute[0]); + dir1 = new File(tempDir.toFile(), "dir1"); + TomcatBaseTest.recursiveCopy(new File("test/webresources/dir1").toPath(), dir1.toPath()); + } + + @AfterClass + public static void after() { + ExpandWar.delete(tempDir.toFile()); + } + + + @Override + public WebResourceRoot getWebResourceRoot() { + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = + new DirResourceSet(new TesterWebResourceRoot(), "/", + getBaseDir().getAbsolutePath(), "/"); + root.setMainResources(webResourceSet); + + WebResourceSet f1 = new FileResourceSet(root, "/f1.txt", + dir1.getAbsolutePath() + "/f1.txt", "/"); + root.addPreResources(f1); + + WebResourceSet f2 = new FileResourceSet(root, "/f2.txt", + dir1.getAbsolutePath() + "/f2.txt", "/"); + root.addPreResources(f2); + + WebResourceSet d1 = new DirResourceSet(root, "/d1", + dir1.getAbsolutePath() + "/d1", "/"); + root.addPreResources(d1); + + WebResourceSet d2 = new DirResourceSet(root, "/d2", + dir1.getAbsolutePath() + "/d2", "/"); + root.addPreResources(d2); + + return root; + } + + @Override + protected boolean isWritable() { + return true; + } + + @Override + public File getBaseDir() { + return new File("test/webresources/dir3"); + } + + @Override + public void testNoArgConstructor() { + // NO-OP. Tested in TestDirResource + } + + @Override + protected String getNewDirName() { + return "test-dir-05"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-05"; + } + + @Override + protected String getNewFileName() { + return "test-file-05"; + } +} diff --git a/test/org/apache/catalina/webresources/TestFileResource.java b/test/org/apache/catalina/webresources/TestFileResource.java new file mode 100644 index 0000000..53916e9 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestFileResource.java @@ -0,0 +1,45 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestFileResource extends TomcatBaseTest { + + @Test + public void testGetCodePath() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk out = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug58096.jsp", out, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // Build the expected location the same way the webapp base dir is built + File f = new File("test/webapp/WEB-INF/classes"); + Assert.assertEquals(f.getCanonicalFile().toURI().toURL().toString(), out.toString().trim()); + } +} diff --git a/test/org/apache/catalina/webresources/TestFileResourceSet.java b/test/org/apache/catalina/webresources/TestFileResourceSet.java new file mode 100644 index 0000000..59ea590 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestFileResourceSet.java @@ -0,0 +1,72 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestFileResourceSet extends AbstractTestFileResourceSet { + + private static Path tempDir; + private static File dir2; + + @BeforeClass + public static void before() throws IOException { + tempDir = Files.createTempDirectory("test", new FileAttribute[0]); + dir2 = new File(tempDir.toFile(), "dir2"); + TomcatBaseTest.recursiveCopy(new File("test/webresources/dir2").toPath(), dir2.toPath()); + } + + @AfterClass + public static void after() { + ExpandWar.delete(tempDir.toFile()); + } + + + public TestFileResourceSet() { + super(false); + } + + @Override + protected File getDir2() { + return dir2; + } + + @Override + protected String getNewDirName() { + return "test-dir-06"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-06"; + } + + @Override + protected String getNewFileName() { + return "test-file-06"; + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/webresources/TestFileResourceSetReadOnly.java b/test/org/apache/catalina/webresources/TestFileResourceSetReadOnly.java new file mode 100644 index 0000000..732617e --- /dev/null +++ b/test/org/apache/catalina/webresources/TestFileResourceSetReadOnly.java @@ -0,0 +1,72 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestFileResourceSetReadOnly extends AbstractTestFileResourceSet { + + private static Path tempDir; + private static File dir2; + + @BeforeClass + public static void before() throws IOException { + tempDir = Files.createTempDirectory("test", new FileAttribute[0]); + dir2 = new File(tempDir.toFile(), "dir2"); + TomcatBaseTest.recursiveCopy(new File("test/webresources/dir2").toPath(), dir2.toPath()); + } + + @AfterClass + public static void after() { + ExpandWar.delete(tempDir.toFile()); + } + + + public TestFileResourceSetReadOnly() { + super(true); + } + + @Override + protected File getDir2() { + return dir2; + } + + @Override + protected String getNewDirName() { + return "test-dir-07"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-07"; + } + + @Override + protected String getNewFileName() { + return "test-file-07"; + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/webresources/TestFileResourceSetVirtual.java b/test/org/apache/catalina/webresources/TestFileResourceSetVirtual.java new file mode 100644 index 0000000..539ac0e --- /dev/null +++ b/test/org/apache/catalina/webresources/TestFileResourceSetVirtual.java @@ -0,0 +1,77 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.TomcatBaseTest; + +/** + * Mounts file resources in sub directories that do not exist in the main + * resources. + */ +public class TestFileResourceSetVirtual extends TestFileResourceSet { + + private static Path tempDir; + private static File dir2; + + @BeforeClass + public static void before() throws IOException { + tempDir = Files.createTempDirectory("test", new FileAttribute[0]); + dir2 = new File(tempDir.toFile(), "dir2"); + TomcatBaseTest.recursiveCopy(new File("test/webresources/dir2").toPath(), dir2.toPath()); + } + + @AfterClass + public static void after() { + ExpandWar.delete(tempDir.toFile()); + } + + + @Override + public File getBaseDir() { + return new File("test/webresources/dir3"); + } + + @Override + protected File getDir2() { + return dir2; + } + + @Override + protected String getNewDirName() { + return "test-dir-11"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-11"; + } + + @Override + protected String getNewFileName() { + return "test-file-11"; + } +} diff --git a/test/org/apache/catalina/webresources/TestJarContents.java b/test/org/apache/catalina/webresources/TestJarContents.java new file mode 100644 index 0000000..6d55281 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestJarContents.java @@ -0,0 +1,93 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.util.jar.JarFile; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.WebResourceSet; + +/** + * @author Kamnani, Jatin + */ +public class TestJarContents { + + private static File empty; + private static File jar; + private static TesterWebResourceRoot root; + private static WebResourceSet webResourceSet; + private static JarResourceSet test; + private static JarContents testJarContentsObject; + + @BeforeClass + public static void setup() { + try { + empty = new File("test/webresources/dir3"); + jar = new File("test/webresources/dir1.jar"); + + root = new TesterWebResourceRoot(); + + // Use empty dir for root of web app. + webResourceSet = new DirResourceSet(root, "/", empty.getAbsolutePath(), "/"); + root.setMainResources(webResourceSet); + + // If this JAR was in a web application, this is equivalent to how it + // would be added + test = new JarResourceSet(root, "/", jar.getAbsolutePath(), "/META-INF/resources"); + test.setStaticOnly(true); + root.addJarResources(test); + + testJarContentsObject = new JarContents(new JarFile("test/webresources/dir1.jar")); + + } catch (Exception e) { + Assert.fail("Error happened while testing JarContents, " + e.getMessage()); + } + } + + @Test + public void testMightContainResource() { + Assert.assertTrue(testJarContentsObject.mightContainResource( + "/d1/d1-f1.txt", jar.getAbsolutePath())); + + Assert.assertTrue(testJarContentsObject.mightContainResource( + "d1/d1-f1.txt", jar.getAbsolutePath())); + + Assert.assertFalse(testJarContentsObject.mightContainResource( + "/d7/d1-f1.txt", jar.getAbsolutePath())); + + Assert.assertFalse(testJarContentsObject.mightContainResource( + "/", jar.getAbsolutePath())); + + Assert.assertFalse(testJarContentsObject.mightContainResource( + "/////", jar.getAbsolutePath())); + + } + + @Test(expected = StringIndexOutOfBoundsException.class) + public void testStringOutOfBoundExceptions() { + testJarContentsObject.mightContainResource("", jar.getAbsolutePath()); + } + + @Test(expected = NullPointerException.class) + public void testNullPointerExceptions() { + testJarContentsObject.mightContainResource(null, jar.getAbsolutePath()); + } +} diff --git a/test/org/apache/catalina/webresources/TestJarInputStreamWrapper.java b/test/org/apache/catalina/webresources/TestJarInputStreamWrapper.java new file mode 100644 index 0000000..49ccacd --- /dev/null +++ b/test/org/apache/catalina/webresources/TestJarInputStreamWrapper.java @@ -0,0 +1,141 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.WebResource; + +public class TestJarInputStreamWrapper { + + @Test + public void testReadAfterClose() throws Exception { + Method m = InputStream.class.getMethod("read", (Class[]) null); + testMethodAfterClose(m, (Object[]) null); + } + + + @Test + public void testSkipAfterClose() throws Exception { + Method m = InputStream.class.getMethod("skip", long.class); + testMethodAfterClose(m, Long.valueOf(1)); + } + + + @Test + public void testAvailableAfterClose() throws Exception { + Method m = InputStream.class.getMethod("available", (Class[]) null); + testMethodAfterClose(m, (Object[]) null); + } + + + @Test + public void testCloseAfterClose() throws Exception { + Method m = InputStream.class.getMethod("close", (Class[]) null); + testMethodAfterClose(m, (Object[]) null); + } + + + @Test + public void testMarkAfterClose() throws Exception { + Method m = InputStream.class.getMethod("mark", int.class); + testMethodAfterClose(m, Integer.valueOf(1)); + } + + + @Test + public void testResetAfterClose() throws Exception { + Method m = InputStream.class.getMethod("reset", (Class[]) null); + testMethodAfterClose(m, (Object[]) null); + } + + + @Test + public void testMarkSupportedAfterClose() throws Exception { + Method m = InputStream.class.getMethod("markSupported", (Class[]) null); + testMethodAfterClose(m, (Object[]) null); + } + + + private void testMethodAfterClose(Method m, Object... params) throws IOException { + InputStream unwrapped = getUnwrappedClosedInputStream(); + InputStream wrapped = getWrappedClosedInputStream(); + + Object unwrappedReturn = null; + Exception unwrappedException = null; + Object wrappedReturn = null; + Exception wrappedException = null; + + try { + unwrappedReturn = m.invoke(unwrapped, params); + } catch (Exception e) { + unwrappedException = e; + } + + try { + wrappedReturn = m.invoke(wrapped, params); + } catch (Exception e) { + wrappedException = e; + } + + if (unwrappedReturn == null) { + Assert.assertNull(wrappedReturn); + } else { + Assert.assertNotNull(wrappedReturn); + Assert.assertEquals(unwrappedReturn, wrappedReturn); + } + + if (unwrappedException == null) { + Assert.assertNull(wrappedException); + } else { + Assert.assertNotNull(wrappedException); + Assert.assertEquals(unwrappedException.getClass(), wrappedException.getClass()); + } + } + + + private InputStream getUnwrappedClosedInputStream() throws IOException { + File file = new File("test/webresources/non-static-resources.jar"); + JarFile jarFile = new JarFile(file, true, ZipFile.OPEN_READ, Runtime.version()); + ZipEntry jarEntry = jarFile.getEntry("META-INF/MANIFEST.MF"); + InputStream unwrapped = jarFile.getInputStream(jarEntry); + unwrapped.close(); + jarFile.close(); + return unwrapped; + } + + + private InputStream getWrappedClosedInputStream() throws IOException { + StandardRoot root = new StandardRoot(); + root.setCachingAllowed(false); + JarResourceSet jarResourceSet = + new JarResourceSet(root, "/", "test/webresources/non-static-resources.jar", "/"); + WebResource webResource = jarResourceSet.getResource("/META-INF/MANIFEST.MF"); + InputStream wrapped = webResource.getInputStream(); + wrapped.close(); + return wrapped; + } +} diff --git a/test/org/apache/catalina/webresources/TestJarResourceSet.java b/test/org/apache/catalina/webresources/TestJarResourceSet.java new file mode 100644 index 0000000..8aa14c9 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestJarResourceSet.java @@ -0,0 +1,69 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import org.junit.Test; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; + +public class TestJarResourceSet extends AbstractTestResourceSet { + + @Override + public WebResourceRoot getWebResourceRoot() { + File f = new File("test/webresources/dir1.jar"); + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = + new JarResourceSet(root, "/", f.getAbsolutePath(), "/"); + root.setMainResources(webResourceSet); + return root; + } + + @Override + protected boolean isWritable() { + return false; + } + + @Override + public File getBaseDir() { + return new File("test/webresources"); + } + + @Override + @Test + public void testNoArgConstructor() { + @SuppressWarnings("unused") + Object obj = new JarResourceSet(); + } + + @Override + protected String getNewDirName() { + return "test-dir-08"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-08"; + } + + @Override + protected String getNewFileName() { + return "test-file-08"; + } +} diff --git a/test/org/apache/catalina/webresources/TestJarResourceSetInternal.java b/test/org/apache/catalina/webresources/TestJarResourceSetInternal.java new file mode 100644 index 0000000..2718849 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestJarResourceSetInternal.java @@ -0,0 +1,69 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import org.junit.Test; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; + +public class TestJarResourceSetInternal extends AbstractTestResourceSet { + + @Override + public WebResourceRoot getWebResourceRoot() { + File f = new File("test/webresources/dir1-internal.jar"); + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = + new JarResourceSet(root, "/", f.getAbsolutePath(), "/dir1"); + root.setMainResources(webResourceSet); + return root; + } + + @Override + protected boolean isWritable() { + return false; + } + + @Override + public File getBaseDir() { + return new File("test/webresources"); + } + + @Override + @Test + public void testNoArgConstructor() { + @SuppressWarnings("unused") + Object obj = new JarResourceSet(); + } + + @Override + protected String getNewDirName() { + return "test-dir-09"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-09"; + } + + @Override + protected String getNewFileName() { + return "test-file-09"; + } +} diff --git a/test/org/apache/catalina/webresources/TestJarResourceSetMount.java b/test/org/apache/catalina/webresources/TestJarResourceSetMount.java new file mode 100644 index 0000000..31a04cd --- /dev/null +++ b/test/org/apache/catalina/webresources/TestJarResourceSetMount.java @@ -0,0 +1,60 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; + +public class TestJarResourceSetMount extends AbstractTestResourceSetMount { + + @Override + public WebResourceRoot getWebResourceRoot() { + File f = new File("test/webresources/dir1.jar"); + TesterWebResourceRoot root = new TesterWebResourceRoot(); + WebResourceSet webResourceSet = + new JarResourceSet(root, getMount(), f.getAbsolutePath(), "/"); + root.setMainResources(webResourceSet); + return root; + } + + @Override + protected boolean isWritable() { + return false; + } + + @Override + public File getBaseDir() { + return new File("test/webresources"); + } + + @Override + protected String getNewDirName() { + return "test-dir-10"; + } + + @Override + protected String getNewFileNameNull() { + return "test-null-10"; + } + + @Override + protected String getNewFileName() { + return "test-file-10"; + } +} diff --git a/test/org/apache/catalina/webresources/TestJarWarResourceSet.java b/test/org/apache/catalina/webresources/TestJarWarResourceSet.java new file mode 100644 index 0000000..be5d98f --- /dev/null +++ b/test/org/apache/catalina/webresources/TestJarWarResourceSet.java @@ -0,0 +1,56 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestJarWarResourceSet extends TomcatBaseTest { + + @Before + public void register() { + TomcatURLStreamHandlerFactory.register(); + } + + + @Test + public void testJarWarMetaInf() throws LifecycleException { + Tomcat tomcat = getTomcatInstance(); + + File warFile = new File("test/webresources/war-url-connection.war"); + Context ctx = tomcat.addContext("", warFile.getAbsolutePath()); + + tomcat.start(); + + StandardRoot root = (StandardRoot) ctx.getResources(); + + WebResource[] results = root.getClassLoaderResources("/META-INF"); + + Assert.assertNotNull(results); + Assert.assertEquals(1, results.length); + Assert.assertNotNull(results[0].getURL()); + } +} diff --git a/test/org/apache/catalina/webresources/TestResourceJars.java b/test/org/apache/catalina/webresources/TestResourceJars.java new file mode 100644 index 0000000..a968d3f --- /dev/null +++ b/test/org/apache/catalina/webresources/TestResourceJars.java @@ -0,0 +1,51 @@ +/* + * 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.catalina.webresources; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceSet; + +public class TestResourceJars { + + @Test + public void testNonStaticResources() { + File empty = new File("test/webresources/dir3"); + File jar = new File("test/webresources/non-static-resources.jar"); + + TesterWebResourceRoot root = new TesterWebResourceRoot(); + + // Use empty dir for root of web app. + WebResourceSet webResourceSet = new DirResourceSet(root, "/", empty.getAbsolutePath(), "/"); + root.setMainResources(webResourceSet); + + // If this JAR was in a web application, this is equivalent to how it + // would be added + JarResourceSet test = + new JarResourceSet(root, "/", jar.getAbsolutePath(), "/META-INF/resources"); + test.setStaticOnly(true); + root.addJarResources(test); + + WebResource resource = root.getClassLoaderResource("/org/apache/tomcat/unittest/foo.txt"); + + Assert.assertFalse(resource.exists()); + } +} diff --git a/test/org/apache/catalina/webresources/TestStandardRoot.java b/test/org/apache/catalina/webresources/TestStandardRoot.java new file mode 100644 index 0000000..c44b064 --- /dev/null +++ b/test/org/apache/catalina/webresources/TestStandardRoot.java @@ -0,0 +1,94 @@ +/* + * 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.catalina.webresources; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.webresources.StandardRoot.BaseLocation; + +public class TestStandardRoot { + + private static final File file; + private static final String fileUrl; + + static { + file = new File("/foo"); + String url = null; + try { + url = file.toURI().toURL().toExternalForm(); + } catch (MalformedURLException e) { + // Ignore + } + fileUrl = url; + } + + @Test + public void testBaseLocation01() throws Exception { + doTestBaseLocation(new URL (fileUrl), + file.getAbsolutePath(), null); + } + + @Test + public void testBaseLocation02() throws Exception { + doTestBaseLocation(new URL ("jar:" + fileUrl + "!/"), + file.getAbsolutePath(), null); + } + + @Test + public void testBaseLocation03() throws Exception { + doTestBaseLocation(new URL ("jar:" + fileUrl + "!/bar"), + file.getAbsolutePath(), "bar"); + } + + @Test + public void testBaseLocation04() throws Exception { + doTestBaseLocation(new URL ("jar:" + fileUrl + "!/bar/bar"), + file.getAbsolutePath(), "bar/bar"); + } + + @Test(expected=IllegalArgumentException.class) + public void testBaseLocation05() throws Exception { + doTestBaseLocation(new URL ("http://localhost:8080/foo"), + null, null); + } + + private void doTestBaseLocation(URL url, String expectedBasePath, + String expectedArchivePath) { + BaseLocation baseLocation = new BaseLocation(url); + Assert.assertEquals(expectedBasePath, baseLocation.getBasePath()); + Assert.assertEquals(expectedArchivePath, baseLocation.getArchivePath()); + } + + @Test + public void testArchiveIndexStrategy() { + WebResourceRoot root = new StandardRoot(); + root.setArchiveIndexStrategy(WebResourceRoot.ArchiveIndexStrategy.BLOOM.name()); + Assert.assertEquals(WebResourceRoot.ArchiveIndexStrategy.BLOOM.name(), root.getArchiveIndexStrategy()); + } + + @Test(expected = IllegalArgumentException.class) + public void testArchiveIndexStrategyUnrecognized() { + WebResourceRoot root = new StandardRoot(); + root.setArchiveIndexStrategy("UNRECOGNIZED"); + } +} diff --git a/test/org/apache/catalina/webresources/TestTomcatURLStreamHandlerFactory.java b/test/org/apache/catalina/webresources/TestTomcatURLStreamHandlerFactory.java new file mode 100644 index 0000000..b999c8b --- /dev/null +++ b/test/org/apache/catalina/webresources/TestTomcatURLStreamHandlerFactory.java @@ -0,0 +1,43 @@ +/* + * 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.catalina.webresources; + +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +import org.junit.Before; +import org.junit.Test; + +public class TestTomcatURLStreamHandlerFactory { + + @Before + public void register() { + TomcatURLStreamHandlerFactory.register(); + } + + @Test + public void testUserFactory() throws Exception { + URLStreamHandlerFactory factory = new URLStreamHandlerFactory() { + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + return null; + } + }; + TomcatURLStreamHandlerFactory.getInstance().addUserFactory(factory); + TomcatURLStreamHandlerFactory.release(factory.getClass().getClassLoader()); + } +} \ No newline at end of file diff --git a/test/org/apache/catalina/webresources/TesterAbstractFileResourceSetPerformance.java b/test/org/apache/catalina/webresources/TesterAbstractFileResourceSetPerformance.java new file mode 100644 index 0000000..7d66c12 --- /dev/null +++ b/test/org/apache/catalina/webresources/TesterAbstractFileResourceSetPerformance.java @@ -0,0 +1,94 @@ +/* + * 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.catalina.webresources; + +import java.util.regex.Pattern; + +import org.junit.Test; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterAbstractFileResourceSetPerformance { + + private static final Pattern UNSAFE_WINDOWS_FILENAME_PATTERN = Pattern.compile(" $|[\"<>]"); + + private static final int LOOPS = 10_000_000; + + /* + * Checking individual characters is about 10 times faster on markt's dev + * PC for typical length file names. The file names need to get to ~65 + * characters before the Pattern matching is faster. + */ + @Test + public void testFileNameFiltering() { + + long start = System.nanoTime(); + for (int i = 0; i < LOOPS; i++) { + UNSAFE_WINDOWS_FILENAME_PATTERN.matcher("testfile.jsp ").find(); + } + long end = System.nanoTime(); + System.out.println("Regular expression took " + (end - start) + "ns or " + + (end-start)/LOOPS + "ns per iteration"); + + start = System.nanoTime(); + for (int i = 0; i < LOOPS; i++) { + checkForBadCharsArray("testfile.jsp "); + } + end = System.nanoTime(); + System.out.println("char[] check took " + (end - start) + "ns or " + + (end-start)/LOOPS + "ns per iteration"); + + start = System.nanoTime(); + for (int i = 0; i < LOOPS; i++) { + checkForBadCharsAt("testfile.jsp "); + } + end = System.nanoTime(); + System.out.println("charAt() check took " + (end - start) + "ns or " + + (end-start)/LOOPS + "ns per iteration"); + + } + + private boolean checkForBadCharsArray(String filename) { + char[] chars = filename.toCharArray(); + for (char c : chars) { + if (c == '\"' || c == '<' || c == '>') { + return false; + } + } + if (chars[chars.length -1] == ' ') { + return false; + } + return true; + } + + + private boolean checkForBadCharsAt(String filename) { + final int len = filename.length(); + for (int i = 0; i < len; i++) { + char c = filename.charAt(i); + if (c == '\"' || c == '<' || c == '>') { + return false; + } + } + if (filename.charAt(len - 1) == ' ') { + return false; + } + return true; + } +} diff --git a/test/org/apache/catalina/webresources/TesterWebResourceRoot.java b/test/org/apache/catalina/webresources/TesterWebResourceRoot.java new file mode 100644 index 0000000..a56186e --- /dev/null +++ b/test/org/apache/catalina/webresources/TesterWebResourceRoot.java @@ -0,0 +1,160 @@ +/* + * 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.catalina.webresources; + +import java.net.URL; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.tomcat.unittest.TesterContext; + +/** + * Minimal implementation for use in unit tests that supports main and pre + * resources. + */ +public class TesterWebResourceRoot extends StandardRoot { + + public TesterWebResourceRoot() { + super(); + setCachingAllowed(false); + } + + @Override + public void addLifecycleListener(LifecycleListener listener) { + // NO-OP + } + + @Override + public LifecycleListener[] findLifecycleListeners() { + return null; + } + + @Override + public void removeLifecycleListener(LifecycleListener listener) { + // NO-OP + } + + @Override + public void initInternal() throws LifecycleException { + // NO-OP + } + + @Override + public void startInternal() throws LifecycleException { + setState(LifecycleState.STARTING); + } + + @Override + public void stopInternal() throws LifecycleException { + setState(LifecycleState.STOPPING); + } + + @Override + public void destroyInternal() throws LifecycleException { + // NO-OP + } + + @Override + public LifecycleState getState() { + return LifecycleState.STARTED; + } + + @Override + public String getStateName() { + return null; + } + + Context context = new TesterContext(); + @Override + public Context getContext() { + return context; + } + + @Override + public void setContext(Context context) { + // NO-OP + } + + @Override + public void createWebResourceSet(ResourceSetType type, String webAppPath, + URL url, String internalPath) { + // NO-OP + } + + @Override + public void createWebResourceSet(ResourceSetType type, String webAppPath, + String base, String archivePath, String internalPath) { + // NO-OP + } + + @Override + public void setAllowLinking(boolean allowLinking) { + // NO-OP + } + + @Override + public boolean getAllowLinking() { + return false; + } + + @Override + public void setCachingAllowed(boolean cachingAllowed) { + // NO-OP + } + + @Override + public boolean isCachingAllowed() { + return false; + } + + @Override + public void setCacheTtl(long ttl) { + // NO-OP + } + + @Override + public long getCacheTtl() { + return 0; + } + + @Override + public void setCacheMaxSize(long cacheMaxSize) { + // NO-OP + } + + @Override + public long getCacheMaxSize() { + return 0; + } + + @Override + public void setCacheObjectMaxSize(int cacheObjectMaxSize) { + // NO-OP + } + + @Override + public int getCacheObjectMaxSize() { + return 0; + } + + @Override + public void backgroundProcess() { + // NO-OP + } +} diff --git a/test/org/apache/catalina/webresources/war/TestHandler.java b/test/org/apache/catalina/webresources/war/TestHandler.java new file mode 100644 index 0000000..c4b72be --- /dev/null +++ b/test/org/apache/catalina/webresources/war/TestHandler.java @@ -0,0 +1,81 @@ +/* + * 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.catalina.webresources.war; + +import java.io.File; +import java.net.URL; +import java.net.URLConnection; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; + +public class TestHandler { + + @Before + public void register() { + TomcatURLStreamHandlerFactory.register(); + } + + + @Test + public void testUrlFileInJarInWar() throws Exception { + doTestUrl("jar:war:", "*/WEB-INF/lib/test.jar!/META-INF/resources/index.html"); + } + + + @Test + public void testUrlJarInWar() throws Exception { + doTestUrl("war:", "*/WEB-INF/lib/test.jar"); + } + + + @Test + public void testUrlWar() throws Exception { + doTestUrl("", ""); + } + + + private void doTestUrl(String prefix, String suffix) throws Exception { + File f = new File("test/webresources/war-url-connection.war"); + String fileUrl = f.toURI().toURL().toString(); + + String urlString = prefix + fileUrl + suffix; + URL url = new URL(urlString); + + Assert.assertEquals(urlString, url.toExternalForm()); + } + + + @Test + public void testOldFormat() throws Exception { + File f = new File("test/webresources/war-url-connection.war"); + String fileUrl = f.toURI().toURL().toString(); + + URL indexHtmlUrl = new URL("jar:war:" + fileUrl + + "^/WEB-INF/lib/test.jar!/META-INF/resources/index.html"); + + URLConnection urlConn = indexHtmlUrl.openConnection(); + urlConn.connect(); + + int size = urlConn.getContentLength(); + + Assert.assertEquals(137, size); + } +} diff --git a/test/org/apache/catalina/webresources/war/TestHandlerIntegration.java b/test/org/apache/catalina/webresources/war/TestHandlerIntegration.java new file mode 100644 index 0000000..15f75a3 --- /dev/null +++ b/test/org/apache/catalina/webresources/war/TestHandlerIntegration.java @@ -0,0 +1,52 @@ +/* + * 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.catalina.webresources.war; + +import java.io.File; +import java.net.URL; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestHandlerIntegration extends TomcatBaseTest { + + @Test + public void testToURI() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File docBase = new File("test/webresources/war-url-connection.war"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + skipTldsForResourceJars(ctx); + + ((StandardHost) tomcat.getHost()).setUnpackWARs(false); + + tomcat.start(); + + URL url = ctx.getServletContext().getResource("/index.html"); + try { + url.toURI(); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(); + } + } +} diff --git a/test/org/apache/catalina/webresources/war/TestWarURLConnection.java b/test/org/apache/catalina/webresources/war/TestWarURLConnection.java new file mode 100644 index 0000000..f005115 --- /dev/null +++ b/test/org/apache/catalina/webresources/war/TestWarURLConnection.java @@ -0,0 +1,52 @@ +/* + * 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.catalina.webresources.war; + +import java.io.File; +import java.net.URL; +import java.net.URLConnection; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; + +public class TestWarURLConnection { + + @Before + public void register() { + TomcatURLStreamHandlerFactory.register(); + } + + + @Test + public void testContentLength() throws Exception { + File f = new File("test/webresources/war-url-connection.war"); + String fileUrl = f.toURI().toURL().toString(); + + URL indexHtmlUrl = new URL("jar:war:" + fileUrl + + "*/WEB-INF/lib/test.jar!/META-INF/resources/index.html"); + + URLConnection urlConn = indexHtmlUrl.openConnection(); + urlConn.connect(); + + int size = urlConn.getContentLength(); + + Assert.assertEquals(137, size); + } +} diff --git a/test/org/apache/coyote/TestCompressionConfig.java b/test/org/apache/coyote/TestCompressionConfig.java new file mode 100644 index 0000000..f5dc037 --- /dev/null +++ b/test/org/apache/coyote/TestCompressionConfig.java @@ -0,0 +1,78 @@ +/* + * 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.coyote; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestCompressionConfig { + + @Parameterized.Parameters(name = "{index}: accept-encoding[{0}], ETag [{1}], compress[{2}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { new String[] { }, null, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE }); + + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE }); + + return parameterSets; + } + + @Parameter(0) + public String[] headers; + @Parameter(1) + public String eTag; + @Parameter(2) + public Boolean compress; + + @Test + public void testUseCompression() throws Exception { + + CompressionConfig compressionConfig = new CompressionConfig(); + // Skip length and MIME type checks + compressionConfig.setCompression("force"); + + Request request = new Request(); + Response response = new Response(); + + for (String header : headers) { + request.getMimeHeaders().addValue("accept-encoding").setString(header); + } + + if (eTag != null) { + response.getMimeHeaders().addValue("ETag").setString(eTag); + } + + + Assert.assertEquals(compress, Boolean.valueOf(compressionConfig.useCompression(request, response))); + } +} diff --git a/test/org/apache/coyote/TestIoTimeouts.java b/test/org/apache/coyote/TestIoTimeouts.java new file mode 100644 index 0000000..c9b1786 --- /dev/null +++ b/test/org/apache/coyote/TestIoTimeouts.java @@ -0,0 +1,241 @@ +/* + * 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.coyote; + +import java.io.IOException; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestIoTimeouts extends TomcatBaseTest { + + @Test + public void testNonBlockingReadWithNoTimeout() { + // Sends complete request in 3 packets + ChunkedClient client = new ChunkedClient(true); + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + Assert.assertNull(EchoListener.t); + } + + + @Test + public void testNonBlockingReadTimeout() { + // Sends incomplete request (no end chunk) so read times out + ChunkedClient client = new ChunkedClient(false); + client.doRequest(); + Assert.assertFalse(client.isResponse200()); + Assert.assertFalse(client.isResponseBodyOK()); + // Socket will be closed before the error handler runs. Closing the + // socket triggers the client code's return from the doRequest() method + // above so we need to wait at this point for the error handler to be + // triggered. + int count = 0; + // Shouldn't need to wait long but allow plenty of time as the CI + // systems are sometimes slow. + while (count < 100 && EchoListener.t == null) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // Ignore + } + count++; + } + Assert.assertNotNull(EchoListener.t); + } + + + private class ChunkedClient extends SimpleHttpClient { + + private final boolean sendEndChunk; + + + ChunkedClient(boolean sendEndChunk) { + this.sendEndChunk = sendEndChunk; + } + + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Wrapper w = Tomcat.addServlet(root, "Test", new NonBlockingEchoServlet()); + w.setAsyncSupported(true); + root.addServletMappingDecoded("/test", "Test"); + + try { + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + + // Open connection + connect(); + + int packetCount = 2; + if (sendEndChunk) { + packetCount++; + } + + String[] request = new String[packetCount]; + request[0] = + "POST /test HTTP/1.1" + CRLF + + "Host: localhost:8080" + CRLF + + "Transfer-Encoding: chunked" + CRLF + + "Connection: close" + CRLF + + CRLF; + request[1] = + "b8" + CRLF + + "{" + CRLF + + " \"tenantId\": \"dotCom\", " + CRLF + + " \"locale\": \"en-US\", " + CRLF + + " \"defaultZoneId\": \"25\", " + CRLF + + " \"itemIds\": [\"StaplesUSCAS/en-US/2//\"] , " + CRLF + + " \"assetStoreId\": \"5051\", " + CRLF + + " \"zipCode\": \"98109\"" + CRLF + + "}" + CRLF; + if (sendEndChunk) { + request[2] = + "0" + CRLF + + CRLF; + } + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("98109")) { + return false; + } + return true; + } + + } + + + private static class NonBlockingEchoServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + // Need to be in async mode to use non-blocking I/O + AsyncContext ac = req.startAsync(); + ac.setTimeout(10000); + + ServletInputStream sis = null; + ServletOutputStream sos = null; + + try { + sis = req.getInputStream(); + sos = resp.getOutputStream(); + } catch (IOException ioe) { + throw new ServletException(ioe); + } + + EchoListener listener = new EchoListener(ac, sis, sos); + sis.setReadListener(listener); + sos.setWriteListener(listener); + } + } + + + private static class EchoListener implements ReadListener, WriteListener { + + private static volatile Throwable t; + + private final AsyncContext ac; + private final ServletInputStream sis; + private final ServletOutputStream sos; + private final byte[] buffer = new byte[8192]; + + EchoListener(AsyncContext ac, ServletInputStream sis, ServletOutputStream sos) { + t = null; + this.ac = ac; + this.sis = sis; + this.sos = sos; + } + + @Override + public void onWritePossible() throws IOException { + if (sis.isFinished()) { + sos.flush(); + ac.complete(); + return; + } + while (sis.isReady()) { + int read = sis.read(buffer); + if (read > 0) { + sos.write(buffer, 0, read); + if (!sos.isReady()) { + break; + } + } + } + } + + @Override + public void onDataAvailable() throws IOException { + if (sos.isReady()) { + onWritePossible(); + } + } + + @Override + public void onAllDataRead() throws IOException { + if (sos.isReady()) { + onWritePossible(); + } + } + + @Override + public void onError(Throwable throwable) { + t = throwable; + ac.complete(); + } + } +} diff --git a/test/org/apache/coyote/TestRequest.java b/test/org/apache/coyote/TestRequest.java new file mode 100644 index 0000000..f3b915e --- /dev/null +++ b/test/org/apache/coyote/TestRequest.java @@ -0,0 +1,168 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.junit.Before; +import org.junit.Test; + +import org.apache.coyote.http11.Constants; +import org.apache.coyote.http11.Http11NioProtocol; +import org.apache.coyote.http11.Http11Processor; +import org.apache.tomcat.util.net.ApplicationBufferHandler; +import org.apache.tomcat.util.net.SocketBufferHandler; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.easymock.EasyMock; + +public class TestRequest { + + private final Http11NioProtocol protocol = new Http11NioProtocol(); + private final Http11Processor processor = new Http11Processor(protocol, null); + private final Request request = processor.getRequest(); + private final Response response = request.getResponse(); + private final SocketWrapperBase socketWrapper = EasyMock.createNiceMock(SocketWrapperBase.class); + + + @Before + public void setupTest() { + // Set up the socket wrapper + EasyMock.expect(socketWrapper.getSocketBufferHandler()).andReturn(new SocketBufferHandler(0, 0, false)); + EasyMock.replay(socketWrapper); + // Cast to make the method visible + ((AbstractProcessor) processor).setSocketWrapper(socketWrapper); + } + + + @Test + public void test100ContinueExpectationImmediately() throws IOException { + // Tests that response.sendAcknowledgement is only called when + // request.setContinueHandlingResponsePolicy is called. + + request.setExpectation(true); + + // Configure the mock to verify that a network write is made. + configureMockForOneAcknowledgementWrite(socketWrapper); + + protocol.setContinueResponseTiming(ContinueResponseTiming.IMMEDIATELY.toString()); + response.action(ActionCode.ACK, ContinueResponseTiming.IMMEDIATELY); + + // Verify that acknowledgement is written to network. + EasyMock.verify(socketWrapper); + + // Configure the mock to verify that no network write is made. + configureMockForNoAcknowledgementWrite(socketWrapper); + + request.doRead(new DoNothingApplicationBufferHandler()); + + // Verify that acknowledgement is not written to network. + EasyMock.verify(socketWrapper); + } + + + @Test + public void test100ContinueExpectationOnRequestBodyRead() throws IOException { + // Tests that response.sendAcknowledgement is only called when + // request.doRead is called. + + request.setExpectation(true); + + // Configure the mock to verify that no network write is made. + configureMockForNoAcknowledgementWrite(socketWrapper); + + protocol.setContinueResponseTiming(ContinueResponseTiming.ON_REQUEST_BODY_READ.toString()); + response.action(ActionCode.ACK, ContinueResponseTiming.IMMEDIATELY); + + // Verify that no acknowledgement is written to network. + EasyMock.verify(socketWrapper); + + // Configure the mock to verify that a network write is made. + configureMockForOneAcknowledgementWrite(socketWrapper); + + request.doRead(new DoNothingApplicationBufferHandler()); + + // Verify that acknowledgement is written to network. + EasyMock.verify(socketWrapper); + } + + + @Test + public void testNoExpectationWithOnRequestBodyReadPolicy() throws IOException { + // When expectation is false, sendAcknowledgement must never be called. + + request.setExpectation(false); + + // Configure the mock to verify that no network write is made. + configureMockForNoAcknowledgementWrite(socketWrapper); + + protocol.setContinueResponseTiming(ContinueResponseTiming.ON_REQUEST_BODY_READ.toString()); + request.doRead(new DoNothingApplicationBufferHandler()); + + // Verify that no acknowledgement is written to network. + EasyMock.verify(socketWrapper); + } + + + @Test + public void testNoExpectationWithOnRequestImmediately() { + // When expectation is false, sendAcknowledgement must never be called. + + request.setExpectation(false); + + // Configure the mock to verify that no network write is made. + configureMockForNoAcknowledgementWrite(socketWrapper); + + protocol.setContinueResponseTiming(ContinueResponseTiming.IMMEDIATELY.toString()); + response.action(ActionCode.ACK, ContinueResponseTiming.IMMEDIATELY); + + // Verify that no acknowledgement is written to network. + EasyMock.verify(socketWrapper); + } + + + private static class DoNothingApplicationBufferHandler implements ApplicationBufferHandler { + @Override + public void setByteBuffer(ByteBuffer buffer) { + + } + + @Override + public ByteBuffer getByteBuffer() { + return null; + } + + @Override + public void expand(int size) { + + } + } + + + private void configureMockForOneAcknowledgementWrite(SocketWrapperBase socketWrapper) throws IOException { + EasyMock.reset(socketWrapper); + socketWrapper.write(true, Constants.ACK_BYTES, 0, Constants.ACK_BYTES.length); + EasyMock.expectLastCall().once(); + EasyMock.replay(socketWrapper); + } + + + private void configureMockForNoAcknowledgementWrite(SocketWrapperBase socketWrapper) { + EasyMock.reset(socketWrapper); + EasyMock.replay(socketWrapper); + } +} diff --git a/test/org/apache/coyote/TestResponse.java b/test/org/apache/coyote/TestResponse.java new file mode 100644 index 0000000..b88737f --- /dev/null +++ b/test/org/apache/coyote/TestResponse.java @@ -0,0 +1,146 @@ +/* + * 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.coyote; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestResponse extends TomcatBaseTest { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=61197 + */ + @Test + public void testUserCharsetIsRetained() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "CharsetServlet", new CharsetServlet()); + ctx.addServletMappingDecoded("/*", "CharsetServlet"); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + int rc = getUrl("http://localhost:" + getPort() + "/test?charset=uTf-8", responseBody, + responseHeaders); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String contentType = getSingleHeader("Content-Type", responseHeaders); + Assert.assertEquals("text/plain;charset=uTf-8", contentType); + } + + + private static class CharsetServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding(req.getParameter("charset")); + + resp.getWriter().print("OK"); + } + } + + + @Test + public void testContentTypeWithSpace() throws Exception { + doTestContentTypeSpacing(true); + } + + + @Test + public void testContentTypeWithoutSpace() throws Exception { + doTestContentTypeSpacing(false); + } + + + private void doTestContentTypeSpacing(boolean withSpace) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "ContentTypeServlet", new ContentTypeServlet()); + ctx.addServletMappingDecoded("/*", "ContentTypeServlet"); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + StringBuilder uri = new StringBuilder("http://localhost:"); + uri.append(getPort()); + uri.append("/test"); + if (withSpace) { + uri.append("?withSpace=true"); + } + int rc = getUrl(uri.toString(), responseBody, responseHeaders); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String contentType = getSingleHeader("Content-Type", responseHeaders); + StringBuilder expected = new StringBuilder("text/plain;"); + if (withSpace) { + expected.append(' '); + } + expected.append("v=1;charset=UTF-8"); + Assert.assertEquals(expected.toString() , contentType); + } + + + private static class ContentTypeServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + if (req.getParameter("withSpace") == null) { + resp.setContentType("text/plain;v=1"); + } else { + resp.setContentType("text/plain; v=1"); + } + resp.setCharacterEncoding("UTF-8"); + + resp.getWriter().print("OK"); + } + } +} diff --git a/test/org/apache/coyote/ajp/SimpleAjpClient.java b/test/org/apache/coyote/ajp/SimpleAjpClient.java new file mode 100644 index 0000000..f4ba2a6 --- /dev/null +++ b/test/org/apache/coyote/ajp/SimpleAjpClient.java @@ -0,0 +1,417 @@ +/* + * 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.coyote.ajp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.util.Locale; + +import javax.net.SocketFactory; + +/** + * AJP client that is not (yet) a full AJP client implementation as it just provides the functionality required for the + * unit tests. The client uses blocking IO throughout. + */ +public class SimpleAjpClient { + + private static final int DEFAULT_AJP_PACKET_SIZE = 8192; + private static final byte[] AJP_CPING; + + static { + TesterAjpMessage ajpCping = new TesterAjpMessage(16); + ajpCping.reset(); + ajpCping.appendByte(Constants.JK_AJP13_CPING_REQUEST); + ajpCping.end(); + AJP_CPING = new byte[ajpCping.getLen()]; + System.arraycopy(ajpCping.getBuffer(), 0, AJP_CPING, 0, ajpCping.getLen()); + } + + private final int packetSize; + private String host = "localhost"; + private int port = -1; + /* GET == 2 */ + private int method = 2; + private String protocol = "http"; + private String uri = "/"; + private String remoteAddr = "192.168.0.1"; + private String remoteHost = "client.example.com"; + private String serverName = "www.example.com"; + private int serverPort = 80; + private boolean ssl = false; + private Socket socket = null; + + public SimpleAjpClient() { + this(DEFAULT_AJP_PACKET_SIZE); + } + + public SimpleAjpClient(int packetSize) { + this.packetSize = packetSize; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return port; + } + + public void setMethod(String method) { + method = method.toUpperCase(Locale.ENGLISH); + switch (method) { + case "OPTIONS": + this.method = 1; + break; + case "GET": + this.method = 2; + break; + case "HEAD": + this.method = 3; + break; + case "POST": + this.method = 4; + break; + case "PUT": + this.method = 5; + break; + case "DELETE": + this.method = 6; + break; + case "TRACE": + this.method = 7; + break; + case "PROPFIND": + this.method = 8; + break; + case "PROPPATCH": + this.method = 9; + break; + case "MKCOL": + this.method = 10; + break; + case "COPY": + this.method = 11; + break; + case "MOVE": + this.method = 12; + break; + case "LOCK": + this.method = 13; + break; + case "UNLOCK": + this.method = 14; + break; + case "ACL": + this.method = 15; + break; + case "REPORT": + this.method = 16; + break; + case "VERSION-CONTROL": + this.method = 17; + break; + case "CHECKIN": + this.method = 18; + break; + case "CHECKOUT": + this.method = 19; + break; + case "UNCHECKOUT": + this.method = 20; + break; + case "SEARCH": + this.method = 21; + break; + case "MKWORKSPACE": + this.method = 22; + break; + case "UPDATE": + this.method = 23; + break; + case "LABEL": + this.method = 24; + break; + case "MERGE": + this.method = 25; + break; + case "BASELINE-CONTROL": + this.method = 26; + break; + case "MKACTIVITY": + this.method = 27; + break; + default: + this.method = 99; + } + } + + public String getMethod() { + switch (method) { + case 1: + return "OPTIONS"; + case 2: + return "GET"; + case 3: + return "HEAD"; + case 4: + return "POST"; + case 5: + return "PUT"; + case 6: + return "DELETE"; + case 7: + return "TRACE"; + case 8: + return "PROPFIND"; + case 9: + return "PROPPATCH"; + case 10: + return "MKCOL"; + case 11: + return "COPY"; + case 12: + return "MOVE"; + case 13: + return "LOCK"; + case 14: + return "UNLOCK"; + case 15: + return "ACL"; + case 16: + return "REPORT"; + case 17: + return "VERSION-CONTROL"; + case 18: + return "CHECKIN"; + case 19: + return "CHECKOUT"; + case 20: + return "UNCHECKOUT"; + case 21: + return "SEARCH"; + case 22: + return "MKWORKSPACE"; + case 23: + return "UPDATE"; + case 24: + return "LABEL"; + case 25: + return "MERGE"; + case 26: + return "BASELINE-CONTROL"; + case 27: + return "MKACTIVITY"; + default: + return "UNKNOWN"; + } + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getProtocol() { + return protocol; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getUri() { + return uri; + } + + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + public String getRemoteAddr() { + return remoteAddr; + } + + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + public String getRemoteHost() { + return remoteHost; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public String getServerName() { + return serverName; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public int getServerPort() { + return serverPort; + } + + public void setSsl(boolean ssl) { + this.ssl = ssl; + } + + public boolean isSsl() { + return ssl; + } + + public void connect() throws IOException { + socket = SocketFactory.getDefault().createSocket(host, port); + } + + public void disconnect() throws IOException { + socket.close(); + socket = null; + } + + /* + * Create a message to request the given URL. + */ + public TesterAjpMessage createForwardMessage() { + + TesterAjpMessage message = new TesterAjpMessage(packetSize); + message.reset(); + + // Set the header bytes + message.getBuffer()[0] = 0x12; + message.getBuffer()[1] = 0x34; + + // Code 2 for forward request + message.appendByte(Constants.JK_AJP13_FORWARD_REQUEST); + + // HTTP method, GET = 2 + message.appendByte(method); + + // Protocol + message.appendString(protocol); + + // Request URI + message.appendString(uri); + + // Client address + message.appendString(remoteAddr); + + // Client host + message.appendString(remoteHost); + + // Server name + message.appendString(serverName); + + // Server port + message.appendInt(serverPort); + + // Is ssl + message.appendByte(ssl ? 0x01 : 0x00); + + return message; + } + + public TesterAjpMessage createBodyMessage(byte[] data) { + + TesterAjpMessage message = new TesterAjpMessage(packetSize); + message.reset(); + + // Set the header bytes + message.getBuffer()[0] = 0x12; + message.getBuffer()[1] = 0x34; + + message.appendBytes(data, 0, data.length); + message.end(); + + return message; + } + + + /* + * Sends an TesterAjpMessage to the server and returns the response message. + */ + public TesterAjpMessage sendMessage(TesterAjpMessage headers) throws IOException { + return sendMessage(headers, null); + } + + public TesterAjpMessage sendMessage(TesterAjpMessage headers, TesterAjpMessage body) throws IOException { + // Send the headers + socket.getOutputStream().write(headers.getBuffer(), 0, headers.getLen()); + if (body != null) { + // Send the body of present + socket.getOutputStream().write(body.getBuffer(), 0, body.getLen()); + } + // Read the response + return readMessage(); + } + + /* + * Tests the connection to the server and returns the CPONG response. + */ + public TesterAjpMessage cping() throws IOException { + // Send the ping message + socket.getOutputStream().write(AJP_CPING); + // Read the response + return readMessage(); + } + + /* + * Reads a message from the server. + */ + public TesterAjpMessage readMessage() throws IOException { + + InputStream is = socket.getInputStream(); + + TesterAjpMessage message = new TesterAjpMessage(packetSize); + + byte[] buf = message.getBuffer(); + + read(is, buf, 0, Constants.H_SIZE); + + int messageLength = message.processHeader(false); + if (messageLength < 0) { + throw new IOException("Invalid AJP message length"); + } else if (messageLength == 0) { + return message; + } else { + if (messageLength > buf.length) { + throw new IllegalArgumentException("Message too long [" + Integer.valueOf(messageLength) + + "] for buffer length [" + Integer.valueOf(buf.length) + "]"); + } + read(is, buf, Constants.H_SIZE, messageLength); + return message; + } + } + + protected boolean read(InputStream is, byte[] buf, int pos, int n) throws IOException { + + int read = 0; + int res = 0; + while (read < n) { + res = is.read(buf, read + pos, n - read); + if (res > 0) { + read += res; + } else { + throw new IOException("Read failed"); + } + } + return true; + } +} diff --git a/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java b/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java new file mode 100644 index 0000000..f76deae --- /dev/null +++ b/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java @@ -0,0 +1,1142 @@ +/* + * 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.coyote.ajp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.res.StringManager; + +public class TestAbstractAjpProcessor extends TomcatBaseTest { + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + Connector c = getTomcatInstance().getConnector(); + c.setProperty("secretRequired", "false"); + c.setProperty("allowedRequestAttributesPattern", "MYATTRIBUTE.*"); + } + + + @Override + protected String getProtocol() { + /* + * The tests are all setup for HTTP so need to convert the protocol values to AJP. + */ + // Has a protocol been specified + String protocol = System.getProperty("tomcat.test.protocol"); + + // Use NIO by default + if (protocol == null) { + protocol = "org.apache.coyote.ajp.AjpNioProtocol"; + } else if (protocol.contains("Nio2")) { + protocol = "org.apache.coyote.ajp.AjpNio2Protocol"; + } else { + protocol = "org.apache.coyote.ajp.AjpNioProtocol"; + } + + return protocol; + } + + private void doSnoopTest(RequestDescriptor desc) throws Exception { + + final int ajpPacketSize = 16000; + + Map requestInfo = desc.getRequestInfo(); + Map contextInitParameters = desc.getContextInitParameters(); + Map contextAttributes = desc.getContextAttributes(); + Map headers = desc.getHeaders(); + Map attributes = desc.getAttributes(); + Map params = desc.getParams(); + + Tomcat tomcat = getTomcatInstance(); + Assert.assertTrue(tomcat.getConnector().setProperty("packetSize", Integer.toString(ajpPacketSize))); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "snoop", new SnoopServlet()); + ctx.addServletMappingDecoded("/", "snoop"); + + SimpleAjpClient ajpClient = new SimpleAjpClient(ajpPacketSize); + + if (requestInfo.get("REQUEST-QUERY-STRING") != null && params.size() > 0) { + throw (new IllegalArgumentException( + "Request setting " + "'REQUEST-QUERY-STRING' and explicit params not allowed " + "together")); + } + + String value; + int bodySize = 0; + Map savedRequestInfo = new HashMap<>(); + for (String name : requestInfo.keySet()) { + value = requestInfo.get(name); + switch (name) { + case "REQUEST-METHOD": + ajpClient.setMethod(value); + break; + case "REQUEST-PROTOCOL": + ajpClient.setProtocol(value); + break; + case "REQUEST-URI": + ajpClient.setUri(value); + break; + case "REQUEST-REMOTE-HOST": + /* + * request.getRemoteHost() will default to request.getRemoteAddr() unless enableLookups is set. + */ + tomcat.getConnector().setEnableLookups(true); + ajpClient.setRemoteHost(value); + break; + case "REQUEST-REMOTE-ADDR": + ajpClient.setRemoteAddr(value); + break; + case "REQUEST-SERVER-NAME": + ajpClient.setServerName(value); + break; + case "REQUEST-SERVER-PORT": + ajpClient.setServerPort(Integer.parseInt(value)); + break; + case "REQUEST-IS-SECURE": + ajpClient.setSsl(Boolean.parseBoolean(value)); + break; + case "REQUEST-LOCAL-ADDR": + savedRequestInfo.put(name, value); + break; + case "REQUEST-REMOTE-PORT": + savedRequestInfo.put(name, value); + break; + case "REQUEST-REMOTE-USER": + case "REQUEST-ROUTE": + case "REQUEST-SECRET": + case "REQUEST-AUTH-TYPE": + case "REQUEST-QUERY-STRING": + savedRequestInfo.put(name, value); + break; + case "REQUEST-CONTENT-LENGTH": + headers.put("CONTENT-LENGTH", value); + break; + case "REQUEST-BODY-SIZE": + savedRequestInfo.put(name, value); + bodySize = Integer.parseInt(value); + break; + case "REQUEST-CONTENT-TYPE": + headers.put("CONTENT-TYPE", value); + break; + /* Not yet implemented or not (easily) possible to implement */ + case "REQUEST-LOCAL-NAME": // request.getLocalName() + case "REQUEST-LOCAL-PORT": // request.getLocalPort() + case "REQUEST-SCHEME": // request.getScheme() + case "REQUEST-URL": // request.getRequestURL() + case "REQUEST-CONTEXT-PATH": // request.getContextPath() + case "REQUEST-SERVLET-PATH": // request.getServletPath() + case "REQUEST-PATH-INFO": // request.getPathInfo() + case "REQUEST-PATH-TRANSLATED": // request.getPathTranslated() + case "REQUEST-USER-PRINCIPAL": // request.getUserPrincipal() + case "REQUEST-CHARACTER-ENCODING": // request.getCharacterEncoding() + case "REQUEST-LOCALE": // request.getLocale() + case "SESSION-REQUESTED-ID": // request.getRequestedSessionId() + case "SESSION-REQUESTED-ID-COOKIE": // request.isRequestedSessionIdFromCookie() + case "SESSION-REQUESTED-ID-URL": // request.isRequestedSessionIdFromUrl() + case "SESSION-REQUESTED-ID-VALID": // request.isRequestedSessionIdValid() + default: + throw (new IllegalArgumentException("Request setting '" + name + "' not supported")); + } + } + + ServletContext sc = ctx.getServletContext(); + for (String name : contextInitParameters.keySet()) { + sc.setInitParameter(name, contextInitParameters.get(name)); + } + for (String name : contextAttributes.keySet()) { + sc.setAttribute(name, contextAttributes.get(name)); + } + + /* Basic request properties must be set before this call */ + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + + for (String name : savedRequestInfo.keySet()) { + value = savedRequestInfo.get(name); + switch (name) { + case "REQUEST-LOCAL-ADDR": + forwardMessage.addAttribute("AJP_LOCAL_ADDR", value); + break; + case "REQUEST-REMOTE-PORT": + forwardMessage.addAttribute("AJP_REMOTE_PORT", value); + break; + case "REQUEST-REMOTE-USER": + /* + * request.getRemoteUser() will not trust the AJP info if tomcatAuthentication is set. + */ + Assert.assertTrue(tomcat.getConnector().setProperty("tomcatAuthentication", "false")); + forwardMessage.addAttribute(0x03, value); + break; + case "REQUEST-AUTH-TYPE": + /* + * request.getAuthType() will not trust the AJP info if tomcatAuthentication is set. + */ + Assert.assertTrue(tomcat.getConnector().setProperty("tomcatAuthentication", "false")); + forwardMessage.addAttribute(0x04, value); + break; + case "REQUEST-QUERY-STRING": + forwardMessage.addAttribute(0x05, value); + break; + case "REQUEST-ROUTE": + forwardMessage.addAttribute(0x06, value); + break; + case "REQUEST-SECRET": + forwardMessage.addAttribute(0x0C, value); + break; + case "REQUEST-BODY-SIZE": + break; + default: + throw (new IllegalArgumentException("Request setting '" + name + "' not supported")); + } + } + + if (params.size() > 0) { + StringBuilder query = new StringBuilder(); + boolean sep = false; + for (String name : params.keySet()) { + if (sep) { + query.append('&'); + } else { + sep = true; + } + query.append(name); + query.append('='); + query.append(params.get(name)); + } + forwardMessage.addAttribute(0x05, query.toString()); + } + + for (String name : headers.keySet()) { + value = headers.get(name); + name = name.toUpperCase(Locale.ENGLISH); + switch (name) { + case "ACCEPT": + forwardMessage.addHeader(0xA001, value); + break; + case "ACCEPT-CHARSET": + forwardMessage.addHeader(0xA002, value); + break; + case "ACCEPT-ENCODING": + forwardMessage.addHeader(0xA003, value); + break; + case "ACCEPT-LANGUAGE": + forwardMessage.addHeader(0xA004, value); + break; + case "AUTHORIZATION": + forwardMessage.addHeader(0xA005, value); + break; + case "CONNECTION": + forwardMessage.addHeader(0xA006, value); + break; + case "CONTENT-TYPE": + forwardMessage.addHeader(0xA007, value); + break; + case "CONTENT-LENGTH": + forwardMessage.addHeader(0xA008, value); + break; + case "COOKIE": + forwardMessage.addHeader(0xA009, value); + break; + case "COOKIE2": + forwardMessage.addHeader(0xA00A, value); + break; + case "HOST": + forwardMessage.addHeader(0xA00B, value); + break; + case "PRAGMA": + forwardMessage.addHeader(0xA00C, value); + break; + case "REFERER": + forwardMessage.addHeader(0xA00D, value); + break; + case "USER-AGENT": + forwardMessage.addHeader(0xA00E, value); + break; + default: + forwardMessage.addHeader(name, value); + break; + } + } + for (String name : attributes.keySet()) { + value = attributes.get(name); + forwardMessage.addAttribute(name, value); + } + // Complete the message + forwardMessage.end(); + + tomcat.start(); + ajpClient.setPort(getPort()); + ajpClient.connect(); + + TesterAjpMessage responseHeaders = null; + if (bodySize == 0) { + responseHeaders = ajpClient.sendMessage(forwardMessage); + } else { + TesterAjpMessage bodyMessage = ajpClient.createBodyMessage(new byte[bodySize]); + responseHeaders = ajpClient.sendMessage(forwardMessage, bodyMessage); + // Expect back a request for more data (which will be empty and + // trigger end of stream in Servlet) + validateGetBody(responseHeaders); + bodyMessage = ajpClient.createBodyMessage(new byte[0]); + responseHeaders = ajpClient.sendMessage(bodyMessage); + } + + // Expect 3 packets: headers, body, end + validateResponseHeaders(responseHeaders, 200, "200"); + + String body = extractResponseBody(ajpClient.readMessage()); + RequestDescriptor result = SnoopResult.parse(body); + + /* + * AJP attributes result in Coyote Request attributes, which are not listed by request.getAttributeNames(), so + * SnoopServlet does not see them. Delete attributes before result comparison. + */ + desc.getAttributes().clear(); + + result.compare(desc); + + validateResponseEnd(ajpClient.readMessage(), true); + } + + @Test + public void testServerName() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-SERVER-NAME", "MYSERVER"); + desc.putRequestInfo("REQUEST-URI", "/testServerName"); + doSnoopTest(desc); + } + + @Test + public void testServerPort() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-SERVER-PORT", "8888"); + desc.putRequestInfo("REQUEST-URI", "/testServerPort"); + doSnoopTest(desc); + } + + @Test + public void testLocalAddr() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-LOCAL-ADDR", "10.3.2.1"); + desc.putRequestInfo("REQUEST-URI", "/testLocalAddr"); + doSnoopTest(desc); + } + + @Test + public void testRemoteHost() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-REMOTE-HOST", "MYCLIENT"); + desc.putRequestInfo("REQUEST-URI", "/testRemoteHost"); + doSnoopTest(desc); + } + + @Test + public void testRemoteAddr() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-REMOTE-ADDR", "10.1.2.3"); + desc.putRequestInfo("REQUEST-URI", "/testRemoteAddr"); + doSnoopTest(desc); + } + + @Test + public void testRemotePort() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-REMOTE-PORT", "34567"); + desc.putRequestInfo("REQUEST-URI", "/testRemotePort"); + doSnoopTest(desc); + } + + @Test + public void testMethod() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-METHOD", "LOCK"); + desc.putRequestInfo("REQUEST-URI", "/testMethod"); + doSnoopTest(desc); + } + + @Test + public void testUri() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-URI", "/a/b/c"); + doSnoopTest(desc); + } + + @Test + public void testProtocol() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-PROTOCOL", "HTTP/1.x"); + desc.putRequestInfo("REQUEST-URI", "/testProtocol"); + doSnoopTest(desc); + } + + @Test + public void testSecure() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-IS-SECURE", "true"); + desc.putRequestInfo("REQUEST-URI", "/testSecure"); + doSnoopTest(desc); + } + + @Test + public void testQueryString() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-QUERY-STRING", "p1=1&p2=12&p3=123"); + desc.putRequestInfo("REQUEST-URI", "/testQueryString"); + doSnoopTest(desc); + } + + @Test + public void testRemoteUser() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-REMOTE-USER", "MYUSER"); + desc.putRequestInfo("REQUEST-URI", "/testRemoteUser"); + doSnoopTest(desc); + } + + @Test + public void testAuthType() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-AUTH-TYPE", "MyAuth"); + desc.putRequestInfo("REQUEST-URI", "/testAuthType"); + doSnoopTest(desc); + } + + @Test + public void testOneHeader() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putHeader("MYHEADER", "MYHEADER-VALUE"); + desc.putRequestInfo("REQUEST-URI", "/testOneHeader"); + doSnoopTest(desc); + } + + @Test + public void testOneAttribute() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putAttribute("MYATTRIBUTE", "MYATTRIBUTE-VALUE"); + desc.putRequestInfo("REQUEST-URI", "/testOneAttribute"); + doSnoopTest(desc); + } + + @Test + public void testMulti() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-SERVER-NAME", "MYSERVER"); + desc.putRequestInfo("REQUEST-SERVER-PORT", "8888"); + desc.putRequestInfo("REQUEST-LOCAL-ADDR", "10.3.2.1"); + desc.putRequestInfo("REQUEST-REMOTE-HOST", "MYCLIENT"); + desc.putRequestInfo("REQUEST-REMOTE-ADDR", "10.1.2.3"); + desc.putRequestInfo("REQUEST-REMOTE-PORT", "34567"); + desc.putRequestInfo("REQUEST-METHOD", "LOCK"); + desc.putRequestInfo("REQUEST-URI", "/a/b/c"); + desc.putRequestInfo("REQUEST-PROTOCOL", "HTTP/1.x"); + desc.putRequestInfo("REQUEST-IS-SECURE", "true"); + desc.putRequestInfo("REQUEST-QUERY-STRING", "p1=1&p2=12&p3=123"); + desc.putRequestInfo("REQUEST-REMOTE-USER", "MYUSER"); + desc.putRequestInfo("REQUEST-AUTH-TYPE", "MyAuth"); + desc.putHeader("MYHEADER1", "MYHEADER1-VALUE"); + desc.putHeader("MYHEADER2", "MYHEADER2-VALUE"); + desc.putAttribute("MYATTRIBUTE1", "MYATTRIBUTE-VALUE1"); + desc.putAttribute("MYATTRIBUTE2", "MYATTRIBUTE-VALUE2"); + doSnoopTest(desc); + } + + @Test + public void testSmallBody() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-METHOD", "PUT"); + desc.putRequestInfo("REQUEST-CONTENT-LENGTH", "100"); + desc.putRequestInfo("REQUEST-BODY-SIZE", "100"); + desc.putRequestInfo("REQUEST-URI", "/testSmallBody"); + doSnoopTest(desc); + } + + @Test + public void testLargeBody() throws Exception { + RequestDescriptor desc = new RequestDescriptor(); + desc.putRequestInfo("REQUEST-METHOD", "PUT"); + desc.putRequestInfo("REQUEST-CONTENT-LENGTH", "10000"); + desc.putRequestInfo("REQUEST-BODY-SIZE", "10000"); + desc.putRequestInfo("REQUEST-URI", "/testLargeBody"); + doSnoopTest(desc); + } + + @Test + public void testSecret() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Assert.assertTrue(tomcat.getConnector().setProperty("secret", "RIGHTSECRET")); + tomcat.start(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "helloWorld", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "helloWorld"); + + StringManager smClient = StringManager.getManager("org.apache.catalina.valves"); + String expectedBody = "

    " + smClient.getString("errorReportValve.type") + " " + + smClient.getString("errorReportValve.statusReport") + "

    "; + + SimpleAjpClient ajpClient = new SimpleAjpClient(); + + ajpClient.setPort(getPort()); + + ajpClient.connect(); + validateCpong(ajpClient.cping()); + + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.end(); + + TesterAjpMessage responseHeaders = ajpClient.sendMessage(forwardMessage); + // Expect 3 packets: headers, body, end + validateResponseHeaders(responseHeaders, 403, "403"); + TesterAjpMessage responseBody = ajpClient.readMessage(); + validateResponseBody(responseBody, expectedBody); + validateResponseEnd(ajpClient.readMessage(), false); + + ajpClient.connect(); + validateCpong(ajpClient.cping()); + + forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.addAttribute(0x0C, "WRONGSECRET"); + forwardMessage.end(); + + responseHeaders = ajpClient.sendMessage(forwardMessage); + // Expect 3 packets: headers, body, end + validateResponseHeaders(responseHeaders, 403, "403"); + responseBody = ajpClient.readMessage(); + validateResponseBody(responseBody, expectedBody); + validateResponseEnd(ajpClient.readMessage(), false); + + ajpClient.connect(); + validateCpong(ajpClient.cping()); + + forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.addAttribute(0x0C, "RIGHTSECRET"); + forwardMessage.end(); + + responseHeaders = ajpClient.sendMessage(forwardMessage); + // Expect 3 packets: headers, body, end + validateResponseHeaders(responseHeaders, 200, "200"); + responseBody = ajpClient.readMessage(); + validateResponseBody(responseBody, HelloWorldServlet.RESPONSE_TEXT); + validateResponseEnd(ajpClient.readMessage(), true); + + ajpClient.disconnect(); + } + + @Test + public void testKeepAlive() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Assert.assertTrue(tomcat.getConnector().setProperty("connectionTimeout", "-1")); + tomcat.start(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "helloWorld", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "helloWorld"); + + SimpleAjpClient ajpClient = new SimpleAjpClient(); + + ajpClient.setPort(getPort()); + + ajpClient.connect(); + + validateCpong(ajpClient.cping()); + + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.addHeader("X-DUMMY-HEADER", "IGNORE"); + // Complete the message - no extra headers required. + forwardMessage.end(); + + // Two requests + for (int i = 0; i < 2; i++) { + TesterAjpMessage responseHeaders = ajpClient.sendMessage(forwardMessage); + // Expect 3 packets: headers, body, end + validateResponseHeaders(responseHeaders, 200, "200"); + TesterAjpMessage responseBody = ajpClient.readMessage(); + validateResponseBody(responseBody, HelloWorldServlet.RESPONSE_TEXT); + validateResponseEnd(ajpClient.readMessage(), true); + + // Give connections plenty of time to time out + Thread.sleep(2000); + + // Double check the connection is still open + validateCpong(ajpClient.cping()); + } + + ajpClient.disconnect(); + } + + @Test + public void testPost() throws Exception { + doTestPost(false, HttpServletResponse.SC_OK, "200"); + } + + + @Test + public void testPostMultipleContentLength() throws Exception { + // Multiple content lengths + doTestPost(true, HttpServletResponse.SC_BAD_REQUEST, "400"); + } + + + public void doTestPost(boolean multipleCL, int expectedStatus, String expectedMessage) throws Exception { + + getTomcatInstanceTestWebapp(false, true); + + SimpleAjpClient ajpClient = new SimpleAjpClient(); + ajpClient.setPort(getPort()); + ajpClient.connect(); + + validateCpong(ajpClient.cping()); + + ajpClient.setUri("/test/echo-params.jsp"); + ajpClient.setMethod("POST"); + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.addHeader(0xA008, "9"); + if (multipleCL) { + forwardMessage.addHeader(0xA008, "99"); + } + forwardMessage.addHeader(0xA007, "application/x-www-form-urlencoded"); + forwardMessage.end(); + + TesterAjpMessage bodyMessage = ajpClient.createBodyMessage("test=data".getBytes()); + + TesterAjpMessage responseHeaders = ajpClient.sendMessage(forwardMessage, bodyMessage); + + validateResponseHeaders(responseHeaders, expectedStatus, expectedMessage); + if (expectedStatus == HttpServletResponse.SC_OK) { + // Expect 3 messages: headers, body, end for a valid request + TesterAjpMessage responseBody = ajpClient.readMessage(); + validateResponseBody(responseBody, "test - data"); + validateResponseEnd(ajpClient.readMessage(), true); + + // Double check the connection is still open + validateCpong(ajpClient.cping()); + } else { + // Expect 3 messages: headers, error report body, end for an invalid request + StringManager smClient = StringManager.getManager("org.apache.catalina.valves"); + String expectedBody = "

    " + smClient.getString("errorReportValve.type") + " " + + smClient.getString("errorReportValve.statusReport") + "

    "; + TesterAjpMessage responseBody = ajpClient.readMessage(); + validateResponseBody(responseBody, expectedBody); + validateResponseEnd(ajpClient.readMessage(), false); + } + + + ajpClient.disconnect(); + } + + + /* + * Bug 55453 + */ + @Test + public void test304WithBody() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "bug55453", new Tester304WithBodyServlet()); + ctx.addServletMappingDecoded("/", "bug55453"); + + tomcat.start(); + + SimpleAjpClient ajpClient = new SimpleAjpClient(); + ajpClient.setPort(getPort()); + ajpClient.connect(); + + validateCpong(ajpClient.cping()); + + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.end(); + + TesterAjpMessage responseHeaders = ajpClient.sendMessage(forwardMessage, null); + + // Expect 2 messages: headers, end + validateResponseHeaders(responseHeaders, 304, "304"); + validateResponseEnd(ajpClient.readMessage(), true); + + // Double check the connection is still open + validateCpong(ajpClient.cping()); + + ajpClient.disconnect(); + } + + + @Test + public void testZeroLengthRequestBodyGetA() throws Exception { + doTestZeroLengthRequestBody("GET", true); + } + + @Test + public void testZeroLengthRequestBodyGetB() throws Exception { + doTestZeroLengthRequestBody("GET", false); + } + + @Test + public void testZeroLengthRequestBodyPostA() throws Exception { + doTestZeroLengthRequestBody("POST", true); + } + + @Test + public void testZeroLengthRequestBodyPostB() throws Exception { + doTestZeroLengthRequestBody("POST", false); + } + + private void doTestZeroLengthRequestBody(String method, boolean callAvailable) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + ReadBodyServlet servlet = new ReadBodyServlet(callAvailable); + Tomcat.addServlet(ctx, "ReadBody", servlet); + ctx.addServletMappingDecoded("/", "ReadBody"); + + tomcat.start(); + + SimpleAjpClient ajpClient = new SimpleAjpClient(); + ajpClient.setPort(getPort()); + ajpClient.connect(); + + validateCpong(ajpClient.cping()); + + ajpClient.setMethod(method); + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.addHeader(0xA008, "0"); + forwardMessage.end(); + + TesterAjpMessage responseHeaders = ajpClient.sendMessage(forwardMessage, null); + + // Expect 3 messages: headers, body, end + validateResponseHeaders(responseHeaders, 200, "200"); + validateResponseBody(ajpClient.readMessage(), "Request Body length in bytes: 0"); + validateResponseEnd(ajpClient.readMessage(), true); + + // Double check the connection is still open + validateCpong(ajpClient.cping()); + + ajpClient.disconnect(); + + if (callAvailable) { + boolean success = true; + Iterator itAvailable = servlet.availableList.iterator(); + Iterator itRead = servlet.readList.iterator(); + while (success && itAvailable.hasNext()) { + success = ((itRead.next().intValue() > 0) == (itAvailable.next().intValue() > 0)); + } + if (!success) { + Assert.fail("available() and read() results do not match.\nAvailable: " + servlet.availableList + + "\nRead: " + servlet.readList); + } + } + } + + + @Test + public void testLargeResponse() throws Exception { + + int ajpPacketSize = 16000; + + Tomcat tomcat = getTomcatInstance(); + Assert.assertTrue(tomcat.getConnector().setProperty("packetSize", Integer.toString(ajpPacketSize))); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + FixedResponseSizeServlet servlet = new FixedResponseSizeServlet(15000, 16000); + Tomcat.addServlet(ctx, "FixedResponseSizeServlet", servlet); + ctx.addServletMappingDecoded("/", "FixedResponseSizeServlet"); + + tomcat.start(); + + SimpleAjpClient ajpClient = new SimpleAjpClient(ajpPacketSize); + ajpClient.setPort(getPort()); + ajpClient.connect(); + + validateCpong(ajpClient.cping()); + + ajpClient.setUri("/"); + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.end(); + + TesterAjpMessage responseHeaders = ajpClient.sendMessage(forwardMessage); + + // Expect 3 messages: headers, body, end for a valid request + validateResponseHeaders(responseHeaders, 200, "200"); + TesterAjpMessage responseBody = ajpClient.readMessage(); + Assert.assertTrue(responseBody.len > 15000); + validateResponseEnd(ajpClient.readMessage(), true); + + // Double check the connection is still open + validateCpong(ajpClient.cping()); + + ajpClient.disconnect(); + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=66512 + */ + @Test + public void testInvalidHeader() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "bug66512", new InvalidHeaderServlet()); + ctx.addServletMappingDecoded("/", "bug66512"); + + tomcat.start(); + + SimpleAjpClient ajpClient = new SimpleAjpClient(); + ajpClient.setPort(getPort()); + ajpClient.connect(); + + validateCpong(ajpClient.cping()); + + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.end(); + + TesterAjpMessage responseHeaderMessage = ajpClient.sendMessage(forwardMessage, null); + + // Expect 2 messages: headers, end + Map> responseHeaders = validateResponseHeaders(responseHeaderMessage, 200, "200"); + Assert.assertTrue(responseHeaders.containsKey(InvalidHeaderServlet.VALID_HEADER_A_NAME)); + Assert.assertFalse(responseHeaders.containsKey(InvalidHeaderServlet.INVALID_HEADER_B_NAME)); + Assert.assertTrue(responseHeaders.containsKey(InvalidHeaderServlet.VALID_HEADER_C_NAME)); + + validateResponseEnd(ajpClient.readMessage(), true); + + // Double check the connection is still open + validateCpong(ajpClient.cping()); + + ajpClient.disconnect(); + } + + + private static class InvalidHeaderServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final String VALID_HEADER_A_NAME = "X-Bug66512-A"; + private static final String VALID_HEADER_A_VALUE = "AaAaA"; + private static final String VALID_HEADER_C_NAME = "X-Bug66512-C"; + private static final String VALID_HEADER_C_VALUE = "CcCcC"; + + private static final String INVALID_HEADER_B_NAME = "X-Bug66512-B"; + private static final String INVALID_HEADER_B_VALUE = "Bb\u039abB"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setHeader(VALID_HEADER_A_NAME, VALID_HEADER_A_VALUE); + resp.setHeader(INVALID_HEADER_B_NAME, INVALID_HEADER_B_VALUE); + resp.setHeader(VALID_HEADER_C_NAME, VALID_HEADER_C_VALUE); + } + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=66591 + */ + @Test + public void testNoHeaders() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "bug66591", new NoHeadersServlet()); + ctx.addServletMappingDecoded("/", "bug66591"); + + tomcat.start(); + + SimpleAjpClient ajpClient = new SimpleAjpClient(); + ajpClient.setPort(getPort()); + ajpClient.connect(); + + validateCpong(ajpClient.cping()); + + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.end(); + + TesterAjpMessage responseHeaderMessage = ajpClient.sendMessage(forwardMessage, null); + + // Expect 3 messages: headers, body chunk, end + Map> responseHeaders = validateResponseHeaders(responseHeaderMessage, 200, "200"); + Assert.assertTrue(responseHeaders.isEmpty()); + + String body = extractResponseBody(ajpClient.readMessage()); + Assert.assertTrue(body.isEmpty()); + + validateResponseEnd(ajpClient.readMessage(), true); + + // Double check the connection is still open + validateCpong(ajpClient.cping()); + + ajpClient.disconnect(); + } + + + private static class NoHeadersServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.flushBuffer(); + } + } + + + /** + * Process response header packet and checks the status. Any other data is ignored. + */ + private Map> validateResponseHeaders(TesterAjpMessage message, int expectedStatus, + String expectedMessage) throws Exception { + // First two bytes should always be AB + Assert.assertEquals((byte) 'A', message.buf[0]); + Assert.assertEquals((byte) 'B', message.buf[1]); + + // Set the start position and read the length + message.processHeader(false); + + // Check the length + Assert.assertTrue(message.len > 0); + + // Should be a header message + Assert.assertEquals(0x04, message.readByte()); + + // Check status + Assert.assertEquals(expectedStatus, message.readInt()); + + // Check the reason phrase + Assert.assertEquals(expectedMessage, message.readString()); + + // Get the number of headers + int headerCount = message.readInt(); + + Map> headerMap = new HashMap<>(); + for (int i = 0; i < headerCount; i++) { + String headerName = message.readHeaderName(); + String headerValue = message.readString(); + headerMap.computeIfAbsent(headerName, k -> new ArrayList<>()).add(headerValue); + } + + return headerMap; + } + + private void validateGetBody(TesterAjpMessage message) { + // First two bytes should always be AB + Assert.assertEquals((byte) 'A', message.buf[0]); + Assert.assertEquals((byte) 'B', message.buf[1]); + // Should be a body chunk message + Assert.assertEquals(0x06, message.readByte()); + } + + /** + * Extract the content from a response message. + */ + private String extractResponseBody(TesterAjpMessage message) throws Exception { + + Assert.assertEquals((byte) 'A', message.buf[0]); + Assert.assertEquals((byte) 'B', message.buf[1]); + + // Set the start position and read the length + message.processHeader(false); + + // Should be a body chunk message + Assert.assertEquals(0x03, message.readByte()); + + int len = message.readInt(); + Assert.assertTrue(len >= 0); + return message.readString(len); + } + + /** + * Validates that the response message is valid and contains the expected content. + */ + private void validateResponseBody(TesterAjpMessage message, String expectedBody) throws Exception { + + String body = extractResponseBody(message); + Assert.assertTrue(body.contains(expectedBody)); + } + + private void validateResponseEnd(TesterAjpMessage message, boolean expectedReuse) { + Assert.assertEquals((byte) 'A', message.buf[0]); + Assert.assertEquals((byte) 'B', message.buf[1]); + + message.processHeader(false); + + // Should be an end body message + Assert.assertEquals(0x05, message.readByte()); + + // Check the length + Assert.assertEquals(2, message.getLen()); + + boolean reuse = false; + if (message.readByte() > 0) { + reuse = true; + } + + Assert.assertEquals(Boolean.valueOf(expectedReuse), Boolean.valueOf(reuse)); + } + + private void validateCpong(TesterAjpMessage message) throws Exception { + // First two bytes should always be AB + Assert.assertEquals((byte) 'A', message.buf[0]); + Assert.assertEquals((byte) 'B', message.buf[1]); + // CPONG should have a message length of 1 + // This effectively checks the next two bytes + Assert.assertEquals(1, message.getLen()); + // Data should be the value 9 + Assert.assertEquals(9, message.buf[4]); + } + + + private static class Tester304WithBodyServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + resp.setStatus(304); + resp.getWriter().print("Body not permitted for 304 response"); + } + } + + + private static class ReadBodyServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean callAvailable; + final List availableList; + final List readList; + + ReadBodyServlet(boolean callAvailable) { + this.callAvailable = callAvailable; + this.availableList = callAvailable ? new ArrayList<>() : null; + this.readList = callAvailable ? new ArrayList<>() : null; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doRequest(req, resp, false); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doRequest(req, resp, true); + } + + private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean isPost) + throws IOException { + + long readCount = 0; + + try (InputStream s = request.getInputStream()) { + byte[] buf = new byte[4096]; + int read; + do { + if (callAvailable) { + int available = s.available(); + read = s.read(buf); + availableList.add(Integer.valueOf(available)); + readList.add(Integer.valueOf(read)); + } else { + read = s.read(buf); + } + if (read > 0) { + readCount += read; + } + } while (read > 0); + } + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + try (PrintWriter w = response.getWriter()) { + w.println("Method: " + (isPost ? "POST" : "GET") + ". Reading request body..."); + w.println("Request Body length in bytes: " + readCount); + } + } + } + + + private static class FixedResponseSizeServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final int responseSize; + private final int bufferSize; + + FixedResponseSizeServlet(int responseSize, int bufferSize) { + this.responseSize = responseSize; + this.bufferSize = bufferSize; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setBufferSize(bufferSize); + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.setContentLength(responseSize); + + PrintWriter pw = resp.getWriter(); + for (int i = 0; i < responseSize; i++) { + pw.append('X'); + } + } + } +} diff --git a/test/org/apache/coyote/ajp/TesterAjpMessage.java b/test/org/apache/coyote/ajp/TesterAjpMessage.java new file mode 100644 index 0000000..8067135 --- /dev/null +++ b/test/org/apache/coyote/ajp/TesterAjpMessage.java @@ -0,0 +1,199 @@ +/* + * 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.coyote.ajp; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Extends {@link AjpMessage} to provide additional methods for reading from the message. TODO: See if it makes sense + * for any/all of these methods to be transferred to AjpMessage + */ +public class TesterAjpMessage extends AjpMessage { + + private final List
    headers = new ArrayList<>(); + private final List attributes = new ArrayList<>(); + private transient Charset charset = StandardCharsets.UTF_8; + + + public TesterAjpMessage(int packetSize) { + super(packetSize); + } + + public byte readByte() { + return buf[pos++]; + } + + public int readInt() { + int val = (buf[pos++] & 0xFF) << 8; + val += buf[pos++] & 0xFF; + return val; + } + + public String readString() { + int len = readInt(); + return readString(len); + } + + public String readString(int len) { + CharBuffer buf = getCharset().decode(ByteBuffer.wrap(this.buf, pos, len)); + pos += len; + // Read end of string marker + readByte(); + + return new String(buf.array(), buf.arrayOffset(), buf.length()); + } + + public String readHeaderName() { + byte b = readByte(); + if ((b & 0xFF) == 0xA0) { + // Coded header + return Constants.getResponseHeaderForCode(readByte() - 1); + } else { + int len = (b & 0xFF) << 8; + len += getByte() & 0xFF; + return readString(len); + } + } + + + public void addHeader(int code, String value) { + headers.add(new Header(code, value)); + } + + + public void addHeader(String name, String value) { + headers.add(new Header(name, value)); + } + + + public void addAttribute(int code, String value) { + attributes.add(new Attribute(code, value)); + } + + + public void addAttribute(String name, String value) { + attributes.add(new Attribute(name, value)); + } + + public Charset getCharset() { + return charset; + } + + public void setCharset(Charset charset) { + this.charset = charset; + } + + @Override + public void end() { + // Add the header count + appendInt(headers.size()); + + for (Header header : headers) { + header.append(this); + } + + for (Attribute attribute : attributes) { + attribute.append(this); + } + + // Terminator + appendByte(0xFF); + + len = pos; + int dLen = len - 4; + + buf[0] = (byte) 0x12; + buf[1] = (byte) 0x34; + buf[2] = (byte) ((dLen >>> 8) & 0xFF); + buf[3] = (byte) (dLen & 0xFF); + } + + + @Override + public void reset() { + super.reset(); + headers.clear(); + } + + + public void appendString(String string) { + byte[] bytes = string.getBytes(StandardCharsets.ISO_8859_1); + appendBytes(bytes, 0, bytes.length); + } + + + private static class Header { + private final int code; + private final String name; + private final String value; + + Header(int code, String value) { + this.code = code; + this.name = null; + this.value = value; + } + + Header(String name, String value) { + this.code = 0; + this.name = name; + this.value = value; + } + + public void append(TesterAjpMessage message) { + if (code == 0) { + message.appendString(name); + } else { + message.appendInt(code); + } + message.appendString(value); + } + } + + + private static class Attribute { + private final int code; + private final String name; + private final String value; + + Attribute(int code, String value) { + this.code = code; + this.name = null; + this.value = value; + } + + Attribute(String name, String value) { + this.code = 0; + this.name = name; + this.value = value; + } + + public void append(TesterAjpMessage message) { + if (code == 0) { + message.appendByte(0x0A); + message.appendString(name); + } else { + message.appendByte(code); + } + message.appendString(value); + } + } +} diff --git a/test/org/apache/coyote/http11/TestHttp11InputBuffer.java b/test/org/apache/coyote/http11/TestHttp11InputBuffer.java new file mode 100644 index 0000000..9796330 --- /dev/null +++ b/test/org/apache/coyote/http11/TestHttp11InputBuffer.java @@ -0,0 +1,779 @@ +/* + * 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.coyote.http11; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Enumeration; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestHttp11InputBuffer extends TomcatBaseTest { + + private static final String CR = "\r"; + private static final String LF = "\n"; + private static final String CRLF = CR + LF; + + /** + * Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=48839 + */ + @Test + public void testBug48839() { + + Bug48839Client client = new Bug48839Client(); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + /** + * Bug 48839 test client. + */ + private class Bug48839Client extends SimpleHttpClient { + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Bug48839", new Bug48839Servlet()); + root.addServletMappingDecoded("/test", "Bug48839"); + + try { + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + + // Open connection + connect(); + + String[] request = new String[1]; + request[0] = "GET http://localhost:8080/test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + + "X-Bug48839: abcd" + CRLF + "\tefgh" + CRLF + "Connection: close" + CRLF + CRLF; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("abcd\tefgh")) { + return false; + } + return true; + } + + } + + private static class Bug48839Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + /** + * Only interested in the request headers from a GET request + */ + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Just echo the header value back as plain text + resp.setContentType("text/plain"); + + PrintWriter out = resp.getWriter(); + + Enumeration values = req.getHeaders("X-Bug48839"); + while (values.hasMoreElements()) { + out.println(values.nextElement()); + } + } + } + + + @Test + public void testBug51557Valid() { + + Bug51557Client client = new Bug51557Client("X-Bug51557Valid", "1234"); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("1234abcd", client.getResponseBody()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testBug51557Invalid() { + + Bug51557Client client = new Bug51557Client("X-Bug51557=Invalid", "1234", true); + + client.doRequest(); + Assert.assertTrue(client.isResponse400()); + } + + + @Test + public void testBug51557NoColon() { + + Bug51557Client client = new Bug51557Client("X-Bug51557NoColon"); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("abcd", client.getResponseBody()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testBug51557SeparatorsInName() throws Exception { + char httpSeparators[] = new char[] { '\t', ' ', '\"', '(', ')', ',', '/', ':', ';', '<', '=', '>', '?', '@', + '[', '\\', ']', '{', '}' }; + + for (char s : httpSeparators) { + doTestBug51557CharInName(s); + tearDown(); + setUp(); + } + } + + + @Test + public void testBug51557CtlInName() throws Exception { + for (int i = 0; i < 31; i++) { + doTestBug51557CharInName((char) i); + tearDown(); + setUp(); + } + doTestBug51557CharInName((char) 127); + } + + + @Test + public void testBug51557CtlInValue() throws Exception { + for (int i = 0; i < 31; i++) { + if (i == '\t') { + // TAB is allowed + continue; + } + if (i == '\n') { + // LF is the optional line terminator + continue; + } + doTestBug51557InvalidCharInValue((char) i); + tearDown(); + setUp(); + } + doTestBug51557InvalidCharInValue((char) 127); + } + + + @Test + public void testBug51557ObsTextInValue() throws Exception { + for (int i = 128; i < 255; i++) { + doTestBug51557ValidCharInValue((char) i); + tearDown(); + setUp(); + } + } + + + @Test + public void testBug51557Continuation() { + + Bug51557Client client = new Bug51557Client("X-Bug=51557NoColon", "foo" + SimpleHttpClient.CRLF + " bar"); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("abcd", client.getResponseBody()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testBug51557BoundaryStart() { + + Bug51557Client client = new Bug51557Client("=X-Bug51557", "invalid"); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("abcd", client.getResponseBody()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testBug51557BoundaryEnd() { + + Bug51557Client client = new Bug51557Client("X-Bug51557=", "invalid"); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("abcd", client.getResponseBody()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + private void doTestBug51557CharInName(char s) { + Bug51557Client client = new Bug51557Client("X-Bug" + s + "51557", "invalid"); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("abcd", client.getResponseBody()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + private void doTestBug51557InvalidCharInValue(char s) { + Bug51557Client client = new Bug51557Client("X-Bug51557-Invalid", "invalid" + s + "invalid"); + + client.doRequest(); + Assert.assertTrue("Testing [" + (int) s + "]", client.isResponse200()); + Assert.assertEquals("Testing [" + (int) s + "]", "abcd", client.getResponseBody()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + private void doTestBug51557ValidCharInValue(char s) { + Bug51557Client client = new Bug51557Client("X-Bug51557-Valid", "valid" + s + "valid"); + + client.doRequest(); + Assert.assertTrue("Testing [" + (int) s + "]", client.isResponse200()); + Assert.assertEquals("Testing [" + (int) s + "]", "valid" + s + "validabcd", client.getResponseBody()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + /** + * Bug 51557 test client. + */ + private class Bug51557Client extends SimpleHttpClient { + + private final String headerName; + private final String headerLine; + private final boolean rejectIllegalHeader; + + Bug51557Client(String headerName) { + this.headerName = headerName; + this.headerLine = headerName; + this.rejectIllegalHeader = false; + } + + Bug51557Client(String headerName, String headerValue) { + this(headerName, headerValue, false); + } + + Bug51557Client(String headerName, String headerValue, boolean rejectIllegalHeader) { + this.headerName = headerName; + this.headerLine = headerName + ": " + headerValue; + this.rejectIllegalHeader = rejectIllegalHeader; + } + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Bug51557", new Bug51557Servlet(headerName)); + root.addServletMappingDecoded("/test", "Bug51557"); + + try { + Connector connector = tomcat.getConnector(); + Assert.assertTrue(connector.setProperty("rejectIllegalHeader", Boolean.toString(rejectIllegalHeader))); + tomcat.start(); + setPort(connector.getLocalPort()); + + + // Open connection + connect(); + + String[] request = new String[1]; + request[0] = "GET http://localhost:8080/test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + + headerLine + CRLF + "X-Bug51557: abcd" + CRLF + "Connection: close" + CRLF + CRLF; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("abcd")) { + return false; + } + return true; + } + + } + + private static class Bug51557Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private String invalidHeaderName; + + /** + * @param invalidHeaderName The header name should be invalid and therefore ignored by the header parsing code + */ + Bug51557Servlet(String invalidHeaderName) { + this.invalidHeaderName = invalidHeaderName; + } + + /** + * Only interested in the request headers from a GET request + */ + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Just echo the header value back as plain text + resp.setContentType("text/plain"); + + PrintWriter out = resp.getWriter(); + + processHeaders(invalidHeaderName, req, out); + processHeaders("X-Bug51557", req, out); + } + + private void processHeaders(String header, HttpServletRequest req, PrintWriter out) { + Enumeration values = req.getHeaders(header); + while (values.hasMoreElements()) { + out.println(values.nextElement()); + } + } + } + + + /** + * Test case for new lines at the start of a request. RFC2616 does not permit any, but Tomcat is tolerant of them if + * they are present. + */ + @Test + public void testNewLines() { + + NewLinesClient client = new NewLinesClient(10); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + /** + * Test case for new lines at the start of a request. RFC2616 does not permit any, but Tomcat is tolerant of them if + * they are present. + */ + @Test + public void testNewLinesExcessive() { + + NewLinesClient client = new NewLinesClient(10000); + + // If the connection is closed fast enough, writing the request will + // fail and the response won't be read. + Exception e = client.doRequest(); + if (e == null) { + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + } + Assert.assertFalse(client.isResponseBodyOK()); + } + + + private class NewLinesClient extends SimpleHttpClient { + + private final String newLines; + + private NewLinesClient(int count) { + StringBuilder sb = new StringBuilder(count * 2); + for (int i = 0; i < count; i++) { + sb.append(CRLF); + } + newLines = sb.toString(); + } + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "test", new TesterServlet()); + root.addServletMappingDecoded("/test", "test"); + + try { + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + + // Open connection + connect(); + + String[] request = new String[1]; + request[0] = newLines + "GET http://localhost:8080/test HTTP/1.1" + CRLF + "Host: localhost:8080" + + CRLF + "X-Bug48839: abcd" + CRLF + "\tefgh" + CRLF + "Connection: close" + CRLF + CRLF; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("OK")) { + return false; + } + return true; + } + + } + + + /** + * Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=54947 + */ + @Test + public void testBug54947() { + + Bug54947Client client = new Bug54947Client(); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + /** + * Bug 54947 test client. + */ + private class Bug54947Client extends SimpleHttpClient { + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Bug54947", new TesterServlet()); + root.addServletMappingDecoded("/test", "Bug54947"); + + try { + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + + // Open connection + connect(); + + String[] request = new String[2]; + request[0] = "GET http://localhost:8080/test HTTP/1.1" + CR; + request[1] = LF + "Host: localhost:8080" + CRLF + "Connection: close" + CRLF + CRLF; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("OK")) { + return false; + } + return true; + } + + } + + + /** + * Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=59089 + */ + @Test + public void testBug59089() { + + Bug59089Client client = new Bug59089Client(); + + client.doRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + /** + * Bug 59089 test client. + */ + private class Bug59089Client extends SimpleHttpClient { + + private Exception doRequest() { + + // Ensure body is read correctly + setUseContentLength(true); + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Bug59089", new TesterServlet()); + root.addServletMappingDecoded("/test", "Bug59089"); + + try { + Connector connector = tomcat.getConnector(); + Assert.assertTrue(connector.setProperty("rejectIllegalHeader", "false")); + tomcat.start(); + setPort(connector.getLocalPort()); + + // Open connection + connect(); + + String[] request = new String[1]; + request[0] = "GET http://localhost:8080/test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + + "X-Header: Ignore" + CRLF + "X-Header" + (char) 130 + ": Broken" + CRLF + CRLF; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("OK")) { + return false; + } + return true; + } + } + + + @Test + public void testInvalidMethod() { + + String[] request = new String[1]; + request[0] = "GET" + (char) 0 + " /test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + "Connection: close" + + CRLF + CRLF; + + InvalidClient client = new InvalidClient(request); + + client.doRequest(); + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testInvalidHttp09() { + + String[] request = new String[1]; + request[0] = "GET /test" + CR + " " + LF; + + InvalidClient client = new InvalidClient(request); + + client.doRequest(); + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testInvalidEndOfRequestLine01() { + + String[] request = new String[1]; + request[0] = "GET /test HTTP/1.1" + CR + "Host: localhost:8080" + CRLF + "Connection: close" + CRLF + CRLF; + + InvalidClient client = new InvalidClient(request); + + client.doRequest(); + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testInvalidHeader01() { + + String[] request = new String[1]; + request[0] = "GET /test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + CR + "X-Header: xxx" + CRLF + + "Connection: close" + CRLF + CRLF; + + InvalidClient client = new InvalidClient(request); + + client.doRequest(); + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testInvalidContentLength01() { + doTestInvalidContentLength(false); + } + + + @Test + public void testInvalidContentLength02() { + doTestInvalidContentLength(true); + } + + + private void doTestInvalidContentLength(boolean rejectIllegalHeader) { + getTomcatInstance().getConnector().setProperty("rejectIllegalHeader", Boolean.toString(rejectIllegalHeader)); + + String[] request = new String[1]; + request[0] = "POST /test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + "Content-Length: 12\u000734" + + CRLF + "Connection: close" + CRLF + CRLF; + + InvalidClient client = new InvalidClient(request); + + client.doRequest(); + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + /** + * Invalid request test client. + */ + private class InvalidClient extends SimpleHttpClient { + + private final String[] request; + + InvalidClient(String[] request) { + this.request = request; + } + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + tomcat.addContext("", TEMP_DIR); + + try { + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + + // Open connection + connect(); + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + return true; + } + } + + + private static final class Client extends SimpleHttpClient { + + Client(int port) { + setPort(port); + } + + @Override + public boolean isResponseBodyOK() { + return getResponseBody().contains("test - data"); + } + } + + + @Test + public void testInvalidHeader02() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost" + SimpleHttpClient.CRLF + ":b" + + SimpleHttpClient.CRLF + "X-Dummy:b" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(10000, 600000); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + } +} diff --git a/test/org/apache/coyote/http11/TestHttp11InputBufferCRLF.java b/test/org/apache/coyote/http11/TestHttp11InputBufferCRLF.java new file mode 100644 index 0000000..d87bfa2 --- /dev/null +++ b/test/org/apache/coyote/http11/TestHttp11InputBufferCRLF.java @@ -0,0 +1,215 @@ +/* + * 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.coyote.http11; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +@RunWith(Parameterized.class) +public class TestHttp11InputBufferCRLF extends TomcatBaseTest { + + private static final String CR = "\r"; + private static final String LF = "\n"; + private static final String CRLF = CR + LF; + + @Parameterized.Parameters + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + // Requests to exercise code that allows HT in place of SP + parameterSets.add(new Object[] { Boolean.FALSE, new String[] { + "GET\t/test\tHTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + "Connection: close" + CRLF + CRLF }, + Boolean.TRUE }); + + // Requests to simulate package boundaries + // HTTP/0.9 request + addRequestWithSplits("GET /test" + CRLF, Boolean.TRUE, parameterSets); + + // HTTP/0.9 request with space + // Either malformed but acceptable HTTP/0.9 or invalid HTTP/1.1 + // Tomcat opts for invalid HTTP/1.1 + addRequestWithSplits("GET /test " + CRLF, Boolean.FALSE, Boolean.FALSE, parameterSets); + + // HTTP/0.9 request (no optional CR) + addRequestWithSplits("GET /test" + LF, Boolean.TRUE, parameterSets); + + // HTTP/0.9 request with space (no optional CR) + // Either malformed but acceptable HTTP/0.9 or invalid HTTP/1.1 + // Tomcat opts for invalid HTTP/1.1 + addRequestWithSplits("GET /test " + LF, Boolean.FALSE, Boolean.FALSE, parameterSets); + + // Standard HTTP/1.1 request + addRequestWithSplits( + "GET /test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + "Connection: close" + CRLF + CRLF, + Boolean.FALSE, parameterSets); + + // Standard HTTP/1.1 request with invalid HTTP protocol + addRequestWithSplits("GET /test HTTP/" + CR + "1.1" + CRLF + "Host: localhost:8080" + CRLF + + "Connection: close" + CRLF + CRLF, Boolean.FALSE, Boolean.FALSE, parameterSets); + + // Invalid (request target) HTTP/1.1 request + addRequestWithSplits( + "GET /te parameterSets) { + addRequestWithSplits(request, isHttp09, Boolean.TRUE, parameterSets); + } + + private static void addRequestWithSplits(String request, Boolean isHttp09, Boolean valid, + List parameterSets) { + // Add as a single String + parameterSets.add(new Object[] { isHttp09, new String[] { request }, valid }); + + // Add with all CRLF split between the CR and LF + List parts = new ArrayList<>(); + int lastPos = 0; + int pos = request.indexOf('\n'); + while (pos > -1) { + parts.add(request.substring(lastPos, pos)); + lastPos = pos; + pos = request.indexOf('\n', lastPos + 1); + } + parts.add(request.substring(lastPos)); + parameterSets.add(new Object[] { isHttp09, parts.toArray(new String[0]), valid }); + + // Add with a split between each character + List chars = new ArrayList<>(); + for (char c : request.toCharArray()) { + chars.add(Character.toString(c)); + } + parameterSets.add(new Object[] { isHttp09, chars.toArray(new String[0]), valid }); + } + + @Parameter(0) + public boolean isHttp09; + + @Parameter(1) + public String[] request; + + @Parameter(2) + public boolean valid; + + + @Test + public void testBug54947() { + + Client client = new Client(request, isHttp09); + + Exception e = client.doRequest(); + + if (valid) { + Assert.assertTrue(client.isResponseBodyOK()); + } else if (e == null) { + Assert.assertTrue(client.isResponse400()); + } else { + // The invalid request was detected before the entire request had + // been sent to Tomcat reset the connection when the client tried to + // continue sending the invalid request. + } + } + + + private class Client extends SimpleHttpClient { + + Client(String[] request, boolean isHttp09) { + setRequest(request); + setUseHttp09(isHttp09); + } + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "TesterServlet", new TesterServlet()); + root.addServletMappingDecoded("/test", "TesterServlet"); + + try { + tomcat.start(); + setPort(tomcat.getConnector().getLocalPort()); + setRequestPause(20); + + // Open connection + connect(); + + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("OK")) { + return false; + } + return true; + } + } +} diff --git a/test/org/apache/coyote/http11/TestHttp11OutputBuffer.java b/test/org/apache/coyote/http11/TestHttp11OutputBuffer.java new file mode 100644 index 0000000..00a4664 --- /dev/null +++ b/test/org/apache/coyote/http11/TestHttp11OutputBuffer.java @@ -0,0 +1,139 @@ +/* + * 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.coyote.http11; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.ExpectationClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestHttp11OutputBuffer extends TomcatBaseTest { + + @Test + public void testSendAck() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "echo", new EchoBodyServlet()); + ctx.addServletMappingDecoded("/echo", "echo"); + + tomcat.start(); + + ExpectationClient client = new ExpectationClient(); + + client.setPort(tomcat.getConnector().getLocalPort()); + // Expected content doesn't end with a CR-LF so if it isn't chunked make + // sure the content length is used as reading it line-by-line will fail + // since there is no "line". + client.setUseContentLength(true); + + client.connect(); + + client.doRequestHeaders(); + Assert.assertTrue(client.isResponse100()); + + client.doRequestBody(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testHTTPHeaderBelow128() throws Exception { + doTestHTTPHeaderValue("This should be OK", true); + } + + + @Test + public void testHTTPHeader128To255() throws Exception { + doTestHTTPHeaderValue("\u00A0 should be OK", true); + } + + + @Test + public void testHTTPHeaderAbove255() throws Exception { + doTestHTTPHeaderValue("\u0100 should fail", false); + } + + + private void doTestHTTPHeaderValue(String customHeaderValue, boolean valid) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "header", new HeaderServlet(customHeaderValue)); + ctx.addServletMappingDecoded("/header", "header"); + + tomcat.start(); + + Map> resHeaders = new HashMap<>(); + int rc = getUrl("http://localhost:" + getPort() + "/header", new ByteChunk(), resHeaders); + + if (valid) { + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + List values = resHeaders.get(HeaderServlet.CUSTOM_HEADER_NAME); + Assert.assertNotNull(values); + Assert.assertEquals(1, values.size()); + Assert.assertEquals(customHeaderValue, values.get(0)); + } else { + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + List values = resHeaders.get(HeaderServlet.CUSTOM_HEADER_NAME); + Assert.assertNull(values); + } + } + + + private static class HeaderServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final String CUSTOM_HEADER_NAME = "X-Test"; + + private final String customHeaderValue; + + HeaderServlet(String customHeaderValue) { + this.customHeaderValue = customHeaderValue; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + resp.setHeader(CUSTOM_HEADER_NAME, customHeaderValue); + + resp.flushBuffer(); + } + } +} diff --git a/test/org/apache/coyote/http11/TestHttp11Processor.java b/test/org/apache/coyote/http11/TestHttp11Processor.java new file mode 100644 index 0000000..d84ace5 --- /dev/null +++ b/test/org/apache/coyote/http11/TestHttp11Processor.java @@ -0,0 +1,1915 @@ +/* + * 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.coyote.http11; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.B2CConverter; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.http.parser.TokenList; + +public class TestHttp11Processor extends TomcatBaseTest { + + @Test + public void testResponseWithErrorChunked() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add protected servlet + Tomcat.addServlet(ctx, "ChunkedResponseWithErrorServlet", new ResponseWithErrorServlet(true)); + ctx.addServletMappingDecoded("/*", "ChunkedResponseWithErrorServlet"); + + tomcat.start(); + + String request = "GET /anything HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 200 response followed by an incomplete chunked + // body. + Assert.assertTrue(client.isResponse200()); + // Should use chunked encoding + String transferEncoding = null; + for (String header : client.getResponseHeaders()) { + if (header.startsWith("Transfer-Encoding:")) { + transferEncoding = header.substring(18).trim(); + } + } + Assert.assertEquals("chunked", transferEncoding); + // There should not be an end chunk + Assert.assertFalse(client.getResponseBody().endsWith("0")); + // The last portion of text should be there + Assert.assertTrue(client.getResponseBody().endsWith("line03")); + } + + private static class ResponseWithErrorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean useChunks; + + ResponseWithErrorServlet(boolean useChunks) { + this.useChunks = useChunks; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + if (!useChunks) { + // Longer than it needs to be because response will fail before + // it is complete + resp.setContentLength(100); + } + PrintWriter pw = resp.getWriter(); + pw.print("line01"); + pw.flush(); + resp.flushBuffer(); + pw.print("line02"); + pw.flush(); + resp.flushBuffer(); + pw.print("line03"); + + // Now throw a RuntimeException to end this request + throw new ServletException("Deliberate failure"); + } + } + + + @Test + public void testWithUnknownExpectation() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + String request = "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + + SimpleHttpClient.CRLF + "Expect: unknown" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(getPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse417()); + } + + + @Test + public void testWithTEVoid() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + String request = "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + + SimpleHttpClient.CRLF + "Transfer-encoding: void" + SimpleHttpClient.CRLF + "Content-Length: 9" + + SimpleHttpClient.CRLF + "Content-Type: application/x-www-form-urlencoded" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + "test=data"; + + Client client = new Client(getPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse501()); + } + + + @Test + public void testWithTEBuffered() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + String request = "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + + SimpleHttpClient.CRLF + "Transfer-encoding: buffered" + SimpleHttpClient.CRLF + "Content-Length: 9" + + SimpleHttpClient.CRLF + "Content-Type: application/x-www-form-urlencoded" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + "test=data"; + + Client client = new Client(getPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse501()); + } + + + @Test + public void testWithTEChunked() throws Exception { + doTestWithTEChunked(false); + } + + + @Test + public void testWithTEChunkedWithCL() throws Exception { + // Should be ignored + doTestWithTEChunked(true); + } + + + private void doTestWithTEChunked(boolean withCL) throws Exception { + + getTomcatInstanceTestWebapp(false, true); + + String request = "POST /test/echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + + SimpleHttpClient.CRLF + (withCL ? "Content-length: 1" + SimpleHttpClient.CRLF : "") + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + SimpleHttpClient.CRLF + "Connection: close" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF + "9" + SimpleHttpClient.CRLF + "test=data" + + SimpleHttpClient.CRLF + "0" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(getPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.getResponseBody().contains("test - data")); + } + + + @Test + public void testWithTESavedRequest() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + String request = "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + + SimpleHttpClient.CRLF + "Transfer-encoding: savedrequest" + SimpleHttpClient.CRLF + + "Content-Length: 9" + SimpleHttpClient.CRLF + "Content-Type: application/x-www-form-urlencoded" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF + "test=data"; + + Client client = new Client(getPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse501()); + } + + + @Test + public void testWithTEUnsupported() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + String request = "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + + SimpleHttpClient.CRLF + "Transfer-encoding: unsupported" + SimpleHttpClient.CRLF + "Content-Length: 9" + + SimpleHttpClient.CRLF + "Content-Type: application/x-www-form-urlencoded" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + "test=data"; + + Client client = new Client(getPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse501()); + } + + + @Test + public void testPipelining() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add protected servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String requestPart1 = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF; + String requestPart2 = "Host: any" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + final Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { requestPart1, requestPart2 }); + client.setRequestPause(1000); + client.setUseContentLength(true); + client.connect(); + + Runnable send = new Runnable() { + @Override + public void run() { + try { + client.sendRequest(); + client.sendRequest(); + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } + }; + Thread t = new Thread(send); + t.start(); + + // Sleep for 1500 ms which should mean the all of request 1 has been + // sent and half of request 2 + Thread.sleep(1500); + + // Now read the first response + client.readResponse(true); + Assert.assertFalse(client.isResponse50x()); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("OK", client.getResponseBody()); + + // Read the second response. No need to sleep, read will block until + // there is data to process + client.readResponse(true); + Assert.assertFalse(client.isResponse50x()); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("OK", client.getResponseBody()); + } + + + @Test + public void testPipeliningBug64974() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add protected servlet + Wrapper w = Tomcat.addServlet(ctx, "servlet", new Bug64974Servlet()); + w.setAsyncSupported(true); + ctx.addServletMappingDecoded("/foo", "servlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: any" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + final Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + client.setUseContentLength(true); + client.connect(); + client.sendRequest(); + + // Now read the first response + client.readResponse(true); + Assert.assertFalse(client.isResponse50x()); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("OK", client.getResponseBody()); + + // Read the second response. No need to sleep, read will block until + // there is data to process + client.readResponse(true); + Assert.assertFalse(client.isResponse50x()); + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("OK", client.getResponseBody()); + } + + + @Test + public void testChunking11NoContentLength() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "NoContentLengthFlushingServlet", new NoContentLengthFlushingServlet()); + ctx.addServletMappingDecoded("/test", "NoContentLengthFlushingServlet"); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String transferEncoding = getSingleHeader("Transfer-Encoding", responseHeaders); + Assert.assertEquals("chunked", transferEncoding); + } + + @Test + public void testNoChunking11NoContentLengthConnectionClose() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "NoContentLengthConnectionCloseFlushingServlet", + new NoContentLengthConnectionCloseFlushingServlet()); + ctx.addServletMappingDecoded("/test", "NoContentLengthConnectionCloseFlushingServlet"); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String connection = getSingleHeader("Connection", responseHeaders); + Assert.assertEquals("close", connection); + + Assert.assertFalse(responseHeaders.containsKey("Transfer-Encoding")); + + Assert.assertEquals("OK", responseBody.toString()); + } + + @Test + public void testBug53677a() throws Exception { + doTestBug53677(false); + } + + @Test + public void testBug53677b() throws Exception { + doTestBug53677(true); + } + + private void doTestBug53677(boolean flush) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "LargeHeaderServlet", new LargeHeaderServlet(flush)); + ctx.addServletMappingDecoded("/test", "LargeHeaderServlet"); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + if (responseBody.getLength() > 0) { + // It will be >0 if the standard error page handling has been + // triggered + Assert.assertFalse(responseBody.toString().contains("FAIL")); + } + } + + + private static CountDownLatch bug55772Latch1 = new CountDownLatch(1); + private static CountDownLatch bug55772Latch2 = new CountDownLatch(1); + private static CountDownLatch bug55772Latch3 = new CountDownLatch(1); + private static boolean bug55772IsSecondRequest = false; + private static boolean bug55772RequestStateLeaked = false; + + + @Test + public void testBug55772() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Assert.assertTrue(tomcat.getConnector().setProperty("processorCache", "1")); + Assert.assertTrue(tomcat.getConnector().setProperty("maxThreads", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "async", new Bug55772Servlet()); + ctx.addServletMappingDecoded("/*", "async"); + + tomcat.start(); + + String request1 = "GET /async?1 HTTP/1.1\r\n" + "Host: localhost:" + getPort() + "\r\n" + + "Connection: keep-alive\r\n" + "Cache-Control: max-age=0\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" + + "User-Agent: Request1\r\n" + "Accept-Encoding: gzip,deflate,sdch\r\n" + + "Accept-Language: en-US,en;q=0.8,fr;q=0.6,es;q=0.4\r\n" + + "Cookie: something.that.should.not.leak=true\r\n" + "\r\n"; + + String request2 = "GET /async?2 HTTP/1.1\r\n" + "Host: localhost:" + getPort() + "\r\n" + + "Connection: keep-alive\r\n" + "Cache-Control: max-age=0\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" + + "User-Agent: Request2\r\n" + "Accept-Encoding: gzip,deflate,sdch\r\n" + + "Accept-Language: en-US,en;q=0.8,fr;q=0.6,es;q=0.4\r\n" + "\r\n"; + + try (Socket connection = new Socket("localhost", getPort())) { + connection.setSoLinger(true, 0); + Writer writer = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.US_ASCII); + writer.write(request1); + writer.flush(); + + bug55772Latch1.await(); + connection.close(); + } + + bug55772Latch2.await(); + bug55772IsSecondRequest = true; + + try (Socket connection = new Socket("localhost", getPort())) { + connection.setSoLinger(true, 0); + Writer writer = new OutputStreamWriter(connection.getOutputStream(), B2CConverter.getCharset("US-ASCII")); + writer.write(request2); + writer.flush(); + connection.getInputStream().read(); + } + + bug55772Latch3.await(); + if (bug55772RequestStateLeaked) { + Assert.fail("State leaked between requests!"); + } + } + + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=57324 + @Test + public void testNon2xxResponseWithExpectation() throws Exception { + doTestNon2xxResponseAndExpectation(true); + } + + @Test + public void testNon2xxResponseWithoutExpectation() throws Exception { + doTestNon2xxResponseAndExpectation(false); + } + + private void doTestNon2xxResponseAndExpectation(boolean useExpectation) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "echo", new EchoBodyServlet()); + ctx.addServletMappingDecoded("/echo", "echo"); + + SecurityCollection collection = new SecurityCollection("All", ""); + collection.addPatternDecoded("/*"); + SecurityConstraint constraint = new SecurityConstraint(); + constraint.addAuthRole("Any"); + constraint.addCollection(collection); + ctx.addConstraint(constraint); + + tomcat.start(); + + String request = "POST /echo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + + SimpleHttpClient.CRLF + "Content-Length: 10" + SimpleHttpClient.CRLF; + if (useExpectation) { + request += "Expect: 100-continue" + SimpleHttpClient.CRLF; + } + request += SimpleHttpClient.CRLF + "HelloWorld"; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + client.setUseContentLength(true); + + client.connect(); + client.processRequest(); + + Assert.assertTrue(client.isResponse403()); + String connectionHeaderValue = null; + for (String header : client.getResponseHeaders()) { + if (header.startsWith("Connection:")) { + connectionHeaderValue = header.substring(header.indexOf(':') + 1).trim(); + break; + } + } + + if (useExpectation) { + List connectionHeaders = new ArrayList<>(); + TokenList.parseTokenList(new StringReader(connectionHeaderValue), connectionHeaders); + Assert.assertEquals(1, connectionHeaders.size()); + Assert.assertEquals("close", connectionHeaders.get(0)); + } else { + Assert.assertNull(connectionHeaderValue); + } + } + + + private static class Bug55772Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (bug55772IsSecondRequest) { + Cookie[] cookies = req.getCookies(); + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : req.getCookies()) { + if (cookie.getName().equalsIgnoreCase("something.that.should.not.leak")) { + bug55772RequestStateLeaked = true; + break; + } + } + } + bug55772Latch3.countDown(); + } else { + req.getCookies(); // We have to do this so Tomcat will actually parse the cookies from the request + } + + req.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.TRUE); + AsyncContext asyncContext = req.startAsync(); + asyncContext.setTimeout(5000); + + bug55772Latch1.countDown(); + + PrintWriter writer = asyncContext.getResponse().getWriter(); + writer.print('\n'); + writer.flush(); + + bug55772Latch2.countDown(); + } + } + + + private static final class LargeHeaderServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + boolean flush = false; + + LargeHeaderServlet(boolean flush) { + this.flush = flush; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String largeValue = CharBuffer.allocate(10000).toString().replace('\0', 'x'); + resp.setHeader("x-Test", largeValue); + if (flush) { + resp.flushBuffer(); + } + resp.setContentType("text/plain"); + resp.getWriter().print("FAIL"); + } + + } + + // flushes with no content-length set + // should result in chunking on HTTP 1.1 + private static final class NoContentLengthFlushingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/plain"); + resp.getWriter().write("OK"); + resp.flushBuffer(); + } + } + + // flushes with no content-length set but sets Connection: close header + // should no result in chunking on HTTP 1.1 + private static final class NoContentLengthConnectionCloseFlushingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/event-stream"); + resp.addHeader("Connection", "close"); + resp.flushBuffer(); + resp.getWriter().write("OK"); + resp.flushBuffer(); + } + } + + private static final class Client extends SimpleHttpClient { + + Client(int port) { + setPort(port); + } + + @Override + public boolean isResponseBodyOK() { + return getResponseBody().contains("test - data"); + } + } + + + /* + * Partially read chunked input is not swallowed when it is read during async processing. + */ + @Test + public void testBug57621a() throws Exception { + doTestBug57621(true); + } + + + @Test + public void testBug57621b() throws Exception { + doTestBug57621(false); + } + + + private void doTestBug57621(boolean delayAsyncThread) throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = getProgrammaticRootContext(); + Wrapper w = Tomcat.addServlet(root, "Bug57621", new Bug57621Servlet(delayAsyncThread)); + w.setAsyncSupported(true); + root.addServletMappingDecoded("/test", "Bug57621"); + + tomcat.start(); + + Bug57621Client client = new Bug57621Client(); + client.setPort(tomcat.getConnector().getLocalPort()); + + client.setUseContentLength(true); + + client.connect(); + + client.doRequest(); + Assert.assertTrue(client.getResponseLine(), client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + + // Do the request again to ensure that the remaining body was swallowed + client.resetResponse(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.isResponseBodyOK()); + + client.disconnect(); + } + + + private static class Bug57621Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final boolean delayAsyncThread; + + + Bug57621Servlet(boolean delayAsyncThread) { + this.delayAsyncThread = delayAsyncThread; + } + + + @Override + protected void doPut(HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + final AsyncContext ac = req.startAsync(); + ac.start(new Runnable() { + @Override + public void run() { + if (delayAsyncThread) { + // Makes the difference between calling complete before + // the request body is received of after. + try { + Thread.sleep(1500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + try { + resp.getWriter().print("OK"); + } catch (IOException e) { + // Should never happen. Test will fail if it does. + } + ac.complete(); + } + }); + } + } + + + private static class Bug57621Client extends SimpleHttpClient { + + private Exception doRequest() { + try { + String[] request = new String[2]; + request[0] = "PUT http://localhost:8080/test HTTP/1.1" + CRLF + "Host: localhost:8080" + CRLF + + "Transfer-encoding: chunked" + CRLF + CRLF + "2" + CRLF + "OK"; + + request[1] = CRLF + "0" + CRLF + CRLF; + + setRequest(request); + processRequest(); // blocks until response has been read + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("OK")) { + return false; + } + return true; + } + } + + + @Test + public void testBug59310() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "Bug59310", new Bug59310Servlet()); + ctx.addServletMappingDecoded("/test", "Bug59310"); + + tomcat.start(); + + ByteChunk getBody = new ByteChunk(); + Map> getHeaders = new HashMap<>(); + int getStatus = getUrl("http://localhost:" + getPort() + "/test", getBody, getHeaders); + + ByteChunk headBody = new ByteChunk(); + Map> headHeaders = new HashMap<>(); + int headStatus = getUrl("http://localhost:" + getPort() + "/test", headBody, headHeaders); + + Assert.assertEquals(HttpServletResponse.SC_OK, getStatus); + Assert.assertEquals(HttpServletResponse.SC_OK, headStatus); + + Assert.assertEquals(0, getBody.getLength()); + Assert.assertEquals(0, headBody.getLength()); + + if (getHeaders.containsKey("Content-Length")) { + Assert.assertEquals(getHeaders.get("Content-Length"), headHeaders.get("Content-Length")); + } else { + Assert.assertFalse(headHeaders.containsKey("Content-Length")); + } + } + + + private static class Bug59310Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + } + + @Override + protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + } + } + + + /* + * Tests what happens if a request is completed during a dispatch but the request body has not been fully read. + */ + @Test + public void testRequestBodySwallowing() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + DispatchingServlet servlet = new DispatchingServlet(); + Wrapper w = Tomcat.addServlet(ctx, "Test", servlet); + w.setAsyncSupported(true); + ctx.addServletMappingDecoded("/test", "Test"); + + tomcat.start(); + + // Hand-craft the client so we have complete control over the timing + SocketAddress addr = new InetSocketAddress("localhost", getPort()); + Socket socket = new Socket(); + socket.setSoTimeout(300000); + socket.connect(addr, 300000); + OutputStream os = socket.getOutputStream(); + Writer writer = new OutputStreamWriter(os, "ISO-8859-1"); + InputStream is = socket.getInputStream(); + Reader r = new InputStreamReader(is, "ISO-8859-1"); + BufferedReader reader = new BufferedReader(r); + + // Write the headers + writer.write("POST /test HTTP/1.1\r\n"); + writer.write("Host: localhost:8080\r\n"); + writer.write("Transfer-Encoding: chunked\r\n"); + writer.write("\r\n"); + writer.flush(); + + validateResponse(reader); + + // Write the request body + writer.write("2\r\n"); + writer.write("AB\r\n"); + writer.write("0\r\n"); + writer.write("\r\n"); + writer.flush(); + + // Write the 2nd request + writer.write("POST /test HTTP/1.1\r\n"); + writer.write("Host: localhost:8080\r\n"); + writer.write("Transfer-Encoding: chunked\r\n"); + writer.write("\r\n"); + writer.flush(); + + // Read the 2nd response + validateResponse(reader); + + // Write the 2nd request body + writer.write("2\r\n"); + writer.write("AB\r\n"); + writer.write("0\r\n"); + writer.write("\r\n"); + writer.flush(); + + // Done + socket.close(); + } + + + private void validateResponse(BufferedReader reader) throws IOException { + // First line has the response code and should always be 200 + String line = reader.readLine(); + Assert.assertEquals("HTTP/1.1 200 ", line); + while (!"OK".equals(line)) { + line = reader.readLine(); + } + } + + + private static class DispatchingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (DispatcherType.ASYNC.equals(req.getDispatcherType())) { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write("OK\n"); + } else { + req.startAsync().dispatch(); + } + } + } + + @Test + public void testBug61086() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Bug61086Servlet servlet = new Bug61086Servlet(); + Tomcat.addServlet(ctx, "Test", servlet); + ctx.addServletMappingDecoded("/test", "Test"); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + Map> responseHeaders = new HashMap<>(); + int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); + + Assert.assertEquals(HttpServletResponse.SC_RESET_CONTENT, rc); + String contentLength = getSingleHeader("Content-Length", responseHeaders); + Assert.assertEquals("0", contentLength); + Assert.assertTrue(responseBody.getLength() == 0); + } + + private static final class Bug61086Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(205); + } + } + + /* + * Multiple, different Host headers + */ + @Test + public void testMultipleHostHeader01() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: a" + SimpleHttpClient.CRLF + "Host: b" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + /* + * Multiple instances of the same Host header + */ + @Test + public void testMultipleHostHeader02() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: a" + SimpleHttpClient.CRLF + "Host: a" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + @Test + public void testMissingHostHeader() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + @Test + public void testInconsistentHostHeader01() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://a/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: b" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + @Test + public void testInconsistentHostHeader02() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://a:8080/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: b:8080" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + @Test + public void testInconsistentHostHeader03() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://user:pwd@a/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: b" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + /* + * Hostname (no port) is included in the request line, but Host header is empty. Added for bug 62739. + */ + @Test + public void testInconsistentHostHeader04() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://a/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: " + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + /* + * Hostname (with port) is included in the request line, but Host header is empty. Added for bug 62739. + */ + @Test + public void testInconsistentHostHeader05() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://a:8080/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: " + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + /* + * Hostname (with port and user) is included in the request line, but Host header is empty. Added for bug 62739. + */ + @Test + public void testInconsistentHostHeader06() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://user:pwd@a/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: " + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 400 response. + Assert.assertTrue(client.isResponse400()); + } + + + /* + * Request line host is an exact match for Host header (no port) + */ + @Test + public void testConsistentHostHeader01() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://a/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: a" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 200 response. + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("request.getServerName() is [a] and request.getServerPort() is 80", + client.getResponseBody()); + } + + /* + * Request line host is an exact match for Host header (with port) + */ + @Test + public void testConsistentHostHeader02() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://a:8080/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: a:8080" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 200 response. + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("request.getServerName() is [a] and request.getServerPort() is 8080", + client.getResponseBody()); + + } + + /* + * Request line host is an exact match for Host header (no port, with user info) + */ + @Test + public void testConsistentHostHeader03() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://user:pwd@a/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: a" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 200 response. + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("request.getServerName() is [a] and request.getServerPort() is 80", + client.getResponseBody()); + } + + /* + * Request line host is case insensitive match for Host header (no port, no user info) + */ + @Test + public void testConsistentHostHeader04() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET http://a/foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: A" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 200 response. + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("request.getServerName() is [A] and request.getServerPort() is 80", + client.getResponseBody()); + } + + /* + * Host header exists but its value is an empty string. This is valid if the request line does not include a + * hostname/port. Added for bug 62739. + */ + @Test + public void testBlankHostHeader01() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: " + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 200 response. + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("request.getServerName() is [] and request.getServerPort() is " + getPort(), + client.getResponseBody()); + } + + /* + * Host header exists but has its value is empty (and there are multiple spaces after the ':'. This is valid if the + * request line does not include a hostname/port. Added for bug 62739. + */ + @Test + public void testBlankHostHeader02() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // This setting means the connection will be closed at the end of the + // request + Assert.assertTrue(tomcat.getConnector().setProperty("maxKeepAliveRequests", "1")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new ServerNameTesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: " + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + + // Expected response is a 200 response. + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("request.getServerName() is [] and request.getServerPort() is " + getPort(), + client.getResponseBody()); + } + + + @Test + public void testKeepAliveHeader01() throws Exception { + doTestKeepAliveHeader(false, 3000, 10, false); + } + + @Test + public void testKeepAliveHeader02() throws Exception { + doTestKeepAliveHeader(true, 5000, 1, false); + } + + @Test + public void testKeepAliveHeader03() throws Exception { + doTestKeepAliveHeader(true, 5000, 10, false); + } + + @Test + public void testKeepAliveHeader04() throws Exception { + doTestKeepAliveHeader(true, -1, 10, false); + } + + @Test + public void testKeepAliveHeader05() throws Exception { + doTestKeepAliveHeader(true, -1, 1, false); + } + + @Test + public void testKeepAliveHeader06() throws Exception { + doTestKeepAliveHeader(true, -1, -1, false); + } + + @Test + public void testKeepAliveHeader07() throws Exception { + doTestKeepAliveHeader(false, 3000, 10, true); + } + + @Test + public void testKeepAliveHeader08() throws Exception { + doTestKeepAliveHeader(true, 5000, 1, true); + } + + @Test + public void testKeepAliveHeader09() throws Exception { + doTestKeepAliveHeader(true, 5000, 10, true); + } + + @Test + public void testKeepAliveHeader10() throws Exception { + doTestKeepAliveHeader(true, -1, 10, true); + } + + @Test + public void testKeepAliveHeader11() throws Exception { + doTestKeepAliveHeader(true, -1, 1, true); + } + + @Test + public void testKeepAliveHeader12() throws Exception { + doTestKeepAliveHeader(true, -1, -1, true); + } + + private void doTestKeepAliveHeader(boolean sendKeepAlive, int keepAliveTimeout, int maxKeepAliveRequests, + boolean explicitClose) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + tomcat.getConnector().setProperty("keepAliveTimeout", Integer.toString(keepAliveTimeout)); + tomcat.getConnector().setProperty("maxKeepAliveRequests", Integer.toString(maxKeepAliveRequests)); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet(explicitClose)); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + + SimpleHttpClient.CRLF; + + if (sendKeepAlive) { + request += "Connection: keep-alive" + SimpleHttpClient.CRLF; + } + + request += SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(false); + + Assert.assertTrue(client.isResponse200()); + + String connectionHeaderValue = null; + String keepAliveHeaderValue = null; + for (String header : client.getResponseHeaders()) { + if (header.startsWith("Connection:")) { + connectionHeaderValue = header.substring(header.indexOf(':') + 1).trim(); + } + if (header.startsWith("Keep-Alive:")) { + keepAliveHeaderValue = header.substring(header.indexOf(':') + 1).trim(); + } + } + + if (explicitClose) { + Assert.assertEquals("close", connectionHeaderValue); + Assert.assertNull(keepAliveHeaderValue); + } else if (!sendKeepAlive || keepAliveTimeout < 0 && (maxKeepAliveRequests < 0 || maxKeepAliveRequests > 1)) { + Assert.assertNull(connectionHeaderValue); + Assert.assertNull(keepAliveHeaderValue); + } else { + List connectionHeaders = new ArrayList<>(); + TokenList.parseTokenList(new StringReader(connectionHeaderValue), connectionHeaders); + + if (sendKeepAlive && keepAliveTimeout > 0 && (maxKeepAliveRequests < 0 || maxKeepAliveRequests > 1)) { + Assert.assertEquals(1, connectionHeaders.size()); + Assert.assertEquals("keep-alive", connectionHeaders.get(0)); + Assert.assertEquals("timeout=" + keepAliveTimeout / 1000L, keepAliveHeaderValue); + } + + if (sendKeepAlive && maxKeepAliveRequests == 1) { + Assert.assertEquals(1, connectionHeaders.size()); + Assert.assertEquals("close", connectionHeaders.get(0)); + Assert.assertNull(keepAliveHeaderValue); + } + } + } + + + /** + * Test servlet that prints out the values of HttpServletRequest.getServerName() and + * HttpServletRequest.getServerPort() in the response body, e.g.: "request.getServerName() is [foo] and + * request.getServerPort() is 8080" or: "request.getServerName() is null and request.getServerPort() is 8080" + */ + private static class ServerNameTesterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + resp.setContentType("text/plain"); + PrintWriter out = resp.getWriter(); + + if (null == req.getServerName()) { + out.print("request.getServerName() is null"); + } else { + out.print("request.getServerName() is [" + req.getServerName() + "]"); + } + + out.print(" and request.getServerPort() is " + req.getServerPort()); + } + } + + + @Test + public void testSlowUploadTimeoutWithLongerUploadTimeout() throws Exception { + doTestSlowUploadTimeout(true); + } + + + @Test + public void testSlowUploadTimeoutWithoutLongerUploadTimeout() throws Exception { + doTestSlowUploadTimeout(false); + } + + + private void doTestSlowUploadTimeout(boolean useLongerUploadTimeout) throws Exception { + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + + int connectionTimeout = ((Integer) connector.getProperty("connectionTimeout")).intValue(); + + // These factors should make the differences large enough that the CI + // tests pass consistently. If not, may need to reduce connectionTimeout + // and increase delay and connectionUploadTimeout + int delay = connectionTimeout * 2; + int connectionUploadTimeout = connectionTimeout * 4; + + if (useLongerUploadTimeout) { + connector.setProperty("connectionUploadTimeout", "" + connectionUploadTimeout); + connector.setProperty("disableUploadTimeout", "false"); + } + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new SwallowBodyTesterServlet()); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "POST /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + + SimpleHttpClient.CRLF + "Content-Length: 10" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request, "XXXXXXXXXX" }); + client.setRequestPause(delay); + + client.connect(); + try { + client.processRequest(); + } catch (IOException ioe) { + // Failure is expected on some platforms (notably Windows) if the + // longer upload timeout is not used but record the exception in + // case it is useful for debugging purposes. + // The assertions below will check for the correct behaviour. + ioe.printStackTrace(); + } + + if (useLongerUploadTimeout) { + // Expected response is a 200 response. + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("OK", client.getResponseBody()); + } else { + // Different failure modes with different connectors + Assert.assertFalse(client.isResponse200()); + } + } + + + private static class SwallowBodyTesterServlet extends TesterServlet { + + private static final long serialVersionUID = 1L; + + SwallowBodyTesterServlet() { + super(true); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + // Swallow the body + byte[] buf = new byte[1024]; + InputStream is = req.getInputStream(); + while (is.read(buf) > 0) { + // Loop + } + + // Standard response + doGet(req, resp); + } + } + + + private static class Bug64974Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + // Get requests can have bodies although these requests don't. + // Needs to be async to trigger the problematic code path + AsyncContext ac = req.startAsync(); + ServletInputStream sis = req.getInputStream(); + // This triggers a call to Http11InputBuffer.available(true) which + // did not handle the pipelining case. + sis.setReadListener(new Bug64974ReadListener()); + ac.complete(); + + resp.setContentType("text/plain"); + PrintWriter out = resp.getWriter(); + out.print("OK"); + } + } + + + private static class Bug64974ReadListener implements ReadListener { + + @Override + public void onDataAvailable() throws IOException { + // NO-OP + } + + @Override + public void onAllDataRead() throws IOException { + // NO-OP + } + + @Override + public void onError(Throwable throwable) { + // NO-OP + } + } + + + @Test + public void testTEHeaderUnknown01() throws Exception { + doTestTEHeaderInvalid("identity", false); + } + + + @Test + public void testTEHeaderUnknown02() throws Exception { + doTestTEHeaderInvalid("identity, chunked", false); + } + + + @Test + public void testTEHeaderUnknown03() throws Exception { + doTestTEHeaderInvalid("unknown, chunked", false); + } + + + @Test + public void testTEHeaderUnknown04() throws Exception { + doTestTEHeaderInvalid("void", false); + } + + + @Test + public void testTEHeaderUnknown05() throws Exception { + doTestTEHeaderInvalid("void, chunked", false); + } + + + @Test + public void testTEHeaderUnknown06() throws Exception { + doTestTEHeaderInvalid("void, identity", false); + } + + + @Test + public void testTEHeaderUnknown07() throws Exception { + doTestTEHeaderInvalid("identity, void", false); + } + + + @Test + public void testTEHeaderChunkedNotLast01() throws Exception { + doTestTEHeaderInvalid("chunked, void", true); + } + + + private void doTestTEHeaderInvalid(String headerValue, boolean badRequest) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet(false)); + ctx.addServletMappingDecoded("/foo", "TesterServlet"); + + tomcat.start(); + + String request = "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + + SimpleHttpClient.CRLF + "Transfer-Encoding: " + headerValue + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(false); + + if (badRequest) { + Assert.assertTrue(client.isResponse400()); + } else { + Assert.assertTrue(client.isResponse501()); + } + } + + + @Test + public void testWithTEChunkedHttp10() throws Exception { + + getTomcatInstanceTestWebapp(false, true); + + String request = "POST /test/echo-params.jsp HTTP/1.0" + SimpleHttpClient.CRLF + "Host: any" + + SimpleHttpClient.CRLF + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + SimpleHttpClient.CRLF + "Connection: close" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF + "9" + SimpleHttpClient.CRLF + "test=data" + + SimpleHttpClient.CRLF + "0" + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(getPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.getResponseBody().contains("test - data")); + } + + + @Test + public void test100ContinueWithNoAck() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + final Connector connector = tomcat.getConnector(); + connector.setProperty("continueResponseTiming", "onRead"); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "TestPostNoReadServlet", new TestPostNoReadServlet()); + ctx.addServletMappingDecoded("/foo", "TestPostNoReadServlet"); + + tomcat.start(); + + String request = "POST /foo HTTP/1.1" + SimpleHttpClient.CRLF + "Host: localhost:" + getPort() + + SimpleHttpClient.CRLF + "Expect: 100-continue" + SimpleHttpClient.CRLF + "Content-Length: 10" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF + "0123456789"; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(false); + + Assert.assertTrue(client.isResponse200()); + + if (client.getResponseHeaders().contains("Connection: close")) { + client.connect(); + } + + client.processRequest(false); + + Assert.assertTrue(client.isResponse200()); + + } + + + @Test + public void testConnect() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + String request = "CONNECT example.local HTTP/1.1" + SimpleHttpClient.CRLF + "Host: example.local" + + SimpleHttpClient.CRLF + SimpleHttpClient.CRLF; + + Client client = new Client(getPort()); + client.setRequest(new String[] { request }); + + client.connect(); + client.processRequest(); + Assert.assertTrue(client.isResponse501()); + } + + + private static class TestPostNoReadServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + } + } +} diff --git a/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java b/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java new file mode 100644 index 0000000..dae08a8 --- /dev/null +++ b/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java @@ -0,0 +1,624 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestChunkedInputFilter extends TomcatBaseTest { + + private static final String LF = "\n"; + private static final int EXT_SIZE_LIMIT = 10; + + @Test + public void testChunkHeaderCRLF() throws Exception { + doTestChunkingCRLF(true, true, true, true, true, true); + } + + @Test + public void testChunkHeaderLF() throws Exception { + doTestChunkingCRLF(false, true, true, true, true, false); + } + + @Test + public void testChunkCRLF() throws Exception { + doTestChunkingCRLF(true, true, true, true, true, true); + } + + @Test + public void testChunkLF() throws Exception { + doTestChunkingCRLF(true, false, true, true, true, false); + } + + @Test + public void testFirstTrailingHeadersCRLF() throws Exception { + doTestChunkingCRLF(true, true, true, true, true, true); + } + + @Test + public void testFirstTrailingHeadersLF() throws Exception { + doTestChunkingCRLF(true, true, false, true, true, true); + } + + @Test + public void testSecondTrailingHeadersCRLF() throws Exception { + doTestChunkingCRLF(true, true, true, true, true, true); + } + + @Test + public void testSecondTrailingHeadersLF() throws Exception { + doTestChunkingCRLF(true, true, true, false, true, true); + } + + @Test + public void testEndCRLF() throws Exception { + doTestChunkingCRLF(true, true, true, true, true, true); + } + + @Test + public void testEndLF() throws Exception { + doTestChunkingCRLF(true, true, true, true, false, false); + } + + private void doTestChunkingCRLF(boolean chunkHeaderUsesCRLF, + boolean chunkUsesCRLF, boolean firstheaderUsesCRLF, + boolean secondheaderUsesCRLF, boolean endUsesCRLF, + boolean expectPass) throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Configure allowed trailer headers + Assert.assertTrue(tomcat.getConnector().setProperty("allowedTrailerHeaders", "x-trailer1,x-trailer2")); + + EchoHeaderServlet servlet = new EchoHeaderServlet(expectPass); + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + String[] request = new String[]{ + "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: any" + SimpleHttpClient.CRLF + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + + SimpleHttpClient.CRLF + + "Connection: close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "3" + (chunkHeaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + + "a=0" + (chunkUsesCRLF ? SimpleHttpClient.CRLF : LF) + + "4" + SimpleHttpClient.CRLF + + "&b=1" + SimpleHttpClient.CRLF + + "0" + SimpleHttpClient.CRLF + + "x-trailer1: Test", "Value1" + + (firstheaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + + "x-trailer2: TestValue2" + + (secondheaderUsesCRLF ? SimpleHttpClient.CRLF : LF) + + (endUsesCRLF ? SimpleHttpClient.CRLF : LF) }; + + TrailerClient client = + new TrailerClient(tomcat.getConnector().getLocalPort()); + client.setRequest(request); + + client.connect(); + Exception processException = null; + try { + client.processRequest(); + } catch (Exception e) { + // Socket was probably closed before client had a chance to read + // response + processException = e; + } + + if (expectPass) { + Assert.assertTrue(client.isResponse200()); + Assert.assertEquals("nullnull7TestValue1TestValue2", + client.getResponseBody()); + Assert.assertNull(processException); + Assert.assertFalse(servlet.getExceptionDuringRead()); + } else { + if (processException == null) { + Assert.assertTrue(client.getResponseLine(), client.isResponse500()); + } else { + // Use fall-back for checking the error occurred + Assert.assertTrue(servlet.getExceptionDuringRead()); + } + } + } + + @Test + public void testTrailingHeadersSizeLimit() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(false)); + ctx.addServletMappingDecoded("/", "servlet"); + + // Limit the size of the trailing header + Assert.assertTrue(tomcat.getConnector().setProperty("maxTrailerSize", "10")); + tomcat.start(); + + String[] request = new String[]{ + "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: any" + SimpleHttpClient.CRLF + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + + SimpleHttpClient.CRLF + + "Connection: close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "3" + SimpleHttpClient.CRLF + + "a=0" + SimpleHttpClient.CRLF + + "4" + SimpleHttpClient.CRLF + + "&b=1" + SimpleHttpClient.CRLF + + "0" + SimpleHttpClient.CRLF + + "x-trailer: Test" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }; + + TrailerClient client = + new TrailerClient(tomcat.getConnector().getLocalPort()); + client.setRequest(request); + + client.connect(); + client.processRequest(); + // Expected to fail because the trailers are longer + // than the set limit of 10 bytes + Assert.assertTrue(client.isResponse500()); + } + + + @Test + public void testExtensionSizeLimitOneBelow() throws Exception { + doTestExtensionSizeLimit(EXT_SIZE_LIMIT - 1, true); + } + + + @Test + public void testExtensionSizeLimitExact() throws Exception { + doTestExtensionSizeLimit(EXT_SIZE_LIMIT, true); + } + + + @Test + public void testExtensionSizeLimitOneOver() throws Exception { + doTestExtensionSizeLimit(EXT_SIZE_LIMIT + 1, false); + } + + + private void doTestExtensionSizeLimit(int len, boolean ok) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + Assert.assertTrue(tomcat.getConnector().setProperty( + "maxExtensionSize", Integer.toString(EXT_SIZE_LIMIT))); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(ok)); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + String extName = ";foo="; + StringBuilder extValue = new StringBuilder(len); + for (int i = 0; i < (len - extName.length()); i++) { + extValue.append('x'); + } + + String[] request = new String[]{ + "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: any" + SimpleHttpClient.CRLF + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + + SimpleHttpClient.CRLF + + "Connection: close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "3" + extName + extValue.toString() + SimpleHttpClient.CRLF + + "a=0" + SimpleHttpClient.CRLF + + "4" + SimpleHttpClient.CRLF + + "&b=1" + SimpleHttpClient.CRLF + + "0" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }; + + TrailerClient client = + new TrailerClient(tomcat.getConnector().getLocalPort()); + client.setRequest(request); + + client.connect(); + client.processRequest(); + + if (ok) { + Assert.assertTrue(client.isResponse200()); + } else { + Assert.assertTrue(client.isResponse500()); + } + } + + @Test + public void testNoTrailingHeaders() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(true)); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + String request = + "POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: any" + SimpleHttpClient.CRLF + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + + SimpleHttpClient.CRLF + + "Connection: close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "3" + SimpleHttpClient.CRLF + + "a=0" + SimpleHttpClient.CRLF + + "4" + SimpleHttpClient.CRLF + + "&b=1" + SimpleHttpClient.CRLF + + "0" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + TrailerClient client = + new TrailerClient(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] {request}); + + client.connect(); + client.processRequest(); + Assert.assertEquals("nullnull7nullnull", client.getResponseBody()); + } + + @Test + public void testChunkSizeZero() throws Exception { + doTestChunkSize(true, true, "", 10, 0); + } + + @Test + public void testChunkSizeAbsent() throws Exception { + doTestChunkSize(false, false, SimpleHttpClient.CRLF, 10, 0); + } + + @Test + public void testChunkSizeTwentyFive() throws Exception { + doTestChunkSize(true, true, "19" + SimpleHttpClient.CRLF + + "Hello World!Hello World!!" + SimpleHttpClient.CRLF, 40, 25); + } + + @Test + public void testChunkSizeEightDigit() throws Exception { + doTestChunkSize(true, true, "0000000C" + SimpleHttpClient.CRLF + + "Hello World!" + SimpleHttpClient.CRLF, 20, 12); + } + + @Test + public void testChunkSizeNineDigit() throws Exception { + doTestChunkSize(false, false, "00000000C" + SimpleHttpClient.CRLF + + "Hello World!" + SimpleHttpClient.CRLF, 20, 12); + } + + @Test + public void testChunkSizeLong() throws Exception { + doTestChunkSize(true, false, "7fFFffFF" + SimpleHttpClient.CRLF + + "Hello World!" + SimpleHttpClient.CRLF, 10, 10); + } + + @Test + public void testChunkSizeIntegerMinValue() throws Exception { + doTestChunkSize(false, false, "80000000" + SimpleHttpClient.CRLF + + "Hello World!" + SimpleHttpClient.CRLF, 10, 10); + } + + @Test + public void testChunkSizeMinusOne() throws Exception { + doTestChunkSize(false, false, "ffffffff" + SimpleHttpClient.CRLF + + "Hello World!" + SimpleHttpClient.CRLF, 10, 10); + } + + /** + * @param expectPass + * If the servlet is expected to process the request + * @param expectReadWholeBody + * If the servlet is expected to fully read the body and reliably + * deliver a response + * @param chunks + * Text of chunks + * @param readLimit + * Do not read more than this many bytes + * @param expectReadCount + * Expected count of read bytes + * @throws Exception + * Unexpected + */ + private void doTestChunkSize(boolean expectPass, + boolean expectReadWholeBody, String chunks, int readLimit, + int expectReadCount) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + BodyReadServlet servlet = new BodyReadServlet(expectPass, readLimit); + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + String request = "POST /echo-params.jsp HTTP/1.1" + + SimpleHttpClient.CRLF + "Host: any" + SimpleHttpClient.CRLF + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: text/plain" + SimpleHttpClient.CRLF; + if (expectPass) { + request += "Connection: close" + SimpleHttpClient.CRLF; + } + request += SimpleHttpClient.CRLF + chunks + "0" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + TrailerClient client = new TrailerClient(tomcat.getConnector().getLocalPort()); + // Need to use the content length here as variations in Connector and + // JVM+OS behaviour mean that in some circumstances the client may see + // an IOException rather than the response body when the server closes + // the connection. + client.setUseContentLength(true); + client.setRequest(new String[] { request }); + + Exception processException = null; + client.connect(); + try { + client.processRequest(); + client.disconnect(); + } catch (Exception e) { + // Socket was probably closed before client had a chance to read + // response + processException = e; + } + if (expectPass) { + if (expectReadWholeBody) { + Assert.assertNull(processException); + } + if (processException == null) { + Assert.assertTrue(client.getResponseLine(), client.isResponse200()); + Assert.assertEquals(String.valueOf(expectReadCount), + client.getResponseBody()); + } + Assert.assertEquals(expectReadCount, servlet.getCountRead()); + } else { + if (processException == null) { + Assert.assertTrue(client.getResponseLine(), client.isResponse500()); + } + Assert.assertEquals(0, servlet.getCountRead()); + Assert.assertTrue(servlet.getExceptionDuringRead()); + } + } + + + @Test + public void testTrailerHeaderNameNotTokenThrowException() throws Exception { + doTestTrailerHeaderNameNotToken(false); + } + + @Test + public void testTrailerHeaderNameNotTokenSwallowException() throws Exception { + doTestTrailerHeaderNameNotToken(true); + } + + private void doTestTrailerHeaderNameNotToken(boolean swallowException) throws Exception { + + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new SwallowBodyServlet(swallowException)); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + String[] request = new String[]{ + "POST / HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost" + SimpleHttpClient.CRLF + + "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + + "Content-Type: application/x-www-form-urlencoded" + SimpleHttpClient.CRLF + + "Connection: close" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF + + "3" + SimpleHttpClient.CRLF + + "a=0" + SimpleHttpClient.CRLF + + "4" + SimpleHttpClient.CRLF + + "&b=1" + SimpleHttpClient.CRLF + + "0" + SimpleHttpClient.CRLF + + "x@trailer: Test" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF }; + + TrailerClient client = new TrailerClient(tomcat.getConnector().getLocalPort()); + client.setRequest(request); + + client.connect(); + client.processRequest(); + // Expected to fail because of invalid trailer header name + Assert.assertTrue(client.getResponseLine(), client.isResponse400()); + } + + private static class SwallowBodyServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + private final boolean swallowException; + + SwallowBodyServlet(boolean swallowException) { + this.swallowException = swallowException; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + + // Read the body + InputStream is = req.getInputStream(); + try { + while (is.read() > -1) { + } + pw.write("OK"); + } catch (IOException ioe) { + if (!swallowException) { + throw ioe; + } + } + } + } + + private static class EchoHeaderServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + private boolean exceptionDuringRead = false; + + private final boolean expectPass; + + EchoHeaderServlet(boolean expectPass) { + this.expectPass = expectPass; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + // Headers not visible yet, body not processed + dumpHeader("x-trailer1", req, pw); + dumpHeader("x-trailer2", req, pw); + + // Read the body - quick and dirty + InputStream is = req.getInputStream(); + int count = 0; + try { + while (is.read() > -1) { + count++; + } + } catch (IOException ioe) { + exceptionDuringRead = true; + if (!expectPass) { // as expected + log(ioe.toString()); + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + throw ioe; + } + + pw.write(Integer.toString(count)); + + // Headers should be visible now + dumpHeader("x-trailer1", req, pw); + dumpHeader("x-trailer2", req, pw); + } + + public boolean getExceptionDuringRead() { + return exceptionDuringRead; + } + + private void dumpHeader(String headerName, HttpServletRequest req, + PrintWriter pw) { + String value = req.getTrailerFields().get(headerName); + if (value == null) { + value = "null"; + } + pw.write(value); + } + } + + private static class BodyReadServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + private boolean exceptionDuringRead = false; + private int countRead = 0; + private final boolean expectPass; + private final int readLimit; + + BodyReadServlet(boolean expectPass, int readLimit) { + this.expectPass = expectPass; + this.readLimit = readLimit; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + PrintWriter pw = resp.getWriter(); + + // Read the body - quick and dirty + InputStream is = req.getInputStream(); + try { + while (is.read() > -1 && countRead < readLimit) { + countRead++; + } + } catch (IOException ioe) { + exceptionDuringRead = true; + if (!expectPass) { // as expected + log(ioe.toString()); + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + throw ioe; + } + + pw.write(Integer.toString(countRead)); + } + + public boolean getExceptionDuringRead() { + return exceptionDuringRead; + } + + public int getCountRead() { + return countRead; + } + } + + private static class TrailerClient extends SimpleHttpClient { + + TrailerClient(int port) { + setPort(port); + } + + @Override + public boolean isResponseBodyOK() { + return getResponseBody().contains("TestTestTest"); + } + } +} diff --git a/test/org/apache/coyote/http11/filters/TestGzipOutputFilter.java b/test/org/apache/coyote/http11/filters/TestGzipOutputFilter.java new file mode 100644 index 0000000..7873b0e --- /dev/null +++ b/test/org/apache/coyote/http11/filters/TestGzipOutputFilter.java @@ -0,0 +1,84 @@ +/* + * 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.coyote.http11.filters; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.zip.GZIPOutputStream; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.coyote.Response; + +/** + * Test case to demonstrate the interaction between gzip and flushing in the + * output filter. + */ +public class TestGzipOutputFilter { + + /* + * Test the interaction between gzip and flushing. The idea is to: 1. create + * an internal output buffer, response, and attach an active gzipoutputfilter + * to the output buffer 2. set the output stream of the internal buffer to + * be a ByteArrayOutputStream so we can inspect the output bytes 3. write a + * chunk out using the gzipoutputfilter and invoke a flush on the + * InternalOutputBuffer 4. read from the ByteArrayOutputStream to find out + * what's being written out (flushed) 5. find out what's expected by writing + * to GZIPOutputStream and close it (to force flushing) 6. Compare the size + * of the two arrays, they should be close (instead of one being much + * shorter than the other one) + * + * @throws Exception + */ + @Test + public void testFlushingWithGzip() throws Exception { + // set up response, InternalOutputBuffer, and ByteArrayOutputStream + Response res = new Response(); + TesterOutputBuffer tob = new TesterOutputBuffer(res, 8 * 1024); + res.setOutputBuffer(tob); + + // set up GzipOutputFilter to attach to the TesterOutputBuffer + GzipOutputFilter gf = new GzipOutputFilter(); + tob.addFilter(gf); + tob.addActiveFilter(gf); + + // write a chunk out + byte[] d = "Hello there tomcat developers, there is a bug in JDK".getBytes(); + tob.doWrite(ByteBuffer.wrap(d)); + + // flush the InternalOutputBuffer + tob.flush(); + + // read from the ByteArrayOutputStream to find out what's being written + // out (flushed) + byte[] dataFound = tob.toByteArray(); + + // find out what's expected by writing to GZIPOutputStream and close it + // (to force flushing) + ByteArrayOutputStream gbos = new ByteArrayOutputStream(1024); + GZIPOutputStream gos = new GZIPOutputStream(gbos); + gos.write(d); + gos.close(); + + // read the expected data + byte[] dataExpected = gbos.toByteArray(); + + // most of the data should have been flushed out + Assert.assertTrue(dataFound.length >= (dataExpected.length - 20)); + } +} diff --git a/test/org/apache/coyote/http11/filters/TesterOutputBuffer.java b/test/org/apache/coyote/http11/filters/TesterOutputBuffer.java new file mode 100644 index 0000000..00176e2 --- /dev/null +++ b/test/org/apache/coyote/http11/filters/TesterOutputBuffer.java @@ -0,0 +1,128 @@ +/* + * 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.coyote.http11.filters; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.coyote.Response; +import org.apache.coyote.http11.Http11OutputBuffer; +import org.apache.coyote.http11.HttpOutputBuffer; +import org.apache.tomcat.util.net.SocketWrapperBase; + +/** + * Output buffer for use in unit tests. This is a minimal implementation. + */ +public class TesterOutputBuffer extends Http11OutputBuffer { + + /** + * Underlying output stream. + */ + private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + + public TesterOutputBuffer(Response response, int headerBufferSize) { + super(response, headerBufferSize); + outputStreamOutputBuffer = new OutputStreamOutputBuffer(); + } + + + // --------------------------------------------------------- Public Methods + + @Override + public void init(SocketWrapperBase socketWrapper) { + // NO-OP: Unused + } + + + /** + * Recycle the output buffer. This should be called when closing the + * connection. + */ + @Override + public void recycle() { + super.recycle(); + outputStream = null; + } + + + // ------------------------------------------------ HTTP/1.1 Output Methods + + /** + * Send an acknowledgement. + */ + @Override + public void sendAck() { + // NO-OP: Unused + } + + + @Override + protected void commit() { + // NO-OP: Unused + } + + + @Override + protected boolean flushBuffer(boolean block) throws IOException { + // Blocking IO so ignore block parameter as this will always use + // blocking IO. + // Always blocks so never any data left over. + return false; + } + + + /* + * Expose data written for use by unit tests. + */ + byte[] toByteArray() { + return outputStream.toByteArray(); + } + + + /** + * This class is an output buffer which will write data to an output + * stream. + */ + protected class OutputStreamOutputBuffer implements HttpOutputBuffer { + + @Override + public int doWrite(ByteBuffer chunk) throws IOException { + int length = chunk.remaining(); + outputStream.write(chunk.array(), chunk.arrayOffset() + chunk.position(), length); + byteCount += length; + return length; + } + + @Override + public long getBytesWritten() { + return byteCount; + } + + + @Override + public void flush() throws IOException { + // NO-OP: Unused + } + + @Override + public void end() throws IOException { + // NO-OP: Unused + } + } +} diff --git a/test/org/apache/coyote/http11/upgrade/TestUpgrade.java b/test/org/apache/coyote/http11/upgrade/TestUpgrade.java new file mode 100644 index 0000000..fb0c57c --- /dev/null +++ b/test/org/apache/coyote/http11/upgrade/TestUpgrade.java @@ -0,0 +1,531 @@ +/* + * 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.coyote.http11.upgrade; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.net.Socket; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; + +import javax.net.SocketFactory; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.WebConnection; + +import org.junit.Assert; +import org.junit.Test; + +import static org.apache.catalina.startup.SimpleHttpClient.CRLF; +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestUpgrade extends TomcatBaseTest { + + private static final String MESSAGE = "This is a test."; + + @Test + public void testSimpleUpgradeBlocking() throws Exception { + UpgradeConnection uc = doUpgrade(EchoBlocking.class); + uc.shutdownInput(); + uc.shutdownOutput(); + } + + @Test + public void testSimpleUpgradeNonBlocking() throws Exception { + UpgradeConnection uc = doUpgrade(EchoNonBlocking.class); + uc.shutdownInput(); + uc.shutdownOutput(); + } + + @Test + public void testMessagesBlocking() throws Exception { + doTestMessages(EchoBlocking.class); + } + + @Test + public void testMessagesNonBlocking() throws Exception { + doTestMessages(EchoNonBlocking.class); + } + + @Test + public void testSetNullReadListener() throws Exception { + doTestCheckClosed(SetNullReadListener.class); + } + + @Test + public void testSetNullWriteListener() throws Exception { + doTestCheckClosed(SetNullWriteListener.class); + } + + @Test + public void testSetReadListenerTwice() throws Exception { + doTestCheckClosed(SetReadListenerTwice.class); + } + + @Test + public void testSetWriteListenerTwice() throws Exception { + doTestCheckClosed(SetWriteListenerTwice.class); + } + + @Test + public void testFirstCallToOnWritePossible() throws Exception { + doTestFixedResponse(FixedResponseNonBlocking.class); + } + + private void doTestCheckClosed( + Class upgradeHandlerClass) + throws Exception { + UpgradeConnection conn = doUpgrade(upgradeHandlerClass); + + Reader r = conn.getReader(); + int c; + try { + c = r.read(); + } catch (SocketException se) { + // Some platforms will throw an exception rather than returning -1 + c = -1; + } + + Assert.assertEquals(-1, c); + } + + private void doTestFixedResponse( + Class upgradeHandlerClass) + throws Exception { + UpgradeConnection conn = doUpgrade(upgradeHandlerClass); + + Reader r = conn.getReader(); + int c = r.read(); + + Assert.assertEquals(FixedResponseNonBlocking.FIXED_RESPONSE, c); + } + + private void doTestMessages ( + Class upgradeHandlerClass) + throws Exception { + UpgradeConnection uc = doUpgrade(upgradeHandlerClass); + PrintWriter pw = new PrintWriter(uc.getWriter()); + BufferedReader reader = uc.getReader(); + + pw.println(MESSAGE); + pw.flush(); + + Thread.sleep(500); + + pw.println(MESSAGE); + pw.flush(); + + uc.shutdownOutput(); + + // Note: BufferedReader.readLine() strips new lines + // ServletInputStream.readLine() does not strip new lines + String response = reader.readLine(); + Assert.assertEquals(MESSAGE, response); + response = reader.readLine(); + Assert.assertEquals(MESSAGE, response); + + uc.shutdownInput(); + pw.close(); + } + + + private UpgradeConnection doUpgrade( + Class upgradeHandlerClass) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + UpgradeServlet servlet = new UpgradeServlet(upgradeHandlerClass); + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + // Use raw socket so the necessary control is available after the HTTP + // upgrade + Socket socket = + SocketFactory.getDefault().createSocket("localhost", getPort()); + + socket.setSoTimeout(5000); + + UpgradeConnection uc = new UpgradeConnection(socket); + + uc.getWriter().write("GET / HTTP/1.1" + CRLF); + uc.getWriter().write("Host: whatever" + CRLF); + uc.getWriter().write("Upgrade: test" + CRLF); + uc.getWriter().write(CRLF); + uc.getWriter().flush(); + + String status = uc.getReader().readLine(); + + Assert.assertNotNull(status); + Assert.assertEquals("101", getStatusCode(status)); + + // Skip the remaining response headers + String line = uc.getReader().readLine(); + while (line != null && line.length() > 0) { + // Skip + line = uc.getReader().readLine(); + } + + return uc; + } + + private static class UpgradeServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final Class upgradeHandlerClass; + + UpgradeServlet(Class upgradeHandlerClass) { + this.upgradeHandlerClass = upgradeHandlerClass; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + // In these tests only a single protocol is requested so it is safe + // to echo it to the response. + resp.setHeader("upgrade", req.getHeader("upgrade")); + req.upgrade(upgradeHandlerClass); + } + } + + private static class UpgradeConnection { + private final Socket socket; + private final Writer writer; + private final BufferedReader reader; + + UpgradeConnection(Socket socket) { + this.socket = socket; + InputStream is; + OutputStream os; + try { + is = socket.getInputStream(); + os = socket.getOutputStream(); + } catch (IOException ioe) { + throw new IllegalArgumentException(ioe); + } + + BufferedReader reader = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8); + + this.writer = writer; + this.reader = reader; + } + + public Writer getWriter() { + return writer; + } + + public BufferedReader getReader() { + return reader; + } + + public void shutdownOutput() throws IOException { + writer.flush(); + socket.shutdownOutput(); + } + + public void shutdownInput() throws IOException { + socket.shutdownInput(); + } + } + + + public static class EchoBlocking implements HttpUpgradeHandler { + @Override + public void init(WebConnection connection) { + + try (ServletInputStream sis = connection.getInputStream(); + ServletOutputStream sos = connection.getOutputStream()){ + byte[] buffer = new byte[8192]; + int read; + while ((read = sis.read(buffer)) >= 0) { + sos.write(buffer, 0, read); + sos.flush(); + } + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + @Override + public void destroy() { + // NO-OP + } + } + + + public static class EchoNonBlocking implements HttpUpgradeHandler { + + @Override + public void init(WebConnection connection) { + ServletInputStream sis; + ServletOutputStream sos; + + try { + sis = connection.getInputStream(); + sos = connection.getOutputStream(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + + EchoListener echoListener = new EchoListener(sis, sos); + sis.setReadListener(echoListener); + sos.setWriteListener(echoListener); + } + + @Override + public void destroy() { + // NO-OP + } + + + private static class EchoListener implements ReadListener, WriteListener { + + private final ServletInputStream sis; + private final ServletOutputStream sos; + private final byte[] buffer = new byte[8192]; + + EchoListener(ServletInputStream sis, ServletOutputStream sos) { + this.sis = sis; + this.sos = sos; + } + + @Override + public void onWritePossible() throws IOException { + if (sis.isFinished()) { + sis.close(); + sos.close(); + } + while (sis.isReady()) { + int read = sis.read(buffer); + if (read > 0) { + sos.write(buffer, 0, read); + if (!sos.isReady()) { + break; + } + } + } + } + + @Override + public void onDataAvailable() throws IOException { + if (sos.isReady()) { + onWritePossible(); + } + } + + @Override + public void onAllDataRead() throws IOException { + if (sos.isReady()) { + onWritePossible(); + } + } + + @Override + public void onError(Throwable throwable) { + throwable.printStackTrace(); + } + } + } + + + public static class SetNullReadListener implements HttpUpgradeHandler { + + @Override + public void init(WebConnection connection) { + ServletInputStream sis; + try { + sis = connection.getInputStream(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + sis.setReadListener(null); + } + + @Override + public void destroy() { + // NO-OP + } + } + + + public static class SetNullWriteListener implements HttpUpgradeHandler { + + @Override + public void init(WebConnection connection) { + ServletOutputStream sos; + try { + sos = connection.getOutputStream(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + sos.setWriteListener(null); + } + + @Override + public void destroy() { + // NO-OP + } + } + + + public static class SetReadListenerTwice implements HttpUpgradeHandler { + + @Override + public void init(WebConnection connection) { + ServletInputStream sis; + ServletOutputStream sos; + try { + sis = connection.getInputStream(); + sos = connection.getOutputStream(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + sos.setWriteListener(new NoOpWriteListener()); + ReadListener rl = new NoOpReadListener(); + sis.setReadListener(rl); + sis.setReadListener(rl); + } + + @Override + public void destroy() { + // NO-OP + } + } + + + public static class SetWriteListenerTwice implements HttpUpgradeHandler { + + @Override + public void init(WebConnection connection) { + ServletInputStream sis; + ServletOutputStream sos; + try { + sis = connection.getInputStream(); + sos = connection.getOutputStream(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + sis.setReadListener(new NoOpReadListener()); + WriteListener wl = new NoOpWriteListener(); + sos.setWriteListener(wl); + sos.setWriteListener(wl); + } + + @Override + public void destroy() { + // NO-OP + } + } + + + public static class FixedResponseNonBlocking implements HttpUpgradeHandler { + + public static final char FIXED_RESPONSE = 'F'; + + private ServletInputStream sis; + private ServletOutputStream sos; + + @Override + public void init(WebConnection connection) { + + try { + sis = connection.getInputStream(); + sos = connection.getOutputStream(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + + sis.setReadListener(new NoOpReadListener()); + sos.setWriteListener(new FixedResponseWriteListener()); + } + + @Override + public void destroy() { + // NO-OP + } + + private class FixedResponseWriteListener extends NoOpWriteListener { + @Override + public void onWritePossible() { + try { + sos.write(FIXED_RESPONSE); + sos.flush(); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + } + } + + + private static class NoOpReadListener implements ReadListener { + + @Override + public void onDataAvailable() { + // NO-OP + } + + @Override + public void onAllDataRead() { + // Always NO-OP for HTTP Upgrade + } + + @Override + public void onError(Throwable throwable) { + // NO-OP + } + } + + + private static class NoOpWriteListener implements WriteListener { + + @Override + public void onWritePossible() { + // NO-OP + } + + @Override + public void onError(Throwable throwable) { + // NO-OP + } + } +} diff --git a/test/org/apache/coyote/http11/upgrade/TestUpgradeInternalHandler.java b/test/org/apache/coyote/http11/upgrade/TestUpgradeInternalHandler.java new file mode 100644 index 0000000..fbb1b2c --- /dev/null +++ b/test/org/apache/coyote/http11/upgrade/TestUpgradeInternalHandler.java @@ -0,0 +1,284 @@ +/* + * 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.coyote.http11.upgrade; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Writer; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.CompletionHandler; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +import javax.net.SocketFactory; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.WebConnection; + +import org.junit.Assert; +import org.junit.Test; + +import static org.apache.catalina.startup.SimpleHttpClient.CRLF; +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; +import org.apache.tomcat.util.net.SSLSupport; +import org.apache.tomcat.util.net.SocketEvent; +import org.apache.tomcat.util.net.SocketWrapperBase; +import org.apache.tomcat.util.net.SocketWrapperBase.BlockingMode; +import org.apache.tomcat.util.net.SocketWrapperBase.CompletionState; + +public class TestUpgradeInternalHandler extends TomcatBaseTest { + + private static final String MESSAGE = "This is a test."; + + @Test + public void testUpgradeInternal() throws Exception { + UpgradeConnection uc = doUpgrade(EchoAsync.class); + PrintWriter pw = new PrintWriter(uc.getWriter()); + BufferedReader reader = uc.getReader(); + + // Add extra sleep to avoid completing inline + Thread.sleep(500); + pw.println(MESSAGE); + pw.flush(); + Thread.sleep(500); + uc.shutdownOutput(); + + // Note: BufferedReader.readLine() strips new lines + // ServletInputStream.readLine() does not strip new lines + String response = reader.readLine(); + Assert.assertEquals(MESSAGE, response); + + uc.shutdownInput(); + pw.close(); + } + + private UpgradeConnection doUpgrade( + Class upgradeHandlerClass) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + Assert.assertTrue(tomcat.getConnector().setProperty("useAsyncIO", "true")); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + UpgradeServlet servlet = new UpgradeServlet(upgradeHandlerClass); + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + // Use raw socket so the necessary control is available after the HTTP + // upgrade + Socket socket = + SocketFactory.getDefault().createSocket("localhost", getPort()); + + socket.setSoTimeout(5000); + + UpgradeConnection uc = new UpgradeConnection(socket); + + uc.getWriter().write("GET / HTTP/1.1" + CRLF); + uc.getWriter().write("Host: whatever" + CRLF); + uc.getWriter().write(CRLF); + uc.getWriter().flush(); + + String status = uc.getReader().readLine(); + + Assert.assertNotNull(status); + Assert.assertEquals("101", getStatusCode(status)); + + // Skip the remaining response headers + String line = uc.getReader().readLine(); + while (line != null && line.length() > 0) { + // Skip + line = uc.getReader().readLine(); + } + + return uc; + } + + private static class UpgradeServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final Class upgradeHandlerClass; + + UpgradeServlet(Class upgradeHandlerClass) { + this.upgradeHandlerClass = upgradeHandlerClass; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + req.upgrade(upgradeHandlerClass); + } + } + + private static class UpgradeConnection { + private final Socket socket; + private final Writer writer; + private final BufferedReader reader; + + UpgradeConnection(Socket socket) { + this.socket = socket; + InputStream is; + OutputStream os; + try { + is = socket.getInputStream(); + os = socket.getOutputStream(); + } catch (IOException ioe) { + throw new IllegalArgumentException(ioe); + } + + BufferedReader reader = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8); + + this.writer = writer; + this.reader = reader; + } + + public Writer getWriter() { + return writer; + } + + public BufferedReader getReader() { + return reader; + } + + public void shutdownOutput() throws IOException { + writer.flush(); + socket.shutdownOutput(); + } + + public void shutdownInput() throws IOException { + socket.shutdownInput(); + } + } + + + public static class EchoAsync implements InternalHttpUpgradeHandler { + private SocketWrapperBase wrapper; + @Override + public void init(WebConnection connection) { + System.out.println("Init: " + connection); + // Arbitrarily located in the init, could be in the initial read event, asynchronous, etc. + // Note: the completion check used will not call the completion handler if the IO completed inline and without error. + // Using a completion check that always calls complete would be easier here since the action is the same even with inline completion. + final ByteBuffer buffer = ByteBuffer.allocate(1024); + CompletionState state = wrapper.read(BlockingMode.NON_BLOCK, 10, TimeUnit.SECONDS, null, SocketWrapperBase.READ_DATA, new CompletionHandler() { + @Override + public void completed(Long result, Void attachment) { + System.out.println("Read: " + result.longValue()); + write(buffer); + } + @Override + public void failed(Throwable exc, Void attachment) { + exc.printStackTrace(); + } + }, buffer); + System.out.println("CompletionState: " + state); + if (state == CompletionState.INLINE) { + write(buffer); + } + } + + private void write(ByteBuffer buffer) { + buffer.flip(); + CompletionState state = wrapper.write(BlockingMode.BLOCK, 10, TimeUnit.SECONDS, null, SocketWrapperBase.COMPLETE_WRITE, new CompletionHandler() { + @Override + public void completed(Long result, Void attachment) { + System.out.println("Write: " + result.longValue()); + } + @Override + public void failed(Throwable exc, Void attachment) { + exc.printStackTrace(); + } + }, buffer); + System.out.println("CompletionState: " + state); + // Test zero length write used by websockets + wrapper.write(BlockingMode.BLOCK, 10, TimeUnit.SECONDS, null, SocketWrapperBase.COMPLETE_WRITE_WITH_COMPLETION, new CompletionHandler() { + @Override + public void completed(Long result, Void attachment) { + System.out.println("Write: " + result.longValue()); + } + @Override + public void failed(Throwable exc, Void attachment) { + exc.printStackTrace(); + } + }, buffer); + } + + @Override + public void pause() { + // NO-OP + } + + @Override + public void destroy() { + // NO-OP + } + + @Override + public SocketState upgradeDispatch(SocketEvent status) { + System.out.println("Processing: " + status); + switch (status) { + case OPEN_READ: + // Note: there's always an initial read event at the moment (reading should be skipped since it ends up in the internal buffer) + break; + case OPEN_WRITE: + break; + case STOP: + case DISCONNECT: + case ERROR: + case TIMEOUT: + case CONNECT_FAIL: + return SocketState.CLOSED; + } + return SocketState.UPGRADED; + } + + @Override + public void timeoutAsync(long now) { + // NO-OP + } + + @Override + public void setSocketWrapper(SocketWrapperBase wrapper) { + this.wrapper = wrapper; + } + + @Override + public void setSslSupport(SSLSupport sslSupport) { + // NO-OP + } + } + +} diff --git a/test/org/apache/coyote/http2/Http2TestBase.java b/test/org/apache/coyote/http2/Http2TestBase.java new file mode 100644 index 0000000..8a37bcd --- /dev/null +++ b/test/org/apache/coyote/http2/Http2TestBase.java @@ -0,0 +1,1537 @@ +/* + * 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.coyote.http2; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; + +import javax.net.SocketFactory; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.util.IOTools; +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.coyote.http2.HpackDecoder.HeaderEmitter; +import org.apache.coyote.http2.Http2Parser.Input; +import org.apache.coyote.http2.Http2Parser.Output; +import org.apache.tomcat.util.codec.binary.Base64; +import org.apache.tomcat.util.compat.JrePlatform; +import org.apache.tomcat.util.http.FastHttpDateFormat; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.http.parser.Priority; +import org.apache.tomcat.util.net.TesterSupport; + +/** + * Tests for compliance with the HTTP/2 specification. + */ +@RunWith(Parameterized.class) +public abstract class Http2TestBase extends TomcatBaseTest { + + @Parameters(name = "{index}: loop [{0}], useAsyncIO[{1}]") + public static Collection data() { + int loopCount = Integer.getInteger("tomcat.test.http2.loopCount", 1).intValue(); + List parameterSets = new ArrayList<>(); + + for (int loop = 0; loop < loopCount; loop++) { + for (Boolean useAsyncIO : booleans) { + parameterSets.add(new Object[] { Integer.valueOf(loop), useAsyncIO }); + } + } + + return parameterSets; + } + + @Parameter(0) + public int loop; + + @Parameter(1) + public boolean useAsyncIO; + + // Nothing special about this date apart from it being the date I ran the + // test that demonstrated that most HTTP/2 tests were failing because the + // response now included a date header + protected static final String DEFAULT_DATE = "Wed, 11 Nov 2015 19:18:42 GMT"; + protected static final long DEFAULT_TIME = FastHttpDateFormat.parseDate(DEFAULT_DATE); + + private static final String HEADER_IGNORED = "x-ignore"; + + static final String DEFAULT_CONNECTION_HEADER_VALUE = "Upgrade, HTTP2-Settings"; + private static final byte[] EMPTY_SETTINGS_FRAME = { 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 }; + static final String EMPTY_HTTP2_SETTINGS_HEADER; + + static { + byte[] empty = new byte[0]; + EMPTY_HTTP2_SETTINGS_HEADER = "HTTP2-Settings: " + Base64.encodeBase64URLSafeString(empty) + "\r\n"; + } + + protected static final String TRAILER_HEADER_NAME = "x-trailertest"; + protected static final String TRAILER_HEADER_VALUE = "test"; + + // Client + private Socket s; + protected HpackEncoder hpackEncoder; + protected Input input; + protected TestOutput output; + protected TesterHttp2Parser parser; + protected OutputStream os; + + // Server + protected Http2Protocol http2Protocol; + + private long pingAckDelayMillis = 0; + + + protected void setPingAckDelayMillis(long delay) { + pingAckDelayMillis = delay; + } + + /** + * Standard setup. Creates HTTP/2 connection via HTTP upgrade and ensures that the first response is correctly + * received. + */ + protected void http2Connect() throws Exception { + http2Connect(false); + } + + protected void http2Connect(boolean tls) throws Exception { + enableHttp2(tls); + configureAndStartWebApplication(); + openClientConnection(tls); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + } + + + protected void validateHttp2InitialResponse() throws Exception { + validateHttp2InitialResponse(200); + } + + protected void validateHttp2InitialResponse(long maxConcurrentStreams) throws Exception { + + // - 101 response acts as acknowledgement of the HTTP2-Settings header + // Need to read 5 frames + // - settings (server settings - must be first) + // - settings ack (for the settings frame in the client preface) + // - ping + // - headers (for response) + // - data (for response body) + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals("0-Settings-[3]-[" + maxConcurrentStreams + "]\n" + "0-Settings-End\n" + + "0-Settings-Ack\n" + "0-Ping-[0,0,0,0,0,0,0,1]\n" + getSimpleResponseTrace(1), output.getTrace()); + output.clearTrace(); + } + + + protected void sendEmptyGetRequest(int streamId) throws IOException { + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildEmptyGetRequest(frameHeader, headersPayload, null, streamId); + writeFrame(frameHeader, headersPayload); + } + + + protected void sendSimpleGetRequest(int streamId) throws IOException { + sendSimpleGetRequest(streamId, null); + } + + + protected void sendSimpleGetRequest(int streamId, byte[] padding) throws IOException { + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildSimpleGetRequest(frameHeader, headersPayload, padding, streamId); + writeFrame(frameHeader, headersPayload); + } + + + protected void sendLargeGetRequest(int streamId) throws IOException { + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildLargeGetRequest(frameHeader, headersPayload, streamId); + writeFrame(frameHeader, headersPayload); + } + + + protected void buildEmptyGetRequest(byte[] frameHeader, ByteBuffer headersPayload, byte[] padding, int streamId) { + buildGetRequest(frameHeader, headersPayload, padding, streamId, "/empty"); + } + + + protected void buildSimpleGetRequest(byte[] frameHeader, ByteBuffer headersPayload, byte[] padding, int streamId) { + buildGetRequest(frameHeader, headersPayload, padding, streamId, "/simple"); + } + + + protected void buildLargeGetRequest(byte[] frameHeader, ByteBuffer headersPayload, int streamId) { + buildGetRequest(frameHeader, headersPayload, null, streamId, "/large"); + } + + + protected void buildGetRequest(byte[] frameHeader, ByteBuffer headersPayload, byte[] padding, int streamId, + String url) { + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", url)); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildGetRequest(frameHeader, headersPayload, padding, headers, streamId); + } + + + protected void buildGetRequest(byte[] frameHeader, ByteBuffer headersPayload, byte[] padding, List
    headers, + int streamId) { + if (padding != null) { + headersPayload.put((byte) (0xFF & padding.length)); + } + MimeHeaders mimeHeaders = new MimeHeaders(); + for (Header header : headers) { + mimeHeaders.addValue(header.getName()).setString(header.getValue()); + } + hpackEncoder.encode(mimeHeaders, headersPayload); + if (padding != null) { + headersPayload.put(padding); + } + headersPayload.flip(); + + ByteUtil.setThreeBytes(frameHeader, 0, headersPayload.limit()); + frameHeader[3] = FrameType.HEADERS.getIdByte(); + // Flags. end of headers (0x04). end of stream (0x01) + frameHeader[4] = 0x05; + if (padding != null) { + frameHeader[4] += 0x08; + } + // Stream id + ByteUtil.set31Bits(frameHeader, 5, streamId); + } + + + protected void buildSimpleGetRequestPart1(byte[] frameHeader, ByteBuffer headersPayload, int streamId) { + List
    headers = new ArrayList<>(3); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + + buildSimpleGetRequestPart1(frameHeader, headersPayload, headers, streamId); + } + + + protected void buildSimpleGetRequestPart1(byte[] frameHeader, ByteBuffer headersPayload, List
    headers, + int streamId) { + MimeHeaders mimeHeaders = new MimeHeaders(); + for (Header header : headers) { + mimeHeaders.addValue(header.getName()).setString(header.getValue()); + } + hpackEncoder.encode(mimeHeaders, headersPayload); + + headersPayload.flip(); + + ByteUtil.setThreeBytes(frameHeader, 0, headersPayload.limit()); + frameHeader[3] = FrameType.HEADERS.getIdByte(); + // Flags. end of stream (0x01) + frameHeader[4] = 0x01; + // Stream id + ByteUtil.set31Bits(frameHeader, 5, streamId); + } + + + protected void buildSimpleGetRequestPart2(byte[] frameHeader, ByteBuffer headersPayload, int streamId) { + List
    headers = new ArrayList<>(3); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildSimpleGetRequestPart2(frameHeader, headersPayload, headers, streamId); + } + + + protected void buildSimpleGetRequestPart2(byte[] frameHeader, ByteBuffer headersPayload, List
    headers, + int streamId) { + MimeHeaders mimeHeaders = new MimeHeaders(); + for (Header header : headers) { + mimeHeaders.addValue(header.getName()).setString(header.getValue()); + } + hpackEncoder.encode(mimeHeaders, headersPayload); + + headersPayload.flip(); + + ByteUtil.setThreeBytes(frameHeader, 0, headersPayload.limit()); + frameHeader[3] = FrameType.CONTINUATION.getIdByte(); + // Flags. end of headers (0x04) + frameHeader[4] = 0x04; + // Stream id + ByteUtil.set31Bits(frameHeader, 5, streamId); + } + + + protected void sendSimplePostRequest(int streamId, byte[] padding) throws IOException { + sendSimplePostRequest(streamId, padding, true); + } + + + protected void sendSimplePostRequest(int streamId, byte[] padding, boolean writeBody) throws IOException { + sendSimplePostRequest(streamId, padding, writeBody, false); + } + + protected void sendSimplePostRequest(int streamId, byte[] padding, boolean writeBody, boolean useExpectation) + throws IOException { + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(128); + + buildPostRequest(headersFrameHeader, headersPayload, useExpectation, dataFrameHeader, dataPayload, padding, + streamId); + writeFrame(headersFrameHeader, headersPayload); + if (writeBody) { + writeFrame(dataFrameHeader, dataPayload); + } + } + + + protected void sendParameterPostRequest(int streamId, byte[] padding, String body, long contentLength, + boolean useExpectation) throws IOException { + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(128); + + buildPostRequest(headersFrameHeader, headersPayload, useExpectation, "application/x-www-form-urlencoded", + contentLength, "/parameter", dataFrameHeader, dataPayload, padding, null, null, streamId); + writeFrame(headersFrameHeader, headersPayload); + if (body != null) { + dataPayload.put(body.getBytes(StandardCharsets.ISO_8859_1)); + writeFrame(dataFrameHeader, dataPayload); + } + } + + + protected void buildPostRequest(byte[] headersFrameHeader, ByteBuffer headersPayload, boolean useExpectation, + byte[] dataFrameHeader, ByteBuffer dataPayload, byte[] padding, int streamId) { + buildPostRequest(headersFrameHeader, headersPayload, useExpectation, dataFrameHeader, dataPayload, padding, + null, null, streamId); + } + + protected void buildPostRequest(byte[] headersFrameHeader, ByteBuffer headersPayload, boolean useExpectation, + byte[] dataFrameHeader, ByteBuffer dataPayload, byte[] padding, byte[] trailersFrameHeader, + ByteBuffer trailersPayload, int streamId) { + buildPostRequest(headersFrameHeader, headersPayload, useExpectation, null, -1, "/simple", dataFrameHeader, + dataPayload, padding, trailersFrameHeader, trailersPayload, streamId); + } + + protected void buildPostRequest(byte[] headersFrameHeader, ByteBuffer headersPayload, boolean useExpectation, + String contentType, long contentLength, String path, byte[] dataFrameHeader, ByteBuffer dataPayload, + byte[] padding, byte[] trailersFrameHeader, ByteBuffer trailersPayload, int streamId) { + + MimeHeaders headers = new MimeHeaders(); + headers.addValue(":method").setString("POST"); + headers.addValue(":scheme").setString("http"); + headers.addValue(":path").setString(path); + headers.addValue(":authority").setString("localhost:" + getPort()); + if (useExpectation) { + headers.addValue("expect").setString("100-continue"); + } + if (contentType != null) { + headers.addValue("content-type").setString(contentType); + } + if (contentLength > -1) { + headers.addValue("content-length").setLong(contentLength); + } + hpackEncoder.encode(headers, headersPayload); + + headersPayload.flip(); + + ByteUtil.setThreeBytes(headersFrameHeader, 0, headersPayload.limit()); + headersFrameHeader[3] = FrameType.HEADERS.getIdByte(); + // Flags. end of headers (0x04) + headersFrameHeader[4] = 0x04; + // Stream id + ByteUtil.set31Bits(headersFrameHeader, 5, streamId); + + // Data + if (padding != null) { + dataPayload.put((byte) (padding.length & 0xFF)); + dataPayload.limit(dataPayload.capacity() - padding.length); + } + + while (dataPayload.hasRemaining()) { + dataPayload.put((byte) 'x'); + } + if (padding != null && padding.length > 0) { + dataPayload.limit(dataPayload.capacity()); + dataPayload.put(padding); + } + + dataPayload.flip(); + + // Size + ByteUtil.setThreeBytes(dataFrameHeader, 0, dataPayload.limit()); + // Data is type 0 + // Flags: End of stream 1, Padding 8 + if (trailersPayload == null) { + dataFrameHeader[4] = 0x01; + } else { + dataFrameHeader[4] = 0x00; + } + if (padding != null) { + dataFrameHeader[4] += 0x08; + } + ByteUtil.set31Bits(dataFrameHeader, 5, streamId); + + // Trailers + if (trailersPayload != null) { + MimeHeaders trailerHeaders = new MimeHeaders(); + trailerHeaders.addValue(TRAILER_HEADER_NAME).setString(TRAILER_HEADER_VALUE); + hpackEncoder.encode(trailerHeaders, trailersPayload); + + trailersPayload.flip(); + + ByteUtil.setThreeBytes(trailersFrameHeader, 0, trailersPayload.limit()); + trailersFrameHeader[3] = FrameType.HEADERS.getIdByte(); + // Flags. end of headers (0x04) and end of stream (0x01) + trailersFrameHeader[4] = 0x05; + // Stream id + ByteUtil.set31Bits(trailersFrameHeader, 5, streamId); + } + } + + + protected void buildHeadRequest(byte[] headersFrameHeader, ByteBuffer headersPayload, int streamId, String path) { + MimeHeaders headers = new MimeHeaders(); + headers.addValue(":method").setString("HEAD"); + headers.addValue(":scheme").setString("http"); + headers.addValue(":path").setString(path); + headers.addValue(":authority").setString("localhost:" + getPort()); + hpackEncoder.encode(headers, headersPayload); + + headersPayload.flip(); + + ByteUtil.setThreeBytes(headersFrameHeader, 0, headersPayload.limit()); + headersFrameHeader[3] = FrameType.HEADERS.getIdByte(); + // Flags. end of headers (0x04) + headersFrameHeader[4] = 0x04; + // Stream id + ByteUtil.set31Bits(headersFrameHeader, 5, streamId); + } + + + protected void sendHeadRequest(int streamId, String path) throws IOException { + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildHeadRequest(frameHeader, headersPayload, streamId, path); + writeFrame(frameHeader, headersPayload); + } + + + protected void writeFrame(byte[] header, ByteBuffer payload) throws IOException { + writeFrame(header, payload, 0, payload.limit()); + } + + + protected void writeFrame(byte[] header, ByteBuffer payload, int offset, int len) throws IOException { + writeFrame(header, payload, offset, len, 0); + } + + + protected void writeFrame(byte[] header, ByteBuffer payload, int offset, int len, int delayms) throws IOException { + os.write(header); + os.write(payload.array(), payload.arrayOffset() + offset, len); + os.flush(); + if (delayms > 0) { + try { + Thread.sleep(delayms); + } catch (InterruptedException e) { + // Ignore + } + } + } + + + protected void readSimpleGetResponse() throws Http2Exception, IOException { + // Headers + parser.readFrame(); + // Body + parser.readFrame(); + } + + + protected void readSimplePostResponse(boolean padding) throws Http2Exception, IOException { + /* + * If there is padding there will always be a window update for the connection and, depending on timing, there + * may be an update for the stream. The Window updates for padding (if present) may appear at any time. The + * comments in the code below are only indicative of what the frames are likely to contain. Actual frame order + * with padding may be different. + */ + + // Connection window update after reading request body + parser.readFrame(); + // Stream window update after reading request body + parser.readFrame(); + // Headers + parser.readFrame(); + // Body (includes end of stream) + parser.readFrame(); + + if (padding) { + // Connection window update for padding + parser.readFrame(); + + // If EndOfStream has not been received then the stream window + // update must have been received so a further frame needs to be + // read for EndOfStream. + if (!output.getTrace().contains("EndOfStream")) { + parser.readFrame(); + } + } + } + + + protected String getEmptyResponseTrace(int streamId) { + return getResponseBodyFrameTrace(streamId, "0"); + } + + + protected String getSimpleResponseTrace(int streamId) { + return getResponseBodyFrameTrace(streamId, "8192"); + } + + + protected String getCookieResponseTrace(int streamId, int cookieCount) { + return getResponseBodyFrameTrace(streamId, 200, "text/plain;charset=UTF-8", null, + "Cookie count: " + cookieCount, null); + } + + + protected String getResponseBodyFrameTrace(int streamId, String body) { + return getResponseBodyFrameTrace(streamId, 200, "application/octet-stream", null, body, body); + } + + + protected String getResponseBodyFrameTrace(int streamId, int status, String contentType, String contentLanguage, + String body, String cl) { + StringBuilder result = new StringBuilder(); + result.append(streamId); + result.append("-HeadersStart\n"); + result.append(streamId); + result.append("-Header-[:status]-["); + result.append(status); + result.append("]\n"); + result.append(streamId); + result.append("-Header-[content-type]-["); + result.append(contentType); + result.append("]\n"); + if (contentLanguage != null) { + result.append(streamId); + result.append("-Header-[content-language]-["); + result.append(contentLanguage); + result.append("]\n"); + } + if (cl != null) { + result.append(streamId); + result.append("-Header-[content-length]-["); + result.append(cl); + result.append("]\n"); + } + result.append(streamId); + result.append("-Header-[date]-["); + result.append(DEFAULT_DATE); + result.append("]\n"); + result.append(streamId); + result.append("-HeadersEnd\n"); + result.append(streamId); + result.append("-Body-"); + result.append(body); + result.append("\n"); + result.append(streamId); + result.append("-EndOfStream\n"); + + return result.toString(); + } + + + protected void enableHttp2() { + enableHttp2(200); + } + + protected void enableHttp2(long maxConcurrentStreams) { + enableHttp2(maxConcurrentStreams, false); + } + + protected void enableHttp2(boolean tls) { + enableHttp2(200, tls); + } + + protected void enableHttp2(long maxConcurrentStreams, boolean tls) { + enableHttp2(maxConcurrentStreams, tls, 10000, 10000, 25000, 5000, 5000); + } + + protected void enableHttp2(long maxConcurrentStreams, boolean tls, long readTimeout, long writeTimeout, + long keepAliveTimeout, long streamReadTimout, long streamWriteTimeout) { + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + Assert.assertTrue(connector.setProperty("useAsyncIO", Boolean.toString(useAsyncIO))); + http2Protocol = new UpgradableHttp2Protocol(); + // Short timeouts for now. May need to increase these for CI systems. + http2Protocol.setReadTimeout(readTimeout); + http2Protocol.setWriteTimeout(writeTimeout); + http2Protocol.setKeepAliveTimeout(keepAliveTimeout); + http2Protocol.setStreamReadTimeout(streamReadTimout); + http2Protocol.setStreamWriteTimeout(streamWriteTimeout); + http2Protocol.setMaxConcurrentStreams(maxConcurrentStreams); + http2Protocol.setHttp11Protocol((AbstractHttp11Protocol) connector.getProtocolHandler()); + connector.addUpgradeProtocol(http2Protocol); + if (tls) { + // Enable TLS + TesterSupport.initSsl(tomcat); + } + } + + private static class UpgradableHttp2Protocol extends Http2Protocol { + @Override + public String getHttpUpgradeName(boolean isSSLEnabled) { + return "h2c"; + } + } + + protected void configureAndStartWebApplication() throws LifecycleException { + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "empty", new EmptyServlet()); + ctxt.addServletMappingDecoded("/empty", "empty"); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Tomcat.addServlet(ctxt, "large", new LargeServlet()); + ctxt.addServletMappingDecoded("/large", "large"); + Tomcat.addServlet(ctxt, "cookie", new CookieServlet()); + ctxt.addServletMappingDecoded("/cookie", "cookie"); + Tomcat.addServlet(ctxt, "parameter", new ParameterServlet()); + ctxt.addServletMappingDecoded("/parameter", "parameter"); + + tomcat.start(); + } + + + protected void openClientConnection() throws IOException { + openClientConnection(false); + } + + protected void openClientConnection(boolean tls) throws IOException { + SocketFactory socketFactory = tls ? TesterSupport.configureClientSsl() : SocketFactory.getDefault(); + // Open a connection + s = socketFactory.createSocket("localhost", getPort()); + s.setSoTimeout(30000); + + os = new BufferedOutputStream(s.getOutputStream()); + InputStream is = s.getInputStream(); + + input = new TestInput(is); + output = new TestOutput(); + parser = new TesterHttp2Parser("-1", input, output); + hpackEncoder = new HpackEncoder(); + } + + + protected void doHttpUpgrade() throws IOException { + doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2c", EMPTY_HTTP2_SETTINGS_HEADER, true); + } + + protected void doHttpUpgrade(String connection, String upgrade, String settings, boolean validate) + throws IOException { + byte[] upgradeRequest = ("GET /simple HTTP/1.1\r\n" + "Host: localhost:" + getPort() + "\r\n" + "Connection: " + + connection + "\r\n" + "Upgrade: " + upgrade + "\r\n" + settings + "\r\n") + .getBytes(StandardCharsets.ISO_8859_1); + os.write(upgradeRequest); + os.flush(); + + if (validate) { + Assert.assertTrue("Failed to read HTTP Upgrade response", readHttpUpgradeResponse()); + } + } + + + boolean readHttpUpgradeResponse() throws IOException { + String[] responseHeaders = readHttpResponseHeaders(); + + if (responseHeaders.length < 3) { + return false; + } + if (!responseHeaders[0].startsWith("HTTP/1.1 101 ")) { + return false; + } + + if (!validateHeader(responseHeaders, "Connection: Upgrade")) { + return false; + } + if (!validateHeader(responseHeaders, "Upgrade: h2c")) { + return false; + } + + return true; + } + + + private boolean validateHeader(String[] responseHeaders, String header) { + boolean found = false; + for (String responseHeader : responseHeaders) { + if (responseHeader.equalsIgnoreCase(header)) { + found = true; + break; + } + } + return found; + } + + + String[] readHttpResponseHeaders() throws IOException { + // Only used by test code so safe to keep this just a little larger than + // we are expecting. + ByteBuffer data = ByteBuffer.allocate(256); + byte[] singleByte = new byte[1]; + // Looking for \r\n\r\n + int seen = 0; + while (seen < 4) { + input.fill(true, singleByte); + switch (seen) { + case 0: + case 2: { + if (singleByte[0] == '\r') { + seen++; + } else { + seen = 0; + } + break; + } + case 1: + case 3: { + if (singleByte[0] == '\n') { + seen++; + } else { + seen = 0; + } + break; + } + } + data.put(singleByte[0]); + } + + if (seen != 4) { + throw new IOException("End of headers not found"); + } + + String response = new String(data.array(), data.arrayOffset(), data.arrayOffset() + data.position(), + StandardCharsets.ISO_8859_1); + + return response.split("\r\n"); + } + + + void parseHttp11Response() throws IOException { + String[] responseHeaders = readHttpResponseHeaders(); + Assert.assertTrue(responseHeaders[0], responseHeaders[0].startsWith("HTTP/1.1 200 ")); + + // Find the content length (chunked responses not handled) + for (int i = 1; i < responseHeaders.length; i++) { + if (responseHeaders[i].toLowerCase(Locale.ENGLISH).startsWith("content-length")) { + String cl = responseHeaders[i]; + int pos = cl.indexOf(':'); + if (pos == -1) { + throw new IOException("Invalid: [" + cl + "]"); + } + int len = Integer.parseInt(cl.substring(pos + 1).trim()); + byte[] content = new byte[len]; + input.fill(true, content); + return; + } + } + Assert.fail("No content-length in response"); + } + + + void sendClientPreface() throws IOException { + os.write(Http2Parser.CLIENT_PREFACE_START); + os.write(EMPTY_SETTINGS_FRAME); + os.flush(); + } + + + void sendRst(int streamId, long errorCode) throws IOException { + byte[] rstFrame = new byte[13]; + // length is always 4 + rstFrame[2] = 0x04; + rstFrame[3] = FrameType.RST.getIdByte(); + // no flags + // Stream ID + ByteUtil.set31Bits(rstFrame, 5, streamId); + // Payload + ByteUtil.setFourBytes(rstFrame, 9, errorCode); + + os.write(rstFrame); + os.flush(); + } + + + void sendPing() throws IOException { + sendPing(0, false, new byte[8]); + } + + + void sendPing(int streamId, boolean ack, byte[] payload) throws IOException { + if (ack && pingAckDelayMillis > 0) { + try { + Thread.sleep(pingAckDelayMillis); + } catch (InterruptedException e) { + // Ignore + } + } + byte[] pingHeader = new byte[9]; + // length + ByteUtil.setThreeBytes(pingHeader, 0, payload.length); + // Type + pingHeader[3] = FrameType.PING.getIdByte(); + // Flags + if (ack) { + setOneBytes(pingHeader, 4, 0x01); + } + // Stream + ByteUtil.set31Bits(pingHeader, 5, streamId); + os.write(pingHeader); + os.write(payload); + os.flush(); + } + + + byte[] buildGoaway(int streamId, int lastStreamId, long errorCode) { + byte[] goawayFrame = new byte[17]; + ByteUtil.setThreeBytes(goawayFrame, 0, 8); + // Type + goawayFrame[3] = FrameType.GOAWAY.getIdByte(); + // No flags + // Stream + ByteUtil.set31Bits(goawayFrame, 5, streamId); + // Last stream + ByteUtil.set31Bits(goawayFrame, 9, lastStreamId); + ByteUtil.setFourBytes(goawayFrame, 13, errorCode); + return goawayFrame; + } + + + void sendGoaway(int streamId, int lastStreamId, long errorCode) throws IOException { + byte[] goawayFrame = buildGoaway(streamId, lastStreamId, errorCode); + os.write(goawayFrame); + os.flush(); + } + + + void sendWindowUpdate(int streamId, int increment) throws IOException { + byte[] updateFrame = new byte[13]; + // length is always 4 + updateFrame[2] = 0x04; + updateFrame[3] = FrameType.WINDOW_UPDATE.getIdByte(); + // no flags + // Stream ID + ByteUtil.set31Bits(updateFrame, 5, streamId); + // Payload + ByteUtil.set31Bits(updateFrame, 9, increment); + + os.write(updateFrame); + os.flush(); + } + + + void sendData(int streamId, byte[] payload) throws IOException { + byte[] header = new byte[9]; + // length + ByteUtil.setThreeBytes(header, 0, payload.length); + // Type is zero + // No flags + // Stream ID + ByteUtil.set31Bits(header, 5, streamId); + + os.write(header); + os.write(payload); + os.flush(); + } + + + void sendPriority(int streamId, int streamDependencyId, int weight) throws IOException { + byte[] priorityFrame = new byte[14]; + // length + ByteUtil.setThreeBytes(priorityFrame, 0, 5); + // type + priorityFrame[3] = FrameType.PRIORITY.getIdByte(); + // No flags + // Stream ID + ByteUtil.set31Bits(priorityFrame, 5, streamId); + + // Payload + ByteUtil.set31Bits(priorityFrame, 9, streamDependencyId); + setOneBytes(priorityFrame, 13, weight); + + os.write(priorityFrame); + os.flush(); + } + + + void sendPriorityUpdate(int streamId, int urgency, boolean incremental) throws IOException { + // Need to know the payload length first + StringBuilder sb = new StringBuilder("u="); + sb.append(urgency); + if (incremental) { + sb.append(", i"); + } + byte[] payload = sb.toString().getBytes(StandardCharsets.US_ASCII); + + byte[] priorityUpdateFrame = new byte[13 + payload.length]; + + // length + ByteUtil.setThreeBytes(priorityUpdateFrame, 0, 4 + payload.length); + // type + priorityUpdateFrame[3] = FrameType.PRIORITY_UPDATE.getIdByte(); + // Stream ID + ByteUtil.set31Bits(priorityUpdateFrame, 5, 0); + + // Payload + ByteUtil.set31Bits(priorityUpdateFrame, 9, streamId); + System.arraycopy(payload, 0, priorityUpdateFrame, 13, payload.length); + + os.write(priorityUpdateFrame); + os.flush(); + } + + + void sendSettings(int streamId, boolean ack, SettingValue... settings) throws IOException { + // length + int settingsCount; + if (settings == null) { + settingsCount = 0; + } else { + settingsCount = settings.length; + } + + byte[] settingFrame = new byte[9 + 6 * settingsCount]; + + ByteUtil.setThreeBytes(settingFrame, 0, 6 * settingsCount); + // type + settingFrame[3] = FrameType.SETTINGS.getIdByte(); + + if (ack) { + settingFrame[4] = 0x01; + } + + // Stream + ByteUtil.set31Bits(settingFrame, 5, streamId); + + // Payload + for (int i = 0; i < settingsCount; i++) { + // Stops IDE complaining about possible NPE + Assert.assertNotNull(settings); + ByteUtil.setTwoBytes(settingFrame, (i * 6) + 9, settings[i].getSetting()); + ByteUtil.setFourBytes(settingFrame, (i * 6) + 11, settings[i].getValue()); + } + + os.write(settingFrame); + os.flush(); + } + + + void handleGoAwayResponse(int lastStream) throws Http2Exception, IOException { + handleGoAwayResponse(lastStream, Http2Error.PROTOCOL_ERROR); + } + + + protected void skipWindowSizeFrames() throws Http2Exception, IOException { + do { + output.clearTrace(); + parser.readFrame(); + } while (output.getTrace().contains("WindowSize")); + } + + + void handleGoAwayResponse(int lastStream, Http2Error expectedError) throws Http2Exception, IOException { + try { + parser.readFrame(); + + Assert.assertTrue(output.getTrace(), + output.getTrace().startsWith("0-Goaway-[" + lastStream + "]-[" + expectedError.getCode() + "]-[")); + } catch (SocketException se) { + // On some platform / Connector combinations (e.g. Windows / NIO2), + // the TCP connection close will be processed before the client gets + // a chance to read the connection close frame. + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + + Assume.assumeTrue("This test is only expected to trigger an exception with NIO2", + connector.getProtocolHandlerClassName().contains("Nio2")); + + Assume.assumeTrue("This test is only expected to trigger an exception on Windows", JrePlatform.IS_WINDOWS); + } + } + + + static void setOneBytes(byte[] output, int firstByte, int value) { + output[firstByte] = (byte) (value & 0xFF); + } + + + private static class TestInput implements Http2Parser.Input { + + private final InputStream is; + + + TestInput(InputStream is) { + this.is = is; + } + + + @Override + public boolean fill(boolean block, byte[] data, int offset, int length) throws IOException { + // Note: Block is ignored for this test class. Reads always block. + int off = offset; + int len = length; + while (len > 0) { + int read = is.read(data, off, len); + if (read == -1) { + throw new IOException("End of input stream with [" + len + "] bytes left to read"); + } + off += read; + len -= read; + } + return true; + } + + + @Override + public int getMaxFrameSize() { + // Hard-coded to use the default + return ConnectionSettingsBase.DEFAULT_MAX_FRAME_SIZE; + } + } + + + public class TestOutput implements Output, HeaderEmitter { + + private StringBuffer trace = new StringBuffer(); + private String lastStreamId = "0"; + private ConnectionSettingsRemote remoteSettings = new ConnectionSettingsRemote("-1"); + private boolean traceBody = false; + private ByteBuffer bodyBuffer = null; + private long bytesRead; + private volatile HpackDecoder hpackDecoder = null; + + public void setTraceBody(boolean traceBody) { + this.traceBody = traceBody; + } + + + @Override + public HpackDecoder getHpackDecoder() { + if (hpackDecoder == null) { + synchronized (this) { + if (hpackDecoder == null) { + hpackDecoder = new HpackDecoder(remoteSettings.getHeaderTableSize()); + } + } + } + return hpackDecoder; + } + + + @Override + public ByteBuffer startRequestBodyFrame(int streamId, int payloadSize, boolean endOfStream) { + lastStreamId = Integer.toString(streamId); + bytesRead += payloadSize; + if (traceBody) { + bodyBuffer = ByteBuffer.allocate(payloadSize); + return bodyBuffer; + } else { + trace.append(lastStreamId + "-Body-" + payloadSize + "\n"); + return null; + } + } + + + @Override + public void endRequestBodyFrame(int streamId, int dataLength) throws Http2Exception { + if (bodyBuffer != null) { + if (bodyBuffer.limit() > 0) { + trace.append(lastStreamId + "-Body-"); + bodyBuffer.flip(); + while (bodyBuffer.hasRemaining()) { + trace.append((char) bodyBuffer.get()); + } + trace.append("\n"); + bodyBuffer = null; + } + } + } + + + @Override + public HeaderEmitter headersStart(int streamId, boolean headersEndStream) { + lastStreamId = Integer.toString(streamId); + trace.append(lastStreamId + "-HeadersStart\n"); + return this; + } + + + @Override + public void emitHeader(String name, String value) { + if ("date".equals(name)) { + // Date headers will always change so use a hard-coded default + value = DEFAULT_DATE; + } else if ("etag".equals(name) && value.startsWith("W/\"")) { + // etag headers will vary depending on when the source was + // checked out, unpacked, copied etc so use the same default as + // for date headers + int startOfTime = value.indexOf('-'); + value = value.substring(0, startOfTime + 1) + DEFAULT_TIME + "\""; + } + // Some header values vary so ignore them + if (HEADER_IGNORED.equals(name)) { + trace.append(lastStreamId + "-Header-[" + name + "]-[...]\n"); + } else { + trace.append(lastStreamId + "-Header-[" + name + "]-[" + value + "]\n"); + } + } + + + @Override + public void validateHeaders() { + // NO-OP: Accept anything the server sends for the unit tests + } + + + @Override + public void setHeaderException(StreamException streamException) { + // NO-OP: Accept anything the server sends for the unit tests + } + + + @Override + public void headersContinue(int payloadSize, boolean endOfHeaders) { + // NO-OP: Logging occurs per header + } + + + @Override + public void headersEnd(int streamId, boolean endOfStream) { + trace.append(streamId + "-HeadersEnd\n"); + if (endOfStream) { + receivedEndOfStream(streamId) ; + } + } + + + @Override + public void receivedEndOfStream(int streamId) { + lastStreamId = Integer.toString(streamId); + trace.append(lastStreamId + "-EndOfStream\n"); + } + + + @Override + public void reset(int streamId, long errorCode) { + trace.append(streamId + "-RST-[" + errorCode + "]\n"); + } + + + @Override + public void setting(Setting setting, long value) throws ConnectionException { + trace.append("0-Settings-[" + setting + "]-[" + value + "]\n"); + remoteSettings.set(setting, value); + } + + + @Override + public void settingsEnd(boolean ack) throws IOException { + if (ack) { + trace.append("0-Settings-Ack\n"); + } else { + trace.append("0-Settings-End\n"); + sendSettings(0, true); + } + } + + + @Override + public void pingReceive(byte[] payload, boolean ack) throws IOException { + trace.append("0-Ping-"); + if (ack) { + trace.append("Ack-"); + } else { + sendPing(0, true, payload); + } + trace.append('['); + boolean first = true; + for (byte b : payload) { + if (first) { + first = false; + } else { + trace.append(','); + } + trace.append(b & 0xFF); + } + trace.append("]\n"); + } + + + @Override + public void goaway(int lastStreamId, long errorCode, String debugData) { + trace.append("0-Goaway-[" + lastStreamId + "]-[" + errorCode + "]-[" + debugData + "]"); + } + + + @Override + public void incrementWindowSize(int streamId, int increment) { + trace.append(streamId + "-WindowSize-[" + increment + "]\n"); + } + + + @Override + public void priorityUpdate(int prioritizedStreamID, Priority p) throws Http2Exception { + trace.append( + prioritizedStreamID + "-PriorityUpdate-[" + p.getUrgency() + "]-[" + p.getIncremental() + "]\n"); + } + + + @Override + public void onSwallowedUnknownFrame(int streamId, int frameTypeId, int flags, int size) { + trace.append(streamId); + trace.append(','); + trace.append(frameTypeId); + trace.append(','); + trace.append(flags); + trace.append(','); + trace.append(size); + trace.append("\n"); + } + + + @Override + public void onSwallowedDataFramePayload(int streamId, int swallowedDataBytesCount) { + // NO-OP + // Many tests swallow request bodies which triggers this + // notification. It is added to the trace to reduce noise. + } + + + public void pushPromise(int streamId, int pushedStreamId) { + trace.append(streamId); + trace.append("-PushPromise-"); + trace.append(pushedStreamId); + trace.append("\n"); + } + + + public void clearTrace() { + trace = new StringBuffer(); + bytesRead = 0; + } + + + public String getTrace() { + return trace.toString(); + } + + + public int getMaxFrameSize() { + return remoteSettings.getMaxFrameSize(); + } + + + public long getBytesRead() { + return bytesRead; + } + + + @Override + public void increaseOverheadCount(FrameType frameType) { + // NO-OP. Client doesn't track overhead. + } + } + + + private static class EmptyServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Generate an empty response + resp.setContentType("application/octet-stream"); + resp.setContentLength(0); + resp.flushBuffer(); + } + } + + + public static class NoContentServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + } + } + + + public static class SimpleServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public static final int CONTENT_LENGTH = 8192; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Generate content with a simple known format. + resp.setContentType("application/octet-stream"); + + int count = 4 * 1024; + // Two bytes per entry + resp.setContentLengthLong(count * 2); + + OutputStream os = resp.getOutputStream(); + byte[] data = new byte[2]; + for (int i = 0; i < count; i++) { + data[0] = (byte) (i & 0xFF); + data[1] = (byte) ((i >> 8) & 0xFF); + os.write(data); + } + } + + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Do not do this at home. The unconstrained buffer is a DoS risk. + + // Have to read into a buffer because clients typically do not start + // to read the response until the request is fully written. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IOTools.flow(req.getInputStream(), baos); + + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + IOTools.flow(bais, resp.getOutputStream()); + + // Check for trailer headers + String trailerValue = req.getTrailerFields().get(TRAILER_HEADER_NAME); + if (trailerValue != null) { + resp.getOutputStream().write(trailerValue.getBytes(StandardCharsets.UTF_8)); + } + } + } + + + private static class LargeServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Generate content with a simple known format that will exceed the + // default flow control window for a stream. + resp.setContentType("application/octet-stream"); + + int count = 128 * 1024; + // Two bytes per entry + resp.setContentLengthLong(count * 2L); + + OutputStream os = resp.getOutputStream(); + byte[] data = new byte[2]; + for (int i = 0; i < count; i++) { + data[0] = (byte) (i & 0xFF); + data[1] = (byte) ((i >> 8) & 0xFF); + os.write(data); + } + } + } + + + private static class CookieServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().print("Cookie count: " + req.getCookies().length); + resp.flushBuffer(); + } + } + + + static class LargeHeaderServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + StringBuilder headerValue = new StringBuilder(); + Random random = new Random(); + while (headerValue.length() < 2048) { + headerValue.append(Long.toString(random.nextLong())); + } + resp.setHeader(HEADER_IGNORED, headerValue.toString()); + resp.getWriter().print("OK"); + } + } + + + static class ParameterServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + Map params = req.getParameterMap(); + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + resp.getWriter().print(params.size()); + } + } + + + static class ReadRequestBodyServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Request bodies are unusual with GET but not illegal + doPost(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + long total = 0; + long read = 0; + + if ("true".equals(req.getParameter("useReader"))) { + char[] buffer = new char[1024]; + + try (InputStream is = req.getInputStream(); + InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8);) { + while ((read = reader.read(buffer)) > 0) { + total += read; + } + } + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + pw.print("Total chars read from request body [" + total + "]"); + + } else { + byte[] buffer = new byte[1024]; + try (InputStream is = req.getInputStream()) { + while ((read = is.read(buffer)) > 0) { + total += read; + } + } + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + pw.print("Total bytes read from request body [" + total + "]"); + } + } + } + + + static class SettingValue { + private final int setting; + private final long value; + + SettingValue(int setting, long value) { + this.setting = setting; + this.value = value; + } + + public int getSetting() { + return setting; + } + + public long getValue() { + return value; + } + } + + + static class Header { + private final String name; + private final String value; + + Header(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + } +} diff --git a/test/org/apache/coyote/http2/TestAsync.java b/test/org/apache/coyote/http2/TestAsync.java new file mode 100644 index 0000000..f15cc22 --- /dev/null +++ b/test/org/apache/coyote/http2/TestAsync.java @@ -0,0 +1,275 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; + +/* + * Based on + * https://bz.apache.org/bugzilla/show_bug.cgi?id=62614 + * https://bz.apache.org/bugzilla/show_bug.cgi?id=62620 + * https://bz.apache.org/bugzilla/show_bug.cgi?id=62628 + */ +@RunWith(Parameterized.class) +public class TestAsync extends Http2TestBase { + + private static final int BLOCK_SIZE = 0x8000; + + @Parameterized.Parameters(name = "{index}: loop[{0}], useAsyncIO[{1}], expandConnectionFirst[{1}], " + + "connectionUnlimited[{2}], streamUnlimited[{3}], useNonContainerThreadForWrite[{4}]," + + "largeInitialWindow[{5}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + Collection baseData = data(); + + for (Object[] base : baseData) { + for (Boolean expandConnectionFirst : booleans) { + for (Boolean connectionUnlimited : booleans) { + for (Boolean streamUnlimited : booleans) { + for (Boolean useNonContainerThreadForWrite : booleans) { + for (Boolean largeInitialWindow : booleans) { + parameterSets.add( + new Object[] { base[0], base[1], expandConnectionFirst, connectionUnlimited, + streamUnlimited, useNonContainerThreadForWrite, largeInitialWindow }); + } + } + } + } + } + } + return parameterSets; + } + + + @Parameter(2) + public boolean expandConnectionFirst; + + @Parameter(3) + public boolean connectionUnlimited; + + @Parameter(4) + public boolean streamUnlimited; + + @Parameter(5) + public boolean useNonContainerThreadForWrite; + + @Parameter(6) + public boolean largeInitialWindow; + + + @Test + public void testEmptyWindow() throws Exception { + int blockCount = 8; + + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncServlet(blockCount, useNonContainerThreadForWrite)); + w.setAsyncSupported(true); + ctxt.addServletMappingDecoded("/async", "async"); + tomcat.start(); + + int startingWindowSize; + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + // Reset connection window size after initial response + sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH); + + if (largeInitialWindow) { + startingWindowSize = ((1 << 17) - 1); + SettingValue sv = new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), startingWindowSize); + sendSettings(0, false, sv); + // Test code assumes connection window and stream window size are the same at the start + sendWindowUpdate(0, startingWindowSize - ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE); + } else { + startingWindowSize = ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE; + } + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/async"); + writeFrame(frameHeader, headersPayload); + + if (connectionUnlimited) { + // Effectively unlimited for this test + sendWindowUpdate(0, blockCount * BLOCK_SIZE * 2); + } + if (streamUnlimited) { + // Effectively unlimited for this test + sendWindowUpdate(3, blockCount * BLOCK_SIZE * 2); + } + + // Headers + parser.readFrame(); + // Body + + if (!connectionUnlimited || !streamUnlimited) { + while (output.getBytesRead() < startingWindowSize) { + parser.readFrame(); + } + + // Check that the right number of bytes were received + Assert.assertEquals(startingWindowSize, output.getBytesRead()); + + // Increase the Window size (50% of total body) + int windowSizeIncrease = blockCount * BLOCK_SIZE / 2; + if (expandConnectionFirst) { + sendWindowUpdate(0, windowSizeIncrease); + sendWindowUpdate(3, windowSizeIncrease); + } else { + sendWindowUpdate(3, windowSizeIncrease); + sendWindowUpdate(0, windowSizeIncrease); + } + + while (output.getBytesRead() < startingWindowSize + windowSizeIncrease) { + parser.readFrame(); + } + + // Check that the right number of bytes were received + Assert.assertEquals(startingWindowSize + windowSizeIncrease, output.getBytesRead()); + + // Increase the Window size + if (expandConnectionFirst) { + sendWindowUpdate(0, windowSizeIncrease); + sendWindowUpdate(3, windowSizeIncrease); + } else { + sendWindowUpdate(3, windowSizeIncrease); + sendWindowUpdate(0, windowSizeIncrease); + } + } + + while (!output.getTrace().endsWith("3-EndOfStream\n")) { + parser.readFrame(); + } + + // Check that the right number of bytes were received + Assert.assertEquals((long) blockCount * BLOCK_SIZE, output.getBytesRead()); + } + + + public static class AsyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final int blockLimit; + private final boolean useNonContainerThreadForWrite; + private final transient ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private transient volatile Future future; + + public AsyncServlet(int blockLimit, boolean useNonContainerThreadForWrite) { + this.blockLimit = blockLimit; + this.useNonContainerThreadForWrite = useNonContainerThreadForWrite; + } + + /* + * Not thread-safe. OK for this test. NOt OK for use in the real world. + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + + final AsyncContext asyncContext = request.startAsync(); + + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/binary"); + + final ServletOutputStream output = response.getOutputStream(); + output.setWriteListener(new WriteListener() { + + // Intermittent CI errors were observed where the response body + // was exactly one block too small. Use an AtomicInteger to be + // sure blockCount is thread-safe. + final AtomicInteger blockCount = new AtomicInteger(0); + byte[] bytes = new byte[BLOCK_SIZE]; + + + @Override + public void onWritePossible() throws IOException { + if (useNonContainerThreadForWrite) { + future = scheduler.schedule(new Runnable() { + + @Override + public void run() { + try { + write(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + }, 200, TimeUnit.MILLISECONDS); + } else { + write(); + } + } + + + private void write() throws IOException { + while (output.isReady()) { + blockCount.incrementAndGet(); + output.write(bytes); + if (blockCount.get() == blockLimit) { + asyncContext.complete(); + scheduler.shutdown(); + return; + } + } + } + + @Override + public void onError(Throwable t) { + if (future != null) { + future.cancel(false); + } + t.printStackTrace(); + } + }); + } + } +} diff --git a/test/org/apache/coyote/http2/TestAsyncError.java b/test/org/apache/coyote/http2/TestAsyncError.java new file mode 100644 index 0000000..41bc398 --- /dev/null +++ b/test/org/apache/coyote/http2/TestAsyncError.java @@ -0,0 +1,169 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; + +/* + * Based on + * https://bz.apache.org/bugzilla/show_bug.cgi?id=66841 + */ +public class TestAsyncError extends Http2TestBase { + + @Test + public void testError() throws Exception { + + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncErrorServlet()); + w.setAsyncSupported(true); + ctxt.addServletMappingDecoded("/async", "async"); + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + // Send request + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/async"); + writeFrame(frameHeader, headersPayload); + + // Read response + // Headers + parser.readFrame(); + + // Read 3 'events' + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + // Reset the stream + sendRst(3, Http2Error.CANCEL.getCode()); + + int count = 0; + while (count < 50 && TestListener.getErrorCount() == 0) { + count++; + Thread.sleep(100); + } + + Assert.assertEquals(1, TestListener.getErrorCount()); + } + + + private static final class AsyncErrorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + final AsyncContext asyncContext = req.startAsync(); + TestListener testListener = new TestListener(); + asyncContext.addListener(testListener); + + MessageGenerator msgGenerator = new MessageGenerator(resp); + asyncContext.start(msgGenerator); + } + } + + + private static final class MessageGenerator implements Runnable { + + private final HttpServletResponse resp; + + MessageGenerator(HttpServletResponse resp) { + this.resp = resp; + } + + @Override + public void run() { + try { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + + while (true) { + pw.println("OK"); + pw.flush(); + if (pw.checkError()) { + throw new IOException(); + } + Thread.sleep(1000); + } + } catch (IOException | InterruptedException e) { + // Expect async error handler to handle clean-up + } + } + } + + + private static final class TestListener implements AsyncListener { + + private static final AtomicInteger errorCount = new AtomicInteger(0); + + public static int getErrorCount() { + return errorCount.get(); + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onError(AsyncEvent event) throws IOException { + errorCount.incrementAndGet(); + event.getAsyncContext().complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + // NO-OP + } + } +} diff --git a/test/org/apache/coyote/http2/TestAsyncFlush.java b/test/org/apache/coyote/http2/TestAsyncFlush.java new file mode 100644 index 0000000..a99d0fb --- /dev/null +++ b/test/org/apache/coyote/http2/TestAsyncFlush.java @@ -0,0 +1,153 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; + +/* + * Based on + * https://bz.apache.org/bugzilla/show_bug.cgi?id=62635 + * + * Note: Calling blocking I/O methods (such as flushBuffer()) during + * non-blocking I/O is explicitly called out as illegal in the Servlet + * specification but also goes on to say the behaviour if such a call is + * made is undefined. Which means it is OK if the call works as expected + * (a non-blocking flush is triggered) :). + * If any of these tests fail, that should not block a release since - + * while the specification allows this to work - it doesn't require that + * it does work. + */ +public class TestAsyncFlush extends Http2TestBase { + + private static final int BLOCK_SIZE = 1024; + + @Test + public void testFlush() throws Exception { + int blockCount = 2048; + + int targetSize = BLOCK_SIZE * blockCount; + + int totalWindow = ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE; + + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncFlushServlet(blockCount)); + w.setAsyncSupported(true); + ctxt.addServletMappingDecoded("/async", "async"); + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + // Reset connection window size after initial response + sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH); + + // Send request + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/async"); + writeFrame(frameHeader, headersPayload); + + // Headers + parser.readFrame(); + // Body + + while (output.getBytesRead() < targetSize) { + if (output.getBytesRead() == totalWindow) { + sendWindowUpdate(3, ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE); + sendWindowUpdate(0, ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE); + totalWindow += ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE; + } + parser.readFrame(); + } + + // Check that the right number of bytes were received + Assert.assertEquals(targetSize, output.getBytesRead()); + } + + + public static class AsyncFlushServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final int blockLimit; + + public AsyncFlushServlet(int blockLimit) { + this.blockLimit = blockLimit; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + + final AsyncContext asyncContext = request.startAsync(); + + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/binary"); + + final ServletOutputStream output = response.getOutputStream(); + output.setWriteListener(new WriteListener() { + + int blockCount; + byte[] bytes = new byte[BLOCK_SIZE]; + + + @Override + public void onWritePossible() throws IOException { + while (output.isReady()) { + blockCount++; + output.write(bytes); + if (blockCount % 5 == 0) { + response.flushBuffer(); + } + if (blockCount == blockLimit) { + asyncContext.complete(); + return; + } + } + } + + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + } + }); + } + } +} diff --git a/test/org/apache/coyote/http2/TestAsyncTimeout.java b/test/org/apache/coyote/http2/TestAsyncTimeout.java new file mode 100644 index 0000000..8f0a457 --- /dev/null +++ b/test/org/apache/coyote/http2/TestAsyncTimeout.java @@ -0,0 +1,235 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; + +public class TestAsyncTimeout extends Http2TestBase { + + @Test + public void testTimeout() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + // This is the target of the HTTP/2 upgrade request + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + // This is the servlet that does that actual test + // This latch is used to signal that that async thread used by the test + // has ended. It isn't essential to the test but it allows the test to + // complete without Tomcat logging an error about a still running thread. + CountDownLatch latch = new CountDownLatch(1); + Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncTimeoutServlet(latch)); + w.setAsyncSupported(true); + ctxt.addServletMappingDecoded("/async", "async"); + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + // Reset connection window size after initial response + sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH); + + // Include the response body in the trace so we can check for the PASS / + // FAIL text. + output.setTraceBody(true); + + // Send request + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/async"); + writeFrame(frameHeader, headersPayload); + + // Headers + parser.readFrame(); + // Body + parser.readFrame(); + + // Check that the expected text was received + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("PASS")); + + latch.await(10, TimeUnit.SECONDS); + } + + + public static class AsyncTimeoutServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final transient CountDownLatch latch; + + public AsyncTimeoutServlet(CountDownLatch latch) { + this.latch = latch; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + + // The idea of this test is that the timeout kicks in after 2 + // seconds and stops the async thread early rather than letting it + // complete the full 5 seconds of processing. + final AsyncContext asyncContext = request.startAsync(); + + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + // Only want to call complete() once (else we get stack traces in + // the logs so use this to track when complete() is called). + AtomicBoolean completeCalled = new AtomicBoolean(false); + Ticker ticker = new Ticker(asyncContext, completeCalled); + TimeoutListener listener = new TimeoutListener(latch, ticker, completeCalled); + asyncContext.addListener(listener); + asyncContext.setTimeout(2000); + ticker.start(); + } + } + + + private static class Ticker extends Thread { + + private final AsyncContext asyncContext; + private final AtomicBoolean completeCalled; + private volatile boolean running = true; + + Ticker(AsyncContext asyncContext, AtomicBoolean completeCalled) { + this.asyncContext = asyncContext; + this.completeCalled = completeCalled; + } + + public void end() { + running = false; + } + + @Override + public void run() { + try { + PrintWriter pw = asyncContext.getResponse().getWriter(); + int counter = 0; + + // If the test works running will be set to false before + // counter reaches 50. + while (running && counter < 50) { + sleep(100); + counter++; + pw.print("Tick " + counter); + } + // Need to call complete() here if the test fails but complete() + // should have been called by the listener. Use the flag to make + // sure we only call complete once. + if (completeCalled.compareAndSet(false, true)) { + asyncContext.complete(); + } + } catch (IOException | InterruptedException e) { + // Ignore + } + } + } + + + private static class TimeoutListener implements AsyncListener { + + private final AtomicBoolean ended = new AtomicBoolean(false); + private final CountDownLatch latch; + private final Ticker ticker; + private final AtomicBoolean completeCalled; + + TimeoutListener(CountDownLatch latch, Ticker ticker, AtomicBoolean completeCalled) { + this.latch = latch; + this.ticker = ticker; + this.completeCalled = completeCalled; + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + ticker.end(); + // Wait for the ticker to exit to avoid concurrent access to the + // response and associated writer. + // Excessively long timeout just in case things so wrong so test + // does not lock up. + try { + ticker.join(10 * 1000); + } catch (InterruptedException e) { + throw new IOException(e); + } + + if (ended.compareAndSet(false, true)) { + PrintWriter pw = event.getAsyncContext().getResponse().getWriter(); + pw.write("PASS"); + pw.flush(); + // If the timeout fires we should always need to call complete() + // here but use the flag to be safe. + if (completeCalled.compareAndSet(false, true)) { + event.getAsyncContext().complete(); + } + } + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onError(AsyncEvent event) throws IOException { + // NO-OP + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + if (ended.compareAndSet(false, true)) { + PrintWriter pw = event.getAsyncContext().getResponse().getWriter(); + pw.write("FAIL"); + pw.flush(); + } + try { + // Wait for the async thread to end before we signal that the + // test is complete. This avoids logging an exception about a + // still running thread when the unit test shuts down. + ticker.join(); + latch.countDown(); + } catch (InterruptedException e) { + // Ignore + } + } + } +} diff --git a/test/org/apache/coyote/http2/TestByteUtil.java b/test/org/apache/coyote/http2/TestByteUtil.java new file mode 100644 index 0000000..3382439 --- /dev/null +++ b/test/org/apache/coyote/http2/TestByteUtil.java @@ -0,0 +1,39 @@ +/* + * 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.coyote.http2; + +import org.junit.Assert; +import org.junit.Test; + +public class TestByteUtil { + + @Test + public void testGet31Bits() { + byte[] input = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }; + int result = ByteUtil.get31Bits(input, 0); + Assert.assertEquals(0x7fffffff, result); + } + + + @Test + public void testGetFourBytes() { + byte[] input = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }; + long result = ByteUtil.getFourBytes(input, 0); + Assert.assertEquals(0xffffffffL, result); + } + +} diff --git a/test/org/apache/coyote/http2/TestCancelledUpload.java b/test/org/apache/coyote/http2/TestCancelledUpload.java new file mode 100644 index 0000000..75ceeb9 --- /dev/null +++ b/test/org/apache/coyote/http2/TestCancelledUpload.java @@ -0,0 +1,175 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http11.AbstractHttp11Protocol; + +public class TestCancelledUpload extends Http2TestBase { + + @Test + public void testCancelledRequest() throws Exception { + http2Connect(); + + ((AbstractHttp11Protocol) http2Protocol.getHttp11Protocol()).setAllowedTrailerHeaders(TRAILER_HEADER_NAME); + + int bodySize = 8192; + int bodyCount = 20; + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(bodySize); + byte[] trailerFrameHeader = new byte[9]; + ByteBuffer trailerPayload = ByteBuffer.allocate(256); + + buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload, null, + trailerFrameHeader, trailerPayload, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + // Body + for (int i = 0; i < bodyCount; i++) { + writeFrame(dataFrameHeader, dataPayload); + } + + // Trailers + writeFrame(trailerFrameHeader, trailerPayload); + + // The Server will process the request on a separate thread to the + // incoming frames. + // The request processing thread will: + // - read up to 128 bytes of request body + // (and issue a window update for bytes read) + // - write a 403 response with no response body + // The connection processing thread will: + // - read the request body until the flow control window is exhausted + // - reset the stream if further DATA frames are received + parser.readFrame(); + + // Check for reset and exit if found + if (checkReset()) { + return; + } + + // Not window update, not reset, must be the headers + Assert.assertEquals("3-HeadersStart\n" + "3-Header-[:status]-[403]\n" + "3-Header-[content-length]-[0]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n", output.getTrace()); + output.clearTrace(); + parser.readFrame(); + + // Check for reset and exit if found + if (checkReset()) { + return; + } + + // Not window update, not reset, must be the response body + Assert.assertEquals("3-Body-0\n" + "3-EndOfStream\n", output.getTrace()); + output.clearTrace(); + parser.readFrame(); + + Assert.assertTrue(checkReset()); + + // If there are any more frames after this, ignore them + } + + + /* + * Looking for a RST frame with error type 3 (flow control error). + * + * If there is a flow control window update for stream 0 it may be followed by one for stream 3. + * + * If there is a flow control window update for stream 3 it will always be preceded by one for stream 0. + */ + private boolean checkReset() throws IOException, Http2Exception { + int lastConnectionFlowControlWindowUpdate = -1; + while (true) { + String trace = output.getTrace(); + if (trace.startsWith("3-RST-[3]\n")) { + // This is the reset we are looking for + return true; + } else if (trace.startsWith("3-RST-[")) { + // Probably error type 8 (cancel) - ignore + } else if (trace.startsWith("0-WindowSize-[")) { + // Connection flow control window update + lastConnectionFlowControlWindowUpdate = Integer.parseInt(trace.substring(14, trace.length() - 2)); + } else if (trace.startsWith("3-WindowSize-[")) { + // Stream flow control window update + // Must be same size as last connection flow control window + // update. True for Tomcat anyway. + Assert.assertEquals("3-WindowSize-[" + lastConnectionFlowControlWindowUpdate + "]\n", trace); + } else { + return false; + } + output.clearTrace(); + parser.readFrame(); + } + } + + + @Override + protected void configureAndStartWebApplication() throws LifecycleException { + Tomcat tomcat = getTomcatInstance(); + + // Retain '/simple' url-pattern since it enables code re-use + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "cancel", new CancelServlet()); + ctxt.addServletMappingDecoded("/simple", "cancel"); + + tomcat.start(); + } + + + private static class CancelServlet extends SimpleServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Read up to 128 bytes and then return a 403 response + + InputStream is = req.getInputStream(); + byte[] buf = new byte[128]; + int toRead = 128; + + int read = is.read(buf); + while (read != -1 && toRead > 0) { + toRead -= read; + read = is.read(buf); + } + + if (toRead == 0) { + resp.setStatus(HttpServletResponse.SC_FORBIDDEN); + } else { + resp.setStatus(HttpServletResponse.SC_OK); + } + } + } +} diff --git a/test/org/apache/coyote/http2/TestFlowControl.java b/test/org/apache/coyote/http2/TestFlowControl.java new file mode 100644 index 0000000..2450ffb --- /dev/null +++ b/test/org/apache/coyote/http2/TestFlowControl.java @@ -0,0 +1,144 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.LogManager; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.res.StringManager; + +public class TestFlowControl extends Http2TestBase { + + private static final StringManager sm = StringManager.getManager(TestFlowControl.class); + + /* + * https://tomcat.markmail.org/thread/lijsebphms7hr3zj + */ + @Test + public void testNotFound() throws Exception { + + LogManager.getLogManager().getLogger("org.apache.coyote.http2").setLevel(Level.ALL); + try { + http2Connect(); + + // Connection and per-stream flow control default to 64k-1 bytes + + // Set up a POST request to a non-existent end-point + // Generate headers + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + MimeHeaders headers = new MimeHeaders(); + headers.addValue(":method").setString("POST"); + headers.addValue(":scheme").setString("http"); + headers.addValue(":path").setString("/path-does-not-exist"); + headers.addValue(":authority").setString("localhost:" + getPort()); + headers.addValue("content-length").setLong(65536); + hpackEncoder.encode(headers, headersPayload); + + headersPayload.flip(); + + ByteUtil.setThreeBytes(headersFrameHeader, 0, headersPayload.limit()); + headersFrameHeader[3] = FrameType.HEADERS.getIdByte(); + // Flags. end of headers (0x04) + headersFrameHeader[4] = 0x04; + // Stream id + ByteUtil.set31Bits(headersFrameHeader, 5, 3); + + writeFrame(headersFrameHeader, headersPayload); + + // Generate body + // Max data payload is 16k + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(16 * 1024); + + while (dataPayload.hasRemaining()) { + dataPayload.put((byte) 'x'); + } + dataPayload.flip(); + + // Size + ByteUtil.setThreeBytes(dataFrameHeader, 0, dataPayload.limit()); + ByteUtil.set31Bits(dataFrameHeader, 5, 3); + + // Read the 404 error page + // headers + parser.readFrame(); + // body + parser.readFrame(); + // reset (because the request body was not fully read) + parser.readFrame(); + + // Validate response + // Response size varies as error page is generated and includes version + // number + String trace = output.getTrace(); + int start = trace.indexOf("[content-length]-[") + 18; + int end = trace.indexOf("]", start); + String contentLength = trace.substring(start, end); + // Language will depend on locale + String language = sm.getLocale().toLanguageTag(); + + Assert.assertEquals("3-HeadersStart\n" + "3-Header-[:status]-[404]\n" + + "3-Header-[content-type]-[text/html;charset=utf-8]\n" + "3-Header-[content-language]-[" + language + + "]\n" + "3-Header-[content-length]-[" + contentLength + "]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n" + "3-Body-" + contentLength + + "\n" + "3-EndOfStream\n" + "3-RST-[0]\n", output.getTrace()); + output.clearTrace(); + + // Write 3*16k=48k of request body + int count = 0; + while (count < 3) { + writeFrame(dataFrameHeader, dataPayload); + waitForWindowSize(0); + dataPayload.position(0); + count++; + } + + // EOS + dataFrameHeader[4] = 0x01; + writeFrame(dataFrameHeader, dataPayload); + waitForWindowSize(0); + } finally { + LogManager.getLogManager().getLogger("org.apache.coyote.http2").setLevel(Level.INFO); + } + } + + + /* + * This might be unnecessary but given the potential for timing differences across different systems a more robust + * approach seems prudent. + */ + private void waitForWindowSize(int streamId) throws Http2Exception, IOException { + String prefix = streamId + "-WindowSize-"; + boolean found = false; + String trace; + do { + parser.readFrame(); + trace = output.getTrace(); + output.clearTrace(); + found = trace.startsWith(prefix); + } while (!found); + } +} + diff --git a/test/org/apache/coyote/http2/TestHpack.java b/test/org/apache/coyote/http2/TestHpack.java new file mode 100644 index 0000000..0d88ed1 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHpack.java @@ -0,0 +1,150 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.http.MimeHeaders; + +public class TestHpack { + + @Test + public void testEncode() throws Exception { + MimeHeaders headers = new MimeHeaders(); + headers.setValue("header1").setString("value1"); + headers.setValue(":status").setString("200"); + headers.setValue("header2").setString("value2"); + ByteBuffer output = ByteBuffer.allocate(512); + HpackEncoder encoder = new HpackEncoder(); + encoder.encode(headers, output); + output.flip(); + // Size is supposed to be 33 without huffman, or 27 with it + // TODO: use the HpackHeaderFunction to enable huffman predictably + Assert.assertEquals(27, output.remaining()); + output.clear(); + encoder.encode(headers, output); + output.flip(); + // Size is now 3 after using the table + Assert.assertEquals(3, output.remaining()); + } + + @Test + public void testDecode() throws Exception { + MimeHeaders headers = new MimeHeaders(); + headers.setValue("header1").setString("value1"); + headers.setValue(":status").setString("200"); + headers.setValue("header2").setString("value2"); + ByteBuffer output = ByteBuffer.allocate(512); + HpackEncoder encoder = new HpackEncoder(); + encoder.encode(headers, output); + output.flip(); + MimeHeaders headers2 = new MimeHeaders(); + HpackDecoder decoder = new HpackDecoder(); + decoder.setHeaderEmitter(new HeadersListener(headers2)); + decoder.decode(output); + // Redo (table is supposed to be updated) + output.clear(); + encoder.encode(headers, output); + output.flip(); + headers2.recycle(); + Assert.assertEquals(3, output.remaining()); + // Check that the decoder is using the table right + decoder.decode(output); + Assert.assertEquals("value2", headers2.getHeader("header2")); + } + + private static class HeadersListener implements HpackDecoder.HeaderEmitter { + private final MimeHeaders headers; + + HeadersListener(MimeHeaders headers) { + this.headers = headers; + } + + @Override + public void emitHeader(String name, String value) { + headers.setValue(name).setString(value); + } + + @Override + public void setHeaderException(StreamException streamException) { + // NO-OP + } + + @Override + public void validateHeaders() throws StreamException { + // NO-OP + } + } + + @Test + public void testHeaderValueBug60451() throws HpackException { + doTestHeaderValueBug60451("fooébar"); + } + + @Test + public void testHeaderValueFullRange() { + for (int i = 0; i < 256; i++) { + // Skip the control characters except VTAB + if (i == 9 || i > 31 && i < 127 || i > 127) { + try { + doTestHeaderValueBug60451("foo" + Character.toString((char) i) + "bar"); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(e.getMessage() + "[" + i + "]"); + } + } + } + } + + @Test(expected = HpackException.class) + public void testExcessiveStringLiteralPadding() throws Exception { + MimeHeaders headers = new MimeHeaders(); + headers.setValue("X-test").setString("foobar"); + ByteBuffer output = ByteBuffer.allocate(512); + HpackEncoder encoder = new HpackEncoder(); + encoder.encode(headers, output); + // Hack the output buffer to extend the EOS marker for the header value + // by another byte + output.array()[7] = (byte) -122; + output.put((byte) -1); + output.flip(); + MimeHeaders headers2 = new MimeHeaders(); + HpackDecoder decoder = new HpackDecoder(); + decoder.setHeaderEmitter(new HeadersListener(headers2)); + decoder.decode(output); + } + + + private void doTestHeaderValueBug60451(String filename) throws HpackException { + String headerName = "Content-Disposition"; + String headerValue = "attachment;filename=\"" + filename + "\""; + MimeHeaders headers = new MimeHeaders(); + headers.setValue(headerName).setString(headerValue); + ByteBuffer output = ByteBuffer.allocate(512); + HpackEncoder encoder = new HpackEncoder(); + encoder.encode(headers, output); + output.flip(); + MimeHeaders headers2 = new MimeHeaders(); + HpackDecoder decoder = new HpackDecoder(); + decoder.setHeaderEmitter(new HeadersListener(headers2)); + decoder.decode(output); + Assert.assertEquals(headerValue, headers2.getHeader(headerName)); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2ConnectionTimeouts.java b/test/org/apache/coyote/http2/TestHttp2ConnectionTimeouts.java new file mode 100644 index 0000000..c18fa3b --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2ConnectionTimeouts.java @@ -0,0 +1,37 @@ +/* + * 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.coyote.http2; + +import org.junit.Test; + +public class TestHttp2ConnectionTimeouts extends Http2TestBase { + + @Test + public void testConnectionTimeout() throws Exception { + + // Reduce default timeouts so test completes sooner + enableHttp2(200, false, 5000, 5000, 10000, 5000, 5000); + configureAndStartWebApplication(); + openClientConnection(false); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + // Wait for timeout - should receive GoAway frame + handleGoAwayResponse(1, Http2Error.NO_ERROR); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2InitialConnection.java b/test/org/apache/coyote/http2/TestHttp2InitialConnection.java new file mode 100644 index 0000000..af62757 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2InitialConnection.java @@ -0,0 +1,164 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.util.ServerInfo; +import org.apache.catalina.valves.ErrorReportValve; +import org.apache.tomcat.util.res.StringManager; + +public class TestHttp2InitialConnection extends Http2TestBase { + + private TestData testData; + + + @Test + public void testValidHostHeader() throws Exception { + List hostHeaders = new ArrayList<>(1); + hostHeaders.add("localhost:8080"); + + testData = new TestData(hostHeaders, 200); + + http2Connect(); + } + + + @Test + public void testMultipleHostHeaders() throws Exception { + List hostHeaders = new ArrayList<>(1); + hostHeaders.add("localhost:8080"); + hostHeaders.add("localhost:8081"); + + testData = new TestData(hostHeaders, 400); + + http2Connect(); + } + + + @Test + public void testNoHostHeader() throws Exception { + List hostHeaders = new ArrayList<>(1); + + testData = new TestData(hostHeaders, 400); + http2Connect(); + } + + + @Override + protected void doHttpUpgrade(String connection, String upgrade, String settings, boolean validate) + throws IOException { + StringBuilder request = new StringBuilder(); + request.append("GET /simple HTTP/1.1\r\n"); + for (String hostHeader : testData.getHostHeaders()) { + request.append("Host: "); + request.append(hostHeader); + request.append("\r\n"); + } + // Connection + request.append("Connection: "); + request.append(connection); + request.append("\r\n"); + // Upgrade + request.append("Upgrade: "); + request.append(upgrade); + request.append("\r\n"); + // Settings + request.append(settings); + // Locale - Force the en Locale else the i18n on the error page changes + // the size of the response body and that triggers a failure as the test + // checks the exact response length + request.append("Accept-Language: en\r\n"); + // Request terminator + request.append("\r\n"); + + byte[] upgradeRequest = request.toString().getBytes(StandardCharsets.ISO_8859_1); + os.write(upgradeRequest); + os.flush(); + + if (validate) { + Assert.assertTrue("Failed to read HTTP Upgrade response", readHttpUpgradeResponse()); + } + } + + + @Override + protected String getResponseBodyFrameTrace(int streamId, String body) { + if (testData.getExpectedStatus() == 200) { + return super.getResponseBodyFrameTrace(streamId, body); + } else if (testData.getExpectedStatus() == 400) { + /* + * Need to be careful here. The test wants the exact content length in bytes. This will vary depending on + * where the test is run due to: - The length of the version string that appears once in the error page - + * The status header uses a UTF-8 EN dash. When running in an IDE the UTF-8 properties files will be used + * directly rather than after native2ascii conversion. + * + * Note: The status header appears twice in the error page. + */ + int serverInfoLength = ServerInfo.getServerInfo().getBytes().length; + StringManager sm = StringManager.getManager(ErrorReportValve.class.getPackage().getName(), Locale.ENGLISH); + String reason = sm.getString("http." + testData.getExpectedStatus() + ".reason"); + int descriptionLength = sm.getString("http." + testData.getExpectedStatus() + ".desc") + .getBytes(StandardCharsets.UTF_8).length; + int statusHeaderLength = sm + .getString("errorReportValve.statusHeader", String.valueOf(testData.getExpectedStatus()), reason) + .getBytes(StandardCharsets.UTF_8).length; + int typeLabelLength = sm.getString("errorReportValve.type").getBytes(StandardCharsets.UTF_8).length; + int statusReportLabelLength = sm.getString("errorReportValve.statusReport") + .getBytes(StandardCharsets.UTF_8).length; + int descriptionLabelLength = sm.getString("errorReportValve.description") + .getBytes(StandardCharsets.UTF_8).length; + // 196 bytes is the static length of the pure HTML code from the ErrorReportValve + int len = 196 + org.apache.catalina.util.TomcatCSS.TOMCAT_CSS.getBytes(StandardCharsets.UTF_8).length + + typeLabelLength + statusReportLabelLength + descriptionLabelLength + descriptionLength + + serverInfoLength + statusHeaderLength * 2; + String contentLength = String.valueOf(len); + return getResponseBodyFrameTrace(streamId, testData.getExpectedStatus(), "text/html;charset=utf-8", "en", + contentLength, contentLength); + } else { + Assert.fail(); + // To keep the IDE happy + return null; + } + } + + + private static class TestData { + private final List hostHeaders; + private final int expectedStatus; + + TestData(List hostHeaders, int expectedStatus) { + this.hostHeaders = hostHeaders; + this.expectedStatus = expectedStatus; + } + + public List getHostHeaders() { + return hostHeaders; + } + + public int getExpectedStatus() { + return expectedStatus; + } + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Limits.java b/test/org/apache/coyote/http2/TestHttp2Limits.java new file mode 100644 index 0000000..b184270 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Limits.java @@ -0,0 +1,597 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import org.hamcrest.Description; +import org.hamcrest.MatcherAssert; +import org.hamcrest.TypeSafeMatcher; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.coyote.http2.HpackEncoder.State; +import org.apache.tomcat.util.http.MimeHeaders; +import org.apache.tomcat.util.res.StringManager; + +public class TestHttp2Limits extends Http2TestBase { + + private static final StringManager sm = StringManager.getManager(TestHttp2Limits.class); + + + @Test + public void testSettingsOverheadLimits() throws Exception { + http2Connect(); + String errMsg = sm.getString("upgradeHandler.tooMuchOverhead", "\\p{XDigit}++").replace("[", "\\["); + String overHeadMsgRegx = "0-Goaway-\\[1]-\\[11]-\\[" + errMsg + "]"; + + for (int i = 0; i < 100; i++) { + try { + sendSettings(0, false); + parser.readFrame(); + } catch (IOException ioe) { + // Server closed connection before client has a chance to read + // the Goaway frame. Treat this as a pass. + return; + } + String trace = output.getTrace(); + if (trace.equals("0-Settings-Ack\n")) { + // Test continues + output.clearTrace(); + } else if (trace.matches(overHeadMsgRegx)) { + // Test passed + return; + } else { + // Test failed + Assert.fail("Unexpected output: " + output.getTrace()); + } + Thread.sleep(100); + } + + // Test failed + Assert.fail("Connection not closed down"); + } + + + @Test + public void testHeaderLimits1x128() throws Exception { + // Well within limits + doTestHeaderLimits(1, 128, FailureMode.NONE); + } + + + @Test + public void testHeaderLimits100x32() throws Exception { + // Just within default maxHeaderCount + // Note request has 4 standard headers + doTestHeaderLimits(96, 32, FailureMode.NONE); + } + + + @Test + public void testHeaderLimits101x32() throws Exception { + // Just above default maxHeaderCount + doTestHeaderLimits(97, 32, FailureMode.STREAM_RESET); + } + + + @Test + public void testHeaderLimits20x32WithLimit10() throws Exception { + // Check lower count limit is enforced + doTestHeaderLimits(20, 32, -1, 10, Constants.DEFAULT_MAX_HEADER_SIZE, 0, FailureMode.STREAM_RESET); + } + + + @Test + public void testHeaderLimits8x1144() throws Exception { + // Just within default maxHttpHeaderSize + // per header overhead plus standard 3 headers + doTestHeaderLimits(7, 1144, FailureMode.NONE); + } + + + @Test + public void testHeaderLimits8x1145() throws Exception { + // Just above default maxHttpHeaderSize + doTestHeaderLimits(7, 1145, FailureMode.STREAM_RESET); + } + + + @Test + public void testHeaderLimits3x1024WithLimit2048() throws Exception { + // Check lower size limit is enforced + doTestHeaderLimits(3, 1024, -1, Constants.DEFAULT_MAX_HEADER_COUNT, 2 * 1024, 0, FailureMode.STREAM_RESET); + } + + + @Test + public void testHeaderLimits1x12k() throws Exception { + // Bug 60232 + doTestHeaderLimits(1, 12 * 1024, FailureMode.STREAM_RESET); + } + + + @Test + public void testHeaderLimits1x12kin1kChunks() throws Exception { + // Bug 60232 + doTestHeaderLimits(1, 12 * 1024, 1024, FailureMode.STREAM_RESET); + } + + + @Test + public void testHeaderLimits1x12kin1kChunksThenNewRequest() throws Exception { + // Bug 60232 + doTestHeaderLimits(1, 12 * 1024, 1024, FailureMode.STREAM_RESET); + + output.clearTrace(); + sendSimpleGetRequest(5); + parser.readFrame(); + parser.readFrame(); + Assert.assertEquals(getSimpleResponseTrace(5), output.getTrace()); + } + + + @Test + public void testHeaderLimits1x32k() throws Exception { + // Bug 60232 + doTestHeaderLimits(1, 32 * 1024, FailureMode.CONNECTION_RESET); + } + + + @Test + public void testHeaderLimits1x32kin1kChunks() throws Exception { + // Bug 60232 + // 500ms per frame write delay to give server a chance to process the + // stream reset and the connection reset before the request is fully + // sent. + doTestHeaderLimits(1, 32 * 1024, 1024, 500, FailureMode.STREAM_RESET_THEN_CONNECTION_RESET); + } + + + @Test + public void testHeaderLimits1x128k() throws Exception { + // Bug 60232 + doTestHeaderLimits(1, 128 * 1024, FailureMode.CONNECTION_RESET); + } + + + @Test + public void testHeaderLimits1x512k() throws Exception { + // Bug 60232 + doTestHeaderLimits(1, 512 * 1024, FailureMode.CONNECTION_RESET); + } + + + @Test + public void testHeaderLimits10x512k() throws Exception { + // Bug 60232 + doTestHeaderLimits(10, 512 * 1024, FailureMode.CONNECTION_RESET); + } + + + private void doTestHeaderLimits(int headerCount, int headerSize, FailureMode failMode) throws Exception { + doTestHeaderLimits(headerCount, headerSize, -1, failMode); + } + + + private void doTestHeaderLimits(int headerCount, int headerSize, int maxHeaderPayloadSize, FailureMode failMode) + throws Exception { + doTestHeaderLimits(headerCount, headerSize, maxHeaderPayloadSize, 0, failMode); + } + + + private void doTestHeaderLimits(int headerCount, int headerSize, int maxHeaderPayloadSize, int delayms, + FailureMode failMode) throws Exception { + doTestHeaderLimits(headerCount, headerSize, maxHeaderPayloadSize, Constants.DEFAULT_MAX_HEADER_COUNT, + Constants.DEFAULT_MAX_HEADER_SIZE, delayms, failMode); + } + + + private void doTestHeaderLimits(int headerCount, int headerSize, int maxHeaderPayloadSize, int maxHeaderCount, + int maxHeaderSize, int delayms, FailureMode failMode) throws Exception { + + // Build the custom headers + List customHeaders = new ArrayList<>(); + StringBuilder headerValue = new StringBuilder(headerSize); + // Does not need to be secure + Random r = new Random(); + for (int i = 0; i < headerSize; i++) { + // Random lower case characters + headerValue.append((char) ('a' + r.nextInt(26))); + } + String v = headerValue.toString(); + for (int i = 0; i < headerCount; i++) { + customHeaders.add(new String[] { "X-TomcatTest" + i, v }); + } + + enableHttp2(); + + http2Protocol.setMaxHeaderCount(maxHeaderCount); + ((AbstractHttp11Protocol) http2Protocol.getHttp11Protocol()).setMaxHttpHeaderSize(maxHeaderSize); + + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + if (maxHeaderPayloadSize == -1) { + maxHeaderPayloadSize = output.getMaxFrameSize(); + } + + // Build the simple request + byte[] frameHeader = new byte[9]; + // Assumes at least one custom header and that all headers are the same + // length. These assumptions are valid for these tests. + ByteBuffer headersPayload = ByteBuffer + .allocate(200 + (int) (customHeaders.size() * customHeaders.iterator().next()[1].length() * 1.2)); + + populateHeadersPayload(headersPayload, customHeaders, "/simple"); + + Exception e = null; + try { + int written = 0; + int left = headersPayload.limit() - written; + while (left > 0) { + int thisTime = Math.min(left, maxHeaderPayloadSize); + populateFrameHeader(frameHeader, written, left, thisTime, 3); + writeFrame(frameHeader, headersPayload, headersPayload.limit() - left, thisTime, delayms); + left -= thisTime; + written += thisTime; + } + } catch (IOException ioe) { + e = ioe; + } + + switch (failMode) { + case NONE: { + // Expect a normal response + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + Assert.assertNull(e); + break; + } + case STREAM_RESET: { + // Expect a stream reset + parser.readFrame(); + Assert.assertEquals("3-RST-[11]\n", output.getTrace()); + Assert.assertNull(e); + break; + } + case STREAM_RESET_THEN_CONNECTION_RESET: { + // Expect a stream reset + // On some platform / Connector combinations the TCP connection close + // will be processed before the client gets a chance to read the + // connection close frame which will trigger an + // IOException when we try to read the frame. + try { + parser.readFrame(); + Assert.assertEquals("3-RST-[11]\n", output.getTrace()); + output.clearTrace(); + } catch (IOException ioe) { + // Expected on some platforms + } + } + //$FALL-THROUGH$ + case CONNECTION_RESET: { + // This message uses i18n and needs to be used in a regular + // expression (since we don't know the connection ID). Generate the + // string as a regular expression and then replace '[' and ']' with + // the escaped values. + String limitMessage = sm.getString("http2Parser.headerLimitSize", "\\p{XDigit}++", "3"); + limitMessage = limitMessage.replace("[", "\\[").replace("]", "\\]"); + // Connection reset. Connection ID will vary so use a pattern + // On some platform / Connector combinations the TCP connection close + // will be processed before the client gets a chance to read the + // connection close frame which will trigger an + // IOException when we try to read the frame. + // Note: Some platforms will allow the read if if the write fails + // above. + try { + parser.readFrame(); + MatcherAssert.assertThat(output.getTrace(), + RegexMatcher.matchesRegex("0-Goaway-\\[1\\]-\\[11\\]-\\[" + limitMessage + "\\]")); + } catch (IOException se) { + // Expected on some platforms + } + break; + } + } + } + + + private void populateHeadersPayload(ByteBuffer headersPayload, List customHeaders, String path) + throws Exception { + MimeHeaders headers = new MimeHeaders(); + headers.addValue(":method").setString("GET"); + headers.addValue(":scheme").setString("http"); + headers.addValue(":path").setString(path); + headers.addValue(":authority").setString("localhost:" + getPort()); + for (String[] customHeader : customHeaders) { + headers.addValue(customHeader[0]).setString(customHeader[1]); + } + State state = hpackEncoder.encode(headers, headersPayload); + if (state != State.COMPLETE) { + throw new Exception("Unable to build headers"); + } + headersPayload.flip(); + + log.debug("Headers payload generated of size [" + headersPayload.limit() + "]"); + } + + + private void populateFrameHeader(byte[] frameHeader, int written, int left, int thisTime, int streamId) + throws Exception { + ByteUtil.setThreeBytes(frameHeader, 0, thisTime); + if (written == 0) { + frameHeader[3] = FrameType.HEADERS.getIdByte(); + // Flags. End of stream + frameHeader[4] = 0x01; + } else { + frameHeader[3] = FrameType.CONTINUATION.getIdByte(); + } + if (left == thisTime) { + // Flags. End of headers + frameHeader[4] = (byte) (frameHeader[4] + 0x04); + } + + // Stream id + ByteUtil.set31Bits(frameHeader, 5, streamId); + } + + + @Test + public void testCookieLimit1() throws Exception { + doTestCookieLimit(1, 0); + } + + + @Test + public void testCookieLimit2() throws Exception { + doTestCookieLimit(2, 0); + } + + + @Test + public void testCookieLimit100() throws Exception { + doTestCookieLimit(100, 0); + } + + + @Test + public void testCookieLimit100WithLimit50() throws Exception { + doTestCookieLimit(100, 50, 1); + } + + + @Test + public void testCookieLimit200() throws Exception { + doTestCookieLimit(200, 0); + } + + + @Test + public void testCookieLimit201() throws Exception { + doTestCookieLimit(201, 1); + } + + + private void doTestCookieLimit(int cookieCount, int failMode) throws Exception { + doTestCookieLimit(cookieCount, Constants.DEFAULT_MAX_COOKIE_COUNT, failMode); + } + + + private void doTestCookieLimit(int cookieCount, int maxCookieCount, int failMode) throws Exception { + + enableHttp2(); + + Connector connector = getTomcatInstance().getConnector(); + connector.setMaxCookieCount(maxCookieCount); + + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + output.setTraceBody(true); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(8192); + + List customHeaders = new ArrayList<>(); + for (int i = 0; i < cookieCount; i++) { + customHeaders.add(new String[] { "Cookie", "a" + cookieCount + "=b" + cookieCount }); + } + + populateHeadersPayload(headersPayload, customHeaders, "/cookie"); + populateFrameHeader(frameHeader, 0, headersPayload.limit(), headersPayload.limit(), 3); + + writeFrame(frameHeader, headersPayload); + + switch (failMode) { + case 0: { + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + System.out.println(output.getTrace()); + Assert.assertEquals(getCookieResponseTrace(3, cookieCount), output.getTrace()); + break; + } + case 1: { + // Check status is 400 + parser.readFrame(); + Assert.assertTrue(output.getTrace(), + output.getTrace().startsWith("3-HeadersStart\n3-Header-[:status]-[400]")); + output.clearTrace(); + // Check EOS followed by error page body + parser.readFrame(); + Assert.assertTrue(output.getTrace(), output.getTrace().startsWith("3-EndOfStream\n3-Body-) http2Protocol.getHttp11Protocol()).setAllowedTrailerHeaders(TRAILER_HEADER_NAME); + http2Protocol.setMaxTrailerCount(maxTrailerCount); + ((AbstractHttp11Protocol) http2Protocol.getHttp11Protocol()).setMaxTrailerSize(maxTrailerSize); + // Disable overhead protection for window update as it breaks some tests + http2Protocol.setOverheadWindowUpdateThreshold(0); + + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(256); + byte[] trailerFrameHeader = new byte[9]; + ByteBuffer trailerPayload = ByteBuffer.allocate(256); + + buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload, null, + trailerFrameHeader, trailerPayload, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + // Body + writeFrame(dataFrameHeader, dataPayload); + // Trailers + writeFrame(trailerFrameHeader, trailerPayload); + + switch (failMode) { + case NONE: { + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + String len = Integer.toString(256 + TRAILER_HEADER_VALUE.length()); + + Assert.assertEquals("0-WindowSize-[256]\n" + "3-WindowSize-[256]\n" + "3-HeadersStart\n" + + "3-Header-[:status]-[200]\n" + "3-Header-[content-length]-[" + len + "]\n" + + "3-Header-[date]-[" + DEFAULT_DATE + "]\n" + "3-HeadersEnd\n" + "3-Body-" + len + "\n" + + "3-EndOfStream\n", output.getTrace()); + break; + } + case STREAM_RESET: { + // NIO2 can sometimes send window updates depending timing + skipWindowSizeFrames(); + + // Async I/O can sometimes result in a stream closed reset before + // the enhance your calm reset + if ("3-RST-[5]\n".equals(output.getTrace())) { + output.clearTrace(); + parser.readFrame(); + } + + Assert.assertEquals("3-RST-[11]\n", output.getTrace()); + break; + } + case STREAM_RESET_THEN_CONNECTION_RESET: { + Assert.fail("Not used"); + break; + } + case CONNECTION_RESET: { + // NIO2 can sometimes send window updates depending timing + skipWindowSizeFrames(); + + // This message uses i18n and needs to be used in a regular + // expression (since we don't know the connection ID). Generate the + // string as a regular expression and then replace '[' and ']' with + // the escaped values. + String limitMessage = sm.getString("http2Parser.headerLimitSize", "\\p{XDigit}++", "3"); + limitMessage = limitMessage.replace("[", "\\[").replace("]", "\\]"); + MatcherAssert.assertThat(output.getTrace(), + RegexMatcher.matchesRegex("0-Goaway-\\[3\\]-\\[11\\]-\\[" + limitMessage + "\\]")); + break; + } + } + } + + + private enum FailureMode { + NONE, + STREAM_RESET, + CONNECTION_RESET, + STREAM_RESET_THEN_CONNECTION_RESET, + } + + + private static class RegexMatcher extends TypeSafeMatcher { + + private final String pattern; + + + RegexMatcher(String pattern) { + this.pattern = pattern; + } + + + @Override + public void describeTo(Description description) { + description.appendText("match to regular expression pattern [" + pattern + "]"); + + } + + @Override + protected boolean matchesSafely(String item) { + return item.matches(pattern); + } + + + public static RegexMatcher matchesRegex(String pattern) { + return new RegexMatcher(pattern); + } + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_3_2.java b/test/org/apache/coyote/http2/TestHttp2Section_3_2.java new file mode 100644 index 0000000..fa1c15b --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_3_2.java @@ -0,0 +1,173 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +/** + * Unit tests for Section 3.2 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_3_2 extends Http2TestBase { + + // Note: Tests for zero/multiple HTTP2-Settings fields can be found below + // in the tests for section 3.2.1 + + // TODO: Test initial requests with bodies of various sizes + + @Test + public void testConnectionNoHttp2Support() throws Exception { + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2c", EMPTY_HTTP2_SETTINGS_HEADER, false); + parseHttp11Response(); + } + + + @Test + public void testConnectionUpgradeWrongProtocol() throws Exception { + enableHttp2(); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2", EMPTY_HTTP2_SETTINGS_HEADER, false); + parseHttp11Response(); + } + + + @Test(timeout = 10000) + public void testConnectionNoPreface() throws Exception { + setupAsFarAsUpgrade(); + + // If we don't send the preface the server should kill the connection. + try { + // Make the parser read something. + parser.readFrame(); + } catch (IOException ioe) { + // Expected because the server is going to drop the connection. + } + } + + + @Test(timeout = 10000) + public void testConnectionIncompletePrefaceStart() throws Exception { + setupAsFarAsUpgrade(); + + // If we send an incomplete preface the server should kill the + // connection. + os.write("PRI * HTTP/2.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1)); + os.flush(); + try { + // Make the parser read something. + parser.readFrame(); + } catch (IOException ioe) { + // Expected because the server is going to drop the connection. + } + } + + + @Test(timeout = 10000) + public void testConnectionInvalidPrefaceStart() throws Exception { + setupAsFarAsUpgrade(); + + // If we send an incomplete preface the server should kill the + // connection. + os.write("xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxxx".getBytes(StandardCharsets.ISO_8859_1)); + os.flush(); + try { + // Make the parser read something. + parser.readFrame(); + } catch (IOException ioe) { + // Expected because the server is going to drop the connection. + } + } + + + @Test + public void testConnectionUpgradeFirstResponse() throws Exception { + super.http2Connect(); + } + + + private void setupAsFarAsUpgrade() throws Exception { + enableHttp2(); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + } + + + // ------------------------------------------------------------ Section 3.2.1 + + @Test + public void testZeroHttp2Settings() throws Exception { + enableHttp2(); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2c", "", false); + parseHttp11Response(); + } + + + @Test + public void testMultipleHttp2Settings() throws Exception { + enableHttp2(); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2c", EMPTY_HTTP2_SETTINGS_HEADER + EMPTY_HTTP2_SETTINGS_HEADER, + false); + parseHttp11Response(); + } + + + @Test + public void testMissingConnectionValue() throws Exception { + enableHttp2(); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade("Upgrade", "h2c", EMPTY_HTTP2_SETTINGS_HEADER, false); + parseHttp11Response(); + } + + + @Test + public void testSplitConnectionValue01() throws Exception { + enableHttp2(); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade("Upgrade\r\nConnection: HTTP2-Settings", "h2c", EMPTY_HTTP2_SETTINGS_HEADER, true); + sendClientPreface(); + validateHttp2InitialResponse(); + } + + + @Test + public void testSplitConnectionValue02() throws Exception { + enableHttp2(); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade("HTTP2-Settings\r\nConnection: Upgrade", "h2c", EMPTY_HTTP2_SETTINGS_HEADER, true); + sendClientPreface(); + validateHttp2InitialResponse(); + } + + // No need to test how trailing '=' are handled here. HTTP2Settings payloads + // are always a multiple of 6 long which means valid payloads never end in + // '='. Invalid payloads will be rejected anyway. +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_3_5.java b/test/org/apache/coyote/http2/TestHttp2Section_3_5.java new file mode 100644 index 0000000..4f88571 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_3_5.java @@ -0,0 +1,67 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +public class TestHttp2Section_3_5 extends Http2TestBase { + + @Test + public void testNoConnectionPreface() throws Exception { + enableHttp2(); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + + // Server settings + parser.readFrame(); + Assert.assertEquals("0-Settings-[3]-[200]\n" + "0-Settings-End\n", output.getTrace()); + output.clearTrace(); + + // Should send client preface here. This will trigger an error. + // Send two pings (2*(9+8)=34 bytes) as server looks for entire preface + // of 24 bytes. + sendPing(); + // Depending on timing, this ping may fail after the header has been + // sent but before the ping body since: + // 9 (ping 1 header) + 8 (ping 1 body) + 9 (ping 2 header) = 26 which + // which is enough data for the server to determine that the preface is + // invalid and close the connection. A subsequent attempt to send ping 2 + // body will fail. + try { + sendPing(); + } catch (IOException e) { + // Ignore + } + + // If the client preface had been valid, this would be an + // acknowledgement. Of the settings. As the preface was invalid, it + // should be a GOAWAY frame. + try { + parser.readFrame(); + Assert.assertTrue(output.getTrace(), output.getTrace().startsWith("0-Goaway-[1]-[1]-[")); + } catch (IOException ioe) { + // Ignore + // This is expected on some platforms and depends on timing. Once + // the server closes the connection the client may see an exception + // when trying to read the GOAWAY frame. + } + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_4_1.java b/test/org/apache/coyote/http2/TestHttp2Section_4_1.java new file mode 100644 index 0000000..35872be --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_4_1.java @@ -0,0 +1,68 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 4.1 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_4_1 extends Http2TestBase { + + private static final byte[] UNKNOWN_FRAME = new byte[] { 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + // TODO: Tests for over-sized frames. Better located in tests for section 6? + + + @Test + public void testUnknownFrameType() throws Exception { + http2Connect(); + os.write(UNKNOWN_FRAME); + os.flush(); + sendSimpleGetRequest(3); + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + } + + + // TODO: Tests for unexpected flags. Better located in tests for section 6? + + + @Test + public void testReservedBitIgnored() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Build the simple request + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequest(frameHeader, headersPayload, null, 3); + + // Tweak the header to set the reserved bit + frameHeader[5] = (byte) (frameHeader[5] | 0x80); + + // Process the request + writeFrame(frameHeader, headersPayload); + + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_4_2.java b/test/org/apache/coyote/http2/TestHttp2Section_4_2.java new file mode 100644 index 0000000..d73a249 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_4_2.java @@ -0,0 +1,131 @@ +/* + * 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.coyote.http2; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 4.2 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_4_2 extends Http2TestBase { + + @Test + public void testFrameSizeLimitsTooBig() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Overly large settings + // Settings have to be a multiple of six + int settingsCount = (ConnectionSettingsBase.DEFAULT_MAX_FRAME_SIZE / 6) + 1; + int size = settingsCount * 6; + byte[] settings = new byte[size + 9]; + // Header + // Length + ByteUtil.setThreeBytes(settings, 0, size); + // Type + settings[3] = FrameType.SETTINGS.getIdByte(); + // No flags + // Stream 0 + + // payload + for (int i = 0; i < settingsCount; i++) { + // Enable server push over and over again + ByteUtil.setTwoBytes(settings, (i * 6) + 9, 2); + ByteUtil.setFourBytes(settings, (i * 6) + 9 + 2, 1); + } + + os.write(settings); + os.flush(); + + handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR); + } + + @Test + public void testFrameTypeLimitsTooBig() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Overly large ping + byte[] ping = new byte[109]; + + // Header + // Length + ByteUtil.setThreeBytes(ping, 0, 100); + // Type + ping[3] = FrameType.PING.getIdByte(); + // No flags + // Stream 0 + // Empty payload + + os.write(ping); + os.flush(); + + handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR); + } + + + @Test + public void testFrameTypeLimitsTooSmall() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Too small ping + byte[] ping = new byte[9]; + + // Header + // Length 0 + // Type + ping[3] = FrameType.PING.getIdByte(); + // No flags + // Stream 0 + // Empty payload + + os.write(ping); + os.flush(); + + handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR); + } + + + @Test + public void testFrameTypeLimitsStream() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Invalid priority + byte[] priority = new byte[9]; + + // Header + // Length 0 + // Type + priority[3] = FrameType.PRIORITY.getIdByte(); + // No flags + // Stream 3 + ByteUtil.set31Bits(priority, 5, 3); + // Empty payload + + os.write(priority); + os.flush(); + + // Read Stream reset frame + parser.readFrame(); + + Assert.assertTrue(output.getTrace(), output.getTrace().startsWith("3-RST-[6]")); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_4_3.java b/test/org/apache/coyote/http2/TestHttp2Section_4_3.java new file mode 100644 index 0000000..772c26f --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_4_3.java @@ -0,0 +1,89 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 4.3 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_4_3 extends Http2TestBase { + + @Test + public void testHeaderDecodingError() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Build the simple request + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequest(frameHeader, headersPayload, null, 3); + + // Try and corrupt the headerPayload + headersPayload.put(0, (byte) (headersPayload.get(0) + 128)); + + // Process the request + writeFrame(frameHeader, headersPayload); + + handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR); + } + + + @Test + public void testHeaderContinuationContiguous() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Part 1 + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequestPart1(frameHeader, headersPayload, 3); + writeFrame(frameHeader, headersPayload); + + // Part 2 + headersPayload.clear(); + buildSimpleGetRequestPart2(frameHeader, headersPayload, 3); + writeFrame(frameHeader, headersPayload); + + // headers, body + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + } + + + @Test + public void testHeaderContinuationNonContiguous() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Part 1 + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequestPart1(frameHeader, headersPayload, 3); + writeFrame(frameHeader, headersPayload); + + sendPing(); + + handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_5_1.java b/test/org/apache/coyote/http2/TestHttp2Section_5_1.java new file mode 100644 index 0000000..cbe224c --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_5_1.java @@ -0,0 +1,434 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.LogManager; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 5.§ of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_5_1 extends Http2TestBase { + + @Test + public void testIdleStateInvalidFrame01() throws Exception { + http2Connect(); + + sendWindowUpdate(3, 200); + + handleGoAwayResponse(1); + } + + + @Test + public void testIdleStateInvalidFrame02() throws Exception { + http2Connect(); + + sendData(3, new byte[] {}); + + handleGoAwayResponse(1); + } + + + // TODO: reserved local + // TODO: reserved remote + + + @Test + public void halfClosedRemoteInvalidFrame() throws Exception { + http2Connect(); + + // This half-closes the stream since it includes the end of stream flag + sendSimpleGetRequest(3); + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + output.clearTrace(); + + // This should trigger a connection error + sendData(3, new byte[] {}); + + handleGoAwayResponse(3, Http2Error.STREAM_CLOSED); + } + + + @Test + public void testClosedInvalidFrame01() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Build the simple request + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequest(frameHeader, headersPayload, null, 3); + + // Remove the end of stream and end of headers flags + frameHeader[4] = 0; + + // Process the request + writeFrame(frameHeader, headersPayload); + + // Send a rst + sendRst(3, Http2Error.INTERNAL_ERROR.getCode()); + + // Then try sending some data (which should fail) + sendData(3, new byte[] {}); + parser.readFrame(); + + Assert.assertTrue(output.getTrace(), + output.getTrace().startsWith("3-RST-[" + Http2Error.STREAM_CLOSED.getCode() + "]")); + } + + + @Test + public void testClosedInvalidFrame02() throws Exception { + http2Connect(); + + // Stream 1 is closed. This should trigger a connection error + sendData(1, new byte[] {}); + + handleGoAwayResponse(1, Http2Error.STREAM_CLOSED); + } + + + // TODO: Invalid frames for each of the remaining states + + // Section 5.1.1 + + @Test + public void testClientSendEvenStream() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Part 1 + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequestPart1(frameHeader, headersPayload, 4); + writeFrame(frameHeader, headersPayload); + + handleGoAwayResponse(1); + } + + + @Test + public void testClientSendOldStream() throws Exception { + http2Connect(); + sendSimpleGetRequest(5); + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(5), output.getTrace()); + output.clearTrace(); + + + // Build the simple request on an old stream + sendSimpleGetRequest(3); + + handleGoAwayResponse(5); + } + + + @Test + public void testImplicitClose() throws Exception { + doTestImplicitClose(5); + } + + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=64467 + @Test + public void testImplicitCloseLargeId() throws Exception { + doTestImplicitClose(Integer.MAX_VALUE - 8); + } + + + private void doTestImplicitClose(int lastStreamId) throws Exception { + + long startFirst = System.nanoTime(); + http2Connect(); + long durationFirst = System.nanoTime() - startFirst; + + sendPriority(3, 0, 16); + sendPriority(lastStreamId, 0, 16); + + long startSecond = System.nanoTime(); + sendSimpleGetRequest(lastStreamId); + readSimpleGetResponse(); + long durationSecond = System.nanoTime() - startSecond; + + Assert.assertEquals(getSimpleResponseTrace(lastStreamId), output.getTrace()); + output.clearTrace(); + + // Allow second request to take up to 5 times first request or up to 1 second - whichever is the larger - mainly + // to allow for CI systems under load that can exhibit significant timing variation. + Assert.assertTrue( + "First request took [" + durationFirst / 1000000 + "ms], second request took [" + + durationSecond / 1000000 + "ms]", + durationSecond < 1000000000 || durationSecond < durationFirst * 3); + + // Should trigger an error since stream 3 should have been implicitly + // closed. + sendSimpleGetRequest(3); + + handleGoAwayResponse(lastStreamId); + } + + + @Test + public void testExceedMaxActiveStreams01() throws Exception { + // http2Connect() - modified + enableHttp2(1); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + + // validateHttp2InitialResponse() - modified + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals("0-Settings-[3]-[1]\n" + "0-Settings-End\n" + "0-Settings-Ack\n" + + "0-Ping-[0,0,0,0,0,0,0,1]\n" + getSimpleResponseTrace(1), output.getTrace()); + output.clearTrace(); + + sendLargeGetRequest(3); + + sendSimpleGetRequest(5); + + // Default connection window size is 64k-1. + // Initial request will have used 8k leaving 56k-1. + // Stream window will be 64k-1. + // Expecting + // 1 * headers + // 56k-1 of body (7 * ~8k) + // 1 * error + // for a total of 9 frames (could be in any order) + for (int i = 0; i < 9; i++) { + parser.readFrame(); + } + + Assert.assertTrue(output.getTrace(), + output.getTrace().contains("5-RST-[" + Http2Error.REFUSED_STREAM.getCode() + "]")); + output.clearTrace(); + + // Connection window is zero. + // Stream window is 8k + + // Release the remaining body + sendWindowUpdate(0, (1 << 31) - 2); + // Allow for the ~8k still in the stream window + sendWindowUpdate(3, (1 << 31) - 8193); + + // Read until the end of stream 3 + while (!output.getTrace().contains("3-EndOfStream")) { + parser.readFrame(); + } + output.clearTrace(); + + // Confirm another request can be sent once concurrency falls back below limit + sendSimpleGetRequest(7); + parser.readFrame(); + parser.readFrame(); + Assert.assertEquals(getSimpleResponseTrace(7), output.getTrace()); + } + + + @Test + public void testExceedMaxActiveStreams02() throws Exception { + // http2Connect() - modified + enableHttp2(1); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + + // validateHttp2InitialResponse() - modified + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals("0-Settings-[3]-[1]\n" + "0-Settings-End\n" + "0-Settings-Ack\n" + + "0-Ping-[0,0,0,0,0,0,0,1]\n" + getSimpleResponseTrace(1), output.getTrace()); + output.clearTrace(); + + sendLargeGetRequest(3); + + sendSimpleGetRequest(5); + + // Default connection window size is 64k-1. + // Initial request will have used 8k leaving 56k-1. + // Stream window will be 64k-1. + // Expecting + // 1 * headers + // 56k-1 of body (7 * ~8k) + // 1 * error + // for a total of 9 frames (could be in any order) + for (int i = 0; i < 9; i++) { + parser.readFrame(); + } + + Assert.assertTrue(output.getTrace(), + output.getTrace().contains("5-RST-[" + Http2Error.REFUSED_STREAM.getCode() + "]")); + output.clearTrace(); + + // Connection window is zero. + // Stream window is 8k + + // Reset stream 3 (client cancel) + sendRst(3, Http2Error.NO_ERROR.getCode()); + // Client reset triggers a write error which in turn triggers a server + // reset + parser.readFrame(); + Assert.assertEquals("3-RST-[5]\n", output.getTrace()); + output.clearTrace(); + + // Open up the connection window. + sendWindowUpdate(0, (1 << 31) - 2); + + // Confirm another request can be sent once concurrency falls back below limit + sendSimpleGetRequest(7); + parser.readFrame(); + parser.readFrame(); + Assert.assertEquals(getSimpleResponseTrace(7), output.getTrace()); + } + + + @Test + public void testErrorOnWaitingStream01() throws Exception { + LogManager.getLogManager().getLogger("org.apache.coyote.http2").setLevel(Level.ALL); + try { + // http2Connect() - modified + enableHttp2(1); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + + // validateHttp2InitialResponse() - modified + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals("0-Settings-[3]-[1]\n" + "0-Settings-End\n" + "0-Settings-Ack\n" + + "0-Ping-[0,0,0,0,0,0,0,1]\n" + getSimpleResponseTrace(1), output.getTrace()); + output.clearTrace(); + + sendLargeGetRequest(3); + + sendSimpleGetRequest(5); + + // Default connection window size is 64k-1. + // Initial request will have used 8k leaving 56k-1. + // Stream window will be 64k-1. + // Expecting + // 1 * headers + // 56k-1 of body (7 * ~8k) + // 1 * error (could be in any order) + for (int i = 0; i < 8; i++) { + parser.readFrame(); + } + parser.readFrame(); + + Assert.assertTrue(output.getTrace(), + output.getTrace().contains("5-RST-[" + Http2Error.REFUSED_STREAM.getCode() + "]")); + output.clearTrace(); + + // Connection window is zero. + // Stream window is 8k + + // Expand the stream window too much to trigger an error + // Allow for the 8k still in the stream window + sendWindowUpdate(3, (1 << 31) - 1); + + parser.readFrame(); + Assert.assertEquals("3-RST-[" + Http2Error.FLOW_CONTROL_ERROR.getCode() + "]\n", output.getTrace()); + } finally { + LogManager.getLogManager().getLogger("org.apache.coyote.http2").setLevel(Level.INFO); + } + } + + + @Test + public void testErrorOnWaitingStream02() throws Exception { + // http2Connect() - modified + enableHttp2(1); + configureAndStartWebApplication(); + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + + // validateHttp2InitialResponse() - modified + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals("0-Settings-[3]-[1]\n" + "0-Settings-End\n" + "0-Settings-Ack\n" + + "0-Ping-[0,0,0,0,0,0,0,1]\n" + getSimpleResponseTrace(1), output.getTrace()); + output.clearTrace(); + + // Default connection window size is 64k-1. + // Initial request will have used 8k leaving 56k-1. + // Stream window will be 64k-1. + + // Increase Connection window by 16k + // Do this before sending the requests to ensure the connection window + // is increased before request processing starts else stream 5 may + // consume the connection window before the update is processed which + // will result in at least one addition body frame and break the tests + // below. + sendWindowUpdate(0, 16 * 1024); + + sendLargeGetRequest(3); + + sendSimpleGetRequest(5); + + // Expecting + // 1 * headers + // 64k-1 of body (8 * ~8k) + // 1 * error + // Could be in any order + // + for (int i = 0; i < 9; i++) { + parser.readFrame(); + } + parser.readFrame(); + + Assert.assertTrue(output.getTrace(), + output.getTrace().contains("5-RST-[" + Http2Error.REFUSED_STREAM.getCode() + "]")); + + // Connection window is 8k. + // Stream window is zero. + + // Expand the connection window too much to trigger an error + // Allow for the 8k still in the connection window + sendWindowUpdate(0, (1 << 31) - 1); + + parser.readFrame(); + Assert.assertTrue(output.getTrace(), + output.getTrace().contains("0-Goaway-[5]-[" + Http2Error.FLOW_CONTROL_ERROR.getCode() + "]")); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_5_2.java b/test/org/apache/coyote/http2/TestHttp2Section_5_2.java new file mode 100644 index 0000000..fbdd148 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_5_2.java @@ -0,0 +1,117 @@ +/* + * 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.coyote.http2; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for Section 5.2 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_5_2 extends Http2TestBase { + + /* + * Get the connection to a point where 1k of 8k response body has been read and the flow control for the stream has + * no capacity left. + */ + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + http2Connect(); + + // This test uses small window updates that will trigger the excessive + // overhead protection so disable it. + http2Protocol.setOverheadWindowUpdateThreshold(0); + + // Set the default window size to 1024 bytes + sendSettings(0, false, new SettingValue(4, 1024)); + // Wait for the ack + parser.readFrame(); + output.clearTrace(); + + // Headers + 8k response + sendSimpleGetRequest(3); + + // Headers + parser.readFrame(); + // First 1k of body + parser.readFrame(); + output.clearTrace(); + } + + + @Test + public void testFlowControlLimits01() throws Exception { + readBytes(20); + clearRemainder(); + } + + + @Test + public void testFlowControlLimits02() throws Exception { + readBytes(1); + readBytes(1); + readBytes(1024); + readBytes(1); + clearRemainder(); + } + + + @Test + public void testFlowControlLimits03() throws Exception { + readBytes(8192, 7168); + } + + + @Test + public void testFlowControlLimits04() throws Exception { + readBytes(7168, 7168, true); + } + + + private void readBytes(int len) throws Exception { + readBytes(len, len); + } + + + private void readBytes(int len, int expected) throws Exception { + readBytes(len, expected, len > expected); + } + + + private void readBytes(int len, int expected, boolean eos) throws Exception { + sendWindowUpdate(3, len); + parser.readFrame(); + String expectedTrace = "3-Body-" + expected + "\n"; + if (eos) { + expectedTrace += "3-EndOfStream\n"; + } + Assert.assertEquals(expectedTrace, output.getTrace()); + output.clearTrace(); + } + + + private void clearRemainder() throws Exception { + // Remainder + sendWindowUpdate(3, 8192); + parser.readFrame(); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_5_5.java b/test/org/apache/coyote/http2/TestHttp2Section_5_5.java new file mode 100644 index 0000000..c383a2e --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_5_5.java @@ -0,0 +1,93 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 5.5 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_5_5 extends Http2TestBase { + + private static final byte[] UNKNOWN_FRAME; + + static { + // Unknown frame type + UNKNOWN_FRAME = new byte[29]; + // Frame header + ByteUtil.setThreeBytes(UNKNOWN_FRAME, 0, 20); + // Type + UNKNOWN_FRAME[3] = (byte) 0x80; + // No flags + // Stream + ByteUtil.set31Bits(UNKNOWN_FRAME, 5, 5); + // zero payload + } + + + // Section 5.5 + + @Test + public void testUnknownSetting() throws Exception { + http2Connect(); + + // Unknown setting (should be ack'd) + sendSettings(0, false, new SettingValue(1 << 15, 0)); + + parser.readFrame(); + + Assert.assertEquals("0-Settings-Ack\n", output.getTrace()); + } + + + @Test + public void testUnknownFrame() throws Exception { + http2Connect(); + + os.write(UNKNOWN_FRAME); + os.flush(); + + // Ping + sendPing(); + + parser.readFrame(); + + Assert.assertEquals("0-Ping-Ack-[0,0,0,0,0,0,0,0]\n", output.getTrace()); + } + + + @Test + public void testNonContiguousHeaderWithUnknownFrame() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Part 1 + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequestPart1(frameHeader, headersPayload, 3); + writeFrame(frameHeader, headersPayload); + + os.write(UNKNOWN_FRAME); + os.flush(); + + handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_1.java b/test/org/apache/coyote/http2/TestHttp2Section_6_1.java new file mode 100644 index 0000000..bd064ae --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_1.java @@ -0,0 +1,187 @@ +/* + * 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.coyote.http2; + +import java.util.logging.Level; +import java.util.logging.LogManager; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 6.1 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_6_1 extends Http2TestBase { + + @Test + public void testDataFrame() throws Exception { + http2Connect(); + + // Disable overhead protection for window update as it breaks the test + http2Protocol.setOverheadWindowUpdateThreshold(0); + + sendSimplePostRequest(3, null); + readSimplePostResponse(false); + + Assert.assertEquals( + "0-WindowSize-[128]\n" + "3-WindowSize-[128]\n" + "3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + + "3-Header-[content-length]-[128]\n" + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + + "3-HeadersEnd\n" + "3-Body-128\n" + "3-EndOfStream\n", + output.getTrace()); + } + + + @Test + public void testDataFrameWithPadding() throws Exception { + LogManager.getLogManager().getLogger("org.apache.coyote").setLevel(Level.ALL); + LogManager.getLogManager().getLogger("org.apache.tomcat.util.net").setLevel(Level.ALL); + try { + http2Connect(); + + // Disable overhead protection for window update as it breaks the + // test + http2Protocol.setOverheadWindowUpdateThreshold(0); + + byte[] padding = new byte[8]; + + sendSimplePostRequest(3, padding); + readSimplePostResponse(true); + + // The window updates for padding could occur anywhere since they + // happen on a different thread to the response. + // The connection window update is always present if there is + // padding. + String trace = output.getTrace(); + String paddingWindowUpdate = "0-WindowSize-[9]\n"; + Assert.assertTrue(trace, trace.contains(paddingWindowUpdate)); + trace = trace.replace(paddingWindowUpdate, ""); + + // The stream window update may or may not be present depending on + // timing. Remove it if present. + if (trace.contains("3-WindowSize-[9]\n")) { + trace = trace.replace("3-WindowSize-[9]\n", ""); + } + + Assert.assertEquals("0-WindowSize-[119]\n" + "3-WindowSize-[119]\n" + "3-HeadersStart\n" + + "3-Header-[:status]-[200]\n" + "3-Header-[content-length]-[119]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n" + "3-Body-119\n" + + "3-EndOfStream\n", trace); + } finally { + LogManager.getLogManager().getLogger("org.apache.coyote").setLevel(Level.INFO); + LogManager.getLogManager().getLogger("org.apache.tomcat.util.net").setLevel(Level.INFO); + } + } + + + @Test + public void testDataFrameWithNonZeroPadding() throws Exception { + http2Connect(); + + byte[] padding = new byte[8]; + padding[4] = 0x01; + + sendSimplePostRequest(3, padding); + + // May see Window updates depending on timing + skipWindowSizeFrames(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.startsWith("0-Goaway-[3]-[1]-[")); + } + + + @Test + public void testDataFrameOnStreamZero() throws Exception { + http2Connect(); + + byte[] dataFrame = new byte[10]; + + // Header + // length + ByteUtil.setThreeBytes(dataFrame, 0, 1); + // type (0 for data) + // flags (0) + // stream (0) + // payload (0) + + os.write(dataFrame); + os.flush(); + + handleGoAwayResponse(1); + } + + + @Test + public void testDataFrameTooMuchPadding() throws Exception { + http2Connect(); + + byte[] dataFrame = new byte[10]; + + // Header + // length + ByteUtil.setThreeBytes(dataFrame, 0, 1); + // type 0 (data) + // flags 8 (padded) + dataFrame[4] = 0x08; + // stream 3 + ByteUtil.set31Bits(dataFrame, 5, 3); + // payload (pad length of 1) + dataFrame[9] = 1; + + os.write(dataFrame); + os.flush(); + + handleGoAwayResponse(1); + } + + + @Test + public void testDataFrameWithZeroLengthPadding() throws Exception { + http2Connect(); + + // Disable overhead protection for window update as it breaks the test + http2Protocol.setOverheadWindowUpdateThreshold(0); + + byte[] padding = new byte[0]; + + sendSimplePostRequest(3, padding); + readSimplePostResponse(true); + + // The window updates for padding could occur anywhere since they + // happen on a different thread to the response. + // The connection window update is always present if there is + // padding. + String trace = output.getTrace(); + String paddingWindowUpdate = "0-WindowSize-[1]\n"; + Assert.assertTrue(trace, trace.contains(paddingWindowUpdate)); + trace = trace.replace(paddingWindowUpdate, ""); + + // The stream window update may or may not be present depending on + // timing. Remove it if present. + paddingWindowUpdate = "3-WindowSize-[1]\n"; + if (trace.contains(paddingWindowUpdate)) { + trace = trace.replace(paddingWindowUpdate, ""); + } + + Assert.assertEquals( + "0-WindowSize-[127]\n" + "3-WindowSize-[127]\n" + "3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + + "3-Header-[content-length]-[127]\n" + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + + "3-HeadersEnd\n" + "3-Body-127\n" + "3-EndOfStream\n", + trace); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_2.java b/test/org/apache/coyote/http2/TestHttp2Section_6_2.java new file mode 100644 index 0000000..9e903ce --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_2.java @@ -0,0 +1,104 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 6.2 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_6_2 extends Http2TestBase { + + @Test + public void testHeaderFrameOnStreamZero() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Part 1 + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequestPart1(frameHeader, headersPayload, 0); + writeFrame(frameHeader, headersPayload); + + handleGoAwayResponse(1); + } + + + @Test + public void testHeaderFrameWithPadding() throws Exception { + http2Connect(); + + byte[] padding = new byte[8]; + + sendSimpleGetRequest(3, padding); + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + } + + + @Test + public void testHeaderFrameWithNonZeroPadding() throws Exception { + http2Connect(); + + byte[] padding = new byte[8]; + padding[4] = 1; + + sendSimpleGetRequest(3, padding); + + handleGoAwayResponse(1); + } + + + @Test + public void testHeaderFrameTooMuchPadding() throws Exception { + http2Connect(); + + byte[] headerFrame = new byte[10]; + + // Header + // length + ByteUtil.setThreeBytes(headerFrame, 0, 1); + headerFrame[3] = FrameType.HEADERS.getIdByte(); + // flags 8 (padded) + headerFrame[4] = 0x08; + // stream 3 + ByteUtil.set31Bits(headerFrame, 5, 3); + // payload (pad length of 1) + headerFrame[9] = 1; + + os.write(headerFrame); + os.flush(); + + handleGoAwayResponse(1); + } + + + @Test + public void testHeaderFrameWithZeroLengthPadding() throws Exception { + http2Connect(); + + byte[] padding = new byte[0]; + + sendSimpleGetRequest(3, padding); + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_3.java b/test/org/apache/coyote/http2/TestHttp2Section_6_3.java new file mode 100644 index 0000000..1115f52 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_3.java @@ -0,0 +1,82 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 6.3 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_6_3 extends Http2TestBase { + + @Test + public void testPriorityFrameOnStreamZero() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendPriority(0, 1, 15); + + handleGoAwayResponse(1); + } + + + @Test + public void testPriorityFrameBetweenHeaderFrames() throws Exception { + // HTTP2 upgrade + http2Connect(); + + // Part 1 + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildSimpleGetRequestPart1(frameHeader, headersPayload, 3); + writeFrame(frameHeader, headersPayload); + + sendPriority(5, 3, 15); + + handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR); + } + + + @Test + public void testPriorityFrameWrongLength() throws Exception { + // HTTP2 upgrade + http2Connect(); + + byte[] priorityFrame = new byte[10]; + // length + ByteUtil.setThreeBytes(priorityFrame, 0, 1); + // type + priorityFrame[3] = FrameType.PRIORITY.getIdByte(); + // No flags + // Stream ID + ByteUtil.set31Bits(priorityFrame, 5, 3); + + // Payload - left as zero + + os.write(priorityFrame); + os.flush(); + + // Read reset frame + parser.readFrame(); + + Assert.assertEquals("3-RST-[" + Http2Error.FRAME_SIZE_ERROR.getCode() + "]\n", output.getTrace()); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_4.java b/test/org/apache/coyote/http2/TestHttp2Section_6_4.java new file mode 100644 index 0000000..ba0dc64 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_4.java @@ -0,0 +1,75 @@ +/* + * 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.coyote.http2; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 6.4 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_6_4 extends Http2TestBase { + + @Test + public void testResetFrameOnStreamZero() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendRst(0, Http2Error.NO_ERROR.getCode()); + + handleGoAwayResponse(1); + } + + + @Test + public void testResetFrameOnIdleStream() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendPriority(3, 0, 15); + sendRst(3, Http2Error.NO_ERROR.getCode()); + + handleGoAwayResponse(1); + } + + + @Test + public void testResetFrameWrongLength() throws Exception { + // HTTP2 upgrade + http2Connect(); + + byte[] resetFrame = new byte[10]; + // length + ByteUtil.setThreeBytes(resetFrame, 0, 1); + // type + resetFrame[3] = FrameType.RST.getIdByte(); + // No flags + // Stream ID + ByteUtil.set31Bits(resetFrame, 5, 3); + + // Payload - left as zero + + os.write(resetFrame); + os.flush(); + + // Read reset frame + parser.readFrame(); + + Assert.assertEquals("3-RST-[" + Http2Error.FRAME_SIZE_ERROR.getCode() + "]\n", output.getTrace()); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_5.java b/test/org/apache/coyote/http2/TestHttp2Section_6_5.java new file mode 100644 index 0000000..f826914 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_5.java @@ -0,0 +1,123 @@ +/* + * 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.coyote.http2; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 6.5 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_6_5 extends Http2TestBase { + + + @Test + public void testSettingsFrameNonEmptAck() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendSettings(0, true, new SettingValue(1, 1)); + + handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR); + } + + + @Test + public void testSettingsFrameNonZeroStream() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendPriority(3, 0, 15); + sendSettings(3, true, new SettingValue(1, 1)); + + handleGoAwayResponse(1); + } + + + @Test + public void testSettingsFrameWrongLength() throws Exception { + // HTTP2 upgrade + http2Connect(); + + byte[] resetFrame = new byte[10]; + // length + ByteUtil.setThreeBytes(resetFrame, 0, 1); + // type + resetFrame[3] = FrameType.SETTINGS.getIdByte(); + // No flags + // Stream ID 0 + + // Payload - left as zero + + os.write(resetFrame); + os.flush(); + + handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR); + } + + + // Need to test sending push promise when push promise support is disabled + + @Test + public void testSettingsFrameInvalidPushSetting() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendSettings(0, false, new SettingValue(0x2, 0x2)); + + handleGoAwayResponse(1); + } + + + @Test + public void testSettingsFrameInvalidWindowSizeSetting() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendSettings(0, false, new SettingValue(0x4, 1 << 31)); + + handleGoAwayResponse(1, Http2Error.FLOW_CONTROL_ERROR); + } + + + @Test + public void testSettingsFrameInvalidMaxFrameSizeSetting() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendSettings(0, false, new SettingValue(0x5, 1 << 31)); + + handleGoAwayResponse(1); + } + + + @Test + public void testSettingsUnknownSetting() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendSettings(0, false, new SettingValue(0xFF, 0xFF)); + + // Ack + parser.readFrame(); + + Assert.assertTrue(output.getTrace(), output.getTrace().startsWith("0-Settings-Ack")); + } + + // delayed ACKs. Requires an API (TBD) for applications to send settings. +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_6.java b/test/org/apache/coyote/http2/TestHttp2Section_6_6.java new file mode 100644 index 0000000..ae61fd3 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_6.java @@ -0,0 +1,127 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.PushBuilder; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; + +/** + * Unit tests for Section 6.5 of RFC 7540. + */ +public class TestHttp2Section_6_6 extends Http2TestBase { + + + @Test + public void testPushPromise() throws Exception { + http2Connect(); + + // Build the push request + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/push")); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + // Send the request + writeFrame(frameHeader, headersPayload); + + // Read the response + // push promise + parser.readFrame(); + // stream 3 response headers + parser.readFrame(); + // stream 3 response body + parser.readFrame(); + // stream 2 response headers + parser.readFrame(); + // stream 2 response body + parser.readFrame(); + + String trace = output.getTrace(); + + Assert.assertTrue(trace, trace.contains("3-PushPromise-2")); + Assert.assertTrue(trace, trace.contains("2-Header-[:status]-[200]")); + Assert.assertTrue(trace, trace.contains("2-Body-8192")); + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[200]")); + Assert.assertTrue(trace, trace.contains("3-Body-1024")); + } + + + @Override + protected void configureAndStartWebApplication() throws LifecycleException { + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = tomcat.addContext("", null); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + Tomcat.addServlet(ctxt, "push", new PushServlet()); + ctxt.addServletMappingDecoded("/push", "push"); + + tomcat.start(); + } + + + private static class PushServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + PushBuilder pb = req.newPushBuilder(); + pb.path("/simple").push(); + + // Generate content with a simple known format. + resp.setContentType("application/octet-stream"); + + int count = 512; + + // Two bytes per entry (1k data) + resp.setContentLengthLong(count * 2); + + OutputStream os = resp.getOutputStream(); + byte[] data = new byte[2]; + for (int i = 0; i < count; i++) { + data[0] = (byte) (i & 0xFF); + data[1] = (byte) ((i >> 8) & 0xFF); + os.write(data); + } + } + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_7.java b/test/org/apache/coyote/http2/TestHttp2Section_6_7.java new file mode 100644 index 0000000..cb54223 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_7.java @@ -0,0 +1,80 @@ +/* + * 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.coyote.http2; + +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 6.7 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_6_7 extends Http2TestBase { + + + @Test + public void testPingFrame() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendPing(0, false, "01234567".getBytes(StandardCharsets.ISO_8859_1)); + + // Ping ack + parser.readFrame(); + + Assert.assertEquals("0-Ping-Ack-[48,49,50,51,52,53,54,55]\n", output.getTrace()); + } + + + @Test + public void testPingFrameUnexpectedAck() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendPing(0, true, "01234567".getBytes(StandardCharsets.ISO_8859_1)); + sendPing(0, false, "76543210".getBytes(StandardCharsets.ISO_8859_1)); + + // Ping ack (only for second ping) + parser.readFrame(); + + Assert.assertEquals("0-Ping-Ack-[55,54,53,52,51,50,49,48]\n", output.getTrace()); + } + + + @Test + public void testPingFrameNonZeroStream() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendPing(1, false, "76543210".getBytes(StandardCharsets.ISO_8859_1)); + + handleGoAwayResponse(1); + } + + + @Test + public void testPingFrameWrongPayloadSize() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendPing(0, false, "6543210".getBytes(StandardCharsets.ISO_8859_1)); + + handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_8.java b/test/org/apache/coyote/http2/TestHttp2Section_6_8.java new file mode 100644 index 0000000..ae245ca --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_8.java @@ -0,0 +1,84 @@ +/* + * 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.coyote.http2; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 6.8 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_6_8 extends Http2TestBase { + + private static final boolean RELAX_TIMING = Boolean.getBoolean("tomcat.test.relaxTiming"); + + private static final long PING_ACK_DELAY_MS = 2000; + // On slow systems (Gump) may need to be higher + private static final long TIMING_MARGIN_MS = RELAX_TIMING ? 1000 : 200; + + @Test + public void testGoawayIgnoreNewStreams() throws Exception { + setPingAckDelayMillis(PING_ACK_DELAY_MS); + + http2Connect(); + + http2Protocol.setMaxConcurrentStreams(200); + + Thread.sleep(PING_ACK_DELAY_MS + TIMING_MARGIN_MS); + + getTomcatInstance().getConnector().pause(); + + // Go away + parser.readFrame(); + Assert.assertEquals("0-Goaway-[2147483647]-[0]-[null]", output.getTrace()); + output.clearTrace(); + + // Should be processed + sendSimpleGetRequest(3); + + Thread.sleep(PING_ACK_DELAY_MS + TIMING_MARGIN_MS); + + // Should be ignored + sendSimpleGetRequest(5); + + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + output.clearTrace(); + + // Finally the go away frame + parser.readFrame(); + Assert.assertEquals("0-Goaway-[3]-[0]-[null]", output.getTrace()); + } + + + @Test + public void testGoawayFrameNonZeroStream() throws Exception { + // HTTP2 upgrade + http2Connect(); + + sendGoaway(1, 1, Http2Error.NO_ERROR.getCode()); + + handleGoAwayResponse(1); + } + + + // TODO Test header processing and window size processing for ignored + // streams +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_6_9.java b/test/org/apache/coyote/http2/TestHttp2Section_6_9.java new file mode 100644 index 0000000..966bf88 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_6_9.java @@ -0,0 +1,273 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Section 6.9 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the requirements in the RFC. + */ +public class TestHttp2Section_6_9 extends Http2TestBase { + + @Test + public void testZeroWindowUpdateConnection() throws Exception { + http2Connect(); + + sendWindowUpdate(0, 0); + + handleGoAwayResponse(1); + } + + + @Test + public void testZeroWindowUpdateStream() throws Exception { + http2Connect(); + + sendSimplePostRequest(3, null, false); + sendWindowUpdate(3, 0); + + parser.readFrame(); + + Assert.assertEquals("3-RST-[" + Http2Error.PROTOCOL_ERROR.getCode() + "]\n", output.getTrace()); + } + + + @Test + public void testWindowUpdateOnClosedStream() throws Exception { + http2Connect(); + + // Should not be an error so should be nothing to read + sendWindowUpdate(1, 200); + + // So the next request should process normally + sendSimpleGetRequest(3); + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + } + + + // TODO: Test always accounting for changes in flow control windows even if + // the frame is in error. + + + @Test + public void testWindowUpdateWrongLength() throws Exception { + http2Connect(); + + byte[] zeroLengthWindowFrame = new byte[9]; + // Length zero + setOneBytes(zeroLengthWindowFrame, 3, FrameType.WINDOW_UPDATE.getIdByte()); + // No flags + // Stream 1 + ByteUtil.set31Bits(zeroLengthWindowFrame, 5, 1); + + os.write(zeroLengthWindowFrame); + os.flush(); + + handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR); + } + + + @Test + public void testEmptyDataFrameWithNoAvailableFlowControl() throws Exception { + http2Connect(); + + // Default connection window size is 64k - 1. Initial request will have + // used 8k (56k -1). + + // Use up the remaining connection window. These requests require 48k. + for (int i = 3; i < 15; i += 2) { + sendSimpleGetRequest(i); + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(i), output.getTrace()); + output.clearTrace(); + } + // This request requires 8k but there is only 8k-1 available + sendSimpleGetRequest(15); + readSimpleGetResponse(); + String expected = getResponseBodyFrameTrace(15, 200, "application/octet-stream", null, "8191", "8192"); + // No end of stream + expected = expected.substring(0, expected.length() - "15-EndOfStream\n".length()); + Assert.assertEquals(expected, output.getTrace()); + output.clearTrace(); + + // It should be possible to send a request that generates an empty + // response at this point + sendEmptyGetRequest(17); + // Headers + parser.readFrame(); + // Body + parser.readFrame(); + + // Release Stream 15 which is waiting for a single byte. + sendWindowUpdate(0, 1024); + + Assert.assertEquals(getEmptyResponseTrace(17), output.getTrace()); + } + + + @Test + public void testWindowSizeTooLargeStream() throws Exception { + http2Connect(); + + // Set up stream 3 + sendSimplePostRequest(3, null, false); + + // Super size the flow control window. + sendWindowUpdate(3, (1 << 31) - 1); + + parser.readFrame(); + + Assert.assertEquals("3-RST-[" + Http2Error.FLOW_CONTROL_ERROR.getCode() + "]\n", output.getTrace()); + } + + + @Test + public void testWindowSizeTooLargeConnection() throws Exception { + http2Connect(); + + // Super size the flow control window. + sendWindowUpdate(0, (1 << 31) - 1); + + handleGoAwayResponse(1, Http2Error.FLOW_CONTROL_ERROR); + } + + + @Test + public void testWindowSizeAndSettingsFrame() throws Exception { + http2Connect(); + + // Disable overhead protection for window update as it breaks the test + http2Protocol.setOverheadWindowUpdateThreshold(0); + + // Set up a POST request that echoes the body back + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(8 * 1024); + + buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload, null, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + + // Now use a settings frame to reduce the size of the flow control + // window. + sendSettings(0, false, new SettingValue(4, 4 * 1024)); + // Ack + parser.readFrame(); + Assert.assertEquals("0-Settings-Ack\n", output.getTrace()); + output.clearTrace(); + + // Write the body + writeFrame(dataFrameHeader, dataPayload); + + // Window size updates after reading POST body + parser.readFrame(); + parser.readFrame(); + Assert.assertEquals("0-WindowSize-[8192]\n" + "3-WindowSize-[8192]\n", output.getTrace()); + output.clearTrace(); + + // Read stream 3 headers and first part of body + parser.readFrame(); + parser.readFrame(); + Assert.assertEquals( + "3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + "3-Header-[content-length]-[8192]\n" + + "3-Header-[date]-[" + DEFAULT_DATE + "]\n" + "3-HeadersEnd\n" + "3-Body-4096\n", + output.getTrace()); + output.clearTrace(); + + // Now use a settings frame to further reduce the size of the flow + // control window. This should make the stream 3 window negative + sendSettings(0, false, new SettingValue(4, 2 * 1024)); + // Ack + parser.readFrame(); + Assert.assertEquals("0-Settings-Ack\n", output.getTrace()); + output.clearTrace(); + + // Now use a settings frame to increase the size of the flow control + // window. The stream 3 window should still be negative + sendSettings(0, false, new SettingValue(4, 3 * 1024)); + // Ack + parser.readFrame(); + Assert.assertEquals("0-Settings-Ack\n", output.getTrace()); + output.clearTrace(); + + // Do a POST that won't be affected by the above limit + sendSimplePostRequest(5, null); + // Window size updates after reading POST body + parser.readFrame(); + parser.readFrame(); + Assert.assertEquals("0-WindowSize-[128]\n" + "5-WindowSize-[128]\n", output.getTrace()); + output.clearTrace(); + // Headers + body + parser.readFrame(); + parser.readFrame(); + Assert.assertEquals("5-HeadersStart\n" + "5-Header-[:status]-[200]\n" + "5-Header-[content-length]-[128]\n" + + "5-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "5-HeadersEnd\n" + "5-Body-128\n" + + "5-EndOfStream\n", output.getTrace()); + output.clearTrace(); + + // Now use a settings frame to restore the size of the flow control + // window. + sendSettings(0, false, new SettingValue(4, 64 * 1024 - 1)); + + // Settings ack and stream 3 body are written from different threads. + // Order depends on server side timing. Handle both possibilities. + parser.readFrame(); + String trace = output.getTrace(); + String settingsAck = "0-Settings-Ack\n"; + String endOfStreamThree = "3-Body-4096\n3-EndOfStream\n"; + + if (settingsAck.equals(trace)) { + // Ack the end of stream 3 + output.clearTrace(); + parser.readFrame(); + Assert.assertEquals(endOfStreamThree, output.getTrace()); + } else { + // End of stream 3 thenack + Assert.assertEquals(endOfStreamThree, output.getTrace()); + output.clearTrace(); + parser.readFrame(); + Assert.assertEquals(settingsAck, output.getTrace()); + } + output.clearTrace(); + } + + + @Test + public void testWindowSizeTooLargeViaSettings() throws Exception { + http2Connect(); + + // Set up stream 3 + sendSimplePostRequest(3, null, false); + + // Increase the flow control window but keep it under the limit + sendWindowUpdate(3, 1 << 30); + + // Now increase beyond the limit via a settings frame + sendSettings(0, false, new SettingValue(4, 1 << 30)); + // Ack + parser.readFrame(); + Assert.assertEquals("3-RST-[" + Http2Error.FLOW_CONTROL_ERROR.getCode() + "]\n", output.getTrace()); + + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Section_8_1.java b/test/org/apache/coyote/http2/TestHttp2Section_8_1.java new file mode 100644 index 0000000..5bc3e38 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Section_8_1.java @@ -0,0 +1,482 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.ContinueResponseTiming; +import org.apache.coyote.http11.AbstractHttp11Protocol; + +/** + * Unit tests for Section 8.1 of RFC 7540.
    + * The order of tests in this class is aligned with the order of the examples in the RFC. + */ +public class TestHttp2Section_8_1 extends Http2TestBase { + + @Test + public void testPostWithTrailerHeaders() throws Exception { + doTestPostWithTrailerHeaders(true); + } + + + @Test + public void testPostWithTrailerHeadersBlocked() throws Exception { + doTestPostWithTrailerHeaders(false); + } + + + private void doTestPostWithTrailerHeaders(boolean allowTrailerHeader) throws Exception { + http2Connect(); + if (allowTrailerHeader) { + ((AbstractHttp11Protocol) http2Protocol.getHttp11Protocol()) + .setAllowedTrailerHeaders(TRAILER_HEADER_NAME); + } + + // Disable overhead protection for window update as it breaks some tests + http2Protocol.setOverheadWindowUpdateThreshold(0); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(256); + byte[] trailerFrameHeader = new byte[9]; + ByteBuffer trailerPayload = ByteBuffer.allocate(256); + + buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload, null, + trailerFrameHeader, trailerPayload, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + // Body + writeFrame(dataFrameHeader, dataPayload); + // Trailers + writeFrame(trailerFrameHeader, trailerPayload); + + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + String len; + if (allowTrailerHeader) { + len = Integer.toString(256 + TRAILER_HEADER_VALUE.length()); + } else { + len = "256"; + } + + Assert.assertEquals("0-WindowSize-[256]\n" + "3-WindowSize-[256]\n" + "3-HeadersStart\n" + + "3-Header-[:status]-[200]\n" + "3-Header-[content-length]-[" + len + "]\n" + "3-Header-[date]-[" + + DEFAULT_DATE + "]\n" + "3-HeadersEnd\n" + "3-Body-" + len + "\n" + "3-EndOfStream\n", + output.getTrace()); + } + + + @Test + public void testSendAckWithDefaultPolicy() throws Exception { + testSendAck(); + } + + + @Test + public void testSendAckWithImmediatelyPolicy() throws Exception { + setContinueHandlingResponsePolicy(ContinueResponseTiming.IMMEDIATELY); + testSendAck(); + } + + + @Test + public void testSendAckWithOnRequestBodyReadPolicy() throws Exception { + setContinueHandlingResponsePolicy(ContinueResponseTiming.ON_REQUEST_BODY_READ); + testSendAck(); + } + + + public void setContinueHandlingResponsePolicy(ContinueResponseTiming policy) throws Exception { + final Tomcat tomcat = getTomcatInstance(); + + final Connector connector = tomcat.getConnector(); + connector.setProperty("continueHandlingResponsePolicy", policy.toString()); + } + + + @Test + public void testSendAck() throws Exception { + // makes a request that expects a 100 Continue response and verifies + // that the 100 Continue response is received. This does not check + // that the correct ContinueHandlingResponsePolicy was followed, just + // that a 100 Continue response is received. The unit tests for + // Request verify that the various policies are implemented. + http2Connect(); + + // Disable overhead protection for window update as it breaks some tests + http2Protocol.setOverheadWindowUpdateThreshold(0); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(256); + + buildPostRequest(headersFrameHeader, headersPayload, true, null, -1, "/simple", dataFrameHeader, dataPayload, + null, null, null, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + Assert.assertEquals("3-HeadersStart\n" + "3-Header-[:status]-[100]\n" + "3-HeadersEnd\n", output.getTrace()); + output.clearTrace(); + + // Write the body + writeFrame(dataFrameHeader, dataPayload); + + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals("0-WindowSize-[256]\n" + "3-WindowSize-[256]\n" + "3-HeadersStart\n" + + "3-Header-[:status]-[200]\n" + "3-Header-[content-length]-[256]\n" + "3-Header-[date]-[" + + DEFAULT_DATE + "]\n" + "3-HeadersEnd\n" + "3-Body-256\n" + "3-EndOfStream\n", output.getTrace()); + } + + + @Test + public void testUndefinedPseudoHeader() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(5); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(":foo", "bar")); + + doInvalidPseudoHeaderTest(headers); + } + + + @Test + public void testInvalidPseudoHeader() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(5); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(":status", "200")); + + doInvalidPseudoHeaderTest(headers); + } + + + @Test + public void testPseudoHeaderOrder() throws Exception { + // Need to do this in two frames because HPACK encoder automatically + // re-orders fields + + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("x-test", "test")); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildSimpleGetRequestPart1(headersFrameHeader, headersPayload, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + headers.clear(); + headers.add(new Header(":authority", "localhost:" + getPort())); + headersPayload.clear(); + + buildSimpleGetRequestPart2(headersFrameHeader, headersPayload, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + + parser.readFrame(); + + Assert.assertEquals("3-RST-[1]\n", output.getTrace()); + } + + + @Test + public void testHostHeaderValid() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "localhost:" + getPort())); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[200]")); + } + + + @Test + public void testHostHeaderDuplicate() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "localhost:" + getPort())); + headers.add(new Header("host", "localhost:" + getPort())); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("0-Goaway-[1]-[9]")); + } + + + @Test + public void testHostHeaderConsistent() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "localhost:" + getPort())); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[200]")); + } + + + @Test + public void testHostHeaderConsistentNoPort() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":authority", "localhost")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "localhost")); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[200]")); + } + + + @Test + public void testHostHeaderInconsistent01() throws Exception { + http2Connect(); + + doTestHostHeaderInconsistent("localhost:" + getPort(), "otherhost:" + getPort()); + } + + + @Test + public void testHostHeaderInconsistent02() throws Exception { + http2Connect(); + + doTestHostHeaderInconsistent("localhost", "otherhost"); + } + + + @Test + public void testHostHeaderInconsistent03() throws Exception { + http2Connect(); + + doTestHostHeaderInconsistent("localhost:" + getPort(), "localhost"); + } + + + @Test + public void testHostHeaderInconsistent04() throws Exception { + http2Connect(); + + doTestHostHeaderInconsistent("localhost", "localhost:" + getPort()); + } + + + @Test + public void testHostHeaderInconsistent05() throws Exception { + http2Connect(); + + doTestHostHeaderInconsistent("localhost:" + getPort(), "otherhost:" + (getPort() + 1)); + } + + + private void doTestHostHeaderInconsistent(String authority, String host) throws Exception { + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":authority", authority)); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", host)); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("0-Goaway-[1]-[9]")); + } + + + private void doInvalidPseudoHeaderTest(List
    headers) throws Exception { + doInvalidPseudoHeaderTest(headers, "3-RST-[1]\n"); + } + + private void doInvalidPseudoHeaderTest(List
    headers, String expected) throws Exception { + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + if (trace.length() > expected.length()) { + trace = trace.substring(0, expected.length()); + } + Assert.assertEquals(output.getTrace(), expected, trace); + } + + + @Test + public void testSchemeHeaderValid() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "abcd")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "localhost:" + getPort())); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[200]")); + } + + + @Test + public void testSchemeHeaderInvalid01() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "ab!cd")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "localhost:" + getPort())); + + doInvalidPseudoHeaderTest(headers, "3-HeadersStart\n3-Header-[:status]-[400]\n"); + } + + + @Test + public void testSchemeHeaderInvalid02() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "localhost:" + getPort())); + + doInvalidPseudoHeaderTest(headers, "3-HeadersStart\n3-Header-[:status]-[400]\n"); + } + + + @Test + public void testSchemeHeaderMissing() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "localhost:" + getPort())); + + doInvalidPseudoHeaderTest(headers, "0-Goaway-[3]-[1]-"); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2Timeouts.java b/test/org/apache/coyote/http2/TestHttp2Timeouts.java new file mode 100644 index 0000000..4456b70 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2Timeouts.java @@ -0,0 +1,105 @@ +/* + * 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.coyote.http2; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TestHttp2Timeouts extends Http2TestBase { + + @Override + @Before + public void http2Connect() throws Exception { + super.http2Connect(); + } + + + /* + * Simple request won't fill buffer so timeout will occur in Tomcat internal code during response completion. + */ + @Test + public void testClientWithEmptyWindow() throws Exception { + sendSettings(0, false, new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), 0)); + sendSimpleGetRequest(3); + + // Settings + parser.readFrame(); + // Headers + parser.readFrame(); + + output.clearTrace(); + + parser.readFrame(); + Assert.assertEquals("3-RST-[11]\n", output.getTrace()); + } + + + /* + * Large request will fill buffer so timeout will occur in application code during response write (when Tomcat + * commits the response and flushes the buffer as a result of the buffer filling). + */ + @Test + public void testClientWithEmptyWindowLargeResponse() throws Exception { + sendSettings(0, false, new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), 0)); + sendLargeGetRequest(3); + + // Settings + parser.readFrame(); + // Headers + parser.readFrame(); + + output.clearTrace(); + + parser.readFrame(); + Assert.assertEquals("3-RST-[11]\n", output.getTrace()); + } + + + /* + * Timeout with app reading request body directly. + */ + @Test + public void testClientPostsNoBody() throws Exception { + sendSimplePostRequest(3, null, false); + + // Headers + parser.readFrame(); + output.clearTrace(); + + parser.readFrame(); + + Assert.assertEquals("3-RST-[11]\n", output.getTrace()); + } + + + /* + * Timeout with app processing parameters. + */ + @Test + public void testClientPostsNoParameters() throws Exception { + sendParameterPostRequest(3, null, null, 10, false); + + // Headers + parser.readFrame(); + output.clearTrace(); + + parser.readFrame(); + + Assert.assertEquals("3-RST-[11]\n", output.getTrace()); + } +} diff --git a/test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java b/test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java new file mode 100644 index 0000000..5fb1505 --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java @@ -0,0 +1,220 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; + +public class TestHttp2UpgradeHandler extends Http2TestBase { + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=60970 + @Test + public void testLargeHeader() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Tomcat.addServlet(ctxt, "large", new LargeHeaderServlet()); + ctxt.addServletMappingDecoded("/large", "large"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/large"); + writeFrame(frameHeader, headersPayload); + + // Headers + parser.readFrame(); + parser.readFrame(); + // Body + parser.readFrame(); + + Assert.assertEquals("3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + "3-Header-[x-ignore]-[...]\n" + + "3-Header-[content-type]-[text/plain;charset=UTF-8]\n" + "3-Header-[content-length]-[2]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n" + "3-Body-2\n" + + "3-EndOfStream\n", output.getTrace()); + } + + + @Test + public void testUpgradeWithRequestBodyGet() throws Exception { + doTestUpgradeWithRequestBody(false, false, false); + } + + + @Test + public void testUpgradeWithRequestBodyGetTooBig() throws Exception { + doTestUpgradeWithRequestBody(false, false, true); + } + + + @Test + public void testUpgradeWithRequestBodyPost() throws Exception { + doTestUpgradeWithRequestBody(true, false, false); + } + + + @Test + public void testUpgradeWithRequestBodyPostTooBig() throws Exception { + doTestUpgradeWithRequestBody(true, false, true); + } + + + @Test + public void testUpgradeWithRequestBodyGetReader() throws Exception { + doTestUpgradeWithRequestBody(false, true, false); + } + + + @Test + public void testUpgradeWithRequestBodyGetReaderTooBig() throws Exception { + doTestUpgradeWithRequestBody(false, true, true); + } + + + @Test + public void testUpgradeWithRequestBodyPostReader() throws Exception { + doTestUpgradeWithRequestBody(true, true, false); + } + + + @Test + public void testUpgradeWithRequestBodyPostReaderTooBig() throws Exception { + doTestUpgradeWithRequestBody(true, true, true); + } + + + private void doTestUpgradeWithRequestBody(boolean usePost, boolean useReader, boolean tooBig) throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "ReadRequestBodyServlet", new ReadRequestBodyServlet()); + ctxt.addServletMappingDecoded("/", "ReadRequestBodyServlet"); + + if (tooBig) { + // Reduce maxSavePostSize rather than use a larger request body + tomcat.getConnector().setProperty("maxSavePostSize", "10"); + } + tomcat.start(); + + openClientConnection(); + + byte[] upgradeRequest = ((usePost ? "POST" : "GET") + " /" + (useReader ? "?useReader=true " : " ") + + "HTTP/1.1\r\n" + "Host: localhost:" + getPort() + "\r\n" + "Content-Length: 18\r\n" + + "Connection: Upgrade,HTTP2-Settings\r\n" + "Upgrade: h2c\r\n" + EMPTY_HTTP2_SETTINGS_HEADER + "\r\n" + + "Small request body").getBytes(StandardCharsets.ISO_8859_1); + os.write(upgradeRequest); + os.flush(); + + if (tooBig) { + String[] responseHeaders = readHttpResponseHeaders(); + Assert.assertNotNull(responseHeaders); + Assert.assertNotEquals(0, responseHeaders.length); + Assert.assertEquals("HTTP/1.1 413 ", responseHeaders[0]); + } else { + Assert.assertTrue("Failed to read HTTP Upgrade response", readHttpUpgradeResponse()); + + sendClientPreface(); + + // - 101 response acts as acknowledgement of the HTTP2-Settings header + // Need to read 5 frames + // - settings (server settings - must be first) + // - settings ack (for the settings frame in the client preface) + // - ping + // - headers (for response) + // - data (for response body) + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals("0-Settings-[3]-[200]\n" + "0-Settings-End\n" + "0-Settings-Ack\n" + + "0-Ping-[0,0,0,0,0,0,0,1]\n" + "1-HeadersStart\n" + "1-Header-[:status]-[200]\n" + + "1-Header-[content-type]-[text/plain;charset=UTF-8]\n" + "1-Header-[content-length]-[39]\n" + + "1-Header-[date]-[" + DEFAULT_DATE + "]\n" + "1-HeadersEnd\n" + "1-Body-39\n" + "1-EndOfStream\n", + output.getTrace()); + } + } + + + @Test + public void testActiveConnectionCountAndClientTimeout() throws Exception { + + enableHttp2(2, false, 10000, 10000, 4000, 2000, 2000); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(2); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataFramePayload = ByteBuffer.allocate(128); + + // Should be able to make more than 2 requests even if they timeout + // since they should be removed from active connections once they + // timeout + for (int stream = 3; stream < 8; stream += 2) { + // Don't write the body. Allow the read to timeout. + buildPostRequest(frameHeader, headersPayload, false, dataFrameHeader, dataFramePayload, null, stream); + writeFrame(frameHeader, headersPayload); + + // 400 response (triggered by IOException trying to read body that never arrived) + parser.readFrame(); + Assert.assertTrue(output.getTrace(), + output.getTrace().startsWith(stream + "-HeadersStart\n" + stream + "-Header-[:status]-[400]\n")); + output.clearTrace(); + + // reset frame + parser.readFrame(); + Assert.assertEquals(stream + "-RST-[11]\n", output.getTrace()); + output.clearTrace(); + + // Prepare buffers for re-use + headersPayload.clear(); + dataFramePayload.clear(); + } + } +} diff --git a/test/org/apache/coyote/http2/TestHttpServlet.java b/test/org/apache/coyote/http2/TestHttpServlet.java new file mode 100644 index 0000000..de3749c --- /dev/null +++ b/test/org/apache/coyote/http2/TestHttpServlet.java @@ -0,0 +1,59 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +/* + * Implement here rather than jakarta.servlet.http.TestHttpServlet because it + * needs access to package private classes. + */ +public class TestHttpServlet extends Http2TestBase { + + @Test + public void testUnimplementedMethodHttp2() throws Exception { + http2Connect(); + + // Build a POST request for a Servlet that does not implement POST + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(0); + + buildPostRequest(headersFrameHeader, headersPayload, false, null, -1, "/empty", dataFrameHeader, dataPayload, + null, null, null, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + // Body + writeFrame(dataFrameHeader, dataPayload); + + parser.readFrame(); + parser.readFrame(); + + String trace = output.getTrace(); + String[] lines = trace.split("\n"); + + // Check the response code + Assert.assertEquals("3-HeadersStart", lines[0]); + Assert.assertEquals("3-Header-[:status]-[405]", lines[1]); + } + +} diff --git a/test/org/apache/coyote/http2/TestLargeUpload.java b/test/org/apache/coyote/http2/TestLargeUpload.java new file mode 100644 index 0000000..5904eba --- /dev/null +++ b/test/org/apache/coyote/http2/TestLargeUpload.java @@ -0,0 +1,164 @@ +/* + * 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.coyote.http2; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.tomcat.util.net.TesterSupport; + +@RunWith(Parameterized.class) +public class TestLargeUpload extends Http2TestBase { + + @Parameters(name = "{0}: {1} {2}]") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + parameterSets.add(new Object[] { base[0], base[1], "JSSE", Boolean.FALSE, + "org.apache.tomcat.util.net.jsse.JSSEImplementation" }); + parameterSets.add(new Object[] { base[0], base[1], "OpenSSL", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }); + parameterSets.add(new Object[] { base[0], base[1], "OpenSSL-FFM", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); + } + + return parameterSets; + } + + @Parameter(2) + public String connectorName; + + @Parameter(3) + public boolean needApr; + + @Parameter(4) + public String sslImplementationName; + + + int bodySize = 13107; + int bodyCount = 5; + + volatile int read = 0; + CountDownLatch done = new CountDownLatch(1); + + @Test + public void testLargePostRequest() throws Exception { + + http2Connect(true); + + ((AbstractHttp11Protocol) http2Protocol.getHttp11Protocol()).setAllowedTrailerHeaders(TRAILER_HEADER_NAME); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + byte[] dataFrameHeader = new byte[9]; + ByteBuffer dataPayload = ByteBuffer.allocate(bodySize); + byte[] trailerFrameHeader = new byte[9]; + ByteBuffer trailerPayload = ByteBuffer.allocate(256); + + buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload, null, + trailerFrameHeader, trailerPayload, 3); + + // Write the headers + writeFrame(headersFrameHeader, headersPayload); + // Body + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (int i = 0; i < bodyCount; i++) { + baos.write(dataFrameHeader); + baos.write(dataPayload.array(), dataPayload.arrayOffset(), dataPayload.limit()); + } + os.write(baos.toByteArray()); + os.flush(); + + // Trailers + writeFrame(trailerFrameHeader, trailerPayload); + + done.await(); + Assert.assertEquals(Integer.valueOf(bodySize * bodyCount), Integer.valueOf(read)); + + } + + + @Override + protected void configureAndStartWebApplication() throws LifecycleException { + Tomcat tomcat = getTomcatInstance(); + + // Retain '/simple' url-pattern since it enables code re-use + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "read", new DataReadServlet()); + ctxt.addServletMappingDecoded("/simple", "read"); + + tomcat.start(); + } + + private class DataReadServlet extends SimpleServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + byte[] buf = new byte[8192]; + InputStream is = req.getInputStream(); + int n = is.read(buf); + try { + while (n > 0) { + read += n; + n = is.read(buf); + } + } finally { + done.countDown(); + } + if (read != bodySize * bodyCount) { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } else { + resp.setStatus(HttpServletResponse.SC_OK); + } + } + } + + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + } +} diff --git a/test/org/apache/coyote/http2/TestRfc9218.java b/test/org/apache/coyote/http2/TestRfc9218.java new file mode 100644 index 0000000..6cfd557 --- /dev/null +++ b/test/org/apache/coyote/http2/TestRfc9218.java @@ -0,0 +1,182 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +public class TestRfc9218 extends Http2TestBase { + + @Test + public void testPriority() throws Exception { + http2Connect(); + + /* + * This test uses small window updates and data frames that will trigger the excessive overhead protection so + * disable it. + */ + http2Protocol.setOverheadWindowUpdateThreshold(0); + http2Protocol.setOverheadDataThreshold(0); + + // Default connection window size is 64k - 1. Initial request will have used 8k (56k -1). Increase it to 57k. + sendWindowUpdate(0, 1 + 1024); + + // Consume 56k of the connection window + for (int i = 3; i < 17; i += 2) { + sendSimpleGetRequest(i); + readSimpleGetResponse(); + } + + String trace = output.getTrace(); + System.out.println(trace); + output.clearTrace(); + + // At this point the connection window should be 1k + + // Process a request on stream 17. This should consume the connection window. + sendSimpleGetRequest(17); + // 17-headers, 17-1k-body + parser.readFrame(); + parser.readFrame(); + trace = output.getTrace(); + System.out.println(trace); + output.clearTrace(); + + // Send additional requests. Connection window is empty so only headers will be returned. + sendSimpleGetRequest(19); + sendSimpleGetRequest(21); + + // 19-headers, 21-headers + parser.readFrame(); + parser.readFrame(); + trace = output.getTrace(); + System.out.println(trace); + output.clearTrace(); + + // At this point 17, 19 and 21 are all blocked because the connection window is zero. + // 17 - 7k body left + // 19 - 8k body left + // 21 - 8k body left + + // Add 1k to the connection window. Should be used for stream 17. + sendWindowUpdate(0, 1024); + parser.readFrame(); + Assert.assertEquals("17-Body-1024\n", output.getTrace()); + output.clearTrace(); + + // 17 - 6k body left + // 19 - 8k body left + // 21 - 8k body left + + // Re-order the priorities + sendPriorityUpdate(19, 2, false); + sendPriorityUpdate(21, 1, false); + + // Add 1k to the connection window. Should be used for stream 21. + sendWindowUpdate(0, 1024); + parser.readFrame(); + + Assert.assertEquals("21-Body-1024\n", output.getTrace()); + output.clearTrace(); + + // 17 - 6k body left + // 19 - 8k body left + // 21 - 7k body left + + // Re-order the priorities + sendPriorityUpdate(17, 3, true); + sendPriorityUpdate(19, 3, true); + sendPriorityUpdate(21, 3, true); + + // Add 3k to the connection window. Should be split between 17, 19 and 21. + sendWindowUpdate(0, 1024 * 3); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + trace = output.getTrace(); + Assert.assertTrue(trace.contains("17-Body-877\n")); + trace = trace.replace("17-Body-877\n", ""); + Assert.assertTrue(trace.contains("19-Body-1170\n")); + trace = trace.replace("19-Body-1170\n", ""); + Assert.assertTrue(trace.contains("21-Body-1024\n")); + trace = trace.replace("21-Body-1024\n", ""); + Assert.assertEquals(0, trace.length()); + output.clearTrace(); + + // 1 byte unallocated in connection window + // 17 - 5267 body left + // 19 - 7022 body left + // 21 - 6144 body left + + // Add 1 byte to the connection window. Due to rounding up, each stream should get 1 byte. + sendWindowUpdate(0, 1); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + trace = output.getTrace(); + Assert.assertTrue(trace.contains("17-Body-1\n")); + trace = trace.replace("17-Body-1\n", ""); + Assert.assertTrue(trace.contains("19-Body-1\n")); + trace = trace.replace("19-Body-1\n", ""); + Assert.assertTrue(trace.contains("21-Body-1\n")); + trace = trace.replace("21-Body-1\n", ""); + Assert.assertEquals(0, trace.length()); + output.clearTrace(); + + // 1 byte over allocated in connection window + // 17 - 5266 body left + // 19 - 7021 body left + // 21 - 6143 body left + + // Re-order the priorities + sendPriorityUpdate(17, 2, true); + + /* + * Add 8k to the connection window. Should clear the connection window over allocation and fully allocate 17 + * with the remainder split equally between 17 and 21. + */ + sendWindowUpdate(0, 1024 * 8); + // Use try/catch as third read has been failing on some tests runs + try { + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + } catch (IOException ioe) { + // Dump for debugging purposes + ioe.printStackTrace(); + // Continue - we'll get trace dumped to stdout below + } + + trace = output.getTrace(); + System.out.println(trace); + Assert.assertTrue(trace.contains("17-Body-5266\n")); + trace = trace.replace("17-Body-5266\n", ""); + Assert.assertTrue(trace.contains("17-EndOfStream\n")); + trace = trace.replace("17-EndOfStream\n", ""); + Assert.assertTrue(trace.contains("19-Body-1560\n")); + trace = trace.replace("19-Body-1560\n", ""); + Assert.assertTrue(trace.contains("21-Body-1365\n")); + trace = trace.replace("21-Body-1365\n", ""); + Assert.assertEquals(0, trace.length()); + + // Test doesn't read the read of the body for streams 19 and 21. + } +} diff --git a/test/org/apache/coyote/http2/TestStream.java b/test/org/apache/coyote/http2/TestStream.java new file mode 100644 index 0000000..9ca9a92 --- /dev/null +++ b/test/org/apache/coyote/http2/TestStream.java @@ -0,0 +1,137 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; + +import trailers.ResponseTrailers; + + +public class TestStream extends Http2TestBase { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=61120 + */ + @Test + public void testPathParam() throws Exception { + + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Tomcat.addServlet(ctxt, "pathparam", new PathParam()); + ctxt.addServletMappingDecoded("/pathparam", "pathparam"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/pathparam;jsessionid=" + PathParam.EXPECTED_SESSION_ID); + writeFrame(frameHeader, headersPayload); + + readSimpleGetResponse(); + + Assert.assertEquals("3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + + "3-Header-[content-type]-[text/plain;charset=UTF-8]\n" + "3-Header-[content-length]-[2]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n" + "3-Body-2\n" + + "3-EndOfStream\n", output.getTrace()); + } + + + @Test + public void testResponseTrailerFields() throws Exception { + + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Tomcat.addServlet(ctxt, "trailers", new ResponseTrailers()); + ctxt.addServletMappingDecoded("/trailers", "trailers"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/trailers"); + writeFrame(frameHeader, headersPayload); + + // Headers + parser.readFrame(); + // Body + parser.readFrame(); + // Trailers + parser.readFrame(); + + Assert.assertEquals( + "3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + + "3-Header-[content-type]-[text/plain;charset=UTF-8]\n" + "3-Header-[content-length]-[44]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n" + "3-Body-44\n" + + "3-HeadersStart\n" + "3-Header-[x-trailer-2]-[Trailer value two]\n" + + "3-Header-[x-trailer-1]-[Trailer value one]\n" + "3-HeadersEnd\n" + "3-EndOfStream\n", + output.getTrace()); + } + + + private static final class PathParam extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public static final String EXPECTED_SESSION_ID = "0123456789ABCDEF"; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + if (EXPECTED_SESSION_ID.equals(request.getRequestedSessionId())) { + response.getWriter().write("OK"); + } else { + response.getWriter().write("FAIL"); + } + } + } +} diff --git a/test/org/apache/coyote/http2/TestStreamProcessor.java b/test/org/apache/coyote/http2/TestStreamProcessor.java new file mode 100644 index 0000000..ac362c6 --- /dev/null +++ b/test/org/apache/coyote/http2/TestStreamProcessor.java @@ -0,0 +1,589 @@ +/* + * 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.coyote.http2; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.compat.JrePlatform; +import org.apache.tomcat.util.http.FastHttpDateFormat; + +public class TestStreamProcessor extends Http2TestBase { + + @Test + public void testAsyncComplete() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + // Map the async servlet to /simple so we can re-use the HTTP/2 handling + // logic from the super class. + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncComplete()); + w.setAsyncSupported(true); + ctxt.addServletMappingDecoded("/async", "async"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/async"); + writeFrame(frameHeader, headersPayload); + + readSimpleGetResponse(); + // Flush before startAsync means body is written in two packets so an + // additional frame needs to be read + parser.readFrame(); + + Assert.assertEquals("3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + + "3-Header-[content-type]-[text/plain;charset=UTF-8]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n" + "3-Body-17\n" + "3-Body-8\n" + + "3-EndOfStream\n", output.getTrace()); + } + + + @Test + public void testAsyncDispatch() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + // Map the async servlet to /simple so we can re-use the HTTP/2 handling + // logic from the super class. + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncDispatch()); + w.setAsyncSupported(true); + ctxt.addServletMappingDecoded("/async", "async"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/async"); + writeFrame(frameHeader, headersPayload); + + readSimpleGetResponse(); + Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace()); + } + + + @Test + public void testPrepareHeaders() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(3); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/index.html")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header("if-modified-since", FastHttpDateFormat.getCurrentDate())); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + StringBuilder expected = new StringBuilder(); + expected.append("3-HeadersStart\n"); + expected.append("3-Header-[:status]-[304]\n"); + // Different line-endings -> different files size -> different weak eTag + if (JrePlatform.IS_WINDOWS) { + expected.append("3-Header-[etag]-[W/\"957-1447269522000\"]\n"); + } else { + expected.append("3-Header-[etag]-[W/\"934-1447269522000\"]\n"); + } + expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n"); + expected.append("3-HeadersEnd\n"); + expected.append("3-EndOfStream\n"); + + Assert.assertEquals(expected.toString(), output.getTrace()); + } + + + @Test + public void testPrepareHeadersNoContent() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Tomcat.addServlet(ctxt, "noContent", new NoContentServlet()); + ctxt.addServletMappingDecoded("/noContent", "noContent"); + + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(3); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/noContent")); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + StringBuilder expected = new StringBuilder(); + expected.append("3-HeadersStart\n"); + expected.append("3-Header-[:status]-[204]\n"); + expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n"); + expected.append("3-HeadersEnd\n"); + expected.append("3-EndOfStream\n"); + + Assert.assertEquals(expected.toString(), output.getTrace()); + } + + + @Test + public void testValidateRequestMethod() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "not,token")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/index.html")); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + StringBuilder expected = new StringBuilder(); + expected.append("3-HeadersStart\n"); + expected.append("3-Header-[:status]-[400]\n"); + expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n"); + expected.append("3-HeadersEnd\n"); + + Assert.assertEquals(expected.toString(), output.getTrace()); + } + + + @Test + public void testValidateRequestHeaderName() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(5); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/index.html")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header("not token", "value")); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + StringBuilder expected = new StringBuilder(); + expected.append("3-HeadersStart\n"); + expected.append("3-Header-[:status]-[400]\n"); + expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n"); + expected.append("3-HeadersEnd\n"); + + Assert.assertEquals(expected.toString(), output.getTrace()); + } + + + @Test + public void testValidateRequestURI() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/index^html")); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + StringBuilder expected = new StringBuilder(); + expected.append("3-HeadersStart\n"); + expected.append("3-Header-[:status]-[400]\n"); + expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n"); + expected.append("3-HeadersEnd\n"); + + Assert.assertEquals(expected.toString(), output.getTrace()); + } + + + @Test + public void testValidateRequestQueryString() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/index.html?foo=[]")); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + StringBuilder expected = new StringBuilder(); + expected.append("3-HeadersStart\n"); + expected.append("3-Header-[:status]-[400]\n"); + expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n"); + expected.append("3-HeadersEnd\n"); + + Assert.assertEquals(expected.toString(), output.getTrace()); + } + + + @Test + public void testValidateRequestQueryStringRelaxed() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + + tomcat.getConnector().setProperty("relaxedQueryChars", "[]"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/index.html?foo=[]")); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + StringBuilder expected = new StringBuilder(); + expected.append("3-HeadersStart\n"); + expected.append("3-Header-[:status]-[200]\n"); + + // The status code is the most important thing to test + Assert.assertTrue(output.getTrace().startsWith(expected.toString())); + } + + + private static final class AsyncComplete extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + PrintWriter pw = response.getWriter(); + pw.print("Enter-"); + + final AsyncContext asyncContext = request.startAsync(request, response); + pw.print("StartAsync-"); + pw.flush(); + + asyncContext.start(new Runnable() { + + @Override + public void run() { + try { + asyncContext.getResponse().getWriter().print("Complete"); + asyncContext.complete(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + } + + + private static final class AsyncDispatch extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + final AsyncContext asyncContext = request.startAsync(request, response); + asyncContext.start(new Runnable() { + + @Override + public void run() { + try { + asyncContext.dispatch("/simple"); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + } + + + @Test + public void testCompression() throws Exception { + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Tomcat.addServlet(ctxt, "compression", new CompressionServlet()); + ctxt.addServletMappingDecoded("/compression", "compression"); + + // Enable compression + Connector connector = tomcat.getConnector(); + Assert.assertTrue(connector.setProperty("compression", "on")); + + tomcat.start(); + + enableHttp2(); + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(3); + headers.add(new Header(":method", "GET")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/compression")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header("accept-encoding", "gzip")); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + readSimpleGetResponse(); + + Assert.assertEquals("3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + "3-Header-[vary]-[accept-encoding]\n" + + "3-Header-[content-encoding]-[gzip]\n" + "3-Header-[content-type]-[text/plain;charset=UTF-8]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n" + "3-Body-97\n" + + "3-EndOfStream\n", output.getTrace()); + } + + + private static class CompressionServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Generate content type that is compressible + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + // Make ir large enough to trigger compression + int count = 64 * 1024; + + // One bytes per entry + resp.setContentLengthLong(count); + + OutputStream os = resp.getOutputStream(); + for (int i = 0; i < count; i++) { + os.write('X'); + } + } + } + + + @Test + public void testConnect() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", "CONNECT")); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":authority", "example.local")); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[501]")); + } +} diff --git a/test/org/apache/coyote/http2/TestStreamQueryString.java b/test/org/apache/coyote/http2/TestStreamQueryString.java new file mode 100644 index 0000000..90a2d4d --- /dev/null +++ b/test/org/apache/coyote/http2/TestStreamQueryString.java @@ -0,0 +1,173 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.buf.HexUtils; + +/* + * See https://bz.apache.org/bugzilla/show_bug.cgi?id=60482 + */ +@RunWith(Parameterized.class) +public class TestStreamQueryString extends Http2TestBase { + + @Parameters + public static Collection inputs() { + List result = new ArrayList<>(); + Collection baseData = data(); + + for (Object[] base : baseData) { + // Test ASCII characters from 32 to 126 inclusive + for (int i = 32; i < 128; i++) { + result.add(new Object[] { base[0], base[1], "%" + HexUtils.toHexString(new byte[] { (byte) i }) }); + } + } + + return result; + } + + @Parameter(2) + public String queryValueToTest; + + + @Test + public void testQueryString() throws Exception { + String queryValue = "xxx" + queryValueToTest + "xxx"; + + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "query", new Query(queryValue)); + ctxt.addServletMappingDecoded("/query", "query"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(queryValue); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/query?" + Query.PARAM_NAME + "=" + queryValue); + writeFrame(frameHeader, headersPayload); + + readSimpleGetResponse(); + + Assert.assertEquals(queryValue, + "3-HeadersStart\n" + "3-Header-[:status]-[200]\n" + + "3-Header-[content-type]-[text/plain;charset=UTF-8]\n" + "3-Header-[content-length]-[2]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "3-HeadersEnd\n" + "3-Body-2\n" + + "3-EndOfStream\n", + output.getTrace()); + } + + + protected void doHttpUpgrade(String queryValue) throws IOException { + byte[] upgradeRequest = ("GET /query?" + Query.PARAM_NAME + "=" + queryValue + " HTTP/1.1\r\n" + + "Host: localhost:" + getPort() + "\r\n" + "Connection: " + DEFAULT_CONNECTION_HEADER_VALUE + "\r\n" + + "Upgrade: h2c\r\n" + EMPTY_HTTP2_SETTINGS_HEADER + "\r\n").getBytes(StandardCharsets.ISO_8859_1); + os.write(upgradeRequest); + os.flush(); + + Assert.assertTrue("Failed to read HTTP Upgrade response", readHttpUpgradeResponse()); + } + + + @Override + protected void validateHttp2InitialResponse() throws Exception { + // - 101 response acts as acknowledgement of the HTTP2-Settings header + // Need to read 5 frames + // - settings (server settings - must be first) + // - settings ack (for the settings frame in the client preface) + // - ping + // - headers (for response) + // - data (for response body) + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + parser.readFrame(); + + Assert.assertEquals("0-Settings-[3]-[200]\n" + "0-Settings-End\n" + "0-Settings-Ack\n" + + "0-Ping-[0,0,0,0,0,0,0,1]\n" + "1-HeadersStart\n" + "1-Header-[:status]-[200]\n" + + "1-Header-[content-type]-[text/plain;charset=UTF-8]\n" + "1-Header-[content-length]-[2]\n" + + "1-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + "1-HeadersEnd\n" + "1-Body-2\n" + + "1-EndOfStream\n", output.getTrace()); + + output.clearTrace(); + } + + + private static final class Query extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final String PARAM_NAME = "param"; + + private final String expectedValue; + + Query(String expectedValue) { + String decoded; + try { + decoded = URLDecoder.decode(expectedValue, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Can't happen with UTF-8 + decoded = null; + } + this.expectedValue = decoded; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("text/plain"); + response.setCharacterEncoding("UTF-8"); + + if (expectedValue.equals(request.getParameter(PARAM_NAME))) { + response.getWriter().write("OK"); + } else { + response.getWriter().write("FAIL"); + } + } + } +} diff --git a/test/org/apache/coyote/http2/TesterHttp2Parser.java b/test/org/apache/coyote/http2/TesterHttp2Parser.java new file mode 100644 index 0000000..3b4f10a --- /dev/null +++ b/test/org/apache/coyote/http2/TesterHttp2Parser.java @@ -0,0 +1,92 @@ +/* + * 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.coyote.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.coyote.http2.Http2TestBase.TestOutput; + +/** + * Expose the parser outside of this package for use in other tests. + */ +public class TesterHttp2Parser extends Http2Parser { + + private final TestOutput output; + + TesterHttp2Parser(String connectionId, Input input, TestOutput output) { + super(connectionId, input, output); + this.output = output; + } + + + /** + * {@inheritDoc} + *

    + * Note: The test implementation always uses blocking IO for both the initial read and the remainder. + */ + @Override + public boolean readFrame() throws Http2Exception, IOException { + return super.readFrame(); + } + + @Override + protected void readPushPromiseFrame(int streamId, int flags, int payloadSize, ByteBuffer buffer) + throws Http2Exception, IOException { + + // Parse flags used in this method + boolean hasPadding = Flags.hasPadding(flags); + boolean headersEndStream = Flags.isEndOfStream(flags); + + // Padding size + int paddingSize = 0; + if (hasPadding) { + byte[] bPadSize = new byte[1]; + if (buffer == null) { + input.fill(true, bPadSize); + } else { + buffer.get(bPadSize); + } + paddingSize = ByteUtil.getOneByte(bPadSize, 0); + } + + // Pushed stream ID + byte[] bPushedStreamId = new byte[4]; + if (buffer == null) { + input.fill(true, bPushedStreamId); + } else { + buffer.get(bPushedStreamId); + } + int pushedStreamId = ByteUtil.get31Bits(bPushedStreamId, 0); + + output.pushPromise(streamId, pushedStreamId); + + int headerSize = payloadSize - 4 - paddingSize; + if (hasPadding) { + headerSize--; + } + + HpackDecoder hpackDecoder = output.getHpackDecoder(); + hpackDecoder.setHeaderEmitter(output.headersStart(pushedStreamId, headersEndStream)); + + readHeaderPayload(pushedStreamId, headerSize, buffer); + + if (hasPadding) { + swallowPayload(streamId, FrameType.PUSH_PROMISE.getId(), paddingSize, true, buffer); + } + } +} diff --git a/test/org/apache/el/TestELEvaluation.java b/test/org/apache/el/TestELEvaluation.java new file mode 100644 index 0000000..fb560b1 --- /dev/null +++ b/test/org/apache/el/TestELEvaluation.java @@ -0,0 +1,257 @@ +/* + * 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.el; + +import java.io.File; +import java.util.Date; + +import jakarta.el.ELException; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.el.lang.ELSupport; +import org.apache.jasper.el.ELContextImpl; + +/** + * Tests the EL engine directly. Similar tests may be found in {@link org.apache.jasper.compiler.TestAttributeParser} + * and {@link TestELInJsp}. + */ +public class TestELEvaluation { + + /** + * Test use of spaces in ternary expressions. This was primarily an EL parser bug. + */ + @Test + public void testBug42565() { + Assert.assertEquals("false", evaluateExpression("${false?true:false}")); + Assert.assertEquals("false", evaluateExpression("${false?true: false}")); + Assert.assertEquals("false", evaluateExpression("${false?true :false}")); + Assert.assertEquals("false", evaluateExpression("${false?true : false}")); + Assert.assertEquals("false", evaluateExpression("${false? true:false}")); + Assert.assertEquals("false", evaluateExpression("${false? true: false}")); + Assert.assertEquals("false", evaluateExpression("${false? true :false}")); + Assert.assertEquals("false", evaluateExpression("${false? true : false}")); + Assert.assertEquals("false", evaluateExpression("${false ?true:false}")); + Assert.assertEquals("false", evaluateExpression("${false ?true: false}")); + Assert.assertEquals("false", evaluateExpression("${false ?true :false}")); + Assert.assertEquals("false", evaluateExpression("${false ?true : false}")); + Assert.assertEquals("false", evaluateExpression("${false ? true:false}")); + Assert.assertEquals("false", evaluateExpression("${false ? true: false}")); + Assert.assertEquals("false", evaluateExpression("${false ? true :false}")); + Assert.assertEquals("false", evaluateExpression("${false ? true : false}")); + } + + + /** + * Test use nested ternary expressions. This was primarily an EL parser bug. + */ + @Test + public void testBug44994() { + Assert.assertEquals("none", evaluateExpression("${0 lt 0 ? 1 lt 0 ? 'many': 'one': 'none'}")); + Assert.assertEquals("one", evaluateExpression("${0 lt 1 ? 1 lt 1 ? 'many': 'one': 'none'}")); + Assert.assertEquals("many", evaluateExpression("${0 lt 2 ? 1 lt 2 ? 'many': 'one': 'none'}")); + } + + @Test + public void testParserBug45511() { + // Test cases provided by OP + Assert.assertEquals("true", evaluateExpression("${empty ('')}")); + Assert.assertEquals("true", evaluateExpression("${empty('')}")); + Assert.assertEquals("false", evaluateExpression("${(true) and (false)}")); + Assert.assertEquals("false", evaluateExpression("${(true)and(false)}")); + } + + @Test + public void testBug48112() { + // bug 48112 + Assert.assertEquals("{world}", evaluateExpression("${fn:trim('{world}')}")); + } + + @Test + public void testParserLiteralExpression() { + // Inspired by work on bug 45451, comments from kkolinko on the dev + // list and looking at the spec to find some edge cases + + // '\' is only an escape character inside a StringLiteral + Assert.assertEquals("\\\\", evaluateExpression("\\\\")); + + /* + * LiteralExpressions can only contain ${ or #{ if escaped with \ \ is not an escape character in any other + * circumstances including \\ + */ + Assert.assertEquals("\\", evaluateExpression("\\")); + Assert.assertEquals("$", evaluateExpression("$")); + Assert.assertEquals("#", evaluateExpression("#")); + Assert.assertEquals("\\$", evaluateExpression("\\$")); + Assert.assertEquals("\\#", evaluateExpression("\\#")); + Assert.assertEquals("\\\\$", evaluateExpression("\\\\$")); + Assert.assertEquals("\\\\#", evaluateExpression("\\\\#")); + Assert.assertEquals("${", evaluateExpression("\\${")); + Assert.assertEquals("#{", evaluateExpression("\\#{")); + Assert.assertEquals("\\${", evaluateExpression("\\\\${")); + Assert.assertEquals("\\#{", evaluateExpression("\\\\#{")); + + // '\' is only an escape for '${' and '#{'. + Assert.assertEquals("\\$", evaluateExpression("\\$")); + Assert.assertEquals("${", evaluateExpression("\\${")); + Assert.assertEquals("\\$a", evaluateExpression("\\$a")); + Assert.assertEquals("\\a", evaluateExpression("\\a")); + Assert.assertEquals("\\\\", evaluateExpression("\\\\")); + } + + @Test + public void testParserStringLiteral() { + // Inspired by work on bug 45451, comments from kkolinko on the dev + // list and looking at the spec to find some edge cases + + // The only characters that can be escaped inside a String literal + // are \ " and '. # and $ are not escaped inside a String literal. + Assert.assertEquals("\\", evaluateExpression("${'\\\\'}")); + Assert.assertEquals("\\", evaluateExpression("${\"\\\\\"}")); + Assert.assertEquals("\\\"'$#", evaluateExpression("${'\\\\\\\"\\'$#'}")); + Assert.assertEquals("\\\"'$#", evaluateExpression("${\"\\\\\\\"\\'$#\"}")); + + // Trying to quote # or $ should throw an error + Exception e = null; + try { + evaluateExpression("${'\\$'}"); + } catch (ELException el) { + e = el; + } + Assert.assertNotNull(e); + + Assert.assertEquals("\\$", evaluateExpression("${'\\\\$'}")); + Assert.assertEquals("\\\\$", evaluateExpression("${'\\\\\\\\$'}")); + + + // Can use ''' inside '"' when quoting with '"' and vice versa without + // escaping + Assert.assertEquals("\\\"", evaluateExpression("${'\\\\\"'}")); + Assert.assertEquals("\"\\", evaluateExpression("${'\"\\\\'}")); + Assert.assertEquals("\\'", evaluateExpression("${'\\\\\\''}")); + Assert.assertEquals("'\\", evaluateExpression("${'\\'\\\\'}")); + Assert.assertEquals("\\'", evaluateExpression("${\"\\\\'\"}")); + Assert.assertEquals("'\\", evaluateExpression("${\"'\\\\\"}")); + Assert.assertEquals("\\\"", evaluateExpression("${\"\\\\\\\"\"}")); + Assert.assertEquals("\"\\", evaluateExpression("${\"\\\"\\\\\"}")); + } + + @Test + public void testMultipleEscaping() throws Exception { + Assert.assertEquals("''", evaluateExpression("${\"\'\'\"}")); + } + + private void compareBoth(String msg, int expected, Object o1, Object o2) { + int i1 = ELSupport.compare(null, o1, o2); + int i2 = ELSupport.compare(null, o2, o1); + Assert.assertEquals(msg, expected, i1); + Assert.assertEquals(msg, expected, -i2); + } + + @Test + public void testElSupportCompare() { + compareBoth("Nulls should compare equal", 0, null, null); + compareBoth("Null should compare equal to \"\"", 0, "", null); + compareBoth("Null should be less than File()", -1, null, new File("")); + compareBoth("Null should be less than Date()", -1, null, new Date()); + compareBoth("Date(0) should be less than Date(1)", -1, new Date(0), new Date(1)); + try { + compareBoth("Should not compare", 0, new Date(), new File("")); + Assert.fail("Expecting ClassCastException"); + } catch (ClassCastException expected) { + // Expected + } + Assert.assertTrue(null == null); + } + + /** + * Test mixing ${...} and #{...} in the same expression. + */ + @Test + public void testMixedTypes() { + // Mixing types should throw an error + Exception e = null; + try { + evaluateExpression("${1+1}#{1+1}"); + } catch (ELException el) { + e = el; + } + Assert.assertNotNull(e); + } + + @Test + public void testEscape01() { + Assert.assertEquals("$${", evaluateExpression("$\\${")); + } + + @Test + public void testBug49081a() { + Assert.assertEquals("$2", evaluateExpression("$${1+1}")); + } + + @Test + public void testBug49081b() { + Assert.assertEquals("#2", evaluateExpression("##{1+1}")); + } + + @Test + public void testBug49081c() { + Assert.assertEquals("#2", evaluateExpression("#${1+1}")); + } + + @Test + public void testBug49081d() { + Assert.assertEquals("$2", evaluateExpression("$#{1+1}")); + } + + @Test + public void testBug60431a() { + Assert.assertEquals("OK", evaluateExpression("${fn:concat('O','K')}")); + } + + @Test + public void testBug60431b() { + Assert.assertEquals("OK", evaluateExpression("${fn:concat(fn:toArray('O','K'))}")); + } + + @Test + public void testBug60431c() { + Assert.assertEquals("", evaluateExpression("${fn:concat()}")); + } + + @Test + public void testBug60431d() { + Assert.assertEquals("OK", evaluateExpression("${fn:concat2('OK')}")); + } + + @Test + public void testBug60431e() { + Assert.assertEquals("RUOK", evaluateExpression("${fn:concat2('RU', fn:toArray('O','K'))}")); + } + + // ************************************************************************ + + private String evaluateExpression(String expression) { + ExpressionFactoryImpl exprFactory = new ExpressionFactoryImpl(); + ELContextImpl ctx = new ELContextImpl(exprFactory); + ctx.setFunctionMapper(new TesterFunctions.FMapper()); + ValueExpression ve = exprFactory.createValueExpression(ctx, expression, String.class); + return (String) ve.getValue(ctx); + } +} diff --git a/test/org/apache/el/TestELInJsp.java b/test/org/apache/el/TestELInJsp.java new file mode 100644 index 0000000..4c8bfb0 --- /dev/null +++ b/test/org/apache/el/TestELInJsp.java @@ -0,0 +1,515 @@ +/* + * 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.el; + +import java.io.File; +import java.math.RoundingMode; +import java.util.Collections; + +import jakarta.servlet.DispatcherType; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.servlet.JasperInitializer; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Tests EL with an without JSP attributes using a test web application. Similar tests may be found in + * {@link TestELEvaluation} and {@link org.apache.jasper.compiler.TestAttributeParser}. + */ +public class TestELInJsp extends TomcatBaseTest { + + @Test + public void testBug36923() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug36923.jsp"); + + String result = res.toString(); + assertEcho(result, "00-${hello world}"); + } + + @Test + public void testBug42565() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug42565.jsp"); + + String result = res.toString(); + assertEcho(result, "00-false"); + assertEcho(result, "01-false"); + assertEcho(result, "02-false"); + assertEcho(result, "03-false"); + assertEcho(result, "04-false"); + assertEcho(result, "05-false"); + assertEcho(result, "06-false"); + assertEcho(result, "07-false"); + assertEcho(result, "08-false"); + assertEcho(result, "09-false"); + assertEcho(result, "10-false"); + assertEcho(result, "11-false"); + assertEcho(result, "12-false"); + assertEcho(result, "13-false"); + assertEcho(result, "14-false"); + assertEcho(result, "15-false"); + } + + @Test + public void testBug44994() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug44994.jsp"); + + String result = res.toString(); + assertEcho(result, "00-none"); + assertEcho(result, "01-one"); + assertEcho(result, "02-many"); + } + + @Test + public void testBug45427() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45427.jsp"); + + String result = res.toString(); + // Warning: JSP attribute escaping != Java String escaping + assertEcho(result, "00-hello world"); + assertEcho(result, "01-hello 'world"); + assertEcho(result, "02-hello \"world"); + assertEcho(result, "03-hello \"world"); + assertEcho(result, "04-hello world"); + assertEcho(result, "05-hello 'world"); + assertEcho(result, "06-hello 'world"); + assertEcho(result, "07-hello \"world"); + assertEcho(result, "08-hello world"); + assertEcho(result, "09-hello 'world"); + assertEcho(result, "10-hello \"world"); + assertEcho(result, "11-hello \"world"); + assertEcho(result, "12-hello world"); + assertEcho(result, "13-hello 'world"); + assertEcho(result, "14-hello 'world"); + assertEcho(result, "15-hello \"world"); + } + + @Test + public void testBug45451() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45451a.jsp"); + + String result = res.toString(); + // Warning: JSP attribute escaping != Java String escaping + assertEcho(result, "00-\\'hello world\\'"); + assertEcho(result, "01-\\'hello world\\'"); + assertEcho(result, "02-\\'hello world\\'"); + assertEcho(result, "03-\\'hello world\\'"); + + res = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45451b.jsp"); + result = res.toString(); + // Warning: JSP attribute escaping != Java String escaping + // Warning: Attributes are always unescaped before passing to the EL + // processor + assertEcho(result, "00-2"); + assertEcho(result, "01-${1+1}"); + assertEcho(result, "02-\\${1+1}"); + assertEcho(result, "03-\\\\${1+1}"); + assertEcho(result, "04-$500"); + // Inside an EL literal '\' is only used to escape '\', ''' and '"' + assertEcho(result, "05-\\$"); + assertEcho(result, "06-\\${"); + assertEcho(result, "10-2"); + assertEcho(result, "11-${1+1}"); + assertEcho(result, "12-\\2"); + assertEcho(result, "13-\\${1+1}"); + assertEcho(result, "14-\\\\2"); + assertEcho(result, "15-$500"); + assertEcho(result, "16-\\$"); + assertEcho(result, "17-\\${"); + assertEcho(result, "20-2"); + assertEcho(result, "21-#{1+1}"); + assertEcho(result, "22-\\2"); + assertEcho(result, "23-\\#{1+1}"); + assertEcho(result, "24-\\\\2"); + assertEcho(result, "25-\\#"); + assertEcho(result, "26-\\#{"); + + res = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45451c.jsp"); + result = res.toString(); + // Warning: JSP attribute escaping != Java String escaping + // TODO - Currently we allow a single unescaped \ in attribute values + // Review if this should cause a warning/error + assertEcho(result, "00-${1+1}"); + assertEcho(result, "01-\\${1+1}"); + assertEcho(result, "02-\\\\${1+1}"); + assertEcho(result, "03-\\\\\\${1+1}"); + assertEcho(result, "04-\\$500"); + assertEcho(result, "10-${1+1}"); + assertEcho(result, "11-\\${1+1}"); + assertEcho(result, "12-\\${1+1}"); + assertEcho(result, "13-\\\\${1+1}"); + assertEcho(result, "14-\\\\${1+1}"); + assertEcho(result, "15-\\$500"); + assertEcho(result, "20-#{1+1}"); + assertEcho(result, "21-\\#{1+1}"); + assertEcho(result, "22-\\#{1+1}"); + assertEcho(result, "23-\\\\#{1+1}"); + assertEcho(result, "24-\\\\#{1+1}"); + + res = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45451d.jspx"); + result = res.toString(); + // Warning: JSP attribute escaping != Java String escaping + // \\ Is *not* an escape sequence in XML attributes + assertEcho(result, "00-2"); + assertEcho(result, "01-${1+1}"); + assertEcho(result, "02-\\${1+1}"); + assertEcho(result, "03-\\\\${1+1}"); + assertEcho(result, "04-$500"); + assertEcho(result, "10-2"); + assertEcho(result, "11-${1+1}"); + assertEcho(result, "12-\\${1+1}"); + assertEcho(result, "13-\\\\${1+1}"); + assertEcho(result, "14-\\\\\\${1+1}"); + assertEcho(result, "15-$500"); + assertEcho(result, "20-2"); + assertEcho(result, "21-#{1+1}"); + assertEcho(result, "22-\\#{1+1}"); + assertEcho(result, "23-\\\\#{1+1}"); + assertEcho(result, "24-\\\\\\#{1+1}"); + + res = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45451e.jsp"); + result = res.toString(); + // Warning: JSP attribute escaping != Java String escaping + // Warning: Attributes are always unescaped before passing to the EL + // processor + assertEcho(result, "00-2"); + assertEcho(result, "01-${1+1}"); + assertEcho(result, "02-\\${1+1}"); + assertEcho(result, "03-\\\\${1+1}"); + assertEcho(result, "04-$500"); + assertEcho(result, "10-2"); + assertEcho(result, "11-${1+1}"); + assertEcho(result, "12-\\2"); + assertEcho(result, "13-\\${1+1}"); + assertEcho(result, "14-\\\\2"); + assertEcho(result, "15-$500"); + assertEcho(result, "20-#{1+1}"); + assertEcho(result, "21-\\#{1+1}"); + assertEcho(result, "22-\\#{1+1}"); + assertEcho(result, "23-\\\\#{1+1}"); + assertEcho(result, "24-\\\\#{1+1}"); + } + + @Test + public void testBug45511() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45511.jsp"); + + String result = res.toString(); + assertEcho(result, "00-true"); + assertEcho(result, "01-false"); + } + + @Test + public void testBug46596() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug46596.jsp"); + String result = res.toString(); + assertEcho(result, "{OK}"); + } + + @Test + public void testBug47413() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug47413.jsp"); + + String result = res.toString(); + assertEcho(result, "00-hello world"); + assertEcho(result, "01-hello world"); + assertEcho(result, "02-3.22"); + assertEcho(result, "03-3.22"); + assertEcho(result, "04-17"); + assertEcho(result, "05-17"); + assertEcho(result, "06-hello world"); + assertEcho(result, "07-hello world"); + assertEcho(result, "08-0.0"); + assertEcho(result, "09-0.0"); + assertEcho(result, "10-0"); + assertEcho(result, "11-0"); + } + + @Test + public void testBug48112() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug48nnn/bug48112.jsp"); + String result = res.toString(); + assertEcho(result, "{OK}"); + } + + @Test + public void testBug49555() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49555.jsp"); + + String result = res.toString(); + assertEcho(result, "00-" + TesterFunctions.Inner$Class.RETVAL); + } + + @Test + public void testBug51544() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug51544.jsp"); + + String result = res.toString(); + assertEcho(result, "Empty list: true"); + } + + @Test + public void testELMiscNoQuoteAttributeEL() throws Exception { + doTestELMisc(false); + } + + @Test + public void testELMiscWithQuoteAttributeEL() throws Exception { + doTestELMisc(true); + } + + private void doTestELMisc(boolean quoteAttributeEL) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // Create the context (don't use addWebapp as we want to modify the + // JSP Servlet settings). + File appDir = new File("test/webapp"); + StandardContext ctxt = (StandardContext) tomcat.addContext(null, "/test", appDir.getAbsolutePath()); + + ctxt.addServletContainerInitializer(new JasperInitializer(), null); + + // Configure the defaults and then tweak the JSP servlet settings + // Note: Min value for maxLoadedJsps is 2 + Tomcat.initWebappDefaults(ctxt); + Wrapper w = (Wrapper) ctxt.findChild("jsp"); + + String jspName; + if (quoteAttributeEL) { + jspName = "/test/el-misc-with-quote-attribute-el.jsp"; + w.addInitParameter("quoteAttributeEL", "true"); + } else { + jspName = "/test/el-misc-no-quote-attribute-el.jsp"; + w.addInitParameter("quoteAttributeEL", "false"); + } + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + jspName); + String result = res.toString(); + + assertEcho(result, "00-\\\\\\\"${'hello world'}"); + assertEcho(result, "01-\\\\\\\"\\${'hello world'}"); + assertEcho(result, "02-\\\"${'hello world'}"); + assertEcho(result, "03-\\\"\\hello world"); + assertEcho(result, "2az-04"); + assertEcho(result, "05-a2z"); + assertEcho(result, "06-az2"); + assertEcho(result, "2az-07"); + assertEcho(result, "08-a2z"); + assertEcho(result, "09-az2"); + assertEcho(result, "10-${'foo'}bar"); + assertEcho(result, "11-\\\"}"); + assertEcho(result, "12-foo\\bar\\baz"); + assertEcho(result, "13-foo\\bar\\baz"); + assertEcho(result, "14-foo\\bar\\baz"); + assertEcho(result, "15-foo\\bar\\baz"); + assertEcho(result, "16-foo\\bar\\baz"); + assertEcho(result, "17-foo\\'bar'\\"baz""); + assertEcho(result, "18-3"); + assertEcho(result, "19-4"); + assertEcho(result, "20-4"); + assertEcho(result, "21-[{value=11}, {value=12}, {value=13}, {value=14}]"); + assertEcho(result, "22-[{value=11}, {value=12}, {value=13}, {value=14}]"); + } + + @Test + public void testScriptingExpression() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/script-expr.jsp"); + String result = res.toString(); + assertEcho(result, "00-hello world"); + assertEcho(result, "01-hello \"world"); + assertEcho(result, "02-hello \\\"world"); + assertEcho(result, "03-hello ${world"); + assertEcho(result, "04-hello \\${world"); + assertEcho(result, "05-hello world"); + assertEcho(result, "06-hello \"world"); + assertEcho(result, "07-hello \\\"world"); + assertEcho(result, "08-hello ${world"); + assertEcho(result, "09-hello \\${world"); + assertEcho(result, "10-hello <% world"); + assertEcho(result, "11-hello %> world"); + } + + @Test + public void testELMethod() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-method.jsp"); + String result = res.toString(); + assertEcho(result, "00-Hello JUnit from Tomcat"); + assertEcho(result, "01-Hello JUnit from Tomcat"); + assertEcho(result, "02-Hello JUnit from Tomcat"); + assertEcho(result, "03-Hello JUnit from Tomcat"); + assertEcho(result, "04-Hello JUnit from Tomcat"); + assertEcho(result, "05-Hello JUnit from Tomcat"); + } + + + @Test + public void testBug56029() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug56029.jspx"); + + String result = res.toString(); + + Assert.assertTrue(result.contains("[1]:[1]")); + } + + + @Test + public void testBug56147() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug56147.jsp"); + + String result = res.toString(); + assertEcho(result, "00-OK"); + } + + + @Test + public void testBug56612() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug56612.jsp"); + + String result = res.toString(); + Assert.assertTrue(result.contains("00-''")); + } + + + /* + * java.lang should be imported by default + */ + @Test + public void testBug57141() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug57141.jsp"); + + String result = res.toString(); + assertEcho(result, "00-true"); + assertEcho(result, "01-false"); + assertEcho(result, "02-2147483647"); + } + + + /* + * BZ https://bz.apache.org/bugzilla/show_bug.cgi?id=57142 jakarta.servlet, jakarta.servlet.http and + * jakarta.servlet.jsp should be imported by default. + */ + @Test + public void testBug57142() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug57142.jsp"); + + String result = res.toString(); + // jakarta.servlet + assertEcho(result, "00-" + DispatcherType.ASYNC); + // No obvious static fields for jakarta.servlet.http + // Could hack something with HttpUtils... + // No obvious static fields for jakarta.servlet.jsp + // Wild card (package) import + assertEcho(result, "01-" + RoundingMode.HALF_UP); + // Class import + assertEcho(result, "02-" + Collections.EMPTY_LIST.size()); + } + + + /* + * BZ https://bz.apache.org/bugzilla/show_bug.cgi?id=57441 Can't validate function names defined in lambdas (or via + * imports) + */ + @Test + public void testBug57441() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug57441.jsp"); + + String result = res.toString(); + assertEcho(result, "00-11"); + } + + + @Test + public void testBug60032() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug60032.jsp"); + String result = res.toString(); + assertEcho(result, "{OK}"); + } + + + @Test + public void testBug60431() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug60431.jsp"); + String result = res.toString(); + assertEcho(result, "01-OK"); + assertEcho(result, "02-OK"); + assertEcho(result, "03-OK"); + } + + + @Test + public void testBug61854a() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug61854.jsp"); + String result = res.toString(); + assertEcho(result, "01-OK"); + } + + + // Assertion for text contained with

    , e.g. printed by tags:echo + private static void assertEcho(String result, String expected) { + Assert.assertTrue(result, result.indexOf("

    " + expected + "

    ") > 0); + } +} diff --git a/test/org/apache/el/TestExpressionFactory.java b/test/org/apache/el/TestExpressionFactory.java new file mode 100644 index 0000000..3f7e2e7 --- /dev/null +++ b/test/org/apache/el/TestExpressionFactory.java @@ -0,0 +1,48 @@ +/* + * 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.el; + +import jakarta.el.ELContext; +import jakarta.el.ExpressionFactory; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.el.ELContextImpl; + +public class TestExpressionFactory { + + @Test(expected = NullPointerException.class) + public void testCreateValueExpression2ParamNullExpectedType() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + Assert.assertNotNull(factory); + + factory.createValueExpression("foo", null); + } + + + @Test(expected = NullPointerException.class) + public void testCreateValueExpression3ParamNullExpectedType() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + Assert.assertNotNull(factory); + + ELContext context = new ELContextImpl(factory); + Assert.assertNotNull(context); + + factory.createValueExpression(context, "foo", null); + } +} diff --git a/test/org/apache/el/TestMethodExpressionImpl.java b/test/org/apache/el/TestMethodExpressionImpl.java new file mode 100644 index 0000000..f9af586 --- /dev/null +++ b/test/org/apache/el/TestMethodExpressionImpl.java @@ -0,0 +1,747 @@ +/* + * 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.el; + +import java.util.function.Function; + +import jakarta.el.ELContext; +import jakarta.el.ELProcessor; +import jakarta.el.ExpressionFactory; +import jakarta.el.MethodExpression; +import jakarta.el.MethodInfo; +import jakarta.el.MethodNotFoundException; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.jasper.el.ELContextImpl; + +public class TestMethodExpressionImpl { + + private static final String BUG53792 = "TEST_PASS"; + + private ExpressionFactory factory; + private ELContext context; + private TesterBeanB beanB; + + @Before + public void setUp() { + factory = ExpressionFactory.newInstance(); + context = new ELContextImpl(factory); + + TesterBeanA beanA = new TesterBeanA(); + beanA.setName("A"); + context.getVariableMapper().setVariable("beanA", factory.createValueExpression(beanA, TesterBeanA.class)); + + TesterBeanAA beanAA = new TesterBeanAA(); + beanAA.setName("AA"); + context.getVariableMapper().setVariable("beanAA", factory.createValueExpression(beanAA, TesterBeanAA.class)); + + TesterBeanAAA beanAAA = new TesterBeanAAA(); + beanAAA.setName("AAA"); + context.getVariableMapper().setVariable("beanAAA", factory.createValueExpression(beanAAA, TesterBeanAAA.class)); + + beanB = new TesterBeanB(); + beanB.setName("B"); + context.getVariableMapper().setVariable("beanB", factory.createValueExpression(beanB, TesterBeanB.class)); + + TesterBeanBB beanBB = new TesterBeanBB(); + beanBB.setName("BB"); + context.getVariableMapper().setVariable("beanBB", factory.createValueExpression(beanBB, TesterBeanBB.class)); + + TesterBeanBBB beanBBB = new TesterBeanBBB(); + beanBBB.setName("BBB"); + context.getVariableMapper().setVariable("beanBBB", factory.createValueExpression(beanBBB, TesterBeanBBB.class)); + + TesterBeanC beanC = new TesterBeanC(); + context.getVariableMapper().setVariable("beanC", factory.createValueExpression(beanC, TesterBeanC.class)); + + TesterBeanEnum beanEnum = new TesterBeanEnum(); + context.getVariableMapper().setVariable("beanEnum", + factory.createValueExpression(beanEnum, TesterBeanEnum.class)); + } + + @Test + public void testIsParametersProvided() { + MethodExpression me1 = factory.createMethodExpression(context, "${beanB.getName}", String.class, + new Class[] {}); + MethodExpression me2 = factory.createMethodExpression(context, "${beanB.sayHello('JUnit')}", String.class, + new Class[] { String.class }); + + Assert.assertFalse(me1.isParametersProvided()); + Assert.assertTrue(me2.isParametersProvided()); + } + + @Test + public void testInvoke() { + MethodExpression me1 = factory.createMethodExpression(context, "${beanB.getName}", String.class, + new Class[] {}); + MethodExpression me2 = factory.createMethodExpression(context, "${beanB.sayHello('JUnit')}", String.class, + new Class[] { String.class }); + MethodExpression me3 = factory.createMethodExpression(context, "${beanB.sayHello}", String.class, + new Class[] { String.class }); + + Assert.assertEquals("B", me1.invoke(context, null)); + Assert.assertEquals("Hello JUnit from B", me2.invoke(context, null)); + Assert.assertEquals("Hello JUnit from B", me2.invoke(context, new Object[] { "JUnit2" })); + Assert.assertEquals("Hello JUnit2 from B", me3.invoke(context, new Object[] { "JUnit2" })); + Assert.assertEquals("Hello JUnit from B", me2.invoke(context, new Object[] { null })); + Assert.assertEquals("Hello from B", me3.invoke(context, new Object[] { null })); + } + + @Test + public void testInvokeWithSuper() { + MethodExpression me = factory.createMethodExpression(context, "${beanA.setBean(beanBB)}", null, + new Class[] { TesterBeanB.class }); + me.invoke(context, null); + ValueExpression ve = factory.createValueExpression(context, "${beanA.bean.name}", String.class); + Object r = ve.getValue(context); + Assert.assertEquals("BB", r); + } + + @Test + public void testInvokeWithSuperABNoReturnTypeNoParamTypes() { + MethodExpression me2 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanB)}", null, null); + Object r2 = me2.invoke(context, null); + Assert.assertEquals("AB: Hello A from B", r2.toString()); + } + + @Test + public void testInvokeWithSuperABReturnTypeNoParamTypes() { + MethodExpression me3 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanB)}", String.class, + null); + Object r3 = me3.invoke(context, null); + Assert.assertEquals("AB: Hello A from B", r3.toString()); + } + + @Test + public void testInvokeWithSuperABNoReturnTypeParamTypes() { + MethodExpression me4 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanB)}", null, + new Class[] { TesterBeanA.class, TesterBeanB.class }); + Object r4 = me4.invoke(context, null); + Assert.assertEquals("AB: Hello A from B", r4.toString()); + } + + @Test + public void testInvokeWithSuperABReturnTypeParamTypes() { + MethodExpression me5 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanB)}", String.class, + new Class[] { TesterBeanA.class, TesterBeanB.class }); + Object r5 = me5.invoke(context, null); + Assert.assertEquals("AB: Hello A from B", r5.toString()); + } + + @Test + public void testInvokeWithSuperABB() { + MethodExpression me6 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanBB)}", null, null); + Object r6 = me6.invoke(context, null); + Assert.assertEquals("ABB: Hello A from BB", r6.toString()); + } + + @Test + public void testInvokeWithSuperABBB() { + MethodExpression me7 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanBBB)}", null, null); + Object r7 = me7.invoke(context, null); + Assert.assertEquals("ABB: Hello A from BBB", r7.toString()); + } + + @Test + public void testInvokeWithSuperAAB() { + MethodExpression me8 = factory.createMethodExpression(context, "${beanC.sayHello(beanAA,beanB)}", null, null); + Object r8 = me8.invoke(context, null); + Assert.assertEquals("AAB: Hello AA from B", r8.toString()); + } + + @Test + public void testInvokeWithSuperAABB() { + MethodExpression me9 = factory.createMethodExpression(context, "${beanC.sayHello(beanAA,beanBB)}", null, null); + Exception e = null; + try { + me9.invoke(context, null); + } catch (Exception e1) { + e = e1; + } + // Expected to fail + Assert.assertNotNull(e); + } + + @Test + public void testInvokeWithSuperAABBB() { + // The Java compiler reports this as ambiguous. Using the parameter that + // matches exactly seems reasonable to limit the scope of the method + // search so the EL will find a match. + MethodExpression me10 = factory.createMethodExpression(context, "${beanC.sayHello(beanAA,beanBBB)}", null, + null); + Object r10 = me10.invoke(context, null); + Assert.assertEquals("AAB: Hello AA from BBB", r10.toString()); + } + + @Test + public void testInvokeWithSuperAAAB() { + MethodExpression me11 = factory.createMethodExpression(context, "${beanC.sayHello(beanAAA,beanB)}", null, null); + Object r11 = me11.invoke(context, null); + Assert.assertEquals("AAB: Hello AAA from B", r11.toString()); + } + + @Test + public void testInvokeWithSuperAAABB() { + // The Java compiler reports this as ambiguous. Using the parameter that + // matches exactly seems reasonable to limit the scope of the method + // search so the EL will find a match. + MethodExpression me12 = factory.createMethodExpression(context, "${beanC.sayHello(beanAAA,beanBB)}", null, + null); + Object r12 = me12.invoke(context, null); + Assert.assertEquals("ABB: Hello AAA from BB", r12.toString()); + } + + @Test + public void testInvokeWithSuperAAABBB() { + MethodExpression me13 = factory.createMethodExpression(context, "${beanC.sayHello(beanAAA,beanBBB)}", null, + null); + Exception e = null; + try { + me13.invoke(context, null); + } catch (Exception e1) { + e = e1; + } + // Expected to fail + Assert.assertNotNull(e); + } + + @Test + public void testInvokeWithVarArgsAB() throws Exception { + MethodExpression me1 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanB,beanB)}", null, + null); + Exception e = null; + try { + me1.invoke(context, null); + } catch (Exception e1) { + e = e1; + } + // Expected to fail + Assert.assertNotNull(e); + } + + @Test + public void testInvokeWithVarArgsABB() throws Exception { + MethodExpression me2 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanBB,beanBB)}", null, + null); + Object r2 = me2.invoke(context, null); + Assert.assertEquals("ABB[]: Hello A from BB, BB", r2.toString()); + } + + @Test + public void testInvokeWithVarArgsABBB() throws Exception { + MethodExpression me3 = factory.createMethodExpression(context, "${beanC.sayHello(beanA,beanBBB,beanBBB)}", null, + null); + Object r3 = me3.invoke(context, null); + Assert.assertEquals("ABB[]: Hello A from BBB, BBB", r3.toString()); + } + + @Test + public void testInvokeWithVarArgsAAB() throws Exception { + MethodExpression me4 = factory.createMethodExpression(context, "${beanC.sayHello(beanAA,beanB,beanB)}", null, + null); + Exception e = null; + try { + me4.invoke(context, null); + } catch (Exception e1) { + e = e1; + } + // Expected to fail + Assert.assertNotNull(e); + } + + @Test + public void testInvokeWithVarArgsAABB() throws Exception { + MethodExpression me5 = factory.createMethodExpression(context, "${beanC.sayHello(beanAA,beanBB,beanBB)}", null, + null); + Object r5 = me5.invoke(context, null); + Assert.assertEquals("ABB[]: Hello AA from BB, BB", r5.toString()); + } + + @Test + public void testInvokeWithVarArgsAABBB() throws Exception { + MethodExpression me6 = factory.createMethodExpression(context, "${beanC.sayHello(beanAA,beanBBB,beanBBB)}", + null, null); + Object r6 = me6.invoke(context, null); + Assert.assertEquals("ABB[]: Hello AA from BBB, BBB", r6.toString()); + } + + @Test + public void testInvokeWithVarArgsAAAB() throws Exception { + MethodExpression me7 = factory.createMethodExpression(context, "${beanC.sayHello(beanAAA,beanB,beanB)}", null, + null); + Exception e = null; + try { + me7.invoke(context, null); + } catch (Exception e1) { + e = e1; + } + // Expected to fail + Assert.assertNotNull(e); + } + + @Test + public void testInvokeWithVarArgsAAABB() throws Exception { + MethodExpression me8 = factory.createMethodExpression(context, "${beanC.sayHello(beanAAA,beanBB,beanBB)}", null, + null); + Object r8 = me8.invoke(context, null); + Assert.assertEquals("ABB[]: Hello AAA from BB, BB", r8.toString()); + } + + @Test + public void testInvokeWithVarArgsAAABBB() throws Exception { + MethodExpression me9 = factory.createMethodExpression(context, "${beanC.sayHello(beanAAA,beanBBB,beanBBB)}", + null, null); + Object r9 = me9.invoke(context, null); + Assert.assertEquals("ABB[]: Hello AAA from BBB, BBB", r9.toString()); + } + + /* + * This is also tested implicitly in numerous places elsewhere in this class. + */ + @Test + public void testBug49655() throws Exception { + // This is the call the failed + MethodExpression me = factory.createMethodExpression(context, "#{beanA.setName('New value')}", null, null); + // The rest is to check it worked correctly + me.invoke(context, null); + ValueExpression ve = factory.createValueExpression(context, "#{beanA.name}", String.class); + Assert.assertEquals("New value", ve.getValue(context)); + } + + @Test + public void testBugPrimitives() throws Exception { + MethodExpression me = factory.createMethodExpression(context, "${beanA.setValLong(5)}", null, null); + me.invoke(context, null); + ValueExpression ve = factory.createValueExpression(context, "#{beanA.valLong}", String.class); + Assert.assertEquals("5", ve.getValue(context)); + } + + @Test + public void testBug50449a() throws Exception { + MethodExpression me1 = factory.createMethodExpression(context, "${beanB.sayHello()}", null, null); + String actual = (String) me1.invoke(context, null); + Assert.assertEquals("Hello from B", actual); + } + + @Test + public void testBug50449b() throws Exception { + MethodExpression me1 = factory.createMethodExpression(context, "${beanB.sayHello('Tomcat')}", null, null); + String actual = (String) me1.invoke(context, null); + Assert.assertEquals("Hello Tomcat from B", actual); + } + + @Test + public void testBug50790a() throws Exception { + ValueExpression ve = + factory.createValueExpression(context, "#{beanAA.name.contains(beanA.name)}", Boolean.class); + Boolean actual = (Boolean) ve.getValue(context); + Assert.assertEquals(Boolean.TRUE, actual); + } + + @Test + public void testBug50790b() throws Exception { + ValueExpression ve = + factory.createValueExpression(context, "#{beanA.name.contains(beanAA.name)}", Boolean.class); + Boolean actual = (Boolean) ve.getValue(context); + Assert.assertEquals(Boolean.FALSE, actual); + } + + @Test + public void testBug52445a() { + MethodExpression me = factory.createMethodExpression(context, "${beanA.setBean(beanBB)}", null, + new Class[] { TesterBeanB.class }); + me.invoke(context, null); + + MethodExpression me1 = factory.createMethodExpression(context, "${beanA.bean.sayHello()}", null, null); + String actual = (String) me1.invoke(context, null); + Assert.assertEquals("Hello from BB", actual); + } + + @Test + public void testBug52970() { + MethodExpression me = factory.createMethodExpression(context, "${beanEnum.submit('APPLE')}", null, + new Class[] { TesterBeanEnum.class }); + me.invoke(context, null); + + ValueExpression ve = factory.createValueExpression(context, "#{beanEnum.lastSubmitted}", TesterEnum.class); + TesterEnum actual = (TesterEnum) ve.getValue(context); + Assert.assertEquals(TesterEnum.APPLE, actual); + + } + + @Test + public void testBug53792a() { + MethodExpression me = factory.createMethodExpression(context, "${beanA.setBean(beanB)}", null, + new Class[] { TesterBeanB.class }); + me.invoke(context, null); + me = factory.createMethodExpression(context, "${beanB.setName('" + BUG53792 + "')}", null, + new Class[] { TesterBeanB.class }); + me.invoke(context, null); + + ValueExpression ve = factory.createValueExpression(context, "#{beanA.getBean().name}", String.class); + String actual = (String) ve.getValue(context); + Assert.assertEquals(BUG53792, actual); + } + + @Test + public void testBug53792b() { + MethodExpression me = factory.createMethodExpression(context, "${beanA.setBean(beanB)}", null, + new Class[] { TesterBeanB.class }); + me.invoke(context, null); + me = factory.createMethodExpression(context, "${beanB.setName('" + BUG53792 + "')}", null, + new Class[] { TesterBeanB.class }); + me.invoke(context, null); + + ValueExpression ve = factory.createValueExpression(context, "#{beanA.getBean().name.length()}", Integer.class); + Integer actual = (Integer) ve.getValue(context); + Assert.assertEquals(Integer.valueOf(BUG53792.length()), actual); + } + + + @Test + public void testBug53792c() { + MethodExpression me = factory.createMethodExpression(context, "#{beanB.sayHello().length()}", null, + new Class[] {}); + Integer result = (Integer) me.invoke(context, null); + Assert.assertEquals(beanB.sayHello().length(), result.intValue()); + } + + + @Test + public void testBug53792d() { + MethodExpression me = factory.createMethodExpression(context, "#{beanB.sayHello().length()}", null, + new Class[] {}); + Integer result = (Integer) me.invoke(context, new Object[] { "foo" }); + Assert.assertEquals(beanB.sayHello().length(), result.intValue()); + } + + + @Test + public void testBug56797a() { + MethodExpression me = factory.createMethodExpression(context, "${beanAA.echo1('Hello World!')}", null, null); + Object r = me.invoke(context, null); + Assert.assertEquals("AA1Hello World!", r.toString()); + } + + + @Test + public void testBug56797b() { + MethodExpression me = factory.createMethodExpression(context, "${beanAA.echo2('Hello World!')}", null, null); + Object r = me.invoke(context, null); + Assert.assertEquals("AA2Hello World!", r.toString()); + } + + + @Test(expected = MethodNotFoundException.class) + public void testBug57855a() { + MethodExpression me = factory.createMethodExpression(context, "${beanAA.echo2}", null, + new Class[] { String.class }); + me.invoke(context, new Object[0]); + } + + + @Test(expected = IllegalArgumentException.class) + public void testBug57855b() { + MethodExpression me = factory.createMethodExpression(context, "${beanAA.echo2}", null, + new Class[] { String.class }); + me.invoke(context, null); + } + + @Test + public void testBug57855c() { + MethodExpression me = factory.createMethodExpression(context, "${beanB.echo}", null, + new Class[] { String.class }); + me.invoke(context, null); + } + + + @Test + public void testBug57855d() { + MethodExpression me = factory.createMethodExpression(context, "${beanB.echo}", null, + new Class[] { String.class }); + Object r = me.invoke(context, new String[] { "aaa" }); + Assert.assertEquals("aaa", r.toString()); + } + + @Test(expected = MethodNotFoundException.class) + public void testBug57855e() { + MethodExpression me = factory.createMethodExpression(context, "${beanB.echo}", null, + new Class[] { String.class }); + Object r = me.invoke(context, new String[] { "aaa", "bbb" }); + Assert.assertEquals("aaa, bbb", r.toString()); + } + + + @Test(expected = IllegalArgumentException.class) + public void testBug60844() { + MethodExpression me2 = factory.createMethodExpression(context, "${beanC.sayHello}", null, + new Class[] { TesterBeanA.class, TesterBeanB.class }); + me2.invoke(context, new Object[] { new Object() }); + } + + + @Test + public void testVarArgsBeanFEnum() { + doTestVarArgsBeanF("beanF.doTest(apple)", (a) -> a.doTest(TesterEnum.APPLE)); + } + + + @Test + public void testVarArgsBeanFEnumEnum() { + doTestVarArgsBeanF("beanF.doTest(apple,apple)", (a) -> a.doTest(TesterEnum.APPLE, TesterEnum.APPLE)); + } + + + @Test + public void testVarArgsBeanFEnumString() { + doTestVarArgsBeanF("beanF.doTest(apple,'apple')", (a) -> a.doTest(TesterEnum.APPLE, "apple")); + } + + + @Test + public void testVarArgsBeanFEnumVEnum() { + doTestVarArgsBeanF("beanF.doTest(apple,apple,apple)", + (a) -> a.doTest(TesterEnum.APPLE, TesterEnum.APPLE, TesterEnum.APPLE)); + } + + + @Test + public void testVarArgsBeanFEnumVString() { + doTestVarArgsBeanF("beanF.doTest(apple,'apple','apple')", (a) -> a.doTest(TesterEnum.APPLE, "apple", "apple")); + } + + + @Test + public void testVarArgsBeanFString() { + doTestVarArgsBeanF("beanF.doTest('apple')", (a) -> a.doTest("apple")); + } + + + @Test + public void testVarArgsBeanFStringEnum() { + doTestVarArgsBeanF("beanF.doTest('apple',apple)", (a) -> a.doTest("apple", TesterEnum.APPLE)); + } + + + @Test + public void testVarArgsBeanFStringString() { + doTestVarArgsBeanF("beanF.doTest('apple','apple')", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanFStringVEnum() { + doTestVarArgsBeanF("beanF.doTest('apple',apple,apple)", + (a) -> a.doTest("apple", TesterEnum.APPLE, TesterEnum.APPLE)); + } + + + @Test + public void testVarArgsBeanFStringVString() { + doTestVarArgsBeanF("beanF.doTest('apple','apple','apple')", (a) -> a.doTest("apple", "apple", "apple")); + } + + + private void doTestVarArgsBeanF(String expression, Function func) { + ELProcessor elp = new ELProcessor(); + elp.defineBean("apple", TesterEnum.APPLE); + elp.defineBean("beanF", new TesterBeanF()); + String elResult = elp.eval(expression); + String javaResult = func.apply(new TesterBeanF()); + Assert.assertEquals(javaResult, elResult); + } + + + @Test + public void testVarArgsBeanGEnum() { + doTestVarArgsBeanG("beanG.doTest(apple)", (a) -> a.doTest("apple")); + } + + + @Test + public void testVarArgsBeanGEnumEnum() { + doTestVarArgsBeanG("beanG.doTest(apple,apple)", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanGEnumString() { + doTestVarArgsBeanG("beanG.doTest(apple,'apple')", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanGEnumVEnum() { + doTestVarArgsBeanG("beanG.doTest(apple,apple,apple)", (a) -> a.doTest("apple", "apple", "apple")); + } + + + @Test + public void testVarArgsBeanGEnumVString() { + doTestVarArgsBeanG("beanG.doTest(apple,'apple','apple')", (a) -> a.doTest("apple", "apple", "apple")); + } + + + @Test + public void testVarArgsBeanGString() { + doTestVarArgsBeanG("beanG.doTest('apple')", (a) -> a.doTest("apple")); + } + + + @Test + public void testVarArgsBeanGStringEnum() { + doTestVarArgsBeanG("beanG.doTest('apple',apple)", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanGStringString() { + doTestVarArgsBeanG("beanG.doTest('apple','apple')", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanGStringVEnum() { + doTestVarArgsBeanG("beanG.doTest('apple',apple,apple)", (a) -> a.doTest("apple", "apple", "apple")); + } + + + @Test + public void testVarArgsBeanGStringVString() { + doTestVarArgsBeanG("beanG.doTest('apple','apple','apple')", (a) -> a.doTest("apple", "apple", "apple")); + } + + + private void doTestVarArgsBeanG(String expression, Function func) { + ELProcessor elp = new ELProcessor(); + elp.defineBean("apple", TesterEnum.APPLE); + elp.defineBean("beanG", new TesterBeanG()); + String elResult = elp.eval(expression); + String javaResult = func.apply(new TesterBeanG()); + Assert.assertEquals(javaResult, elResult); + } + + @Test + public void testVarArgsBeanHEnum() { + doTestVarArgsBeanH("beanH.doTest(apple)", (a) -> a.doTest("apple")); + } + + + @Test + public void testVarArgsBeanHEnumEnum() { + doTestVarArgsBeanH("beanH.doTest(apple,apple)", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanHEnumString() { + doTestVarArgsBeanH("beanH.doTest(apple,'apple')", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanHEnumVEnum() { + doTestVarArgsBeanH("beanH.doTest(apple,apple,apple)", (a) -> a.doTest("apple", "apple", "apple")); + } + + + @Test + public void testVarArgsBeanHEnumVString() { + doTestVarArgsBeanH("beanH.doTest(apple,'apple','apple')", (a) -> a.doTest("apple", "apple", "apple")); + } + + + @Test + public void testVarArgsBeanHString() { + doTestVarArgsBeanH("beanH.doTest('apple')", (a) -> a.doTest("apple")); + } + + + @Test + public void testVarArgsBeanHStringEnum() { + doTestVarArgsBeanH("beanH.doTest('apple',apple)", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanHStringString() { + doTestVarArgsBeanH("beanH.doTest('apple','apple')", (a) -> a.doTest("apple", "apple")); + } + + + @Test + public void testVarArgsBeanHStringVEnum() { + doTestVarArgsBeanH("beanH.doTest('apple',apple,apple)", (a) -> a.doTest("apple", "apple", "apple")); + } + + + @Test + public void testVarArgsBeanHStringVString() { + doTestVarArgsBeanH("beanH.doTest('apple','apple','apple')", (a) -> a.doTest("apple", "apple", "apple")); + } + + + private void doTestVarArgsBeanH(String expression, Function func) { + ELProcessor elp = new ELProcessor(); + elp.defineBean("apple", TesterEnum.APPLE); + elp.defineBean("beanH", new TesterBeanH()); + String elResult = elp.eval(expression); + String javaResult = func.apply(new TesterBeanH()); + Assert.assertEquals(javaResult, elResult); + } + + + @Test + public void testPreferNoVarArgs() { + ELProcessor elp = new ELProcessor(); + TesterBeanAAA bean = new TesterBeanAAA(); + bean.setName("xyz"); + elp.defineBean("bean2", bean); + elp.defineBean("bean1", new TesterBeanI()); + String elResult = elp.eval("bean1.echo(bean2)"); + Assert.assertEquals("No varargs: xyz", elResult); + } + + + @Test + public void testGetMethodInfo01() throws Exception { + MethodExpression me = factory.createMethodExpression(context, "#{beanA.setName('New value')}", null, null); + // This is the call that failed + MethodInfo mi = me.getMethodInfo(context); + // The rest is to check it worked correctly + Assert.assertEquals(void.class, mi.getReturnType()); + Assert.assertEquals(1, mi.getParamTypes().length); + Assert.assertEquals(String.class, mi.getParamTypes()[0]); + } + + + @Test + public void testGetMethodInfo02() throws Exception { + MethodExpression me = factory.createMethodExpression(context, "#{beanA.setName}", null, + new Class[] { String.class }); + // This is the call that failed + MethodInfo mi = me.getMethodInfo(context); + // The rest is to check it worked correctly + Assert.assertEquals(void.class, mi.getReturnType()); + Assert.assertEquals(1, mi.getParamTypes().length); + Assert.assertEquals(String.class, mi.getParamTypes()[0]); + } +} diff --git a/test/org/apache/el/TestValueExpressionImpl.java b/test/org/apache/el/TestValueExpressionImpl.java new file mode 100644 index 0000000..a56f569 --- /dev/null +++ b/test/org/apache/el/TestValueExpressionImpl.java @@ -0,0 +1,330 @@ +/* + * 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.el; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import jakarta.el.ELContext; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; +import jakarta.el.ValueReference; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.el.ELContextImpl; + +public class TestValueExpressionImpl { + + @Test + public void testGetValueReference() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanB beanB = new TesterBeanB(); + beanB.setName("Tomcat"); + ValueExpression var = factory.createValueExpression(beanB, TesterBeanB.class); + context.getVariableMapper().setVariable("beanB", var); + + ValueExpression ve = factory.createValueExpression(context, "${beanB.name}", String.class); + + // First check the basics work + String result = (String) ve.getValue(context); + Assert.assertEquals("Tomcat", result); + + // Now check the value reference + ValueReference vr = ve.getValueReference(context); + Assert.assertNotNull(vr); + + Assert.assertEquals(beanB, vr.getBase()); + Assert.assertEquals("name", vr.getProperty()); + } + + @Test + public void testGetValueReferenceVariable() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanB beanB = new TesterBeanB(); + beanB.setName("Tomcat"); + ValueExpression var = factory.createValueExpression(beanB, TesterBeanB.class); + context.getVariableMapper().setVariable("beanB", var); + + ValueExpression var2 = factory.createValueExpression(context, "${beanB.name}", String.class); + + context.getVariableMapper().setVariable("foo", var2); + + ValueExpression ve = factory.createValueExpression(context, "${foo}", ValueExpression.class); + + + // Now check the value reference + ValueReference vr = ve.getValueReference(context); + Assert.assertNotNull(vr); + + Assert.assertEquals(beanB, vr.getBase()); + Assert.assertEquals("name", vr.getProperty()); + } + + @Test + public void testBug49345() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanA beanA = new TesterBeanA(); + TesterBeanB beanB = new TesterBeanB(); + beanB.setName("Tomcat"); + beanA.setBean(beanB); + + ValueExpression var = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", var); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.bean.name}", String.class); + + // First check the basics work + String result = (String) ve.getValue(context); + Assert.assertEquals("Tomcat", result); + + // Now check the value reference + ValueReference vr = ve.getValueReference(context); + Assert.assertNotNull(vr); + + Assert.assertEquals(beanB, vr.getBase()); + Assert.assertEquals("name", vr.getProperty()); + } + + @Test + public void testBug50105() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterEnum testEnum = TesterEnum.APPLE; + + ValueExpression var = factory.createValueExpression(testEnum, TesterEnum.class); + context.getVariableMapper().setVariable("testEnum", var); + + // When coercing an Enum to a String, name() should always be used. + ValueExpression ve1 = factory.createValueExpression(context, "${testEnum}", String.class); + String result1 = (String) ve1.getValue(context); + Assert.assertEquals("APPLE", result1); + + ValueExpression ve2 = factory.createValueExpression(context, "foo${testEnum}bar", String.class); + String result2 = (String) ve2.getValue(context); + Assert.assertEquals("fooAPPLEbar", result2); + } + + @Test + public void testBug51177ObjectMap() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + Object o1 = "String value"; + Object o2 = Integer.valueOf(32); + + Map map = new HashMap<>(); + map.put("key1", o1); + map.put("key2", o2); + + ValueExpression var = factory.createValueExpression(map, Map.class); + context.getVariableMapper().setVariable("map", var); + + ValueExpression ve1 = factory.createValueExpression(context, "${map.key1}", Object.class); + ve1.setValue(context, o2); + Assert.assertEquals(o2, ve1.getValue(context)); + + ValueExpression ve2 = factory.createValueExpression(context, "${map.key2}", Object.class); + ve2.setValue(context, o1); + Assert.assertEquals(o1, ve2.getValue(context)); + } + + @Test + public void testBug51177ObjectList() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + Object o1 = "String value"; + Object o2 = Integer.valueOf(32); + + List list = new ArrayList<>(); + list.add(0, o1); + list.add(1, o2); + + ValueExpression var = factory.createValueExpression(list, List.class); + context.getVariableMapper().setVariable("list", var); + + ValueExpression ve1 = factory.createValueExpression(context, "${list[0]}", Object.class); + ve1.setValue(context, o2); + Assert.assertEquals(o2, ve1.getValue(context)); + + ValueExpression ve2 = factory.createValueExpression(context, "${list[1]}", Object.class); + ve2.setValue(context, o1); + Assert.assertEquals(o1, ve2.getValue(context)); + } + + + /* + * Test returning an empty list as a bean property. + */ + @Test + public void testBug51544Bean() throws Exception { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanA beanA = new TesterBeanA(); + beanA.setValList(Collections.emptyList()); + + ValueExpression var = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", var); + + ValueExpression ve = factory.createValueExpression(context, "${beanA.valList.size()}", Integer.class); + + Integer result = (Integer) ve.getValue(context); + Assert.assertEquals(Integer.valueOf(0), result); + } + + + /* + * Test using list directly as variable. + */ + @Test + public void testBug51544Direct() throws Exception { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + List list = Collections.emptyList(); + + ValueExpression var = factory.createValueExpression(list, List.class); + context.getVariableMapper().setVariable("list", var); + + ValueExpression ve = factory.createValueExpression(context, "${list.size()}", Integer.class); + + Integer result = (Integer) ve.getValue(context); + Assert.assertEquals(Integer.valueOf(0), result); + } + + + @Test + public void testBug56522SetNullValue() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanB beanB = new TesterBeanB(); + beanB.setName("Tomcat"); + ValueExpression var = factory.createValueExpression(beanB, TesterBeanB.class); + context.getVariableMapper().setVariable("beanB", var); + + ValueExpression ve = factory.createValueExpression(context, "${beanB.name}", String.class); + + // First check the basics work + String result = (String) ve.getValue(context); + Assert.assertEquals("Tomcat", result); + + // Now set the value to null + ve.setValue(context, null); + + Assert.assertEquals("", beanB.getName()); + } + + + @Test + public void testOptional01() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + final String data = "some data"; + + TesterBeanJ beanJ = new TesterBeanJ(); + TesterBeanJ beanJ2 = new TesterBeanJ(); + beanJ2.setData(data); + beanJ.setBean(beanJ2); + + ValueExpression var = factory.createValueExpression(beanJ, TesterBeanJ.class); + context.getVariableMapper().setVariable("beanJ", var); + + ValueExpression ve = factory.createValueExpression(context, "${beanJ.optionalBean.map(b -> b.data)}", + Optional.class); + + @SuppressWarnings("unchecked") + Optional result = (Optional) ve.getValue(context); + Assert.assertEquals(data, result.get()); + } + + + @Test + public void testOptional02() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanJ beanJ = new TesterBeanJ(); + + ValueExpression var = factory.createValueExpression(beanJ, TesterBeanJ.class); + context.getVariableMapper().setVariable("beanJ", var); + + ValueExpression ve = factory.createValueExpression(context, "${beanJ.optionalBean.map(b -> b.data)}", + Optional.class); + + @SuppressWarnings("unchecked") + Optional result = (Optional) ve.getValue(context); + Assert.assertTrue(result.isEmpty()); + } + + + @Test + public void testOptional03() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + final String data = "some data"; + + TesterBeanJ beanJ = new TesterBeanJ(); + TesterBeanJ beanJ2 = new TesterBeanJ(); + beanJ2.setData(data); + beanJ.setBean(beanJ2); + + ValueExpression var = factory.createValueExpression(beanJ, TesterBeanJ.class); + context.getVariableMapper().setVariable("beanJ", var); + + ValueExpression ve = factory.createValueExpression(context, "${beanJ.optionalBean.get().data}", String.class); + + String result = (String) ve.getValue(context); + Assert.assertEquals(data, result); + } + + + @Test + public void testOptional04() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanJ beanJ = new TesterBeanJ(); + + ValueExpression var = factory.createValueExpression(beanJ, TesterBeanJ.class); + context.getVariableMapper().setVariable("beanJ", var); + + ValueExpression ve = factory.createValueExpression(context, + "${beanJ.optionalBean.map(b -> b.data).orElse(null)}", String.class); + + String result = (String) ve.getValue(context); + // Result is null but is coerced to String which makes it "" + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } +} diff --git a/test/org/apache/el/TesterBeanA.java b/test/org/apache/el/TesterBeanA.java new file mode 100644 index 0000000..6e87d88 --- /dev/null +++ b/test/org/apache/el/TesterBeanA.java @@ -0,0 +1,66 @@ +/* + * 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.el; + +import java.util.List; + +public class TesterBeanA { + private TesterBeanB bean; + private String name; + private long valLong; + private List valList; + + public TesterBeanB getBean() { + return bean; + } + + public void setBean(TesterBeanB bean) { + this.bean = bean; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getValLong() { + return valLong; + } + + public void setValLong(long valLong) { + this.valLong = valLong; + } + + public List getValList() { + return valList; + } + + public void setValList(List valList) { + this.valList = valList; + } + + public CharSequence echo1(CharSequence cs) { + return "A1" + cs; + } + + public CharSequence echo2(String s) { + return "A2" + s; + } +} diff --git a/test/org/apache/el/TesterBeanAA.java b/test/org/apache/el/TesterBeanAA.java new file mode 100644 index 0000000..a0fef0a --- /dev/null +++ b/test/org/apache/el/TesterBeanAA.java @@ -0,0 +1,30 @@ +/* + * 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.el; + +public class TesterBeanAA extends TesterBeanA { + + @Override + public String echo1(CharSequence cs) { + return "AA1" + cs.toString(); + } + + @Override + public String echo2(String s) { + return "AA2" + s; + } +} diff --git a/test/org/apache/el/TesterBeanAAA.java b/test/org/apache/el/TesterBeanAAA.java new file mode 100644 index 0000000..b907ec1 --- /dev/null +++ b/test/org/apache/el/TesterBeanAAA.java @@ -0,0 +1,22 @@ +/* + * 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.el; + +public class TesterBeanAAA extends TesterBeanAA { + // No additional implementation - just need a class that extends AA for + // testing EL methods calls +} diff --git a/test/org/apache/el/TesterBeanB.java b/test/org/apache/el/TesterBeanB.java new file mode 100644 index 0000000..e98f6ee --- /dev/null +++ b/test/org/apache/el/TesterBeanB.java @@ -0,0 +1,52 @@ +/* + * 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.el; + +public class TesterBeanB { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String sayHello() { + return "Hello from " + name; + } + + public String sayHello(String to) { + return "Hello " + to + " from " + name; + } + + public String echo(String... strings) { + if (strings == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < strings.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(strings[i]); + } + return sb.toString(); + } +} diff --git a/test/org/apache/el/TesterBeanBB.java b/test/org/apache/el/TesterBeanBB.java new file mode 100644 index 0000000..e50bfaf --- /dev/null +++ b/test/org/apache/el/TesterBeanBB.java @@ -0,0 +1,30 @@ +/* + * 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.el; + +public class TesterBeanBB extends TesterBeanB { + + private String extra; + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } +} diff --git a/test/org/apache/el/TesterBeanBBB.java b/test/org/apache/el/TesterBeanBBB.java new file mode 100644 index 0000000..7b7d50d --- /dev/null +++ b/test/org/apache/el/TesterBeanBBB.java @@ -0,0 +1,22 @@ +/* + * 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.el; + +public class TesterBeanBBB extends TesterBeanBB { + // No additional implementation - just need a class that extends BB for + // testing EL methods calls +} diff --git a/test/org/apache/el/TesterBeanC.java b/test/org/apache/el/TesterBeanC.java new file mode 100644 index 0000000..aa4801e --- /dev/null +++ b/test/org/apache/el/TesterBeanC.java @@ -0,0 +1,42 @@ +/* + * 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.el; + +public class TesterBeanC { + public String sayHello(TesterBeanA a, TesterBeanB b) { + return "AB: Hello " + a.getName() + " from " + b.getName(); + } + + public String sayHello(TesterBeanAA a, TesterBeanB b) { + return "AAB: Hello " + a.getName() + " from " + b.getName(); + } + + public String sayHello(TesterBeanA a, TesterBeanBB b) { + return "ABB: Hello " + a.getName() + " from " + b.getName(); + } + + public String sayHello(TesterBeanA a, TesterBeanBB... b) { + StringBuilder result = new StringBuilder("ABB[]: Hello " + a.getName() + " from "); + for (int i = 0; i < b.length; i++) { + if (i > 0) { + result.append(", "); + } + result.append(b[i].getName()); + } + return result.toString(); + } +} diff --git a/test/org/apache/el/TesterBeanD.java b/test/org/apache/el/TesterBeanD.java new file mode 100644 index 0000000..d4d70d7 --- /dev/null +++ b/test/org/apache/el/TesterBeanD.java @@ -0,0 +1,40 @@ +/* + * 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.el; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class used to test the calling of overloaded methods. + */ +public class TesterBeanD { + + private List things = new ArrayList<>(); + + public void addThing(String thing) { + things.add(thing); + } + + public void addThing(Class clazz) { + things.add(clazz); + } + + public List getThings() { + return things; + } +} diff --git a/test/org/apache/el/TesterBeanEnum.java b/test/org/apache/el/TesterBeanEnum.java new file mode 100644 index 0000000..df1805a --- /dev/null +++ b/test/org/apache/el/TesterBeanEnum.java @@ -0,0 +1,30 @@ +/* + * 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.el; + +public class TesterBeanEnum { + + private volatile TesterEnum lastSubmitted = null; + + public void submit(TesterEnum testerEnum) { + this.lastSubmitted = testerEnum; + } + + public TesterEnum getLastSubmitted() { + return lastSubmitted; + } +} diff --git a/test/org/apache/el/TesterBeanF.java b/test/org/apache/el/TesterBeanF.java new file mode 100644 index 0000000..42e340c --- /dev/null +++ b/test/org/apache/el/TesterBeanF.java @@ -0,0 +1,82 @@ +/* + * 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.el; + +/** + * Test cases based on https://bz.apache.org/bugzilla/show_bug.cgi?id=65358 + */ +public class TesterBeanF { + + @SuppressWarnings("unused") + public String doTest(TesterEnum param1) { + return "Enum"; + } + + + @SuppressWarnings("unused") + public String doTest(TesterEnum param1, TesterEnum param2) { + return "Enum-Enum"; + } + + + @SuppressWarnings("unused") + public String doTest(TesterEnum param1, String param2) { + return "Enum-String"; + } + + + @SuppressWarnings("unused") + public String doTest(TesterEnum param1, TesterEnum... param2) { + return "Enum-VEnum"; + } + + + @SuppressWarnings("unused") + public String doTest(TesterEnum param1, String... param2) { + return "Enum-VString"; + } + + + @SuppressWarnings("unused") + public String doTest(String param1) { + return "String"; + } + + + @SuppressWarnings("unused") + public String doTest(String param1, TesterEnum param2) { + return "String-Enum"; + } + + + @SuppressWarnings("unused") + public String doTest(String param1, String param2) { + return "String-String"; + } + + + @SuppressWarnings("unused") + public String doTest(String param1, TesterEnum... param2) { + return "String-VEnum"; + } + + + @SuppressWarnings("unused") + public String doTest(String param1, String... param2) { + return "String-VString"; + } +} diff --git a/test/org/apache/el/TesterBeanG.java b/test/org/apache/el/TesterBeanG.java new file mode 100644 index 0000000..87a03ab --- /dev/null +++ b/test/org/apache/el/TesterBeanG.java @@ -0,0 +1,41 @@ +/* + * 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.el; + +/** + * Test cases based on https://bz.apache.org/bugzilla/show_bug.cgi?id=65358 BeanG is BeanF with all the methods that use + * Enum parameters removed so that the EL caller always has to coerce the Enum to String. + */ +public class TesterBeanG { + + @SuppressWarnings("unused") + public String doTest(String param1) { + return "String"; + } + + + @SuppressWarnings("unused") + public String doTest(String param1, String param2) { + return "String-String"; + } + + + @SuppressWarnings("unused") + public String doTest(String param1, String... param2) { + return "String-VString"; + } +} diff --git a/test/org/apache/el/TesterBeanH.java b/test/org/apache/el/TesterBeanH.java new file mode 100644 index 0000000..f6d7f0c --- /dev/null +++ b/test/org/apache/el/TesterBeanH.java @@ -0,0 +1,28 @@ +/* + * 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.el; + +/** + * Test cases based on https://bz.apache.org/bugzilla/show_bug.cgi?id=65358 BeanH is BeanG with just the varargs method. + */ +public class TesterBeanH { + + @SuppressWarnings("unused") + public String doTest(String param1, String... param2) { + return "String-VString"; + } +} diff --git a/test/org/apache/el/TesterBeanI.java b/test/org/apache/el/TesterBeanI.java new file mode 100644 index 0000000..9372fc1 --- /dev/null +++ b/test/org/apache/el/TesterBeanI.java @@ -0,0 +1,29 @@ +/* + * 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.el; + +public class TesterBeanI { + + public String echo(TesterBeanA beanA) { + return "No varargs: " + beanA.getName(); + } + + + public String echo(TesterBeanAAA beanAAA, @SuppressWarnings("unused") String... extras) { + return "With varargs: " + beanAAA.getName(); + } +} diff --git a/test/org/apache/el/TesterBeanJ.java b/test/org/apache/el/TesterBeanJ.java new file mode 100644 index 0000000..da0c416 --- /dev/null +++ b/test/org/apache/el/TesterBeanJ.java @@ -0,0 +1,50 @@ +/* + * 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.el; + +import java.util.Optional; + +public class TesterBeanJ { + + private TesterBeanJ bean; + private String data; + + + public TesterBeanJ getBean() { + return bean; + } + + + public void setBean(TesterBeanJ bean) { + this.bean = bean; + } + + + public Optional getOptionalBean() { + return Optional.ofNullable(bean); + } + + + public String getData() { + return data; + } + + + public void setData(String data) { + this.data = data; + } +} diff --git a/test/org/apache/el/TesterEnum.java b/test/org/apache/el/TesterEnum.java new file mode 100644 index 0000000..1ff0bbf --- /dev/null +++ b/test/org/apache/el/TesterEnum.java @@ -0,0 +1,27 @@ +/* + * 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.el; + +public enum TesterEnum { + APPLE, + ORANGE; + + @Override + public String toString() { + return "This is a " + this.name(); + } +} diff --git a/test/org/apache/el/TesterFunctions.java b/test/org/apache/el/TesterFunctions.java new file mode 100644 index 0000000..904fba0 --- /dev/null +++ b/test/org/apache/el/TesterFunctions.java @@ -0,0 +1,102 @@ +/* + * 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.el; + +import java.lang.reflect.Method; + +import jakarta.el.FunctionMapper; + +public class TesterFunctions { + public static String trim(String input) { + return input.trim(); + } + + public static String concat(String... inputs) { + if (inputs == null || inputs.length == 0) { + return null; + } + StringBuilder result = new StringBuilder(); + for (String input : inputs) { + result.append(input); + } + return result.toString(); + } + + public static String concat2(String prefix, String... inputs) { + StringBuilder result = new StringBuilder(prefix); + for (String input : inputs) { + result.append(input); + } + return result.toString(); + } + + public static String[] toArray(String a, String b) { + return new String[] { a, b }; + } + + + public static class Inner$Class { + + public static final String RETVAL = "Return from bug49555"; + + public static String bug49555() { + return RETVAL; + } + } + + + public static class FMapper extends FunctionMapper { + + @Override + public Method resolveFunction(String prefix, String localName) { + if ("trim".equals(localName)) { + Method m; + try { + m = TesterFunctions.class.getMethod("trim", String.class); + return m; + } catch (SecurityException | NoSuchMethodException e) { + // Ignore + } + } else if ("concat".equals(localName)) { + Method m; + try { + m = TesterFunctions.class.getMethod("concat", String[].class); + return m; + } catch (SecurityException | NoSuchMethodException e) { + // Ignore + } + } else if ("concat2".equals(localName)) { + Method m; + try { + m = TesterFunctions.class.getMethod("concat2", String.class, String[].class); + return m; + } catch (SecurityException | NoSuchMethodException e) { + // Ignore + } + } else if ("toArray".equals(localName)) { + Method m; + try { + m = TesterFunctions.class.getMethod("toArray", String.class, String.class); + return m; + } catch (SecurityException | NoSuchMethodException e) { + // Ignore + } + } + return null; + } + } +} diff --git a/test/org/apache/el/lang/TestELArithmetic.java b/test/org/apache/el/lang/TestELArithmetic.java new file mode 100644 index 0000000..fd9ec2c --- /dev/null +++ b/test/org/apache/el/lang/TestELArithmetic.java @@ -0,0 +1,148 @@ +/* + * 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.el.lang; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestELArithmetic { + private static final String a = "1.1"; + private static final BigInteger b = + new BigInteger("1000000000000000000000"); + + @Test + public void testAdd01() throws Exception { + Assert.assertEquals("1000000000000000000001.1", + String.valueOf(ELArithmetic.add(a, b))); + } + + @Test + public void testAdd02() { + ELProcessor processor = new ELProcessor(); + Long result = processor.eval("null + null"); + Assert.assertEquals(Long.valueOf(0), result); + } + + @Test + public void testSubtract01() throws Exception { + Assert.assertEquals("-999999999999999999998.9", + String.valueOf(ELArithmetic.subtract(a, b))); + } + + @Test + public void testSubtract02() { + ELProcessor processor = new ELProcessor(); + Long result = processor.eval("null - null"); + Assert.assertEquals(Long.valueOf(0), result); + } + + @Test + public void testMultiply01() throws Exception { + Assert.assertEquals("1100000000000000000000.0", + String.valueOf(ELArithmetic.multiply(a, b))); + } + + @Test + public void testMultiply02() { + ELProcessor processor = new ELProcessor(); + Long result = processor.eval("null * null"); + Assert.assertEquals(Long.valueOf(0), result); + } + + @Test + public void testDivide01() throws Exception { + Assert.assertEquals("0.0", + String.valueOf(ELArithmetic.divide(a, b))); + } + + @Test + public void testDivide02() { + ELProcessor processor = new ELProcessor(); + Long result = processor.eval("null / null"); + Assert.assertEquals(Long.valueOf(0), result); + } + + @Test + public void testMod01() throws Exception { + Assert.assertEquals("1.1", + String.valueOf(ELArithmetic.mod(a, b))); + } + + @Test + public void testMod02() { + ELProcessor processor = new ELProcessor(); + Long result = processor.eval("null % null"); + Assert.assertEquals(Long.valueOf(0), result); + } + + @Test + public void testMod03() { + ELProcessor processor = new ELProcessor(); + // Large so BigInteger rather than Long is used internally + BigInteger result = processor.eval("1 % -9999999999999999999"); + Assert.assertEquals(BigInteger.valueOf(1), result); + } + + @Test + public void testUnaryMinus01() { + ELProcessor processor = new ELProcessor(); + Long result = processor.eval("-null"); + Assert.assertEquals(Long.valueOf(0), result); + } + + @Test + public void testBug47371bigDecimal() throws Exception { + Assert.assertEquals(BigDecimal.valueOf(1), + ELArithmetic.add("", BigDecimal.valueOf(1))); + } + + @Test + public void testBug47371double() throws Exception { + Assert.assertEquals(Double.valueOf(7), ELArithmetic.add("", Double.valueOf(7))); + } + + @Test + public void testBug47371doubleString() throws Exception { + Assert.assertEquals(Double.valueOf(2), ELArithmetic.add("", "2.")); + } + + @Test + public void testBug47371bigInteger() throws Exception { + Assert.assertEquals(BigInteger.valueOf(0), + ELArithmetic.multiply("", BigInteger.valueOf(1))); + } + + @Test + public void testBug47371long() throws Exception { + Assert.assertEquals(Long.valueOf(1), ELArithmetic.add("", Integer.valueOf(1))); + } + + @Test + public void testBug47371long2() throws Exception { + Assert.assertEquals(Long.valueOf(-3), ELArithmetic.subtract("1", "4")); + } + + @Test + public void testBug47371doubleString2() throws Exception { + Assert.assertEquals(Double.valueOf(2), ELArithmetic.add("1.", "1")); + } +} diff --git a/test/org/apache/el/lang/TestELSupport.java b/test/org/apache/el/lang/TestELSupport.java new file mode 100644 index 0000000..87d4f4e --- /dev/null +++ b/test/org/apache/el/lang/TestELSupport.java @@ -0,0 +1,501 @@ +/* + * 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.el.lang; + +import java.beans.PropertyEditorManager; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +import jakarta.el.ELException; +import jakarta.el.ELManager; +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestELSupport { + @Test + public void testEquals() { + Assert.assertTrue(ELSupport.equals(null, "01", Long.valueOf(1))); + } + + @Test + public void testBigDecimal() { + testIsSame(new BigDecimal( + "0.123456789012345678901234567890123456789012345678901234567890123456789")); + } + + @Test + public void testBigInteger() { + testIsSame(new BigInteger( + "1234567890123456789012345678901234567890123456789012345678901234567890")); + } + + @Test + public void testLong() { + testIsSame(Long.valueOf(0x0102030405060708L)); + } + + @Test + public void testInteger() { + testIsSame(Integer.valueOf(0x01020304)); + } + + @Test + public void testShort() { + testIsSame(Short.valueOf((short) 0x0102)); + } + + @Test + public void testByte() { + testIsSame(Byte.valueOf((byte) 0xEF)); + } + + @Test + public void testDouble() { + testIsSame(Double.valueOf(0.123456789012345678901234)); + } + + @Test + public void testFloat() { + testIsSame(Float.valueOf(0.123456F)); + } + + @Test + public void testCoerceIntegerToNumber() { + Integer input = Integer.valueOf(4390241); + Object output = ELSupport.coerceToType(null, input, Number.class); + Assert.assertEquals(input, output); + } + + @Test + public void testCoerceNullToNumber() { + Object output = ELSupport.coerceToType(null, null, Number.class); + Assert.assertNull(output); + } + + @Test + public void testCoerceEnumAToEnumA() { + Object output = null; + try { + output = ELSupport.coerceToEnum(null, TestEnumA.VALA1, TestEnumA.class); + } finally { + Assert.assertEquals(TestEnumA.VALA1, output); + } + } + + @Test + public void testCoerceEnumAToEnumB() { + Object output = null; + try { + output = ELSupport.coerceToEnum(null, TestEnumA.VALA1, TestEnumB.class); + } catch (ELException ele) { + // Ignore + } + Assert.assertNull(output); + } + + @Test + public void testCoerceEnumAToEnumC() { + Object output = null; + try { + output = ELSupport.coerceToEnum(null, TestEnumA.VALA1, TestEnumC.class); + } catch (ELException ele) { + // Ignore + } + Assert.assertNull(output); + } + + @Test + public void testCoerceToType01() { + Object result = ELManager.getExpressionFactory().coerceToType( + null, Integer.class); + Assert.assertNull("Result: " + result, result); + } + + @Test + public void testCoerceToType02() { + Object result = ELManager.getExpressionFactory().coerceToType( + null, int.class); + Assert.assertEquals(Integer.valueOf(0), result); + } + + @Test + public void testCoerceToType03() { + Object result = ELManager.getExpressionFactory().coerceToType( + null, boolean.class); + Assert.assertEquals(Boolean.valueOf(null), result); + } + + @Test + public void testCoerceToType04() { + Object result = ELManager.getExpressionFactory().coerceToType( + null, String.class); + Assert.assertEquals("", result); + } + + @Test + public void testCoerceToType05() { + Object result = ELManager.getExpressionFactory().coerceToType( + null, Character.class); + Assert.assertNull("Result: " + result, result); + } + + @Test + public void testCoerceToType06() { + Object result = ELManager.getExpressionFactory().coerceToType( + "", Character.class); + Assert.assertEquals(Character.valueOf((char) 0), result); + } + + @Test + public void testCoerceToType07() { + Object result = ELManager.getExpressionFactory().coerceToType( + null, char.class); + Assert.assertEquals(Character.valueOf((char) 0), result); + } + + @Test + public void testCoerceToType08() { + Object result = ELManager.getExpressionFactory().coerceToType( + "", char.class); + Assert.assertEquals(Character.valueOf((char) 0), result); + } + + @Test + public void testCoerceToType09() { + Object result = ELManager.getExpressionFactory().coerceToType( + null, Boolean.class); + Assert.assertNull("Result: " + result, result); + } + + @Test + public void testCoerceToType10() { + Object result = ELManager.getExpressionFactory().coerceToType( + "", Boolean.class); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void testCoerceToType11() { + Object result = ELManager.getExpressionFactory().coerceToType( + null, boolean.class); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void testCoerceToType12() { + Object result = ELManager.getExpressionFactory().coerceToType( + "", boolean.class); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void testCoerceToType13() { + Object result = ELManager.getExpressionFactory().coerceToType( + "", TesterType.class); + Assert.assertNull(result); + } + + @Test + public void testCoerceToType14() { + PropertyEditorManager.registerEditor(TesterType.class, TesterTypeEditorNoError.class); + Object result = ELManager.getExpressionFactory().coerceToType( + "Foo", TesterType.class); + Assert.assertEquals("Foo", ((TesterType) result).getValue()); + } + + @Test(expected=ELException.class) + public void testCoerceToType15() { + PropertyEditorManager.registerEditor(TesterType.class, TesterTypeEditorError.class); + Object result = ELManager.getExpressionFactory().coerceToType( + "Foo", TesterType.class); + Assert.assertEquals("Foo", ((TesterType) result).getValue()); + } + + @Test + public void testCoerceToType16() { + PropertyEditorManager.registerEditor(TesterType.class, TesterTypeEditorError.class); + Object result = ELManager.getExpressionFactory().coerceToType( + "", TesterType.class); + Assert.assertNull(result); + } + + @Test + public void testCoerceToNumber01() { + Object result = ELSupport.coerceToNumber(null, null, Integer.class); + Assert.assertNull("Result: " + result, result); + } + + @Test + public void testCoerceToNumber02() { + Object result = ELSupport.coerceToNumber(null, null, int.class); + Assert.assertEquals(Integer.valueOf(0), result); + } + + @Test + public void testCoerceToBoolean01() { + Object result = ELSupport.coerceToBoolean(null, null, true); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void testCoerceToBoolean02() { + Object result = ELSupport.coerceToBoolean(null, null, false); + Assert.assertNull("Result: " + result, result); + } + + private static void testIsSame(Object value) { + Assert.assertEquals(value, ELSupport.coerceToNumber(null, value, value.getClass())); + } + + private enum TestEnumA { + VALA1, + VALA2 + } + private enum TestEnumB { + VALB1, + VALB2 + } + private enum TestEnumC { + VALA1, + VALA2, + VALB1, + VALB2 + } + + + @Test + public void testCoercetoFunctionalInterface01() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateA"); + Object result = elp.eval("testPredicateA(x -> x.equals('data'))"); + Assert.assertEquals("PASS", result); + } + + + @Test + public void testCoercetoFunctionalInterface02() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateA"); + Object result = elp.eval("testPredicateA(x -> !x.equals('data'))"); + Assert.assertEquals("BLOCK", result); + } + + + @Test + public void testCoercetoFunctionalInterface03() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateB"); + Object result = elp.eval("testPredicateB(x -> x > 50)"); + Assert.assertEquals("PASS", result); + } + + + @Test + public void testCoercetoFunctionalInterface04() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateB"); + Object result = elp.eval("testPredicateB(x -> x < 50)"); + Assert.assertEquals("BLOCK", result); + } + + + @Test(expected = ELException.class) + public void testCoercetoFunctionalInterface05() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateC"); + elp.eval("testPredicateC(x -> x > 50)"); + } + + + @Test + public void testCoercetoFunctionalInterface06() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testBiPredicateA"); + Object result = elp.eval("testBiPredicateA((x,y) -> x.name().equals('VALA1') && y)"); + Assert.assertEquals("PASS", result); + } + + + /* + * Note: The following tests use compareTo(). When the target object (Long or String) is examined by reflection + * both compareTo(Object) and compareTo(Long)/compareTo(String) methods will be found as potential matches. The + * method matching rules (see section 1.2.1.2 of the specification) require that overload resolution has a higher + * precedence than coercion resolution so it is always the compareTo(Object) method that will be used for the + * following tests resulting in a ClassCastException (which is wrapped in an ELException). + */ + + @Test(expected = ELException.class) + public void testCoercetoFunctionalInterface07() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateB"); + // This should trigger an exception as String isn't assignable to Long + Object result = elp.eval("testPredicateB(x -> x.compareTo('data') == 0)"); + Assert.assertEquals("BLOCK", result); + } + + + @Test(expected = ELException.class) + public void testCoercetoFunctionalInterface08() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateA"); + // This should trigger an exception as Long isn't assignable to String + Object result = elp.eval("testPredicateA(x -> x.compareTo(1234) == 0)"); + Assert.assertEquals("BLOCK", result); + } + + + @Test(expected = ELException.class) + public void testCoercetoFunctionalInterface09() throws Exception { + final ELProcessor elp = new ELProcessor(); + elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateB"); + // This should trigger an exception as String isn't assignable to Long despite this String being coercible + Object result = elp.eval("testPredicateB(x -> x.compareTo('1234') == 0)"); + Assert.assertEquals("BLOCK", result); + } + + + public static String testPredicateA(Predicate filter) { + String s = "data"; + if (filter.test(s)) { + return "PASS"; + } else { + return "BLOCK"; + } + } + + + public static String testPredicateB(Predicate filter) { + Long l = Long.valueOf(100); + if (filter.test(l)) { + return "PASS"; + } else { + return "BLOCK"; + } + } + + + public static String testPredicateC(Predicate filter) { + String s = "text"; + if (filter.test(s)) { + return "PASS"; + } else { + return "BLOCK"; + } + } + + + public static String testBiPredicateA(BiPredicate filter) { + // Mainly interested in if these coerce correctly + if (filter.test(TestEnumC.VALA1, Boolean.TRUE)) { + return "PASS"; + } else { + return "BLOCK"; + } + } + + + @Test + public void testIsFunctionalInterface01() { + Assert.assertTrue(ELSupport.isFunctionalInterface(Predicate.class)); + } + + + @Test + public void testIsFunctionalInterface02() { + // Interface but more than one abstract method + Assert.assertFalse(ELSupport.isFunctionalInterface(Map.class)); + } + + + @Test + public void testIsFunctionalInterface03() { + // Not an interface + Assert.assertFalse(ELSupport.isFunctionalInterface(String.class)); + } + + + @Test + public void testIsFunctionalInterface04() { + // Extends a functional interface with no changes + Assert.assertTrue(ELSupport.isFunctionalInterface(FunctionalA.class)); + } + + + @Test + public void testIsFunctionalInterface05() { + // Extends a functional interface with additional abstract method + Assert.assertFalse(ELSupport.isFunctionalInterface(FunctionalB.class)); + } + + + @Test + public void testIsFunctionalInterface06() { + // Extends a functional interface with additional default method + Assert.assertTrue(ELSupport.isFunctionalInterface(FunctionalC.class)); + } + + + @Test + public void testIsFunctionalInterface07() { + // Extends a functional interface and overrides method in Object + Assert.assertTrue(ELSupport.isFunctionalInterface(FunctionalD.class)); + } + + + @Test + public void testIsFunctionalInterface08() { + // Extends a functional interface adds a method that looks like a + // method from Object + Assert.assertFalse(ELSupport.isFunctionalInterface(FunctionalE.class)); + } + + + private interface FunctionalA extends Predicate { + } + + + private interface FunctionalB extends Predicate { + void extra(); + } + + + private interface FunctionalC extends Predicate { + @SuppressWarnings("unused") + default void extra() { + } + } + + + private interface FunctionalD extends Predicate { + @Override + String toString(); + @Override + int hashCode(); + @Override + boolean equals(Object o); + } + + + private interface FunctionalE extends Predicate { + boolean equals(String s); + } +} diff --git a/test/org/apache/el/lang/TesterBean.java b/test/org/apache/el/lang/TesterBean.java new file mode 100644 index 0000000..6a2cd18 --- /dev/null +++ b/test/org/apache/el/lang/TesterBean.java @@ -0,0 +1,21 @@ +/* + * 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.el.lang; + +public class TesterBean { + +} diff --git a/test/org/apache/el/lang/TesterType.java b/test/org/apache/el/lang/TesterType.java new file mode 100644 index 0000000..5690f58 --- /dev/null +++ b/test/org/apache/el/lang/TesterType.java @@ -0,0 +1,30 @@ +/* + * 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.el.lang; + +public class TesterType { + + private final String value; + + public TesterType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/test/org/apache/el/lang/TesterTypeEditorBase.java b/test/org/apache/el/lang/TesterTypeEditorBase.java new file mode 100644 index 0000000..e2b8fb1 --- /dev/null +++ b/test/org/apache/el/lang/TesterTypeEditorBase.java @@ -0,0 +1,79 @@ +/* + * 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.el.lang; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.beans.PropertyChangeListener; +import java.beans.PropertyEditor; + +public abstract class TesterTypeEditorBase implements PropertyEditor { + + protected TesterType type = null; + + @Override + public void setValue(Object value) { + } + + @Override + public Object getValue() { + return type; + } + + @Override + public boolean isPaintable() { + return false; + } + + @Override + public void paintValue(Graphics gfx, Rectangle box) { + } + + @Override + public String getJavaInitializationString() { + return null; + } + + @Override + public String getAsText() { + return null; + } + + @Override + public String[] getTags() { + return null; + } + + @Override + public Component getCustomEditor() { + return null; + } + + @Override + public boolean supportsCustomEditor() { + return false; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + } +} diff --git a/test/org/apache/el/lang/TesterTypeEditorError.java b/test/org/apache/el/lang/TesterTypeEditorError.java new file mode 100644 index 0000000..84b73e3 --- /dev/null +++ b/test/org/apache/el/lang/TesterTypeEditorError.java @@ -0,0 +1,25 @@ +/* + * 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.el.lang; + +public class TesterTypeEditorError extends TesterTypeEditorBase { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + throw new IllegalArgumentException(); + } +} diff --git a/test/org/apache/el/lang/TesterTypeEditorNoError.java b/test/org/apache/el/lang/TesterTypeEditorNoError.java new file mode 100644 index 0000000..6f36bd2 --- /dev/null +++ b/test/org/apache/el/lang/TesterTypeEditorNoError.java @@ -0,0 +1,25 @@ +/* + * 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.el.lang; + +public class TesterTypeEditorNoError extends TesterTypeEditorBase { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + type = new TesterType(text); + } +} diff --git a/test/org/apache/el/lang/TesterVariableMapperImpl.java b/test/org/apache/el/lang/TesterVariableMapperImpl.java new file mode 100644 index 0000000..31fd6ca --- /dev/null +++ b/test/org/apache/el/lang/TesterVariableMapperImpl.java @@ -0,0 +1,55 @@ +/* + * 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.el.lang; + +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ExpressionFactory; +import jakarta.el.TesterELContext; +import jakarta.el.ValueExpression; +import jakarta.el.VariableMapper; + +import org.junit.Assert; +import org.junit.Test; + +public class TesterVariableMapperImpl { + + @Test + public void testSetVariable01() { + ExpressionFactory factory = ELManager.getExpressionFactory(); + ELContext context = new TesterELContext(); + ValueExpression ve1 = + factory.createValueExpression(context, "${1}", int.class); + ValueExpression ve2 = + factory.createValueExpression(context, "${2}", int.class); + ValueExpression ve3 = + factory.createValueExpression(context, "${3}", int.class); + + VariableMapper mapper = new VariableMapperImpl(); + + mapper.setVariable("var1", ve1); + mapper.setVariable("var2", ve2); + mapper.setVariable("var3", ve3); + + + mapper.setVariable("var2", null); + + Assert.assertEquals(ve1, mapper.resolveVariable("var1")); + Assert.assertNull(mapper.resolveVariable("var2")); + Assert.assertEquals(ve3, mapper.resolveVariable("var3")); + } +} diff --git a/test/org/apache/el/parser/TestAstAnd.java b/test/org/apache/el/parser/TestAstAnd.java new file mode 100644 index 0000000..8303eaf --- /dev/null +++ b/test/org/apache/el/parser/TestAstAnd.java @@ -0,0 +1,53 @@ +/* + * 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.el.parser; + +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstAnd { + + @Test + public void test01() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("true && true"); + Assert.assertEquals(Boolean.TRUE, result); + } + + @Test + public void test02() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("true && null"); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void test03() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("null && true"); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void test04() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("null && null"); + Assert.assertEquals(Boolean.FALSE, result); + } +} diff --git a/test/org/apache/el/parser/TestAstAssign.java b/test/org/apache/el/parser/TestAstAssign.java new file mode 100644 index 0000000..3e0e0cd --- /dev/null +++ b/test/org/apache/el/parser/TestAstAssign.java @@ -0,0 +1,83 @@ +/* + * 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.el.parser; + +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ELProcessor; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstAssign { + + @Test + public void testGetValue01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("bean01", new TesterBeanB()); + + Object result = processor.getValue( + "bean01.text = 'hello'", String.class); + + Assert.assertEquals("hello", result); + } + + + @Test + public void testGetValue02() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("bean01", new TesterBeanB()); + + Object result = processor.getValue( + "bean01.text = 'hello'; bean01.text", String.class); + + Assert.assertEquals("hello", result); + } + + + + @Test + public void testGetType01() { + ELProcessor processor = new ELProcessor(); + ELContext context = processor.getELManager().getELContext(); + ExpressionFactory factory = ELManager.getExpressionFactory(); + + processor.defineBean("bean01", new TesterBeanB()); + ValueExpression ve = factory.createValueExpression( + context, "${bean01.text = 'hello'}", String.class); + + Assert.assertEquals(String.class, ve.getType(context)); + Assert.assertEquals("hello", ve.getValue(context)); + } + + + @Test + public void testGetType02() { + ELProcessor processor = new ELProcessor(); + ELContext context = processor.getELManager().getELContext(); + ExpressionFactory factory = ELManager.getExpressionFactory(); + + processor.defineBean("bean01", new TesterBeanB()); + ValueExpression ve = factory.createValueExpression( + context, "${bean01.text = 'hello'; bean01.text}", String.class); + + Assert.assertEquals(String.class, ve.getType(context)); + Assert.assertEquals("hello", ve.getValue(context)); + } +} diff --git a/test/org/apache/el/parser/TestAstChoice.java b/test/org/apache/el/parser/TestAstChoice.java new file mode 100644 index 0000000..23e8fdd --- /dev/null +++ b/test/org/apache/el/parser/TestAstChoice.java @@ -0,0 +1,32 @@ +/* + * 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.el.parser; + +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstChoice { + + @Test + public void test01() { + ELProcessor processor = new ELProcessor(); + Long result = processor.eval("null?1:2"); + Assert.assertEquals(Long.valueOf(2), result); + } +} diff --git a/test/org/apache/el/parser/TestAstConcatenation.java b/test/org/apache/el/parser/TestAstConcatenation.java new file mode 100644 index 0000000..68c51a8 --- /dev/null +++ b/test/org/apache/el/parser/TestAstConcatenation.java @@ -0,0 +1,122 @@ +/* + * 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.el.parser; + +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ELProcessor; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstConcatenation { + + /** + * Test string concatenation. + */ + @Test + public void testConcatenation01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("'a' += 'b'", String.class); + Assert.assertEquals("ab", result); + } + + /** + * Test coercion to string then concatenation. + */ + @Test + public void testConcatenation02() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("1 += 2", String.class); + Assert.assertEquals("12", result); + } + + /** + * Test string concatenation with whitespace. + */ + @Test + public void testConcatenation03() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("' a' += ' b '", String.class); + Assert.assertEquals(" a b ", result); + } + + /** + * Test string concatenation with mixed types. + */ + @Test + public void testConcatenation04() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("'a' += 3", String.class); + Assert.assertEquals("a3", result); + } + + /** + * Test operator precedence (+ before +=). + */ + @Test + public void testPrecedence01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("1 + 2 += 3", String.class); + Assert.assertEquals("33", result); + } + + /** + * Test operator precedence (+ before +=). + */ + @Test + public void testPrecedence02() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("1 += 2 + 3", String.class); + Assert.assertEquals("15", result); + } + + /** + * Test operator precedence (+= before >). + */ + @Test + public void testPrecedence03() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("10 > 2 += 3", String.class); + Assert.assertEquals("false", result); + } + + /** + * Test operator precedence (+= before >). + */ + @Test + public void testPrecedence04() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("1 += 2 > 3", String.class); + Assert.assertEquals("true", result); + } + + @Test + public void testGetType() { + ELProcessor processor = new ELProcessor(); + ELContext context = processor.getELManager().getELContext(); + ExpressionFactory factory = ELManager.getExpressionFactory(); + + ValueExpression ve = factory.createValueExpression( + context, "${'a' += 3}", String.class); + + Assert.assertEquals(String.class, ve.getType(context)); + Assert.assertEquals("a3", ve.getValue(context)); + } +} diff --git a/test/org/apache/el/parser/TestAstFloatingPoint.java b/test/org/apache/el/parser/TestAstFloatingPoint.java new file mode 100644 index 0000000..a7faf34 --- /dev/null +++ b/test/org/apache/el/parser/TestAstFloatingPoint.java @@ -0,0 +1,46 @@ +/* + * 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.el.parser; + +import java.math.BigDecimal; + +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.el.lang.ELSupport; + +public class TestAstFloatingPoint { + + @Test + public void testValidDouble() { + String value = "1.234e100"; + ELProcessor processor = new ELProcessor(); + Number result = processor.eval(value); + Assert.assertTrue(ELSupport.equals(null, Double.valueOf(value), result)); + } + + + @Test + public void testValidBigDecimal() { + String value = "1.234e1000"; + ELProcessor processor = new ELProcessor(); + Number result = processor.eval(value); + Assert.assertTrue(ELSupport.equals(null, new BigDecimal(value), result)); + } +} diff --git a/test/org/apache/el/parser/TestAstFunction.java b/test/org/apache/el/parser/TestAstFunction.java new file mode 100644 index 0000000..4b3a657 --- /dev/null +++ b/test/org/apache/el/parser/TestAstFunction.java @@ -0,0 +1,62 @@ +/* + * 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.el.parser; + +import jakarta.el.ELContext; +import jakarta.el.ELProcessor; +import jakarta.el.ExpressionFactory; +import jakarta.el.StandardELContext; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstFunction { + + @Test + public void testImport01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("Integer(1000)", Integer.class); + Assert.assertEquals(Integer.valueOf(1000), result); + } + + @Test + public void testImport02() { + ELProcessor processor = new ELProcessor(); + processor.getELManager().getELContext().getImportHandler().importStatic("java.lang.Integer.valueOf"); + Object result = processor.getValue("valueOf(1000)", Integer.class); + Assert.assertEquals(Integer.valueOf(1000), result); + } + + @Test + public void testVarargMethod() throws NoSuchMethodException, SecurityException { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new StandardELContext(factory); + context.getFunctionMapper().mapFunction("fn", "format", + String.class.getMethod("format", String.class, Object[].class)); + + //Object result = factory.createValueExpression(context, "${fn:format('%s-%s','one','two')}", String.class) + // .getValue(context); + //Assert.assertEquals("one-two", result); + + Object result = factory.createValueExpression(context, "${fn:format('%s-%s','one,two'.split(','))}", String.class) + .getValue(context); + Assert.assertEquals("one-two", result); + + result = factory.createValueExpression(context, "${fn:format('%s','one')}", String.class).getValue(context); + Assert.assertEquals("one", result); + } +} diff --git a/test/org/apache/el/parser/TestAstIdentifier.java b/test/org/apache/el/parser/TestAstIdentifier.java new file mode 100644 index 0000000..7f8b925 --- /dev/null +++ b/test/org/apache/el/parser/TestAstIdentifier.java @@ -0,0 +1,46 @@ +/* + * 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.el.parser; + +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstIdentifier { + + @Test + public void testImport01() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("Integer.MAX_VALUE", + Integer.class); + Assert.assertEquals(Integer.valueOf(Integer.MAX_VALUE), result); + } + + + @Test + public void testImport02() { + ELProcessor processor = new ELProcessor(); + processor.getELManager().getELContext().getImportHandler().importStatic( + "java.lang.Integer.MAX_VALUE"); + Object result = + processor.getValue("MAX_VALUE", + Integer.class); + Assert.assertEquals(Integer.valueOf(Integer.MAX_VALUE), result); + } +} diff --git a/test/org/apache/el/parser/TestAstInteger.java b/test/org/apache/el/parser/TestAstInteger.java new file mode 100644 index 0000000..f16d3ed --- /dev/null +++ b/test/org/apache/el/parser/TestAstInteger.java @@ -0,0 +1,52 @@ +/* + * 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.el.parser; + +import java.math.BigInteger; + +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.el.lang.ELSupport; + +public class TestAstInteger { + + @Test + public void testValidLong() { + doTestValid("1234"); + } + + + @Test + public void testValidBigInteger() { + doTestValid("12345678901234567890"); + } + + + private void doTestValid(String value) { + ELProcessor processor = new ELProcessor(); + Number result = processor.eval(value); + Assert.assertTrue(ELSupport.equals(null, new BigInteger(value), result)); + } + + + // Note: Testing an invalid integer would require a number larger than + // 2^Integer.MAX_VALUE. It is not practical to test with a String of + // digits representing a number that large. +} diff --git a/test/org/apache/el/parser/TestAstLambdaExpression.java b/test/org/apache/el/parser/TestAstLambdaExpression.java new file mode 100644 index 0000000..83f21e2 --- /dev/null +++ b/test/org/apache/el/parser/TestAstLambdaExpression.java @@ -0,0 +1,236 @@ +/* + * 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.el.parser; + +import jakarta.el.ELException; +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstLambdaExpression { + + @Test + public void testSpec01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("(x->x+1)(1)", Integer.class); + Assert.assertEquals(Integer.valueOf(2), result); + } + + + @Test + public void testSpec02() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("((x,y)->x+y)(1,2)", Integer.class); + Assert.assertEquals(Integer.valueOf(3), result); + } + + + @Test + public void testSpec03() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("(()->64)", Integer.class); + Assert.assertEquals(Integer.valueOf(64), result); + } + + + @Test + public void testSpec04() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("v = (x,y)->x+y; v(3,4)", Integer.class); + Assert.assertEquals(Integer.valueOf(7), result); + } + + + @Test + public void testSpec05() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("fact = n -> n==0? 1: n*fact(n-1); fact(5)", + Integer.class); + Assert.assertEquals(Integer.valueOf(120), result); + } + + + @Test + public void testSpec06() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("(x->y->x-y)(2)(1)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testInvocation01() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("(()->2)()", + Integer.class); + Assert.assertEquals(Integer.valueOf(2), result); + } + + + @Test + public void testNested01() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("(()->y->2-y)()(1)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testNested02() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("(()->y->()->2-y)()(1)()", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test(expected=ELException.class) + public void testNested03() { + ELProcessor processor = new ELProcessor(); + // More method parameters than there are nested lambda expressions + processor.getValue("(()->y->()->2-y)()(1)()()", + Integer.class); + } + + + @Test + public void testNested04() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("(()->y->()->x->x-y)()(1)()(2)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testNested05() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("(()->y->()->()->x->x-y)()(1)()()(2)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testNested06() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("(()->y->()->()->x->x-y)()(1)()(3)(2)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testNested07() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("()->()->()->42", + Integer.class); + Assert.assertEquals(Integer.valueOf(42), result); + } + + + @Test + public void testLambdaAsFunction01() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("v = (x->y->x-y); v(2)(1)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testLambdaAsFunction02() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("v = (()->y->2-y); v()(1)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testLambdaAsFunction03() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("v = (()->y->()->2-y); v()(1)()", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test(expected=ELException.class) + public void testLambdaAsFunction04() { + ELProcessor processor = new ELProcessor(); + // More method parameters than there are nested lambda expressions + processor.getValue("v = (()->y->()->2-y); v()(1)()()", + Integer.class); + } + + + @Test + public void testLambdaAsFunction05() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("v = (()->y->()->x->x-y); v()(1)()(2)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testLambdaAsFunction06() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("v = (()->y->()->()->x->x-y); v()(1)()()(2)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test + public void testLambdaAsFunction07() { + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("v = (()->y->()->()->x->x-y); v()(1)()(3)(2)", + Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } + + + @Test(expected=ELException.class) + public void testLambdaAsFunction08() { + // Using a name space for the function is not allowed + ELProcessor processor = new ELProcessor(); + Object result = + processor.getValue("foo:v = (x)->x+1; foo:v(0)", Integer.class); + Assert.assertEquals(Integer.valueOf(1), result); + } +} diff --git a/test/org/apache/el/parser/TestAstListData.java b/test/org/apache/el/parser/TestAstListData.java new file mode 100644 index 0000000..0309ff9 --- /dev/null +++ b/test/org/apache/el/parser/TestAstListData.java @@ -0,0 +1,87 @@ +/* + * 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.el.parser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ELProcessor; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstListData { + + private static final List simpleList = new ArrayList<>(); + private static final List nestedList = new ArrayList<>(); + + static { + simpleList.add("a"); + simpleList.add("b"); + simpleList.add("c"); + simpleList.add("b"); + simpleList.add("c"); + nestedList.add(simpleList); + nestedList.add(Collections.EMPTY_LIST); + nestedList.add("d"); + } + + + @Test + public void testSimple01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue( + "['a','b','c', 'b', 'c']", List.class); + Assert.assertEquals(simpleList, result); + } + + + @Test + public void testSimple02() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("[]", List.class); + Assert.assertEquals(Collections.EMPTY_LIST, result); + } + + + @Test + public void testNested01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue( + "[['a','b','c','b','c'],[],'d']", List.class); + Assert.assertEquals(nestedList, result); + } + + + @Test + public void testGetType() { + ELProcessor processor = new ELProcessor(); + ELContext context = processor.getELManager().getELContext(); + ExpressionFactory factory = ELManager.getExpressionFactory(); + + ValueExpression ve = factory.createValueExpression( + context, "${['a','b','c','b','c']}", List.class); + + Assert.assertEquals(List.class, ve.getType(context)); + Assert.assertEquals(simpleList, ve.getValue(context)); + } +} diff --git a/test/org/apache/el/parser/TestAstMapData.java b/test/org/apache/el/parser/TestAstMapData.java new file mode 100644 index 0000000..f84ca2c --- /dev/null +++ b/test/org/apache/el/parser/TestAstMapData.java @@ -0,0 +1,105 @@ +/* + * 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.el.parser; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ELProcessor; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstMapData { + + private static final Map simpleMap = new HashMap<>(); + private static final Map nestedMap = new HashMap<>(); + + static { + simpleMap.put("a", "1"); + simpleMap.put("b", "2"); + simpleMap.put("c", "3"); + + nestedMap.put("simple", simpleMap); + // {} will get parsed as an empty Set as there is nothing to hint to the + // parser that Map is expected here. + nestedMap.put("empty", Collections.EMPTY_SET); + nestedMap.put("d", "4"); + } + + + @Test + public void testSimple01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue( + "{'a':'1','b':'2','c':'3'}", Map.class); + Assert.assertEquals(simpleMap, result); + } + + + @Test + public void testSimple02() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("{}", Map.class); + Assert.assertEquals(Collections.EMPTY_MAP, result); + } + + + @Test + public void testNested01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue( + "{'simple':{'a':'1','b':'2','c':'3'}," + + "'empty':{}," + + "'d':'4'}", Map.class); + Assert.assertEquals(nestedMap, result); + } + + + @Test + public void testGetType() { + ELProcessor processor = new ELProcessor(); + ELContext context = processor.getELManager().getELContext(); + ExpressionFactory factory = ELManager.getExpressionFactory(); + + ValueExpression ve = factory.createValueExpression( + context, "${{'a':'1','b':'2','c':'3'}}", Map.class); + + Assert.assertEquals(Map.class, ve.getType(context)); + Assert.assertEquals(simpleMap, ve.getValue(context)); + } + + + @Test + public void testLiteralWithVariable() { + ELProcessor elp = new ELProcessor(); + + String key = "myKey"; + String value = "myValue"; + elp.setVariable("aaa", "'" + key + "'"); + elp.setVariable("bbb", "'" + value + "'"); + + Object result = elp.eval("{ aaa : bbb }.get(aaa)"); + + Assert.assertEquals(value, result); + } +} diff --git a/test/org/apache/el/parser/TestAstNot.java b/test/org/apache/el/parser/TestAstNot.java new file mode 100644 index 0000000..11f53f3 --- /dev/null +++ b/test/org/apache/el/parser/TestAstNot.java @@ -0,0 +1,46 @@ +/* + * 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.el.parser; + +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstNot { + + @Test + public void test01() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("!null"); + Assert.assertEquals(Boolean.TRUE, result); + } + + @Test + public void test02() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("!true"); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void test03() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("!false"); + Assert.assertEquals(Boolean.TRUE, result); + } +} diff --git a/test/org/apache/el/parser/TestAstOr.java b/test/org/apache/el/parser/TestAstOr.java new file mode 100644 index 0000000..bb7a2bd --- /dev/null +++ b/test/org/apache/el/parser/TestAstOr.java @@ -0,0 +1,53 @@ +/* + * 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.el.parser; + +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstOr { + + @Test + public void test01() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("true || true"); + Assert.assertEquals(Boolean.TRUE, result); + } + + @Test + public void test02() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("true || null"); + Assert.assertEquals(Boolean.TRUE, result); + } + + @Test + public void test03() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("null || true"); + Assert.assertEquals(Boolean.TRUE, result); + } + + @Test + public void test04() { + ELProcessor processor = new ELProcessor(); + Boolean result = processor.eval("null || null"); + Assert.assertEquals(Boolean.FALSE, result); + } +} diff --git a/test/org/apache/el/parser/TestAstSemicolon.java b/test/org/apache/el/parser/TestAstSemicolon.java new file mode 100644 index 0000000..0660850 --- /dev/null +++ b/test/org/apache/el/parser/TestAstSemicolon.java @@ -0,0 +1,66 @@ +/* + * 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.el.parser; + +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ELProcessor; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstSemicolon { + + @Test + public void testGetValue01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("1;2", String.class); + Assert.assertEquals("2", result); + } + + + @Test + public void testGetValue02() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("1;2", Integer.class); + Assert.assertEquals(Integer.valueOf(2), result); + } + + + @Test + public void testGetValue03() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("1;2 + 3", Integer.class); + Assert.assertEquals(Integer.valueOf(5), result); + } + + + @Test + public void testGetType() { + ELProcessor processor = new ELProcessor(); + ELContext context = processor.getELManager().getELContext(); + ExpressionFactory factory = ELManager.getExpressionFactory(); + + ValueExpression ve = factory.createValueExpression( + context, "${1+1;2+2}", Integer.class); + + Assert.assertEquals(Number.class, ve.getType(context)); + Assert.assertEquals(Integer.valueOf(4), ve.getValue(context)); + } +} diff --git a/test/org/apache/el/parser/TestAstSetData.java b/test/org/apache/el/parser/TestAstSetData.java new file mode 100644 index 0000000..db7c357 --- /dev/null +++ b/test/org/apache/el/parser/TestAstSetData.java @@ -0,0 +1,84 @@ +/* + * 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.el.parser; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import jakarta.el.ELContext; +import jakarta.el.ELManager; +import jakarta.el.ELProcessor; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAstSetData { + + private static final Set simpleSet = new HashSet<>(); + private static final Set nestedSet = new HashSet<>(); + + static { + simpleSet.add("a"); + simpleSet.add("b"); + simpleSet.add("c"); + + nestedSet.add(simpleSet); + nestedSet.add(Collections.EMPTY_SET); + nestedSet.add("d"); + } + + + @Test + public void testSimple01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("{'a','b','c'}", Set.class); + Assert.assertEquals(simpleSet, result); + } + + + @Test + public void testSimple02() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("{}", Set.class); + Assert.assertEquals(Collections.EMPTY_SET, result); + } + + + @Test + public void testNested01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("{{'a','b','c'},{},'d'}", Set.class); + Assert.assertEquals(nestedSet, result); + } + + + @Test + public void testGetType() { + ELProcessor processor = new ELProcessor(); + ELContext context = processor.getELManager().getELContext(); + ExpressionFactory factory = ELManager.getExpressionFactory(); + + ValueExpression ve = factory.createValueExpression( + context, "${{'a','b','c'}}", Set.class); + + Assert.assertEquals(Set.class, ve.getType(context)); + Assert.assertEquals(simpleSet, ve.getValue(context)); + } +} diff --git a/test/org/apache/el/parser/TestELParser.java b/test/org/apache/el/parser/TestELParser.java new file mode 100644 index 0000000..884f7ab --- /dev/null +++ b/test/org/apache/el/parser/TestELParser.java @@ -0,0 +1,229 @@ +/* + * 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.el.parser; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.el.ELContextImpl; + +public class TestELParser { + + @Test + public void testBug49081() { + // OP's report + testExpression("#${1+1}", "#2"); + + // Variations on a theme + testExpression("#", "#"); + testExpression("##", "##"); + testExpression("###", "###"); + testExpression("$", "$"); + testExpression("$$", "$$"); + testExpression("$$$", "$$$"); + testExpression("#$", "#$"); + testExpression("#$#", "#$#"); + testExpression("$#", "$#"); + testExpression("$#$", "$#$"); + + testExpression("#{1+1}", "2"); + testExpression("##{1+1}", "#2"); + testExpression("###{1+1}", "##2"); + testExpression("${1+1}", "2"); + testExpression("$${1+1}", "$2"); + testExpression("$$${1+1}", "$$2"); + testExpression("#${1+1}", "#2"); + testExpression("#$#{1+1}", "#$2"); + testExpression("$#{1+1}", "$2"); + testExpression("$#${1+1}", "$#2"); + } + + @Test + public void testJavaKeyWordSuffix() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanA beanA = new TesterBeanA(); + beanA.setInt("five"); + ValueExpression var = + factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", var); + + // Should fail + Exception e = null; + try { + factory.createValueExpression(context, "${beanA.int}", + String.class); + } catch (ELException ele) { + e = ele; + } + Assert.assertNotNull(e); + } + + @Test + public void testJavaKeyWordIdentifier() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanA beanA = new TesterBeanA(); + beanA.setInt("five"); + ValueExpression var = + factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("this", var); + + // Should fail + Exception e = null; + try { + factory.createValueExpression(context, "${this}", String.class); + } catch (ELException ele) { + e = ele; + } + Assert.assertNotNull(e); + } + + + @Test + public void bug56179a() { + doTestBug56179(0, "test == true"); + } + + @Test + public void bug56179b() { + doTestBug56179(1, "test == true"); + } + + @Test + public void bug56179c() { + doTestBug56179(2, "test == true"); + } + + @Test + public void bug56179d() { + doTestBug56179(3, "test == true"); + } + + @Test + public void bug56179e() { + doTestBug56179(4, "test == true"); + } + + @Test + public void bug56179f() { + doTestBug56179(5, "test == true"); + } + + @Test + public void bug56179g() { + doTestBug56179(0, "(test) == true"); + } + + @Test + public void bug56179h() { + doTestBug56179(1, "(test) == true"); + } + + @Test + public void bug56179i() { + doTestBug56179(2, "(test) == true"); + } + + @Test + public void bug56179j() { + doTestBug56179(3, "(test) == true"); + } + + @Test + public void bug56179k() { + doTestBug56179(4, "(test) == true"); + } + + @Test + public void bug56179l() { + doTestBug56179(5, "(test) == true"); + } + + @Test + public void bug56179m() { + doTestBug56179(5, "((test)) == true"); + } + + @Test + public void bug56179n() { + doTestBug56179(5, "(((test))) == true"); + } + + private void doTestBug56179(int parenthesesCount, String innerExpr) { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + ValueExpression var = + factory.createValueExpression(Boolean.TRUE, Boolean.class); + context.getVariableMapper().setVariable("test", var); + + StringBuilder expr = new StringBuilder(); + expr.append("${"); + for (int i = 0; i < parenthesesCount; i++) { + expr.append('('); + } + expr.append(innerExpr); + for (int i = 0; i < parenthesesCount; i++) { + expr.append(')'); + } + expr.append('}'); + ValueExpression ve = factory.createValueExpression( + context, expr.toString(), String.class); + + String result = (String) ve.getValue(context); + Assert.assertEquals("true", result); + } + + @Test + public void bug56185() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + TesterBeanC beanC = new TesterBeanC(); + ValueExpression var = + factory.createValueExpression(beanC, TesterBeanC.class); + context.getVariableMapper().setVariable("myBean", var); + + ValueExpression ve = factory.createValueExpression(context, + "${(myBean.int1 > 1 and myBean.myBool) or "+ + "((myBean.myBool or myBean.myBool1) and myBean.int1 > 1)}", + Boolean.class); + Assert.assertEquals(Boolean.FALSE, ve.getValue(context)); + beanC.setInt1(2); + beanC.setMyBool1(true); + Assert.assertEquals(Boolean.TRUE, ve.getValue(context)); + } + + private void testExpression(String expression, String expected) { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + ValueExpression ve = factory.createValueExpression( + context, expression, String.class); + + String result = (String) ve.getValue(context); + Assert.assertEquals(expected, result); + } +} diff --git a/test/org/apache/el/parser/TestELParserPerformance.java b/test/org/apache/el/parser/TestELParserPerformance.java new file mode 100644 index 0000000..6a282cd --- /dev/null +++ b/test/org/apache/el/parser/TestELParserPerformance.java @@ -0,0 +1,92 @@ +/* + * 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.el.parser; + +import java.io.StringReader; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.collections.SynchronizedStack; + +/* + * This is a relative performance test so it remains part of the standard test run. + */ +public class TestELParserPerformance { + + /* + * Test to explore if re-using Parser instances is faster. + * + * Tests on my laptop show: + * - overhead by introducing the stack is in the noise for parsing even the + * simplest expression + * - efficiency from re-using the ELParser is measurable for even a single + * reuse of the parser + * - with large numbers of parses (~10k) performance for a trivial parse is + * three times faster + * - around the 100 iterations mark GC overhead adds significant noise to + * the results - for consistent results you either need fewer parses to + * avoid triggering GC or more parses so the GC effects are evenly + * distributed between the runs + * + * Note that the test is single threaded. + */ + @Test + public void testParserInstanceReuse() throws ParseException { + final int runs = 20; + final int parseIterations = 10000; + + long reinitTotalTime = 0; + long newTotalTime = 0; + + for (int j = 0; j < runs; j ++) { + long start = System.nanoTime(); + SynchronizedStack stack = new SynchronizedStack<>(); + + for (int i = 0; i < parseIterations; i ++) { + ELParser parser = stack.pop(); + if (parser == null) { + parser = new ELParser(new StringReader("${'foo'}")); + } else { + parser.ReInit(new StringReader("${'foo'}")); + } + parser.CompositeExpression(); + stack.push(parser); + } + long end = System.nanoTime(); + reinitTotalTime += (end - start); + + System.out.println(parseIterations + + " iterations using ELParser.ReInit(...) took " + (end - start) + "ns"); + } + + for (int j = 0; j < runs; j ++) { + long start = System.nanoTime(); + for (int i = 0; i < parseIterations; i ++) { + ELParser parser = new ELParser(new StringReader("${'foo'}")); + parser.CompositeExpression(); + } + long end = System.nanoTime(); + newTotalTime += (end - start); + + System.out.println(parseIterations + + " iterations using new ELParser(...) took " + (end - start) + "ns"); + } + + Assert.assertTrue("Using new ElParser() was faster then using ELParser.ReInit", reinitTotalTime < newTotalTime); + } +} diff --git a/test/org/apache/el/parser/TesterBeanA.java b/test/org/apache/el/parser/TesterBeanA.java new file mode 100644 index 0000000..fc758ea --- /dev/null +++ b/test/org/apache/el/parser/TesterBeanA.java @@ -0,0 +1,29 @@ +/* + * 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.el.parser; + +public class TesterBeanA { + private String keywordInt; + + public String getInt() { + return keywordInt; + } + + public void setInt(String keywordInt) { + this.keywordInt = keywordInt; + } +} diff --git a/test/org/apache/el/parser/TesterBeanB.java b/test/org/apache/el/parser/TesterBeanB.java new file mode 100644 index 0000000..f9b6dcf --- /dev/null +++ b/test/org/apache/el/parser/TesterBeanB.java @@ -0,0 +1,30 @@ +/* + * 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.el.parser; + +public class TesterBeanB { + + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/test/org/apache/el/parser/TesterBeanC.java b/test/org/apache/el/parser/TesterBeanC.java new file mode 100644 index 0000000..d4cecb4 --- /dev/null +++ b/test/org/apache/el/parser/TesterBeanC.java @@ -0,0 +1,49 @@ +/* + * 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.el.parser; + +public class TesterBeanC { + + private int int1; + private boolean myBool; + private boolean myBool1; + + public int getInt1() { + return int1; + } + + public void setInt1(int int1) { + this.int1 = int1; + } + + public boolean isMyBool() { + return myBool; + } + + public void setMyBool(boolean myBool) { + this.myBool = myBool; + } + + public boolean isMyBool1() { + return myBool1; + } + + public void setMyBool1(boolean myBool1) { + this.myBool1 = myBool1; + } + +} diff --git a/test/org/apache/el/stream/TestCollectionOperations.java b/test/org/apache/el/stream/TestCollectionOperations.java new file mode 100644 index 0000000..1d33a6c --- /dev/null +++ b/test/org/apache/el/stream/TestCollectionOperations.java @@ -0,0 +1,777 @@ +/* + * 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.el.stream; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jakarta.el.ELException; +import jakarta.el.ELProcessor; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.el.TesterBeanA; +import org.apache.el.lang.ELSupport; + +public class TestCollectionOperations { + + private static final TesterBeanA bean01 = new TesterBeanA(); + private static final TesterBeanA bean02 = new TesterBeanA(); + private static final TesterBeanA bean03 = new TesterBeanA(); + private static final List beans; + + static { + List list = new ArrayList<>(); + + bean01.setValLong(1); + bean01.setName("bean01"); + list.add(bean01); + + bean02.setValLong(2); + bean02.setName("bean02"); + list.add(bean02); + + bean03.setValLong(3); + bean03.setName("bean03"); + list.add(bean03); + + beans = Collections.unmodifiableList(list); + } + + + @Test + public void testToList01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue("['a','b','c'].stream().toList()", + List.class); + List expected = new ArrayList<>(3); + expected.add("a"); + expected.add("b"); + expected.add("c"); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testToList02() { + ELProcessor processor = new ELProcessor(); + String[] src = new String[] { "a", "b", "c" }; + processor.defineBean("src", src); + Object result = processor.getValue("src.stream().toList()", + List.class); + List expected = new ArrayList<>(3); + expected.add("a"); + expected.add("b"); + expected.add("c"); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testFilter01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + Object result = processor.getValue( + "beans.stream().filter(b->b.valLong > 2).toList()", + List.class); + List expected = new ArrayList<>(1); + expected.add(bean03); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testMap01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + Object result = processor.getValue( + "beans.stream().map(b->b.name).toList()", + List.class); + List expected = new ArrayList<>(3); + expected.add("bean01"); + expected.add("bean02"); + expected.add("bean03"); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testMap02() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + Object result = processor.getValue( + "beans.stream().filter(b->b.valLong > 1).map(b->[b.name, b.valLong]).toList()", + List.class); + + @SuppressWarnings("unchecked") + List> list = (List>) result; + + Assert.assertEquals(2, list.size()); + Assert.assertEquals("bean02", list.get(0).get(0)); + Assert.assertEquals(Long.valueOf(2), list.get(0).get(1)); + Assert.assertEquals("bean03", list.get(1).get(0)); + Assert.assertEquals(Long.valueOf(3), list.get(1).get(1)); + } + + + @Test + public void testFlatMap01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + Object result = processor.getValue( + "beans.stream().flatMap(b->b.name.toCharArray().stream()).toList()", + List.class); + + List expected = new ArrayList<>(18); + expected.add(Character.valueOf('b')); + expected.add(Character.valueOf('e')); + expected.add(Character.valueOf('a')); + expected.add(Character.valueOf('n')); + expected.add(Character.valueOf('0')); + expected.add(Character.valueOf('1')); + expected.add(Character.valueOf('b')); + expected.add(Character.valueOf('e')); + expected.add(Character.valueOf('a')); + expected.add(Character.valueOf('n')); + expected.add(Character.valueOf('0')); + expected.add(Character.valueOf('2')); + expected.add(Character.valueOf('b')); + expected.add(Character.valueOf('e')); + expected.add(Character.valueOf('a')); + expected.add(Character.valueOf('n')); + expected.add(Character.valueOf('0')); + expected.add(Character.valueOf('3')); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testDistinct01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue( + "['a', 'b', 'b', 'c'].stream().distinct().toList()", + List.class); + List expected = new ArrayList<>(3); + expected.add("a"); + expected.add("b"); + expected.add("c"); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testSorted01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue( + "['c', 'd', 'b', 'a'].stream().sorted().toList()", + List.class); + List expected = new ArrayList<>(4); + expected.add("a"); + expected.add("b"); + expected.add("c"); + expected.add("d"); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testSortedLambdaExpression01() { + ELProcessor processor = new ELProcessor(); + Object result = processor.getValue( + "['c', 'd', 'b', 'a'].stream().sorted((x,y)->x.compareTo(y)*-1).toList()", + List.class); + List expected = new ArrayList<>(4); + expected.add("d"); + expected.add("c"); + expected.add("b"); + expected.add("a"); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testForEach01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + processor.getValue( + "beans.stream().forEach(b->b.setValLong(b.valLong + 1))", + Object.class); + + Assert.assertEquals(2, bean01.getValLong()); + Assert.assertEquals(3, bean02.getValLong()); + Assert.assertEquals(4, bean03.getValLong()); + + // Restore the beans to their default state + processor.getValue( + "beans.stream().forEach(b->b.setValLong(b.valLong - 1))", + Object.class); + + Assert.assertEquals(1, bean01.getValLong()); + Assert.assertEquals(2, bean02.getValLong()); + Assert.assertEquals(3, bean03.getValLong()); + } + + + @Test + public void testPeek01() { + ELProcessor processor = new ELProcessor(); + List debug = new ArrayList<>(); + processor.defineBean("beans", beans); + processor.defineBean("debug", debug); + + Object result = processor.getValue( + "beans.stream().peek(b->debug.add(b)).toList()", + Object.class); + + List expected = new ArrayList<>(3); + expected.add(bean01); + expected.add(bean02); + expected.add(bean03); + + Assert.assertEquals(expected, result); + Assert.assertEquals(expected, debug); + } + + + @Test + public void testLimit01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + Object result = processor.getValue( + "beans.stream().limit(2).toList()", + Object.class); + + List expected = new ArrayList<>(2); + expected.add(bean01); + expected.add(bean02); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testSubstreamStart01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + Object result = processor.getValue( + "beans.stream().substream(1).toList()", + Object.class); + + List expected = new ArrayList<>(2); + expected.add(bean02); + expected.add(bean03); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testSubstreamStartEnd01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + Object result = processor.getValue( + "beans.stream().substream(1,2).toList()", + Object.class); + + List expected = new ArrayList<>(2); + expected.add(bean02); + + Assert.assertEquals(expected, result); + } + + + @Test + public void testToArray01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + Object result = processor.getValue( + "beans.stream().toArray()", + Object.class); + + Object[] expected = new Object[3]; + expected[0] = bean01; + expected[1] = bean02; + expected[2] = bean03; + + Assert.assertArrayEquals(expected, (Object[]) result); + } + + + @Test + public void testReduceLambda01() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[1,2,3,4,5].stream().reduce((x,y)->x+y)", + Object.class); + + Assert.assertEquals(Long.valueOf(15), ((Optional) result).get()); + } + + + @Test(expected=ELException.class) + public void testReduceLambda02() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[].stream().reduce((x,y)->x+y)", + Object.class); + + ((Optional) result).get(); + } + + + @Test + public void testReduceLambdaSeed01() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[1,2,3,4,5].stream().reduce(10, (x,y)->x+y)", + Object.class); + + Assert.assertEquals(Long.valueOf(25), result); + } + + + @Test + public void testMax01() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[1,2,3,4,5].stream().max()", + Object.class); + + Assert.assertEquals(Long.valueOf(5), ((Optional) result).get()); + } + + + @Test + public void testMax02() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[5,4,3,2,1].stream().max()", + Object.class); + + Assert.assertEquals(Long.valueOf(5), ((Optional) result).get()); + } + + + @Test(expected=ELException.class) + public void testMax03() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[].stream().max()", + Object.class); + + ((Optional) result).get(); + } + + + @Test(expected=ELException.class) + public void testMax04() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + processor.getValue( + "beans.stream().max()", + Object.class); + } + + + @Test + public void testMaxLambda01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + Object result = processor.getValue( + "beans.stream().max((x,y)->x.name.compareTo(y.name))", + Object.class); + + Assert.assertEquals(bean03, ((Optional) result).get()); + } + + + @Test + public void testMaxLambda02() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + processor.setVariable("comparison", "v->(x,y)->v(x).compareTo(v(y))"); + + Object result = processor.getValue( + "beans.stream().max(comparison(x->x.name))", + Object.class); + + Assert.assertEquals(bean03, ((Optional) result).get()); + } + @Test + public void testMin01() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[1,2,3,4,5].stream().min()", + Object.class); + + Assert.assertEquals(Long.valueOf(1), ((Optional) result).get()); + } + + + @Test + public void testMin02() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[5,4,3,2,1].stream().min()", + Object.class); + + Assert.assertEquals(Long.valueOf(1), ((Optional) result).get()); + } + + + @Test(expected=ELException.class) + public void testMin03() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[].stream().min()", + Object.class); + + ((Optional) result).get(); + } + + + @Test(expected=ELException.class) + public void testMin04() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + processor.getValue( + "beans.stream().min()", + Object.class); + } + + + @Test + public void testMinLambda01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + Object result = processor.getValue( + "beans.stream().min((x,y)->x.name.compareTo(y.name))", + Object.class); + + Assert.assertEquals(bean01, ((Optional) result).get()); + } + + + @Test + public void testAverage01() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[1,2,3,4,5].stream().average()", + Object.class); + + Number average = (Number) ((Optional) result).get(); + Assert.assertTrue("Result: " + average.toString(), + ELSupport.equals(null, Long.valueOf(3), average)); + } + + + @Test + public void testAverage02() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[1,2,3,4,5,6].stream().average()", + Object.class); + + Number average = (Number) ((Optional) result).get(); + Assert.assertTrue("Result: " + average.toString(), + ELSupport.equals(null, Double.valueOf(3.5), average)); + } + + + @Test(expected=ELException.class) + public void testAverage03() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[].stream().average()", + Object.class); + + ((Optional) result).get(); + } + + + @Test + public void testAverage04() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[].stream().average().orElseGet(()->10)", + Object.class); + + Assert.assertEquals(Long.valueOf(10), result); + } + + + @Test + public void testAverage05() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[].stream().average().orElseGet(()->()->10)", + Object.class); + + Assert.assertEquals(Long.valueOf(10), result); + } + + + @Test(expected=ELException.class) + public void testAverage06() { + ELProcessor processor = new ELProcessor(); + + processor.getValue( + "[].stream().average().orElseGet(10)", + Object.class); + } + + + @Test + public void testSum01() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[1,2,3,4,5].stream().sum()", + Object.class); + + Assert.assertTrue("Result: " + result.toString(), + ELSupport.equals(null, Long.valueOf(15), result)); + } + + + @Test + public void testSum02() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[].stream().sum()", + Object.class); + + Assert.assertTrue("Result: " + result.toString(), + ELSupport.equals(null, Long.valueOf(0), result)); + } + + + @Test + public void testCount01() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[1,2,3,4,5].stream().count()", + Object.class); + + Assert.assertTrue("Result: " + result.toString(), + ELSupport.equals(null, Long.valueOf(5), result)); + } + + + @Test + public void testCount02() { + ELProcessor processor = new ELProcessor(); + + Object result = processor.getValue( + "[].stream().count()", + Object.class); + + Assert.assertTrue("Result: " + result.toString(), + ELSupport.equals(null, Long.valueOf(0), result)); + } + + + @Test + public void testAnyMatch01() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[1,2,3,4,5].stream().anyMatch(x->x==7)", + Object.class); + + Assert.assertEquals(Boolean.FALSE, result.get()); + } + + + @Test + public void testAnyMatch02() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[1,2,3,4,5].stream().anyMatch(x->x==3)", + Object.class); + + Assert.assertEquals(Boolean.TRUE, result.get()); + } + + + @Test(expected=ELException.class) + public void testAnyMatch03() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[].stream().anyMatch(x->x==7)", + Object.class); + + result.get(); + } + + + @Test + public void testAllMatch01() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[1,2,3,4,5].stream().allMatch(x->x>3)", + Object.class); + + Assert.assertEquals(Boolean.FALSE, result.get()); + } + + + @Test + public void testAllMatch02() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[1,2,3,4,5].stream().allMatch(x->x>0)", + Object.class); + + Assert.assertEquals(Boolean.TRUE, result.get()); + } + + + @Test + public void testAllMatch03() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[1,2,3,4,5].stream().allMatch(x->x>10)", + Object.class); + + Assert.assertEquals(Boolean.FALSE, result.get()); + } + + + @Test(expected=ELException.class) + public void testAllMatch04() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[].stream().allMatch(x->x==7)", + Object.class); + + result.get(); + } + + + @Test + public void testNoneMatch01() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[1,2,3,4,5].stream().allMatch(x->x>3)", + Object.class); + + Assert.assertEquals(Boolean.FALSE, result.get()); + } + + + @Test + public void testNoneMatch02() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[1,2,3,4,5].stream().noneMatch(x->x>0)", + Object.class); + + Assert.assertEquals(Boolean.FALSE, result.get()); + } + + + @Test + public void testNoneMatch03() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[1,2,3,4,5].stream().noneMatch(x->x>10)", + Object.class); + + Assert.assertEquals(Boolean.TRUE, result.get()); + } + + + @Test(expected=ELException.class) + public void testNoneMatch04() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[].stream().noneMatch(x->x==7)", + Object.class); + + result.get(); + } + + + @Test + public void testFindFirst01() { + ELProcessor processor = new ELProcessor(); + processor.defineBean("beans", beans); + + Optional result = (Optional) processor.getValue( + "beans.stream().findFirst()", + Object.class); + + Assert.assertEquals(bean01, result.get()); + } + + + @Test(expected=ELException.class) + public void testFindFirst02() { + ELProcessor processor = new ELProcessor(); + + Optional result = (Optional) processor.getValue( + "[].stream().findFirst()", + Object.class); + + result.get(); + } +} diff --git a/test/org/apache/el/util/TestMessageFactory.java b/test/org/apache/el/util/TestMessageFactory.java new file mode 100644 index 0000000..0b354bf --- /dev/null +++ b/test/org/apache/el/util/TestMessageFactory.java @@ -0,0 +1,67 @@ +/* + * 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.el.util; + +import java.math.BigDecimal; +import java.util.ResourceBundle; + +import org.junit.Assert; +import org.junit.Test; + +public class TestMessageFactory { + + private static final Integer ZERO = Integer.valueOf(0); + + MessageFactory messageFactory = new MessageFactory(ResourceBundle.getBundle("org.apache.el.util.TestStrings")); + + @Test + public void testFormatNone() { + String input = "1E+2"; + // Trailing '0" is an extra Number argument, not printed by the + // message pattern. It reflects the case when a translation has not + // been updated with new arguments. + String result = + messageFactory.getInternal("messageFactory.formatNone", new BigDecimal(input), ZERO /*ignored*/); + // Should be unchanged + Assert.assertEquals(input, result); + } + + @Test + public void testFormatNumeric() { + String input = "1E+2"; + String result = messageFactory.getInternal("messageFactory.formatNumeric", new BigDecimal(input)); + // Should be formatted as an integer + Assert.assertEquals("100", result); + } + + @Test + public void testFormatChoice() { + String input = "1E+2"; + String result = + messageFactory.getInternal("messageFactory.formatChoice", new BigDecimal(input), ZERO /*ignored*/); + // Should be formatted as an integer + Assert.assertEquals("100 is enough", result); + } + + @Test + public void testFormatNoArguments() { + String input = "1E+2"; + String result = + messageFactory.getInternal("messageFactory.formatNoArguments", new BigDecimal(input), ZERO /*ignored*/); + Assert.assertEquals("A message", result); + } +} diff --git a/test/org/apache/el/util/TestReflectionUtil.java b/test/org/apache/el/util/TestReflectionUtil.java new file mode 100644 index 0000000..4362fc2 --- /dev/null +++ b/test/org/apache/el/util/TestReflectionUtil.java @@ -0,0 +1,87 @@ +/* + * 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.el.util; + +import jakarta.el.ELContext; +import jakarta.el.ExpressionFactory; +import jakarta.el.MethodExpression; +import jakarta.el.MethodNotFoundException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.el.ELContextImpl; + +public class TestReflectionUtil { + + private static final Tester BASE = new Tester(); + + /* + * Expect failure as it is not possible to identify which method named + * "testA()" is intended. + */ + @Test(expected=MethodNotFoundException.class) + public void testBug54370a() { + ReflectionUtil.getMethod(null, BASE, "testA", + new Class[] {null, String.class}, + new Object[] {null, ""}); + } + + /* + * Expect failure as it is not possible to identify which method named + * "testB()" is intended. Note: In EL null can always be coerced to a valid + * value for a primitive. + */ + @Test(expected=MethodNotFoundException.class) + public void testBug54370b() { + ReflectionUtil.getMethod(null, BASE, "testB", + new Class[] {null, String.class}, + new Object[] {null, ""}); + } + + @Test + public void testBug54370c() { + ReflectionUtil.getMethod(null, BASE, "testC", + new Class[] {null}, + new Object[] {null}); + } + + @Test + public void testBug54370d() { + ReflectionUtil.getMethod(null, BASE, "testD", + new Class[] {null}, + new Object[] {null}); + } + + @Test + public void testStaticMethodOnInstance() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + MethodExpression methodExpression = factory.createMethodExpression(context, "${\"1\".format(2)}", String.class, new Class[] {}); + + try { + methodExpression.invoke(context, null); + } catch (IllegalArgumentException iae) { + // Ensure correct IllegalArgumentException is thrown + String msg = iae.getMessage(); + Assert.assertTrue(msg, msg.contains("[format]")); + return; + } + Assert.fail("No exception"); + } +} diff --git a/test/org/apache/el/util/TestStrings.properties b/test/org/apache/el/util/TestStrings.properties new file mode 100644 index 0000000..c9e7e95 --- /dev/null +++ b/test/org/apache/el/util/TestStrings.properties @@ -0,0 +1,19 @@ +# 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. + +messageFactory.formatNone={0} +messageFactory.formatNumeric={0,number} +messageFactory.formatChoice={0,choice,0#{0,number} is too few|99#{0,number} is enough|100<{0,number} is too many} +messageFactory.formatNoArguments=A message diff --git a/test/org/apache/el/util/Tester.java b/test/org/apache/el/util/Tester.java new file mode 100644 index 0000000..bea0385 --- /dev/null +++ b/test/org/apache/el/util/Tester.java @@ -0,0 +1,52 @@ +/* + * 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.el.util; + +import java.io.InputStream; + +public class Tester { + + @SuppressWarnings("unused") + public void testA(InputStream param1, String param2) { + // NO-OP + } + + @SuppressWarnings("unused") + public void testA(Long param1, String param2) { + // NO-OP + } + + @SuppressWarnings("unused") + public void testB(InputStream param1, String param2) { + // NO-OP + } + + @SuppressWarnings("unused") + public void testB(long param1, String param2) { + // NO-OP + } + + @SuppressWarnings("unused") + public void testC(long param1) { + // NO-OP + } + + @SuppressWarnings("unused") + public void testD(String param1) { + // NO-OP + } +} diff --git a/test/org/apache/jasper/TestJspC.java b/test/org/apache/jasper/TestJspC.java new file mode 100644 index 0000000..b544c7b --- /dev/null +++ b/test/org/apache/jasper/TestJspC.java @@ -0,0 +1,162 @@ +/* + * 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.jasper; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TestJspC { + + private JspC jspc; + private File outputDir; + + @Before + public void init() { + File tempDir = new File(System.getProperty("tomcat.test.temp", + "output/tmp")); + outputDir = new File(tempDir, "jspc"); + jspc = new JspC(); + } + + @After + public void cleanup() throws IOException { + remove(outputDir); + } + + @Test + public void precompileWebapp_2_2() throws IOException { + File appDir = new File("test/webapp-2.2"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + @Test + public void precompileWebapp_2_3() throws IOException { + File appDir = new File("test/webapp-2.3"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + @Test + public void precompileWebapp_2_4() throws IOException { + File appDir = new File("test/webapp-2.4"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + @Test + public void precompileWebapp_2_5() throws IOException { + File appDir = new File("test/webapp-2.5"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + @Test + public void precompileWebapp_3_0() throws IOException { + File appDir = new File("test/webapp-3.0"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + @Test + public void precompileWebapp_3_1() throws IOException { + File appDir = new File("test/webapp-3.1"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + @Test + public void precompileWebapp_4_0() throws IOException { + File appDir = new File("test/webapp-4.0"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + @Test + public void precompileWebapp_5_0() throws IOException { + File appDir = new File("test/webapp-5.0"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + @Test + public void precompileWebapp_6_0() throws IOException { + File appDir = new File("test/webapp-6.0"); + File webappOut = new File(outputDir, appDir.getName()); + precompile(appDir, webappOut); + verify(webappOut); + } + + private void verify(File webappOut) { + // for now, just check some expected files exist + Assert.assertTrue(new File(webappOut, "generated_web.xml").exists()); + Assert.assertTrue(new File(webappOut, + "org/apache/jsp/el_002das_002dliteral_jsp.java").exists()); + Assert.assertTrue(new File(webappOut, + "org/apache/jsp/tld_002dversions_jsp.java").exists()); + } + + private void precompile(File appDir, File webappOut) throws IOException { + remove(webappOut); + Assert.assertTrue("Failed to create [" + webappOut + "]", webappOut.mkdirs()); + jspc.setUriroot(appDir.toString()); + jspc.setOutputDir(webappOut.toString()); + jspc.setValidateTld(false); + jspc.setWebXml(new File(webappOut, "generated_web.xml").toString()); + jspc.execute(); + } + + + private void remove(File base) throws IOException{ + if (!base.exists()) { + return; + } + Files.walkFileTree(base.toPath(), new SimpleFileVisitor(){ + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, + IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } +} diff --git a/test/org/apache/jasper/TestJspCompilationContext.java b/test/org/apache/jasper/TestJspCompilationContext.java new file mode 100644 index 0000000..25236ac --- /dev/null +++ b/test/org/apache/jasper/TestJspCompilationContext.java @@ -0,0 +1,74 @@ +/* + * 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.jasper; + +import java.io.File; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestJspCompilationContext extends TomcatBaseTest { + + @Test + public void testTagFileInJar() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk body = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/jsp/tagFileInJar.jsp", body, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(body.toString().contains("00 - OK")); + } + + + /* + * Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=57626 + */ + @Test + public void testModifiedTagFileInJar() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk body = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/jsp/tagFileInJar.jsp", body, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(body.toString().contains("00 - OK")); + + File jsp = new File("test/webapp/jsp/tagFileInJar.jsp"); + Assert.assertTrue("Failed to set last modified for [" + jsp + "]", + jsp.setLastModified(jsp.lastModified() + 10000)); + + // This test requires that modificationTestInterval is set to zero in + // web.xml. If not, a sleep longer that modificationTestInterval is + // required here. + + rc = getUrl("http://localhost:" + getPort() + + "/test/jsp/tagFileInJar.jsp", body, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(body.toString().contains("00 - OK")); + } +} diff --git a/test/org/apache/jasper/compiler/Dumper.java b/test/org/apache/jasper/compiler/Dumper.java new file mode 100644 index 0000000..d051724 --- /dev/null +++ b/test/org/apache/jasper/compiler/Dumper.java @@ -0,0 +1,218 @@ +/* + * 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.jasper.compiler; + +import org.apache.jasper.JasperException; +import org.xml.sax.Attributes; + +class Dumper { + + static class DumpVisitor extends Node.Visitor { + private int indent = 0; + + private String getAttributes(Attributes attrs) { + if (attrs == null) { + return ""; + } + + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < attrs.getLength(); i++) { + buf.append(" " + attrs.getQName(i) + "=\"" + attrs.getValue(i) + "\""); + } + return buf.toString(); + } + + private void printString(String str) { + printIndent(); + System.out.print(str); + } + + private void printString(String prefix, String str, String suffix) { + printIndent(); + if (str != null) { + System.out.print(prefix + str + suffix); + } else { + System.out.print(prefix + suffix); + } + } + + private void printAttributes(String prefix, Attributes attrs, String suffix) { + printString(prefix, getAttributes(attrs), suffix); + } + + private void dumpBody(Node n) throws JasperException { + Node.Nodes page = n.getBody(); + if (page != null) { + // indent++; + page.visit(this); + // indent--; + } + } + + @Override + public void visit(Node.PageDirective n) throws JasperException { + printAttributes("<%@ page", n.getAttributes(), "%>"); + } + + @Override + public void visit(Node.TaglibDirective n) throws JasperException { + printAttributes("<%@ taglib", n.getAttributes(), "%>"); + } + + @Override + public void visit(Node.IncludeDirective n) throws JasperException { + printAttributes("<%@ include", n.getAttributes(), "%>"); + dumpBody(n); + } + + @Override + public void visit(Node.Comment n) throws JasperException { + printString("<%--", n.getText(), "--%>"); + } + + @Override + public void visit(Node.Declaration n) throws JasperException { + printString("<%!", n.getText(), "%>"); + } + + @Override + public void visit(Node.Expression n) throws JasperException { + printString("<%=", n.getText(), "%>"); + } + + @Override + public void visit(Node.Scriptlet n) throws JasperException { + printString("<%", n.getText(), "%>"); + } + + @Override + public void visit(Node.IncludeAction n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.ForwardAction n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.GetProperty n) throws JasperException { + printAttributes(""); + } + + @Override + public void visit(Node.SetProperty n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.UseBean n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.PlugIn n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.ParamsAction n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.ParamAction n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.NamedAttribute n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.JspBody n) throws JasperException { + printAttributes(""); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.ELExpression n) throws JasperException { + printString("${" + n.getText() + "}"); + } + + @Override + public void visit(Node.CustomTag n) throws JasperException { + printAttributes("<" + n.getQName(), n.getAttributes(), ">"); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.UninterpretedTag n) throws JasperException { + String tag = n.getQName(); + printAttributes("<" + tag, n.getAttributes(), ">"); + dumpBody(n); + printString(""); + } + + @Override + public void visit(Node.TemplateText n) throws JasperException { + printString(n.getText()); + } + + private void printIndent() { + for (int i = 0; i < indent; i++) { + System.out.print(" "); + } + } + } + + public static void dump(Node n) { + try { + n.accept(new DumpVisitor()); + } catch (JasperException e) { + e.printStackTrace(); + } + } + + public static void dump(Node.Nodes page) { + try { + page.visit(new DumpVisitor()); + } catch (JasperException e) { + e.printStackTrace(); + } + } +} + diff --git a/test/org/apache/jasper/compiler/TestAttributeParser.java b/test/org/apache/jasper/compiler/TestAttributeParser.java new file mode 100644 index 0000000..286de76 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestAttributeParser.java @@ -0,0 +1,167 @@ +/* + * 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.jasper.compiler; + +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.el.ExpressionFactoryImpl; +import org.apache.el.TesterFunctions; +import org.apache.jasper.el.ELContextImpl; + +/** + * Test the EL processing from JSP attributes. Similar tests may be found in {@link org.apache.el.TestELEvaluation} and + * {@link org.apache.el.TestELInJsp}. + */ +public class TestAttributeParser { + + /** + * Test use of spaces in ternary expressions. This was primarily an EL parser bug. + */ + @Test + public void testBug42565() { + Assert.assertEquals("false", evalAttr("${false?true:false}", '\"')); + Assert.assertEquals("false", evalAttr("${false?true: false}", '\"')); + Assert.assertEquals("false", evalAttr("${false?true :false}", '\"')); + Assert.assertEquals("false", evalAttr("${false?true : false}", '\"')); + Assert.assertEquals("false", evalAttr("${false? true:false}", '\"')); + Assert.assertEquals("false", evalAttr("${false? true: false}", '\"')); + Assert.assertEquals("false", evalAttr("${false? true :false}", '\"')); + Assert.assertEquals("false", evalAttr("${false? true : false}", '\"')); + Assert.assertEquals("false", evalAttr("${false ?true:false}", '\"')); + Assert.assertEquals("false", evalAttr("${false ?true: false}", '\"')); + Assert.assertEquals("false", evalAttr("${false ?true :false}", '\"')); + Assert.assertEquals("false", evalAttr("${false ?true : false}", '\"')); + Assert.assertEquals("false", evalAttr("${false ? true:false}", '\"')); + Assert.assertEquals("false", evalAttr("${false ? true: false}", '\"')); + Assert.assertEquals("false", evalAttr("${false ? true :false}", '\"')); + Assert.assertEquals("false", evalAttr("${false ? true : false}", '\"')); + } + + + /** + * Test use nested ternary expressions. Full tests in {@link org.apache.el.TestELEvaluation}. This is just a smoke + * test to ensure JSP attribute processing doesn't cause any additional issues. + */ + @Test + public void testBug44994() { + Assert.assertEquals("none", evalAttr("${0 lt 0 ? 1 lt 0 ? 'many': 'one': 'none'}", '\"')); + Assert.assertEquals("one", evalAttr("${0 lt 1 ? 1 lt 1 ? 'many': 'one': 'none'}", '\"')); + Assert.assertEquals("many", evalAttr("${0 lt 2 ? 1 lt 2 ? 'many': 'one': 'none'}", '\"')); + } + + + /** + * Test the quoting requirements of JSP attributes. This doesn't make use of EL. See {@link #testBug45451()} for a + * test that combines JSP attribute quoting and EL quoting. + */ + @Test + public void testBug45015() { + // Warning: Java String quoting vs. JSP attribute quoting + Assert.assertEquals("hello 'world'", evalAttr("hello 'world'", '\"')); + Assert.assertEquals("hello 'world", evalAttr("hello 'world", '\"')); + Assert.assertEquals("hello world'", evalAttr("hello world'", '\"')); + Assert.assertEquals("hello world'", evalAttr("hello world\\'", '\"')); + Assert.assertEquals("hello world\"", evalAttr("hello world\\\"", '\"')); + Assert.assertEquals("hello \"world\"", evalAttr("hello \"world\"", '\"')); + Assert.assertEquals("hello \"world", evalAttr("hello \"world", '\"')); + Assert.assertEquals("hello world\"", evalAttr("hello world\"", '\"')); + Assert.assertEquals("hello world'", evalAttr("hello world\\'", '\"')); + Assert.assertEquals("hello world\"", evalAttr("hello world\\\"", '\"')); + + Assert.assertEquals("hello 'world'", evalAttr("hello 'world'", '\'')); + Assert.assertEquals("hello 'world", evalAttr("hello 'world", '\'')); + Assert.assertEquals("hello world'", evalAttr("hello world'", '\'')); + Assert.assertEquals("hello world'", evalAttr("hello world\\'", '\'')); + Assert.assertEquals("hello world\"", evalAttr("hello world\\\"", '\'')); + Assert.assertEquals("hello \"world\"", evalAttr("hello \"world\"", '\'')); + Assert.assertEquals("hello \"world", evalAttr("hello \"world", '\'')); + Assert.assertEquals("hello world\"", evalAttr("hello world\"", '\'')); + Assert.assertEquals("hello world'", evalAttr("hello world\\'", '\'')); + Assert.assertEquals("hello world\"", evalAttr("hello world\\\"", '\'')); + + } + + @Test + public void testBug45451() { + Assert.assertEquals("2", evalAttr("${1+1}", '\"')); + Assert.assertEquals("${1+1}", evalAttr("\\${1+1}", '\"')); + Assert.assertEquals("\\2", evalAttr("\\\\${1+1}", '\"')); + } + + @Test + public void testBug49081() { + Assert.assertEquals("#2", evalAttr("#${1+1}", '\"')); + } + + @Test + public void testLiteral() { + // Inspired by work on bug 45451, comments from kkolinko on the dev + // list and looking at the spec to find some edge cases + + // '\' is only an escape character inside a StringLiteral + // Attribute escaping does not apply inside EL expressions + Assert.assertEquals("\\", evalAttr("${'\\\\'}", '\"')); + + // Can use ''' inside '"' when quoting with '"' and vice versa without + // escaping + Assert.assertEquals("\\\"", evalAttr("${'\\\\\"'}", '\"')); + Assert.assertEquals("\"\\", evalAttr("${'\\\"\\\\'}", '\"')); + Assert.assertEquals("\\'", evalAttr("${'\\\\\\''}", '\"')); + Assert.assertEquals("'\\", evalAttr("${'\\'\\\\'}", '\"')); + + // Quoting <% and %> + Assert.assertEquals("hello <% world", evalAttr("hello <\\% world", '\"')); + Assert.assertEquals("hello %> world", evalAttr("hello %> world", '\"')); + + // Test that the end of literal in EL expression is recognized in + // parseEL(), be it quoted with single or double quotes. That is, that + // AttributeParser correctly switches between parseLiteral and parseEL + // methods. + // + // The test is based on the difference in how the '\' character is printed: + // when in parseLiteral \\${ will be printed as ${'\'}${, but if we are still + // inside of parseEL it will be printed as \${, thus preventing the EL + // expression that follows from being evaluated. + // + Assert.assertEquals("foo\\bar\\baz", evalAttr("${\'foo\'}\\\\${\'bar\'}\\\\${\'baz\'}", '\"')); + Assert.assertEquals("foo\\bar\\baz", evalAttr("${\'foo\'}\\\\${\"bar\"}\\\\${\'baz\'}", '\"')); + Assert.assertEquals("foo\\bar\\baz", evalAttr("${\"foo\"}\\\\${\'bar\'}\\\\${\"baz\"}", '\"')); + } + + @Test + public void testScriptExpressionLiterals() { + Assert.assertEquals(" \"hello world\" ", parseScriptExpression(" \"hello world\" ", (char) 0)); + Assert.assertEquals(" \"hello \\\"world\" ", parseScriptExpression(" \"hello \\\\\"world\" ", (char) 0)); + } + + private String evalAttr(String expression, char quote) { + + ExpressionFactoryImpl exprFactory = new ExpressionFactoryImpl(); + ELContextImpl ctx = new ELContextImpl(exprFactory); + ctx.setFunctionMapper(new TesterFunctions.FMapper()); + ValueExpression ve = exprFactory.createValueExpression(ctx, + AttributeParser.getUnquoted(expression, quote, false, false, false, false), String.class); + return (String) ve.getValue(ctx); + } + + private String parseScriptExpression(String expression, char quote) { + return AttributeParser.getUnquoted(expression, quote, false, false, false, false); + } +} diff --git a/test/org/apache/jasper/compiler/TestCompiler.java b/test/org/apache/jasper/compiler/TestCompiler.java new file mode 100644 index 0000000..9f439b9 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestCompiler.java @@ -0,0 +1,192 @@ +/* + * 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.jasper.compiler; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestCompiler extends TomcatBaseTest { + + @Test + public void testBug49726a() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + Map> headers = new HashMap<>(); + + getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49726a.jsp", res, headers); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + + // Check content type + String contentType = getSingleHeader("Content-Type", headers); + Assert.assertTrue(contentType.startsWith("text/html")); + } + + @Test + public void testBug49726b() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + Map> headers = new HashMap<>(); + + getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49726b.jsp", res, headers); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + + // Check content type + String contentType = getSingleHeader("Content-Type", headers); + Assert.assertTrue(contentType.startsWith("text/plain")); + } + + @Test + public void testBug53257a() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + // foo;bar.jsp + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug53257/foo%3bbar.jsp"); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + } + + @Test + public void testBug53257b() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug53257/foo&bar.jsp"); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + } + + @Test + public void testBug53257c() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + // foo#bar.jsp + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug53257/foo%23bar.jsp"); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + } + + @Test + public void testBug53257d() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + // foo%bar.jsp + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug53257/foo%25bar.jsp"); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + } + + @Test + public void testBug53257e() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug53257/foo+bar.jsp"); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + } + + @Test + public void testBug53257f() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug53257/foo%20bar.jsp"); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + } + + @Test + public void testBug53257g() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug53257/foo%20bar/foobar.jsp"); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + } + + @Test + public void testBug53257z() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + // Check that URL decoding is not done twice + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/bug53257/foo%2525bar.jsp", res, null); + Assert.assertEquals(404, rc); + } + + @Test + public void testBug51584() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + skipTldsForResourceJars(ctx); + + tomcat.start(); + + // No further tests required. The bug triggers an infinite loop on + // context start so the test will crash before it reaches this point if + // it fails + } + + @Test + public void testBug55262() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug55262.jsp"); + String result = res.toString(); + Pattern prelude = Pattern.compile("(.*This is a prelude\\.){2}.*", Pattern.MULTILINE | Pattern.DOTALL); + Pattern coda = Pattern.compile("(.*This is a coda\\.){2}.*", Pattern.MULTILINE | Pattern.DOTALL); + Assert.assertTrue(prelude.matcher(result).matches()); + Assert.assertTrue(coda.matcher(result).matches()); + } + + /** Assertion for text printed by tags:echo */ + private static void assertEcho(String result, String expected) { + Assert.assertTrue(result, result.indexOf("

    " + expected + "

    ") > 0); + } +} diff --git a/test/org/apache/jasper/compiler/TestELInterpreterFactory.java b/test/org/apache/jasper/compiler/TestELInterpreterFactory.java new file mode 100644 index 0000000..47608b8 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestELInterpreterFactory.java @@ -0,0 +1,97 @@ +/* + * 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.jasper.compiler; + +import java.io.File; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.compiler.ELInterpreterFactory.DefaultELInterpreter; + +public class TestELInterpreterFactory extends TomcatBaseTest { + + public static class SimpleELInterpreter implements ELInterpreter { + + @Override + public String interpreterCall(JspCompilationContext context, boolean isTagFile, String expression, + Class expectedType, String fnmapvar) { + return expression; + } + } + + @Test + public void testBug54239() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + tomcat.start(); + + ServletContext context = ctx.getServletContext(); + + ELInterpreter interpreter = ELInterpreterFactory.getELInterpreter(context); + Assert.assertNotNull(interpreter); + assertThat(interpreter, instanceOf(DefaultELInterpreter.class)); + + context.removeAttribute(ELInterpreter.class.getName()); + + context.setAttribute(ELInterpreter.class.getName(), SimpleELInterpreter.class.getName()); + interpreter = ELInterpreterFactory.getELInterpreter(context); + Assert.assertNotNull(interpreter); + assertThat(interpreter, instanceOf(SimpleELInterpreter.class)); + + context.removeAttribute(ELInterpreter.class.getName()); + + SimpleELInterpreter simpleInterpreter = new SimpleELInterpreter(); + context.setAttribute(ELInterpreter.class.getName(), simpleInterpreter); + interpreter = ELInterpreterFactory.getELInterpreter(context); + Assert.assertNotNull(interpreter); + assertThat(interpreter, instanceOf(SimpleELInterpreter.class)); + Assert.assertTrue(interpreter == simpleInterpreter); + + context.removeAttribute(ELInterpreter.class.getName()); + + ctx.stop(); + ctx.addApplicationListener(Bug54239Listener.class.getName()); + ctx.start(); + + interpreter = ELInterpreterFactory.getELInterpreter(ctx.getServletContext()); + Assert.assertNotNull(interpreter); + assertThat(interpreter, instanceOf(SimpleELInterpreter.class)); + } + + public static class Bug54239Listener implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + sce.getServletContext().setInitParameter(ELInterpreter.class.getName(), + SimpleELInterpreter.class.getName()); + } + } +} diff --git a/test/org/apache/jasper/compiler/TestELParser.java b/test/org/apache/jasper/compiler/TestELParser.java new file mode 100644 index 0000000..f68507c --- /dev/null +++ b/test/org/apache/jasper/compiler/TestELParser.java @@ -0,0 +1,322 @@ +/* + * 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.jasper.compiler; + +import jakarta.el.ELContext; +import jakarta.el.ELException; +import jakarta.el.ELManager; +import jakarta.el.ExpressionFactory; +import jakarta.el.ValueExpression; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.JasperException; +import org.apache.jasper.compiler.ELNode.Nodes; +import org.apache.jasper.compiler.ELParser.TextBuilder; + +/** + * You will need to keep your wits about you when working with this class. Keep in mind the following: + *
      + *
    • If in doubt, read the EL and JSP specifications. Twice.
    • + *
    • The escaping rules are complex and subtle. The explanation below (as well as the tests and the implementation) + * may have missed an edge case despite trying hard not to. + *
    • The strings passed to {@link #doTestParser(String,String)} are Java escaped in the source code and will be + * unescaped before being used.
    • + *
    • LiteralExpressions always occur outside of "${...}" and "#{...}". Literal expressions escape '$' and '#' with + * '\\'
    • + *
    • LiteralStrings always occur inside "${...}" or "#{...}". Literal strings escape '\'', '\"' and '\\' with '\\'. + * Escaping '\"' is optional if the literal string is delimited by '\''. Escaping '\'' is optional if the literal string + * is delimited by '\"'.
    • + *
    + */ +public class TestELParser { + + @Test + public void testText() throws JasperException { + doTestParser("foo", "foo"); + } + + + @Test + public void testLiteral() throws JasperException { + doTestParser("${'foo'}", "foo"); + } + + + @Test + public void testVariable() throws JasperException { + doTestParser("${test}", null); + } + + + @Test + public void testFunction01() throws JasperException { + doTestParser("${do(x)}", null); + } + + + @Test + public void testFunction02() throws JasperException { + doTestParser("${do:it(x)}", null); + } + + + @Test + public void testFunction03() throws JasperException { + doTestParser("${do:it(x,y)}", null); + } + + + @Test + public void testFunction04() throws JasperException { + doTestParser("${do:it(x,y,z)}", null); + } + + + @Test + public void testFunction05() throws JasperException { + doTestParser("${do:it(x, '\\\\y',z)}", null); + } + + + @Test + public void testCompound01() throws JasperException { + doTestParser("1${'foo'}1", "1foo1"); + } + + + @Test + public void testCompound02() throws JasperException { + doTestParser("1${test}1", null); + } + + + @Test + public void testCompound03() throws JasperException { + doTestParser("${foo}${bar}", null); + } + + + @Test + public void testTernary01() throws JasperException { + doTestParser("${true?true:false}", "true"); + } + + + @Test + public void testTernary02() throws JasperException { + doTestParser("${a==1?true:false}", null); + } + + + @Test + public void testTernary03() throws JasperException { + doTestParser("${a eq1?true:false}", null); + } + + + @Test + public void testTernary04() throws JasperException { + doTestParser(" ${ a eq 1 ? true : false } ", null); + } + + + @Test + public void testTernary05() throws JasperException { + // Note this is invalid EL + doTestParser("${aeq1?true:false}", null); + } + + + @Test + public void testTernary06() throws JasperException { + doTestParser("${do:it(a eq1?true:false,y)}", null); + } + + + @Test + public void testTernary07() throws JasperException { + doTestParser(" ${ do:it( a eq 1 ? true : false, y ) } ", null); + } + + + @Test + public void testTernary08() throws JasperException { + doTestParser(" ${ do:it ( a eq 1 ? true : false, y ) } ", null); + } + + + @Test + public void testTernary09() throws JasperException { + doTestParser(" ${ do : it ( a eq 1 ? true : false, y ) } ", null); + } + + + @Test + public void testTernary10() throws JasperException { + doTestParser(" ${!empty my:link(foo)} ", null); + } + + + @Test + public void testTernary11() throws JasperException { + doTestParser("${true?'true':'false'}", "true"); + } + + + @Test + public void testTernary12() throws JasperException { + doTestParser("${true?'tr\"ue':'false'}", "tr\"ue"); + } + + + @Test + public void testTernary13() throws JasperException { + doTestParser("${true?'tr\\'ue':'false'}", "tr'ue"); + } + + + @Test + public void testTernaryBug56031() throws JasperException { + doTestParser("${my:link(!empty registration ? registration : '/test/registration')}", null); + } + + + @Test + public void testQuotes01() throws JasperException { + doTestParser("'", "'"); + } + + + @Test + public void testQuotes02() throws JasperException { + doTestParser("'${foo}'", null); + } + + + @Test + public void testQuotes03() throws JasperException { + doTestParser("'${'foo'}'", "'foo'"); + } + + + @Test + public void testEscape01() throws JasperException { + doTestParser("${'\\\\'}", "\\"); + } + + + @Test + public void testEscape02() throws JasperException { + doTestParser("\\\\x${'\\\\'}", "\\\\x\\"); + } + + + @Test + public void testEscape03() throws JasperException { + doTestParser("\\\\", "\\\\"); + } + + + @Test + public void testEscape04() throws JasperException { + // When parsed as EL in JSP the escaping of $ as \$ is optional + doTestParser("\\$", "\\$", "$"); + } + + + @Test + public void testEscape05() throws JasperException { + // When parsed as EL in JSP the escaping of # as \# is optional + doTestParser("\\#", "\\#", "#"); + } + + + @Test + public void testEscape07() throws JasperException { + doTestParser("${'\\\\$'}", "\\$"); + } + + + @Test + public void testEscape08() throws JasperException { + doTestParser("${'\\\\#'}", "\\#"); + } + + + @Test + public void testEscape09() throws JasperException { + doTestParser("\\${", "${"); + } + + + @Test + public void testEscape10() throws JasperException { + doTestParser("\\#{", "#{"); + } + + + @Test + public void testEscape11() throws JasperException { + // Bug 56612 + doTestParser("${'\\'\\''}", "''"); + } + + + private void doTestParser(String input, String expected) throws JasperException { + doTestParser(input, expected, input); + } + + private void doTestParser(String input, String expectedResult, String expectedBuilderOutput) + throws JasperException { + + ELException elException = null; + String elResult = null; + + // Don't try and evaluate expressions that depend on variables or functions + if (expectedResult != null) { + try { + ELManager manager = new ELManager(); + ELContext context = manager.getELContext(); + ExpressionFactory factory = ELManager.getExpressionFactory(); + ValueExpression ve = factory.createValueExpression(context, input, String.class); + elResult = ve.getValue(context).toString(); + Assert.assertEquals(expectedResult, elResult); + } catch (ELException ele) { + elException = ele; + } + } + + Nodes nodes = null; + try { + nodes = ELParser.parse(input, false); + Assert.assertNull(elException); + } catch (IllegalArgumentException iae) { + Assert.assertNotNull(elResult, elException); + // Not strictly true but enables us to report both + iae.initCause(elException); + throw iae; + } + + TextBuilder textBuilder = new TextBuilder(false); + + nodes.visit(textBuilder); + + Assert.assertEquals(expectedBuilderOutput, textBuilder.getText()); + } +} diff --git a/test/org/apache/jasper/compiler/TestEncodingDetector.java b/test/org/apache/jasper/compiler/TestEncodingDetector.java new file mode 100644 index 0000000..6d4f07e --- /dev/null +++ b/test/org/apache/jasper/compiler/TestEncodingDetector.java @@ -0,0 +1,94 @@ +/* + * 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.jasper.compiler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +@RunWith(Parameterized.class) +public class TestEncodingDetector extends TomcatBaseTest { + + @Parameters + public static Collection inputs() { + /// Note: These files are saved using the encoding indicated by the BOM + List result = new ArrayList<>(); + result.add(new Object[] { "bom-none-prolog-none.jsp", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-none-prolog-none.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-none-prolog-utf16be.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-none-prolog-utf16le.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-none-prolog-utf8.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf8-prolog-none.jsp", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf8-prolog-none.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf8-prolog-utf16be.jspx", Integer.valueOf(500), null }); + result.add(new Object[] { "bom-utf8-prolog-utf16le.jspx", Integer.valueOf(500), null }); + result.add(new Object[] { "bom-utf8-prolog-utf8.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf16be-prolog-none.jsp", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf16be-prolog-none.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf16be-prolog-utf16be.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf16be-prolog-utf16le.jspx", Integer.valueOf(500), null }); + result.add(new Object[] { "bom-utf16be-prolog-utf8.jspx", Integer.valueOf(500), null }); + result.add(new Object[] { "bom-utf16le-prolog-none.jsp", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf16le-prolog-none.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf16le-prolog-utf16be.jspx", Integer.valueOf(500), null }); + result.add(new Object[] { "bom-utf16le-prolog-utf16le.jspx", Integer.valueOf(200), Boolean.TRUE }); + result.add(new Object[] { "bom-utf16le-prolog-utf8.jspx", Integer.valueOf(500), null }); + result.add(new Object[] { "bug60769a.jspx", Integer.valueOf(500), null }); + result.add(new Object[] { "bug60769b.jspx", Integer.valueOf(200), Boolean.TRUE }); + return result; + } + + + @Parameter(0) + public String jspName; + + @Parameter(1) + public int expectedResponseCode; + + @Parameter(2) + public Boolean responseBodyOK; + + @Test + public void testEncodedJsp() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk responseBody = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/encoding/" + jspName, responseBody, null); + + Assert.assertEquals(expectedResponseCode, rc); + + if (expectedResponseCode == 200) { + // trim() to remove whitespace like new lines + String bodyText = responseBody.toString().trim(); + if (responseBodyOK.booleanValue()) { + Assert.assertEquals("OK", bodyText); + } else { + Assert.assertNotEquals("OK", bodyText); + } + } + } +} diff --git a/test/org/apache/jasper/compiler/TestGenerator.java b/test/org/apache/jasper/compiler/TestGenerator.java new file mode 100644 index 0000000..0e9a5ff --- /dev/null +++ b/test/org/apache/jasper/compiler/TestGenerator.java @@ -0,0 +1,982 @@ +/* + * 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.jasper.compiler; + +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.beans.PropertyEditorSupport; +import java.io.File; +import java.io.IOException; +import java.nio.charset.CodingErrorAction; +import java.util.Date; +import java.util.Scanner; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.BodyTagSupport; +import jakarta.servlet.jsp.tagext.DynamicAttributes; +import jakarta.servlet.jsp.tagext.JspIdConsumer; +import jakarta.servlet.jsp.tagext.Tag; +import jakarta.servlet.jsp.tagext.TagData; +import jakarta.servlet.jsp.tagext.TagExtraInfo; +import jakarta.servlet.jsp.tagext.TagSupport; +import jakarta.servlet.jsp.tagext.TryCatchFinally; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.servlet.JasperInitializer; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.compat.JreCompat; + +public class TestGenerator extends TomcatBaseTest { + + @Test + public void testBug45015a() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45015a.jsp"); + + String result = res.toString(); + // Beware of the differences between escaping in JSP attributes and + // in Java Strings + assertEcho(result, "00-hello 'world'"); + assertEcho(result, "01-hello 'world"); + assertEcho(result, "02-hello world'"); + assertEcho(result, "03-hello world'"); + assertEcho(result, "04-hello world\""); + assertEcho(result, "05-hello \"world\""); + assertEcho(result, "06-hello \"world"); + assertEcho(result, "07-hello world\""); + assertEcho(result, "08-hello world'"); + assertEcho(result, "09-hello world\""); + } + + @Test + public void testBug45015b() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45015b.jsp", new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + @Test + public void testBug45015c() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug45nnn/bug45015c.jsp", new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + @Test + public void testBug48701Fail() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug48nnn/bug48701-fail.jsp", new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + @Test + public void testBug48701UseBean() throws Exception { + testBug48701("bug48nnn/bug48701-UseBean.jsp"); + } + + @Test + public void testBug48701VariableInfo() throws Exception { + testBug48701("bug48nnn/bug48701-VI.jsp"); + } + + @Test + public void testBug48701TagVariableInfoNameGiven() throws Exception { + testBug48701("bug48nnn/bug48701-TVI-NG.jsp"); + } + + @Test + public void testBug48701TagVariableInfoNameFromAttribute() throws Exception { + testBug48701("bug48nnn/bug48701-TVI-NFA.jsp"); + } + + private void testBug48701(String jsp) throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/" + jsp); + + String result = res.toString(); + assertEcho(result, "00-PASS"); + } + + public static class Bug48701 extends TagSupport { + + private static final long serialVersionUID = 1L; + + private String beanName = null; + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public String getBeanName() { + return beanName; + } + + @Override + public int doStartTag() throws JspException { + Bean bean = new Bean(); + bean.setTime((new Date()).toString()); + pageContext.setAttribute("now", bean); + return super.doStartTag(); + } + } + + + public static class Bug48701TEI extends TagExtraInfo { + + @Override + public VariableInfo[] getVariableInfo(TagData data) { + return new VariableInfo[] { + new VariableInfo("now", Bean.class.getCanonicalName(), true, VariableInfo.AT_END) }; + } + } + + + public static class Bean { + private String time; + + public void setTime(String time) { + this.time = time; + } + + public String getTime() { + return time; + } + } + + @Test + public void testBug49799() throws Exception { + + String[] expected = + { "

    00-Red

    ", "

    01-Not Red

    ", "

    02-Red

    ", + "

    03-Not Red

    ", "

    04-Red

    ", "

    05-Not Red

    " }; + + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49799.jsp", res, null); + + // Check request completed + String result = res.toString(); + String[] lines = result.split("\n|\r|\r\n"); + int i = 0; + for (String line : lines) { + if (line.length() > 0) { + Assert.assertEquals(expected[i], line); + i++; + } + } + } + + /** Assertion for text printed by tags:echo */ + private static void assertEcho(String result, String expected) { + Assert.assertTrue(result.indexOf("

    " + expected + "

    ") > 0); + } + + @Test + public void testBug56529() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug56529.jsp", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + String response = bc.toStringInternal(CodingErrorAction.REPORT, CodingErrorAction.REPORT); + Assert.assertTrue(response, response.contains("[1:attribute1: '', attribute2: '']")); + Assert.assertTrue(response, response.contains("[2:attribute1: '', attribute2: '']")); + } + + public static class Bug56529 extends TagSupport { + + private static final long serialVersionUID = 1L; + + private String attribute1 = null; + + private String attribute2 = null; + + public void setAttribute1(String attribute1) { + this.attribute1 = attribute1; + } + + public String getAttribute1() { + return attribute1; + } + + public void setAttribute2(String attribute2) { + this.attribute2 = attribute2; + } + + public String getAttribute2() { + return attribute2; + } + + @Override + public int doEndTag() throws JspException { + try { + pageContext.getOut().print("attribute1: '" + attribute1 + "', " + "attribute2: '" + attribute2 + "'"); + } catch (IOException e) { + throw new JspException(e); + } + return EVAL_PAGE; + } + + } + + @Test + public void testBug56581() throws LifecycleException { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + try { + getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug56581.jsp", res, null); + Assert.fail("An IOException was expected."); + } catch (IOException expected) { + // ErrorReportValve in Tomcat 8.0.9+ flushes and aborts the + // connection when an unexpected error is encountered and response + // has already been committed. It results in an exception here: + // java.io.IOException: Premature EOF + } + + String result = res.toString(); + Assert.assertTrue(result.startsWith("0 Hello world!\n")); + Assert.assertTrue(result.endsWith("999 Hello world!\n")); + } + + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=43400 + @Test + public void testTagsWithEnums() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug43nnn/bug43400.jsp"); + + String result = res.toString(); + System.out.println(result); + assertEcho(result, "ASYNC"); + } + + @Test + public void testTrimSpacesExtended01() throws Exception { + doTestTrimSpacesExtended(false); + } + + @Test + public void testTrimSpacesExtended02() throws Exception { + doTestTrimSpacesExtended(true); + } + + private void doTestTrimSpacesExtended(boolean removeBlankLines) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + ctxt.addServletContainerInitializer(new JasperInitializer(), null); + + Tomcat.initWebappDefaults(ctxt); + Wrapper w = (Wrapper) ctxt.findChild("jsp"); + if (removeBlankLines) { + w.addInitParameter("trimSpaces", "extended"); + } + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/jsp/trim-spaces-extended.jsp"); + + String result = res.toString(); + Scanner scanner = new Scanner(result); + int blankLineCount = 0; + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.length() == 0) { + blankLineCount++; + } + } + if (!removeBlankLines && blankLineCount == 0) { + Assert.fail("TrimSpaceOptions.EXTENDED not configured but balnk lines have been removed"); + } else if (removeBlankLines && blankLineCount > 0) { + Assert.fail("TrimSpaceOptions.EXTENDED does not allow the line to be just a new line character"); + } + scanner.close(); + } + + @Test + public void testEscape01() { + String result = Generator.escape("\"\\\n\r"); + Assert.assertEquals("\\\"\\\\\\n\\r", result); + } + + @Test + public void testEscape02() { + String result = Generator.escape("\\"); + Assert.assertEquals("\\\\", result); + } + + @Test + public void testEscape03() { + String result = Generator.escape("xxx\\"); + Assert.assertEquals("xxx\\\\", result); + } + + @Test + public void testEscape04() { + String result = Generator.escape("\\xxx"); + Assert.assertEquals("\\\\xxx", result); + } + + @Test + public void testQuote01() { + String result = Generator.quote('\''); + Assert.assertEquals("\'\\\'\'", result); + } + + @Test + public void testQuote02() { + String result = Generator.quote('\\'); + Assert.assertEquals("\'\\\\\'", result); + } + + @Test + public void testQuote03() { + String result = Generator.quote('\n'); + Assert.assertEquals("\'\\n\'", result); + } + + @Test + public void testQuote04() { + String result = Generator.quote('\r'); + Assert.assertEquals("\'\\r\'", result); + } + + @Test + public void testQuote05() { + String result = Generator.quote('x'); + Assert.assertEquals("\'x\'", result); + } + + @Test + public void testJspId() throws Exception { + doTestJspId(false); + } + + @Test + public void testJspIdDocument() throws Exception { + doTestJspId(true); + } + + private void doTestJspId(boolean document) throws Exception { + getTomcatInstanceTestWebapp(false, true); + + String uri = "http://localhost:" + getPort() + "/test/jsp/generator/jsp-id.jsp"; + if (document) { + uri += "x"; + } + ByteChunk res = getUrl(uri); + + String result = res.toString(); + + // Two tags should have different IDs + String[] ids = new String[2]; + int start = 0; + int end = 0; + for (int i = 0; i < ids.length; i++) { + start = result.indexOf("Jsp ID is [", start) + 11; + end = result.indexOf("]", start); + ids[i] = result.substring(start, end); + } + + // Confirm the IDs are not the same + Assert.assertNotEquals(ids[0], ids[1]); + } + + @Test + public void testTryCatchFinally02() throws Exception { + doTestJsp("try-catch-finally-02.jsp"); + } + + public static class JspIdTag extends TagSupport implements JspIdConsumer { + + private static final long serialVersionUID = 1L; + + private volatile String jspId; + + @Override + public int doStartTag() throws JspException { + try { + pageContext.getOut().print("

    Jsp ID is [" + jspId + "]

    "); + } catch (IOException ioe) { + throw new JspException(ioe); + } + return super.doStartTag(); + } + + @Override + public void setJspId(String jspId) { + this.jspId = jspId; + } + } + + public static class TryCatchFinallyBodyTag extends BodyTagSupport implements TryCatchFinally { + + private static final long serialVersionUID = 1L; + + @Override + public int doStartTag() throws JspException { + try { + pageContext.getOut().print("

    OK

    "); + } catch (IOException ioe) { + throw new JspException(ioe); + } + return super.doStartTag(); + } + + @Override + public void doCatch(Throwable t) throws Throwable { + // NO-OP + } + + @Override + public void doFinally() { + // NO-OP + } + } + + public static class TryCatchFinallyTag extends TagSupport implements TryCatchFinally { + + private static final long serialVersionUID = 1L; + + @Override + public void doCatch(Throwable t) throws Throwable { + // NO-OP + } + + @Override + public void doFinally() { + // NO-OP + } + } + + public static class TesterBodyTag extends BodyTagSupport { + + private static final long serialVersionUID = 1L; + + @Override + public int doStartTag() throws JspException { + try { + pageContext.getOut().print("

    OK

    "); + } catch (IOException ioe) { + throw new JspException(ioe); + } + return super.doStartTag(); + } + } + + public static class TesterTag implements Tag { + + private Tag parent; + + @Override + public void setPageContext(PageContext pc) { + } + + @Override + public void setParent(Tag t) { + parent = t; + } + + @Override + public Tag getParent() { + return parent; + } + + @Override + public int doStartTag() throws JspException { + return 0; + } + + @Override + public int doEndTag() throws JspException { + return 0; + } + + @Override + public void release() { + } + } + + public static class TesterTagA extends TesterTag { + private String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + public static class DataPropertyEditor extends PropertyEditorSupport { + } + + public static class TesterScriptingTag extends TagSupport { + + private static final long serialVersionUID = 1L; + + private String attribute02; + private String attribute03; + + public String getAttribute02() { + return attribute02; + } + + public void setAttribute02(String attribute02) { + this.attribute02 = attribute02; + } + + public String getAttribute03() { + return attribute03; + } + + public void setAttribute03(String attribute03) { + this.attribute03 = attribute03; + } + } + + public static class TesterScriptingTagB extends TagSupport { + + private static final long serialVersionUID = 1L; + + private String attribute02; + + public String getAttribute02() { + return attribute02; + } + + public void setAttribute02(String attribute02) { + this.attribute02 = attribute02; + } + } + + public static class TesterScriptingTagBTEI extends TagExtraInfo { + + @Override + public VariableInfo[] getVariableInfo(TagData data) { + return new VariableInfo[] { new VariableInfo("variable01", "java.lang.String", true, VariableInfo.NESTED), + new VariableInfo(data.getAttribute("attribute02").toString(), "java.lang.String", true, + VariableInfo.NESTED), + new VariableInfo("variable03", "java.lang.String", false, VariableInfo.NESTED) }; + } + + } + + public static class TesterDynamicTag extends TagSupport implements DynamicAttributes { + + private static final long serialVersionUID = 1L; + + @Override + public void setDynamicAttribute(String uri, String localName, Object value) throws JspException { + // NO-OP + } + } + + public static class TesterAttributeTag extends TagSupport { + + private static final long serialVersionUID = 1L; + + private Object attribute01; + private Object attribute02; + private Object attribute03; + private Object attribute04; + private Object attribute05; + private Object attribute06; + + public Object getAttribute01() { + return attribute01; + } + + public void setAttribute01(Object attribute01) { + this.attribute01 = attribute01; + } + + public Object getAttribute02() { + return attribute02; + } + + public void setAttribute02(Object attribute02) { + this.attribute02 = attribute02; + } + + public Object getAttribute03() { + return attribute03; + } + + public void setAttribute03(Object attribute03) { + this.attribute03 = attribute03; + } + + public Object getAttribute04() { + return attribute04; + } + + public void setAttribute04(Object attribute04) { + this.attribute04 = attribute04; + } + + public Object getAttribute05() { + return attribute05; + } + + public void setAttribute05(Object attribute05) { + this.attribute05 = attribute05; + } + + public Object getAttribute06() { + return attribute06; + } + + public void setAttribute06(Object attribute06) { + this.attribute06 = attribute06; + } + } + + + @Test + public void testInfoConflictNone() throws Exception { + doTestJsp("info-conflict-none.jsp"); + } + + @Test + public void testInfoConflict() throws Exception { + doTestJsp("info-conflict.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testTagWithVariable() throws Exception { + doTestJsp("variable-tei-nested.jsp"); + } + + @Test + public void testTagWithVariableFromAttr() throws Exception { + doTestJsp("variable-from-attr-nested.jsp"); + } + + @Test + public void testTagFileWithVariable() throws Exception { + doTestJsp("variable-tagfile-nested.jsp"); + } + + @Test + public void testTagFileWithVariableFromAttr() throws Exception { + doTestJsp("variable-tagfile-from-attr-nested.jsp"); + } + + @Test + public void testSingleThreaded() throws Exception { + doTestJsp("single-threaded.jsp"); + } + + @Test + public void testXpoweredBy() throws Exception { + doTestJsp("x-powered-by.jsp"); + } + + @Test + public void testXmlProlog01() throws Exception { + doTestJsp("xml-prolog-01.jspx"); + } + + @Test + public void testXmlProlog02() throws Exception { + doTestJsp("xml-prolog-02.jspx"); + } + + @Test + public void testXmlPrologTag() throws Exception { + doTestJsp("xml-prolog-tag.jspx"); + } + + @Test + public void testXmlDoctype01() throws Exception { + doTestJsp("xml-doctype-01.jspx"); + } + + @Test + public void testXmlDoctype02() throws Exception { + doTestJsp("xml-doctype-02.jspx"); + } + + @Test + public void testPlugin01() throws Exception { + doTestJsp("plugin-01.jspx"); + } + + @Test + public void testForward01() throws Exception { + doTestJsp("forward-01.jsp"); + } + + @Test + public void testForward02() throws Exception { + doTestJsp("forward-02.jsp"); + } + + @Test + public void testForward03() throws Exception { + doTestJsp("forward-03.jsp"); + } + + @Test + public void testForward04() throws Exception { + doTestJsp("forward-04.jsp"); + } + + @Test + public void testElement01() throws Exception { + doTestJsp("element-01.jsp"); + } + + @Test + public void testInclude01() throws Exception { + doTestJsp("include-01.jsp"); + } + + @Test + public void testSetProperty01() throws Exception { + doTestJsp("setproperty-01.jsp"); + } + + @Test + public void testUseBean01() throws Exception { + doTestJsp("usebean-01.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testUseBean02() throws Exception { + doTestJsp("usebean-02.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testUseBean03() throws Exception { + doTestJsp("usebean-03.jsp"); + } + + @Test + public void testUseBean04() throws Exception { + doTestJsp("usebean-04.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testUseBean05() throws Exception { + // Whether this test passes or fails depends on the Java version used + // and the JRE settings. + // For the test to pass it requires --illegal-access=deny + // This is the default setting on Java 16 upwards + if (JreCompat.isJre16Available()) { + doTestJsp("usebean-05.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } else { + doTestJsp("usebean-05.jsp", HttpServletResponse.SC_OK); + } + } + + @Test + public void testUseBean06() throws Exception { + doTestJsp("usebean-06.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testUseBean07() throws Exception { + doTestJsp("usebean-07.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testUseBean08() throws Exception { + doTestJsp("usebean-08.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testCustomTag01() throws Exception { + doTestJsp("try-catch-finally-01.jsp"); + } + + @Test + public void testCustomTag02() throws Exception { + doTestJsp("customtag-02.jsp"); + } + + @Test + public void testCustomTag03() throws Exception { + doTestJsp("customtag-03.jsp"); + } + + @Test + public void testCustomTag04() throws Exception { + doTestJsp("customtag-04.jsp", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void testTemplateText01() throws Exception { + doTestJsp("templatetext-01.jsp"); + } + + @Test + public void testTemplateText02() throws Exception { + doTestJsp("templatetext-02.jsp"); + } + + @Test + public void testInvoke01() throws Exception { + doTestJsp("invoke-01.jsp"); + } + + @Test + public void testDoBody01() throws Exception { + doTestJsp("dobody-01.jsp"); + } + + @Test + public void testScriptingVariables01() throws Exception { + doTestJsp("scriptingvariables-01.jsp"); + } + + @Test + public void testScriptingVariables02() throws Exception { + doTestJsp("scriptingvariables-02.jsp"); + } + + @Test + public void testAttribute01() throws Exception { + doTestJsp("attribute-01.jsp"); + } + + @Test + public void testAttribute02() throws Exception { + doTestJsp("attribute-02.jsp"); + } + + @Test + public void testAttribute03() throws Exception { + doTestJsp("attribute-03.jsp"); + } + + @Test + public void testAttribute04() throws Exception { + doTestJsp("attribute-04.jsp"); + } + + @Test + public void testSetters01() throws Exception { + doTestJsp("setters-01.jsp"); + } + + @Test + public void testCircular01() throws Exception { + doTestJsp("circular-01.jsp"); + } + + @Test + public void testDeferredMethod01() throws Exception { + doTestJsp("deferred-method-01.jsp"); + } + + @Test + public void testDeferredMethod02() throws Exception { + doTestJsp("deferred-method-02.jsp"); + } + + @Test + public void testBeanInfo01() throws Exception { + BeanInfo bi = Introspector.getBeanInfo(TesterTagA.class); + for (PropertyDescriptor pd : bi.getPropertyDescriptors()) { + if (pd.getName().equals("data")) { + pd.setPropertyEditorClass(DataPropertyEditor.class); + } + } + + doTestJsp("beaninfo-01.jsp"); + } + + @Test + public void testBreakELInterpreter() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + // This should break all subsequent requests + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/generator/break-el-interpreter.jsp", body, null); + Assert.assertEquals(body.toString(), HttpServletResponse.SC_OK, rc); + + body.recycle(); + + rc = getUrl("http://localhost:" + getPort() + "/test/jsp/generator/info.jsp", body, null); + Assert.assertEquals(body.toString(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + @Test + public void testBreakStringInterpreter() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + // This should break all subsequent requests + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/generator/break-string-interpreter.jsp", body, + null); + Assert.assertEquals(body.toString(), HttpServletResponse.SC_OK, rc); + + body.recycle(); + + rc = getUrl("http://localhost:" + getPort() + "/test/jsp/generator/info.jsp", body, null); + Assert.assertEquals(body.toString(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + @Test + public void testBug65390() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug65390.jsp", body, null); + + Assert.assertEquals(body.toString(), HttpServletResponse.SC_OK, rc); + } + + private void doTestJsp(String jspName) throws Exception { + doTestJsp(jspName, HttpServletResponse.SC_OK); + } + + private void doTestJsp(String jspName, int expectedResponseCode) throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/generator/" + jspName, body, null); + + Assert.assertEquals(body.toString(), expectedResponseCode, rc); + } +} diff --git a/test/org/apache/jasper/compiler/TestJspConfig.java b/test/org/apache/jasper/compiler/TestJspConfig.java new file mode 100644 index 0000000..4f9cf26 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestJspConfig.java @@ -0,0 +1,332 @@ +/* + * 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.jasper.compiler; + +import java.io.File; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestJspConfig extends TomcatBaseTest { + + @Test + public void testServlet22NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-2.2"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-${'hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    01-#{'hello world'}

    ") > 0); + } + + @Test + public void testServlet23NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-2.3"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-${'hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    01-#{'hello world'}

    ") > 0); + } + + @Test + public void testServlet24NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-2.4"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    01-#{'hello world'}

    ") > 0); + } + + @Test + public void testServlet25NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-2.5"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + } + + @Test + public void testServlet30NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-3.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + } + + @Test + public void testServlet31NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-3.1"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + } + + @Test + public void testServlet40NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-4.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + } + + @Test + public void testServlet50NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-5.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + } + + @Test + public void testServlet60NoEL() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-6.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/el-as-literal.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + } + + @Test + public void testErrorOnELNotFound01() throws Exception { + // Defaults + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/errorOnELNotFound/default.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = res.toString(); + Assert.assertTrue(result, result.indexOf("

    00-OK

    ") > 0); + } + + @Test + public void testErrorOnELNotFound02() throws Exception { + // Page directive true + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/errorOnELNotFound/page-directive-true.jsp", res, + null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + // Look for the non-i18n part of the Exception message + String result = res.toString(); + Assert.assertTrue(result, result.indexOf("[unknown]") > 0); + } + + @Test + public void testErrorOnELNotFound03() throws Exception { + // Page directive false + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/errorOnELNotFound/page-directive-false.jsp", res, + null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = res.toString(); + Assert.assertTrue(result, result.indexOf("

    00-OK

    ") > 0); + } + + @Test + public void testErrorOnELNotFound04() throws Exception { + // web.xml true + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/errorOnELNotFound/web-xml-true.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + // Look for the non-i18n part of the Exception message + String result = res.toString(); + Assert.assertTrue(result, result.indexOf("[unknown]") > 0); + } + + @Test + public void testErrorOnELNotFound05() throws Exception { + // web.xml false + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/errorOnELNotFound/web-xml-false.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = res.toString(); + Assert.assertTrue(result, result.indexOf("

    00-OK

    ") > 0); + } + + @Test + public void testErrorOnELNotFound06() throws Exception { + // tag file true + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/errorOnELNotFound/tag-file-true.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + // Look for the non-i18n part of the Exception message + String result = res.toString(); + Assert.assertTrue(result, result.indexOf("[unknown]") > 0); + } + + @Test + public void testErrorOnELNotFound07() throws Exception { + // tag file false + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/errorOnELNotFound/tag-file-false.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = res.toString(); + Assert.assertTrue(result, result.indexOf("

    00-OK

    ") > 0); + } +} diff --git a/test/org/apache/jasper/compiler/TestJspDocumentParser.java b/test/org/apache/jasper/compiler/TestJspDocumentParser.java new file mode 100644 index 0000000..71eedd8 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestJspDocumentParser.java @@ -0,0 +1,220 @@ +/* + * 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.jasper.compiler; + +import java.io.IOException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.w3c.dom.Document; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public class TestJspDocumentParser extends TomcatBaseTest { + + @Test + public void testBug47977() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug47977.jspx", new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + @Test + public void testBug48827() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + Exception e = null; + try { + getUrl("http://localhost:" + getPort() + "/test/bug48nnn/bug48827.jspx"); + } catch (IOException ioe) { + e = ioe; + } + + // Should not fail + Assert.assertNull(e); + } + + @Test + public void testBug54801() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug54801a.jspx", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + bc.recycle(); + rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug54801b.jspx", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + + @Test + public void testBug54821() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug54821a.jspx", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + bc.recycle(); + rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug54821b.jspx", bc, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + + @Test + public void testSchemaValidation() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + String path = "http://localhost:" + getPort() + "/test/valid.jspx"; + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setValidating(true); + dbf.setFeature("http://apache.org/xml/features/validation/schema", true); + DocumentBuilder db = dbf.newDocumentBuilder(); + db.setErrorHandler(new ErrorHandler() { + @Override + public void warning(SAXParseException exception) throws SAXException { + throw exception; + } + + @Override + public void error(SAXParseException exception) throws SAXException { + throw exception; + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + throw exception; + } + }); + Document document = db.parse(path); + Assert.assertEquals("urn:valid", document.getDocumentElement().getNamespaceURI()); + Assert.assertEquals("root", document.getDocumentElement().getLocalName()); + } + + @Test + public void testDocument_0_4() throws Exception { + doTestDocument(false, "0.4"); + } + + @Test + public void testDocument_1_1() throws Exception { + doTestDocument(false, "1.1"); + } + + @Test + public void testDocument_1_2() throws Exception { + doTestDocument(true, "1.2"); + } + + @Test + public void testDocument_1_2_1() throws Exception { + doTestDocument(false, "1.2.1"); + } + + @Test + public void testDocument_1_3() throws Exception { + doTestDocument(false, "1.3"); + } + + @Test + public void testDocument_1_9() throws Exception { + doTestDocument(false, "1.9"); + } + + @Test + public void testDocument_2_0() throws Exception { + doTestDocument(true, "2.0"); + } + + @Test + public void testDocument_2_1() throws Exception { + doTestDocument(true, "2.1"); + } + + @Test + public void testDocument_2_2() throws Exception { + doTestDocument(true, "2.2"); + } + + @Test + public void testDocument_2_3() throws Exception { + doTestDocument(true, "2.3"); + } + + @Test + public void testDocument_2_4() throws Exception { + doTestDocument(false, "2.4"); + } + + @Test + public void testDocument_3_0() throws Exception { + doTestDocument(true, "3.0"); + } + + @Test + public void testDocument_3_1() throws Exception { + doTestDocument(true, "3.1"); + } + + @Test + public void testDocument_3_2() throws Exception { + doTestDocument(false, "3.2"); + } + + @Test + public void testDocument_4_0() throws Exception { + doTestDocument(false, "4.0"); + } + + @Test + public void testDocument_5_4() throws Exception { + doTestDocument(false, "5.4"); + } + + private void doTestDocument(boolean valid, String version) throws Exception { + getTomcatInstanceTestWebapp(false, true); + + StringBuilder url = new StringBuilder("http://localhost:"); + url.append(getPort()); + url.append("/test/jsp/doc-version-"); + if (!valid) { + url.append("in"); + } + url.append("valid/document-"); + url.append(version); + url.append(".jspx"); + + int rc = getUrl(url.toString(), new ByteChunk(), null); + + if (valid) { + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } else { + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + } +} diff --git a/test/org/apache/jasper/compiler/TestJspReader.java b/test/org/apache/jasper/compiler/TestJspReader.java new file mode 100644 index 0000000..9e12a65 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestJspReader.java @@ -0,0 +1,34 @@ +/* + * 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.jasper.compiler; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestJspReader extends TomcatBaseTest { + + @Test + public void testBug53986() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug53986.jsp"); + Assert.assertTrue(res.toString().contains("OK")); + } +} diff --git a/test/org/apache/jasper/compiler/TestJspUtil.java b/test/org/apache/jasper/compiler/TestJspUtil.java new file mode 100644 index 0000000..9516f03 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestJspUtil.java @@ -0,0 +1,49 @@ +/* + * 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.jasper.compiler; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestJspUtil extends TomcatBaseTest { + + @Test + public void testBoxedPrimitiveConstructors() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug65377.jsp", bc, null); + + String body = bc.toString(); + String[] lines = body.split("\n"); + + for (String line : lines) { + line = line.trim(); + if (line.startsWith("

    ")) { + line = line.substring(3, line.length() - 4); + String[] parts = line.split(":"); + Assert.assertEquals(parts[0], parts[1], parts[2]); + } + } + Assert.assertEquals(bc.toString(), HttpServletResponse.SC_OK, rc); + } +} diff --git a/test/org/apache/jasper/compiler/TestJspUtilMakeJavaPackage.java b/test/org/apache/jasper/compiler/TestJspUtilMakeJavaPackage.java new file mode 100644 index 0000000..dba45a6 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestJspUtilMakeJavaPackage.java @@ -0,0 +1,63 @@ +/* + * 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.jasper.compiler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestJspUtilMakeJavaPackage { + + @Parameterized.Parameters(name = "{index}: input[{0}], expected [{1}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { "/foo", "foo" }); + parameterSets.add(new Object[] { "//foo", "foo" }); + parameterSets.add(new Object[] { "//foo//", "foo" }); + parameterSets.add(new Object[] { "/foo//", "foo" }); + parameterSets.add(new Object[] { "/foo/", "foo" }); + parameterSets.add(new Object[] { "foo/", "foo" }); + + parameterSets.add(new Object[] { "/foo/bar", "foo.bar" }); + parameterSets.add(new Object[] { "//foo/bar", "foo.bar" }); + parameterSets.add(new Object[] { "//foo//bar", "foo.bar" }); + parameterSets.add(new Object[] { "/foo//bar", "foo.bar" }); + parameterSets.add(new Object[] { "/foo/bar", "foo.bar" }); + parameterSets.add(new Object[] { "foo/bar", "foo.bar" }); + + return parameterSets; + } + + @Parameter(0) + public String input; + + @Parameter(1) + public String expected; + + @Test + public void doTest() { + Assert.assertEquals(expected, JspUtil.makeJavaPackage(input)); + } +} diff --git a/test/org/apache/jasper/compiler/TestNode.java b/test/org/apache/jasper/compiler/TestNode.java new file mode 100644 index 0000000..c959b09 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestNode.java @@ -0,0 +1,73 @@ +/* + * 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.jasper.compiler; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.compiler.Node.PageDirective; + +public class TestNode { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=57099 + */ + @Test(expected = IllegalArgumentException.class) + public void testPageDirectiveImport01() { + doTestPageDirectiveImport("java.io.*;\r\n\timport java.net.*"); + } + + @Test + public void testPageDirectiveImport02() { + doTestPageDirectiveImport("a,b,c"); + } + + @Test + public void testPageDirectiveImport03() { + doTestPageDirectiveImport(" a , b , c "); + } + + @Test + public void testPageDirectiveImport04() { + doTestPageDirectiveImport(" a\n , \r\nb , \nc\r "); + } + + @Test + public void testPageDirectiveImport05() { + doTestPageDirectiveImport("java.util.List,java.util.ArrayList,java.util.Set"); + } + + @Test(expected = IllegalArgumentException.class) + public void testPageDirectiveImport06() { + doTestPageDirectiveImport("java.util.List;import java.util.ArrayList; import java.util.Set"); + } + + @Test + public void testPageDirectiveImport07() { + doTestPageDirectiveImport("java .\nutil.List,java.util.ArrayList,java.util.Set"); + } + + private void doTestPageDirectiveImport(String importDirective) { + PageDirective pd = new PageDirective(null, null, null); + pd.addImport(importDirective); + List imports = pd.getImports(); + + Assert.assertEquals(3, imports.size()); + } +} diff --git a/test/org/apache/jasper/compiler/TestNodeIntegration.java b/test/org/apache/jasper/compiler/TestNodeIntegration.java new file mode 100644 index 0000000..9a1bb05 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestNodeIntegration.java @@ -0,0 +1,37 @@ +/* + * 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.jasper.compiler; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestNodeIntegration extends TomcatBaseTest { + + @Test + public void testJspAttributeIsLiteral() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug55642a.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("/test/bug5nnnn/bug55642b.jsp?foo=bar&a=1&b=2") > 0); + } +} diff --git a/test/org/apache/jasper/compiler/TestParser.java b/test/org/apache/jasper/compiler/TestParser.java new file mode 100644 index 0000000..e2f2079 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestParser.java @@ -0,0 +1,269 @@ +/* + * 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.jasper.compiler; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Tests that depend on strict whitespace parsing are duplicated in {@link TestParserNoStrictWhitespace} with the strict + * whitespace parsing disabled. + */ +public class TestParser extends TomcatBaseTest { + + @Test + public void testBug48627() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug48nnn/bug48627.jsp"); + + String result = res.toString(); + // Beware of the differences between escaping in JSP attributes and + // in Java Strings + assertEcho(result, "00-\\"); + assertEcho(result, "01-\\"); + } + + @Test + public void testBug48668a() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug48nnn/bug48668a.jsp"); + String result = res.toString(); + assertEcho(result, "00-Hello world

    #{foo.bar}"); + assertEcho(result, "01-Hello world

    ${foo.bar}"); + assertEcho(result, "10-Hello ${'foo.bar}"); + assertEcho(result, "11-Hello ${'foo.bar}"); + assertEcho(result, "12-Hello #{'foo.bar}"); + assertEcho(result, "13-Hello #{'foo.bar}"); + assertEcho(result, "14-Hello ${'foo}"); + assertEcho(result, "15-Hello ${'foo}"); + assertEcho(result, "16-Hello #{'foo}"); + assertEcho(result, "17-Hello #{'foo}"); + assertEcho(result, "18-Hello ${'foo.bar}"); + assertEcho(result, "19-Hello ${'foo.bar}"); + assertEcho(result, "20-Hello #{'foo.bar}"); + assertEcho(result, "21-Hello #{'foo.bar}"); + assertEcho(result, "30-Hello ${'foo}"); + assertEcho(result, "31-Hello ${'foo}"); + assertEcho(result, "32-Hello #{'foo}"); + assertEcho(result, "33-Hello #{'foo}"); + assertEcho(result, "34-Hello ${'foo}"); + assertEcho(result, "35-Hello ${'foo}"); + assertEcho(result, "36-Hello #{'foo}"); + assertEcho(result, "37-Hello #{'foo}"); + assertEcho(result, "40-Hello ${'foo}"); + assertEcho(result, "41-Hello ${'foo}"); + assertEcho(result, "42-Hello #{'foo}"); + assertEcho(result, "43-Hello #{'foo}"); + assertEcho(result, "50-Hello ${'foo}"); + assertEcho(result, "51-Hello ${'foo}"); + assertEcho(result, "52-Hello #{'foo}"); + assertEcho(result, "53-Hello #{'foo}"); + } + + @Test + public void testBug48668b() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug48nnn/bug48668b.jsp"); + String result = res.toString(); + assertEcho(result, "00-Hello world

    #{foo.bar}"); + assertEcho(result, "01-Hello world

    #{foo2"); + } + + @Test + public void testBug49297NoSpaceStrict() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297NoSpaceStrict.jsp", new ByteChunk(), + null); + + Assert.assertEquals(500, sc); + } + + @Test + public void testBug49297DuplicateAttr() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297DuplicateAttr.jsp", new ByteChunk(), + null); + + Assert.assertEquals(500, sc); + } + + @Test + public void testBug49297MultipleImport1() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297MultipleImport1.jsp", res, null); + + Assert.assertEquals(200, sc); + assertEcho(res.toString(), "OK"); + } + + @Test + public void testBug49297MultipleImport2() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297MultipleImport2.jsp", res, null); + + Assert.assertEquals(200, sc); + assertEcho(res.toString(), "OK"); + } + + @Test + public void testBug49297MultiplePageEncoding1() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297MultiplePageEncoding1.jsp", res, + null); + + Assert.assertEquals(500, sc); + } + + @Test + public void testBug49297MultiplePageEncoding2() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297MultiplePageEncoding2.jsp", res, + null); + + Assert.assertEquals(500, sc); + } + + @Test + public void testBug49297MultiplePageEncoding3() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297MultiplePageEncoding3.jsp", res, + null); + + Assert.assertEquals(500, sc); + } + + @Test + public void testBug49297MultiplePageEncoding4() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297MultiplePageEncoding4.jsp", res, + null); + + Assert.assertEquals(500, sc); + } + + @Test + public void testBug49297Tag() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297Tag.jsp", res, null); + + Assert.assertEquals(200, sc); + assertEcho(res.toString(), "OK"); + } + + @Test + public void testBug52335() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug52335.jsp"); + + String result = res.toString(); + // Beware of the differences between escaping in JSP attributes and + // in Java Strings + assertEcho(result, "00 - \\% \\\\% <%"); + assertEcho(result, "01 - <%"); + assertEcho(result, "02 -

    Foo

    <%"); + } + + @Test + public void testBug55198() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug55198.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result, + result.contains(""1foo1<&>"") || result.contains(""1foo1<&>"")); + Assert.assertTrue(result, + result.contains(""2bar2<&>"") || result.contains(""2bar2<&>"")); + Assert.assertTrue(result, result.contains(""3a&b3"") || result.contains(""3a&b3"")); + Assert.assertTrue(result, result.contains(""4&4"") || result.contains(""4&4"")); + Assert.assertTrue(result, result.contains(""5'5"") || result.contains(""5'5"")); + } + + @Test + public void testBug56265() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug56265.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result, result.contains("[1: [data-test]: [window.alert('Hello World <&>!')]]")); + Assert.assertTrue(result, result.contains("[2: [data-test]: [window.alert('Hello World <&>!')]]")); + Assert.assertTrue(result, result.contains("[3: [data-test]: [window.alert('Hello 'World <&>'!')]]")); + Assert.assertTrue(result, result.contains("[4: [data-test]: [window.alert('Hello 'World <&>'!')]]")); + } + + @Test + public void testBug56334And56561() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug56334and56561.jspx"); + + String result = res.toString(); + + // NOTE: The expected values must themselves be \ escaped below + Assert.assertTrue(result, result.contains("01a\\?resize01a")); + Assert.assertTrue(result, result.contains("01b\\\\x\\?resize01b")); + Assert.assertTrue(result, result.contains("")); + Assert.assertTrue(result, result.contains("")); + Assert.assertTrue(result, result.contains("")); + Assert.assertTrue(result, result.contains("")); + Assert.assertTrue(result, result.contains("<04a\\?resize04a/>")); + Assert.assertTrue(result, result.contains("<04b\\\\x\\?resize04b/>")); + Assert.assertTrue(result, result.contains("")); + Assert.assertTrue(result, result.contains("")); + Assert.assertTrue(result, result.contains("")); + Assert.assertTrue(result, result.contains("05x:")); + Assert.assertTrue(result, result.contains("")); + Assert.assertTrue(result, result.contains("07a:")); + Assert.assertTrue(result, result.contains("07b:")); + Assert.assertTrue(result, result.contains("07c:")); + Assert.assertTrue(result, result.contains("07d:")); + Assert.assertTrue(result, result.contains("07e:")); + Assert.assertTrue(result, result.contains("07f:")); + Assert.assertTrue(result, result.contains("07g:")); + } + + /** Assertion for text printed by tags:echo */ + private static void assertEcho(String result, String expected) { + Assert.assertTrue(result.indexOf("

    " + expected + "

    ") > 0); + } +} diff --git a/test/org/apache/jasper/compiler/TestParserNoStrictWhitespace.java b/test/org/apache/jasper/compiler/TestParserNoStrictWhitespace.java new file mode 100644 index 0000000..56c6663 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestParserNoStrictWhitespace.java @@ -0,0 +1,47 @@ +/* + * 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.jasper.compiler; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Tests duplicate those in {@link TestParser} where the strict whitespace parsing is enabled by default. Strict + * whitespace parsing is disabled for these tests in web.xml. + */ +public class TestParserNoStrictWhitespace extends TomcatBaseTest { + + @Test + public void testBug49297NoSpaceNotStrict() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + int sc = getUrl("http://localhost:" + getPort() + "/test/bug49nnn/bug49297NoSpace.jsp", res, null); + + Assert.assertEquals(200, sc); + assertEcho(res.toString(), "Hello World"); + } + + + /** Assertion for text printed by tags:echo */ + private static void assertEcho(String result, String expected) { + Assert.assertTrue(result.indexOf("

    " + expected + "

    ") > 0); + } +} diff --git a/test/org/apache/jasper/compiler/TestScriptingVariabler.java b/test/org/apache/jasper/compiler/TestScriptingVariabler.java new file mode 100644 index 0000000..ca502e5 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestScriptingVariabler.java @@ -0,0 +1,95 @@ +/* + * 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.jasper.compiler; + +import java.io.IOException; + +import jakarta.servlet.jsp.tagext.TagData; +import jakarta.servlet.jsp.tagext.TagExtraInfo; +import jakarta.servlet.jsp.tagext.TagSupport; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestScriptingVariabler extends TomcatBaseTest { + + @Test + public void testBug42390() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + Exception e = null; + try { + getUrl("http://localhost:" + getPort() + "/test/bug42390.jsp"); + } catch (IOException ioe) { + e = ioe; + } + + // Should not fail + Assert.assertNull(e); + } + + public static class Bug48616aTag extends TagSupport { + private static final long serialVersionUID = 1L; + } + + public static class Bug48616bTag extends TagSupport { + private static final long serialVersionUID = 1L; + } + + public static class Bug48616bTei extends TagExtraInfo { + /** + * Return information about the scripting variables to be created. + */ + @Override + public VariableInfo[] getVariableInfo(TagData data) { + return new VariableInfo[] { new VariableInfo("Test", "java.lang.String", true, VariableInfo.AT_END) }; + } + } + + @Test + public void testBug48616() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + Exception e = null; + try { + getUrl("http://localhost:" + getPort() + "/test/bug48nnn/bug48616.jsp"); + } catch (IOException ioe) { + e = ioe; + } + + // Should not fail + Assert.assertNull(e); + } + + @Test + public void testBug48616b() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + Exception e = null; + try { + getUrl("http://localhost:" + getPort() + "/test/bug48nnn/bug48616b.jsp"); + } catch (IOException ioe) { + e = ioe; + } + + // Should not fail + Assert.assertNull(e); + } +} diff --git a/test/org/apache/jasper/compiler/TestSmapStratum.java b/test/org/apache/jasper/compiler/TestSmapStratum.java new file mode 100644 index 0000000..ee3fe75 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestSmapStratum.java @@ -0,0 +1,57 @@ +/* + * 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.jasper.compiler; + +import org.junit.Assert; +import org.junit.Test; + +public class TestSmapStratum { + + @Test + public void test01() { + // Formerly part of the main() method in SmapGenerator + + SmapStratum s = new SmapStratum(); + s.addFile("foo.jsp"); + s.addFile("bar.jsp", "/foo/foo/bar.jsp"); + s.addLineData(1, "foo.jsp", 1, 1, 1); + s.addLineData(2, "foo.jsp", 1, 6, 1); + s.addLineData(3, "foo.jsp", 2, 10, 5); + s.addLineData(20, "/foo/foo/bar.jsp", 1, 30, 1); + s.setOutputFileName("foo.java"); + + //@formatter:off + Assert.assertEquals( + "SMAP\n" + + "foo.java\n" + + "JSP\n" + + "*S JSP\n" + + "*F\n" + + "+ 0 foo.jsp\n" + + "foo.jsp\n" + + "+ 1 bar.jsp\n" + + "foo/foo/bar.jsp\n" + + "*L\n" + + "1:1\n" + + "2:6\n" + + "3,2:10,5\n" + + "20#1:30\n" + + "*E\n", + s.getSmapString()); + //@formatter:on + } +} diff --git a/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java b/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java new file mode 100644 index 0000000..9012262 --- /dev/null +++ b/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java @@ -0,0 +1,56 @@ +/* + * 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.jasper.compiler; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Test case for {@link TagLibraryInfoImpl}. + */ +public class TestTagLibraryInfoImpl extends TomcatBaseTest { + + @Test + public void testRelativeTldLocation() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/test.jsp", res, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=64373 + */ + @Test + public void testTldFromExplodedWar() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug64373.jsp", res, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + +} diff --git a/test/org/apache/jasper/compiler/TestTagPluginManager.java b/test/org/apache/jasper/compiler/TestTagPluginManager.java new file mode 100644 index 0000000..f7eb60f --- /dev/null +++ b/test/org/apache/jasper/compiler/TestTagPluginManager.java @@ -0,0 +1,66 @@ +/* + * 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.jasper.compiler; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.jsp.tagext.TagFileInfo; +import jakarta.servlet.jsp.tagext.TagInfo; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +/** + * Test case for {@link TagPluginManager}. + */ +public class TestTagPluginManager extends TomcatBaseTest { + + private static TagInfo tagInfo = + new TagInfo("ATag", "org.apache.jasper.compiler.ATagSupport", "", "", null, null, null); + + @Test + public void testBug54240() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, true); + + + ServletContext context = ((Context) tomcat.getHost().findChildren()[0]).getServletContext(); + + TagPluginManager manager = new TagPluginManager(context); + + Node.Nodes nodes = new Node.Nodes(); + Node.CustomTag c = new Node.CustomTag("test:ATag", "test", "ATag", "http://tomcat.apache.org/jasper", null, + null, null, null, null, new TagFileInfo("ATag", "http://tomcat.apache.org/jasper", tagInfo)); + c.setTagHandlerClass(TesterTag.class); + nodes.add(c); + manager.apply(nodes, null, null); + + Node n = nodes.getNode(0); + Assert.assertNotNull(n); + + Node.CustomTag t = (Node.CustomTag) n; + Assert.assertNotNull(t.getAtSTag()); + + Node.Nodes sTag = c.getAtSTag(); + Node scriptlet = sTag.getNode(0); + Assert.assertNotNull(scriptlet); + Node.Scriptlet s = (Node.Scriptlet) scriptlet; + Assert.assertEquals("//Just a comment", s.getText()); + } +} diff --git a/test/org/apache/jasper/compiler/TestValidator.java b/test/org/apache/jasper/compiler/TestValidator.java new file mode 100644 index 0000000..aec4efc --- /dev/null +++ b/test/org/apache/jasper/compiler/TestValidator.java @@ -0,0 +1,278 @@ +/* + * 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.jasper.compiler; + +import java.io.File; +import java.io.IOException; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.TagSupport; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestValidator extends TomcatBaseTest { + + @Test + public void testBug47331() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug47331.jsp", new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + @Test + public void testTldVersions22() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-2.2"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    ${'00-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    ${'02-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    ${'04-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    ${'06-hello world'}

    ") > 0); + } + + @Test + public void testTldVersions23() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-2.3"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    ${'00-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    ${'02-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    ${'04-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    ${'06-hello world'}

    ") > 0); + } + + @Test + public void testTldVersions24() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-2.4"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    02-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    04-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    06-hello world

    ") > 0); + } + + @Test + public void testTldVersions25() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-2.5"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    02-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    04-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    06-hello world

    ") > 0); + } + + @Test + public void testTldVersions30() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-3.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    02-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    04-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    06-hello world

    ") > 0); + } + + @Test + public void testTldVersions31() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-3.1"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    02-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    04-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    06-hello world

    ") > 0); + } + + @Test + public void testTldVersions40() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-4.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    02-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    04-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    06-hello world

    ") > 0); + } + + @Test + public void testTldVersions50() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-5.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    02-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    04-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    06-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    07-hello world

    ") > 0); + } + + @Test + public void testTldVersions60() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-6.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/tld-versions.jsp"); + + String result = res.toString(); + + Assert.assertTrue(result.indexOf("

    00-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'01-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    02-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'03-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    04-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    #{'05-hello world'}

    ") > 0); + Assert.assertTrue(result.indexOf("

    06-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    07-hello world

    ") > 0); + Assert.assertTrue(result.indexOf("

    08-hello world

    ") > 0); + } + + public static class Echo extends TagSupport { + + private static final long serialVersionUID = 1L; + + private String echo = null; + + public void setEcho(String echo) { + this.echo = echo; + } + + public String getEcho() { + return echo; + } + + @Override + public int doStartTag() throws JspException { + try { + pageContext.getOut().print("

    " + echo + "

    "); + } catch (IOException e) { + pageContext.getServletContext().log("Tag (Echo21) failure", e); + } + return super.doStartTag(); + } + } +} diff --git a/test/org/apache/jasper/compiler/TesterTag.java b/test/org/apache/jasper/compiler/TesterTag.java new file mode 100644 index 0000000..c77351f --- /dev/null +++ b/test/org/apache/jasper/compiler/TesterTag.java @@ -0,0 +1,27 @@ +/* + * 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.jasper.compiler; + +import jakarta.servlet.jsp.tagext.TagSupport; + +/** + * A tag for test purpose + */ +public class TesterTag extends TagSupport { + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/test/org/apache/jasper/compiler/TesterTagPlugin.java b/test/org/apache/jasper/compiler/TesterTagPlugin.java new file mode 100644 index 0000000..0102e3b --- /dev/null +++ b/test/org/apache/jasper/compiler/TesterTagPlugin.java @@ -0,0 +1,31 @@ +/* + * 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.jasper.compiler; + +import org.apache.jasper.compiler.tagplugin.TagPlugin; +import org.apache.jasper.compiler.tagplugin.TagPluginContext; + +/** + * Plug-in for {@link TesterTag}. + */ +public class TesterTagPlugin implements TagPlugin { + + @Override + public void doTag(TagPluginContext ctxt) { + ctxt.generateJavaSource("//Just a comment"); + } +} \ No newline at end of file diff --git a/test/org/apache/jasper/compiler/TesterValidator.java b/test/org/apache/jasper/compiler/TesterValidator.java new file mode 100644 index 0000000..b6ed4cc --- /dev/null +++ b/test/org/apache/jasper/compiler/TesterValidator.java @@ -0,0 +1,98 @@ +/* + * 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.jasper.compiler; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.security.Escape; + +/** + * Performance tests for {@link Validator}. + */ +public class TesterValidator { + + private static String[] bug53867TestData = new String[] { "Hello World!", "", + "This connection has limited network connectivity.", + "Please use this web page & to access file server resources." }; + + @Test + public void testBug53867() { + for (int i = 0; i < 10; i++) { + doTestBug53867(); + } + } + + private static void doTestBug53867() { + int count = 100000; + + for (String testDatum : bug53867TestData) { + Assert.assertEquals(doTestBug53867OldVersion(testDatum), Escape.xml(testDatum)); + } + + for (int i = 0; i < 100; i++) { + for (String bug53867TestDatum : bug53867TestData) { + doTestBug53867OldVersion(bug53867TestDatum); + } + } + for (int i = 0; i < 100; i++) { + for (String bug53867TestDatum : bug53867TestData) { + Escape.xml(bug53867TestDatum); + } + } + + long start = System.currentTimeMillis(); + for (int i = 0; i < count; i++) { + for (String bug53867TestDatum : bug53867TestData) { + doTestBug53867OldVersion(bug53867TestDatum); + } + } + System.out.println("Old escape:" + (System.currentTimeMillis() - start)); + + start = System.currentTimeMillis(); + for (int i = 0; i < count; i++) { + for (String bug53867TestDatum : bug53867TestData) { + Escape.xml(bug53867TestDatum); + } + } + System.out.println("New escape:" + (System.currentTimeMillis() - start)); + } + + private static String doTestBug53867OldVersion(String s) { + if (s == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '<') { + sb.append("<"); + } else if (c == '>') { + sb.append(">"); + } else if (c == '\'') { + sb.append("'"); // ' + } else if (c == '&') { + sb.append("&"); + } else if (c == '"') { + sb.append("""); // " + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/test/org/apache/jasper/el/TestJasperELResolver.java b/test/org/apache/jasper/el/TestJasperELResolver.java new file mode 100644 index 0000000..ca15a25 --- /dev/null +++ b/test/org/apache/jasper/el/TestJasperELResolver.java @@ -0,0 +1,117 @@ +/* + * 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.jasper.el; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.el.ELContext; +import jakarta.el.ELResolver; +import jakarta.servlet.jsp.el.ImplicitObjectELResolver; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.el.stream.StreamELResolverImpl; +import org.apache.jasper.runtime.JspRuntimeLibrary; + +public class TestJasperELResolver { + + private static final int STANDARD_RESOLVERS_COUNT = 11; + + @Test + public void testConstructorNone() throws Exception { + doTestConstructor(0); + } + + @Test + public void testConstructorOne() throws Exception { + doTestConstructor(1); + } + + @Test + public void testConstructorFive() throws Exception { + doTestConstructor(5); + } + + private void doTestConstructor(int count) throws Exception { + + List list = new ArrayList<>(); + for (int i = 0; i < count; i++) { + list.add(new ImplicitObjectELResolver()); + } + + int adjustedForGraalCount = JspRuntimeLibrary.GRAAL ? count + 1 : count; + + JasperELResolver resolver = + new JasperELResolver(list, new StreamELResolverImpl()); + + Assert.assertEquals(Integer.valueOf(count), + getField("appResolversSize", resolver)); + Assert.assertEquals(STANDARD_RESOLVERS_COUNT + adjustedForGraalCount, + ((ELResolver[])getField("resolvers", resolver)).length); + Assert.assertEquals(Integer.valueOf(STANDARD_RESOLVERS_COUNT + adjustedForGraalCount), + Integer.valueOf(((AtomicInteger) getField("resolversSize", resolver)).get())); + } + + private static Object getField(String name, Object target) + throws NoSuchFieldException, SecurityException, + IllegalArgumentException, IllegalAccessException { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(target); + } + + @Test + public void testGraalResolver() throws Exception { + ELResolver resolver = new JasperELResolver.GraalBeanELResolver(); + ELContext context = new ELContextImpl(resolver); + Assert.assertEquals("foo", resolver.getValue(context, new TestBean(), "foo")); + Assert.assertEquals(Boolean.TRUE, resolver.getValue(context, new TestBean(), "foo2")); + Assert.assertEquals("bla", resolver.getValue(context, new TestBean(), "bla")); + Assert.assertNull(resolver.getValue(context, new TestBean(), "foobar")); + Assert.assertNull(resolver.getValue(context, new TestBean(), "bar")); + Assert.assertFalse(resolver.isReadOnly(context, new TestBean(), "foo")); + Assert.assertTrue(resolver.isReadOnly(context, new TestBean(), "bla")); + } + + public static class TestBean { + public String getFoo() { + return "foo"; + } + public void setFoo(@SuppressWarnings("unused") String foo) { + } + public boolean isFoo2() { + return true; + } + public void setFoo2(@SuppressWarnings("unused") boolean foo) { + } + public String getBar(@SuppressWarnings("unused") boolean i) { + return "bar"; + } + public String isFoobar() { + return "foobar"; + } + public String getBla() { + return "bla"; + } + public void setBla(@SuppressWarnings("unused") Object bla) { + } + } +} diff --git a/test/org/apache/jasper/optimizations/TestELInterpreterTagSetters.java b/test/org/apache/jasper/optimizations/TestELInterpreterTagSetters.java new file mode 100644 index 0000000..1cdf5bd --- /dev/null +++ b/test/org/apache/jasper/optimizations/TestELInterpreterTagSetters.java @@ -0,0 +1,555 @@ +/* + * 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.jasper.optimizations; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.SimpleTagSupport; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.JspCompilationContext; +import org.apache.jasper.compiler.ELInterpreter; +import org.apache.jasper.compiler.ELInterpreterFactory; +import org.apache.jasper.compiler.StringInterpreter; +import org.apache.tomcat.util.buf.ByteChunk; + +@RunWith(Parameterized.class) +public class TestELInterpreterTagSetters extends TomcatBaseTest { + + private static final Integer ZERO = Integer.valueOf(0); + private static final Integer TWO = Integer.valueOf(2); + private static final Integer THREE = Integer.valueOf(3); + + @Parameters(name="{index}: {0} {1} {3},{4}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + ELInterpreter[] elInterpreters = new ELInterpreter[] { + // First call will trigger compilation (and therefore be slower) + // For performance tests call each once to warm-up and once to + // test + new ELInterpreterWrapper(true, "TagSetters"), + new ELInterpreterWrapper(false, "Default"), + // Uncomment for a performance test and compare times of these + // test runs + //new ELInterpreterWrapper(true, "TagSetters"), + //new ELInterpreterWrapper(false, "Default"), + }; + + for (ELInterpreter elInterpreter : elInterpreters) { + parameterSets.add(new Object[] { elInterpreter, "boolean", "false", ZERO, THREE }); + parameterSets.add(new Object[] { elInterpreter, "boolean", "true", THREE, THREE }); + parameterSets.add(new Object[] { elInterpreter, "primitive-boolean", "false", ZERO, THREE }); + parameterSets.add(new Object[] { elInterpreter, "primitive-boolean", "true", THREE, THREE }); + parameterSets.add(new Object[] { elInterpreter, "character", "b", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "primitive-character", "b", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "bigdecimal", "12.34", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "biginteger", "1234", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "long", "1234", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "primitive-long", "1234", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "integer", "1234", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "primitive-integer", "1234", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "short", "1234", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "primitive-short", "1234", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "byte", "12", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "byte", "-12", TWO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "primitive-byte", "12", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "primitive-byte", "-12", TWO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "double", "12.34", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "primitive-double", "12.34", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "float", "12.34", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "primitive-float", "12.34", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "timeunit", "SECONDS", ZERO, TWO }); + parameterSets.add(new Object[] { elInterpreter, "string", "bar", ZERO, TWO }); + } + return parameterSets; + } + + + @Parameter(0) + public ELInterpreter elInterpreter; + @Parameter(1) + public String target; + @Parameter(2) + public String expectedValue; + @Parameter(3) + public int offset; + @Parameter(4) + public int len; + + + @Test + public void testTag() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, true); + Context ctxt = (Context) tomcat.getHost().findChild("/test"); + ctxt.getServletContext().setAttribute(ELInterpreter.class.getCanonicalName(), elInterpreter); + + ctxt.getServletContext().setAttribute(StringInterpreter.class.getCanonicalName(), new StringInterpreterEnum()); + + // Change this to 1000000 to test performance + String iterations = "1"; + + ByteChunk bc = getUrl("http://localhost:" + getPort() + + "/test/bug6nnnn/bug64872-" + target + ".jsp?iterations=" + iterations); + + String actual = bc.toString(); + + for (int i = offset; i < offset + len; i++) { + String expected = String.format("%02d The value of foo is [%s]", Integer.valueOf(i+1), expectedValue); + Assert.assertTrue(actual, actual.contains(expected)); + } + } + + + public static class TagPrimitiveBoolean extends SimpleTagSupport { + + private boolean foo; + + public boolean getFoo() { + return foo; + } + + public void setFoo(boolean foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagBoolean extends SimpleTagSupport { + + private Boolean foo; + + public Boolean getFoo() { + return foo; + } + + public void setFoo(Boolean foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagPrimitiveCharacter extends SimpleTagSupport { + + private char foo; + + public char getFoo() { + return foo; + } + + public void setFoo(char foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagCharacter extends SimpleTagSupport { + + private Character foo; + + public Character getFoo() { + return foo; + } + + public void setFoo(Character foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagPrimitiveLong extends SimpleTagSupport { + + private long foo; + + public long getFoo() { + return foo; + } + + public void setFoo(long foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagLong extends SimpleTagSupport { + + private Long foo; + + public Long getFoo() { + return foo; + } + + public void setFoo(Long foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagPrimitiveInteger extends SimpleTagSupport { + + private int foo; + + public int getFoo() { + return foo; + } + + public void setFoo(int foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagInteger extends SimpleTagSupport { + + private Integer foo; + + public Integer getFoo() { + return foo; + } + + public void setFoo(Integer foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagPrimitiveShort extends SimpleTagSupport { + + private short foo; + + public short getFoo() { + return foo; + } + + public void setFoo(short foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagShort extends SimpleTagSupport { + + private Short foo; + + public Short getFoo() { + return foo; + } + + public void setFoo(Short foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagPrimitiveByte extends SimpleTagSupport { + + private byte foo; + + public byte getFoo() { + return foo; + } + + public void setFoo(byte foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagByte extends SimpleTagSupport { + + private Byte foo; + + public Byte getFoo() { + return foo; + } + + public void setFoo(Byte foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagPrimitiveDouble extends SimpleTagSupport { + + private double foo; + + public double getFoo() { + return foo; + } + + public void setFoo(double foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagDouble extends SimpleTagSupport { + + private Double foo; + + public Double getFoo() { + return foo; + } + + public void setFoo(Double foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagPrimitiveFloat extends SimpleTagSupport { + + private float foo; + + public float getFoo() { + return foo; + } + + public void setFoo(float foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagFloat extends SimpleTagSupport { + + private Float foo; + + public Float getFoo() { + return foo; + } + + public void setFoo(Float foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagString extends SimpleTagSupport { + + private String foo; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagTimeUnit extends SimpleTagSupport { + + private TimeUnit foo; + + public TimeUnit getFoo() { + return foo; + } + + public void setFoo(TimeUnit foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagBigDecimal extends SimpleTagSupport { + + private BigDecimal foo; + + public BigDecimal getFoo() { + return foo; + } + + public void setFoo(BigDecimal foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + public static class TagBigInteger extends SimpleTagSupport { + + private BigInteger foo; + + public BigInteger getFoo() { + return foo; + } + + public void setFoo(BigInteger foo) { + this.foo = foo; + } + + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().print(foo); + } + } + + + /* + * Wrapper so we can use sensible names in the test labels + */ + private static class ELInterpreterWrapper implements ELInterpreter { + + private final boolean optimised; + private final String name; + private volatile ELInterpreter elInterpreter = null; + + ELInterpreterWrapper(boolean optimised, String name) { + this.optimised = optimised; + this.name = name; + } + + @Override + public String interpreterCall(JspCompilationContext context, boolean isTagFile, + String expression, Class expectedType, String fnmapvar) { + return getElInterpreter().interpreterCall(context, isTagFile, expression, expectedType, fnmapvar); + } + + @Override + public String toString() { + return name; + } + + // Lazy init to avoid LogManager init issues when running parameterized tests + private ELInterpreter getElInterpreter() { + if (elInterpreter == null) { + synchronized (this) { + if (elInterpreter == null) { + if (optimised) { + elInterpreter = new ELInterpreterTagSetters(); + } else { + elInterpreter = new ELInterpreterFactory.DefaultELInterpreter(); + } + } + } + } + return elInterpreter; + } + } +} diff --git a/test/org/apache/jasper/optimizations/TestStringInterpreterTagSetters.java b/test/org/apache/jasper/optimizations/TestStringInterpreterTagSetters.java new file mode 100644 index 0000000..ca6db58 --- /dev/null +++ b/test/org/apache/jasper/optimizations/TestStringInterpreterTagSetters.java @@ -0,0 +1,120 @@ +/* + * 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.jasper.optimizations; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.compiler.StringInterpreter; +import org.apache.jasper.compiler.StringInterpreterFactory; +import org.apache.tomcat.util.buf.ByteChunk; + +@RunWith(Parameterized.class) +public class TestStringInterpreterTagSetters extends TomcatBaseTest { + + @Parameters(name="{index}: {0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + StringInterpreter[] stringInterpreters = new StringInterpreter[] { + // Warm-up + // First call will trigger compilation (and therefore be slower) + // Call both to ensure both are warmed up + new StringInterpreterWrapper(true, "Enum"), + new StringInterpreterWrapper(false, "Default"), + // Compare times of these test runs + new StringInterpreterWrapper(true, "Enum"), + new StringInterpreterWrapper(false, "Default"), + }; + + for (StringInterpreter stringInterpreter : stringInterpreters) { + parameterSets.add(new Object[] { stringInterpreter }); + } + return parameterSets; + } + + + @Parameter(0) + public StringInterpreter stringInterpreter; + + @Test + public void testTag() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, true); + Context ctxt = (Context) tomcat.getHost().findChild("/test"); + ctxt.getServletContext().setAttribute(StringInterpreter.class.getCanonicalName(), stringInterpreter); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug64872b-timeunit.jsp"); + + String actual = bc.toString(); + + Assert.assertTrue(actual, actual.contains("01 The value of foo is [SECONDS]")); + } + + + /* + * Wrapper so we can use sensible names in the test labels + */ + private static class StringInterpreterWrapper implements StringInterpreter { + + private final boolean optimised; + private final String name; + private volatile StringInterpreter stringInterpreter = null; + + StringInterpreterWrapper(boolean optimised, String name) { + this.optimised = optimised; + this.name = name; + } + + @Override + public String convertString(Class c, String s, String attrName, Class propEditorClass, + boolean isNamedAttribute) { + return getStringInterpreter().convertString(c, s, attrName, propEditorClass, isNamedAttribute) ; + } + + @Override + public String toString() { + return name; + } + + // Lazy init to avoid LogManager init issues when running parameterized tests + private StringInterpreter getStringInterpreter() { + if (stringInterpreter == null) { + synchronized (this) { + if (stringInterpreter == null) { + if (optimised) { + stringInterpreter = new StringInterpreterEnum(); + } else { + stringInterpreter = new StringInterpreterFactory.DefaultStringInterpreter(); + } + } + } + } + return stringInterpreter; + } + } +} diff --git a/test/org/apache/jasper/runtime/TestCustomHttpJspPage.java b/test/org/apache/jasper/runtime/TestCustomHttpJspPage.java new file mode 100644 index 0000000..ed36870 --- /dev/null +++ b/test/org/apache/jasper/runtime/TestCustomHttpJspPage.java @@ -0,0 +1,62 @@ +/* + * 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.jasper.runtime; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestCustomHttpJspPage extends TomcatBaseTest { + + /* + * Bug 58444 + */ + @Test + public void testCustomBasePageWhenUsingTagFiles01() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk out = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug58444a.jsp", out, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = out.toString(); + + Assert.assertTrue(result, result.contains("00-PASS")); + } + + + @Test + public void testCustomBasePageWhenUsingTagFiles02() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk out = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug58444b.jsp", out, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = out.toString(); + + Assert.assertTrue(result, result.contains("00-PASS")); + } +} diff --git a/test/org/apache/jasper/runtime/TestJspContextWrapper.java b/test/org/apache/jasper/runtime/TestJspContextWrapper.java new file mode 100644 index 0000000..6edd5fe --- /dev/null +++ b/test/org/apache/jasper/runtime/TestJspContextWrapper.java @@ -0,0 +1,85 @@ +/* + * 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.jasper.runtime; + +import java.math.RoundingMode; +import java.util.Collections; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestJspContextWrapper extends TomcatBaseTest { + + @Test + public void testELTagFilePageContext() throws Exception { + getTomcatInstanceTestWebapp(true, true); + + ByteChunk out = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug58178.jsp", out, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = out.toString(); + + Assert.assertTrue(result, result.contains("PASS")); + } + + @Test + public void testELTagFileImports() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk out = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug58178b.jsp", out, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = out.toString(); + + Assert.assertTrue(result, result.contains("00-" + DispatcherType.ASYNC)); + // No obvious status fields for jakarta.servlet.http + // Could hack something with HttpUtils... + // No obvious status fields for jakarta.servlet.jsp + // Wild card (package) import + Assert.assertTrue(result, result.contains("01-" + RoundingMode.HALF_UP)); + // Class import + Assert.assertTrue(result, result.contains("02-" + Collections.EMPTY_LIST.size())); + } + + @Test + public void testELTagFileELContextListener() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk out = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug58178c.jsp", out, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String result = out.toString(); + + Assert.assertTrue(result, result.contains("JSP count: 1")); + Assert.assertTrue(result, result.contains("Tag count: 1")); + } +} diff --git a/test/org/apache/jasper/runtime/TestJspRuntimeLibrary.java b/test/org/apache/jasper/runtime/TestJspRuntimeLibrary.java new file mode 100644 index 0000000..7148e86 --- /dev/null +++ b/test/org/apache/jasper/runtime/TestJspRuntimeLibrary.java @@ -0,0 +1,143 @@ +/* + * 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.jasper.runtime; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestJspRuntimeLibrary extends TomcatBaseTest { + + /* + * Tests successful conversions + */ + @Test + public void testBug63359a() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug6nnnn/bug63359a.jsp"); + String result = res.toString(); + + assertEcho(result, "01-false"); + assertEcho(result, "02-false"); + assertEcho(result, "03-true"); + assertEcho(result, "04-true"); + assertEcho(result, "05-false"); + + assertEcho(result, "11-false"); + assertEcho(result, "12-false"); + assertEcho(result, "13-true"); + assertEcho(result, "14-true"); + assertEcho(result, "15-false"); + + assertEcho(result, "21-0"); + assertEcho(result, "22-42"); + assertEcho(result, "23--42"); + assertEcho(result, "24-42"); + + assertEcho(result, "31-0"); + assertEcho(result, "32-42"); + assertEcho(result, "33--42"); + assertEcho(result, "34-42"); + + assertEcho(result, "41-\u0000"); + assertEcho(result, "42-f"); + assertEcho(result, "43-b"); + assertEcho(result, "44-" + System.lineSeparator().charAt(0)); + + assertEcho(result, "51-\u0000"); + assertEcho(result, "52-f"); + assertEcho(result, "53-b"); + assertEcho(result, "54-" + System.lineSeparator().charAt(0)); + + assertEcho(result, "61-0.0"); + assertEcho(result, "62-42.0"); + assertEcho(result, "63--42.0"); + assertEcho(result, "64-42.0"); + + assertEcho(result, "71-0.0"); + assertEcho(result, "72-42.0"); + assertEcho(result, "73--42.0"); + assertEcho(result, "74-42.0"); + + assertEcho(result, "81-0"); + assertEcho(result, "82-42"); + assertEcho(result, "83--42"); + assertEcho(result, "84-42"); + + assertEcho(result, "91-0"); + assertEcho(result, "92-42"); + assertEcho(result, "93--42"); + assertEcho(result, "94-42"); + + assertEcho(result, "101-0.0"); + assertEcho(result, "102-42.0"); + assertEcho(result, "103--42.0"); + assertEcho(result, "104-42.0"); + + assertEcho(result, "111-0.0"); + assertEcho(result, "112-42.0"); + assertEcho(result, "113--42.0"); + assertEcho(result, "114-42.0"); + + assertEcho(result, "121-0"); + assertEcho(result, "122-42"); + assertEcho(result, "123--42"); + assertEcho(result, "124-42"); + + assertEcho(result, "131-0"); + assertEcho(result, "132-42"); + assertEcho(result, "133--42"); + assertEcho(result, "134-42"); + + assertEcho(result, "141-0"); + assertEcho(result, "142-42"); + assertEcho(result, "143--42"); + assertEcho(result, "144-42"); + + assertEcho(result, "151-0"); + assertEcho(result, "152-42"); + assertEcho(result, "153--42"); + assertEcho(result, "154-42"); + + assertEcho(result, "161-"); + assertEcho(result, "162-42"); + assertEcho(result, "163--42"); + assertEcho(result, "164-+42"); + + assertEcho(result, "171-"); + assertEcho(result, "172-42"); + assertEcho(result, "173--42"); + assertEcho(result, "174-+42"); + + assertEcho(result, "181-"); + assertEcho(result, "182-42"); + assertEcho(result, "183--42"); + assertEcho(result, "184-42"); + + // NB In EL null coerces to the empty String + assertEcho(result, "191-"); + } + + + // Assertion for text contained with

    , e.g. printed by tags:echo + private static void assertEcho(String result, String expected) { + Assert.assertTrue(result, result.indexOf("

    " + expected + "

    ") > 0); + } +} diff --git a/test/org/apache/jasper/runtime/TestJspWriterImpl.java b/test/org/apache/jasper/runtime/TestJspWriterImpl.java new file mode 100644 index 0000000..c8b5c78 --- /dev/null +++ b/test/org/apache/jasper/runtime/TestJspWriterImpl.java @@ -0,0 +1,57 @@ +/* + * 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.jasper.runtime; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestJspWriterImpl extends TomcatBaseTest { + + @Test + public void bug54241a() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug54241a.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("01: null")); + Assert.assertTrue(body.contains("02: null")); + } + + @Test + public void bug54241b() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug54241b.jsp", res, null); + + Assert.assertEquals(res.toString(), + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } +} diff --git a/test/org/apache/jasper/runtime/TestPageContextImpl.java b/test/org/apache/jasper/runtime/TestPageContextImpl.java new file mode 100644 index 0000000..ee326ec --- /dev/null +++ b/test/org/apache/jasper/runtime/TestPageContextImpl.java @@ -0,0 +1,140 @@ +/* + * 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.jasper.runtime; + +import java.io.File; +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.JspFactory; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.PageContext; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.Constants; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestPageContextImpl extends TomcatBaseTest { + + @Test + public void testDoForward() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug53545.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK")); + Assert.assertFalse(body.contains("FAIL")); + } + + @Test + public void testDefaultBufferSize() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + // app dir is relative to server home + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + // Add the Servlet + Tomcat.addServlet(ctx, "bug56010", new Bug56010()); + ctx.addServletMappingDecoded("/bug56010", "bug56010"); + + tomcat.start(); + + ByteChunk res = getUrl("http://localhost:" + getPort() + "/test/bug56010"); + + String result = res.toString(); + Assert.assertTrue(result.contains("OK")); + } + + @Test + public void testIncludeThrowsIOException() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/jsp/pageContext1.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK")); + Assert.assertFalse(body.contains("FAILED")); + + res = new ByteChunk(); + + rc = getUrl("http://localhost:" + getPort() + "/test/jsp/pageContext1.jsp?flush=true", res, + null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + body = res.toString(); + Assert.assertTrue(body.contains("Flush")); + Assert.assertTrue(body.contains("OK")); + Assert.assertFalse(body.contains("FAILED")); + } + + public static class Bug56010 extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + PageContext pageContext = JspFactory.getDefaultFactory().getPageContext( + this, req, resp, null, false, JspWriter.DEFAULT_BUFFER, true); + JspWriter out = pageContext.getOut(); + if (Constants.DEFAULT_BUFFER_SIZE == out.getBufferSize()) { + resp.getWriter().println("OK"); + } else { + resp.getWriter().println("FAIL"); + } + } + + } + + + @Test + public void testIncludeThenForward() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/jsp/includeThenForward.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK")); + Assert.assertFalse(body.contains("FAIL")); + } +} diff --git a/test/org/apache/jasper/runtime/TesterBean.java b/test/org/apache/jasper/runtime/TesterBean.java new file mode 100644 index 0000000..e8e07e4 --- /dev/null +++ b/test/org/apache/jasper/runtime/TesterBean.java @@ -0,0 +1,278 @@ +/* + * 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.jasper.runtime; + +public class TesterBean { + + private boolean booleanPrimitive; + private Boolean booleanObject; + + private byte bytePrimitive; + private Byte byteObject; + + private char charPrimitive; + private Character charObject; + + private double doublePrimitive; + private Double doubleObject; + + private int intPrimitive; + private Integer intObject; + + private float floatPrimitive; + private Float floatObject; + + private long longPrimitive; + private Long longObject; + + private short shortPrimitive; + private Short shortObject; + + private String stringValue; + + private Object objectValue; + + private TesterTypeA testerTypeA; + + private TesterTypeB testerTypeB; + + + public boolean getBooleanPrimitive() { + return booleanPrimitive; + } + + + public void setBooleanPrimitive(boolean booleanPrimitive) { + this.booleanPrimitive = booleanPrimitive; + } + + + public Boolean getBooleanObject() { + return booleanObject; + } + + + public void setBooleanObject(Boolean booleanObject) { + this.booleanObject = booleanObject; + } + + + public byte getBytePrimitive() { + return bytePrimitive; + } + + + public void setBytePrimitive(byte bytePrimitive) { + this.bytePrimitive = bytePrimitive; + } + + + public Byte getByteObject() { + return byteObject; + } + + + public void setByteObject(Byte byteObject) { + this.byteObject = byteObject; + } + + + public char getCharPrimitive() { + return charPrimitive; + } + + + public void setCharPrimitive(char charPrimitive) { + this.charPrimitive = charPrimitive; + } + + + public Character getCharObject() { + return charObject; + } + + + public void setCharObject(Character charObject) { + this.charObject = charObject; + } + + + public double getDoublePrimitive() { + return doublePrimitive; + } + + + public void setDoublePrimitive(double doublePrimitive) { + this.doublePrimitive = doublePrimitive; + } + + + public Double getDoubleObject() { + return doubleObject; + } + + + public void setDoubleObject(Double doubleObject) { + this.doubleObject = doubleObject; + } + + + public int getIntPrimitive() { + return intPrimitive; + } + + + public void setIntPrimitive(int intPrimitive) { + this.intPrimitive = intPrimitive; + } + + + public Integer getIntObject() { + return intObject; + } + + + public void setIntObject(Integer intObject) { + this.intObject = intObject; + } + + + public float getFloatPrimitive() { + return floatPrimitive; + } + + + public void setFloatPrimitive(float floatPrimitive) { + this.floatPrimitive = floatPrimitive; + } + + + public Float getFloatObject() { + return floatObject; + } + + + public void setFloatObject(Float floatObject) { + this.floatObject = floatObject; + } + + + public long getLongPrimitive() { + return longPrimitive; + } + + + public void setLongPrimitive(long longPrimitive) { + this.longPrimitive = longPrimitive; + } + + + public Long getLongObject() { + return longObject; + } + + + public void setLongObject(Long longObject) { + this.longObject = longObject; + } + + + public short getShortPrimitive() { + return shortPrimitive; + } + + + public void setShortPrimitive(short shortPrimitive) { + this.shortPrimitive = shortPrimitive; + } + + + public Short getShortObject() { + return shortObject; + } + + + public void setShortObject(Short shortObject) { + this.shortObject = shortObject; + } + + + public String getStringValue() { + return stringValue; + } + + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + + public Object getObjectValue() { + return objectValue; + } + + + public void setObjectValue(Object objectValue) { + this.objectValue = objectValue; + } + + + public TesterTypeA getTesterTypeA() { + return testerTypeA; + } + + + public void setTesterTypeA(TesterTypeA testerTypeA) { + this.testerTypeA = testerTypeA; + } + + + public TesterTypeB getTesterTypeB() { + return testerTypeB; + } + + + public void setTesterTypeB(TesterTypeB testerTypeB) { + this.testerTypeB = testerTypeB; + } + + + public static class Inner { + private String data; + + + public String getData() { + return data; + } + + + public void setData(String data) { + this.data = data; + } + } + + + @SuppressWarnings("unused") + private static class Inner2 { + Inner2() { + } + } + + + public abstract static class Inner4 { + } +} diff --git a/test/org/apache/jasper/runtime/TesterHttpJspBase.java b/test/org/apache/jasper/runtime/TesterHttpJspBase.java new file mode 100644 index 0000000..c78aa07 --- /dev/null +++ b/test/org/apache/jasper/runtime/TesterHttpJspBase.java @@ -0,0 +1,70 @@ +/* + * 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.jasper.runtime; + +import java.io.IOException; + +import jakarta.servlet.GenericServlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.HttpJspPage; + +public abstract class TesterHttpJspBase extends GenericServlet implements HttpJspPage { + + private static final long serialVersionUID = 1L; + + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + _jspService((HttpServletRequest) req, (HttpServletResponse) res); + } + + + @Override + public abstract void _jspService(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException; + + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + jspInit(); + } + + + @Override + public void jspInit() { + // NO-OP by default + } + + + @Override + public void destroy() { + super.destroy(); + jspDestroy(); + } + + + @Override + public void jspDestroy() { + // NO-OP by default + } +} diff --git a/test/org/apache/jasper/runtime/TesterTagHandlerPoolPerformance.java b/test/org/apache/jasper/runtime/TesterTagHandlerPoolPerformance.java new file mode 100644 index 0000000..5c3b556 --- /dev/null +++ b/test/org/apache/jasper/runtime/TesterTagHandlerPoolPerformance.java @@ -0,0 +1,91 @@ +/* + * 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.jasper.runtime; + +import java.util.function.IntConsumer; +import java.util.function.Supplier; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.Tag; + +import org.junit.Test; + +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.unittest.TesterThreadedPerformance; +import org.apache.tomcat.unittest.tags.Bug53545; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterTagHandlerPoolPerformance extends TomcatBaseTest { + + @Test + public void testConcurrency() throws Exception { + // Create a working TagHandlerPool + Tomcat tomcat = getTomcatInstanceTestWebapp(false, true); + + Wrapper w = (Wrapper) tomcat.getHost().findChildren()[0].findChild("jsp"); + TagHandlerPool tagHandlerPool = new TagHandlerPool(); + tagHandlerPool.init(w.getServlet().getServletConfig()); + + for (int i = 1; i < 9; i++) { + TesterThreadedPerformance test = new TesterThreadedPerformance( + i, 5000000, new TestInstanceSupplier(tagHandlerPool)); + long duration = test.doTest(); + System.out.println(i + " threads completed in " + duration + "ns"); + } + } + + + private static class TestInstanceSupplier implements Supplier { + + private final TagHandlerPool tagHandlerPool; + + TestInstanceSupplier(TagHandlerPool tagHandlerPool) { + this.tagHandlerPool = tagHandlerPool; + } + + + @Override + public IntConsumer get() { + return new TestInstance(tagHandlerPool); + } + } + + + private static class TestInstance implements IntConsumer { + + private final TagHandlerPool tagHandlerPool; + + TestInstance(TagHandlerPool tagHandlerPool) { + this.tagHandlerPool = tagHandlerPool; + } + + @Override + public void accept(int value) { + try { + Tag t = tagHandlerPool.get(Bug53545.class); + tagHandlerPool.reuse(t); + } catch (JspException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/test/org/apache/jasper/runtime/TesterTypeA.java b/test/org/apache/jasper/runtime/TesterTypeA.java new file mode 100644 index 0000000..cd933da --- /dev/null +++ b/test/org/apache/jasper/runtime/TesterTypeA.java @@ -0,0 +1,42 @@ +/* + * 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.jasper.runtime; + +public class TesterTypeA { + + private Integer data; + + + public Integer getData() { + return data; + } + + + public void setData(Integer data) { + this.data = data; + } + + + @Override + public String toString() { + if (data == null) { + return ""; + } else { + return data.toString(); + } + } +} diff --git a/test/org/apache/jasper/runtime/TesterTypeAEditor.java b/test/org/apache/jasper/runtime/TesterTypeAEditor.java new file mode 100644 index 0000000..3d5a0fc --- /dev/null +++ b/test/org/apache/jasper/runtime/TesterTypeAEditor.java @@ -0,0 +1,30 @@ +/* + * 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.jasper.runtime; + +import java.beans.PropertyEditorSupport; + +public class TesterTypeAEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + TesterTypeA value = new TesterTypeA(); + // Zero length values deliberately trigger an exception + value.setData(Integer.valueOf(text)); + setValue(value); + } +} diff --git a/test/org/apache/jasper/runtime/TesterTypeB.java b/test/org/apache/jasper/runtime/TesterTypeB.java new file mode 100644 index 0000000..3900a0c --- /dev/null +++ b/test/org/apache/jasper/runtime/TesterTypeB.java @@ -0,0 +1,25 @@ +/* + * 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.jasper.runtime; + +public class TesterTypeB { + + @Override + public String toString() { + return "TesterTypeB"; + } +} diff --git a/test/org/apache/jasper/servlet/TestJasperInitializer.java b/test/org/apache/jasper/servlet/TestJasperInitializer.java new file mode 100644 index 0000000..4b30aaa --- /dev/null +++ b/test/org/apache/jasper/servlet/TestJasperInitializer.java @@ -0,0 +1,71 @@ +/* + * 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.jasper.servlet; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.jsp.JspFactory; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.jasper.Constants; +import org.apache.jasper.runtime.JspFactoryImpl; +import org.apache.tomcat.unittest.TesterServletContext; + +public class TestJasperInitializer { + + @Test + public void testPoolSize() throws Exception { + + final AtomicInteger actualPoolSize = new AtomicInteger(-1); + final JspFactoryImpl defaultFactory = new JspFactoryImpl() { + @Override + public void setPoolSize(int poolSize) { + actualPoolSize.set(poolSize); + super.setPoolSize(poolSize); + } + }; + + JspFactory.setDefaultFactory(defaultFactory); + new JasperInitializer().onStartup(Collections.emptySet(), new TesterServletContext(){ + @Override + public void setAttribute(String name, Object object) { + // ignore + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return null; + } + + @Override + public String getInitParameter(String name) { + if (Constants.JSP_FACTORY_POOL_SIZE_INIT_PARAM.equals(name)) { + return "3"; + } + return super.getInitParameter(name); + } + }); + + // Default value is 8 in JasperInitializer but we have overridden it + Assert.assertEquals(3, actualPoolSize.get()); + } + +} diff --git a/test/org/apache/jasper/servlet/TestJspCServletContext.java b/test/org/apache/jasper/servlet/TestJspCServletContext.java new file mode 100644 index 0000000..cc0032e --- /dev/null +++ b/test/org/apache/jasper/servlet/TestJspCServletContext.java @@ -0,0 +1,181 @@ +/* + * 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.jasper.servlet; + +import java.io.File; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.descriptor.JspPropertyGroupDescriptor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestJspCServletContext { + + @Test + public void testWebapp() throws Exception { + File appDir = new File("test/webapp"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(6, context.getEffectiveMajorVersion()); + Assert.assertEquals(0, context.getEffectiveMinorVersion()); + JspConfigDescriptor jspConfigDescriptor = + context.getJspConfigDescriptor(); + Assert.assertTrue(jspConfigDescriptor.getTaglibs().isEmpty()); + Collection propertyGroups = + jspConfigDescriptor.getJspPropertyGroups(); + Assert.assertEquals(6, propertyGroups.size()); + Iterator groupIterator = + propertyGroups.iterator(); + JspPropertyGroupDescriptor groupDescriptor; + + groupDescriptor = groupIterator.next(); + Assert.assertEquals("text/plain", + groupDescriptor.getDefaultContentType()); + Collection urlPatterns =groupDescriptor.getUrlPatterns(); + Assert.assertEquals(2, urlPatterns.size()); + Iterator iterator = urlPatterns.iterator(); + Assert.assertEquals("/bug49nnn/bug49726a.jsp", iterator.next()); + Assert.assertEquals("/bug49nnn/bug49726b.jsp", iterator.next()); + + groupDescriptor = groupIterator.next(); + Assert.assertEquals(2, groupDescriptor.getIncludePreludes().size()); + Assert.assertEquals(2, groupDescriptor.getIncludeCodas().size()); + } + + @Test + public void testWebapp_2_2() throws Exception { + File appDir = new File("test/webapp-2.2"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(2, context.getEffectiveMajorVersion()); + Assert.assertEquals(2, context.getEffectiveMinorVersion()); + } + + @Test + public void testWebapp_2_3() throws Exception { + File appDir = new File("test/webapp-2.3"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(2, context.getEffectiveMajorVersion()); + Assert.assertEquals(3, context.getEffectiveMinorVersion()); + } + + @Test + public void testWebapp_2_4() throws Exception { + File appDir = new File("test/webapp-2.4"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(2, context.getEffectiveMajorVersion()); + Assert.assertEquals(4, context.getEffectiveMinorVersion()); + } + + @Test + public void testWebapp_2_5() throws Exception { + File appDir = new File("test/webapp-2.5"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(2, context.getEffectiveMajorVersion()); + Assert.assertEquals(5, context.getEffectiveMinorVersion()); + } + + @Test + public void testWebapp_3_0() throws Exception { + File appDir = new File("test/webapp-3.0"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(3, context.getEffectiveMajorVersion()); + Assert.assertEquals(0, context.getEffectiveMinorVersion()); + } + + @Test + public void testWebapp_3_1() throws Exception { + File appDir = new File("test/webapp-3.1"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(3, context.getEffectiveMajorVersion()); + Assert.assertEquals(1, context.getEffectiveMinorVersion()); + } + + @Test + public void testWebapp_4_0() throws Exception { + File appDir = new File("test/webapp-4.0"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(4, context.getEffectiveMajorVersion()); + Assert.assertEquals(0, context.getEffectiveMinorVersion()); + } + + @Test + public void testWebapp_5_0() throws Exception { + File appDir = new File("test/webapp-5.0"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(5, context.getEffectiveMajorVersion()); + Assert.assertEquals(0, context.getEffectiveMinorVersion()); + } + + + @Test + public void testWebapp_6_0() throws Exception { + File appDir = new File("test/webapp-6.0"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(6, context.getEffectiveMajorVersion()); + Assert.assertEquals(0, context.getEffectiveMinorVersion()); + } + + + @Test + public void testWebresources() throws Exception { + File appDir = new File("test/webresources/dir1"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + Assert.assertEquals(6, context.getEffectiveMajorVersion()); + Assert.assertEquals(0, context.getEffectiveMinorVersion()); + } + + + @Test + public void testResourceJARs() throws Exception { + File appDir = new File("test/webapp-fragments"); + JspCServletContext context = new JspCServletContext( + null, appDir.toURI().toURL(), null, false, false); + + Set paths = context.getResourcePaths("/"); + Assert.assertEquals(10, paths.size()); + Assert.assertTrue(paths.contains("/WEB-INF/")); + Assert.assertTrue(paths.contains("/folder/")); + Assert.assertTrue(paths.contains("/'singlequote.jsp")); + Assert.assertTrue(paths.contains("/'singlequote2.jsp")); + Assert.assertTrue(paths.contains("/bug51396.jsp")); + Assert.assertTrue(paths.contains("/jndi.jsp")); + Assert.assertTrue(paths.contains("/resourceA.jsp")); + Assert.assertTrue(paths.contains("/resourceB.jsp")); + Assert.assertTrue(paths.contains("/resourceF.jsp")); + Assert.assertTrue(paths.contains("/warDirContext.jsp")); + + paths = context.getResourcePaths("/folder/"); + Assert.assertEquals(3, paths.size()); + Assert.assertTrue(paths.contains("/folder/resourceC.jsp")); + Assert.assertTrue(paths.contains("/folder/resourceD.jsp")); + Assert.assertTrue(paths.contains("/folder/resourceE.jsp")); + } +} diff --git a/test/org/apache/jasper/servlet/TestJspServlet.java b/test/org/apache/jasper/servlet/TestJspServlet.java new file mode 100644 index 0000000..bb77086 --- /dev/null +++ b/test/org/apache/jasper/servlet/TestJspServlet.java @@ -0,0 +1,109 @@ +/* + * 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.jasper.servlet; + +import java.io.File; +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.ErrorPage; + +public class TestJspServlet extends TomcatBaseTest { + + @Test + public void testBug56568a() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // Use the test web application so JSP support is available and the + // default JSP error page can be used. + File appDir = new File("test/webapp"); + Context context = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + // Create a servlet that always throws an exception for a PUT request + Tomcat.addServlet(context, "Bug56568Servlet", new Bug56568aServlet()); + context.addServletMappingDecoded("/bug56568", "Bug56568Servlet"); + + // Configure a JSP page to handle the 500 error response + // The JSP page will see the same method as the original request (PUT) + // PUT requests are normally blocked for JSPs + ErrorPage ep = new ErrorPage(); + ep.setErrorCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + ep.setLocation("/jsp/error.jsp"); + context.addErrorPage(ep); + + tomcat.start(); + + // When using JaCoCo, the CI system seems to need a longer timeout + int rc = methodUrl("http://localhost:" + getPort() + "/test/bug56568", + new ByteChunk(), 30000, null, null, "PUT"); + + // Make sure we get the original 500 response and not a 405 response + // which would indicate that error.jsp is complaining about being called + // with the PUT method. + Assert.assertEquals(500, rc); + } + + + @Test + public void testBug56568b() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + int rc = methodUrl("http://localhost:" + getPort() + "/test/jsp/error.jsp", + new ByteChunk(), 500000, null, null, "PUT"); + + // Make sure we get a 200 response and not a 405 response + // which would indicate that error.jsp is complaining about being called + // with the PUT method. + Assert.assertEquals(200, rc); + } + + + @Test + public void testBug56568c() throws Exception { + getTomcatInstanceTestWebapp(false, true); + + int rc = methodUrl("http://localhost:" + getPort() + "/test/jsp/test.jsp", + new ByteChunk(), 500000, null, null, "PUT"); + + // Make sure we get a 405 response which indicates that test.jsp is + // complaining about being called with the PUT method. + Assert.assertEquals(405, rc); + } + + + private static class Bug56568aServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + throw new ServletException(); + } + } +} diff --git a/test/org/apache/jasper/servlet/TestTldScanner.java b/test/org/apache/jasper/servlet/TestTldScanner.java new file mode 100644 index 0000000..ce9727e --- /dev/null +++ b/test/org/apache/jasper/servlet/TestTldScanner.java @@ -0,0 +1,125 @@ +/* + * 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.jasper.servlet; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.scan.JarFactory; +import org.apache.tomcat.util.scan.StandardJarScanner; +import org.easymock.EasyMock; + +public class TestTldScanner extends TomcatBaseTest { + + @Test + public void testWithWebapp() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File appDir = new File("test/webapp-3.0"); + Context context = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + tomcat.start(); + + TldScanner scanner = + new TldScanner(context.getServletContext(), true, true, true); + scanner.scan(); + Assert.assertEquals(5, scanner.getUriTldResourcePathMap().size()); + Assert.assertEquals(1, scanner.getListeners().size()); + } + + + @Test + public void testBug55807() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context context = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + ((StandardJarScanner) context.getJarScanner()).setScanAllDirectories(true); + tomcat.start(); + + ByteChunk res = new ByteChunk(); + Map> headers = new HashMap<>(); + + getUrl("http://localhost:" + getPort() + "/test/bug5nnnn/bug55807.jsp", + res, headers); + + // Check request completed + String result = res.toString(); + assertEcho(result, "OK"); + + // Check the dependencies count + Assert.assertTrue(result.contains("

    DependenciesCount: 1

    ")); + + // Check the right timestamp was used in the dependency + File tld = new File("test/webapp/WEB-INF/classes/META-INF/bug55807.tld"); + String expected = "

    /WEB-INF/classes/META-INF/bug55807.tld : " + + tld.lastModified() + "

    "; + Assert.assertTrue(result.contains(expected)); + + + // Check content type + String contentType = getSingleHeader("Content-Type", headers); + Assert.assertTrue(contentType.startsWith("text/html")); + } + + + /** Assertion for text printed by tags:echo */ + private static void assertEcho(String result, String expected) { + Assert.assertTrue(result, result.indexOf("

    " + expected + "

    ") > 0); + } + + @Test + public void testBug57647() throws Exception { + TldScanner scanner = EasyMock.createMock(TldScanner.class); + Field f = TldScanner.class.getDeclaredField("log"); + f.setAccessible(true); + f.set(scanner, LogFactory.getLog(TldScanner.class)); + Constructor constructor = + TldScanner.TldScannerCallback.class.getDeclaredConstructor(TldScanner.class); + constructor.setAccessible(true); + TldScanner.TldScannerCallback callback = constructor.newInstance(scanner); + + File webappDir = new File("webapps/examples"); + Assert.assertFalse(callback.scanFoundNoTLDs()); + scan(callback, webappDir, "WEB-INF/lib/taglibs-standard-spec-1.2.5-migrated-0.0.1.jar"); + Assert.assertTrue(callback.scanFoundNoTLDs()); + scan(callback, webappDir, "WEB-INF/lib/taglibs-standard-impl-1.2.5-migrated-0.0.1.jar"); + Assert.assertTrue(callback.scanFoundNoTLDs()); + } + + private static void scan(TldScanner.TldScannerCallback callback, File webapp, String path) + throws Exception { + String fullPath = new File(webapp, path).toURI().toString(); + URL jarUrl = new URL("jar:" + fullPath + "!/"); + try (Jar jar = JarFactory.newInstance(jarUrl)) { + callback.scan(jar, path, true); + } + } +} + diff --git a/test/org/apache/jasper/tagplugins/jstl/core/AbstractTestTag.java b/test/org/apache/jasper/tagplugins/jstl/core/AbstractTestTag.java new file mode 100644 index 0000000..1599348 --- /dev/null +++ b/test/org/apache/jasper/tagplugins/jstl/core/AbstractTestTag.java @@ -0,0 +1,59 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Before; + +import org.apache.catalina.Context; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.webresources.StandardRoot; + +public abstract class AbstractTestTag extends TomcatBaseTest { + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + ctx.setResources(new StandardRoot(ctx)); + + // Add the JSTL (we need the TLD) + File lib = new File("webapps/examples/WEB-INF/lib"); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/lib", + lib.getAbsolutePath(), null, "/"); + + // Configure the use of the plug-in rather than the standard impl + File plugin = new File( + "java/org/apache/jasper/tagplugins/jstl/tagPlugins.xml"); + Assert.assertTrue(plugin.isFile()); + ctx.getResources().createWebResourceSet( + WebResourceRoot.ResourceSetType.POST, "/WEB-INF/tagPlugins.xml", + plugin.getAbsolutePath(), null, "/"); + + tomcat.start(); + } +} diff --git a/test/org/apache/jasper/tagplugins/jstl/core/TestForEach.java b/test/org/apache/jasper/tagplugins/jstl/core/TestForEach.java new file mode 100644 index 0000000..e9acb94 --- /dev/null +++ b/test/org/apache/jasper/tagplugins/jstl/core/TestForEach.java @@ -0,0 +1,58 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestForEach extends AbstractTestTag { + + @Test + public void testBug54242() throws Exception { + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug54242.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK - 1")); + Assert.assertTrue(body.contains("OK - 2")); + Assert.assertFalse(body.contains("FAIL")); + } + + + @Test + public void testBug54888() throws Exception { + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug54888.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK - 1")); + Assert.assertTrue(body.contains("OK - 2")); + Assert.assertTrue(body.contains("OK - 3")); + } +} diff --git a/test/org/apache/jasper/tagplugins/jstl/core/TestOut.java b/test/org/apache/jasper/tagplugins/jstl/core/TestOut.java new file mode 100644 index 0000000..f578928 --- /dev/null +++ b/test/org/apache/jasper/tagplugins/jstl/core/TestOut.java @@ -0,0 +1,59 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestOut extends AbstractTestTag { + + @Test + public void testBug54011() throws Exception { + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug54011.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK - 1")); + Assert.assertTrue(body.contains("OK - 2")); + } + + + @Test + public void testBug54144() throws Exception { + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug54144.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK - 1")); + Assert.assertTrue(body.contains("OK - 2")); + Assert.assertTrue(body.contains("OK - 3")); + Assert.assertTrue(body.contains("OK - 4")); + Assert.assertFalse(body.contains("FAIL")); + } +} diff --git a/test/org/apache/jasper/tagplugins/jstl/core/TestSet.java b/test/org/apache/jasper/tagplugins/jstl/core/TestSet.java new file mode 100644 index 0000000..07e57e7 --- /dev/null +++ b/test/org/apache/jasper/tagplugins/jstl/core/TestSet.java @@ -0,0 +1,54 @@ +/* + * 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.jasper.tagplugins.jstl.core; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestSet extends AbstractTestTag { + + @Test + public void testBug54011() throws Exception { + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug54012.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK")); + } + + + @Test + public void testBug54338() throws Exception { + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug54338.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertTrue(body.contains("OK - 42")); + } +} diff --git a/test/org/apache/jasper/util/TestFastRemovalDequeue.java b/test/org/apache/jasper/util/TestFastRemovalDequeue.java new file mode 100644 index 0000000..cf676ae --- /dev/null +++ b/test/org/apache/jasper/util/TestFastRemovalDequeue.java @@ -0,0 +1,197 @@ +/* + * 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.jasper.util; + +import org.junit.Assert; +import org.junit.Test; + + +public class TestFastRemovalDequeue { + + @Test + public void testSinglePushPop() throws Exception { + FastRemovalDequeue q = new FastRemovalDequeue<>(2); + + Object o1 = new Object(); + + q.push(o1); + + Object r = q.pop(); + + Assert.assertEquals(o1, r); + Assert.assertNull(q.first); + Assert.assertNull(q.last); + } + + + @Test + public void testDoublePushPop() throws Exception { + FastRemovalDequeue q = new FastRemovalDequeue<>(2); + + Object o1 = new Object(); + Object o2 = new Object(); + + q.push(o1); + q.push(o2); + + Assert.assertEquals(o2, q.first.getContent()); + Assert.assertEquals(o1, q.last.getContent()); + + Object r1 = q.pop(); + + Assert.assertEquals(o1, r1); + Assert.assertEquals(o2, q.first.getContent()); + Assert.assertEquals(o2, q.last.getContent()); + + + Object r2 = q.pop(); + Assert.assertEquals(o2, r2); + Assert.assertNull(q.first); + Assert.assertNull(q.last); + } + + + @Test + public void testSingleUnpopPop() throws Exception { + FastRemovalDequeue q = new FastRemovalDequeue<>(2); + + Object o1 = new Object(); + + q.unpop(o1); + + Object r = q.pop(); + + Assert.assertEquals(o1, r); + Assert.assertNull(q.first); + Assert.assertNull(q.last); + } + + + @Test + public void testDoubleUnpopPop() throws Exception { + FastRemovalDequeue q = new FastRemovalDequeue<>(2); + + Object o1 = new Object(); + Object o2 = new Object(); + + q.unpop(o1); + q.unpop(o2); + + Assert.assertEquals(o1, q.first.getContent()); + Assert.assertEquals(o2, q.last.getContent()); + + Object r2 = q.pop(); + + Assert.assertEquals(o2, r2); + Assert.assertEquals(o1, q.first.getContent()); + Assert.assertEquals(o1, q.last.getContent()); + + + Object r1 = q.pop(); + Assert.assertEquals(o1, r1); + Assert.assertNull(q.first); + Assert.assertNull(q.last); + } + + + @Test + public void testSinglePushUnpush() throws Exception { + FastRemovalDequeue q = new FastRemovalDequeue<>(2); + + Object o1 = new Object(); + + q.push(o1); + + Object r = q.unpush(); + + Assert.assertEquals(o1, r); + Assert.assertNull(q.first); + Assert.assertNull(q.last); + } + + + @Test + public void testDoublePushUnpush() throws Exception { + FastRemovalDequeue q = new FastRemovalDequeue<>(2); + + Object o1 = new Object(); + Object o2 = new Object(); + + q.push(o1); + q.push(o2); + + Assert.assertEquals(o2, q.first.getContent()); + Assert.assertEquals(o1, q.last.getContent()); + + Object r2 = q.unpush(); + + Assert.assertEquals(o2, r2); + Assert.assertEquals(o1, q.first.getContent()); + Assert.assertEquals(o1, q.last.getContent()); + + + Object r1 = q.unpush(); + Assert.assertEquals(o1, r1); + Assert.assertNull(q.first); + Assert.assertNull(q.last); + } + + + @Test + public void testSinglePushRemove() throws Exception { + FastRemovalDequeue q = new FastRemovalDequeue<>(2); + + Object o1 = new Object(); + + FastRemovalDequeue.Entry e1 = q.push(o1); + + Assert.assertEquals(o1, e1.getContent()); + + q.remove(e1); + + Assert.assertNull(q.first); + Assert.assertNull(q.last); + } + + + @Test + public void testDoublePushRemove() throws Exception { + FastRemovalDequeue q = new FastRemovalDequeue<>(2); + + Object o1 = new Object(); + Object o2 = new Object(); + + FastRemovalDequeue.Entry e1 = q.push(o1); + FastRemovalDequeue.Entry e2 = q.push(o2); + + Assert.assertEquals(o1, e1.getContent()); + Assert.assertEquals(o2, e2.getContent()); + + Assert.assertEquals(o2, q.first.getContent()); + Assert.assertEquals(o1, q.last.getContent()); + + q.remove(e1); + + Assert.assertEquals(o2, q.first.getContent()); + Assert.assertEquals(o2, q.last.getContent()); + + q.remove(e2); + + Assert.assertNull(q.first); + Assert.assertNull(q.last); + } +} diff --git a/test/org/apache/juli/TestAsyncFileHandlerOverflow.java b/test/org/apache/juli/TestAsyncFileHandlerOverflow.java new file mode 100644 index 0000000..a7d2435 --- /dev/null +++ b/test/org/apache/juli/TestAsyncFileHandlerOverflow.java @@ -0,0 +1,151 @@ +/* + * 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.juli; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.juli.AsyncFileHandler.LoggerExecutorService; + +@RunWith(Parameterized.class) +public class TestAsyncFileHandlerOverflow { + + private static final String PREFIX = "TestAsyncFileHandler."; + private static final String SUFFIX = ".log"; + private static final Logger logger = Logger.getLogger(TestAsyncFileHandlerOverflow.class.getName()); + { + logger.setUseParentHandlers(false); + } + + @Parameters(name = "{index}: overflowDropType[{0}]") + public static Collection parameters() { + return Arrays.asList(new Object[][] { + { Integer.valueOf(AsyncFileHandler.OVERFLOW_DROP_LAST), "START\n1\n3\n" }, + { Integer.valueOf(AsyncFileHandler.OVERFLOW_DROP_FIRST), "START\n2\n3\n" }, + { Integer.valueOf(AsyncFileHandler.OVERFLOW_DROP_FLUSH), "START\n1\n2\n3\n" }, + { Integer.valueOf(AsyncFileHandler.OVERFLOW_DROP_CURRENT), "START\n1\n2\n" } }); + } + + private final CountDownLatch latch = new CountDownLatch(1); + private Path logsDir; + private LoggerExecutorService loggerService; + private AsyncFileHandler handler; + + private final int overflowDropType; + private final String expected; + + public TestAsyncFileHandlerOverflow(final int overflowDropType, final String expected) { + this.overflowDropType = overflowDropType; + this.expected = expected; + } + + @Before + public void setUp() throws IOException { + final Path logsBase = Paths.get(System.getProperty("tomcat.test.temp", "output/tmp")); + Files.createDirectories(logsBase); + this.logsDir = Files.createTempDirectory(logsBase, "test"); + final Formatter formatter = new Formatter() { + + @Override + public String format(LogRecord record) { + return record.getMessage() + "\n"; + } + }; + // Setup an executor that blocks until the first rejection + this.loggerService = new LoggerExecutorService(overflowDropType, 2) { + + @Override + protected void beforeExecute(Thread t, Runnable r) { + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + super.beforeExecute(t, r); + } + }; + final RejectedExecutionHandler rejectionHandler = loggerService.getRejectedExecutionHandler(); + loggerService.setRejectedExecutionHandler(new RejectedExecutionHandler() { + + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + // Generally, the latch needs to be released after the + // RejectedExecutionHandler has completed but for the flush case + // the latch needs to be released first (else the test loops) + if (overflowDropType == AsyncFileHandler.OVERFLOW_DROP_FLUSH) { + latch.countDown(); + } + rejectionHandler.rejectedExecution(r, executor); + if (overflowDropType != AsyncFileHandler.OVERFLOW_DROP_FLUSH) { + latch.countDown(); + } + } + }); + this.handler = new AsyncFileHandler(logsDir.toString(), PREFIX, SUFFIX, Integer.valueOf(1), loggerService); + handler.setFormatter(formatter); + logger.addHandler(handler); + handler.open(); + } + + @After + public void cleanUp() { + handler.close(); + logger.removeHandler(handler); + } + + @Test + public void testOverFlow() throws IOException, InterruptedException { + handler.open(); + logger.warning("START"); // blocks async thread + // these are queued + logger.warning("1"); + logger.warning("2"); + logger.warning("3"); // overflows executor and unblocks aync thread + loggerService.shutdown(); + // after shutdown was issued + logger.warning("IGNORE"); + + loggerService.awaitTermination(1, TimeUnit.SECONDS); + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + Files.copy(logsDir.resolve(PREFIX + LocalDate.now() + SUFFIX), os); + final String actual = new String(os.toByteArray(), StandardCharsets.UTF_8); + Assert.assertEquals(expected, actual); + handler.close(); + } +} \ No newline at end of file diff --git a/test/org/apache/juli/TestClassLoaderLogManager.java b/test/org/apache/juli/TestClassLoaderLogManager.java new file mode 100644 index 0000000..b2482af --- /dev/null +++ b/test/org/apache/juli/TestClassLoaderLogManager.java @@ -0,0 +1,194 @@ +/* + * 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.juli; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test cases for {@link ClassLoaderLogManager}. + */ +public class TestClassLoaderLogManager { + + private static final byte[] EMPTY_BYTES = {}; + + @Test + public void testReplace() { + ClassLoaderLogManager logManager = new ClassLoaderLogManager(); + Assert.assertEquals("", logManager.replace("")); + Assert.assertEquals("${", logManager.replace("${")); + Assert.assertEquals("${undefinedproperty}", logManager.replace("${undefinedproperty}")); + Assert.assertEquals( + System.lineSeparator() + File.pathSeparator + File.separator, + logManager.replace("${line.separator}${path.separator}${file.separator}")); + Assert.assertEquals( + "foo" + File.separator + "bar" + System.lineSeparator() + File.pathSeparator + "baz", + logManager.replace("foo${file.separator}bar${line.separator}${path.separator}baz")); + // BZ 51249 + Assert.assertEquals( + "%{file.separator}" + File.separator, + logManager.replace("%{file.separator}${file.separator}")); + Assert.assertEquals( + File.separator + "${undefinedproperty}" + File.separator, + logManager.replace("${file.separator}${undefinedproperty}${file.separator}")); + Assert.assertEquals("${}" + File.pathSeparator, logManager.replace("${}${path.separator}")); + } + + @Test + public void testBug56082() { + ClassLoaderLogManager logManager = new ClassLoaderLogManager(); + + LoggerCreateThread[] createThreads = new LoggerCreateThread[10]; + for (int i = 0; i < createThreads.length; i ++) { + createThreads[i] = new LoggerCreateThread(logManager); + createThreads[i].setName("LoggerCreate-" + i); + createThreads[i].start(); + } + + LoggerListThread listThread = new LoggerListThread(logManager); + listThread.setName("LoggerList"); + listThread.start(); + + try { + listThread.join(2000); + } catch (InterruptedException e) { + // Ignore + } + + for (LoggerCreateThread createThread : createThreads) { + createThread.setRunning(false); + } + + Assert.assertTrue(listThread.isRunning()); + listThread.setRunning(false); + } + + /* + * Tests if a per-app root logger has a not {@code null} level. + */ + @Test + public void testBug66184() throws IOException { + final ClassLoader cl = new TestClassLoader(); + final Thread currentThread = Thread.currentThread(); + final ClassLoader oldCL = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(cl); + final ClassLoaderLogManager logManager = new ClassLoaderLogManager(); + logManager.readConfiguration(); + final Logger rootLogger = logManager.getLogger(""); + Assert.assertNotNull("root logger is null", rootLogger); + Assert.assertNull("root logger has a parent", rootLogger.getParent()); + Assert.assertEquals(Level.INFO, rootLogger.getLevel()); + } finally { + currentThread.setContextClassLoader(oldCL); + } + } + + private static class LoggerCreateThread extends Thread { + + private final LogManager logManager; + private volatile boolean running = true; + + LoggerCreateThread(LogManager logManager) { + this.logManager = logManager; + } + + @Override + public void run() { + Random r = new Random(); + while (running) { + Logger logger = Logger.getLogger("Bug56082-" + r.nextInt(100000)); + logManager.addLogger(logger); + } + } + + public void setRunning(boolean running) { + this.running = running; + } + } + + private static class LoggerListThread extends Thread { + + private final LogManager logManager; + private volatile boolean running = true; + + LoggerListThread(LogManager logManager) { + this.logManager = logManager; + } + + @Override + public void run() { + while (running) { + try { + Collections.list(logManager.getLoggerNames()); + } catch (Exception e) { + e.printStackTrace(); + running = false; + } + } + } + + public boolean isRunning() { + return running; + } + + public void setRunning(boolean running) { + this.running = running; + } + } + + private static class TestClassLoader extends ClassLoader implements WebappProperties { + + @Override + public String getWebappName() { + return "webapp"; + } + + @Override + public String getHostName() { + return "localhost"; + } + + @Override + public String getServiceName() { + return "Catalina"; + } + + @Override + public boolean hasLoggingConfig() { + return true; + } + + @Override + public InputStream getResourceAsStream(final String resource) { + if ("logging.properties".equals(resource)) { + return new ByteArrayInputStream(EMPTY_BYTES); + } + return null; + } + } +} diff --git a/test/org/apache/juli/TestDateFormatCache.java b/test/org/apache/juli/TestDateFormatCache.java new file mode 100644 index 0000000..9205364 --- /dev/null +++ b/test/org/apache/juli/TestDateFormatCache.java @@ -0,0 +1,109 @@ +/* + * 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.juli; + +import java.lang.reflect.Field; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.Assert; +import org.junit.Test; + +public class TestDateFormatCache { + + // Note that there is a similar test: + // org.apache.catalina.valves.TestAccessLogValve.testBug54044() + @Test + public void testBug54044() throws Exception { + + final String timeFormat = "dd-MMM-yyyy HH:mm:ss"; + final int cacheSize = 10; + + SimpleDateFormat sdf = new SimpleDateFormat(timeFormat, Locale.US); + sdf.setTimeZone(TimeZone.getDefault()); + + DateFormatCache dfc = new DateFormatCache(cacheSize, timeFormat, null); + + // Get dfc.cache.cache field + Object dfcCache; + Field dfcCacheArray; + Field dfcCacheField = dfc.getClass().getDeclaredField("cache"); + dfcCacheField.setAccessible(true); + dfcCache = dfcCacheField.get(dfc); + dfcCacheArray = dfcCache.getClass().getDeclaredField("cache"); + dfcCacheArray.setAccessible(true); + + // Create an array to hold the expected values + String[] expected = new String[cacheSize]; + + // Fill the cache & populate the expected values + for (int secs = 0; secs < (cacheSize); secs++) { + dfc.getFormat(secs * 1000); + expected[secs] = generateExpected(sdf, secs); + } + Assert.assertArrayEquals(expected, + (String[]) dfcCacheArray.get(dfcCache)); + + // Cause the cache to roll-around by one and then confirm + dfc.getFormat(cacheSize * 1000); + expected[0] = generateExpected(sdf, cacheSize); + Assert.assertArrayEquals(expected, + (String[]) dfcCacheArray.get(dfcCache)); + + // Jump 2 ahead and then confirm (skipped value should be null) + dfc.getFormat((cacheSize + 2) * 1000); + expected[1] = null; + expected[2] = generateExpected(sdf, cacheSize + 2); + Assert.assertArrayEquals(expected, + (String[]) dfcCacheArray.get(dfcCache)); + + // Back 1 to fill in the gap + dfc.getFormat((cacheSize + 1) * 1000); + expected[1] = generateExpected(sdf, cacheSize + 1); + Assert.assertArrayEquals(expected, + (String[]) dfcCacheArray.get(dfcCache)); + + // Return to 1 and confirm skipped value is null + dfc.getFormat(1 * 1000); + expected[1] = generateExpected(sdf, 1); + expected[2] = null; + Assert.assertArrayEquals(expected, + (String[]) dfcCacheArray.get(dfcCache)); + + // Go back one further + dfc.getFormat(0); + expected[0] = generateExpected(sdf, 0); + Assert.assertArrayEquals(expected, + (String[]) dfcCacheArray.get(dfcCache)); + + // Jump ahead far enough that the entire cache will need to be cleared + dfc.getFormat(42 * 1000); + for (int i = 0; i < cacheSize; i++) { + expected[i] = null; + } + expected[0] = generateExpected(sdf, 42); + Assert.assertArrayEquals(expected, + (String[]) dfcCacheArray.get(dfcCache)); + } + + private String generateExpected(SimpleDateFormat sdf, long secs) { + return sdf.format(new Date(secs * 1000)); + } + +} diff --git a/test/org/apache/juli/TestFileHandler.java b/test/org/apache/juli/TestFileHandler.java new file mode 100644 index 0000000..b6f4f2c --- /dev/null +++ b/test/org/apache/juli/TestFileHandler.java @@ -0,0 +1,136 @@ +/* + * 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.juli; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.LocalDateTime; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TestFileHandler { + + private static final String PREFIX_1 = "localhost."; + private static final String PREFIX_2 = "test."; + private static final String PREFIX_3 = ""; + private static final String PREFIX_4 = "localhost1"; + private static final String SUFFIX_1 = ".log"; + private static final String SUFFIX_2 = ".txt"; + + private File logsDir; + + @Before + public void setUp() throws Exception { + File logsBase = new File(System.getProperty("tomcat.test.temp", "output/tmp")); + if (!logsBase.mkdirs() && !logsBase.isDirectory()) { + Assert.fail("Unable to create logs directory."); + } + Path logsBasePath = FileSystems.getDefault().getPath(logsBase.getAbsolutePath()); + logsDir = Files.createTempDirectory(logsBasePath, "test").toFile(); + + generateLogFiles(logsDir, PREFIX_1, SUFFIX_2, 3); + generateLogFiles(logsDir, PREFIX_2, SUFFIX_1, 3); + generateLogFiles(logsDir, PREFIX_3, SUFFIX_1, 3); + generateLogFiles(logsDir, PREFIX_4, SUFFIX_1, 3); + + String date = LocalDateTime.now().minusDays(3).toString().replace(":", "-"); + File file = new File(logsDir, PREFIX_1 + date + SUFFIX_1); + if (!file.createNewFile()) { + Assert.fail("Unable to create " + file.getAbsolutePath()); + } + } + + @After + public void tearDown() { + File[] files = logsDir.listFiles(); + if (files != null) { + for (File file : files) { + Assert.assertTrue("Failed to delete [" + file + "]", file.delete()); + } + Assert.assertTrue("Failed to create [" + logsDir + "]", logsDir.delete()); + } + } + + @Test + public void testCleanOnInitOneHandler() throws Exception { + generateLogFiles(logsDir, PREFIX_1, SUFFIX_1, 3); + + FileHandler fh1 = new FileHandler(logsDir.getAbsolutePath(), PREFIX_1, SUFFIX_1, Integer.valueOf(2)); + fh1.open(); + + Thread.sleep(1000); + + Assert.assertTrue(logsDir.list().length == 16); + + fh1.close(); + } + + @Test + public void testCleanOnInitMultipleHandlers() throws Exception { + generateLogFiles(logsDir, PREFIX_1, SUFFIX_1, 3); + + FileHandler fh1 = new FileHandler(logsDir.getAbsolutePath(), PREFIX_1, SUFFIX_1, Integer.valueOf(2)); + FileHandler fh2 = new FileHandler(logsDir.getAbsolutePath(), PREFIX_1, SUFFIX_2, Integer.valueOf(2)); + FileHandler fh3 = new FileHandler(logsDir.getAbsolutePath(), PREFIX_2, SUFFIX_1, Integer.valueOf(2)); + FileHandler fh4 = new FileHandler(logsDir.getAbsolutePath(), PREFIX_3, SUFFIX_1, Integer.valueOf(2)); + fh1.open(); + fh2.open(); + fh3.open(); + fh4.open(); + + Thread.sleep(1000); + + Assert.assertTrue(logsDir.list().length == 16); + + fh1.close(); + fh2.close(); + fh3.close(); + fh4.close(); + } + + @Test + public void testCleanDisabled() throws Exception { + generateLogFiles(logsDir, PREFIX_1, SUFFIX_1, 3); + + FileHandler fh1 = new FileHandler(logsDir.getAbsolutePath(), PREFIX_1, SUFFIX_1, null); + fh1.open(); + + Thread.sleep(1000); + + Assert.assertTrue(logsDir.list().length == 17); + + fh1.close(); + } + + private void generateLogFiles(File dir, String prefix, String suffix, int amount) + throws IOException { + for (int i = 0; i < amount; i++) { + String date = LocalDate.now().minusDays(i + 1).toString().substring(0, 10); + File file = new File(dir, prefix + date + suffix); + if (!file.createNewFile()) { + Assert.fail("Unable to create " + file.getAbsolutePath()); + } + } + } +} diff --git a/test/org/apache/juli/TestFileHandlerNonRotatable.java b/test/org/apache/juli/TestFileHandlerNonRotatable.java new file mode 100644 index 0000000..a5affe8 --- /dev/null +++ b/test/org/apache/juli/TestFileHandlerNonRotatable.java @@ -0,0 +1,83 @@ +/* + * 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.juli; + +import java.io.File; +import java.net.URLDecoder; + +import org.junit.After; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.startup.LoggingBaseTest; + +public class TestFileHandlerNonRotatable extends LoggingBaseTest { + private FileHandler testHandler; + + @BeforeClass + public static void setUpPerTestClass() throws Exception { + LoggingBaseTest.setUpPerTestClass(); + + System.setProperty("java.util.logging.manager", + "org.apache.juli.ClassLoaderLogManager"); + String configLoggingPath = TestFileHandlerNonRotatable.class + .getResource("logging-non-rotatable.properties") + .getFile(); + System.setProperty("java.util.logging.config.file", + URLDecoder.decode(configLoggingPath, java.nio.charset.StandardCharsets.UTF_8.toString())); + } + + @Override + @After + public void tearDown() throws Exception { + if (testHandler != null) { + testHandler.close(); + } + super.tearDown(); + } + + @Test + public void testBug61232() throws Exception { + testHandler = new FileHandler(this.getTemporaryDirectory().toString(), + "juli.", ".log"); + testHandler.open(); + + File logFile = new File(this.getTemporaryDirectory(), "juli.log"); + Assert.assertTrue(logFile.exists()); + } + + @Test + public void testCustomSuffixWithoutSeparator() throws Exception { + testHandler = new FileHandler(this.getTemporaryDirectory().toString(), + "juli.", "log"); + testHandler.open(); + + File logFile = new File(this.getTemporaryDirectory(), "juli.log"); + Assert.assertTrue(logFile.exists()); + } + + @Test + public void testCustomPrefixWithoutSeparator() throws Exception { + testHandler = new FileHandler(this.getTemporaryDirectory().toString(), + "juli", ".log"); + testHandler.open(); + + File logFile = new File(this.getTemporaryDirectory(), "juli.log"); + Assert.assertTrue(logFile.exists()); + } +} \ No newline at end of file diff --git a/test/org/apache/juli/TestOneLineFormatterPerformance.java b/test/org/apache/juli/TestOneLineFormatterPerformance.java new file mode 100644 index 0000000..a90ebd6 --- /dev/null +++ b/test/org/apache/juli/TestOneLineFormatterPerformance.java @@ -0,0 +1,85 @@ +/* + * 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.juli; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Compares date/time format implementations. The current implementation ({@link + * DateFormatCache} is one to two orders of magnitude faster than + * {@link String#format(String, Object...)} + */ +public class TestOneLineFormatterPerformance { + + @Test + public void testDateFormat() throws Exception { + + DateFormat stringFormatImpl = new StringFormatImpl(); + long stringFormatImplTime = doTestDateFormat(stringFormatImpl); + + DateFormat dateFormatCacheImpl = new DateFormatCacheImpl(); + long dateFormatCacheImplTime = doTestDateFormat(dateFormatCacheImpl); + + Assert.assertTrue("String#format was faster that DateFormatCache", + dateFormatCacheImplTime < stringFormatImplTime); + } + + + private long doTestDateFormat(DateFormat df) { + int iters = 1000000; + + long start = System.nanoTime(); + for (int i = 0; i < iters; i++) { + df.format(System.nanoTime()); + } + long end = System.nanoTime(); + System.out.println( + "Impl: [" + df.getClass().getName() + "] took [" + (end - start) + "] ns"); + + return end - start; + } + + + private interface DateFormat { + String format(long timestamp); + } + + + private static class StringFormatImpl implements DateFormat { + + @Override + public String format(long timestamp) { + return String.format("%1$td-%1$tb-%1$tY %1$tH:%1$tM:%1$tS", Long.valueOf(timestamp)); + } + } + + + private static class DateFormatCacheImpl implements DateFormat { + + private final DateFormatCache cache; + + DateFormatCacheImpl() { + cache = new DateFormatCache(5, "dd-MMM-yyyy HH:mm:ss", null); + } + + @Override + public String format(long timestamp) { + return cache.getFormat(timestamp); + } + } +} diff --git a/test/org/apache/juli/TestThreadNameCache.java b/test/org/apache/juli/TestThreadNameCache.java new file mode 100644 index 0000000..d32efb8 --- /dev/null +++ b/test/org/apache/juli/TestThreadNameCache.java @@ -0,0 +1,61 @@ +/* + * 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.juli; + +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; + +import org.junit.Assert; +import org.junit.Test; + +public class TestThreadNameCache { + private Integer threadId; + + @Test + public void testCache() throws Exception { + final String THREAD_NAME = "t-TestThreadNameCache"; + final CountDownLatch threadIdLatch = new CountDownLatch(1); + final CountDownLatch cacheLatch = new CountDownLatch(1); + + OneLineFormatter olf = new OneLineFormatter(); + Method getThreadName = olf.getClass().getDeclaredMethod("getThreadName", int.class); + getThreadName.setAccessible(true); + Thread thread = new Thread() { + @Override + public void run() { + setName(THREAD_NAME); + threadId = Integer.valueOf((int) getId()); + threadIdLatch.countDown(); + try { + cacheLatch.await(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + }; + + thread.start(); + threadIdLatch.await(); + Object name = getThreadName.invoke(olf, threadId); + cacheLatch.countDown(); + Assert.assertEquals(THREAD_NAME, name); + + thread.join(); + name = getThreadName.invoke(olf, threadId); + Assert.assertEquals(THREAD_NAME, name); + } +} diff --git a/test/org/apache/juli/TesterOneLineFormatterMillisPerformance.java b/test/org/apache/juli/TesterOneLineFormatterMillisPerformance.java new file mode 100644 index 0000000..41dc3a8 --- /dev/null +++ b/test/org/apache/juli/TesterOneLineFormatterMillisPerformance.java @@ -0,0 +1,74 @@ +/* + * 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.juli; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +@RunWith(Parameterized.class) +public class TesterOneLineFormatterMillisPerformance { + + @Parameterized.Parameters(name = "{index}: format[{0}]") + public static Collection parameters() { + + List parameterSets = new ArrayList<>(); + + parameterSets.add(new String[] { "dd-MMM-yyyy HH:mm:ss.SSS" }); + parameterSets.add(new String[] { "dd-MMM-yyyy HH:mm:ss.SS" }); + parameterSets.add(new String[] { "dd-MMM-yyyy HH:mm:ss.S" }); + parameterSets.add(new String[] { "dd-MMM-yyyy HH:mm:ss" }); + parameterSets.add(new String[] { "dd-MMM-yyyy HH:mm:ss XXX" }); + parameterSets.add(new String[] { "dd-MMM-yyyy HH:mm:ss.SSSXXX" }); + parameterSets.add(new String[] { "dd-MMM-yyyy HH:mm:ss.SSXXX" }); + parameterSets.add(new String[] { "dd-MMM-yyyy HH:mm:ss.SXXX" }); + parameterSets.add(new String[] { "SSS dd-MMM-yyyy HH:mm:ss" }); + parameterSets.add(new String[] { "SS dd-MMM-yyyy HH:mm:ss" }); + parameterSets.add(new String[] { "S dd-MMM-yyyy HH:mm:ss" }); + + return parameterSets; + } + + + @Parameter(0) + public String timestampFormat; + + @Test + public void testMillisHandling() { + OneLineFormatter olf = new OneLineFormatter(); + olf.setTimeFormat(timestampFormat); + + long timeStamp = System.currentTimeMillis(); + StringBuilder buf = new StringBuilder(64); + + long start = System.nanoTime(); + for (int i = 0; i < 10000000; i++) { + buf.setLength(0); + olf.addTimestamp(buf, timeStamp); + } + System.out.println("Format: [" + timestampFormat + "], Output: [" + buf + "], Duration: [" + (System.nanoTime() - start) + "] ns"); + } +} diff --git a/test/org/apache/juli/logging-non-rotatable.properties b/test/org/apache/juli/logging-non-rotatable.properties new file mode 100644 index 0000000..06a9c02 --- /dev/null +++ b/test/org/apache/juli/logging-non-rotatable.properties @@ -0,0 +1,16 @@ +# 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. + +org.apache.juli.FileHandler.rotatable = false \ No newline at end of file diff --git a/test/org/apache/naming/TestEnvEntry.java b/test/org/apache/naming/TestEnvEntry.java new file mode 100644 index 0000000..4a16e42 --- /dev/null +++ b/test/org/apache/naming/TestEnvEntry.java @@ -0,0 +1,136 @@ +/* + * 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.naming; + +import java.io.File; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestEnvEntry extends TomcatBaseTest { + + @Test + public void testEnvEntryBasic() throws Exception { + doTestJndiLookup("env-entry/basic", "basic-value"); + } + + + @Test + public void testEnvEntryValid() throws Exception { + doTestJndiLookup("env-entry/valid", "valid"); + } + + + @Test + public void testEnvEntryInvalid() throws Exception { + doTestJndiLookup("env-entry/invalid", "Not Found"); + } + + + @Test + public void testEnvEntryInjectField() throws Exception { + doTestJndiInjection("property1", "inject-value-1"); + } + + + @Test + public void testEnvEntryInjectProperty() throws Exception { + doTestJndiInjection("property2", "inject-value-2"); + } + + + @Test + public void testEnvEntryInjectFieldNoType() throws Exception { + doTestJndiInjection("property3", "inject-value-3"); + } + + + @Test + public void testEnvEntryInjectionNoValue() throws Exception { + doTestJndiLookup("env-entry/injectNoValue", "Not Found"); + } + + + @Test + public void testEnvEntryLookup() throws Exception { + doTestJndiLookup("env-entry/lookup", "basic-value"); + } + + + @Test + public void testEnvEntryLookupCircular() throws Exception { + doTestJndiLookup("env-entry/circular1", "Naming Error"); + } + + + @Test + public void testEnvEntryLookupInvalid() throws Exception { + doTestJndiLookup("env-entry/lookup-invalid", "Naming Error"); + } + + + private void doTestJndiLookup(String jndiName, String expected) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.enableNaming(); + tomcat.start(); + + ByteChunk out = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/jndi.jsp?jndiName=" + + jndiName, out, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // JSP has leading and trailing white-space + String result = out.toString().trim(); + Assert.assertEquals(expected, result); + } + + + private void doTestJndiInjection(String injectionName, String expected) throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + Context context = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + Tomcat.addServlet(context, "InjectionServlet", "org.apache.naming.TesterInjectionServlet"); + context.addServletMappingDecoded("/injection", "InjectionServlet"); + + tomcat.enableNaming(); + tomcat.start(); + + ByteChunk out = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + "/test/injection?injectionName=" + + injectionName, out, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // JSP has leading and trailing white-space + String result = out.toString().trim(); + Assert.assertEquals(expected, result); + } +} diff --git a/test/org/apache/naming/TestNamingContext.java b/test/org/apache/naming/TestNamingContext.java new file mode 100644 index 0000000..25ea465 --- /dev/null +++ b/test/org/apache/naming/TestNamingContext.java @@ -0,0 +1,104 @@ +/* + * 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.naming; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.naming.factory.ResourceLinkFactory; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextResourceLink; + +public class TestNamingContext extends TomcatBaseTest { + + private static final String COMP_ENV = "comp/env"; + private static final String GLOBAL_NAME = "global"; + private static final String LOCAL_NAME = "local"; + private static final String DATA = "Cabbage"; + + + @Test + public void testGlobalNaming() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + org.apache.catalina.Context ctx = getProgrammaticRootContext(); + + tomcat.start(); + + Context webappInitial = ContextBindings.getContext(ctx); + + // Nothing added at the moment so should be null + Object obj = doLookup(webappInitial, COMP_ENV + "/" + LOCAL_NAME); + Assert.assertNull(obj); + + ContextEnvironment ce = new ContextEnvironment(); + ce.setName(GLOBAL_NAME); + ce.setValue(DATA); + ce.setType(DATA.getClass().getName()); + + tomcat.getServer().getGlobalNamingResources().addEnvironment(ce); + + // No link so still should be null + obj = doLookup(webappInitial, COMP_ENV + "/" + LOCAL_NAME); + Assert.assertNull(obj); + + // Now add a resource link to the context + ContextResourceLink crl = new ContextResourceLink(); + crl.setGlobal(GLOBAL_NAME); + crl.setName(LOCAL_NAME); + crl.setType(DATA.getClass().getName()); + ctx.getNamingResources().addResourceLink(crl); + + // Link exists so should be OK now + obj = doLookup(webappInitial, COMP_ENV + "/" + LOCAL_NAME); + Assert.assertEquals(DATA, obj); + + // Try shortcut + ResourceLinkFactory factory = new ResourceLinkFactory(); + ResourceLinkRef rlr = new ResourceLinkRef(DATA.getClass().getName(), GLOBAL_NAME, null, null); + obj = factory.getObjectInstance(rlr, null, null, null); + Assert.assertEquals(DATA, obj); + + // Remove the link + ctx.getNamingResources().removeResourceLink(LOCAL_NAME); + + // No link so should be null + obj = doLookup(webappInitial, COMP_ENV + "/" + LOCAL_NAME); + Assert.assertNull(obj); + + // Shortcut should fail too + obj = factory.getObjectInstance(rlr, null, null, null); + Assert.assertNull(obj); + } + + + private Object doLookup(Context context, String name) { + Object result = null; + try { + result = context.lookup(name); + } catch (NamingException nnfe) { + // Ignore + } + return result; + } +} diff --git a/test/org/apache/naming/TesterEnvEntry.java b/test/org/apache/naming/TesterEnvEntry.java new file mode 100644 index 0000000..62c589d --- /dev/null +++ b/test/org/apache/naming/TesterEnvEntry.java @@ -0,0 +1,33 @@ +/* + * 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.naming; + +public class TesterEnvEntry { + + private static final String VALID = "valid"; + + public TesterEnvEntry(String value) { + if (!VALID.equals(value)) { + throw new IllegalArgumentException(); + } + } + + @Override + public String toString() { + return VALID; + } +} diff --git a/test/org/apache/naming/TesterInjectionServlet.java b/test/org/apache/naming/TesterInjectionServlet.java new file mode 100644 index 0000000..b7ec3f0 --- /dev/null +++ b/test/org/apache/naming/TesterInjectionServlet.java @@ -0,0 +1,68 @@ +/* + * 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.naming; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.tomcat.util.IntrospectionUtils; + +public class TesterInjectionServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private String property1 = null; + public String getProperty1() { return property1; } + + // Not used directly. + // Here to ensure properties are injected in preference to fields + private String property2 = null; + public void setProperty2a(String property2) { this.property2 = property2; } + public String getProperty2a() { return property2; } + + private String property2a = null; + public void setProperty2(String property2) { this.property2a = property2; } + public String getProperty2() { return property2a; } + + private String property3 = null; + public String getProperty3() { return property3; } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + String injectionName = req.getParameter("injectionName"); + + PrintWriter pw = resp.getWriter(); + pw.print(IntrospectionUtils.getProperty(this, injectionName)); + + // The property should take precedence over the field and this should + // be null + if (getProperty2a() != null) { + pw.println(); + pw.print(getProperty2a()); + } + } +} diff --git a/test/org/apache/naming/factory/TestBeanFactory.java b/test/org/apache/naming/factory/TestBeanFactory.java new file mode 100644 index 0000000..ec85ff6 --- /dev/null +++ b/test/org/apache/naming/factory/TestBeanFactory.java @@ -0,0 +1,67 @@ +/* + * 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.naming.factory; + +import javax.naming.StringRefAddr; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.naming.ResourceRef; + +public class TestBeanFactory { + + private static final String IP_ADDRESS = "127.0.0.1"; + + @Test + public void testForceStringAlternativeWithout() throws Exception { + doTestForceStringAlternatove(false); + } + + + @Test + public void testForceStringAlternativeWith() throws Exception { + doTestForceStringAlternatove(true); + } + + + private void doTestForceStringAlternatove(boolean useForceString) throws Exception { + + // Create the resource definition + ResourceRef resourceRef = new ResourceRef(TesterBean.class.getName(), null, null, null, false); + StringRefAddr server = new StringRefAddr("server", IP_ADDRESS); + resourceRef.add(server); + if (useForceString) { + StringRefAddr force = new StringRefAddr("forceString", "server"); + resourceRef.add(force); + } + + // Create the factory + BeanFactory factory = new BeanFactory(); + + // Use the factory to create the resource from the definition + Object obj = factory.getObjectInstance(resourceRef, null, null, null); + + // Check the correct type was created + Assert.assertNotNull(obj); + Assert.assertEquals(obj.getClass(), TesterBean.class); + // Check the server field was set + TesterBean result = (TesterBean) obj; + Assert.assertNotNull(result.getServer()); + Assert.assertEquals(IP_ADDRESS, result.getServer().getHostAddress()); + } +} diff --git a/test/org/apache/naming/factory/TesterBean.java b/test/org/apache/naming/factory/TesterBean.java new file mode 100644 index 0000000..d2beef2 --- /dev/null +++ b/test/org/apache/naming/factory/TesterBean.java @@ -0,0 +1,41 @@ +/* + * 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.naming.factory; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class TesterBean { + + private InetAddress server; + + public InetAddress getServer() { + return server; + } + + public void setServer(InetAddress server) { + this.server = server; + } + + public void setServer(String server) { + try { + this.server = InetAddress.getByName(server); + } catch (UnknownHostException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/test/org/apache/naming/resources/TestNamingContext.java b/test/org/apache/naming/resources/TestNamingContext.java new file mode 100644 index 0000000..87c9683 --- /dev/null +++ b/test/org/apache/naming/resources/TestNamingContext.java @@ -0,0 +1,366 @@ +/* + * 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.naming.resources; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +import javax.naming.Binding; +import javax.naming.CompositeName; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.ContextEnvironment; +import org.apache.tomcat.util.descriptor.web.ContextResource; + +public class TestNamingContext extends TomcatBaseTest { + + @Test + public void testLookupSingletonResource() throws Exception { + doTestLookup(true); + } + + @Test + public void testLookupNonSingletonResource() throws Exception { + doTestLookup(false); + } + + public void doTestLookup(boolean useSingletonResource) throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + // No file system docBase required + org.apache.catalina.Context ctx = getProgrammaticRootContext(); + + // Create the resource + ContextResource cr = new ContextResource(); + cr.setName("list/foo"); + cr.setType("org.apache.naming.resources.TesterObject"); + cr.setProperty("factory", "org.apache.naming.resources.TesterFactory"); + cr.setSingleton(useSingletonResource); + ctx.getNamingResources().addResource(cr); + + // Map the test Servlet + Bug49994Servlet bug49994Servlet = new Bug49994Servlet(); + Tomcat.addServlet(ctx, "bug49994Servlet", bug49994Servlet); + ctx.addServletMappingDecoded("/", "bug49994Servlet"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + + String expected; + if (useSingletonResource) { + expected = "EQUAL"; + } else { + expected = "NOTEQUAL"; + } + Assert.assertEquals(expected, bc.toString()); + + } + + public static final class Bug49994Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain;UTF-8"); + PrintWriter out = resp.getWriter(); + + try { + Context ctx = new InitialContext(); + Object obj1 = ctx.lookup("java:comp/env/list/foo"); + Object obj2 = ctx.lookup("java:comp/env/list/foo"); + if (obj1 == obj2) { + out.print("EQUAL"); + } else { + out.print("NOTEQUAL"); + } + } catch (NamingException ne) { + ne.printStackTrace(out); + } + } + } + + @Test + public void testListBindings() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + // No file system docBase required + org.apache.catalina.Context ctx = getProgrammaticRootContext(); + + // Create the resource + ContextResource cr = new ContextResource(); + cr.setName("list/foo"); + cr.setType("org.apache.naming.resources.TesterObject"); + cr.setProperty("factory", "org.apache.naming.resources.TesterFactory"); + ctx.getNamingResources().addResource(cr); + + // Map the test Servlet + Bug23950Servlet bug23950Servlet = new Bug23950Servlet(); + Tomcat.addServlet(ctx, "bug23950Servlet", bug23950Servlet); + ctx.addServletMappingDecoded("/", "bug23950Servlet"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("org.apache.naming.resources.TesterObject", bc.toString()); + } + + public static final class Bug23950Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain;UTF-8"); + PrintWriter out = resp.getWriter(); + + try { + Context ctx = new InitialContext(); + NamingEnumeration enm = + ctx.listBindings("java:comp/env/list"); + while (enm.hasMore()) { + Binding b = enm.next(); + out.print(b.getObject().getClass().getName()); + } + } catch (NamingException ne) { + ne.printStackTrace(out); + } + } + } + + @Test + public void testBeanFactory() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + // No file system docBase required + org.apache.catalina.Context ctx = getProgrammaticRootContext(); + + // Create the resource + ContextResource cr = new ContextResource(); + cr.setName("bug50351"); + cr.setType("org.apache.naming.resources.TesterObject"); + cr.setProperty("factory", "org.apache.naming.factory.BeanFactory"); + cr.setProperty("foo", "value"); + ctx.getNamingResources().addResource(cr); + + // Map the test Servlet + Bug50351Servlet bug50351Servlet = new Bug50351Servlet(); + Tomcat.addServlet(ctx, "bug50351Servlet", bug50351Servlet); + ctx.addServletMappingDecoded("/", "bug50351Servlet"); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + "/"); + Assert.assertEquals("value", bc.toString()); + } + + public static final class Bug50351Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain;UTF-8"); + PrintWriter out = resp.getWriter(); + + try { + Context ctx = new InitialContext(); + Object obj = ctx.lookup("java:comp/env/bug50351"); + TesterObject to = (TesterObject) obj; + out.print(to.getFoo()); + } catch (NamingException ne) { + ne.printStackTrace(out); + } + } + } + + @Test + public void testBug51744a() throws Exception { + doTestBug51744(true); + } + + @Test + public void testBug51744b() throws Exception { + doTestBug51744(false); + } + + private void doTestBug51744(boolean exceptionOnFailedWrite) + throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + // No file system docBase required + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + + ctx.setJndiExceptionOnFailedWrite(exceptionOnFailedWrite); + + // Map the test Servlet + Bug51744Servlet bug51744Servlet = new Bug51744Servlet(); + Tomcat.addServlet(ctx, "bug51744Servlet", bug51744Servlet); + ctx.addServletMappingDecoded("/", "bug51744Servlet"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/", bc, null); + Assert.assertEquals(200, rc); + Assert.assertTrue(bc.toString().contains(Bug51744Servlet.EXPECTED)); + if (exceptionOnFailedWrite) { + Assert.assertTrue(bc.toString().contains(Bug51744Servlet.ERROR_MESSAGE)); + } + } + + public static final class Bug51744Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public static final String EXPECTED = "TestValue"; + public static final String ERROR_MESSAGE = "Error"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain;UTF-8"); + PrintWriter out = resp.getWriter(); + + try { + Context ctx1 = new InitialContext(); + Context env1 = (Context) ctx1.lookup("java:comp/env"); + env1.addToEnvironment("TestName", EXPECTED); + + out.print(env1.getEnvironment().get("TestName")); + + try { + env1.close(); + } catch (NamingException ne) { + out.print(ERROR_MESSAGE); + } + } catch (NamingException ne) { + ne.printStackTrace(out); + } + } + } + + @Test + public void testBug52830() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + // No file system docBase required + org.apache.catalina.Context ctx = getProgrammaticRootContext(); + + // Create the resource + ContextEnvironment env = new ContextEnvironment(); + env.setName("boolean"); + env.setType(Boolean.class.getName()); + env.setValue("true"); + ctx.getNamingResources().addEnvironment(env); + + // Map the test Servlet + Bug52830Servlet bug52830Servlet = new Bug52830Servlet(); + Tomcat.addServlet(ctx, "bug52830Servlet", bug52830Servlet); + ctx.addServletMappingDecoded("/", "bug52830Servlet"); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/", bc, null); + Assert.assertEquals(200, rc); + Assert.assertTrue(bc.toString().contains("truetrue")); + } + + public static final class Bug52830Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + public static final String JNDI_NAME = "java:comp/env/boolean"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain;UTF-8"); + PrintWriter out = resp.getWriter(); + + try { + Context initCtx = new InitialContext(); + + Boolean b1 = (Boolean) initCtx.lookup(JNDI_NAME); + Boolean b2 = (Boolean) initCtx.lookup( + new CompositeName(JNDI_NAME)); + + out.print(b1); + out.print(b2); + + } catch (NamingException ne) { + throw new ServletException(ne); + } + } + } + + @Test + public void testBug53465() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + File appDir = + new File("test/webapp"); + // app dir is relative to server home + org.apache.catalina.Context ctxt = + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + + "/test/bug5nnnn/bug53465.jsp", bc, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(bc.toString().contains("

    10

    ")); + + ContextEnvironment ce = + ctxt.getNamingResources().findEnvironment("bug53465"); + Assert.assertEquals("Bug53465MappedName", ce.getProperty("mappedName")); + } +} diff --git a/test/org/apache/naming/resources/TestWarDirContext.java b/test/org/apache/naming/resources/TestWarDirContext.java new file mode 100644 index 0000000..a86603f --- /dev/null +++ b/test/org/apache/naming/resources/TestWarDirContext.java @@ -0,0 +1,130 @@ +/* + * 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.naming.resources; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.core.JreMemoryLeakPreventionListener; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.webresources.StandardRoot; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestWarDirContext extends TomcatBaseTest { + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + // The test fails if JreMemoryLeakPreventionListener is not + // present. The listener affects the JVM, and thus not only the current, + // but also the subsequent tests that are run in the same JVM. So it is + // fair to add it in every test. + tomcat.getServer().addLifecycleListener( + new JreMemoryLeakPreventionListener()); + } + + /* + * Check https://jira.springsource.org/browse/SPR-7350 isn't really an issue + */ + @Test + public void testLookupException() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + ByteChunk bc = getUrl("http://localhost:" + getPort() + + "/test/warDirContext.jsp"); + Assert.assertEquals("

    java.lang.ClassNotFoundException

    ", + bc.toString()); + } + + + /* + * Additional test following on from SPR-7350 above to check files that + * contain JNDI reserved characters can be served when caching is enabled. + */ + @Test + public void testReservedJNDIFileNamesWithCache() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + // app dir is relative to server home + StandardContext ctxt = (StandardContext) tomcat.addWebapp( + null, "/test", appDir.getAbsolutePath()); + StandardRoot root = new StandardRoot(); + root.setCachingAllowed(true); + ctxt.setResources(root); + + tomcat.start(); + + // Should be found in resources.jar + ByteChunk bc = getUrl("http://localhost:" + getPort() + + "/test/'singlequote.jsp"); + Assert.assertEquals("

    'singlequote.jsp in resources.jar

    ", + bc.toString()); + + // Should be found in file system + bc = getUrl("http://localhost:" + getPort() + + "/test/'singlequote2.jsp"); + Assert.assertEquals("

    'singlequote2.jsp in file system

    ", + bc.toString()); + } + + + /* + * Additional test following on from SPR-7350 above to check files that + * contain JNDI reserved characters can be served when caching is disabled. + */ + @Test + public void testReservedJNDIFileNamesNoCache() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-fragments"); + // app dir is relative to server home + StandardContext ctxt = (StandardContext) tomcat.addWebapp( + null, "/test", appDir.getAbsolutePath()); + StandardRoot root = new StandardRoot(); + root.setCachingAllowed(true); + ctxt.setResources(root); + skipTldsForResourceJars(ctxt); + + tomcat.start(); + + // Should be found in resources.jar + ByteChunk bc = getUrl("http://localhost:" + getPort() + + "/test/'singlequote.jsp"); + Assert.assertEquals("

    'singlequote.jsp in resources.jar

    ", + bc.toString()); + + // Should be found in file system + bc = getUrl("http://localhost:" + getPort() + + "/test/'singlequote2.jsp"); + Assert.assertEquals("

    'singlequote2.jsp in file system

    ", + bc.toString()); + } +} diff --git a/test/org/apache/naming/resources/TesterFactory.java b/test/org/apache/naming/resources/TesterFactory.java new file mode 100644 index 0000000..13e0926 --- /dev/null +++ b/test/org/apache/naming/resources/TesterFactory.java @@ -0,0 +1,49 @@ +/* + * 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.naming.resources; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +public class TesterFactory implements ObjectFactory { + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, + Hashtable environment) throws Exception { + + if (obj instanceof Reference) { + Reference ref = (Reference)obj; + String className = ref.getClassName(); + + if (className == null) { + throw new RuntimeException(); + } + + if (className.equals("org.apache.naming.resources.TesterObject")) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Class clazz = + cl.loadClass("org.apache.naming.resources.TesterObject"); + return clazz.getConstructor().newInstance(); + } + } + return null; + } +} diff --git a/test/org/apache/naming/resources/TesterObject.java b/test/org/apache/naming/resources/TesterObject.java new file mode 100644 index 0000000..f5ec41a --- /dev/null +++ b/test/org/apache/naming/resources/TesterObject.java @@ -0,0 +1,35 @@ +/* + * 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.naming.resources; + +public class TesterObject { + + private String foo; + + @Override + public String toString() { + return "This is a test object (" + super.toString() + ")."; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getFoo() { + return this.foo; + } +} diff --git a/test/org/apache/tomcat/buildutil/translate/TestFixedStrings.java b/test/org/apache/tomcat/buildutil/translate/TestFixedStrings.java new file mode 100644 index 0000000..a401611 --- /dev/null +++ b/test/org/apache/tomcat/buildutil/translate/TestFixedStrings.java @@ -0,0 +1,68 @@ +/* + * 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.tomcat.buildutil.translate; + +import java.io.File; +import java.util.Properties; + +import org.junit.Assert; +import org.junit.Test; + +public class TestFixedStrings { + + @Test + public void testManagerApps() { + doTestStartsWith("org.apache.catalina.manager", "OK -"); + doTestStartsWith("org.apache.catalina.manager.host", "OK -"); + } + + + private void doTestStartsWith(String packageName, String text) { + File dir = new File("java/" + packageName.replace('.', '/')); + + // English + File fileEn = new File(dir, Constants.L10N_PREFIX + Constants.L10N_SUFFIX); + Properties propsEn = Utils.load(fileEn); + + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + String fileName = file.getName(); + if (fileName.startsWith(Constants.L10N_PREFIX)) { + if (!Utils.getLanguage(fileName).equals("en")) { + doTestStartsFile(file, propsEn, text); + } + } + } + } + } + + + private void doTestStartsFile(File fileTranslated, Properties propsEn, String text) { + Properties propsTranslated = Utils.load(fileTranslated); + + for (Object key : propsEn.keySet()) { + String value = (String) propsEn.get(key); + if (value != null && value.startsWith(text)) { + String valueTranslated = (String) propsTranslated.get(key); + if (valueTranslated != null && !valueTranslated.startsWith(text)) { + Assert.fail(fileTranslated.getPath() + " : " + key); + } + } + } + } +} diff --git a/test/org/apache/tomcat/buildutil/translate/TestUtils.java b/test/org/apache/tomcat/buildutil/translate/TestUtils.java new file mode 100644 index 0000000..d2d5490 --- /dev/null +++ b/test/org/apache/tomcat/buildutil/translate/TestUtils.java @@ -0,0 +1,79 @@ +/* + * 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.tomcat.buildutil.translate; + +import org.junit.Assert; +import org.junit.Test; + +public class TestUtils { + + @Test + public void testQuoteReplacement01() { + Assert.assertEquals("[{0}] a''a", Utils.formatValueImport("[{0}] a'a")); + } + + @Test + public void testQuoteReplacement02() { + Assert.assertEquals("[{0}] a''", Utils.formatValueImport("[{0}] a'")); + } + + + @Test + public void testQuoteReplacement03() { + Assert.assertEquals("''a [{0}]", Utils.formatValueImport("'a [{0}]")); + } + + @Test + public void testQuoteReplacement05() { + Assert.assertEquals("[{0}] ''a'' bbb", Utils.formatValueImport("[{0}] 'a' bbb")); + } + + @Test + public void testQuoteReplacement06() { + Assert.assertEquals("[{0}] ''aa'' bbb", Utils.formatValueImport("[{0}] 'aa' bbb")); + } + + @Test + public void testFormatValue01() { + // Import from Tomcat + Assert.assertEquals("\\n\\\n\\n", Utils.formatValueImport("\\n\\\n\\n")); + } + + @Test + public void testFormatValue02() { + // Import from POEditor + Assert.assertEquals("\\n\\\n\\n", Utils.formatValueImport("\\n\\n")); + } + + @Test + public void testFormatValue03() { + // Export from Tomcat + Assert.assertEquals("line1\\n\\\nline2\\n\\\nline3", Utils.formatValueExport("line1\nline2\nline3")); + } + + @Test + public void testFormatValue04() { + // Export from Tomcat + Assert.assertEquals(Utils.PADDING + "\\n\\\nline2\\n\\\nline3", Utils.formatValueExport("\nline2\nline3")); + } + + @Test + public void testFormatValue05() { + // Export from Tomcat + Assert.assertEquals("line1\\n\\\n\\tline2\\n\\\n\\tline3", Utils.formatValueExport("line1\n\tline2\n\tline3")); + } +} diff --git a/test/org/apache/tomcat/jni/TesterSSL.java b/test/org/apache/tomcat/jni/TesterSSL.java new file mode 100644 index 0000000..3ef57ee --- /dev/null +++ b/test/org/apache/tomcat/jni/TesterSSL.java @@ -0,0 +1,58 @@ +/* + * 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.tomcat.jni; + +import org.junit.Test; + +/* + * Helper class to investigate native memory leaks. Needs to be used with tools + * to monitor native memory usage. + * + * Note: Moving the Pool, SSLContext, SSL and BIO creation in/out of the loop + * can help identify where the memory is leaking. + */ +public class TesterSSL { + + @Test + public void testCreateDestroy() throws Exception { + Library.initialize(null); + SSL.initialize(null); + + long memoryPool = Pool.create(0); + long sslCtx = SSLContext.make(memoryPool, SSL.SSL_PROTOCOL_ALL, SSL.SSL_MODE_SERVER); + + for (int i = 0; i < 10000000; i++) { + doNative(sslCtx); + if (i % 1000 == 0) { + System.gc(); + } + } + + SSLContext.free(sslCtx); + Pool.destroy(memoryPool); + + System.gc(); + } + + + private void doNative(long sslCtx) throws Exception { + long ssl = SSL.newSSL(sslCtx, true); + long bio = SSL.makeNetworkBIO(ssl); + SSL.freeBIO(bio); + SSL.freeSSL(ssl); + } +} diff --git a/test/org/apache/tomcat/unittest/TesterBug66582.java b/test/org/apache/tomcat/unittest/TesterBug66582.java new file mode 100644 index 0000000..fb8c971 --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterBug66582.java @@ -0,0 +1,21 @@ +/* + * 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.tomcat.unittest; + +public interface TesterBug66582 { + String DATA = "data"; +} diff --git a/test/org/apache/tomcat/unittest/TesterContext.java b/test/org/apache/tomcat/unittest/TesterContext.java new file mode 100644 index 0000000..3c7a5cd --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterContext.java @@ -0,0 +1,1335 @@ +/* + * 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.tomcat.unittest; + +import java.beans.PropertyChangeListener; +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.ObjectName; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration.Dynamic; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.descriptor.JspConfigDescriptor; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Authenticator; +import org.apache.catalina.Cluster; +import org.apache.catalina.Container; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Loader; +import org.apache.catalina.Manager; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Realm; +import org.apache.catalina.ThreadBindingListener; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.deploy.NamingResourcesImpl; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.JarScanner; +import org.apache.tomcat.util.descriptor.web.ApplicationParameter; +import org.apache.tomcat.util.descriptor.web.ErrorPage; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.http.CookieProcessor; + +/** + * Minimal implementation for use in unit tests. + */ +public class TesterContext implements Context { + + private static final Log log = LogFactory.getLog(TesterContext.class); + + private List securityRoles = new ArrayList<>(); + @Override + public void addSecurityRole(String role) { + securityRoles.add(role); + } + + @Override + public boolean findSecurityRole(String role) { + return securityRoles.contains(role); + } + + @Override + public String[] findSecurityRoles() { + return securityRoles.toArray(new String[0]); + } + + @Override + public void removeSecurityRole(String role) { + securityRoles.remove(role); + } + + private List securityConstraints = new ArrayList<>(); + @Override + public void addConstraint(SecurityConstraint constraint) { + securityConstraints.add(constraint); + } + + @Override + public SecurityConstraint[] findConstraints() { + return securityConstraints.toArray(new SecurityConstraint[0]); + } + + @Override + public void removeConstraint(SecurityConstraint constraint) { + securityConstraints.remove(constraint); + } + + + @Override + public Log getLogger() { + return log; + } + + @Override + public String getLogName() { + return null; + } + + @Override + public ObjectName getObjectName() { + return null; + } + + @Override + public String getDomain() { + return null; + } + + @Override + public String getMBeanKeyProperties() { + return null; + } + + @Override + public Pipeline getPipeline() { + return null; + } + + @Override + public Cluster getCluster() { + return null; + } + + @Override + public void setCluster(Cluster cluster) { + // NO-OP + } + + @Override + public int getBackgroundProcessorDelay() { + return 0; + } + + @Override + public void setBackgroundProcessorDelay(int delay) { + // NO-OP + } + + private String name = "/test"; + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + private Container parent = null; + @Override + public Container getParent() { + return parent; + } + + @Override + public void setParent(Container container) { + this.parent = container; + } + + @Override + public ClassLoader getParentClassLoader() { + return null; + } + + @Override + public void setParentClassLoader(ClassLoader parent) { + // NO-OP + } + + @Override + public Realm getRealm() { + return null; + } + + @Override + public void setRealm(Realm realm) { + // NO-OP + } + + @Override + public void backgroundProcess() { + // NO-OP + } + + @Override + public void addChild(Container child) { + // NO-OP + } + + @Override + public void addContainerListener(ContainerListener listener) { + // NO-OP + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + // NO-OP + } + + @Override + public Container findChild(String name) { + return null; + } + + @Override + public Container[] findChildren() { + return null; + } + + @Override + public ContainerListener[] findContainerListeners() { + return null; + } + + @Override + public void removeChild(Container child) { + // NO-OP + } + + @Override + public void removeContainerListener(ContainerListener listener) { + // NO-OP + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + // NO-OP + } + + @Override + public void fireContainerEvent(String type, Object data) { + // NO-OP + } + + @Override + public void logAccess(Request request, Response response, long time, + boolean useDefault) { + // NO-OP + } + + @Override + public AccessLog getAccessLog() { + return null; + } + + @Override + public int getStartStopThreads() { + return 0; + } + + @Override + public void setStartStopThreads(int startStopThreads) { + // NO-OP + } + + @Override + public File getCatalinaBase() { + return null; + } + + @Override + public File getCatalinaHome() { + return null; + } + + @Override + public void addLifecycleListener(LifecycleListener listener) { + // NO-OP + } + + @Override + public LifecycleListener[] findLifecycleListeners() { + return null; + } + + @Override + public void removeLifecycleListener(LifecycleListener listener) { + // NO-OP + } + + @Override + public void init() throws LifecycleException { + // NO-OP + } + + @Override + public void start() throws LifecycleException { + // NO-OP + } + + @Override + public void stop() throws LifecycleException { + // NO-OP + } + + @Override + public void destroy() throws LifecycleException { + // NO-OP + } + + @Override + public LifecycleState getState() { + return null; + } + + @Override + public String getStateName() { + return null; + } + + @Override + public boolean getAllowCasualMultipartParsing() { + return false; + } + + @Override + public void setAllowCasualMultipartParsing( + boolean allowCasualMultipartParsing) { + // NO-OP + } + + @Override + public Object[] getApplicationEventListeners() { + return null; + } + + @Override + public void setApplicationEventListeners(Object[] listeners) { + // NO-OP + } + + @Override + public Object[] getApplicationLifecycleListeners() { + return null; + } + + @Override + public void setApplicationLifecycleListeners(Object[] listeners) { + // NO-OP + } + + @Override + public URL getConfigFile() { + return null; + } + + @Override + public void setConfigFile(URL configFile) { + // NO-OP + } + + @Override + public boolean getConfigured() { + return false; + } + + @Override + public void setConfigured(boolean configured) { + // NO-OP + } + + @Override + public boolean getCookies() { + return false; + } + + @Override + public void setCookies(boolean cookies) { + // NO-OP + } + + @Override + public String getSessionCookieName() { + return null; + } + + @Override + public void setSessionCookieName(String sessionCookieName) { + // NO-OP + } + + @Override + public boolean getUseHttpOnly() { + return false; + } + + @Override + public void setUseHttpOnly(boolean useHttpOnly) { + // NO-OP + } + + @Override + public boolean getUsePartitioned() { + return false; + } + + @Override + public void setUsePartitioned(boolean usePartitioned) { + // NO-OP + } + + @Override + public String getSessionCookieDomain() { + return null; + } + + @Override + public void setSessionCookieDomain(String sessionCookieDomain) { + // NO-OP + } + + @Override + public String getSessionCookiePath() { + return null; + } + + @Override + public void setSessionCookiePath(String sessionCookiePath) { + // NO-OP + } + + @Override + public boolean getSessionCookiePathUsesTrailingSlash() { + return false; + } + + @Override + public void setSessionCookiePathUsesTrailingSlash( + boolean sessionCookiePathUsesTrailingSlash) { + // NO-OP + } + + @Override + public boolean getCrossContext() { + return false; + } + + @Override + public String getAltDDName() { + return null; + } + + @Override + public void setAltDDName(String altDDName) { + // NO-OP + } + + @Override + public void setCrossContext(boolean crossContext) { + // NO-OP + } + + @Override + public boolean getDenyUncoveredHttpMethods() { + return false; + } + + @Override + public void setDenyUncoveredHttpMethods(boolean denyUncoveredHttpMethods) { + // NO-OP + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public void setDisplayName(String displayName) { + // NO-OP + } + + @Override + public boolean getDistributable() { + return false; + } + + @Override + public void setDistributable(boolean distributable) { + // NO-OP + } + + @Override + public String getDocBase() { + return null; + } + + @Override + public void setDocBase(String docBase) { + // NO-OP + } + + @Override + public String getEncodedPath() { + return null; + } + + @Override + public boolean getIgnoreAnnotations() { + return false; + } + + @Override + public void setIgnoreAnnotations(boolean ignoreAnnotations) { + // NO-OP + } + + @Override + public LoginConfig getLoginConfig() { + return null; + } + + @Override + public void setLoginConfig(LoginConfig config) { + // NO-OP + } + + @Override + public NamingResourcesImpl getNamingResources() { + return null; + } + + @Override + public void setNamingResources(NamingResourcesImpl namingResources) { + // NO-OP + } + + @Override + public String getPath() { + return null; + } + + @Override + public void setPath(String path) { + // NO-OP + } + + @Override + public String getPublicId() { + return null; + } + + @Override + public void setPublicId(String publicId) { + // NO-OP + } + + @Override + public boolean getReloadable() { + return false; + } + + @Override + public void setReloadable(boolean reloadable) { + // NO-OP + } + + @Override + public boolean getOverride() { + return false; + } + + @Override + public void setOverride(boolean override) { + // NO-OP + } + + @Override + public boolean getPrivileged() { + return false; + } + + @Override + public void setPrivileged(boolean privileged) { + // NO-OP + } + + private ServletContext servletContext; + @Override + public ServletContext getServletContext() { + return servletContext; + } + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Override + public int getSessionTimeout() { + return 0; + } + + @Override + public void setSessionTimeout(int timeout) { + // NO-OP + } + + @Override + public boolean getSwallowAbortedUploads() { + return false; + } + + @Override + public void setSwallowAbortedUploads(boolean swallowAbortedUploads) { + // NO-OP + } + + @Override + public boolean getSwallowOutput() { + return false; + } + + @Override + public void setSwallowOutput(boolean swallowOutput) { + // NO-OP + } + + @Override + public String getWrapperClass() { + return null; + } + + @Override + public void setWrapperClass(String wrapperClass) { + // NO-OP + } + + @Override + public boolean getXmlNamespaceAware() { + return false; + } + + @Override + public boolean getXmlValidation() { + return false; + } + + @Override + public void setXmlValidation(boolean xmlValidation) { + // NO-OP + } + + @Override + public boolean getXmlBlockExternal() { + return true; + } + + @Override + public void setXmlBlockExternal(boolean xmlBlockExternal) { + // NO-OP + } + + @Override + public boolean getTldValidation(){ + return false; + } + + @Override + public void setTldValidation(boolean tldValidation){ + // NO-OP + } + + @Override + public void setXmlNamespaceAware(boolean xmlNamespaceAware) { + // NO-OP + } + + @Override + public JarScanner getJarScanner() { + return null; + } + + @Override + public void setJarScanner(JarScanner jarScanner) { + // NO-OP + } + + @Override + public Authenticator getAuthenticator() { + return null; + } + + @Override + public void setLogEffectiveWebXml(boolean logEffectiveWebXml) { + // NO-OP + } + + @Override + public boolean getLogEffectiveWebXml() { + return false; + } + + @Override + public void addApplicationListener(String listener) { + // NO-OP + } + + @Override + public void addApplicationParameter(ApplicationParameter parameter) { + // NO-OP + } + + @Override + public void addErrorPage(ErrorPage errorPage) { + // NO-OP + } + + @Override + public void addFilterDef(FilterDef filterDef) { + // NO-OP + } + + @Override + public void addFilterMap(FilterMap filterMap) { + // NO-OP + } + + @Override + public void addFilterMapBefore(FilterMap filterMap) { + // NO-OP + } + + private final Map localEncodingMap = new ConcurrentHashMap<>(); + + @Override + public void addLocaleEncodingMappingParameter(String locale, String encoding) { + localEncodingMap.put(locale, encoding); + } + @Override + public String getCharset(Locale locale) { + // Match full language_country_variant first, then language_country, + // then language only + String charset = localEncodingMap.get(locale.toString()); + if (charset == null) { + charset = localEncodingMap.get(locale.getLanguage() + "_" + locale.getCountry()); + if (charset == null) { + charset = localEncodingMap.get(locale.getLanguage()); + } + } + return charset; + } + + @Override + public void addMimeMapping(String extension, String mimeType) { + // NO-OP + } + + @Override + public void addParameter(String name, String value) { + // NO-OP + } + + @Override + public void addRoleMapping(String role, String link) { + // NO-OP + } + + @Override + public void addServletMappingDecoded(String pattern, String name, + boolean jspWildcard) { + // NO-OP + } + + @Override + public void addWatchedResource(String name) { + // NO-OP + } + + @Override + public void addWelcomeFile(String name) { + // NO-OP + } + + @Override + public void addWrapperLifecycle(String listener) { + // NO-OP + } + + @Override + public void addWrapperListener(String listener) { + // NO-OP + } + + @Override + public InstanceManager createInstanceManager() { + return null; + } + + @Override + public Wrapper createWrapper() { + return null; + } + + @Override + public String[] findApplicationListeners() { + return null; + } + + @Override + public ApplicationParameter[] findApplicationParameters() { + return null; + } + + @Override + public ErrorPage findErrorPage(int errorCode) { + return null; + } + + @Override + public ErrorPage findErrorPage(Throwable exceptionType) { + return null; + } + + @Override + public ErrorPage[] findErrorPages() { + return null; + } + + @Override + public FilterDef findFilterDef(String filterName) { + return null; + } + + @Override + public FilterDef[] findFilterDefs() { + return null; + } + + @Override + public FilterMap[] findFilterMaps() { + return null; + } + + @Override + public String findMimeMapping(String extension) { + return null; + } + + @Override + public String[] findMimeMappings() { + return null; + } + + @Override + public String findParameter(String name) { + return null; + } + + @Override + public String[] findParameters() { + return null; + } + + @Override + public String findRoleMapping(String role) { + return null; + } + + @Override + public String findServletMapping(String pattern) { + return null; + } + + @Override + public String[] findServletMappings() { + return null; + } + + @Override + public String[] findWatchedResources() { + return null; + } + + @Override + public boolean findWelcomeFile(String name) { + return false; + } + + @Override + public String[] findWelcomeFiles() { + return null; + } + + @Override + public String[] findWrapperLifecycles() { + return null; + } + + @Override + public String[] findWrapperListeners() { + return null; + } + + @Override + public boolean fireRequestInitEvent(ServletRequest request) { + return false; + } + + @Override + public boolean fireRequestDestroyEvent(ServletRequest request) { + return false; + } + + @Override + public void reload() { + // NO-OP + } + + @Override + public void removeApplicationListener(String listener) { + // NO-OP + } + + @Override + public void removeApplicationParameter(String name) { + // NO-OP + } + + @Override + public void removeErrorPage(ErrorPage errorPage) { + // NO-OP + } + + @Override + public void removeFilterDef(FilterDef filterDef) { + // NO-OP + } + + @Override + public void removeFilterMap(FilterMap filterMap) { + // NO-OP + } + + @Override + public void removeMimeMapping(String extension) { + // NO-OP + } + + @Override + public void removeParameter(String name) { + // NO-OP + } + + @Override + public void removeRoleMapping(String role) { + // NO-OP + } + + @Override + public void removeServletMapping(String pattern) { + // NO-OP + } + + @Override + public void removeWatchedResource(String name) { + // NO-OP + } + + @Override + public void removeWelcomeFile(String name) { + // NO-OP + } + + @Override + public void removeWrapperLifecycle(String listener) { + // NO-OP + } + + @Override + public void removeWrapperListener(String listener) { + // NO-OP + } + + @Override + public String getRealPath(String path) { + return null; + } + + @Override + public int getEffectiveMajorVersion() { + return 0; + } + + @Override + public void setEffectiveMajorVersion(int major) { + // NO-OP + } + + @Override + public int getEffectiveMinorVersion() { + return 0; + } + + @Override + public void setEffectiveMinorVersion(int minor) { + // NO-OP + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return null; + } + + @Override + public void setJspConfigDescriptor(JspConfigDescriptor descriptor) { + // NO-OP + } + + @Override + public void addServletContainerInitializer(ServletContainerInitializer sci, + Set> classes) { + // NO-OP + } + + @Override + public boolean getPaused() { + return false; + } + + @Override + public boolean isServlet22() { + return false; + } + + @Override + public Set addServletSecurity(Dynamic registration, + ServletSecurityElement servletSecurityElement) { + return null; + } + + @Override + public void setResourceOnlyServlets(String resourceOnlyServlets) { + // NO-OP + } + + @Override + public String getResourceOnlyServlets() { + return null; + } + + @Override + public boolean isResourceOnlyServlet(String servletName) { + return false; + } + + @Override + public String getBaseName() { + return null; + } + + @Override + public void setWebappVersion(String webappVersion) { + // NO-OP + } + + @Override + public String getWebappVersion() { + return null; + } + + @Override + public void setFireRequestListenersOnForwards(boolean enable) { + // NO-OP + } + + @Override + public boolean getFireRequestListenersOnForwards() { + return false; + } + + @Override + public void setPreemptiveAuthentication(boolean enable) { + // NO-OP + } + + @Override + public boolean getPreemptiveAuthentication() { + return false; + } + + @Override + public void setSendRedirectBody(boolean enable) { + // NO-OP + } + + @Override + public boolean getSendRedirectBody() { + return false; + } + + @Override + public Loader getLoader() { + return null; + } + + @Override + public void setLoader(Loader loader) { + // NO-OP + } + + @Override + public WebResourceRoot getResources() { + return null; + } + + @Override + public void setResources(WebResourceRoot resources) { + // NO-OP + } + + @Override + public Manager getManager() { + return null; + } + + @Override + public void setManager(Manager manager) { + // NO-OP + } + + @Override + public void setAddWebinfClassesResources(boolean addWebinfClassesResources) { + // NO-OP + } + + @Override + public boolean getAddWebinfClassesResources() { + return false; + } + + @Override + public void addPostConstructMethod(String clazz, String method) { + // NO-OP + } + + @Override + public void addPreDestroyMethod(String clazz, String method) { + // NO-OP + } + + @Override + public void removePostConstructMethod(String clazz) { + // NO-OP + } + + @Override + public void removePreDestroyMethod(String clazz) { + // NO-OP + } + + @Override + public String findPostConstructMethod(String clazz) { + return null; + } + + @Override + public String findPreDestroyMethod(String clazz) { + return null; + } + + @Override + public Map findPostConstructMethods() { + return null; + } + + @Override + public Map findPreDestroyMethods() { + return null; + } + + @Override + public InstanceManager getInstanceManager() { + return null; + } + + @Override + public void setInstanceManager(InstanceManager instanceManager) { + // NO-OP + } + + @Override + public void setContainerSciFilter(String containerSciFilter) { /* NO-OP */ } + + @Override + public String getContainerSciFilter() { return null; } + + @Override + public ThreadBindingListener getThreadBindingListener() { return null; } + + @Override + public void setThreadBindingListener(ThreadBindingListener threadBindingListener) { /* NO-OP */ } + + @Override + public ClassLoader bind(boolean usePrivilegedAction, ClassLoader originalClassLoader) { + return null; + } + + @Override + public void unbind(boolean usePrivilegedAction, ClassLoader originalClassLoader) { + // NO-OP + } + + @Override + public Object getNamingToken() { return null; } + + @Override + public void setCookieProcessor(CookieProcessor cookieProcessor) { /* NO-OP */ } + + @Override + public CookieProcessor getCookieProcessor() { return null; } + + @Override + public void setValidateClientProvidedNewSessionId(boolean validateClientProvidedNewSessionId) { + //NO-OP + } + + @Override + public boolean getValidateClientProvidedNewSessionId() { return false; } + + @Override + public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) { + // NO-OP + } + + @Override + public boolean getMapperContextRootRedirectEnabled() { return false; } + + @Override + public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) { + // NO-OP + } + + @Override + public boolean getMapperDirectoryRedirectEnabled() { return false; } + + @Override + public void setUseRelativeRedirects(boolean useRelativeRedirects) { /* NO-OP */ } + @Override + public boolean getUseRelativeRedirects() { return true; } + + @Override + public void setDispatchersUseEncodedPaths(boolean dispatchersUseEncodedPaths) { /* NO-OP */ } + @Override + public boolean getDispatchersUseEncodedPaths() { return true; } + + @Override + public void setRequestCharacterEncoding(String encoding) { /* NO-OP */ } + @Override + public String getRequestCharacterEncoding() { return null; } + + @Override + public void setResponseCharacterEncoding(String encoding) { /* NO-OP */ } + @Override + public String getResponseCharacterEncoding() { return null; } + + @Override + public void setAllowMultipleLeadingForwardSlashInPath( + boolean allowMultipleLeadingForwardSlashInPath) { + // NO-OP + } + @Override + public boolean getAllowMultipleLeadingForwardSlashInPath() { return false; } + + @Override + public void incrementInProgressAsyncCount() { /* NO-OP */ } + @Override + public void decrementInProgressAsyncCount() { /* NO-OP */ } + + @Override + public void setCreateUploadTargets(boolean createUploadTargets) { /* NO-OP */} + @Override + public boolean getCreateUploadTargets() { return false; } + + @Override + public boolean getAlwaysAccessSession() { return false; } + @Override + public void setAlwaysAccessSession(boolean alwaysAccessSession) {} + + @Override + public boolean getContextGetResourceRequiresSlash() { return false; } + @Override + public void setContextGetResourceRequiresSlash(boolean contextGetResourceRequiresSlash) {} + + @Override + public boolean getDispatcherWrapsSameObject() { return false; } + @Override + public void setDispatcherWrapsSameObject(boolean dispatcherWrapsSameObject) {} + + @Override + public boolean getParallelAnnotationScanning() { return false; } + @Override + public void setParallelAnnotationScanning(boolean parallelAnnotationScanning) {} + + boolean useBloomFilterForArchives = false; + @Override + public boolean getUseBloomFilterForArchives() { + return useBloomFilterForArchives; + } + + @Override + public void setUseBloomFilterForArchives(boolean useBloomFilterForArchives) { + this.useBloomFilterForArchives = useBloomFilterForArchives; + } + + @Override + public boolean getSuspendWrappedResponseAfterForward() { return true; } + @Override + public void setSuspendWrappedResponseAfterForward(boolean suspendWrappedResponseAfterForward) {} + +} diff --git a/test/org/apache/tomcat/unittest/TesterCounter.java b/test/org/apache/tomcat/unittest/TesterCounter.java new file mode 100644 index 0000000..c5930c0 --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterCounter.java @@ -0,0 +1,40 @@ +/* + * 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.tomcat.unittest; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class TesterCounter { + + private static final Log log = LogFactory.getLog(TesterCounter.class); + + static { + log.info("TestCounter loaded by " + TesterCounter.class.getClassLoader() + + " in thread " + Thread.currentThread().getName()); + } + + private int count = 0; + + public void increment() { + count++; + } + + public int getCount() { + return count; + } +} \ No newline at end of file diff --git a/test/org/apache/tomcat/unittest/TesterData.java b/test/org/apache/tomcat/unittest/TesterData.java new file mode 100644 index 0000000..fbdc72a --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterData.java @@ -0,0 +1,37 @@ +/* + * 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.tomcat.unittest; + +public class TesterData { + + public static String string(char c, int count) { + StringBuilder sb = new StringBuilder(count); + for (int i = 0; i < count; i++) { + sb.append(c); + } + return sb.toString(); + } + + + public static String string(String str, int count) { + StringBuilder sb = new StringBuilder(str.length() * count); + for (int i = 0; i < count; i++) { + sb.append(str); + } + return sb.toString(); + } +} diff --git a/test/org/apache/tomcat/unittest/TesterHost.java b/test/org/apache/tomcat/unittest/TesterHost.java new file mode 100644 index 0000000..b194879 --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterHost.java @@ -0,0 +1,392 @@ +/* + * 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.tomcat.unittest; + +import java.beans.PropertyChangeListener; +import java.io.File; +import java.util.concurrent.ExecutorService; +import java.util.regex.Pattern; + +import javax.management.ObjectName; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Cluster; +import org.apache.catalina.Container; +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Host; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Pipeline; +import org.apache.catalina.Realm; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.juli.logging.Log; + +public class TesterHost implements Host { + + @Override + public Log getLogger() { + return null; + } + + @Override + public String getLogName() { + return null; + } + + @Override + public ObjectName getObjectName() { + return null; + } + + @Override + public String getDomain() { + return null; + } + + @Override + public String getMBeanKeyProperties() { + return null; + } + + @Override + public Pipeline getPipeline() { + return null; + } + + @Override + public Cluster getCluster() { + return null; + } + + @Override + public void setCluster(Cluster cluster) { + // NO-OP + } + + @Override + public int getBackgroundProcessorDelay() { + return 0; + } + + @Override + public void setBackgroundProcessorDelay(int delay) { + // NO-OP + } + + private String name = "TestHost"; + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public Container getParent() { + return null; + } + + @Override + public void setParent(Container container) { + // NO-OP + } + + @Override + public ClassLoader getParentClassLoader() { + return null; + } + + @Override + public void setParentClassLoader(ClassLoader parent) { + // NO-OP + } + + @Override + public Realm getRealm() { + return null; + } + + @Override + public void setRealm(Realm realm) { + // NO-OP + } + + @Override + public void backgroundProcess() { + // NO-OP + } + + @Override + public void addChild(Container child) { + // NO-OP + } + + @Override + public void addContainerListener(ContainerListener listener) { + // NO-OP + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + // NO-OP + } + + @Override + public Container findChild(String name) { + return null; + } + + @Override + public Container[] findChildren() { + return null; + } + + @Override + public ContainerListener[] findContainerListeners() { + return null; + } + + @Override + public void removeChild(Container child) { + // NO-OP + } + + @Override + public void removeContainerListener(ContainerListener listener) { + // NO-OP + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + // NO-OP + } + + @Override + public void fireContainerEvent(String type, Object data) { + // NO-OP + } + + @Override + public void logAccess(Request request, Response response, long time, boolean useDefault) { + // NO-OP + } + + @Override + public AccessLog getAccessLog() { + return null; + } + + @Override + public int getStartStopThreads() { + return 0; + } + + @Override + public void setStartStopThreads(int startStopThreads) { + // NO-OP + } + + @Override + public File getCatalinaBase() { + return null; + } + + @Override + public File getCatalinaHome() { + return null; + } + + @Override + public void addLifecycleListener(LifecycleListener listener) { + // NO-OP + } + + @Override + public LifecycleListener[] findLifecycleListeners() { + return null; + } + + @Override + public void removeLifecycleListener(LifecycleListener listener) { + // NO-OP + } + + @Override + public void init() throws LifecycleException { + // NO-OP + } + + @Override + public void start() throws LifecycleException { + // NO-OP + } + + @Override + public void stop() throws LifecycleException { + // NO-OP + } + + @Override + public void destroy() throws LifecycleException { + // NO-OP + } + + @Override + public LifecycleState getState() { + return null; + } + + @Override + public String getStateName() { + return null; + } + + @Override + public String getXmlBase() { + return null; + } + + @Override + public void setXmlBase(String xmlBase) { + // NO-OP + } + + @Override + public File getConfigBaseFile() { + return null; + } + + @Override + public String getAppBase() { + return null; + } + + @Override + public File getAppBaseFile() { + return null; + } + + @Override + public void setAppBase(String appBase) { + // NO-OP + } + + @Override + public String getLegacyAppBase() { + return null; + } + + @Override + public File getLegacyAppBaseFile() { + return null; + } + + @Override + public void setLegacyAppBase(String legacyAppBase) { + // NO-OP + } + + @Override + public boolean getAutoDeploy() { + return false; + } + + @Override + public void setAutoDeploy(boolean autoDeploy) { + // NO-OP + } + + @Override + public String getConfigClass() { + return null; + } + + @Override + public void setConfigClass(String configClass) { + // NO-OP + } + + @Override + public boolean getDeployOnStartup() { + return false; + } + + @Override + public void setDeployOnStartup(boolean deployOnStartup) { + // NO-OP + } + + @Override + public String getDeployIgnore() { + return null; + } + + @Override + public Pattern getDeployIgnorePattern() { + return null; + } + + @Override + public void setDeployIgnore(String deployIgnore) { + // NO-OP + } + + @Override + public ExecutorService getStartStopExecutor() { + return null; + } + + @Override + public boolean getCreateDirs() { + return false; + } + + @Override + public void setCreateDirs(boolean createDirs) { + // NO-OP + } + + @Override + public boolean getUndeployOldVersions() { + return false; + } + + @Override + public void setUndeployOldVersions(boolean undeployOldVersions) { + // NO-OP + } + + @Override + public void addAlias(String alias) { + // NO-OP + } + + @Override + public String[] findAliases() { + return null; + } + + @Override + public void removeAlias(String alias) { + // NO-OP + } +} diff --git a/test/org/apache/tomcat/unittest/TesterLeakingServlet1.java b/test/org/apache/tomcat/unittest/TesterLeakingServlet1.java new file mode 100644 index 0000000..b928911 --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterLeakingServlet1.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.unittest; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class TesterLeakingServlet1 extends HttpServlet { + + private static final Log log = LogFactory.getLog(TesterLeakingServlet1.class); + + private static final long serialVersionUID = 1L; + + private static ThreadLocal myThreadLocal = new ThreadLocal<>(); + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, + IOException { + + TesterCounter counter = myThreadLocal.get(); + if (counter == null) { + log.info("Adding thread local to thread " + Thread.currentThread().getName()); + counter = new TesterCounter(); + myThreadLocal.set(counter); + } + + counter.increment(); + response.getWriter().println( + "The current thread served this servlet " + + counter.getCount() + " times"); + } +} \ No newline at end of file diff --git a/test/org/apache/tomcat/unittest/TesterLeakingServlet2.java b/test/org/apache/tomcat/unittest/TesterLeakingServlet2.java new file mode 100644 index 0000000..b0e110b --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterLeakingServlet2.java @@ -0,0 +1,58 @@ +/* + * 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.tomcat.unittest; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + + +public class TesterLeakingServlet2 extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final Log log = LogFactory.getLog(TesterLeakingServlet2.class); + + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, + IOException { + + List counterList = TesterThreadScopedHolder.getFromHolder(); + TesterCounter counter; + if (counterList == null) { + log.info("Adding thread local to thread " + Thread.currentThread().getName()); + counter = new TesterCounter(); + TesterThreadScopedHolder.saveInHolder(Arrays.asList(counter)); + } else { + counter = counterList.get(0); + } + + counter.increment(); + response.getWriter().println( + "The current thread served this servlet " + + counter.getCount() + " times"); + } +} \ No newline at end of file diff --git a/test/org/apache/tomcat/unittest/TesterLogValidationFilter.java b/test/org/apache/tomcat/unittest/TesterLogValidationFilter.java new file mode 100644 index 0000000..e6f43c1 --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterLogValidationFilter.java @@ -0,0 +1,90 @@ +/* + * 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.tomcat.unittest; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; + +public class TesterLogValidationFilter implements Filter { + + private final Level targetLevel; + private final String targetMessage; + private final String targetThrowableString; + private final AtomicInteger messageCount = new AtomicInteger(0); + + + public TesterLogValidationFilter(Level targetLevel, String targetMessage, + String targetThrowableString) { + this.targetLevel = targetLevel; + this.targetMessage = targetMessage; + this.targetThrowableString = targetThrowableString; + } + + + public int getMessageCount() { + return messageCount.get(); + } + + + @Override + public boolean isLoggable(LogRecord record) { + if (targetLevel != null) { + Level level = record.getLevel(); + if (targetLevel != level) { + return true; + } + } + + if (targetMessage != null) { + String msg = record.getMessage(); + if (msg == null || !msg.contains(targetMessage)) { + return true; + } + } + + if (targetThrowableString != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + record.getThrown().printStackTrace(pw); + pw.close(); + String throwableString = sw.toString(); + if (!throwableString.contains(targetThrowableString)) { + return true; + } + + + } + + messageCount.incrementAndGet(); + + return true; + } + + + public static TesterLogValidationFilter add(Level targetLevel, String targetMessage, + String targetThrowableString, String loggerName) { + TesterLogValidationFilter f = new TesterLogValidationFilter(targetLevel, targetMessage, + targetThrowableString); + LogManager.getLogManager().getLogger(loggerName).setFilter(f); + return f; + } +} diff --git a/test/org/apache/tomcat/unittest/TesterRequest.java b/test/org/apache/tomcat/unittest/TesterRequest.java new file mode 100644 index 0000000..cdecdff --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterRequest.java @@ -0,0 +1,142 @@ +/* + * 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.tomcat.unittest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.SessionTrackingMode; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Request; +import org.apache.catalina.session.StandardSession; + +public class TesterRequest extends Request { + + private final TesterContext context; + private final TesterServletContext servletContext; + + + public TesterRequest() { + this(false); + } + + + public TesterRequest(boolean withSession) { + super(null); + context = new TesterContext(); + servletContext = new TesterServletContext(); + context.setServletContext(servletContext); + if (withSession) { + Set modes = new HashSet<>(); + modes.add(SessionTrackingMode.URL); + modes.add(SessionTrackingMode.COOKIE); + servletContext.setSessionTrackingModes(modes); + session = new StandardSession(null); + session.setId("1234", false); + session.setValid(true); + } + } + + + @Override + public String getScheme() { + return "http"; + } + + @Override + public String getServerName() { + return "localhost"; + } + + @Override + public int getServerPort() { + return 8080; + } + + + @Override + public String getRequestURI() { + return "/level1/level2/foo.html"; + } + + @Override + public String getDecodedRequestURI() { + // Decoding not required + return getRequestURI(); + } + + + @Override + public Context getContext() { + return context; + } + + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + + private String method; + public void setMethod(String method) { + this.method = method; + } + @Override + public String getMethod() { + return method; + } + + private final Map> headers = new HashMap<>(); + public void addHeader(String name, String value) { + headers.computeIfAbsent(name, k -> new ArrayList<>()).add(value); + } + @Override + public String getHeader(String name) { + List values = headers.get(name); + if (values == null || values.size() == 0) { + return null; + } + return values.get(0); + } + @Override + public Enumeration getHeaders(String name) { + List values = headers.get(name); + if (values == null || values.size() == 0) { + return Collections.emptyEnumeration(); + } + return Collections.enumeration(headers.get(name)); + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public String getRemoteAddr() { + return "127.0.0.1"; + } +} diff --git a/test/org/apache/tomcat/unittest/TesterResponse.java b/test/org/apache/tomcat/unittest/TesterResponse.java new file mode 100644 index 0000000..59f79ee --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterResponse.java @@ -0,0 +1,73 @@ +/* + * 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.tomcat.unittest; + +import java.io.IOException; + +import org.apache.catalina.connector.Response; + +/** + * Minimal changes to Response to enable tests that use this class to operate + * correctly. + */ +public class TesterResponse extends Response { + + @Override + public boolean isCommitted() { + return false; + } + + @Override + public void sendError(int status, String message) throws IOException { + // NO-OP by default. + /* + System.out.println("TesterResponse.sendError(" + status + ", \"" + + message + "\")"); + */ + } + + @Override + public void resetBuffer(boolean resetWriterStreamFlags) { + // NO-OP by default. + // There is no buffer created for this test object since no test depends + // on one being present or on this method resetting it. + } + + @Override + public org.apache.coyote.Response getCoyoteResponse() { + // Lazy init + if (super.getCoyoteResponse() == null) { + this.coyoteResponse = new org.apache.coyote.Response(); + } + return super.getCoyoteResponse(); + } + + @Override + public void setSuspended(boolean suspended) { + // NO-OP by default. + // There is no buffer created for this test object since no test depends + // on one being present or on this method suspending it. + } + + @Override + public void reset() { + // Minimal implementation for tests that avoids using OutputBuffer + if (super.getCoyoteResponse() != null) { + super.getCoyoteResponse().reset(); + } + } +} diff --git a/test/org/apache/tomcat/unittest/TesterServletContext.java b/test/org/apache/tomcat/unittest/TesterServletContext.java new file mode 100644 index 0000000..4dadfeb --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterServletContext.java @@ -0,0 +1,352 @@ +/* + * 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.tomcat.unittest; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +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.SessionCookieConfig; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.descriptor.JspConfigDescriptor; + +import org.apache.catalina.core.ApplicationFilterRegistration; +import org.apache.tomcat.util.descriptor.web.FilterDef; + +public class TesterServletContext implements ServletContext { + + /** + * {@inheritDoc} + *

    + * This test implementation is hard coded to return an empty String. + */ + @Override + public String getContextPath() { + return ""; + } + + /** + * {@inheritDoc} + *

    + * This test implementation is hard coded to return an empty Set. + */ + @Override + public Set getResourcePaths(String path) { + return Collections.emptySet(); + } + + /** + * {@inheritDoc} + *

    + * This test implementation is hard coded to return the class loader that + * loaded this class. + */ + @Override + public ClassLoader getClassLoader() { + return getClass().getClassLoader(); + } + + @Override + public ServletContext getContext(String uripath) { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getMajorVersion() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getMinorVersion() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getMimeType(String file) { + throw new RuntimeException("Not implemented"); + } + + @Override + public URL getResource(String path) throws MalformedURLException { + return null; + } + + @Override + public InputStream getResourceAsStream(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + + throw new RuntimeException("Not implemented"); + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + + throw new RuntimeException("Not implemented"); + } + + @Override + public void log(String msg) { + // NOOP + } + + @Override + public void log(String message, Throwable throwable) { + // NOOP + } + + @Override + public String getRealPath(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getServerInfo() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getInitParameter(String name) { + return null; + } + + @Override + public Enumeration getInitParameterNames() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Object getAttribute(String name) { + // Used by websockets + return null; + } + + @Override + public Enumeration getAttributeNames() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setAttribute(String name, Object object) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void removeAttribute(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getServletContextName() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getEffectiveMajorVersion() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getEffectiveMinorVersion() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean setInitParameter(String name, String value) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Dynamic addServlet(String servletName, String className) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Dynamic addServlet(String servletName, Servlet servlet) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Dynamic addServlet(String servletName, + Class servletClass) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Dynamic addJspFile(String jspName, String jspFile) { + throw new RuntimeException("Not implemented"); + } + + @Override + public T createServlet(Class c) + throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public ServletRegistration getServletRegistration(String servletName) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Map getServletRegistrations() { + throw new RuntimeException("Not implemented"); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) { + throw new RuntimeException("Not implemented"); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + return new ApplicationFilterRegistration(new FilterDef(), new TesterContext()); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Class filterClass) { + return new ApplicationFilterRegistration(new FilterDef(), new TesterContext()); + } + + @Override + public T createFilter(Class c) + throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Map getFilterRegistrations() { + throw new RuntimeException("Not implemented"); + } + + private SessionCookieConfig sessionCookieConfig = new TesterSessionCookieConfig(); + @Override + public SessionCookieConfig getSessionCookieConfig() { + return sessionCookieConfig; + } + + private final Set sessionTrackingModes = new HashSet<>(); + @Override + public void setSessionTrackingModes( + Set sessionTrackingModes) { + this.sessionTrackingModes.clear(); + this.sessionTrackingModes.addAll(sessionTrackingModes); + } + + @Override + public Set getDefaultSessionTrackingModes() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Set getEffectiveSessionTrackingModes() { + return sessionTrackingModes; + } + + @Override + public void addListener(String className) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addListener(T t) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addListener(Class listenerClass) { + throw new RuntimeException("Not implemented"); + } + + @Override + public T createListener(Class c) + throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void declareRoles(String... roleNames) { + throw new RuntimeException("Not implemented"); + } + + /** + * {@inheritDoc} + *

    + * This test implementation is hard coded to return localhost. + */ + @Override + public String getVirtualServerName() { + return "localhost"; + } + + @Override + public int getSessionTimeout() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setSessionTimeout(int sessionTimeout) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRequestCharacterEncoding() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setRequestCharacterEncoding(String encoding) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getResponseCharacterEncoding() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setResponseCharacterEncoding(String encoding) { + throw new RuntimeException("Not implemented"); + } +} diff --git a/test/org/apache/tomcat/unittest/TesterSessionCookieConfig.java b/test/org/apache/tomcat/unittest/TesterSessionCookieConfig.java new file mode 100644 index 0000000..afae9b1 --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterSessionCookieConfig.java @@ -0,0 +1,110 @@ +/* + * 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.tomcat.unittest; + +import java.util.Map; + +import jakarta.servlet.SessionCookieConfig; + +public class TesterSessionCookieConfig implements SessionCookieConfig { + + private String name; + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setDomain(String domain) { + throw new UnsupportedOperationException(); + } + + @Override + public String getDomain() { + throw new UnsupportedOperationException(); + } + + @Override + public void setPath(String path) { + throw new UnsupportedOperationException(); + } + + @Override + public String getPath() { + throw new UnsupportedOperationException(); + } + + @Override + public void setComment(String comment) { + throw new UnsupportedOperationException(); + } + + @Override + public String getComment() { + throw new UnsupportedOperationException(); + } + + @Override + public void setHttpOnly(boolean httpOnly) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isHttpOnly() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSecure(boolean secure) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSecure() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMaxAge(int MaxAge) { + throw new UnsupportedOperationException(); + } + + @Override + public int getMaxAge() { + throw new UnsupportedOperationException(); + } + + @Override + public void setAttribute(String name, String value) { + throw new UnsupportedOperationException(); + } + + @Override + public String getAttribute(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Map getAttributes() { + throw new UnsupportedOperationException(); + } +} diff --git a/test/org/apache/tomcat/unittest/TesterThreadScopedHolder.java b/test/org/apache/tomcat/unittest/TesterThreadScopedHolder.java new file mode 100644 index 0000000..8ae8568 --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterThreadScopedHolder.java @@ -0,0 +1,32 @@ +/* + * 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.tomcat.unittest; + +import java.util.List; + +public class TesterThreadScopedHolder { + private static final ThreadLocal> threadLocal = + new ThreadLocal<>(); + + public static void saveInHolder(List o) { + threadLocal.set(o); + } + + public static List getFromHolder() { + return threadLocal.get(); + } +} \ No newline at end of file diff --git a/test/org/apache/tomcat/unittest/TesterThreadedPerformance.java b/test/org/apache/tomcat/unittest/TesterThreadedPerformance.java new file mode 100644 index 0000000..9e0ea4c --- /dev/null +++ b/test/org/apache/tomcat/unittest/TesterThreadedPerformance.java @@ -0,0 +1,81 @@ +/* + * 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.tomcat.unittest; + +import java.util.function.IntConsumer; +import java.util.function.Supplier; + +/* + * Support class for threaded performance tests. + */ +public class TesterThreadedPerformance { + + private final int threadCount; + private final int iterationCount; + private final Supplier testInstanceSupplier; + + + public TesterThreadedPerformance(int threadCount, int iterationCount, + Supplier testInstanceSupplier) { + this.threadCount = threadCount; + this.iterationCount = iterationCount; + this.testInstanceSupplier = testInstanceSupplier; + } + + + public long doTest() throws InterruptedException { + long start = System.nanoTime(); + + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + IntConsumer testTarget = testInstanceSupplier.get(); + threads[i] = new Thread( + new TesterThreadedPerformanceRunnable(testTarget, iterationCount)); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + + return System.nanoTime() - start; + } + + + private static class TesterThreadedPerformanceRunnable implements Runnable { + + private final IntConsumer testTarget; + private final int iterationCount; + + TesterThreadedPerformanceRunnable(IntConsumer testTarget, int iterationCount) { + this.testTarget = testTarget; + this.iterationCount = iterationCount; + } + + + @Override + public void run() { + for (int i = 0; i < iterationCount; i++) { + testTarget.accept(i); + } + } + } +} diff --git a/test/org/apache/tomcat/unittest/tags/Bug53545.java b/test/org/apache/tomcat/unittest/tags/Bug53545.java new file mode 100644 index 0000000..18685b0 --- /dev/null +++ b/test/org/apache/tomcat/unittest/tags/Bug53545.java @@ -0,0 +1,23 @@ +/* + * 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.tomcat.unittest.tags; + +import jakarta.servlet.jsp.tagext.BodyTagSupport; + +public class Bug53545 extends BodyTagSupport { + private static final long serialVersionUID = 1L; +} diff --git a/test/org/apache/tomcat/util/TestIntrospectionUtils.java b/test/org/apache/tomcat/util/TestIntrospectionUtils.java new file mode 100644 index 0000000..73ea86a --- /dev/null +++ b/test/org/apache/tomcat/util/TestIntrospectionUtils.java @@ -0,0 +1,159 @@ +/* + * 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.tomcat.util; + +import java.util.Properties; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.core.StandardContext; + +public class TestIntrospectionUtils { + + // Test for all the classes and interfaces in StandardContext's type hierarchy + + @Test + public void testIsInstanceStandardContext01() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "org.apache.catalina.core.StandardContext")); + } + + + @Test + public void testIsInstanceStandardContext02() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "org.apache.catalina.util.LifecycleMBeanBase")); + } + + + @Test + public void testIsInstanceStandardContext03() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "org.apache.catalina.util.LifecycleBase")); + } + + + @Test + public void testIsInstanceStandardContext04() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "java.lang.Object")); + } + + + @Test + public void testIsInstanceStandardContext05() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "org.apache.catalina.Lifecycle")); + } + + + @Test + public void testIsInstanceStandardContext06() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "org.apache.catalina.JmxEnabled")); + } + + + @Test + public void testIsInstanceStandardContext07() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "javax.management.MBeanRegistration")); + } + + + @Test + public void testIsInstanceStandardContext08() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "org.apache.catalina.Container")); + } + + + @Test + public void testIsInstanceStandardContext09() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "org.apache.tomcat.ContextBind")); + } + + + @Test + public void testIsInstanceStandardContext10() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "javax.management.NotificationEmitter")); + } + + + @Test + public void testIsInstanceStandardContext11() { + Assert.assertTrue(IntrospectionUtils.isInstance( + StandardContext.class, "javax.management.NotificationBroadcaster")); + } + + + // And one to check that non-matches return false + + @Test + public void testIsInstanceStandardContext12() { + Assert.assertFalse(IntrospectionUtils.isInstance( + StandardContext.class, "com.example.Other")); + } + + + @Test + public void testReplaceProperties() { + Properties properties = new Properties(); + + Assert.assertEquals("no-expression", IntrospectionUtils.replaceProperties( + "no-expression", properties, null, null)); + + Assert.assertEquals("${normal}", IntrospectionUtils.replaceProperties( + "${normal}", properties, null, null)); + + properties.setProperty("normal", "value1"); + Assert.assertEquals("value1", IntrospectionUtils.replaceProperties( + "${normal}", properties, null, null)); + + Assert.assertEquals("abcvalue1xyz", IntrospectionUtils.replaceProperties( + "abc${normal}xyz", properties, null, null)); + + properties.setProperty("prop_with:-colon", "value2"); + Assert.assertEquals("value2", IntrospectionUtils.replaceProperties( + "${prop_with:-colon}", properties, null, null)); + + Assert.assertEquals("value1", IntrospectionUtils.replaceProperties( + "${normal:-default}", properties, null, null)); + + properties.remove("normal"); + Assert.assertEquals("default", IntrospectionUtils.replaceProperties( + "${normal:-default}", properties, null, null)); + + Assert.assertEquals("abc${normal}xyz", IntrospectionUtils.replaceProperties( + "abc${normal}xyz", properties, null, null)); + + properties.setProperty("my.ajp.port", "8009"); + properties.setProperty("tomcat.ajp.port", "${my.ajp.port}"); + Assert.assertEquals("8009", IntrospectionUtils.replaceProperties( + "${tomcat.ajp.port}", properties, null, null)); + + } + @Test + public void testReplacePropertiesRecursively() { + Properties properties = new Properties(); + properties.setProperty("replaceMe", "something ${replaceMe}"); + IntrospectionUtils.replaceProperties("${replaceMe}", properties, null, null); + } +} diff --git a/test/org/apache/tomcat/util/bcel/TesterPerformance.java b/test/org/apache/tomcat/util/bcel/TesterPerformance.java new file mode 100644 index 0000000..de78557 --- /dev/null +++ b/test/org/apache/tomcat/util/bcel/TesterPerformance.java @@ -0,0 +1,82 @@ +/* + * 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.tomcat.util.bcel; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.Jar; +import org.apache.tomcat.util.bcel.classfile.ClassParser; +import org.apache.tomcat.util.scan.JarFactory; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + * + * The test also requires that the Jira lib directory has been extracted from a Jira install and copied to /tmp. + */ +public class TesterPerformance { + + private static final String JAR_LOCATION = "/tmp/jira-libs"; + + @Test + public void testClassParserPerformance() throws IOException { + File libDir = new File(JAR_LOCATION); + String[] libs = libDir.list(); + + Assert.assertNotNull(libs); + + Set jarURLs = new HashSet<>(); + + for (String lib : libs) { + if (!lib.toLowerCase(Locale.ENGLISH).endsWith(".jar")) { + continue; + } + jarURLs.add(new URL("jar:" + new File (libDir, lib).toURI().toURL().toExternalForm() + "!/")); + } + + long duration = 0; + + for (URL jarURL : jarURLs) { + try (Jar jar = JarFactory.newInstance(jarURL)) { + jar.nextEntry(); + String jarEntryName = jar.getEntryName(); + while (jarEntryName != null) { + if (jarEntryName.endsWith(".class")) { + InputStream is = jar.getEntryInputStream(); + long start = System.nanoTime(); + ClassParser cp = new ClassParser(is); + cp.parse(); + duration += System.nanoTime() - start; + } + jar.nextEntry(); + jarEntryName = jar.getEntryName(); + } + } + } + + System.out.println("ClassParser performance test took: " + duration + " ns"); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestAscii.java b/test/org/apache/tomcat/util/buf/TestAscii.java new file mode 100644 index 0000000..25cc260 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestAscii.java @@ -0,0 +1,65 @@ +/* + * 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.tomcat.util.buf; + +import java.math.BigInteger; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAscii { + + @Test + public void testParseLong1() { + String value = "9223372036854775807"; // Long.MAX_VALUE + byte[] bytes = value.getBytes(); + long result = Ascii.parseLong(bytes, 0, bytes.length); + Assert.assertEquals(value, String.valueOf(result)); + } + + @Test(expected = NumberFormatException.class) + public void testParseLong2() { + byte[] bytes = "9223372036854775808".getBytes(); // Long.MAX_VALUE + 1 + long result = Ascii.parseLong(bytes, 0, bytes.length); + Assert.fail("NumberFormatException expected, got: " + result); + } + + @Test(expected = NumberFormatException.class) + public void testParseLong3() { + byte[] bytes = "9223372036854775810".getBytes(); // Long.MAX_VALUE + 3 + long result = Ascii.parseLong(bytes, 0, bytes.length); + Assert.fail("NumberFormatException expected, got: " + result); + } + + @Test(expected = NumberFormatException.class) + public void testParseLong4() { + BigInteger x = BigInteger.valueOf(5000000000L).shiftLeft(32); + byte[] bytes = String.valueOf(x).getBytes(); + long result = Ascii.parseLong(bytes, 0, bytes.length); + Assert.fail("NumberFormatException expected, got: " + result); + } + + @Test + public void testParseLong5() { + String value = "9223372036854775806"; // Long.MAX_VALUE - 1 + byte[] bytes = value.getBytes(); + long result = Ascii.parseLong(bytes, 0, bytes.length); + Assert.assertEquals(value, String.valueOf(result)); + } + + +} diff --git a/test/org/apache/tomcat/util/buf/TestB2CConverter.java b/test/org/apache/tomcat/util/buf/TestB2CConverter.java new file mode 100644 index 0000000..8c30591 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestB2CConverter.java @@ -0,0 +1,143 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.charset.Charset; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import org.junit.Assert; +import org.junit.Test; + +public class TestB2CConverter { + + private static final byte[] UTF16_MESSAGE = + new byte[] {-2, -1, 0, 65, 0, 66, 0, 67}; + + private static final byte[] UTF8_INVALID = new byte[] {-8, -69, -73, -77}; + + private static final byte[] UTF8_PARTIAL = new byte[] {-50}; + + @Test + public void testSingleMessage() throws Exception { + testMessages(1); + } + + @Test + public void testTwoMessage() throws Exception { + testMessages(2); + } + + @Test + public void testManyMessage() throws Exception { + testMessages(10); + } + + private void testMessages(int msgCount) throws Exception { + B2CConverter conv = new B2CConverter(StandardCharsets.UTF_16); + + ByteChunk bc = new ByteChunk(); + CharChunk cc = new CharChunk(32); + + + for (int i = 0; i < msgCount; i++) { + bc.append(UTF16_MESSAGE, 0, UTF16_MESSAGE.length); + conv.convert(bc, cc, true); + Assert.assertEquals("ABC", cc.toString()); + bc.recycle(); + cc.recycle(); + conv.recycle(); + } + + System.out.println(cc); + } + + @Test + public void testLeftoverSize() { + float maxLeftover = 0; + String charsetName = "UNSET"; + for (Charset charset : Charset.availableCharsets().values()) { + float leftover; + if (charset.name().toLowerCase(Locale.ENGLISH).startsWith("x-")) { + // Non-standard charset that browsers won't be using + // Likely something used internally by the JRE + continue; + } + try { + leftover = charset.newEncoder().maxBytesPerChar(); + } catch (UnsupportedOperationException uoe) { + // Skip it + continue; + } + if (leftover > maxLeftover) { + maxLeftover = leftover; + charsetName = charset.name(); + } + } + Assert.assertTrue("Limit needs to be at least " + maxLeftover + + " (used in charset '" + charsetName + "')", + maxLeftover <= B2CConverter.LEFTOVER_SIZE); + } + + @Test(expected=MalformedInputException.class) + public void testBug54602a() throws Exception { + // Check invalid input is rejected straight away + B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8); + ByteChunk bc = new ByteChunk(); + CharChunk cc = new CharChunk(); + + bc.append(UTF8_INVALID, 0, UTF8_INVALID.length); + cc.allocate(bc.getLength(), -1); + + conv.convert(bc, cc, false); + } + + @Test(expected=MalformedInputException.class) + public void testBug54602b() throws Exception { + // Check partial input is rejected + B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8); + ByteChunk bc = new ByteChunk(); + CharChunk cc = new CharChunk(); + + bc.append(UTF8_PARTIAL, 0, UTF8_PARTIAL.length); + cc.allocate(bc.getLength(), -1); + + conv.convert(bc, cc, true); + } + + @Test + public void testBug54602c() throws Exception { + // Check partial input is rejected once it is known to be all available + B2CConverter conv = new B2CConverter(StandardCharsets.UTF_8); + ByteChunk bc = new ByteChunk(); + CharChunk cc = new CharChunk(); + + bc.append(UTF8_PARTIAL, 0, UTF8_PARTIAL.length); + cc.allocate(bc.getLength(), -1); + + conv.convert(bc, cc, false); + + Exception e = null; + try { + conv.convert(bc, cc, true); + } catch (MalformedInputException mie) { + e = mie; + } + Assert.assertNotNull(e); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestByteChunk.java b/test/org/apache/tomcat/util/buf/TestByteChunk.java new file mode 100644 index 0000000..ce82342 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestByteChunk.java @@ -0,0 +1,182 @@ +/* + * 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.tomcat.util.buf; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test cases for {@link ByteChunk}. + */ +public class TestByteChunk { + + @Test + public void testConvertToBytes() throws UnsupportedEncodingException { + String string = "HTTP/1.1 100 \r\n\r\n"; + byte[] bytes = ByteChunk.convertToBytes(string); + byte[] expected = string.getBytes("ISO-8859-1"); + Assert.assertTrue(Arrays.equals(bytes, expected)); + } + + + /* + * Test for {@code findByte} vs. {@code indexOf} methods difference. + * + *

    + * As discussed in the "Re: r944918" thread on dev@, {@code + * ByteChunk.indexOf()} works for 0-127 ASCII chars only, and cannot find + * any chars outside of the range. {@code ByteChunk.findByte()} works for + * any ISO-8859-1 chars. + */ + @Test + public void testFindByte() throws UnsupportedEncodingException { + // 0xa0 = 160 =   character + byte[] bytes = "Hello\u00a0world".getBytes("ISO-8859-1"); + final int len = bytes.length; + + // indexOf() does not work outside of 0-127 + Assert.assertEquals(5, ByteChunk.findByte(bytes, 0, len, (byte) '\u00a0')); + Assert.assertEquals(-1, ByteChunk.indexOf(bytes, 0, len, '\u00a0')); + + Assert.assertEquals(0, ByteChunk.findByte(bytes, 0, len, (byte) 'H')); + Assert.assertEquals(0, ByteChunk.indexOf(bytes, 0, len, 'H')); + + Assert.assertEquals(len - 1, ByteChunk.findByte(bytes, 0, len, (byte) 'd')); + Assert.assertEquals(len - 1, ByteChunk.indexOf(bytes, 0, len, 'd')); + + Assert.assertEquals(-1, ByteChunk.findByte(bytes, 0, len, (byte) 'x')); + Assert.assertEquals(-1, ByteChunk.indexOf(bytes, 0, len, 'x')); + + Assert.assertEquals(7, ByteChunk.findByte(bytes, 5, len, (byte) 'o')); + Assert.assertEquals(7, ByteChunk.indexOf(bytes, 5, len, 'o')); + + Assert.assertEquals(-1, ByteChunk.findByte(bytes, 2, 5, (byte) 'w')); + Assert.assertEquals(-1, ByteChunk.indexOf(bytes, 5, 5, 'w')); + } + + + @Test + public void testIndexOf_Char() throws UnsupportedEncodingException { + byte[] bytes = "Hello\u00a0world".getBytes("ISO-8859-1"); + final int len = bytes.length; + + ByteChunk bc = new ByteChunk(); + bc.setBytes(bytes, 0, len); + + Assert.assertEquals(0, bc.indexOf('H', 0)); + Assert.assertEquals(6, bc.indexOf('w', 0)); + + // Does not work outside of 0-127 + Assert.assertEquals(-1, bc.indexOf('\u00a0', 0)); + + bc.setBytes(bytes, 6, 5); + Assert.assertEquals(1, bc.indexOf('o', 0)); + + bc.setBytes(bytes, 6, 2); + Assert.assertEquals(0, bc.indexOf('w', 0)); + Assert.assertEquals(-1, bc.indexOf('d', 0)); + } + + + @Test + public void testIndexOf_String() throws UnsupportedEncodingException { + byte[] bytes = "Hello\u00a0world".getBytes("ISO-8859-1"); + final int len = bytes.length; + + ByteChunk bc = new ByteChunk(); + bc.setBytes(bytes, 0, len); + + Assert.assertEquals(0, bc.indexOf("Hello", 0, "Hello".length(), 0)); + Assert.assertEquals(2, bc.indexOf("ll", 0, 2, 0)); + Assert.assertEquals(2, bc.indexOf("Hello", 2, 2, 0)); + + Assert.assertEquals(7, bc.indexOf("o", 0, 1, 5)); + + // Does not work outside of 0-127 + Assert.assertEquals(-1, bc.indexOf("\u00a0", 0, 1, 0)); + + bc.setBytes(bytes, 6, 5); + Assert.assertEquals(1, bc.indexOf("o", 0, 1, 0)); + + bc.setBytes(bytes, 6, 2); + Assert.assertEquals(0, bc.indexOf("wo", 0, 1, 0)); + Assert.assertEquals(-1, bc.indexOf("d", 0, 1, 0)); + } + + + @Test + public void testFindBytes() throws UnsupportedEncodingException { + byte[] bytes = "Hello\u00a0world".getBytes("ISO-8859-1"); + final int len = bytes.length; + + Assert.assertEquals(0, ByteChunk.findBytes(bytes, 0, len, new byte[] { 'H' })); + Assert.assertEquals(5, ByteChunk.findBytes(bytes, 0, len, new byte[] { + (byte) '\u00a0', 'x' })); + Assert.assertEquals(5, ByteChunk.findBytes(bytes, 0, len - 4, new byte[] { + 'x', (byte) '\u00a0' })); + Assert.assertEquals(len - 1, ByteChunk.findBytes(bytes, 2, len, new byte[] { + 'x', 'd' })); + Assert.assertEquals(1, ByteChunk.findBytes(bytes, 0, len, new byte[] { 'o', + 'e' })); + Assert.assertEquals(-1, ByteChunk.findBytes(bytes, 2, 5, new byte[] { 'w' })); + } + + + @Test + public void testSerialization() throws Exception { + String data = "Hello world!"; + byte[] bytes = data.getBytes(StandardCharsets.UTF_8); + + ByteChunk bcIn = new ByteChunk(); + bcIn.setBytes(bytes, 0, bytes.length); + bcIn.setCharset(StandardCharsets.UTF_8); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(bcIn); + oos.close(); + + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ByteChunk bcOut = (ByteChunk) ois.readObject(); + + Assert.assertArrayEquals(bytes, bcOut.getBytes()); + Assert.assertEquals(bcIn.getCharset(), bcOut.getCharset()); + } + + + @Test + public void testToString() { + ByteChunk bc = new ByteChunk(); + Assert.assertNull(bc.toString()); + byte[] data = new byte[8]; + bc.setBytes(data, 0, data.length); + Assert.assertNotNull(bc.toString()); + bc.recycle(); + // toString() should behave consistently for new ByteChunk and + // immediately after a call to recycle(). + Assert.assertNull(bc.toString()); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestByteChunkLargeHeap.java b/test/org/apache/tomcat/util/buf/TestByteChunkLargeHeap.java new file mode 100644 index 0000000..e0b0f2f --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestByteChunkLargeHeap.java @@ -0,0 +1,60 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.ByteChunk.ByteOutputChannel; + +/** + * Test cases for {@link ByteChunk} that require a large heap. + */ +public class TestByteChunkLargeHeap { + + @Test + public void testAppend() throws Exception { + ByteChunk bc = new ByteChunk(); + bc.setByteOutputChannel(new Sink()); + // Defaults to no limit + + byte data[] = new byte[32 * 1024 * 1024]; + + for (int i = 0; i < 100; i++) { + bc.append(data, 0, data.length); + } + + Assert.assertEquals(AbstractChunk.ARRAY_MAX_SIZE, bc.getBuffer().length); + } + + + public static class Sink implements ByteOutputChannel { + + @Override + public void realWriteBytes(byte[] cbuf, int off, int len) throws IOException { + // NO-OP + } + + @Override + public void realWriteBytes(ByteBuffer from) throws IOException { + // NO-OP + } + } +} diff --git a/test/org/apache/tomcat/util/buf/TestCharChunk.java b/test/org/apache/tomcat/util/buf/TestCharChunk.java new file mode 100644 index 0000000..c2182af --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestCharChunk.java @@ -0,0 +1,80 @@ +/* + * 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.tomcat.util.buf; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test cases for {@link CharChunk}. + */ +public class TestCharChunk { + + @Test + public void testEndsWith() { + CharChunk cc = new CharChunk(); + Assert.assertFalse(cc.endsWith("test")); + cc.setChars("xxtestxx".toCharArray(), 2, 4); + Assert.assertTrue(cc.endsWith("")); + Assert.assertTrue(cc.endsWith("t")); + Assert.assertTrue(cc.endsWith("st")); + Assert.assertTrue(cc.endsWith("test")); + Assert.assertFalse(cc.endsWith("x")); + Assert.assertFalse(cc.endsWith("xxtest")); + } + + + @Test + public void testIndexOf_String() { + char[] chars = "Hello\u00a0world".toCharArray(); + final int len = chars.length; + + CharChunk cc = new CharChunk(); + cc.setChars(chars, 0, len); + + Assert.assertEquals(0, cc.indexOf("Hello", 0, "Hello".length(), 0)); + Assert.assertEquals(2, cc.indexOf("ll", 0, 2, 0)); + Assert.assertEquals(2, cc.indexOf("Hello", 2, 2, 0)); + + Assert.assertEquals(7, cc.indexOf("o", 0, 1, 5)); + + // Does work outside of 0-127 (unlike ByteChunk) + Assert.assertEquals(5, cc.indexOf("\u00a0", 0, 1, 0)); + + cc.setChars(chars, 6, 5); + Assert.assertEquals(1, cc.indexOf("o", 0, 1, 0)); + + cc.setChars(chars, 6, 2); + Assert.assertEquals(0, cc.indexOf("wo", 0, 1, 0)); + Assert.assertEquals(-1, cc.indexOf("d", 0, 1, 0)); + } + + + @Test + public void testToString() { + CharChunk cc = new CharChunk(); + Assert.assertNull(cc.toString()); + char[] data = new char[8]; + cc.setChars(data, 0, data.length); + Assert.assertNotNull(cc.toString()); + cc.recycle(); + // toString() should behave consistently for new ByteChunk and + // immediately after a call to recycle(). + Assert.assertNull(cc.toString()); + } + +} diff --git a/test/org/apache/tomcat/util/buf/TestCharChunkLargeHeap.java b/test/org/apache/tomcat/util/buf/TestCharChunkLargeHeap.java new file mode 100644 index 0000000..fd6820b --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestCharChunkLargeHeap.java @@ -0,0 +1,55 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.CharChunk.CharOutputChannel; + +/** + * Test cases for {@link CharChunk}. + */ +public class TestCharChunkLargeHeap { + + @Test + public void testAppend() throws Exception { + CharChunk cc = new CharChunk(); + cc.setCharOutputChannel(new Sink()); + // Defaults to no limit + + char data[] = new char[32 * 1024 * 1024]; + + for (int i = 0; i < 100; i++) { + cc.append(data, 0, data.length); + } + + Assert.assertEquals(AbstractChunk.ARRAY_MAX_SIZE, cc.getBuffer().length); + } + + + public static class Sink implements CharOutputChannel { + + @Override + public void realWriteChars(char[] cbuf, int off, int len) throws IOException { + // NO-OP + } + } + +} diff --git a/test/org/apache/tomcat/util/buf/TestCharsetCache.java b/test/org/apache/tomcat/util/buf/TestCharsetCache.java new file mode 100644 index 0000000..ee3293f --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestCharsetCache.java @@ -0,0 +1,78 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +public class TestCharsetCache { + + @Test + public void testAllKnownCharsets() { + + Set known = new HashSet<>(Arrays.asList(CharsetCache.LAZY_CHARSETS)); + Set initial = new HashSet<>(Arrays.asList(CharsetCache.INITIAL_CHARSETS)); + + List cacheMisses = new ArrayList<>(); + + for (Charset charset: Charset.availableCharsets().values()) { + String name = charset.name().toLowerCase(Locale.ENGLISH); + + // No need to test the charsets that are pre-loaded + if (initial.contains(name)) { + continue; + } + + if (!known.contains(name)) { + cacheMisses.add(name); + } + + for (String alias : charset.aliases()) { + alias = alias.toLowerCase(Locale.ENGLISH); + if (!known.contains(alias)) { + cacheMisses.add(alias); + } + } + } + + if (cacheMisses.size() != 0) { + StringBuilder sb = new StringBuilder(); + Collections.sort(cacheMisses); + for (String name : cacheMisses) { + if (sb.length() == 0) { + sb.append('"'); + } else { + sb.append(", \""); + } + sb.append(name.toLowerCase(Locale.ENGLISH)); + sb.append('"'); + } + System.out.println(sb.toString()); + } + + Assert.assertTrue(cacheMisses.size() == 0); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestCharsetCachePerformance.java b/test/org/apache/tomcat/util/buf/TestCharsetCachePerformance.java new file mode 100644 index 0000000..0a0646d --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestCharsetCachePerformance.java @@ -0,0 +1,142 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +/* + * This is a relative performance test so it remains part of the standard test run. + */ +public class TestCharsetCachePerformance { + + @Test + public void testCache() throws Exception { + long timeNone = doTest(new NoCsCache()); + long timeFull = doTest(new FullCsCache()); + long timeLazy = doTest(new LazyCsCache()); + + Assert.assertTrue("No cache was faster than full cache", timeFull < timeNone); + Assert.assertTrue("No cache was faster than lazy cache", timeLazy < timeNone); + // On average full cache is faster than lazy cache but they are close enough the test will fail sometimes + //Assert.assertTrue("Lazy cache was faster than full cache ", timeFull < timeLazy); + } + + + private long doTest(CsCache cache) throws Exception { + int threadCount = 10; + int iterations = 10000000; + String[] lookupNames = new String[] { + "ISO-8859-1", "ISO-8859-2", "ISO-8859-3", "ISO-8859-4", "ISO-8859-5" }; + + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new TestCsCacheThread(iterations, cache, lookupNames); + } + + long startTime = System.nanoTime(); + + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + + long endTime = System.nanoTime(); + + System.out.println(cache.getClass().getName() + ": " + (endTime - startTime) + "ns"); + + return endTime - startTime; + } + + + private interface CsCache { + Charset getCharset(String charsetName); + } + + + private static class NoCsCache implements CsCache { + + @Override + public Charset getCharset(String charsetName) { + return Charset.forName(charsetName); + } + } + + + private static class FullCsCache implements CsCache { + + private static final Map cache = new HashMap<>(); + + static { + for (Charset charset: Charset.availableCharsets().values()) { + cache.put(charset.name().toLowerCase(Locale.ENGLISH), charset); + for (String alias : charset.aliases()) { + cache.put(alias.toLowerCase(Locale.ENGLISH), charset); + } + } + } + + + @Override + public Charset getCharset(String charsetName) { + return cache.get(charsetName.toLowerCase(Locale.ENGLISH)); + } + } + + + private static class LazyCsCache implements CsCache { + + private CharsetCache cache = new CharsetCache(); + + @Override + public Charset getCharset(String charsetName) { + return cache.getCharset(charsetName); + } + } + + + private static class TestCsCacheThread extends Thread { + + private final int iterations; + private final CsCache cache; + private final String[] lookupNames; + private final int lookupNamesCount; + + TestCsCacheThread(int iterations, CsCache cache, String[] lookupNames) { + this.iterations = iterations; + this.cache = cache; + this.lookupNames = lookupNames; + this.lookupNamesCount = lookupNames.length; + } + + @Override + public void run() { + for (int i = 0; i < iterations; i++) { + cache.getCharset(lookupNames[i % lookupNamesCount]); + } + } + } +} diff --git a/test/org/apache/tomcat/util/buf/TestCharsetUtil.java b/test/org/apache/tomcat/util/buf/TestCharsetUtil.java new file mode 100644 index 0000000..2240d77 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestCharsetUtil.java @@ -0,0 +1,92 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +public class TestCharsetUtil { + + /* + * Check the standard character sets return the expected values + */ + @Test + public void testIsAsciiSupersetStandardCharsets() { + Assert.assertTrue(CharsetUtil.isAsciiSuperset(StandardCharsets.US_ASCII)); + Assert.assertTrue(CharsetUtil.isAsciiSuperset(StandardCharsets.ISO_8859_1)); + Assert.assertTrue(CharsetUtil.isAsciiSuperset(StandardCharsets.UTF_8)); + + Assert.assertFalse(CharsetUtil.isAsciiSuperset(StandardCharsets.UTF_16)); + Assert.assertFalse(CharsetUtil.isAsciiSuperset(StandardCharsets.UTF_16BE)); + Assert.assertFalse(CharsetUtil.isAsciiSuperset(StandardCharsets.UTF_16LE)); + } + + + /* + * More comprehensive test that checks that, apart from where the encoding + * overlaps with ASCII, no valid ASCII bytes are used. + * + * This is relatively slow. + * Only need to run this when we detect a new Charset. + */ + //@Test + public void testIsAcsiiSupersetAll() { + for (Charset charset : Charset.availableCharsets().values()) { + System.out.println("Testing: " + charset.name()); + + if (CharsetUtil.isAsciiSuperset(charset)) { + // Run a more in-depth check to make sure + // Encoding Unicode 128 onwards should never generate bytes 0 to 127. + CharsetEncoder encoder = charset.newEncoder(); + CharBuffer inChars = CharBuffer.allocate(8); + ByteBuffer outBytes; + + for (int i = 128; i < Character.MAX_CODE_POINT; i++) { + inChars.clear(); + char[] chars = Character.toChars(i); + for (char c : chars) { + inChars.append(c); + } + inChars.flip(); + try { + outBytes = encoder.encode(inChars); + } catch (CharacterCodingException e) { + // Ignore. The encoding can't handle the codepoint. That is fine. + continue; + } + outBytes.flip(); + while (outBytes.hasRemaining()) { + byte b = outBytes.get(); + // All bytes should have the highest bit set + if ((b & 0x80) == 0) { + Assert.fail("[" + charset.name() + " is not a superset of ASCII"); + } + } + } + } else { + System.out.println("Not: " + charset.name()); + } + } + } +} diff --git a/test/org/apache/tomcat/util/buf/TestHexUtils.java b/test/org/apache/tomcat/util/buf/TestHexUtils.java new file mode 100644 index 0000000..44609cb --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestHexUtils.java @@ -0,0 +1,75 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test cases for {@link HexUtils}. + */ +public class TestHexUtils { + + private static final String TEST01_STRING = "Hello World"; + private static final byte[] TEST01_BYTES = TEST01_STRING.getBytes(StandardCharsets.UTF_8); + private static final String TEST02_STRING = "foo"; + private static final byte[] TEST02_BYTES = TEST02_STRING.getBytes(StandardCharsets.UTF_8); + + @Test + public void testGetDec() { + Assert.assertEquals(0, HexUtils.getDec('0')); + Assert.assertEquals(9, HexUtils.getDec('9')); + Assert.assertEquals(10, HexUtils.getDec('a')); + Assert.assertEquals(15, HexUtils.getDec('f')); + Assert.assertEquals(10, HexUtils.getDec('A')); + Assert.assertEquals(15, HexUtils.getDec('F')); + Assert.assertEquals(-1, HexUtils.getDec(0)); + Assert.assertEquals(-1, HexUtils.getDec('Z')); + Assert.assertEquals(-1, HexUtils.getDec(255)); + Assert.assertEquals(-1, HexUtils.getDec(-60)); + } + + @Test + public void testRoundTrip01() { + Assert.assertArrayEquals(TEST01_STRING, TEST01_BYTES, + HexUtils.fromHexString(HexUtils.toHexString(TEST01_BYTES))); + } + + @Test + public void testRoundTrip02() { + Assert.assertArrayEquals(TEST02_STRING, TEST02_BYTES, + HexUtils.fromHexString(HexUtils.toHexString(TEST02_BYTES))); + } + + @Test(expected=IllegalArgumentException.class) + public void testFromHex01() { + HexUtils.fromHexString("Not a hex string"); + } + + @Test(expected=IllegalArgumentException.class) + public void testFromHex02() { + // Odd number of hex characters + HexUtils.fromHexString("aaa"); + } + + @Test + public void testToHex01() { + Assert.assertEquals("fedc", HexUtils.toHexString((char) 0xfedc)); + } +} \ No newline at end of file diff --git a/test/org/apache/tomcat/util/buf/TestMessageBytes.java b/test/org/apache/tomcat/util/buf/TestMessageBytes.java new file mode 100644 index 0000000..f9af7fd --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestMessageBytes.java @@ -0,0 +1,69 @@ +/* + * 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.tomcat.util.buf; + +import org.junit.Test; + +public class TestMessageBytes { + + @Test + public void testToStringFromNull() { + MessageBytes mb = MessageBytes.newInstance(); + mb.toString(); + } + + + @Test + public void testToBytesFromNull() { + MessageBytes mb = MessageBytes.newInstance(); + mb.toBytes(); + } + + + @Test + public void testToCharsFromNull() { + MessageBytes mb = MessageBytes.newInstance(); + mb.toChars(); + } + + + @Test + public void testToStringAfterRecycle() { + MessageBytes mb = MessageBytes.newInstance(); + mb.setString("foo"); + mb.recycle(); + mb.toString(); + } + + + @Test + public void testToBytesAfterRecycle() { + MessageBytes mb = MessageBytes.newInstance(); + mb.setString("foo"); + mb.recycle(); + mb.toBytes(); + } + + + @Test + public void testToCharsAfterRecycle() { + MessageBytes mb = MessageBytes.newInstance(); + mb.setString("foo"); + mb.recycle(); + mb.toChars(); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestMessageBytesConversion.java b/test/org/apache/tomcat/util/buf/TestMessageBytesConversion.java new file mode 100644 index 0000000..09e41e7 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestMessageBytesConversion.java @@ -0,0 +1,207 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Checks that all MessageBytes getters are consistent with most recently used + * setter. + */ +@RunWith(Parameterized.class) +public class TestMessageBytesConversion { + + private static final String PREVIOUS_STRING = "previous-string"; + private static final byte[] PREVIOUS_BYTES = "previous-bytes".getBytes(StandardCharsets.ISO_8859_1); + private static final char[] PREVIOUS_CHARS = "previous-chars".toCharArray(); + + private static final String EXPECTED_STRING = "expected"; + private static final byte[] EXPECTED_BYTES = "expected".getBytes(StandardCharsets.ISO_8859_1); + private static final char[] EXPECTED_CHARS = "expected".toCharArray(); + + @Parameters(name = "{index}: previous({0}, {1}, {2}, {3}), set {4}, check({5}, {6}, {7}") + public static Collection parameters() { + List previousTypes = new ArrayList<>(); + previousTypes.add(MessageBytesType.BYTE); + previousTypes.add(MessageBytesType.CHAR); + previousTypes.add(MessageBytesType.STRING); + previousTypes.add(MessageBytesType.NULL); + + List setTypes = new ArrayList<>(); + setTypes.add(MessageBytesType.BYTE); + setTypes.add(MessageBytesType.CHAR); + setTypes.add(MessageBytesType.STRING); + + List parameterSets = new ArrayList<>(); + + List> previousPermutations = permutations(previousTypes); + List> checkPermutations = permutations(setTypes); + + for (List setPermutation : previousPermutations) { + for (MessageBytesType setType : setTypes) { + for (List checkPermutation : checkPermutations) { + parameterSets.add(new Object[] { + setPermutation.get(0), setPermutation.get(1), setPermutation.get(2), setPermutation.get(3), + setType, + checkPermutation.get(0), checkPermutation.get(1), checkPermutation.get(2)}); + } + } + } + + + return parameterSets; + } + + @Parameter(0) + public MessageBytesType setFirst; + @Parameter(1) + public MessageBytesType setSecond; + @Parameter(2) + public MessageBytesType setThird; + @Parameter(3) + public MessageBytesType setFourth; + + @Parameter(4) + public MessageBytesType expected; + + @Parameter(5) + public MessageBytesType checkFirst; + @Parameter(6) + public MessageBytesType checkSecond; + @Parameter(7) + public MessageBytesType checkThird; + + + @Test + public void testConversion() { + MessageBytes mb = MessageBytes.newInstance(); + + setFirst.setPrevious(mb); + setSecond.setPrevious(mb); + setThird.setPrevious(mb); + setFourth.setPrevious(mb); + + expected.setExpected(mb); + + checkFirst.checkExpected(mb); + checkSecond.checkExpected(mb); + checkThird.checkExpected(mb); + } + + + @Test + public void testConversionNull() { + MessageBytes mb = MessageBytes.newInstance(); + + setFirst.setPrevious(mb); + setSecond.setPrevious(mb); + setThird.setPrevious(mb); + setFourth.setPrevious(mb); + + mb.setString(null); + + checkFirst.checkNull(mb); + checkSecond.checkNull(mb); + checkThird.checkNull(mb); + } + + + public enum MessageBytesType { + BYTE((x) -> x.setBytes(PREVIOUS_BYTES, 0, PREVIOUS_BYTES.length), + (x) -> x.setBytes(EXPECTED_BYTES, 0, EXPECTED_BYTES.length), + (x) -> {x.toBytes(); Assert.assertTrue(x.getByteChunk().equals(EXPECTED_BYTES, 0, EXPECTED_BYTES.length) );}, + (x) -> {x.toBytes(); Assert.assertTrue(x.getByteChunk().isNull());}), + + CHAR((x) -> x.setChars(PREVIOUS_CHARS, 0, PREVIOUS_CHARS.length), + (x) -> x.setChars(EXPECTED_CHARS, 0, EXPECTED_CHARS.length), + (x) -> {x.toChars(); Assert.assertArrayEquals(EXPECTED_CHARS, x.getCharChunk().getChars());}, + (x) -> {x.toChars(); Assert.assertTrue(x.getCharChunk().isNull());}), + + STRING((x) -> x.setString(PREVIOUS_STRING), + (x) -> x.setString(EXPECTED_STRING), + (x) -> Assert.assertEquals(EXPECTED_STRING, x.toString()), + (x) -> Assert.assertNull(x.toString())), + + NULL((x) -> x.setString(null), + (x) -> x.setString(null), + (x) -> Assert.assertTrue(x.isNull()), + (x) -> Assert.assertTrue(x.isNull())); + + private final Consumer setPrevious; + private final Consumer setExpected; + private final Consumer checkExpected; + private final Consumer checkNull; + + MessageBytesType(Consumer setPrevious, Consumer setExpected, + Consumer checkExpected, Consumer checkNull) { + this.setPrevious = setPrevious; + this.setExpected = setExpected; + this.checkExpected = checkExpected; + this.checkNull = checkNull; + } + + public void setPrevious(MessageBytes mb) { + setPrevious.accept(mb); + } + + public void setExpected(MessageBytes mb) { + setExpected.accept(mb); + } + + public void checkExpected(MessageBytes mb) { + checkExpected.accept(mb); + } + + public void checkNull(MessageBytes mb) { + checkNull.accept(mb); + } + } + + + private static List> permutations(List items) { + List> results = new ArrayList<>(); + + if (items.size() == 1) { + results.add(items); + } else { + List others = new ArrayList<>(items); + T first = others.remove(0); + List> subPermutations = permutations(others); + + for (List subPermutation : subPermutations) { + for (int i = 0; i <= subPermutation.size(); i++) { + List result = new ArrayList<>(subPermutation); + result.add(i, first); + results.add(result); + } + } + } + + return results; + } +} diff --git a/test/org/apache/tomcat/util/buf/TestMessageBytesIntegration.java b/test/org/apache/tomcat/util/buf/TestMessageBytesIntegration.java new file mode 100644 index 0000000..5087ccb --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestMessageBytesIntegration.java @@ -0,0 +1,106 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestMessageBytesIntegration extends TomcatBaseTest { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=66488 + */ + @Test + public void testBytesStringBytesMixup() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("", null); + + ctx.addApplicationListener("org.apache.tomcat.util.buf.TestMessageBytesIntegration$MixUpConfig"); + + // Add servlet + Tomcat.addServlet(ctx, "MixUpServlet", new MixUpServlet()); + ctx.addServletMappingDecoded("/mixup", "MixUpServlet"); + + tomcat.start(); + + ByteChunk body = new ByteChunk(); + Map> requestHeaders = new HashMap<>(); + requestHeaders.put("Cookie", Arrays.asList("a=b; c=d")); + getUrl("http://localhost:" + getPort() + "/mixup", body, requestHeaders, null); + + Assert.assertEquals("/mixup", body.toString()); + } + + + private static class MixUpServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Convert all headers to String + Enumeration names = req.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + Enumeration values = req.getHeaders(name); + while (values.hasMoreElements()) { + String value = values.nextElement(); + System.out.println("[" + name + "] - [" + value + "]"); + } + } + + // Parsing cookies turns cookie header back to bytes (and triggers the bug) + req.getCookies(); + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + resp.getWriter().print(req.getRequestURI()); + } + } + + + public static class MixUpConfig implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + sce.getServletContext().setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.URL))); + } + } +} diff --git a/test/org/apache/tomcat/util/buf/TestMessageBytesPerformance.java b/test/org/apache/tomcat/util/buf/TestMessageBytesPerformance.java new file mode 100644 index 0000000..2486521 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestMessageBytesPerformance.java @@ -0,0 +1,116 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +import org.apache.tomcat.util.compat.JreCompat; + +public class TestMessageBytesPerformance { + + private static final String CONVERSION_STRING = + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; + + private static final int CONVERSION_LOOPS = 1000000; + + /* + * Checks the the optimized code is faster than the non-optimized code. + */ + @Test + public void testConversionPerformance() { + + // ISO_8859_1 conversion appears to be optimised in Java 16 onwards + Assume.assumeFalse(JreCompat.isJre16Available()); + + long optimized = -1; + long nonOptimized = -1; + + /* + * One loop is likely to be enough as the optimised code is + * significantly (3x to 4x on markt's desktop) faster than the + * non-optimised code. Loop three times allows once to warn up the JVM + * once to run the test and once more in case of unexpected CI /GC + * slowness. The test will exit early if possible. + * + * MessageBytes only optimises conversion for ISO_8859_1 + */ + for (int i = 0; i < 3; i++) { + optimized = doTestOptimisedConversionPerformance(); + nonOptimized = doTestConversionPerformance(); + + System.out.println(" Optimized: " + optimized + "\nNon-optimized: " + nonOptimized); + if (optimized * 2 < nonOptimized) { + break; + } + } + + Assert.assertTrue("Non-optimised code was faster (" + nonOptimized + "ns) compared to optimized (" + + optimized + "ns)", optimized < nonOptimized); + } + + + private long doTestOptimisedConversionPerformance() { + MessageBytes mb = MessageBytes.newInstance(); + + long start = System.nanoTime(); + for (int i = 0; i < CONVERSION_LOOPS; i++) { + mb.recycle(); + mb.setCharset(StandardCharsets.ISO_8859_1); + mb.setString(CONVERSION_STRING); + mb.toBytes(); + } + return System.nanoTime() - start; + } + + + private long doTestConversionPerformance() { + long start = System.nanoTime(); + for (int i = 0; i < CONVERSION_LOOPS; i++) { + CharsetEncoder encoder = StandardCharsets.ISO_8859_1.newEncoder().onMalformedInput( + CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT); + try { + encoder.encode(CharBuffer.wrap(CONVERSION_STRING)); + } catch (CharacterCodingException cce) { + Assert.fail(); + } + } + return System.nanoTime() - start; + } +} diff --git a/test/org/apache/tomcat/util/buf/TestStringCache.java b/test/org/apache/tomcat/util/buf/TestStringCache.java new file mode 100644 index 0000000..2345d2d --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestStringCache.java @@ -0,0 +1,100 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +public class TestStringCache { + + private static final byte[] BYTES_VALID = new byte[] { 65, 66, 67, 68}; + private static final byte[] BYTES_INVALID = new byte[] {65, 66, -1, 67, 68}; + + private static final ByteChunk INPUT_VALID = new ByteChunk(); + private static final ByteChunk INPUT_INVALID = new ByteChunk(); + + private static final CodingErrorAction[] actions = + new CodingErrorAction[] { CodingErrorAction.IGNORE, CodingErrorAction.REPLACE, CodingErrorAction.REPORT }; + + static { + INPUT_VALID.setBytes(BYTES_VALID, 0, BYTES_VALID.length); + INPUT_VALID.setCharset(StandardCharsets.UTF_8); + INPUT_INVALID.setBytes(BYTES_INVALID, 0, BYTES_INVALID.length); + INPUT_INVALID.setCharset(StandardCharsets.UTF_8); + } + + + @Test + public void testCodingErrorLookup() { + + System.setProperty("tomcat.util.buf.StringCache.byte.enabled", "true"); + + Assert.assertTrue(StringCache.byteEnabled); + StringCache sc = new StringCache(); + sc.reset(); + + for (int i = 0; i < StringCache.trainThreshold * 2; i++) { + for (CodingErrorAction malformedInputAction : actions) { + try { + // UTF-8 doesn't have any unmappable characters + INPUT_VALID.toString(malformedInputAction, CodingErrorAction.IGNORE); + INPUT_INVALID.toString(malformedInputAction, CodingErrorAction.IGNORE); + } catch (CharacterCodingException e) { + // Ignore + } + } + } + + Assert.assertNotNull(StringCache.bcCache); + + // Check the valid input is cached correctly + for (CodingErrorAction malformedInputAction : actions) { + try { + Assert.assertEquals("ABCD", INPUT_VALID.toString(malformedInputAction, CodingErrorAction.IGNORE)); + } catch (CharacterCodingException e) { + // Should not happen for valid input + Assert.fail(); + } + } + + // Check the valid input is cached correctly + try { + Assert.assertEquals("ABCD", INPUT_INVALID.toString(CodingErrorAction.IGNORE, CodingErrorAction.IGNORE)); + } catch (CharacterCodingException e) { + // Should not happen for invalid input with IGNORE + Assert.fail(); + } + try { + Assert.assertEquals("AB\ufffdCD", INPUT_INVALID.toString(CodingErrorAction.REPLACE, CodingErrorAction.IGNORE)); + } catch (CharacterCodingException e) { + // Should not happen for invalid input with REPLACE + Assert.fail(); + } + try { + Assert.assertEquals("ABCD", INPUT_INVALID.toString(CodingErrorAction.REPORT, CodingErrorAction.IGNORE)); + // Should throw exception + Assert.fail(); + } catch (CharacterCodingException e) { + // Ignore + } + + } +} diff --git a/test/org/apache/tomcat/util/buf/TestStringUtils.java b/test/org/apache/tomcat/util/buf/TestStringUtils.java new file mode 100644 index 0000000..b7ac950 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestStringUtils.java @@ -0,0 +1,77 @@ +/* + * 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.tomcat.util.buf; + +import java.util.Collection; + +import org.junit.Assert; +import org.junit.Test; + +/* + * None of these tests should throw a NPE. + */ +public class TestStringUtils { + + @Test + public void testNullArray() { + Assert.assertEquals("", StringUtils.join((String[]) null)); + } + + + @Test + public void testNullArrayCharStringBuilder() { + StringBuilder sb = new StringBuilder(); + StringUtils.join((String[]) null, ',', sb); + Assert.assertEquals("", sb.toString()); + } + + + @Test + public void testNullCollection() { + Assert.assertEquals("", StringUtils.join((Collection) null)); + } + + + @Test + public void testNullCollectionChar() { + Assert.assertEquals("", StringUtils.join(null, ',')); + } + + + @Test + public void testNullIterableCharStringBuilder() { + StringBuilder sb = new StringBuilder(); + StringUtils.join((Iterable) null, ',', sb); + Assert.assertEquals("", sb.toString()); + } + + + @Test + public void testNullArrayCharFunctionStringBuilder() { + StringBuilder sb = new StringBuilder(); + StringUtils.join((String[]) null, ',', null, sb); + Assert.assertEquals("", sb.toString()); + } + + + @Test + public void testNullIterableCharFunctionStringBuilder() { + StringBuilder sb = new StringBuilder(); + StringUtils.join((Iterable) null, ',', null, sb); + Assert.assertEquals("", sb.toString()); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUDecoder.java b/test/org/apache/tomcat/util/buf/TestUDecoder.java new file mode 100644 index 0000000..b2ca73d --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUDecoder.java @@ -0,0 +1,241 @@ +/* + * 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.tomcat.util.buf; + +import java.io.CharConversionException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +public class TestUDecoder { + + @Test(expected = IllegalArgumentException.class) + public void testURLDecodeStringInvalid01() { + // %n rather than %nn should throw an IAE according to the Javadoc + UDecoder.URLDecode("%5xxxxx", StandardCharsets.UTF_8); + } + + + @Test(expected = IllegalArgumentException.class) + public void testURLDecodeStringInvalid02() { + // Edge case trying to trigger ArrayIndexOutOfBoundsException + UDecoder.URLDecode("%5", StandardCharsets.UTF_8); + } + + + @Test + public void testURLDecodeStringValidIso88591Start() { + String result = UDecoder.URLDecode("%41xxxx", StandardCharsets.ISO_8859_1); + Assert.assertEquals("Axxxx", result); + } + + + @Test + public void testURLDecodeStringValidIso88591Middle() { + String result = UDecoder.URLDecode("xx%41xx", StandardCharsets.ISO_8859_1); + Assert.assertEquals("xxAxx", result); + } + + + @Test + public void testURLDecodeStringValidIso88591End() { + String result = UDecoder.URLDecode("xxxx%41", StandardCharsets.ISO_8859_1); + Assert.assertEquals("xxxxA", result); + } + + + @Test + public void testURLDecodeStringValidUtf8Start() { + String result = UDecoder.URLDecode("%c3%aaxxxx", StandardCharsets.UTF_8); + Assert.assertEquals("\u00eaxxxx", result); + } + + + @Test + public void testURLDecodeStringValidUtf8Middle() { + String result = UDecoder.URLDecode("xx%c3%aaxx", StandardCharsets.UTF_8); + Assert.assertEquals("xx\u00eaxx", result); + } + + + @Test + public void testURLDecodeStringValidUtf8End() { + String result = UDecoder.URLDecode("xxxx%c3%aa", StandardCharsets.UTF_8); + Assert.assertEquals("xxxx\u00ea", result); + } + + + @Test + public void testURLDecodeStringNonAsciiValidNone() { + String result = UDecoder.URLDecode("\u00eaxxxx", StandardCharsets.UTF_8); + Assert.assertEquals("\u00eaxxxx", result); + } + + + @Test + public void testURLDecodeStringNonAsciiValidUtf8() { + String result = UDecoder.URLDecode("\u00ea%c3%aa", StandardCharsets.UTF_8); + Assert.assertEquals("\u00ea\u00ea", result); + } + + + @Test + public void testURLDecodeStringSolidus01() throws IOException { + doTestSolidus("xxxxxx", "xxxxxx"); + } + + + @Test + public void testURLDecodeStringSolidus02() throws IOException { + doTestSolidus("%20xxxx", " xxxx"); + } + + + @Test + public void testURLDecodeStringSolidus03() throws IOException { + doTestSolidus("xx%20xx", "xx xx"); + } + + + @Test + public void testURLDecodeStringSolidus04() throws IOException { + doTestSolidus("xxxx%20", "xxxx "); + } + + + @Test(expected = CharConversionException.class) + public void testURLDecodeStringSolidus05a() throws IOException { + doTestSolidus("%2fxxxx", EncodedSolidusHandling.REJECT); + } + + + @Test + public void testURLDecodeStringSolidus05b() throws IOException { + String result = doTestSolidus("%2fxxxx", EncodedSolidusHandling.PASS_THROUGH); + Assert.assertEquals("%2fxxxx", result); + } + + + @Test + public void testURLDecodeStringSolidus05c() throws IOException { + String result = doTestSolidus("%2fxxxx", EncodedSolidusHandling.DECODE); + Assert.assertEquals("/xxxx", result); + } + + + @Test(expected = CharConversionException.class) + public void testURLDecodeStringSolidus06a() throws IOException { + doTestSolidus("%2fxx%20xx", EncodedSolidusHandling.REJECT); + } + + + @Test + public void testURLDecodeStringSolidus06b() throws IOException { + String result = doTestSolidus("%2fxx%20xx", EncodedSolidusHandling.PASS_THROUGH); + Assert.assertEquals("%2fxx xx", result); + } + + + @Test + public void testURLDecodeStringSolidus06c() throws IOException { + String result = doTestSolidus("%2fxx%20xx", EncodedSolidusHandling.DECODE); + Assert.assertEquals("/xx xx", result); + } + + + @Test(expected = CharConversionException.class) + public void testURLDecodeStringSolidus07a() throws IOException { + doTestSolidus("xx%2f%20xx", EncodedSolidusHandling.REJECT); + } + + + @Test + public void testURLDecodeStringSolidus07b() throws IOException { + String result = doTestSolidus("xx%2f%20xx", EncodedSolidusHandling.PASS_THROUGH); + Assert.assertEquals("xx%2f xx", result); + } + + + @Test + public void testURLDecodeStringSolidus07c() throws IOException { + String result = doTestSolidus("xx%2f%20xx", EncodedSolidusHandling.DECODE); + Assert.assertEquals("xx/ xx", result); + } + + + @Test(expected = CharConversionException.class) + public void testURLDecodeStringSolidus08a() throws IOException { + doTestSolidus("xx%20%2fxx", EncodedSolidusHandling.REJECT); + } + + + @Test + public void testURLDecodeStringSolidus08b() throws IOException { + String result = doTestSolidus("xx%20%2fxx", EncodedSolidusHandling.PASS_THROUGH); + Assert.assertEquals("xx %2fxx", result); + } + + + @Test + public void testURLDecodeStringSolidus08c() throws IOException { + String result = doTestSolidus("xx%20%2fxx", EncodedSolidusHandling.DECODE); + Assert.assertEquals("xx /xx", result); + } + + + @Test(expected = CharConversionException.class) + public void testURLDecodeStringSolidus09a() throws IOException { + doTestSolidus("xx%20xx%2f", EncodedSolidusHandling.REJECT); + } + + + @Test + public void testURLDecodeStringSolidus09b() throws IOException { + String result = doTestSolidus("xx%20xx%2f", EncodedSolidusHandling.PASS_THROUGH); + Assert.assertEquals("xx xx%2f", result); + } + + + @Test + public void testURLDecodeStringSolidus09c() throws IOException { + String result = doTestSolidus("xx%20xx%2f", EncodedSolidusHandling.DECODE); + Assert.assertEquals("xx xx/", result); + } + + + private void doTestSolidus(String input, String expected) throws IOException { + for (EncodedSolidusHandling solidusHandling : EncodedSolidusHandling.values()) { + String result = doTestSolidus(input, solidusHandling); + Assert.assertEquals(expected, result); + } + } + + + private String doTestSolidus(String input, EncodedSolidusHandling solidusHandling) throws IOException { + byte[] b = input.getBytes(StandardCharsets.UTF_8); + ByteChunk bc = new ByteChunk(16); + bc.setBytes(b, 0, b.length); + bc.setCharset(StandardCharsets.UTF_8); + + UDecoder udecoder = new UDecoder(); + udecoder.convert(bc, solidusHandling); + + return bc.toString(); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUEncoder.java b/test/org/apache/tomcat/util/buf/TestUEncoder.java new file mode 100644 index 0000000..73f3b13 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUEncoder.java @@ -0,0 +1,45 @@ +/* + * 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.tomcat.util.buf; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.UEncoder.SafeCharsSet; + +/** + * Test cases for {@link UEncoder}. + */ +public class TestUEncoder { + + @Test + public void testEncodeURLWithSlashInit() throws IOException { + UEncoder urlEncoder = new UEncoder(SafeCharsSet.WITH_SLASH); + + String s = "a+b/c/d+e.class"; + Assert.assertTrue(urlEncoder.encodeURL(s, 0, s.length()).equals( + "a%2bb/c/d%2be.class")); + Assert.assertTrue(urlEncoder.encodeURL(s, 2, s.length() - 2).equals( + "b/c/d%2be.cla")); + + s = new String(new char[] { 0xD801, 0xDC01 }); + Assert.assertTrue(urlEncoder.encodeURL(s, 0, s.length()) + .equals("%f0%90%90%81")); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUriUtil.java b/test/org/apache/tomcat/util/buf/TestUriUtil.java new file mode 100644 index 0000000..fc7531c --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUriUtil.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat.util.buf; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.Assert; +import org.junit.Test; + +public class TestUriUtil { + + @Test + public void testResolve01() throws URISyntaxException, MalformedURLException { + URI base = new URI("file:/aaa/bbb/base.xml"); + String target = "target.xml"; + URI result = UriUtil.resolve(base, target); + Assert.assertEquals(new URI("file:/aaa/bbb/target.xml"), result); + } + + @Test + public void testResolve02() throws URISyntaxException, MalformedURLException { + URI base = new URI("file:/aaa/bbb/base.xml"); + String target = "../target.xml"; + URI result = UriUtil.resolve(base, target); + Assert.assertEquals(new URI("file:/aaa/target.xml"), result); + } + + @Test + public void testResolve03() throws URISyntaxException, MalformedURLException { + URI base = new URI("jar:file:/aaa/bbb!/ccc/ddd/base.xml"); + String target = "target.xml"; + URI result = UriUtil.resolve(base, target); + Assert.assertEquals(new URI("jar:file:/aaa/bbb!/ccc/ddd/target.xml"), result); + } + + @Test + public void testResolve04() throws URISyntaxException, MalformedURLException { + URI base = new URI("jar:file:/aaa/bbb!/ccc/ddd/base.xml"); + String target = "../target.xml"; + URI result = UriUtil.resolve(base, target); + Assert.assertEquals(new URI("jar:file:/aaa/bbb!/ccc/target.xml"), result); + } + + @Test + public void testResolve05() throws URISyntaxException, MalformedURLException { + URI base = new URI("jar:file:/aaa/bbb!/ccc/ddd/base.xml"); + String target = "../../target.xml"; + URI result = UriUtil.resolve(base, target); + Assert.assertEquals(new URI("jar:file:/aaa/bbb!/target.xml"), result); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUriUtil24.java b/test/org/apache/tomcat/util/buf/TestUriUtil24.java new file mode 100644 index 0000000..2f4ae76 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUriUtil24.java @@ -0,0 +1,24 @@ +/* + * 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.tomcat.util.buf; + +public class TestUriUtil24 extends TesterUriUtilBase { + + public TestUriUtil24() { + super("$"); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUriUtil26.java b/test/org/apache/tomcat/util/buf/TestUriUtil26.java new file mode 100644 index 0000000..4e6a6d0 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUriUtil26.java @@ -0,0 +1,24 @@ +/* + * 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.tomcat.util.buf; + +public class TestUriUtil26 extends TesterUriUtilBase { + + public TestUriUtil26() { + super("&"); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUriUtil2A.java b/test/org/apache/tomcat/util/buf/TestUriUtil2A.java new file mode 100644 index 0000000..c328e9f --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUriUtil2A.java @@ -0,0 +1,24 @@ +/* + * 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.tomcat.util.buf; + +public class TestUriUtil2A extends TesterUriUtilBase { + + public TestUriUtil2A() { + super("*"); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUriUtil40.java b/test/org/apache/tomcat/util/buf/TestUriUtil40.java new file mode 100644 index 0000000..10dcaf7 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUriUtil40.java @@ -0,0 +1,24 @@ +/* + * 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.tomcat.util.buf; + +public class TestUriUtil40 extends TesterUriUtilBase { + + public TestUriUtil40() { + super("@"); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUriUtilIsAbsoluteURI.java b/test/org/apache/tomcat/util/buf/TestUriUtilIsAbsoluteURI.java new file mode 100644 index 0000000..f1ede74 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUriUtilIsAbsoluteURI.java @@ -0,0 +1,77 @@ +/* + * 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.tomcat.util.buf; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + + +@RunWith(Parameterized.class) +public class TestUriUtilIsAbsoluteURI { + + @Parameterized.Parameters(name = "{index}: path[{0}], expected[{1}]") + public static Collection parameters() { + + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { "", Boolean.FALSE } ); + + parameterSets.add(new Object[] { "h", Boolean.FALSE } ); + parameterSets.add(new Object[] { "ht", Boolean.FALSE } ); + parameterSets.add(new Object[] { "htt", Boolean.FALSE } ); + parameterSets.add(new Object[] { "http", Boolean.FALSE } ); + parameterSets.add(new Object[] { "http:", Boolean.FALSE } ); + parameterSets.add(new Object[] { "http:/", Boolean.FALSE } ); + parameterSets.add(new Object[] { "http://", Boolean.TRUE } ); + parameterSets.add(new Object[] { "http://foo", Boolean.TRUE } ); + + parameterSets.add(new Object[] { "f", Boolean.FALSE } ); + parameterSets.add(new Object[] { "fi", Boolean.FALSE } ); + parameterSets.add(new Object[] { "fil", Boolean.FALSE } ); + parameterSets.add(new Object[] { "file", Boolean.FALSE } ); + parameterSets.add(new Object[] { "file:", Boolean.FALSE } ); + parameterSets.add(new Object[] { "file:/", Boolean.TRUE } ); + parameterSets.add(new Object[] { "file://", Boolean.TRUE } ); + + parameterSets.add(new Object[] { "c", Boolean.FALSE } ); + parameterSets.add(new Object[] { "c:", Boolean.FALSE } ); + parameterSets.add(new Object[] { "c:/", Boolean.FALSE } ); + parameterSets.add(new Object[] { "c:/foo", Boolean.FALSE } ); + + return parameterSets; + } + + + @Parameter(0) + public String path; + + @Parameter(1) + public Boolean valid; + + @Test + public void test() { + boolean result = UriUtil.isAbsoluteURI(path); + Assert.assertEquals(path, valid, Boolean.valueOf(result)); + } +} diff --git a/test/org/apache/tomcat/util/buf/TestUtf8.java b/test/org/apache/tomcat/util/buf/TestUtf8.java new file mode 100644 index 0000000..3bc55a6 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TestUtf8.java @@ -0,0 +1,412 @@ +/* + * 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.tomcat.util.buf; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +/** + * These tests have been written with reference to + * unicode 6.2, + * chapter 3, section 3.9. + */ +public class TestUtf8 { + + public static final List TEST_CASES; + + static { + // All known issues have been fixed in Java 8 + // https://bugs.openjdk.java.net/browse/JDK-8039751 + + ArrayList testCases = new ArrayList<>(); + + testCases.add(new Utf8TestCase( + "Zero length input", + new int[] {}, + -1, + "")); + testCases.add(new Utf8TestCase( + "Valid one byte sequence", + new int[] {0x41}, + -1, + "A")); + testCases.add(new Utf8TestCase( + "Valid two byte sequence", + new int[] {0xC2, 0xA9}, + -1, + "\u00A9")); + testCases.add(new Utf8TestCase( + "Valid three byte sequence", + new int[] {0xE0, 0xA4, 0x87}, + -1, + "\u0907")); + testCases.add(new Utf8TestCase( + "Valid four byte sequence", + new int[] {0xF0, 0x90, 0x90, 0x80}, + -1, + "\uD801\uDC00")); + testCases.add(new Utf8TestCase( + "Invalid code point - out of range", + new int[] {0x41, 0xF4, 0x90, 0x80, 0x80, 0x41}, + 2, + "A\uFFFD\uFFFD\uFFFD\uFFFDA")); + testCases.add(new Utf8TestCase( + "Valid sequence padded from one byte to two", + new int[] {0x41, 0xC0, 0xC1, 0x41}, + 1, + "A\uFFFD\uFFFDA")); + testCases.add(new Utf8TestCase( + "Valid sequence padded from one byte to three", + new int[] {0x41, 0xE0, 0x80, 0xC1, 0x41}, + 2, + "A\uFFFD\uFFFD\uFFFDA")); + testCases.add(new Utf8TestCase( + "Valid sequence padded from one byte to four", + new int[] {0x41, 0xF0, 0x80, 0x80, 0xC1, 0x41}, + 2, + "A\uFFFD\uFFFD\uFFFD\uFFFDA")); + testCases.add(new Utf8TestCase( + "Invalid one byte 1111 1111", + new int[] {0x41, 0xFF, 0x41}, + 1, + "A\uFFFDA")); + testCases.add(new Utf8TestCase( + "Invalid one byte 1111 0000", + new int[] {0x41, 0xF0, 0x41}, + 2, + "A\uFFFDA")); + testCases.add(new Utf8TestCase( + "Invalid one byte 1110 0000", + new int[] {0x41, 0xE0, 0x41}, + 2, + "A\uFFFDA")); + testCases.add(new Utf8TestCase( + "Invalid one byte 1100 0000", + new int[] {0x41, 0xC0, 0x41}, + 1, + "A\uFFFDA")); + testCases.add(new Utf8TestCase( + "Invalid one byte 1000 000", + new int[] {0x41, 0x80, 0x41}, + 1, + "A\uFFFDA")); + testCases.add(new Utf8TestCase( + "Invalid sequence from unicode 6.2 spec, table 3-8", + new int[] {0x61, 0xF1, 0x80, 0x80, 0xE1, 0x80, 0xC2, 0x62, 0x80, + 0x63, 0x80, 0xBF, 0x64}, + 4, + "a\uFFFD\uFFFD\uFFFDb\uFFFDc\uFFFD\uFFFDd")); + testCases.add(new Utf8TestCase( + "Valid 4-byte sequence truncated to 3 bytes", + new int[] {0x61, 0xF0, 0x90, 0x90}, + 3, + "a\uFFFD")); + testCases.add(new Utf8TestCase( + "Valid 4-byte sequence truncated to 2 bytes", + new int[] {0x61, 0xF0, 0x90}, + 2, + "a\uFFFD")); + testCases.add(new Utf8TestCase( + "Valid 4-byte sequence truncated to 1 byte", + new int[] {0x61, 0xF0}, + 1, + "a\uFFFD")); + testCases.add(new Utf8TestCase( + "Valid 4-byte sequence truncated to 3 bytes with trailer", + new int[] {0x61, 0xF0, 0x90, 0x90, 0x61}, + 4, + "a\uFFFDa")); + testCases.add(new Utf8TestCase( + "Valid 4-byte sequence truncated to 2 bytes with trailer", + new int[] {0x61, 0xF0, 0x90, 0x61}, + 3, + "a\uFFFDa")); + testCases.add(new Utf8TestCase( + "Valid 4-byte sequence truncated to 1 byte with trailer", + new int[] {0x61, 0xF0, 0x61}, + 2, + "a\uFFFDa")); + testCases.add(new Utf8TestCase( + "U+0000 zero-padded to two bytes", + new int[] {0x61, 0xC0, 0x80, 0x61}, + 1, + "a\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "U+007F zero-padded to two bytes", + new int[] {0x61, 0xC1, 0xBF, 0x61}, + 1, + "a\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Two bytes, all 1's", + new int[] {0x61, 0xFF, 0xFF, 0x61}, + 1, + "a\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Two bytes, 1110 first byte first nibble", + new int[] {0x61, 0xE0, 0x80, 0x61}, + 2, + "a\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Two bytes, 101x first byte first nibble", + new int[] {0x61, 0xA0, 0x80, 0x61}, + 1, + "a\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Two bytes, invalid second byte", + new int[] {0x61, 0xC2, 0x00, 0x61}, + 2, + "a\uFFFD\u0000a")); + testCases.add(new Utf8TestCase( + "Two bytes, invalid second byte", + new int[] {0x61, 0xC2, 0xC0, 0x61}, + 2, + "a\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Three bytes, U+0000 zero-padded", + new int[] {0x61, 0xE0, 0x80, 0x80, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Three bytes, U+007F zero-padded", + new int[] {0x61, 0xE0, 0x81, 0xBF, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Three bytes, U+07FF zero-padded", + new int[] {0x61, 0xE0, 0x9F, 0xBF, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Three bytes, all 1's", + new int[] {0x61, 0xFF, 0xFF, 0xFF, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Three bytes, invalid first byte", + new int[] {0x61, 0xF8, 0x80, 0x80, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Three bytes, invalid second byte", + new int[] {0x61, 0xE0, 0xC0, 0x80, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Three bytes, invalid third byte", + new int[] {0x61, 0xE1, 0x80, 0xC0, 0x61}, + 3, + "a\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, U+0000 zero-padded", + new int[] {0x61, 0xF0, 0x80, 0x80, 0x80, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, U+007F zero-padded", + new int[] {0x61, 0xF0, 0x80, 0x81, 0xBF, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, U+07FF zero-padded", + new int[] {0x61, 0xF0, 0x80, 0x9F, 0xBF, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, U+FFFF zero-padded", + new int[] {0x61, 0xF0, 0x8F, 0xBF, 0xBF, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, all 1's", + new int[] {0x61, 0xFF, 0xFF, 0xFF, 0xFF, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, invalid first byte", + new int[] {0x61, 0xF8, 0x80, 0x80, 0x80, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, invalid second byte", + new int[] {0x61, 0xF1, 0xC0, 0x80, 0x80, 0x61}, + 2, + "a\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, invalid third byte", + new int[] {0x61, 0xF1, 0x80, 0xC0, 0x80, 0x61}, + 3, + "a\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Four bytes, invalid fourth byte", + new int[] {0x61, 0xF1, 0x80, 0x80, 0xC0, 0x61}, + 4, + "a\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Five bytes, U+0000 zero padded", + new int[] {0x61, 0xF8, 0x80, 0x80, 0x80, 0x80, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Five bytes, U+007F zero padded", + new int[] {0x61, 0xF8, 0x80, 0x80, 0x81, 0xBF, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Five bytes, U+07FF zero padded", + new int[] {0x61, 0xF8, 0x80, 0x80, 0x9F, 0xBF, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Five bytes, U+FFFF zero padded", + new int[] {0x61, 0xF8, 0x80, 0x8F, 0xBF, 0xBF, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Six bytes, U+0000 zero padded", + new int[] {0x61, 0xFC, 0x80, 0x80, 0x80, 0x80, 0x80, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Six bytes, U+007F zero padded", + new int[] {0x61, 0xFC, 0x80, 0x80, 0x80, 0x81, 0xBF, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Six bytes, U+07FF zero padded", + new int[] {0x61, 0xFC, 0x80, 0x80, 0x80, 0x9F, 0xBF, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Six bytes, U+FFFF zero padded", + new int[] {0x61, 0xFC, 0x80, 0x80, 0x8F, 0xBF, 0xBF, 0x61}, + 1, + "a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDa")); + testCases.add(new Utf8TestCase( + "Original test case - derived from Autobahn?", + new int[] {0xCE, 0xBA, 0xE1, 0xDB, 0xB9, 0xCF, 0x83, 0xCE, + 0xBC, 0xCE, 0xB5, 0xED, 0x80, 0x65, 0x64, 0x69, + 0x74, 0x65, 0x64}, + 3, + "\u03BA\uFFFD\u06F9\u03C3\u03BC\u03B5\uFFFDedited")); + + TEST_CASES = Collections.unmodifiableList(testCases); + } + + @Test + public void testJvmDecoder() { + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); + int testCount = 0; + try { + for (Utf8TestCase testCase : TEST_CASES) { + doTest(decoder, testCase); + testCount++; + } + } finally { + if (testCount < TEST_CASES.size()) { + System.err.println("Executed " + testCount + " of " + + TEST_CASES.size() + " UTF-8 tests before " + + "encountering a failure"); + } + } + } + + + private void doTest(CharsetDecoder decoder, Utf8TestCase testCase) { + + int len = testCase.input.length; + ByteBuffer bb = ByteBuffer.allocate(len); + CharBuffer cb = CharBuffer.allocate(len); + + // Configure decoder to fail on an error + decoder.reset(); + decoder.onMalformedInput(CodingErrorAction.REPORT); + decoder.onUnmappableCharacter(CodingErrorAction.REPORT); + + // Add each byte one at a time. The decoder should fail as soon as + // an invalid sequence has been provided + for (int i = 0; i < len; i++) { + bb.put((byte) testCase.input[i]); + bb.flip(); + CoderResult cr = decoder.decode(bb, cb, false); + if (cr.isError()) { + int expected = testCase.invalidIndex; + Assert.assertEquals(testCase.description, expected, i); + break; + } + bb.compact(); + } + + // Configure decoder to replace on an error + decoder.reset(); + decoder.onMalformedInput(CodingErrorAction.REPLACE); + decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); + + // Add each byte one at a time. + bb.clear(); + cb.clear(); + for (int i = 0; i < len; i++) { + bb.put((byte) testCase.input[i]); + bb.flip(); + CoderResult cr = decoder.decode(bb, cb, false); + if (cr.isError()) { + Assert.fail(testCase.description); + } + bb.compact(); + } + // For incomplete sequences at the end of the input need to tell + // the decoder the input has ended + bb.flip(); + CoderResult cr = decoder.decode(bb, cb, true); + if (cr.isError()) { + Assert.fail(testCase.description); + } + cb.flip(); + + String expected = testCase.outputReplaced; + + Assert.assertEquals(testCase.description, expected, cb.toString()); + } + + + /** + * Encapsulates a single UTF-8 test case + */ + public static class Utf8TestCase { + public final String description; + public final int[] input; + public final int invalidIndex; + public final String outputReplaced; + + public Utf8TestCase(String description, int[] input, int invalidIndex, + String outputReplaced) { + this.description = description; + this.input = input; + this.invalidIndex = invalidIndex; + this.outputReplaced = outputReplaced; + } + } +} diff --git a/test/org/apache/tomcat/util/buf/TesterUriUtilBase.java b/test/org/apache/tomcat/util/buf/TesterUriUtilBase.java new file mode 100644 index 0000000..b176788 --- /dev/null +++ b/test/org/apache/tomcat/util/buf/TesterUriUtilBase.java @@ -0,0 +1,136 @@ +/* + * 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.tomcat.util.buf; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; + +public abstract class TesterUriUtilBase { + + private final String separator; + + protected TesterUriUtilBase(String separator) { + this.separator = separator; + TomcatURLStreamHandlerFactory.register(); + System.setProperty("org.apache.tomcat.util.buf.UriUtil.WAR_SEPARATOR", separator); + } + + + @Test + public void testBuildJarUrl01() throws MalformedURLException { + File jarFile = new File("/patha/pathb!/pathc"); + String result = UriUtil.buildJarUrl(jarFile).toString(); + + int index = result.indexOf("!/"); + Assert.assertEquals(result, result.length() - 2, index); + } + + + @Test + public void testBuildJarUrl02() throws MalformedURLException { + File jarFile = new File("/patha/pathb*/pathc"); + String result = UriUtil.buildJarUrl(jarFile).toString(); + + int index = result.indexOf("!/"); + Assert.assertEquals(result, result.length() - 2, index); + + index = result.indexOf("*/"); + Assert.assertEquals(result, -1, index); + } + + + @Test + public void testBuildJarUrl03() throws MalformedURLException { + File jarFile = new File("/patha/pathb^/pathc"); + String result = UriUtil.buildJarUrl(jarFile).toString(); + + int index = result.indexOf("!/"); + Assert.assertEquals(result, result.length() - 2, index); + + index = result.indexOf("^/"); + Assert.assertEquals(result, -1, index); + } + + + @Test + public void testBuildJarUrl04() throws MalformedURLException { + File jarFile = new File("/patha/pathb" + separator + "/pathc"); + String result = UriUtil.buildJarUrl(jarFile).toString(); + + int index = result.indexOf("!/"); + Assert.assertEquals(result, result.length() - 2, index); + + index = result.indexOf(separator + "/"); + Assert.assertEquals(result, -1, index); + } + + + @Test + public void testWarToJar01() throws MalformedURLException { + doTestWarToJar("^"); + } + + + @Test + public void testWarToJar02() throws MalformedURLException { + doTestWarToJar("*"); + } + + + @Test + public void testWarToJar03() throws MalformedURLException { + doTestWarToJar(separator); + } + + + private void doTestWarToJar(String separator) throws MalformedURLException { + URL warUrl = new URL("war:file:/external/path" + separator + "/internal/path"); + URL jarUrl = UriUtil.warToJar(warUrl); + Assert.assertEquals("jar:file:/external/path!/internal/path", jarUrl.toString()); + } + + + // @Test /* Uncomment to test performance for different implementations. */ + public void performanceTestBuildJarUrl() throws MalformedURLException { + File jarFile = new File("/patha/pathb^/pathc"); + + URL url = null; + + int count = 1000000; + + // Warm up + for (int i = 0; i < count / 10; i++) { + url = UriUtil.buildJarUrl(jarFile); + } + + // Test + long start = System.nanoTime(); + for (int i = 0; i < count / 10; i++) { + url = UriUtil.buildJarUrl(jarFile); + } + long duration = System.nanoTime() - start; + + System.out.println("[" + count + "] iterations took [" + + duration + "] ns for [" + url + "]"); + } +} diff --git a/test/org/apache/tomcat/util/collections/TestCaseInsensitiveKeyMap.java b/test/org/apache/tomcat/util/collections/TestCaseInsensitiveKeyMap.java new file mode 100644 index 0000000..c094bfa --- /dev/null +++ b/test/org/apache/tomcat/util/collections/TestCaseInsensitiveKeyMap.java @@ -0,0 +1,215 @@ +/* + * 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.tomcat.util.collections; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +public class TestCaseInsensitiveKeyMap { + + @Test + public void testPut() { + Object o1 = new Object(); + Object o2 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + Object o = map.put("A", o2); + + Assert.assertEquals(o1, o); + + Assert.assertEquals(o2, map.get("a")); + Assert.assertEquals(o2, map.get("A")); + } + + + @Test(expected=NullPointerException.class) + public void testPutNullKey() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put(null, o1); + } + + + @Test + public void testGet() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Assert.assertEquals(o1, map.get("a")); + Assert.assertEquals(o1, map.get("A")); + } + + + @Test + public void testGetNullKey() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Assert.assertNull(map.get(null)); + } + + + @Test + public void testContainsKey() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Assert.assertTrue(map.containsKey("a")); + Assert.assertTrue(map.containsKey("A")); + } + + + @Test + public void testContainsKeyNonString() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Assert.assertFalse(map.containsKey(o1)); + } + + + @Test + public void testContainsKeyNull() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Assert.assertFalse(map.containsKey(null)); + } + + + @Test + public void testContainsValue() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Assert.assertTrue(map.containsValue(o1)); + } + + + @Test + public void testRemove() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + Assert.assertFalse(map.isEmpty()); + map.remove("A"); + Assert.assertTrue(map.isEmpty()); + + map.put("A", o1); + Assert.assertFalse(map.isEmpty()); + map.remove("a"); + Assert.assertTrue(map.isEmpty()); + } + + + @Test + public void testClear() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + for (int i = 0; i < 10; i++) { + map.put(Integer.toString(i), o1); + } + Assert.assertEquals(10, map.size()); + map.clear(); + Assert.assertEquals(0, map.size()); + } + + + @Test + public void testPutAll() { + Object o1 = new Object(); + Object o2 = new Object(); + + Map source = new HashMap<>(); + source.put("a", o1); + source.put("A", o2); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.putAll(source); + + Assert.assertEquals(1, map.size()); + Assert.assertTrue(map.containsValue(o1) != map.containsValue(o2)); + } + + + @Test + public void testKeySetContains() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Set keys = map.keySet(); + + Assert.assertTrue(keys.contains("a")); + Assert.assertTrue(keys.contains("A")); + } + + + @Test + public void testKeySetRemove() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Iterator iter = map.keySet().iterator(); + Assert.assertTrue(iter.hasNext()); + iter.next(); + iter.remove(); + Assert.assertTrue(map.isEmpty()); + } + + + @Test + public void testEntrySetRemove() { + Object o1 = new Object(); + + CaseInsensitiveKeyMap map = new CaseInsensitiveKeyMap<>(); + map.put("a", o1); + + Iterator> iter = map.entrySet().iterator(); + Assert.assertTrue(iter.hasNext()); + Entry entry = iter.next(); + Assert.assertEquals("a", entry.getKey()); + Assert.assertEquals(o1, entry.getValue()); + iter.remove(); + Assert.assertTrue(map.isEmpty()); + } +} diff --git a/test/org/apache/tomcat/util/collections/TestSynchronizedQueue.java b/test/org/apache/tomcat/util/collections/TestSynchronizedQueue.java new file mode 100644 index 0000000..7ecad08 --- /dev/null +++ b/test/org/apache/tomcat/util/collections/TestSynchronizedQueue.java @@ -0,0 +1,117 @@ +/* + * 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.tomcat.util.collections; + +import org.junit.Assert; +import org.junit.Test; + +public class TestSynchronizedQueue { + + public void testPollEmpty() { + SynchronizedQueue queue = new SynchronizedQueue<>(); + Assert.assertNull(queue.poll()); + } + + @Test + public void testOfferPollOrder() { + SynchronizedQueue queue = new SynchronizedQueue<>(); + + Object o1 = new Object(); + Object o2 = new Object(); + Object o3 = new Object(); + Object o4 = new Object(); + + queue.offer(o1); + queue.offer(o2); + queue.offer(o3); + queue.offer(o4); + + Assert.assertSame(queue.poll(), o1); + Assert.assertSame(queue.poll(), o2); + Assert.assertSame(queue.poll(), o3); + Assert.assertSame(queue.poll(), o4); + + Assert.assertNull(queue.poll()); + } + + @Test + public void testExpandOfferPollOrder() { + SynchronizedQueue queue = new SynchronizedQueue<>(); + + Object o1 = new Object(); + Object o2 = new Object(); + Object o3 = new Object(); + Object o4 = new Object(); + + for (int i = 0; i < 300; i++) { + queue.offer(o1); + queue.offer(o2); + queue.offer(o3); + queue.offer(o4); + } + + for (int i = 0; i < 300; i++) { + Assert.assertSame(queue.poll(), o1); + Assert.assertSame(queue.poll(), o2); + Assert.assertSame(queue.poll(), o3); + Assert.assertSame(queue.poll(), o4); + } + + Assert.assertNull(queue.poll()); + } + + @Test + public void testExpandOfferPollOrder2() { + SynchronizedQueue queue = new SynchronizedQueue<>(); + + Object o1 = new Object(); + Object o2 = new Object(); + Object o3 = new Object(); + Object o4 = new Object(); + + for (int i = 0; i < 100; i++) { + queue.offer(o1); + queue.offer(o2); + queue.offer(o3); + queue.offer(o4); + } + + for (int i = 0; i < 50; i++) { + Assert.assertSame(queue.poll(), o1); + Assert.assertSame(queue.poll(), o2); + Assert.assertSame(queue.poll(), o3); + Assert.assertSame(queue.poll(), o4); + } + + for (int i = 0; i < 200; i++) { + queue.offer(o1); + queue.offer(o2); + queue.offer(o3); + queue.offer(o4); + } + + for (int i = 0; i < 250; i++) { + Assert.assertSame(queue.poll(), o1); + Assert.assertSame(queue.poll(), o2); + Assert.assertSame(queue.poll(), o3); + Assert.assertSame(queue.poll(), o4); + } + + + Assert.assertNull(queue.poll()); + } +} diff --git a/test/org/apache/tomcat/util/collections/TestSynchronizedStack.java b/test/org/apache/tomcat/util/collections/TestSynchronizedStack.java new file mode 100644 index 0000000..f112bce --- /dev/null +++ b/test/org/apache/tomcat/util/collections/TestSynchronizedStack.java @@ -0,0 +1,119 @@ +/* + * 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.tomcat.util.collections; + +import org.junit.Assert; +import org.junit.Test; + +public class TestSynchronizedStack { + + @Test + public void testPopEmpty() { + SynchronizedStack stack = new SynchronizedStack<>(); + Assert.assertNull(stack.pop()); + } + + @Test + public void testPushPopOrder() { + SynchronizedStack stack = new SynchronizedStack<>(); + + Object o1 = new Object(); + Object o2 = new Object(); + Object o3 = new Object(); + Object o4 = new Object(); + + stack.push(o1); + stack.push(o2); + stack.push(o3); + stack.push(o4); + + Assert.assertSame(stack.pop(), o4); + Assert.assertSame(stack.pop(), o3); + Assert.assertSame(stack.pop(), o2); + Assert.assertSame(stack.pop(), o1); + + Assert.assertNull(stack.pop()); + } + + @Test + public void testExpandPushPopOrder() { + SynchronizedStack stack = new SynchronizedStack<>(); + + Object o1 = new Object(); + Object o2 = new Object(); + Object o3 = new Object(); + Object o4 = new Object(); + + for (int i = 0; i < 300; i++) { + stack.push(o1); + stack.push(o2); + stack.push(o3); + stack.push(o4); + } + + for (int i = 0; i < 300; i++) { + Assert.assertSame(stack.pop(), o4); + Assert.assertSame(stack.pop(), o3); + Assert.assertSame(stack.pop(), o2); + Assert.assertSame(stack.pop(), o1); + } + + Assert.assertNull(stack.pop()); + } + + @Test + public void testLimit() { + SynchronizedStack stack = new SynchronizedStack<>(2,2); + + Object o1 = new Object(); + Object o2 = new Object(); + Object o3 = new Object(); + Object o4 = new Object(); + + stack.push(o1); + stack.push(o2); + stack.push(o3); + stack.push(o4); + + Assert.assertSame(stack.pop(), o2); + Assert.assertSame(stack.pop(), o1); + + Assert.assertNull(stack.pop()); + } + + + @Test + public void testLimitExpand() { + SynchronizedStack stack = new SynchronizedStack<>(1,3); + + Object o1 = new Object(); + Object o2 = new Object(); + Object o3 = new Object(); + Object o4 = new Object(); + + stack.push(o1); + stack.push(o2); + stack.push(o3); + stack.push(o4); + + Assert.assertSame(stack.pop(), o3); + Assert.assertSame(stack.pop(), o2); + Assert.assertSame(stack.pop(), o1); + + Assert.assertNull(stack.pop()); + } +} diff --git a/test/org/apache/tomcat/util/collections/TesterPerformanceSynchronizedQueue.java b/test/org/apache/tomcat/util/collections/TesterPerformanceSynchronizedQueue.java new file mode 100644 index 0000000..df79c72 --- /dev/null +++ b/test/org/apache/tomcat/util/collections/TesterPerformanceSynchronizedQueue.java @@ -0,0 +1,107 @@ +/* + * 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.tomcat.util.collections; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.junit.Test; + +public class TesterPerformanceSynchronizedQueue { + + private static final int THREAD_COUNT = 4; + private static final int ITERATIONS = 1000000; + + private static final SynchronizedQueue S_QUEUE = + new SynchronizedQueue<>(); + + private static final Queue QUEUE = new ConcurrentLinkedQueue<>(); + + @Test + public void testSynchronizedQueue() throws InterruptedException { + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new StackThread(); + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i].start(); + } + + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i].join(); + } + + long end = System.currentTimeMillis(); + + System.out.println("SynchronizedQueue: " + (end - start) + "ms"); + } + + public static class StackThread extends Thread { + + @Override + public void run() { + for(int i = 0; i < ITERATIONS; i++) { + Object obj = S_QUEUE.poll(); + if (obj == null) { + obj = new Object(); + } + S_QUEUE.offer(obj); + } + super.run(); + } + } + + @Test + public void testConcurrentQueue() throws InterruptedException { + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new QueueThread(); + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i].start(); + } + + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i].join(); + } + + long end = System.currentTimeMillis(); + + System.out.println("ConcurrentLinkedQueue: " + (end - start) + "ms"); + } + + public static class QueueThread extends Thread { + + @Override + public void run() { + for(int i = 0; i < ITERATIONS; i++) { + Object obj = QUEUE.poll(); + if (obj == null) { + obj = new Object(); + } + QUEUE.offer(obj); + } + super.run(); + } + } +} diff --git a/test/org/apache/tomcat/util/collections/TesterPerformanceSynchronizedStack.java b/test/org/apache/tomcat/util/collections/TesterPerformanceSynchronizedStack.java new file mode 100644 index 0000000..e601891 --- /dev/null +++ b/test/org/apache/tomcat/util/collections/TesterPerformanceSynchronizedStack.java @@ -0,0 +1,107 @@ +/* + * 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.tomcat.util.collections; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.junit.Test; + +public class TesterPerformanceSynchronizedStack { + + private static final int THREAD_COUNT = 4; + private static final int ITERATIONS = 1000000; + + private static final SynchronizedStack STACK = + new SynchronizedStack<>(); + + private static final Queue QUEUE = new ConcurrentLinkedQueue<>(); + + @Test + public void testSynchronizedStack() throws InterruptedException { + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new StackThread(); + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i].start(); + } + + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i].join(); + } + + long end = System.currentTimeMillis(); + + System.out.println("SynchronizedStack: " + (end - start) + "ms"); + } + + public static class StackThread extends Thread { + + @Override + public void run() { + for(int i = 0; i < ITERATIONS; i++) { + Object obj = STACK.pop(); + if (obj == null) { + obj = new Object(); + } + STACK.push(obj); + } + super.run(); + } + } + + @Test + public void testConcurrentQueue() throws InterruptedException { + Thread[] threads = new Thread[THREAD_COUNT]; + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i] = new QueueThread(); + } + + long start = System.currentTimeMillis(); + + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i].start(); + } + + for (int i = 0; i < THREAD_COUNT; i++) { + threads[i].join(); + } + + long end = System.currentTimeMillis(); + + System.out.println("ConcurrentLinkedQueue: " + (end - start) + "ms"); + } + + public static class QueueThread extends Thread { + + @Override + public void run() { + for(int i = 0; i < ITERATIONS; i++) { + Object obj = QUEUE.poll(); + if (obj == null) { + obj = new Object(); + } + QUEUE.offer(obj); + } + super.run(); + } + } +} diff --git a/test/org/apache/tomcat/util/descriptor/TestLocalResolver.java b/test/org/apache/tomcat/util/descriptor/TestLocalResolver.java new file mode 100644 index 0000000..8ebe28f --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/TestLocalResolver.java @@ -0,0 +1,125 @@ +/* + * 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.tomcat.util.descriptor; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.ServletContext; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class TestLocalResolver { + + private final Map publicIds = new HashMap<>(); + private final Map systemIds = new HashMap<>(); + + private LocalResolver resolver = new LocalResolver(publicIds, systemIds, true); + private String WEB_22_LOCAL; + private String WEB_31_LOCAL; + private String WEBCOMMON_31_LOCAL; + + @Before + public void init() { + WEB_22_LOCAL = urlFor("resources/web-app_2_2.dtd"); + WEB_31_LOCAL = urlFor("resources/web-app_3_1.xsd"); + WEBCOMMON_31_LOCAL = urlFor("resources/web-common_3_1.xsd"); + publicIds.put(XmlIdentifiers.WEB_22_PUBLIC, WEB_22_LOCAL); + systemIds.put(XmlIdentifiers.WEB_31_XSD, WEB_31_LOCAL); + systemIds.put(WEBCOMMON_31_LOCAL, WEBCOMMON_31_LOCAL); + } + + public String urlFor(String id) { + return ServletContext.class.getResource(id).toExternalForm(); + } + + @Test(expected = FileNotFoundException.class) + public void unknownNullId() throws IOException, SAXException { + Assert.assertNull(resolver.resolveEntity(null, null)); + } + + @Test(expected = FileNotFoundException.class) + public void unknownPublicId() throws IOException, SAXException { + Assert.assertNull(resolver.resolveEntity("unknown", null)); + } + + @Test(expected = FileNotFoundException.class) + public void unknownSystemId() throws IOException, SAXException { + InputSource source = resolver.resolveEntity(null, "unknown"); + Assert.assertEquals(null, source.getPublicId()); + Assert.assertEquals("unknown", source.getSystemId()); + } + + @Test(expected = FileNotFoundException.class) + public void unknownRelativeSystemId() + throws IOException, SAXException { + InputSource source = resolver.resolveEntity( + null, null, "http://example.com/home.html", "unknown"); + Assert.assertEquals(null, source.getPublicId()); + Assert.assertEquals("http://example.com/unknown", source.getSystemId()); + } + + @Test + public void publicIdIsResolved() throws IOException, SAXException { + InputSource source = resolver.resolveEntity( + XmlIdentifiers.WEB_22_PUBLIC, XmlIdentifiers.WEB_22_SYSTEM); + Assert.assertEquals(XmlIdentifiers.WEB_22_PUBLIC, source.getPublicId()); + Assert.assertEquals(WEB_22_LOCAL, source.getSystemId()); + } + + @Test + public void systemIdIsIgnoredWhenPublicIdIsResolved() + throws IOException, SAXException { + InputSource source = resolver.resolveEntity( + XmlIdentifiers.WEB_22_PUBLIC, "unknown"); + Assert.assertEquals(XmlIdentifiers.WEB_22_PUBLIC, source.getPublicId()); + Assert.assertEquals(WEB_22_LOCAL, source.getSystemId()); + } + + @Test + public void systemIdIsResolved() throws IOException, SAXException { + InputSource source = + resolver.resolveEntity(null, XmlIdentifiers.WEB_31_XSD); + Assert.assertEquals(null, source.getPublicId()); + Assert.assertEquals(WEB_31_LOCAL, source.getSystemId()); + } + + @Test + public void relativeSystemIdIsResolvedAgainstBaseURI() + throws IOException, SAXException { + InputSource source = resolver.resolveEntity( + null, null, WEB_31_LOCAL, "web-common_3_1.xsd"); + Assert.assertEquals(null, source.getPublicId()); + Assert.assertEquals(WEBCOMMON_31_LOCAL, source.getSystemId()); + } + + @Test + public void absoluteSystemIdOverridesBaseURI() + throws IOException, SAXException { + InputSource source = resolver.resolveEntity(null, null, + "http://example.com/home.html", XmlIdentifiers.WEB_31_XSD); + Assert.assertEquals(null, source.getPublicId()); + Assert.assertEquals(WEB_31_LOCAL, source.getSystemId()); + } +} diff --git a/test/org/apache/tomcat/util/descriptor/tld/TestImplicitTldParser.java b/test/org/apache/tomcat/util/descriptor/tld/TestImplicitTldParser.java new file mode 100644 index 0000000..8d13e73 --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/tld/TestImplicitTldParser.java @@ -0,0 +1,59 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import java.io.File; +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public class TestImplicitTldParser { + private TldParser parser; + + @Before + public void init() { + parser = new TldParser(true, true, new ImplicitTldRuleSet(), true); + } + + @Test + public void testImplicitTldGood() throws Exception { + TaglibXml xml = parse("test/tld/implicit-good.tld"); + Assert.assertEquals("1.0", xml.getTlibVersion()); + Assert.assertEquals("2.1", xml.getJspVersion()); + Assert.assertEquals("Ignored", xml.getShortName()); + } + + @Test(expected=SAXParseException.class) + public void testImplicitTldBad() throws Exception { + TaglibXml xml = parse("test/tld/implicit-bad.tld"); + Assert.assertEquals("1.0", xml.getTlibVersion()); + Assert.assertEquals("2.1", xml.getJspVersion()); + Assert.assertEquals("Ignored", xml.getShortName()); + } + + private TaglibXml parse(String pathname) throws IOException, SAXException { + File file = new File(pathname); + TldResourcePath path = new TldResourcePath(file.toURI().toURL(), null); + return parser.parse(path); + } + +} diff --git a/test/org/apache/tomcat/util/descriptor/tld/TestTldParser.java b/test/org/apache/tomcat/util/descriptor/tld/TestTldParser.java new file mode 100644 index 0000000..d04d691 --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/tld/TestTldParser.java @@ -0,0 +1,173 @@ +/* + * 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.tomcat.util.descriptor.tld; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import jakarta.servlet.jsp.tagext.FunctionInfo; +import jakarta.servlet.jsp.tagext.TagAttributeInfo; +import jakarta.servlet.jsp.tagext.TagVariableInfo; +import jakarta.servlet.jsp.tagext.VariableInfo; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.xml.sax.SAXException; + +public class TestTldParser { + private TldParser parser; + + @Before + public void init() { + parser = new TldParser(true, true, new TldRuleSet(), true); + } + + @Test + public void testTld() throws Exception { + TaglibXml xml = parse("test/tld/test.tld"); + Assert.assertEquals("1.0", xml.getTlibVersion()); + Assert.assertEquals("2.1", xml.getJspVersion()); + Assert.assertEquals("test", xml.getShortName()); + Assert.assertEquals("http://tomcat.apache.org/TldTests", xml.getUri()); + Assert.assertEquals(1, xml.getFunctions().size()); + + ValidatorXml validator = xml.getValidator(); + Assert.assertEquals("com.example.Validator", validator.getValidatorClass()); + Assert.assertEquals(1, validator.getInitParams().size()); + Assert.assertEquals("value", validator.getInitParams().get("name")); + + Assert.assertEquals(1, xml.getTags().size()); + TagXml tag = xml.getTags().get(0); + Assert.assertEquals("org.apache.jasper.compiler.TestValidator$Echo", tag.getTagClass()); + Assert.assertEquals("empty", tag.getBodyContent()); + Assert.assertTrue(tag.hasDynamicAttributes()); + + Assert.assertEquals(1, tag.getVariables().size()); + TagVariableInfo variableInfo = tag.getVariables().get(0); + Assert.assertEquals("var", variableInfo.getNameGiven()); + Assert.assertEquals("java.lang.Object", variableInfo.getClassName()); + Assert.assertTrue(variableInfo.getDeclare()); + Assert.assertEquals(VariableInfo.AT_END, variableInfo.getScope()); + + Assert.assertEquals(4, tag.getAttributes().size()); + TagAttributeInfo attributeInfo = tag.getAttributes().get(0); + Assert.assertEquals("Echo Tag", tag.getInfo()); + Assert.assertEquals("Echo", tag.getDisplayName()); + Assert.assertEquals("small", tag.getSmallIcon()); + Assert.assertEquals("large", tag.getLargeIcon()); + Assert.assertEquals("echo", attributeInfo.getName()); + Assert.assertTrue(attributeInfo.isRequired()); + Assert.assertTrue(attributeInfo.canBeRequestTime()); + + attributeInfo = tag.getAttributes().get(1); + Assert.assertEquals("fragment", attributeInfo.getName()); + Assert.assertTrue(attributeInfo.isFragment()); + Assert.assertTrue(attributeInfo.canBeRequestTime()); + Assert.assertEquals("jakarta.servlet.jsp.tagext.JspFragment", attributeInfo.getTypeName()); + + attributeInfo = tag.getAttributes().get(2); + Assert.assertEquals("deferredValue", attributeInfo.getName()); + Assert.assertEquals("jakarta.el.ValueExpression", attributeInfo.getTypeName()); + Assert.assertEquals("java.util.Date", attributeInfo.getExpectedTypeName()); + + attributeInfo = tag.getAttributes().get(3); + Assert.assertEquals("deferredMethod", attributeInfo.getName()); + Assert.assertEquals("jakarta.el.MethodExpression", attributeInfo.getTypeName()); + Assert.assertEquals("java.util.Date getDate()", attributeInfo.getMethodSignature()); + + Assert.assertEquals(1, xml.getTagFiles().size()); + TagFileXml tagFile = xml.getTagFiles().get(0); + Assert.assertEquals("Echo", tag.getDisplayName()); + Assert.assertEquals("small", tag.getSmallIcon()); + Assert.assertEquals("large", tag.getLargeIcon()); + Assert.assertEquals("Echo2", tagFile.getName()); + Assert.assertEquals("/echo.tag", tagFile.getPath()); + + Assert.assertEquals(1, xml.getFunctions().size()); + FunctionInfo fn = xml.getFunctions().get(0); + Assert.assertEquals("trim", fn.getName()); + Assert.assertEquals("org.apache.el.TesterFunctions", fn.getFunctionClass()); + Assert.assertEquals("java.lang.String trim(java.lang.String)", fn.getFunctionSignature()); + } + + @Test + public void testParseTld21() throws Exception { + TaglibXml xml = parse("test/tld/tags21.tld"); + Assert.assertEquals("1.0", xml.getTlibVersion()); + Assert.assertEquals("2.1", xml.getJspVersion()); + Assert.assertEquals("Tags21", xml.getShortName()); + Assert.assertEquals("http://tomcat.apache.org/tags21", xml.getUri()); + verifyTags(xml.getTags()); + } + + @Test + public void testParseTld20() throws Exception { + TaglibXml xml = parse("test/tld/tags20.tld"); + Assert.assertEquals("1.0", xml.getTlibVersion()); + Assert.assertEquals("2.0", xml.getJspVersion()); + Assert.assertEquals("Tags20", xml.getShortName()); + Assert.assertEquals("http://tomcat.apache.org/tags20", xml.getUri()); + verifyTags(xml.getTags()); + } + + @Test + public void testParseTld12() throws Exception { + TaglibXml xml = parse("test/tld/tags12.tld"); + Assert.assertEquals("1.0", xml.getTlibVersion()); + Assert.assertEquals("1.2", xml.getJspVersion()); + Assert.assertEquals("Tags12", xml.getShortName()); + Assert.assertEquals("http://tomcat.apache.org/tags12", xml.getUri()); + verifyTags(xml.getTags()); + } + + @Test + public void testParseTld11() throws Exception { + TaglibXml xml = parse("test/tld/tags11.tld"); + Assert.assertEquals("1.0", xml.getTlibVersion()); + Assert.assertEquals("1.1", xml.getJspVersion()); + Assert.assertEquals("Tags11", xml.getShortName()); + Assert.assertEquals("http://tomcat.apache.org/tags11", xml.getUri()); + verifyTags(xml.getTags()); + } + + private void verifyTags(List tags) { + Assert.assertEquals(1, tags.size()); + TagXml tag = tags.get(0); + Assert.assertEquals("Echo", tag.getName()); + Assert.assertEquals("org.apache.jasper.compiler.TestValidator$Echo", tag.getTagClass()); + Assert.assertEquals("empty", tag.getBodyContent()); + } + + @Test + public void testListener() throws Exception { + TaglibXml xml = parse("test/tld/listener.tld"); + Assert.assertEquals("1.0", xml.getTlibVersion()); + List listeners = xml.getListeners(); + Assert.assertEquals(1, listeners.size()); + Assert.assertEquals("org.apache.catalina.core.TesterTldListener", listeners.get(0)); + } + + private TaglibXml parse(String pathname) throws IOException, SAXException { + File file = new File(pathname); + TldResourcePath path = new TldResourcePath(file.toURI().toURL(), null); + return parser.parse(path); + } + +} diff --git a/test/org/apache/tomcat/util/descriptor/web/TestFilterDef.java b/test/org/apache/tomcat/util/descriptor/web/TestFilterDef.java new file mode 100644 index 0000000..726b8a2 --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestFilterDef.java @@ -0,0 +1,45 @@ +/* + * 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.tomcat.util.descriptor.web; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test case for {@link FilterDef}. + */ +public class TestFilterDef { + + @Test(expected = IllegalArgumentException.class) + public void testSetFilterNameNull() { + new FilterDef().setFilterName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetFilterNameEmptyString() { + new FilterDef().setFilterName(""); + } + + @Test + public void testSetFilterName() { + FilterDef filterDef = new FilterDef(); + filterDef.setFilterName("test"); + Assert.assertEquals("'test' is expected as filter name", + "test", filterDef.getFilterName()); + } + +} \ No newline at end of file diff --git a/test/org/apache/tomcat/util/descriptor/web/TestJspConfigDescriptorImpl.java b/test/org/apache/tomcat/util/descriptor/web/TestJspConfigDescriptorImpl.java new file mode 100644 index 0000000..5374bbf --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestJspConfigDescriptorImpl.java @@ -0,0 +1,51 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jakarta.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.descriptor.JspPropertyGroupDescriptor; +import jakarta.servlet.descriptor.TaglibDescriptor; + +import org.junit.Assert; +import org.junit.Test; + +public class TestJspConfigDescriptorImpl { + + @Test + public void testTaglibsAreIsolate() { + List taglibs = new ArrayList<>(); + taglibs.add(new TaglibDescriptorImpl("location", "uri")); + List propertyGroups = Collections.emptyList(); + JspConfigDescriptor descriptor = new JspConfigDescriptorImpl(propertyGroups, taglibs); + descriptor.getTaglibs().clear(); + Assert.assertEquals(taglibs, descriptor.getTaglibs()); + } + + @Test + public void testPropertyGroupsAreIsolate() { + List taglibs = Collections.emptyList(); + List propertyGroups = new ArrayList<>(); + propertyGroups.add(new JspPropertyGroupDescriptorImpl(new JspPropertyGroup())); + JspConfigDescriptor descriptor = new JspConfigDescriptorImpl(propertyGroups, taglibs); + descriptor.getJspPropertyGroups().clear(); + Assert.assertEquals(propertyGroups, descriptor.getJspPropertyGroups()); + } +} diff --git a/test/org/apache/tomcat/util/descriptor/web/TestJspPropertyGroup.java b/test/org/apache/tomcat/util/descriptor/web/TestJspPropertyGroup.java new file mode 100644 index 0000000..de0849e --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestJspPropertyGroup.java @@ -0,0 +1,35 @@ +/* + * 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.tomcat.util.descriptor.web; + +import org.junit.Assert; +import org.junit.Test; + +public class TestJspPropertyGroup { + + private JspPropertyGroup group = new JspPropertyGroup(); + + @Test + public void testBug55262() { + group.addIncludePrelude("/prelude"); + group.addIncludePrelude("/prelude"); + group.addIncludeCoda("/coda"); + group.addIncludeCoda("/coda"); + Assert.assertEquals(2, group.getIncludePreludes().size()); + Assert.assertEquals(2, group.getIncludeCodas().size()); + } +} diff --git a/test/org/apache/tomcat/util/descriptor/web/TestJspPropertyGroupDescriptorImpl.java b/test/org/apache/tomcat/util/descriptor/web/TestJspPropertyGroupDescriptorImpl.java new file mode 100644 index 0000000..15ce498 --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestJspPropertyGroupDescriptorImpl.java @@ -0,0 +1,50 @@ +/* + * 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.tomcat.util.descriptor.web; + +import org.junit.Assert; +import org.junit.Test; + +public class TestJspPropertyGroupDescriptorImpl { + + @Test + public void testPreludesAreIsolated() { + JspPropertyGroup jpg = new JspPropertyGroup(); + jpg.addIncludePrelude("prelude"); + JspPropertyGroupDescriptorImpl descriptor = new JspPropertyGroupDescriptorImpl(jpg); + descriptor.getIncludePreludes().clear(); + Assert.assertEquals(1, descriptor.getIncludePreludes().size()); + } + + @Test + public void testCodasAreIsolated() { + JspPropertyGroup jpg = new JspPropertyGroup(); + jpg.addIncludeCoda("coda"); + JspPropertyGroupDescriptorImpl descriptor = new JspPropertyGroupDescriptorImpl(jpg); + descriptor.getIncludeCodas().clear(); + Assert.assertEquals(1, descriptor.getIncludeCodas().size()); + } + + @Test + public void testUrlPatternsAreIsolated() { + JspPropertyGroup jpg = new JspPropertyGroup(); + jpg.addUrlPatternDecoded("pattern"); + JspPropertyGroupDescriptorImpl descriptor = new JspPropertyGroupDescriptorImpl(jpg); + descriptor.getUrlPatterns().clear(); + Assert.assertEquals(1, descriptor.getUrlPatterns().size()); + } +} diff --git a/test/org/apache/tomcat/util/descriptor/web/TestSecurityConstraint.java b/test/org/apache/tomcat/util/descriptor/web/TestSecurityConstraint.java new file mode 100644 index 0000000..a9ce27a --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestSecurityConstraint.java @@ -0,0 +1,452 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import jakarta.servlet.HttpConstraintElement; +import jakarta.servlet.HttpMethodConstraintElement; +import jakarta.servlet.ServletSecurityElement; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class TestSecurityConstraint { + + private static final String URL_PATTERN = "/test"; + private static final String ROLE1 = "R1"; + + private static final Log DUMMY_LOG = LogFactory.getLog("DUMMY"); + + private static final SecurityConstraint GET_ONLY; + private static final SecurityConstraint POST_ONLY; + + private static final SecurityConstraint GET_OMIT; + private static final SecurityConstraint POST_OMIT; + + static { + // Configure the constraints to use in the tests + GET_ONLY = new SecurityConstraint(); + GET_ONLY.addAuthRole(ROLE1); + SecurityCollection scGetOnly = new SecurityCollection(); + scGetOnly.addMethod("GET"); + scGetOnly.addPatternDecoded(URL_PATTERN); + scGetOnly.setName("GET-ONLY"); + GET_ONLY.addCollection(scGetOnly); + + POST_ONLY = new SecurityConstraint(); + POST_ONLY.addAuthRole(ROLE1); + SecurityCollection scPostOnly = new SecurityCollection(); + scPostOnly.addMethod("POST"); + scPostOnly.addPatternDecoded(URL_PATTERN); + scPostOnly.setName("POST_ONLY"); + POST_ONLY.addCollection(scPostOnly); + + GET_OMIT = new SecurityConstraint(); + GET_OMIT.addAuthRole(ROLE1); + SecurityCollection scGetOmit = new SecurityCollection(); + scGetOmit.addOmittedMethod("GET"); + scGetOmit.addPatternDecoded(URL_PATTERN); + scGetOmit.setName("GET_OMIT"); + GET_OMIT.addCollection(scGetOmit); + + POST_OMIT = new SecurityConstraint(); + POST_OMIT.addAuthRole(ROLE1); + SecurityCollection scPostOmit = new SecurityCollection(); + scPostOmit.addOmittedMethod("POST"); + scPostOmit.addPatternDecoded(URL_PATTERN); + scPostOmit.setName("POST_OMIT"); + POST_OMIT.addCollection(scPostOmit); + } + + /** + * Uses the examples in SRV.13.4 as the basis for these tests + */ + @Test + public void testCreateConstraints() { + + ServletSecurityElement element; + SecurityConstraint[] result; + Set hmces = new HashSet<>(); + + // Example 13-1 + // @ServletSecurity + element = new ServletSecurityElement(); + result = SecurityConstraint.createConstraints(element, URL_PATTERN); + + Assert.assertEquals(0, result.length); + + // Example 13-2 + // @ServletSecurity( + // @HttpConstraint( + // transportGuarantee = TransportGuarantee.CONFIDENTIAL)) + element = new ServletSecurityElement( + new HttpConstraintElement( + ServletSecurity.TransportGuarantee.CONFIDENTIAL)); + result = SecurityConstraint.createConstraints(element, URL_PATTERN); + + Assert.assertEquals(1, result.length); + Assert.assertFalse(result[0].getAuthConstraint()); + Assert.assertTrue(result[0].findCollections()[0].findPattern(URL_PATTERN)); + Assert.assertEquals(0, result[0].findCollections()[0].findMethods().length); + Assert.assertEquals(ServletSecurity.TransportGuarantee.CONFIDENTIAL.name(), + result[0].getUserConstraint()); + + // Example 13-3 + // @ServletSecurity(@HttpConstraint(EmptyRoleSemantic.DENY)) + element = new ServletSecurityElement( + new HttpConstraintElement(EmptyRoleSemantic.DENY)); + result = SecurityConstraint.createConstraints(element, URL_PATTERN); + + Assert.assertEquals(1, result.length); + Assert.assertTrue(result[0].getAuthConstraint()); + Assert.assertTrue(result[0].findCollections()[0].findPattern(URL_PATTERN)); + Assert.assertEquals(0, result[0].findCollections()[0].findMethods().length); + Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(), + result[0].getUserConstraint()); + + // Example 13-4 + // @ServletSecurity(@HttpConstraint(rolesAllowed = "R1")) + element = new ServletSecurityElement(new HttpConstraintElement( + ServletSecurity.TransportGuarantee.NONE, ROLE1)); + result = SecurityConstraint.createConstraints(element, URL_PATTERN); + + Assert.assertEquals(1, result.length); + Assert.assertTrue(result[0].getAuthConstraint()); + Assert.assertEquals(1, result[0].findAuthRoles().length); + Assert.assertTrue(result[0].findAuthRole(ROLE1)); + Assert.assertTrue(result[0].findCollections()[0].findPattern(URL_PATTERN)); + Assert.assertEquals(0, result[0].findCollections()[0].findMethods().length); + Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(), + result[0].getUserConstraint()); + + // Example 13-5 + // @ServletSecurity((httpMethodConstraints = { + // @HttpMethodConstraint(value = "GET", rolesAllowed = "R1"), + // @HttpMethodConstraint(value = "POST", rolesAllowed = "R1", + // transportGuarantee = TransportGuarantee.CONFIDENTIAL) + // }) + hmces.clear(); + hmces.add(new HttpMethodConstraintElement("GET", + new HttpConstraintElement( + ServletSecurity.TransportGuarantee.NONE, ROLE1))); + hmces.add(new HttpMethodConstraintElement("POST", + new HttpConstraintElement( + ServletSecurity.TransportGuarantee.CONFIDENTIAL, + ROLE1))); + element = new ServletSecurityElement(hmces); + result = SecurityConstraint.createConstraints(element, URL_PATTERN); + + Assert.assertEquals(2, result.length); + for (int i = 0; i < 2; i++) { + Assert.assertTrue(result[i].getAuthConstraint()); + Assert.assertEquals(1, result[i].findAuthRoles().length); + Assert.assertTrue(result[i].findAuthRole(ROLE1)); + Assert.assertTrue(result[i].findCollections()[0].findPattern(URL_PATTERN)); + Assert.assertEquals(1, result[i].findCollections()[0].findMethods().length); + String method = result[i].findCollections()[0].findMethods()[0]; + if ("GET".equals(method)) { + Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(), + result[i].getUserConstraint()); + } else if ("POST".equals(method)) { + Assert.assertEquals(ServletSecurity.TransportGuarantee.CONFIDENTIAL.name(), + result[i].getUserConstraint()); + } else { + Assert.fail("Unexpected method :[" + method + "]"); + } + } + + // Example 13-6 + // @ServletSecurity(value = @HttpConstraint(rolesAllowed = "R1"), + // httpMethodConstraints = @HttpMethodConstraint("GET")) + hmces.clear(); + hmces.add(new HttpMethodConstraintElement("GET")); + element = new ServletSecurityElement( + new HttpConstraintElement( + ServletSecurity.TransportGuarantee.NONE, + ROLE1), + hmces); + result = SecurityConstraint.createConstraints(element, URL_PATTERN); + + Assert.assertEquals(2, result.length); + for (int i = 0; i < 2; i++) { + Assert.assertTrue(result[i].findCollections()[0].findPattern(URL_PATTERN)); + if (result[i].findCollections()[0].findMethods().length == 1) { + Assert.assertEquals("GET", + result[i].findCollections()[0].findMethods()[0]); + Assert.assertFalse(result[i].getAuthConstraint()); + } else if (result[i].findCollections()[0].findOmittedMethods().length == 1) { + Assert.assertEquals("GET", + result[i].findCollections()[0].findOmittedMethods()[0]); + Assert.assertTrue(result[i].getAuthConstraint()); + Assert.assertEquals(1, result[i].findAuthRoles().length); + Assert.assertEquals(ROLE1, result[i].findAuthRoles()[0]); + } else { + Assert.fail("Unexpected number of methods defined"); + } + Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(), + result[i].getUserConstraint()); + } + + // Example 13-7 + // @ServletSecurity(value = @HttpConstraint(rolesAllowed = "R1"), + // httpMethodConstraints = @HttpMethodConstraint(value="TRACE", + // emptyRoleSemantic = EmptyRoleSemantic.DENY)) + hmces.clear(); + hmces.add(new HttpMethodConstraintElement("TRACE", + new HttpConstraintElement(EmptyRoleSemantic.DENY))); + element = new ServletSecurityElement( + new HttpConstraintElement( + ServletSecurity.TransportGuarantee.NONE, + ROLE1), + hmces); + result = SecurityConstraint.createConstraints(element, URL_PATTERN); + + Assert.assertEquals(2, result.length); + for (int i = 0; i < 2; i++) { + Assert.assertTrue(result[i].findCollections()[0].findPattern(URL_PATTERN)); + if (result[i].findCollections()[0].findMethods().length == 1) { + Assert.assertEquals("TRACE", + result[i].findCollections()[0].findMethods()[0]); + Assert.assertTrue(result[i].getAuthConstraint()); + Assert.assertEquals(0, result[i].findAuthRoles().length); + } else if (result[i].findCollections()[0].findOmittedMethods().length == 1) { + Assert.assertEquals("TRACE", + result[i].findCollections()[0].findOmittedMethods()[0]); + Assert.assertTrue(result[i].getAuthConstraint()); + Assert.assertEquals(1, result[i].findAuthRoles().length); + Assert.assertEquals(ROLE1, result[i].findAuthRoles()[0]); + } else { + Assert.fail("Unexpected number of methods defined"); + } + Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(), + result[i].getUserConstraint()); + } + + // Example 13-8 is the same as 13-4 + // Example 13-9 is the same as 13-7 + } + + + @Test + public void testFindUncoveredHttpMethods01() { + // No new constraints if denyUncoveredHttpMethods is false + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_ONLY}, false, DUMMY_LOG); + Assert.assertEquals(0, result.length); + } + + + @Test + public void testFindUncoveredHttpMethods02() { + // No new constraints if denyUncoveredHttpMethods is false + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_OMIT}, false, DUMMY_LOG); + Assert.assertEquals(0, result.length); + } + + + @Test + public void testFindUncoveredHttpMethods03() { + // No new constraints if denyUncoveredHttpMethods is false + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {POST_ONLY}, false, DUMMY_LOG); + Assert.assertEquals(0, result.length); + } + + + @Test + public void testFindUncoveredHttpMethods04() { + // No new constraints if denyUncoveredHttpMethods is false + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {POST_OMIT}, false, DUMMY_LOG); + Assert.assertEquals(0, result.length); + } + + + @Test + public void testFindUncoveredHttpMethods05() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_ONLY}, true, DUMMY_LOG); + Assert.assertEquals(1, result.length); + // Should be a deny constraint + Assert.assertTrue(result[0].getAuthConstraint()); + // Should have a single collection + Assert.assertEquals(1, result[0].findCollections().length); + SecurityCollection sc = result[0].findCollections()[0]; + // Should list GET as an omitted method + Assert.assertEquals(0, sc.findMethods().length); + Assert.assertEquals(1, sc.findOmittedMethods().length); + Assert.assertEquals("GET", sc.findOmittedMethods()[0]); + } + + + @Test + public void testFindUncoveredHttpMethods06() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {POST_ONLY}, true, DUMMY_LOG); + Assert.assertEquals(1, result.length); + // Should be a deny constraint + Assert.assertTrue(result[0].getAuthConstraint()); + // Should have a single collection + Assert.assertEquals(1, result[0].findCollections().length); + SecurityCollection sc = result[0].findCollections()[0]; + // Should list POST as an omitted method + Assert.assertEquals(0, sc.findMethods().length); + Assert.assertEquals(1, sc.findOmittedMethods().length); + Assert.assertEquals("POST", sc.findOmittedMethods()[0]); + } + + + @Test + public void testFindUncoveredHttpMethods07() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_OMIT}, true, DUMMY_LOG); + Assert.assertEquals(1, result.length); + // Should be a deny constraint + Assert.assertTrue(result[0].getAuthConstraint()); + // Should have a single collection + Assert.assertEquals(1, result[0].findCollections().length); + SecurityCollection sc = result[0].findCollections()[0]; + // Should list GET as an method + Assert.assertEquals(0, sc.findOmittedMethods().length); + Assert.assertEquals(1, sc.findMethods().length); + Assert.assertEquals("GET", sc.findMethods()[0]); + } + + + @Test + public void testFindUncoveredHttpMethods08() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {POST_OMIT}, true, DUMMY_LOG); + Assert.assertEquals(1, result.length); + // Should be a deny constraint + Assert.assertTrue(result[0].getAuthConstraint()); + // Should have a single collection + Assert.assertEquals(1, result[0].findCollections().length); + SecurityCollection sc = result[0].findCollections()[0]; + // Should list POST as an method + Assert.assertEquals(0, sc.findOmittedMethods().length); + Assert.assertEquals(1, sc.findMethods().length); + Assert.assertEquals("POST", sc.findMethods()[0]); + } + + + @Test + public void testFindUncoveredHttpMethods09() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_ONLY, GET_OMIT}, true, + DUMMY_LOG); + Assert.assertEquals(0, result.length); + } + + + @Test + public void testFindUncoveredHttpMethods10() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {POST_ONLY, POST_OMIT}, true, + DUMMY_LOG); + Assert.assertEquals(0, result.length); + } + + + @Test + public void testFindUncoveredHttpMethods11() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_ONLY, POST_ONLY}, true, + DUMMY_LOG); + Assert.assertEquals(1, result.length); + // Should be a deny constraint + Assert.assertTrue(result[0].getAuthConstraint()); + // Should have a single collection + Assert.assertEquals(1, result[0].findCollections().length); + SecurityCollection sc = result[0].findCollections()[0]; + // Should list GET and POST as omitted methods + Assert.assertEquals(0, sc.findMethods().length); + Assert.assertEquals(2, sc.findOmittedMethods().length); + HashSet omittedMethods = new HashSet<>(); + omittedMethods.addAll(Arrays.asList(sc.findOmittedMethods())); + Assert.assertTrue(omittedMethods.remove("GET")); + Assert.assertTrue(omittedMethods.remove("POST")); + } + + + @Test + public void testFindUncoveredHttpMethods12() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_OMIT, POST_OMIT}, true, + DUMMY_LOG); + Assert.assertEquals(0, result.length); + } + + + @Test + public void testFindUncoveredHttpMethods13() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_ONLY, POST_OMIT}, true, + DUMMY_LOG); + Assert.assertEquals(1, result.length); + // Should be a deny constraint + Assert.assertTrue(result[0].getAuthConstraint()); + // Should have a single collection + Assert.assertEquals(1, result[0].findCollections().length); + SecurityCollection sc = result[0].findCollections()[0]; + // Should list POST as a method + Assert.assertEquals(1, sc.findMethods().length); + Assert.assertEquals(0, sc.findOmittedMethods().length); + Assert.assertEquals("POST", sc.findMethods()[0]); + } + + + @Test + public void testFindUncoveredHttpMethods14() { + SecurityConstraint[] result = + SecurityConstraint.findUncoveredHttpMethods( + new SecurityConstraint[] {GET_OMIT, POST_ONLY}, true, + DUMMY_LOG); + Assert.assertEquals(1, result.length); + // Should be a deny constraint + Assert.assertTrue(result[0].getAuthConstraint()); + // Should have a single collection + Assert.assertEquals(1, result[0].findCollections().length); + SecurityCollection sc = result[0].findCollections()[0]; + // Should list GET as a method + Assert.assertEquals(1, sc.findMethods().length); + Assert.assertEquals(0, sc.findOmittedMethods().length); + Assert.assertEquals("GET", sc.findMethods()[0]); + } +} diff --git a/test/org/apache/tomcat/util/descriptor/web/TestServletDef.java b/test/org/apache/tomcat/util/descriptor/web/TestServletDef.java new file mode 100644 index 0000000..33145f7 --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestServletDef.java @@ -0,0 +1,45 @@ +/* + * 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.tomcat.util.descriptor.web; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test case for {@link ServletDef} + */ +public class TestServletDef { + + @Test(expected = IllegalArgumentException.class) + public void testSetServletNameNull() { + new ServletDef().setServletName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetServletNameEmptyString() { + new ServletDef().setServletName(""); + } + + @Test + public void testSetServletName() { + ServletDef servletDef = new ServletDef(); + servletDef.setServletName("test"); + Assert.assertEquals("'test' is expected as servlet name", + "test", servletDef.getServletName()); + } + +} \ No newline at end of file diff --git a/test/org/apache/tomcat/util/descriptor/web/TestWebRuleSet.java b/test/org/apache/tomcat/util/descriptor/web/TestWebRuleSet.java new file mode 100644 index 0000000..7024d46 --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestWebRuleSet.java @@ -0,0 +1,155 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.digester.Digester; + +public class TestWebRuleSet { + + private Digester fragmentDigester = new Digester(); + private WebRuleSet fragmentRuleSet = new WebRuleSet(true); + + private Digester webDigester = new Digester(); + private WebRuleSet webRuleSet = new WebRuleSet(false); + + public TestWebRuleSet() { + fragmentDigester.addRuleSet(fragmentRuleSet); + webDigester.addRuleSet(webRuleSet); + } + + + @Test + public void testSingleNameInWebFragmentXml() throws Exception { + + WebXml webXml = new WebXml(); + + parse(webXml, "web-fragment-1name.xml", true, true); + Assert.assertEquals("name1", webXml.getName()); + } + + + @Test + public void testMultipleNameInWebFragmentXml() throws Exception { + parse(new WebXml(), "web-fragment-2name.xml", true, false); + } + + + @Test + public void testSingleOrderingInWebFragmentXml() throws Exception { + + WebXml webXml = new WebXml(); + + parse(webXml, "web-fragment-1ordering.xml", true, true); + Assert.assertEquals(1, webXml.getBeforeOrdering().size()); + Assert.assertTrue(webXml.getBeforeOrdering().contains("bar")); + } + + + @Test + public void testMultipleOrderingInWebFragmentXml() throws Exception { + parse(new WebXml(), "web-fragment-2ordering.xml", true, false); + } + + + @Test + public void testSingleOrderingInWebXml() throws Exception { + + WebXml webXml = new WebXml(); + + parse(webXml, "web-1ordering.xml", false, true); + Assert.assertEquals(1, webXml.getAbsoluteOrdering().size()); + Assert.assertTrue(webXml.getAbsoluteOrdering().contains("bar")); + } + + + @Test + public void testMultipleOrderingInWebXml() throws Exception { + parse(new WebXml(), "web-2ordering.xml", false, false); + } + + + @Test + public void testRecycle() throws Exception { + // Name + parse(new WebXml(), "web-fragment-2name.xml", true, false); + parse(new WebXml(), "web-fragment-1name.xml", true, true); + parse(new WebXml(), "web-fragment-2name.xml", true, false); + parse(new WebXml(), "web-fragment-1name.xml", true, true); + + // Relative ordering + parse(new WebXml(), "web-fragment-2ordering.xml", true, false); + parse(new WebXml(), "web-fragment-1ordering.xml", true, true); + parse(new WebXml(), "web-fragment-2ordering.xml", true, false); + parse(new WebXml(), "web-fragment-1ordering.xml", true, true); + + // Absolute ordering + parse(new WebXml(), "web-2ordering.xml", false, false); + parse(new WebXml(), "web-1ordering.xml", false, true); + parse(new WebXml(), "web-2ordering.xml", false, false); + parse(new WebXml(), "web-1ordering.xml", false, true); +} + + @Test + public void testLifecycleMethodsDefinitions() throws Exception { + // post-construct and pre-destroy + parse(new WebXml(), "web-1lifecyclecallback.xml", false, true); + // conflicting post-construct definitions + parse(new WebXml(), "web-2lifecyclecallback.xml", false, false); + } + + private synchronized void parse(WebXml webXml, String target, + boolean fragment, boolean expected) { + + Digester d; + if (fragment) { + d = fragmentDigester; + fragmentRuleSet.recycle(); + } else { + d = webDigester; + webRuleSet.recycle(); + } + + d.push(webXml); + + File f = new File("test/org/apache/catalina/startup/" + target); + + boolean result = true; + + try (InputStream is = new FileInputStream(f)) { + d.parse(is); + } catch (Exception e) { + if (expected) { + // Didn't expect an exception + e.printStackTrace(); + } + result = false; + } + + if (expected) { + Assert.assertTrue(result); + } else { + Assert.assertFalse(result); + } + } +} diff --git a/test/org/apache/tomcat/util/descriptor/web/TestWebXml.java b/test/org/apache/tomcat/util/descriptor/web/TestWebXml.java new file mode 100644 index 0000000..fa223aa --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestWebXml.java @@ -0,0 +1,632 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.descriptor.DigesterFactory; +import org.apache.tomcat.util.descriptor.XmlErrorHandler; +import org.apache.tomcat.util.descriptor.XmlIdentifiers; +import org.apache.tomcat.util.digester.Digester; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Test case for {@link WebXml}. + */ +public class TestWebXml { + + @Test + public void testParseVersion() { + + WebXml webxml = new WebXml(); + + // Defaults + Assert.assertEquals(6, webxml.getMajorVersion()); + Assert.assertEquals(0, webxml.getMinorVersion()); + + // Both get changed + webxml.setVersion("2.5"); + Assert.assertEquals(2, webxml.getMajorVersion()); + Assert.assertEquals(5, webxml.getMinorVersion()); + + // unknown input should be ignored + webxml.setVersion("0.0"); + Assert.assertEquals(2, webxml.getMajorVersion()); + Assert.assertEquals(5, webxml.getMinorVersion()); + + // null input should be ignored + webxml.setVersion(null); + Assert.assertEquals(2, webxml.getMajorVersion()); + Assert.assertEquals(5, webxml.getMinorVersion()); + } + + @Test + public void testParsePublicIdVersion22() { + + WebXml webxml = new WebXml(); + + webxml.setPublicId(XmlIdentifiers.WEB_22_PUBLIC); + Assert.assertEquals(2, webxml.getMajorVersion()); + Assert.assertEquals(2, webxml.getMinorVersion()); + Assert.assertEquals("2.2", webxml.getVersion()); + } + + @Test + public void testParsePublicIdVersion23() { + + WebXml webxml = new WebXml(); + + webxml.setPublicId(XmlIdentifiers.WEB_23_PUBLIC); + Assert.assertEquals(2, webxml.getMajorVersion()); + Assert.assertEquals(3, webxml.getMinorVersion()); + Assert.assertEquals("2.3", webxml.getVersion()); + } + + @Test + public void testParseVersion24() { + + WebXml webxml = new WebXml(); + + webxml.setVersion("2.4"); + Assert.assertEquals(2, webxml.getMajorVersion()); + Assert.assertEquals(4, webxml.getMinorVersion()); + Assert.assertEquals("2.4", webxml.getVersion()); + } + + @Test + public void testParseVersion25() { + + WebXml webxml = new WebXml(); + + webxml.setVersion("2.5"); + Assert.assertEquals(2, webxml.getMajorVersion()); + Assert.assertEquals(5, webxml.getMinorVersion()); + Assert.assertEquals("2.5", webxml.getVersion()); + } + + @Test + public void testParseVersion30() { + + WebXml webxml = new WebXml(); + + webxml.setVersion("3.0"); + Assert.assertEquals(3, webxml.getMajorVersion()); + Assert.assertEquals(0, webxml.getMinorVersion()); + Assert.assertEquals("3.0", webxml.getVersion()); + } + + @Test + public void testParseVersion31() { + + WebXml webxml = new WebXml(); + + webxml.setVersion("3.1"); + Assert.assertEquals(3, webxml.getMajorVersion()); + Assert.assertEquals(1, webxml.getMinorVersion()); + Assert.assertEquals("3.1", webxml.getVersion()); + } + + @Test + public void testParseVersion40() { + + WebXml webxml = new WebXml(); + + webxml.setVersion("4.0"); + Assert.assertEquals(4, webxml.getMajorVersion()); + Assert.assertEquals(0, webxml.getMinorVersion()); + Assert.assertEquals("4.0", webxml.getVersion()); + } + + @Test + public void testValidateVersion22() throws IOException, SAXException { + doTestValidateVersion("2.2"); + } + + @Test + public void testValidateVersion23() throws IOException, SAXException { + doTestValidateVersion("2.3"); + } + + @Test + public void testValidateVersion24() throws IOException, SAXException { + doTestValidateVersion("2.4"); + } + + @Test + public void testValidateVersion25() throws IOException, SAXException { + doTestValidateVersion("2.5"); + } + + @Test + public void testValidateVersion30() throws IOException, SAXException { + doTestValidateVersion("3.0"); + } + + @Test + public void testValidateVersion31() throws IOException, SAXException { + doTestValidateVersion("3.1"); + } + + @Test + public void testValidateVersion40() throws IOException, SAXException { + doTestValidateVersion("4.0"); + } + + @Test + public void testValidateVersion50() throws IOException, SAXException { + doTestValidateVersion("5.0"); + } + + @Test + public void testValidateVersion60() throws IOException, SAXException { + doTestValidateVersion("6.0"); + } + + private void doTestValidateVersion(String version) throws IOException, SAXException { + WebXml webxml = new WebXml(); + + // Special cases + if ("2.2".equals(version)) { + webxml.setPublicId(XmlIdentifiers.WEB_22_PUBLIC); + } else if ("2.3".equals(version)) { + webxml.setPublicId(XmlIdentifiers.WEB_23_PUBLIC); + } else { + webxml.setVersion(version); + } + + // Merged web.xml that is published as MERGED_WEB_XML context attribute + // in the simplest case consists of webapp's web.xml file + // plus the default conf/web.xml one. + Set defaults = new HashSet<>(); + defaults.add(getDefaultWebXmlFragment()); + webxml.merge(defaults); + + Digester digester = DigesterFactory.newDigester(true, true, new WebRuleSet(), true); + + XmlErrorHandler handler = new XmlErrorHandler(); + digester.setErrorHandler(handler); + + InputSource is = new InputSource(new StringReader(webxml.toXml())); + WebXml webxmlResult = new WebXml(); + digester.push(webxmlResult); + digester.parse(is); + + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + + Assert.assertEquals(version, webxml.getVersion()); + Assert.assertEquals(version, webxmlResult.getVersion()); + } + + // A simplified copy of ContextConfig.getDefaultWebXmlFragment(). + // Assuming that global web.xml exists, host-specific web.xml does not exist. + private WebXml getDefaultWebXmlFragment() throws IOException, SAXException { + InputSource globalWebXml = new InputSource(new File("conf/web.xml") + .getAbsoluteFile().toURI().toString()); + + WebXml webXmlDefaultFragment = new WebXml(); + webXmlDefaultFragment.setOverridable(true); + webXmlDefaultFragment.setDistributable(true); + webXmlDefaultFragment.setAlwaysAddWelcomeFiles(false); + + Digester digester = DigesterFactory.newDigester(true, true, new WebRuleSet(), true); + XmlErrorHandler handler = new XmlErrorHandler(); + digester.setErrorHandler(handler); + digester.push(webXmlDefaultFragment); + digester.parse(globalWebXml); + Assert.assertEquals(0, handler.getErrors().size()); + Assert.assertEquals(0, handler.getWarnings().size()); + + webXmlDefaultFragment.setReplaceWelcomeFiles(true); + + // Assert that web.xml was parsed and is not empty. Default servlet is known to be there. + Assert.assertNotNull(webXmlDefaultFragment.getServlets().get("default")); + + // Manually add some version specific features to ensure that these do + // not cause problems for the merged web.xml + + // Filters were added in 2.3 so should be excluded in 2.2 + FilterDef filterDef = new FilterDef(); + filterDef.setFilterClass("org.apache.tomcat.DummyFilter"); + filterDef.setFilterName("Dummy"); + webXmlDefaultFragment.addFilter(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName("Dummy"); + filterMap.addURLPatternDecoded("/*"); + webXmlDefaultFragment.addFilterMapping(filterMap); + + // Listeners were added in 2.3 so should be excluded in 2.2 + webXmlDefaultFragment.addListener("org.apache.tomcat.DummyListener"); + + // resource-env-ref was added in 2.3 so should be excluded in 2.2 + ContextResourceEnvRef resourceEnvRef = new ContextResourceEnvRef(); + resourceEnvRef.setName("dummy"); + resourceEnvRef.setType("dummy"); + + webXmlDefaultFragment.addResourceEnvRef(resourceEnvRef); + + // ejb-local-ref was added in 2.3 so should be excluded in 2.2 + ContextLocalEjb ejbLocalRef = new ContextLocalEjb(); + ejbLocalRef.setName("dummy"); + ejbLocalRef.setType("Session"); + ejbLocalRef.setLocal("dummy"); + ejbLocalRef.setHome("dummy"); + webXmlDefaultFragment.addEjbLocalRef(ejbLocalRef); + + // Servlet/run-as was added in 2.3 so should be excluded in 2.2 + ServletDef servletDef = new ServletDef(); + servletDef.setServletName("Dummy"); + servletDef.setServletClass("org.apache.tomcat.DummyServlet"); + servletDef.setRunAs("dummy"); + webXmlDefaultFragment.addServlet(servletDef); + + webXmlDefaultFragment.addServletMapping("/dummy", "Dummy"); + + // resource-ref/res-sharing-scope was added in 2.3 so should be excluded + // in 2.2 + ContextResource contextResource = new ContextResource(); + contextResource.setName("dummy"); + contextResource.setType("dummy"); + contextResource.setAuth("Container"); + contextResource.setScope("Shareable"); + webXmlDefaultFragment.addResourceRef(contextResource); + + // security-constraint/display-name was added in 2.3 so should be + // excluded in 2.2 + SecurityConstraint sc = new SecurityConstraint(); + sc.setDisplayName("dummy"); + SecurityCollection collection = new SecurityCollection(); + collection.setName("dummy"); + collection.addPatternDecoded("/*"); + collection.addMethod("DELETE"); + sc.addCollection(collection); + webXmlDefaultFragment.addSecurityConstraint(sc); + + // service-ref was added in 2.4 so should be excluded in 2.3 and earlier + ContextService serviceRef = new ContextService(); + serviceRef.setName("dummy"); + serviceRef.setInterface("dummy"); + webXmlDefaultFragment.addServiceRef(serviceRef); + + // message-destination-ref was added in 2.4 so should be excluded in 2.3 + // and earlier + MessageDestinationRef mdRef = new MessageDestinationRef(); + mdRef.setName("dummy"); + mdRef.setType("dummy"); + mdRef.setUsage("Consumes"); + webXmlDefaultFragment.addMessageDestinationRef(mdRef); + + // message-destination was added in 2.4 so should be excluded in 2.3 + // and earlier + MessageDestination md = new MessageDestination(); + md.setName("dummy"); + webXmlDefaultFragment.addMessageDestination(md); + + // local-encoding-mapping-list was added in 2.4 so should be excluded in + // 2.3 and earlier + webXmlDefaultFragment.addLocaleEncodingMapping("en", "UTF-8"); + + // jsp-config was added in Servlet 2.4 + webXmlDefaultFragment.addTaglib("dummy", "dummy"); + + // filter-mapping/dispatcher added in Servlet 2.4 + filterMap.setDispatcher("REQUEST"); + + // listener-[description|display-name|icon] added in Servlet 2.4 + // None of these are supported in WebXml + + // filter-mapping/dispatcher/ASYNC added in Servlet 3.0 + filterMap.setDispatcher("ASYNC"); + + // error-page with just location allowed in Servlet 3.0+ + ErrorPage errorPage = new ErrorPage(); + errorPage.setLocation("/dummy"); + webXmlDefaultFragment.addErrorPage(errorPage); + + // async-supported added to Servlet and Filter in 3.0 + filterDef.setAsyncSupported("false"); + servletDef.setAsyncSupported("false"); + + // session-cookie-config added in 3.0 + SessionConfig sessionConfig = new SessionConfig(); + sessionConfig.setCookieDomain("dummy"); + webXmlDefaultFragment.setSessionConfig(sessionConfig); + + // http-method-omission added in Servlet 3.0 + // Let this trigger a validation error as dropping it silently could + // be a security concern + + // multi-part-config added in Servlet 3.0 + MultipartDef multiPart = new MultipartDef(); + servletDef.setMultipartDef(multiPart); + + // deny-uncovered-http-methods added in Servlet 3.1 + webXmlDefaultFragment.setDenyUncoveredHttpMethods(true); + + return webXmlDefaultFragment; + } + + @Test + public void testLifecycleMethodsWebXml() { + WebXml webxml = new WebXml(); + webxml.addPostConstructMethods("a", "a"); + webxml.addPreDestroyMethods("b", "b"); + + WebXml fragment = new WebXml(); + fragment.addPostConstructMethods("c", "c"); + fragment.addPreDestroyMethods("d", "d"); + + Set fragments = new HashSet<>(); + fragments.add(fragment); + + webxml.merge(fragments); + + Map postConstructMethods = webxml.getPostConstructMethods(); + Map preDestroyMethods = webxml.getPreDestroyMethods(); + Assert.assertEquals(1, postConstructMethods.size()); + Assert.assertEquals(1, preDestroyMethods.size()); + + Assert.assertEquals("a", postConstructMethods.get("a")); + Assert.assertEquals("b", preDestroyMethods.get("b")); + } + + @Test + public void testLifecycleMethodsWebFragments() { + WebXml webxml = new WebXml(); + + WebXml fragment1 = new WebXml(); + fragment1.addPostConstructMethods("a", "a"); + fragment1.addPreDestroyMethods("b", "b"); + + WebXml fragment2 = new WebXml(); + fragment2.addPostConstructMethods("c", "c"); + fragment2.addPreDestroyMethods("d", "d"); + + Set fragments = new HashSet<>(); + fragments.add(fragment1); + fragments.add(fragment2); + + webxml.merge(fragments); + + Map postConstructMethods = webxml.getPostConstructMethods(); + Map preDestroyMethods = webxml.getPreDestroyMethods(); + Assert.assertEquals(2, postConstructMethods.size()); + Assert.assertEquals(2, preDestroyMethods.size()); + + Assert.assertEquals("a", postConstructMethods.get("a")); + Assert.assertEquals("c", postConstructMethods.get("c")); + Assert.assertEquals("b", preDestroyMethods.get("b")); + Assert.assertEquals("d", preDestroyMethods.get("d")); + } + + @Test + public void testLifecycleMethodsWebFragmentsWithConflicts() { + WebXml webxml = new WebXml(); + + WebXml fragment1 = new WebXml(); + fragment1.addPostConstructMethods("a", "a"); + fragment1.addPreDestroyMethods("b", "a"); + + WebXml fragment2 = new WebXml(); + fragment2.addPostConstructMethods("a", "b"); + + Set fragments = new HashSet<>(); + fragments.add(fragment1); + fragments.add(fragment2); + + Assert.assertFalse(webxml.merge(fragments)); + + Assert.assertEquals(0, webxml.getPostConstructMethods().size()); + + WebXml fragment3 = new WebXml(); + fragment3.addPreDestroyMethods("b", "b"); + + fragments.remove(fragment2); + fragments.add(fragment3); + + Assert.assertFalse(webxml.merge(fragments)); + + Assert.assertEquals(0, webxml.getPreDestroyMethods().size()); + } + + @Test(expected=IllegalArgumentException.class) + public void testBug54387a() { + // Multiple servlets may not be mapped to the same url-pattern + WebXml webxml = new WebXml(); + webxml.addServletMapping("/foo", "a"); + webxml.addServletMapping("/foo", "b"); + } + + @Test(expected=IllegalArgumentException.class) + public void testBug54387b() { + // Multiple servlets may not be mapped to the same url-pattern + WebXml webxml = new WebXml(); + WebXml f1 = new WebXml(); + WebXml f2 = new WebXml(); + + HashSet fragments = new HashSet<>(); + fragments.add(f1); + fragments.add(f2); + + f1.addServletMapping("/foo", "a"); + f2.addServletMapping("/foo", "b"); + + webxml.merge(fragments); + } + + @Test + public void testBug54387c() { + // Multiple servlets may not be mapped to the same url-pattern but main + // web.xml takes priority + WebXml webxml = new WebXml(); + WebXml f1 = new WebXml(); + WebXml f2 = new WebXml(); + + HashSet fragments = new HashSet<>(); + fragments.add(f1); + fragments.add(f2); + + f1.addServletMapping("/foo", "a"); + f2.addServletMapping("/foo", "b"); + webxml.addServletMapping("/foo", "main"); + + webxml.merge(fragments); + } + + + @Test + public void testEncoding() { + WebXml webXml = new WebXml(); + webXml.setCharset(StandardCharsets.ISO_8859_1); + + webXml.addErrorPage(new ErrorPage()); + Collection errorPages = webXml.getErrorPages().values(); + for (ErrorPage errorPage : errorPages) { + Assert.assertEquals(StandardCharsets.ISO_8859_1, errorPage.getCharset()); + } + + webXml.addFilterMapping(new FilterMap()); + Set filterMaps = webXml.getFilterMappings(); + for (FilterMap filterMap : filterMaps) { + Assert.assertEquals(StandardCharsets.ISO_8859_1, filterMap.getCharset()); + } + + webXml.addJspPropertyGroup(new JspPropertyGroup()); + Set jspPropertyGroups = webXml.getJspPropertyGroups(); + for (JspPropertyGroup jspPropertyGroup : jspPropertyGroups) { + Assert.assertEquals(StandardCharsets.ISO_8859_1, jspPropertyGroup.getCharset()); + } + + webXml.setLoginConfig(new LoginConfig()); + LoginConfig loginConfig = webXml.getLoginConfig(); + Assert.assertEquals(StandardCharsets.ISO_8859_1, loginConfig.getCharset()); + + SecurityConstraint constraint = new SecurityConstraint(); + constraint.addCollection(new SecurityCollection()); + webXml.addSecurityConstraint(constraint); + Set securityConstraints = webXml.getSecurityConstraints(); + for (SecurityConstraint securityConstraint : securityConstraints) { + Assert.assertEquals(StandardCharsets.ISO_8859_1, securityConstraint.getCharset()); + for (SecurityCollection securityCollection : securityConstraint.findCollections()) { + Assert.assertEquals(StandardCharsets.ISO_8859_1, securityCollection.getCharset()); + } + } + } + + + @Test + public void testMergeSessionCookieConfig01() { + WebXml main = new WebXml(); + WebXml fragmentA = new WebXml(); + WebXml fragmentB = new WebXml(); + + fragmentA.getSessionConfig().setCookieHttpOnly("true"); + fragmentB.getSessionConfig().setCookieSecure("true"); + + Set fragments = new HashSet<>(); + fragments.add(fragmentA); + fragments.add(fragmentB); + + Assert.assertTrue(main.merge(fragments)); + Assert.assertEquals(Boolean.TRUE, main.getSessionConfig().getCookieHttpOnly()); + Assert.assertEquals(Boolean.TRUE, main.getSessionConfig().getCookieSecure()); + } + + + @Test + public void testMergeSessionCookieConfig02() { + WebXml main = new WebXml(); + WebXml fragmentA = new WebXml(); + WebXml fragmentB = new WebXml(); + + fragmentA.getSessionConfig().setCookieHttpOnly("true"); + fragmentB.getSessionConfig().setCookieHttpOnly("false"); + + Set fragments = new HashSet<>(); + fragments.add(fragmentA); + fragments.add(fragmentB); + + Assert.assertFalse(main.merge(fragments)); + } + + + @Test + public void testMergeSessionCookieConfig03() { + WebXml main = new WebXml(); + WebXml fragmentA = new WebXml(); + WebXml fragmentB = new WebXml(); + + main.getSessionConfig().setCookieHttpOnly("false"); + fragmentA.getSessionConfig().setCookieHttpOnly("true"); + fragmentB.getSessionConfig().setCookieSecure("true"); + + Set fragments = new HashSet<>(); + fragments.add(fragmentA); + fragments.add(fragmentB); + + Assert.assertTrue(main.merge(fragments)); + Assert.assertEquals(Boolean.FALSE, main.getSessionConfig().getCookieHttpOnly()); + Assert.assertEquals(Boolean.TRUE, main.getSessionConfig().getCookieSecure()); + } + + + @Test + public void testMergeSessionCookieConfig04() { + WebXml main = new WebXml(); + WebXml fragmentA = new WebXml(); + WebXml fragmentB = new WebXml(); + + fragmentA.getSessionConfig().setCookieAttribute("aaa", "bbb"); + fragmentB.getSessionConfig().setCookieAttribute("AAA", "bbb"); + + Set fragments = new HashSet<>(); + fragments.add(fragmentA); + fragments.add(fragmentB); + + Assert.assertTrue(main.merge(fragments)); + Assert.assertEquals("bbb", main.getSessionConfig().getCookieAttribute("aAa")); + } + + + @Test + public void testMergeSessionCookieConfig05() { + WebXml main = new WebXml(); + WebXml fragmentA = new WebXml(); + WebXml fragmentB = new WebXml(); + + fragmentA.getSessionConfig().setCookieAttribute("aaa", "bBb"); + fragmentB.getSessionConfig().setCookieAttribute("AAA", "bbb"); + + Set fragments = new HashSet<>(); + fragments.add(fragmentA); + fragments.add(fragmentB); + + Assert.assertFalse(main.merge(fragments)); + } +} diff --git a/test/org/apache/tomcat/util/descriptor/web/TestWebXmlOrdering.java b/test/org/apache/tomcat/util/descriptor/web/TestWebXmlOrdering.java new file mode 100644 index 0000000..40b62ab --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestWebXmlOrdering.java @@ -0,0 +1,708 @@ +/* + * 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.tomcat.util.descriptor.web; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Test case for {@link WebXml} fragment ordering. + */ +public class TestWebXmlOrdering { + private WebXml app; + private WebXml a; + private WebXml b; + private WebXml c; + private WebXml d; + private WebXml e; + private WebXml f; + private Map fragments; + private int posA; + private int posB; + private int posC; + private int posD; + private int posE; + private int posF; + + @Before + public void setUp() { + app = new WebXml(); + a = new WebXml(); + a.setName("a"); + b = new WebXml(); + b.setName("b"); + c = new WebXml(); + c.setName("c"); + d = new WebXml(); + d.setName("d"); + e = new WebXml(); + e.setName("e"); + f = new WebXml(); + f.setName("f"); + // Control the input order + fragments = new LinkedHashMap<>(); + fragments.put("a",a); + fragments.put("b",b); + fragments.put("c",c); + fragments.put("d",d); + fragments.put("e",e); + fragments.put("f",f); + } + + @Test + public void testOrderWebFragmentsAbsolute() { + app.addAbsoluteOrdering("c"); + app.addAbsoluteOrdering("a"); + app.addAbsoluteOrdering("b"); + app.addAbsoluteOrdering("e"); + app.addAbsoluteOrdering("d"); + + Set ordered = WebXml.orderWebFragments(app, fragments, null); + + Iterator iter = ordered.iterator(); + Assert.assertEquals(c,iter.next()); + Assert.assertEquals(a,iter.next()); + Assert.assertEquals(b,iter.next()); + Assert.assertEquals(e,iter.next()); + Assert.assertEquals(d,iter.next()); + Assert.assertFalse(iter.hasNext()); + } + + @Test + public void testOrderWebFragmentsAbsolutePartial() { + app.addAbsoluteOrdering("c"); + app.addAbsoluteOrdering("a"); + + Set ordered = WebXml.orderWebFragments(app, fragments, null); + + Iterator iter = ordered.iterator(); + Assert.assertEquals(c,iter.next()); + Assert.assertEquals(a,iter.next()); + Assert.assertFalse(iter.hasNext()); + } + + @Test + public void testOrderWebFragmentsAbsoluteOthersStart() { + app.addAbsoluteOrdering(WebXml.ORDER_OTHERS); + app.addAbsoluteOrdering("b"); + app.addAbsoluteOrdering("d"); + + Set others = new HashSet<>(); + others.add(a); + others.add(c); + others.add(e); + others.add(f); + + Set ordered = WebXml.orderWebFragments(app, fragments, null); + + Iterator iter = ordered.iterator(); + while (others.size() > 0) { + WebXml o = iter.next(); + Assert.assertTrue(others.contains(o)); + others.remove(o); + } + Assert.assertEquals(b,iter.next()); + Assert.assertEquals(d,iter.next()); + Assert.assertFalse(iter.hasNext()); + } + + @Test + public void testOrderWebFragmentsAbsoluteOthersMiddle() { + app.addAbsoluteOrdering("b"); + app.addAbsoluteOrdering(WebXml.ORDER_OTHERS); + app.addAbsoluteOrdering("d"); + + Set others = new HashSet<>(); + others.add(a); + others.add(c); + others.add(e); + others.add(f); + + Set ordered = WebXml.orderWebFragments(app, fragments, null); + + Iterator iter = ordered.iterator(); + Assert.assertEquals(b,iter.next()); + + while (others.size() > 0) { + WebXml o = iter.next(); + Assert.assertTrue(others.contains(o)); + others.remove(o); + } + Assert.assertEquals(d,iter.next()); + Assert.assertFalse(iter.hasNext()); + } + + @Test + public void testWebFragmentsAbsoluteWrongFragmentName() { + app.addAbsoluteOrdering("a"); + app.addAbsoluteOrdering("z"); + Set ordered = WebXml.orderWebFragments(app, fragments, null); + Assert.assertEquals(1,ordered.size()); + Assert.assertEquals(fragments.get("a"),ordered.toArray()[0]); + } + + @Test + public void testOrderWebFragmentsAbsoluteOthersEnd() { + app.addAbsoluteOrdering("b"); + app.addAbsoluteOrdering("d"); + app.addAbsoluteOrdering(WebXml.ORDER_OTHERS); + + Set others = new HashSet<>(); + others.add(a); + others.add(c); + others.add(e); + others.add(f); + + Set ordered = WebXml.orderWebFragments(app, fragments, null); + + Iterator iter = ordered.iterator(); + Assert.assertEquals(b,iter.next()); + Assert.assertEquals(d,iter.next()); + + while (others.size() > 0) { + WebXml o = iter.next(); + Assert.assertTrue(others.contains(o)); + others.remove(o); + } + Assert.assertFalse(iter.hasNext()); + } + + private void doRelativeOrderingTest(RelativeOrderingTestRunner runner) { + // Confirm we have all 720 possible input orders + // Set orders = new HashSet<>(); + + // Test all possible input orders since some bugs were discovered that + // depended on input order + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 5; j++) { + for (int k = 0; k < 4; k++) { + for (int l = 0; l < 3; l++) { + for (int m = 0; m < 2; m++) { + setUp(); + runner.init(); + ArrayList source = new ArrayList<>(fragments.values()); + Map input = + new LinkedHashMap<>(); + + WebXml one = source.remove(i); + input.put(one.getName(), one); + + WebXml two = source.remove(j); + input.put(two.getName(), two); + + WebXml three = source.remove(k); + input.put(three.getName(), three); + + WebXml four = source.remove(l); + input.put(four.getName(), four); + + WebXml five = source.remove(m); + input.put(five.getName(), five); + + WebXml six = source.remove(0); + input.put(six.getName(), six); + + /* + String order = one.getName() + two.getName() + + three.getName() + four.getName() + + five.getName() + six.getName(); + orders.add(order); + */ + + Set ordered = + WebXml.orderWebFragments(app, input, null); + populatePositions(ordered); + + runner.validate(getOrder(ordered)); + } + } + } + } + } + // System.out.println(orders.size()); + } + + private String getOrder(Set ordered) { + StringBuilder sb = new StringBuilder(ordered.size()); + for (WebXml webXml : ordered) { + sb.append(webXml.getName()); + } + return sb.toString(); + } + + private void populatePositions(Set ordered) { + List indexed = new ArrayList<>(ordered); + + posA = indexed.indexOf(a); + posB = indexed.indexOf(b); + posC = indexed.indexOf(c); + posD = indexed.indexOf(d); + posE = indexed.indexOf(e); + posF = indexed.indexOf(f); + } + + @Test + public void testOrderWebFragmentsRelative1() { + // First example from servlet spec + doRelativeOrderingTest(new RelativeTestRunner1()); + } + + @Test + public void testOrderWebFragmentsRelative2() { + // Second example - use fragment a for no-id fragment + doRelativeOrderingTest(new RelativeTestRunner2()); + } + + @Test + public void testOrderWebFragmentsRelative3() { + // Third example from spec with e & f added + doRelativeOrderingTest(new RelativeTestRunner3()); + } + + @Test + public void testOrderWebFragmentsRelative4Bug54068() { + // Simple sequence that failed for some inputs + doRelativeOrderingTest(new RelativeTestRunner4()); + } + + @Test + public void testOrderWebFragmentsRelative5Bug54068() { + // Simple sequence that failed for some inputs + doRelativeOrderingTest(new RelativeTestRunner5()); + } + + @Test + public void testOrderWebFragmentsRelative6Bug54068() { + // Simple sequence that failed for some inputs + doRelativeOrderingTest(new RelativeTestRunner6()); + } + + @Test + public void testOrderWebFragmentsRelative7() { + // Reference loop (but not circular dependencies) + doRelativeOrderingTest(new RelativeTestRunner7()); + } + + @Test + public void testOrderWebFragmentsRelative8() { + // More complex, trying to break the algorithm + doRelativeOrderingTest(new RelativeTestRunner8()); + } + + @Test + public void testOrderWebFragmentsRelative9() { + // Variation on bug 54068 + doRelativeOrderingTest(new RelativeTestRunner9()); + } + + @Test + public void testOrderWebFragmentsRelative10() { + // Variation on bug 54068 + doRelativeOrderingTest(new RelativeTestRunner10()); + } + + @Test + public void testOrderWebFragmentsRelative11() { + // Test references to non-existent fragments + doRelativeOrderingTest(new RelativeTestRunner11()); + } + + @Test(expected=IllegalArgumentException.class) + public void testOrderWebFragmentsrelativeCircular1() { + a.addBeforeOrdering("b"); + b.addBeforeOrdering("a"); + + WebXml.orderWebFragments(app, fragments, null); + } + + @Test(expected=IllegalArgumentException.class) + public void testOrderWebFragmentsrelativeCircular2() { + a.addBeforeOrderingOthers(); + b.addAfterOrderingOthers(); + c.addBeforeOrdering("a"); + c.addAfterOrdering("b"); + + WebXml.orderWebFragments(app, fragments, null); + } + + @Test(expected=IllegalArgumentException.class) + public void testOrderWebFragmentsRelativeDuplicate() throws MalformedURLException { + WebXml withDuplicate = new WebXml(); + withDuplicate.setURL(new URL("https://example.com/original.jar")); + withDuplicate.addDuplicate("https://example.com/duplicate.jar"); + + Map fragmentsWithDuplicate = new LinkedHashMap<>(); + fragmentsWithDuplicate.put("has-duplicate", withDuplicate); + + WebXml.orderWebFragments(app, fragmentsWithDuplicate, null); + } + + private interface RelativeOrderingTestRunner { + void init(); + void validate(String order); + } + + private class RelativeTestRunner1 implements RelativeOrderingTestRunner { + + @Override + public void init() { + a.addAfterOrderingOthers(); + a.addAfterOrdering("c"); + b.addBeforeOrderingOthers(); + c.addAfterOrderingOthers(); + f.addBeforeOrderingOthers(); + f.addBeforeOrdering("b"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + //a.addAfterOrderingOthers(); + Assert.assertTrue(order, posA > posB); + Assert.assertTrue(order, posA > posC); + Assert.assertTrue(order, posA > posD); + Assert.assertTrue(order, posA > posE); + Assert.assertTrue(order, posA > posF); + + // a.addAfterOrdering("c"); + Assert.assertTrue(order, posA > posC); + + // b.addBeforeOrderingOthers(); + Assert.assertTrue(order, posB < posC); + + // c.addAfterOrderingOthers(); + Assert.assertTrue(order, posC > posB); + Assert.assertTrue(order, posC > posD); + Assert.assertTrue(order, posC > posE); + Assert.assertTrue(order, posC > posF); + + // f.addBeforeOrderingOthers(); + Assert.assertTrue(order, posF < posA); + Assert.assertTrue(order, posF < posB); + Assert.assertTrue(order, posF < posC); + Assert.assertTrue(order, posF < posD); + Assert.assertTrue(order, posF < posE); + + // f.addBeforeOrdering("b"); + Assert.assertTrue(order, posF < posB); + } + } + + private class RelativeTestRunner2 implements RelativeOrderingTestRunner { + + @Override + public void init() { + a.addAfterOrderingOthers(); + a.addBeforeOrdering("c"); + b.addBeforeOrderingOthers(); + d.addAfterOrderingOthers(); + e.addBeforeOrderingOthers(); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // a.addAfterOrderingOthers(); + Assert.assertTrue(order, posA > posB); + Assert.assertTrue(order, posA > posE); + Assert.assertTrue(order, posA > posF); + + // a.addBeforeOrdering("c"); + Assert.assertTrue(order, posC > posA); + Assert.assertTrue(order, posC > posB); + Assert.assertTrue(order, posC > posE); + Assert.assertTrue(order, posC > posF); + + // b.addBeforeOrderingOthers(); + Assert.assertTrue(order, posB < posA); + Assert.assertTrue(order, posB < posC); + Assert.assertTrue(order, posB < posD); + Assert.assertTrue(order, posB < posF); + + // d.addAfterOrderingOthers(); + Assert.assertTrue(order, posD > posB); + Assert.assertTrue(order, posD > posE); + Assert.assertTrue(order, posD > posF); + + // e.addBeforeOrderingOthers(); + Assert.assertTrue(order, posE < posA); + Assert.assertTrue(order, posE < posC); + Assert.assertTrue(order, posE < posD); + Assert.assertTrue(order, posE < posF); + } + } + + private class RelativeTestRunner3 implements RelativeOrderingTestRunner { + + @Override + public void init() { + a.addAfterOrdering("b"); + c.addBeforeOrderingOthers(); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // a.addAfterOrdering("b"); + Assert.assertTrue(order, posA > posB); + + // c.addBeforeOrderingOthers(); + Assert.assertTrue(order, posC < posA); + Assert.assertTrue(order, posC < posB); + Assert.assertTrue(order, posC < posD); + Assert.assertTrue(order, posC < posE); + Assert.assertTrue(order, posC < posF); + } + } + + private class RelativeTestRunner4 implements RelativeOrderingTestRunner { + + @Override + public void init() { + b.addAfterOrdering("a"); + c.addAfterOrdering("b"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // b.addAfterOrdering("a"); + Assert.assertTrue(order, posB > posA); + + // c.addAfterOrdering("b"); + Assert.assertTrue(order, posC > posB); + } + } + + private class RelativeTestRunner5 implements RelativeOrderingTestRunner { + + @Override + public void init() { + b.addBeforeOrdering("a"); + c.addBeforeOrdering("b"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // b.addBeforeOrdering("a"); + Assert.assertTrue(order, posB < posA); + + // c.addBeforeOrdering("b"); + Assert.assertTrue(order, posC < posB); + } + } + + private class RelativeTestRunner6 implements RelativeOrderingTestRunner { + + @Override + public void init() { + b.addBeforeOrdering("a"); + b.addAfterOrdering("c"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // b.addBeforeOrdering("a"); + Assert.assertTrue(order, posB < posA); + + //b.addAfterOrdering("c"); + Assert.assertTrue(order, posB > posC); + } + } + + private class RelativeTestRunner7 implements RelativeOrderingTestRunner { + + @Override + public void init() { + b.addBeforeOrdering("a"); + c.addBeforeOrdering("b"); + a.addAfterOrdering("c"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // b.addBeforeOrdering("a"); + Assert.assertTrue(order, posB < posA); + + // c.addBeforeOrdering("b"); + Assert.assertTrue(order, posC < posB); + + // a.addAfterOrdering("c"); + Assert.assertTrue(order, posA > posC); + } + } + + private class RelativeTestRunner8 implements RelativeOrderingTestRunner { + + @Override + public void init() { + a.addBeforeOrderingOthers(); + a.addBeforeOrdering("b"); + b.addBeforeOrderingOthers(); + c.addAfterOrdering("b"); + d.addAfterOrdering("c"); + e.addAfterOrderingOthers(); + f.addAfterOrderingOthers(); + f.addAfterOrdering("e"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // a.addBeforeOrderingOthers(); + Assert.assertTrue(order, posA < posB); + Assert.assertTrue(order, posA < posC); + Assert.assertTrue(order, posA < posD); + Assert.assertTrue(order, posA < posE); + Assert.assertTrue(order, posA < posF); + + // a.addBeforeOrdering("b"); + Assert.assertTrue(order, posA < posB); + + // b.addBeforeOrderingOthers(); + Assert.assertTrue(order, posB < posC); + Assert.assertTrue(order, posB < posD); + Assert.assertTrue(order, posB < posE); + Assert.assertTrue(order, posB < posF); + + // c.addAfterOrdering("b"); + Assert.assertTrue(order, posC > posB); + + // d.addAfterOrdering("c"); + Assert.assertTrue(order, posD > posC); + + // e.addAfterOrderingOthers(); + Assert.assertTrue(order, posE > posA); + Assert.assertTrue(order, posE > posB); + Assert.assertTrue(order, posE > posC); + Assert.assertTrue(order, posE > posD); + + // f.addAfterOrderingOthers(); + Assert.assertTrue(order, posF > posA); + Assert.assertTrue(order, posF > posB); + Assert.assertTrue(order, posF > posC); + Assert.assertTrue(order, posF > posD); + Assert.assertTrue(order, posF > posE); + + // f.addAfterOrdering("e"); + Assert.assertTrue(order, posF > posE); + } + } + + private class RelativeTestRunner9 implements RelativeOrderingTestRunner { + + @Override + public void init() { + a.addBeforeOrderingOthers(); + b.addBeforeOrdering("a"); + c.addBeforeOrdering("b"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // a.addBeforeOrderingOthers(); + Assert.assertTrue(order, posA < posD); + Assert.assertTrue(order, posA < posE); + Assert.assertTrue(order, posA < posF); + + // b.addBeforeOrdering("a"); + Assert.assertTrue(order, posB < posA); + + // c.addBeforeOrdering("b"); + Assert.assertTrue(order, posC < posB); + } + } + + private class RelativeTestRunner10 implements RelativeOrderingTestRunner { + + @Override + public void init() { + a.addAfterOrderingOthers(); + b.addAfterOrdering("a"); + c.addAfterOrdering("b"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // a.addAfterOrderingOthers(); + Assert.assertTrue(order, posA > posD); + Assert.assertTrue(order, posA > posE); + Assert.assertTrue(order, posA > posF); + + // b.addAfterOrdering("a"); + Assert.assertTrue(order, posB > posA); + + // c.addAfterOrdering("b"); + Assert.assertTrue(order, posC > posB); + } + } + + private class RelativeTestRunner11 implements RelativeOrderingTestRunner { + + @Override + public void init() { + a.addAfterOrdering("b"); + b.addAfterOrdering("z"); + b.addBeforeOrdering("y"); + } + + @Override + public void validate(String order) { + // There is some duplication in the tests below - it is easier to + // check the tests are complete this way. + + // a.addAfterOrdering("b"); + Assert.assertTrue(order, posA > posB); + } + } +} diff --git a/test/org/apache/tomcat/util/file/TestConfigFileLoader.java b/test/org/apache/tomcat/util/file/TestConfigFileLoader.java new file mode 100644 index 0000000..3439c62 --- /dev/null +++ b/test/org/apache/tomcat/util/file/TestConfigFileLoader.java @@ -0,0 +1,67 @@ +/* + * 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.tomcat.util.file; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.Globals; +import org.apache.catalina.startup.CatalinaBaseConfigurationSource; +import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; + +public class TestConfigFileLoader { + + @BeforeClass + public static void setup() { + TomcatURLStreamHandlerFactory.getInstance(); + System.setProperty(Globals.CATALINA_BASE_PROP, ""); + ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(System.getProperty(Globals.CATALINA_BASE_PROP)), null)); + } + + @Test + public void test01() throws IOException { + doTest("classpath:org/apache/catalina/mbeans-descriptors.xml"); + } + + @Test(expected=FileNotFoundException.class) + public void test02() throws IOException { + doTest("classpath:org/apache/catalina/foo"); + } + + @Test + public void test03() throws IOException { + doTest("test/webresources/dir1"); + } + + @Test(expected=FileNotFoundException.class) + public void test04() throws IOException { + doTest("test/webresources/unknown"); + } + + private void doTest(String path) throws IOException { + try (InputStream is = ConfigFileLoader.getSource().getResource(path).getInputStream()) { + Assert.assertNotNull(is); + } + } +} diff --git a/test/org/apache/tomcat/util/http/TestConcurrentDateFormat.java b/test/org/apache/tomcat/util/http/TestConcurrentDateFormat.java new file mode 100644 index 0000000..51731a0 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestConcurrentDateFormat.java @@ -0,0 +1,51 @@ +/* + * 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.tomcat.util.http; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.Assert; +import org.junit.Test; + +public class TestConcurrentDateFormat { + + private static final String DATE_RFC5322 = "EEE, dd MMM yyyy HH:mm:ss z"; + TimeZone tz = TimeZone.getTimeZone("GMT"); + + @Test + public void testFormatReturnsGMT() { + ConcurrentDateFormat format = createConcurrentDateFormat(); + Date date = new Date(); + String formattedDate = format.format(date); + Assert.assertTrue(formattedDate.endsWith("GMT")); + } + + @Test + public void testFormatReturnsGMTAfterParseCET() throws Exception { + ConcurrentDateFormat format = createConcurrentDateFormat(); + format.parse("Thu, 12 Mar 2020 14:40:22 CET"); + Date date = new Date(); + String formattedDate = format.format(date); + Assert.assertTrue(formattedDate.endsWith("GMT")); + } + + private ConcurrentDateFormat createConcurrentDateFormat() { + return new ConcurrentDateFormat(DATE_RFC5322, Locale.US, tz); + } +} diff --git a/test/org/apache/tomcat/util/http/TestCookieParsing.java b/test/org/apache/tomcat/util/http/TestCookieParsing.java new file mode 100644 index 0000000..eb97917 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestCookieParsing.java @@ -0,0 +1,230 @@ +/* + * 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.tomcat.util.http; + +import java.io.IOException; +import java.util.Enumeration; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + + +public class TestCookieParsing extends TomcatBaseTest { + + private static final String[] COOKIES_WITH_EQUALS = new String[] { + "name=equals=middle", "name==equalsstart", "name=equalsend=" }; + + private static final String[] COOKIES_WITH_NAME_ONLY = new String[] { + "bob", "bob=" }; + private static final String COOKIES_WITH_NAME_ONLY_CONCAT = "bob=bob="; + + private static final String[] COOKIES_WITH_SEPS = new String[] { + "name=val/ue" }; + + private static final String[] COOKIES_WITH_QUOTES = new String[] { + "name=\"val\\\"ue\"", "name=\"value\"" }; + + private static final String[] COOKIES_V0 = new String[] { + "$Version=0;name=\"val ue\"", "$Version=0;name=\"val\tue\""}; + + private static final String COOKIES_V0_CONCAT = "$Version=0$Version=0"; + + private static final String[] COOKIES_V1 = new String[] { + "$Version=1;name=\"val ue\"", "$Version=1;name=\"val\tue\""}; + + private static final String COOKIES_V1_CONCAT = "$Version=1$Version=1"; + + + @Test + public void testRfc6265Equals() throws Exception { + // Always allows equals + TestCookieParsingClient client = new TestCookieParsingClient( + new Rfc6265CookieProcessor(), COOKIES_WITH_EQUALS, concat(COOKIES_WITH_EQUALS)); + client.doRequest(); + } + + + @Test + public void testRfc6265NameOnly() throws Exception { + // Always allows equals + TestCookieParsingClient client = new TestCookieParsingClient( + new Rfc6265CookieProcessor(), COOKIES_WITH_NAME_ONLY, + COOKIES_WITH_NAME_ONLY_CONCAT); + client.doRequest(); + } + + + @Test + public void testRfc6265V0() throws Exception { + TestCookieParsingClient client = new TestCookieParsingClient( + new Rfc6265CookieProcessor(), COOKIES_V0, COOKIES_V0_CONCAT); + client.doRequest(); + } + + + @Test + public void testRfc6265V1() throws Exception { + TestCookieParsingClient client = new TestCookieParsingClient( + new Rfc6265CookieProcessor(), COOKIES_V1, COOKIES_V1_CONCAT); + client.doRequest(); + } + + + @Test + public void testRfc6265Seps() throws Exception { + // Always allows equals + TestCookieParsingClient client = new TestCookieParsingClient( + new Rfc6265CookieProcessor(), COOKIES_WITH_SEPS, concat(COOKIES_WITH_SEPS)); + client.doRequest(); + } + + + @Test + public void testRfc6265PreserveHeader() throws Exception { + // Always allows equals + TestCookieParsingClient client = new TestCookieParsingClient(new Rfc6265CookieProcessor(), + true, COOKIES_WITH_QUOTES, concat(COOKIES_WITH_QUOTES)); + client.doRequest(); + } + + + private static String concat(String[] input) { + StringBuilder result = new StringBuilder(); + for (String s : input) { + result.append(s); + } + return result.toString(); + } + + + private class TestCookieParsingClient extends SimpleHttpClient { + + private final CookieProcessor cookieProcessor; + private final String[] cookies; + private final String expected; + private final boolean echoHeader; + + + TestCookieParsingClient(CookieProcessor cookieProcessor, + String[] cookies, String expected) { + this(cookieProcessor, false, cookies, expected); + } + + TestCookieParsingClient(CookieProcessor cookieProcessor, + boolean echoHeader, String[] cookies, String expected) { + this.cookieProcessor = cookieProcessor; + this.echoHeader = echoHeader; + this.cookies = cookies; + this.expected = expected; + } + + + private void doRequest() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context root = tomcat.addContext("", TEMP_DIR); + root.setCookieProcessor(cookieProcessor); + + if (echoHeader) { + Tomcat.addServlet(root, "Cookies", new EchoCookieHeader()); + } else { + Tomcat.addServlet(root, "Cookies", new EchoCookies()); + } + root.addServletMappingDecoded("/test", "Cookies"); + + tomcat.start(); + // Open connection + setPort(tomcat.getConnector().getLocalPort()); + connect(); + + StringBuilder request = new StringBuilder(); + request.append("GET /test HTTP/1.0"); + request.append(CRLF); + for (String cookie : cookies) { + request.append("Cookie: "); + request.append(cookie); + request.append(CRLF); + } + request.append(CRLF); + setRequest(new String[] {request.toString()}); + processRequest(true); // blocks until response has been read + String response = getResponseBody(); + + // Close the connection + disconnect(); + reset(); + tomcat.stop(); + + Assert.assertEquals(expected, response); + } + + + @Override + public boolean isResponseBodyOK() { + return true; + } + } + + + private static class EchoCookies extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + Cookie cookies[] = req.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + resp.getWriter().write(cookie.getName() + "=" + + cookie.getValue()); + } + } + resp.flushBuffer(); + } + } + + + private static class EchoCookieHeader extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + req.getCookies(); + // Never do this in production code. It triggers an XSS. + Enumeration cookieHeaders = req.getHeaders("Cookie"); + while (cookieHeaders.hasMoreElements()) { + String cookieHeader = cookieHeaders.nextElement(); + resp.getWriter().write(cookieHeader); + } + resp.flushBuffer(); + } + } + +} diff --git a/test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java b/test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java new file mode 100644 index 0000000..3079f42 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java @@ -0,0 +1,297 @@ +/* + * 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.tomcat.util.http; + +import jakarta.servlet.http.Cookie; + +import org.junit.Assert; +import org.junit.Test; + +public class TestCookieProcessorGeneration { + + @Test + public void simpleCookie() { + doTest(new Cookie("foo", "bar"), "foo=bar"); + } + + @Test + public void nullValue() { + doTest(new Cookie("foo", null), "foo="); + } + + @Test + public void quotedValue() { + doTest(new Cookie("foo", "\"bar\""), "foo=\"bar\""); + } + + @Test + public void valueContainsSemicolon() { + doTest(new Cookie("foo", "a;b"), null); + } + + @Test + public void valueContainsComma() { + doTest(new Cookie("foo", "a,b"), null); + } + + @Test + public void valueContainsSpace() { + doTest(new Cookie("foo", "a b"), null); + } + + @Test + public void valueContainsEquals() { + doTest(new Cookie("foo", "a=b"), "foo=a=b"); + } + + @Test + public void valueContainsQuote() { + Cookie cookie = new Cookie("foo", "a\"b"); + doTest(cookie, null); + } + + @Test + public void valueContainsNonV0Separator() { + Cookie cookie = new Cookie("foo", "a()<>@:\\\"/[]?={}b"); + doTest(cookie, null); + } + + @Test + public void valueContainsBackslash() { + Cookie cookie = new Cookie("foo", "a\\b"); + doTest(cookie, null); + } + + @Test + public void valueContainsBackslashAtEnd() { + Cookie cookie = new Cookie("foo", "a\\"); + doTest(cookie, null); + } + + @Test + public void valueContainsBackslashAndQuote() { + Cookie cookie = new Cookie("foo", "a\"b\\c"); + doTest(cookie, null); + } + + @Test + public void valueUTF8() { + String value = "\u2300"; + Cookie cookie = new Cookie("foo", value); + doTest(cookie, "foo=" + value); + } + + @Test + public void valueNull() { + Cookie cookie = new Cookie("foo", "bar"); + cookie.setAttribute("other", "anything"); + cookie.setAttribute("other", null); + doTest(cookie, "foo=bar"); + } + + @Test + public void testMaxAgePositive() { + doTestMaxAge(100, "foo=bar; Max-Age=100"); + } + + @Test + public void testMaxAgeZero() { + doTestMaxAge(0, "foo=bar; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:10 GMT"); + } + + @Test + public void testMaxAgeNegative() { + doTestMaxAge(-100, "foo=bar"); + } + + @Test + public void testDomainValid01() { + doTestDomain("example.com", "foo=bar; Domain=example.com"); + } + + @Test + public void testDomainValid02() { + doTestDomain("exa-mple.com", "foo=bar; Domain=exa-mple.com"); + } + + @Test + public void testDomainInvalid01() { + doTestDomain("example.com.", null); + } + + @Test + public void testDomainInvalid02() { + doTestDomain("example.com-", null); + } + + @Test + public void testDomainInvalid03() { + doTestDomain(".example.com.", null); + } + + @Test + public void testDomainInvalid04() { + doTestDomain("-example.com.", null); + } + + @Test + public void testDomainInvalid05() { + doTestDomain("example..com.", null); + } + + @Test + public void testDomainInvalid06() { + doTestDomain("example-.com.", null); + } + + @Test + public void testDomainInvalid07() { + doTestDomain("exam$ple.com.", null); + } + + @Test + public void testPathValid() { + doTestPath("/example", "foo=bar; Path=/example"); + } + + @Test + public void testPathInvalid01() { + doTestPath("exa\tmple", null); + } + + @Test + public void testSameSiteCookies() { + Rfc6265CookieProcessor rfc6265 = new Rfc6265CookieProcessor(); + + Cookie cookie = new Cookie("foo", "bar"); + + Assert.assertEquals("foo=bar", rfc6265.generateHeader(cookie, null)); + + rfc6265.setSameSiteCookies("unset"); + + Assert.assertEquals("foo=bar", rfc6265.generateHeader(cookie, null)); + + rfc6265.setSameSiteCookies("none"); + + Assert.assertEquals("foo=bar; SameSite=None", rfc6265.generateHeader(cookie, null)); + + rfc6265.setSameSiteCookies("lax"); + + Assert.assertEquals("foo=bar; SameSite=Lax", rfc6265.generateHeader(cookie, null)); + + rfc6265.setSameSiteCookies("strict"); + + Assert.assertEquals("foo=bar; SameSite=Strict", rfc6265.generateHeader(cookie, null)); + + cookie.setSecure(true); + cookie.setHttpOnly(true); + + rfc6265.setSameSiteCookies("unset"); + + Assert.assertEquals("foo=bar; Secure; HttpOnly", rfc6265.generateHeader(cookie, null)); + + rfc6265.setSameSiteCookies("none"); + + Assert.assertEquals("foo=bar; Secure; HttpOnly; SameSite=None", rfc6265.generateHeader(cookie, null)); + + rfc6265.setSameSiteCookies("lax"); + + Assert.assertEquals("foo=bar; Secure; HttpOnly; SameSite=Lax", rfc6265.generateHeader(cookie, null)); + + rfc6265.setSameSiteCookies("strict"); + + Assert.assertEquals("foo=bar; Secure; HttpOnly; SameSite=Strict", rfc6265.generateHeader(cookie, null)); + } + + + @Test + public void testPartitionedCookies() { + Rfc6265CookieProcessor rfc6265 = new Rfc6265CookieProcessor(); + + Cookie cookie = new Cookie("foo", "bar"); + + Assert.assertEquals("foo=bar", rfc6265.generateHeader(cookie, null)); + + rfc6265.setPartitioned(false); + + Assert.assertEquals("foo=bar", rfc6265.generateHeader(cookie, null)); + + rfc6265.setPartitioned(true); + + Assert.assertEquals("foo=bar; Partitioned", rfc6265.generateHeader(cookie, null)); + + cookie.setSecure(true); + cookie.setHttpOnly(true); + + rfc6265.setPartitioned(false); + + Assert.assertEquals("foo=bar; Secure; HttpOnly", rfc6265.generateHeader(cookie, null)); + + rfc6265.setPartitioned(true); + + Assert.assertEquals("foo=bar; Secure; HttpOnly; Partitioned", rfc6265.generateHeader(cookie, null)); + } + + + private void doTest(Cookie cookie, String expectedRfc6265) { + CookieProcessor rfc6265 = new Rfc6265CookieProcessor(); + doTest(cookie, rfc6265, expectedRfc6265); + } + + + private void doTest(Cookie cookie, CookieProcessor cookieProcessor, String expected) { + if (expected == null) { + IllegalArgumentException e = null; + try { + cookieProcessor.generateHeader(cookie, null); + } catch (IllegalArgumentException iae) { + e = iae; + } + Assert.assertNotNull("Failed to throw IAE", e); + } else { + if (cookieProcessor instanceof Rfc6265CookieProcessor && + cookie.getMaxAge() > 0) { + // Expires attribute will depend on time cookie is generated so + // use a modified test + Assert.assertTrue(cookieProcessor.generateHeader(cookie, null).startsWith(expected)); + } else { + Assert.assertEquals(expected, cookieProcessor.generateHeader(cookie, null)); + } + } + } + + + private void doTestMaxAge(int age, String expectedRfc6265) { + Cookie cookie = new Cookie("foo", "bar"); + cookie.setMaxAge(age); + doTest(cookie, new Rfc6265CookieProcessor(), expectedRfc6265); + } + + + private void doTestDomain(String domain, String expectedRfc6265) { + Cookie cookie = new Cookie("foo", "bar"); + cookie.setDomain(domain); + doTest(cookie, new Rfc6265CookieProcessor(), expectedRfc6265); + } + + + private void doTestPath(String path, String expectedRfc6265) { + Cookie cookie = new Cookie("foo", "bar"); + cookie.setPath(path); + doTest(cookie, new Rfc6265CookieProcessor(), expectedRfc6265); + } +} diff --git a/test/org/apache/tomcat/util/http/TestCookieProcessorGenerationHttp.java b/test/org/apache/tomcat/util/http/TestCookieProcessorGenerationHttp.java new file mode 100644 index 0000000..468d051 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestCookieProcessorGenerationHttp.java @@ -0,0 +1,86 @@ +/* + * 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.tomcat.util.http; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestCookieProcessorGenerationHttp extends TomcatBaseTest { + + @Test + public void testUtf8CookieValue() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.setCookieProcessor(new Rfc6265CookieProcessor()); + Tomcat.addServlet(ctx, "test", new CookieServlet("\u0120")); + ctx.addServletMappingDecoded("/test", "test"); + tomcat.start(); + + Map> headers = new HashMap<>(); + ByteChunk res = new ByteChunk(); + getUrl("http://localhost:" + getPort() + "/test", res, headers); + List cookieHeaders = headers.get("Set-Cookie"); + Assert.assertEquals("There should only be one Set-Cookie header in this test", + 1, cookieHeaders.size()); + // Client is assuming header is ISO-8859-1 encoding which it isn't. Turn + // the header value back into the received bytes (this isn't guaranteed + // to work with all values but it will for this test value) + byte[] headerBytes = cookieHeaders.get(0).getBytes(StandardCharsets.ISO_8859_1); + // Now convert those bytes to a String using UTF-8 + String utf8Header = new String(headerBytes, StandardCharsets.UTF_8); + Assert.assertEquals("Test=\u0120", utf8Header); + } + + + private static class CookieServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final String cookieValue; + + CookieServlet(String cookieValue) { + this.cookieValue = cookieValue; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + Cookie cookie = new Cookie("Test", cookieValue); + resp.addCookie(cookie); + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + } + } +} diff --git a/test/org/apache/tomcat/util/http/TestCookies.java b/test/org/apache/tomcat/util/http/TestCookies.java new file mode 100644 index 0000000..2658017 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestCookies.java @@ -0,0 +1,297 @@ +/* + * 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.tomcat.util.http; + +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.http.Cookie; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.MessageBytes; + +public class TestCookies { + private final Cookie FOO = new Cookie("foo", "bar"); + private final Cookie FOO_EMPTY = new Cookie("foo", ""); + private final Cookie FOO_CONTROL = new Cookie("foo", "b\u00e1r"); + private final Cookie BAR = new Cookie("bar", "rab"); + private final Cookie BAR_EMPTY = new Cookie("bar", ""); + private final Cookie A = new Cookie("a", "b"); + private final Cookie HASH_EMPTY = new Cookie("#", ""); + private final Cookie $PORT = new Cookie("$Port", "8080"); + // RFC 2109 attributes are generally interpreted as additional cookies by + // RFC 6265 + private final Cookie $VERSION_0 = new Cookie("$Version", "0"); + private final Cookie $VERSION_1 = new Cookie("$Version", "1"); + private final Cookie $DOMAIN_ASF = new Cookie("$Domain", "apache.org"); + private final Cookie $DOMAIN_YAHOO = new Cookie("$Domain", "yahoo.com"); + private final Cookie $PATH = new Cookie("$Path", "/examples"); + + @Test + public void testBasicCookieRfc6265() { + test("foo=bar; a=b", FOO, A); + test("foo=bar;a=b", FOO, A); + test("foo=bar;a=b;", FOO, A); + test("foo=bar;a=b; ", FOO, A); + test("foo=bar;a=b; ;", FOO, A); + } + + @Test + public void testNameOnlyAreDroppedRfc6265() { + // Name only cookies are not dropped in RFC6265 + test("foo=;a=b; ;", FOO_EMPTY, A); + test("foo;a=b; ;", FOO_EMPTY, A); + test("foo;a=b;bar", FOO_EMPTY, A, BAR_EMPTY); + test("foo;a=b;bar;", FOO_EMPTY, A, BAR_EMPTY); + test("foo;a=b;bar ", FOO_EMPTY, A, BAR_EMPTY); + test("foo;a=b;bar ;", FOO_EMPTY, A, BAR_EMPTY); + + // Bug 49000 + Cookie fred = new Cookie("fred", "1"); + Cookie jim = new Cookie("jim", "2"); + Cookie bobEmpty = new Cookie("bob", ""); + Cookie george = new Cookie("george", "3"); + test("fred=1; jim=2; bob", fred, jim, bobEmpty); + test("fred=1; jim=2; bob; george=3", fred, jim, bobEmpty, george); + test("fred=1; jim=2; bob=; george=3", fred, jim, bobEmpty, george); + test("fred=1; jim=2; bob=", fred, jim, bobEmpty); + } + + @Test + public void testQuotedValueRfc6265() { + test("foo=bar;a=\"b\"", FOO, A); + test("foo=bar;a=\"b\";", FOO, A); + } + + @Test + public void testEmptyPairsRfc6265() { + test("foo;a=b; ;bar", FOO_EMPTY, A, BAR_EMPTY); + test("foo;a=b;;bar", FOO_EMPTY, A, BAR_EMPTY); + test("foo;a=b; ;;bar=rab", FOO_EMPTY, A, BAR); + test("foo;a=b;; ;bar=rab", FOO_EMPTY, A, BAR); + test("foo;a=b;;#;bar=rab", FOO_EMPTY, A, HASH_EMPTY, BAR); + test("foo;a=b;;\\;bar=rab", FOO_EMPTY, A, BAR); + } + + @Test + public void testSeparatorsInValueRfc6265() { + test("a=()<>@:\\\"/[]?={}\t; foo=bar", FOO); + } + + + @Test + public void v1TokenValueRfc6265() { + test("$Version=1; foo=bar;a=b", $VERSION_1, FOO, A); + test("$Version=1;foo=bar;a=b; ; ", $VERSION_1, FOO, A); + } + + @Test + public void v1NameOnlyRfc6265() { + test("$Version=1;foo=;a=b; ; ", $VERSION_1, FOO_EMPTY, A); + test("$Version=1;foo= ;a=b; ; ", $VERSION_1, FOO_EMPTY, A); + test("$Version=1;foo;a=b; ; ", $VERSION_1, FOO_EMPTY, A); + } + + @Test + public void v1QuotedValueRfc6265() { + test("$Version=1;foo=\"bar\";a=b; ; ", $VERSION_1, FOO, A); + } + + @Test + public void v1DQuoteInValueRfc6265() { + test("$Version=1;foo=\"b\"ar\";a=b", $VERSION_1, A); // Incorrectly escaped. + } + + @Test + public void v1QuoteInValueRfc6265() { + FOO.setValue("b'ar"); + test("$Version=1;foo=b'ar;a=b", $VERSION_1, FOO, A); + } + + + @Test + public void v1QuoteInQuotedValueRfc6265() { + FOO.setValue("b'ar"); + test("$Version=1;foo=\"b'ar\";a=b", $VERSION_1, FOO, A); + } + + @Test + public void v1EscapedDQuoteInValueRfc6265() { + // RFC 2109 considers the 2nd cookie to be correctly escaped. + // RFC 6265 considers the 2nd cookie to be invalid + test("$Version=1;foo=\"b\\\"ar\";a=b", $VERSION_1, A); + } + + @Test + public void v1QuotedValueEndsInBackslashRfc6265() { + test("$Version=1;foo=bar;a=\"b\\\"", $VERSION_1, FOO); + } + + @Test + public void v1MismatchedQuotesRfc6265() { + test("$Version=1;foo=bar;a=\"b\\", $VERSION_1, FOO); + } + + @Test + public void v1SingleQuotesAreValidTokenCharactersRfc6265() { + FOO.setValue("'bar'"); + test("$Version=1; foo='bar'", $VERSION_1, FOO); + } + + @Test + public void v1DomainIsParsedRfc6265() { + FOO.setDomain("apache.org"); + A.setDomain("yahoo.com"); + test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b;$Domain=yahoo.com", + $VERSION_1, FOO, $DOMAIN_ASF, A, $DOMAIN_YAHOO); + } + + @Test + public void v1DomainOnlyAffectsPrecedingCookieRfc6265() { + FOO.setDomain("apache.org"); + test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b", $VERSION_1, FOO, $DOMAIN_ASF, A); + } + + @Test + public void v1PortIsIgnoredRfc6265() { + FOO.setDomain("apache.org"); + test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b", $VERSION_1, FOO, $DOMAIN_ASF, $PORT, A); + } + + @Test + public void v1PathAffectsPrecedingCookieRfc6265() { + FOO.setPath("/examples"); + test("$Version=1;foo=\"bar\";$Path=/examples;a=b; ; ", $VERSION_1, FOO, $PATH, A); + } + + @Test + public void rfc2109Version0Rfc6265() { + test("$Version=0;foo=bar", $VERSION_0, FOO); + } + + @Test + public void disallow8bitInName() { + // Bug 55917 + test("f\u00f6o=bar"); + } + + @Test + public void disallowControlInName() { + // Bug 55917 + test("f\010o=bar"); + } + + @Test + public void disallow8BitControlInName() { + // Bug 55917 + test("f\210o=bar"); + } + + @Test + public void allow8BitInV0Value() { + // Bug 55917 + test("foo=b\u00e1r", FOO_CONTROL); + } + + @Test + public void eightBitInV1UnquotedValue() { + // Bug 55917 + // RFC 6265 considers this valid UTF-8 + FOO.setValue("b\u00e1r"); + test("$Version=1; foo=b\u00e1r", $VERSION_1, FOO); + } + + @Test + public void allow8bitInV1QuotedValue() { + // Bug 55917 + test("$Version=1; foo=\"b\u00e1r\"", $VERSION_1, FOO_CONTROL); + } + + @Test + public void disallowControlInV0Value() { + // Bug 55917 + test("foo=b\010r"); + } + + @Test + public void disallowControlInV1UnquotedValue() { + // Bug 55917 + test("$Version=1; foo=b\010r", $VERSION_1); + } + + @Test + public void disallowControlInV1QuotedValue() { + // Bug 55917 / Bug 55918 + test("$Version=1; foo=\"b\u0008r\"", $VERSION_1); + } + + @Test + public void eightBitControlInV1UnquotedValue() { + // Bug 55917 + // RFC 6265 considers this to be a valid UTF-8 value + test("$Version=1; foo=b\u0088r", $VERSION_1, new Cookie("foo", "b\u0088r")); + } + + @Test + public void testJsonInV0() { + // Bug 55921 + test("{\"a\":true, \"b\":false};a=b", A); + } + + @Test + public void testJsonInV1() { + // Bug 55921 + test("$Version=1;{\"a\":true, \"b\":false};a=b", $VERSION_1, A); + } + + @Test + public void testSkipSemicolonOrComma() { + // RFC 2109 cookies can also use commas to separate cookies + // RFC 6265 considers the second cookie invalid and skips it + test("$Version=1;x\tx=yyy,foo=bar;a=b", $VERSION_1, A); + } + + @Test + public void testBug60788Rfc6265() { + Cookie c1 = new Cookie("userId", "foo"); + Cookie c2 = new Cookie("$Path", "/"); + Cookie c3 = new Cookie("$Domain", "www.example.org"); + + test("$Version=\"1\"; userId=\"foo\";$Path=\"/\";$Domain=\"www.example.org\"", + $VERSION_1, c1, c2, c3); + } + + + private void test(String header, Cookie... expected) { + MimeHeaders mimeHeaders = new MimeHeaders(); + ServerCookies serverCookies = new ServerCookies(4); + CookieProcessor cookieProcessor = new Rfc6265CookieProcessor(); + MessageBytes cookieHeaderValue = mimeHeaders.addValue("Cookie"); + byte[] bytes = header.getBytes(StandardCharsets.UTF_8); + cookieHeaderValue.setBytes(bytes, 0, bytes.length); + cookieProcessor.parseCookieHeader(mimeHeaders, serverCookies); + Assert.assertEquals(expected.length, serverCookies.getCookieCount()); + for (int i = 0; i < expected.length; i++) { + Cookie cookie = expected[i]; + ServerCookie actual = serverCookies.getCookie(i); + Assert.assertEquals(cookie.getName(), actual.getName().toString()); + actual.getValue().getByteChunk().setCharset(StandardCharsets.UTF_8); + Assert.assertEquals(cookie.getValue(), actual.getValue().toString()); + } + } +} diff --git a/test/org/apache/tomcat/util/http/TestHeaderUtiltoPrintableString.java b/test/org/apache/tomcat/util/http/TestHeaderUtiltoPrintableString.java new file mode 100644 index 0000000..71da5e4 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestHeaderUtiltoPrintableString.java @@ -0,0 +1,83 @@ +/* + * 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.tomcat.util.http; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestHeaderUtiltoPrintableString { + + @Parameterized.Parameters(name = "{index}: expected[{1}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new String[] { "", "" }); + + parameterSets.add(new String[] { "abcd", "abcd" }); + + parameterSets.add(new String[] { "\u0000abcd", "0x00abcd" }); + parameterSets.add(new String[] { "ab\u0000cd", "ab0x00cd" }); + parameterSets.add(new String[] { "abcd\u0000", "abcd0x00" }); + + parameterSets.add(new String[] { "\tabcd", "0x09abcd" }); + parameterSets.add(new String[] { "ab\tcd", "ab0x09cd" }); + parameterSets.add(new String[] { "abcd\t", "abcd0x09" }); + + parameterSets.add(new String[] { " abcd", " abcd" }); + parameterSets.add(new String[] { "ab cd", "ab cd" }); + parameterSets.add(new String[] { "abcd ", "abcd " }); + + parameterSets.add(new String[] { "~abcd", "~abcd" }); + parameterSets.add(new String[] { "ab~cd", "ab~cd" }); + parameterSets.add(new String[] { "abcd~", "abcd~" }); + + parameterSets.add(new String[] { "\u007fabcd", "0x7fabcd" }); + parameterSets.add(new String[] { "ab\u007fcd", "ab0x7fcd" }); + parameterSets.add(new String[] { "abcd\u007f", "abcd0x7f" }); + + parameterSets.add(new String[] { "\u00a3abcd", "0xa3abcd" }); + parameterSets.add(new String[] { "ab\u00a3cd", "ab0xa3cd" }); + parameterSets.add(new String[] { "abcd\u00a3", "abcd0xa3" }); + + return parameterSets; + } + + + @Parameter(0) + public String input; + @Parameter(1) + public String expected; + + + @Test + public void doTest() { + byte[] bytes = input.getBytes(StandardCharsets.ISO_8859_1); + + String result = HeaderUtil.toPrintableString(bytes, 0, bytes.length); + + Assert.assertEquals(expected, result); + } +} diff --git a/test/org/apache/tomcat/util/http/TestMimeHeaders.java b/test/org/apache/tomcat/util/http/TestMimeHeaders.java new file mode 100644 index 0000000..efc7832 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestMimeHeaders.java @@ -0,0 +1,110 @@ +/* + * 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.tomcat.util.http; + +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +public class TestMimeHeaders { + + public static final String HEADER_NAME_LC_STRING = "test"; + public static final String HEADER_NAME_UC_STRING = "TEST"; + public static final String HEADER_NAME_MIXED_STRING = "tEsT"; + public static final String HEADER_NAME_A = "aaa"; + public static final String HEADER_NAME_B = "bbb"; + public static final String HEADER_NAME_C = "ccc"; + + @Test + public void testSetValueStringIgnoresCase01() { + MimeHeaders mh = new MimeHeaders(); + + mh.setValue(HEADER_NAME_LC_STRING).setString(HEADER_NAME_LC_STRING); + mh.setValue(HEADER_NAME_UC_STRING).setString(HEADER_NAME_UC_STRING); + + Assert.assertEquals(HEADER_NAME_UC_STRING, mh.getValue(HEADER_NAME_UC_STRING).toString()); + Assert.assertEquals(HEADER_NAME_UC_STRING, mh.getValue(HEADER_NAME_LC_STRING).toString()); + Assert.assertEquals(HEADER_NAME_UC_STRING, mh.getValue(HEADER_NAME_MIXED_STRING).toString()); + } + + @Test + public void testSetValueStringIgnoresCase02() { + MimeHeaders mh = new MimeHeaders(); + + mh.setValue(HEADER_NAME_UC_STRING).setString(HEADER_NAME_UC_STRING); + mh.setValue(HEADER_NAME_LC_STRING).setString(HEADER_NAME_LC_STRING); + + Assert.assertEquals(HEADER_NAME_LC_STRING, mh.getValue(HEADER_NAME_LC_STRING).toString()); + Assert.assertEquals(HEADER_NAME_LC_STRING, mh.getValue(HEADER_NAME_UC_STRING).toString()); + Assert.assertEquals(HEADER_NAME_LC_STRING, mh.getValue(HEADER_NAME_MIXED_STRING).toString()); + } + + @Test + public void testSetValueStringIgnoresCase03() { + MimeHeaders mh = new MimeHeaders(); + + mh.setValue(HEADER_NAME_UC_STRING).setString(HEADER_NAME_UC_STRING); + mh.setValue(HEADER_NAME_MIXED_STRING).setString(HEADER_NAME_MIXED_STRING); + + Assert.assertEquals(HEADER_NAME_MIXED_STRING, mh.getValue(HEADER_NAME_LC_STRING).toString()); + Assert.assertEquals(HEADER_NAME_MIXED_STRING, mh.getValue(HEADER_NAME_UC_STRING).toString()); + Assert.assertEquals(HEADER_NAME_MIXED_STRING, mh.getValue(HEADER_NAME_MIXED_STRING).toString()); + } + + @Test + public void testNamesEnumerator() { + MimeHeaders mh = new MimeHeaders(); + + mh.setValue(HEADER_NAME_A); + mh.setValue(HEADER_NAME_B); + mh.setValue(HEADER_NAME_C); + + Set expected = new HashSet<>(); + expected.add(HEADER_NAME_A); + expected.add(HEADER_NAME_B); + expected.add(HEADER_NAME_C); + + Enumeration names = mh.names(); + while (names.hasMoreElements()) { + Assert.assertTrue(expected.remove(names.nextElement())); + } + Assert.assertFalse(names.hasMoreElements()); + } + + @Test + public void testNamesEnumeratorWithNull() { + MimeHeaders mh = new MimeHeaders(); + + mh.setValue(HEADER_NAME_A); + mh.setValue(null); + mh.setValue(HEADER_NAME_C); + + Set expected = new HashSet<>(); + expected.add(HEADER_NAME_A); + expected.add(null); + expected.add(HEADER_NAME_C); + + Enumeration names = mh.names(); + while (names.hasMoreElements()) { + Assert.assertTrue(expected.remove(names.nextElement())); + } + Assert.assertFalse(names.hasMoreElements()); + } +} diff --git a/test/org/apache/tomcat/util/http/TestMimeHeadersIntegration.java b/test/org/apache/tomcat/util/http/TestMimeHeadersIntegration.java new file mode 100644 index 0000000..118294c --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestMimeHeadersIntegration.java @@ -0,0 +1,181 @@ +/* + * 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.tomcat.util.http; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.SocketException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.valves.TesterAccessLogValve; + +public class TestMimeHeadersIntegration extends TomcatBaseTest { + + private HeaderCountLogValve alv; + + private void setupHeadersTest(Tomcat tomcat) { + Context ctx = tomcat.addContext("", getTemporaryDirectory() + .getAbsolutePath()); + Tomcat.addServlet(ctx, "servlet", new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + public void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + res.setContentType("text/plain; charset=ISO-8859-1"); + res.getWriter().write("OK"); + } + }); + ctx.addServletMappingDecoded("/", "servlet"); + + alv = new HeaderCountLogValve(); + tomcat.getHost().getPipeline().addValve(alv); + } + + private void runHeadersTest(final boolean successExpected, + final Tomcat tomcat, final int count, + final int expectedMaxHeaderCount) throws Exception { + tomcat.start(); + + String header = "A:B" + SimpleHttpClient.CRLF; + StringBuilder request = new StringBuilder(); + request.append("GET / HTTP/1.0" + SimpleHttpClient.CRLF); + for (int i = 0; i < count; i++) { + request.append(header); + } + request.append(SimpleHttpClient.CRLF); + + Client client = new Client(tomcat); + client.setRequest(new String[] { request.toString() }); + try { + client.connect(); + client.processRequest(); + client.disconnect(); + } catch (SocketException ex) { + // Connection was closed by Tomcat + if (successExpected) { + // unexpected + log.error(ex.getMessage(), ex); + } else { + log.warn(ex.getMessage(), ex); + } + } + if (successExpected) { + alv.validateAccessLog(1, 200, 0, 3000); + // Response 200 + Assert.assertTrue("Response line is: " + client.getResponseLine(), + client.getResponseLine() != null && client.isResponse200()); + Assert.assertEquals("OK", client.getResponseBody()); + } else { + alv.validateAccessLog(1, 400, 0, 3000); + // Connection cancelled or response 400 + Assert.assertTrue("Response line is: " + client.getResponseLine(), + client.getResponseLine() == null || client.isResponse400()); + } + int maxHeaderCount = + ((Integer) tomcat.getConnector().getProperty("maxHeaderCount")).intValue(); + Assert.assertEquals(expectedMaxHeaderCount, maxHeaderCount); + if (maxHeaderCount > 0) { + Assert.assertEquals(maxHeaderCount, alv.arraySize); + } else if (maxHeaderCount < 0) { + int maxHttpHeaderSize = ((Integer) tomcat.getConnector().getProperty("maxHttpHeaderSize")).intValue(); + int headerCount = Math.min(count, + maxHttpHeaderSize / header.length() + 1); + int arraySize = 1; + while (arraySize < headerCount) { + arraySize <<= 1; + } + Assert.assertEquals(arraySize, alv.arraySize); + } + } + + @Test + public void testHeaderLimits1() throws Exception { + // Bumping into maxHttpHeaderSize + Tomcat tomcat = getTomcatInstance(); + setupHeadersTest(tomcat); + Assert.assertTrue(tomcat.getConnector().setProperty("maxHeaderCount", "-1")); + runHeadersTest(false, tomcat, 8 * 1024, -1); + } + + @Test + public void testHeaderLimits2() throws Exception { + // Can process 100 headers + Tomcat tomcat = getTomcatInstance(); + setupHeadersTest(tomcat); + runHeadersTest(true, tomcat, 100, 100); + } + + @Test + public void testHeaderLimits3() throws Exception { + // Cannot process 101 header + Tomcat tomcat = getTomcatInstance(); + setupHeadersTest(tomcat); + runHeadersTest(false, tomcat, 101, 100); + } + + @Test + public void testHeaderLimits4() throws Exception { + // Can change maxHeaderCount + Tomcat tomcat = getTomcatInstance(); + setupHeadersTest(tomcat); + Assert.assertTrue(tomcat.getConnector().setProperty("maxHeaderCount", "-1")); + runHeadersTest(true, tomcat, 300, -1); + } + + private static final class HeaderCountLogValve extends TesterAccessLogValve { + public volatile int arraySize = -1; + + @Override + public void log(Request request, Response response, long time) { + super.log(request, response, time); + try { + MimeHeaders mh = request.getCoyoteRequest().getMimeHeaders(); + Field headersArrayField = MimeHeaders.class + .getDeclaredField("headers"); + headersArrayField.setAccessible(true); + arraySize = ((Object[]) headersArrayField.get(mh)).length; + } catch (Exception ex) { + Assert.assertNull(ex.getMessage(), ex); + } + } + } + + private static final class Client extends SimpleHttpClient { + Client(Tomcat tomcat) { + setPort(tomcat.getConnector().getLocalPort()); + } + + @Override + public boolean isResponseBodyOK() { + return true; + } + } +} diff --git a/test/org/apache/tomcat/util/http/TestParameters.java b/test/org/apache/tomcat/util/http/TestParameters.java new file mode 100644 index 0000000..885b563 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestParameters.java @@ -0,0 +1,318 @@ +/* + * 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.tomcat.util.http; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; + +import org.junit.Assert; +import org.junit.Test; + +public class TestParameters { + + private static final Parameter SIMPLE = + new Parameter("foo1", "bar1"); + private static final Parameter SIMPLE_MULTIPLE = + new Parameter("foo2", "bar1", "bar2", "hello world", "?%@"); + private static final Parameter NO_VALUE = + new Parameter("foo3"); + private static final Parameter EMPTY_VALUE = + new Parameter("foo4", ""); + private static final Parameter EMPTY = + new Parameter(""); + private static final Parameter UTF8 = + new Parameter("\ufb6b\ufb6a\ufb72", "\uffee\uffeb\uffe2"); + + @Test + public void testProcessParametersByteArrayIntInt() { + doTestProcessParametersByteArrayIntInt(-1, SIMPLE); + doTestProcessParametersByteArrayIntInt(-1, SIMPLE_MULTIPLE); + doTestProcessParametersByteArrayIntInt(-1, NO_VALUE); + doTestProcessParametersByteArrayIntInt(-1, EMPTY_VALUE); + doTestProcessParametersByteArrayIntInt(-1, EMPTY); + doTestProcessParametersByteArrayIntInt(-1, UTF8); + doTestProcessParametersByteArrayIntInt(-1, + SIMPLE, SIMPLE_MULTIPLE, NO_VALUE, EMPTY_VALUE, EMPTY, UTF8); + doTestProcessParametersByteArrayIntInt(-1, + SIMPLE_MULTIPLE, NO_VALUE, EMPTY_VALUE, EMPTY, UTF8, SIMPLE); + doTestProcessParametersByteArrayIntInt(-1, + NO_VALUE, EMPTY_VALUE, EMPTY, UTF8, SIMPLE, SIMPLE_MULTIPLE); + doTestProcessParametersByteArrayIntInt(-1, + EMPTY_VALUE, EMPTY, UTF8, SIMPLE, SIMPLE_MULTIPLE, NO_VALUE); + doTestProcessParametersByteArrayIntInt(-1, + EMPTY, UTF8, SIMPLE, SIMPLE_MULTIPLE, NO_VALUE, EMPTY_VALUE); + doTestProcessParametersByteArrayIntInt(-1, + UTF8, SIMPLE, SIMPLE_MULTIPLE, NO_VALUE, EMPTY_VALUE, EMPTY); + + doTestProcessParametersByteArrayIntInt(1, + SIMPLE, NO_VALUE, EMPTY_VALUE, UTF8); + doTestProcessParametersByteArrayIntInt(2, + SIMPLE, NO_VALUE, EMPTY_VALUE, UTF8); + doTestProcessParametersByteArrayIntInt(3, + SIMPLE, NO_VALUE, EMPTY_VALUE, UTF8); + doTestProcessParametersByteArrayIntInt(4, + SIMPLE, NO_VALUE, EMPTY_VALUE, UTF8); + } + + // Make sure the inner Parameter class behaves correctly + @Test + public void testInternal() { + Assert.assertEquals("foo1=bar1", SIMPLE.toString()); + // Note: testing requires that ' ' is encoded as '+', + // because that is what browsers will send us. + Assert.assertEquals("foo2=bar1&foo2=bar2&foo2=hello+world&foo2=%3F%25%40", + SIMPLE_MULTIPLE.toString()); + Assert.assertEquals("foo3", NO_VALUE.toString()); + Assert.assertEquals("foo4=", EMPTY_VALUE.toString()); + } + + private long doTestProcessParametersByteArrayIntInt(int limit, + Parameter... parameters) { + + // Build the byte array + StringBuilder input = new StringBuilder(); + boolean first = true; + for (Parameter parameter : parameters) { + if (first) { + first = false; + } else { + input.append('&'); + } + input.append(parameter.toString()); + } + + byte[] data = input.toString().getBytes(); + + Parameters p = new Parameters(); + p.setCharset(StandardCharsets.UTF_8); + p.setLimit(limit); + + long start = System.nanoTime(); + p.processParameters(data, 0, data.length); + long end = System.nanoTime(); + + if (limit == -1) { + validateParameters(parameters, p); + } else { + Parameter[] limitParameters = new Parameter[limit]; + System.arraycopy(parameters, 0, limitParameters, 0, limit); + validateParameters(limitParameters, p); + } + return end - start; + } + + @Test + public void testNonExistantParameter() { + Parameters p = new Parameters(); + + String value = p.getParameter("foo"); + Assert.assertNull(value); + + Enumeration names = p.getParameterNames(); + Assert.assertFalse(names.hasMoreElements()); + + String[] values = p.getParameterValues("foo"); + Assert.assertNull(values); + } + + + @Test + public void testAddParameters() { + Parameters p = new Parameters(); + + // Empty at this point + Enumeration names = p.getParameterNames(); + Assert.assertFalse(names.hasMoreElements()); + String[] values = p.getParameterValues("foo"); + Assert.assertNull(values); + + // Add a parameter with two values + p.addParameter("foo", "value1"); + p.addParameter("foo", "value2"); + + names = p.getParameterNames(); + Assert.assertTrue(names.hasMoreElements()); + Assert.assertEquals("foo", names.nextElement()); + Assert.assertFalse(names.hasMoreElements()); + + values = p.getParameterValues("foo"); + Assert.assertEquals(2, values.length); + Assert.assertEquals("value1", values[0]); + Assert.assertEquals("value2", values[1]); + + // Add two more values + p.addParameter("foo", "value3"); + p.addParameter("foo", "value4"); + + names = p.getParameterNames(); + Assert.assertTrue(names.hasMoreElements()); + Assert.assertEquals("foo", names.nextElement()); + Assert.assertFalse(names.hasMoreElements()); + + values = p.getParameterValues("foo"); + Assert.assertEquals(4, values.length); + Assert.assertEquals("value1", values[0]); + Assert.assertEquals("value2", values[1]); + Assert.assertEquals("value3", values[2]); + Assert.assertEquals("value4", values[3]); + } + + @Test + public void testAddParametersLimit() { + Parameters p = new Parameters(); + + p.setLimit(2); + + // Empty at this point + Enumeration names = p.getParameterNames(); + Assert.assertFalse(names.hasMoreElements()); + String[] values = p.getParameterValues("foo1"); + Assert.assertNull(values); + + // Add a parameter + p.addParameter("foo1", "value1"); + + names = p.getParameterNames(); + Assert.assertTrue(names.hasMoreElements()); + Assert.assertEquals("foo1", names.nextElement()); + Assert.assertFalse(names.hasMoreElements()); + + values = p.getParameterValues("foo1"); + Assert.assertEquals(1, values.length); + Assert.assertEquals("value1", values[0]); + + // Add another parameter + p.addParameter("foo2", "value2"); + + names = p.getParameterNames(); + Assert.assertTrue(names.hasMoreElements()); + Assert.assertEquals("foo1", names.nextElement()); + Assert.assertEquals("foo2", names.nextElement()); + Assert.assertFalse(names.hasMoreElements()); + + values = p.getParameterValues("foo1"); + Assert.assertEquals(1, values.length); + Assert.assertEquals("value1", values[0]); + + values = p.getParameterValues("foo2"); + Assert.assertEquals(1, values.length); + Assert.assertEquals("value2", values[0]); + + // Add another parameter + IllegalStateException e = null; + try { + p.addParameter("foo3", "value3"); + } catch (IllegalStateException ise) { + e = ise; + } + Assert.assertNotNull(e); + + // Check current parameters remain unaffected + names = p.getParameterNames(); + Assert.assertTrue(names.hasMoreElements()); + Assert.assertEquals("foo1", names.nextElement()); + Assert.assertEquals("foo2", names.nextElement()); + Assert.assertFalse(names.hasMoreElements()); + + values = p.getParameterValues("foo1"); + Assert.assertEquals(1, values.length); + Assert.assertEquals("value1", values[0]); + + values = p.getParameterValues("foo2"); + Assert.assertEquals(1, values.length); + Assert.assertEquals("value2", values[0]); + + } + + private void validateParameters(Parameter[] parameters, Parameters p) { + Enumeration names = p.getParameterNames(); + + int i = 0; + while (names.hasMoreElements()) { + while (parameters[i].getName() == null) { + i++; + } + + String name = names.nextElement(); + String[] values = p.getParameterValues(name); + + boolean match = false; + + for (Parameter parameter : parameters) { + if (name.equals(parameter.getName())) { + match = true; + if (parameter.values.length == 0) { + // Special case + Assert.assertArrayEquals(new String[] {""}, values); + } else { + Assert.assertArrayEquals(parameter.getValues(), values); + } + break; + } + } + Assert.assertTrue(match); + } + } + + private static class Parameter { + private final String name; + private final String[] values; + + Parameter(String name, String... values) { + this.name = name; + this.values = values; + } + + public String getName() { + return name; + } + + public String[] getValues() { + return values; + } + + @Override + public String toString() { + try { + StringBuilder result = new StringBuilder(); + boolean first = true; + if (values.length == 0) { + return URLEncoder.encode(name, "UTF-8"); + } + for (String value : values) { + if (first) { + first = false; + } else { + result.append('&'); + } + if (name != null) { + result.append(URLEncoder.encode(name, "UTF-8")); + } + if (value != null) { + result.append('='); + result.append(URLEncoder.encode(value, "UTF-8")); + } + } + return result.toString(); + } catch (UnsupportedEncodingException ex) { + return ex.toString(); + } + } + } +} diff --git a/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java b/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java new file mode 100644 index 0000000..b642868 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java @@ -0,0 +1,77 @@ +/* + * 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.tomcat.util.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestRequestUtilNormalize { + + @Parameterized.Parameters(name = "{index}: input[{0}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new String[] { "//something", "/something" }); + parameterSets.add(new String[] { "some//thing", "/some/thing" }); + parameterSets.add(new String[] { "something//", "/something/" }); + parameterSets.add(new String[] { "//", "/" }); + parameterSets.add(new String[] { "///", "/" }); + parameterSets.add(new String[] { "////", "/" }); + parameterSets.add(new String[] { "/.", "/" }); + parameterSets.add(new String[] { "/./", "/" }); + parameterSets.add(new String[] { ".", "/" }); + parameterSets.add(new String[] { "/..", null }); + parameterSets.add(new String[] { "/../", null }); + parameterSets.add(new String[] { "..", null }); + parameterSets.add(new String[] { "//..", null }); + parameterSets.add(new String[] { "//../", null }); + parameterSets.add(new String[] { "/./..", null }); + parameterSets.add(new String[] { "/./../", null }); + parameterSets.add(new String[] { "/a/../..", null }); + parameterSets.add(new String[] { "/a/../../", null }); + parameterSets.add(new String[] { "/a/..", "/" }); + parameterSets.add(new String[] { "/a/.", "/a" }); + parameterSets.add(new String[] { "/a/../", "/" }); + parameterSets.add(new String[] { "/a/./", "/a/" }); + parameterSets.add(new String[] { "/a/b/..", "/a" }); + parameterSets.add(new String[] { "/a/b/.", "/a/b" }); + parameterSets.add(new String[] { "/a/b/../", "/a/" }); + parameterSets.add(new String[] { "/a/b/./", "/a/b/" }); + + return parameterSets; + } + + + @Parameter(0) + public String input; + @Parameter(1) + public String expected; + + + @Test + public void testNormalize() { + Assert.assertEquals(expected,RequestUtil.normalize(input)); + } +} diff --git a/test/org/apache/tomcat/util/http/TestRequestUtilSameOrigin.java b/test/org/apache/tomcat/util/http/TestRequestUtilSameOrigin.java new file mode 100644 index 0000000..49882c3 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestRequestUtilSameOrigin.java @@ -0,0 +1,113 @@ +/* + * 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.tomcat.util.http; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.connector.Request; + +@RunWith(Parameterized.class) +public class TestRequestUtilSameOrigin { + + @Parameterized.Parameters(name = "{index}: request[{0}], origin[{1}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + TesterRequest request1 = new TesterRequest("http", "example.com", 80); + TesterRequest request2 = new TesterRequest("ws", "example.com", 80); + TesterRequest request3 = new TesterRequest("http", "example.com", 443); + TesterRequest request4 = new TesterRequest("http", "example.com", 8080); + + parameterSets.add(new Object[] { request1, "http://example.com", Boolean.TRUE }); + parameterSets.add(new Object[] { request1, "http://example.com:80", Boolean.TRUE }); + parameterSets.add(new Object[] { request1, "http://example.com:8080", Boolean.FALSE}); + + parameterSets.add(new Object[] { request2, "ws://example.com", Boolean.TRUE }); + parameterSets.add(new Object[] { request2, "ws://example.com:80", Boolean.TRUE }); + parameterSets.add(new Object[] { request2, "ws://example.com:8080", Boolean.FALSE}); + + parameterSets.add(new Object[] { request3, "http://example.com", Boolean.FALSE }); + parameterSets.add(new Object[] { request3, "http://example.com:80", Boolean.FALSE }); + parameterSets.add(new Object[] { request3, "http://example.com:443", Boolean.TRUE}); + + parameterSets.add(new Object[] { request4, "http://example.com", Boolean.FALSE }); + parameterSets.add(new Object[] { request4, "http://example.com:80", Boolean.FALSE }); + parameterSets.add(new Object[] { request4, "http://example.com:8080", Boolean.TRUE}); + + return parameterSets; + } + + + @Parameter(0) + public HttpServletRequest request; + @Parameter(1) + public String origin; + @Parameter(2) + public Boolean same; + + + @Test + public void testSameOrigin() { + Assert.assertEquals(same, Boolean.valueOf(RequestUtil.isSameOrigin(request, origin))); + } + + + private static class TesterRequest extends HttpServletRequestWrapper { + + private final String scheme; + private final String host; + private final int port; + + TesterRequest(String scheme, String host, int port) { + super(new Request(null)); + this.scheme = scheme; + this.host = host; + this.port = port; + } + + @Override + public String getScheme() { + return scheme; + } + + @Override + public String getServerName() { + return host; + } + + @Override + public int getServerPort() { + return port; + } + + @Override + public String toString() { + return scheme + "://" + host + ":" + port; + } + } +} diff --git a/test/org/apache/tomcat/util/http/TestResponseUtil.java b/test/org/apache/tomcat/util/http/TestResponseUtil.java new file mode 100644 index 0000000..d988a20 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestResponseUtil.java @@ -0,0 +1,253 @@ +/* + * 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.tomcat.util.http; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.unittest.TesterResponse; + +public class TestResponseUtil { + + @Test + public void testAddValidWithAll() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "host"); + List expected = new ArrayList<>(); + expected.add("*"); + doTestAddVaryFieldName(response, "*", expected); + } + + + @Test + public void testAddAllWithAll() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "*"); + List expected = new ArrayList<>(); + expected.add("*"); + doTestAddVaryFieldName(response, "*", expected); + } + + + @Test + public void testAddAllWithNone() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + List expected = new ArrayList<>(); + expected.add("*"); + doTestAddVaryFieldName(response, "*", expected); + } + + + @Test + public void testAddValidWithValidSingleHeader() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "foo, bar"); + List expected = new ArrayList<>(); + expected.add("foo"); + expected.add("bar"); + expected.add("too"); + doTestAddVaryFieldName(response, "too", expected); + } + + + @Test + public void testAddValidWithValidSingleHeaderIncludingAll() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "foo, *"); + List expected = new ArrayList<>(); + expected.add("*"); + doTestAddVaryFieldName(response, "too", expected); + } + + + @Test + public void testAddValidWithValidSingleHeaderAlreadyPresent() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "foo, bar"); + List expected = new ArrayList<>(); + expected.add("foo"); + expected.add("bar"); + doTestAddVaryFieldName(response, "foo", expected); + } + + + @Test + public void testAddValidWithValidHeaders() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "foo"); + response.addHeader("vary", "bar"); + List expected = new ArrayList<>(); + expected.add("foo"); + expected.add("bar"); + expected.add("too"); + doTestAddVaryFieldName(response, "too", expected); + } + + + @Test + public void testAddValidWithValidHeadersIncludingAll() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "foo"); + response.addHeader("vary", "*"); + List expected = new ArrayList<>(); + expected.add("*"); + doTestAddVaryFieldName(response, "too", expected); + } + + + @Test + public void testAddValidWithValidHeadersAlreadyPresent() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "foo"); + response.addHeader("vary", "bar"); + List expected = new ArrayList<>(); + expected.add("foo"); + expected.add("bar"); + doTestAddVaryFieldName(response, "foo", expected); + } + + + @Test + public void testAddValidWithPartiallyValidSingleHeader() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "{{{, bar"); + List expected = new ArrayList<>(); + expected.add("bar"); + expected.add("too"); + doTestAddVaryFieldName(response, "too", expected); + } + + + @Test + public void testAddValidWithPartiallyValidSingleHeaderIncludingAll() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "{{{, *"); + List expected = new ArrayList<>(); + expected.add("*"); + doTestAddVaryFieldName(response, "too", expected); + } + + + @Test + public void testAddValidWithPartiallyValidSingleHeaderAlreadyPresent() { + TesterResponse response = new TesterResponse(); + response.getCoyoteResponse(); + response.addHeader("vary", "{{{, bar"); + List expected = new ArrayList<>(); + expected.add("bar"); + doTestAddVaryFieldName(response, "bar", expected); + } + + + private void doTestAddVaryFieldName(TesterResponse response, String fieldName, + List expected) { + ResponseUtil.addVaryFieldName(response, fieldName); + // There will now only be one Vary header + String resultHeader = response.getHeader("vary"); + List result = new ArrayList<>(); + // Deliberately do not use Vary.parseVary as it will skip invalid values. + for (String value : resultHeader.split(",")) { + result.add(value.trim()); + } + Assert.assertEquals(expected, result); + } + + + @Test + public void testMimeHeadersAddAllWithNone() { + MimeHeaders mh = new MimeHeaders(); + List expected = new ArrayList<>(); + expected.add("*"); + doTestAddVaryFieldName(mh, "*", expected); + } + + + @Test + public void testMimeHeadersAddValidWithValidHeaders() { + MimeHeaders mh = new MimeHeaders(); + mh.addValue("vary").setString("foo"); + mh.addValue("vary").setString("bar"); + List expected = new ArrayList<>(); + expected.add("foo"); + expected.add("bar"); + expected.add("too"); + doTestAddVaryFieldName(mh, "too", expected); + } + + private void doTestAddVaryFieldName(MimeHeaders mh, String fieldName, + List expected) { + ResponseUtil.addVaryFieldName(mh, fieldName); + // There will now only be one Vary header + String resultHeader = mh.getHeader("vary"); + List result = new ArrayList<>(); + // Deliberately do not use Vary.parseVary as it will skip invalid values. + for (String value : resultHeader.split(",")) { + result.add(value.trim()); + } + Assert.assertEquals(expected, result); + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=65505 + */ + @Test + public void testAddVaryHeaderOrder() { + MimeHeaders responseHeaders = new MimeHeaders(); + responseHeaders.addValue("Vary").setString("Origin"); + responseHeaders.addValue("Vary").setString("Access-Control-Request-Method"); + responseHeaders.addValue("Vary").setString("Access-Control-Request-Headers"); + responseHeaders.addValue("Access-Control-Allow-Origin").setString("https://xxxx"); + responseHeaders.addValue("Access-Control-Allow-Credentials").setString("true"); + responseHeaders.addValue("Set-Cookie").setString("rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Tue, 17-Aug-2021 11:19:04 GMT; SameSite=lax"); + responseHeaders.addValue("Set-Cookie").setString("rememberMe=rememberMeData; Path=/; Max-Age=1296000; Expires=Thu, 02-Sep-2021 11:19:04 GMT; HttpOnly; SameSite=lax"); + + String cookiesBefore = getHeaderValues(responseHeaders, "Set-Cookie"); + + ResponseUtil.addVaryFieldName(responseHeaders, "accept-encoding"); + + String cookiesAfter = getHeaderValues(responseHeaders, "Set-Cookie"); + + Assert.assertEquals(cookiesBefore, cookiesAfter); + } + + + private String getHeaderValues(MimeHeaders headers, String headerName) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < headers.size(); i++) { + if (headers.getName(i).equals(headerName)) { + sb.append(headers.getValue(i)); + sb.append('\n'); + } + } + return sb.toString(); + } +} diff --git a/test/org/apache/tomcat/util/http/TestSameSiteCookies.java b/test/org/apache/tomcat/util/http/TestSameSiteCookies.java new file mode 100644 index 0000000..e3f8889 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestSameSiteCookies.java @@ -0,0 +1,116 @@ +/* + * 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.tomcat.util.http; + +import org.junit.Assert; +import org.junit.Test; + + +public class TestSameSiteCookies { + + @Test + public void testUnset() { + SameSiteCookies attribute = SameSiteCookies.UNSET; + + Assert.assertEquals("Unset", attribute.getValue()); + Assert.assertEquals(SameSiteCookies.UNSET, attribute); + + Assert.assertNotEquals(SameSiteCookies.NONE, attribute); + Assert.assertNotEquals(SameSiteCookies.LAX, attribute); + Assert.assertNotEquals(SameSiteCookies.STRICT, attribute); + } + + @Test + public void testNone() { + SameSiteCookies attribute = SameSiteCookies.NONE; + + Assert.assertEquals("None", attribute.getValue()); + Assert.assertEquals(SameSiteCookies.NONE, attribute); + + Assert.assertNotEquals(SameSiteCookies.UNSET, attribute); + Assert.assertNotEquals(SameSiteCookies.LAX, attribute); + Assert.assertNotEquals(SameSiteCookies.STRICT, attribute); + } + + @Test + public void testLax() { + SameSiteCookies attribute = SameSiteCookies.LAX; + + Assert.assertEquals("Lax", attribute.getValue()); + Assert.assertEquals(SameSiteCookies.LAX, attribute); + + Assert.assertNotEquals(SameSiteCookies.UNSET, attribute); + Assert.assertNotEquals(SameSiteCookies.NONE, attribute); + Assert.assertNotEquals(SameSiteCookies.STRICT, attribute); + } + + @Test + public void testStrict() { + SameSiteCookies attribute = SameSiteCookies.STRICT; + + Assert.assertEquals("Strict", attribute.getValue()); + Assert.assertEquals(SameSiteCookies.STRICT, attribute); + + Assert.assertNotEquals(SameSiteCookies.UNSET, attribute); + Assert.assertNotEquals(SameSiteCookies.NONE, attribute); + Assert.assertNotEquals(SameSiteCookies.LAX, attribute); + } + + @Test + public void testToValidAttribute() { + Assert.assertEquals(SameSiteCookies.fromString("unset"), SameSiteCookies.UNSET); + Assert.assertEquals(SameSiteCookies.fromString("Unset"), SameSiteCookies.UNSET); + Assert.assertEquals(SameSiteCookies.fromString("UNSET"), SameSiteCookies.UNSET); + + Assert.assertEquals(SameSiteCookies.fromString("none"), SameSiteCookies.NONE); + Assert.assertEquals(SameSiteCookies.fromString("None"), SameSiteCookies.NONE); + Assert.assertEquals(SameSiteCookies.fromString("NONE"), SameSiteCookies.NONE); + + Assert.assertEquals(SameSiteCookies.fromString("lax"), SameSiteCookies.LAX); + Assert.assertEquals(SameSiteCookies.fromString("Lax"), SameSiteCookies.LAX); + Assert.assertEquals(SameSiteCookies.fromString("LAX"), SameSiteCookies.LAX); + + Assert.assertEquals(SameSiteCookies.fromString("strict"), SameSiteCookies.STRICT); + Assert.assertEquals(SameSiteCookies.fromString("Strict"), SameSiteCookies.STRICT); + Assert.assertEquals(SameSiteCookies.fromString("STRICT"), SameSiteCookies.STRICT); + } + + @Test(expected = IllegalStateException.class) + public void testToInvalidAttribute01() { + SameSiteCookies.fromString(""); + } + + @Test(expected = IllegalStateException.class) + public void testToInvalidAttribute02() { + SameSiteCookies.fromString(" "); + } + + @Test(expected = IllegalStateException.class) + public void testToInvalidAttribute03() { + SameSiteCookies.fromString("Strict1"); + } + + @Test(expected = IllegalStateException.class) + public void testToInvalidAttribute04() { + SameSiteCookies.fromString("foo"); + } + + @Test(expected = IllegalStateException.class) + public void testToInvalidAttribute05() { + SameSiteCookies.fromString("Lax "); + } +} \ No newline at end of file diff --git a/test/org/apache/tomcat/util/http/TesterCookiesPerformance.java b/test/org/apache/tomcat/util/http/TesterCookiesPerformance.java new file mode 100644 index 0000000..5504bde --- /dev/null +++ b/test/org/apache/tomcat/util/http/TesterCookiesPerformance.java @@ -0,0 +1,75 @@ +/* + * 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.tomcat.util.http; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.MessageBytes; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterCookiesPerformance { + + @Test + public void testPerformance01() throws Exception { + final int cookieCount = 100; + final int parsingLoops = 200000; + + MimeHeaders mimeHeaders = new MimeHeaders(); + + StringBuilder cookieHeader = new StringBuilder(); + // Create cookies + for (int i = 0; i < cookieCount; i++) { + cookieHeader.append("name"); + cookieHeader.append(i); + cookieHeader.append('='); + cookieHeader.append("value"); + cookieHeader.append(i); + cookieHeader.append(';'); + } + + byte[] cookieHeaderBytes = cookieHeader.toString().getBytes("UTF-8"); + + MessageBytes headerValue = mimeHeaders.addValue("Cookie"); + headerValue.setBytes(cookieHeaderBytes, 0, cookieHeaderBytes.length); + ServerCookies serverCookies = new ServerCookies(4); + + Rfc6265CookieProcessor rfc6265CookieProcessor = new Rfc6265CookieProcessor(); + + // warm up + for (int i = 0; i < parsingLoops; i++) { + rfc6265CookieProcessor.parseCookieHeader(mimeHeaders, serverCookies); + Assert.assertEquals(cookieCount, serverCookies.getCookieCount()); + serverCookies.recycle(); + } + + long newStart = System.nanoTime(); + for (int i = 0; i < parsingLoops; i++) { + rfc6265CookieProcessor.parseCookieHeader(mimeHeaders, serverCookies); + Assert.assertEquals(cookieCount, serverCookies.getCookieCount()); + serverCookies.recycle(); + } + long newDuration = System.nanoTime() - newStart; + + System.out.println("RFC6265 duration: " + newDuration); + + // As of November 2021 markt's desktop runs this test in 970ms to 1000ms + } +} diff --git a/test/org/apache/tomcat/util/http/TesterFastHttpDateFormatPerformance.java b/test/org/apache/tomcat/util/http/TesterFastHttpDateFormatPerformance.java new file mode 100644 index 0000000..4d6e579 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TesterFastHttpDateFormatPerformance.java @@ -0,0 +1,68 @@ +/* + * 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.tomcat.util.http; + +import org.junit.Test; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterFastHttpDateFormatPerformance { + + @Test + public void testGetCurrentDateConcurrent() throws InterruptedException { + int threadCount = 8; + int callCount = 100000000; + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new GetCurrentDateThread(callCount); + } + + long startTime = System.nanoTime(); + + for (int i = 0; i < threadCount; i++) { + threads[i].start(); + } + + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + + long endTime = System.nanoTime(); + + System.out.println("Duration: " + (endTime - startTime)); + } + + + private static class GetCurrentDateThread extends Thread { + + private final int callCount; + + GetCurrentDateThread(int callCount) { + this.callCount = callCount; + } + + @Override + public void run() { + for (int i = 0; i < callCount; i++) { + FastHttpDateFormat.getCurrentDate(); + } + } + } +} diff --git a/test/org/apache/tomcat/util/http/TesterParametersPerformance.java b/test/org/apache/tomcat/util/http/TesterParametersPerformance.java new file mode 100644 index 0000000..abcc47f --- /dev/null +++ b/test/org/apache/tomcat/util/http/TesterParametersPerformance.java @@ -0,0 +1,132 @@ +/* + * 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.tomcat.util.http; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.LogManager; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.B2CConverter; + +/* + * This is an absolute performance test. There is no benefit it running it as part of a standard test run so it is + * excluded due to the name starting Tester... + */ +public class TesterParametersPerformance { + + @Test + public void testProcessParametersByteArrayIntInt() { + LogManager.getLogManager().getLogger("").setLevel(Level.OFF); + doTestProcessParametersMultiple("foo".getBytes()); + } + + private void doTestProcessParametersMultiple(byte[] input) { + System.out.println(doTestProcessParameters(input, 10000)); + System.out.println(doTestProcessParameters(input, 20000)); + System.out.println(doTestProcessParameters(input, 40000)); + System.out.println(doTestProcessParameters(input, 80000)); + System.out.println(doTestProcessParameters(input, 160000)); + System.out.println(doTestProcessParameters(input, 320000)); + System.out.println(doTestProcessParameters(input, 640000)); + System.out.println(doTestProcessParameters(input, 1280000)); + } + + private long doTestProcessParameters(byte[] input, int size) { + Assert.assertEquals(input.length, 3); + + Parameters p = new Parameters(); + + byte[] params = createParams(input, size); + //byte[] input = createParams(8); + p.setCharset(StandardCharsets.ISO_8859_1); + long start = System.nanoTime(); + p.processParameters(params, 0, params.length); + return System.nanoTime() - start; + } + + private byte[] createParams(byte[] input, int len) { + byte[] result = new byte[len * 4 - 1]; + + for (int i = 0; i < len; i++) { + result[i * 4] = input[0]; + result[i * 4 + 1] = input[1]; + result[i * 4 + 2] = input[2]; + if (i < len -1) { + result[i * 4 + 3] = 38; + } + } + return result; + } + + @Test + public void testCreateString() throws UnsupportedEncodingException { + B2CConverter.getCharset("ISO-8859-1"); + doCreateStringMultiple("foo"); + } + + private void doCreateStringMultiple(String input) { + System.out.println(doCreateString(input, 10, true)); + System.out.println(doCreateString(input, 100, true)); + System.out.println(doCreateString(input, 1000, true)); + System.out.println(doCreateString(input, 10000, true)); + System.out.println(doCreateString(input, 100000, true)); + System.out.println(doCreateString(input, 1000000, true)); + System.out.println(doCreateString(input, 2000000, true)); + //System.out.println(doCreateString(input, 4000000, true)); + //System.out.println(doCreateString(input, 8000000, true)); + System.out.println(doCreateString(input, 10, false)); + System.out.println(doCreateString(input, 100, false)); + System.out.println(doCreateString(input, 1000, false)); + System.out.println(doCreateString(input, 10000, false)); + System.out.println(doCreateString(input, 100000, false)); + System.out.println(doCreateString(input, 1000000, false)); + System.out.println(doCreateString(input, 2000000, false)); + //System.out.println(doCreateString(input, 4000000, false)); + //System.out.println(doCreateString(input, 8000000, false)); + } + + private long doCreateString(String input, int size, + boolean defensiveCopyWorkAround) { + int loops = 10000; + byte[] inputBytes = input.getBytes(); + byte[] bytes = new byte[size]; + int inputLength = inputBytes.length; + + System.arraycopy(inputBytes, 0, bytes, 0, inputLength); + + String[] result = new String[loops]; + Charset charset = StandardCharsets.ISO_8859_1; + + long start = System.nanoTime(); + for (int i = 0; i < loops; i++) { + if (defensiveCopyWorkAround) { + byte[] tmp = new byte[inputLength]; + System.arraycopy(bytes, 0, tmp, 0, inputLength); + result[i] = new String(tmp, 0, inputLength, charset); + } else { + result[i] = new String(bytes, 0, inputLength, charset); + } + } + + return System.nanoTime() - start; + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestAcceptLanguage.java b/test/org/apache/tomcat/util/http/parser/TestAcceptLanguage.java new file mode 100644 index 0000000..93df636 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestAcceptLanguage.java @@ -0,0 +1,358 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.StringReader; +import java.util.List; +import java.util.Locale; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAcceptLanguage { + + private static final Locale L_EN = Locale.forLanguageTag("en"); + private static final Locale L_EN_GB = Locale.forLanguageTag("en-gb"); + private static final Locale L_FR = Locale.forLanguageTag("fr"); + private static final double Q1_000 = 1; + private static final double Q0_500 = 0.5; + private static final double Q0_050 = 0.05; + + @Test + public void testSingle01() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle02() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle03() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle04() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb; ")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle05() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=1")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle06() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb; q=1")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle07() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb; q= 1")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle08() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb; q = 1")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle09() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb; q = 1 ")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle10() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb; q =\t1")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle11() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb; q =1\t")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle12() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb; q =\t1\t")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle13() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=0.5")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle14() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=0.50")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle15() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=0.500")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle16() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=0.5009")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testSingle17() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;,")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + + + @Test + public void testMalformed01() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;x=1,en-gb;q=0.5")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed02() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=a,en-gb;q=0.5")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed03() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=0.5a,en-gb;q=0.5")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed04() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=0.05a,en-gb;q=0.5")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed05() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=0.005a,en-gb;q=0.5")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed06() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=0.00005a,en-gb;q=0.5")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed07() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en,,")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed08() throws Exception { + List actual = AcceptLanguage.parse(new StringReader(",en,")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed09() throws Exception { + List actual = AcceptLanguage.parse(new StringReader(",,en")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + } + + @Test + public void testMalformed10() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en;q")); + + Assert.assertEquals(0, actual.size()); + } + + @Test + public void testMalformed11() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=1a0")); + + Assert.assertEquals(0, actual.size()); + } + + @Test + public void testMalformed12() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=1.a0")); + + Assert.assertEquals(0, actual.size()); + } + + @Test + public void testMalformed13() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=1.0a0")); + + Assert.assertEquals(0, actual.size()); + } + + @Test + public void testMalformed14() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=1.1")); + + Assert.assertEquals(0, actual.size()); + } + + @Test + public void testMalformed15() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en-gb;q=1a0,en-gb;q=0.5")); + + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(L_EN_GB, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + } + + + @Test + public void testMultiple01() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en,fr")); + + Assert.assertEquals(2, actual.size()); + Assert.assertEquals(L_EN, actual.get(0).getLocale()); + Assert.assertEquals(Q1_000, actual.get(0).getQuality(), 0.0001); + Assert.assertEquals(L_FR, actual.get(1).getLocale()); + Assert.assertEquals(Q1_000, actual.get(1).getQuality(), 0.0001); + } + + @Test + public void testMultiple02() throws Exception { + List actual = AcceptLanguage.parse(new StringReader("en; q= 0.05,fr;q=0.5")); + + Assert.assertEquals(2, actual.size()); + Assert.assertEquals(L_EN, actual.get(0).getLocale()); + Assert.assertEquals(Q0_050, actual.get(0).getQuality(), 0.0001); + Assert.assertEquals(L_FR, actual.get(1).getLocale()); + Assert.assertEquals(Q0_500, actual.get(1).getQuality(), 0.0001); + } + + + @Test + public void bug56848() throws Exception { + List actual = + AcceptLanguage.parse(new StringReader("zh-hant-CN;q=0.5,zh-hans-TW;q=0.05")); + + Assert.assertEquals(2, actual.size()); + + Locale.Builder b = new Locale.Builder(); + b.setLanguage("zh").setRegion("CN").setScript("hant"); + Locale l1 = b.build(); + + b.clear().setLanguage("zh").setRegion("TW").setScript("hans"); + Locale l2 = b.build(); + + Assert.assertEquals(l1, actual.get(0).getLocale()); + Assert.assertEquals(Q0_500, actual.get(0).getQuality(), 0.0001); + Assert.assertEquals(l2, actual.get(1).getLocale()); + Assert.assertEquals(Q0_050, actual.get(1).getQuality(), 0.0001); + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestAuthorizationDigest.java b/test/org/apache/tomcat/util/http/parser/TestAuthorizationDigest.java new file mode 100644 index 0000000..9436f33 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestAuthorizationDigest.java @@ -0,0 +1,515 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.StringReader; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class TestAuthorizationDigest { + + @Test + public void testBug54060a() throws Exception { + String header = "Digest username=\"mthornton\", " + + "realm=\"optrak.com\", " + + "nonce=\"1351427243671:c1d6360150712149bae931a3ed7cb498\", " + + "uri=\"/files/junk.txt\", " + + "response=\"c5c2410bfc46753e83a8f007888b0d2e\", " + + "opaque=\"DB85C1A73933A7EB586D10E4BF2924EF\", " + + "qop=auth, " + + "nc=00000001, " + + "cnonce=\"9926cb3c334ede11\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("mthornton", result.get("username")); + Assert.assertEquals("optrak.com", result.get("realm")); + Assert.assertEquals("1351427243671:c1d6360150712149bae931a3ed7cb498", + result.get("nonce")); + Assert.assertEquals("/files/junk.txt", result.get("uri")); + Assert.assertEquals("c5c2410bfc46753e83a8f007888b0d2e", + result.get("response")); + Assert.assertEquals("DB85C1A73933A7EB586D10E4BF2924EF", + result.get("opaque")); + Assert.assertEquals("auth", result.get("qop")); + Assert.assertEquals("00000001", result.get("nc")); + Assert.assertEquals("9926cb3c334ede11", result.get("cnonce")); + } + + @Test + public void testBug54060b() throws Exception { + String header = "Digest username=\"mthornton\", " + + "realm=\"optrak.com\", " + + "nonce=\"1351427480964:a01c16fed5168d72a2b5267395a2022e\", " + + "uri=\"/files\", " + + "algorithm=MD5, " + + "response=\"f310c44b87efc0bc0a7aab7096fd36b6\", " + + "opaque=\"DB85C1A73933A7EB586D10E4BF2924EF\", " + + "cnonce=\"MHg3ZjA3ZGMwMTUwMTA6NzI2OToxMzUxNDI3NDgw\", " + + "nc=00000001, " + + "qop=auth"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("mthornton", result.get("username")); + Assert.assertEquals("optrak.com", result.get("realm")); + Assert.assertEquals("1351427480964:a01c16fed5168d72a2b5267395a2022e", + result.get("nonce")); + Assert.assertEquals("/files", result.get("uri")); + Assert.assertEquals("MD5", result.get("algorithm")); + Assert.assertEquals("f310c44b87efc0bc0a7aab7096fd36b6", + result.get("response")); + Assert.assertEquals("DB85C1A73933A7EB586D10E4BF2924EF", + result.get("opaque")); + Assert.assertEquals("MHg3ZjA3ZGMwMTUwMTA6NzI2OToxMzUxNDI3NDgw", + result.get("cnonce")); + Assert.assertEquals("00000001", result.get("nc")); + Assert.assertEquals("auth", result.get("qop")); + } + + @Test + public void testBug54060c() throws Exception { + String header = "Digest username=\"mthornton\", qop=auth"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("mthornton", result.get("username")); + Assert.assertEquals("auth", result.get("qop")); + } + + @Test + public void testBug54060d() throws Exception { + String header = "Digest username=\"mthornton\"," + + "qop=auth," + + "cnonce=\"9926cb3c334ede11\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("mthornton", result.get("username")); + Assert.assertEquals("auth", result.get("qop")); + Assert.assertEquals("9926cb3c334ede11", result.get("cnonce")); + } + + @Test + public void testEndWithLhex() throws Exception { + String header = "Digest nc=00000001"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("00000001", result.get("nc")); + } + + @Test + public void testEndWithLhexReverse() throws Exception { + String header = "Digest nc=10000000"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("10000000", result.get("nc")); + } + + @Test + public void testQuotedLhex() throws Exception { + String header = "Digest nc=\"09abcdef\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("09abcdef", result.get("nc")); + } + + @Test + public void testQuotedLhexReverse() throws Exception { + String header = "Digest nc=\"fedcba90\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("fedcba90", result.get("nc")); + } + + @Test + public void testLhex() throws Exception { + String header = "Digest nc=09abcdef"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("09abcdef", result.get("nc")); + } + + @Test + public void testLhexReverse() throws Exception { + String header = "Digest nc=fedcba90"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("fedcba90", result.get("nc")); + } + + @Test + public void testQuotedLhexUppercase() throws Exception { + String header = "Digest nc=\"00ABCDEF\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("00abcdef", result.get("nc")); + } + + @Test + public void testQuotedLhexUppercaseReverse() throws Exception { + String header = "Digest nc=\"FEDCBA00\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("fedcba00", result.get("nc")); + } + + @Test + public void testLhexUppercase() throws Exception { + String header = "Digest nc=00ABCDEF"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("00abcdef", result.get("nc")); + } + + @Test + public void testLhexUppercaseReverse() throws Exception { + String header = "Digest nc=FEDCBA00"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertEquals("fedcba00", result.get("nc")); + } + + @Test + public void testUnclosedQuotedLhex() throws Exception { + String header = "Digest nc=\"00000001"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertNull(result); + } + + @Test + public void testEmptyLhex() throws Exception { + String header = "Digest nc="; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertNull(result); + } + + @Test + public void testQuotedEmptyLhex() throws Exception { + String header = "Digest nc=\"\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + + Assert.assertNull(result); + } + + @Test + public void testUnclosedQuotedString1() throws Exception { + String header = "Digest username=\"test"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testUnclosedQuotedString2() throws Exception { + String header = "Digest username=\"test\\"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testNonTokenDirective1() throws Exception { + String header = "Digest user{name=\"test\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testNonTokenDirective2() throws Exception { + String header = "Digest a=b,{name=test"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testTokenQop() throws Exception { + String header = "Digest qop=auth"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertEquals("auth", result.get("qop")); + } + + @Test + public void testQuotedTokenQop() throws Exception { + String header = "Digest qop=\"auth\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertEquals("auth", result.get("qop")); + } + + @Test + public void testEmptyQop() throws Exception { + String header = "Digest qop="; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testEmptyQuotedTokenQop() throws Exception { + String header = "Digest qop=\"\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testNonTokenQop01() throws Exception { + String header = "Digest qop=au{th"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testNonTokenQop02() throws Exception { + String header = "Digest qop=auth{"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testQuotedNonTokenQop() throws Exception { + String header = "Digest qop=\"au{th\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testQuotedNonTokenQop2() throws Exception { + String header = "Digest qop=\"{auth\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testUnclosedQuotedTokenQop() throws Exception { + String header = "Digest qop=\"auth"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testWrongCharacterInToken() throws Exception { + String header = "Digest \u044f"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testWrongCharacterInToken2() throws Exception { + String header = "Digest qop=\u044f"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testWrongCharacterInQuotedToken() throws Exception { + String header = "Digest qop=\"\u044f\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testWrongCharacterInHex01() throws Exception { + String header = "Digest nc=\u044f"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testWrongCharacterInHex02() throws Exception { + String header = "Digest nc=aaa\u044f"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testWrongCharacterInHex03() throws Exception { + String header = "Digest nc=\u044faaa"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testWrongCharacterInQuotedHex() throws Exception { + String header = "Digest nc=\"\u044f\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testParseAuthParamA() throws Exception { + // Test for HttpParser.readTokenOrQuotedString() + // auth-param = token "=" ( token | quoted-string ) + String header = "Digest a=b"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertEquals("b", result.get("a")); + } + + @Test + public void testParseAuthParamB() throws Exception { + // Test for HttpParser.readTokenOrQuotedString() + // auth-param = token "=" ( token | quoted-string ) + String header = "Digest a=\"b\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertEquals("b", result.get("a")); + } + + @Test + public void testParseAuthParamBEscaped() throws Exception { + // Test for HttpParser.readTokenOrQuotedString() + // auth-param = token "=" ( token | quoted-string ) + String header = "Digest a=\"b\\\"b\""; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertEquals("b\"b", result.get("a")); + } + + @Test + public void testQuotedStringNoQuotes() throws Exception { + String header = "Digest username=a"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } + + @Test + public void testNotDigest() throws Exception { + String header = "SomethingElse a=b"; + + StringReader input = new StringReader(header); + + Map result = Authorization.parseAuthorizationDigest(input); + Assert.assertNull(result); + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestHttpParser.java b/test/org/apache/tomcat/util/http/parser/TestHttpParser.java new file mode 100644 index 0000000..618b520 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestHttpParser.java @@ -0,0 +1,189 @@ +/* + * 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.tomcat.util.http.parser; + +import org.junit.Assert; +import org.junit.Test; + +public class TestHttpParser { + + @Test + public void testTokenDel() { + Assert.assertFalse("DEL is not a token", HttpParser.isToken(127)); + } + + + @Test + public void testAbsolutePathRelaxedLargeInvalid() { + HttpParser httpParser = new HttpParser(null, null); + Assert.assertFalse(httpParser.isAbsolutePathRelaxed(Integer.MAX_VALUE)); + } + + + @Test + public void testAbsolutePathRelaxed01() { + HttpParser httpParser = new HttpParser(null, null); + Assert.assertFalse(httpParser.isAbsolutePathRelaxed('{')); + } + + + @Test + public void testAbsolutePathRelaxed02() { + HttpParser httpParser = new HttpParser("{", null); + Assert.assertTrue(httpParser.isAbsolutePathRelaxed('{')); + } + + + @Test + public void testAbsolutePathRelaxed03() { + HttpParser httpParser = new HttpParser(null, "{"); + Assert.assertFalse(httpParser.isAbsolutePathRelaxed('{')); + } + + + @Test + public void testAbsolutePathRelaxed04() { + HttpParser httpParser = new HttpParser("\u1000", null); + Assert.assertFalse(httpParser.isAbsolutePathRelaxed('{')); + } + + + @Test + public void testAbsolutePathRelaxed05() { + HttpParser httpParser = new HttpParser("", null); + Assert.assertFalse(httpParser.isAbsolutePathRelaxed('{')); + } + + + @Test + public void testQueryRelaxedLargeInvalid() { + HttpParser httpParser = new HttpParser(null, null); + Assert.assertFalse(httpParser.isQueryRelaxed(Integer.MAX_VALUE)); + } + + + @Test + public void testRequestTargetLargeInvalid() { + Assert.assertTrue(HttpParser.isNotRequestTarget(Integer.MAX_VALUE)); + } + + + @Test + public void testHttpProtocolLargeInvalid() { + Assert.assertFalse(HttpParser.isHttpProtocol(Integer.MAX_VALUE)); + } + + + @Test + public void testUserInfoLargeInvalid() { + Assert.assertFalse(HttpParser.isUserInfo(Integer.MAX_VALUE)); + } + + + @Test + public void testAbsolutePathLargeInvalid() { + Assert.assertFalse(HttpParser.isAbsolutePath(Integer.MAX_VALUE)); + } + + + @Test + public void testQueryLargeInvalid() { + Assert.assertFalse(HttpParser.isQuery(Integer.MAX_VALUE)); + } + + + @Test + public void testUnquoteNull() { + Assert.assertNull(HttpParser.unquote(null)); + } + + + @Test + public void testUnquoteShort() { + String shortText = "a"; + Assert.assertEquals(shortText, HttpParser.unquote(shortText)); + } + + + @Test + public void testUnquoteUnquoted() { + String shortText = "abcde"; + Assert.assertEquals(shortText, HttpParser.unquote(shortText)); + } + + + @Test + public void testUnquoteEscaped() { + // Note: Test string is also Java escaped + String shortText = "\"ab\\\"ab\""; + String result = "ab\"ab"; + Assert.assertEquals(result, HttpParser.unquote(shortText)); + } + + + @Test + public void testUnquoteUnquotedEscaped() { + // Note: Test string is also Java escaped + String shortText = "ab\\\"ab"; + String result = "ab\"ab"; + Assert.assertEquals(result, HttpParser.unquote(shortText)); + } + + + @Test + public void testUnquoteInvalid01() { + // Note: Test string is also Java escaped + String shortText = "aaa\\"; + Assert.assertNull(shortText, HttpParser.unquote(shortText)); + } + + + @Test + public void testTokenStringNull() { + Assert.assertFalse(HttpParser.isToken(null)); + } + + + @Test + public void testTokenStringEmpty() { + Assert.assertFalse(HttpParser.isToken("")); + } + + + @Test + public void testTokenStringLws01() { + Assert.assertFalse(HttpParser.isToken(" ")); + } + + + @Test + public void testTokenStringLws02() { + Assert.assertFalse(HttpParser.isToken(" aaa")); + } + + + @Test + public void testTokenStringLws03() { + Assert.assertFalse(HttpParser.isToken("\taaa")); + } + + + @Test + public void testTokenStringValid() { + Assert.assertTrue(HttpParser.isToken("token")); + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestHttpParserHost.java b/test/org/apache/tomcat/util/http/parser/TestHttpParserHost.java new file mode 100644 index 0000000..fa6e063 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestHttpParserHost.java @@ -0,0 +1,286 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TestHttpParserHost { + + private static final Class IAE = IllegalArgumentException.class; + + @Parameter(0) + public TestType testType; + + @Parameter(1) + public String input; + + @Parameter(2) + public Integer expectedResult; + + @Parameter(3) + public Class expectedException; + + + @Parameters(name="{index}: host {1}") + public static Collection inputs() { + List result = new ArrayList<>(); + // IPv4 - valid + result.add(new Object[] { TestType.IPv4, "127.0.0.1", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "127.0.0.1:8080", Integer.valueOf(9), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.0:8080", Integer.valueOf(7), null} ); + // IPv4 - invalid + result.add(new Object[] { TestType.IPv4, ".0.0.0", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "0..0.0", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "0]", Integer.valueOf(-1), IAE} ); + // Domain Name - valid + result.add(new Object[] { TestType.IPv4, "0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0:8080", Integer.valueOf(3), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0:8080", Integer.valueOf(5), null} ); + result.add(new Object[] { TestType.IPv4, "0.00.0.0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.00.0.0:8080", Integer.valueOf(8), null} ); + result.add(new Object[] { TestType.IPv4, "256.0.0.0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "256.0.0.0:8080", Integer.valueOf(9), null} ); + result.add(new Object[] { TestType.IPv4, "0.256.0.0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.256.0.0:8080", Integer.valueOf(9), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.256.0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.256.0:8080", Integer.valueOf(9), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.256", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.256:8080", Integer.valueOf(9), null} ); + result.add(new Object[] { TestType.IPv4, "0.a.0.0", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.a.0.0:8080", Integer.valueOf(7), null} ); + result.add(new Object[] { TestType.IPv4, "localhost", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "localhost:8080", Integer.valueOf(9), null} ); + result.add(new Object[] { TestType.IPv4, "4294967295.localhost", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "4294967295.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "tomcat.apache.org", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "tomcat.apache.org:8080", Integer.valueOf(17), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.com:8080", Integer.valueOf(9), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.0.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.0.com:8080", Integer.valueOf(11), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.com:8080", Integer.valueOf(11), null} ); + result.add(new Object[] { TestType.IPv4, "1foo.0.0.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "1foo.0.0.com:8080", Integer.valueOf(12), null} ); + result.add(new Object[] { TestType.IPv4, "foo1.0.0.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo1.0.0.com:8080", Integer.valueOf(12), null} ); + result.add(new Object[] { TestType.IPv4, "1foo1.0.0.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "1foo1.0.0.com:8080", Integer.valueOf(13), null} ); + result.add(new Object[] { TestType.IPv4, "1-foo.0.0.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "1-foo.0.0.com:8080", Integer.valueOf(13), null} ); + result.add(new Object[] { TestType.IPv4, "1--foo.0.0.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "1--foo.0.0.com:8080", Integer.valueOf(14), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.1com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.1com:8080", Integer.valueOf(12), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.com1", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.com1:8080", Integer.valueOf(12), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.1com1", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.1com1:8080", Integer.valueOf(13), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.1-com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.1-com:8080", Integer.valueOf(13), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.1--com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo.0.0.1--com:8080", Integer.valueOf(14), null} ); + result.add(new Object[] { TestType.IPv4, "com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "com:8080", Integer.valueOf(3), null} ); + result.add(new Object[] { TestType.IPv4, "0com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0com:8080", Integer.valueOf(4), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0com:8080", Integer.valueOf(8), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.0com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "0.0.0.0com:8080", Integer.valueOf(10), null} ); + result.add(new Object[] { TestType.IPv4, "123", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "123:8080", Integer.valueOf(3), null} ); + result.add(new Object[] { TestType.IPv4, "foo.bar.0com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo.bar.0com:8080", Integer.valueOf(12), null} ); + result.add(new Object[] { TestType.IPv4, "myapp-t.mydomain.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "myapp-t.mydomain.com:8080", Integer.valueOf(20), null} ); + result.add(new Object[] { TestType.IPv4, "myapp-t.my-domain.com", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "myapp-t.my-domain.com:8080", Integer.valueOf(21), null} ); + result.add(new Object[] { TestType.IPv4, "myapp-t.my-domain.c-om", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "myapp-t.my-domain.c-om:8080", Integer.valueOf(22), null} ); + result.add(new Object[] { TestType.IPv4, "gateway.demo-ilt-latest-demo:9000", Integer.valueOf(28), null} ); + // Domain Name with trailing dot - valid + result.add(new Object[] { TestType.IPv4, "0.0.0.", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "myapp-t.mydomain.com.", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "myapp-t.mydomain.com.:8080", Integer.valueOf(21), null} ); + result.add(new Object[] { TestType.IPv4, "foo.bar.", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv4, "foo.bar.:8080", Integer.valueOf(8), null} ); + // Domain Name - invalid + result.add(new Object[] { TestType.IPv4, ".", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, ".:8080", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, ".foo.bar", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "-foo.bar", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "foo.bar-", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "foo.bar-:8080", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "^foo.bar", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "foo-.bar", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "f*oo.bar", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "foo..bar", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "foo.-bar", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "foo.^bar", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "foo.b*ar", Integer.valueOf(-1), IAE} ); + // IPv6 - valid + result.add(new Object[] { TestType.IPv6, "[::1]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[::1]:8080", Integer.valueOf(5), null} ); + result.add(new Object[] { TestType.IPv6, "[1::1]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[1::1]:8080", Integer.valueOf(6), null} ); + result.add(new Object[] { TestType.IPv6, "[A::A]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[A::A]:8080", Integer.valueOf(6), null} ); + result.add(new Object[] { TestType.IPv6, "[A:0::A]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[A:0::A]:8080", Integer.valueOf(8), null} ); + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:5678:90AB:CDEF]", + Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:5678:90AB:CDEF]:8080", + Integer.valueOf(41), null} ); + result.add(new Object[] { TestType.IPv6, "[::5678:90AB:CDEF:1234:5678:90AB:CDEF]:8080", + Integer.valueOf(38), null} ); + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:5678:90AB::]:8080", + Integer.valueOf(38), null} ); + result.add(new Object[] { TestType.IPv6, "[0:0:0:0:0:0:0:0]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[0:0:0:0:0:0:0:0]:8080", + Integer.valueOf(17), null} ); + result.add(new Object[] { TestType.IPv6, "[::127.0.0.1]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[::127.0.0.1]:8080", Integer.valueOf(13), null} ); + result.add(new Object[] { TestType.IPv6, "[1::127.0.0.1]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[1::127.0.0.1]:8080", Integer.valueOf(14), null} ); + result.add(new Object[] { TestType.IPv6, "[A::127.0.0.1]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[A::127.0.0.1]:8080", Integer.valueOf(14), null} ); + result.add(new Object[] { TestType.IPv6, "[A:0::127.0.0.1]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[A:0::127.0.0.1]:8080", Integer.valueOf(16), null} ); + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:5678:127.0.0.1]", + Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:5678:127.0.0.1]:8080", + Integer.valueOf(41), null} ); + result.add(new Object[] { TestType.IPv6, "[::5678:90AB:CDEF:1234:5678:127.0.0.1]:8080", + Integer.valueOf(38), null} ); + result.add(new Object[] { TestType.IPv6, "[0:0:0:0:0:0:127.0.0.1]", Integer.valueOf(-1), null} ); + result.add(new Object[] { TestType.IPv6, "[0:0:0:0:0:0:127.0.0.1]:8080", + Integer.valueOf(23), null} ); + result.add(new Object[] { TestType.IPv6, "[::1.2.3.4]", Integer.valueOf(-1), null} ); + // IPv6 - invalid + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:127.0.0.1]", + Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:5678:127.0.0.1", + Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[0::0::0]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[0:0:G:0:0:0:0:0]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[00000:0:0:0:0:0:0:0]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:5678:90AB:]", + Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1234:5678:90AB:CDEF:1234:5678:90AB:CDEF", + Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[::127.00.0.1]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[0::0::127.0.0.1]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[0:0:G:0:0:0:127.0.0.1]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[00000:0:0:0:0:0:127.0.0.1]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1::127..0.1]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1::127..0.1]:8080", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1::127.a.0.1]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1::127.a.0.1]:8080", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1::127.-.0.1]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1::127.-.0.1]:8080", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[::1]'", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[:2222:3333:4444:5555:6666:7777:8888]", + Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1111:::3333:4444:5555:6666:7777:8888]", + Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "::1]", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1111:2222:3333:4444:5555:6666:7777:8888:9999]", + Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1111:2222:3333:4444:5555:6666:7777:1.2.3.4]", + Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv6, "[1111:2222:3333]", + Integer.valueOf(-1), IAE} ); + // Domain name - invalid port + result.add(new Object[] { TestType.IPv4, "localhost:x", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "localhost:-1", Integer.valueOf(-1), IAE} ); + // IPv4 - invalid port + result.add(new Object[] { TestType.IPv4, "127.0.0.1:x", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "127.0.0.1:-1", Integer.valueOf(-1), IAE} ); + // IPv6 - invalid port + result.add(new Object[] { TestType.IPv4, "[::1]:x", Integer.valueOf(-1), IAE} ); + result.add(new Object[] { TestType.IPv4, "[::1]:-1", Integer.valueOf(-1), IAE} ); + return result; + } + + + @Test + public void testHost() { + Class exceptionClass = null; + int result = -1; + try { + result = Host.parse(input); + } catch (Exception e) { + exceptionClass = e.getClass(); + } + Assert.assertEquals(input, expectedResult.intValue(), result); + if (expectedException == null) { + Assert.assertNull(input, exceptionClass); + } else { + Assert.assertNotNull(exceptionClass); + Assert.assertTrue(input, expectedException.isAssignableFrom(exceptionClass)); + } + } + + + @Test + public void testHostType() { + Class exceptionClass = null; + int result = -1; + try { + StringReader sr = new StringReader(input); + switch(testType) { + case IPv4: + result = HttpParser.readHostIPv4(sr, false); + break; + case IPv6: + result = HttpParser.readHostIPv6(sr); + break; + + } + } catch (Exception e) { + exceptionClass = e.getClass(); + } + Assert.assertEquals(input, expectedResult.intValue(), result); + if (expectedException == null) { + Assert.assertNull(input, exceptionClass); + } else { + Assert.assertNotNull(exceptionClass); + Assert.assertTrue(input, expectedException.isAssignableFrom(exceptionClass)); + } + } + + + private enum TestType { + IPv4, + IPv6 + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestMediaType.java b/test/org/apache/tomcat/util/http/parser/TestMediaType.java new file mode 100644 index 0000000..9a38d19 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestMediaType.java @@ -0,0 +1,327 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for {@link HttpParser} focusing on media-type as defined in + * section 3.7 of RFC 2616. + */ +public class TestMediaType { + + // Include whitespace to ensure Parser handles it correctly (it should be + // skipped). + private static final String TYPE = " foo "; + private static final String SUBTYPE = " bar "; + private static final String TYPES = TYPE + "/" + SUBTYPE; + + private static final Parameter PARAM_TOKEN = + new Parameter("a", "b"); + private static final Parameter PARAM_ESCAPED = + new Parameter("v", "\"w\\\"w\""); + private static final Parameter PARAM_QUOTED = + new Parameter("x", "\"y\""); + private static final Parameter PARAM_EMPTY_QUOTED = + new Parameter("z", "\"\""); + private static final Parameter PARAM_COMPLEX_QUOTED = + new Parameter("w", "\"foo'bar,a=b;x=y\""); + + private static final String CHARSET = "UTF-8"; + private static final String WS_CHARSET = " \tUTF-8"; + private static final String CHARSET_WS = "UTF-8 \t"; + // Since this is quoted, it should retain the space at the end + private static final String CHARSET_QUOTED = "\"" + CHARSET_WS + "\""; + private static final Parameter PARAM_CHARSET = + new Parameter("charset", CHARSET); + private static final Parameter PARAM_WS_CHARSET = + new Parameter("charset", WS_CHARSET); + private static final Parameter PARAM_CHARSET_WS = + new Parameter("charset", CHARSET_WS); + private static final Parameter PARAM_CHARSET_QUOTED = + new Parameter("charset", CHARSET_QUOTED); + + + private static final String[] LWS_VALUES = new String[] { + "", " ", "\t", "\r", "\n", "\r\n", " \r", " \n", " \r\n", + "\r ", "\n ", "\r\n ", " \r ", " \n ", " \r\n " }; + + + @Test + public void testSimple() throws IOException { + doTest(); + } + + + @Test + public void testSimpleWithToken() throws IOException { + doTest(PARAM_TOKEN); + } + + + @Test + public void testSimpleWithEscapedString() throws IOException { + doTest(PARAM_ESCAPED); + } + + + @Test + public void testSimpleWithQuotedString() throws IOException { + doTest(PARAM_QUOTED); + } + + + @Test + public void testSimpleWithEmptyQuotedString() throws IOException { + doTest(PARAM_EMPTY_QUOTED); + } + + + @Test + public void testSimpleWithComplexQuotedString() throws IOException { + doTest(PARAM_COMPLEX_QUOTED); + } + + + @Test + public void testSimpleWithCharset() throws IOException { + doTest(PARAM_CHARSET); + } + + + @Test + public void testSimpleWithCharsetWhitespaceBefore() throws IOException { + doTest(PARAM_WS_CHARSET); + } + + + @Test + public void testSimpleWithCharsetWhitespaceAfter() throws IOException { + doTest(PARAM_CHARSET_WS); + } + + + @Test + public void testSimpleWithCharsetQuoted() throws IOException { + doTest(PARAM_CHARSET_QUOTED); + } + + + @Test + public void testSimpleWithAll() throws IOException { + doTest(PARAM_COMPLEX_QUOTED, PARAM_EMPTY_QUOTED, PARAM_QUOTED, + PARAM_TOKEN, PARAM_CHARSET); + } + + + @Test + public void testCharset() throws IOException { + StringBuilder sb = new StringBuilder(); + sb.append(TYPES); + sb.append(PARAM_CHARSET); + sb.append(PARAM_TOKEN); + + StringReader sr = new StringReader(sb.toString()); + MediaType m = MediaType.parseMediaType(sr); + + Assert.assertEquals("foo/bar;charset=UTF-8;a=b", m.toString()); + Assert.assertEquals(CHARSET, m.getCharset()); + Assert.assertEquals("foo/bar;a=b", m.toStringNoCharset()); + } + + + @Test + public void testCharsetQuoted() throws IOException { + StringBuilder sb = new StringBuilder(); + sb.append(TYPES); + sb.append(PARAM_CHARSET_QUOTED); + + StringReader sr = new StringReader(sb.toString()); + MediaType m = MediaType.parseMediaType(sr); + + Assert.assertEquals(CHARSET_WS, m.getCharset()); + Assert.assertEquals(TYPES.replace(" ", ""), + m.toStringNoCharset()); + } + + + @Test + public void testBug52811() throws IOException { + String input = "multipart/related;boundary=1_4F50BD36_CDF8C28;" + + "Start=\"<31671603.smil>\";" + + "Type=\"application/smil;charset=UTF-8\""; + + StringReader sr = new StringReader(input); + MediaType m = MediaType.parseMediaType(sr); + + // Check the types + Assert.assertEquals("multipart", m.getType()); + Assert.assertEquals("related", m.getSubtype()); + + // Check the parameters + Assert.assertTrue(m.getParameterCount() == 3); + + Assert.assertEquals("1_4F50BD36_CDF8C28", m.getParameterValue("boundary")); + Assert.assertEquals("\"<31671603.smil>\"", m.getParameterValue("Start")); + Assert.assertEquals("\"application/smil;charset=UTF-8\"", + m.getParameterValue("Type")); + + String expected = "multipart/related;boundary=1_4F50BD36_CDF8C28;" + + "start=\"<31671603.smil>\";" + + "type=\"application/smil;charset=UTF-8\""; + Assert.assertEquals(expected, m.toString()); + Assert.assertEquals(expected, m.toStringNoCharset()); + Assert.assertNull(m.getCharset()); + } + + + @Test + public void testBug53353() throws IOException { + String input = "text/html; UTF-8;charset=UTF-8"; + + StringReader sr = new StringReader(input); + MediaType m = MediaType.parseMediaType(sr); + + // Check the types + Assert.assertEquals("text", m.getType()); + Assert.assertEquals("html", m.getSubtype()); + + // Check the parameters + Assert.assertTrue(m.getParameterCount() == 2); + + Assert.assertEquals("", m.getParameterValue("UTF-8")); + Assert.assertEquals("UTF-8", m.getCharset()); + + // Note: Invalid input is filtered out + Assert.assertEquals("text/html;charset=UTF-8", m.toString()); + Assert.assertEquals("UTF-8", m.getCharset()); + } + + + @Test + public void testBug55454() throws IOException { + String input = "text/html;;charset=UTF-8"; + + StringReader sr = new StringReader(input); + MediaType m = MediaType.parseMediaType(sr); + + Assert.assertEquals("text", m.getType()); + Assert.assertEquals("html", m.getSubtype()); + + Assert.assertTrue(m.getParameterCount() == 1); + + Assert.assertEquals("UTF-8", m.getParameterValue("charset")); + Assert.assertEquals("UTF-8", m.getCharset()); + + Assert.assertEquals("text/html;charset=UTF-8", m.toString()); + } + + + private void doTest(Parameter... parameters) throws IOException { + for (String lws : LWS_VALUES) { + doTest(lws, parameters); + } + } + + private void doTest(String lws, Parameter... parameters) + throws IOException { + StringBuilder sb = new StringBuilder(); + sb.append(TYPES); + for (Parameter p : parameters) { + sb.append(p.toString(lws)); + } + + StringReader sr = new StringReader(sb.toString()); + MediaType m = MediaType.parseMediaType(sr); + + // Check all expected parameters are present + Assert.assertTrue(m.getParameterCount() == parameters.length); + + // Check the types + Assert.assertEquals(TYPE.trim(), m.getType()); + Assert.assertEquals(SUBTYPE.trim(), m.getSubtype()); + + // Check the parameters + for (Parameter parameter : parameters) { + Assert.assertEquals(parameter.getValue().trim(), + m.getParameterValue(parameter.getName().trim())); + } + } + + + private static class Parameter { + private final String name; + private final String value; + + Parameter(String name,String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return toString(""); + } + + public String toString(String lws) { + StringBuilder sb = new StringBuilder(); + sb.append(lws); + sb.append(';'); + sb.append(lws); + sb.append(name); + sb.append(lws); + sb.append('='); + sb.append(lws); + sb.append(value); + sb.append(lws); + return sb.toString(); + } + } + + @Test + public void testCase() throws Exception { + StringReader sr = new StringReader("type/sub-type;a=1;B=2"); + MediaType m = MediaType.parseMediaType(sr); + + Assert.assertEquals("1", m.getParameterValue("A")); + Assert.assertEquals("1", m.getParameterValue("a")); + Assert.assertEquals("2", m.getParameterValue("B")); + Assert.assertEquals("2", m.getParameterValue("b")); + } + + @Test + public void testEmptyParameter() throws Exception { + // RFC 9110 + StringReader sr = new StringReader("type/sub-type;;a=1;;b=2;;"); + MediaType m = MediaType.parseMediaType(sr); + + Assert.assertEquals("1", m.getParameterValue("a")); + Assert.assertEquals("2", m.getParameterValue("b")); + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestPriority.java b/test/org/apache/tomcat/util/http/parser/TestPriority.java new file mode 100644 index 0000000..eeac940 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestPriority.java @@ -0,0 +1,37 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.Reader; +import java.io.StringReader; + +import org.junit.Assert; +import org.junit.Test; + +public class TestPriority { + + @Test + public void testOnlyIncremental() throws Exception { + String input = "i"; + Reader reader = new StringReader(input); + + Priority p = Priority.parsePriority(reader); + + Assert.assertEquals(Priority.DEFAULT_URGENCY, p.getUrgency()); + Assert.assertTrue(p.getIncremental()); + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestRanges.java b/test/org/apache/tomcat/util/http/parser/TestRanges.java new file mode 100644 index 0000000..0e973e0 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestRanges.java @@ -0,0 +1,147 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.http.parser.Ranges.Entry; + +public class TestRanges { + + @Test + public void testCaseInsensitive() throws Exception { + Ranges lower = parse("bytes=1-10"); + Ranges upper = parse("Bytes=1-10"); + + compareRanges(lower, upper); + } + + + @Test + public void testInvalid01() throws Exception { + doTestInvalid(""); + } + + + @Test + public void testInvalid02() throws Exception { + doTestInvalid("=1-10"); + } + + + @Test + public void testInvalid03() throws Exception { + doTestInvalid("bytes"); + } + + + @Test + public void testInvalid04() throws Exception { + doTestInvalid("bytes=1"); + } + + + @Test + public void testInvalid05() throws Exception { + doTestInvalid("bytes=-"); + } + + + @Test + public void testInvalid06() throws Exception { + doTestInvalid("bytes=1-10 a"); + } + + + @Test + public void testValid01() throws Exception { + Ranges r = parse("bytes=1-10,21-30"); + Assert.assertEquals("bytes", r.getUnits()); + List l = r.getEntries(); + Assert.assertEquals(2, l.size()); + Entry e1 = l.get(0); + Assert.assertEquals(1, e1.getStart()); + Assert.assertEquals(10, e1.getEnd()); + Entry e2 = l.get(1); + Assert.assertEquals(21, e2.getStart()); + Assert.assertEquals(30, e2.getEnd()); + } + + + @Test + public void testValid02() throws Exception { + Ranges r = parse("bytes=-20"); + List l = r.getEntries(); + Assert.assertEquals(1, l.size()); + Entry e1 = l.get(0); + Assert.assertEquals(-1, e1.getStart()); + Assert.assertEquals(20, e1.getEnd()); + } + + + @Test + public void testNullUnits() throws Exception { + Ranges r = new Ranges(null, new ArrayList<>()); + Assert.assertNotNull(r); + Assert.assertNull(r.getUnits()); + } + + + @Test + public void testValid03() throws Exception { + Ranges r = parse("bytes=21-"); + List l = r.getEntries(); + Assert.assertEquals(1, l.size()); + Entry e1 = l.get(0); + Assert.assertEquals(21, e1.getStart()); + Assert.assertEquals(-1, e1.getEnd()); + } + + + private void doTestInvalid(String s) throws IOException { + Ranges r = parse(s); + Assert.assertNull(r); + } + + + private Ranges parse(String s) throws IOException { + return Ranges.parse(new StringReader(s)); + } + + + private void compareRanges(Ranges r1, Ranges r2) { + Assert.assertEquals(r1.getUnits(), r2.getUnits()); + + List l1 = r1.getEntries(); + List l2 = r2.getEntries(); + + Assert.assertEquals(l1.size(), l2.size()); + for (int i = 0; i < l1.size(); i++) { + Entry e1 = l1.get(i); + Entry e2 = l2.get(i); + + Assert.assertEquals(e1.getStart(), e2.getStart()); + Assert.assertEquals(e1.getEnd(), e2.getEnd()); + } + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestTokenList.java b/test/org/apache/tomcat/util/http/parser/TestTokenList.java new file mode 100644 index 0000000..0b5c5f5 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestTokenList.java @@ -0,0 +1,222 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +public class TestTokenList { + + @Test + public void testAll() throws IOException { + Set expected = new HashSet<>(); + expected.add("*"); + doTestVary("*", expected, true); + } + + + @Test + public void testSingle() throws IOException { + Set expected = new HashSet<>(); + expected.add("host"); + doTestVary("Host", expected, true); + } + + + @Test + public void testMultiple() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("Host, Foo, Bar", expected, true); + } + + + @Test + public void testEmptyString() throws IOException { + doTestVary("", Collections.emptySet(), false); + } + + + @Test + public void testSingleInvalid() throws IOException { + doTestVary("{{{", Collections.emptySet(), false); + } + + + @Test + public void testMultipleWithInvalidStart() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("{{{, Host, Foo, Bar", expected, false); + } + + + @Test + public void testMultipleWithInvalidMiddle() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("Host, {{{, Foo, Bar", expected, false); + } + + + @Test + public void testMultipleWithInvalidEnd() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("Host, Foo, Bar, {{{", expected, false); + } + + + @Test + public void testMultipleWithInvalidStart2() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("OK {{{, Host, Foo, Bar", expected, false); + } + + + @Test + public void testMultipleWithInvalidMiddle2() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("Host, OK {{{, Foo, Bar", expected, false); + } + + + @Test + public void testMultipleWithInvalidEnd2() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("Host, Foo, Bar, OK {{{", expected, false); + } + + + private void doTestVary(String input, Set expectedTokens, boolean expectedResult) throws IOException { + StringReader reader = new StringReader(input); + Set tokens = new HashSet<>(); + boolean result = TokenList.parseTokenList(reader, tokens); + Assert.assertEquals(expectedTokens, tokens); + Assert.assertEquals(Boolean.valueOf(expectedResult), Boolean.valueOf(result)); + } + + + @Test + public void testMultipleHeadersValidWithoutNull() throws IOException { + doTestMultipleHeadersValid(false); + } + + + @Test + public void testMultipleHeadersValidWithNull() throws IOException { + doTestMultipleHeadersValid(true); + } + + + private void doTestMultipleHeadersValid(boolean withNull) throws IOException { + Set expectedTokens = new HashSet<>(); + expectedTokens.add("bar"); + expectedTokens.add("foo"); + expectedTokens.add("foo2"); + + Set inputs = new HashSet<>(); + inputs.add("foo"); + if (withNull) { + inputs.add(null); + } + inputs.add("bar, foo2"); + + Set tokens = new HashSet<>(); + + + boolean result = TokenList.parseTokenList(Collections.enumeration(inputs), tokens); + Assert.assertEquals(expectedTokens, tokens); + Assert.assertTrue(result); + } + + + @Test + public void doTestMultipleHeadersInvalid() throws IOException { + Set expectedTokens = new HashSet<>(); + expectedTokens.add("bar"); + expectedTokens.add("bar2"); + expectedTokens.add("foo"); + expectedTokens.add("foo2"); + expectedTokens.add("foo3"); + + Set inputs = new HashSet<>(); + inputs.add("foo"); + inputs.add("bar2, }}}, foo3"); + inputs.add("bar, foo2"); + + Set tokens = new HashSet<>(); + + + boolean result = TokenList.parseTokenList(Collections.enumeration(inputs), tokens); + Assert.assertEquals(expectedTokens, tokens); + Assert.assertFalse(result); + } + + + @Test + public void testMultipleWithEmptyStart() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary(",Host, Foo, Bar", expected, true); + } + + + @Test + public void testMultipleWithEmptyMiddle() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("Host, Foo,,Bar", expected, true); + } + + + @Test + public void testMultipleWithEmptyEnd() throws IOException { + Set expected = new HashSet<>(); + expected.add("bar"); + expected.add("foo"); + expected.add("host"); + doTestVary("Host, Foo, Bar,", expected, true); + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TestUpgrade.java b/test/org/apache/tomcat/util/http/parser/TestUpgrade.java new file mode 100644 index 0000000..3910b21 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TestUpgrade.java @@ -0,0 +1,86 @@ +/* + * 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.tomcat.util.http.parser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestUpgrade { + + private static final Integer FOUR = Integer.valueOf(4); + + @Parameterized.Parameters(name = "{index}: headers[{0}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + // Valid example taken from RFC 7230 + parameterSets.add(new Object[] { Arrays.asList(new String[] { "HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11" }), FOUR } ); + parameterSets.add(new Object[] { Arrays.asList(new String[] { "HTTP/2.0", "SHTTP/1.3, IRC/6.9, RTA/x11" }),FOUR } ); + parameterSets.add(new Object[] { Arrays.asList(new String[] { "HTTP/2.0", "SHTTP/1.3", "IRC/6.9", "RTA/x11" }), FOUR } ); + + // As above but without the version info + parameterSets.add(new Object[] { Arrays.asList(new String[] { "HTTP", "SHTTP", "IRC", "RTA" }), FOUR } ); + + // Empty version for final protocol + parameterSets.add(new Object[] { Arrays.asList(new String[] { "HTTP/2.0", "SHTTP/1.3", "IRC/6.9", "RTA/" }), null} ); + + // Empty name for final protocol + parameterSets.add(new Object[] { Arrays.asList(new String[] { "HTTP/2.0", "SHTTP/1.3", "IRC/6.9", "/x11" }), null} ); + + // Make final protocol a non-token + parameterSets.add(new Object[] { Arrays.asList(new String[] { "HTTP/2.0", "SHTTP/1.3", "IRC/6.9", "RTA[/x11" }), null} ); + + // Make final version a non-token + parameterSets.add(new Object[] { Arrays.asList(new String[] { "HTTP/2.0", "SHTTP/1.3", "IRC/6.9", "RTA/x}11" }), null} ); + + // Empty header isn't valid + parameterSets.add(new Object[] { Arrays.asList(new String[] { "" }), null } ); + + // Nulls shouldn't happen but check they are treated as invalid to be safe + parameterSets.add(new Object[] { Arrays.asList(new String[] { null }), null } ); + + return parameterSets; + } + + + @Parameter(0) + public List headers; + + @Parameter(1) + public Integer count; + + @Test + public void testUpgrade() { + List result = Upgrade.parse(Collections.enumeration(headers)); + if (count == null) { + Assert.assertNull(result); + } else { + Assert.assertNotNull(result); + Assert.assertEquals(count.intValue(), result.size()); + } + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TesterHostPerformance.java b/test/org/apache/tomcat/util/http/parser/TesterHostPerformance.java new file mode 100644 index 0000000..b579be3 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TesterHostPerformance.java @@ -0,0 +1,76 @@ +/* + * 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.tomcat.util.http.parser; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.tomcat.util.buf.MessageBytes; + +@RunWith(Parameterized.class) +public class TesterHostPerformance { + + @Parameters + public static Collection inputs() { + List result = new ArrayList<>(); + result.add(new Object[] { "localhost" }); + result.add(new Object[] { "tomcat.apache.org" }); + result.add(new Object[] { "tomcat.apache.org." }); + result.add(new Object[] { "127.0.0.1" }); + result.add(new Object[] { "255.255.255.255" }); + result.add(new Object[] { "[::1]" }); + result.add(new Object[] { "[0123:4567:89AB:CDEF:0123:4567:89AB:CDEF]" }); + return result; + } + + @Parameter(0) + public String hostname; + + private static final int ITERATIONS = 100000000; + + @Test + public void testParseHost() throws Exception { + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) { + Host.parse(hostname); + } + long time = System.nanoTime() - start; + + System.out.println("St " + hostname + ": " + ITERATIONS + " iterations in " + time + "ns"); + System.out.println("St " + hostname + ": " + ITERATIONS * 1000000000.0/time + " iterations per second"); + + MessageBytes mb = MessageBytes.newInstance(); + mb.setString(hostname); + mb.toBytes(); + + start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) { + Host.parse(mb); + } + time = System.nanoTime() - start; + + System.out.println("MB " + hostname + ": " + ITERATIONS + " iterations in " + time + "ns"); + System.out.println("MB " + hostname + ": " + ITERATIONS * 1000000000.0/time + " iterations per second"); + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TesterHttpWgStructuredField.java b/test/org/apache/tomcat/util/http/parser/TesterHttpWgStructuredField.java new file mode 100644 index 0000000..1c86dd5 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TesterHttpWgStructuredField.java @@ -0,0 +1,130 @@ +/* + * 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.tomcat.util.http.parser; + +import java.io.File; +import java.io.FileInputStream; +import java.io.Reader; +import java.io.StringReader; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.json.JSONParser; + +/* + * Not run automatically (due to name) as it requires a local git clone of + * https://github.com/httpwg/structured-field-tests + */ +public class TesterHttpWgStructuredField { + + private static final String testsPath = System.getProperty("user.home") + "/repos/httpwg-sf-tests"; + + + @Test + public void test() throws Exception { + File testDir = new File(testsPath); + doTestDirectory(testDir); + } + + + private void doTestDirectory(File directory) throws Exception { + for (File file : directory.listFiles()) { + if (file.isDirectory()) { + if (!file.getName().equals("serialisation-tests")) { + doTestDirectory(file); + } + } else if (file.isFile()) { + if (file.getName().endsWith(".json")) { + doTestFile(file); + } + } + } + } + + + private void doTestFile(File file) throws Exception { + System.out.println(file.getAbsolutePath()); + + try (FileInputStream fis = new FileInputStream(file)) { + JSONParser parser = new JSONParser(fis); + List array = parser.parseArray(); + for (Object obj : array) { + if (obj instanceof Map) { + doTestMap((Map) obj); + } else { + Assert.fail(); + } + } + } + } + + + private void doTestMap(Map map) throws Exception { + String name = (String) map.get("name"); + @SuppressWarnings("unchecked") + List rawLines = (List) map.get("raw"); + String headerType = (String) map.get("header_type"); + Boolean mustFail = ((Boolean) map.get("must_fail")); + if (mustFail == null) { + mustFail = Boolean.FALSE; + } + Boolean canFail = ((Boolean) map.get("can_fail")); + if (canFail == null) { + canFail = Boolean.FALSE; + } + String raw = StringUtils.join(rawLines); + /* + * The simple JSON parser may not be handling escape sequences + * correctly. + */ + String unescaped = raw.replace("\\\"", "\""); + unescaped = unescaped.replace("\\b", "\u0008"); + unescaped = unescaped.replace("\\t", "\t"); + unescaped = unescaped.replace("\\n", "\n"); + unescaped = unescaped.replace("\\f", "\u000c"); + unescaped = unescaped.replace("\\r", "\r"); + unescaped = unescaped.replace("\\\\", "\\"); + Reader input = new StringReader(unescaped); + + try { + switch (headerType) { + case "item": { + StructuredField.parseSfItem(input); + break; + } + case "list": { + StructuredField.parseSfList(input); + break; + } + case "dictionary": { + StructuredField.parseSfDictionary(input); + break; + } + default: + System.out.println("Type unsupported " + headerType); + } + } catch (Exception e) { + Assert.assertTrue(name + ": raw [" + unescaped + "]", mustFail.booleanValue() || canFail.booleanValue()); + return; + } + Assert.assertFalse(name + ": raw [" + unescaped + "]", mustFail.booleanValue()); + } +} diff --git a/test/org/apache/tomcat/util/http/parser/TesterParserPerformance.java b/test/org/apache/tomcat/util/http/parser/TesterParserPerformance.java new file mode 100644 index 0000000..b834f91 --- /dev/null +++ b/test/org/apache/tomcat/util/http/parser/TesterParserPerformance.java @@ -0,0 +1,143 @@ +/* + * 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.tomcat.util.http.parser; + +import java.util.BitSet; + +import org.junit.Test; + +public class TesterParserPerformance { + + @Test + public void testBitSetVsBooleanArray() { + Lookup bitsetLookup = new BitSetLookup(); + Lookup booleanArrayLookup = new BooleanArrayLookup(); + + int count = 10000; + int loops = 5; + + // Warm up + doLookupTest(bitsetLookup, count); + doLookupTest(booleanArrayLookup, count); + + for (int i = 0; i < loops; i++) { + System.out.println("Bitset : " + doLookupTest(bitsetLookup, count) + "ns"); + System.out.println("Boolean[]: " + doLookupTest(booleanArrayLookup, count) + "ns"); + } + } + + + private long doLookupTest(Lookup lookup, int iterations) { + long start = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + for (int j = 0; j < 128; j++) { + lookup.doLookup(j); + } + } + return System.nanoTime() - start; + } + + + @Test + public void testExceptionVsBoundsCheck() { + Lookup boundsCheck = new BooleanArrayLookupBoundsCheck(); + Lookup exceptionCheck = new BooleanArrayLookupExceptionCheck(); + + int count = 10000; + int loops = 5; + + // Warm up + doLookupTestCheck(boundsCheck, count, 0, 127); + doLookupTestCheck(exceptionCheck, count, 0, 127); + doLookupTestCheck(boundsCheck, count, 128, 255); + doLookupTestCheck(exceptionCheck, count, 128, 255); + + for (int i = 0; i < loops; i++) { + System.out.println("Bounds:Valid : " + doLookupTestCheck(boundsCheck, count, 0, 127) + "ns"); + System.out.println("ExceptionValid : " + doLookupTestCheck(exceptionCheck, count, 0, 127) + "ns"); + System.out.println("Bounds:Invalid : " + doLookupTestCheck(boundsCheck, count, 128, 255) + "ns"); + System.out.println("ExceptionInvalid : " + doLookupTestCheck(exceptionCheck, count, 128, 255) + "ns"); + } + } + + + private long doLookupTestCheck(Lookup lookup, int iterations, int testStart, int testEnd) { + long start = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + for (int j = testStart; j < testEnd; j++) { + lookup.doLookup(j); + } + } + return System.nanoTime() - start; + } + + + private interface Lookup { + boolean doLookup(int i); + } + + + private static class BitSetLookup implements Lookup { + + private BitSet values = new BitSet(128); + + @Override + public boolean doLookup(int i) { + return values.get(i); + } + } + + + private static class BooleanArrayLookup implements Lookup { + + private boolean[] values = new boolean[128]; + + @Override + public boolean doLookup(int i) { + return values[i]; + } + } + + + private static class BooleanArrayLookupBoundsCheck implements Lookup { + + private boolean[] values = new boolean[128]; + + @Override + public boolean doLookup(int i) { + if (i < 0 || i > 127) { + return false; + } + return values[i]; + } + } + + + private static class BooleanArrayLookupExceptionCheck implements Lookup { + + private boolean[] values = new boolean[128]; + + @Override + public boolean doLookup(int i) { + try { + return values[i]; + } catch (ArrayIndexOutOfBoundsException aioe) { + return false; + } + } + } +} diff --git a/test/org/apache/tomcat/util/json/TestJSONFilter.java b/test/org/apache/tomcat/util/json/TestJSONFilter.java new file mode 100644 index 0000000..58dddeb --- /dev/null +++ b/test/org/apache/tomcat/util/json/TestJSONFilter.java @@ -0,0 +1,90 @@ +/* + * 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.tomcat.util.json; + +import java.util.ArrayList; +import java.util.Collection; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + + +@RunWith(Parameterized.class) +public class TestJSONFilter { + + // Use something ... + private static final char SUB = 0x1A; + private static final char STX = 0x02; + + @Parameterized.Parameters(name = "{index}: input[{0}], output[{1}]") + public static Collection parameters() { + Collection parameterSets = new ArrayList<>(); + + // Empty + parameterSets.add(new String[] { "", "" }); + + // Must escape + parameterSets.add(new String[] { "\"", "\\\"" }); + parameterSets.add(new String[] { "\\", "\\\\" }); + // Sample of controls + parameterSets.add(new String[] { "\t", "\\t" }); + parameterSets.add(new String[] { "\n", "\\n" }); + parameterSets.add(new String[] { "\r", "\\r" }); + + // No escape + parameterSets.add(new String[] { "aaa", "aaa" }); + + // Start + parameterSets.add(new String[] { "\naaa", "\\naaa" }); + parameterSets.add(new String[] { "\n\naaa", "\\n\\naaa" }); + parameterSets.add(new String[] { "/aaa", "/aaa" }); + + // Middle + parameterSets.add(new String[] { "aaa\naaa", "aaa\\naaa" }); + parameterSets.add(new String[] { "aaa\n\naaa", "aaa\\n\\naaa" }); + + // End + parameterSets.add(new String[] { "aaa\n", "aaa\\n" }); + parameterSets.add(new String[] { "aaa\n\n", "aaa\\n\\n" }); + + // Start, middle and end + parameterSets.add(new String[] { "\naaa\naaa\n", "\\naaa\\naaa\\n" }); + parameterSets.add(new String[] { "\n\naaa\n\naaa\n\n", "\\n\\naaa\\n\\naaa\\n\\n" }); + + // Multiple + parameterSets.add(new String[] { "\n\n", "\\n\\n" }); + parameterSets.add(new String[] { "\n" + STX + "\n", "\\n\\u0002\\n" }); + parameterSets.add(new String[] { "\n" + STX + "\n" + SUB, "\\n\\u0002\\n\\u001A" }); + parameterSets.add(new String[] { SUB + "\n" + STX + "\n" + SUB, "\\u001A\\n\\u0002\\n\\u001A" }); + + return parameterSets; + } + + @Parameter(0) + public String input; + + @Parameter(1) + public String output; + + @Test + public void testStringEscaping() { + Assert.assertEquals(output, JSONFilter.escape(input)); + } +} diff --git a/test/org/apache/tomcat/util/net/TestClientCert.java b/test/org/apache/tomcat/util/net/TestClientCert.java new file mode 100644 index 0000000..7ebfb4c --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestClientCert.java @@ -0,0 +1,221 @@ +/* + * 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.tomcat.util.net; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * The keys and certificates used in this file are all available in svn and were + * generated using a test CA the files for which are in the Tomcat PMC private + * repository since not all of them are AL2 licensed. + */ +@RunWith(Parameterized.class) +public class TestClientCert extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + + @Test + public void testClientCertGetWithoutPreemptive() throws Exception { + doTestClientCertGet(false); + } + + @Test + public void testClientCertGetWithPreemptive() throws Exception { + doTestClientCertGet(true); + } + + private void doTestClientCertGet(boolean preemptive) throws Exception { + if (preemptive) { + Tomcat tomcat = getTomcatInstance(); + // Only one context deployed + Context c = (Context) tomcat.getHost().findChildren()[0]; + // Enable pre-emptive auth + c.setPreemptiveAuthentication(true); + } + + getTomcatInstance().start(); + + // Unprotected resource + ByteChunk res = getUrl("https://localhost:" + getPort() + "/unprotected"); + + int count = TesterSupport.getLastClientAuthRequestedIssuerCount(); + if (log.isDebugEnabled()) { + log.debug("Last client KeyManager usage: " + TesterSupport.getLastClientAuthKeyManagerUsage() + + ", " + count + " requested Issuers, first one: " + + (count > 0 ? TesterSupport.getLastClientAuthRequestedIssuer(0).getName() : "NONE")); + log.debug("Expected requested Issuer: " + + (preemptive ? TesterSupport.getClientAuthExpectedIssuer() : "NONE")); + } + + if (preemptive) { + Assert.assertTrue("Checking requested client issuer against " + + TesterSupport.getClientAuthExpectedIssuer(), + TesterSupport.checkLastClientAuthRequestedIssuers()); + Assert.assertEquals("OK-" + TesterSupport.ROLE, res.toString()); + } else { + Assert.assertEquals(0, count); + Assert.assertEquals("OK", res.toString()); + } + + // Protected resource + res = getUrl("https://localhost:" + getPort() + "/protected"); + + if (log.isDebugEnabled()) { + count = TesterSupport.getLastClientAuthRequestedIssuerCount(); + log.debug("Last client KeyManager usage: " + TesterSupport.getLastClientAuthKeyManagerUsage() + + ", " + count + " requested Issuers, first one: " + + (count > 0 ? TesterSupport.getLastClientAuthRequestedIssuer(0).getName() : "NONE")); + log.debug("Expected requested Issuer: " + TesterSupport.getClientAuthExpectedIssuer()); + } + Assert.assertTrue("Checking requested client issuer against " + + TesterSupport.getClientAuthExpectedIssuer(), + TesterSupport.checkLastClientAuthRequestedIssuers()); + + Assert.assertEquals("OK-" + TesterSupport.ROLE, res.toString()); + } + + @Test + public void testClientCertPostZero() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.getConnector().setMaxSavePostSize(0); + doTestClientCertPost(1024, false); + } + + @Test + public void testClientCertPostSmaller() throws Exception { + Tomcat tomcat = getTomcatInstance(); + int bodySize = tomcat.getConnector().getMaxSavePostSize() / 2; + doTestClientCertPost(bodySize, false); + } + + @Test + public void testClientCertPostSame() throws Exception { + Tomcat tomcat = getTomcatInstance(); + int bodySize = tomcat.getConnector().getMaxSavePostSize(); + doTestClientCertPost(bodySize, false); + } + + @Test + public void testClientCertPostLarger() throws Exception { + Tomcat tomcat = getTomcatInstance(); + int bodySize = tomcat.getConnector().getMaxSavePostSize() * 2; + doTestClientCertPost(bodySize, true); + } + + private void doTestClientCertPost(int bodySize, boolean expectProtectedFail) + throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + + byte[] body = new byte[bodySize]; + Arrays.fill(body, TesterSupport.DATA); + + // Unprotected resource + ByteChunk res = postUrl(body, "https://localhost:" + getPort() + "/unprotected"); + + int count = TesterSupport.getLastClientAuthRequestedIssuerCount(); + if (log.isDebugEnabled()) { + log.debug("Last client KeyManager usage: " + TesterSupport.getLastClientAuthKeyManagerUsage() + + ", " + count + " requested Issuers, first one: " + + (count > 0 ? TesterSupport.getLastClientAuthRequestedIssuer(0).getName() : "NONE")); + log.debug("Expected requested Issuer: NONE"); + } + + // Unprotected resource with no preemptive authentication + Assert.assertEquals(0, count); + // No authentication no need to buffer POST body during TLS handshake so + // no possibility of hitting buffer limit + Assert.assertEquals("OK-" + bodySize, res.toString()); + + // Protected resource + res.recycle(); + int rc = postUrl(body, "https://localhost:" + getPort() + "/protected", res, null); + + count = TesterSupport.getLastClientAuthRequestedIssuerCount(); + if (log.isDebugEnabled()) { + log.debug("Last client KeyManager usage: " + TesterSupport.getLastClientAuthKeyManagerUsage() + + ", " + count + " requested Issuers, first one: " + + (count > 0 ? TesterSupport.getLastClientAuthRequestedIssuer(0).getName() : "NONE")); + log.debug("Expected requested Issuer: " + TesterSupport.getClientAuthExpectedIssuer()); + } + + if (expectProtectedFail) { + Assert.assertEquals(401, rc); + // POST body buffer fails so TLS handshake never happens + Assert.assertEquals(0, count); + } else { + int expectedBodySize; + if (tomcat.getConnector().getMaxSavePostSize() == 0) { + expectedBodySize = 0; + } else { + expectedBodySize = bodySize; + } + Assert.assertTrue("Checking requested client issuer against " + + TesterSupport.getClientAuthExpectedIssuer(), + TesterSupport.checkLastClientAuthRequestedIssuers()); + Assert.assertEquals("OK-" + expectedBodySize, res.toString()); + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.configureClientCertContext(tomcat); + + TesterSupport.configureClientSsl(); + + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + } +} diff --git a/test/org/apache/tomcat/util/net/TestClientCertTls13.java b/test/org/apache/tomcat/util/net/TestClientCertTls13.java new file mode 100644 index 0000000..742668f --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestClientCertTls13.java @@ -0,0 +1,132 @@ +/* + * 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.tomcat.util.net; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.openssl.OpenSSLImplementation; + +/** + * The keys and certificates used in this file are all available in svn and were + * generated using a test CA the files for which are in the Tomcat PMC private + * repository since not all of them are AL2 licensed. + * + * The JSSE implementation of TLSv1.3 only supports authentication during the + * initial handshake. This test requires TLSv1.3 on client and server so it is + * skipped unless running on a Java version that supports TLSv1.3. + */ +@RunWith(Parameterized.class) +public class TestClientCertTls13 extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + + @Test + public void testClientCertGet() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + ByteChunk res = getUrl("https://localhost:" + getPort() + "/protected"); + Assert.assertEquals("OK-" + TesterSupport.ROLE, res.toString()); + } + + @Test + public void testClientCertPost() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + + int size = 32 * 1024; + + byte[] body = new byte[size]; + Arrays.fill(body, TesterSupport.DATA); + + // Protected resource + ByteChunk res = new ByteChunk(); + int rc = postUrl(body, "https://localhost:" + getPort() + "/protected", res, null); + + Assert.assertEquals(200, rc); + Assert.assertEquals("OK-" + size, res.toString()); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.configureClientCertContext(tomcat); + + TesterSupport.configureClientSsl(); + + Connector connector = tomcat.getConnector(); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + + if (needApr) { + if (OpenSSLImplementation.class.getName().equals(sslImplementationName)) { + // Need at least OpenSSL 1.1.1 for TLSv1.3 support + Assume.assumeTrue(SSL.version() >= 0x1010100f); + } + } + + // Tests default to TLSv1.2 when client cert auth is used + // Need to override some of the previous settings + SSLHostConfig[] sslHostConfigs = connector.findSslHostConfigs(); + Assert.assertNotNull(sslHostConfigs); + Assert.assertEquals(1, sslHostConfigs.length); + + SSLHostConfig sslHostConfig = sslHostConfigs[0]; + + // TLS 1.3 support + sslHostConfig.setProtocols(Constants.SSL_PROTO_TLSv1_3); + // And add force authentication to occur on the initial handshake + sslHostConfig.setCertificateVerification("required"); + } +} diff --git a/test/org/apache/tomcat/util/net/TestCustomSsl.java b/test/org/apache/tomcat/util/net/TestCustomSsl.java new file mode 100644 index 0000000..fc458cf --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestCustomSsl.java @@ -0,0 +1,78 @@ +/* + * 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.tomcat.util.net; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.jsse.TesterBug50640SslImpl; +import org.apache.tomcat.websocket.server.WsContextListener; + +/** + * The keys and certificates used in this file are all available in svn and were + * generated using a test CA the files for which are in the Tomcat PMC private + * repository since not all of them are AL2 licensed. + */ +public class TestCustomSsl extends TomcatBaseTest { + + @Test + public void testCustomSslImplementation() throws Exception { + + TesterSupport.configureClientSsl(); + + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + + SSLHostConfig sslHostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED); + sslHostConfig.addCertificate(certificate); + connector.addSslHostConfig(sslHostConfig); + + Assert.assertTrue(connector.setProperty( + "sslImplementationName", "org.apache.tomcat.util.net.jsse.TesterBug50640SslImpl")); + + // This setting will break ssl configuration unless the custom + // implementation is used. + sslHostConfig.setProtocols(TesterBug50640SslImpl.PROPERTY_VALUE); + + sslHostConfig.setSslProtocol("tls"); + + File keystoreFile = new File(TesterSupport.LOCALHOST_RSA_JKS); + certificate.setCertificateKeystoreFile(keystoreFile.getAbsolutePath()); + + connector.setSecure(true); + Assert.assertTrue(connector.setProperty("SSLEnabled", "true")); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + Context ctxt = tomcat.addWebapp( + null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + + tomcat.start(); + ByteChunk res = getUrl("https://localhost:" + getPort() + + "/examples/servlets/servlet/HelloWorldExample"); + Assert.assertTrue(res.toString().indexOf("") > 0); + } +} diff --git a/test/org/apache/tomcat/util/net/TestCustomSslTrustManager.java b/test/org/apache/tomcat/util/net/TestCustomSslTrustManager.java new file mode 100644 index 0000000..f978fa5 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestCustomSslTrustManager.java @@ -0,0 +1,159 @@ +/* + * 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.tomcat.util.net; + +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.SSLException; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.coyote.ProtocolHandler; +import org.apache.coyote.http11.AbstractHttp11JsseProtocol; +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * The keys and certificates used in this file are all available in svn and were + * generated using a test CA the files for which are in the Tomcat PMC private + * repository since not all of them are AL2 licensed. + */ +@RunWith(Parameterized.class) +public class TestCustomSslTrustManager extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + + private enum TrustType { + ALL, + CA, + NONE + } + + @Test + public void testCustomTrustManagerAll() throws Exception { + doTestCustomTrustManager(TrustType.ALL); + } + + @Test + public void testCustomTrustManagerCA() throws Exception { + doTestCustomTrustManager(TrustType.CA); + } + + @Test + public void testCustomTrustManagerNone() throws Exception { + doTestCustomTrustManager(TrustType.NONE); + } + + private void doTestCustomTrustManager(TrustType trustType) + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.configureClientCertContext(tomcat); + + Connector connector = tomcat.getConnector(); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + + // Override the defaults + ProtocolHandler handler = connector.getProtocolHandler(); + if (handler instanceof AbstractHttp11JsseProtocol) { + connector.findSslHostConfigs()[0].setTruststoreFile(null); + } else { + // Unexpected + Assert.fail("Unexpected handler type"); + } + if (trustType.equals(TrustType.ALL)) { + connector.findSslHostConfigs()[0].setTrustManagerClassName( + "org.apache.tomcat.util.net.TesterSupport$TrustAllCerts"); + } else if (trustType.equals(TrustType.CA)) { + connector.findSslHostConfigs()[0].setTrustManagerClassName( + "org.apache.tomcat.util.net.TesterSupport$SequentialTrustManager"); + } + + // Start Tomcat + tomcat.start(); + + TesterSupport.configureClientSsl(); + + // Unprotected resource + ByteChunk res = getUrl("https://localhost:" + getPort() + "/unprotected"); + Assert.assertEquals("OK", res.toString()); + + // Protected resource + res.recycle(); + int rc = -1; + try { + rc = getUrl("https://localhost:" + getPort() + "/protected", res, null, null); + } catch (SocketException | SSLException e) { + if (!trustType.equals(TrustType.NONE)) { + Assert.fail(e.getMessage()); + e.printStackTrace(); + } + } + + if (trustType.equals(TrustType.CA)) { + if (log.isDebugEnabled()) { + int count = TesterSupport.getLastClientAuthRequestedIssuerCount(); + log.debug("Last client KeyManager usage: " + TesterSupport.getLastClientAuthKeyManagerUsage() + + ", " + count + " requested Issuers, first one: " + + (count > 0 ? TesterSupport.getLastClientAuthRequestedIssuer(0).getName() : "NONE")); + log.debug("Expected requested Issuer: " + TesterSupport.getClientAuthExpectedIssuer()); + } + Assert.assertTrue("Checking requested client issuer against " + + TesterSupport.getClientAuthExpectedIssuer(), + TesterSupport.checkLastClientAuthRequestedIssuers()); + } + + if (trustType.equals(TrustType.NONE)) { + Assert.assertTrue(rc != 200); + Assert.assertNull(res.toString()); + } else { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK-" + TesterSupport.ROLE, res.toString()); + } + } +} diff --git a/test/org/apache/tomcat/util/net/TestIPv6Utils.java b/test/org/apache/tomcat/util/net/TestIPv6Utils.java new file mode 100644 index 0000000..5d667ed --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestIPv6Utils.java @@ -0,0 +1,144 @@ +/* + * 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.tomcat.util.net; + +import org.junit.Assert; +import org.junit.Test; + + +/** + * Mostly examples from RFC 5952 + */ +public class TestIPv6Utils { + + @Test + public void testMayBeIPv6Address() { + Assert.assertFalse(IPv6Utils.mayBeIPv6Address(null)); + + Assert.assertTrue(IPv6Utils.mayBeIPv6Address("::1")); + Assert.assertTrue(IPv6Utils.mayBeIPv6Address("::")); + Assert.assertTrue(IPv6Utils.mayBeIPv6Address("2001:db8:0:0:1:0:0:1")); + + Assert.assertFalse(IPv6Utils.mayBeIPv6Address("")); + Assert.assertFalse(IPv6Utils.mayBeIPv6Address(":1")); + Assert.assertFalse(IPv6Utils.mayBeIPv6Address("123.123.123.123")); + Assert.assertFalse(IPv6Utils.mayBeIPv6Address("tomcat.eu.apache.org:443")); + } + + @Test + public void testCanonize() { + Assert.assertNull(IPv6Utils.canonize(null)); + Assert.assertEquals("", IPv6Utils.canonize("")); + + // IPv4-safe + Assert.assertEquals("123.123.123.123", IPv6Utils.canonize("123.123.123.123")); + Assert.assertEquals("123.1.2.23", IPv6Utils.canonize("123.1.2.23")); + + // Introductory RFC 5952 examples + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8:0:0:1:0:0:1")); + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:0db8:0:0:1:0:0:1")); + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8::1:0:0:1")); + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8::0:1:0:0:1")); + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:0db8::1:0:0:1")); + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8:0:0:1::1")); + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8:0000:0:1::1")); + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:DB8:0:0:1::1")); + + // Strip leading zeros (2.1) + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:0001")); + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:001")); + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:01")); + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:1")); + + // Zero compression (2.2) + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:0:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd::1")); + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:0:1", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:0:1")); + + Assert.assertEquals("2001:db8::1", IPv6Utils.canonize("2001:db8:0:0:0::1")); + Assert.assertEquals("2001:db8::1", IPv6Utils.canonize("2001:db8:0:0::1")); + Assert.assertEquals("2001:db8::1", IPv6Utils.canonize("2001:db8:0::1")); + Assert.assertEquals("2001:db8::1", IPv6Utils.canonize("2001:db8::1")); + + Assert.assertEquals("2001:db8::aaaa:0:0:1", IPv6Utils.canonize("2001:db8::aaaa:0:0:1")); + Assert.assertEquals("2001:db8::aaaa:0:0:1", IPv6Utils.canonize("2001:db8:0:0:aaaa::1")); + + // Uppercase or lowercase (2.3) + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa")); + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:AAAA")); + Assert.assertEquals("2001:db8:aaaa:bbbb:cccc:dddd:eeee:aaaa", IPv6Utils.canonize("2001:db8:aaaa:bbbb:cccc:dddd:eeee:AaAa")); + + // Some more zero compression for localhost addresses + Assert.assertEquals("::1", IPv6Utils.canonize("0:0:0:0:0:0:0:1")); + Assert.assertEquals("::1", IPv6Utils.canonize("0000:0:0:0:0:0:0:0001")); + Assert.assertEquals("::1", IPv6Utils.canonize("00:00:0:0:00:00:0:01")); + Assert.assertEquals("::1", IPv6Utils.canonize("::0001")); + Assert.assertEquals("::1", IPv6Utils.canonize("::1")); + + // IPv6 unspecified address + Assert.assertEquals("::", IPv6Utils.canonize("0:0:0:0:0:0:0:0")); + Assert.assertEquals("::", IPv6Utils.canonize("0000:0:0:0:0:0:0:0000")); + Assert.assertEquals("::", IPv6Utils.canonize("00:00:0:0:00:00:0:00")); + Assert.assertEquals("::", IPv6Utils.canonize("::0000")); + Assert.assertEquals("::", IPv6Utils.canonize("::0")); + Assert.assertEquals("::", IPv6Utils.canonize("::")); + + // Leading zeros (4.1) + Assert.assertEquals("2001:db8::1", IPv6Utils.canonize("2001:0db8::0001")); + + // Shorten as much as possible (4.2.1) + Assert.assertEquals("2001:db8::2:1", IPv6Utils.canonize("2001:db8:0:0:0:0:2:1")); + Assert.assertEquals("2001:db8::", IPv6Utils.canonize("2001:db8:0:0:0:0:0:0")); + + // Handling One 16-Bit 0 Field (4.2.2) + Assert.assertEquals("2001:db8:0:1:1:1:1:1", IPv6Utils.canonize("2001:db8:0:1:1:1:1:1")); + Assert.assertEquals("2001:db8:0:1:1:1:1:1", IPv6Utils.canonize("2001:db8::1:1:1:1:1")); + + // Choice in Placement of "::" (4.2.3) + Assert.assertEquals("2001:0:0:1::1", IPv6Utils.canonize("2001:0:0:1:0:0:0:1")); + Assert.assertEquals("2001:db8::1:0:0:1", IPv6Utils.canonize("2001:db8:0:0:1:0:0:1")); + + // Already ends with "::" + Assert.assertEquals("2001:db8::", IPv6Utils.canonize("2001:db8::")); + + // IPv4 inside IPv6 + Assert.assertEquals("::ffff:192.0.2.1", IPv6Utils.canonize("::ffff:192.0.2.1")); + Assert.assertEquals("::ffff:192.0.2.1", IPv6Utils.canonize("0:0:0:0:0:ffff:192.0.2.1")); + Assert.assertEquals("::192.0.2.1", IPv6Utils.canonize("::192.0.2.1")); + Assert.assertEquals("::192.0.2.1", IPv6Utils.canonize("0:0:0:0:0:0:192.0.2.1")); + + // Zone ID + Assert.assertEquals("fe80::f0f0:c0c0:1919:1234%4", IPv6Utils.canonize("fe80::f0f0:c0c0:1919:1234%4")); + Assert.assertEquals("fe80::f0f0:c0c0:1919:1234%4", IPv6Utils.canonize("fe80:0:0:0:f0f0:c0c0:1919:1234%4")); + + Assert.assertEquals("::%4", IPv6Utils.canonize("::%4")); + Assert.assertEquals("::%4", IPv6Utils.canonize("::0%4")); + Assert.assertEquals("::%4", IPv6Utils.canonize("0:0::0%4")); + Assert.assertEquals("::%4", IPv6Utils.canonize("0:0:0:0:0:0:0:0%4")); + + Assert.assertEquals("::1%4", IPv6Utils.canonize("::1%4")); + Assert.assertEquals("::1%4", IPv6Utils.canonize("0:0::1%4")); + Assert.assertEquals("::1%4", IPv6Utils.canonize("0:0:0:0:0:0:0:1%4")); + + Assert.assertEquals("::1%eth0", IPv6Utils.canonize("::1%eth0")); + Assert.assertEquals("::1%eth0", IPv6Utils.canonize("0:0::1%eth0")); + Assert.assertEquals("::1%eth0", IPv6Utils.canonize("0:0:0:0:0:0:0:1%eth0")); + + // Hostname safety + Assert.assertEquals("www.apache.org", IPv6Utils.canonize("www.apache.org")); + Assert.assertEquals("ipv6.google.com", IPv6Utils.canonize("ipv6.google.com")); + } +} diff --git a/test/org/apache/tomcat/util/net/TestSSLHostConfig.java b/test/org/apache/tomcat/util/net/TestSSLHostConfig.java new file mode 100644 index 0000000..1ee4f80 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLHostConfig.java @@ -0,0 +1,112 @@ +/* + * 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.tomcat.util.net; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.net.openssl.OpenSSLConf; +import org.apache.tomcat.util.net.openssl.OpenSSLConfCmd; +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; + +public class TestSSLHostConfig { + + @Test + public void testCipher01() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c = Cipher.TLS_RSA_WITH_NULL_MD5; + + // Single JSSE name + hc.setCiphers(c.getJsseNames().iterator().next()); + Assert.assertEquals(c.getOpenSSLAlias(), hc.getCiphers()); + } + + + @Test + public void testCipher02() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c1 = Cipher.TLS_RSA_WITH_NULL_MD5; + Cipher c2 = Cipher.TLS_RSA_WITH_NULL_SHA; + + // Two JSSE names + hc.setCiphers(c1.getJsseNames().iterator().next() + "," + + c2.getJsseNames().iterator().next()); + Assert.assertEquals(c1.getOpenSSLAlias() + ":" + c2.getOpenSSLAlias(), hc.getCiphers()); + } + + + @Test + public void testCipher03() { + SSLHostConfig hc = new SSLHostConfig(); + // Single OpenSSL alias + hc.setCiphers("ALL"); + Assert.assertEquals("ALL", hc.getCiphers()); + } + + + @Test + public void testCipher04() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c = Cipher.TLS_RSA_WITH_NULL_MD5; + + // Single OpenSSLName name + hc.setCiphers(c.getOpenSSLAlias()); + Assert.assertEquals(c.getOpenSSLAlias(), hc.getCiphers()); + } + + + @Test + public void testSerialization() throws IOException, ClassNotFoundException { + // Dummy OpenSSL command name/value pair + String name = "foo"; + String value = "bar"; + + // Set up the object + SSLHostConfig sslHostConfig = new SSLHostConfig(); + OpenSSLConf openSSLConf = new OpenSSLConf(); + OpenSSLConfCmd openSSLConfCmd = new OpenSSLConfCmd(); + openSSLConfCmd.setName(name); + openSSLConfCmd.setValue(value); + openSSLConf.addCmd(openSSLConfCmd); + sslHostConfig.setOpenSslConf(openSSLConf); + + // Serialize + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(sslHostConfig); + oos.close(); + + // Deserialize + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + SSLHostConfig output = (SSLHostConfig) ois.readObject(); + + // Check values + List commands = output.getOpenSslConf().getCommands(); + Assert.assertEquals(1, commands.size()); + OpenSSLConfCmd command = commands.get(0); + Assert.assertEquals(name, command.getName()); + Assert.assertEquals(value, command.getValue()); + } +} diff --git a/test/org/apache/tomcat/util/net/TestSSLHostConfigCompat.java b/test/org/apache/tomcat/util/net/TestSSLHostConfigCompat.java new file mode 100644 index 0000000..9059406 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLHostConfigCompat.java @@ -0,0 +1,336 @@ +/* + * 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.tomcat.util.net; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.StoreType; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.TesterSupport.ClientSSLSocketFactory; + +/* + * Tests compatibility of JSSE and OpenSSL settings. + */ +@RunWith(Parameterized.class) +public class TestSSLHostConfigCompat extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}-{3}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + for (StoreType storeType : new StoreType[] { StoreType.KEYSTORE, StoreType.PEM } ) { + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation", storeType}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation", storeType}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation", storeType}); + } + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + @Parameter(3) + public StoreType storeType; + + + @Test + public void testHostEC() throws Exception { + configureHostEC(); + doTest(); + } + + + @Test + public void testHostRSA() throws Exception { + configureHostRSA(); + doTest(); + } + + + @Test + public void testHostRSAandECwithDefaultClient() throws Exception { + configureHostRSA(); + configureHostEC(); + doTest(); + } + + + /* + * This test and the next just swap the order in which the server certs are + * configured to ensure correct operation isn't dependent on order. + */ + @Test + public void testHostRSAandECwithRSAClient() throws Exception { + configureHostRSA(); + configureHostEC(); + + // Configure cipher suite that requires an RSA certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] {"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + /* + * This test and the previous just swap the order in which the server certs + * are configured to ensure correct operation isn't dependent on order. + */ + @Test + public void testHostECandRSAwithRSAClient() throws Exception { + configureHostEC(); + configureHostRSA(); + + // Configure cipher suite that requires an RSA certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] {"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + /* + * This test and the next just swap the order in which the server certs are + * configured to ensure correct operation isn't dependent on order. + */ + @Test + public void testHostRSAandECwithECClient() throws Exception { + configureHostRSA(); + configureHostEC(); + + // Configure cipher suite that requires an EC certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] {"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + /* + * This test and the previous just swap the order in which the server certs + * are configured to ensure correct operation isn't dependent on order. + */ + @Test + public void testHostECandRSAwithECClient() throws Exception { + configureHostEC(); + configureHostRSA(); + + // Configure cipher suite that requires an EC certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] {"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + @Test + public void testHostRSAwithRSAClient() throws Exception { + configureHostRSA(); + + // Configure cipher suite that requires an RSA certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] {"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + @Test(expected=javax.net.ssl.SSLHandshakeException.class) + public void testHostRSAwithECClient() throws Exception { + configureHostRSA(); + + // Configure cipher suite that requires an EC certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] {"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + @Test + public void testHostRSAwithRSAandECClient() throws Exception { + configureHostRSA(); + + // Configure cipher suite that requires an EC certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] { + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + @Test(expected=javax.net.ssl.SSLHandshakeException.class) + public void testHostECwithRSAClient() throws Exception { + configureHostEC(); + + // Configure cipher suite that requires an RSA certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] {"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + @Test + public void testHostECwithECClient() throws Exception { + configureHostEC(); + + // Configure cipher suite that requires an EC certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] {"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + @Test + public void testHostECwithRSAandECClient() throws Exception { + configureHostEC(); + + // Configure cipher suite that requires an RSA certificate on the server + ClientSSLSocketFactory clientSSLSocketFactory = TesterSupport.configureClientSsl(); + clientSSLSocketFactory.setCipher(new String[] { + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"}); + + doTest(false); + } + + + private void configureHostRSA() { + SSLHostConfig sslHostConfig = getSSLHostConfig(); + switch (storeType) { + case KEYSTORE: { + SSLHostConfigCertificate sslHostConfigCertificateRsa = new SSLHostConfigCertificate(sslHostConfig, Type.RSA); + sslHostConfigCertificateRsa.setCertificateKeystoreFile(getPath(TesterSupport.LOCALHOST_RSA_JKS)); + sslHostConfig.addCertificate(sslHostConfigCertificateRsa); + break; + } + case PEM: { + SSLHostConfigCertificate sslHostConfigCertificateRsa = new SSLHostConfigCertificate(sslHostConfig, Type.RSA); + sslHostConfigCertificateRsa.setCertificateFile(getPath(TesterSupport.LOCALHOST_RSA_CERT_PEM)); + sslHostConfigCertificateRsa.setCertificateKeyFile(getPath(TesterSupport.LOCALHOST_RSA_KEY_PEM)); + sslHostConfig.addCertificate(sslHostConfigCertificateRsa); + break; + } + } + } + + + private void configureHostEC() { + SSLHostConfig sslHostConfig = getSSLHostConfig(); + switch (storeType) { + case KEYSTORE: { + SSLHostConfigCertificate sslHostConfigCertificateEc = new SSLHostConfigCertificate(sslHostConfig, Type.EC); + sslHostConfigCertificateEc.setCertificateKeystoreFile(getPath(TesterSupport.LOCALHOST_EC_JKS)); + sslHostConfig.addCertificate(sslHostConfigCertificateEc); + break; + } + case PEM: { + SSLHostConfigCertificate sslHostConfigCertificateEc = new SSLHostConfigCertificate(sslHostConfig, Type.EC); + sslHostConfigCertificateEc.setCertificateFile(getPath(TesterSupport.LOCALHOST_EC_CERT_PEM)); + sslHostConfigCertificateEc.setCertificateKeyFile(getPath(TesterSupport.LOCALHOST_EC_KEY_PEM)); + sslHostConfig.addCertificate(sslHostConfigCertificateEc); + break; + } + } + } + + + private void doTest() throws Exception { + // Use the default client TLS config + doTest(true); + } + + + private void doTest(boolean configureClientSsl) throws Exception { + if (configureClientSsl) { + TesterSupport.configureClientSsl(); + } + + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + + // Check a request can be made + ByteChunk res = getUrl("https://localhost:" + getPort() + "/"); + Assert.assertEquals("OK", res.toString()); + } + + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + + connector.setPort(0); + connector.setScheme("https"); + connector.setSecure(true); + Assert.assertTrue(connector.setProperty("SSLEnabled", "true")); + SSLHostConfig sslHostConfig = new SSLHostConfig(); + sslHostConfig.setProtocols("TLSv1.2"); + connector.addSslHostConfig(sslHostConfig); + + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + + // Simple webapp + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "TesterServlet", new TesterServlet()); + ctxt.addServletMappingDecoded("/*", "TesterServlet"); + } + + + private SSLHostConfig getSSLHostConfig() { + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + return connector.findSslHostConfigs()[0]; + } + + + private static String getPath(String relativePath) { + File f = new File(relativePath); + return f.getAbsolutePath(); + } +} diff --git a/test/org/apache/tomcat/util/net/TestSSLHostConfigIntegration.java b/test/org/apache/tomcat/util/net/TestSSLHostConfigIntegration.java new file mode 100644 index 0000000..fede2c6 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLHostConfigIntegration.java @@ -0,0 +1,91 @@ +/* + * 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.tomcat.util.net; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.server.WsContextListener; + +@RunWith(Parameterized.class) +public class TestSSLHostConfigIntegration extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + + @Test + public void testSslHostConfigIsSerializable() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + org.apache.catalina.Context ctxt = tomcat.addWebapp( + null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + + TesterSupport.initSsl(tomcat); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + + tomcat.start(); + + SSLHostConfig[] sslHostConfigs = + tomcat.getConnector().getProtocolHandler().findSslHostConfigs(); + + boolean written = false; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + for (SSLHostConfig sslHostConfig : sslHostConfigs) { + oos.writeObject(sslHostConfig); + written = true; + } + } + + Assert.assertTrue(written); + } +} diff --git a/test/org/apache/tomcat/util/net/TestSocketBufferHandler.java b/test/org/apache/tomcat/util/net/TestSocketBufferHandler.java new file mode 100644 index 0000000..048c182 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSocketBufferHandler.java @@ -0,0 +1,139 @@ +/* + * 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.tomcat.util.net; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + + +@RunWith(Parameterized.class) +public class TestSocketBufferHandler { + + @Parameterized.Parameters(name = "{index}: direct[{0}]") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { Boolean.FALSE }); + parameterSets.add(new Object[] { Boolean.TRUE }); + + return parameterSets; + } + + + @Parameter(0) + public boolean direct; + + + @Test + public void testReturnWhenEmpty() { + SocketBufferHandler sbh = new SocketBufferHandler(8, 8, direct); + sbh.unReadReadBuffer(ByteBuffer.wrap(getBytes("WXYZ"))); + + validate(sbh, "WXYZ"); + } + + + @Test + public void testReturnWhenWritable() { + SocketBufferHandler sbh = new SocketBufferHandler(8, 8, direct); + + sbh.configureReadBufferForWrite(); + sbh.getReadBuffer().put(getBytes("AB")); + + sbh.unReadReadBuffer(ByteBuffer.wrap(getBytes("WXYZ"))); + + validate(sbh, "WXYZAB"); + } + + + @Test(expected = BufferOverflowException.class) + public void testReturnWhenWritableAndFull() { + SocketBufferHandler sbh = new SocketBufferHandler(8, 8, direct); + + sbh.configureReadBufferForWrite(); + sbh.getReadBuffer().put(getBytes("ABCDEFGH")); + + sbh.unReadReadBuffer(ByteBuffer.wrap(getBytes("WXYZ"))); + } + + + @Test + public void testReturnWhenReadableAndUnread() { + SocketBufferHandler sbh = new SocketBufferHandler(8, 8, direct); + + sbh.configureReadBufferForWrite(); + sbh.getReadBuffer().put(getBytes("AB")); + sbh.configureReadBufferForRead(); + + sbh.unReadReadBuffer(ByteBuffer.wrap(getBytes("WXYZ"))); + + validate(sbh, "WXYZAB"); + } + + + @Test(expected = BufferOverflowException.class) + public void testReturnWhenReadableAndUnreadAndFull() { + SocketBufferHandler sbh = new SocketBufferHandler(8, 8, direct); + + sbh.configureReadBufferForWrite(); + sbh.getReadBuffer().put(getBytes("ABCDEF")); + sbh.configureReadBufferForRead(); + + sbh.unReadReadBuffer(ByteBuffer.wrap(getBytes("WXYZ"))); + } + + + @Test + public void testReturnWhenReadableAndPartiallyead() { + SocketBufferHandler sbh = new SocketBufferHandler(8, 8, direct); + + sbh.configureReadBufferForWrite(); + sbh.getReadBuffer().put(getBytes("ABCDEFGH")); + sbh.configureReadBufferForRead(); + for (int i = 0; i < 4; i++) { + sbh.getReadBuffer().get(); + } + + sbh.unReadReadBuffer(ByteBuffer.wrap(getBytes("WXYZ"))); + + validate(sbh, "WXYZEFGH"); + } + + + private void validate(SocketBufferHandler sbh, String expected) { + sbh.configureReadBufferForRead(); + for (byte b : getBytes(expected)) { + Assert.assertEquals(b, sbh.getReadBuffer().get()); + } + Assert.assertEquals(0, sbh.getReadBuffer().remaining()); + } + + + private byte[] getBytes(String input) { + return input.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/test/org/apache/tomcat/util/net/TestSsl.java b/test/org/apache/tomcat/util/net/TestSsl.java new file mode 100644 index 0000000..99a51d3 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSsl.java @@ -0,0 +1,380 @@ +/* + * 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.tomcat.util.net; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.SocketFactory; +import javax.net.ssl.HandshakeCompletedEvent; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.websocket.server.WsContextListener; + +/** + * The keys and certificates used in this file are all available in svn and were + * generated using a test CA the files for which are in the Tomcat PMC private + * repository since not all of them are AL2 licensed. + */ +@RunWith(Parameterized.class) +public class TestSsl extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + + @Test + public void testSimpleSsl() throws Exception { + TesterSupport.configureClientSsl(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + Context ctxt = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + + TesterSupport.initSsl(tomcat); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + + tomcat.start(); + ByteChunk res = getUrl("https://localhost:" + getPort() + + "/examples/servlets/servlet/HelloWorldExample"); + Assert.assertTrue(res.toString().indexOf("") > 0); + Assert.assertTrue("Checking no client issuer has been requested", + TesterSupport.getLastClientAuthRequestedIssuerCount() == 0); + } + + private static final int POST_DATA_SIZE = 16 * 1024 * 1024; + private static final byte[] POST_DATA; + static { + POST_DATA = new byte[POST_DATA_SIZE]; // 16 MiB + Arrays.fill(POST_DATA, (byte) 1); + + } + + @Test + public void testPost() throws Exception { + SocketFactory socketFactory = TesterSupport.configureClientSsl(); + + Tomcat tomcat = getTomcatInstance(); + TesterSupport.initSsl(tomcat); + Connector connector = tomcat.getConnector(); + // Increase timeout as default (3s) can be too low for some CI systems + Assert.assertTrue(connector.setProperty("connectionTimeout", "20000")); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "post", new SimplePostServlet()); + ctxt.addServletMappingDecoded("/post", "post"); + tomcat.start(); + int iterations = 8; + CountDownLatch latch = new CountDownLatch(iterations); + AtomicInteger errorCount = new AtomicInteger(0); + for (int i = 0; i < iterations; i++) { + new Thread() { + @Override + public void run() { + try { + SSLSocket socket = (SSLSocket) socketFactory.createSocket("localhost", + getPort()); + + OutputStream os = socket.getOutputStream(); + + os.write("POST /post HTTP/1.1\r\n".getBytes()); + os.write("Host: localhost\r\n".getBytes()); + os.write(("Content-Length: " + Integer.valueOf(POST_DATA.length) + "\r\n\r\n").getBytes()); + // Write in 128 KiB blocks + for (int i = 0; i < POST_DATA.length / (128 * 1024); i++) { + os.write(POST_DATA, 0, 1024 * 128); + sleep(10); + } + os.flush(); + + InputStream is = socket.getInputStream(); + + // Skip to the end of the headers + byte[] endOfHeaders = "\r\n\r\n".getBytes(); + int found = 0; + while (found != endOfHeaders.length) { + int c = is.read(); + if (c == -1) { + // EOF + System.err.println("Unexpected EOF"); + errorCount.incrementAndGet(); + break; + } else if (c == endOfHeaders[found]) { + found++; + } else { + found = 0; + } + } + + for (int i = 0; i < POST_DATA.length; i++) { + int read = is.read(); + if (POST_DATA[i] != read) { + System.err.println("Byte in position [" + i + "] had value [" + read + + "] rather than [" + Byte.toString(POST_DATA[i]) + "]"); + errorCount.incrementAndGet(); + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + errorCount.incrementAndGet(); + } finally { + latch.countDown(); + } + } + }.start(); + } + latch.await(); + Assert.assertEquals(0, errorCount.get()); + } + + @Test + public void testKeyPass() throws Exception { + TesterSupport.configureClientSsl(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + Context ctxt = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + + TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, + TesterSupport.JKS_PASS, null, TesterSupport.JKS_KEY_PASS, null); + + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + + tomcat.start(); + ByteChunk res = getUrl("https://localhost:" + getPort() + + "/examples/servlets/servlet/HelloWorldExample"); + Assert.assertTrue(res.toString().indexOf("") > 0); + Assert.assertTrue("Checking no client issuer has been requested", + TesterSupport.getLastClientAuthRequestedIssuerCount() == 0); + } + + @Test + public void testKeyPassFile() throws Exception { + TesterSupport.configureClientSsl(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + Context ctxt = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + + TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, + null, TesterSupport.JKS_PASS_FILE, null, TesterSupport.JKS_KEY_PASS_FILE); + + tomcat.start(); + ByteChunk res = getUrl("https://localhost:" + getPort() + + "/examples/servlets/servlet/HelloWorldExample"); + Assert.assertTrue(res.toString().indexOf("") > 0); + Assert.assertTrue("Checking no client issuer has been requested", + TesterSupport.getLastClientAuthRequestedIssuerCount() == 0); + } + + @Test + public void testClientInitiatedRenegotiation() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + TesterSupport.initSsl(tomcat); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + + boolean renegotiationSupported = TesterSupport.isClientRenegotiationSupported(getTomcatInstance()); + + Context root = tomcat.addContext("", TEMP_DIR); + Wrapper w = Tomcat.addServlet(root, "tester", new TesterServlet()); + w.setAsyncSupported(true); + root.addServletMappingDecoded("/", "tester"); + + tomcat.start(); + + SSLContext sslCtx; + // Force TLS 1.2 if TLS 1.3 is available as JSSE's TLS 1.3 + // implementation doesn't support Post Handshake Authentication + // which is required for this test to pass. + sslCtx = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_2); + + sslCtx.init(null, TesterSupport.getTrustManagers(), null); + SSLSocketFactory socketFactory = sslCtx.getSocketFactory(); + SSLSocket socket = (SSLSocket) socketFactory.createSocket("localhost", getPort()); + + OutputStream os = socket.getOutputStream(); + InputStream is = socket.getInputStream(); + Reader r = new InputStreamReader(is); + + doRequest(os, r); + Assert.assertTrue("Checking no client issuer has been requested", + TesterSupport.getLastClientAuthRequestedIssuerCount() == 0); + + TesterHandshakeListener listener = new TesterHandshakeListener(); + socket.addHandshakeCompletedListener(listener); + + socket.startHandshake(); + + try { + doRequest(os, r); + if (!renegotiationSupported) { + Assert.fail("Renegotiation started when it should have failed"); + } + } catch (IOException e) { + if (renegotiationSupported) { + Assert.fail("Renegotiation failed when it should be supported"); + } + return; + } + // Handshake complete appears to be called asynchronously + int wait = 0; + while (wait < 5000 && !listener.isComplete()) { + wait += 50; + Thread.sleep(50); + } + Assert.assertTrue("Checking no client issuer has been requested", + TesterSupport.getLastClientAuthRequestedIssuerCount() == 0); + Assert.assertTrue(listener.isComplete()); + System.out.println("Renegotiation completed after " + wait + " ms"); + } + + private void doRequest(OutputStream os, Reader r) throws IOException { + char[] expectedResponseLine = "HTTP/1.1 200 \r\n".toCharArray(); + + os.write("GET /tester HTTP/1.1\r\n".getBytes()); + os.write("Host: localhost\r\n".getBytes()); + os.write("Connection: Keep-Alive\r\n\r\n".getBytes()); + os.flush(); + + // First check we get the expected response line + for (char c : expectedResponseLine) { + int read = r.read(); + Assert.assertEquals(c, read); + } + + // Skip to the end of the headers + char[] endOfHeaders ="\r\n\r\n".toCharArray(); + int found = 0; + while (found != endOfHeaders.length) { + int c = r.read(); + if (c == -1) { + // EOF + Assert.fail("Unexpected EOF"); + } else if (c == endOfHeaders[found]) { + found++; + } else { + found = 0; + } + } + + // Read the body + char[] expectedBody = "OK".toCharArray(); + for (char c : expectedBody) { + int read = r.read(); + Assert.assertEquals(c, read); + } + } + + private static class TesterHandshakeListener implements HandshakeCompletedListener { + + private volatile boolean complete = false; + + @Override + public void handshakeCompleted(HandshakeCompletedEvent event) { + complete = true; + } + + public boolean isComplete() { + return complete; + } + } + + public static class SimplePostServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(POST_DATA_SIZE); + byte[] in = new byte[1500]; + InputStream input = req.getInputStream(); + while (true) { + int n = input.read(in); + if (n > 0) { + baos.write(in, 0, n); + } else { + break; + } + } + byte[] out = baos.toByteArray(); + // Set the content-length to avoid having to parse chunked + resp.setContentLength(out.length); + resp.getOutputStream().write(out); + } + + } +} diff --git a/test/org/apache/tomcat/util/net/TestTLSClientHelloExtractor.java b/test/org/apache/tomcat/util/net/TestTLSClientHelloExtractor.java new file mode 100644 index 0000000..2044b52 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestTLSClientHelloExtractor.java @@ -0,0 +1,89 @@ +/* + * 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.tomcat.util.net; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.net.TLSClientHelloExtractor.ExtractorResult; + +public class TestTLSClientHelloExtractor { + + @Test + public void testInputNeedRead01() throws IOException { + ByteBuffer testInput = ByteBuffer.allocate(1024); + doTestInputNeedRead(testInput); + } + + + @Test(expected=IOException.class) + public void testInputMalformed01() throws IOException { + ByteBuffer testInput = ByteBuffer.allocate(1024); + + // TLS handshake + testInput.put((byte) 22); + // TLS 1.0 + testInput.put((byte) 3); + testInput.put((byte) 1); + // Record length 0 (correct, but not legal) + testInput.put((byte) 0); + testInput.put((byte) 0); + + doTestInputNeedRead(testInput); + } + + + @Test(expected=IOException.class) + public void testInputMalformed02() throws IOException { + ByteBuffer testInput = ByteBuffer.allocate(1024); + + // TLS handshake + testInput.put((byte) 22); + // TLS 1.0 + testInput.put((byte) 3); + testInput.put((byte) 1); + // Record length 4 + testInput.put((byte) 0); + testInput.put((byte) 4); + // Type 1 (client hello) + testInput.put((byte) 1); + // Client hello size 0 (correct, but not legal) + testInput.put((byte) 0); + testInput.put((byte) 0); + testInput.put((byte) 0); + + doTestInputNeedRead(testInput); + } + + + public void doTestInputMalformed(ByteBuffer input) throws IOException { + TLSClientHelloExtractor extractor = new TLSClientHelloExtractor(input); + // Expect this to fail + extractor.getResult(); + } + + + public void doTestInputNeedRead(ByteBuffer input) throws IOException { + TLSClientHelloExtractor extractor = new TLSClientHelloExtractor(input); + // Expect this to fail + ExtractorResult result = extractor.getResult(); + Assert.assertEquals(ExtractorResult.NEED_READ, result); + } +} diff --git a/test/org/apache/tomcat/util/net/TestXxxEndpoint.java b/test/org/apache/tomcat/util/net/TestXxxEndpoint.java new file mode 100644 index 0000000..654da4f --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestXxxEndpoint.java @@ -0,0 +1,116 @@ +/* + * 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.tomcat.util.net; + +import java.io.File; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.compat.JreCompat; + +/** + * Test case for the Endpoint implementations. The testing framework will ensure + * that each implementation is tested. + */ +public class TestXxxEndpoint extends TomcatBaseTest { + + @Test + public void testStartStopBindOnInit() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File appDir = new File(getBuildDirectory(), "webapps/examples"); + tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + + tomcat.start(); + + int port = getPort(); + + tomcat.getConnector().stop(); + Exception e = null; + try (ServerSocket s = new ServerSocket(port, 100, InetAddress.getByName("localhost"))){ + } catch (Exception e1) { + e = e1; + } + if (e != null) { + log.info("Exception was", e); + } + Assert.assertNotNull(e); + tomcat.getConnector().start(); + } + + @Test + public void testStartStopBindOnStart() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Connector c = tomcat.getConnector(); + Assert.assertTrue(c.setProperty("bindOnInit", "false")); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + + + tomcat.start(); + int port = getPort(); + + tomcat.getConnector().stop(); + Exception e = null; + try (ServerSocket s = new ServerSocket(port, 100, InetAddress.getByName("localhost"))) { + // This should not throw an Exception + } catch (Exception e1) { + e = e1; + } + Assert.assertNull(e); + tomcat.getConnector().start(); + } + + @Test + public void testUnixDomainSocket() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Connector c = tomcat.getConnector(); + + if (!c.getProtocolHandlerClassName().contains("NioProtocol") || !JreCompat.isJre16Available()) { + // Only the NIO connector supports UnixDomainSockets + // and only when running on Java 16+ + return; + } + + File tempPath = File.createTempFile("uds-tomcat-test-", ".sock"); + String unixDomainSocketPath = tempPath.getAbsolutePath(); + // Need to delete the file to make way for the actual socket + Assert.assertTrue(tempPath.delete()); + Assert.assertTrue(c.setProperty("unixDomainSocketPath", unixDomainSocketPath)); + tomcat.start(); + + // Connect to the domain socket as a client + SocketAddress sa = JreCompat.getInstance().getUnixDomainSocketAddress(unixDomainSocketPath); + ByteBuffer response = ByteBuffer.allocate(1024); + try (SocketChannel socket = JreCompat.getInstance().openUnixDomainSocketChannel()) { + socket.connect(sa); + socket.write(ByteBuffer.wrap("OPTIONS * HTTP/1.0\r\n\r\n".getBytes())); + socket.read(response); + } + + Assert.assertTrue((new String(response.array(), 0, response.position()).startsWith("HTTP/1.1 200"))); + } +} diff --git a/test/org/apache/tomcat/util/net/TesterSupport.java b/test/org/apache/tomcat/util/net/TesterSupport.java new file mode 100644 index 0000000..5d56e49 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TesterSupport.java @@ -0,0 +1,667 @@ +/* + * 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.tomcat.util.net; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Assume; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.authenticator.SSLAuthenticator; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.AprLifecycleListener; +import org.apache.catalina.core.OpenSSLLifecycleListener; +import org.apache.catalina.core.StandardServer; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.jni.Library; +import org.apache.tomcat.jni.LibraryNotFoundError; +import org.apache.tomcat.jni.SSL; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.jsse.JSSEImplementation; +import org.apache.tomcat.util.net.openssl.OpenSSLImplementation; + +public final class TesterSupport { + + public static final String SSL_DIR = "test/org/apache/tomcat/util/net/"; + public static final String CA_ALIAS = "ca"; + public static final String CA_JKS = SSL_DIR + CA_ALIAS + ".jks"; + public static final String CLIENT_ALIAS = "user1"; + public static final String CLIENT_JKS = SSL_DIR + CLIENT_ALIAS + ".jks"; + public static final String LOCALHOST_EC_JKS = SSL_DIR + "localhost-ec.jks"; + public static final String LOCALHOST_RSA_JKS = SSL_DIR + "localhost-rsa.jks"; + public static final String LOCALHOST_KEYPASS_JKS = SSL_DIR + "localhost-rsa-copy1.jks"; + public static final String JKS_PASS = "changeit"; + public static final String JKS_PASS_FILE = SSL_DIR + "keystore-password"; + public static final String JKS_KEY_PASS = "tomcatpass"; + public static final String JKS_KEY_PASS_FILE = SSL_DIR + "key-password"; + public static final String CA_CERT_PEM = SSL_DIR + CA_ALIAS + "-cert.pem"; + public static final String LOCALHOST_EC_CERT_PEM = SSL_DIR + "localhost-ec-cert.pem"; + public static final String LOCALHOST_EC_KEY_PEM = SSL_DIR + "localhost-ec-key.pem"; + public static final String LOCALHOST_RSA_CERT_PEM = SSL_DIR + "localhost-rsa-cert.pem"; + public static final String LOCALHOST_RSA_KEY_PEM = SSL_DIR + "localhost-rsa-key.pem"; + public static final boolean OPENSSL_AVAILABLE; + public static final int OPENSSL_VERSION; + public static final String OPENSSL_ERROR; + public static final boolean TLSV13_AVAILABLE; + + public static final String ROLE = "testrole"; + + private static String clientAuthExpectedIssuer; + private static String lastUsage = "NONE"; + private static Principal[] lastRequestedIssuers = new Principal[0]; + + static { + boolean available = false; + int version = 0; + String err = ""; + try { + Library.initialize(null); + available = true; + version = SSL.version(); + Library.terminate(); + } catch (Exception | LibraryNotFoundError ex) { + err = ex.getMessage(); + } + OPENSSL_AVAILABLE = available; + OPENSSL_VERSION = version; + OPENSSL_ERROR = err; + + available = false; + try { + SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_3); + available = true; + } catch (NoSuchAlgorithmException ex) { + } + TLSV13_AVAILABLE = available; + } + + public static boolean isOpensslAvailable() { + return OPENSSL_AVAILABLE; + } + + public static int getOpensslVersion() { + return OPENSSL_VERSION; + } + + public static boolean isTlsv13Available() { + return TLSV13_AVAILABLE; + } + + public static void initSsl(Tomcat tomcat) { + initSsl(tomcat, LOCALHOST_RSA_JKS, null, null, null, null); + } + + protected static void initSsl(Tomcat tomcat, String keystore, + String keystorePass, String keystorePassFile, String keyPass, String keyPassFile) { + + Connector connector = tomcat.getConnector(); + connector.setSecure(true); + Assert.assertTrue(connector.setProperty("SSLEnabled", "true")); + + SSLHostConfig sslHostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED); + sslHostConfig.addCertificate(certificate); + connector.addSslHostConfig(sslHostConfig); + + String sslImplementation = System.getProperty("tomcat.test.sslImplementation"); + if (sslImplementation != null && !"${test.sslImplementation}".equals(sslImplementation)) { + StandardServer server = (StandardServer) tomcat.getServer(); + AprLifecycleListener listener = new AprLifecycleListener(); + listener.setSSLRandomSeed("/dev/urandom"); + server.addLifecycleListener(listener); + Assert.assertTrue(connector.setProperty("sslImplementationName", sslImplementation)); + } + sslHostConfig.setSslProtocol("tls"); + certificate.setCertificateKeystoreFile(new File(keystore).getAbsolutePath()); + sslHostConfig.setTruststoreFile(new File(CA_JKS).getAbsolutePath()); + if (keystorePassFile != null) { + certificate.setCertificateKeystorePasswordFile(new File(keystorePassFile).getAbsolutePath()); + } + if (keystorePass != null) { + certificate.setCertificateKeystorePassword(keystorePass); + } + if (keyPassFile != null) { + certificate.setCertificateKeyPasswordFile(new File(keyPassFile).getAbsolutePath()); + } + if (keyPass != null) { + certificate.setCertificateKeyPassword(keyPass); + } + } + + protected static KeyManager[] getUser1KeyManagers() throws Exception { + KeyManagerFactory kmf = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(getKeyStore(CLIENT_JKS), JKS_PASS.toCharArray()); + KeyManager[] managers = kmf.getKeyManagers(); + KeyManager manager; + for (int i=0; i < managers.length; i++) { + manager = managers[i]; + if (manager instanceof X509ExtendedKeyManager) { + managers[i] = new TrackingExtendedKeyManager((X509ExtendedKeyManager)manager); + } else if (manager instanceof X509KeyManager) { + managers[i] = new TrackingKeyManager((X509KeyManager)manager); + } + } + return managers; + } + + protected static TrustManager[] getTrustManagers() throws Exception { + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(getKeyStore(CA_JKS)); + return tmf.getTrustManagers(); + } + + public static ClientSSLSocketFactory configureClientSsl() { + ClientSSLSocketFactory clientSSLSocketFactory = null; + try { + SSLContext sc; + if (TLSV13_AVAILABLE) { + sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_3); + } else { + sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_2); + } + sc.init(getUser1KeyManagers(), getTrustManagers(), null); + clientSSLSocketFactory = new ClientSSLSocketFactory(sc.getSocketFactory()); + javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(clientSSLSocketFactory); + } catch (Exception e) { + e.printStackTrace(); + } + return clientSSLSocketFactory; + } + + private static KeyStore getKeyStore(String keystore) throws Exception { + File keystoreFile = new File(keystore); + KeyStore ks = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(keystoreFile)) { + ks.load(is, JKS_PASS.toCharArray()); + } + return ks; + } + + protected static boolean isClientRenegotiationSupported(Tomcat tomcat) { + // Disabled for Tomcat Native (part of response to CVE-2009-3555) + // Only JRE provided JSSE implementation supports this + String sslImplementation = (String) tomcat.getConnector().getProperty("sslImplementationName"); + if (!JSSEImplementation.class.getName().equals(sslImplementation)) { + return false; + } + + return true; + } + + public static void configureSSLImplementation(Tomcat tomcat, String sslImplementationName, boolean openSSL) { + if (openSSL) { + LifecycleListener listener = null; + if (OpenSSLImplementation.class.getName().equals(sslImplementationName)) { + listener = new AprLifecycleListener(); + Assume.assumeTrue(AprLifecycleListener.isAprAvailable()); + } else { + listener = new OpenSSLLifecycleListener(); + Assume.assumeTrue(OpenSSLLifecycleListener.isAvailable()); + } + StandardServer server = (StandardServer) tomcat.getServer(); + server.addLifecycleListener(listener); + } + Assert.assertTrue(tomcat.getConnector().setProperty("sslImplementationName", sslImplementationName)); + } + + public static void configureClientCertContext(Tomcat tomcat) { + initSsl(tomcat); + + /* When running on Java 11, TLSv1.3 is enabled by default. The JSSE + * implementation of TLSv1.3 does not support + * certificateVerification="optional", a setting on which these tests + * depend. Therefore, force these tests to use TLSv1.2 so that they pass + * when running on TLSv1.3. + */ + tomcat.getConnector().findSslHostConfigs()[0].setProtocols(Constants.SSL_PROTO_TLSv1_2); + + // Need a web application with a protected and unprotected URL + // No file system docBase required + Context ctx = tomcat.addContext("", null); + + Tomcat.addServlet(ctx, "simple", new SimpleServlet()); + ctx.addServletMappingDecoded("/unprotected", "simple"); + ctx.addServletMappingDecoded("/protected", "simple"); + + // Security constraints + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded("/protected"); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + ctx.addConstraint(sc); + + // Configure the Realm + TesterMapRealm realm = new TesterMapRealm(); + + // Get the CA subject the server should send us for client cert selection + try { + KeyStore ks = getKeyStore(CA_JKS); + X509Certificate cert = (X509Certificate)ks.getCertificate(CA_ALIAS); + clientAuthExpectedIssuer = cert.getSubjectX500Principal().toString(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + String cn = "NOTFOUND"; + try { + KeyStore ks = getKeyStore(CLIENT_JKS); + X509Certificate cert = (X509Certificate)ks.getCertificate(CLIENT_ALIAS); + cn = cert.getSubjectX500Principal().toString(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + realm.addUser(cn, "not used"); + realm.addUserRole(cn, ROLE); + ctx.setRealm(realm); + + // Configure the authenticator + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("CLIENT-CERT"); + ctx.setLoginConfig(lc); + ctx.getPipeline().addValve(new SSLAuthenticator()); + + // Clear the tracking data + lastUsage = "NONE"; + lastRequestedIssuers = new Principal[0]; + } + + protected static String getClientAuthExpectedIssuer() { + return clientAuthExpectedIssuer; + } + + protected static void trackTrackingKeyManagers(@SuppressWarnings("unused") KeyManager wrapper, + @SuppressWarnings("unused") KeyManager wrapped, String usage, Principal[] issuers) { + lastUsage = usage; + lastRequestedIssuers = issuers; + } + + protected static String getLastClientAuthKeyManagerUsage() { + return lastUsage; + } + + protected static int getLastClientAuthRequestedIssuerCount() { + return lastRequestedIssuers == null ? 0 : lastRequestedIssuers.length; + } + + protected static Principal getLastClientAuthRequestedIssuer(int index) { + return lastRequestedIssuers[index]; + } + + protected static boolean checkLastClientAuthRequestedIssuers() { + if (lastRequestedIssuers == null || lastRequestedIssuers.length != 1) { + return false; + } + return (new X500Principal(clientAuthExpectedIssuer)).equals( + new X500Principal(lastRequestedIssuers[0].getName())); + } + + public static final byte DATA = (byte)33; + + public static class SimpleServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + if (req.isUserInRole(ROLE)) { + resp.getWriter().print("-" + ROLE); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Swallow any request body + int read = 0; + int len = 0; + byte[] buffer = new byte[4096]; + InputStream is = req.getInputStream(); + boolean contentOK = true; + while (len > -1) { + len = is.read(buffer); + read = read + len; + for (int i=0; i}9>T`V6cQdZq@J3=C|m4Vu`N8#J->FJNY3WMblE z*jIY&T}x`-HUnNZPOUbNw(q=*jNGgY291*qxeYkkm_u3EgqcEvVH^%&CSONGAp-%B z47)IAcw%;HT4riailMrJDoBh=SS}atjMW6@}#ICMT9C1o%1|${EOj3z>EpmN#_KJV9T5OU~u{x99$`RMwAYl|SG5b2(j+#fNLn8*8=$@0_%Ac5X~s`h5QcZu_&pg{_5| zH<@{^{k_NI>Jz*3*&-%ee6+XCUX-ss`;Tbd#fF%U)tx&huX?F*%<50sMCRswSGo1$ zR=!(Oq**2Q^YzTGldG~moXLz0im~Wh`1DFsy84`NpQAsYj|AC|1p-OG{N9ChC=4e@cdp2dgn1$&wRohQ<)e`4-^`E?%ICsUay>r(o z$;!-Fy02L4o0|M}-es9T!+STirT*S#%z4&l+TlYR<0jg#aCN-RS$ds0VTaQn&Sgcv ztG9G6Se47f%*epFxbe3^;|~K_VEUKkV-aH!VfZw|S5b$-#$k5LmwQ?t)ZQ8K%{OQ~ z0g_i{X*^)ixVHhTnuU!smo!croLQj>sled@`9zq7)qt6i@jr6@02V*M{K3e;RQPDm z^L4lGbe=zY$$iodky&S4TxUuKSp{C%D5&y#`K$Xgswdri)Fb%Dwx(O)+s?yxDpyR4 z&(1k2rLjnEiDPH2%ZC|W?kgS_)*ika++4FuEjCGAj*+>T*|(_r&5~Tfz}m$@a?h4W zY%Q)`eeGPSy#ot-&olo;Gv6^t`bllr<`w@UM102b&{)Rk`h)zJ-?fWPd>GytBlu32 zCn>%7=+Cb`4|iuh_;j(xD!HKWxNzJJC;2A($2`mO`DWj)iqXi)kviq?60Hr=C0Qg>I(0S)A;c zVCiG;T@zLIz2S_=Il)a)b6fi@Hiq%*d!LIGo+JLqv7dkC$DarIc~;%+FF91US5&Si zbU~)^K8?e@|Mq3va9@7zYNGN?UXD_~<^-O=ceCrZbyio;f0enrdWVkT_TregGd&(i z?Ns`?MSpJ0yhN>R)8wsf-7DTp-+Ub*w%y9KpZktAtBJYjT89G)CfnjBzK--@`u4d_ zyv^h5Hsf!ycJ8%5CGz+txF+={`M+m%n)28+X^O%0`iyy&@(w}Q?(`W{mWo}xpz=JQ zd;8t;b#1GpTbDKD%}`U0U5B%+CcYpD;3fzA;qoWJ}SF*;y literal 0 HcmV?d00001 diff --git a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java new file mode 100644 index 0000000..d5f9b29 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java @@ -0,0 +1,142 @@ +/* + * 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.tomcat.util.net.jsse; + +import java.io.File; +import java.io.IOException; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestPEMFile { + + private static final String KEY_PASSWORD = "changeit"; + private static final String KEY_PASSWORD_FILE = "key-password"; + + private static final String KEY_PKCS1 = "key-pkcs1.pem"; + private static final String KEY_ENCRYPTED_PKCS1_DES_CBC = "key-encrypted-pkcs1-des-cbc.pem"; + private static final String KEY_ENCRYPTED_PKCS1_DES_EDE3_CBC = "key-encrypted-pkcs1-des-ede3-cbc.pem"; + private static final String KEY_ENCRYPTED_PKCS1_AES256 = "key-encrypted-pkcs1-aes256.pem"; + private static final String KEY_ENCRYPTED_PKCS8_HMACSHA1DEFAULT_DES_EDE3_CBC = "key-encrypted-pkcs8-hmacsha1default-des-ede3-cbc.pem"; + private static final String KEY_ENCRYPTED_PKCS8_HMACSHA256_AES_128_CBC = "key-encrypted-pkcs8-hmacsha256-aes-128-cbc.pem"; + private static final String KEY_ENCRYPTED_PKCS8_HMACSHA256_AES_256_CBC = "key-encrypted-pkcs8-hmacsha256-aes-256-cbc.pem"; + private static final String KEY_ENCRYPTED_PKCS8_HMACSHA256_DES_EDE3_CBC = "key-encrypted-pkcs8-hmacsha256-des-ede3-cbc.pem"; + + @Parameterized.Parameters + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { KEY_PASSWORD, null }); + parameterSets.add(new Object[] { null, KEY_PASSWORD_FILE }); + parameterSets.add(new Object[] { KEY_PASSWORD, KEY_PASSWORD_FILE }); + + return parameterSets; + } + + + @Parameter(0) + public String password; + + @Parameter(1) + public String passwordFile; + + + @Test + public void testKeyPkcs1() throws Exception { + testKey(KEY_PKCS1, null, null); + } + + + @Test + public void testKeyPkcs1WithUnnecessaryPassword() throws Exception { + testKey(KEY_PKCS1, "ignore-me", null); + } + + + @Test + public void testKeyEncryptedPkcs1DesEde3Cbc() throws Exception { + testKeyEncrypted(KEY_ENCRYPTED_PKCS1_DES_EDE3_CBC); + } + + + @Test + public void testKeyEncryptedPkcs1DesCbc() throws Exception { + testKeyEncrypted(KEY_ENCRYPTED_PKCS1_DES_CBC); + } + + + @Test + public void testKeyEncryptedPkcs1Aes256() throws Exception { + testKeyEncrypted(KEY_ENCRYPTED_PKCS1_AES256); + } + + + @Test + public void testKeyEncryptedPkcs8HmacSha1DefaultDesEde3Cbc() throws Exception { + testKeyEncrypted(KEY_ENCRYPTED_PKCS8_HMACSHA1DEFAULT_DES_EDE3_CBC); + } + + + @Test + public void testKeyEncryptedPkcs8HmacSha256Aes128() throws Exception { + testKeyEncrypted(KEY_ENCRYPTED_PKCS8_HMACSHA256_AES_128_CBC); + } + + + @Test + public void testKeyEncryptedPkcs8HmacSha256Aes256() throws Exception { + testKeyEncrypted(KEY_ENCRYPTED_PKCS8_HMACSHA256_AES_256_CBC); + } + + + @Test + public void testKeyEncryptedPkcs8HmacSha256DesEde3Cbc() throws Exception { + testKeyEncrypted(KEY_ENCRYPTED_PKCS8_HMACSHA256_DES_EDE3_CBC); + } + + + private void testKeyEncrypted(String file) throws Exception { + testKey(file, password, passwordFile); + } + + + private void testKey(String file, String password, String passwordFile) throws Exception { + PEMFile pemFile = new PEMFile(getPath(file), password, getPath(passwordFile), null); + PrivateKey pk = pemFile.getPrivateKey(); + Assert.assertNotNull(pk); + } + + + private String getPath(String file) throws IOException { + if (file == null) { + return null; + } + String packageName = this.getClass().getPackageName(); + String path = packageName.replace(".", File.separator); + File f = new File("test" + File.separator + path + File.separator + file); + + return f.getCanonicalPath(); + } +} diff --git a/test/org/apache/tomcat/util/net/jsse/TesterBug50640SslImpl.java b/test/org/apache/tomcat/util/net/jsse/TesterBug50640SslImpl.java new file mode 100644 index 0000000..478bbfa --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/TesterBug50640SslImpl.java @@ -0,0 +1,40 @@ +/* + * 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.tomcat.util.net.jsse; + +import org.apache.tomcat.util.net.Constants; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLUtil; + +public class TesterBug50640SslImpl extends JSSEImplementation { + + public static final String PROPERTY_VALUE = "magic"; + + + @Override + public SSLUtil getSSLUtil(SSLHostConfigCertificate certificate) { + SSLHostConfig sslHostConfig = certificate.getSSLHostConfig(); + if (sslHostConfig.getProtocols().size() == 1 && + sslHostConfig.getProtocols().contains(PROPERTY_VALUE)) { + sslHostConfig.setProtocols(Constants.SSL_PROTO_TLSv1_2); + return super.getSSLUtil(certificate); + } else { + return null; + } + } +} diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes256.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes256.pem new file mode 100644 index 0000000..ce7df89 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes256.pem @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,27A5EA847546F673F91037D067581427 + +OkmT+rurr+LlEAMBYUfHBm1Phv5+bP3891iXciYozdQrus7Nhk2ZD1RX8fCL9A2a +8I/8/0KRgOvPXbi9ZioBMSCRGObjT1qFXnviN9hQ+6dXrYRDjALkvV3I1mUmxF/H +Q3PE6M8uEt2X0ioiyzmbrD3Lrmd6lfuQ7dPpWUQ2zrnUNslF1DgaGyw6FvhWpzqW +PVN+lTdvHsv1HxkwW5npWYSF97d55D93k6FyIB09Ns/jh6snuM7iGzU7OeAT5jz7 +XBprlNQhFkZGVu0qq/cYjDkrPqX3dhEy89zQ0Rv7Fff5IsIumPY8vIkpF0k9PCVp +tqYhXAt8Tl9TR8cu+fdzgVkkCR2vutMPPjArp5wrd9jrmHNek4jlknsE2rvQACvo +gfJu0o+Ll5d/+BHE7pvB3z9QeqUKUshm//8XzOY76IfzbASwZI6V1EejLx+kc+u6 +/fr25xWw3Fzojq2uME+lh+JrFgial4eNY4EMP4uP6nqH7Gri5toonbW+yUC+J9oH +mbm+T4VGsrYBja2L0ckLOURCCX9gpZn8JLpLiuh1rN+opVpEy4qi0/2cvXDlLJVw +igCBUen8q4Yv7uubD60uxgnKXqO/SE26LOn+NHC+p/ESDh4Qhob2ejrgckX6wAyP +tMcqBYHDI9cKgMlFjR3a8aJ15pedUOQONyCnfE1e6CHduJta7Geqh/eQuP1lf3/B +IpD6WieNzLYr6Yg/5iCUoPnLdcmP5hWHcBAukSNNWQKIV7aKD86vwl5WU+tag2hx +0eMSw9VeZeWx/03plDro2EeltNPsycHcB8n7MhgrdiyS3Z0DNeSqh0thjMr4GWnh +-----END RSA PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-des-cbc.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-des-cbc.pem new file mode 100644 index 0000000..54c16d3 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-des-cbc.pem @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-CBC,B516994DD363C185 + +sG4BzUe/TJCntL2nizUtbLjPzsW6+SIRgwC07V1iJs1JeYw7SsobfHl4DiAKO9CW +86DT9hd4XCgCYGPvK6vUpFLk6Ms05eRrUx00clUk29MD18radLzwJyalYrbhXGOW +6m+dHm4dKXwpy9CFWudIFpotsCRPZnTiEa5/A2yjIyMFz/iAN4V+sGLLQJHjO/Nv +Cf5saTaRri5goYAIIVgetBx6D4B+dR8IqUdcfry3yhPAtxsBPd4QN5+wjLuf8mCV +LCG13+tfG1uM0FP8srAc/b1B+DDexsRwb/sLUDABofsmme7mRsUyZlm6dGbDuchx +RyMRzgBy5zmp0Okxafi94jInT9uiaaHFgn+BRpnA+rSoBNHtVEAPpHpeI2l3LYjJ +XPeobWgMS4e80kZJdOrwHEpsc0z5jUHdd/0puzoJs/H5UqeNRW+1Rgr5kPiIEj/H +JHc8xgKHumDfejxF3jhIIGOXHuav0PaCxR30t2aCz28wS8+c/j3EWzzA7Mfn8XTL +jxn3pUa57iDYcnmvjWk9KkZgnUBabIW3Q5CTK9UE2eIHEVoKYitXQ/DUoFGe94LB +Ip+7lSnfn/Cp5HhP1mhFcYIx6OfDCXTsbf1wu86Qy6ykHluYDRx3lQHxVzO7uJ3x +Mb1MzeqG4ojmlWl+Siew2fuinbDvH48sR+R9Oc/Xa6H/2UFwWkq40Z9fqERTumU5 +ES3KN1ah9RLSeAJWryOF4ngw3mywgPGjFGxBURXcQ8gQSG0zizEpvPWTataDp/3I +P/2V2YIjD1fH77DKBFfVjGvnL0mgQ7vPqzF3+cFHFB2XV17MehqkLA== +-----END RSA PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-des-ede3-cbc.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-des-ede3-cbc.pem new file mode 100644 index 0000000..2746f9d --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-des-ede3-cbc.pem @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,23913AE126BDAFA4 + +nyKtTN/WFNPLUujp5ZSg2iPkLZ2y6yCbzr/tXjzTvxJNzw+XwfZepUc3pYFpNNdI +QVGrgXngjubQAk9E3m2xmlV+70hvT7huqrKaQ1ForySuCk8xCGyA8xedFFs9o5hB +mIttqRYjKcFhIqdmZ1XfS/ecenghiLB/e2cpN6jEXQQXfM79soAQUb4VZx72aIjq +vYMJbro/InOcz7xz+PH1dnP/5Z4t/Ze5Bp1v23PwXaqMBQeP4buQhfLjtGqn0Kr8 +N/EW+88TEhD/BL42FeJCBfMMAkZb+QGu10qtOx6LN1ijeuI+sY0Vzbu2orgoXNL5 +1iyagdeCfmMqEGUhIHlPTkmbf5L0f9Y7rfrno33obC5cNH7x9Kpfg95ORz+GKk6H +VVXBR8aUgWHVxoEVnBSWO0karQ6yFXZq6rfiA9Y8qisi3uTL7jmdVaii4fDfGzhu +JThC+HeZMoFebrS1bj1ak8D/imDZRpf7ww7jmrvmZhTpVVn9z/6MnQ9yziUb9eM+ +0HyUx4aTKbKqUSZQnV1QByg9eqYHv1pZcd3lvPy0MCWixYYFU76tone0iSQRUb8K +1b6Onlyi479Bbgwx/QNi7vvQo5CpPwqLJe1zrwrZ1iwV+FdNZ96QAew1dHgah/1E +s8ZYYyPMNxE3KiMXFpdh2JoakvFA+FM2c51Jh5joY2rCc8rKwa1o9lElNlMaGY4q +bTx88a6Wdn0dsWU3iuZ6EavquLnAYD2qvkbVs0LCDTBifplTYy/2KcZwKh2MIFXh +IxsatIfnVnR3joR3qzarkIs74GyDBGHAqRMJ/eFWwaEaG6P68Wwq8w== +-----END RSA PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha1default-des-ede3-cbc.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha1default-des-ede3-cbc.pem new file mode 100644 index 0000000..8dbbe78 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha1default-des-ede3-cbc.pem @@ -0,0 +1,54 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIJjjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIv8Q75ZYGJvACAggA +MBQGCCqGSIb3DQMHBAg1OK0wCzoApASCCUhArEqtGLPypOPIuQ/r6KD1MIZpkhei +tzvdPUN0H00oUECBHhWS4r5xuVEOrNVzNJzuDrvpcZL58i9CagWenWc1qOCi+WFL +/bCkkjirNIOIFMdBwI0A/L0vqhfJETjJEFD5lr/jQF4H3KgaQNO6Eq6c+TTkgD7T +TN340iX6RkS5fHWnZBWz1cmjzKNHSwYP+92eJTfqZhXhCx13nMZig3VAsS8eZjIx +Q83Tlhxz5AG8fbe5+WIxbndwM6mIUbHNzp0IA5+QW5CA80BaJZw/bAvfCKt6hGsv +1d1xwKMXzIbbBbfPDKJVrMjSEaKUyoedxZdiI+607LdDYGQHtqB2lVuHBrc18EWF +0VvOBIZptmB4a0uGsPTAWlf1ZbnFoVOUovuaKwhhD5tCIw53QiW+/dGTlAxXj+1Y +fSnoDREEfKXQY88xXNALg118px3+Wc5i3QFxvlTsOYKvOEtr0hsuFUUBE2+R8ycI +hjQ7zZoR2e0Em7B1UG50GhleWzpoc90W7uSEL5+GQ4RZ053EPkX5fqLphAFfdFBl +ndJI0uNsb2jsaQ/S/UNjt6JqTRJfFQTW7jpGb9K/y2QfRq862h2k5kuw0zMvOV2V +GTpNJx8bPswB10+//3+OMfUTYFtCgw0TM/uvbMr5AY3KzxBBm+/eLO4CRUADJhB4 +lYbcXHHIIJ+I8WAKNz1kJcIdvrhxoUmMNjwvBPbssAWjLqpGXODVNs2FD3ophe0o +IZejwNA9CFL+ItiRtE8c10v0Y9x4IIjig2pdURHEBtDT18m2ljY/paoUy8cQ9sGa +NlEsYMLJK9v1jkK/iJyngHvo7BOhgEQlGR2pudqwXAcKsGgbShLaDY2pZ02U0VtE +RrzqQkSYcKVzf/KnbDC3DT+FPuhH06umT3kqm6BraFePasFUqkNa29GW7oQU4UQi +lMm3gz+B2okB5EbccqfjWO0TALdOCigbrlHKh7h1q9JiXIL4dkpCWiXWFIwOcA81 +hbg08oBZ0q+zaeWIRsnNZ20nLeeNPh4sOBFcE8y7RBN8OdoeDLegWgkL+hcBf5xb +md6vDzNWUGKs+4W2nQQM9O1cniru2JAb7Jf3AL+aZQfwa4SRas9nBGAx+6Kb8YN6 +yapQcSKIqKA5N5ENWIv+xiG40DIERTA9dRQgl1f9usW2hopbQWafWNmR+kx5GmO5 +tbDtMEXhUa/tKFNMvAVh86T09QpehEGh+xsPrqfFwUs8mJ313hTzVESf8MkMJUaZ +E0sUpMmoF6abwvLtBxs51vR1bwaLXKZzDwSa8AQQuue8skw1UGhuTZ2lLDngFmrx +rvqeFv+mH9hd+PmTgsEiKY3NZYhkEiPip0mFvh76CHlPRp1mHlJ0UtRDzOJ65PAp +57BLaLtXAzHbPjHwcNQHRRzwzlgHOw3KiZ7d07i/0Cltm/FMdfbR+yokoBRWEzrP +HKYOS5fYOj3gKh20nijmMvtNB3dpiSOn94GgGkLza0bW+/JUkpAfMSihbGdFWCa1 +boC+jhuiAi2YnO4n7FCYEG+IBHJtmg9WrUCRgSfUeiTVBys1kHUQJgzmEcUABUBN +rwCZyv3flKZNwYwNRLtUyMrFqOwt9rQEnRL3spBrJsbnnSIusAOk2Y/ETWrzaKo3 +Bl+XCJI5JG8SqdvxL4+qDl1EBpOBsSTP1DeqjJfKAoNWVS7rN6FvM0C0+1dVsYHE +EsuatZkyvYuEEQmSVc6o6YNKWNd/VnVJoc9XCuhC6ChgFJPK8oXWnOcYNd1XyO6X +6aj/5T5g4k1ShOQs/ND6ceqwvmhyZdXXKZ6EHbLu/sTZyG5/uSdq9moavD5uyQ2d +HGMdeqcmyE8gMbixtsdJvpJeiLvoMN+biB/vyN+HPuNGZyRQYKQrbq/aK8W6Yt+C +0GyUuMBs/bHXi+0p+/lYk/wEdcWsgF8uXNQDOnSrecbZt+3zuShKEINLk+T5z+aH +8dBDmTf+92ysbWiKy8Xy+/vUCV3TNbn/2VN0YIEmZx5GDo4PbJ33eO01aahhuA90 +EBDZe/FV+7B02LyHOSGni9TKe337rXlq4JVuZN+Ka5Pk9hoeF4dRtUNl9Ixc4n6O +1esHJh2nez9z3xdMEk6nbxMtDchJrd4lzUE8cHG/z2ucPFnid4KHnmyX8Vq+NHvP +mpLzwGGKOrnGdOguGU8sIXoQGQ39SQm27cOMt1dlY2iAErdSsHn+mS4C1lEsFfio +3f00LJnaAvLXaiIYE6VxiR3ckr19OtZXta08tt+UeLRg3ZGTTH7wXGqtbMyze59G +EaPjAn2GYyT8zPI219oQuDYApMspuQ+PXFsKZSZPZ4x9xxYcBiHSECLLiYBYzfEY +mW4a/6GTE/TtWZwktUp/bg8Sa/AizjRE5akI2CcX90Il3aBTvqDMwJPlXsU3Ia6K +rBRhurEu7J4dyl0DlsNXM0/EyBvoiquyETEA9EdQgoWu9FY5cQNCMMs3bRsMCfeA +sX5mfYPHlYHM2y8r5GEedUvi3HpJBti+RU9t3LrCTid/A8g7zXHVR3iwxCazXkyq +fRsg/JXWX/wRK3r4oO6tkIy5qU2X8rStTQAXVbEr422K4NZMvn8g7RlGZcuETfRj +WexxUX01s0OfT+EWJZE7URvAwiIBsCUzZubpGlN0ybH0v0NdvNM0W2hUTQCtrGYh +qb37BY8XxcC62nMKhJc6x477Z604htFgdnUlgMhOx4DXEgvYZUTNpTdtTJCoXcu0 +cbllT1LWUJ8yqyrQXVUEOZqI5WMtLHHzqFriM5KZV1S8VXY1vAQYyQJfEgcg23ZM +I8l8MMcOitDf1mvjVBpnFHBAc0gdFBJ0ACKL4YgetgafejQDCnkDrUaoMDd8ROX1 +bLuhB8R36e/0wd2P1Dtd/D11BsgPnJhjgvuIBgDlOglp75nZKrJddhEJEqeqj7/0 +mNKWf0M5uHRRctdkVxkznK6ck+jIMkqy1rGEzAeWDeZc+M9nwl0InVppttOQs8Ip +TJNfivVugQJExlEfUGQuM9tX1oVDKLCmJlIjCLJHt1zXpSgi7iSsXGOQnxDczHkO +wJVA7NQh1+De8gzVjT6TIPn2jp17d/9uni3vu4icAE9W5f/V7QkvzM4Oa+qIPpmU +pMEkkjUnAoH9UmvVmnNp0VoUMNFG9JfdGdzU6Eu8o3ElwArz5PWkXkcVJJx78gry +dmk= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-aes-128-cbc.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-aes-128-cbc.pem new file mode 100644 index 0000000..dfefd75 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-aes-128-cbc.pem @@ -0,0 +1,54 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI/ze5ju8ZpdwCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBCKvOamzivBoPc0g4FihbH6BIIJ +UNPyodBA3cPJ2FJ+4dKYCb/AXj0JuOZ7fJQi1atYvpX8ADBUHFoBFittr7WGfVpl +DNny63vhwnubewE17xuurpItncD8NPxxiWBklgrca+jM15E8WOKBlyfMExilxtIL +SyIIFd3nbkUbGTAExwBEbZisHuJSkZemP/oBinVAoYVT7kbWV0NRFbBt7q4peeJt +nptBTxNhaYZrXH3ydVUYTDSSMLASe1J1tlfH91qsVz2TUbL0g19LWk74Lolmkjym +o4DZXJqM+ZCuHaSZwd1tihEdwUU7so/t4/LGH4mx1rpjchQHiK2J33+eosScSlcx +tMnfuYRlEC34n8MPyIHFbhCvye0NovsQfQ6waK+uJMO3ZBfd0gIRE/23JmNSoh7c +6uHwZhxM6N0DOAviRdd/dBPEALreB8lD2MmxJ4Fs/RxjYjAE1R9jGSoMLk6wbnx3 +UxlM/iBo31S1M48JwDctUYU7LBUb0Kzg/C7fxSJCUJDE2OXlY732voQ8kvqKwggz +EOzP3Mt9MsLZazlnDrssTb3mzDPsENW7U4SW6A5CDbgQkAuSRnivjcrU+GPV//Us +W/PEQAM2m/hA9kBipvcoXuK+onc+TQqc1Ib+/roW9rr6qCC0zzFqghC0fT/gW7Qb +2MDizaAtP56i90zIaOS0SWAKbWMHtjFFiQqXVRqvPCb9POHfnS6Qxo5HGhTsDG0o +Dr5gjHFfEnkMeF5IF/Pa/vaUliN7F16KbL69YQ9P4aHxDkGtXqi6v+IAJWSua9Nt +odG4l0QCdcTiJZyOMGR8pvUo9VeJ2TOsbdpep5wdtpkCemBGGZorRjXIOv0gIssa +4UZFHcB8p0pfTRJaIUXz03JFuL8eIPTX8bGPdzlHl95qlJsLZmMJBdcUIszsphO0 +TVCj+6TLrUMkb9T6pUuwLPgPx7cdQS9b1gbmxX6N/tlFWWB4mMG4oQ0L3kO9sYoC +3/rltP9bbkOv9NG1NpLW9Vu+1A5adj7JYXcNsAm8hMLnx9tmJ8sdSPNxOnh2ggnV +Lwl5lXVer8pwZRctudYOLp9MPtySwnGiJlgMqxQapkxBfehzCKALqnTG/hUyfUx0 +mj05PTKE6FXJJAx+6nWmayrXbCs0ZI+bk1DgYvU74G+A+BkzLbf9jkXG8+f3PoOJ +aU7J2IL3GHPgy0a/D9VLBORasa1Y7+WM3dQ8Y3JWXM5jCbkMfm4iQAQJeFkR0PAa +1uBr5sL+QjhUu0YzaIj9SfuUN+jw4s0p5UoyelxXTyA0W2+2HNQ7LWSSlg1vXp+s +ooHtHpDY7ZnpIRqbN4G6h7WPwX5hRLSxE7w5xadj05m+BfbZW8J/RdcSxqwy1Qzu +khiKSoFaUneq7bXhrsLTTdnMhK2yu3Pm7ARTXgAdd7htUr8WD6ySt8jPQT8T42lv +rOLe4YoMCawNZUbKJh66um8k8Jj0EaXqVx1/o/pi+nPtni/Mph0qky3l2ezfVkjl +WMNARZ9s5c1iBq1SPyrFkIJzGT0dhvSfg38VgL2BdVd5IkgyrFvyiXby/PvUkKft +591t9/FkGBhtOYcNrSuSl48YuoB/Fqn04C8Cc4PGwbTOwRSqt+R/lIFCSdG7LsnZ +Nt4IWKNxiYMLdv4ugqCKNIdzuAJrk35gEeIOgCzDxwZT7NMEaiFuQW0/9D/j59uD +/bxcDMJHvBHjBJEPwGKopMy6CA+giMjW94XGONpx2Ko+R1tVVVHWjupZGlaIE7yP +lm//ATvrDjXhJL6rURLG0hUbJtGrsLo1NZqUY/XoXv5h0b9PTpH0AVjppfISB84O +/8yBpIcYlBlRhB7nBzQn85m7eDSM13izZ9Gjq+uX+q0ELuTPR2k6ksdFh9aNSFmF +bO/OPvTyvILTD+cQhD4BaQKa9WclqRcRPfff1nsslWResjkmNji8VKoPKeBW2UdZ +uewZnDAW++ivaZAk6WZGgrWw9UwqVNT2jRxS7tybAk+lwYoatpAr4QKXHnHqgp22 +X+twN/nD67bwhnZ1UpwK75SV/iYRcgvNaAsRltMoIm8W4GLxpUD6dlLdVLCCb3Dv +lCn81fUQkTEKEHUSfjPGooRanFOj6uoKiu2VK3tRY5U4vpJ6Ik0XTVuzCW1UFIFT +LV+mTYwqQcbQAxULVUDo1ppEcPALG14LkunNrxgtkDrDvwxSIu26HW5efEPey7Xi +JV/dcbiMvGTVruqH1r89L+RWd5FvDwm31bB7g0hGzuzRJLey3PY8y3MYHVoSoPwe +dF9ymkcIPYZ/JI8Mz/4SliSnlgzqDIfL8BD2ku7TcWrOeZcJthRPml2wfVJxKqqF ++XzQ98619GEWq2z8V5aq8IYqSV/eOIRELYPy2JNCY6w5xPLvf/00TVNb0LyWrLb1 +zdTf7TkB37BbCmvOK5YhPrJtkjqcgS9u25/CeZdTgRw6KDJv9af++LAq8qYzZIms +GAgLtxdpDPOUqryeC2fPaRXGYlwGOyePqAlmaCDpAwvfe6KnwYbAxTN7KE4Vj7qq +qspQYvDBRGZMQ76p5PdlLvYQddd++1a1OFz94jfw4mNtLw348KY+sFGt2fa6thWO +N6h63HNLoVF6ztqUxS0wtiGUAcjWE2l2jQvW0DIgBglLGfK6rfze3F2SmucYt2vZ +z+TZf0Mzgxjh47V7RpgS0tSHXEsQKl3/dz2pX9l+sOHEumEk0BgLdfBc8OXx0IYh +jXhfTNQcQsJIusrjDoZBoYjvmDyGg0FKimsagf4TbBykxgxC78HFsVFhnPp9c+tO +acb7fHK8DLG7XvWjSUDt2AM0j6LcjbOrrwGtSgZe+rbLsQ+z23U06Ipu5ZSIAZ9t +T/62ieXM8c8rdm09vTwbE3BiDWXjt12vJOXMb+MFBd31xRcZHUtU1PWQ/hROpw03 +3TPS1k8Xvpt2U9mH4XQcmIkwHo5otTJxQpUFwmCabYzESgQMca+zs1HjJHv7U4Gf +0w/72B90NJw9IGVyuP7bbCzsX/NX02236ZsMviddpMA4tZfWZwl8CgSevySFCVZY +S5KvX2ZpS8oHCjxFLYGLeMeJriVxmzk54qlztOUbnRBcurmdAUYDdAcW9+udP0xg +CoP49Pcqw6DQy2XS78+pM4H9IvbWKFhEsdEUNl7+BBQj6MTiDpxDDKYG4XlTzJp/ +X8Gof1N9qDcmp6yIIImCJhbMgKK63BjBJJZu6yjH4ktE +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-aes-256-cbc.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-aes-256-cbc.pem new file mode 100644 index 0000000..ff8bfc0 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-aes-256-cbc.pem @@ -0,0 +1,18 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIC3TBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI4iggy9PsVlkCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDIg7tqUopcrjfAUxiYMJl3BIIC +gBfRSHed9lR7I3vdkETPKb9BgCeG/iDnM1jIfoDI9iuWs/vXtxF4x/c7bdZWRM7j +gM1XYuL3+9afC27imlaW9qdwz2aBHJ3EJiHx/bip8J0NY3U3mHWkalGM3cNXJoui +g0tTJ5BHhappQsDLdU/dV1uj0UzPupNQDbUv7UwrfgcfZlJDiQqKUgxLOjb/RiJN +k38HtEERAo6GQtHUQVSe5Oidqq0xMq3AAgV/Cd6SHFA6m5NbTjvIqr/vijCFbYag +Iw3F2wvwC0OPpE5RpTfu8Yw8x/ydgES4fgUJQC8oZUDGSPpI39otZFLCYoaSapKq +ptJfdY8jFstyfIaH5o8XYMsZpQ85nwsC4Fxf3BVM2qFJ4GhQ8/W/Ccu3WxfmuNeU +LeNIYvA5SW/iHNFM9aiR4bgFGdsJxqoYi38QUA0K9wb2sqp4Mxwhe7R7jennaWhV +sl/6vXlgZOWz0m5QFrWUdACYhkNeIUeHapyzEv0drWX4gjUilQtV9djvz89FlQO4 +8sijrJae8Z8+P57yZBRxZHdOt+UGVu+96QGlxh/2PuYBR8pe4df0zCR0U7oLE0by +EetkeyB1WsH+orHqyYuxlJEfTk2Ms5TYiF5qHR/ABsq0ABF2SBklb8FfevZzJmHh +8TZ5df1pKlMqfmbed+ICE3pVhXAbpkwzOOwQ5I7POlxr1VaIe/zk9kHVSX/7SFkw +aLdov4jjqDw6t0peEmTU1Oz5Z8Yw1VSv0c3HGpfekapPPD8Th6lDG8/g4luxW0dT +EdLcAsrZILnKFgQQqLSSVuNCqkHQrV6mIlYlYU0+1HnVXIaELR9Vqxl4c6VVxI0K +OlILOMmTFx6lHeeIU2LKsnw= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-des-ede3-cbc.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-des-ede3-cbc.pem new file mode 100644 index 0000000..dd5c196 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs8-hmacsha256-des-ede3-cbc.pem @@ -0,0 +1,54 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIJnDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIyUfJbtponGMCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECE+aQ96chwGeBIIJSMd8ovzLv5Qm +167Z7lczgBnIReORoh7txYTjzi7/AVfrCjufW1HCPinsbuKPsDfYrABz05TV68Vz +7zIh8l3KJanKf+kQBIhRhfQ7+s/Vdf2GWVLiVAb/HavnWw1oEU5KhJglG/+PESVi +NWfCQAtpQLvEK9Ym9xT23Wb+baVpynt3GhvrROtN2vBuouV8cYRG4cWh9kxqN22G +32K0uNWOCyk323rSvo8+gNfi5NaKDXS2dqMf5QStHXtzWU0ixuX+fiRRSGMx1R/R +iReUsCgqHdskClwoqk1gppGzmc9t9PjUH+KOgvF425gk+V1ye0/AlLkTLJ9AvB/r ++SYy6mYkofLG0Qh9KWQfYgpq7XKDBXUoqw+/pcg3ea6KnOA93FnT3Ld2IE06jKQr +yTQ1bt5eAEZEUA/YKjOwiykgXs6torFFcVMvvTNkivV6CwspNUthzAyZPdMpy+qU +4dR38mvYWggKRDFaGgprNHieiSJecr8RzRddH7Mb+gx28eVOxWQfFelSYlkaYKzM +e3d09AtGEWAfbwoMvTOiFI29Jqt02PDtNfVokGes2VoYrnHOujUom7hFeKlbQGJO +XvSrqUpCsJVmHYD9a4F06NvBN0LlUrI4yIHAAjTRVkARK+9ug5WX8k85KFYtZXEr +YMzJEI74auZsjP8xsOGEpS+tX1J7LmTjm31/4oJb1G0RbO6q+Vu2pPig+v8uE4vc +k32EVeir7q8722dC7jGSBWp9Jz2QVNwWIwnpNl77DZLYUW2mnOyvx3RL268W2LVL +7R7VQXo80e1ChMdwkd2sZqjiqdlALgkVNaTDh8WudlJf4Nd1u9SVOS4FJ7WRlgiT +XI8zF45mb3QSQ3g3CsGvI+jHM+D+4fmViUqW1W/B1BieMpEVC/g0KnCa6JC+1bx9 +7KP2dhzOAlOeKqgsR8g/4UCEqwa7+pM+pb1VRAx59nURwPxZdLGkDCvVNOB+oCu/ +EVttpOrLFIT8VMyEo6a96+5IZWVOQqT9EUvw6VlBkIejRi/iz55cdANqMu8c7Ygn +Wb7oTnu8zYJqNxetSAe0dylA5to+2bDRXobXQBB3QvTLzyVG/5XnoR/eH5vGCUCx +x55Nqqs60gg1jOHWg43HslVmYr7D+Sur8bT4V9DIOUnrr1ZIfzirV5IuzsZNBARb +ubrTXz5K8OdRySZ7PVzEA4Q8lPdb/LKbaZ3aCpLMxr3oeSTcaCQejz/0snRwov+V +9Yr8/XI7BA8V8D3uZT8AZeEFjG9/CfMHnFdyQ4TxaKHRRTwI7VDS+Ik+btmzAdXj +Wu8KN94VjH2hgjZfH1s5Ur+u4lJZ+P/t6rhJtmWDXTyUDpu8p716Zg82lEYAdWgg ++Zun4FFzZBdZffhRXG+HzJjvQ1ygJDNSPG+3Ge5HPz95z0rxryJyeWV9k5iAGCSU +XFKcAx9ItVJDuF7TjTUiPuZvZz/yyui2Oo3FV+T+EqpmPZ9woycJ+W7JbiRjroGF +1v2qn8r0qbGX2SvRfJ171+2bXYge9PoACixaChKLlz7DPSbPokCwycD7sg4Yhsww +foMxicNmOezrlzJOMqsdzfQLiO80QpWk+lh53VNS35meARqoasn0yqKvQsgAIZ4q +RXgRoTCYl+XU6lh7lDgeSfiy0EQLZFd1UaxzDAMcn5uZzUNuBafhLLqTTlM6xLQv +h0FrA0DaI7zsZB/iRYai2AAYlIGPCirCo5MbgU15DCcSxyromkcNukWsRKD5Z39c +ak1hy9ddImabNZDM1ng3vKldN4KehkCpMzVN2CcSGDQHOl/lya1+o7CwjvQkViKn +DH0xBJYJB4GLEzlzKxJkacm8LQMSJ2z58RMo4X6tN+m4SQYp7rB2jyiKz7uAeMMB +9wFkKjQiue4DhbIj+e8wl43FccQNTsk2PPJts6KSwuA3bBEYCGnl3jcJP3Hl8Txi +VOmmssO8or94GoS8FrCeHEk3RWKPDDcZC7xrF5au27gtxbgUZZV92VxOtfrl9glz +Q7pX07YXI8ROLOhcp1Jg9d42D9kzvk4M2oqu0H3nj4ZOyHd1mFlVTy7bEW9Shm/J +ynhKC4fTslxNZAi6y2zcavgcj5aqUVj+SoZolExf4sjT7vdmW/uuHkAQ/QcuobF3 +9UfhDFujH1IbbtKji8W3OxOzYpREoyE72+LkvY+ps+awdNeaoi4nSc9x4QPV8KU3 +XQf6VMREYfWguBCSvcJN2AinO06bzz7pzO2tBJ47O594uZP0XQMXhc74MM0+PfMv +Y7qFhwAVskJ1bMQRHdePKwI2osiE3rOx4ecroPrlFFmy1GYCS0fh+4E+JWvogq8R +ueQKAGuls7jkma4gH7nB534Cd2zelcApbm99RO6ByvjlRCfooG6btxLweYBa5LsW +UfiTNmInh8ODqM+aOXnQLFdA7GM/Wn1/4F1RQulA7noY18f50VrASHTTe79CFIOE +Z9D7AehU0du+gijN3T4kK5WD1+OytFk7dEITAiTJhqbguTcIoQ++PnTOKv4mj30k +h5PugySPiVzzcNz93G50sicuRsxAvCefy95KTYTMfgyfBhLGhtetFXYOYYyJWcyO +/3omaCEo/UYAlzpR1bEmSuWQVWMFmnsLdzrso6bgTKBavUNGs2M9CVoeltrCtu24 +lspxCYIe3VZBuZXS1HI3dS5/zW8HspHMJpO4WRwOgwZc7/d+yq8/k7ceZltEdw+7 +zIaycl+tb8/sQWxDLqW2292NPmfCaNebrBH4AljJwFF8TOHztI6PQlX7QmJLT6wc +VsphfofjhFK4jqCo5njJuvA0IVPzXzr4U+4l12R+8FRLerF0OmgrCAiKOn6z0CoK +h6l/c+Ks3L9lWOZYP4iXHAPMlJDjStqHVuw4JAVjFrAfjtVFoNF6QfHTLALmo/gB +wO/3IV9nEQHM+EmdaD1r6gRpmGJlMuCm7KtOjnLcwqkq6suC8o1F3mHhk/yT1D1x +GV3bEhcc9Ec9jJEkGgj9H0svxximnjP4+YxXGeJ7HEZCTM4VogwlDo1aLDGaXMBg +EGg6kleeDEtdjPOvV7TsF7AXXPFIODW3Zq9jm2eP4KEfmd0NmUn+I+bx7nMhim1z +eaOFbGMQpZjorBGS10z+7PpEAbfDcBgHh+H5/ouxMC2QD/ODoC0sfR4EDMgOFDcz +XRdksDlwnGCCHdhG7YfRuQ== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/jsse/key-password b/test/org/apache/tomcat/util/net/jsse/key-password new file mode 100644 index 0000000..5bbaf87 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-password @@ -0,0 +1 @@ +changeit \ No newline at end of file diff --git a/test/org/apache/tomcat/util/net/jsse/key-pkcs1.pem b/test/org/apache/tomcat/util/net/jsse/key-pkcs1.pem new file mode 100644 index 0000000..c2e3422 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-pkcs1.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDYX3+ogFJtAt/gGmoYb1CpYu4O1jmQ22OefAsEzQSlCqlhe7Lx +Gf1/IjZgjBX+oIJ4LR/irpsxIbXRQsF4NYj1jK+VVZpxmXp9sLYNhDsb2XECKBSQ +qGCOv8hqTdMaLg5LqclpgHV1NX2YwEjYcYWzbxeRT8F4OmalYO3L91k+qQIDAQAB +AoGBAMT/DadITcNaXqIW6omcr3/Ixp1Thc3RMP3WSeHxF018S4KpsN26oAXkDEYS +xOOzF5Z63xDvj/RHkNYZRTRA6ZOaFztfDtqiU01vz4ljNP13Zp3g7kC2abkDmMfD +I8G3l63kt5CRButL4hoRqwzSjziXbsT+YDGcHEF/G2pWDkpBAkEA69czNfridqhY +1LAdvqlUORNxHBQQ/mvSV/eOheR0K3IAnMr4qv9dkiZDDDKtgZh/+q0uUk+BIFVh +JX3FzzM6RQJBAOreTDuNiDtjVRZivdY5IxiiQlQUiVKyIlcGKlAcnuzcWeTUVniV +s+H1OwSjct8xnGL/z6cMZx1NkMr9nlKaixUCQQCAR2J8hwUtI26F6XGUZkgAb5nD +ewqvSHh2DppAK74gb3bz0dcmM5Zyy0sG1H3ZkthxwkcC0Gnc63PWz62LgUK9AkAo +ma7x6IBxS7WMvhr06kGf44S1xisK6ZI4Gu+7k4cBiQHdJbug8rf6yoqePacA4DGZ +h4Ec7m7wyNTL0lXJD8AVAkEAvzwfeKpisNMu+ZmBsNb+0xWx9UKG6bg2UYYzwNdj +bhitdcU2ON62TnFyGhIYIqI3o5BSgoOPtZeKZfa380SQEg== +-----END RSA PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/key-password b/test/org/apache/tomcat/util/net/key-password new file mode 100644 index 0000000..05557f9 --- /dev/null +++ b/test/org/apache/tomcat/util/net/key-password @@ -0,0 +1 @@ +tomcatpass \ No newline at end of file diff --git a/test/org/apache/tomcat/util/net/keystore-info.txt b/test/org/apache/tomcat/util/net/keystore-info.txt new file mode 100644 index 0000000..986a4bf --- /dev/null +++ b/test/org/apache/tomcat/util/net/keystore-info.txt @@ -0,0 +1,28 @@ +================================================================================ + 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. +================================================================================ + +ca.jks (changeit) + ca CN=Apache Tomcat Test CA + +localhost-rsa.jks (changeit) + tomcat CN=localhost + +localhost-rsa-copy1.jks (changeit) + tomcat CN=localhost (tomcatpass) + +user1.jks (changeit) + user1 CN=user1 diff --git a/test/org/apache/tomcat/util/net/keystore-password b/test/org/apache/tomcat/util/net/keystore-password new file mode 100644 index 0000000..5bbaf87 --- /dev/null +++ b/test/org/apache/tomcat/util/net/keystore-password @@ -0,0 +1 @@ +changeit \ No newline at end of file diff --git a/test/org/apache/tomcat/util/net/localhost-ec-cert.pem b/test/org/apache/tomcat/util/net/localhost-ec-cert.pem new file mode 100644 index 0000000..3bb68c4 --- /dev/null +++ b/test/org/apache/tomcat/util/net/localhost-ec-cert.pem @@ -0,0 +1,86 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4110 (0x100e) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=MA, L=Wakefield, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=Apache Tomcat Test CA + Validity + Not Before: Feb 17 16:05:19 2023 GMT + Not After : Feb 16 16:05:19 2025 GMT + Subject: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=localhost + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:38:6d:73:27:ea:ab:41:cc:7b:ac:36:18:94:ab: + 1f:de:f3:aa:35:f4:ef:8e:2e:60:39:63:5c:08:75: + cf:86:aa:4d:ef:40:85:16:40:d1:d5:a5:36:2c:25: + 05:20:8d:f5:ff:2b:a9:cf:5d:4b:b9:e1:b0:d4:9e: + 81:be:2a:cb:8d + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + A8:26:CE:0D:E5:8E:35:2E:AC:B0:5B:E1:89:42:FA:64:26:F3:4C:69 + X509v3 Authority Key Identifier: + 00:F2:98:4D:21:2C:00:3C:40:9B:84:F4:DE:2A:F0:26:EE:32:0E:9F + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 40:ba:a4:ff:c2:df:92:55:88:e4:8e:35:9e:28:28:ce:f4:63: + f0:da:a2:3e:f4:af:68:60:fb:f3:d4:d8:9d:d4:d3:54:3e:dc: + 33:e4:24:74:0f:4b:af:fa:b5:bd:56:5c:dd:0d:86:7f:99:b7: + fc:84:8c:88:b9:f5:50:f1:8f:ea:3c:7a:95:a6:6c:5e:20:9a: + 2d:a9:b5:35:d6:f4:37:23:1a:79:ce:e3:40:23:79:09:b4:08: + fc:22:3e:c4:d3:66:70:84:06:e8:54:21:b2:7a:22:ed:78:d3: + 9f:8f:da:92:2d:1c:0a:95:3f:b2:fe:18:50:60:73:ea:2f:8d: + cc:e8:73:52:16:38:76:ba:9c:2c:85:2c:a6:3d:30:fd:f8:a0: + da:6b:24:d8:a0:ad:d0:5c:d3:d3:a2:13:47:49:d8:49:1b:0d: + 56:fd:4a:4d:62:b7:32:f0:97:23:22:ea:a3:68:5a:c7:f3:56: + dc:bf:95:82:fe:8b:ff:10:f5:79:1a:e5:07:12:ca:46:75:42: + 0d:b2:2b:2e:26:39:72:08:0f:53:f4:d7:de:75:da:c3:a1:69: + 32:0e:eb:94:63:3f:7d:81:13:cf:c2:a6:ae:8f:02:3f:0d:a9: + 99:68:e1:ff:b1:17:c8:94:58:57:ae:cb:08:9e:88:1e:17:1d: + e3:8b:f7:a9:f5:fc:d5:0d:87:bd:6f:f3:26:b1:6b:cb:62:59: + d8:4d:2d:21:40:57:77:b2:53:05:4f:2c:b7:f2:f8:57:d5:93: + 85:00:1f:97:e3:6b:d0:05:1e:8e:4a:4f:02:98:3f:69:37:75: + 64:37:07:aa:d6:b3:00:bc:8f:52:e9:c9:8e:98:82:f5:6a:d8: + c2:94:dc:5f:2c:31:09:15:6a:62:05:6d:4c:9d:14:e8:a0:f4: + ae:f5:14:f3:61:28:13:55:bf:22:50:78:23:4f:80:34:7e:f2: + b0:89:e2:fa:6b:d2:00:b1:aa:71:a7:2f:1c:04:b9:1e:39:6c: + c5:fc:10:8a:4c:7f:d1:16:74:87:45:f2:11:a9:71:12:0f:2b: + 0e:d7:20:16:a7:a1:8a:36:a9:bc:2c:0d:b9:eb:e8:5b:bc:75: + e2:3b:84:28:3f:c0:c3:7d:87:99:e0:aa:42:ef:66:73:5f:ec: + be:14:c0:70:cf:46:23:c2:bf:ee:c1:c7:af:28:99:e2:09:23: + 4e:de:b5:be:d6:44:1c:fa:b0:77:ee:b3:7a:b4:a6:d7:e8:24: + a4:10:50:cb:94:2d:94:0e:52:19:d1:d3:88:b8:36:35:2b:c3: + af:82:a2:f8:4c:55:b2:6e:42:65:80:00:74:17:53:af:7a:f9: + 60:59:b7:3e:82:bc:2d:00 +-----BEGIN CERTIFICATE----- +MIIESTCCAjGgAwIBAgICEA4wDQYJKoZIhvcNAQELBQAwgZMxCzAJBgNVBAYTAlVT +MQswCQYDVQQIEwJNQTESMBAGA1UEBxMJV2FrZWZpZWxkMScwJQYDVQQKEx5UaGUg +QXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRpb24xGjAYBgNVBAsTEUFwYWNoZSBUb21j +YXQgUE1DMR4wHAYDVQQDExVBcGFjaGUgVG9tY2F0IFRlc3QgQ0EwHhcNMjMwMjE3 +MTYwNTE5WhcNMjUwMjE2MTYwNTE5WjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +AkRFMRMwEQYDVQQHDApXaWxtaW5ndG9uMScwJQYDVQQKDB5UaGUgQXBhY2hlIFNv +ZnR3YXJlIEZvdW5kYXRpb24xGjAYBgNVBAsMEUFwYWNoZSBUb21jYXQgUE1DMRIw +EAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ4bXMn +6qtBzHusNhiUqx/e86o19O+OLmA5Y1wIdc+Gqk3vQIUWQNHVpTYsJQUgjfX/K6nP +XUu54bDUnoG+KsuNo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVu +U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUqCbODeWONS6ssFvh +iUL6ZCbzTGkwHwYDVR0jBBgwFoAUAPKYTSEsADxAm4T03irwJu4yDp8wDQYJKoZI +hvcNAQELBQADggIBAEC6pP/C35JViOSONZ4oKM70Y/Daoj70r2hg+/PU2J3U01Q+ +3DPkJHQPS6/6tb1WXN0Nhn+Zt/yEjIi59VDxj+o8epWmbF4gmi2ptTXW9DcjGnnO +40AjeQm0CPwiPsTTZnCEBuhUIbJ6Iu1405+P2pItHAqVP7L+GFBgc+ovjczoc1IW +OHa6nCyFLKY9MP34oNprJNigrdBc09OiE0dJ2EkbDVb9Sk1itzLwlyMi6qNoWsfz +Vty/lYL+i/8Q9Xka5QcSykZ1Qg2yKy4mOXIID1P019512sOhaTIO65RjP32BE8/C +pq6PAj8NqZlo4f+xF8iUWFeuywieiB4XHeOL96n1/NUNh71v8yaxa8tiWdhNLSFA +V3eyUwVPLLfy+FfVk4UAH5fja9AFHo5KTwKYP2k3dWQ3B6rWswC8j1LpyY6YgvVq +2MKU3F8sMQkVamIFbUydFOig9K71FPNhKBNVvyJQeCNPgDR+8rCJ4vpr0gCxqnGn +LxwEuR45bMX8EIpMf9EWdIdF8hGpcRIPKw7XIBanoYo2qbwsDbnr6Fu8deI7hCg/ +wMN9h5ngqkLvZnNf7L4UwHDPRiPCv+7Bx68omeIJI07etb7WRBz6sHfus3q0ptfo +JKQQUMuULZQOUhnR04i4NjUrw6+CovhMVbJuQmWAAHQXU696+WBZtz6CvC0A +-----END CERTIFICATE----- diff --git a/test/org/apache/tomcat/util/net/localhost-ec-key.pem b/test/org/apache/tomcat/util/net/localhost-ec-key.pem new file mode 100644 index 0000000..656272c --- /dev/null +++ b/test/org/apache/tomcat/util/net/localhost-ec-key.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKJoMeaqKfUg2NywCJdy61UF8pk3kAO73SICW8i1/9PaoAoGCCqGSM49 +AwEHoUQDQgAEOG1zJ+qrQcx7rDYYlKsf3vOqNfTvji5gOWNcCHXPhqpN70CFFkDR +1aU2LCUFII31/yupz11LueGw1J6BvirLjQ== +-----END EC PRIVATE KEY----- diff --git a/test/org/apache/tomcat/util/net/localhost-ec.jks b/test/org/apache/tomcat/util/net/localhost-ec.jks new file mode 100644 index 0000000000000000000000000000000000000000..284c3162d56d7b7e9db2382281b634b2b85bf091 GIT binary patch literal 1372 zcmezO_TO6u1_mY|W&~rllKkA{#1bI4EkW@V3sCHYLE}*aJ~l3GHbxdkEha%mMpg!v z#!a0I%_VH7UwOhQ%cknC!jz=*c8_CIqTQl4e-Bj$59Ec#i3 zm_}2zeB;ijO5>*sSFfM9zizpH^}cs?0?#}*^L%+{{n}QRZ?667hK|3L25d#AN85K^Mn-N{27|`QhTI06Y|No7Y{E>T!7vVoFq5yNp^$+9 zNQPaQGdwXnH7zqWC&f_RKoum$B`g<`k*eTWkeCdlg7ec#$`gxH72NVm^HLH^GV}8c zr3@rM3b};^p^8GlfvynX>ue}zAOn(P78XU73rQ_5QE+xNkQ3)MGBz+WG&eLeFg3J{ z0&-1(Tr()wps@qyP*hj*FuAxI3L6N5UCqN4o|%)InU`JybPdeeJj6Pi2aB_z;la$q znUkNKn3IuTTw)MuAk4-N4AFD8OpI(CUn9Bx2^Jh@6amdaPjI=GaXe{h2F3KwO5{x_1^h#!21a3`Te&h>B?|TwcqqlA|Rpom45G; z7sWwh7G=BU=(OrAvo-kpW5KO#l^YAzUWmDTd6BTY=M7J3-mt%3zDe7SK1^3udbKzs z>iFldJNu_L{pFvDZ9`qw^AqGP1<^DmPH)=!C$W5E4_7iVWtt^>nX|hwT;5( z4=r2Q&t%WLa%RTE{~N_mOo<3zcba2fhn%?VV(zy7((+rBsdv)ab&(@Bvxe03Ea z!pk=Wv-<07|MVmL>f}}i`RR|dFR;q>dHFNVu+KCvO)+O*b!{`lp8lYhC;MhJea*UY zXv&><9YaphtR&W4pSdD07JOOvRpfJ`hH&V9rGN@${|1w~Pa8TP{mQ1?SzA5AA<<@c4R-nU6S?{qAkucg;oS*M{V zDY{emf+R0q?$}{ws(pBU)1n_fp_}rYQX3db#DmvY{Y;45Zr8L&mx1Zkq6_c;{5tgM Q@l@+iDvOlzUeCJ-0Nbt(r2qf` literal 0 HcmV?d00001 diff --git a/test/org/apache/tomcat/util/net/localhost-rsa-cert.pem b/test/org/apache/tomcat/util/net/localhost-rsa-cert.pem new file mode 100644 index 0000000..c343d2f --- /dev/null +++ b/test/org/apache/tomcat/util/net/localhost-rsa-cert.pem @@ -0,0 +1,105 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4109 (0x100d) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=MA, L=Wakefield, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=Apache Tomcat Test CA + Validity + Not Before: Feb 17 16:03:54 2023 GMT + Not After : Feb 16 16:03:54 2025 GMT + Subject: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:f2:2e:c8:d3:25:64:2f:76:ae:51:35:45:e4:7a: + 5e:2a:ff:01:21:d7:08:5a:ec:2a:cb:03:7d:da:4d: + 10:6e:74:83:e1:e9:79:6f:48:bd:d7:90:46:55:ab: + df:b7:c1:dd:3a:a6:c5:ae:fd:c0:9f:8c:1e:fb:b3: + 3b:b0:9a:d6:fb:6e:05:51:e7:25:d8:ae:f8:5c:fa: + 56:c2:7b:c6:5e:5b:bd:8a:6d:e9:46:ec:db:f1:8d: + 0b:26:1a:ff:9a:30:27:06:63:a1:d8:16:40:b1:d8: + 15:59:61:a5:7a:6f:1a:6a:e3:11:05:54:9d:25:3c: + 0d:fa:6c:8b:9a:6a:93:6f:9f:c9:15:2a:73:9c:cc: + c7:2c:5b:b2:5f:fd:ad:5d:66:4a:5d:75:f0:ed:e1: + 61:e6:0a:96:4b:a3:a8:71:d7:9b:cb:04:d0:f2:e1: + 2a:1a:f1:42:67:53:f9:ee:00:97:80:f8:47:fb:4b: + 6e:01:63:9c:cc:41:5b:25:75:b8:ca:ca:b9:f7:1b: + 52:bf:a7:68:c7:eb:a2:f4:ff:7a:9b:5b:bc:fd:fa: + cf:9b:15:3a:33:32:75:f7:ca:40:42:f9:a8:2d:eb: + cb:b7:a0:64:0d:89:08:1c:2e:38:56:45:24:4f:ad: + 94:19:e2:35:43:2c:fe:31:dc:38:b4:c1:10:f6:a1: + 6a:75 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 5D:45:8F:DA:02:13:CC:A4:01:75:0C:BB:72:E2:3C:EA:FB:AA:A9:09 + X509v3 Authority Key Identifier: + 00:F2:98:4D:21:2C:00:3C:40:9B:84:F4:DE:2A:F0:26:EE:32:0E:9F + X509v3 Subject Alternative Name: + DNS:localhost, IP Address:127.0.0.1 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 0c:21:e7:d7:33:bb:6c:65:01:91:54:fa:0b:1c:c5:c9:83:51: + 02:03:a9:e7:c4:dc:30:4e:90:a8:73:8f:a8:ca:4e:2c:5a:ef: + 87:43:a2:de:3f:db:21:ea:60:7c:e4:a1:d4:75:66:3a:e9:44: + af:e0:4a:2c:4e:59:bc:1f:af:5c:8c:8a:ed:06:ee:56:0a:cf: + 55:08:6a:51:90:ce:cf:cc:b9:22:95:3a:91:78:a5:84:96:4b: + 2b:a0:bf:7d:72:f7:01:b5:a1:ca:62:04:2e:73:d5:2c:30:fa: + 98:e3:89:35:9d:e6:d1:b2:79:19:55:dc:52:56:99:84:9f:4a: + f0:16:d0:27:9b:12:13:55:9c:fd:f1:60:fa:21:79:58:69:cd: + 4f:0a:a4:5f:1c:9a:2f:fb:10:2c:94:95:13:54:3c:08:61:fd: + 43:8b:de:1f:c8:0b:c6:c5:92:3e:97:f8:c3:56:8b:98:7e:23: + dd:73:c8:24:6d:39:e8:5b:eb:c9:15:60:dd:1e:d7:9a:62:bb: + 34:28:27:f5:59:de:8e:4a:d9:0f:3f:1c:95:39:c3:ab:82:d9: + 02:a0:d7:c4:85:39:f1:24:96:3a:e4:7a:cd:06:6b:93:dd:86: + 47:00:e6:2b:c8:f2:c4:d7:56:87:f2:25:2c:df:f8:21:62:86: + 19:b4:54:07:c2:4a:a8:f2:2e:0a:5b:9b:a6:51:31:c6:97:32: + 1b:e9:13:08:79:23:3b:f8:a4:28:79:3f:d0:76:0d:04:94:16: + 47:01:02:1d:44:ad:d4:54:22:30:a1:b9:f7:75:ff:c8:a1:91: + 78:f0:57:00:92:72:a3:45:81:10:10:77:4c:41:37:56:7a:a4: + 97:b2:1d:5f:b4:d2:67:00:25:1a:d4:08:18:a4:d8:4b:e6:3e: + 42:ac:f9:6b:72:46:bc:83:d7:aa:0f:e8:da:c5:1c:ce:55:1c: + 8b:d8:f2:3f:d4:e1:a0:f0:c0:d5:54:c1:7b:ac:3a:b8:2a:57: + 37:cf:55:2a:ce:c4:18:1d:97:b0:26:5b:b8:f4:bd:7f:5c:91: + 94:fa:fc:74:aa:e7:15:d1:2c:a9:12:bb:65:93:fe:39:62:82: + 50:d1:9d:dc:6f:e5:45:82:c0:fe:96:66:4a:5a:cf:55:82:56: + 92:20:12:8c:4b:da:35:f1:73:d9:02:cc:38:ab:1f:5e:60:56: + 9a:41:75:8e:a2:2e:33:25:3c:28:79:63:de:3b:06:75:26:9f: + 2e:b5:5b:24:32:00:48:5f:af:ee:5d:91:3f:24:42:6b:c8:22: + 76:41:00:b7:99:d8:05:4a:7a:50:44:bd:6f:d6:b0:fc:26:9d: + 1b:c1:8c:03:c8:26:79:e6 +-----BEGIN CERTIFICATE----- +MIIFMjCCAxqgAwIBAgICEA0wDQYJKoZIhvcNAQELBQAwgZMxCzAJBgNVBAYTAlVT +MQswCQYDVQQIEwJNQTESMBAGA1UEBxMJV2FrZWZpZWxkMScwJQYDVQQKEx5UaGUg +QXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRpb24xGjAYBgNVBAsTEUFwYWNoZSBUb21j +YXQgUE1DMR4wHAYDVQQDExVBcGFjaGUgVG9tY2F0IFRlc3QgQ0EwHhcNMjMwMjE3 +MTYwMzU0WhcNMjUwMjE2MTYwMzU0WjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgT +AkRFMRMwEQYDVQQHEwpXaWxtaW5ndG9uMScwJQYDVQQKEx5UaGUgQXBhY2hlIFNv +ZnR3YXJlIEZvdW5kYXRpb24xGjAYBgNVBAsTEUFwYWNoZSBUb21jYXQgUE1DMRIw +EAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDyLsjTJWQvdq5RNUXkel4q/wEh1wha7CrLA33aTRBudIPh6XlvSL3XkEZVq9+3 +wd06psWu/cCfjB77szuwmtb7bgVR5yXYrvhc+lbCe8ZeW72KbelG7NvxjQsmGv+a +MCcGY6HYFkCx2BVZYaV6bxpq4xEFVJ0lPA36bIuaapNvn8kVKnOczMcsW7Jf/a1d +ZkpddfDt4WHmCpZLo6hx15vLBNDy4Soa8UJnU/nuAJeA+Ef7S24BY5zMQVsldbjK +yrn3G1K/p2jH66L0/3qbW7z9+s+bFTozMnX3ykBC+agt68u3oGQNiQgcLjhWRSRP +rZQZ4jVDLP4x3Di0wRD2oWp1AgMBAAGjgZgwgZUwCQYDVR0TBAIwADAsBglghkgB +hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE +FF1Fj9oCE8ykAXUMu3LiPOr7qqkJMB8GA1UdIwQYMBaAFADymE0hLAA8QJuE9N4q +8CbuMg6fMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF +AAOCAgEADCHn1zO7bGUBkVT6CxzFyYNRAgOp58TcME6QqHOPqMpOLFrvh0Oi3j/b +IepgfOSh1HVmOulEr+BKLE5ZvB+vXIyK7QbuVgrPVQhqUZDOz8y5IpU6kXilhJZL +K6C/fXL3AbWhymIELnPVLDD6mOOJNZ3m0bJ5GVXcUlaZhJ9K8BbQJ5sSE1Wc/fFg ++iF5WGnNTwqkXxyaL/sQLJSVE1Q8CGH9Q4veH8gLxsWSPpf4w1aLmH4j3XPIJG05 +6FvryRVg3R7XmmK7NCgn9VnejkrZDz8clTnDq4LZAqDXxIU58SSWOuR6zQZrk92G +RwDmK8jyxNdWh/IlLN/4IWKGGbRUB8JKqPIuClubplExxpcyG+kTCHkjO/ikKHk/ +0HYNBJQWRwECHUSt1FQiMKG593X/yKGRePBXAJJyo0WBEBB3TEE3Vnqkl7IdX7TS +ZwAlGtQIGKTYS+Y+Qqz5a3JGvIPXqg/o2sUczlUci9jyP9ThoPDA1VTBe6w6uCpX +N89VKs7EGB2XsCZbuPS9f1yRlPr8dKrnFdEsqRK7ZZP+OWKCUNGd3G/lRYLA/pZm +SlrPVYJWkiASjEvaNfFz2QLMOKsfXmBWmkF1jqIuMyU8KHlj3jsGdSafLrVbJDIA +SF+v7l2RPyRCa8gidkEAt5nYBUp6UES9b9aw/CadG8GMA8gmeeY= +-----END CERTIFICATE----- diff --git a/test/org/apache/tomcat/util/net/localhost-rsa-copy1.jks b/test/org/apache/tomcat/util/net/localhost-rsa-copy1.jks new file mode 100644 index 0000000000000000000000000000000000000000..506405d8a59b588d8df8f572393a43f9788cebcb GIT binary patch literal 2685 zcmb_ec{tQ-8=lR+4TBovAUSXu;SDr?Q2SV-E=tNz}1r zKUp)#nk;3>QrY>YzRvZX@B91v0zsGo{u!(kl0N}Y zVZta^sjnCihy?`DV7mY>E2knW7zRc`_`zTn5R3+!$C(e9+%IOwrT?`hcQ$-J%$@I# zLr9!liY$GL`-+76?+p5Nr!bsC^A}5V>OiKsFCFhPe1^idJd8 zp>=$fGuyhv`9JPKW38%^YZY&X8|@xTbt#At)`fXJQE^nM9V{T%RZr^c>>d6R_6e-_Uw{A%OT> znTu~aLCoiSIV<1iO0|Nkh(qJtZ>7-d02YpT^B$8;V~_A63s2h%%<(k ziI5q1Lg@F=zNy>&BbkPzCcYHWKmrH(`k}g@+4|kpfzpyYPeYC;rcH`cyghk(E1$-zcCX(s zqQwVMwvL-|`Dho_bBv^BQN0hrq)#6;Mw~ZW@5&OD$R21Yd-*Upk*to@{O~;bYPfhw zZ%1Z7%>;;?tB|2+9v_@ht9ZVn*9-D?sc)q;{<9m=J^|r2VkJir_Sqr32!EPJD{ajWyY_^l`R*Y`dM-2*CSXD${l7H&x z^C)uSR*rhwiHvuWmTuqkWaMjKGIoxyz=i%|BU0b@ z=2Z1sGhPv^$G6_yfZgisQrAvW6{=1(k$HE7SPIfkOEs#l?BA#&8IUP*U7GH~G=`i3wK$Qo*ewUi)c{Xt;e>ZNA zW4Y1Rsv_<8h=lo;7#*9es>xSgiE$=3yu?$;;?;Kc`XtaSgkswlYF0^#!U~yB#uUJCi$-TWXAvn>t~n}KYiqkhIx$rF>cvZ0XLF*_ z8V|espl++V-(W9hV@?T&t6D}cE1}bJaLa`S4vVc|*cp}UFMkVhdZJ4AozS@2nQ*4d zA&TWqU49;NGk^NCK>B2dr_{by+OehEBSoI@al5G@oVE9dhS$!8^WS~cek;n+S7m&J zSEDA=?lpP)Jr@cUxsR1eEfeAHImn@JXxraUc+LWKsExYPoJvu~o;fx%AaYb~$r;up zRYJ7ryKq?B2*~&UYBsrxGPxih5Kwu0ve2HZ;m=b-CyMF(CWHv_h7+aOpT5X)L0KKp zV%nhwKx0t_Xi(8CCM>Ri+Y4;akFw9BwW6)u2-6^eziA|P1XzX3Y} zVu4l>0N_6uHUx(~-q*v^+r!UYMFEieamR@e$9Q>2qA%kK%)^%CNx6zAdq^6Ss6=-> z#hXM_5d{waNaRBB|4PFAG;2vK3j-B#KSu_lj}f7+XA1-7u#8=uPQfeP*2a&a=}`>4Q5&7V9~=q0&v%l=xKV9y0SEdpXGdDmRop*k=_aGS(`$MuIgX1-C72vgl?Mbm=VE*hh1nV zIqHj!j+%`lHh&g)wJzj*JqS*Bs@vP{P8ZTvQ>AWn=;?129$)CJ&T{9u!!D+L%FamE zvLs35gSvszzRKvS$_DuNY#%BF3I>65!%~5;aE&?#j@r}*^S~s8 zk(QS|h_<%ok|rKR4>F$O;Vx<5L8f?ndNcRC2Y_TY6v+z{h6!FYx;Fwrbmf7m+_mHn zI-hqQ7jXa*KWG`)VL&)k5VV$RA*BS;(Myl`I*wYAUsmPK1Vn!%@WYssLHkeZqG2J- zpTj?!$9xb_8UzgDmYQRz)%tmW<1yP@VsF~Rts&5&x#m&e{Qbg!dxah6l^nlB8{~}Z zzL)y!dgEjE0M%3blVRDUsnU6eI*Bq|T+9;dvK?nPmfgqt{@d=Z8tG*1_-pwQ56l#^ z{=7-v09R#qxWSYI-YEgwsnd7VGiG}$f<&;RHg;(dnWihkJqqap2<*eXRo88)pbOrA zS#svNh|y2%z?G7c5f~kI{GLJVxI`OQ%bSF=DVt4pv8lIY#sb=8{k7(u7TSee$HWkYZL~DUo}T-+6CvOR3Kd{`!0dxL|&oB8;DArOIb2S2@Y5g3h^=Enke+mDTrclMY0Ks?&y8OoOcqUy%l%?#gEzX^4Zi c$p_6|c{k54Kcn}w+1Sh8mO(doRN^PK1R=kv$;<9>Z#pYL`5aozWIUGL9*yn4I}002Oz3;1(UV9~c7 zk=RoiX3uXo3jmM&o2NLb?k)Fo4}YfM-gyR4YXSGt-2pN)DrM zz4M%o{*7I|0Ja0udPh9}v8?@4UsotubA8bD;fZaDCAm1K$00=&&)S>TN@f3KS57ueKq)e58f_Nb8d)jui|C-lWO*AgkM3^<;p@HUmkSw6^@#jAYYej zMkCT>;xPFcFG?)QzsTQ5Eiw#EdZtb!Ea);-f7dSTcJJ7Kdgme2@{|MIQf1)YBstmB z^JZ-?cOi0EM_}}-WBho5nE|@M=F-JRCcAt!`)2Q;DuEn?omU6 zrvkr=5sS__XUHA$XStk)?))B)=Q=x)jfUj>O#z&WYzqQC?Rx0#emPUzP|(Eg&6$}G zvX{H~m1gLc@+RYd14J)@_7;jgg!)xO&5IgAencs`$SdCbyqsBKfh`mGHVL?%A?^>7EwVJ?+ ztrRvchw19EQLn)d1VnFUY!dJ2z`eumIN$`Ow3ja0RG~DlZ2Bao)LSE5A-?95NoFbs zD^Rq?ydY)0)4NL0WD%(f^XhS=qS6Sxx$*smjc`Nk`Glcmqxkuvn(MBQzP^@VvPkTo zNisBQCoZe2zW_I)T1m4k=8qqdm&>Y|*Eh6GSE%{UGx#3G7_ulkt z5395E7}`aPkesy9qK=(aN5@hqK8d^9U&6-f3Neuq+-a-rhL(vP?E`oG;w0PyO|O;F zSrMN!D`}9|B(GYuFwW_NmbKSZEFyPLJR5l&M+Y<4MDHa!{D}0pcGJsBNQWSt;1DAT zKJ0f|V;Zm(50rYIzqqw3QYY{20#B7g_leE2P|e~*)IB{)jL@IvEs2v&Y1o%FRigU4 z{fL3S%cT_DAtY3>V38AB0MZqzA7otV=eTN2)zqrH;MAQuDzR+ih7P^@Si#~`t=v|cAE%839Tebv9MGz_P9-D)a~9b`%zTH7g&nK#ZZh;?Ij{Sd~js2={^ z%>KbdWk*)ur;rC<3oPI!Cx#yn!1SUL_-ONpInxZ%{$ns*=$O{nQ%jpEfqX6%K=0Ls zNW)ykn2i}r<3rmcCM#!s{o?1CH9KdM>dXzw%7$si zNqbQ_PRUKSX`$(MZ6YPr^L5w5D~c^#+n-`2B#c{p_&F4PyR&M1r)n7GMV?(pznMwP zhAoP=iCH-Ylqa_&*QTt}BVAf-BgZ%q-^Q8CT%u;!LOEKmrxMJ+D-k%Efj1{H)U;y( z`W~7fH5#`$ETd~h%BqHj{S5c0s)k_V>|7{|F}(ZBDqYd6$%9AD3;whXKlw<0OHvl9 zaEe1YD4tvziYMd9B?AFLAP^%xl%9fGEKDnGmmUbDJ;h~k0*n?)O+jV`rhtIVO#Tv7 z5RjfK>^zk5EJ6vPHb;7(oZV2KPB0Ou(Ahs42#={N>Y}PQ((&|Tf_BE=nyXDSImAeo1KXlfM%EXVw~U7zE1nXVx_4Zl1T@yj-xS+5Eqx{p&u+Ak>~{ zN2I4K8iR%6f&72J(^I=4!vh&keF#Vc!UKVTP05Zwgq);&pBu@-=lpHOPJnzvRF*4Z zAISno^%%XdAv249Xsw2!*c)bLQ?HsQ6pPxPAHB_q;yHM!RFVGqz>C~yL1_5-p7jqz zOF+A=RYSz>#TzSQ8^6;EbDX3@MJOEehFMi!46|4u3;od??q8V5O|yiq(f{y_Om|N} z=X_uh!(?{77q_akJ9=j0tZjo^U!6hD(k18FT~tT;VBMVEPW>GPAO9!PT)KK2DumG41%KDHQu$ijICl0u#lJRJ3QfytABz-Oi%5k=V34Gca z`2Q*0WAKC1m&4zScRCPccn}ak$G0#fQ}2la#+m-09S$H=N)sH1&Vu+pk z;+AWV)$=En$M}})@6Y88;G7i~HOi;8#q}-T@RnOgMXXYg5H!7JRPIKxJ-uCZ{7H&& z_X-~+>s-!l3iR0pR_Ap(fF&`5;?N(dU&3Xx=KCuB*v-a`5owQdwAWcbiDaCIm}MSq z*#F@3yXDqxKtr(ON|!od6i-Zom|mko9;ru8@^;X+x5Zyi*=t2arUnU2U^)bED}1wB z{=i~C!84TZP=7^KWZPmgT6=^+nJY=5wG2N3${l(as<0uLtT^Z2P2rI+5vB>4zud9; zZV2&sQ%HPjkIx~Dy~>obMVq)ONn@2!WCUwZk>*^4Q27Zc?GZ%%lt1~>gA-Xbfgo;; zX9K4E(7d``+(}1X+`V;kK)g=@JeZO3uCD4;gg+srlH0DT-vuDVF+jye7}lA;uJ&}_ z!{^4EkfBnBZ=-EoJ!V{y!<)(jGr8+;2Thv;o+{RgnP2TS6YF`$#+_0jY*o9}@W47Q z@y8*ybb+N$yySd6D&bhc0dLrsHIDuY$G<&JcGkA+HNzv~FP@Lm8I|3@jDWi2%Xn?= z5$US9=zK{Tp=+Xkj+06hIN==0YAZo$fRpm>Vn(>GG~(Jxk5FDU|7e?kFGpclq4H397*s}1 zM(&KGXHxE${Fi2p2mN29g3gc%Iz{T~cs|dEle!14aVs*JwQQ|W-r+_g(h4qlZ)Y4I z#^OHadCZ!N3_jnVoGRvU^Ivg^QV5|=aP8MB5~v%#sM2*8m*gL`?;gUQb?*4hR6B}% zqg&wn=;0qOd|+Lgrz=VnZ%JyGJ?maL6fQKy(kgc!K$IY4m2x+q9lX&RoV(uZ!F;7k z_i{}}K3XK>kR_<^q4krp@VbQ3MbUP}L*F>Ekj6n;Dccef!RL}c>;7_D#D`M%^)5FX zV{7^7ysraz7m>`J?^fTt>6yQ(O-4++(V@oW{W*BMhTAWxA`(a%S5y_+wpU4d`SZ8w zVYcaN?(+dxuPFstN~7ySj2SDNy)ef*X8!3@Q6_(^Tp&y&AuQfAsqa9BiFBJ8pc~!N z8ITT=bA9Yf&|dg~4`W^Ab!l<`!P~YXD~6vdzlpS#XV5P-MM&1(w9w9rkSOB4wK3B& zpLDdfA(Q!CYzr3x?4B7wdI9voH7xv zI!28fBD1emJESi1IS8l<&n>LtbziAvpQp zISTl{0f+AFBoHt;lne+w`3HV}x)$ip?3;z>qvPSdZT*_@!_4Vj8t^oBV@0EZ7fgZ& z#YSw0mVNHQDXV}fqvj3r(?|ezeU6G6ZM$!#7gCGO zY=Bk|ni>8eJ!XxYF%P$9B1zCWxM14$x1(n2-KRJE?khTa-+2$Q9aiJTD}SLYLZ3^O zWN1FJeQO=~Ao<%XN?YAaMZ>`fi4#|&(-=I7Cf}@-o?^j7>S!W}~JY*fK!y<`moFmJ(SYIj1#?X!{kzcvW@=WFJ#Va+o zamyB3pq=kQ7s9l*Yh-q~uWJVGvw5A1g*(JJ7_5=2C4PZBBtlajxMnNxsu+JBkB0i; zSo=N+E}&^&P27DRR>~P#^w2BEo?~*`5|c*AzZ}1PV9ZJQ+_(ASL`{$`KB~R(f}_B7DJwqPdp#pa?0*27AWnn; literal 0 HcmV?d00001 diff --git a/test/org/apache/tomcat/util/net/openssl/TestOpenSSLConf.java b/test/org/apache/tomcat/util/net/openssl/TestOpenSSLConf.java new file mode 100644 index 0000000..74e8f3b --- /dev/null +++ b/test/org/apache/tomcat/util/net/openssl/TestOpenSSLConf.java @@ -0,0 +1,148 @@ +/* + * 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.tomcat.util.net.openssl; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.AprLifecycleListener; +import org.apache.catalina.core.StandardServer; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.jni.SSLContext; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestOpenSSLConf extends TomcatBaseTest { + + private static final String ENABLED_CIPHER = "AES256-SHA256"; + private static final String[] EXPECTED_CIPHERS = {ENABLED_CIPHER}; + private static final String[] ENABLED_PROTOCOLS = {"TLSv1.1"}; + private static final String[] DISABLED_PROTOCOLS = {"SSLv3", "TLSv1", "TLSv1.2"}; + private static final String[] DISABLED_PROTOCOLS_TLS13 = {"TLSv1.3"}; + // Test behavior needs to adjust for OpenSSL 1.1.1-pre3 and above + private static final int OPENSSL_TLS13_SUPPORT_MIN_VERSION = 0x10101003; + + private static int OPENSSL_VERSION = TesterSupport.getOpensslVersion(); + + private static boolean hasTLS13() { + return OPENSSL_VERSION >= OPENSSL_TLS13_SUPPORT_MIN_VERSION; + } + + private SSLHostConfig initOpenSSLConfCmd(String... commands) throws Exception { + Assert.assertNotNull(commands); + Assert.assertTrue("Invalid length", commands.length % 2 == 0); + + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + + TesterSupport.initSsl(tomcat); + + Assert.assertTrue(connector.setProperty("sslImplementationName", OpenSSLImplementation.class.getName())); + + OpenSSLConf conf = new OpenSSLConf(); + for (int i = 0; i < commands.length;) { + OpenSSLConfCmd cmd = new OpenSSLConfCmd(); + cmd.setName(commands[i++]); + cmd.setValue(commands[i++]); + conf.addCmd(cmd); + } + + SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + Assert.assertEquals("Wrong SSLHostConfigCount", 1, sslHostConfigs.length); + sslHostConfigs[0].setOpenSslConf(conf); + + tomcat.start(); + + sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + Assert.assertEquals("Wrong SSLHostConfigCount", 1, sslHostConfigs.length); + return sslHostConfigs[0]; + } + + @Test + public void testOpenSSLConfCmdCipher() throws Exception { + SSLHostConfig sslHostConfig; + if (hasTLS13()) { + // Ensure TLSv1.3 ciphers aren't returned + sslHostConfig = initOpenSSLConfCmd("CipherString", ENABLED_CIPHER, + "CipherSuites", ""); + } else { + sslHostConfig = initOpenSSLConfCmd("CipherString", ENABLED_CIPHER); + } + String[] ciphers = sslHostConfig.getEnabledCiphers(); + MatcherAssert.assertThat("Wrong HostConfig ciphers", ciphers, + CoreMatchers.is(EXPECTED_CIPHERS)); + ciphers = SSLContext.getCiphers(sslHostConfig.getOpenSslContext().longValue()); + MatcherAssert.assertThat("Wrong native SSL context ciphers", ciphers, + CoreMatchers.is(EXPECTED_CIPHERS)); + } + + @Test + public void testOpenSSLConfCmdProtocol() throws Exception { + Set disabledProtocols = new HashSet<>(Arrays.asList(DISABLED_PROTOCOLS)); + StringBuilder sb = new StringBuilder(); + for (String protocol : DISABLED_PROTOCOLS) { + sb.append(',').append('-').append(protocol); + } + if (hasTLS13()) { + // Also disable TLSv1.3 + for (String protocol : DISABLED_PROTOCOLS_TLS13) { + sb.append(',').append('-').append(protocol); + disabledProtocols.add(protocol); + } + } + for (String protocol : ENABLED_PROTOCOLS) { + sb.append(',').append(protocol); + } + SSLHostConfig sslHostConfig = initOpenSSLConfCmd("Protocol", sb.substring(1)); + String[] protocols = sslHostConfig.getEnabledProtocols(); + for (String protocol : protocols) { + Assert.assertFalse("Protocol " + protocol + " is not allowed", + disabledProtocols.contains(protocol)); + } + Set enabledProtocols = new HashSet<>(Arrays.asList(protocols)); + for (String protocol : ENABLED_PROTOCOLS) { + Assert.assertTrue("Protocol " + protocol + " is not enabled", + enabledProtocols.contains(protocol)); + } + } + + + @Override + public void setUp() throws Exception { + super.setUp(); + + // Tests are only intended for OpenSSL + Assume.assumeTrue(TesterSupport.isOpensslAvailable()); + + Tomcat tomcat = getTomcatInstance(); + + AprLifecycleListener listener = new AprLifecycleListener(); + Assume.assumeTrue(AprLifecycleListener.isAprAvailable()); + StandardServer server = (StandardServer) tomcat.getServer(); + server.addLifecycleListener(listener); + } +} diff --git a/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java b/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java new file mode 100644 index 0000000..9b9eb5e --- /dev/null +++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java @@ -0,0 +1,1113 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +public class TestCipher { + + /* + * Checks that every cipher suite returned by OpenSSL is mapped to at least + * one cipher suite that is recognised by JSSE or is a cipher suite known + * not to be supported by JSSE. + */ + @Test + public void testAllOpenSSLCiphersMapped() throws Exception { + Set openSSLCipherSuites = TesterOpenSSL.getOpenSSLCiphersAsSet("ALL:eNULL"); + + StringBuilder errors = new StringBuilder(); + + for (String openSSLCipherSuite : openSSLCipherSuites) { + List jsseCipherSuites = + OpenSSLCipherConfigurationParser.parseExpression(openSSLCipherSuite); + + for (JsseImpl jsseImpl : JSSE_IMPLS) { + boolean found = false; + for (String jsseCipherSuite : jsseCipherSuites) { + if (jsseImpl.getStandardNames().contains(jsseCipherSuite)) { + found = true; + if (jsseImpl.getOpenSslUnmapped().contains(openSSLCipherSuite)) { + errors.append("Mapping found in " + jsseImpl.getVendor() + + "'s JSSE implementation for " + openSSLCipherSuite + + " when none was expected\n"); + } + break; + } + } + if (!found && !jsseImpl.getOpenSslUnmapped().contains(openSSLCipherSuite)) { + errors.append("No mapping found in " + jsseImpl.getVendor() + + "'s JSSE implementation for " + openSSLCipherSuite + + " when one was expected\n"); + } + } + } + Assert.assertTrue(errors.toString(), errors.length() == 0); + } + + + /* + * Checks that the unit tests are running with a version of OpenSSL that + * includes all the expected ciphers and does not include any unexpected + * ones. + */ + @Test + public void testOpenSSLCipherAvailability() throws Exception { + // OpenSSL does not include ECDH/ECDHE ciphers in all and there is no + // EC alias. Use aRSA. + // OpenSSL 1.0.0 onwards does not include eNULL in all. + Set availableCipherSuites = TesterOpenSSL.getOpenSSLCiphersAsSet("ALL:eNULL:aRSA"); + + Set expectedCipherSuites = new HashSet<>(); + for (Cipher cipher : Cipher.values()) { + if (TesterOpenSSL.OPENSSL_UNIMPLEMENTED_CIPHERS.contains(cipher)) { + continue; + } + expectedCipherSuites.add(cipher.getOpenSSLAlias() + "+" + + cipher.getProtocol().getOpenSSLName()); + } + + Set unavailableCipherSuites = new HashSet<>(expectedCipherSuites); + unavailableCipherSuites.removeAll(availableCipherSuites); + StringBuilder unavailableList = new StringBuilder("Unavailable cipher suites: "); + for (String cipher : unavailableCipherSuites) { + unavailableList.append(cipher); + unavailableList.append(' '); + } + Assert.assertEquals(unavailableList.toString(), 0, unavailableCipherSuites.size()); + + Set unexpectedCipherSuites = new HashSet<>(availableCipherSuites); + unexpectedCipherSuites.removeAll(expectedCipherSuites); + StringBuilder unexpectedList = new StringBuilder("Unexpected cipher suites: "); + for (String cipher : unexpectedCipherSuites) { + unexpectedList.append(cipher); + unexpectedList.append(' '); + } + Assert.assertEquals(unexpectedList.toString(), 0, unexpectedCipherSuites.size()); + } + + + /** + * Check that the elements of the Cipher enumeration are all using standard + * names from the TLS registry or are known exceptions. + */ + @Test + public void testNames() { + for (Cipher cipher : Cipher.values()) { + String name = cipher.name(); + // These do not appear in TLS registry + if (name.contains("FORTEZZA")) { + continue; + } + if (name.contains("EXPORT1024") || name.equals("TLS_DHE_DSS_WITH_RC4_128_SHA")) { + continue; + } + if (name.startsWith("SSL_CK") || name.startsWith("SSL2")) { + continue; + } + Assert.assertTrue("Non-registered name used in Cipher enumeration: " + cipher, + REGISTERED_NAMES.contains(name)); + } + } + + + /** + * These are all the Oracle standard Java names for cipher suites taken from + * http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#ciphersuites + * on 15th July 2014. + */ + private static final Set CIPHER_SUITE_STANDARD_NAMES_ORACLE = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5", + "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_anon_WITH_AES_128_CBC_SHA", + "TLS_DH_anon_WITH_AES_128_CBC_SHA256", + "TLS_DH_anon_WITH_AES_128_GCM_SHA256", + "TLS_DH_anon_WITH_AES_256_CBC_SHA", + "TLS_DH_anon_WITH_AES_256_CBC_SHA256", + "TLS_DH_anon_WITH_AES_256_GCM_SHA384", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", + "SSL_DH_anon_WITH_DES_CBC_SHA", + "SSL_DH_anon_WITH_RC4_128_MD5", + "TLS_DH_anon_WITH_SEED_CBC_SHA", + "SSL_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DH_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "SSL_DH_DSS_WITH_DES_CBC_SHA", + "TLS_DH_DSS_WITH_SEED_CBC_SHA", + "SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "SSL_DH_RSA_WITH_DES_CBC_SHA", + "TLS_DH_RSA_WITH_SEED_CBC_SHA", + "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA", + "SSL_DHE_DSS_EXPORT1024_WITH_RC4_56_SHA", + "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "SSL_DHE_DSS_WITH_DES_CBC_SHA", + "SSL_DHE_DSS_WITH_RC4_128_SHA", + "TLS_DHE_DSS_WITH_SEED_CBC_SHA", + "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384", + "TLS_DHE_PSK_WITH_NULL_SHA", + "TLS_DHE_PSK_WITH_NULL_SHA256", + "TLS_DHE_PSK_WITH_NULL_SHA384", + "TLS_DHE_PSK_WITH_RC4_128_SHA", + "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "SSL_DHE_RSA_WITH_DES_CBC_SHA", + "TLS_DHE_RSA_WITH_SEED_CBC_SHA", + "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", + "TLS_ECDH_anon_WITH_NULL_SHA", + "TLS_ECDH_anon_WITH_RC4_128_SHA", + "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_NULL_SHA", + "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_NULL_SHA", + "TLS_ECDH_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_NULL_SHA", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_NULL_SHA", + "TLS_ECDHE_PSK_WITH_NULL_SHA256", + "TLS_ECDHE_PSK_WITH_NULL_SHA384", + "TLS_ECDHE_PSK_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_NULL_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV", + "SSL_FORTEZZA_DMS_WITH_FORTEZZA_CBC_SHA", + "SSL_FORTEZZA_DMS_WITH_NULL_SHA", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC4_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC4_40_SHA", + "TLS_KRB5_WITH_3DES_EDE_CBC_MD5", + "TLS_KRB5_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_MD5", + "TLS_KRB5_WITH_DES_CBC_SHA", + "TLS_KRB5_WITH_IDEA_CBC_MD5", + "TLS_KRB5_WITH_IDEA_CBC_SHA", + "TLS_KRB5_WITH_RC4_128_MD5", + "TLS_KRB5_WITH_RC4_128_SHA", + "TLS_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_PSK_WITH_AES_128_CBC_SHA", + "TLS_PSK_WITH_AES_128_CBC_SHA256", + "TLS_PSK_WITH_AES_128_GCM_SHA256", + "TLS_PSK_WITH_AES_256_CBC_SHA", + "TLS_PSK_WITH_AES_256_CBC_SHA384", + "TLS_PSK_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_NULL_SHA", + "TLS_PSK_WITH_NULL_SHA256", + "TLS_PSK_WITH_NULL_SHA384", + "TLS_PSK_WITH_RC4_128_SHA", + "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5", + "SSL_RSA_EXPORT_WITH_RC4_40_MD5", + "SSL_RSA_EXPORT1024_WITH_DES_CBC_SHA", + "SSL_RSA_EXPORT1024_WITH_RC4_56_SHA", + "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_FIPS_WITH_DES_CBC_SHA", + "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_NULL_SHA", + "TLS_RSA_PSK_WITH_NULL_SHA256", + "TLS_RSA_PSK_WITH_NULL_SHA384", + "TLS_RSA_PSK_WITH_RC4_128_SHA", + "SSL_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "SSL_RSA_WITH_DES_CBC_SHA", + "SSL_RSA_WITH_IDEA_CBC_SHA", + "SSL_RSA_WITH_NULL_MD5", + "SSL_RSA_WITH_NULL_SHA", + "TLS_RSA_WITH_NULL_SHA256", + "SSL_RSA_WITH_RC4_128_MD5", + "SSL_RSA_WITH_RC4_128_SHA", + "TLS_RSA_WITH_SEED_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_256_CBC_SHA"))); + + + /** + * These are the cipher suites implemented by OpenSSL that are not + * implemented by Oracle's JSSE implementation. + */ + private static Set OPENSSL_UNMAPPED_ORACLE = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "AES128-CCM+TLSv1.2", + "AES128-CCM8+TLSv1.2", + "AES256-CCM+TLSv1.2", + "AES256-CCM8+TLSv1.2", + "ARIA128-GCM-SHA256+TLSv1.2", + "ARIA256-GCM-SHA384+TLSv1.2", + "DES-CBC-MD5+SSLv2", + "DES-CBC3-MD5+SSLv2", + "DHE-DSS-ARIA128-GCM-SHA256+TLSv1.2", + "DHE-DSS-ARIA256-GCM-SHA384+TLSv1.2", + "DHE-PSK-AES128-CCM+TLSv1.2", + "DHE-PSK-AES128-CCM8+TLSv1.2", + "DHE-PSK-AES256-CCM+TLSv1.2", + "DHE-PSK-AES256-CCM8+TLSv1.2", + "DHE-PSK-ARIA128-GCM-SHA256+TLSv1.2", + "DHE-PSK-ARIA256-GCM-SHA384+TLSv1.2", + "DHE-PSK-CAMELLIA128-SHA256+TLSv1", + "DHE-PSK-CAMELLIA256-SHA384+TLSv1", + "DHE-PSK-CHACHA20-POLY1305+TLSv1.2", + "DHE-RSA-AES128-CCM+TLSv1.2", + "DHE-RSA-AES128-CCM8+TLSv1.2", + "DHE-RSA-AES256-CCM+TLSv1.2", + "DHE-RSA-AES256-CCM8+TLSv1.2", + "DHE-RSA-ARIA128-GCM-SHA256+TLSv1.2", + "DHE-RSA-ARIA256-GCM-SHA384+TLSv1.2", + "ECDHE-ARIA128-GCM-SHA256+TLSv1.2", + "ECDHE-ARIA256-GCM-SHA384+TLSv1.2", + "DHE-RSA-CHACHA20-POLY1305+TLSv1.2", + "ECDH-ECDSA-CAMELLIA128-SHA256+TLSv1.2", + "ECDH-ECDSA-CAMELLIA256-SHA384+TLSv1.2", + "ECDH-RSA-CAMELLIA128-SHA256+TLSv1.2", + "ECDH-RSA-CAMELLIA256-SHA384+TLSv1.2", + "ECDHE-ECDSA-AES128-CCM+TLSv1.2", + "ECDHE-ECDSA-AES128-CCM8+TLSv1.2", + "ECDHE-ECDSA-AES256-CCM+TLSv1.2", + "ECDHE-ECDSA-AES256-CCM8+TLSv1.2", + "ECDHE-ECDSA-ARIA128-GCM-SHA256+TLSv1.2", + "ECDHE-ECDSA-ARIA256-GCM-SHA384+TLSv1.2", + "ECDHE-ECDSA-CAMELLIA128-SHA256+TLSv1.2", + "ECDHE-ECDSA-CAMELLIA256-SHA384+TLSv1.2", + "ECDHE-ECDSA-CHACHA20-POLY1305+TLSv1.2", + "ECDHE-PSK-CAMELLIA128-SHA256+TLSv1", + "ECDHE-PSK-CAMELLIA256-SHA384+TLSv1", + "ECDHE-PSK-CHACHA20-POLY1305+TLSv1.2", + "ECDHE-RSA-CAMELLIA128-SHA256+TLSv1.2", + "ECDHE-RSA-CAMELLIA256-SHA384+TLSv1.2", + "ECDHE-RSA-CHACHA20-POLY1305+TLSv1.2", + "EXP-RC2-CBC-MD5+SSLv2", + "EXP-RC4-MD5+SSLv2", + "IDEA-CBC-MD5+SSLv2", + "PSK-AES128-CCM+TLSv1.2", + "PSK-AES128-CCM8+TLSv1.2", + "PSK-AES256-CCM+TLSv1.2", + "PSK-AES256-CCM8+TLSv1.2", + "PSK-ARIA128-GCM-SHA256+TLSv1.2", + "PSK-ARIA256-GCM-SHA384+TLSv1.2", + "PSK-CAMELLIA128-SHA256+TLSv1", + "PSK-CAMELLIA256-SHA384+TLSv1", + "PSK-CHACHA20-POLY1305+TLSv1.2", + "RC2-CBC-MD5+SSLv2", + "RC4-MD5+SSLv2", + "RSA-PSK-ARIA128-GCM-SHA256+TLSv1.2", + "RSA-PSK-ARIA256-GCM-SHA384+TLSv1.2", + "RSA-PSK-CAMELLIA128-SHA256+TLSv1", + "RSA-PSK-CAMELLIA256-SHA384+TLSv1", + "RSA-PSK-CHACHA20-POLY1305+TLSv1.2", + "TLS_AES_128_CCM_SHA256+TLSv1.3", + "TLS_AES_128_CCM_8_SHA256+TLSv1.3", + "TLS_AES_128_GCM_SHA256+TLSv1.3", + "TLS_AES_256_GCM_SHA384+TLSv1.3", + "TLS_CHACHA20_POLY1305_SHA256+TLSv1.3"))); + + + /** + * These are all the IBM standard Java names for cipher suites taken from + * http://www-01.ibm.com/support/knowledgecenter/SSYKE2_7.0.0/com.ibm.java.security.component.71.doc/security-component/jsse2Docs/ciphersuites.html?lang=en + * on 29th July 2014. + *
    + * As of 16 February 2015 the list for IBM Java 7 was identical to that for + * IBM Java 8 + * http://www-01.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.security.component.80.doc/security-component/jsse2Docs/ciphersuites.html?lang=en + *
    + * Note that IBM cipher suites names can begin with TLS or SSL. + */ + private static final Set CIPHER_SUITE_STANDARD_NAMES_IBM; + + static { + Set sslNames = new HashSet<>(Arrays.asList( + "SSL_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "SSL_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "SSL_RSA_WITH_AES_256_CBC_SHA256", + "SSL_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", + "SSL_ECDH_RSA_WITH_AES_256_CBC_SHA384", + "SSL_DHE_RSA_WITH_AES_256_CBC_SHA256", + "SSL_DHE_DSS_WITH_AES_256_CBC_SHA256", + "SSL_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "SSL_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "SSL_RSA_WITH_AES_256_CBC_SHA", + "SSL_ECDH_ECDSA_WITH_AES_256_CBC_SHA", + "SSL_ECDH_RSA_WITH_AES_256_CBC_SHA", + "SSL_DHE_RSA_WITH_AES_256_CBC_SHA", + "SSL_DHE_DSS_WITH_AES_256_CBC_SHA", + "SSL_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "SSL_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "SSL_RSA_WITH_AES_128_CBC_SHA256", + "SSL_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "SSL_ECDH_RSA_WITH_AES_128_CBC_SHA256", + "SSL_DHE_RSA_WITH_AES_128_CBC_SHA256", + "SSL_DHE_DSS_WITH_AES_128_CBC_SHA256", + "SSL_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "SSL_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "SSL_RSA_WITH_AES_128_CBC_SHA", + "SSL_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "SSL_ECDH_RSA_WITH_AES_128_CBC_SHA", + "SSL_DHE_RSA_WITH_AES_128_CBC_SHA", + "SSL_DHE_DSS_WITH_AES_128_CBC_SHA", + "SSL_ECDHE_ECDSA_WITH_RC4_128_SHA", + "SSL_ECDHE_RSA_WITH_RC4_128_SHA", + "SSL_RSA_WITH_RC4_128_SHA", + "SSL_ECDH_ECDSA_WITH_RC4_128_SHA", + "SSL_ECDH_RSA_WITH_RC4_128_SHA", + "SSL_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "SSL_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", + "SSL_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_RC4_128_MD5", + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV", + "SSL_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "SSL_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "SSL_RSA_WITH_AES_256_GCM_SHA384", + "SSL_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", + "SSL_ECDH_RSA_WITH_AES_256_GCM_SHA384", + "SSL_DHE_DSS_WITH_AES_256_GCM_SHA384", + "SSL_DHE_RSA_WITH_AES_256_GCM_SHA384", + "SSL_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "SSL_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "SSL_RSA_WITH_AES_128_GCM_SHA256", + "SSL_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + "SSL_ECDH_RSA_WITH_AES_128_GCM_SHA256", + "SSL_DHE_RSA_WITH_AES_128_GCM_SHA256", + "SSL_DHE_DSS_WITH_AES_128_GCM_SHA256", + "SSL_DH_anon_WITH_AES_256_CBC_SHA256", + "SSL_ECDH_anon_WITH_AES_256_CBC_SHA", + "SSL_DH_anon_WITH_AES_256_CBC_SHA", + "SSL_DH_anon_WITH_AES_256_GCM_SHA384", + "SSL_DH_anon_WITH_AES_128_GCM_SHA256", + "SSL_DH_anon_WITH_AES_128_CBC_SHA256", + "SSL_ECDH_anon_WITH_AES_128_CBC_SHA", + "SSL_DH_anon_WITH_AES_128_CBC_SHA", + "SSL_ECDH_anon_WITH_RC4_128_SHA", + "SSL_DH_anon_WITH_RC4_128_MD5", + "SSL_ECDH_anon_WITH_3DES_EDE_CBC_SHA", + "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_NULL_SHA256", + "SSL_ECDHE_ECDSA_WITH_NULL_SHA", + "SSL_ECDHE_RSA_WITH_NULL_SHA", + "SSL_RSA_WITH_NULL_SHA", + "SSL_ECDH_ECDSA_WITH_NULL_SHA", + "SSL_ECDH_RSA_WITH_NULL_SHA", + "SSL_ECDH_anon_WITH_NULL_SHA", + "SSL_RSA_WITH_NULL_MD5", + "SSL_RSA_WITH_DES_CBC_SHA", + "SSL_DHE_RSA_WITH_DES_CBC_SHA", + "SSL_DHE_DSS_WITH_DES_CBC_SHA", + "SSL_DH_anon_WITH_DES_CBC_SHA", + "SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_FIPS_WITH_DES_EDE_CBC_SHA", + "SSL_DHE_DSS_WITH_RC4_128_SHA", + "SSL_RSA_EXPORT_WITH_RC4_40_MD5", + "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5", + "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + "SSL_KRB5_WITH_RC4_128_SHA", + "SSL_KRB5_WITH_RC4_128_MD5", + "SSL_KRB5_WITH_3DES_EDE_CBC_SHA", + "SSL_KRB5_WITH_3DES_EDE_CBC_MD5", + "SSL_KRB5_WITH_DES_CBC_SHA", + "SSL_KRB5_WITH_DES_CBC_MD5", + "SSL_KRB5_EXPORT_WITH_RC4_40_SHA", + "SSL_KRB5_EXPORT_WITH_RC4_40_MD5", + "SSL_KRB5_EXPORT_WITH_DES_CBC_40_SHA", + "SSL_KRB5_EXPORT_WITH_DES_CBC_40_MD5", + "SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5")); + + Set allNames = new HashSet<>(sslNames); + + for (String sslName : sslNames) { + allNames.add("TLS" + sslName.substring(3)); + } + + CIPHER_SUITE_STANDARD_NAMES_IBM = Collections.unmodifiableSet(allNames); + } + + + /** + * These are the cipher suites implemented by OpenSSL that are not + * implemented by IBM's JSSE implementation. + */ + private static Set OPENSSL_UNMAPPED_IBM = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "ADH-CAMELLIA128-SHA+SSLv3", + "ADH-CAMELLIA256-SHA+SSLv3", + "ADH-CAMELLIA128-SHA256+TLSv1.2", + "ADH-CAMELLIA256-SHA256+TLSv1.2", + "ADH-SEED-SHA+SSLv3", + "AES128-CCM+TLSv1.2", + "AES128-CCM8+TLSv1.2", + "AES256-CCM+TLSv1.2", + "AES256-CCM8+TLSv1.2", + "ARIA128-GCM-SHA256+TLSv1.2", + "ARIA256-GCM-SHA384+TLSv1.2", + "CAMELLIA128-SHA+SSLv3", + "CAMELLIA256-SHA+SSLv3", + "CAMELLIA128-SHA256+TLSv1.2", + "CAMELLIA256-SHA256+TLSv1.2", + "DES-CBC-MD5+SSLv2", + "DES-CBC3-MD5+SSLv2", + "DH-DSS-AES128-GCM-SHA256+TLSv1.2", + "DH-DSS-AES256-GCM-SHA384+TLSv1.2", + "DH-DSS-AES128-SHA+SSLv3", + "DH-DSS-AES128-SHA256+TLSv1.2", + "DH-DSS-AES256-SHA+SSLv3", + "DH-DSS-AES256-SHA256+TLSv1.2", + "DH-DSS-CAMELLIA128-SHA+SSLv3", + "DH-DSS-CAMELLIA128-SHA256+TLSv1.2", + "DH-DSS-CAMELLIA256-SHA+SSLv3", + "DH-DSS-CAMELLIA256-SHA256+TLSv1.2", + "DH-DSS-DES-CBC-SHA+SSLv3", + "DH-DSS-DES-CBC3-SHA+SSLv3", + "DH-DSS-SEED-SHA+SSLv3", + "DH-RSA-AES128-GCM-SHA256+TLSv1.2", + "DH-RSA-AES256-GCM-SHA384+TLSv1.2", + "DH-RSA-AES128-SHA+SSLv3", + "DH-RSA-AES128-SHA256+TLSv1.2", + "DH-RSA-AES256-SHA+SSLv3", + "DH-RSA-AES256-SHA256+TLSv1.2", + "DH-RSA-CAMELLIA128-SHA+SSLv3", + "DH-RSA-CAMELLIA128-SHA256+TLSv1.2", + "DH-RSA-CAMELLIA256-SHA+SSLv3", + "DH-RSA-CAMELLIA256-SHA256+TLSv1.2", + "DH-RSA-DES-CBC-SHA+SSLv3", + "DH-RSA-DES-CBC3-SHA+SSLv3", + "DH-RSA-SEED-SHA+SSLv3", + "DHE-DSS-ARIA128-GCM-SHA256+TLSv1.2", + "DHE-DSS-ARIA256-GCM-SHA384+TLSv1.2", + "DHE-DSS-CAMELLIA128-SHA+SSLv3", + "DHE-DSS-CAMELLIA128-SHA256+TLSv1.2", + "DHE-DSS-CAMELLIA256-SHA+SSLv3", + "DHE-DSS-CAMELLIA256-SHA256+TLSv1.2", + "DHE-DSS-SEED-SHA+SSLv3", + "DHE-PSK-3DES-EDE-CBC-SHA+SSLv3", + "DHE-PSK-AES128-CBC-SHA+SSLv3", + "DHE-PSK-AES128-CBC-SHA256+TLSv1", + "DHE-PSK-AES128-CCM+TLSv1.2", + "DHE-PSK-AES128-CCM8+TLSv1.2", + "DHE-PSK-AES128-GCM-SHA256+TLSv1.2", + "DHE-PSK-AES256-CBC-SHA+SSLv3", + "DHE-PSK-AES256-CBC-SHA384+TLSv1", + "DHE-PSK-AES256-CCM+TLSv1.2", + "DHE-PSK-AES256-CCM8+TLSv1.2", + "DHE-PSK-AES256-GCM-SHA384+TLSv1.2", + "DHE-PSK-ARIA128-GCM-SHA256+TLSv1.2", + "DHE-PSK-ARIA256-GCM-SHA384+TLSv1.2", + "DHE-PSK-CAMELLIA128-SHA256+TLSv1", + "DHE-PSK-CAMELLIA256-SHA384+TLSv1", + "DHE-PSK-CHACHA20-POLY1305+TLSv1.2", + "DHE-PSK-NULL-SHA+SSLv3", + "DHE-PSK-NULL-SHA256+TLSv1", + "DHE-PSK-NULL-SHA384+TLSv1", + "DHE-PSK-RC4-SHA+SSLv3", + "DHE-RSA-AES128-CCM+TLSv1.2", + "DHE-RSA-AES128-CCM8+TLSv1.2", + "DHE-RSA-AES256-CCM+TLSv1.2", + "DHE-RSA-AES256-CCM8+TLSv1.2", + "DHE-RSA-ARIA128-GCM-SHA256+TLSv1.2", + "DHE-RSA-ARIA256-GCM-SHA384+TLSv1.2", + "DHE-RSA-CAMELLIA128-SHA+SSLv3", + "DHE-RSA-CAMELLIA128-SHA256+TLSv1.2", + "DHE-RSA-CAMELLIA256-SHA+SSLv3", + "DHE-RSA-CAMELLIA256-SHA256+TLSv1.2", + "DHE-RSA-CHACHA20-POLY1305+TLSv1.2", + "DHE-RSA-SEED-SHA+SSLv3", + "ECDH-ECDSA-CAMELLIA128-SHA256+TLSv1.2", + "ECDH-ECDSA-CAMELLIA256-SHA384+TLSv1.2", + "ECDH-RSA-CAMELLIA128-SHA256+TLSv1.2", + "ECDH-RSA-CAMELLIA256-SHA384+TLSv1.2", + "ECDHE-ARIA128-GCM-SHA256+TLSv1.2", + "ECDHE-ARIA256-GCM-SHA384+TLSv1.2", + "ECDHE-ECDSA-AES128-CCM+TLSv1.2", + "ECDHE-ECDSA-AES128-CCM8+TLSv1.2", + "ECDHE-ECDSA-AES256-CCM+TLSv1.2", + "ECDHE-ECDSA-AES256-CCM8+TLSv1.2", + "ECDHE-ECDSA-ARIA128-GCM-SHA256+TLSv1.2", + "ECDHE-ECDSA-ARIA256-GCM-SHA384+TLSv1.2", + "ECDHE-ECDSA-CAMELLIA128-SHA256+TLSv1.2", + "ECDHE-ECDSA-CAMELLIA256-SHA384+TLSv1.2", + "ECDHE-ECDSA-CHACHA20-POLY1305+TLSv1.2", + "ECDHE-PSK-3DES-EDE-CBC-SHA+SSLv3", + "ECDHE-PSK-AES128-CBC-SHA+TLSv1", + "ECDHE-PSK-AES128-CBC-SHA256+TLSv1", + "ECDHE-PSK-AES256-CBC-SHA+TLSv1", + "ECDHE-PSK-AES256-CBC-SHA384+TLSv1", + "ECDHE-PSK-CAMELLIA128-SHA256+TLSv1", + "ECDHE-PSK-CAMELLIA256-SHA384+TLSv1", + "ECDHE-PSK-CHACHA20-POLY1305+TLSv1.2", + "ECDHE-PSK-NULL-SHA+TLSv1", + "ECDHE-PSK-NULL-SHA256+TLSv1", + "ECDHE-PSK-NULL-SHA384+TLSv1", + "ECDHE-PSK-RC4-SHA+SSLv3", + "ECDHE-RSA-CAMELLIA128-SHA256+TLSv1.2", + "ECDHE-RSA-CAMELLIA256-SHA384+TLSv1.2", + "ECDHE-RSA-CHACHA20-POLY1305+TLSv1.2", + "EXP-DH-DSS-DES-CBC-SHA+SSLv3", + "EXP-DH-RSA-DES-CBC-SHA+SSLv3", + "EXP-RC2-CBC-MD5+SSLv2", + "EXP-RC4-MD5+SSLv2", + "IDEA-CBC-MD5+SSLv2", + "IDEA-CBC-SHA+SSLv3", + "PSK-3DES-EDE-CBC-SHA+SSLv3", + "PSK-AES128-CBC-SHA+SSLv3", + "PSK-AES128-CBC-SHA256+TLSv1", + "PSK-AES128-CCM+TLSv1.2", + "PSK-AES128-CCM8+TLSv1.2", + "PSK-AES128-GCM-SHA256+TLSv1.2", + "PSK-AES256-CBC-SHA+SSLv3", + "PSK-AES256-CBC-SHA384+TLSv1", + "PSK-AES256-CCM+TLSv1.2", + "PSK-AES256-CCM8+TLSv1.2", + "PSK-AES256-GCM-SHA384+TLSv1.2", + "PSK-ARIA128-GCM-SHA256+TLSv1.2", + "PSK-ARIA256-GCM-SHA384+TLSv1.2", + "PSK-CAMELLIA128-SHA256+TLSv1", + "PSK-CAMELLIA256-SHA384+TLSv1", + "PSK-CHACHA20-POLY1305+TLSv1.2", + "PSK-NULL-SHA+SSLv3", + "PSK-NULL-SHA256+TLSv1", + "PSK-NULL-SHA384+TLSv1", + "PSK-RC4-SHA+SSLv3", + "RC2-CBC-MD5+SSLv2", + "RC4-MD5+SSLv2", + "RSA-PSK-3DES-EDE-CBC-SHA+SSLv3", + "RSA-PSK-AES128-CBC-SHA+SSLv3", + "RSA-PSK-AES128-CBC-SHA256+TLSv1", + "RSA-PSK-AES128-GCM-SHA256+TLSv1.2", + "RSA-PSK-AES256-CBC-SHA+SSLv3", + "RSA-PSK-AES256-CBC-SHA384+TLSv1", + "RSA-PSK-AES256-GCM-SHA384+TLSv1.2", + "RSA-PSK-ARIA128-GCM-SHA256+TLSv1.2", + "RSA-PSK-ARIA256-GCM-SHA384+TLSv1.2", + "RSA-PSK-CAMELLIA128-SHA256+TLSv1", + "RSA-PSK-CAMELLIA256-SHA384+TLSv1", + "RSA-PSK-CHACHA20-POLY1305+TLSv1.2", + "RSA-PSK-NULL-SHA+SSLv3", + "RSA-PSK-NULL-SHA256+TLSv1", + "RSA-PSK-NULL-SHA384+TLSv1", + "RSA-PSK-RC4-SHA+SSLv3", + "SEED-SHA+SSLv3", + "SRP-AES-128-CBC-SHA+SSLv3", + "SRP-AES-256-CBC-SHA+SSLv3", + "SRP-3DES-EDE-CBC-SHA+SSLv3", + "SRP-DSS-3DES-EDE-CBC-SHA+SSLv3", + "SRP-DSS-AES-128-CBC-SHA+SSLv3", + "SRP-DSS-AES-256-CBC-SHA+SSLv3", + "SRP-RSA-3DES-EDE-CBC-SHA+SSLv3", + "SRP-RSA-AES-128-CBC-SHA+SSLv3", + "SRP-RSA-AES-256-CBC-SHA+SSLv3", + "TLS_AES_128_CCM_SHA256+TLSv1.3", + "TLS_AES_128_CCM_8_SHA256+TLSv1.3", + "TLS_AES_128_GCM_SHA256+TLSv1.3", + "TLS_AES_256_GCM_SHA384+TLSv1.3", + "TLS_CHACHA20_POLY1305_SHA256+TLSv1.3"))); + + + private static JsseImpl ORACLE_JSSE_CIPHER_IMPL = new JsseImpl("Oracle", + CIPHER_SUITE_STANDARD_NAMES_ORACLE, OPENSSL_UNMAPPED_ORACLE); + + + private static JsseImpl IBM_JSSE_CIPHER_IMPL = new JsseImpl("IBM", + CIPHER_SUITE_STANDARD_NAMES_IBM, OPENSSL_UNMAPPED_IBM); + + + private static Set JSSE_IMPLS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(ORACLE_JSSE_CIPHER_IMPL, IBM_JSSE_CIPHER_IMPL))); + + + private static class JsseImpl { + private final String vendor; + private final Set standardNames; + private final Set openSslUnmapped; + + JsseImpl(String vendor, Set standardNames, + Set openSslUnmapped) { + this.vendor = vendor; + this.standardNames = standardNames; + this.openSslUnmapped = openSslUnmapped; + } + + public String getVendor() { + return vendor; + } + + public Set getStandardNames() { + return standardNames; + } + + public Set getOpenSslUnmapped() { + return openSslUnmapped; + } + } + + + // Retrieved on 30 July 2014 from + // http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-4 + private static Set REGISTERED_NAMES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + "TLS_NULL_WITH_NULL_NULL", + "TLS_RSA_WITH_NULL_MD5", + "TLS_RSA_WITH_NULL_SHA", + "TLS_RSA_EXPORT_WITH_RC4_40_MD5", + "TLS_RSA_WITH_RC4_128_MD5", + "TLS_RSA_WITH_RC4_128_SHA", + "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_RSA_WITH_IDEA_CBC_SHA", + "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_RSA_WITH_DES_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_DSS_WITH_DES_CBC_SHA", + "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_RSA_WITH_DES_CBC_SHA", + "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DHE_DSS_WITH_DES_CBC_SHA", + "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DHE_RSA_WITH_DES_CBC_SHA", + "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", + "TLS_DH_anon_WITH_RC4_128_MD5", + "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", + "TLS_DH_anon_WITH_DES_CBC_SHA", + "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_SHA", + "TLS_KRB5_WITH_3DES_EDE_CBC_SHA", + "TLS_KRB5_WITH_RC4_128_SHA", + "TLS_KRB5_WITH_IDEA_CBC_SHA", + "TLS_KRB5_WITH_DES_CBC_MD5", + "TLS_KRB5_WITH_3DES_EDE_CBC_MD5", + "TLS_KRB5_WITH_RC4_128_MD5", + "TLS_KRB5_WITH_IDEA_CBC_MD5", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", + "TLS_KRB5_EXPORT_WITH_RC4_40_SHA", + "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", + "TLS_KRB5_EXPORT_WITH_RC4_40_MD5", + "TLS_PSK_WITH_NULL_SHA", + "TLS_DHE_PSK_WITH_NULL_SHA", + "TLS_RSA_PSK_WITH_NULL_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DH_anon_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DH_anon_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_NULL_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DH_anon_WITH_AES_128_CBC_SHA256", + "TLS_DH_anon_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", + "TLS_PSK_WITH_RC4_128_SHA", + "TLS_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_PSK_WITH_AES_128_CBC_SHA", + "TLS_PSK_WITH_AES_256_CBC_SHA", + "TLS_DHE_PSK_WITH_RC4_128_SHA", + "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_PSK_WITH_RC4_128_SHA", + "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_SEED_CBC_SHA", + "TLS_DH_DSS_WITH_SEED_CBC_SHA", + "TLS_DH_RSA_WITH_SEED_CBC_SHA", + "TLS_DHE_DSS_WITH_SEED_CBC_SHA", + "TLS_DHE_RSA_WITH_SEED_CBC_SHA", + "TLS_DH_anon_WITH_SEED_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DH_DSS_WITH_AES_128_GCM_SHA256", + "TLS_DH_DSS_WITH_AES_256_GCM_SHA384", + "TLS_DH_anon_WITH_AES_128_GCM_SHA256", + "TLS_DH_anon_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_AES_128_GCM_SHA256", + "TLS_PSK_WITH_AES_256_GCM_SHA384", + "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256", + "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", + "TLS_PSK_WITH_AES_128_CBC_SHA256", + "TLS_PSK_WITH_AES_256_CBC_SHA384", + "TLS_PSK_WITH_NULL_SHA256", + "TLS_PSK_WITH_NULL_SHA384", + "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_NULL_SHA256", + "TLS_DHE_PSK_WITH_NULL_SHA384", + "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_NULL_SHA256", + "TLS_RSA_PSK_WITH_NULL_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV", + "TLS_ECDH_ECDSA_WITH_NULL_SHA", + "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_NULL_SHA", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_RSA_WITH_NULL_SHA", + "TLS_ECDH_RSA_WITH_RC4_128_SHA", + "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_NULL_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_anon_WITH_NULL_SHA", + "TLS_ECDH_anon_WITH_RC4_128_SHA", + "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", + "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", + "TLS_SRP_SHA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", + "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_RC4_128_SHA", + "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_NULL_SHA", + "TLS_ECDHE_PSK_WITH_NULL_SHA256", + "TLS_ECDHE_PSK_WITH_NULL_SHA384", + "TLS_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256", + "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384", + "TLS_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256", + "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384", + "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256", + "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384", + "TLS_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384", + "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256", + "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384", + "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", + "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", + "TLS_RSA_WITH_AES_128_CCM", + "TLS_RSA_WITH_AES_256_CCM", + "TLS_DHE_RSA_WITH_AES_128_CCM", + "TLS_DHE_RSA_WITH_AES_256_CCM", + "TLS_RSA_WITH_AES_128_CCM_8", + "TLS_RSA_WITH_AES_256_CCM_8", + "TLS_DHE_RSA_WITH_AES_128_CCM_8", + "TLS_DHE_RSA_WITH_AES_256_CCM_8", + "TLS_PSK_WITH_AES_128_CCM", + "TLS_PSK_WITH_AES_256_CCM", + "TLS_DHE_PSK_WITH_AES_128_CCM", + "TLS_DHE_PSK_WITH_AES_256_CCM", + "TLS_PSK_WITH_AES_128_CCM_8", + "TLS_PSK_WITH_AES_256_CCM_8", + "TLS_PSK_DHE_WITH_AES_128_CCM_8", + "TLS_PSK_DHE_WITH_AES_256_CCM_8", + "TLS_ECDHE_ECDSA_WITH_AES_128_CCM", + "TLS_ECDHE_ECDSA_WITH_AES_256_CCM", + "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8", + "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8", + // From https://tools.ietf.org/html/draft-ietf-tls-chacha20-poly1305-04 + // These might change. + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256", + "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256", + "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256", + // TLV 1.3 draft 26 - subject to change + "TLS_AES_128_CCM_SHA256", + "TLS_AES_128_CCM_8_SHA256", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256" + ))); +} diff --git a/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java b/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java new file mode 100644 index 0000000..1c2b946 --- /dev/null +++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java @@ -0,0 +1,618 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +import java.util.List; +import java.util.TreeSet; + +import org.junit.Assert; +import org.junit.Test; + +public class TestOpenSSLCipherConfigurationParser { + + @Test + public void testDEFAULT() throws Exception { + testSpecification("DEFAULT"); + } + + + @Test + public void testCOMPLEMENTOFDEFAULT() throws Exception { + testSpecification("COMPLEMENTOFDEFAULT"); + } + + + @Test + public void testALL() throws Exception { + testSpecification("ALL"); + } + + + @Test + public void testCOMPLEMENTOFALL() throws Exception { + testSpecification("COMPLEMENTOFALL"); + } + + + @Test + public void testaNULL() throws Exception { + testSpecification("aNULL"); + } + + + @Test + public void testeNULL() throws Exception { + testSpecification("eNULL"); + } + + + @Test + public void testHIGH() throws Exception { + if (TesterOpenSSL.VERSION < 30200) { + // OpenSSL 3.2.x moved the CCM8 ciphers from high to medium + testSpecification("HIGH:!AESCCM8"); + } else { + testSpecification("HIGH"); + } + } + + + @Test + public void testMEDIUM() throws Exception { + if (TesterOpenSSL.VERSION < 30200) { + // OpenSSL 3.2.x moved the CCM8 ciphers from high to medium + testSpecification("MEDIUM:AESCCM8"); + } else { + testSpecification("MEDIUM"); + } + } + + + @Test + public void testLOW() throws Exception { + testSpecification("LOW"); + } + + + @Test + public void testEXPORT40() throws Exception { + testSpecification("EXPORT40"); + } + + + @Test + public void testEXPORT() throws Exception { + testSpecification("EXPORT"); + } + + + @Test + public void testRSA() throws Exception { + testSpecification("RSA"); + } + + + @Test + public void testaRSA() throws Exception { + testSpecification("aRSA"); + } + + + @Test + public void testkRSA() throws Exception { + testSpecification("kRSA"); + } + + + @Test + public void testkEDH() throws Exception { + testSpecification("kEDH"); + } + + + @Test + public void testkDHE() throws Exception { + testSpecification("kDHE"); + } + + + @Test + public void testEDH() throws Exception { + testSpecification("EDH"); + } + + + @Test + public void testDHE() throws Exception { + testSpecification("DHE"); + } + + + @Test + public void testkDHr() throws Exception { + testSpecification("kDHr"); + } + + + @Test + public void testkDHd() throws Exception { + testSpecification("kDHd"); + } + + + @Test + public void testkDH() throws Exception { + testSpecification("kDH"); + } + + + @Test + public void testkECDHr() throws Exception { + testSpecification("kECDHr"); + } + + + @Test + public void testkECDHe() throws Exception { + testSpecification("kECDHe"); + } + + + @Test + public void testkECDH() throws Exception { + testSpecification("kECDH"); + } + + + @Test + public void testkEECDH() throws Exception { + testSpecification("kEECDH"); + } + + + @Test + public void testECDH() throws Exception { + testSpecification("ECDH"); + } + + + @Test + public void testkECDHE() throws Exception { + testSpecification("kECDHE"); + } + + + @Test + public void testECDHE() throws Exception { + testSpecification("ECDHE"); + } + + + @Test + public void testAECDH() throws Exception { + testSpecification("AECDH"); + } + + + @Test + public void testDSS() throws Exception { + testSpecification("DSS"); + } + + + @Test + public void testaDSS() throws Exception { + testSpecification("aDSS"); + } + + + @Test + public void testaDH() throws Exception { + testSpecification("aDH"); + } + + + @Test + public void testaECDH() throws Exception { + testSpecification("aECDH"); + } + + + @Test + public void testaECDSA() throws Exception { + testSpecification("aECDSA"); + } + + + @Test + public void testECDSA() throws Exception { + testSpecification("ECDSA"); + } + + + @Test + public void testkFZA() throws Exception { + testSpecification("kFZA"); + } + + + @Test + public void testaFZA() throws Exception { + testSpecification("aFZA"); + } + + + @Test + public void testeFZA() throws Exception { + testSpecification("eFZA"); + } + + + @Test + public void testFZA() throws Exception { + testSpecification("FZA"); + } + + + @Test + public void testTLSv1_2() throws Exception { + testSpecification("TLSv1.2"); + } + + + @Test + public void testTLSv1() throws Exception { + testSpecification("TLSv1"); + } + + + @Test + public void testSSLv3() throws Exception { + testSpecification("SSLv3"); + } + + + @Test + public void testSSLv2() throws Exception { + testSpecification("SSLv2"); + } + + + @Test + public void testDH() throws Exception { + testSpecification("DH"); + } + + + @Test + public void testADH() throws Exception { + testSpecification("ADH"); + } + + + @Test + public void testAES128() throws Exception { + testSpecification("AES128"); + } + + + @Test + public void testAES256() throws Exception { + testSpecification("AES256"); + } + + + @Test + public void testAES() throws Exception { + testSpecification("AES"); + } + + + @Test + public void testAESGCM() throws Exception { + testSpecification("AESGCM"); + } + + + @Test + public void testAESCCM() throws Exception { + testSpecification("AESCCM"); + } + + + @Test + public void testAESCCM8() throws Exception { + testSpecification("AESCCM8"); + } + + + @Test + public void testCAMELLIA128() throws Exception { + testSpecification("CAMELLIA128"); + } + + + @Test + public void testCAMELLIA256() throws Exception { + testSpecification("CAMELLIA256"); + } + + + @Test + public void testCAMELLIA() throws Exception { + testSpecification("CAMELLIA"); + } + + + @Test + public void testCHACHA20() throws Exception { + testSpecification("CHACHA20"); + } + + + @Test + public void test3DES() throws Exception { + testSpecification("3DES"); + } + + + @Test + public void testDES() throws Exception { + testSpecification("DES"); + } + + + @Test + public void testRC4() throws Exception { + testSpecification("RC4"); + } + + + @Test + public void testRC2() throws Exception { + testSpecification("RC2"); + } + + + @Test + public void testIDEA() throws Exception { + testSpecification("IDEA"); + } + + + @Test + public void testSEED() throws Exception { + testSpecification("SEED"); + } + + + @Test + public void testMD5() throws Exception { + testSpecification("MD5"); + } + + + @Test + public void testSHA1() throws Exception { + testSpecification("SHA1"); + } + + + @Test + public void testSHA() throws Exception { + testSpecification("SHA"); + } + + + @Test + public void testSHA256() throws Exception { + testSpecification("SHA256"); + } + + + @Test + public void testSHA384() throws Exception { + testSpecification("SHA384"); + } + + + @Test + public void testKRB5() throws Exception { + testSpecification("KRB5"); + } + + + @Test + public void testaGOST() throws Exception { + testSpecification("aGOST"); + } + + + @Test + public void testaGOST01() throws Exception { + testSpecification("aGOST01"); + } + + + @Test + public void testaGOST94() throws Exception { + testSpecification("aGOST94"); + } + + + @Test + public void testkGOST() throws Exception { + testSpecification("kGOST"); + } + + + @Test + public void testGOST94() throws Exception { + testSpecification("GOST94"); + } + + + @Test + public void testGOST89MAC() throws Exception { + testSpecification("GOST89MAC"); + } + + + @Test + public void testaPSK() throws Exception { + testSpecification("aPSK"); + } + + + @Test + public void testkPSK() throws Exception { + testSpecification("kPSK"); + } + + + @Test + public void testkRSAPSK() throws Exception { + testSpecification("kRSAPSK"); + } + + + @Test + public void testkECDHEPSK() throws Exception { + testSpecification("kECDHEPSK"); + } + + + @Test + public void testkDHEPSK() throws Exception { + testSpecification("kDHEPSK"); + } + + + @Test + public void testPSK() throws Exception { + testSpecification("PSK"); + } + + + @Test + public void testARIA() throws Exception { + testSpecification("ARIA"); + } + + + @Test + public void testARIA128() throws Exception { + testSpecification("ARIA128"); + } + + + @Test + public void testARIA256() throws Exception { + testSpecification("ARIA256"); + } + + + // TODO: Add tests for the individual operators + + @Test + public void testSpecification01() throws Exception { + // Tomcat 8 default as of 2014-08-04 + // This gets an A- from https://www.ssllabs.com/ssltest with no FS for + // a number of the reference browsers + if (TesterOpenSSL.VERSION < 30200) { + // OpenSSL 3.2.x moved the CCM8 ciphers from high to medium + testSpecification("HIGH:!AESCCM8:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5"); + } else { + testSpecification("HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5"); + } + } + + + @Test + public void testSpecification02() throws Exception { + // Suggestion from dev list (s/ECDHE/kEECDH/, s/DHE/EDH/ + testSpecification("!aNULL:!eNULL:!EXPORT:!DSS:!DES:!SSLv2:kEECDH:ECDH:EDH:AES256-GCM-SHA384:AES128-GCM-SHA256:+RC4:HIGH:aRSA:kECDHr:MEDIUM"); + } + + + @Test + public void testSpecification03() throws Exception { + // Reported as failing during 8.0.11 release vote by Ognjen Blagojevic + // EDH was introduced in 1.0.0 + testSpecification("EECDH+aRSA+SHA384:EECDH:EDH+aRSA:RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS"); + } + + + /* + * Cipher string extracted from https://bz.apache.org/bugzilla/show_bug.cgi?id=67628 + */ + @Test + public void testSpecification04() throws Exception { + if (TesterOpenSSL.VERSION < 30200) { + // OpenSSL 3.2.x moved the CCM8 ciphers from high to medium + testSpecification("HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK:!DSS:!SHA1:!SHA256:!SHA384:!AESCCM8"); + } else { + testSpecification("HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK:!DSS:!SHA1:!SHA256:!SHA384:"); + } + } + + + private void testSpecification(String specification) throws Exception { + // Filter out cipher suites that OpenSSL does not implement + String openSSLCipherList = TesterOpenSSL.getOpenSSLCiphersAsExpression(specification); + List jsseCipherListFromOpenSSL = + OpenSSLCipherConfigurationParser.parseExpression(openSSLCipherList); + List jsseCipherListFromParser = + OpenSSLCipherConfigurationParser.parseExpression(specification); + + TesterOpenSSL.removeUnimplementedCiphersJsse(jsseCipherListFromParser); + + // First check the lists have the same entries + // Order is NOT important at this point. It is checked below. + Assert.assertEquals( + "Expected " + jsseCipherListFromParser.size() + " ciphers but got " + + jsseCipherListFromOpenSSL.size() + " for the specification '" + + specification + "'", + new TreeSet<>(jsseCipherListFromParser), new TreeSet<>(jsseCipherListFromOpenSSL)); + + // OpenSSL treats many ciphers as having equal preference. The order + // returned depends on the order they are requested. The following code + // checks that the Parser produces a cipher list that is consistent with + // OpenSSL's preference order by confirming that running through OpenSSL + // does not change the order. + String parserOrderedExpression = listToString(jsseCipherListFromParser, ','); + Assert.assertEquals( + listToString(OpenSSLCipherConfigurationParser.parseExpression( + parserOrderedExpression), ','), + parserOrderedExpression); + } + + + private String listToString(List list, char separator) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (String entry : list) { + if (first) { + first = false; + } else { + sb.append(separator); + } + sb.append(entry); + } + return sb.toString(); + } +} diff --git a/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParserOnly.java b/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParserOnly.java new file mode 100644 index 0000000..795e977 --- /dev/null +++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParserOnly.java @@ -0,0 +1,113 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +import java.util.LinkedHashSet; + +import org.junit.Assert; +import org.junit.Test; + + +/* + * The unit test is independent of OpenSSL version and does not require OpenSSL + * to be present. + */ +public class TestOpenSSLCipherConfigurationParserOnly { + + @Test + public void testDefaultSort01() throws Exception { + // Reproducing a failure observed on Gump with OpenSSL 1.1.x + + // Everything else being equal, AES is preferred + LinkedHashSet input = new LinkedHashSet<>(); + input.add(Cipher.TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384); + input.add(Cipher.TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384); + input.add(Cipher.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384); + input.add(Cipher.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384); + LinkedHashSet result = OpenSSLCipherConfigurationParser.defaultSort(input); + + LinkedHashSet expected = new LinkedHashSet<>(); + expected.add(Cipher.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384); + expected.add(Cipher.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384); + expected.add(Cipher.TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384); + expected.add(Cipher.TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384); + + Assert.assertEquals(expected.toString(), result.toString()); + } + + @Test + public void testDefaultSort02() throws Exception { + // Reproducing a failure observed on Gump with OpenSSL 1.1.x + + // ECHDE should beat AES + LinkedHashSet input = new LinkedHashSet<>(); + input.add(Cipher.TLS_RSA_WITH_AES_256_CBC_SHA); + input.add(Cipher.TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384); + LinkedHashSet result = OpenSSLCipherConfigurationParser.defaultSort(input); + + LinkedHashSet expected = new LinkedHashSet<>(); + expected.add(Cipher.TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384); + expected.add(Cipher.TLS_RSA_WITH_AES_256_CBC_SHA); + + Assert.assertEquals(expected.toString(), result.toString()); + } + + @Test + public void testDefaultSort03() throws Exception { + // Reproducing a failure observed on Gump with OpenSSL 1.1.x + + // AES should beat CAMELLIA + LinkedHashSet input = new LinkedHashSet<>(); + input.add(Cipher.TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384); + input.add(Cipher.TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384); + LinkedHashSet result = OpenSSLCipherConfigurationParser.defaultSort(input); + + LinkedHashSet expected = new LinkedHashSet<>(); + expected.add(Cipher.TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384); + expected.add(Cipher.TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384); + + Assert.assertEquals(expected.toString(), result.toString()); + } + + @Test + public void testRename01() throws Exception { + // EDH -> DHE + LinkedHashSet result = + OpenSSLCipherConfigurationParser.parse("EXP-EDH-DSS-DES-CBC-SHA"); + LinkedHashSet expected = new LinkedHashSet<>(); + expected.add(Cipher.TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA); + + Assert.assertEquals(expected, result); + } + + @Test + public void testCustomOrdering() throws Exception { + // https://bz.apache.org/bugzilla/show_bug.cgi?id=59081 + LinkedHashSet result = OpenSSLCipherConfigurationParser.parse( + "ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:" + + "DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:DES-CBC3-SHA"); + LinkedHashSet expected = new LinkedHashSet<>(); + expected.add(Cipher.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384); + expected.add(Cipher.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA); + expected.add(Cipher.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA); + expected.add(Cipher.TLS_DHE_RSA_WITH_AES_256_CBC_SHA); + expected.add(Cipher.TLS_DHE_RSA_WITH_AES_128_CBC_SHA); + expected.add(Cipher.TLS_RSA_WITH_3DES_EDE_CBC_SHA); + + Assert.assertEquals(expected.toString(), result.toString()); + } +} diff --git a/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java b/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java new file mode 100644 index 0000000..6f51c27 --- /dev/null +++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java @@ -0,0 +1,404 @@ +/* + * 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.tomcat.util.net.openssl.ciphers; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.catalina.util.IOTools; + +public class TesterOpenSSL { + + public static final int VERSION; + + public static final Set OPENSSL_UNIMPLEMENTED_CIPHERS; + + public static final Map OPENSSL_RENAMED_CIPHERS; + + static { + // Note: The following lists are intended to be aligned with the most + // recent release of each OpenSSL release branch. Running the unit + // tests with earlier releases is likely to result in failures. + + String versionString = null; + try { + versionString = executeOpenSSLCommand("version"); + } catch (IOException e) { + versionString = ""; + } + if (versionString.startsWith("OpenSSL 3.3.")) { + // Note: Gump currently tests 11.x with OpenSSL 3.3.x + VERSION = 30300; + } else if (versionString.startsWith("OpenSSL 3.2.")) { + VERSION = 30200; + } else if (versionString.startsWith("OpenSSL 3.1.")) { + VERSION = 30100; + } else if (versionString.startsWith("OpenSSL 3.0.")) { + VERSION = 30000; + } else if (versionString.startsWith("OpenSSL 1.1.1")) { + // LTS + // Supported until at least 2023-09-11 + // Note: Gump currently tests 9.x and earlier with OpenSSL 1.1.1[x] + VERSION = 10101; + // Note: Release branches 1.1.0 and earlier are no longer supported by + // the OpenSSL team so these tests don't support them either. + } else { + VERSION = -1; + } + + HashSet unimplemented = new HashSet<>(); + + // These have been removed from all supported versions. + unimplemented.add(Cipher.TLS_DHE_DSS_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA); + unimplemented.add(Cipher.TLS_RSA_EXPORT1024_WITH_DES_CBC_SHA); + unimplemented.add(Cipher.TLS_RSA_EXPORT1024_WITH_RC2_CBC_56_MD5); + unimplemented.add(Cipher.TLS_DHE_DSS_EXPORT1024_WITH_RC4_56_SHA); + unimplemented.add(Cipher.TLS_RSA_EXPORT1024_WITH_RC4_56_SHA); + unimplemented.add(Cipher.TLS_RSA_EXPORT1024_WITH_RC4_56_MD5); + unimplemented.add(Cipher.TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256); + unimplemented.add(Cipher.TLS_DH_anon_WITH_DES_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_anon_EXPORT_WITH_RC4_40_MD5); + unimplemented.add(Cipher.TLS_DHE_RSA_WITH_DES_CBC_SHA); + unimplemented.add(Cipher.TLS_DHE_DSS_WITH_DES_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_DES_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_DES_CBC_SHA); + unimplemented.add(Cipher.TLS_RSA_WITH_DES_CBC_SHA); + unimplemented.add(Cipher.SSL2_DES_64_CBC_WITH_MD5); + unimplemented.add(Cipher.TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA); + unimplemented.add(Cipher.TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA); + unimplemented.add(Cipher.TLS_RSA_EXPORT_WITH_DES40_CBC_SHA); + unimplemented.add(Cipher.TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5); + unimplemented.add(Cipher.TLS_RSA_EXPORT_WITH_RC4_40_MD5); + unimplemented.add(Cipher.SSL_CK_RC2_128_CBC_EXPORT40_WITH_MD5); + unimplemented.add(Cipher.SSL_CK_RC2_128_CBC_WITH_MD5); + unimplemented.add(Cipher.SSL_CK_RC4_128_WITH_MD5); + unimplemented.add(Cipher.SSL2_RC4_128_EXPORT40_WITH_MD5); + unimplemented.add(Cipher.SSL2_IDEA_128_CBC_WITH_MD5); + unimplemented.add(Cipher.SSL2_DES_192_EDE3_CBC_WITH_MD5); + // These were removed in 1.1.0 so won't be available from that + // version onwards. + unimplemented.add(Cipher.TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_AES_128_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_AES_128_CBC_SHA256); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_AES_128_GCM_SHA256); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_AES_256_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_AES_256_CBC_SHA256); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_AES_256_GCM_SHA384); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_DSS_WITH_SEED_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_AES_128_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_AES_128_CBC_SHA256); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_AES_128_GCM_SHA256); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_AES_256_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_AES_256_CBC_SHA256); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_AES_256_GCM_SHA384); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_RSA_WITH_SEED_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_NULL_SHA); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_NULL_SHA); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_AES_128_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_AES_256_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256); + unimplemented.add(Cipher.TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256); + unimplemented.add(Cipher.TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384); + unimplemented.add(Cipher.TLS_RSA_WITH_RC4_128_MD5); + unimplemented.add(Cipher.TLS_DH_anon_WITH_RC4_128_MD5); + unimplemented.add(Cipher.TLS_ECDHE_PSK_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_RSA_PSK_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_ECDHE_RSA_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_RSA_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_PSK_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_DHE_PSK_WITH_RC4_128_SHA); + unimplemented.add(Cipher.TLS_ECDH_anon_WITH_RC4_128_SHA); + // 3DES requires a compile time switch to enable. Treat as removed. + unimplemented.add(Cipher.TLS_PSK_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_anon_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_RSA_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA); + unimplemented.add(Cipher.TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA); + + // These are TLS v1.3 cipher suites + // Java does not currently support these so they are excluded from the + // testing. + // Note: If OpenSSL is used then some of these may be available + // depending on the OpenSSL version used and the defaults for that + // version + unimplemented.add(Cipher.TLS_AES_128_CCM_8_SHA256); + unimplemented.add(Cipher.TLS_AES_128_CCM_SHA256); + unimplemented.add(Cipher.TLS_AES_128_GCM_SHA256); + unimplemented.add(Cipher.TLS_AES_256_GCM_SHA384); + unimplemented.add(Cipher.TLS_CHACHA20_POLY1305_SHA256); + + if (VERSION < 30000) { + // No new ciphers in 3.0.0 so far + } else { + // These were moved to the legacy provider in 3.0.0 so won't be + // available from that version onwards. + unimplemented.add(Cipher.TLS_RSA_WITH_IDEA_CBC_SHA); + unimplemented.add(Cipher.TLS_DH_anon_WITH_SEED_CBC_SHA); + unimplemented.add(Cipher.TLS_DHE_DSS_WITH_SEED_CBC_SHA); + unimplemented.add(Cipher.TLS_RSA_WITH_SEED_CBC_SHA); + unimplemented.add(Cipher.TLS_DHE_RSA_WITH_SEED_CBC_SHA); + } + + String skipCiphers = System.getProperty("tomcat.test.openssl.unimplemented", ""); + if (!skipCiphers.isEmpty()) { + String[] skip = skipCiphers.split(","); + for (Cipher c : Cipher.values()) { + for (String s : skip) { + if (c.toString().contains(s)) { + unimplemented.add(c); + } + } + } + } + + OPENSSL_UNIMPLEMENTED_CIPHERS = Collections.unmodifiableSet(unimplemented); + + Map renamed = new HashMap<>(); + renamed.put("ECDH-ECDSA-RC4-SHA+SSLv3", "ECDH-ECDSA-RC4-SHA+TLSv1"); + renamed.put("ECDHE-ECDSA-NULL-SHA+SSLv3", "ECDHE-ECDSA-NULL-SHA+TLSv1"); + renamed.put("ECDHE-ECDSA-DES-CBC3-SHA+SSLv3", "ECDHE-ECDSA-DES-CBC3-SHA+TLSv1"); + renamed.put("ECDHE-ECDSA-AES128-SHA+SSLv3", "ECDHE-ECDSA-AES128-SHA+TLSv1"); + renamed.put("ECDHE-ECDSA-AES256-SHA+SSLv3", "ECDHE-ECDSA-AES256-SHA+TLSv1"); + renamed.put("ECDHE-RSA-NULL-SHA+SSLv3", "ECDHE-RSA-NULL-SHA+TLSv1"); + renamed.put("ECDHE-RSA-RC4-SHA+SSLv3", "ECDHE-RSA-RC4-SHA+TLSv1"); + renamed.put("ECDHE-RSA-DES-CBC3-SHA+SSLv3", "ECDHE-RSA-DES-CBC3-SHA+TLSv1"); + renamed.put("ECDHE-RSA-AES128-SHA+SSLv3", "ECDHE-RSA-AES128-SHA+TLSv1"); + renamed.put("ECDHE-RSA-AES256-SHA+SSLv3", "ECDHE-RSA-AES256-SHA+TLSv1"); + renamed.put("AECDH-NULL-SHA+SSLv3", "AECDH-NULL-SHA+TLSv1"); + renamed.put("AECDH-RC4-SHA+SSLv3", "AECDH-RC4-SHA+TLSv1"); + renamed.put("AECDH-DES-CBC3-SHA+SSLv3", "AECDH-DES-CBC3-SHA+TLSv1"); + renamed.put("AECDH-AES128-SHA+SSLv3", "AECDH-AES128-SHA+TLSv1"); + renamed.put("AECDH-AES256-SHA+SSLv3", "AECDH-AES256-SHA+TLSv1"); + renamed.put("ECDHE-PSK-RC4-SHA+SSLv3", "ECDHE-PSK-RC4-SHA+TLSv1"); + renamed.put("ECDHE-PSK-3DES-EDE-CBC-SHA+SSLv3", "ECDHE-PSK-3DES-EDE-CBC-SHA+TLSv1"); + renamed.put("ECDHE-PSK-AES128-CBC-SHA+SSLv3", "ECDHE-PSK-AES128-CBC-SHA+TLSv1"); + renamed.put("ECDHE-PSK-AES256-CBC-SHA+SSLv3", "ECDHE-PSK-AES256-CBC-SHA+TLSv1"); + renamed.put("ECDHE-PSK-NULL-SHA+SSLv3", "ECDHE-PSK-NULL-SHA+TLSv1"); + OPENSSL_RENAMED_CIPHERS = Collections.unmodifiableMap(renamed); + } + + + private TesterOpenSSL() { + // Utility class. Hide default constructor. + } + + + public static Set getOpenSSLCiphersAsSet(String specification) throws Exception { + String[] ciphers = getOpenSSLCiphersAsExpression(specification).trim().split(":"); + Set result = new HashSet<>(Arrays.asList(ciphers)); + return result; + } + + + public static String getOpenSSLCiphersAsExpression(String specification) throws Exception { + + List args = new ArrayList<>(); + // Standard command to list the ciphers + args.add("ciphers"); + args.add("-v"); + // Need to exclude the TLSv1.3 ciphers + args.add("-ciphersuites"); + args.add(""); + // Include the specification if provided + if (specification != null) { + args.add(specification); + } + + String stdout = executeOpenSSLCommand(args.toArray(new String[0])); + + if (stdout.length() == 0) { + return stdout; + } + + StringBuilder output = new StringBuilder(); + boolean first = true; + + // OpenSSL should have returned one cipher per line + String ciphers[] = stdout.split("\n"); + for (String cipher : ciphers) { + // Handle rename for 1.1.0 onwards + cipher = cipher.replace("EDH", "DHE"); + if (first) { + first = false; + } else { + output.append(':'); + } + StringBuilder name = new StringBuilder(); + + // Name is first part + int i = cipher.indexOf(' '); + name.append(cipher.substring(0, i)); + + // Advance i past the space + while (Character.isWhitespace(cipher.charAt(i))) { + i++; + } + + // Protocol is the second + int j = cipher.indexOf(' ', i); + name.append('+'); + name.append(cipher.substring(i, j)); + + // More renames + if (OPENSSL_RENAMED_CIPHERS.containsKey(name.toString())) { + output.append(OPENSSL_RENAMED_CIPHERS.get(name.toString())); + } else { + output.append(name.toString()); + } + } + return output.toString(); + } + + + /* + * Use this method to filter parser results when comparing them to OpenSSL + * results to take account of unimplemented cipher suites. + */ + public static void removeUnimplementedCiphersJsse(List list) { + for (Cipher cipher : OPENSSL_UNIMPLEMENTED_CIPHERS) { + for (String jsseName : cipher.getJsseNames()) { + list.remove(jsseName); + } + } + } + + + private static String executeOpenSSLCommand(String... args) throws IOException { + String openSSLPath = System.getProperty("tomcat.test.openssl.path"); + String openSSLLibPath = null; + if (openSSLPath == null || openSSLPath.length() == 0) { + openSSLPath = "openssl"; + } else { + // Explicit OpenSSL path may also need explicit lib path + // (e.g. Gump needs this) + openSSLLibPath = openSSLPath.substring(0, openSSLPath.lastIndexOf('/')); + openSSLLibPath = openSSLLibPath + "/../:" + openSSLLibPath + "/../lib:" + openSSLLibPath + "/../lib64"; + } + List cmd = new ArrayList<>(); + cmd.add(openSSLPath); + cmd.addAll(Arrays.asList(args)); + + ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0])); + + if (openSSLLibPath != null) { + Map env = pb.environment(); + String libraryPath = env.get("LD_LIBRARY_PATH"); + if (libraryPath == null) { + libraryPath = openSSLLibPath; + } else { + libraryPath = libraryPath + ":" + openSSLLibPath; + } + env.put("LD_LIBRARY_PATH", libraryPath); + } + + Process p = pb.start(); + + InputStreamToText stdout = new InputStreamToText(p.getInputStream()); + InputStreamToText stderr = new InputStreamToText(p.getErrorStream()); + + Thread t1 = new Thread(stdout); + t1.setName("OpenSSL stdout reader"); + t1.start(); + + Thread t2 = new Thread(stderr); + t2.setName("OpenSSL stderr reader"); + t2.start(); + + try { + t1.join(); + t2.join(); + } catch (InterruptedException e) { + throw new IOException(e); + } + + String errorText = stderr.getText(); + if (errorText.length() > 0) { + System.err.println(errorText); + } + + return stdout.getText().trim(); + } + + private static class InputStreamToText implements Runnable { + + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private final InputStream is; + + InputStreamToText(InputStream is) { + this.is = is; + } + + @Override + public void run() { + try { + IOTools.flow(is, baos); + } catch (IOException e) { + // Ignore + } + } + + public String getText() { + return baos.toString(); + } + } +} diff --git a/test/org/apache/tomcat/util/net/user1.jks b/test/org/apache/tomcat/util/net/user1.jks new file mode 100644 index 0000000000000000000000000000000000000000..16435067428fbc4bfccb596a60314d7c933a7abb GIT binary patch literal 4386 zcmd6pWmME#+r}s98izQ5bc4VQ4BZ_P(n{9|LpMq(Lr6&pNC-Hzl!q1sB&1YO5ReuW zM-Uu3MigH-p0n0@&+~kIKAaDG{nxec`@^;N|K7iApME|43IG5=XAAhpf(8eo1CVDb z@pLFw6aavN5m?Ai1Puv=2ni4Z6b8Y7Kqvr$g>0lBbu0-@UmX=JI5g)1HeclGlMLv| zw?7I4+P|xUZNs7`1&u|s(-M&ZR~nr@OXaV;Vav>kNFi>f&)@-Qz$(P$AizP*r< zF(pfP88S*vp5ZBtepKyJ$Dx}hg?MAYa{fXuBj*xpL%1U)@Te3=OdEbo&`&O|T=t!866(rk>pfPx4cd{D7JcC+Kq7aRQ6@XQxa7zQtCbhKpqTBSmxVSnS z=<+hH+S3m#xuGqNi^s+!7g5U?z6pKrXal{a3oQ}*G^{y?!(ffVaSddep?O_1d2cumOmP7!__n8mW==;9KXFwBETB6%jU z51&i5J`2%i7zte>{}?{Kdsoamt-dgVIh0Psat?Fpec!@m;tmo;z84YpVUXdAE}AvT zQagxZV-nlxF&+8VBu2~#Kx%pA_pC{`RVCq*laP z32XCBQ@y%9l?xqniK#E@M93ew7(YSQ*;+reHn?_axh~>6+_NT*tGCK1JW@muz0oh9 zz?NY=r~REiWKcNDQ8TW-cw39%BNe5Qjpk4Amy1|`{_$o>`ciJu zG$X|TV~r-w!~+HLxfM`Sc>yyOGfsmCThN3EBG-b@8;3g?DvrS#7})X3#gE% z`jE;kTzrL5*VNt`GYhh?sV@=`6V)V~f^lOxPFEKrS&38S2>teBQaSQ zm4#nJXY7`GI@tHcT|E>V_DAZMzwpOtwOsJ{YRf)ay z4f=HFW_E$r%4flEG$@^qb~)v_)V)$K}aayCUQtv@9F8m8O5r#d#V z@pYF+m`q2O4Jdq;x@q6*POB;ugu21 z)iRi+N7xuD7DnL=al)fR6c1AOIumPI4-{F@$Y3hE_XPtCNe;a4L&kViV#{3-&&PDb ztUtbpSHF`*HQohTS4XVY;};>C*U9J0duTk??A7Gw)-En+p7QU_8V_t4>134jmejXc z?a(BI(g7Iz^)%{8qv||6=D4$=uh^JVzlY_RX-zm?x+g^a|AqQ|oy;#jQtML9{G9ckMG z3l&M9*LSA#6-V-IWvY^we^!f|sPV@H*<9berZTaMFFtluT3cSEkTd>+kkG4QQV6*- zBJv=xP!0qZO!^QE0)jvwI$8uZ3Au2*Zu}875J(9HAfoRfDG}r(U<(Kd9Ashkmmq_K z4Aqbq5On7iQaHJllPCH*2JPjF6hQEu?@_?n&E3(QYJN^GXCE`)>p`JT0ccKb-(VkC zryz{450VA(+j%1;9QJ3Exv#g2QxNABLk%Q5;^KJ?3}^WNHFI=e5T}M3f}M$4LJ}d1 zlt!W?B~X&qXHx1+O8qGj(XoFS{ikb9EhHQP`{kO#3ghLC@o@_}bMrs3O@pBNW&2n3 zAh1C0zn|w>+JLb@TEN*p1qcfS0)}H{fa;cgIzp!>rPxEHl|DTu2`cd*5BA7x!h22V z&YJv*!KRlq22Zy640CuBe`71V{E%sxQ|)5!YSGtjvAZqrg(buWABQt2L61XKUzmTloYaVBKHIGb;eqPUXt}7)Och8f-M_Tum=!i`}0C=sGfhgilr#Ht{ zGiE)aJQ>Ui%QBz?*F?4sqWx$ON;WbEAG-&{z`npsDVzCjxen45eQJ-a?@W53ZLwXc z(opUzJ3rf$sz#czbfXicu&YZ}+`0vNxlkMJ>hkqKN?yw&if7PkKTOc?M9umT8j&W; zH#A=PEt2+5Oa7L>-6P@syYnD05CANVPDDh<|1o21a0mziKwKgrcZ}Bs#viK#sUaMU zY{q_QA2TxpP93xlI>0Fi?aHZv4hX_r$DEA|8o_pUJ!%(@aJKYa#En z@TIvsl#wGm4&IzNH*t#`Jlf_h)fejl2QL?zMH#viqP=p$-!2 zeYnrh<&k048-yjlftC;v8|TKANkHC<#-hQ*E4(tek>t9O*WuI^gyE__m8`}DV{JZ9 z-66{AP5Oe*`FVzy+LWfBep`nlYCTfl3@AKS|LJ{Sr%igV+V|E`A;;u_so{rrI0L7W zB0v{+2vda4g_$|=#cgiiStanhK08#oT~xiClVvTj{f5QQ?WxTM_hSxrDsn#8s3qCk zDxne314WXcQX74EH^}$n+KiGb|K?Ny83QVLZwm^Uq{zMeDm3|c!a5f@+|uomnWR`F z;V=5ei1S660B1S9n``V5DB{bhBv)wNGq;W5-FGfe#f-{g1S*#uKY2_IrB03;MCmPX zs!sbCee-WCS}ixLd{b&t8w)YkBpvyx3C#C|sd7fYtmxip+06i$!mVEQRaUmsasOa{ z&XLj0wbI~a%V1Mi$xtJ*tJ%pzA-2Kfu+eZTx4mz4O(T$H{X)OVT}_;>M&^8nVG~MQ zwO^Wbza5-8YNy;2R1Q)N86_KjdtY=-gtL=QhK%C%1G$#$WsX7Rsv&3qC;7{8I6h`; z?mj~bY(iyl5SwO=7rUT3)O}H-G4-v+y&=gMfkd&5`K8NPHl>SrxOI>3@ z|H)9$IYU8b3_ZJ^^(9(_(Md%%d6-4t{wAqEo3CYFsgU16!Rhz(;Fmbh4^{$G^~9z3 zl`NRBO}7-e81j4W6S@^V?K7OJuR?>f!=i{DG0cTDr=9Ny(a@byp1ry6Z{4^c1{AfM z3M4%QbrG-j2Iun6&(bMXMo-`haB!ovUfoHj?$oOCYfpMqvw=u!L5Z(G!FPtJ@!Pfu zHHq!_s@DYv<-dnyfMdF+D8=m`5y}FD!WfAgc}XMH9^0=lcBZznskl#*ST}(OhkG`A zqtx`D)R&T$F;wUU*)SH4S2Ke`YL@p1nkY57frGCE!xFqVHPSv`-2)#kqo@#NE#cc9 zV@lW9>l1i-*&;0eeTv!JO{TKNY|CpyFpWd_Ocm8`0Z4mPq5wWjxc)S~33Qxz5sOTb{@zW5?tnMHi2x&GdQ0M;rj-8Y3vMSLoGVd7c*?DRQoDkq8k4Z+-q-*7q zu_@%f;qQkf{0r;M!$Y(N6}(VOoK8uLm$3XHKI<`~tW*V`(rzKYQ*lz-$gERgRYzB0 zJvZCM-16=~;X{6ochnUaqSf8Tc=SoDB>AX8PJds6eTGVvmfAdd@H?>M6ZP-p6#*yV z&4~}Iz5mRR6GZg!-=g5Zl0JI)R~AI~Afmhedm7-P^U9)g{+(9=`#0d6zcbF#5{du= zfj|F&pFdIyx;_HD4oy-~f{8yS?-jm8N?{mX5XFsICHkBfh z=?>fb7j9PNICy#uun3m0m#HO2YHsK1>r{R8kL+JGi;3vqw{sR?2Z95^h5_N5W!^B8 z$Wl{weC5@az{r~Ev0xQd2x$u5xFqk4n?@|Zx9U5rnKR{9TG#CEQ`mSg}g9 zg%L!loZSKkhzBVvPd%1*$0Ouj{QQUD_A}}nSe1`d6}~jNVzj+K?0ak@Z{@7Dk=h#w zRm?=+%UYC4&7<|IGTUoehX`%e!UM&Fe+DtKL!jxgQfNwMbF()#o{4G zvRknK?rFFCj85g)loL-LHCeDxj3bo^p&+U?F+9BZ3+8F~lS|09K-;f*y2~u>+{9+_ zLfaxIVNWTSmiXkVE!NqMtBh^(QfZV63Q&{`L!D|5C#uyxW8<1G=x{HJDPH%WRq~Kc zStpYCn-5L8mUEi3@fK7)>!X%)7UKRb_aZqCRnzH(RK(3-#_>12pM5FY7DMaft6AeJ zZu=BFvMe1~2j-QPh-4mUFBa#D<%??bvjx~KT3A1h*}g~dfbEA_DhS>3B49nrbbPnr a=k!qyiQ5!XRea8Va-MQTysJi} CJK_LOCALES; + + static { + CJK_LOCALES = new HashSet<>(); + CJK_LOCALES.add(Locale.SIMPLIFIED_CHINESE); + CJK_LOCALES.add(Locale.JAPANESE); + CJK_LOCALES.add(Locale.KOREAN); + } + + @Test + public void testNullKey() { + boolean iaeThrown = false; + + try { + sm.getString(null); + } catch (IllegalArgumentException iae) { + iaeThrown = true; + } + Assert.assertTrue("IAE not thrown on null key", iaeThrown); + } + + @Test + public void testBug46933() { + // Check null args are OK + sm.getString("namingContext.nameNotBound"); + sm.getString("namingContext.nameNotBound", (Object[]) null); + sm.getString("namingContext.nameNotBound", new Object[1]); + } + + @Test + public void testFrench() { + StringManager sm = StringManager.getManager(PACKAGE_NAME, Locale.FRENCH); + Assert.assertEquals(Locale.FRENCH, sm.getLocale()); + } + + @Test + public void testMissingWithTccl() { + Thread.currentThread().setContextClassLoader(TestStringManager.class.getClassLoader()); + StringManager sm = StringManager.getManager("org.does.not.exist"); + Assert.assertNull(sm.getLocale()); + } + + + @Test + public void testMissingNullTccl() { + Thread.currentThread().setContextClassLoader(null); + StringManager sm = StringManager.getManager("org.does.not.exist"); + Assert.assertNull(sm.getLocale()); + } + + + @Test + public void testVersionLoggerListenerAlignment() throws Exception{ + // Get full list of properties from English + InputStream is = TestStringManager.class.getClassLoader().getResourceAsStream("org/apache/catalina/startup/LocalStrings.properties"); + Properties props = new Properties(); + props.load(is); + Set versionLoggerListenerKeys = new HashSet<>(); + for (Object key : props.keySet()) { + if (key instanceof String) { + if (((String) key).startsWith("versionLoggerListener.")) { + versionLoggerListenerKeys.add((String) key); + } + } + } + + for (Locale locale : ALL_LOCALES) { + testVersionLoggerListenerAlignment(versionLoggerListenerKeys, locale); + } + } + + + private void testVersionLoggerListenerAlignment(Set keys, Locale locale) { + System.out.println("\n" + locale.getDisplayName()); + StringManager sm = StringManager.getManager("org.apache.catalina.startup", locale); + int standardLength = -1; + for (String key : keys) { + String fullLine = sm.getString(key, "XXX"); + // Provides a visual check but be aware CJK characters may be + // displayed using full width (1 CJK character uses the space of two + // ASCII characters) as assumed by this test or may use a narrower + // representation. + System.out.println(fullLine); + int insertIndex = fullLine.indexOf("XXX"); + String preInsert = fullLine.substring(0, insertIndex); + int length = getFixedWidth(preInsert, locale); + if (standardLength == -1) { + standardLength = length; + } else { + Assert.assertEquals(locale.getDisplayName() + " - " + key, standardLength, length); + } + } + } + + + private int getFixedWidth(String s, Locale l) { + if (CJK_LOCALES.contains(l)) { + // This isn't perfect but it is good enough for this test. + // The test assumes CJK characters are all displayed double width + // Ubuntu uses double width characters by default. + // Eclipse uses 1.5 width characters by default. + int len = 0; + for (char c : s.toCharArray()) { + if (c < 128) { + len ++; + } else { + len += 2; + } + } + return len; + } else { + return s.length(); + } + } +} diff --git a/test/org/apache/tomcat/util/scan/FooSCI.java b/test/org/apache/tomcat/util/scan/FooSCI.java new file mode 100644 index 0000000..be45d4f --- /dev/null +++ b/test/org/apache/tomcat/util/scan/FooSCI.java @@ -0,0 +1,37 @@ +/* + * 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.tomcat.util.scan; + +import java.util.Set; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.annotation.HandlesTypes; + +import org.apache.tomcat.util.scan.TestClassParser.Foo; + +@HandlesTypes(Foo.class) +public class FooSCI implements ServletContainerInitializer { + + static Set> classSet; + + @Override + public void onStartup(final Set> set, final ServletContext servletContext) { + classSet = set; + } + +} diff --git a/test/org/apache/tomcat/util/scan/TestAbstractInputStreamJar.java b/test/org/apache/tomcat/util/scan/TestAbstractInputStreamJar.java new file mode 100644 index 0000000..b6f96e2 --- /dev/null +++ b/test/org/apache/tomcat/util/scan/TestAbstractInputStreamJar.java @@ -0,0 +1,59 @@ +/* + * 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.tomcat.util.scan; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.net.URL; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.util.IOTools; +import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; +import org.apache.tomcat.Jar; + +public class TestAbstractInputStreamJar { + + @Before + public void register() { + TomcatURLStreamHandlerFactory.register(); + } + + + @Test + public void testNestedJarGetInputStream() throws Exception { + File f = new File("test/webresources/war-url-connection.war"); + StringBuilder sb = new StringBuilder("war:"); + sb.append(f.toURI().toURL()); + sb.append("*/WEB-INF/lib/test.jar"); + + Jar jar = JarFactory.newInstance(new URL(sb.toString())); + + InputStream is1 = jar.getInputStream("META-INF/resources/index.html"); + ByteArrayOutputStream baos1 = new ByteArrayOutputStream(); + IOTools.flow(is1, baos1); + + InputStream is2 = jar.getInputStream("META-INF/resources/index.html"); + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + IOTools.flow(is2, baos2); + + Assert.assertArrayEquals(baos1.toByteArray(), baos2.toByteArray()); + } +} diff --git a/test/org/apache/tomcat/util/scan/TestClassParser.java b/test/org/apache/tomcat/util/scan/TestClassParser.java new file mode 100644 index 0000000..3756535 --- /dev/null +++ b/test/org/apache/tomcat/util/scan/TestClassParser.java @@ -0,0 +1,58 @@ +/* + * 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.tomcat.util.scan; + +import java.io.File; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestClassParser extends TomcatBaseTest { + + @Target({ElementType.TYPE, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface Foo { + } + + /** and beans */ + @Foo + public class OnClass { + } + + public class OnField { + @Foo + private String name; + } + + @Test + public void testAnnotations() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File appDir = new File("test/webapp-sci"); + tomcat.addWebapp("", appDir.getAbsolutePath()); + tomcat.start(); + Assert.assertTrue(FooSCI.classSet.size() == 2); + Assert.assertTrue(FooSCI.classSet.contains(OnClass.class)); + Assert.assertTrue(FooSCI.classSet.contains(OnField.class)); + } +} diff --git a/test/org/apache/tomcat/util/scan/TestJarScanner.java b/test/org/apache/tomcat/util/scan/TestJarScanner.java new file mode 100644 index 0000000..faf32de --- /dev/null +++ b/test/org/apache/tomcat/util/scan/TestJarScanner.java @@ -0,0 +1,46 @@ +/* + * 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.tomcat.util.scan; + +import java.util.StringTokenizer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestJarScanner extends TomcatBaseTest { + + @Test + public void testJarsToSkipFormat() { + + String jarList = System.getProperty(Constants.SKIP_JARS_PROPERTY); + Assert.assertNotNull("Jar skip list is null", jarList); + Assert.assertFalse("Jar skip list is empty", jarList.isEmpty()); + StringTokenizer tokenizer = new StringTokenizer(jarList, ","); + String token; + while (tokenizer.hasMoreElements()) { + token = tokenizer.nextToken(); + Assert.assertTrue("Token \"" + token + "\" does not end with \".jar\"", + token.endsWith(".jar")); + Assert.assertEquals("Token \"" + token + "\" contains sub string \".jar\"" + + " or separator \",\" is missing", + token.length() - ".jar".length(), + token.indexOf(".jar")); + } + } +} diff --git a/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java b/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java new file mode 100644 index 0000000..da9d941 --- /dev/null +++ b/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java @@ -0,0 +1,81 @@ +/* + * 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.tomcat.util.scan; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import org.apache.tomcat.Jar; +import org.apache.tomcat.JarScanType; +import org.apache.tomcat.JarScannerCallback; +import org.apache.tomcat.unittest.TesterServletContext; + +public class TestStandardJarScanner { + + /** + * Tomcat should ignore URLs which do not have a file part and do not use the file scheme. + */ + @Test + public void skipsInvalidClasspathURLNoFilePartNoFileScheme() { + StandardJarScanner scanner = new StandardJarScanner(); + LoggingCallback callback = new LoggingCallback(); + TesterServletContext context = new TesterServletContext() { + @Override + public ClassLoader getClassLoader() { + URLClassLoader urlClassLoader; + try { + urlClassLoader = new URLClassLoader( + new URL[] { new URL("http://felix.extensions:9/") }); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + return urlClassLoader; + } + }; + scanner.scan(JarScanType.PLUGGABILITY, context, callback); + } + + + private static class LoggingCallback implements JarScannerCallback { + + List callbacks = new ArrayList<>(); + + @Override + public void scan(Jar jar, String webappPath, + boolean isWebapp) throws IOException { + callbacks.add(jar.getJarFileURL().toString() + "::" + webappPath + "::" + isWebapp); + } + + @Override + public void scan(File file, String webappPath, boolean isWebapp) + throws IOException { + callbacks.add(file.toString() + "::" + webappPath + "::" + isWebapp); + } + + @Override + public void scanWebInfClasses() throws IOException { + callbacks.add("N/A::WEB-INF/classes::N/A"); + } + } +} diff --git a/test/org/apache/tomcat/util/security/SecurityManagerBaseTest.java b/test/org/apache/tomcat/util/security/SecurityManagerBaseTest.java new file mode 100644 index 0000000..af7ff8f --- /dev/null +++ b/test/org/apache/tomcat/util/security/SecurityManagerBaseTest.java @@ -0,0 +1,50 @@ +/* + * 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.tomcat.util.security; + +import java.security.Permission; + +import org.apache.catalina.security.SecurityUtil; + +/** + * Base test class for unit tests which require the Java 2 {@link + * SecurityManager} to be enabled. Tests that extend this class must be run in a + * forked SecurityManager test batch since this class modifies global {@link + * System} settings which may interfere with other tests. On static class + * initialization, this class sets up the {@code "package.definition"} and + * {@code "package.access"} system properties and adds a no-op SecurityManager + * which does not check permissions. These settings are required in order to + * make {@link org.apache.catalina.Globals#IS_SECURITY_ENABLED} and {@link + * SecurityUtil#isPackageProtectionEnabled()} return true. + */ +public abstract class SecurityManagerBaseTest { + static { + System.setProperty("package.definition", "test"); + System.setProperty("package.access", "test"); + System.setSecurityManager(new SecurityManager() { + @Override + public void checkPermission(final Permission permission) { + // no-op + } + + @Override + public void checkPermission(final Permission permission, Object context) { + // no-op + } + }); + } +} diff --git a/test/org/apache/tomcat/util/security/TestConcurrentMessageDigest.java b/test/org/apache/tomcat/util/security/TestConcurrentMessageDigest.java new file mode 100644 index 0000000..2ce4c13 --- /dev/null +++ b/test/org/apache/tomcat/util/security/TestConcurrentMessageDigest.java @@ -0,0 +1,40 @@ +/* + * 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.tomcat.util.security; + +import org.junit.Test; + +public class TestConcurrentMessageDigest { + + private static final String SHA256 = "SHA-256"; + private static final String SHA512 = "SHA-512"; + + @Test(expected=IllegalStateException.class) + public void testNoInit() throws Exception { + // Need to use a different algorithm to the other tests otherwise if one + // of the other tests is called first it will initialise the + // ConcurrentDigest for that algorithm. + ConcurrentMessageDigest.digest(SHA512, new byte[8]); + } + + + @Test + public void testUncommon() throws Exception { + ConcurrentMessageDigest.init(SHA256); + ConcurrentMessageDigest.digest(SHA256, new byte[8]); + } +} diff --git a/test/org/apache/tomcat/util/security/TestEscape.java b/test/org/apache/tomcat/util/security/TestEscape.java new file mode 100644 index 0000000..a975377 --- /dev/null +++ b/test/org/apache/tomcat/util/security/TestEscape.java @@ -0,0 +1,56 @@ +/* + * 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.tomcat.util.security; + +import org.junit.Assert; +import org.junit.Test; + +public class TestEscape { + + @Test + public void testHtmlContent() { + Assert.assertEquals("<>&'"/", Escape.htmlElementContent("<>&'\"/")); + } + + + @Test + public void testHtmlContentNullString() { + Assert.assertEquals(null, Escape.htmlElementContent((String) null)); + } + + + @Test + public void testHtmlContentNullObject() { + Assert.assertEquals("?", Escape.htmlElementContent((Object) null)); + } + + + @Test + public void testHtmlContentObject() { + StringBuilder sb = new StringBuilder("test"); + Assert.assertEquals("test", Escape.htmlElementContent(sb)); + } + + + @Test + public void testHtmlContentObjectException() { + StringBuilder sb = new StringBuilder("test"); + Assert.assertEquals("test", Escape.htmlElementContent(sb)); + } + + +} diff --git a/test/org/apache/tomcat/util/threads/TestLimitLatch.java b/test/org/apache/tomcat/util/threads/TestLimitLatch.java new file mode 100644 index 0000000..4538f51 --- /dev/null +++ b/test/org/apache/tomcat/util/threads/TestLimitLatch.java @@ -0,0 +1,236 @@ +/* + * 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.tomcat.util.threads; + +import org.junit.Assert; +import org.junit.Test; + +public class TestLimitLatch { + + // This should be plenty of time, even on slow systems. + private static final long THREAD_WAIT_TIME = 60000; + + @Test + public void testNoThreads() throws Exception { + LimitLatch latch = new LimitLatch(0); + Assert.assertFalse("No threads should be waiting", latch.hasQueuedThreads()); + } + + @Test + public void testOneThreadNoWait() throws Exception { + LimitLatch latch = new LimitLatch(1); + Object lock = new Object(); + checkWaitingThreadCount(latch, 0); + TestThread testThread = new TestThread(latch, lock); + testThread.start(); + if (!waitForThreadToStart(testThread)) { + Assert.fail("Test thread did not start"); + } + checkWaitingThreadCount(latch, 0); + if (!waitForThreadToStop(testThread, lock)) { + Assert.fail("Test thread did not stop"); + } + checkWaitingThreadCount(latch, 0); + } + + @Test + public void testOneThreadWaitCountDown() throws Exception { + LimitLatch latch = new LimitLatch(1); + Object lock = new Object(); + checkWaitingThreadCount(latch, 0); + TestThread testThread = new TestThread(latch, lock); + latch.countUpOrAwait(); + testThread.start(); + if (!waitForThreadToStart(testThread)) { + Assert.fail("Test thread did not start"); + } + checkWaitingThreadCount(latch, 1); + latch.countDown(); + if (!waitForThreadToStop(testThread, lock)) { + Assert.fail("Test thread did not stop"); + } + checkWaitingThreadCount(latch, 0); + } + + @Test + public void testOneRelease() throws Exception { + LimitLatch latch = new LimitLatch(1); + Object lock = new Object(); + checkWaitingThreadCount(latch, 0); + TestThread testThread = new TestThread(latch, lock); + latch.countUpOrAwait(); + testThread.start(); + if (!waitForThreadToStart(testThread)) { + Assert.fail("Test thread did not start"); + } + checkWaitingThreadCount(latch, 1); + latch.releaseAll(); + if (!waitForThreadToStop(testThread, lock)) { + Assert.fail("Test thread did not stop"); + } + checkWaitingThreadCount(latch, 0); + } + + @Test + public void testTenWait() throws Exception { + LimitLatch latch = new LimitLatch(10); + Object lock = new Object(); + checkWaitingThreadCount(latch, 0); + + TestThread[] testThreads = new TestThread[30]; + for (int i = 0; i < 30; i++) { + testThreads[i] = new TestThread(latch, lock); + testThreads[i].start(); + } + + // Should have 10 threads in stage 2 and 20 in stage 1 + + for (int i = 0; i < 30; i++) { + if (!waitForThreadToStart(testThreads[i])) { + Assert.fail("Test thread [" + i + "] did not start"); + } + } + + if (!waitForThreadsToReachStage(testThreads, 20, 10, 0)) { + Assert.fail("Failed at 20-10-00"); + } + checkWaitingThreadCount(latch, 20); + + synchronized (lock) { + lock.notifyAll(); + } + + if (!waitForThreadsToReachStage(testThreads, 10, 10, 10)) { + Assert.fail("Failed at 10-10-10"); + } + checkWaitingThreadCount(latch, 10); + + synchronized (lock) { + lock.notifyAll(); + } + + if (!waitForThreadsToReachStage(testThreads, 0, 10, 20)) { + Assert.fail("Failed at 00-10-20"); + } + checkWaitingThreadCount(latch, 0); + + synchronized (lock) { + lock.notifyAll(); + } + + if (!waitForThreadsToReachStage(testThreads, 0, 0, 30)) { + Assert.fail("Failed at 00-00-30"); + } + } + + private boolean waitForThreadToStart(TestThread t) throws InterruptedException { + long wait = 0; + while (t.getStage() == 0 && wait < THREAD_WAIT_TIME) { + Thread.sleep(100); + wait += 100; + } + return t.getStage() > 0; + } + + private boolean waitForThreadToStop(TestThread t, Object lock) throws InterruptedException { + long wait = 0; + while (t.getStage() < 3 && wait < THREAD_WAIT_TIME) { + Thread.sleep(100); + wait += 100; + synchronized (lock) { + lock.notifyAll(); + } + } + return t.getStage() == 3; + } + + private void checkWaitingThreadCount(LimitLatch latch, int target) throws InterruptedException { + long wait = 0; + while (latch.getQueuedThreads().size() != target && wait < THREAD_WAIT_TIME) { + Thread.sleep(100); + wait += 100; + } + Assert.assertEquals(target, latch.getQueuedThreads().size()); + } + + private boolean waitForThreadsToReachStage(TestThread[] testThreads, + int stage1Target, int stage2Target, int stage3Target) throws InterruptedException { + + long wait = 0; + + int stage1 = 0; + int stage2 = 0; + int stage3 = 0; + + while((stage1 != stage1Target || stage2 != stage2Target || stage3 != stage3Target) && + wait < THREAD_WAIT_TIME) { + stage1 = 0; + stage2 = 0; + stage3 = 0; + for (TestThread testThread : testThreads) { + switch(testThread.getStage()){ + case 1: + stage1++; + break; + case 2: + stage2++; + break; + case 3: + stage3++; + break; + } + } + Thread.sleep(100); + wait += 100; + } + return stage1 == stage1Target && stage2 == stage2Target && stage3 == stage3Target; + } + + private static class TestThread extends Thread { + + private final Object lock; + private final LimitLatch latch; + private volatile int stage = 0; + + TestThread(LimitLatch latch, Object lock) { + this.latch = latch; + this.lock = lock; + } + + public int getStage() { + return stage; + } + + @Override + public void run() { + try { + stage = 1; + latch.countUpOrAwait(); + stage = 2; + if (lock != null) { + synchronized (lock) { + lock.wait(); + } + } + latch.countDown(); + stage = 3; + } catch (InterruptedException x) { + x.printStackTrace(); + } + } + } +} diff --git a/test/org/apache/tomcat/websocket/TestPerMessageDeflate.java b/test/org/apache/tomcat/websocket/TestPerMessageDeflate.java new file mode 100644 index 0000000..51f3b31 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestPerMessageDeflate.java @@ -0,0 +1,205 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jakarta.websocket.Extension; +import jakarta.websocket.Extension.Parameter; + +import org.junit.Assert; +import org.junit.Test; + +public class TestPerMessageDeflate { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=61491 + */ + @Test + public void testSendEmptyMessagePartWithContextTakeover() throws IOException { + + // Set up the extension using defaults + List parameters = Collections.emptyList(); + List> preferences = new ArrayList<>(); + preferences.add(parameters); + + PerMessageDeflate perMessageDeflate = PerMessageDeflate.negotiate(preferences, true); + perMessageDeflate.setNext(new TesterTransformation()); + + ByteBuffer bb1 = ByteBuffer.wrap("A".getBytes(StandardCharsets.UTF_8)); + MessagePart mp1 = new MessagePart(true, 0, Constants.OPCODE_TEXT, bb1, null, null, -1); + + List uncompressedParts1 = new ArrayList<>(); + uncompressedParts1.add(mp1); + perMessageDeflate.sendMessagePart(uncompressedParts1); + + ByteBuffer bb2 = ByteBuffer.wrap("".getBytes(StandardCharsets.UTF_8)); + MessagePart mp2 = new MessagePart(true, 0, Constants.OPCODE_TEXT, bb2, null, null, -1); + + List uncompressedParts2 = new ArrayList<>(); + uncompressedParts2.add(mp2); + perMessageDeflate.sendMessagePart(uncompressedParts2); + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=65317 + */ + @Test + public void testMessagePartThatFillsBufffer() throws IOException { + + // Set up the extension using defaults + List parameters = Collections.emptyList(); + List> preferences = new ArrayList<>(); + preferences.add(parameters); + + // Set up the compression and sending of the message. + PerMessageDeflate perMessageDeflateTx = PerMessageDeflate.negotiate(preferences, true); + perMessageDeflateTx.setNext(new TesterTransformation()); + + byte[] data = new byte[8192]; + + ByteBuffer bb = ByteBuffer.wrap(data); + MessagePart mp = new MessagePart(true, 0, Constants.OPCODE_BINARY, bb, null, null, -1); + + List uncompressedParts = new ArrayList<>(); + uncompressedParts.add(mp); + List compressedParts = perMessageDeflateTx.sendMessagePart(uncompressedParts); + + MessagePart compressedPart = compressedParts.get(0); + + // Set up the decompression and process the received message + PerMessageDeflate perMessageDeflateRx = PerMessageDeflate.negotiate(preferences, true); + perMessageDeflateRx.setNext(new TesterTransformation(compressedPart.getPayload())); + + ByteBuffer received = ByteBuffer.allocate(8192); + + TransformationResult tr = perMessageDeflateRx.getMoreData(compressedPart.getOpCode(), compressedPart.isFin(), + compressedPart.getRsv(), received); + + Assert.assertEquals(8192, received.position()); + Assert.assertEquals(TransformationResult.END_OF_FRAME, tr); + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=66681 + */ + @Test + public void testFlushBatchMessagePart() throws IOException { + // Set up the extension using defaults + List parameters = Collections.emptyList(); + List> preferences = new ArrayList<>(); + preferences.add(parameters); + + // Set up the compression and sending of the message. + PerMessageDeflate perMessageDeflateTx = PerMessageDeflate.negotiate(preferences, true); + perMessageDeflateTx.setNext(new TesterTransformation()); + + List uncompressedParts = new ArrayList<>(); + + // First message part + byte[] data = new byte[1024]; + ByteBuffer bb = ByteBuffer.wrap(data); + MessagePart mp1 = new MessagePart(true, 0, Constants.OPCODE_BINARY, bb, null, null, -1); + uncompressedParts.add(mp1); + + // Flush message (replicates result of calling flushBatch() + MessagePart mp2 = new MessagePart(true, 0, Constants.INTERNAL_OPCODE_FLUSH, null, null, null, -1); + uncompressedParts.add(mp2); + + List compressedParts = perMessageDeflateTx.sendMessagePart(uncompressedParts); + + Assert.assertEquals(2, compressedParts.size()); + + // Check the first compressed part + MessagePart compressedPart1 = compressedParts.get(0); + + // Set up the decompression and process the received message + PerMessageDeflate perMessageDeflateRx = PerMessageDeflate.negotiate(preferences, true); + perMessageDeflateRx.setNext(new TesterTransformation(compressedPart1.getPayload())); + + ByteBuffer received = ByteBuffer.allocate(8192); + + TransformationResult tr = perMessageDeflateRx.getMoreData(compressedPart1.getOpCode(), compressedPart1.isFin(), + compressedPart1.getRsv(), received); + + Assert.assertEquals(1024, received.position()); + Assert.assertEquals(TransformationResult.END_OF_FRAME, tr); + + // Check the second compressed part (should be passed through unchanged) + Assert.assertEquals(mp2, compressedParts.get(1)); + } + + /* + * Minimal implementation to enable other transformations to be tested. It is NOT robust. + */ + private static class TesterTransformation implements Transformation { + + final ByteBuffer data; + + TesterTransformation() { + this(null); + } + + TesterTransformation(ByteBuffer data) { + this.data = data; + } + + @Override + public boolean validateRsvBits(int i) { + return false; + } + + @Override + public boolean validateRsv(int rsv, byte opCode) { + return false; + } + + @Override + public void setNext(Transformation t) { + } + + @Override + public List sendMessagePart(List messageParts) { + return messageParts; + } + + @Override + public TransformationResult getMoreData(byte opCode, boolean fin, int rsv, ByteBuffer dest) throws IOException { + if (data == null) { + return TransformationResult.UNDERFLOW; + } + dest.put(data); + return TransformationResult.END_OF_FRAME; + } + + @Override + public Extension getExtensionResponse() { + return null; + } + + @Override + public void close() { + } + } +} diff --git a/test/org/apache/tomcat/websocket/TestUtil.java b/test/org/apache/tomcat/websocket/TestUtil.java new file mode 100644 index 0000000..e7a8c59 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestUtil.java @@ -0,0 +1,445 @@ +/* + * 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.tomcat.websocket; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.websocket.EncodeException; +import jakarta.websocket.Encoder; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Extension; +import jakarta.websocket.Extension.Parameter; +import jakarta.websocket.MessageHandler; + +import org.junit.Assert; +import org.junit.Test; + +public class TestUtil { + + // Used to init SecureRandom prior to running tests + public static void generateMask() { + Util.generateMask(); + } + + @Test + public void testGetMessageTypeSimple() { + Assert.assertEquals(String.class, Util.getMessageType(new SimpleMessageHandler())); + } + + + @Test + public void testGetMessageTypeSubclass() { + Assert.assertEquals(String.class, Util.getMessageType(new SubSimpleMessageHandler())); + } + + + @Test + public void testGetMessageTypeGenericSubclass() { + Assert.assertEquals(String.class, Util.getMessageType(new GenericSubMessageHandler())); + } + + + @Test + public void testGetMessageTypeGenericMultipleSubclass() { + Assert.assertEquals(String.class, Util.getMessageType(new GenericMultipleSubSubMessageHandler())); + } + + + @Test + public void testGetMessageTypeGenericMultipleSubclassSwap() { + Assert.assertEquals(String.class, Util.getMessageType(new GenericMultipleSubSubSwapMessageHandler())); + } + + + @Test + public void testGetEncoderTypeSimple() { + Assert.assertEquals(String.class, Util.getEncoderType(SimpleEncoder.class)); + } + + + @Test + public void testGetEncoderTypeSubclass() { + Assert.assertEquals(String.class, Util.getEncoderType(SubSimpleEncoder.class)); + } + + + @Test + public void testGetEncoderTypeGenericSubclass() { + Assert.assertEquals(String.class, Util.getEncoderType(GenericSubEncoder.class)); + } + + + @Test + public void testGetEncoderTypeGenericMultipleSubclass() { + Assert.assertEquals(String.class, Util.getEncoderType(GenericMultipleSubSubEncoder.class)); + } + + + @Test + public void testGetEncoderTypeGenericMultipleSubclassSwap() { + Assert.assertEquals(String.class, Util.getEncoderType(GenericMultipleSubSubSwapEncoder.class)); + } + + + @Test + public void testGetEncoderTypeSimpleWithGenericType() { + Assert.assertEquals(List.class, Util.getEncoderType(SimpleEncoderWithGenericType.class)); + } + + + @Test + public void testGenericArrayEncoderString() { + Assert.assertEquals(String[].class, Util.getEncoderType(GenericArrayEncoderString.class)); + } + + + @Test + public void testGenericArraySubEncoderString() { + Assert.assertEquals(String[][].class, Util.getEncoderType(GenericArraySubEncoderString.class)); + } + + + private static class SimpleMessageHandler implements MessageHandler.Whole { + @Override + public void onMessage(String message) { + // NO-OP + } + } + + + private static class SubSimpleMessageHandler extends SimpleMessageHandler { + } + + + private abstract static class GenericMessageHandler implements MessageHandler.Whole { + } + + + private static class GenericSubMessageHandler extends GenericMessageHandler { + @Override + public void onMessage(String message) { + // NO-OP + } + } + + + private interface Foo { + void doSomething(T thing); + } + + + private abstract static class GenericMultipleMessageHandler implements MessageHandler.Whole
    , Foo { + } + + + private abstract static class GenericMultipleSubMessageHandler extends GenericMultipleMessageHandler { + } + + + private static class GenericMultipleSubSubMessageHandler extends GenericMultipleSubMessageHandler { + + @Override + public void onMessage(String message) { + // NO-OP + } + + @Override + public void doSomething(Boolean thing) { + // NO-OP + } + } + + + private abstract static class GenericMultipleSubSwapMessageHandler + extends GenericMultipleMessageHandler { + } + + + private static class GenericMultipleSubSubSwapMessageHandler + extends GenericMultipleSubSwapMessageHandler { + + @Override + public void onMessage(String message) { + // NO-OP + } + + @Override + public void doSomething(Boolean thing) { + // NO-OP + } + } + + + private static class SimpleEncoder implements Encoder.Text { + + @Override + public void init(EndpointConfig endpointConfig) { + // NO-OP + } + + @Override + public void destroy() { + // NO-OP + } + + @Override + public String encode(String object) throws EncodeException { + return null; + } + } + + + private static class SubSimpleEncoder extends SimpleEncoder { + } + + + private abstract static class GenericEncoder implements Encoder.Text { + + @Override + public void init(EndpointConfig endpointConfig) { + // NO-OP + } + + @Override + public void destroy() { + // NO-OP + } + } + + + private static class GenericSubEncoder extends GenericEncoder { + + @Override + public String encode(String object) throws EncodeException { + return null; + } + + } + + + private abstract static class GenericMultipleEncoder implements Encoder.Text, Foo { + + @Override + public void init(EndpointConfig endpointConfig) { + // NO-OP + } + + @Override + public void destroy() { + // NO-OP + } + } + + + private abstract static class GenericMultipleSubEncoder extends GenericMultipleEncoder { + } + + + private static class GenericMultipleSubSubEncoder extends GenericMultipleSubEncoder { + + @Override + public String encode(String object) throws EncodeException { + return null; + } + + @Override + public void doSomething(Boolean thing) { + // NO-OP + } + + } + + + private abstract static class GenericMultipleSubSwapEncoder extends GenericMultipleEncoder { + } + + + private static class GenericMultipleSubSubSwapEncoder extends GenericMultipleSubSwapEncoder { + + @Override + public String encode(String object) throws EncodeException { + return null; + } + + @Override + public void doSomething(Boolean thing) { + // NO-OP + } + } + + + private static class SimpleEncoderWithGenericType implements Encoder.Text> { + + @Override + public void init(EndpointConfig endpointConfig) { + // NO-OP + } + + @Override + public void destroy() { + // NO-OP + } + + @Override + public String encode(List object) throws EncodeException { + return null; + } + } + + + private abstract static class GenericArrayEncoder implements Encoder.Text { + } + + + private static class GenericArrayEncoderString extends GenericArrayEncoder { + + @Override + public void init(EndpointConfig endpointConfig) { + // NO-OP + } + + @Override + public void destroy() { + // NO-OP + } + + @Override + public String encode(String[] object) throws EncodeException { + return null; + } + } + + + private abstract static class GenericArraySubEncoder extends GenericArrayEncoder { + } + + + private static class GenericArraySubEncoderString extends GenericArraySubEncoder { + + @Override + public void init(EndpointConfig endpointConfig) { + // NO-OP + } + + @Override + public void destroy() { + // NO-OP + } + + @Override + public String encode(String[][] object) throws EncodeException { + return null; + } + } + + + @Test + public void testParseExtensionHeaderSimple01() { + doTestParseExtensionHeaderSimple("ext;a=1;b=2"); + } + + @Test + public void testParseExtensionHeaderSimple02() { + doTestParseExtensionHeaderSimple("ext;a=\"1\";b=2"); + } + + @Test + public void testParseExtensionHeaderSimple03() { + doTestParseExtensionHeaderSimple("ext;a=1;b=\"2\""); + } + + @Test + public void testParseExtensionHeaderSimple04() { + doTestParseExtensionHeaderSimple(" ext ; a = 1 ; b = 2 "); + } + + private void doTestParseExtensionHeaderSimple(String header) { + // Simple test + List result = new ArrayList<>(); + Util.parseExtensionHeader(result, header); + + Assert.assertEquals(1, result.size()); + + Extension ext = result.get(0); + Assert.assertEquals("ext", ext.getName()); + List params = ext.getParameters(); + Assert.assertEquals(2, params.size()); + Parameter paramA = params.get(0); + Assert.assertEquals("a", paramA.getName()); + Assert.assertEquals("1", paramA.getValue()); + Parameter paramB = params.get(1); + Assert.assertEquals("b", paramB.getName()); + Assert.assertEquals("2", paramB.getValue()); + } + + + @Test + public void testParseExtensionHeaderMultiple01() { + doTestParseExtensionHeaderMultiple("ext;a=1;b=2,ext2;c;d=xyz,ext3"); + } + + @Test + public void testParseExtensionHeaderMultiple02() { + doTestParseExtensionHeaderMultiple(" ext ; a = 1 ; b = 2 , ext2 ; c ; d = xyz , ext3 "); + } + + private void doTestParseExtensionHeaderMultiple(String header) { + // Simple test + List result = new ArrayList<>(); + Util.parseExtensionHeader(result, header); + + Assert.assertEquals(3, result.size()); + + Extension ext = result.get(0); + Assert.assertEquals("ext", ext.getName()); + List params = ext.getParameters(); + Assert.assertEquals(2, params.size()); + Parameter paramA = params.get(0); + Assert.assertEquals("a", paramA.getName()); + Assert.assertEquals("1", paramA.getValue()); + Parameter paramB = params.get(1); + Assert.assertEquals("b", paramB.getName()); + Assert.assertEquals("2", paramB.getValue()); + + Extension ext2 = result.get(1); + Assert.assertEquals("ext2", ext2.getName()); + List params2 = ext2.getParameters(); + Assert.assertEquals(2, params2.size()); + Parameter paramC = params2.get(0); + Assert.assertEquals("c", paramC.getName()); + Assert.assertNull(paramC.getValue()); + Parameter paramD = params2.get(1); + Assert.assertEquals("d", paramD.getName()); + Assert.assertEquals("xyz", paramD.getValue()); + + Extension ext3 = result.get(2); + Assert.assertEquals("ext3", ext3.getName()); + List params3 = ext3.getParameters(); + Assert.assertEquals(0, params3.size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseExtensionHeaderInvalid01() { + Util.parseExtensionHeader(new ArrayList<>(), "ext;a=\"1;b=2"); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseExtensionHeaderInvalid02() { + Util.parseExtensionHeader(new ArrayList<>(), "ext;a=1\";b=2"); + } +} diff --git a/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java b/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java new file mode 100644 index 0000000..1d7caea --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWebSocketFrameClient.java @@ -0,0 +1,220 @@ +/* + * 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.tomcat.websocket; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ClientEndpointConfig.Configurator; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.authenticator.AuthenticatorBase; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + +public class TestWebSocketFrameClient extends WebSocketBaseTest { + + private static final String USER = "Aladdin"; + private static final String PWD = "open sesame"; + private static final String ROLE = "role"; + private static final String URI_PROTECTED = "/foo"; + + @Test + public void testConnectToServerEndpoint() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterFirehoseServer.ConfigInline.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + // BZ 62596 + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create() + .configurator(new Configurator() { + @Override + public void beforeRequest(Map> headers) { + headers.put("Dummy", + Collections.singletonList(String.join("", Collections.nCopies(4000, "A")))); + super.beforeRequest(headers); + } + }).build(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("ws://localhost:" + getPort() + TesterFirehoseServer.PATH)); + CountDownLatch latch = new CountDownLatch(TesterFirehoseServer.MESSAGE_COUNT); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText("Hello"); + + System.out.println("Sent Hello message, waiting for data"); + + // Ignore the latch result as the message count test below will tell us + // if the right number of messages arrived + handler.getLatch().await(TesterFirehoseServer.WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS); + + Queue messages = handler.getMessages(); + Assert.assertEquals(TesterFirehoseServer.MESSAGE_COUNT, messages.size()); + for (String message : messages) { + Assert.assertEquals(TesterFirehoseServer.MESSAGE, message); + } + } + + @Test + public void testConnectToRootEndpoint() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + Context ctx2 = tomcat.addContext("/foo", null); + ctx2.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx2, "default", new DefaultServlet()); + ctx2.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + echoTester("", null); + echoTester("/", null); + // This will trigger a redirect so there will be 5 requests logged + echoTester("/foo", null); + echoTester("/foo/", null); + } + + public void echoTester(String path, ClientEndpointConfig clientEndpointConfig) throws Exception { + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + if (clientEndpointConfig == null) { + clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + } + // Increase default timeout from 5s to 10s to try and reduce errors on + // CI systems. + clientEndpointConfig.getUserProperties().put(Constants.IO_TIMEOUT_MS_PROPERTY, "10000"); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("ws://localhost:" + getPort() + path)); + CountDownLatch latch = new CountDownLatch(1); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText("Hello"); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + Assert.assertTrue(latchResult); + + Queue messages = handler.getMessages(); + Assert.assertEquals(1, messages.size()); + for (String message : messages) { + Assert.assertEquals("Hello", message); + } + wsSession.close(); + } + + @Test + public void testConnectToBasicEndpoint() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + Context ctx = tomcat.addContext(URI_PROTECTED, null); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded("/"); + String utf8User = "test"; + String utf8Pass = "123\u00A3"; // pound sign + + tomcat.addUser(utf8User, utf8Pass); + tomcat.addRole(utf8User, ROLE); + + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + ctx.addConstraint(sc); + + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("BASIC"); + ctx.setLoginConfig(lc); + + AuthenticatorBase basicAuthenticator = new org.apache.catalina.authenticator.BasicAuthenticator(); + ctx.getPipeline().addValve(basicAuthenticator); + + tomcat.start(); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME, utf8User); + clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD, utf8Pass); + + echoTester(URI_PROTECTED, clientEndpointConfig); + } + + @Test + public void testConnectToDigestEndpoint() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + Context ctx = tomcat.addContext(URI_PROTECTED, null); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded("/*"); + + tomcat.addUser(USER, PWD); + tomcat.addRole(USER, ROLE); + + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(ROLE); + sc.addCollection(collection); + ctx.addConstraint(sc); + + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("DIGEST"); + ctx.setLoginConfig(lc); + + AuthenticatorBase digestAuthenticator = new org.apache.catalina.authenticator.DigestAuthenticator(); + ctx.getPipeline().addValve(digestAuthenticator); + + tomcat.start(); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME, USER); + clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD, PWD); + + echoTester(URI_PROTECTED, clientEndpointConfig); + } +} diff --git a/test/org/apache/tomcat/websocket/TestWebSocketFrameClientSSL.java b/test/org/apache/tomcat/websocket/TestWebSocketFrameClientSSL.java new file mode 100644 index 0000000..0853d13 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWebSocketFrameClientSSL.java @@ -0,0 +1,300 @@ +/* + * 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.tomcat.websocket; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URI; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.net.TesterSupport; +import org.apache.tomcat.util.security.KeyStoreUtil; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText; +import org.apache.tomcat.websocket.TesterMessageCountClient.SleepingText; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + +@RunWith(Parameterized.class) +public class TestWebSocketFrameClientSSL extends WebSocketBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation" }); + parameterSets.add( + new Object[] { "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }); + parameterSets.add(new Object[] { "OpenSSL-FFM", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + + @SuppressWarnings("removal") + @Test + public void testConnectToServerEndpointLegacy() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = tomcat.addContext("", null); + ctx.addApplicationListener(TesterFirehoseServer.ConfigInline.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + clientEndpointConfig.getUserProperties().put(Constants.SSL_TRUSTSTORE_PROPERTY, TesterSupport.CA_JKS); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("wss://localhost:" + getPort() + TesterFirehoseServer.PATH)); + CountDownLatch latch = new CountDownLatch(TesterFirehoseServer.MESSAGE_COUNT); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText("Hello"); + + System.out.println("Sent Hello message, waiting for data"); + + // Ignore the latch result as the message count test below will tell us + // if the right number of messages arrived + handler.getLatch().await(TesterFirehoseServer.WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS); + + Queue messages = handler.getMessages(); + Assert.assertEquals(TesterFirehoseServer.MESSAGE_COUNT, messages.size()); + for (String message : messages) { + Assert.assertEquals(TesterFirehoseServer.MESSAGE, message); + } + } + + + @Test + public void testConnectToServerEndpoint() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterFirehoseServer.ConfigInline.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + // Build the SSLContext + SSLContext sslContext = SSLContext.getInstance("TLS"); + File trustStoreFile = new File(TesterSupport.CA_JKS); + KeyStore ks = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(trustStoreFile)) { + KeyStoreUtil.load(ks, is, TesterSupport.JKS_PASS.toCharArray()); + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + sslContext.init(null, tmf.getTrustManagers(), null); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().sslContext(sslContext) + .build(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("wss://localhost:" + getPort() + TesterFirehoseServer.PATH)); + CountDownLatch latch = new CountDownLatch(TesterFirehoseServer.MESSAGE_COUNT); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText("Hello"); + + System.out.println("Sent Hello message, waiting for data"); + + // Ignore the latch result as the message count test below will tell us + // if the right number of messages arrived + handler.getLatch().await(TesterFirehoseServer.WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS); + + Queue messages = handler.getMessages(); + Assert.assertEquals(TesterFirehoseServer.MESSAGE_COUNT, messages.size()); + for (String message : messages) { + Assert.assertEquals(TesterFirehoseServer.MESSAGE, message); + } + } + + + @SuppressWarnings("removal") + @Test + public void testBug56032Legacy() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = tomcat.addContext("", null); + ctx.addApplicationListener(TesterFirehoseServer.ConfigInline.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + clientEndpointConfig.getUserProperties().put(Constants.SSL_TRUSTSTORE_PROPERTY, TesterSupport.CA_JKS); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("wss://localhost:" + getPort() + TesterFirehoseServer.PATH)); + + // Process incoming messages very slowly + MessageHandler handler = new SleepingText(5000); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText("Hello"); + + // Wait long enough for the buffers to fill and the send to timeout + int count = 0; + int limit = TesterFirehoseServer.WAIT_TIME_MILLIS / 100; + + System.out.println("Waiting for server to report an error"); + while (TesterFirehoseServer.Endpoint.getErrorCount() == 0 && count < limit) { + Thread.sleep(100); + count++; + } + + if (TesterFirehoseServer.Endpoint.getErrorCount() == 0) { + Assert.fail("No error reported by Endpoint when timeout was expected"); + } + + // Wait again for the connection to be closed - + // should be a lot faster. + System.out.println("Waiting for connection to be closed"); + count = 0; + limit = TesterFirehoseServer.WAIT_TIME_MILLIS / 100; + while (TesterFirehoseServer.Endpoint.getOpenConnectionCount() != 0 && count < limit) { + Thread.sleep(100); + count++; + } + + int openConnectionCount = TesterFirehoseServer.Endpoint.getOpenConnectionCount(); + if (openConnectionCount != 0) { + Assert.fail("There are [" + openConnectionCount + "] connections still open"); + } + + // Close the client session. + wsSession.close(); + } + + + @Test + public void testBug56032() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterFirehoseServer.ConfigInline.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + // Build the SSLContext + SSLContext sslContext = SSLContext.getInstance("TLS"); + File trustStoreFile = new File(TesterSupport.CA_JKS); + KeyStore ks = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(trustStoreFile)) { + KeyStoreUtil.load(ks, is, TesterSupport.JKS_PASS.toCharArray()); + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + sslContext.init(null, tmf.getTrustManagers(), null); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().sslContext(sslContext) + .build(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("wss://localhost:" + getPort() + TesterFirehoseServer.PATH)); + + // Process incoming messages very slowly + MessageHandler handler = new SleepingText(5000); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText("Hello"); + + // Wait long enough for the buffers to fill and the send to timeout + int count = 0; + int limit = TesterFirehoseServer.WAIT_TIME_MILLIS / 100; + + System.out.println("Waiting for server to report an error"); + while (TesterFirehoseServer.Endpoint.getErrorCount() == 0 && count < limit) { + Thread.sleep(100); + count++; + } + + if (TesterFirehoseServer.Endpoint.getErrorCount() == 0) { + Assert.fail("No error reported by Endpoint when timeout was expected"); + } + + // Wait again for the connection to be closed - + // should be a lot faster. + System.out.println("Waiting for connection to be closed"); + count = 0; + limit = TesterFirehoseServer.WAIT_TIME_MILLIS / 100; + while (TesterFirehoseServer.Endpoint.getOpenConnectionCount() != 0 && count < limit) { + Thread.sleep(100); + count++; + } + + int openConnectionCount = TesterFirehoseServer.Endpoint.getOpenConnectionCount(); + if (openConnectionCount != 0) { + Assert.fail("There are [" + openConnectionCount + "] connections still open"); + } + + // Close the client session. + wsSession.close(); + } + + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.initSsl(tomcat); + + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsFrame.java b/test/org/apache/tomcat/websocket/TestWsFrame.java new file mode 100644 index 0000000..c14386d --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsFrame.java @@ -0,0 +1,61 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +public class TestWsFrame { + + @Test + public void testByteArrayToLong() throws IOException { + Assert.assertEquals(0L, WsFrameBase.byteArrayToLong(new byte[] { 0 }, 0, 1)); + Assert.assertEquals(1L, WsFrameBase.byteArrayToLong(new byte[] { 1 }, 0, 1)); + Assert.assertEquals(0xFF, WsFrameBase.byteArrayToLong(new byte[] { -1 }, 0, 1)); + Assert.assertEquals(0xFFFF, WsFrameBase.byteArrayToLong(new byte[] { -1, -1 }, 0, 2)); + Assert.assertEquals(0xFFFFFF, WsFrameBase.byteArrayToLong(new byte[] { -1, -1, -1 }, 0, 3)); + Assert.assertEquals(0xFFFFFFFFL, WsFrameBase.byteArrayToLong(new byte[] { -1, -1, -1, -1 }, 0, 4)); + Assert.assertEquals(0xFFFFFFFFFFL, WsFrameBase.byteArrayToLong(new byte[] { -1, -1, -1, -1, -1 }, 0, 5)); + Assert.assertEquals(0xFFFFFFFFFFFFL, WsFrameBase.byteArrayToLong(new byte[] { -1, -1, -1, -1, -1, -1 }, 0, 6)); + Assert.assertEquals(0xFFFFFFFFFFFFFFL, + WsFrameBase.byteArrayToLong(new byte[] { -1, -1, -1, -1, -1, -1, -1 }, 0, 7)); + Assert.assertEquals(0x7FFFFFFFFFFFFFFFL, + WsFrameBase.byteArrayToLong(new byte[] { 127, -1, -1, -1, -1, -1, -1, -1 }, 0, 8)); + Assert.assertEquals(-1, WsFrameBase.byteArrayToLong(new byte[] { -1, -1, -1, -1, -1, -1, -1, -1 }, 0, 8)); + } + + + @Test + public void testByteArrayToLongOffset() throws IOException { + Assert.assertEquals(0L, WsFrameBase.byteArrayToLong(new byte[] { 20, 0 }, 1, 1)); + Assert.assertEquals(1L, WsFrameBase.byteArrayToLong(new byte[] { 20, 1 }, 1, 1)); + Assert.assertEquals(0xFF, WsFrameBase.byteArrayToLong(new byte[] { 20, -1 }, 1, 1)); + Assert.assertEquals(0xFFFF, WsFrameBase.byteArrayToLong(new byte[] { 20, -1, -1 }, 1, 2)); + Assert.assertEquals(0xFFFFFF, WsFrameBase.byteArrayToLong(new byte[] { 20, -1, -1, -1 }, 1, 3)); + Assert.assertEquals(0xFFFFFFFFL, WsFrameBase.byteArrayToLong(new byte[] { 20, -1, -1, -1, -1 }, 1, 4)); + Assert.assertEquals(0xFFFFFFFFFFL, WsFrameBase.byteArrayToLong(new byte[] { 20, -1, -1, -1, -1, -1 }, 1, 5)); + Assert.assertEquals(0xFFFFFFFFFFFFL, + WsFrameBase.byteArrayToLong(new byte[] { 20, -1, -1, -1, -1, -1, -1 }, 1, 6)); + Assert.assertEquals(0xFFFFFFFFFFFFFFL, + WsFrameBase.byteArrayToLong(new byte[] { 20, -1, -1, -1, -1, -1, -1, -1 }, 1, 7)); + Assert.assertEquals(0x7FFFFFFFFFFFFFFFL, + WsFrameBase.byteArrayToLong(new byte[] { 20, 127, -1, -1, -1, -1, -1, -1, -1 }, 1, 8)); + Assert.assertEquals(-1, WsFrameBase.byteArrayToLong(new byte[] { 20, -1, -1, -1, -1, -1, -1, -1, -1 }, 1, 8)); + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsPingPongMessages.java b/test/org/apache/tomcat/websocket/TestWsPingPongMessages.java new file mode 100644 index 0000000..ef90796 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsPingPongMessages.java @@ -0,0 +1,89 @@ +/* + * 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.tomcat.websocket; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterEndpoint; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + + +public class TestWsPingPongMessages extends WebSocketBaseTest { + + ByteBuffer applicationData = ByteBuffer.wrap("mydata".getBytes(StandardCharsets.UTF_8)); + + @Test + public void testPingPongMessages() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + ClientEndpointConfig.Builder.create().build(), + new URI("ws://localhost:" + getPort() + TesterEchoServer.Config.PATH_ASYNC)); + + CountDownLatch latch = new CountDownLatch(1); + TesterEndpoint tep = (TesterEndpoint) wsSession.getUserProperties().get("endpoint"); + tep.setLatch(latch); + + PongMessageHandler handler = new PongMessageHandler(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendPing(applicationData); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + Assert.assertTrue(latchResult); + Assert.assertArrayEquals(applicationData.array(), (handler.getMessages().peek()).getApplicationData().array()); + } + + public static class PongMessageHandler extends TesterMessageCountClient.BasicHandler { + public PongMessageHandler(CountDownLatch latch) { + super(latch); + } + + @Override + public void onMessage(PongMessage message) { + getMessages().add(message); + if (getLatch() != null) { + getLatch().countDown(); + } + } + } +} \ No newline at end of file diff --git a/test/org/apache/tomcat/websocket/TestWsRemoteEndpoint.java b/test/org/apache/tomcat/websocket/TestWsRemoteEndpoint.java new file mode 100644 index 0000000..67001bd --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsRemoteEndpoint.java @@ -0,0 +1,241 @@ +/* + * 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.tomcat.websocket; + +import java.io.OutputStream; +import java.io.Writer; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.websocket.ClientEndpointConfig.Builder; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Endpoint; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TesterMessageCountClient.AsyncBinary; +import org.apache.tomcat.websocket.TesterMessageCountClient.AsyncHandler; +import org.apache.tomcat.websocket.TesterMessageCountClient.AsyncText; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterAnnotatedEndpoint; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterEndpoint; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + +public class TestWsRemoteEndpoint extends WebSocketBaseTest { + + private static final String SEQUENCE = "ABCDE"; + private static final int S_LEN = SEQUENCE.length(); + private static final String TEST_MESSAGE_5K; + + static { + StringBuilder sb = new StringBuilder(S_LEN * 1024); + for (int i = 0; i < 1024; i++) { + sb.append(SEQUENCE); + } + TEST_MESSAGE_5K = sb.toString(); + } + + @Test + public void testWriterAnnotation() throws Exception { + doTestWriter(TesterAnnotatedEndpoint.class, true, TEST_MESSAGE_5K); + } + + @Test + public void testWriterProgrammatic() throws Exception { + doTestWriter(TesterProgrammaticEndpoint.class, true, TEST_MESSAGE_5K); + } + + @Test + public void testWriterZeroLengthAnnotation() throws Exception { + doTestWriter(TesterAnnotatedEndpoint.class, true, ""); + } + + @Test + public void testWriterZeroLengthProgrammatic() throws Exception { + doTestWriter(TesterProgrammaticEndpoint.class, true, ""); + } + + @Test + public void testStreamAnnotation() throws Exception { + doTestWriter(TesterAnnotatedEndpoint.class, false, TEST_MESSAGE_5K); + } + + @Test + public void testStreamProgrammatic() throws Exception { + doTestWriter(TesterProgrammaticEndpoint.class, false, TEST_MESSAGE_5K); + } + + private void doTestWriter(Class clazz, boolean useWriter, String testMessage) throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Session wsSession; + URI uri = new URI("ws://localhost:" + getPort() + TesterEchoServer.Config.PATH_ASYNC); + if (Endpoint.class.isAssignableFrom(clazz)) { + @SuppressWarnings("unchecked") + Class endpointClazz = (Class) clazz; + wsSession = wsContainer.connectToServer(endpointClazz, Builder.create().build(), uri); + } else { + wsSession = wsContainer.connectToServer(clazz, uri); + } + + CountDownLatch latch = new CountDownLatch(1); + TesterEndpoint tep = (TesterEndpoint) wsSession.getUserProperties().get("endpoint"); + tep.setLatch(latch); + AsyncHandler handler; + if (useWriter) { + handler = new AsyncText(latch); + } else { + handler = new AsyncBinary(latch); + } + + wsSession.addMessageHandler(handler); + + if (useWriter) { + Writer w = wsSession.getBasicRemote().getSendWriter(); + + for (int i = 0; i < 8; i++) { + w.write(testMessage); + } + + w.close(); + } else { + OutputStream s = wsSession.getBasicRemote().getSendStream(); + + for (int i = 0; i < 8; i++) { + s.write(testMessage.getBytes(StandardCharsets.UTF_8)); + } + + s.close(); + } + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + List results = new ArrayList<>(); + if (useWriter) { + @SuppressWarnings("unchecked") + List messages = (List) handler.getMessages(); + results.addAll(messages); + } else { + // Take advantage of the fact that the message uses characters that + // are represented as a single UTF-8 byte so won't be split across + // binary messages + @SuppressWarnings("unchecked") + List messages = (List) handler.getMessages(); + for (ByteBuffer message : messages) { + byte[] bytes = new byte[message.limit()]; + message.get(bytes); + results.add(new String(bytes, StandardCharsets.UTF_8)); + } + } + + int offset = 0; + int i = 0; + for (String result : results) { + if (testMessage.length() == 0) { + Assert.assertEquals(0, result.length()); + } else { + // First may be a fragment + Assert.assertEquals(SEQUENCE.substring(offset, S_LEN), result.substring(0, S_LEN - offset)); + i = S_LEN - offset; + while (i + S_LEN < result.length()) { + if (!SEQUENCE.equals(result.substring(i, i + S_LEN))) { + Assert.fail(); + } + i += S_LEN; + } + offset = result.length() - i; + if (!SEQUENCE.substring(0, offset).equals(result.substring(i))) { + Assert.fail(); + } + } + } + } + + @Test + public void testWriterErrorAnnotation() throws Exception { + doTestWriterError(TesterAnnotatedEndpoint.class); + } + + @Test + public void testWriterErrorProgrammatic() throws Exception { + doTestWriterError(TesterProgrammaticEndpoint.class); + } + + private void doTestWriterError(Class clazz) throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Session wsSession; + URI uri = new URI("ws://localhost:" + getPort() + TesterEchoServer.Config.PATH_WRITER_ERROR); + if (Endpoint.class.isAssignableFrom(clazz)) { + @SuppressWarnings("unchecked") + Class endpointClazz = (Class) clazz; + wsSession = wsContainer.connectToServer(endpointClazz, Builder.create().build(), uri); + } else { + wsSession = wsContainer.connectToServer(clazz, uri); + } + + CountDownLatch latch = new CountDownLatch(1); + TesterEndpoint tep = (TesterEndpoint) wsSession.getUserProperties().get("endpoint"); + tep.setLatch(latch); + AsyncHandler handler; + handler = new AsyncText(latch); + + wsSession.addMessageHandler(handler); + + // This should trigger the error + wsSession.getBasicRemote().sendText("Start"); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + @SuppressWarnings("unchecked") + List messages = (List) handler.getMessages(); + + Assert.assertEquals(0, messages.size()); + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsSession.java b/test/org/apache/tomcat/websocket/TestWsSession.java new file mode 100644 index 0000000..4b08fd7 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsSession.java @@ -0,0 +1,107 @@ +/* + * 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.tomcat.websocket; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +public class TestWsSession { + + @Test + public void testAppendCloseReasonWithTruncation01() { + doTestAppendCloseReasonWithTruncation(100); + } + + + @Test + public void testAppendCloseReasonWithTruncation02() { + doTestAppendCloseReasonWithTruncation(119); + } + + + @Test + public void testAppendCloseReasonWithTruncation03() { + doTestAppendCloseReasonWithTruncation(120); + } + + + @Test + public void testAppendCloseReasonWithTruncation04() { + doTestAppendCloseReasonWithTruncation(121); + } + + + @Test + public void testAppendCloseReasonWithTruncation05() { + doTestAppendCloseReasonWithTruncation(122); + } + + + @Test + public void testAppendCloseReasonWithTruncation06() { + doTestAppendCloseReasonWithTruncation(123); + } + + + @Test + public void testAppendCloseReasonWithTruncation07() { + doTestAppendCloseReasonWithTruncation(124); + } + + + @Test + public void testAppendCloseReasonWithTruncation08() { + doTestAppendCloseReasonWithTruncation(125); + } + + + @Test + public void testAppendCloseReasonWithTruncation09() { + doTestAppendCloseReasonWithTruncation(150); + } + + + private void doTestAppendCloseReasonWithTruncation(int reasonLength) { + StringBuilder reason = new StringBuilder(reasonLength); + for (int i = 0; i < reasonLength; i++) { + reason.append('a'); + } + + ByteBuffer buf = ByteBuffer.allocate(256); + + WsSession.appendCloseReasonWithTruncation(buf, reason.toString()); + + // Check the position and contents + if (reasonLength <= 123) { + Assert.assertEquals(reasonLength, buf.position()); + for (int i = 0; i < reasonLength; i++) { + Assert.assertEquals('a', buf.get(i)); + } + } else { + // Must have been truncated + Assert.assertEquals(123, buf.position()); + for (int i = 0; i < 120; i++) { + Assert.assertEquals('a', buf.get(i)); + } + Assert.assertEquals(0xE2, buf.get(120) & 0xFF); + Assert.assertEquals(0x80, buf.get(121) & 0xFF); + Assert.assertEquals(0xA6, buf.get(122) & 0xFF); + } + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsSessionSuspendResume.java b/test/org/apache/tomcat/websocket/TestWsSessionSuspendResume.java new file mode 100644 index 0000000..6a8aad0 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsSessionSuspendResume.java @@ -0,0 +1,243 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.CloseReason; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; +import org.apache.tomcat.websocket.server.Constants; +import org.apache.tomcat.websocket.server.TesterEndpointConfig; +import org.apache.tomcat.websocket.server.WsServerContainer; + +public class TestWsSessionSuspendResume extends WebSocketBaseTest { + + @Test + public void testSuspendResume() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(SuspendResumeConfig.class.getName()); + + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("ws://localhost:" + getPort() + SuspendResumeConfig.PATH)); + + CountDownLatch latch = new CountDownLatch(2); + wsSession.addMessageHandler(String.class, message -> { + Assert.assertTrue("[echo, echo, echo]".equals(message)); + latch.countDown(); + }); + for (int i = 0; i < 8; i++) { + wsSession.getBasicRemote().sendText("echo"); + } + + boolean latchResult = latch.await(30, TimeUnit.SECONDS); + Assert.assertTrue(latchResult); + + wsSession.close(); + } + + + public static final class SuspendResumeConfig extends TesterEndpointConfig { + private static final String PATH = "/echo"; + + @Override + protected Class getEndpointClass() { + return SuspendResumeEndpoint.class; + } + + @Override + protected ServerEndpointConfig getServerEndpointConfig() { + return ServerEndpointConfig.Builder.create(getEndpointClass(), PATH).build(); + } + } + + + public static final class SuspendResumeEndpoint extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig epc) { + SuspendResumeMessageProcessor processor = new SuspendResumeMessageProcessor(session, 3); + session.addMessageHandler(String.class, message -> processor.addMessage(message)); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + try { + session.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onError(Session session, Throwable t) { + t.printStackTrace(); + } + } + + + private static final class SuspendResumeMessageProcessor { + private final Session session; + private final int count; + private final List messages = new ArrayList<>(); + + SuspendResumeMessageProcessor(Session session, int count) { + this.session = session; + this.count = count; + } + + void addMessage(String message) { + if (messages.size() == count) { + ((WsSession) session).suspend(); + try { + session.getBasicRemote().sendText(messages.toString()); + messages.clear(); + ((WsSession) session).resume(); + } catch (IOException e) { + Assert.fail(); + } + } else { + messages.add(message); + } + } + } + + + @Test + public void testSuspendThenClose() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(SuspendCloseConfig.class.getName()); + ctx.addApplicationListener(WebSocketFastServerTimeout.class.getName()); + + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("ws://localhost:" + getPort() + SuspendResumeConfig.PATH)); + + wsSession.getBasicRemote().sendText("start test"); + + // Wait for the client response to be received by the server + int count = 0; + while (count < 50 && !SuspendCloseEndpoint.isServerSessionFullyClosed()) { + Thread.sleep(100); + count ++; + } + Assert.assertTrue(SuspendCloseEndpoint.isServerSessionFullyClosed()); + } + + + public static final class SuspendCloseConfig extends TesterEndpointConfig { + private static final String PATH = "/echo"; + + @Override + protected Class getEndpointClass() { + return SuspendCloseEndpoint.class; + } + + @Override + protected ServerEndpointConfig getServerEndpointConfig() { + return ServerEndpointConfig.Builder.create(getEndpointClass(), PATH).build(); + } + } + + + public static final class SuspendCloseEndpoint extends Endpoint { + + // Yes, a static variable is a hack. + private static volatile WsSession serverSession; + + @Override + public void onOpen(Session session, EndpointConfig epc) { + serverSession = (WsSession) session; + // Set a short session close timeout (milliseconds) + serverSession.getUserProperties().put( + org.apache.tomcat.websocket.Constants.SESSION_CLOSE_TIMEOUT_PROPERTY, Long.valueOf(2000)); + // Any message will trigger the suspend then close + serverSession.addMessageHandler(String.class, message -> { + try { + serverSession.getBasicRemote().sendText("server session open"); + serverSession.getBasicRemote().sendText("suspending server session"); + serverSession.suspend(); + serverSession.getBasicRemote().sendText("closing server session"); + serverSession.close(); + } catch (IOException ioe) { + ioe.printStackTrace(); + // Attempt to make the failure more obvious + throw new RuntimeException(ioe); + } + }); + } + + @Override + public void onError(Session session, Throwable t) { + t.printStackTrace(); + } + + public static boolean isServerSessionFullyClosed() { + return serverSession != null && serverSession.isClosed(); + } + } + + + public static class WebSocketFastServerTimeout implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + WsServerContainer container = (WsServerContainer) sce.getServletContext().getAttribute( + Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + container.setProcessPeriod(0); + } + } +} \ No newline at end of file diff --git a/test/org/apache/tomcat/websocket/TestWsSubprotocols.java b/test/org/apache/tomcat/websocket/TestWsSubprotocols.java new file mode 100644 index 0000000..2c413a5 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsSubprotocols.java @@ -0,0 +1,114 @@ +/* + * 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.tomcat.websocket; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpoint; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Test; + + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; +import org.apache.tomcat.websocket.server.TesterEndpointConfig; + +public class TestWsSubprotocols extends WebSocketBaseTest { + + @Test + public void testWsSubprotocols() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(Config.class.getName()); + + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + ClientEndpointConfig.Builder.create().preferredSubprotocols(Arrays.asList("sp3")).build(), + new URI("ws://localhost:" + getPort() + SubProtocolsEndpoint.PATH_BASIC)); + + Assert.assertTrue(wsSession.isOpen()); + if (wsSession.getNegotiatedSubprotocol() != null) { + Assert.assertTrue(wsSession.getNegotiatedSubprotocol().isEmpty()); + } + wsSession.close(); + SubProtocolsEndpoint.recycle(); + + wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + ClientEndpointConfig.Builder.create().preferredSubprotocols(Arrays.asList("sp2")).build(), + new URI("ws://localhost:" + getPort() + SubProtocolsEndpoint.PATH_BASIC)); + + Assert.assertTrue(wsSession.isOpen()); + Assert.assertEquals("sp2", wsSession.getNegotiatedSubprotocol()); + // Client thread might move faster than server. Wait for up to 5s for the + // subProtocols to be set + int count = 0; + while (count < 50 && SubProtocolsEndpoint.subprotocols == null) { + count++; + Thread.sleep(100); + } + Assert.assertNotNull(SubProtocolsEndpoint.subprotocols); + Assert.assertArrayEquals(new String[] { "sp1", "sp2" }, + SubProtocolsEndpoint.subprotocols.toArray(new String[0])); + wsSession.close(); + SubProtocolsEndpoint.recycle(); + } + + @ServerEndpoint(value = "/echo", subprotocols = { "sp1", "sp2" }) + public static class SubProtocolsEndpoint { + public static final String PATH_BASIC = "/echo"; + public static volatile List subprotocols; + + @OnOpen + public void processOpen(@SuppressWarnings("unused") Session session, EndpointConfig epc) { + subprotocols = ((ServerEndpointConfig) epc).getSubprotocols(); + } + + public static void recycle() { + subprotocols = null; + } + + } + + public static class Config extends TesterEndpointConfig { + + @Override + protected Class getEndpointClass() { + return SubProtocolsEndpoint.class; + } + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java b/test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java new file mode 100644 index 0000000..871bcad --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsWebSocketContainer.java @@ -0,0 +1,665 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.servlet.ServletContextEvent; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Extension; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.OnMessage; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpoint; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicBinary; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicHandler; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterEndpoint; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; +import org.apache.tomcat.websocket.server.WsContextListener; + +public class TestWsWebSocketContainer extends WsWebSocketContainerBaseTest { + + private static final String MESSAGE_EMPTY = ""; + private static final String MESSAGE_STRING_1 = "qwerty"; + private static final String MESSAGE_TEXT_4K; + + // 5s should be plenty but Gump can be a lot slower + private static final long START_STOP_WAIT = 60 * 1000; + + static { + StringBuilder sb = new StringBuilder(4096); + for (int i = 0; i < 4096; i++) { + sb.append('*'); + } + MESSAGE_TEXT_4K = sb.toString(); + } + + + @Test + public void testConnectToServerEndpoint() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + // Set this artificially small to trigger + // https://bz.apache.org/bugzilla/show_bug.cgi?id=57054 + wsContainer.setDefaultMaxBinaryMessageBufferSize(64); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + ClientEndpointConfig.Builder.create().build(), + new URI("ws://" + getHostName() + ":" + getPort() + TesterEchoServer.Config.PATH_ASYNC)); + CountDownLatch latch = new CountDownLatch(1); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText(MESSAGE_STRING_1); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + Queue messages = handler.getMessages(); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(MESSAGE_STRING_1, messages.peek()); + + ((WsWebSocketContainer) wsContainer).destroy(); + } + + + @Test(expected = DeploymentException.class) + public void testConnectToServerEndpointInvalidScheme() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + wsContainer.connectToServer(TesterProgrammaticEndpoint.class, ClientEndpointConfig.Builder.create().build(), + new URI("ftp://" + getHostName() + ":" + getPort() + TesterEchoServer.Config.PATH_ASYNC)); + } + + + @Test(expected = DeploymentException.class) + public void testConnectToServerEndpointNoHost() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + wsContainer.connectToServer(TesterProgrammaticEndpoint.class, ClientEndpointConfig.Builder.create().build(), + new URI("ws://" + TesterEchoServer.Config.PATH_ASYNC)); + } + + + @Test + public void testSmallTextBufferClientTextMessage() throws Exception { + doBufferTest(true, false, true, false); + } + + + @Test + public void testSmallTextBufferClientBinaryMessage() throws Exception { + doBufferTest(true, false, false, true); + } + + + @Test + public void testSmallTextBufferServerTextMessage() throws Exception { + doBufferTest(true, true, true, false); + } + + + @Test + public void testSmallTextBufferServerBinaryMessage() throws Exception { + doBufferTest(true, true, false, true); + } + + + @Test + public void testSmallBinaryBufferClientTextMessage() throws Exception { + doBufferTest(false, false, true, true); + } + + + @Test + public void testSmallBinaryBufferClientBinaryMessage() throws Exception { + doBufferTest(false, false, false, false); + } + + + @Test + public void testSmallBinaryBufferServerTextMessage() throws Exception { + doBufferTest(false, true, true, true); + } + + + @Test + public void testSmallBinaryBufferServerBinaryMessage() throws Exception { + doBufferTest(false, true, false, false); + } + + + private void doBufferTest(boolean isTextBuffer, boolean isServerBuffer, boolean isTextMessage, boolean pass) + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + if (isServerBuffer) { + if (isTextBuffer) { + ctx.addParameter( + org.apache.tomcat.websocket.server.Constants.TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM, + "1024"); + } else { + ctx.addParameter( + org.apache.tomcat.websocket.server.Constants.BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM, + "1024"); + } + } else { + if (isTextBuffer) { + wsContainer.setDefaultMaxTextMessageBufferSize(1024); + } else { + wsContainer.setDefaultMaxBinaryMessageBufferSize(1024); + } + } + + tomcat.start(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + ClientEndpointConfig.Builder.create().build(), + new URI("ws://" + getHostName() + ":" + getPort() + TesterEchoServer.Config.PATH_BASIC)); + BasicHandler handler; + CountDownLatch latch = new CountDownLatch(1); + TesterEndpoint tep = (TesterEndpoint) wsSession.getUserProperties().get("endpoint"); + tep.setLatch(latch); + if (isTextMessage) { + handler = new BasicText(latch); + } else { + handler = new BasicBinary(latch); + } + + wsSession.addMessageHandler(handler); + try { + if (isTextMessage) { + wsSession.getBasicRemote().sendText(MESSAGE_TEXT_4K); + } else { + wsSession.getBasicRemote().sendBinary(ByteBuffer.wrap(MESSAGE_BINARY_4K)); + } + } catch (IOException ioe) { + // Some messages sends are expected to fail. Assertions further on + // in this method will check for the correct behaviour so ignore any + // exception here. + } + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + Queue messages = handler.getMessages(); + if (pass) { + Assert.assertEquals(1, messages.size()); + if (isTextMessage) { + Assert.assertEquals(MESSAGE_TEXT_4K, messages.peek()); + } else { + Assert.assertEquals(ByteBuffer.wrap(MESSAGE_BINARY_4K), messages.peek()); + } + } else { + // When the message exceeds the buffer size, the WebSocket is + // closed. The endpoint ensures that the latch is cleared when the + // WebSocket closes. However, the session isn't marked as closed + // until after the onClose() method completes so there is a small + // window where this test could fail. Therefore, wait briefly to + // give the session a chance to complete the close process. + for (int i = 0; i < 500; i++) { + if (!wsSession.isOpen()) { + break; + } + Thread.sleep(10); + } + Assert.assertFalse(wsSession.isOpen()); + } + } + + + public static class BlockingConfig extends WsContextListener { + + public static final String PATH = "/block"; + + @Override + public void contextInitialized(ServletContextEvent sce) { + super.contextInitialized(sce); + ServerContainer sc = (ServerContainer) sce.getServletContext().getAttribute( + org.apache.tomcat.websocket.server.Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + try { + // Reset blocking state + BlockingPojo.resetBlock(); + sc.addEndpoint(BlockingPojo.class); + } catch (DeploymentException e) { + throw new IllegalStateException(e); + } + } + } + + + @ServerEndpoint("/block") + public static class BlockingPojo { + + private static Object monitor = new Object(); + // Enable blocking by default + private static boolean block = true; + + /** + * Clear any current block. + */ + public static void clearBlock() { + synchronized (monitor) { + block = false; + monitor.notifyAll(); + } + } + + public static void resetBlock() { + synchronized (monitor) { + block = true; + } + } + + @SuppressWarnings("unused") + @OnMessage + public void echoTextMessage(Session session, String msg, boolean last) { + try { + synchronized (monitor) { + while (block) { + monitor.wait(); + } + } + } catch (InterruptedException e) { + // Ignore + } + } + + + @SuppressWarnings("unused") + @OnMessage + public void echoBinaryMessage(Session session, ByteBuffer msg, boolean last) { + try { + synchronized (monitor) { + while (block) { + monitor.wait(); + } + } + } catch (InterruptedException e) { + // Ignore + } + } + } + + + public static class BlockingBinaryHandler implements MessageHandler.Partial { + + @Override + public void onMessage(ByteBuffer messagePart, boolean last) { + try { + Thread.sleep(TIMEOUT_MS * 10); + } catch (InterruptedException e) { + // Ignore + } + } + } + + + @Test + public void testGetOpenSessions() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + EndpointA endpointA = new EndpointA(); + Session s1a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + Session s2a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + Session s3a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + + EndpointB endpointB = new EndpointB(); + Session s1b = connectToEchoServer(wsContainer, endpointB, TesterEchoServer.Config.PATH_BASIC); + Session s2b = connectToEchoServer(wsContainer, endpointB, TesterEchoServer.Config.PATH_BASIC); + + Set setA = s3a.getOpenSessions(); + Assert.assertEquals(3, setA.size()); + Assert.assertTrue(setA.remove(s1a)); + Assert.assertTrue(setA.remove(s2a)); + Assert.assertTrue(setA.remove(s3a)); + + s1a.close(); + + setA = s3a.getOpenSessions(); + Assert.assertEquals(2, setA.size()); + Assert.assertFalse(setA.remove(s1a)); + Assert.assertTrue(setA.remove(s2a)); + Assert.assertTrue(setA.remove(s3a)); + + Set setB = s1b.getOpenSessions(); + Assert.assertEquals(2, setB.size()); + Assert.assertTrue(setB.remove(s1b)); + Assert.assertTrue(setB.remove(s2b)); + + // Close sessions explicitly as Gump reports a session remains open at + // the end of this test + s2a.close(); + s3a.close(); + s1b.close(); + s2b.close(); + } + + + @Test + public void testSessionExpiryOnUserPropertyReadIdleTimeout() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + // Need access to implementation methods for configuring unit tests + WsWebSocketContainer wsContainer = (WsWebSocketContainer) ContainerProvider.getWebSocketContainer(); + + wsContainer.setDefaultMaxSessionIdleTimeout(90000); + wsContainer.setProcessPeriod(1); + + EndpointA endpointA = new EndpointA(); + Session s1a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + s1a.setMaxIdleTimeout(90000); + + s1a.getUserProperties().put(Constants.READ_IDLE_TIMEOUT_MS, Long.valueOf(5000)); + + // maxIdleTimeout is 90s but the readIdleTimeout is 5s. The session + // should get closed after 5 seconds as nothing is read on it. + + // First confirm the session has been opened. + Assert.assertEquals(1, s1a.getOpenSessions().size()); + + // Now wait for it to close. Allow up to 30s as some CI systems are slow + // but that is still well under the 90s configured for the session. + int count = 0; + while (count < 300 && s1a.isOpen()) { + count++; + Thread.sleep(100); + } + Assert.assertFalse(s1a.isOpen()); + } + + + @Test + public void testSessionExpiryOnUserPropertyWriteIdleTimeout() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + // Need access to implementation methods for configuring unit tests + WsWebSocketContainer wsContainer = (WsWebSocketContainer) ContainerProvider.getWebSocketContainer(); + + wsContainer.setDefaultMaxSessionIdleTimeout(90000); + wsContainer.setProcessPeriod(1); + + EndpointA endpointA = new EndpointA(); + Session s1a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + s1a.setMaxIdleTimeout(90000); + + s1a.getUserProperties().put(Constants.WRITE_IDLE_TIMEOUT_MS, Long.valueOf(5000)); + + // maxIdleTimeout is 90s but the writeIdleTimeout is 5s. The session + // should get closed after 5 seconds as nothing is written on it. + + // First confirm the session has been opened. + Assert.assertEquals(1, s1a.getOpenSessions().size()); + + // Now wait for it to close. Allow up to 30s as some CI systems are slow + // but that is still well under the 90s configured for the session. + int count = 0; + while (count < 300 && s1a.isOpen()) { + count++; + Thread.sleep(100); + } + Assert.assertFalse(s1a.isOpen()); + } + + + public static final class EndpointA extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig config) { + // NO-OP + } + } + + + public static final class EndpointB extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig config) { + // NO-OP + } + } + + + @Test + public void testMaxMessageSize01() throws Exception { + doMaxMessageSize(TesterEchoServer.Config.PATH_BASIC_LIMIT_LOW, TesterEchoServer.BasicLimitLow.MAX_SIZE - 1, + true); + } + + + @Test + public void testMaxMessageSize02() throws Exception { + doMaxMessageSize(TesterEchoServer.Config.PATH_BASIC_LIMIT_LOW, TesterEchoServer.BasicLimitLow.MAX_SIZE, true); + } + + + @Test + public void testMaxMessageSize03() throws Exception { + doMaxMessageSize(TesterEchoServer.Config.PATH_BASIC_LIMIT_LOW, TesterEchoServer.BasicLimitLow.MAX_SIZE + 1, + false); + } + + + @Test + public void testMaxMessageSize04() throws Exception { + doMaxMessageSize(TesterEchoServer.Config.PATH_BASIC_LIMIT_HIGH, TesterEchoServer.BasicLimitHigh.MAX_SIZE - 1, + true); + } + + + @Test + public void testMaxMessageSize05() throws Exception { + doMaxMessageSize(TesterEchoServer.Config.PATH_BASIC_LIMIT_HIGH, TesterEchoServer.BasicLimitHigh.MAX_SIZE, true); + } + + + @Test + public void testMaxMessageSize06() throws Exception { + doMaxMessageSize(TesterEchoServer.Config.PATH_BASIC_LIMIT_HIGH, TesterEchoServer.BasicLimitHigh.MAX_SIZE + 1, + false); + } + + + private void doMaxMessageSize(String path, long size, boolean expectOpen) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + Session s = connectToEchoServer(wsContainer, new EndpointA(), path); + + // One for the client, one for the server + validateBackgroundProcessCount(2); + + StringBuilder msg = new StringBuilder(); + for (long i = 0; i < size; i++) { + msg.append('x'); + } + + s.getBasicRemote().sendText(msg.toString()); + + // Wait for up to 5 seconds for the client session to open + boolean open = s.isOpen(); + int count = 0; + while (open != expectOpen && count < 50) { + Thread.sleep(100); + count++; + open = s.isOpen(); + } + + Assert.assertEquals(Boolean.valueOf(expectOpen), Boolean.valueOf(s.isOpen())); + + // Close the session if it is expected to be open + if (expectOpen) { + s.close(); + } + + // Ensure both server and client have shutdown + validateBackgroundProcessCount(0); + } + + + private void validateBackgroundProcessCount(int expected) throws Exception { + int count = 0; + while (count < (START_STOP_WAIT / 100)) { + if (BackgroundProcessManager.getInstance().getProcessCount() == expected) { + break; + } + Thread.sleep(100); + count++; + } + Assert.assertEquals(expected, BackgroundProcessManager.getInstance().getProcessCount()); + + } + + @Test + public void testPerMessageDeflateClient01() throws Exception { + doTestPerMessageDeflateClient(MESSAGE_STRING_1, 1); + } + + + @Test + public void testPerMessageDeflateClient02() throws Exception { + doTestPerMessageDeflateClient(MESSAGE_EMPTY, 1); + } + + + @Test + public void testPerMessageDeflateClient03() throws Exception { + doTestPerMessageDeflateClient(MESSAGE_STRING_1, 2); + } + + + @Test + public void testPerMessageDeflateClient04() throws Exception { + doTestPerMessageDeflateClient(MESSAGE_EMPTY, 2); + } + + + private void doTestPerMessageDeflateClient(String msg, int count) throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + Extension perMessageDeflate = new WsExtension(PerMessageDeflate.NAME); + List extensions = new ArrayList<>(1); + extensions.add(perMessageDeflate); + + ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create().extensions(extensions).build(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientConfig, + new URI("ws://" + getHostName() + ":" + getPort() + TesterEchoServer.Config.PATH_ASYNC)); + CountDownLatch latch = new CountDownLatch(count); + BasicText handler = new BasicText(latch, msg); + wsSession.addMessageHandler(handler); + for (int i = 0; i < count; i++) { + wsSession.getBasicRemote().sendText(msg); + } + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + ((WsWebSocketContainer) wsContainer).destroy(); + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsWebSocketContainerGetOpenSessions.java b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerGetOpenSessions.java new file mode 100644 index 0000000..3f9f990 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerGetOpenSessions.java @@ -0,0 +1,391 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.ServletContextEvent; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.CloseReason; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.OnMessage; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpoint; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.server.Constants; +import org.apache.tomcat.websocket.server.WsContextListener; + +/* + * This method is split out into a separate class to make it easier to track the + * various permutations and combinations of client and server endpoints. + * + * Each test uses 2 client endpoint and 2 server endpoints with each client + * connecting to each server for a total of four connections (note sometimes + * the two clients and/or the two servers will be the same). + */ +public class TestWsWebSocketContainerGetOpenSessions extends WebSocketBaseTest { + + @Test + public void testClientAClientAPojoAPojoA() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointA(); + + doTest(client1, client2, "/pojoA", "/pojoA", 2, 2, 4, 4); + } + + + @Test + public void testClientAClientBPojoAPojoA() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointB(); + + doTest(client1, client2, "/pojoA", "/pojoA", 2, 2, 4, 4); + } + + + @Test + public void testClientAClientAPojoAPojoB() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointA(); + + doTest(client1, client2, "/pojoA", "/pojoB", 2, 2, 2, 2); + } + + + @Test + public void testClientAClientBPojoAPojoB() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointB(); + + doTest(client1, client2, "/pojoA", "/pojoB", 2, 2, 2, 2); + } + + + @Test + public void testClientAClientAProgAProgA() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointA(); + + doTest(client1, client2, "/progA", "/progA", 2, 2, 4, 4); + } + + + @Test + public void testClientAClientBProgAProgA() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointB(); + + doTest(client1, client2, "/progA", "/progA", 2, 2, 4, 4); + } + + + @Test + public void testClientAClientAProgAProgB() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointA(); + + doTest(client1, client2, "/progA", "/progB", 2, 2, 2, 2); + } + + + @Test + public void testClientAClientBProgAProgB() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointB(); + + doTest(client1, client2, "/progA", "/progB", 2, 2, 2, 2); + } + + + @Test + public void testClientAClientAPojoAProgA() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointA(); + + doTest(client1, client2, "/pojoA", "/progA", 2, 2, 2, 2); + } + + + @Test + public void testClientAClientBPojoAProgA() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointB(); + + doTest(client1, client2, "/pojoA", "/progA", 2, 2, 2, 2); + } + + + @Test + public void testClientAClientAPojoAProgB() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointA(); + + doTest(client1, client2, "/pojoA", "/progB", 2, 2, 2, 2); + } + + + @Test + public void testClientAClientBPojoAProgB() throws Exception { + Endpoint client1 = new ClientEndpointA(); + Endpoint client2 = new ClientEndpointB(); + + doTest(client1, client2, "/pojoA", "/progB", 2, 2, 2, 2); + } + + + private void doTest(Endpoint client1, Endpoint client2, String server1, String server2, int client1Count, + int client2Count, int server1Count, int server2Count) throws Exception { + Tracker.reset(); + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + Session sClient1Server1 = createSession(wsContainer, client1, "client1", server1); + Session sClient1Server2 = createSession(wsContainer, client1, "client1", server2); + Session sClient2Server1 = createSession(wsContainer, client2, "client2", server1); + Session sClient2Server2 = createSession(wsContainer, client2, "client2", server2); + + + int delayCount = 0; + // Wait for up to 20s for this to complete. It should be a lot faster + // but some CI systems get be slow at times. + while (Tracker.getUpdateCount() < 8 && delayCount < 400) { + Thread.sleep(50); + delayCount++; + } + + Assert.assertTrue(Tracker.dump(), Tracker.checkRecord("client1", client1Count)); + Assert.assertTrue(Tracker.dump(), Tracker.checkRecord("client2", client2Count)); + // Note: need to strip leading '/' from path + Assert.assertTrue(Tracker.dump(), Tracker.checkRecord(server1.substring(1), server1Count)); + Assert.assertTrue(Tracker.dump(), Tracker.checkRecord(server2.substring(1), server2Count)); + + sClient1Server1.close(); + sClient1Server2.close(); + sClient2Server1.close(); + sClient2Server2.close(); + } + + + private Session createSession(WebSocketContainer wsContainer, Endpoint client, String clientName, String server) + throws DeploymentException, IOException, URISyntaxException { + + Session s = wsContainer.connectToServer(client, ClientEndpointConfig.Builder.create().build(), + new URI("ws://localhost:" + getPort() + server)); + Tracker.addRecord(clientName, s.getOpenSessions().size()); + s.getBasicRemote().sendText("X"); + return s; + } + + + public static class Config extends WsContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + super.contextInitialized(sce); + ServerContainer sc = (ServerContainer) sce.getServletContext() + .getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + + try { + sc.addEndpoint(PojoEndpointA.class); + sc.addEndpoint(PojoEndpointB.class); + sc.addEndpoint(ServerEndpointConfig.Builder.create(ServerEndpointA.class, "/progA").build()); + sc.addEndpoint(ServerEndpointConfig.Builder.create(ServerEndpointB.class, "/progB").build()); + } catch (DeploymentException e) { + throw new IllegalStateException(e); + } + } + } + + + public abstract static class PojoEndpointBase { + + @OnMessage + public void onMessage(@SuppressWarnings("unused") String msg, Session session) { + Tracker.addRecord(getTrackingName(), session.getOpenSessions().size()); + } + + protected abstract String getTrackingName(); + } + + + @ServerEndpoint("/pojoA") + public static class PojoEndpointA extends PojoEndpointBase { + + @Override + protected String getTrackingName() { + return "pojoA"; + } + } + + + @ServerEndpoint("/pojoB") + public static class PojoEndpointB extends PojoEndpointBase { + + @Override + protected String getTrackingName() { + return "pojoB"; + } + } + + + public abstract static class ServerEndpointBase extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig config) { + session.addMessageHandler(new TrackerMessageHandler(session, getTrackingName())); + } + + protected abstract String getTrackingName(); + } + + + public static final class ServerEndpointA extends ServerEndpointBase { + + @Override + protected String getTrackingName() { + return "progA"; + } + } + + + public static final class ServerEndpointB extends ServerEndpointBase { + + @Override + protected String getTrackingName() { + return "progB"; + } + } + + + public static final class TrackerMessageHandler implements MessageHandler.Whole { + + private final Session session; + private final String trackingName; + + public TrackerMessageHandler(Session session, String trackingName) { + this.session = session; + this.trackingName = trackingName; + } + + @Override + public void onMessage(String message) { + Tracker.addRecord(trackingName, session.getOpenSessions().size()); + } + } + + + public abstract static class ClientEndpointBase extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig config) { + // NO-OP + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + // NO-OP + } + + protected abstract String getTrackingName(); + } + + + public static final class ClientEndpointA extends ClientEndpointBase { + + @Override + protected String getTrackingName() { + return "clientA"; + } + } + + + public static final class ClientEndpointB extends ClientEndpointBase { + + @Override + protected String getTrackingName() { + return "clientB"; + } + } + + + public static class Tracker { + + private static final Map records = new HashMap<>(); + private static int updateCount = 0; + + public static synchronized void addRecord(String key, int count) { + // Need to avoid out of order updates to the Map. If out of order + // updates occur, keep the one with the highest count. + Integer oldCount = records.get(key); + if (oldCount == null || oldCount.intValue() < count) { + records.put(key, Integer.valueOf(count)); + } + updateCount++; + } + + public static synchronized boolean checkRecord(String key, int expectedCount) { + Integer actualCount = records.get(key); + if (actualCount == null) { + if (expectedCount == 0) { + return true; + } else { + return false; + } + } else { + return actualCount.intValue() == expectedCount; + } + } + + public static synchronized int getUpdateCount() { + return updateCount; + } + + public static synchronized void reset() { + records.clear(); + updateCount = 0; + } + + public static synchronized String dump() { + return records.toString(); + } + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSSL.java b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSSL.java new file mode 100644 index 0000000..2fdfcf4 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSSL.java @@ -0,0 +1,169 @@ +/* + * 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.tomcat.websocket; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URI; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.net.TesterSupport; +import org.apache.tomcat.util.security.KeyStoreUtil; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + +@RunWith(Parameterized.class) +public class TestWsWebSocketContainerSSL extends WebSocketBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation" }); + parameterSets.add( + new Object[] { "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }); + parameterSets.add(new Object[] { "OpenSSL-FFM", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean needApr; + + @Parameter(2) + public String sslImplementationName; + + + private static final String MESSAGE_STRING_1 = "qwerty"; + + @SuppressWarnings("removal") + @Test + public void testConnectToServerEndpointSslLegacy() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = tomcat.addContext("", null); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + clientEndpointConfig.getUserProperties().put(org.apache.tomcat.websocket.Constants.SSL_TRUSTSTORE_PROPERTY, + TesterSupport.CA_JKS); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("wss://localhost:" + getPort() + TesterEchoServer.Config.PATH_ASYNC)); + CountDownLatch latch = new CountDownLatch(1); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText(MESSAGE_STRING_1); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + Queue messages = handler.getMessages(); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(MESSAGE_STRING_1, messages.peek()); + } + + + @Test + public void testConnectToServerEndpointSSL() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + // Build the SSLContext + SSLContext sslContext = SSLContext.getInstance("TLS"); + File trustStoreFile = new File(TesterSupport.CA_JKS); + KeyStore ks = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(trustStoreFile)) { + KeyStoreUtil.load(ks, is, TesterSupport.JKS_PASS.toCharArray()); + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + sslContext.init(null, tmf.getTrustManagers(), null); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().sslContext(sslContext) + .build(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("wss://localhost:" + getPort() + TesterEchoServer.Config.PATH_ASYNC)); + CountDownLatch latch = new CountDownLatch(1); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText(MESSAGE_STRING_1); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + Queue messages = handler.getMessages(); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(MESSAGE_STRING_1, messages.peek()); + } + + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.initSsl(tomcat); + + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, needApr); + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpiryContainerClient.java b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpiryContainerClient.java new file mode 100644 index 0000000..2f101f3 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpiryContainerClient.java @@ -0,0 +1,91 @@ +/* + * 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.tomcat.websocket; + +import java.util.Set; + +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TestWsWebSocketContainer.EndpointA; + +/* + * Moved to separate test class to improve test concurrency. These tests are + * some of the last tests to start and having them all in a single class + * significantly extends the length of a test run when using multiple test + * threads. + */ +public class TestWsWebSocketContainerSessionExpiryContainerClient extends WsWebSocketContainerBaseTest { + + @Test + public void testSessionExpiryContainer() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + // Need access to implementation methods for configuring unit tests + WsWebSocketContainer wsContainer = (WsWebSocketContainer) ContainerProvider.getWebSocketContainer(); + + // 5 second timeout + wsContainer.setDefaultMaxSessionIdleTimeout(5000); + wsContainer.setProcessPeriod(1); + + EndpointA endpointA = new EndpointA(); + connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + Session s3a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + + // Check all three sessions are open + Set setA = s3a.getOpenSessions(); + Assert.assertEquals(3, setA.size()); + + int count = 0; + boolean isOpen = true; + while (isOpen && count < 100) { + count++; + Thread.sleep(100); + isOpen = false; + for (Session session : setA) { + if (session.isOpen()) { + isOpen = true; + break; + } + } + } + + if (isOpen) { + for (Session session : setA) { + if (session.isOpen()) { + System.err.println("Session with ID [" + session.getId() + "] is open"); + } + } + Assert.fail("There were open sessions"); + } + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpiryContainerServer.java b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpiryContainerServer.java new file mode 100644 index 0000000..16fbf41 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpiryContainerServer.java @@ -0,0 +1,107 @@ +/* + * 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.tomcat.websocket; + +import java.util.Set; + +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TestWsWebSocketContainer.EndpointA; +import org.apache.tomcat.websocket.server.Constants; +import org.apache.tomcat.websocket.server.WsServerContainer; + +/* + * Moved to separate test class to improve test concurrency. These tests are + * some of the last tests to start and having them all in a single class + * significantly extends the length of a test run when using multiple test + * threads. + */ +public class TestWsWebSocketContainerSessionExpiryContainerServer extends WsWebSocketContainerBaseTest { + + @Test + public void testSessionExpiryContainer() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + ctx.addApplicationListener(WebSocketServerTimeoutConfig.class.getName()); + + tomcat.start(); + + // Need access to implementation methods for configuring unit tests + WsWebSocketContainer wsContainer = (WsWebSocketContainer) ContainerProvider.getWebSocketContainer(); + + EndpointA endpointA = new EndpointA(); + connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + Session s3a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + + // Check all three sessions are open + Set setA = s3a.getOpenSessions(); + Assert.assertEquals(3, setA.size()); + + int count = 0; + boolean isOpen = true; + while (isOpen && count < 100) { + count++; + Thread.sleep(100); + isOpen = false; + for (Session session : setA) { + if (session.isOpen()) { + isOpen = true; + break; + } + } + } + + if (isOpen) { + for (Session session : setA) { + if (session.isOpen()) { + System.err.println("Session with ID [" + session.getId() + "] is open"); + } + } + Assert.fail("There were open sessions"); + } + } + + + public static class WebSocketServerTimeoutConfig implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + WsServerContainer container = (WsServerContainer) sce.getServletContext().getAttribute( + Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + // Process timeouts on every run of the background thread + container.setProcessPeriod(0); + // Set session timeout to 5s + container.setDefaultMaxSessionIdleTimeout(5000); + } + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpirySession.java b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpirySession.java new file mode 100644 index 0000000..e061204 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerSessionExpirySession.java @@ -0,0 +1,96 @@ +/* + * 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.tomcat.websocket; + +import java.util.Set; + +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TestWsWebSocketContainer.EndpointA; + +/* + * Moved to separate test class to improve test concurrency. These tests are + * some of the last tests to start and having them all in a single class + * significantly extends the length of a test run when using multiple test + * threads. + */ +public class TestWsWebSocketContainerSessionExpirySession extends WsWebSocketContainerBaseTest { + + @Test + public void testSessionExpirySession() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + // Need access to implementation methods for configuring unit tests + WsWebSocketContainer wsContainer = (WsWebSocketContainer) ContainerProvider.getWebSocketContainer(); + + // 5 second timeout + wsContainer.setDefaultMaxSessionIdleTimeout(5000); + wsContainer.setProcessPeriod(1); + + EndpointA endpointA = new EndpointA(); + Session s1a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + s1a.setMaxIdleTimeout(3000); + Session s2a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + s2a.setMaxIdleTimeout(6000); + Session s3a = connectToEchoServer(wsContainer, endpointA, TesterEchoServer.Config.PATH_BASIC); + s3a.setMaxIdleTimeout(9000); + + // Check all three sessions are open + Set setA = s3a.getOpenSessions(); + + int expected = 3; + while (expected > 0) { + Assert.assertEquals(expected, getOpenCount(setA)); + + int count = 0; + while (getOpenCount(setA) == expected && count < 50) { + count++; + Thread.sleep(100); + } + + expected--; + } + + Assert.assertEquals(0, getOpenCount(setA)); + } + + + private int getOpenCount(Set sessions) { + int result = 0; + for (Session session : sessions) { + if (session.isOpen()) { + result++; + } + } + return result; + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutClient.java b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutClient.java new file mode 100644 index 0000000..86e2f23 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutClient.java @@ -0,0 +1,119 @@ +/* + * 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.tomcat.websocket; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.Future; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TestWsWebSocketContainer.BlockingConfig; +import org.apache.tomcat.websocket.TestWsWebSocketContainer.BlockingPojo; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + +/* + * Moved to separate test class to improve test concurrency. These tests are + * some of the last tests to start and having them all in a single class + * significantly extends the length of a test run when using multiple test + * threads. + */ +public class TestWsWebSocketContainerTimeoutClient extends WsWebSocketContainerBaseTest { + + @Test + public void testWriteTimeoutClientContainer() throws Exception { + doTestWriteTimeoutClient(true); + } + + + @Test + public void testWriteTimeoutClientEndpoint() throws Exception { + doTestWriteTimeoutClient(false); + } + + + private void doTestWriteTimeoutClient(boolean setTimeoutOnContainer) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(BlockingConfig.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + // Set the async timeout + if (setTimeoutOnContainer) { + wsContainer.setAsyncSendTimeout(TIMEOUT_MS); + } + + tomcat.start(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + ClientEndpointConfig.Builder.create().build(), + new URI("ws://" + getHostName() + ":" + getPort() + BlockingConfig.PATH)); + + if (!setTimeoutOnContainer) { + wsSession.getAsyncRemote().setSendTimeout(TIMEOUT_MS); + } + + long lastSend = 0; + + // Should send quickly until the network buffers fill up and then block + // until the timeout kicks in + Exception exception = null; + try { + while (true) { + lastSend = System.currentTimeMillis(); + Future f = wsSession.getAsyncRemote().sendBinary(ByteBuffer.wrap(MESSAGE_BINARY_4K)); + f.get(); + } + } catch (Exception e) { + exception = e; + } + + long timeout = System.currentTimeMillis() - lastSend; + + // Clear the server side block and prevent further blocks to allow the + // server to shutdown cleanly + BlockingPojo.clearBlock(); + + // Close the client session, primarily to allow the + // BackgroundProcessManager to shut down. + wsSession.close(); + + String msg = "Time out was [" + timeout + "] ms"; + + // Check correct time passed + Assert.assertTrue(msg, timeout >= TIMEOUT_MS - MARGIN); + + // Check the timeout wasn't too long + Assert.assertTrue(msg, timeout < TIMEOUT_MS * 2); + + Assert.assertNotNull(exception); + } +} diff --git a/test/org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutServer.java b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutServer.java new file mode 100644 index 0000000..b3b31b6 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutServer.java @@ -0,0 +1,191 @@ +/* + * 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.tomcat.websocket; + +import java.net.SocketTimeoutException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import jakarta.servlet.ServletContextEvent; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TestWsWebSocketContainer.BlockingBinaryHandler; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; +import org.apache.tomcat.websocket.server.WsContextListener; + +/* + * Moved to separate test class to improve test concurrency. These tests are + * some of the last tests to start and having them all in a single class + * significantly extends the length of a test run when using multiple test + * threads. + */ +public class TestWsWebSocketContainerTimeoutServer extends WsWebSocketContainerBaseTest { + + @Test + public void testWriteTimeoutServerContainer() throws Exception { + doTestWriteTimeoutServer(true); + } + + + @Test + public void testWriteTimeoutServerEndpoint() throws Exception { + doTestWriteTimeoutServer(false); + } + + + private static volatile boolean timeoutOnContainer = false; + + private void doTestWriteTimeoutServer(boolean setTimeoutOnContainer) throws Exception { + + /* + * Note: There are all sorts of horrible uses of statics in this test because the API uses classes and the tests + * really need access to the instances which simply isn't possible. + */ + timeoutOnContainer = setTimeoutOnContainer; + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ConstantTxConfig.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + ClientEndpointConfig.Builder.create().build(), + new URI("ws://" + getHostName() + ":" + getPort() + ConstantTxConfig.PATH)); + + wsSession.addMessageHandler(new BlockingBinaryHandler()); + + int loops = 0; + while (loops < 15) { + Thread.sleep(1000); + if (!ConstantTxEndpoint.getRunning()) { + break; + } + loops++; + } + + // Close the client session, primarily to allow the + // BackgroundProcessManager to shut down. + wsSession.close(); + + // Check the right exception was thrown + Assert.assertNotNull(ConstantTxEndpoint.getException()); + Assert.assertEquals(ExecutionException.class, ConstantTxEndpoint.getException().getClass()); + Assert.assertNotNull(ConstantTxEndpoint.getException().getCause()); + Assert.assertEquals(SocketTimeoutException.class, ConstantTxEndpoint.getException().getCause().getClass()); + + // Check correct time passed + Assert.assertTrue(ConstantTxEndpoint.getTimeout() >= TIMEOUT_MS); + + // Check the timeout wasn't too long + Assert.assertTrue(ConstantTxEndpoint.getTimeout() < TIMEOUT_MS * 2); + } + + + public static class ConstantTxConfig extends WsContextListener { + + private static final String PATH = "/test"; + + @Override + public void contextInitialized(ServletContextEvent sce) { + super.contextInitialized(sce); + ServerContainer sc = (ServerContainer) sce.getServletContext().getAttribute( + org.apache.tomcat.websocket.server.Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + try { + sc.addEndpoint(ServerEndpointConfig.Builder.create(ConstantTxEndpoint.class, PATH).build()); + if (timeoutOnContainer) { + sc.setAsyncSendTimeout(TIMEOUT_MS); + } + } catch (DeploymentException e) { + throw new IllegalStateException(e); + } + } + } + + + public static class ConstantTxEndpoint extends Endpoint { + + // Have to be static to be able to retrieve results from test case + private static volatile long timeout = -1; + private static volatile Exception exception = null; + private static volatile boolean running = true; + + + @Override + public void onOpen(Session session, EndpointConfig config) { + + // Reset everything + timeout = -1; + exception = null; + running = true; + + if (!timeoutOnContainer) { + session.getAsyncRemote().setSendTimeout(TIMEOUT_MS); + } + + long lastSend = 0; + + // Should send quickly until the network buffers fill up and then + // block until the timeout kicks in + try { + while (true) { + lastSend = System.currentTimeMillis(); + Future f = session.getAsyncRemote().sendBinary(ByteBuffer.wrap(MESSAGE_BINARY_4K)); + f.get(); + } + } catch (ExecutionException | InterruptedException e) { + exception = e; + } + timeout = System.currentTimeMillis() - lastSend; + running = false; + } + + public static long getTimeout() { + return timeout; + } + + public static Exception getException() { + return exception; + } + + public static boolean getRunning() { + return running; + } + } +} diff --git a/test/org/apache/tomcat/websocket/TesterAsyncTiming.java b/test/org/apache/tomcat/websocket/TesterAsyncTiming.java new file mode 100644 index 0000000..780b3a4 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterAsyncTiming.java @@ -0,0 +1,87 @@ +/* + * 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.tomcat.websocket; + +import java.nio.ByteBuffer; +import java.util.concurrent.Semaphore; + +import jakarta.websocket.OnMessage; +import jakarta.websocket.RemoteEndpoint.Async; +import jakarta.websocket.SendHandler; +import jakarta.websocket.SendResult; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +import org.apache.tomcat.websocket.server.TesterEndpointConfig; + +public class TesterAsyncTiming { + + public static class Config extends TesterEndpointConfig { + + public static final String PATH = "/timing"; + public static final int ITERATIONS = 500; + public static final int SLEEP_MILLI = 50; + + @Override + protected Class getEndpointClass() { + return Endpoint.class; + } + } + + @ServerEndpoint(Config.PATH) + public static class Endpoint { + + private static final ByteBuffer LARGE_DATA = ByteBuffer.allocate(16 * 1024); + private static final ByteBuffer SMALL_DATA = ByteBuffer.allocate(4 * 1024); + + @OnMessage + public void onMessage(Session session, @SuppressWarnings("unused") String text) { + + Semaphore semaphore = new Semaphore(1); + SendHandler handler = new SemaphoreSendHandler(semaphore); + + Async remote = session.getAsyncRemote(); + for (int i = 0; i < Config.ITERATIONS; i++) { + try { + semaphore.acquire(1); + remote.sendBinary(LARGE_DATA, handler); + semaphore.acquire(1); + remote.sendBinary(SMALL_DATA, handler); + Thread.sleep(Config.SLEEP_MILLI); + LARGE_DATA.flip(); + SMALL_DATA.flip(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + } + + private static class SemaphoreSendHandler implements SendHandler { + + private final Semaphore semaphore; + + private SemaphoreSendHandler(Semaphore semaphore) { + this.semaphore = semaphore; + } + + @Override + public void onResult(SendResult result) { + semaphore.release(); + } + } + } +} diff --git a/test/org/apache/tomcat/websocket/TesterBlockWebSocketSCI.java b/test/org/apache/tomcat/websocket/TesterBlockWebSocketSCI.java new file mode 100644 index 0000000..05c9acd --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterBlockWebSocketSCI.java @@ -0,0 +1,43 @@ +/* + * 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.tomcat.websocket; + +import java.util.Collections; +import java.util.Set; + +import jakarta.websocket.Endpoint; +import jakarta.websocket.server.ServerApplicationConfig; +import jakarta.websocket.server.ServerEndpointConfig; + +/** + * This configuration blocks any endpoints discovered by the SCI from being deployed. It is intended to prevent testing + * errors generated when the WebSocket SCI scans the test classes for endpoints as it will discover multiple endpoints + * mapped to the same path ('/'). The tests all explicitly configure their required endpoints so have no need for SCI + * based configuration. + */ +public class TesterBlockWebSocketSCI implements ServerApplicationConfig { + + @Override + public Set getEndpointConfigs(Set> scanned) { + return Collections.emptySet(); + } + + @Override + public Set> getAnnotatedEndpointClasses(Set> scanned) { + return Collections.emptySet(); + } +} diff --git a/test/org/apache/tomcat/websocket/TesterConnectionLimitPerformance.java b/test/org/apache/tomcat/websocket/TesterConnectionLimitPerformance.java new file mode 100644 index 0000000..d5deb7e --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterConnectionLimitPerformance.java @@ -0,0 +1,108 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + +/* + * This test runs until the test machine runs out of resources. There is no benefit it running it as part of a standard + * test run so it is excluded due to the name starting Tester... + */ +public class TesterConnectionLimitPerformance extends TomcatBaseTest { + + /* + * Simple test to see how many outgoing connections can be created on a single machine. + */ + @Test + public void testSingleMachine() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + Assert.assertTrue(tomcat.getConnector().setProperty("maxConnections", "-1")); + + tomcat.start(); + + URI uri = new URI("ws://localhost:" + getPort() + TesterEchoServer.Config.PATH_ASYNC); + AtomicInteger counter = new AtomicInteger(0); + + int threadCount = 50; + + Thread[] threads = new ConnectionThread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new ConnectionThread(counter, uri); + threads[i].start(); + } + + // Wait for the threads to die + for (Thread thread : threads) { + thread.join(); + } + + System.out.println("Maximum connection count was " + counter.get()); + } + + private static class ConnectionThread extends Thread { + + private final AtomicInteger counter; + private final URI uri; + + private ConnectionThread(AtomicInteger counter, URI uri) { + this.counter = counter; + this.uri = uri; + } + + @Override + public void run() { + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + int count = 0; + + try { + while (true) { + wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + jakarta.websocket.ClientEndpointConfig.Builder.create().build(), uri); + count = counter.incrementAndGet(); + if (count % 100 == 0) { + System.out.println(count + " and counting..."); + } + } + } catch (IOException | DeploymentException ioe) { + // Let thread die + } + } + } +} diff --git a/test/org/apache/tomcat/websocket/TesterEchoServer.java b/test/org/apache/tomcat/websocket/TesterEchoServer.java new file mode 100644 index 0000000..f3007af --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterEchoServer.java @@ -0,0 +1,228 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jakarta.servlet.ServletContextEvent; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.OnMessage; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpoint; + +import org.apache.tomcat.websocket.server.Constants; +import org.apache.tomcat.websocket.server.WsContextListener; + +public class TesterEchoServer { + + public static class Config extends WsContextListener { + + public static final String PATH_ASYNC = "/echoAsync"; + public static final String PATH_BASIC = "/echoBasic"; + public static final String PATH_BASIC_LIMIT_LOW = "/echoBasicLimitLow"; + public static final String PATH_BASIC_LIMIT_HIGH = "/echoBasicLimitHigh"; + public static final String PATH_WRITER_ERROR = "/echoWriterError"; + + @Override + public void contextInitialized(ServletContextEvent sce) { + super.contextInitialized(sce); + ServerContainer sc = (ServerContainer) sce.getServletContext() + .getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + try { + sc.addEndpoint(Async.class); + sc.addEndpoint(Basic.class); + sc.addEndpoint(BasicLimitLow.class); + sc.addEndpoint(BasicLimitHigh.class); + sc.addEndpoint(WriterError.class); + sc.addEndpoint(RootEcho.class); + } catch (DeploymentException e) { + throw new IllegalStateException(e); + } + } + } + + + @ServerEndpoint("/echoAsync") + public static class Async { + + @OnMessage + public void echoTextMessage(Session session, String msg, boolean last) { + try { + session.getBasicRemote().sendText(msg, last); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + + + @OnMessage + public void echoBinaryMessage(Session session, ByteBuffer msg, boolean last) { + try { + session.getBasicRemote().sendBinary(msg, last); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + } + + + @ServerEndpoint("/echoBasic") + public static class Basic { + @OnMessage + public void echoTextMessage(Session session, String msg) { + try { + session.getBasicRemote().sendText(msg); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + + + @OnMessage + public void echoBinaryMessage(Session session, ByteBuffer msg) { + try { + session.getBasicRemote().sendBinary(msg); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + } + + + @ServerEndpoint("/echoBasicLimitLow") + public static class BasicLimitLow { + + public static final long MAX_SIZE = 10; + + @OnMessage(maxMessageSize = MAX_SIZE) + public void echoTextMessage(Session session, String msg) { + try { + session.getBasicRemote().sendText(msg); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + + + @OnMessage(maxMessageSize = MAX_SIZE) + public void echoBinaryMessage(Session session, ByteBuffer msg) { + try { + session.getBasicRemote().sendBinary(msg); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + } + + + @ServerEndpoint("/echoBasicLimitHigh") + public static class BasicLimitHigh { + + public static final long MAX_SIZE = 32 * 1024; + + @OnMessage(maxMessageSize = MAX_SIZE) + public void echoTextMessage(Session session, String msg) { + try { + session.getBasicRemote().sendText(msg); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + + + @OnMessage(maxMessageSize = MAX_SIZE) + public void echoBinaryMessage(Session session, ByteBuffer msg) { + try { + session.getBasicRemote().sendBinary(msg); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + } + + + @ServerEndpoint("/echoWriterError") + public static class WriterError { + + @OnMessage + public void echoTextMessage(Session session, @SuppressWarnings("unused") String msg) { + try { + session.getBasicRemote().getSendWriter(); + // Simulate an error + throw new RuntimeException(); + } catch (IOException e) { + // Should not happen + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + } + + @ServerEndpoint("/") + public static class RootEcho { + + @OnMessage + public void echoTextMessage(Session session, String msg) { + try { + session.getBasicRemote().sendText(msg); + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + } +} diff --git a/test/org/apache/tomcat/websocket/TesterFirehoseServer.java b/test/org/apache/tomcat/websocket/TesterFirehoseServer.java new file mode 100644 index 0000000..a903966 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterFirehoseServer.java @@ -0,0 +1,194 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.RemoteEndpoint.Basic; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.websocket.server.TesterEndpointConfig; + +/** + * Sends {@link #MESSAGE_COUNT} messages of size {@link #MESSAGE_SIZE} bytes as quickly as possible after the client + * sends its first message. + */ +public class TesterFirehoseServer { + + public static final int MESSAGE_COUNT = 100000; + public static final String MESSAGE; + public static final int MESSAGE_SIZE = 1024; + public static final int WAIT_TIME_MILLIS = 300000; + public static final int SEND_TIME_OUT_MILLIS = 5000; + + public static final String PATH = "/firehose"; + + static { + StringBuilder sb = new StringBuilder(MESSAGE_SIZE); + for (int i = 0; i < MESSAGE_SIZE; i++) { + sb.append('x'); + } + MESSAGE = sb.toString(); + } + + + public static class ConfigInline extends TesterEndpointConfig { + + @Override + protected Class getEndpointClass() { + return EndpointInline.class; + } + } + + + public static class ConfigThread extends TesterEndpointConfig { + + @Override + protected Class getEndpointClass() { + return EndpointThread.class; + } + } + + + public abstract static class Endpoint { + + private static final AtomicInteger openConnectionCount = new AtomicInteger(0); + private static final AtomicInteger errorCount = new AtomicInteger(0); + + private final boolean inline; + + private volatile boolean started = false; + + public static int getOpenConnectionCount() { + return openConnectionCount.intValue(); + } + + public static int getErrorCount() { + return errorCount.intValue(); + } + + public Endpoint(boolean inline) { + this.inline = inline; + } + + @OnOpen + public void onOpen() { + openConnectionCount.incrementAndGet(); + } + + @OnMessage + public void onMessage(Session session, String msg) throws IOException { + + if (started) { + return; + } + synchronized (this) { + if (started) { + return; + } else { + started = true; + } + } + + System.out.println("Received " + msg + ", now sending data"); + + Writer writer = new Writer(session); + + if (inline) { + writer.doRun(); + } else { + Thread t = new Thread(writer); + t.start(); + } + } + + @OnError + public void onError(@SuppressWarnings("unused") Throwable t) { + errorCount.incrementAndGet(); + } + + @OnClose + public void onClose() { + openConnectionCount.decrementAndGet(); + } + } + + + private static class Writer implements Runnable { + + private static final Log log = LogFactory.getLog(Writer.class); + + private final Session session; + + Writer(Session session) { + this.session = session; + } + + @Override + public void run() { + try { + doRun(); + } catch (IOException ioe) { + log.error("Error on non-container thread", ioe); + } + } + + public void doRun() throws IOException { + session.getUserProperties().put(Constants.BLOCKING_SEND_TIMEOUT_PROPERTY, + Long.valueOf(SEND_TIME_OUT_MILLIS)); + + Basic remote = session.getBasicRemote(); + remote.setBatchingAllowed(true); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + remote.sendText(MESSAGE); + if (i % (MESSAGE_COUNT * 0.4) == 0) { + remote.setBatchingAllowed(false); + remote.setBatchingAllowed(true); + } + } + + // Flushing should happen automatically on session close + session.close(); + } + } + + @ServerEndpoint(PATH) + public static class EndpointInline extends Endpoint { + + public EndpointInline() { + super(true); + } + } + + + @ServerEndpoint(PATH) + public static class EndpointThread extends Endpoint { + + public EndpointThread() { + super(false); + } + } +} diff --git a/test/org/apache/tomcat/websocket/TesterMessageCountClient.java b/test/org/apache/tomcat/websocket/TesterMessageCountClient.java new file mode 100644 index 0000000..85a2f77 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterMessageCountClient.java @@ -0,0 +1,239 @@ +/* + * 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.tomcat.websocket; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.CloseReason; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; + +public class TesterMessageCountClient { + + public interface TesterEndpoint { + void setLatch(CountDownLatch latch); + } + + public static class TesterProgrammaticEndpoint extends Endpoint implements TesterEndpoint { + + private CountDownLatch latch = null; + + @Override + public void setLatch(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + clearLatch(); + } + + @Override + public void onError(Session session, Throwable throwable) { + clearLatch(); + } + + private void clearLatch() { + if (latch != null) { + while (latch.getCount() > 0) { + latch.countDown(); + } + } + } + + @Override + public void onOpen(Session session, EndpointConfig config) { + session.getUserProperties().put("endpoint", this); + } + } + + @ClientEndpoint + public static class TesterAnnotatedEndpoint implements TesterEndpoint { + + private CountDownLatch latch = null; + + @Override + public void setLatch(CountDownLatch latch) { + this.latch = latch; + } + + @OnClose + public void onClose() { + clearLatch(); + } + + @OnError + public void onError(@SuppressWarnings("unused") Throwable throwable) { + clearLatch(); + } + + private void clearLatch() { + if (latch != null) { + while (latch.getCount() > 0) { + latch.countDown(); + } + } + } + + @OnOpen + public void onOpen(Session session) { + session.getUserProperties().put("endpoint", this); + } + } + + + public abstract static class BasicHandler implements MessageHandler.Whole { + + private final CountDownLatch latch; + + private final Queue messages = new LinkedBlockingQueue<>(); + + public BasicHandler(CountDownLatch latch) { + this.latch = latch; + } + + public CountDownLatch getLatch() { + return latch; + } + + public Queue getMessages() { + return messages; + } + } + + public static class BasicBinary extends BasicHandler { + + public BasicBinary(CountDownLatch latch) { + super(latch); + } + + @Override + public void onMessage(ByteBuffer message) { + getMessages().add(message); + if (getLatch() != null) { + getLatch().countDown(); + } + } + } + + public static class BasicText extends BasicHandler { + + private final String expected; + + public BasicText(CountDownLatch latch) { + this(latch, null); + } + + public BasicText(CountDownLatch latch, String expected) { + super(latch); + this.expected = expected; + } + + @Override + public void onMessage(String message) { + if (expected == null) { + getMessages().add(message); + } else { + if (!expected.equals(message)) { + throw new IllegalStateException("Expected: [" + expected + "]\r\n" + "Was: [" + message + "]"); + } + } + if (getLatch() != null) { + getLatch().countDown(); + } + } + } + + public static class SleepingText implements MessageHandler.Whole { + + private final int sleep; + + public SleepingText(int sleep) { + this.sleep = sleep; + } + + @Override + public void onMessage(String message) { + try { + Thread.sleep(sleep); + } catch (InterruptedException e) { + // Ignore + } + } + } + + public abstract static class AsyncHandler implements MessageHandler.Partial { + + private final CountDownLatch latch; + + private final List messages = new CopyOnWriteArrayList<>(); + + public AsyncHandler(CountDownLatch latch) { + this.latch = latch; + } + + public CountDownLatch getLatch() { + return latch; + } + + public List getMessages() { + return messages; + } + } + + public static class AsyncBinary extends AsyncHandler { + + public AsyncBinary(CountDownLatch latch) { + super(latch); + } + + @Override + public void onMessage(ByteBuffer message, boolean last) { + getMessages().add(message); + if (last && getLatch() != null) { + getLatch().countDown(); + } + } + } + + public static class AsyncText extends AsyncHandler { + + + public AsyncText(CountDownLatch latch) { + super(latch); + } + + @Override + public void onMessage(String message, boolean last) { + getMessages().add(message); + if (last && getLatch() != null) { + getLatch().countDown(); + } + } + } +} diff --git a/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java b/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java new file mode 100644 index 0000000..4182b5b --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java @@ -0,0 +1,189 @@ +/* + * 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.tomcat.websocket; + +import java.net.URI; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.authenticator.AuthenticatorBase; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + +/* + * Tests WebSocket connections via a forward proxy. + * + * These tests have been successfully used with Apache Web Server (httpd) + * configured with the following: + * + * Listen 8888 + * + * ProxyRequests On + * ProxyVia On + * AllowCONNECT 0-65535 + * + * + * Listen 8889 + * + * ProxyRequests On + * ProxyVia On + * AllowCONNECT 0-65535 + * + * Order deny,allow + * Allow from all + * AuthType Basic + * AuthName "Proxy Password Required" + * AuthUserFile password.file + * Require valid-user + * + * + * + * and + * # htpasswd -c password.file proxy + * New Password: proxy-pass + * + */ +public class TesterWebSocketClientProxy extends WebSocketBaseTest { + + private static final String MESSAGE_STRING = "proxy-test-message"; + + private static final String PROXY_ADDRESS = "192.168.0.200"; + private static final String PROXY_PORT_NO_AUTH = "8888"; + private static final String PROXY_PORT_AUTH = "8889"; + // The IP address of the test instance that is reachable from the proxy + private static final String TOMCAT_ADDRESS = "192.168.0.100"; + + private static final String TOMCAT_USER = "tomcat"; + private static final String TOMCAT_PASSWORD = "tomcat-pass"; + private static final String TOMCAT_ROLE = "tomcat-role"; + + private static final String PROXY_USER = "proxy"; + private static final String PROXY_PASSWORD = "proxy-pass"; + + @Test + public void testConnectToServerViaProxyWithNoAuthentication() throws Exception { + doTestConnectToServerViaProxy(false, false); + } + + + @Test + public void testConnectToServerViaProxyWithServerAuthentication() throws Exception { + doTestConnectToServerViaProxy(true, false); + } + + + @Test + public void testConnectToServerViaProxyWithProxyAuthentication() throws Exception { + doTestConnectToServerViaProxy(false, true); + } + + + @Test + public void testConnectToServerViaProxyWithServerAndProxyAuthentication() throws Exception { + doTestConnectToServerViaProxy(true, true); + } + + + private void doTestConnectToServerViaProxy(boolean serverAuthentication, boolean proxyAuthentication) + throws Exception { + + // Configure the proxy + System.setProperty("http.proxyHost", PROXY_ADDRESS); + if (proxyAuthentication) { + System.setProperty("http.proxyPort", PROXY_PORT_AUTH); + } else { + System.setProperty("http.proxyPort", PROXY_PORT_NO_AUTH); + } + + Tomcat tomcat = getTomcatInstance(); + + // Need to listen on all addresses, not just loop-back + tomcat.getConnector().setProperty("address", "0.0.0.0"); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + if (serverAuthentication) { + // Configure Realm + tomcat.addUser(TOMCAT_USER, TOMCAT_PASSWORD); + tomcat.addRole(TOMCAT_USER, TOMCAT_ROLE); + + // Configure security constraints + SecurityCollection securityCollection = new SecurityCollection(); + securityCollection.addPatternDecoded("/*"); + SecurityConstraint securityConstraint = new SecurityConstraint(); + securityConstraint.addAuthRole(TOMCAT_ROLE); + securityConstraint.addCollection(securityCollection); + ctx.addConstraint(securityConstraint); + + // Configure authenticator + LoginConfig loginConfig = new LoginConfig(); + loginConfig.setAuthMethod(BasicAuthenticator.schemeName); + ctx.setLoginConfig(loginConfig); + AuthenticatorBase basicAuthenticator = new org.apache.catalina.authenticator.BasicAuthenticator(); + ctx.getPipeline().addValve(basicAuthenticator); + } + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + // Configure the client + if (serverAuthentication) { + clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME, TOMCAT_USER); + clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD, TOMCAT_PASSWORD); + } + if (proxyAuthentication) { + clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PROXY_USER_NAME, PROXY_USER); + clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PROXY_PASSWORD, PROXY_PASSWORD); + } + + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("ws://" + TOMCAT_ADDRESS + ":" + getPort() + TesterEchoServer.Config.PATH_ASYNC)); + CountDownLatch latch = new CountDownLatch(1); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText(MESSAGE_STRING); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + + Assert.assertTrue(latchResult); + + Queue messages = handler.getMessages(); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(MESSAGE_STRING, messages.peek()); + } +} diff --git a/test/org/apache/tomcat/websocket/TesterWsClientAutobahn.java b/test/org/apache/tomcat/websocket/TesterWsClientAutobahn.java new file mode 100644 index 0000000..78259eb --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterWsClientAutobahn.java @@ -0,0 +1,201 @@ +/* + * 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.tomcat.websocket; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.Endpoint; +import jakarta.websocket.Extension; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.websocket.pojo.PojoEndpointClient; + +/** + * Runs the Autobahn test suite in client mode for testing the WebSocket client implementation. + */ +public class TesterWsClientAutobahn { + + private static final String HOST = "localhost"; + private static final int PORT = 9001; + private static final String USER_AGENT = "ApacheTomcat8WebSocketClient"; + + + public static void main(String[] args) throws Exception { + + WebSocketContainer wsc = ContainerProvider.getWebSocketContainer(); + + int testCaseCount = getTestCaseCount(wsc); + System.out.println("There are " + testCaseCount + " test cases"); + for (int testCase = 1; testCase <= testCaseCount; testCase++) { + if (testCase % 50 == 0) { + System.out.println(testCase); + } else { + System.out.print('.'); + } + try { + executeTestCase(wsc, testCase); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + t.printStackTrace(); + } + + } + System.out.println("Testing complete"); + updateReports(wsc); + } + + + private static int getTestCaseCount(WebSocketContainer wsc) throws Exception { + + URI uri = new URI("ws://" + HOST + ":" + PORT + "/getCaseCount"); + CaseCountClient caseCountClient = new CaseCountClient(); + wsc.connectToServer(caseCountClient, uri); + return caseCountClient.getCaseCount(); + } + + + private static void executeTestCase(WebSocketContainer wsc, int testCase) throws Exception { + URI uri = new URI("ws://" + HOST + ":" + PORT + "/runCase?case=" + testCase + "&agent=" + USER_AGENT); + TestCaseClient testCaseClient = new TestCaseClient(); + + Extension permessageDeflate = new WsExtension("permessage-deflate"); + // Advertise support for client_max_window_bits + // Client only supports some values so there will be some failures here + // Note Autobahn returns a 400 response if you provide a value for + // client_max_window_bits + permessageDeflate.getParameters().add(new WsExtensionParameter("client_max_window_bits", null)); + List extensions = new ArrayList<>(1); + extensions.add(permessageDeflate); + + Endpoint ep = new PojoEndpointClient(testCaseClient, null, null); + ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create(); + ClientEndpointConfig config = builder.extensions(extensions).build(); + + wsc.connectToServer(ep, config, uri); + testCaseClient.waitForClose(); + } + + + private static void updateReports(WebSocketContainer wsc) throws Exception { + + URI uri = new URI("ws://" + HOST + ":" + PORT + "/updateReports?agent=" + USER_AGENT); + UpdateReportsClient updateReportsClient = new UpdateReportsClient(); + wsc.connectToServer(updateReportsClient, uri); + } + + + @ClientEndpoint + public static class CaseCountClient { + + private final CountDownLatch latch = new CountDownLatch(1); + private volatile int caseCount = 0; + + // Need to wait for message + public int getCaseCount() throws InterruptedException { + latch.await(); + return caseCount; + } + + @OnMessage + public void onMessage(String msg) { + latch.countDown(); + caseCount = Integer.parseInt(msg); + } + + + @OnError + public void onError(Throwable t) { + latch.countDown(); + t.printStackTrace(); + } + } + + + @ClientEndpoint + public static class TestCaseClient { + + private final CountDownLatch latch = new CountDownLatch(1); + + public void waitForClose() throws InterruptedException { + latch.await(); + } + + @OnMessage + public void echoTextMessage(Session session, String msg, boolean last) { + try { + if (session.isOpen()) { + session.getBasicRemote().sendText(msg, last); + } + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + + @OnMessage + public void echoBinaryMessage(Session session, ByteBuffer bb, boolean last) { + try { + if (session.isOpen()) { + session.getBasicRemote().sendBinary(bb, last); + } + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + + @OnClose + public void releaseLatch() { + latch.countDown(); + } + } + + + @ClientEndpoint + public static class UpdateReportsClient { + + private final CountDownLatch latch = new CountDownLatch(1); + + public void waitForClose() throws InterruptedException { + latch.await(); + } + + @OnClose + public void onClose() { + latch.countDown(); + } + } +} diff --git a/test/org/apache/tomcat/websocket/TesterWsWebSocketContainerWithProxy.java b/test/org/apache/tomcat/websocket/TesterWsWebSocketContainerWithProxy.java new file mode 100644 index 0000000..8b6e452 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TesterWsWebSocketContainerWithProxy.java @@ -0,0 +1,53 @@ +/* + * 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.tomcat.websocket; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; + +/* + * Additional infrastructure (a reverse proxy) is required to run this test. + */ +public class TesterWsWebSocketContainerWithProxy extends TestWsWebSocketContainer { + + @BeforeClass + public static void init() { + // Set the system properties for an HTTP proxy on 192.168.0.100:80 + // I used an httpd instance configured as an open forward proxy for this + // Update the IP/hostname as required + System.setProperty("http.proxyHost", "192.168.0.100"); + System.setProperty("http.proxyPort", "80"); + System.setProperty("http.nonProxyHosts", ""); + } + + @Before + public void setPort() { + // With httpd 2.2, AllowCONNECT requires fixed ports. From 2.4, a range + // can be used. + getTomcatInstance().getConnector().setPort(8080); + Assert.assertTrue(getTomcatInstance().getConnector().setProperty("address", "0.0.0.0")); + } + + @Override + protected String getHostName() { + // The IP/hostname where the tests are running. The proxy will connect + // back to this expecting to find the Tomcat instance created by the + // unit test. + return "192.168.0.200"; + } +} diff --git a/test/org/apache/tomcat/websocket/WebSocketBaseTest.java b/test/org/apache/tomcat/websocket/WebSocketBaseTest.java new file mode 100644 index 0000000..5e714ab --- /dev/null +++ b/test/org/apache/tomcat/websocket/WebSocketBaseTest.java @@ -0,0 +1,70 @@ +/* + * 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.tomcat.websocket; + +import org.junit.After; +import org.junit.Assert; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.server.WsContextListener; + +public abstract class WebSocketBaseTest extends TomcatBaseTest { + + protected Tomcat startServer(final Class configClass) throws LifecycleException { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(configClass.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + return tomcat; + } + + + @After + public void checkBackgroundProcessHasStopped() throws Exception { + // Need to stop Tomcat to ensure background processed have been stopped. + getTomcatInstance().stop(); + + // Make sure the background process has stopped. In some test + // environments it will continue to run and break other tests that check + // it has stopped. + int count = 0; + // 5s should be plenty here but Gump can be a lot slower so allow 60s. + while (count < 600) { + if (BackgroundProcessManager.getInstance().getProcessCount() == 0) { + break; + } + Thread.sleep(100); + count++; + } + + try { + Assert.assertEquals(0, BackgroundProcessManager.getInstance().getProcessCount()); + } finally { + // Ensure the next test is not affected + BackgroundProcessManager.getInstance().shutdown(); + } + } +} diff --git a/test/org/apache/tomcat/websocket/WsWebSocketContainerBaseTest.java b/test/org/apache/tomcat/websocket/WsWebSocketContainerBaseTest.java new file mode 100644 index 0000000..c7059d6 --- /dev/null +++ b/test/org/apache/tomcat/websocket/WsWebSocketContainerBaseTest.java @@ -0,0 +1,47 @@ +/* + * 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.tomcat.websocket; + +import java.net.URI; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.Endpoint; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +public class WsWebSocketContainerBaseTest extends WebSocketBaseTest { + + protected static final byte[] MESSAGE_BINARY_4K = new byte[4096]; + + protected static final long TIMEOUT_MS = 5 * 1000; + protected static final long MARGIN = 500; + + + /* + * Make this possible to override so sub-class can more easily test proxy + */ + protected String getHostName() { + return "localhost"; + } + + + protected Session connectToEchoServer(WebSocketContainer wsContainer, Endpoint endpoint, String path) + throws Exception { + return wsContainer.connectToServer(endpoint, ClientEndpointConfig.Builder.create().build(), + new URI("ws://" + getHostName() + ":" + getPort() + path)); + } +} diff --git a/test/org/apache/tomcat/websocket/pojo/TestEncodingDecoding.java b/test/org/apache/tomcat/websocket/pojo/TestEncodingDecoding.java new file mode 100644 index 0000000..c955e91 --- /dev/null +++ b/test/org/apache/tomcat/websocket/pojo/TestEncodingDecoding.java @@ -0,0 +1,789 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import jakarta.servlet.ServletContextEvent; +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DecodeException; +import jakarta.websocket.Decoder; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.EncodeException; +import jakarta.websocket.Encoder; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.Extension; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpoint; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.pojo.TesterUtil.ServerConfigListener; +import org.apache.tomcat.websocket.pojo.TesterUtil.SingletonConfigurator; +import org.apache.tomcat.websocket.server.WsContextListener; + +public class TestEncodingDecoding extends TomcatBaseTest { + + private static final String MESSAGE_ONE = "message-one"; + private static final String MESSAGE_TWO = "message-two"; + private static final String PATH_PROGRAMMATIC_EP = "/echoProgrammaticEP"; + private static final String PATH_ANNOTATED_EP = "/echoAnnotatedEP"; + private static final String PATH_GENERICS_EP = "/echoGenericsEP"; + private static final String PATH_MESSAGES_EP = "/echoMessagesEP"; + private static final String PATH_BATCHED_EP = "/echoBatchedEP"; + + private static final int WAIT_LOOPS = 100; + private static final int WAIT_DELAY = 100; + + + @Test + public void testProgrammaticEndPoints() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ProgrammaticServerEndpointConfig.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Client client = new Client(); + URI uri = new URI("ws://localhost:" + getPort() + PATH_PROGRAMMATIC_EP); + Session session = wsContainer.connectToServer(client, uri); + + MsgString msg1 = new MsgString(); + msg1.setData(MESSAGE_ONE); + session.getBasicRemote().sendObject(msg1); + // Should not take very long + int i = 0; + while (i < WAIT_LOOPS) { + if (MsgStringMessageHandler.received.size() > 0 && client.received.size() > 0) { + break; + } + i++; + Thread.sleep(WAIT_DELAY); + } + + // Check messages were received + Assert.assertEquals(1, MsgStringMessageHandler.received.size()); + Assert.assertEquals(1, client.received.size()); + + // Check correct messages were received + Assert.assertEquals(MESSAGE_ONE, ((MsgString) MsgStringMessageHandler.received.peek()).getData()); + Assert.assertEquals(MESSAGE_ONE, new String(((MsgByte) client.received.peek()).getData())); + session.close(); + } + + + @Test + public void testAnnotatedEndPoints() throws Exception { + // Set up utility classes + Server server = new Server(); + SingletonConfigurator.setInstance(server); + ServerConfigListener.setPojoClazz(Server.class); + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ServerConfigListener.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Client client = new Client(); + URI uri = new URI("ws://localhost:" + getPort() + PATH_ANNOTATED_EP); + Session session = wsContainer.connectToServer(client, uri); + + MsgString msg1 = new MsgString(); + msg1.setData(MESSAGE_ONE); + session.getBasicRemote().sendObject(msg1); + + // Should not take very long + int i = 0; + while (i < WAIT_LOOPS) { + if (server.received.size() > 0 && client.received.size() > 0) { + break; + } + i++; + Thread.sleep(WAIT_DELAY); + } + + // Check messages were received + Assert.assertEquals(1, server.received.size()); + Assert.assertEquals(1, client.received.size()); + + // Check correct messages were received + Assert.assertEquals(MESSAGE_ONE, ((MsgString) server.received.peek()).getData()); + Assert.assertEquals(MESSAGE_ONE, ((MsgString) client.received.peek()).getData()); + session.close(); + + // Should not take very long but some failures have been seen + i = testEvent(MsgStringEncoder.class.getName() + ":init", 0); + i = testEvent(MsgStringDecoder.class.getName() + ":init", i); + i = testEvent(MsgByteEncoder.class.getName() + ":init", i); + i = testEvent(MsgByteDecoder.class.getName() + ":init", i); + i = testEvent(MsgStringEncoder.class.getName() + ":destroy", i); + i = testEvent(MsgStringDecoder.class.getName() + ":destroy", i); + i = testEvent(MsgByteEncoder.class.getName() + ":destroy", i); + i = testEvent(MsgByteDecoder.class.getName() + ":destroy", i); + } + + + @Test + public void testGenericsCoders() throws Exception { + // Set up utility classes + GenericsServer server = new GenericsServer(); + SingletonConfigurator.setInstance(server); + ServerConfigListener.setPojoClazz(GenericsServer.class); + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ServerConfigListener.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + GenericsClient client = new GenericsClient(); + URI uri = new URI("ws://localhost:" + getPort() + PATH_GENERICS_EP); + Session session = wsContainer.connectToServer(client, uri); + + ArrayList list = new ArrayList<>(2); + list.add("str1"); + list.add("str2"); + session.getBasicRemote().sendObject(list); + + // Should not take very long + int i = 0; + while (i < WAIT_LOOPS) { + if (server.received.size() > 0 && client.received.size() > 0) { + break; + } + i++; + Thread.sleep(WAIT_DELAY); + } + + // Check messages were received + Assert.assertEquals(1, server.received.size()); + Assert.assertEquals(server.received.peek().toString(), "[str1, str2]"); + + Assert.assertEquals(1, client.received.size()); + Assert.assertEquals(client.received.peek().toString(), "[str1, str2]"); + + session.close(); + } + + + @Test + public void testMessagesEndPoints() throws Exception { + // Set up utility classes + MessagesServer server = new MessagesServer(); + SingletonConfigurator.setInstance(server); + ServerConfigListener.setPojoClazz(MessagesServer.class); + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ServerConfigListener.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + StringClient client = new StringClient(); + URI uri = new URI("ws://localhost:" + getPort() + PATH_MESSAGES_EP); + Session session = wsContainer.connectToServer(client, uri); + + session.getBasicRemote().sendText(MESSAGE_ONE); + + // Should not take very long + int i = 0; + while (i < WAIT_LOOPS) { + if (server.received.size() > 0 && client.received.size() > 1) { + break; + } + i++; + Thread.sleep(WAIT_DELAY); + } + + // Check messages were received + Assert.assertEquals(1, server.received.size()); + Assert.assertEquals(2, client.received.size()); + + // Check correct messages were received + Assert.assertEquals(MESSAGE_ONE, server.received.peek()); + session.close(); + + Assert.assertNull(server.t); + } + + + @Test + public void testBatchedEndPoints() throws Exception { + // Set up utility classes + BatchedServer server = new BatchedServer(); + SingletonConfigurator.setInstance(server); + ServerConfigListener.setPojoClazz(BatchedServer.class); + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ServerConfigListener.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + StringClient client = new StringClient(); + URI uri = new URI("ws://localhost:" + getPort() + PATH_BATCHED_EP); + Session session = wsContainer.connectToServer(client, uri); + + session.getBasicRemote().sendText(MESSAGE_ONE); + + // Should not take very long + int i = 0; + while (i++ < WAIT_LOOPS) { + if (server.received.size() > 0 && client.received.size() > 1) { + break; + } + i++; + Thread.sleep(WAIT_DELAY); + } + + // Check messages were received + Assert.assertEquals(1, server.received.size()); + Assert.assertEquals(2, client.received.size()); + + // Check correct messages were received + Assert.assertEquals(MESSAGE_ONE, server.received.peek()); + session.close(); + + Assert.assertNull(server.t); + } + + + private int testEvent(String name, int count) throws InterruptedException { + int i = count; + while (i < WAIT_LOOPS * 3) { + if (Server.isLifeCycleEventCalled(name)) { + break; + } + i++; + Thread.sleep(WAIT_DELAY); + } + Assert.assertTrue(Server.isLifeCycleEventCalled(name)); + return i; + } + + + @ClientEndpoint(decoders = ListStringDecoder.class, encoders = ListStringEncoder.class) + public static class GenericsClient { + private final Queue received = new ConcurrentLinkedQueue<>(); + + @OnMessage + public void rx(List in) { + received.add(in); + } + } + + + @ClientEndpoint(decoders = { MsgStringDecoder.class, MsgByteDecoder.class }, encoders = { MsgStringEncoder.class, + MsgByteEncoder.class }) + public static class Client { + + private final Queue received = new ConcurrentLinkedQueue<>(); + + @OnMessage + public void rx(MsgString in) { + received.add(in); + } + + @OnMessage + public void rx(MsgByte in) { + received.add(in); + } + } + + + @ClientEndpoint + public static class StringClient { + + private final Queue received = new ConcurrentLinkedQueue<>(); + + @OnMessage + public void rx(String in) { + received.add(in); + } + + } + + + @ServerEndpoint(value = PATH_GENERICS_EP, decoders = ListStringDecoder.class, encoders = ListStringEncoder.class, configurator = SingletonConfigurator.class) + public static class GenericsServer { + + private final Queue received = new ConcurrentLinkedQueue<>(); + + @OnMessage + public List rx(List in) { + received.add(in); + // Echo the message back + return in; + } + } + + + @ServerEndpoint(value = PATH_MESSAGES_EP, configurator = SingletonConfigurator.class) + public static class MessagesServer { + + private final Queue received = new ConcurrentLinkedQueue<>(); + private volatile Throwable t = null; + + @OnMessage + public String onMessage(String message, Session session) throws Exception { + received.add(message); + session.getBasicRemote().sendText(MESSAGE_ONE); + return message; + } + + @OnError + public void onError(@SuppressWarnings("unused") Session session, Throwable t) { + t.printStackTrace(); + this.t = t; + } + } + + + @ServerEndpoint(value = PATH_BATCHED_EP, configurator = SingletonConfigurator.class) + public static class BatchedServer { + + private final Queue received = new ConcurrentLinkedQueue<>(); + private volatile Throwable t = null; + + @OnMessage + public String onMessage(String message, Session session) throws IOException { + received.add(message); + session.getBasicRemote().setBatchingAllowed(true); + session.getBasicRemote().sendText(MESSAGE_ONE); + session.getBasicRemote().setBatchingAllowed(false); + return MESSAGE_TWO; + } + + @OnError + public void onError(@SuppressWarnings("unused") Session session, Throwable t) { + t.printStackTrace(); + this.t = t; + } + } + + @ServerEndpoint(value = PATH_ANNOTATED_EP, decoders = { MsgStringDecoder.class, MsgByteDecoder.class }, encoders = { + MsgStringEncoder.class, MsgByteEncoder.class }, configurator = SingletonConfigurator.class) + public static class Server { + + private final Queue received = new ConcurrentLinkedQueue<>(); + static final Map lifeCyclesCalled = new ConcurrentHashMap<>(8); + + @OnMessage + public MsgString rx(MsgString in) { + received.add(in); + // Echo the message back + return in; + } + + @OnMessage + public MsgByte rx(MsgByte in) { + received.add(in); + // Echo the message back + return in; + } + + public static void addLifeCycleEvent(String event) { + lifeCyclesCalled.put(event, Boolean.TRUE); + } + + public static boolean isLifeCycleEventCalled(String event) { + Boolean called = lifeCyclesCalled.get(event); + return called == null ? false : called.booleanValue(); + } + } + + + public static class MsgByteMessageHandler implements MessageHandler.Whole { + + public static final Queue received = new ConcurrentLinkedQueue<>(); + private final Session session; + + public MsgByteMessageHandler(Session session) { + this.session = session; + } + + @Override + public void onMessage(MsgByte in) { + System.out.println(getClass() + " received"); + received.add(in); + try { + MsgByte msg = new MsgByte(); + msg.setData("got it".getBytes()); + session.getBasicRemote().sendObject(msg); + } catch (IOException | EncodeException e) { + throw new IllegalStateException(e); + } + } + } + + + public static class MsgStringMessageHandler implements MessageHandler.Whole { + + public static final Queue received = new ConcurrentLinkedQueue<>(); + private final Session session; + + public MsgStringMessageHandler(Session session) { + this.session = session; + } + + @Override + public void onMessage(MsgString in) { + received.add(in); + try { + MsgByte msg = new MsgByte(); + msg.setData(MESSAGE_ONE.getBytes()); + session.getBasicRemote().sendObject(msg); + } catch (IOException | EncodeException e) { + e.printStackTrace(); + } + } + } + + + public static class ProgrammaticEndpoint extends Endpoint { + @Override + public void onOpen(Session session, EndpointConfig config) { + session.addMessageHandler(new MsgStringMessageHandler(session)); + } + } + + + public static class MsgString { + private volatile String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + + public static class MsgStringEncoder implements Encoder.Text { + + @Override + public void init(EndpointConfig endpointConfig) { + Server.addLifeCycleEvent(getClass().getName() + ":init"); + } + + @Override + public void destroy() { + Server.addLifeCycleEvent(getClass().getName() + ":destroy"); + } + + @Override + public String encode(MsgString msg) throws EncodeException { + return "MsgString:" + msg.getData(); + } + } + + + public static class MsgStringDecoder implements Decoder.Text { + + @Override + public void init(EndpointConfig endpointConfig) { + Server.addLifeCycleEvent(getClass().getName() + ":init"); + } + + @Override + public void destroy() { + Server.addLifeCycleEvent(getClass().getName() + ":destroy"); + } + + @Override + public MsgString decode(String s) throws DecodeException { + MsgString result = new MsgString(); + result.setData(s.substring(10)); + return result; + } + + @Override + public boolean willDecode(String s) { + return s.startsWith("MsgString:"); + } + } + + + public static class MsgByte { + private volatile byte[] data; + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + } + + + public static class MsgByteEncoder implements Encoder.Binary { + + @Override + public void init(EndpointConfig endpointConfig) { + Server.addLifeCycleEvent(getClass().getName() + ":init"); + } + + @Override + public void destroy() { + Server.addLifeCycleEvent(getClass().getName() + ":destroy"); + } + + @Override + public ByteBuffer encode(MsgByte msg) throws EncodeException { + byte[] data = msg.getData(); + ByteBuffer reply = ByteBuffer.allocate(2 + data.length); + reply.put((byte) 0x12); + reply.put((byte) 0x34); + reply.put(data); + reply.flip(); + return reply; + } + } + + + public static class MsgByteDecoder implements Decoder.Binary { + + @Override + public void init(EndpointConfig endpointConfig) { + Server.addLifeCycleEvent(getClass().getName() + ":init"); + } + + @Override + public void destroy() { + Server.addLifeCycleEvent(getClass().getName() + ":destroy"); + } + + @Override + public MsgByte decode(ByteBuffer bb) throws DecodeException { + MsgByte result = new MsgByte(); + byte[] data = new byte[bb.limit() - bb.position()]; + bb.get(data); + result.setData(data); + return result; + } + + @Override + public boolean willDecode(ByteBuffer bb) { + bb.mark(); + if (bb.get() == 0x12 && bb.get() == 0x34) { + return true; + } + bb.reset(); + return false; + } + } + + + public static class ListStringEncoder implements Encoder.Text> { + + @Override + public void init(EndpointConfig endpointConfig) { + Server.addLifeCycleEvent(getClass().getName() + ":init"); + } + + @Override + public void destroy() { + Server.addLifeCycleEvent(getClass().getName() + ":destroy"); + } + + @Override + public String encode(List str) throws EncodeException { + StringBuffer sbuf = new StringBuffer(); + sbuf.append('['); + for (String s : str) { + sbuf.append(s).append(','); + } + sbuf.deleteCharAt(sbuf.lastIndexOf(",")).append(']'); + return sbuf.toString(); + } + } + + + public static class ListStringDecoder implements Decoder.Text> { + + @Override + public void init(EndpointConfig endpointConfig) { + Server.addLifeCycleEvent(getClass().getName() + ":init"); + } + + @Override + public void destroy() { + Server.addLifeCycleEvent(getClass().getName() + ":destroy"); + } + + @Override + public List decode(String str) throws DecodeException { + List lst = new ArrayList<>(1); + str = str.substring(1, str.length() - 1); + String[] strings = str.split(","); + lst.addAll(Arrays.asList(strings)); + return lst; + } + + @Override + public boolean willDecode(String str) { + return str.startsWith("[") && str.endsWith("]"); + } + } + + + public static class ProgrammaticServerEndpointConfig extends WsContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + super.contextInitialized(sce); + ServerContainer sc = (ServerContainer) sce.getServletContext().getAttribute( + org.apache.tomcat.websocket.server.Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + try { + sc.addEndpoint(new ServerEndpointConfig() { + @Override + public Map getUserProperties() { + return Collections.emptyMap(); + } + + @Override + public List> getEncoders() { + List> encoders = new ArrayList<>(2); + encoders.add(MsgStringEncoder.class); + encoders.add(MsgByteEncoder.class); + return encoders; + } + + @Override + public List> getDecoders() { + List> decoders = new ArrayList<>(2); + decoders.add(MsgStringDecoder.class); + decoders.add(MsgByteDecoder.class); + return decoders; + } + + @Override + public List getSubprotocols() { + return Collections.emptyList(); + } + + @Override + public String getPath() { + return PATH_PROGRAMMATIC_EP; + } + + @Override + public List getExtensions() { + return Collections.emptyList(); + } + + @Override + public Class getEndpointClass() { + return ProgrammaticEndpoint.class; + } + + @Override + public Configurator getConfigurator() { + return new ServerEndpointConfig.Configurator() { + }; + } + }); + } catch (DeploymentException e) { + throw new IllegalStateException(e); + } + } + } + + + @Test + public void testUnsupportedObject() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ProgrammaticServerEndpointConfig.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Client client = new Client(); + URI uri = new URI("ws://localhost:" + getPort() + PATH_PROGRAMMATIC_EP); + Session session = wsContainer.connectToServer(client, uri); + + // This should fail + Object msg1 = new Object(); + try { + session.getBasicRemote().sendObject(msg1); + Assert.fail("No exception thrown "); + } catch (EncodeException e) { + // Expected + } catch (Throwable t) { + Assert.fail("Wrong exception type"); + } finally { + session.close(); + } + } +} diff --git a/test/org/apache/tomcat/websocket/pojo/TestPojoEndpointBase.java b/test/org/apache/tomcat/websocket/pojo/TestPojoEndpointBase.java new file mode 100644 index 0000000..0eaaacc --- /dev/null +++ b/test/org/apache/tomcat/websocket/pojo/TestPojoEndpointBase.java @@ -0,0 +1,147 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpoint; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.TestUtil; +import org.apache.tomcat.websocket.pojo.TesterUtil.ServerConfigListener; +import org.apache.tomcat.websocket.pojo.TesterUtil.SingletonConfigurator; + +public class TestPojoEndpointBase extends TomcatBaseTest { + + @Test + public void testBug54716() throws Exception { + TestUtil.generateMask(); + // Set up utility classes + Bug54716 server = new Bug54716(); + SingletonConfigurator.setInstance(server); + ServerConfigListener.setPojoClazz(Bug54716.class); + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ServerConfigListener.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + + tomcat.start(); + + Client client = new Client(); + URI uri = new URI("ws://localhost:" + getPort() + "/"); + + wsContainer.connectToServer(client, uri); + + // Server should close the connection after the exception on open. + boolean closed = client.waitForClose(5); + Assert.assertTrue("Server failed to close connection", closed); + } + + + @Test + public void testOnOpenPojoMethod() throws Exception { + // Set up utility classes + OnOpenServerEndpoint server = new OnOpenServerEndpoint(); + SingletonConfigurator.setInstance(server); + ServerConfigListener.setPojoClazz(OnOpenServerEndpoint.class); + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ServerConfigListener.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + + tomcat.start(); + + Client client = new Client(); + URI uri = new URI("ws://localhost:" + getPort() + "/"); + + Session session = wsContainer.connectToServer(client, uri); + + client.waitForClose(5); + Assert.assertTrue(session.isOpen()); + } + + + @ServerEndpoint("/") + public static class OnOpenServerEndpoint { + + @OnOpen + public void onOpen(@SuppressWarnings("unused") Session session, EndpointConfig config) { + if (config == null) { + throw new RuntimeException(); + } + } + + @OnError + public void onError(@SuppressWarnings("unused") Throwable t) { + throw new RuntimeException(); + } + } + + + @ServerEndpoint("/") + public static class Bug54716 { + + @OnOpen + public void onOpen() { + throw new RuntimeException(); + } + } + + + @ClientEndpoint + public static final class Client { + + private final CountDownLatch closeLatch = new CountDownLatch(1); + + @OnClose + public void onClose() { + closeLatch.countDown(); + } + + public boolean waitForClose(int seconds) throws InterruptedException { + return closeLatch.await(seconds, TimeUnit.SECONDS); + } + } +} diff --git a/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java b/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java new file mode 100644 index 0000000..73a47aa --- /dev/null +++ b/test/org/apache/tomcat/websocket/pojo/TestPojoMethodMapping.java @@ -0,0 +1,142 @@ +/* + * 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.tomcat.websocket.pojo; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.pojo.TesterUtil.ServerConfigListener; +import org.apache.tomcat.websocket.pojo.TesterUtil.SimpleClient; +import org.apache.tomcat.websocket.pojo.TesterUtil.SingletonConfigurator; + +public class TestPojoMethodMapping extends TomcatBaseTest { + + private static final String PARAM_ONE = "abcde"; + private static final String PARAM_TWO = "12345"; + private static final String PARAM_THREE = "true"; + + @Test + public void test() throws Exception { + + // Set up utility classes + Server server = new Server(); + SingletonConfigurator.setInstance(server); + ServerConfigListener.setPojoClazz(Server.class); + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(ServerConfigListener.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + + tomcat.start(); + + SimpleClient client = new SimpleClient(); + URI uri = new URI("ws://localhost:" + getPort() + "/" + PARAM_ONE + "/" + PARAM_TWO + "/" + PARAM_THREE); + + Session session = wsContainer.connectToServer(client, uri); + session.getBasicRemote().sendText("NO-OP"); + session.close(); + + // Give server 20s to close. 5s should be plenty but the Gump VM is slow + int count = 0; + while (count < 200) { + if (server.isClosed()) { + break; + } + count++; + Thread.sleep(100); + } + if (count == 50) { + Assert.fail("Server did not process an onClose event within 5 " + + "seconds of the client sending a close message"); + } + + // Check no errors + List errors = server.getErrors(); + for (String error : errors) { + System.err.println(error); + } + Assert.assertEquals("Found errors", 0, errors.size()); + } + + + @ServerEndpoint(value = "/{one}/{two}/{three}", configurator = SingletonConfigurator.class) + public static final class Server { + + private final List errors = new ArrayList<>(); + private volatile boolean closed; + + @OnOpen + public void onOpen(@PathParam("one") String p1, @PathParam("two") int p2, @PathParam("three") boolean p3) { + checkParams("onOpen", p1, p2, p3); + } + + @OnMessage + public void onMessage(@SuppressWarnings("unused") String msg, @PathParam("one") String p1, + @PathParam("two") int p2, @PathParam("three") boolean p3) { + checkParams("onMessage", p1, p2, p3); + } + + @OnClose + public void onClose(@PathParam("one") String p1, @PathParam("two") int p2, @PathParam("three") boolean p3) { + checkParams("onClose", p1, p2, p3); + closed = true; + } + + public List getErrors() { + return errors; + } + + public boolean isClosed() { + return closed; + } + + private void checkParams(String method, String p1, int p2, boolean p3) { + checkParam(method, PARAM_ONE, p1); + checkParam(method, PARAM_TWO, Integer.toString(p2)); + checkParam(method, PARAM_THREE, Boolean.toString(p3)); + } + + private void checkParam(String method, String expected, String actual) { + if (!expected.equals(actual)) { + errors.add("Method [" + method + "]. Expected [" + expected + "] was + [" + actual + "]"); + } + } + } +} diff --git a/test/org/apache/tomcat/websocket/pojo/TesterUtil.java b/test/org/apache/tomcat/websocket/pojo/TesterUtil.java new file mode 100644 index 0000000..ee48faf --- /dev/null +++ b/test/org/apache/tomcat/websocket/pojo/TesterUtil.java @@ -0,0 +1,62 @@ +/* + * 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.tomcat.websocket.pojo; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.server.ServerEndpointConfig.Configurator; + +import org.apache.tomcat.websocket.server.TesterEndpointConfig; + +public class TesterUtil { + + public static class ServerConfigListener extends TesterEndpointConfig { + + private static Class pojoClazz; + + public static void setPojoClazz(Class pojoClazz) { + ServerConfigListener.pojoClazz = pojoClazz; + } + + + @Override + protected Class getEndpointClass() { + return pojoClazz; + } + } + + + public static class SingletonConfigurator extends Configurator { + + private static Object instance; + + public static void setInstance(Object instance) { + SingletonConfigurator.instance = instance; + } + + @Override + public T getEndpointInstance(Class clazz) throws InstantiationException { + @SuppressWarnings("unchecked") + T result = (T) instance; + return result; + } + } + + + @ClientEndpoint + public static final class SimpleClient { + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestAsyncMessagesPerformance.java b/test/org/apache/tomcat/websocket/server/TestAsyncMessagesPerformance.java new file mode 100644 index 0000000..d4c7d3c --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestAsyncMessagesPerformance.java @@ -0,0 +1,165 @@ +/* + * 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.tomcat.websocket.server; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.websocket.TesterAsyncTiming; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; + +/* + * This test is very timing sensitive. Any failures need to be checked to see if the thresholds just need adjusting to + * support a wider range of platforms and/or Java versions or if the failure is an indication of a performance drop in + * the WebSocket implementation. + */ +public class TestAsyncMessagesPerformance extends TomcatBaseTest { + + @Test + public void testAsyncTiming() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterAsyncTiming.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("ws://localhost:" + getPort() + TesterAsyncTiming.Config.PATH)); + + AsyncTimingClientHandler handler = new AsyncTimingClientHandler(); + wsSession.addMessageHandler(ByteBuffer.class, handler); + wsSession.getBasicRemote().sendText("Hello"); + + System.out.println("Sent Hello message, waiting for data"); + handler.waitForLatch(); + Assert.assertFalse(handler.hasFailed()); + } + + private static class AsyncTimingClientHandler implements MessageHandler.Partial { + + private long lastMessage = 0; + private int sequence = 0; + private int count = 0; + private long seqZeroTimingFailureCount = 0; + private long seqOneTimingFailureCount = 0; + private long seqTwoTimingFailureCount = 0; + + private CountDownLatch latch = new CountDownLatch(1); + private volatile boolean fail = false; + + @Override + public void onMessage(ByteBuffer message, boolean last) { + // Expected received data is: + // 1 * 16k message in 2 * 8k chunks + // 1 * 4k message in 1 * 4k chunk + // 50 ms pause + // loop + if (lastMessage == 0) { + // First message. Don't check + sequence++; + lastMessage = System.nanoTime(); + } else { + long newTime = System.nanoTime(); + long diff = newTime - lastMessage; + lastMessage = newTime; + + if (sequence == 0) { + sequence++; + if (message.capacity() != 8192) { + System.out.println( + "SEQ0: Expected size 8192 but was [" + message.capacity() + "], count [" + count + "]"); + fail = true; + } + if (diff < 40000000) { + System.out.println("SEQ0: Expected diff > 40ms but was [" + diff + "], count [" + count + "]"); + seqZeroTimingFailureCount++; + } + } else if (sequence == 1) { + sequence++; + if (message.capacity() != 8192) { + System.out.println( + "SEQ1: Expected size 8192 but was [" + message.capacity() + "], count [" + count + "]"); + fail = true; + } + // Gap between 2* 8k chunks of 16k message expected to be less than 0.5ms + if (diff > 500000) { + System.out.println("SEQ1: Expected diff < 500,000 but was [" + diff + "], count [" + count + "]"); + seqOneTimingFailureCount++; + } + } else if (sequence == 2) { + sequence = 0; + if (message.capacity() != 4096) { + System.out.println( + "SEQ2: Expected size 4096 but was [" + message.capacity() + "], count [" + count + "]"); + fail = true; + } + // Gap between 16k message and 4k message expected to be less than 0.5ms + if (diff > 500000) { + System.out.println("SEQ2: Expected diff < 500,000 but was [" + diff + "], count [" + count + "]"); + seqTwoTimingFailureCount++; + } + } + } + + count++; + if (count >= TesterAsyncTiming.Config.ITERATIONS * 3) { + latch.countDown(); + } + } + + public void waitForLatch() throws InterruptedException { + latch.await(); + } + + public boolean hasFailed() { + // Total iterations are 1500 + if (!fail) { + if (seqZeroTimingFailureCount > 1) { + // The 50ms pause after the short message may very rarely appear to be less than 40ms + fail = true; + } else if (seqOneTimingFailureCount > 10) { + // The two chunks of the 16k message may rarely be more than 0.5ms apart + fail = true; + } else if (seqTwoTimingFailureCount > 100) { + // The short message may often be more than 0.5ms after the long message + fail = true; + } + } + return fail; + } + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestClassLoader.java b/test/org/apache/tomcat/websocket/server/TestClassLoader.java new file mode 100644 index 0000000..12037a6 --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestClassLoader.java @@ -0,0 +1,150 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpoint; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.loader.WebappClassLoaderBase; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.WebSocketBaseTest; + +/** + * Tests endpoint methods are called with the correct class loader. + */ +public class TestClassLoader extends WebSocketBaseTest { + + private static final String PASS = "PASS"; + private static final String FAIL = "FAIL"; + + + /* + * Checks class loader for the server endpoint during onOpen and onMessage + */ + @Test + public void testSimple() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(Config.class.getName()); + + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + Client client = new Client(); + + Session wsSession = wsContainer.connectToServer(client, new URI("ws://localhost:" + getPort() + "/test")); + + Assert.assertTrue(wsSession.isOpen()); + + // Wait up to 5s for a message + int count = 0; + while (count < 50 && client.getMsgCount() < 1) { + Thread.sleep(100); + } + + // Check it + Assert.assertEquals(1, client.getMsgCount()); + Assert.assertFalse(client.hasFailed()); + + wsSession.getBasicRemote().sendText("Testing"); + + // Wait up to 5s for a message + count = 0; + while (count < 50 && client.getMsgCount() < 2) { + Thread.sleep(100); + } + + Assert.assertEquals(2, client.getMsgCount()); + Assert.assertFalse(client.hasFailed()); + + wsSession.close(); + } + + @ClientEndpoint + public static class Client { + + private final AtomicInteger msgCount = new AtomicInteger(0); + private boolean failed = false; + + public boolean hasFailed() { + return failed; + } + + public int getMsgCount() { + return msgCount.get(); + } + + @OnMessage + public void onMessage(String msg) { + if (!failed && !PASS.equals(msg)) { + failed = true; + } + msgCount.incrementAndGet(); + } + } + + + @ServerEndpoint("/test") + public static class ClassLoaderEndpoint { + + @OnOpen + public void onOpen(Session session) throws IOException { + if (Thread.currentThread().getContextClassLoader() instanceof WebappClassLoaderBase) { + session.getBasicRemote().sendText(PASS); + } else { + session.getBasicRemote().sendText(FAIL); + } + } + + @OnMessage + public String onMessage(@SuppressWarnings("unused") String msg) { + if (Thread.currentThread().getContextClassLoader() instanceof WebappClassLoaderBase) { + return PASS; + } else { + return FAIL; + } + } + } + + public static class Config extends TesterEndpointConfig { + + @Override + protected Class getEndpointClass() { + return ClassLoaderEndpoint.class; + } + + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestClose.java b/test/org/apache/tomcat/websocket/server/TestClose.java new file mode 100644 index 0000000..618a602 --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestClose.java @@ -0,0 +1,328 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCode; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.websocket.WebSocketBaseTest; + +/** + * Test the behavior of closing websockets under various conditions. + */ +public class TestClose extends WebSocketBaseTest { + + private static Log log = LogFactory.getLog(TestClose.class); + + // These are static because it is simpler than trying to inject them into + // the endpoint + private static volatile Events events; + + + public static class Events { + // Used to block in the @OnMessage + public final CountDownLatch onMessageWait = new CountDownLatch(1); + + // Used to check which methods of a server endpoint were called + public final CountDownLatch onErrorCalled = new CountDownLatch(1); + public final CountDownLatch onMessageCalled = new CountDownLatch(1); + public final CountDownLatch onCloseCalled = new CountDownLatch(1); + + // Parameter of an @OnClose call + public volatile CloseReason closeReason = null; + // Parameter of an @OnError call + public volatile Throwable onErrorThrowable = null; + + // This is set to true for tests where the @OnMessage should send a message + public volatile boolean onMessageSends = false; + } + + + private static void awaitLatch(CountDownLatch latch, String failMessage) { + try { + if (!latch.await(5000, TimeUnit.MILLISECONDS)) { + Assert.fail(failMessage); + } + } catch (InterruptedException e) { + // Won't happen + throw new RuntimeException(e); + } + } + + + public static void awaitOnClose(CloseCode... codes) { + Set set = new HashSet<>(Arrays.asList(codes)); + awaitOnClose(set); + } + + + public static void awaitOnClose(Set codes) { + awaitLatch(events.onCloseCalled, "onClose not called"); + CloseCode received = events.closeReason.getCloseCode(); + Assert.assertTrue("Rx: " + received, codes.contains(received)); + } + + + public static void awaitOnError(Class exceptionClazz) { + awaitLatch(events.onErrorCalled, "onError not called"); + Assert.assertTrue(events.onErrorThrowable.getClass().getName(), + exceptionClazz.isAssignableFrom(events.onErrorThrowable.getClass())); + } + + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + events = new Events(); + } + + + @Test + public void testTcpClose() throws Exception { + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.closeSocket(); + + awaitOnClose(CloseCodes.CLOSED_ABNORMALLY); + } + + + @Test + public void testTcpReset() throws Exception { + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.forceCloseSocket(); + + // TODO: I'm not entirely sure when onError should be called + awaitOnError(IOException.class); + awaitOnClose(CloseCodes.CLOSED_ABNORMALLY); + } + + + @Test + public void testWsCloseThenTcpClose() throws Exception { + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.sendCloseFrame(CloseCodes.GOING_AWAY); + client.closeSocket(); + + awaitOnClose(CloseCodes.GOING_AWAY); + } + + + @Test + public void testWsCloseThenTcpReset() throws Exception { + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.sendCloseFrame(CloseCodes.GOING_AWAY); + client.forceCloseSocket(); + + // WebSocket 1.1, section 2.1.5 requires this to be CLOSED_ABNORMALLY if + // the container initiates the close and the close code from the client + // if the client initiates it. When the client resets the TCP connection + // after sending the close, different operating systems react different + // ways. Some present the close message then drop the connection, some + // just drop the connection. Therefore, this test has to handle both + // close codes. + awaitOnClose(CloseCodes.CLOSED_ABNORMALLY, CloseCodes.GOING_AWAY); + } + + + @Test + public void testTcpCloseInOnMessage() throws Exception { + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.sendTextMessage("Test"); + awaitLatch(events.onMessageCalled, "onMessage not called"); + + client.closeSocket(); + events.onMessageWait.countDown(); + + awaitOnClose(CloseCodes.CLOSED_ABNORMALLY); + } + + + @Test + public void testTcpResetInOnMessage() throws Exception { + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.sendTextMessage("Test"); + awaitLatch(events.onMessageCalled, "onMessage not called"); + + client.forceCloseSocket(); + events.onMessageWait.countDown(); + + awaitOnError(IOException.class); + awaitOnClose(CloseCodes.CLOSED_ABNORMALLY); + } + + + @Test + public void testTcpCloseWhenOnMessageSends() throws Exception { + events.onMessageSends = true; + testTcpCloseInOnMessage(); + } + + + @Test + public void testTcpResetWhenOnMessageSends() throws Exception { + events.onMessageSends = true; + testTcpResetInOnMessage(); + } + + + @Test + public void testWsCloseThenTcpCloseWhenOnMessageSends() throws Exception { + events.onMessageSends = true; + + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.sendTextMessage("Test"); + awaitLatch(events.onMessageCalled, "onMessage not called"); + + client.sendCloseFrame(CloseCodes.NORMAL_CLOSURE); + client.closeSocket(); + events.onMessageWait.countDown(); + + awaitOnClose(CloseCodes.CLOSED_ABNORMALLY); + } + + + @Test + public void testWsCloseThenTcpResetWhenOnMessageSends() throws Exception { + events.onMessageSends = true; + + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.sendTextMessage("Test"); + awaitLatch(events.onMessageCalled, "onMessage not called"); + + client.sendCloseFrame(CloseCodes.NORMAL_CLOSURE); + client.forceCloseSocket(); + events.onMessageWait.countDown(); + + awaitOnClose(CloseCodes.CLOSED_ABNORMALLY); + } + + + public static class TestEndpoint { + + @OnOpen + public void onOpen() { + log.info("Session opened"); + } + + @OnMessage + public void onMessage(Session session, String message) { + log.info("Message received: " + message); + events.onMessageCalled.countDown(); + awaitLatch(events.onMessageWait, "onMessageWait not triggered"); + + if (events.onMessageSends) { + try { + int count = 0; + // The latches above are meant to ensure the correct + // sequence of events but in some cases there is a short + // delay between the client closing / resetting the + // connection and the server recognising that fact. This + // loop tries to ensure that it lasts much longer than that + // delay so any close / reset from the client triggers an + // error here. + while (count < 10) { + count++; + session.getBasicRemote().sendText("Test reply"); + Thread.sleep(500); + } + } catch (IOException | InterruptedException e) { + // Expected to fail + } + } + } + + @OnError + public void onError(Throwable t) { + log.info("onError", t); + events.onErrorThrowable = t; + events.onErrorCalled.countDown(); + } + + @OnClose + public void onClose(CloseReason cr) { + log.info("onClose: " + cr); + events.closeReason = cr; + events.onCloseCalled.countDown(); + } + } + + + public static class TestEndpointConfig extends BaseEndpointConfig { + + @Override + protected Class getEndpointClass() { + return TestEndpoint.class; + } + + } + + + public abstract static class BaseEndpointConfig extends TesterEndpointConfig { + + public static final String PATH = "/test"; + + @Override + protected ServerEndpointConfig getServerEndpointConfig() { + return ServerEndpointConfig.Builder.create(getEndpointClass(), PATH).build(); + } + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestCloseBug58624.java b/test/org/apache/tomcat/websocket/server/TestCloseBug58624.java new file mode 100644 index 0000000..f03d9f8 --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestCloseBug58624.java @@ -0,0 +1,167 @@ +/* + * 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.tomcat.websocket.server; + +import java.net.URI; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.ServletContextEvent; +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.CloseReason; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.WebSocketBaseTest; + +public class TestCloseBug58624 extends WebSocketBaseTest { + + @Test + public void testOnErrorNotCalledWhenClosingConnection() throws Throwable { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(Bug58624ServerConfig.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Bug58624ClientEndpoint client = new Bug58624ClientEndpoint(); + URI uri = new URI("ws://localhost:" + getPort() + Bug58624ServerConfig.PATH); + + Session session = wsContainer.connectToServer(client, uri); + + // Wait for session to open on the server + int count = 0; + while (count < 50 && Bug58624ServerEndpoint.getOpenSessionCount() == 0) { + count++; + Thread.sleep(100); + } + Assert.assertNotEquals(0, Bug58624ServerEndpoint.getOpenSessionCount()); + + // Now close the session + session.close(); + + // Wait for session to close on the server + count = 0; + while (count < 50 && Bug58624ServerEndpoint.getOpenSessionCount() > 0) { + count++; + Thread.sleep(100); + } + Assert.assertEquals(0, Bug58624ServerEndpoint.getOpenSessionCount()); + + // Ensure no errors were reported on the server + Assert.assertEquals(0, Bug58624ServerEndpoint.getErrorCount()); + + if (client.getError() != null) { + throw client.getError(); + } + } + + @ClientEndpoint + public static class Bug58624ClientEndpoint { + + private volatile Throwable t; + + + @OnError + public void onError(Throwable t) { + this.t = t; + } + + + public Throwable getError() { + return this.t; + } + } + + public static class Bug58624ServerConfig extends WsContextListener { + + public static final String PATH = "/bug58624"; + + + @Override + public void contextInitialized(ServletContextEvent sce) { + super.contextInitialized(sce); + + ServerContainer sc = (ServerContainer) sce.getServletContext() + .getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + + ServerEndpointConfig sec = ServerEndpointConfig.Builder.create(Bug58624ServerEndpoint.class, PATH).build(); + + try { + sc.addEndpoint(sec); + } catch (DeploymentException e) { + throw new RuntimeException(e); + } + } + } + + public static class Bug58624ServerEndpoint { + + private static AtomicInteger openSessionCount = new AtomicInteger(0); + private static AtomicInteger errorCount = new AtomicInteger(0); + + public static int getOpenSessionCount() { + return openSessionCount.get(); + } + + public static int getErrorCount() { + return errorCount.get(); + } + + @OnOpen + public void onOpen() { + openSessionCount.incrementAndGet(); + } + + + @OnMessage + public void onMessage(@SuppressWarnings("unused") Session session, String message) { + System.out.println("Received message " + message); + } + + + @OnError + public void onError(Throwable t) { + errorCount.incrementAndGet(); + t.printStackTrace(); + } + + + @OnClose + public void onClose(@SuppressWarnings("unused") CloseReason cr) { + openSessionCount.decrementAndGet(); + } + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestKeyHeader.java b/test/org/apache/tomcat/websocket/server/TestKeyHeader.java new file mode 100644 index 0000000..e731344 --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestKeyHeader.java @@ -0,0 +1,92 @@ +/* + * 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.tomcat.websocket.server; + +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.websocket.CloseReason.CloseCodes; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.websocket.TesterEchoServer; +import org.apache.tomcat.websocket.WebSocketBaseTest; + +public class TestKeyHeader extends WebSocketBaseTest { + + @Test + public void testEmptyString() throws Exception { + doTest("", HttpServletResponse.SC_BAD_REQUEST); + } + + + @Test + public void testValid() throws Exception { + // "0123456789012345" encoded with base64 + doTest("MDEyMzQ1Njc4OTAxMjM0NQ==", HttpServletResponse.SC_SWITCHING_PROTOCOLS); + } + + + @Test + public void testInvalidCharacter() throws Exception { + // "0123456789012345" encoded with base64 + doTest("MDEy(zQ1Njc4OTAxMjM0NQ==", HttpServletResponse.SC_BAD_REQUEST); + } + + + @Test + public void testTooShort() throws Exception { + // "012345678901234" encoded with base64 + doTest("MDEyMzQ1Njc4OTAxMjM0", HttpServletResponse.SC_BAD_REQUEST); + } + + + @Test + public void testTooLong01() throws Exception { + // "01234567890123456" encoded with base64 + doTest("MDEyMzQ1Njc4OTAxMjM0NTY=", HttpServletResponse.SC_BAD_REQUEST); + } + + + @Test + public void testTooLong02() throws Exception { + // "012345678901234678" encoded with base64 + doTest("MDEyMzQ1Njc4OTAxMjM0NTY3OA==", HttpServletResponse.SC_BAD_REQUEST); + } + + private void doTest(String keyHeaderValue, int expectedStatusCode) throws Exception { + startServer(TesterEchoServer.Config.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort(), keyHeaderValue); + String req = client.createUpgradeRequest(TesterEchoServer.Config.PATH_BASIC); + client.write(req.getBytes(StandardCharsets.UTF_8)); + int rc = client.readUpgradeResponse(); + + Assert.assertEquals(expectedStatusCode, rc); + + if (expectedStatusCode == HttpServletResponse.SC_SWITCHING_PROTOCOLS) { + client.sendCloseFrame(CloseCodes.NORMAL_CLOSURE); + // Read (and ignore) the response + byte[] buf = new byte[256]; + while (client.read(buf) > 0) { + // Ignore + } + } + client.closeSocket(); + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestShutdown.java b/test/org/apache/tomcat/websocket/server/TestShutdown.java new file mode 100644 index 0000000..5851c4b --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestShutdown.java @@ -0,0 +1,107 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpoint; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; +import org.apache.tomcat.websocket.WebSocketBaseTest; + +/** + * Tests inspired by https://bz.apache.org/bugzilla/show_bug.cgi?id=58835 to check that WebSocket connections are closed + * gracefully on Tomcat shutdown. + */ +public class TestShutdown extends WebSocketBaseTest { + + @Test + public void testShutdownBufferedMessages() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(EchoBufferedConfig.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, clientEndpointConfig, + new URI("ws://localhost:" + getPort() + "/test")); + CountDownLatch latch = new CountDownLatch(1); + BasicText handler = new BasicText(latch); + wsSession.addMessageHandler(handler); + wsSession.getBasicRemote().sendText("Hello"); + + int count = 0; + while (count < 10 && EchoBufferedEndpoint.messageCount.get() == 0) { + Thread.sleep(200); + count++; + } + Assert.assertNotEquals("Message not received by server", EchoBufferedEndpoint.messageCount.get(), 0); + + tomcat.stop(); + + Assert.assertTrue("Latch expired waiting for message", latch.await(10, TimeUnit.SECONDS)); + } + + public static class EchoBufferedConfig extends TesterEndpointConfig { + + @Override + protected Class getEndpointClass() { + return EchoBufferedEndpoint.class; + } + + } + + @ServerEndpoint("/test") + public static class EchoBufferedEndpoint { + + private static AtomicLong messageCount = new AtomicLong(0); + + @OnOpen + public void onOpen(Session session, @SuppressWarnings("unused") EndpointConfig epc) throws IOException { + session.getAsyncRemote().setBatchingAllowed(true); + } + + @OnMessage + public void onMessage(Session session, String msg) throws IOException { + messageCount.incrementAndGet(); + session.getBasicRemote().sendText(msg); + } + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestSlowClient.java b/test/org/apache/tomcat/websocket/server/TestSlowClient.java new file mode 100644 index 0000000..411c395 --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestSlowClient.java @@ -0,0 +1,98 @@ +/* + * 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.tomcat.websocket.server; + +import java.net.URI; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.AbstractProtocol; +import org.apache.tomcat.websocket.TesterFirehoseServer; +import org.apache.tomcat.websocket.TesterMessageCountClient.TesterProgrammaticEndpoint; +import org.apache.tomcat.websocket.WebSocketBaseTest; + +public class TestSlowClient extends WebSocketBaseTest { + + @Test + public void testSendingFromAppThread() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context ctx = getProgrammaticRootContext(); + // Server side endpoint that sends a stream of messages on a new thread + // in response to any message received. + ctx.addApplicationListener(TesterFirehoseServer.ConfigThread.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + // WebSocket client + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + Session wsSession = wsContainer.connectToServer(TesterProgrammaticEndpoint.class, + ClientEndpointConfig.Builder.create().build(), + new URI("ws://localhost:" + getPort() + TesterFirehoseServer.PATH)); + // Configure a handler designed to create a backlog causing the server + // side write to time out. + wsSession.addMessageHandler(new VerySlowHandler()); + + // Trigger the sending of the messages from the server + wsSession.getBasicRemote().sendText("start"); + + // Wait for server to close connection (it shouldn't) + // 20s should be long enough even for the slowest CI system. May need to + // extend this if not. + int count = 0; + while (wsSession.isOpen() && count < 200) { + Thread.sleep(100); + count++; + } + Assert.assertTrue(wsSession.isOpen()); + wsSession.close(); + + // BZ 64848 (non-container thread variant) + // Confirm there are no waiting processors + AbstractProtocol protocol = (AbstractProtocol) tomcat.getConnector().getProtocolHandler(); + count = 0; + while (protocol.getWaitingProcessorCount() > 0 && count < 200) { + Thread.sleep(100); + count++; + } + Assert.assertEquals(0, protocol.getWaitingProcessorCount()); + } + + + public static class VerySlowHandler implements MessageHandler.Whole { + + @Override + public void onMessage(String message) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + // Ignore + } + } + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestUriTemplate.java b/test/org/apache/tomcat/websocket/server/TestUriTemplate.java new file mode 100644 index 0000000..b2071be --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestUriTemplate.java @@ -0,0 +1,234 @@ +/* + * 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.tomcat.websocket.server; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class TestUriTemplate { + + @Test + public void testBasic() throws Exception { + UriTemplate t = new UriTemplate("/{a}/{b}"); + Map result = t.match(new UriTemplate("/foo/bar")); + + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.containsKey("a")); + Assert.assertTrue(result.containsKey("b")); + Assert.assertEquals("foo", result.get("a")); + Assert.assertEquals("bar", result.get("b")); + } + + + @Test + public void testOneOfTwo() throws Exception { + UriTemplate t = new UriTemplate("/{a}/{b}"); + Map result = t.match(new UriTemplate("/foo")); + Assert.assertNull(result); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testBasicPrefix() throws Exception { + @SuppressWarnings("unused") + UriTemplate t = new UriTemplate("/x{a}/y{b}"); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testPrefixOneOfTwo() throws Exception { + UriTemplate t = new UriTemplate("/x{a}/y{b}"); + t.match(new UriTemplate("/xfoo")); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testPrefixTwoOfTwo() throws Exception { + UriTemplate t = new UriTemplate("/x{a}/y{b}"); + t.match(new UriTemplate("/ybar")); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testQuote1() throws Exception { + UriTemplate t = new UriTemplate("/.{a}"); + t.match(new UriTemplate("/yfoo")); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testQuote2() throws Exception { + @SuppressWarnings("unused") + UriTemplate t = new UriTemplate("/.{a}"); + } + + + @Test + public void testNoParams() throws Exception { + UriTemplate t = new UriTemplate("/foo/bar"); + Map result = t.match(new UriTemplate("/foo/bar")); + + Assert.assertEquals(0, result.size()); + } + + + @Test + public void testSpecExample1_01() throws Exception { + UriTemplate t = new UriTemplate("/a/b"); + Map result = t.match(new UriTemplate("/a/b")); + + Assert.assertEquals(0, result.size()); + } + + + @Test + public void testSpecExample1_02() throws Exception { + UriTemplate t = new UriTemplate("/a/b"); + Map result = t.match(new UriTemplate("/a")); + + Assert.assertNull(result); + } + + + @Test + public void testSpecExample1_03() throws Exception { + UriTemplate t = new UriTemplate("/a/b"); + Map result = t.match(new UriTemplate("/a/bb")); + + Assert.assertNull(result); + } + + + @Test + public void testSpecExample2_01() throws Exception { + UriTemplate t = new UriTemplate("/a/{var}"); + Map result = t.match(new UriTemplate("/a/b")); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("b", result.get("var")); + } + + + @Test + public void testSpecExample2_02() throws Exception { + UriTemplate t = new UriTemplate("/a/{var}"); + Map result = t.match(new UriTemplate("/a/apple")); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("apple", result.get("var")); + } + + + @Test + public void testSpecExample2_03() throws Exception { + UriTemplate t = new UriTemplate("/a/{var}"); + Map result = t.match(new UriTemplate("/a")); + + Assert.assertNull(result); + } + + + @Test + public void testSpecExample2_04() throws Exception { + UriTemplate t = new UriTemplate("/a/{var}"); + Map result = t.match(new UriTemplate("/a/b/c")); + + Assert.assertNull(result); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testDuplicate01() throws Exception { + @SuppressWarnings("unused") + UriTemplate t = new UriTemplate("/{var}/{var}"); + } + + + @Test + public void testDuplicate02() throws Exception { + UriTemplate t = new UriTemplate("/{a}/{b}"); + Map result = t.match(new UriTemplate("/x/x")); + + Assert.assertEquals(2, result.size()); + Assert.assertEquals("x", result.get("a")); + Assert.assertEquals("x", result.get("b")); + } + + + public void testEgMailingList01() throws Exception { + UriTemplate t = new UriTemplate("/a/{var}"); + Map result = t.match(new UriTemplate("/a/b/")); + + Assert.assertNull(result); + } + + + public void testEgMailingList02() throws Exception { + UriTemplate t = new UriTemplate("/a/{var}"); + Map result = t.match(new UriTemplate("/a/")); + + Assert.assertNull(result); + } + + + @Test + public void testEgMailingList03() throws Exception { + UriTemplate t = new UriTemplate("/a/{var}"); + Map result = t.match(new UriTemplate("/a")); + + Assert.assertNull(result); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testEgMailingList04() throws Exception { + UriTemplate t = new UriTemplate("/a/{var1}/{var2}"); + @SuppressWarnings("unused") + Map result = t.match(new UriTemplate("/a//c")); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testEgMailingList05() throws Exception { + UriTemplate t = new UriTemplate("/a/{var}/"); + @SuppressWarnings("unused") + Map result = t.match(new UriTemplate("/a/b/")); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testSpecIssue194a() throws Exception { + @SuppressWarnings("unused") + UriTemplate t = new UriTemplate("/a/../b"); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testSpecIssue194b() throws Exception { + @SuppressWarnings("unused") + UriTemplate t = new UriTemplate("/./b"); + } + + + @Test(expected = jakarta.websocket.DeploymentException.class) + public void testSpecIssue194c() throws Exception { + @SuppressWarnings("unused") + UriTemplate t = new UriTemplate("//b"); + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestWsRemoteEndpointImplServerDeadlock.java b/test/org/apache/tomcat/websocket/server/TestWsRemoteEndpointImplServerDeadlock.java new file mode 100644 index 0000000..7959daa --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestWsRemoteEndpointImplServerDeadlock.java @@ -0,0 +1,221 @@ +/* + * 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.tomcat.websocket.server; + +import java.lang.reflect.Field; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.WebSocketBaseTest; +import org.apache.tomcat.websocket.WsSession; + +/* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=66508 + * + * If the client sends a close while the server waiting for the client before sending the rest of a message, the + * processing of the close from the client can hang until the sending of the message times out. + * + * This is packaged in a separate class to allow test specific parameterisation. + */ +@RunWith(Parameterized.class) +public class TestWsRemoteEndpointImplServerDeadlock extends WebSocketBaseTest { + + @Parameterized.Parameters(name = "{index}: useAsyncIO[{0}], sendOnContainerThread[{1}]") + public static Collection parameters() { + + List parameterSets = new ArrayList<>(); + + for (Boolean useAsyncIO : booleans) { + for (Boolean sendOnContainerThread : booleans) { + parameterSets.add(new Object[] { useAsyncIO, sendOnContainerThread }); + } + } + + return parameterSets; + } + + @Parameter(0) + public Boolean useAsyncIO; + + @Parameter(1) + public Boolean sendOnContainerThread; + + /* + * Statics used to pass state to instances that are configured and created by class name so there is no easy way to + * configure the created instances directly. + * + * Every component that uses these statics takes a local copy ASAP to avoid issues with previous test runs retaining + * references to the instance stored in the static and interfering with the current test run. + */ + private static volatile boolean initialSendOnContainerThread; + private static volatile CountDownLatch initialServerSendLatch; + private static volatile CountDownLatch initialClientReceiveLatch; + + @Test + public void testTemporaryDeadlockOnClientClose() throws Exception { + // Configure the statics + initialSendOnContainerThread = sendOnContainerThread.booleanValue(); + initialServerSendLatch = new CountDownLatch(1); + initialClientReceiveLatch = new CountDownLatch(1); + + // Local copies of the statics used in this method + CountDownLatch serverSendLatch = initialServerSendLatch; + CountDownLatch clientReceiveLatch = initialClientReceiveLatch; + + Tomcat tomcat = getTomcatInstance(); + Assert.assertTrue(tomcat.getConnector().setProperty("useAsyncIO", useAsyncIO.toString())); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(Bug66508Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Bug66508Client client = new Bug66508Client(); + URI uri = new URI("ws://localhost:" + getPort() + Bug66508Config.PATH); + + Session session = wsContainer.connectToServer(client, uri); + // Server starts to send messages. + // Wait for server sending to block. + serverSendLatch.await(); + // Server buffers are full. Server cannot send any more messages. + // Server is now blocked waiting for the client to read the messages. + + // Close the session from the client + session.close(); + + // Wait for server to complete sending the close message + // This is the process that deadlocks when the bug is experienced + Field f = WsSession.class.getDeclaredField("state"); + f.setAccessible(true); + Object state = f.get(Bug66508Endpoint.serverSession); + int count = 0; + long start = System.nanoTime(); + // Send times out after 20s so test should complete in less than that. Allow large margin as VMs can sometimes + // be slow when running tests. + while (!"CLOSED".equals(state.toString()) && count < 190) { + count++; + Thread.sleep(100); + state = f.get(Bug66508Endpoint.serverSession); + if (count == 10) { + // If deadlock is present, this should be long enough to trigger it. + // Release the client latch so it starts processing messages again else the server will never be able to + // send the close message. + clientReceiveLatch.countDown(); + } + } + long closeDelay = System.nanoTime() - start; + + Assert.assertTrue("Close delay was [" + closeDelay + "] ns", closeDelay < 10_000_000_000L); + + } + + public static class Bug66508Config extends TesterEndpointConfig { + + public static final String PATH = "/bug66508"; + + + @Override + protected ServerEndpointConfig getServerEndpointConfig() { + return ServerEndpointConfig.Builder.create(Bug66508Endpoint.class, PATH).build(); + } + } + + public static class Bug66508Endpoint { + + // 1024k message + private static final String MSG = "a".repeat(1024 * 8); + + private static volatile Session serverSession = null; + private CountDownLatch serverSendLatch = initialServerSendLatch; + private boolean sendOnContainerThread = initialSendOnContainerThread; + + @OnOpen + public void onOpen(Session session) { + serverSession = session; + // Send messages to the client until they appear to hang + // Need to do this on a non-container thread + Runnable r = () -> { + Future sendMessageFuture; + while (true) { + sendMessageFuture = session.getAsyncRemote().sendText(MSG); + try { + sendMessageFuture.get(2, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + break; + } + } + serverSendLatch.countDown(); + }; + if (sendOnContainerThread) { + r.run(); + } else { + new Thread(r).start(); + } + } + + @OnError + public void onError(@SuppressWarnings("unused") Throwable t) { + // Expected. Swallow the error. + } + } + + @ClientEndpoint + public static class Bug66508Client { + + private CountDownLatch clientReceiveLatch = initialClientReceiveLatch; + + @OnMessage + public void onMessage(@SuppressWarnings("unused") String msg) { + try { + // Block client from processing messages + clientReceiveLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} diff --git a/test/org/apache/tomcat/websocket/server/TestWsServerContainer.java b/test/org/apache/tomcat/websocket/server/TestWsServerContainer.java new file mode 100644 index 0000000..8bc0094 --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestWsServerContainer.java @@ -0,0 +1,315 @@ +/* + * 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.tomcat.websocket.server; + +import java.net.URI; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpoint; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.unittest.TesterServletContext; +import org.apache.tomcat.websocket.TesterEchoServer; +import org.apache.tomcat.websocket.TesterMessageCountClient.BasicText; +import org.apache.tomcat.websocket.WebSocketBaseTest; +import org.apache.tomcat.websocket.pojo.TesterUtil.SimpleClient; + + +public class TestWsServerContainer extends WebSocketBaseTest { + + @Test + public void testBug54807() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(Bug54807Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + Assert.assertEquals(LifecycleState.STARTED, ctx.getState()); + } + + + @Test + public void testBug58232() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(Bug54807Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + Assert.assertEquals(LifecycleState.STARTED, ctx.getState()); + + SimpleClient client = new SimpleClient(); + URI uri = new URI("ws://localhost:" + getPort() + "/echoBasic"); + + try (Session session = wsContainer.connectToServer(client, uri)) { + CountDownLatch latch = new CountDownLatch(1); + BasicText handler = new BasicText(latch); + session.addMessageHandler(handler); + session.getBasicRemote().sendText("echoBasic"); + + boolean latchResult = handler.getLatch().await(10, TimeUnit.SECONDS); + Assert.assertTrue(latchResult); + + Queue messages = handler.getMessages(); + Assert.assertEquals(1, messages.size()); + for (String message : messages) { + Assert.assertEquals("echoBasic", message); + } + } + } + + + public static class Bug54807Config extends TesterEndpointConfig { + + @Override + protected ServerEndpointConfig getServerEndpointConfig() { + return ServerEndpointConfig.Builder.create(TesterEchoServer.Basic.class, "/{param}").build(); + } + } + + + @Test + public void testSpecExample3() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Object.class, "/a/{var}/c").build(); + ServerEndpointConfig configB = ServerEndpointConfig.Builder.create(Object.class, "/a/b/c").build(); + ServerEndpointConfig configC = ServerEndpointConfig.Builder.create(Object.class, "/a/{var1}/{var2}").build(); + + sc.addEndpoint(configA); + sc.addEndpoint(configB); + sc.addEndpoint(configC); + + Assert.assertEquals(configB, sc.findMapping("/a/b/c").getConfig()); + Assert.assertEquals(configA, sc.findMapping("/a/d/c").getConfig()); + Assert.assertEquals(configC, sc.findMapping("/a/x/y").getConfig()); + } + + + @Test + public void testSpecExample4() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Object.class, "/{var1}/d").build(); + ServerEndpointConfig configB = ServerEndpointConfig.Builder.create(Object.class, "/b/{var2}").build(); + + sc.addEndpoint(configA); + sc.addEndpoint(configB); + + Assert.assertEquals(configB, sc.findMapping("/b/d").getConfig()); + } + + + @Test(expected = DeploymentException.class) + public void testDuplicatePaths01() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Object.class, "/a/b/c").build(); + ServerEndpointConfig configB = ServerEndpointConfig.Builder.create(Object.class, "/a/b/c").build(); + + sc.addEndpoint(configA); + sc.addEndpoint(configB); + } + + + @Test(expected = DeploymentException.class) + public void testDuplicatePaths02() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Object.class, "/a/b/{var}").build(); + ServerEndpointConfig configB = ServerEndpointConfig.Builder.create(Object.class, "/a/b/{var}").build(); + + sc.addEndpoint(configA); + sc.addEndpoint(configB); + } + + + @Test(expected = DeploymentException.class) + public void testDuplicatePaths03() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Object.class, "/a/b/{var1}").build(); + ServerEndpointConfig configB = ServerEndpointConfig.Builder.create(Object.class, "/a/b/{var2}").build(); + + sc.addEndpoint(configA); + sc.addEndpoint(configB); + } + + + @Test + public void testDuplicatePaths04() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Object.class, "/a/{var1}/{var2}").build(); + ServerEndpointConfig configB = ServerEndpointConfig.Builder.create(Object.class, "/a/b/{var2}").build(); + + sc.addEndpoint(configA); + sc.addEndpoint(configB); + + Assert.assertEquals(configA, sc.findMapping("/a/x/y").getConfig()); + Assert.assertEquals(configB, sc.findMapping("/a/b/y").getConfig()); + } + + + /* + * Simulates a class that gets picked up for extending Endpoint and for being annotated. + */ + @Test(expected = DeploymentException.class) + public void testDuplicatePaths11() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Pojo.class, "/foo").build(); + + sc.addEndpoint(configA, false); + sc.addEndpoint(Pojo.class, true); + } + + + /* + * POJO auto deployment followed by programmatic duplicate. Keep POJO. + */ + @Test + public void testDuplicatePaths12() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Pojo.class, "/foo").build(); + + sc.addEndpoint(Pojo.class, true); + sc.addEndpoint(configA); + + Assert.assertNotEquals(configA, sc.findMapping("/foo").getConfig()); + } + + + /* + * POJO programmatic followed by programmatic duplicate. + */ + @Test(expected = DeploymentException.class) + public void testDuplicatePaths13() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Pojo.class, "/foo").build(); + + sc.addEndpoint(Pojo.class); + sc.addEndpoint(configA); + } + + + /* + * POJO auto deployment followed by programmatic on same path. + */ + @Test(expected = DeploymentException.class) + public void testDuplicatePaths14() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Object.class, "/foo").build(); + + sc.addEndpoint(Pojo.class, true); + sc.addEndpoint(configA); + } + + + /* + * Simulates a class that gets picked up for extending Endpoint and for being annotated. + */ + @Test(expected = DeploymentException.class) + public void testDuplicatePaths21() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(PojoTemplate.class, "/foo/{a}").build(); + + sc.addEndpoint(configA, false); + sc.addEndpoint(PojoTemplate.class, true); + } + + + /* + * POJO auto deployment followed by programmatic duplicate. Keep POJO. + */ + @Test + public void testDuplicatePaths22() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(PojoTemplate.class, "/foo/{a}").build(); + + sc.addEndpoint(PojoTemplate.class, true); + sc.addEndpoint(configA); + + Assert.assertNotEquals(configA, sc.findMapping("/foo/{a}").getConfig()); + } + + + /* + * POJO programmatic followed by programmatic duplicate. + */ + @Test(expected = DeploymentException.class) + public void testDuplicatePaths23() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(PojoTemplate.class, "/foo/{a}").build(); + + sc.addEndpoint(PojoTemplate.class); + sc.addEndpoint(configA); + } + + + /* + * POJO auto deployment followed by programmatic on same path. + */ + @Test(expected = DeploymentException.class) + public void testDuplicatePaths24() throws Exception { + WsServerContainer sc = new WsServerContainer(new TesterServletContext()); + + ServerEndpointConfig configA = ServerEndpointConfig.Builder.create(Object.class, "/foo/{a}").build(); + + sc.addEndpoint(PojoTemplate.class, true); + sc.addEndpoint(configA); + } + + + @ServerEndpoint("/foo") + public static class Pojo { + } + + + @ServerEndpoint("/foo/{a}") + public static class PojoTemplate { + } + +} diff --git a/test/org/apache/tomcat/websocket/server/TesterEndpointConfig.java b/test/org/apache/tomcat/websocket/server/TesterEndpointConfig.java new file mode 100644 index 0000000..ad4e0d4 --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TesterEndpointConfig.java @@ -0,0 +1,54 @@ +/* + * 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.tomcat.websocket.server; + +import jakarta.servlet.ServletContextEvent; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpointConfig; + +public abstract class TesterEndpointConfig extends WsContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + super.contextInitialized(sce); + + ServerContainer sc = (ServerContainer) sce.getServletContext() + .getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE); + + try { + ServerEndpointConfig sec = getServerEndpointConfig(); + if (sec == null) { + sc.addEndpoint(getEndpointClass()); + } else { + sc.addEndpoint(sec); + } + } catch (DeploymentException e) { + throw new RuntimeException(e); + } + } + + + protected Class getEndpointClass() { + return null; + } + + + protected ServerEndpointConfig getServerEndpointConfig() { + return null; + } +} diff --git a/test/org/apache/tomcat/websocket/server/TesterWsClient.java b/test/org/apache/tomcat/websocket/server/TesterWsClient.java new file mode 100644 index 0000000..ed52cee --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TesterWsClient.java @@ -0,0 +1,148 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +import jakarta.websocket.CloseReason.CloseCode; + +/** + * A client for testing Websocket behavior that differs from standard client behavior. + */ +public class TesterWsClient { + + private static final byte[] maskingKey = new byte[] { 0x12, 0x34, 0x56, 0x78 }; + private static final String DEFAULT_KEY_HEADER_VALUE = "OEvAoAKn5jsuqv2/YJ1Wfg=="; + + private final Socket socket; + private final String keyHeaderValue; + + public TesterWsClient(String host, int port) throws Exception { + this(host, port, DEFAULT_KEY_HEADER_VALUE); + } + + public TesterWsClient(String host, int port, String keyHeaderValue) throws Exception { + this.socket = new Socket(host, port); + // Set read timeout in case of failure so test doesn't hang + socket.setSoTimeout(2000); + // Disable Nagle's algorithm to ensure packets sent immediately + // TODO: Hoping this causes writes to wait for a TCP ACK for TCP RST + // test cases but I'm not sure? + socket.setTcpNoDelay(true); + this.keyHeaderValue = keyHeaderValue; + } + + public void httpUpgrade(String path) throws IOException { + String req = createUpgradeRequest(path); + write(req.getBytes(StandardCharsets.UTF_8)); + readUpgradeResponse(); + } + + public void sendTextMessage(String text) throws IOException { + sendTextMessage(text.getBytes(StandardCharsets.UTF_8)); + } + + public void sendTextMessage(byte[] utf8Bytes) throws IOException { + write(createFrame(true, 1, utf8Bytes)); + } + + public void sendCloseFrame(CloseCode closeCode) throws IOException { + int code = closeCode.getCode(); + byte[] codeBytes = new byte[2]; + codeBytes[0] = (byte) (code >> 8); + codeBytes[1] = (byte) code; + write(createFrame(true, 8, codeBytes)); + } + + public int readUpgradeResponse() throws IOException { + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + int result = -1; + String line = in.readLine(); + while (line != null && !line.isEmpty()) { + if (result == -1) { + if (line.length() > 11) { + // First line expected to be "HTTP/1.1 nnn " + result = Integer.parseInt(line.substring(9, 12)); + } else { + // No response code - treat as server error for this test + result = 500; + } + } + line = in.readLine(); + } + return result; + } + + public void closeSocket() throws IOException { + // Enable SO_LINGER to ensure close() only returns when TCP closing + // handshake completes + socket.setSoLinger(true, 65535); + socket.close(); + } + + /* + * Send a TCP RST instead of a TCP closing handshake + */ + public void forceCloseSocket() throws IOException { + // SO_LINGER sends a TCP RST when timeout expires + socket.setSoLinger(true, 0); + socket.close(); + } + + public int read(byte[] bytes) throws IOException { + return socket.getInputStream().read(bytes); + } + + public void write(byte[] bytes) throws IOException { + socket.getOutputStream().write(bytes); + socket.getOutputStream().flush(); + } + + public String createUpgradeRequest(String path) { + String[] upgradeRequestLines = { "GET " + path + " HTTP/1.1", "Connection: Upgrade", "Host: localhost:8080", + "Origin: localhost:8080", "Sec-WebSocket-Key: " + keyHeaderValue, "Sec-WebSocket-Version: 13", + "Upgrade: websocket" }; + StringBuffer sb = new StringBuffer(); + for (String line : upgradeRequestLines) { + sb.append(line); + sb.append("\r\n"); + } + sb.append("\r\n"); + return sb.toString(); + } + + private static byte[] createFrame(boolean fin, int opCode, byte[] payload) { + byte[] frame = new byte[6 + payload.length]; + frame[0] = (byte) (opCode | (fin ? 1 << 7 : 0)); + frame[1] = (byte) (0x80 | payload.length); + + frame[2] = maskingKey[0]; + frame[3] = maskingKey[1]; + frame[4] = maskingKey[2]; + frame[5] = maskingKey[3]; + + for (int i = 0; i < payload.length; i++) { + frame[i + 6] = (byte) (payload[i] ^ maskingKey[i % 4]); + } + + return frame; + } +} diff --git a/test/org/apache/tomcat/websocket/server/TesterWsRemoteEndpointImplServer.java b/test/org/apache/tomcat/websocket/server/TesterWsRemoteEndpointImplServer.java new file mode 100644 index 0000000..8d5b73f --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TesterWsRemoteEndpointImplServer.java @@ -0,0 +1,161 @@ +/* + * 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.tomcat.websocket.server; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.EncodeException; +import jakarta.websocket.Encoder; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; +import jakarta.websocket.server.ServerEndpointConfig; + +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.WebSocketBaseTest; +import org.apache.tomcat.websocket.pojo.TesterUtil.SimpleClient; + +/* + * This test requires manual intervention to create breakpoints etc. + */ +public class TesterWsRemoteEndpointImplServer extends WebSocketBaseTest { + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=58624 + * + * This test requires three breakpoints to be set. Two in this file (marked A & B with comments) and one (C) at the + * start of WsRemoteEndpointImplServer.doWrite(). + * + * With the breakpoints in place, run this test. Once breakpoints A & B are reached, progress the thread at + * breakpoint A one line to close the connection. Once breakpoint C is reached, allow the thread at breakpoint B to + * continue. Then allow the thread at breakpoint C to continue. + * + * In the failure mode, the thread at breakpoint B will not progress past the call to sendObject(). If the issue is + * fixed, the thread at breakpoint B will continue past sendObject() and terminate with a TimeoutException. + */ + @Test + public void testClientDropsConnection() throws Exception { + Tomcat tomcat = getTomcatInstance(); + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(Bug58624Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + tomcat.start(); + + SimpleClient client = new SimpleClient(); + URI uri = new URI("ws://localhost:" + getPort() + Bug58624Config.PATH); + + Session session = wsContainer.connectToServer(client, uri); + // Break point A required on following line + session.close(); + } + + public static class Bug58624Config extends TesterEndpointConfig { + + public static final String PATH = "/bug58624"; + + + @Override + protected ServerEndpointConfig getServerEndpointConfig() { + List> encoders = new ArrayList<>(); + encoders.add(Bug58624Encoder.class); + return ServerEndpointConfig.Builder.create(Bug58624Endpoint.class, PATH).encoders(encoders).build(); + } + } + + public static class Bug58624Endpoint { + + private static final ExecutorService ex = Executors.newFixedThreadPool(1); + + @OnOpen + public void onOpen(Session session) { + // Disabling blocking timeouts for this test + session.getUserProperties().put(org.apache.tomcat.websocket.Constants.BLOCKING_SEND_TIMEOUT_PROPERTY, + Long.valueOf(-1)); + ex.submit(new Bug58624SendMessage(session)); + } + + @OnMessage + public void onMessage(String message) { + System.out.println("OnMessage: " + message); + } + + @OnError + public void onError(Throwable t) { + System.err.println("OnError:"); + t.printStackTrace(); + } + + @OnClose + public void onClose(@SuppressWarnings("unused") Session session, CloseReason cr) { + System.out.println("Closed " + cr); + } + } + + public static class Bug58624SendMessage implements Runnable { + private Session session; + + public Bug58624SendMessage(Session session) { + this.session = session; + } + + @Override + public void run() { + try { + // Breakpoint B required on following line + session.getBasicRemote().sendObject("test"); + } catch (IOException | EncodeException e) { + e.printStackTrace(); + } + } + } + + public static class Bug58624Encoder implements Encoder.Text { + + @Override + public void destroy() { + } + + @Override + public void init(EndpointConfig endpointConfig) { + } + + @Override + public String encode(Object object) throws EncodeException { + return (String) object; + } + } +} diff --git a/test/tld/implicit-bad.tld b/test/tld/implicit-bad.tld new file mode 100644 index 0000000..ddf8708 --- /dev/null +++ b/test/tld/implicit-bad.tld @@ -0,0 +1,30 @@ + + + + 1.0 + Ignored + + Foo + org.apache.tomcat.ignored.Anything.class + empty + + \ No newline at end of file diff --git a/test/tld/implicit-good.tld b/test/tld/implicit-good.tld new file mode 100644 index 0000000..c0616e3 --- /dev/null +++ b/test/tld/implicit-good.tld @@ -0,0 +1,25 @@ + + + + 1.0 + Ignored + \ No newline at end of file diff --git a/test/tld/listener.tld b/test/tld/listener.tld new file mode 100644 index 0000000..457d682 --- /dev/null +++ b/test/tld/listener.tld @@ -0,0 +1,29 @@ + + + 1.0 + listener + http://tomcat.apache.org/listener + + + org.apache.catalina.core.TesterTldListener + + \ No newline at end of file diff --git a/test/tld/tags11.tld b/test/tld/tags11.tld new file mode 100644 index 0000000..3c7ae98 --- /dev/null +++ b/test/tld/tags11.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/tld/tags12.tld b/test/tld/tags12.tld new file mode 100644 index 0000000..533235b --- /dev/null +++ b/test/tld/tags12.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/tld/tags20.tld b/test/tld/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/tld/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/tld/tags21.tld b/test/tld/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/tld/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/tld/test.tld b/test/tld/test.tld new file mode 100644 index 0000000..47c5226 --- /dev/null +++ b/test/tld/test.tld @@ -0,0 +1,93 @@ + + + + 1.0 + test + http://tomcat.apache.org/TldTests + + + com.example.Validator + + name + value + + + + + Echo Tag + Echo + + small + large + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + var + java.lang.Object + yes + AT_END + + + echo + yes + true + + + fragment + yes + + + deferredValue + + java.util.Date + + + + deferredMethod + + java.util.Date getDate() + + + true + + + + Echo Tag + Echo + + small + large + + Echo2 + /echo.tag + + + + trim + org.apache.el.TesterFunctions + + java.lang.String trim(java.lang.String) + + + \ No newline at end of file diff --git a/test/util/TestCookieFilter.java b/test/util/TestCookieFilter.java new file mode 100644 index 0000000..6384d1d --- /dev/null +++ b/test/util/TestCookieFilter.java @@ -0,0 +1,93 @@ +/* + * 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 util; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.authenticator.Constants; + +public class TestCookieFilter { + + @Test + public void test01() { + // Single cookie + Assert.assertEquals("a=b", CookieFilter.filter("a=b", null)); + } + + @Test + public void test02() { + // Two cookies + Assert.assertEquals("a=b;c=d", CookieFilter.filter("a=b;c=d", null)); + } + + @Test + public void test03() { + // Cookies with leading and trailing whitespace + Assert.assertEquals(" a=b ; c=d ", + CookieFilter.filter(" a=b ; c=d ", null)); + } + + @Test + public void test04() { + // Empty name (not necessarily valid but checking edge cases in filter) + Assert.assertEquals("=b", CookieFilter.filter("=b", null)); + } + + @Test + public void test05() { + // Empty value (not necessarily valid but checking edge cases in filter) + Assert.assertEquals("a=", CookieFilter.filter("a=", null)); + } + + @Test + public void test06() { + // Simple case + Assert.assertEquals("JSESSIONID=[obfuscated]", + CookieFilter.filter("JSESSIONID=0123456789", null)); + } + + @Test + public void test07() { + // Simple SSO case + Assert.assertEquals(Constants.SINGLE_SIGN_ON_COOKIE + "=[obfuscated]", + CookieFilter.filter(Constants.SINGLE_SIGN_ON_COOKIE + "=0123456789", null)); + } + + + @Test + public void test08() { + // Simple case + String id = "0123456789"; + String cookie = "JSESSIONID=" + id; + Assert.assertEquals(cookie, CookieFilter.filter(cookie, id)); + } + + @Test + public void test09() { + // Simple SSO case + String id = "0123456789"; + String cookie = Constants.SINGLE_SIGN_ON_COOKIE + "=" + id; + Assert.assertEquals(cookie, CookieFilter.filter(cookie, id)); + } + + @Test + public void test10() { + // Single cookie + Assert.assertEquals("a=\"xx;x\"", CookieFilter.filter("a=\"xx;x\"", null)); + } +} diff --git a/test/util/a/Foo.java b/test/util/a/Foo.java new file mode 100644 index 0000000..ce6b556 --- /dev/null +++ b/test/util/a/Foo.java @@ -0,0 +1,23 @@ +/* + * 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 util.a; + +/** + * Tester class used for {@link jakarta.el.TestImportHandler}. + */ +class Foo { +} diff --git a/test/util/b/Foo.java b/test/util/b/Foo.java new file mode 100644 index 0000000..77bc623 --- /dev/null +++ b/test/util/b/Foo.java @@ -0,0 +1,23 @@ +/* + * 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 util.b; + +/** + * Tester class used for {@link jakarta.el.TestImportHandler}. + */ +public class Foo { +} diff --git a/test/webapp-2.2/WEB-INF/tags11.tld b/test/webapp-2.2/WEB-INF/tags11.tld new file mode 100644 index 0000000..b78be03 --- /dev/null +++ b/test/webapp-2.2/WEB-INF/tags11.tld @@ -0,0 +1,36 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + + + + \ No newline at end of file diff --git a/test/webapp-2.2/WEB-INF/tags12.tld b/test/webapp-2.2/WEB-INF/tags12.tld new file mode 100644 index 0000000..c2d02b6 --- /dev/null +++ b/test/webapp-2.2/WEB-INF/tags12.tld @@ -0,0 +1,36 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + + + + \ No newline at end of file diff --git a/test/webapp-2.2/WEB-INF/tags20.tld b/test/webapp-2.2/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-2.2/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.2/WEB-INF/tags21.tld b/test/webapp-2.2/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-2.2/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.2/WEB-INF/web.xml b/test/webapp-2.2/WEB-INF/web.xml new file mode 100644 index 0000000..de11c94 --- /dev/null +++ b/test/webapp-2.2/WEB-INF/web.xml @@ -0,0 +1,32 @@ + + + + + Tomcat Servlet 2.2 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 2.2 of the + Servlet specification and version 1.1 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + \ No newline at end of file diff --git a/test/webapp-2.2/el-as-literal.jsp b/test/webapp-2.2/el-as-literal.jsp new file mode 100644 index 0000000..9b47cc0 --- /dev/null +++ b/test/webapp-2.2/el-as-literal.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    +

    01-#{'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-2.2/tld-versions.jsp b/test/webapp-2.2/tld-versions.jsp new file mode 100644 index 0000000..debf250 --- /dev/null +++ b/test/webapp-2.2/tld-versions.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> + + + + + + + + \ No newline at end of file diff --git a/test/webapp-2.3/WEB-INF/tags11.tld b/test/webapp-2.3/WEB-INF/tags11.tld new file mode 100644 index 0000000..b78be03 --- /dev/null +++ b/test/webapp-2.3/WEB-INF/tags11.tld @@ -0,0 +1,36 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + + + + \ No newline at end of file diff --git a/test/webapp-2.3/WEB-INF/tags12.tld b/test/webapp-2.3/WEB-INF/tags12.tld new file mode 100644 index 0000000..c2d02b6 --- /dev/null +++ b/test/webapp-2.3/WEB-INF/tags12.tld @@ -0,0 +1,36 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + + + + \ No newline at end of file diff --git a/test/webapp-2.3/WEB-INF/tags20.tld b/test/webapp-2.3/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-2.3/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.3/WEB-INF/tags21.tld b/test/webapp-2.3/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-2.3/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.3/WEB-INF/web.xml b/test/webapp-2.3/WEB-INF/web.xml new file mode 100644 index 0000000..7c6a953 --- /dev/null +++ b/test/webapp-2.3/WEB-INF/web.xml @@ -0,0 +1,32 @@ + + + + + Tomcat Servlet 2.3 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 2.3 of the + Servlet specification and version 1.2 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + \ No newline at end of file diff --git a/test/webapp-2.3/el-as-literal.jsp b/test/webapp-2.3/el-as-literal.jsp new file mode 100644 index 0000000..9b47cc0 --- /dev/null +++ b/test/webapp-2.3/el-as-literal.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    +

    01-#{'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-2.3/tld-versions.jsp b/test/webapp-2.3/tld-versions.jsp new file mode 100644 index 0000000..debf250 --- /dev/null +++ b/test/webapp-2.3/tld-versions.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> + + + + + + + + \ No newline at end of file diff --git a/test/webapp-2.4/WEB-INF/tags11.tld b/test/webapp-2.4/WEB-INF/tags11.tld new file mode 100644 index 0000000..3c7ae98 --- /dev/null +++ b/test/webapp-2.4/WEB-INF/tags11.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.4/WEB-INF/tags12.tld b/test/webapp-2.4/WEB-INF/tags12.tld new file mode 100644 index 0000000..533235b --- /dev/null +++ b/test/webapp-2.4/WEB-INF/tags12.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.4/WEB-INF/tags20.tld b/test/webapp-2.4/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-2.4/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.4/WEB-INF/tags21.tld b/test/webapp-2.4/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-2.4/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.4/WEB-INF/web.xml b/test/webapp-2.4/WEB-INF/web.xml new file mode 100644 index 0000000..6020a81 --- /dev/null +++ b/test/webapp-2.4/WEB-INF/web.xml @@ -0,0 +1,33 @@ + + + + Tomcat Servlet 2.4 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 2.4 of the + Servlet specification and version 2.0 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + \ No newline at end of file diff --git a/test/webapp-2.4/el-as-literal.jsp b/test/webapp-2.4/el-as-literal.jsp new file mode 100644 index 0000000..9b47cc0 --- /dev/null +++ b/test/webapp-2.4/el-as-literal.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    +

    01-#{'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-2.4/tld-versions.jsp b/test/webapp-2.4/tld-versions.jsp new file mode 100644 index 0000000..debf250 --- /dev/null +++ b/test/webapp-2.4/tld-versions.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> + + + + + + + + \ No newline at end of file diff --git a/test/webapp-2.5/WEB-INF/tags11.tld b/test/webapp-2.5/WEB-INF/tags11.tld new file mode 100644 index 0000000..3c7ae98 --- /dev/null +++ b/test/webapp-2.5/WEB-INF/tags11.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.5/WEB-INF/tags12.tld b/test/webapp-2.5/WEB-INF/tags12.tld new file mode 100644 index 0000000..533235b --- /dev/null +++ b/test/webapp-2.5/WEB-INF/tags12.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.5/WEB-INF/tags20.tld b/test/webapp-2.5/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-2.5/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.5/WEB-INF/tags21.tld b/test/webapp-2.5/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-2.5/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-2.5/WEB-INF/web.xml b/test/webapp-2.5/WEB-INF/web.xml new file mode 100644 index 0000000..53612ed --- /dev/null +++ b/test/webapp-2.5/WEB-INF/web.xml @@ -0,0 +1,33 @@ + + + + Tomcat Servlet 2.5 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 2.5 of the + Servlet specification and version 2.1 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + \ No newline at end of file diff --git a/test/webapp-2.5/el-as-literal.jsp b/test/webapp-2.5/el-as-literal.jsp new file mode 100644 index 0000000..f48e114 --- /dev/null +++ b/test/webapp-2.5/el-as-literal.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-2.5/tld-versions.jsp b/test/webapp-2.5/tld-versions.jsp new file mode 100644 index 0000000..debf250 --- /dev/null +++ b/test/webapp-2.5/tld-versions.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> + + + + + + + + \ No newline at end of file diff --git a/test/webapp-3.0/WEB-INF/listener.tld b/test/webapp-3.0/WEB-INF/listener.tld new file mode 100644 index 0000000..457d682 --- /dev/null +++ b/test/webapp-3.0/WEB-INF/listener.tld @@ -0,0 +1,29 @@ + + + 1.0 + listener + http://tomcat.apache.org/listener + + + org.apache.catalina.core.TesterTldListener + + \ No newline at end of file diff --git a/test/webapp-3.0/WEB-INF/tags11.tld b/test/webapp-3.0/WEB-INF/tags11.tld new file mode 100644 index 0000000..3c7ae98 --- /dev/null +++ b/test/webapp-3.0/WEB-INF/tags11.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-3.0/WEB-INF/tags12.tld b/test/webapp-3.0/WEB-INF/tags12.tld new file mode 100644 index 0000000..533235b --- /dev/null +++ b/test/webapp-3.0/WEB-INF/tags12.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-3.0/WEB-INF/tags20.tld b/test/webapp-3.0/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-3.0/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-3.0/WEB-INF/tags21.tld b/test/webapp-3.0/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-3.0/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-3.0/WEB-INF/web.xml b/test/webapp-3.0/WEB-INF/web.xml new file mode 100644 index 0000000..3cab0cf --- /dev/null +++ b/test/webapp-3.0/WEB-INF/web.xml @@ -0,0 +1,36 @@ + + + + + Tomcat Servlet 3.0 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 3.0 of the + Servlet specification and version 2.2 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + + \ No newline at end of file diff --git a/test/webapp-3.0/el-as-literal.jsp b/test/webapp-3.0/el-as-literal.jsp new file mode 100644 index 0000000..f48e114 --- /dev/null +++ b/test/webapp-3.0/el-as-literal.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-3.0/tld-versions.jsp b/test/webapp-3.0/tld-versions.jsp new file mode 100644 index 0000000..debf250 --- /dev/null +++ b/test/webapp-3.0/tld-versions.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> + + + + + + + + \ No newline at end of file diff --git a/test/webapp-3.1/WEB-INF/tags11.tld b/test/webapp-3.1/WEB-INF/tags11.tld new file mode 100644 index 0000000..3c7ae98 --- /dev/null +++ b/test/webapp-3.1/WEB-INF/tags11.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-3.1/WEB-INF/tags12.tld b/test/webapp-3.1/WEB-INF/tags12.tld new file mode 100644 index 0000000..533235b --- /dev/null +++ b/test/webapp-3.1/WEB-INF/tags12.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-3.1/WEB-INF/tags20.tld b/test/webapp-3.1/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-3.1/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-3.1/WEB-INF/tags21.tld b/test/webapp-3.1/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-3.1/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-3.1/WEB-INF/web.xml b/test/webapp-3.1/WEB-INF/web.xml new file mode 100644 index 0000000..426f51f --- /dev/null +++ b/test/webapp-3.1/WEB-INF/web.xml @@ -0,0 +1,36 @@ + + + + + Tomcat Servlet 3.1 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 3.1 of the + Servlet specification and version 2.3 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + + \ No newline at end of file diff --git a/test/webapp-3.1/el-as-literal.jsp b/test/webapp-3.1/el-as-literal.jsp new file mode 100644 index 0000000..f48e114 --- /dev/null +++ b/test/webapp-3.1/el-as-literal.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-3.1/tld-versions.jsp b/test/webapp-3.1/tld-versions.jsp new file mode 100644 index 0000000..debf250 --- /dev/null +++ b/test/webapp-3.1/tld-versions.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> + + + + + + + + \ No newline at end of file diff --git a/test/webapp-4.0/WEB-INF/tags11.tld b/test/webapp-4.0/WEB-INF/tags11.tld new file mode 100644 index 0000000..3c7ae98 --- /dev/null +++ b/test/webapp-4.0/WEB-INF/tags11.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-4.0/WEB-INF/tags12.tld b/test/webapp-4.0/WEB-INF/tags12.tld new file mode 100644 index 0000000..533235b --- /dev/null +++ b/test/webapp-4.0/WEB-INF/tags12.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-4.0/WEB-INF/tags20.tld b/test/webapp-4.0/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-4.0/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-4.0/WEB-INF/tags21.tld b/test/webapp-4.0/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-4.0/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-4.0/WEB-INF/web.xml b/test/webapp-4.0/WEB-INF/web.xml new file mode 100644 index 0000000..257c92d --- /dev/null +++ b/test/webapp-4.0/WEB-INF/web.xml @@ -0,0 +1,36 @@ + + + + + Tomcat Servlet 4.0 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 4.0 of the + Servlet specification and version 2.4 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + + \ No newline at end of file diff --git a/test/webapp-4.0/el-as-literal.jsp b/test/webapp-4.0/el-as-literal.jsp new file mode 100644 index 0000000..f48e114 --- /dev/null +++ b/test/webapp-4.0/el-as-literal.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-4.0/tld-versions.jsp b/test/webapp-4.0/tld-versions.jsp new file mode 100644 index 0000000..debf250 --- /dev/null +++ b/test/webapp-4.0/tld-versions.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> + + + + + + + + \ No newline at end of file diff --git a/test/webapp-5.0/WEB-INF/tags11.tld b/test/webapp-5.0/WEB-INF/tags11.tld new file mode 100644 index 0000000..3c7ae98 --- /dev/null +++ b/test/webapp-5.0/WEB-INF/tags11.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-5.0/WEB-INF/tags12.tld b/test/webapp-5.0/WEB-INF/tags12.tld new file mode 100644 index 0000000..533235b --- /dev/null +++ b/test/webapp-5.0/WEB-INF/tags12.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-5.0/WEB-INF/tags20.tld b/test/webapp-5.0/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-5.0/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-5.0/WEB-INF/tags21.tld b/test/webapp-5.0/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-5.0/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-5.0/WEB-INF/tags30.tld b/test/webapp-5.0/WEB-INF/tags30.tld new file mode 100644 index 0000000..76443f5 --- /dev/null +++ b/test/webapp-5.0/WEB-INF/tags30.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags30 + http://tomcat.apache.org/tags30 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-5.0/WEB-INF/web.xml b/test/webapp-5.0/WEB-INF/web.xml new file mode 100644 index 0000000..2e26fb5 --- /dev/null +++ b/test/webapp-5.0/WEB-INF/web.xml @@ -0,0 +1,36 @@ + + + + + Tomcat Servlet 5.0 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 5.0 of the + Servlet specification and version 3.0 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + + \ No newline at end of file diff --git a/test/webapp-5.0/el-as-literal.jsp b/test/webapp-5.0/el-as-literal.jsp new file mode 100644 index 0000000..f48e114 --- /dev/null +++ b/test/webapp-5.0/el-as-literal.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-5.0/tld-versions.jsp b/test/webapp-5.0/tld-versions.jsp new file mode 100644 index 0000000..1d87b0b --- /dev/null +++ b/test/webapp-5.0/tld-versions.jsp @@ -0,0 +1,31 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> +<%@ taglib prefix="tags30" uri="http://tomcat.apache.org/tags30" %> + + + + + + + + + \ No newline at end of file diff --git a/test/webapp-6.0/WEB-INF/tags11.tld b/test/webapp-6.0/WEB-INF/tags11.tld new file mode 100644 index 0000000..3c7ae98 --- /dev/null +++ b/test/webapp-6.0/WEB-INF/tags11.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/tags11 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-6.0/WEB-INF/tags12.tld b/test/webapp-6.0/WEB-INF/tags12.tld new file mode 100644 index 0000000..533235b --- /dev/null +++ b/test/webapp-6.0/WEB-INF/tags12.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.2 + Tags12 + http://tomcat.apache.org/tags12 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-6.0/WEB-INF/tags20.tld b/test/webapp-6.0/WEB-INF/tags20.tld new file mode 100644 index 0000000..056c484 --- /dev/null +++ b/test/webapp-6.0/WEB-INF/tags20.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags20 + http://tomcat.apache.org/tags20 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-6.0/WEB-INF/tags21.tld b/test/webapp-6.0/WEB-INF/tags21.tld new file mode 100644 index 0000000..4a19675 --- /dev/null +++ b/test/webapp-6.0/WEB-INF/tags21.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags21 + http://tomcat.apache.org/tags21 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-6.0/WEB-INF/tags30.tld b/test/webapp-6.0/WEB-INF/tags30.tld new file mode 100644 index 0000000..76443f5 --- /dev/null +++ b/test/webapp-6.0/WEB-INF/tags30.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags30 + http://tomcat.apache.org/tags30 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-6.0/WEB-INF/tags31.tld b/test/webapp-6.0/WEB-INF/tags31.tld new file mode 100644 index 0000000..7d8ef32 --- /dev/null +++ b/test/webapp-6.0/WEB-INF/tags31.tld @@ -0,0 +1,37 @@ + + + 1.0 + Tags31 + http://tomcat.apache.org/tags31 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-6.0/WEB-INF/web.xml b/test/webapp-6.0/WEB-INF/web.xml new file mode 100644 index 0000000..ed9955d --- /dev/null +++ b/test/webapp-6.0/WEB-INF/web.xml @@ -0,0 +1,36 @@ + + + + + Tomcat Servlet 6.0 Tests + + Provides a web application used by the Tomcat unit tests to ensure that + Tomcat meets the requirements of the current JSP and Servlet specification + for web applications that declare that they follow version 6.0 of the + Servlet specification and version 3.1 of the JSP specification. This + typically means ensuring that features introduced in later versions of the + specification do not change the behaviour of applications that declared an + earlier version of the specification. + + + \ No newline at end of file diff --git a/test/webapp-6.0/el-as-literal.jsp b/test/webapp-6.0/el-as-literal.jsp new file mode 100644 index 0000000..f48e114 --- /dev/null +++ b/test/webapp-6.0/el-as-literal.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-${'hello world'}

    + + \ No newline at end of file diff --git a/test/webapp-6.0/tld-versions.jsp b/test/webapp-6.0/tld-versions.jsp new file mode 100644 index 0000000..c20d743 --- /dev/null +++ b/test/webapp-6.0/tld-versions.jsp @@ -0,0 +1,33 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="tags11" uri="http://tomcat.apache.org/tags11" %> +<%@ taglib prefix="tags12" uri="http://tomcat.apache.org/tags12" %> +<%@ taglib prefix="tags20" uri="http://tomcat.apache.org/tags20" %> +<%@ taglib prefix="tags21" uri="http://tomcat.apache.org/tags21" %> +<%@ taglib prefix="tags30" uri="http://tomcat.apache.org/tags30" %> +<%@ taglib prefix="tags31" uri="http://tomcat.apache.org/tags31" %> + + + + + + + + + + \ No newline at end of file diff --git a/test/webapp-fragments-empty-absolute-ordering/WEB-INF/lib/resources.jar b/test/webapp-fragments-empty-absolute-ordering/WEB-INF/lib/resources.jar new file mode 100644 index 0000000000000000000000000000000000000000..1cd5bb840301d8f00e53bb76ec25568ddd6d2e95 GIT binary patch literal 16055 zcmbWeV~{RQ)3$q!jWxDy+qSWWYi!%LXN_&!wr$(C{oT)w{XOr!-=8}xJEA(OGb=ix z&Ww((IuxWqKv4jY|FpVpVj;l43`yK}X9%Lsc zWTa{7=HR4hsir4q8k86onRk!&kAMLR(h!jKzQEVm|D?kF=Ti9Z1myo~0>pnt|2u(< zq_C)*vgnn!r6acZ-(6R+TmD5#gMmRX&cGQeKUF_Wf9j`M}rp(9yd&+A#&0Xh-+`MtJ zsg_^u`B=&ANqFXEctv%>(sms`B*22;k>Eli4vjekcm410%Tg41*bPbl2DfvfYCkJAv+cYL4YkXMV`gEewv zzEv+9J)5hsi*uLrkac!4d~QIF$p&}IW%G(?+Ls*nxsWygnl8B(#D{JY)F)FpB%NIl|MTe5E8P+xKgxmEe+i0YM^=nBPz^CK{D}TCrD#+_6k-21#V(Q_4 znpnu9aQ?LJ;J=>~Rwp-HvK-5*j7X$@voY<+49T(oaoBr*Vu z1hB~)-GZ__921#YuEfmecb%h>SD4LIp=~h0o{wXdUF&kUr7A)HzOYbMtMygNt+e?l@UBSpba zq{AjYWli6_nuvn{>g}DY?W)bwh=A7F^4~VL@$*#czvxVXQgSCHsL!}QMbt(6nL5^! z9s$}jRMAG`Z)C&IiKhL`@CA*w#!&wE?{e4Z1*zy_~xTr;G6lfd|{Qb~Sc{Cdbb0qev}X8-u1l z3W+_ephL7?5()`!LhUd#@kbtsy@*i)oEuykeok$Y^{Px-Wikx=<&h3(W%k*S8l-}1 zrHdfiy$iaRjNQ9uQE?eoEv4R&vd}fv_MoN`{G=CfmvB9xZ3rqeJ%i#O8M4rx|HxVs zwFNgaBB62829UvCCS0i^f8OY~(MT-BkSondgz~DPv`2e9mo5@I z0q&WouE%LM-}Y8k^1N$`5)oQiQl!8T8BeAzd=f24JmVSz9w^QzB>5}2IV-tro>qPB zM|r;k>CQ}mO_Iu^QG(m`30tV#CUaaU(5@ElG0D~|1Y0yTzx~ZP?p_C~Y1=)Gm>qhR zj2GSSS8mNq(W>dhDl!98_xn=`%rMLc9O05%V8w0}O;rfUv!+v|#)TP?oJf9=hmebS z7fN6cW<6ZQYvA>^MfUwxSJeOx3uUE||L->qSP&f$_6r6lLIdO`vTr$gJH`g6uJ%C9 zJZi0>hF{$EzUt&()L)C%;rx?hB5OwuVDOkk{a#pc!L93R{`RLx<=0&nRAI~=FrVxK zU`k9{do}8Me*F$&Iqs2V?G?>u?GYl}6h}G{t>~L_~bmNMxDC>qZYcbrpU|E0S;6&;v{uirZ|` z6_-@xSpb%BcF|R9QprSJc`?7oL5=P!UUS3KbkXbB>dRO6iP8LD#>ef=H67q?CX|S1 zsVS6Wu6`RfMjZ80KXo8NntQ_!z_ruoI9G-PkyPorkDJ^qy>=&tv7k+Tn?@t$>Z8p| zF!eH~!OqZJiDV)uA{Ql!$|)z_I4 zJ?td)Q%6+SF$>QZCynCU8d#a$41<{auClA4z(Lw2<-i=kh;>{scK=M++msDBKe#sV zoQ)ckLe~#yuL-aA9JztV?;LsuxXvK5hxR(t$Yyf=f|tkn%>~hb`q!UWX|8~sq6}Il z95*U0)Mq zfk{(pEQ$N)9wMBVYP)UyTIH+;twXDq5bHQIkWIG~lK>aLlL^U!of6GaWSN^;=vp1x&h=ru>{zA5NZ$-o?!LuE!sM z)FNe5q0>CHP(4q*4DZJZwzCS$s_33GOKjYitG-{65q!KCas6oIL2X%Yu8-TiV(|CT zB&of=pY=Q))CbwQG#T9z?oho`Se{w3$YcwcPnGJCBIIaxNW0X`ZOQRq5j@?a@q?=k zX1q&Gyw)JhDy!IQ zYr+658wDFcHSGiK@k()aYws%HtPaT>Fv@9QfVD!S z=W5bw!ai5equ?i45D@b#NkVSd!H9Dn)-yXP7yy=2VyBI=^+Vk+IS#YLo|7>(;RHjo zqAI*G2sBy%$UP@-^!(rjv3H6;33$GQZgN%Brc-LHZC^elh}Z%LPXuwGC;-b3sT|_( z8#k;gk3@8f41m>B5Oh-#p?c73W3`AzSXexEUZmxsnBt)g+X!pYxf zZO0=UV>ixUsU`13>6z(gf%}}TUuvGWli8(=1!yk{_?uHZD5{ZSNH$-DH4?k`ndT`w z;Ron+$Z_Xu)kzycL{`oh3W?K5L5ZAQ9K2cM$nI-`y&X}@(VXon=$Gy4VroPYSq$cG=(@R9Cxfo862bd@8#c+x%o+T z6whGOXdBQ+)q-JBnO~fL)kMJl1XXHFLDwcJL>8`L_o*H-pjk!T8=D=9so6))+4cw1 znp5eH6qZC7!|><~2ut6y-aNZ8u7Xqq&Ckt6lHB%Wts;+d+F169R! z{cGaD3jyJhRGES%)`q8IoWNFnnvH`(n2+RPUGxDn*%+3g{q}XAfTrfQ-m$B{Tz)g-+lb?i;+r9}o4+Ts9pUS)l|LOG z0@;${o?#q~i3n+u)0Oye$wo~nx0dp)p7^)B4Cm+Kqzy1P#(?|%tP02X& z5G+bAbYVddJ&xal3#SVgLHq{XG;pr*VjBG5;00j%Gj;34@g>pt=)!>uN2WbByCO>W zZrIv7vGBdBqa8y(O%Ad=oLQo+4-OJj>u=3GZliz}mNkyG)eDw9R6IGGT_Q~HKYfdt zKS&TJOOsrXstOi@oV^-(S4kBA2lZjnbDidjbV@o;H{-lh;`is8dGcgE zD(PKlX*w>3X0Ik%6K8gfPxo zL!<9tGpGr?+FBG1KkyCo0T!WPeuhmQPJTX(H+M~~O!Ol(xZCGc71O#VFpSgC7*|^a zGuW$Rl^moq^b*++PLHe~h^s&9Bqa=sBZonhq=1Gh7<}%%M$**s7p)V6H3NJs3v5n<~|*Cc?lzqbfh_&^EUra*C{Pl~@ywJIFiD zs6uBff=Ywd)t$jlLa=$MSg#)FY;`}VMUI+)fJB1j^rDVeXAt5tVMtc&Y^`W90l*GP z)VL_LtZwsKck=G2f))myG}7wMPvI~?fL}eg9NX3sU)ae$R04kS*-pXHEk(GP=!Nz0I7j0sSIzbghLNym%p3hLn{g(i zQxXQazgU?bPEMSB8`fe)rmhCjsmxo1Xut+tcfOXR^Q$R%SVc#Dr=U$XT0_}9b3Lto zp&>n=L9UX&AP^f}m8A47;qJJBPnL~pc4r>_`OlxsJ3-v?R|I03NGKC5ko$hbk_+M~`3WcPWg($SYT08(({DS9lZ$>w*J#(uR80KD z#t_HmY;Ho!AQC{|Li3R9D`IrnI1Y?*FX2kO%k#lW`vZXki9mchsOrl_=)V8ds%lX<&_;QSe{rm))kw1Nfk03haCWJV>THq(tF?p;c0Y>j8o} z$KKwBlv=qWF&R8rp{w-f&Ngg zyUSXZ5(n=4KV=M_Ej`XS+!;!mc)Lpz_QYzt!U!+RtG=Rf6qCER|nS9_kt}XpGy_B720Tv3+rXe68;L!1D0=WlS z0BRJ|tr1zAm9D+7IGzg|z2*Ok{J#6}&&bZfeJQbVcsqXmyxjK!ZT>nW7e`=#l8t=g zSz8xy?ADrD-70rsE(|9s!2z2KdP%yd1X@REqlbQd^zsu8E0nUdG)Ww>ltsYD6NwMf ztW=XMJ$Y@_@p`XU{09z~ha$baDg8e}bn-aOU=TC0Xyzq9+Evu>UM> zwVue_E4LPWGj7Ap{&M?sZQ%87v&j!4HQ4jb!-JXGIx#nB@Gr^VgRFx`4H?~(Lv+N;TrumR+UKg&p z14?LIDH<%^d||FC*>F%K=m4hJy%eO(MZk#~C(F1|lQi1vyBewMwf#(AVy~y#Y+`I= zk|9$J!3|uGd&hf%nd96h&Fq43KzJ#i`6aaMGujVQ!AX+7oE@F5AyKCuX2;f{dA{ zN<3K=^RDiwupYzpH7q<>poRiJOd~7t9au= z2xhK)`6F5cn87AYp^_+X01Z1iH}78DoE@**lRqo$(4xw6-YhVl^JRqr7w^?E_c~fS zN#<7qt*S*8E;o-EC7xy8^tEvLaAjD0eQnxKxr;lW65^Pp6#2HZx2#5j@+fU{QqEyy zh{cbB9@~els7qCEWSPX6iNDs zjX7bAT$e({@Rb!`pLuXfJ*h{f7y4RzFa^^RK9PJrGlO7hnC(`4gc_A%3OE-awLV9W z@MEa&zARjHTBIc#J1L6aGb|#Nmuhi|f>afK$^W8F(Z4SS>?{?|WOS_LHzkUMlL)y; z_d^VfkvP6Gmh?rkI!0$tyUpKfG)~=*NfhbnHz-dNw7S@!gIr2d?_QpAlA z;BoSR93)lWbC+v=t6Xpag1B4>Mwh^EVU|s@mbDCC=v^@L&Vx`d?bM zu?f6ZDc>%fm@#Z3;XHg?LU^N=g5>s2i@(~9@T%@tjIsqvqBlC$2j@W_4o|E)@9SLB z8|7_9@1m3xZJg!K%@je>1;zgD`l9%$mqt@tH}4n`PI`+>*`CN5P7ziUXBG^0f_)>T z^(I}O=zWLKuVz}PVvUE6bQuX$IyE2jdn>_uu54 zPZf^h6)b&$Et6ow-B`d|B|4<2$lDJ=p5){?hy4&Vy_NntmMB$ej*yl522@2^MIY_b zFL%~eY8=|h)hELHBsda()>5mJMt`+gP_)Uc=!M6h^yKr6Hg_bI?zKofRRN=C99 z@dA~WLBTOdsOS=7h^`%{<+F@Xn<8`0X(c90w{3E(IVZurMG14VS)grY=tZ|!tp>UU zsq?ye`GgoF4}6;&mJifPu9h!~i>&K`l>jKQ_*1Y}B#J_im_L7a5>7?}o{~_Ti~NHF zfT7o*xR1RT9h1J2E$EUd2v2>Rqa4B?X%aZE6T`%q%#RTV;m8E0s>Nrtw$9QoXxD&y z($z@gdv~9G$MI-u$F{aJ()YLVvIcMsEXC`R()}bX*pxFbp%aZ9Ak7EsDr$h#c`-U> z=a3yo*mf80Lh$SHf1#Hs6}<@DSB65QQrk$*_m2y^LoTUxHd>qQJ^HFMKhwM%6P!}J zve1tP7-c;kCT0)jNN<5k$}Ml|Slg5Z6pSCy;4BLp>&f1ZcBvci#^>hf<7TWH@8pCXw%oWZPItdvVHuq{Fpa0K9ndwQsx?_ zzdw`mAb$-HvUWpC@l%R$Zd!X@Hp?R_$k!&I?`8V&gJ7hN#jNng5Zil80Gaal#>J*V z6_xB%5!as4iTntuRu<*N4T~<;5YG&B3vX==8fH69Mak#oL8z~2;eFXAl14U&lA2^6 z#L!2KfhcBAbuR3O16R)5ip_lXfKl>%``5mfCaJ8BZ? zLX`QFrpCuq zO|gF=iVy`8la2k=47$dN>1u9|LE6Of((U7|66k_o?HH9DMiDc1)ovW_qzj#54%Cd> z9*Q_BVZC@iUw9k;#>#&Pw<>i$EaYvTsn5Mhgw36l`#TOxm?n;!7=C*3*eASvEhxWq z|2+7G&DxGHg5+ev>O=iTq}cqq!uSm?agCzD(1wv4j4=2!0Wj<0|6WDC&w~B-fRc}Y z+u^_H0cq#Ex0>w-yMFXy0^>LHf(PX}bK3fNKnTp)gBBSOZ9?;8~d z>hdF5@i|bmoc^+m&rMjl(x-1W^-3kAl0YBojm^J~_5Rj;`@huG62h0<1O}Un z0b!|r$V2LG{#&a}Chzp;`8NafTFsVANKf9anIM*vobDp~KXM|z*JI9bGxM2M$z+vi zYWw@xEcj#OJLdY4%k?Y5<^*pI0TYvqTB$UQzgY(Q<%poV;ag=jN$Br$3rEjoy8%Go zr&|;o7v8o6XAu+F&rL7xxG`eDaKj=GGvYC9tB$=&{fTZ`x0MI0;KZKCnb+mUv3`3X zv&-I%es{P96-h_j_Q~Zs?jYN|;tLFMetpZ8`_MwT0itzXo+h+|4p7msPBmk1BPBN-I{Pl?XpYG~%UtDAbd6I)exVarYGpvm<+W&nNvJ3D(e!{#}NvYh;R8 z@N`6s%3DHUC<)tK{0}puSo|#23qk`pgT;r=|9k}4u~XqBHO1uu(h^ptOK!Ev87cC} z+mey{56n<^wc;FMX11GitLlIb^4^4d^@K17FR9R37i930*~>ztGU5(cxlpPrkG!5# z5_4EzRf-|0^_5}Hu`*5K3pm6LfURzJ$q>)pg{z5=5-5(+JIb1-=i1a-#&m+A`b+Uj z)d<_s8BACux0IozUR93q!7i?-8{u2CHLLEeMF17cGQ)qxJrN|bh0t)amb+W^?Z4-ks0Ej z=B`X7>~)?}9TdGpy^IUJ=|s)!c~#yqIl1^^Bie0+f}n;KCkyF$Nh6Soar1%01A}u! zeUS~J8;^e6MQQc`>ete*Afu|Lg5mF1Ch`@_{xquBT7+%h%#3nj4jd`7VGPeKPm1F# ziIm(`kE->k;^V&rLTyedWPTuIy4AW#wVgEae)>t!2+W`@P>MiscZOC8fLG^GHUdho z#9JKm13qtjjHT;y^N_Z!^-LJ3 zhCcKa^8k$9H5Qjgkl9}F)^;XY`pqW2RWw4uzkRWdNQtnPjcH#}d6&?ou#V#r0^L^R zwTCwqR?oDbJ7kIJFOOczZ{5by5d^bq)~pH_^>~HNN2Wcd8R&oN_(_H&NJLd1RDJNU7hkU#50sNZNk$i zqy`cs6=`)!DuUSxE%4J3#@3udYaDbUek($TpVJHdc&n|=X($<=e~4VoIq_3Mh7}j4 z_tv(inE<0MZCjP~3ws1a7BnOxB@V_h-AXd3}6^9qlAkb=K5tUepF|A19p>1&q zWEjrJFUFbKnF$m#=uqXc)@xCZ$vfI3KlH<>aBGz-Ag&!oi^+!B?v}czE~eG8kER&i z9=$~09hcUq8T!jso>qz&K$=r7sBpR8q{{wzb!vx|I?e|0Kw69sc*fB%w|H?t6!-c0l1L$___U&a=k~@1L*A8^T~1^A7zB!AuwBj%A%L z#wxfnOE^|(F;V5HZz$;jQHtgh7E}@_-p$~3cmF4VUiYrbjM)hBB3YC|ebN|#;6^#k zu3jt%My!96lG~~?#ER5u$Fqg9fe2Ot-r9B25XCY(vnR{vG!{Y;?FeqjMCZrh{oCvE z`+=V_LnvMg)+1yvETMCBPjJ-3$@=f%6e)NmDGw}+{0yga!5c~jjk~9t!c4$!OpbIQ z2k3U_ro0EnA#RrPtFwn&uf4rvDmw@?Q4TyF8m222#>nbdipcQB3_J#3_aWE#00{I? zcf1SB%k}4HkR2ChlrJ700$B~?UpaGP3rFrV`H#j1k>x)JJ3HEPAz@hqfqQ0L7_s#9 z3~pO*@2MSys0^pq-;|85K{A2Y)p(96a@<9Ip`cS>)_fLnCQ~`C7-}H=NP$2)u9=m6 z=o-5`5feSLAU$f2OFweQmX}LMUX|=X8!|ooo5Nwo%6<^0@JB;7>ay^Aj!X>wg~6d~*%0~Qzy6~v8}(;}Imi9LkXN4d#qAq%TR z_VNtL*N7e;Q6r2j`wYP)gn-Zu$%=3aS(Jzb{u_^52><{3k0`fLsO>b~?02|4Z{-rQ z#ED>6_H|xW_LL3l&c#`G`4Tf7WVi?K)=HB{xo*QhV+f5N0hKLX+0m~PDNP=1(;uOl zya)*Sy7Dj1RoNdF5QUw4eO1{QcbhB!gcmO@&|p|jG%s!%R-E`+SDXYP*A;gl6+o5z z_$=F=;gSTtLwEN`*r*M`?KSjFOR>N4enq!0H5W6>?;tI8qmf67qjW(%_NGU?8!N2q z`#)yt>PHdAbns|Q@T+TlWB1HP&But}vZi|H0{g?)W)&9P2Wd%rzwGe#+9?;hv6M%p{!zaFhWF`b9{d%ES`x-jBS4nu^pyd+^S z1MygcFkFAZdAy}GD6^;i3A7zm1@)D)L1M>fO>kS*xhDLC;KBvLxgH&?2-a+%AD0rU8 zVJw;7BJzz^1j>1RMNo>MZk-PIh7`B*InI}VVZ=%uz^}5Uu;V-HoiSpy{o-fH5PYz= zlP0e!!FL(#iB9s}K_gt-V#^AiO07|~=M2<`M%}CtZbK0KG;wnQ^#XsCy%+Nhg|3QbL z3Lo-gui&YboDJPkeQy1WIg{^akBBSBEnYe839q?H&{Tc1I(^5}0#Tr0)2kPbh25x) zNaiXwZ8?fN9DSiVV2U>BfGe2`hoUyd<11;4eih%eGw>4i%=N8nK_%8jugIE*xP55? z5~lg;Z?D*eN4=Mr-oZH3+VX24jh7O0huCf)QfH#VtV&<>%3Fe zVYsdK?#ICZ_!D!7;pkb_37Wlbvm0@wZm717DK?emv(G(~md%)S<^iAh4(~^coG>fL zF7>&}9&lHo5w=_MG3D|KvH$82T;V!L$@Coexe?>dv(KDobrCnKWoQm-ES}{lj`5V( zpIu#fLrafc;~P#uSg_Q5)3IqcMYC|7rsu7Yaep%^7v0gqQlW6@_V@Icp?n6M(N^o6 zjLy2m4$+xJ_>cD#p7?MaY!`(zjXW4IRIz5?IHvvsOErW zpyY{jfQ}n;x&~&=Ylcc?(gu?~+B`J6{B%&XKUH73G-G3GP|X{A+8hs}TZn>j&*7sO z>pr;ul|#IlhT=998ut8=5u}m%VDYoh)TP#U`-f=q;1qGBAB_uwNS85!y=jaVu4Qiz}Q0(l`ua9hv$rY3Hb6i^oa`9iv3dz!f533$AnWzkUcJy(hAPE0qDdjS=O`I{RSQ zTHna_udJG^B&447>nN7E9%UAFy*^n|(e_P$vV8nQyzWL=NM0h#lA}bJ7?><4MHSMH z{oQ!mI#OA)OYy(@@wgu2x zGDs|Xl}#)a7^-CCRF>x#eK1WF>|*iGj3o{3XdeJwbv71ZtIRcw1hJ}7*Sso@P`oQj z+9#B}Z*I;>(i%4xEdUCsElSH-qwnl!ngu1KQ}XJR0%qrDmkR5!-kpbn9NJeleLzq8 zMEF~-u64@%-MC5a8)Un$33v?`=Iox848#Q;OkqQ^BTkee=F_z9hHzX#Z|>=QMDsrgBTq94pz~Xg5sf{ zK8G)>S?y#X2Q{F%(hUZq2Vd^|qJox!@OJ(EZ2KCWo=pf9#lU@+5X<|KT%d+evae0$ ztD2$OH%u0HHZZFWaYVxIR_(067<)$|)PztMy-1Eg%p|QA4`d5&qve#vu#$E+5-_xdeH&Rb+FdQo8D)aSppMTi-j5iqGO+46$$?wTEG%!$;{`y z9|JYXug{CPc#H|;ucTYL~$uOAv!QDzkY4KP&?fl|48wUCjL=^UM#78raPI173MixuYxTfOf{ z*8zjUwx$9%NMNNjzFkvDZ@9lAc_u=>I}>^z;c^_FHH{m?N9=&r33 zqB2ztkJ+G;Ft$73r;Aq|9pum0k6=6NpupgdF5ULXw!NfWLhO)B)I32Zxd=qk4wecr z;J|GSc9YjESyPRVKX^C#2{j6<0LPJmO5nwsgd0qm#oT zg67S^?2;9?ICguVDwdsX(PHY2s@nY9ZiX|(I!5A*4rPvKt@jZNK=n8%~%e`{EmrjFz zHw%T%OiPn{o%19+Q<5*_1JIEm7!S(BNuCH%9BS&g)wZYOF$|?8y<~7dh$WzDztu-& zNA+MNQ*8YA8{K|U3YLrCAJ)~d;ka=9!3V?jNNfrhXBuqxj2bFzqM|JQtYR82J;`YhTUj=m4B*PIsqiu&b?hHOm~-%#=+9mk@1KAen~7Nj%`y0I24Vis1w5IJyLh>-=T4siYFk+a-LL;mNUS`a?=2I1Wv-fU@r)&Tgy-XVP>5st5i+|K7y z{mmZ;2S$v6e$zc%h%1JD%&n1!iR_GC4S9JtQ32Hh{mwA|Ok+sjgg0ovei$r9pvYEY zuULq#7SNqcKSMYLVmky-UC!8&Tz$CdsK(P?OtKVH5Pt`zr4KB)n7xqqgV;vIp4-4` z6Q0&ngb|)5gD7IUT>}ppoGX&!%L)QJGuX<|l$ilK#J6u*L?IoP=3}NPU$X(c9%|#) z8<63Q9LPPR=g_=0KynOx--;frxQMdYxA%;(F`PPG6?@XhZA>SG>_%omrC9yIY7LuQ zok)JM)%7=A)z0LgcBL%$xbU1sUX7=kQkW8v4}DG>7#1r*5f<+zC>Q^BBw9x)-0@{Z z#At7vOfxr1-@RNZD_qMYX&?ZN~@(JQ~ed z6VgDxWsR|<3!PIt#@iFE6Y8Lwf+8Uu9JLq=bs;Ub^c^fEt;phRF_>mhd#!WV=c*@- zN#{Z#XJ^w1jVViFD^d0t=P=H(My^E;`c+HaXrF^e*%{kE+jwqx<5U;I*M*a#dUU0v$yEV0lMnHxFWe^l-Ag=Ensx zYF-3n)CZqAgL+nkctGtvYE093GF9y4jnh1+M$qk5zKl&l)UPl7=jtg}^Ei##*x-xn z4)v;twIfV98|o%U=LYJEtkmLg-=p719&=L;pmD|u)NQ&-paF3%#KFsIpv!dFi_q8b z!1B_#aXj;ysN{0`tX&5bAQY@@(#n{XhVPA}qo@!|XR(+FmWwFt?}WN=sbYoa-E&IE zOBww#iA2!YP6m56=qs4gCb~(u{6(uqq;TWkZXA5wJQ=f|d9SoK-xUje9p7b>wa|tY z7R+U4O$;gJ3vj|!`IxFYX}e%($}5`EeM;x|i}b&%^{Jh+@b6jYW^R8wKMeA-w{;3E zxX94QI9hFyN?gJjuh#z8G}wiUTq{ zH7ZXddM|*<14FN5+KTkP`B74Gq zEiVI+ru5l-DTWSD5kId<-0u%`@~Ey9XK!Sb?efXb0JP3ZGqu*vih&R*%Q9dYvWKX7 zF*+$O)wQ>BTPFB$ItDZsZH&}CEt+}UrvQ6S_HG1Po!eX0th)DR1fh?h@0FD1F)XBD z=z$B4hsNK#Q4s(9XZB+dDsuPD^NlY`3UAZvpmoa*Tv+%vv-2@8-AZ+x(~^8OK36%p zsX{7T;}j6mQBi8>9;P82pvOJ7xd9c%N2+)Q+xi4AH1pAC|E>HY3w8)YGx&Rx8HvG= zd3^dO65(>iIt{ac@1mxIsRHd&&RSD$8;#o5RhhW4>9eTFlS1TtQT5mD!CBFsDBa)L zQXxHf%bBjG!#d-=&k!US>EE^=jlcPZ7J^>>*jd)3QKH8pM~0KTt+Ms*nxsrD4 z7NuUi67d>7veZMSmUlc_Dqu1?vKRKKgDt_^&xm*<(M`p%RyBXiJ~XCZwG~)Fi<3X2-@SgmrD-Trus{}jHq*^7Eb;fe$0al+JBRzXMN9aGayu08ojv3 zUPWQUArv|U7x&jxE`=1H14BpE1@AQ*F^a_Lzd*BVrctaMq`pNln;hG?lTK*g%s5K;IV6=GZ9;IiE z*nlFv9oVT(U1(^Jy3LU^vkfh$mUU+ACW1&8V95yu<2xys^pqIIaxDnY2WjMXPXlzF z%M9mF6C_QOmMOKh8$9bN_ei$i=5#%A?$~ZcY>F88R1r#pj?1HnWmDY;)7UZ6bLD%`YKgD_QxB;W9d5iM181evigqf|1&V7H=M!e{j5MxOmJm;L^ z(@RxU8YlysA0pK|w910N$NLH6aYSC9CMvU=yV_E{97#3=>gOex?&S^^2V1N6&pp3b z`?x|g-s)%oeouq|f1hA$P>|8MUfH9OiFEB&$DU_kUmION9h7lH}reQpg7SRq@iTt)1lc2~KKtXfWH zk<8Ap?cpA|hJA>PcY677ZRuLGN``Su2BQikb%W~`3yY#SO*w102^L+wkW1>9uG(cA ze|8lRZ_GsvXr8r*1O*OO+a6awIje3X3Mi#9h#c<#5D4S!Ca}cfyVxbe2~ln}g|B*^ zS%Tj;In7#rz?%jj8B?ZdvJjx^G^!J#QR_>WQSGy$Q&+49UZZNNOCvMLOv}}O6%Ogx zL#W-mFK(F`9=pLB!1X|eYIKAD!8lL0R6Dphzvk91?Lb9WSQkFA1CPcB!;)8$RJX*U zRPkpv&A~|=u?L+ar!Uool@rK0F(&wQ=&Sl%+Rm`mUw!c=0Ppi}R^@nu$)Q{#*{}wm z+kD8!a*D}+-;$ID%Aq5GHK!U3Ph`tKxq~Cd5|hXBkk+WMfV0Wu9hfwIq_3AGz|28c zFca#d8Yh@myCojqx0)iTvHwo&KD@=kg;)GL(g8aU!c#bAm^orDRKhDV_RD8@>;Nkc zm>BPqAsIJLW=!9JQ^rNJP!hNXDkrN?DBD0mTDU*usJCv^jt!3j3L^p z2)}vk>n1XcEhFRO$iVViGVt2UU-9-Bc7#Qzbys%P+N|`F{y!wBDE2>xn1BEPX^?*} z8`1x@Z2rTE{+9%GH8!9%anLulHnwr1bF;RpQc;xuXTA7{@AGfogDNGU%#sYR=fi9M znrlL$G~^*m5|Nh9zY6eYhY~VgJ2}DUe3cyy0Fdzr<}rd1_f;Z>kV>DTPZX7<31Lh~6;2`& z;lLf-G0q~PO866?qA2BwN)|Fi&{GL}x(7niD2IVT7l8O-%=O8eKAc*BN@o)nYO_nK zbRC-*`%gw#mKME3Ne391k6W83gP=DmvWTFV$Y`jlv}{sjSHX^I#@(XQgTOGiNbTRn zMB3QtsLsDpR8&PJ?^_dd-By_sWPd*n(NZ=kFiX2|60 zw`S?O1}F`b!dFWhqaw=MWQ3PFy0A-dL%YaaYNn*#lFCb@k5VNQ8vU&fs8v?Vr)q63 zwjfH?OwB~!Y&D<1aaJVUX>lza4(?H*X;6lBY74sd)FSq&HAOp>0yT1$5YL;nL6~cJ zrF%7~dgJTkQqSRBk?Yt>xcyj>lwm`gJQ2d?rjpG+Mqj^@H`<5%79QJ*>-_po$7n|7 z%i?toY?8x`{bF_yn+e;Zm}T&QOuaF|hI{5Mpn3!cJhzhn_6Kp0QKJ&vg4t?M8_E`Y zlA#O?Ik7qGh{K+Ad?QfJj#-{0`k;+dkGHK)$Ta`r`ywravIFgB3*;3NWDht#_?azN zxlWnHNN*EtjfMN^t-*bZ(1}#INn&oi!~Tyr%*6Dzmw^F*EwKNH0|*!e@INH>zY*&H zkl6oU`>$C37nl9-EdRl+|6R5JgIoWP>HpLHZ + + + + Tomcat Test Application + + Used as part of the Tomcat unit tests when a full web application is + required. + + + + + \ No newline at end of file diff --git a/test/webapp-fragments/'singlequote2.jsp b/test/webapp-fragments/'singlequote2.jsp new file mode 100644 index 0000000..dce8317 --- /dev/null +++ b/test/webapp-fragments/'singlequote2.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%><%-- + Resource file that is present both in the web application and in the + WEB-INF/lib/resources.jar file. The one in the web application should win. +--%>

    'singlequote2.jsp in file system

    \ No newline at end of file diff --git a/test/webapp-fragments/WEB-INF/classes/#Bug51584.txt b/test/webapp-fragments/WEB-INF/classes/#Bug51584.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/webapp-fragments/WEB-INF/classes/META-INF/resources/resourceG.jsp b/test/webapp-fragments/WEB-INF/classes/META-INF/resources/resourceG.jsp new file mode 100644 index 0000000..91046ca --- /dev/null +++ b/test/webapp-fragments/WEB-INF/classes/META-INF/resources/resourceG.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> +<%-- + Resource file that is present both in the web application and in the + WEB-INF/lib/resources.jar file. The one in the web application should win. +--%> +

    resourceG.jsp in WEB-INF/classes

    diff --git a/test/webapp-fragments/WEB-INF/lib/resources.jar b/test/webapp-fragments/WEB-INF/lib/resources.jar new file mode 100644 index 0000000000000000000000000000000000000000..3dd25e52a9d3e5e3382294ed8bcd88cdbee22b40 GIT binary patch literal 19989 zcmbT7V~{7!qNo3DW7@Xu?rBWh=1kkRZQHhO+qP}n*3Nr&&$;)*jg5`1$jqpU%B=kI z#8Vkn`ID0b0Yw1-0FZ#m!~*WWCjj7on!g7W02ClCti(?%E+az!H3k5H0ObDDd4@HW z+sRzOH<29xu-pRx5dRMNPiHA{L17t1;Y&|*du-9T9cPgn-UUj%{sA!1b3=Cv&lZ0% zu|8NRJHSkxlb(JP5X(rDuay)#U)a}U@^o3n6}a$deX~9K!o-vWUp^Hz^>+0yZkz1) z`E8Q>^My>KdzR|C{&%k%do>l+*mm?zW6kGdl5Nt%O!wQ1)$-5owk<1mUeEW-%S%i8 z+(5+_VO+s?wo8Hi2eG|^Jtlk+t(8V4&1D0va8G26-^m%O?Etuh-q26=-Yid z9!I7Oso{S1ru54jm}Dg@-}WLH_ICSthpBSzV7M_o zXU9t~he4#T*k1cVk7k*BOXP+;iyk(5HfKX8$4fpbdTBOJ6H1wgG19c33es`WX1*VhRyuW17^>6<`lW3N? zJD?g=AIL1<{7~aNZ(Bc4U;r5KVUstw1Z1wmLct{c90Q=W6`paf?g^osPR z&UU!C?Dzh`$iAu9h6=XE)0Gw88ZGMZT#xdNE`t8-PWaR2k+gWnz*W)?TC|jX($9=&wqQ-h)Y)dAC8o!?SdMzONo}WAWCkwH1 z{`*@sHq|zSMn{fqBS_7i>jTE$atYn6po6p?Vsh~=0&Orfv4?I6J%|x}Ki4@`eH>cF z>y(+)OQjh0%EIl?N^P?sRZ00(N)|vgdgiq*7`t{(BVy7mnoB$(rJ<{kY&L3^|gFL?|z+ z3cIvN0~s(Ips7HRp~4tf!plJww(gme)s+E0>g#?%03+J@^oT1IQ>+_ek9~z6d-=0& z9k^EOam_ZYv)%^ayj)2!opwE8Zs6|msydt|(=AU$1^3&=2qA&xMR^Jgp|K?Df=A)} zgj3E@;QpfY0+KlXjT!MJ)6}XfAIiIJNLOY8Y?2gi)neSv57+|5R;i-`zBZLmw+Xf$ z0ocO9xvek8G1pp9HS4Y^#H`@UB)rHzpE64xiWW5oR-tK_+CLu(VEQ3m;0PC7e9JZ? zXvzXW?$sSa)lSTag=;_Wx!D<@m&idptd`74_q^KhI6UkA5N8~ z^{~Ok(qnV19}U{ryJ0X~_Isph5vEScIM5NAa{-g)X96XF9n-_$rw<_%IhejpycG~n z77{;B6zlw9XGXpa$(#w@v>+n;QDJGhPjvir7LYzq*(2Xh}}L?ZR|gCL9+kb=CsFKp0PH)cTF>B&nY-Lb)b6JEf{W9?9Kc z_#qOUzzqB;bOF;hJ!ER$#@(mOD1lL9N;HY<$1WnAhjN>B-D<^*Dy?0MhXCssGmur6 z1QQ=8ulY1YNr5s=SSQP*Z+bZuC9~d=Ar!DUZZ6Bc+pL6Nei?|DLu_##EkBvx?A|8e zP4g4tOX`9!oGu8th7rS`+V6x&Quq-X<0As!6>nnL&*b6PHE%{RQwew}yAlR+zaex)eS(BeU-TqBA`67%T8g$rVcozi3G4O1zhE z;hB}%?MY6rXoqv~@6Z~(5R1$rkIivCuq+g80M(QiwA%~C>5Z*3pW|;xX1|eN&>A-2 z$3px@mj^q4_Ogdek>_w6kM~*2Rl6@IEXHkfblmd41Ni|_KN7`ccI*s(&cV88B?bb( zvWsmrP&U7*`^3j!7TL4YC&wLNXqJ@)*9U+`@&P$#(`Q@F|iK%p&&t;om?q1gfJq|ghS6#BgEjxa4cB0dh@(L>?OLf^ z9Eqe5C57XPb}NNr6#cXG2Qnuwv6kWqYzl21`ml;WBqHPUXPk-~?eX1}219qaXz3j^EE@oN~`K&NE@ zgU^gutgzYI`YA>k**Z29Rn(0OMDY>N!qp+yT^z}APnT5LaO=yoL7EZ{m3F1qr9h3U*ZGS9LE5{m%r1ixQ=BYFO*;@-cjyb*WZ%av@&g2epy= z%w(fj`nFqFy?knFTRKO4Ur;(wJoo()JvlFvXD%6wCq$B+n@ z7g0PqcU!YO%znOA@f%r)s^>xB$fj~&Ceer*uskdt8t)w7BKhi9It8!^z42cIaOAQZ z>0bsMw-jFt8C|^H8Epukw=KMB@DRw(znWNqkF7PmA7(!dIXHpRDfV$5$YDEFjuQG5?>s;9ibP=264KjeL$RjQ70&2SnN6UBEcc0=dRK_m;J5-jQw_>BN5iO8X`Njee8l*h7K?Q1fKFHT0-9y02?$8U zSWeDsc{B$gF5(BJMNU@>7vcfzkVFj&0!zQGUTTlu?3K_$pc98%TzM(%`U&u>W|yK{ zn_~+)*awTj&p+BIIJzVV7dQ@OZ#5Whk|@P75WPY50 zM5f;oN`{3M4ms3MsN!!z8Zdwh42b;kKzLzr!cR1HVrD3rv^MfR@fDD5*fUL}=@;G4 z3&iC%il02haq;pX7*^VhniNTIQ_UStaMCLtQIUgxd1 z`N-T#G9FgpVebiOqm}w#7WZs-i%)P+_eX%U_zwug24@8cU30iwF5u%OgX*2>2VdS( z31v~7xEm)IAPpry^6teGF?J!4B-J)GC3pvj8{Tq%Od~Nxf_ZZ9_h@o{JOv-YgxyRe zG;wuX%t-nzdr@-XugGeR8maQ}@8~Gv=qW{_G=9&jNB#$v8W&NLBm7u$bYGb=;3vglBd?T%F27 z*Ka{Df`>YWD-|hmP4&sMs2d<{$~9MM^AckJJ>SRlfz!o@X}eo}aU-`q=%yH$T7Nwe zpcaxa5GrNzb-vGR>Qo-EaBmh&Zm^m|QH0v_XG}^3T+#CsG0l2#7<-f_-%g`<{D#%V zKgJi*b1c9Dp;bLqq{P*W~2MqvT%h)?491!*KTyAsT?07N6&js_u(3U9K#Dwc} z_F4`(@A!CtMA;s$jtt=0Xo%tda6+2rNzhze4XJnHn?+RkZftpcm1=}7cv1e?rLyr7 ztRX`rhULP6qK#+f%Hnl9d4hIeik%C7${Yloh%vJC zYZY;W-QLUL+8*nVv_u?O`fHDnP`llW1zel7qrTekEbDFn=56(Uzbl-rMnos^S%Cu+)$*X7Qe z8FFA&X+CG-AN%upnE@B?#XjdMQZiBMM?9^vStTwPw+SVldG6GeVA)VbNNinA>UNov z>n{bwQF96MEk{pj)p*4b+NQ+pgYY1;Z#f;dcWYs%%AW92vDcd$J`6?_FL}D~^ER_V zEsY~b*~H}7fo$Q6{uoippfxCxv|%e#!YG+ex$>b)i(kE_fys5GZWSKrt8IZ4OpEwL zvU$u5{3RjQo3UXkRPxE-oPdQ2wwM)uoNDug+&TdW%Nbg z^HzD^-YBrs6gZ=i(c(XpC}IvmB!fc7&W1`bN1eeY>}0K zRoAH|rDa4S1Azp29$VyqjF%Z+)&%9=nZA!AgJ0-JF*OzRcJ55mEmsn%JuF0U{U(eX zd=^8YKjbRUQ$PVo2=Adzz`l-9Bq`Ir88KImyCK04SIBvw|7yCtXS?@P`WS0;+=Qtm zS#_e*;nbX=t|hbd$)9lDSyP3uYXQV5{%`!RFLkYrsyVP23LgjQ#9T=W91?#q)WU9N z#B|3mC?-^ug9~$WsXf00h0MezHPd~_$e0#lbhFX z7-0@N3rtz=$mtGY7UQR83^x3|!=!aao$lzp2hcAj>Zl?O8%*m*zuGK6X+s&iRM=anlf(b}Qx=4%6)D%qK=Z(p%i`cR6jErcdVXMN-!bw1k%RsFF8B`KV^oI&t{j=mt zAsVasq|!+mV#6_=#G4xpA%&h#7>FEHx^;8u2Jz%A7i2*LX+N=8?mHAxjU<7~bs24S z!UWf|B6Kg!qfR6lnAhzLn<-uYkpFxvw-+sE>Gf}(02}JU0^TgvBt=Eux({$CC(l0U zgP`fD@YOU&sZ6zptkBh?D$Fc=ZSGE8lZ{Bu@4AxW}z zgG`Sa;%AZ>T@nT1iFZ?kUFZW%{Lia| z5D_NRBg6qXGQPZ^=aL0b`_<;DEGn#b#fvg)rAdAwTyeMEd}YMY$a zps#7&pl^?6d)GF9pEEH$5Gy-Wi}^SnhQiL4hPF~L57p^F#=QN*6&SkQj=04)Ub-UMAA*L!}?A^jF~X+WGQxL zYYchx`&lQ}D~i8F0>l-CU%89yuu-TJQA#^kKIsdOvjf%)-=ad`*^Jg%-1Ne~^b{=) zGXyMS%i%}XnHgxsqeXK8ehEA~q+H;r7;fBeR`WreTi;_OSzU4THtJbV1NK%k`hwm< zcBu3QwHRU4V~v2N&gA=%rCx1jo2H$!_AKm7^S4)GRf%q$x5-CxtDEsb7lzafFNgSE z6mzEH>rES_@exN$1$qgptY{5eTB*To5y#R>uu>4JBRJp43O60gv#&fQB>8tej3T;3 z&-SMYF2wR*~E^+gOWu zy1*A3Mg_YO#Pl7NYr9*?0*9!56~os1LXHYp51x-_o`$$+*>}Mfg^v6A+>O)Uv#(+y zv&Ut=_CsRE31de3A0FJc@z0<0iqBo&_dX#r)?*7G*%`39P=639Hoh-0{(y^Jp~x|` zV&nuO41A9ROgeeLR#5LUVZYpJ$NvI@tSzRgL0obY`)(k z_-F4z4u6hA)gY=A{7Ci^BKw8!9T5ZS@#w`z4sI|5^$wu!fe_`%S~~`Ca=b18Gh=md z;dWumA>rFZk9%Oqfa5OXBapxJ*pSdbp9q!|asqbVq;>(9;NPGZKIANzB|S3>n1^#a zHZ}V`XIOw(UIY;<$mq-u?g9IAzvK8(xzvx!bPc@j-yKMwA2aqge!VT;Vc%GE`ys;N z^OE*@Ry`hhP&n<}gE2katXU2_f&Cyl;Lvty6+nTGA+si^g!bJL)#;=v1*gBlm2gFB zqMi;b#2&?Y+(`LHlIA9|Ll zSB^}?;92-ateHm!#eT)aTI|jn{fcf_cGnLV01k!_odre9?ki3I*npKOdHiDgU7>(f z?C(Xrz7c0%=WF?E?}v&?eCVPJ-#}9lASA^HdGNQ3@8(LQ(Hs3)-gQ5{dXxDg(xYcf z28j8@PgkM6Zy6!)t5HX|>A8%`B(e%LmAyS|7W`52ZByOwrMhK7Q-arezwrr1^%NS$ zKP>%yGDJ{a@Ga76B=mPV1tVwDT>zl3lTC{Cb5Co6)39;ur^aVj+$a%XxFMnYY0)UQ z75g5Az62Nbn~MDvaANnPjH@!kXrEn>nI+E#pIhAg^29?f+oUp0SCB0p(Rqd#pWda4 zJ!k=3KjGR=cO%*XJE%xlhw9N+67;8{5j2)Qfw1jkg=Guj3WV=M8qpFR6zYju%>e|0 zn7eYhnc-c$r{g}icuVL8-%kCNRWf-jcse3R#Z3V)l=v-9-uvkhEMAtXd4YbMfuaM) zzl#7Hb}F32#+V#HYW(t4@r?#KBSkKGYZ7wbz6t7%dW=2H^j1?&Wi8Nt?yF#rjsWJs z1r<8$ycB*CdufnFddxm6CrVYtp~s_ALN@D*QV}Gzt|H7CR)$e*K8L6tu*LNb8RF@i zU={IUJjG#Jduij;Y^zH1sAeEkUkP4`3Sk>MgAuFvrXm#f9j$CZ9z_@$j)O3O^cf$e zH5iIU)4`k$n@@6o;!1o!wVq8K3;{vlv zK)WTEA5g#SU?w>yZU9m~X4-$SuXl#1E3__f?be68Aj$4W{ZjH7U{KkZKlJs&M80g^ zmrC_ggRsSukzOXqfg@ougyEj)PH~hemYlQVR=E~YbQDJ*(CUy(<^w{eRi%|!(?JvK zqnikgzzo_9B@YC5t8WnxcySD7BcSw1xWO@yRUJslie#P7)&EEq+s;A)tV@@>R- zz2`kO5=FL|%A;&dU%Waq4Qkz7%YcEZ??qoW^~2a%WpR1{ndt#y2(eN`N(QNd1(^y?`!(wI37X>#`uPIk+yjc%uE-CQC?re(+Fy?J|@MBbZsWWR*Lw z!z*YyH10M|N7t_X^<8B-xOjbjK#j&-q`RcheZ{}T>6Dl;?LOTC-wkG&McKJ(IXveT zuOwT4mhM8rSMS^B?2wBgny#p36`D#R(H}1^Ppes69>`W;hM$Hoy6O;IZKoObM;HsMtb;X;}gfZIhEPU4JfiA;!eUgs+G}lPZ_BPMvyG*4`HRz7Ix; zOTA1EarGclL^{NJr^Gd7A+?5mB-!BR@Hz7KsH9p&-&eZgq(Vp!(v)&uiPQBuMOypC zp$%5zC<6=x2SFj9rTftJgCMJEsJ3+s4MXl11c2IA$6^s95pD8M?;BK=9SSc|Rw$Zc zt}Rw*-&|$xAO^#zXK)+@GhKu$mSvgZ2}1;eYsFNXI*|Yvk-iN|E{l#J3sQ$|_h!m^B3LnaOXmrF z6!WZ%?o6+fXb5?PfX{t-6^%eaF{Qt%2= zZde%E>7S1IuPEs>uI?^!(|$Wq*^>Sopj*KkvThg$xS5JCj&3eJwzl>u>>$uY+308Vx3r?uRcBkY&bC^yz%f5$f_CRWK4<8?72>5 z-y74yfLNfdPcTG4kqUq@wTsB|dQrZhp8BVUgC>fmtr2Ma{@a&UixC(oN zK_|g1f0@Y`O=de|sDSVy`2%S>XH@i}tL|`zjd#z0bgMipe#;!0Uo0MaRImfBOLg;Z z422jf`rHrG4bh+IIb>VvMgXHl^F*K(4C4)Xm*R;$uME8We;4oEP<Z`gs-H|n!+@!42V0khVRL6+P5 z#@kd<&%&P3@7ssw*n?QTlp8vLc*77A7Ep z|H0!D!2fso7UmKNww}V9`3jZgsaQl7JLd1qy2`E0nzUlwK0oa$TV$q#40Yq#TyAtL z)2jb_523-$ucEm#EAnMLxzUYn>OEME2LT~ZOZM5ZGV9$8qM&29w=xUkc4PVP@S??e z8VvLCriBgt@?&qy^5X#H+M;%(e5m4YuO;hKToV5`=&o)tE0saG-TLk+3HDc>&&amL zrXpt9ZKTC6H1cp!luoFJp0uzxL%B6w--iq>-3Y>{c5c;i-ruTU*xfS`b5Wuw{LlD6%&xzPeK-`uf3|F6UZm-GpitMS{{?;SPpx!c8NbDFb z@ecJ=S0M?q2e=kCx4gJ`EyY*AUC5-rzcjBE8*A`4it4+^xx&l4dB5Mo-Mh1^<+d7m zo<5D2JYQ=Z`}J==6>UzxzV5E4^QMminudnQnR1URZot4TO3HT)Ztiy;K7jYRP+&v* z;JX%gUk(l$_P6)Qq)}J=V(3Q27_MWyw%oVEiO)1Rq+G2lN?S(J;<9T|S2OICxU|CS z`^WbigOKo%QC*EfnJvDF*0g-DQp-+~1Sae+U(-UWQQuw)o~hO*O*v_wx}#f$O6TGF z)X_t|Wk|uZPpNJo+(IGt6edEi=lADxjw~VYe}tZKtS#d}cV{`T&Gc%}9)ga@MGmf_ z=O6d^9YNe?l_<5I5SA(+YY#GMCXswlE!yn-HgVr+pIFteJ-lGVa?l}Dw#Gzu5fH`$ zYSNGK*L}U70oEUX0xg(@MlIVkKC)0{jzEr0{40j3iX7BzGA<;${^>Wj8pF9iEH-M}0ItfG{ppv=q444B&!TX5+ zWnz8G**Z(n`OGq6arS1TdN&#{4x2pN?#YN+qR3@&YF}v@3Ikb-GLoDWusp{}8>(?* zKJ?dhzsydyW8CAn#A!{Fg6%(7%Ei$Hv84V8$u?LJDCYJSLdk==blBbLQ{2dAJ6^=W zh?LlYUuH>Q$9B{?Vnl2B#7>hTcwuiPPFz-i?=aXB9p|}%hB>!Jm*zi~SfXmo>ir%Z zaj`_W2}1CZ5#$KCAlzgpSbGXvc_UPF_K=pvRIyrTE{~Y|BXs0x&icFI62xWiXNn-) zO?L#{MAFyf+OMi%r?d>A^=L}#CW&~eCyulp->$L|hpz(TXW| zc(o0J#;WU;sax)5h;?@)GG~z~^AX&k$aA%RW3&l7T=5(@6qQkKZ*gn% z%h<;4{^y7%&Mz%9Dv?fldDdLStqUWN5VaRyTlr2r>fMC2cE-V$<{$m3Jd~K*#5Vom zD*X;Z$|6dRe(b8JyedCL4hmOOhY$~w4phP8+4%D{&s=$yQytN^%0NpFmK~v2e8_=9 zc91lu0?bQv<{UB)LajA+-uL^#ADP?rM@}n`(QLJvT!_Q9f;BXa zv8l|TyzZFPtwtp?_J4_P^Sn392(ohQP@gI70(TY|V7nw8Q7$bL`>qVa6|8X-Pt9_j z88BWydCj_46>_nf2WPWJ`9vOF0Gzr$Kx!(vF z_BEk$(jCq(76=A!eNBDp%cjE_Y_`lwX|9QF6P=2MetS;hi4MiUc5?7RgrgKoIuISo z$LmMdI~XF$E~gIh!*@D)n(K7rH9@R7^{9b%XlCLqKA86RU9pgtE=W0ACRHif55tfx z={>T};B_+(S{Fp6Syt=tDdwUryX2rkH21RF-g?nmMP366s7O(^=3ptr;kVIDARHL; zZIwG8*5B&EC4s8*AyHeN)x}7bu_3R7UjW}`X76JxUKns==RC_dl#~P&X3JASYvDVu z2&CI--2GLdm*!Qf{N1*6d(4n*Nl3!SG}OTiwgz;Kw}M#2db76qdf_a`!kMzHeXoKG z9Q0~w#h{kFi#OjL{cz@6P6AN;Dt1_U3hp@j=(tfQt6-KqCa6?KtuR?5O@kv#kNbst zlXVq~(^kfMRXourO|c+a1t=JI9A5I#t^<3Y*~A;EC@zD+Ay4n=0je4IX5V{EohrRI z+C&rkCy2v+Xq*s4T8v@rjiUs7o+8DCNqRj|vbs}oROM!XG$#KxIuj@^mIwb-sKJu= z;`87uPw2g@lC;5if&q3>PiodnC(7~6zrV&wV=#y{rRq8*9V3R!9x5O-4H7W@mx+MQ zIM2xAd=P|sj-~yVEBv$?!pfF3_rS2#zmRQTSk+ibNZskzP|R`NO3i9}yfP&sZ5y>S zy?lc_ZiiV&p2JI%B7~S2n9L`HZZ%sx{hEk)J68YmzaUL>K-@%kf+_g`(ReKU>RNO^C}-rYt+G<|=y<%UrZSI`v#Vxb>DhAygDZKNOvRG~T3^adgao^O330u}@Cc6@!T zdm9{{j0ojLz`Yj{%leR3tWF{iTX!5#Z^Hjj4C1_qg9Zvbqv|5;J^oKF&MEW(=!df0}c%EKl z@{MOM6sYaL>0^H#nlC$jS%mXZ$+|O6l^& zOiDtkV`O%UZ{WHAG~g91T9hMX<*q$V6AT91k_ucep4EKIOLD1Yk>+M8456aA6yMm4 zC^AT*!#_t4%ViwCBq`vJT`{nN*Z97uKxgf085bbeG?EC(mzm5be=#{x;SDzJYHo zx~<_YTXC8A=s~B5If4vwA&A6nEG1&VzROf3*Ue5}Mw3bI>L$i@s35lCAw;BN=Fe!N zyt#5tP33kbR3}21m<|{3bw`Su%OCBFO#KPf_=pQ~u}t~GV7b+0mMK})Gmirb$3YVr z7k17`y8VlqU{YF+yBPOcous|8F!3tDm+LV^uRH!52B^)BIi$FbnhVh@7gaG)Mp_b; z8T1xEA%!2PI3sE15U0zxB*QKm9PAbl)UNku7A?3$vD@NC3-{N10C{%aiiQH=L!+U zpr(vjY`NPX!BCphiwE|Bm;)O3TD+9DmG_4;M8^KS((M%{V>$U~v#x{;#f0h(-0QDZ z=y>cTaX%56prXhQA%)?DB?%d6Qjdl!Asr+CVx5*kU#K@W82bZ9iX6+&0niBcHB{WGJkAXU5tNZF z{TX(-zVEL6tpyYpI&azA=NgCK9w^}C|kqczCqr3+s_hzz(j#PB>+JGk!C@M%uCKJQa^4ahI{ zdcmD;o-9d#dO!I7?g4!R5stT;%=X7)-L*D^9V146pYbj(#3jQX=H~GIcvgCks;sPw zFrV_iZbyi3h9RVP{42Ci9}E^FPIrdGw61Pk3O6nu?+&K*3anT99_7nh=!9M zOtNHS5MMi{#dj>YsNJBq{pbe7?i>FqBkq41eO7_I}o2U*5>Gh2K3X!`0m1;JbTA{omi>oiV%I%2(jS6Y5F~M20+-i3fg%AZI zFZ%3OFf3MrLM)yQP)^>haJ2RkxTA}(u#uh^sU|Lz-aDBRR=DN~(q2gQ4b;akgdc+P zpTYcu>|*OlCc;lJjz4QTSO-Usur4Bo=V5bJ$!&Jh z4m8G4j=EJ#0n{(Xi8ydc1$2oHdja|i9#~cqH->vo4V7F*m$h@B0)&E2m$z`mfD~@* z%Z1}t7kB!Md+rOZ)mQm^Z~Is2L=Cilxfye*Nh3pY**u(JWge!oX6g6 z-2(leDqU*FO#D06+3A}#lwts7Pw{Gj5R+u-|K z?F|DrFIynq(=cLNC)-f<&PxPY`e6f!>_^#ct*9th&!1?Wr3h96wKzuj)ED~TaBRXX zry2Co9nnr0>*+vVTmtp(+pr8x`jDOy&j&T1m||*W@lrTxFd7%N_PbJ(kr@c;>|(EO)$>(n|^~3ee-1Ka_*7> z*X*!n_^-mJ!;zEs>*IO4*i8Ylv$DK<0#}#5z^*)qt}Vi4u|rJg;eb{%k@L*VcRy1H zr{3`OHFEX+HNJ|o+An~rwh7}SO0eC1F5vJ6g$8>XSbKeD%d;>;(k$m4n}>s6B4P9;~Z5`$ubi ziaWn)Uky6v3~PVfJOmq0YvYn;<{xK9H8H*BW>!`5)0qvOou2bNlEM^rILA*Fwetj5 zX4TKyu!ys}aZ)85sq(3-Xg_8n3hfH|G(Y!;8`Ed~O4he~4EugbQ6oaO(li;t|1!2etO&^t7}j}e5CRh zu+0zf0uwJ?_CJcx(qIQLGy{J&n2{Ltna8HS!x1i)EmJY`f1OviGnJ!#$XKe$Y@tzG zJ1Y`5G=3Box>E?9EhzuE**`7Z6{d@uDG|_tH=pioJg7C?{Rl#Wk^E!*-tgzw;C#Td zwvBmpDkXX}a(F0tP6?anfEBu1t}|(uR$HCJX%jQDjkqOnPiMKn>oO@uC19-$1saI)wPLuRt=XpJG4cUO4 z(T$u~r9mA1(LYlN2N$WcB`)cS_ccQQ$V8dPz#ONg46 zDxu_m;78p!p?x<Jv0;9!JcPl)pM*9`&Y{O1=XhB1})ou)@nyjlkG_NsZHxh(9 z0gI2z8Qx03q$Nkmm#IUz-%BF5x$B{8UZgvI7$K<{HBYLnUgKF#x`wm;F{SH{am98i zWRu6hrwUUTuwNQMES>DypTd?Zw?^h6XWic6tD-#&C5;j1RMUb}Nbv1LFIh4G&Xb{g z)S^-NQ+PI>EO`Rw>-iHj{dLcdyLI5VnP1}1-Iw}zM$k20t`AXeJTAbYGUU7Of&b?r1N5EQZMA!$@gn zV@E@xha=I7;P+WErfZp<+5YCr-Bb4u)?Uuw^w(M%fOc3QEeNTIwzEc9H z&Y>Nhgfxw~^sHkUC{)TR<^XmYwFw6r%0U5@%Mt0h>7CuJ9nimKgZ}$m0HF2%qznIb z@wY+#TmLhgouR$8qn*B?J^gp73C`2JQw0Y@Y~R9a>aEz&XkG1^|E7Ck}Fq zuNsyKQb~eS@Nnlv-B|#17GnUfu>J>M6vj@yW$dqtT|Qg^>8FK(3*y!!-Hdpctrn52G-Fa*T-48c}}@}A$|3^*kH<(u*Iy#6(8(BoztCIWZ3ol z_K1=%Qyf``iVw4bn(t|Y56-stU{M>J;lyHxIy%|REMHY}TuEbXZkw?&s8dr$@KgG{ zz9n-|Rp?t#+3>}6_I_7)e(L@6oW;Duw;WNikbcC5X)hP_UVBRQ9-Vr##aYIyxs5oB z)4s3}8hHFBlaF}@g_#(x9=bxh4_fyoBeq47HS-skN86a6krrrTPtv}6#DBNP!kc#a zypzb@7ecs-hgYv318c=?(v5~WWcE8jAgo_7(#}~96~mFN;&m0S?jyyd^x34W)qv2) zd!+oiHz*cPBYk0oGA^&p15vh+X7e{e#Cn8@d{EDueK7cxqlKy9krL%;t&x86)|P5e z)7+G12X!)8)S=3FDn^~cem-+)%~h&8d=dB#IZUwgc`}%$rbIF8D7@<7ds+84!oIL0<8r_;LdW zFx@uEp233qD(@3T#y}P*vt(b>C^b91s-@C45%F_q+IDN@gbL{^3 zE$^5Itc3V!rT%)=`V&xjr9|1W0ByBz;SSLt1fxc10#n%}hDgzHr*f28Y&{tWnO7G= zOLa};1IxHGze%WSy+eW>pwKo>Shgjp%{;O^$6tLZ%ke`Zq3~cX52*A+(EW#AEmBiL zSzgS=a=w-8w8aZo z`#ZET=Vll)#bvI&OW;s5a3pzYK{;zAN?8xJ@ii+&^gdv=6}>?llsrdvd7ba&G$8s; z(cQX>CfRdaln64ap)x7I`w+P>%_vL@daqyx>1qZc{pjxc)hmGh)(gKl z*dV^w3gKMyEd?h|Q=*)9;Pgiq`RP7R3~)Z@ixCYML26w80F#3gPJujdG*oqFuRxXo z|B}*TD0J>z_bgQwl7CZTRQj2i&S;8A(_FBr-S=CbK=|O~r$b%a#*dZ!`OHEiEwU1{ ztjhXdSk@zr)j#>j9#W!z2Fn-P^;aVf9O=dusGF&GHGs?uSNDP2pbRBhFL~=!Eot^U z3wu(qJQJJXc5?QMeiKzE1vPyF{g)Gw{{p8c_ELoS7r27|XW)eX0nSiy`yVfQMJIgk zqLeC7gZ=fQaH2{4KVB4Qqlkvr=)qmdvJ|=BzJ?zeW6_G4+;rZK)^|h)y~per!Aa}~ z?f};&91uBp7<(9W?jy2rEQj7LRI$M~UWdpbR-1w5S2t-82KLpUwmxiqboh-BLLd>o zl54AWoOI)F<1%)9gDkjdKdJZ8 z=JEpl`^;(pHQ{N7GlOs6J!%M9d2caA4LWgOXmuO&VAvFN)|kFT>p_A~yU38>!(*uO zLuo6WUVgz*lftbpx5?099=$WB+_VYGZEoYf%*-LG;%!kv5SFFg0Z(0J5bnXWf-m3e|CA7R`IRQ#pEM+zAl%@$m|$)}h@vX0arC%(UV^BgGZzi69o zcgXyQw&K6ELH(u84N5F%7RQ4<2^CR#)JSiZ++Do@8%zuN7ngr)$4Ev&>ED2hK&f}W zatA56B{6RBS%^+AL!(8(b@S+FbvQU!@8-To6V}&1v{e=|1jCgD7@tl=FQs?xAMGDe zip|j)+C^Qg?DVng2GdoZokP^Pb|hBxvm0aVXJ;uJlw2VE7gQsy8Qey=Ndy137{UK+ zv4899?<)T9TlU}jg8A?Iviz66Hvez>IzKjOqUjwPW zSuWvHin=kd`h;c@iq;@7gT`qYOCV=KzL&)+6m(rFn08_80xSiW|rANLA zGST5!P{}i4z@L9T+lC~egle=<7)D_>_=;vgkJf@zS=w=Obgb<*2O@BZbxtT^ zbXB}5v!R9gAALb=Mzhi3MDx?^{6}9d(;gpO-Jj6poSUJ@fA!_;;ycug^e=reSE7^+ z!2C;J{gAVP|65|OpkT%U-j+U8U z;`KPE&@%`XKqy^EA!bSqZl!G^Z^C1WL1S2X2G&yh9MO6)D{2tW$K=)OD}W4r^cN_$ z;P<~kfeC8GeXL*o3{H0Gj-KaUHKC%; z9Wc~FXJ&$%#`Wm;nH$$6h^of1DnI9mlssBDgcT3uTo&vXaU%U;EK1CL&`sOu?kf3T zm7IAv+vyg^BWkJLNY&O-%SDN$mZ&AMm0H@`2}*+?MG&DV%~-1~?Tk`OP_ANNdufK>i({0`I zpdOT<3+mIb0uCOq{J^UzQ6L(x(Ku{X9vln} z9h>jsH+;^QO_ZX^RwOj*e+&2Kt9bVF3z@;WCUukcvdc{CVXBozRQ@9DWD&MGcmD~9aM0rPdz#>~M+w56mL$yZ2a!%DAjRbivg z?p&Pz(F>>FrNwd)gt%3=Hi+OKWMVn#jCF#t5|&Hv!>96aH8wy z+Yce$5PaP{aF|!12@JX3(XUl5q4*_IJ%8z8l>{xyTBuQ$qrz>LeYhP9rQpT3CJX6}^ieZMG zQ<@hpk5I%$e+3}oQl;&`HUY~Xr*TbWAi3lnF}b_fs9`i7EFHur4r>iM!A)$fsj#uwuY4zu1AOb>=XslXvNN|QzC;E*B-=qB$SQxa~IJgqbi9J0c z8zkB1_D?`~dtIezy|^VF9Fys(h=Vy;wFesn6U0iMlGZnr_|Y#TIn)DdUYr#xIJO0_ zn`!^4T8jGjBv@kSLD=*a3X3voVhL2 z!jANIR~p-B7w=2dwF^p0&kUML_iVcA^NXmkIZ7zVX}|&?UUKnNemii3c2;6g_6bg)supv;8&B>Xu6`4ouIeg)tvtSAtt(9IgInpo zkEr+X^v=dY=b{v+j92P}jW!xWWC|?fQ}xMOooI+&yr5)nT^DCo)R4R3kKK`1GvunD z-a#K$cDl;d>lI`ND~ghdCGRQR@3ogQL)H{p+(>i$>~VC=vnzZo^5^AbxscxIyXr-D zuA>XkwX&nLCeDH2=3gyU_;sS|*@lHGJyp|C3i|g4J*I0F1qyF@-D%*8nl&Oy#jf2! zo==qjx(v`cp~P(=E)NGtTfj3vicM?g;R>LAIWyFJga7W7=SZEbA#@zY1bMt%khVIgl1QdCdI2voQR?s7p=FwL_fg zOM>3DwQ(oT%(n9`jmLE0L~&b&Ji-OyJ%ME+ry9*k0+>0fY)>RjuSpjn?)I3;1+Y9Q@{z z2!zoSy6vi%&y`|I4cXn-57gCeRUCoDM4|q(C zuX4wJG~QT<;IV3uzF;HU*4kfhWKVLjms5?gl9e9QNOeZ!3~*Znx=UaKDTMpTF)1Xs%uQ~pHh9>b@rSf#vB+REM+pqJ=>@-J#Zw? zk%V#e7Bb2~27MRMEM)>M?Wd6aE^J%d*|u$Xu!itqDn<^T?gE{W``pok|LxQ8JA^Qy@Z)V zn_|U_ru~zjBEx^@P#bwi;dLVo#n6*Q@D-=wUz(bSbxD^RVN#i-70$_%HRWD{Cm$D9 zB8%Y?W=qRXegB&M*uM1TxL~H7?l1$h%#}G{`#yLZ z^wKN$bwnaDD|PNldXYTZqweuUxAd6`-%b2@mHoq0yz-C`gs{r-7eGSyVf#CP)qKB+ zPlMtQyDyp*`1iPUegoj+H*@`ZpxL7WlWW`)*Vn>1l=Aip5?Vk~X|^J(PkqD|TWv~{ z?a1G4^zKf}a@%XigF6(TKX~TzO)){68412Opl_uUb~bE45WOqJ zdN$N<(9##XQA?W?#caxIQWV1!%pCClo*Tt%&8kVouwJH4pW1yeQ_G4*-~s%3bjL+{!NE*pl}0jXK2$;7jfovbg!;~k zo>Vf^SfvmQT9Y{w?fZmc8}sK%zKx-cM#EY%VVHEC4S0}!d-~dcE4vuooGSEyvV=00 g_Wzg8b8PGUcN%YJ!@)^=4nOrhLlu4rYuc|r0K%sH=>Px# literal 0 HcmV?d00001 diff --git a/test/webapp-fragments/WEB-INF/lib/resources2.jar b/test/webapp-fragments/WEB-INF/lib/resources2.jar new file mode 100644 index 0000000000000000000000000000000000000000..57e8424bc69c0ca2f8724e1a191edfeff39dfd64 GIT binary patch literal 17002 zcmbV!W0WM_wr$zAZQHh8)m^r2+pg-eZQHhO+cvuD^>^+)=X~dm`+mH)VrS&e5qqxO z8L`IPYeeK+3evzJC;$);5CG```hoy|Ge`gs09jELL0U;UG5W7@003YB1!-`IzeWK4 z_b`S3D;VX^h<|})1?42gM3q(OWW^q2Cnsd2Y3b%*rD>_ACubUz7#5j#j~wWvWN9R2 z=bXzyAX6_e2eDJB&0f(-$xaHXI8M+}579}J0g^259qt?e|1k+3E@ejHKVKp8=k@PN z%<`u3{P)cM@rLkc|F22NND7O}DT`itTRLKk|JrpGyX9Y`G#D5J1-UTxwDN8Zl#u9$ zfph?vt#>gnYzAZ*ZT7d3VHb$_dPeomJ%qSqNd)d;p4H(`B>N? zdAL~2GI?OBnIHJ@xpmY~Q;+XJ?=sbXIU(60J<9UDyIiaI-P68p!_M#hadmZNO`jL- zA?p5g6~SK7&C8RUBL{xa5PlIWw|Vz=YQ%w?9ZPn7^|q=l2d2%Arxi6LPZxW4K*#IM zlqoYZz@G9NPIK3JA~$cGY^v#3dp=fjdlH^`8D3GHuryw^K;o*&amPlxk|cc}oPqup z;CqrRdgh|K?rhvn898JVgYW80>*W@&y1|rjbqk%MV&mUY49(u*nBX*B!4nEQZs6*C z<>NGj^cCOdIONqL_h60Om~Yj~M$hJI?Bd+zJY=1n43`^_W3s`Wa@o8hn)X|c`&`JH ze@%y66Z}Ib3G$PvCqEPf9TDbCwcY_(qYH3^ZbW$=Q7L8892XSC4shn23zUaYz{Gs0 z16ZfUxbD2YQ057r8mKdpykFk+4akR$e))oErRY%7HN(2bop8JUWE+iCuzoEm1L%~T zU`4m9r-HnG5}8Z(D5f6nySY_%f=(Jd%z0|pX0nR6Tft-a`+Yhzbw!U~dNtMCYCh41}( zt;gA{2S{OX^@aMXc?r!fmK`ufwK$0m(sFtp2Z3>BSE%3!w($9!AFYG@8p0>qmpaGk z@@l{rk&%7PAhaTBdOo~5&Ic&00ZJcmbwynj@eO&5QF`{OQDF;&EfZKCsHf^eq=j<9 zSR=U>z`KKF@H-@q)=`fW543o+zMGZLY^sw?f$ z9uHlK12 z8k*n!VjOp`1JSVUo<_(Hy-LQ5?)NLV=A~%WaAFmifv)@csRU{m<^u+Q$t|#AH;Sez z1n61QDN^IYj6hB#zsN(#MZ60sum`;!F5)%tdfOuVeygKu0E>mP(#Zeoiv~1^4iNhV zgA=|1ViVc7oV*=l14Kt_pk^Mm)==Fq?s{K!@)s)NqIEd`IVu%A zeJX*jQMPFIahl@}wMvmeJ%yN7;0(9!icz>++7%Mvq2o^hrdhM9#{?oGzG@`0%;I&U z2kp8FzoZq(H*BZ@CJe=GHtC8>D)KA+lCzsEuK?kiq%!_#!p>)7hc zSNDn0d_?2p_U4)nFgFuQ1hmu?$}v~J4I3klda3U^U?GjY;Rm4F>2sVb!+}Vube+dd zZkArV6T?`LroK(1k@6p-%}dbrGN!@KP+W_cG@OZ)0PL7vMx1_xkmR6-b_q6syxB;C zIMrw^nCryu@%*GnrMs)IGbMW1Nk2{fFX!R0e9cKo#>6T&= z;NrKO0WU37rHSZbnexx5prT|pST=?PlElqpdGMH%3MeQC_Hl|Y$)^=03z*yA0(jT{ z4gMu_Nf=2Nf?Ugp5lHU2x$j+yciFawG)B;>fGR;>XzRv=ke53+MwW zf+kmoyFd4HhRu=Zahy*MSS!?ft|qM}>~r-z3ciB{0b&r7B;Ho{uIjGXvz@!+!ognibLw&RhFu^Z>F)RK3i^vv|Lz!gjqBP-_%g~Vy3AV+P1fCE86H@*t} z$e@e`+e~brT{IQ4O0~`QP4*QV^(@p) zvFn+7y$`7aQ749!c}}u=@^B1&cI^wE;>mXVhYL3Yru zQn3X3%}Iu?^KtxsP>vb+`_j)T))$(mD4v2!!|O-T=?a3jAmT=9F-HBOK{M}Oty&t5 zrVuBE<&O0zgJl%|x%?9%H$SP4;u&-rZ3F74S}-gs^Ec-&H4#wVph_(%sM;ij$ig-3 zKGj18G^?n4W3xjsHT%dp+x}o$b1I#Y!jcGMXddkWVd;C;n`bu$w%wDr2(-a2>mUZd zS&4X2i}j5&jB>IKY)Gn@TQ~5MW8THB(=#(Ydapad%qiR-ucBiNcXWnJhx8UKB*Fl=2 zY0~^fj@0jxc%FrcXR1mLR2A3tuZaUM1cXacWeOTt8=i`B0$cTIHVz75K9YxZ(Fe?A zV_1gv+t+;p8XDVr#{yrFdXT&i15&+EjpS*&$H`~>E@_7q3VT27SWEBEEc%!zmQyLj z-2hhf(kA!f90>c=`zY*)pDQ|xR>(c_McFCZeNNjFDv4U4ABLr004r4mrn62U;IA%Y zc=hhL=XjX|{Hqf-vk}xULL!mP6+q2m5wu`uOQtn7yCO>WZrIv7vGBF3trbH*O%A*~oLQo!4+b1l>u=3GZliz} zmNkyG)eD+DR6IGGT_Q~HKYfdtKS&TJOOsrXstOu{oV^-(S4kAFi~2C>xlVIMIwhT_ zlW}70F(4J}d)`ML@$+-dJbAJnmGmyOG#wX1@-10lPj$Bdbx09V=62b7Rm{;;KvEOi zV|1ZH6-;fN=gP@@z6)D4LKx?)q0x7+8N>umZ7qt1ALs_^0E}%%M$**s7p)V6H3NJs4WPn<~|*Cc?lz zqbfh_&^EUra*C{Pl~@CoJIFiDs6u-zf=Zp%)t$jlLa=$MSg#)NY;`}VMUI+)fJB1j z^rDVedl39GVMtc&Y^`W90e~HXsBuwf`G?JG-O0P73R)Oc(nza2KZU~p0eeMQdO3~)27K*T0nd44497uQZ&HBkm)tkgldYd_41Nuec=voUkIPpTR3@>A3 z`fZUE7#Pv8Bg4dM!DggEBiP`eXv9atOQTakqUlo$W9j7e(eKHxpd91g86xd~*a3b( zZjU*nO!;xva5B~*UNTtMn{g(iQxXQ)Us#zQPEMSB8`fe)rmhCjsmxo1Xg~&CcfOXR z^Q$R%SVc#Dryxx>nnT$n=L9UXB;0TSbN>cikuy@=*C(A}PyEBje{AW_C z;yAx9-xl$wj}SYqV-*Dki>TV~AsOHaDSU5D1`dp?FC46*0PO90x|ZmvANC<@sQx z{Q|%X<7L|wjjPhO)Un3RDEKe*%WhNT0r;R| z3haCWJV>THq(tF>pj1+W>j4CFj=jAL$w8!lIrv1YI~GO6jeKt4e!juKum|AkRTa5^ z3;Pf})-znI$w+EzPMycx0_sw(yUSXZ5(n=4KV=M_Ej`XS+!;!mcQ=c6b74492@dF7&`Z)qCEz-I8$HzP zqnDp(SfP}qrAgw5r7S!?o=ALB(!W26w`=ulIXd==U2dA#=DZ(U{*3yaZbx zl&M|>I`sBwV=8$?PG5)!QO36QJf(P>Vr^kk1Y@oi={_xC75MUGIeSJ)1s-q2rIQe^87vnbUETWt4wSm`{%_cvv)L_pS4-aN$ z>%`oc-sP>Lnrdmo_5g6;!NT5<5rEGM_AUtrctZlW$Gk5)UL5gD;Q}#~HA)UK;l{k9 zj#KUjJ{~}ld@pxrCeU0g_{cycAoQ(*35+^qlwBo`ZcCJo}p{mI1^k+ zPgSW>bYpu}GRq#aWttDE=5^tkJHUj-m7>Ao%@^jXk_`t%f({^x-Ah5rTzH(Qak7jX zHA$nrzN?YCUfa*~CH8u%%_hc1CK)ot5Zu7^NG=I$RN%E{sXDH(D8BJ}33u;!cD%1* zq>V;&aCw&2n+91h@=$OzpmONfaEo2a;anthZ5kJ97dY>{Mp2YyjR1)^Rh-J)11EiY z8Rq6FtUZBd@3Q@@b7BViCWx4cs>G93QBTI-hKy0CAp|{FzWfm_0>oewrcg#fQ$F)n0p;9og{;pK&xs|h0D!jMu}&cH+?N!K3o|VUtgQHQ|{u< zr-U$ODMh~R>@BOFpgc<3oRo7I8DjCRpvU%ME9z3!8(Ajtc6%#;!HD9cNEdn0ZZV{z zb?hvkloCIfBYHUyCr%l%4oQ+eVq;DiBiE%+F??mk*JmD_Qcvnp>4m=59!$Zsgij=& z&&(iL8fLo{AE8F2m;%NHkXoOkNBA++cV8ARIxW(Yjhz(5?->@6%1gDlL_w;GzT|(= zrs&@n1A3MUYce`k@{pYBWxrk4Y5j0kM6+ z=8^zXgv3mc0S1KNA!(F6RCDT_$3m*-j-O{A=d4}2Inhc(y?26QrLl>56P^9#*SKuJ zc_}&hBh-xb6z7H3wt?@I=)d;L_QMdfSaPi~14Nn6#Wu$|^N)lZ!mFYRcnU$6SP5A5 zU20QXN2N01Nst$?#SY2%nc?KkP##?A`zbO7MG#A0A zB)J?-&l~GoGs~X-jMSeqSBkh1LYNl(#{c@#)Y+__2aTifbCONUleWSk36wxB>S0F6 zaQ=d1LRCAww8Z(m0~&0=T>nMOHa3CRD&^aS6ElWQB%FtjO9*GwQjpx*gIJ z!bxwDDcch{!zsdQ;>?1eWmWRjhH7Y2%o$-TF6eIAgaO`>a7w zm`0m*1LX|aWdlG(?CDVc3CJNCVa39T$lb%aGE39xB7hz&p{m6Qtok<#|S9QksX z)>;9nY_gWbNL&~3)@EZ^k@qtO0tc0T{d|T|0(t8tS;$~ICl<>?r&5}UG*E>;qrF~) z@J4o&{*`6SsWb!ghJ$enrTb5E&Zi1T@d}o{z?MnS;chITtrBfgROIc4AWw4goWp)_ zn%+u(ZA+A@G)IU^eFLhZtfG&0>6bg}Dm4zR9$R7 zHRmMQwYI1{@}&vn4Lp*9AVpCxC_Cr$45jjQ7U>7xUUQaPo=hzobMkO zc86F}>uj_(+k5o=!Te0~a!hba?aD$w8eo+5c$kSHdYN~Lxh$e$9cVg*9U1cP%ZX!zX>u%l(YV0m zYE{GO+9n?H6Vk zXy&#PV)nTONad4NOCf$K96N+U@R$VduOFe5QcrmC+ZV&hRZZG$pASU3&M@mx22j)6z zkI!zcIIb=ZD`H=+`<%-{!EA^sLXn@>P%&KMXL6>5{xV zo+DIHc#8>^z$w=+JzT#RLD#ii6q{myffpeNCMFyEs~L2S6Vuh)9s{?D<)z!lTP4s1 zzuGY>IgBD??5f>3+({QY#T=*^w>=bbRKj@ie!lQF{)(0V5N=iKd|1faJo_>CCJ{Dw zQtt0KEMb~BZesZ9#bclF@_Rw~rThEAFKpI!d=WS&6Gk8MCj!Oh_Z7xZFo|mv1%@_^ z++g^@?+F02F8;4o)cY)$FAqrh__rPYiyq*1zI&_Le$eYjFD6iaGcPz0o-?Pdj|cd` zoIQw<-xH9v2x^6hDLx`(eE7amaUfn#J_6)mMzfF~0MxzU;=I}GCjeX=Z;L?8Se@Ls z-Pj69_;#@qUKnyV&N|u!^tJj%d}Glago#Ej zNIM$X^m^q(;&ku~#r5*AW;^W$4*=_dK{=#X0tP#W&6%MRI`%}>WRR*Ap7BMh;EL78 zJRem`JV`))BPGI~qct}!QITPT38U>wSV2GW^{aae!@Y4{5FjYTRk`+lT3K2SzsNPH zMkitLE`B4_E+B(ozhPo6_2iFz#Wt>Z8b%5Mg@TLEfuQB|mt}lz!pN0AeX;$hR6;5V z^r7C^{N-5hZ_T%lsHT<>zT_q_*jx+{mgVZ#$-6ZZ z*m9E7U1a}TPQ>?m%o%oOKC>#BtP)Lae;=C#e~f&`Tt9NTenr@v;H@EGVvhI{e5oX=(%h+0N~f@7RAPew=Kb0#02(p(~CQ9j2IB?u*k!VcnsUB zW3N(wqMPPz<-sZ#vFCB-b-8h@-yZPnvUj839d1EI(h;|Pa=ErU@HVgb0z;f%-*V+X zln`!!XkC}53GJW*WHgLZ&Da|W`g8Fp8cV-W#LkJ*ij`<3{C5$Jc&Q!=^<q#XshxJvZ7=l_~8TuS6(SmV=;p|A?vTJj*RflU(>9$x6I8W8CAhMSQTyg0g>r&y%qzPq=NXjG?EOCZ~h zx)K#v2_OlbY|MHbBIs*9UmkR1hWNX=D^m%3ou^bAMK4h=<3evbQ6qa^m3K@|F22}^ zc3Yt!sA0v)LV8}(2)JV0eBkiF;2c3;WJBo2qaSxsnmvH}we)w8QB_mH@YgF7`HE$K z8r5qp{5Ef9M!7Htj+EIjhG&*1#c`HIO75yh)p}I%@h<|QHm4LaKVUMQYMrFoPMUZ> z{Uj)OW{?(0ML^g)L#qUUSLaYR0!pvMTO9KR%9=iyv!S6S{NCK5!ZCzO?#4+O|0aC* z2mUh?ab&yce9ESbrR#I^khZP$OlZi4KJ*py0F2!=7MDlh*4%Idy4 zwZljqXMzIbz$+E7^c=Z=5@a_I*R_qKVJPr{15mr`SuJ5Cp-uhldxxxcK;b9K4o6eY zv&RbWpRdXr!eAKl4*dnrOc&*jWt}d@D!4LBI96#fQRS#_DCq%Sislm*R1zrO&ER!+ zuNy$Gb5~`?Y=m%;EK2cX(iooLMmf!{UMvV&tbdb|+p06fiqvVxvxTyO2u1?V+I7+p z#WFjyC(GwF7F-eS2zJOs``h9D%j@#%fuAx%C|(oBBV;fvp>uRkaMZ)e`q$wUDOe>b z4-B;Y45xF!8%hR^yQiDNOu%kTj&vXg$ad(aya&c1ZkF<^vxi%+y}e^9J1`Vc4jdjD zrYjc4$m;JDk>QOQI1Ik-L$2`wV5slzco&wJ>(9?1J1)#9Upzc`vKq!;a^}Pqj@)PR zAB_zn%en_UJ6dufVOax#duCi1vGnu|Zd-5fsU3x=45!y$l#H%HGJ)6Cc#bJ@+(mt% zAXA{$d=_#hQ#q~}YQX$Rfq>esnU#I$>bpD<6FsxQJ!+3j-*U&6mrF-pmF$2UGClm8 z!(qnCeh(vb!}O;HPC3^4Q9x+1yisU{BY4BUWq4vQtAnrq-_18KT+yQNTU|ElvhZt; zObq>n!J%u}5c%L=?ORzkN_Tref|~D{;a}~G{gGk#9XnX|R&x$EAqNXGXwLR2#Cqqz zbcagjMbta?W9P^cdkCwKa+A|S7Dk)wQiC|aubzW8Wlnv|7#aVax5;Gk{xCignN|Q&q zPQ#yL@Qoe;l`UP_(XSIJO&)C1AE6q&@bLLM@-NO+*&i0*g`InSRoNJKn=60d#Y+n` z7?u;wi<^cOC%)DdCqc+{#T`fmkR{(f%eH5@B!TZx-8~XEYD2Jl4L#FR>~FlkquZC7 zi<#wjke0g9$Rov3x*#8W(<9!E71s6rA2W6IqX=Ul~iT>pmkcuQ$eW>3=%v>jCi@s+bdV#jDr zaB85s4oj3j#I>@!TMkf3HrG2B^RExh^+|V<@9a+)C|NRl^*^^VFu-(M_ z{M&Td`>ob_!0`6BvfbI&*Zs{*{>*Vu^YF+7Q{HjqEhv~(X~mw=?ZfWlC(r>m3QTxE zT=&x6>)~PJ!OlLJEb3}N9Nm}%!%dvew&!*v@wpaKsO4M6{PA=R_3IAz0=kIpv_hUg5Fe!5#-i)ZF%G_3Gj#cL2&=7SfqbbQjEAw2m;aazr z!+*Gr&*F3^&NE?KlGZ#q)baOfg(R9VmdsBP`9>=O<-EQkNJS90PKSF#id*>{=gVKv zVxA)~H%@20w;I z-K^nnL*V`7ggJsP3Afk@)}JF*-w8Eby=3Jv)oeDHE28FqiX3}evi@ki0(RTyOcjQ` z?TMnBO#Yf$=c^uZNzWA7h^4e`mP(*{=1A}L?>>s~a+NV(&bIbt;*~~$n~c=-@TAs< zl+2NF1J>TfRfeL9f4H@np?jXzMTe#eAM#_b;Hj0I4c$?FZvBNhlkaDbfGfu>UODXv zr?E-URDH8LeaF)RUZ8H%s~3)i-Kd2?<|;OAIf^?ReW5X6iZPt0Qk{>9K2k!wC=;EH&SBY}!rHEL^AIc`Ibx z-;BydceJonC>*-|HT~OAJ_FWht94FBdtG9O=u9H~+j|O6d^irKi$efB5~W1iiRef% z!7#eP$rwR?C2d#`uFJ*SQm-?=8GPNPR|BL|I}2~=(R`r)nuWY1e$Ew z;E8n>uZMZawlF5$x<*exIS*~cEf*EMrH|GA&WF|}`UaqoiWFsg9)>~^ZU@Z_+=;Qk zUbX9SGr#G0M>HbMF>(*%>m0m$rI-Q9XIB54b+;~43)~H4LW7ZzT zs=jh*#>Uj3nm6{eIUZQ25C!9&!$&dJeQ^JG4)JChirY|V*z-q5kb35W#rHl_ms;Pg zF45${DZ)rU8W%W`4r2s+(-?t(w^&J0vO#Z*y#DkrstOB$bf&;|Ix|RamdC&}$f44Y zl8ex5Z>at3()6JOfj3bov#|(UWv*c)h*gcc=2dZo;$2bFKB44&b8}9T*0{N70iclDqO_bf`p%A~Sx`bc zC9h5?V0M0Xsjv>~-FYa;p?zi32jrwrgumtLTD#2Ojhp1YLALvvkhi;CWAVJ=@FO3u ze5IxsHe+15;*qVMaU`DheRU*OQq^o;1ih2pRS5dE4^p3+p@5kHuD-=NuR#ppSEW6_ zqY`qaxT-iUs#XL{5QAgi!792^P(0Mr=kR4UtDOwQpgI&+y1`)d;LDv~RM1io-mbr& zZC|6)vk9T17?|%8LU})u3*_)g_O+>eRWoG!hRNd224>YEj!4+us-5+3#@>+#H6heR zFOnl*GfArjc}*IA=DBqvY>=8q5;1bRjRBpxp-!MaoMHxL$yk!WU2^5Z{?}{}b*mp} zEUE3KZr$OtR&;)s2m1(6DkjTgoU4;+i9P|$J9(58vE|D^d1nC*+XT5DQu+D;vHT*b z8-n651A`&qve#vu#$ES_2VGdeH&RbuiY)o8D)a zSppMTi-qa|qGO+46$yX-w16edl9|tYKL%owU!NDz-#|0B}uyADEs?!-L&h(ZSg^1y?$s^MVVCqB*64X2&Br@ ztA&h|Oy}s_w7}rYz**26XskF#*y?>px;7{@wlx))K?1AgwvY63>k`fFas+&3OBueY z1yOW}RA*qW0hZeYTxoL9Plpm9C7+1{aiOldwQ@FqP)g#{QyN>5q-QEw+rc?vsB#&+ zHB*XrKu`jMg{P2yrNji70Y)Olg#{&HV0~U~X=u+AagqdFA(e$E;r?Q}pB-!9shaCx zsJE4tRVZV+z2{My-0Z^*%D?X|1`KI5Cn{6b@R$uc31hnhzPosTpabiUeG9g;4hjr@ z>(FhFY}-r9CBzQ7M9mXql8b;R?O>@8101+bM|0oq_GdPm<*jXD+=L5b8y|s3D`#=W z66McVaA~V{Frm5-LdSKw`EEE<++HDeEHMov*5D&7D#SAth=S(Tlv}4}*UUZ*DxHK( zW?nkDChHF@X@kn>IPYOR=yZ|x$wMco0bOmx5xwmOZW^JsIOme$I%_Y+u3lEh#hBaUsH)As?PfT$ z9DCOUdc;a@DZ%}qey(cbJq;F_OWZ9JePY~Hnv;v-8SjgP)uxn)Rk+aO$#`yr(b{%q zI~c>ov=qC+D!zvc4PC(xXSr97_tI|A?`EOUo@r@vuXCPcXG-#gcmO;S1m!__ILQ+s zibG8ux7zk}Jcg#Uq?Zit2et%g+Hdty*-wF!ig zE#n1drJ?`62 zi}!axjLpO>yv7*Z7lSZ=@y&;_#gi@Y?<+Tf0bn^WTkw$u$PO_5=aIAANJIYTo?2i& z_6FhI9^Pze0L=lggS|ugMj{+v54oMsr}`URa0f<=fqv6HT<|M~eax+qhl%WrUUhkS zH&FrA1O3i0|4d^D--I_PzkX;eM!?8cVy{^6t`?A;Og}?d1!6mR5FO6gl3ab*>8Qrj zUQDtSQ(%7wrlk)o*qFVL_k-9*gr3{LY7?H;Q}_{{CW9zqx?KYg8JsJU+ zioZhz3E3q!lFdY)p`AJFI9P|qjiyYx>7rUJ z;l+pC58u-?e39Gq>tC-}A+#JL81QH`VogW`{g&0ok}kAQ?HF%QG*765ZVHNov~kp8 zEPe=SvZe1}DQQL)XNy5MgV<}HyFOPvsZTl=3OPHQPN+{=5?hI~&p3y1jx};Ea@f!G z`{_w3HzuZG?7pa)8#E|?uzw=eq&^!4Iq?+E!7wJHHM7a_wxD;R&v{gRE*#w#-vqA> z6_cyl(idnW_y@~7D!X|Q`=f`G3AulUnY?V3fswG&jx)3Q`$r)375ZU)rb^! z{L77lubU@h)-&&w*5<2Xp|9hsY_b;0u)>15%&dtarF;QaxGEo0RXc4L6is)g!kPv?h0e)hIbfdv;C`WQ#6%`*lnCaybGr#)+1J*jf^O^q;j6*cXVQ6+`T!`y2{{fgz9ii@To5i!C=`$ zS@GAFg7v(eYk}hJa=FinhmdT7@9aWB~{eKE85{(UJ-4{v0li)T=1jZybf@+ zvHbXPe!n@$G`}64p@imj;pXIEmU<#{^UHRI_R%YQY~jsA{F-E(8!-O{Ci>>h)aTkG z1*+X;%?w(A27u8c5|2p$jQ$3?G0XA{sO)BBD%4Pl*bM;qlX1t%RW`L7^wAClHG9$SiM7-5LJrp>30c>HzYFD+sNAIH&>C38J2Fn;M_7&_8gRi z%O}Fiszv&8E-w?%)zW1TpLE%9a(#*eJUrw3dDkJgGS-Nbreuk3%cAkL8GpE;;TjmL z^IOvOUH5v(HFre!^Y$^+bVe7KG^^kwE2f$0EibFON|4TC`26gG_lXp`sM9rJy10Wk zv?{w{&W=Tr-GhrN@mO6zQ%(0N2SH>{*stYfAkvgRn=i%C;VI(#HHrKEfleOPmE!D; zjIv!m`56GMv(ikhwX;^ic59vt=yIgE}V`5%|#0%bx)IK9``B0 zo|C;Bo>u$zRyC{cy%}EUBj{@-rFjesDHv+tg5#m_=WZ1EpZm;y3_?ZjzInd!MM>dp zdL6WG*?|iSUuJeb=A~Pyu5+4_ug2#pCpT3{g=?GwLfR@y4c)^ugah=r$2K=0!uUuP zub^9>V1;Hr`s_cIUt~cKp=kzxZZab=7&4Dfe@DV!u2`pG7VurvbTCz*eacyD$Zexh z+qx(-X5G4?TOO;nk^O5gR`9JYC5bl-unzef|mYi`_cH5Z)hRt zMc2-e<_`}Pzxl*@`B>eqd;bxt9kHe|pn+O9%!iS(ej(fS}U@fX&w95$E zS8CzpKjFqaxS;$uNqW}z{5AtZg{9Gpd+b#dHXK5sLU3^rr*bKz@FW-SvFc66vJb1g zNr79yCEg3&m8Zr-+^&%_v$lC!dc$)8bU92p*w|{N8fEZSu79SV45C-Sf7kFvNwb?QJtdDLx=q?v7KI<>4bV>b~* zx&TQ|C>Y;KL8qt0D3)u2dp<}bw|g3(YhPwKf0`g^n6yl(t=-^RPq|03{WPcRiF3zx zD`Hc`z^95(8gyJ9MJSu5)6{w~?3MY+|+j78lJU0hz|%JP!y+JwbMLj`gArt%pZims2ag+IbTw~LeilO> zfE|8jtD6M2X0Mm-IOpJK8!s7#cg$|62oCo>llC8bE^o z9q>=65S@jiow4$kV?`-zjW0unap#GfOumr zYC!X>StJNhn%efb1y36f2nL|*k;JCIQyT1g0ClMjO`o;D@7-4wWCkj>ZnFf zE2{Kpnp5sRfCdrDrg0?9YdC4?>MH8;nxE1bSB;~#)_I1cxjxd@OA=t_z$=&u^)bm1 zNG0E5kSW$1#I;cT_HmQlqvgRH;D~mC#EH3e3d<>A;Tm*W7sklh6-VK(0ia~q?}Im7 z0~|}lAv^YNSRj%`a+7_%gfXd)!&Fx`rj`m)`WCEklZIIr|fsd@Xy2bY==GxMra$L=oa%-M`H|yG~^ac$^HSW2boDDRWu9d0bFa+e}8pG=L z%Fh7j2I2?1nYM#m2=0L14&JhXnfeX-m)JGk1j+*KkJ_~y`0qMXdgFrsmlzZKuNd30 z&0s+AiB0_8Ln%|D24#A(kEEJoo13K~0&S;=h12TAUCp)@d)T>w6PjSriJRJT-HA2y zMh1Dn>>R~O8UX49(>YZL|Y*P^yP zDtmJJ0Ut&n6}g&wPX{7_==o}tG9-`9F@cpAXJeLs_O~E24L}klbZ;ybq-r+``P6FSeY^?r8W|SJ0RJaiziEZ~$R`^i5Q4tlsUx6t7D#^if#P`{+(4y7Q z>Z85s9SH5rz7HhjX@)D8d&et&5KeVxAwwHHZlC`r7kRVq3^+|dSE2VH&7E0bLh#`` zQ1hX>nOa#Yzi3x@3=+1USSq!B5~?OUBRiFJoT(_EqJ58JdPkSHfR=veOuj2m*FV6qLd6<8vFjr#(S`!C-Q)^=zCptH4t7sKP`9Iv{BfihSc@LtL zfHF%myq*tJidH3ER2+2|w0W5BA$>XNBJpwK>ViGGI*^<)YP#e4XIm?{uvYD7rkX0k z@HFk41x87mY@-;~$(;T@G4X=`$$aDC;Kkx`@-ZR~z?_{on;ICe#|EJ|#q?g}V69P% zP+X75$s7bhuE@m;s*w*wiBK3IMb$`=h9Y*SYDl4Wjp&aGRVSqH1Q18W^=i}|WqYkR zB^{J`;fl>ud`u*UK{KP(}8)xTy>_M1J$nYh%SlRb+{1bxpS+5 z!`2~9gyn-3< z5w#2G)1?)X9w*%MY$rO2rL0ZoKL$`)-I{>akV~Sf!pz4sEOQ!0)#Y(CM7z+203Q21 zp%-3Gn~ZS$ z$s5y!5_$SlINERllm<%SY%GocgrH4Ec$;H0c415aE^@~*tBKbX^DnZ95LAfZ!U}@R z#Klso>Z-DIh-0+kLsWK~^v9j{(zu%q?)m+3U23&eO0ae(mt*g9gdah(1C{Aj}>((HnqtUA#84{50R;w#>g3?V~8*DF&l|( zpKlqM52zfv?2taSlbDeSW(UEUFjy5c3|^2)*M?}Yk!(d+4`hH2UZl!?NKP{Bq&zoyv+V!^05}W$_38frit^&z literal 0 HcmV?d00001 diff --git a/test/webapp-fragments/WEB-INF/web.xml b/test/webapp-fragments/WEB-INF/web.xml new file mode 100644 index 0000000..3b71550 --- /dev/null +++ b/test/webapp-fragments/WEB-INF/web.xml @@ -0,0 +1,179 @@ + + + + + Tomcat Test Application + + Used as part of the Tomcat unit tests when a full web application is + required. + + + + AllowByAnnotation + org.apache.catalina.core.TestStandardWrapper$SubclassAllowAllServlet + + + AllowByAnnotation + /testStandardWrapper/securityAnnotationsWebXmlPriority + + + + + /testStandardWrapper/securityAnnotationsWebXmlPriority + + + + + + bug51396 + /bug51396.jsp + + + + org.apache.catalina.startup.TesterServletWithLifeCycleMethods + postConstruct1 + + + org.apache.catalina.startup.TesterServletWithLifeCycleMethods + preDestroy1 + + + + envEntry2 + java.lang.Integer + 2 + + org.apache.catalina.startup.TesterServletWithAnnotations + envEntry2 + + + + envEntry3 + java.lang.Integer + 33 + + org.apache.catalina.startup.TesterServletWithAnnotations + envEntry3 + + + + envEntry5 + java.lang.Integer + 55 + + + envEntry6 + java.lang.Integer + 66 + + + + injection + org.apache.naming.TesterInjectionServlet + + + injection + /injection + + + + env-entry/basic + java.lang.String + basic-value + + + + env-entry/valid + org.apache.naming.TesterEnvEntry + valid + + + + env-entry/invalid + org.apache.naming.TesterEnvEntry + invalid + + + + env-entry/injectField + java.lang.String + inject-value-1 + + org.apache.naming.TesterInjectionServlet + property1 + + + + + env-entry/injectProperty + java.lang.String + inject-value-2 + + org.apache.naming.TesterInjectionServlet + property2 + + + + + env-entry/injectFieldNoType + inject-value-3 + + org.apache.naming.TesterInjectionServlet + property3 + + + + + env-entry/injectNoValue + java.lang.String + + org.apache.naming.TesterInjectionServlet + property4 + + + + + env-entry/lookup + java.lang.String + java:comp/env/env-entry/basic + + + + env-entry/circular1 + java.lang.String + java:comp/env/env-entry/circular2 + + + + env-entry/circular2 + java.lang.String + java:comp/env/env-entry/circular1 + + + + env-entry/lookup-invalid + java.lang.Integer + java:comp/env/env-entry/basic + + + \ No newline at end of file diff --git a/test/webapp-fragments/bug51396.jsp b/test/webapp-fragments/bug51396.jsp new file mode 100644 index 0000000..9863826 --- /dev/null +++ b/test/webapp-fragments/bug51396.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp-fragments/folder/resourceC.jsp b/test/webapp-fragments/folder/resourceC.jsp new file mode 100644 index 0000000..df079ab --- /dev/null +++ b/test/webapp-fragments/folder/resourceC.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> +<%-- + Resource file that is present both in the web application and in the + WEB-INF/lib/resources.jar file. The one in the web application should win. +--%> +

    resourceC.jsp in the web application

    diff --git a/test/webapp-fragments/folder/resourceE.jsp b/test/webapp-fragments/folder/resourceE.jsp new file mode 100644 index 0000000..f94a9ca --- /dev/null +++ b/test/webapp-fragments/folder/resourceE.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%-- + Resource file that is present in the web application only. +--%> +

    resourceE.jsp in the web application

    diff --git a/test/webapp-fragments/jndi.jsp b/test/webapp-fragments/jndi.jsp new file mode 100644 index 0000000..ff7e506 --- /dev/null +++ b/test/webapp-fragments/jndi.jsp @@ -0,0 +1,31 @@ +<%-- + 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. +--%> +<%@page contentType="text/plain" pageEncoding="UTF-8"%><% + String jndiName = request.getParameter("jndiName"); + + javax.naming.Context initCtx = new javax.naming.InitialContext(); + javax.naming.Context envCtx = (javax.naming.Context) initCtx.lookup("java:comp/env"); + + try { + Object obj = envCtx.lookup(jndiName); + out.println(obj.toString()); + } catch (javax.naming.NameNotFoundException e) { + out.println("Not Found"); + } catch (javax.naming.NamingException e) { + out.println("Naming Error"); + } +%> \ No newline at end of file diff --git a/test/webapp-fragments/resourceA.jsp b/test/webapp-fragments/resourceA.jsp new file mode 100644 index 0000000..8a8d423 --- /dev/null +++ b/test/webapp-fragments/resourceA.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> +<%-- + Resource file that is present both in the web application and in the + WEB-INF/lib/resources.jar file. The one in the web application should win. +--%> +

    resourceA.jsp in the web application

    diff --git a/test/webapp-fragments/warDirContext.jsp b/test/webapp-fragments/warDirContext.jsp new file mode 100644 index 0000000..09bbb16 --- /dev/null +++ b/test/webapp-fragments/warDirContext.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%><% +try { + Class clazz = Class.forName("'P'"); +} catch (Exception e) { + out.print("

    " + e.getClass().getName() + "

    "); +}%> \ No newline at end of file diff --git a/test/webapp-role-mapping/WEB-INF/classes/com/example/prefixed-role-mapping.properties b/test/webapp-role-mapping/WEB-INF/classes/com/example/prefixed-role-mapping.properties new file mode 100644 index 0000000..2758c7f --- /dev/null +++ b/test/webapp-role-mapping/WEB-INF/classes/com/example/prefixed-role-mapping.properties @@ -0,0 +1,17 @@ +# 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. + +app-roles.admin=de25f8f5-e534-4980-9351-e316384b1127 +app-roles.user=13f6b886-cba8-4b5b-9a1b-06a6fe533356 diff --git a/test/webapp-role-mapping/WEB-INF/classes/com/example/role-mapping.properties b/test/webapp-role-mapping/WEB-INF/classes/com/example/role-mapping.properties new file mode 100644 index 0000000..5091905 --- /dev/null +++ b/test/webapp-role-mapping/WEB-INF/classes/com/example/role-mapping.properties @@ -0,0 +1,17 @@ +# 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. + +admin=de25f8f5-e534-4980-9351-e316384b1127 +user=13f6b886-cba8-4b5b-9a1b-06a6fe533356 diff --git a/test/webapp-role-mapping/WEB-INF/prefixed-role-mapping.properties b/test/webapp-role-mapping/WEB-INF/prefixed-role-mapping.properties new file mode 100644 index 0000000..2758c7f --- /dev/null +++ b/test/webapp-role-mapping/WEB-INF/prefixed-role-mapping.properties @@ -0,0 +1,17 @@ +# 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. + +app-roles.admin=de25f8f5-e534-4980-9351-e316384b1127 +app-roles.user=13f6b886-cba8-4b5b-9a1b-06a6fe533356 diff --git a/test/webapp-role-mapping/WEB-INF/role-mapping.properties b/test/webapp-role-mapping/WEB-INF/role-mapping.properties new file mode 100644 index 0000000..5091905 --- /dev/null +++ b/test/webapp-role-mapping/WEB-INF/role-mapping.properties @@ -0,0 +1,17 @@ +# 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. + +admin=de25f8f5-e534-4980-9351-e316384b1127 +user=13f6b886-cba8-4b5b-9a1b-06a6fe533356 diff --git a/test/webapp-role-mapping/admin.txt b/test/webapp-role-mapping/admin.txt new file mode 100644 index 0000000..1c188dc --- /dev/null +++ b/test/webapp-role-mapping/admin.txt @@ -0,0 +1,18 @@ +================================================================================ + 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. +================================================================================ + +admin diff --git a/test/webapp-role-mapping/unmapped.txt b/test/webapp-role-mapping/unmapped.txt new file mode 100644 index 0000000..292952a --- /dev/null +++ b/test/webapp-role-mapping/unmapped.txt @@ -0,0 +1,18 @@ +================================================================================ + 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. +================================================================================ + +unmapped diff --git a/test/webapp-role-mapping/user.txt b/test/webapp-role-mapping/user.txt new file mode 100644 index 0000000..0fed8a4 --- /dev/null +++ b/test/webapp-role-mapping/user.txt @@ -0,0 +1,18 @@ +================================================================================ + 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. +================================================================================ + +user diff --git a/test/webapp-sci/WEB-INF/classes/META-INF/services/jakarta.servlet.ServletContainerInitializer b/test/webapp-sci/WEB-INF/classes/META-INF/services/jakarta.servlet.ServletContainerInitializer new file mode 100644 index 0000000..19b37e3 --- /dev/null +++ b/test/webapp-sci/WEB-INF/classes/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -0,0 +1,16 @@ +# 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. + +org.apache.tomcat.util.scan.FooSCI diff --git a/test/webapp-servletsecurity-a/WEB-INF/web.xml b/test/webapp-servletsecurity-a/WEB-INF/web.xml new file mode 100644 index 0000000..c41f52a --- /dev/null +++ b/test/webapp-servletsecurity-a/WEB-INF/web.xml @@ -0,0 +1,48 @@ + + + + + + + Tomcat Test Application + + Used as part of the Tomcat unit tests when a full web application is + required. + + + + RoleProtected + org.apache.catalina.core.TestStandardWrapper$RoleAllowServlet + + + + RoleProtected + / + + + \ No newline at end of file diff --git a/test/webapp-servletsecurity-b/WEB-INF/web.xml b/test/webapp-servletsecurity-b/WEB-INF/web.xml new file mode 100644 index 0000000..447fc9c --- /dev/null +++ b/test/webapp-servletsecurity-b/WEB-INF/web.xml @@ -0,0 +1,43 @@ + + + + + + + Tomcat Test Application + + Used as part of the Tomcat unit tests when a full web application is + required. + + + + + + /protected.jsp + + + \ No newline at end of file diff --git a/test/webapp-servletsecurity-b/protected.jsp b/test/webapp-servletsecurity-b/protected.jsp new file mode 100644 index 0000000..535c611 --- /dev/null +++ b/test/webapp-servletsecurity-b/protected.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> + + Protected page + +

    00-OK

    + + + diff --git a/test/webapp-servletsecurity-b/unprotected.jsp b/test/webapp-servletsecurity-b/unprotected.jsp new file mode 100644 index 0000000..d1d95f2 --- /dev/null +++ b/test/webapp-servletsecurity-b/unprotected.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> + + Unprotected page + +

    00-OK

    + + + diff --git a/test/webapp-virtual-library/target/WEB-INF/C.tld b/test/webapp-virtual-library/target/WEB-INF/C.tld new file mode 100644 index 0000000..1c70809 --- /dev/null +++ b/test/webapp-virtual-library/target/WEB-INF/C.tld @@ -0,0 +1,37 @@ + + + 1.0 + C + http://tomcat.apache.org/C + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-virtual-library/target/WEB-INF/classes/META-INF/resources/rsrc/resourceE.properties b/test/webapp-virtual-library/target/WEB-INF/classes/META-INF/resources/rsrc/resourceE.properties new file mode 100644 index 0000000..6abf389 --- /dev/null +++ b/test/webapp-virtual-library/target/WEB-INF/classes/META-INF/resources/rsrc/resourceE.properties @@ -0,0 +1,16 @@ +# 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. + +resourceEInDependentLibraryTargetClasses=true diff --git a/test/webapp-virtual-library/target/WEB-INF/classes/rsrc/resourceC.properties b/test/webapp-virtual-library/target/WEB-INF/classes/rsrc/resourceC.properties new file mode 100644 index 0000000..4f3b042 --- /dev/null +++ b/test/webapp-virtual-library/target/WEB-INF/classes/rsrc/resourceC.properties @@ -0,0 +1,16 @@ +# 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. + +resourceCInDependentLibraryTargetClasses=true diff --git a/test/webapp-virtual-webapp/src/main/lib/META-INF/B.tld b/test/webapp-virtual-webapp/src/main/lib/META-INF/B.tld new file mode 100644 index 0000000..1e6437b --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/lib/META-INF/B.tld @@ -0,0 +1,37 @@ + + + 1.0 + B + http://tomcat.apache.org/B + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/lib/rsrc/resourceD.properties b/test/webapp-virtual-webapp/src/main/lib/rsrc/resourceD.properties new file mode 100644 index 0000000..4b66382 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/lib/rsrc/resourceD.properties @@ -0,0 +1,16 @@ +# 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. + +resourceDInPackagedJarInWebInfLib=true diff --git a/test/webapp-virtual-webapp/src/main/misc/resourceI.properties b/test/webapp-virtual-webapp/src/main/misc/resourceI.properties new file mode 100644 index 0000000..051b715 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/misc/resourceI.properties @@ -0,0 +1,16 @@ +# 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. + +resourceIInWebapp=true diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/A.tld b/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/A.tld new file mode 100644 index 0000000..a488ef8 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/A.tld @@ -0,0 +1,37 @@ + + + 1.0 + A + http://tomcat.apache.org/A + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/classes/rsrc/resourceA.properties b/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/classes/rsrc/resourceA.properties new file mode 100644 index 0000000..d8cccaf --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/classes/rsrc/resourceA.properties @@ -0,0 +1,16 @@ +# 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. + +resourceAInWebInfClasses=true diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/lib/rsrc.jar b/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/lib/rsrc.jar new file mode 100644 index 0000000000000000000000000000000000000000..f725016d0a7ed782635fc4a421b25898a9a64c59 GIT binary patch literal 1936 zcmWIWW@Zs#-~ht({ZaM|NPv@pg~8V~#8KDN&rSc|DFy~+h5&DN4v-2asImZ@nni#r z;F^6M{XE@VgG2Ou-9G!CIql=Et9OytTUYDcne&^246YbIcv__A<*VcAd$DvC3+IfN zl1HRxYGhc5i9A`NRq;&qb>^p{k421N+iU(X9kc8nfqGl@1dSl&Vc2we}!n6JzlX{)qm3KC-?g&y*0UOkWk6# zGV%Cs<+U%8f~S6H@+#Imx3{q14P%$jvlx-*ag)ydy?nWU{-2HP;vY4tp8r;E&^#ZI zaps_hL3WGhS)*UxPfu;}QM_}??a3Jd1sR`9x1AR#YfWHs)LOB`iRt7LgD!n(=Y^Y? zS;91qaJ61!bm)3xKZaFt;jegNw3jUVAFZr(Vv!G5PlH0+##MpC71L zs4Zch{pjoTdk@oZ1Qj0Lrm&>Z?XjFup3xd!LDjI(*djHfnyFh~JH*+1lPJoRSZb`! z{3OescZOu(!oO>KSDrt#A@_EJ<)+3ESBnhplxN3*EYL>6u->e-<$W)^)!joW-#FqY-~1}5VCkA$5UhQA zzDLn(rDtUe4u9iec~f*_^E$)F+Vdvr1)8jJ&``(yk<*Q2V{slfB7Z2^UvnqXS=1W*M4iB zedXlGj~0JE=FYnGLhD)a!rr^Rv1^~D--`83eRkuo`{V7aKfd3y;po!;YhL?JPs;jT z^2vnH%u4>&d%5n(32%;y^zfwqsWvCJXMoH9JRlaY4h&cYoKUL>&*YY_Ktw+O^3m``4?G4Gy{cHcz@N zta|k~-{Y+tn4?bByuPj8CAU~&^{t%6AAHw%CohYCx#Rn?$F21r7XA28dNle<;127z z>w04!i-(y>NMBW0`o?uv+QZp-ea1T<{X6;ghdC(oZta!d$jr>ZaEcX@eYk)bg@M5W zE&Bi^8H$RFlJyZe?9s+Zl)^z0rlcsfIKQ+gIn_n4peVl}wWuUBwKz5CY~O4HfqkDv z7C+gfy)@-&@@lykY8Q9)dWha|vy_rZ*pd)@d&=3{Nwk6L()r(wKD4_gXX@B_f9-v zmqeCmJ$swiw(3@+oQPC(uz1ysE7qQTo5iY+%SPUO9{PQ9Z|h^t4&Fa0GUq=T)_e-! z-M(_E|{KxoaU;PjfB2wEv>UepT67)7HqZN^NXy&s?Euo;UgL{cs5u zpS9JKAN#G4H>ywG^Jun-Q>=mTDx08hM`y}4+k+BQ-Lzny$BYaNj=%&J;LXS+!hl+l zz-kFlMS=?8H3O(xLD!00#Dl641h54%;aZU@6=V}YRRMA;2UP_Kup7vPn*gpDKzg|t xaOVSr-d{j(LURSeub_N^91fs-fdH~VkAt;f%OwHctZX2qtUzcC3`Tb_4*-@q<$nMG literal 0 HcmV?d00001 diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/web.xml b/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/web.xml new file mode 100644 index 0000000..7177522 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/WEB-INF/web.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResourceAsStream.jsp b/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResourceAsStream.jsp new file mode 100644 index 0000000..5fbc1cc --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResourceAsStream.jsp @@ -0,0 +1,32 @@ +<%-- + 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. +--%> + +<% +response.setContentType("text/plain"); +String path = request.getParameter("path"); +if(path.startsWith("/")) path = path.substring(1); +java.io.InputStream is = this.getClass().getClassLoader().getResourceAsStream(path); +if(is==null) { + response.setStatus(404); + response.getWriter().println(path+ " not found in classpath"); + return; +} +int b; +while((b=is.read()) != -1){ + out.write(b); +} +%> \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResourceUrlThenGetStream.jsp b/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResourceUrlThenGetStream.jsp new file mode 100644 index 0000000..e483cad --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResourceUrlThenGetStream.jsp @@ -0,0 +1,33 @@ +<%-- + 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. +--%> + +<% +response.setContentType("text/plain"); +String path = request.getParameter("path"); +if(path.startsWith("/")) path = path.substring(1); +java.net.URL url = this.getClass().getClassLoader().getResource(path); +if(url==null) { + response.setStatus(404); + response.getWriter().println(path+ " not found in classpath"); + return; +} +java.io.InputStream is = url.openStream(); +int b; +while((b=is.read()) != -1){ + out.write(b); +} +%> \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResources.jsp b/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResources.jsp new file mode 100644 index 0000000..cf164ee --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/classpathGetResources.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%><% +response.setContentType("text/plain"); +String path = request.getParameter("path"); +if(path.startsWith("/")) path = path.substring(1); +java.util.Enumeration e = this.getClass().getClassLoader().getResources(path); +while(e.hasMoreElements()){ + java.net.URL url = e.nextElement(); + out.println(url); +} +%> \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/contextGetRealPath.jsp b/test/webapp-virtual-webapp/src/main/webapp-a/contextGetRealPath.jsp new file mode 100644 index 0000000..2ca208d --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/contextGetRealPath.jsp @@ -0,0 +1,28 @@ +<%-- + 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. +--%> + +<% +response.setContentType("text/plain"); +String path = request.getParameter("path"); +String realPath = application.getRealPath(path); +if(realPath==null) { + response.setStatus(404); + response.getWriter().println(path+ " cannot find real path"); + return; +} +response.getWriter().println(realPath); +%> \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/contextGetResource.jsp b/test/webapp-virtual-webapp/src/main/webapp-a/contextGetResource.jsp new file mode 100644 index 0000000..814803a --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/contextGetResource.jsp @@ -0,0 +1,36 @@ +<%-- + 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. +--%> + +<% +response.setContentType("text/plain"); +String path = request.getParameter("path"); +java.net.URL url = application.getResource(path); +if(url==null) { + response.setStatus(404); + response.getWriter().println(path+ " not found as context resource"); + return; +} +java.io.InputStream is = url.openStream(); +try { + int b; + while((b=is.read()) != -1){ + out.write(b); + } +} finally { + is.close(); +} +%> \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/contextGetResourcePaths.jsp b/test/webapp-virtual-webapp/src/main/webapp-a/contextGetResourcePaths.jsp new file mode 100644 index 0000000..0dde397 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/contextGetResourcePaths.jsp @@ -0,0 +1,27 @@ +<%-- + 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. +--%> + +<% +response.setContentType("text/plain"); +String path = request.getParameter("path"); +java.util.Set paths = application.getResourcePaths(path); +if(paths!=null){ + for(String p : paths) { + out.println(p); + } +} +%> diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/rsrc/resourceF.properties b/test/webapp-virtual-webapp/src/main/webapp-a/rsrc/resourceF.properties new file mode 100644 index 0000000..24e6b8f --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/rsrc/resourceF.properties @@ -0,0 +1,16 @@ +# 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. + +resourceFInWebapp=true diff --git a/test/webapp-virtual-webapp/src/main/webapp-a/testTlds.jsp b/test/webapp-virtual-webapp/src/main/webapp-a/testTlds.jsp new file mode 100644 index 0000000..5fddd21 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-a/testTlds.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> + +<%@ taglib prefix="A" uri="http://tomcat.apache.org/A" %> +<%@ taglib prefix="B" uri="http://tomcat.apache.org/B" %> +<%@ taglib prefix="C" uri="http://tomcat.apache.org/C" %> +<%@ taglib prefix="D" uri="http://tomcat.apache.org/D" %> + + + + + \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/D.tld b/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/D.tld new file mode 100644 index 0000000..38b69f4 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/D.tld @@ -0,0 +1,37 @@ + + + 1.0 + D + http://tomcat.apache.org/D + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc-2/resourceK.properties b/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc-2/resourceK.properties new file mode 100644 index 0000000..65f2811 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc-2/resourceK.properties @@ -0,0 +1,16 @@ +# 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. + +resourceKInWebInfClasses=true diff --git a/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc/resourceG.properties b/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc/resourceG.properties new file mode 100644 index 0000000..89f3ed2 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-b/WEB-INF/classes/rsrc/resourceG.properties @@ -0,0 +1,16 @@ +# 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. + +resourceGInWebInfClasses=true diff --git a/test/webapp-virtual-webapp/src/main/webapp-b/rsrc-2/resourceJ.properties b/test/webapp-virtual-webapp/src/main/webapp-b/rsrc-2/resourceJ.properties new file mode 100644 index 0000000..21b9ae8 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-b/rsrc-2/resourceJ.properties @@ -0,0 +1,16 @@ +# 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. + +resourceJInWebapp=true diff --git a/test/webapp-virtual-webapp/src/main/webapp-b/rsrc/resourceF.properties b/test/webapp-virtual-webapp/src/main/webapp-b/rsrc/resourceF.properties new file mode 100644 index 0000000..8484819 --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-b/rsrc/resourceF.properties @@ -0,0 +1,17 @@ +# 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. + +This file should not be served, it is masked by +/test/webapp-virtual-webapp/src/main/webapp-a/rsrc/resourceF.properties \ No newline at end of file diff --git a/test/webapp-virtual-webapp/src/main/webapp-b/rsrc/resourceH.properties b/test/webapp-virtual-webapp/src/main/webapp-b/rsrc/resourceH.properties new file mode 100644 index 0000000..f26ea6f --- /dev/null +++ b/test/webapp-virtual-webapp/src/main/webapp-b/rsrc/resourceH.properties @@ -0,0 +1,16 @@ +# 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. + +resourceHInWebapp=true diff --git a/test/webapp-virtual-webapp/target/classes/rsrc/resourceB.properties b/test/webapp-virtual-webapp/target/classes/rsrc/resourceB.properties new file mode 100644 index 0000000..9c07719 --- /dev/null +++ b/test/webapp-virtual-webapp/target/classes/rsrc/resourceB.properties @@ -0,0 +1,16 @@ +# 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. + +resourceBInTargetClasses=true diff --git a/test/webapp/404.html b/test/webapp/404.html new file mode 100644 index 0000000..50c55f6 --- /dev/null +++ b/test/webapp/404.html @@ -0,0 +1 @@ +It is 404.html \ No newline at end of file diff --git a/test/webapp/WEB-INF/bug53545.tld b/test/webapp/WEB-INF/bug53545.tld new file mode 100644 index 0000000..41aab87 --- /dev/null +++ b/test/webapp/WEB-INF/bug53545.tld @@ -0,0 +1,29 @@ + + + + 1.0 + bug53545 + + test + org.apache.tomcat.unittest.tags.Bug53545 + scriptless + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/bugs.tld b/test/webapp/WEB-INF/bugs.tld new file mode 100644 index 0000000..1fff162 --- /dev/null +++ b/test/webapp/WEB-INF/bugs.tld @@ -0,0 +1,191 @@ + + + 1.0 + BugTags + http://tomcat.apache.org/bugs + + + Bug46816a + org.apache.jasper.compiler.TestScriptingVariabler$Bug48616aTag + JSP + + + Bug48616b + org.apache.jasper.compiler.TestScriptingVariabler$Bug48616bTag + org.apache.jasper.compiler.TestScriptingVariabler$Bug48616bTei + JSP + + + Bug48701a + org.apache.jasper.compiler.TestGenerator$Bug48701 + org.apache.jasper.compiler.TestGenerator$Bug48701TEI + empty + + + Bug48701b + org.apache.jasper.compiler.TestGenerator$Bug48701 + empty + + now + org.apache.jasper.compiler.TestGenerator.Bean + AT_END + + + + Bug48701c + org.apache.jasper.compiler.TestGenerator$Bug48701 + JSP + + beanName + org.apache.jasper.compiler.TestGenerator.Bean + AT_END + + + beanName + true + java.lang.String + + + + Bug56529 + org.apache.jasper.compiler.TestGenerator$Bug56529 + JSP + + attribute1 + true + true + + + attribute2 + true + + + + JspIdTag + org.apache.jasper.compiler.TestGenerator$JspIdTag + JSP + + + TryCatchFinallyTag + org.apache.jasper.compiler.TestGenerator$TryCatchFinallyTag + JSP + + + TryCatchFinallyBodyTag + org.apache.jasper.compiler.TestGenerator$TryCatchFinallyBodyTag + JSP + + + TesterBodyTag + org.apache.jasper.compiler.TestGenerator$TesterBodyTag + JSP + + + TesterTag + org.apache.jasper.compiler.TestGenerator$TesterTag + JSP + + foo + false + + + + TesterTagA + org.apache.jasper.compiler.TestGenerator$TesterTagA + JSP + + + TesterScriptingTag + org.apache.jasper.compiler.TestGenerator$TesterScriptingTag + JSP + + variable01 + + + attribute02 + + + attribute03 + false + + + attribute02 + + + attribute03 + + + + TesterScriptingTagB + org.apache.jasper.compiler.TestGenerator$TesterScriptingTagB + org.apache.jasper.compiler.TestGenerator$TesterScriptingTagBTEI + JSP + + attribute02 + + + + TesterDynamicTag + org.apache.jasper.compiler.TestGenerator$TesterDynamicTag + JSP + true + + + TesterAttributeTag + org.apache.jasper.compiler.TestGenerator$TesterAttributeTag + JSP + + attribute01 + + + + attribute02 + true + + + + attribute03 + true + + + attribute04 + true + jakarta.el.ValueExpression + + + attribute05 + + java.lang.String sayHello(java.lang.String) + + + + attribute06 + true + jakarta.el.MethodExpression + + + + bug49555 + org.apache.el.TesterFunctions$Inner$Class + java.lang.String bug49555() + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/classes/META-INF/bug55807.tld b/test/webapp/WEB-INF/classes/META-INF/bug55807.tld new file mode 100644 index 0000000..33c71f3 --- /dev/null +++ b/test/webapp/WEB-INF/classes/META-INF/bug55807.tld @@ -0,0 +1,37 @@ + + + + 1.0 + 1.1 + Tags11 + http://tomcat.apache.org/bug55807 + + + Echo + org.apache.jasper.compiler.TestValidator$Echo + empty + + echo + yes + true + + + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/classes/META-INF/bug64373.tld b/test/webapp/WEB-INF/classes/META-INF/bug64373.tld new file mode 100644 index 0000000..5fcf458 --- /dev/null +++ b/test/webapp/WEB-INF/classes/META-INF/bug64373.tld @@ -0,0 +1,31 @@ + + + 1.0 + bug64373 + http://tomcat.apache.org/bug64373 + + + bug64373 + /META-INF/tags/bug64373.tag + + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/classes/META-INF/org.apache.jasper/tagPlugins.xml b/test/webapp/WEB-INF/classes/META-INF/org.apache.jasper/tagPlugins.xml new file mode 100644 index 0000000..36dcf7d --- /dev/null +++ b/test/webapp/WEB-INF/classes/META-INF/org.apache.jasper/tagPlugins.xml @@ -0,0 +1,23 @@ + + + + + org.apache.jasper.compiler.TesterTag + org.apache.jasper.compiler.TesterTagPlugin + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/classes/META-INF/tags/bug64373.tag b/test/webapp/WEB-INF/classes/META-INF/tags/bug64373.tag new file mode 100644 index 0000000..14c713e --- /dev/null +++ b/test/webapp/WEB-INF/classes/META-INF/tags/bug64373.tag @@ -0,0 +1,17 @@ +<%-- + 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. +--%><%@ tag %><%@ +tag body-content="empty" %>

    OK

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/classes/org/apache/tomcat/Bug58096.class b/test/webapp/WEB-INF/classes/org/apache/tomcat/Bug58096.class new file mode 100644 index 0000000000000000000000000000000000000000..f25ceb2bce35fae56d96eb018fc436774b48b19c GIT binary patch literal 285 zcmZ{fF;Bxl427T5q$#1L6-y<=!UhZ+1E3-*29P>MWdL<|j#GM7nnU5j|6)R7;0N%d zP<<7QEbsZ*lKuJn=kp7|1x6_%!hvf_#V@RvMtQf^?A2XcUQW)huTsQ>(Td+#Ra}?q zp;#I13Eio!t-mF7j!zbZ_})F63?1zBk)TT$=C(HTc3qg}iHph*_H(DXT5w}S{%yv7 zX + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/lib/test-lib.jar b/test/webapp/WEB-INF/lib/test-lib.jar new file mode 100644 index 0000000000000000000000000000000000000000..fab58c7abea837bfc012897f9a8ff8c22000ea29 GIT binary patch literal 1943 zcmWIWW@Zs#00EBZ3C>^!l;8x?zOEsTx}JV+`T=ldMNz2A_>h&AB&HX`)p9T>CT(-d zIW75Jm5G7j3Nr(P7>3%^h<-WND^U-4@6slIb5V~wxLmM)LR zb*C0HvsW)YLOwIyp-o#K|b|M%bAy>UPO)v$fHaZWT~*FG4=u`o$;uTbQT zzRZ@nIj-k)rZcTOlv2*S#f3LW!GpK=iig4k&qP!Et<(NpO?OFH>Xs7_AGm?vHIFmTdVUvI70wO1Q?UD*DRx_G2$5&>`@j{k~>Ze!CvsB-rbuM)}%i9{awTFWv zSKF-%i@na8BOv1uyH4+4*!L+}b8>2hYTs1~m`XVZ{>|FCtij*bVCjl^b3HQ8T1nkE zSd{q1uXy>+r9RUWj@=YK^WV6tFGuLa*={CJH8#*!|SX6O;TV%kp1RIVd|h zF;^sQ&jZOw;ndS!EkSk*#okTPxWa3mx1x>Z8Q-#JyxR|F8m+GtocLbf?Df&~4QpB+ zX4@r~nQ%3kRyy8LROqZ;GnSt?=39^>Zfee?9fqA7)r0fk6}{VWk$A z=;mZ5>6PT9tPKgwzic2-H-F3jf+w6yRO8wwB$T+emMSG>FI}4aCbMXA_N$3E4wg;( zdcU}A(y|Pv$Bp~8*wuf(TmF5n^}jD6Vjntgaj2ee3M`RfI2DrW^eo+Rj?SU)@9)=g zxHHzwl9B6)Z+-ZX@xW4#K&dkav^Ge$WF8Iv<#pwjS&VYbim0?ZLaKY_sfp(|^sR98 zWLdf}L`v3s)sm}8q0wEUy2>A7l&5K|?PL&g@wboHd8}_7(aoeNyv%petKJRLN=iKj z7c~9~UvoLS*=NV4Jq(JP0o$BRp2h6mwjyuqRi*n80liDknJ?;+IXlT~;;p#uk9+p5 zdi~k&YOJ*L@xKav52~#lEjs&TuV%M%?z;Wptox0ivXk2trnb)dC9~qA=FwA=LN0}d zwLUO9x^VMCwzH4dDFuds2^w&*2}P0_;jW}?fIbc3>dpP8N)EOO0v z+@-rRVEQ48U|!BMGQNsliGR&zKCca2Xi$=WuI?k@G(d!%0D zuKIqC6y9}**n_vO&0M$ryZZOVFHWZHZFs2vL`8~KSX?&xP+M$`(|?`hZSt9hX^Xe; zs_GXeUA!@YA=!*2I$-V5C6_ZoJ>R~~|2*MY!dp3Oey6I~T)Vl(CM(6JmX@Z!bUQfd zdGa>#yNswIpg^A`o;V2@rU2ZQ+t!OZBE-| zdEQ_0B6i14z8~Pt$YjretKe4wW+xC3fKZ5PfQvx^EX2ql!LT~$WMts4bBr+hYRbB- z03aXPXw1q2*=Tj3S@0SLTy-Fuf1+{ICXjhBy72ys38>~{t6*T}gWMU6VLq(7LAHng z$TH_=VF-I*_96U%t-L|DXAQ7=L-+%!^g*^}?<~PHpx`?0>E(({2Y9mr3or%-79i|m LWMKHt3gQ6(>s13# literal 0 HcmV?d00001 diff --git a/test/webapp/WEB-INF/tag-setters.tld b/test/webapp/WEB-INF/tag-setters.tld new file mode 100644 index 0000000..e02ae14 --- /dev/null +++ b/test/webapp/WEB-INF/tag-setters.tld @@ -0,0 +1,245 @@ + + + + 1.0 + tag-setters + http://tomcat.apache.org/tag-setters + + tagPrimitiveBoolean + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagPrimitiveBoolean + scriptless + + foo + true + true + boolean + + + + tagBoolean + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagBoolean + scriptless + + foo + true + true + Boolean + + + + tagPrimitiveCharacter + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagPrimitiveCharacter + scriptless + + foo + true + true + char + + + + tagCharacter + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagCharacter + scriptless + + foo + true + true + Character + + + + tagPrimitiveLong + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagPrimitiveLong + scriptless + + foo + true + true + long + + + + tagLong + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagLong + scriptless + + foo + true + true + Long + + + + tagPrimitiveInteger + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagPrimitiveInteger + scriptless + + foo + true + true + int + + + + tagInteger + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagInteger + scriptless + + foo + true + true + Integer + + + + tagPrimitiveShort + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagPrimitiveShort + scriptless + + foo + true + true + short + + + + tagShort + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagShort + scriptless + + foo + true + true + Short + + + + tagPrimitiveByte + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagPrimitiveByte + scriptless + + foo + true + true + byte + + + + tagByte + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagByte + scriptless + + foo + true + true + Byte + + + + tagPrimitiveDouble + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagPrimitiveDouble + scriptless + + foo + true + true + double + + + + tagDouble + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagDouble + scriptless + + foo + true + true + Double + + + + tagPrimitiveFloat + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagPrimitiveFloat + scriptless + + foo + true + true + float + + + + tagFloat + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagFloat + scriptless + + foo + true + true + Float + + + + tagBigDecimal + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagBigDecimal + scriptless + + foo + true + true + BigDecimal + + + + tagBigInteger + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagBigInteger + scriptless + + foo + true + true + BigInteger + + + + tagString + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagString + scriptless + + foo + true + true + String + + + + tagTimeUnit + org.apache.jasper.optimizations.TestELInterpreterTagSetters$TagTimeUnit + scriptless + + foo + true + true + java.util.concurrent.TimeUnit + + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/bug42390.tag b/test/webapp/WEB-INF/tags/bug42390.tag new file mode 100644 index 0000000..bf6a398 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug42390.tag @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ variable name-given="X" scope="AT_END" %> + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/bug43400.tag b/test/webapp/WEB-INF/tags/bug43400.tag new file mode 100644 index 0000000..c94bf90 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug43400.tag @@ -0,0 +1,18 @@ +<%-- + 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. +--%><%@ tag %><%@ +attribute name="type" type="jakarta.servlet.DispatcherType"%><%@ +tag body-content="empty" %>

    ${type}

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/bug48668.tagx b/test/webapp/WEB-INF/tags/bug48668.tagx new file mode 100644 index 0000000..9e56e02 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug48668.tagx @@ -0,0 +1,26 @@ + + + + + + + +${expr}${noexpr} + + + diff --git a/test/webapp/WEB-INF/tags/bug49297.tag b/test/webapp/WEB-INF/tags/bug49297.tag new file mode 100644 index 0000000..940d968 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug49297.tag @@ -0,0 +1,21 @@ +<%-- + 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. +--%><%@ tag import="java.util.List" import="java.util.ArrayList"%><%@ +tag body-content="empty" %><% + // Make sure the imports above do work + List l = new ArrayList<>(); + l.add("OK"); + %>

    <%=l.get(0)%>

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/bug54012.tag b/test/webapp/WEB-INF/tags/bug54012.tag new file mode 100644 index 0000000..6d52aaa --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug54012.tag @@ -0,0 +1,20 @@ +<%-- + 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. +--%><%@ tag %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ tag body-content="empty" %> + +

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/bug55198.tagx b/test/webapp/WEB-INF/tags/bug55198.tagx new file mode 100644 index 0000000..8d2545f --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug55198.tagx @@ -0,0 +1,26 @@ + + + + +foo +bar +foo +foo +foo + + diff --git a/test/webapp/WEB-INF/tags/bug56265.tagx b/test/webapp/WEB-INF/tags/bug56265.tagx new file mode 100644 index 0000000..5a76883 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug56265.tagx @@ -0,0 +1,24 @@ + + + + + + [${e.key}]: [${e.value}] + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/bug58178.tag b/test/webapp/WEB-INF/tags/bug58178.tag new file mode 100644 index 0000000..2734504 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug58178.tag @@ -0,0 +1,32 @@ +<%-- + 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. +--%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + +

    PASS
    + Error detected
    + The exception is : ${error}
    + The message is: ${error.message}

    +
    + +

    FAIL
    + Error not detected

    +
    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/bug58178b.tag b/test/webapp/WEB-INF/tags/bug58178b.tag new file mode 100644 index 0000000..7eedc82 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug58178b.tag @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@tag import="java.math.*, java.util.Collections" %> +

    00-${DispatcherType.ASYNC}

    +

    01-${RoundingMode.HALF_UP}

    +

    02-${Collections.EMPTY_LIST.stream().count()}

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/bug62453.tag b/test/webapp/WEB-INF/tags/bug62453.tag new file mode 100644 index 0000000..f7db062 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug62453.tag @@ -0,0 +1,26 @@ +<%-- + 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. +--%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ attribute name="foo" required="true" %> +<%@ attribute name="bar" %> +<%@ attribute name="baz" %> + +
    +
    foo: ${foo.toString()}
    +
    bar: ${bar.toString()}
    +
    baz: ${baz.toString()}
    +
    diff --git a/test/webapp/WEB-INF/tags/bug65390.tag b/test/webapp/WEB-INF/tags/bug65390.tag new file mode 100644 index 0000000..51d38c8 --- /dev/null +++ b/test/webapp/WEB-INF/tags/bug65390.tag @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ tag language="java" pageEncoding="UTF-8" body-content="scriptless"%> +<% +getJspBody().invoke(null); +%> \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/circular01.tag b/test/webapp/WEB-INF/tags/circular01.tag new file mode 100644 index 0000000..ca70c5a --- /dev/null +++ b/test/webapp/WEB-INF/tags/circular01.tag @@ -0,0 +1,25 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + <% + if (getParent() == null) { + %> + + <% + } + %> + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/circular02.tag b/test/webapp/WEB-INF/tags/circular02.tag new file mode 100644 index 0000000..03b8f4c --- /dev/null +++ b/test/webapp/WEB-INF/tags/circular02.tag @@ -0,0 +1,25 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<% + if (getParent() == null) { +%> + +<% + } +%> + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/dobody.tagx b/test/webapp/WEB-INF/tags/dobody.tagx new file mode 100644 index 0000000..1b799ef --- /dev/null +++ b/test/webapp/WEB-INF/tags/dobody.tagx @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/test/webapp/WEB-INF/tags/echo-deferred-method.tag b/test/webapp/WEB-INF/tags/echo-deferred-method.tag new file mode 100644 index 0000000..6710a8c --- /dev/null +++ b/test/webapp/WEB-INF/tags/echo-deferred-method.tag @@ -0,0 +1,19 @@ +<%-- + 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. +--%><%@ tag %><%@ attribute name="echo" deferredMethod="true" %><%@ +attribute name="echo2" deferredMethod="true" %><%@ +variable name-given="var1" scope="AT_END" %><%@ +tag body-content="empty" %>

    ${echo} ${echo2}

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/echo-deferred.tag b/test/webapp/WEB-INF/tags/echo-deferred.tag new file mode 100644 index 0000000..a884e9b --- /dev/null +++ b/test/webapp/WEB-INF/tags/echo-deferred.tag @@ -0,0 +1,17 @@ +<%-- + 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. +--%><%@ tag %><%@ attribute name="echo" deferredValue="true" %><%@ +tag body-content="empty" %>

    ${echo}

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/echo-double.tag b/test/webapp/WEB-INF/tags/echo-double.tag new file mode 100644 index 0000000..4ba9cc6 --- /dev/null +++ b/test/webapp/WEB-INF/tags/echo-double.tag @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ tag %><%@ +attribute name="echo" type="java.lang.Double"%><%@ +attribute name="index" type="java.lang.String" %><%@ +tag body-content="empty" %>

    ${index}-${echo}

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/echo-long.tag b/test/webapp/WEB-INF/tags/echo-long.tag new file mode 100644 index 0000000..47852ff --- /dev/null +++ b/test/webapp/WEB-INF/tags/echo-long.tag @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ tag %><%@ +attribute name="echo" type="java.lang.Long"%><%@ +attribute name="index" type="java.lang.String" %><%@ +tag body-content="empty" %>

    ${index}-${echo}

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/echo-noel.tag b/test/webapp/WEB-INF/tags/echo-noel.tag new file mode 100644 index 0000000..edb174a --- /dev/null +++ b/test/webapp/WEB-INF/tags/echo-noel.tag @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@ tag%><%@ +attribute name="echo" type="java.lang.String" rtexprvalue="false" +%><%@ tag body-content="empty" %>

    ${echo}

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/echo.tag b/test/webapp/WEB-INF/tags/echo.tag new file mode 100644 index 0000000..0f489c1 --- /dev/null +++ b/test/webapp/WEB-INF/tags/echo.tag @@ -0,0 +1,19 @@ +<%-- + 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. +--%><%@ tag %><%@ +attribute name="echo" type="java.lang.String"%><%@ +attribute name="dummy" type="java.lang.String" required="false"%><%@ +tag body-content="empty" %>

    ${echo}

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/error-on-el-not-found-false.tag b/test/webapp/WEB-INF/tags/error-on-el-not-found-false.tag new file mode 100644 index 0000000..8466d0e --- /dev/null +++ b/test/webapp/WEB-INF/tags/error-on-el-not-found-false.tag @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ tag errorOnELNotFound="false" %> +

    00-O${unknown}K

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/error-on-el-not-found-true.tag b/test/webapp/WEB-INF/tags/error-on-el-not-found-true.tag new file mode 100644 index 0000000..ba23cac --- /dev/null +++ b/test/webapp/WEB-INF/tags/error-on-el-not-found-true.tag @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ tag errorOnELNotFound="true" %> +

    00-O${unknown}K

    \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/forward.tag b/test/webapp/WEB-INF/tags/forward.tag new file mode 100644 index 0000000..c5f76b8 --- /dev/null +++ b/test/webapp/WEB-INF/tags/forward.tag @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ tag body-content="empty" %> + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/implicit.tld b/test/webapp/WEB-INF/tags/implicit.tld new file mode 100644 index 0000000..e99820a --- /dev/null +++ b/test/webapp/WEB-INF/tags/implicit.tld @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/invoke.tagx b/test/webapp/WEB-INF/tags/invoke.tagx new file mode 100644 index 0000000..be146dc --- /dev/null +++ b/test/webapp/WEB-INF/tags/invoke.tagx @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/test/webapp/WEB-INF/tags/jsp-root.tagx b/test/webapp/WEB-INF/tags/jsp-root.tagx new file mode 100644 index 0000000..5cae0cf --- /dev/null +++ b/test/webapp/WEB-INF/tags/jsp-root.tagx @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/no-jsp-root.tagx b/test/webapp/WEB-INF/tags/no-jsp-root.tagx new file mode 100644 index 0000000..154f72b --- /dev/null +++ b/test/webapp/WEB-INF/tags/no-jsp-root.tagx @@ -0,0 +1,19 @@ + + + + diff --git a/test/webapp/WEB-INF/tags/setters.tag b/test/webapp/WEB-INF/tags/setters.tag new file mode 100644 index 0000000..01f9096 --- /dev/null +++ b/test/webapp/WEB-INF/tags/setters.tag @@ -0,0 +1,22 @@ +<%-- + 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. +--%> +<%@ tag %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/variable-from-attr.tag b/test/webapp/WEB-INF/tags/variable-from-attr.tag new file mode 100644 index 0000000..285709b --- /dev/null +++ b/test/webapp/WEB-INF/tags/variable-from-attr.tag @@ -0,0 +1,21 @@ +<%-- + 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. +--%> +<%@ attribute name="varName" type="java.lang.String" required="true" rtexprvalue="false" %> +<%@ attribute name="varName2" type="java.lang.String" required="true" rtexprvalue="false" %> +<%@ variable name-from-attribute="varName" alias="data" scope="AT_END" %> +<%@ variable name-from-attribute="varName2" alias="data2" scope="AT_END" %> + \ No newline at end of file diff --git a/test/webapp/WEB-INF/tags/variable.tag b/test/webapp/WEB-INF/tags/variable.tag new file mode 100644 index 0000000..29dec90 --- /dev/null +++ b/test/webapp/WEB-INF/tags/variable.tag @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ variable name-given="var1" scope="AT_END" %> +<%@ variable name-given="var2" scope="AT_END" %> +<%@ variable name-given="var3" scope="NESTED" %> +<%@ variable name-given="var4" scope="NESTED" %> +<%@ variable name-given="var5" scope="AT_BEGIN" %> +<%@ variable name-given="var16" scope="AT_BEGIN" %> + \ No newline at end of file diff --git a/test/webapp/WEB-INF/test.tld b/test/webapp/WEB-INF/test.tld new file mode 100644 index 0000000..0f95eb5 --- /dev/null +++ b/test/webapp/WEB-INF/test.tld @@ -0,0 +1,64 @@ + + + + 1.0 + TesterFunctions + http://tomcat.apache.org/testerFunctions + + + trim + org.apache.el.TesterFunctions + + java.lang.String trim(java.lang.String) + + + + + concat + org.apache.el.TesterFunctions + + java.lang.String concat(java.lang.String[]) + + + + + toArray + org.apache.el.TesterFunctions + + + java.lang.String toArray(java.lang.String, + java.lang.String) + + + + + toArrayB + org.apache.el.TesterFunctions + + + java.lang.String toArray (java.lang.String,java.lang.String) + + + + \ No newline at end of file diff --git a/test/webapp/WEB-INF/web.xml b/test/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..43eb0f1 --- /dev/null +++ b/test/webapp/WEB-INF/web.xml @@ -0,0 +1,338 @@ + + + + + Tomcat Test Application + + Used as part of the Tomcat unit tests when a full web application is + required. + + + + jsp + org.apache.jasper.servlet.JspServlet + + fork + false + + + xpoweredBy + false + + + + modificationTestInterval + 0 + + 3 + + + + + Bug49922 + + org.apache.catalina.core.TestStandardContext$Bug49922Filter + + + + Bug49922 + /bug49922/servlet/* + *.do + Bug49922 + + + Bug49922 + Bug49922Target + FORWARD + INCLUDE + + + bug49297NoSpace + /bug49nnn/bug49297NoSpace.jsp + + strictWhitespace + false + + -2 + + + bug49297NoSpace + /bug49nnn/bug49297NoSpace.jsp + + + Bug49922Forward + + org.apache.catalina.core.TestStandardContext$Bug49922ForwardServlet + + + + Bug49922Forward + /bug49922/forward + + + Bug49922Include + + org.apache.catalina.core.TestStandardContext$Bug49922IncludeServlet + + + + Bug49922Include + /bug49922/include + + + Bug49922Target + + org.apache.catalina.core.TestStandardContext$Bug49922TargetServlet + + + + Bug49922Target + /bug49922/target + + + Bug49922 + + org.apache.catalina.core.TestStandardContext$Bug49922Servlet + + + + Bug49922 + /bug49922/servlet + + + Bug49922 + *.do + + + Bug49922 + *.od + + + + + /bug49nnn/bug49726a.jsp + /bug49nnn/bug49726b.jsp + text/plain + + + /bug5nnnn/bug55262.jsp + /bug5nnnn/bug55262-prelude.jspf + /bug5nnnn/bug55262-prelude.jspf + /bug5nnnn/bug55262-coda.jspf + /bug5nnnn/bug55262-coda.jspf + text/plain + + + /jsp/encoding/bug60769a.jspx + UTF-8 + true + + + /jsp/encoding/bug60769b.jspx + ISO-8859-1 + true + + + /jsp/errorOnELNotFound/web-xml-true.jsp + true + + + /jsp/errorOnELNotFound/web-xml-false.jsp + false + + + + + DenyByAnnotation + org.apache.catalina.core.TestStandardWrapper$DenyAllServlet + + + DenyByAnnotation + /testStandardWrapper/securityAnnotationsMetaDataPriority + + + + Bug53574 + /WEB-INF/jsp/bug53574.jsp + -2 + + + Bug53574 + /bug53574 + + + + JSP-x-powered-by + /jsp/generator/x-powered-by.jsp + + xpoweredBy + true + + -2 + + + JSP-x-powered-by + /jsp/generator/x-powered-by.jsp + + + + JSP-usebean-04 + /jsp/generator/usebean-04.jsp + + errorOnUseBeanInvalidClassAttribute + false + + -2 + + + JSP-usebean-04 + /jsp/generator/usebean-04.jsp + + + + JSP-usebean-07 + /jsp/generator/usebean-07.jsp + + errorOnUseBeanInvalidClassAttribute + false + + -2 + + + JSP-usebean-07 + /jsp/generator/usebean-07.jsp + + + + JSP-templatetext-01 + /jsp/generator/templatetext-01.jsp + + trimSpaces + extended + + + genStringAsCharArray + true + + -2 + + + JSP-templatetext-01 + /jsp/generator/templatetext-01.jsp + + + + JSP-templatetext-02 + /jsp/generator/templatetext-02.jsp + + mappedfile + false + + -2 + + + JSP-templatetext-02 + /jsp/generator/templatetext-02.jsp + + + + JSP-jsp-id + /jsp/generator/jsp-id.jsp + + enablePooling + false + + + useInstanceManagerForTags + true + + -2 + + + JSP-jsp-id + /jsp/generator/jsp-id.jsp + + + + bug48701-no-strict + /bug48nnn/bug48701-VI.jsp + + strictGetProperty + false + + -2 + + + bug48701-no-strict + /bug48nnn/bug48701-VI.jsp + + + + bug58445b + /bug5nnnn/bug58444b.jsp + + poolTagsWithExtends + true + + -2 + + + bug58445b + /bug5nnnn/bug58444b.jsp + + + + deferred-method-02 + /jsp/generator/deferred-method-02.jsp + + enablePooling + false + + -2 + + + deferred-method-02 + /jsp/generator/deferred-method-02.jsp + + + + BASIC + + + + Resource for testing bug 53465 + bug53465 + java.lang.Integer + 10 + Bug53465MappedName + + + + + + aaa + bbb + + + + \ No newline at end of file diff --git a/test/webapp/annotations.jsp b/test/webapp/annotations.jsp new file mode 100644 index 0000000..04b63ce --- /dev/null +++ b/test/webapp/annotations.jsp @@ -0,0 +1,28 @@ +<%-- + 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. +--%> +<%@page import="jakarta.annotation.PostConstruct"%> + + Annotations test case + +

    Hello world

    + + +<%! + @PostConstruct + public void doNothing() { + } +%> \ No newline at end of file diff --git a/test/webapp/bug36923.jsp b/test/webapp/bug36923.jsp new file mode 100644 index 0000000..5e871cf --- /dev/null +++ b/test/webapp/bug36923.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ page isELIgnored="true" %> + + Bug 36923 test case + +

    00-${<%= "hello world" %>}

    + + + diff --git a/test/webapp/bug42390.jsp b/test/webapp/bug42390.jsp new file mode 100644 index 0000000..7e4c000 --- /dev/null +++ b/test/webapp/bug42390.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + \ No newline at end of file diff --git a/test/webapp/bug42565.jsp b/test/webapp/bug42565.jsp new file mode 100644 index 0000000..fbec1a1 --- /dev/null +++ b/test/webapp/bug42565.jsp @@ -0,0 +1,38 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 42565 test case + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/webapp/bug43nnn/bug43400.jsp b/test/webapp/bug43nnn/bug43400.jsp new file mode 100644 index 0000000..be08d3f --- /dev/null +++ b/test/webapp/bug43nnn/bug43400.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/testerFunctions" prefix="fn" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 46596 test case + + + + \ No newline at end of file diff --git a/test/webapp/bug44994.jsp b/test/webapp/bug44994.jsp new file mode 100644 index 0000000..47d8c11 --- /dev/null +++ b/test/webapp/bug44994.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 44994 test case + + + + + + \ No newline at end of file diff --git a/test/webapp/bug45nnn/bug45015a.jsp b/test/webapp/bug45nnn/bug45015a.jsp new file mode 100644 index 0000000..096e08d --- /dev/null +++ b/test/webapp/bug45nnn/bug45015a.jsp @@ -0,0 +1,32 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 45015 test case A + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/webapp/bug45nnn/bug45015b.jsp b/test/webapp/bug45nnn/bug45015b.jsp new file mode 100644 index 0000000..2b3e946 --- /dev/null +++ b/test/webapp/bug45nnn/bug45015b.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 45015 test case B + + + + \ No newline at end of file diff --git a/test/webapp/bug45nnn/bug45015c.jsp b/test/webapp/bug45nnn/bug45015c.jsp new file mode 100644 index 0000000..ceb740b --- /dev/null +++ b/test/webapp/bug45nnn/bug45015c.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 45015 test case C + + " /> + + \ No newline at end of file diff --git a/test/webapp/bug45nnn/bug45427.jsp b/test/webapp/bug45nnn/bug45427.jsp new file mode 100644 index 0000000..816ed3b --- /dev/null +++ b/test/webapp/bug45nnn/bug45427.jsp @@ -0,0 +1,39 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 45427 test case + +

    00-${'hello world'}

    +

    01-${'hello \'world'}

    +

    02-${'hello "world'}

    +

    03-${'hello \"world'}

    +

    04-${"hello world"}

    +

    05-${"hello 'world"}

    +

    06-${"hello \'world"}

    +

    07-${"hello \"world"}

    + + + + + + + + + + + \ No newline at end of file diff --git a/test/webapp/bug45nnn/bug45451.jspf b/test/webapp/bug45nnn/bug45451.jspf new file mode 100644 index 0000000..672c1cd --- /dev/null +++ b/test/webapp/bug45nnn/bug45451.jspf @@ -0,0 +1,38 @@ +<%-- + 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. +--%> +

    00-${1+1}

    +

    01-\${1+1}

    +

    02-\\${1+1}

    +

    03-\\\${1+1}

    +

    04-\$500

    +

    05-${'\\$'}

    +

    06-${'\\${'}

    + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/webapp/bug45nnn/bug45451a.jsp b/test/webapp/bug45nnn/bug45451a.jsp new file mode 100644 index 0000000..43bbe47 --- /dev/null +++ b/test/webapp/bug45nnn/bug45451a.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 45451 test case + + + + + + + \ No newline at end of file diff --git a/test/webapp/bug45nnn/bug45451b.jsp b/test/webapp/bug45nnn/bug45451b.jsp new file mode 100644 index 0000000..3f3e166 --- /dev/null +++ b/test/webapp/bug45nnn/bug45451b.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@page isELIgnored="false" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ include file="bug45451.jspf" %> diff --git a/test/webapp/bug45nnn/bug45451c.jsp b/test/webapp/bug45nnn/bug45451c.jsp new file mode 100644 index 0000000..38324a3 --- /dev/null +++ b/test/webapp/bug45nnn/bug45451c.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@page isELIgnored="true" deferredSyntaxAllowedAsLiteral="true" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ include file="bug45451.jspf" %> diff --git a/test/webapp/bug45nnn/bug45451d.jspx b/test/webapp/bug45nnn/bug45451d.jspx new file mode 100644 index 0000000..89bc564 --- /dev/null +++ b/test/webapp/bug45nnn/bug45451d.jspx @@ -0,0 +1,42 @@ + + + + + +

    00-${1+1}

    +

    01-\${1+1}

    +

    02-\\${1+1}

    +

    03-\\\${1+1}

    +

    04-\$500

    + + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/test/webapp/bug45nnn/bug45451e.jsp b/test/webapp/bug45nnn/bug45451e.jsp new file mode 100644 index 0000000..b181d13 --- /dev/null +++ b/test/webapp/bug45nnn/bug45451e.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@page deferredSyntaxAllowedAsLiteral="true" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ include file="bug45451.jspf" %> diff --git a/test/webapp/bug45nnn/bug45511.jsp b/test/webapp/bug45nnn/bug45511.jsp new file mode 100644 index 0000000..d643cf9 --- /dev/null +++ b/test/webapp/bug45nnn/bug45511.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 45511 test case + + + + + \ No newline at end of file diff --git a/test/webapp/bug46381.jsp b/test/webapp/bug46381.jsp new file mode 100644 index 0000000..fbd0b05 --- /dev/null +++ b/test/webapp/bug46381.jsp @@ -0,0 +1,28 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 46381 test case + <% + pageContext.setAttribute("foo","hello"); + pageContext.setAttribute("bar","world"); + %> + + + + + \ No newline at end of file diff --git a/test/webapp/bug46596.jsp b/test/webapp/bug46596.jsp new file mode 100644 index 0000000..d9a1da8 --- /dev/null +++ b/test/webapp/bug46596.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/testerFunctions" prefix="fn" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 46596 test case + + + + \ No newline at end of file diff --git a/test/webapp/bug47331.jsp b/test/webapp/bug47331.jsp new file mode 100644 index 0000000..eca3a1a --- /dev/null +++ b/test/webapp/bug47331.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 47331 test case + +

    This is a #{'test'}

    + + \ No newline at end of file diff --git a/test/webapp/bug47413.jsp b/test/webapp/bug47413.jsp new file mode 100644 index 0000000..1d86962 --- /dev/null +++ b/test/webapp/bug47413.jsp @@ -0,0 +1,51 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + Bug 47413 test case + + +

    00-${values.stringValue}

    + + + +

    02-${values.doubleValue}

    + + + +

    04-${values.longValue}

    + + + +

    06-${values.stringValue}

    + + + +

    08-${values.doubleValue}

    + + + +

    10-${values.longValue}

    + + + + + diff --git a/test/webapp/bug47977.jspx b/test/webapp/bug47977.jspx new file mode 100644 index 0000000..082e8f1 --- /dev/null +++ b/test/webapp/bug47977.jspx @@ -0,0 +1,26 @@ + + + + + + xxx + + + \ No newline at end of file diff --git a/test/webapp/bug48nnn/bug48112.jsp b/test/webapp/bug48nnn/bug48112.jsp new file mode 100644 index 0000000..d418b6c --- /dev/null +++ b/test/webapp/bug48nnn/bug48112.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/testerFunctions" prefix="fn" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 48112 test case + + + + \ No newline at end of file diff --git a/test/webapp/bug48nnn/bug48616.jsp b/test/webapp/bug48nnn/bug48616.jsp new file mode 100644 index 0000000..efc77cb --- /dev/null +++ b/test/webapp/bug48nnn/bug48616.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> +<%@ taglib prefix="bugs" uri="http://tomcat.apache.org/bugs" %> + + + + \ No newline at end of file diff --git a/test/webapp/bug48nnn/bug48616b.jsp b/test/webapp/bug48nnn/bug48616b.jsp new file mode 100644 index 0000000..715d877 --- /dev/null +++ b/test/webapp/bug48nnn/bug48616b.jsp @@ -0,0 +1,31 @@ +<%-- + 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. +--%> +<%@ taglib prefix="bugs" uri="http://tomcat.apache.org/bugs" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%-- + Tries to place the classic tag that defines a variable + into a simple tag +--%> + + + + + + +<% + out.println(X); +%> diff --git a/test/webapp/bug48nnn/bug48627.jsp b/test/webapp/bug48nnn/bug48627.jsp new file mode 100644 index 0000000..5716506 --- /dev/null +++ b/test/webapp/bug48nnn/bug48627.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 46596 test case + + + + + \ No newline at end of file diff --git a/test/webapp/bug48nnn/bug48668a.jsp b/test/webapp/bug48nnn/bug48668a.jsp new file mode 100644 index 0000000..dd8bb7c --- /dev/null +++ b/test/webapp/bug48nnn/bug48668a.jsp @@ -0,0 +1,60 @@ +<%-- + 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. +--%> +<%@ page isELIgnored="true" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 48668a test case + +

    #{foo.bar}

    +

    ${foo.bar}

    + +

    10-

    +

    11-Hello

    +

    12-

    +

    13-Hello

    + +

    14-}

    +

    15-Hello }

    +

    16-}

    +

    17-Hello }

    + +

    18-Hello ${'foo.bar}

    +

    19-Hello ${'foo.bar}

    +

    20-Hello #{'foo.bar}

    +

    21-Hello #{'foo.bar}

    + +

    30-

    +

    31-Hello

    +

    32-

    +

    33-Hello

    +

    34-Hello ${'foo}

    +

    35-Hello ${'foo}

    +

    36-Hello #{'foo}

    +

    37-Hello #{'foo}

    + +

    40-Hello ${'foo}

    +

    41-Hello ${'foo}

    +

    42-Hello #{'foo}

    +

    43-Hello #{'foo}

    + +

    50-Hello ${'foo}

    +

    51-Hello ${'foo}

    +

    52-Hello #{'foo}

    +

    53-Hello #{'foo}

    + + + diff --git a/test/webapp/bug48nnn/bug48668b.jsp b/test/webapp/bug48nnn/bug48668b.jsp new file mode 100644 index 0000000..ebdb58b --- /dev/null +++ b/test/webapp/bug48nnn/bug48668b.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> +<%@ page deferredSyntaxAllowedAsLiteral="true" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 48668b test case + +

    #{foo.bar}

    +

    #{foo${1 + 1}

    + + + diff --git a/test/webapp/bug48nnn/bug48701-TVI-NFA.jsp b/test/webapp/bug48nnn/bug48701-TVI-NFA.jsp new file mode 100644 index 0000000..dd00f57 --- /dev/null +++ b/test/webapp/bug48nnn/bug48701-TVI-NFA.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + +

    00-PASS

    diff --git a/test/webapp/bug48nnn/bug48701-TVI-NG.jsp b/test/webapp/bug48nnn/bug48701-TVI-NG.jsp new file mode 100644 index 0000000..2b40962 --- /dev/null +++ b/test/webapp/bug48nnn/bug48701-TVI-NG.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + +

    00-PASS

    diff --git a/test/webapp/bug48nnn/bug48701-UseBean.jsp b/test/webapp/bug48nnn/bug48701-UseBean.jsp new file mode 100644 index 0000000..a64214e --- /dev/null +++ b/test/webapp/bug48nnn/bug48701-UseBean.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> + + +

    00-PASS

    \ No newline at end of file diff --git a/test/webapp/bug48nnn/bug48701-VI.jsp b/test/webapp/bug48nnn/bug48701-VI.jsp new file mode 100644 index 0000000..7f042ca --- /dev/null +++ b/test/webapp/bug48nnn/bug48701-VI.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + +

    00-PASS

    diff --git a/test/webapp/bug48nnn/bug48701-fail.jsp b/test/webapp/bug48nnn/bug48701-fail.jsp new file mode 100644 index 0000000..5b04d4a --- /dev/null +++ b/test/webapp/bug48nnn/bug48701-fail.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + diff --git a/test/webapp/bug48nnn/bug48827.jspx b/test/webapp/bug48nnn/bug48827.jspx new file mode 100644 index 0000000..a6c760e --- /dev/null +++ b/test/webapp/bug48nnn/bug48827.jspx @@ -0,0 +1,30 @@ + + + + + + + 00-Hello World + + + + + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49196.jsp b/test/webapp/bug49nnn/bug49196.jsp new file mode 100644 index 0000000..9797a94 --- /dev/null +++ b/test/webapp/bug49nnn/bug49196.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> +<% +// As nonsensical as this is, it shouldn't throw an NPE +pageContext.getErrorData(); +%> + + Bug 49196 test case + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297DuplicateAttr.jsp b/test/webapp/bug49nnn/bug49297DuplicateAttr.jsp new file mode 100644 index 0000000..9790c80 --- /dev/null +++ b/test/webapp/bug49nnn/bug49297DuplicateAttr.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 49297 duplicate attribute test case + + + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297MultipleImport1.jsp b/test/webapp/bug49nnn/bug49297MultipleImport1.jsp new file mode 100644 index 0000000..01fa614 --- /dev/null +++ b/test/webapp/bug49nnn/bug49297MultipleImport1.jsp @@ -0,0 +1,28 @@ +<%-- + 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. +--%> +<%@page import="java.util.List" import="java.util.ArrayList" %> + + Bug 49297 multiple import test case + + <% + // Make sure the imports above do work + List l = new ArrayList<>(); + l.add("OK"); + %> +

    <%=l.get(0)%>

    + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297MultipleImport2.jsp b/test/webapp/bug49nnn/bug49297MultipleImport2.jsp new file mode 100644 index 0000000..33b0dbd --- /dev/null +++ b/test/webapp/bug49nnn/bug49297MultipleImport2.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> +<%@page import="java.util.List" %> +<%@page import="java.util.ArrayList" %> + + Bug 49297 multiple import test case + + <% + // Make sure the imports above do work + List l = new ArrayList<>(); + l.add("OK"); + %> +

    <%=l.get(0)%>

    + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297MultiplePageEncoding1.jsp b/test/webapp/bug49nnn/bug49297MultiplePageEncoding1.jsp new file mode 100644 index 0000000..0d4381d --- /dev/null +++ b/test/webapp/bug49nnn/bug49297MultiplePageEncoding1.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@page pageEncoding="ISO-8859-1" %> +<%@page pageEncoding="ISO-8859-1" %> + + Bug 49297 multiple pageEncoding test case + +

    Should fail

    + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297MultiplePageEncoding2.jsp b/test/webapp/bug49nnn/bug49297MultiplePageEncoding2.jsp new file mode 100644 index 0000000..4ab49e1 --- /dev/null +++ b/test/webapp/bug49nnn/bug49297MultiplePageEncoding2.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@page pageEncoding="ISO-8859-1" pageEncoding="ISO-8859-1" %> + + Bug 49297 multiple pageEncoding test case + +

    Should fail

    + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297MultiplePageEncoding3.jsp b/test/webapp/bug49nnn/bug49297MultiplePageEncoding3.jsp new file mode 100644 index 0000000..2c631ca --- /dev/null +++ b/test/webapp/bug49nnn/bug49297MultiplePageEncoding3.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@page pageEncoding="ISO-8859-1" %> +<%@page pageEncoding="UTF-8" %> + + Bug 49297 multiple pageEncoding test case + +

    Should fail

    + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297MultiplePageEncoding4.jsp b/test/webapp/bug49nnn/bug49297MultiplePageEncoding4.jsp new file mode 100644 index 0000000..22e2a65 --- /dev/null +++ b/test/webapp/bug49nnn/bug49297MultiplePageEncoding4.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@page pageEncoding="ISO-8859-1" pageEncoding="UTF-8"%> + + Bug 49297 multiple pageEncoding test case + +

    Should fail

    + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297NoSpace.jsp b/test/webapp/bug49nnn/bug49297NoSpace.jsp new file mode 100644 index 0000000..5a857c3 --- /dev/null +++ b/test/webapp/bug49nnn/bug49297NoSpace.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 49297 whitespace test case + + + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297NoSpaceStrict.jsp b/test/webapp/bug49nnn/bug49297NoSpaceStrict.jsp new file mode 100644 index 0000000..5a857c3 --- /dev/null +++ b/test/webapp/bug49nnn/bug49297NoSpaceStrict.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 49297 whitespace test case + + + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49297Tag.jsp b/test/webapp/bug49nnn/bug49297Tag.jsp new file mode 100644 index 0000000..f8a836e --- /dev/null +++ b/test/webapp/bug49nnn/bug49297Tag.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 49297 duplicate attribute test case + + + + \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49464-cp1252.txt b/test/webapp/bug49nnn/bug49464-cp1252.txt new file mode 100644 index 0000000..9c95a6b --- /dev/null +++ b/test/webapp/bug49nnn/bug49464-cp1252.txt @@ -0,0 +1 @@ +½ \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49464-ibm850.txt b/test/webapp/bug49nnn/bug49464-ibm850.txt new file mode 100644 index 0000000..f982586 --- /dev/null +++ b/test/webapp/bug49nnn/bug49464-ibm850.txt @@ -0,0 +1 @@ +« \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49464-iso-8859-1.txt b/test/webapp/bug49nnn/bug49464-iso-8859-1.txt new file mode 100644 index 0000000..9c95a6b --- /dev/null +++ b/test/webapp/bug49nnn/bug49464-iso-8859-1.txt @@ -0,0 +1 @@ +½ \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49464-utf-8-bom.txt b/test/webapp/bug49nnn/bug49464-utf-8-bom.txt new file mode 100644 index 0000000..6ee21c1 --- /dev/null +++ b/test/webapp/bug49nnn/bug49464-utf-8-bom.txt @@ -0,0 +1 @@ +½ \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49464-utf-8.txt b/test/webapp/bug49nnn/bug49464-utf-8.txt new file mode 100644 index 0000000..66f5eb7 --- /dev/null +++ b/test/webapp/bug49nnn/bug49464-utf-8.txt @@ -0,0 +1 @@ +½ \ No newline at end of file diff --git a/test/webapp/bug49nnn/bug49555.jsp b/test/webapp/bug49nnn/bug49555.jsp new file mode 100644 index 0000000..de65fe9 --- /dev/null +++ b/test/webapp/bug49nnn/bug49555.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> +

    00-${bugs:bug49555()}

    diff --git a/test/webapp/bug49nnn/bug49726a.jsp b/test/webapp/bug49nnn/bug49726a.jsp new file mode 100644 index 0000000..a9e177b --- /dev/null +++ b/test/webapp/bug49nnn/bug49726a.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ page contentType="text/html"%> + + Bug 49726 test case + +

    OK

    + + + diff --git a/test/webapp/bug49nnn/bug49726b.jsp b/test/webapp/bug49nnn/bug49726b.jsp new file mode 100644 index 0000000..eaf7737 --- /dev/null +++ b/test/webapp/bug49nnn/bug49726b.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> + + Bug 49726 test case + +

    OK

    + + + diff --git a/test/webapp/bug49nnn/bug49799.jsp b/test/webapp/bug49nnn/bug49799.jsp new file mode 100644 index 0000000..8fd39db --- /dev/null +++ b/test/webapp/bug49nnn/bug49799.jsp @@ -0,0 +1,40 @@ +<%-- + 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. +--%> + + color:red + 00-Red + + + color:red + 01-Not Red + + + color:red + 02-Red + + + color:red + 03-Not Red + + + color:red + 04-Red + + + color:red + 05-Not Red + \ No newline at end of file diff --git a/test/webapp/bug53257/foo bar.jsp b/test/webapp/bug53257/foo bar.jsp new file mode 100644 index 0000000..9863826 --- /dev/null +++ b/test/webapp/bug53257/foo bar.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug53257/foo bar.txt b/test/webapp/bug53257/foo bar.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug53257/foo bar.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug53257/foo bar/foobar.jsp b/test/webapp/bug53257/foo bar/foobar.jsp new file mode 100644 index 0000000..9863826 --- /dev/null +++ b/test/webapp/bug53257/foo bar/foobar.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug53257/foo bar/foobar.txt b/test/webapp/bug53257/foo bar/foobar.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug53257/foo bar/foobar.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug53257/foo#bar.jsp b/test/webapp/bug53257/foo#bar.jsp new file mode 100644 index 0000000..9863826 --- /dev/null +++ b/test/webapp/bug53257/foo#bar.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug53257/foo#bar.txt b/test/webapp/bug53257/foo#bar.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug53257/foo#bar.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug53257/foo%bar.jsp b/test/webapp/bug53257/foo%bar.jsp new file mode 100644 index 0000000..9863826 --- /dev/null +++ b/test/webapp/bug53257/foo%bar.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug53257/foo%bar.txt b/test/webapp/bug53257/foo%bar.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug53257/foo%bar.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug53257/foo&bar.jsp b/test/webapp/bug53257/foo&bar.jsp new file mode 100644 index 0000000..9863826 --- /dev/null +++ b/test/webapp/bug53257/foo&bar.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug53257/foo&bar.txt b/test/webapp/bug53257/foo&bar.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug53257/foo&bar.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug53257/foo+bar.jsp b/test/webapp/bug53257/foo+bar.jsp new file mode 100644 index 0000000..9863826 --- /dev/null +++ b/test/webapp/bug53257/foo+bar.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug53257/foo+bar.txt b/test/webapp/bug53257/foo+bar.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug53257/foo+bar.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug53257/foo;bar.jsp b/test/webapp/bug53257/foo;bar.jsp new file mode 100644 index 0000000..9863826 --- /dev/null +++ b/test/webapp/bug53257/foo;bar.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug53257/foo;bar.txt b/test/webapp/bug53257/foo;bar.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug53257/foo;bar.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug53257/index.jsp b/test/webapp/bug53257/index.jsp new file mode 100644 index 0000000..aa1f9dc --- /dev/null +++ b/test/webapp/bug53257/index.jsp @@ -0,0 +1,36 @@ +<%-- + 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. +--%> +<%@page contentType="text/plain; charset=UTF-8" +%><%@page import="java.net.URL,java.net.URLConnection"%><% + String[] testFiles = new String[] {"foo;bar.txt", "foo&bar.txt", + "foo#bar.txt", "foo%bar.txt", "foo+bar.txt", "foo bar.txt", + "foo bar/foobar.txt"}; + for (String testFile : testFiles) { + URL url = application.getResource("/bug53257/" + testFile); + if (url == null) { + out.print("FAIL (url) - " + testFile + "\n"); + } else { + URLConnection conn = url.openConnection(); + long lastModified = conn.getLastModified(); + if (lastModified == -1) { + out.print("FAIL (last modified)- " + testFile + "\n"); + } else { + out.print("PASS - " + testFile + "\n"); + } + } + } +%> \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug50408.jsp b/test/webapp/bug5nnnn/bug50408.jsp new file mode 100644 index 0000000..31e9635 --- /dev/null +++ b/test/webapp/bug5nnnn/bug50408.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> +<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + +

    ${name} : ${value}

    +
    +
    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug51544.jsp b/test/webapp/bug5nnnn/bug51544.jsp new file mode 100644 index 0000000..ff26692 --- /dev/null +++ b/test/webapp/bug5nnnn/bug51544.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> +<%@ page import="java.util.Collections" %> + + Bug 51544 test case + <% + pageContext.setAttribute("list",Collections.emptyList()); + %> + +

    Empty list: ${list.isEmpty()}

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug52335.jsp b/test/webapp/bug5nnnn/bug52335.jsp new file mode 100644 index 0000000..6a5716e --- /dev/null +++ b/test/webapp/bug5nnnn/bug52335.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 52335 test case + +

    00 - \% \\% <\%

    +

    01 - <\%

    +

    02 - <\%

    + + + diff --git a/test/webapp/bug5nnnn/bug53387.shtml b/test/webapp/bug5nnnn/bug53387.shtml new file mode 100644 index 0000000..34069ca --- /dev/null +++ b/test/webapp/bug5nnnn/bug53387.shtml @@ -0,0 +1,27 @@ + +

    Before

    + + + + + + + + +

    abcd

    +

    After

    \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug53465.jsp b/test/webapp/bug5nnnn/bug53465.jsp new file mode 100644 index 0000000..cb21f8b --- /dev/null +++ b/test/webapp/bug5nnnn/bug53465.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> +<%@ page import="javax.naming.Context" %> +<%@ page import="javax.naming.InitialContext" %> + + + <% + Context initCtx = new InitialContext(); + Context envCtx = (Context) initCtx.lookup("java:comp/env"); + Integer test = (Integer) envCtx.lookup("bug53465"); + + out.print("

    " + test.intValue() + "

    "); + %> + + diff --git a/test/webapp/bug5nnnn/bug53467].jsp b/test/webapp/bug5nnnn/bug53467].jsp new file mode 100644 index 0000000..102dcc3 --- /dev/null +++ b/test/webapp/bug5nnnn/bug53467].jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK

    + + diff --git a/test/webapp/bug5nnnn/bug53545.html b/test/webapp/bug5nnnn/bug53545.html new file mode 100644 index 0000000..f1f6808 --- /dev/null +++ b/test/webapp/bug5nnnn/bug53545.html @@ -0,0 +1,21 @@ + + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug53545.jsp b/test/webapp/bug5nnnn/bug53545.jsp new file mode 100644 index 0000000..1170a1f --- /dev/null +++ b/test/webapp/bug5nnnn/bug53545.jsp @@ -0,0 +1,34 @@ +<%-- + 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. +--%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1"%> +<%@ taglib prefix="bug53545" uri="/WEB-INF/bug53545.tld" %> + + +

    FAIL

    + +

    FAIL

    + +

    FAIL

    + +

    FAIL

    +
    +

    FAIL

    +
    +

    FAIL

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug53986.jsp b/test/webapp/bug5nnnn/bug53986.jsp new file mode 100644 index 0000000..b8664fe --- /dev/null +++ b/test/webapp/bug5nnnn/bug53986.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> +<%--- Test comment ---%> + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54011.jsp b/test/webapp/bug5nnnn/bug54011.jsp new file mode 100644 index 0000000..9ae4374 --- /dev/null +++ b/test/webapp/bug5nnnn/bug54011.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" session="false"%> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + +

    +

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54012.jsp b/test/webapp/bug5nnnn/bug54012.jsp new file mode 100644 index 0000000..370abbf --- /dev/null +++ b/test/webapp/bug5nnnn/bug54012.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" session="false"%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54144.jsp b/test/webapp/bug5nnnn/bug54144.jsp new file mode 100644 index 0000000..3bed00c --- /dev/null +++ b/test/webapp/bug5nnnn/bug54144.jsp @@ -0,0 +1,35 @@ +<%-- + 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. +--%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" session="true" + import="java.io.Reader,java.io.StringReader" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + +

    OK - 1

    +

    ${'OK - '}${1+1}

    +

    FAIL

    + + + <% + Reader r = new StringReader("OK - 4"); + session.setAttribute("reader", r); + %> +

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54241a.jsp b/test/webapp/bug5nnnn/bug54241a.jsp new file mode 100644 index 0000000..f0f1219 --- /dev/null +++ b/test/webapp/bug5nnnn/bug54241a.jsp @@ -0,0 +1,28 @@ +<%-- + 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. +--%> + + + <% + String nullString = null; + %> + +

    01: <%= (Object) null %>

    + +

    02: <%= nullString %>

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54241b.jsp b/test/webapp/bug5nnnn/bug54241b.jsp new file mode 100644 index 0000000..a9907fb --- /dev/null +++ b/test/webapp/bug5nnnn/bug54241b.jsp @@ -0,0 +1,34 @@ +<%-- + 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. +--%> +<%! + class Bug54241 { + public String toString() { + return null; + } + } +%> + + + <% + Bug54241 bug54241 = new Bug54241(); + %> + +

    01: <%= bug54241 %>

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54242.jsp b/test/webapp/bug5nnnn/bug54242.jsp new file mode 100644 index 0000000..abd9009 --- /dev/null +++ b/test/webapp/bug5nnnn/bug54242.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" session="true" + import="java.io.Reader,java.io.StringReader" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + +

    OK - 1

    + +

    FAIL

    +
    +

    OK - 2

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54338.jsp b/test/webapp/bug5nnnn/bug54338.jsp new file mode 100644 index 0000000..41e78b3 --- /dev/null +++ b/test/webapp/bug5nnnn/bug54338.jsp @@ -0,0 +1,35 @@ +<%-- + 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. +--%> +<%! + public class Bug54338 { + private int answer; + public int getAnswer() { return answer; } + public void setAnswer(int answer) { this.answer = answer; } + } +%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + <% + pageContext.getSession().setAttribute("foo", new Bug54338()); + %> + +

    OK -

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54801a.jspx b/test/webapp/bug5nnnn/bug54801a.jspx new file mode 100644 index 0000000..bfd6842 --- /dev/null +++ b/test/webapp/bug5nnnn/bug54801a.jspx @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54801b.jspx b/test/webapp/bug5nnnn/bug54801b.jspx new file mode 100644 index 0000000..4187cc7 --- /dev/null +++ b/test/webapp/bug5nnnn/bug54801b.jspx @@ -0,0 +1,23 @@ + + + + + // ${foo} + out.println("Hello, world!!"); + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54821a.jspx b/test/webapp/bug5nnnn/bug54821a.jspx new file mode 100644 index 0000000..16b8b82 --- /dev/null +++ b/test/webapp/bug5nnnn/bug54821a.jspx @@ -0,0 +1,21 @@ + + + + +${Hello, world!!} + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54821b.jspx b/test/webapp/bug5nnnn/bug54821b.jspx new file mode 100644 index 0000000..266ae5f --- /dev/null +++ b/test/webapp/bug5nnnn/bug54821b.jspx @@ -0,0 +1,21 @@ + + + + +${Hello, world!! + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug54888.jsp b/test/webapp/bug5nnnn/bug54888.jsp new file mode 100644 index 0000000..7dc8cb2 --- /dev/null +++ b/test/webapp/bug5nnnn/bug54888.jsp @@ -0,0 +1,27 @@ +<%-- + 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. +--%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" session="true" + import="java.io.Reader,java.io.StringReader" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + +

    OK - ${foo}

    +
    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug55198.jsp b/test/webapp/bug5nnnn/bug55198.jsp new file mode 100644 index 0000000..2e91487 --- /dev/null +++ b/test/webapp/bug5nnnn/bug55198.jsp @@ -0,0 +1,27 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<% +request.setAttribute("text", "a&b"); +%> + + Bug 55198 test case + +

    + + + diff --git a/test/webapp/bug5nnnn/bug55262-coda.jspf b/test/webapp/bug5nnnn/bug55262-coda.jspf new file mode 100644 index 0000000..e30fee8 --- /dev/null +++ b/test/webapp/bug5nnnn/bug55262-coda.jspf @@ -0,0 +1,17 @@ +<%-- + 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. + --%> +This is a coda. diff --git a/test/webapp/bug5nnnn/bug55262-prelude.jspf b/test/webapp/bug5nnnn/bug55262-prelude.jspf new file mode 100644 index 0000000..4bb6a56 --- /dev/null +++ b/test/webapp/bug5nnnn/bug55262-prelude.jspf @@ -0,0 +1,17 @@ +<%-- + 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. + --%> +This is a prelude. diff --git a/test/webapp/bug5nnnn/bug55262.jsp b/test/webapp/bug5nnnn/bug55262.jsp new file mode 100644 index 0000000..01371e4 --- /dev/null +++ b/test/webapp/bug5nnnn/bug55262.jsp @@ -0,0 +1,17 @@ +<%-- + 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. + --%> +There should be two preludes before and two codas after this line. diff --git a/test/webapp/bug5nnnn/bug55642a.jsp b/test/webapp/bug5nnnn/bug55642a.jsp new file mode 100644 index 0000000..85d6b26 --- /dev/null +++ b/test/webapp/bug5nnnn/bug55642a.jsp @@ -0,0 +1,21 @@ +<%-- + 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. + --%> +<% request.setAttribute("target","bug55642b.jsp?foo=bar"); %> + + + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug55642b.jsp b/test/webapp/bug5nnnn/bug55642b.jsp new file mode 100644 index 0000000..3c51e9e --- /dev/null +++ b/test/webapp/bug5nnnn/bug55642b.jsp @@ -0,0 +1,18 @@ +<%-- + 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. + --%> +<% String qs = request.getQueryString(); %> +

    <%= request.getRequestURL() + ( qs == null ? "" : "?" + qs) %> \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug55807.jsp b/test/webapp/bug5nnnn/bug55807.jsp new file mode 100644 index 0000000..3177c04 --- /dev/null +++ b/test/webapp/bug5nnnn/bug55807.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" import="java.util.Map"%> +<%@ taglib prefix="bug55807" uri="http://tomcat.apache.org/bug55807" %> + + + +

    DependenciesCount: <%=_jspx_dependants.size() %>

    + <% + for (Map.Entry entry : _jspx_dependants.entrySet()) { + out.println("

    " + entry.getKey() + " : " + entry.getValue() + "

    "); + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug56029.jspx b/test/webapp/bug5nnnn/bug56029.jspx new file mode 100644 index 0000000..84f9e3a --- /dev/null +++ b/test/webapp/bug5nnnn/bug56029.jspx @@ -0,0 +1,26 @@ + + + + + + + + [${limitA}]:[${limitB}] + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug56147.jsp b/test/webapp/bug5nnnn/bug56147.jsp new file mode 100644 index 0000000..ffd795e --- /dev/null +++ b/test/webapp/bug5nnnn/bug56147.jsp @@ -0,0 +1,33 @@ +<%-- + 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. +--%> +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" + import="java.util.List,java.util.ArrayList,org.apache.el.TesterBeanD"%> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + <% + List list = new ArrayList<>(); + list.add(new TesterBeanD()); + pageContext.setAttribute("listItems", list); + %> + + +

    ${item.addThing("h")}

    +
    +

    00-OK

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug56265.jsp b/test/webapp/bug5nnnn/bug56265.jsp new file mode 100644 index 0000000..6b995e4 --- /dev/null +++ b/test/webapp/bug5nnnn/bug56265.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<% +request.setAttribute("text", "World <&>"); +request.setAttribute("textQuote", "'World <&>'"); +%> + + Bug 56265 test case + +

    [1: ]

    +

    [2: ]

    +

    [3: ]

    +

    [4: ]

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug56334and56561.jspx b/test/webapp/bug5nnnn/bug56334and56561.jspx new file mode 100644 index 0000000..12c0694 --- /dev/null +++ b/test/webapp/bug5nnnn/bug56334and56561.jspx @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 05x: + + + + + + 07a: + 07b: + 07c: + 07d: + 07e: + 07f: + 07g: + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug56529.jsp b/test/webapp/bug5nnnn/bug56529.jsp new file mode 100644 index 0000000..8b54d7f --- /dev/null +++ b/test/webapp/bug5nnnn/bug56529.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> +[1: + +] + +[2: + +] diff --git a/test/webapp/bug5nnnn/bug56581.jsp b/test/webapp/bug5nnnn/bug56581.jsp new file mode 100644 index 0000000..963e690 --- /dev/null +++ b/test/webapp/bug5nnnn/bug56581.jsp @@ -0,0 +1,27 @@ +<%-- + 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. + +--%><%@page contentType="text/plain;charset=ISO-8859-1" +%><% for (int i=0; i<1000; i++) { + out.print(i); + out.print(" Hello world!\n"); +} +if (true) throw new RuntimeException("Oops. Testing exception handling."); +for (int i=0; i<1000; i++) { + out.print(i); + out.print(" Footer"); +} +%> \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug56612.jsp b/test/webapp/bug5nnnn/bug56612.jsp new file mode 100644 index 0000000..79b1150 --- /dev/null +++ b/test/webapp/bug5nnnn/bug56612.jsp @@ -0,0 +1,17 @@ +<%-- + 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. +--%> +

    00-${'\'\''}

    \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug57141.jsp b/test/webapp/bug5nnnn/bug57141.jsp new file mode 100644 index 0000000..22ff053 --- /dev/null +++ b/test/webapp/bug5nnnn/bug57141.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> + + +

    00-${Boolean.TRUE}

    +

    01-${Boolean.FALSE}

    + <% + pageContext.getELContext().getImportHandler().importStatic("java.lang.Integer.MAX_VALUE"); + %> +

    02-${MAX_VALUE}

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug57142.jsp b/test/webapp/bug5nnnn/bug57142.jsp new file mode 100644 index 0000000..142f33b --- /dev/null +++ b/test/webapp/bug5nnnn/bug57142.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@page import="java.math.*, java.util.Collections" %> + + +

    00-${DispatcherType.ASYNC}

    +

    01-${RoundingMode.HALF_UP}

    +

    02-${Collections.EMPTY_LIST.stream().count()}

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug57441.jsp b/test/webapp/bug5nnnn/bug57441.jsp new file mode 100644 index 0000000..228dec8 --- /dev/null +++ b/test/webapp/bug5nnnn/bug57441.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> +<%@page import="java.math.*, java.util.Collections" %> + + +

    00-${incr = x->x+1; incr(10)}

    + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug57601.jsp b/test/webapp/bug5nnnn/bug57601.jsp new file mode 100644 index 0000000..c7ff6bb --- /dev/null +++ b/test/webapp/bug5nnnn/bug57601.jsp @@ -0,0 +1,18 @@ +<%-- + 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. + --%> +Outer + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug57601.txt b/test/webapp/bug5nnnn/bug57601.txt new file mode 100644 index 0000000..1e3f545 --- /dev/null +++ b/test/webapp/bug5nnnn/bug57601.txt @@ -0,0 +1,16 @@ +# 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. + +Inner \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug58096.jsp b/test/webapp/bug5nnnn/bug58096.jsp new file mode 100644 index 0000000..73171a5 --- /dev/null +++ b/test/webapp/bug5nnnn/bug58096.jsp @@ -0,0 +1,17 @@ +<%-- + 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. +--%> +<%= org.apache.tomcat.Bug58096.class.getProtectionDomain().getCodeSource().getLocation() %> \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug58178.jsp b/test/webapp/bug5nnnn/bug58178.jsp new file mode 100644 index 0000000..13341a4 --- /dev/null +++ b/test/webapp/bug5nnnn/bug58178.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + +Catch Tag Example + + + + + + +Parsed value: + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug58178b.jsp b/test/webapp/bug5nnnn/bug58178b.jsp new file mode 100644 index 0000000..80df555 --- /dev/null +++ b/test/webapp/bug5nnnn/bug58178b.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug58178c.jsp b/test/webapp/bug5nnnn/bug58178c.jsp new file mode 100644 index 0000000..5709727 --- /dev/null +++ b/test/webapp/bug5nnnn/bug58178c.jsp @@ -0,0 +1,63 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%!static class TestListener implements jakarta.el.ELContextListener { + + private int jspCount = 0; + private int tagCount = 0; + + @Override + public void contextCreated(jakarta.el.ELContextEvent event) { + jakarta.el.ELContext elContext = event.getELContext(); + if (elContext instanceof org.apache.jasper.el.ELContextImpl) { + jspCount++; + } else { + tagCount++; + } + (new Exception()).printStackTrace(); + } + + public int getJspCount() { + return jspCount; + } + + public int getTagCount() { + return tagCount; + } +} + +static TestListener listener = new TestListener(); + +private boolean listenerAdded;%> +<% +synchronized(this) { + if (!listenerAdded) { + JspFactory factory = JspFactory.getDefaultFactory(); + JspApplicationContext jspApplicationContext = factory.getJspApplicationContext(application); + jspApplicationContext.addELContextListener(listener); + listenerAdded = true; + } +} +%> + + +

    JSP count: <%= listener.getJspCount() %>

    +

    Tag count: <%= listener.getTagCount() %>

    + +

    JSP count: <%= listener.getJspCount() %>

    +

    Tag count: <%= listener.getTagCount() %>

    + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug58444a.jsp b/test/webapp/bug5nnnn/bug58444a.jsp new file mode 100644 index 0000000..f25ba54 --- /dev/null +++ b/test/webapp/bug5nnnn/bug58444a.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ page extends="org.apache.jasper.runtime.TesterHttpJspBase" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + + \ No newline at end of file diff --git a/test/webapp/bug5nnnn/bug58444b.jsp b/test/webapp/bug5nnnn/bug58444b.jsp new file mode 100644 index 0000000..f25ba54 --- /dev/null +++ b/test/webapp/bug5nnnn/bug58444b.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ page extends="org.apache.jasper.runtime.TesterHttpJspBase" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + + \ No newline at end of file diff --git a/test/webapp/bug66609/_listing.xslt b/test/webapp/bug66609/_listing.xslt new file mode 100644 index 0000000..ee13205 --- /dev/null +++ b/test/webapp/bug66609/_listing.xslt @@ -0,0 +1,90 @@ + + + + + + + + + + + + Sample Directory Listing For + <xsl:value-of select="@directory"/> + + + + +

    Sample Directory Listing For + +

    +
    + + + + + + + +
    FilenameSizeLast Modified
    + +
    +

    Apache Tomcat/11.0

    + + +
    + + + + + + + +
    +
    +
    + + + + + + +
    +
    + + +
    + + +
    + + +
    + +
    \ No newline at end of file diff --git a/test/webapp/bug66609/a&a.txt b/test/webapp/bug66609/a&a.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug66609/a&a.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug66609/b'b.txt b/test/webapp/bug66609/b'b.txt new file mode 100644 index 0000000..a0aba93 --- /dev/null +++ b/test/webapp/bug66609/b'b.txt @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug60032.jsp b/test/webapp/bug6nnnn/bug60032.jsp new file mode 100644 index 0000000..cbee5e4 --- /dev/null +++ b/test/webapp/bug6nnnn/bug60032.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/testerFunctions" prefix="fn" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 60032 test case + + + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug60431.jsp b/test/webapp/bug6nnnn/bug60431.jsp new file mode 100644 index 0000000..845269b --- /dev/null +++ b/test/webapp/bug6nnnn/bug60431.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/testerFunctions" prefix="fn" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 60431 test case + + + + + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug61854.jsp b/test/webapp/bug6nnnn/bug61854.jsp new file mode 100644 index 0000000..6b6d128 --- /dev/null +++ b/test/webapp/bug6nnnn/bug61854.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Bug 60431 test case + + < + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug62453.jsp b/test/webapp/bug6nnnn/bug62453.jsp new file mode 100644 index 0000000..80efe30 --- /dev/null +++ b/test/webapp/bug6nnnn/bug62453.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + +

    Example of Uninitialized Tag Attributes

    + + + diff --git a/test/webapp/bug6nnnn/bug63359a.jsp b/test/webapp/bug6nnnn/bug63359a.jsp new file mode 100644 index 0000000..400b582 --- /dev/null +++ b/test/webapp/bug6nnnn/bug63359a.jsp @@ -0,0 +1,202 @@ +<%-- + 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. +--%> + + + Bug 63359 test cases + + +

    01-${bean.booleanPrimitive}

    + +

    02-${bean.booleanPrimitive}

    + +

    03-${bean.booleanPrimitive}

    + +

    04-${bean.booleanPrimitive}

    + +

    05-${bean.booleanPrimitive}

    + + +

    11-${bean.booleanObject}

    + +

    12-${bean.booleanObject}

    + +

    13-${bean.booleanObject}

    + +

    14-${bean.booleanObject}

    + +

    15-${bean.booleanPrimitive}

    + + +

    21-${bean.bytePrimitive}

    + +

    22-${bean.bytePrimitive}

    + +

    23-${bean.bytePrimitive}

    + +

    24-${bean.bytePrimitive}

    + + +

    31-${bean.byteObject}

    + +

    32-${bean.byteObject}

    + +

    33-${bean.byteObject}

    + +

    34-${bean.byteObject}

    + + +

    41-${bean.charPrimitive}

    + +

    42-${bean.charPrimitive}

    + +

    43-${bean.charPrimitive}

    + +

    44-${bean.charPrimitive}

    + + +

    51-${bean.charObject}

    + +

    52-${bean.charObject}

    + +

    53-${bean.charObject}

    + +

    54-${bean.charObject}

    + + +

    61-${bean.doublePrimitive}

    + +

    62-${bean.doublePrimitive}

    + +

    63-${bean.doublePrimitive}

    + +

    64-${bean.doublePrimitive}

    + + +

    71-${bean.doubleObject}

    + +

    72-${bean.doubleObject}

    + +

    73-${bean.doubleObject}

    + +

    74-${bean.doubleObject}

    + + +

    81-${bean.intPrimitive}

    + +

    82-${bean.intPrimitive}

    + +

    83-${bean.intPrimitive}

    + +

    84-${bean.intPrimitive}

    + + +

    91-${bean.intObject}

    + +

    92-${bean.intObject}

    + +

    93-${bean.intObject}

    + +

    94-${bean.intObject}

    + + +

    101-${bean.floatPrimitive}

    + +

    102-${bean.floatPrimitive}

    + +

    103-${bean.floatPrimitive}

    + +

    104-${bean.floatPrimitive}

    + + +

    111-${bean.floatObject}

    + +

    112-${bean.floatObject}

    + +

    113-${bean.floatObject}

    + +

    114-${bean.floatObject}

    + + +

    121-${bean.longPrimitive}

    + +

    122-${bean.longPrimitive}

    + +

    123-${bean.longPrimitive}

    + +

    124-${bean.longPrimitive}

    + + +

    131-${bean.longObject}

    + +

    132-${bean.longObject}

    + +

    133-${bean.longObject}

    + +

    134-${bean.longObject}

    + + +

    141-${bean.shortPrimitive}

    + +

    142-${bean.shortPrimitive}

    + +

    143-${bean.shortPrimitive}

    + +

    144-${bean.shortPrimitive}

    + + +

    151-${bean.shortObject}

    + +

    152-${bean.shortObject}

    + +

    153-${bean.shortObject}

    + +

    154-${bean.shortObject}

    + + +

    161-${bean.stringValue}

    + +

    162-${bean.stringValue}

    + +

    163-${bean.stringValue}

    + +

    164-${bean.stringValue}

    + + +

    171-${bean.objectValue}

    + +

    172-${bean.objectValue}

    + +

    173-${bean.objectValue}

    + +

    174-${bean.objectValue}

    + + +

    181-${bean.testerTypeA}

    + +

    182-${bean.testerTypeA}

    + +

    183-${bean.testerTypeA}

    + +

    184-${bean.testerTypeA}

    + + +

    191-${bean.testerTypeB}

    + + + diff --git a/test/webapp/bug6nnnn/bug64373.jsp b/test/webapp/bug6nnnn/bug64373.jsp new file mode 100644 index 0000000..f7fe445 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64373.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bug64373" prefix="bugs" %> + + Bug 64373 test case + + + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-bigdecimal.jsp b/test/webapp/bug6nnnn/bug64872-bigdecimal.jsp new file mode 100644 index 0000000..9279a04 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-bigdecimal.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 BigDecimal test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-biginteger.jsp b/test/webapp/bug6nnnn/bug64872-biginteger.jsp new file mode 100644 index 0000000..a81d995 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-biginteger.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 BigInteger test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-boolean.jsp b/test/webapp/bug6nnnn/bug64872-boolean.jsp new file mode 100644 index 0000000..fed123c --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-boolean.jsp @@ -0,0 +1,37 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 Boolean.TRUE test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    +

    03 The value of foo is []

    +

    04 The value of foo is []

    +

    05 The value of foo is []

    +

    06 The value of foo is []

    + <%-- +

    04 The value of foo is []

    + --%> + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-byte.jsp b/test/webapp/bug6nnnn/bug64872-byte.jsp new file mode 100644 index 0000000..e8684dd --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-byte.jsp @@ -0,0 +1,32 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 Byte test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    +

    03 The value of foo is []

    +

    04 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-character.jsp b/test/webapp/bug6nnnn/bug64872-character.jsp new file mode 100644 index 0000000..1821399 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-character.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 Character test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-double.jsp b/test/webapp/bug6nnnn/bug64872-double.jsp new file mode 100644 index 0000000..3d5d32d --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-double.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 Double test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-float.jsp b/test/webapp/bug6nnnn/bug64872-float.jsp new file mode 100644 index 0000000..6f70dcf --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-float.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 Float test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-integer.jsp b/test/webapp/bug6nnnn/bug64872-integer.jsp new file mode 100644 index 0000000..b19ca58 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-integer.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 Integer test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-long.jsp b/test/webapp/bug6nnnn/bug64872-long.jsp new file mode 100644 index 0000000..6ad9407 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-long.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 Long test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-primitive-boolean.jsp b/test/webapp/bug6nnnn/bug64872-primitive-boolean.jsp new file mode 100644 index 0000000..2fee98f --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-primitive-boolean.jsp @@ -0,0 +1,37 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 boolean false test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    +

    03 The value of foo is []

    +

    04 The value of foo is []

    +

    05 The value of foo is []

    +

    06 The value of foo is []

    + <%-- +

    04 The value of foo is []

    + --%> + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-primitive-byte.jsp b/test/webapp/bug6nnnn/bug64872-primitive-byte.jsp new file mode 100644 index 0000000..3507773 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-primitive-byte.jsp @@ -0,0 +1,32 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 byte test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    +

    03 The value of foo is []

    +

    04 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-primitive-character.jsp b/test/webapp/bug6nnnn/bug64872-primitive-character.jsp new file mode 100644 index 0000000..4d74290 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-primitive-character.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 char test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-primitive-double.jsp b/test/webapp/bug6nnnn/bug64872-primitive-double.jsp new file mode 100644 index 0000000..ad83637 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-primitive-double.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 double test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-primitive-float.jsp b/test/webapp/bug6nnnn/bug64872-primitive-float.jsp new file mode 100644 index 0000000..f46ff67 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-primitive-float.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 float test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-primitive-integer.jsp b/test/webapp/bug6nnnn/bug64872-primitive-integer.jsp new file mode 100644 index 0000000..091f8b0 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-primitive-integer.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 int test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-primitive-long.jsp b/test/webapp/bug6nnnn/bug64872-primitive-long.jsp new file mode 100644 index 0000000..b5861b3 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-primitive-long.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 long test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-primitive-short.jsp b/test/webapp/bug6nnnn/bug64872-primitive-short.jsp new file mode 100644 index 0000000..96f2c54 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-primitive-short.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 short test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-short.jsp b/test/webapp/bug6nnnn/bug64872-short.jsp new file mode 100644 index 0000000..98a3d39 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-short.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 Short test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-string.jsp b/test/webapp/bug6nnnn/bug64872-string.jsp new file mode 100644 index 0000000..658d4b0 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-string.jsp @@ -0,0 +1,33 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 String test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <%-- +

    03 The value of foo is []

    + --%> + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872-timeunit.jsp b/test/webapp/bug6nnnn/bug64872-timeunit.jsp new file mode 100644 index 0000000..1f0fb33 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872-timeunit.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872 TimeUnit test case + + <% + for (int i=0; i < Integer.parseInt(request.getParameter("iterations")); i++) { + %> +

    01 The value of foo is []

    +

    02 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug64872b-timeunit.jsp b/test/webapp/bug6nnnn/bug64872b-timeunit.jsp new file mode 100644 index 0000000..8425671 --- /dev/null +++ b/test/webapp/bug6nnnn/bug64872b-timeunit.jsp @@ -0,0 +1,29 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 64872b TimeUnit test case + + <% + for (int i=0; i < 5000; i++) { + %> +

    01 The value of foo is []

    + <% + } + %> + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug65377.jsp b/test/webapp/bug6nnnn/bug65377.jsp new file mode 100644 index 0000000..9cb8b73 --- /dev/null +++ b/test/webapp/bug6nnnn/bug65377.jsp @@ -0,0 +1,39 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/tag-setters" prefix="ts" %> + + Bug 65377 test case + +

    boolean01::

    +

    boolean02::

    +

    boolean03::

    +

    byte01::

    +

    byte02::

    +

    short01::

    +

    short02::

    +

    character01::

    +

    character02::

    +

    integer01::

    +

    integer02::

    +

    long01::

    +

    long02::

    +

    float01::

    +

    float02::

    +

    double01::

    +

    double02::

    + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug65390-empty.jsp b/test/webapp/bug6nnnn/bug65390-empty.jsp new file mode 100644 index 0000000..2f7bb10 --- /dev/null +++ b/test/webapp/bug6nnnn/bug65390-empty.jsp @@ -0,0 +1,17 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug65390.jsp b/test/webapp/bug6nnnn/bug65390.jsp new file mode 100644 index 0000000..59317ec --- /dev/null +++ b/test/webapp/bug6nnnn/bug65390.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ include file="bug65390-empty.jsp" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + qqq + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug66441.jsp b/test/webapp/bug6nnnn/bug66441.jsp new file mode 100644 index 0000000..ecc45c7 --- /dev/null +++ b/test/webapp/bug6nnnn/bug66441.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ page import="static java.lang.Long.MIN_VALUE"%> + + +

    EL - Long min value is ${MIN_VALUE}

    +

    JSP - Long min value is <%= MIN_VALUE %>

    + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug66582.jsp b/test/webapp/bug6nnnn/bug66582.jsp new file mode 100644 index 0000000..4def99d --- /dev/null +++ b/test/webapp/bug6nnnn/bug66582.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> +<%@ page import="static org.apache.tomcat.unittest.TesterBug66582.DATA"%> + + +

    ${DATA}

    + + \ No newline at end of file diff --git a/test/webapp/bug6nnnn/bug69303.txt b/test/webapp/bug6nnnn/bug69303.txt new file mode 100644 index 0000000..972f9d4 --- /dev/null +++ b/test/webapp/bug6nnnn/bug69303.txt @@ -0,0 +1,18 @@ +================================================================================ + 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. +================================================================================ + +The content is not important. \ No newline at end of file diff --git a/test/webapp/echo-params.jsp b/test/webapp/echo-params.jsp new file mode 100644 index 0000000..81ddea5 --- /dev/null +++ b/test/webapp/echo-params.jsp @@ -0,0 +1,33 @@ +<%-- + 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. +--%> +<%@page import="java.util.Enumeration" %> + + +<% +Enumeration params = request.getParameterNames(); +while (params.hasMoreElements()) { + String param = params.nextElement(); + + String[] values = request.getParameterValues(param); + for (String value : values) { + // Don't do this in a real webapp - XSS issues + out.println("

    " + param + " - " + value + "

    "); + } +} +%> + + diff --git a/test/webapp/el-method.jsp b/test/webapp/el-method.jsp new file mode 100644 index 0000000..bbb74c1 --- /dev/null +++ b/test/webapp/el-method.jsp @@ -0,0 +1,38 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ page import="org.apache.el.TesterBeanA" %> +<%@ page import="org.apache.el.TesterBeanB" %> + + EL method test cases + + <% + TesterBeanA beanA = new TesterBeanA(); + TesterBeanB beanB = new TesterBeanB(); + beanB.setName("Tomcat"); + beanA.setBean(beanB); + pageContext.setAttribute("testBeanA", beanA, PageContext.REQUEST_SCOPE); + pageContext.setAttribute("testBeanB", beanB, PageContext.REQUEST_SCOPE); + %> + + + + + + + + \ No newline at end of file diff --git a/test/webapp/el-misc-no-quote-attribute-el.jsp b/test/webapp/el-misc-no-quote-attribute-el.jsp new file mode 100644 index 0000000..2c30b9e --- /dev/null +++ b/test/webapp/el-misc-no-quote-attribute-el.jsp @@ -0,0 +1,44 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Misc EL test cases + +

    00-\\\"\${'hello world'}

    +

    01-\\\"\\${'hello world'}

    + + + + + + + + + + + + + + + + + + +

    20-${{1,2,3,4}.stream().max().orElse(-1)}

    + +

    22-${{1,2,3,4}.stream().sorted().map(u->{"value":u+10}).toList()}

    + \ No newline at end of file diff --git a/test/webapp/el-misc-with-quote-attribute-el.jsp b/test/webapp/el-misc-with-quote-attribute-el.jsp new file mode 100644 index 0000000..b55e88d --- /dev/null +++ b/test/webapp/el-misc-with-quote-attribute-el.jsp @@ -0,0 +1,44 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Misc EL test cases + +

    00-\\\"\${'hello world'}

    +

    01-\\\"\\${'hello world'}

    + + + + + + + + + + + + + + + + + + +

    20-${{1,2,3,4}.stream().max().orElse(-1)}

    + +

    22-${{1,2,3,4}.stream().sorted().map(u->{"value":u+10}).toList()}

    + \ No newline at end of file diff --git a/test/webapp/index.html b/test/webapp/index.html new file mode 100644 index 0000000..392fa41 --- /dev/null +++ b/test/webapp/index.html @@ -0,0 +1,24 @@ + + + + Index page + + +

    This is the index page served by the default Servlet.

    + + \ No newline at end of file diff --git a/test/webapp/index.html.br b/test/webapp/index.html.br new file mode 100644 index 0000000000000000000000000000000000000000..1428b9ced3f405349cbeebe978fe87dde70b2268 GIT binary patch literal 367 zcmV-#0g(P1r2_yIN(;}&y818a&8CAZpmn59{`dE7KCs^R&i%`>wo&$h&IA~hg<;@L z>cPKV;pL{9Z>d)^NV1hwW>V69;OxcgVOcvKRX}KRZ7kJY6H2(zo(0ZZovRNGLIE*h z!Q7@bSdfWT1xPCd;*rZr_}R1BF66pu-9-2t1vDv*y0a$wRxRhW%y#wH!IIGuxT9%5 z>J0=M#bTwvs5Hkv68P8t`2PmATjvfqSo{~K^a5$_w_JK)*JI;oWK==XUpPMz0GKsp zkc5uGru(bDxSxOGplft3grlwApAj-3;=cC7aU!@MQag*okB8d}DdT64w7)qe%2MU1 zD!f|5Mo6CPf9CS_THdtdR^|RmKj-egV|lY*(H}SrOgvW~{2WcC2dNR#LDb1lm$_V+ zqEYhsi&Odnd*2pT_GI4@@T`F?Q5S%5W7gkfaPfSgRDo6ZcbL>Oz)F+P+BHqg0l;uy Nogoi?4qx08AP$+-zFPnQ literal 0 HcmV?d00001 diff --git a/test/webapp/index.html.gz b/test/webapp/index.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..5aca6e92494cf31c4d35d42d7a7e2f65a382a1a4 GIT binary patch literal 562 zcmV-20?qv&iwFqiU~*Id18Ht#Wq2-VbZu+^Wl~>nn=lZ6&!@QMsg)9#_PC))tW7ku zib#RB%A*4ZoCdaMn~;3_oguW5#tX82_xpDjeOa#;054Rav{-^u;2MNji-Lcds@xz9 z!cO(F6poaHhdA3k0w2+Uk_gH`rwlSkp`K$LT6CjA%Aa*F74FEF45k1}K$ha)F9E=hIGIv zxwnz+W+irl9XZ+LQS~-wzC;6}aYYy-6;Si^Jvdv(BfTw->oaD(q<5Dv^aGu@XBOa>!=J6jf+{N5m#lkHBh>1QM*7{*Xgacf zmX_s}jM7p^Zd@Vzzo}rcUT+pr8^59Jv-Z33psTGf5OqH@+;K>!On? zuVX``H@V*mvLily^npclo<`yyN~}cRIyPs{ExI5I^#nN~F|^n650C6aN~Qw<0PFn? AGXMYp literal 0 HcmV?d00001 diff --git a/test/webapp/jsp/doc-version-invalid/document-0.4.jspx b/test/webapp/jsp/doc-version-invalid/document-0.4.jspx new file mode 100644 index 0000000..8ad57be --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-0.4.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-invalid/document-1.1.jspx b/test/webapp/jsp/doc-version-invalid/document-1.1.jspx new file mode 100644 index 0000000..38d8391 --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-1.1.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-invalid/document-1.2.1.jspx b/test/webapp/jsp/doc-version-invalid/document-1.2.1.jspx new file mode 100644 index 0000000..dcadc60 --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-1.2.1.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-invalid/document-1.3.jspx b/test/webapp/jsp/doc-version-invalid/document-1.3.jspx new file mode 100644 index 0000000..deaa6cb --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-1.3.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-invalid/document-1.9.jspx b/test/webapp/jsp/doc-version-invalid/document-1.9.jspx new file mode 100644 index 0000000..4634f91 --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-1.9.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-invalid/document-2.4.jspx b/test/webapp/jsp/doc-version-invalid/document-2.4.jspx new file mode 100644 index 0000000..d10b068 --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-2.4.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-invalid/document-3.2.jspx b/test/webapp/jsp/doc-version-invalid/document-3.2.jspx new file mode 100644 index 0000000..7e9fba1 --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-3.2.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-invalid/document-4.0.jspx b/test/webapp/jsp/doc-version-invalid/document-4.0.jspx new file mode 100644 index 0000000..9fcdc6b --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-4.0.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-invalid/document-5.4.jspx b/test/webapp/jsp/doc-version-invalid/document-5.4.jspx new file mode 100644 index 0000000..8126079 --- /dev/null +++ b/test/webapp/jsp/doc-version-invalid/document-5.4.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-valid/document-1.2.jspx b/test/webapp/jsp/doc-version-valid/document-1.2.jspx new file mode 100644 index 0000000..0530e6b --- /dev/null +++ b/test/webapp/jsp/doc-version-valid/document-1.2.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-valid/document-2.0.jspx b/test/webapp/jsp/doc-version-valid/document-2.0.jspx new file mode 100644 index 0000000..1aeb3af --- /dev/null +++ b/test/webapp/jsp/doc-version-valid/document-2.0.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-valid/document-2.1.jspx b/test/webapp/jsp/doc-version-valid/document-2.1.jspx new file mode 100644 index 0000000..3c90e07 --- /dev/null +++ b/test/webapp/jsp/doc-version-valid/document-2.1.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-valid/document-2.2.jspx b/test/webapp/jsp/doc-version-valid/document-2.2.jspx new file mode 100644 index 0000000..7612ea1 --- /dev/null +++ b/test/webapp/jsp/doc-version-valid/document-2.2.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-valid/document-2.3.jspx b/test/webapp/jsp/doc-version-valid/document-2.3.jspx new file mode 100644 index 0000000..d074935 --- /dev/null +++ b/test/webapp/jsp/doc-version-valid/document-2.3.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-valid/document-3.0.jspx b/test/webapp/jsp/doc-version-valid/document-3.0.jspx new file mode 100644 index 0000000..11a98ab --- /dev/null +++ b/test/webapp/jsp/doc-version-valid/document-3.0.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/doc-version-valid/document-3.1.jspx b/test/webapp/jsp/doc-version-valid/document-3.1.jspx new file mode 100644 index 0000000..67f429d --- /dev/null +++ b/test/webapp/jsp/doc-version-valid/document-3.1.jspx @@ -0,0 +1,25 @@ + + + + + +

    00-Hello World

    + + +
    \ No newline at end of file diff --git a/test/webapp/jsp/encoding/README.txt b/test/webapp/jsp/encoding/README.txt new file mode 100644 index 0000000..0c4c10f --- /dev/null +++ b/test/webapp/jsp/encoding/README.txt @@ -0,0 +1,40 @@ +================================================================================ + 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. +================================================================================ + +A number of the test files in this directory specify conflicting encoding +in the BOM and and in the XML prolog. The rules for determining the actual +encoding used in the file are as follows: + +1. If there is a BOM, use the encoding defined by the BOM. + +2. If there is no BOM but there is an XML prolog, use the encoding defined by + the XML prolog. + +3. If there is no BOM and no XML prolog, use UTF-8. + + +Notes: + +1. The svn:mime-type property has been set correctly for all files. + +2. For files in UTF-8 encoding, the svn:eol-style property has been set to + native. Some svn clients do not seem to be able to handle setting that + property on files using UTF-16LE or UTF16-BE. For these files, the + line-ending used is CRLF (Windows) since that is where they were created. + +2. Notepad++ is a useful text editor for working with these files since it + provides explicit control of encoding and BOM used. \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bom-none-prolog-none.jsp b/test/webapp/jsp/encoding/bom-none-prolog-none.jsp new file mode 100644 index 0000000..8dc6bd8 --- /dev/null +++ b/test/webapp/jsp/encoding/bom-none-prolog-none.jsp @@ -0,0 +1,17 @@ +<%-- + 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. +--%> +<%@ page contentType="text/plain"%>OK \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bom-none-prolog-none.jspx b/test/webapp/jsp/encoding/bom-none-prolog-none.jspx new file mode 100644 index 0000000..e2a6dab --- /dev/null +++ b/test/webapp/jsp/encoding/bom-none-prolog-none.jspx @@ -0,0 +1,21 @@ + + + + + OK + \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bom-none-prolog-utf16be.jspx b/test/webapp/jsp/encoding/bom-none-prolog-utf16be.jspx new file mode 100644 index 0000000000000000000000000000000000000000..cb79f3e1312acb04c2f667c7f74c0c030575b925 GIT binary patch literal 2010 zcmai#T~8WO5QgWvN&mx^E1M|vBk6^;8nv4G5o!c$yhK2Xm4(70`sdp|?+nKk)HEA* zIcMg~$2;%L`SaH{Y|HL!U^kZ8t#xf|6YJa1a{FQxdv8^H$2UWgTbun3(%gD3J+r3m z+ZtM*Y{&LoZ_76AwY|db8zgJ~KjZu{=h|*%!n?yxfi=BYt#GbE3~#XzQe}{#oBOGuuLi7TMR6$><&k4$jGar)DH5H!S%@*i zQ_s(J!AJQQU{iGq4c3Y}MPIk?U~yo(ppX%HA6t?Ot;vF?$bH7X&Zda;Uz!;iE0$kw zC8BC!MRrowEw7cTaOJv#1w@o*&rwU2ihM_Q@A>xdRA7CGCt;*|%lC-PQZ!9=q9($2 z85c!a##`nZ^}N9H6phE#kapO6T;oMXta9eZJf7fRW*?k=DXJCsDm#@WHdUzqXZG*s zzJzuW6osy%@Jr{Ks_KTMz*A%LO*MJ@Ip~a5ms?5`xX@4<~;739sMKXkE1-8O| zYwkleQ6;W;CyW-YiGzjLC7~ex>QNO7VqCWx&&;1H+9kqOERW$rZHd-DOUXk$37N=!IvJp`~^;Jd|&zr@ufvP%1m8Vc2U{&fx`@W@LCf^ z4460?W#6Z0Uh1RgShkE{^wmoiGzk==-W8uF8Q3&@mhJdjtJR0zI7iyFZ}hvSUuNg% zobqg2-6y~v+8NSQ-n)G3_SH`u`2Ucpr=)+u_Xw$Ulzn&3u$WFxn zI+Q)TJdu`Bh*?qcY=V_8%MI^lbQvbCECJGN~v?S*aGD|)^y2Jd6WB0DMTw%1BkxOCm&5+cg8@2I6pMZROZ^L+bwDzHAnlQ2@f<$FwKDVi2LQ4?Xi zii@Hw<1KTIdY)r>hQ`BcNIUF3tno4<);aTI5l`?hvvco{G8$`^C$#YYNrCYsilH&}K(+RpxQcw7;B3@FSs+A{oK60$X9f zz3`!$s1ldF6GqF{#KFSrl28zT^{9#kF>YFoXYNlG?GoWCmM3tbw!-TvJM{%u8+6Y; zU0NcY{G5`vI6QS$idDbCae&2O@P)@Ne}Pj6-xq#Dd}$MpGE-NTT~u~`;4nuXyw*e! z1E!8f+4m`$=lbX=maQTfef69LO#;QJcg3ek1~yHfWuJYm)%tyJoFQ%5SNdJkFS9dr zPIz{$;S*pV?F{J&?>)W^`{E}K{eQ^RQ_?@@dxTUv%D%T?Sj;dnbL@CW4Fb&pvnUW( z9m<|vok*)F#H=WJHpNO0W)FN?b?~mLsSY!*n3&zC8gVD&=t-9OsR=O%)pS#6BIs=Q z9;IvK(h=v=+>+M+>lGR4=1BL*6@J9?3DHD9$9I_7(fv=0Uq;(C1ZsUzMaizw7_TMJ*CK8@7}9nVc?t3(*q%G)Jwj23+&q!nSo k_m+2!9ic5u<-PW#m!!Q7bB^#{yZ2v8)E^He38QWM2Uq`2j{pDw literal 0 HcmV?d00001 diff --git a/test/webapp/jsp/encoding/bom-none-prolog-utf8.jspx b/test/webapp/jsp/encoding/bom-none-prolog-utf8.jspx new file mode 100644 index 0000000..0da705d --- /dev/null +++ b/test/webapp/jsp/encoding/bom-none-prolog-utf8.jspx @@ -0,0 +1,21 @@ + + + + + OK + \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bom-utf16be-prolog-none.jsp b/test/webapp/jsp/encoding/bom-utf16be-prolog-none.jsp new file mode 100644 index 0000000000000000000000000000000000000000..051c377fb83906f98abaafb5726a6fb004e6d3a1 GIT binary patch literal 1764 zcmai#T~AX%5QgX4#Q(4%A;APey)eeFh*~0n(8LxAm!&<_M%q(*PA&X-^?7HyJ+wm1 zrh9g0XXoRccc;Jq*jwASr;Nw;$P(kkM%K5&CRQ;IkknTC{GENW1Dje8jo5WKyFz1u zZ06^ONaoxgAgQrcvQDHB%j*~l)rr$}_`dM(C; zry=r!G3GZxud+S#5ej_zaGCW7&<9?x+DJe9jU!*ndfVTW^e@;Qb1EHWUo06GJxt7;I^I!( z=msO+dmyelG;emZBWE@1+v_(uu~?o8m#3Qh}364;~DZBp*N#WRnzj_D*L8=x3(SGz8xU# z;b(wGAln?9)kv}S1^cQzp`-uO3{i$UeDM$DY@E&$p=hng>;H@PTk`0%vmW`{RYPzkCDMw zp1a~joVvumatraLLq2Kxswt|bKH{)&MvB+ssTeSGG^)8z**rBzkFjhM!8lh>dC&iC; znPy7*m#oJ~rK9Szl40=-6Ei1{ckDqt4RX$+h`9Pt&Ftohw24AID=MDN@RGso+)t}6 z(bYBeVb0n|7I=@#vwl?eD7e*>9E57R0dykB_IioZ0J(I;`E+hc>;L_V73tl1N9Yt#GbE3~#XzQe}{#oBOGuuLi7TMR6$><&k4$jGar)DH5H!S%@*i zQ_s(J!AJQQU{iGq4c3Y}MPIk?U~yo(ppX%HA6t?Ot;vF?$bH7X&Zda;Uz!;iE0$kw zC8BC!MRrowEw7cTaOJv#1w@o*&rwU2ihM_Q@A>xdRA7CGCt;*|%lC-PQZ!9=q9($2 z85c!a##`nZ^}N9H6phE#kapO6T;oMXta9eZJf7fRW*?k=DXJCsDm#@WHdUzqXZG*s zzJzuW6osy%@Jr{Ks_KTMz*A%LO*MJ@Ip~a5ms?5`xX@4<~;739sMKXkE1-8O| zYwkleQ6;W;CyW-YiGzjLC7~ex>QNO7VqCWx&&;1H+9kqOERW$rZHd-DOUXk$37N=!IvJp`~^;Jd|&zr@ufvP%1m8Vc2U{&fx`@W@LCf^ z4460?W#6Z0Uh1RgShkE{^wmoiGzk==-W8uF8Q3&@mhJdjtJR0zI7iyFZ}hvSUuNg% zobqg2-6y~v+8NSQ-n)G3_SH`u`2Ucpr=)+u_Xw$Ulzn&3u$WFxn zI+Q)TJdu`Bh*?qcY=V_8%MI^lbQvzw(qh$r}$**j-nifYZh%1&j4O%>|@k^Q@c zZy|EWI%ln1XXL1JPet8>{o>`=HHB*6HDpJ8XtN`^D)YEz+F#Bi_>oXZk&NJ3fvvFL zUieT=REbO838Q6e;$Y!*NhpZFdQ`=N7&ooPGxw*8c8PEm%M-XzTjBMTo%({S4Z3Ha zE-jHxeoo0-9G*HW#j4-nIKW~s_`+kCzrd-3?+ZU6zO;!)nW?MFE-Je|aF`@CmSwc82tX_a5Jdeen~A{y${uDe0f{JwhrSW#3ycEM}OPId;6G27%^)Srmw? z4rR}-PNY>7Vpfzqn_{I0vj;w{I(S#rREL>YOw8_6jkptX^d!st)Pxv>YPu;j5p=eD zkJ2@A>4@`bZb|F^^@@yibEJFZ3P0lcglM9lW_z#gwggt3vEu1 literal 0 HcmV?d00001 diff --git a/test/webapp/jsp/encoding/bom-utf16be-prolog-utf8.jspx b/test/webapp/jsp/encoding/bom-utf16be-prolog-utf8.jspx new file mode 100644 index 0000000000000000000000000000000000000000..efb585bf5132ee6b47b83c89e5b82435bbd91bca GIT binary patch literal 2004 zcmai#TTc^V5QgX4#Q(7N3KM8O+!#a%B1D8#LW_jU(v!t@sqL1+pI4uErXNcS#B93T zZ_e}1;rsK~wrt04ZDd2s?8dq_v8fI0+H(736??a#aGZa{C|e#F&b^xpYrZNDX^pWPtKX`+Spo1#Hj(V2X78-;`{?BGkkWS z6pPq0TSKpb_t#hmu42!S&0Xu-Pb1EfS?Q(<_5TR{ZsA*q+;Pr1E7uu0 zO7E$tyY#*Ka_o{qHSiiiQ9rbxh_A{#u9@h|c|?CC6jCJD^jU$eu-{tvP)$^c9`A(F zvNiQ!;dMzUsDJgSiUl!lT8(G!PZjBMimF(i&DgyWOQe&ZGxAms zPrWPERlmV;fW_$GOOIXt0;e{4eaIozgwW!|;-GCN1+lxNrKz5(`; z&fuQ%-s4-hFRpRu|AVKQlKchVBe;@L_T5E?#ST+vjveo)L7+Kc7X{*~Lpign8)+4V z*cD};O|jCYvj@Jd+IUyhREL>YOr71Q8gU!s$cdM^)`S>@YPut|5v1F>M`;LOGV1xX zwD@z2#klBD966 byw{%elBBokoFlx~?){e>&BsGd!f5*+5phk= literal 0 HcmV?d00001 diff --git a/test/webapp/jsp/encoding/bom-utf16le-prolog-none.jsp b/test/webapp/jsp/encoding/bom-utf16le-prolog-none.jsp new file mode 100644 index 0000000000000000000000000000000000000000..1a41c4ea38d76c842d6869617cc5e4f8ce66a0ca GIT binary patch literal 1764 zcmai#T~AX%5QgX4#Q(4%A;APey)eeFh*~0n&_s%a%hDcdBkid@rt7gms)2=qEpw)zWt({}!uQdVXGWHnc15DH7efS&K2@ zX@I<7jQEYwt85RwgilG8L&SS(ZR^;PeXgQOk}KB zez=v0s!PtYle6w(JFy?*M;fCwL=^A9QPZ6<>jbJL=q@~p0iJ4PC7#CUPVp+=6Ee%u zbdW?%gzY9Sin58f$u;VEiRBp@cdH@ouy?n{>x|gt&bMVexwDel(9rr3bTN`ZJ zqW*7?FP6TA$b$Wty>gwAqwd`obz!&8%ZY0WvA}BtBH}|AiFm8Z{97~0*YgN|WE65F zB|NLK752MJAF7Eeam}1DTDK++){a1~jo7E5D%QmK+V&Z9AA4vw2=8HeO8xgYcwHem zS#tFhCbxK{4bs`qIeCl2m9tW;`U#FhECz$GJa+jDoO<}a@*Cnymw58)YhF?F>H~+l zGg7=3PsM=B(P-X%j^?3vbRWw$5sY{BkcH?YscZj!*}jCHb)zSnMd#lj0tJNAY5 ze$F@91v+0D2X^Wmpyd|YND|$kjr6nMIP$fuxBX2?|B~G?r_xdO#gbvs!^F&q;~h1K zZZPD%2jZ$j^JX_Y(k2SgD;l0vSSeuk(7RO+@2Z;WFylMmEHRh%i`5i))F}Ba^&FvE zF$+I+1l{egP`cqnI^ulpThjV}y&@z1b8Optx2J!S58M^ABd*-PqbGM@qH=jh&1$~4 y%=<``kD^v*(Ep15Nz?~wG*!Dpe7y2HXUi?LWjl9HSJhGjV-s2RPW-tzCO=&uqtD*lQmB4?HJm zcd^T{>)ACs1=f21LwkjXC{<>` z^OD!Ge~oQR%1Ca*o_jpG9a`a}@scKrjTV>z0@7A^>+qVOx zef$j32xOaKvl=PZzF=RKCv@~*nt_Uy%P;p5v+CeQep1yP??FaBeb*hWA){`BtrSYLwk$S@&SIcsX%R zAr|o(u_Hco*pXbFdDt`Uuh$Xzkx)pPjNw^S95T+t%P&_)|x_Lb#6SDgAG3@OsA1Ny*hFOg>;rE2LALbLtj{XUNhxMc#I6b z^4t|K;?yPfm0O4}9r8)jS4~kh^$~}KGg7=3PsMb82*$a3%7acY zp{#e!Pm=&PEkDaX`?tQfma}nzv~6FR_jA@@7wDYv>{-)KfCIDx(lg%stWEpk7DxU+ z$~05bzhpf|DjikdD;XBgFfnuDc*h>Z(;(w4iioQZ)y!_5NSi3cv!deJ3@<&Ho%w0i zCAzw%KFnDM$O7+idDiD;kAhoG$w8>58$c(5Y_FFn4UtPnoKNSLwEo|(SdngybdTH+ zM?9aBO`PX=59ub^wkXvdv_26xM84%$M@v!behQA@{x>>w%6f`ab=Ju1;fZ| zyL_LWDV{s*6!Qo(Tj5-T7~Wzbq{<*eH}_LRUkzBxisDi%$|J|h7(17oQzSZdvk+s3 zr=Fkdf{*eqz^3XJ8mtv{ioR~&!Q#MnK_MgZKDHzmT9XA&k^78&olOzxzce#4RxH2V zN<`JditMDUTV5+w;mUOf3y3Jso}-p375R?r-t+C@slfUWPr^v`mhTanrD&S$L`{V4 zGA@d;jJM1+>Un|XDH@NfA?>jDxW=!Rbt|?RluK_#aLyH~BRhg$X)Ba)}!H>SUzTBm#V z=+YADvwQmpz7j(sc!gD*XH`3s!d_`dWL;!BHol$pA!?4q*k1BV&%;I$@- z7%*`(%Dzw0ywpd}v1}Q^=&P43Xc8z!y(>OVGO%g*EZgz5R;v%aagMZU-{^Nuzs%0j zIpx{5x=(;Tv@@iqym$H5?W>*#s+HnBDhj)yBK3raDZ$Vq$iWYQ&w8qbFJBr^dt}RMSnNiJ-Hc z2b6A*OGlhfb4yzPuUBNGnbCECJGN~v?S*aGD|)^y2Jd6WB0DMTw%1BkxOCm&5+cg8@2I6pMZROZ^L+bwDzHAnlQ2@f<$FwKDVi2LQ4?Xi zii@Hw<1KTIdY)r>hQ`BcNIUF3tno4<);aTI5l`?hvvco{G8$`^C$#YYNrCYsilH&}K(+RpxQcw7;B3@FSs+A{oK60$X9f zz3`!$s1ldF6GqF{#KFSrl28zT^{9#kF>YFoXYNlG?GoWCmM3tbw!-TvJM{%u8+6Y; zU0NcY{G5`vI6QS$idDbCae&2O@P)@Ne}Pj6-xq#Dd}$MpGE-NTT~u~`;4nuXyw*e! z1E!8f+4m`$=lbX=maQTfef69LO#;QJcg3ek1~yHfWuJYm)%tyJoFQ%5SNdJkFS9dr zPIz{$;S*pV?F{J&?>)W^`{E}K{eQ^RQ_?@@dxTUv%D%T?Sj;dnbL@CW4Fb&pvnUW( z9m<|vok*)F#H=WJHpNO0W)FN?b?~mLsSY!*n3&zC8gVD&=t-9OsR=O%)pS#6BIs=Q z9;IvK(h=v=+>+M+>lGR4=1BL*6@J9?3DHD9$9I_7(fv=0Uq;(C1ZsUzMaizw7_TMJ*CK8@7}9nVc?t3(*q%G)Jwj23+&q!nSo k_m+2!9ic5u<-PW#m!!Q7bB^#{yZ2v8)E^He38QWM2Uq`2j{pDw literal 0 HcmV?d00001 diff --git a/test/webapp/jsp/encoding/bom-utf16le-prolog-utf8.jspx b/test/webapp/jsp/encoding/bom-utf16le-prolog-utf8.jspx new file mode 100644 index 0000000000000000000000000000000000000000..c6a8112142063a1a65ba0ee9fb35a904f9cf57bd GIT binary patch literal 2004 zcmai#TTc^V5QgX4#Q(7N3KM8O+!#a%B1D8#LW_jU(v!t@sqL1+pI4uErXNcS#B93T zZ_e}1;rsh%%XaM6MmDs}Zmeq)o7%vxEw@irvA0&WH+(ZVxwWBp;O5qM?wK|1z&7lI zOYGRTy|fp$Wv`%Z`2P&eV>H^XKjqzlQea2#pPVz>wXwC3h*JYz58fQw#Q6tMX87zt zDHgG1wuW8<@2{~CT*aOto4eMvpGKTzMR6$>WsqZKf}KnB6p6HkOEG46>bqVSWR!UU zDpj}8;H;=q^mY3V3Wv65`%rt>89HA$O_w}H?sMqUO%dt8Br`HrEWg}JMAaf^*-2Tq zyh_5q=dzCc<_V7e!gdTjm<| zyuk7diHFsY^y3;Ycf>k6KNj&s=VkWJ`z}Sb=3Zr|veHcz>i-e?-NLsJx#OI3R<1L0 zl-^TOcjKYTz}3qJC&W5nq*gTr<&^^N9XPD5OZP>9Yb`VZXKTp_-@?J>ChU zWozoe!t0VyQ2**t6$@hAv>MOcpDNPj6jiZ2p%-c^eLaP8yy(?B)3eW%mPjW*XXLFO zo_bfRtA2yy0E^MVmma(P1x{^zU%H0+rA0i-OjA`(Q91R2!yJC}wRVX*VCra;bDyGl zZjPSnvQ-3QuAZ}?O`sU{uJ|^|z^385?6aSBTECx-bGS|W%DijuWp<9tDbKFeeFN+x zoxwfjy~nq1UtHtR{|8SqCHV`!M{p&h?7NE&iyfxU96R1ogFth@E(*j|hjM0DH_|E! zu`9|xn_{I)XAgW^weha1sSY!*m^!;pHR3kNkrOX-tqCy*)pSQ_BS^P%kJ1pnWYqI% zZ%OL^^@@yibEJFZ3P0-e3DLwn$4!+(-?ZTBmRg>OD?H!ut0E<@Rp0s}djA_0I^lZ) zw+{vt;(C1NX(Hakizqa4TMJ*CzKt~pZO=`*RU!;) +<%@ page contentType="text/plain"%>OK \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bom-utf8-prolog-none.jspx b/test/webapp/jsp/encoding/bom-utf8-prolog-none.jspx new file mode 100644 index 0000000..ede1fbe --- /dev/null +++ b/test/webapp/jsp/encoding/bom-utf8-prolog-none.jspx @@ -0,0 +1,21 @@ + + + + + OK + \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bom-utf8-prolog-utf16be.jspx b/test/webapp/jsp/encoding/bom-utf8-prolog-utf16be.jspx new file mode 100644 index 0000000..3e99157 --- /dev/null +++ b/test/webapp/jsp/encoding/bom-utf8-prolog-utf16be.jspx @@ -0,0 +1,21 @@ + + + + + OK + \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bom-utf8-prolog-utf16le.jspx b/test/webapp/jsp/encoding/bom-utf8-prolog-utf16le.jspx new file mode 100644 index 0000000..e24a14f --- /dev/null +++ b/test/webapp/jsp/encoding/bom-utf8-prolog-utf16le.jspx @@ -0,0 +1,21 @@ + + + + + OK + \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bom-utf8-prolog-utf8.jspx b/test/webapp/jsp/encoding/bom-utf8-prolog-utf8.jspx new file mode 100644 index 0000000..208eedd --- /dev/null +++ b/test/webapp/jsp/encoding/bom-utf8-prolog-utf8.jspx @@ -0,0 +1,21 @@ + + + + + OK + \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bug60769a.jspx b/test/webapp/jsp/encoding/bug60769a.jspx new file mode 100644 index 0000000..6cea958 --- /dev/null +++ b/test/webapp/jsp/encoding/bug60769a.jspx @@ -0,0 +1,21 @@ + + + + + OK + \ No newline at end of file diff --git a/test/webapp/jsp/encoding/bug60769b.jspx b/test/webapp/jsp/encoding/bug60769b.jspx new file mode 100644 index 0000000..6cea958 --- /dev/null +++ b/test/webapp/jsp/encoding/bug60769b.jspx @@ -0,0 +1,21 @@ + + + + + OK + \ No newline at end of file diff --git a/test/webapp/jsp/error.jsp b/test/webapp/jsp/error.jsp new file mode 100644 index 0000000..f613acc --- /dev/null +++ b/test/webapp/jsp/error.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> +<%@page session="false" isErrorPage="true" %> + + +

    ERROR

    + + \ No newline at end of file diff --git a/test/webapp/jsp/errorOnELNotFound/default.jsp b/test/webapp/jsp/errorOnELNotFound/default.jsp new file mode 100644 index 0000000..a3e66c9 --- /dev/null +++ b/test/webapp/jsp/errorOnELNotFound/default.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-O${unknown}K

    + + \ No newline at end of file diff --git a/test/webapp/jsp/errorOnELNotFound/page-directive-false.jsp b/test/webapp/jsp/errorOnELNotFound/page-directive-false.jsp new file mode 100644 index 0000000..26ba5de --- /dev/null +++ b/test/webapp/jsp/errorOnELNotFound/page-directive-false.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> +<%@ page errorOnELNotFound="false" %> + + +

    00-O${unknown}K

    + + \ No newline at end of file diff --git a/test/webapp/jsp/errorOnELNotFound/page-directive-true.jsp b/test/webapp/jsp/errorOnELNotFound/page-directive-true.jsp new file mode 100644 index 0000000..fe241b0 --- /dev/null +++ b/test/webapp/jsp/errorOnELNotFound/page-directive-true.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> +<%@ page errorOnELNotFound="true" %> + + +

    00-O${unknown}K

    + + \ No newline at end of file diff --git a/test/webapp/jsp/errorOnELNotFound/tag-file-false.jsp b/test/webapp/jsp/errorOnELNotFound/tag-file-false.jsp new file mode 100644 index 0000000..df20a0b --- /dev/null +++ b/test/webapp/jsp/errorOnELNotFound/tag-file-false.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ page errorOnELNotFound="true" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/errorOnELNotFound/tag-file-true.jsp b/test/webapp/jsp/errorOnELNotFound/tag-file-true.jsp new file mode 100644 index 0000000..84c9021 --- /dev/null +++ b/test/webapp/jsp/errorOnELNotFound/tag-file-true.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ page errorOnELNotFound="false" %> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/errorOnELNotFound/web-xml-false.jsp b/test/webapp/jsp/errorOnELNotFound/web-xml-false.jsp new file mode 100644 index 0000000..a3e66c9 --- /dev/null +++ b/test/webapp/jsp/errorOnELNotFound/web-xml-false.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-O${unknown}K

    + + \ No newline at end of file diff --git a/test/webapp/jsp/errorOnELNotFound/web-xml-true.jsp b/test/webapp/jsp/errorOnELNotFound/web-xml-true.jsp new file mode 100644 index 0000000..a3e66c9 --- /dev/null +++ b/test/webapp/jsp/errorOnELNotFound/web-xml-true.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    00-O${unknown}K

    + + \ No newline at end of file diff --git a/test/webapp/jsp/forward.jsp b/test/webapp/jsp/forward.jsp new file mode 100644 index 0000000..eb87acd --- /dev/null +++ b/test/webapp/jsp/forward.jsp @@ -0,0 +1,17 @@ +<%-- + 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. +--%> + diff --git a/test/webapp/jsp/generator/attribute-01.jsp b/test/webapp/jsp/generator/attribute-01.jsp new file mode 100644 index 0000000..ba356fb --- /dev/null +++ b/test/webapp/jsp/generator/attribute-01.jsp @@ -0,0 +1,27 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> +<% + String variable03 = null; +%> + + foo02 + foo03 + + + foo02 + \ No newline at end of file diff --git a/test/webapp/jsp/generator/attribute-02.jsp b/test/webapp/jsp/generator/attribute-02.jsp new file mode 100644 index 0000000..2e6f104 --- /dev/null +++ b/test/webapp/jsp/generator/attribute-02.jsp @@ -0,0 +1,36 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> +<%@ page import="org.apache.el.TesterBeanA" %> +<%@ page import="org.apache.el.TesterBeanB" %> + + + + + + +<% +TesterBeanA beanA = new TesterBeanA(); +TesterBeanB beanB = new TesterBeanB(); +beanB.setName("Tomcat"); +beanA.setBean(beanB); +pageContext.setAttribute("testBeanA", beanA, PageContext.REQUEST_SCOPE); +pageContext.setAttribute("testBeanB", beanB, PageContext.REQUEST_SCOPE); +%> + + + diff --git a/test/webapp/jsp/generator/attribute-03.jsp b/test/webapp/jsp/generator/attribute-03.jsp new file mode 100644 index 0000000..e1043a8 --- /dev/null +++ b/test/webapp/jsp/generator/attribute-03.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> +<% + String variable03 = null; +%> + + ${'foo02'} + foo${'03'} + \ No newline at end of file diff --git a/test/webapp/jsp/generator/attribute-04.jsp b/test/webapp/jsp/generator/attribute-04.jsp new file mode 100644 index 0000000..fc42a11 --- /dev/null +++ b/test/webapp/jsp/generator/attribute-04.jsp @@ -0,0 +1,62 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> +<% + String variable03 = null; +%> + + aaa + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/beaninfo-01.jsp b/test/webapp/jsp/generator/beaninfo-01.jsp new file mode 100644 index 0000000..1c953ad --- /dev/null +++ b/test/webapp/jsp/generator/beaninfo-01.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + \ No newline at end of file diff --git a/test/webapp/jsp/generator/break-el-interpreter.jsp b/test/webapp/jsp/generator/break-el-interpreter.jsp new file mode 100644 index 0000000..9d96495 --- /dev/null +++ b/test/webapp/jsp/generator/break-el-interpreter.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<% +application.setAttribute(org.apache.jasper.compiler.ELInterpreter.class.getName(), "does.not.exist"); +%> \ No newline at end of file diff --git a/test/webapp/jsp/generator/break-string-interpreter.jsp b/test/webapp/jsp/generator/break-string-interpreter.jsp new file mode 100644 index 0000000..4b84fcc --- /dev/null +++ b/test/webapp/jsp/generator/break-string-interpreter.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<% +application.setAttribute(org.apache.jasper.compiler.StringInterpreter.class.getName(), "does.not.exist"); +%> \ No newline at end of file diff --git a/test/webapp/jsp/generator/circular-01.jsp b/test/webapp/jsp/generator/circular-01.jsp new file mode 100644 index 0000000..6252ecb --- /dev/null +++ b/test/webapp/jsp/generator/circular-01.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + \ No newline at end of file diff --git a/test/webapp/jsp/generator/customtag-02.jsp b/test/webapp/jsp/generator/customtag-02.jsp new file mode 100644 index 0000000..57cd254 --- /dev/null +++ b/test/webapp/jsp/generator/customtag-02.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/customtag-03.jsp b/test/webapp/jsp/generator/customtag-03.jsp new file mode 100644 index 0000000..a0901a2 --- /dev/null +++ b/test/webapp/jsp/generator/customtag-03.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/customtag-04.jsp b/test/webapp/jsp/generator/customtag-04.jsp new file mode 100644 index 0000000..c779084 --- /dev/null +++ b/test/webapp/jsp/generator/customtag-04.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/deferred-method-01.jsp b/test/webapp/jsp/generator/deferred-method-01.jsp new file mode 100644 index 0000000..3da8727 --- /dev/null +++ b/test/webapp/jsp/generator/deferred-method-01.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ page import="org.apache.el.TesterBeanB" %> +<% +TesterBeanB beanB = new TesterBeanB(); +beanB.setName("Tomcat"); +pageContext.setAttribute("testBeanB", beanB, PageContext.REQUEST_SCOPE); +%> + diff --git a/test/webapp/jsp/generator/deferred-method-02.jsp b/test/webapp/jsp/generator/deferred-method-02.jsp new file mode 100644 index 0000000..3da8727 --- /dev/null +++ b/test/webapp/jsp/generator/deferred-method-02.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> +<%@ page import="org.apache.el.TesterBeanB" %> +<% +TesterBeanB beanB = new TesterBeanB(); +beanB.setName("Tomcat"); +pageContext.setAttribute("testBeanB", beanB, PageContext.REQUEST_SCOPE); +%> + diff --git a/test/webapp/jsp/generator/dobody-01.jsp b/test/webapp/jsp/generator/dobody-01.jsp new file mode 100644 index 0000000..0818fcc --- /dev/null +++ b/test/webapp/jsp/generator/dobody-01.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + +

    Hello world

    +
    \ No newline at end of file diff --git a/test/webapp/jsp/generator/element-01.jsp b/test/webapp/jsp/generator/element-01.jsp new file mode 100644 index 0000000..37dd390 --- /dev/null +++ b/test/webapp/jsp/generator/element-01.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> + + color:red + 00-Red + + + color:red + 00-Red + +color:red diff --git a/test/webapp/jsp/generator/forward-01.jsp b/test/webapp/jsp/generator/forward-01.jsp new file mode 100644 index 0000000..3c4173e --- /dev/null +++ b/test/webapp/jsp/generator/forward-01.jsp @@ -0,0 +1,21 @@ +<%-- + 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. + --%> + + + + + diff --git a/test/webapp/jsp/generator/forward-02.jsp b/test/webapp/jsp/generator/forward-02.jsp new file mode 100644 index 0000000..29aa7c1 --- /dev/null +++ b/test/webapp/jsp/generator/forward-02.jsp @@ -0,0 +1,22 @@ +<%-- + 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. + --%> + + x-powered-by.jsp + + + + diff --git a/test/webapp/jsp/generator/forward-03.jsp b/test/webapp/jsp/generator/forward-03.jsp new file mode 100644 index 0000000..d289c48 --- /dev/null +++ b/test/webapp/jsp/generator/forward-03.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + diff --git a/test/webapp/jsp/generator/forward-04.jsp b/test/webapp/jsp/generator/forward-04.jsp new file mode 100644 index 0000000..956694a --- /dev/null +++ b/test/webapp/jsp/generator/forward-04.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + diff --git a/test/webapp/jsp/generator/include-01.jsp b/test/webapp/jsp/generator/include-01.jsp new file mode 100644 index 0000000..a3ba002 --- /dev/null +++ b/test/webapp/jsp/generator/include-01.jsp @@ -0,0 +1,24 @@ +<%-- + 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. + --%> + + + x-powered-by.jsp + + + x-powered-by.jsp + + diff --git a/test/webapp/jsp/generator/info-conflict-none.jsp b/test/webapp/jsp/generator/info-conflict-none.jsp new file mode 100644 index 0000000..690db8f --- /dev/null +++ b/test/webapp/jsp/generator/info-conflict-none.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ page info="abc" %> +<%@ include file="info.jsp" %> \ No newline at end of file diff --git a/test/webapp/jsp/generator/info-conflict.jsp b/test/webapp/jsp/generator/info-conflict.jsp new file mode 100644 index 0000000..c6e73e7 --- /dev/null +++ b/test/webapp/jsp/generator/info-conflict.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ page info="123" %> +<%@ include file="info.jsp" %> \ No newline at end of file diff --git a/test/webapp/jsp/generator/info.jsp b/test/webapp/jsp/generator/info.jsp new file mode 100644 index 0000000..142ee6b --- /dev/null +++ b/test/webapp/jsp/generator/info.jsp @@ -0,0 +1,17 @@ +<%-- + 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. +--%> +<%@ page info="abc" %> \ No newline at end of file diff --git a/test/webapp/jsp/generator/invoke-01.jsp b/test/webapp/jsp/generator/invoke-01.jsp new file mode 100644 index 0000000..ac4e1d1 --- /dev/null +++ b/test/webapp/jsp/generator/invoke-01.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + +

    Hello world

    +
    \ No newline at end of file diff --git a/test/webapp/jsp/generator/jsp-id.jsp b/test/webapp/jsp/generator/jsp-id.jsp new file mode 100644 index 0000000..c148e99 --- /dev/null +++ b/test/webapp/jsp/generator/jsp-id.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/jsp-id.jspx b/test/webapp/jsp/generator/jsp-id.jspx new file mode 100644 index 0000000..e677085 --- /dev/null +++ b/test/webapp/jsp/generator/jsp-id.jspx @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/plugin-01.jspx b/test/webapp/jsp/generator/plugin-01.jspx new file mode 100644 index 0000000..ab8af3e --- /dev/null +++ b/test/webapp/jsp/generator/plugin-01.jspx @@ -0,0 +1,54 @@ + + + + + + 10 + 10 + + + + + + + + + +

    + + + + +

    + + + + + bar + + + + + + +

    + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/scriptingvariables-01.jsp b/test/webapp/jsp/generator/scriptingvariables-01.jsp new file mode 100644 index 0000000..792236a --- /dev/null +++ b/test/webapp/jsp/generator/scriptingvariables-01.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> +<% + String variable03a = null; + String variable03b = null; +%> + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/scriptingvariables-02.jsp b/test/webapp/jsp/generator/scriptingvariables-02.jsp new file mode 100644 index 0000000..909cdb4 --- /dev/null +++ b/test/webapp/jsp/generator/scriptingvariables-02.jsp @@ -0,0 +1,24 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> +<% + String variable03 = null; +%> + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/setproperty-01.jsp b/test/webapp/jsp/generator/setproperty-01.jsp new file mode 100644 index 0000000..222552e --- /dev/null +++ b/test/webapp/jsp/generator/setproperty-01.jsp @@ -0,0 +1,32 @@ +<%-- + 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. +--%> + + + jsp:setProeprty test cases + + + + + + + + 1 + + + + + diff --git a/test/webapp/jsp/generator/setters-01.jsp b/test/webapp/jsp/generator/setters-01.jsp new file mode 100644 index 0000000..53bc50d --- /dev/null +++ b/test/webapp/jsp/generator/setters-01.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + diff --git a/test/webapp/jsp/generator/single-threaded.jsp b/test/webapp/jsp/generator/single-threaded.jsp new file mode 100644 index 0000000..371ec50 --- /dev/null +++ b/test/webapp/jsp/generator/single-threaded.jsp @@ -0,0 +1,17 @@ +<%-- + 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. +--%> +<%@ page isThreadSafe="false" %> \ No newline at end of file diff --git a/test/webapp/jsp/generator/templatetext-01.jsp b/test/webapp/jsp/generator/templatetext-01.jsp new file mode 100644 index 0000000..66e486f --- /dev/null +++ b/test/webapp/jsp/generator/templatetext-01.jsp @@ -0,0 +1,283 @@ +<%-- + 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. +--%> + +

    Hello${' '} +

    +World!
    +

    +

    + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + 123456789 123456789 123456789 123456789 123456789 123456789 123456789 +

    +

    ${1+1}

    +

    ${1+1}

    +

    ${1+1}

    + \ No newline at end of file diff --git a/test/webapp/jsp/generator/templatetext-02.jsp b/test/webapp/jsp/generator/templatetext-02.jsp new file mode 100644 index 0000000..6545a4e --- /dev/null +++ b/test/webapp/jsp/generator/templatetext-02.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> + +

    123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789

    + \ No newline at end of file diff --git a/test/webapp/jsp/generator/try-catch-finally-01.jsp b/test/webapp/jsp/generator/try-catch-finally-01.jsp new file mode 100644 index 0000000..d715c34 --- /dev/null +++ b/test/webapp/jsp/generator/try-catch-finally-01.jsp @@ -0,0 +1,22 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + +

    OK

    +
    +
    diff --git a/test/webapp/jsp/generator/try-catch-finally-02.jsp b/test/webapp/jsp/generator/try-catch-finally-02.jsp new file mode 100644 index 0000000..f2bd7d9 --- /dev/null +++ b/test/webapp/jsp/generator/try-catch-finally-02.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + diff --git a/test/webapp/jsp/generator/usebean-01.jsp b/test/webapp/jsp/generator/usebean-01.jsp new file mode 100644 index 0000000..b96c2d3 --- /dev/null +++ b/test/webapp/jsp/generator/usebean-01.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> + + + + diff --git a/test/webapp/jsp/generator/usebean-02.jsp b/test/webapp/jsp/generator/usebean-02.jsp new file mode 100644 index 0000000..bb9760e --- /dev/null +++ b/test/webapp/jsp/generator/usebean-02.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> + + + + diff --git a/test/webapp/jsp/generator/usebean-03.jsp b/test/webapp/jsp/generator/usebean-03.jsp new file mode 100644 index 0000000..be51fd3 --- /dev/null +++ b/test/webapp/jsp/generator/usebean-03.jsp @@ -0,0 +1,38 @@ +<%-- + 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. +--%> + + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/usebean-04.jsp b/test/webapp/jsp/generator/usebean-04.jsp new file mode 100644 index 0000000..2a3b086 --- /dev/null +++ b/test/webapp/jsp/generator/usebean-04.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> + + + + diff --git a/test/webapp/jsp/generator/usebean-05.jsp b/test/webapp/jsp/generator/usebean-05.jsp new file mode 100644 index 0000000..127c735 --- /dev/null +++ b/test/webapp/jsp/generator/usebean-05.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> + + + + diff --git a/test/webapp/jsp/generator/usebean-06.jsp b/test/webapp/jsp/generator/usebean-06.jsp new file mode 100644 index 0000000..53b39b7 --- /dev/null +++ b/test/webapp/jsp/generator/usebean-06.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> + + + + diff --git a/test/webapp/jsp/generator/usebean-07.jsp b/test/webapp/jsp/generator/usebean-07.jsp new file mode 100644 index 0000000..b96c2d3 --- /dev/null +++ b/test/webapp/jsp/generator/usebean-07.jsp @@ -0,0 +1,20 @@ +<%-- + 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. +--%> + + + + diff --git a/test/webapp/jsp/generator/usebean-08.jsp b/test/webapp/jsp/generator/usebean-08.jsp new file mode 100644 index 0000000..1344ba7 --- /dev/null +++ b/test/webapp/jsp/generator/usebean-08.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> + + + bean2 + + + + diff --git a/test/webapp/jsp/generator/variable-from-attr-nested.jsp b/test/webapp/jsp/generator/variable-from-attr-nested.jsp new file mode 100644 index 0000000..72f2828 --- /dev/null +++ b/test/webapp/jsp/generator/variable-from-attr-nested.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + + + diff --git a/test/webapp/jsp/generator/variable-tagfile-from-attr-nested.jsp b/test/webapp/jsp/generator/variable-tagfile-from-attr-nested.jsp new file mode 100644 index 0000000..adc476a --- /dev/null +++ b/test/webapp/jsp/generator/variable-tagfile-from-attr-nested.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/variable-tagfile-nested.jsp b/test/webapp/jsp/generator/variable-tagfile-nested.jsp new file mode 100644 index 0000000..7266190 --- /dev/null +++ b/test/webapp/jsp/generator/variable-tagfile-nested.jsp @@ -0,0 +1,28 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + + + + +

    Text

    + + +
    +
    \ No newline at end of file diff --git a/test/webapp/jsp/generator/variable-tei-nested.jsp b/test/webapp/jsp/generator/variable-tei-nested.jsp new file mode 100644 index 0000000..6043a54 --- /dev/null +++ b/test/webapp/jsp/generator/variable-tei-nested.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/bugs" prefix="bugs" %> + + + + + + + + diff --git a/test/webapp/jsp/generator/x-powered-by.jsp b/test/webapp/jsp/generator/x-powered-by.jsp new file mode 100644 index 0000000..f08c640 --- /dev/null +++ b/test/webapp/jsp/generator/x-powered-by.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +

    This JSP is configured, via a Servlet element in web.xml, to enable the + X-Powered-By header

    \ No newline at end of file diff --git a/test/webapp/jsp/generator/xml-doctype-01.jspx b/test/webapp/jsp/generator/xml-doctype-01.jspx new file mode 100644 index 0000000..9fb6f17 --- /dev/null +++ b/test/webapp/jsp/generator/xml-doctype-01.jspx @@ -0,0 +1,23 @@ + + + + + diff --git a/test/webapp/jsp/generator/xml-doctype-02.jspx b/test/webapp/jsp/generator/xml-doctype-02.jspx new file mode 100644 index 0000000..eabebf9 --- /dev/null +++ b/test/webapp/jsp/generator/xml-doctype-02.jspx @@ -0,0 +1,24 @@ + + + + + diff --git a/test/webapp/jsp/generator/xml-prolog-01.jspx b/test/webapp/jsp/generator/xml-prolog-01.jspx new file mode 100644 index 0000000..38494df --- /dev/null +++ b/test/webapp/jsp/generator/xml-prolog-01.jspx @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/xml-prolog-02.jspx b/test/webapp/jsp/generator/xml-prolog-02.jspx new file mode 100644 index 0000000..a170887 --- /dev/null +++ b/test/webapp/jsp/generator/xml-prolog-02.jspx @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/generator/xml-prolog-tag.jspx b/test/webapp/jsp/generator/xml-prolog-tag.jspx new file mode 100644 index 0000000..3febd85 --- /dev/null +++ b/test/webapp/jsp/generator/xml-prolog-tag.jspx @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/test/webapp/jsp/includeThenForward.jsp b/test/webapp/jsp/includeThenForward.jsp new file mode 100644 index 0000000..9c17bcc --- /dev/null +++ b/test/webapp/jsp/includeThenForward.jsp @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +

    FAIL

    + +

    FAIL

    \ No newline at end of file diff --git a/test/webapp/jsp/ok.html b/test/webapp/jsp/ok.html new file mode 100644 index 0000000..35b67d7 --- /dev/null +++ b/test/webapp/jsp/ok.html @@ -0,0 +1,5 @@ + + +

    OK

    + + \ No newline at end of file diff --git a/test/webapp/jsp/pageContext1.jsp b/test/webapp/jsp/pageContext1.jsp new file mode 100644 index 0000000..1f44f97 --- /dev/null +++ b/test/webapp/jsp/pageContext1.jsp @@ -0,0 +1,35 @@ +<%-- + 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. +--%> +<%@ page import="java.io.IOException" contentType="text/plain"%> +<% + boolean flush = Boolean.parseBoolean(request.getParameter("flush")); + try { + if (flush) { + out.println("Flush"); + pageContext.include("/jsp/pageContext2.jsp", true); + } else { + pageContext.include("/jsp/pageContext2.jsp"); + } + } catch (IOException e) { + out.println("OK"); + return; + } catch (Throwable t) { + out.println("FAILED. Expected IOException, received: " + t.getClass().getName()); + return; + } + out.println("FAILED. Expected IOException."); +%> \ No newline at end of file diff --git a/test/webapp/jsp/pageContext2.jsp b/test/webapp/jsp/pageContext2.jsp new file mode 100644 index 0000000..ddef591 --- /dev/null +++ b/test/webapp/jsp/pageContext2.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ page import="java.io.IOException" contentType="text/plain"%> +<% throw new IOException("Throws IOException."); %> \ No newline at end of file diff --git a/test/webapp/jsp/session.jsp b/test/webapp/jsp/session.jsp new file mode 100644 index 0000000..87ec0c3 --- /dev/null +++ b/test/webapp/jsp/session.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ page session="true" %> +

    OK

    \ No newline at end of file diff --git a/test/webapp/jsp/tagFileInJar.jsp b/test/webapp/jsp/tagFileInJar.jsp new file mode 100644 index 0000000..1c38c02 --- /dev/null +++ b/test/webapp/jsp/tagFileInJar.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ taglib uri="http://tomcat.apache.org/test-lib" prefix="testlib" %> + \ No newline at end of file diff --git a/test/webapp/jsp/test.jsp b/test/webapp/jsp/test.jsp new file mode 100644 index 0000000..d558ef5 --- /dev/null +++ b/test/webapp/jsp/test.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ page contentType="text/plain" %> +<%@ taglib uri="../WEB-INF/test.tld" prefix="test" %> diff --git a/test/webapp/jsp/trim-spaces-extended.jsp b/test/webapp/jsp/trim-spaces-extended.jsp new file mode 100644 index 0000000..817f44f --- /dev/null +++ b/test/webapp/jsp/trim-spaces-extended.jsp @@ -0,0 +1,49 @@ +<%-- + 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. +--%> + + + + Apache Tomcat JSP White Space Trim Optimisation Configuration + + + + + +

    Apache Tomcat JSP white space trim test page

    +

    If the value of init param jspWhiteSpaceTrimming is set to true, the compiler +trims all the excess white spaces and empty line characters from the JSP files +and thus reduces build size.

    + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/webapp/script-expr.jsp b/test/webapp/script-expr.jsp new file mode 100644 index 0000000..1f43225 --- /dev/null +++ b/test/webapp/script-expr.jsp @@ -0,0 +1,34 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + Scripting expression test cases + +

    <%= "00-hello world" %>

    +

    <%= "01-hello \"world" %>

    +

    <%= "02-hello \\\"world" %>

    +

    <%= "03-hello ${world" %>

    +

    <%= "04-hello \\${world" %>

    + + + + + + + + + \ No newline at end of file diff --git a/test/webapp/valid.jspx b/test/webapp/valid.jspx new file mode 100644 index 0000000..ff56403 --- /dev/null +++ b/test/webapp/valid.jspx @@ -0,0 +1,22 @@ + + + Hello World + \ No newline at end of file diff --git a/test/webapp/valid.xsd b/test/webapp/valid.xsd new file mode 100644 index 0000000..ddcc232 --- /dev/null +++ b/test/webapp/valid.xsd @@ -0,0 +1,20 @@ + + + + diff --git a/test/webapp/welcome-files/index.jsp b/test/webapp/welcome-files/index.jsp new file mode 100644 index 0000000..92c9eab --- /dev/null +++ b/test/webapp/welcome-files/index.jsp @@ -0,0 +1,21 @@ +<%-- + 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. +--%> + + +

    OK-JSP

    + + \ No newline at end of file diff --git a/test/webapp/welcome-files/sub/.gitignore b/test/webapp/welcome-files/sub/.gitignore new file mode 100644 index 0000000..0073da0 --- /dev/null +++ b/test/webapp/welcome-files/sub/.gitignore @@ -0,0 +1,24 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +# Git ignores empty directories and the unit tests require this directory to +# be present for the welcome file tests to pass. The presence of this file +# doesn't break the unit tests. +# +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/test/webresources/dir1-internal.jar b/test/webresources/dir1-internal.jar new file mode 100644 index 0000000000000000000000000000000000000000..bd1228832faabb45b301cc03fb2f7edb2d4df59b GIT binary patch literal 960 zcmWIWW@Zs#0D;}1dX8WQlwbwYDVarv`T=ky?(20?lyJb5q(D@0fwiKm5=2s!VyK&D zs8>=^f-tn92$!KoV5=d9qN_p}YNVfHgk&g0c`BNr+%PvFY=0`egyZ9Dh!FVi5CtiQx@m@bB^4zgl@OyEim(`E zgk%)D08WU^<#DMp%@bb!#xm(0RSf%iJ1TZ literal 0 HcmV?d00001 diff --git a/test/webresources/dir1/META-INF/MANIFEST.MF b/test/webresources/dir1/META-INF/MANIFEST.MF new file mode 100644 index 0000000..59499bc --- /dev/null +++ b/test/webresources/dir1/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/test/webresources/dir1/d1/d1-f1.txt b/test/webresources/dir1/d1/d1-f1.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/webresources/dir1/d2/d2-f1.txt b/test/webresources/dir1/d2/d2-f1.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/webresources/dir1/f1.txt b/test/webresources/dir1/f1.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/webresources/dir1/f2.txt b/test/webresources/dir1/f2.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/webresources/dir2/d1/.ignore-me.txt b/test/webresources/dir2/d1/.ignore-me.txt new file mode 100644 index 0000000..69fb24b --- /dev/null +++ b/test/webresources/dir2/d1/.ignore-me.txt @@ -0,0 +1,21 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + +This file is added here so that the directory is not empty and does not +disappear in Git. +Whether it is visible in AbstractTestResourceSet tests, depends on +configuration of mappings in specific test. Thus it is optional. diff --git a/test/webresources/dir2/d2/.ignore-me.txt b/test/webresources/dir2/d2/.ignore-me.txt new file mode 100644 index 0000000..69fb24b --- /dev/null +++ b/test/webresources/dir2/d2/.ignore-me.txt @@ -0,0 +1,21 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + +This file is added here so that the directory is not empty and does not +disappear in Git. +Whether it is visible in AbstractTestResourceSet tests, depends on +configuration of mappings in specific test. Thus it is optional. diff --git a/test/webresources/dir3/.ignore-me.txt b/test/webresources/dir3/.ignore-me.txt new file mode 100644 index 0000000..69fb24b --- /dev/null +++ b/test/webresources/dir3/.ignore-me.txt @@ -0,0 +1,21 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- + +This file is added here so that the directory is not empty and does not +disappear in Git. +Whether it is visible in AbstractTestResourceSet tests, depends on +configuration of mappings in specific test. Thus it is optional. diff --git a/test/webresources/non-static-resources.jar b/test/webresources/non-static-resources.jar new file mode 100644 index 0000000000000000000000000000000000000000..751ebe31960222c16a574ef305160fac66e535c8 GIT binary patch literal 1681 zcmWIWW@Zs#;Nak3SUA(njR6U8GO#fCx`sIFdiuHP|2xINz|0Wf&CUT*!30$nfK#&w zPz7AGucM!*n`>~0p0C?y-!rFuymj?1@_OrPojY@WbCAIm;|EWR^t^m^Jbf>gu43Vg zcqRE_nVMF{4{=qs=S#FKo{Nh<5m%kYFg5#A(Z?c2up_<&v@|*ct#AQikR!Ml7=RAT zMe~6$vLlL8i}Op1l2eQI5gPK*G)QCD5bo*(4g`e80yK@v_%$ZyBo-H=*j$8WfI4vo zFP;(3{ zJAiTwDu8DJQ2s&Jikyf+`2_)NflRnoq}&5Cfr|lmVn>*82-yVWq>nHFTjEC;@EN-S zh^&Ax2wP@A7^K699E^!7zvI@dbY?%dN=qBO~g=Zav+1N4< z!t8$}n+?xO2rJMNJSa0EKp-#)!V?VcKn8^g)cH^=kg^scU4!gGPV=D5g#c?QwhNlo W0=!w-K*oYnv^O&Yg8>jTFaQ7>dTagw literal 0 HcmV?d00001 diff --git a/test/webresources/war-url-connection.war b/test/webresources/war-url-connection.war new file mode 100644 index 0000000000000000000000000000000000000000..729bf3cfdf0354c34a5073a38d59e98e0d807026 GIT binary patch literal 868 zcmWIWW@h1HVBlb2_~T^h%zy+q7+4s>U7d72{oM5bonl}JfUBHXsf(hL2dE3CGAA=h zAEZ)*frEpCp&3o32%5^0)Z!Aoti&Sj4{2$Fi42Tyy<*eZR1R<+*|$!yZ`ZuKykCq3 zsS`6!wM?8Ux>W4>bTQTCVuwu&3{sd)Ev8u5o@e=F)fgeRyY0-LDOb4m_Ds4n=Z;93lvSrxGxecQaZP_Xdk>w?r(r{{}_Zmj$Fdt%Hye`-!u z*mXcpMR{`sV~3FFo+f6IOGd>(qDG%Z5@w0+k!k*t^M%_X>uchyF9(;gR2UoGG=9*< zBC6rd;B|QJ(W46(#1c{_Bxr25sM(u+*wRhnZ~3|B-U?DTGtP84n?=d)?w_c;Q+vm> z<MreYiV9O%8o4c}$@}D|f+p*qr&&^gB=QW2#^G{s+t0>Za z;@Zpkv$Q!k<*oP9WH*w1aP|~i&|#*Q&k~Xnvm_mDfHBU_p>$+L?=(gR26JZQ6o8&q z1d!r2IVZ8WIJFp<3K*HV7;vX8pcW(mOFxj5hR}&Ets%5&0hutJkd(*BB*K75d&sT_ qr9A`)0Wy(w!crnWSA%?s06I(v6A*r8Wdo^T0m3gpItr+UfdK$<-TylP literal 0 HcmV?d00001 diff --git a/webapps/ROOT/WEB-INF/web.xml b/webapps/ROOT/WEB-INF/web.xml new file mode 100644 index 0000000..e243601 --- /dev/null +++ b/webapps/ROOT/WEB-INF/web.xml @@ -0,0 +1,30 @@ + + + + + Welcome to Tomcat + + Welcome to Tomcat + + + diff --git a/webapps/ROOT/asf-logo-wide.svg b/webapps/ROOT/asf-logo-wide.svg new file mode 100644 index 0000000..5743c42 --- /dev/null +++ b/webapps/ROOT/asf-logo-wide.svg @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/ROOT/bg-button.png b/webapps/ROOT/bg-button.png new file mode 100644 index 0000000000000000000000000000000000000000..b544bbfcdf91f6044ece15ffd1ca57575cebb8ee GIT binary patch literal 713 zcmV;)0yh1LP)8;Kh4yc88a9&Xs#a;crB)WJpK+3`7r~*sH&=# z&*$&aK8-j=7tiyY`Fw7S$78+SZoi_VU!>oGNyLyjm=SK7ruj@!l*b5N{ID$Rd97A^ zO3N$KikT$Azp+vx4~N4?jYi`MLKRO6g~I(-t99A$_YIy!ydZ# zKj+LFfk9Hs4o}u4xk*YS)`#3C<%R9cOz?IUMCdll`;2SS4-)G;rmPL&=Ge^X?$72XxFIAi vwodbN5Ixv+cEwG?O!1Si*W5tD9{~mcOy(T`4wOWJ00000NkvXXu0mjf|HwjX literal 0 HcmV?d00001 diff --git a/webapps/ROOT/bg-middle.png b/webapps/ROOT/bg-middle.png new file mode 100644 index 0000000000000000000000000000000000000000..0c95a828d10281783d0cf82dba498f954320346e GIT binary patch literal 1918 zcmV-^2Z8vBP)4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY3ljhU3ljkVnw%H_00cHkL_t(o z3GJ5cvYRjrL<7V7KXD5^XIBR9+-ax(TPN7EwEDDgvR#bNUyS|px%m8kM8IMj{Q}K&-T7#dPN{+J zD6r;$PIyt;0ji0{HGWgZZCXG@<%~l>ilIf_vwDC@D1v&^a6GDT=}N!y&OzbZ7?JQ@ zOyk=Nh)4N3xbt(I1|eMLuG0w$+Tl(a$-FE?()|KuMM=NX8I8;R!;3(R!QKKhMt6p` z4Zt-~DO?eyx4VIdWlqh{@thYt3auajtuAv~bni%q-9?xL^ zDSys+ftqvY$blGCitpQ}=tRA>UXgYMI7P^%1I^bnIMbA9#*NBYNb_UyYL8WxPSS|r zUDtZ=3*?-SNfKxZp&B)C?VKEj@7y8hJo#lD95o{B{^k1gT0(Q{{%0cP2^X zXh0isRGNs1TIM5FupUI4q@jBS%n8D@>vuc-6am0fxJ>V<3TQT3Y`&aI%&{ij0VhbT zA?sNiQ{s=L**Oo-mp3;>Bqo7iS`t>{j*R&3E2Y{~B9yiS-@V{NE0!dWblep%Il%oKoZMl8?-;CM!@jf5T{M;- zEQQw`;A44n{~l|v@r!IJ%+x~^()Q+4Cn-}QYl|2RdBG+*B1;f9?FyF|dm}2e>T#K= zknTCc66t&&;nFWPd5w;bY_d4)Zg*HqZ^v2~BSXSsb@hUW zvME-fA^Fji!85O3!0`Mvke5ge83>HbC80h4Fj*J*^}ku(T||P#234gVWb}1&O9~uM z@;Tr~xTwk}zA@aJVj-||wLvR>CBt7X4THo?E-96u^IzBB!DZ22b1Xw~INGP)4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY3ljhU3ljkVnw%H_00J>dL_t(Y z4egX$lEWYj1dYr0KW_dE+dUFaYCpDefF&-_WAsF1yk4($e6g+r*cy#pBPJuqIu_!= zPwfQ@30O`8ztDnYVIa{MT3GGZEV;}z zPZ^|cIi*?9>^o|I@$g2)mohg5!vZ?c9KRtt(RJ#1l)rU{K3 zv;)sAt02ns&_c2T7!YX}iMD0R?FPRK5%QS|Q)2`Xy46BP!)Y!t?pe)9Q=XO~Yx+#1 z)9f2-gWOd;4lu7Qpy~>h;Z^T@g<{jxFQJn=QBtKyB4io9v|vFbk7+F0ISK2#u`z2zIK8M*ybd5VO|AL8gaZ z-Rn$Wqj>t>6&djy9h$7u6$TpZhzTJP2$$Gy)nR+wHZTBUZU)#wmBp=-m=dc?d6ioiS z6ry8(&Fv#`^Hk)GSAxks@4EQxze4V4gR0v@m8WNlQx=aJ>v@u+tMl|uqtGh3X`_s) zeX7p`R1zkHTNWd8oc6hH0_(KY*J${B0nT+-==dNox~##e)Y_G~B|VE_ z9yN2AGe1RC9fFW&s-Sz!^jVFT642X2dcj|4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY3ljhU3ljkVnw%H_00`GfL_t(& z1?`#ba_cq>L}QQN|BY|lJ!b)0&bZV5?KDg*lHg+TL5Px*+w=MT{QA0my;`Pcp7HaR z-{;r;1?cD*&@(^$`U&WkpT{oKmkaiM0(iCO*Q1Ls5Nu*@FmD^v%Oogk&jHo*!pAh4 z6vk|GrHkqE}GC#gg*i! zbpyCjIhutjfLzL#d45fs$r&@60tdls5@59TXB~$P58PyD+(s0v1PpZCh z0I11n0MUD|pIL_{c}#jTQJFppHA#@n{vg40ev`}77_Z_8Aq$FyE0}Q*d}xmEoIP0x z*c7VdvKN>tk6nu>T6nV`P>dIaLhr&&17=9on#I@rFr7ncR#u^WaN1yhuq(o}gfHqeyRT zVKf3{XH4h;vtfM7!uX^o@N+hzT?zl=X<=E z@&<4-C4h;#pL$N^8?MTaXH)JOAluAk&Dd;UT32$Z3#l61^4?IbkMW>#D{wT=j?Ci$ z?9FG#xYS@IKh*c*O4QLvqkC+<-U7HASVD*?T)3TTjgLF|gO^z#r~G*4e<$H845Pad z0q(=*qP}A|Eo*v|9ki9Dx%3HAu>=oG45|ea8)e4rz}WYOy|2-X{G3r#s>yH@9mlk( z1{y(}ndTD#1qs6Fe(D91Bp$0OWSq%t1r=VFz|si43Q|Qya~-(-JkW11Zqd<#Bf_J#vBq8`Z60^zgMc&mzaRedKPTkfpmaS5kzKnnDjweNzm<~uVS|f{? z0(oU26Eh@!)P~jcD90QmvL0?(SxVHX4AibSYGr=waGmFXRoG_%MeC^mNg8X-X-sc& zsgI0hT^ZoME}8JHaKwy-k?E5vee5!T2Kb700@jGtvo&rlVp_x~%=|q)=w6bbt)caH zLuoU|cA(r`|(vlxoe=&I=m-xim)vhigx`|h(bjk+cPWK^>o^YKKFGB zH?FR^9ASJjK5VhlEO6`z!sWC{i$mlz`;+kGG6HXc1tblg>ZBtre^to2!Z?Cgg`nt} zm<8{kuZQ=ZRw}s1ao;ncxwLsYuLSd9(n1CV#`v&%0zu2(#25f&M2%Nd;U1BDn@xI< zsTvS<6Fqh2oiXZ{rU#=Co+Nn|QGth_Qy^}#rfXsW)v2$Ji7w51MiZUu$f{P9{B#^W>SBv)8i&ko%Qm=jsI5=KTbVH<1h(4l7cZQpNhnWWV;c zZ^Zz_MPxYwgw;LOSuPWSD~dPyau%KZ_Pk~#30Y;5Lw>XF*&~060$du{3L@TD9y8%9 z-uJLwR+QyPiBYO8lrjcVKE$<@4|i?2JDw~$>E)?^H4?4*y4o8)<#{#{Msd|DroCE|L1U_)Mx2YZ@h3{}?%!~jT zLBTzv)(RsZHa*9UY3ev8!Dm)QZHz44MeQQ;}e$u3WQn9kEbW^f~kC=HX_ zWSKFoN?p&+#QId{6JYA|v~@b`d7Y^S_qj|nU+}WO5^jjsXac<`UF3#lx27b7Y-;=Wm|L{or4E!nmF#kU%{6ji^ tihk4cGb3;Ff1RHE?bN>jy=i}%`~}dpS12dp7GgxTSG#Vo@ z(O42=Ta6`lqX=qx-EY&s#3b&TWN?4G%lE(U%{<2eO*5PP@_8NJyXD+-&pY?LbI-l! zJc%r*Hw_#(kXRaLLDYb|p-9WDh>oL9DEA~9V@))2BoF_252AH_Sef-E+HOO%;vtiA zKcW%#M9$7U{IXs|=SH!z0lq(x(|D8eFrqhRfDSaX9wYH*L6*1@z06(t3y;lMp(etQ z)N1~QE3)+R%P-^38alZjb#}5G{_u}gv5O%MKmqB7@ou9+Nv~DgRCQG8wVE3Pv}&zZ zp}H}kL!nV<0VmhuAL>lIZRIqvAL)oQuBrjsw5;x~@VsQ4v+ck&6$ zNAkpc3CWPh*!1nU-_rH#*XiSrKc@HIdylSOyGE_8t#sy<%XH@4c{+Oh1RXqljCSrl zNKfy2k)D3~X=>W`Jk_@Bp^9e?lJ2QpRQ=?$RQANPw6bvv)irOXinNYqaGF5@*wko z1OHz*z}|gt^K|FKdwZLg2lqt{lsi$+{*Mp#_U`9MZ3Z)z!sWG~~}cSs4pSACKwNMm+N9ut8k82U%G5 zb{zW1=!b2&{=KX`AEJRnMh+eRhy~!G_o2JSIND7ZGm$SdV%&QZS-IUVvpOUWp3fS} zX3zUebS&$8r=E4-2JysNcpnm`=SL=fupbEwFdkmLc0Dw@(Cf=Dzr1zpmQeiUlTV=I zp=wLl*75M$cJE7DwH^Tf`Okl#){$=9xPdNLuDuHF4`mG9izVY<|N0jX4=}6(B{iG! z3rnD4TXycnf&rcGQ+Ev77)l#!3qPDdKUN|rW!J{@^VjX2({LJ!8#;SN{RL=yEDBho z%-}!x;DdMGc}G>dVcxp^QyVYg=T&_KO9ED$s04Zd0CxTT?|(-n@?a*`z4VG& ztfE-(uy0rL?s!1u`fJb1p>kG}qLduAlY z^2DFf?o)GW2o*^8+wSAB5*h41BlC=Q;EyibXjMj7X85U1%L0XYSAYyw)@v1H-3c2QgR*j`UXCspPSdI9%B+7U2Cz|RZkM$- zKxlKjtVYL9_3bi70U5W;tcb+$o2DNu8|V4=a%bzwNc?%?&w_ZnU2TbiF`3!z(Au2c z4mC)xzy3OBx1Tz7ijEySM*H{gr=2@@QcKIzw0ZMpTEBihRn^o``RZ!YRaR2Dwvx)q zwN$#Qf@J4DrNk?@C=GUce0v8aUjB~KE?lSdv!7GOsgEh?+*cHR`4+{teov9D9Tag% zPjP3jQ^tu8Dd)(WlzZqZ$zE)woaZl4?(Q>`b>goiJ^Thq_g*6DbEhchnIn|ba)`3G z9VY2h2PkX9PLeimr_9=|R8+p2N=jE!`no45y|RHaGOT zek}e*HC`d7=F2psu8s1PrIcNurc^~4rOHc5QoNqx3M(mg#Yze#TuAoZ0>aX7?Mk3O+p0rNw9^&rsK-y@#s!X8P==zg41 zv>KS=;qNbwWd*cbB)HjPlajB@VO=fz#zwgZ1O)qJbH+J)KC1;DVSSb>m5DZe2S}nk z`~!V`gS{5;M03yI+s8dCV&S3;4|j2JV1U1WfH)!xyA_tWcIuxc4hj-S1o{U1BL%pp zPD?OQ%D4B8n;jS+juNw0|A0BkqlY_8=O3DDx9mp`Mfiy4`TGa^2M3Cm*$p2weE3X` z0Ir=eu#ZA4_6T(Mb9YM}JL1uCV;zPpwZapyb_>qtu?$ss`ee`Z%3CpE_{3!A5#!UP z*b1@so;Yq}sduJwQHVJ8(P5KPos(T0MlFHa#M*lpZ6=F8F>}2ZI1G1m96Elo!-yFe zmK%TI?R`wo=vj{poxEU@bF!1eBplsw?N$T9>K@TKQasZ`(}y|OKW^{fIBD3>sfKp4 z8Q7=yVx=-42%{YwoF+`NALQ`Zc!yA9J2rY&mi=&QNEG8Re9#2Zv~j~99yushXlE^a z3UzRCoj9gw+5~48hwj>^xF{2?d-v&M6Rn&+rJMc(6mo;LC@uVRH|@O_8;wbeXdGK| z&u1LFoBOZd{sZrKH#GfMtHxopk@)k(p9S&zUNr>cH%O-T-u=z<4|9KUnqvE7;9uX+ zP*-1HQ&)$xW7DVmsd;Q%pMCaO7-Ufun-`TfFbj!G(Q%1hwMQjo8?e#+sW^Ff?5p?f z+m|TKpTBySd*hkFwR>Rd?cBYa$B%7uR9TZ}-HDc#7Bl?VRBzd`C5lUJZEa?)D8l^S zvu97*isF#+ryk#M1=ePca^-~!7x+^HVSZtC2dvu6S7wXR3C&n&mZF8_$D|w7yuH9 zM1cS2KmYmeyYJ$OGV&CW^3^yq#|g{CjaOk5gS{|I(MteEM!=6e=!srYfpZyH&Dc~h zTa`=c>FJo4mTk{qR{=wM!HUX&b$dZA3~t!t_&GMWhiF=sCk)W@4zr#80%E&l^&=2jn(q%BWd-Ly0#r@g)X$Kc<# zZ5wt7*suU67z>&sR#eVzJY$L(_<G@R$@Hn1z$FaG>MEF-93|p3;xARnVt6p~KTIPwr z%J!T~yz~!j6|pP2cI}!VMW*;6-Z-bgiIqSAwyg5vQfzVpSMQRjo3InY`5iHxNCnaw-n@=G$Rx8M=5o5kj?>p3H^eeuN?Yu2n0(3)nT zxOVT}EmIUntLuZypN?IwPHfkM=Q-OB-NUJYLW zcZZJxUJm#-G^|2K-0%v9gdqTbv}%WYK@+wm%%fSi~%0VRWjCr z!=r+GgvX<IrORNvl`sRfgy41i}t$I7`sUS`VL(!bZd3nWLpst=0gU zTBA}}>kaiC9KmWWX}|_}e>4g;2hZ1op>l1_77o z_3%+)20*P=4=#fdD$ZZT;RXrJ=hF!vlUxNC5yK-Sc&1bx)_S=KJlCd?5g}rWtj*xz zMbm?+Y8$h91^>2nkbxfK05VP#z!CnrjMa=^Tr%)Ms5i;nLFezGp6Qgkr`skI5A&e> z-{Qla>x>OItQrT2LSm=VNRT1={`>E_csm&T1jCqKd+jyOZwGsijvhTqF!~54AN+PL zv~lA`YHV)ie0SJ?(5h9d2qrASD(C!k$!*_J`YT^k#EVW$NT7HyUrl~?p?}8efGg?1Y4Z4cb+D%^)Hj`nU^SM>wc1M zKLLLoV)8ccC26z4Usu;c(&|m{)or3QT?3_7Hc=|>De%`Nm#yP`b@OWeLO6>cpPD0t zy(0y!-brC)PjLRZ*$t;T-`teOi!{Cdtid;T)!>_JqX~_d>9OW^dIYh0!|K~;PVI3@ zD$!AVQ3Zu7YY7`3k`$~X$%@q^$tfdd6tj&+e10)yWMuGuxLhUta0L{WsicKzITV_b zM@!-pC^9*VBIA=OBqf)E6QvXz7f)fa@XN(5gKsX8{1?WPIBF@0agU9QqlltLOp?B?t16&9(} z^^OVfO}Qt$Mepncw|R3VF-iR`Fkk6z0r9stBo+~Qc}1pv6pQ}qnAtw_Jac<9Zyexz z$-P{So;&K932D;u%MByWYCyhZwy%$mcM6<K7*qn?1)@EcTftD>kldoV@kOpB-4x-@+oD zNj7M)ulVb4X^7If4I#17->^ugpTNsg;lOl2DfxQ4PPE{Hl zmj?QA>yzv4FN^UA3Gv7cpY7=__Voc~mc+j9;Rz-hlcR@q$XX`xMGA=V@Dq!9SO)Hy zP_VT8Hn;ib1w2Kmycw8pc(|_s?d9WHxL}ZIWWjCz8u9l^k4*9Q@$&I;jrS4YeFAeQ zi)?L0!}1N>G(Im;x;6F79Vd46^mH=;FLWAWizFJ%Jg8hU;O`rWlFf2?+8nX3mp6EV zN1o@MGC^c#XKQOWNaU~-E>)Hc@Oj)L^@z&#^mUhb`+B|c`X?jy+^1lCNq2YZ}Hi$sH_GnN?Ww=&Yts*rm0UYMhDva_`xKHX{f z=;6U*9%3v?Kfj{Yj0CMoS^eme+Tr|RYdXkf~)1t*rkJ?UD-lm@{d4`fzL|)N~ zD8}nvrOx)YB9X(4A-GH*^Cvrz3ws9)_{plMKi(<}PNehL`_+5uP|;v#kwXwB%+}s% z#%Pfv6H_BRSw^NuEnHZt(`g2BuVX(aNX|4D=SjAMnMByxiN>-wPk;yV9z88A`}bj5 z0BNA3$aaY6;fZ#mm)P0b*@|pMqasjk0?&a7mxxg!`w5TQ*$$bZ8z!>*(_@2w0Dj;o zyL_Eiou5*!%T3BJ(pgP<#VSXtZjwz0PCtJ6i_`Mh^RgFnHgI*w6% zL!_=e)Rdp*;AFW{7iH92tZ@Jo8E$udUWIMB z@<}dJgeZYugA_#SoH=u5$&w{Fn?e8(!Y}Yc5J`k$4U0tQ62fJ|G75**vzVBGsQ9nG z`s&`4;OR`$y@7?+n>TMF-XbJ4EI1((hk2u$+i`3s$z3VQD3mOYN5};`&Eyi0gJ4jL zZ?!du-K505O;U?rlzK@FlIHN1Ya=V04+Sd0AVPIp&8TMKC|(x zUvLDx&Nxhk1Do%{(}O%{m?I8CzzrgDN=r-45DG3?J^y<-r8Hoc148Z3TLTlFy zf{kGF_|6ZI$ke{IM@KhZ1u=+5Li|c#Ow!)Hd(FBC^q?LA7NN;Gh}4-i*B>9En?a%o zZaX-zfXbmmhk}04hCs2dV5q^zZCkf)T^t)XZ_(1Qm75UNG`9H~LZdKE2;4yo7N!hB z(zUNCJ!oCNe0g};CY-d+6$hBcIU$IF9XT6QMTH?Oz!V{3U+_^~b8|BeSjCcP#B3pc z3sGN?$w|%ah^Yc+a270%T;eigHV(B-`wDq8dJsJ4>pu@Me;#$mXS#cWV;BVq(q*KD z-T81pNF8K}4~Q(0(KD?+3EY7gVX5k^vMD*t_+ zK!a(wrj?8_-qrqN10S^u`CUe{X0m6@O4XE51CSMzzm<@)T5nxvAmL9+$ zR0UC7I6{Yh;8hlXg^mJwp{q6{yJI zK+uvjxR8Q%JK#72B~U5h#dUby;ERjMD-TUCfTIciI(*`cnF5Q_Pks~#9T^G{ta`KbD%vgSDD?k11y5$4*o_6QpWkOGitXcm#&2YR$X0vZ~j1; z3sH||#0W+3u(-IGIoXK(7c9lQ4|*_nm@;=Caq8L6!4?pcdHNHaawDD+!F(*9ue@fK zr#D=pXvI`w;xL0|g58xrSn2S6yfE}VGM|0+8J^DB#RZmL@R~hL70Al`g2N!Rm?{2; zSi`ilU-GXQ$N*b1kG;p!g8)ZRX!_z{tgiHcd-&S0tNh%JFf73^W70A*=7mV0KQT4z zg=+7BkieUqQc;)n#2(qPce9VYC41@Z_<~gkwZu2>5O1(3{lYi!oSH_ucclm8BGwSm z(sv^OF`ak^`7laT7?^o*og(rTWMC|~wKMCsf}YUWB*b4rfN_n@DT3PeEyM$F zQ}XHmMUd{|Rz2p3g`%GS8Z!tFJDm1V1*kzxD&8OgNw@T%74M*{t1F_t5v&PTz|;X( znb+AQ!45IC4`DsRB@fSkTzdyXb+a3u0Z9pGufvDVv=Fys;F^v=-liRRxw4PH4|=k< z9VodR72^fS7 zjM(gFk7Edoh-C|eAu0koFrn@H?4$1>LI58=qX{bru4o0Xn3(N40~rvI+-<%DfFGX$ zS|G<1q@DXhw)G$)#SuV^K|xzm*}9%RdmAw1a!u0VkUSoYjW1cD2r+(&xK z%gYhX4^f#L7y`8TiW3_yuXzDb*F%R5M~z=dcvxIAYFGpV5#B5Wi=&H1!{XlfbRX%# z#!$NLSYol3$GLLlN(dNUNqpmun04&?5HN$CGM07rzfy6DtsvWunHsV8k)9V{d@(_( zVP9oq5%KpolXfEm4EGf)Rsd|o!w(~B1)m8Ct@n{0=puxXz@?4%@dr;NphH%#UJYQ; zkfE0@UHUcX!JC+!osC_m5R@;Ba<>dZ`LT7v*9Zc@edLdT^Os~E&~tC@J>bs+{@j}b Z_ip9o=XZF$ZAu$O{K1`Z;MeEN{{f@AXfgl* literal 0 HcmV?d00001 diff --git a/webapps/ROOT/index.jsp b/webapps/ROOT/index.jsp new file mode 100644 index 0000000..abcf517 --- /dev/null +++ b/webapps/ROOT/index.jsp @@ -0,0 +1,219 @@ +<%-- +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. +--%> +<%@ page session="false" pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %> +<% +java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy"); +request.setAttribute("year", sdf.format(new java.util.Date())); +request.setAttribute("tomcatUrl", "https://tomcat.apache.org/"); +request.setAttribute("tomcatDocUrl", "/docs/"); +request.setAttribute("tomcatExamplesUrl", "/examples/"); +%> + + + + + <%=request.getServletContext().getServerInfo() %> + + + + + +
    + +
    +

    ${pageContext.servletContext.serverInfo}

    +
    +
    +
    +

    If you're seeing this, you've successfully installed Tomcat. Congratulations!

    +
    + +
    + + + +
    +
    +
    +
    +

    Developer Quick Start

    + + +
    +
    +

    Examples

    +
    +
    + +
    +
    +
    +
    +
    +

    Managing Tomcat

    +

    For security, access to the manager webapp is restricted. + Users are defined in:

    +
    $CATALINA_HOME/conf/tomcat-users.xml
    +

    In Tomcat @VERSION_MAJOR_MINOR@ access to the manager application is split between + different users.   Read more...

    +
    +

    Release Notes

    +

    Changelog

    +

    Migration Guide

    +

    Security Notices

    +
    +
    +
    +
    +

    Documentation

    +

    Tomcat @VERSION_MAJOR_MINOR@ Documentation

    +

    Tomcat @VERSION_MAJOR_MINOR@ Configuration

    +

    Tomcat Wiki

    +

    Find additional important configuration information in:

    +
    $CATALINA_HOME/RUNNING.txt
    +

    Developers may be interested in:

    + +
    +
    +
    +
    +

    Getting Help

    +

    FAQ and Mailing Lists

    +

    The following mailing lists are available:

    + +
    +
    +
    +
    + + +
    + + + diff --git a/webapps/ROOT/tomcat.css b/webapps/ROOT/tomcat.css new file mode 100644 index 0000000..0cbaf7f --- /dev/null +++ b/webapps/ROOT/tomcat.css @@ -0,0 +1,398 @@ +/* + 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. +*/ +body { + margin: 10px 20px; + text-align: center; + font-family: Arial, sans-serif; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p, +ul, +ol { + margin: 0 0 0.5em; +} + +h1 { + font-size: 18pt; + margin: 0.5em 0 0; +} + +h2 { + font-size: 16pt; +} + +h3 { + font-size: 13pt; +} + +h4 { + font-size: 12pt; +} + +h5 { + font-size: 11pt; +} + +p { + font-size: 11pt +} + +ul { + margin: 0; + padding: 0 0 0 0.25em; + text-indent: 0; + list-style: none; +} + +li { + margin: 0; + padding: 0 0 0.25em; + text-indent: 0; + font-size: 80%; +} + +pre { + text-indent: 0.25em; + width: 90%; + font-size: 90%; +} + +br.separator { + margin: 0; + padding: 0; + clear: both; +} + +a img { + border: 0 none; +} + +.container { + padding: 10px; + margin: 0 0 10px; +} + +.col20 { + float: left; + width: 20%; +} + +.col25 { + float: left; + width: 25%; +} + +#wrapper { + display: block; + margin: 0 auto; + text-align: left; + min-width: 720px; + max-width: 1000px; +} + +.curved { + border-radius: 10px; +} + +#tomcat-logo { + width: 150px; + height: 106px; +} + +#navigation { + background: #eee url(bg-nav.png) repeat-x top left; + margin: 0 0 10px; + padding: 0; +} + +#navigation span { + float: left; +} + +#navigation span a { + display: block; + padding: 10px; + font-weight: bold; + text-shadow: 1px 1px 1px #fff; +} + +#navigation span a:link, +#navigation span a:visited, +#navigation span a:hover, +#navigation span a:active { + color: #666; + text-decoration: none; +} + +#navigation span#nav-help { + float: right; + margin-right: 0; +} + +#asf-box { + height: 40px; + background: #fff url(asf-logo-wide.svg) no-repeat top right; +} + +#asf-box h1 { + margin: 0; + line-height: 1.5; +} + +#upper { + background: #fff url(bg-upper.png) repeat-x top left; +} + +#congrats { + text-align: center; + padding: 10px; + margin: 0 40px 20px; + background-color: #9c9; +} + +#congrats h2 { + font-size: 14pt; + padding: 0; + margin: 0; + color: #fff; +} + +#notice { + float: left; + width: 560px; + color: #696; +} + +#notice a:link, +#notice a:visited, +#notice a:hover, +#notice a:active { + color: #090; + text-decoration: none; +} + +#notice img, +#notice #tasks { + float: left; +} + +#tasks a:link, +#tasks a:visited, +#tasks a:hover, +#tasks a:active { + text-decoration: underline; +} + +#notice img { + margin-right: 20px; +} + +#actions { + float: right; + width: 140px; +} + +#actions .button { + display: block; + padding: 0; + height: 36px; + background: url(bg-button.png) no-repeat top left; +} + +#actions .button a { + display: block; + padding: 0; +} + +#actions .button a:link, +#actions .button a:visited, +#actions .button a:hover, +#actions .button a:active { + color: #696; + text-decoration: none; +} + +#actions .button a span { + display: block; + padding: 6px 10px; + color: #666; + text-shadow: 1px 1px 1px #fff; + font-size: 10pt; + font-weight: bold; +} + +#middle { + background: #eef url(bg-middle.png) repeat-x top left; + margin: 20px 0; + padding: 1px 10px; +} + +#middle h3 { + margin: 0 0 10px; + color: #033; +} + +#middle p { + font-size: 10pt; +} + +#middle a:link, +#middle a:visited, +#middle a:hover, +#middle a:active { + color: #366; + font-weight: bold; +} + +#middle .col25 .container { + padding: 0 0 1px; +} + +#developers { + float: left; + width: 40%; +} + +#security { + float: right; + width: 50%; +} + +#lower { + padding: 0; +} + +#lower a:link, +#lower a:visited, +#lower a:hover, +#lower a:active { + color: #600; +} + +#lower strong a:link, +#lower strong a:visited, +#lower strong a:hover, +#lower strong a:active { + color: #c00; +} + +#lower h3 { + color: #963; + font-size: 14pt; +} + +#lower h4 { + font-size: 12pt; +} + +#lower ul { + padding: 0; + margin: 0.5em 0; +} + +#lower p, +#lower li { + font-size: 9pt; + color: #753; + margin: 0 0 0.1em; +} + +#lower li { + padding: 3px 5px; +} + +#lower li strong { + color: #a53; +} + +#lower li#list-announce { + border: 1px solid #f90; + background-color: #ffe8c8; +} + +#lower p { + font-size: 10.5pt; +} + +#low-manage, +#low-docs, +#low-help { + float: left; + width: 32%; +} + +#low-docs { + margin: 0 0 0 2.2%; +} + +#low-help { + float: right; +} + +#low-manage div, +#low-docs div, +#low-help div { + min-height: 280px; + border: 3px solid #ffdc75; + background-color: #fff1c8; + padding: 10px; +} + +#footer { + padding: 0; + margin: 20px 0; + color: #999; + background-color: #eee; +} + +#footer h4 { + margin: 0 0 10px; + font-size: 10pt; +} + +#footer p { + margin: 0 0 10px; + font-size: 10pt; +} + +#footer ul { + margin: 6px 0 1px; + padding: 0; +} + +#footer li { + margin: 0; + font-size: 9pt; +} + +#footer a:link, +#footer a:visited, +#footer a:hover, +#footer a:active { + color: #666; +} + +.copyright { + font-size: 10pt; + color: #666; +} \ No newline at end of file diff --git a/webapps/ROOT/tomcat.svg b/webapps/ROOT/tomcat.svg new file mode 100644 index 0000000..8823f79 --- /dev/null +++ b/webapps/ROOT/tomcat.svg @@ -0,0 +1,967 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 2006-05-09T08:17:21Z + 2006-05-09T08:37:38Z + Illustrator + + + + JPEG + 256 + 184 + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAuAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXhH/OYHnWfQ/wAurfRLSUxXXmK49GQqaN9VtwJJqH3cxqfYnFXhP5Y/ +85O+f/JU0enaw769okbBJLS8ZvrUKg0IhnarDj/I9R2HHFX2F+Xn5neT/P8ApP6R8u3glKAfW7KS +iXNuzdFljqaezCqnsTirK8VdirsVdirsVdirsVdirC/zM/Nvyd+XemC71255Xcqk2WmQUa5nI2+F +CRxUd3ag+nbFXx1+Zf8Azkn+YvneaW1tLh9C0NgwXTrB2V3Sm/rzji8m3UDitP2cVfV//OOfmabz +D+T3l+6uHMl1aRPYTsxqSbVzEhJ7kxKhxV6VirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd +irsVfHn/ADlxdSa7+bvlvyvGx4RW0EVARtNfXJVqf7BY+uRlKgT3JAt5r/zkD5ZGgfmfqSRR+nZ6 +gsd9agdOMq0f/ksj5h9nZvEwgnmNi2Z4cMiw/wAqebPMHlTXLfW9BvHstQtjVZEPwstQWjkXo6NT +4lOxzOan3v8Akl+cel/mX5a+tAJa69ZcU1fTlJojGvGWLluYpKbV6GqmtKlV6NirsVdirsVdirsV +eWfnr+eGl/lroywwBLzzPfox02wJqqL0+sT03EanoOrnYdyFXwh5i8x655j1i41jW7yS+1K6blNc +SmpPgABQKo6BVFB2xVnf5Q+SjrWh+d9Yli5w6XolylsadbqSNnTj8kiYf7IZg6zUeHKERzlIfL8U +3YoWCe4Pff8AnCfVTN5D1zTCamz1P11HcLcQIAPlWE5nNL6KxV2KuxV2KuxV2KuxV2KuxV2KuxV2 +KuxV2KuxV2KuxV2KvjD8wm/Sv/OX8UTGsdrqGnCMNUU+rW0Mp6f5ammY2sNYZ/1T9zZi+oe9m/8A +zkx+Xc/mPytFrunRepqehc3ljUVeS0cAyAU6mMqHA8OXfNB2PqhCfAeUvv8A2uZqcdix0fIedQ69 +m35OefrryN+YOla2kpjsjKttqqDo9nMwEoI78ftr/lKMVfaeqf8AOSH5KaaSs3meCZx0W1inuanf +YNDG69vHFWM3v/OYn5QW5YQ/pK8ArQwWqitPD1pIuvviqVT/APObH5cKR6GjaxIP2i8dqhB9qTvi +qmP+c2fIFd9C1Wnfa2/6q4qmFv8A85n/AJUSvxksdZtx/NJb25H/ACTuHOKp3bf85XfkpPBI7avN +BIisywS2lwGcqCeIZUdKmm1WGKvijzz5x1bzl5q1HzFqjlrm+lLrHWqxRDaOFP8AJjSij7+uKpNb +W1xdXMVtbRtNcTuscMKAszu54qqgbkkmgwE1uVfbHkL8uk8o/lTPoMiK+o3drPNqZHRrieIhlr4I +tEB9q5yWo1fi6gS/hBFfN2UMfDAjqwT/AJwdvyt/5usC20sVlOq77em0yMR2/wB2Cudc619ZYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXxZKTJ/zmFc+oedNTmA5b/ZtG49fCgpmH2h/ +cS9zbh+sPqDrsc4t2r57/Nf/AJxkGo3c+teSTFb3ExMlxo0hEcTMdybd/spU/sN8PgQNs3+i7Xoc +OX5/rcLLpusWIaF/zif56vFWTVr6y0pG6xgtczL81QLH90mZWTtnFH6bk1x0sjz2Z1pf/OIvlOIL ++lNbvrthSv1dYrZSe+zC4ND88wp9uTP0xA9+/wCptGkHUsms/wDnGf8AKS3AEunT3dOpmupxXam/ +pNFmPPtjOeRA+H67bBpoPDv+ch/yt03yXrdjeaFbG30HUouCQ8pJBFcQ0DqXkZ2+NSrCrfzeGbns +vWHNAiX1BxdRi4TtySH8jfJdn5u/MOy07UIfrGl28ct3fw1IDRxrxUEqQaGV0By7X6g4sRkOfRhh +hxSp9N3X/OO/5P3FSdBETGnxRXN0nT/JEvH8M50dq6gfxfYHOOnh3JDqP/OKn5a3NTazajYt+yIp +0dfpEsbn/hsvj21lHMRP497A6SPmwzW/+cQr9A76H5himO/CG9haL5AyxGT/AIhmXj7cifqiR7t/ +1NUtIehZh+S3/OP8Xk+5GveYXivNfTkLSKIloLYGqlwzBecjL3p8P45i9odqeIOCH09fNtw6fh3P +N7DfIz2VwijkzRuFA6klTmpxmpD3uRLk+bf+cJrrj+Yet2tT+90hpeP7J9O5hWp9/wB5tneunfZm +KuxV2KuxV2KuxV2KuxVZLNFDG0srrHGu7O5CqB7k4qks3nzyNC5jm8xaZHIOqPeW6nf2L4qmFhrW +j6iK6ff294KVrbypLt1r8BPjirAvzb/Pnyf+WrW9rqKS6hq90vqRaba8eaxVp6krMQEUkEL1JPbq +cVYFof8Azmp5BupVj1fR9Q0wNsZo/SuY1/1qGN6fJDir2Xyf+Yfkrzjam48taxb6iqgGSKNisyA9 +PUhcLKn+yXFWRYq7FXYq7FXxRrBNj/zl/NVwC+rL8XtcWw+Hf/jJTMXXC8M/6pbMP1h9SZxLtnYq +7FWG+afzg/LnyvdNZ6vrUSXqGj2sKvcSofB1hV+B/wBamZmHs/NkFxjt8mqWaMeZRPk78zvI/nF5 +ItA1RLm5hHKS1dXhmC1pyEcoRmXputRkdRosuLeQ2TDLGXJCfm/5JXzj5D1HSo05X8a/WtNPcXMI +JUD/AFxVP9lk+z9R4WUE8jsWOaHFGnl3/OI/lpodN1zzFMlGuJUsLcsKELCPUlpXsWkQfNc2Xbmb +eMPj+r9LRpI8y+hc0DmuxV2KuxV2Kvl//nClHP5oas4B4Lok6luwLXdqQPpoc9AdK+08VdirsVdi +rsVdiqXeYPMOi+XtIudY1q7jsdNtF5z3EpooHQAd2ZjsqjcnYYq+VfPf/OV3nXzNqp0D8stPlto5 +mMcF0IfrGoT+8UIDrGD8mbvVcVSqz/5xn/Pjzs66h5t1RbUueX+5W7kurgA/yxx+sq/6pZaeGKsj +h/5wanMYM3nNUk7qmml1/wCCN0n6sVQt7/zhDr8B56Z5stppEIMZntZLfcb1qkk9KHFXzr5mtdUs +tfv9O1S5a7vtOuJbKaZndwWt3MZ4mSjcartUDFUsxVFabqeo6XfQ3+m3UtlfW7c4Lq3dopUbxV1I +IxV9Sfkr/wA5aNcT2+gfmG6K8hWO18wqAi1OwF2q0Vf+Mi0H8w6tir6lVlZQykMrCqsNwQe4xVvF +XYq+Kfzzro3/ADlLa6oxKJLdaReFiaApGsMLeG1ISMqzw4sco94LKBogvqPOEdw7FXkf55/mBrlj +Jp3kbykX/wAVeYSFE0Zo8FuzFOSt+wzlW+P9lQx2NDm27N0sZXlyfRFxs+Qj0jmUd5B/IHyP5bsI +31Oyh1zWnAa6vb1BMnqHciKKSqKAehI5e+Q1XamTIfSeGPlzTj08YjfcsJ/PDy5pXkHX/LH5geW7 +WPTGhvlt9Rt7RBFHKpBk+wgCjnGkiPQbg5m9m5jnhLFM3s1Z4iBEg+hOu4zn3NQOkaLpuj20ltp8 +IghlnnunRe8tzK0sh/4JzQdhtlmXLKZuXdXyYxiByR2VsnYqxjV/zO/L3SJWh1DzDYQzoaPD66PI +p/ykQsw+kZlY9Dmnyifu+9qOWI6pvoOvaRr+kwato9yt3p1zz9C4UMob03MbbMFOzoR0ynLiljkY +yFEM4yBFhV1WVYdLvJWJCxwSOxHWioTjhFzA8wsuRfPn/OEVoX83eZLzekOnxQnpSsswb/mVneOn +fYOKuxV2KuxV2KqF9e2lhZT315KsFpaxtNcTuaKkcYLMzHwAFcVfFHnPzR50/wCchPzJi8veXlaH +y7aO5sYnqsUUCkK97dU/bYdB2qFXcklV9U/lj+UnlH8u9IWz0a2WS+dQL7VpVBuLhh1q37KV+yg2 +Huakqs1xV2KuxV8v/nf/AM4patrnmG+80eSp4Xn1GR7m/wBIuW9ImdyWd4JSOH7xjUq9KGvxb0Cr +5/1j8mPzX0iRkvfKepgL9qSC3e5jG9P7yASJ1PjiqRjyb5vMvpDQ9QMtePpi1m5culKca1xVPtG/ +JT82dYdUsvKepUf7MlxA1rGe395cekn44q+zf+cffKv5m+VvJ50bzvPbzRwFf0RFHK01xbxU+KCV +6cCqmnDizU3FaUAVeo4q7FXx5/zmxpD2vnTy7rcdUN5YPbh12POzmL1qO4FyuKsl/Lz/AJyc8ra2 +sNj5mUaHqZAU3TGtnI3Qnn1ir1o/wj+bOY1XY8474/UO7r+1z8epB2Oz2iKWKaJJYnWSKQBkkQhl +ZTuCCNiDmnIINFygVGXTNOmvYb6W1hkvbbkLe6eNWljDgq3ByOS1UkGhwjJIDhs0ei0LtE5FLxD/ +AJyycP5F0ezQcp59WjaNdt+NvMp/GQZuuxI/vJH+j+lxNWfSPe9rgiEMEcQNRGoQE9+IpmmlKyS5 +QCpgSsllihieWVxHFGpeR2NFVVFSST0AGEAk0EEvn2fVfOv5269e6foN9Jof5e6fIYbm9QMst2af +ZIBUtyG4QkKqkFqmgzfiGLRQBkOLKfx+C4ZMspobRZzof/OOv5U6VCiyaUdSnUUa4vZZJGb5opSL +7kzBydrZ5HY8PuDbHTQDP9G0XStE02HTNJtks9Pt+Xo20Qoi83LtQe7MTmBkyynLikbJboxAFBJv +zO1Aaf8Al35lu60ZNNuljP8AlvEyJ/wzDL9FDizQH9IfYxymol59/wA4P6S0eg+adXI+G6ura0Vv +e2jeRgP+kkZ2zqX01irsVdirsVdir50/5zJ/MGbSfK1j5PspOFxrrGa/KmhFpAwon/PWWn0KR3xV +mf8Azjd+WEPkj8vrae5iA17XES91KQijorrWG333HpI24/mLYq9YxV2KuxV2KuxV2KuxV2KuxV2K +obUdT03TbR7zUbuGytI/7y4uJFijX5u5VRir5U/5yz/MX8tfNfl7S7DQtZh1LW9NvS5W2V3iFvJG +yyUnC+kfjVPsscVSv8i/yi/LTzn5Ij1XVLSafU4J5rW9C3EkaFlIdCFQrT926980XaOuy4cnDGqI +vk5eDDGQsvdvKXkby35StXtdBgmtrZ6Vge6uZ4wf5ljmkkRCe5UCuaPPqp5Tc9/gHLhjEeSN8x3+ +o6foGoX2m2hv9QtoJJbWyFazSKpKxjjv8R22yOCEZTAkaBZTJAsPHv8AlcP53/8Altpv+BuP+ac3 +H8n6X/VPti4vjZP5rzz8wfPP5i+bfNvluw1Dyq1rqWjzG+g0ROZmuRVZDVGHPjxgbcDpXNhpdNiw +wkYy9Mutj8dWnJOUiAQ9D/5XD+d//ltpv+BuP+ac1/8AJ+l/1T7Yt3jZP5rv+Vw/nf8A+W2m/wCB +uP8AmnH+T9L/AKp9sV8bJ/NYp+ZX5v8A5qXnli40LVfKbaCutAWkdyxlWRwWXnHGrheRdfhI8DmV +pNBgE+KMuLh9zXkzTIoirR/kbzf+bvlHy1Y+XtO/LedobYENM6zK0kjtyeRzxoOTH6BtkNTp9Plm +ZyyfaEwnOIoRej+RPO35o6xr62fmPyf+hdNMTub71C1HWnFaV/azX6rS4IQuE+KXds348kyaIZ7q +jaqthKdKSCS/pSBbp3jhr4uY1kbbwA38Rmux8PF6r4fJuldbPlv8+YvzstdPS483apafoO7nEEVh +pcjJbl6NIA0bKkjgenWsnKhpnTdnHTH+7HqHfz+f6nAz8f8AFyfQ3/OLHl06N+TWkyOnCfVpJ9Rm +Hj6r+nEfphiQ5t3GeuYq7FXYq7FXYq+MfzQhXzz/AM5YWmgz1lsLe7sbB4zvW3gRbi5TvSrNLir7 +OxV2KuxV2KuxV2KuxV2KuxV5j59/5yM/K7yb6kFxqQ1TU0qP0dpvG4cMO0kgIij36hn5e2KvAvMv +/OWP5p+arl9P8laWukxtXiYIzfXvHpUuy+mg+UdR/NkJ5IwFyIA80xiSaDF/+VT/AJo+b7sah5w1 +h1kavx3sz3k617KgYoo9uYp4ZptR7QYIbRuZ8uXzP7XMx6GcuezJYf8AnH3yrBptwjXFxd6g8LrB +NIwSNJSpCOEQA7NvRmOak+0eQzGwjCxfU11/FOT/ACfEDnZYH+S+sfmZZeajoHlC8htrq6ZnubC/ +K/VnMAPLkrAtyUdfT+Kg8BnSa7HhMOLINg6/CZA1F9k6KdbOmw/pxbZdTp/pH1IyNAW8U9UK9Pnn +I5eDi9F8PnzdlG63R2VsmndUUu5CooJZiaAAdSTiBaHhP5N8/On5r+bPzEkBbT7dv0do7EGhWgUM +tRswgjUsP+LM3vaH7nBDCOZ5/j3/AHOJh9UzJ7vmicx2KvEf+clQLS78i63cEjT9O1cC6O3H4mjl +FR/qwPm77G3GSPUj9f63E1XQvbQQQCDUHoc0jlN4pSXzN5z8q+V7ZLjX9Tg0+OSvpLK37x+PXhGv +J3pXfiMuw6bJlNQFsJ5BHmXzJ+dn5haf+Z/mby75e8qtLPbLN6EbyI0YluruRI0oh+KigChIHU50 +/ZmilhieL6i4GoyiZ2fbWh6Ra6Noun6PaClpp1tFaW4/4rgQRr+C5s3HR2KuxV2KuxV2KvjfymCP ++c0p/rdK/pTU+POlKfUp/S/4144q+yMVdirsVdirsVdirsVeQfmX/wA5Ofl55MaaxtZv0/rcdVNl +ZMDEj+E1x8SL4ELyYdxir5W/Mf8A5yD/ADJ88GSC6vjpmjyVC6VYFoYmQ1FJXr6kte/I8fADFXme +Kvpj8jdTtb3yJBFFGkdxYyyW9zwVU5MDzRzTqSjipPU1zhvaDHKOosk8Mht5d/6/i7rQSBh5h6Fm +ic12Kvnvz6l35B/Nqz8z2CEQyzLqMSqeIY143UVf8upr7Pnedl5RqdLwS5gcJ/R9n2uj1MPDyWPe ++wdL1Ky1TTbXUrGQTWd5Ek9vKOjJIoZT9xznMkDCRieYc2JsWisgyYZ+b1p5vvfIGqWPlSFZ9Tu0 +9F1LiN/q77TelXYuV+EAkddt6A5vZ8sccoMzsPv6NOYSMdnzl+Wn5m/mVoKR+RtEtNLsrmGWSsOp +q1vM87t8Su8ssS+p0UKaGgAGdDqtHhyfvJ2fd3fBwseWUfSHq36V/wCcqf8AqzaN/wAGn/ZRms4N +B/OP2/qci83c79K/85U/9WbRv+DT/sox4NB/OP2/qW83c8o/Mj8z/wAy/MAm8i6zaaZfXU0sY9HT +Ea4lSdGqqxvFLKvqbFSBXqQc2el0eHH+8jY2693xcfJllL0l9KflXb+bbXyJpVp5riWLV7aIQsqu +JGMSbRGUio9ThQNQnx70znNccZyk4+R+9zsIkIi2W5iNqB1xdH/RF2+sxQy6XFE8t4tyiyRelGpZ +i6uCpAAyzFxcQ4D6ixlVb8nzj/zjB5UtfNn5xal5tisltNE0Rpbu1tEUCOOa6ZktYgBt+7j5tt3U +Z3UIkRAJt1BO77PySHYq7FXYq7FXYq+M/wAyX/wb/wA5b2WsP+7s7q90+7Zz8NILlEt7htqV3EmK +vszFXYq7FXYq7FWGfmR+bnkn8vrD6xr16PrkilrXS4KPdTdacY6jitRTmxC++Kvjz80/+clPPvnk +TWVq50Py45KfULRj6kqntcTjiz1H7K8V8QeuKsQ/KyLyvP5wtbTzFbC4trn91bc2IjW4JBj9QAjk +G+zQ7VIrmB2mcowE4jUh93Vv0wiZgS5Po7zD5J8ta/pa6bf2UfoQrxtWiAjeDbb0io+Hp06eIzht +N2jmwz4oyu+d7373dZNPCYoh8/effyj17yuZLu3B1DRgSRdRr8cS9f3yD7P+sPh+XTOz7P7Wxajb +6Z936u90+fSyx78wnP8Azj5r4s/M11o8jUi1OHlED/v63qwA+cbP92YvtDp+PCJjnA/Ydv1NugyV +Ou99C5xDuWDeefKvnzV9WiufL+v/AKKskt1jkt+Ui8pQ7sX+AEbqyj6M3XZ2t02LGRlhxyvnQO23 +e4eow5JSuJoe8sD81/lL+ZF9pj3Go65Hq7WKPLBbMZGc7VZY+S9WC9O+bnSdsaQTEYQ4OLyAHxou +Jl0mWrJuvel/5Q/8rK80ySeXdA85S6P9Qh9W2spZ51RouXx+kEDD4CwqPfbvmz1pw4xxzhxX5Bxc +XFLYGnv35Y+RfzR0DXri881+af03p0lq8MVp6s0nGZpI2WSkiqNkRh9OaLW6rBkgBjjwm+4D7nMx +Y5g7m3p2axyGGfmF+U3k/wA82pGq23paii8bfVIAFuEpWgLU+NN/st9FDvmZpddkwnbePc1ZMMZ+ +95R/iv8AMz8lbm20/wAzMPMvk2Z/Ssr5XpcIBvxXmSwKr/ut6r2Vxm28HDrAZQ9OTr+P0uNxzxbH +cNSeb/zJ/Om9uNM8pk+XPJ0Lelf6g7D13DD7L8DyJZf91oafzNTEYMOjAlP1ZOn7P1qZyymhsHrH +5d/lN5R8i2gXS7f1tRdaXGqTgNcPXqAeiJ/kr9NTvmq1euyZjvtHucjHhEPezPMJuePedvy3/OXV +fNF/qGg+c/0ZpM7KbWx9a4X0wI1VhxRSoqwJ2zc6fWaaMAJQuXuDizxZCbB2eNfm7F+Z3lQQaDr3 +nKXV21SJmm0+GedgIQwCmVXC7OwIUd6HNtopYcvrhDhrrQcbKJR2JeieSv8AnHD8+9H0SJtG83Q+ +XlvlS5udPinuonSR0Hwy+nHxLqPhO5zYtD2r8mvJH5m+V/0x/jjzN/iL659W/R/76eb0PS9X1f75 +Vpz5p08MVel4q7FXYq7FXYq+Xv8AnNjya81joXnG3Sv1Vm0y/YCp4SEy25PgquJB82GKva/yY87J +5z/LXRNbaTneNALfUfEXVv8Au5SR25leY9mGKs2xV2KrZJI4o2kkYJGgLO7EBVUCpJJ6AYq+aPzm +/wCctrTTWn0L8vmjvL1ax3GvOA9vEehFsh2lYH9tvg8A1cVeMfl95AvPzCvLrzP5l1SW6iNwUueT +tJdTyqqsQ7tXgvFgPGmwp1zS9rdrflqjEXMj4OZpdL4m5Oz3O18seXrXSP0PDp0C6ZSjWhjVkb3c +NXk3ud842etzSyeIZHi73bDDAR4a2eaeb/yBsLlmvPK9x9QuQeX1OYs0JPX4JN3j/EfLN9ovaIj0 +5hfmP0j9XycLNoBzh8noHku+1y50OKLXrV7XWLT9xeB6FZGUCkyOvwsHG549DUds03aOLHHJxYiD +jluPLy8v1OXp5SMakPUE9IBBBFQdiDmCDTe841/8pLaHW7bzL5U42OqWkyzvYfZt5+JqyrT+6LrV +f5fl1zoNL21xQOLPvGQri6j39/3+9wMujo8UOY6PSB06U9s54uewnzt5H8z69qsV5pXme60W3jgW +F7WAyhWcO7GQ+nLGKkMB07Zt9BrsGGBjkxiZvnt5d7iZ8M5m4ypj/wDyqbz9/wBT/f8A/BXP/ZRm +d/K+k/1CPyj+pp/K5f55+15z518keZ/y91G01W01SZ2nLiPVrYyW8qTMDzQurFgXQnfl8Qrm90Pa +GLVxIrl/CXCz4JYiHv8A+Qeia/NDH5tufO155k0u+s3gGm3Tzt9XufUjZuQkmlUPHwZdh0NQaHfV +9qTgP3YgIyB57bhv04PO7eyZp3KYZ+afm/zN5Z0KGby5okmtanezC1gVAXSF3UlXkRPjYbdqDxYd +83Q6eGWR45cIG7TmmYjYMC8p/kVrGu6ovmj81b1tV1Njyi0YODBEOoWQp8FB/vuP4fEtXM7P2nGE +eDAKHf8Aj7y1QwEm5orzX+Rd9pepP5n/ACuvm0HWlq0mlhqWc46lFBqqV/kYFP8AVyODtMSHBnHF +Hv8Ax9/NM8BBuGxZB+VP5j+ZPMs9/ovmbQJ9J13R1Q3s3ErbPzNEoGPJWehIA5KQKhu2Ua7RwxgT +hK4yZYcplsRuHo2a1yHh35u+SvN1nNrXnD/lYl/omiIFli0yB7gBSEVFiiC3EacpHGwAG5zd6HPi +lw4/DEpd+3z5OJmhIXLi2eW/lJ+UXnn829Svtdl1ue0XTjGo127MtzM9ytDHHG5dXrGg5E8vh+Hx +zo4QERQFBwSSeb2z/oXX86P/AC8Gq/8AI2+/7Kskh6L+UP5dedPJv6W/xN5wu/Nf1/6v9U+tvO/1 +f0fV9Th68s3956i1pT7OKvRcVdirsVdirsVY/wCf/J9l5x8nar5bvKLFqMDRpKRX05R8UUlP8iRV +b6MVfLf/ADiz50vvJX5han+XXmGtsmoztDHE/SLU4Dw4jt++Qca9yEpir7ExVK/MnmbQvLOjXGs6 +5eR2Om2q8pZ5TT5KoG7M3RVUVJ6Yq+M/zS/PHzr+bWrnyv5Vt5rPy67fDZoaS3CqaerduDRU/wAi +vEd+RplWbNDFEymaiGUIGRoc0Nc/846uugI1vqXPX1BaRGFLVtv7tTTmtP5z18BnOw9pInLRj+77 ++vv/AB9rsD2eeHY+pV/Io6rofmDWPK2rwSWlzJEl3FBIKCsbem5UjZuYddxUHjke34xy4YZYGwDW +3n/YuhJjMxL2rOSdq7FXYq7FXYq7FXYq7FUt8w6Bp2v6Pc6VqCc7a5XiSPtIw3V0J6Mp3GZGl1M8 +GQTjzH2+TXlxicaLxryB5w1r8nPPM+i63yl8v3rKbrgCVKE0ju4V8R0ZR13HUDO3ywx67CJw59P1 +H8ebpgZYZ0X1xZXlpfWkN5ZyrPa3CLLBNGQyOjiqspHUEZzE4mJo8w54N7q2RS7FXYq73xVTuLi3 +treS4uJFht4VMk00hCoiKKszMdgAOpwxiSaHNBNPlfzv5j8wfnh+Yll5O8qBhoVtKTFKwIQqvwzX +047IgNEB33p9p6Z13Z2iGGNn6zz/AFOtz5eM+T7B8j+TdG8m+V7Hy7o8fCzso+Jc/blkO8ksh7s7 +bn7htTNi0J9irsVdirsVdirsVdirsVfLP/OXf5WXENxb/mXoKNHNCY4tbMNVdWQhbe7BG9RtGx/1 +PfFWefl3/wA5I+VdQ/KqTzN5mu0ttV0YLbavarT1Z7gqfSaCPbl9YCkgdFIb9la4q+cvNPm3z/8A +nr5uCUNnolo1YLRSxtrOIkgSSdPUmYd+p7cV6Yms1mPTw4pn3DqW3FhlkNB695O8l6J5U00Wemx/ +vHAN1duB6szDux8B2XoM4LXdoZNTK5cug7vx3u7w4I4xQT/MFvUJbGzluYbqSFGubfl6ExA5oHFG +AbrQjqMsjmkImIPplzDEwBIPUNahew2Nhc3s54wWsTzSt4JGpZj9wxw4zOYiP4iB81nLhBPc8w/J +Tzn5v8y3mqHV7oXFlaIhjHpojLJKxIAZQtQFQ9a50XbujwYYRMI8MifsH4DgaLNOZNmwHq+cy7F2 +KuxV2KuxV2KuxVjXnzyLpnm/SDZ3P7m7hq9leAVaJyO/ijftL/EDNj2d2jLTTsbxPMfjq4+o04yD +zeb/AJZ/mj5g/KrXZPKnmyKSTQS9QFq5t+Z/v7c/txP1ZR8x8VQet1Gmx6vGMmM+r8bF1UJyxS4Z +PqrTNT0/VLCDUNOuI7qyuVDwXETBkZT3BGczkxygeGQohzgQRYRWRZOxVSurq2tLaW6upUgtoVLz +TSMEREUVLMxoABhjEyNDcoJp8v8A5n/mrr/5n65D5E8hQTTadcy+kxQcZL1lNeTV+xbpTl8VNvia +nTOp7O7OGL1S+v7v2uvz5+LYcn0j+SX5N6V+Wvlv6uCl1r96FfV9RUGjMKlYoq7iKOu38x+I+A2z +jPR8VdirsVdirsVdirsVdirsVSDz3rvlfQ/KWp6h5oaMaGsDx3kUgDCZJFK+iqEjm0leIXvir81d +SfTpdTupdPhkt9MedzawyMJJI4WYmNGeihmCbV74q+q/y8tfLEHlOyPlsV06VefqGnqvJ0czH/fl +RQ+HQbUzzrtWeY5z4v1D5V5eTv8ATCAgOFkma5yHYq7FWIfm3qBsfy81mRftSxLbge08ixN/wrHN +r2Jj4tVHys/Z+txdZKsZSD/nH3TRb+S5rwj4767kYH/IjVYwP+CDZm+0mQnNGPQR+/8AAauz4+gn +zenZzrnuxV2KuxV2KuxV2KuxVjnnbyLovm3Tfqt+np3MYJtL1APUiY+Feqn9pe/zocz9B2jk00rj +vHqPx1aM+njkG/N4/ovmf8xfyX1w2rr9b0W4fkbVyxtLgDq8T0Jikp12r4gimdkPA12PiHP7R7/x +7nUETwyovpX8vvzc8m+eLZf0ZdCDUgKzaVcEJcKR1KitJF/ykr70O2aHVaDJhO4uPf8Ajk5ePNGX +vTXzl578seTtMOoa9eLboa+hAPimmYfsxRjdj+A7kZVp9LPMaiP1Mp5BEbvmXzJ54/Mb87vMcflj +y1ZyQ6SzhksENFCKf96L2YbcV60+yDQAM1Cep0eghgF85d/6nX5cxn7n1H+S35IaB+Wmkkxlb3zD +eIo1LVGHyJhgrukQbfxbqewGe0vSsVdirsVdirsVdirsVdirsVQup6np+l6fc6jqNwlrY2kbTXNx +KeKJGgqzMfYYq+HfzQ/MTzL+dvnmHSNFR4PLtm7fo+2eoUIKh7y5pX42BoB+yPhG5JajU6mGGBnM +7BnjxmZoPQ4Pyv8AK8fk1vK5i5W8g5yXVAJjcU2nr/MO3am3TOGl2xmOfxfs6V3ft73dDSQ4OH7X +kehaz5g/KfzbLpWqK0+jXLB5VQfDJGaqlxDU7MKfEv0HsR0uowYu0MAlA+ocvI9x/HmHXY5ywTo8 +n0Fp2o2OpWMN9YzLcWlwoeGZDUEH/Pcds4jNhljkYyFSDuYTEhY5KzTQoaPIqnwJAOCOOR3AKmQH +VyzQueKyKx8AQTiccgLIKiQPV5t/zkDctD5FijHS5voYm37BJJP1x5vPZwf4Qf6h+8OH2h/dj3p3 ++UNt9X/LnRkoQXjklNRQ/vJnf9TbZjdtyvVT+H3Bs0Y/dBmOalynYq7FXYq7FXYq7FXYq7FUHq+j +6ZrFhLYanbJdWkwo8Tjb2II3Vh2I3GXYNRPFLigaLCeMSFF4R50/JTXdCnOq+VpJby1ib1FjjJF5 +ARuCvGhenYr8Xt3zstB25jzenJ6Z/Yf1fF1OfRShvHcJFJ5F/M7zRY3PmTUI7m8eKMFHvZHa6mRe +0SvV2CjcdK/s1OZsu0NNimMVgHy5D39zQMGSQ4qfTP8AziV518hXnlX/AA3p1lBpPmi0XnqUIr6l +6F2+sq7lnfr8SV+A9AFIzYtD6BxV2KuxV2KuxV2KuxV2KuxV2KvjX/nI7847/wA+eYk/L/ye7XGj +QTiO4kgNRfXSnswNDBEeh6Egt0CnIZMkYRMpGgExiSaDJvy88h2PlDRRbJxl1G4o9/dAfbcDZVPX +gn7P3988/wC0+0Zamd8oDkP0+93um04xx82vOP5meVvKoMV7OZ7+lVsLejy+3PcKg/1j8q4dF2Tm +1G4HDDvP6O9c2qhj25l47r/mfzt+ak6aXovlxrmO3f1I47SF7meOuxLzAURT32UZ1/Z/ZcNNdEkn +n3fJ1OfUnJzDFvNXl7z35Lu/8P8AmCG60uQoLhbNpaxMsg+2nps0TVpQkHqKHcZseEXdbtFsbySH +Yqu9ST0/T5H068uFTx5UpWnjir2HyZ+T/wCfGr+U9O1/yreSS6VdKzWkEOo+iQI5HRlMcjxoPjjI +pXKMmmxT+qMT7wGcckhyJCOudA/5yq0IfvtM1G4VDuscNvqFadqwidj07HMXJ2Tpp84D4bfc2x1W +QdUvl/Oj8y9CmEPmHQ0iPQpc209pKT1/aNP+FzCyezunly4o/H9bbHX5Bzop1pv/ADkboslBqWkX +FsfG3dJx8/j9HNfl9mZfwTB94r9bkR7RHUMv0r82/wAvtSoserx28ndLoNb0/wBlIFT7mzWZuxdT +D+HiHlv9nP7HIhrMcutMst7i3uIlmt5Umib7MkbBlPyIqM1s8coGpAg+bkxkDuFTIJdirsVdirsV +dirH/PXm608q+XZ9Umo8391ZwH/dk7A8V+Qpyb2GZ/Z2iOoyiP8AD19zRqMwxxvq+cfL9n+Yf19/ +Omi29ytzYytfnU41CgPyLOyhqCTqeSqDt1FM7+WoxYyIGQBOwDoxjlIE0+1/yK/O7S/zJ0IpP6dp +5nsVA1LT1OzrsPrEAO5jYncdVOx/ZJyGt6jirsVdirsVdirsVdirsVfO/wDzlT+dh8vaa/kfQJ6a +7qUf+5S4jPxWtrINoxTpJMD8wm/7SnFWA/k3+W48v6eNZ1OL/c1ep8EbDe3hbfhQ9Hbq3h08a8V2 +52n4svCgfRHn5n9Q/HR3Gi03COI8yl/5qfm5LYTt5d8sP6mqM3pXd3GOZiY7elFStZa9T+z0+10v +7I7G4gMmUbdI/pP6mGr1demPzZX+UH/OJcl6I/MP5lNKZJj6sehB2EjV35XkoPKp68FNfFuq51wF +OqfT2j6Jo+i2Een6RZQafYxf3dtbRrFGPfigAqe5xVj35mflh5Y/MLy++k61CBKgLWGoIB69tKf2 +o2PY0HJejD6CFXwV+Z35WeaPy715tL1qHlbyFmsNRjB9C4jBoGU/st/Mh3X5UJVYdirsVfb3/OHX +mKPUfyrfSS9Z9EvpovTrUiK4/wBIRvYM7yD6MVe7YqsmhhniaKaNZYnFHjcBlI8CDtirDde/JX8q +Ne5HUvK1g0j15zQRC1lJPcyW/pOT9OKvMfMn/OF/5eXwZ9D1K+0aY/ZRit3AP9g/CT/krirzTVv+ +cTvzh8tSPdeVNVh1EDoLS4exuWp4rIVj/wCSpyGTHGYqQBHmmMiNwxq58/fnT5ImW382aVMYgeIO +oWzRch0pHcRhUfp1+LNVn7C02TcDhPl+rk5UNbkj1tlGgf8AOQHlS94x6rBNpUx6uR68P/BIOf8A +wmaPUezmWO+MiX2H9X2uZj7QifqFPRNK1vR9Wg9fTL2G9iHVoHV6V7NQ7H2OaTPpsmI1OJi5sMkZ +cjaNyhm7FXYqlGq+VNC1fULe91S2F69opW2hn+OFCxqzekfhLGg3avTbMzDrsuKBhA8N8yOfz/U0 +zwRlKzumyqqqFUAKBQKNgAO2YhJJttp84edta0nyl+Y0Gu+Qr/0NQtH9W4WAfuI5wfiRSDxdJBUO +lOPUd6D0PsqWc4R4w36d5Hm6HUiAn6H2P+TH5xaN+ZXlwXcIW11u0ATVdM5VMbnpJHXcxP8Asnt0 +PTNk470PFXYq7FXYq7FXYqwf84fzP078uvJtxrU/GXUJawaTZMf765YbVA34IPic+G3UjFXyR+U/ +lPUvNnmK589+ZXa65XDzRPKB/pF2Wq0h7cIz0AFK7D7NM5/tztLwo+HA+uXPyH6z+OjnaLT8R4jy +DOPzf89t5Y8v+hZScdX1HlHbEdY0A/eS/MVovufbNJ2J2f4+TikPRD7T3fr/AGubrM/BGhzKf/8A +OK/5HQWtjb/mF5ltxLqV3+90K2mBPoxHpdMD1kk6x+C/F1O3dukfTGKuxV2KpL5v8neXfN+hz6J5 +gs0vLCffi2zxuPsyROPiR17EfqxV8N/nR/zj/wCZfy5umvYeep+VpXpb6mq/FFyPwx3Kj7Ddg32W +7UO2KvKcVeu/84z/AJoQeRvPwi1KX0tC11Vs7+RjRIpA1YJ29kZipJ6KxPbFX3sCCKjcHocVbxV2 +KuxV2Kqc9vBcQvBcRrNDIOMkUihlYHsVNQcVeX+cP+cZ/wAovM3OQ6QNIvH/AOPrSmFsQf8AjDRo +D/yLrirw/wA0f84fef8AQZ21DyRrKal6dTHEWNhejwVH5GJvmXT5ZGURIURYSCRyYf8A8rL/ADW8 +jXo03zjpUslK8Y7+JreVlXasU6rxdf8AKo3zzT6rsHBk3j6D5cvl+qnLx62cee7P/LX5zeSdbKxS +XJ0y7bb0byiKT/kygmP5VIPtnO6rsLPi3iOOPlz+X6rc/HrYS57FnSsrKGUhlIqCNwRmmIINFywW +8CWLebfLnmTzCG0+PVV0jRm2n+rK0lzOpG6s7FFjXtRa17nembXRavBp/VwmeTz2A93P5uLmxTnt +dRSjR/yO8g6cVea2l1GVTUPdyEiv+pH6aEfMHL83tBqJ/TUfcP12whocY57sS80+XfMH5YeaLfz3 +5JdorSKStxbAExxBz8UUigjlbydP8n58Tm97H7WGccE/7wf7L9vf8/dhavS8BsfT9z6x/Kf81NB/ +MbyzHq2nEQXsVI9U0xmDSW03genJHpVHpuPAggb1wmbYq7FXYq7FVK6ure0tprq5lWG2gRpZ5nIV +ERByZmJ2AAFTir4W89eZtV/PD81xHas8Xlyw5RWXb0bJGHqTsDt6s7U/4Vei1zE12rjp8Rmfh5lt +w4jOVB7Zp2n2enWMFjZxiG1tkWKGMdAqig655xmyyyTM5G5F6CEREUOTxPS9Gb81/wA/YNJlLNo1 +tMUuKbUsrEky0I6es9QD25jPQ+zNL4OCMevM+8/inQ6nJxzJfdcUUUUSRRIscUahY41AVVVRQAAb +AAZntC/FXYq7FXYqo3dnaXtrLaXkKXFrOpjnglUOjowoVZWqCD74q+T/AM7f+cTri0a48wfl7E09 +pvJdeX6lpY+5NqTu6/8AFZ+Ifs16BV8xyRyRSNHIpSRCVdGBDBgaEEHoRiqLv9b1nUEjS/v7m7SF +VjhWeV5QiIOKqocmgUbADFU/8k/mp588l38N1oOrzwxREcrCR2ktJFH7MkDHgRTaoow7EYq/Qb8v +POFv5y8laR5mt4/RXUoBI8NeXpyqxjlQNtULIjCuKsixV2KuxV2KuxVB6rpGlavZSWGq2cF/ZS7S +W1zGssbfNHBGKvD/AD5/zh75B1r1Lny1PL5cvmqREtbizY/8YnYOlT/K9B/LirxDWPy7/Pr8pmea +GKW90OI8nuLOt5ZcQakvERzhHixVfnmJqdDhzj1xvz6/Ntx5pw5FNvKv/OQWi3fCDzDbNp0/Q3UI +aWAmnUqKyJv2+L55zWr9nJDfEeLyPP58vudhi7QB2kKepWGo6fqNst1YXMd1bP8AZmhcOp+lSc57 +LhnjPDMGJ83YRmJCwbROVMlk0MU8LwzIJIZVKSRsKqysKEEHqCMlCZiQRsQggEUXiepWHmf8m/OM +PnDyiS+jSH07i3erxhHYFrafuY2oOD9QadwCe77J7UGojwy2yD7fN0mq0xxmx9L7C/Lr8wvL/n3y +zBr+iyExSfBc2z/3tvOAC8Ug8RXY9CNxm5cRk+KuxV2Kvm7/AJzA/NOTTNHg8haVKRf6ugn1ZkJ5 +JacqJDt3mdTyH8op0bFUg/KjyOvlfy2n1iMDVr8LNfsaVXb4Ia/8Vg7/AOVXOB7Z1/j5aH0R5fpL +vNJg4I2eZZRr1/8Ao/Q9Rv8A/lktZp/+RUZf+Ga7SwE8sInkZAfa35ZVEnyYp/zg/o0Ump+atccV +mghtbKJu/Gd3ll/GBM9PecfWeKuxV2KuxV2KuxV2KvOfPf5Aflj521UatrGmtHqRFJ7m0kMDTdKG +Xjs7CmzUr+GKsb/6FD/Jv/lmvv8ApLb+mKu/6FD/ACb/AOWa+/6S2/pir0/yZ5Q0byf5as/LmirI +mmWPqfV1lcyOPWleZ6sevxyHFU7xV2KuxV2KuxV2KuxV2KvMfzC/5x1/LLzr6lzcaf8AovVn3/Se +ncYJGbrWSOhikr3LLy9xir5080f846/nH+XVzJqnlK6k1nT1NTLpwYXHFenrWR58/kvMZTmwQyx4 +ZgSDKEzE2DSH8r/85ABZRZea7IwSoeD3lup+FgaH1YT8Qp34/wDA5zes9nBzwn4H9B/X83Y4u0Ok +w9b0nWdK1e0W80y7iu7ZukkTBgD4Hup9jvnM59PkxS4ZgxLsYZIyFg2q31jaX9pNZ3kKz2s6lJoX +FVZT2ORxZZY5CUTUgmURIUeTxy2svzN/KLzbcaj5Eil1DS9RRkNuIZLqMqDVUnij35Rk/A+3z3YZ +3Wg7YxZYXOQhMc7NfK/wHS59JKMthYZVB/zlL+eWlMZNc8owTWiEmRzaXlsaClaS83jp/sTmxx6r +FM1GUZe4guPLHIcwQ9C8jf8AOYH5ea7NFaa9bzeW7uUhRLMwns+RNADOgVl+bxhR3OXsHulvcW9z +BHcW0qTW8yh4Zo2Do6MKqysKggjoRir849U/MZtX/M6688azZnUTNdNcxWTSekFVPhtk5cZPhhVV +FKb0yjU4pZMZjE8JPVnjkIyBItnP/Qyn/fuf9Pv/AF4zm/8AQx/tn+x/487D+Uv6P2/sQWuf85A/ +pXRNQ0z9A+j9etprb1vrfLh60ZTlx9Fa05VpXLcHs74eSM+O+Eg/T3f5zGev4okcPPz/AGPU/wDn +B7UUbTvNmmkgPFNaXCjuRIsqH7vTH350zrn1DirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV +dirsVdirsVdirBPzB/JP8uvPivJremKmpFaJqtofQul2oKuopJTsJFYYq+afOP8AzjN+afkK7fWP +JF7LrNjGeX+iVjvVUb0ktqlZh/qcq/yjK8uKGSPDIAjzZRkYmwl/lf8AP1opf0f5vsmgnjb05LyB +CCrA0PqwH4lI78f+BzmtZ7OA74T8D+g/r+bsMPaHSfzet6TrOlavZreaZdR3ds3SSJgwB8D3B9jv +nMZ9PkxS4ZgxLsoZIyFg2jMpZsJ87flR5Z8zxSTLCthqxBKX0Kgcm/4uQUEg9/te+bjQds5cBAke +KHcf0H8BxM+kjPlsWPfkJ+aPmL8t/PS+QfNEjHQbycWyo7FktbiZh6U8LH/dMpYcxsN+WxBr3OHN +HLATibiXSzgYmjzfWP8AyrzyB/1LOlf9INt/zRlrF3/KvPIH/Us6V/0g23/NGKu/5V55A/6lnSv+ +kG2/5oxVHaV5Z8uaRJJJpOlWenySgLK9rbxQMyg1AYxqtRiqZYq7FXYq7FXYq7FXYq7FXYq7FXYq +7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqwT8xvyU/L/AM/xFtbsBHqQXjFq1pSG6XsKuARIB2EisB2x +V856t/ziZ+bHl/VpT5M1qO4sZhtcpcPYT0B2SVFJBp4hj8hleTFCYqQEh5i2UZGPI0of9C+f85Nf +9XeT/uLS/wDNWUfkNP8A6nD/AEo/Uz8ef84/N3/Qvn/OTX/V3k/7i0v/ADVj+Q0/+pw/0o/Uvjz/ +AJx+aX3n/OK/576ldpcalLBdTgKguLi/MzqoNQAzVagqTTMjHijAVECI8tmEpEmybf/Z + + + + + + + image/svg+xml + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eJzdffle8sqy6H0B3gFUFGQwEyEBB2YHUEFwwJlJRJlkWGuv88d59lvVSUgICWmQ75x1716/7aed +Tnd1dXXN1fF6iuVQsjmot0J8mHG7vN70qFWbDEYxN2l1n3e70/FkhE2+G7+bZcMMdEqeS29qx7vW +aNwZ9GPkEXmYw7d951e565vTrN/t80NbpTPptqB1Mug1apPw+K+2X5sLXs7UJvAwciAfMKKbZWJ8 +1J28hOepwbTf7PTbqcF/YPyo6OYZzi3AU0GKwuOzzk1rbO4TjrK8jB3DnAy/CLwYluBNQYInDL6V +GTSmvVZ/UhwNGq3xOD3oDkbjmDv9T63vvqy14UnNXW11u4O/3alurfHtgtVG3nKdbgsW1qtN3FFc +ZfKcfyOv3o7hHXgdf8fm6Nt5D1rKrckEoIKBESXpy2reOB9Aqv7ne7pptTsEw4CIF78ycqXVG3YB +KWRRPCCFl0XtX7UHwEOehqJsmJdlGfAmhiMy9BMlPiwwjAC/RMgj5Q193a2/Oq2/Y+6rQb+lLC45 +mpQ7/9XCqRg3xzBK68202xrd9jsTWASHTbKy4stBs9VVm8i7uW6NLJT8x+o/lQ6V2qjdmsBODbrT +CaEUSZvhator1P5pjfQJroetfmVwR+ALiUJYFMWIWxQY5Rc2HHFLouyOMoA6ScEgC8tUp2TJtKwy +No6E42gTRHHvi7Az16NOu9OPsYLoDnHYint2Ouo09S2Lcm5J+UHWEZYM/5e1/ysAw9onk1Zf2eZs +v5ke9BDJY6Re2Ng+7Hp30FaezX4nT2C66VCBlfz9BvtRHHX6CIPrijyR3ordKTw6HQ2mw/P+x8Dl +U05lEScd9a/78MunOzWajj/dlcGgC6dtroP6SBkFH44mxt5L54C+9uPrA601drrW7Xbao9rws9Ow +Gt7i+Wweu3eXTgjbNGrpY5A/Z/8ufbPcIKi0gnL+0WxwizeWz/BPrz7odsY9fWBDi/67E0XARnVb +/eZ4Nozypw5YofOX1rh8sEzrA1idYWtJa7b/V6s7GBrQOGup9Zvu+9poaDcsQvfR6TcBK+VpZ9LS +N3rQGyIDd5c/a0NsXuipnBA4PcbzEQotPzgrvyArT5ARTv7ptsaug3x/8Hef/OGOuXxPgJLatDt5 +8bsPrmq9ljvoOih3gEm3tC6M+9rFqDzwG367cWn8MO/SuCLjfvgH/riAX76g6W+34L50P70w7ia0 +Pty4kIE9NF0HxRoA54673AcwLfxLAIQV6eA5rrFY6wI7axEginWXnbhBkMauhdZiY/bGt+XTYmoG +gjbTKvgtwHBGpC6skHRYZyNZRnmkHBsc5v+ozTCQqdFmcBVWTV6CclJzed8OtL9hr/GvTgOxURv9 +o/z9cFm4ArlI/vBtN9W+QC3lCQzedvv+0+v2oUMIf/SBgvxAQt436+d/1bpTtYPsPjiHOeceT/4Z +qk8PkqNRzQqCXmtSawLgvweAXQ+Av2qjTq3eRT1o/G8A4n8dhv9JLMT1Po3PTrc5avXVPiayNXQE +mTXq1KcTBDRIHgUX1xIb15Dn4ZH4H95Y6iXNQ4zvOIPp2+2P3xpg5wx6cZvOBpi5/9lt0NawuB3k +QewvuuUBHY7/rYvDNQRpyHFNKoC1A7leEYQ44areIeYk++9DlXEVi8TQHTS+W03n9fXB6vv3rU2D +/k9SwQq84N98WCiRNL/28cff/2sScNztNP6/EH9kIeXBdNRoEa/Tv3JN8yD/4wjizFN2cNOqdf81 +pP6PpcBzXM3MAfjvWs1/rFbzd6c5+XRcEScyYVbk2H/ZilTgF1f12eq0P53VbVYSwgLL/9uWpUG/ +uK76YALqYaH1MVEciM4rdB+kBoN/z9IWF/AvEbYgm/4fl7WbEzgbAt7ggMAWRsVd8pxl3TM/BnFA +uwu1fntaa7fcxcFwOjSRLnmhOGqNW6O/Wu5K6z8Td7bZmdTqnW5norJoMRLhI7MJZHdtNKkPaqOm +u4HBAjfrHmmKnWPP9qilrdexb31GGRFO4CT7rpwOgGNPAwCOfesLQnyx2zzp4vPJqNYfD2uwr41/ +YLpO0z3u/Fdrtk0a2mX3sDZsjeBhb9olfjdNWjMax8RO19PJcDpx39TGk9ao81+ko1sPtajgRebe +uWyNPx3eYOb2X6Mldwd61SYtWHmL2EhLO3/3QaUfAHBtdAOrx/3pstXsTHuGCV8MJ9+KPNX4CqCC +kOHEbbB/TEdCIxfAvIr4qIb55rATNkFb63bGpqZebfytolnUMDasNXWzJHnuTk4ngxn2tP1nDAeM +cX/MQB6RfqG/Wo0JkEy91q31G4t7PfcKYKzb6bfcEzhrdD3Hk9HgWzv7rE3nRrczBJJE581/4Dy0 +AW0Obwy1Uz/4qzUaooN0xl4ANY3BqNlqLm6D++BqMJl7vCrvcRhOp5YDne8djJqjcVhx4JgV74Vu +tX5/MJmtXdnlhU4aHsbjeQ662HHabzh0AXkHJ6ZJdQSML/9nGNYlpdXo0GEwbE4dOoydRmgM5tmY +qQOSzvIOgz6QyEShw6VzqT112iasyaonMOJ5lsQzNj1H5p7RiHXHueNnufNDZd+X7zp0AjY038/A +lc1dP2vN1qi1fLwuiyezNlnaCXA3Ia6bpX16eGzHRkZu1a/fagPj/2v5YPUOnsF5CWYGvPVXq2s/ +yEd/Eh5P6+MlC8Muze5w9DGY8RcrKlO69UDbUbUDS3S3e9/hXm30PR58fIQVdZe6+0jX+yl6TwZD +6r5d0LhnCLDpDPyh1TRDTdHdADVF7xnUFH3noF7ce+xLNJx6bbSMuLHfyBA9dOg6BGHQ6X8MnGYe +GVZi3YUsRO0T5iK2C262PlCKGsxZa2ZMOn8N6hNMZHLsqIiij0532RHDjmMMdjr0mZMfVr0ao2Z4 +Ahq5ppFZnSDsM240+ssOo9Jn2G38Y9BrFvGmdKt1W+G/KPt9LiE77DUYtbWxlvZRx7Fi8NhlOBh3 +lhMZ9oL9Hn4ORv+lcraoXb/BqIO5YA4DdkfhmYJUx3Sx5X01WTkcTJYcG+ypMztrOgNadFAPsEe9 +M+nVhmYRadebrKI2Vl6i6DpYTuGzfnXVW7qsY7M17rT7TugeDkdhYkItoxbs9AlMbNxaxhtJt7/p +uhndQksGc2Qi0Enfs2iUDwuWjAm6dTCJcE4cROSIU3eDOGClsLVsmnWeSQNWdOqqC4OozNl1NeJI +ZG27GZBkxaewS1NJC1nCFqGTs7Y/nnTVXsNh035G7KbOOOtnPyB0wZPZtfLxL/RF2m+N5lyCS6dX ++muGgiHlyGoGEL/dFjGVdJM4PnPZYAJRUuvsRpuKyryyO504WW3icNZHoA6Oxi0cbWS/YOw5/u4M +gVv2v504HCoEcNzbluu7GNQxvcywOt0TA52yxbL72mS8zvlP1D4FtKIxexGz2IiPa6kHRX3rdFRr +ooAgbyk+FTtDZPaO4jc4uFP8ASk7f4AKumrfV3RrybZP2c4HoHRLo/WfVq3/G6P1T+ORwRGWuGFY +o9eqP9D9Be5On7gcUCpbuWwWqc/3ZEg3d69B/1Z2Cq6hmMm9pYmN1TG6Lq3IU+uueT0NEKHrE8BI +14aKA7TTWmKyaOOcItbg6FQ+p716v9bpLpGD2juYtwz/5pZKV61zDojqvlXHd5yhIQncmcHffSWR +J9/pNw0kTvuamdI5zkols3mZpMcn64O/dFtu+atp3arV4V2+0/NvlaY1fc+5iOOEmFtf1r17yzZ3 +VPtndWzOv7UaMuffXQWX+ObKqDS9tAIm8U16RF4O+oPG52jQa1mh09r5s+xdM1KFpRuCI9gjVaCa +2xK1y4+i8gJIHudDXhl1epfoUXDuCvydsich9tRSA37GDQEl50sNc51vEiUGQajMwnN2Jrh5efct +BzeM9sI1UdtzgHhA39+D0XdhpqKu9l7KyU1k++bNuqBWlrphtNdS6MAoLPcdzfW9cTBR5jqvAIMR +Q8voWQG4019iAWtds716q3meThdHxILUpOjSU16e1hGNg/7kBo1EZ3hmqh+FCFW0m4ohNkelHi0Z +C54rmtKVIdNmKbLNL17W/rNED6UaodO31Ulp3lf01JTJb079OmqdqtKp6JyrD6Hqt2WH0ILD6xVj +LM1R4Us2RoN6baLUjc3MDuihrmqmdppNDtkc3hrW+pp7XJOx5btTJGGFmCcLHjv1cWHQqC3OAA/J +wVGsCJWm9GcAXqOju/4NM2b7jYEerxX0B6TUQufSM00eHpHyHKRdOBANi+daheLik2L7Y7HxoWZO +LcDpu53GDKz4ojmgF77M12Lgjik1Griz2jMX2UljC5oYyXL6/FyKZGDcJlbteAPHYmgnMfY/bGXy +F42PnL/EJRM/qVefcHL9fhy955lmvBXz9smf8fPx4CP3Xpju5TyBJ8bUFji5qx8wXHcSSd5UcpVE +bPgii49i79HlPQy95wZkMJgvPk6Wp7e+ZL/eHqvvHP/0kvn77PZodFzrn3bvvuqp98tSMhnssy/x +E/ZOymw3p9lM+uz5hQwVOD4aeoUxv1MKnHxOeAKIy0sBygqAHNWTweHVRSIvj4+ls8P7cG7wKNy5 +vNnR8yOTecxVK7mj5FHDCp7jof9wCBOchdLcztF7JjxN3Cajz29VsTpki7nd0kNXna+R3M18DP1s +snIxmeptLq/Smn/wT2Cci2kmfP15OBoJmQ7DiVvDxN1eeUfpzjLFWs4/2a1lgy9XBykxyG2p47wP +EqNRfFwBeIPnDBv6iunIiqdu0i2XdyzlJnfc6+B7Vyy19gMRT9p/LRyWYpXA0Y34OXphxodhviBz +geNTz64w5saXAM2dFD4YS6eC9BP/gj/9fqa5W83MT/o8erl8LpFJgcbmp4V3o6+R2Plr2HLS152r +gu2kYid/6rWa1OUdjQ49vtGY9Y6s1jqWiuyzsMXF9q0mHe8FL0M2k0Y+fbW9apZM6vIurFXwPwcO +uXbJctKt3KuwfTvsFqwmZXKpfMJqUpcXphW3d/oj/5E1goXqK5P7uCpbT3rqOdxlL94qlpOennEV +Mime/UUEc4/HlXcyKbufrGfnd/V+9Dw9LuCk8cU99VX5py7rh0lDQX1SmEUhpQKTUtda3NszTRqJ +9N6GdpO+jV4++xWbSRM1MZrbYV1e07QqKZ2839hNerbD++LP1pMeel7G25+tG9OkwGGUaUtp//HP +Tq9gNWkg3o0d20wa+dw/eUxcW08qVKtMTmaugMas1rqVa0d3bnrctdWkTO7lJWczqbjt/e5fpk2T +wizatDXmNPh+Zz3pKZPca/miVUv0TraDJ+qk1ZDPhN6TK+Ho2aWcVTb7/J2bW+vjIVOIhlic9HBh +0rPWQLyphTiYlAmZV1p4eqyZJiWzqGuNfjdzr3aTZpjL/RfZetLzn1jia3R1YzlpOb7Hw6m0Xqu4 +nW+VecZm0qcQU37zb1lPmj9rXT09+n36pC6vYdq7vX7bdtLyZ+m9bjfpKXOXHx5aTw== + + + WuC9Lu9tLnF4ZLnWu+HFlu2kd2+nWxO7Sa+Z+5N8Rp8U1mKY9vI4+/ZaenmxnPTl+vvcdtKvaqSZ +s5n0Gbgl8zLuhqzXevU17F3LEm856dt5qG876chbDnj0SVGKGc/qLZPt9C4sJ5WuQluexEsoC5Py +YfOhmb5F39RJ67zfdGhcXv9jobZPpuX2jn1n82stMO/7sSROemyaFIb9+tGYvnhgnnQ82D1SJ52c +BGBf5tfqea49+ZVJU1X2fJ4VBkfj22MPTppY5EnnIW2lh6xpUsBY/GxLVKY9YfMhEysMDoUrRdLw +O7F0fn7SndGoVu/jpCnzSkfJ1kCj3hTQmFmUR75iqqQ5iZXCJgRvDVrvFUWmtmpv4jxIZ7e7r4OY +1VMikSNn1RLbu7N7+5M5e/dObZ8C683s2jyFHdgNpL0qt2RaX62o6bkosW8a3ONvyfy0/7n1YPs0 +WjyPPetPF3Zf4vZv3m3flj5rr3u2T5Pc7mPD6qmqwxQC/RPO9u1C/fojbvv0eqtRP7N5Kp3tnh3e +jjWMfez9yKa3bwMdTT39YLdi5qf1i3Lf9uldJvA90p8uYOzeWz/w2L59/5yJHdk+ffe+RnesnqoY ++5oUh2e2b3/fcamS7dPed+741e4poKoUj8wwtvj8ghOOH2yfNvr1csHu6a5n9/x53x5ju9nkZb1l ++/YFd7LF2j1Nergdf8wWY5EzJnu0r6065oubznSgUhqfqE/T4UPT08r76X7S+FQI3iBDKSnGXDq0 +nwbdcjJ8fUm3Pyvo1EseHctnO0hZ9z7VWj5pxGzMvvFD4u7jtpysVLz3hEUlK5dNIVsbPXkDqcH4 +Sm8Du7I2etwjfC7GSp4rwsw8+/k46wlmbu49wbvXsif41qx4fE/+Kf5WBBL8TntC+bfIolFYbSdL +fFkCqNMBsE4H3+JOVP5AS3yf82h25YuUe5s81xLxIbuVuQhsR7Sl7faSg8wrkOm2vMXtHRWPM639 +rJecOzRnnjQsWvdzKT3R2pKX9yT9jmPpp6pjPzDD6js333o/l9e257730DNwHFHcpl0L2GLRG/8L +xYg7fT7+RtHPe925rFGsRdxGod6gGHHvvB5ua/22e7n0x4V0cHnRisKf+9vJ6GOXV2xkPwjHj0OF +Tpgx101Wkv0ccxER9hWyQfcHWMsRThe84lZVuMw+Nn4+DjpHdb/4KBbOVLs5ujuaCeB0cvBz60cO +s7glft/JU3c5eGhLv9AAt5WrhY1eBVvwmFz+sGgCz3I3hKvMuxVwhFvq4FXfqMA73RFpgDstbT8a +dH478KSzOWKxxV31ZjlwQGPK1l7l72jAy2ZvczPcZZLl4PcODFCqHnS2Y8G5CQKHZhqLGUBh9yKv +mY9KhkeQBVzaob5SNnjLhvRJR1M+zVBMCjr//LREO15z0kBsMMnipEOCFoabJj7Tn8Kbui+gah4P +M9lGsSJqbsX2NNuoth6UNo2P5zPnzSPQlHLTbjReui6ib5GbPb3B38AI/5bPAergdy59EiuTbTdY +FuPA8XF2D6At7yOMYbLq46GvOVZdNfMORmWlbW83ebt9hFoBs5Usdz2jXFa6OVAHvWr8BI6LuwOY +BYWZOPGxp+qLO82MojYDZKmDz1bGq/wAOriHwYqiam3BfLMtIcvIoJMhN7+MjMGrQJbhNfzAmWPv +P8WYQbTOgfezEnDkVC4Fr86fWYFnAdy+LXC4FhW8MQ14hEIJVaojXkh2y53q42m7b7tg+HGjLFfx +3VgsF4yrwvLlulbZjb2tNUlF5ckLu3Fa7CERt/EgbStcR7wgauyddCyf3hbBctr1kh/c3glzjoCc +z4YqaZyvKELnpwzsCxhId5T7S0F8A3Y/9ZVjWDnyleATj6jB7fpmvosK04Rd9Xq1H8K+eiCJy2Au +AhF7H43rsE3xEC0CXXSn7fT55zcI1LVxFYWoJz/++oDoCORSj/IF+i3nULgSAi042o0VR5udympw +aMYyM3xNr8fRsgjNqY4RVSJb4+Q0v4sz31jufvb5emLaq8jwQC6a9oqwd5fXlsHPjXjnoRhR/VF7 +yCCCzmx3/zXL78Tzhbm92t6z3KtWMbyr7osFxk5ipcvNYCwToNzJXZfKD615w2sWHQX3Jvm6Okgu +LwIVpgXKASSwWatWIFnISic8MU4gDQJHugpBWIFyXi6WgJcOPy3F2K6uihhPL3FeamC6vBbnt7xE +I6lzCyLf+fSSfbE8vzkrxcpi43Xd6omMqAbW5sZzeZURT3zZPBUpGYTMpWzNI2G5CmOenTqiw5jO +nU+yVv3mUG2giNrWJbcci3he5mhCXzq8PTmdLX2ojy1VdvcuTyvPX02GTT23M+Gb26Ae7iczw1C3 +I50nqbLSSiYtV2PnRnwYL5dxLu8cITrrWd/SZHW9zeVdOuJ0M5rgTIp9yx6qEY/q+/o5sKJa7HyK +3v0LM082SXYa82JuXz63N70v8s6m90Wmsm5W2RdppMhSJ5UGjVCCVFXtOrXhtM1TXWt1eZeqXTRM +St3u07uB7eYAT17nGN4tCJmlqHR5nY/hiK3t7J39BpUmHQaQSafBroLK+hilmKOWvbJhfmsSgzN7 +n2BnckxlXNKpsWe6GutAY7pqb6lscKmHT7PSaYUMl8HosN79yQmVNbn0aJowdkLFPuiM5zPdeP4t +xqpbu5vB2PGYjvXMrKlFDV3RYYAcTsv9lSxHW5BWtpGtzQYEqTpcCSQlwmsNFBVfoQDpbUR19uct +bDulun1moVQv8Y/NLOxyfD70dKMNe+hLRl89Ye5lXE+lP6Nnw0w+/5PSgjVk0q9zprlXyxJkuLz0 +RjjJFrIg55dx34EBuLwWODmzwcnX+Yp7pQHnMqd5auBNHNSSleSey8u9TLzUJGBlZpuWu2hk0/iU +bHdjEtijBc5FsxuYg3C7qgfIaN3M8eQTX2ZixSDWJ75PbhZ7XUUu2nD58+UuNKOmREvOq7vQiAZr +YyAiAokT7TcIJAxu5k9WtY97eyP8hL1YMGcoXWiWnt4LkxNtTe8LvxPz7ZC9Aj7m7ESjON0wYtgs +m/XxFnbf8XT3LlZ3odlgbN6JtjbG5B9m8bys46/qXVC40Fy0QPG/caGpUR4FKDsn2sp4iphAAilG +QfNzvpI5igezgcI561qmOqgpJ9eGIOJJrDixCyLmkc6zlB5FZ/89UOD2SttkoOR52hnmTT4um2NB +ZTKTUwkjvkxTeZqDhj+WSxX+5DbmM+0V6JbWrnT/LuECdhjzbwpjFnJ4HcI+ufXyixHedRgAgPSx +9/NgC9JcJNERKPNpowXJZO8jUAvi1tYba61Pz+2fxypiZUtZ1j5vC1MfyWc7btLQdT72ULY9uusE +3k6LPTb7Mj416fxrBHPSALdg1o+s+RitzEXCPqDWLubtdEtZiTAe0YTWHE4voo0/uatc0u2+E9r8 +PmcPPM25I7Sx4M2jXd+8hwRWuPwQ0x5h3ES/brj9msb8C4FxC4pw0UpfMiJtlM48noHGLGIfrz9L +Ylen5T6toHN5KUQd7n7lN+GmmY08B+MqLNPrwDJxPDgvjrFCpxEtnBqob/p1Xcflndd20sARYpTH +giJ95OGWGCmEJ//2bIy/HRjcjJJpIyPAAFZXeHAtNioPwugQIaTkTrd4XjZhqyBIgUX/prpIrLBb +gaVgrk1w9fXNPIomwlj0TK4lX+4GxFzZEI0FFnmN0S9AMiHnA8eOfBPR5hjlmQsbu+hNF8SibeAY +xZL9hilnf6WIRxoenI9W2jU7fzLAvWn75eFuo1kEAKHJ8WCVRUB3crLPz2YHqi3aXN5l5A7bvREj +BuULUMeiH3HN9Vkn8Gj1lSvwvjsr7+HaNLZW7p21WCYV3DiiTbThN7EGZGEBIpZdXqtMZmAuBUqc +0IhlktO7Ce8hws3ScRh6sfz8s5JYdjl4IhBG4ddiWeEw9xsSywBSyEos28Qslh++2tblatlzth4S +WN+mxPLzD3KYTfgRCbJsxbKuw6wqlq3yuezEMkpkLcePTiy/jZaI5ZiPnlvSiGUA7snJWG9Nbg8s +rSS7wOrDg0Vm/9JsRf1sl+O2PPlttHG5/7BZuf82WkHu2/mr5rdb3KgljnlDNLa9YTw7Xx9usrMj +ZEY7NA4/Jb7vfPZpMl2tvH5c6qGaJF4/l3cxMv9Q3azXD/OUdL+fDcGumEqKKoSZ9VhERqhSSY1k +kXr43lq+k2pkhNqtCIDSOpUNbkXAmE1oGGGcmAP/zoqMJYtSzn6VXiLTkG59bFKvl2baL0tRQtwd +OKrXKuXMPL3OZz/18OboZFhMTCdYnJf7qtjSdYpUrPMafEiWgw+D5E2/nk+FpMH1Ap5md2iZb8vi +xm1PqF96c3mxejrrCSX5V0/oQs6qhdMXctUT/Pyq4I8wtuWw1DpN6q3xBonDZPm795Ft3J80cC1Z +rMisZmrl40LOPzpD9+rOXi7zHdrCKmpB1ZSUAuviWaWYDYz5XV3Sikdf9fNsPZRYLLa9OrnR92pJ +v+IBM1f3at+zFCtS9BtN38Mhq34u72LPGEsz81bu1XLExX7NA7q1bOW+KGce8lRrwQuyIlQjnoZk +Qz+7StnROBYYG+4hsS/kFR4+C7P7CewKeQ+3fLaFvBizMGwJG6YAL3AY7904Aidub/+I02c98m5f +Z/xEV6PdcyggV28GY3KDSoUCvN1C67PpXATdMgEH1qtlfftV6YECd0z26umUYmNd3rnCf3vwSmsX +Ru8fHRhojLoeG6ux4ytN6vIa1o/3Da0zqeAvKpOqroXCQyKW89ZUGJRYktgflhOHnckDjn2bDew3 +w8lr9uQe2qZbJhVrSTgmNx052vu6OWCvwVsZTcXdnl0aRkbX6hwyh/cpyygX6hnnbT9DJNEYMLWv +Gl1wp9AAZ2clFXcXsOxcqGILHHKYhYKt3yTwZywMroy5sn4Fk6u4R12XQ1fDu6gTrkYq8xHe4u6A +LkccmB5dyXJghXxLGuKjrUyZVwjtq74tAtoOCCT5lsuqvqkSkp0QiLOoKFyIBv7igJgSy5ZU2C1B +oBVHqwb7dhwtq9wISmGp0nG0atCSMa/ugX/Nru6gUSq57OLtMOJGEsu03c8+Vx3sXCofZTVkmYq3 +DsZWj8lYZ9pjIIXWs+NYCQ1HeANRUQSJMvJOA5RTloS9V8geY/YRiOX2sNXpVXZfO791bmB3fnO/ +Ob0WOSR1jio9nDqMUue3qHMUKUq3csSvZ3Xq1PtgLTyFzj7mWo62It5lsXTHCh7QtxM2FTzNU72C +Z3nNCH3NqWNRhjqUGk2gKpSkUist9TbLvL5Tytt6qEs5qXXL+XNgX1tsWcA4x5NpzIu5fVmUPr/e +F+dLgFbdFzqVdaV9obsJaJVqZXPYCj0kv7mcCauVnSuZXKsdQ0rl1YzK2W3zVoeG7k4gClSqOgwi +0zH6uCoq9Zqm5TWJKxvmk+MfKzGo172aBeH6hvnk2CwG7Whsptpb00H7bCWTY3anyg== + + + wu6fUTK4oKJdvHOv77ZHU8OYhfm8tvE8OTGL+bUxRjK1N4IxOtaj62N2xDDGU7kop5dYjvYgrWwj +2wK1GBCkAsngUZwBRcVXKEDSbbblZ3/ewrYJUGKQaSFlxto/ZrKwna65GX7YX3PzMm6bkxhcXmcj +fEkRNOU2uSjLZH/MVudKwJki71/ny1NNV1U/YTxUc2jvhXOq3+JeJj77GxbsfUpLarRXYS5OudZY +tLyOB8jCulF48vlyBrH6ck1aiOXtQBZycUnJMoU8cy0e8SUIdDjitgi01GDPraozV0TgnO7h8qrV +mezF2M4Iv/i1C22+sp7NL5gf69X1Utw/ttLphhGdXWgu+urMnRhrvuJgreonwJhDSRZ9JTR93oVT +2TF1FcbyGw6xEnoThV0uLwL1+xxUFSQbF9oKt50Y9+9iungVwpJct1m2rVOe1oN9EBFLQilreaj8 +9/k1k5mNGRFaES1Lk51EfRXCyW10b+Gum5XuEzVV5tpehbBaLU9+zTxmK4x5Hd2vlJXQs6sQDBHe +Ncuzac6ai648+5cpzKq9j0DRJIFSpoDmrSNWK2bSWSYuF3u2GjreBV17o6rGpzu4WFMaNitda90S +gHCvFLYy1FjZlomWf51XvFApvJHbgBFtiSnt7juhjTZ25aIqX/5tPZGijZNiY+p7ih0rvReM7LVp +bKWbEVzOhb44In2Zr6U0V2ZZLPT9avlsv4zANHePaOr4dEE3k/u2NxB8tRwvTHYMNyla3wxGjr7e +XB/Rtn4dxjugvrfHdkSr+vWRla4zu90US1QXfFhWx4IqfQTFpCFErHGY9eqMaS9ypo6MYLVrYbqm +wmNTKbwYa1mzuJffzK1NpHh5wb9pnWlPU77sfAWU7fpUzq+XLzuWKlBXeoc3R2M2gWNT2NgQ5XHO +08eqb6c6TMtUApRijiUmoK8cH9sEjkmBqpPPwbVCxOO0Olxz18z+5PRK30Ogqv/Y++E2mUWQts3d +mcsioDo51eGiR3Gte+HSK30UwbF4+W5Aa/E5rW9Td0Mpld4L3sO1acz5ywj0lgwZj13re0mOYnkS +WHYXNGiFmxTLYCjSaxfLqeieQizPqjgpxXJt640yZ84olpdVCm9GLAPaIpu5S40ULy8Ry6abJ53K +l9cUywseElK+vCGxXNu63NDNkwRZTmIZ7f3VKj8BbSuLZWqPYnFZPhcpZF0ill1GK4pKLL+NVq/7 +tF7LfC3watmKVnWfCzwZetZpvlpEL/djPmGjch8g/N6hlfsUt35jzfcKdZ8UlvjbiD6J16buU/X1 +kfp+2spP6rrPvaM71vnsr5DpOuf1q49NXr85ywJLWTfo9eNSD82p453DK6aSYuG3zXUFhsiI/YUF +NmRRH9O6Falvn6tu+va5+pjarUhzXUE6HF3h7NOVLK9834X9Jq96XQHF2Qfq/+11BarcV8SWzhuO +xRKfvBkkaqluNN87DDCZV4tjMftQ9eInqX03O1GsmC5jxXTB5fWEpNCb5Yeq9ytJcc0PVZs+U+3y +/pEPVZs+U004/+Y/VG3q5/L+kQ9Vmz5TvXwta3+o2nYtG/1Qtekz1XgTxR/4ULXVl743/6FqE3Au +7x/5UHVg/jPVsJY/8aFqE3DEB7v5D1WbPlOt1b5t+EPVps9UY5baH/hQtWlS/Dr2H/hQNcJg+Ez1 +zKuw2Q9Vz0sfowd+kx+qXjVz2KGM0uZD1RaRRINm88sPVdsBZ7aSaD9UTVtAPqar4V3vQ9Wm8Rbv +6nQ0uWg+VL1SDS/dh6qdSUVZC/WHqqlLluc/U22fb0lLfJYfqnbOt/zth6otEGj7pe8Vk8eXIxDv +7KK6LJP2gAysvrFjVWFHjcAF14nTLQEOZu+y8uwNR3lsyrMp/HYLX5Te4P15enE27dcZnDJLlxdn +r+KDtSnPXgdjm7ky11Sc/ZuoqFqevYGsG4rybAqQyL78rjybojjbDmNrOq1sirO1L0pv8nKFxeJs +my8ZrVKeTZ2j+KvybAqPmm2Uh748m6I4Wz+Va5dnU3A527XQl2dTf7X8V+XZFptoLs6212FWGNGx +dtFKiq1Ynm2gWrvi7CX7QluevcK+/KI82xKL87GGjeyLo/a74l2dVuXZv/8CO015NoWyoX+Vae3y +bIribGM04beV7vbF2evdaj5Xnk2BSrv85BXKsymKs1eOii6WZ1OUGlvl9a1Ynk1RnO1wLxxNeTZF +cfZ81s1a5dlrY2zFCkrH4myn80JRdkwhpV2UQK1YfWkCaZZtu3559iJIFnnjvy7Ppqys/2V5tnNx +tjHXGtfaMicOUq/U6uad2bezre7oNn49m75Wfdm3s6m+J05dNm7HmEw1VrTgrfjtbKdM+818O1uX +yNZfz157N+a+nU19k96vvp1tpcEuq6OmUmkWlmv7bfRPuotcHAvRSRU1sffX8Out9u1sy7s6qRFI +p8jQ36vwuVAZR319CahDBj9//s9VjNvc1LrhinEKL9wGKsYXa0X/RMW4PcY2WTG+gRsOKSrGaW84 +/F3FuDHn6s9VjNN80eD3FeMuh4SjzVSML2YQ/YmK8VmFnWWx8aYqxvVK4SjlQVunYnzdb9itVjG+ +HGObqhjH/OT1a6doK8bNsdc/UzFuWVm/8Ypx+rsIflMxPl+V9qcqxpfljm6uYnyluwfXrhi3/VrW +RivGN1OX5FQxvkJd0i8qxhdy4P9IxfgGaIyiYtxFL31/UTE+R2N/rGJ8lW/Wr18xbvPN+g1XjJMb +QTna6Oq6FeMu7+I3zzdfMb6hGiuHinEDJdOXpq1cMa74+uxUnk1VjCvaBb8ptNmUpq1e97pOxbi1 +72LTFeObo7HFWPbi3YMrlqatWDHuWsl0Wbdi3Hxn15+pGLe/qXWTFeOz6qc96vvX1qgY/81dN/QV +4y6KD17/XtnQvlpOUdLyi4pxu+/ybLZifDmNLVaMr1rfPXc7kOVHHzZXMY7f4LbKl95sxbjyjdTf +524trxg3c5g/UzHucvZEbKBifMZh2C3ar5KuUTG+5t1QK1aML/GQbLBiHLTxWc34H6kYJ2LZ/gsg +m6wY178AssK3gleuGKeIjGygYtzCSvoDFeNk923LiTdVMW6oeqZ1WK9RMb7eDYerVoyvZImvXTFu +cUfEH6gYJxVDd5v8NtKc10+tGHd5rT9xv9mKcVjLrGb8z1WMW0ZGNl4xrkdGqN2Ka1SMk7w+20/c +b6pifHb20+Hon6sYd8i031DF+Er3j61dMW5z/9jyivFFPC2pGMfacPwG95+qDtdrw+Hs/7HqcL0f +YuxPVYfr/VzeP1cdvnwtm6oO1/u5vH+uOlwvtl380vfmqsP12nC9amDz1eE6cHNfYd5wdThdZf1v +q8NNlfV/qDrcsbJ+I9Xhepk26Px/rDpcrw3X5MufqA5XRUK3PQWM/bHqcF0xVK2kP1IdbpcDv9nq +cJMO84eqw80+pT9THb65L+Utqw5fo15sjerwpV8v3Vh1uF4bvkZOL3V1uL5cy69mbKg63CKj+w9U +h1tmdG+8OlwPYzvUWfyqOtzKStpYdbiGO6s6iz9QHa4jw+VIT+tXhy/U8P6R6nC6/LHfVocbswj+ +XHX4YtXzn6gOX5ajuLnqcKcI72aqw/XacIds219Vh9thbLPV4XptOE3m8LrV4TbZgxuuDtdlvVrN +8Ueqw3VEm+5V2Gh1uJ6M5PL+uepw27VstDpcrw0309gmq8PtdZhNVodbSbHNV4cv2ZcNVofrteGb +3Re7T3f/Yl9W+HT3Eovv19Xh+mZb6Pwbqw7XNxtj4n+qOtypinMz1eFzttgfqw6nuleB+SgIz0tR +afAbqAs3xpENCRIur5Yi0WvZf8A39fC6+gdAz23PfvtsU4W8lLdq6NLeUsOD9X1TfQH4nXtFz1Wn ++MA6kFLq4cd8K/ZKpZwLGFuxlNMOY7T3XThjbPHm3xXu7Jpbmq0JvxJIJJL4RpWO5Py9dFtmZZ/X +Z4unFYtI56xXE1Br3OJmF+giX2Cnrfek0PlxpQu5O7jSOZ3fwlK31/RPfJmRnaZ/brLTf/0V5uxC +GZoN56er9l3L6Wh7EziAt2AU/8bpCON5V/gyi6PT8dzW6bg8D9Z+N/ZWOjQOVtL5eldSLqust0gJ ++90nwGcC0eXdxM0Jnwvp7fMItPkO7xIELknctkWgrQ6DxetOyWHUCMRZsHjdyZ5QcWchIS0yRsft +JbVv/I48pKlsoPPB9i6sdn+NrMCLjX/172KzXzQALrFwsNcrfdbzvX+LMccP/tH5LbF6ekPfSL0g +Gd+/zxsnIFmkelvd1EqBJ0c/03zKnlLLY5eihcXrv/w86sw7Olfsxp9UJhkb79Iwv2aWt7UPlj+5 +DZhvgFlnm2IlLSrqlFNHK95jJftjtsaN0/nVE7xtMXbk3wjGPFq92C8TqfMUSWR0X/xEoH6T222I +8eWtfBtr4skUNVoe5XFS8rF0nSYd0LV6gcZwScIClsZaHNy1b5zGuBB1kY/L61RTTCdzl0vcue8j +n73HzO6W9S+KwNJ17fQaIrxrfWm39kZzwYOLshSbSvDakrj+FWasW9/EbRFK1fr8EV73Vg1StR7c +HI2tKn2tZK9uv5AR69Q0tkLyeLmPPNnGj4iOHqoybmdBp+9+uW97HdLqYWyEcF9nmY66pWFEuzIP +MuLSa3VcXooRLWtT95ORI7simFtLD8n6sVAQkxTizUVzNgDuHA3x2TruF+ssgAHQ3j1DVZu6nywu +3j24VvnY3WB5berc7juWYu+vaT6Z/MmEOjZTm4rl15qz9LfyBavWnWtTaWksvpAgYXYiLctAsPlm +vQXfNDtCKArlZoFxwpO1ezoAj/u2yaJs9jlNYxRa+Rws13K30lVZSyuKKb9dSx01A/N3o3fcIYyr +3fdg/33ku5Xuy1peiK1V1/76kga7FCVLi8/5S+freB+svl+JdetrXZplhSyrexStXKSUH8PEEe3C +H1Y0tsKVEbWt0xM7sXyvi2UHbkkplp9/lovlFeRLbeuBxqtpJZZt7iGB3ac9dzRimdS91jfgc0G0 +2Yrlxd13/Hg3taavHz1Lnny/MbGM5dcgljdwi6ZStb5ULK9EY5eOFzzYiOVl36zfnFhe8Chi2DVo +K5YBxlsa08wklu3X8uAslqkrV3Ur6W3kqFgtfPPcuX6d27TcfxttWO7HfLQXSnjnIolLyuv7v7xq +cd4Sf1jBEaKOaIs7kxuEourZWUxi/brz2aepYLfw+r15lnn96oaPECy3xOm8fh/psIONiH4YmysX +lpU0fzpduDCfgeuiuNOg+mu34sI9ilY5Sb9wK1Yt8y3Xvn2YlK7TS2S6z50fUKvXWi2PjdiyTC1a +48KF+bNfpfHzr2JN6kIL8y0LQthebL2M2w7Xg6nLWPZdiHnr9TZ2YDsfRcTcdr7ZjTqqxxHxWKhk +6weNXrLfCA2Ske700iQDyFSpbktkEoff18+5/d1rjbk0kruZj6GfTebvHwuzfQYrSQ== + + + r7xXT5G5+/uV3l3vrNRqqTgpXKe6kodNX92XWrnD7HMY1nfvy/lLXDLxk3r1YWWKYg7MWk8aORyx +mIhXSlOsjz6TQp7dafY+de+ZP1zFTCNWa2Yb79niaMSNk5799qh0EPLGdvn7y0gyIgbGn+cHg2nN +5d07a0ny/snTzuOW53zi9yRzNwdb1VcxtlO+3854v/vlwl7rcxgSy4Wft+h3s9BOfL9ffeZbZVm+ +ej77uSuzw/xH+bPU6d6eHlxO795O/b63t3TA/1WNfF33ioH4h8s7DDwmJiNv2bc3GvE7nq1Ba3Dg +ZXY+4/7HQuMuIAcufcc/O71CjHu/OUoyh4dbo9FJrOjZfzm99HCp624g3hASTI45OWZylfscc7o9 +uGJOr68+R6POSWg0/TwGbjneC17Vw3xBjiXL2+09UkIO63vOhqKF3S8pepLOp7rRUoAUb8NKKxWA +odUMWFy4YJHnoOyky2t55YK/05U9ga5Qwul5nXRjXG2vlDgMnQlKQfte6ufGGsddVih/3u78jBJ9 +8crl9dxf5QMOKJq+h3f2d70PxR0p3k15i+XTi7338vmRIO9eirCqlCdZOaifYIF8JXB0ELpH4KKJ ++MtrN3Ph852Qr2NXS0z2Lvo2Grcvthmu9LydjL4kWpnz3slDIjZ8OTR58oFHXv5kg9I4LBcvSp3Z +6TXQOciFYlo/FC6vdixeLnXxznDdyWH6U7yuwl6NX5OwF/dz4zx5rcY2433/JTf1BD+/HvC+i7vX +rCd4ef/g8YVqW3jLxQX+iHtCSb7oCR3svuI2HXtC/dKzJ9AIy4iOSPKm8ryXrTPxKBk7es8zTXLz +Bfkt3notfgL3vffBqrYP8Tbgs4+XTGs0CiZ7g8IYVnX/g23NXDrl2Up8bSe76U7tgGWY8ftupn79 +dpWUgsNBIHv3cAadH7ZgvsA7ov+ceztrBhn2ddub6l2/SLl0clpLDMdsXfvi57HYCGYbje39RPzi +G459Pj8hRhPDTSfbidF2c5QKSd0rht3zt5Plxt40WdqX7pKDQbiAiuqOWM3384nYdfor+x5qTxKH +J4I3kyrcNXAtUZc39/52PUxWMo8ckHvwKPPBVPFaD5kla8U/3xOHomeEx8I/a+sd+hOFcqrb3wvh +YLe4k5HU90/Mmyold8SUMAk85XyDdzbzEXq6Jpz/XQCiak5wnGrm4+U+muomujyOSOJBnvTnU2Ma +uwzwT0gHeQI8kFfoIpe9vJNynuBNLvfmf8qLk+FrEfbqJAiAeHynk7dhHVb1ICBPvpgGEvGCB+/V +3d/O5PNnUXh7Ozw3jm9P25zMCBc+zqVboaeUEOn7CQloe/rgTX+Gzptn22FvVN9nPPvv51sAfI8N +ZILeac53dn4eu0pNnzLn54ldvS3e5qrfqbPb/o8yHzethmCRzS29i/gofj0lbz7ao8zHwc5btrH9 +fUKmgrUAAP5c9uc5ltv3BwPiduyhnG0ED0Nzi7ziYNJUn7iOdBhT35c3B0AHzyO8iyBNeFvmI3Uv +5TKRRw4IqVrIZTk/6DAGdHDbb98JPnXwpfzJtqMDgDa9bVhQ/vF0YAD+KPdzRnYNtuSplBhNc3tA +DNU7hvdsSZnw9WeGbCLoYx+9+E6y5Km2YRnRi8zF+3gX5N3RC8MFW92UmLj/IfKVYQfVLrCmh0ny +mg2V1c1pDktkf9UuqXorcfdxew8Mda+QKlVPGYIMQmMfsIM3vXLu+azQSl6nU9VM/txzt/CgzVyQ +033kCwXfxWrhNpF7be1UM+fdwFXmYzA6VhgXHy/vptqPpbv4x7SYRsJ9hrOPh1PRrggLm3U68k25 +99PDoa+We/fHMhhuu1ZhfNr/zgY6jxNxO339tgCS2JzAvpzswMjnfTwvd2Qtzy9HL/nsQaYd8tWT +khgN5zLVShDQFn1iuEk8KRe97Yf4RapxnzsZnxZz73fVJChTFQGPVCpZDu1cpi+P+mmVWzQK41Sp +dH2bavSnr+q+SP32M5+tlx5QF8yHxRPx4TSdvREmKgnkq9108z54n/kojd+QCd/msuxoJxOs/eAK +HkOZg9rhbqzd37sFuCK+TGt8Iqi48wgyUYJAt+S5wiT3FryLoRpbMpoNVr4prnAPxBmXcP+KtDOD +fLGYe+WZc+nPaF710CJHAz6dq6AElMXbI7wVMHm7G38nnfAukXt85yLV9VYq6XZ77AG4LyUET8o2 +Iu/+6F23cKqfl+hd6akCP3q13OHp0ylhZqQtdtWOd2KdV+kDHrCwFvVAAgCZVmfwdOo9ff7K1ivZ +iD4YcLTUrfhQG96R7UbiLGdaD35+9ud76ps78mhC5GwPr35pprrhmwBYQS9+QmPZXaB5IZvztU5e +koPP633VAtMfNL+esrVaRD6Wz7mBgtnLs7vvxOi0B/qD/7WUCd80OPJ0XudPI59+gbW0PytvoFsn +jyx02ZjBLQ5MagtY6+1OIj7oVVPXk9o43op5+4oFdj4efOTeC9M9PUtAb5vd1nj80yNX1YDiGN2L +7Q44ARTjn7vEwWQ/E5AjwiH8dniROBgPT+DB5UVmWitew4Ojk5y8c9NMv1xsnxEFK3oX2rrMvV/u +RlWn4zAJVtL8Mb0GkLlptvF4RITjDUixwxGezw7uwTnoAjxPfBd44ivkyEXjjYeQqV82sC9+iEdf +9fNsPZQ4V7I7duIfZweV5DUzvERCO1dE+bTH12HPfyrkphhyKxdwgYc0UWjhnAs+YKg/DaJsHIbe +cwP99i5JHhx8JMuT4TSZf/RLsBYkP0PrTf/7OVvf3X87O3rm7xV1+PrH30hdT/d8yMzOMh+vW99n +2zenYeDyz/d4MdBPznfx5QO0DBh1qGmJUCAcXCZ44PKysOXHMJjMAQNsbwMf37uA/f3ZBUWm6kve +dIf72cb3jwwaUDKEU3WIiILDEHwE9p/YIlRCrhc68t8HOgQtZLnECsTPI4XVbFv29DWMBS/ZrVw2 +44+aepqdFWyl4wOQmE9QjbY+U6FYZkdhlLMfYBS2PDDAzVaydNX6grMvtKeNeRLA+wKkQeZi/2kH +hmgNDVpfIwVSG9EvC29PN4fBx8xzLhMUAwb5eXJzeJ/z7b50svVyfpoNFMJKpXDKm8mn3vncc3On +TXYf21hgAPEJKMHXHrmbazzHW7f+TqrxszVSucXO1jjnL3ZigePo3Vn6/Tu1n2m9Tx9zb5PPbiKW +89ZmD7ZcXmApL8cKD3kU28+gj+UN2gxYP2I0l+kGFUGIBzslTvKTo9xb98CTiGXjxaQcz4e03Q0O +QPdoT3PJXiyo3X7Uvp73Wq9iNhivwQvtJMb+h61M/uLxPc3e9sY5z345pD/AL0tefLRhzyPRlBhk +H3DEi9Q3W2tkcs+X7Wzg8/URT+A78TilO1/iAehM99PoiD/rpAqPgwc4i7FnEOSfO8Ajr4RE7Opb +Tl0Xhx1QaWK78tfI8+TyqqSksLAPJtXtDUPK0dUV7MdGixmNjg6wyzZoLh95llC8PphisOxGBvr1 +WFWQcjdbOX+1OAEau06HjomCiRufRMM9hBYKo8O9ON8ooHuzjnzdeAfw2fYkfpLbicRhsNkkl34R +X8H2jzi9dHkNyuTifEXDDVx2OFHuvkrvnGRtVwq7r6z1kbuwWqvFzNuOKx3sm1ZK/DBWawXb3pva +ySxdaYl2pWBX8udPPaPF0GyXTLhlcw+n7MztlIpx9YMO2mw1NCC+iDwj/EB7UM+bpB2efXgeb3u8 +X9lG9Z1LdS8OemAWHkfQ45jCjwsUcu+xgZDzyaI/GwoMhrqNddAJxvAsPsqJ+5J3y/Ld1w+s5AJG +ko/GOunMJ+G1KPJ9yDyD8Y/g6DHdvLu/VC3e42wQDTcZTIlTwGKd2cl8fJT4mR3rz4TD58E4c/Lh +gS4Hr7psdnnl67v2de71eaAIodgV030Tpzf8Pdj7500Ypxgh3hC8bxF08OHTa/K6cPaIqn0p984l +IwqNHcZ7Nwx3dj3NnE6irVQkE7rTnwLG4o3rS5BExQFIIrkGnLgxVuAhasD0nTXc9mcECeSnPUhP +I5CzM5BA5zcDlRmXbYEKxLu3V6uDhLtf3O3ZAKXcI3lli6f52wqvwIZ6207ExqEBbvxZNnA1qIBh +cwUWX7LYK7dz6cZNGv0+j3jkuHTnKBWwnPSmx71qzuLMOHMR2N9OCfdcCk1PJlke98tAVAfTufUl +eCLF/FOQKhcPZNq5Sf3PhnNnNAA7JQWpuWSmYYlyBanntQcyKdiVxmljrPVOT45/aHd6YVKuXTJE +RgxoxS8bF3S0Lq7PFqkwaeJ+v0ZAgmMWfCVP0T+mPce7Sp8VoEr78Wcr8rICCXSh70s7kF4JSGhZ +2AGFmDABpT/FOzhfaEGC86IDdVKZXC0Hak2QXN6VgNJBwrinn5YdWZz9ZQxpTXZEfH3V4UrUbzpy +6oGc5J4tJ8XjiDT2ctFWVNq5jajaHMiX2rJzDgfO4pRjnpI6LF7IKh/j7jcsJn3Qd9+O5LoWrOcE +bwS6ACm2ucNne/RA66Mlqo90OLQmm4Hd/xMCBUjqc6qTlI0UW0JUR5z1sMVne0rV1rJkWDs+XXz7 +oT0AQGP00LbGtsMyuZR4bQuryzveyrUZm2EHnvWPK8A6O654e7bNgV31uL63bY8rruX0LLVMLFf7 +NsM23myHBQR9GbgAcpiFYV/smMuLDdWW+9YnrCAc6XFkI0fgT24LBQt0vNpsXXVgqSkFC4G4znDI +rebKBAkuVWXPLSZozaLHX9n6wQfGNkqvROlW3kD/fOr7MjQBdf/5PtvYFncUV/j1q3hMwhHogVfM +WX6XuInU3y4+vtEPc8MGvqN7DJtP9k4fTo7qSeb4oqc5Z6Gtuu9ppD+fUt6Z77+L8ZwfLb7kF4l3 +BTO7NL8QrL8bRhfNceajUsJgzuN++uPos4iGu+KwP6yL8X7q23fzEW/tiUXcqyd84wIMlrO+4myC +P6VZXOgZzDrf9uyrGaL/Z2YbfQEXzDJKqIfkTighsVz1VQkVsPnvSS7BXNwCHsKP6p97mXsllKV4 +/LEtefotkKgRWuJq3OgcgxhcsnJ3GwC6ewkYbRowOQ5Pu8fVbH1wFkoWm/GmIVjF+/vbYMfd7mTO +u7e3JscnCQShlcTEo5pDNzhGk8urhE8OQ/et1Hfha6T4B+eCObXk7e7hMWz33fViQOm+q9u2JCKF +33ycj0lxZ+mhQiL5armv7fTkSwmu5E9+BD3yg9vZSXde3zit7TEOW7LvnwWw9mHNzD5Sss/vM8TF +Lr62f8haovf8Yc04PYnHPo2F1PfeaU4z631eY6ArffyilmvNXmPzVZT775feLkasRMXi67xe9IHG +RiLG6bbk72QW0ObnZzHTBw8cvvxohp2JEj0ddRp+DdZ7KfHA+MPan7tg7+d8ecaLzulLOHK1XYUY +mrvBfG6fO+a0bXyU0Oru5N738i9wAo+2SWQE3ZgZA0sBGkqA/f3QS1Yus3l9c1xezQ== + + + /+uVsVMcWj1fydIVaJkY6tHRMYsgyXgOCkrwCO24+QfFRLx3nDYEfV74bBdsZOKA04JCxEFV1UJG +4ihRTudvQA/xn5MoHjpBJQw4PGldUl9JqfH4kDl4KEWVcNosGAXblPlIxJ+TF4Qnm4GKYrToKNmv +196V84K35xzujVvtrG/rrp3JeT8u5gNKF+J9zz9IDobDekqYpkqmiJTCYT5zJ+Pnu8Dx9uE2vnMG +mA/siNssc39wWf5GHfVkB/Sj/SfVEfJ+fgyc6mRsiFx2vZWSIZQzH4QBi6/16v8A7SqHmTPbEvqg +B7MY0d3Z9s1RHInhEihmb0LcEYpD15wHqmVuzYIs1/qkuJa5aTc+Kbt3gRbfee27uBjb8l9IJVk6 +BS7YqpDAuPoOCcaNIoVUKB6PEe89MArvNF0YTK4RzKF2Srhp+nN6U8HARA45jMLCSGvvK1lJ336Q +ZcyOoV8JtxWfLp6zb8PmF9DiVkQbjASC2Ex2J/R5z3Ah3yRZDHUraswf/yxdle8wMuJvXish++/W +C4xTYGup7vd5WQvjA51PfOHLxfi++kDc/vGUT4c7hyL63bcQqYVovHF7Q4QWSfZAWQm41aJFC6I1 +1u6nSing0lupwmMpqbJWRbqeMBhFD6NQ68/kx2Gq+yzHCAx6nGYWf+metnOJduJZOQJkG9lU/dMU +0VHjNPL4hHwyQOmH9/wnfhKjQ+IdVN+VO57sXn+3BCCxTy6vOTfk+DTvI9GLROzB10lxX2E1Jq70 +C4Z7Sngk3SjHzCGTdGtag+mTLVX3eKgWsvVws4VWUqe9rTiWQDiO1SDN6O5UTZBIpTygMBwntFCP +b5cczePdcqwTOK4OFE1BCfoQl+xD7eeC8Omzo9c7hhAX0cf0AI/gzZxWE/Vk/8QzyJw/idtKyi1/ +6BOSkebpbeLno91XL/h6mISVQodOMBZAPWqQrNzv7GffxWHD0M/iTntxB4DK/aAH/gsTaUZqoHP2 +A0OQmon31MzWvc26iQQQTxktEUqVj2QtO3u5zHfw9Oj+I51guO3vj6z/tdYGPaMyzj62Jruq0Nv7 +8CYOI18dZMcvChcAZCW1lL5sH/NdSMpQEiQJxyditUI/9fl+iXZlJ/n8nq2XqjPZpgV97t+PK6nu +Vj9HojyyeOpvYDgGtbCHDHkQPbwX3zDtpYMh8lI2WPk4MMCFn6AA6v0Za7qlwj7k5Hvi8CwdxsHi +amhY3d9YshxMgLLRqUxIsgtqzJdaduT4v49dAsMKbiHKiu6Dm2m3NboeddqdvjvoirsOkucse9tv +DnKjVqvS+s8kM2hMe63+xB1zHyTL6fNzCfhrY9BsQXfvXDyn8aFnOuppV7QxX8sg/aPYe3R5SehT +zeES42PMMPWBrGuPF6LEx7X+affuq556vywlk8E++xI/Ye+kzHZzms2kz55ftCjq0FyxBKLIGZQV +AAHdPji8ukjk5fGxdHZ4H84NHgVgrNnR8yOTecxVK7mj5FHDCh6NISo6z9dKkfOZwFseORfqB6ks +d5YgQfPEx23lKnkmvl9RRc5Re1dVBEWT7/a30KR4UyLit6+5GvD8pytDogS2SZXn1H3qe1eaaCcE +j+VXp6bwx5kQ1APgbLDdPlaYkS9br3QOM/n84d68CVMAtb8XxByeoprIo6RHvEQTsWtfCznJKVE0 +NTWz9pT6zpwxKg9nMi8khIRhp/RTCxihvK9EX0h8ngTkhffQi/IlF9x4NVyvpRLqOVN6WyzPHhzo +gRwlf6ReOdxGsRp8ieTejpM1+4w5e6PncXAJyziVVaEU8vWUPKrKxVRWRYh8+ePyJkaNMFGkJ9lm +7LhnYbj00UwMn748+2LpVrl4OxtnlohmCAKWrYwjlzd2WSudKOaBvtP3l8NzAvycDXSHcfeTH2Nb +z7eX8zWeZ+zyBZTP4uOBIeqO6QioVPWOMDmvWwWB+CIbktfQqkp1Wyk20bseltQw3selrKf7YZxw +V8lHNybtFB7A0mC+CWtE5OfwEz6RgYRZmSJIxZNbEA3j4JzhkhxlznOPW9qfhwB3/uHDuKDGjxfU +ndeQJky07M3rglgiGYGouIeeMOVd+diaoOkWJGW20Mrkdv2lVOSuNNEyygw+EYwEHqN0vP/J+c4f +bmabc6mmuoYS6J5gdmD3MTdAswwq9Rk6QpqZ4XlDFfBmMauNPDg6U3TDxWS5C80weQ7PJeeh7UOU +Lu1oPj5ye8e+M2OXe/kj0w7kaxa2y2G2cfyyDasPPphAArFaehx2zGL+ZVwvgfWSDYPpIbfErdxb +CbpIrOZveX45er5KH+WSjXYDU+Su0vxR5ZjYuEoCByba6VYMWct9kFhBhgQ7dvBzBiRXJCSSydQu +Lm4T8ZPpZE4rP79Pd46OYwoTBu39OxEvT4qpkPQVSHXD7f1ksfFRQLjK5FNkzcfsY/MCY7Avx5ge +gihq+NKd8SFL6iTmy2FN18KkNPPg+9p2UnSmzE3760nF41vhKskcTscqHzt4BEpW0unZg+3xYeA2 +PlI6oXZE3lET4zBZbhZZVlMAX/NZ4xFXbKDgI1ZonKksDNtaA6EBp3LBelET8UwJduoJROvlOtXL +qZuMVsDgIvui/4kZna3UWWX7Ffc0jSkfQWK0n2DKSLo3S7W7RLfLQ7J/tv8zy+vSH1zMvvU10JBa +64FmXPlBsVVNxCs3Y+Xp3AUcqyfMcIUK+t0+QUQNt+OtyvsNCNtaz1yaUrmYTGdyXy9YUT7seDHF +xN3D0Yib7Hr8h1eCJ3Swd4cp9BnPTkY6nGXUn+CDC0/wZ3iND04wwb7pCV4+nHn2f7ZHsC+XLyYX +J70RtroJhqkzqxphq5tgcF5WNsJWN8HURJCVjLDVTTDtoodVjLDVTTDislnRCFvdBMOSjFWNsNVN +MCUpfzUjbHUTzOVdNMK0mljiifZXR6TqoZ87zDVZs0Kv2AbG4rezTMszDOIZaiTuKue1XFYqtZRy +39zsURv3PEw+5KoU3UrcbS5ZnmyVcZObWPNxrBTKGzKq2qXXWXrdrVVaHGoXmL71Vpm8ZANjfnfG +wpZngiUcM8Eau0ZbbHnWG36is7Q0E+yOIhNMS2jzTHzj5ZlgzPlzMKxFkpalCk7rxR/TSkHuO2e9 +2a50a+v45HTpSm/nPnW5NOuNyR5eZx1W2tqKHNiuVNt9ZRmpg0untMiz3TNjcdjJaSATDouPxGhQ +5JlS/aA+uCfSjhTkaPJOed7bAuMk86Vkus1y2fb53fgTO59e92Wwqy4i4bieJ2d61+VV3gbl9gvs +pdhuLlnqjWcxpQZ7+tpmQOa8jlDNO8/6b8/HimoL6ucNBu5bWAHYIlFivZgJU+7ec8kn1gPyI/WK +7lq9sH2+NsY+2sy1S7TRZhJrNiVOhUNrRZtfLpcHwNHe/2XGCgVIJPBgCMlerRnsXxLqrwZBVm48 +L6+2dXpuymExpM44pUYZP1JtMPvSy/PyDGlAq+TldW9WT3UgsnJZvgdGnH6dFApr2VwWFl6BPbYC +yeWlShb8ZQ6P8ZpCI1Cx4mTt/MVFkDBM+/vEorvBcna0/OwvMKQ12ZHp7L+8U1C/He2/2uaqkQJp +Mu062WpW59wqVw3k/m+z1Shy1VAik1yamC/4RzKyydGb7f5qRFW3T9SiSP/dlECZJ6kW8fasS1R2 +qU/vZkq1Wsvq+dPvS9M1HdOybaFtLMv/ah6YBjWnmD/ZDfvxG8wCT16WXLrmcU2Y8pXNa+ktT5dH +dvxjMWwSTbysDReoPitrsR8W7Zd31SF06x0ky4+eSYp7v/wislmpNrlKH7wSp/l83cmLfrOBegH6 +8rsNfn2zAWDM+W6DX99soAbpl99t8OubDVxeirsNAr+92YAU4TvdbfDrmw3UhLbldxv8+mYDl5fi +boNf32wANOZ8t8GvbzZweSnuNvj1zQbouXK82yDw25sNMC7meLfBr282wFJMx7sNAr+92QB23/lu +g1/fbABrcbrbAAxgh/sFHC81QHt/tQsVVptUuc9gVh6/eKPBBu8zUEvKrW402OB9Bvq1OAs3GgQ2 +d58BXryl3mhgIVoNlZsF081B0jDIjVPfmb3PmTC6RWob5fZ9/g7AkL4HWVHdJnC5lExV9Au9zF8K +EM+cN47u9J7Yj/jopGQ5+L1DHEuGywO2qsJl9rHpbRNXMuxVXxV0IPck5YqfWUq47+ygs1XcnaVN +vM2c5kRg9vQwgybU78d6W1LknzvJSJNrq36B0vM2iWjgqVRiGiUmt783muphDxQE7yAIvv2pXmC3 +qgyWPxmWofMgljn/+jnjMqGngDGW0mxfZAPj7G7685kPJpnjaZQQl8ur31iAQraRhR0ZAPqDUzWO +fFS8IUEYY1TiOBuaRSXUT3ZwwZEHyed7vp93ti/aB8qUKvGr3e/qLBSSnt3Nu6uFQuDgoslBrihY +JIHPJ4Z9ufxMRfziyJgRQfJFMOuWI/EJvK/sKdPKPuwBEvQ06fte7r0w4MFw2/pMdfKJseoO1Or9 +UUDf4+5Xoe00DIpKqJ3zfUivWP5fx5i4d7hvOGiRA7meaX1dHDKRev4ED1Ioze0cvWfC0/EDSJ/K +VHvABJOV7G5HzWzeO+NzvnxSVyH2E7HsAXDQVP1E0S0vSbRBi2Tc1xOVG6mMmaEldX+1AIia4Y4R +R/2GgMF/H7viQEbnLPuW7TeNuWQurxdayq3JdIgdIm+pVrvTL9T+aY1crFv5j4H/WDfnjspuLhLB +fxg3D/8v1F2+xqA7GI397kLf5X07SI4mmU5j0hn0a6N/3DFsergs3J5n3DH3rGvc7QNImDfoDE/8 +mLz2BtC9uRh3Ev7/8LdrfwpzZ+D3axcTZjg+4mbCLCcL8E+EkSWY+tvFqIDBC//AHxfwyxc0/e0W +3JfupxfG3cSxblwCK4R5GcAWInyYkeDtntIWjUKTFGZFRnRjQ1SGhQnRaDgiwUwCy4Yl8prEhwWZ +4dxpl8AIAI0IDyUAxM3LXDjKiCym74UlkHxuXpLDgihIbkFkw5gCBC/xUT4s8xzMIQphjmOjbj4i +hAWOg8lEeCREeTfPywCaQF7jZFguvMaL4QjHRsjgcoQX3TwnhMWIDFBH5TDLsvAaQC1HRAXGKBPF +11gmLIk8QBAVwxLDYCc2zETgF5wtKokiaYmwLC6fA0AkjrzHwioFVmljYd2kJRpRWxgYkrREohHS +wsusSN7jw1wEloC4EWVWgLXwAGaUcwOCw7LMwS+48IgIA0RkpQXekyPhKC/xSi+JgUHZCKBHgl8Y +QIYgSWRT+EhUIDsHC5ZwC8jOMazSxghKJwkXjNsr46YubHja9QETQn+YXMGxBLvWI22wgIjSJsJa +sUXgcBBsEQSlJSLwSgP8z91wKZ0krRMfdSsDCbOBRPfidA0AggGiBsTBS6zyROQEAgduiCiSpoiE +hAQtUVmQlRYetxGIjYmyCiCw3wIBZLEXjiSpI0UlZaS56RCO/Vvrw0pOoXb4wiyMzA== + + + IPBw1sNSVMLjx8Ay4fBxoFlGOaQlwDEflWRcBeCdY4AKeUEGJCBa4HiJER7pmwHqwpPCSHBSgKp5 +IBegdDwgAg/HgQFUzdoKpC0aZZR+oN5ESFtEwSTPiGExipvEc2FJ4pAykT/g8YWJGFw/0iXP424J +XAQQF4XXWJhbJCQjKQTCs3Bco4h3eJ8nYEELJ/HaYQfCBrA4EagPVyHKYZ6JwuBwklgBtrvqQtqO +EMKOIow8TAa/8BKLrUDRPJwVNxx2RAgH/Tl8EoFfkUphRs7NCZEwnGGBkIyMZMVxeKwFmCfKAb9D +YIBvhCWYi8DAsTATxwHIwK2gJRoWETwOliUzEraIsE+AKGA2wEMAoQilIMEpbbhYOHiiRHYJUCVE +ZDcL7AQISybwwiRzLSpXSLv0Njie5GQADAycDQ4OmKmN4QSln8wqsDLALqMRmBMQIUUA8xwTBUaJ +x59H/gHUzOGGMQJH4OJkgKLhQvgJqQMWYbGMRFCj8KIIo5x2DqkCuQ3wpGiEENQicRacaL6QUoQf +iEIi+kKhtYThZNBr1CZUwlDrSiUMiSB0t1eQeVGNBQq6zIuqMk/WZF5ElXkozRSZx2kyT9RlXkST +eTyReYwm83hN5kU1mcfrMo/VZJ64IPMiZpnHW8g8XpN5vCrzJE6Tebwm80RN5gGlqTJPVmUesBmT +zIOWBZkHbSaZhy3zMg9bFmQeYy/zhAWZJ1rIPEGTeZIq81hGk3lRTeYJusyLajJPUGVeVJN5gibz +zBuuyDxWE0K8LvNYTebxmsxjNZnHazKPVWUer8s8VpN5vCbzWE3m8ZrMM083k3mSrAkhXpN50KTK +PF6VedCiSjNek2aMJvP4mcxb7EVGktSRiMwzT4dwIHELMuKMCYtAKeRI8ApDR/TLEYmwYCaCHB4o +CTghS/YxCoyHsHxRIrsv4mmRCOETKQBDANkCBxeEqCInkZNGeZacLAGJFV8DkmR5RCD8AhySMEMg +OlwdSC+ZQTICBHKIUiAaEJ4CvofgImkRlVKAo4AaUwQXIIiEJHG/kGgiynuyQtx4bkVCLNBL5nBX +NNkrgFgBquHIsiMRWSRwskyUSGNAU1RAARVl8OiwbgVxiE0CCse7F1CZduKvU3K2EYG8DP/KcHh6 +5NzKHApWra1gbOPIlhQMby62zN77mDXCsQ3LUZCehglmbQVjG6yPFwTDeJZNszfxPAE9inxkBgqS +kKjqBDPoDE36svQ3rdq0Vw1TzGAxzGGAz9CmL01/16pt9u4HbgfsuETYAHBYVuIJa4jgaRH1poJC +fUwkMtfGAwELeAIt20D3kGUydQQYRASp2dgGS5WRunhU6YncRtUZGR7IA4kTUFSwirAXBGQ/ArIR +hANoH4hRAJYbxbMCSiacF2xRtSzQhsKiGNUb4DXUcfA8651gX4Fjw2t8FMSOzJOWqERAEhXJogii +qBTR2mSi2oFKysC5I2/KyIyUNo5lUO+UUF7gaHjM4QVBANYuskaw1AZlNaAyCoZOEdT0WMJBQNuV +CVok5ABkwShj8DUQpiIyOgHPLbK+CAhTCTkPoJOXREXOwmAGpKM0jhJJqTaR/cLtZ/Dko/rECgo3 +4ySJJfsQAWUayBKMYxaPu9aCImzWBlwQeQiOBaQYlWW9DWQTTITyiVdsDpwO6JHVQUgrVpQEG2Ak +D2xTCVCWOA3QOTolupxiovCgFER5lKPA2qISCKqe0oaESJoiZAxg5qh5ALeXJNRCo7AIGeUbil8B +dWHoIQqMYWbEGlgZojxP5lFgqjzqOmAnSBLuN/RiCC9EWxwsFTJ6RJYUoSICTnAs2GdgOpLephyt +KIeUgnZTBAWKonQocImipFDB3IlMW59INLhA8qmWTZTQHEgunqwZ9CeOaNDA9QRiXcvAEQhUAop5 +BVeirFC0iDJc5MnuE9JWDrLEsaoNilZVgfAK2DhJkb6oguC7qABGFH4eYSKyijGOVfuBbCNNomrb +oZohs4Sjo56IG4jTMqgN4NaDwBNV8ECdB4YqabIJNxctzDuVBiReaSPDkhY8FKQFjaYFOklrigmA +LEWJwQ1oifJEakTQRANyB8qAHZIi2sZzgtrEaaudf1W1Mm5dstvndz/cu5ZJRK0Ph4pVhKiAcGii +HApGMKlQj5VUHQtW0HVxUcQpSm+wfjng5Ni28G4XQLCfkxMRUlRv0XCLAP5gLmAOIOUVNUqG0wHD +ossCSJNsBuEqXYtXHaYCWgazQVaEG2jvOFUE7UtBUa4icNpgWCBB0CZEhf3wUVzB4qvLp0IHGyIB +2WOEiaJURnKKsGhBC7hvcIi7hAswHE6vyZfu4qsOM0mCIkN5kEwysapwBKJO88hIQVTgqBJaNsjE +gXEC5ZAm05sOExGjAfmliLiAXcc1RZQTh2IRrG6ewI/ePpRT0I8YUV2Ldx3mQncZw0pEFBDeDHNx +ILPwBHBwGkEq4LCg0ooiylt4xrEcWejCq05TgeYqEi0BmJhEDhs6TDhUldHW59RhkVsSLhuBbZTJ +qsyvLp9J0WKRXDkZLVFclMAAGxNR7YU5QQ1EaJEfS0TX4HhFu+5avOswF4NMl0eGDxKDEYkvDHkC +4V3o5kEGDOPCwY2KaIKybDjKsjJpM7+7fC5V8KG4n3Nzwepwq2zcXBxrdnNxrIWbS1LdXGC8L7q5 +ZDRaoIlRLCvQQBn0I6C1CFsDz4jbDu0oBjUAaGPRVIA2dF2xqGihBSSJskpVHDrIOFSyZYJpURkL +IOTRT4WePEmUOGKBy+iDRGUClKYosbeiRNxBi4xuCvQIRlVvInE7RfQmlFUo/1nF18QSrzlxcxG5 +xqMfh7jQ0JeG+pQiHBUnEqpmcIyEKJxiOHthiZhbyAtZ0c0BouDoKwoFx0RY8hpOi2tDJicR4cYR +7xu6xMhxBV7LEZNZVjGM/jD8BRBEOBF6z2CkCApgVlaYUxQ9hRFeUxRgEjTUOaI/otNMa2kQlspw +6GDRezEKmnliVgLj5UBTg8MkkwWjcgrT8cAtRBRnuMXoSOOQIEgngB+1EHS+SRySmIiCNYLmKScg +USP5yKjqwUpA1spIh3iMiJwiXEIhSPTTRKNq2EB1jQqKaxTQBBhTXaOi5hrlFlyj7IJrFOYBjosi +JIKyBNaLvh5CKKCHMKiHKO5GpBiM4PACKuEcg5qbdkRBhSDvCSIwaWQGQjQCK4ZHgog8F6hRBnMD +34OHircJoBPRaObQlGAjCmcGUgYwQUkDBRNb0KJnRLKfwLii2AtMBOLcIr1ERlGcJVDHoQU1LZkl +DJUokQ0iNVEnBgg4Hl7n0HkUVZkzUfU5UMBFDq0HHmMVMiHfqKS4YdHyAWQAMmFfRRZ1woga2MGR +0Y0gYDhJIjhBB5PMysQw4XmwylAYK6cPNlpGjxi2sCKgXiAqJfH3gdmGDjTSiZFYluxcFIQPaYmi +3sfxsuo0gxYRthIxGVUIDJvIatHhSxyR8DqPh59DhiKyioEDFEe8wgxhqVGVj+PSgLMD61HCeBLL +oH8ZGST6LOAXYLKc+l6EHAwZNw5sKw4dh1FB8dezHJIOMk+Dnz1NfNBmP3tkwc8uWPjZuQU/u6h5 +1XnNzy7qPvWZn33WFtV96hHNzz7Xtuhn51H9jxBWSyw2JHJRwBYOYzLYMvOzEwPY7GfnzH52IAST +n52Z+dmB04DOAfwAqJwHBgK0gfChps6rMoSLomcN0QAnV0RhAu/JnGKdzNoKxjYeScDUJoaBAGA0 +9DAyRKRhZAnIEpQTVbrA+WAjxE7mkBGAugRtOABuGViRsEkCacHzzEWRVyBeAFCGJxYBgioJguIj +YDmF4cObPEGLwn70yAKrRBbSxrBX1Bz24qSFsBenh71ELezF6WEv0SLsFV0Ie0UWwl68Oeyl2DeE +wnlZIFSIZlhPDdDwircRaE5ws6CskOOD+8FEyYFCZiMpwWMiQ4AwgbEz6GuYtRWgDf2caCkAaxcx +ukDeRCUHcSZE8HDAoMSYAP01wiiEg5FscoCA3eHZQk4gS0TWgcxB1zOHngMGhS0GDnji58eRMPSA +yFdpGTkB8E0SH4oi3zGv1zHaaRX58RZr7VZlVOt0WyNXe1z7q+Wu9fuDSW3SGsITd3vUGk8Go5Z7 +/Dn4G1vgFa2715u9zrn+L1T7Dxc= + + + TM + \ No newline at end of file diff --git a/webapps/docs/META-INF/context.xml b/webapps/docs/META-INF/context.xml new file mode 100644 index 0000000..ae6803b --- /dev/null +++ b/webapps/docs/META-INF/context.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/webapps/docs/WEB-INF/jsp/403.jsp b/webapps/docs/WEB-INF/jsp/403.jsp new file mode 100644 index 0000000..e2dee6d --- /dev/null +++ b/webapps/docs/WEB-INF/jsp/403.jsp @@ -0,0 +1,44 @@ +<%-- + 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. +--%> +<%@ page session="false" trimDirectiveWhitespaces="true" %> + + + + 403 Access Denied + + + +

    403 Access Denied

    +

    + You are not authorized to view this page. +

    +

    + By default the documentation web application is only accessible from a browser + running on the same machine as Tomcat. If you wish to modify this + restriction, you'll need to edit the documentation web applications's + context.xml file. +

    + + + diff --git a/webapps/docs/WEB-INF/web.xml b/webapps/docs/WEB-INF/web.xml new file mode 100644 index 0000000..0731a2d --- /dev/null +++ b/webapps/docs/WEB-INF/web.xml @@ -0,0 +1,35 @@ + + + + + Tomcat Documentation + + Tomcat Documentation. + + + + 403 + /WEB-INF/jsp/403.jsp + + + \ No newline at end of file diff --git a/webapps/docs/aio.xml b/webapps/docs/aio.xml new file mode 100644 index 0000000..291a7f7 --- /dev/null +++ b/webapps/docs/aio.xml @@ -0,0 +1,87 @@ + + + +]> + + + &project; + + + Advanced IO and Tomcat + Remy Maucherat + + + + +
    + +
    + +
    + +

    + IMPORTANT NOTE: Usage of these features requires using the + HTTP connectors. The AJP connectors do not support them. +

    + +
    + +
    + +

    + When using HTTP connectors Tomcat supports using sendfile to send large + static files. These writes, as soon as the system load increases, will be performed + asynchronously in the most efficient way. Instead of sending a large response using + blocking writes, it is possible to write content to a static file, and write it + using a sendfile code. A caching valve could take advantage of this to cache the + response data in a file rather than store it in memory. Sendfile support is + available if the request attribute org.apache.tomcat.sendfile.support + is set to Boolean.TRUE. +

    + +

    + Any servlet can instruct Tomcat to perform a sendfile call by setting the appropriate + request attributes. It is also necessary to correctly set the content length + for the response. When using sendfile, it is best to ensure that neither the + request or response have been wrapped, since as the response body will be sent later + by the connector itself, it cannot be filtered. Other than setting the 3 needed + request attributes, the servlet should not send any response data, but it may use + any method which will result in modifying the response header (like setting cookies). +

    + +
      +
    • org.apache.tomcat.sendfile.filename: Canonical filename of the file which will be sent as + a String
    • +
    • org.apache.tomcat.sendfile.start: Start offset as a Long
    • +
    • org.apache.tomcat.sendfile.end: End offset as a Long
    • +
    +

    + In addition to setting these parameters it is necessary to set the content-length header. + Tomcat will not do that for you, since you may have already written data to the output stream. +

    + +

    + Note that the use of sendfile will disable any compression that Tomcat may + otherwise have performed on the response. +

    + +
    + + +
    diff --git a/webapps/docs/annotationapi/index.html b/webapps/docs/annotationapi/index.html new file mode 100644 index 0000000..2c633f5 --- /dev/null +++ b/webapps/docs/annotationapi/index.html @@ -0,0 +1,34 @@ + + + + + + API docs + + + + +The Annotation API Javadoc is not installed by default. Download and install +the "fulldocs" package to get it. + +You can also access the javadoc online in the Tomcat + + documentation bundle. + + + diff --git a/webapps/docs/api/index.html b/webapps/docs/api/index.html new file mode 100644 index 0000000..246dc78 --- /dev/null +++ b/webapps/docs/api/index.html @@ -0,0 +1,34 @@ + + + + + + API docs + + + + +Tomcat's internal javadoc is not installed by default. Download and install +the "fulldocs" package to get it. + +You can also access the javadoc online in the Tomcat + +documentation bundle. + + + diff --git a/webapps/docs/appdev/build.xml.txt b/webapps/docs/appdev/build.xml.txt new file mode 100644 index 0000000..a75afe1 --- /dev/null +++ b/webapps/docs/appdev/build.xml.txt @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/docs/appdev/deployment.xml b/webapps/docs/appdev/deployment.xml new file mode 100644 index 0000000..62aaa98 --- /dev/null +++ b/webapps/docs/appdev/deployment.xml @@ -0,0 +1,249 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Deployment + + + + +
    + +
    + +
    + +

    Before describing how to organize your source code directories, +it is useful to examine the runtime organization of a web application. +Prior to the Servlet API Specification, version 2.2, there was little +consistency between server platforms. However, servers that conform +to the 2.2 (or later) specification are required to accept a +Web Application Archive in a standard format, which is discussed +further below.

    + +

    A web application is defined as a hierarchy of directories and files +in a standard layout. Such a hierarchy can be accessed in its "unpacked" +form, where each directory and file exists in the filesystem separately, +or in a "packed" form known as a Web ARchive, or WAR file. The former format +is more useful during development, while the latter is used when you +distribute your application to be installed.

    + +

    The top-level directory of your web application hierarchy is also the +document root of your application. Here, you will place the HTML +files and JSP pages that comprise your application's user interface. When the +system administrator deploys your application into a particular server, they +assign a context path to your application (a later section +of this manual describes deployment on Tomcat). Thus, if the +system administrator assigns your application to the context path +/catalog, then a request URI referring to +/catalog/index.html will retrieve the index.html +file from your document root.

    + +
    + + +
    + +

    To facilitate creation of a Web Application Archive file in the required +format, it is convenient to arrange the "executable" files of your web +application (that is, the files that Tomcat actually uses when executing +your app) in the same organization as required by the WAR format itself. +To do this, you will end up with the following contents in your +application's "document root" directory:

    +
      +
    • *.html, *.jsp, etc. - The HTML and JSP pages, along + with other files that must be visible to the client browser (such as + JavaScript, stylesheet files, and images) for your application. + In larger applications you may choose to divide these files into + a subdirectory hierarchy, but for smaller apps, it is generally + much simpler to maintain only a single directory for these files. +

    • +
    • /WEB-INF/web.xml - The Web Application Deployment + Descriptor for your application. This is an XML file describing + the servlets and other components that make up your application, + along with any initialization parameters and container-managed + security constraints that you want the server to enforce for you. + This file is discussed in more detail in the following subsection. +

    • +
    • /WEB-INF/classes/ - This directory contains any Java + class files (and associated resources) required for your application, + including both servlet and non-servlet classes, that are not combined + into JAR files. If your classes are organized into Java packages, + you must reflect this in the directory hierarchy under + /WEB-INF/classes/. For example, a Java class named + com.mycompany.mypackage.MyServlet + would need to be stored in a file named + /WEB-INF/classes/com/mycompany/mypackage/MyServlet.class. +

    • +
    • /WEB-INF/lib/ - This directory contains JAR files that + contain Java class files (and associated resources) required for your + application, such as third party class libraries or JDBC drivers.
    • +
    + +

    When you install an application into Tomcat (or any other 2.2 or later +Servlet container), the classes in the WEB-INF/classes/ +directory, as well as all classes in JAR files found in the +WEB-INF/lib/ directory, are made visible to other classes +within your particular web application. Thus, if +you include all of the required library classes in one of these places (be +sure to check licenses for redistribution rights for any third party libraries +you utilize), you will simplify the installation of your web application -- +no adjustment to the system class path (or installation of global library +files in your server) will be necessary.

    + +

    Much of this information was extracted from Chapter 9 of the Servlet +API Specification, version 2.3, which you should consult for more details.

    + +
    + + +
    + +

    Like most servlet containers, Tomcat also supports mechanisms to install +library JAR files (or unpacked classes) once, and make them visible to all +installed web applications (without having to be included inside the web +application itself). The details of how Tomcat locates and shares such +classes are described in the +Class Loader How-To documentation. +The location commonly used within a Tomcat installation for shared code is +$CATALINA_HOME/lib. JAR files placed here are visible both to +web applications and internal Tomcat code. This is a good place to put JDBC +drivers that are required for both your application or internal Tomcat use +(such as for a DataSourceRealm).

    + +

    Out of the box, a standard Tomcat installation includes a variety +of pre-installed shared library files, including:

    +
      +
    • The Servlet 6.0 and JSP 3.1 APIs that are fundamental + to writing servlets and JSPs.

    • +
    + +
    + + +
    + +

    As mentioned above, the /WEB-INF/web.xml file contains the +Web Application Deployment Descriptor for your application. As the filename +extension implies, this file is an XML document, and defines everything about +your application that a server needs to know (except the context path, +which is assigned by the system administrator when the application is +deployed).

    + +

    The complete syntax and semantics for the deployment descriptor is defined +in Chapter 13 of the Servlet API Specification, version 2.3. Over time, it +is expected that development tools will be provided that create and edit the +deployment descriptor for you. In the meantime, to provide a starting point, +a basic web.xml file +is provided. This file includes comments that describe the purpose of each +included element.

    + +

    NOTE - The Servlet Specification includes a Document +Type Descriptor (DTD) for the web application deployment descriptor, and +Tomcat enforces the rules defined here when processing your application's +/WEB-INF/web.xml file. In particular, you must +enter your descriptor elements (such as <filter>, +<servlet>, and <servlet-mapping> in +the order defined by the DTD (see Section 13.3).

    + +
    + + +
    + +

    A /META-INF/context.xml file can be used to define Tomcat specific +configuration options, such as an access log, data sources, session manager +configuration and more. This XML file must contain one Context element, which +will be considered as if it was the child of the Host element corresponding +to the Host to which the web application is being deployed. The +Tomcat configuration documentation contains +information on the Context element.

    + +
    + + +
    + +

    The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + +

    In order to be executed, a web application must be deployed on +a servlet container. This is true even during development. +We will describe using Tomcat to provide the execution environment. +A web application can be deployed in Tomcat by one of the following +approaches:

    +
      +
    • Copy unpacked directory hierarchy into a subdirectory in directory + $CATALINA_BASE/webapps/. Tomcat will assign a + context path to your application based on the subdirectory name you + choose. We will use this technique in the build.xml + file that we construct, because it is the quickest and easiest approach + during development. Be sure to restart Tomcat after installing or + updating your application. +

    • +
    • Copy the web application archive file into directory + $CATALINA_BASE/webapps/. When Tomcat is started, it will + automatically expand the web application archive file into its unpacked + form, and execute the application that way. This approach would typically + be used to install an additional application, provided by a third party + vendor or by your internal development staff, into an existing + Tomcat installation. NOTE - If you use this approach, + and wish to update your application later, you must both replace the + web application archive file AND delete the expanded + directory that Tomcat created, and then restart Tomcat, in order to reflect + your changes. +

    • +
    • Use the Tomcat "Manager" web application to deploy and undeploy + web applications. Tomcat includes a web application, deployed + by default on context path /manager, that allows you to + deploy and undeploy applications on a running Tomcat server without + restarting it. See Manager App How-To + for more information on using the Manager web application.

    • +
    • Use "Manager" Ant Tasks In Your Build Script. Tomcat + includes a set of custom task definitions for the Ant + build tool that allow you to automate the execution of commands to the + "Manager" web application. These tasks are used in the Tomcat deployer. +

    • +
    • Use the Tomcat Deployer. Tomcat includes a packaged tool + bundling the Ant tasks, and can be used to automatically precompile JSPs + which are part of the web application before deployment to the server. +

    • +
    + +

    Deploying your app on other servlet containers will be specific to each +container, but all containers compatible with the Servlet API Specification +(version 2.2 or later) are required to accept a web application archive file. +Note that other containers are NOT required to accept an +unpacked directory structure (as Tomcat does), or to provide mechanisms for +shared library files, but these features are commonly available.

    + +
    + + + +
    diff --git a/webapps/docs/appdev/index.xml b/webapps/docs/appdev/index.xml new file mode 100644 index 0000000..cf45388 --- /dev/null +++ b/webapps/docs/appdev/index.xml @@ -0,0 +1,79 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Table of Contents + + + + + +
    + +

    This manual includes contributions from many members of the Tomcat Project +developer community. The following authors have provided significant content: +

    + + +
    + + +
    + +

    The information presented is divided into the following sections:

    +
      +
    • Introduction - + Briefly describes the information covered here, with + links and references to other sources of information.
    • +
    • Installation - + Covers acquiring and installing the required software + components to use Tomcat for web application development.
    • +
    • Deployment Organization - + Discusses the standard directory layout for a web application + (defined in the Servlet API Specification), the Web Application + Deployment Descriptor, and options for integration with Tomcat + in your development environment.
    • +
    • Source Organization - + Describes a useful approach to organizing the source code + directories for your project, and introduces the + build.xml used by Ant to manage compilation.
    • +
    • Development Processes - + Provides brief descriptions of typical development processes + utilizing the recommended deployment and source organizations.
    • +
    • Example Application - + This directory contains a very simple, but functionally complete, + "Hello, World" application built according to the principles + described in this manual. You can use this application to + practice using the described techniques.
    • +
    + +
    + + + +
    diff --git a/webapps/docs/appdev/installation.xml b/webapps/docs/appdev/installation.xml new file mode 100644 index 0000000..b232c15 --- /dev/null +++ b/webapps/docs/appdev/installation.xml @@ -0,0 +1,103 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Yoav Shapira + Installation + + + + + +
    + +

    In order to use Tomcat for developing web applications, you must first +install it (and the software it depends on). The required steps are outlined +in the following subsections.

    + + + +

    Tomcat was designed to run on Java SE or later. +

    + +

    Compatible JDKs for many platforms (or links to where they can be found) +are available at +http://www.oracle.com/technetwork/java/javase/downloads/index.html.

    + +
    + + + +

    Binary downloads of the Tomcat server are available from +https://tomcat.apache.org/. +This manual assumes you are using the most recent release +of Tomcat . Detailed instructions for downloading and installing +Tomcat are available here.

    + +

    In the remainder of this manual, example shell scripts assume that you have +set an environment variable CATALINA_HOME that contains the +pathname to the directory in which Tomcat has been installed. Optionally, if +Tomcat has been configured for multiple instances, each instance will have its +own CATALINA_BASE configured.

    + +
    + + + + +

    Binary downloads of the Ant build tool are available from +https://ant.apache.org/. +This manual assumes you are using Ant 1.8 or later. The instructions may +also be compatible with other versions, but this has not been tested.

    + +

    Download and install Ant. +Then, add the bin directory of the Ant distribution to your +PATH environment variable, following the standard practices for +your operating system platform. Once you have done this, you will be able to +execute the ant shell command directly.

    + +
    + + + + +

    Besides the required tools described above, you are strongly encouraged +to download and install a source code control system, such as Git, +Subversion, CVS or one of the many alternatives. You will need appropriate +client tools to check out source code files, and check in modified versions and, +depending on the tool and hosting option you choose, you may need to obtain and +install server software or sign up for an account with a cloud provider.

    + +

    Detailed instructions for installing and using source code control +applications is beyond the scope of this manual.

    + +
    + + +
    + + + +
    diff --git a/webapps/docs/appdev/introduction.xml b/webapps/docs/appdev/introduction.xml new file mode 100644 index 0000000..74e4fe0 --- /dev/null +++ b/webapps/docs/appdev/introduction.xml @@ -0,0 +1,88 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Introduction + + + + + +
    + +

    Congratulations! You've decided to (or been told to) learn how to +build web applications using servlets and JSP pages, and picked the +Tomcat server to use for your learning and development. But now what +do you do?

    + +

    This manual is a primer covering the basic steps of using Tomcat to +set up a development environment, organize your source code, and then +build and test your application. It does not discuss architectures or +recommended coding practices for web application development, +or provide in depth instructions on operating the development +tools that are discussed. References to sources of additional information +are included in the following subsections.

    + +

    The discussion in this manual is aimed at developers who will be using +a text editor along with command line tools to develop and debug their +applications. As such, the recommendations are fairly generic – but you +should easily be able to apply them in either a Windows-based or Unix-based +development environment. If you are utilizing an Integrated Development +Environment (IDE) tool, you will need to adapt the advice given here to +the details of your particular environment.

    + +
    + + +
    + +

    The following links provide access to selected sources of online +information, documentation, and software that is useful in developing +web applications with Tomcat.

    +
      +
    • https://jakarta.ee/specifications/pages/3.1/ - + Jakarta Server Pages (JSP), Version 3.1. Describes + the programming environment provided by standard implementations + of the Jakarta Server Pages technology. In conjunction with + the Servlet API Specification (see below), this document describes + what a portable JSP page is allowed to contain. Specific + information on scripting (Chapter 9), tag extensions (Chapter 7), + and packaging JSP pages (Appendix A) is useful..

    • +
    • https://jakarta.ee/specifications/servlet/6.0/ - + Jakarta Servlet API Specification, Version 6.0. Describes the + programming environment that must be provided by all servlet + containers conforming to this specification. In particular, you + will need this document to understand the web application + directory structure and deployment file (Chapter 10), methods of + mapping request URIs to servlets (Chapter 12), container managed + security (Chapter 13), and the syntax of the web.xml + Web Application Deployment Descriptor (Chapter 14).

    • +
    + +
    + + + +
    diff --git a/webapps/docs/appdev/processes.xml b/webapps/docs/appdev/processes.xml new file mode 100644 index 0000000..758db1b --- /dev/null +++ b/webapps/docs/appdev/processes.xml @@ -0,0 +1,290 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Development Processes + + + + +
    + +
    + +
    + +

    Although application development can take many forms, this manual proposes +a fairly generic process for creating web applications using Tomcat. The +following sections highlight the commands and tasks that you, as the developer +of the code, will perform. The same basic approach works when you have +multiple programmers involved, as long as you have an appropriate source code +control system and internal team rules about who is working on what parts +of the application at any given time.

    + +

    The task descriptions below do not assume any particular source code control +system but simply identify when and what source code control tasks are typically +performed. You will need to idenitfy the appropriate source code control +commands for your system.

    + + + + +

    In order to take advantage of the special Ant tasks that interact with the +Manager web application, you need to perform the following tasks +once (no matter how many web applications you plan to develop).

    +
      +
    • Configure the Ant custom tasks. The implementation code for the + Ant custom tasks is in a JAR file named + $CATALINA_HOME/lib/catalina-ant.jar, which must be + copied in to the lib directory of your Ant installation. +

    • +
    • Define one or more Tomcat users. The Manager web + application runs under a security constraint that requires a user to be + logged in, and have the security role manager-script assigned + to then. How such users are defined depends on which Realm you have + configured in Tomcat's conf/server.xml file -- see the + Realm Configuration How-To for more + information. You may define any number of users (with any username + and password that you like) with the manager-script role. +

    • +
    + +
    + + + + +

    The first step is to create a new project source directory, and customize +the build.xml and build.properties files you will +be using. The directory structure is described in the +previous section, or you can use the +sample application as a starting point.

    + +

    Create your project source directory, and define it within your source code +control system. This might be done by a series of commands like this:

    + + +

    To verify that the project was created correctly in the source code control +repository, you may wish to check out the project to a separate directory and +confirm that all the expected contents are present.

    + +

    Next, you will need to create and check in an initial version of the +build.xml script to be used for development. For getting +started quickly and easily, base your build.xml on the +basic build.xml file, included with this manual, +or code it from scratch.

    + + +

    The next step is to customize the Ant properties that are +named in the build.xml script. This is done by creating a +file named build.properties in your project's top-level +directory. The supported properties are listed in the comments inside +the sample build.xml script. At a minimum, you will generally +need to define the catalina.home property defining where +Tomcat is installed, and the manager application username and password. +You might end up with something like this:

    +# Context path to install this application on +app.path=/hello + +# Tomcat installation directory +catalina.home=/usr/local/apache-tomcat- + +# Manager webapp username and password +manager.username=myusername +manager.password=mypassword + +

    In general, you will not want to check the +build.properties file in to the source code control repository, +because it is unique to each developer's environment.

    + +

    Now, create the initial version of the web application deployment +descriptor. You can base web.xml on the +basic web.xml file, or code it from scratch.

    + + +Note that this is only an example web.xml file. The full definition +of the deployment descriptor file is in the +Servlet Specification. + +
    + + + + +

    The edit/build/test tasks will generally be your most common activities +during development and maintenance. The following general principles apply. +As described in Source Organization, newly created +source files should be located in the appropriate subdirectory, under your +project source directory.

    + +

    You should regularly refresh your development directory to reflect the +work performed by other developers.

    + +

    To create a new file, go to the appropriate directory and create the file. +When you are satisfied with its contents (after building and testing is +successful), add the new file to the repository. For example, to create a new +JSP page:

    + + +

    Java source code that is defined in packages must be organized in a directory +hierarchy (under the src/ subdirectory) that matches the +package names. For example, a Java class named +com.mycompany.mypackage.MyClass.java should be stored in file +src/com/mycompany/mypackage/MyClass.java. +Whenever you create a new file, don't forget to add it to the source code +control system.

    + +

    To edit an existing source file, you will generally just start editing +and testing, then commit the changed file when everything works.

    + +
    + + + + +

    When you are ready to compile the application, issue the following +commands (generally, you will want a shell window open that is set to +the project source directory, so that only the last command is needed):

    + + +

    The Ant tool will be execute the default "compile" target in your +build.xml file, which will compile any new or updated Java +code. If this is the first time you compile after a "build clean", +it will cause everything to be recompiled.

    + +

    To force the recompilation of your entire application, do this instead:

    + + +

    This is a very good habit immediately before checking in changes, to +make sure that you have not introduced any subtle problems that Javac's +conditional checking did not catch.

    + +
    + + + + +

    To test your application, you will want to install it under Tomcat. The +quickest way to do that is to use the custom Ant tasks that are included in +the sample build.xml script. Using these commands might follow +a pattern like this:

    +
      +
    • Start Tomcat if needed. If Tomcat is not already running, + you will need to start it in the usual way. +

    • +
    • Compile your application. Use the ant compile + command (or just ant, since this is the default). Make + sure that there are no compilation errors. +

    • +
    • Install the application. Use the ant install + command. This tells Tomcat to immediately start running your app on + the context path defined in the app.path build property. + Tomcat does NOT have to be restarted for this to + take effect. +

    • +
    • Test the application. Using your browser or other testing + tools, test the functionality of your application. +

    • +
    • Modify and rebuild as needed. As you discover that changes + are required, make those changes in the original source + files, not in the output build directory, and re-issue the + ant compile command. This ensures that your changes will be + available to be saved (via your chosen source code control system) later on + -- the output build directory is deleted and recreated as necessary. +

    • +
    • Reload the application. Tomcat will recognize changes in + JSP pages automatically, but it will continue to use the old versions + of any servlet or JavaBean classes until the application is reloaded. + You can trigger this by executing the ant reload command. +

    • +
    • Remove the application when you are done. When you are through + working on this application, you can remove it from live execution by + running the ant remove command. +

    • +
    + +

    Do not forget to commit your changes to the source code repository when +you have completed your testing!

    + +
    + + + + +

    When you are through adding new functionality, and you've tested everything +(you DO test, don't you :-), it is time to create the distributable version +of your web application that can be deployed on the production server. The +following general steps are required:

    +
      +
    • Issue the command ant all from the project source + directory, to rebuild everything from scratch one last time. +

    • +
    • Use the source code control system to tag the current state of the code + to create an identifier for all of the source files utilized to create this + release. This allows you to reliably reconstruct a release (from sources) + at a later time. +

    • +
    • Issue the command ant dist to create a distributable + web application archive (WAR) file, as well as a JAR file containing + the corresponding source code. +

    • +
    • Package the contents of the dist directory using the + tar or zip utility, according to + the standard release procedures used by your organization. +

    • +
    + +
    + + +
    + + +
    diff --git a/webapps/docs/appdev/project.xml b/webapps/docs/appdev/project.xml new file mode 100644 index 0000000..45a84ce --- /dev/null +++ b/webapps/docs/appdev/project.xml @@ -0,0 +1,48 @@ + + + + + Application Developer's Guide + + + The Apache Tomcat Servlet/JSP Container + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/docs/appdev/sample/docs/README.txt b/webapps/docs/appdev/sample/docs/README.txt new file mode 100644 index 0000000..f146b0e --- /dev/null +++ b/webapps/docs/appdev/sample/docs/README.txt @@ -0,0 +1,17 @@ + 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. + +This is a dummy README file for the sample +web application. diff --git a/webapps/docs/appdev/sample/index.html b/webapps/docs/appdev/sample/index.html new file mode 100644 index 0000000..589bd71 --- /dev/null +++ b/webapps/docs/appdev/sample/index.html @@ -0,0 +1,55 @@ + + + + + + +Sample Application + + +

    Sample Application

    +

    + The example app has been packaged as a war file and can be downloaded + here (Note: make sure your browser doesn't + change file extension or append a new one). +

    +

    + The easiest way to run this application is simply to move the war file + to your CATALINA_BASE/webapps directory. A default Tomcat install + will automatically expand and deploy the application for you. You can + view it with the following URL (assuming that you're running tomcat on + port 8080 which is the default): +
    + http://localhost:8080/sample +

    +

    + If you just want to browse the contents, you can unpack the war file + with the jar command. +

    +
    +        jar -xvf sample.war
    +      
    +

    + Note: CATALINA_BASE is usually the directory in which you + unpacked the Tomcat distribution. For more information on + CATALINA_HOME, CATALINA_BASE and the difference between + them see RUNNING.txt in the directory you unpacked your Tomcat + distribution. +

    + + \ No newline at end of file diff --git a/webapps/docs/appdev/sample/sample.war b/webapps/docs/appdev/sample/sample.war new file mode 100644 index 0000000000000000000000000000000000000000..b879399810422c62f1f622cc55b532df5917f0c2 GIT binary patch literal 4704 zcmaJ^2{@E(_kU(F1~VFr$S(UnmSinU$eLX=_K|(bObEr8B3sHvn+^<0ZFekgB0XQ5E=g!oq%bMzcG94Z=F_w`XLmJCS zjtoC(u*5u7oL>qa)@3~-ecWW^xDAj15_!C3a;`Zy(UQ&B(u$BLsr}aUGhzWbOh`kR zuq(wFe?mtprX_w%lu9(V#3!HM7u)$7C_FBFZJyPqo_?}3;5@7`Sh!Q1~_kgL0|tEUGIZZ8)E4bIQo?SFXE zlRdrt$dboGcD$f;GgDm&hMcIDo ze$6YYSAKX8m)CWUu7&xZ$T;xe)8aIuQq`UVvBO>~Goe;f1#gjYlnOO;_z^b#pGIkbkskC;HWAW?j z5&BRhbkJrd&K>2Lbq8t5r{H~^V;6B^|4lDMeSHCo=|o7b>ZUYFYoxrMXn zstSk~9rC{AQFn!17v5y1DXB7O)h-_Rpr(^!!VmS$>G3neni8BZDSLjhjywMKq~YpS ztBlW%)l(IQ&atI-rd2tsxy1yjnkvNFLFr^HpP<501x8uzW8>moE?e}3k&siOS&E;? zsr0*}oB2NP`HaeU^z)Xtj!G6Al?@Ed7G0}bt?Vzik z?i|7OS+s`t!1mdpowMPBBeg?IxX+FYzH8H>U@v*^lBgUD*jm(%C+E+x^`*}RJ3G2A z3myF;9dj?-RL~5E#HNiJpG^d4^#mQsm!I=Lyl$%JK7nDIdyX1tmFIb!vg?7O-_=TE zCOUoh4?lKgoV)0#qf0o7;l%i$=+L7tCXvvHzXPHy6(B0+gs<2q)9a#k1jHO&o=hi*^QGY-- z$qL;qVzu}>l+sXQ%HDGeK>%Qaj<(_LeTnzcymg@-Zn9ziex<2h#@!fn-xKDa{CsU; zbpy)m1Ol73x%LN)c3^7hlSA2}f^w(IquT7J{qx{_(rsHmX0LyZs2z~CD~%Nix12~y z1FeZg>rY{`zN<9n94T8_@#J_T(45QjrJm8VGKkr&Z&XD>CrD^yuGbRdz$xlKP~40B z@Kw+)c#`z(X>T!mrGH_Sq-`E%Z6$K@{l0HQ-8EFf7mC`L&s3u?sJq+p6#M)&$zij zSQ072>*JKjoxr;@T`|7cqI=9cEM1RvmL54MwLh7tx_F^dzVWAHrQ)wf3x#&i9oB0O zDAU0cVz1+F2vz^pa?(98ETrbE{kTGa_-s#&>H!paA!$1HaLq0g_Ew%U17|4g_^>;(`!3Ui;AM%07BU@lx;o}b=4qSY493V0*VJ79!sU*)ETa@QBb z3x?=qy|#qaOM{l?MvdOm+csP)*O;qWWVDmVCJq|eztKoMJcbCaW*k*#TQN15s9B03 zY6dn0-JD!cl|Zi*pLwn<*8Ki?)%o41-k6l`>u1%|`)wgFRbFzgGERC`jxt7L85r;+ z$q%{_Cu%oMUsRvzjNATDM_NCCOMITjclz=+Gm|IB;|4Fu6AN_AyqTYMc5pkh9h9;J z{7@O0CnrrQ&3`Wo3)$n}*9;-h-`$lU>*;+Obd%0$+rUs?Mb#At7yu0bu(7crCc?iu zi1)T;FHf@%2{>6(Q!_q3KGNu!n3zx!!*6D+vpaKkyo84wZ+CZB@H>>e003#q4v_jIFMP zhP}gZ-TcRJk=wwtO=!YNnWp=)DZu&oYAKMbq4E6bp=e+`!YU2{CP=$Ts1xWA_>c>x zfHWe3!~nn;80eU?b8_hD7+^k{;8d?CKykmjo-nE-yzz9?9@$Kr%HP6$FI##iLTJ>!eVH3t(R`SU})DBT-vGVK=OcaTx}Q z7F;XbVAL|5ukztZlM#B(luo?Zhm{}1;1-n?sug-?ls!e0#FUP26-U$WFGWa2Rv|Db zVtxeta%ssWNM`~dLQ2xee3hVDdF@tN>piJA;70S1vI>q6T{FA0BEyA@<{PTj*3b1F z#^`%z*)mGbY^U4lq?TZwxVkoJEi?-noAUVZETd9DF+vQQYe0>W_UdD(dVCD>fl`lX4l&lzoVY z7nseH6tsw$qhr#}pDK8{meFFoIAzw4`0uc9ad4)*GXhFqJYVj2+(q#73S#$zX5!{z zAmWPG!lzBxt{&*fvUXv18msG!;aM;sMNQZB47gc1W^%c4JQT>63MV`&BuzJ7%JFw> zWk<0YlvZ#dAu}FYaq!M%=5R%;I5|V90r02K_k>~B z^}nw-J$d*BxXdHreTy)NjW#H=31)oPg1piH(wvCs%4TV|7&Wh8dZ;SW3LG9StmV;m zwPuT8PrV7*otg@)1RYh!gfB;C)UY@wY}#~>=y25aNH;b;?&RP;1E-e`T3=_Bj2eE` z`TFh0XU!i_APiDAD#t6;GJXB{ei=pI*;rK)w%eiQY^RA-<6Erm*xcZBE)cRk1NB)@ z-tZ$qwRjK3EZ)U1^H~jYE5%uyLk46VOZs%O+c7uUna77z-FY3!C*3EQ)gGDTmiR(F zX~t)lyXKiAaxAptVDgeV=;DgB+!fcJrZC82oxtF(RH2KdJs-$~O|=!`$tI% zsx}>s6%W01WP9Qe!9S2aVy7i%AG=21IasaOsL|@vW4C~QZqXehlt;@tnHLJl0-Y-aRv`@FC=VLa>m>-!B)i} zPy}T`@Vr~){Eh5+ZT{`Li}rrS$n3(xdLuK7vAEN4ts&SvgQ@Tmz2Rk>xYlpIqQjdB zBa*tje4FgpvG`w~KT>f`aSge2h|Czfck zrctLf`HCN~>t*C&<>vn!Sjf5v@D9Lvgvojl{QZ(GO^v&?P;DIB{IS^9bp6^?7*zX% zu=QrEq3Dac9FrR_8}D_@zHm$kM_s&QKl$}gN~IsCW1ExBep8NJVoY~(&*9CY#_9(dI&c!EI406?-|jA(az`mx^}(o;@OyhNAH$8G;l0K| zN|h|8Z#E2q!g_?%@+|^1G-RH>`V#R=!Opf!8DFO+P_4}GZpKzLbHGeG4w-w7IAIc9 zFe$4>pL)b-m`gKa;;or{p1$h0h-VJt`&ANBVUiHrJ9F&HnXNmUc#Dp8XR9b1@3$KT zlSN;|4dPxgN~Hz2_Ut@jN(+6*L^+n17-EVu`QD8LQ|?|67!CZErvFK0Q*KDA{UfA* zYiRQXC9XD@e(j;5L&O`PAVn#zAKlA^1rMkFKuQQ6dDQ)ts_0gBG1 zS{coDf0CxsrcvF$Gx&Xe6dtsbDn&$4jfedH{;9;jCBrW~nv?qZw`Y!e~1L tW!R(ID!CN@Z}i&>r+tzsZG>u%k+e<4+?bBc0ss)?&ocR`$Nin(zW_9N?K}Vg literal 0 HcmV?d00001 diff --git a/webapps/docs/appdev/sample/src/mypackage/Hello.java b/webapps/docs/appdev/sample/src/mypackage/Hello.java new file mode 100644 index 0000000..de70781 --- /dev/null +++ b/webapps/docs/appdev/sample/src/mypackage/Hello.java @@ -0,0 +1,83 @@ +/* + * 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 mypackage; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + + +/** + * Simple servlet to validate that the Hello, World example can + * execute servlets. In the web application deployment descriptor, + * this servlet must be mapped to correspond to the link in the + * "index.html" file. + * + * @author Craig R. McClanahan <Craig.McClanahan@eng.sun.com> + */ + +public final class Hello extends HttpServlet { + + private static final long serialVersionUID = 1L; + + /** + * Respond to a GET request for the content produced by + * this servlet. + * + * @param request The servlet request we are processing + * @param response The servlet response we are producing + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + try (PrintWriter writer = response.getWriter()) { + + writer.println(""); + writer.println(""); + writer.println(""); + writer.println("Sample Application Servlet Page"); + writer.println(""); + writer.println(""); + + + writer.println("
    "); + writer.println("\"\""); + writer.println("
    "); + writer.println("

    Sample Application Servlet

    "); + writer.println("

    "); + writer.println("This is the output of a servlet that is part of"); + writer.println("the Hello, World application."); + writer.println("

    "); + + writer.println(""); + writer.println(""); + } + } + + +} diff --git a/webapps/docs/appdev/sample/web/WEB-INF/web.xml b/webapps/docs/appdev/sample/web/WEB-INF/web.xml new file mode 100644 index 0000000..f9e24c1 --- /dev/null +++ b/webapps/docs/appdev/sample/web/WEB-INF/web.xml @@ -0,0 +1,40 @@ + + + + + Hello, World Application + + This is a simple web application with a source code organization + based on the recommendations of the Application Developer's Guide. + + + + HelloServlet + mypackage.Hello + + + + HelloServlet + /hello + + + diff --git a/webapps/docs/appdev/sample/web/hello.jsp b/webapps/docs/appdev/sample/web/hello.jsp new file mode 100644 index 0000000..bd5680a --- /dev/null +++ b/webapps/docs/appdev/sample/web/hello.jsp @@ -0,0 +1,37 @@ +<%-- + 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. +--%> +<%@ page session="false" pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %> + + + + +Sample Application JSP Page + + + +
    + +
    +

    Sample Application JSP Page

    +This is the output of a JSP page that is part of the Hello, World +application. + + +<%= new String("Hello!") %> + + + diff --git a/webapps/docs/appdev/sample/web/images/tomcat.gif b/webapps/docs/appdev/sample/web/images/tomcat.gif new file mode 100644 index 0000000000000000000000000000000000000000..f2aa6f863e43e3924a35854c556a9c1b6d125cba GIT binary patch literal 2066 zcmb7-`9Bkk1AuqiW)t(qMA~Goi7YF;%8Fr3&3&wxJJ;MtNiUNrOkuHPUPq1)l@X~Z zSI&?*UMjqVP^8{^du89x`~45z=ZELF=kr+6ERBr4a{*@oT>yYYA{ndHhDb^Md=smN zQ?WW_7!m4O&3Da=BIf4iyng+9d;3eHNHiky*&2;#;p+kZBly1||1Wg^^}kO5RD#20 z`vn!Buc`O3*9z6WO@Gd3)SFXI{AetRNN|8>!?2NvYP|guFVQi7j|M{LmbEp@UwR`S zK-e07+VI0>itQD`FWXSiihOTqGW@#|)W`Q&k{Tiw8kU-1re^Pa<%S^$f{wMyh)iL^ zp&(BU=WJfi9Str7EX)^p6*AEtFwN4;ie?WexSkpB#?5bSY3{TT!oWB4K~QNgR6j`S z@j#cD71n!q6bvQ5&g4YtPx+liJJPvF51Cy^wQ4c( z$fbHW--q9)&#u-k#1*T6^NlK(>W|%YtOoz$iW+DNh)#N@Gq-FEg~f230$6j%@Ve=^ zn#XR94uy?DGa3p#L!%e}ULl-^=Td^@JEwcpYWzc^o~cU@Px~aNe8SY8)YH!qhE_ya z5D9GCZNONSLem%xV}7jbYp&hGEhco(^_r(3NNq;q3@JL6#VGBYyMB?lv+(Ae)LVjO z3K^|67-k7fI0f20B;qh2Du-F_N()VcX=l*s**a>KM+(`xN;s_asvYTo`{GW5m=o$9 zJ*>}@+Y*@Wz*EXI!g9dAu07q|0&>UR8xvd@osMUbXb& zn-5I~oefi`WhkJFQ-j#)BldF{7O@(WRf05EUY1ykQWg!xZBhWprv??wfe0P-_&k=9 zj=^a@mjW0(8sY@5;}SX=I#eIaN$kdfoF%!ko(Ggt_TuA^#>JXbhp6;Y(1%LxJ`t4{ zxdZ%!c^7Q+@#)}S!5hx7@x@Q=l3)W}C*c!EqD#_vn*;d!+Yl!aed8#MDHpGN?a3vr zoe>~9Z)U9{Ms0NICF*YOLT#L&KTPG3>O6-8jiG<+6ztvceF?J|$Z4)rU87sIzF#}g z`ORU8Q(Y&WX1z`hQl|GV$(vnrMFt{~fS*;s?@>SdX9vgX@D~!PbxRdQ>8%ayz~>+6 zQR<`8*hcuGF*m?R@zJ29P#+YeshDuWlU#8*#;LV8A%3uFwrKh8GEVUWxc$S zSjir`eS~hr?i9A~e%&IObwFzu7kTXGL=(p51(|%1inzV?Ve9(U^jB*i&(O$KH=YhN zGq4fau2S<0z50zh;NKKvzfpdsv}=dQe0;bA2*vIn_7}kB4kbdp4@| zbm9QXjy0&7nza3Edu0RNM9V`x3={PfhTr^&Kdv>fEbh#oM){s9CKynX8P0%od8+mg zxViNcHH5;ZrbJflBQWf+uVQ=iwPx%l!bcZ)W!&hI=V&DFe)M!hM{~PT?iO117d6%H zbW1(;00-l&a`>(hLLI`uL$H23sAW>}EG@}>T9z=cXr^aFy3At2Mt0v9pj&UqyfZJ- zG^4%5cw@BMaUEO8Rq~d1>P-M-W&pm~&na0+5smMob7_NuSxmv_KRuR^mNt1Ud#*m# z%^^fBVgxLiThV!Q&~CU2FnKfWTprda>I)|HXx+gXdYO(X);#F)qSl>T0k$%SEljyl zm?05YdwYfS{BVcx6VuOhB`5D@%bj98zoaFLL-E=T4=Gnq`N#C($uEQDj9p_Joxl~& zSSfPc7Y5jOGD1w zPX1PQ#KdzS(|tquOD5(vFGfoT1k(ujh(d!)&t1+D893W*Ct16Ngc{0+tGkV`Iuk4I z{E=Dv81f0Chp(_`?E`9eHJgj+2^(Mq%T9ZA!Mj)C{k~e>S;5pc8N6f5%=zyxF5a*1 z5jWV>1fPx?w-qO^p)-WaJQq#C`_T``ge2<&4av8|RhBB23U$;k1I^1wz$#U_+FZTN zD}xImjf$E3Tdn=4aE9q+Io2+OuxqcZjYhoL&fs(Qe=v!Bg~mX`jMK}=sYDv1`MSk@ z@$qKnG=8dI)QqO!+W^AV%1*5`$9rmlAs(j{hRvW;a`J8iJ^Xu$(t=Y}iGAd@^41hH z^Ou;<*(0<6^ix`_XtffJ(E%??UI#22%=xC=6>r?<&->hvbnpomksf}<FHYidn+`U2X=qZRq`VR#7O*Z%?iw2vkL literal 0 HcmV?d00001 diff --git a/webapps/docs/appdev/sample/web/index.html b/webapps/docs/appdev/sample/web/index.html new file mode 100644 index 0000000..1c6938a --- /dev/null +++ b/webapps/docs/appdev/sample/web/index.html @@ -0,0 +1,39 @@ + + + + +Sample "Hello, World" Application + + + +
    + +
    +

    Sample "Hello, World" Application

    +

    This is the home page for a sample application used to illustrate the +source directory organization of a web application utilizing the principles +outlined in the Application Developer's Guide. + +

    To prove that they work, you can execute either of the following links:

    + + + + diff --git a/webapps/docs/appdev/source.xml b/webapps/docs/appdev/source.xml new file mode 100644 index 0000000..016cddd --- /dev/null +++ b/webapps/docs/appdev/source.xml @@ -0,0 +1,289 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Source Organization + + + + +
    + +
    + +
    + +

    The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + +

    A key recommendation of this manual is to separate the directory +hierarchy containing your source code (described in this section) from +the directory hierarchy containing your deployable application +(described in the preceding section). Maintaining this separation has +the following advantages:

    +
      +
    • The contents of the source directories can be more easily administered, + moved, and backed up if the "executable" version of the application + is not intermixed. +

    • +
    • Source code control is easier to manage on directories that contain + only source files. +

    • +
    • The files that make up an installable distribution of your + application are much easier to select when the deployment + hierarchy is separate.

    • +
    + +

    As we will see, the ant development tool makes the creation +and processing of such directory hierarchies nearly painless.

    + +

    The actual directory and file hierarchy used to contain the source code +of an application can be pretty much anything you like. However, the +following organization has proven to be quite generally applicable, and is +expected by the example build.xml configuration file that +is discussed below. All of these components exist under a top level +project source directory for your application:

    +
      +
    • docs/ - Documentation for your application, in whatever + format your development team is using.

    • +
    • src/ - Java source files that generate the servlets, + beans, and other Java classes that are unique to your application. + If your source code is organized in packages (highly + recommended), the package hierarchy should be reflected as a directory + structure underneath this directory.

    • +
    • web/ - The static content of your web site (HTML pages, + JSP pages, JavaScript files, CSS stylesheet files, and images) that will + be accessible to application clients. This directory will be the + document root of your web application, and any subdirectory + structure found here will be reflected in the request URIs required to + access those files.

    • +
    • web/WEB-INF/ - The special configuration files required + for your application, including the web application deployment descriptor + (web.xml, defined in the + Servlet Specification), + tag library descriptors for custom tag libraries + you have created, and other resource files you wish to include within + your web application. Even though this directory appears to be a + subdirectory of your document root, the Servlet Specification + prohibits serving the contents of this directory (or any file it contains) + directly to a client request. Therefore, this is a good place to store + configuration information that is sensitive (such as database connection + usernames and passwords), but is required for your application to + operate successfully.
    • +
    + +

    During the development process, two additional directories will be +created on a temporary basis:

    +
      +
    • build/ - When you execute a default build + (ant), this directory will contain an exact image + of the files in the web application archive for this application. + Tomcat allows you to deploy an application in an unpacked + directory like this, either by copying it to the + $CATALINA_BASE/webapps directory, or by installing + it via the "Manager" web application. The latter approach is very + useful during development, and will be illustrated below. +

    • +
    • dist/ - When you execute the ant dist + target, this directory will be created. It will create an exact image + of the binary distribution for your web application, including an license + information, documentation, and README files that you have prepared.
    • +
    + +

    Note that these two directories should NOT be archived in +your source code control system, because they are deleted and recreated (from +scratch) as needed during development. For that reason, you should not edit +any source files in these directories if you want to maintain a permanent +record of the changes, because the changes will be lost the next time that a +build is performed.

    + + + +

    What do you do if your application requires JAR files (or other + resources) from external projects or packages? A common example is that + you need to include a JDBC driver in your web application, in order to + operate.

    + +

    Different developers take different approaches to this problem. + Some will encourage checking a copy of the JAR files you depend on into + the source code control archives for every application that requires those + JAR files. However, this can cause significant management issues when you + use the same JAR in many applications - particular when faced with a need + to upgrade to a different version of that JAR file.

    + +

    Therefore, this manual recommends that you NOT store + a copy of the packages you depend on inside the source control archives + of your applications. Instead, the external dependencies should be + integrated as part of the process of building your + application. In that way, you can always pick up the appropriate version + of the JAR files from wherever your development system administrator has + installed them, without having to worry about updating your application + every time the version of the dependent JAR file is changed.

    + +

    In the example Ant build.xml file, we will demonstrate + how to define build properties that let you configure the locations + of the files to be copied, without having to modify build.xml + when these files change. The build properties used by a particular + developer can be customized on a per-application basis, or defaulted to + "standard" build properties stored in the developer's home directory.

    + +

    In many cases, your development system administrator will have already + installed the required JAR files into the lib directory of Tomcat. + If this has been done, you need + to take no actions at all - the example build.xml file + automatically constructs a compile classpath that includes these files.

    + +
    + +
    + + +
    + +

    As mentioned earlier, it is highly recommended that you place all of the +source files that comprise your application under the management of a +source code control system. If you elect to do this, every directory and file +in the source hierarchy should be registered and saved -- but none of the +generated files. If you register binary format files (such as images or JAR +libraries), be sure to indicate this to your source code control system.

    + +

    We recommended (in the previous section) that you should not store the +contents of the build/ and dist/ directories +created by your development process in the source code control system. Source +code control systems typically provide mechanisms to ignore these directories +(Git uses a .gitignore file, Subversion uses the +svn:ignore property, CVS uses a .cvsignore file, etc.) +You should configure your source code control system to ignore:

    +
      +
    • build
    • +
    • dist
    • +
    • build.properties
    • +
    + +

    The reason for mentioning build.properties here will be +explained in the Processes section.

    + +

    Detailed instructions for your source code control environment are beyond +the scope of this manual.

    + +
    + + +
    + +

    We will be using the ant tool to manage the compilation of +our Java source code files, and creation of the deployment hierarchy. Ant +operates under the control of a build file, normally called +build.xml, that defines the processing steps required. This +file is stored in the top-level directory of your source code hierarchy, and +should be checked in to your source code control system.

    + +

    Like a Makefile, the build.xml file provides several +"targets" that support optional development activities (such as creating +the associated Javadoc documentation, erasing the deployment home directory +so you can build your project from scratch, or creating the web application +archive file so you can distribute your application. A well-constructed +build.xml file will contain internal documentation describing +the targets that are designed for use by the developer, versus those targets +used internally. To ask Ant to display the project documentation, change to +the directory containing the build.xml file and type:

    +ant -projecthelp + +

    To give you a head start, a basic build.xml file +is provided that you can customize and install in the project source directory +for your application. This file includes comments that describe the various +targets that can be executed. Briefly, the following targets are generally +provided:

    +
      +
    • clean - This target deletes any existing + build and dist directories, so that they + can be reconstructed from scratch. This allows you to guarantee that + you have not made source code modifications that will result in + problems at runtime due to not recompiling all affected classes. +

    • +
    • compile - This target is used to compile any source code + that has been changed since the last time compilation took place. The + resulting class files are created in the WEB-INF/classes + subdirectory of your build directory, exactly where the + structure of a web application requires them to be. Because + this command is executed so often during development, it is normally + made the "default" target so that a simple ant command will + execute it. +

    • +
    • all - This target is a short cut for running the + clean target, followed by the compile target. + Thus, it guarantees that you will recompile the entire application, to + ensure that you have not unknowingly introduced any incompatible changes. +

    • +
    • javadoc - This target creates Javadoc API documentation + for the Java classes in this web application. The example + build.xml file assumes you want to include the API + documentation with your app distribution, so it generates the docs + in a subdirectory of the dist directory. Because you normally + do not need to generate the Javadocs on every compilation, this target is + usually a dependency of the dist target, but not of the + compile target. +

    • +
    • dist - This target creates a distribution directory for + your application, including any required documentation, the Javadocs for + your Java classes, and a web application archive (WAR) file that will be + delivered to system administrators who wish to install your application. + Because this target also depends on the deploy target, the + web application archive will have also picked up any external dependencies + that were included at deployment time.
    • +
    + +

    For interactive development and testing of your web application using +Tomcat, the following additional targets are defined:

    +
      +
    • install - Tell the currently running Tomcat to make + the application you are developing immediately available for execution + and testing. This action does not require Tomcat to be restarted, but + it is also not remembered after Tomcat is restarted the next time. +

    • +
    • reload - Once the application is installed, you can + continue to make changes and recompile using the compile + target. Tomcat will automatically recognize changes made to JSP pages, + but not to servlet or JavaBean classes - this command will tell Tomcat + to restart the currently installed application so that such changes are + recognized. +

    • +
    • remove - When you have completed your development and + testing activities, you can optionally tell Tomcat to remove this + application from service. +
    • +
    + +

    Using the development and testing targets requires some additional +one-time setup that is described on the next page.

    + +
    + + + +
    diff --git a/webapps/docs/appdev/web.xml.txt b/webapps/docs/appdev/web.xml.txt new file mode 100644 index 0000000..70caa81 --- /dev/null +++ b/webapps/docs/appdev/web.xml.txt @@ -0,0 +1,165 @@ + + + + + + + + My Web Application + + This is version X.X of an application to perform + a wild and wonderful task, based on servlets and + JSP pages. It was written by Dave Developer + (dave@mycompany.com), who should be contacted for + more information. + + + + + + + webadmin + myaddress@mycompany.com + + The EMAIL address of the administrator to whom questions + and comments about this application should be addressed. + + + + + + + + controller + + This servlet plays the "controller" role in the MVC architecture + used in this application. It is generally mapped to the ".do" + filename extension with a servlet-mapping element, and all form + submits in the app will be submitted to a request URI like + "saveCustomer.do", which will therefore be mapped to this servlet. + + The initialization parameter names for this servlet are the + "servlet path" that will be received by this servlet (after the + filename extension is removed). The corresponding value is the + name of the action class that will be used to process this request. + + com.mycompany.mypackage.ControllerServlet + + listOrders + com.mycompany.myactions.ListOrdersAction + + + saveCustomer + com.mycompany.myactions.SaveCustomerAction + + + 5 + + + + graph + + This servlet produces GIF images that are dynamically generated + graphs, based on the input parameters included on the request. + It is generally mapped to a specific request URI like "/graph". + + + + + + + + controller + *.do + + + + graph + /graph + + + + + + + 30 + + + + diff --git a/webapps/docs/apr.xml b/webapps/docs/apr.xml new file mode 100644 index 0000000..1987d39 --- /dev/null +++ b/webapps/docs/apr.xml @@ -0,0 +1,120 @@ + + + +]> + + + &project; + + + Apache Portable Runtime (APR) based Native library for Tomcat + Remy Maucherat + + + + +
    + +
    + +
    + +

    + Tomcat can use the Apache Portable Runtime to + provide an OpenSSL based TLS implementation for the HTTP connectors. +

    + +

    + These features allows making Tomcat a general purpose webserver, will enable much better + integration with other native web technologies, and overall make Java much more viable as + a full fledged webserver platform rather than simply a backend focused technology. +

    + +
    + +
    + +

    + APR support requires three main native components to be installed: +

    +
      +
    • APR library
    • +
    • JNI wrappers for APR used by Tomcat (libtcnative)
    • +
    • OpenSSL libraries
    • +
    + + + +

    + Windows binaries are provided for tcnative-2, which is a statically compiled .dll which includes + OpenSSL and APR. It can be downloaded from here + as 32bit or AMD x86-64 binaries. + In security conscious production environments, it is recommended to use separate shared dlls + for OpenSSL, APR, and libtcnative-2, and update them as needed according to security bulletins. + Windows OpenSSL binaries are linked from the Official OpenSSL + website (see related/binaries). +

    + +
    + + + +

    + Most Linux distributions will ship packages for APR and OpenSSL. The JNI wrapper (libtcnative) will + then have to be compiled. It depends on APR, OpenSSL, and the Java headers. +

    + +

    + Requirements: +

    +
      +
    • APR 1.6.3+ development headers (libapr1-dev package)
    • +
    • OpenSSL 1.1.1+ development headers (libssl-dev package)
    • +
    • JNI headers from Java compatible JDK 1.4+
    • +
    • GNU development environment (gcc, make)
    • +
    + +

    + The wrapper library sources are located in the Tomcat binary bundle, in the + bin/tomcat-native.tar.gz archive. + Once the build environment is installed and the source archive is extracted, the wrapper library + can be compiled using (from the folder containing the configure script): +

    + ./configure && make && make install + +
    + +
    + +
    + +

    + Once the libraries are properly installed and available to Java (if loading fails, the library path + will be displayed), the Tomcat connectors will automatically use APR. +

    + +
    + +
    +

    See the + listener configuration.

    +
    + + +
    diff --git a/webapps/docs/architecture/index.xml b/webapps/docs/architecture/index.xml new file mode 100644 index 0000000..fcedf9a --- /dev/null +++ b/webapps/docs/architecture/index.xml @@ -0,0 +1,69 @@ + + + +]> + + + &project; + + + Yoav Shapira + Table of Contents + + + + + +
    + +

    This section of the Tomcat documentation attempts to explain +the architecture and design of the Tomcat server. It includes significant +contributions from several tomcat developers: +

    + + +
    + + +
    + +

    The information presented is divided into the following sections:

    +
      +
    • Overview - + An overview of the Tomcat server architecture with key terms + and concepts.
    • +
    • Server Startup - + A detailed description, with sequence diagrams, of how the Tomcat + server starts up.
    • +
    • Request Process Flow - + A detailed description of how Tomcat handles a request.
    • +
    + +
    + + + +
    diff --git a/webapps/docs/architecture/overview.xml b/webapps/docs/architecture/overview.xml new file mode 100644 index 0000000..be1f9a1 --- /dev/null +++ b/webapps/docs/architecture/overview.xml @@ -0,0 +1,138 @@ + + + +]> + + + &project; + + + Yoav Shapira + Architecture Overview + + + + + +
    +

    +This page provides an overview of the Tomcat server architecture. +

    +
    + +
    + + +

    +In the Tomcat world, a +Server represents the whole container. +Tomcat provides a default implementation of the +Server interface +which is rarely customized by users. +

    +
    + + +

    +A Service is an intermediate component +which lives inside a Server and ties one or more Connectors to exactly one +Engine. The Service element is rarely customized by users, as the default +implementation is simple and sufficient: +Service interface. +

    +
    + + +

    +An +Engine represents request processing +pipeline for a specific Service. As a Service may have multiple Connectors, +the Engine receives and processes all requests from these connectors, handing +the response back to the appropriate connector for transmission to the client. +The Engine interface +may be implemented to supply custom Engines, though this is uncommon. +

    +

    +Note that the Engine may be used for Tomcat server clustering via the +jvmRoute parameter. Read the Clustering documentation for more information. +

    +
    + + +

    +A Host is an association of a network name, +e.g. www.yourcompany.com, to the Tomcat server. An Engine may contain +multiple hosts, and the Host element also supports network aliases such as +yourcompany.com and abc.yourcompany.com. Users rarely create custom +Hosts +because the +StandardHost +implementation provides significant additional functionality. +

    +
    + + +

    +A Connector handles communications with the client. There are multiple +connectors available with Tomcat. These include the +HTTP connector which is used for +most HTTP traffic, especially when running Tomcat as a standalone server, +and the AJP connector which implements +the AJP protocol used when connecting Tomcat to a web server such as +Apache HTTPD server. Creating a customized connector is a significant +effort. +

    +
    + + +

    +A +Context +represents a web application. A Host may contain multiple +contexts, each with a unique path. The +Context +interface may be implemented to create custom Contexts, but +this is rarely the case because the + +StandardContext provides significant additional functionality. +

    +
    +
    + +
    +

    +Tomcat is designed to be a fast and efficient implementation of the +Servlet Specification. Tomcat came about as the reference implementation +of this specification, and has remained rigorous in adhering to the +specification. At the same time, significant attention has been paid +to Tomcat's performance and it is now on par with other servlet containers, +including commercial ones. +

    +

    +In recent releases of Tomcat, mostly starting with Tomcat 5, +we have begun efforts to make more aspects of Tomcat manageable via +JMX. In addition, the Manager and Admin webapps have been greatly +enhanced and improved. Manageability is a primary area of concern +for us as the product matures and the specification becomes more +stable. +

    +
    + + +
    diff --git a/webapps/docs/architecture/project.xml b/webapps/docs/architecture/project.xml new file mode 100644 index 0000000..6b77dc9 --- /dev/null +++ b/webapps/docs/architecture/project.xml @@ -0,0 +1,45 @@ + + + + + Apache Tomcat 10 Architecture + + + The Apache Tomcat Servlet/JSP Container + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/docs/architecture/requestProcess.xml b/webapps/docs/architecture/requestProcess.xml new file mode 100644 index 0000000..1d8adbc --- /dev/null +++ b/webapps/docs/architecture/requestProcess.xml @@ -0,0 +1,74 @@ + + + +]> + + + &project; + + + Yoav Shapira + Request Process Flow + + + + + +
    + +

    +This page describes the process used by Tomcat to handle +an incoming request. This process is largely defined by +the Servlet Specification, which outlines the order +of events that must take place. +

    + + +

    +TODO +

    +
    + + +

    +A UML sequence diagram of the request process is available +here. +

    +

    +A UML sequence diagram of the authentication process is available +here. +

    + +
    + + +

    +The Servlet Specification provides many opportunities for +listening in (using Listeners) or modifying (using Filters) +the request handling process even before the request arrives +at the servlet that will handle it. +

    + +
    + +
    + + + +
    diff --git a/webapps/docs/architecture/requestProcess/authentication-process.png b/webapps/docs/architecture/requestProcess/authentication-process.png new file mode 100644 index 0000000000000000000000000000000000000000..e23c33359c42abc6303b216e24275cae80305430 GIT binary patch literal 42682 zcmdRX2{@E(-?wg+Hnd49v{ES)CF^vzsfbEawuwR|O|pz-m`ZU+QOOoV60&DsXQoIJ z#=d2lEE)UIFk@!S%y-U=Ww`5ppXYhs@BNOi9=DN=1zyAKqdCudrI+}vZHZ0@g z;}bk}^4NJkK7n(5eEd2~{s8||i80>?J{Fs6X&wWg;1Bg^J`#K^y>s%i9Uq_2C-&b3 z0Wv}x`S>>TojUgC1*fpVw&gE3HM&XmK3}n9v)jeA^!#R%=P!>*Tlpo%iWrTlIEIBa znG&vv?`YpH?d$JguGxIc@v2s!#c^-cxI*e)tcYshkDb-KU!3yW_1NX`*28M2o^77s zJ2kSX^NiZkXPXzSd~-!FE$iNA#SLxsZTi_U`M2BX^;F#TL%rcQ%2@^n9FRNf_aHu5 zrZ?X@7+S&eaq1S7(QhE?&AM!yFUofhcd50RY0by?T7>|6sDflpb`*K}NkZKF>*5HD zvua~?DedHJ1uI;bx+|GVh-jq#!Ds*M06*X1>_fA;+%;^}DoKsMT{QUggV4* zwIJ_MhTVjR5Z%5>EfVe%)qaTXZ7NXqi}#$yRq*YZ=t>A{vai-E+>M-T6|S5K!+(?x zZ=yGK>8eeRT!q81u(Ci^NyMF4oGAOP%{Li1N^7ni*zk}DMp{RW@o5(_4jw_+A@@VOQNWCeXVhi!^;VwWM1`fbTXho!NIZ3N}if z7+r%WspIW>tDc+Wli9n=t9-R!>g5cQx*JV<6sLqFDPf0fV6K&}?RMWkM0!cOYXcgB zKUE8kqF|}&tVt4{mV~E~v#|_*$%zk|5L&e`a-ztUJcwm61spK!XZcb$_9H4$^iEYg z6~}peEEa;kQ5$2^lGTP}G2m?uja`0_NYWzq!+)9dIbe!aOZ*}Z-R*9QJgOJGNkd!zCtTza3=iO4U+JN$gT8rF-a9&#tpCrOnN z8g4xyRyrs(>X@D`%4u@6O&BuS#4kKpKxr3;w`aO_#k6>-;vuHlK>Ct*Hmpd(F6QHl zw_~1oZ%S|-Zp}6AaP9n5+Et>vN!?|#!%Ncb`+curG^`}chSKU@VzTkz?P|6VHg$d@ zL;3mm>Xnh(nt&X_^7>-7Y5ImK+x2ul6%Ti&^;zkJBV6zpMn0VAW=;t=$q#oLOf~US z``JVuqPS0XNpghw#k4T92g$Z+ajZR~qP6LLQ1NY95qT^Zp>}4n!v18_o57{ zzUDkh@c@FfjsRO&3-)&H3R_h7 zTvFisExijnzoBP@Odci2cX-_lFYSTCTz+rK$2&da`xK*p_2t~P@htpvr(fd`Bg!b^ z%E?ii8*Yj{!5y0f6-#eyu)Vdq?7Z|Fr!E3KQv<%`)PBQVrF$J#Z-pH0w#R@Se|~f5 z_@(zxZxv@pk5>m@B_-L}DON2tg2&$Ylxp+X;?!EJ)TNKFS0Ky4OKAm0x2X$y4imKG zQLsHO1S5jZurh1#euGyUEK%_Wyb@}gPWY4 z7#{^-L!CGLm1FjmED^Z*s?;y%PkA!3FT|(WsmJ`FY)O~-5!r{`qoyaru`6HP58Kq% z>~|Wnw^IC@t+KpHzuA1*t*|Sz5{^*t-e~Vir=ks9fKg>88B6|>n&E$W%l)XAZf-OA0pK9 zS-RnEaAsXQsdA7}qu`1e8ygl4ckg`UpKp37b-+hjg4L<#hjQ(&wkwfzW;PW)O4o#k ztJA8Qw+V+UPksm~Qq_^%-O6h9r>Mh6+O34!NrU%?Vuf1WZnD~9$u_Q*c$3OOG^r15 z6w#hO-aV|UGmK$;T4SW6mP-AUT%oIu>HAWYZK7uL&4Fa&8k7N%vMaylyGls0;fCX_ z*ZbJq5JEfA%n#~7%Rm2cGS>d`=-FW3qi!wHBm1BEukjm?s?jYYl{lITl?R06t_>|K z6++?hwy>|zpw7oLN)l2vy-KTp6<6i#N@6R$V`ZPnM*2&ALa2T_yo_bNu z2Q4_DbgMlhw<@4k%1-+#d+UWa9BsK<&-+G|3XFdFW_`y3%_B8`HC!*>I*71SRXiU0 zEbW8+Ym!6VAAu-b(3K7pQM!S6Rcvqa(_Vy3VR1Y7myO$rp5V$&+csT)SUcEx_^O1h z$CbBLRH$2o6|ZU~#yzr3O%p4IPU4hpK`# zTQ{#)!LL0gQoKa?(CQZ_GEQ8LA=#L)Uw?uV{qh_vMFoFptTLO19nVC1D@?fW%?xis z-WkzB=kUNwFbBidwwi5W>f+50P-pR?JVrtU50^AypvE8=|qe zD2nuSyP+V`Nnh=Vj?;(ll8fLNk5u4@*7-G3b#zYly)1H9t9R?HGT3zHTOBUj2{DeN z9C4Brk4f%D2)KVX8Aix$*CZ^@(BzuB(+SZRaT)72*kiOZi7%7yY{BBP$LgFLOggR* zj0$Qax*pk?qhM8sjY7H}qVGn!dq9Sv=JuGbo{fnVvG|~Ef~xrbM(V{ugk0jo6tzFr z%I@(jOC}{XQBC9B95A(Sa-L}Vy}vH#i1{e62?g68m`zIh*{W=hVGJN#D^NIi%+e6n z>1&89fw?5~n8NKi5_&=*8K5MTTvc*$F$%aBmD7ZcK1)qHdLdcD?r+DSg)$n}t&&fz zgFN#JA3e7*CbS?wTp_4B^`PI^iI624+G=BVqhJBbvR#)nRq>I>U5RFl^WL_W$cf7B=^0PXB{n_L{_E>^P0#a6vI$YCVO3PJgnS*< zRH!llEanRkQ5nN%yNJoF?-aGxgqzahPP5YSR5GNAh@eaMv9#(7A4N61#AQ?7f5gEz z-!-tuM1|B*eWkWfs7X&;uqxb=%mUb;1=&FDC^zm$&{O*?V(?TUoSQL0^;uXMK+Rcr zs_cp+V|&bpJ$;C!F{G!*+gw$jgs(s26g!;0IJ?~(X4c25bM7Oy4o@2a; zg_LYcCfQi{LM5m{!OVj$Ft10ZLGaWs1DQ_Cwcy5mNj9GyFz98LW>?hJG7Mt!)=R)) zUbg%jXcIYcPqj`a-MWCV>TMTdpcvI#9-`U_ znGo{d*8xgQ4jdsu<6Ro5vU?cxu`7G%6vs=KZVsH~U?X6qf)=E0UGgE#4WSWi zOXbBiHy5zkMOAT^#`Q@3l^f7+?wGwLiRWq9o5jiX1}$37&x*w{4I;i#$=SZ)2lX#! zC`q0zwyJnT8iFxccEd-S$G?^C`6eASK?>>0LEpXeYWW&fJo91#`2Mm3CR(etdchfq z-tT&gC5_>^sx-0BmQjhMy=a`$YtoRFWn|;qCqTn9Na#)5QLrC7bq=o0`k}sH{TF@e zjl;xywfNnpgao#Q@%Ou1@24iBa8TJ@FNHJ&#{`iae>h;48#rPZNnaf>D)^d*ijd07 zpZTk+eBxTbYME&9b+U}N`EN)jD%C@*CmUT+up5LH{HDx%&9+?EelC@ij7#BV+2%a1(ZKRqywG67@t8wsM$_J834lpmnb76_*@-(70u7AS# z3^t^^o0YE{P|JrRhD9A1u2LKx(f`p~H$wO}k~&Z)0{ON$g@i%?$z;~QGML1d858nX zERCb{Tj%PyfBi#`4om_AA^I6;&o>|}C~^H9Tq9ciUPujfwKgzLr4+~Yh6RFdZ^u@| zb*7(|H2&d^VOR+=S%WlefrVM0cVXm4YCyYykuPiBpgYmxJ@O^}C>U%bI)ZovHGrVk8h-&hwIzw8e&(17 zy1h5&i;uM!Lp^#-A&Dax2JK>YgA4>Pd+pCB&-?Gcv+2_MK14*W@x*QMf(YV+0mQ&D z+q6eA+0}ZF;;J`LzDEjZgOLkAIAA>FP7yW+>}FLmN+mC^r`|sx`%K?`51C_k*sOJF z`3?&&c$vM70#q<}XyO45uDtp@gKKXz7bDxx^ddx00N%Z~xB2>=fpFCdc%(%*F6vH) zjxzhCi9|Stzf_DVej6x#f}~;Yp_>cRqewe~aZ2mzMP2QxU_p!BWp0-enXe|7=hM&p zc@g`*Wm9eN)rb5^hbkVWwOHe>zcSf$y?op4l~1DaS21-|As`BH6b!2_HfhttcsEvi zcl3{U(a5~~{R#{meB?w318S}DEJ9TPIDr9#oa_@NsSp4qdP_7{kg#4+$vdCtEw!pDZ5Vs|pQ^2Iv`${WA_N|aK3dBp6@@gb zkNGDfVP0CPxlSnJ2ebbBKt_Pw$mzV2i9Ly{63$UwEOa@Uw8#?2;9A%V1fzAXJ;77} zao*vSsH>EgeAZUMM-M0lrISo@#>y%jFsYi=etul+-G?v|wK?+jk0{cjyKlLKI){Q? z9t^qKWB26F>s31GKKxIeKM6kAmwYozugR9F2Y$~oqMu}HP@4XNB$c>lzq3Rxk= z91S<1OWyT0=hv-A$vxYKlKW_nN&TxIQD$DbNg)Do@#&pu_N_p{mc8*de<-kN>8Wjo zQTuACS+d_AzFc&Iu)O;u7nHnF!KXDTl#~`8fTqhYY)IZgCBPq(DXFPlgqb5`(VtqOi&+RO86 zx2b(Ud2&PbhNqXiQ81y8oYDHu6GD$V-J@PIZf&MnzmEFf-G`xZe)f3dVa@*zoc;G1 z{g3h$NjmfAg>z3v?rqz0HJXSL47AFHS_o8xz$aQg84nc|xR8x&Bmfj4J>+S+3d`?gkcu>Sudai)M3kJ|G@ zHoXYFmHM*+7=p{@2F@zb&6>misTTal#Np;k{sTp){F7z+C&>ZCS=UZ&PqT%T^{&Z2 zZjTY^Fee5+r5i>(SME`m8^P_e$1J_JD|(i(>0eawn;vk?V{J4}$!mm^y{ELz`6UhH(f%3nvJpZ4$kpCxU7Y#C#(v-Ea z#!2*=I0jTDllaJrj2W>vs}$88W_FfJ<4{cw!*M2+knsspiKMFQWOaIpgtF_{a|KOF z4ZCZEzHP!NjmhtJUJ^Ui=-s|n5e2Mf4+=`<4l$0OqLz+*&**c5L(uS+QZyXZYDaji zZUhPrE}f56!(BSbv5q#%7;DmKiISx2t=iO*gA#7`MT`7y9r`&!^AK+TR{V=R&;L-u zu{iz^JKs19TO9jF96FjZ=|^gGjGt#P&sq0A8*lCKDM^qKzF4_w{h{-M5;2~B=R~(r zoQS;{wtc-B8ugNPZEPDK=-zIkp*W0V<}UViwNkjayZA4PTN?~=#eKih27 zxHKgDLSc;mcJ0WPggp@DhXTREQw9u8+S^jqkPEiDr%gpklOOedN7nj-o$IncrET|4 zZ_1;0ZIgJT4=)?4+P+svshx=wbY=`Q!6>+@Vffxp^yt-&NoJ_w@Ti={r>f0$@D=F(Q`*s zHUvK@;xB*32~(F0!5XC-p*aZ#qgmqy``@rr=$;^fenDVE+h4V(kM~^mR`T|-rr0YR zB>BY*7)D%?HV?ELF=SngT@|c0Jq*;-?JgtSM_S+25V=Fq%=huWvnNh1a@Z?797bE8 zYcBNZiQ&S$b`t#&Hbn{_<1$8_M_X zTl{rr+-q5s-1C(RH&?CSY{fBYV&u|sS3H^G{?@65CKI{TPW#FI{ik=oOZdw8-tDbC z^(e=${ySBQ6>I`Vno2JK=^b zdW?!@%zh4``VfyF@(%Oi(YH_D&K zaft}Fyq!zq5IS}Bo&pkMu!nzpU!b6-(-6F(&V{Pf7D<%8zbbmF(xfgE8=-Ot8NYp!xOJO3H}h~^CJp-27nPkALi z{_&kQtZl{ohREk38;1~@Wq3)~$`r(4AiaE_{zX+bu|Q-st%uxINsT{753Lz+6bvjZ zho*e6$!Y66mRsgM%6P=03VMFo9QQie8^Aj|)SRKRoO7 zD3(P3{5B9O>YGKqwWg={nAm1J9v;g&UY{#B@vy7DMF0?gLVtA z?B^|a+8GJG@YIDhspiV;k*K2il()tz4ZHrxY{W9pC>4;Txt2?&<)lL+P(Ks=qycd5=3ZCvmREtOzZmMkvP1&;l*~&0y`wN20c?!I} z%hs6r-k<|(1H@jhz=jd9QW!`Es9n!$9!E#)A3c^Y$)G;1=V)e{<+B-k!?9?b`L#pq zg??i<-^gem`@?Q>Qd}3OH-N<7qke`Q>p8Xi-F#I%-@A-zyxOjgs)K?}9=#S0tneJ$ z34)R2T*l@b6Xlk9x(xe_MyEA>IeHz6se!uA4Sy+6AptXPgBtT0aB|kfGsywyyc)lLUH}TVm8BB^` z@GqA%^R(@|5b=UN@b@+1F6B9=;jOiFRx=Hs9aw3jJo4DVkK>#^9sRA`?XrKAc*@+j zE579;TndTkPtR+i{-J0!+d475D6?66YRxI+e+Eq%Guy*;s^G5;5Ajun@$_Jvij=u_ zX0=^dD>d+9Hb`TJmV=g;31%yjPUw={O4<$-W)z-NDv~b6<1AksG5GRod(WXuWwQCz z=W{Yo*?gn04I?A8ora?@`_EWQ#2>&p9>;l}rEiMytg8ai``v0DH{i zV3iW(5mknp?Yudn``F;hU4 zE;$kCk5t3A3r4hL!0sAcQhPlvT6&?UNd$e$B-sUzD9UfASo=iam1)7HsFeGEa4_0< z5Fg(aG0+GEI%?Qv227gm%sS9$3N;)zbq6KX;04oWb;>YYJoVIf|f|bhhG~|F!u#B zL2VQLVFBqGCAH(!9%h!Y4AAqJR&s|5H2(eKnEB9h&WnF@h8*klvKJ9&UJ=NT9kFIX z*=YHQ!G*se#Oc*9FXwGj;@EC5^Obco4R6{6pJe$}J;AF{a`MZ0{Nl`1opzxVBmv68 zu;I#v9ox`ws~Ixo9Ix%R(=L5@ zW5Z52blAy1>VJ(2XI57TBz&DHT*vK+@jd~ri%a}U-T1PGI!%H9nt$`4|Lltq*skw> zXhIMhx~|T9^9yv(aiqVK!9|ulEl@K=kmv0b<4WPF031bOBW10y^~WIrLl(12ue_7U z_|CG}dcmNNW6z{{>=}1`Z%-3kD;yf-GE$Q+rPjQWS?%tp?4aKdtXDIy_2SCY>LCYA zMDu&2z|Mb^*VRLynj>mrO}u$#;Ob4(0eOdc0m1YpQgBMQHpz#~VWmBW75CVpAAmlSEQqUb0O2&3=k}IjFQwAr6I?596s^Bd07^JLB>lW%pgeP6NepR9ApJqn zwtong-iv57brxuE4yiQgTM10rFZ|rpG}SjSPCaKwBh*1F+b-r<`bM~@@Id~J3VsN) z9qJ5xtxIITiMsJPIXlv-USG{kEIng5xrkIpyJYopP5#09UrO4G>KkOwLuR2>Bw1pX zdGS|IrA5QFR9P*Z!EYC0e}!pJgX%t5Kp%8S)xMedG$NN+f~PRG8mYdmHIdi@1vttB zi`|IEenikNX-AuQV61tg4G$EtA(q)q-hpLIklY}ycm);w%f4l`SA2Jgfv%GfVnz1w z$3Xr69y0l+@0)!WEsFF6R;Dhg>V-671E_k9h*zKjDmf&e*^;emx_RCr_1o~%&WTJ zw8f!dtOX1>2$qiw%yc|@s^E#NTw%tDCJag3OJRLELs{EC-i*TL*k8DCJ27O!H57J6v~C|Jg4 z{cI1SHglrRst;i{(<>hAs``4$Ga5zLi|4(Ek8BIE$3UB0MhNY!2u|bzOyxW@Fo71o z-QpOm*y>X9QZ~-%@POQlxbl5FqG|^PM_WtPi&c+$3p?6l(sOX|LSz<`V&aGnv$q|~ zu=qu66F1_jn!&8OEX&;^;P{#>SseT>!N{;p2h^7z4j{m`WL4*T-b-K+hRD7>09t^| z4wxG;S{@m>+_mx6H$AmPSeuELA+p&)upzDNcE;M<45F$x`~X2l zV!L<9nPlD^mq~FR_rp>QG}1>O@76uFd1-kC=yKJQ+%|(JIH13^Wxms8gsHb zf+d;qe23-fIeEF4OF$qEpy)0OrPxSrL5th|50LpZRbHlr*3KFZoal}tAawxX=jrB( z8((HKXg)K`-`ZNC)av%oLMh)_KUcEVrbVbNjad!6V_j}9vlx^ueI|3tYk~a31QK`_e<{-=4f#oa0y56f_aoZH&{a7Qjsbn%Yp*MFTgTz0jmD0=f z={iIDp~zxSxy-S?D7=4j#eA(Y-?M57a4vx3y9?V8BVU_84&NQS`z=!VVnL@v@3v^- z3it6x_if%?NSYAQiqOb-iix+CyW3iH8fwMLO%I)mrR_1FE^S@;VG}$0x$rQbnj_)Z zuiEvkzLdBSkyN_ZZah|Z!e=4&t!)I-k@UCFV$MA|_3O(( z&Rvh76+Y|IRG@R-QOf~MP0qv|h+#75{t6dzU*He1dI;L|`9|+>60Q3Jmn`Ipj7Z0A z(}X7R_(PhI{T*hRvhk?aR2^AUO_{Dp-9v`Vi_WK_ge7QdcKwJp_PHEAa4rX^+ZlRq^y=2#l3LL#o4A*`e#_4nK9K!?T0m%jJ17e7Q-c1Gxz8M=q=TH zx#N%V5Vpy9`6@3I*+F(IzWSQD+=wjur1>J$VI1JJQBNi8xCzmf)%i;NsP3+IG9-;T zT$JP$@hLIiNzsTPAXd-L#&OEIT$sw)OLJY@e^ZzFe~EjpooG*7h=jf^d_svnH~q{I-tCz-N3V zMV&=fa-om{WO{TTY0jox*SXHxgJjSRn&5&R-N-tR?H6|ep_wl4T2h;|W=xp_KvAV^ z!->>~^|7lbRJ{&P9fKQ3^FGS9dY@?d!mN(Ha844+{=sy;!>8DE*0P*rpH>6eTF~7; z=kKQ3xv}sW%BC%X z1H@NpO)`J8#u+&|c4HoQizmm=OB-I;4tR9B690M`N{*NETR^4iESG?sLeRgy7Er!} zbB4ERb)U#aves}SD%9rR&I9t`z3fi2Vsp7!ulAOG4rm=mG%pSA07w$pqL*!9o~@PO z=#>#ysOD2R)xE_ZPZr0Uu+z3xOJ|b!^Y_&ot&{QYyZ zJNe_d(q(?^vLFa&0VY|{i6xKY$G%!ayF|OL6*54PZ0Ze8F=v*> z2k;e2w;_{sNk$=r*&#`fNOrFG7LhSXGDP38T@$O_JNxF`xpq4#LE!`@1^K-TNwQ&$ z{E%bi=_jNZrkejK3$1l8V{ z%3G{dIIBa}6RvZZ2OV0v(HaT&HIi>#dgdey9HpGi8a%?4^ibJz=Sswv<8|_Y>gbTgC2voGL=G^}Ft@JUBUZ{avUL zyady^&ngKA;h%B9UdB+U4uKdgeIk)nufplLHXvW0=<%x-V z1aekC)V$T|9X;CGEkFvH9|FgS3=1^+glt=(u!EaH<8tAR%&H|7oj z5gu5Y_EToPrQ-fM^?uOkX*8^Qdc(n1^`v7t^ip=M26%8O?b^e3>+rWt-G_#%1O!;= z84i2hCVLmX@7mB3SqT}|(q*iOhT2{ao$QpJJX3=HUQKrHgIMiPGp{(i&k_LDsnB4m zs&forH?F&wDL<8D0GyaVMLV<*|fo>3f{ig-A}^7huXdO+0EdK zsg8yng{$wcq>Z5ItZEVGn`o-<;|=(t<`#9tsRkKzS$ zTLBj3MjyK|Z3X=nx%F~S#FH#!0(*Zz1!Lo_+(}XlI6On zI1=0n( z^!I~vJxK;ka>}GQW$k72DbP5DCT4=8nBks*;Z~^HVp@YuTeiHVkz4AdTURJ-qBMe5 zuZ!ro9~rwAMwe_x5T7>`>6#w|*5l~3m*A%LxaJUy(}?Nc_*n#T&}gLRdQsXG;^G2^ z>0&@Mcr2M@@nviPf$j5zvqCANbCKf4e?p2IKets<_Tp*n5!8xy21rUz!#XetXbVAi zF|WQgt0f=x7j>PB$V1w~0Qve1(D(^Ym@~eT*+WoLKgcfMgdfD)f&Zgh&zdw)*|A|9 zs5-~vwL#=;5=vA?3?+c{1B}{WTA$CxP5bUH;#-+iLsEKogfQ!`K^Xcwu=p#3$c}Vt z*nJZuINePXGV#1}9lKG714DleSC7d#yg7dSw}|GqNDZ`H{tjbs*8DGHN_)&a7&hZn zxVeEj--1-|nfbw(r1&HZHwWXwh4w3Lw<#kLB`?)WT!znbeiso zW0QjMRrA3Y6i-6}S5iQZ|6{@%0H)g#SCPB<8Ssy&xE}7qeN_)VU(TAkjGYW_|x79}YVFd%zR- zy$w-I_P~zjE4kzGR4+MXabuF9TfKLHjA(M}r(J8<$L;5HQgBgMv4WR3#TAEY)tjvL z%qNTb_0d~By^u5lWW)wPY9i;_91lw0-+~IdqnBUJij;m4zY*5DkM_}==8oXw`&2sH z#lb1!UB^v9NAOnoBwuI}mdeJ8AWQ+3{^@$r`FL<|2K=rr>n1+3%pHOrjzrRzvpVEh z0YuY09A!rrO{0-&S?Ita5(tpDaKj&txNwVM*9sK{6|F+cRPei988JfwND`!dEE~fT zfP!WdXr@ahooZPIbZSN)=SK+-RPh+qVF)dPb!P~qHC0n`$p_eJQSd5G2^>iN4Nkk+ zP8G^?u23f;D&;W4g0Nh2-az&gQ(ym2fV2T7SAYQ^r%T+Ly}MU~cExA{?Cc}lY{UwHWfjOpL`jZyc~D#Ox|&`+ z^;9ajSq`BK|g zgn(1_EGIs|ah~QOb_S3uYMyzyf-OR8{rc<%j(URdh|#rxbm3wDs{zw(f8#Nz#IsYM z0m))n!-oz|=q}orCBB|U;b%F0NE;BZ*pxOK8y@Fzt{d1TtF~p+?UP*m$D6!lhs?aq zoxGkL?0!0z`Zurr-y1kSk-ZIahacV$guV4IrqB;J4w#cC<>_*olOLX`)q}__%PV}V zAiNcM?e62VQ=JyUFF=b%RTyv=?=C51@KiSE3<7T8h}Ezh@QuRXErPQJA#=O-@6)^h z4Vji-dmOTwr_J)eK`s$+eN2=4NkZtJ9Y@#|8E#@29Lk#RA?5g=f0#Gi&fG?v4hR6ZoV#1E8eDd5h{y!$H|4T-wKzY|138dS;7>5_9O`%6j6;Uz4$ zUOp#_KCje1hl&4GDFA8ou-lJ#T9QFNlroIceGb+Z^KxD~6r>OlWoX@Xem9Cy!kcUu z^r8#Jugv-0KUqwTYGyKf&^PuPEI=aK-x^g@scReg7!L`1iI>q8*fe|GHw2{8r#2^Xs?zH zJMJ&ni0tgO3Lep1kaEWvTj`HwtpPbV%nH7<&pGZ_t6;|;nNxKEM&0Z9pJ?9dzno`f?mn@#T!2d znue$j)0)8vOpIqc(l_A`Eoe=ShW%KId~$1;ainZnw)vRdL28e-)1`pRdm6LN`^zLI zE4}gFOk@^DPQkwFUg=EIs&ntEcg5V8I3wQ~bgCtPTHzFXI#z5%C!liWWSSQJtC*Y2 z?sX^!?CB!nhu-WrU-xWq*Nr)0k&jann#zrFrEWBENu4=2H2&6xxJyvYBqr2^1iE{c z9yc2gPdy!RiBj+Dp499nBPEJjAm=n@LxL4|dkMPB4_X*dFOFqm;pUAS0+U7dBPOAiC#0AbZLFlP4r+jv9fzc$NnnoTbcwv%GfdZMpo@@Z9(!=Z2RGG@(6o zCF#3*EDT%9s2{2&CrVgWMJ%rc)Rof*5@wWzn~Qnpge9=6>XJ4X!j;irjNC2fF)L;p za-t0RBj1#qrf*=v$dq&L(-QrB>!@yGf|JW(!g6rg5p*%%JN9WW(4Bf4IkE6Y4(WcB ze|}--f+Xz8nEfR$*|pN1TRYdIw`(^g=k@oO3SatcQ~WgmuI@Bgqh*s{ZT)s=SLKCN z%j=8-e0*n7oO`oo)^_0rxEHKDoLP@K8pKF$9vi7-))3w4aQDjS0R;5t3)`ndfvypI zG@RE2EysJc$l5qyx@Nk5!AWR_6=t$G4&pY^CpV4&S5MJ2etv%}+@1*q4uo6M2M#2_ z5u;D0C8-J_zaRBM(uvF{1B3~cfe2F8Nup$_;P-L50fTLX#aVsOi6nLdPT<4Goc`wY z-2g20pC`}*QZ#;+Q@wm6T)+1=hqLS}JHmmV>h*vRu}~HH_L$Ndk(iHMZM#Egfm3j? z8hJTPO=htl)UFLQ%>!gUt`9o!muq1ytRrzLq6y6J?>J&^ z(~ZR(hfQiVqWftw!{t@2+N#ZB8Aae?f?(yDsylFR?@5G8R!58%a}?&*kwl#ED8*1> zHE90W(N(ihFJ9xcDX(8HS@HlXR8+i~kwD7Dv2?uX2E+`wv<@h*K*%6^_5_%XcN{4HeYM5VKV~R9YXgKWX1>1BTD$o6tlw zA*?#x1kzr%iN}&nkai)+BuV2_JO?ypy&6T)q`_Pz;HEhuk#z; zE&<^CjpZ!UK5291d#Zn*7ZuILS^!9*{lYd2wn&7X-@0~ghyjA}-3=@BBx`*__UxN# zftYTzf=9PC^^Z^m^F zApuUAYSf-`TD+Y2r%^AYlH7_L8OpPi{5#a1xf2g`i4a}Kw%^F1M_%>UdN|h`fU@w> z;#ZO5jeLBE9XK~5&XOG5GF7Ho)oOlZ&Dqft*t>*E7&)XUq8(`?> zYn$f9d-H%KKKb1*0>)YJ0pO_fC4!NfMK8**@>Mk<#bQKjEc?BI25szKkUH&UWM$ku zmd!;};V78@ANDaQVqJ<+s?hD9HexPcdt>MKq|8N4pykRWM`aB6JtEL_i>gbtHR~e< z>TEL9Tbva|)MaL@G5lsLclBq`PR5(4N|docko$Nw{Ii?CCN_eSA0byg=Ro4z<$$b8 zv_p4VfkN_LlR({#BV#q@N)riQXl8Y`3bb=OPuuZo*yF={xy6MyZ590BZlu7wju{QN zm?F{!$$HpNITBf&gd6SP-YA8y76d#4tc?{idVE~=IsLRcA$`wpB6qNFc^ z#F!IQhe?VEMik6T70QvnwVcZ&d8Q78E3pP`C|J51Yj_-nD9cQ7sUV|DLb2~7L3{7{ zhRB4obe3|YGRpX^klW)LA1lq4%F>9ev5z9GWl_T6Bhj($J>GIR##+wP8td3q|CjSX z3aM?fT~h}!=-0^lf}I$(P6ma-08qj;ZIcn$`1uB?{^loS+!bVQe?BR~W20w<(n$$V z{{9?Kt?I-x^I5$jDHF7Mb}9Gnk1B^WaG4J327Ecb279C!-$BMRC@hb;#i69zrK%LW5o&PUbShTBO+Ma}gg%KeMveY*%@vk5*mg_E9 zTznHHd*Su6MYjZux(}ekOxr46?|jpil+`7FdfU!q%M)vV?Ap2W(VIiEx85#!y9(dD z^Jj)rokF@qh)77l<;#}?LY@=j_uNUMUG1@NlalIh7bk?L>mCcUL(=UUtlAv9KIbaw zd?IFdg)^S|t6E^TpHcjrv*u>jPz$_)_};VSx+1NHxdzSp>zy>oVsnOA()IPPukDAX zl_H*tEROVj{snRBv?V4ea9vOe8I>~#Ct3|Vbr~hIVk_O4$b1~Q;J1Eti!W&0U5K$W zGb2WZ(7#-4;`{dGqT-6PX;K8(v+bng7uP>jIkA%K{^14IiW|TU zy>3fFh%qS)>8qjNCS$EC;5c{2kG)hUKS=7W{m{inLaV(!7o3(N?FcOwy==C!fBoa~ zptr*z#M4Nok`k5-!Su!9X-}k~nfdqtv7h^!pEOpwU;Md*u#cP>F2^5Pas7A^-;VTf zx#nw_FUvg6mYlqX*`8$6lbq z>->IFQnP2(_5smxT)}rZS^@7|j;sJT^Y(ztc6k~6>iQE+If>Am_Y_k0rUdfkhOQc~ zQjygc-5onsTF>}FVQ?2hw~TH9b8K4znpqxXGV~=@*d3!7qHqC4s)L~$VCx$K4n~Ox z^1syX(K;9+msYsBZr|zeYp5U$8x{xb) z`^)=I_LLpIu3%U#f4KA-=9)is>#-$A#J4@CzPJ`#d#-34F}`}>441H$>TIF?{kvtA z2pF2KxDVmXdQ4jx`7M&-aZSqM$=jG$%MAAyh55!S7nQqxwuuS4WH48H|60kF-ctG4 zs7H_76ezh=oE~-@voXG<-7oONl**jJ;WwY^@maZrOZyjQI+{J;E^iXJ1Q|#V?{=@@ zK%st$RlJWkfQx;D<#6~fK=OI#l(O>-9X>>TAjUH>{M>nfTSpg_Th%NbtCFcNYrm&*n7qZckd++~l>CeZ(MZ;5a z%gf_0JgUOGqVj1m|6zs67C&@mg;LwTZO7g@_A)kyoD$y_7Z>v&u$;|Mw3p?u9SJ3k z?8c+hconswds6VlT+Y>9!^z)PK!3`s;k8o#Dgl=-r&|thGcx0!lIK_82=R+@QQlQw z!>oA7t4A|rRPdsT!XTzYwaZzVIOAv3{VTaLx<6!&ZK~MRV?g|t9v3^5R(*0%711!O zSq*cI;73z%bg*R@+U%b#sa6yg{}m2sz-q%H0~SV1YbsX;rmacM6));1kxOWSplbu@ z(gl2%PH|MdNO0Bl;~b4tY%-H22}V(*1o*D(7+}j!*`1>c4bO3`rl4am!2(mSCpDbm z(m+Eqe+DAdfHvqI_EYY||;ep-FDnC=DW(PB@3!V)94&zhXx zApcI>vPaeJf2vq#r~rq@*tfAi|S#FUbA zh#1u2xg_Ha$1`pilurRCo>a_?6I2tTQ`?|O75St}M7keRWf6|PrCZ#iJV=69r1!V3 zynm5v8a>2&zR&IzFQkYq#Z{m{CGd` zYFFdJiZi}R9a=l0ngk~vmm->-rlbRKVK0P)3VtS!y^SGVbNlP;P!;{p{r)N^&g-37 zw72=25D81`PJ*y#O6ucA5u@D+j`+Kcal-;xKNEDJiFrRi21vG>x>EbU3LCUHk4@~Q ziuj9WG++mBYHGNRlm^-StVkxu(Fg}2-(IP%Nl{}6CSN>r$!^Mi0%M7mx$pTCeHM~r(Xs5CPCM2l-yOXQKsS0^%3gaGL9c_CAt+hB&j+Mwx(H>#pq`}md?^w zH_`or@58Hkx^e946V*t|7R*&EhLCm6(-Yeat_Vgb^YeW`fcr0a9b#!YrKYCyXFX<3 z5Q8~H7^c-{qbd0q`l7A@gc7X1z|g#9R0Z$-0WNz9bY=en1hibM!)LHQg4-)0OaoSn zFtY8(Kw7vQ=?%ae4f#RsjCSFRf48JEE4W_sC5kGWKlGt*qXxM&VgipOml_OhczU8G z%O;4rF1j@ABkN1a;$LF0IM<&18mQaPhDJ2A0{_#d(bQr5z(vcQ$A0BgrUBm#8wKdH z`R;uNSH7UuJpV1^3!$1C8YT{Fzmurjt_>jAZ_wBd1^lZs{_=m$hY8agR;*s{XR3D4 z){MxppXq~H{IRYO%f-=VJ@hf_h#yKT22TG9m~p(?G^PSN)P}4SGtU`M7TaHPv;1NN`E9I&x}^4vh?=CXtr;?&8L7?R8q>AoFvap8!b<2*F11|OeYtWzdU`R>sq*e;t_TB20pFye zwF2M@eaWpCRL9((mB00UUUxz7Pc6&f+UmmgqY4x;UL^MKxcdKs?uM6|l^jih?5M5X_65c?`oAR9Saj--y+)Y}j>wXV!@qG-~HvUv{U`}w~) z(Q-5QG85@|H4M5WwZ(}J$N`Moa>uL{fc=`!bWW^H) z%z~^G5}Jw=81FW|)a3e(auxj8kTO?CW-MBhnTE#uA1Uslb{2WB@FNhnbi90`3@^f- z1AUBpR2%yfmJa;MW9H^Um>?=HlXkPvWyjGg@L;P@})N^(ESaVM-L%3Yg>g7_WA$3*h7Qldqs zq-BL)LQjGm@)GpSCE;1F%?F^3#snitJ;&U1!woHraqImJ^?%?C8lgkc{R+MHw?kW> z&$9s2K$HVMoxu@U>laiV>0s*#5$gv%QX#~0j}Z6gj~THTg9;=R^~`}&?v1;_v5qT;xbf30s?NwNnp_i+%MfJ} zvk_(4>Rs_bhVlva6#mdJ2x(d4q}-Pdnb$L=+}k+tEEbCFDNAGB zf9`)Kp4b;PgiZyp|0yT#oDu3YFRBeZ0)dyD9L2i#vEhipEs&Q1*%_BfDbkR{Tio&Q zp=#eA-yF^-jKPtW%T-Njg5$I9$jH6Nm4YVxWyVrhIP&nOSG(|wkh*;@UftY%y>u=- zYjbJzV8b(HZKqNDXgiCVAAxlkXaK*Sb-OvuqIP%7r!_6{;g~e(|I^x+$2E1e-J(`$ zUq#wli;6(4ifvU;5KyM5IJBZ*oj@5PlPH5x5h5fcDk`N`QF#%|j9?}fTw6bIDbBy*dg7=>Mh94t1CEw$;>bTj_OR#WEHpijOv99P1`@py z_*d1z*)}Y~fS~j!9u#)e#5>sfS-5sPeKS^jSxyw(oZNU7^uWYA2h@zeQ7;B{YIt04 z?1L>$wlD@w@H%6K?mzJ5)FfPaO%R*XmsRGC5_u%U`t=0OI|4nA1ye|knCxX+Pr&Kf@8QAR z)0qrxNew$MCRR%kRpbfkC5>-(-IM2kf0{FJk-l@Q@Y)&bt0pshcn!PRTzUm_Gk53n zuKevYs$ti9Vr5s)DDGs4UzBj`9AwCTa^=hWaHb5Jr=1|os@(st=D!~`5v-Q@^=-xi)^BwoQis|7)0(R}$Xv~SxVduPJrVxOBUF_Fu#a7k)^ zx9AXM-$tQ%nAhZ%W;^)x-%H_${-_M^Z}_?f7xL`#qx8_x#{j}KiosU`UnIzB$cUvs zeg`uM-<^xKM?b**5ZKV~)8TifyT_v5p~sF+7+!5xO6X$+E)O3h%ZDnSZDVYQ0)DVcxi{z>xu>rWAHqRwYYp%wS8*Ihn0ro9nsXY zAjpPb2p5Y%yJMt}A|bl%tAH(l-$taeaxyfBREBRHuu7i|N*DO-LJhUeXm~;^*O+m5 zF$Ygt$1`X7l5uYg={;s?nucrF9&*|6cgPEjw%U1J1b;7Q8c1$-PS}i`ByM0Go*&PW zCJlwqME_Acnhi)@K+J0DTG`LT>c*{aPM^xqWuC5Evy%$yMe5G+$kcAzLOoLZPV~c@ z12jP&S;iJa(dkn09iSPkUDL;b^ZVU=CApjz*w|rA3E~zHFq5sWheQR;2`zu7jZJ)W zcbSr{*2hh9s8AO0qva&u?4F~AV1KFM|LVPV@U#3zS1yxO^30BJWnZmyon%5G2Jg#A zNc?~?actjg6)yXaLAj;?_c`)Hh(cBT6BZn%UfCKr#=u$$f+1}B#QDVpA&zVy;BNur z0LXDb-KySuE?}JDK~#O&nIHv|$28>A9yBqqL$tXwMx-9jHXu}r2P>quflTquby~{z z`fh#m>I-sq4Wh-}8+rX!SaUL=gRnzEs(w#v8~UQn}M| z5{B9vpIB7C>sdWV&^pqj&zr3rr|rA>yY9&2E)WKuhtRa6lwq=K$+sPHtp`#8Kz_D3 z<;lGIw(z>w_WU%R5Y(^7_Er4qla_jCr7@%DL7ee!l%pNm|8v38O|2_yZvBxAGw}2^ z!O}T-8S%H9dJH!S1bzf7ZcrbvqWB7uIJEI*Ot2VtvIUzb=$di<*XgE8ZA?V^Ep^}4tqlbp6}8m{+9qES zxtl_y0q7D6*h=1#<|;)A#QKB89ji6ov}z_}5G(mEbGyy7zxr=s5?4o%kjB5z(1$vA z27tMZ5-PvMd%Vi2eN$8b+}H@n59SV=7e4|@T6_7MD`Q=*$P<8f?l`&ckWZ`_QvPSd9xMrV-yuq zL;^e@q2?C2+5A>q5Qwss53nSGNy&o{4`xBX`4>M$$w$60=ZYYc!}COu;>9xWp%c1=K7^Y*k_*P>GZ9UFwvhHZ$mZI zhF5^9_OD2Wa4>*do|i3n@!21&;N3TDZ~Sn#_Zm3kad)_y*DXY#DP9k0eU?_T$E9BX{+PvF{!2#kcBmf$r zMMg~=Jh!O(U6noxFWVl+p_0{h{eb8gSR)1fR(+J!sQ>++{Z`u(4XJ{Ko4-Ol)u^d> z{Wu)+M!d4QzDBI#=lJxQpvwFt{pxumIAMq9%p41|f6?$rKzF=yTWCNwYULkZY|&`x zn{U5osT103=1+ejX4#Z0=0pp#{@TxTZ_RJnR9tt~vDJa3_9xKp0rF&WfBwez!`qv= zqfu1VL=DVoh?g3_Lvx=sk00ufKe_-N(&_W&UHIz49{WA?ma0b^f@Oofzj1uTtLX0I z%Q7X|gu)(7P$!4)s(*f~%5)4%)UG##r#AGm^=_Tp21tKxiDw<|_tLgb7tc7m>3N*x z5%%0{w`uz5SGFyBj+k~RN@$0V)k!g>UkWReiB{K4Qz87j$wr z9cq6Q?zm`6X!)E)ed3l7V0Xom{F&oC{t1HL>aMp9v=?(?zPhs%x#L$J?S3{=y)Fgp zx}k@ef_x{wUML4ZloAY)pVfvN*c6f_zL4)ySID*-F0Qma&JiR_&(sL)3B?g%yLI_z z+pU06;CKJ?HWdbvN;JNAFH2UbyV(%&PN3Vt?kc~3lpz280|7vfK#f>haZ^Ve;BWXt z+uIdq!&Z9us=Wcb0$`euao5udEqe7nbnW6%0}R-vHk{tGUQPRvoP|@F%AoP2~IQTxrvICie?)etNIm+|KgIW^RqY*Mp<9To86ot(kj z;;%WS6VfY#8-uj3?a2? zm+)DlmOxsRt@#>^4w;wv>$l+;|CqOch=c{qDbKV8dGwemwpw}*gR21l@f=G>+5Rvk#K5lrmJu!EH#Ws@lLYJ|kzo~yJS0OFFC z;m@``0qp?Oj@C`#tIG^6g+f1dP;N2s5LU*59mm)5(2-KEWRxo|1N!Ci8BOQFOG1Rm zyRq%=j+W*gISwj%9YE1L8xq{)(=_&Y?rGl!%o0tt>lxt4Vt`MkwsCh}^F6J|J?JK! z;BRO|2dJ{G+;9yw>c`+y4pSbGo7A9P#(r3$5qCof&l2~OyA_eF6YP2O+pq&5o%mbP zS~(d2KX$#DzGA9E0lrK6a@l4QqHh$p(auiddegIIT0~(ci=XF9ZqH|K4_?Dl*%M$v z9Nthe_p@sxn^obsK23nqWleDKS8W^6yzz18pZ2T&t;ULf`Hpb^tvtF?x1)LC74A8 z$9Qoix!mB#Z!(>*;M3b))wlPg`z^5l$Mc}^IrMVoT`=OY2usC8-m`O$5$m*JgCAJ9 z8LY?0oczJt>XHl<`GE>&ZpowOQW{E=e64IY3ZY9b&J6X$FmG^;^JG zbbIw-z1m9j$mZ!OaD&lYiejI6wdalU8vzP(5X9^>_PlycyO9P4!y*3WWAPG~zr;HM zF4>ht-vXE&WD^zGWvenaz=#lrV{qsv8EQWQw|I`faBC%uj*!3k2%I3_MgeyMNjfKo3t zKTG+hr7~Yu@IpsAA4aSgc+_#1A$%qiPk`Z*UKBgruVUd3$&y^ zovgnvsXgrVrdPO>{Iv$J>n9Rlqt;5@Or`hfmhk(#0y|$P=LabhnV45ml$FAL0)RP` zVI&-*H3Vo|H%WRbbq;Giy?;}%F50-Czsk5|enj_v+9aXs$l#u^C=>rWrL zkU7>H(R7xb=((b0(5~HK_na4*^Yf);apKk1ee~wuHk?eTMI94E4mFq@QA}yoSNoO3 z-S*%J(K}z=oA8ST&p&zzncp$8em5(*9%PQk0h9vPg{hZdc*pbEoy{VL=d@Zg?m{1f zK9{viTuC#l@iTr{A)j*dusJ(nF$c;}fbAlVaLQlY%R`rRX<+co`XoYs10}0xO!FZ? zve7Y*v-r=?oH;^F?PL6te@qs)G*~p~bG=wv)`Rivgi_dq;M!ZDYuLVEthwk40E7x0`y31I#2?>(kY@*Tx@e)=z3WG&*vK zWN3uG`wC2Ic1^I;7x`0O)WCss|A&2N` zp${vfnt&-8e}R#(xz=19Ixmn@kZ6%y6pZld{vLrOqNcotj$mJusT1f9RNLSx)LNBC{5g1h;MvE1n!FEp?aJu0U+iJN7-gDC81RvSDfM(EyYANLCQkLrj$ z*A@Flz5}=98U2poQ*$rc>Vu)9w)A0siq&|D3uSa+dRobcRMzkYjOPX=)uQwjr;67R z`$8CXrNiE?xP}IbbL8PWY>qI*4Di-7;f5+j6#hE&N8_AHIbwjI&L)2QYMX~;$dF?o zvG3I@_tDfafdK>s0%bSRM1fe^%yTUQ@)Ojt6Hs79nr7&S+db;(z4qR;gFDw^8@I*l z@T-9z?%s2JG6B@ZS`VRl1_SYlR1u zT&drQ4^k~oF4pImlY_dS2<=+!`Hh%*;^xj0oggX?rhTS~C238|I>mYuZo^-65@L^|GQD>EZ z>L@;c|FT;Fb1-^J8WaHo0Sy6Jj4XmW|E7zcb`wM>NoxZoNG?+q0+}iY7+*wN5Z59T zstI$p89`W!mNpck#uqpCw@|ymIuDTzG^s&i&83}_-d82E29|8w3!h|OA(3hnQ1gm) zGbN8OLIXO+=<9>~T~5D`K63SnlHpWtiO}^R3LT|^G1TWxrZ9{c;Cr2hSiME!LII%m zQty^MPc|Ut)dVH`k0%=($osASYfqZ=+@l@lptEpXlmoD@tYA#Col3S{A%``$cD}3! z`seOQ(%#%8GDECbEk0@10=AQ~irPcskxrVrAz#XK)Blvh49Q6~CiY5) zN+U4wi0!Q<*ko!dQZ0m$no`SM(icbzAF#;HS*7RLyN7GCWMjY_rK3n`6qsYdSS4)$ zifXp9hXGMH4Y$vP2jLj1;Hy*#@rec9tHQjPfgW|9i*kHYf7#j^^G~LY+gA{jFOdrA#Km7amh4nF= zC*@+n)2@1p4E*JYI=j*@O(0-X9O-vi;|^>&^95=6Z$@a976RXK0X6r?6=o38XR17? z^21VVUS@HcS#dy_!wuoFsRnPAMg>Q4@V?g3yD{Fhiwv_cD6}EHP+mpLW-4&;o1j?Y z5Su~}I!GioNh6n!u$}5;MBgrSn96>Jd^4r*HjTFsg zlIKGK#Js_wnDG~p&V=%4W+6uJ+KzZ#y1yq|{HTlE(SKY6o%RK?C_?Efy}Mo)k)n5a zc&CNl?x%=pSSqN_>c*kLzAOhk6Z^7XdPqr9!d!&^Ou^=N~164s0ISW z&7j?N6Jfw+E>RR_D0{g&tLO~V9H@Y$$Bn#?Q_PJ$r?uh!43$DjUYFryd>)F|n@kUB zG=V@Czu$;;S9OMg7#&d``E}X&DuXvEa~kGT^;Q@I*~p~fz>8bpwHb^;+HW*`bsK${ zjkWA*@g8WWc8^tt%ePu!n5k*?Zbf=%rZ`w$wH%{;_fd7ugogy=+aDE^Ou#DOeE~Fw zV;gC&)q~$+3*=VVNykk_rBHjt6nF!SB3q+2mCZ`gB`_@rxyVp7rM4^L**Pd9o{NiY zB^I=JH;38AgaEg1`E=vyrol7LNEuB<`coD={;}CWp2xk)z7ot$J?_WZZ-R(sbH4xa zhAmv}km1&j`(+7JbRZKruR*^NdZWpJjKQ3nsj!&J(`K)u%VrkC!duxAwzqC7%{_f- zz}P%Jl=9dQ#5T?E^_;gYF5j23zOzSIxP) z>b3JRi_^sXl-u>iM(^ zW`EW9{Q6f~#!gh>>{1QE&?f)zNfqki3h+tnX-?X2^qo!Ux<2F4-s;J28a7bx-N2nU*qc{wjX#3zNc zQ3yQj{H5mB)JU59as^EOh#5P+v4p+gO_k|}f{#(=Um)Lr zDjFbN;@JXtv~F7x!}L{oMQF45r2XZS*&8f-Dor)hb<(_T4F{khIlguYM5G{O2c>}a z|HBpowLig<_nZ~VzWO;64hKZhuB`eaa-ssM&4MTnI|P3C+?@GDirc@A-cvA(E@Go` zBI7J(zBmr7;(>9B$1FhNUr?-(DTDnwG&UJDc$KwK=s*>91GNPQS54P@#KYy*Dr-xr ztpRi?*}lWzRL_Ys4jalQSA%Oo{?fOdm`>Ho+=AZ@$bss76X=A z{rgC2HHQOm($KY6$NcTyqWziJWOI*v=f5h2~PKw2&o} zXqBE*Saz$$z!m;wBRHi%;e>*rp*$v8+^oXK|0Pnla~*rWQ`YyMJZ9$LOmeMruvq9W z=VIHf)zs#!hV4O0`3)jKoXo|~SD{7)WI_!msGoLhL&y_;Khul09rBnraPOAww4Z9g zn^^er`t-t9!rK~5XhnXbb%4MGXO~1M5^%3;`~Y@)T5_^46k@0T0ZpH8|AkW2PyNi-bkCDZ zaijS0A@F(1^Kg3mF$FTNnFQ+73(oRywnvh-q*f|^E?@FLPJmBH#yeA+4Sp0&{3VtR z9j6|u@W%v5)1RIi9w{50ri=^r52~ph2Op=KQnC%WhP{C=QKAZ&<78_8F#+qwPLEN| zCXxCFWUyg`PS#%^j^dg9dP}9E{r|Zp=A%O2|69hvgk+pxEr@m!=wyH`?lw@hA8ks$c-@d|H(ovQ2nf#P`bCj z{_1{jgHb?q{0rEashCuGIsza&Xx?>=m|j@H>QDv0!^W&!E!wVmDYc9*zfrXi0;Hm6 z8TfaLcbC$dO>%6eQ>O&N7feT=9K zQ*A~HsK&f{%7KzQ9K=3ZuLq(hNM;?)aXps}MQD}d5@3X@QuT9%#C;lFoZg??BCU)j zdeJgBfRiIWUwn-TOvH>|EHxHj6EVP5rgGp1qQ>b66O zFVw|TCpnU^0IrUMf(ip*B1sN;`%Vb|>ZR@rTggj2CGAA{{nJiob^Mf8^TAFjWKAUJK=Y7SG8YFZDw*$HzdAE_26!*5 zsC7AaJds5PQ?Ui?ME$nE`|rR)_!>4nOw4j{$jju0OW_ui<5tk$UN+Q4cTuD*4(;|c#AI~BQ0Lr~W>Gs3&&dciU=OD4`|R-mLq76U~#Z0#Ev>*m0! zGY}&HQ(DuSlIM5d7L?DqajSNE(ncqq3KgL4%WpeXrm@N{;lTiYG#x!&<-uGYX?bR# zRcP#8n+Q%49O*G$WtUXKrHL_=etu+VpNA3ayyxZhu*g0|*5Kxu&}$1T!~gr{*ja@l zcgUX`%EiUpYY*2A8GBNGwYv^oB$KLN*R={7YriLG4H)BxV)6U{vjG;dTTk*>?>TF(&oEoFZ0kIjH6fKr-n}Z1_O*C)3DP7%efm$W5&N|cIb9(v zm0EKS*9A9H$f#3wFXOZxp8IC+e1Pfvf8kCn?t9`+a4g$~#P520CUhlbwR zBlQ8A_zuG?qdw=!*Aw-5Aj7w^7m6D6Uzx_&9Lv4joG^RF`y0TC9Gu8acO6Ybk6lKe z6Hr*=jl}Ur(862}J}hctz~eSCT)J(o0P?2BhbD4!lX=?Q5}IfbIv#;|(0qt^J1OY< zFTS&3+ZObU;tidP9NWMbf>^=`dwl1k3$u1!U&^G*3^>9aK}QZPx~MqO0&>WK`ryzq z!|}90t$%&oKt7`SPLOS<(}NK7z&mLs4sm}l5xFpB7+Q$q|=uB~l~eUfFMpiR)n zA~t3g4v2?iFhU87I21(`kN{Kv#74Gxo_B4{#?-W_#WC=CL=&avz0m#TVB(p4#y-0M z+GqtBUh%hRa-?I1j9V9EhHfhGy+C9p*w zWNMhTo|23WUDYzQF32t;)u+_0S(|8$A8QBx*0Y02<+If1w+A*}0O5#p6)hQ7M~!VE zVK&>Pxg&}`pku{GOIb74fQ%)x1w8$kCULBcOO%O;^dRwDa19vKtst63r7cp{D8LG! z+fq>H{yV018FWAgw(zTlusHRfc0s^7t>FG`Cb1eX10T9NmfP$npMi=$mz$)h+b?a8 W+8Ty}kKL=O{kGk1Th`X27ykz`$F?Q_ literal 0 HcmV?d00001 diff --git a/webapps/docs/architecture/requestProcess/request-process.png b/webapps/docs/architecture/requestProcess/request-process.png new file mode 100644 index 0000000000000000000000000000000000000000..33ae3c3766459f39c8bb19c035234afa44be1dbd GIT binary patch literal 109471 zcmeFa30PCt)&^_`uT@&9R*8y|*4ZM&2~inRXAzMqDhQYqQ9wh8pv)vuD@6rNMFj$ioU`{nd#}CL zyWX|-I+woRyYq`_^QKLjH0cYoUEB6gnxt`X(xk}|pM4B|6PdbvANcs_^!}Y&CZ#@~ z&j&wz>b%)v^Q1}X6b*&LWbpIUQ@akIo-}ECk?P+^Ov3$RlP1CU%(iVl;DhObrh516 zsb6|*0ETCYJ~LVEZMCP^*y{U(pMNn6=NUBHW7m~4wx)X*2nyP|ZPTB}A4tF#6YchI zx+jE`IWQ8mKJs-V2mWOA+h5j8$fMs*s-ZT+{S_aL{`mbfzxti9<=R8;n;KPPSA8X( zhMD({X{djg6#X1RMuh2&{&Z@SglxZ?I{MSE{3h6a#UD5N4=E{31kIo*w=1m?zNq{v z&-37#6NaCUUcMaPbgi#-$7N5xwhlHQJ4h4y_=q!<;tJ^8g{V(QFKQ~v)@ywjwTvCv z+8F7;a9W8%1+b9oyLsEm4fTeOzP z1)<;?L+rms+vs!TDKBVB43o_@Wma;8G2KUgMN^}Q=PmUsk9WpqNqQQcv3c0R&i=+U zEO$PAtm_w^Jgq>%-avk8p!k%HwaMW|L48PDTi`=v+TjQ-IU=NuzG@FHyq*r8HYdS1 zK+2DFo{k;6`_yIkh(fQ7!eT@HrXka*f2?@@DVT|EZPy&V@Wq;b3Au?fX;cKx>gK?_ zW7T7#qTjBhti3dS^xEtgIePCR!_l9LBgOxwaSk>Ijwep}$>2J{ zjuBEk?-{O6xP1-Igv^p%vq_HIW*BP#mgE4LCkYNh?5@?1?QyOdGmTyev zJjKHRp1+oy{*$d?U_{C}3=V2sS@%6=6}?nkfPv`(BZUwa zp-WdO8_~lQSoI(HPN@mg$O@!_07Ga67S1CxLg6+f*j7Ga@fa4BQJQhAL$ zj0BgZfsPEA?sB?~aB7w(H(3HcgpMNXCi4$0o zgxG!Cd}e%mG37g@a~rA@N4hrnN)fPfSorNtTOZV&tVde?XvXaRy6w70=cWW>m<(!N ze=NPW-%`mVE+X8^+|knVDrCPOD&@MR-!S4iF^GpyzT8UtHU9aHFv@P_RsB@qLd*8N z{!?K`24HN*+8(q|@(&vel-=eUye3&%fH5sqZ)HcFrDZZfZ-Wy4DgNh%!TE}1z7K{y zGVAJIDjV(G?YxnihwO-X|B_=rO?g6DLYGJ_Tl@kZK880|KD4LUT58(c#YGU?eSgC^ zp^WA~C}kD*H%#{v3M)+yi6LIgelA0731^V_iAag~3;K=h`H~iMMG*Ybuc_N)lj!!E znAvQ$;(Hg|=~--cllbYfSe!t@9NJdLQJh*>#&(L4^}74a=N^+|(#Y5)1^dH9d&yIN zURM1T)6SsG2iHLqU8ze5zqSpTl4Ke__|&5pyI0|unXw&d?p_8ILlQ575qs!AOmeG| zVqJpTct*px%YMc1{Z~l-OPT(_F}k+qvc@H3U=dc4YWMBx@tv-`W?UPv(H4hu?}dFz z{>=l&%F%b28C&e4kkjz^E8vq8BYkx`1yXh@DX|p)`8rI)MtXfW*V;@TrVJ!2v7L?< z9T!CyGoh95LKFA=gBc+-ua=!c7yQYMW}>RdF2{|QD?5~JPtiM!>_q9aXdw(T%v{k% zDsMR>YZ0xz{26M?)6uB2 zfU#9>(2rgNg*0Y5aR5LuITD^F#++#;N z;S(hfkXF^YOU#5K?@9mNbAB!%M{gnx%TH{-fQUQaFzprjkTNX4-EgDj^9p7doO(Vf zP_ocg12+RK#&Jd1zma=ym^qney&&*uOvej!sa1JId=^!!LrHP&IWB%&6c=fHWIjdk zi$pnH;CP&i?$sO0ffxB1|J^h7!qVYvW@|d}nr9rGIQJoGk6GIF#RV+xN3Sl4p7tLq zU(>?&MHa+oCABTC+KGEWX){ibmPdRA&k?v*G50sE0cfI2+9ph#Jp*%L+ELw~;lsn` z1CiWU#v9Q{b7kJN1g(}}7ro@|*)Z=$zxnj$k13n1cbk4YyfZhO9&CI|SW;x>Rmz2s zh!CvjteO5XKf;SG4X{`0*tXGfbWD0gW7+helLKemjj2Dj$c-{7H;8aHtp+@6XMZwS zpCBNM@!*rX%yifu$3W-8ybhjD5L0YX7pRPNN|vP~BWmcU2+8wNQdZUOlh_t>TY6g; zW%tQ6#^bxphHVNtElPl5s#@ZCr zO?qo(y@HK)-SIItWJ#hSONO2ogl)m}_53~nO=Ab+4)(1Kr_woTbIZ6$F8sh*7#uls zn1>^Q09* z3X08Vqy%PNbe51aty7s|i5$(2lyC*K{kDtBkfp*zc8j@g+cnHWA?$+c72`?_Pua8dBl*X-jx)!I2Y|CK~xB zH`e#XLD@43c~V&;l48ja)DvcLA_CS9Q|Wcu#jUHusZInxv5_P6tXNco;29msBqt8K z+70p@5>qTW+vS^CO`J$KfH28qMU{0Z0;SPyM%7CLDh*bRKO(f*VPiomVm3R{5)8MS zTajX}QE;)Y(DtdF#yKXun*Hqc%~=QvS3!#{n8O&RFk$9bZvN1jLZUc|i<#>`>m(Y7 z?tgu0D1rgK@Mg4T`rpS_nBIH+dkb``Hm+r-(f$3gfh*R|h zPqs~yYT>G>&aswnFIb*%{v^ifmJ_&X2U+WXK$^e)t1LJcs?_A3H0n&rtG=HswBQ=| zX7z^4T5`4h50Pngbt{ePIm)d0lDe53S+0b3Wo}4iioj9a6)#GVWn9_vO$iH|$>rjDsLrGnJ4 zHsUJA-)C&E&9TzfU=1L6eaE56EXqxtEB&qzFOrD}sfzV`=rorg$fvYav1=kEWYJe- zZDP)CJ(%s>!QoRZuWOina0+< zeaFR-;jxk|dsfsBrTK~J_n+xMisdJk$!~CxWYIyWm__igQ*I6uJoHd5m2A9Hx_9hP zT zeyP;pYL%b@?3B;r;tN)mq7;>+-LXQO29(g6q>_IDx$d)kJt3KfHYv=kH&*R0*uIg} z^qIpvC<%q>wTZzDmIL7&>UU0A)=15dIToQ{(BXLNN6{Fr{%2; zI}0I%1Kc#aui_ia*oEHH-Q=v?f*uwaZx(!*d}dNWTmH3-Na8G_10x?hJhI8K#?VuK z>n31CWYc~9t|`iSm~OWup@0lr4;aI}2Aki^dMZ9pB}dB!Nafxov2=PUGQf-h^R594 zL-v7p`j`7fD=fLnSpvDE-JE5QTiRt}f41(mtVUAr{2CgQSAWGoo=6{FBqsk%MfrO9 zxLTU{lN;(TAayu~hY>PzWO^5q?R~RQ7QdS4>)&9K8eq>dPW4@xq(=!0j|j*%OH7+f zK(PmIEXhL6wVK0oKW?}7khskW?n#8jRpChFdR{9D+AwJ-h-VE}o%@lao-mi6LP^Vo9nmeME@om$WIBnlO3#otnF9ABy;uWE1BzyZb-^tiYP=;ZNx9ut zpT}pk;zdE_e!XPagkXyma7izPQxe1kBfFKBVyrwXPh7^xFCgU&$EO|kI!CKQ>K~*x z4_6`Ak?v8}y7DcuQnCX=yw`CS%i zKalfnr(1QKBABWnG9a&j#cf1=;wDGmWIlbcukqPXKQqlQ;H1pLt<0p9dg|se*divZM|d^7 zgWJP(5F1!4fps-aXhM|uaIP17>zLZ*m@1_QD`L6;M!;5vFrJq5=kpmAc!w27VPzul zeTtXAgc_7y2YmgD1_7WScwh)DZFdqC%2WEG2p-@cO4gywIaHn_JjJF(wkutY=>zy5 zwYa`g*EX5Y#xoM{nDWG+Ug{$_{uj8Ab|E4nIp~Ub=Vo%YM54SaJ14d(dAjp6M+?dA zpo&AO6f~(eBe5jS+pd?{MXjXN64LB?xT&IFIcaCe52nzSyBV;l#Bpdq5k-7Kw5Z8> z3}u`4Gpk}FF_k*K^#L6YgW1NIwL8e-VDA<9)?nl>;m5C1nbE* zsi8VAW~o-I_wChUb*Ay1FVh)k;`7|tIJS#M*$PK_@Gsru83BG^l@w7BfJ@x0@*&iD z>Q|wR&rUYMtLx6F7e&gmwy+Fk$r{So3<}wlLbGWmp>TdiyccyLEE&%(Sy_pplBGRK z=uMNvEF!DIsGB~Bu_*rJ2hQ0U+p%+a_^pW7Et&XtvB>g5&OoogeNa3d51ai~6qQM8 z;xHTFBpz_@I3>A(8116+lMCdJY z5AIjYOUZ~_QdhOGK-u$sPKe-J3|E)4g#pK>xyr80Z2%U2QM!3eJA`~~{p8pmvj~s%MesVuT1u91)k67`oAdx< z#ZiY4e$t`+c5^x&#Ua0tf8^d{9Dmn+{`@6^3I}o?z}&=X^3^HxHRXk(Q?~FBuB4Dx zCT6_su5y+{IQwsChF4!Pf`En-q0h0mSf=Ui;b$2DI9|!ZX^epJM9T*i4>@VWN8?b! z`hbEysW4W5k7lf%=p>r~dCODcgA7kXi>qM#@q(ct<&=s?3U(U6hUEH}>=`GA%dztU zj39>KWAh*7I-D(Nd?uNz{L;2%3i$N8EQvycMpC|$(_%NW84)7ynxmT-=-kc704Tp& zOXlkveV83tH0WWsJRK-7_STy_?6@jueWKi3tHehfsRI2mWV;PZU}~S?00G;QxOQpn z>kBpnxLbO3PPqmFV6wpwsI5kVhrFHmm`HCXmD|MB1voo|5J0_ZVApBYn3s^$<_BOkW0Ja?SOn&@q<|9pW%Fbyum4tfjY#8%>r5zisB-AKbRVDRBj%De)l zA6e{mTycoDXpfzbzcxcYpQ^X#94%1C$F5WT@!3XeDzbi=X(%L1H`WUnY|R8}C2jb5 znjiXZ4|loPQO+|G_j_B(C6O7gy4n9SE_rNtNLf`_S-+=(l)I|HabuH)xTBGd2mk;F zHsxrf3hzL_nD(z`kGK$)IC{pa5<|vG0s|ESUz3T2no0@SGeiyO+6AVTGK!o?_WROH zfs9Y-Q2`zG;GviN(?61CZR(eDl|+pI$qM_LefObc({9rdf8lkb(k~L5x8nW%*2qu2 zzFE=pknMxbiC#*D+OGXrdw|<3YBqOeKBO$sU)C?t;Xda<73UM=adXJ5<2$=3#VG^#U4F zu8houvfj_G6P6bzytjcBxDGXxnbK!iEsG)_ta*YKc zp>1_qwozp^rvA5Dz91PpQ3I)t|DraM+${NWUcb31v=o1wh8-*})Kq?bdpZRwIwz6L zCU$thO#L=~E6Je~m~>{5SeUpZlySR75-TKaB3qaV%w@C0j<&-+t(joIBQykDx8(U^ z@*rB-mSc1^U|=6XKAWIXw2MH@N7t?7X9cpj(OE*)vMi)y8$yGa6Q#4>v7FS}&qW^r zeurJBFp4v(D;Dk3CdL`mYDFHZ>!lI3x%qHBMSRL7F;w})f+Y@AhNqs$+e$X9R0@P$ zKsEecr7Y-~RZZHhDQIk9{IHVjJhH(d57^2%A8lvE#>UH9AY|U4q=VcQ?Xz;zJ~@BU zD<5r+<&V9l^zj^atscBp1!r#yO}4#$!EimKAoTsLjpq0V-NfzprQ9 z`*9KXHwMu+_W8OJ8KlAXFfQ3J!#94y7W$zJhJ9s7SHwh3AAj${BS#?A*x97*L*^dmoD#a2j4TTyTAu z)k2e`7L9?F+p7V#Z^P$A#mIne_J;6aV|#VFX>j5AAD(jHXvy)6~U9!=#T|`3XVwvFbv!E{D?nRcy)Q z?NR9-`7c;OMWph?GGOGX?S0n7d`AKdl)`m|^q_EO81+<}8k;x*Y_j^0(lrjH$;u$M z$_Z@F013?C34*Cf1(nRitlH*TMfyHg%9~>;z_P;g_(>=&nQHQmISjLg9I!X_lh(8B zZo9t%&C@nPYddKX`eHp1Wk$?`;5GAM=A4RNY~3HXWnnv+Fm=@xm{;IuH(cBg3ml^< zh6_LEUEqW>r_%J8Du1XeqlJ?00lV6P)uG#TmNkoN5;YiIDu%(*(g|Ji49a*tk*am> zValJitxFf6unUrNm3uwc0Xx~RtUn9~|8n~&P#ki2k(AV!td}p?Hi-R{fb^e!E~0!X zeyzAl!1Or@M^aUjoL(6L9u6v0>K&4MHy6ji2312WKf8w6qGrv7>@5#yHrJ>oCehGe z7N;+9%C8Qaggx*T{WFtXwJYNr{yg}Mz!60P-Nz9_c^Gb#j$M>EslCW;D7FByWYF4% z6j?{0rs+f|4d|*KCFPseI6`A;dC)3y?y4Pjtk58GexFkEyy2tzEgWqICGd(I{Zq5H zP?K3&1aOQ8tw3Ah^Si4Cp82_J2GkT&jSLkZWFbsb=Q<_})A%~Ee&Q+oRSR^OQy7qD zwR`(XFQW)K+G~ZSCKBL0aipdBJhG91i^&Y!SDisa)L*Zm-?el=C*0tH<<;5S%>Xvh zC*Uw}J*Iz$2i}zU5qz2QRjE=_sk~Lj+3YMCuHTiG(zsWLXOnTaUTZJ;bRE6o<^gO2 zid;|`A;HNC=di3Ou#ykdW8)Symj=Q=V!NH1$k41RgesmAl*bk00|o&RR0#`A^o
    >6&dn0#WXIvg!d4Bj zhNH-Uh;=Dr-MXizc#+?vl>p$zwNqFZ^cm!Kr2K~CFhQ+b>;qN;>NIh!(>gsY^^xUc zWGw@ZQuwSop-~a$s!RkY&%t00n870rz8=AOM-!rX&wlYq6uEQ&dU= z@~@u-!`PC6OB!7C88Woq3D)2$7XzfeFYs|wT2vq3ELJg!ru`>+>M3m}S{sYNH)#LkcBiEem3@+$CtL=;_1P#q zSd((XF_4X>ZqeWA~*z`tyDH6g8@$HNC~{pgXgGt*T_JUf}nvmQqltX zQtlcq5(b85d5zXl$`*mM1&fQq=J!3^n-|0u@0lTp*s2A5#Su6#_~StUn1h>*YB4W4 zv7g`_t~Z3s%>tU>s&KhZjaE40GdghXXU+3zbgf<=A0%F#t65gRvWI&#i(G%C$DeFU z3rw@@j-?l$#PuBK+DjWhdc=@7$9l{1xE@n}rG8|!RNmH&(1Dw^TDUqC$y8o;y(EEI zCuSBLYLd>mo8rq!iBWat{RJIcXy=tq3)~b$Pk)3pJ>Du$Q=D|LcHx4p30FMMbi>RKlV$r9LtqSmezZ5?818T&-73< z+E{Ej8~VLglTvVz)GpJ0lcg6g*l`R!hZR8yk`DpJr?Z}tc5C-Zc?JEhG7?kWa5|DA zgj02sh^03R=TLMxk?D_7hP+1l0IN;e*WZ}##Hbwl5hq%=*7{>GLN2rnYkX2MF zBevT%jwlK$+q;jiq|lA^)oUzVZ{!M@GM%(*0v@Ct(W4|%w7Vy-2km9xkl;j3QUN{hDD4&+wHYqFSdKBcV;zBIi ziE&V+DRd$qFF9(PL4GjT(qa@ESzsH`MlA?Is6u|LKu@G#swojInSP~KiTn<&UQfA0 zDJWx}5wDG`>xn&CV#QM7ilA~E7D5c{IRA%gN*9F6|c8`=_z>z=E&WcAp533Z{_v?fB@+i+ZM~cVCH>C5DZ+^3|E=+$%`f2zP5t| z5C>+_55^97mkbPY_u#~q2E3#b0A#R=5|Fu^LTR9|JBnYK4(O1A(8HL=P@#8OR4J|E z>qv%VS*>Pf?7+%Uz!4o-8xuKV+FeT`>5s@>#yuU+pcx4%Jp}7;1ig`Qgn!PGU@h~P z&hQ~~8&vAW0YoqIem;F%?!M^qY8~q<26zX+$7x2P!X=t{&@zA?u#OJ4LG z5J39?^;(k%T$qi71i&Lk_SXP^K9BFPwoViXW#lD>)4KDDl(3kN%2deBsM^jA3D%Wp z8ym|sVeDp2xwjE-_epd*T`+?@xHT>h?ZB+>4(Oh7sl#T8R zED{OjY}%VmiPaXhImyZiWxNP<-|`w#9Z!__*UovZX`2K(Cpm*FAO3X?#lZ%SZOjU- zmRy_$VMvdB-9TQDz-K;e6BQ`8uPQhUE#qK66)aZr0V>&^>iNcWLDBhjC+g~tmLppEzBXHZeo-6~p+W(6J~q%Xz$>sbAb2wn*Sw6ANT z+_RQB`273cNe$~?9NwVPKTgI5dMb?E5Mn?CD@Q7~E-PbxU&JxWgqD3%7L}Ql1>Jh6 zomb0oCjdGg?jPX8Gf%D+rwx5!cY7-q_C}Y;+!!Fxc1sKJ!q6%cqq-jJD?vytIuYKv zcELAHZd^en6qAiUK;4oEAWKp?px18U;{-R);i<74t4+jAVG}Xk1K@NBpiZBj+B3YO za|uWtro6~~s$)%+cdD0bQ*4e5d6(eWocNq<*FrDNt%O-22hnx(qKhlMLTCv2=>{bs z5nAxM*>2fpKk|yc%Y5Ru6{uI5B^jaQH-aJ_b;5Xq+MVcr#zCs$$=W}%Hym?@9Z&QL zloZ-b?mYXQLiZ#OIS4}FuT)K2;K&!@GuJZbINy+GWjr85@+K#O>{k6J#t)xi?VcX~ zhzf#uvdxnP%MPWzc&fn8PkY7HDjY;zKgOxaopMbbgM zK6x1&J{(G_WqjFCEQ%+WIwVg0T`?>@6^0*%3Fb-yatC=sk@838v~X58gPytSc?$fD zdAgfj=-~#5p33UZ^I!paFzVZMnjjx~LJmueMD4`k^?G9qX45<+{Xb+04Bklm$cABf z&4V{{KJGumh=-n}lpkf6vFG_W9xnQ5)$2R~4)kQr6@PhM%mPWcj4DTm^IGUnXfq>t zE&i840?vF1`2bpEVk#f%hF?mPD`~_>S;MW{>e%xGsq1Mb{iPkYa>(7}>d<<0^ly); za}P0#vYDKL*U_`pAQz-svs!L`jteOM(m{>kANyT-+y{qC-V82V45dde3qK8eyhO&o z4u5&^hJ>f13i!?#&A#Ms$@m%0lch4rFj%-CuyDy>%cD7y*alb$5uM-Y(5GxKemeEbKvkvd^KWyiSGQw?`)XsnD<2t24;_U)5 z4}Pz|ZTde?RuzOqBPBj>?23fD*1N-6_`*p8+x%Yoyea)*ok04T9x*o0p|VTl5HD({ z;|jOAz3>K4l}OM1e>dR*s+OZ?hB||FM}r)oH`ibNNE?i)ues9|{2O6+y=6vcjjzUR zX!&ned+!2<0?J%i82tTa*U1BDU9uTwFe-XkM~7o6+4@Nh*oGjY|CxlGzlKyAXZK=X z$L`XdOW<9Idhe^t2Ah30l6{q`RUnRAdRoU7FgV66<@f!ce*6%H1 z^sg|zD*eep;`GNtBoOx|c!tT}$HXt>Niv*7KpgX0=QNFZ$iz>|)x*3L9 zVde+8=ik2KU*e@i9?#DjH1*qwvnn&YL#aD}Eb~3OQG!~gT+MF0L%ElI9HhF`^p|lh zbrgFo^u<@*?KvPS){Ey&iC2fO5v2 zi11y*4Gp^16ULCE_2)3UmM<0?Pl3&1PSWS6WMyrTkc}Nt84(Mn*S>nak&wD_r8^<1 z0zy{t4ufaW1VXnHeBvzw26F0158)y_df3QFwEYM$bf%`+D>0>HNNXj=Yyj7- z`6BW811HF9<@4P`8la+s`{ox#vPFmOZzcGtINK=FXd2hd(;Qt~_@}^kQncTk|Z8R&~29<8VG^^H@lyz|9r=Tf>y= z4#{x=98nvB(#0p9l*P>6NyY4)tExO9`U<5(Yle-4hjw z1kZk{38R#yxqHpyHn`XZl%EjcQ96viN)za5Fq2?vy%#Ne*&lU{ybk|+G0)p;HB@>3 z>hR(`P=vw)-T$&5N7=%vDDc@JB|EUG#)}58cDW~?_$hKd|D^*;UYLnTXx8>vXkWwE zZ0gNQFPV3eefL1orJl;nFAx;Yd>zg<^~q+x=FA&l;7dSpl>Y4UG-->-ME8noLG+qB zKV<@c8OGQx#}N8Ga|uFo`#RQod*cJE@V^%G_6}QeyIz8T?X^VY^<&4%;NbLj>0>?z zV@zs|^Os!$hhhU#qe-G-p5Zdsg?%%aDeO5ai`%zo(9FxCix5eAS{ZvD6a1y!5wl7~xpnf1E`t@KF?Qm8h1DF>JpP>ICbsS6P+08az*9 zv1n`jj`nLkku}_PsufBznP!2Oyli~|gTW4j{J!%6Gd*E0nC9T1f*uVP^@#C-3`6`L z>b~ym`jUCL`zNmL|6gIvy@#TP^#)#lr6&9}^GJ@BQu()sGK zmBZLQFA!h(%qs1O3;wQ9bFt#fqiMTvS)ICiwX^OdN9}X*psjG(b1bR?YA?~;g<4i{ z;BMC9Su^%dPJcz#GAjv#i$5i;KDYB~fKKjbixr@SZ^V}~4+Yb!_gj4PEO&0;Qtznq zCEKXOO$0Z^+5hjlm$r?SpIQF&KbL;->X4(GejAv^Z@U|DIWzpi!osAl!xa?^OKk=i zH-AMf?OPsJu@!Od0&6Car9%#!eQexGj>Tq3tM_-pGeUn-n$pM<+|tEX%@ZfCx>2g z9n9f2Nk97kbN6AKO%3={b}i_9Opj&9+=+^vv@Q7Yjs8btwSM|SX8Bw3OF-e%R|%OnR}bLxbmFXfjqb>ZYqi2b>*q+u+lz)g6lp32GL7 zIoj$EkDL8L4pdY7Pe1;8$u(U$TvgKNy#b(EaoPb_KTDO3F#;&)d2&I*) zd=#aFS=!YxU@*~n0Nbz4H6?mopqQu>+1(zB-x%=@L%Yl_iOq(fyf3=hDB|F8`#q@H zC-kOnzhJd_<#6owFrXS%p96Yj-{ROH{g0=jf9~6Qv0!0J?VYsD16CIwyX}Z8ZBF=Q z^;Nv7BuZQFvSo@TxxL%@(w%qMYg)zib@hUP+^|y<-Em1qG-{h-3GV4O*7k|1NXzD& zEIpVD^T`8zX`BCnC}q72mrz;Ki5e3>k9pQ&nufv?CC>Y?_11a_JUNP8v?6nLg7sK^ ztE3QJIcKsm7a?zc(zho{*^(=cr;?`OL!7Z=mmFJ_+CxA{yD>f9Q;OE&WkFLj5qsi7 z-KANh{e1U}`*CvKuUN@ZhS40%Hw81#?7LbQ_WiaEv!8~+hZoN^S0cWW7JsU`aOhsR4(5<%BlkGM)2wdLnG%KL zEh|*Ru_1oCS=y&ZCTK!xZrgDN_$7g>d-et0wLY&u!L8NxJOw{|hF3U8e1B`T?q{Mh$!YuQ%9pRp;mlgM^l^D&ZxcJ*(ojFdfwJjBliW%0(0$KBxzXaWMEqk;}gA6Tsy5-P4THc!aXzA2oyV=(duZbSvW-90C&EQS)>JI75~~;nwddbF^4{$jdPY1r7ov1dVQu zQ>M%Qjw9Bs^_k@HeHa}0)HQW2n0!+NEvoWRZYE;5IYx|ZV+eg^$v|C+s;u&WZ z>uyX01CL1Fk*|>>N=fycCy9PlI(t1sRu_b2zMm$0Q*h{$-HDsPsktS`;*%!gnW*LI zc6k?#(=fThf}vS>6DB8<`(_M^#%8P zb`R^`jktz!`_5`2-ilg>P97Ms%paP0%ai+O&rQq;7Th*P;^-E+JW^bF{T)Uwk1D!RaoXg6`+^H9cdDeSl zF?M=))g4%0RCf@PGG@3E+R_uEbe3KxYmg`6bEsj&ccvqV%j4VnvW5r9l_5dbAQ{!T zz}4^O_ewp*k_j*zZ|RY;BzqM8VJ@h}v-O$iz}ghPW>gwD6fbjokL}mH)=F2@2UU_@ z#wshj4Z|!~jGt$CTRN?BF#4Pa;vcun>_mIp)RXv8C?3ITqJD$Ms}aBt^vyBWQMg7X za;B5Q%O*gEygkTkn8zp5$!e3C6Egv7<}I_dk=U#;JEeA1hekWw2RG@P)fE^w0&ovx z1eY45(u=AdQy5O);zE?8SDrl`io42LQkomF?8s?ipo-aAuP0VJbVuIvAK5qyG5b@WT`IkLCpyIf z?FU}Rz$$_Y`-*X5jjdQ#NL!H#okAXK3p`dD`_EO6sEOm%Ix?WF(E&wFdOaZiPPBi6 z)=%_=!$=`!t=3QJ38)@$!nS8f(E12F$R9`qK?NFlgGuqwcmr^MCpig76CH6NzqN&D z#r5rP@uqLZ&e2>ZA7X(s{E%)GRK#DA`tGRVIX;D0s!8vv;u$uQJD?0Aajl7~7$+_( z_h~?g#kvvoB%kuk1hgyNrdJ1fg&f30e7S49@j*3u-rqgqg_oJ3X5X&1^=||0_vFNY zYcossB(8vCKCRwA-T=d2q&qFM4%3%3#79IASUixf%f#OJZtuh>&Nu8t=aiJJA*>x% za^8w=cH(`gmxMZY(c(IRJZ15h;|{fsOeJJc4~-!$my-gcCDg#Qp@>n{A8^wCV3WRn zT|s37fVtF7KRrwwfw|vC+REc+av}kY9SxTTIQ{!84_^W`13gFI+w!Y($-!d5%cIR2 zp^u=qbbjgvF;NuC3>3tPKAtkNo1}`Kr~~lP7myQ^3jP}#?1bL3 z)}V1m?EbQ$Y|G4~?`{UK){RO>@L$}j?O2+YOS7r^?=QScCO;A6{?LrOFsi@%e8=?n zaG+Ye&uVKUKBaY^_e`77p5Jae;+u_FwNaTiT(AF`Tm9s2%%b0Y%d^x${(qo3XN%$) z4PpH?N&|e39g>`hg^SCMwlzJ#ZhvRp+U6>lg7yn2RpQW)DJ4xII-Wk$t(p)&-YE6o zprZ9qXa6HT5HMjy$e%eS-vU*i>Bh6cn5K`y!55 zvE`k$H~=gxijF2D;}MPQ#rR(sG({^C@M6eb7)*y+oPXu6)vv%?^SdzVAPjf}_}IsP z+gd$h{y4VsFl0b&JU=QV2#eZNR-O^W<2v|#roK^O!YR&E#wPU6&I4Q>aEI-GG9JN` zRVz}mtN|;IJ@w3R<2uqa1N7*dmO82Co)FriIoNA-e6Q5H1vXpBu-;}%$)h(dxcEy=ynU-rjoijnL%kFv&Yb;WrQvn=N*QAXxuOS6ysnCx#-y}fQE`fy-f zmRQ5wbuhQPMlH-=C%Ol^<$Z50xpM^2!(g&fx9I?S*e_|!ZW^vx>RHyP*6!c!<#^_f zNxmGHDsNr$>YI^FscLE+$&Y4A!VYFPS>Q~G4Ltz-yS`50QHHtWdyAG7cqT=TZkZp| zTV@}-uEY|4QkW4`p3&yyH%GU&MDgyJa5)~wON6Vo)ay)_8?Xj!SQeU&_t-bNX78-M z@8R(Z(VLX@`x~(!(Yu>>ygXN=?2-Mj`VH3l{dG~V5cOLryC^uW!jb`Fn|0`0-@}Xu z9>;WUXM{dg4Gxgf&&aPy z>Fm(4i*K3}$JjmfGpGWu!<5wpqwg+!pdIlmRoB5Y?Q%|yWHisw>s5+eI04Z?e$%9nWy|d}B6`m@c?zbI1XE4r{SW?rkFM6;s_b92BK}plTK^p0y08l~ z`*f0U_K5LSO{-eE`}{-9f8gABlxtmV-{r5y;Ht+Qg)ch1nGIe`l6w!Xj*x*2H9&-o zWT>q_3f`MG{%j*_sX9Y#EZtT+9lV@u{E2S50b>EHkqot~p&z6bt%d>4jv@UlkeHl%p!vq-4n>!ET%8he!2? z3P$56?`@N8`zJ{&U4LOEfY23$82^8bFV*<3N;`bp^M#~A;aM*H&ienK+mvF|FX}T4 z>HeP6c0V^F!2{Fva7;+9W?W=Q;w9W+XLQ>XY%TqjGg|$=Vi5R!xIPWD?%6MZz6Pew zI&qAZS?r`EbWJ<%e=ltgzTKJsrsjxKe|(2U{>EHiir&5l z@bil2Tk4T}0ks|JjO7@c4iy)!N;#p4tWP$tAa)oiOx{`apXO}OL=8W&Hi@L0!PIh@ z4=rIXPTUQ+4DkA%zlZ+VZUKn>Kd6`15HFChHsfo=ZMz#*J8 z>ec)Sz|)bXSLNA{oyYn|VeWT8F<8Wm*VOigitOoGN3J~ZKk-0qS>BL!53N>jwd9^< zIM@fV^>@}5th+I|A3FxSfqGQ5b3bCh@9r4=W1D)a>~uIXg(0Yqp~W2!zc>{-c~5{Nl%sam;>XBD3b?SfS7U+ zYfFYP8XcWnp{G-SYW4eg3KeZlrmj-GstUYVu`CUwGd5@3V9nEC`hFgW*P5tNWV2Rp zUD+U^q$qt=h=J-E81q!Gr0N0h`059CQ0+3lu;&ynqDZkF{(WpXMd$J^;u2978Zzk z0_VGI?`t)e2rOg{EtU90WTEWzk9qtNS0bZmb6a2PsqK(7kZQhmggjpds2UaI8aOki zwEK6@5KNA>jV1kYvv*wpQ?jv5d%*GC;fUsCmu?1?-m40O-ysb)tp%^F9`^~Zw;kJr zh3eQcae~cBB4pyXtXBVR40vgVtV}xhBXA&i{KGT%d(-buSg5=oL2U=;CKMMZy%!sX z?!IS|MN4hl`RSYq7@t^u{+ry$`?vWBwPts7wI5{b6Ke}TZ)dl>_H%&+R{IbyN=gjU z1l88fWj~|}Ui-2DGW>_z1+9p>G<6eaBVRXxFB@6Ktb#F!^nngjpL73PI_w|i>Cbvk zyp&q%CPbjT1LAo<AbBIN%$F#UX2NS33@^>{w)&r zD)pk6l?Q=rv=RG%c&$^58Ze8THOR8_{ortT$?mMx zjGy1#tyhhl`7iJWlH^8|u^l=yXe+s)9=x^2CTV>;Vj`;j?O6^(RMA@9%}XNrlU3&c zd%J_u_v3#@y${%R2kHa|Hy=5zT!&qWHg%PGU>nCy^=ChHcjzoSepem%Q(=-xuRSpD;|A%0)koWj<{#SrR39 zX3_j`!zeab_u8S0yU2KOT$(g=LfvqxL28XF!nF&Ne*$=@KxSqq{h@vp2la~5QqnlX zXO={~yFAnN&!56s+HY?kPmPbileF)jVr$nZY{})<6?34;_GS}kP#^WC{Zuyy@(u2o zM28_A{gxnp9v(krC^LT(GR*3?&iY(Zmxat4vKnwSmd{k`+!|LM0=09HzwWJw4EbPL z->MSv`4z*evw*Bp#rOkPZTH^vrd4$y4jdzmr)-Lv2^*+>D{yteYBU0Kyksasux3xQ z`I^+VuOWfz%(hzIvVYH3K0fkOlD}DrXTl%h8&JGO{r8Bu^;l}zDo<2t_H)sj#ZJ_J zezArpEAyJ0(7R-A{jE2GG#h5$T{t*?o+l#WO^QNw3~2w=cVBt%FiF4tgH~qqw`;~# z=tIrE`=0nCWU4MVGy6RVaO;7eFaJKSsx@k=7c=h-c>n)`IH|?W-pQWas9#i5u$Z@r z^&0=azdiGRa%FZ+M8p3{A%pXM|EvxDj@s!zh|_3Tu`Hw;#W{X6H{zJRv)>=_>G!s0 z{!gyV|3OttJhJ&0Pq|uV+>Y6Q+L#(xQ=nfBI3fTCf=yl{zssLRWPB`8Cr8rO>7RG4 z-_nIal=|LIdc$(IC4Z-G>Q}wc;IBDZKj+qO_rN$m7>gu){yqga^z^J$qC!HQ|c|8;S-zefpWho1z4EMH%{IkHIWWUdP!B8|jc_&n0v zd`s|uU5QMcf%9h|6OnNqK^92|Ac-$=QwNELGIoQpkUMR0`hQ;{?eC4vM%|mFGyV-J z6a*_$c`7Zfr(!+JgGQDX%X)*`prVAd6D36_;tt2eBKOvHXtB1^m!`gtE=4|4*Vf>A zO=5YAO(PecgeU*5bg@0k$Ep z)#he+Ub>U5OL&L1W4X81PpVhltCFPyEZ$dd!GWph0I{n?+9P@l-aU(x;9`jQL1_&_ z;~Jd=3;TM^rPs4Tak~GqF4^D1mD_fk$C8yr1WqyqAvYlAG@^Snh~*eERr{^@X$l1n z7)>ygymT7PXFjmbw2x~lN%Q|g6;%gl2(kWkoNv~Q-s{uYx&YpzZqiFva0mU;2x95< z?O&<)>{3n;tfFSiCIR;gxCPl%_c3Y~?6$N6m2bp8eG4+~EZ;;Nx$#Anw1F%aF?w&D z1xz)NZ~K-FV{yIjw`C&_RK5;vsyk{78Qal-nFe6?RC>o4GY#q~CQMV4s>ex!rj)SU+&&N}EP`#@x{{-QzCX;J+*$o0$R2lt# z+C$^+j*zMqX2S9NMMHPr_KOS;_8na$FyQ*mt=RQmn)~Id%@)(~6x>q_w#lDxxW#Oj z#o<3s07CzD{^zNEHmQHfnFfHmk{RFtsmszP<`Dwe`2eMS=JJpbFmI z|NB2^$CJMPN4ZS@EcmC^VESuQ8LklyU6?TU+x)S}v8sg1|1zEMVP(e3;{7jiAo`~# zqg&1j104LzDGZoA zguTTOoL6>YcrX;%ec6wrbkx2WILtITSQRwRx;$!%&u)Cf6#qUu!$@Z_dI9NlZQUT{qcq`I5SUz%y(;}k?EU}wwgP2;BjBdh zn+Eg*{fC}(5RoSa*+uE?tUxygcq$bef6r{ZB@_QF8%@IGANB~+o~IKB(U6kt=}($H zj(W>~?-IzfW@k1+f~jKU>SP>4R372JYaG>D{~p#J=#yklLT8ymx6)$U{)&B(0U@tZ?-=e`7iIcNwsnGqD*rP~ZV}x2f zJwq#CTG=E|)YR3O#3aif28_29I*>E&Z49dhJR+D zf#ZXE4+{y(>S7m;Tfkb>)mqiLe=3?4R9au;(sE^6DqS_Tq}9rrB~w?HDP)#d zYE-7oOfgAF(Et&-z2W_Py%8%*5X8^-v)?}3U%3aocwNtVp69v!+-$N<=kqQFZOom} z@*%fqQ}o?gk0xyd&0d@zLBsIsb1{6ut)Q58u86c3mfD-lxG#suH~TG>9o;J!*_ON1 zT8wZ%ALde%?IGJN}c|m$MY8z^ahh1>YS_4h}cEs(Y(( z_5Q&PNrL6$xBY5usrohpcLPUS8?qL-AJN;@ZoY1&6&q-VF zY9+NG$+T*`9bEkVd@SjH#(s2JuuttnRExB-S@Z!L{;tL7(%m{`@ZRN{;0%kG1Kq1r zqQkCh#-@)ow(Px*Y;7ZF}Q1umTp6H(DP3$SiQ7+4o;FnML8>&|I6Rg&LW!xsh!yICjmn=hw zK$z&sgfrx~Xb9wK3pXtz1Q1*6aMcf|;_;|mY<0m+1itw6qjHggh;P>}pS5~0s3_7J ze^wVwxti&gDIh(rM0m-`2PneUiZgA!80ZhE8JBnL8WL}0HwS$h2?(;_KgZF_a1Onv z&m;GAKdLaYy+i9eo1^GJ+{X;Pn_QMWtnvBa>ft35ZtN{GsgN}4KII*)**eFTR&VcK zowwJdbz_dVmZWVbdBO3Fyzt1|zV~#XE_co}Z6`Y%5x7WAJdpqFFxu45y`SYLmsLcv zgRliHbx=Eep$+ra!hmt>2?0685HDH8R_X)BTjX_XN^Q3d^!M7Ie`j-bBDLcy=)0te zX)V%eU^pBU3&}CAk4dXQk8FA$zma3tj0r^yOwt;y1pWOl%+p4A^Y`S#F2r%z18%A9 zCEN>gY|(<4{+s$%f}&Fi>ig;@?rrY5OFH zJaiKST}1w3>bi)Wv8E;jqLRwgpO+@*wTH<_W<3OKR1l2r{b%Iz2A@SVZBV_uHb417 zMPzaicD}Su^uEYP7T=op{|m9L&4_4LwAqgn~9i@4xaf z)WQN|nx5%o&|uql;-OkwqO7_Nk)d4-$^J%1tVTngqZPr%Hr3I$`$5T&rBg`u=d369 z_pd!|M5idjr|=mMOA1``5=o3{aDNqt8fNRhrzCIOf@xgYr=2%_>va807V$MPyX@%J zeBl>Hv_>;{i7)>IEIk!)#Y-zaW;90wa)p?-)s69Jx7Q8 zdPF-i$DE)>*KO+KlHn}+PlG<5VPp>_>ay2Wg5N(vn*(Lu_F$6F*-bOMVUj>EcfDP&JJds37Tn|8n0J~=#=_WHd(Kqei3!@$tmY`|ZTar!hs^Ie z>Ccv-yJALE{`~P$Wm(#St+ez-B?r?6_<MvD#$&Vx}7>BYGCD>fEU~|0ffr0qF}} zS53J6>5ef4kKys(Z>(d)S9^|19*2vZxbKeTXV`CBVlI~*%DF#f@#y@SQL_)t+IS}P zu)vwfn)ZqiO4;e1v@G*U%DqW8o>xAab0_}ug>dq?`1A0_ef~e6I!my*_R(k9`Ort@ zv(i_iTfQ!w8a-TK-)G{hlRxrir%IY#Dy~avUG9g5&a3=^Hsakg4fNk36u zjnyQ6ob=VsUz&pc8Z{sKTs+!WSP+o&SBhQ9)D4&OhW~lpr%!^#jRz&Nt=tE3vT^)c ztDuEB2@Q{rl>TD>-Kme$o5{Z%^;F_l*~?LI4x`DM$%e#KEfgs&a=Mu+%U051$JQlO zJ-K<_d-0ySf`GZhM?Lt^Q;D5>&ra<$YrMR%KMO75XxR||>s3u*YR*GhF>MFGF#Ncr zDWtK4e>kKsO;+pzW-?*5|tA6^TX3^pE@bcqDkI5PPa`_xph9#X0 z321Zzu;q_b$Sua3c~!L26Sha>h`?$a>*vfi%($MIi}>F`}5}Zooe5ropOTDLBm$ zA^8NuRWWnxz%NOMVXFiI?RsM zxGkHq5H9|Z{BT@L4x4RvnlDvq{r~cy*Ix}4HrRA3XJb=U z`K%7*6WH^c(bMSb>YQm6oWh7Fsm8nvziE12hC{B?pzM#X3jUaTSU$VPwV>)qX;_-O z=Y`F8JR&D=+QIBM)pFsH5(;OdXAsqkb7w9 z%>ipZ{2BEn>8A!Dn1_X*@JpB%6*N3zXmHUtlF!+p%A{@26iu<^+0N%WK0?kS(Wrma zmLLE+cf#h+X6a5Qj|l?9MN&)1Z;ylY1?SWye!9W0myc7daWh z7dv_noJHHgCwtgVemi5&Y-{1*{Lug51DGUgC=)$oSzXAb4sFK zIFVS|Ph%O_h!vW9vojKM;oK`~zLzoBq;RWVY=U3qAje+63TK=BUIY5T7MU&7U}J#J z#0k@>i5G#BV3Kir45DQpX)!dFn zKLOdHYFZMbHkd4AbL1A}S;);pv%~FZu(gt2F-7T5i+RmJu$FeK2Kp7#@Nb{&8G!$PA4xbK{N=k50b*;3{ z9!nyM6mbXewg_=q9b7D1nVRXYq?fi$46A=)af~pMQW3EklTNT#i?+mC@RQM?cX0!3 zt*|mv+Fjl#s--1SN$PdV2pm963UYF7N&gl&8j3Ok1sl3)En7w6HJa*RqEu=bs=k-U zzqUyS5G&2iw^WL14xuR8DuLH_BXp~D*VfPmO)av2$V?2n=#=t(4uH{my( zR1})x1R9Cjsa_$e{8g~{EFC`_0c@ialkHpu4w=Jr|ba z;S-$cbRGqU7{5JLLxn~dM~2XGhGZY7ss-BjgT>a>s`+LL8_)7;2_Dsfu|Hx3e~G!a z!2Lc{vWkU~Ys5lw&}vNo-*=5Qf(pqjGVnnC05hO|ml?SD;Pg5(^}~t&7`TN>6sfs5 zvb0v^7E%A~ltp*_GobN0A>j;Z)c9(0-h3Hi(B~os{SQVA7oDhIFt-jj2gQJ> z;=0cW-i7Yz@tV#_sT~dmOmAd=FshJdax^opTB0n+ueVf3bjn_GH|n<##>hGLk1NZp zaesVBWj6|IX^+7ccEUnB1sZsI`v9dHYwkLo=PJ_8-v06T5p>|1Al$kZzrC55kvD6) zaA`;MD?PsY@A28V^W@HHm1jlzQQU`a%jkCI-iVN2n*8bjr_u>CXYFK&npu7vy%6r4 zf4m;A-3L+CWf zdG6ogv)%Fp|Gm<0`I~^Jr#r6nabcka@fioZ=qGa@*bf=mkAQC9>r|XMEH&6lN#`z= z*qoZwA(4z96Y_m=(>#PWteVR4zFy_`QWZ0&ICy)OUZLt(Xl(;N9#TRGe;yY1t8mr; zM^0|tw;l4^8}}tYvBU31-pFD}<9>nH0lIMd6M{u@-ws`OU@L#ffKY&O$s}lU7#hAE zL%=3;Im@5hYCJpj{1{ZS&gVBOu<{f>-U>y~$fdbgXpCAmS%~|L@gq6DQ`XzCc)sMR z5#U^GLltrewpBo4kyOmK$dgfkk459J*qzb9(NG3;24W1Oj`5C#XtES)W@F_LgF_Hy zxZz^moC-CK@bpgIL59UK*zhKImK&CAU+hXJ{Xu}5!hGdQG2it-ZAo!DS%qC9@w^P_9d}0uGdERHtxFb)%DW;J2cf-{12WNiRxiND(!I1SaeqrdGCtrk^zcTBy z19CezGLj7$86o$7AZb}D1d%VTB23_hxtV9k@20Rgt*vg#lwlcX zcnfaI@QcmWBxo$&haj4QB@*okB$0%P!^)LNv_qxYf!h`xxML z*z1vQS0N9f%}V93SwhRdsvJY~g+u`MWB?Lab1Q=9R>HO{C6aHoq~>-UR|8-b60@h( zjfP{a>`ofyIBc$6oFnRta_id(7G|LE;Ou(<71asr)n z@s+CAwc!4G!)2BuA&ubEqBDw;X6c+D4(gAbEh7C$2!y2md#m0=`f8b%f7+J(M{f_f zYar%}dVz-eU8ULE=oO(a=1VlVRv;%GIzv$baKCpIkMbwm?HZAM-v$_#p7v;L7DumP zd7r&(*CDb5+Y1(V7OM+J>Nr*xaT@`5!OVa_F<#?ejn$YdNnML4YKq~05VJGAzA@J@ z?<}}@<1Rx%{SlUT7KPiXhH%W3M=-)Pu^`@=4WFtFRx9y@r7zI6a}9@_cc$Z!?eBo; zS`Gq&7IhNbInCe!Xq4-KkGGEd2)3hYwk5(dF@wy* z6G2a&ZBV4}lNp*^KCI?NP>IUc@bk=6>Qo#8#>G1U7zgJB6%q7;@o_kC;<0TpS54B0 zT5cSBbRL_6#ZIxKf;V>SiX!8M(!t*q!NNcf`}6c-VeLIk>zKm=BkdJ>VK>u}uy}4Q z2#x{Aqlj1N364QrIWhj3!x^=fB(^&sf7n!K0zp%IL?I^ldSFPQL?~?gNlJFMEx{85 zc%{8-B>wlLV_7js=q)%MR*_yh2;dgy^tL5azezBTxkSi%tmLvb@e2zp$TpV*vl!3if?F&1WZB8Gx3I-eYVv4b|Q zis!UoSSLQPSGPKmMPPCnoAc4c7V$X7g6^1HVo8)P0Bi5^V(K9pilWk7bOwZG@H%gj zhM||xJWlf|zl}7n6vZgG66x~Y9O}iK#Ric~;m`|+aT{EOqSB~mRhsV_x9u~ejXbW| z_Xj_a?W2(s)PpN!V*~Jm444+$d(do+Vnj0LJU4u7SjHBS!azW}7NrG!WZTJUb$Zvw z-Mou}8{4vZVA4Uk5qBBf4YC8o=6wLVlR(IT)ggCY_M5 zvVNn+(Og}ZzNSbUZUoEOIHE#>WHE)5NPE=@-~)6lAqIrn0I^BEi0?3q1f(DU;Q>w@ zs$h`p?PSw1rzwyqB93KAZ&g2uJhue4lv?d6UAq(z;!U8hbjhZ|6* zkrOpXmkAM{g)1FisrlN>b88(g9EFqJvqbq?nmHO%w&v63v`^Q3WNR=rtW&tTRU8); z#yN@0m`Y_sBU(OnHe@s_LW2x5BSC-Fpsl1sQ4G<&knNi>G!)5&OsO_V2eDE*dGC2c z$@eY>TW=@iJ|=i)GUEI{Hn?OcduAsH8w4bRdjGdVd6v7`tRNyeUlJiO$c>DePXJ*T{(MsmoHS8}!45kW6>O0Y+$RvS)H*|T7+DVelK7-j)SlR=_VLnLW6E4#1JMvH4 z5|H`2JxS54Qo75FLlr~SN`n;1N1vfUyn(aCB1^!}qGP#=T&ez;9BsbhfBXgfd%1Mk zm92X5@ATeIM8iz7iU#G29mxb$V-k|cz_!F_BLhB~ z`ig)k4N~e&13(%iAoy`v3}OdK;?pN>P->vi`781u*6rR8!79xk`wIBTY$PI*)5wq&h+Iz10s&sWAL z3t3ozLa6Ti+Q=+JC}iuv71K0O1-T_xMj@+9a^)c4TDq5O30GoaT9qYsw3*kV8J~}G zRaGR1tA~^@&vBG@(OgeM>C#4^g4s3jSLdpbBqs_1PJzDamf_m+usLJQT`-Zb4>Pm| zeJo3V);e+x^_6tc5xpOM1_MAvmnjp@E3&MZ$g0LN-40GvT8gyL(IkIo7yR-`>)23<%W{tN?jsPlC#l!U4qGQkX>Ula~^rRU~y zP!3Q-b0>NUnSNle7_>(MOVA<%+!4V?vuKBU(<2$PA9j|W9C0nH(V8>L_~4R(S`@Aq zI(;ZeXd&&Jdi_N)6h}6925PkhEe^YVKDkGal!yi2&0aC#u%To1P3ZhsL2TS7zrE65 zAp;fP39s^Z+n(;&5&*NS(ec>jYq6`F)sypBOFDzi5qCf^7YHFU{yt(6EdQVXk?uI$ z->qN@T#r*kbGK;ZEO_DXm6nX#dP}MpcVTAQX&PNv=y5?=F_+TxmUuDlSp@(sE0tIK zk(7WChpPD$21z8%1=+D@W(MD;Wpu~r?X#(%nR*L=kc@hB7dJu*ceo(}Z%d8`QJHQy z3d4@tr}V}k(rLZrAFxvlm4DEB+Ck-p6%HCGl2 z0U|GvWm@(yM71NiY-h;!n(1tu%Toc1im~n8V=tKnk>tv2*M*q{01Y8`P^B@9hLC&= zighIt{n)cdHFYwhKb>yDe{dz}+e&Y~`sWjM!7Ef4Cf#(ZeAnW`N?pexW1+qpKlEU#QEK(0@}529!;0{MG~o^vi6KaocShDA1z-mBxW!mU+7(Wu zRLE3boI4EtAv~|kC+z{cr?(`rLJBmov6pXYS=ieWB`USE{E{nxcwm6et|+ zi`{!(U*#aqO;+dJm)1KEKy_c9Qzlz>oyg!w7ek%Fdc?b9(W4q?&$y98XEv(TsrZ`C zC_^MN0aO=(_bG}Sr7nRSVK8wJ+zEQuW^i=nId#iB{%4)B#UCs5mDCHD8D+-&jRZkg zL?6fBQh6NrkTT9n;H>xNY-&+o$ly7Nz4VlQLL6ASDgaN40>zwB?&hA@#rVn^#@tjh z^(<N+<7sHvhc#Fm=wEAE%(oK+u`4 zFk;9wm3$&ubd#=BMrLbXJm*T#lp+d$bP2(ilv_v;fih0oDo~Zhcu}fQq9n1L04oa= z9Q{a+VOAE2Ru|82X`lavR4D?;|DYf-hYj~9G8)_FT4>B%$ExJ}kN|ckf_hGp@)#qN z55lL{F%;XXq?O(rAq1j+kORuh=*$!WDp2#eDg;6tss~&(hslv^%>>BU4fimZi8Wk} zy!TE$AQXlcLk5ZuA+b)nLVkCSs@+!&M-hZlzpdv&^@)*0sd0keBONaLgnI;RoZp^D z+4?M%Vx>{0g{xgTymh~LA>j4yPfC#KDXB^KG!AYb{7RKLK^sED_3VbhZVs6_WwH6& zwWeo}y`zhT8)^6e6)8e+v&9G-z^?oX@HQOb1!u6Gt`Zr!QZJPeI4rh-TY@J*wjk?e zBgrY8xV%DU{cH!H%J>;3C{op(8nYmptSjiRTVUru3X=S@Y?J>pq}~O?_w42tEIqT0 zn6&6|=7OSsm6Qm?w?ZerozjgF-&Y$lBNK2BU&>u41=;JVd1@?kFtr#X+}42uakHn_ zt7G~ErYVCrf;=}^E@RA`NR2J6#e|xRE=n3CRiu_&Ckms;BAJ~`V}gY3!up-mY&|8j zjbwSS;BZ3mSXH%($#%R9%iEtF$p+!g!9S7iXPRy>Sch9fR{GScDq(9k=Gx1;exP1N1TgE{y zq}=$`jpsV7#ZT|6zZU;%$8B5ni!FbXiHzuROPIhJG#kt*tvK<@iYtdoK<)8_7<&k_VhhU`Pe&r7<|%CTQ)ps=wa& z9DDaTnJOHm4LvGb57Y-(DFM6N81)e|Gj$ri;l0fGbqSoYYdMpe)me*+F9t6K2+|4rp$g{B`e>=icw{VZt!l1qyTM8{;>7)6i!=+SNht z72(%LMV4q&+Ey_?(&#EbPASQb1>FlZNeb30DGkpd|4a-Q8>b9b+l-d$Jda=$r02t4 zW-)mWjZ9Q&7B*BIGQu`T8Z^`o&&4NF&>kBTK!L~JJ(8xt<2u&1m+9SONRjDpC#)`=jp7uw z#r)!z!vo^qZ!lugZJt4*RV*(;k0Vt%@Z_%V_ldgsi}65P`q+;a>cEYNQ`89}o=YOA zSdB85jnpt(c!_|wGjpb3?b-3#3NswI{!hZ{?`y+-*1y-2$`rX+wlFS-R}8%qNfi5o zqROK?HsWpby}QQ5VTZ@Av7tV2CCPIO`Q5d(X>4cS14#^H3m<3k4N=0`FQ7)!;T0gp znx1kpjtm=)v3{$j7u$YGmf{Z@V}~+i@Luv?5WiaK{;QDjDPnX3g+K#e1u>xoh=JNo0hz1)z!=``@S&3;6;{4#tS14ID(9SO~$?@jNw``x}k zZfuj3(49>xolZ|L06P7*5xw5+CHB7r|+|EO-5V|ZdP zAs0*#nDU-|-fd*3NZw*ROw+oBn*Gka+k}psWjqjS?Wl+AhKclmmh-l#q@QtQwN;Gu zMjGK-cT_KtTRg3iy^z3(3x^MM9yQSU>tT99m4oy;57Z7>U}nUYH*6am7EhR>pm=OXcq~qT#QR2<{KUa~`x1i?1l~9>Y59H`iJnrF}DrRmGbj z_nxA-qG5jtg02a-1QwI?^jGnHueDyl5{U(+TzFq$Ap1oPw*b5VIZW z=&7kQE>EDX8)pg+6a(obqT<3(r+YMq_6_Xd-Nr;Pze)99xhnx_uB554K-1hJ6=DHs zLNS#fS+`?i;8C1^`>v#2SVPA_6$z`KeaU#xzOki=YG@_64%+%B(ug`sQk}Gp4}+qH zTLN|*gJ!B(B+Kq^E*sWi||iG~f8%b~*260V|PDk>f)nt~#PA{AR!FF%y{ zf^F5l)2XRSCd@bC{cwxg*}#c~^F_A)W!RN)q^l{?Mb+f7{S(B!5gH#aS4KKZ$CB|Z z&Ig}P#nq+`yGchAbR(8RMsa1@Hic4)lBn11pn6uPv^`@`GYL67_^|$Byzjln*v$xXph;4f;huo0qn+D zDQTkVYkGzhG=Haq(Uy2#X6y(G1hAweZUOOK(1kBG33PcDP9up1m$k zz{}Y2;>jh@s;F7~GLs}Oe0keEMG?FCm*H2*AOnT`LWMhTW2SPrFn+@mO%;<(Y|68! zD>QFrWwMfo1Q{M%GDTR3%Y)KgHvsW|-+$Qu1-%RIYwExb!jAi1^H)8NSg zu&#*e)~O&HrLN#$`xewAjCCZpwYMELuTj?q*zX*YSNiba!1k@m9ZhhtYAqG8;>=v; zFgxpFB+)dxDDIG89_1Mt*;3`%qT;)a(p22hNLTqZFtV~4KEPJINB28sgqFsV6_so#;iTUPR?r* zL%T_kvf58E_#!v#skTsG6jH_#ywbvl#%r0vsnJlmha?X4wU4WyLFMdTrEA@)5DiXE z17H!Eu9cqlub|^Y2fgc$aBQ|X%co$~Qp*buO_o;CgHcw!t@!#r!tG zJTc!bLZCqA)0_AVC0v$nrZkyfoCXCq4gShZy%7CmWFdL1TlY-ogPbCMgY-$GYHF$? z{jdFcT9UsB1AnVXQ$S2DW6(=Kj6W?okdwTlIPR_y*(qqkW=jPy_c7QX$lSoaeL2y^15&^6SE{0W&t`xoLJMkdCL*@`oX2 zEaUkR>`$}rwL_L&P|#%ive#)BpRP+(oXSbcB2x2p8eyAh8o}>!Yfqlt_S6ja@=Re# z$TI~$?k+2ieZgpt4+5V&^OYwWTZ@*8geTn~fr2Bux()>GG&!Ns+l>m1X)DH&9wl8< z5v}dd^CLN*P=jN^bXsq)yf{W^=#QNgGHT!8;fUX5Y(=29HS`aAXS7!tibPXqD@rV)jH3G-spwgwMR84_IdjpTE7s4{6= z>y8^PaowK8)gE4Y<@(F3+THU)j;Z(D*J_KlH_sf@s&;yig+64O!476LxvV_OquqQ= ze|L90{y(%N`i5`_H?Dg-D(U+|JPDmt2hxTHw^?)`I%eAFq3v!$fC-UVZ7Gu7vG4v4 z8`?b&)1mr6pVP(Wnz;Y44e2RNL=R)9%;UI!iaunD-x8RGdu7F4G{=1_JUGy1ZXC#a z1%a7WFg#i#5i4lwd?AC!zzX9ud*uGb`~sniEMP>dg|j8sBnxM&wIC7O=uWD059CSM zP#GS|Urruysxj<>HiYz)RqLpIO&b+@<-yHW$XTGixyuwg6w3{VvZP{3@sw$c`)2n#;$(4>Lqdr&izuJ~4vVfR#cO_ITNL6wG24FSy1>h0tTx%cnCYhVOmzVi zFdWO83ZUHt`y5*<3y*lcRP0IK=t2vsTI9|ITzUzLWRE3w-_H_WNOPO& z{Bh(ucY262gQHQz(G}64mOj)BQjGV<$RM{F=9y}`!{nnwTrhUj0aO%dcnOw3v?r?1 z!*D4>RDq*_E{w2{YL6@x3#}sochJ*snc+ znDkz?hRDN0!UJ!&O1c|20e#4bYs#GBrKQZ&tNs5j#@g zcUHLY06W(tUyQF+;htNllQc_q;dh+4el%lq)>^AK6o4y)w8OBdCKu}_kbhmGwFYTM zIFjlQ!pv;j zP*2SMh+_CD%nxmPvzk?g`fuo}%e=Lk+%z6XrPf7-2}z=I!gh@iPt>CM{SjWQ{aU9> z58YOEW$+28MI5piskb9D zm?3k=oUDRsSl*VJGCYn{SjgsF$L$ZVwr2L5YPs`tvann?c9dx(W`a*NLs`p}z~L{r z$6$Y*G-g_|(%!ZFj#-!*v1Nmx02XjN`AQW8F{`l3*Ak3Yee?7B?Q0Xv57+cTXNn8)0<_VV>av z8RPqQinl9tT6EN0x=b8Ig+#OKr<-Te5wJre6&|Oj#8~;YvwA&ELu2^?zIIJHY_{F3 z2Lfqtky8}@1zDQW0u}0% z|0C0)5H8QlAJvvyAA{u9|0pXqYhsP#xP4=7_(7`h=ajCGB(U$(SbK6AZ+5Dr*(K5B zMS<}BIG=g(t=BPWu89m3NWt)aeb}KxO^KdILK328htJq}yw%^!Gxcl*_C8nf1j0bS zkq-CUkpUUTr6p@aEfF%{Z$q&DOKh7I0oZL01@lqQzr^<2$-sU{LivA(*#571hyJxk zwB?Ysxq{ljyKdN!7kdHFXNeb<5MF?y4?-9o$Y?ones?yXeBVspy>?*4f|$X>6L*Yn z43y(`6@A0r^}-A)_zH7Gu)ImEfL)rz_sCQdp_z>M2{f=f?2nm_we_YB0Bp9uV(@UD zq=uY?MWKQmKtoIr7uc;N& zaDCRY&S@=~uXUD;#xyfYkd|*l2Q6;N6at}%lZ-+EcTWM|GL{mlsDnVEPo(Ur%5tW8 z0Cu>*y=ZXx7%m>Jv@WQ!&otA0ldqh6Zk}_2!1e;gl_5QZn`h>|z2gDEIzlZX5!PRs z9hC+EZCj;{ATvEGbkw0d!sR7a%%~~tOg=l4Oum6Wj#GvzkOEsad{AKP`>un-{|(^( z)dmeaNbOoC!%ka!|;ffokinX zgIkI-UYZutUADg8)56K;inPI7)Bd*>{{0<@CQwo?3(4ENV5}`2#Up#M$-YE5qAO?i z+2pPp&w0a-A5`GjQj{kLuKxGB1cNMeO6=Twc50_VgpMP-wN;D$zkO#j!XI*G z-j>(&ig|W-5pjrc4H<;7y@-#4e={iz9T8P83!N{+QL^j%7@z=IB1tz+lBwg=O?i1e zK8^*tQtQx|tN`M~fpByt2pS=HU~2K#Ymi^4AnJbMq^ZBKdGgDhV zjtK#&2$?DngMZHf4No?S>pqw^pv3Rt4hIPd1j{4rrznG&EUs%-HcRD&O`FG`5H@DE z3PL1`^_agmvtNs>^E&)*fFIc1I>(oSSbEi81YgBID#AM}G$lWL&8fiaSRsJd#@$aRbB3UZ~N;W*)F&2%Yl0$t-_5_KT z?WQePud6@9c*7AUIZI}4{Da||lhE+^Na-*3-<|q6y_x*WQA1wnd?mT@>7zXr*JI~C zg5mnW<`W}UOinwI{%OOm?fj7=R$e>z9slfob`xP?{`8cHtJkwxyL zo1r|x$#rCkG~n_1(WSq9I%>&2I~8riJv$YCqo}xF(gwxMKR=p6IxHStRCOdE=jF=S zsnG|6hR;skQB~|pf}%?#$sA=~W~q}7@|@rVCMjl)`HSO|ld!vP{?$=G9X$4Vp(e#+ zN}(pvir^5D>_HzeabFb57y2k8__x*QuoGV`9=*Y4-orC#BD@P(5ln|m=e06<71x_8 ziXCvw4^`op-H%POxwPu1Pj`IgTTULbaAP^yAp*>Ba3TBGxrfCOQKV)l^A{Xp$-~#@xXOsN*$BL#z{qkx&0B49(ZBFu-QBrJ(e;g|5F3`Qp1% zA9}Xn?jH4Q;S^OTS~=fs3feT#7y6v*dNOBpY1pF_yUim83hejwiQdnUg|My^5?rbt zKFO8~`Vm>X0)BJp%G$2*(J)iqQszew6gK#ILqP}V{27jqZOc*?#Ko+b@a@6rAE!4k ze)%e+ff1kUpt>EFP0{QHGiha22@6xmGo{y)FDqH_y{}Fu|VVY)YJ`3-~6Qpn+ z-!^Mrq*+ZNd0>G#>YNkBhZqkY%AAwuJxBvP?#zrjbNBR!OtA>duPF6KX|2GydPZLj~<`9+zuBxbauYw6w*)SvwpZTEa(2w3j@;zjNY^(xsUFs zmqh;=Y3jQ;G|TZ}*SB!>vr9kn?&ADkh9@l_-MHS=Csebf|5UVQ<{XaX)Ckk>=g{DQ zGofO(l`@I{WPZ2@ZH(8G0RFMd|RyKH!3nkEP8y^f}kU3VXZ3=2F=q zZ@&d42R$MO{&{_QuYZr+^w<3Fk#|jU{TE;P$*Z%Octx(T#@dIqCQcFHBJJ{vxBGZz zm7-p;eAV#qs;nY+!6klIUqA2I8E$`u6!Pnap)6)ISqvEgH6h9csD%{ilP-(XU43WL z&nK|Iq0z((S-_>$UegChvV(_+C^eQVxU4Q<9rZLkWUL^E#xB^kPbzS_;D z%W^aG3|26UR97lVzHHx;z5gVwY&ol|st+tPk_du(Uu=VJr12--AzV!g>#`yYoGrb= zdos$5v-=(Im^?JT75A8~3{S3EN=%t?wV=z7F}7dYvr0Yu4yhx_mHQMJpPaW!9SrZFjJ`YZb&A++bDS6 zA{AujamVI4aURf6IOk{d+WGEkCU9juXKKxjrp=^6)AqWk;*5iUq1+G#y=akCs2IwX zq@bedZ2xL8DCJV#YQ%JLRQZMkY&>oeh1!oLgcV6_8p@V~A!)X&V%?ch4sY9mUHkzW zkt|wB!IM>abuANVF;@P%K$;1RN@q!J2^4GEr9GKB8VBzoR0KQU@Hp98K$j!(;tU$7ddW*439YWk(PG-x*zlnCd?#KgCy&9tuDL2cr? zhl9wHu0Pfy+3biUHuKkwH7UC}_1yb-Cl*05j^SsTO>$x5QtEkb@g4U%dg+#`xn3q( z6_>1@_}7Ivh*xIer3t~yhM8#I2K147&a7HQIPq#t4$te!eiQFa!fawJeP_`WT>|P_ zMI$dBjjGKw(O`eluRf#sgimZ6m1NFV(ne+!z0KVex+*4`$U`_rDD8-~C;i&dBAEN8 z3NP)^OkmozfAGspl~ap~&tMkfW^9btRlG=r_DG82C{1g+aMa5ClMCC3z*gyg+*IkF zG3}*%B3ACjSltx4v|M|0h+RE*9kwsAv{THvxOEKud)XCCSC@wYc7kXP3#Ykt6#LVC z^WG#+2k$u+vCtPQrVYZKSFJ*6nZPdGrpzFd(D5)idn5yU}UZmf( zS-!q?lpEVjg;fYhu8Fy1->SEtO{1DfnCGwQ$N;yQgye{5#NYFH|}mHQnUXT6pu- z;4e3jvQjNQIhLnxz8qwVdfj>GyjSoipH1%KgSv6|aq6tzUB{wA`aj(vrsGc%@1~ouH4iFNs zj;Oej?eu5MOoU+mgFsci58CSY)Mh3>C)_w^lF%HO_FxRCXYpZMCdPfdTb^R4YmR9k zzb7@3a!7FLk?iA|G`UHlc3@xmGs4=nZqanNPbqgz60Qe5Zt;2CnlMn*a9@ZW$uzC= z0mcb6jJ^DvaL^qeu}M#1R9X3VAVa&YCXMd&Fa8go{^`Xa@&DttB&QRcqyv)Lm@pGT zOd_a=GU$!D>(ZZJXd^Yz+?@t$JC129iQrBOAMqe=R=sJ)QP;0yfh=ub65C?8y|7$R z));P0nZQ66kx^&T1TI+>@6D!3LX5!BzXISLoDmw*Rke4lHWF7rfKTQK?@|s)R8Z= zl`@ZE2@y4MO!LI%{9+HBx)iQI%1gw~C~#r~-=o5rE9uh?_!{55SNTPhv|F$(k=%G;hr*z?UHm-<5L>`^DQAoe_YN zXZV+(09#iCI5_e^&}X0}5q$w0DsjR&XA}AELfVDapVI_p!%O#2&7qXYO-ws|VoNqz zp9H39R^AjJ!q#Ss z)X!}XnM4wGk1_X8I(@;`QRbrAzmL85|J!vuBJ}C)LbH6it}26lr9w8%w$&u_FZu6S z6L!>NE6qkcz<9unP4NI#n9ZVHw1(QMG~adrpbN~4qmR0prYSF_Yf{BhmgeyN2eTfn zM{MyX%B*_DN+%uYG?C9~8}$K4Dj4QJy@>E=N0(3N1-&8c99JmfU2|P}d)N4p+$vGc8%D97rUVaw9D> z@jqtryd;$HhS#^k)M>}w86hM{p*qB82;YaSWpF9EbUkwhYx0k&vtG6Zm2`pp!Tjv_ z+OzqBvpPt8{#5YUypiUyLD1)Aa)m5Os6yPOxkt?&vZ&ZR;~#=c95F`x`~~lmc~_)CNqfFiPlc3{Gl(U6>Rsb6D;aa z>dd0&Gfd#s!MeTCI|*u$vQfa!gIY3iMR-8hW zxMPQ&pJ?`I*zt~Zc@=k?ozu0Px$)=>)HmiGiy)SVJ29d)R2r;#s2{y(0Q75@ER?MC z3eQX8g|sBB-_^9niT4TdHu5==9Xnhcm6+MiL$St(g9a^>tiL2q!iHL79a@*!i@XUl zDK&W>XY4^{jR_1!;O67MFS4oL)x~L?Tj5uK%1r-rR`%h+==>aL582!VEOS{bU zfm~iQ;{4ak`R1BUnNz=CdEtS%jWGQ@;IxR3|U`}v7~ zy|SwA%W_&)Fpqe9n|0MX(cegWg z!S9_t;C}-7Yt3t86GP~s`!8B9L$&=oN1^B6ry(I0niFe++GxwTYBfV<2)9CeO$%)t z4jNCkGWU`P`Ie>xwX~*s{jdJ8_<1kgac;W9fZI;?WwD-rH@+al&r#{7#zcVx$W{f- z3ian&m=czQq`t1!5#Dl9Pu&Ek)^Nyeb8Y7RkNvwvMTXlp;+xSdvQ#X8{UjH^^(0TL cSulj=e7k7k+{T6iWV;s3UGiP(x9j%(KWwln>;M1& literal 0 HcmV?d00001 diff --git a/webapps/docs/architecture/startup.xml b/webapps/docs/architecture/startup.xml new file mode 100644 index 0000000..3f821b6 --- /dev/null +++ b/webapps/docs/architecture/startup.xml @@ -0,0 +1,73 @@ + + + +]> + + + &project; + + + Yoav Shapira + Startup + + + + + +
    + +

    +This page describes how the Tomcat server starts up. There are several +different ways to start tomcat, including: +

    +
      +
    • From the command line.
    • +
    • From a Java program as an embedded server.
    • +
    • Automatically as a Windows service.
    • +
    + + +

    +A text description of the startup procedure is available +here. +

    +
    + + +

    +A UML sequence diagram of the startup procedure is available +here. +

    +
    + + +

    +The startup process can be customized in many ways, both +by modifying Tomcat code and by implementing your own +LifecycleListeners which are then registered in the server.xml +configuration file. +

    + +
    + +
    + + + +
    diff --git a/webapps/docs/architecture/startup/serverStartup.pdf b/webapps/docs/architecture/startup/serverStartup.pdf new file mode 100644 index 0000000000000000000000000000000000000000..34aa59808b3a7bdf36d07278bdbfe8d9750b68c0 GIT binary patch literal 46175 zcmc$`1yr0%(lCr$a0w2BySoN=_h7*WcLs+90>Rzgoj`C265QPh5FkK;yIcMtxw(7y z?!M>U^M8BJd*ETFyUV((`swcKDyUQ>q?p*4IZ>#-Zj4W(Ah7{h0S+eCD1w3j6c!Cn zM<9Sj#n=J}E~pqg1MNWoc1|#a8n~*#D>zcd!S6L0kYFe^e*}&8&>Y z96SJe;0jg%Hyb;EhmRd#0LB(~c5wXnEou%RV-OGwjv_1!KmppDJ&E(g;!m+uoE=Qn zfgmtd7O?aH77d^W=qFJrumAwC;R5mVm%RIk@NsbtN$eX)GDpc2H@ZZQ&a=| zY!!E~b#PX9G&TkPL>71925|i9!c+5Kq36eO9@I_*XKB`Iv*@7l-{xWys;wJv=Qq06wM`UNqclgkkB zLfH~5WhtqpPA`O#(`n<2uoD}M^f+2G1YE`p#`GiIqDI{mzGbi25*s{lC+*wg(sd^q zJ4L+eWfr>1vYX!aJ$J@5P^3nhi@#EhuqsaND89F5j1~&d+z_oEq{qAw*VU;SjCfp@ z#?7acPYQQEwLK-f1!5j#%9AyF^w=yVWBU_M@9djoQ0;lvWJXUP%!8KPHmTen&7{A$ zlt&5Zj!Y%dY+=vuYA6r&3cl`{wmh?oFU%kH${edvyi@p0OR`*=Rj1jSUNSWq?s-bL z_cn@2$E=|6O6eS!Koy{5@yTM5hHllCXVJXjWkYM~T9eR?iWaRs2w|nt(08AI@w?3N znrZ5i0r{g@^`>p2gE&N?APcR!lxYMpIX0Yr^~pBcLTlGHbY!IY6@r}G@Z*hk6<=XY z?-v$>{ia++NGHnv!m#G^sHl_E@7v1!vTI+yA~rL9dMhvYVl$vXT~Hx)w|cFM1BLW$ z7d3p*XG{O}_2=)(-DyoL1;rcqlN{zIAydv$*laaX>v{C)8zTCsqUx5}hAI4>w+aVV zzx^OG)~Da^Z$Kb|#~8CAX9H2}Xv<==sZ>z8nk+G5xfe>^l$)nA83>KEq>R<3aHD%R zW8h9-p27UxlU1>gKw&3k_f@1F-(kUe(*cBpj2%WfKn&iV?wvfj#7L$lG?stnGpsSn zw}zL|=+!Bj5*k{t7@I}165ofA3ZooRJu{}KG-E$xU3Vmk(c{mLmWfsJeX=Mca8FSw$hA>A|KD!SN}& zGuYfLYg3-wPIcwgc#Vh_L?LSdm|UHiydMT>cxXLYDg)Iy8^(yU)Kc-9-!aM}hqor6 z#nAMe?KIG>`4Yb~lY{nUPiNMWHk8PtmQY0kFY)w-nVC#z5kAqp)#G#AV3%`7dGLOY zCKaSVH75w#t@>O>E+#nD6p>iaXze)cM1#?92M_4ddH*fu(;Rtpg%zfgv+#Wi#Q?v1 zQu_3EgP2S1K`ID7p$={dUag6|rH-!2ZJGEeQp+;~cdB8LxSVl}G)qOXLW}Zb&x5u< zv)X0Z4dDWDa-(!p$JpxC7!03lzwkp%-#@GkNf@A+L+a_SE0eZ`id5C@DAT2dfZ0`?wl0;Q@{-8lg)2x1jJxoZW{NwVW( zF%kOaSBG3M%t#H>*l~9d%zRPj z`P)c|Xb5dobZKEw-sN1WL$A%~V%g^;QSrHveOh7mjxD(WrVdaLb;Bi^?c| z>Nbi^qG2A`SPGCR(zNHgbQ~P;>g16$QbRtrxxRfDsHg zz$!s4*M}?rvTNq}e2p@;<^`ULuXSr=s;G$ow!%9vjFK%K4NbbQUT`%2q3)cJTf_=3 zQo;9zHOnmOZq-8+G94__=|%HkVvoawjDy4_MP9X_}b75@>Yzi zr`71K2%KxacCA+Uu(<8T-?li?{(j1}!F14Ttj{WmY0kZ6=e*bFUFKnpU7TG5tGqjD z?P&21sOIHp++|}mc}pe-`^5uuA?0=<3!WWDgEsj_g^%AA5OW9Y zGhRQ&Ra(%syrgsoIp;96_7)-J+SPnC|G?uO{<6l5i#al1JbGh@Y_c0efrf}!OydRb z)oS>8mH}Ju;`_}Frju%Y%2oBgFO=?d>VXfN3lN@IeTZq6ze zIrRFw*C**rgDpA|vSv>4uv*Uo$fS{KDTb(b@NVvexaLWmIkB-(?4ahVbfz_ZcR?4W z+7^WU-w3J0$IaW*T5s>_&D_&Nk@R7{wHX^$PY)ED?AbaCcnV~;rYYgH7B|Ls6jntc z`jkm;GRI&&|Loo-n-;EH&47J54)95uh8?747j&>fiz-?M#?Zus)7Afl)~8&4*+YuzYk zOK>T?fGUo$5qqRlMmIb_@1xENllGlIrCv=aMo@aO{jkjNioekhj|JN+F?r=4 zriB5qZ#O=pNZ(rBunjLo`lca}v|YXfzkS6LqtEWhF*?OP0ox%N1{*C{K|~!Bd$pu% zXUaZzneBxA?W}YQRCnWVVD%;z5&5gszgPKE7J4G2WzzT4Y1)20XpCq0EWOzo%x-;Q zi5(r)j8{!>XL|I02GyTI@(Y|(SlIIgpEO?a?{hkvRBOdfQpgXN&ukjizAmo>T3$g2 zmv%75mMthX7d%T#`uKqYJNInR^&Mx0TgM5~&Z5Wz^M`fy1rbu zrJgFsc)b)`hU)+W=WG)}ELw@V%H>NRk*@-Q3wCz(;-UzVilxkC1#<((l8CCzi!;q( z6XGGzQGv}GS)}Rc3BlNLxwR_O6$r+ZS6!s<(5DCwOUs3VF$~p;Nw5*8k8Y=N&h#tYflS?M$w%AkreIPGO||0321} z1bnsxcLj3w`(RsQl*J|-GjWT!%3(A+Q`d=CQKVpjC&2>W{}kA=B!BLnRmsJ)?_02x zq98vHb1}M!BAV&sa`beVOqWezHjO!u&Gw>-0^=c4eK7)v!5;UMS>jE%!R@^4r~Zj<;M< zz*izU_B%{Chd}vP%!?XzVH4`N&`}$SU%t|_cC5`z&rW1`E?0q$vs7+!Q%++s5M9wZ ztF84TCZFpXoERzE@Hg#r8@ENZi22YUe5E%SU`zo}Q1%j^i%CLR>zU6F_ZgKc3n=94 zik@Q;^DHfTdAm5DDfV_J!>X$#hk!twpE-@FT=ypXu%_`W4`CsdcweewDghGJvE3)+ zO;dWfIQ)w03a7eBi55>fHJaWKWx@r+>y`18H*qv@ZG|F!HF*9$7dJnm)Cy+6JpdqJ zNlZ#-Y}`awOL!WGCy>4|%hCdzY!7*854?A49&3N~#r*8WcTcwy=?17r2bM1n*NhOxgKa4GpKQw7{@wSL6U1 zHg^%RaQfTiTcTo2sr9kC7{wva&#>PRS?p{GU^i9(SG~vB$hpCN5YN&3#0}BMtD9x- zM73gX8+u_Qm`HLbC7g>EpG^&moERaCOxG3oTdQZKp-;qw8X{8{6=48YQJ`2UE&@z+ z0wo-ucm`o-kOmTsxcd=76CA$#uxgnWMr$iyt|UpXu8R@suds`X9BDNYrXi~K9+o6* zAFd5RCppuN%wTCtb^??MT3RM?v?N+P%8AI2;ISSw{!ITeij>&P(u4r=cm>db zsYo;$sDMPdtys7LYwyj6z!&IYq``E@TN*uQ`uQ#1Z~$`VTaBVR0G?>%1u-_kbP8}xO`)-$ftA=C3uSe#jd69Wv4-KPUCerni zSa9hqE;^bLVBOKngq4YWIPpy_P?A|jbtf&cgig(uTRRJGX|WxRp795XB+m%LwsBMC z*hHUy)U#8hcxwnRdpwiH@9RKcic6B`+FiBbS_kkiZ?+ZPvrSqhMe(p9`(OfpJ(K|D zAb5!<&|;@x3iS$BmW&I1FPA5AX4r&)Aj9+-4Dzj_<;94#DtGEAhGVb_=bW442L-xA z^dzyCF>}J{lqPLaHHkv#yatxU8F?%H=I)MsPRI^Zs2HYWi%(2vz5IHSVXfyunQ>oH z%FqSy3S=ZXi`ZCKp1o}%=q^ArbJkD$YKVV4wSA;jBC0USKsHuiw;W7CD-Eq$UNxAA$>k+L~Mq#W1EKS%) zpA(=CbDKZfwQxludf`yMTM@-bT14>_b%$l+g^In#@Rtq2JJFY(E_Hc^}Yu=*kAABr`#i*89Q+O@Ii>9gBLarQzeV~GMMNc%1objpf4dbm8ahBL&& z2<|sZ$eVDY`J9F25ca7{e(~J;!^X4pJE#_Fw*+?|%3dxx)4qFrF0fUiV8u6)9)LoJ z6bbBc)?iaBfXrufA$0Dt%z=k3@5?kr?jG#)_kK^%&j=kvP~K9U$4&(d+01mVW{c1c zq?dqlC?IU337?^IeDU74{-VILpwAWquBeIUs0jBc+c4mh2cfLk=QZKkzB5{xM{C;a z91WWxnzXp6&B9!2&He)NqG=hZ+Lz^Ak&a}M87E&(ZyryrdLFj)rt|dyCnk=RlUBps z*VN;Sf9!4E)UwF?P}x@fQtA~tDCeC!?Y^Mu-wS#M&JoUzwLZJ(ePskG#&_;7pf#`q64~=FA1sdCy4ly%VwGuz@Q!%uN*DK==#pw_o){J(P zUWI5;*>*80>?g%XQi{;&JAV?l=hy}*KEk1l$Oq0DL z?#{^_IO4xM=hGYIaBIK+fjKR@(~q1&=8s|x?f8*iZlqrAcu;vee{TN&!!rrnlMnLA z!}#Z+1nf8bRrB9FmHcG#r)2+U$C95t`}?_sjf?v~aVfTH+c+#~VLpt#a`J!p`uyU; z_r*O^eQk?Q57?zq3;VUpi1M>ac`yT;?Aeib(AViay8gm>C}d~Ov{di0WzP(QeWLV( z6h`5eMSi}!d5M}9gFLF}Fi(dR?R`$?(gkjL`*_mg!9`G6j!>H-6mFjt^)Vf3Vca**&F z8nXwJ+IE9w;@gn8a+T|tuGkjS*9jBLS6SsQX~X-kn{U$W&;+f?n%=;Si96`Z9^SSY zFdxzc82| zB@JDh1vw1uVrh*afYk9V=_ln!`QQ+l*KO{4!q(#kM$80qxI}p@;S9v1 z>x+nAkL%XjI*O1y&`fzWI@BfUg|>xnw#u<#y4h0`jN zuwn#~0TxC~DaT5x=nHysud6a1w`R2+eYHZFyQq~GM|yCs9) zcblk`V(X{MurO8X5~YerS?dg62}4DdMTz*=(N@VpyBar(HY8HYteNI}(j%^J%6U$p z^iC!Aj(;5hG2}KOf6Q&Mgnlgb39vj{Sn<(`mtT47o(JdpKG_4P5IlO?7JZ#xMp5N& z(P1CDxUp*`)Irbmb9INAYJLJo_H9-TVOo>S2+#e z$MpEH5Y2;v-OP8^^}ZjXmj$O@M9gr;&EBZBUlq=&&SF@yrubuDI95=Ed3iL?lnK!- zJ4v_nxTx(}MWp+=-;7vwE|@G>vwh)7oYUo6k3L7i3eTFVYI<`-b)T|}z^N1Z`g=(@ zG{MaQh%{<`)UxerjZG;3B`l-MmBb{3`J)tE|8ek(}d9kOOP` z826Tvs*(m25V*7zde)0)XyLDJP9YOE)@X=dqtl8suAZTD@CO#4s)i;p#rFDaA%`A| zD{2;CJou)NewJW0bll(yMV?o>^t88GZ`p>MPM~&Apf|3n#9=+EaYlaJ+8w>FGC7zf zb2DO+wA0a)h*i(*p3TEBX0mI56vqk?enQMOAPWdKuHU8~@zVCcH9z57f))@iD@lP; zS}IA(<1VX%-5r~x9}G!3RBY(Pj1$=MzIRI?M=IwFqP5TFiu!hrIg>qqwX=V<@?+{? zJ15OA2wMOr2eV#;Ya4^j?a-Kl0Bvr_{2BXwH#W(wU}HV&!L|Io7`Anfj2}5Po>$+{vx*I#QL; zl*0#feT;x&Q#g*=l8=wf%E`mktYvVN#DZ!O*68iMIn;!c2ms=RK*E3vF$qlLVk(vZ zxs*fGnSjtj(^#FjzVl>1IX=q9s)Osg#GJW^uvve_rq0`SHh&%+6nf%*xm+#l`9AB0 z(J4VfVtOhlp2_;$G6_?$S!ea!9M;a}h0Zc%c@id_Awt^(IoQwIh-G)67D7_G$bI+T zI&02I`ZA12znLeR)EML|Obk`tD!Bx6P#*SGT2>$hBUxGcE7LFhckHj_a{X!L8Fj6i z;v&spGDy+_u|~|hyfKaa>l0MO99o#&%jpP3#WIKebb^mY#X;_TKrts4LegnC$z6M zfQe`JKuxEtV0*b!nCYMiH(;;)G}eg0tCS&l9;&Hv$GW<`+)=zs2v%SaAbGJgI;Y%$ zQJ*OP9=ceKPahs>MG6~pi(r!7Nz5g>`cflLU&3xF54|}N)c#q6>E`;u<@m>^ckV_$ zo|df#M-v_tUJ3deu_a-$?`-N=s(z^KllR8d>J$@ok8zXwvQ0WtSF_eXkC&U#Rp6A% zJ79hlY1!IaP_wAZR~Kks<(nm-3&~|!wI*Y2pkt+PL+9!G=EWm--aDAdQFgWV=*I9F z)G{IQUOyOH?pSD8jTeU(YXeQMBbyuxButsf!M%52fZBlQ*ofqbIFwU z9uP1^65e$iGuEzlGwCZtZ{H4i)G5Z04xj0ykmLz$D(}sAlhn-G@Uy+W;nDqRdcE%m zKp~6mXMDOnhqt%0L~SP3!;Mz$i(*Vm4_OX}A3hYP&oYiW?2nFXJ6v10n_W+T@5)DZHay7wUJ!T_=ZE=!wH?_^M6+n|E@x6BRyNDB&#SFzfied@Otto$Dbj%qYhnX)5SqYWdZ?KXQ{ zW=S16A|#wUFM9cIAH-wrJd+GhRqWv2xLu>B=}qt5l_`AZ**X2@!^Tn@y`_Z*ByMC5 zlBMKnA*VAd^q#aPZyP(RCC_#^lF{qEAYhWt4idMrAt5t&QFq|`t~VJmXJegJ!(vK= z8x2TF{E!@eXZ!NoVww2tCxoQhSWQ|;IjnQIprd@uU6nUgo;3NNG3hkTGZS9#$qx~9 z8Wfw+CTcGnS;!(}LCD=sRd06fT|4oIJHfW&j>BiBoG}OrU_e8pmY-=?-QXx6?JUAw zb`%&T?~i|iX(_!df(VmYsGhF!vpMQS$V!wn%wm6W*Dfct9*U^Be<4lL>^?#jHk@+T zhf(&5!lJjXc(wnnNT}lsqV}3^R+*z>5j&Lipn7^8)(i}hoI>^Dt!#OqKzG>WY}!lb z=59y4&tX%FA~pa90nawr^B ziADUjrMs8;w+5TCosD~^GwU=YjRYgRrMMTwL3jDgu3fOYpR3)j*MKN3$Rt(i-CZQe z^TKsiG#c5LR#nhYwjbX;#A+F&OTx9TBdiYl#ihYXMeg5LCkJE}-PPv!K>)4au36r5 zA&!}z-G&nRs1n^r#sd`_B3p^#Y_>W}mHGAa=WtxMeo=z5mg3 z(IQpNSMYXSB5Jj$_YGI^Nn)^pQHh#t7!gMtj@i|Qk zm&ki?fATYng3&$R>dC4>^LOxr-Rc&4F^wiZ#N-g(2uQ*F=pwL#h|vrw*2!UGm}vrL zXh|}6GAjnnw_yr0?cAxh9vP7;mq^H@dKoVnfXXv-eTWkf_vM0K46v{IQlJz+TIXHu zIM?50oq-VS0J*Gl&$kh~eboC-ggT`s4^<1t$)e52B}&*<;4)-Y{e4O)My-~4t96t{6azU46Vxq+keYO%el_&NTe9#!cRrz~|jOF;)(Bp!gmjtrvigJY; z=zu;*Awp`cbGk%KjDIl5Z`WFR!@sP&!YAO+GE`FxWvJAEUu5YsD3sQcU35C`a$A)X z0los6-qr(~9%JGn1+o5{AfIRL(0BKJM)~g!L#-ZTDlYHu1x&;Dt`_Ueu10(P6{GBv z!|{iOvue)Glg4lQUpg&t!wVtP6hzC2(WlqR!%yI+Gf#F*Bw!-*Gv;0d)6T-ZkCkx=u8fY7#liOyz!Qavf(A&S65AeI+EZGvWg$J zh+c7r?A^tRY7~WeP zZp&rx?P|;92BoBa%4HjAnZjufwumYd4kXJDxWL@SUSgxvO32I8SfN7Hd|Mr22B3K&;tILR;h_}VdLZDV_{`yVP|IoLom5`+JlTe znCx9BXpsIuw9@$#&)$XE7#vjxWHxoMW3kdzi|~E%>ItX#>LOd&Ckxo1V(0M{e}A*>^B;CS7_#H3Un40 zHFb6{F$Mu7tXx1=wzl91J}zcf0IjwaIOx#bh3*&jpU}V2MO{Ib4q#+6J1cuIqO&o` z!TA^HA1%KDe|8NFsiOXKK)IOBY;6HrKxdbyq2*#`|4YxnP`|M>tU$IvVYQ!wVr&ag zb8rCym;f3McBaOlS6r_&6&0B6%zmN%0tM@h<;h6?qPYJDWqKGp{xS%#Jikm1T=Vah z`6uX4*}$^`yhCx21p-+-R_{8R5K7Qi*;+k~7r(8sy@e2s2a_4l+}PC?MDxFE@`;qK)lW*l z6!`~TL6#?>fAjo*)Ul(hv+d6}g_$V}&=zR-6sHT;GuvM#t(obcYW|<0f2!Kdl;8Ae z+WzY-_)E*vpn;2^|ID2Kg#7o`{Hyoi*Pt-cKi;U|xF;bRXYgC{DN6XCW1-mi{(oFN zfFpVTcJaXe`zqqUcR>Bg)wRqt6>xlX$ED{h%Y`m%jQc44LA0ud)pgk8ZA*t;a!CrbHpA(=dgQj+76X&)3ZJwQ-Aoj zt@sWcCfkhN#B%H?XB!qvLn>Wo_V|whAi*F+%RB*V)2I4XN@36*(Tzu0iiyE08BXH;@Tu#k0 zHow8VhPA(59sPPa5S|z(%r_jK7--t9w(Nb+v%3QwN-opN+-3%36n*(Wy; z2217f(mHB~AA_6g2|o9ajt{SP8eR7NWjlqd-~3=)@CnR-w_8|19400sYMzdb#9@Yu zdW+TiM&LFUl?)bFtaP){hdWNMSKo12fPY}Uo^jRmsH5&Xe^vEbyU4et0Yq~D>JSCq zZv$4Ro};_no9IWR;xolDS>+v!ura6!4m+(m6^C+YVZtw?Dv?U88cDiI??jT7-!0P} z!qVm)=Oj`otxdBTyvQh!@Uc9a%&8ohYOV_kM<2gt1)m#THg6J1**-RldKky4o=r#>CSaf(AZ?Va5WRdn=+F)d+=nh*TpikRO-?qM>A zdXl9=6)n*h+*!_$AlodzSas8amxeq-K-u65VcjFy~Wc` zL>rZy`A)4b`b@;Oj#BXGP;!W&w$+c;eg+>_qLWCsLRCZ#Nz>u{)N4o6-e z)=#4cu%zAj4AU3B3r3% zFFn4#u1$!XBR*S$@H@HjE2UF|p?Mm1=D2QbzJ@hz{)bnXB5)r`1KDFeB=v0wo#iR+ z13nAjts@`$m3k+mWHp^BOl^7dXYy|uY~@F~`4)8-ezESZfSs9>3t97^zo3>8!$pvl ztGi6wQz7RBIg&gSeZ(X%b8U~Rz)EK>u34dDe$zAA$L*K!c8bi&j1<^bXUlAClfojM zQ<-m2D`_WrH-yGm)@DG6+6rs86~y zsunnvjf&Vu>_~zom~mK|i;`+U^kQ~gh^y&Q?tGJoA)7jh!Oruo_h#{E`8-Q2pxV2p zk;9tV!{Kz6)mY;8x`ELYdK4uk^XZ%WugSg6hBN2Mia`ZPmS$1@C`sQCh&1TbSx{ml zDQJ!vR4vsslbYtjDs78#*v<0y#^8z3eENz7H}UbY6=751eS@)b!V4><^5fV)8WKQ_ zJmH)Mu{Dr8%2L%#%fty@tmx3AY>k$D%VWVv2>nFm^omHn;`B=dmIoz^T5F8L(2kcC zIxAXc2TOfNO2>>~0r$KW6%|^STYf1pk23T{Hit{15Hegra1cJBMe}p|$Z$+|ni=`w zzPJm0n=nHLbjhJ_QxJ}%{GuR&To0s$JmlMn=W^3SlRk6MJW^5w4keaNM2B__E&9se zXG6Hry(4=I@Le&Rn*3B4W~a~{N%;I<4QMF~_3FT0sv+*yIa>yaT~*-*!0y(-h9-JZ zUFIFQ>R8v{V(8Lb(W*0{41Vd7jZf|>jMNpg(v_`aWAR=fMBy-8UyA#fT~xkv z;J2(8vscnwRfOo-JmaUI-yC<;$G2AqNyd`N*)hXeRY<*Lhttd|gbcq)wA+QJ%6@NN zJ;;p9v$*fnFe3bzC9yQSd6!N7OTAiE?iwz%(~c{Mh9nuX6YFPSYFa9iLUCO^A31g0MVjHUsb z6skQkJxaZOaPOipb_;=~h{d>Rhn2m{fDBt^l$bbinh;c~6*_5o2#tEl8VhWZ`>co* z1lcvb(o~ZWe68=ADdiz{`3@$A%v`M6mImY~@UybuuCal;wx$7SimAxuta!V@c0NJ} z$Bv$oFD7b$`R#1q{C3INz1nbv(f$>rH?|9KljO2j4a<9*tzNpu8Yr{rLaHdEsPOfU zH>L<9A#a!Nw2yDM6v^ZZEZG_4$>Zzh6h-&&3u~kEy(vS@bh(t-S#y!dg=4+4Pah>f zy7(I27iXjJ*t0#QJmc!{0DTuyHK0S*a9UfDm-zfF(c zdj*JT69E=sbzUZjexpO@roJBN935Oik>&Z$)Ohny*%y0?Qt4;vKVA6V{Vr%eOC0wr zyAjQT=smtT%ZHbdaSCtlu0se&Lcfxv6sqH_kRR-zx90j6p66~;3^mDAPrq&}%B&mR z@G=q3RJlPQPZYmT>g0&k)b08-@@?&l>f#Rhy+cO6YJZM0AC06577yhLp@JSD}^VRSUvh9nR&4s$1WOR}r9Bq>Bj#7O&LFhPYomG>j}H^ORpi`Rdr# zEZX15P`A8dzi%(3bL7ZDXh^G4(dXawkU(WJY2%d8jmm&HM&Prjg;z0#Abu@S5hJ*^ zlKGrL5G_a+go({JG(_)>hut4SLS>58X985gX83?3m|~i#u816MS0GZXCL}iJFR-{SmBVL=v7X*RvSg%&peZB} zTApwH%uY>4&(Ev5Y0x~n7AR1rrN^x19zw~FJinfLkb874WmG&Nh#%ONP#Cr^m~Oo7 zI+h^a&DosJ>{#$gD#e9r6Y-Q*eHwg+&Rv`6mpqzWX;Z&^r zN9hZ#t)Y36-s2z7kFwus58DT@qPJc#T&6%5u0J$MN(#Z!sZC`uk|paP;IP6I(K=z^tL!yNsT`S=m5<)>P2>REn;(|WnY4-Nc&-e(aZd}Y2zWatlay{on z97zNBTH#U`FtH*Qxf+YuLZ-ns2Hd>$#M(6hsoO6SZ1GzL_`lPi)y$t;&drA%^PYSO z&-7EGE5uaWuvZ~>R{a!?YA^sGM7L_Gy>3NJP_A<~wAA-`CWo8m$2{0wx#6lH{W5{? zgQb`qPD8#VEMEA#v2R|ynm^#|;X5oCzm!j`%AH6mZ>9(GdT4SBimM)Je8bH2Afuhp z0}2cSY}(&nsx$}!cJ@qcSturGEoq4|vt`F4lX8_-c;=9A&;Hfr^)tQm&*RBIBDP$> z@mTU!W-j2QOZKO;l^WPH_VoAHo!-wlFL2ebtjV8;FSTE3o(`_yluYh_%A#cd%iY*h zBIrN-KLA<@{wEg@8#@~~t&)`s?ERqwr=tAw^8B@tjf<80r;mq*m6Zkz^4muPp#AqQ z9sS=v9ep}5^e?vn81}Co9B};~{u%)7|KzX%!~8ss18Dyj_YB=%vqsr@|Me^?4t62h zI#_@Q^(ng+oI?BiZuC$8BHNSi@hJ-KsYg-{&UOIKKaQt=Nl5*vihrZ6pLGAHbX|^r zIp%WwgZqCgVfQDKKPCG=yNBiYH~Gv@S;D`4We(0yBkW@Ol-bP3$^|Zn8#~GXtt>1- z03J48aQO-Hsg;Qx95RUVl;LXuPH<*_`U<8a_A7^(iJO}Xz{CNzY<5<#y4hGcc>YXC zRx-8&Cs8XYNh*tpF^D=_fqi-?EV5wrTbYX5TiE{0bY@Wp0qwK^e4OAWoj=%ffp3d{ zF#%^61Ht*se_WRTMg2choc|o;Uz7D`4m8R?ywe)auE3uyU|LqrE+BDBW9MJ;D;WP# zVq<^mj<%H<$nxj>=6Nz$Rsa_`4Dspn>4yWH5dHJveVY92KOvs}K0Qxxe}7Q^Uhy00 zH$iYVI5*GFhX>64>G@d(k1-FJX(XNi{zL?Sf0FH~?)USQ zbi zpGcVjz$W+8du8zC4SO<*pR@W8>-h72f{*Jza`C*^wsTnKMt(eeMP~GBDo^u*nQt|1 zS<6-?%%u51o&&pb1IFIhedVU+{oMrx)y>6z%d-hM%kDT#QsDxb+fksMj3L(Hn_ZvG zR!{ftLGer(#R{@HdJ|aBgAx}rJ<6-WUUtt~eWWVGFEmL-%lKVuG>>ICu8uu;X-GBt zR3i*0CQ?a!j~59U2DaQSUX7#eZ91=IUmH?qx@YMQ&D1UVTD%pYFK)|Z8Vb()g0m!q z+ofy0JjlG?8lG-)r(E4ee-wW2ZHS`{RHR!$3r&DoNYHq8G~;9)^)V5g^e}C|s+m6i z^1@ryfuzw>HU9_QCpknf=McG$jC@^C34$t#$tT^54@zNuk zw0MNB@#c^4A3pGWvXSDe8l%)jos6_;V8a|mRkVm`5zg8$`pJ&s$o&rn`V-t!^>IiW{&?!lvm*LzBqqV0X_O0B&l6cd{ zIfK93W4sF!9ELN!9Qnn>X16&YOl8+|8Ig`KBrA?qxyF6@QsOaC2BSnRx0}o#FF34D zqQ0)r9an=;b_=Gn8d50TDE&5xRr0t5i%XD)MsPgljpO8o=a_O?EPgx`Yw+u_oa%!x z*W~s-zsZC^_sjSDvdrn%zEXLDscjn#H0H`C9>*%M#r7{eAI^KUh^L`fbh8skN`)BW zcko^J2TZ-Yis8fFX2%ehqPQ)Hx_!KvR#vwOwW>jS^Tseu?T4JeZh&BBQGP>l5#0qk zRbYSACf!Km_jdBa5!>)=BG7^VaR{fc`3JQsk#i*8ka$XxTESQ`AlE&QIVo-wTY&0t z3o{jtmRk1_Zk!)%1YybqD~%TG7f%|k$P)F48ZkY~pbCU_0GF~^i4K+0x)U^x!(~*x zE=85hNmBC(k#6%lcz!v9W@h(fO|5zD^!>_(xkigd7V66o84dC}@o0jNp_n=!-ijLL z>-eYSQH)~OSH>rv$*av0Ic$xyyg85T-YZ*oJvhi;()COdG3I@qj4H`haIDu*@$78@ z+i9Bmn7V9I_~!e|+INpFWz-0X`hY?$7oyh?$OI_uNdRc}@5w4ZH1rU?s-o$#BFST} zp&bYfH6tr`D@nfK6ux|bu5#8hhjgAp73JIpG90?iIEl}7hVGj-_L^Fs?`WcmpO!RMFBKSrs+d>+a8uw3N zg?mgO>9iYgtT%Nxc~eAngJ2wVi)1)D6eN`^`CqDR01x_KF}SNRBnbwzZYU_`V$ebY$@tI#p z2#bmO=r$1uA$ja1s07=7goTEP1#`&-p@zRJ)ANMz$bpnEk-z`ibt*B;*H+U6M+9M; zpDBujarDvS*45ys#`QG>t4m51s$)zsL}L;L`m`}i#&ma#7Ce3swg``Z7@Pm<#|7Vk zhTe~#_D7-SC$3X>{G>ghJS_12tZ3PR{A=D`!2+j{%T~0m-&-J{NJsq)W3dW1C^08E zB`7XcQL!!iOI{R;Qjx4HuBkDJ`O-uHeMMhfegW2Kpf%}RWe>AGTY zfyZc90@!D0+(W)$K?HT1Sy&iuQGBkSn=+SSkpF#Afek^#mg}6$7`Moaj&M26mDiA3cM_H^J_ccYv zq-i7H_4{{c;R$sO_{KDA@03xe`w2JcS}TA((4)Z@Cs7v@5Ix4NQn}Acog@zkEFZbN zX`lDDWoxBh5IL4e&BBvx(ydhhVW{^Ag_+PaME|pu&~ikbg6a!YN(8tCi#0~W4PjD z)7w&wcw1j-`>+*Mx3t^K%L8IY9$?#&dCTbYk+9o6`?G^fx8|2yi-+-#=lvHr1}mB{ zj{`16x+5iM2*@zWH zy*{PIlC7&!B{v!{OVexu-*+4H1#>~ye4jC%xFwAQ%RT#xh==hS4{`^DV#?@Mf^5eg zj8jN<36Jyp9}u>|aFG9cX#2S}{JqluyZ=S=d-(@%XxKS^2P-`N6C5sH-hT>LkO|e- zc;=y~{rqwM%lBo%LB+rzLCefNnHXt|jm*#(Z*gf%PZge^p+pHx346hOlsvf+s*-5r zGJYw|nF*|T^dNj+ci~99gSy!rk9y);mDC&I+0u^|b8QjEW4B{`HXquuei(ZDLj>kM z$K=C0gQpxnZ$gD8411Yops{|Lp*yJt`+f;-A}O!;Sv}l%b8qVvz7am}u)1z_!O>&7 z-z;2-)Xs8Yt@ZfTGdN6dD6F8Q#HR37;wwe&I~()#Tp;7N?j(@HNT7317>M(%yqGAn zzuGq0FC^B5A$*y$cn6`%4D$mq&Y`t(aeiIm0rC3}qb}MBGg*6tNHZ3vxA4kaZ{KC` zytoK`^c}y)9Or>AuY_=mBK46Lin4rdI^!Fn^%Wg9Mf;}h+mE}UQJY8KAFq5y=PIrY zb1>jbiQrFYT1AO%F9@P?@sG$bQTHWK8gfvdD*y*S#Pk=`_&k$LAI0-q$r!h~{hc)1EQB@36js)T=Nr@HRzuRtfbQ;>0y3V5To;MNUnDN1IJ!b_r-+^uhObQfRmM zB8p(5(lcZJy7;xp>mbU8g|LGrnRJ1AKxF<*&A0HBXkzD&V&qDoL;C_4&`@`@K19F57XT1qJZ6K+jmjx@eTd*E8g7SBr-TC2y;a$Z=SiAxxv zQ@b?WEu4~qBO9GkO)Mc43uq+YsM5I&-XMGp2}lW39rJK7 zJ;J5+k3vYdO!iKI9B+I+X8g5M#XNX#sl@-K&>5@73l@ z_88}Vm{;}MKV93W4t^upbbj%EbHC99SKAHwfOi~(p)FI0j$}GDpakBj6m4lJe=3yv ztUOAz#b}WGVfz&*-u@ZuO9?b%+_qE@+bc#4y&k2>F_4xI!y^*~&>Y5o{;5w8U=tXN~n%*=ekwk*$D>Xh%Zs5f*oX&zR@R6i)d z1rOr1$c!vyl>TiJ8Da~d{LT=V=Z zA1A$fZjDvQ+`p$=cB%>H8L&5NB21I{qsmdqxp znqUL-#Y0WsuVS!-9`(|uN@CHH*7oMKd||{GD!-Rk{nF#Q1vBp!B&4lpTc^KVwcwZn zka8Sp+!CP@&FU*YC4ZT?a-EK;5DPNckLO#4rAgG7g>6|56|V_P%9Z5x_?LNtR!>45&bqzCrL-L{bL#Ym3FC20afqpN855sNw8RJQk{ker| zrAIHt407)N_gyI!(l~3Fto$A4;H@CuXh}=$w@#vuQMC@Z!DdNQx!+XrSN(uG0rW0Z zNoCAiM2TWk@gbVbC!q4!!LZC93?5|)8>w!A4p&ZD(57b3<@1_c*qd3X&jJ@Z>mt7g zb`figx%(g~S94f4C}395YIcVdqvm8>ZW))|AT^{HrdZfZVU7pz*9Hl3f0ehDps2VM zf<`0RNO$pPpGBUmG*6N3Vi*henLIweVgFo>{q935Dj9U?F6o-4u#~V=`rha#XZNcO z#1>+XlV?7iny>pY`i9l<_J^MCe&J)B zd5*bEH(;%bLx&KmB$Thh@f8V$N{RG^@ZhJp6-LJw09sDY=Rm>trk3v@xekm+Ip;Di z-lSQ%b4?%Gs5K?MO(>8UEwzzK7oXR)vaSrj@d`G~!Jmir(ehixUK3AkTv`-ykxJy_ z3_#*4j;(GQ*<%D%wG_pyfrQ#9dFe0|JIO|ZSS=)Wv#7%q3Lz-SGbz$x-iqUGZFtR}Q!j*Fm|SrtldZr$;{AUBLO{L0T@pC1!-#zPi>UuC*?HG=SKWiVJ*3we zRj&D$UT>Xd{3h4+;I7^OEhu0gY%5|j3cVQ|dOd?UAz$NYVs(Mbpw%2w^H<`p>TZ1{ zy3;QyoUIw1^s#!GcXlBkO^nw--ctMFn3%L<<|V{9HSZ*sZZ}5v&=KF|FZbIUYHYZ!d2fO{K)?R&!xHyBi6u7tCj6|w zPwuxG@Bf!K5)zE~cz=sn)A>v}$JD@jNK)eBR-x;jaE*TqA95I5cfrU348Ps(Kj3w( zX_(0iqc+!dcdg+++*fOGRaX^W=bU}cy-<)qyimwc5<&_TLP8?12y%I)Km-~>Ubj@j z!%7JV7F*G2%VokNToSD)ObV1H5hNx!Tp+c9sS=oJBXtVeF;p!+`pArJ97{X2Gli1l zxAxiRa&urBtop~y{qpU#_G|66*Is*{6-g&3m@%d_s+Vz1t6LK&9rYSEMEyz`f#Z^P<9oc}6yc!R0^_ zkgvZ=>%><{gTJ0JJIG-8iP%qn0j7ynv=FieI}6Na+Q%}mKw_hPLmZ%5<0?%?-S@1$ zSO>mj`Q@m8Od0w=DBVh<&4{nr7=H_J5M%yb#=|OFg89dRvqguDq3u8w=6c`t6Z;18 z7VPU{%=M|CW4MtDfcvp8VPGxWgZVSq=e+M)QP&i-eFtrpGN2zMfwgl?av$!GvC>o= z*572RN1JN+^>1JS1lR@_GR_oG_cF#A3|PQWU>wi^)C1)}4)6jn3Ud?YHDtUZW;ixw zd>}T=Huw;>$(VK4a8T~2b7tz)SUgXPb(G2#Qg}>0FC4S5<+wXGpDB(Rmt}mHbKGkm z@H#oBVvMsVI_ZA4L7qXxmdkwZU^AbGy?P;4g7+bgD>!D^K9|`q9Ba}iI1`Ge#;r9| zo)NG?l&WIA!e!SNFFF3GoizJ&^D@f(qcc} zwb;u=ke~5z4a3*2tub4%&L;S?ot}kHza`rB;i6qL%_jR@leF_@lm3(}-@;yKtxg>? zBfjq6)qfu7m&_*Q%qIIiv)#6_p51&Ve4Sb*knA$_Cx9?;pMQ*2<1g1*e1()?5nTn= ziy-BgL7F65u|EOid7K643(Ys_j8RQHMZ5h+Elgp(owoY|v>e|b{dg|@5>+sGeDH?2 zIO-O2-dJ+kjaM&o&Q~!t3~b^2T~K&O;bY(<#L-VQXO2mnpXL3491U!D;?=$ui_i0P z7`)4kPfwhdd*ZW?H!i&_?-t~D_ng9xo$mTMKXQ)a9Laf(^Nicx%W?f9dL!bP^YE(_ zQs;WO!hG2HmOA^$!`Sa)+qQD;BUZEBU<|MutSprO0GRd>tb0@JPA##!)O|YIy-OWE z-R0h+W;U%;`RtI)Hy_fE^pLxw6|8BLHHAPmy0s^d>>U|9fOS> zIFHZ+OW7xKFNp5udx7snV-NhycTqJ}7;oUcz|ko1M@kGBZN7yANZS^-)t-xHnmoLNZ4I74;5Fx6RwICBANz!-IJh-%Om{o#;PI;JE7;k)A=pK zZv;Lsz91!9VfhXqX#>AgFu(CG)fw}t5^+4kzXyKUM(MaOG{oLLz<38zeoCRrfz%~9 ztfd$U^rX~h(`$O8aTns2@6c0X3q2x=D9ad6gGGNz7r(<<{e<=#{b-qZh4zaUs$y9r z(kKJ(T%#FpOV(ec2UxGa2DyepVk-Jq(l^C2+Grd{?ERj4i-)n-=x2T(aWVq^Kg75S z+S?Q|ig32Jqx^||l-tQUn(bnSm`mxhUmT>ZE<85tdGOnO>?!Q!ayNTnYs^m9#@pB@ z*U96eAGZ4B$xw~t91Ye6n;)~B_9X!amP>4 zo&d&)3G@Q61?92eUjfG)8E>}<^aijEW1j_|TdXt?E5^x{%!^ylK%ZC1yl@uLAyl5!A0OA<1yOA zfSeAw)b*!!6|u%>pdU$nZ2ufRVukRIo95MfW$(WBBqi8Sjwkct^J5eexhx z5Zyp7`7{IgDe}+~@axdw_U;-D^hLQpbl(6!OLYAslplq@-GQx(B&t>K%E8e6LiP2l z@d}*&*MCiP4H0$inA87yBd`YL4}d38{xkS2xCZUtL%*%yb58w@Wsn~SUWD8Uc`ZG2K?u*8M`+U|nj?36j^@j&xD)lo4(ffc67&Hw3Gk`_FGT;ZmbATn= zv1UE688`!6ku^bM(23pI!4}XiNhjB=&647BM_l@l6i*hHI=mp?;o0+?W;sr?@n3S( z<7PTMa;(Dx(z3$Lz58ah&KPJ6q~8MizZkFj*e1?1{=V-yzO#evJI9U~>CJCRhHVsODGG$yU2MCOnaw!z=YjdF(EDB6X_k>m#Q zF?tm+31G;_aDGHSf_Lt9xSbuq3BU+o5^x1D2QZWCaDE8q2jl}#y^k6I3BXps2w)O0 z2Vlwjz&v@6J{%0DegZ(qd%!$-7pCONI=P%&MH+xWXIG)|t4IcP0tNtk0A}#6fp-n5fENI#0U7}Q zc>wbOnalt#1FoUv00qzq;K&~e(2Y*66e4X>vX=awTtXgL)ZdZc8ul`|XxO*N8-{%o zYyj*GxmXBDC|M181a)|@JlF=vSCQWo>zq<4DUeB69tpSskN|W5wgQd<%w&=*F6@+? z(8zf-1J6B)6woJz{RMi7L&{T<5?Kd(Or{iBvlcv{5qTs+l*oybP^1(&@2dlIXR%sus z(mrG&`{;>oo9NhnQmC(o6+5Lwm()w@1XeF#brV)!!s;Ma_ha<{R^wRRg4HNiMXUy} zs$lgzZi1yyvGUqO#b(8e)fudw#cCd_5v+!>T8C8`#}u6e3twJon6zPvNqS#_UEA8| zfH4MPMT4-dgK*nV0$%}?jFJK(d2w01FhK3%Vtt}qUbd$3>EwDc4Hc*1GEAcn0VcRW z)3A-Ep^<55zyX{9Yz0gK<^Uyt8Ptnm;71L105<>ojFf}XAn@l|7B z4V6K6&=Q%3b1|Gj5>))W$Vd70%ux|L0=Oek3XmA8h3|Fb6gWZ0wz1D`Uwm#umZXIo zAxDuPF2oBJd$e%dFX{Nlg~)j+>BcXjfC+XTZblIt2HOOmezd$Ai5#`7kVswu+gK2L zpqir)St?CnJFPV)-4YZ#L*0L@q>h3}!+$ zq~l_fbauv&9)QHDg0!ESG3gnxUD_=giNSJWOCCy!L)sMSlh#9fDRH}`~gl%?zmHp_OfPqK~dQZ~p+te^Fq->9P?5{bQ$ycJ@F&FZc|sY;c&=fNpbbc?VKOAs)rK?L z)DD!{E^D{CLOQl??9-}3ZCG<=P^P<0Yl>!cwzNr$MKc=P`St85#z%4x)yNNYjJmTr zF453IpXOYb9Y+`o2cP#*`}o1_ttx7dot056ZkOV|Sf)^ptkcfDZA?bM_X{bmJ-M)0he)yYYv234768DwQmvluPp%vH05xj`t@b zvDYvzkG+<-J@$pf?X@i;Zl6_TTt2Ixad~Y4v*5K!X2EB*`g~zzaUo=}dA(te-R1Gv z-6ZT{7{s$-!K~vkrO9r$*{oIz@_D@;53;&lE`e{gvt~0xT9Nmd&2!8awktw&hke9O z?88CpF`wlavCB@$N6}fVX454mzlo{OC~fo z03WJa7JY_){m?QmHIDx{RxHPxH-Fnx6I#8pCb&9Svy!1$=?*bLcaRCy1euy(P4FAN zFJIa)KZhH7PxRuJ-WPk%zLmkX^Ot*1^vqxE9mZ=i^KUG~XO83D$MMRuR|{`xeCTjLb01uY{jI zXUzEStsDHU8!){@oQ@Vph_EX5Wb9vTR|`;7*BL(N+}G}X?B3mb_q{9&tBbmdM4p>q zxVAb?hy;^}uev1(j2hM%YA{5Rh#Cb&qu3N*qZO?-#Y$`t;tCi|lZllXng`aInAVsM z8pjF7cBswN?9%@((xjd2-Fwf@?#?;?`Tp|KIxJBr-`RMqb9qYSbg(4^vo=zzj#SpP5amkcgAk zA6{PxAWKe05Uy;y!)}vFd?OBp(r0GWZR}f8@?qjBd>wz$cXm(nl27i(uii=gBVmBm zHL8OA8CG|qMy*)D0`I{dp5~3B$t+tqR=~t*x2Xz;Xv*NQN*u>BPMBS>T4b{%3M$P} z2F$aJfROB0v3j0mkZ>Fb)~ZR&aim$49OQES93lqggi1!@L%MV#g+KwTBMfhB85{NC zp)3?p^6d`T0?Xy)J6O){;DQ+}mywqr)G`l0rr_O_x_nD!L*2~kH4m2-7u2qvv61fB zSunYKc13H>)SXjIxm(Lhx4u?fwky-6`;rrhYxq?(fOzy|HxGjU9fUSd3u3B}5X|F3 z#KSR>Dnx7n_i!mHN7bkSwLx)c6OQZylt&^XL-KHv#rjhomgB<^F+`f3n{89|$nx^v z9+FOiW0RjkbQ}EMMknXL7l}XyhsUPt z85kHyda2PbNf>fMp=Zt_YV=yC8YZ#PYnopT@1-!I4pXP#a9fKo2pmH>gPKL%LgW@O zrM(*=`g9%44C)(}$Jn7OEdm{c-Wk1)>~LG*HiHvXRf#jZsSr-L1-<$i`yTR7kI1+>0s$_;m9f!K?V8?!>!2HyzCivyA^292GTQv zifqrGk!O*Efpm^7iw?HccCP+W)!>oKYj>Y*udi=!UteDwA%mF4Gf$SEPK;hnBogm+ z>^p-GCH8)O7q7rocV1}JbMZL zld)U%9U-hM+buGhWX@s6x0J5iLkyy6bYpT zEG*0A0>`q+{jOPq^Y{L3`f7UJqxGq8{-nQLkFOB&%|U!V^re>f2=fTr%baI>xr>HN zUhZ*mu{hrp6)R13);eXIwa8X(bsM~H;GDtjr6ivmBgamcS}>fhoZXU7WsomMdNng9B46Vegn0CP;$E^5 z`H_qZG||#sM9N7uX&^M|Arqhp?s{u1p}@|2A)TbjR$4S1j|^GH%R!SVGh_n7jwua| zXSo1HaOA~_wzObAAd;7tn*-@hN)U1nVuZ5Gay0jCK5l_wetNrO(TToeTc3LfkX$f&lePnrAvCB>VkEmX zM{1PlIhKW%S`YQKeWhHbR@z^Z)~bzCv%1Z5T;dr&r5}$3L6l51hXay`b$~ROtQR}c zWF+C-E>X16&K`0MxyTA_g3ae;Xy0VXT2t~V2<#e7^cSXFWws0!^i_gj6}vn5eNLj)1g4=K#t)EECS?!NO%>LRb3HD$hiFf zejGQKvdT>@iCb6R9mTIY z_c#51Q~eS3S7gmA6`MA1_IF=g-dS0GFw=K#=Y_;SZ$iXwV5iLVfO;e}qUBi^i7Uhd zV!L=rWad(HrC-vN6}W{&mf{#*pg1IolGIPpU{q*ILPV6Hzx0w`WB}XNrtyfT;V-D4 zr(2-K>NIsrChF- z8{}OwEdvjoNgg^ea>`EPZ1G$Ba4s_AR$y^Kr>Xmg_?Y^?eEg1)db+icft{}Stbf0U}^Dh*ZEO8;uZOWrMpM(-8FWxIs~L3d^N1O7B!*_KH@O`0bW zpppjh&#jukY3J0( zqYGx0Jx^x!E$@oI{L$tw5<`c!-Rk&jJim13lP?}Sx~}d7{fwz9t0e2uJAbKoHu0}d znupipCvZJ(fA>FJR|`;7*BL&K``o>|d-ut*u$Q|oTwW?K6%h9p8YTza0-}N#oZ@g4E~lUDhz^!dvGvl;cVFf1ckOsH>WDt*x!Q%l z!=Js!I=K0e-LDXlg`_*f1tf8dbZoa*g1L)Aa}%9)$*8)$rV{GiSeB zy=L3a=)EgfqW9WYY+YTy<<;7{)@ifa=5Olk+Pv{FP0DIewt^^a#TE z?5=ZoL4C_B%WAf@42@K@%{|n-`J+zYRWCqfD`&voi)d;{=XfNDG7(BRT7m@*d~OaZ zMTGgE_oYz}-N-TxZZx*2*t(oxcPX5`V0mfP`SU}E!TOSsVTAZ9&ykUngNrJF+zMMo<|I#1<*ayJp=}CQ*u82KFNkEL*enupx9!r`@x>;>35ORJ>`PP!2a&h5#5Y)a zql7gQgFkP3V><%kvrQcsunr7tWO9=|L*9a>gaW+qDZ zYp_cxnr26m*ANz@8H#VbU(;i5H=JF(BnE1rG%&nkaLn4l3B|1h)V3(cUc|gf+?qg< zE3gM1@g7zZ#DV;{u`_}oo%w}NkofmK#OVAyQd_rW*E7xMc0_mM>6@m`E-%}>FM1WP zS!&E!G;P6~JEC2DUsdnQrH2aAPdC>ctInhI?4H_+S!**Nbco8-)n)Tu$Yb@bcH~?B zH_%`CQBSD4hOF`fTJtS6r~$3>H=`DR8`{esp&wX!=}F6HmW$|y|E}L|b@=UmKg|*{ z>{-cr$}cPnUC+1|Cf4(-{l9f=ckHF_SobD(;)A5q{*~2{Fdl3T-5$S-pq=(o}1GZ{etgv`=447y)bY%8vSs2e%F+|nYRz!`frxT zP;daa@K0E$87&UE1zwXRQACtu3pL4_8A&3`S+eSwBrc${b(3x(CZC0qM^~*BFODaQ zwQaFf{ct1ualq&0Lh$au=y`xk9ok2;hrXgMd|y|z^y8?di$&52k!*oTWKSF%~Bbq&|TKdb=`^a2Oc z(B;s~Bv+D~RHx&mk`p^5s1P8Q3~z!DY9l)^kT>5=>C%@aOKtg7{6|G2UeU~yQTm6z|?J+ zv~Rfc1S$O(4}a4X>BUPgU&4`-bvdz|3LJ|=H-Rh&N6urxHB$>se0Bq z0Og(TNgUN=Q-|rIiI{jo%o3167eyi5Y{3>{jwh5s%?Oe;kmjf+9CAyC8Al{+F!#0@i0C%Y>RvMs7I^GP*8m?+=Dj5!s<%zXl45$V6GyP1h zAlAZ>Z|U-wLREy+m84EsN7f6imR7qU6Y$%!`bWEBY=C8O|20lXEC*MRu40`5Ki`n*>bwviiif9rkGpz&EDZSXV1>t znZ48Od-MMDd+=(vPr`$TDaxb3@2O~my^X>OXW$6OY!r8jV^qE)(1*N-xFPzdUXP9yS*~HR!LpbWC8>tx zz~scomB*0{P{VP&PY@(YmQ{-5agRQzV|^44L19;WhPAjXsFJ3MtWo&olGsHH`n z_7?^sA`G-^tII&pL3qA|onKSp;hO~&&!JQzh$B}IG{DxMBT8A3+x*+MuHhzPK#NTuR+ z41ok!^P5$wzK+rwFn|%Y2JJ>o@1fS8zv+y;b;TJS8bi_Re?pPXH(vR`xsKmO>(Of$ zoge@CFU}ih$I!~(IRAC7pav8fM#A@%3uUB z1}R7d&HQ<-r?{{cM(`gtrGiF;ro(Z6=5wuvcXGL3&}jd*?&tjytn?V{Q~+UEXDM1_E4m;f zy{UL*$&=jr5=PHX{(V>vH$Z=DxU{@MBlpqr2DQ*Dg&yYrFdR+g=izy^(35$40F0!( zpm>3`Oz19NU;G_@le}5k;oGgeDxFZW$|v&2iYkkur74D{DViekdIZPA0iQ?LR7qq_ zo(}}Wp;$0T#YUt%ypCrZtN@5*KmeF-}d`_Q;aD3hp3!G$mk z#RZfV2sn9{Jfn(*+KM=J3JEB32H8H&R?>?4pr$`a_BZNxWkiNF35U`eB$5tIwu{qh zsR6A7+EiwAO)FLaM~lkn85p8d=0=Kysw{pC@4fW?mUn-@xMKOD$vYR9KhseiU-&n4 zY{#J`uO4@5*wMDJ%}1|AlZho8oPJdM!v0x;H@AVVZ{9rbTRVx$bWeWF{0pe8hH~tq zYv?u10R0@pB+DD=bn&C~eD9*@+>$wo*79X^m$y6GQL(2;u8>j*oS@5-O-e)AoaEe@ zE3Fk}%aiMbZ%b?CCyicnv#>?lqHI++BnFZ@>Ak`pX|M94x+C#?@}P7`IpmKe6S5?* zabWxi=k+i&_Ml{<6vhBWBh@d%F1{I{s#Rp6cGQFV&;jIullYXKtd7M3G#jhtBdPEL zK1Efc%5YsgrK6PobvI{gr?7DRrux--M$Ki$q219SXb>}vD4tsuQkj0p=%VHruCFUl zexf{;YHVm~st5NMDjmNO3@|}A8gP~;Qr+jI)ni+q{n@hi?%B?|rB6Tgz0ZDd{C_*y zQRU=`Q^(S?&>uSox9psF?Xq+G2)eF5^J2#%19Rp+RTlh4RrB%QXMg*|)9-JW_r0`z zRa<@i+KSm{H*UBx@Z1e}hZ@NHQOG7Q)n-d9j)8js$UQ8>XYs(W3j+w9^H>Paq!A7F zS(FDlnB_)}$v$l-cVv z3$MSf^xo+YoqNY$LedSo?cSNU&VKVEGRWF_P@h&V=AQ zJBJ3j1038VWJ}ntNUz4|W&AqEn2cx^+s@(|Ad~~_1@;!p0;TL87J7#Al+r(*>8~Qq zRBsY6Yzc>Mnh&)Q76Gv!ECLxgs!YuU+9XSp8&B^B^klM5x( zI0(F6o9S%Da*1G}>>^ zsiaa`rJgd=RFjt0o6PytJZ-){&+Mc+v`)RlR1b596-+aLcpfkK$|r~-FUg7`8b!M9 z4+KrqpPg(OW+~Gmy{K!Xw^#asIw&YR06h$ZD3j&5nBO$~ep45DKIYfK)HP92ELAg9 zRnvKqGySZhsj#nDzsS<2ssM0v90r{>bzRdaE*uPo)rWbsl(Hxhu6}4X#iFGnmW2=s zg|cYh>4Ij5LyL1jgt>4iXD*rBJLjV*<-SjZM7AOQwfhaYXYq7C{AveEyh~OuU4l!? zrTf9Kv^jsp(BNa7I0{QwSPeApcL9d|46qh*Fsm?6`B!R-m;aAtj)2}mJ8Px zw&cdk)&#Oa4sJ`?0gbaqN+@tQ+tTl`q?4`Z`99C{dEj95Zoo#FQ-mXjc!M^|tr#|$ z0aQi;sK~G=B>;-yh>bz$t;}Lto%6hd4y5jnNyGwmA`1Y z5+ctJ1u|Fvv;8o605B=_a!&p9+wa`Tj4Ft?f)^NtP$d1x%i%(~P$@@cWVyZEQ7%-7 zrE;0V%1VwLjLYUuY#+XO2v%Wqb&Rf%5*Lj>gF%^Bk>+k4TF2oqRmc=@Q86q(Kvmc) zIP;D(?DMFUeG#`%oF^|s^;DyM1=k?f%gxlYcC$IGlh-L*sBO$PwuNf9ACpf~r|f^B z{%Y?Q&&rpnetW-oNgfNTb2t#xJ(A=kNpXsTfFwz9$%?|kvtfZFd4=VWO~Q(URg@gU z0rkK^VnN8^NjJ}vkmw|du_6dOPuPn*$O9_M;SC=1?P&3_9Cjz$!IEq{YCp1gG^g7Q z)^^yCYNalTl;9$R%_015&nH1Mr(?4QO>L!oeg1*eKne^?Ua9-^^&)+rln7H`iG zswZjhymuK>+)X9p)CB0HGmSkuC$#VaZt zQ&c!+S{SU@eJ^y64I~5Qrv%+CK6g~>z-1Z!GGgcE6;DBtuZD(j?0RO09xV1`cH-~x zA2M5;D{B{^%|kWc-^ARw(%LZ@WU6(0rUCn`zE^JO6d6NUqHQsPks*YP?EppRTWzk@ z{^s&KTsxSwx*J#{4Nh?dJ$bGY&vx`mcXy_NnbUH!yT!aWF?<;(fO1G;rV&C2P>X== zmr(zSox~nM*tH~fn_59|2}|}9?P%XI+>a1-=vnwN7%okSNRHI*)DNUbn~%LOv*0!K zzMsB&%MHI2$uywD)>Fg{jiO_WO(JoOK(dXd5k5eX`3%wO*tHPuI{1H{00m27`Y8>f z!(stX z!=_E<1o7~2Kc!I*5(PvV$~Ing2#%;<2t*5oXfz=_;3>!*e7`2^Y1S>>i@W))8AF_rRQ9M{UI;XRoxdG8;vKZ#Mmlo#@I*gkHwy}H^kQR zFY>4Oo5IbQt1K=eN-7G))rmp3Z*k$OLR_d7iIw6Gakn@u(!0er@tR1AqDHFic)#KG zwYW8nA*M#yi5kfjE|HcH`N2>-e%z2E1`(FRIpGQ6wlEzoPFM;blNXvn$6< zdMxcrZ?86%JUo8=_1l{#m7{Y7np99zG^6hE*^|~m1h+s0bx@N;WD{qNRz&m85M4@7 zrRhrjppJDtS1Zweq^;I>>b43c=}iV^dS?bw_LT5rXUhANK%@OBVYzdqcV(bM@8)~d z9{)d-tLjz%x7ioRA}Gq^f#BS&%1AXf=dLgr)Kfq>5Upo8c-qI6V=W+M{$$4oC9X~pTLWQ{a5XmWSj zfWkH3x2i{H;|2VGrwBcD7?oXaU5lj@VuAb!hefNF?X?Z zm2-oWbRtu%RTF?XYKb~xwN*1N64#-@I-MNgx~y@GPop@^iJT!ZHw*qnh>Ph^U4Tuh zG*76DK{`{(V5T@L-f2~@^Z@Lds^{iaTY!K}Kp1Em9B8s;70{|HQ6#~@lIp!Ppdi&0 zDdCFC=2-H>RAM@dOn#Vs?LbfFzfJ#q={JAV+x#0AZr#84=Z&wT&FZl(l#SR{gr9GF zJ8Q*LpMG`rlTDzkY2c*`pe&cAtPfaB3Hd@?cu1g2-KE+BJdb_EJy%~lFM+_xi-2e*E4eH1k=S_V1^mW z^xbTRWc*{|wWGqS830K^U6zKDz1B2gZg-_+CUvmXOlrvL5r=`yko&H*$nyHm4c|03 ze*Hq->qUo#a$1|$zWe@j>)+n;*2}lw*^S6;b0%Z))-)`C@!20w_I%L^KC6ao90SVo zfX~hwOLao?;Q3^VPC4dt_2deAm7|`sON4|Z9Fe=}TkabHrdY1<7i*K{n!sdjj=aeK zh_*z2IULxh=MYjj0BUf=yDu=2dEfRTy67nK1;+ zE2btim`q5Y%s#ho<#NZhgMG@Ta8YvQ&L>crEgZU{*3cUPD& z(;0g|?ON;I``vTCa}H)Rk@e29BHOl)gH1!RQsmObuocU2xX_x5{P%i`Kkd1j`3&oS z{Rlho!#hgni))UguX2lQ)0e&U{M&e$@2xHzgjrz2lQK6l_qEWu-VONJq1hYGFg^Bw zJT-$J`_P5t1YO3?z&8R@1Ia)`;3u}%?Qh$q9Q!1DN1!i20&I1Ya)R-E$tmiI@tFki+)Y9tnB9%Z=G})btT-2G1!Td})Z~#XESlgLG$EL;0 z%K%rvLqh#0z>lCy-oJf~7MG0&zL-k2FvOuv87hc#c)x60S-BfcF@ zY@&(5f=kW;=S}CBlLVcW&RUp?foSoLFe4=RVn#?1UW=3Ip;53j!%THkL#!sOs0vfe z=QDg+?JNRxaam^i!13so{(7FE=lXMM>Fga%hy4!R*l}&-*(bt=-P>px)!tA7v0=k$bfhI*Q7inoA zOQ06q9MmD24rC%)~4eAWR6Ie z(Pw1dY5%t{O8M{byA;k%L!NM4>K+@p;4X?ogd6FC>K3dEVe3Njhj4`wA`n_G&xP*L zuPJrPE`FzSmH(KEXDE))LeZ2e=ybW#UTa@YmeV?Mx!gqd2q)#s^bh1ubco&+|4Z*n zUbmtMJWn`EMOl{MM3y8|q`D|lo)A+|bU{!Q*m)vh*k6H)lBB9gA>G*7DNK~0BqJfq zzvQ%n^r$A{Oz?dOa!4)UEnu_X4$ocxRo`K-g3^qp7Gz@+VQht7dvb>vL0p*XKA*-AZS7hTS=sV~Xibmm%9z@6b zI>Qyf=DtpYl^dN}1j~=sca3)X+0FkvpPT(U zPA#|0Dx+gI=ZXMm{LbCXCVc5e=F|^_-iH@)N2W2omJ9C9tN;WY1Q?WC=swbe1Q4b2 z>2ZOHQ+!&sPA$pSiKbPPCQK!#5ER;kn*ynY!-&8Kg$AKn7!wFg6@}xogNFSlYvAO7 z0<|IB2R4*@V(O5`oQy`Zth9#jc55YU1qUIWep#KJjIxV%~OGbm_~8}nlfnORATAa$@gTy?FLOb}(88lYf!U3BU-*iskP*^DU{HoXPo--tx`GJ?O@@JD zK?z`kE#_vKYSTT1y3Lh~-FENzxMZ${t105%K+Kfz36-P*_eKRhG+sD~29*%D~r9u1$+2K~4EV}25nbcb%ZK~OL??26rCa}=nS zs;C%#)Ed9FkxW(mBYv*I-|p}5_xXvR=VD&tk+8V`Pgs28qVjnnn{+`|fMHBoV8lJv z6@$8DMN&kCr&^(lI&!g7aX)IG*d!-CSL^mE*yXSdEe+Mm7%sG9?RxCnY;M*;NPWXdfn0oz>ql(dX4m%dLoN>W^# z;htek^H*u}-1Ceof4xvIFV<4-l(E>qRoE)8)wa5~8f*Q#uveyneFeW%SgNeBZQ<7m z>y#}v#g|7!7eG!g5Lqic1!jDzh>@s?A+WY3HyP02dVrZ<`0OY^N%#r_8iz`9nDK>H zMTf!krbbh#sFgs6fe$PlF#5|&geCH7VYN(v;~uRXI)S{F7eP;YkvXSddjA?W_T78= zW@fmjv*l3dg%?{oIS(#+wJ~!${rh__;0f5?|6%`?_domq9^I1J_&>VKKDLSTj^oes z+@0^9$9KLr$M*R>wqrYX2sCj>lK?(m+CWl>fWbycSTrCkfff={UIwf|(1vytC}@I8 zol1d0r_vP)DS?SDT7iyLh-kcQbgDpV)=}1I)s*o^S&}_x18TL+vYxv~zLWg?UcSHY zhb^RW(WuM2YW`AG#wnAPdWFq~PlQn@Tr5TdWd&t{X@NE2Jz=5RUG1xL*ZGZ|2{=@5 z)W9tvB&CqD49ZrT8OFY3-$P{oqcH}T=)=i%Oa?m6c@_q^x5_q{yN`=cZGt>bWhnQg4kUBKN|thcjM(;NNDRdUUQ z^0X+~W9MoFTrc?M)wxF$1%|GXnc5daDhBqLFX(*9cpUV2bY3lN6i#-UT~I1cMy*4p zm|V3u&cwgz9UH+;I=bfZ%8Bn6^H{^ZsJap^#Sf@Ze?;h{3J7?U@vhO|(%WO%E| zdyp{gByu>=3I`$wXn58#H8&X3M|LNfh!@*NN4g42Zc7x~)`&F}_rx+{CR;1`oR?a2 ziMomPv5q8nz38U zzOSutk#u%P-5cK(3Szw~QWh-6B*Ef?))*Xn@u4vB+KuwA)0TNDlK+gxyVF|=C`R|(1gQ-TXH)4x6xMz>jrXo z?hvXL`&_9!*&ch9SjrCfg1}2j-&@otZLc@k`W-!UNtbl8YoS8`bF=7qe46X)6T!_A z{=D>W*qy4U71^R@m}9N$e7#fhk@VmQK8#@tjb*^|if-uJ9sLBs!447J@XlU;ZplaG zojuMPUOnHaWQ)ncr6d?3s!ZKFEJa=b^lCdkHvX> zXyasLt)QXi^uMf82d+0Jv?fsF)$iuI>?2q0R`Zpp$K>LQ;hoRa3Z0elk=RBwqX`CW>~N$WwIu%?ntT3AfVWy zbm=j*rgn`NvIsBcM6AcxEwAOCcVG3Zum+K6kMJ#Iy0C?yxEH{({dr4#w^O7}B0!8U z+26B7z2>~e)GRQAR?(M@alj>v2Bu8&NOVQK%s-uJeJ?V@)iTRoj0`o<75xc6qxdl5 z0==JByWeY{lvxUduM(W4Qh9`|$lj4$bgrQN?8ZZ-+caKg){kG3tD#XnWHKt17cZik zYp3X&#gnz?GL3ju=5o;Y-3ins!Ic)oVxTSHc|4pWY zwGfBwTtt+v%d|ktYrh?0(PtqV!$VzL)0;1hLg+5-a;n&86AR5sQl4zN3A2ruyYIkL zHYdn6{*>n7DLZaq-g7)5e#W|NAfhXoN}h<)mGk#Ayi9AH*Y3K|wKF&b+^E`IBQAY@ z!+YQ%nYws?B$h5CK$fnX%8&;VocunNLNiYDx{=e>nCm#B89%+WTN#8Q`IZS8ofK!a zO54<4q=YF+&Cdpk3ciIeZHQ?r=R*!z3Hk;q z(NYqmS9cy>2&`nW>r+*bv!+gf5uUQe%I zO0|?6)Xb$@WD?n!KljdIW_jdc2G=ziDdOs(F1t5L|ADZZKW6(t4#erMJEO zus$^T`{dylW2!BIdTpVU62T>IIBWH)lthsqnbcO-J*PzRZ6Tt`(WWo5cIN#jIqk6wm~AmR1Td$Bx_PoNBpsC%V&pyN!7g4L6?2dNI_oGE~;NwgIsrG6CCAk(81f*AYN`&E|InrphXsh;`l#5`Np@%7Nh>F*-GqvJ;; zjk9zX^6OT`F;^NUXW~S!^p`P#s;ycTy)B(Q!k60&m=C>>PR-1(T==lE`!mTSB^JXw6Qs0Rx~7_e z){9>#$c%8}jqSB83tpQWyO~9~cRn)P+J4jQPc?l_5Gk<-uh>CEi7stzE{U`)Z;IU* zd^UMuKHO9IGDd54jU-Gwk3@+`+3Lb`Vut2fDb!nC$6)^eC{Jc{v96A9#`XA@x|kT) zPYTa~LQV1~XXXv-!|Us7yZoQAAcoUKxkD6CU)q3+8qTuXC(rf*ZgEoF9OXXJa?Cb2S*&lx5{0PHZzw-lZFBChyoL+55E(oB8`fQk+ z#W}GUv8E=KP4Xa;C;w5X+6%AbbeZdV7HDm!pBNW8_qeNq-Ma~C`n)t zx1$_0n;bdT36l;)O2@Hvexj6~%X`|LvfRPuBLZqFVI(2`;K%OdTOu#5)(#d?(C?FH zH|Da6vb_9~snx=a@(G<&M2ksD#~um&WPI|C1*spSaLxz2ktlx^RKtg^K0BR8os855Vid)lj5m`YHu-tG5YLrT^Tsmgc7``Z%r>~YUm&JKW; z6sRLv%~p*|8S)a#@zb7c!o`m%s1#q@Fk}x!qNf%JU#szUHm$p5nq>3WOIUaLxamzd zWEKi7vE-AMJ^slw;pFaZ*(dbHLwzrPMt(tld955eML$3I<>^e$vg}lLxyHJP({LC? zOT(Ess2}HrZAy0&Y7y)smql~#tinQ?eC7#MgrM>XL*Mg~<`M92`)b2?J;~(5hAN2{TZQ$J2|4+$8C>Fz zs@U}#rwhyspXti48a`+-;V}_NhPYsf7R>@H8>E?L<1@S2;p`pb!Yj$X>v&|o!`eM|@ zGU3Wd%_3u+gB#r%qgXPNnd zQ;tPl9L~8^=DfyNH$DubpQ00@R><>SqF*{SfdgieW~LU*L+NN$D3yzDvkrE&B|Fr% zlsP}yw&7L)mHucIMTYxyau~!1B?Qy>(4V;C;_%cFluaamo7A!W>M1Xdry^cqa?H%t z8|qgI*>^6H64pS=Exrg!3a&A@r_AGPNBJ4vDgC3|Cnj0bU4svf zRVt`g_83T^iPe6o8ivVgJ>Q=fY`T7?Vu_Pdc)`>^2&>J)7QVMlo|Vo%@o1vh?1p`B z@A=)t^%m>~M~SE6%>C`p{tbxGH0^g^Oe=SE;Gbi5bD*kQ)AAApEp@0 zNx8P5QlQAX_k@q$5y&qRu%cyI%pokjk|5Z8bs|lsD1-I^nKvQb4MNSwQLnd8#+}~O+T)R`v42p1gP^a`=+RyO zx-+i`VOUZ-I^t`xu12i{`LU$>4|i;8_GZ)CSiKo>jsN+zM@>LNRJtWKcAlI~uF&!< zLC4$MH_6V4hxX32flr<%^}SV_%j!LGNdlhRkEddDp);o zntft2>-`)OtuoFiNmrLkW`|~Y^T-=6;ZK#MmG0~OGw))B(b=UUA#y>>-ZN4)k><{C z#>3*}lZb{At}}=ba((DHf9fd`MYz3aFyOV``!@gSx}Bw^jh?D%U0y0F*^?y&SM_f?ZsYC@@wtf54NKz%VA;eHpNLn?+SVubr(YISC zSNZ&vt|z%HTTn)&d#M!b{M3$V(XW&iJFR*zLc4+WUfXM8@I%6 zDt$BESEszmHjnR)X10_zLAvm;1(CAV3Nixol?jUfWu@GGq`_ar1bG)nMQuj_KMSm; zuPqI-fTIE8DsXrc1i22`CnMa};sXI3hy$MIp9}HtFS-Bky1VKiU}aqZ-HF86?)w4^ zoKOg)mZHqR>+OR01cU_sKYYdLP}keq&U6hP*blJ*AvrY&Dx(KEqyYkGcp%e>TPKxI z64J;h^^r)+k(`&8^UMh0>%llGXvoK0&q8FL?s1>*Al`t?&kE<$_4>!q3oFK`Tc&g_ zvn+3lY&tJ55092@#;g&xNJgJnxBXQ}L^#qdjkyL{30V0U|m}z~`EIomY&N zY%Hi=HOFv>{JU?g30VNDj7_bn60)-QjJJ1=ia?4%n2am<9WGkTJ}X0LR_z-@v}+D<2~(!6HCGo)?&e^(WV#oPHK}TuI^&ByJYA&tcXZk|d)ntbkbN4k z8@+{vh-|{_KC+zas|2g>DG8=J2(^M(YG@vR7qj*omFY9rs-)E<}YOw`Sc z^nGbQbqAMNZhOCN-GGQFj)-H7@ai4v=!arwh&;{^RZGL3l6?R%0e{iWIo~KP(oP_e zM#dY*WrqjAeW83DT<2G5M41O60yLd1NHweIZJH%3DwEp4dHy z+;ma7OtURV2=xq?y9Uxd6ZNdDHnWzvcCuErwxrg8=9#(tDw}*jhcaK#!!G#&nE}Ne zKgS5zO*i$FK&Hl@pHJXe4OEvv)Ipm8H?Ej+jIviGPS70o7TLVw%Elwe}A4~9y5RL;`3=a%4B3mSN>E!>%g+rtIiZF@GN67r?Ct; z;nLi~EU+G2XGOvdIOKw@pOpe@O{IQAUyE?vR^-$aZ40S7dB5hM=E|MchNE-$@bnf8aaWjk47+C#afQsku>}H zZ2L0Hc*)n~?>zTe_CR~0d&yog@_t?&X3y*H`!>_HUBg^U7YpL}LxK5d$boAH=^s}uL%@a=B6 ze|}euhQokEgxS0HNrtvN$f zomDM^WP`%4yi$)(HBUX2dhg|YM23b$C~4}IRE%0lSvx-7Y1~CrDJ%u6R`HUxG3~>a zEC-aI-Qn^Y*dXCe=$p6wLtiGoNc3wsm^tj=Y^RE6E;?vTL)Uy(on|DOUouqJR=C*ArzAYZPi`;Ypw+_91IJiL6*PF_1Ix=y%ugv`(CtcD=oQ zXPf<zn**Z&zts4 zxy-^W-mLg+x@_MZf*gF#eD0mxo;=mO>bJsgv+}RxN54Dw&ZmH|z_noIz191%LW9Cj zMJh$r#bU+pO0Jirma>*cmC=^HD5og*EZ?hetJtWttNc=BSv6U0T0K~!U(;24tG2aH zwXU&Vp}wX;x}l;`qOtUY=!c>v;imV^g3SdV1wOuO5omeWD%e`kCe&8gF4A87N&HiJ z$BmAvPPxwdF6FM~ZuRa@Jvu#oz5n!%^_ln0_M`j14mb_$4n7zn8S)uEGaNd?IFc~R zJ(@ixI94_;H~w)#dtz`BF}d*B;q&(?&uOaZ&>5DQ^jW^y(m92>_IacEsRg@*-7ilT z&n`wUaV_O9OD%s~(O;SRiu<~^>bJ(QmbwmGuinty82N_zw!7)G#jusOExg^Zqqj4& z>-?SUd-xCTAH_dae-7^*9TA#VIZ&>4(S90jez5@w!jXBL*OwMXrv1WY>st=BM!K6U@N?>B^-yxpy7B& z5EyHTwME*1z_@)Z7!}wNg*Hcm_(ecqdpy<==>SLpBAkdI2yBUi1HPdEO%7~^N28H| ztvTG%5{dhFZ*H~^l0%}=C_4v~0|<=7n8O{cfW7VE-U5vU8UcYVaBzepYCk|rJPLi# z0vc%nxchaAJHXvhfiWmsJmAO3VvEOHh4h}*Ft{vJR-oe2Y zs>9>s@`H8{ojBs)=12@22V}tl1^l3a4^*Rpbdf@t$^#lPvHps{>LH)4}uB< z-;W1c=KFMNAE`n1sE>-m+kRz9AI{EYz0RH0Uoq< zBnE~VR0Oya>BGVTOCPl?eY|X?59<*u{W~oHv2Y(?Ck^zB?C)sgkG#kqd67TvMgF(7 z@{ykMk)HA~J%C;JTT$K-C|&iV<~5G2G>)t^j#+^<{#E*A3{);2?TE5NyMn+PM|Gid zFo0>ck; z1qV7582pDl2L=jaeckGc7 z_Lvb+H2-S-wl+sOp@2sY7<-f`{E-9vkpui64)DJXU5^xiXTyG?Tz_2w-aN;i*Dg4u z1r-DYa2Tl||84;2Q9uBI6j@MF{3`0+g~#1KGku5J0|0Y0B^59Wl#|jfQ0#{ zj1LO?k3T*>VSpX^ryg(ypdf(_dOQ{gz_k2Z20e`Bn4SR8$lv}1_R)XLz+ivp4=Nx4JVgK269%}hf5`;-kLuu94g~pOf7c)INEZ6r z9}FTW{I?zqA_VYX|7-&|177Rdn~~C zg^t${@L~l>u{bJmabQM;azg@x5O~JgT3~^{JRVH@sK8oS;0*@MDGo@XU}ZTYadVgu zQivapfC6tgIhq6LP$Xf l0*Zv0iNHi8je+q`<=_aseGUdcOaRI+M0MqgyoLhR{{U{hcx3

    System Loader + sharedLoader (shared)-> commonLoader -> System Loader + catalinaLoader(server) -> commonLoader -> System Loader + (by default the commonLoader is used for the + sharedLoader and the serverLoader) + b) Load startup class (reflection) + org.apache.catalina.startup.Catalina + setParentClassloader -> sharedLoader + Thread.contextClassloader -> catalinaLoader + c) Bootstrap.daemon.init() complete + +Sequence 2. Process command line argument (start, stop) +Class: org.apache.catalina.startup.Bootstrap (assume command->start) +What it does: + a) Catalina.setAwait(true); + b) Catalina.load() + b1) initDirs() -> set properties like + catalina.home + catalina.base == catalina.home (most cases) + b2) initNaming + setProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY, + org.apache.naming.java.javaURLContextFactory ->default) + b3) createStartDigester() + Configures a digester for the main server.xml elements like + org.apache.catalina.core.StandardServer (can change of course :) + org.apache.catalina.deploy.NamingResources + Stores naming resources in the Jakarta EE JNDI tree + org.apache.catalina.LifecycleListener + implements events for start/stop of major components + org.apache.catalina.core.StandardService + The single entry for a set of connectors, + so that a container can listen to multiple connectors + ie, single entry + org.apache.catalina.Connector + Connectors to listen for incoming requests only + It also adds the following rulesets to the digester + NamingRuleSet + EngineRuleSet + HostRuleSet + ContextRuleSet + b4) Load the server.xml and parse it using the digester + Parsing the server.xml using the digester is an automatic + XML-object mapping tool, that will create the objects defined in server.xml + Startup of the actual container has not started yet. + b5) Assigns System.out and System.err to the SystemLogHandler class + b6) Calls initialize on all components, this makes each object register itself with the + JMX agent. + During the process call the Connectors also initialize the adapters. + The adapters are the components that do the request pre-processing. + Typical adapters are HTTP1.1 (default if no protocol is specified, + org.apache.coyote.http11.Http11NioProtocol) + AJP1.3 for mod_jk etc. + + c) Catalina.start() + c1) Starts the NamingContext and binds all JNDI references into it + c2) Starts the services under which are: + StandardService -> starts Engine (ContainerBase -> Realm,Cluster etc) + c3) StandardHost (started by the service) + Configures an ErrorReportValve to do proper HTML output for different HTTP + errors codes + Starts the Valves in the pipeline (at least the ErrorReportValve) + Configures the StandardHostValve, + this valves ties the Webapp Class loader to the thread context + it also finds the session for the request + and invokes the context pipeline + Starts the HostConfig component + This component deploys all the webapps + (webapps & conf/Catalina/localhost/*.xml) + HostConfig will create a Digester for your context, this digester + will then invoke ContextConfig.start() + The ContextConfig.start() will process the default web.xml (conf/web.xml) + and then process the applications web.xml (WEB-INF/web.xml) + + c4) During the lifetime of the container (StandardEngine) there is a background thread that + keeps checking if the context has changed. If a context changes (timestamp of war file, + context xml file, web.xml) then a reload is issued (stop/remove/deploy/start) + + d) Tomcat receives a request on an HTTP port + d1) The request is received by a separate thread which is waiting in the ThreadPoolExecutor + class. It is waiting for a request in a regular ServerSocket.accept() method. + When a request is received, this thread wakes up. + d2) The ThreadPoolExecutor assigns the a TaskThread to handle the request. + It also supplies a JMX object name to the catalina container (not used I believe) + d3) The processor to handle the request in this case is Coyote Http11Processor, + and the process method is invoked. + This same processor is also continuing to check the input stream of the socket + until the keep alive point is reached or the connection is disconnected. + d4) The HTTP request is parsed using an internal buffer class (Http11InputBuffer) + The buffer class parses the request line, the headers, etc and store the result in a + Coyote request (not an HTTP request) This request contains all the HTTP info, such + as servername, port, scheme, etc. + d5) The processor contains a reference to an Adapter, in this case it is the + CoyoteAdapter. Once the request has been parsed, the Http11Processor + invokes service() on the adapter. In the service method, the Request contains a + CoyoteRequest and CoyoteResponse (null for the first time) + The CoyoteRequest(Response) implements HttpRequest(Response) and HttpServletRequest(Response) + The adapter parses and associates everything with the request, cookies, the context through a + Mapper, etc + d6) When the parsing is finished, the CoyoteAdapter invokes its container (StandardEngine) + and invokes the invoke(request,response) method. + This initiates the HTTP request into the Catalina container starting at the engine level + d7) The StandardEngine.invoke() simply invokes the container pipeline.invoke() + d8) By default the engine only has one valve the StandardEngineValve, this valve simply + invokes the invoke() method on the Host pipeline (StandardHost.getPipeLine()) + d9) the StandardHost has two valves by default, the StandardHostValve and the ErrorReportValve + d10) The standard host valve associates the correct class loader with the current thread + It also retrieves the Manager and the session associated with the request (if there is one) + If there is a session access() is called to keep the session alive + d11) After that the StandardHostValve invokes the pipeline on the context associated + with the request. + d12) The first valve that gets invoked by the Context pipeline is the FormAuthenticator + valve. Then the StandardContextValve gets invoke. + The StandardContextValve invokes any context listeners associated with the context. + Next it invokes the pipeline on the Wrapper component (StandardWrapperValve) + d13) During the invocation of the StandardWrapperValve, the JSP wrapper (Jasper) gets invoked + This results in the actual compilation of the JSP. + And then invokes the actual servlet. + e) Invocation of the servlet class diff --git a/webapps/docs/balancer-howto.xml b/webapps/docs/balancer-howto.xml new file mode 100644 index 0000000..5b35381 --- /dev/null +++ b/webapps/docs/balancer-howto.xml @@ -0,0 +1,55 @@ + + + +]> + + + &project; + + + Yoav Shapira + Remy Maucherat + Andy Oliver + Load Balancer How-To + + + + +

    + +
    + +
    + +Please refer to the JK 1.2.x documentation. + +
    + +
    + +Please refer to the mod_proxy documentation for Apache HTTP Server 2.2. This supports either +HTTP or AJP load balancing. This new version of mod_proxy is also usable with +Apache HTTP Server 2.0, but mod_proxy will have to be compiled separately using the code +from Apache HTTP Server 2.2. + +
    + + + + diff --git a/webapps/docs/building.xml b/webapps/docs/building.xml new file mode 100644 index 0000000..86c7294 --- /dev/null +++ b/webapps/docs/building.xml @@ -0,0 +1,267 @@ + + + +]> + + + &project; + + + Building Tomcat + + + + +
    + +
    + +
    + +
    + +

    +Building Apache Tomcat requires a JDK (version ) or later to be installed. You +can download one from +https://adoptium.net/temurin/releases +or another JDK vendor. +

    + +

    +IMPORTANT: Set an environment variable JAVA_HOME to the pathname of the +directory into which you installed the JDK release. +

    + +
    + +
    + +

    +Download a binary distribution of Ant or later from +here. +

    + +

    +Unpack the binary distribution into a convenient location so that the +Ant release resides in its own directory (conventionally named +apache-ant-[version]). For the remainder of this guide, +the symbolic name ${ant.home} is used to refer to the full pathname of + the Ant installation directory. +

    + +

    +IMPORTANT: Create an ANT_HOME environment variable to point the directory ${ant.home}, +and modify the PATH environment variable to include directory +${ant.home}/bin in its list. This makes the ant command line script +available, which will be used to actually perform the build. +

    + +
    + +
    + +

    + Tomcat Git repository URL: + https://github.com/apache/tomcat +

    +

    + Tomcat source packages: + https://tomcat.apache.org/download-.cgi. +

    + +

    + Clone the source repository using Git, selecting a tag for released version or + main for the current development code, or download and unpack a + source package. For the remainder of this guide, the symbolic name + ${tomcat.source} is used to refer to the + location where the source has been placed. +

    + +
    + +
    + +

    + Building Tomcat involves downloading a number of libraries that it depends on. + It is strongly recommended to configure download area for those libraries. +

    + +

    + By default the build is configured to download the dependencies into the + ${user.home}/tomcat-build-libs directory. You can change this + (see below) but it must be an absolute path. +

    + +

    + The build is controlled by creating a + ${tomcat.source}/build.properties file. It can be used to + redefine any property that is present in build.properties.default + and build.xml files. The build.properties file + does not exist by default. You have to create it. +

    + +

    + The download area is defined by property base.path. For example: +

    + + + +

    + Different versions of Tomcat are allowed to share the same download area. +

    + +

    + Another example: +

    + +base.path=${user.dir}/../libraries-tomcat + +

    + Users who access the Internet through a proxy must use the properties + file to indicate to Ant the proxy configuration: +

    + + + +
    + +
    + +

    +Use the following commands to build Tomcat: +

    + +

    +cd ${tomcat.source}
    +ant +

    + +

    +Once the build has completed successfully, a usable Tomcat installation will have been +produced in the ${tomcat.source}/output/build directory, and can be started +and stopped with the usual scripts. +

    +
    + +
    + +

    +IMPORTANT: This is not a supported means of building Tomcat; this information is +provided without warranty :-). +The only supported means of building Tomcat is with the Ant build described above. +However, some developers like to work on Java code with a Java IDE, +and the following steps have been used by some developers. +

    + +

    +NOTE: This will not let you build everything under Eclipse; +the build process requires use of Ant for the many stages that aren't +simple Java compilations. +However, it will allow you to view and edit the Java code, +get warnings, reformat code, perform refactorings, run Tomcat +under the IDE, and so on. +

    + +

    +WARNING: Do not forget to create and configure + ${tomcat.source}/build.properties file as described above + before running any Ant targets. +

    + +

    +Sample Eclipse project files and launch targets are provided in the +res/ide-support/eclipse directory of the source tree. +The instructions below will automatically copy these into the required locations. +

    +

    +An Ant target is provided as a convenience to download all binary dependencies, and to create +the Eclipse project and classpath files in the root of the source tree. +

    + +

    +cd ${tomcat.source}
    +ant ide-eclipse +

    + +

    +Start Eclipse and create a new Workspace. +

    + +

    +Use File->Import and choose Existing Projects into Workspace. +From there choose the root directory of the Tomcat source tree (${tomcat.source}) +and import the Tomcat project located there. +

    + +

    +start-tomcat and stop-tomcat launch configurations are provided in +res/ide-support/eclipse and will be available in the Run->Run Configurations +dialog. Use these to start and stop Tomcat from Eclipse. +If you want to configure these yourself (or are using a different IDE) +then use org.apache.catalina.startup.Bootstrap as the main class, +start/stop etc. as program arguments, and specify -Dcatalina.home=... +(with the name of your build directory) as VM arguments. +

    + +

    +Tweaking a few formatting preferences will make it much easier to keep consistent with Tomcat +coding conventions (and have your contributions accepted): +

    + + + + + + + + +
    Java -> Code Style -> Formatter -> Edit...Tab policy: Spaces only
    Tab and Indentation size: 4
    General -> Editors -> Text EditorsDisplayed tab width: 2
    Insert spaces for tabs
    Show whitespace characters (optional)
    XML -> XML Files -> EditorIndent using spaces
    Indentation size: 2
    Ant -> Editor -> FormatterTab size: 2
    Use tab character instead of spaces: unchecked
    + +

    +The recommended configuration of Compiler Warnings is documented in +res/ide-support/eclipse/java-compiler-errors-warnings.txt file. +

    + +
    + +
    +

    +The same general approach should work for most IDEs; it has been reported +to work in IntelliJ IDEA, for example. +

    + +
    + + + diff --git a/webapps/docs/cdi.xml b/webapps/docs/cdi.xml new file mode 100644 index 0000000..6a6f1a2 --- /dev/null +++ b/webapps/docs/cdi.xml @@ -0,0 +1,187 @@ + + + +]> + + + &project; + + + CDI 2, JAX-RS and dependent libraries support + + + + +
    + +
    + +
    + +

    + CDI and JAX-RS are dependencies for many other APIs and libraries. This + guide explains how to add support for them in Tomcat using two optional + modules that are provided in the Tomcat sources. +

    + +
    + +
    + +

    + CDI 2 support is provided by the modules/owb optional module. + It packages the Apache OpenWebBeans project and allows adding CDI 2 support + to the Tomcat container. The build process of the module uses Apache Maven, + and is not available as a binary bundle as it is built using a number of + publicly available JARs. +

    + +

    + The process to build CDI support is the following. + + The resulting JAR at + target/tomcat-owb-x.y.z.jar (where x.y.z depends on the + Apache OpenWebBeans version used during the build) + should be processed by the Tomcat migration tool for Jakarta EE, and + then be placed into the lib folder of the Tomcat + installation.
    + CDI support can then be enabled for all webapps in the container by adding + the following listener in server.xml nested inside the + Server element: + ]]> + The listener will produce a non fatal error if the CDI container loading + fails.
    + CDI support can also be enabled at the individual webapp level by adding + the following listener to the webapp context.xml file nested + inside the Server element: + ]]> +

    + +
    + +
    + +

    + JAX-RS support is provided by the modules/cxf optional module. + It packages the Apache CXF project and allows adding JAX-RS support + to individual webapps. The build process of the module uses Apache Maven, + and is not available as a binary bundle as it is built using a number of + publicly available JARs. The support depends on CDI 2 support, which should + have previously been installed at either the container or webapp level. +

    + +

    + The process to build JAX-RS support is the following. + + The resulting JAR at + target/tomcat-cxf-x.y.z.jar (where x.y.z depends on the + Apache CXF version used during the build) + should then be placed into the /WEB-INF/lib folder of the + desired web application. +

    + +

    + If the CDI 2 support is available at the container + level, the JAR can also be placed in the Tomcat lib folder, + but in that case the CXF Servlet declaration must be individually added + in each webapp as needed (it is normally loaded by the web fragment that is + present in the JAR). The CXF Servlet class that should be used is + org.apache.cxf.cdi.CXFCdiServlet and should be mapped to the + desired root path where JAX-RS resources will be available. +

    + +

    + The webapp as a whole should be processed by the Tomcat migration tool for + Jakarta EE. +

    + +
    + +
    + +

    + ASF artifacts are available that implement Eclipse Microprofile + specifications using CDI 2 extensions. Once the CDI 2 and JAX-RS support + is installed, they will be usable by individual webapps. +

    + +

    + The following implementations are available (reference: + org.apache.tomee.microprofile.TomEEMicroProfileListener) as + Maven artifacts which must be added to the webapp /WEB-INF/lib + folders: +

      +
    • Configuration: + Maven artifact: + org.apache.geronimo.config:geronimo-config + CDI extension class: + org.apache.geronimo.config.cdi.ConfigExtension +
    • +
    • Fault Tolerance: + Maven artifact: + org.apache.geronimo.safeguard:safeguard-parent + CDI extension class: + org.apache.safeguard.impl.cdi.SafeguardExtension +
    • +
    • Health: + Maven artifact: + org.apache.geronimo:geronimo-health + CDI extension class: + org.apache.geronimo.microprofile.impl.health.cdi.GeronimoHealthExtension +
    • +
    • Metrics: + Maven artifact: + org.apache.geronimo:geronimo-metrics + CDI extension class: + org.apache.geronimo.microprofile.metrics.cdi.MetricsExtension +
    • +
    • OpenTracing: + Maven artifact: + org.apache.geronimo:geronimo-opentracing + CDI extension class: + org.apache.geronimo.microprofile.opentracing.microprofile.cdi.OpenTracingExtension +
    • +
    • OpenAPI: + Maven artifact: + org.apache.geronimo:geronimo-openapi + CDI extension class: + org.apache.geronimo.microprofile.openapi.cdi.GeronimoOpenAPIExtension +
    • +
    • Rest client: + Maven artifact: + org.apache.cxf:cxf-rt-rs-mp-client + CDI extension class: + org.apache.cxf.microprofile.client.cdi.RestClientExtension +
    • +
    • JSON Web Tokens: + Note: Fore reference only, unusable outside Apache TomEE; + Maven artifact: + org.apache.tomee:mp-jwt + CDI extension class: + org.apache.tomee.microprofile.jwt.cdi.MPJWTCDIExtension +
    • +
    +

    + +
    + + +
    diff --git a/webapps/docs/cgi-howto.xml b/webapps/docs/cgi-howto.xml new file mode 100644 index 0000000..b3b7693 --- /dev/null +++ b/webapps/docs/cgi-howto.xml @@ -0,0 +1,168 @@ + + + +]> + + + &project; + + + CGI How To + + + + +
    + +
    + +
    + +

    The CGI (Common Gateway Interface) defines a way for a web server to +interact with external content-generating programs, which are often +referred to as CGI programs or CGI scripts. +

    + +

    Within Tomcat, CGI support can be added when you are using Tomcat as your +HTTP server and require CGI support. Typically this is done +during development when you don't want to run a web server like +Apache httpd. +Tomcat's CGI support is largely compatible with Apache httpd's, +but there are some limitations (e.g., only one cgi-bin directory). +

    + +

    CGI support is implemented using the servlet class +org.apache.catalina.servlets.CGIServlet. Traditionally, +this servlet is mapped to the URL pattern "/cgi-bin/*".

    + +

    By default CGI support is disabled in Tomcat.

    +
    + +
    + +

    CAUTION - CGI scripts are used to execute programs +external to the Tomcat JVM. If you are using the Java SecurityManager this +will bypass your security policy configuration in catalina.policy.

    + +

    To enable CGI support:

    + +
      +
    1. There are commented-out sample servlet and servlet-mapping elements for +CGI servlet in the default $CATALINA_BASE/conf/web.xml file. +To enable CGI support in your web application, copy that servlet and +servlet-mapping declarations into WEB-INF/web.xml file of your +web application.

      + +

      Uncommenting the servlet and servlet-mapping in +$CATALINA_BASE/conf/web.xml file enables CGI for all installed +web applications at once.

      +
    2. + +
    3. Set privileged="true" on the Context element for your +web application.

      + +

      Only Contexts which are marked as privileged are allowed to use the +CGI servlet. Note that modifying the global $CATALINA_BASE/conf/context.xml +file affects all web applications. See +Context documentation for details.

      +
    4. +
    + +
    + +
    + +

    There are several servlet init parameters which can be used to +configure the behaviour of the CGI servlet.

    +
      +
    • cgiMethods - Comma separated list of HTTP methods. Requests +using one of these methods will be passed to the CGI script for the script to +generate the response. The default value is GET,POST. Use +* for the script to handle all requests regardless of method. +Unless over-ridden by the configuration of this parameter, requests using HEAD, +OPTIONS or TRACE will have handled by the superclass.
    • +
    • cgiPathPrefix - The CGI search path will start at +the web application root directory + File.separator + this prefix. +By default there is no value, which results in the web application root +directory being used as the search path. The recommended value is +WEB-INF/cgi
    • +
    • cmdLineArgumentsDecoded - If command line arguments +are enabled (via enableCmdLineArguments) and Tomcat is running +on Windows then each individual decoded command line argument must match this +pattern else the request will be rejected. This is to protect against known +issues passing command line arguments from Java to Windows. These issues can +lead to remote code execution. For more information on these issues see +Markus +Wulftange's blog and this archived +blog +by Daniel Colascione.
    • +
    • cmdLineArgumentsEncoded - If command line arguments +are enabled (via enableCmdLineArguments) individual encoded +command line argument must match this pattern else the request will be rejected. +The default matches the allowed values defined by RFC3875 and is +[\w\Q%;/?:@&,$-.!~*'()\E]+
    • +
    • enableCmdLineArguments - Are command line arguments +generated from the query string as per section 4.4 of 3875 RFC? The default is +false.
    • +
    • environment-variable- - An environment to be set for the +execution environment of the CGI script. The name of variable is taken from the +parameter name. To configure an environment variable named FOO, configure a +parameter named environment-variable-FOO. The parameter value is used as the +environment variable value. The default is no environment variables.
    • +
    • executable - The name of the executable to be used to +run the script. You may explicitly set this parameter to be an empty string +if your script is itself executable (e.g. an exe file). Default is +perl.
    • +
    • executable-arg-1, executable-arg-2, +and so on - additional arguments for the executable. These precede the +CGI script name. By default there are no additional arguments.
    • +
    • envHttpHeaders - A regular expression used to select the +HTTP headers passed to the CGI process as environment variables. Note that +headers are converted to upper case before matching and that the entire header +name must match the pattern. Default is +ACCEPT[-0-9A-Z]*|CACHE-CONTROL|COOKIE|HOST|IF-[-0-9A-Z]*|REFERER|USER-AGENT +
    • +
    • parameterEncoding - Name of the parameter encoding +to be used with the CGI servlet. Default is +System.getProperty("file.encoding","UTF-8"). That is the system +default encoding, or UTF-8 if that system property is not available.
    • +
    • passShellEnvironment - Should the shell environment +variables from Tomcat process (if any) be passed to the CGI script? Default is +false.
    • +
    • stderrTimeout - The time (in milliseconds) to wait for +the reading of stderr to complete before terminating the CGI process. Default +is 2000.
    • +
    + +

    The CGI script executed depends on the configuration of the CGI Servlet and +how the request is mapped to the CGI Servlet. The CGI search path starts at the +web application root directory + File.separator + cgiPathPrefix. The +pathInfo is then searched unless it is null - in +which case the servletPath is searched.

    + +

    The search starts with the first path segment and expands one path segment +at a time until no path segments are left (resulting in a 404) or a script is +found. Any remaining path segments are passed to the script in the +PATH_INFO environment variable.

    + +
    + + + +
    diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml new file mode 100644 index 0000000..1299da4 --- /dev/null +++ b/webapps/docs/changelog.xml @@ -0,0 +1,4551 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]> + + + + &project; + + + Changelog + + + + + +
    + + + + Deprecate and remove sessionCounter (replaced by the + addition of the active session count and the expired session count, + as a reasonable approximation) and duplicates (which + does not represent a possible event in current implementations) + statistics from the session manager. (remm) + + + 68890 Align output encoding of JSPs in the Manager webapp + with the XML declarations in those same files. (schultz) + + + Update Basic authentication to implement the requirements of RFC 7617 + including the changing of the trimCredentials setting which + is now defaults to false. Note that the + trimCredentials setting will be removed in Tomcat 11. + (markt) + + + + + + + Fix bnd jar descriptor to include the OpenSSL FFM support. (remm) + + + Add OpenSSL FFM classes to tomcat-embed-core.jar. (remm) + + + +
    +
    + + + + Release re-built using correct JDK version. + + + +
    +
    + + + + Change the thread-safety mechanism for protecting StandardServer.services + from a simple synchronized lock to a ReentrantReadWriteLock to allow + multiple readers to operate simultaneously. Based upon a suggestion by + Markus Wolfe. (schultz) + + + Improve Service connectors, Container children and Service executors + access sync using a ReentrantReadWriteLock. (remm) + + + Improve handling of integer overflow if an attempt is made to upload a + file via the Servlet API and the file is larger than + Integer.MAX_VALUE. (markt) + + + 68862: Handle possible response commit when processing read + errors. (remm) + + + + + + + Add OpenSSL integration using the FFM API rather than Tomcat Native. + OpenSSL support may be enabled by adding the + org.apache.catalina.core.OpenSSLLifecycleListener + listener on the Server element when using Java 22 + or later. (remm) + + + + + + + Update the internal fork of Apache Commons BCEL to 6.8.2. (markt) + + + Update the internal fork of Apache Commons Codec to 1.16.1. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations by tak7iji. (remm) + + + Improvements to Chinese translations by leeyazhou. (remm) + + + +
    +
    + + + + Minor performance improvement for building filter chains. Based on + ideas from 702 by Luke Miao. (remm) + + + Align error handling for Writer and + OutputStream. Ensure use of either once the response has + been recycled triggers a NullPointerException provided that + discardFacades is configured with the default value of + true. (markt) + + + 68692: The standard thread pool implementations that are + configured using the Executor element now implement + ExecutorService for better support NIO2. (remm) + + + 68495: When restoring a saved POST request after a successful + FORM authentication, ensure that neither the URI, the query string nor + the protocol are corrupted when restoring the request body. (markt) + + + After forwarding a request, attempt to unwrap the response in order to + suspend it, instead of simply closing it if it was wrapped. Add a new + suspendWrappedResponseAfterForward boolean attribute on + Context to control the bahavior, defaulting to + false. (remm) + + + 68721: Workaround a possible cause of duplicate class + definitions when using ClassFileTransformers and the + transformation of a class also triggers the loading of the same class. + (markt) + + + The rewrite valve should not do a rewrite if the output is identical + to the input. (remm) + + + Add a new valveSkip (or VS) rule flag to the + rewrite valve to allow skipping over the next valve in the Catalina + pipeline. (remm) + + + Add highConcurrencyStatus attribute to the + SemaphoreValve to optionally allow the valve to return an + error status code to the client when a permit cannot be acquired from + the semaphore. (remm) + + + Add checking of the "age" of the running Tomcat instance since its + build-date to the SecurityListener, and log a warning if the server + is old. (schultz) + + + When using the AsyncContext, throw an + IllegalStateException, rather than allowing an + NullPointerException, if an attempt is made to use the + AsyncContext after it has been recycled. (markt) + + + + + + + Improve the HTTP/2 stream prioritisation process. If a stream uses all + of the connection windows and still has content to write, it will now be + added to the backlog immediately rather than waiting until the write + attempt for the remaining content. (markt) + + + Add threadsMaxIdleTime attribute to the endpoint, + to allow configuring the amount of time before an internal executor + will scale back to the configured minSpareThreads size. + (remm) + + + Correct a regression in the support for user provided + SSLContext instances that broke the + org.apache.catalina.security.TLSCertificateReloadListener. + (markt) + + + + + + + Add support for specifying Java 22 (with the value 22) as + the compiler source and/or compiler target for JSP compilation. If used + with an Eclipse JDT compiler version that does not support these values, + a warning will be logged and the default will used. + (markt) + + + Handle the case where the JSP engine forwards a request/response to a + Servlet that uses an OutputStream rather than a + Writer. This was triggering an + IllegalStateException on code paths where there was a + subsequent attempt to obtain a Writer. (markt) + + + Correctly handle the case where a tag library is packaged in a JAR file + and the web application is deployed as a WAR file rather than an + unpacked directory. (markt) + + + Prevent the web application's ClassLoader from being pinned by the JSP + compiler if an application uses a custom XMLInputFactory. Based upon a + suggestion from Simon Niederberger. (schultz) + + + + + + + Avoid updating request count stats on async. (remm) + + + + + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations by tak7iji. (markt) + + + 57130: Allow digest.(sh|bat) to accept password from a file + or stdin. (csutherl/schultz) + + + Update Checkstyle to 10.14.1. (markt) + + + +
    +
    + + + + Correct JPMS and OSGi meta-data for tomcat-embed-core.jar + by removing reference to org.apache.catalina.ssi package + that is no longer included in the JAR. Based on pull request + 684 by Jendrik Johannes. (markt) + + + Fix ServiceBindingPropertySource so that trailing \r\n + sequences are correctly removed from files containing property values + when configured to do so. Bug identified by Coverity Scan. (markt) + + + Add improvements to the CSRF prevention filter including the ability + to skip adding nonces for resource name and subtree URL patterns. (schultz) + + + Review usage of debug logging and downgrade trace or data dumping + operations from debug level to trace. (remm) + + + 68089: Further improve the performance of request attribute + access for ApplicationHttpRequest and + ApplicationRequest. (markt) + + + 68559: Allow asynchronous error handling to write to the + response after an error during asynchronous processing. (markt) + + + + + + + Setting a null value for a cookie attribute should remove + the attribute. (markt) + + + Make asynchronous error handling more robust. Ensure that once a + connection is marked to be closed, further asynchronous processing + cannot change that. (markt) + + + Make asynchronous error handling more robust. Ensure that once the call + to AsyncListener.onError() has returned to the container, + only container threads can access the AsyncContext. This + protects against various race conditions that woudl otherwise occur if + application threads continued to access the AsyncContext. + + + Review usage of debug logging and downgrade trace or data dumping + operations from debug level to trace. In particular, most of the + HTTP/2 debug logging has been changed to trace level. (remm) + + + Add support for user provided SSLContext instances + configured on SSLHostConfigCertificate instances. Based on + pull request 673 provided by Hakan Altındağ. (markt) + + + Partial fix for 68558: Cache the result of converting to + String for request URI, HTTP header names and the request + Content-Type value to improve performance by reducing + repeated byte[] to String conversions. (markt) + + + Improve error reporting to HTTP/2 clients for header processing errors + by reporting problems at the end of the frame where the error was + detected rather than at the end of the headers. (markt) + + + Remove the remaining reference to a stream once the stream has been + recycled. This makes the stream eligible for garbage collection earlier + and thereby improves scalability. (markt) + + + + + + + 68546: Generate optimal size and types for JSP imports maps, + as suggested by John Engebretson. (remm) + + + Review usage of debug logging and downgrade trace or data dumping + operations from debug level to trace. (remm) + + + + + + + Correct a regression in the fix for 66508 that could cause an + UpgradeProcessor leak in some circumstances. (markt) + + + Review usage of debug logging and downgrade trace or data dumping + operations from debug level to trace. (remm) + + + Ensure that WebSocket connection closure completes if the connection is + closed when the server side has used the proprietary suspend/resume + feature to suspend the connection. (markt) + + + + + + + Add support for responses in JSON format from the examples application + RequestHeaderExample. (schultz) + + + + + + + Correct the remaining OSGi contract references in the manifest files to + refer to the Jakarta EE contract names rather than the Java EE contract + names. Based on pull request 685 provided by Paul A. Nicolucci. + (markt) + + + Update Checkstyle to 10.13.0. (markt) + + + Update JSign to 6.0. (markt) + + + Update the packaged version of the Tomcat Migration Tool for Jakarta EE + to 1.0.7. (markt) + + + Update Tomcat Native to 2.0.7. (markt) + + + Add strings for debug level messages. (remm) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations by tak7iji. (markt) + + + +
    +
    + + + + 68378: Align extension to MIME type mappings in the global + web.xml with those in httpd by adding + application/vnd.geogebra.slides for ggs, + text/javascript for mjs and + audio/ogg for opus. (markt) + + + + + + + Refactor the VirtualThreadExecutor so that it can be used + by the NIO2 connector which was using platform threads even when + configured to use virtual threads. (markt) + + + Correct a regression in the fix for 67675 that broke TLS key + file parsing for PKCS#8 format keys that do not specify an explicit + pseudo-random function and rely on the default. This typically affects + keys generated by OpenSSL 1.0.2. (markt) + + + Allow multiple operations with the same name on introspected mbeans, + fixing a regression caused by the introduction of a second + addSslHostConfig method. (remm) + + + Relax the check that the HTTP Host header is consistent with the host + used in the request line, if any, to make the check case insensitive + since host names are case insensitive. (markt) + + + 68348: Add support for the partitioned attribute for cookies + including session cookies. (markt) + + + + + + + 68035: Additional fix to the Manager application to enable + the deployment of a web application located in a Host's + appBase where the web application is specified by a bare + (no path) WAR or directory name as shown in the documentation. (markt) + + + + + + + Update Checkstyle to 10.12.7. (markt) + + + Update SpotBugs to 4.8.3. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations by tak7iji. (markt) + + + +
    +
    + + + + Background processes should not be run concurrently with lifecycle + operations of a container. (remm) + + + Correct unintended escaping of XML in some WebDAV responses. The XML + list of support locks when provided in response to a PROPFIND request + was incorrectly XML escaped. (markt) + + + 68227: Ensure that AsyncListener.onComplete() is + called if AsyncListener.onError() calls + AsyncContext.dispatch(). (markt) + + + 68228: Use a 408 status code if a read timeout occurs during + HTTP request processing. Includes a test case based on code provided by + adwsingh. (markt) + + + + + + + 68119: Refactor the CompositeELResolver to + improve performance during type conversion operations. (markt) + + + + + + + Examples. Improve the error handling so snakes associated with a user + that drops from the network are removed from the game. (markt) + + + + + + + 68124: Migrate sample.war from javax to jakarta. (lihan) + + + Update UnboundID to 6.0.11. (markt) + + + Update Checkstyle to 10.12.5. (markt) + + + Update SpotBugs to 4.8.2. (markt) + + + Update Derby to 10.17.1. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations by tak7iji. (markt) + + + Improvements to Brazilian Portuguese translations by John William + Vicente. (markt) + + + Improvements to Russian translations by usmazat and remm. (markt) + + + +
    +
    + + + + 67667: TLSCertificateReloadListener prints + unreadable rendering of X509Certificate#getNotAfter(). + (michaelo) + + + The status servlet included in the manager webapp can now output + statistics as JSON, using the JSON=true URL parameter. + (remm) + + + Optionally allow ServiceBindingPropertySource to trim a trailing newline + from a file containing a property-value. (schultz) + + + 67793: Ensure the original session timeout is restored after + FORM authentication if the user refreshes a page during the FORM + authentication process. Based on a suggestion by Mircea Butmalai. + (markt) + + + 67926: PEMFile prints unidentifiable string + representation of ASN.1 OIDs. (michaelo) + + + 66875: Ensure that setting the request attribute + jakarta.servlet.error.exception is not sufficient to + trigger error handling for the current request and response. (markt) + + + 68054: Avoid some file canonicalization calls introduced + by the fix for 65433. (remm) + + + 68089: Improve performance of request attribute access for + ApplicationHttpRequest and ApplicationRequest. + (markt) + + + Use a 400 status code to report an error due to a bad request (e.g. an + invalid trailer header) rather than a 500 status code. (markt) + + + Ensure that an IOException during the reading of the + request triggers always error handling, regardless of whether the + application swallows the exception. (markt) + + + + + + + 66670: Add + SSLHostConfig#certificateKeyPasswordFile and + SSLHostConfig#certificateKeystorePasswordFile. (michaelo) + + + When calling + SSLHostConfigCertificate.setCertificateKeystore(ks), + automatically call + setCertificateKeystoreType(ks.getType()). (markt) + + + 67628: Clarify how the ciphers attribute of the + SSLHostConfig is used. (markt) + + + 67666: Ensure TLS connectors using PEM files either work with + the TLSCertificateReloadListener or, in the rare case that + they do not, log a warning on Connector start. (markt) + + + 67675: Support a wider range of KDF and ciphers for PEM files + than the combinations supported by the JVM by default. Specifically, + support the OpenSSL default of HmacSHA256 and DES-EDE3-CBC. (markt) + + + 67927: Reloading TLS configuration can cause the Connector to + refuse new connections or the JVM to crash. (markt) + + + 67938: Correct handling of large TLS client hello messages + that were causing the TLS handshake to fail. (markt) + + + 68026: Convert selected MessageByte values to + String when first accessed to speed up subsequent accesses and reduce + garbage collection. (markt) + + + + + + + 68068: Performance improvement for EL. Based on a suggestion + by John Engebretson. (markt) + + + + + + + Correct missing metadata in the MANIFEST of the for WebSocket client API + JAR file. (markt) + + + + + + + 68035: Correct a regression in the fix for 56248 + that prevented deployment via the Manager of a WAR or directory that was + already present in the appBase or a context file that was + already present in the xmlBase. (markt) + + + + + + + 67538: Make use of Ant's <javaversion /> task + to enfore the mininum Java build version. (michaelo) + + + Update Checkstyle to 10.12.4. (markt) + + + Update JaCoCo to 0.8.11. (markt) + + + Update SpotBugs to 4.8.0. (markt) + + + Update BND to 7.0.0. (markt) + + + The minimum Java version required to build Tomcat has been raised to + Java 17. (markt) + + + Update the OWB module to Apache OpenWebBeans 4.0.0. (remm) + + + +
    +
    + + + + 67670: Fix regression with HTTP compression after code + refactoring. (remm) + + + + + + + 67664: Correct a regression in the clean-up of unnecessary + use of fully qualified class names in 10.1.14 that broke the jdbc-pool. + (markt) + + + +
    +
    + + + + 65770: Provide a lifecycle listener that will automatically + reload TLS configurations a set time before the certificate is due to + expire. This is intended to be used with third-party tools that + regularly renew TLS certificates. (markt) + + + Fix handling of an error reading a context descriptor on deployment. + (remm) + + + Fix rewrite rule qsd (query string discard) being ignored if qsa was + also use, while it should instead take precedence. (remm) + + + 67472: Send fewer CORS-related headers when CORS is not + actually being engaged. (schultz) + + + Improve handling of failures within recycle() methods. + (markt) + + + + + + + 67198: Ensure that the AJP connector attribute + tomcatAuthorization takes precedence over the + tomcatAuthentication attribute when processing an + auth_type attribute received from a proxy server. (markt) + + + 67235: Fix a NullPointerException when an + AsyncListener handles an error with a dispatch rather than + a complete. (markt) + + + When an error occurs during asynchronous processing, ensure that the + error handling process is only triggered once per asynchronous cycle. + (markt) + + + Fix logic issue trying to match no argument method in IntropectionUtil. + (remm) + + + Improve thread safety around readNotify and writeNotify in the NIO2 + endpoint. (remm) + + + Avoid rare thread safety issue accessing message digest map. (remm) + + + Improve statistics collection for upgraded connections under load. + (remm) + + + Align validation of HTTP trailer fields with standard fields. (markt) + + + Improvements to HTTP/2 overhead protection. (markt) + + + + + + + 67080: Improve performance of EL expressions in JSPs that use + implicit objects. Based on suggestions by John Engebretson, Anurag Dubey + and Christopher Schultz. (markt) + + + + + + + Update the internal fork of Apache Commons FileUpload to 7a8c324 + (2023-09-16, 1.x-SNAPSHOT). Due to significant refactoring in the 2.x + branch requiring additional Commons IO dependencies, Tomcat has switched + to tracking the 1.x branch. (markt) + + + Add the Bundle-License header to the JAR manifest for all + Tomcat JARs. (markt) + + + Update UnboundID to 6.0.10. (markt) + + + Update Checkstyle to 10.12.3. (markt) + + + Update Tomcat Native to 2.0.6. (markt) + + + Update Commons Pool to 2.12.0. (markt) + + + 67611: Correct the download link in BUILDING.txt. (lihan) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations by tak7iji. (markt) + + + Improvements to Russian translations by usmazat. (markt) + + + +
    +
    + + + + If an application or library sets both a non-500 error code and the + jakarta.servlet.error.exception request attribute, use the + provided error code during error page processing rather than assuming an + error code of 500. (markt) + + + Update code comments and Tomcat output to use MiB for 1024 * 1024 bytes + and KiB for 1024 bytes rather than MB and kB. (martk) + + + Avoid protocol relative redirects in FORM authentication. (markt) + + + + + + + Documentation. Update documentation to use MiB for 1024 * 1024 bytes and + KiB for 1024 bytes rather than MB and kB. (martk) + + + + + + + Improvements to Chinese translations. (lihan) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations by tak7iji. (markt) + + + +
    +
    + + + + 66680: When serializing a session during the session + presistence process, do not log a warning that null Principals are not + serializable. Pull request 638 provided by tsryo. (markt) + + + Catch NamingException in JNDIRealm#getPrincipal. + It is used in Java up to 17 to signal closed connections. (fschumacher) + + + 66822: Use the same naming format in log messages for + Connector instances as the associated ProtocolHandler instance. (markt) + + + The parts count should also lower the actual + maxParameterCount used for parsing parameters if parts are + parsed first. (remm) + + + + + + + Correct a regression introduced in 10.1.11 and use the correct + constant when constructing the default value for the + certificateKeystoreFile attribute of an + SSLHostConfigCertificate instance. (markt) + + + Refactor HTTP/2 implementation to reduce pinning when using virtual + threads. (markt) + + + Pass through ciphers referring to an OpenSSL profile, such as + PROFILE=SYSTEM instead of producing an error trying to + parse it. (remm) + + + 66841: Ensure that AsyncListener.onError() is + called after an error during asynchronous processing with HTTP/2. + (markt) + + + 66842: When using asynchronous I/O (the default), include + DATA frames when calculating the HTTP/2 overhead count to ensure that + connections are not prematurely terminated. (markt) + + + Correct a race condition that could cause spurious RST messages to be + sent after the response had been written to an HTTP/2 stream. (markt) + + + + + + + 66681: Fix a NullPointerException when flushing + batched messages with compression enabled using + permessage-deflate. (markt) + + + + + + + Fix the releaseIdleCounter does not increment when testAllIdle + releases them. Pull request 241 provided by Arun Chaitanya Miriappalli + (lihan) + + + Fix the ConnectionState state will be inconsistent with actual + state on the connection when an exception occurs while writing. Pull request + 643 provided by Wenjun Xiao. (lihan) + + + + + + + Update NSIS to 3.0.9. (markt) + + + Update Checkstyle to 10.12.2. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations. Contributed by tak7iji and + Shirayuking. (markt) + + + 66829: Fix quoting so users can use the _RUNJAVA + environment variable as intended on Windows when the path to the Java + executable contains spaces. (markt) + + + 66834: Correct the OSGi contract references in the manifest + files to refer to the Jakarta EE contract names rather than the Java EE + contract names. (markt) + + + Update Tomcat Native to 2.0.5. (markt) + + + +
    +
    + + + + 59232: Add + org.apache.catalina.core.ContextNamingInfoListener, + a listener which creates context naming information environment entries. + (michaelo) + + + 66665: Add + org.apache.catalina.core.PropertiesRoleMappingListener, + a listener which populates the context's role mapping from a properties + file. (michaelo) + + + Fix an edge case where intra-web application symlinks would be followed + if the web applications were deliberately crafted to allow it even when + allowLinking was set to false. (markt) + + + Add utlity config file resource lookup on Context to allow + looking up resources from the webapp (prefixed with + webapp:) and make the resource lookup API more visible. + (remm) + + + Fix potential database connection leaks in + DataSourceUserDatabase identified by Coverity Scan. (markt) + + + Make parsing of ExtendedAccessLogValve patterns more + robust. (markt) + + + + + + + 66627: Restore the documented behaviour of + MessageBytes.getType() that it returns the type of the + original content rather than reflecting the most recent conversion. + (markt) + + + 66635: Correct certificate logging on start-up so it + differentiates between keystore based keys/certificates and PEM file + based keys/certificates and logs the relevant information for each. + (markt) + + + Refactor blocking reads and writes for the NIO connector to remove + code paths that could allow a notification from the Poller to be missed + resuting in a timeout rather than the expected read or write. (markt) + + + Refactor waiting for an HTTP/2 stream or connection window update to + handle spurious wake-ups during the wait. (markt) + + + + + + + Improve handling of error conditions for the WebSocket server, + particularly during Tomcat shutdown. (markt) + + + Correct a regression in the fix for 66574 that meant the + WebSocket session could return false for onOpen() before + the onClose() event had been completed. (markt) + + + + + + + Documentation. Expand the security guidance to cover the embedded use + case and add notes on the uses made of the java.io.tmpdir + system property. (markt) + + + 66662: Documentation. Fix a typo in the name of the + algorithms attribute in the configuration section for + the Digest authentication valve. Pull request 629 provided by + gohilmca. (markt) + + + + + + + Include the Windows specific binary distributions in the files uploaded + to Maven Central. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations. Contributed by tak7iji. (markt) + + + Update UnboundID to 6.0.9. (markt) + + + Update Checkstyle to 10.12.1. (markt) + + + Update BND to 6.4.1. (markt) + + + Update JSign to 5.0. (markt/rjung) + + + Align documentation for maxParameterCount to match hard-coded defaults. + Contributed by Michal Sobkiewicz. (schultz) + + + +
    +
    + + + + Move the management of the utility executor from the + init()/destroy() methods of components to the + start()/stop() methods. (markt) + + + Add org.apache.catalina.core.StandardVirtualThreadExecutor, + a virtual thread based executor that may be used with one or more + Connectors to process requests received by those Connectors using + virtual threads. This Executor requires a minimum Java version of Java + 21. (markt) + + + 66513: Add a per session Semaphore to the + PersistentValve that ensures that, within a single Tomcat + instance, there is no more than one concurrent request per session. Also + expand the debug logging to include whether a request bypasses the Valve + and the reason if a request fails to obtain the per session Semaphore. + (markt) + + + 66609: Ensure that the default servlet correctly escapes + file names in directory listings when using XML output. Based on pull + request 621 by Alex Kachanov. (markt) + + + 66618: Add a numeric last modified field to the XML directory + listings produced by the default servlet to enable sorting in the XSLT. + Pull request 622 by Alex Kachanov. (markt) + + + 66621: Attempts to lock a collection with WebDAV may + incorrectly fail if a child collection has an expired lock. (markt) + + + 66622: Deprecate the xssProtectionEnabled + setting from the HttpHeaderSecurityFilter and change the + default value to false as support for the associated HTTP + header has been removed from all major browsers. (markt) + + + + + + + Update the HTTP/2 implementation to use the prioritization scheme + defined in RFC 9218 rather than the one defined in RFC 7540. + (markt) + + + 66602: not sending WINDOW_UPDATE when dataLength is ZERO + on call SwallowedDataFramePayload. Pull request #619 by + ledefe. (lihan) + + + + + + + Update to Commons Daemon 1.3.4. (markt) + + + Improvements to French translations. (remm) + + + Update Checkstyle to 10.12.0. (markt) + + + Update the packaged version of the Apache Tomcat Native Library to 2.0.4 + to pick up the Windows binaries built with with OpenSSL 3.0.9. (markt) + + + +
    +
    + + + + 66567: Fix missing IllegalArgumentException + after the Tomcat code was converted to using URI instead of URL. (remm) + + + Escape timestamp output in AccessLogValve if a + SimpleDateFormat is used which contains verbatim + characters that need escaping. (rjung) + + + Change output of vertical tab in AccessLogValve from + \v to \u000b. (rjung) + + + Improve performance of escaping in AccessLogValve + roughly by a factor of two. (rjung) + + + Improve JsonAccessLogValve: support more patterns + like for headers and attributes. Those will be logged as sub objects. + (rjung) + + + 613: Fix possible partial corrupted file copies when using + file locking protection or the manager servlet. Submitted + by Jack Shirazi. (remm) + + + Add RateLimitFilter which can be used to mitigate DoS and Brute Force + attacks. (isapir) + + + + + + + Add support for a new character set, gb18030-2022 - + introduced in Java 21, to the character set caching mechanism. (markt) + + + Fix an edge case in HTTP header parsing and ensure that HTTP headers + without names are treated as invalid. (markt) + + + Deprecate the HTTP Connector settings rejectIllegalHeader + and allowHostHeaderMismatch as they have been removed in + Tomcat 11 onwards. (markt) + + + 66591: Fix a regression introduced in the fix for + 66512 that meant that an AJP Send Headers was not sent for + responses where no HTTP headers were set. (markt) + + + + + + + 66582: Account for EL having stricter requirements for static + imports than JSPs when adding JSP static imports to the EL context. + (markt) + + + + + + + 66574: Refactor WebSocket session close to remove the lock on + the SocketWrapper which was a potential cause of deadlocks + if the application code used simulated blocking. (markt) + + + 66575: Avoid unchecked use of the backing array of a + buffer provided by the user in the compression transformation. (remm) + + + Improve exception handling when flushing batched messages during + WebSocket session close. (markt) + + + 66581: Update AsyncChannelGroupUtil to align it + with the current defaults for AsynchronousChannelGroup. Pull request + 612 by Matthew Painter. (markt) + + + + + + + Improvements to French translations. (remm) + + + Improvements to Chinese translations. (lihan) + + + Update Checkstyle to 10.10.0. (markt) + + + Update Jacoco to 0.8.10. (markt) + + + Update the packaged version of the Tomcat Migration Tool for Jakarta EE + to 1.0.7. (markt) + + + +
    +
    + + + + 65995: Implement RFC 9239 and use + text/javascript as the media type for JavaScript rather + than application/javascript. (markt) + + + Add an access log valve that uses a json format. Based on pull request + 539 provided by Thomas Meyer. (remm) + + + Harden the FORM authentication process against DoS attacks by using a + reduced session timeout if the FORM authentication process creates a + session. The duration of this timeout is configured by the + authenticationSessionTimeout attribute of the FORM + authenticator. (markt) + + + 66527: Correct the Javadoc for the + Tomcat.addWebapp() methods that incorrectly stated that the + docBase parameter could be a relative path. (markt) + + + 66524 Correct eviction ordering in WebResource cache to + be LRU as intended. (schultz) + + + Use server.xml to reduce the default value of + maxParameterCount from 10,000 to 1,000. If not configured + in server.xml, the default remains 10,000. (markt) + + + Update Digest authentication support to align with RFC 7616. This adds a + new configuration attribute, algorithms, to the + DigestAuthenticator with a default of + SHA-256,MD5. (markt) + + + Add support code for custom user attributes in RealmBase. + Based on code from 473 by Carsten Klein. (remm) + + + Expand the set of HTTP request headers considered sensitive that should + be skipped when generating a response to a TRACE request. + This aligns with 11.0.x. (markt) + + + 66541: Improve handling for cached resources for resources + that use custom URL schemes. The scheme specific equals() + and hashCode() algorithms, if present, will now be used for + URLs for these resources. This addresses a potential performance issue + with some OSGi custom URL schemes that can trigger potentially slow DNS + lookups in some configurations. Based on a patch provided by Tom + Whitmore. (markt) + + + When using a custom session manager deployed as part of the web + application, avoid ClassNotFoundExceptions when validating + session IDs extracted from requests. (markt) + + + 66543: Give StandardContext#fireRequestDestroyEvent + its own log message. (fschumacher) + + + 66554: Initialize Random during server initialization to + avoid possible JVM thread creation in the webapp context on some + platforms. (remm) + + + Make the server utility executor available to webapps using a Servlet + context attribute named + org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor. (remm) + + + + + + + JSON filter should support specific escaping for common special + characters as defined in RFC 8259. Based on code submitted by + Thomas Meyer. (remm) + + + 66511: Fix GzipOutputFilter (used for compressed + HTTP responses) when used with direct buffers. Patch suggested by Arjen + Poutsma. (markt) + + + 66512: Align AJP handling of invalid HTTP response headers + (they are now removed from the response) with HTTP. (markt) + + + 66530: Correct a regression in the fix for bug + 66442 that meant that streams without a response body did not + decrement the active stream count when completing leading to + ERR_HTTP2_SERVER_REFUSED_STREAM for some connections. + (markt) + + + + + + + Fix bug that meant some instances of coercing a + LambdaExpression to a functional interface invocation + failed. (markt) + + + 66536: Fix parsing of tag files that meant that tag + directives could be ignored for some tag files. (markt) + + + + + + + 66535: Redefine the maxValidTime attribute of + FarmWarDeployer to be the maximum time allowed between + receiving parts of a transferred file before the transfer is cancelled + and the associated resources cleaned-up. A new warning message will be + logged if the file transfer is cancelled. (markt) + + + + + + + 66508: When using WebSocket with NIO2, avoid waiting for + a timeout before sending the close frame if an I/O error occurs during a + write. (markt) + + + 66548: Expand the validation of the value of the + Sec-Websocket-Key header in the HTTP upgrade request that + initiates a WebSocket connection. The value is not decoded but it is + checked for the correct length and that only valid characters from the + base64 alphabet are used. (markt) + + + + + + + 66542: Documentation. Update the JNDI documentation to + replace references to JavaMail with references to Jakarta Mail. (markt) + + + + + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations. Contributed by Shirayuking and + tak7iji. (markt) + + + Improvements to Chinese translations. Contributed by totoo. (markt) + + + Refactor code using MD5Encoder to use + HexUtils.toHexString(). (markt) + + + 66507: Fix a bug that $JAVA_OPTS is not passed + to the jvm in catalina.sh when calling version. + Patch suggested by Eric Hamilton. (lihan) + + + Update the internal fork of Commons DBCP to f131286 (2023-03-08, + 2.10.0-SNAPSHOT). This corrects a regression introduced in 10.1.5. + (markt) + + + Improve the error messages if JRE_HOME or + JAVA_HOME are not set correctly. On windows, align the + handling of JRE_HOME and JAVA_HOME for the + start-up scripts and the service install script. (markt) + + + Update to the Eclipse JDT compiler 4.27. (markt) + + + Update UnboundID to 6.0.8. (markt) + + + Update Checkstyle to 10.9.3. (markt) + + + Update Jacoco to 0.8.9. (markt) + + + Enhance PEMFile to load from an InputStream. Patch provided by + Romain Manni-Bucau. (schultz) + + + +
    +
    + + + + Fix a bug that memory allocation is larger than limit in + SynchronizedStack to reduce memory footprint. (lihan) + + + + + + + Add support for txt: and rnd: rewrite map + types from mod_rewrite. Based on a pull request 591 + provided by Dimitrios Soumis. (remm) + + + Provide a more appropriate response (501 rather than 400) when rejecting + an HTTP request using the CONNECT method. (markt) + + + 66488: Correct a regression introduced in the fix for bug + 66196 that meant that the HTTP headers and/or request line + could get corrupted (one part overwriting another part) within a single + request. (markt) + + + 66491: Revert the switch to using the ServiceLoader mechanism + to load the custom URL protocol handlers that Tomcat uses. The original + system property based approach has been restored. (markt) + + + + + + + Add a check for the validity of the scheme pseudo-header in HTTP/2. + (markt) + + + 66482: Restore inline state after async operation in NIO2, + to account the fact that unexpected exceptions are sometimes thrown + by the implementation. Patch submitted by zhougang. (remm) + + + +
    +
    + + + + Allow a Valve to access cookies from a request that cannot be mapped to + a Context. (markt) + + + 66438: Correct names of Jakarta modules in JPMS metadata. + (markt) + + + Switch to using the ServiceLoader mechanism to load the custom URL + protocol handlers that Tomcat uses. (markt) + + + Avoid possible ISE when scanning from bad JAR URLs, to restore the + previous behavior following the removal of Java 9+ reflection code which + caught the ISE. (remm) + + + Refactor uses of String.replaceAll() to use + String.replace() where regular expressions where not being + used. Pull request 581 provided by Andrei Briukhov. (markt) + + + Add error report valve that allows redirecting to of proxying from an + external web server. Based on code and ideas from pull request + 506 provided by Max Fortun. (remm) + + + 66470: Add the Shared Address Space defined by RFC 6598 + (100.64.0.0/10) to the regular expression used to identify internal + proxies for the RemoteIpFilter and + RemoteIpValve. (markt) + + + 66471: Fix JSessionId secure attribute missing When + RemoteIpFilter determines that this request was submitted + via a secure channel. (lihan) + + + + + + + Log basic information for each configured TLS certificate when Tomcat + starts. (markt) + + + 66442: When an HTTP/2 response must not include a body, + ensure that the end of stream flag is set on the headers frame and that + no data frame is sent. (markt) + + + 66455: Fix the cause of a potential + ClassCastException when processing a + WINDOW_UPDATE frame on an HTTP/2 connection where the flow + control window for the overall connection has been exhausted. (markt) + + + Fix a regression introduced in 10.1.0-M17 that prevented HTTP/2 + connections from timing out when using a Connector configured with + useAsyncIO=true (the default). (markt) + + + Provided dedicated loggers + (org.apache.tomcat.util.net.NioEndpoint.certificate / + org.apache.tomcat.util.net.Nio2Endpoint.certificate) for + logging of configured TLS certificates. (markt) + + + + + + + 66419: Fix calls from expression language to a method that + accepts varargs when only one argument was passed. (markt) + + + 66441: Make imports of static fields in JSPs visible to any + EL expressions used on the page. (markt) + + + + + + + 66429: Documentation. Limit access to the documentation web + application to localhost by default. (markt) + + + 66429: Examples. Limit access to the examples web application + to localhost by default. (markt) + + + + + + + Update BND to 6.4.0. (markt) + + + Improvements to Korean translations. (woonsan) + + + Update the packaged version of the Apache Tomcat Native Library to 2.0.3 + to pick up the Windows binaries built with with OpenSSL 3.0.8. (markt) + + + +
    +
    + + + + 66388: Correct a regression in the refactoring that replaced + the use of the URL constructors. The regression broke + lookups for resources that contained one or more characters in their + name that required escaping when used in a URI path. (markt) + + + 66392: Change the default value of AccessLogValve's + file encoding to UTF-8 and update documentation. (lihan) + + + 66393: Align ExtendedAccessLogValve's x-P(XXX) with the + documentation. (lihan) + + + + + + + When resetting an HTTP/2 stream because the final response has been + generated before the request has been fully read, use the HTTP/2 error + code NO_ERROR so that client does not discard the response. + Based on a suggestion by Lorenzo Dalla Vecchia. (markt) + + + 66385: Correct a bug in HTTP/2 where a non-blocking read for + a new frame with the NIO2 connector was incorrectly made using the read + timeout leading to unexpected stream closure. (markt) + + + + + + + 66370: Change the default of the + org.apache.el.GET_CLASSLOADER_USE_PRIVILEGED system + property to true unless the EL library is running on Tomcat + in which case the default remains false as the EL library + is already called from within a privileged block and skipping the + unnecessary privileged block improves performance. (markt) + + + Add support for specifying Java 21 (with the value 21) as + the compiler source and/or compiler target for JSP compilation. If used + with an Eclipse JDT compiler version that does not support these values, + a warning will be logged and the default will used. + (markt) + + + + + + + Update the internal fork of Apache Commons BCEL to 2ee2bff (2023-01-03, + 6.7.1-SNAPSHOT). (markt) + + + Update the internal fork of Apache Commons Codec to 3eafd6c (2023-01-03, + 1.16-SNAPSHOT). (markt) + + + Update the internal fork of Apache Commons FileUpload to 34eb241 + (2023-01-03, 2.0-SNAPSHOT). (markt) + + + Update the internal fork of Apache Commons DBCP to f131286 (2023-01-03, + 2.10.0-SNAPSHOT). (markt) + + + Improvements to Japanese translations. Contributed by Shirayuking. + (markt) + + + Improvements to Portuguese translations. Contributed by Guilherme + Custódio. (markt) + + + Update to the Eclipse JDT compiler 4.26. (markt) + + + Update Checkstyle to 10.6.0. (markt) + + + Update Unboundid to 6.0.7. (markt) + + + Update SpotBugs to 4.7.3. (markt) + + + +
    +
    + + + + Update the packaged version of the Apache Tomcat Migration Tool for + Jakarta EE to 1.0.6. (markt) + + + +
    +
    + + + + Correct the default implementation of + HttpServletRequest.isTrailerFieldsReady() to return + true so it is consistent with the default implementation of + HttpServletRequest.getTrailerFields() and with the Servlet + API provided by the Jakarta EE project. (markt) + + + Refactor WebappLoader so it only has a runtime dependency + on the migration tool for Jakarta EE if configured to use the converter + as classes are loaded. (markt) + + + Improve the behavior of the credential handler attribute that is set in + the Servlet context so that it actually reflects what is used during + authentication. (remm) + + + 66359: Update javadoc for RemoteIpValve and RemoteIpFilter with + correct protocolHeader default value of "X-Forwarded-Proto". + (lihan) + + + + + + + When an HTTP/2 stream was reset, the current active stream count was not + reduced. If enough resets occurred on a connection, the current active + stream count limit was reached and no new streams could be created on + that connection. (markt) + + + + + + + 66348: Update the JARs listed in the class loader + documentation and note which ones are optional. (markt) + + + Documentation. Replace references in the application developer's guide + to CVS with more general references to a source code control system. + (markt) + + + + + + + Refactor code base to replace use of URL constructors. While they are + deprecated in Java 20 onwards, the reasons for deprecation are valid for + all versions so move away from them now. (markt) + + + Refine the Tomcat native image metadata to avoid including unintended + non-Tomcat resources. Pull request 569 provided by Sébastien + Deleuze. (markt) + + + Update the internal fork of Apache Commons BCEL to b015e90 (2022-11-28, + 6.7.0-RC1). (markt) + + + Update the internal fork of Apache Commons Codec to ae32a3f (2022-11-29, + 1.16-SNAPSHOT). (markt) + + + Update to Commons Daemon 1.3.3. (markt) + + + Update the internal fork of Apache Commons FileUpload to aa8eff6 + (2022-11-29, 2.0-SNAPSHOT). (markt) + + + Improvements to Japanese translations. Contributed by Shirayuking and + tak7iji. (markt) + + + +
    +
    + + + + 66209: Add a configuration option to allow bloom filters used + to index JAR files to be retained for the lifetime of the web + application. Prior to this addition, the indexes were always flushed by + the periodic calls to WebResourceRoot.gc(). As part of this + addition, configuration of archive indexing moves from + Context to WebResourceRoot. Based on a patch + provided by Rahul Jaisimha. (markt) + + + 66330: Correct a regression introduced when fixing + 62897 that meant any value configured for + skipMemoryLeakChecksOnJvmShutdown on the + Context was ignored and the default was always used. + (markt) + + + 66331: Fix a regression in refactoring for Stack + on the SystemLogHandler which caught incorrect exception. + (lihan) + + + 66338: Fix a regression that caused a nuance in refactoring + for ErrorReportValve. (lihan) + + + Escape values used to construct output for the + JsonErrorReportValve to ensure that it always outputs valid + JSON. (markt) + + + + + + + Correct the date format used with the expires attribute of HTTP cookies. + A single space rather than a single dash should be used to separate the + day, month and year components to be compliant with RFC 6265. (markt) + + + Include the name of the current stream state in the error message when a + stream is cancelled due to an attempt to write to the stream when it is + in a state that does not permit writes. (markt) + + + NIO writes never return -1 so refactor CLOSED_NIO_CHANNEL + not to do so and remove checks for this return value. Based on + 562 by tianshuang. (markt) + + + Remove unnecessary code that exposed the asyncTimeout to + components that never used it. (markt) + + + + + + + 66294: Make the use of a privileged block to obtain the + thread context class loader added to address 62080 optional + and disabled by default. This is now controlled by the + org.apache.el.GET_CLASSLOADER_USE_PRIVILEGED system + property. (markt) + + + 66317: Fix for Lambda coercion security manager missing + privileges. Based on pull request #557 by Isaac Rivera Rivas (lihan) + + + 66325: Fix concurrency issue in evaluation of expression + language containing lambda expressions. (markt) + + + + + + + 66346: Ensure all JDBC pool JARs are reproducible. Pull + request 566 provided by John Neffenger. (markt) + + + + + + + Update to Commons Daemon 1.3.2. (markt) + + + 66323: Move module start up parameters from + JDK_JAVA_OPTIONS to JAVA_OPTS now that the + minimum Java version is 11 and these options are always required. (markt) + + + Improvements to Chinese translations. Contributed by DigitalCat and + lihan. (markt) + + + Improvements to French translations. Contributed by Mathieu Bouchard. + (markt) + + + Improvements to Japanese translations. Contributed by Shirayuking. + (markt) + + + Correct a regression in the removal of the APR connector that broke + Graal native image support. Pull request 564 provided by + Sébastien Deleuze. (markt) + + + Update the packaged version of the Apache Tomcat Native Library to 2.0.2 + to pick up the Windows binaries built with with OpenSSL 3.0.7. (markt) + + + Update the packaged version of the Apache Tomcat Migration Tool for + Jakarta EE to 1.0.5. (markt) + + + +
    +
    + + + + Update the RewriteValve to perform pattern matching using + dotall mode to avoid unexpected behaviour if the URL includes encoded + line terminators. (markt) + + + + + + + 66276: Fix incorrect class cast when adding + a descendant of HTTP/2 streams. (lihan) + + + 66281: Fix unexpected timeouts that may appear as client + disconnections when using HTTP/2 and NIO2. (markt) + + + Enforce the requirement of RFC 7230 onwards that a request with a + malformed content-length header should always be rejected + with a 400 response. (markt) + + + + + + + 66277: Fix regressions in refactoring from Stack + ArrayDeque. + + + Add support for specifying Java 20 (with the value 20) as + the compiler source and/or compiler target for JSP compilation. If used + with an Eclipse JDT compiler version that does not support these values, + a warning will be logged and the default will used. + (markt) + + + + + + + Documentation. Document the nonceRequestParameterName + attribute for the CsrfPreventionFilter. Based on + 553 by Mert Ülkgün. (markt) + + + + + + + Update to the Eclipse JDT compiler 4.23. (markt) + + + Update Objenesis to 3.2. (markt) + + + Update UnboundID to 6.0.6. (markt) + + + Update Checkstyle to 10.3.4. (markt) + + + Update JaCoCo to 0.8.8. (markt) + + + Update SpotBugs to 4.7.2. (markt) + + + Update JSign to 4.2. (markt) + + + Update Derby to 10.16.1.1. (markt) + + + Improvements to Chinese translations. (markt) + + + Improvements to Czech translations. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations. Contributed by tak7iji and + Shirayuking. (markt) + + + Improvements to Korean translations. (markt) + + + Improvements to Spanish translations. (markt) + + + +
    +
    + + + + Update Panama OpenSSL code for the extensive Java 20 changes. (remm) + + + Fix a regression in refactoring for Hashtables which caused mbeans to + lose many of their attributes. (remm) + + + + + + + 66203: Log an error message when the JSP compiler is unable + to create the output directory for the generated code. (markt) + + + + + + + Further automation to the build process to reduce the number of manual + steps that release managers must perform. (markt) + + + +
    +
    + + + + Prepare OpenSSL Panama module for Java 20 API changes. (remm) + + + + + + + Update the Apache Tomcat migration tool for Jakarta EE library to 1.0.4. + (markt) + + + +
    +
    + + + + Correct a regression in the previous fix for 66236. (markt) + + + +
    +
    + + + + Correct handling of HTTP TRACE requests where there are multiple + instances of an HTTP header with the same name. (markt) + + + Implement the requirements of RFC 7231 and do not include sensitive + headers in responses to HTTP TRACE requests. (markt) + + + Implement the clarification in RFC 9110 that the units in HTTP range + specifiers are case insensitive. (markt) + + + Properly-escape role and group information when writing + MemoryUserDatabase to an XML file. (schultz) + + + Move control of XML-export logic from individual support classes into + MemoryUserDatabase.save(). Deprecate and discontinue use of MemoryUser, + MemoryRole, and MemoryGroup classes. (schultz) + + + 66183: When logging cookie values in an access log valve and + there are multiple cookies with the same name, log all cookie values + rather than just the first. Based on pull request 541 by Han + Li. (markt) + + + 66184: Ensure that JULI root loggers have a default level of + INFO. Pull request 533 provided by Piotr P. + Karwasz. (markt) + + + Improve handling of stack overflow errors when parsing SSI expressions. + (markt) + + + 66120: Enable FORM authentication to work correctly if + session persistence and restoration occurs during the authentication + process. (markt) + + + 66233: Include an error message when sending a 400 response + because a request has too many cookies. (markt) + + + When web application deployment fails due to JARs with duplicate + fragment names, improve the error message by listing the JARs that + contain the duplicates. Based on pull request 535 by Mads + Rolsdorph. (markt) + + + Replace logging thread for JULI's AsyncFileHandler with an + executor to protect against failure of the logging thread. Based on pull + request 545 by Piotr P. Karwasz. (markt) + + + + + + + Avoid potential NPE by skipping duplicate accept check when using a Unix + Domain Socket. Based on 532 by Han Li. (markt) + + + Address an edge case in HTTP header parsing that allowed CRCRLF to be + used as a valid line terminator. (markt) + + + Ensure HTTP/2 requests that include connection specific headers are + rejected. (markt) + + + When processing HTTP/2 requests, allow a host header to be + used in place of an :authority header. (markt) + + + When processing HTTP/2 requests, allow a host header and an + :authority header to be present providing they are + consistent. (markt) + + + When processing HTTP/2 requests, reject requests containing multiple + host headers. (markt) + + + Make parsing of invalid filename directives in + Content-Disposition headers more robust. Invalid filename + directives will now be ignored rather than triggering a 500 response. + (markt) + + + 66194: Log HTTP/2 stream closures (usually caused by client + errors) via a UserDataHelper to broadly align it with the + behaviour of HTTP/1.1 for parsing issues and exceeding limits. (markt) + + + 66196: Align HTTP/1.1 with HTTP/2 and throw an exception when + attempting to commit a response with an header value that includes one + or more characters with a code point above 255. (markt) + + + 66236: Implement support for the special values zero and + minus one when configuring maxSavePostSize for a Connector + when used in conjunction with TLS renegotiation. (markt) + + + 66240: Avoid int overflow when parsing octets by limiting + the maximum value to 255. Based on a PR 548 by Stefan Mayr. + (lihan) + + + 550: Correctly handle case where a Servlet responds to a + request with an expectation with a 2xx response without reading the + request body. Pull request provided by Malay Shah. (markt) + + + 551: Avoid potential IndexOutOfBoundsException by fixing + incorrect check when matching HTTP/2 preface. Submitted by 刘文章. + (lihan) + + + + + + + Improve handling of stack overflow errors when parsing EL expressions. + (markt) + + + Correct parsing of integer and floating point literals in EL expressions + so that larger values are correctly parsed to BigInteger + and BigDecimal respectively. (markt) + + + 66235: Fix various issues with the bean resolver used for + Graal. (remm) + + + Improve the performance of the ImportHandler in the + Expression Language implementation. This removes a previous optimisation + that is now detrimental rather than helpful. Pull request 547 + provided by rmannibucau. (markt) + + + Improve handling of EL error messages so instances of Number are not + formatted in unexpected ways. (markt/kkolinko) + + + Switch to using ELException rather than IllegalArgumentException when a + type conversion fails during an EL arithmetic operation. This is an EL + error so ELException seems more appropriate. (markt) + + + Fix a bug in MethodExpression handling that triggered an + error when invoking a static method on an instance of the class rather + than directly on the class. (markt) + + + Use BigInteger.remainder() rather than + BigInteger.mod() when performing the modulus operation for + instances of BigInteger as part of an EL expression. + (markt) + + + + + + + To aid future additions of new functionality, rather than throw an + IllegalArgumentException if a DeltaRequest is + passed an unrecognised action type, a warning message will now be + logged. (markt) + + + 66120: Enable FORM authentication to work correctly if + session failover occurs during the authentication process. (markt) + + + + + + + 62312: Add support for authenticating WebSocket clients with + an HTTP forward proxy when establishing a connection to a WebSocket + endpoint via a forward proxy that requires authentication. Based on a + patch provided by Joe Mokos. (markt) + + + + + + + Ensure that zip archives use UTC for file modification times to ensure + repeatable builds across time zones. (markt) + + + Improvements to Chinese translations. (lihan) + + + Improvements to Czech translations. (markt) + + + Improvements to French translations. (remm) + + + Improvements to German translations. (markt) + + + Improvements to Japanese translations. Contributed by tak7iji and + Shirayuking. (markt) + + + Improvements to Korean translations. Contributed by 수현. (markt) + + + Improvements to Brazilian Portuguese translations. (markt) + + + Improvements to Russian translations. (markt) + + + Improvements to Spanish translations. (markt) + + + Update the Apache Tomcat migration tool for Jakarta EE library to 1.0.3. + (markt) + + + +
    +
    + + + + 66104: Avoid error message by not trying to clean up old + files from the logging directory before the directory has been created. + Based on 521 by HanLi. (markt) + + + Update the Jakarta Common Annotations API to 2.1.1. This deprecates the + ManagedBean annotation which will be removed in a future + release. (markt) + + + + + + + Provide dedicated loggers + (org.apache.tomcat.util.net.NioEndpoint.handshake / + org.apache.tomcat.util.net.Nio2Endpoint.handshake) for TLS + handshake failures. (markt) + + + Enable the use of the FIPS provider for TLS enabled Connectors when + using Tomcat Native 1.2.34 onwards built with OpenSSL 3.0.x onwards. + (markt) + + + Remove the jvmRoute system property used to configure a + default value for the jvmRoute attribute of an Engine. + (markt) + + + Update experimental Panama modules with support for OpenSSL 3.0+. + OpenSSL 1.1 remains supported. (remm) + + + Correct a regression in the refactoring to support experimentation with + project Loom that broke HTTP/2 support if async IO was disabled. (markt) + + + Fix duplicate Poller registration with HTTP/2, NIO and async IO that + could cause HTTP/2 connections to unexpectedly fail. (markt) + + + Refactor Panama module to better take advantage of the Panama preview + API updates and fixes. Improves memory session usage and avoids some + allocations. Review from Maurizio Cimadamore. (remm) + + + Update the minimum recommended version of the Tomcat Native Library to + 2.0.1. (markt) + + + + + + + Add support for specifying Java 19 (with the value 19) as + the compiler source and/or compiler target for JSP compilation. If used + with an Eclipse JDT compiler version that does not support these values, + a warning will be logged and the default will used. + (markt) + + + + + + + Remove configuration settings related to the restriction on WebSocket + endpoint deployment that was removed in version 2.1 of the + specification. (markt) + + + + + + + Documentation. 62245: Include contextXsltFile + when discussing options for configuring directory listings. (markt) + + + Examples. Fix CVE-2022-34305, a low severity XSS vulnerability in the + Form authentication example. (markt) + + + Documentation. Expand the description of the useSendfile + attribute for HTTP/2 and reference the possibility of file locking when + using this feature on Windows operating systems. (markt) + + + + + + + Update to bnd 6.3.1. (markt) + + + The minimum Ant version required to build Tomcat 10.1.x is now 1.10.2. + (markt) + + + Add additional automation to the build process to reduce the number of + manual steps that release managers must perform. (schultz) + + + Implement support for reproducible builds. Reproducible builds are + independent of operating system but require the same Ant version and + same JDK (vendor and version) to be used as associated version + information is embedded in a number of build outputs such as JAR file + manifests. (markt) + + + Update the minimum supported version of Tomcat Native to 1.2.34 to allow + the removal of the deprecated Java API associated with features that + will be removed in Tomcat Native 2.0.x. (markt) + + + Remove and/or update references to the removed + org.apache.tomcat.util.threads.res package. The + LocalStrings*.properties files in that package were moved + to org.apache.tomcat.util.threads package for consistency + with the rest of the Tomcat code base. (markt) + + + 66134: The NSIS based Tomcat installer for Windows now + correctly handles the combination of TomcatAdminRoles + defined in a configuration file and selecting the Manager and/or + Host Manager web applications in the installer's GUI. (markt) + + + Update the OWB module to Apache OpenWebBeans 2.0.27. (remm) + + + Update the CXF module to Apache CXF 3.5.3. (remm) + + + Update the Apache Tomcat migration tool for Jakarta EE library to 1.0.1. + (markt) + + + Update the packaged version of the Tomcat Native Library to 2.0.1 to + pick up the Windows binaries built with with OpenSSL 3.0.5. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations contributed tak7iji. (markt) + + + +
    +
    + + + + Update the memory leak protection code to support stopping application + created executor threads when running on Java 19 and later. (markt) + + + Improve the error message if a required --add-opens option + is missing. (markt) + + + Disable the memory leak correction code enabled by the Context attribute + clearReferencesObjectStreamClassCaches when running on a + JRE that includes a fix for the underlying memory leak. (markt) + + + 515: Avoid deadlock on startup with some utility executor + configurations. Submitted by Han Li. (remm) + + + 66068: Ensure that the changes made to a request by the + RemoteIPValve persist after the request is put into + asynchronous mode. (markt) + + + Include the major version in the recommended version used for Tomcat + Native with the AprLifecycleListener. (markt) + + + Remove the reporting of the unused APR feature flags. (markt) + + + + + + + Additional fix for 65118. Fix a potential + NullPointerException when pruning closed HTTP/2 streams + from the connection. (markt) + + + Refactor synchronization blocks locking on SocketWrapper to + use ReentrantLock to support users wishing to experiment + with project Loom. (markt) + + + 66076: When using TLS with non-blocking writes and the NIO + connector, ensure that flushing the buffers attempts to empty all of the + output buffers. (markt) + + + 66084: Correctly calculate bytes written to a response. Pull + request 516 provided by aooohan HanLi. (markt) + + + Correct a regression in the support added for encrypted PKCS#1 formatted + private keys in the previous release that broke support for unencrypted + PKCS#1 formatted private keys. (jfclere/markt) + + + Remove support for NPN when using the Tomcat Native Connector as NPN was + never standardised and browser support for NPN was removed several years + ago. (markt) + + + + + + + Update XML schema used for generated web fragments to use the Servlet + 6.0 web fragment schema. (markt) + + + Update the XML schema used by the web fragment defined for the Jasper EL + JAR to use the Servlet 6.0 web fragment schema. (markt) + + + Update ImportHandler optimisation for new classes + introduced in Java 19. (markt) + + + + + + + Update the XML schema used by the web fragment defined for the WebSocket + JAR to use the Servlet 6.0 web fragment schema. (markt) + + + + + + + 66064: Update the building page in the documentation web + application to reflect changes in required Java version and source + repository. (markt) + + + Documentation. Make the description of the HTTP/1.1 configuration + attributes that control the maximum allowed HTTP header size more + specific. (markt) + + + + + + + Increase the default buffer size for replication messages from 43800 to + 65536 bytes. This is expected to improve performance for large messages + when running on Linux based systems. (markt) + + + + + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations contributed by Shirayuking and + tak7iji. (markt) + + + Improvements to Chinese translations contributed by Dingzi2012. (markt) + + + +
    +
    + + + + 65853: Refactor the CsrfPreventionFilter to make + it easier for sub-classes to modify the nonce generation and storage. + Based on suggestions by Marvin Fröhlich. (markt) + + + 65991: Avoid NPE with SSLAuthenticator when + boundOnInit is used on a connector, during the check + for client certificate authentication availability. (remm) + + + 66009: Use getSubjectX500Principal().toString() + rather than getSubjectX500Principal().getName(...) to + retrieve a certificate DN, to match the output of the deprecated + getSubjectDN().getName() that was used previously. (remm) + + + Revert the change in 10.1.0-M11 that added a mapping of + Shift_JIS for the ja locale to the default + mappings used by ServletResponse.setLocale() as it + caused regressions for applications using UTF-8. (markt) + + + Provide a property source that sources values from Kubernetes service + bindings. Pull request 512 provided by Sumit Kulhadia and + Gareth Evans. (markt) + + + + + + + 501: Add new maxHttpRequestHeaderSize and + maxHttpResponseHeaderSize attributes which allow setting + the maximum HTTP header sizes independently. If not specified, the + value of the maxHttpHeaderSize connector attribute will + be used. Submitted by Zhongming Hua. (remm) + + + The root cause of the Linux kernel duplicate accept bug has been + identified along with the version of the kernel that includes the fix. + The error message displayed when this bug occurs has been updated to + reflect this new information and to advise users to update to a version + of the OS that uses kernel 5.10 or later. Thanks to Christopher Gual for + the research into this issue. (markt) + + + Remove the custom UTF-8 decoder that was introduced to work around + various UTF-8 decoding bugs in Java. These issues were fixed in early + Java 8 releases. Now the minimum Java version is 11, we can be sure that + Tomcat will not be running on a JRE where these issues are present. + (markt) + + + 66023: Improve the fix for 65726 and support HTTP + upgrade with a request body for a wider set of use cases. (markt) + + + 66035: Add NULL check on the SSL session reference in the + Panama code before accessing the session id and creation time. (remm) + + + Add support for encrypted PKCS#1 formatted private keys when configuring + the internal, in memory key store. Based on 511. + (jfclere/markt) + + + Remove the prestartminSpareThreads attribute of the + StandardThreadExecutor since all core threads are always + started by default making this attribute meaningless. Pull request + 510 provided by Aooohan. (markt) + + + + + + + To align with the JSP 3.1 specification, make the + jsp:plugin action a NO-OP. No HTML will be generated as a + result the jsp:plugin action being included in a JSP. This + is be because the associated HTML elements are no longer supported by + any major browser. (markt) + + + 66031: Fix NPE when using a custom JspFactory. Patch by + Jean-Louis Monteiro. (remm) + + + + + + + 66008: In the documentation web application, clarify the + recommendation for the use the trimSpaces option for Jasper + in production environments. (markt) + + + Update the documentation web application to state that the + EncryptInterceptor does not provide sufficient protection + to run Tomcat clustering over an untrusted network. This is + CVE-2022-29885. (markt) + + + + + + + Improvements to Chinese translations contributed by shawn. (markt) + + + Improvements to French translations. (remm) + + + Improvements to German translations contributed by Thomas Hoffmann. + (markt) + + + Improvements to Japanese translations contributed by Shirayuking. + (markt) + + + Improvements to Korean translations. (woonsan) + + + Update to Commons Daemon 1.3.1. This fixes a known regression in 1.3.0 + when configuring the Windows service with custom scripts as described in + 66055. (markt) + + + Update to JSign 4.1. (markt) + + + Update the packaged version of the Tomcat Native Library to 1.2.33 to + pick up Windows binaries built with OpenSSL 1.1.1o.(markt) + + + +
    +
    + + + + 65736: Disable the forceString option for the + JNDI BeanFactory and replace it with an automatic search + for an alternative setter with the same name that accepts a + String. This is a security hardening measure. (markt) + + + Remove the WebappClassLoaderBase.getResources() method as + it is not used and if something accidentally exposes the class loader + this method can be used to gain access to Tomcat internals. (markt) + + + +
    +
    + + + + Update the JASPIC 2.0 API to Jakarta Authentication 3.0 (JASPIC was + renamed for Jakarta EE 10) including the implementation of the new + methods on AuthConfigFactory. (markt) + + + Harden the CredentialHandler implementations by switching to a + constant-time implementation for credential comparisons. (schultz/markt) + + + + + + + Use a constant for the default TLS cipher suite. This will allow + skipping setting it in some cases (for example, it does not make + sense for OpenSSL TLS 1.3). (remm) + + + 487: Improve logging of unknown settings frames. Pull request + by Thomas Hoffmann. (remm) + + + 65975: Add a warning if a TLS virtual host is configured with + optional certificate authentication and the containing connector is also + configured to support HTTP/2 as HTTP/2 does not permit optional + certificate authentication. (markt) + + + 65975: Add a warning if a TLS virtual host is configured for + TLS 1.3 with a JSSE implementation and a web application is configured + for CLIENT-CERT authentication. CLIENT-CERT + authentication requires post-handshake authentication (PHA) when used + with TLS 1.3 but the JSSE TLS 1.3 implementation does not support PHA. + (markt) + + + Improve the recycling of Processor objects to make it more robust. + (markt) + + + + + + + 65959: Serialize Function as String[] rather Class[]. (remm) + + + + + + + 65947: Correct the name of HTTP/1.1 configuration property + (maxHttpHeaderSize) that is inherited by the HTTP/2 upgrade + protocol. Thanks to Thomas Hoffmann. (markt) + + + 65952: Align --add-opens configuration for jsvc + with the current Tomcat scripts. (markt) + + + Correct the AJP and HTTP/1.1 Connector configuration pages in the + documentation web application to show which attributes are applicable to + all Connectors and which are implementation specific. (markt) + + + + + + + Correct a spelling mistake in the German translations. Thanks to Thomas + Hoffmann. (markt) + + + 65951: Use the tomcat.output property for OSGi + bundle manifest paths. (isapir) + + + Update to Commons Daemon 1.3.0. (markt) + + + Update to Checkstyle 10.0. (markt) + + + Update to SpotBugs 4.6.0. (markt) + + + Expand the spotbugs Ant task to also cover test code. + (markt) + + + Update to bnd 6.2.0. (markt) + + + Remove OSGi annotations dependency as it is no longer required with bnd + 6.2.0. (markt) + + + Update to the Eclipse JDT compiler 4.23. (markt) + + + Refactor the resource files for the Apache Tomcat installer for Windows + so that all the resource files are located in a single directory in the + source tree. (markt) + + + Update the packaged version of the Tomcat Native Library to 1.2.32 to + pick up Windows binaries built with OpenSSL 1.1.1n.(markt) + + + Improvements to Chinese translations contributed by 15625988003. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations contributed by tak7iji. (markt) + + + Expand coverage of translations for jakarta.el package. + Based on 488 from Volodymyr Siedlecki. (markt) + + + +
    +
    + + + + 477: Update the default list of JARs to skip to include the + Apache Log4j JAR for Jakarta EE platforms. Pull request by Michael + Seele. (markt) + + + 65921: The type substitution flag for the + rewrite valve should set the content type for the response, not the + request. (markt) + + + 479: Enable the rewrite valve to redirect requests when the + original request cannot be mapped to a context. This typically happens + when no ROOT context is defined. Pull request by elkman. (markt) + + + 65940: Fix NullPointerException if an exception + occurs during the destruction of a Servlet. (markt) + + + + + + + Fix regression introduced with 65757 bugfix which better + identified non request threads but which introduced a similar problem + when user code was doing sequential operations in a single thread. + Test case code submitted by Istvan Szekely. (remm) + + + Fix potential thread-safety issue that could cause HTTP/1.1 request + processing to wait, and potentially timeout, waiting for additional + data when the full request has been received. (markt) + + + Throw IOException rather than + IllegalStateException when the application attempts to + write to an HTTP/2 stream after the client has closed the stream. + (markt) + + + + + + + When resolving methods in EL expressions that use beans and/or static + fields, ensure that any custom type conversion is considered when + identifying the method to call. (markt) + + + + + + + Correct the name of the value attribute in the new + documentation of OpenSSLConfCmd elements. (rjung) + + + + + + + Fix typo in JPMS substitution configuration for WebSocket client module. + (markt) + + + +
    +
    + + + + Add ha-api-*.jar and jaxws-rt-*.jar to the + list of JARs to skip when scanning for TLDs, web fragments and + annotations. (michaelo) + + + Expand the default mappings used by + ServletResponse.setLocale() to include a mapping from the + ja locale to the Shift_JIS encoding. (markt) + + + 65806: Improve the handling of session ID generation when the + default algorithm for SecureRandom (SHA1PRNG) + is not supported by the configured providers as will be the case for a + FIPS compliant configuration. (markt) + + + 463: Add support for additional user attributes to + TomcatPrincipal and GenericPrincipal. + Patch provided by Carsten Klein. (michaelo) + + + 464: Fall back to the class loader used to load JULI when the + thread context class loader is not set. In a normal Tomcat + configuration, this will be the system class loader. Based on a pull + request by jackshirazi. (markt) + + + 469: Include the Jakarata Annotations API in the classes that + Tomcat will not load from web applications. Pull request provided by + ppkarwasz. (markt) + + + Fix a potential StringIndexOutOfBoundsException exception + when generating a WebDAV multi-status response after an error during a + copy or delete. Report the paths relative to the server root for any + resources with an error. (markt) + + + Improve the format of WebDAV XML responses to make them easier for + humans to read. The change ensures that there is always a line break + before starting a new element. (markt) + + + Improve validation of the Destination header for WebDAV + MOVE and COPY requests. (markt) + + + + + + + Correct a regression in the fix for 65454 that meant that + minSpareThreads and maxThreads settings were + ignored when the Connector used an internal executor. (markt) + + + 65776: Improve the detection of the Linux duplicate accept + bug and reduce (hopefully avoid) instances of false positives. (markt) + + + 65848: Revert the change that attempted to align the + behaviour of client certificate authentication with NIO or NIO2 with + OpenSSL for TLS between MacOS and Linux/Windows as the root cause was + traced to configuration differences. (markt) + + + 467: When system time moves backwards (e.g. after clock + correction), ensure that the cached formatted current date used for + HTTP headers tracks this change. Pull request provided by zhenguoli. + (markt) + + + + + + + 474: Prevent a tag file from corrupting the ELContext of the + calling page. Pull request provided by Dmitri Blinov. (markt) + + + Minor optimisation of serialization for FunctionMapperImpl + in response to pull request 476. (markt) + + + + + + + Remove the applet example from the example web application as applets + are no longer supported in any major browser. (markt) + + + Refactor a small number of pages in the examples web application to + avoid an issue with reproducible builds due to differences in file + ordering across different operating systems with Ant's zip task. (markt) + + + Better documentation for the protocol attribute of the + JNDIRealm. (markt) + + + Clarify the settings described in the documentation web application to + configure a cluster using static membership. (markt) + + + Add information on the OpenSSLConf and + OpenSSLConfCmd elements to the HTTP SSL configuration page + in the documentation web applications. (markt) + + + + + + + Use LF line endings for text files in JARs to support reproducible + builds across different operating systems. (markt) + + + + + + + Use LF line endings for text files in JARs to support reproducible + builds across different operating systems. (markt) + + + Fix dependencies for individual test targets in Ant build file. Based on + 468 provided by Totoo chenyonghui. (markt) + + + Update the OWB module to Apache OpenWebBeans 2.0.26. (remm) + + + Revert the cherry-pick of JavaDoc fix from DBCP applied in 10.1.0.M9 + that broke the DataSourceMXBean by using a type that isn't + supported by MXBeans. (markt) + + + Improvements to Chinese translations contributed by cloudgyb, totoo and + Chenyonghui1028. (markt) + + + Improvements to French translations. (remm) + + + Improvements to German translations contributed by Andreas Abraham. + (markt) + + + Improvements to Japanese translations contributed by tak7iji and + Shirayuking. (markt) + + + Improvements to Korean translations. (woonsan) + + + Improvements to Spanish translations contributed by ceciliabarudi. + (markt) + + + +
    +
    + + + + Correct a regression in the fix for 65785 that broke HTTP/2 + server push. (markt) + + + +
    +
    + + + + Add missing check in SessionCookieConfig.setAttribute() to + ensure that the method fails if called after the web application has + started. (markt) + + + Add additional locking to DataSourceUserDatabase to provide + improved protection for concurrent modifications. (markt) + + + Add recycling check in the input and output stream isReady to try to + give a more informative ISE when the facade has been recycled. (remm) + + + Make the calculation of the session storage location more robust when + using file based persistent storage. (markt) + + + + + + + 65726: Implement support for HTTP/1.1 upgrade when the + request includes a body. The maximum permitted size of the body is + controlled by maxSavePostSize. (markt) + + + Restore pre-starting of minSpareThreads lost in the fix for + 65454. (markt) + + + Revert the previous fix for 65714 and implement a more + comprehensive fix. (markt) + + + Allow freeing up context on JVM shutdown in the OpenSSL Panama module + by properly using a shared scope. (remm) + + + 65757: Missing initial IO listener notification on Servlet + container dispatch to another container thread. (remm) + + + Expand the fix for 65757 so that rather than just checking if + processing is happening on a container thread, the check is now if + processing is happening on the container thread currently allocated to + this request/response. (markt) + + + Improve the fix for RST frame ordering added in 10.1.0-M8 to avoid a + potential deadlock on some systems in non-default configurations. + (markt) + + + 65767: Add support for certificates that use keys encrypted + using PBES2. Based on a pull request provided by xiezhaokun. (markt) + + + Refactor testing whether a String is a valid HTTP token. (markt) + + + 65785: Perform additional validation of HTTP headers when + using HTTP/2. (markt) + + + When a Connector or Endpoint is paused, ensure that only new connections + and new requests on existing connections are stopped while allowing in + progress requests to run to completion. (markt) + + + Explicitly release ByteBuffer instances associated with pooled channels + when stopping the NioEndpoint and Nio2Endpoint. (markt) + + + Narrow the scope of the logging of invalid cookie headers to just the + invalid cookie rather than the whole cookie header. (markt) + + + + + + + 65724: Fix missing messages for some + PropertyNotWritableExceptions caused by a typo in the name + used for a resource string. (markt) + + + Add support for specifying Java 18 (with the value 18) as + the compiler source and/or compiler target for JSP compilation. If used + with an Eclipse JDT compiler version that does not support these values, + a warning will be logged and the default will used. + (markt) + + + To align with the JSP 3.1 specification that requires Java 11 as a + minimum, make the default JSP source version and target version Java 11. + (markt) + + + + + + + Remove the ALLOW_UNSUPPORTED_EXTENSIONS system property. As + per RFC 6455, all extensions are optional. If an endpoint declares an + extension that isn't supported there is no need to trigger an error. The + extension can just be excluded from the result of the negotiation. + (markt) + + + Remove the DISABLE_BUILTIN_EXTENSIONS. It was added to + enable Tomcat to pass the WebSocket TCK but after updates to the TCK, it + is no longer required. (markt) + + + Add support for POJO WebSocket endpoints to the programmatic upgrade + that allows applications to opt to upgrade an HTTP connection to + WebSocket. (markt) + + + Add support for the WebSocket 2.1 client-side API for configuring TLS + connection for wss client connections. (markt) + + + 65763: Improve handling of WebSocket connection close if a + message write times out before the message is fully written. (markt) + + + + + + + Update the OWB module to Apache OpenWebBeans 2.0.25. (remm) + + + Update the CXF module to Apache CXF 3.5.0. (remm) + + + Improvements to Chinese translations contributed by zhnnn. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations contributed by Shirayuking, yoshy + and tak7iji. (markt) + + + Improvements to Korean translations. (woonsan) + + + Improvements to Spanish translations contributed by Israel. (markt) + + + Update SpotBugs to 4.5.2. (markt) + + + Update to the Eclipse JDT compiler 4.22. (markt) + + + Update the NSIS installer to 3.08. (markt) + + + Update UnboundID to 6.0.3. (markt) + + + Update CheckStyle to 9.2.1. (markt) + + + Update BND to 6.1.0. (markt) + + + Update OSGI annotations to 1.1.1. (markt) + + + +
    +
    + + + + Log warning if a listener is not nested inside a Server element + although it must have been. (michaelo) + + + Where the getter can be called safely, remove the checks for + ServletContext getters called from a + contextInitialized() method of a + ServletContextListener that was not defined in a + web.xml file, a web-fragment.xml file nor + annotated with WebListener. (markt) + + + Make SPNEGO authentication more robust for the case where the provided + credential has expired. (markt) + + + Limit cookie support to RFC 6265 to align with recent updates to the + Servlet specification. (markt) + + + 65684: Fix a potential NullPointerException when + using JULI. (markt) + + + Document conditions under which the AprLifecycleListener + can be used to avoid JVM crashes. (michaelo) + + + Refactor the AsyncFileHandler to reduce the possibility of + log messages being lost on shutdown. (markt) + + + Refactor the AsyncFileHandler to remove the need for the + org.apache.juli.AsyncLoggerPollInterval. If set, this + property now has no effect. (markt) + + + Add debug logging to the RestCsrfPreventionFilter. Based on + pull request 452 by Polina Georgieva. (markt) + + + + + + + Use implicit scopes in the OpenSSL Panama module to tie the cleanup of + OpenSSL memory to the Java GC. (remm) + + + Provide protection against a known OS + bug that causes the acceptor to report an incoming connection more + than once. (markt) + + + Avoid unnecessary duplicate read registrations for blocking I/O with the + NIO connector. (markt) + + + 65677: Improve exception handling for errors during HTTP/1.1 + reads with NIO2. (markt) + + + When an error occurs that triggers a stream reset, ensure that the first + RST frame sent to the client is the one associated with the + error that triggered the reset. (markt) + + + 65714: Fix exceptions when the security manager is enabled + and the first request received after starting is an HTTP request to a + TLS enabled NIO2 connector. (markt) + + + Ensure that using NIO or NIO2 with OpenSSL for TLS behaves the same way + on MacOS as it does on Linux and Windows when no trusted certificate + authorities are configured and reject all client certificates. (markt) + + + Avoid a potential deadlock during the concurrent processing of incoming + HTTP/2 frames for a stream and that stream being reset. (markt) + + + + + + + Update the WebSocket API packaging to remove the copy of the client API + from the server API and replace it with a dependency on the client API. + This aligns Tomcat with changes in the WebSocket 2.1 specification. + (markt) + + + +
    +
    + + + + Refactor HttpServlet so the default doHead() + implementation now calls doGet() and relies on the + container to ensure that the response body is not sent. The previous + behaviour (wrapping the response) may be enabled per Servlet by setting + the jakarta.servlet.http.legacyDoHead Servlet + initialisation parameter to true. This aligns Tomcat with + recent changes updates for Servlet 6.0 in the Jakarta Servlet + specification project. (markt) + + + Add support for setting generic attributes for session cookies. This + aligns Apache Tomcat with recent changes in the Jakarta Servlet + specification project. (markt) + + + Do not add a trailing / to a request URI during + canonicalization. (markt) + + + Invalid byte sequences (typically in %nn form) in a request URi that are + not valid for the given URI encoding now trigger a 400 response. (markt) + + + Ensure that a request URI starts with a /. (markt) + + + Add a new Connector option, rejectSuspiciousURIs that will + causes 'suspicious' (see the Servlet 6.0 specification) URIs to be + rejected with a 400 response. (markt) + + + Improve robustness of JNDIRealm for exceptions occurring when getting + the connection. Also add missing close when running into issues + getting the passord of a user. (remm) + + + Add Javadoc comment which listeners must be nested within + Server elements only. (michaelo) + + + Add support for custom caching strategies for web application resources. + This initial implementation allows control over whether or not a + resource is cached. (markt) + + + + + + + Improve performance of Connector shutdown - primarily to reduce the time + it takes to run the test suite. (markt) + + + 457: Add a toString() method to + MimeHeader to aid debugging. (dblevins) + + + Add experimental OpenSSL support through the Panama API incubating in + Java 17, with support for OpenSSL 1.1+. This no longer requires + tomcat-native or APR. Please refer to the openssl-java17 + module for more details. (remm) + + + + + + + Regenerate the EL parser using JavaCC 7.0.10. (markt) + + + Fix a bug that prevented the EL parser correctly parsing a literal Map + that used variables rather than literals for both keys and values. + (markt) + + + Ensure that the getType() method of any + ELResolver implementation returns null if + either the ELResolver or the resolved property is read-only + to align Tomcat with recent updates in the Jakarta EL specification + project. (markt) + + + Implement an alternative solution to support the JSP page directive + attribute isThreadSafe now that the + SingleThreadModel interface has been removed from the + Servlet API. The new approach synchronizes the service() + method. + + + + + + + Add a new method + ServerEndpointConfig.Configurator.getContainerDefaultConfigurator() + to align with recent updates in the WebSocket specification + project. (markt) + + + Add a new method ServerContainer.upgradeHttpToWebSocket() + to align with recent updates in the WebSocket specification project. + (markt) + + + + + + + 454: Differentiate warning messages in + KubernetesMembershipProvider so that the missing attribute + is clear to the user. PR provided by Hal Deadman. (markt) + + + + + + + Switch from Cobertura to JaCoCo for code coverage as Cobertura does not + support code coverage for code compiled for Java 11 onwards. It also + removes the need to use a single thread to run the tests. (markt) + + + +
    +
    + + + + Provide the DataSource in the constructor of + DataSourceUserDatabase, since it is always global. (remm) + + + Fix delete then create object manipulations with + DataSourceUserDatabase. (remm) + + + Remove all deprecated code from the Servlet API to align Tomcat with + recent changes in the Jakarta Servlet specification project. (markt) + + + Add the currently available Jakarta EE 10 schemas from the Jakarta EE + schema project. (markt) + + + Implement the new connection ID and request ID API for Servlet 6.0. + (markt) + + + 65553: Implement a work-around for a + JRE bug + that can trigger a memory leak when using the JNDI realm. (markt) + + + 65586: Fix the bloom filter used to improve performance of + archive file look ups in the web resources implementation so it works + correctly for directory lookups whether or not the provided directory + name includes the trailing /. (markt) + + + 451: Improve the usefulness of the thread name cache used in + JULI. Pull request provided by t-gergely. (markt) + + + + + + + 65563: Correct parsing of HTTP Content-Range + headers. Tomcat was incorrectly requiring an = character + after bytes. Fix based on pull request 449 by + Thierry Guérin. (markt) + + + Correct a potential StackOverflowException with HTTP/2 and + sendfile. (markt) + + + Further improvements in the management of the connection flow control + window. This addresses various bugs that caused streams to incorrectly + report that they had timed out waiting for an allocation from the + connection flow control window. (markt) + + + 65577: Fix a AccessControlException reporting + when running an NIO2 connector with TLS enabled. (markt) + + + Reclassify TLS ciphers that use AESCCM8 as medium security rather than + high security to align with recent changes in OpenSSL. (markt) + + + Fix an issue that caused some Servlet non-blocking API reads of the HTTP + request body to incorrectly use blocking IO. (markt) + + + + + + + Deprecate ELResolver.getFeatureDescriptors to align Tomcat + with recent updates in the Jakarta EL specification project. (markt) + + + Add support for default methods to BeanRELResolver to align + Tomcat with recent updates in the Jakarta EL specification project. + (markt) + + + Add support for MethodReference and the associated getter + on MethodExpression to align Tomcat with recent updates in + the Jakarta EL specification project. (markt) + + + Refactor ScopedAttributeELResolver to separate out the + functionality that is unrelated to scoped attributes into two new + resolvers: ImportELResolver and + NotFoundELResolver. This aligns Tomcat with recent updates + to the Jakarta Server Pages specification. (markt) + + + Fix the implementation of MethodExpression.getMethodInfo() + so that it returns the expected value rather than failing when the + method expression is defined with the parameter values in the expression + rather than the types being passed explicitly to + ExpressionFactory.createMethodExpression(). (markt) + + + Add support for a new page/tag directive errorOnELNotFound + that can be used to trigger an identifier if an EL expression in a + page/tag contains an identifier that cannot be resolved. (markt) + + + + + + + The internal upgrade handler should close the associated + WebConnection on destroy. (remm) + + + + + + + Update the web applications that are included with Apache Tomcat to use + the Jakarta EE 10 schema for web.xml. (markt) + + + Clarify the JASPIC configuration options in the documentation web + application. (markt) + + + + + + + 65585: Update obsolete comments at the start of the + build.properties.default file. (markt) + + + +
    +
    + + + + Enable Tomcat to start if an (old) XML parser is configured that does + not support allow-java-encodings. A warning will be logged + if such an XML parser is detected. (markt) + + + Change the behaviour of custom error pages. If an error occurs after the + response is committed, once the custom error page content has been added + to the response the connection is now closed immediately rather than + closed cleanly. i.e. the last chunk that marks the end of the response + body is no longer sent. This acts as an additional signal to the client + that the request experienced an error. (markt) + + + 65479: When handling requests using JASPIC authentication, + ensure that PasswordValidationCallback.getResult() returns + the result of the password validation rather than always returning + false. Fixed via pull request 438 provided by + Robert Rodewald. (markt) + + + Improve the reusability of the UserDatabase by adding + intermediate concrete implementation classes and allowing to do + partial database updates on save. (remm) + + + Refactor the authenticators to delegate the check for preemptive + authentication to the individual authenticators where an authentication + scheme specific check can be performed. Based on pull request + 444 by Robert Rodewald. (markt) + + + Add a UserDatabase implementation as a superset of the + DataSourceRealm functionality. (remm) + + + Make sure the dynamic Principal returned by + UserDatabaseRealm stays up to date with the database + contents, and add an option to have it be static, similar to the other + realms. (remm) + + + Add derby-*.jar to the list of JARs to skip when scanning + for TLDs, web fragments and annotations. (markt) + + + 447. Correct JPMS metadata for catalina.jar. Pull request + provided by Hui Wang. (markt) + + + + + + + Correct a logic error that meant setting + certificateKeystoreFile to NONE did not have + the expected effect. NONE was incorrectly treated as a file + path. Patch provided by Mikael Sterner. (markt) + + + Remove the deprecated APR/Native connector which includes the HTTP APR + and the AJP APR connector. Also remove the Java interfaces to the + APR/Native library that are not used by the OpenSSL integration for the + NIO and NIO2 connectors. (markt) + + + Refactor the JSSE/OpenSSL integration to avoid the use of + finalize(). (markt) + + + 65505: When an HTTP header value is removed, ensure that the + order of the remaining header values is unchanged. (markt) + + + + + + + 65506: Fix write timeout check that was using the read + timeout value. Patch submitted by Gustavo Mahlow. (remm) + + + + + + + Remove unnecessary Context settings from the examples web application. + (markt) + + + Document default value for unpackWARs and related clean-up. + Pull request 439 provided by Robert Rodewald. (markt) + + + Clarify the documentation of the compressionMinSize and + compressibleMimeType HTTP Connector + attributes. Pull request 442 provided by crisgeek. (markt) + + + + + + + Refactor the ParallelNioSender to avoid the use of + finalize(). (markt) + + + + + + + Fix failing build when building on non-English locales. Pull request + 441 provided by Dachuan J. (markt) + + + Update to JSign version 4.0 to enable code signing without the need for + the installation of additional client tools. (markt) + + + Add Apache Derby 10.15.2.0 to the testsuite dependencies, for JDBC + and DataSource testing. (remm) + + + Update the internal fork of Apache Commons BCEL to 40d5eb4 (2021-09-01, + 6.6.0-SNAPSHOT). Code clean-up only. (markt) + + + Update the internal fork of Apache Commons Codec to fd44e6b (2021-09-01, + 1.16-SNAPSHOT). Minor refactoring. (markt) + + + 65661: Update the internal fork of Apache Commons FileUpload + to 33d2d79 (2021-09-01, 2.0-SNAPSHOT). Refactoring and code clean-up. As + a result of Commons File Upload now using + java.nio.file.Files, applications using multi-part uploads + need to ensure that the JVM is configured with sufficient direct memory + to store all in progress multi-part uploads. (markt) + + + Update the internal fork of Apache Commons Pool to 2.11.1 (2021-08-17). + Improvements, code clean-up and refactoring. (markt) + + + Update the internal fork of Apache Commons DBCP to 2.9.0 (2021-08-03). + Improvements, code clean-up and refactoring. (markt) + + + Update the packaged version of the Tomcat Native Library to 1.2.31 to + pick up Windows binaries built with OpenSSL 1.1.1l.(markt) + + + Switch to the CDN as the primary download location for ASF dependencies. + (markt) + + + Improvements to Chinese translations contributed by syseal, wolibo, + ZhangJieWen and DigitalFatCat. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations contributed by tak7iji. (markt) + + + Improvements to Korean translations. (woonsan) + + + +
    +
    + + + + Correct a regression in the Java 8 to Java 11 changes made in 10.1.0-M3 + that caused all WebSocket end points to fail to register. (markt) + + + +
    +
    + + + + Update the minimum required Java version to Java 11. (markt) + + + + + + + Incremented the supported Jakarta Servlet version to 6.0 to align with + the current development branch of the Jakarta Servlet specification. + Plans have changed and the next iteration of the Servlet specification + will be 6.0 rather than 5.1. (markt) + + + 65411: Always close the connection when an uncaught + NamingException occurs to avoid connection locking. + Submitted by Ole Ostergaard. (remm) + + + 65433: Correct a regression in the fix for 65397 + where a StringIndexOutOfBoundsException could be triggered + if the canonical path of the target of a symlink was shorter than the + canonical path of the directory in which the symlink had been created. + Patch provided by Cedomir Igaly. (markt) + + + 65443: Refactor the CorsFilter to make it easier + to extend. (markt) + + + To avoid unnecessary cache revalidation, do not add an HTTP + Expires header when setting adding an HTTP header of + CacheControl: private. (markt) + + + Refactor JULI's custom LogManager, the + web application class loader implementation, the web resources + implementation, the JreLeakPreventionListener + implementation and the StandardJarScanner implementation to + remove Java 8 specific code now that the minimum Java version has been + increased to 11. (markt) + + + Remove all references to the endorsed standards override feature and the + specifying of optional packages (extensions) in the manifest as these + are not supported in Java 11. (markt) + + + + + + + When writing an HTTP/2 response via sendfile (only enabled when + useAsyncIO is true) the connection flow control window was + sometimes ignored leading to various error conditions. sendfile now + checks both the stream and connection flow control windows before + writing. (markt) + + + Add debug logging for writing an HTTP/2 response via sendfile. (markt) + + + Correct bugs in the HTTP/2 connection flow control management that meant + it was possible for a connection to stall waiting for a connection flow + control window update that had already arrived. Any streams on that + connection that were trying to write when this happened would time out. + (markt) + + + 65448: When using TLS with NIO, it was possible for a + blocking response write to hang just before the final TLS packet + associated with the response until the connection timed out at which + point the final packet would be sent and the connection closed. (markt) + + + 65454: Fix a race condition that could result in a delay to + a new request. The new request could be queued to wait for an existing + request to finish processing rather than the thread pool creating a new + thread to process the new request. (markt) + + + 65460: Correct a regression introduced in the previous + release in the change to reduce the number of small HTTP/2 window + updates sent for streams. A logic error meant that small window updates + for the connection were dropped. This meant that the connection flow + window slowly reduced over time until nothing could be sent. (markt) + + + Remove NIO workarounds and code that is no longer needed with Java 11. + (remm) + + + Refactor the endpoints to remove Java 8 specific code now that the + minimum Java version has been increased to 11. (markt) + + + + + + + Add additional generics to the EL API to align with the latest changes + in the EL specification project. (markt) + + + Enable EL lambda expressions to be coerced to functional interfaces. + This is an implementation of a proposed extension to the Jakarta + Expression Language specification. (markt) + + + Refactor the EL API and implementation to remove Java 8 specific code + now that the minimum Java version has been increased to 11. (markt) + + + + + + + Refactor the WebSocket implementation to remove Java 8 specific code now + that the minimum Java version has been increased to 11. (markt) + + + + + + + 65404: Correct a regression in the fix for 63362 + that caused the server status page in the Manager web application to be + truncated if HTTP upgrade was used such as when starting a WebSocket + connection. (markt) + + + + + + + Improvements to Chinese translations contributed by ZhangJieWen and + chengzheyan. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations contributed by tak7iji. (markt) + + + Improvements to Korean translations. (woonsan) + + + Use of GraalVM native images no longer automatically disables JMX + support. JMX support may still be disabled by calling + org.apache.tomcat.util.modeler.Registry.disableRegistry(). + (markt) + + + +
    +
    + + + + Refactor the RemoteIpValve to use the common utility method + for list to comma separated string conversion. (markt) + + + Refactor JNDIRealm$JNDIConnection so its fields are + accessible to sub-classes of JNDIRealm. (markt) + + + Fix serialization warnings in UserDatabasePrincipal + reported by SpotBugs. (markt) + + + 65397: Calls to + ServletContext.getResourcePaths() no longer include + symbolic links in the results unless allowLinking has been + set to true. If a resource is skipped because of this + change, a warning will be logged as this typically indicates a + configuration issue. (markt) + + + + + + + 65368: Improve handling of clean closes of inbound TLS + connections. Treat them the same way as clean closes of non-TLS + connections rather than as unknown errors. (markt) + + + Modify the HTTP/2 connector not to sent small updates for stream flow + control windows to the user agent as, depending on how the user agent is + written, this may trigger small writes from the user agent that in turn + trigger the overhead protection. Small updates for stream flow control + windows are now combined with subsequent flow control window updates for + that stream to ensure that all stream flow control window updates sent + from Tomcat are larger than overheadWindowUpdateThreshold. + (markt) + + + Add additional debug logging to track the current state of the HTTP/2 + overhead count that Tomcat uses to detect and close potentially + malicious connections. (markt) + + + Many HTTP/2 requests from browsers will trigger one overhead frame and + one non-overhead frame. Change the overhead calculation so that a + non-overhead frame reduces the current overhead count by 2 rather than + 1. This means that, over time, the overhead count for a well-behaved + connection will trend downwards. (markt) + + + Change the initial HTTP/2 overhead count from -10 to + -10 * overheadCountFactor. This means that, regardless of + the value chosen for overheadCountFactor, when a connection + opens 10 overhead frames in a row will be required to trigger the + overhead protection. (markt) + + + Increase the default overheadCountFactor from + 1 to 10 and change the reduction in overhead + count for a non-overhead frame from -2 to -20. + This allows for a larger range (0-20) to be used for + overheadCountFactor providing for finer-grained control. + (markt) + + + Modify the parsing of HTTP header values that use the + 1#token to ignore empty elements as per RFC 7230 section 7 + instead of treating the presence of empty elements as an error. (markt) + + + Expand the unit tests for HttpServlet.doHead() and correct + the flushing of the response buffer. The buffer used to behave as if it + was one byte smaller than the configured size. The buffer was flushed + (and the response committed if required) when the buffer was full. The + buffer is now flushed (and the response committed if required) if the + buffer is full and there is more data to write. (markt) + + + Fix an issue where concurrent HTTP/2 writes (or concurrent reads) to the + same connection could hang and eventually timeout when async IO was + enabled (it is enabled by default). (markt) + + + + + + + 65387: Correct a regression in the fix for 65124 + and restore the local definition of out for tags that + implement TryCatchFinally. (markt) + + + 65390: Correct a regression in the fix for 65124 + and restore code that was removed in error leading to JSP compilation + failures in some circumstances. (markt) + + + Update to the Eclipse JDT compiler 4.20. (markt) + + + Add support for specifying Java 17 (with the value 17) as + the compiler source and/or compiler target for JSP compilation. If used + with an Eclipse JDT compiler version that does not support these values, + a warning will be logged and the latest supported version will used. + (markt) + + + 65377: Update the Java code generation for JSPs not to use + the boxed primitive constructors as they have been deprecated in Java 9 + and marked for future removal in Java 16. valueOf() is now + used instead. (markt) + + + + + + + Refactor the DigestAuthenticator to reuse a shared + SecureRandom instance rather than create a new one to + generate the cnonce if required. (markt) + + + + + + + 65385: Correct the link in the documentation web application + the Maven Central repository. (markt) + + + + + + + Use JSign to integrate the build script with the code signing service to + enable release builds to be created on Linux as well as Windows. (markt) + + + Update the OWB module to Apache OpenWebBeans 2.0.23. (remm) + + + Update the CXF module to Apache CXF 3.4.4. (remm) + + + 65369 / 422: Add the additional + --add-opens=... options required for running Tomcat on Java + 16 onwards to the service.bat script to align it with the + other start-up scripts. PR provided by MCMicS. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Korean translations. (woonsan) + + + Update JUnit to version 4.13.2. (markt) + + + Update EasyMock to 4.3. (markt) + + + Update Objenesis to 3.2. (markt) + + + Update UnboundID to 6.0.0. (markt) + + + Update CheckStyle to 8.43. (markt) + + + Update SpotBugs to 4.2.3. (markt) + + + Update OSGi annotations to 1.1.0. (markt) + + + +
    +
    + + + + This release contains all of the changes up to and including those in + Apache Tomcat 10.0.6 plus the additional changes listed below. (markt) + + + Remove code previously marked for removal in Tomcat 10.1.x. (markt) + + + + + + + Incremented the supported Jakarta Servlet version to 5.1 to align with + the current development branch of the Jakarta Servlet specification. + (markt) + + + 65301: RemoteIpValve will now avoid getting + the local host name when it is not needed. (remm) + + + 65308: NPE in JNDIRealm when no userRoleAttribute + is given. (fschumacher) + + + 412: Add commented out, sample users for the Tomcat Manager app + to the default tomcat-users.xml file. Based on a PR by + Arnaud Dagnelies. (markt) + + + 418: Add a new option, pass-through, to the + default servlet's useBomIfPresent initialization parameter + that causes the default servlet to leave any BOM in place when + processing a static file and not to use the BOM to determine the + encoding of the file. Based on a pull request by Jean-Louis Monteiro. + (markt) + + + 419: When processing POST requests of type + multipart/form-data for parts without a filename that are + added to the parameter map in String form, check the size of the part + before attempting conversion to String. Pull request provided by + tianshuang. (markt) + + + Implement the new Cookie methods + setAttribute(), getAttribute() and + getAttributes() introduced in Servlet 6.0. (markt) + + + AprLifecycleListener does not show dev version suffix for libtcnative + and libapr. (michaelo) + + + Refactor principal handling in UserDatabaseRealm using + an inner class that extends GenericPrincipal. (remm) + + + Enable the default doHead() implementation in + HttpServlet to correctly handle responses where the content + length needs to be represented as a long since it is larger than the + maximum value that can be represented by an int. (markt) + + + Avoid synchronization on roles verification for the memory + UserDatabase. (remm) + + + Fix the default doHead() implementation in + HttpServlet to correctly handle responses where the Servlet + calls ServletResponse.reset() and/or + ServletResponse.resetBuffer(). (markt) + + + Fix the default doHead() implementation in + HttpServlet to correctly handle responses generated using + the Servlet non-blocking API. (markt) + + + + + + + 65303: Fix a possible NullPointerException if + an error occurs on an HTTP/1.1 connection being upgraded to HTTP/2 or on + a pushed HTTP/2 stream. (markt) + + + Simplify AprEndpoint socket bind for all platforms. (michaelo) + + + 65340: Add missing check for a negative return value for + Hpack.decodeInteger in the HpackDecoder, + which could cause a NegativeArraySizeException exception. + Submitted by Thomas, and verified the fix is present in the donated + hpack code in a further update. (remm) + + + Add debug logging for HTTP/2 HPACK header decoding. (markt) + + + Correct parsing of HTTP headers consisting of a list of tokens so that a + header with an empty token is treated consistently regardless of whether + the empty token is at the start, middle or end of the list of tokens. + (markt) + + + Remove support for the identity transfer encoding. The + inclusion of this encoding in RFC 2616 was an error that was corrected + in 2001. Requests using this transfer encoding will now receive a 501 + response. (markt) + + + Process transfer encoding headers from both HTTP 1.0 and HTTP 1.1 + clients. (markt) + + + Ensure that if the transfer encoding header contains the + chunked, that the chunked encoding is the + final encoding listed. (markt) + + + + + + + Incremented the supported Jakarta Expression Language version to 5.0 to + align with the current development branch of the Jakarta Expression + Language specification. (markt) + + + Review code used to generate Java source from JSPs and tags and remove + code found to be unnecessary. (markt) + + + Refactor use of internal ChildInfo class to use compile + time type checking rather than run time type checking. (markt) + + + 65124: Partial fix. When generating Java source code to call + a tag handler, only define the local variable JspWriter out + when it is going to be used. (markt) + + + Add generics to the EL 5.0 API to align with the current EL 5.0 + development branch. (markt) + + + Update the web-fragment.xml included in + jasper.jar and jasper-el.jar to use the + Servlet 5.0 schema. (markt) + + + Update JspC to generate web.xml and + web-fragment.xml files using Servlet 5.0 schemas. (markt) + + + Remove the deprecated method + MethodExpression.isParmetersProvided() from the EL API to + align with the current EL 5.0 development branch. (markt) + + + 65358: Improve expression language method matching for + methods with varargs. Where multiple methods may match the provided + parameters, the method that requires the fewest varargs is preferred. + (markt) + + + 65332: Add a commented out section in + catalina.policy that provides the necessary permissions to + compile JSPs with javac when running on Java 9 onwards with a security + manager. It is commented out as it will cause errors if used with + earlier Java versions. (markt) + + + + + + + 65317: When using permessage-deflate, the + WebSocket connection was incorrectly closed if the uncompressed payload + size was an exact multiple of 8192. Based on a patch provided by Saksham + Verma. (markt) + + + Update the web-fragment.xml included in + tomcat-websocket.jar to use the Servlet 5.0 schema. (markt) + + + 65342: Correct a regression introduced with the fix for + 65262 that meant Tomcat's WebSocket implementation would only + work with Tomcat's implementation of the Jakarta WebSocket API. (markt) + + + + + + + Improve the description of the maxConnections and + acceptCount attributes in the Connector section of the + documentation web application. (markt) + + + + + + + Improvements to French translations. (remm) + + + Improvements to Korean translations. (woonsan) + + + 65362: Correct a regression in the previous release. The + change to create OSGi Require-Capability sections in + manifests for Jakarta API JARs manually rather than with bnd annotations + did not add the necessary manual entries to the embedded JARs. (markt) + + + Update the packaged version of the Tomcat Native Library to 1.2.30. Also + update the minimum recommended version to 1.2.30. (markt) + + + +
    + +
    + diff --git a/webapps/docs/class-loader-howto.xml b/webapps/docs/class-loader-howto.xml new file mode 100644 index 0000000..92fcc6e --- /dev/null +++ b/webapps/docs/class-loader-howto.xml @@ -0,0 +1,294 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Yoav Shapira + Class Loader How-To + + + + +
    + +
    + +
    + +

    Like many server applications, Tomcat installs a variety of class loaders +(that is, classes that implement java.lang.ClassLoader) to allow +different portions of the container, and the web applications running on the +container, to have access to different repositories of available classes and +resources. This mechanism is used to provide the functionality defined in the +Servlet Specification, version 2.4 — in particular, Sections 9.4 +and 9.6.

    + +

    In a Java environment, class loaders are +arranged in a parent-child tree. Normally, when a class loader is asked to +load a particular class or resource, it delegates the request to a parent +class loader first, and then looks in its own repositories only if the parent +class loader(s) cannot find the requested class or resource. Note, that the +model for web application class loaders differs slightly from this, +as discussed below, but the main principles are the same.

    + +

    When Tomcat is started, it creates a set of class loaders that are +organized into the following parent-child relationships, where the parent +class loader is above the child class loader:

    + + Bootstrap + | + System + | + Common + / \ + Webapp1 Webapp2 ... + +

    The characteristics of each of these class loaders, including the source +of classes and resources that they make visible, are discussed in detail in +the following section.

    + +
    + +
    + +

    As indicated in the diagram above, Tomcat creates the following class +loaders as it is initialized:

    +
      +
    • Bootstrap — This class loader contains the basic + runtime classes provided by the Java Virtual Machine, plus any classes from + JAR files present in the System Extensions directory + ($JAVA_HOME/jre/lib/ext). Note: some JVMs may + implement this as more than one class loader, or it may not be visible + (as a class loader) at all.

    • +
    • System — This class loader is normally initialized + from the contents of the CLASSPATH environment variable. All + such classes are visible to both Tomcat internal classes, and to web + applications. However, the standard Tomcat startup scripts + ($CATALINA_HOME/bin/catalina.sh or + %CATALINA_HOME%\bin\catalina.bat) totally ignore the contents + of the CLASSPATH environment variable itself, and instead + build the System class loader from the following repositories: +

      +
        +
      • $CATALINA_HOME/bin/bootstrap.jar — Contains the + main() method that is used to initialize the Tomcat server, and the + class loader implementation classes it depends on.

      • +
      • $CATALINA_BASE/bin/tomcat-juli.jar or + $CATALINA_HOME/bin/tomcat-juli.jar — Logging + implementation classes. These include enhancement classes to + java.util.logging API, known as Tomcat JULI, + and a package-renamed copy of Apache Commons Logging library + used internally by Tomcat. + See logging documentation for more + details.

        +

        If tomcat-juli.jar is present in + $CATALINA_BASE/bin, it is used instead of the one in + $CATALINA_HOME/bin. It is useful in certain logging + configurations

      • +
      • $CATALINA_HOME/bin/commons-daemon.jar — The classes + from Apache Commons + Daemon project. + This JAR file is not present in the CLASSPATH built by + catalina.bat|.sh scripts, but is referenced + from the manifest file of bootstrap.jar.

      • +
      +
    • +
    • Common — This class loader contains additional + classes that are made visible to both Tomcat internal classes and to all + web applications.

      +

      Normally, application classes should NOT + be placed here. The locations searched by this class loader are defined by + the common.loader property in + $CATALINA_BASE/conf/catalina.properties. The default setting will search the + following locations in the order they are listed:

      +
        +
      • unpacked classes and resources in $CATALINA_BASE/lib
      • +
      • JAR files in $CATALINA_BASE/lib
      • +
      • unpacked classes and resources in $CATALINA_HOME/lib
      • +
      • JAR files in $CATALINA_HOME/lib
      • +
      +

      By default, this includes the following:

      +
        +
      • annotations-api.jar — Jakarta Annotations 2.1.1 classes. +
      • +
      • catalina.jar — Implementation of the Catalina servlet + container portion of Tomcat.
      • +
      • catalina-ant.jar — Optional. Tomcat Catalina Ant tasks + for working with the Manager web application.
      • +
      • catalina-ha.jar — Optional. High availability package + that provides session clustering functionality built on Tribes.
      • +
      • catalina-ssi.jar — Optional. Server-side Includes module. +
      • +
      • catalina-storeconfig.jar — Optional. Generation of XML + configuration files from current state.
      • +
      • catalina-tribes.jar — Optional. Group communication + package used by the high availabaility package.
      • +
      • ecj-*.jar — Optional. Eclipse JDT Java compiler used to + compile JSPs to Servlets.
      • +
      • el-api.jar — Optional. EL 5.0 API.
      • +
      • jakartaee-migration-*-shaded.jar — Optional. Provides + conversion of web applications from Java EE 8 to Jakarta EE 9.
      • +
      • jasper.jar — Optional. Tomcat Jasper JSP Compiler and + Runtime.
      • +
      • jasper-el.jar — Optional. Tomcat EL implementation.
      • +
      • jaspic-api.jar — Jakarta Authentication 3.0 API.
      • +
      • jsp-api.jar — Optional. Jakarta Pages 3.1 API.
      • +
      • servlet-api.jar — Jakarta Servlet 6.0 API.
      • +
      • tomcat-api.jar — Several interfaces defined by Tomcat. +
      • +
      • tomcat-coyote.jar — Tomcat connectors and utility + classes.
      • +
      • tomcat-dbcp.jar — Optional. Database connection pool + implementation based on package-renamed copy of Apache Commons Pool 2 + and Apache Commons DBCP 2.
      • +
      • tomcat-i18n-**.jar — Optional JARs containing resource + bundles for other languages. As default bundles are also included in + each individual JAR, they can be safely removed if no + internationalization of messages is needed.
      • +
      • tomcat-jdbc.jar — Optional. An alternative database + connection pool implementation, known as Tomcat JDBC pool. See + documentation for more details.
      • +
      • tomcat-jni.jar — Provides the integration with the Tomcat + Native library.
      • +
      • tomcat-util.jar — Common classes used by various + components of Apache Tomcat.
      • +
      • tomcat-util-scan.jar — Provides the class scanning + functionality used by Tomcat.
      • +
      • tomcat-websocket.jar — Optional. Jakarta WebSocket 2.1 + implementation
      • +
      • websocket-api.jar — Optional. Jakarta WebSocket 2.1 API +
      • +
      • websocket-client-api.jar — Optional. Jakarta WebSocket + 2.1 Client API
      • +
    • +
    • WebappX — A class loader is created for each web + application that is deployed in a single Tomcat instance. All unpacked + classes and resources in the /WEB-INF/classes directory of + your web application, plus classes and resources in JAR files + under the /WEB-INF/lib directory of your web application, + are made visible to this web application, but not to other ones.

    • +
    + +

    As mentioned above, the web application class loader diverges from the +default Java delegation model (in accordance with the recommendations in the +Servlet Specification, version 2.4, section 9.7.2 Web Application Classloader). +When a request to load a +class from the web application's WebappX class loader is processed, +this class loader will look in the local repositories first, +instead of delegating before looking. There are exceptions. Classes which are +part of the JRE base classes cannot be overridden. There are some exceptions +such as the XML parser components which can be overridden using the upgradeable +modules feature. +Lastly, the web application class loader will always delegate first for Jakarta +EE API classes for the specifications implemented by Tomcat +(Servlet, JSP, EL, WebSocket). All other class loaders in Tomcat follow the +usual delegation pattern.

    + +

    Therefore, from the perspective of a web application, class or resource +loading looks in the following repositories, in this order:

    +
      +
    • Bootstrap classes of your JVM
    • +
    • /WEB-INF/classes of your web application
    • +
    • /WEB-INF/lib/*.jar of your web application
    • +
    • System class loader classes (described above)
    • +
    • Common class loader classes (described above)
    • +
    + +

    If the web application class loader is +configured with +<Loader delegate="true"/> +then the order becomes:

    +
      +
    • Bootstrap classes of your JVM
    • +
    • System class loader classes (described above)
    • +
    • Common class loader classes (described above)
    • +
    • /WEB-INF/classes of your web application
    • +
    • /WEB-INF/lib/*.jar of your web application
    • +
    + +
    + + +
    + +

    In older versions of Tomcat, you could simply replace the XML parser +in the Tomcat libraries directory to change the parser +used by all web applications. However, this technique will not be effective +when you are running modern versions of Java, because the usual class loader +delegation process will always choose the implementation inside the JDK in +preference to this one.

    + +

    Java supports a mechanism called upgradeable modules to allow replacement +of APIs created outside of the JCP (i.e. DOM and SAX from W3C). It can also be +used to update the XML parser implementation.

    + +

    Note that overriding any JRE component carries risk. If the overriding +component does not provide a 100% compatible API (e.g. the API provided by +Xerces is not 100% compatible with the XML API provided by the JRE) then there +is a risk that Tomcat and/or the deployed application will experience errors.

    + +
    + + +
    + +

    When running under a security manager the locations from which classes +are permitted to be loaded will also depend on the contents of your policy +file. See Security Manager How-To +for further information.

    + +
    + +
    + +

    A more complex class loader hierarchy may also be configured. See the diagram +below. By default, the Server and Shared +class loaders are not defined and the simplified hierarchy shown above is used. +This more complex hierarchy may be use by defining values for the +server.loader and/or shared.loader properties in +conf/catalina.properties.

    + + + Bootstrap + | + System + | + Common + / \ +Server Shared + / \ + Webapp1 Webapp2 ... + +

    The Server class loader is only visible to Tomcat internals +and is completely invisible to web applications.

    + +

    The Shared class loader is visible to all web applications +and may be used to shared code across all web applications. However, any updates +to this shared code will require a Tomcat restart.

    + +
    + + + +
    diff --git a/webapps/docs/cluster-howto.xml b/webapps/docs/cluster-howto.xml new file mode 100644 index 0000000..e89cdb6 --- /dev/null +++ b/webapps/docs/cluster-howto.xml @@ -0,0 +1,707 @@ + + + +]> + + + &project; + + + Filip Hanik + Peter Rossbach + Clustering/Session Replication How-To + + + + +
    +

    You can also check the configuration reference documentation. +

    +
    + +
    + +
    + +
    +

    + Simply add +

    + <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/> +

    + to your <Engine> or your <Host> element to enable clustering. +

    +

    + Using the above configuration will enable all-to-all session replication + using the DeltaManager to replicate session deltas. By all-to-all, we mean that every + session gets replicated to all the other nodes in the cluster. + This works great for smaller clusters, but we don't recommend it for larger clusters — more than 4 nodes or so. + Also, when using the DeltaManager, Tomcat will replicate sessions to all nodes, + even nodes that don't have the application deployed.
    + To get around these problem, you'll want to use the BackupManager. The BackupManager + only replicates the session data to one backup node, and only to nodes that have the application deployed. + Once you have a simple cluster running with the DeltaManager, you will probably want to + migrate to the BackupManager as you increase the number of nodes in your cluster. +

    +

    + Here are some of the important default values: +

    +
      +
    1. Multicast address is 228.0.0.4
    2. +
    3. Multicast port is 45564 (the port and the address together determine cluster membership.
    4. +
    5. The IP broadcasted is java.net.InetAddress.getLocalHost().getHostAddress() (make sure you don't broadcast 127.0.0.1, this is a common error)
    6. +
    7. The TCP port listening for replication messages is the first available server socket in range 4000-4100
    8. +
    9. Listener is configured ClusterSessionListener
    10. +
    11. Two interceptors are configured TcpFailureDetector and MessageDispatchInterceptor
    12. +
    +

    + The following is the default cluster configuration: +

    + + + + + + + + + + + + + + + + + + + + + + ]]> +

    We will cover this section in more detail later in this document.

    +
    + +
    + +

    The cluster implementation is written on the basis that a secure, trusted +network is used for all of the cluster related network traffic. It is not safe +to run a cluster on a insecure, untrusted network.

    + +

    There are many options for providing a secure, trusted network for use by a +Tomcat cluster. These include:

    +
      +
    • private LAN
    • +
    • a Virtual Private Network (VPN)
    • +
    • IPSEC
    • +
    + +

    The EncryptInterceptor +provides confidentiality and integrity protection but it does not protect +against all risks associated with running a Tomcat cluster on an untrusted +network, particularly DoS attacks.

    + +
    + +
    + +

    To run session replication in your Tomcat container, the following steps +should be completed:

    +
      +
    • All your session attributes must implement java.io.Serializable
    • +
    • Uncomment the Cluster element in server.xml
    • +
    • If you have defined custom cluster valves, make sure you have the ReplicationValve defined as well under the Cluster element in server.xml
    • +
    • If your Tomcat instances are running on the same machine, make sure the Receiver.port + attribute is unique for each instance, in most cases Tomcat is smart enough to resolve this on it's own by autodetecting available ports in the range 4000-4100
    • +
    • Make sure your web.xml has the + <distributable/> element
    • +
    • If you are using mod_jk, make sure that jvmRoute attribute is set at your Engine <Engine name="Catalina" jvmRoute="node01" > + and that the jvmRoute attribute value matches your worker name in workers.properties
    • +
    • Make sure that all nodes have the same time and sync with NTP service!
    • +
    • Make sure that your loadbalancer is configured for sticky session mode.
    • +
    +

    Load balancing can be achieved through many techniques, as seen in the +Load Balancing chapter.

    +

    Note: Remember that your session state is tracked by a cookie, so your URL must look the same from the out + side otherwise, a new session will be created.

    +

    The Cluster module uses the Tomcat JULI logging framework, so you can configure logging + through the regular logging.properties file. To track messages, you can enable logging on the key: org.apache.catalina.tribes.MESSAGES

    +
    + + +
    + +

    To enable session replication in Tomcat, three different paths can be followed to achieve the exact same thing:

    +
      +
    1. Using session persistence, and saving the session to a shared file system (PersistenceManager + FileStore)
    2. +
    3. Using session persistence, and saving the session to a shared database (PersistenceManager + JDBCStore)
    4. +
    5. Using in-memory-replication, using the SimpleTcpCluster that ships with Tomcat (lib/catalina-tribes.jar + lib/catalina-ha.jar)
    6. +
    + +

    Tomcat can perform an all-to-all replication of session state using the DeltaManager or + perform backup replication to only one node using the BackupManager. + The all-to-all replication is an algorithm that is only efficient when the clusters are small. For larger clusters, you + should use the BackupManager to use a primary-secondary session replication strategy where the session will only be + stored at one backup node.
    + + Currently you can use the domain worker attribute (mod_jk > 1.2.8) to build cluster partitions + with the potential of having a more scalable cluster solution with the DeltaManager + (you'll need to configure the domain interceptor for this). + In order to keep the network traffic down in an all-to-all environment, you can split your cluster + into smaller groups. This can be easily achieved by using different multicast addresses for the different groups. + A very simple setup would look like this: +

    + + DNS Round Robin + | + Load Balancer + / \ + Cluster1 Cluster2 + / \ / \ + Tomcat1 Tomcat2 Tomcat3 Tomcat4 + +

    What is important to mention here, is that session replication is only the beginning of clustering. + Another popular concept used to implement clusters is farming, i.e., you deploy your apps only to one + server, and the cluster will distribute the deployments across the entire cluster. + This is all capabilities that can go into with the FarmWarDeployer (s. cluster example at server.xml)

    +

    In the next section will go deeper into how session replication works and how to configure it.

    + +
    + +
    +

    Membership is established using multicast heartbeats. + Hence, if you wish to subdivide your clusters, you can do this by + changing the multicast IP address or port in the <Membership> element. +

    +

    + The heartbeat contains the IP address of the Tomcat node and the TCP port that + Tomcat listens to for replication traffic. All data communication happens over TCP. +

    +

    + The ReplicationValve is used to find out when the request has been completed and initiate the + replication, if any. Data is only replicated if the session has changed (by calling setAttribute or removeAttribute + on the session). +

    +

    + One of the most important performance considerations is the synchronous versus asynchronous replication. + In a synchronous replication mode the request doesn't return until the replicated session has been + sent over the wire and reinstantiated on all the other cluster nodes. + Synchronous vs. asynchronous is configured using the channelSendOptions + flag and is an integer value. The default value for the SimpleTcpCluster/DeltaManager combo is + 8, which is asynchronous. + See the configuration reference + for more discussion on the various channelSendOptions values. +

    +

    + For convenience, channelSendOptions can be set by name(s) rather than integer, + which are then translated to their integer value upon startup. The valid option names are: + "asynchronous" (alias "async"), "byte_message" (alias "byte"), "multicast", "secure", + "synchronized_ack" (alias "sync"), "udp", "use_ack". Use comma to separate multiple names, + e.g. pass "async, multicast" for the options + SEND_OPTIONS_ASYNCHRONOUS | SEND_OPTIONS_MULTICAST. +

    +

    + You can read more on the send flag(overview) or the + send flag(javadoc). + During async replication, the request is returned before the data has been replicated. async + replication yields shorter request times, and synchronous replication guarantees the session + to be replicated before the request returns. +

    +
    + +
    +

    + If you are using mod_jk and not using sticky sessions or for some reasons sticky session don't + work, or you are simply failing over, the session id will need to be modified as it previously contained + the worker id of the previous tomcat (as defined by jvmRoute in the Engine element). + To solve this, we will use the JvmRouteBinderValve. +

    +

    + The JvmRouteBinderValve rewrites the session id to ensure that the next request will remain sticky + (and not fall back to go to random nodes since the worker is no longer available) after a fail over. + The valve rewrites the JSESSIONID value in the cookie with the same name. + Not having this valve in place, will make it harder to ensure stickiness in case of a failure for the mod_jk module. +

    +

    + Remember, if you are adding your own valves in server.xml then the defaults are no longer valid, + make sure that you add in all the appropriate valves as defined by the default. +

    +

    + Hint:
    + With attribute sessionIdAttribute you can change the request attribute name that included the old session id. + Default attribute name is org.apache.catalina.ha.session.JvmRouteOriginalSessionID. +

    +

    + Trick:
    + You can enable this mod_jk turnover mode via JMX before you drop a node to all backup nodes! + Set enable true on all JvmRouteBinderValve backups, disable worker at mod_jk + and then drop node and restart it! Then enable mod_jk Worker and disable JvmRouteBinderValves again. + This use case means that only requested session are migrated. +

    + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + ]]> +

    + Break it down!! +

    + ]]> +

    + The main element, inside this element all cluster details can be configured. + The channelSendOptions is the flag that is attached to each message sent by the + SimpleTcpCluster class or any objects that are invoking the SimpleTcpCluster.send method. + The description of the send flags is available at + our javadoc site + The DeltaManager sends information using the SimpleTcpCluster.send method, while the backup manager + sends it itself directly through the channel. +
    For more info, Please visit the reference documentation +

    + + ]]> +

    + This is a template for the manager configuration that will be used if no manager is defined in the <Context> + element. In Tomcat 5.x each webapp marked distributable had to use the same manager, this is no longer the case + since Tomcat you can define a manager class for each webapp, so that you can mix managers in your cluster. + Obviously the managers on one node's application has to correspond with the same manager on the same application on the other node. + If no manager has been specified for the webapp, and the webapp is marked <distributable/> Tomcat will take this manager configuration + and create a manager instance cloning this configuration. +
    For more info, Please visit the reference documentation +

    + ]]> +

    + The channel element is Tribes, the group communication framework + used inside Tomcat. This element encapsulates everything that has to do with communication and membership logic. +
    For more info, Please visit the reference documentation +

    + ]]> +

    + Membership is done using multicasting. Please note that Tribes also supports static memberships using the + StaticMembershipInterceptor if you want to extend your membership to points beyond multicasting. + The address attribute is the multicast address used and the port is the multicast port. These two together + create the cluster separation. If you want a QA cluster and a production cluster, the easiest config is to + have the QA cluster be on a separate multicast address/port combination than the production cluster.
    + The membership component broadcasts TCP address/port of itself to the other nodes so that communication between + nodes can be done over TCP. Please note that the address being broadcasted is the one of the + Receiver.address attribute. +
    For more info, Please visit the reference documentation +

    + ]]> +

    + In tribes the logic of sending and receiving data has been broken into two functional components. The Receiver, as the name suggests + is responsible for receiving messages. Since the Tribes stack is thread less, (a popular improvement now adopted by other frameworks as well), + there is a thread pool in this component that has a maxThreads and minThreads setting.
    + The address attribute is the host address that will be broadcasted by the membership component to the other nodes. +
    For more info, Please visit the reference documentation +

    + + + ]]> +

    + The sender component, as the name indicates is responsible for sending messages to other nodes. + The sender has a shell component, the ReplicationTransmitter but the real stuff done is done in the + sub component, Transport. + Tribes support having a pool of senders, so that messages can be sent in parallel and if using the NIO sender, + you can send messages concurrently as well.
    + Concurrently means one message to multiple senders at the same time and Parallel means multiple messages to multiple senders + at the same time. +
    For more info, Please visit the reference documentation +

    + + + + ]]> +

    + Tribes uses a stack to send messages through. Each element in the stack is called an interceptor, and works much like the valves do + in the Tomcat servlet container. + Using interceptors, logic can be broken into more manageable pieces of code. The interceptors configured above are:
    + TcpFailureDetector - verifies crashed members through TCP, if multicast packets get dropped, this interceptor protects against false positives, + ie the node marked as crashed even though it still is alive and running.
    + MessageDispatchInterceptor - dispatches messages to a thread (thread pool) to send message asynchronously.
    + ThroughputInterceptor - prints out simple stats on message traffic.
    + Please note that the order of interceptors is important. The way they are defined in server.xml is the way they are represented in the + channel stack. Think of it as a linked list, with the head being the first most interceptor and the tail the last. +
    For more info, Please visit the reference documentation +

    + ]]> +

    + The cluster uses valves to track requests to web applications, we've mentioned the ReplicationValve and the JvmRouteBinderValve above. + The <Cluster> element itself is not part of the pipeline in Tomcat, instead the cluster adds the valve to its parent container. + If the <Cluster> elements is configured in the <Engine> element, the valves get added to the engine and so on. +
    For more info, Please visit the reference documentation +

    + ]]> +

    + The default tomcat cluster supports farmed deployment, ie, the cluster can deploy and undeploy applications on the other nodes. + The state of this component is currently in flux but will be addressed soon. There was a change in the deployment algorithm + between Tomcat 5.0 and 5.5 and at that point, the logic of this component changed to where the deploy dir has to match the + webapps directory. +
    For more info, Please visit the reference documentation +

    + + ]]> +

    + Since the SimpleTcpCluster itself is a sender and receiver of the Channel object, components can register themselves as listeners to + the SimpleTcpCluster. The listener above ClusterSessionListener listens for DeltaManager replication messages + and applies the deltas to the manager that in turn applies it to the session. +
    For more info, Please visit the reference documentation +

    + +
    + +
    + +

    Component Levels:

    + Server + | + Service + | + Engine + | \ + | --- Cluster --* + | + Host + | + ------ + / \ + Cluster Context(1-N) + | \ + | -- Manager + | \ + | -- DeltaManager + | -- BackupManager + | + --------------------------- + | \ + Channel \ + ----------------------------- \ + | \ + Interceptor_1 .. \ + | \ + Interceptor_N \ + ----------------------------- \ + | | | \ + Receiver Sender Membership \ + -- Valve + | \ + | -- ReplicationValve + | -- JvmRouteBinderValve + | + -- LifecycleListener + | + -- ClusterListener + | \ + | -- ClusterSessionListener + | + -- Deployer + \ + -- FarmWarDeployer + + + + +
    +
    +

    To make it easy to understand how clustering works, we are gonna to take you through a series of scenarios. + In this scenario we only plan to use two tomcat instances TomcatA and TomcatB. + We will cover the following sequence of events:

    + +
      +
    1. TomcatA starts up
    2. +
    3. TomcatB starts up (Wait the TomcatA start is complete)
    4. +
    5. TomcatA receives a request, a session S1 is created.
    6. +
    7. TomcatA crashes
    8. +
    9. TomcatB receives a request for session S1
    10. +
    11. TomcatA starts up
    12. +
    13. TomcatA receives a request, invalidate is called on the session (S1)
    14. +
    15. TomcatB receives a request, for a new session (S2)
    16. +
    17. TomcatA The session S2 expires due to inactivity.
    18. +
    + +

    Ok, now that we have a good sequence, we will take you through exactly what happens in the session replication code

    + +
      +
    1. TomcatA starts up +

      + Tomcat starts up using the standard start up sequence. When the Host object is created, a cluster object is associated with it. + When the contexts are parsed, if the distributable element is in place in the web.xml file, + Tomcat asks the Cluster class (in this case SimpleTcpCluster) to create a manager + for the replicated context. So with clustering enabled, distributable set in web.xml + Tomcat will create a DeltaManager for that context instead of a StandardManager. + The cluster class will start up a membership service (multicast) and a replication service (tcp unicast). + More on the architecture further down in this document. +

      +
    2. +
    3. TomcatB starts up +

      + When TomcatB starts up, it follows the same sequence as TomcatA did with one exception. + The cluster is started and will establish a membership (TomcatA, TomcatB). + TomcatB will now request the session state from a server that already exists in the cluster, + in this case TomcatA. TomcatA responds to the request, and before TomcatB starts listening + for HTTP requests, the state has been transferred from TomcatA to TomcatB. + In case TomcatA doesn't respond, TomcatB will time out after 60 seconds, issue a log + entry, and continue starting. The session state gets transferred for each web + application that has distributable in its web.xml. (Note: To use session replication + efficiently, all your tomcat instances should be configured the same.) +

      +
    4. +
    5. TomcatA receives a request, a session S1 is created. +

      + The request coming in to TomcatA is handled exactly the same way as without session + replication, until the request is completed, at which time the + ReplicationValve will intercept the request before the response is + returned to the user. At this point it finds that the session has been modified, + and it uses TCP to replicate the session to TomcatB. Once the serialized data has + been handed off to the operating system's TCP logic, the request returns to the user, + back through the valve pipeline. For each request the entire session is replicated, + this allows code that modifies attributes in the session without calling setAttribute + or removeAttribute to be replicated. A useDirtyFlag configuration parameter can + be used to optimize the number of times a session is replicated. +

      + +
    6. +
    7. TomcatA crashes +

      + When TomcatA crashes, TomcatB receives a notification that TomcatA has dropped out + of the cluster. TomcatB removes TomcatA from its membership list, and TomcatA will + no longer be notified of any changes that occurs in TomcatB. The load balancer + will redirect the requests from TomcatA to TomcatB and all the sessions are current. +

      +
    8. +
    9. TomcatB receives a request for session S1 +

      Nothing exciting, TomcatB will process the request as any other request. +

      +
    10. +
    11. TomcatA starts up +

      Upon start up, before TomcatA starts taking new request and making itself + available to it will follow the start up sequence described above 1) 2). + It will join the cluster, contact TomcatB for the current state of all the sessions. + And once it receives the session state, it finishes loading and opens its HTTP/mod_jk ports. + So no requests will make it to TomcatA until it has received the session state from TomcatB. +

      +
    12. +
    13. TomcatA receives a request, invalidate is called on the session (S1) +

      The invalidate call is intercepted, and the session is queued with invalidated sessions. + When the request is complete, instead of sending out the session that has changed, it sends out + an "expire" message to TomcatB and TomcatB will invalidate the session as well. +

      + +
    14. +
    15. TomcatB receives a request, for a new session (S2) +

      Same scenario as in step 3) +

      + + +
    16. +
    17. TomcatA The session S2 expires due to inactivity. +

      The invalidate call is intercepted the same way as when a session is invalidated by the user, + and the session is queued with invalidated sessions. + At this point, the invalidated session will not be replicated across until + another request comes through the system and checks the invalid queue. +

      +
    18. +
    + +

    Phuuuhh! :)

    + +

    Membership + Clustering membership is established using very simple multicast pings. + Each Tomcat instance will periodically send out a multicast ping, + in the ping message the instance will broadcast its IP and TCP listen port + for replication. + If an instance has not received such a ping within a given timeframe, the + member is considered dead. Very simple, and very effective! + Of course, you need to enable multicasting on your system. +

    + +

    TCP Replication + Once a multicast ping has been received, the member is added to the cluster + Upon the next replication request, the sending instance will use the host and + port info and establish a TCP socket. Using this socket it sends over the serialized data. + The reason I chose TCP sockets is because it has built in flow control and guaranteed delivery. + So I know, when I send some data, it will make it there :) +

    + +

    Distributed locking and pages using frames + Tomcat does not keep session instances in sync across the cluster. + The implementation of such logic would be to much overhead and cause all + kinds of problems. If your client accesses the same session + simultaneously using multiple requests, then the last request + will override the other sessions in the cluster. +

    + +
    + + +
    +

    Monitoring is a very important question when you use a cluster. Some of the cluster objects are JMX MBeans

    +

    Add the following parameter to your startup script:

    +set CATALINA_OPTS=\ +-Dcom.sun.management.jmxremote \ +-Dcom.sun.management.jmxremote.port=%my.jmx.port% \ +-Dcom.sun.management.jmxremote.ssl=false \ +-Dcom.sun.management.jmxremote.authenticate=false + +

    + List of Cluster Mbeans +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionMBean ObjectName - EngineMBean ObjectName - Host
    ClusterThe complete cluster elementtype=Clustertype=Cluster,host=${HOST}
    DeltaManagerThis manager control the sessions and handle session replication type=Manager,context=${APP.CONTEXT.PATH}, host=${HOST}type=Manager,context=${APP.CONTEXT.PATH}, host=${HOST}
    FarmWarDeployerManages the process of deploying an application to all nodes in the clusterNot supportedtype=Cluster, host=${HOST}, component=deployer
    MemberRepresents a node in the clustertype=Cluster, component=member, name=${NODE_NAME}type=Cluster, host=${HOST}, component=member, name=${NODE_NAME}
    ReplicationValveThis valve control the replication to the backup nodestype=Valve,name=ReplicationValvetype=Valve,name=ReplicationValve,host=${HOST}
    JvmRouteBinderValveThis is a cluster fallback valve to change the Session ID to the current tomcat jvmroute.type=Valve,name=JvmRouteBinderValve, + context=${APP.CONTEXT.PATH}type=Valve,name=JvmRouteBinderValve,host=${HOST}, + context=${APP.CONTEXT.PATH}
    +
    + +
    +

    Please see the clustering section of the FAQ.

    +
    + + + +
    diff --git a/webapps/docs/comments.xml b/webapps/docs/comments.xml new file mode 100644 index 0000000..01ca65e --- /dev/null +++ b/webapps/docs/comments.xml @@ -0,0 +1,118 @@ + + + +]> + + + &project; + + + Documentation User Comments + + + + +
    + +

    The Tomcat documentation integrates the +Apache Comments System. +It allows users to add comments to most documentation pages. The comments +section can be found at the end of each page.

    + +
    + +
    + +

    Please use the Apache Comments System responsibly. We can only provide +this service to the community as long as it isn't misused.

    + +

    The comments are not for general Q&A. +Comments should be pointed towards suggestions on improving the documentation +or server. Questions on how to use Apache Tomcat should be directed +to our mailing lists.

    + +

    Comments may be removed by moderators if they are either +implemented or considered invalid/off-topic.

    + +

    HTML is not allowed in comments, and will just display as raw source code +if attempted. Links that do not point to an Apache site (*.apache.org) will +need approval by a moderator before the comment is visible to regular visitors.

    + +
    + +
    + +

    Any submitted comments must be contributed under the terms of the +Apache License, Version 2.0.

    + +
    + +
    + +

    Verified users gain the Apache feather next to their name, +and may post comments with links in them without requiring approval +by a moderator before the comments are shown. Being a verified user +in itself does not give you moderating rights. If you are interested +in becoming a verified user, please contact us on the +users mailing list.

    + +

    All ASF committers are automatically verified users.

    + +
    + +
    + +

    Moderators are allowed to mark comments as "Resolved", "Invalid" +or "Sticky", remove marks, approve comments e.g. if they contain +a link, and delete comments. Moderators can also subscribe to new +comments and comment updates and use the dashboard to gain some +overview over all comments of a site.

    + +

    To use the moderation features, you need to login to the comments +system. Furthermore you will need to allow cookies to be set for +comments.apache.org (this is done using a secure https cookie). Once +logged in as a moderator you will see additional moderation +options attached to each comment.

    + +

    If you are a long time follower of the Apache Tomcat projects +and you are interested in becoming a moderator, please contact us on the +users mailing list.

    + +
    + +
    + +

    No data except what you personally submit is kept on record. +A cookie is used to keep track of moderators and other people +who wish to create an account to avoid having to enter their +credentials whenever they wish to post a comment.

    + +

    To prevent spam and unsolicited comments, we use a digest of +visitors' IPs to keep track of comments posted by them.

    + +

    Entering an email address when you post a comment is completely +optional, and will not be shared with anyone. If you enter an +email address, it will be used to notify you when someone posts +a reply to one of your comments, provided you have registered +an account and validated your email address.

    + +
    + + +
    diff --git a/webapps/docs/config/ajp.xml b/webapps/docs/config/ajp.xml new file mode 100644 index 0000000..aff9f44 --- /dev/null +++ b/webapps/docs/config/ajp.xml @@ -0,0 +1,892 @@ + + + +]> + + + &project; + + + Yoav Shapira + Andrew R. Jaquith + The AJP Connector + + + + +
    + +
    + +
    + +

    The AJP Connector element represents a + Connector component that communicates with a web + connector via the AJP protocol. This is used for cases + where you wish to invisibly integrate Tomcat into an existing (or new) + Apache installation, and you want Apache to handle the static content + contained in the web application, and/or utilize Apache's SSL + processing.

    + +

    Use of the AJP protocol requires additional security considerations because + it allows greater direct manipulation of Tomcat's internal data structures + than the HTTP connectors. Particular attention should be paid to the values + used for the address, secret, + secretRequired and allowedRequestAttributesPattern + attributes.

    + +

    This connector supports load balancing when used in conjunction with + the jvmRoute attribute of the + Engine.

    + +

    The native connectors supported with this Tomcat release are:

    +
      +
    • JK 1.2.x with any of the supported servers. See + the JK docs + for details.
    • +
    • mod_proxy on Apache httpd 2.x (included by default in Apache HTTP + Server 2.2), with AJP enabled: see + the + httpd docs for details.
    • +
    + +

    Other native connectors supporting AJP may work, but are no longer + supported.

    + +
    + + +
    + + + +

    All implementations of Connector + support the following attributes:

    + + + + +

    If this is true the '\' character will be permitted as a + path delimiter.

    +

    If not specified, the default value of false will be used.

    +
    + + +

    A boolean value which can be used to enable or disable the TRACE + HTTP method. If not specified, this attribute is set to false. As per RFC + 7231 section 4.3.8, cookie and authorization headers will be excluded from + the response to the TRACE request. If you wish to include these, you can + implement the doTrace() method for the target Servlet and + gain full control over the response.

    +
    + + +

    The default timeout for asynchronous requests in milliseconds. If not + specified, this attribute is set to the Servlet specification default of + 30000 (30 seconds).

    +
    + + +

    A boolean value which can be used to enable or disable the recycling + of the facade objects that isolate the container internal request + processing objects. If set to true the facades will be + set for garbage collection after every request, otherwise they will be + reused. This setting has no effect when the security manager is enabled. + If not specified, this attribute is set to true.

    +
    + + +

    Set to true if you want calls to + request.getRemoteHost() to perform DNS lookups in + order to return the actual host name of the remote client. Set + to false to skip the DNS lookup and return the IP + address in String form instead (thereby improving performance). + By default, DNS lookups are disabled.

    +
    + + +

    When set to reject request paths containing a + %2f sequence will be rejected with a 400 response. When set + to decode request paths containing a %2f + sequence will have that sequence decoded to / at the same + time other %nn sequences are decoded. When set to + passthrough request paths containing a %2f + sequence will be processed with the %2f sequence unchanged. + If not specified the default value is reject.

    +
    + + +

    If this is true then + a call to Response.getWriter() if no character encoding + has been specified will result in subsequent calls to + Response.getCharacterEncoding() returning + ISO-8859-1 and the Content-Type response header + will include a charset=ISO-8859-1 component. (SRV.15.2.22.1)

    +

    If not specified, the default specification compliant value of + true will be used.

    +
    + + +

    The maximum number of cookies that are permitted for a request. A value + of less than zero means no limit. If not specified, a default value of 200 + will be used.

    +
    + + +

    The maximum total number of request parameters (including uploaded + files) obtained from the query string and, for POST requests, the request + body if the content type is + application/x-www-form-urlencoded or + multipart/form-data. Request parameters beyond this limit + will be ignored. A value of less than 0 means no limit. If not specified, + a default of 10000 is used. Note that FailedRequestFilter + filter can be used to reject requests that + exceed the limit.

    +
    + + +

    The maximum size in bytes of the POST which will be handled by + the container FORM URL parameter parsing. The limit can be disabled by + setting this attribute to a value less than zero. If not specified, this + attribute is set to 2097152 (2 MiB). Note that the + FailedRequestFilter + can be used to reject requests that exceed this limit.

    +
    + + +

    The maximum size in bytes of the POST which will be saved/buffered by + the container during FORM or CLIENT-CERT authentication. For both types + of authentication, the POST will be saved/buffered before the user is + authenticated. For CLIENT-CERT authentication, the POST is buffered for + the duration of the SSL handshake and the buffer emptied when the request + is processed. For FORM authentication the POST is saved whilst the user + is re-directed to the login form and is retained until the user + successfully authenticates or the session associated with the + authentication request expires. The limit can be disabled by setting this + attribute to -1. Setting the attribute to zero will disable the saving of + POST data during authentication. If not specified, this attribute is set + to 4096 (4 KiB).

    +
    + + +

    A comma-separated list of HTTP methods for which request + bodies using application/x-www-form-urlencoded will be parsed + for request parameters identically to POST. This is useful in RESTful + applications that want to support POST-style semantics for PUT requests. + Note that any setting other than POST causes Tomcat + to behave in a way that goes against the intent of the servlet + specification. + The HTTP method TRACE is specifically forbidden here in accordance + with the HTTP specification. + The default is POST

    +
    + + +

    The TCP port number on which this Connector + will create a server socket and await incoming connections. Your + operating system will allow only one server application to listen + to a particular port number on a particular IP address. If the special + value of 0 (zero) is used, then Tomcat will select a free port at random + to use for this connector. This is typically only useful in embedded and + testing applications.

    +
    + + +

    Sets the protocol to handle incoming traffic. To configure an AJP + connector this must be specified. If no value for protocol is provided, + an HTTP connector rather than an AJP connector + will be configured.
    + The standard protocol value for an AJP connector is AJP/1.3 + which uses a Java NIO based connector.
    + To use an explicit protocol, the following values may be used:
    + org.apache.coyote.ajp.AjpNioProtocol + - non blocking Java NIO connector.
    + org.apache.coyote.ajp.AjpNio2Protocol + - non blocking Java NIO2 connector.
    + Custom implementations may also be used.
    + Take a look at our Connector + Comparison chart. +

    +
    + + +

    If this Connector is being used in a proxy + configuration, configure this attribute to specify the server name + to be returned for calls to request.getServerName(). + See Proxy Support for more + information.

    +
    + + +

    If this Connector is being used in a proxy + configuration, configure this attribute to specify the server port + to be returned for calls to request.getServerPort(). + See Proxy Support for more + information.

    +
    + + +

    If this Connector is supporting non-SSL + requests, and a request is received for which a matching + <security-constraint> requires SSL transport, + Catalina will automatically redirect the request to the port + number specified here.

    +
    + + +

    Should this Connector reject a requests if the URI + matches one of the suspicious URIs patterns identified by the Servlet 6.0 + specification? The default value is false.

    +
    + + +

    Set this attribute to the name of the protocol you wish to have + returned by calls to request.getScheme(). For + example, you would set this attribute to "https" + for an SSL Connector. The default value is "http". +

    +
    + + +

    Set this attribute to true if you wish to have + calls to request.isSecure() to return true + for requests received by this Connector. You would want this on an + SSL Connector or a non SSL connector that is receiving data from a + SSL accelerator, like a crypto card, an SSL appliance or even a webserver. + The default value is false.

    +
    + + +

    This specifies the character encoding used to decode the URI bytes, + after %xx decoding the URL. The default value is UTF-8.

    +
    + + +

    This specifies if the encoding specified in contentType should be used + for URI query parameters, instead of using the URIEncoding. This + setting is present for compatibility with Tomcat 4.1.x, where the + encoding specified in the contentType, or explicitly set using + Request.setCharacterEncoding method was also used for the parameters from + the URL. The default value is false. +

    +

    Notes: See notes on this attribute in + HTTP Connector documentation.

    +
    + + +

    Set this attribute to true to cause Tomcat to use + the IP address passed by the native web server to determine the Host + to send the request to. The default value is false.

    +
    + + +

    Set this attribute to true to cause Tomcat to advertise + support for the Servlet specification using the header recommended in the + specification. The default value is false.

    +
    + +
    + +
    + + + +

    To use AJP, you must specify the protocol attribute (see above).

    + +

    The standard AJP connectors (NIO and NIO2) both support the following + attributes in addition to the common Connector attributes listed above.

    + + + + +

    The maximum length of the operating system provided queue for incoming + connection requests when maxConnections has been reached. The + operating system may ignore this setting and use a different size for the + queue. When this queue is full, the operating system may actively refuse + additional connections or those connections may time out. The default + value is 100.

    +
    + + +

    The number of threads to be used to accept connections. Increase this + value on a multi CPU machine, although you would never really need more + than 2. Also, with a lot of non keep alive connections, you + might want to increase this value as well. Default value is + 1.

    +
    + + +

    The priority of the acceptor threads. The threads used to accept + new connections. The default value is 5 (the value of the + java.lang.Thread.NORM_PRIORITY constant). See the JavaDoc + for the java.lang.Thread class for more details on what + this priority means.

    +
    + + +

    For servers with more than one IP address, this attribute specifies + which address will be used for listening on the specified port. By + default, the connector will listen on the loopback address. Unless the JVM + is configured otherwise using system properties, the Java based connectors + (NIO, NIO2) will listen on both IPv4 and IPv6 addresses when configured + with either 0.0.0.0 or ::.

    +
    + + +

    A boolean value which can be used to enable or disable sending + AJP flush messages to the fronting proxy whenever an explicit + flush happens. The default value is true.
    + An AJP flush message is a SEND_BODY_CHUNK packet with no body content. + Proxy implementations like mod_jk or mod_proxy_ajp will flush the + data buffered in the web server to the client when they receive + such a packet. Setting this to false can reduce + AJP packet traffic but might delay sending packets to the client. + At the end of the response, AJP does always flush to the client.

    +
    + + +

    The AJP protocol passes some information from the reverse proxy to the + AJP connector using request attributes. These attributes are:

    +
      +
    • javax.servlet.request.cipher_suite
    • +
    • javax.servlet.request.key_size
    • +
    • javax.servlet.request.ssl_session
    • +
    • javax.servlet.request.X509Certificate
    • +
    • AJP_LOCAL_ADDR
    • +
    • AJP_REMOTE_PORT
    • +
    • AJP_SSL_PROTOCOL
    • +
    • JK_LB_ACTIVATION
    • +
    • CERT_ISSUER (IIS only)
    • +
    • CERT_SUBJECT (IIS only)
    • +
    • CERT_COOKIE (IIS only)
    • +
    • HTTPS_SERVER_SUBJECT (IIS only)
    • +
    • CERT_FLAGS (IIS only)
    • +
    • HTTPS_SECRETKEYSIZE (IIS only)
    • +
    • CERT_SERIALNUMBER (IIS only)
    • +
    • HTTPS_SERVER_ISSUER (IIS only)
    • +
    • HTTPS_KEYSIZE (IIS only)
    • +
    +

    The AJP protocol supports the passing of arbitrary request attributes. + Requests containing arbitrary request attributes will be rejected with a + 403 response unless the entire attribute name matches this regular + expression. If not specified, the default value is null.

    +
    + + +

    Controls when the socket used by the connector is bound. If set to + true it is bound when the connector is initiated and unbound + when the connector is destroyed. If set to false, the socket + will be bound when the connector is started and unbound when it is + stopped. If not specified, the default is true.

    +
    + + +

    When client certificate information is presented in a form other than + instances of java.security.cert.X509Certificate it needs to + be converted before it can be used and this property controls which JSSE + provider is used to perform the conversion. If not specified, the default + provider will be used.

    +
    + + +

    The number of seconds during which the sockets used by this + Connector will linger when they are closed. The default + value is -1 which disables socket linger.

    +
    + + +

    The number of milliseconds this Connector will wait, + after accepting a connection, for the request URI line to be + presented. The default value for AJP protocol connectors + is -1 (i.e. infinite).

    +
    + + +

    A reference to the name in an Executor + element. If this attribute is set, and the named executor exists, the + connector will use the executor, and all the other thread attributes will + be ignored. Note that if a shared executor is not specified for a + connector then the connector will use a private, internal executor to + provide the thread pool.

    +
    + + +

    The time that the private internal executor will wait for request + processing threads to terminate before continuing with the process of + stopping the connector. If not set, the default is 5000 (5 + seconds).

    +
    + + +

    The number of milliseconds this Connector will wait for + another AJP request before closing the connection. + The default value is to use the value that has been set for the + connectionTimeout attribute.

    +
    + + +

    The maximum number of connections that the server will accept and + process at any given time. When this number has been reached, the server + will accept, but not process, one further connection. This additional + connection be blocked until the number of connections being processed + falls below maxConnections at which point the server will + start accepting and processing new connections again. Note that once the + limit has been reached, the operating system may still accept connections + based on the acceptCount setting. The default value + is 8192.

    +

    For NIO/NIO2 only, setting the value to -1, will disable the + maxConnections feature and connections will not be counted.

    +
    + + +

    The maximum number of headers in a request that are allowed by the + container. A request that contains more headers than the specified limit + will be rejected. A value of less than 0 means no limit. + If not specified, a default of 100 is used.

    +
    + + +

    The maximum number of request processing threads to be created + by this Connector, which therefore determines the + maximum number of simultaneous requests that can be handled. If + not specified, this attribute is set to 200. If an executor is associated + with this connector, this attribute is ignored as the connector will + execute tasks using the executor rather than an internal thread pool. Note + that if an executor is configured any value set for this attribute will be + recorded correctly but it will be reported (e.g. via JMX) as + -1 to make clear that it is not used.

    +
    + + +

    The minimum number of threads always kept running. This includes both + active and idle threads. If not specified, the default of 10 + is used. If an executor is associated with this connector, this attribute + is ignored as the connector will execute tasks using the executor rather + than an internal thread pool. Note that if an executor is configured any + value set for this attribute will be recorded correctly but it will be + reported (e.g. via JMX) as -1 to make clear that it is not + used.

    +
    + + +

    This attribute sets the maximum AJP packet size in Bytes. The maximum + value is 65536. It should be the same as the max_packet_size + directive configured for mod_jk. Normally it is not necessary to change + the maximum packet size. Problems with the default value have been + reported when sending certificates or certificate chains. The default + value is 8192. If set to less than 8192 then the setting will ignored and + the default value of 8192 used.

    +
    + + +

    The protocol handler caches Processor objects to speed up performance. + This setting dictates how many of these objects get cached. + -1 means unlimited, default is 200. If not using + Servlet 3.0 asynchronous processing, a good default is to use the same as + the maxThreads setting. If using Servlet 3.0 asynchronous processing, a + good default is to use the larger of maxThreads and the maximum number of + expected concurrent requests (synchronous and asynchronous).

    +
    + + +

    Only requests from workers with this secret keyword will be accepted. + The default value is null. This attribute must be specified + with a non-null, non-zero length value unless + secretRequired is explicitly configured to be + false. If this attribute is configured with a non-null, + non-zero length value then the workers must provide a + matching value else the request will be rejected irrespective of the + setting of secretRequired.

    +
    + + +

    If this attribute is true, the AJP Connector will only + start if the secret attribute is configured with a + non-null, non-zero length value. This attribute only controls whether + the secret attribute is required to be specified for the + AJP Connector to start. It does not control whether + workers are required to provide the secret. The default value is + true. This attribute should only be set to false + when the Connector is used on a trusted network.

    +
    + + +

    If set to true, the TCP_NO_DELAY option will be + set on the server socket, which improves performance under most + circumstances. This is set to true by default.

    +
    + + +

    The priority of the request processing threads within the JVM. + The default value is 5 (the value of the + java.lang.Thread.NORM_PRIORITY constant). See the JavaDoc + for the java.lang.Thread class for more details on what + this priority means.If an executor is associated + with this connector, this attribute is ignored as the connector will + execute tasks using the executor rather than an internal thread pool. Note + that if an executor is configured any value set for this attribute will be + recorded correctly but it will be reported (e.g. via JMX) as + -1 to make clear that it is not used.

    +
    + + +

    If the Connector experiences an Exception during a Lifecycle transition + should the Exception be rethrown or logged? If not specified, the default + of false will be used. Note that the default can be changed + by the org.apache.catalina.startup.EXIT_ON_INIT_FAILURE + system property.

    +
    + + +

    If set to true, the authentication will be done in Tomcat. + Otherwise, the authenticated principal will be propagated from the native + webserver and used for authorization in Tomcat.

    +

    The web server must send the user principal (username) as a request + attribute named REMOTE_USER.

    +

    Note that this principal will have no roles associated with it.

    +

    The default value is true. If + tomcatAuthorization is set to true this + attribute has no effect.

    +
    + + +

    If set to true, the authenticated principal will be + propagated from the native webserver and considered already authenticated + in Tomcat. If the web application has one or more security constraints, + authorization will then be performed by Tomcat and roles assigned to the + authenticated principal. If the appropriate Tomcat Realm for the request + does not recognise the provided user name, a Principal will be still be + created but it will have no roles. The default value is + false.

    +
    + + +

    (bool) Use this attribute to enable or disable usage of virtual threads + with the internal executor. If an executor is associated with this + connector, this attribute is ignored. The default value is + false.

    +
    + +
    + +
    + + + +

    The NIO and NIO2 implementation support the following Java TCP socket + attributes in addition to the common Connector and HTTP attributes listed + above.

    + + + +

    (int)The socket receive buffer (SO_RCVBUF) size in bytes. JVM default + used if not set.

    +
    + +

    (int)The socket send buffer (SO_SNDBUF) size in bytes. JVM default + used if not set. Care should be taken if explicitly setting this value. + Very poor performance has been observed on some JVMs with values less + than ~8k.

    +
    + +

    (bool)This is equivalent to standard attribute + tcpNoDelay.

    +
    + +

    (bool)Boolean value for the socket's keep alive setting + (SO_KEEPALIVE). JVM default used if not set.

    +
    + +

    (bool)Boolean value for the socket OOBINLINE setting. JVM default + used if not set.

    +
    + +

    (bool)Boolean value for the sockets reuse address option + (SO_REUSEADDR). JVM default used if not set.

    +
    + +

    (bool)Boolean value for the sockets so linger option (SO_LINGER). + A value for the standard attribute connectionLinger + that is >=0 is equivalent to setting this to true. + A value for the standard attribute connectionLinger + that is <0 is equivalent to setting this to false. + Both this attribute and soLingerTime must be set else the + JVM defaults will be used for both.

    +
    + +

    (int)Value in seconds for the sockets so linger option (SO_LINGER). + This is equivalent to standard attribute + connectionLinger. + Both this attribute and soLingerOn must be set else the + JVM defaults will be used for both.

    +
    + +

    This is equivalent to standard attribute + connectionTimeout.

    +
    + +

    (int)The first value for the performance settings. See + Socket Performance Options + All three performance attributes must be set else the JVM defaults will + be used for all three.

    +
    + +

    (int)The second value for the performance settings. See + Socket Performance Options + All three performance attributes must be set else the JVM defaults will + be used for all three.

    +
    + +

    (int)The third value for the performance settings. See + Socket Performance Options + All three performance attributes must be set else the JVM defaults will + be used for all three.

    +
    + +

    (int) The timeout for a socket unlock. When a connector is stopped, it will try to release the acceptor thread by opening a connector to itself. + The default value is 250 and the value is in milliseconds

    +
    +
    +
    + + + +

    The following attributes are specific to the NIO connector.

    + + + + +

    (bool)Boolean value, whether to use direct ByteBuffers or java mapped + ByteBuffers. Default is false.
    + When you are using direct buffers, make sure you allocate the + appropriate amount of memory for the direct memory space. On Sun's JDK + that would be something like -XX:MaxDirectMemorySize=256m. +

    +
    + + +

    (int)Each connection that is opened up in Tomcat get associated with + a read ByteBuffer. This attribute controls the size of this buffer. By + default this read buffer is sized at 8192 bytes. For lower + concurrency, you can increase this to buffer more data. For an extreme + amount of keep alive connections, decrease this number or increase your + heap size.

    +
    + + +

    (int)Each connection that is opened up in Tomcat get associated with + a write ByteBuffer. This attribute controls the size of this buffer. By + default this write buffer is sized at 8192 bytes. For low + concurrency you can increase this to buffer more response data. For an + extreme amount of keep alive connections, decrease this number or + increase your heap size.
    + The default value here is pretty low, you should up it if you are not + dealing with tens of thousands concurrent connections.

    +
    + + +

    (int)The NIO connector uses a class called NioChannel that holds + elements linked to a socket. To reduce garbage collection, the NIO + connector caches these channel objects. This value specifies the size of + this cache. The default value is 500, and represents that + the cache will hold 500 NioChannel objects. Other values are + -1 for unlimited cache and 0 for no cache.

    +
    + + +

    (int)The NioChannel pool can also be size based, not used object + based. The size is calculated as follows:
    + NioChannel + buffer size = read buffer size + write buffer size
    + SecureNioChannel buffer size = application read buffer size + + application write buffer size + network read buffer size + + network write buffer size
    + The value is in bytes, the default value is 1024*1024*100 + (100 MiB).

    +
    + + +

    (int)Tomcat will cache SocketProcessor objects to reduce garbage + collection. The integer value specifies how many objects to keep in the + cache at most. The default is 0. Other values are + -1 for unlimited cache and 0 for no cache.

    +
    + + +

    (int)Tomcat will cache PollerEvent objects to reduce garbage + collection. The integer value specifies how many objects to keep in the + cache at most. The default is 0. Other values are + -1 for unlimited cache and 0 for no cache.

    +
    + +
    +
    + + + +

    The following attributes are specific to the NIO2 connector.

    + + + + +

    (bool)Use this attribute to enable or disable object caching to + reduce the amount of GC objects produced. + The default value is false.

    +
    + + +

    (bool)Boolean value, whether to use direct ByteBuffers or java mapped + ByteBuffers. Default is false.
    + When you are using direct buffers, make sure you allocate the + appropriate amount of memory for the direct memory space. On Sun's JDK + that would be something like -XX:MaxDirectMemorySize=256m. +

    +
    + + +

    (int)Each connection that is opened up in Tomcat get associated with + a read ByteBuffer. This attribute controls the size of this buffer. By + default this read buffer is sized at 8192 bytes. For lower + concurrency, you can increase this to buffer more data. For an extreme + amount of keep alive connections, decrease this number or increase your + heap size.

    +
    + + +

    (int)Each connection that is opened up in Tomcat get associated with + a write ByteBuffer. This attribute controls the size of this buffer. By + default this write buffer is sized at 8192 bytes. For low + concurrency you can increase this to buffer more response data. For an + extreme amount of keep alive connections, decrease this number or + increase your heap size.
    + The default value here is pretty low, you should up it if you are not + dealing with tens of thousands concurrent connections.

    +
    + + +

    (int)The NIO2 connector uses a class called Nio2Channel that holds + elements linked to a socket. To reduce garbage collection, the NIO + connector caches these channel objects. This value specifies the size of + this cache. The default value is 500, and represents that + the cache will hold 500 Nio2Channel objects. Other values are + -1 for unlimited cache and 0 for no cache.

    +
    + + +

    (int)Tomcat will cache SocketProcessor objects to reduce garbage + collection. The integer value specifies how many objects to keep in the + cache at most. The default is 0. Other values are + -1 for unlimited cache and 0 for no cache.

    +
    + +
    +
    + +
    + + +
    + +

    None at this time.

    + +
    + +
    + + + +

    The proxyName and proxyPort attributes can + be used when Tomcat is run behind a proxy server. These attributes + modify the values returned to web applications that call the + request.getServerName() and request.getServerPort() + methods, which are often used to construct absolute URLs for redirects. + Without configuring these attributes, the values returned would reflect + the server name and port on which the connection from the proxy server + was received, rather than the server name and port to whom the client + directed the original request.

    + +

    For more information, see the + Proxy Support How-To.

    + +
    + + + +

    Below is a small chart that shows how the connectors differ.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Java Nio Connector
    NIO
    Java Nio2 Connector
    NIO2
    ClassnameAjpNioProtocolAjpNio2Protocol
    Tomcat Version7.x onwards8.x onwards
    Support PollingYESYES
    Polling SizemaxConnectionsmaxConnections
    Read Request HeadersBlockingBlocking
    Read Request BodyBlockingBlocking
    Write Response Headers and BodyBlockingBlocking
    Wait for next RequestNon BlockingNon Blocking
    Max ConnectionsmaxConnectionsmaxConnections
    + +
    + +
    + + + +
    diff --git a/webapps/docs/config/automatic-deployment.xml b/webapps/docs/config/automatic-deployment.xml new file mode 100644 index 0000000..03f9bb9 --- /dev/null +++ b/webapps/docs/config/automatic-deployment.xml @@ -0,0 +1,548 @@ + + + +]> + + + &project; + + + Automatic Deployment - Use cases + + + + +
    + +
    + + +
    + +

    This page defines the expected behaviour of the automatic deployer in many + typical use cases. This is a complex area of Tomcat's functionality. + While any difference between this document and Tomcat's behaviour is a + bug, the fix may be to change this document, Tomcat's behaviour or + both.

    + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TermDescription
    XMLAn XML configuration file located in the Host's + configBase. It must contain a single <Context> element + and may contain optional nested elements. It does not define an + explicit docBase attribute. It represents a single web + application. It is often referred to as a context.xml file.
    XML+EWAn XML configuration file located in the Host's + configBase. It must contain a single <Context> element + and may contain optional nested elements. It includes an explicit + docBase attribute that points to an external WAR. It + represents a single web application. It is often referred to as a + context.xml file.
    XML+EDAn XML configuration file located in the Host's + configBase. It must contain a single <Context> element + and may contain optional nested elements. It includes an explicit + docBase attribute that points to an external directory. It + represents a single web application. It is often referred to as a + context.xml file.
    WARA WAR file located in the Host's appBase. The WAR does + not include an embedded context.xml file.
    WAR+XMLA WAR file located in the Host's appBase. The WAR does + include an embedded context.xml file.
    DIRA directory located in the Host's appBase. The directory + does not include an embedded context.xml file.
    DIR+XMLA directory located in the Host's appBase. The directory + does include an embedded context.xml file.
    redeployThe Context object that represents the web application is destroyed + and a new Context object is created. If present and permitted by the + configuration, this new Context object is created by parsing the + context.xml file. The web.xml file is parsed during the application + start process. Any sessions stored in the standard Manager in the + default configuration will not be persisted. Any requests to the web + application during the redeploy will be handled as if the web + application is not deployed.
    reloadThe Context object that represents the web application is stopped and + then started. The web.xml file is parsed during the application start + process. Any sessions stored in the standard Manager in the default + configuration will not be persisted. Any requests to the web + application during the reload will be held until the reload completes + at which point they will continue using the reloaded web application. +
    + +
    + + +
    + +

    This section describes Tomcat's behaviour when the automatic + deployment process discovers a new web application.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Starting artifact(s)Configuration SettingsResult
    deployXMLcopyXMLunpackWARsXMLWARDIRNotes
    XMLeithereithereitherYNN1, 2, 3
    XML+EWeithereitherfalseYNN1
    XML+EWeithereithertrueYNY1
    XML+EDeithereithereitherYNN1, 2
    WAR+XMLfalseeitherfalseNYN4
    WAR+XMLfalseeithertrueNYY4
    WAR+XMLtruefalsefalseNYN
    WAR+XMLtruefalsetrueNYY
    WAR+XMLtruetruefalseYYN
    WAR+XMLtruetruetrueYYY
    WAReithereitherfalseNYN
    WAReithereithertrueNYY
    DIR+XMLfalseeithereitherNNY4
    DIR+XMLtruefalseeitherNNY
    DIR+XMLtruetrueeitherYNY
    DIRfalseeithereitherNNY
    + +
    + + +
    + +

    This section describes Tomcat's behaviour when the automatic + deployment process detects that a web application file has been deleted.

    + +

    When a file is deleted or modified any redeploy resources that are listed + after the modified/deleted resource are themselves deleted (and possibly + re-created). The order of redeploy resources is:

    + +
      +
    1. WAR
    2. +
    3. DIR
    4. +
    5. XML
    6. +
    7. global resources
    8. +
    + +

    There are some exceptions to the deletion rule above:

    + +
      +
    • global resources are never deleted
    • +
    • external resources are never deleted
    • +
    • if the WAR or DIR has been modified then the XML file is only deleted if + copyXML is true and deployXML is + true
    • +
    + +

    In the following table:

    + +
      +
    • '-' means "unchanged from not present". i.e. the artifact wasn't present + before the change and isn't present after it either. '-' rather than 'N' + is used to focus attention on what changes.
    • +
    • 'R' means that the directory is re-created by expanding the WAR file. + This will only happen if unpackWARs is true.
    • +
    • 'XW' means that the if the WAR contains a META-INF/context.xml file it + will be extracted and placed in the Host's configBase. + This only happens if copyXML is true and + deployXML is true.
    • +
    • 'XD' means that the if the directory contains a META-INF/context.xml + file it will be copied to the Host's configBase. This only + happens if copyXML is true and deployXML + is true.
    • +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Artifacts presentArtifact removedArtifacts remaining
    XMLWARDIRXMLWARDIRNotes
    NNYDIR--N
    NYNWAR-N-
    NYYDIR-YR
    NYYWAR-NN
    YNNXMLN--
    YNYDIRN-N5
    YNYXMLXD-Y
    YYNWARNN-5
    YYNXMLXWY-
    YYYDIRXWYR
    YYYWARNNN
    YYYXMLXWYY
    YY (external)NWARYN-3
    YY (external)NXMLNY (external)-6
    YNY (external)DIRY-N3
    YNY (external)XMLN-Y (external)6
    YY (external)YDIRYY (external)R
    YY (external)YWARYNN3
    YY (external)YXMLNY (external)N6
    + +
    + + +
    + +

    This section describes Tomcat's behaviour when the automatic + deployment process detects that a web application file has been modified.

    + +

    In the following table:

    + +
      +
    • '-' means "unchanged from not present". i.e. the artifact wasn't present + before the change and isn't present after it either. '-' rather than 'N' + is used to focus attention on what changes.
    • +
    • 'M' means that the artifact has been modified.
    • +
    • 'R' means that the directory is deleted and re-created by expanding the + WAR file. This will only happen if unpackWARs is + true.
    • +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Artifacts presentArtifact modifiedArtifacts remaining
    XMLWARDIRXMLWARDIRAction
    NNYDIR--MNone
    NYNWAR-M-Redeploy
    NYYDIR-YMNone
    NYYWAR-MRRedeploy
    YNNXMLM--Redeploy
    YNYDIRY-MNone
    YNYXMLM-YRedeploy
    YYNWARYM-Reload
    YYNXMLMY-Redeploy
    YYYDIRYYMNone
    YYYWARYMRReload
    YYYXMLMYYRedeploy
    YY(external)NWARYM(external)-Reload
    YY(external)NXMLMY(external)-Redeploy
    YNY(external)DIRY-M(external)None
    YNY(external)XMLM-Y(external)Redeploy
    YY(external)YDIRYY(external)MNone
    YY(external)YWARYM(external)RReload
    YY(external)YXMLMY(external)YRedeploy
    + +
    + + +
    + +

    This is treated as if the added file has been modified with the following + additional actions:

    + +
      +
    • If a WAR is added, any DIR is removed and may be recreated depending on + unpackWARs.
    • +
    • If an XML file is added that refers to an external docBase any + WAR or DIR in the appBase will be removed. The DIR may be recreated if + the external resource is a WAR and unpackWARs is true.
    • +
    • If a DIR is added when a WAR already exists and unpackWARs is + false, the DIR will be ignored but a warning will be + logged when the DIR is first detected. If the WAR is removed, the DIR + will be left and may be deployed via automatic deployment.
    • +
    • If a WAR is added to the appBase when an external WAR already + exists, the WAR in the appBase will be ignored but a warning + will be logged when the WAR in the appBase is first detected. + If the external WAR is removed, the WAR in the appBase will be + left and may be deployed via automatic deployment.
    • +
    • If an XML file is added to the META-INF directory of an application + deployed from that DIR, the application will always be redeployed. The + result will be the same as for a new deployment.
    • +
    + +
    + + +
    + +
      +
    1. deployXML and copyXML are ignored since an XML file + was discovered in the configBase.
    2. +
    3. unpackWARs is ignored since there is no WAR file.
    4. +
    5. The context will fail to start because there is no content in the + expected docBase.
    6. +
    7. The web application fails to deploy because it contains an embedded + META-INF/context.xml, deployXML is false and an + XML has not been provided in the configBase.
    8. +
    9. The XML file is only deleted if copyXML is true + and deployXML is true.
    10. +
    11. Although the external resource is still present, the web application is + fully undeployed as Tomcat has no knowledge of the external resource. +
    12. +
    + +
    + + + +
    diff --git a/webapps/docs/config/cluster-channel.xml b/webapps/docs/config/cluster-channel.xml new file mode 100644 index 0000000..f563974 --- /dev/null +++ b/webapps/docs/config/cluster-channel.xml @@ -0,0 +1,146 @@ + + + +]> + + + &project; + + + Filip Hanik + The Cluster Channel object + + + + +
    + +
    + +
    + The cluster channel is the main component of a small framework we've nicknamed Apache Tribes.
    + The channel manages a set of sub components and together they create a group communication framework.
    + This framework is then used internally by the components that need to send messages between different Tomcat instances. +
    + A few examples of these components would be the SimpleTcpCluster that does the messaging for the DeltaManager, + or the BackupManager that uses a different replication strategy. The ReplicatedContext object does also + use the channel object to communicate context attribute changes. +
    +
    +

    Channel/Membership:
    + The Membership component is responsible for auto discovering new nodes in the cluster + and also to provide for notifications for any nodes that have not responded with a heartbeat. + The default implementation uses multicast.
    + In the membership component you configure how your nodes, aka. members, are to be discovered and/or + divided up. + You can always find out more about Apache Tribes +

    +

    Channel/Sender:
    + The Sender component manages all outbound connections and data messages that are sent + over the network from one node to another. + This component allows messages to be sent in parallel. + The default implementation uses TCP client sockets, and socket tuning for outgoing messages are + configured here.
    + You can always find out more about Apache Tribes +

    +

    Channel/Sender/Transport:
    + The Transport component is the bottom IO layer for the sender component. + The default implementation uses non-blocking TCP client sockets.
    + You can always find out more about Apache Tribes +

    +

    Channel/Receiver:
    + The receiver component listens for messages from other nodes. + Here you will configure the cluster thread pool, as it will dispatch incoming + messages to a thread pool for faster processing. + The default implementation uses non-blocking TCP server sockets.
    + You can always find out more about Apache Tribes +

    +

    Channel/Interceptor:
    + The channel will send messages through an interceptor stack. Because of this, you have the ability to + customize the way messages are sent and received, and even how membership is handled.
    + You can always find out more about Apache Tribes +

    +
    + + +
    + + + + + + + The default value here is org.apache.catalina.tribes.group.GroupChannel and is + currently the only implementation available. + + + + + + + + + + + + + + Flag whether the channel manages its own heartbeat. + If set to true, the channel start a local thread for the heart beat. + If set this flag to false, you must set SimpleTcpCluster#heartbeatBackgroundEnabled + to true. default value is true. + + + + If heartbeat == true, specifies the interval of heartbeat thread in milliseconds. + The default is 5000 (5 seconds). + + + + If set to true, the GroupChannel will check the option flags that each + interceptor is using. Reports an error if two interceptor share the same + flag. The default is false. + + + + Flag whether the channel components register with JMX or not. + The default value is true. + + + + if jmxEnabled set to true, specifies the jmx domain which + this channel should be registered. The ClusterChannel is used as the + default value. + + + + if jmxEnabled set to true, specifies the jmx prefix which + will be used with channel ObjectName. + + + + + + +
    + + + + +
    diff --git a/webapps/docs/config/cluster-deployer.xml b/webapps/docs/config/cluster-deployer.xml new file mode 100644 index 0000000..2479487 --- /dev/null +++ b/webapps/docs/config/cluster-deployer.xml @@ -0,0 +1,109 @@ + + + +]> + + + &project; + + + Filip Hanik + The Cluster Deployer object + + + + +
    + +
    + +
    +

    The Farm War Deployer can deploy and undeploy web applications on the other + nodes in the cluster.

    +

    Note: FarmWarDeployer can be configured at host level + cluster only. +

    +
    + + +
    + + + + + + The cluster deployer class, currently only one is available, + org.apache.catalina.ha.deploy.FarmWarDeployer. + + + Deployment directory. This is the pathname of a directory where deploy + the web applications. You may specify an absolute pathname, or a + pathname that is relative to the $CATALINA_BASE directory. In the + current implementation, this attribute must be the same value as the + Host's appBase. + + + The temporaryDirectory to store binary data when downloading a war from + the cluster. You may specify an absolute pathname, or a pathname that is + relative to the $CATALINA_BASE directory. + + + This is the pathname of a directory where watch for changes(add/modify/remove) + of web applications. You may specify an absolute pathname, or a pathname + that is relative to the $CATALINA_BASE directory. + Note: if watchEnabled is false, this + attribute will have no effect. + + + Set to true if you want to watch for changes of web applications. + Only when this attribute set to true, you can trigger a deploy/undeploy + of web applications. The flag's value defaults to false. + + + Frequency of the Farm watchDir check. Cluster wide deployment will be + done once for the specified amount of backgroundProcess calls (ie, the + lower the amount, the most often the checks will occur). The minimum + value is 1, and the default value is 2. + Note: if watchEnabled is false, this + attribute will have no effect. + + + FileMessageFactory instances used by the FarmWarDeployer are only + retained while they are required. When receiving a WAR file, the + associated FileMessageFactory instance is deleted once the WAR file has + been fully received. To avoid memory leaks under various error + conditions (part of the file never received, very slow message transfer, + etc.), this attribute defines the maximum time permitted between + receiving valid messages that contain part of the WAR file. If that + maximum time is exceeded, the FileMessageFactory will be deleted and the + WAR file transfer will fail for that node. If a negative value is + specified, the FileMessageFactory will only be removed once the WAR file + is fully received. If not specified, the default value of 300 (5 + minutes) will be used. + + + + + +
    + + + + +
    diff --git a/webapps/docs/config/cluster-interceptor.xml b/webapps/docs/config/cluster-interceptor.xml new file mode 100644 index 0000000..995af17 --- /dev/null +++ b/webapps/docs/config/cluster-interceptor.xml @@ -0,0 +1,320 @@ + + + +]> + + + &project; + + + Filip Hanik + The Channel Interceptor object + + + + +
    + +
    + +
    +

    + Apache Tribes supports an interceptor architecture to intercept both messages and membership notifications. + This architecture allows decoupling of logic and opens the way for some very useful feature add-ons. +

    +
    + +
    +
      +
    • org.apache.catalina.tribes.group.interceptors.TcpFailureDetector
    • +
    • org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.NonBlockingCoordinator
    • +
    • org.apache.catalina.tribes.group.interceptors.OrderInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.SimpleCoordinator
    • +
    • org.apache.catalina.tribes.group.interceptors.StaticMembershipInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.TwoPhaseCommitInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.DomainFilterInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.FragmentationInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.GzipInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.TcpPingInterceptor
    • +
    • org.apache.catalina.tribes.group.interceptors.EncryptInterceptor
    • +
    +
    + +
    +

    + In addition to dynamic discovery, Apache Tribes also supports static membership, with membership verification. + To achieve this add the org.apache.catalina.tribes.group.interceptors.StaticMembershipInterceptor + after the org.apache.catalina.tribes.group.interceptors.TcpFailureDetector interceptor. + Inside the StaticMembershipInterceptor you can add the static members you wish to have. + The TcpFailureDetector will do a health check on the static members,and also monitor them for crashes + so they will have the same level of notification mechanism as the members that are automatically discovered.

    + + + + ]]> +
    + +
    + + + + + Required, as there is no default + + + If you want the interceptor to trigger on certain message depending on the message's option flag, + you can setup the interceptors flag here. + The default value is 0, meaning this interceptor will trigger on all messages. + + + + + + + + The logical cluster domain that this Interceptor accepts. + Two different type of values are possible:
    + 1. Regular string values like "staging-domain" or "tomcat-cluster" will be converted into bytes + using ISO-8859-1 encoding.
    + 2. byte array in string form, for example {216,123,12,3}
    +
    + + This value indicates the interval for logging for messages from different domains. + The default is 100, which means that to log per 100 messages. + +
    +
    + + + + How long do we keep the fragments in memory and wait for the rest to arrive. + The default is 60000 ms. + + + The maximum message size in bytes. If the message size exceeds this value, this interceptor fragments the message and sends them. + If it is less than this value, this interceptor does not fragment the message and sent in as one message. The default is 1024*100. + + + + + + + The default and hard coded value is 8 (org.apache.catalina.tribes.Channel.SEND_OPTIONS_ASYNCHRONOUS). + The dispatcher will trigger on this value only, as it is predefined by Tribes. + + + What behavior should be executed when the dispatch queue is full. If true (default), then the message is + is sent synchronously, if false an error is thrown. + + + Size in bytes of the dispatch queue, the default value is 1024*1024*64 (64 MiB) sets the maximum queue size for the dispatch queue + if the queue fills up, one can trigger the behavior, if alwaysSend is set to true, the message will be sent synchronously + if the flag is false, an error is thrown + + + The maximum number of threads in this pool, default is 10. + + + The number of threads to keep in the pool, default is 2. + + + Maximum number of milliseconds of until Idle thread terminates. Default value is 5000(5 seconds). + + + + + + + Specifies the timeout, in milliseconds, to use when attempting a TCP connection + to the suspect node. Default is 1000. + + + If true is set, send a test message to the suspect node. Default is true. + + + If true is set, read the response of the test message that sent. Default is false. + Note: if performSendTest is false, this attribute will have no effect. + + + Specifies the timeout, in milliseconds, to use when performing a read test + to the suspicious node. Default is 5000. + + + The maximum time(in seconds) for remove from removeSuspects. Member of + removeSuspects will be automatically removed after removeSuspectsTimeout. + If a negative value specified, the removeSuspects members never be + removed until disappeared really. If the attribute is not provided, + a default of 300 seconds (5 minutes) is used. + + + + + + + If useThread == true, defines the interval of sending a ping message. + default is 1000 ms. + + + Flag of whether to start a thread for sending a ping message. + If set to true, this interceptor will start a local thread for sending a ping message. + if set to false, channel heartbeat will send a ping message. + default is false. + + + + + + + Defines the interval in number of messages when we are to report the throughput statistics. + The report is logged to the org.apache.juli.logging.LogFactory.getLog(ThroughputInterceptor.class) + logger under the INFO level. + Default value is to report every 10000 messages. + + + + +

    + The EncryptInterceptor adds encryption to the channel messages carrying + session data between nodes. +

    +

    + If using the TcpFailureDetector, the EncryptInterceptor + must be inserted into the interceptor chain before the + TcpFailureDetector. This is because when validating cluster + members, TcpFailureDetector writes channel data directly + to the other members without using the remainder of the interceptor chain, + but on the receiving side, the message still goes through the chain (in reverse). + Because of this asymmetry, the EncryptInterceptor must execute + before the TcpFailureDetector on the sender and after + it on the receiver, otherwise message corruption will occur. +

    + + + The encryption algorithm to be used, including the mode and padding. Please see + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html + for the standard JCA names that can be used. + + EncryptInterceptor currently supports the following + block-cipher modes: + CBC, OFB, CFB, and GCM. + + The length of the key will specify the flavor of the encryption algorithm + to be used, if applicable (e.g. AES-128 versus AES-256). + + The default algorithm is AES/CBC/PKCS5Padding. + + + The key to be used with the encryption algorithm. + + The key should be specified as hex-encoded bytes of the appropriate + length for the algorithm (e.g. 16 bytes / 32 characters / 128 bits for + AES-128, 32 bytes / 64 characters / 256 bits for AES-256, etc.). + + +
    +
    + +
    + + +

    LocalMember:
    + Static member that is the local member of the static cluster group. +

    + + + Only one implementation available:org.apache.catalina.tribes.membership.StaticMember + + + There is no need to set. + The value of this attribute inherits from the cluster receiver setting. + + + There is no need to set. + The value of this attribute inherits from the cluster receiver setting. + + + There is no need to set. + The value of this attribute inherits from the cluster receiver setting. + + + The logical cluster domain for that this static member listens for cluster messages. + Two different type of values are possible:
    + 1. Regular string values like "staging-domain" or "tomcat-cluster" will be converted into bytes + using ISO-8859-1 encoding. + 2. byte array in string form, for example {216,123,12,3}
    +
    + + A universally uniqueId for this static member. + The values must be 16 bytes in the following form:
    + 1. byte array in string form, for example {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}
    +
    +
    + +

    Member:
    + Static member that add to the static cluster group. +

    + + + Only one implementation available:org.apache.catalina.tribes.membership.StaticMember + + + The port that this static member listens to for cluster messages + + + The secure port this static member listens to for encrypted cluster messages + default value is -1, this value means the member is not listening on a secure port + + + The host (or network interface) that this static member listens for cluster messages. + Three different type of values are possible:
    + 1. IP address in the form of "216.123.1.23"
    + 2. Hostnames like "tomcat01.mydomain.com" or "tomcat01" as long as they resolve correctly
    + 3. byte array in string form, for example {216,123,12,3}
    +
    + + The logical cluster domain for that this static member listens for cluster messages. + Two different type of values are possible:
    + 1. Regular string values like "staging-domain" or "tomcat-cluster" will be converted into bytes + using ISO-8859-1 encoding.
    + 2. byte array in string form, for example {216,123,12,3}
    +
    + + A universally uniqueId for this static member. + The values must be 16 bytes in the following form:
    + 1. byte array in string form, for example {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}
    +
    +
    +
    + + +
    + + + + +
    diff --git a/webapps/docs/config/cluster-listener.xml b/webapps/docs/config/cluster-listener.xml new file mode 100644 index 0000000..f27f1b3 --- /dev/null +++ b/webapps/docs/config/cluster-listener.xml @@ -0,0 +1,67 @@ + + + +]> + + + &project; + + + Filip Hanik + The ClusterListener object + + + + +
    + +
    + +
    +

    + The org.apache.catalina.ha.ClusterListener base class + lets you listen in on messages that are received by the Cluster component. +

    + +
    +
    +

    + When using the DeltaManager, the messages are received by the Cluster object and are propagated to the + to the manager through this listener. +

    +
    + +
    + + + + + Set value to org.apache.catalina.ha.session.ClusterSessionListener + + + + + + +
    + + + + +
    diff --git a/webapps/docs/config/cluster-manager.xml b/webapps/docs/config/cluster-manager.xml new file mode 100644 index 0000000..7d742cb --- /dev/null +++ b/webapps/docs/config/cluster-manager.xml @@ -0,0 +1,293 @@ + + + +]> + + + &project; + + + Filip Hanik + The ClusterManager object + + + + +
    + +
    + +
    +

    A cluster manager is an extension to Tomcat's session manager interface, + org.apache.catalina.Manager. + A cluster manager must implement the + org.apache.catalina.ha.ClusterManager and is solely responsible + for how the session is replicated.
    + There are currently two different managers, the + org.apache.catalina.ha.session.DeltaManager replicates deltas of + session data to all members in the cluster. This implementation is proven and + works very well, but has a limitation as it requires the cluster members to be + homogeneous, all nodes must deploy the same applications and be exact + replicas. The org.apache.catalina.ha.session.BackupManager also + replicates deltas but only to one backup node. The location of the backup node + is known to all nodes in the cluster. It also supports heterogeneous + deployments, so the manager knows at what locations the web application is + deployed.

    +
    + +
    +

    The <Manager> element defined inside the + <Cluster> element is the template defined for all web + applications that are marked <distributable/> in their + web.xml file. However, you can still override the manager + implementation on a per web application basis, by putting the + <Manager> inside the <Context> element + either in the context.xml file or the + server.xml file.

    +
    + +
    + + + + + + The name of this cluster manager, the name is used to identify a + session manager on a node. The name might get modified by the + Cluster element to make it unique in the container. + + + Set to true if you wish to have session listeners notified + when session attributes are being replicated or removed across Tomcat + nodes in the cluster. + + +

    Frequency of the session expiration, and related manager operations. + Manager operations will be done once for the specified amount of + backgroundProcess calls (i.e., the lower the amount, the more often the + checks will occur). The minimum value is 1, and the default value is 6. +

    +
    + +

    Name of the Java class that extends + java.security.SecureRandom to use to generate session IDs. + If not specified, the default value is + java.security.SecureRandom.

    +
    + +

    Name of the provider to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the Manager + will use the platform default provider and the default algorithm. If not + specified, the platform default provider will be used.

    +
    + +

    Name of the algorithm to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the Manager + will use the platform default provider and the default algorithm. If not + specified, the default algorithm of SHA1PRNG will be used. If the + default algorithm is not supported, the platform default will be used. + To specify that the platform default should be used, do not set the + secureRandomProvider attribute and set this attribute to the empty + string.

    +
    + +

    Flag whether send all actions for session across Tomcat cluster + nodes. If set to false, if already done something to the same attribute, + make sure don't send multiple actions across Tomcat cluster nodes. + In that case, sends only the actions that have been added at last. + Default is false.

    +
    +
    +
    + + + + When a web application is being shutdown, Tomcat issues an expire call + to each session to notify all the listeners. If you wish for all + sessions to expire on all nodes when a shutdown occurs on one node, set + this value to true. + Default value is false. + + + The maximum number of active sessions that will be created by this + Manager, or -1 (the default) for no limit. For this manager, all + sessions are counted as active sessions irrespective if whether or not + the current node is the primary node for the session. + + + Set to true if you wish to have session listeners notified + when sessions are created and expired across Tomcat nodes in the + cluster. + + + Set to true if you wish to have container listeners notified + across Tomcat nodes in the cluster. + + + The time in seconds to wait for a session state transfer to complete + from another node when a node is starting up. + Default value is 60 seconds. + + + Flag whether send sessions as split blocks. + If set to true, send all sessions as one big block. + If set to false, send sessions as split blocks. + Default value is true. + + + The number of sessions in a session block message. This value is + effective only when sendAllSessions is false. + Default is 1000. + + + Wait time between sending of session block messages. This value is + effective only when sendAllSessions is false. + Default is 2000 milliseconds. + + +

    A regular expression used to filter which session attributes will be + replicated. An attribute will only be replicated if its name matches + this pattern. If the pattern is zero length or null, all + attributes are eligible for replication. The pattern is anchored so the + session attribute name must fully match the pattern. As an example, the + value (userName|sessionHistory) will only replicate the + two session attributes named userName and + sessionHistory. If not specified, the default value of + null will be used.

    +
    + +

    A regular expression used to filter which session attributes will be + replicated. An attribute will only be replicated if the implementation + class name of the value matches this pattern. If the pattern is zero + length or null, all attributes are eligible for + replication. The pattern is anchored so the fully qualified class name + must fully match the pattern. If not specified, the default value of + null will be used unless a SecurityManager is + enabled in which case the default will be + java\\.lang\\.(?:Boolean|Integer|Long|Number|String).

    +
    + + When this node sends a GET_ALL_SESSIONS message to other + node, all session messages that are received as a response are queued. + If this attribute is set to true, the received session + messages (except any GET_ALL_SESSIONS sent by other nodes) + are filtered by their timestamp. A message is dropped if it is not a + GET_ALL_SESSIONS message and its timestamp is earlier than + the timestamp of our GET_ALL_SESSIONS message. + If set to false, all queued session messages are handled. + Default is true. + + +

    If sessionAttributeNameFilter or + sessionAttributeValueClassNameFilter blocks an + attribute, should this be logged at WARN level? If + WARN level logging is disabled then it will be logged at + DEBUG. The default value of this attribute is + false unless a SecurityManager is enabled in + which case the default will be true.

    +
    +
    +
    + + + + The backup manager uses a replicated map, this map is sending and + receiving messages. You can setup the flag for how this map is sending + messages, the default value is 6(synchronous).
    + Note that if you use asynchronous messaging it is possible for update + messages for a session to be processed by the receiving node in a + different order to the order in which they were sent. +
    + + The maximum number of active sessions that will be created by this + Manager, or -1 (the default) for no limit. For this manager, only + sessions where the current node is the primary node for the session are + considered active sessions. + + + Timeout for RPC message used for broadcast and transfer state from + another map. + Default value is 15000 milliseconds. + + +

    A regular expression used to filter which session attributes will be + replicated. An attribute will only be replicated if its name matches + this pattern. If the pattern is zero length or null, all + attributes are eligible for replication. The pattern is anchored so the + session attribute name must fully match the pattern. As an example, the + value (userName|sessionHistory) will only replicate the + two session attributes named userName and + sessionHistory. If not specified, the default value of + null will be used.

    +
    + +

    A regular expression used to filter which session attributes will be + replicated. An attribute will only be replicated if the implementation + class name of the value matches this pattern. If the pattern is zero + length or null, all attributes are eligible for + replication. The pattern is anchored so the fully qualified class name + must fully match the pattern. If not specified, the default value of + null will be used unless a SecurityManager is + enabled in which case the default will be + java\\.lang\\.(?:Boolean|Integer|Long|Number|String).

    +
    + + Set to true if you wish to terminate replication map when replication + map fails to start. If replication map is terminated, associated context + will fail to start. If you set this attribute to false, replication map + does not end. It will try to join the map membership in the heartbeat. + Default value is false . + + +

    If sessionAttributeNameFilter or + sessionAttributeValueClassNameFilter blocks an + attribute, should this be logged at WARN level? If + WARN level logging is disabled then it will be logged at + DEBUG. The default value of this attribute is + false unless a SecurityManager is enabled in + which case the default will be true.

    +
    + + The timeout for a ping message. If a remote map does not respond within + this timeout period, its regarded as disappeared. + Default value is 5000 milliseconds. + +
    +
    +
    +
    +

    All Manager Implementations

    +

    All Manager implementations allow nesting of a + <SessionIdGenerator> element. It defines + the behavior of session id generation. All implementations + of the SessionIdGenerator allow the + following attributes: +

    + + +

    The length of the session ID may be changed with the + sessionIdLength attribute. +

    +
    +
    +
    + +
    diff --git a/webapps/docs/config/cluster-membership.xml b/webapps/docs/config/cluster-membership.xml new file mode 100644 index 0000000..2ec0d70 --- /dev/null +++ b/webapps/docs/config/cluster-membership.xml @@ -0,0 +1,327 @@ + + + +]> + + + &project; + + + Filip Hanik + The Cluster Membership object + + + + +
    + +
    + +
    +

    + The membership component in the Apache Tribes Channel is responsible + for dynamic discovery of other members(nodes) in the cluster. + There are currently two different membership service, the org.apache.catalina.tribes.membership.McastService + and the org.apache.catalina.tribes.membership.StaticMembershipService. + The McastService builds a multicast based membership service + that sends UDP packets to multicast IP addresses. + The StaticMembershipService builds a unicast based membership + service that sends TCP packets to predefined member address. +

    +
    + +
    +

    + The default implementation of the cluster group notification is built on top of multicast heartbeats + sent using UDP packets to a multicast IP address. + Cluster members are grouped together by using the same multicast address/port combination. + Each member sends out a heartbeat with a given interval (frequency), and this + heartbeat is used for dynamic discovery. + In a similar fashion, if a heartbeat has not been received in a timeframe specified by dropTime + ms. a member is considered suspect and the channel and any membership listener will be notified. +

    +
    + + + +
    + + + +

    + The implementation of the membership component. + Two implementations available, org.apache.catalina.tribes.membership.McastService + and org.apache.catalina.tribes.membership.StaticMembershipService. +

    +
    +
    +
    + + + + + +

    + The value is org.apache.catalina.tribes.membership.McastService. + This implementation uses multicast heartbeats for member discovery. +

    +
    + +

    + The multicast address that the membership will broadcast its presence and listen + for other heartbeats on. The default value is 228.0.0.4 + Make sure your network is enabled for multicast traffic.
    + The multicast address, in conjunction with the port is what + creates a cluster group. To divide up your farm into several different group, or to + split up QA from production, change the port or the address +
    Previously known as mcastAddr. +

    +
    + +

    + The multicast port, the default value is 45564
    + The multicast port, in conjunction with the address is what + creates a cluster group. To divide up your farm into several different group, or to + split up QA from production, change the port or the address +

    +
    + +

    + The frequency in milliseconds in which heartbeats are sent out. The default value is 500 ms.
    + In most cases the default value is sufficient. Changing this value, simply changes the interval in between heartbeats. +

    +
    + +

    + The membership component will time out members and notify the Channel if a member fails to send a heartbeat within + a give time. The default value is 3000 ms. This means, that if a heartbeat is not received from a + member in that timeframe, the membership component will notify the cluster of this.
    + On a high latency network you may wish to increase this value, to protect against false positives.
    + Apache Tribes also provides a TcpFailureDetector that will + verify a timeout using a TCP connection when a heartbeat timeout has occurred. This protects against false positives. +

    +
    + +

    + Use this attribute if you wish to bind your multicast traffic to a specific network interface. + By default, or when this attribute is unset, it tries to bind to 0.0.0.0 and sometimes on multihomed hosts + this becomes a problem. +

    +
    + +

    + The time-to-live setting for the multicast heartbeats. + This setting should be a value between 0 and 255. The default value is VM implementation specific. +

    +
    + +

    + Apache Tribes has the ability to logically group members into domains, by using this domain attribute. + The org.apache.catalina.tribes.Member.getDomain() method returns the value specified here. +

    +
    + +

    + The sending and receiving of heartbeats is done on a single thread, hence to avoid blocking this thread forever, + you can control the SO_TIMEOUT value on this socket.
    + If a value smaller or equal to 0 is presented, the code will default this value to frequency +

    +
    + +

    + In case of a network failure, Java multicast socket don't transparently fail over, instead the socket will continuously + throw IOException upon each receive request. When recoveryEnabled is set to true, this will close the multicast socket + and open a new socket with the same properties as defined above.
    + The default is true.
    +

    +
    + +

    + When recoveryEnabled==true this value indicates how many + times an error has to occur before recovery is attempted. The default is + 10.
    +

    +
    + +

    + When recoveryEnabled==true this value indicates how long time (in milliseconds) + the system will sleep in between recovery attempts, until it recovers successfully. + The default is 5000 (5 seconds).
    +

    +
    + + +

    + Membership uses multicast, it will call java.net.MulticastSocket.setLoopbackMode(localLoopbackDisabled). + When localLoopbackDisabled==true multicast messages will not reach other nodes on the same local machine. + The default is false.
    +

    +
    + +
    + +
    + + + +

    When using the static membership service you must ensure that the + channelStartOptions attribute of the Cluster + element is set to the default value of 15.

    + + + +

    + The value is org.apache.catalina.tribes.membership.StaticMembershipService. +

    +
    + +

    + Timeout for attempting a TCP connection to address of predefined static member. + Default is 500 ms. +

    +
    + +

    + If members have failed to update their alive time within the given time, + this membership will notify the memberDisappeared event to cluster. + Default is 5000 ms. +

    +
    + +

    + Timeout for messages that used for member notification to/from othee nodes. + Default is 3000 ms. +

    +
    + +

    + If set to true, this membership service will start a local thread for + sending a ping message. if set to false, channel heartbeat + will send a ping message. Default is false. +

    +
    + +

    + If useThread == true, defines the interval of + sending a ping message. Default is 1000 ms. +

    +
    +
    +
    + +
    + +
    +

    + Static Membership Service allows nesting of a <LocalMember> + and <Member> element. +

    + +

    LocalMember:
    + Static member that is the local member of the static cluster group. +

    +

    Note: It is not necessary to explicitly configure the local member using the + <LocalMember> element. All cluster members, including the local member, may be defined using + the <Member> element and Tomcat work out which one is the local member. +

    + + + Only one implementation available:org.apache.catalina.tribes.membership.StaticMember + + + There is no need to set. + The value of this attribute inherits from the cluster receiver setting. + + + There is no need to set. + The value of this attribute inherits from the cluster receiver setting. + + + There is no need to set. + The value of this attribute inherits from the cluster receiver setting. + + + The logical cluster domain for that this static member listens for cluster messages. + Two different type of values are possible:
    + 1. Regular string values like "staging-domain" or "tomcat-cluster" will be converted into bytes + using ISO-8859-1 encoding. + 2. byte array in string form, for example {216,123,12,3}
    +
    + + A universally uniqueId for this static member. + The values must be 16 bytes in the following form:
    + 1. byte array in string form, for example {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}
    +
    +
    + +

    Member:
    + Static member that add to the static cluster group. +

    + + + Only one implementation available:org.apache.catalina.tribes.membership.StaticMember + + + The port that this static member listens to for cluster messages + + + The secure port this static member listens to for encrypted cluster messages + default value is -1, this value means the member is not listening on a secure port + + + The host (or network interface) that this static member listens for cluster messages. + Three different type of values are possible:
    + 1. IP address in the form of "216.123.1.23"
    + 2. Hostnames like "tomcat01.mydomain.com" or "tomcat01" as long as they resolve correctly
    + 3. byte array in string form, for example {216,123,12,3}
    +
    + + The logical cluster domain for that this static member listens for cluster messages. + Two different type of values are possible:
    + 1. Regular string values like "staging-domain" or "tomcat-cluster" will be converted into bytes + using ISO-8859-1 encoding.
    + 2. byte array in string form, for example {216,123,12,3}
    +
    + + A universally uniqueId for this static member. + The values must be 16 bytes in the following form:
    + 1. byte array in string form, for example {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}
    +
    +
    +
    + + +

    + + + + ]]> +

    +
    +
    + + + +
    diff --git a/webapps/docs/config/cluster-receiver.xml b/webapps/docs/config/cluster-receiver.xml new file mode 100644 index 0000000..461dd6c --- /dev/null +++ b/webapps/docs/config/cluster-receiver.xml @@ -0,0 +1,167 @@ + + + +]> + + + &project; + + + Filip Hanik + The Cluster Receiver object + + + + +
    + +
    + +
    +

    + The receiver component is responsible for receiving cluster messages. + As you might notice through the configuration, is that the receiving of messages + and sending of messages are two different components, this is different from many other + frameworks, but there is a good reason for it, to decouple the logic for how messages are sent from + how messages are received.
    + The receiver is very much like the Tomcat Connector, its the base of the thread pool + for incoming cluster messages. The receiver is straight forward, but all the socket settings + for incoming traffic are managed here. +

    +
    + +
    +

    + The receiver supports a non blocking org.apache.catalina.tribes.transport.nio.NioReceiver receiver.
    + Using the non blocking receiver allows you to with a very limited thread count to serve a large number of messages. + Usually the rule is to use 1 thread per node in the cluster for small clusters, and then depending on your message frequency + and your hardware, you'll find an optimal number of threads peak out at a certain number. +

    +
    + +
    + + + + The implementation of the receiver component. + org.apache.catalina.tribes.transport.nio.NioReceiver + is provided by Tomcat. + + + The address (network interface) to listen for incoming traffic. + Same as the bind address. The default value is auto and translates to + java.net.InetAddress.getLocalHost().getHostAddress(). + + + Possible values are true or false. + Set to true if you want the receiver to use direct bytebuffers when reading data + from the sockets. + + + The listen port for incoming data. The default value is 4000. + To avoid port conflicts the receiver will automatically bind to a free port within the range of + port <= bindPort < port+autoBind + So for example, if port is 4000, and autoBind is set to 10, then the receiver will open up + a server socket on the first available port in the range 4000-4009. + + + Default value is 100. + Use this value if you wish to automatically avoid port conflicts the cluster receiver will try to open a + server socket on the port attribute port, and then work up autoBind number of times. + + + The secure listen port. This port is SSL enabled. If this attribute is omitted no SSL port is opened up. + There default value is unset, meaning there is no SSL socket available. + + + The UDP listen port. If this attribute is omitted no UDP port is opened up. + There default value is unset, meaning there is no UDP listener available. + + + The value in milliseconds for the polling timeout in the NioReceiver. On older versions of the JDK + there have been bugs, that should all now be cleared out where the selector never woke up. + The default value is a very high 5000 milliseconds. + + + The maximum number of threads in the receiver thread pool. The default value is 15 + Adjust this value relative to the number of nodes in the cluster, the number of messages being exchanged and + the hardware you are running on. A higher value doesn't mean more efficiency, tune this value according to your + own test results. + + + Minimum number of threads to be created when the receiver is started up. Default value is 6 + + + Maximum number of milliseconds of until Idle thread terminates. Default value is 60000 milliseconds. + + + Boolean value for the socket OOBINLINE option. Possible values are true or false. + + + The receiver buffer size on the receiving sockets. Value is in bytes, the default value is 65536 bytes. + + + The sending buffer size on the receiving sockets. Value is in bytes, the default value is 25188 bytes. + + + The receive buffer size on the datagram socket. + Default value is 25188 bytes. + + + The send buffer size on the datagram socket. + Default value is 65536 bytes. + + + Boolean value for the socket SO_KEEPALIVE option. Possible values are true or false. + + + Boolean value to determine whether to use the SO_LINGER socket option. + Possible values are true or false. Default value is true. + + + Sets the SO_LINGER socket option time value. The value is in seconds. + The default value is 3 seconds. + + + Boolean value for the socket SO_REUSEADDR option. Possible values are true or false. + + + Boolean value for the socket TCP_NODELAY option. Possible values are true or false. + The default value is true + + + Sets the SO_TIMEOUT option on the socket. The value is in milliseconds and the default value is 3000 + milliseconds. + + + Boolean value whether to use a shared buffer pool of cached org.apache.catalina.tribes.io.XByteBuffer + objects. If set to true, the XByteBuffer that is used to pass a message up the channel, will be recycled at the end + of the requests. This means that interceptors in the channel must not maintain a reference to the object + after the org.apache.catalina.tribes.ChannelInterceptor#messageReceived method has exited. + + + + + + + + +
    + +
    diff --git a/webapps/docs/config/cluster-sender.xml b/webapps/docs/config/cluster-sender.xml new file mode 100644 index 0000000..9411ec7 --- /dev/null +++ b/webapps/docs/config/cluster-sender.xml @@ -0,0 +1,176 @@ + + + +]> + + + &project; + + + Filip Hanik + The Cluster Sender object + + + + +
    + +
    + +
    +

    + The channel sender component is responsible for delivering outgoing cluster messages over the network. + In the default implementation, org.apache.catalina.tribes.transport.ReplicationTransmitter, + the sender is a fairly empty shell with not much logic around a fairly complex <Transport> + component the implements the actual delivery mechanism. +

    +
    + +
    +

    + In the default transport implementation, org.apache.catalina.tribes.transport.nio.PooledParallelSender, + Apache Tribes implements what we like to call "Concurrent Parallel Delivery". + This means that we can send a message to more than one destination at the same time(parallel), and + deliver two messages to the same destination at the same time(concurrent). Combine these two and we have + "Concurrent Parallel Delivery". +

    +

    + When is this useful? The simplest example we can think of is when part of your code is sending a 10MB message, + like a war file being deployed, and you need to push through a small 10KB message, say a session being replicated, + you don't have to wait for the 10MB message to finish, as a separate thread will push in the small message + transmission at the same time. Currently there is no interrupt, pause or priority mechanism available, but check back soon. +

    +
    + +
    +

    + The nested element <Transport> is not required, but encouraged, as this is where + you would set all the socket options for the outgoing messages. Please see its attributes below. +

    +
    + +
    + + + + Required, only available implementation is org.apache.catalina.tribes.transport.ReplicationTransmitter + + + + + + + The implementation of the sender component. + org.apache.catalina.tribes.transport.nio.PooledParallelSender + is provided by Tomcat. + + + The receive buffer size on the socket. + Default value is 25188 bytes. + + + The send buffer size on the socket. + Default value is 65536 bytes. + + + The receive buffer size on the datagram socket. + Default value is 25188 bytes. + + + The send buffer size on the datagram socket. + Default value is 65536 bytes. + + + Possible values are true or false. + Set to true if you want the receiver to use direct bytebuffers when writing data + to the sockets. Default value is false + + + The number of requests that can go through the socket before the socket is closed, and reopened + for the next request. The default value is -1, which is unlimited. + + + The number of milliseconds a connection is kept open after its been opened. + The default value is -1, which is unlimited. + + + Sets the SO_TIMEOUT option on the socket. The value is in milliseconds and the default value is 3000 + milliseconds.(3 seconds) This timeout starts when a message send attempt is starting, until the transfer has been completed. + For the NIO sockets, this will mean, that the caller can guarantee that we will not attempt sending the message + longer than this timeout value. For the blocking IO implementation, this translated directly to the soTimeout.
    + A timeout will not spawn a retry attempt, in order to guarantee the return of the application thread. +
    + + How many times do we retry a failed message, that received a IOException at the socket level. + The default value is 1, meaning we will retry a message that has failed once. + In other words, we will attempt a message send no more than twice. One is the original send, and one is the + maxRetryAttempts. + + + Boolean value for the socket OOBINLINE option. Possible values are true or false. + + + Boolean value for the socket SO_KEEPALIVE option. Possible values are true or false. + + + Boolean value to determine whether to use the SO_LINGER socket option. + Possible values are true or false. Default value is true. + + + Sets the SO_LINGER socket option time value. The value is in seconds. + The default value is 3 seconds. + + + Boolean value for the socket SO_REUSEADDR option. Possible values are true or false. + + + Sets the traffic class level for the socket, the value is between 0 and 255. + Default value is int soTrafficClass = 0x04 | 0x08 | 0x010; + Different values are defined in + java.net.Socket#setTrafficClass(int). + + + Boolean value for the socket TCP_NODELAY option. Possible values are true or false. + The default value is true + + + Boolean value, default value is true. + If set to true, the sender will throw a org.apache.catalina.tribes.RemoteProcessException + when we receive a negative ack from the remote member. + Set to false, and Tribes will treat a positive ack the same way as a negative ack, that the message was received. + +
    +
    + + + + The maximum number of concurrent connections from A to B. + The value is based on a per-destination count. + The default value is 25 + + + The maximum number of milliseconds that the senderPool will wait when + there are no available senders. The default value is 3000 + milliseconds.(3 seconds). + + + +
    + +
    diff --git a/webapps/docs/config/cluster-valve.xml b/webapps/docs/config/cluster-valve.xml new file mode 100644 index 0000000..1a1b3de --- /dev/null +++ b/webapps/docs/config/cluster-valve.xml @@ -0,0 +1,172 @@ + + + +]> + + + &project; + + + Filip Hanik + The Cluster Valve object + + + + +
    + +
    + +
    +

    + A cluster valve is no different from any other Tomcat Valve. + The cluster valves are interceptors in the invocation chain for HTTP requests, and the clustering implementation + uses these valves to make intelligent decision around data and when data should be replicated. +

    +

    + A cluster valve must implement the org.apache.catalina.ha.ClusterValve interface. + This is a simple interface that extends the org.apache.catalina.Valve interface. +

    +
    + +
    + The ReplicationValve will notify the cluster at the end of an HTTP request + so that the cluster can make a decision whether there is data to be replicated or not. + + + + Set value to org.apache.catalina.ha.tcp.ReplicationValve + + + For known file extensions or urls, you can use this Valve to notify the + cluster that the session has not been modified during this request and + the cluster doesn't have to probe the session managers for changes. If + the request matches this filter pattern, the cluster assumes there has + been no session change. An example filter would look like + filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt" + . The filter is a regular expression using + java.util.regex. If no filter pattern specified or if the + filter pattern is configured to be the empty string, no filtering will + take place. + + + Boolean value, so to true, and the replication valve will insert a request attribute with the name + defined by the primaryIndicatorName attribute. + The value inserted into the request attribute is either Boolean.TRUE or + Boolean.FALSE + + + Default value is org.apache.catalina.ha.tcp.isPrimarySession + The value defined here is the name of the request attribute that contains the boolean value + if the session is primary on this server or not. + + + Boolean value. Set to true if you want the valve to collect request statistics. + Default value is false + + + +
    + +
    + In case of a mod_jk failover, the JvmRouteBinderValve will replace the + jvmWorker attribute in the session Id, to make future requests stick to this + node. If you want fallback capability, don't enable this valve, but if you want your failover to stick, + and for mod_jk not to have to keep probing the node that went down, you use this valve. + + + + org.apache.catalina.ha.session.JvmRouteBinderValve + + + Default value is true + Runtime attribute to turn on and off turn over of the session's jvmRoute value. + + + Old sessionid before failover is registered in request attributes with this attribute. + Default attribute name is org.apache.catalina.ha.session.JvmRouteOriginalSessionID. + + + +
    + +
    + The ClusterSingleSignOn supports feature of single sign on in cluster. + By using ClusterSingleSignOn, the security identity authenticated + by one web application is recognized by other web applications on the same virtual host, + and it is propagated to other nodes in the cluster. + +

    See the Single Sign On special + feature on the Host element for more information.

    + +

    Note: ClusterSingleSignOn can be configured at host level cluster only. +

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.ha.authenticator.ClusterSingleSignOn.

    +
    + +

    Sets the host domain to be used for sso cookies.

    +
    + +

    The Valve uses a replicated map. You can setup the flag for how this + map sends messages. The default value is 6 (synchronous). + Note that if you use asynchronous messaging it is possible for update + messages to be processed by the receiving node in a different order to + the order in which they were sent.

    +
    + +

    Default false. Flag to determine whether each request needs to be + reauthenticated to the security Realm. If "true", this + Valve uses cached security credentials (username and password) to + reauthenticate to the Realm each request associated + with an SSO session. If "false", the Valve can itself authenticate + requests based on the presence of a valid SSO cookie, without + rechecking with the Realm.

    +
    + +

    The Valve uses a replicated map. This is the timeout for messages + that transfer state to/from the other nodes in the cluster. If not + specified, a default value of 15000 milliseconds is used. +

    +
    + +

    Set to true if you wish this Valve to fail if the + underlying replication fails to start. If the Valve fails, then the + associated container will fail to start. If you set this attribute to + false, and the underlying replications fails to start, the Valve will + start and it will attempt to join the cluster and start replication as + part of the heartbeat process. If not specified, the default value of + false is used.

    +
    + + The timeout for a ping message. If a remote map does not respond within + this timeout period, its regarded as disappeared. + Default value is 5000 milliseconds. + +
    +
    +
    + + + +
    diff --git a/webapps/docs/config/cluster.xml b/webapps/docs/config/cluster.xml new file mode 100644 index 0000000..409aac4 --- /dev/null +++ b/webapps/docs/config/cluster.xml @@ -0,0 +1,214 @@ + + + +]> + + + &project; + + + Filip Hanik + The Cluster object + + + + +
    + +
    + +
    +

    + The tomcat cluster implementation provides session replication, context attribute replication and + cluster wide WAR file deployment. + While the Cluster configuration is fairly complex, the default configuration will work + for most people out of the box.

    + The Tomcat Cluster implementation is very extensible, and hence we have exposed a myriad of options, + making the configuration seem like a lot, but don't lose faith, instead you have a tremendous control + over what is going on.

    +
    +
    + +

    The cluster implementation is written on the basis that a secure, trusted +network is used for all of the cluster related network traffic. It is not safe +to run a cluster on a insecure, untrusted network.

    + +

    There are many options for providing a secure, trusted network for use by a +Tomcat cluster. These include:

    +
      +
    • private LAN
    • +
    • a Virtual Private Network (VPN)
    • +
    • IPSEC
    • +
    + +

    The EncryptInterceptor +provides confidentiality and integrity protection but it does not protect +against all risks associated with running a Tomcat cluster on an untrusted +network, particularly DoS attacks.

    + +
    +
    +

    + You can place the <Cluster> element inside either the <Engine> + container or the <Host> container.
    + Placing it in the engine, means that you will support clustering in all virtual hosts of Tomcat, + and share the messaging component. When you place the <Cluster> inside the <Engine> + element, the cluster will append the host name of each session manager to the managers name so that two contexts with + the same name but sitting inside two different hosts will be distinguishable. +

    +
    +
    +

    To configure context attribute replication, simply do this by swapping out the context implementation + used for your application context.

    + <Context className="org.apache.catalina.ha.context.ReplicatedContext"/> +

    + This context extends the Tomcat StandardContext + so all the options from the base implementation are valid. +

    +
    +
    +

    Manager:
    + The session manager element identifies what kind of session manager is used in this cluster implementation. + This manager configuration is identical to the one you would use in a regular <Context> configuration. +
    The default value is the org.apache.catalina.ha.session.DeltaManager that is closely coupled with + the SimpleTcpCluster implementation. Other managers like the org.apache.catalina.ha.session.BackupManager + are/could be loosely coupled and don't rely on the SimpleTcpCluster for its data replication. +

    +

    Channel:
    + The Channel and its sub components are all part of the IO layer + for the cluster group, and is a module in it's own that we have nick named "Tribes" +
    + Any configuring and tuning of the network layer, the messaging and the membership logic + will be done in the channel and its nested components. + You can always find out more about Apache Tribes +

    +

    Valve:
    + The Tomcat Cluster implementation uses Tomcat Valves to + track when requests enter and exit the servlet container. It uses these valves to be able to make + intelligent decisions on when to replicate data, which is always at the end of a request. +

    +

    Deployer:
    + The Deployer component is the Tomcat Farm Deployer. It allows you to deploy and undeploy applications + cluster wide. +

    +

    ClusterListener:
    + ClusterListener's are used to track messages sent and received using the SimpleTcpCluster. + If you wish to track messages, you can add a listener here, or you can add a valve to the channel object. +

    +
    + +
    + + + +

    The main cluster class, currently only one is available, + org.apache.catalina.ha.tcp.SimpleTcpCluster +

    +
    + +

    The Tribes channel send options, default is 8.
    + This option is used to set the flag that all messages sent through the + SimpleTcpCluster uses. The flag decides how the messages are sent, and is a simple logical OR.

    + + int options = Channel.SEND_OPTIONS_ASYNCHRONOUS | + Channel.SEND_OPTIONS_SYNCHRONIZED_ACK | + Channel.SEND_OPTIONS_USE_ACK; +

    Some of the values are:
    + Channel.SEND_OPTIONS_SYNCHRONIZED_ACK = 0x0004
    + Channel.SEND_OPTIONS_ASYNCHRONOUS = 0x0008
    + Channel.SEND_OPTIONS_USE_ACK = 0x0002
    + So to use ACK and ASYNC messaging, the flag would be 10 (8+2) +
    + Note that if you use ASYNC messaging it is possible for update messages + for a session to be processed by the receiving nodes in a different order + to the order in which they were sent. +

    +

    + The various channelSendOptions values offer a tradeoff + between throughput on the sending node and the reliability of + replication should the sending or receiving node(s) fail. Here are + some common options. "Message" could be any message sent between nodes, + but only session-change messages are considered, here. +

    +

    + channelSendOptions="8" / channelSendOptions="async" + As far as the sender is concerned, the message is "sent" + as soon as it has been placed in the queue on the sender for + transmission to the other nodes. The message may not reach any or all + of the recipient nodes and may not be successfully processed on any + nodes that it does reach. This option offers the highest throughput on + the sender but the least reliability, as the triggering request will + complete without any knowledge of the success/failure of the session + replication. +

    +

    + channelSendOptions="2" / channelSendOptions="use_ack" + The sender will block the completion of the current request until all + of the receiving nodes have acknowledged that they have received the + message, but have not necessarily processed that message. This option + will result in lower throughput on the sending node, because the message + must be transmitted and the acknowledgement received, but the + reliability is greater than the asynchronous model. +

    +

    + channelSendOptions="6" / channelSendOptions="sync,use_ack" + The sender will block the completion of the current request until + all of the receiving nodes have acknowledged that they have received + and processed the message. This option will have the lowest + throughput (of these three) but the greatest reliability. +

    +

    + You may also set these options as a comma separated string, e.g. "async, multicast", which + will be translated into Channel.SEND_OPTIONS_ASYNCHRONOUS | Channel.SEND_OPTIONS_MULTICAST +
    + The valid option names are "asynchronous" (alias "async"), "byte_message" (alias "byte") + , "multicast", "secure", "synchronized_ack" (alias "sync"), "udp", "use_ack" +

    +
    + +

    Sets the start and stop flags for the <Channel> object used by the cluster. + The default is Channel.DEFAULT which starts all the channel services, such as + sender, receiver, membership sender and membership receiver. + The following flags are available today:

    + Channel.DEFAULT = Channel.SND_RX_SEQ (1) | + Channel.SND_TX_SEQ (2) | + Channel.MBR_RX_SEQ (4) | + Channel.MBR_TX_SEQ (8); +

    When using the static membership service + org.apache.catalina.tribes.membership.StaticMembershipService + you must ensure that this attribute is configured to use the default + value.

    +
    + + +

    Flag whether invoke channel heartbeat at container background thread. Default value is false. + Enable this flag don't forget to disable the channel heartbeat thread. +

    +
    + + +

    Flag whether notify LifecycleListeners if all ClusterListener couldn't accept channel message. + Default value is false. +

    +
    +
    +
    +
    + +
    diff --git a/webapps/docs/config/context.xml b/webapps/docs/config/context.xml new file mode 100644 index 0000000..5097e59 --- /dev/null +++ b/webapps/docs/config/context.xml @@ -0,0 +1,1481 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + The Context Container + + + + +
    + +
    + +
    + +

    + The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat. +

    + +

    The Context element represents a web + application, which is run within a particular virtual host. + Each web application is based on a Web Application Archive + (WAR) file, or a corresponding directory containing the corresponding + unpacked contents, as described in the Servlet Specification (version + 2.2 or later). For more information about web application archives, + you can download the + Servlet + Specification, and review the Tomcat + Application Developer's Guide.

    + +

    The web application used to process each HTTP request is selected + by Catalina based on matching the longest possible prefix of the + Request URI against the context path of each defined Context. + Once selected, that Context will select an appropriate servlet to + process the incoming request, according to the servlet mappings defined + by the web application deployment.

    + +

    You may define as many Context elements as you + wish. Each such Context MUST have a unique context name within a virtual + host. The context path does not need to be unique (see parallel + deployment below). In addition, a Context must be present with a + context path equal to + a zero-length string. This Context becomes the default + web application for this virtual host, and is used to process all + requests that do not match any other Context's context path.

    + + +

    You may deploy multiple versions of a web application with the same + context path at the same time. The rules used to match requests to a + context version are as follows: +

    +
      +
    • If no session information is present in the request, use the latest + version.
    • +
    • If session information is present in the request, check the session + manager of each version for a matching session and if one is found, use that + version.
    • +
    • If session information is present in the request but no matching session + can be found, use the latest version.
    • +
    +

    The Host may be configured (via the + undeployOldVersions) to remove old versions deployed in this way + once they are no longer in use.

    +
    + + +

    When autoDeploy or deployOnStartup operations + are performed by a Host, the name and context path of the web application are + derived from the name(s) of the file(s) that define(s) the web application. + Consequently, the context path may not be defined in a + META-INF/context.xml embedded in the application and there is a + close relationship between the context name, context path, + context version and the base file name (the name minus any + .war or .xml extension) of the file.

    + +

    If no version is specified then the context name is always the + same as the context path. If the context path is the empty + string then the base name will be ROOT (always in upper case) + otherwise the base name will be the context path with the + leading '/' removed and any remaining '/' characters replaced with '#'.

    + +

    If a version is specified then the context path remains unchanged + and both the context name and the base name have the string + '##' appended to them followed by the version identifier.

    + +

    Some examples of these naming conventions are given below.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Context PathContext VersionContext NameBase File NameExample File Names (.xml, .war & directory)
    /fooNone/foofoofoo.xml, foo.war, foo
    /foo/barNone/foo/barfoo#barfoo#bar.xml, foo#bar.war, foo#bar
    Empty StringNoneEmpty StringROOTROOT.xml, ROOT.war, ROOT
    /foo42/foo##42foo##42foo##42.xml, foo##42.war, foo##42
    /foo/bar42/foo/bar##42foo#bar##42foo#bar##42.xml, foo#bar##42.war, foo#bar##42
    Empty String42##42ROOT##42ROOT##42.xml, ROOT##42.war, ROOT##42
    + +

    The version component is treated as a String both for + performance reasons and to allow flexibility in versioning schemes. String + comparisons are used to determine version order. If version is not specified, + it is treated as the empty string. + Therefore, + foo.war will be treated as an earlier version than + foo##11.war and + foo##11.war will be treated as an earlier version than + foo##2.war. If using a purely numerical versioning scheme it is + recommended that zero padding is used so that foo##002.war is + treated as an earlier version than foo##011.war. +

    + +

    If you want to deploy a WAR file or a directory using a context path that + is not related to the base file name then one of the following options must + be used to prevent double-deployment: +

    +
      +
    • Disable autoDeploy and deployOnStartup and define all + Contexts in server.xml
    • +
    • Locate the WAR and/or directory outside of the Host's + appBase and use a context.xml file with a + docBase attribute to define it.
    • +
    +
    + + +

    It is NOT recommended to place <Context> elements directly in the + server.xml file. This is because it makes modifying the + Context configuration more invasive since the main + conf/server.xml file cannot be reloaded without restarting + Tomcat. Default Context elements (see below) will also + overwrite the configuration of any <Context> elements + placed directly in server.xml. To prevent this, the override + attribute of the <Context> element defined in server.xml should be set + to true.

    + +

    Individual Context elements may be explicitly defined: +

    +
      +
    • In an individual file at /META-INF/context.xml inside the + application files. Optionally (based on the Host's copyXML attribute) + this may be copied to + $CATALINA_BASE/conf/[enginename]/[hostname]/ and renamed to + application's base file name plus a ".xml" extension.
    • +
    • In individual files (with a ".xml" extension) in the + $CATALINA_BASE/conf/[enginename]/[hostname]/ directory. + The context path and version will be derived from the base name of the file + (the file name less the .xml extension). This file will always take precedence + over any context.xml file packaged in the web application's META-INF + directory.
    • +
    • Inside a Host element in the main + conf/server.xml.
    • +
    + +

    Default Context elements may be defined that apply to + multiple web applications. Configuration for an individual web application + will override anything configured in one of these defaults. Any nested + elements, e.g. <Resource> elements, that are defined in a default + Context will be created once for each + Context to which the default applies. They will not be + shared between Context elements. +

    +
      +
    • In the $CATALINA_BASE/conf/context.xml file: + the Context element information will be loaded by all web applications.
    • +
    • In the + $CATALINA_BASE/conf/[enginename]/[hostname]/context.xml.default + file: the Context element information will be loaded by all web applications + of that host.
    • +
    + +

    With the exception of server.xml, files that define Context + elements may only define a single Context element. +

    + +

    In addition to explicitly specified Context elements, there are + several techniques by which Context elements can be created automatically + for you. See + Automatic Application Deployment and + User Web Applications + for more information.

    + +

    To define multiple contexts that use a single WAR file or directory, + use one of the options described in the Naming + section above for creating a Context that has a path + that is not related to the base file name.

    +
    +
    + + +
    + + + +

    All implementations of Context + support the following attributes:

    + + + + +

    Set to true if Tomcat should automatically parse + multipart/form-data request bodies when HttpServletRequest.getPart* + or HttpServletRequest.getParameter* is called, even when the + target servlet isn't marked with the @MultipartConfig annotation + (See Servlet Specification 3.0, Section 3.2 for details). + Note that any setting other than false causes Tomcat + to behave in a way that is not technically spec-compliant. + The default is false.

    +
    + + +

    Tomcat normalises sequences of multiple / characters in + a URI to a single /. This is for consistency with the + behaviour of file systems as URIs are often translated to file system + paths. As a result, the return value of + HttpServletRequest#getContextPath() is expected to start + with multiple / characters for some URIs. This will cause + problems if this value is used directly with + HttpServletResponse#sendRedirect() as redirect paths that + start with // are treated as protocol relative redirects. + To avoid potential issues, Tomcat will collapse multiple leading + / characters at the start of the return value for + HttpServletRequest#getContextPath() to a single + /. This attribute has a default value of false + which enables the collapsing of multiple / characters. To + disable this behaviour, set this attribute to true.

    +
    + + +

    The absolute path to the alternative deployment descriptor for this + context. This overrides the default deployment descriptor located at + /WEB-INF/web.xml.

    +
    + + +

    If this is true, every request that is associated with a + session will cause the session's last accessed time to be updated + regardless of whether or not the request explicitly accesses the session.

    +

    If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + true, the default of this setting will be true, + else the default value will be false.

    +
    + + +

    This value represents the delay in seconds between the + invocation of the backgroundProcess method on this context and + its child containers, including all wrappers. + Child containers will not be invoked if their delay value is not + negative (which would mean they are using their own processing + thread). Setting this to a positive value will cause + a thread to be spawn. After waiting the specified amount of time, + the thread will invoke the backgroundProcess method on this host + and all its child containers. A context will use background + processing to perform session expiration and class monitoring for + reloading. If not specified, the default value for this attribute is + -1, which means the context will rely on the background processing + thread of its parent host.

    +
    + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Context interface. + If not specified, the standard value (defined below) will be used.

    +
    + + +

    The regular expression that specifies which container provided SCIs + should be filtered out and not used for this context. Matching uses + java.util.regex.Matcher.find() so the regular expression + only has to match a sub-string of the fully qualified class name of the + container provided SCI for it to be filtered out. If not specified, + no filtering will be applied.

    +
    + + +

    If this is true then the path passed to + ServletContext.getResource() or + ServletContext.getResourceAsStream() must start with + "/". If false, code like + getResource("myfolder/myresource.txt") will work as Tomcat + will prepend "/" to the provided path.

    +

    If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + true, the default of this setting will be true, + else the default value will be false.

    +
    + + +

    Set to true if you want cookies to be used for + session identifier communication if supported by the client (this + is the default). Set to false if you want to disable + the use of cookies for session identifier communication, and rely + only on URL rewriting by the application.

    +
    + + +

    Set to true if Tomcat should attempt to create the + temporary upload location specified in the MultipartConfig + for a Servlet if the location does not already exist. If not specified, + the default value of false will be used.

    +
    + + +

    Set to true if you want calls within this application + to ServletContext.getContext() to successfully return a + request dispatcher for other web applications running on this virtual + host. Set to false (the default) in security + conscious environments, to make getContext() always + return null.

    +
    + + +

    If this is true then any wrapped request or response + object passed to an application dispatcher will be checked to ensure that + it has wrapped the original request or response.

    +

    If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + true, the default of this setting will be true, + else the default value will be false.

    +
    + + +

    The Document Base (also known as the Context + Root) directory for this web application, or the pathname + to the web application archive file (if this web application is + being executed directly from the WAR file). You may specify + an absolute pathname for this directory or WAR file, or a pathname + that is relative to the appBase directory of the + owning Host.

    +

    The value of this field must not be set unless the Context element is + defined in server.xml or the docBase is not located under + the Host's appBase.

    +

    If a symbolic link is used for docBase then changes to + the symbolic link will only be effective after a Tomcat restart or + by undeploying and redeploying the context. A context reload is not + sufficient.

    +
    + + +

    Controls whether paths used in calls to obtain a request dispatcher + ares expected to be encoded. This affects both how Tomcat handles calls + to obtain a request dispatcher as well as how Tomcat generates paths + used to obtain request dispatchers internally. If not specified, the + default value of true is used. When encoding/decoding paths + for a request dispatcher, UTF-8 is always used.

    +
    + + +

    Set to true to have the context fail its startup if any + servlet that has load-on-startup >=0 fails its own startup.

    +

    If not specified, the attribute of the same name in the parent Host + configuration is used if specified. Otherwise the default value of + false is used.

    +
    + + +

    Set to true to fire any configured + ServletRequestListeners when Tomcat forwards a request. This is + primarily of use to users of CDI frameworks that use + ServletRequestListeners to configure the necessary environment for a + request. If not specified, the default value of false is + used.

    +
    + + +

    Set to true if you want the effective web.xml used for a + web application to be logged (at INFO level) when the application + starts. The effective web.xml is the result of combining the + application's web.xml with any defaults configured by Tomcat and any + web-fragment.xml files and annotations discovered. If not specified, the + default value of false is used.

    +
    + + +

    If enabled, requests for a web application context root will be + redirected (adding a trailing slash) if necessary by the Mapper rather + than the default Servlet. This is more efficient but has the side effect + of confirming that the context path exists. If not specified, the + default value of true is used.

    +
    + + +

    If enabled, requests for a web application directory will be + redirected (adding a trailing slash) if necessary by the Mapper rather + than the default Servlet. This is more efficient but has the side effect + of confirming that the directory is exists. If not specified, the + default value of false is used.

    +
    + + +

    Set to true to ignore any settings in both the global + or Host default contexts. By default, settings + from a default context will be used but may be overridden by a setting + the same attribute explicitly for the Context.

    +
    + + +

    When set to true annotation scanning will be performed + using the utility executor. It will allow processing scanning in + parallel which may improve deployment type at the expense of higher + server load. If not specified, the default of false is + used.

    +
    + + +

    The context path of this web application, which is + matched against the beginning of each request URI to select the + appropriate web application for processing. All of the context paths + within a particular Host must be unique. + If you specify a context path of an empty string (""), you are + defining the default web application for this Host, which + will process all requests not assigned to other Contexts.

    +

    This attribute must only be used when statically defining a Context + in server.xml. In all other circumstances, the path will be inferred + from the filenames used for either the .xml context file or the + docBase. +

    +

    Even when statically defining a Context in server.xml, this attribute + must not be set unless either the docBase is not located + under the Host's appBase or + both deployOnStartup and autoDeploy are + false. If this rule is not followed, double deployment is + likely to result.

    +
    + + +

    When set to true and the user presents credentials for a + resource that is not protected by a security constraint, if the + authenticator supports preemptive authentication (the standard + authenticators provided with Tomcat do) then the user' credentials + will be processed. If not specified, the default of false is + used. +

    +
    + + +

    Set to true to allow this context to use container + servlets, like the manager servlet. Use of the privileged + attribute will change the context's parent class loader to be the + Server class loader rather than the Shared class + loader. Note that in a default installation, the Common class + loader is used for both the Server and the Shared + class loaders.

    +
    + + +

    Set to true if you want Catalina to monitor classes in + /WEB-INF/classes/ and /WEB-INF/lib for + changes, and automatically reload the web application if a change + is detected. This feature is very useful during application + development, but it requires significant runtime overhead and is + not recommended for use on deployed production applications. That's + why the default setting for this attribute is false. You + can use the Manager web + application, however, to trigger reloads of deployed applications + on demand.

    +
    + + +

    Comma separated list of Servlet names (as used in + /WEB-INF/web.xml) that expect a resource to be present. + Ensures that welcome files associated with Servlets that expect a + resource to be present (such as the JSP Servlet) are not used when there + is no resource present. This prevents issues caused by the clarification + of welcome file mapping in section 10.10 of the Servlet 3.0 + specification. If the + org.apache.catalina.STRICT_SERVLET_COMPLIANCE + system property is set to + true, the default value of this attribute will be the empty + string, else the default value will be jsp.

    +
    + + +

    If true, redirect responses will include a short + response body that includes details of the redirect as recommended by + RFC 2616. This is disabled by default since including a response body + may cause problems for some application component such as compression + filters.

    +
    + + +

    The domain to be used for all session cookies created for this + context. If set, this overrides any domain set by the web application. + If not set, the value specified by the web application, if any, will be + used.

    +
    + + +

    The name to be used for all session cookies created for this + context. If set, this overrides any name set by the web application. + If not set, the value specified by the web application, if any, will be + used, or the name JSESSIONID if the web application does + not explicitly set one.

    +
    + + +

    The path to be used for all session cookies created for this + context. If set, this overrides any path set by the web application. + If not set, the value specified by the web application will be used, or + the context path used if the web application does not explicitly set + one. To configure all web application to use an empty path (this can be + useful for portlet specification implementations) set this attribute to + / in the global CATALINA_BASE/conf/context.xml + file.

    +

    Note: Once one web application using + sessionCookiePath="/" obtains a session, all + subsequent sessions for any other web application in the same host also + configured with sessionCookiePath="/" will always + use the same session ID. This holds even if the session is invalidated + and a new one created. This makes session fixation protection more + difficult and requires custom, Tomcat specific code to change the + session ID shared by the multiple applications.

    +
    + + +

    Some browsers, such as Internet Explorer, Safari and Edge, will send + a session cookie for a context with a path of /foo with a + request to /foobar in violation of RFC6265. This could + expose a session ID from an application deployed at /foo to + an application deployed at /foobar. If the application + deployed at /foobar is untrusted, this could create a + security risk. However, it should be noted that RFC 6265, section 8.5 + makes clear that path alone should not be view as sufficient to prevent + untrusted applications accessing cookies from other applications. To + mitigate this risk, this attribute may be set to true and + Tomcat will add a trailing slash to the path associated with the session + cookie so, in the above example, the cookie path becomes /foo/. However, + with a cookie path of /foo/, browsers will no longer send the cookie + with a request to /foo. This should not be a problem unless there is a + servlet mapped to /*. In this case this attribute will need to be set to + false to disable this feature. The default value for this + attribute is false.

    +
    + + +

    If the value of this flag is true, Catalina will + suspend wrapped responses after a forward, instead of closing them. + If not specified, the default value of the flag is + false.

    +
    + + +

    Set to false if Tomcat should not read any + additional request body data for aborted uploads and instead abort the + client connection. This setting is used in the following situations: +

    +
      +
    • the size of the request body is larger than the + maxPostSize configured in the connector
    • +
    • the size limit of a MultiPart upload is reached
    • +
    • the servlet sets the response status to 413 (Request Entity Too + Large)
    • +
    +

    + Not reading the additional data will free the request processing thread + more quickly. Unfortunately most HTTP clients will not read the response + if they cannot write the full request.

    +

    The default is true, so additional data will be + read.

    +

    Note if an error occurs during the request processing that triggers + a 5xx response, any unread request data will always be ignored and the + client connection will be closed once the error response has been + written.

    +
    + + +

    If the value of this flag is true, the bytes output to + System.out and System.err by the web application will be redirected to + the web application logger. If not specified, the default value + of the flag is false.

    +
    + + +

    If the value of this flag is true, the TLD files + will be XML validated on context startup. If the + org.apache.catalina.STRICT_SERVLET_COMPLIANCE + system property is set to + true, the default value of this attribute will be + true, else the default value will be false. + Setting this attribute to true will incur a performance + penalty.

    +
    + + +

    DEPRECATED: If this is true then a bloom filter will be + used to speed up archive lookups. This can be beneficial to the deployment + speed to web applications that contain very large amount of JARs.

    +

    If not specified, the default value of false will be + used.

    +

    This value can be overridden by archiveIndexStrategy in + Resources

    +
    + + +

    Should the HttpOnly flag be set on session cookies to prevent client + side script from accessing the session ID? Defaults to + true.

    +
    + + +

    Should the Partitioned flag be set on session cookies? Defaults to false.

    +

    Note: The name of the attribute used to indicate a partitioned cookie as part of + CHIPS is not defined by an RFC and + may change in a non-backwards compatible way once equivalent functionality is included in an RFC.

    +
    + + +

    Controls whether HTTP 1.1 and later location headers generated by a + call to + jakarta.servlet.http.HttpServletResponse#sendRedirect(String) + will use relative or absolute redirects. Relative redirects are more + efficient but may not work with reverse proxies that change the context + path. It should be noted that it is not recommended to use a reverse + proxy to change the context path because of the multiple issues it + creates. Absolute redirects should work with reverse proxies that change + the context path but may cause issues with the + org.apache.catalina.filters.RemoteIpFilter if the filter is + changing the scheme and/or port. If the + org.apache.catalina.STRICT_SERVLET_COMPLIANCE + system property is set to + true, the default value of this attribute will be + false, else the default value will be true. +

    +
    + + +

    When a client provides the ID for a new session, this attribute + controls whether that ID is validated. The only use case for using a + client provided session ID is to have a common session ID across + multiple web applications. Therefore, any client provided session ID + should already exist in another web application. If this check is + enabled, the client provided session ID will only be used if the session + ID exists in at least one other web application for the current host. + Note that the following additional tests are always applied, + irrespective of this setting:

    +
      +
    • The session ID is provided by a cookie
    • +
    • The session cookie has a path of {@code /}
    • +
    +

    If not specified, the default value of true will be + used.

    +
    + + +

    Java class name of the org.apache.catalina.Wrapper + implementation class that will be used for servlets managed by this + Context. If not specified, a standard default value will be used.

    +
    + + +

    If the value of this flag is true, the parsing of + web.xml, web-fragment.xml, + tomcat-web.xml, *.tld, *.jspx, + *.tagx and tagPlugins.xml files for this web + application will not permit external entities to be loaded. If not + specified, the default value of true will be used.

    +
    + + +

    If the value of this flag is true, the parsing of + web.xml, web-fragment.xml and + tomcat-web.xml files for this web application will be + namespace-aware. Note that *.tld, *.jspx and + *.tagx files are always parsed using a namespace-aware + parser and that the tagPlugins.xml file (if any) is never + parsed using a namespace-aware parser. Note also that if you turn this + flag on, you should probably also turn xmlValidation on. If + the org.apache.catalina.STRICT_SERVLET_COMPLIANCE + system property is set to + true, the default value of this attribute will be + true, else the default value will be false. + Setting this attribute to true will incur a performance + penalty.

    +
    + + +

    If the value of this flag is true, the parsing of + web.xml, web-fragment.xml and + tomcat-web.xml files for this web application will use a + validating parser. If the + org.apache.catalina.STRICT_SERVLET_COMPLIANCE + system property is set to + true, the default value of this attribute will be + true, else the default value will be false. + Setting this attribute to true will incur a performance + penalty.

    +
    + +
    + +
    + + + + +

    The standard implementation of Context is + org.apache.catalina.core.StandardContext. + It supports the following additional attributes (in addition to the + common attributes listed above):

    + + + + +

    This attribute controls if, in addition to static resources being + served from META-INF/resources inside web application JAR + files, static resources are also served from + WEB-INF/classes/META-INF/resources. This only applies to + web applications with a major version of 3 or higher. Since this is a + proprietary extension to the Servlet 3 specification, it is disabled by + default. To enable this feature, set the attribute to true. +

    +
    + + +

    If true, Tomcat will prevent any file locking. + This will significantly impact startup time of applications, + but allows full webapp hot deploy and undeploy on platforms + or configurations where file locking can occur. + If not specified, the default value is false.

    + +

    Please note that setting this to true has some side + effects, including the disabling of JSP reloading in a running server: + see + Bugzilla 37668.

    + +

    Please note that setting this flag to true in + applications that are outside the appBase for the Host (the + webapps directory by default) will cause the application to + be deleted on Tomcat shutdown. You probably don't want + to do this, so think twice before setting antiResourceLocking=true on a + webapp that's outside the appBase for its Host.

    +
    + + +

    If true and an sun.net.www.http.HttpClient + keep-alive timer thread has been started by this web application and is + still running, Tomcat will change the context class loader for that + thread from the web application class loader to the parent of the web + application class loader to prevent a memory leak. Note that the + keep-alive timer thread will stop on its own once the keep-alives all + expire however, on a busy system that might not happen for some time. If + not specified, the default value of true will be used.

    +
    + + +

    If true, when the web application is stopped Tomcat + looks for SoftReferences to classes loaded by the web + application in the ObjectStreamClass class used for + serialization and clears any SoftReferences it finds. This + feature uses reflection to identify the SoftReferences and + therefore requires that the command line option + -XaddExports:java.base/java.io=ALL-UNNAMED is set. If not + specified, the default value of true will be used.

    +

    The memory leak associated with ObjectStreamClass has + been fixed in Java 19 onwards, Java 17.0.4 onwards and Java 11.0.16 + onwards. The check will be disabled when running on a version + of Java that contains the fix.

    +
    + + +

    If true, Tomcat looks for memory leaks associated with + RMI Targets and clears any it finds. This feature uses reflection to + identify the leaks and therefore requires that the command line option + -XaddExports:java.rmi/sun.rmi.transport=ALL-UNNAMED is set. + Applications without memory leaks should operate correctly with this + attribute set to false. If not specified, the default value + of true will be used.

    +
    + + +

    If true, Tomcat attempts to terminate threads that have + been started by the web application. Stopping threads is performed via + the deprecated (for good reason) Thread.stop() method and + is likely to result in instability. As such, enabling this should be + viewed as an option of last resort in a development environment and is + not recommended in a production environment. If not specified, the + default value of false will be used. If this feature is + enabled, web applications may take up to two seconds longer to stop as + executor threads are given up to two seconds to stop gracefully before + Thread.stop() is called on any remaining threads.

    +
    + + +

    If true, Tomcat attempts to terminate + java.util.Timer threads that have been started by the web + application. Unlike standard threads, timer threads can be stopped + safely although there may still be side-effects for the application. If + not specified, the default value of false will be used.

    +
    + + +

    If true, Tomcat attempts to clear + java.lang.ThreadLocal variables that have been populated + with classes loaded by the web application. If not specified, the + default value of true will be used.

    +
    + + +

    Set to true if you want a context XML descriptor + embedded inside the application (located at + /META-INF/context.xml) to be copied to the owning + Host's xmlBase when the application + is deployed. On subsequent starts, the copied context XML descriptor + will be used in preference to any context XML descriptor embedded inside + the application even if the descriptor embedded inside the application + is more recent. The default is false. Note if + the deployXML attribute of the owning + Host is false or if the + copyXML attribute of the owning + Host is true, this attribute will + have no effect.

    +
    + + +

    If true, any attempt by an application to modify the + provided JNDI context with a call to bind(), unbind(), + createSubContext(), destroySubContext() or close() will trigger a + javax.naming.OperationNotSupportedException as required by + section EE.5.3.4 of the Jakarta EE specification. This exception can be + disabled by setting this attribute to false in which case + any calls to modify the JNDI context will return without making + any changes and methods that return values will return + null. If not specified, the specification compliant default + of true will be used.

    +
    + + +

    If true, when this context is stopped, Tomcat renews all + the threads from the thread pool that was used to serve this context. + This also requires that the + ThreadLocalLeakPreventionListener be configured in + server.xml and that the threadRenewalDelay + property of the Executor be >=0. If not specified, the + default value of true will be used.

    +
    + + +

    If true, Tomcat will not perform the usual memory leak + checks when the web application is stopped if that web application is + stopped as part of a JVM shutdown. If not specified, the default value + of false will be used.

    +
    + + +

    Number of ms that the container will wait for servlets to unload. + If not specified, the default value is 2000 ms.

    +
    + + +

    If false, the unpackWARs attribute of + the owning Host will be overridden and the WAR + file will not be unpacked. If true, the value of the owning + Host's unpackWARs + attribute will determine if the WAR is unpacked. If not specified, the + default value is true.

    +
    + + +

    Set to true (the default) to have Catalina enable a + JNDI InitialContext for this web application that is + compatible with Jakarta EE platform conventions.

    +
    + + +

    Pathname to a scratch directory to be provided by this Context + for temporary read-write use by servlets within the associated web + application. This directory will be made visible to servlets in the + web application by a servlet context attribute (of type + java.io.File) named + jakarta.servlet.context.tempdir as described in the + Servlet Specification. If not specified, a suitable directory + underneath $CATALINA_BASE/work will be provided.

    +
    + +
    + +
    + + +
    + + +
    + +

    You can nest at most one instance of the following utility components + by nesting a corresponding element inside your Context + element:

    +
      +
    • Cookie Processor - + Configure parsing and generation of HTTP cookie headers.
    • +
    • Loader - + Configure the web application class loader that will be used to load + servlet and bean classes for this web application. Normally, the + default configuration of the class loader will be sufficient.
    • +
    • Manager - + Configure the session manager that will be used to create, destroy, + and persist HTTP sessions for this web application. Normally, the + default configuration of the session manager will be sufficient.
    • +
    • Realm - + Configure a realm that will allow its + database of users, and their associated roles, to be utilized solely + for this particular web application. If not specified, this web + application will utilize the Realm associated with the owning + Host or Engine.
    • +
    • Resources - + Configure the resource manager that will be used to access the static + resources associated with this web application. Normally, the + default configuration of the resource manager will be sufficient.
    • +
    • WatchedResource - The auto deployer will monitor the + specified static resource of the web application for updates, and will + reload the web application if it is updated. The content of this element + must be a string.
    • +
    • JarScanner - + Configure the Jar Scanner that will be used to scan the web application + for JAR files and directories of class files. It is typically used during + web application start to identify configuration files such as TLDs o + web-fragment.xml files that must be processed as part of the web + application initialisation. Normally, the default Jar Scanner + configuration will be sufficient.
    • +
    + +
    + + +
    + + + + +

    A context is associated with the + org.apache.catalina.core.ContainerBase.[enginename].[hostname].[path] + log category. Note that the brackets are actually part of the name, don't omit them.

    + +
    + + + + +

    When you run a web server, one of the output files normally generated + is an access log, which generates one line of information for + each request processed by the server, in a standard format. Catalina + includes an optional Valve implementation that + can create access logs in the same standard format created by web servers, + or in any number of custom formats.

    + +

    You can ask Catalina to create an access log for all requests + processed by an Engine, + Host, or Context + by nesting a Valve element like this:

    + + + ... + + ... +]]> + +

    See Access Logging Valves + for more information on the configuration attributes that are + supported.

    + +
    + + + + +

    If you use the standard Context implementation, + the following configuration steps occur automatically when Catalina + is started, or whenever this web application is reloaded. No special + configuration is required to enable this feature.

    + +
      +
    • If you have not declared your own Loader + element, a standard web application class loader will be configured. +
    • +
    • If you have not declared your own Manager + element, a standard session manager will be configured.
    • +
    • If you have not declared your own Resources + element, a standard resources manager will be configured.
    • +
    • The web application properties listed in conf/web.xml + will be processed as defaults for this web application. This is used + to establish default mappings (such as mapping the *.jsp + extension to the corresponding JSP servlet), and other standard + features that apply to all web applications.
    • +
    • The web application properties listed in the + /WEB-INF/tomcat-web.xml resource for this web application + will be processed (if this resource exists), taking precedence over the + defaults.
    • +
    • The web application properties listed in the + /WEB-INF/web.xml resource for this web application + will be processed (if this resource exists).
    • +
    • If your web application has specified security constraints that might + require user authentication, an appropriate Authenticator that + implements the login method you have selected will be configured.
    • +
    + +
    + + + + +

    You can configure named values that will be made visible to the + web application as servlet context initialization parameters by nesting + <Parameter> elements inside this element. For + example, you can create an initialization parameter like this:

    + + ... + + ... +]]> + +

    This is equivalent to the inclusion of the following element in the + web application deployment descriptor (/WEB-INF/web.xml): +

    + + companyName + My Company, Incorporated +]]> +

    but does not require modification of the deployment descriptor + to customize this value.

    + +

    The valid attributes for a <Parameter> element + are as follows:

    + + + + +

    Optional, human-readable description of this context + initialization parameter.

    +
    + + +

    The name of the context initialization parameter to be created.

    +
    + + +

    Set this to false if you do not want + a <context-param> for the same parameter name, + found in the web application deployment descriptor, to override the + value specified here. By default, overrides are allowed.

    +
    + + +

    The parameter value that will be presented to the application + when requested by calling + ServletContext.getInitParameter().

    +
    + +
    + +
    + + + + +

    You can configure named values that will be made visible to the + web application as environment entry resources, by nesting + <Environment> entries inside this element. For + example, you can create an environment entry like this:

    + + ... + + ... +]]> + +

    This is equivalent to the inclusion of the following element in the + web application deployment descriptor (/WEB-INF/web.xml): +

    + + maxExemptions + 10 + java.lang.Integer +]]> +

    but does not require modification of the deployment descriptor + to customize this value.

    + +

    The valid attributes for an <Environment> element + are as follows:

    + + + + +

    Optional, human-readable description of this environment entry.

    +
    + + +

    The name of the environment entry to be created, relative to the + java:comp/env context.

    +
    + + +

    Set this to false if you do not want + an <env-entry> for the same environment entry name, + found in the web application deployment descriptor, to override the + value specified here. By default, overrides are allowed.

    +
    + + +

    The fully qualified Java class name expected by the web application + for this environment entry. Must be a legal value for + <env-entry-type> in the web application deployment + descriptor.

    +
    + + +

    The parameter value that will be presented to the application + when requested from the JNDI context. This value must be convertible + to the Java type defined by the type attribute.

    +
    + +
    + +
    + + + + +

    If you have implemented a Java object that needs to know when this + Context is started or stopped, you can declare it by + nesting a Listener element inside this element. The + class name you specify must implement the + org.apache.catalina.LifecycleListener interface, and + the class must be packaged in a jar and placed in the + $CATALINA_HOME/lib directory. + It will be notified about the occurrence of the corresponding + lifecycle events. Configuration of such a listener looks like this:

    + + + ... + + ... +]]> + +

    Note that a Listener can have any number of additional properties + that may be configured from this element. Attribute names are matched + to corresponding JavaBean property names using the standard property + method naming patterns.

    + +
    + + + + +

    You can ask Catalina to check the IP address, or host name, on every + incoming request directed to the surrounding + Engine, Host, or + Context element. The remote address or name + will be checked against configured "accept" and/or "deny" + filters, which are defined using java.util.regex Regular + Expression syntax. Requests that come from locations that are + not accepted will be rejected with an HTTP "Forbidden" error. + Example filter declarations:

    + + + ... + + + ... +]]> + +

    See Remote Address Filter + and Remote Host Filter for + more information about the configuration options that are supported.

    + +
    + + + + +

    You can declare the characteristics of the resource + to be returned for JNDI lookups of <resource-ref> and + <resource-env-ref> elements in the web application + deployment descriptor. You MUST also define + the needed resource parameters as attributes of the Resource + element, to configure the object factory to be used (if not known to Tomcat + already), and the properties used to configure that object factory.

    + +

    For example, you can create a resource definition like this:

    + + ... + + ... +]]> + +

    This is equivalent to the inclusion of the following element in the + web application deployment descriptor (/WEB-INF/web.xml):

    + + Employees Database for HR Applications + jdbc/EmployeeDB + javax.sql.DataSource + Container +]]> + +

    but does not require modification of the deployment + descriptor to customize this value.

    + +

    The valid attributes for a <Resource> element + are as follows:

    + + + + +

    Specify whether the web Application code signs on to the + corresponding resource manager programmatically, or whether the + Container will sign on to the resource manager on behalf of the + application. The value of this attribute must be + Application or Container. This + attribute is required if the web application + will use a <resource-ref> element in the web + application deployment descriptor, but is optional if the + application uses a <resource-env-ref> instead.

    +
    + + +

    Name of the zero-argument method to call on a singleton resource when + it is no longer required. This is intended to speed up clean-up of + resources that would otherwise happen as part of garbage collection. + This attribute is ignored if the singleton attribute is + false.

    +

    For javax.sql.DataSource and + javax.sql.XADataSource resources that implement + AutoCloseable such as Apache Commons DBCP 2 and the default + Apache Tomcat connection pool, this attribute is defaults to + close. This may be disabled by setting the attribute to the + empty string. For all other resource types no default is defined and no + close method will be called by default.

    +
    + + +

    Optional, human-readable description of this resource.

    +
    + + +

    The name of the resource to be created, relative to the + java:comp/env context.

    +
    + + +

    Specify whether connections obtained through this resource + manager can be shared. The value of this attribute must be + Shareable or Unshareable. By default, + connections are assumed to be shareable.

    +
    + + +

    Specify whether this resource definition is for a singleton resource, + i.e. one where there is only a single instance of the resource. If this + attribute is true, multiple JNDI lookups for this resource + will return the same object. If this attribute is false, + multiple JNDI lookups for this resource will return different objects. + This attribute must be true for + javax.sql.DataSource resources to enable JMX registration + of the DataSource. The value of this attribute must be true + or false. By default, this attribute is true. +

    +
    + + +

    The fully qualified Java class name expected by the web + application when it performs a lookup for this resource.

    +
    + +
    + + +
    + + + + +

    This element is used to create a link to a global JNDI resource. Doing + a JNDI lookup on the link name will then return the linked global + resource.

    + +

    For example, you can create a resource link like this:

    + + ... + ]]> + +

    The valid attributes for a <ResourceLink> element + are as follows:

    + + + + +

    The name of the linked global resource in the + global JNDI context.

    +
    + + +

    The name of the resource link to be created, relative to the + java:comp/env context.

    +
    + + +

    The fully qualified Java class name expected by the web + application when it performs a lookup for this resource link.

    +
    + + +

    The fully qualified Java class name for the class creating these objects. + This class should implement the javax.naming.spi.ObjectFactory interface.

    +
    +
    + +

    When the attribute factory="org.apache.naming.factory.DataSourceLinkFactory" the resource link can be used with + two additional attributes to allow a shared data source to be used with different credentials. + When these two additional attributes are used in combination with the javax.sql.DataSource + type, different contexts can share a global data source with different credentials. + Under the hood, what happens is that a call to getConnection() + is simply translated to a call + getConnection(username, password) on the global data source. This is an easy way to get code to be transparent to what schemas are being used, + yet be able to control connections (or pools) in the global configuration. +

    + + + +

    username value for the getConnection(username, password) + call on the linked global DataSource. +

    +
    + + +

    password value for the getConnection(username, password) + call on the linked global DataSource. +

    +
    + +
    +

    Shared Data Source Example:

    +

    Warning: This feature works only if the global DataSource +supports getConnection(username, password) method. +Apache Commons DBCP 2 pool that +Tomcat uses by default does not support it. See its Javadoc for +BasicDataSource class. +Apache Tomcat JDBC pool does support it, +but by default this support is disabled and can be enabled by +alternateUsernameAllowed attribute. See its documentation +for details.

    + + ... + + + + ... + + + ... + ]]> +

    When a request for getConnection() is made in the + /foo context, the request is translated into + getConnection("foo","foopass"), + while a request in the /bar gets passed straight through.

    +
    + + + +

    You can declare the characteristics of the UserTransaction + to be returned for JNDI lookup for java:comp/UserTransaction. + You MUST define an object factory class to instantiate + this object as well as the needed resource parameters as attributes of the + Transaction + element, and the properties used to configure that object factory.

    + +

    The valid attributes for the <Transaction> element + are as follows:

    + + + + +

    The class name for the JNDI object factory.

    +
    + +
    + +
    + +
    + + + + + +
    diff --git a/webapps/docs/config/cookie-processor.xml b/webapps/docs/config/cookie-processor.xml new file mode 100644 index 0000000..d09b522 --- /dev/null +++ b/webapps/docs/config/cookie-processor.xml @@ -0,0 +1,148 @@ + + + +]> + + + &project; + + + The Cookie Processor Component + + + + +
    + +
    + +
    + +

    The CookieProcessor element represents the component that + parses received cookie headers into jakarta.servlet.http.Cookie + objects accessible through HttpServletRequest.getCookies() and + converts jakarta.servlet.http.Cookie objects added to the response + through HttpServletResponse.addCookie() to the HTTP headers + returned to the client.

    + +

    A CookieProcessor element MAY be nested inside a + Context component. If it is not included, a default + implementation will be created automatically.

    + +
    + + +
    + + + +

    All implementations of CookieProcessor support the + following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.tomcat.util.http.CookieProcessor + interface. If not specified, the standard value (defined below) will be + used.

    +
    + +
    + +
    + + + + +

    The standard implementation of CookieProcessor is + org.apache.tomcat.util.http.Rfc6265CookieProcessor.

    + +

    This cookie processor is based on RFC6265 with the following changes to + support better interoperability:

    + +
      +
    • Values 0x80 to 0xFF are permitted in cookie-octet to support the use + of UTF-8 in cookie values as used by HTML 5.
    • +
    • For cookies without a value, the '=' is not required after the name as + some browsers do not sent it.
    • +
    + +

    The RFC 6265 cookie processor is generally more lenient than the legacy + cookie parser. In particular:

    + +
      +
    • The '=' and '/' characters are always + permitted in a cookie value.
    • +
    • Name only cookies are always permitted.
    • +
    • The cookie header is always preserved.
    • +
    + +

    The RFC 6265 Cookie Processor supports the following + additional attributes.

    + + + + +

    Should the Partitioned flag be set on cookies? Defaults to false.

    +

    Note: The name of the attribute used to indicate a partitioned cookie as part of + CHIPS is not defined by an RFC and + may change in a non-backwards compatible way once equivalent functionality is included in an RFC.

    +
    + + +

    Enables setting same-site cookie attribute.

    + +

    If value is unset then the same-site cookie attribute + won't be set. This is the default value.

    + +

    If value is none then the same-site cookie attribute + will be set and the cookie will always be sent in cross-site requests.

    + +

    If value is lax then the browser only sends the cookie + in same-site requests and cross-site top level GET requests.

    + +

    If value is strict then the browser prevents sending the + cookie in any cross-site request.

    +
    + +
    + +
    + +
    + + +
    + +

    No element may be nested inside a CookieProcessor.

    + +
    + + +
    + +

    No special features are associated with a CookieProcessor + element.

    + +
    + + + +
    diff --git a/webapps/docs/config/credentialhandler.xml b/webapps/docs/config/credentialhandler.xml new file mode 100644 index 0000000..c7fe271 --- /dev/null +++ b/webapps/docs/config/credentialhandler.xml @@ -0,0 +1,219 @@ + + + +]> + + + &project; + + + The CredentialHandler Component + + + + +
    + +
    + +
    + +

    The CredentialHandler element represents the component + used by a Realm to compare a provided credential such + as a password with the version of the credential stored by the + Realm. The CredentialHandler can + also be used to generate a new stored version of a given credential that would + be required, for example, when adding a new user to a + Realm or when changing a user's password.

    + +

    A CredentialHandler element MUST be nested inside a + Realm component. If it is not included, + a default CredentialHandler will be created using the + MessageDigestCredentialHandler.

    + +
    + + +
    + + + +

    All implementations of CredentialHandler support the + following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.CredentialHandler + interface.

    +
    + +
    + +

    Unlike most Catalina components, there are several standard + CredentialHandler implementations available. As a result, + if a CredentialHandler element is present then the + className attribute MUST be used to select the implementation + you wish to use.

    + +
    + + + + +

    The MessageDigestCredentialHandler is used when stored + passwords are protected by a message digest. This credential handler + supports the following forms of stored passwords:

    +
      +
    • plainText - the plain text credentials if no + algorithm is specified
    • +
    • encodedCredential - a hex encoded digest of the + password digested using the configured digest
    • +
    • {MD5}encodedCredential - a Base64 encoded MD5 + digest of the password
    • +
    • {SHA}encodedCredential - a Base64 encoded SHA1 digest + of the password
    • +
    • {SSHA}encodedCredential - 20 character salt followed + by the salted SHA1 digest Base64 encoded
    • +
    • salt$iterationCount$encodedCredential - a hex encoded + salt, iteration code and a hex encoded credential, each separated by + $
    • +
    + +

    If the stored password form does not include an iteration count then an + iteration count of 1 is used.

    + +

    If the stored password form does not include salt then no salt is + used.

    + + + + +

    The name of the java.security.MessageDigest algorithm + used to encode user passwords stored in the database. If not specified, + user passwords are assumed to be stored in clear-text.

    +
    + + +

    Digesting the password requires that it is converted to bytes. This + attribute determines the character encoding to use for conversions + between characters and bytes. If not specified, UTF-8 will be used.

    +
    + + +

    The number of iterations to use when creating a new stored credential + from a clear text credential.

    +
    + + +

    The length of the randomly generated salt to use when creating a + new stored credential from a clear text credential.

    +
    + +
    + +
    + + + +

    The NestedCredentialHandler is an implementation of + CredentialHandler that delegates to one or more + sub-CredentialHandlers.

    + +

    Using the NestedCredentialHandler gives the developer + the ability to combine multiple CredentialHandlers of the + same or different types.

    + +

    Sub-CredentialHandlers are defined by nesting CredentialHandler elements + inside the CredentialHandler element that defines the + NestedCredentialHandler. Credentials will be matched against each + CredentialHandler in the order they are listed. A match against + any CredentialHandler will be sufficient for the credentials to be + considered matched.

    + +
    + + + +

    The SecretKeyCredentialHandler is used when stored + passwords are built using javax.crypto.SecretKeyFactory. This + credential handler supports the following forms of stored passwords:

    +
      +
    • salt$iterationCount$encodedCredential - a hex encoded + salt, iteration code and a hex encoded credential, each separated by + $
    • +
    + +

    If the stored password form does not include an iteration count then an + iteration count of 1 is used.

    + +

    If the stored password form does not include salt then no salt is + used.

    + + + + +

    The name of the secret key algorithm used to encode user passwords + stored in the database. If not specified, a default of + PBKDF2WithHmacSHA1 is used.

    +
    + + +

    The length of key to generate for the stored credential. If not + specified, a default of 160 is used.

    +
    + + +

    The number of iterations to use when creating a new stored credential + from a clear text credential.

    +
    + + +

    The length of the randomly generated salt to use when creating a + new stored credential from a clear text credential.

    +
    + +
    + +
    + +
    + + +
    + +

    If you are using the NestedCredentialHandler Implementation or a + CredentialHandler that extends the NestedCredentialHandler one or more + <CredentialHandler> elements may be nested inside it. +

    + +
    + + +
    + +

    No special features are associated with a + CredentialHandler element.

    + +
    + + + +
    diff --git a/webapps/docs/config/engine.xml b/webapps/docs/config/engine.xml new file mode 100644 index 0000000..054eb78 --- /dev/null +++ b/webapps/docs/config/engine.xml @@ -0,0 +1,264 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + The Engine Container + + + + +
    + +
    + +
    + +

    The Engine element represents the entire request + processing machinery associated with a particular Catalina + Service. It receives and processes + all requests from one or more Connectors, + and returns the completed response to the Connector for ultimate + transmission back to the client.

    + +

    Exactly one Engine element MUST be nested inside + a Service element, following all of the + corresponding Connector elements associated with this Service.

    + +
    + + +
    + + + +

    All implementations of Engine + support the following attributes:

    + + + + +

    This value represents the delay in seconds between the + invocation of the backgroundProcess method on this engine and + its child containers, including all hosts and contexts. + Child containers will not be invoked if their delay value is not + negative (which would mean they are using their own processing + thread). Setting this to a positive value will cause + a thread to be spawn. After waiting the specified amount of time, + the thread will invoke the backgroundProcess method on this engine + and all its child containers. If not specified, the default value for + this attribute is 10, which represent a 10 seconds delay.

    +
    + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Engine interface. + If not specified, the standard value (defined below) will be used.

    +
    + + +

    The default host name, which identifies the + Host that will process requests directed + to host names on this server, but which are not configured in + this configuration file. This name MUST match the name + attributes of one of the Host elements + nested immediately inside.

    +
    + + +

    Identifier which must be used in load balancing scenarios to enable + session affinity. The identifier, which must be unique across all + Tomcat servers which participate in the cluster, will be appended to + the generated session identifier, therefore allowing the front end + proxy to always forward a particular session to the same Tomcat + instance.

    +
    + + +

    Logical name of this Engine, used in log and error messages. When + using multiple Service elements in the same + Server, each Engine MUST be assigned a unique + name.

    +
    + + +

    The number of threads this Engine will use to start + child Host elements in parallel. The special + value of 0 will result in the value of + Runtime.getRuntime().availableProcessors() being used. + Negative values will result in + Runtime.getRuntime().availableProcessors() + value being + used unless this is less than 1 in which case 1 thread will be used. If + not specified, the default value of 1 will be used. If 1 thread is + used then, rather than using an ExecutorService, the + current thread will be used.

    +
    + +
    + +
    + + + + +

    The standard implementation of Engine is + org.apache.catalina.core.StandardEngine. + It supports the following additional attributes (in addition to the + common attributes listed above):

    + + + + + +
    + + +
    + + +
    + +

    You can nest one or more Host elements inside + this Engine element, each representing a different virtual + host associated with this server. At least one Host + is required, and one of the nested Hosts MUST + have a name that matches the name specified for the + defaultHost attribute, listed above.

    + +

    You can nest at most one instance of the following utility components + by nesting a corresponding element inside your Engine + element:

    +
      +
    • Realm - + Configure a realm that will allow its + database of users, and their associated roles, to be shared across all + Hosts and Contexts + nested inside this Engine, unless overridden by a + Realm configuration at a lower level.
    • +
    + +
    + + +
    + + + + +

    An engine is associated with the + org.apache.catalina.core.ContainerBase.[enginename] + log category. Note that the brackets are actually part of the name, + don't omit them.

    + +
    + + + + +

    When you run a web server, one of the output files normally generated + is an access log, which generates one line of information for + each request processed by the server, in a standard format. Catalina + includes an optional Valve implementation that + can create access logs in the same standard format created by web servers, + or in any number of custom formats.

    + +

    You can ask Catalina to create an access log for all requests + processed by an Engine, + Host, or Context + by nesting a Valve element like this:

    + + + ... + + ... +]]> + +

    See Access Logging Valves + for more information on the configuration attributes that are + supported.

    + +
    + + + + +

    If you have implemented a Java object that needs to know when this + Engine is started or stopped, you can declare it by + nesting a Listener element inside this element. The + class name you specify must implement the + org.apache.catalina.LifecycleListener interface, and + it will be notified about the occurrence of the corresponding + lifecycle events. Configuration of such a listener looks like this:

    + + + ... + + ... +]]> + +

    Note that a Listener can have any number of additional properties + that may be configured from this element. Attribute names are matched + to corresponding JavaBean property names using the standard property + method naming patterns.

    + +
    + + + + +

    You can ask Catalina to check the IP address, or host name, on every + incoming request directed to the surrounding + Engine, Host, or + Context element. The remote address or name + will be checked against configured "accept" and/or "deny" + filters, which are defined using java.util.regex Regular + Expression syntax. Requests that come from locations that are + not accepted will be rejected with an HTTP "Forbidden" error. + Example filter declarations:

    + + + ... + + + ... +]]> + +

    See Remote Address Filter + and Remote Host Filter for + more information about the configuration options that are supported.

    + +
    + + +
    + + + + + +
    diff --git a/webapps/docs/config/executor.xml b/webapps/docs/config/executor.xml new file mode 100644 index 0000000..f0e7e0f --- /dev/null +++ b/webapps/docs/config/executor.xml @@ -0,0 +1,152 @@ + + + +]> + + + &project; + + + Filip Hanik + The Executor (thread pool) + + + + +
    + +
    + +
    + +

    The Executor represents a thread pool that can be shared + between components in Tomcat. Historically there has been a thread pool per + connector created but this allows you to share a thread pool, between (primarily) connector + but also other components when those get configured to support executors

    + + +

    The executor has to implement the org.apache.catalina.Executor interface.

    + +

    The executor is a nested element to the Service element. + And in order for it to be picked up by the connectors, the Executor element has to appear + prior to the Connector element in server.xml

    +
    + + +
    + + + +

    All implementations of Executor + support the following attributes:

    + + + + +

    The class of the implementation. The implementation has to implement the + org.apache.catalina.Executor interface. + This interface ensures that the object can be referenced through its name attribute + and that implements Lifecycle, so that it can be started and stopped with the container. + The default value for the className is org.apache.catalina.core.StandardThreadExecutor

    +
    + + +

    The name used to reference this pool in other places in server.xml. + The name is required and must be unique.

    +
    + +
    + +
    + + + +

    This implemtenation uses a pool of platform threads to execute the tasks assigned to the Executor.

    + +

    The className attribute must be org.apache.catalina.core.StandardThreadExecutor to use + this implementation.

    + +

    The standard implementation supports the following attributes:

    + + + + +

    (int) The thread priority for threads in the executor, the default is + 5 (the value of the Thread.NORM_PRIORITY constant)

    +
    + +

    (boolean) Whether the threads should be daemon threads or not, the default is true

    +
    + +

    (String) The name prefix for each thread created by the executor. + The thread name for an individual thread will be namePrefix+threadNumber. The default value is + tomcat-exec-.

    +
    + +

    (int) The max number of active threads in this pool, default is 200

    +
    + +

    (int) The minimum number of threads (idle and active) always kept alive, default is 25

    +
    + +

    (int) The number of milliseconds before an idle thread shutsdown, unless the number of active threads are less + or equal to minSpareThreads. Default value is 60000(1 minute)

    +
    + +

    (int) The maximum number of runnable tasks that can queue up awaiting + execution before we reject them. Default value is Integer.MAX_VALUE

    +
    + +

    (long) If a ThreadLocalLeakPreventionListener is configured, + it will notify this executor about stopped contexts. + After a context is stopped, threads in the pool are renewed. To avoid renewing all threads at the same time, + this option sets a delay between renewal of any 2 threads. The value is in ms, + default value is 1000 ms. If value is negative, threads are not renewed.

    +
    +
    + + +
    + + + +

    This implemtenation uses a new virtual thread to execute each task assigned to the Executor. This Executor requires + a minimum Java version of Java 21.

    + +

    The className attribute must be org.apache.catalina.core.StandardVirtualThreadExecutor to + use this implementation.

    + +

    The virtual thread implementation supports the follow attributes:

    + + + +

    (String) The name prefix for each thread created by the executor. + The thread name for an individual thread will be namePrefix+threadNumber. The default value is + tomcat-virt-

    +
    +
    + +
    + +
    + + + + +
    diff --git a/webapps/docs/config/filter.xml b/webapps/docs/config/filter.xml new file mode 100644 index 0000000..12e7018 --- /dev/null +++ b/webapps/docs/config/filter.xml @@ -0,0 +1,2027 @@ + + + +]> + + + &project; + + + Container Provided Filters + + + + +
    + +
    + +
    + +

    Tomcat provides a number of Filters which may be + configured for use with all web applications using + $CATALINA_BASE/conf/web.xml or may be configured for individual + web applications by configuring them in the application's + WEB-INF/web.xml. Each filter is described below.

    + +

    This description uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + +
    + + +
    + + + +

    The HTTP specification is clear that if no character set is specified for + media sub-types of the "text" media type, the ISO-8859-1 character set must + be used. However, browsers may attempt to auto-detect the character set. + This may be exploited by an attacker to perform an XSS attack. Internet + Explorer and other browsers have an option to + enable this behavior.

    + +

    This filter prevents the attack by explicitly setting a character set. + Unless the provided character set is explicitly overridden by the user the + browser will adhere to the explicitly set character set, thus preventing the + XSS attack.

    + +
    + + + +

    The filter class name for the Add Default Character Set Filter is + org.apache.catalina.filters.AddDefaultCharsetFilter + .

    + +
    + + + +

    The Add Default Character Set Filter supports the following initialization + parameters:

    + + + + +

    Name of the character set which should be set, if no other character set + was set explicitly by a Servlet. This parameter has two special values + default and system. A value of system + uses the JVM wide default character set, which is usually set by locale. + A value of default will use ISO-8859-1.

    +
    + +
    + +
    + +
    + +
    + +

    This filter is an implementation of W3C's CORS (Cross-Origin Resource + Sharing) specification, which is a + mechanism that enables cross-origin requests.

    +

    The filter works by adding required Access-Control-* headers + to HttpServletResponse object. The filter also protects against HTTP + response splitting. If request is invalid, or is not permitted, then request + is rejected with HTTP status code 403 (Forbidden). A + flowchart that + demonstrates request processing by this filter is available.

    +

    The minimal configuration required to use this filter is:

    + + CorsFilter + org.apache.catalina.filters.CorsFilter + + + CorsFilter + /* +]]> +

    The above configuration enables the filter but does not relax the + cross-origin policy. As a minimum, you will need to add a + cors.allowed.origins initialisation parameter as described + below to enable cross-origin requests. Depending on your requirements, you + may need to provide additional configuration.

    +

    An instance of this filter can only implement one policy. If you want to + apply different policies (e.g. different allowed origins) to different URLs + or sets of URLs within your web application you will need to configure a + separate instance of this filter for each policy you wish to configure.

    +
    + +

    The filter class name for the CORS Filter is + org.apache.catalina.filters.CorsFilter.

    +
    + +

    The CORS Filter supports following initialisation parameters:

    + + +

    A list of origins + that are allowed to access the resource. A * can be + specified to enable access to resource from any origin. Otherwise, an + allow list of comma separated origins can be provided. Eg: + https://www.w3.org, https://www.apache.org. + Defaults: The empty String. (No origin is allowed to + access the resource).

    +
    + +

    A comma separated list of HTTP methods that can be used to access the + resource, using cross-origin requests. These are the methods which will + also be included as part of Access-Control-Allow-Methods + header in pre-flight response. Eg: GET, POST. + Defaults: GET, POST, HEAD, OPTIONS

    +
    + +

    A comma separated list of request headers that can be used when + making an actual request. These headers will also be returned as part + of Access-Control-Allow-Headers header in a pre-flight + response. Eg: Origin,Accept. Defaults: + Origin, Accept, X-Requested-With, Content-Type, + Access-Control-Request-Method, Access-Control-Request-Headers

    +
    + +

    A comma separated list of headers other than simple response headers + that browsers are allowed to access. These are the headers which will + also be included as part of Access-Control-Expose-Headers + header in the pre-flight response. Eg: + X-CUSTOM-HEADER-PING,X-CUSTOM-HEADER-PONG. + Default: None. Non-simple headers are not exposed by + default.

    +
    + +

    The amount of seconds, browser is allowed to cache the result of the + pre-flight request. This will be included as part of + Access-Control-Max-Age header in the pre-flight response. + A negative value will prevent CORS Filter from adding this response + header to pre-flight response. Defaults: + 1800

    +
    + +

    A flag that indicates whether the resource supports user credentials. + This flag is exposed as part of + Access-Control-Allow-Credentials header in a pre-flight + response. It helps browser determine whether or not an actual request + can be made using credentials. Defaults: + false

    +
    + +

    A flag to control if CORS specific attributes should be added to + HttpServletRequest object or not. Defaults: + true

    +
    +
    +

    Here's an example of a more advanced configuration, that overrides + defaults:

    + + CorsFilter + org.apache.catalina.filters.CorsFilter + + cors.allowed.origins + https://www.apache.org + + + cors.allowed.methods + GET,POST,HEAD,OPTIONS,PUT + + + cors.allowed.headers + Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers + + + cors.exposed.headers + Access-Control-Allow-Origin,Access-Control-Allow-Credentials + + + cors.support.credentials + true + + + cors.preflight.maxage + 10 + + + + CorsFilter + /* +]]> +
    + +

    CORS Filter adds information about the request, in HttpServletRequest + object, for consumption downstream. Following attributes are set, if + cors.request.decorate initialisation parameter is + true:

    +
      +
    • cors.isCorsRequest: Flag to determine if request is + a CORS request.
    • +
    • cors.request.origin: The Origin URL, i.e. the URL of + the page from where the request originated.
    • +
    • cors.request.type: Type of CORS request. Possible + values: +
        +
      • SIMPLE: A request which is not preceded by a + pre-flight request.
      • +
      • ACTUAL: A request which is preceded by a pre-flight + request.
      • +
      • PRE_FLIGHT: A pre-flight request.
      • +
      • NOT_CORS: A normal same-origin request.
      • +
      • INVALID_CORS: A cross-origin request, which is + invalid.
      • +
      +
    • +
    • cors.request.headers: Request headers sent as + Access-Control-Request-Headers header, for a pre-flight + request. +
    • +
    +
    +
    + +
    + + + +

    This filter provides basic CSRF protection for a web application. The + filter assumes that it is mapped to /* and that all URLs + returned to the client are encoded via a call to + HttpServletResponse#encodeRedirectURL(String) or + HttpServletResponse#encodeURL(String).

    + +

    This filter prevents CSRF by generating a nonce and storing it in the + session. URLs are also encoded with the same nonce. When the next request is + received the nonce in the request is compared to the nonce in the session + and only if they are the same is the request allowed to continue.

    + +
    + + + +

    The filter class name for the CSRF Prevention Filter is + org.apache.catalina.filters.CsrfPreventionFilter + .

    + +
    + + + +

    The CSRF Prevention Filter supports the following initialisation + parameters:

    + + + + +

    HTTP response status code that is used when rejecting denied + request. The default value is 403.

    +
    + + +

    A flag to enable or disable enforcement. When enforcement is + disabled, the CsrfPreventionFilter will allow all requests and + log CSRF failures as DEBUG messages. The default is true, + enabling the enforcement of CSRF protection.

    +
    + + +

    A comma separated list of URLs that will not be tested for the + presence of a valid nonce. They are used to provide a way to navigate + back to a protected application after having navigated away from it. + Entry points will be limited to HTTP GET requests and should not trigger + any security sensitive actions.

    +
    + + +

    The number of previously issued nonces that will be cached on a LRU + basis to support parallel requests, limited use of the refresh and back + in the browser and similar behaviors that may result in the submission + of a previous nonce rather than the current one. If not set, the default + value of 5 will be used.

    +
    + + +

    The name of the request parameter used for the nonce. If not set, the + default value of org.apache.catalina.filters.CSRF_NONCE + will be used.

    +
    + + +

    The name of the class to use to generate nonces. The class must be an + instance of java.util.Random. If not set, the default value + of java.security.SecureRandom will be used.

    +
    + + +

    A list of URL patterns that will not have CSRF nonces added + to them. You may not want to add nonces to certain URLs to avoid + creating unique URLs which may defeat resource caching, etc.

    + +

    There are several types of patterns supported:

    + +
      +
    • Prefix matches using a pattern that ends with a *. + For example, /images/*.
    • + +
    • Suffix matches using a pattern that begins with a *. + For example, *.css.
    • + +
    • Mime-type matches which begin with mime: and specify + one of the above matches which will be checked against the MIME type + of the URL filename. For example, mime:image/*. + Note that the MIME-type will be determined using + ServletContext.getMimeType.
    • + +
    • A single complete regular expression pattern which begins and + ends with / (slash / solidus) symbols. For example + //images/.*|/scripts/.*/. The leading and trailing + / characters will be removed from the pattern before + being compiled. Note that there can be only a single pattern, + but that pattern can of course have as many alternatives as desired + by using the regular expression | (OR) + operator. The regular expression will be matched against the entire + URL (i.e. match not find semantics), and the regex + dialect is Java (java.util.regex.Pattern). +
    • +
    + +

    The default is *.css, *.js, *.gif, *.png, *.jpg, *.svg, *.ico, *.jpeg, *.mjs.

    +
    +
    + +
    + +
    + +
    + + + +

    This filter provides basic CSRF protection for REST APIs. The CSRF + protection is applied only for modifying HTTP requests (different from GET, + HEAD, OPTIONS) to protected resources. It is based on a custom header + X-CSRF-Token that provides a valid nonce.

    + +

    CSRF protection mechanism for REST APIs consists of the following steps: +

      +
    • Client asks for a valid nonce. This is performed with a + non-modifying "Fetch" request to protected resource.
    • +
    • Server responds with a valid nonce mapped to the current user + session.
    • +
    • Client provides this nonce in the subsequent modifying requests in + the frame of the same user session.
    • +
    • Server rejects all modifying requests to protected resources that + do not contain a valid nonce.
    • +
    +

    + +
    + + + +

    On the server side

    + +
      +
    • All CSRF protected REST APIs should be protected with an authentication + mechanism.
    • +
    • Protect modifying REST APIs with this filter.
    • +
    • Provide at least one non-modifying operation.
    • +
    + + RestCSRF + org.apache.catalina.filters.RestCsrfPreventionFilter + + + RestCSRF + + /resources/removeResource + /resources/addResource + + /resources/listResources +]]> + +

    On the client side

    + +
      +
    • Make a non-modifying "Fetch" request in order to obtain a valid nonce. + This can be done with sending additional header + X-CSRF-Token: Fetch
    • +
    • Cache the returned session id and nonce in order to provide them in + the subsequent modifying requests to protected resources.
    • +
    • Modifying requests can be denied and header + X-CSRF-Token: Required will be returned in case of + invalid or missing nonce, expired session or in case the session + id is changed by the server.
    • +
    + + +
    + + + +

    When the client is not able to insert custom headers in its calls to + REST APIs there is additional capability to configure URLs for which a + valid nonce will be accepted as a request parameter.

    + +

    Note: If there is a X-CSRF-Token header, it will be taken + with preference over any parameter with the same name in the request. + Request parameters cannot be used to fetch new nonce, only header can be + used to request a new nonce.

    + + + RestCSRF + org.apache.catalina.filters.RestCsrfPreventionFilter + + pathsAcceptingParams + /resources/removeResource,/resources/addResource + + + + RestCSRF + /resources/* +]]> + +
    + + + +

    The filter class name for the CSRF Prevention Filter for REST APIs is + org.apache.catalina.filters.RestCsrfPreventionFilter + .

    + +
    + + + +

    The CSRF Prevention Filter for REST APIs supports the following + initialisation parameters:

    + + + + +

    HTTP response status code that is used when rejecting denied + request. The default value is 403.

    +
    + + +

    A comma separated list of URLs that can accept nonces via request + parameter X-CSRF-Token. For use cases when a nonce information cannot + be provided via header, one can provide it via request parameters. If + there is a X-CSRF-Token header, it will be taken with preference over + any parameter with the same name in the request. Request parameters + cannot be used to fetch new nonce, only header can be used to request a + new nonce.

    +
    + + +

    The name of the class to use to generate nonces. The class must be an + instance of java.util.Random. If not set, the default value + of java.security.SecureRandom will be used.

    +
    + +
    + +
    + +
    + +
    + + + +

    + ExpiresFilter is a Java Servlet API port of Apache + mod_expires. + This filter controls the setting of the Expires HTTP header and the + max-age directive of the Cache-Control HTTP header in + server responses. The expiration date can set to be relative to either the + time the source file was last modified, or to the time of the client access. +

    + +

    + These HTTP headers are an instruction to the client about the document's + validity and persistence. If cached, the document may be fetched from the + cache rather than from the source until this time has passed. After that, the + cache copy is considered "expired" and invalid, and a new copy must + be obtained from the source. +

    +

    + To modify Cache-Control directives other than max-age (see + RFC + 2616 section 14.9), you can use other servlet filters or Apache Httpd + mod_headers module. +

    + +
    + + +

    + Basic configuration to add 'Expires' and 'Cache-Control: max-age=' + headers to images, CSS and JavaScript. +

    + + + ExpiresFilter + org.apache.catalina.filters.ExpiresFilter + + ExpiresByType image + access plus 10 minutes + + + ExpiresByType text/css + access plus 10 minutes + + + ExpiresByType text/javascript + access plus 10 minutes + + +... + + ExpiresFilter + /* + REQUEST +]]> + +
    + + +

    + The ExpiresDefault and ExpiresByType directives can also be + defined in a more readable syntax of the form: +

    + + + ExpiresDefault + [plus] { }* + + + + ExpiresByType type + [plus] { }* + + + + ExpiresByType type;encoding + [plus] { }* +]]> +

    + where <base> is one of: +

    +
      +
    • access
    • +
    • now (equivalent to 'access')
    • +
    • modification
    • +
    + +

    + The plus keyword is optional. <num> should be an + integer value (acceptable to Integer.parseInt()), and + <type> is one of: +

    +
      +
    • year, years
    • +
    • month, months
    • +
    • week, weeks
    • +
    • day, days
    • +
    • hour, hours
    • +
    • minute, minutes
    • +
    • second, seconds
    • +
    +

    + For example, any of the following directives can be used to make documents + expire 1 month after being accessed, by default: +

    + + + ExpiresDefault + access plus 1 month + + + + ExpiresDefault + access plus 4 weeks + + + + ExpiresDefault + access plus 30 days +]]> +

    +The expiry time can be fine-tuned by adding several +'<num> <type>' clauses: +

    + + + ExpiresByType text/html + access plus 1 month 15 days 2 hours + + + + ExpiresByType image/gif + modification plus 5 hours 3 minutes +]]> +

    + Note that if you use a modification date based setting, the Expires + header will not be added to content that does not come from + a file on disk. This is due to the fact that there is no modification time + for such content. +

    +
    + + +

    + A response is eligible to be enriched by ExpiresFilter if : +

    +
      +
    1. no expiration header is defined (Expires header or the + max-age directive of the Cache-Control header),
    2. +
    3. the response status code is not excluded by the directive + ExpiresExcludedResponseStatusCodes,
    4. +
    5. the Content-Type of the response matches one of the types + defined the in ExpiresByType directives or the + ExpiresDefault directive is defined.
    6. +
    + +

    + Note : If Cache-Control header contains other directives than + max-age, they are concatenated with the max-age directive + that is added by the ExpiresFilter. +

    + +
    + + +

    + The expiration configuration if elected according to the following algorithm: +

    +
      +
    1. ExpiresByType matching the exact content-type returned by + HttpServletResponse.getContentType() possibly including the charset + (e.g. 'text/xml;charset=UTF-8'),
    2. +
    3. ExpiresByType matching the content-type without the charset if + HttpServletResponse.getContentType() contains a charset (e.g. + 'text/xml;charset=UTF-8' -> 'text/xml'),
    4. +
    5. ExpiresByType matching the major type (e.g. substring before + '/') of HttpServletResponse.getContentType() + (e.g. 'text/xml;charset=UTF-8' -> 'text'),
    6. +
    7. ExpiresDefault
    8. +
    + +
    + + + +

    The filter class name for the Expires Filter is + org.apache.catalina.filters.ExpiresFilter + .

    + +
    + + + +

    The Expires Filter supports the following + initialisation parameters:

    + + + + +

    + This directive defines the http response status codes for which the + ExpiresFilter will not generate expiration headers. By default, the + 304 status code ("Not modified") is skipped. The + value is a comma separated list of http status codes. +

    +

    + This directive is useful to ease usage of ExpiresDefault directive. + Indeed, the behavior of 304 Not modified (which does specify a + Content-Type header) combined with Expires and + Cache-Control:max-age= headers can be unnecessarily tricky to + understand. +

    +

    See sample below the table

    +
    + + +

    + This directive defines the value of the Expires header and the + max-age directive of the Cache-Control header generated for + documents of the specified type (e.g., text/html). The second + argument sets the number of seconds that will be added to a base time to + construct the expiration date. The Cache-Control: max-age is + calculated by subtracting the request time from the expiration date and + expressing the result in seconds. +

    +

    + The base time is either the last modification time of the file, or the time + of the client's access to the document. Which should be used is + specified by the <code> field; M means that the + file's last modification time should be used as the base time, and + A means the client's access time should be used. The duration + is expressed in seconds. A2592000 stands for + access plus 30 days in alternate syntax. +

    +

    + The difference in effect is subtle. If M (modification in + alternate syntax) is used, all current copies of the document in all caches + will expire at the same time, which can be good for something like a weekly + notice that's always found at the same URL. If A ( + access or now in alternate syntax) is used, the date of + expiration is different for each client; this can be good for image files + that don't change very often, particularly for a set of related + documents that all refer to the same images (i.e., the images will be + accessed repeatedly within a relatively short timespan). +

    +

    + Note: When the content type includes a charset (e.g. + 'ExpiresByType text/xml;charset=utf-8'), Tomcat removes blank chars + between the ';' and the 'charset' keyword. Due to this, + configuration of an expiration with a charset must not include + such a space character. +

    +

    See sample below the table

    +

    + It overrides, for the specified MIME type only, any + expiration date set by the ExpiresDefault directive. +

    +

    + You can also specify the expiration time calculation using an alternate + syntax, described earlier in this document. +

    +
    + + +

    + This directive sets the default algorithm for calculating the + expiration time for all documents in the affected realm. It can be + overridden on a type-by-type basis by the ExpiresByType directive. See the + description of that directive for details about the syntax of the + argument, and the "alternate syntax" + description as well. +

    +
    +
    + +

    Sample: exclude response status codes 302, 500 and 503

    + + + ExpiresExcludedResponseStatusCodes + 302, 500, 503 +]]> + +

    Sample for ExpiresByType initialization parameter

    + + + ExpiresByType text/html + access plus 1 month 15 days 2 hours + + + + + ExpiresByType image/gif + A2592000 +]]> + +
    + + +

    + To troubleshoot, enable logging on the + org.apache.catalina.filters.ExpiresFilter. +

    +

    + Extract of logging.properties +

    + + org.apache.catalina.filters.ExpiresFilter.level = FINE +

    + Sample of initialization log message: +

    + + Mar 26, 2010 2:01:41 PM org.apache.catalina.filters.ExpiresFilter init +FINE: Filter initialized with configuration ExpiresFilter[ + excludedResponseStatusCode=[304], + default=null, + byType={ + image=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]], + text/css=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]], + text/javascript=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]]}] +

    + Sample of per-request log message where ExpiresFilter adds an + expiration date is below. The message is on one line and is wrapped here + for better readability. +

    + + Mar 26, 2010 2:09:47 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody +FINE: Request "/tomcat.gif" with response status "200" + content-type "image/gif", set expiration date 3/26/10 2:19 PM +

    + Sample of per-request log message where ExpiresFilter does not add + an expiration date: +

    + + Mar 26, 2010 2:10:27 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody +FINE: Request "/docs/config/manager.html" with response status "200" + content-type "text/html", no expiration configured +
    + +
    + +
    + + + +

    This filter triggers parameters parsing in a request and rejects the + request if some parameters were skipped during parameter parsing because + of parsing errors or request size limitations (such as + maxParameterCount attribute in a + Connector). + This filter can be used to ensure that none parameter values submitted by + client are lost.

    + +

    Note that parameter parsing may consume the body of an HTTP request, so + caution is needed if the servlet protected by this filter uses + request.getInputStream() or request.getReader() + calls. In general the risk of breaking a web application by adding this + filter is not so high, because parameter parsing does check content type + of the request before consuming the request body.

    + +

    Note, that for the POST requests to be parsed correctly, a + SetCharacterEncodingFilter filter must be configured above + this one. See CharacterEncoding page in the FAQ for details.

    + +

    The request is rejected with HTTP status code 400 (Bad Request).

    + +
    + + + +

    The filter class name for the Failed Request Filter is + org.apache.catalina.filters.FailedRequestFilter + .

    + +
    + + + +

    The Failed Request Filter does not support any initialization parameters.

    + +
    + +
    + +
    + + + +

    There are a number of HTTP headers that can be added to the response to + improve the security of the connection. This filter provides a mechanism for + adding those headers. Note that security related headers with more complex + requirements, like CORS, are implemented as separate Filters.

    + +
    + + + +

    The filter class name for the HTTP Header Security Filter is + org.apache.catalina.filters.HttpHeaderSecurityFilter + .

    + +
    + + + +

    The HTTP Header Security Filter supports the following initialization + parameters:

    + + + + +

    Will an HTTP Strict Transport Security (HSTS) header + (Strict-Transport-Security) be set on the response for + secure requests. Any HSTS header already present will be replaced. See + RFC 6797 for further + details of HSTS. If not specified, the default value of + true will be used.

    +
    + + +

    The max age value that should be used in the HSTS header. Negative + values will be treated as zero. If not specified, the default value of + 0 will be used.

    +
    + + +

    Should the includeSubDomains parameter be included in the HSTS + header. If not specified, the default value of false will + be used.

    +
    + + +

    Should the preload parameter be included in the HSTS header. If not + specified, the default value of false will be used. See + https://hstspreload.org for + important information about this parameter.

    +
    + + +

    Should the anti click-jacking header (X-Frame-Options) + be set on the response. Any anti click-jacking header already present + will be replaced. If not specified, the default value of + true will be used.

    +
    + + +

    What value should be used for the anticlick-jacking header? Must be + one of DENY, SAMEORIGIN, + ALLOW-FROM (case-insensitive). If not specified, the + default value of DENY will be used.

    +
    + + +

    If ALLOW-FROM is used for antiClickJackingOption, + what URI should be allowed? If not specified, the default value of an + empty string will be used.

    +
    + + +

    Should the header that blocks content type sniffing + (X-Content-Type-Options) be set on every response. If + already present, the header will be replaced. If not specified, the + default value of true will be used.

    +
    + + +

    Note: This setting is deprecated as support for the HTTP + header has been removed from all major browsers. The setting has been + removed in Tomcat 11.0.x onwards.

    +

    Should the header that enables the browser's cross-site scripting + filter protection (X-XSS-Protection: 1; mode=block) + be set on every response. If already present, the header + will be replaced. If not specified, the default value of + false will be used.

    +
    + +
    + +
    + +
    + + +
    + + + +

    The Rate Limit Filter can help mitigate Denial of Service + (DoS) and Brute Force attacks by limiting the number of a requests that are + allowed from a single IP address within a time window (also referred + to as a time bucket), e.g. 300 Requests per 60 seconds.

    + +

    The filter works by incrementing a counter in a time bucket for each IP + address, and if the counter exceeds the allowed limit then further requests + from that IP are dropped with a "429 Too many requests" response + until the bucket time ends and a new bucket starts.

    + +

    The filter is optimized for efficiency and low overhead, so it converts + some configured values to more efficient values. For example, a configuration + of a 60 seconds time bucket is converted to 65.536 seconds. That allows + for very fast bucket calculation using bit shift arithmetic. In order to remain + true to the user intent, the configured number of requests is then multiplied + by the same ratio, so a configuration of 100 Requests per 60 seconds, has the + real values of 109 Requests per 65 seconds.

    + +

    It is common to set up different restrictions for different URIs. + For example, a login page or authentication script is typically expected + to get far less requests than the rest of the application, so you can add + a filter definition that would allow only 5 requests per 15 seconds and map + those URIs to it.

    + +

    You can set enforce to false + to disable the termination of requests that exceed the allowed limit. Then + your application code can inspect the Request Attribute + org.apache.catalina.filters.RateLimitFilter.Count and decide + how to handle the request based on other information that it has, e.g. allow + more requests to certain users based on roles, etc.

    + +

    WARNING: if Tomcat is behind a reverse proxy then you must + make sure that the Rate Limit Filter sees the client IP address, so if for + example you are using the Remote IP Filter, + then the filter mapping for the Rate Limit Filter must come after + the mapping of the Remote IP Filter to ensure that each request has its IP + address resolved before the Rate Limit Filter is applied. Failure to do so + will count requests from different IPs in the same bucket and will result in + a self inflicted DoS attack.

    + +
    + + + +

    The filter class name for the Remote Address Filter is + org.apache.catalina.filters.RateLimitFilter + .

    + +
    + + + +

    The Rate Limit Filter supports the following + initialisation parameters:

    + + + + +

    The number of seconds in a time bucket. Default is 60.

    +
    + + +

    The number of requests that are allowed in a time bucket. + Default is 300.

    +
    + + +

    Set to false to allow requests through even when they exceed + the maximum allowed per time window. Your application code can + still inspect the Request Attribute + org.apache.catalina.filters.RateLimitFilter.Count to retrieve + the number of Requests made from that IP within the time window. + Default is true.

    +
    + + +

    The status code to return when a request is dropped. + Default is 429.

    +
    + + +

    The status message to return when a request is dropped. + Default is "Too many requests".

    +
    + +
    + +
    + + +

    Set the site rate limit to 300 Requests per minute (default):

    + + RateLimitFilter Global + org.apache.catalina.filters.RateLimitFilter + + + + RateLimitFilter Global + * + ]]> + +

    Set the /auth/* scripts rate limit to 20 Requests per minute:

    + + RateLimitFilter Login + org.apache.catalina.filters.RateLimitFilter + + bucketRequests + 20 + + + + + RateLimitFilter Login + /auth/* + ]]> + +
    + +
    + + +
    + + + +

    The Remote Address Filter allows you to compare the + IP address of the client that submitted this request against one or more + regular expressions, and either allow the request to continue + or refuse to process the request from this client.

    + +

    The syntax for regular expressions is different than that for + 'standard' wildcard matching. Tomcat uses the java.util.regex + package. Please consult the Java documentation for details of the + expressions supported.

    + +

    Note: There is a caveat when using this filter with + IPv6 addresses. Format of the IP address that this valve is processing + depends on the API that was used to obtain it. If the address was obtained + from Java socket using Inet6Address class, its format will be + x:x:x:x:x:x:x:x. That is, the IP address for localhost + will be 0:0:0:0:0:0:0:1 instead of the more widely used + ::1. Consult your access logs for the actual value.

    + +

    See also: Remote Host Filter.

    +
    + + + +

    The filter class name for the Remote Address Filter is + org.apache.catalina.filters.RemoteAddrFilter + .

    + +
    + + + +

    The Remote Address Filter supports the following + initialisation parameters:

    + + + + +

    A regular expression (using java.util.regex) that the + remote client's IP address is compared to. If this attribute + is specified, the remote address MUST match for this request to be + accepted. If this attribute is not specified, all requests will be + accepted UNLESS the remote address matches a deny + pattern.

    +
    + + +

    A regular expression (using java.util.regex) that the + remote client's IP address is compared to. If this attribute + is specified, the remote address MUST NOT match for this request to be + accepted. If this attribute is not specified, request acceptance is + governed solely by the accept attribute.

    +
    + + +

    HTTP response status code that is used when rejecting denied + request. The default value is 403. For example, + it can be set to the value 404.

    +
    + +
    + +
    + + +

    To allow access only for the clients connecting from localhost:

    + + Remote Address Filter + org.apache.catalina.filters.RemoteAddrFilter + + allow + 127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1 + + + + Remote Address Filter + /* + ]]> +
    + +
    + + +
    + + + +

    The Remote Host Filter allows you to compare the + hostname of the client that submitted this request against one or more + regular expressions, and either allow the request to continue + or refuse to process the request from this client.

    + +

    The syntax for regular expressions is different than that for + 'standard' wildcard matching. Tomcat uses the java.util.regex + package. Please consult the Java documentation for details of the + expressions supported.

    + +

    Note: This filter processes the value returned by + method ServletRequest.getRemoteHost(). To allow the method + to return proper host names, you have to enable "DNS lookups" feature on + a Connector.

    + +

    See also: Remote Address Filter, + HTTP Connector configuration.

    +
    + + + +

    The filter class name for the Remote Address Filter is + org.apache.catalina.filters.RemoteHostFilter + .

    + +
    + + + +

    The Remote Host Filter supports the following + initialisation parameters:

    + + + + +

    A regular expression (using java.util.regex) that the + remote client's hostname is compared to. If this attribute + is specified, the remote hostname MUST match for this request to be + accepted. If this attribute is not specified, all requests will be + accepted UNLESS the remote hostname matches a deny + pattern.

    +
    + + +

    A regular expression (using java.util.regex) that the + remote client's hostname is compared to. If this attribute + is specified, the remote hostname MUST NOT match for this request to be + accepted. If this attribute is not specified, request acceptance is + governed solely by the accept attribute.

    +
    + + +

    HTTP response status code that is used when rejecting denied + request. The default value is 403. For example, + it can be set to the value 404.

    +
    + +
    + +
    + +
    + +
    + + + +

    The Remote CIDR Filter allows you to compare the + IP address of the client that submitted this request against one or more + netmasks following the CIDR notation, and either allow the request to + continue or refuse to process the request from this client. IPv4 and + IPv6 are both fully supported. +

    + +

    This filter mimics Apache httpd's Order, + Allow from and Deny from directives, + with the following limitations: +

    + +
      +
    • Order will always be allow, deny;
    • +
    • dotted quad notations for netmasks are not supported (that is, you + cannot write 192.168.1.0/255.255.255.0, you must write + 192.168.1.0/24; +
    • +
    • shortcuts, like 10.10., which is equivalent to + 10.10.0.0/16, are not supported; +
    • +
    • as the filter name says, this is a CIDR only filter, + therefore subdomain notations like .mydomain.com are not + supported either. +
    • +
    + +

    Some more features of this filter are: +

    + +
      +
    • if you omit the CIDR prefix, this filter becomes a single IP + filter;
    • +
    • unlike the Remote Host Filter, + it can handle IPv6 addresses in condensed form (::1, + fe80::/71, etc).
    • +
    + +
    + + + +

    The filter class name for the Remote Address Filter is + org.apache.catalina.filters.RemoteCIDRFilter + .

    + +
    + + + +

    The Remote CIDR Filter supports the following + initialisation parameters:

    + + + + +

    A comma-separated list of IPv4 or IPv6 netmasks or addresses + that the remote client's IP address is matched against. + If this attribute is specified, the remote address MUST match + for this request to be accepted. If this attribute is not specified, + all requests will be accepted UNLESS the remote IP is matched by a + netmask in the deny attribute. +

    +
    + + +

    A comma-separated list of IPv4 or IPv6 netmasks or addresses + that the remote client's IP address is matched against. + If this attribute is specified, the remote address MUST NOT match + for this request to be accepted. If this attribute is not specified, + request acceptance is governed solely by the accept + attribute. +

    +
    + +
    + +
    + + +

    To allow access only for the clients connecting from localhost and from local network 192.68.0.*:

    + + Remote CIDR Filter + org.apache.catalina.filters.RemoteCIDRFilter + + allow + 127.0.0.1, ::1, 192.68.0.0/24 + + + + + Remote CIDR Filter + /* + ]]> +
    + +
    + +
    + + + +

    Tomcat port of + mod_remoteip, + this filter replaces the apparent client remote IP address and hostname for + the request with the IP address list presented by a proxy or a load balancer + via a request headers (e.g. "X-Forwarded-For").

    + +

    Another feature of this filter is to replace the apparent scheme + (http/https), server port and request.secure with the scheme presented + by a proxy or a load balancer via a request header + (e.g. "X-Forwarded-Proto").

    + +

    If used in conjunction with Remote Address/Host filters then this filter + should be defined first to ensure that the correct client IP address is + presented to the Remote Address/Host filters.

    + +

    Note: By default this filter has no effect on the + values that are written into access log. The original values are restored + when request processing leaves the filter and that always happens earlier + than access logging. To pass the remote address, remote host, server port + and protocol values set by this filter to the access log, + they are put into request attributes. Publishing these values here + is enabled by default, but AccessLogValve should be explicitly + configured to use them. See documentation for + requestAttributesEnabled attribute of + AccessLogValve.

    + +

    The names of request attributes that are set by this filter + and can be used by access logging are the following:

    + +
      +
    • org.apache.catalina.AccessLog.RemoteAddr
    • +
    • org.apache.catalina.AccessLog.RemoteHost
    • +
    • org.apache.catalina.AccessLog.Protocol
    • +
    • org.apache.catalina.AccessLog.ServerPort
    • +
    • org.apache.tomcat.remoteAddr
    • +
    + +
    + + + +

    The filter class name for the Remote IP Filter is + org.apache.catalina.filters.RemoteIpFilter + .

    + +
    + + +

    + The filter will process the x-forwarded-for http header. +

    + + RemoteIpFilter + org.apache.catalina.filters.RemoteIpFilter + + + + RemoteIpFilter + /* + REQUEST + ]]> +
    + + + +

    + The filter will process x-forwarded-for and + x-forwarded-proto http headers. Expected value for the + x-forwarded-proto header in case of SSL connections is + https (case insensitive).

    + + RemoteIpFilter + org.apache.catalina.filters.RemoteIpFilter + + protocolHeader + x-forwarded-proto + + + + + RemoteIpFilter + /* + REQUEST + ]]> +
    + + +

    RemoteIpFilter configuration:

    + + RemoteIpFilter + org.apache.catalina.filters.RemoteIpFilter + + allowedInternalProxies + 192\.168\.0\.10|192\.168\.0\.11 + + + remoteIpHeader + x-forwarded-for + + + remoteIpProxiesHeader + x-forwarded-by + + + protocolHeader + x-forwarded-proto + + ]]> +

    Request values:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PropertyValue Before RemoteIpFilterValue After RemoteIpFilter
    request.remoteAddr 192.168.0.10 140.211.11.130
    request.header['x-forwarded-for'] 140.211.11.130, 192.168.0.10 null
    request.header['x-forwarded-by'] null null
    request.header['x-forwarded-proto'] https https
    request.scheme http https
    request.secure false true
    request.serverPort 80 443
    + +

    + Note : x-forwarded-by header is null because only + internal proxies has been traversed by the request. + x-forwarded-for is null because all the proxies are + trusted or internal. +

    +
    + + + +

    RemoteIpFilter configuration:

    + + RemoteIpFilter + org.apache.catalina.filters.RemoteIpFilter + + allowedInternalProxies + 192\.168\.0\.10|192\.168\.0\.11 + + + remoteIpHeader + x-forwarded-for + + + remoteIpProxiesHeader + x-forwarded-by + + + trustedProxies + proxy1|proxy2 + + ]]> +

    Request values:

    + + + + + + + + + + + + + + + + + + + + + +
    PropertyValue Before RemoteIpFilterValue After RemoteIpFilter
    request.remoteAddr 192.168.0.10 140.211.11.130
    request.header['x-forwarded-for'] 140.211.11.130, proxy1, proxy2 null
    request.header['x-forwarded-by'] null proxy1, proxy2
    + +

    + Note : proxy1 and proxy2 are both trusted proxies that + come in x-forwarded-for header, they both are migrated in + x-forwarded-by header. x-forwarded-for is null + because all the proxies are trusted or internal. +

    +
    + + +

    RemoteIpFilter configuration:

    + + RemoteIpFilter + org.apache.catalina.filters.RemoteIpFilter + + allowedInternalProxies + 192\.168\.0\.10|192\.168\.0\.11 + + + remoteIpHeader + x-forwarded-for + + + remoteIpProxiesHeader + x-forwarded-by + + + trustedProxies + proxy1|proxy2 + + ]]> +

    Request values:

    + + + + + + + + + + + + + + + + + + + + + +
    PropertyValue Before RemoteIpFilterValue After RemoteIpFilter
    request.remoteAddr 192.168.0.10 140.211.11.130
    request.header['x-forwarded-for'] 140.211.11.130, proxy1, proxy2, 192.168.0.10 null
    request.header['x-forwarded-by'] null proxy1, proxy2
    + +

    + Note : proxy1 and proxy2 are both trusted proxies that + come in x-forwarded-for header, they both are migrated in + x-forwarded-by header. As 192.168.0.10 is an internal + proxy, it does not appear in x-forwarded-by. + x-forwarded-for is null because all the proxies are + trusted or internal. +

    +
    + + + +

    RemoteIpFilter configuration:

    + + RemoteIpFilter + org.apache.catalina.filters.RemoteIpFilter + + allowedInternalProxies + 192\.168\.0\.10|192\.168\.0\.11 + + + remoteIpHeader + x-forwarded-for + + + remoteIpProxiesHeader + x-forwarded-by + + + trustedProxies + proxy1|proxy2 + + ]]> +

    Request values:

    + + + + + + + + + + + + + + + + + + + + + +
    PropertyValue Before RemoteIpFilterValue After RemoteIpFilter
    request.remoteAddr 192.168.0.10 untrusted-proxy
    request.header['x-forwarded-for'] 140.211.11.130, untrusted-proxy, proxy1 140.211.11.130
    request.header['x-forwarded-by'] null proxy1
    + +

    + Note : x-forwarded-by holds the trusted proxy proxy1. + x-forwarded-by holds 140.211.11.130 because + untrusted-proxy is not trusted and thus, we cannot trust that + untrusted-proxy is the actual remote ip. + request.remoteAddr is untrusted-proxy that is an IP + verified by proxy1. +

    +
    + + + +

    The Remote IP Filter supports the + following initialisation parameters:

    + + + + +

    Should a DNS lookup be performed to provide a host name when calling + ServletRequest#getRemoteHost(). If not specified, the + default of false is used.

    +
    + + +

    Name of the HTTP Header read by this valve that holds the list of + traversed IP addresses starting from the requesting client. If not + specified, the default of x-forwarded-for is used.

    +
    + + +

    Regular expression (using java.util.regex) that a + proxy's IP address must match to be considered an internal proxy. + Internal proxies that appear in the remoteIpHeader will + be trusted and will not appear in the proxiesHeader + value. If not specified the default value of + 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|100\.6[4-9]{1}\.\d{1,3}\.\d{1,3}|100\.[7-9]{1}\d{1}\.\d{1,3}\.\d{1,3}|100\.1[0-1]{1}\d{1}\.\d{1,3}\.\d{1,3}|100\.12[0-7]{1}\.\d{1,3}\.\d{1,3}|172\.1[6-9]{1}\.\d{1,3}\.\d{1,3}|172\.2[0-9]{1}\.\d{1,3}\.\d{1,3}|172\.3[0-1]{1}\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1 + will be used.

    +
    + + +

    Name of the HTTP header created by this valve to hold the list of + proxies that have been processed in the incoming + remoteIpHeader. If not specified, the default of + x-forwarded-by is used.

    +
    + + +

    Set to true to set the request attributes used by + AccessLog implementations to override the values returned by the + request for remote address, remote host, server port and protocol. + Request attributes are also used to enable the forwarded remote address + to be displayed on the status page of the Manager web application. + If not set, the default value of true will be used.

    +
    + + +

    Regular expression (using java.util.regex) that a + proxy's IP address must match to be considered an trusted proxy. + Trusted proxies that appear in the remoteIpHeader will + be trusted and will appear in the proxiesHeader value. + If not specified, no proxies will be trusted.

    +
    + + +

    Name of the HTTP Header read by this valve that holds the protocol + used by the client to connect to the proxy. If not specified, the + default of X-Forwarded-Proto is used.

    +
    + + +

    Name of the HTTP Header read by this valve that holds the host + used by the client to connect to the proxy. If not specified, the + default of null is used.

    +
    + + +

    Name of the HTTP Header read by this valve that holds the port + used by the client to connect to the proxy. If not specified, the + default of null is used.

    +
    + + +

    Value of the protocolHeader to indicate that it is + an HTTPS request. If not specified, the default of https is + used.

    +
    + + +

    Value returned by ServletRequest.getServerPort() + when the protocolHeader indicates http + protocol and no portHeader is present. If not + specified, the default of 80 is used.

    +
    + + +

    Value returned by ServletRequest.getServerPort() + when the protocolHeader indicates https + protocol and no portHeader is present. If not + specified, the default of 443 is used.

    +
    + + +

    If true, the value returned by + ServletRequest.getLocalName() and + ServletRequest.getServerName() is modified by the this + filter. If not specified, the default of false is used.

    +
    + + +

    If true, the value returned by + ServletRequest.getLocalPort() and + ServletRequest.getServerPort() is modified by the this + filter. If not specified, the default of false is used.

    +
    + +
    + + +
    + +
    + + +
    + + + +

    The Request Dumper Filter logs information from the request and response + objects and is intended to be used for debugging purposes. When using this + Filter, it is recommended that the + org.apache.catalina.filter.RequestDumperFilter logger is + directed to a dedicated file and that the + org.apache.juli.VerbatimFormatter is used.

    + +

    WARNING: Using this filter has side-effects. The + output from this filter includes any parameters included with the request. + The parameters will be decoded using the default platform encoding. Any + subsequent calls to request.setCharacterEncoding() within + the web application will have no effect.

    + +
    + + + +

    The filter class name for the Request Dumper Filter is + org.apache.catalina.filters.RequestDumperFilter + .

    + +
    + + + +

    The Request Dumper Filter does not support any initialization + parameters.

    + +
    + + + +

    The following entries in a web application's web.xml would enable the + Request Dumper filter for all requests for that web application. If the + entries were added to CATALINA_BASE/conf/web.xml, the Request + Dumper Filter would be enabled for all web applications.

    + + requestdumper + + org.apache.catalina.filters.RequestDumperFilter + + + + requestdumper + * +]]> + +

    The following entries in CATALINA_BASE/conf/logging.properties would + create a separate log file for the Request Dumper Filter output.

    + # To this configuration below, 1request-dumper.org.apache.juli.FileHandler +# also needs to be added to the handlers property near the top of the file +1request-dumper.org.apache.juli.FileHandler.level = INFO +1request-dumper.org.apache.juli.FileHandler.directory = ${catalina.base}/logs +1request-dumper.org.apache.juli.FileHandler.prefix = request-dumper. +1request-dumper.org.apache.juli.FileHandler.encoding = UTF-8 +1request-dumper.org.apache.juli.FileHandler.formatter = org.apache.juli.VerbatimFormatter +org.apache.catalina.filters.RequestDumperFilter.level = INFO +org.apache.catalina.filters.RequestDumperFilter.handlers = \ + 1request-dumper.org.apache.juli.FileHandler +
    +
    + +
    + + +

    The Session Initializer Filter initializes the jakarta.servlet.http.HttpSession + before the Request is processed. This is required for JSR-356 compliant WebSocket implementations, + if the HttpSession is needed during the HandShake phase.

    + +

    The Java API for WebSocket does not mandate that an HttpSession would + be initialized upon request, and thus jakarta.servlet.http.HttpServletRequest's + getSession() returns null if the HttpSession was not + initialized in advance.

    + +

    This filter solves that problem by initializing the HttpSession for any HttpServletRequest + that matches its url-pattern.

    +
    + + +

    The filter class name for the Session Initializer Filter is + org.apache.catalina.filters.SessionInitializerFilter.

    +
    + + +

    The Session Initializer Filter does not support any initialization parameters.

    +
    + + +

    The following entries in the Web Application Deployment Descriptor, web.xml, + would enable the Session Initializer Filter for requests that match the given URL pattern + (in this example, "/ws/*").

    + + + SessionInitializer + org.apache.catalina.filters.SessionInitializerFilter + + + SessionInitializer + /ws/* +]]> +
    +
    + +
    + + + +

    User agents don't always include character encoding information in + requests. Depending on the how the request is processed, usually the + default encoding of ISO-8859-1 is used. This is not always + desirable. This filter provides options for setting that encoding or + forcing it to a particular value. Essentially this filter calls + ServletRequest.setCharacterEncoding() method.

    + +

    Effectively the value set by this filter is used when parsing parameters + in a POST request, if parameter parsing occurs later than this filter. Thus + the order of filter mappings is important. Note that the encoding for GET + requests is not set here, but on a Connector. See + CharacterEncoding page in the FAQ for details.

    + +
    + + + +

    The filter class name for the Set Character Encoding Filter is + org.apache.catalina.filters.SetCharacterEncodingFilter + .

    + +
    + + + +

    The Set Character Encoding Filter supports the following initialization + parameters:

    + + + + +

    Name of the character encoding which should be set.

    +
    + + +

    Determines if any character encoding specified by the user agent is + ignored. If this attribute is true, any value provided by + the user agent is ignored. If false, the encoding is only + set if the user agent did not specify an encoding. The default value + is false.

    +
    + +
    + +
    + +
    + + +
    + + + +

    Microsoft operating systems have two WebDAV clients. One is used with + port 80, the other is used for all other ports. The implementation used with + port 80 does not adhere to the WebDAV specification and fails when trying to + communicate with the Tomcat WebDAV Servlet. This Filter provides a fix for + this by forcing the use of the WebDAV implementation that works, even when + connecting via port 80.

    + +
    + + + +

    The filter class name for the WebDAV Fix Filter is + org.apache.catalina.filters.WebdavFixFilter + .

    + +
    + + + +

    The WebDAV Fix Filter does not support any initialization parameters.

    + +
    + +
    + + + +
    diff --git a/webapps/docs/config/globalresources.xml b/webapps/docs/config/globalresources.xml new file mode 100644 index 0000000..fba311f --- /dev/null +++ b/webapps/docs/config/globalresources.xml @@ -0,0 +1,284 @@ + + + +]> + + + &project; + + + Remy Maucherat + Yoav Shapira + The GlobalNamingResources Component + + + + +
    + +
    + +
    + +

    The GlobalNamingResources element defines the global + JNDI resources for the Server.

    + +

    These resources are listed in the server's global JNDI resource context. + This context is distinct from the per-web-application JNDI contexts + described in + the JNDI Resources How-To. + The resources defined in this element are not visible in + the per-web-application contexts unless you explicitly link them with + <ResourceLink> elements. +

    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + + + +

    You can configure named values that will be made visible to all + web applications as environment entry resources by nesting + <Environment> entries inside this element. For + example, you can create an environment entry like this:

    + + ... + + ... +]]> + +

    This is equivalent to the inclusion of the following element in the + web application deployment descriptor (/WEB-INF/web.xml): +

    + + maxExemptions + 10 + java.lang.Integer +]]> +

    but does not require modification of the deployment descriptor + to customize this value.

    + +

    The valid attributes for an <Environment> element + are as follows:

    + + + + +

    Optional, human-readable description of this environment entry.

    +
    + + +

    The name of the environment entry to be created, relative to the + java:comp/env context.

    +
    + + +

    Set this to false if you do not want + an <env-entry> for the same environment entry name, + found in the web application deployment descriptor, to override the + value specified here. By default, overrides are allowed.

    +
    + + +

    The fully qualified Java class name expected by the web application + for this environment entry. Must be a legal value for + <env-entry-type> in the web application deployment + descriptor.

    +
    + + +

    The parameter value that will be presented to the application + when requested from the JNDI context. This value must be convertible + to the Java type defined by the type attribute.

    +
    + +
    + +
    + + + + +

    You can declare the characteristics of resources + to be returned for JNDI lookups of <resource-ref> and + <resource-env-ref> elements in the web application + deployment descriptor by defining them in this element and then linking + them with <ResourceLink> + elements + in the <Context> element. + + You MUST also define any other needed parameters using + attributes on the Resource element, to configure + the object factory to be used (if not known to Tomcat already), and + the properties used to configure that object factory.

    + +

    For example, you can create a resource definition like this:

    + + ... + + ... +]]> + +

    This is equivalent to the inclusion of the following element in the + web application deployment descriptor (/WEB-INF/web.xml):

    + + Employees Database for HR Applications + jdbc/EmployeeDB + javax.sql.DataSource + Container +]]> + +

    but does not require modification of the deployment + descriptor to customize this value.

    + +

    The valid attributes for a <Resource> element + are as follows:

    + + + + +

    Specify whether the web Application code signs on to the + corresponding resource manager programmatically, or whether the + Container will sign on to the resource manager on behalf of the + application. The value of this attribute must be + Application or Container. This + attribute is required if the web application + will use a <resource-ref> element in the web + application deployment descriptor, but is optional if the + application uses a <resource-env-ref> instead.

    +
    + + +

    Name of the zero-argument method to call on a singleton resource when + it is no longer required. This is intended to speed up clean-up of + resources that would otherwise happen as part of garbage collection. + This attribute is ignored if the singleton attribute is + false. If not specified, no default is defined and no close method will + be called.

    +

    For Apache Commons DBCP 2 and Apache Tomcat JDBC connection pools + you can use closeMethod="close". Note that Apache Commons + DBCP 2 requires this to be set for a clean shutdown. When using the + default Tomcat connection pool (based on DBCP 2) Tomcat will set this + attribute automatically unless it is explicitly set to the empty + string.

    +
    + + +

    Optional, human-readable description of this resource.

    +
    + + +

    The name of the resource to be created, relative to the + java:comp/env context.

    +
    + + +

    Specify whether connections obtained through this resource + manager can be shared. The value of this attribute must be + Shareable or Unshareable. By default, + connections are assumed to be shareable.

    +
    + + +

    Specify whether this resource definition is for a singleton resource, + i.e. one where there is only a single instance of the resource. If this + attribute is true, multiple JNDI lookups for this resource + will return the same object. If this attribute is false, + multiple JNDI lookups for this resource will return different objects. + This attribute must be true for + javax.sql.DataSource resources to enable JMX registration + of the DataSource. The value of this attribute must be true + or false. By default, this attribute is true. +

    +
    + + +

    The fully qualified Java class name expected by the web + application when it performs a lookup for this resource.

    +
    + +
    + + +
    + + +

    Use <ResourceLink> + elements to link resources from the global context into + per-web-application contexts. Here is an example of making a custom + factory available to an application, based on the example definition in the + + JNDI Resource How-To: +

    + + + +]]> + +
    + + + +

    You can declare the characteristics of the UserTransaction + to be returned for JNDI lookup for java:comp/UserTransaction. + You MUST define an object factory class to instantiate + this object as well as the needed resource parameters as attributes of the + Transaction + element, and the properties used to configure that object factory.

    + +

    The valid attributes for the <Transaction> element + are as follows:

    + + + + +

    The class name for the JNDI object factory.

    +
    + +
    + +
    + +
    + + + + + +
    diff --git a/webapps/docs/config/host.xml b/webapps/docs/config/host.xml new file mode 100644 index 0000000..3db2c44 --- /dev/null +++ b/webapps/docs/config/host.xml @@ -0,0 +1,689 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Remy Maucherat + Yoav Shapira + The Host Container + + + + +
    + +
    + +
    + +

    The Host element represents a virtual host, + which is an association of a network name for a server (such as + "www.mycompany.com") with the particular server on which Tomcat is running. + For clients to be able to connect to a Tomcat server using its network name, + this name must be registered in the Domain Name Service (DNS) server + that manages the Internet domain you belong to - contact your Network + Administrator for more information.

    + +

    In many cases, System Administrators wish to associate more than + one network name (such as www.mycompany.com and + company.com) with the same virtual host and applications. + This can be accomplished using the Host + Name Aliases feature discussed below.

    + +

    One or more Host elements are nested inside an + Engine element. Inside the Host element, you + can nest Context elements for the web + applications associated with this virtual host. Exactly one of the Hosts + associated with each Engine MUST have a name matching the + defaultHost attribute of that Engine.

    + +

    Clients normally use host names to identify the server they wish to connect + to. This host name is also included in the HTTP request headers. Tomcat + extracts the host name from the HTTP headers and looks for a + Host with a matching name. If no match is found, the request + is routed to the default host. The name of the default host does not have to + match a DNS name (although it can) since any request where the DNS name does + not match the name of a Host element will be routed to the + default host.

    + +

    The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + +
    + + +
    + + + +

    All implementations of Host + support the following attributes:

    + + + + +

    The Application Base directory for this virtual host. + This is the pathname of a directory that may contain web applications + to be deployed on this virtual host. You may specify an + absolute pathname, or a pathname that is relative to the + $CATALINA_BASE directory. See + Automatic Application + Deployment for more information on automatic recognition and + deployment of web applications. If not specified, the default of + webapps will be used.

    +
    + + +

    The XML Base directory for this virtual host. + This is the pathname of a directory that may contain context XML + descriptors to be deployed on this virtual host. You may specify an + absolute pathname for this directory, or a pathname that is relative + to the $CATALINA_BASE directory. See + Automatic Application + Deployment for more information on automatic recognition and + deployment of web applications. If not specified the default of + conf/<engine_name>/<host_name> will be used.

    +
    + + +

    If set to true, Tomcat will attempt to create the + directories defined by the attributes appBase and + xmlBase during the startup phase. The default value is + true. If set to true, and directory creation + fails, an error message will be printed out but will not halt the + startup sequence.

    +
    + + +

    This flag value indicates if Tomcat should check periodically for new + or updated web applications while Tomcat is running. If + true, Tomcat periodically checks the appBase + and xmlBase directories and deploys any new web + applications or context XML descriptors found. Updated web applications + or context XML descriptors will trigger a reload of the web application. + The default is true. See + Automatic Application + Deployment for more information.

    +
    + + +

    This value represents the delay in seconds between the + invocation of the backgroundProcess method on this host and + its child containers, including all contexts. + Child containers will not be invoked if their delay value is not + negative (which would mean they are using their own processing + thread). Setting this to a positive value will cause + a thread to be spawn. After waiting the specified amount of time, + the thread will invoke the backgroundProcess method on this host + and all its child containers. A host will use background processing to + perform live web application deployment related tasks. If not + specified, the default value for this attribute is -1, which means + the host will rely on the background processing setting of its parent + engine.

    +
    + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Host interface. + If not specified, the standard value (defined below) will be used.

    +
    + + +

    A regular expression defining paths to ignore when + autoDeploy and deployOnStartup are set. This + allows you to keep your configuration in a version control system, for + example, and not deploy a .svn, .git or other + configuration control system folder that happens to be in the + appBase.

    +

    This regular expression is relative to appBase. It is + also anchored, meaning the match is performed against the + entire file/directory name. So, foo matches only a file or + directory named foo but not foo.war, + foobar, or myfooapp. To match anything with + "foo", you could use .*foo.*.

    +

    See Automatic Application + Deployment for more information.

    +
    + + +

    This flag value indicates if web applications from this host should + be automatically deployed when Tomcat starts. The default is + true. See + Automatic Application + Deployment for more information.

    +
    + + +

    Set to true to have each child contexts fail its startup + if any of its servlet that has load-on-startup >=0 fails its own + startup.

    +

    Each child context may override this attribute.

    +

    If not specified, the default value of false is + used.

    +
    + + +

    The legacy Application Base directory for this virtual host. This is + the pathname of a directory that may contain Java EE web applications to + be converted to Jakarta EE before deployment. Java EE applications + packaged as WAR files or directories placed in this directory will be + converted to Jakarta EE using the Apache Tomcart Migration Tool for + Jakarta EE. The conversion will be performed using default settings. The + resulting WAR or directory will be placed in the appBase + confiugured for this virtual host.

    +

    If the default settings are not appropriate for the migration of an + application, the full range of migration options may be accessed by + performing the migration manually. The migrate.[sh|bat] + scripts are provided in the $CATALINA_HOME/bin directory + for this purpose.

    +

    You may specify an absolute pathname, or a pathname that is + relative to the $CATALINA_BASE directory. If not specified, + the default of webapps-javaee will be used.

    +
    + + +

    Usually the network name of this virtual host, as registered in your + Domain Name Service server. Regardless of the case used to + specify the host name, Tomcat will convert it to lower case internally. + One of the Hosts nested within an Engine MUST + have a name that matches the defaultHost setting for that + Engine. See Host Name Aliases for + information on how to assign more than one network name to the same + virtual host. The name can not contain a wildcard, this is only + valid in an Alias.

    +
    + + +

    The number of threads this Host will use to start + child Context elements in parallel. The same + thread pool will be used to deploy new + Contexts if automatic deployment is being + used. + As the thread pool is shared at the Server level, if more than one + Host specifies this setting, only the maximum value will apply and will + be used for all except for the special value 1. If + not specified, the default value of 1 will be used. If 1 thread is + used then rather than using an ExecutorService the current + thread will be used.

    +
    + + +

    This flag determines if Tomcat, as part of the auto deployment + process, will check for old, unused versions of web applications + deployed using parallel deployment and, if any are found, remove them. + This flag only applies if autoDeploy is true. + If not specified the default value of false will be used. +

    +
    + +
    + +
    + + + + +

    The standard implementation of Host is + org.apache.catalina.core.StandardHost. + It supports the following additional attributes (in addition to the + common attributes listed above):

    + + + + +

    Set to true if you want a context XML descriptor + embedded inside the application (located at + /META-INF/context.xml) to be copied to xmlBase + when the application is deployed. On subsequent starts, the copied + context XML descriptor will be used in preference to any context XML + descriptor embedded inside the application even if the descriptor + embedded inside the application is more recent. The default is + false. Note if deployXML is + false, this attribute will have no effect.

    +
    + + +

    Set to false if you want to disable parsing the context + XML descriptor embedded inside the application (located at + /META-INF/context.xml). Security conscious environments + should set this to false to prevent applications from + interacting with the container's configuration. The administrator will + then be responsible for providing an external context configuration + file, and putting it in the location defined by the + xmlBase attribute. If this flag is false, + a descriptor is located at /META-INF/context.xml and no + descriptor is present in xmlBase then the context will + fail to start in case the descriptor contains necessary configuration + for secure deployment (such as a RemoteAddrValve) which should not be + ignored. The default is true unless a security manager is + enabled when the default is false. When running under a + security manager this may be enabled on a per web application basis by + granting the + org.apache.catalina.security.DeployXmlPermission to the web + application. The Manager and Host Manager applications are granted this + permission by default so that they continue to work when running under a + security manager.

    +
    + + +

    Java class name of the error reporting valve which will be used + by this Host. The responsibility of this valve is to output error + reports. Setting this property allows to customize the look of the + error pages which will be generated by Tomcat. This class must + implement the + org.apache.catalina.Valve interface. If none is specified, + the value org.apache.catalina.valves.ErrorReportValve + will be used by default. if set to an empty string, the error report + will be disabled.

    +
    + + +

    Set to true if you want web applications that are + placed in the appBase directory as web application + archive (WAR) files to be unpacked into a corresponding disk directory + structure, false to run such web applications directly + from a WAR file. The default is true. See + Automatic Application + Deployment for more information.

    +

    Note: If Tomcat expands the WAR file then it will add a file + (/META-INF/war-tracking) to the unpacked directory + structure which it uses to detect changes in the WAR file while Tomcat + is not running. Any such change will trigger the deletion of the + expanded directory and the deployment of the updated WAR file when + Tomcat next starts.

    +

    Note: Running with this option set to false will incur + a performance penalty. To avoid a significant performance penalty, the + web application should be configured such that class scanning for + Servlet 3.0+ pluggability features is not required. Users may also wish + to consider the ExtractingRoot + Resources implementation.

    +
    + + +

    Pathname to a scratch directory to be used by applications for + this Host. Each application will have its own sub directory with + temporary read-write use. Configuring a Context workDir will override + use of the Host workDir configuration. This directory will be made + visible to servlets in the web application by a servlet context + attribute (of type java.io.File) named + jakarta.servlet.context.tempdir as described in the + Servlet Specification. If not specified, a suitable directory + underneath $CATALINA_BASE/work will be provided.

    +
    + +
    + +
    + + +
    + + +
    + +

    You can nest one or more Context elements + inside this Host element, each representing a different web + application associated with this virtual host.

    + +

    You can nest at most one instance of the following utility components + by nesting a corresponding element inside your Host + element:

    +
      +
    • Realm - + Configure a realm that will allow its + database of users, and their associated roles, to be shared across all + Contexts nested inside this Host (unless + overridden by a Realm configuration + at a lower level).
    • +
    + +
    + + +
    + + + + +

    A host is associated with the + org.apache.catalina.core.ContainerBase.[engine_name].[host_name] + log category. Note that the brackets are part of the name, + don't omit them.

    + +
    + + + + +

    When you run a web server, one of the output files normally generated + is an access log, which generates one line of information for + each request processed by the server, in a standard format. Catalina + includes an optional Valve implementation that + can create access logs in the same standard format created by web servers, + or in any number of custom formats.

    + +

    You can ask Catalina to create an access log for all requests + processed by an Engine, + Host, or Context + by nesting a Valve element like this:

    + + + ... + + ... +]]> + +

    See Access Logging Valves + for more information on the configuration attributes that are + supported.

    + +
    + + + + +

    If you are using the standard Host implementation with + default settings then applications in the appBase or with context + files in the configBase are automatically deployed when Tomcat + starts (the deployOnStartup property defaults to + true) and reloaded or redeployed (as appropriate) when a change + is detected while Tomcat is running (the autoDeploy attribute + also defaults to true).

    + +

    deployOnStartup and autoDeploy trigger + execution of exactly the same code so the behaviour is very similar. + However, there is one key difference. When Tomcat starts it has no knowledge + of which files are the same, which have been changed and which are new. It + therefore treats all files as new. While Tomcat is running, it can + differentiate between unchanged, modified and new files. This leads to some + differences in behaviour between files being modified while Tomcat is + running and files being modified while Tomcat is stopped.

    + +

    When you use automatic deployment, related files (a web application may + have a context.xml file, a WAR and a directory) that exist in the + Host's appBase and/or configBase + must conform to the expected naming + convention. In short, this means files for the same web application must + share the same base name.

    + +

    The automatic deployment process identifies new and/or modified web + applications using the following search order:

    + +
      +
    1. Web applications with a context.xml file located in the Host's + configBase.
    2. +
    3. Web applications with a WAR file located in the Host's + appBase that have not already been identified during the scan for + context.xml files.
    4. +
    5. Web applications with a directory located in the Host's + appBase that have not already been identified during the scans + for context.xml and/or WAR files.
    6. +
    + +

    When autoDeploy is true, the automatic + deployment process will monitor the deployed web applications for changes. + Depending on exactly what changes, the web application will either be + re-deployed or reloaded. Re-deployment involves the creation of a new web + application and, if using the standard session manager, user sessions will + not be retained. Reloading uses the existing web application but re-parses + the web.xml and reloads any classes. If using the standard session manager, + user sessions will be persisted.

    + +

    Users may add to the files that the automatic deployment process monitors + for reloading (i.e. any change to one of these files triggers a reload of + the web application) by adding a WatchedResources element to the + context.xml file. See the + Context documentation for + further details.

    + +

    When using automatic deployment, the docBase defined by + an XML Context file should be outside of the + appBase directory. If this is not the case, difficulties + may be experienced deploying the web application or the application may + be deployed twice. The deployIgnore attribute can be used + to avoid this situation.

    + +

    Note that if you are defining contexts explicitly in server.xml, you + should probably turn off automatic application deployment or specify + deployIgnore carefully. Otherwise, the web applications + will each be deployed twice, and that may cause problems for the + applications.

    + +

    There are many possible combinations of settings, new files, changed + files and deleted files. A separate page describes the + expected behaviour of the automatic + deployment process in many of these scenarios.

    + +
    + + + + +

    In many server environments, Network Administrators have configured + more than one network name (in the Domain Name Service (DNS) + server), that resolve to the IP address of the same server. Normally, + each such network name would be configured as a separate + Host element in conf/server.xml, each + with its own set of web applications.

    + +

    However, in some circumstances, it is desirable that two or more + network names should resolve to the same virtual host, + running the same set of applications. A common use case for this + scenario is a corporate web site, where it is desirable that users + be able to utilize either www.mycompany.com or + company.com to access exactly the same content and + applications.

    + +

    This is accomplished by utilizing one or more Alias + elements nested inside your Host element. For + example:

    + + ... + mycompany.com + ... +]]> + +

    In order for this strategy to be effective, all of the network names + involved must be registered in your DNS server to resolve to the + same computer that is running this instance of Catalina.

    + +

    Aliases may also use the wildcard form (*.domainname), + unlike for the name attribute of a Host. +

    +
    + + + + +

    If you have implemented a Java object that needs to know when this + Host is started or stopped, you can declare it by + nesting a Listener element inside this element. The + class name you specify must implement the + org.apache.catalina.LifecycleListener interface, and + it will be notified about the occurrence of the corresponding + lifecycle events. Configuration of such a listener looks like this:

    + + + ... + + ... +]]> + +

    Note that a Listener can have any number of additional properties + that may be configured from this element. Attribute names are matched + to corresponding JavaBean property names using the standard property + method naming patterns.

    + +
    + + + + +

    You can ask Catalina to check the IP address, or host name, on every + incoming request directed to the surrounding + Engine, Host, or + Context element. The remote address or name + will be checked against configured "accept" and/or "deny" + filters, which are defined using java.util.regex Regular + Expression syntax. Requests that come from locations that are + not accepted will be rejected with an HTTP "Forbidden" error. + Example filter declarations:

    + + + ... + + + ... +]]> + +

    See Remote Address Filter + and Remote Host Filter for + more information about the configuration options that are supported.

    + +
    + + + + +

    In many environments, but particularly in portal environments, it + is desirable to have a user challenged to authenticate themselves only + once over a set of web applications deployed on a particular virtual + host. This can be accomplished by nesting an element like this inside + the Host element for this virtual host:

    + + + ... + + ... +]]> + +

    The Single Sign On facility operates according to the following rules: +

    +
      +
    • All web applications configured for this virtual host must share the + same Realm. In practice, that means you can + nest the Realm element inside this Host element (or the surrounding + Engine element), but not inside a + Context element for one of the involved + web applications.
    • +
    • As long as the user accesses only unprotected resources in any of the + web applications on this virtual host, they will not be challenged + to authenticate themselves.
    • +
    • As soon as the user accesses a protected resource in + any web application associated with this virtual + host, the user will be challenged to authenticate himself or herself, + using the login method defined for the web application currently + being accessed.
    • +
    • Once authenticated, the roles associated with this user will be + utilized for access control decisions across all + of the associated web applications, without challenging the user + to authenticate themselves to each application individually.
    • +
    • As soon as the user logs out of one web application (for example, + by invalidating the corresponding session if form + based login is used), the user's sessions in all + web applications will be invalidated. Any subsequent attempt to + access a protected resource in any application will require the + user to authenticate himself or herself again.
    • +
    • The Single Sign On feature utilizes HTTP cookies to transmit a token + that associates each request with the saved user identity, so it can + only be utilized in client environments that support cookies.
    • +
    + +
    + + + + +

    Many web servers can automatically map a request URI starting with + a tilde character ("~") and a username to a directory (commonly named + public_html) in that user's home directory on the server. + You can accomplish the same thing in Catalina by using a special + Listener element like this (on a Unix system that + uses the /etc/passwd file to identify valid users):

    + + + ... + + ... +]]> + +

    On a server where /etc/passwd is not in use, you can + request Catalina to consider all directories found in a specified base + directory (such as c:\Homes in this example) to be + considered "user home" directories for the purposes of this directive:

    + + + ... + + ... +]]> + +

    If a user home directory has been set up for a user named + craigmcc, then its contents will be visible from a + client browser by making a request to a URL like:

    + +http://www.mycompany.com:8080/~craigmcc + +

    Successful use of this feature requires recognition of the following + considerations:

    +
      +
    • Each user web application will be deployed with characteristics + established by the global and host level default context settings.
    • +
    • It is legal to include more than one instance of this Listener + element. This would only be useful, however, in circumstances + where you wanted to configure more than one "homeBase" directory.
    • +
    • The operating system username under which Catalina is executed + MUST have read access to each user's web application directory, + and all of its contents.
    • +
    + +
    + + +

    You can override the default values found in conf/context.xml and + conf/web.xml files from $CATALINA_BASE for each virtual host. + Tomcat will look for files named context.xml.default and web.xml.default + in the directory specified by xmlBase and merge the files into + those found in the default ones.

    +
    + +
    + + + + + +
    diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml new file mode 100644 index 0000000..010f403 --- /dev/null +++ b/webapps/docs/config/http.xml @@ -0,0 +1,1792 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Yoav Shapira + The HTTP Connector + + + + +
    + +
    + +
    + +

    The HTTP Connector element represents a + Connector component that supports the HTTP/1.1 protocol. + It enables Catalina to function as a stand-alone web server, in addition + to its ability to execute servlets and JSP pages. A particular instance + of this component listens for connections on a specific TCP port number + on the server. One or more such Connectors can be + configured as part of a single Service, each + forwarding to the associated Engine to perform + request processing and create the response.

    + +

    If you wish to configure the Connector that is used + for connections to web servers using the AJP protocol (such as the + mod_jk 1.2.x connector for Apache 1.3), please refer to the + AJP Connector documentation.

    + +

    Each incoming, non-asynchronous request requires a thread for the duration + of that request. If more simultaneous requests are received than can be + handled by the currently available request processing threads, additional + threads will be created up to the configured maximum (the value of the + maxThreads attribute). If still more simultaneous requests are + received, Tomcat will accept new connections until the current number of + connections reaches maxConnections. Connections are queued inside + the server socket created by the Connector until a thread + becomes available to process the connection. Once maxConnections + has been reached the operating system will queue further connections. The size + of the operating system provided connection queue may be controlled by the + acceptCount attribute. If the operating system queue fills, + further connection requests may be refused or may time out.

    + +
    + + +
    + + + +

    All implementations of Connector + support the following attributes:

    + + + + +

    If this is true the '\' character will be permitted as a + path delimiter.

    +

    If not specified, the default value of false will be used.

    +
    + + +

    A boolean value which can be used to enable or disable the TRACE + HTTP method. If not specified, this attribute is set to false. As per RFC + 7231 section 4.3.8, cookie and authorization headers will be excluded from + the response to the TRACE request. If you wish to include these, you can + implement the doTrace() method for the target Servlet and + gain full control over the response.

    +
    + + +

    The default timeout for asynchronous requests in milliseconds. If not + specified, this attribute is set to the Servlet specification default of + 30000 (30 seconds).

    +
    + + +

    A boolean value which can be used to enable or disable the recycling + of the facade objects that isolate the container internal request + processing objects. If set to true the facades will be + set for garbage collection after every request, otherwise they will be + reused. This setting has no effect when the security manager is enabled. + If not specified, this attribute is set to true.

    +
    + + +

    Set to true if you want calls to + request.getRemoteHost() to perform DNS lookups in + order to return the actual host name of the remote client. Set + to false to skip the DNS lookup and return the IP + address in String form instead (thereby improving performance). + By default, DNS lookups are disabled.

    +
    + + +

    When set to reject request paths containing a + %2f sequence will be rejected with a 400 response. When set + to decode request paths containing a %2f + sequence will have that sequence decoded to / at the same + time other %nn sequences are decoded. When set to + passthrough request paths containing a %2f + sequence will be processed with the %2f sequence unchanged. + If not specified the default value is reject.

    +
    + + +

    If this is true then + a call to Response.getWriter() if no character encoding + has been specified will result in subsequent calls to + Response.getCharacterEncoding() returning + ISO-8859-1 and the Content-Type response header + will include a charset=ISO-8859-1 component. (SRV.15.2.22.1)

    +

    If not specified, the default specification compliant value of + true will be used.

    +
    + + +

    The maximum number of cookies that are permitted for a request. A value + of less than zero means no limit. If not specified, a default value of 200 + will be used.

    +
    + + +

    The maximum total number of request parameters (including uploaded + files) obtained from the query string and, for POST requests, the request + body if the content type is + application/x-www-form-urlencoded or + multipart/form-data. Request parameters beyond this limit + will be ignored. A value of less than 0 means no limit. If not specified, + a default of 10000 is used. Note that FailedRequestFilter + filter can be used to reject requests that + exceed the limit.

    +
    + + +

    The maximum size in bytes of the POST which will be handled by + the container FORM URL parameter parsing. The limit can be disabled by + setting this attribute to a value less than zero. If not specified, this + attribute is set to 2097152 (2 MiB). Note that the + FailedRequestFilter + can be used to reject requests that exceed this limit.

    +
    + + +

    The maximum size in bytes of the request body which will be + saved/buffered by the container during FORM or CLIENT-CERT authentication + or during HTTP/1.1 upgrade. For both types of authentication, the request + body will be saved/buffered before the user is authenticated. For + CLIENT-CERT authentication, the request body is buffered for the duration + of the SSL handshake and the buffer emptied when the request is processed. + For FORM authentication the POST is saved whilst the user is re-directed + to the login form and is retained until the user successfully + authenticates or the session associated with the authentication request + expires. For HTTP/1.1 upgrade, the request body is buffered for the + duration of the upgrade process. The limit can be disabled by setting this + attribute to -1. Setting the attribute to zero will disable the saving of + the request body data during authentication and HTTP/1.1 upgrade. If not + specified, this attribute is set to 4096 (4 kilobytes).

    +
    + + +

    A comma-separated list of HTTP methods for which request + bodies using application/x-www-form-urlencoded will be parsed + for request parameters identically to POST. This is useful in RESTful + applications that want to support POST-style semantics for PUT requests. + Note that any setting other than POST causes Tomcat + to behave in a way that goes against the intent of the servlet + specification. + The HTTP method TRACE is specifically forbidden here in accordance + with the HTTP specification. + The default is POST

    +
    + + +

    The TCP port number on which this Connector + will create a server socket and await incoming connections. Your + operating system will allow only one server application to listen + to a particular port number on a particular IP address. If the special + value of 0 (zero) is used, then Tomcat will select a free port at random + to use for this connector. This is typically only useful in embedded and + testing applications.

    +
    + + +

    Sets the protocol to handle incoming traffic. The default value is + HTTP/1.1 which uses a Java NIO based connector.
    + To use an explicit protocol, the following values may be used:
    + org.apache.coyote.http11.Http11NioProtocol - + non blocking Java NIO connector
    + org.apache.coyote.http11.Http11Nio2Protocol - + non blocking Java NIO2 connector
    + Custom implementations may also be used.
    + Take a look at our Connector + Comparison chart. The configuration for Java connectors is + identical, for http and https. +

    +
    + + +

    If this Connector is being used in a proxy + configuration, configure this attribute to specify the server name + to be returned for calls to request.getServerName(). + See Proxy Support for more + information.

    +
    + + +

    If this Connector is being used in a proxy + configuration, configure this attribute to specify the server port + to be returned for calls to request.getServerPort(). + See Proxy Support for more + information.

    +
    + + +

    If this Connector is supporting non-SSL + requests, and a request is received for which a matching + <security-constraint> requires SSL transport, + Catalina will automatically redirect the request to the port + number specified here.

    +
    + + +

    Should this Connector reject a requests if the URI + matches one of the suspicious URIs patterns identified by the Servlet 6.0 + specification? The default value is false.

    +
    + + +

    Set this attribute to the name of the protocol you wish to have + returned by calls to request.getScheme(). For + example, you would set this attribute to "https" + for an SSL Connector. The default value is "http". +

    +
    + + +

    Set this attribute to true if you wish to have + calls to request.isSecure() to return true + for requests received by this Connector. You would want this on an + SSL Connector or a non SSL connector that is receiving data from a + SSL accelerator, like a crypto card, an SSL appliance or even a webserver. + The default value is false.

    +
    + + +

    This specifies the character encoding used to decode the URI bytes, + after %xx decoding the URL. The default value is UTF-8.

    +
    + + +

    This specifies if the encoding specified in contentType should be used + for URI query parameters, instead of using the URIEncoding. This + setting is present for compatibility with Tomcat 4.1.x, where the + encoding specified in the contentType, or explicitly set using + Request.setCharacterEncoding method was also used for the parameters from + the URL. The default value is false. +

    +

    Notes: 1) This setting is applied only to the + query string of a request. Unlike URIEncoding it does not + affect the path portion of a request URI. 2) If request character + encoding is not known (is not provided by a browser and is not set by + SetCharacterEncodingFilter or a similar filter using + Request.setCharacterEncoding method), the default encoding is always + "ISO-8859-1". The URIEncoding setting has no effect on + this default. +

    +
    + + +

    Set this attribute to true to cause Tomcat to use + the IP address that the request was received on to determine the Host + to send the request to. The default value is false.

    +
    + + +

    Set this attribute to true to cause Tomcat to advertise + support for the Servlet specification using the header recommended in the + specification. The default value is false.

    +
    + +
    + +
    + + + +

    The standard HTTP connectors (NIO and NIO2) all support the following + attributes in addition to the common Connector attributes listed above.

    + + + + +

    The maximum length of the operating system provided queue for incoming + connection requests when maxConnections has been reached. The + operating system may ignore this setting and use a different size for the + queue. When this queue is full, the operating system may actively refuse + additional connections or those connections may time out. The default + value is 100.

    +
    + + +

    The priority of the acceptor thread. The thread used to accept + new connections. The default value is 5 (the value of the + java.lang.Thread.NORM_PRIORITY constant). See the JavaDoc + for the java.lang.Thread class for more details on what + this priority means.

    +
    + + +

    For servers with more than one IP address, this attribute specifies + which address will be used for listening on the specified port. By + default, the connector will listen all local addresses. Unless the JVM is + configured otherwise using system properties, the Java based connectors + (NIO, NIO2) will listen on both IPv4 and IPv6 addresses when configured + with either 0.0.0.0 or ::.

    +
    + + +

    By default Tomcat will reject requests that specify a host in the + request line but specify a different host in the host header. This + check can be disabled by setting this attribute to true. If + not specified, the default is false. +
    + This setting will be removed in Tomcat 11 onwards where it will be + hard-coded to false.

    +
    + + +

    By default Tomcat will ignore all trailer headers when processing + chunked input. For a header to be processed, it must be added to this + comma-separated list of header names.

    +
    + + +

    Controls when the socket used by the connector is bound. If set to + true it is bound when the connector is initiated and unbound + when the connector is destroyed. If set to false, the socket + will be bound when the connector is started and unbound when it is + stopped. If not specified, the default is true.

    +
    + + +

    When client certificate information is presented in a form other than + instances of java.security.cert.X509Certificate it needs to + be converted before it can be used and this property controls which JSSE + provider is used to perform the conversion.

    +
    + + +

    The value is a comma separated list of MIME types for which HTTP + compression may be used. + The default value is + + text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml + . + If you specify a type explicitly, the default is over-ridden.

    +
    + + +

    The Connector may use HTTP/1.1 GZIP compression in + an attempt to save server bandwidth. The acceptable values for the + parameter is "off" (disable compression), "on" (allow compression, which + causes text data to be compressed), "force" (forces compression in all + cases), or a numerical integer value (which is equivalent to "on", but + specifies the minimum amount of data before the output is compressed). If + the content-length is not known and compression is set to "on" or more + aggressive, the output will also be compressed. If not specified, this + attribute is set to "off".

    +

    Note: There is a tradeoff between using compression (saving + your bandwidth) and using the sendfile feature (saving your CPU cycles). + If the connector supports the sendfile feature, e.g. the NIO connector, + using sendfile will take precedence over compression. The symptoms will + be that static files greater that 48 KiB will be sent uncompressed. + You can turn off sendfile by setting useSendfile attribute + of the connector, as documented below, or change the sendfile usage + threshold in the configuration of the + DefaultServlet in the default + conf/web.xml or in the web.xml of your web + application. +

    +
    + + +

    If compression is set to "on" then this attribute + may be used to specify the minimum amount of data before the output is + compressed. If not specified, this attribute is defaults to "2048". + Units are in bytes.

    +
    + + +

    The number of seconds during which the sockets used by this + Connector will linger when they are closed. The default + value is -1 which disables socket linger.

    +
    + + +

    The number of milliseconds this Connector will wait, + after accepting a connection, for the request URI line to be + presented. Use a value of -1 to indicate no (i.e. infinite) timeout. + The default value is 60000 (i.e. 60 seconds) but note that the standard + server.xml that ships with Tomcat sets this to 20000 (i.e. 20 seconds). + Unless disableUploadTimeout is set to false, + this timeout will also be used when reading the request body (if any).

    +
    + + +

    Specifies the timeout, in milliseconds, to use while a data upload is + in progress. This only takes effect if + disableUploadTimeout is set to false. +

    +
    + + +

    When to respond with a 100 intermediate response code to a + request containing an Expect: 100-continue header. + The following values may used: +

      +
    • immediately - an intermediate 100 status response + will be returned as soon as practical
    • +
    • onRead - an intermediate 100 status + response will be returned only when the Servlet reads the request body, + allowing the servlet to inspect the headers and possibly respond + before the user agent sends a possibly large request body.
    • +
    +

    +
    + + +

    The name of the default SSLHostConfig that will be + used for secure connections (if this connector is configured for secure + connections) if the client connection does not provide SNI or if the SNI + is provided but does not match any configured + SSLHostConfig. If not specified the default value of + _default_ will be used. Provided values are always converted + to lower case.

    +
    + + +

    This flag allows the servlet container to use a different, usually + longer connection timeout during data upload. If not specified, this + attribute is set to true which disables this longer timeout. +

    +
    + + +

    A reference to the name in an Executor + element. If this attribute is set, and the named executor exists, the + connector will use the executor, and all the other thread attributes will + be ignored. Note that if a shared executor is not specified for a + connector then the connector will use a private, internal executor to + provide the thread pool.

    +
    + + +

    The time that the private internal executor will wait for request + processing threads to terminate before continuing with the process of + stopping the connector. If not set, the default is 5000 (5 + seconds).

    +
    + + +

    The number of milliseconds this Connector will wait + for another HTTP request before closing the connection. The default value + is to use the value that has been set for the + connectionTimeout attribute. + Use a value of -1 to indicate no (i.e. infinite) timeout.

    +
    + + +

    The maximum number of connections that the server will accept and + process at any given time. When this number has been reached, the server + will accept, but not process, one further connection. This additional + connection be blocked until the number of connections being processed + falls below maxConnections at which point the server will + start accepting and processing new connections again. Note that once the + limit has been reached, the operating system may still accept connections + based on the acceptCount setting. The default value + is 8192.

    +

    For NIO/NIO2 only, setting the value to -1, will disable the + maxConnections feature and connections will not be counted.

    +
    + + +

    Limits the total length of chunk extensions in chunked HTTP requests. + If the value is -1, no limit will be imposed. If not + specified, the default value of 8192 will be used.

    +
    + + +

    The maximum number of headers in a request that are allowed by the + container. A request that contains more headers than the specified limit + will be rejected. A value of less than 0 means no limit. + If not specified, a default of 100 is used.

    +
    + + +

    Provides the default value for + maxHttpRequestHeaderSize and + maxHttpResponseHeaderSize. If not specified, this + attribute is set to 8192 (8 KiB).

    +
    + + +

    The maximum permitted size of the request line and headers associated + with an HTTP request, specified in bytes. This is compared to the number + of bytes received so includes line terminators and whitespace as well as + the request line, header names and header values. If not specified, this + attribute is set to the value of the maxHttpHeaderSize + attribute.

    +

    If you see "Request header is too large" errors you can increase this, + but be aware that Tomcat will allocate the full amount you specify for + every request. For example, if you specify a maxHttpRequestHeaderSize of + 1 MB and your application handles 100 concurrent requests, you will see + 100 MB of heap consumed by request headers.

    +
    + + +

    The maximum permitted size of the response line and headers associated + with an HTTP response, specified in bytes. This is compared to the number + of bytes written so includes line terminators and whitespace as well as + the status line, header names and header values. If not specified, this + attribute is set to the value of the maxHttpHeaderSize + attribute.

    +
    + + +

    The maximum number of HTTP requests which can be pipelined until + the connection is closed by the server. Setting this attribute to 1 will + disable HTTP/1.0 keep-alive, as well as HTTP/1.1 keep-alive and + pipelining. Setting this to -1 will allow an unlimited amount of + pipelined or keep-alive HTTP requests. + If not specified, this attribute is set to 100.

    +
    + + +

    The maximum number of request body bytes (excluding transfer encoding + overhead) that will be swallowed by Tomcat for an aborted upload. An + aborted upload is when Tomcat knows that the request body is going to be + ignored but the client still sends it. If Tomcat does not swallow the body + the client is unlikely to see the response. If not specified the default + of 2097152 (2 MiB) will be used. A value of less than zero indicates + that no limit should be enforced.

    +
    + + +

    The maximum number of request processing threads to be created + by this Connector, which therefore determines the + maximum number of simultaneous requests that can be handled. If + not specified, this attribute is set to 200. If an executor is associated + with this connector, this attribute is ignored as the connector will + execute tasks using the executor rather than an internal thread pool. Note + that if an executor is configured any value set for this attribute will be + recorded correctly but it will be reported (e.g. via JMX) as + -1 to make clear that it is not used.

    +
    + + +

    Limits the total length of trailing headers in the last chunk of + a chunked HTTP request. If the value is -1, no limit will be + imposed. If not specified, the default value of 8192 will be + used.

    +
    + + +

    The minimum number of threads always kept running. This includes both + active and idle threads. If not specified, the default of 10 + is used. If an executor is associated with this connector, this attribute + is ignored as the connector will execute tasks using the executor rather + than an internal thread pool. Note that if an executor is configured any + value set for this attribute will be recorded correctly but it will be + reported (e.g. via JMX) as -1 to make clear that it is not + used.

    +
    + + +

    The value is a regular expression (using java.util.regex) + matching the user-agent header of HTTP clients for which + compression should not be used, + because these clients, although they do advertise support for the + feature, have a broken implementation. + The default value is an empty String (regexp matching disabled).

    +
    + + +

    The protocol handler caches Processor objects to speed up performance. + This setting dictates how many of these objects get cached. + -1 means unlimited, default is 200. If not using + Servlet 3.0 asynchronous processing, a good default is to use the same as + the maxThreads setting. If using Servlet 3.0 asynchronous processing, a + good default is to use the larger of maxThreads and the maximum number of + expected concurrent requests (synchronous and asynchronous).

    +
    + + +

    If an HTTP request is received that contains an illegal header name or + value (e.g. the header name is not a token) this setting determines if the + request will be rejected with a 400 response (true) or if the + illegal header be ignored (false). The default value is + true which will cause the request to be rejected. +
    + This setting will be removed in Tomcat 11 onwards where it will be + hard-coded to true.

    +
    + + +

    The HTTP/1.1 + specification requires that certain characters are %nn encoded when + used in URI paths. Unfortunately, many user agents including all the major + browsers are not compliant with this specification and use these + characters in unencoded form. To prevent Tomcat rejecting such requests, + this attribute may be used to specify the additional characters to allow. + If not specified, no additional characters will be allowed. The value may + be any combination of the following characters: + " < > [ \ ] ^ ` { | } . Any other characters + present in the value will be ignored.

    +
    + + +

    The HTTP/1.1 + specification requires that certain characters are %nn encoded when + used in URI query strings. Unfortunately, many user agents including all + the major browsers are not compliant with this specification and use these + characters in unencoded form. To prevent Tomcat rejecting such requests, + this attribute may be used to specify the additional characters to allow. + If not specified, no additional characters will be allowed. The value may + be any combination of the following characters: + " < > [ \ ] ^ ` { | } . Any other characters + present in the value will be ignored.

    +
    + + +

    The value is a regular expression (using java.util.regex) + matching the user-agent header of HTTP clients for which + HTTP/1.1 or HTTP/1.0 keep alive should not be used, even if the clients + advertise support for these features. + The default value is an empty String (regexp matching disabled).

    +
    + + +

    Overrides the Server header for the http response. If set, the value + for this attribute overrides any Server header set by a web application. + If not set, any value specified by the application is used. If the + application does not specify a value then no Server header is set.

    +
    + + +

    If true, any Server header set by a web + application will be removed. Note that if server is set, + this attribute is effectively ignored. If not set, the default value of + false will be used.

    +
    + + +

    Use this attribute to enable SSL traffic on a connector. + To turn on SSL handshake/encryption/decryption on a connector + set this value to true. + The default value is false. + When turning this value true you will want to set the + scheme and the secure attributes as well + to pass the correct request.getScheme() and + request.isSecure() values to the servlets + See SSL Support for more information. +

    +
    + + +

    If set to true, the TCP_NO_DELAY option will be + set on the server socket, which improves performance under most + circumstances. This is set to true by default.

    +
    + + +

    The priority of the request processing threads within the JVM. + The default value is 5 (the value of the + java.lang.Thread.NORM_PRIORITY constant). See the JavaDoc + for the java.lang.Thread class for more details on what + this priority means. If an executor is associated + with this connector, this attribute is ignored as the connector will + execute tasks using the executor rather than an internal thread pool. Note + that if an executor is configured any value set for this attribute will be + recorded correctly but it will be reported (e.g. via JMX) as + -1 to make clear that it is not used.

    +
    + + +

    The amount of time in milliseconds that threads will be kept alive by + the thread pool, if there are more than minSpareThreads + threads in the executor. If not specified, the default of + 60000 milliseconds is used. If an executor is associated + with this connector, this attribute + is ignored as the connector will execute tasks using the executor rather + than an internal thread pool. Note that if an executor is configured any + value set for this attribute will be recorded correctly but it will be + reported (e.g. via JMX) as -1 to make clear that it is not + used.

    +
    + + +

    If the Connector experiences an Exception during a Lifecycle transition + should the Exception be rethrown or logged? If not specified, the default + of false will be used. Note that the default can be changed + by the org.apache.catalina.startup.EXIT_ON_INIT_FAILURE + system property.

    +
    + + +

    (bool) Use this attribute to enable or disable usage of the + asynchronous IO API. The default value is true.

    +
    + + +

    (bool) Use this attribute to enable or disable the addition of the + Keep-Alive HTTP response header as described in + this + Internet-Draft. The default value is true.

    +
    + + +

    (bool) Use this attribute to enable or disable usage of virtual threads + with the internal executor. If an executor is associated with this + connector, this attribute is ignored. The default value is + false.

    +
    + +
    + +
    + + + +

    The NIO and NIO2 implementation support the following Java TCP + socket attributes in addition to the common Connector and HTTP attributes + listed above.

    + + + +

    (int)The socket receive buffer (SO_RCVBUF) size in bytes. JVM default + used if not set.

    +
    + +

    (int)The socket send buffer (SO_SNDBUF) size in bytes. JVM default + used if not set. Care should be taken if explicitly setting this value. + Very poor performance has been observed on some JVMs with values less + than ~8k.

    +
    + +

    (bool)This is equivalent to standard attribute + tcpNoDelay.

    +
    + +

    (bool)Boolean value for the socket's keep alive setting + (SO_KEEPALIVE). JVM default used if not set.

    +
    + +

    (bool)Boolean value for the socket OOBINLINE setting. JVM default + used if not set.

    +
    + +

    (bool)Boolean value for the sockets reuse address option + (SO_REUSEADDR). JVM default used if not set.

    +
    + +

    (bool)Boolean value for the sockets so linger option (SO_LINGER). + A value for the standard attribute connectionLinger + that is >=0 is equivalent to setting this to true. + A value for the standard attribute connectionLinger + that is <0 is equivalent to setting this to false. + Both this attribute and soLingerTime must be set else the + JVM defaults will be used for both.

    +
    + +

    (int)Value in seconds for the sockets so linger option (SO_LINGER). + This is equivalent to standard attribute + connectionLinger. + Both this attribute and soLingerOn must be set else the + JVM defaults will be used for both.

    +
    + +

    This is equivalent to standard attribute + connectionTimeout.

    +
    + +

    (int)The first value for the performance settings. See + Socket Performance Options. + All three performance attributes must be set else the JVM defaults will + be used for all three.

    +
    + +

    (int)The second value for the performance settings. See + Socket Performance Options. + All three performance attributes must be set else the JVM defaults will + be used for all three.

    +
    + +

    (int)The third value for the performance settings. See + Socket Performance Options. + All three performance attributes must be set else the JVM defaults will + be used for all three.

    +
    + +

    (int) The timeout for a socket unlock. When a connector is stopped, it will try to release the acceptor thread by opening a connector to itself. + The default value is 250 and the value is in milliseconds

    +
    +
    +
    + + + +

    The following attributes are specific to the NIO connector.

    + + + + +

    (int)The priority of the poller threads. + The default value is 5 (the value of the + java.lang.Thread.NORM_PRIORITY constant). See the JavaDoc + for the java.lang.Thread class for more details on what + this priority means.

    +
    + + +

    (int)The time in milliseconds to timeout on a select() for the + poller. This value is important, since connection clean up is done on + the same thread, so do not set this value to an extremely high one. The + default value is 1000 milliseconds.

    +
    + + +

    (bool)Use this attribute to enable or disable sendfile capability. + The default value is true. Note that the use of sendfile + will disable any compression that Tomcat may otherwise have performed on + the response.

    +
    + + +

    (bool)Boolean value, whether to use direct ByteBuffers or java mapped + ByteBuffers. If true then + java.nio.ByteBuffer.allocateDirect() is used to allocate + the buffers, if false then + java.nio.ByteBuffer.allocate() is used. The default value + is false.
    + When you are using direct buffers, make sure you allocate the + appropriate amount of memory for the direct memory space. On Sun's JDK + that would be something like -XX:MaxDirectMemorySize=256m. +

    +
    + + +

    (bool)Boolean value, whether to use direct ByteBuffers or java mapped + ByteBuffers for the SSL buffers. If true then + java.nio.ByteBuffer.allocateDirect() is used to allocate + the buffers, if false then + java.nio.ByteBuffer.allocate() is used. The default value + is false.
    + When you are using direct buffers, make sure you allocate the + appropriate amount of memory for the direct memory space. On Oracle's JDK + that would be something like -XX:MaxDirectMemorySize=256m. +

    +
    + + +

    (int)Each connection that is opened up in Tomcat get associated with + a read ByteBuffer. This attribute controls the size of this buffer. By + default this read buffer is sized at 8192 bytes. For lower + concurrency, you can increase this to buffer more data. For an extreme + amount of keep alive connections, decrease this number or increase your + heap size.

    +
    + + +

    (int)Each connection that is opened up in Tomcat get associated with + a write ByteBuffer. This attribute controls the size of this buffer. By + default this write buffer is sized at 8192 bytes. For low + concurrency you can increase this to buffer more response data. For an + extreme amount of keep alive connections, decrease this number or + increase your heap size.
    + The default value here is pretty low, you should up it if you are not + dealing with tens of thousands concurrent connections.

    +
    + + +

    (int)The NIOx connector uses a class called NioXChannel that holds + elements linked to a socket. To reduce garbage collection, the NIOx + connector caches these channel objects. This value specifies the size of + this cache. The default value is -2. Special values are + -1 for unlimited cache, 0 for no cache, + and -2 for a value computed using the bufferPoolSize + attribute.

    +
    + + +

    (int)The NioXChannel pool can also be size based, not used object + based. If bufferPool is not -2, then this value will not be used.
    + The value is in bytes except for special values. Special values are + -1 for unlimited cache, 0 for no cache, + and -2 for a value computed as follows:
    + NioXChannel + buffer size = read buffer size + write buffer size
    + SecureNioXChannel buffer size = application read buffer size + + application write buffer size + twice the max SNI parse size. + If the maximum memory as reported by the runtime is greater than + 1GB, then the pool size value is the memory divided by the buffer + size. Otherwise, it will be 0. +

    +
    + + +

    (int)Tomcat will cache SocketProcessor objects to reduce garbage + collection. The integer value specifies how many objects to keep in the + cache at most. The default is 0. Special values are + -1 for unlimited cache and 0 for no cache.

    +
    + + +

    (int)Tomcat will cache PollerEvent objects to reduce garbage + collection. The integer value specifies how many objects to keep in the + cache at most. The default is 0. Special values are + -1 for unlimited cache and 0 for no cache.

    +
    + + +

    Where supported, the path to a Unix Domain Socket that this + Connector will create and await incoming connections. + When this is specified, the otherwise mandatory port + attribute may be omitted. + See Unix Domain Socket Support + for more information.

    +
    + + +

    Where supported, the posix permissions that will be applied to the + to the Unix Domain Socket specified with + unixDomainSocketPath above. The + permissions are specified as a string of nine characters, in three sets + of three: (r)ead, (w)rite and e(x)ecute for owner, group and others + respectively. If a permission is not granted, a hyphen is used. If + unspecified, the permissions default to rw-rw-rw-.

    +
    + + +

    (bool)Defines if this connector should inherit an inetd/systemd network socket. + Only one connector can inherit a network socket. This can option can be + used to automatically start Tomcat once a connection request is made to + the systemd super daemon's port. + The default value is false. See the JavaDoc + for the java.nio.channels.spi.SelectorProvider class for + more details.

    +
    + +
    +
    + + + +

    The following attributes are specific to the NIO2 connector.

    + + + + +

    (bool)Use this attribute to enable or disable sendfile capability. + The default value is true. Note that the use of sendfile + will disable any compression that Tomcat may otherwise have performed on + the response.

    +
    + + +

    (bool)Boolean value, whether to use direct ByteBuffers or java mapped + ByteBuffers. If true then + java.nio.ByteBuffer.allocateDirect() is used to allocate + the buffers, if false then + java.nio.ByteBuffer.allocate() is used. The default value + is false.
    + When you are using direct buffers, make sure you allocate the + appropriate amount of memory for the direct memory space. On Sun's JDK + that would be something like -XX:MaxDirectMemorySize=256m. +

    +
    + + +

    (bool)Boolean value, whether to use direct ByteBuffers or java mapped + ByteBuffers for the SSL buffers. If true then + java.nio.ByteBuffer.allocateDirect() is used to allocate + the buffers, if false then + java.nio.ByteBuffer.allocate() is used. The default value + is false.
    + When you are using direct buffers, make sure you allocate the + appropriate amount of memory for the direct memory space. On Oracle's JDK + that would be something like -XX:MaxDirectMemorySize=256m. +

    +
    + + +

    (int)Each connection that is opened up in Tomcat get associated with + a read ByteBuffer. This attribute controls the size of this buffer. By + default this read buffer is sized at 8192 bytes. For lower + concurrency, you can increase this to buffer more data. For an extreme + amount of keep alive connections, decrease this number or increase your + heap size.

    +
    + + +

    (int)Each connection that is opened up in Tomcat get associated with + a write ByteBuffer. This attribute controls the size of this buffer. By + default this write buffer is sized at 8192 bytes. For low + concurrency you can increase this to buffer more response data. For an + extreme amount of keep alive connections, decrease this number or + increase your heap size.
    + The default value here is pretty low, you should up it if you are not + dealing with tens of thousands concurrent connections.

    +
    + + +

    (int)The NIO2 connector uses a class called Nio2Channel that holds + elements linked to a socket. To reduce garbage collection, the NIO2 + connector caches these channel objects. This value specifies the size of + this cache. The default value is 500, and represents that + the cache will hold 500 Nio2Channel objects. Other values are + -1 for unlimited cache and 0 for no cache.

    +
    + + +

    (int)Tomcat will cache SocketProcessor objects to reduce garbage + collection. The integer value specifies how many objects to keep in the + cache at most. The default is 0. Other values are + -1 for unlimited cache and 0 for no cache.

    +
    + +
    +
    + +
    + + +
    + +

    Tomcat supports Server Name Indication (SNI). This allows multiple SSL + configurations to be associated with a single secure connector with the + configuration used for any given connection determined by the host name + requested by the client. To facilitate this, the + SSLHostConfig element was added which can be used to define + one of these configurations. Any number of SSLHostConfig may + be nested in a Connector. At the same time, support was added + for multiple certificates to be associated with a single + SSLHostConfig. Each SSL certificate is therefore configured + in a Certificate element within an + SSLHostConfig. For further information, see the SSL Support + section below.

    + +

    When OpenSSL is providing the TLS implementation, one or more + OpenSSLConfCmd elements may be nested inside a + OpenSSLConf element to configure OpenSSL via OpenSSL's + SSL_CONF API. A single OpenSSLConf element may + be nested in a SSLHostConfig element. For further + information, see the SSL Support section below

    + +
    + + +
    + + + + +

    This Connector supports all of the required features + of the HTTP/1.1 protocol, as described in RFCs 7230-7235, including persistent + connections, pipelining, expectations and chunked encoding. If the client + supports only HTTP/1.0 or HTTP/0.9, the + Connector will gracefully fall back to supporting this + protocol as well. No special configuration is required to enable this + support. The Connector also supports HTTP/1.0 + keep-alive.

    + +

    RFC 7230 requires that HTTP servers always begin their responses with + the highest HTTP version that they claim to support. Therefore, this + Connector will always return HTTP/1.1 at + the beginning of its responses.

    + +
    + + + +

    HTTP/2 support is provided for TLS (h2), non-TLS via HTTP upgrade (h2c) + and direct HTTP/2 (h2c) connections. To enable HTTP/2 support for an HTTP + connector the following UpgradeProtocol element must be + nested within the Connector with a + className attribute of + org.apache.coyote.http2.Http2Protocol.

    + + + +]]> + +

    Additional configuration attributes are available. See the + HTTP/2 Upgrade Protocol documentation for details.

    + +
    + + + +

    The proxyName and proxyPort attributes can + be used when Tomcat is run behind a proxy server. These attributes + modify the values returned to web applications that call the + request.getServerName() and request.getServerPort() + methods, which are often used to construct absolute URLs for redirects. + Without configuring these attributes, the values returned would reflect + the server name and port on which the connection from the proxy server + was received, rather than the server name and port to whom the client + directed the original request.

    + +

    For more information, see the + Proxy Support How-To.

    + +
    + + + + +

    When the unixDomainSocketPath attribute is used, connectors + that support Unix Domain Sockets will bind to the socket at the given path. +

    + +

    For users of Java 16 and higher, support is provided within the NIO + connectors. +

    + +

    The socket path is created with read and write permissions for all + users. To protect this socket, place it in a directory with suitable + permissions appropriately configured to restrict access as required. + Alternatively, on platforms that support posix permissions, the + permissions on the socket can be set directly with the + unixDomainSocketPathPermissions option. +

    + +

    Tomcat will automatically remove the socket on server shutdown. If the + socket already exists startup will fail. Care must be taken by the + administrator to remove the socket after verifying that the socket isn't + already being used by an existing Tomcat process.

    + +

    The Unix Domain Socket can be accessed using the + --unix-socket option of the curl command line + client, and the Unix Domain Socket support in Apache HTTP server's + mod_proxy module. +

    + +
    + + + + +

    You can enable SSL support for a particular instance of this + Connector by setting the SSLEnabled attribute to + true.

    + +

    You will also need to set the scheme and secure + attributes to the values https and true + respectively, to pass correct information to the servlets.

    + +

    The NIO and NIO2 connectors use either the JSSE Java SSL implementation or + an OpenSSL implementation. As far as possible, common configuration attributes + are used for both JSSE and OpenSSL.

    + +

    Each secure connector must define at least one + SSLHostConfig. The names of the + SSLHostConfig elements must be unique and one of them must + match the defaultSSLHostConfigName attribute of the + Connector.

    + +

    Each SSLHostConfig must in turn define at least one + Certificate. The types of the Certificates + must be unique.

    + +

    In addition to the standard TLS related request attributes defined in + section 3.10 of the Servlet specification, Tomcat supports a number of + additional TLS related attributes. The full list may be found in the SSLSupport + Javadoc.

    + +

    For more information, see the + SSL Configuration How-To.

    + +
    + + + +

    + + + + +

    Name of the file that contains the concatenated certificate revocation + lists for the certificate authorities. The format is PEM-encoded. If not + defined, client certificates will not be checked against a certificate + revocation list (unless an OpenSSL based connector is used and + certificateRevocationListPath is defined). Relative paths + will be resolved against $CATALINA_BASE. JSSE based + connectors may also specify a URL for this attribute.

    +
    + + +

    OpenSSL only.

    +

    Name of the directory that contains the certificate revocation lists + for the certificate authorities. The format is PEM-encoded. Relative paths + will be resolved against $CATALINA_BASE.

    +
    + + +

    Set to required if you want the SSL stack to require a + valid certificate chain from the client before accepting a connection. + Set to optional if you want the SSL stack to request a client + Certificate, but not fail if one isn't presented. Set to + optionalNoCA if you want client certificates to be optional + and you don't want Tomcat to check them against the list of trusted CAs. + If the TLS provider doesn't support this option (OpenSSL does, JSSE does + not) it is treated as if optional was specified. If + optionalNoCA is configured then OCSP will also be disabled. + none value (which is the default) will not require a + certificate chain unless the client requests a resource protected by a + security constraint that uses CLIENT-CERT authentication.

    +
    + + +

    The maximum number of intermediate certificates that will be allowed + when validating client certificates. If not specified, the default value + of 10 will be used.

    +
    + + +

    OpenSSL only.

    +

    Name of the file that contains the concatenated certificates for the + trusted certificate authorities. The format is PEM-encoded.

    +
    + + +

    OpenSSL only.

    +

    Name of the directory that contains the certificates for the trusted + certificate authorities. The format is PEM-encoded.

    +
    + + +

    The ciphers to enable using the OpenSSL syntax. (See the OpenSSL + documentation for the list of ciphers supported and the syntax). + Alternatively, a comma separated list of ciphers using the standard + OpenSSL cipher names or the standard JSSE cipher names may be used.

    +

    Different versions of OpenSSL may interpret the same cipher string + differently. For example, the CCM8 ciphers were moved from + HIGH to MEDIUM in OpenSSL 3.2. Regardless of + the OpenSSL or JSSE version used, Tomcat converts the provided cipher + value to a list of ciphers in a manner consistent with the latest OpenSSL + development branch. This list of ciphers is then passed to the SSL + implementation.

    +

    Only the ciphers that are supported by the SSL implementation will be + used. Any ciphers in the list derived from a non-default cipher string + that are not supported by the SSL implementation will be logged in a + WARNING message when the Connector starts. The warning can be + avoided by providing an explicit list of ciphers that are supported by the + configured SSL implementation.

    +

    If not specified, a default (using the OpenSSL notation) of + HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!kRSA will be + used.

    +

    Note that, by default, the order in which ciphers are defined is + treated as an order of preference. See honorCipherOrder.

    +
    + + +

    OpenSSL only.

    +

    Configures if compression is disabled. The default is + true. If the OpenSSL version used does not support disabling + compression then the default for that OpenSSL version will be used.

    +
    + + +

    OpenSSL only.

    +

    Disables use of TLS session tickets (RFC 5077) if set to + true. Default is false. Note that when TLS + session tickets are in use, the full peer certificate chain will only be + available on the first connection. Subsequent connections (that use a + ticket to estrablish the TLS session) will only have the peer certificate, + not the full chain.

    +
    + + +

    Set to true to enforce the server's cipher order + (from the ciphers setting) instead of allowing + the client to choose the cipher. The default is false.

    +
    + + +

    The name of the SSL Host. This should either be the fully qualified + domain name (e.g. tomcat.apache.org) or a wild card domain + name (e.g. *.apache.org). If not specified, the default value + of _default_ will be used. Provided values are always + converted to lower case.

    +
    + + +

    OpenSSL only.

    +

    Configures if insecure renegotiation is allowed. The default is + false. If the OpenSSL version used does not support + configuring if insecure renegotiation is allowed then the default for that + OpenSSL version will be used.

    +
    + + +

    JSSE only.

    +

    The KeyManager algorithm to be used. This defaults to + KeyManagerFactory.getDefaultAlgorithm() which returns + SunX509 for Sun JVMs. IBM JVMs return + IbmX509. For other vendors, consult the JVM + documentation for the default value.

    +
    + + +

    The names of the protocols to support when communicating with clients. + This should be a list of any combination of the following: +

    +
    • SSLv2Hello
    • SSLv3
    • TLSv1
    • TLSv1.1
    • +
    • TLSv1.2
    • TLSv1.3
    • all
    +

    Each token in the list can be prefixed with a plus sign ("+") + or a minus sign ("-"). A plus sign adds the protocol, a minus sign + removes it form the current list. The list is built starting from + an empty list.

    +

    The token all is an alias for + SSLv2Hello,TLSv1,TLSv1.1,TLSv1.2,TLSv1.3.

    +

    Note that TLSv1.3 is only supported for JSSE when using a + JVM that implements TLSv1.3.

    +

    Note that SSLv2Hello will be ignored for OpenSSL based + secure connectors. If more than one protocol is specified for an OpenSSL + based secure connector it will always support SSLv2Hello. If a + single protocol is specified it will not support + SSLv2Hello.

    +

    Note that SSLv2 and SSLv3 are inherently + unsafe.

    +

    If not specified, the default value of all will be + used.

    +
    + + +

    JSSE only.

    +

    Should the JSSE provider enable certificate revocation checks? If + certificateRevocationListFile is set then this attribute + is ignored and revocation checks are always enabled. This attribute is + intended to enable revocation checks that have been configured for the + current JSSE provider via other means. If not specified, a default of + false is used.

    +
    + + +

    The number of SSL sessions to maintain in the session cache. Specify + -1 to use the implementation default. Values of zero and + above are passed to the implementation. Zero is used to specify an + unlimited cache size and is not recommended. If not specified, a default + of -1 is used.

    +
    + + +

    The time, in seconds, after the creation of an SSL session that it will + timeout. Specify -1 to use the implementation default. Values + of zero and above are passed to the implementation. Zero is used to + specify an unlimited timeout and is not recommended. If not specified, a + default of 86400 (24 hours) is used.

    +
    + + +

    JSSE only.

    +

    The SSL protocol(s) to use (a single value may enable multiple + protocols - see the JVM documentation for details). If not specified, the + default is TLS. The permitted values may be obtained from the + JVM documentation for the allowed values for algorithm when creating an + SSLContext instance e.g. + + Oracle Java 11. Note: There is overlap between this attribute and + protocols.

    +
    + + +

    JSSE only.

    +

    The name of a custom trust manager class to use to validate client + certificates. The class must have a zero argument constructor and must + also implement javax.net.ssl.X509TrustManager. If this + attribute is set, the trust store attributes may be ignored.

    +
    + + +

    JSSE only.

    +

    The algorithm to use for truststore. If not specified, the default + value returned by + javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm() is + used.

    +
    + + +

    JSSE only.

    +

    The trust store file to use to validate client certificates. The + default is the value of the javax.net.ssl.trustStore system + property. If neither this attribute nor the default system property is + set, no trust store will be configured. Relative paths + will be resolved against $CATALINA_BASE. A URL may also be + used for this attribute.

    +
    + + +

    JSSE only.

    +

    The password to access the trust store. The default is the value of the + javax.net.ssl.trustStorePassword system property. If that + property is null, no trust store password will be configured. If an + invalid trust store password is specified, a warning will be logged and an + attempt will be made to access the trust store without a password which + will skip validation of the trust store contents.

    +
    + + +

    JSSE only.

    +

    The name of the truststore provider to be used for the server + certificate. The default is the value of the + javax.net.ssl.trustStoreProvider system property. If + that property is null, the value of keystoreProvider is used + as the default. If neither this attribute, the default system property nor + keystoreProvider is set, the list of registered providers is + traversed in preference order and the first provider that supports the + truststoreType is used. +

    +
    + + +

    JSSE only.

    +

    The type of key store used for the trust store. The default is the + value of the javax.net.ssl.trustStoreType system property. If + that property is null, a single certificate has been configured for this + TLS virtual host and that certificate has a keystoreType that + is not PKCS12 then the default will be the + keystoreType of the single certificate. If none of these + identify a default, the default will be JKS. See the notes on + key store types below.

    +
    + +
    + +
    + + + +

    + + + + +

    Name of the file that contains the server certificate. The format is + PEM-encoded. Relative paths will be resolved against + $CATALINA_BASE.

    +

    In addition to the certificate, the file can also contain as optional + elements DH parameters and/or an EC curve name for ephemeral keys, as + generated by openssl dhparam and openssl ecparam, + respectively. The output of the respective OpenSSL command can simply + be concatenated to the certificate file.

    +

    This attribute is required unless + certificateKeystoreFile is specified.

    +
    + + +

    Name of the file that contains the certificate chain associated with + the server certificate used. The format is + PEM-encoded. Relative paths will be resolved against + $CATALINA_BASE.

    +

    The certificate chain used for Tomcat should not include the server + certificate as its first element.

    +

    Note that when using more than one certificate for different types, + they all must use the same certificate chain.

    +
    + + +

    JSSE only.

    +

    The alias used for the server key and certificate in the keystore. If + not specified, the first key read from the keystore will be used. The + order in which keys are read from the keystore is implementation + dependent. It may not be the case that keys are read from the keystore in + the same order as they were added. If more than one key is present in the + keystore it is strongly recommended that a keyAlias is configured to + ensure that the correct key is used.

    +
    + + +

    Name of the file that contains the server private key. The format is + PEM-encoded. The default value is the value of + certificateFile and in this case both certificate and + private key have to be in this file (NOT RECOMMENDED). Relative paths will + be resolved against $CATALINA_BASE.

    +
    + + +

    The password used to access the private key associated with the server + certificate from the specified file.

    +

    If not specified, the default behaviour for JSSE is to use the + certificateKeystorePassword. For OpenSSL the default + behaviour is not to use a password, but OpenSSL will prompt for one, + if required.

    +
    + + +

    The password file used to access the private key associated with the server + certificate from the specified file. This attribute takes precedence over + certificateKeyPassword.

    +

    If not specified, the default behaviour for JSSE is to use the + certificateKeystorePasswordFile. For OpenSSL the default + behaviour is not to use a password (file), but OpenSSL will prompt for one, + if required.

    +
    + + +

    JSSE only.

    +

    The pathname of the keystore file where you have stored the server + certificate and key to be loaded. By default, the pathname is the file + .keystore in the operating system home directory of the user + that is running Tomcat. If your keystoreType doesn't need a + file use "" (empty string) or NONE for this + parameter. Relative paths will be resolved against + $CATALINA_BASE. A URI may also be used for this attribute. + When using a domain keystore (keystoreType of + DKS), this parameter should be the URI to the domain + keystore.

    +

    This attribute is required unless + certificateFile is specified.

    +
    + + +

    JSSE only.

    +

    The password to use to access the keystore containing the server's + private key and certificate. If not specified, a default of + changeit will be used.

    +
    + + +

    JSSE only.

    +

    The password file to use to access the keystore containing the server's + private key and certificate. This attribute takes precedence over + certificateKeystorePassword.

    +
    + + +

    JSSE only.

    +

    The name of the keystore provider to be used for the server + certificate. If not specified, the value of the system property + javax.net.ssl.keyStoreProvider is used. If neither this + attribute nor the system property are set, the list of registered + providers is traversed in preference order and the first provider that + supports the keystoreType is used. +

    +
    + + +

    JSSE only.

    +

    The type of keystore file to be used for the server certificate. + If not specified, the value of the system property + javax.net.ssl.keyStoreType is used. If neither this attribute + nor the system property are set, a default value of "JKS". is + used. See the notes on key store types + below.

    +
    + + +

    The type of certificate. This is used to identify the ciphers that are + compatible with the certificate. It must be one of UNDEFINED, + RSA, DSA or EC. If only one + Certificate is nested within a SSLHostConfig + then this attribute is not required and will default to + UNDEFINED. If multiple Certificates are + nested within a SSLHostConfig then this attribute is required + and each Certificate must have a unique type.

    +
    + +
    + +
    + + + +

    When APR/native is enabled, the connectors will default to using + OpenSSL through JSSE, which may be more optimized than the JSSE Java + implementation depending on the processor being used, + and can be complemented with many commercial accelerator components.

    + +

    The following NIO and NIO2 SSL configuration attributes are not specific to + a virtual host and, therefore, must be configured on the connector.

    + + + + +

    In order to implement SNI support, Tomcat has to parse the first TLS + message received on a new TLS connection (the client hello) to extract the + requested server name. The message needs to be buffered so it can then be + passed to the JSSE implementation for normal TLS processing. In theory, + this first message could be very large although in practice it is + typically a few hundred bytes. This attribute sets the maximum message + size that Tomcat will buffer. If a message exceeds this size, the + connection will be configured as if no server name was indicated by the + client. If not specified a default of 65536 (64k) will be + used.

    +
    + + +

    The class name of the SSL implementation to use. If not specified and + the tomcat-native library is not installed, the + default of org.apache.tomcat.util.net.jsse.JSSEImplementation + will be used which wraps JVM's default JSSE provider. Note that the + JVM can be configured to use a different JSSE provider as the default. + Tomcat also bundles a special SSL implementation for JSSE that is backed + by OpenSSL. To enable it, the native library should be enabled and Tomcat + will automatically enable it and the default value of this attribute + becomes + org.apache.tomcat.util.net.openssl.OpenSSLImplementation. + In that case, the attributes from either JSSE and OpenSSL + configuration styles can be used, as long as the two types are not mixed + (for example, it is not allowed to define use of a Java keystore and + specify a separate pem private key using the OpenSSL attribute).

    +
    + +
    + +
    + + + +

    When OpenSSL is providing the TLS implementation, one or more + OpenSSLConfCmd elements may be nested inside a + OpenSSLConf element to configure OpenSSL via OpenSSL's + SSL_CONF API. A single OpenSSLConf element may + be nested in a SSLHostConfig element.

    + +

    The set of configuration file commands available depends on the OpenSSL + version being used. For a list of supported command names and values, see the + section Supported configuration file commands in the SSL_CONF_cmd(3) manual page for OpenSSL. Some of the configuration file + commands can be used as alternatives to SSLHostConfig + attributes. It is recommended that configuration file commands are only used + where the feature cannot be configured using SSLHostConfig + attributes.

    + +

    The OpenSSLConf element does not support any + attributes.

    + +

    The OpenSSLConfCmd element supports the following + attributes.

    + + + + +

    The name of the configuration file command.

    +
    + + +

    The value to use for the configuration file command.

    +
    + +
    + +
    + + + +

    In addition to the standard key store types (JKS and PKCS12), most Java + runtimes support additional key store types such as Windows-ROOT, + Windows-My, DKS as well as hardware security modules. Generally, to use + these additional keystore types with a TLS Connector in Tomcat:

    + +
      +
    • Set the certificateKeystoreType and/or truststoreType Connector + attribute (as appropriate) to the necessary type
    • +
    • If a configuration file is required, set the certificateKeystoreFile + and/or truststoreFile Connector attribute (as appropriate) to point to + the file
    • +
    • If no configuration file is required then you will almost certainly + need to explicitly set the certificateKeystoreFile and/or + truststoreFile Connector attribute (as appropriate) to the empty + string ("")
    • +
    • If a password is required, set the certificateKeystorePassword and/or + truststorePassword Connector attribute (as appropriate) to the + required password
    • +
    • If no password is required then you will almost certainly need to + explicitly set the certificateKeystorePassword and/or + truststorePassword Connector attribute (as appropriate) to the empty + string ("")
    • +
    + +

    Variations in key store implementations, combined with the key store + manipulation Tomcat does in the background to allow interoperability between + JSSE and OpenSSL configuration styles, means that some keystores may need + slightly different configuration. Assistance is always available from the + Apache Tomcat + users mailing list. We aim to document any key stores that vary from the + above advice here. Currently there are none we are aware of.

    + +
    + + + +

    Below is a small chart that shows how the connectors differ.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Java Nio Connector
    NIO
    Java Nio2 Connector
    NIO2
    ClassnameHttp11NioProtocolHttp11Nio2Protocol
    Tomcat Versionsince 6.0.xsince 8.0.x
    Support PollingYESYES
    Polling SizemaxConnectionsmaxConnections
    Read Request HeadersNon BlockingNon Blocking
    Read Request BodyBlockingBlocking
    Write Response Headers and BodyBlockingBlocking
    Wait for next RequestNon BlockingNon Blocking
    SSL SupportJava SSL or OpenSSLJava SSL or OpenSSL
    SSL HandshakeNon blockingNon blocking
    Max ConnectionsmaxConnectionsmaxConnections
    + +
    +
    + + + + +
    diff --git a/webapps/docs/config/http2.xml b/webapps/docs/config/http2.xml new file mode 100644 index 0000000..f78aa03 --- /dev/null +++ b/webapps/docs/config/http2.xml @@ -0,0 +1,260 @@ + + + +]> + + + &project; + + + The HTTP2 Upgrade Protocol + + + + +
    + +
    + +
    + +

    The HTTP Upgrade Protocol element represents an + Upgrade Protocol component that supports the HTTP/2 protocol. + An instance of this component must be associated with an existing + HTTP/1.1 Connector.

    + +

    HTTP/2 connectors use non-blocking I/O, only utilising a container thread + from the thread pool when there is data to read and write. However, because + the Servlet API is fundamentally blocking, each HTTP/2 stream requires a + dedicated container thread for the duration of that stream.

    + +

    Requests processed using HTTP/2 will have the following additional request + attributes available:

    +
      +
    • org.apache.coyote.connectionID will return the HTTP/2 + connection ID
    • +
    • org.apache.coyote.streamID will return the HTTP/2 stream + ID
    • +
    + +
    + + +
    + + + +

    All implementations of Upgrade Protocol support the + following attributes:

    + + + + +

    This must be org.apache.coyote.http2.Http2Protocol.

    +
    + +
    + +
    + + + +

    The HTTP/2 Upgrade Protocol implementation supports the + following attributes in addition to the common attributes listed above.

    + + + + +

    Controls the initial size of the flow control window for streams that + Tomcat advertises to clients. If not specified, the default value of + 65535 is used.

    +
    + + +

    The time, in milliseconds, that Tomcat will wait between HTTP/2 frames + when there is no active Stream before closing the connection. Negative + values will be treated as an infinite timeout. If not specified, a default + value of 20000 will be used.

    +
    + + +

    The controls the maximum number of streams for any one connection that + can be allocated threads from the container thread pool. If more streams + are active than threads are available, those streams will have to wait + for a stream to become available. If not specified, the default value of + 20 will be used.

    +
    + + +

    The controls the maximum number of active streams permitted for any one + connection. If a client attempts to open more active streams than this + limit, the stream will be reset with a STREAM_REFUSED error. + If not specified, the default value of 100 will be used.

    +
    + + +

    The maximum number of headers in a request that is allowed by the + container. A request that contains more headers than the specified limit + will be rejected. A value of less than 0 means no limit. + If not specified, a default of 100 is used.

    +
    + + +

    The maximum number of trailer headers in a request that is allowed by + the container. A request that contains more trailer headers than the + specified limit will be rejected. A value of less than 0 means no limit. + If not specified, a default of 100 is used.

    +
    + + +

    The threshold below which the payload size of a non-final + CONTINUATION frame will trigger an increase in the overhead + count (see overheadCountFactor). The overhead count will + be increased by overheadContinuationThreshold/payloadSize so + that the smaller the CONTINUATION frame, the greater the + increase in the overhead count. A value of zero or less disables the + checking of non-final CONTINUATION frames. If not specified, + a default value of 1024 will be used.

    +
    + + +

    The factor to apply when counting overhead frames to determine if a + connection has too high an overhead and should be closed. The overhead + count starts at -10 * overheadCountFactor. The count is + decreased by 20 for each data frame sent or received and each headers frame + received. The count is increased by the overheadCountFactor + for each setting, priority, priority update and ping frame received. If + the overhead count exceeds zero, the connection is closed. A value of less + than 1 disables this protection. In normal usage a value of + approximately 20 or higher will close the connection before + any streams can complete. If not specified, a default value of + 10 will be used.

    +
    + + +

    The amount by which the overhead count (see + overheadCountFactor) will be increased for each reset + frame received. If not specified, a default value of 50 will + be used. A value of less than zero will be treated as zero.

    +
    + + +

    The threshold below which the average payload size of the current and + previous non-final DATA frames will trigger an increase in + the overhead count (see overheadCountFactor). The + overhead count will be increased by + overheadDataThreshold/average so that the smaller the + average, the greater the increase in the overhead count. A value of zero + or less disables the checking of non-final DATA frames. If + not specified, a default value of 1024 will be used.

    +
    + + +

    The threshold below which the average size of current and previous + WINDOW_UPDATE frame will trigger an increase in the overhead + count (see overheadCountFactor). The overhead count will + be increased by overheadWindowUpdateThreshold/average so + that the smaller the average, the greater the increase in the overhead + count. A value of zero or less disables the checking of + WINDOW_UPDATE frames. If not specified, a default value of + 1024 will be used.

    +
    + + +

    The time, in milliseconds, that Tomcat will wait for additional data + when a partial HTTP/2 frame has been received. Negative values will be + treated as an infinite timeout. If not specified, a default value of + 5000 will be used.

    +
    + + +

    The time, in milliseconds, that Tomcat will wait for additional data + frames to arrive for the stream when an application is performing a + blocking I/O read and additional data is required. Negative values will be + treated as an infinite timeout. If not specified, a default value of + 20000 will be used.

    +
    + + +

    The time, in milliseconds, that Tomcat will wait for additional window + update frames to arrive for the stream and/or connection when an + application is performing a blocking I/O write and the stream and/or + connection flow control window is too small for the write to complete. + Negative values will be treated as an infinite timeout. If not specified, + a default value of 20000 will be used.

    +
    + + +

    Use this boolean attribute to enable or disable sendfile capability. + The default value is true.

    +

    This setting is ignored, and the sendfile capability disabled, if the + useAsyncIO attribute of the associated + Connector is set to false.

    +

    The HTTP/2 sendfile capability uses MappedByteBuffer which is known to cause file locking on Windows.

    +
    + + +

    The time, in milliseconds, that Tomcat will wait to write additional + data when an HTTP/2 frame has been partially written. Negative values will + be treated as an infinite timeout. If not specified, a default value of + 5000 will be used.

    +
    + +
    + +

    The HTTP/2 upgrade protocol will also inherit the following limits from the + HTTP Connector it is nested with:

    + +
      +
    • allowedTrailerHeaders
    • +
    • compressibleMimeType
    • +
    • compression
    • +
    • compressionMinSize
    • +
    • maxCookieCount
    • +
    • maxHttpHeaderSize
    • +
    • maxHttpRequestHeaderSize
    • +
    • maxParameterCount
    • +
    • maxPostSize
    • +
    • maxSavePostSize
    • +
    • maxTrailerSize
    • +
    • noCompressionUserAgents
    • +
    + +
    + +
    + +
    + +

    This component does not support any nested components.

    + +
    + + +
    + +

    This component does not support any special features.

    + +
    + + + +
    diff --git a/webapps/docs/config/index.xml b/webapps/docs/config/index.xml new file mode 100644 index 0000000..539967e --- /dev/null +++ b/webapps/docs/config/index.xml @@ -0,0 +1,96 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Overview + + + + + +
    + +

    This manual contains reference information about all of the configuration +directives that can be included in a conf/server.xml file to +configure the behavior of the Tomcat Servlet/JSP container. It does not +attempt to describe which configuration directives should be used to perform +specific tasks - for that, see the various How-To documents on the +main index page.

    + +

    Tomcat configuration files are formatted as schemaless XML; elements and +attributes are case-sensitive. Apache Ant-style variable substitution +is supported; a system property with the name propname may be +used in a configuration file using the syntax ${propname}. All +system properties are available including those set using the -D +syntax, those automatically made available by the JVM and those configured in +the $CATALINA_BASE/conf/catalina.properties file. +

    + +

    The configuration element descriptions are organized into the following +major categories:

    +
      +
    • Top Level Elements - <Server> is the + root element of the entire configuration file, while + <Service> represents a group of Connectors that is + associated with an Engine.
    • +
    • Connectors - Represent the interface between external + clients sending requests to (and receiving responses from) a particular + Service.
    • +
    • Containers - Represent components whose function is to + process incoming requests, and create the corresponding responses. + An Engine handles all requests for a Service, a Host handles all requests + for a particular virtual host, and a Context handles all requests for a + specific web application.
    • +
    • Nested Components - Represent elements that can be + nested inside the element for a Container. Some elements can be nested + inside any Container, while others can only be nested inside a + Context.
    • +
    + +

    For each element, the corresponding documentation follows this general +outline:

    +
      +
    • Introduction - Overall description of this particular + component. There will be a corresponding Java interface (in + the org.apache.catalina package) that is implemented by one + or more standard implementations.
    • +
    • Attributes - The set of attributes that are legal for + this element. Generally, this will be subdivided into Common + attributes that are supported by all implementations of the corresponding + Java interface, and Standard Implementation attributes that are + specific to a particular Java class that implements this interface. + The names of required attributes are bolded.
    • +
    • Nested Components - Enumerates which of the Nested + Components can be legally nested within this element.
    • +
    • Special Features - Describes the configuration of a large + variety of special features (specific to each element type) that are + supported by the standard implementation of this interface.
    • +
    + +
    + + + +
    diff --git a/webapps/docs/config/jar-scan-filter.xml b/webapps/docs/config/jar-scan-filter.xml new file mode 100644 index 0000000..a3d9c93 --- /dev/null +++ b/webapps/docs/config/jar-scan-filter.xml @@ -0,0 +1,191 @@ + + + +]> + + + &project; + + + The Jar Scan Filter Component + + + + +
    + +
    + +
    + +

    The Jar Scan Filter element represents the component that + filters results from the Jar Scanner before + they are passed back to the application. It is typically used to skip the + scanning of JARs that are known not to be relevant to some or all types of + scan.

    + +

    A Jar Scan Filter element MAY be nested inside a + Jar Scanner component.

    + +

    For example you can specify additional jar files when scanning for pluggable + features:

    + + ... + + + + ... +]]> + +

    If a Jar Scan Filter element is not included, a default Jar Scan Filter + configuration will be created automatically, which is sufficient for most + requirements.

    + +
    + + +
    + + + +

    All implementations of Jar Scan Filter + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.tomcat.JarScanFilter interface. + If not specified, the standard value (defined below) will be used.

    +
    + +
    + +
    + + + + +

    The standard implementation of Jar Scan Filter is + org.apache.tomcat.util.scan.StandardJarScanFilter. + Additional attributes that it supports (in addition to the common attributes + listed above) are listed in the table.

    + +

    The values for pluggabilitySkip, + pluggabilityScan, tldSkip, + tldScan attributes are lists of file name pattern. The + patterns are separated by comma (','). The leading and trailing whitespace + characters in a pattern are ignored. The patterns are matched + case-sensitively. The following two special characters are supported:

    + +
      +
    • '*' - means zero or more characters,
    • +
    • '?' - means one and only one character.
    • +
    + +

    Note that excluding a JAR from the pluggability scan will prevent a + ServletContainerInitializer from being loaded from a web application JAR + (i.e. one located in /WEB-INF/lib) but it will not prevent + a ServletContainerInitializer from being loaded from the container (Tomcat). + To prevent a ServletContainerInitializer provided by container from being + loaded, use the containerSciFilter property of the + Context.

    + + + + +

    The comma separated list of JAR file name patterns + to skip when scanning for pluggable features introduced by Servlet 3.0 + specification. If not specified, the default is obtained from the + tomcat.util.scan.StandardJarScanFilter.jarsToSkip system + property.

    +
    + + +

    The comma separated list of JAR file name patterns + to scan when scanning for pluggable features introduced by Servlet 3.0 + specification. If not specified, the default is obtained from the + tomcat.util.scan.StandardJarScanFilter.jarsToScan system + property.

    +
    + + +

    Controls if JARs are scanned or skipped by default when scanning + for the pluggable features. + If true, a JAR is scanned when its name either matches + none of pluggabilitySkip patterns or + any of pluggabilityScan patterns. + If false, a JAR is scanned when its name matches + any of pluggabilityScan patterns and + none of pluggabilitySkip patterns. + If not specified, the default value is true.

    +
    + + +

    The comma separated list of JAR file name patterns + to skip when scanning for tag libraries (TLDs). + If not specified, the default is obtained + from the tomcat.util.scan.StandardJarScanFilter.jarsToSkip + system property.

    +
    + + +

    The comma separated list of JAR file name patterns + to scan when scanning for tag libraries (TLDs). + If not specified, the default is obtained + from the tomcat.util.scan.StandardJarScanFilter.jarsToScan + system property.

    +
    + + +

    Controls if JARs are scanned or skipped by default when scanning + for TLDs. + If true, a JAR is scanned when its name either matches + none of tldSkip patterns or + any of tldScan patterns. + If false, a JAR is scanned when its name matches + any of tldScan patterns and + none of tldSkip patterns. + If not specified, the default value is true.

    +
    + +
    + +
    + + +
    + + +
    +

    No components may be nested inside a Jar Scan Filter element. +

    +
    + + +
    +

    No special features are associated with a Jar Scan Filter + element.

    +
    + + + +
    diff --git a/webapps/docs/config/jar-scanner.xml b/webapps/docs/config/jar-scanner.xml new file mode 100644 index 0000000..4a4022d --- /dev/null +++ b/webapps/docs/config/jar-scanner.xml @@ -0,0 +1,147 @@ + + + +]> + + + &project; + + + The Jar Scanner Component + + + + +
    + +
    + +
    + +

    The Jar Scanner element represents the component that is + used to scan the web application for JAR files and directories of class files. + It is typically used during web application start to identify configuration + files such as TLDs or web-fragment.xml files that must be processed as part of + the web application initialisation.

    + +

    A Jar Scanner element MAY be nested inside a + Context component.

    + +

    For example you can include the bootstrap classpath when scanning for jar + files:

    + + ... + + ... +]]> + +

    If a Jar Scanner element is not included, a default Jar Scanner configuration + will be created automatically, which is sufficient for most requirements.

    + +
    + + +
    + + + +

    All implementations of Jar Scanner + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.tomcat.JarScanner interface. + If not specified, the standard value (defined below) will be used.

    +
    + +
    + +
    + + + + +

    The standard implementation of Jar Scanner is + org.apache.tomcat.util.scan.StandardJarScanner. + It supports the following additional attributes (in addition to the + common attributes listed above):

    + + + + +

    If true, any directories found on the classpath will be + checked to see if they are expanded JAR files. + The default is false.

    +

    Tomcat determines if a directory is an expanded JAR file by looking + for a META-INF sub-directory. Only if the META-INF sub-directory exists, + the directory is assumed to be an expanded JAR file. Note that for scans + for matches to @HandlesTypes annotations, all directories + will be scanned irrespective of the presence or not of a META-INF + sub-directory.

    +
    + + +

    If true, any files found on the classpath will be checked + to see if they are Jar files rather than relying on the file extension + being .jar. The default is false.

    +
    + + +

    If true, the full web application classpath, including + the shared and common classloaders and the system classpath (but not the + bootstrap classpath) will be scanned for Jar files in addition to the web + application. The default is true.

    +
    + + +

    If scanClassPath is true and this is + true the bootstrap classpath will also be scanned for Jar + files. The default is false.

    +
    + + +

    If true, the Manifest files of any JARs found will be + scanned for additional class path entries and those entries will be added + to the URLs to scan. The default is true.

    +
    + +
    + +
    + + +
    + + +
    +

    Only a Jar Scan Filter may be nested + inside a Jar Scanner element.

    +
    + + +
    +

    No special features are associated with a Jar Scanner + element.

    +
    + + + +
    diff --git a/webapps/docs/config/jaspic.xml b/webapps/docs/config/jaspic.xml new file mode 100644 index 0000000..546918a --- /dev/null +++ b/webapps/docs/config/jaspic.xml @@ -0,0 +1,203 @@ + + + +]> + + + &project; + + + Jakarta Authentication (formerly JASPIC) + + + + +
    + +
    + +
    + +

    Tomcat implements Jakarta + Authentication 3.0. The implementation is primarily intended to enable the + integration of 3rd party authentication implementations with Tomcat.

    + +

    Jakarta Authentication may be configured in one of two ways:

    +
      +
    • At the container level via the static configuration file + $CATALINA_BASE/conf/jaspic-providers.xml. With this + approach all required classes must be visible to Tomcat's Common class + loader which normally means placing a JAR in + $CATALINA_BASE/lib.
    • +
    • At the web application level via dynamic configuration using the + Jakarta Authentication API. With this approach all required classes + must be visible to the web application class loader which normally + means placing a JAR in the web application's WEB-INF/lib + directory.
    • +
    + +

    Users should be aware that if the static Jakarta Authentication + configuration file configures Jakarta Authentication for a given web + application then the Jakarta Authentication configuration will take + precedence over any <login-config> present in the web + application's WEB-INF/web.xml file.

    + +
    + +
    + + + +

    If the 3rd party implementation includes an + AuthConfigProvider then a web application can be configured to + use it by nesting the following inside the + <jaspic-providers> element in + $CATALINA_BASE/conf/jaspic-providers.xml.

    + + +]]> + +

    The name and description attributes are not + used by Tomcat.

    + +

    The className attribute must be the fully qualified class + name of the AuthConfigProvider. The implementation may be + packaged with the web application or in Tomcat's + $CATALINA_BASE/lib directory.

    + +

    The layer attribute must be HttpServlet.

    + +

    The appContext attribute must be exactly the concatenation + of:

    +
      +
    • The engine name
    • +
    • The forward slash character
    • +
    • The host name
    • +
    • A single space
    • +
    • The context path
    • +
    + +

    If the AuthConfigProvider supports configuration via + properties these may be specified via <property> elements + nesting inside the <provide> element.

    + +
    + + + +

    If the 3rd party implementation only provides an + ServerAuthModule then it will be necessary to provide a number + of supporting classes. These may be a custom implementation or, + alternatively, Tomcat provides a simple wrapper implementation for + ServerAuthModules. +

    + +

    Tomcat's wrapper for ServerAuthModule can be configured + by nesting the following inside the + <jaspic-providers> element in + $CATALINA_BASE/conf/jaspic-providers.xml.

    + + + +]]> + +

    The configuration is similar to the AuthConfigProvider in + the previous section but with some key differences.

    + +

    The className attribute must be + org.apache.catalina.authenticator.jaspic.SimpleAuthConfigProvider.

    + +

    The ServerAuthModule(s) are specified via properties. The + property name must be + org.apache.catalina.authenticator.jaspic.ServerAuthModule.n + where n is the index of the module. The index must start at 1 + an increment in steps of 1 until all modules are defined. The value of the + property must be the fully qualified class name of the module.

    +
    + +
    + +
    + +

    Jakarta Authentication modules and configuration can be packaged within a + WAR file with the web application. The web application can then register the + required Jakarta Authentication configuration when it starts using the + standard Jakarta Authentication APIs.

    + +

    If parallel deployment is being used then dynamic configuration should not + be used. The Jakarta Authentication API assumes that a context path is unique + for any given host which is not the case when using parallel deployment. When + using parallel deployment, static Jakarta Authentication configuration should + be used. This will require that all versions of the application use the same + Jakarta Authentication configuration.

    + +
    + +
    + +

    This is not an exhaustive list. The Tomcat community welcomes contributions + that add to this section.

    + + + +

    The source code for this module along with the + documentation + which includes details of the necessary Google API configuration is + available on GitHub.

    + +

    A sample configuration for using this module with Tomcat would look like + this:

    + + + + + + + +]]> +
    + +
    + + + +
    diff --git a/webapps/docs/config/listeners.xml b/webapps/docs/config/listeners.xml new file mode 100644 index 0000000..76d993e --- /dev/null +++ b/webapps/docs/config/listeners.xml @@ -0,0 +1,654 @@ + + + +]> + + + &project; + + + The LifeCycle Listener Component + + + + +
    + +
    + +
    + +

    A Listener element defines a component that performs + actions when specific events occur, usually Tomcat starting or Tomcat + stopping.

    + +

    Listeners may be nested inside a Server, + Engine, Host or + Context. Some Listeners are only intended to be + nested inside specific elements. These constraints are noted in the + documentation below.

    + +
    + +
    + + + +

    All implementations of Listener + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.LifecycleListener + interface.

    +
    + +
    + +
    + +
    + +
    + +

    No element may be nested inside a Listener.

    + +
    + +
    + +

    Unlike most Catalina components, there are several standard + Listener implementations available. As a result, + the className attribute MUST be used to select the + implementation you wish to use.

    + + + +

    The APR Lifecycle Listener checks for the presence of + the Apache Tomcat Native library and loads the library if it is present. + For more information see the APR/native guide.

    + +

    This listener must only be nested within Server + elements.

    + +

    The following additional attributes are supported by the APR + Lifecycle Listener:

    + + + + +

    Name of the SSLEngine to use. off: do not use SSL, + on: use SSL but no specific ENGINE.

    +

    The default value is on. This initializes the + native SSL engine, which must be enabled in the APR/native connector by + the use of the SSLEnabled attribute.

    +

    See the Official OpenSSL website + for more details on supported SSL hardware engines and manufacturers. +

    +

    Tomcat Native 2.x onwards requires SSL so if SSLEngine is set to + off when using Tomcat Native 2.x onwards, the APR/native + library will be disabled.

    +
    + + +

    Entropy source used to seed the SSLEngine's PRNG. The default value + is builtin. On development systems, you may want to set + this to /dev/urandom to allow quicker start times.

    +
    + + +

    The behaviour of this attribute depends on whether Tomcat Native has + been compiled against OpenSSL 1.x or OpenSSL 3.x.

    +

    For OpenSSL 1.x: Set to on to request that OpenSSL be in + FIPS mode (if OpenSSL is already in FIPS mode, it will remain in FIPS + mode). + Set to enter to force OpenSSL to enter FIPS mode (an + error will occur if OpenSSL is already in FIPS mode). + Set to require to require that OpenSSL already be + in FIPS mode (an error will occur if OpenSSL is not already in FIPS + mode).

    +

    For OpenSSL 3.x: on, enter and + require all behave the same way. If the FIPS provider is + the default provider, it will be used. If the FIPS provider is not the + default provider, an error will occur.

    +

    FIPS mode requires you to have a FIPS-capable OpenSSL library. + If this attribute is set to anything other than off, the + SSLEngine must be enabled as well.

    +

    The default value is off.

    +
    + + +

    This attribute controls the auto-selection of the OpenSSL JSSE + implementation. The default is true which will use OpenSSL + if the native library is available and a NIO or NIO2 connector is used.

    +
    + +
    + +
    + + + +

    The Context Naming Info Listener adds the following + environment entries (java:comp/env implied) from the + Context: context/path, + context/encodedPath, context/webappVersion, + context/name, context/baseName, + context/displayName.

    + +

    This listener must only be nested within + Context elements.

    + +

    The following additional attributes are supported by the + Context Naming Info Listener:

    + + + +

    Whether for the root context context/path and + context/encodedPath will contain "/" and + context/name will contain "ROOT" with a version, + if any.

    +

    The default value is true.

    +
    +
    + +
    + + + +

    The Global Resources Lifecycle Listener initializes the + Global JNDI resources defined in server.xml as part of the Global Resources element. Without this + listener, none of the Global Resources will be available.

    + +

    This listener must only be nested within Server + elements.

    + +

    No additional attributes are supported by the Global Resources + Lifecycle Listener.

    + +
    + + + +

    The JNI Library Loading Listener makes it possible + for multiple Webapps to use a native library, by loading the native + library using a shared class loader (typically the Common class loader but + may vary in some configurations)

    + +

    The listener supports two mutually exclusive attributes, so one of them must be used, but you can not use both together:

    + + + +

    The name of the native library, as defined in + java.lang.System.loadLibrary() +

    +
    + +

    The absolute path of the native library, as defined in + java.lang.System.load() +

    +
    +
    +
    + + + +

    The JRE Memory Leak Prevention Listener provides + work-arounds for known places where the Java Runtime environment uses + the context class loader to load a singleton as this will cause a memory + leak if a web application class loader happens to be the context class + loader at the time. The work-around is to initialise these singletons when + this listener starts as Tomcat's common class loader is the context class + loader at that time. It also provides work-arounds for known issues that + can result in locked JAR files.

    + +

    This listener must only be nested within Server + elements.

    + +

    The following additional attributes are supported by the JRE + Memory Leak Prevention Listener:

    + + + + +

    Enables protection so that calls to + sun.awt.AppContext.getAppContext() triggered by a web + application do not result in a memory leak. Note that enabling this + protection will trigger a requirement for a graphical environment unless + Java is started in head-less mode. The default is false. +

    +
    + + +

    List of comma-separated fully qualified class names to load and initialize + during the startup of this Listener. This allows to pre-load classes that are + known to provoke classloader leaks if they are loaded during a request + processing. Non-JRE classes may be referenced, like + oracle.jdbc.driver.OracleTimeoutThreadPerVM. + The default value is empty, but specific JRE classes are loaded by other leak + protection features managed by other attributes of this Listener.

    +
    + + +

    The first use of java.sql.DriverManager will trigger the + loading of JDBC Drivers visible to the current class loader and its + parents. The web application level memory leak protection can take care + of this in most cases but triggering the loading here has fewer + side-effects. The default is true.

    +
    + + +

    The first use of SeedGenerator, an internal class of + the default security spi implementation, might create a thread on some + platforms. Depending on the timing of the first use of a secure random + this thread might become associated with a webapp classloader, causing + a memory leak. Setting this to true will initialize the + seed. The default is false to avoid consuming random if + not needed.

    +
    + + +

    Enables protection so that reading resources from JAR files using + java.net.URLConnections does not result in the JAR file + being locked. Note that enabling this protection disables caching by + default for all resources obtained via + java.net.URLConnections. Caching may be re-enabled on a + case by case basis as required. Defaults to true.

    +
    + +
    + + + +

    The following is an example of how to configure the + classesToInitialize attribute of this listener.

    + +

    If this listener was configured in server.xml as:

    + + ]]> + +

    then the OracleTimeoutThreadPerVM class would be loaded + and initialized during listener startup instead of during request + processing.

    + +
    + +
    + + + +

    The OpenSSL Lifecycle Listener checks for the presence + of the OpenSSL library and loads the library if it is present. This + uses the FFM API from Java 22 instead of additional native code. When + enabled and successfully loaded, NIO and NIO2 connector will then make use + of OpenSSL for TLS functionality. This is a functional replacement to the + APR Lifecycle Listener described above.

    + +

    This listener must only be nested within Server + elements.

    + +

    The following additional attributes are supported by the OpenSSL + Lifecycle Listener:

    + + + + +

    Name of the SSLEngine to use, for OpenSSL 1.x.

    +

    See the Official OpenSSL website + for more details on supported SSL hardware engines and manufacturers. +

    +
    + + +

    Entropy source used to seed the SSLEngine's PRNG. The default value + is builtin. On development systems, you may want to set + this to /dev/urandom to allow quicker start times.

    +
    + + +

    The behaviour of this attribute depends on whether Tomcat Native has + been compiled against OpenSSL 1.x or OpenSSL 3.x.

    +

    For OpenSSL 1.x: Set to on to request that OpenSSL be in + FIPS mode (if OpenSSL is already in FIPS mode, it will remain in FIPS + mode). + Set to enter to force OpenSSL to enter FIPS mode (an + error will occur if OpenSSL is already in FIPS mode). + Set to require to require that OpenSSL already be + in FIPS mode (an error will occur if OpenSSL is not already in FIPS + mode).

    +

    For OpenSSL 3.x: on, enter and + require all behave the same way. If the FIPS provider is + the default provider, it will be used. If the FIPS provider is not the + default provider, an error will occur.

    +

    FIPS mode requires you to have a FIPS-capable OpenSSL library. + If this attribute is set to anything other than off, the + SSLEngine must be enabled as well.

    +

    The default value is off.

    +
    + + +

    This attribute controls the auto-selection of the OpenSSL JSSE + implementation. The default is true which will use OpenSSL + if the FFM API is available.

    +
    + +
    + +
    + + + +

    The Properties Role Mapping Listener populates the context's role mapping + from a properties file. The keys represent application roles (e.g., admin, user, uservisor, + etc.) while the values represent technical roles (e.g., DNs, SIDs, UUIDs, etc.). A key can + also be prefixed if, e.g., the properties file contains generic application configuration + as well: app-roles..

    + +

    This listener must only be nested within + Context elements.

    + +

    The following additional attributes are supported by the + Properties Role Mapping Listener:

    + + + + +

    The path to the role mapping properties file. You can use protocol webapp: + and whatever ConfigFileLoader supports.

    +

    The default value is webapp:/WEB-INF/role-mapping.properties.

    +
    + + +

    The prefix to filter from property keys. All other keys will be ignored which do + not have the prefix.

    +
    + +
    + +
    + + + +

    The Security Lifecycle Listener performs a number of + security checks when Tomcat starts and prevents Tomcat from starting if they + fail. The listener is not enabled by default. To enabled it uncomment the + listener in $CATALINA_BASE/conf/server.xml.

    + +

    This listener must only be nested within Server + elements.

    + +

    The following additional attributes are supported by the Security + Lifecycle Listener:

    + + + + +

    A comma separated list of OS users that must not be used to start + Tomcat. If not specified, the default value of root is used. To + disable this check, set the attribute to the empty string. Usernames + are checked in a case-insensitive manner.

    +
    + + +

    The least restrictive umask that must be configured before Tomcat + will start. If not specified, the default value of 0007 is used. + To disable this check, set the attribute to the empty string. The check + is not performed on Windows platforms.

    +
    + + +

    The maximim number of days between the build-date of this instance + of Tomcat and its startup date can be before warnings will be logged. + Set to anything less than 0 (e.g. -1) to disable this check. + If not specified, the default value of -1 is used.

    +
    +
    + +
    + + + +

    The StoreConfig Lifecycle Listener configures a + StoreConfig MBean that may be used to save the current server configuration + in server.xml or the current configuration for a web application in a + context.xml file.

    + +

    This listener must only be nested within Server + elements.

    + +

    The following additional attributes are supported by the + StoreConfig Lifecycle Listener:

    + + + + +

    The name of the IStoreConfig implementation to use. If + not specified the default of + org.apache.catalina.storeconfig.StoreConfig will be + used.

    +
    + + +

    The URL of the configuration file that configures how the + IStoreConfig is to save the configuration. If not specified + the built in resource + /org/apache/catalina/storeconfig/server-registry.xml will + be used.

    +
    + +
    + +
    + + + +

    The ThreadLocal Leak Prevention Listener triggers the + renewal of threads in Executor pools when a + Context is being stopped to avoid thread-local + related memory leaks. Active threads will be renewed one by one when they + come back to the pool after executing their task. The renewal happens + only for contexts that have their renewThreadsWhenStoppingContext + attribute set to true.

    + +

    This listener must only be nested within Server + elements.

    + +

    No additional attributes are supported by the ThreadLocal Leak + Prevention Listener.

    + +
    + + + +

    This listener may be used to monitor the expiration dates of TLS + certificates and trigger automatic reloading of the TLS configuration a set + number of days before the TLS certificate expires.

    + +

    This listener assumes there is some other process (certbot, cloud + infrastructure, etc) that renews the certificate on a regular basis and + replaces the current certificate with the new one.

    + +

    This listener does NOT re-read the Tomcat configuration from + server.xml. If you make changes to server.xml you must restart the Tomcat + process to pick up those changes.

    + +

    This listener must only be nested within Server + elements.

    + + + + +

    The time, in seconds, between reloading checks. The periodic process + for LifecycleListener typically runs much more frequently + than this listener requires. This attribute controls the period between + checks. If not specified, a default of 86,400 seconds (24 hours) is + used.

    +
    + + +

    The number of days before the expiry of a TLS certificate that it is + expected that the new certificate will be in place and the reloading can + be triggered. If not specified, a default of 14 days is used.

    +
    + +
    + +
    + + + +

    The UserConfig provides feature of User Web Applications. + User Web Applications map a request URI starting with a tilde character ("~") + and a username to a directory (commonly named public_html) in that user's + home directory on the server.

    + +

    See the User Web Applications + special feature on the Host element for more information.

    + +

    The following additional attributes are supported by the + UserConfig:

    + + + + +

    The directory name to be searched for within each user home directory. + The default is public_html.

    +
    + + +

    The class name of the user database class. + There are currently two user database, the + org.apache.catalina.startup.PasswdUserDatabase is used on a + Unix system that uses the /etc/passwd file to identify valid users. + The org.apache.catalina.startup.HomesUserDatabase is used on + a server where /etc/passwd is not in use. HomesUserDatabase deploy all + directories found in a specified base directory.

    +
    + + +

    The base directory containing user home directories. This is effective + only when org.apache.catalina.startup.HomesUserDatabase is + used.

    +
    + + +

    A regular expression defining user who deployment is allowed. If this + attribute is specified, the user to deploy must match for this pattern. + If this attribute is not specified, all users will be deployed unless the + user matches a deny pattern.

    +
    + + +

    A regular expression defining user who deployment is denied. If this + attribute is specified, the user to deploy must not match for this + pattern. If this attribute is not specified, deployment of user will be + governed by a allow attribute.

    +
    + +
    + +
    + + + +

    The Version Logging Lifecycle Listener logs Tomcat, Java + and operating system information when Tomcat starts.

    + +

    This listener must only be nested within Server + elements and should be the first listener defined.

    + +

    The following additional attributes are supported by the Version + Logging Lifecycle Listener:

    + + + + +

    If true, the command line arguments passed to Java when + Tomcat started will be logged. If not specified, the default value of + true will be used.

    +
    + + +

    If true, the current environment variables when Tomcat + starts will be logged. If not specified, the default value of + false will be used.

    +
    + + +

    If true, the current Java system properties will be + logged. If not specified, the default value of + false will be used.

    +
    + +
    + +
    + + + +

    The HTTPD mod_heartmonitor Listener allows tomcat to send heart beat message to + the Apache HTTPD mod_heartmonitor module.

    + +

    The following additional attributes are supported by the HTTPD mod_heartmonitor + Listener:

    + + + +

    Port the connector that will received proxied traffic from HTTPD, default the first connector will be used

    +
    + + +

    Host it is the IP corresponding the address of the connector that will received proxied traffic, + default empty the Port will be used

    +
    + + +

    proxyURL is the URL corresponding to the Location in httpd configuration of the heartbeat Handler, + default /HeartbeatListener

    +
    + + +

    ProxyList is the list of proxies from which tomcat is going to receive requests, + formatted like "address:port,address:port" once filled the multicast logic is disable and the multi parameters are + ignored

    +
    + + +

    Group is the Multicast IP to broadcast messages to HTTPD, default 224.0.1.105

    +
    + + +

    Multiport is the Multicast port to broadcast messages to HTTPD, default 23364

    +
    + + +

    Ttl is the TTL for the broadcast messages, default 16

    +
    + +
    +
    +
    + + + +
    diff --git a/webapps/docs/config/loader.xml b/webapps/docs/config/loader.xml new file mode 100644 index 0000000..16cb521 --- /dev/null +++ b/webapps/docs/config/loader.xml @@ -0,0 +1,149 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + The Loader Component + + + + +
    + +
    + +
    + +

    The Loader element represents the web + application class loader that will be used to load Java + classes and resources for your web application. Such + a class loader must follow the requirements of the Servlet + Specification, and load classes from the following locations:

    +
      +
    • From the /WEB-INF/classes directory inside your + web application.
    • +
    • From JAR files in the /WEB-INF/lib directory + inside your web application.
    • +
    • From resources made available by Catalina to all web + applications globally.
    • +
    + +

    A Loader element MAY be nested inside a Context + component. If it is not included, a default Loader configuration will be + created automatically, which is sufficient for most requirements.

    + +

    For a more in-depth description of the class loader hierarchy + that is implemented by Catalina, see the ClassLoader HowTo.

    + +

    The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + +
    + + +
    + + + +

    All implementations of Loader + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Loader interface. + If not specified, the standard value (defined below) will be used.

    +
    + + +

    Set to true if you want the class loader to follow + the standard Java2 delegation model, and attempt to load classes from + parent class loaders before looking inside the web + application. Set to false (the default) to have the + class loader look inside the web application first, before asking + parent class loaders to find requested classes or resources.

    +
    + +
    + +
    + + + + +

    The standard implementation of Loader is + org.apache.catalina.loader.WebappLoader. + It supports the following additional attributes (in addition to the + common attributes listed above):

    + + + + +

    Java class name of the java.lang.ClassLoader + implementation class to use. Custom implementations must extend + org.apache.catalina.loader.WebappClassLoaderBase. +

    + +

    If not specified, the default value is + org.apache.catalina.loader.ParallelWebappClassLoader. The + default loaderClass is parallel capable, which + means that multiple threads may load difference classes in parallel. + A non-parallel capable loaderClass is available and can + be used by specifying + org.apache.catalina.loader.WebappClassLoader.

    +
    + +
    + +
    + +
    + + +
    + +

    No components may be nested inside a Loader element.

    + +
    + + +
    + + + +

    A loader is associated with the log category based on its classname.

    + +
    + +
    + + + + + +
    diff --git a/webapps/docs/config/manager.xml b/webapps/docs/config/manager.xml new file mode 100644 index 0000000..ed7ae2b --- /dev/null +++ b/webapps/docs/config/manager.xml @@ -0,0 +1,612 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Yoav Shapira + The Manager Component + + + + +
    + +
    + +
    + +

    The Manager element represents the session + manager that will be used to create and maintain HTTP sessions + as requested by the associated web application.

    + +

    A Manager element MAY be nested inside a + Context component. If it is not included, + a default Manager configuration will be created automatically, which + is sufficient for most requirements, — see + Standard Manager Implementation below for the details + of this configuration.

    + +
    + + +
    + + + +

    All implementations of Manager + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Manager interface. + If not specified, the standard value (defined below) will be used.

    +
    + + +

    The maximum number of active sessions that will be created by + this Manager, or -1 (the default) for no limit.

    + +

    When the limit is reached, any attempt to create a new session + (e.g. with HttpServletRequest.getSession() call) + will fail with an IllegalStateException.

    +
    + + +

    If an attribute is added to the session and that attribute is already + present in the session under the same name will any + HttpSessionAttributeListener be notified that the attribute + has been replaced. If not specified, the default value of + true will be used.

    +
    + + +

    If an attribute is added to the session, that attribute is already + present in the session under the same name and the attribute implements + HttpSessionBindingListener, will the listener be notified + that the attribute has been unbound and bound again. If not specified, + the default value of false will be used.

    +
    + + +

    If this is true, Tomcat will track the number of active + requests for each session. When determining if a session is valid, any + session with at least one active request will always be considered valid.

    +

    If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + true, the default of this setting will be true, + else the default value will be false.

    +
    + + +

    If this is true, the last accessed time for sessions will + be calculated from the beginning of the previous request. If + false, the last accessed time for sessions will be calculated + from the end of the previous request. This also affects how the idle time + is calculated.

    +

    If org.apache.catalina.STRICT_SERVLET_COMPLIANCE is set to + true, the default of this setting will be true, + else the default value will be false.

    +
    + +
    + +
    + + + + +

    Tomcat provides two standard implementations of Manager + for use — the default one stores active sessions, while the optional one + stores active sessions that have been swapped out (in addition to saving + sessions across a restart of Tomcat) in a storage location that is selected + via the use of an appropriate Store nested element.

    + +

    Standard Manager Implementation

    + +

    The standard implementation of Manager is + org.apache.catalina.session.StandardManager. + It supports the following additional attributes (in addition to the + common attributes listed above):

    + + + + +

    Absolute or relative (to the work directory for this Context) + pathname of the file in which session state will be preserved + across application restarts, if possible. The default is + null.
    See + Persistence Across Restarts + for more information. This persistence may be + enabled by setting this attribute to a non empty string.

    +
    + + +

    Should authentication information be included when session state is + preserved across application restarts? If true, the session's + authentication is preserved so that the session remains authenticated + after the application has been restarted. If not specified, the default + value of false will be used.
    See + Persistence Across Restarts + for more information.

    + +

    Please note that the session's Principal class as well + as its descendant classes are all subject to the + sessionAttributeValueClassNameFilter. If such a filter + is specified or a SecurityManager is enabled, the names of + the Principal class and descendant classes must match that + filter pattern in order to be restored.

    +
    + + +

    Frequency of the session expiration, and related manager operations. + Manager operations will be done once for the specified amount of + backgroundProcess calls (i.e., the lower the amount, the more often the + checks will occur). The minimum value is 1, and the default value is 6. +

    +
    + + +

    Name of the Java class that extends + java.security.SecureRandom to use to generate session IDs. + If not specified, the default value is + java.security.SecureRandom.

    +
    + + +

    Name of the provider to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the Manager + will use the platform default provider and the default algorithm. If not + specified, the platform default provider will be used.

    +
    + + +

    Name of the algorithm to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the Manager + will use the platform default provider and the default algorithm. If not + specified, the default algorithm of SHA1PRNG will be used. If the + default algorithm is not supported, the platform default will be used. + To specify that the platform default should be used, do not set the + secureRandomProvider attribute and set this attribute to the empty + string.

    +
    + + +

    A regular expression used to filter which session attributes will be + serialized for clustering/replication, or storage to any persistent store. + An attribute will only be serialized if its name matches + this pattern. If the pattern is zero length or null, all + attributes are eligible for distribution. The pattern is anchored so the + session attribute name must fully match the pattern. As an example, the + value (userName|sessionHistory) will only distribute the + two session attributes named userName and + sessionHistory. If not specified, the default value of + null will be used.

    +
    + + +

    A regular expression used to filter which session attributes will be + serialized for clustering/replication, or storage to any persistent store. + An attribute will only be serialized if the implementation + class name of the value matches this pattern. If the pattern is zero + length or null, all attributes are eligible for + distribution. The pattern is anchored so the fully qualified class name + must fully match the pattern. If not specified, the default value of + null will be used unless a SecurityManager is + enabled in which case the default will be + java\\.lang\\.(?:Boolean|Integer|Long|Number|String)|org\\.apache\\.catalina\\.realm\\.GenericPrincipal\\$SerializablePrincipal|\\[Ljava.lang.String;.

    +
    + + +

    If sessionAttributeNameFilter or + sessionAttributeValueClassNameFilter blocks an + attribute, should this be logged at WARN level? If + WARN level logging is disabled then it will be logged at + DEBUG. The default value of this attribute is + false unless a SecurityManager is enabled in + which case the default will be true.

    +
    +
    + +

    Persistent Manager Implementation

    + +

    NOTE: You must set either the + org.apache.catalina.session.StandardSession.ACTIVITY_CHECK or + org.apache.catalina.STRICT_SERVLET_COMPLIANCE + system properties to true for + the persistent manager to work correctly.

    + +

    The persistent implementation of Manager is + org.apache.catalina.session.PersistentManager. In + addition to the usual operations of creating and deleting sessions, a + PersistentManager has the capability to swap active (but + idle) sessions out to a persistent storage mechanism, as well as to save + all sessions across a normal restart of Tomcat. The actual persistent + storage mechanism used is selected by your choice of a + Store element nested inside the Manager + element - this is required for use of PersistentManager.

    + +

    This implementation of Manager supports the following attributes in + addition to the Common Attributes + described earlier.

    + + + + +

    It has the same meaning as described in the + Common Attributes above. + You must specify + org.apache.catalina.session.PersistentManager to use + this manager implementation.

    +
    + + +

    The time interval (in seconds) since the last access to a session + before it is eligible for being persisted to the session store, or + -1 to disable this feature. By default, this feature is + disabled.

    +
    + + +

    The maximum time a session may be idle before it is eligible to be + swapped to disk due to inactivity. Setting this to -1 means + sessions should not be swapped out just because of inactivity. If this + feature is enabled, the time interval specified here should be equal to + or longer than the value specified for maxIdleBackup. By + default, this feature is disabled.

    +
    + + +

    The minimum time in seconds a session must be idle before it is + eligible to be swapped to disk to keep the active session count below + maxActiveSessions. Setting to -1 means sessions will not be + swapped out to keep the active session count down. If specified, this + value should be less than that specified by maxIdleSwap. + By default, this value is set to -1.

    +
    + + +

    Should authentication information be included when sessions are + swapped out to persistent storage? If true, the session's + authentication is preserved so that the session remains authenticated + after being reloaded (swapped in) from persistent storage. If not + specified, the default value of false will be used.

    + +

    Please note that the session's Principal class as well + as its descendant classes are all subject to the + sessionAttributeValueClassNameFilter. If such a filter + is specified or a SecurityManager is enabled, the names of + the Principal class and descendant classes must match that + filter pattern in order to be restored.

    +
    + + +

    It is the same as described above for the + org.apache.catalina.session.StandardManager class. +

    +
    + + +

    Should all sessions be persisted and reloaded when Tomcat is shut + down and restarted (or when this application is reloaded)? By default, + this attribute is set to true.

    +
    + + + +

    It is the same as described above for the + org.apache.catalina.session.StandardManager class. +

    +
    + + +

    It is the same as described above for the + org.apache.catalina.session.StandardManager class. +

    +
    + + +

    It is the same as described above for the + org.apache.catalina.session.StandardManager class. +

    +
    + + +

    A regular expression used to filter which session attributes will be + serialized for clustering/replication, or storage to any persistent store. + An attribute will only be serialized if its name matches + this pattern. If the pattern is zero length or null, all + attributes are eligible for distribution. The pattern is anchored so the + session attribute name must fully match the pattern. As an example, the + value (userName|sessionHistory) will only distribute the + two session attributes named userName and + sessionHistory. If not specified, the default value of + null will be used.

    +
    + + +

    A regular expression used to filter which session attributes will be + serialized for clustering/replication, or storage to any persistent store. + An attribute will only be serialized if the implementation + class name of the value matches this pattern. If the pattern is zero + length or null, all attributes are eligible for + distribution. The pattern is anchored so the fully qualified class name + must fully match the pattern. If not specified, the default value of + null will be used unless a SecurityManager is + enabled in which case the default will be + java\\.lang\\.(?:Boolean|Integer|Long|Number|String)|org\\.apache\\.catalina\\.realm\\.GenericPrincipal\\$SerializablePrincipal|\\[Ljava.lang.String;.

    +
    + + +

    If sessionAttributeNameFilter or + sessionAttributeValueClassNameFilter blocks an + attribute, should this be logged at WARN level? If + WARN level logging is disabled then it will be logged at + DEBUG. The default value of this attribute is + false unless a SecurityManager is enabled in + which case the default will be true.

    +
    +
    + +

    In order to successfully use a PersistentManager, you must nest inside + it a <Store> element, as described below.

    + +
    + + +
    + + +
    + +

    All Manager Implementations

    + +

    All Manager implementations allow nesting of a + <SessionIdGenerator> element. It defines + the behavior of session id generation. All implementations + of the SessionIdGenerator allow the + following attributes: +

    + + + + +

    The length of the session ID may be changed with the + sessionIdLength attribute. +

    +
    + +
    + +

    Persistent Manager Implementation

    + +

    If you are using the Persistent Manager Implementation + as described above, you MUST nest a + <Store> element inside, which defines the + characteristics of the persistent data storage. Two implementations + of the <Store> element are currently available, + with different characteristics, as described below.

    + +
    File Based Store
    + +

    The File Based Store implementation saves swapped out + sessions in individual files (named based on the session identifier) + in a configurable directory. Therefore, you are likely to encounter + scalability problems as the number of active sessions increases, and + this should primarily be considered a means to easily experiment.

    + +

    To configure this, add a <Store> nested inside + your <Manager> element with the following attributes: +

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Store interface. You + must specify + org.apache.catalina.session.FileStore + to use this implementation.

    +
    + + +

    Absolute or relative (to the temporary work directory for this web + application) pathname of the directory into which individual session + files are written. If not specified, the temporary work directory + assigned by the container is utilized.

    +
    + +
    + + +
    Data source Based Store
    + +

    The Data source Based Store implementation saves swapped out + sessions in individual rows of a preconfigured table in a database + that is accessed via a data sourcer. With large numbers of swapped out + sessions, this implementation will exhibit improved performance over + the File Based Store described above.

    + +

    To configure this, add a <Store> nested inside + your <Manager> element with the following attributes: +

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Store interface. You + must specify + org.apache.catalina.session.DataSourceStore + to use this implementation.

    +
    + + +

    Name of the JNDI resource for a JDBC DataSource-factory. Since this + code uses prepared statements, you might want to configure pooled + prepared statements as shown in + the JNDI resources + How-To.

    +
    + + +

    This allows the Store to use a DataSource defined for the Context + rather than a global DataSource. If not specified, the default is + false: use a global DataSource.

    +
    + + +

    Name of the database column, contained in the specified session table, + that contains the Engine, Host, and Web Application Context name in the + format /Engine/Host/Context. If not specified the default + value of app will be used.

    +
    + + +

    Name of the database column, contained in the specified session table, + that contains the serialized form of all session attributes for a swapped + out session. The column type must accept a binary object (typically called + a BLOB). If not specified the default value of data will be + used.

    +
    + + +

    Name of the database column, contained in the specified session table, + that contains the session identifier of the swapped out session. The + column type must accept character string data of at least as many + characters as are contained in session identifiers created by Tomcat + (typically 32). If not specified the default value of id will + be used.

    +
    + + +

    Name of the database column, contained in the specified session table, + that contains the lastAccessedTime property of this session. + The column type must accept a Java long (64 bits). If not + specified the default value of maxinactive will be used.

    +
    + + +

    Name of the database column, contained in the specified session table, + that contains the maxInactiveInterval property of this + session. The column type must accept a Java integer (32 + bits). If not specified, the default value of maxinactive + will be used.

    +
    + + +

    Name of the database table to be used for storing swapped out sessions. + This table must contain (at least) the database columns that are + configured by the other attributes of this element. If not specified the + default value of tomcat$sessions will be used.

    +
    + + +

    Name of the database column, contained in the specified session table, + that contains a flag indicating whether this swapped out session is still + valid or not. The column type must accept a single character. If not + specified the default value of valid will be used.

    +
    + +
    + +

    Before attempting to use the data source Store for the first time, + you must create the table that will be used to store swapped out sessions. + Detailed SQL commands vary depending on the database you are using, but + a script like this will generally be required:

    + +create table tomcat_sessions ( + session_id varchar(100) not null primary key, + valid_session char(1) not null, + max_inactive int not null, + last_access bigint not null, + app_name varchar(255), + session_data mediumblob, + KEY kapp_name(app_name) +); + +

    Note: The SQL command above does not use the default names for either the + table or the columns so the data source Store would need to be configured + to reflect this.

    + +
    + + +
    + + + + +

    Whenever Apache Tomcat is shut down normally and restarted, or when an + application reload is triggered, the standard Manager implementation + will attempt to serialize all currently active sessions to a disk + file located via the pathname attribute. All such saved + sessions will then be deserialized and activated (assuming they have + not expired in the mean time) when the application reload is completed.

    + +

    In order to successfully restore the state of session attributes, + all such attributes MUST implement the java.io.Serializable + interface. You MAY cause the Manager to enforce this restriction by + including the <distributable> element in your web + application deployment descriptor (/WEB-INF/web.xml).

    + +

    Note that, if persistAuthentication is also set to + true, the Principal class present in the session + MUST also implement the java.io.Serializable interface in order + to make authentication persistence work properly. The actual type of that + Principal class is determined by the + Realm implementation used with the application. Tomcat's standard + Principal class instantiated by most of the Realms (except + JAASRealm) implements java.io.Serializable.

    + +

    The persistence across restarts provided by the + StandardManager is a simpler implementation than that + provided by the PersistentManager. If robust, production + quality persistence across restarts is required then the + PersistentManager should be used with an appropriate + configuration.

    + +
    + + + +

    As documented above, every web application by default has + standard manager implementation configured, which can perform session + persistence across restarts. To enable this persistence feature, create + a Context configuration file for your web + application and add the following element there (in this example, + it will save sessions to files named SESSIONS.ser):

    + + ]]> +
    + +
    + + + + + +
    diff --git a/webapps/docs/config/project.xml b/webapps/docs/config/project.xml new file mode 100644 index 0000000..e9a2ba5 --- /dev/null +++ b/webapps/docs/config/project.xml @@ -0,0 +1,98 @@ + + + + + Apache Tomcat 10 Configuration Reference + + + The Apache Tomcat Servlet/JSP Container + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/docs/config/realm.xml b/webapps/docs/config/realm.xml new file mode 100644 index 0000000..529a70c --- /dev/null +++ b/webapps/docs/config/realm.xml @@ -0,0 +1,1116 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + The Realm Component + + + + +
    + +
    + +
    + +

    A Realm element represents a "database" of usernames, + passwords, and roles (similar to Unix groups) assigned + to those users. Different implementations of Realm allow Catalina to be + integrated into environments where such authentication information is already + being created and maintained, and then utilize that information to implement + Container Managed Security as described in the Servlet + Specification.

    + +

    A Catalina container (Engine, + Host, or Context) may + contain no more than one Realm element (although if supported by the Realm + this one Realm may itself contain multiple nested Realms). In addition, the + Realm associated with an Engine or a Host is automatically inherited by + lower-level containers unless the lower level container explicitly defines its + own Realm. If no Realm is configured for the Engine, an instance of the + Null Realm + will be configured for the Engine automatically.

    + +

    For more in-depth information about container managed security in web + applications, as well as more information on configuring and using the + standard realm component implementations, please see the + Container-Managed Security Guide. +

    + +

    The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + +
    + + +
    + + + +

    All implementations of Realm + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Realm interface.

    +
    + +
    + +

    Unlike most Catalina components, there are several standard + Realm implementations available. As a result, + the className attribute MUST be used to select the + implementation you wish to use.

    + +
    + + + + +

    The DataSource Database Realm connects Tomcat to + a relational database, accessed through a JNDI named JDBC DataSource + to perform lookups of usernames, passwords, and their associated + roles. Because the lookup is done each time that it is required, + changes to the database will be immediately reflected in the + information used to authenticate new logins.

    + +

    The JDBC Realm uses a single db connection. This requires that + realm based authentication be synchronized, i.e. only one authentication + can be done at a time. This could be a bottleneck for applications + with high volumes of realm based authentications.

    + +

    The DataSource Database Realm supports simultaneous realm based + authentications and allows the underlying JDBC DataSource to + handle optimizations like database connection pooling.

    + +

    A rich set of additional attributes lets you configure the name + of the JNDI JDBC DataSource, as well as the table and + column names used to retrieve the required information:

    + + + + +

    This attribute controls how the special role name * is + handled when processing authorization constraints in web.xml. By + default, the specification compliant value of strict is + used which means that the user must be assigned one of the roles defined + in web.xml. The alternative values are authOnly which means + that the user must be authenticated but no check is made for assigned + roles and strictAuthOnly which means that the user must be + authenticated and no check will be made for assigned roles unless roles + are defined in web.xml in which case the user must be assigned at least + one of those roles.

    +

    When this attribute has the value of authOnly or + strictAuthOnly, the roleNameCol and + userRoleTable attributes become optional. If those two + attributes are omitted, the user's roles will not be loaded by this + Realm.

    +
    + + +

    The name of the JNDI JDBC DataSource for this Realm.

    +
    + + +

    When the realm is nested inside a Context element, this allows the + realm to use a DataSource defined for the Context rather than a global + DataSource. If not specified, the default is false: use a + global DataSource.

    +
    + + +

    Name of the column, in the "user roles" table, which contains + a role name assigned to the corresponding user.

    +

    This attribute is required in majority of + configurations. See allRolesMode attribute for + a rare case when it can be omitted.

    +
    + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + + +

    When processing users authenticated via the GSS-API, this attribute + controls if any "@..." is removed from the end of the user + name. If not specified, the default is true.

    +
    + + +

    Name of the column, in the "users" table, which contains + the user's credentials (i.e. password). If a + CredentialHandler is specified, this component + will assume that the passwords have been encoded with the + specified algorithm. Otherwise, they will be assumed to be + in clear text.

    +
    + + +

    Name of the column, in the "users" and "user roles" table, + that contains the user's username.

    +
    + + +

    Name of the "user roles" table, which must contain columns + named by the userNameCol and roleNameCol + attributes.

    +

    This attribute is required in majority of + configurations. See allRolesMode attribute for + a rare case when it can be omitted.

    +
    + + +

    Name of the "users" table, which must contain columns named + by the userNameCol and userCredCol + attributes.

    +
    + + +

    When using X509 client certificates, this specifies the class name + that will be used to retrieve the user name from the certificate. + The class must implement the + org.apache.catalina.realm.X509UsernameRetriever + interface. The default is to use the certificate's SubjectDN + as the username.

    +
    +
    + +

    See the + DataSource Realm How-To for more information on setting up container + managed security using the DataSource Database Realm component.

    + +
    + + + + +

    The JNDI Directory Realm connects Tomcat to + an LDAP Directory, accessed through an appropriate JNDI driver, + that stores usernames, passwords, and their associated + roles. Changes to the directory are immediately reflected in the + information used to authenticate new logins.

    + +

    The directory realm supports a variety of approaches to using + LDAP for authentication:

    + +
      +
    • The realm can either use a pattern to determine the + distinguished name (DN) of the user's directory entry, or search + the directory to locate that entry. +
    • + +
    • The realm can authenticate the user either by binding to the + directory with the DN of the user's entry and the password + presented by the user, or by retrieving the password from the + user's entry and performing a comparison locally. +
    • + +
    • Roles may be represented in the directory as explicit entries + found by a directory search (e.g. group entries of which the user + is a member), as the values of an attribute in the user's entry, + or both. +
    • +
    + +

    A rich set of additional attributes lets you configure the + required behaviour as well as the connection to the underlying + directory and the element and attribute names used to retrieve + information from the directory:

    + + + + +

    Microsoft Active Directory often returns referrals. + When iterating over NamingEnumerations these lead to + PartialResultExceptions. If you want us to ignore those exceptions, + set this attribute to "true". Unfortunately there's no stable way + to detect, if the Exceptions really come from an AD referral. + The default value is "false".

    +
    + + +

    This attribute controls how the special role name * is + handled when processing authorization constraints in web.xml. By + default, the specification compliant value of strict is + used which means that the user must be assigned one of the roles defined + in web.xml. The alternative values are authOnly which means + that the user must be authenticated but no check is made for assigned + roles and strictAuthOnly which means that the user must be + authenticated and no check will be made for assigned roles unless roles + are defined in web.xml in which case the user must be assigned at least + one of those roles.

    +
    + + +

    If a socket connection cannot be made to the provider at + the connectionURL an attempt will be made to use the + alternateURL.

    +
    + + +

    A string specifying the type of authentication to use. + "none", "simple", "strong" or a provider specific definition + can be used. If no value is given the providers default is used.

    +
    + + +

    Specify which cipher suites are allowed when trying to open + a secured connection using StartTLS. The allowed cipher suites + are specified by a comma separated list. The default is to use the + cipher suites of the JVM.

    +
    + + +

    A role name assigned to each successfully authenticated user in + addition to the roles retrieved from LDAP. If not specified, only + the roles retrieved via LDAP are used.

    +
    + + +

    The directory username to use when establishing a + connection to the directory for LDAP search operations. If not + specified an anonymous connection is made, which is often + sufficient unless you specify the userPassword + property.

    +
    + + +

    The directory password to use when establishing a + connection to the directory for LDAP search operations. If not + specified an anonymous connection is made, which is often + sufficient unless you specify the userPassword + property.

    +
    + + +

    The JNDI realm can use a pool of connections to the directory server + to avoid blocking on a single connection. This attribute value is the + maximum pool size. If not specified, it will use 1, which + means a single connection will be used.

    +
    + + +

    The timeout in milliseconds to use when establishing the connection + to the LDAP directory. If not specified, a value of 5000 (5 seconds) is + used.

    +
    + + +

    The connection URL to be passed to the JNDI driver when + establishing a connection to the directory.

    +
    + + +

    Fully qualified Java class name of the factory class used + to acquire our JNDI InitialContext. By default, + assumes that the standard JNDI LDAP provider will be utilized.

    +
    + + +

    A string specifying how aliases are to be dereferenced during + search operations. The allowed values are "always", "never", + "finding" and "searching". If not specified, "always" is used.

    +
    + + +

    A setting of true forces escaping in the String + representation of a distinguished name to use the \nn form. + This may avoid issues with realms using Active Directory which appears + to be more tolerant of optional escaping when the \nn form + is used. If not specified, the default of false will be + used.

    +
    + + +

    The name of the class to use for hostname verification when + using StartTLS for securing the connection to the ldap server. + The default constructor will be used to construct an instance of + the verifier class. The default is to accept only those hostnames, + that are valid according to the peer certificate of the ldap + server.

    +
    + + +

    A string specifying the TLS protocol to use. If not given, the + Java runtime's default is used.

    +
    + + +

    The timeout, in milliseconds, to use when trying to read from a + connection to the directory. If not specified, the default of 5000 + (5 seconds) is used.

    +
    + + +

    How do we handle JNDI referrals? Allowed values are + "ignore", "follow", or "throw" (see javax.naming.Context.REFERRAL + for more information). + Microsoft Active Directory often returns referrals. + If you need to follow them set referrals to "follow". + Caution: if your DNS is not part of AD, the LDAP client lib might try + to resolve your domain name in DNS to find another LDAP server.

    +
    + + +

    The base directory entry for performing role searches. If not + specified the top-level element in the directory context will be used. + If specified it may optionally include pattern replacements + "{0}".."{n}" corresponding to the name parts of the + user's distinguished name (as returned by + javax.naming.Name.get()).

    +
    + + +

    The name of the attribute that contains role names in the + directory entries found by a role search. In addition you can + use the userRoleName property to specify the name + of an attribute, in the user's entry, containing additional + role names.

    +

    If roleName is not specified a role + search does not take place, and roles are taken only from the + user's entry.

    +
    + + +

    Set to true if you want to nest roles into roles. + When a role search is performed and the value of this property is + true, the search will be repeated recursively to find + all the roles that belong to the user either directly or indirectly. + If not specified, the default value of false is used.

    +
    + + +

    The LDAP filter expression used for performing role + searches.

    + +

    Use {0} to substitute the distinguished name (DN) + of the user, and/or {1} to substitute the username, + and/or {2} for the value of an attribute from the + user's directory entry, of the authenticated user. + The name of the attribute that provides the value for {2} + is configured by the userRoleAttribute property.

    + +

    When roleNested property is true, + this filter expression will be also used to recursively search for + other roles, which indirectly belong to this user. To find the + roles that match the newly found role, the following values + are used: + {0} is substituted by the distinguished name of the newly + found role, and both {1} and {2} are + substituted by the name of the role (see the roleName + property). The userRoleAttribute property is not + applicable to this search.

    + +

    If this property is not specified, a role search does not take + place and roles are taken only from the attribute in the user's entry + specified by the userRoleName property.

    +
    + + +

    When searching for user roles, should the search be performed as the + user currently being authenticated? If false, + connectionName and connectionPassword will be + used if specified, else an anonymous. If not specified, the default + value of false is used. Note that when accessing the + directory using delegated credentials, this attribute is always ignored + and the search is performed using the delegated credentials.

    +
    + + +

    Set to true if you want to search the entire + subtree of the element specified by the roleBase + property for role entries associated with the user. The + default value of false causes only the top level + to be searched.

    +
    + + +

    Specifies the maximum number of records to return when using the + userSearch attribute. If not specified, the default of + 0 is used which indicates no limit.

    +
    + + +

    When the JNDI Realm is used with the SPNEGO authenticator and + useDelegatedCredential is true this attribute + controls the QOP (Quality of Protection) that should be used for + the connection to the LDAP + server after authentication. This value is used to set the + javax.security.sasl.qop environment property for the LDAP + connection. This attribute should be a comma-separated list of values + selected from auth-conf, auth-int and + auth. See Java documentation for more details.

    +

    The default value is auth-conf.

    +
    + + +

    Specifies which ssl protocol should be used, when connecting with + StartTLS. The default is to let the jre decide. If you need even more + control, you can specify the SSLSocketFactory to use.

    +
    + + +

    Specifies which SSLSocketFactory to use when connecting + to the ldap server using StartTLS. An instance of the class will be + constructed using the default constructor. If none class name is given + the default jre SSLSocketFactory will be used.

    +
    + + +

    When processing users authenticated via the GSS-API, this attribute + controls if any "@..." is removed from the end of the user + name. If not specified, the default is true.

    +
    + + +

    Specifies the time (in milliseconds) to wait for records to be + returned when using the userSearch attribute. If not + specified, the default of 0 is used which indicates no + limit.

    +
    + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + + +

    Instructs JNDIRealm to use the context class loader when opening the + connection for the JNDI provider. The default value is + true. To load classes using the container's classloader, + specify false.

    +
    + + +

    When the JNDIRealm is used with the SPNEGO authenticator, delegated + credentials for the user may be available. If such credentials are + present, this attribute controls whether or not they are used to + connect to the directory. If not specified, the default value of + true is used.

    +
    + + +

    The base element for user searches performed using the + userSearch expression. Not used if you are using + the userPattern expression.

    +
    + + +

    Name of the attribute in the user's entry containing the + user's password. If you specify this value, JNDIRealm will + bind to the directory using the values specified by + connectionName and + connectionPassword properties, and retrieve the + corresponding attribute for comparison to the value specified + by the user being authenticated. If you do + not specify this value, JNDIRealm will + attempt a simple bind to the directory using the DN of the + user's entry and the password presented by the user, with a + successful bind being interpreted as an authenticated + user.

    +
    + + +

    Pattern for the distinguished name (DN) of the user's + directory entry, with {0} marking where the + actual username should be inserted. You can use this property + instead of userSearch, userSubtree + and userBase when the distinguished name contains + the username and is otherwise the same for all users. Note that + when accessing the directory using delegated credentials, this + attribute is always ignored and userSearch, + userSubtree and userBase are always + used instead.

    +
    + + +

    The name of an attribute in the user's directory entry + containing zero or more values for the names of roles assigned + to this user. In addition you can use the + roleName property to specify the name of an + attribute to be retrieved from individual role entries found + by searching the directory. If userRoleName is + not specified all the roles for a user derive from the role + search.

    +
    + + +

    The name of an attribute in the user's directory entry + containing the value that you wish to use when you search for + roles. This is especially useful for RFC 2307 where + the role memberUid can be the uid or the + uidNumber of the user. This value will be + marked as {2} in your role search filter expression. + This value will NOT be available for nested role searches.

    +
    + + +

    The LDAP filter expression to use when searching for a + user's directory entry, with {0} marking where + the actual username should be inserted. Use this property + (along with the userBase and + userSubtree properties) instead of + userPattern to search the directory for the + user's entry.

    +
    + + +

    When searching for a user's entry, should the search be performed as + the user currently being authenticated? If false, + connectionName and connectionPassword will be + used if specified, else an anonymous. If not specified, the default + value of false is used. Note that when accessing the + directory using delegated credentials, this attribute is always ignored + and the search is performed using the delegated credentials.

    +
    + + +

    Set to true if you want to search the entire + subtree of the element specified by the userBase + property for the user's entry. The default value of + false causes only the top level to be searched. + Not used if you are using the userPattern + expression.

    +
    + + +

    Set to true if you want to use StartTLS for securing + the connection to the ldap server. The default value is false. +

    +
    + + +

    When using X509 client certificates, this specifies the class name + that will be used to retrieve the user name from the certificate. + The class must implement the + org.apache.catalina.realm.X509UsernameRetriever + interface. The default is to use the certificate's SubjectDN + as the username.

    +
    +
    + +

    See the Container-Managed Security Guide for more + information on setting up container managed security using the + JNDI Directory Realm component.

    + +
    + + + + +

    The UserDatabase Realm is a Realm implementation + that is based on a UserDatabase resource made available through the global + JNDI resources configured for this Tomcat instance.

    + +

    The UserDatabase Realm implementation supports the following + additional attributes:

    + + + + +

    This attribute controls how the special role name * is + handled when processing authorization constraints in web.xml. By + default, the specification compliant value of strict is + used which means that the user must be assigned one of the roles defined + in web.xml. The alternative values are authOnly which means + that the user must be authenticated but no check is made for assigned + roles and strictAuthOnly which means that the user must be + authenticated and no check will be made for assigned roles unless roles + are defined in web.xml in which case the user must be assigned at least + one of those roles.

    +
    + + +

    When the realm is nested inside a Context element, this allows the + realm to use a UserDatabase defined for the Context rather than a global + UserDatabase. If not specified, the default is false: use a + global UserDatabase.

    +
    + + +

    The name of the global UserDatabase resource + that this realm will use for user, password and role information.

    +
    + + +

    This allows using a static Principal instance + disconnected from the database if needed. This makes the behavior of + authenticated prinicipals equivalent to that of the other realms. + If there is a plan to use serialization, it is best to set this to + true as the principal will always be replaced by this + equivalent static principal when serializing. + If not specified, the default is false: use a + Principal connected to the UserDatabase.

    +
    + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + + +

    When using X509 client certificates, this specifies the class name + that will be used to retrieve the user name from the certificate. + The class must implement the + org.apache.catalina.realm.X509UsernameRetriever + interface. The default is to use the certificate's SubjectDN + as the username.

    +
    +
    + +

    See the + Container-Managed Security Guide for more + information on setting up container managed security using the UserDatabase + Realm component and the + JNDI resources how-to for more + information on how to configure a UserDatabase resource.

    + +
    + + + + +

    The Memory Based Realm is a simple Realm implementation + that reads user information from an XML format, and represents it as a + collection of Java objects in memory. This implementation is intended + solely to get up and running with container managed security - it is NOT + intended for production use. As such, there are no mechanisms for + updating the in-memory collection of users when the content of the + underlying data file is changed.

    + +

    The Memory Based Realm implementation supports the following + additional attributes:

    + + + + +

    This attribute controls how the special role name * is + handled when processing authorization constraints in web.xml. By + default, the specification compliant value of strict is + used which means that the user must be assigned one of the roles defined + in web.xml. The alternative values are authOnly which means + that the user must be authenticated but no check is made for assigned + roles and strictAuthOnly which means that the user must be + authenticated and no check will be made for assigned roles unless roles + are defined in web.xml in which case the user must be assigned at least + one of those roles.

    +
    + + +

    URL, absolute path or relative path (to $CATALINA_BASE) for the XML + file containing our user information. See below for details on the + XML element format required. If no pathname is specified, the + default value is conf/tomcat-users.xml.

    +
    + + +

    When processing users authenticated via the GSS-API, this attribute + controls if any "@..." is removed from the end of the user + name. If not specified, the default is true.

    +
    + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + + +

    When using X509 client certificates, this specifies the class name + that will be used to retrieve the user name from the certificate. + The class must implement the + org.apache.catalina.realm.X509UsernameRetriever + interface. The default is to use the certificate's SubjectDN + as the username.

    +
    +
    + +

    The XML document referenced by the pathname attribute must + conform to the following requirements:

    +
      +
    • The root (outer) element must be <tomcat-users>. +
    • +
    • Each authorized user must be represented by a single XML element + <user>, nested inside the root element.
    • +
    • Each <user> element must have the following + attributes: +
        +
      • username - Username of this user (must be unique + within this file).
        + For compatibility, it is allowed to use name as an + alternative name for this attribute.
      • +
      • password - Password of this user (in + clear text).
      • +
      • roles - Comma-delimited list of the role names + assigned to this user.
      • +
    • +
    + +

    See the Container-Managed Security Guide for more + information on setting up container managed security using the + Memory Based Realm component.

    + +
    + + + + +

    JAASRealm is an implementation of the Tomcat + Realm interface that authenticates users through the Java + Authentication & Authorization Service (JAAS) framework which is now + provided as part of the standard J2SE API.

    + +

    Using JAASRealm gives the developer the ability to combine practically + any conceivable security realm with Tomcat's CMA.

    + +

    JAASRealm is prototype for Tomcat of the JAAS-based J2EE authentication + framework for J2EE v1.4, based on the JCP Specification Request + 196 to enhance container-managed security and promote 'pluggable' + authentication mechanisms whose implementations would be + container-independent.

    + +

    Based on the JAAS login module and principal + (see javax.security.auth.spi.LoginModule and + javax.security.Principal), you can develop your own security + mechanism or wrap another third-party mechanism for integration with the CMA + as implemented by Tomcat.

    + +

    The JAAS Realm implementation supports the following additional + attributes:

    + + + + +

    This attribute controls how the special role name * is + handled when processing authorization constraints in web.xml. By + default, the specification compliant value of strict is + used which means that the user must be assigned one of the roles defined + in web.xml. The alternative values are authOnly which means + that the user must be authenticated but no check is made for assigned + roles and strictAuthOnly which means that the user must be + authenticated and no check will be made for assigned roles unless roles + are defined in web.xml in which case the user must be assigned at least + one of those roles.

    +
    + + +

    The name of the application as configured in your login configuration + file + (JAAS LoginConfig).

    +

    If not specified appName is derived from the Container's + name it is placed in, for example Catalina or ROOT. + If the realm is not placed in any Container, the default is Tomcat. +

    +
    + + +

    A comma-separated list of the names of the classes that you have made + for your user Principals.

    +
    + + +

    The name of a JAAS configuration file to use with this Realm. It will + be searched for using ClassLoader#getResource(String) so it + is possible for the configuration to be bundled within a web + application. If not specified, the default JVM global JAAS configuration + will be used.

    +
    + + +

    A comma-separated list of the names of the classes that you have made + for your role Principals.

    +
    + + +

    When processing users authenticated via the GSS-API, this attribute + controls if any "@..." is removed from the end of the user + name. If not specified, the default is true.

    +
    + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + + +

    Instructs JAASRealm to use the context class loader for loading the + user-specified LoginModule class and associated + Principal classes. The default value is true, + which is backwards-compatible with the way Tomcat 5 works. To load + classes using the container's classloader, specify + false.

    +
    + + +

    When using X509 client certificates, this specifies the class name + that will be used to retrieve the user name from the certificate. + The class must implement the + org.apache.catalina.realm.X509UsernameRetriever + interface. The default is to use the certificate's SubjectDN + as the username.

    +
    +
    + +

    See the Container-Managed Security + Guide for more information on setting up container managed security + using the JAAS Realm component.

    + +
    + + + + +

    CombinedRealm is an implementation of the Tomcat + Realm interface that authenticates users through one or more + sub-Realms.

    + +

    Using CombinedRealm gives the developer the ability to combine multiple + Realms of the same or different types. This can be used to authenticate + against different sources, provide fall back in case one Realm fails or for + any other purpose that requires multiple Realms.

    + +

    Sub-realms are defined by nesting Realm elements inside the + Realm element that defines the CombinedRealm. Authentication + will be attempted against each Realm in the order they are + listed. Authentication against any Realm will be sufficient to authenticate + the user. The authenticated user, and their associated roles, will be taken + from the first Realm that successfully authenticates the user.

    + +

    See the Container-Managed Security + Guide for more information on setting up container managed security + using the CombinedRealm component.

    + +

    The CombinedRealm implementation supports the following additional + attributes.

    + + + + +

    This attribute controls how the special role name * is + handled when processing authorization constraints in web.xml. By + default, the specification compliant value of strict is + used which means that the user must be assigned one of the roles defined + in web.xml. The alternative values are authOnly which means + that the user must be authenticated but no check is made for assigned + roles and strictAuthOnly which means that the user must be + authenticated and no check will be made for assigned roles unless roles + are defined in web.xml in which case the user must be assigned at least + one of those roles.

    +
    + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + +
    +
    + + + + +

    LockOutRealm is an implementation of the Tomcat + Realm interface that extends the CombinedRealm to provide lock + out functionality to provide a user lock out mechanism if there are too many + failed authentication attempts in a given period of time.

    + +

    To ensure correct operation, there is a reasonable degree of + synchronization in this Realm.

    + +

    This Realm does not require modification to the underlying Realms or the + associated user storage mechanisms. It achieves this by recording all failed + logins, including those for users that do not exist. To prevent a DOS by + deliberating making requests with invalid users (and hence causing this + cache to grow) the size of the list of users that have failed authentication + is limited.

    + +

    Sub-realms are defined by nesting Realm elements inside the + Realm element that defines the LockOutRealm. Authentication + will be attempted against each Realm in the order they are + listed. Authentication against any Realm will be sufficient to authenticate + the user.

    + +

    The LockOutRealm implementation supports the following additional + attributes.

    + + + +

    This attribute controls how the special role name * is + handled when processing authorization constraints in web.xml. By + default, the specification compliant value of strict is + used which means that the user must be assigned one of the roles defined + in web.xml. The alternative values are authOnly which means + that the user must be authenticated but no check is made for assigned + roles and strictAuthOnly which means that the user must be + authenticated and no check will be made for assigned roles unless roles + are defined in web.xml in which case the user must be assigned at least + one of those roles.

    +
    + + +

    If a failed user is removed from the cache because the cache is too + big before it has been in the cache for at least this period of time (in + seconds) a warning message will be logged. Defaults to 3600 (1 hour).

    +
    + + +

    Number of users that have failed authentication to keep in cache. Over + time the cache will grow to this size and may not shrink. Defaults to + 1000.

    +
    + + +

    The number of times in a row a user has to fail authentication to be + locked out. Defaults to 5.

    +
    + + +

    The time (in seconds) a user is locked out for after too many + authentication failures. Defaults to 300 (5 minutes). Further + authentication failures during the lock out time will cause the lock out + timer to reset to zero, effectively extending the lock out time. Valid + authentication attempts during the lock out period will not succeed but + will also not reset the lock out time.

    +
    + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + +
    + +

    See the Container-Managed Security + Guide for more information on setting up container managed security + using the LockOutRealm component.

    + +
    + + + +

    NullRealm is a minimal implementation of the Tomcat + Realm interface that always returns null when an attempt is + made to validate a user name and associated credentials. It is intended to + be used as a default Realm implementation when no other Realm is + specified.

    + +

    The NullRealm implementation supports the following additional + attributes.

    + + + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + +
    + +
    + + + +

    AuthenticatedUserRealm is intended for use with + Authenticator implementations (SSLAuthenticator, SpnegoAuthenticator) that + authenticate the user as well as obtain the user credentials. An + authenticated Principal is always created from the user name presented to + without further validation.

    +

    Note: It is unsafe to use this Realm with Authenticator + implementations that do not validate the provided credentials.

    + +

    The AuthenticatedUserRealm implementation supports the following + additional attributes.

    + + + + +

    The HTTP status code to use when the container needs to issue an HTTP + redirect to meet the requirements of a configured transport + guarantee. The provided status code is not validated. If not + specified, the default value of 302 is used.

    +
    + +
    + +
    + +
    + + +
    + +

    You can nest the following components by nesting the corresponding element + inside your Realm element:

    +
      +
    • CombinedRealm Implementation - If you are using the + CombinedRealm Implementation or a Realm + that extends the CombinedRealm, e.g. the LockOutRealm, one or more + <Realm> elements may be nested inside it.
    • +
    • CredentialHandler - + You may nest at most one instance of this element inside a Realm. This + configures the credential handler that will be used to validate provided + credentials with those stored by the Realm. If not specified a default + MessageDigestCredentialHandler will be configured.
    • +
    + +
    + + +
    + +

    See Single Sign On for information about + configuring Single Sign On support for a virtual host.

    + +
    + + + + + +
    diff --git a/webapps/docs/config/resources.xml b/webapps/docs/config/resources.xml new file mode 100644 index 0000000..4bad617 --- /dev/null +++ b/webapps/docs/config/resources.xml @@ -0,0 +1,371 @@ + + + +]> + + + &project; + + + The Resources Component + + + + +
    + +
    + +
    + +

    The Resources element represents all the resources + available to the web application. This includes classes, JAR files, HTML, JSPs + and any other files that contribute to the web application. Implementations + are provided to use directories, JAR files and WARs as the source of these + resources and the resources implementation may be extended to provide support + for files stored in other forms such as in a database or a versioned + repository.

    + +

    Resources are cached by default.

    + +

    Note: Running a webapp with non-filesystem based + Resources implementations is only possible when the webapp does not + rely on direct filesystem access to its own resources, and uses the methods + in the ServletContext interface to access them.

    + +

    A Resources element MAY be nested inside a + Context component. If it is not included, + a default filesystem based Resources will be created automatically, + which is sufficient for most requirements.

    + +
    + + +
    + + + +

    All implementations of Resources support the following + attributes:

    + + + + +

    If the value of this flag is true, symlinks will be + allowed inside the web application, pointing to resources inside or + outside the web application base path. If not specified, the default + value of the flag is false.

    +

    NOTE: This flag MUST NOT be set to true on the Windows platform + (or any other OS which does not have a case sensitive filesystem), + as it will disable case sensitivity checks, allowing JSP source code + disclosure, among other security problems.

    +
    + + +

    The maximum size of the static resource cache in kilobytes. + If not specified, the default value is 10240 + (10 MiB). This value may be changed while the web application is + running (e.g. via JMX). If the cache is using more memory than the new + limit the cache will attempt to reduce in size over time to meet the + new limit. If necessary, cacheObjectMaxSize will be + reduced to ensure that it is no larger than + cacheMaxSize/20.

    +
    + + +

    Maximum size of the static resource that will be placed in the cache. + If not specified, the default value is 512 + (512 kilobytes). If this value is greater than + cacheMaxSize/20 it will be reduced to + cacheMaxSize/20. This value may be changed while the web + application is running (e.g. via JMX).

    +
    + + +

    The amount of time in milliseconds between the revalidation of cache + entries. If not specified, the default value is 5000 (5 + seconds). This value may be changed while the web application is running + (e.g. via JMX). When a resource is cached it will inherit the TTL in + force at the time it was cached and retain that TTL until the resource + is evicted from the cache regardless of any subsequent changes that may + be made to this attribute.

    +
    + + +

    If the value of this flag is true, the cache for static + resources will be used. If not specified, the default value + of the flag is true. This value may be changed while the + web application is running (e.g. via JMX). When the cache is disabled + any resources currently in the cache are cleared from the cache.

    +
    + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.WebResourceRoot + interface. If not specified, the standard value (defined below) will be + used.

    +
    + + +

    Controls whether the track locked files feature is enabled. If + enabled, all calls to methods that return objects that lock a file and + need to be closed to release that lock (e.g. + ServletContext.getResourceAsStream()) will perform a number + of additional tasks.

    +
      +
    • The stack trace at the point where the method was called will be + recorded and associated with the returned object.
    • +
    • The returned object will be wrapped so that the point where + close() (or equivalent) is called to release the resources can be + detected. Tracking of the object will cease once the resources have + been released.
    • +
    • All remaining locked resources on web application shutdown will be + logged and then closed.
    • +
    +

    If not specified, the default value of false will be + used.

    +
    + + +

    If this is simple then a hash map will be used for + JAR archive lookups, unless useBloomFilterForArchives in + Context is explicitly defined.

    +

    If this is bloom then a bloom filter will be used to + speed up archive lookups. This can be beneficial to the deployment + speed to web applications that contain very large amount of JARs.

    +

    If this is purged then a bloom filter will be used to + speed up archive lookups, but can be purged at runtime. It is recommended + to use bloom to avoid reinitializing the bloom filters.

    +

    If not specified, the default value of simple will be + used.

    +
    + +
    + +
    + + + + +

    Standard Root Implementation

    + +

    The standard implementation of Resources is + org.apache.catalina.webresources.StandardRoot. It does not + support any additional attributes.

    + +

    Extracting Root Implementation

    + +

    The extracting implementation of Resources is + org.apache.catalina.webresources.ExtractingRoot. It does not + support any additional attributes.

    + +

    When deploying web applications as packed WAR files, the extracting root + will extract any JAR files from /WEB-INF/lib to a + application-jars directory located in the web + application's working directory. These extracted JARs will be removed + when the web application stops.

    + +

    Extracting JAR files from a packed WAR may provide a performance + improvement, particularly at web application start when JAR scanning is + required by the application.

    + +
    + +
    + + +
    + +

    A web application's main resources are defined by the + docBase defined for the Context. + Additional configuration settings and/or resources may be made available to + the web application by defining one or more nested components.

    + +

    PreResources

    + +

    PreResources are searched before the main resources. They will be searched + in the order they are defined. To configure PreResources, nest a + <PreResources> element inside the <Resources> element with the + following attributes:

    + + + + +

    Identifies where the resources to be used are located. This attribute + is required by the org.apache.catalina.WebResourceSet + implementations provided by Tomcat and should specify the absolute path to + the file, directory or JAR where the resources are located. Custom + implementations may not require it.

    +
    + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.WebResourceSet interface. + Tomcat provides three standard implementations: + org.apache.catalina.webresources.DirResourceSet, + org.apache.catalina.webresources.FileResourceSet and + org.apache.catalina.webresources.JarResourceSet. Custom + implementations may also be used. +

    +
    + + +

    Identifies the path within the base where the + resources are to be found. This is typically only used with JAR files when + the resources are not located at the root of the JAR as is the case with + resource JARs. This attribute is required by the + org.apache.catalina.WebResourceSet implementations provided + by Tomcat and must start with '/'. Custom implementations may not require + it. If not specified, the default value '/' will be used.

    +
    + + +

    If true, resources within this resource set may not be + deleted, created or modified. For instance of + org.apache.catalina.webresources.JarResourceSet, this + attribute is hard-coded to true and may not be changed. For + instances of org.apache.catalina.webresources.DirResourceSet + and org.apache.catalina.webresources.FileResourceSet the + default value of this attribute is false.

    +
    + + +

    Identifies the path within the web application that these resources + will be made available. For the + org.apache.catalina.WebResourceSet implementations provided + by Tomcat, this attribute is required and must start with '/'. Custom + implementations may not require it. If not specified, the default value of + '/' will be used.

    +
    + +
    + +

    JAR resources

    + +

    JarResources are searched after the main resources but before the + PostResources. They will be searched in the order they are defined. To + configure JarResources, nest a <JarResources> element inside the + <Resources> element. The configuration attributes are the same as for + PreResources. +

    + +

    During web application start, the JAR scanning process checks scanned JARs + for content under /META-INF/resources. Where found, this static + content is added to the JarResources. +

    + +

    Post-resources

    + +

    PostResources are searched after the resource JARs. They will be searched + in the order they are defined. To configure PostResources, nest a + <PostResources> element inside the <Resources> element. The + configuration attributes are the same as for PreResources. +

    + +

    Ordering

    + +

    In addition to the sets of resources described above, the standard + implementation also maintains ClassResources which represent the classes + contained in the JAR files mapped to /WEB-INF/classes. This + allows other components to search for classes with a single call rather than + one call to search /WEB-INF/classes followed by another to search + the JARs in /WEB-INF/lib. The ClassResources are populated + from the JARs mapped to /WEB-INF/lib when the web application + starts.

    + +

    Therefore, the complete search order is:

    +
      +
    • PreResources
    • +
    • MainResources
    • +
    • ClassResources
    • +
    • JarResources
    • +
    • PostResources
    • +
    + +

    The population of ClassResources and JarResources at web application start + means that care needs to be taken to add JAR based resources correctly to + obtain the desired behaviour. Consider the following example:

    + + + + +]]> + +

    Since both resources are PostResources, it might be expected that + D:\Projects\external\classes will be searched for classes before + D:\Projects\lib\library1.jar. However, by adding the JAR using a + FileResourceSet, the JAR is mapped to /WEB-INF/lib + and will be processed at application start along with the other JARs in + /WEB-INF/lib. The classes from the JAR file will be added to the + ClassResources which means they will be searched before the classes from + D:\Projects\external\classes. If the desired behaviour is that + D:\Projects\external\classes is searched before + D:\Projects\lib\library1.jar then a slightly different + configuration is required:

    + + + + +]]> + +

    In short, the JAR file should be added as a JarResourceSet + mapped to /WEB-INF/classes rather than using a + FileResourceSet mapped to /WEB-INF/lib. +

    + +

    Cache Strategy

    + +

    Additional control over the caching of static resources can be obtained by + configuring a custom cache strategy. To configure a custom cache strategy, + nest a <CacheStrategy> element inside the <Resources> element + with the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must implement + the org.apache.catalina.WebResourceRoot$CacheStrategy + interface.

    +
    + +
    + +
    + + +
    + +

    No special features are associated with a Resources + element.

    + +
    + + + + + +
    diff --git a/webapps/docs/config/server.xml b/webapps/docs/config/server.xml new file mode 100644 index 0000000..89d03b1 --- /dev/null +++ b/webapps/docs/config/server.xml @@ -0,0 +1,159 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + The Server Component + + + + +
    + +
    + +
    + +

    A Server element represents the entire Catalina + servlet container. Therefore, it must be the single outermost element + in the conf/server.xml configuration file. Its attributes + represent the characteristics of the servlet container as a whole.

    + +
    + + +
    + + + +

    All implementations of Server + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Server interface. + If no class name is specified, the standard implementation will + be used.

    +
    + + +

    The TCP/IP address on which this server waits for a shutdown + command. If no address is specified, localhost is used.

    +
    + + +

    The TCP/IP port number on which this server waits for a shutdown + command. Set to -1 to disable the shutdown port.

    +

    Note: Disabling the shutdown port works well when Tomcat is started + using Apache Commons Daemon + (running as a service on Windows or with jsvc on un*xes). It cannot be + used when running Tomcat with the standard shell scripts though, as it + will prevent shutdown.bat|.sh and catalina.bat|.sh from stopping it + gracefully.

    +
    + + +

    The offset to apply to port and to the ports of any + nested connectors. It must be a non-negative integer. If not specified, + the default value of 0 is used.

    +
    + + +

    The command string that must be received via a TCP/IP connection + to the specified port number, in order to shut down Tomcat.

    +
    + + +

    The number of threads this Service will use for + various utility tasks, including recurring ones. The special value + of 0 will result in the value of + Runtime.getRuntime().availableProcessors() being + used. Negative values will result in + Runtime.getRuntime().availableProcessors() + value being + used unless this is less than 1 in which case 1 thread will be used. + The default value is 1. +

    +
    + +
    + +
    + + + +

    The standard implementation of Server is + org.apache.catalina.core.StandardServer. + It supports the following additional attributes (in addition to the + common attributes listed above):

    + + + + +

    Set the daemon flag value for the utility threads. The default value + is false. +

    +
    + + +

    This value represents the delay in seconds between periodic + lifecycle event invocation of the lifecycle listeners configured on this + Server. The value is in seconds, and a negative or zero value will + disable the invocations. If not specified, the default value for this + attribute is 10 seconds.

    +
    + +
    + +
    + +
    + + +
    + +

    The following components may be nested inside a Server + element:

    + + +
    + + +
    + +

    There are no special features associated with a Server. +

    + +
    + + + + +
    diff --git a/webapps/docs/config/service.xml b/webapps/docs/config/service.xml new file mode 100644 index 0000000..bcca17f --- /dev/null +++ b/webapps/docs/config/service.xml @@ -0,0 +1,119 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + The Service Component + + + + +
    + +
    + +
    + +

    A Service element represents the combination of one or + more Connector components that share a single + Engine component for processing incoming + requests. One or more Service elements may be nested + inside a Server element.

    + +
    + + +
    + + + +

    All implementations of Service + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.Service interface. + If no class name is specified, the standard implementation will + be used.

    +
    + + +

    The display name of this Service, which will + be included in log messages if you utilize standard Catalina + components. The name of each Service that is + associated with a particular Server + must be unique.

    +
    + +
    + +
    + + + +

    The standard implementation of Service is + org.apache.catalina.core.StandardService. + It supports the following additional attributes (in addition to the + common attributes listed above):

    + + + + +

    The time to wait, in milliseconds, when stopping the Service for the + client connections to finish processing and close before the Service's + container hierarchy is stopped. The wait only applies to Connectors + configured with a bindOnInit value of false + which is not the default. Any value of zero or less means there will be no + wait. If not specified, the default value of zero will be used.

    +
    + +
    + +
    + +
    + + +
    + +

    The only components that may be nested inside a Service + element are one or more Connector elements, + followed by exactly one Engine element.

    + +
    + + +
    + +

    There are no special features associated with a Service. +

    + +
    + + + + +
    diff --git a/webapps/docs/config/sessionidgenerator.xml b/webapps/docs/config/sessionidgenerator.xml new file mode 100644 index 0000000..d370f29 --- /dev/null +++ b/webapps/docs/config/sessionidgenerator.xml @@ -0,0 +1,131 @@ + + + +]> + + + &project; + + + The SessionIdGenerator Component + + + + +
    + +
    + +
    + +

    The SessionIdGenerator element represents the session + id generator that will be used to create session ids used by + web application HTTP sessions.

    + +

    A SessionIdGenerator element MAY be nested inside a + Manager component. If it is not included, + a default SessionIdGenerator configuration will be created automatically, which + is sufficient for most requirements, — see + Standard SessionIdGenerator Implementation below for the details + of this configuration.

    + +
    + + +
    + + + +

    All implementations of SessionIdGenerator + support the following attributes:

    + + + + +

    Java class name of the implementation to use. This class must + implement the org.apache.catalina.SessionIdGenerator interface. + If not specified, the standard value (defined below) will be used.

    +
    + + +

    A routing identifier for this Tomcat instance. It will be added + to the session id to allow for stateless stickiness routing by + load balancers. The details on how the jvmRoute + will be included in the id are implementation dependent. + See Standard Implementation + for the default behavior.

    + +

    NOTE - The value for this property is inherited + automatically from the jvmRoute attribute of the + Engine element.

    +
    + + +

    The length of session ids created by this SessionIdGenerator. + The details on how the sessionIdLength + influences the session id length are implementation dependent. + See Standard Implementation + for the default behavior.

    +
    + +
    + +
    + + + + +

    Tomcat provides a standard implementations of SessionIdGenerator + for use.

    + +

    Standard SessionIdGenerator Implementation

    + +

    The standard implementation of SessionIdGenerator is + org.apache.catalina.util.StandardSessionIdGenerator. + It supports the following attributes:

    + + + + +

    A routing identifier for this Tomcat instance. It will be added + to the end of the session id separated by a ".".

    +
    + + +

    The length of session ids created by this SessionIdGenerator. + More precisely the session id length is twice the value of + sessionIdLength plus the length of the trailing + jvmRoute if given. The factor 2 is because + the session id is constructed using sessionIdLength + random bytes, each byte being encoded in two hex characters + in the actual id. The default value is 16.

    +
    + +
    + +
    + + +
    + + + + + +
    diff --git a/webapps/docs/config/systemprops.xml b/webapps/docs/config/systemprops.xml new file mode 100644 index 0000000..11e4cb2 --- /dev/null +++ b/webapps/docs/config/systemprops.xml @@ -0,0 +1,393 @@ + + + +]> + + + &project; + + + System Properties + + + + +
    + +
    + +
    +

    The following sections list the system properties that may be set to modify + the default Tomcat behaviour.

    +
    + +
    + + +

    Set this to a comma separated list of fully qualified name of classes + that implement + org.apache.tomcat.util.IntrospectionUtils.PropertySource. + Required to have a public constructor with no arguments.

    +

    Use this to add a property source, that will be invoked when + ${parameter:-default-value} denoted parameters (with + optional default values) are found in the XML files that Tomcat + parses.

    +

    Property replacement from the specified property source on the JVM + system properties can also be done using the + REPLACE_SYSTEM_PROPERTIES system property.

    +

    org.apache.tomcat.util.digester.ServiceBindingPropertySource + can be used to replace parameters from any Kubernetes service bindings + that follows the servicebinding.io spec

    +

    org.apache.tomcat.util.digester.EnvironmentPropertySource + can be used to replace parameters from the process' environment + variables, e.g. injected ConfigMaps or Secret objects in container + based systems like OpenShift or Kubernetes.

    +

    org.apache.tomcat.util.digester.SystemPropertySource + does replacement with system properties. It is always enabled, + but can also be specified as part of the property value.

    +
    + +

    Set this boolean system property to true to cause + property replacement from the digester property source on the JVM + system properties.

    +
    +
    + +
    + +
    + + + +

    Controls whether the EL API classes make use of a privileged block to + obtain the thread context class loader. When using the EL API within + Apache Tomcat this does not need to be set as all calls are already + wrapped in a privileged block further up the stack. It may be required if + using the EL API under a SecurityManager outside of Apache Tomcat.

    +

    If not specified, the default of false will be used.

    +
    + + +

    The number of jakarta.el.BeanELResolver.BeanProperties objects that will + be cached by the EL Parser.

    +

    If not specified, the default of 1000 will be used.

    +
    + + +

    The number of parsed EL expressions that will be cached by the EL + Parser.

    +

    If not specified, the default of 5000 will be used.

    +
    + + +

    If true, when coercing nulls to objects of + type Number, Character or Boolean the result will be 0 for + Number and Character types and false for Boolean as required + by the EL 2.2 and earlier specifications. If this property is + false the result of the coercion will be null as + required by the EL 3.0 specification.

    +

    If not specified, the default value of false will be + used.

    +
    + + +

    If true, when parsing expressions, identifiers will not be + checked to ensure that they conform to the Java Language Specification for + Java identifiers.

    +

    If not specified, the default value of false will be used.

    +
    + +
    +
    + + +
    + + + + +

    The default value of this system property is false.

    +

    If this is true the default values will be changed + to true instead of false for:

    +
      +
    • The alwaysAccessSession attribute of any + Context element.
    • +
    • The contextGetResourceRequiresSlash attribute of any + Context element.
    • +
    • The dispatcherWrapsSameObject attribute of any + Context element.
    • +
    • The resourceOnlyServlets attribute of any + Context element.
    • +
    • The tldValidation attribute of any + Context element.
    • +
    • The useRelativeRedirects attribute of any + Context element.
    • +
    • The xmlNamespaceAware attribute of any + Context element.
    • +
    • The xmlValidation attribute of any + Context element.
    • +
    • The sessionActivityCheck attribute of any + Manager element.
    • +
    • The sessionLastAccessAtStart attribute of any + Manager element.
    • +
    +
    + +
    + +
    + + +
    + + + + +

    If no logging configuration file is specified and no logging configuration class is specified + using the java.util.logging.config.class and java.util.logging.config.file + properties the default logging framework org.apache.juli will use the default + java.util.logging.SimpleFormatter for all console output. + To simply override the console output formatter, one can use the described property. Example: + -Dorg.apache.juli.formatter=org.apache.juli.OneLineFormatter

    +
    + + +

    The maximum number of log records that the JULI AsyncFileHandler will queue in memory. + New records are added to the queue and get asynchronously removed from the queue + and written to the files by a single writer thread. + When the queue is full and a new record is being logged + the log record will be handled based on the org.apache.juli.AsyncOverflowDropType setting.

    +

    The default value is 10000 records. + This number represents the global number of records, not on a per handler basis. +

    +
    + + +

    When the queue of log records of the JULI AsyncFileHandler is full, + new log records are handled according to the following setting: +

    +
      +
    • 1 - the newest record in the queue will be dropped and not logged
    • +
    • 2 - the oldest record in the queue will be dropped and not logged
    • +
    • 3 - suspend the logging thread until older records got written to the log file and the queue is no longer full. + This is the only setting that ensures that no messages get lost.
    • +
    • 4 - drop the current log record
    • +
    +

    The default value is 1 (drop the newest record in the queue).

    +
    + + +

    The type of logging to use for errors generated by invalid input data. + The options are: DEBUG_ALL, INFO_THEN_DEBUG, + INFO_ALL and NONE. When + INFO_THEN_DEBUG is used, the period for which errors are + logged at DEBUG rather than INFO is controlled by the system property + org.apache.juli.logging.UserDataHelper.SUPPRESSION_TIME. +

    +

    The default value is INFO_THEN_DEBUG.

    +

    The errors currently logged using this system are:

    +
      +
    • invalid cookies;
    • +
    • invalid parameters;
    • +
    • too many headers, too many parameters (hitting + maxHeaderCount or maxParameterCount limits + of a connector).
    • +
    • invalid host names
    • +
    • HTTP/2 stream closures
    • +
    +

    Other errors triggered by invalid input data may be added to this + system in later versions.

    +
    + + +

    When using INFO_THEN_DEBUG for + org.apache.juli.logging.UserDataHelper.CONFIG this system + property controls how long messages are logged at DEBUG after a message + has been logged at INFO. Once this period has elapsed, the next message + will be logged at INFO followed by a new suppression period where + messages are logged at DEBUG and so on. The value is measured + in seconds.

    +

    A value of 0 is equivalent to using INFO_ALL + for org.apache.juli.logging.UserDataHelper.CONFIG.

    +

    A negative value means an infinite suppression period.

    +

    The default value is 86400 (24 hours).

    +
    +
    + +
    + +
    + + + +

    A list of comma-separated file name patterns that is used as the default + value for pluggabilitySkip and tldSkip + attributes of the standard + JarScanFilter implementation.

    +

    The coded default is empty, however the system property is set in + a default Tomcat installation via the + $CATALINA_BASE/conf/catalina.properties file.

    +
    + + +

    A list of comma-separated file name patterns that is used as the default + value for pluggabilityScan and tldScan + attributes of the standard + JarScanFilter implementation.

    +

    The coded default is empty, however the system property is set in + a default Tomcat installation via the + $CATALINA_BASE/conf/catalina.properties file.

    +
    + +
    + +
    + +
    + + + + +

    The default size for buffers used in the Websockets container.

    +

    The default value is 8192 which corresponds to 8 KiB.

    +
    + + +

    Default value of the origin header that will be sent by the client + during the upgrade handshake.

    +

    The default is null so that no origin header is sent.

    +
    + + +

    The number of periodic ticks between periodic processing which + involves in particular session expiration checks.

    +

    The default value is 10 which corresponds to 10 + seconds.

    +
    + +
    + +
    + +
    + + + + +

    If this is false it will override the + useNaming attribute for all + Context elements.

    +
    + + +

    The class name of the factory to use to create resources of type + javax.sql.DataSource. If not specified the default of + org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory is used + which is a package renamed (to avoid conflicts) copy of + Apache Commons DBCP 2.

    +
    + + +

    The class name of the factory to use to create resources of type + jakarta.mail.Session. If not specified the default of + org.apache.naming.factory.MailSessionFactory is used.

    +
    + + +

    The location from which to load the catalina.properties configuration + file. This may be an absolute URL, a relative (to the current working + directory) URL or an alternative file name in which case Tomcat will + attempt to load the file from the default location of + $CATALINA_BASE/conf/.

    +
    + + +

    If true, the String cache is enabled for + ByteChunk.

    +

    If not specified, the default value of false will be used.

    +
    + + +

    If true, the String cache is enabled for + CharChunk.

    +

    If not specified, the default value of false will be used.

    +
    + + +

    The number of times toString() must be called before the + cache is activated.

    +

    If not specified, the default value of 20000 will be used.

    +
    + + +

    The size of the String cache.

    +

    If not specified, the default value of 200 will be used.

    +
    + + +

    The character to use to separate the WAR file and WAR content parts of + a WAR URL using the custom WAR scheme provided by Tomcat. This is + equivalent to how ! is used in JAR URLs.

    +

    If not specified, the default value of * will be used.

    +
    + + +

    The maximum length of String that will be cached.

    +

    If not specified, the default value of 128 will be used.

    +
    + + +

    The size of the cache to use parsed and formatted date value.

    +

    If not specified, the default value of 1000 will be used.

    +
    + + +

    If true, the server will exit if an exception happens + during the server initialization phase. To support this feature, this + system property is used as the default for the + throwOnFailure attribute of a Connector.

    +

    If not specified, the default value of false will be + used.

    +
    + + +

    The CombinedRealm allows nested Realms. This property controls the + maximum permitted number of levels of nesting.

    +

    If not specified, the default value of 3 will be used.

    +
    + + +

    The NestedCredentialHandler allows nested CredentialHandlers. This + property controls the maximum permitted number of levels of nesting.

    +

    If not specified, the default value of 3 will be used.

    +
    + + +

    The size of the buffer pool which is used by Tribes in bytes.

    +

    If not specified, the default value of 100*1024*1024 + (100 MiB) will be used.

    +
    + +
    + +
    + + + +
    diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml new file mode 100644 index 0000000..ceb4a84 --- /dev/null +++ b/webapps/docs/config/valve.xml @@ -0,0 +1,2678 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + The Valve Component + + + + +
    + +
    + +
    + +

    A Valve element represents a component that will be + inserted into the request processing pipeline for the associated + Catalina container (Engine, + Host, or Context). + Individual Valves have distinct processing capabilities, and are + described individually below.

    + +

    The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + +
    + + +
    + +

    Access logging is performed by valves that implement +org.apache.catalina.AccessLog interface.

    + + + + + +

    The Access Log Valve creates log files in the + same format as those created by standard web servers. These logs + can later be analyzed by standard log analysis tools to track page + hit counts, user session activity, and so on. This Valve + uses self-contained logic to write its log files, which can be + automatically rolled over at midnight each day. (The essential + requirement for access logging is to handle a large continuous + stream of data with low overhead. This Valve does not + use Apache Commons Logging, thus avoiding additional overhead and + potentially complex configuration).

    + +

    This Valve may be associated with any Catalina container + (Context, Host, or Engine), and + will record ALL requests processed by that container.

    + +

    Some requests may be handled by Tomcat before they are passed to a + container. These include redirects from /foo to /foo/ and the rejection of + invalid requests. Where Tomcat can identify the Context that + would have handled the request, the request/response will be logged in the + AccessLog(s) associated Context, Host + and Engine. Where Tomcat cannot identify the + Context that would have handled the request, e.g. in cases + where the URL is invalid, Tomcat will look first in the Engine, + then the default Host for the Engine and finally + the ROOT (or default) Context for the default Host + for an AccessLog implementation. Tomcat will use the first + AccessLog implementation found to log those requests that are + rejected before they are passed to a container.

    + +

    The output file will be placed in the directory given by the + directory attribute. The name of the file is composed + by concatenation of the configured prefix, timestamp and + suffix. The format of the timestamp in the file name can be + set using the fileDateFormat attribute. This timestamp will + be omitted if the file rotation is switched off by setting + rotatable to false.

    + +

    Warning: If multiple AccessLogValve instances + are used, they should be configured to use different output files.

    + +

    If sendfile is used, the response bytes will be written asynchronously + in a separate thread and the access log valve will not know how many bytes + were actually written. In this case, the number of bytes that was passed to + the sendfile thread for writing will be recorded in the access log valve. +

    +
    + + + +

    The Access Log Valve supports the following + configuration attributes:

    + + + + +

    Flag to determine if logging will be buffered. + If set to false, then access logging will be written after each + request. Default value: true +

    +
    + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.AccessLogValve to use the + default access log valve.

    +
    + + +

    The same as conditionUnless. This attribute is + provided for backwards compatibility. +

    +
    + + +

    Turns on conditional logging. If set, requests will be + logged only if ServletRequest.getAttribute() is + not null. For example, if this value is set to + important, then a particular request will only be logged + if ServletRequest.getAttribute("important") != null. + The use of Filters is an easy way to set/unset the attribute + in the ServletRequest on many different requests. +

    +
    + + +

    Turns on conditional logging. If set, requests will be + logged only if ServletRequest.getAttribute() is + null. For example, if this value is set to + junk, then a particular request will only be logged + if ServletRequest.getAttribute("junk") == null. + The use of Filters is an easy way to set/unset the attribute + in the ServletRequest on many different requests. +

    +
    + + +

    Absolute or relative pathname of a directory in which log files + created by this valve will be placed. If a relative path is + specified, it is interpreted as relative to $CATALINA_BASE. If + no directory attribute is specified, the default value is "logs" + (relative to $CATALINA_BASE).

    +
    + + +

    Character set used to write the log file. An empty string means + to use the default character set. Default value: UTF-8. +

    +
    + + +

    Allows a customized timestamp in the access log file name. + The file is rotated whenever the formatted timestamp changes. + The default value is .yyyy-MM-dd. + If you wish to rotate every hour, then set this value + to .yyyy-MM-dd.HH. + The date format will always be localized + using the locale en_US. +

    +
    + + +

    Flag to determine if IPv6 addresses should be represented in canonical + representation format as defined by RFC 5952. If set to true, + then IPv6 addresses will be written in canonical format (e.g. + 2001:db8::1:0:0:1, ::1), otherwise it will be + represented in full form (e.g. 2001:db8:0:0:1:0:0:1, + 0:0:0:0:0:0:0:1). Default value: false +

    +
    + + +

    The locale used to format timestamps in the access log + lines. Any timestamps configured using an + explicit SimpleDateFormat pattern (%{xxx}t) + are formatted in this locale. By default the + default locale of the Java process is used. Switching the + locale after the AccessLogValve is initialized is not supported. + Any timestamps using the common log format + (CLF) are always formatted in the locale + en_US. +

    +
    + + +

    The maximum number of days rotated access logs will be retained for + before being deleted. If not specified, the default value of + -1 will be used which means never delete old files.

    +
    + + +

    Log message buffers are usually recycled and re-used. To prevent + excessive memory usage, if a buffer grows beyond this size it will be + discarded. The default is 256 characters. This should be + set to larger than the typical access log message size.

    +
    + + +

    A formatting layout identifying the various information fields + from the request and response to be logged, or the word + common or combined to select a + standard format. See below for more information on configuring + this attribute.

    +
    + + +

    The prefix added to the start of each log file's name. If not + specified, the default value is "access_log".

    +
    + + +

    By default for a rotatable log the active access log file name + will contain the current timestamp in fileDateFormat. + During rotation the file is closed and a new file with the next + timestamp in the name is created and used. When setting + renameOnRotate to true, the timestamp + is no longer part of the active log file name. Only during rotation + the file is closed and then renamed to include the timestamp. + This is similar to the behavior of most log frameworks when + doing time based rotation. + Default value: false +

    +
    + + +

    Set to true to check for the existence of request + attributes (typically set by the RemoteIpValve and similar) that should + be used to override the values returned by the request for remote + address, remote host, server port and protocol. If the attributes are + not set, or this attribute is set to false then the values + from the request will be used. If not set, the default value of + false will be used.

    +
    + + +

    This attribute is no longer supported. Use the connector + attribute enableLookups instead.

    +

    If you have enableLookups on the connector set to + true and want to ignore it, use %a instead of + %h in the value of pattern.

    +
    + + +

    Flag to determine if log rotation should occur. + If set to false, then this file is never rotated and + fileDateFormat is ignored. + Default value: true +

    +
    + + +

    The suffix added to the end of each log file's name. If not + specified, the default value is "" (a zero-length string), + meaning that no suffix will be added.

    +
    + +
    + +

    Values for the pattern attribute are made up of literal + text strings, combined with pattern identifiers prefixed by the "%" + character to cause replacement by the corresponding variable value from + the current request and response. The following pattern codes are + supported:

    +
      +
    • %a - Remote IP address. + See also %{xxx}a below.
    • +
    • %A - Local IP address
    • +
    • %b - Bytes sent, excluding HTTP headers, or '-' if zero
    • +
    • %B - Bytes sent, excluding HTTP headers
    • +
    • %D - Time taken to process the request in microseconds
    • +
    • %F - Time taken to commit the response, in milliseconds
    • +
    • %h - Remote host name (or IP address if + enableLookups for the connector is false)
    • +
    • %H - Request protocol
    • +
    • %I - Current request thread name (can compare later with stacktraces)
    • +
    • %l - Remote logical username from identd (always returns + '-')
    • +
    • %m - Request method (GET, POST, etc.)
    • +
    • %p - Local port on which this request was received. + See also %{xxx}p below.
    • +
    • %q - Query string (prepended with a '?' if it exists)
    • +
    • %r - First line of the request (method and request URI)
    • +
    • %s - HTTP status code of the response
    • +
    • %S - User session ID
    • +
    • %t - Date and time, in Common Log Format
    • +
    • %T - Time taken to process the request, in seconds
    • +
    • %u - Remote user that was authenticated (if any), else '-' (escaped if required)
    • +
    • %U - Requested URL path
    • +
    • %v - Local server name
    • +
    • %X - Connection status when response is completed: +
        +
      • X = Connection aborted before the response completed.
      • +
      • + = Connection may be kept alive after the response is sent.
      • +
      • - = Connection will be closed after the response is sent.
      • +
      +
    • +
    + +

    + There is also support to write information incoming or outgoing + headers, cookies, session or request attributes and special + timestamp formats. + It is modeled after the + Apache HTTP Server log configuration + syntax. Each of them can be used multiple times with different xxx keys: +

    +
      +
    • %{xxx}a write remote address (client) (xxx==remote) or + connection peer address (xxx=peer)
    • +
    • %{xxx}i write value of incoming header with name xxx (escaped if required)
    • +
    • %{xxx}o write value of outgoing header with name xxx (escaped if required)
    • +
    • %{xxx}c write value of cookie(s) with name xxx (comma separated and escaped if required)
    • +
    • %{xxx}r write value of ServletRequest attribute with name xxx (escaped if required, value ?? if request is null)
    • +
    • %{xxx}s write value of HttpSession attribute with name xxx (escaped if required, value ?? if request is null)
    • +
    • %{xxx}p write local (server) port (xxx==local) or + remote (client) port (xxx=remote)
    • +
    • %{xxx}t write timestamp at the end of the request formatted using the + enhanced SimpleDateFormat pattern xxx
    • +
    • %{xxx}T write time taken to process the request using unit xxx + where valid units are ms for milliseconds, us for microseconds, + and s for seconds. %{s}T is equivalent to %T as well + as %{us}T is equivalent to %D.
    • +
    + +

    All formats supported by SimpleDateFormat are allowed in %{xxx}t. + In addition the following extensions have been added:

    +
      +
    • sec - number of seconds since the epoch
    • +
    • msec - number of milliseconds since the epoch
    • +
    • msec_frac - millisecond fraction
    • +
    +

    These formats cannot be mixed with SimpleDateFormat formats in the same format + token.

    + +

    Furthermore one can define whether to log the timestamp for the request start + time or the response finish time:

    +
      +
    • begin or prefix begin: chooses + the request start time
    • +
    • end or prefix end: chooses + the response finish time
    • +
    +

    By adding multiple %{xxx}t tokens to the pattern, one can + also log both timestamps.

    + +

    Escaping is applied as follows:

    +
      +
    • " is escaped as \"
    • +
    • \ is escaped as \\
    • +
    • Standard C escaping are used for \f, \n, + \r and \t
    • +
    • Any other control characters or characters with code points above 127 + are encoded using the standard Java unicode escaping + (\uXXXX)
    • +
    + +

    The shorthand pattern pattern="common" + corresponds to the Common Log Format defined by + '%h %l %u %t "%r" %s %b'.

    + +

    The shorthand pattern pattern="combined" + appends the values of the Referer and User-Agent + headers, each in double quotes, to the common pattern.

    + +

    Fields using unknown pattern identifiers will be logged as ???X??? + where X is the unknown identifier. Fields with unknown pattern identifier + plus {xxx} key will be logged as ???.

    + +

    When Tomcat is operating behind a reverse proxy, the client information + logged by the Access Log Valve may represent the reverse proxy, the browser + or some combination of the two depending on the configuration of Tomcat and + the reverse proxy. For Tomcat configuration options see + Proxies Support and the + Proxy How-To. For reverse proxies that + use mod_jk, see the generic + proxy documentation. For other reverse proxies, consult their + documentation.

    +
    + +
    + + + + + + +

    The Extended Access Log Valve extends the + Access Log Valve class, and so + uses the same self-contained logging logic. This means it + implements many of the same file handling attributes. The main + difference to the standard AccessLogValve is that + ExtendedAccessLogValve creates log files which + conform to the Working Draft for the + Extended Log File Format + defined by the W3C.

    + +
    + + + +

    The Extended Access Log Valve supports all + configuration attributes of the standard + Access Log Valve. Only the + values used for className and pattern differ.

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.ExtendedAccessLogValve to + use the extended access log valve.

    +
    + + +

    A formatting layout identifying the various information fields + from the request and response to be logged. + See below for more information on configuring this attribute.

    +
    + +
    + +

    Values for the pattern attribute are made up of + format tokens. Some of the tokens need an additional prefix. Possible + prefixes are c for "client", s for "server", + cs for "client to server", sc for + "server to client" or x for "application specific". + Furthermore some tokens are completed by an additional selector. + See the W3C specification + for more information about the format.

    + +

    The following format tokens are supported:

    +
      +
    • bytes - Bytes sent, excluding HTTP headers, or '-' if zero
    • +
    • c-dns - Remote host name (or IP address if + enableLookups for the connector is false)
    • +
    • c-ip - Remote IP address
    • +
    • cs-method - Request method (GET, POST, etc.)
    • +
    • cs-uri - Request URI
    • +
    • cs-uri-query - Query string (prepended with a '?' if it exists)
    • +
    • cs-uri-stem - Requested URL path
    • +
    • date - The date in yyyy-mm-dd format for GMT
    • +
    • s-dns - Local host name
    • +
    • s-ip - Local IP address
    • +
    • sc-status - HTTP status code of the response
    • +
    • time - Time the request was served in HH:mm:ss format for GMT
    • +
    • time-taken - Time (in seconds as floating point) taken to serve the request
    • +
    • x-threadname - Current request thread name (can compare later with stacktraces)
    • +
    + +

    For any of the x-H(XXX) the following method will be called from the + HttpServletRequest object:

    +
      +
    • x-H(authType): getAuthType
    • +
    • x-H(characterEncoding): getCharacterEncoding
    • +
    • x-H(contentLength): getContentLength
    • +
    • x-H(locale): getLocale
    • +
    • x-H(protocol): getProtocol
    • +
    • x-H(remoteUser): getRemoteUser
    • +
    • x-H(requestedSessionId): getRequestedSessionId
    • +
    • x-H(requestedSessionIdFromCookie): + isRequestedSessionIdFromCookie
    • +
    • x-H(requestedSessionIdValid): + isRequestedSessionIdValid
    • +
    • x-H(scheme): getScheme
    • +
    • x-H(secure): isSecure
    • +
    + +

    + There is also support to write information about headers + cookies, context, request or session attributes and request + parameters. +

    +
      +
    • cs(XXX) for incoming request headers with name XXX
    • +
    • sc(XXX) for outgoing response headers with name XXX
    • +
    • x-A(XXX) for the servlet context attribute with name XXX
    • +
    • x-C(XXX) for the cookie(s) with name XXX (comma separated if required)
    • +
    • x-O(XXX) for a concatenation of all outgoing response headers with name XXX
    • +
    • x-P(XXX) for the URL encoded (using UTF-8) request parameter with name XXX
    • +
    • x-R(XXX) for the request attribute with name XXX
    • +
    • x-S(XXX) for the session attribute with name XXX
    • +
    + +
    + +
    + + + + + +

    The JSON Access Log Valve extends the + Access Log Valve, and so + uses the same self-contained logging logic. This means it + implements the same file handling attributes. The main + difference to the standard AccessLogValve is that + JsonAccessLogValve creates log files which + follow the JSON syntax as defined by + RFC 8259.

    + +
    + + + +

    The JSON Access Log Valve supports all + configuration attributes of the standard + Access Log Valve. Only the + values used for className differ.

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.JsonAccessLogValve to + use the extended access log valve.

    +
    + +
    + +

    While the patterns supported are the same as for the regular + Access Log Valve, + there are a few differences: +

      +
    • requests are logged as JSON objects.
    • +
    • each supported "%X" single character pattern + identifier results in a key value pair in this object. + See below for the list of keys used for the respective pattern + identifiers.
    • +
    • each pattern identifiers using a subkey of the form %{xxx}X + where "X" is one of "a", "p" or "t" + results in a key value pair of the form "key-xxx". + See below for the list of keys used for the respective pattern + identifiers.
    • +
    • each pattern identifiers using a subkey of the form %{xxx}X + where "X" is one of "c", "i", "o", "r" or "s" + results in a sub object. See below for the key pointing at this + sub object. The keys in the sub object are the "xxx" subkeys in the pattern.
    • +
    • each unsupported "%X" character pattern + identifier results in a key value pair using the key "other-X".
    • +
    • the values logged are the same as the ones logged by + the standard Access Log Valve + for the same pattern identifiers.
    • +
    • any "xxx" subkeys get Json escaped.
    • +
    • any verbatim text between pattern identifiers gets silently ignored.
    • +
    + The JSON object keys used for the pattern identifiers which + do not generate a sub object are the following: +
      +
    • %a: remoteAddr
    • +
    • %A: localAddr
    • +
    • %b: size
    • +
    • %B: byteSentNC
    • +
    • %D: elapsedTime
    • +
    • %F: firstByteTime
    • +
    • %h: host
    • +
    • %H: protocol
    • +
    • %I: threadName
    • +
    • %l: logicalUserName
    • +
    • %m: method
    • +
    • %p: port
    • +
    • %q: query
    • +
    • %r: request
    • +
    • %s: statusCode
    • +
    • %S: sessionId
    • +
    • %t: time
    • +
    • %T: elapsedTimeS
    • +
    • %u: user
    • +
    • %U: path
    • +
    • %v: localServerName
    • +
    • %X: connectionStatus
    • +
    + The JSON object keys used for the pattern identifiers which + generate a sub object are the following: +
      +
    • %c: cookies
    • +
    • %i: requestHeaders
    • +
    • %o: responseHeaders
    • +
    • %r: requestAttributes
    • +
    • %s: sessionAttributes
    • +
    +

    + +
    + +
    + +
    + + +
    + + + + + + +

    The Remote Address Valve allows you to compare the + IP address of the client that submitted this request against one or more + regular expressions, and either allow the request to continue + or refuse to process the request from this client. A Remote Address + Valve can be associated with any Catalina container + (Engine, Host, or + Context), and must accept any request + presented to this container for processing before it will be passed on.

    + +

    The syntax for regular expressions is different than that for + 'standard' wildcard matching. Tomcat uses the java.util.regex + package. Please consult the Java documentation for details of the + expressions supported.

    + +

    After setting the attribute addConnectorPort to + true, one can append the server connector port separated with a + semicolon (";") to allow different expressions for each connector.

    + +

    By setting the attribute usePeerAddress to + true, the valve will use the connection peer address in its + checks. This will differ from the client IP, if a reverse proxy is used + in front of Tomcat in combination with either the AJP protocol, or the + HTTP protocol plus the RemoteIp(Valve|Filter).

    + +

    A refused request will be answered a response with status code + 403. This status code can be overwritten using the attribute + denyStatus.

    + +

    By setting the attribute invalidAuthenticationWhenDeny to + true, the behavior when a request is refused can be changed + to not deny but instead set an invalid authentication + header. This is useful in combination with the context attribute + preemptiveAuthentication="true".

    + +

    Note: There is a caveat when using this valve with + IPv6 addresses. Format of the IP address that this valve is processing + depends on the API that was used to obtain it. If the address was obtained + from Java socket using Inet6Address class, its format will be + x:x:x:x:x:x:x:x. That is, the IP address for localhost + will be 0:0:0:0:0:0:0:1 instead of the more widely used + ::1. Consult your access logs for the actual value.

    + +

    See also: Remote Host Valve, + Remote CIDR Valve, + Remote IP Valve, + HTTP Connector configuration.

    +
    + + + +

    The Remote Address Valve supports the following + configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.RemoteAddrValve.

    +
    + + +

    A regular expression (using java.util.regex) that the + remote client's IP address is compared to. If this attribute + is specified, the remote address MUST match for this request to be + accepted. If this attribute is not specified, all requests will be + accepted UNLESS the remote address matches a deny + pattern.

    +
    + + +

    A regular expression (using java.util.regex) that the + remote client's IP address is compared to. If this attribute + is specified, the remote address MUST NOT match for this request to be + accepted. If this attribute is not specified, request acceptance is + governed solely by the allow attribute.

    +
    + + +

    HTTP response status code that is used when rejecting denied + request. The default value is 403. For example, + it can be set to the value 404.

    +
    + + +

    Append the server connector port to the client IP address separated + with a semicolon (";"). If this is set to true, the + expressions configured with allow and + deny is compared against ADDRESS;PORT + where ADDRESS is the client IP address and + PORT is the Tomcat connector port which received the + request. The default value is false.

    +
    + + +

    When a request should be denied, do not deny but instead + set an invalid authentication header. This only works + if the context has the attribute preemptiveAuthentication="true" + set. An already existing authentication header will not be + overwritten. In effect this will trigger authentication instead of deny + even if the application does not have a security constraint configured.

    +

    This can be combined with addConnectorPort to trigger authentication + depending on the client and the connector that is used to access an application.

    +
    + + +

    Use the connection peer address instead of the client IP address. + They will differ, if a reverse proxy is used in front of Tomcat in + combination with either the AJP protocol, or the HTTP protocol plus + the RemoteIp(Valve|Filter).

    +
    + +
    + +
    + + +

    To allow access only for the clients connecting from localhost:

    + ]]> +
    + + +

    To allow unrestricted access for the clients connecting from localhost + but for all other clients only to port 8443:

    + ]]> +
    + + +

    To allow unrestricted access to port 8009, but trigger basic + authentication if the application is accessed on another port:

    + + ... + + + ... +]]> +
    + +
    + + + + + + +

    The Remote Host Valve allows you to compare the + hostname of the client that submitted this request against one or more + regular expressions, and either allow the request to continue + or refuse to process the request from this client. A Remote Host + Valve can be associated with any Catalina container + (Engine, Host, or + Context), and must accept any request + presented to this container for processing before it will be passed on.

    + +

    The syntax for regular expressions is different than that for + 'standard' wildcard matching. Tomcat uses the java.util.regex + package. Please consult the Java documentation for details of the + expressions supported.

    + +

    After setting the attribute addConnectorPort to + true, one can append the server connector port separated with a + semicolon (";") to allow different expressions for each connector.

    + +

    A refused request will be answered a response with status code + 403. This status code can be overwritten using the attribute + denyStatus.

    + +

    By setting the attribute invalidAuthenticationWhenDeny to + true, the behavior when a request is refused can be changed + to not deny but instead set an invalid authentication + header. This is useful in combination with the context attribute + preemptiveAuthentication="true".

    + +

    Note: This valve processes the value returned by + method ServletRequest.getRemoteHost(). To allow the method + to return proper host names, you have to enable "DNS lookups" feature on + a Connector.

    + +

    See also: Remote Address Valve, + Remote CIDR Valve, + Remote IP Valve, + HTTP Connector configuration.

    +
    + + + +

    The Remote Host Valve supports the following + configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.RemoteHostValve.

    +
    + + +

    A regular expression (using java.util.regex) that the + remote client's hostname is compared to. If this attribute + is specified, the remote hostname MUST match for this request to be + accepted. If this attribute is not specified, all requests will be + accepted UNLESS the remote hostname matches a deny + pattern.

    +
    + + +

    A regular expression (using java.util.regex) that the + remote client's hostname is compared to. If this attribute + is specified, the remote hostname MUST NOT match for this request to be + accepted. If this attribute is not specified, request acceptance is + governed solely by the allow attribute.

    +
    + + +

    HTTP response status code that is used when rejecting denied + request. The default value is 403. For example, + it can be set to the value 404.

    +
    + + +

    Append the server connector port to the client hostname separated + with a semicolon (";"). If this is set to true, the + expressions configured with allow and + deny is compared against HOSTNAME;PORT + where HOSTNAME is the client hostname and + PORT is the Tomcat connector port which received the + request. The default value is false.

    +
    + + +

    When a request should be denied, do not deny but instead + set an invalid authentication header. This only works + if the context has the attribute preemptiveAuthentication="true" + set. An already existing authentication header will not be + overwritten. In effect this will trigger authentication instead of deny + even if the application does not have a security constraint configured.

    +

    This can be combined with addConnectorPort to trigger authentication + depending on the client and the connector that is used to access an application.

    +
    + +
    + +
    + +
    + + + + + +

    The Remote CIDR Valve allows you to compare the + IP address of the client that submitted this request against one or more + netmasks following the CIDR notation, and either allow the request to + continue or refuse to process the request from this client. IPv4 and + IPv6 are both fully supported. A Remote CIDR Valve can be associated + with any Catalina container (Engine, + Host, or Context), and + must accept any request presented to this container for processing before + it will be passed on. +

    + +

    This valve mimics Apache's Order, + Allow from and Deny from directives, + with the following limitations: +

    + +
      +
    • Order will always be allow, deny;
    • +
    • dotted quad notations for netmasks are not supported (that is, you + cannot write 192.168.1.0/255.255.255.0, you must write + 192.168.1.0/24; +
    • +
    • shortcuts, like 10.10., which is equivalent to + 10.10.0.0/16, are not supported; +
    • +
    • as the valve name says, this is a CIDR only valve, + therefore subdomain notations like .mydomain.com are not + supported either. +
    • +
    + +

    After setting the attribute addConnectorPort to + true, one can append the server connector port separated with a + semicolon (";") to allow different expressions for each connector.

    + +

    By setting the attribute usePeerAddress to + true, the valve will use the connection peer address in its + checks. This will differ from the client IP, if a reverse proxy is used + in front of Tomcat in combination with either the AJP protocol, or the + HTTP protocol plus the RemoteIp(Valve|Filter).

    + +

    A refused request will be answered a response with status code + 403. This status code can be overwritten using the attribute + denyStatus.

    + +

    By setting the attribute invalidAuthenticationWhenDeny to + true, the behavior when a request is refused can be changed + to not deny but instead set an invalid authentication + header. This is useful in combination with the context attribute + preemptiveAuthentication="true".

    + +

    Some more features of this valve are: +

    + +
      +
    • if you omit the CIDR prefix, this valve becomes a single IP + valve;
    • +
    • unlike the Remote Host Valve, + it can handle IPv6 addresses in condensed form (::1, + fe80::/71, etc).
    • +
    + +

    See also: Remote Address Valve, + Remote Host Valve, + Remote IP Valve, + HTTP Connector configuration.

    +
    + + + +

    The Remote CIDR Valve supports the following + configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.RemoteCIDRValve.

    +
    + + +

    A comma-separated list of IPv4 or IPv6 netmasks or addresses + that the remote client's IP address is matched against. + If this attribute is specified, the remote address MUST match + for this request to be accepted. If this attribute is not specified, + all requests will be accepted UNLESS the remote IP is matched by a + netmask in the deny attribute. +

    +
    + + +

    A comma-separated list of IPv4 or IPv6 netmasks or addresses + that the remote client's IP address is matched against. + If this attribute is specified, the remote address MUST NOT match + for this request to be accepted. If this attribute is not specified, + request acceptance is governed solely by the accept + attribute. +

    +
    + + +

    HTTP response status code that is used when rejecting denied + request. The default value is 403. For example, + it can be set to the value 404.

    +
    + + +

    Append the server connector port to the client IP address separated + with a semicolon (";"). If this is set to true, the + expressions configured with allow and + deny is compared against ADDRESS;PORT + where ADDRESS is the client IP address and + PORT is the Tomcat connector port which received the + request. The default value is false.

    +
    + + +

    When a request should be denied, do not deny but instead + set an invalid authentication header. This only works + if the context has the attribute preemptiveAuthentication="true" + set. An already existing authentication header will not be + overwritten. In effect this will trigger authentication instead of deny + even if the application does not have a security constraint configured.

    +

    This can be combined with addConnectorPort to trigger authentication + depending on the client and the connector that is used to access an application.

    +
    + + +

    Use the connection peer address instead of the client IP address. + They will differ, if a reverse proxy is used in front of Tomcat in + combination with either the AJP protocol, or the HTTP protocol plus + the RemoteIp(Valve|Filter).

    +
    + +
    + +
    + + +

    To allow access only for the clients connecting from localhost:

    + ]]> +
    + + +

    To allow unrestricted access for the clients connecting from the local network + but for all clients in network 10. only to port 8443:

    + ]]> +
    + + +

    To allow access to port 8009 from network 10., but trigger basic + authentication if the application is accessed on another port:

    + + ... + + + ... +]]> +
    + +
    + +
    + + +
    + + +

    + When using mod_jk or mod_proxy_ajp, the client's session id is used to + determine which back-end server will be used to serve the request. If the + target node is being "drained" (in mod_jk, this is the DISABLED + state; in mod_proxy_ajp, this is the Drain (N) state), requests + for expired sessions can actually cause the draining node to fail to + drain. +

    +

    + Unfortunately, AJP-based load-balancers cannot prove whether the + client-provided session id is valid or not and therefore will send any + requests for a session that appears to be targeted to that node to the + disabled (or "draining") node, causing the "draining" process to take + longer than necessary. +

    +

    + This Valve detects requests for invalid sessions, strips the session + information from the request, and redirects back to the same URL, where + the load-balancer should choose a different (active) node to handle the + request. This will accelerate the "draining" process for the disabled + node(s). +

    + +

    + The activation state of the node is sent by the load-balancer in the + request, so no state change on the node being disabled is necessary. Simply + configure this Valve in your valve pipeline and it will take action when + the activation state is set to "disabled". +

    + +

    + You should take care to register this Valve earlier in the Valve pipeline + than any authentication Valves, because this Valve should be able to + redirect a request before any authentication Valve saves a request to a + protected resource. If this happens, a new session will be created and + the draining process will stall because a new, valid session will be + established. +

    +
    + + +

    The Load Balancer Draining Valve supports the + following configuration attributes:

    + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.LoadBalancerDrainingValve. +

    +
    + + +

    Allows setting a custom redirect code to be used when the client + is redirected to be re-balanced by the load-balancer. The default is + 307 TEMPORARY_REDIRECT.

    +
    + + +

    When used with ignoreCookieValue, a client can present + this cookie (and accompanying value) that will cause this Valve to + do nothing. This will allow you to probe your disabled node + before re-enabling it to make sure that it is working as expected.

    +
    + + +

    When used with ignoreCookieName, a client can present + a cookie (and accompanying value) that will cause this Valve to + do nothing. This will allow you to probe your disabled node + before re-enabling it to make sure that it is working as expected.

    +
    +
    +
    +
    + + + + + +

    Tomcat port of + mod_remoteip, + this valve replaces the apparent client remote IP address and hostname for + the request with the IP address list presented by a proxy or a load balancer + via a request headers (e.g. "X-Forwarded-For").

    + +

    Another feature of this valve is to replace the apparent scheme + (http/https), server port and request.secure with the scheme presented + by a proxy or a load balancer via a request header + (e.g. "X-Forwarded-Proto").

    + +

    This Valve may be used at the Engine, Host or + Context level as required. Normally, this Valve would be used + at the Engine level.

    + +

    If used in conjunction with Remote Address/Host valves then this valve + should be defined first to ensure that the correct client IP address is + presented to the Remote Address/Host valves.

    + +

    Note: By default this valve has no effect on the + values that are written into access log. The original values are restored + when request processing leaves the valve and that always happens earlier + than access logging. To pass the remote address, remote host, server port + and protocol values set by this valve to the access log, + they are put into request attributes. Publishing these values here + is enabled by default, but AccessLogValve should be explicitly + configured to use them. See documentation for + requestAttributesEnabled attribute of + AccessLogValve.

    + +

    The names of request attributes that are set by this valve + and can be used by access logging are the following:

    + +
      +
    • org.apache.catalina.AccessLog.RemoteAddr
    • +
    • org.apache.catalina.AccessLog.RemoteHost
    • +
    • org.apache.catalina.AccessLog.Protocol
    • +
    • org.apache.catalina.AccessLog.ServerPort
    • +
    • org.apache.tomcat.remoteAddr
    • +
    + +
    + + + +

    The Remote IP Valve supports the + following configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.RemoteIpValve.

    +
    + + +

    Name of the HTTP Header read by this valve that holds the list of + traversed IP addresses starting from the requesting client. If not + specified, the default of x-forwarded-for is used.

    +
    + + +

    Regular expression (using java.util.regex) that a + proxy's IP address must match to be considered an internal proxy. + Internal proxies that appear in the remoteIpHeader will + be trusted and will not appear in the proxiesHeader + value. If not specified the default value of + 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|100\.6[4-9]{1}\.\d{1,3}\.\d{1,3}|100\.[7-9]{1}\d{1}\.\d{1,3}\.\d{1,3}|100\.1[0-1]{1}\d{1}\.\d{1,3}\.\d{1,3}|100\.12[0-7]{1}\.\d{1,3}\.\d{1,3}|172\.1[6-9]{1}\.\d{1,3}\.\d{1,3}|172\.2[0-9]{1}\.\d{1,3}\.\d{1,3}|172\.3[0-1]{1}\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1 + will be used.

    +
    + + +

    Name of the HTTP header created by this valve to hold the list of + proxies that have been processed in the incoming + remoteIpHeader. If not specified, the default of + x-forwarded-by is used.

    +
    + + +

    Set to true to set the request attributes used by + AccessLog implementations to override the values returned by the + request for remote address, remote host, server port and protocol. + Request attributes are also used to enable the forwarded remote address + to be displayed on the status page of the Manager web application. + If not set, the default value of true will be used.

    +
    + + +

    Regular expression (using java.util.regex) that a + proxy's IP address must match to be considered an trusted proxy. + Trusted proxies that appear in the remoteIpHeader will + be trusted and will appear in the proxiesHeader value. + If not specified, no proxies will be trusted.

    +
    + + +

    Name of the HTTP Header read by this valve that holds the protocol + used by the client to connect to the proxy. If not specified, the + default of X-Forwarded-Proto is used.

    +
    + + +

    Name of the HTTP Header read by this valve that holds the host + used by the client to connect to the proxy. If not specified, the + default of null is used.

    +
    + + +

    Name of the HTTP Header read by this valve that holds the port + used by the client to connect to the proxy. If not specified, the + default of null is used.

    +
    + + +

    Value of the protocolHeader to indicate that it is + an HTTPS request. If not specified, the default of https is + used.

    +
    + + +

    Value returned by ServletRequest.getServerPort() + when the protocolHeader indicates http + protocol and no portHeader is present. If not + specified, the default of 80 is used.

    +
    + + +

    Value returned by ServletRequest.getServerPort() + when the protocolHeader indicates https + protocol and no portHeader is present. If not + specified, the default of 443 is used.

    +
    + + +

    If true, the value returned by + ServletRequest.getLocalHost() and + ServletRequest.getServerHost() is modified by the this + valve. If not specified, the default of false is used.

    +
    + + +

    If true, the value returned by + ServletRequest.getLocalPort() and + ServletRequest.getServerPort() is modified by the this + valve. If not specified, the default of false is used.

    +
    + +
    + +
    + +
    + + + + + + +

    When using mod_proxy_http, the client SSL information is not included in + the protocol (unlike mod_jk and mod_proxy_ajp). To make the client SSL + information available to Tomcat, some additional configuration is required. + In httpd, mod_headers is used to add the SSL information as HTTP headers. In + Tomcat, this valve is used to read the information from the HTTP headers and + insert it into the request.

    + +

    Note: Ensure that the headers are always set by httpd for all requests to + prevent a client spoofing SSL information by sending fake headers.

    + +

    To configure httpd to set the necessary headers, add the following:

    +<IfModule ssl_module> + RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" + RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" + RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" + RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s" +</IfModule> + +
    + + + +

    The SSL Valve supports the following configuration + attribute:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.SSLValve. +

    +
    + + +

    Allows setting a custom name for the ssl_client_cert header. + If not specified, the default of ssl_client_cert is + used.

    +
    + + +

    Allows setting a custom name for the ssl_client_escaped_cert header. + If not specified, the default of ssl_client_escaped_cert is + used.

    +

    This header is useful for Nginx proxying, and takes precedence over + the ssl_client_cert header.

    +
    + + +

    Allows setting a custom name for the ssl_cipher header. + If not specified, the default of ssl_cipher is + used.

    +
    + + +

    Allows setting a custom name for the ssl_session_id header. + If not specified, the default of ssl_session_id is + used.

    +
    + + +

    Allows setting a custom name for the ssl_cipher_usekeysize header. + If not specified, the default of ssl_cipher_usekeysize is + used.

    +
    + +
    + +
    + +
    + + +
    + +
    + + + +

    The Single Sign On Valve is utilized when you wish to give users + the ability to sign on to any one of the web applications associated with + your virtual host, and then have their identity recognized by all other + web applications on the same virtual host.

    + +

    See the Single Sign On special + feature on the Host element for more information.

    + +
    + + + + +

    The Single Sign On Valve supports the following + configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.authenticator.SingleSignOn.

    +
    + + +

    Default false. Flag to determine whether each request needs to be + reauthenticated to the security Realm. If "true", this + Valve uses cached security credentials (username and password) to + reauthenticate to the Realm each request associated + with an SSO session. If "false", the Valve can itself authenticate + requests based on the presence of a valid SSO cookie, without + rechecking with the Realm.

    +
    + + +

    Sets the host domain to be used for sso cookies.

    +
    + + +

    Sets the cookie name to be used for sso cookies. The default value + is JSESSIONIDSSO

    +
    + +
    + +
    + + +
    + + +
    + +

    The valves in this section implement +org.apache.catalina.Authenticator interface.

    + + + + + +

    The Basic Authenticator Valve is automatically added to + any Context that is configured to use BASIC + authentication.

    + +

    If any non-default settings are required, the valve may be configured + within Context element with the required + values.

    + +
    + + + +

    The Basic Authenticator Valve supports the following + configuration attributes:

    + + + + +

    Are requests that appear to be CORS preflight requests allowed to + bypass the authenticator as required by the CORS specification. The + allowed values are never, filter and + always. never means that a request will never + bypass authentication even if it appears to be a CORS preflight request. + filter means that a request will bypass authentication if + it appears to be a CORS preflight request; it is mapped to a web + application that has the CORS + Filter enabled; and the CORS Filter is mapped to /*. + always means that all requests that appear to be CORS + preflight requests will bypass authentication. If not set, the default + value is never.

    +
    + + +

    Should a session always be used once a user is authenticated? This + may offer some performance benefits since the session can then be used + to cache the authenticated Principal, hence removing the need to + authenticate the user via the Realm on every request. This may be of + help for combinations such as BASIC authentication used with the + JNDIRealm or DataSourceRealms. However there will also be the + performance cost of creating and GC'ing the session. If not set, the + default value of false will be used.

    +
    + + +

    Should we cache authenticated Principals if the request is part of an + HTTP session? If not specified, the default value of true + will be used.

    +
    + + +

    Controls if the session ID is changed if a session exists at the + point where users are authenticated. This is to prevent session fixation + attacks. If not set, the default value of true will be + used.

    +
    + + +

    Controls if the WWW-Authenticate HTTP header includes a + charset authentication parameter as per RFC 7617. The only + permitted options are null, the empty string and + UTF-8. If UTF-8 is specified then the + charset authentication parameter will be sent with that + value and the provided user name and optional password will be converted + from bytes to characters using UTF-8. Otherwise, no charset + authentication parameter will be sent and the provided user name and + optional password will be converted from bytes to characters using + ISO-8859-1. The default value is null

    +
    + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.authenticator.BasicAuthenticator.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers but will also cause secured pages to be + cached by proxies which will almost certainly be a security issue. + securePagesWithPragma offers an alternative, secure, + workaround for browser caching issues. If not set, the default value of + true will be used.

    +
    + + +

    Name of the Java class of the + javax.security.auth.callback.CallbackHandler implementation + which should be used by JASPIC. If none is specified the default + org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl + will be used.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers by using + Cache-Control: private rather than the default of + Pragma: No-cache and Cache-control: No-cache. + If not set, the default value of false will be used.

    +
    + + +

    Name of the algorithm to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the platform + default provider and the default algorithm will be used. If not + specified, the default algorithm of SHA1PRNG will be used. If the + default algorithm is not supported, the platform default will be used. + To specify that the platform default should be used, do not set the + secureRandomProvider attribute and set this attribute to the empty + string.

    +
    + + +

    Name of the Java class that extends + java.security.SecureRandom to use to generate SSO session + IDs. If not specified, the default value is + java.security.SecureRandom.

    +
    + + +

    Name of the provider to use to create the + java.security.SecureRandom instances that generate SSO + session IDs. If an invalid algorithm and/or provider is specified, the + platform default provider and the default algorithm will be used. If not + specified, the platform default provider will be used.

    +
    + + +

    Controls whether the auth information (remote user and auth type) + shall be returned as response headers for a forwarded/proxied request. + When the RemoteIpValve or RemoteIpFilter mark + a forwarded request with the Globals.REQUEST_FORWARDED_ATTRIBUTE + this authenticator can return the values of + HttpServletRequest.getRemoteUser() and + HttpServletRequest.getAuthType() as response headers + remote-user and auth-type to a reverse proxy. + This is useful, e.g., for access log consistency or other decisions to make. + If not specified, the default value is false.

    +
    + + +

    Controls whether leading and/or trailing whitespace is removed from + the parsed credentials. If not specified, the default value is + false. +

    +

    Note: This attribute will be removed from Tomcat 11 onwards.

    +
    + +
    + +
    + +
    + + + + + + +

    The Digest Authenticator Valve is automatically added to + any Context that is configured to use DIGEST + authentication.

    + +

    If any non-default settings are required, the valve may be configured + within Context element with the required + values.

    + +
    + + + +

    The Digest Authenticator Valve supports the following + configuration attributes:

    + + + + +

    A comma-separated list of digest algorithms to be used for the + authentication process. Algorithms may be specified using the Java + Standard names or the names used by RFC 7616. If not specified, the + default value of SHA-256,MD5 will be used.

    +
    + + +

    Are requests that appear to be CORS preflight requests allowed to + bypass the authenticator as required by the CORS specification. The + allowed values are never, filter and + always. never means that a request will never + bypass authentication even if it appears to be a CORS preflight request. + filter means that a request will bypass authentication if + it appears to be a CORS preflight request; it is mapped to a web + application that has the CORS + Filter enabled; and the CORS Filter is mapped to /*. + always means that all requests that appear to be CORS + preflight requests will bypass authentication. If not set, the default + value is never.

    +
    + + +

    Should a session always be used once a user is authenticated? This + may offer some performance benefits since the session can then be used + to cache the authenticated Principal, hence removing the need to + authenticate the user via the Realm on every request. This may be of + help for combinations such as BASIC authentication used with the + JNDIRealm or DataSourceRealms. However there will also be the + performance cost of creating and GC'ing the session. If not set, the + default value of false will be used.

    +
    + + +

    Should we cache authenticated Principals if the request is part of an + HTTP session? If not specified, the default value of false + will be used.

    +
    + + +

    Controls if the session ID is changed if a session exists at the + point where users are authenticated. This is to prevent session fixation + attacks. If not set, the default value of true will be + used.

    +
    + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.authenticator.DigestAuthenticator.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers but will also cause secured pages to be + cached by proxies which will almost certainly be a security issue. + securePagesWithPragma offers an alternative, secure, + workaround for browser caching issues. If not set, the default value of + true will be used.

    +
    + + +

    Name of the Java class of the + javax.security.auth.callback.CallbackHandler implementation + which should be used by JASPIC. If none is specified the default + org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl + will be used.

    +
    + + +

    The secret key used by digest authentication. If not set, a secure + random value is generated. This should normally only be set when it is + necessary to keep key values constant either across server restarts + and/or across a cluster.

    +
    + + +

    To protect against replay attacks, the DIGEST authenticator tracks + server nonce and nonce count values. This attribute controls the size + of that cache. If not specified, the default value of 1000 is used.

    +
    + + +

    Client requests may be processed out of order which in turn means + that the nonce count values may be processed out of order. To prevent + authentication failures when nonce counts are presented out of order + the authenticator tracks a window of nonce count values. This attribute + controls how big that window is. If not specified, the default value of + 100 is used.

    +
    + + +

    The time, in milliseconds, that a server generated nonce will be + considered valid for use in authentication. If not specified, the + default value of 300000 (5 minutes) will be used.

    +
    + + +

    The opaque server string used by digest authentication. If not set, a + random value is generated. This should normally only be set when it is + necessary to keep opaque values constant either across server restarts + and/or across a cluster.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers by using + Cache-Control: private rather than the default of + Pragma: No-cache and Cache-control: No-cache. + If not set, the default value of false will be used.

    +
    + + +

    Name of the algorithm to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the platform + default provider and the default algorithm will be used. If not + specified, the default algorithm of SHA1PRNG will be used. If the + default algorithm is not supported, the platform default will be used. + To specify that the platform default should be used, do not set the + secureRandomProvider attribute and set this attribute to the empty + string.

    +
    + + +

    Name of the Java class that extends + java.security.SecureRandom to use to generate SSO session + IDs. If not specified, the default value is + java.security.SecureRandom.

    +
    + + +

    Name of the provider to use to create the + java.security.SecureRandom instances that generate SSO + session IDs. If an invalid algorithm and/or provider is specified, the + platform default provider and the default algorithm will be used. If not + specified, the platform default provider will be used.

    +
    + + +

    Controls whether the auth information (remote user and auth type) + shall be returned as response headers for a forwarded/proxied request. + When the RemoteIpValve or RemoteIpFilter mark + a forwarded request with the Globals.REQUEST_FORWARDED_ATTRIBUTE + this authenticator can return the values of + HttpServletRequest.getRemoteUser() and + HttpServletRequest.getAuthType() as response headers + remote-user and auth-type to a reverse proxy. + This is useful, e.g., for access log consistency or other decisions to make. + If not specified, the default value is false.

    +
    + + +

    Should the URI be validated as required by RFC2617? If not specified, + the default value of true will be used. This should + normally only be set when Tomcat is located behind a reverse proxy and + the proxy is modifying the URI passed to Tomcat such that DIGEST + authentication always fails.

    +
    + +
    + +
    + +
    + + + + + + +

    The Form Authenticator Valve is automatically added to + any Context that is configured to use FORM + authentication.

    + +

    If any non-default settings are required, the valve may be configured + within Context element with the required + values.

    + +
    + + + +

    The Form Authenticator Valve supports the following + configuration attributes:

    + + + + +

    Are requests that appear to be CORS preflight requests allowed to + bypass the authenticator as required by the CORS specification. The + allowed values are never, filter and + always. never means that a request will never + bypass authentication even if it appears to be a CORS preflight request. + filter means that a request will bypass authentication if + it appears to be a CORS preflight request; it is mapped to a web + application that has the CORS + Filter enabled; and the CORS Filter is mapped to /*. + always means that all requests that appear to be CORS + preflight requests will bypass authentication. If not set, the default + value is never.

    +
    + + +

    If the authentication process creates a session, this is the maximum session timeout (in seconds) during the + authentication process. Once authentication is complete, the default session timeout will apply. Sessions that + exist before the authentication process starts will retain their original session timeout throughout. If not + set, the default value of 120 seconds will be used.

    +
    + + +

    Controls if the session ID is changed if a session exists at the + point where users are authenticated. This is to prevent session fixation + attacks. If not set, the default value of true will be + used.

    +
    + + +

    Character encoding to use to read the username and password parameters + from the request. If not set, the encoding of the request body will be + used.

    +
    + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.authenticator.FormAuthenticator.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers but will also cause secured pages to be + cached by proxies which will almost certainly be a security issue. + securePagesWithPragma offers an alternative, secure, + workaround for browser caching issues. If not set, the default value of + true will be used.

    +
    + + +

    Name of the Java class of the + javax.security.auth.callback.CallbackHandler implementation + which should be used by JASPIC. If none is specified the default + org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl + will be used.

    +
    + + +

    Controls the behavior of the FORM authentication process if the + process is misused, for example by directly requesting the login page + or delaying logging in for so long that the session expires. If this + attribute is set, rather than returning an error response code, Tomcat + will redirect the user to the specified landing page if the login form + is submitted with valid credentials. For the login to be processed, the + landing page must be a protected resource (i.e. one that requires + authentication). If the landing page does not require authentication + then the user will not be logged in and will be prompted for their + credentials again when they access a protected page.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers by using + Cache-Control: private rather than the default of + Pragma: No-cache and Cache-control: No-cache. + If not set, the default value of false will be used.

    +
    + + +

    Name of the algorithm to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the platform + default provider and the default algorithm will be used. If not + specified, the default algorithm of SHA1PRNG will be used. If the + default algorithm is not supported, the platform default will be used. + To specify that the platform default should be used, do not set the + secureRandomProvider attribute and set this attribute to the empty + string.

    +
    + + +

    Name of the Java class that extends + java.security.SecureRandom to use to generate SSO session + IDs. If not specified, the default value is + java.security.SecureRandom.

    +
    + + +

    Name of the provider to use to create the + java.security.SecureRandom instances that generate SSO + session IDs. If an invalid algorithm and/or provider is specified, the + platform default provider and the default algorithm will be used. If not + specified, the platform default provider will be used.

    +
    + + +

    Controls whether the auth information (remote user and auth type) + shall be returned as response headers for a forwarded/proxied request. + When the RemoteIpValve or RemoteIpFilter mark + a forwarded request with the Globals.REQUEST_FORWARDED_ATTRIBUTE + this authenticator can return the values of + HttpServletRequest.getRemoteUser() and + HttpServletRequest.getAuthType() as response headers + remote-user and auth-type to a reverse proxy. + This is useful, e.g., for access log consistency or other decisions to make. + If not specified, the default value is false.

    +
    + +
    + +
    + +
    + + + + + + +

    The SSL Authenticator Valve is automatically added to + any Context that is configured to use SSL + authentication.

    + +

    If any non-default settings are required, the valve may be configured + within Context element with the required + values.

    + +
    + + + +

    The SSL Authenticator Valve supports the following + configuration attributes:

    + + + + +

    Are requests that appear to be CORS preflight requests allowed to + bypass the authenticator as required by the CORS specification. The + allowed values are never, filter and + always. never means that a request will never + bypass authentication even if it appears to be a CORS preflight request. + filter means that a request will bypass authentication if + it appears to be a CORS preflight request; it is mapped to a web + application that has the CORS + Filter enabled; and the CORS Filter is mapped to /*. + always means that all requests that appear to be CORS + preflight requests will bypass authentication. If not set, the default + value is never.

    +
    + + +

    Should we cache authenticated Principals if the request is part of an + HTTP session? If not specified, the default value of true + will be used.

    +
    + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.authenticator.SSLAuthenticator.

    +
    + + +

    Controls if the session ID is changed if a session exists at the + point where users are authenticated. This is to prevent session fixation + attacks. If not set, the default value of true will be + used.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers but will also cause secured pages to be + cached by proxies which will almost certainly be a security issue. + securePagesWithPragma offers an alternative, secure, + workaround for browser caching issues. If not set, the default value of + true will be used.

    +
    + + +

    Name of the Java class of the + javax.security.auth.callback.CallbackHandler implementation + which should be used by JASPIC. If none is specified the default + org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl + will be used.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers by using + Cache-Control: private rather than the default of + Pragma: No-cache and Cache-control: No-cache. + If not set, the default value of false will be used.

    +
    + + +

    Name of the algorithm to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the platform + default provider and the default algorithm will be used. If not + specified, the default algorithm of SHA1PRNG will be used. If the + default algorithm is not supported, the platform default will be used. + To specify that the platform default should be used, do not set the + secureRandomProvider attribute and set this attribute to the empty + string.

    +
    + + +

    Name of the Java class that extends + java.security.SecureRandom to use to generate SSO session + IDs. If not specified, the default value is + java.security.SecureRandom.

    +
    + + +

    Name of the provider to use to create the + java.security.SecureRandom instances that generate SSO + session IDs. If an invalid algorithm and/or provider is specified, the + platform default provider and the default algorithm will be used. If not + specified, the platform default provider will be used.

    +
    + +
    + +
    + +
    + + + + + + +

    The SPNEGO Authenticator Valve is automatically added to + any Context that is configured to use SPNEGO + authentication.

    + +

    If any non-default settings are required, the valve may be configured + within Context element with the required + values.

    + +
    + + + +

    The SPNEGO Authenticator Valve supports the following + configuration attributes:

    + + + + +

    Are requests that appear to be CORS preflight requests allowed to + bypass the authenticator as required by the CORS specification. The + allowed values are never, filter and + always. never means that a request will never + bypass authentication even if it appears to be a CORS preflight request. + filter means that a request will bypass authentication if + it appears to be a CORS preflight request and the web application the + request maps to has the CORS + Filter enabled and mapped to /*. always + means that all requests that appear to be CORS preflight requests will + bypass authentication. If not set, the default value is + never.

    +
    + + +

    Should a session always be used once a user is authenticated? This + may offer some performance benefits since the session can then be used + to cache the authenticated Principal, hence removing the need to + authenticate the user on every request. This will also help with clients + that assume that the server will cache the authenticated user. However + there will also be the performance cost of creating and GC'ing the + session. For an alternative solution see + noKeepAliveUserAgents. If not set, the default value of + false will be used.

    +
    + + +

    A fix introduced in Java 8 update 40 ( + JDK-8048194) + onwards broke SPNEGO authentication for IE with Tomcat running on + Windows 2008 R2 servers. This option enables a work-around that allows + SPNEGO authentication to continue working. The work-around should not + impact other configurations so it is enabled by default. If necessary, + the workaround can be disabled by setting this attribute to + false.

    +
    + + +

    Should we cache authenticated Principals if the request is part of an + HTTP session? If not specified, the default value of true + will be used.

    +
    + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.authenticator.SpnegoAuthenticator. +

    +
    + + +

    Controls if the session ID is changed if a session exists at the + point where users are authenticated. This is to prevent session fixation + attacks. If not set, the default value of true will be + used.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers but will also cause secured pages to be + cached by proxies which will almost certainly be a security issue. + securePagesWithPragma offers an alternative, secure, + workaround for browser caching issues. If not set, the default value of + true will be used.

    +
    + + +

    Name of the Java class of the + javax.security.auth.callback.CallbackHandler implementation + which should be used by JASPIC. If none is specified the default + org.apache.catalina.authenticator.jaspic.CallbackHandlerImpl + will be used.

    +
    + + +

    The name of the JAAS login configuration to be used to login as the + service. If not specified, the default of + com.sun.security.jgss.krb5.accept is used.

    +
    + + +

    Some clients (not most browsers) expect the server to cache the + authenticated user information for a connection and do not resend the + credentials with every request. Tomcat will not do this unless an HTTP + session is available. A session will be available if either the + application creates one or if alwaysUseSession is enabled + for this Authenticator.

    +

    As an alternative to creating a session, this attribute may be used + to define the user agents for which HTTP keep-alive is disabled. This + means that a connection will only used for a single request and hence + there is no ability to cache authenticated user information per + connection. There will be a performance cost in disabling HTTP + keep-alive.

    +

    The attribute should be a regular expression that matches the entire + user-agent string, e.g. .*Chrome.*. If not specified, no + regular expression will be defined and no user agents will have HTTP + keep-alive disabled.

    +
    + + +

    Controls the caching of pages that are protected by security + constraints. Setting this to false may help work around + caching issues in some browsers by using + Cache-Control: private rather than the default of + Pragma: No-cache and Cache-control: No-cache. + If not set, the default value of false will be used.

    +
    + + +

    Name of the algorithm to use to create the + java.security.SecureRandom instances that generate session + IDs. If an invalid algorithm and/or provider is specified, the platform + default provider and the default algorithm will be used. If not + specified, the default algorithm of SHA1PRNG will be used. If the + default algorithm is not supported, the platform default will be used. + To specify that the platform default should be used, do not set the + secureRandomProvider attribute and set this attribute to the empty + string.

    +
    + + +

    Name of the Java class that extends + java.security.SecureRandom to use to generate SSO session + IDs. If not specified, the default value is + java.security.SecureRandom.

    +
    + + +

    Name of the provider to use to create the + java.security.SecureRandom instances that generate SSO + session IDs. If an invalid algorithm and/or provider is specified, the + platform default provider and the default algorithm will be used. If not + specified, the platform default provider will be used.

    +
    + + +

    Controls whether the auth information (remote user and auth type) + shall be returned as response headers for a forwarded/proxied request. + When the RemoteIpValve or RemoteIpFilter mark + a forwarded request with the Globals.REQUEST_FORWARDED_ATTRIBUTE + this authenticator can return the values of + HttpServletRequest.getRemoteUser() and + HttpServletRequest.getAuthType() as response headers + remote-user and auth-type to a reverse proxy. + This is useful, e.g., for access log consistency or other decisions to make. + If not specified, the default value is false.

    +
    + + +

    Controls if the user' delegated credential will be stored in + the user Principal. If available, the delegated credential will be + available to applications (e.g. for onward authentication to external + services) via the org.apache.catalina.realm.GSS_CREDENTIAL + request attribute. If not set, the default value of true + will be used.

    +
    + +
    + +
    + +
    + + +
    + + +
    + + + +

    The Error Report Valve is a simple error handler + for HTTP status codes that will generate and return HTML error pages. It can + also be configured to return pre-defined static HTML pages for specific + status codes and/or exception types.

    + +

    NOTE: Disabling both showServerInfo and showReport will + only return the HTTP status code.

    + +
    + + + +

    The Error Report Valve supports the following + configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.ErrorReportValve to use the + default error report valve.

    +
    + + +

    The location of the UTF-8 encoded HTML file to return for the HTTP + error code represented by nnn. For example, + errorCode.404 specifies the file to return for an HTTP 404 + error. The location may be relative or absolute. If relative, it must be + relative to $CATALINA_BASE. The special value of + errorCode.0 may be used to define a default error page to + be used if no error page is defined for a status code. If no matching + error page is found, the default Error Report Valve + response will be returned.

    +
    + + +

    The location of the UTF-8 encoded HTML file to return if an error has + occurred and the jakarta.servlet.error.exception request + attribute has been set to an instance of + fullyQualifiedClassName or a sub-class of it. For example, + errorCode.java.io.IOException specifies the file to return + for an IOException. The location may be relative or + absolute. If relative, it must be relative to + $CATALINA_BASE. If no matching error page is found, the + default Error Report Valve response will be + returned.

    +
    + + +

    Flag to determine if the error report (custom error message and/or + stack trace) is presented when an error occurs. If set to + false, then the error report is not returned in the HTML + response. + Default value: true +

    +
    + + +

    Flag to determine if server information is presented when an error + occurs. If set to false, then the server version is not + returned in the HTML response. + Default value: true +

    +
    + +
    + +
    + +
    + +
    + + + +

    The Json Error Report Valve is a simple error handler + for HTTP status codes that will return Json error messages.

    + +

    By specifying this class in errorReportValveClass attribute + in Host, it will be used instead of + ErrorReportValve and will return JSON response instead of HTML. +

    + +
    + + + +

    The Json Error Report Valve supports the following + configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.JsonErrorReportValve.

    +
    + +
    + +
    + +
    + +
    + + + +

    The Proxy Error Report Valve is a simple error handler + for HTTP status codes that will redirect or proxy to another location + responsible for the generation of the error report.

    + +

    By specifying this class in errorReportValveClass attribute + in Host, it will be used instead of + ErrorReportValve with the default attribute values. To + configure the attributes, the valve can be defined nested in the + Host element.

    + +
    + + + +

    The Proxy Error Report Valve supports the following + configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.ProxyErrorReportValve.

    +
    + + +

    If true, the valve will use the properties file + described below to associate the URLs with the status code. + If false, the configuration mechanism of the default + ErrorReportValve will be used instead. The default + value is false.

    +
    + + +

    If true, the valve will send a redirect to the URL. + If false, the valve will instead proxy the content from + the specified URL. The default value is true.

    +
    + +
    + +
    + + + +

    The Proxy Error Report Valve can use a resource file + ProxyErrorReportValve.properties + from the class path, where each entry is a statusCode=baseUrl. baseUrl + should not include any url parameters, statusCode, statusDescription, + requestUri, and throwable which will be automatically appended. A special + key named 0 should be used to match any other unmapped + code to a redirect or proxy URL.

    + +
    + +
    + +
    + + + +

    Web crawlers can trigger the creation of many thousands of sessions as + they crawl a site which may result in significant memory consumption. This + Valve ensures that crawlers are associated with a single session - just like + normal users - regardless of whether or not they provide a session token + with their requests.

    + +

    This Valve may be used at the Engine, Host or + Context level as required. Normally, this Valve would be used + at the Engine level.

    + +

    If used in conjunction with Remote IP valve then the Remote IP valve + should be defined before this valve to ensure that the correct client IP + address is presented to this valve.

    + +
    + + + +

    The Crawler Session Manager Valve supports the + following configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.CrawlerSessionManagerValve. +

    +
    + + +

    Flag to use the context name together with the client IP to + identify the session to re-use. Can be combined with hostAware. + Default value: true +

    +
    + + +

    Regular expression (using java.util.regex) that client + IP is matched against to determine if a request is from a web crawler. + By default such regular expression is not set.

    +
    + + +

    Regular expression (using java.util.regex) that the user + agent HTTP request header is matched against to determine if a request + is from a web crawler. If not set, the default of + .*[bB]ot.*|.*Yahoo! Slurp.*|.*Feedfetcher-Google.* is used.

    +
    + + +

    Flag to use the configured host together with the client IP to + identify the session to re-use. Can be combined with contextAware. + Default value: true +

    +
    + + +

    The minimum time in seconds that the Crawler Session Manager Valve + should keep the mapping of client IP to session ID in memory without any + activity from the client. The client IP / session cache will be + periodically purged of mappings that have been inactive for longer than + this interval. If not specified the default value of 60 + will be used.

    +
    + +
    + +
    + +
    + +
    + + + +

    This valve allows to detect requests that take a long time to process, + which might indicate that the thread that is processing it is stuck. + Additionally it can optionally interrupt such threads to try and unblock + them.

    +

    When such a request is detected, the current stack trace of its thread is + written to Tomcat log with a WARN level.

    +

    The IDs and names of the stuck threads are available through JMX in the + stuckThreadIds and stuckThreadNames attributes. + The IDs can be used with the standard Threading JVM MBean + (java.lang:type=Threading) to retrieve other information + about each stuck thread.

    + +
    + + + +

    The Stuck Thread Detection Valve supports the + following configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.StuckThreadDetectionValve. +

    +
    + + +

    Minimum duration in seconds after which a thread is considered stuck. + Default is 600 seconds. If set to 0, the detection is disabled.

    +

    Note: since the detection (and optional interruption) is done in the + background thread of the Container (Engine, Host or Context) declaring + this Valve, the threshold should be higher than the + backgroundProcessorDelay of this Container.

    +
    + + +

    Minimum duration in seconds after which a stuck thread should be + interrupted to attempt to "free" it.

    +

    Note that there's no guarantee that the thread will get unstuck. + This usually works well for threads stuck on I/O or locks, but is + probably useless in case of infinite loops.

    +

    Default is -1 which disables the feature. To enable it, the value + must be greater or equal to threshold.

    +
    + +
    + +
    + +
    + +
    + + + +

    The Semaphore Valve is able to limit the number of + concurrent request processing threads.

    +

    org.apache.catalina.valves.SemaphoreValve provides + methods which may be overridden by a subclass to customize behavior:

    +
      +
    • controlConcurrency may be overridden to add + conditions;
    • +
    • permitDenied may be overridden to add error handling + when a permit isn't granted.
    • +
    + +
    + + + +

    The Semaphore Valve supports the following + configuration attributes:

    + + + + +

    Flag to determine if a thread is blocked until a permit is available. + The default value is true.

    +
    + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.SemaphoreValve.

    +
    + + +

    Concurrency level of the semaphore. The default value is + 10.

    +
    + + +

    Fairness of the semaphore. The default value is + false.

    +
    + + +

    The error status code which will be returned to the client, if the + value is positive, when a permit cannot be acquired from the + sepmaphore. The default value is -1, which will mean + no error status will be sent back.

    +
    + + +

    Flag to determine if a thread may be interrupted until a permit is + available. The default value is false.

    +
    + +
    + +
    + +
    + +
    + + + +

    The Health Check Valve responds to + cloud orchestrators health checks.

    +
    + + + +

    The Health Check Valve supports the + following configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.HealthCheckValve.

    +
    + + +

    Path by the cloud orchestrators health check logic. If the valve + is associated with a context, then this will be relative to the context + path. Otherwise, the valve will match the full URI. + The default value is /health.

    +
    + + +

    If true the valve will check if its associated + container and all its children are available. + The default value is true.

    +
    + +
    + +
    + +
    + +
    + + + +

    The PersistentValve that implements per-request session + persistence. It is intended to be used with non-sticky load-balancers.

    + +
    + + + +

    The PersistentValve Valve supports the + following configuration attributes:

    + + + + +

    Java class name of the implementation to use. This MUST be set to + org.apache.catalina.valves.PersistentValve.

    +
    + + +

    For known file extensions or urls, you can use this filter pattern to + notify the valve that no session required during this request. If the + request matches this filter pattern, the valve assumes there has been no + need to restore session. An example filter would look like + filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt". + The filter is a regular expression using + java.util.regex.

    +
    + + +

    Flag to determine if a thread that blocks waiting for the per session + Semaphore should do so uninterruptibly. Has no effect if + semaphoreBlockOnAcquire is false. If not + specified, the default value of true will be used.

    +
    + + +

    Flag to determine if a thread that wishes to acquire the per session + Semaphore when it is held by another thread should block until it can + acquire the Semaphore or if the waiting request be rejected. If not + specified, the default value of true will be used.

    +
    + + +

    Flag to determine if the per session Semaphore will grant requests + for the Semaphore in the same order they were received. Has no effect if + semaphoreBlockOnAcquire is false. If not + specified, the default value of true will be used.

    +
    + +
    + +
    + +
    + + + + +
    diff --git a/webapps/docs/connectors.xml b/webapps/docs/connectors.xml new file mode 100644 index 0000000..c449e6c --- /dev/null +++ b/webapps/docs/connectors.xml @@ -0,0 +1,80 @@ + + + +]> + + + &project; + + + Remy Maucherat + Connectors How To + + + + +
    + +
    + +
    + +

    Choosing a connector to use with Tomcat can be difficult. This page will +list the connectors which are supported with this Tomcat release, and will +hopefully help you make the right choice according to your needs.

    + +
    + +
    + +

    The HTTP connector is setup by default with Tomcat, and is ready to use. This +connector features the lowest latency and best overall performance.

    + +

    For clustering, an HTTP load balancer with support for web sessions stickiness +must be installed to direct the traffic to the Tomcat servers. Tomcat supports mod_proxy +(on Apache HTTP Server 2.x, and included by default in Apache HTTP Server 2.2) as the load balancer. +It should be noted that the performance of HTTP proxying is usually lower than the +performance of AJP, so AJP clustering is often preferable.

    + +
    + +
    + +

    When using a single server, the performance when using a native webserver in +front of the Tomcat instance is most of the time significantly worse than a +standalone Tomcat with its default HTTP connector, even if a large part of the web +application is made of static files. If integration with the native webserver is +needed for any reason, an AJP connector will provide faster performance than +proxied HTTP. AJP clustering is the most efficient from the Tomcat perspective. +It is otherwise functionally equivalent to HTTP clustering.

    + +

    The native connectors supported with this Tomcat release are:

    +
      +
    • JK 1.2.x with any of the supported servers
    • +
    • mod_proxy on Apache HTTP Server 2.x (included by default in Apache HTTP Server 2.2), +with AJP enabled
    • +
    + +

    Other native connectors supporting AJP may work, but are no longer supported.

    + +
    + + + +
    diff --git a/webapps/docs/default-servlet.xml b/webapps/docs/default-servlet.xml new file mode 100644 index 0000000..33ce47e --- /dev/null +++ b/webapps/docs/default-servlet.xml @@ -0,0 +1,342 @@ + + + +]> + + + &project; + + + Tim Funk + Default Servlet Reference + + + + +
    + +
    + +
    +

    +The default servlet is the servlet which serves static resources as well +as serves the directory listings (if directory listings are enabled). +

    +
    + +
    +

    +It is declared globally in $CATALINA_BASE/conf/web.xml. +By default here is it's declaration: +

    + + default + + org.apache.catalina.servlets.DefaultServlet + + + debug + 0 + + + listings + false + + 1 + + +... + + + default + / + ]]> + +

    So by default, the default servlet is loaded at webapp startup and directory +listings are disabled and debugging is turned off.

    + +

    If you need to change the DefaultServlet settings for an application you can +override the default configuration by re-defining the DefaultServlet in +/WEB-INF/web.xml. However, this will cause problems if you attempt +to deploy the application on another container as the DefaultServlet class will +not be recognised. You can work-around this problem by using the Tomcat specific +/WEB-INF/tomcat-web.xml deployment descriptor. The format is +identical to /WEB-INF/web.xml. It will override any default +settings but not those in /WEB-INF/web.xml. Since it is Tomcat +specific, it will only be processed when the application is deployed on +Tomcat.

    +
    + +
    +

    + The DefaultServlet allows the following initParameters: +

    + + + + Debugging level. It is not very useful unless you are a tomcat + developer. As + of this writing, useful values are 0, 1, 11. [0] + + + If no welcome file is present, can a directory listing be + shown? + value may be true or false [false] +
    + Welcome files are part of the servlet api. +
    + WARNING: Listings of directories containing many entries are + expensive. Multiple requests for large directory listings can consume + significant proportions of server resources. +
    + + If a precompressed version of a file exists (a file with .br + or .gz appended to the file name located alongside the + original file), Tomcat will serve the precompressed file if the user + agent supports the matching content encoding (br or gzip) and this + option is enabled. [false] +
    + The precompressed file with the with .br or .gz + extension will be accessible if requested directly so if the original + resource is protected with a security constraint, the precompressed + versions must be similarly protected. +
    + It is also possible to configure the list of precompressed formats. + The syntax is comma separated list of + [content-encoding]=[file-extension] pairs. For example: + br=.br,gzip=.gz,bzip2=.bz2. If multiple formats are + specified, the client supports more than one and the client does not + express a preference, the order of the list of formats will be treated + as the server preference order and used to select the format returned. +
    + + If a directory listing is presented, a readme file may also + be presented with the listing. This file is inserted as is + so it may contain HTML. + + + If you wish to customize your directory listing, you + can use an XSL transformation. This value is a relative file name (to + either $CATALINA_BASE/conf/ or $CATALINA_HOME/conf/) which will be used + for all directory listings. This can be overridden per context and/or + per directory. See contextXsltFile and + localXsltFile below. The format of the xml is shown + below. + + + You may also customize your directory listing by context by + configuring contextXsltFile. This must be a context + relative path (e.g.: /path/to/context.xslt) to a file with + a .xsl or .xslt extension. This overrides + globalXsltFile. If this value is present but a file does + not exist, then globalXsltFile will be used. If + globalXsltFile does not exist, then the default + directory listing will be shown. + + + You may also customize your directory listing by directory by + configuring localXsltFile. This must be a file in the + directory where the listing will take place to with a + .xsl or .xslt extension. This overrides + globalXsltFile and contextXsltFile. If this + value is present but a file does not exist, then + contextXsltFile will be used. If + contextXsltFile does not exist, then + globalXsltFile will be used. If + globalXsltFile does not exist, then the default + directory listing will be shown. + + + Input buffer size (in bytes) when reading + resources to be served. [2048] + + + Output buffer size (in bytes) when writing + resources to be served. [2048] + + + Is this context "read only", so HTTP commands like PUT and + DELETE are rejected? [true] + + + File encoding to be used when reading static resources. + [platform default] + + + If a static file contains a byte order mark (BOM), should this be used + to determine the file encoding in preference to fileEncoding. This + setting must be one of true (remove the BOM and use it in + preference to fileEncoding), false (remove the BOM but do + not use it) or pass-through (do not use the BOM and do not + remove it). [true] + + + If the connector used supports sendfile, this represents the minimal + file size in KiB for which sendfile will be used. Use a negative value + to always disable sendfile. [48] + + + If true, the Accept-Ranges header will be set when appropriate for the + response. [true] + + + Should server information be presented in the response sent to clients + when directory listing is enabled. [true] + + + Should the server sort the listings in a directory. [false] + + + Should the server list all directories before all files. [false] + + + Should the server treat an HTTP PUT request with a Range header as a + partial PUT? Note that while RFC 7233 clarified that Range headers only + valid for GET requests, RFC 9110 (which obsoletes RFC 7233) now allows + partial puts. [true] + +
    +
    + +
    +

    You can override DefaultServlet with you own implementation and use that +in your web.xml declaration. If you +can understand what was just said, we will assume you can read the code +to DefaultServlet servlet and make the appropriate adjustments. (If not, +then that method isn't for you) +

    +

    +You can use either localXsltFile, contextXsltFile +or globalXsltFile and DefaultServlet will create +an xml document and run it through an xsl transformation based +on the values provided in the XSLT file. localXsltFile is first +checked, then contextXsltFile, followed by +globalXsltFile. If no XSLT files are configured, default behavior +is used. +

    + +

    +Format: +

    + + + + fileName1 + + + fileName2 + + ... + + + ]]> +
      +
    • size will be missing if type='dir'
    • +
    • Readme is a CDATA entry
    • +
    + +

    + The following is a sample xsl file which mimics the default tomcat behavior: +

    +<?xml version="1.0" encoding="UTF-8"?> + +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + version="3.0"> + + <xsl:output method="html" html-version="5.0" + encoding="UTF-8" indent="no" + doctype-system="about:legacy-compat"/> + + <xsl:template match="listing"> + <html> + <head> + <title> + Sample Directory Listing For + <xsl:value-of select="@directory"/> + </title> + <style> + h1 {color : white;background-color : #0086b2;} + h3 {color : white;background-color : #0086b2;} + body {font-family : sans-serif,Arial,Tahoma; + color : black;background-color : white;} + b {color : white;background-color : #0086b2;} + a {color : black;} HR{color : #0086b2;} + table td { padding: 5px; } + </style> + </head> + <body> + <h1>Sample Directory Listing For + <xsl:value-of select="@directory"/> + </h1> + <hr style="height: 1px;" /> + <table style="width: 100%;"> + <tr> + <th style="text-align: left;">Filename</th> + <th style="text-align: center;">Size</th> + <th style="text-align: right;">Last Modified</th> + </tr> + <xsl:apply-templates select="entries"/> + </table> + <xsl:apply-templates select="readme"/> + <hr style="height: 1px;" /> + <h3>Apache Tomcat/</h3> + </body> + </html> + </xsl:template> + + + <xsl:template match="entries"> + <xsl:apply-templates select="entry"/> + </xsl:template> + + <xsl:template match="readme"> + <hr style="height: 1px;" /> + <pre><xsl:apply-templates/></pre> + </xsl:template> + + <xsl:template match="entry"> + <tr> + <td style="text-align: left;"> + <xsl:variable name="urlPath" select="@urlPath"/> + <a href="{$urlPath}"> + <pre><xsl:apply-templates/></pre> + </a> + </td> + <td style="text-align: right;"> + <pre><xsl:value-of select="@size"/></pre> + </td> + <td style="text-align: right;"> + <pre><xsl:value-of select="@date"/></pre> + </td> + </tr> + </xsl:template> + +</xsl:stylesheet> + +
    + +
    +Use web.xml in each individual webapp. See the security section of the +Servlet specification. + +
    + + + +
    diff --git a/webapps/docs/deployer-howto.xml b/webapps/docs/deployer-howto.xml new file mode 100644 index 0000000..ee6454a --- /dev/null +++ b/webapps/docs/deployer-howto.xml @@ -0,0 +1,353 @@ + + + +]> + + + &project; + + + Allistair Crossley + Tomcat Web Application Deployment + + + + +
    + +
    + +
    +

    + Deployment is the term used for the process of installing a web + application (either a 3rd party WAR or your own custom web application) + into the Tomcat server. +

    +

    + Web application deployment may be accomplished in a number of ways + within the Tomcat server. +

    +
      +
    • Statically; the web application is setup before Tomcat is started
    • +
    • + Dynamically; by directly manipulating already deployed web + applications (relying on auto-deployment + feature) or remotely by using the Tomcat Manager web + application +
    • +
    +

    + The Tomcat Manager is a web + application that can be used interactively (via HTML GUI) or + programmatically (via URL-based API) to deploy and manage web + applications. +

    +

    + There are a number of ways to perform deployment that rely on + the Manager web application. Apache Tomcat provides tasks + for Apache Ant build tool. + Apache Tomcat Maven Plugin + project provides integration with Apache Maven. + There is also a tool called the Client Deployer, which can be + used from a command line and provides additional functionality + such as compiling and validating web applications as well as + packaging web application into web application resource (WAR) + files. +

    +
    + +
    +

    + There is no installation required for static deployment of web + applications as this is provided out of the box by Tomcat. Nor is any + installation required for deployment functions with the Tomcat Manager, + although some configuration is required as detailed in the + Tomcat Manager manual. + An installation is however required if you wish + to use the Tomcat Client Deployer (TCD). +

    +

    + The TCD is not packaged with the Tomcat core + distribution, and must therefore be downloaded separately from + the Downloads area. The download is usually labelled + apache-tomcat-.x-deployer. +

    +

    + TCD has prerequisites of Apache Ant 1.6.2+ and a Java installation. + Your environment should define an ANT_HOME environment value pointing to + the root of your Ant installation, and a JAVA_HOME value pointing to + your Java installation. Additionally, you should ensure Ant's ant + command, and the Java javac compiler command run from the command shell + that your operating system provides. +

    +
      +
    1. Download the TCD distribution
    2. +
    3. + The TCD package need not be extracted into any existing Tomcat + installation, it can be extracted to any location. +
    4. +
    5. Read Using the + Tomcat Client Deployer
    6. +
    +
    + +
    +

    + In talking about deployment of web applications, the concept of a + Context is required to be understood. A Context is what Tomcat + calls a web application. +

    +

    + In order to configure a Context within Tomcat a Context Descriptor + is required. A Context Descriptor is simply an XML file that contains + Tomcat related configuration for a Context, e.g naming resources or + session manager configuration. In earlier versions of + Tomcat the content of a Context Descriptor configuration was often stored within + Tomcat's primary configuration file server.xml but this is now + discouraged (although it currently still works). +

    +

    + Context Descriptors not only help Tomcat to know how to configure + Contexts but other tools such as the Tomcat Manager and TCD often use + these Context Descriptors to perform their roles properly. +

    +

    + The locations for Context Descriptors are: +

    +
      +
    1. $CATALINA_BASE/conf/[enginename]/[hostname]/[webappname].xml
    2. +
    3. $CATALINA_BASE/webapps/[webappname]/META-INF/context.xml
    4. +
    +

    + Files in (1) are named [webappname].xml but files in (2) are named + context.xml. If a Context Descriptor is not provided for a Context, + Tomcat configures the Context using default values. +

    +
    + +
    +

    + If you are not interested in using the Tomcat Manager, or TCD, + then you'll need to deploy your web applications + statically to Tomcat, followed by a Tomcat startup. The location you + deploy web applications to for this type of deployment is called the + appBase which is specified per Host. You either copy a + so-called exploded web application, i.e non-compressed, to this + location, or a compressed web application resource .WAR file. +

    +

    + The web applications present in the location specified by the Host's + (default Host is "localhost") appBase attribute (default + appBase is "$CATALINA_BASE/webapps") will be deployed on Tomcat startup + only if the Host's deployOnStartup attribute is "true". +

    +

    + The following deployment sequence will occur on Tomcat startup in that + case: +

    +
      +
    1. Any Context Descriptors will be deployed first.
    2. +
    3. + Exploded web applications not referenced by any Context + Descriptor will then be deployed. If they have an associated + .WAR file in the appBase and it is newer than the exploded web application, + the exploded directory will be removed and the webapp will be + redeployed from the .WAR +
    4. +
    5. .WAR files will be deployed
    6. +
    +
    + +
    +

    + It is possible to deploy web applications to a running Tomcat server. +

    +

    + If the Host autoDeploy attribute is "true", the Host will + attempt to deploy and update web applications dynamically, as needed, + for example if a new .WAR is dropped into the appBase. + For this to work, the Host needs to have background processing + enabled which is the default configuration. +

    + +

    + autoDeploy set to "true" and a running Tomcat allows for: +

    +
      +
    • Deployment of .WAR files copied into the Host appBase.
    • +
    • + Deployment of exploded web applications which are + copied into the Host appBase. +
    • +
    • + Re-deployment of a web application which has already been deployed from + a .WAR when the new .WAR is provided. In this case the exploded + web application is removed, and the .WAR is expanded again. + Note that the explosion will not occur if the Host is configured + so that .WARs are not exploded with a unpackWARs + attribute set to "false", in which case the web application + will be simply redeployed as a compressed archive. +
    • +
    • + Re-loading of a web application if the /WEB-INF/web.xml file (or + any other resource defined as a WatchedResource) is updated. +
    • +
    • + Re-deployment of a web application if the Context Descriptor + file from which the web application has been deployed is + updated. +
    • +
    • + Re-deployment of dependent web applications if the global or + per-host Context Descriptor file used by the web application is + updated. +
    • +
    • + Re-deployment of a web application if a Context Descriptor file (with a + filename corresponding to the Context path of the previously deployed + web application) is added to the + $CATALINA_BASE/conf/[enginename]/[hostname]/ + directory. +
    • +
    • + Undeployment of a web application if its document base (docBase) + is deleted. Note that on Windows, this assumes that anti-locking + features (see Context configuration) are enabled, otherwise it is not + possible to delete the resources of a running web application. +
    • +
    +

    + Note that web application reloading can also be configured in the loader, in which + case loaded classes will be tracked for changes. +

    +
    + +
    +

    + The Tomcat Manager is covered in its own manual page. +

    +
    + +
    +

    + Finally, deployment of web application may be achieved using the + Tomcat Client Deployer. This is a package which can be used to + validate, compile, compress to .WAR, and deploy web applications to + production or development Tomcat servers. It should be noted that this feature + uses the Tomcat Manager and as such the target Tomcat server should be + running. +

    + +

    + It is assumed the user will be familiar with Apache Ant for using the TCD. + Apache Ant is a scripted build tool. The TCD comes pre-packaged with a + build script to use. Only a modest understanding of Apache Ant is + required (installation as listed earlier in this page, and familiarity + with using the operating system command shell and configuring + environment variables). +

    + +

    + The TCD includes Ant tasks, the Jasper page compiler for JSP compilation + before deployment, as well as a task which + validates the web application Context Descriptor. The validator task (class + org.apache.catalina.ant.ValidatorTask) allows only one parameter: + the base path of an exploded web application. +

    + +

    + The TCD uses an exploded web application as input (see the list of the + properties used below). A web application that is programmatically + deployed with the deployer may include a Context Descriptor in + /META-INF/context.xml. +

    + +

    + The TCD includes a ready-to-use Ant script, with the following targets: +

    +
      +
    • + compile (default): Compile and validate the web + application. This can be used standalone, and does not need a running + Tomcat server. The compiled application will only run on the associated + Tomcat X.Y.Z server release, and is not guaranteed to work + on another Tomcat release, as the code generated by Jasper depends on its runtime + component. It should also be noted that this target will also compile + automatically any Java source file located in the + /WEB-INF/classes folder of the web application.
    • +
    • + deploy: Deploy a web application (compiled or not) to + a Tomcat server. +
    • +
    • undeploy: Undeploy a web application
    • +
    • start: Start web application
    • +
    • reload: Reload web application
    • +
    • stop: Stop web application
    • +
    + +

    + In order for the deployment to be configured, create a file + called deployer.properties in the TCD installation + directory root. In this file, add the following name=value pairs per + line: +

    + +

    + Additionally, you will need to ensure that a user has been + setup for the target Tomcat Manager (which TCD uses) otherwise the TCD + will not authenticate with the Tomcat Manager and the deployment will + fail. To do this, see the Tomcat Manager page. +

    + +
      +
    • + build: The build folder used will be, by default, + ${build}/webapp/${path} (${build}, by + default, points to ${basedir}/build). After the end + of the execution of the compile target, the web + application .WAR will be located at + ${build}/webapp/${path}.war. +
    • +
    • + webapp: The directory containing the exploded web application + which will be compiled and validated. By default, the folder is + myapp. +
    • +
    • + path: Deployed context path of the web application, + by default /myapp. +
    • +
    • + url: Absolute URL to the Tomcat Manager web application of a + running Tomcat server, which will be used to deploy and undeploy the + web application. By default, the deployer will attempt to access + a Tomcat instance running on localhost, at + http://localhost:8080/manager/text. +
    • +
    • + username: Tomcat Manager username (user should have a role of + manager-script) +
    • +
    • password: Tomcat Manager password.
    • +
    +
    + + + +
    diff --git a/webapps/docs/developers.xml b/webapps/docs/developers.xml new file mode 100644 index 0000000..f758246 --- /dev/null +++ b/webapps/docs/developers.xml @@ -0,0 +1,79 @@ + + + +]> + + + &project; + + + Remy Maucherat + Yoav Shapira + Tomcat Developers + + + + +
    + +

    + The list indicates the developers' main areas of interest. Feel free to + add to the list :) The developers email addresses are + [login]@apache.org. Please do not contact + developers directly for any support issues (please post to the + tomcat-users mailing list instead, or one of the other support + resources; some organizations and individual consultants also offer + for pay Tomcat support, as listed on the + support and + training page on the Tomcat Wiki). +

    + +
      +
    • Bill Barker (billbarker): Connectors
    • +
    • Costin Manolache (costin): Catalina, Connectors
    • +
    • Filip Hanik (fhanik): Clustering, Release Manager
    • +
    • Jean-Frederic Clere (jfclere): Connectors
    • +
    • Jim Jagielski (jim): Connectors
    • +
    • Konstantin Kolinko (kkolinko): Catalina
    • +
    • Mark Thomas (markt): CGI, SSI, WebDAV, bug fixing
    • +
    • Mladen Turk (mturk): Connectors
    • +
    • Peter Rossbach (pero): Catalina, Clustering, JMX
    • +
    • Rainer Jung (rjung): Catalina, Clustering, Connectors
    • +
    • Remy Maucherat (remm): Catalina, Connectors, Docs
    • +
    • Tim Funk (funkman): Catalina, Docs
    • +
    • Tim Whittington (timw): Connectors
    • +
    + +
    + +
    + +
      +
    • Amy Roh (amyroh): Catalina
    • +
    • Glenn Nielsen (glenn): Catalina, Connectors
    • +
    • Henri Gomez (hgomez): Connectors
    • +
    • Jan Luehe (luehe): Jasper
    • +
    • Jean-Francois Arcand (jfarcand): Catalina
    • +
    • Kin-Man Chung (kinman): Jasper
    • +
    • Yoav Shapira (yoavs): Docs, JMX, Catalina, balancer
    • +
    +
    + + +
    diff --git a/webapps/docs/elapi/index.html b/webapps/docs/elapi/index.html new file mode 100644 index 0000000..00c72fe --- /dev/null +++ b/webapps/docs/elapi/index.html @@ -0,0 +1,34 @@ + + + + + + API docs + + + + +The EL Javadoc is not installed by default. Download and install +the "fulldocs" package to get it. + +You can also access the javadoc online in the Tomcat + +documentation bundle. + + + diff --git a/webapps/docs/graal.xml b/webapps/docs/graal.xml new file mode 100644 index 0000000..9e39ac7 --- /dev/null +++ b/webapps/docs/graal.xml @@ -0,0 +1,229 @@ + + + +]> + + + &project; + + + Ahead of Time compilation support + + + + +
    + +
    + +
    + +

    + Tomcat supports using the GraalVM/Mandrel Native Image tool to produce + a native binary including the container. This documentation page + describes the build process of such an image. +

    + +
    + +
    + +

    + The native image tool is much easier to use with single JARs, as a result + the process will use the Maven shade plugin JAR packaging. The + idea is to produce a single JAR that contains all necessary classes from + Tomcat, the webapps and all additional dependencies. Although Tomcat has + received compatibility fixes to support native images, other libraries + may not be compatible and may require replacement code (the GraalVM + documentation has more details about this). +

    + +

    + Download and install GraalVM or Mandrel. +

    + +

    + Download the Tomcat Stuffed module from + https://github.com/apache/tomcat/tree/10.1.x/modules/stuffed. + For convinience, an env property can be set: + export TOMCAT_STUFFED=/absolute...path...to/stuffed + The build process now requires both Apache Ant and Maven. +

    + +
    + +
    + +

    + Inside the $TOMCAT_STUFFED folder, the directory structure + is the same as for regular Tomcat. The main configuration files + are placed in the conf folder, and if using the default + server.xml the webapps are placed in the webapps + folder. +

    + +

    + All the webapp classes need to be made available to the Maven shade plugin + as well as the compiler during the JSP precompilation step. + Any JARs that are present in /WEB-INF/lib + need to be made available as Maven dependencies. + The webapp-jspc.ant.xml script will copy classes from the + /WEB-INF/classes folder of the webapp + to the target/classes path that Maven uses as the compilation + target, but if any of the JSP sources use them, then they need to be + packaged as JARs instead. +

    + +

    + The first step is to build the shaded Tomcat JAR with all dependencies. + Any JSP in the webapp must all be precompiled and packaged (assuming + that the webapps contains a $WEBAPPNAME webapp): + cd $TOMCAT_STUFFED +mvn package +ant -Dwebapp.name=$WEBAPPNAME -f webapp-jspc.ant.xml + Dependencies for the webapp should now be added to the main + $TOMCAT_STUFFED/pom.xml, + following by building the shaded JAR: + mvn package +

    + +

    + As it is best to avoid using reflection whenever possible with Ahead of + Time compilation, it can be a good idea to generate and compile Tomcat + Embedded code out of the main server.xml configuration as well as the + context.xml files used to configure the contexts. + $JAVA_HOME/bin/java\ + -Dcatalina.base=. -Djava.util.logging.config.file=conf/logging.properties\ + -jar target/tomcat-stuffed-1.0.jar --catalina -generateCode src/main/java + Then stop Tomcat and use the following command to include the generated + embedded code: + mvn package + The rest of the process described here will assume this step was done and + the --catalina -useGeneratedCode arguments are added to the + command lines. If this was not the case, they should be removed. +

    + +
    + +
    + +

    + Native images do not support any form of dynamic classloading or + reflection unless it is defined explicitly in descriptors. Generating + them uses a tracing agent from the GraalVM, and needs additional manual + configuration in some cases. +

    + +

    + Run Tomcat using the GraalVM substrate VM and its trace agent: + $JAVA_HOME/bin/java\ + -agentlib:native-image-agent=config-output-dir=$TOMCAT_STUFFED/target/\ + -Dorg.graalvm.nativeimage.imagecode=agent\ + -Dcatalina.base=. -Djava.util.logging.config.file=conf/logging.properties\ + -jar target/tomcat-stuffed-1.0.jar --catalina -useGeneratedCode +

    + +

    + Now all paths from the webapp that lead to dynamic classloading + (ex: Servlet access, websockets, etc) need to be accessed using a script + that will exercise the webapp. Servlets may be loaded on startup + instead of needing an actual access. Listeners may also be used to load + additional classes on startup. When that is done, Tomcat can be stopped. +

    + +

    + The descriptors have now been generated in the agent output directory. + At this point, further configuration must be made to add items that are + not traced, including: base interfaces, resource bundles, BeanInfo based + reflection, etc. Please refer to the Graal documentation for more + information on this process. +

    + +

    + Even though all classes that are used have to be complied AOT into the + native image, webapps must still be left unchanged, and continue including + all needed classes and JARs in the WEB-INF folder. Although + these classes will not actually be run or loaded, access to them is + required. +

    + +
    + +
    + +

    + If everything has been done properly, the native image can now be built + using the native-image tool. + $JAVA_HOME/bin/native-image --report-unsupported-elements-at-runtime\ + --enable-http --enable-https --enable-url-protocols=http,https,jar,jrt\ + --initialize-at-build-time=org.eclipse.jdt,org.apache.el.parser.SimpleNode,jakarta.servlet.jsp.JspFactory,org.apache.jasper.servlet.JasperInitializer,org.apache.jasper.runtime.JspFactoryImpl\ + -H:+UnlockExperimentalVMOptions\ + -H:+JNI -H:+ReportExceptionStackTraces\ + -H:ConfigurationFileDirectories=$TOMCAT_STUFFED/target/\ + -H:ReflectionConfigurationFiles=$TOMCAT_STUFFED/tomcat-reflection.json\ + -H:ResourceConfigurationFiles=$TOMCAT_STUFFED/tomcat-resource.json\ + -H:JNIConfigurationFiles=$TOMCAT_STUFFED/tomcat-jni.json\ + -jar $TOMCAT_STUFFED/target/tomcat-stuffed-1.0.jar + The additional --static parameter enables static linking of + glibc, zlib and libstd++ in the generated binary. +

    + +

    + Running the native image is then: + ./tomcat-stuffed-1.0 -Dcatalina.base=. -Djava.util.logging.config.file=conf/logging.properties --catalina -useGeneratedCode +

    + +
    + +
    + +

    + Servlets, JSPs, EL, websockets, the Tomcat container, tomcat-native, HTTP/2 + are all supported out of the box in a native image. +

    + +

    + At the time of writing this documentation, JULI is not supported as the + log manager configuration property is not supported by Graal, in addition + to some static initializer problems, and the regular java.util.logging + loggers and implementation should be used instead. +

    + +

    + If using the default server.xml file, some Server listeners have + to be removed from the configuration as they are not compatible with native + images, such as a JMX listener (JMX is unsupported) and leak prevention + listeners (use of internal code that does not exist in Graal). +

    + +

    + Missing items for better Tomcat functionality: +

      +
    • java.util.logging LogManager: Configuration through a system property + is not implemented, so standard java.util.logging must be used instead + of JULI
    • +
    • Static linking configuration: tomcat-native cannot be statically + linked
    • +
    +

    + +
    + + +
    diff --git a/webapps/docs/host-manager-howto.xml b/webapps/docs/host-manager-howto.xml new file mode 100644 index 0000000..63c2ab0 --- /dev/null +++ b/webapps/docs/host-manager-howto.xml @@ -0,0 +1,239 @@ + + + +]> + + &project; + + + Host Manager App -- Text Interface + + + +
    + +
    + +
    +

    + The Tomcat Host Manager application enables you to create, + delete, and otherwise manage virtual hosts within Tomcat. This how-to guide + is best accompanied by the following pieces of documentation: +

    +
      +
    • + Virtual Hosting How-To for more + information about virtual hosting. +
    • +
    • + The Host Container for more information + about the underlying xml configuration of virtual hosts and description + of attributes. +
    • +
    + +

    + The Tomcat Host Manager application is a part of + Tomcat installation, by default available using the following + context: /host-manager. You can use the host manager in the + following ways: +

    + +
      +
    • + Utilizing the graphical user interface, accessible at: + {server}:{port}/host-manager/html. +
    • +
    • + Utilizing a set of minimal HTTP requests suitable for scripting. + You can access this mode at: + {server}:{port}/host-manager/text. +
    • +
    +

    + Both ways enable you to add, remove, start, and stop virtual hosts. Changes + may be persisted by using the persist command. This document + focuses on the text interface. For further information about the graphical + interface, see + Host Manager App -- HTML Interface. +

    +
    + +
    +

    The description below uses $CATALINA_HOME to refer the + base Tomcat directory. It is the directory in which you installed + Tomcat, for example C:\tomcat9, or + /usr/share/tomcat9.

    + +

    + The Host Manager application requires a user with one of the following + roles: +

    + +
      +
    • + admin-gui - use this role for the graphical web interface. +
    • +
    • + admin-script - use this role for the scripting web interface. +
    • +
    + +

    + To enable access to the text interface of the Host Manager application, + either grant your Tomcat user the appropriate role, or create a new one with + the correct role. For example, open + ${CATALINA_BASE}/conf/tomcat-users.xml and enter the following: +

    + ]]> +

    + No further settings is needed. When you now access + {server}:{port}/host-manager/text/${COMMAND},you are able to + log in with the created credentials. For example: + $ curl -u ${USERNAME}:${PASSWORD} http://localhost:8080/host-manager/text/list +OK - Listed hosts +localhost: +

    +

    + If you are using a different realm you will need to add the necessary role + to the appropriate user(s) using the standard user management tools for that + realm. +

    +
    +
    +

    The following commands are supported:

    +
      +
    • list
    • +
    • add
    • +
    • remove
    • +
    • start
    • +
    • stop
    • +
    • persist
    • +
    +

    + In the following subsections, the username and password is assumed to be + test:test. For your environment, use credentials created in the + previous sections. +

    + +

    + Use the list command to see the available virtual hosts on your + Tomcat instance. +

    +

    Example command:

    + curl -u test:test http://localhost:8080/host-manager/text/list +

    Example response:

    + OK - Listed hosts +localhost: +
    + +

    + Use the add command to add a new virtual host. Parameters used + for the add command: +

    +
      +
    • String name: Name of the virtual host. REQUIRED
    • +
    • String aliases: Aliases for your virtual host.
    • +
    • String appBase: Base path for the application that will be + served by this virtual host. Provide relative or absolute path.
    • +
    • Boolean manager: If true, the Manager app is added to the + virtual host. You can access it with the /manager context.
    • +
    • Boolean autoDeploy: If true, Tomcat automatically redeploys + applications placed in the appBase directory.
    • +
    • Boolean deployOnStartup: If true, Tomcat automatically deploys + applications placed in the appBase directory on startup.
    • +
    • Boolean deployXML: If true, the /META-INF/context.xml + file is read and used by Tomcat.
    • +
    • Boolean copyXML: If true, Tomcat copies /META-INF/context.xml + file and uses the original copy regardless of updates to the application's + /META-INF/context.xml file.
    • +
    +

    Example command:

    + +

    Example response:

    + add: Adding host [www.awesomeserver.com] +
    + +

    + Use the remove command to remove a virtual host. Parameters used + for the remove command: +

    +
      +
    • String name: Name of the virtual host to be removed. + REQUIRED
    • +
    +

    Example command:

    + +

    Example response:

    + remove: Removing host [www.awesomeserver.com] +
    + +

    + Use the start command to start a virtual host. Parameters used + for the start command: +

    +
      +
    • String name: Name of the virtual host to be started. + REQUIRED
    • +
    +

    Example command:

    + +

    Example response:

    + OK - Host www.awesomeserver.com started +
    + +

    + Use the stop command to stop a virtual host. Parameters used + for the stop command: +

    +
      +
    • String name: Name of the virtual host to be stopped. + REQUIRED
    • +
    +

    Example command:

    + +

    Example response:

    + OK - Host www.awesomeserver.com stopped +
    + +

    + Use the persist command to persist a virtual host into + server.xml. Parameters used for the persist command: +

    +
      +
    • String name: Name of the virtual host to be persist. + REQUIRED
    • +
    +

    + This functionality is disabled by default. To enable this option, you must + configure the StoreConfigLifecycleListener listener first. + To do so, add the following listener to your server.xml: +

    + ]]> +

    Example command:

    + +

    Example response:

    + OK - Configuration persisted +

    Example manual entry:

    + +]]> +
    +
    + +
    diff --git a/webapps/docs/html-host-manager-howto.xml b/webapps/docs/html-host-manager-howto.xml new file mode 100644 index 0000000..fd23794 --- /dev/null +++ b/webapps/docs/html-host-manager-howto.xml @@ -0,0 +1,212 @@ + + + +]> + + + &project; + + + Host Manager App -- HTML Interface + + +
    + +
    +
    +

    + The Tomcat Host Manager application enables you to create, + delete, and otherwise manage virtual hosts within Tomcat. This how-to guide + is best accompanied by the following pieces of documentation: +

    + + +

    + The Tomcat Host Manager application is a part of + Tomcat installation, by default available using the following + context: /host-manager. You can use the host manager in the + following ways: +

    + +
      +
    • + Utilizing the graphical user interface, accessible at: + {server}:{port}/host-manager/html. +
    • +
    • + Utilizing a set of minimal HTTP requests suitable for scripting. + You can access this mode at: + {server}:{port}/host-manager/text. +
    • +
    +

    + Both ways enable you to add, remove, start, and stop virtual hosts. + Changes may be persisted by using the persist command. This + document focuses on the HTML interface. For further information about the + graphical interface, see + Host Manager App -- Text Interface. +

    +
    + +
    +

    The description below uses $CATALINA_HOME to refer the + base Tomcat directory. It is the directory in which you installed + Tomcat, for example C:\tomcat9, or + /usr/share/tomcat9.

    + +

    + The Host Manager application requires a user with one of the following + roles: +

    + +
      +
    • + admin-gui - use this role for the graphical web interface. +
    • +
    • + admin-script - use this role for the scripting web interface. +
    • +
    + +

    + To enable access to the HTML interface of the Host Manager application, + either grant your Tomcat user the appropriate role, or create a new one with + the correct role. For example, open + ${CATALINA_BASE}/conf/tomcat-users.xml and enter the following: +

    + ]]> +

    + No further settings is needed. When you now access + {server}:{port}/host-manager/html,you are able to + log in with the created credentials. +

    +

    + If you are using a different realm you will need to add the necessary role + to the appropriate user(s) using the standard user management tools for + that realm. +

    +
    + +
    +

    The interface is divided into six sections:

    +
      +
    • Message - Displays success and failure messages.
    • +
    • Host Manager - Provides basic Host Manager operations + , like list and help.
    • +
    • Host name - Provides a list of virtual Host Names and + enables you to operate them.
    • +
    • Add Virtual Host - Enables you to add a new Virtual + Host.
    • +
    • Persist configuration - Enables you to persist your + current Virtual Hosts.
    • +
    • Server Information - Information about the Tomcat + server.
    • +
    +
    + +
    + +

    + Displays information about the success or failure of the last Host Manager + command you performed: +

    +
      +
    • Success: OK is displayed + and may be followed by a success message.
    • +
    • Failure: FAIL + is displayed followed by an error message.
    • +
    +

    + Note that the console of your Tomcat server may reveal more information + about each command. +

    +
    + +
    + +

    The Host Manager section enables you to:

    +
      +
    • List Virtual Hosts - Refresh a list of + currently-configured virtual hosts.
    • +
    • HTML Host Manager Help - A documentation link.
    • +
    • Host Manager Help - A documentation link.
    • +
    • Server Status - A link to the Manager + application. Note that you user must have sufficient permissions to access + the application.
    • +
    +
    + +
    + +

    The Host name section contains a list of currently-configured virtual host + names. It enables you to:

    +
      +
    • View the host names
    • +
    • View the host name aliases
    • +
    • Perform basic commands, that is start, + stop, and remove.
    • +
    +
    + +
    + +

    The Add Virtual Host section enables you to add a virtual host using a + graphical interface. For a description of each property, see the + Host Manager App -- Text Interface + documentation. Note that any configuration added via this interface is + non-persistent.

    +
    + +
    + +

    The Persist Configuration section enables you to persist your current + configuration into the server.xml file.

    + +

    This functionality is disabled by default. To enable this option, you must + configure the StoreConfigLifecycleListener listener first. + To do so, add the following listener to your server.xml:

    + ]]> + +

    After you configure the listener, click All to make your + configuration persistent.

    +
    + +
    +

    + Provides a basic information about the currently-running Tomcat instance, + the JVM, and the underlying operating system. +

    +
    + + +
    diff --git a/webapps/docs/html-manager-howto.xml b/webapps/docs/html-manager-howto.xml new file mode 100644 index 0000000..0b2292f --- /dev/null +++ b/webapps/docs/html-manager-howto.xml @@ -0,0 +1,539 @@ + + + +]> + + + &project; + + + Glenn L. Nielsen + Tomcat Web Application Manager How To + + + + +
    + +
    + +
    + +

    In many production environments it is very useful to have the capability +to manage your web applications without having to shut down and restart +Tomcat. This document is for the HTML web interface to the web application +manager.

    + +

    The interface is divided into six sections:

    +
      +
    • Message - Displays success and failure messages.
    • +
    • Manager - General manager operations like list and + help.
    • +
    • Applications - List of web applications and + commands.
    • +
    • Deploy - Deploying web applications.
    • +
    • Diagnostics - Identifying potential problems.
    • +
    • Server Information - Information about the Tomcat + server.
    • +
    + +
    + +
    + +

    +Displays information about the success or failure of the last web application +manager command you performed. If it succeeded OK is displayed +and may be followed by a success message. If it failed FAIL +is displayed followed by an error message. Common failure messages are +documented below for each command. The complete list of failure messages for +each command can be found in the manager web +application documentation. +

    + +
    + +
    + +

    The Manager section has three links:

    +
      +
    • List Applications - Redisplay a list of web + applications.
    • +
    • HTML Manager Help - A link to this document.
    • +
    • Manager Help - A link to the comprehensive Manager + App HOW TO.
    • +
    + +
    + +
    + +

    The Applications section lists information about all the installed web +applications and provides links for managing them. For each web application +the following is displayed:

    +
      +
    • Path - The web application context path.
    • +
    • Display Name - The display name for the web application + if it has one configured in its "web.xml" file.
    • +
    • Running - Whether the web application is running and + available (true), or not running and unavailable (false).
    • +
    • Sessions - The number of active sessions for remote + users of this web application. The number of sessions is a link which + when submitted displays more details about session usage by the web + application in the Message box.
    • +
    • Commands - Lists all commands which can be performed on + the web application. Only those commands which can be performed will be + listed as a link which can be submitted. No commands can be performed on + the manager web application itself. The following commands can be + performed: +
        +
      • Start - Start a web application which had been + stopped.
      • +
      • Stop - Stop a web application which is currently + running and make it unavailable.
      • +
      • Reload - Reload the web application so that new + ".jar" files in /WEB-INF/lib/ or new classes in + /WEB-INF/classes/ can be used.
      • +
      • Undeploy - Stop and then remove this web + application from the server.
      • +
      +
    • +
    + + + +

    Signal a stopped application to restart, and make itself available again. +Stopping and starting is useful, for example, if the database required by +your application becomes temporarily unavailable. It is usually better to +stop the web application that relies on this database rather than letting +users continuously encounter database exceptions.

    + +

    If this command succeeds, you will see a Message like this:

    +OK - Started application at context path /examples + +

    Otherwise, the Message will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to start the web application. + Check the Tomcat logs for the details.

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character, unless you are + referencing the ROOT web application -- in which case the context path + must be a zero-length string.

      +
    • +
    • No context exists for path /foo +

      There is no deployed application on the context path + that you specified.

      +
    • +
    • No context path was specified +

      + The path parameter is required. +

      +
    • +
    + +
    + + + +

    Signal an existing application to make itself unavailable, but leave it +deployed. Any request that comes in while an application is +stopped will see an HTTP error 404, and this application will show as +"stopped" on a list applications command.

    + +

    If this command succeeds, you will see a Message like this:

    +OK - Stopped application at context path /examples + +

    Otherwise, the Message will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to stop the web application. + Check the Tomcat logs for the details.

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character, unless you are + referencing the ROOT web application -- in which case the context path + must be a zero-length string.

      +
    • +
    • No context exists for path /foo +

      There is no deployed application on the context path + that you specified.

      +
    • +
    • No context path was specified +

      + The path parameter is required. +

      +
    • +
    + +
    + + + +

    Signal an existing application to shut itself down and reload. This can +be useful when the web application context is not reloadable and you have +updated classes or property files in the /WEB-INF/classes +directory or when you have added or updated jar files in the +/WEB-INF/lib directory. +

    +

    NOTE: The /WEB-INF/web.xml +web application configuration file is not checked on a reload; +the previous web.xml configuration is used. +If you have made changes to your web.xml file you must stop +then start the web application. +

    + +

    If this command succeeds, you will see a Message like this:

    + +OK - Reloaded application at context path /examples + + +

    Otherwise, the Message will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to restart the web application. + Check the Tomcat logs for the details.

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character, unless you are + referencing the ROOT web application -- in which case the context path + must be a zero-length string.

      +
    • +
    • No context exists for path /foo +

      There is no deployed application on the context path + that you specified.

      +
    • +
    • No context path was specified +

      The path parameter is required.

      +
    • +
    • Reload not supported on WAR deployed at path /foo +

      Currently, application reloading (to pick up changes to the classes or + web.xml file) is not supported when a web application is + installed directly from a WAR file, which happens when the host is + configured to not unpack WAR files. As it only works when the web + application is installed from an unpacked directory, if you are using + a WAR file, you should undeploy and then deploy + the application again to pick up your changes.

      +
    • +
    + +
    + + + +

    WARNING - This command will delete the +contents of the web application directory and/or ".war" file if it exists within +the appBase directory (typically "webapps") for this virtual host +. The web application temporary work directory is also deleted. If +you simply want to take an application out of service, you should use the +/stop command instead.

    + +

    Signal an existing application to gracefully shut itself down, and then +remove it from Tomcat (which also makes this context path available for +reuse later). This command is the logical opposite of the +/deploy Ant command, and the related deploy features available +in the HTML manager.

    + +

    If this command succeeds, you will see a Message like this:

    +OK - Undeployed application at context path /examples + +

    Otherwise, the Message will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to undeploy the web application. + Check the Tomcat logs for the details.

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character, unless you are + referencing the ROOT web application -- in which case the context path + must be a zero-length string.

      +
    • +
    • No context exists for path /foo +

      There is no deployed application on the context path + that you specified.

      +
    • +
    • No context path was specified + The path parameter is required. +
    • +
    + +
    + +
    + +
    + +

    Web applications can be deployed using files or directories located +on the Tomcat server or you can upload a web application archive (WAR) +file to the server.

    + +

    To install an application, fill in the appropriate fields for the type +of install you want to do and then submit it using the Install +button.

    + + + +

    Deploy and start a new web application, attached to the specified Context +Path: (which must not be in use by any other web application). +This command is the logical opposite of the Undeploy command.

    + +

    There are a number of different ways the deploy command can be used.

    + + + +

    Install a web application directory or ".war" file located on the Tomcat +server. If no Context Path is specified, the directory name or the +war file name without the ".war" extension is used as the path. The +WAR or Directory URL specifies a URL (including the file: +scheme) for either a directory or a web application archive (WAR) file. The +supported syntax for a URL referring to a WAR file is described on the Javadocs +page for the java.net.JarURLConnection class. Use only URLs that +refer to the entire WAR file.

    + +

    In this example the web application located in the directory +C:\path\to\foo on the Tomcat server (running on Windows) +is deployed as the web application context named /footoo.

    +Context Path: /footoo +WAR or Directory URL: file:C:/path/to/foo + + + +

    In this example the ".war" file /path/to/bar.war on the +Tomcat server (running on Unix) is deployed as the web application +context named /bar. Notice that there is no path +parameter so the context path defaults to the name of the web application +archive file without the ".war" extension.

    +WAR or Directory URL: jar:file:/path/to/bar.war!/ + +
    + + + +

    Install a web application directory or ".war" file located in your Host +appBase directory. If no Context Path is specified the directory name +or the war file name without the ".war" extension is used as the path.

    + +

    In this example the web application located in a subdirectory named +foo in the Host appBase directory of the Tomcat server is +deployed as the web application context named /foo. Notice +that there is no path parameter so the context path defaults +to the name of the web application directory.

    +WAR or Directory URL: foo + + +

    In this example the ".war" file bar.war located in your +Host appBase directory on the Tomcat server is deployed as the web +application context named /bartoo.

    +Context Path: /bartoo +WAR or Directory URL: bar.war + +
    + + + +

    If the Host deployXML flag is set to true, you can install a web +application using a Context configuration ".xml" file and an optional +".war" file or web application directory. The Context Path +is not used when installing a web application using a context ".xml" +configuration file.

    + +

    A Context configuration ".xml" file can contain valid XML for a +web application Context just as if it were configured in your +Tomcat server.xml configuration file. Here is an +example for Tomcat running on Windows:

    + +]]> + + +

    Use of the WAR or Directory URL is optional. When used +to select a web application ".war" file or directory it overrides any +docBase configured in the context configuration ".xml" file.

    + +

    Here is an example of installing an application using a Context +configuration ".xml" file for Tomcat running on Windows.

    +XML Configuration file URL: file:C:/path/to/context.xml + + +

    Here is an example of installing an application using a Context +configuration ".xml" file and a web application ".war" file located +on the server (Tomcat running on Unix).

    +XML Configuration file URL: file:/path/to/context.xml +WAR or Directory URL: jar:file:/path/to/bar.war!/ + +
    +
    + + + +

    Upload a WAR file from your local system and install it into the +appBase for your Host. The name of the WAR file without the ".war" +extension is used as the context path name.

    + +

    Use the Browse button to select a WAR file to upload to the +server from your local desktop system.

    + +

    The .WAR file may include Tomcat specific deployment configuration, by +including a Context configuration XML file in +/META-INF/context.xml.

    + +

    Upload of a WAR file could fail for the following reasons:

    +
      +
    • File uploaded must be a .war +

      The upload install will only accept files which have the filename + extension of ".war".

      +
    • +
    • War file already exists on server +

      If a war file of the same name already exists in your Host's + appBase the upload will fail. Either undeploy the existing war file + from your Host's appBase or upload the new war file using a different + name.

      +
    • +
    • File upload failed, no file +

      The file upload failed, no file was received by the server.

      +
    • +
    • Install Upload Failed, Exception: +

      The war file upload or install failed with a Java Exception. + The exception message will be listed.

      +
    • +
    + +
    + + + +

    If the Host is configured with unpackWARs=true and you install a war +file, the war will be unpacked into a directory in your Host appBase +directory.

    + +

    If the application war or directory is deployed in your Host appBase +directory and either the Host is configured with autoDeploy=true the Context +path must match the directory name or war file name without the ".war" +extension.

    + +

    For security when untrusted users can manage web applications, the +Host deployXML flag can be set to false. This prevents untrusted users +from installing web applications using a configuration XML file and +also prevents them from installing application directories or ".war" +files located outside of their Host appBase.

    + +
    + + + +

    If deployment and startup is successful, you will receive a Message +like this:

    +OK - Deployed application at context path /foo + +

    Otherwise, the Message will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Application already exists at path /foo +

      The context paths for all currently running web applications must be + unique. Therefore, you must either undeploy the existing web + application using this context path, or choose a different context path + for the new one.

      +
    • +
    • Document base does not exist or is not a readable directory +

      The URL specified by the WAR or Directory URL: field must + identify a directory on this server that contains the "unpacked" version + of a web application, or the absolute URL of a web application archive + (WAR) file that contains this application. Correct the value entered for + the WAR or Directory URL: field.

      +
    • +
    • Encountered exception +

      An exception was encountered trying to start the new web application. + Check the Tomcat logs for the details, but likely explanations include + problems parsing your /WEB-INF/web.xml file, or missing + classes encountered when initializing application event listeners and + filters.

      +
    • +
    • Invalid application URL was specified +

      The URL for the WAR or Directory URL: field that you specified + was not valid. Such URLs must start with file:, and URLs + for a WAR file must end in ".war".

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character, unless you are + referencing the ROOT web application -- in which case the context path + must be a "/" string.

      +
    • +
    • Context path must match the directory or WAR file name: +

      If the application war or directory is deployed in your Host appBase + directory and either the Host is configured with autoDeploy=true the Context + path must match the directory name or war file name without the ".war" + extension.

      +
    • +
    • Only web applications in the Host web application directory can + be deployed +

      + If the Host deployXML flag is set to false this error will happen + if an attempt is made to install a web application directory or + ".war" file outside of the Host appBase directory. +

    • +
    + +
    +
    + +
    + + + +

    The find leaks diagnostic triggers a full garbage collection. It +should be used with extreme caution on production systems.

    + +

    The find leaks diagnostic attempts to identify web applications that have +caused memory leaks when they were stopped, reloaded or undeployed. Results +should always be confirmed +with a profiler. The diagnostic uses additional functionality provided by the +StandardHost implementation. It will not work if a custom host is used that +does not extend StandardHost.

    + +

    This diagnostic will list context paths for the web applications that were +stopped, reloaded or undeployed, but which classes from the previous runs +are still present in memory, thus being a memory leak. If an application +has been reloaded several times, it may be listed several times.

    + +

    Explicitly triggering a full garbage collection from Java code is documented +to be unreliable. Furthermore, depending on the JVM used, there are options to +disable explicit GC triggering, like -XX:+DisableExplicitGC. +If you want to make sure, that the diagnostics were successfully running a full GC, +you will need to check using tools like GC logging, JConsole or similar.

    + +
    +
    + +
    + +

    This section displays information about Tomcat, the operating system of the +server Tomcat is hosted on, the Java Virtual Machine Tomcat is running in, the +primary host name of the server (may not be the host name used to access Tomcat) +and the primary IP address of the server (may not be the IP address used to +access Tomcat).

    + +
    + + + +
    diff --git a/webapps/docs/images/add.gif b/webapps/docs/images/add.gif new file mode 100644 index 0000000000000000000000000000000000000000..0774d074e5e48291bdacfacb086506e6be117e51 GIT binary patch literal 1037 zcmZ?wbhEHb6k!lyc+SA^9|->a|M&0T|Nno1n4h0tnTz4o;>0&glV!LWO;{KX^ykTP zGrU;!o~1phVTD*cK_!)em>kR#mn&Z^0eO_xlSoe_rhTdU2{GAH%zidEYmsygkrvrz1CG(e()%elNK2f9s9cY)lOA zFHHY+vFYoj2~vCv9}m?1K3M+d$V54A1}%N3>6>p)KJ;Ph^-o+33=fX46z69#)mMIU zX`LuPvyQH3L))$XdH+|R`pLj3#K*_?|NDPF1}|^fSwWhMt)w~)6|3?yj#iYvY3u#U zz+k?5_Z0?4&i}vuGc&jbXzljryOSdOKilYUx!k$z`*f|fsunnK;giFW)5K{fr5eq4b7~gYAGEP z7#KS_xkU;zCM1>#7e(d>$#qP^BWE~tBtO4mJmGb}q literal 0 HcmV?d00001 diff --git a/webapps/docs/images/asf-logo.svg b/webapps/docs/images/asf-logo.svg new file mode 100644 index 0000000..e24cbe5 --- /dev/null +++ b/webapps/docs/images/asf-logo.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/docs/images/code.gif b/webapps/docs/images/code.gif new file mode 100644 index 0000000000000000000000000000000000000000..d27307b5c09467c2e45bbfcd1ff74989eb2e7d65 GIT binary patch literal 394 zcmZ?wbhEHb6k!lySjxcg|NsAg|Na>m7<~Bf;lYCk`uh5YhK2_Y90&`0TwMGlHulx| z^XKp1zrTO~{%OFIr0wCGk>*L_#l_fx07^78uf_y0eALxXeY&ZVS$tf_gsW5<=_ z#~&LR{Wx~~@qq*T{{8>o&~U}u`>TV)%Z7#qKR>_t`1lhio=8gm&d$CqCiY|X>Z2Pr zZ1DH@->~lav}tGl|Nm=XU^H>*6ylK%0}z1x!oarPp`pM-N2)dikZ=q-8iKhfFV&ot-Z?#bsK$q(mDRr`$3(c`=?^VUbBf3LBO?F*3FB z2rH&5?cP|btfH!>&Z(iPC8xdDk6A}sSMT=aD<1m#3{1+}>>KXh^JOqF2nv4n;e)*V JgAa}j)&TJ-upR&a literal 0 HcmV?d00001 diff --git a/webapps/docs/images/cors-flowchart.png b/webapps/docs/images/cors-flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..9abb09d1c7fee7ed73d8d4bbccbff6876aeb378b GIT binary patch literal 86555 zcmZU*WmJ`0*fk7-0@9!eA|Nf&4U!@tNLiG0i`1sOLqHm&LlN+xq;#ir2q+<&Zs~4# zukAU{^S|tMl z|6>s(#|r;Kw}1Xv3ay~;&IMv zL%YnDmwx!tMR%=k)%nIqRpF|HzPFHG|7Wo_Zt=^;V}vZ8k>w_Tn`z;UOy&A|;*61h zaBKN9OM0$>>k&pAL6{!I)2|Ejh>XVK>nw@%yKQ?^nb5yJH{Sdd_6j z;repZIA>W+Az#hW*>QjK_-XBKT8XKuy0f~NPVJw1v(tn1U73p+p-UZX+5jRt{mscb zB_$<%eEj|WeSUOIKKa3gYav+(LJpV64v9Nv&K-e7QrOSpwd*~qva?MFGG&a~Bld&p zoYsGHVqn*L^ZFRzncl4UcF%+Xc~z>JJ%C6mj@^ZqN6dA{n9psz+)B_ISt)i8PoLah zGw^2iM0P|#jig*9{Aln_R&K8OpP#udyHV+B1Ye8jzVe2oD97C;*|8jyYYWN6#D2b!N-pE_4U5w$9ONeX@{!ug9jfr z>)d5P!f!|){}>$Pu^7%Ho*Y$vi<9I8uVl@eDIJBlg^g2zS{hSndHKcOX9?7j4qbiB zlg?sQ#BC`_vwi;eP3=B>`ed}&ljQo%%D;20waFiAf=OmA`hR{R+QrS?{n?C7{c&5a z?W8{GP>#~-%8D%|*|zNezQ$~BL4i18y0O=<$gH1Q;Bdlus>poXW2tDep|a4muD@6619%2@#7f8>a!e3N&(9wiOc@WA^sg4Ed#BBXe?OX-oe4f zL`_9*ZtlmA9}5Z!nDl>rQRS<8GVp6;WGU!6t#ob^#oEkU8gcu@F#8!l#B6Jb`A|;U z1IMoQfM8aNz|(=pG*(ttVz;+`TNGBUi5;FzT^?SBC%&R?){(qrLE#Xnla!M$bt|@V zjjAn@#>RqT(v#xy@=W3~^82&xMhwEEx03oIEo!fS`*_@)Au~}>SHBgbqoad5e7u_a zdg_*oX@A0cJeSk;3Udyf>bQbpqjvE_qqz4Lkf5Hp2d)#pq;|2J2-2+gyRyJ2gCE}iB@BY$Z6W6ew-D#N*Rjg8m~8KF`iq~4-ZAY z`|o^~IEN$Y`&s+xQ;QNq08#N<(vg*+&uLe05eYo7oo(INu&$t?i^sc*?BJk^6hHaL z0_*Ja@Z#ff^ z8jcqb>whiDT=%h}TRz4*Pr>uVNAdP$y-C;t}CsM~mQ=QW2t?hOil_Z|*+}zGy4%nis zf&%OFll@e*cUMw$YhAX-O3WrEgO9j<5GmT#ve+f6g(cKkLy?aK^RwoeC5~~5)eVL; zI~+2`15ylvoDCI7M%5I@G)cziJ<#+OFMo)b%&z(HhrAG{#@o^K%imf)BRD;7azayF zwwik0h7n8Xg2cnAxQB(xu63*Na$&s)ztxdKs0IzXl?d8fH!CP;1A-}Dokp{3f z)IHErdPigANY(nf7$2Lr67jZ%XRKsOt;?mOG!&{($mdU=K0yhptD8u&%euR*BDNpb zr4T^G9Z;fYVzLk}vWB<3w&t+@Fo~#$&WDNhmAImejEt66iiNQc)o7W;$U=9*gm_3I zY>^H2MdJX!)Q6%x{c4J!CNEKfP-l|F$efXk(Oz?j$yoWoy6KZf>_{^K(ea*fPKqeq z-v);hI&;>~F&|j=9|-$0k)F^;@lZE^&4RP)G(Fsy;5F+Lbw9{E#V-|Y+z(RnfHbp? zmBJ<_6hiMy6B$X`45vJt^1f^i#2t7oJ(kVmW89xEmg2K5$u)gnF_CT-Pl4FYNq?>_ z{A8wclm|}n{cK(Vm0vCA=vLI;VS326@;g1;_sIb`HQ&q{8KU-@sfG`?T*~``mib$l z%EP8uBRx&!F3Hi70#cgh&j+?n;l-MV1_peu>(w~*LM=R6OtGo#(XMqFu+qS$HAKs) zG43QWt8Y@Wz$IEJFE4jo9j0wx9QyI&hrVD5`uMdrmzkWAYCo+>)}E3iv+j6yKFz__ zFP&U5cge>IMYc9-Hoaht6?^nT#l~ne*Itr1?|IK1L6j$EB`*$^it7I|*kmvb`)*o+ zXPRC#M{sY}z?b0BAm*(kfeeAfri3(&eQXL|%dhuod8f^;F>TvlFuNi9oH#Zbw78fuDBJt@Pt9%U-tB)(12+km2)hL`TDKsjJl(V|L@o}vAEBT}A6`K4<>%Y-@DTvxq zQ_v;3DiPy-huvOJ%^z8w#_~|t9`X)mOTpXHt*owo&Ci$dOCz`yfI4qf7ulGF{r2;e z3E`cROx`w{AHDONE$Ksket2a=z59|fZmrpAW59U4={%-@kZ6yY=z$C)w%})z3N1Qi zJT~MtW%J4dD#VY+G@g5q$ntKtD7^LVkeid>IT#er)XW)ZuzCu}$$xtgn-yuyI>W%7 z;uP*%v^}GG7isK$mOegLCUz(=O34nVqEbi|W_gl$8E74T zpXC-7dP65nI}X}FQ0hi#8V)k`vif}?dDr5bE5M7!`YKRfpt+ZTO{*MlrrvG;%Q!nR z^EFayVpiev$!c`);9Cyw7Qe7eQ_qyLm1q*HUiEN&YH=K{}i-gfgOv<1$ zwTmyd${=>;<>Q%t%%IJb8$fw5Wv5&;+iGdHzrEJjaCzE5k37kYaiRO$kE2D?p&+Y> zAGfj~Zrl;~7?r``_B?U8broTb&cSPK6F`(HH%gvaJT`4;6ZC`(uM=~# zdU{i{r!!Wz8pBv`sYc7KD4X*eSR$GK#YQ-va&i{?y)Q~)r_dZI%$X!3G@TqBudY~9 zXjg6@Ie2<+)*Z&PYvqg!O9jt1uWS^zQ@1#LGCbZ{IIn1GkFwz-aWK`nw5(?T(k z*G!q~g~agtW)y~dOYOGYlf+xy$sTZzeax0_L6|=!Cot= ziZ(lm<;jqWgiK|BhQy|mTr1wcuifgi7(pqxv9r*fQ@b+Vjzvh#_x9j6{z`M5*SVWt zzE;KHO|p%Pht85^=Xfg+S}wm!&9zPUM1J`cEO#Kmwwj z{J&rRF);8hI@|G||#JO)hgGlN|2y{mbECOwo zyOwxSKIwqUgU^AeN)sA zJZH_r-hTJvqqoRUB=jHt#Wa0RMn0Dhjc#&Yrw#3crVaVEa(;S9I{#kG{otj#x_$WK zn)AV$rsug+PgVOlYwN@owQkv)Uq4Y1mxO?n-E+zv7#pi{T7Tt4tu&A10}SjpQR7_G z&JsjOqfex5R>R$hTX7+xicO5Aoc3V9;bP?S1ZqRt?Im=k3~r;K^HK)wYKKlMt=5}3 z6>@>RSA)XM8UV|Te%T$)`Y|-L+k{2)b9k6;%KdP|F;WxnZB9!DMPJ(*M@j01zhc3lFoJBntjFKUWJIHf}q|vSdV@8)RD;PeYj)FENV@ z(84BW{XIVZSt;dUN#gSOc@`J#klXmi#zuX9Mm*b924u6Z^YG7~c{(-2IOeWf$4eRN zs;Z6-4wawoenXK?6^ZlGk_o&IKX2Z=IoBTPypC4bhw)~wGnScMh)Ca7p!poA1GR`# z7;fYf@<5{E*}avaU%!5BHeA-nE)ci8RCGDoGALP}oYYM_d~y}1Vi{|$Rixo;M`7vX zLhIDUf#7L_urpihQ$%P6pZQ?f!-sJysX_({$)|^#%!{N{-9R@YZ}WHgT}8(mEjGfU zxWboaz_cnrsDqibys;r$x&HfakQuf>FbH>x?f-m1U;C@~k0NSoPFdxbS5~5}5`eo6 zY^neZI|HEOvKr6qGOpr%@@Ss93jDpwepd^Fpfy44Fm%&XnW5-jYn{#K! zyI&Ys*cp%u%6fWwK-&Rm=9-P!%`^vgbv;oWpt{9<503S6o91#RRS=)S{p@Hvv2F9EWZ0l5J3b z>v(smO0J4U8np)&Z!9dF*G9L~DlwSL9#gxv)_WqU=%am4PEMRBUE7mvq7wye($7^^ zlX%UL);P4%EQr-%c4fn*zBJwp*-DJEdfc5`9NLv$=hYnYIFije^-u^=RJV`sJJqE7 zW}QiQybcgODyC@Pe+3YfvVg4EDlIEhN)hPgP`JgZ>rlfOK$LxLCmeCOzi+!TGcWt$ z@|yxR$;J*!qtPn2v`uE&K%DHYgpe}B_NqDBJsSM^vlMV`0g=p1l36-lPbImJ?RzRmsPX20#CKmX?;r zK}n^!nt4`aGbQdpko2g2Vw)t%6_~*4>MG~0C_@?m zU2(OlkH#&_NJ#(uZE_A zk^)c#JL!e0d_0>*(KMm=k^j()Xo<)14pfa4D`#it)yTs@A~?G=7Y3xs6D6hITY4Td zn7nVejA~juFV7E$EG;bPA4v{giQk(Y!;?(w9G5Gq8PONwZ$MtLCm?24-4kt(#H7yx3b(9pPd{WglfWW4=sm= zUN14O5$+}F`Ru6_%s3R7!n8X0J;w2w6OWLai=u84l)agjx4`&TBNE83Te|e63WE+- z6(xm=cS z=&GO9s}1m(22aBCq*O$g4p+#!;pA804@2gT0b5Y=KBv3=$*-8SD>z@TII0`}0$L!` zj0EWuV(as(&vv5Vh|9{#^7HdYc3Xe~v2$gJyY4Vtu{uEh{Ar94GMx@)?Fch~>X zkbq21igbUCyUlzaG@}aY{prMivE7nF`RAY7U%9q?++7BVYq(ytHBzuKKM-R%^jU*B~t&HdI zh`HTp;jOT5SHODnqdpBjGf((uUKo!rpxL zv9Ga?23gXiOJuWl*XHuTW+b%N4{anEANHigv{!0#PZcTLVsN(ALU@eaX~yNWoT{Io zaB$N91w4$nKB8>%cUkvKN;fyRJ@pu=V1|K#ff=?_f>>siVXHuCRQdubqIO^7-tDIQ z#?r`d*z%6E{`ErhiAr6x&mDkl8z4I_40ZbxiXXkxJ2Pwv!uLn+Ud+DZ9M02(E|z+E zFm*8w4Qb19*K#_xA6nl&zK>M!{5I0QUwDMzHE;qdI!(y+1lZE@iVBhT8?;QysjvC* zpr9eJ6y~4k>Ct3pMNo;9d!3&^088w2&^tYIJ=<<)R!ZKRk5y655TD?I?ANGuDZZ4U zoLd_!sjT!;c#a+YGS6rHpD$nRG^2%}!F2;;oCH$wb=DdRWBv5T3687};gjzaZ_EyE-k-JWSSi}TS9f`azqdHMN{ zR?*UMYD%7-=WuZTQ`mEjz8E0#^5V{a3KmhMlR)?y9K+7$8cJh7>w`pAN*ApHS+Pup z1j-tJU6ay={ZgOs6q|dx>mr5TqcH617KR;vl>8hg3}q6Mf@H%Oq+sV)T3X)F_I1b- zI8n$C1Q6cw#Ye#+l5{pG%{((ZJG;92cRa|a9IPa71GLVl2J#9Eorg6GJa#&n4<^Qg ztZi#@TbQU{uw41`XSNyzIzRk2fRs7_*7Hv-gDbp<5s*Z9HxRjJJ~?R?oAxF{ zgLfUTciGMqp`+_Zz3M{9%_zu|pPB~~4o*(V?`j6Q|W=+ z0r1@hm+jet0*;b(cn{le_HQ4}Q=nb|DaS(J8PKH=0}Dld2SwaWT%#X)Dy-#Pq9X0- z0}&D71hKszk3bU?{B><7brI=HbPRGsz&%!;_tC~u3yktGk$7GOp$IX9LaNK3A`P?U zd2E!m!o$NOB`%HtV5k@Bu1kwZI*tea{VGjfSXN!#`7lDrbtelR?i-+m*9g?q`{L{v zsDOTUqM+d(KvVJK`B*}+JtAB4?N2o4!D8)2A!C=&xjj!-KKFgY0L^kgjX}oOtW`z| zJ6bz0%osWis&7+^-6_y(xaeXn{HHeSXEfqn*oTj=E6&#rfZ@X~qcD2x&yJ=#?Hl|a zmiqmKG~#Q&ou_R-{!Yc3_;m9s>K)}o>%&@C>tIKd@94$iq2^I$JS&R z@KN9~e?Kbq_;EWFBsKzxm3cy{2lKZ<1LX(ASOxu|){In(OUi?y6RJ_#C7J(M^}_nI zo$mXbA-t$>LtaY(0tE^H*#a`WaQa?QP|)w+zv1^{vbym}q=Nr>SP9Tg#I%R}#dA(1 z;Okf>r7wATX9wdp?6G2S6y`R?W8X~PbtwRkh85vBA3s%BZy!o)h8m^~Nj{Gr;4Y75 ztuz!sIbLpRZ_oRCLW090RO|10Tgh-Lg0J37cqcu$vb@aId7uczL#3gZGoXW+35jXc z7G`c^BRIpvM6-9;PKGD1*d@pFaR;STkrkt@+C|^P$V+b>`uTJI0l;|)mznaEv6bdD z6V;0M(Uzk{Gc_)sc48_a-T(VAR2ZeQk6*rYnFJEi*|^MH<3y@Gmr5vPr?|sPkAl*v zzuHoOkA1CW5JyFTWUV$s7l!C~0Ohe03vECLWdf((WW8t2cNn-`uwJ30q$IYT>Irxs zjRHy_9rT$g__C@)XfiPU4Io4UB~&ZX6{v9qFdVeae);eF3&Fgo7_hJPzvWW%78NePl<~v>W*Qo%HGlmY@$5E8EUBO^ ze21rMm%b)eun3|}su$+LAP7RCNNAY+*A}2$K{(R=ci=J#3JQ*nW$Y;_DLQH(lls4p z&eLm{TzTX4p@HLx(cbbvk383#pZC@%6xyofUllSBk?}4JnUSaVrU-tm&ARvB!f;YI zR{#6~dAr(SQ)NqhD6vFK)^<<|w5&lzc*|s#gxpFg*|zAi{a2=ei9y`M-eul-z7Vh@A>?*p!p zK=^?;e$G!;wLE=Cn_pg%^Ue4P96D|**$ECM^i%)69SJKvBu@kX9p!Y<+#M2HX#;Xs z3VCwHWX9?+e63W1KC9{;u5SG$ai$OEb(Q-71*2Rf8r^WpF)=Nq{ks%)!vD?Z<{}YU zZC(!D+GSBUA?*gQ^Y`kyK(=Y6bwjek7-;T}F(KLV6Kl{Q>I?sSG>)F^?4GdtOI^Bv z4pQRy==l&RE5Kl7-6`GWUh2k3ph}}$CFvt;6NaRxt|1-aU%uS$yDw#Ih^(}2IPSGc zH4UwC1vZr7_J`$AmxLIV!ahS8@1#3t|JKnhPltcYsA1)D9No3{3wn+(gX;{%FyN)& zHHEle1*}tHJz>|b5Jrv)tLg{w547t{`%;fDPB%rAB9ei`eW{f;j#OqblmqTc#;KdS z`Gw@vRPyvqbad>6m_M=MKEv)}uZYvyXa#y*UEPL$2=U)=Qg4sRVLYOkKR9qQYd@ZW zCVC=l7|r+~JJ_-kAiTYO$@#?mzNo(5(qmK}b%zn1zG?)K5ajs!%uK&UP(^g6I)osA zLFbrsTV;P?HErMFJmt9*5l7CA%2EeMwr4vp{}#w#wgDCy*Y z9)LLVnp+`xrCDk6n&o8e@qA~j0G=he2FPkmAL^Qr< zRY%$q51`IM5;mcwOD^J$uw4oHKef_$5wMSy2tx{2S9xdWimM&mrg)fqN|ghzZzQ4p zDz9bVHotZUTk@OjHW0U+9x`g#A>0Z_?nEK8m$J|0P|>jbM?>m}CuVo4Kjnff@PGRh zF-GQ#&<aRsDNAVphK!p3G3YHrVZ9%^EpF#e1@+suH7RO zhO4ruoGOUO_T!|t{jFtLZaJ2@LlO<|NN47Odcngo13ygAqlop}M6O`Jru#AcZt)

    !gQXrrfU1sAKDKgV`ZSuQ%tle)gfzgVA%k{upPuD!?{{zv6IPRL9=n8glA1i%2AiJA&Z+s$IOHAB`jZnk)%>_S z@GxQ$4o=&GWGZZK0S{1SEl9KmlL2`Yp$}t9BytonaP<^kg&iSljy3KC{HH4WoQ(mQ zDdgbbu+{f?E{~%A!Nz1=%229BFuA@70~jn2Zj|bDY@8_6z@(IHyq(Bx1VaU*;(IsY zct~WQ>yog-SvjD7EJIGlcLyeFz4L^)tTY&>LrJIYKPBy@BhuAMMDNb6~%Y<*?I zu>G)^JyhPp;;U=z*>x(7*l1kqVV=nzF5Aeax^+ zgc`1$z)idX^|IQ|LMXyucwMOET5S zEOkWF!xPw1Pv9y6E+D(J-$@5e-{5_8z;?5kToTq5fYuzO}$;F;d{zrzQGQmc$++^$4KT%yW{B%*H0~*%0R8LOpMKW~U8g z6=Fup4_Xp{|BUQ~mMF7C3uI-JOQeZqBgjMd@88FMU0znEIqi|AB_Uv&x~KadIfF~t1%>U?OWzu#AtCpxUqyOsl@+5X!!Le>dX;9hrYXc z+gEaK$iQ77+mt-2wN`#nz7lG4h zv&o2E$Rp*_S+_T+;8x5Cb?P41%npe%Ti^^z$oua>1i-sNx1sUaLTAq9f|<$C<)pgG1XGCy5T({0!6uBm4v-Uh1zaa4Pir&psY-NOY&94N4%ew>^r9U^x2 z?8o?cjpZ1y&NvteJj487uyxIx_m=Y~nXItdFb0*Lbg}UbzwutXgkIkJobvbKlWcjl z^I)=>f)ZOY_YVqxLxr(}O?fB}E zV1$zS3=SFbA9BiESoxfB(QD&1$M*u+n*y>i=j9HkMzS2yX0Vub9^^lj9Df9rK|kQQLOZTo>auP!kMo{L|flryawn2qrTP{cz0j2 z{leBv3p~~w%Gjm`V+(joICX1Z-xg#0kJT=FpV9j+8A8W&&A&fJB1!a1Ff>o2xcR`T zqoKP8ox_N;ZtwX~#4{V4LJ^J3I9N*};#AqSGnwB)G0lD&B)sPo^0@#rj=uwl7F!DI zQpjxf)?*t%6Yr5$zEnSC*OvSLfQp|#f7V`{tnz{6wss=hHt^}OWBa{a*uuPys@j0q zm@YXKho6r|{!2Xm*>ctf6iE5k%4~Y|wV9cj zIXTyOb104uBmyYSnN&;xYOoEMT#K!6|0UqVaV1_+ic^OV$A@7zATWj7a)94%7RItH zCJfa^aiB}m1qE~~2od8)bjgu`M@(8pU3~<&Jvci&KX*PW0bjuT1Zk<#1x6><2V3jj z*n)!~px)T433C--TE9xwa!s+?`zcqCwWZcj4kHY+a=MxNDIhd)U>RC#rtO?w+qiua z$_1N2_7j8I3XU)C8o+k7mX?<4kE48)5xXrgH5d);QjqQI=vF8)SR0!PCGa??Mmg`8;#C2=e zU+F!DXd}q$&z!@$upktLG2#>c31eu2SLU0q31c7yDt}#1*A9$*X1Qa&x>$lar@w@C zDvRIj43Gc(DSa&|#{$GUfoqpcXepRKY?IkEuVO1!Y7XMH5kvxlo#4JN2sy+6*#TL9 zpI%-7wh3KL)^5IoI4ge(F#Y}J1;##Ag^X}Y!9NktC?IRHuc#L$VFrq7e##a3Y(#T-YW}OTH*XrDekxW*Qj2wA)eL3=Wm^ATw&nr#&e3rn zKmv}_=HxoH+1OL$Up0u~zDur_tNe=>vG${bGa#m`QOGAj`eg^|7KeN$YWDo|lLe2m zALY9$Rvj8{g+a9cB#`m{#?V#&pq#<=^^^R7%@`HpEAP>P)&;N+B$HDPFX;L3{?BH* z75ZQfVJt9tclbU%@m){izdz5s*&KeXl*D5K^|vBMSxaj?PqTz;Gt+$%Mpj_<65kwa zC6o61t7J5-SZPD?dx>OAL$x35$UT`jGPmzc>7v&KCpBNZD1pH{bJjxBQiiv#s;a7X z|D*p^ZM->=9!D?^c@CB?I>V$HLlXu)&;9rERz1RAXO&$FbTdvYaS&6|0nzorgHnZ~ zt+AXlZ9?09oDFA#e5H9u<5L%EIy&L5HAc@6L>r9gmHbMi)|TVRplrN$zO0-f+WmJc zrJ$O_pj*Oz5f4j1d@K1SMTp$P6S^nw_SEw(ZQfufgoKhXayCzlkBh)e6*bj67mz=; zh4aY(2eS9YZpH*L3)}x+^)Z&ujr*%-qBfG?j*$L0Ee9YTnLL2#A+*R=3P-!Ty4u=g zuULw(O=U`l=vJPdoVe^6D#rLn+0?5>LS* zNBl2%B1wp@xA*uGT>WrGsRg)bYAGi3OOn`-y@$`gF zj~I9gI*k|1H!(f#8eCJuj2T`n-i&YvCrT3F1FVPa<{z96c?v;tqpnukguxI2d0k4r zb+h}s8z3I1hc-4g6?RVx=75*A3ZL#1-YjM|oY`1jp5FJh04_jzx_u%7d=>Y(J|4Xe zJsNF!*_KXEiVwF4Ux<~dnk&G2oad&z&8$4XeBv|w)*HT$h4o{)m$~i# zfxVcd`rzmD6cqOf!^znCgLf(|wKgSeS4((e9B`Cr*Xl)YaooNx5#w+9q<1kG2>P4j zCWoB%u( ze90aB0brQEF32o+DKV&BzwRZV@w}o!;azZGO?kN!)LHFg@yoMaaH)KJ3SQ&P5Pi*W zm&&GSZULt;W_ajzLE(G)`gl&^oApKv)=JZ{dDhPIprn~6IYb@BO~#2CFtZ0ig%-1QCv7 zyx0Z`<>TYSCKQde!p(Rk+l|~<)u#RFy#bAA$(U@wFf6z!+pD}YKcY?z)j4n>ea>cp z8xgN;{XY8aeZrh)0*c5b%9u_JJ{(f59T**2rxFz#OU?upA^OuqoK?`4#1sSs1PBfz zzeyJdKd;o%(gLTDCaDtX20o?0^dml{PsH@R&o+C-jfi4x!_k<~N3ikr<oLJd!|D zmL$ku|1r#xk8TQMYY@9egqb{F_I`u0D?o1fl`B%o5F^TbhtHEX)G9a;!`ii6DoRW{ zMOByNW_@iyWWu>@s-TPNmfCZQkd9XSezCV@S^lmy#8Ry{b^qb0rsPDXZm-!Cf2gWT zdNB*sgxwrNx+mm$CGiKC+$NgPec;3CspwI%GLJGd|MG?c+oGU}K_GGlYc%vot#U-b zSTLo9OGu{m#Q{UD&gzr?Be88AFOqQzw@xm~BS&=HB%t;%N164Gt@-+OHH106QP0L0 zpKw@4(0Xf&JyFbEz%^L}V*|8)5UgIQlgG@|hL>iWoS}8AzkCT;&M(KR<1k2_f-FVA z>N$k#jSr}7qocey^}gI>8cX*+`RqNrKUW7?i{?NY1LS+GAAe^LDH*2}(E?72K$$U*2RfA$vbP>SdgQ-g^=Mcjssw>i-up?- zwX>ts0!2xr~@cbYHQDblyh!vt&92xuX7HDhCp9jN*4LuBCE(P0l8{07_s}K!!N~ ztL-q^5Gta7JCfBU*IG0!ZSV#E^1R{FIpHRq7hEV&Q&rt&DK0B}pTPP=QZkl7$2v;x zdNht8XSCpy#}JF6r!@wZZx$4Dj8 zv#ts>nw#%jWRoY-ZxXBLJ5FvQu=x%pJO zvpYX5t1(Zjf}kIDqu{sBvu88kTqXZSEHD!{`jd(QqxWbC1?$m@IK@P+rZR|XZRZ|L zQ4oMOR#)vO=pI5m^YinAE*8jpEBe|dUFj*9PA9;Mw5GJc@`SNPu1hYoxv%EZh(TrA z70_yEYPW)m(w8vlumG5}*uOcSqq$uEE!CPBXES2{?}#0K5)85n{>ejR(QN@Zq!;a% zM>HGfH!!v?rp^`xt(RISYL6FFP}dRMP2LelJwk;m2_a1*5c1nsDiM`0p099$ofKp# zCu(kRkwv+`l$sB{iJ$~*1heDE#?8H785ZB;d|!Y8@4S-gix+c~s?OvhPI)4Jf!Qz% ztFC^qG`NmgVm`$5)%Yp;BL=9>90Y;z6ufk+pztEFEZ^H&4+4l3qb_IO)NO}CnWDd~ z8uCjVRtEnyKDKT(eNe>N!b~6uq34+?$j@I0v=h4y0Hz4khxlRS9erB@6k)2V8Rrj` zR#72rwz^Ub4vouUp3wdF4k!}uZgEDasCLGqjgW^<_p`mY6mksEzWztP`90o=$o4-) zJ284^v$yW3GLJa7q};FoQcE}E*&6%Kj6N5Vpie9I$`WeMo5*^X*~z@3BYbhu)YwNo z+TtApWbw$aUnnCJ&SFR9<;7_V$Z%++Kk(|$tZ19S^+>%(h~A=Mi=u8S>7tz^y!M_L z23no0k0Yr)!DOOHzlN}a&IDFyCZvq$V73Mn`l`TNi$(=y{gv&Lr1l}{( zT32>6*@iE_wz5itOAEJ&FYa4U>T8FB09x^SfG*+wwTjKNNNTCWe1`hh;e<^ahS5N$UKf064dlp zBK(LneF#x5f)k^2W6~sqv!kQf!Kjfk*JDW;neSqUNM5fqm)4P<@c?&VMSR3;S8gQv zFth|0n^gyE-Xhn3K@pbqH76&?N0>q-*3mpu6K_k4w&f2SoP?h${oMCkdZjhapx1A> zOnj`U5G3%9vAmp<0RQE|k2`#VT#F7fZ zkvWNI>SDsqejzZy0#6f)5OY8Fp(r9eaXj4+LAoRgbGL5S?y4a`l^6k^nj`?qNj zqTPFM=&qH7+_*}^0kn?nz>pFW5EUA?;c5f;RkuB>n$0g!{#rl*6U4-H7^dTDn!g7I zw!umR^NfLN>1Mz?>iMts+bC_cskmJt5 z-^$KJfeRav$7=DKu+EIp&2V}j@B3>R^^?V# zC9-2t^bbYrsu%(&W(Ivq+of1+s}W=4$LcimyWhWGO=|Xu?Y0G?jbKN!=;@EX1#AJ{ zpc;QV>D#8`8QvGdOONJY_9$`OsWOiYVR$mLvA*sJ7YnKo4~BYf^6JyL+&uUb#$BjW zQ|>J-sS#al#nwB^i_X$)b;R_rOYO`O^m8L&yT#s=L%7VCc?0}ai|g|QYGE@E3>FwW ztP%cRS?e;{qrQK!`#wx zUjLQ0o}R?~WjGyDl&Y4~VJj9>XW1ahrI|al=z-L0x zEjxJdio$3*8RA{%!Ufsp+7Zu6dJs^HZ7N^cy4k*!4JKMCx9X^heG=DjNXPdA^=8bMeHTjV2`P4x#3><5~OLkgR1nr1k*^F*!qg%ZS%!{t|-Ode288V z!k-b1-5B8BxV_@0#FMQdp#bWhs%#81T)%;F0FF=x;I41;ckd!ONaR0$`s6?2vtoMi zp&T5%PT_e5?01jAlFn{)RO@R63c~L;4eE-62xWcw)?~&O@>;8Lfzt7|s7}pF-+pQs z@6sc% z3adwbX`*yw)=&@6$NIgTG_~td#tlxb@}Aa$0Bf7K6!Na}c~2?i4T@5b7Y70ty&o6r z^y$2&JP(Oui*<3Fn~TQB`}p2sc!hbO-S5dI=?Qr@iJD(bw90@_6Z&l*tEh z1@9)SHEH(!=ia*ZS;yJoNqCBLmjYlcZB4h_k32C_eyX6#f%S-p6X))SkZ|<&DcL8L zq_W^$48o_p(s_q^vDe_H08U%^fSxz$$&ZhZ_?Q8xd_cGtm)Cxe%0}?A>wq_6ucEq1 zKxwed-mjk!E}0nLKQ~ME0epm_nSHr$rTWqscIc2?Hj|>I<5gzI#eY3vZ@hWSer{&% zlvnJ;z*KapC>!Im01W?P;2fuiG1=I!v9h3Rr)KdpB5{lcv*aN&Hz>uAxHq3RTC@9O zYDor71_fPHI5;?X)yT8;=i;Undqd~~`!&qpYKL4T=Vgf;*N8?*64$4j{K;D2LW-w) zUk?yEu=ou!akGsJK)ImRYsLqW;a|I1)k{-Qh)e;QO_p=E9@SL51vDrOS0H1@T>Le@ zhGlJmfKUmv$<)Y*UMb$=PJUiqDLTmpLzCGKRHX+^cbFD?F|Hp%PJ1eHdady?4SVIo zENtjIi3Af(-T>i2584ic6+yRerP^Xct>=if;{>Uzw2#FKOBu)k*NA2-a63_Kut_?SU z-dLL^21g|jH82f|A5qDs>ahgOrWHR^Qg8VUIN62D<#XRi{Iz57^6H!tq{`RS z)D-G9Y*1TKOm`~`7wD{}R=B|4&7<}X=Wsd^VXq5*i#T-Dy`_eW>JYntjw*#Xzv~s- z=Z90D9)pL%RL18^^*?+w#aqEH+8f026f&Hsun3`>Q6^DH;5Y~CZ{a(4e^RSoz`zBvI?h+Lhbc7^iu!u13C35QV!vaG_=Ml6vRL4qT+q4a<^_ zMlgq{epRc3mej{#ZvwW%Lo6^}T!8e;L753FfFUoxU0`R^Y;yALaXRZ8*5HaC^^u#d z<+GiYj)w`+lS#e7cMdym#)DF8=I_-b-We0Yq@5$x`(o>@1T5D99)Tln*KjoZZbY`t z??A_Q7>*JMqeJc$lx77muPC2*E`2kI|gcny5Uyi44@T!%Lk*D6Bu|f%jqnt2QWP^_>3JK9zX8Wc?D`?+|dq60jqN8?jtbfeKYBXpczC34e(YtJp<;% zs@cF4H0++p5Y0j3kn37&Y2@yv>_r@RlJR^e3JwUYM)gUpb4k+ zHj&6@HKLlL0?N>pUi)`o0m)91^lsFxbCe7}*|Lvc_L{h{1TPLBc%no6 zM=ou=PqoaN0%iJiNMH}^|DE-H%c!TJ1q2O?B!NW^jE|p!1s25hVQF40E`8Dk@Qb7F z=G?76a{e)Xtrk02?=V9zUk*Skfoi9~EG2GkORrbfr` zM=Ayw%9#|B<*Kd<+fO&5JM}ATUcBcdksYvty3u|IChDA^ui<{SUz*v4zip8l#y~NB z2UP9+f&#CNswJY0|Lg6Hi(v4nudjz&foJ=L-j4N}2R$i*n{d0Zv}AbS?nJY6hn5L- zgmW;wx#0(l9o$Hw;F5m2enTz))k3vQNQv|2q*=OaP^8L6AxBptjAHC2cbPeyeWd*w z%e{}g^(Mnep$tXax0AvQnkW@JNDD=#qzC`kwf|Q?$lQC#2Vu66SE3D_NCkSGx_Uxd zjl=8{>dA6=1XZPVdF@s+uAcXK+4mM5LCSd~++bzD{z`ki3H44)Z3qe^o9D}pA zcDgw)4=NfQH#EK0V5$taOTo%fZa)H@9-K?^P2&fYHnrRUtI{oECuj}k!MnvSTcddh z#iPs=s+CbNEcZ4m=ka$=uO`P1RVyudFZtB+T2{T67eyY{oo9)FerlIKlw;drB0YnTuHL68t>X8A!@kb z%WxGPn z>8M!Ylz&O?o6c(%v7e)F@o~+`Kh+?w`#xs*3E-&2P~9s*#pc(D<`yvdDDFh~YhL=M zil{*<*^(EbQKyM>tI9JsZfi$8T5^&VYLsycOwY9weZLXWjx=nRfY4s$s zqvhyH*RGJ7TnP|aUfJX!yq1C2qSf+7fyCyymgh}Ej$)=TV9%tqYG;^OE? zQu2P_c<13(m08v6t-c*WR+I?6>9Gn%3eMhT9*TG$MlBw^nDvpevgT$h)@s=z+BU7h zo^;055gLr?IeV9OtCQ@iVZ&K)Y#w0{Re* z%d{`*x(H?Z33TPFfil^cG?ZuQCEDed6xDNm+URh_5G=8XNNt9zmZxK88Fh9G*GHJ< z0H3-PS$G^$Rj(x!fvEz98CpFneSTbF^GXKWAa#-VTcS9ll3~MxRUS(i_5gUrYx8S=8RonJ`(;*-!A<``&9fE)of|Ov< z45`Qrj5H!8AuXXa1|puGB2o@5F{G4&QZtm4oB~oJN%)%f9-BB zE@H$rno|?DYw#nL3of?uFUYX}`hG?D{^WbV`kcCnd8^A9p)i#iy>i1sg0U_Oghbw*GSAW)Xk`1L6=@F_I^Ta}wiz6iRe@mY2}83CNH&w@rl z5{?0#r}^rAq-(7xT^se7L5|n?cL)S1b7ZGXiWt?Qo z+JvmyPKcaFWw2JCBpY_)3KvN$&uf+=SE+_+;2MN~U4wY7!9C6YI50pzJ3g_$5VpSn zjpxv|_#>@Sjt~PMY?>Vu<`TQ0*L4a!?GL}Bo#- zyc%Ek@K3!0yVqt+eo0%b6c?53K&@rY{-@WUmF!%kv>(M~M{$mdbjebs^PR6=bn4P@ zn<#cVYC`3=m8+lA+llceYbP9c8IdDU=WMw*7v^;d489Cd3vh&;1Nl89dDSlyXF(V= zdA0)%MUX(pB$!ILQnoY*lT;PCw`TSN>-w^Ha;05$T0@VJtWUoAld3OZu;08l;n89WsaQPBxUw`il^l`{@ ze%J(37tb5P!F31mO(06pC8}KD9Kg-Qaja+vEcCJdc}!}>iL)SjR7n7-OzRY)VSlEJ z;bxby{ajEF|BV{{o5;S_H~vABXwvr|%c53Yg&}1P*tela+};r9ya3U4EQqd|z-#p| z@Kc;c}-h~@EB)(FEmWAzx8Bhy67wjy&qNozPAQvpRvjDmoGcQ_b|ctWWo19 zz@KB`OlW0KL5w=gW(V(2*zq_g|LcgEY`Y7O7QQwLl<0*DwuF_%MZ*(Wyya0jq&mrK zBM#a{#c6O7L*2s^`b$cJXZm=Kz{SgTRvxrp*c_Y9eY*E**z)YM zkWKV@GHg5>1@Y7`_hdFz%xPkxoHJ=vb}NA#SwdVv2hJbc&XxH+5eIpzmM zG&hrnkHf(Qc~pyu_C#7D=oSU~WnY{T(8nywJL4|VoVQOrMk|Uppc>&>VWY~6D8fV+ z1u1jcj59jBlUuY`6n1Y3T%9Ef!9f9+Et%B(^>Fw{jLlNmEu!u)PCzr;*K9R-5l?2=W<|?% znL_1O-IzQDQ)2JA>Sh0GM=phVw%zbmda34fy4OlGAr%n0HjAdC=!=Y(h=fC`k zUTUF_6xJ!2KiVlYKE5^J4cRmw9P%xKI$Z%jn1-gJ!a*=;QwmjA59^q27X0l8w6HY= zcpPTMIgzGEw&{3-?qUA2z0+mPYco;e7LR%lD0%M1a>Lq_tx;1+yZ))$kp4A8_lq9^ zjRVjVN1XKjCC;o8wZ=`g^Bs!n>gq2GqeSuLP^ivOCeMA;W!>W(tTzaI6GP^`s1CwB zO}N~ac1&~?#xxup(n6Buf4mDfn%)}p#MK05RTQzVoO!NB`K*IU+1V!g2NxoNG&)RI zBFP^0jEBX>157G-=@X}`t;QJDvavQw_r8reh+0a0g6&8!8luJP)qTCFba(CRA*l4J za<}&C6DCp_S!+~WF{9S>rKuUz=qynq)=z9y+0E^P9%T;sjxP0%&l11y(05iXW4B5@ z<1q-CW~DT~_|Ja(9^|=ldiu$`6k$CO?VEwk8B zhM3VkZz_WQ4ZX4=hv9kQMWsD(8{&U%ftFrI`d=+Q+)v+nABPiHlp*|sTFP+e;ZgsK zkbg+M`&bq`_2jZXOG%RQplf!h&Y0~`)3pjV&)TebIS7z4C>l?aP8c_lC?0jj%|jK{ zBV zmYq1{ML~QcZDe~PAQw-$@;&TMK$EN03jJ{!j)@;Ty!hHgtUF4TYKPGz0mEYOoJTiS zdevu4SSmu;Xh%mxYhYfW)Dc%Hv*Tcmmk*WhhxTN-#0hA)_?myV2>>z=FLB=Ap+@La zB0L*b?-i+3%-LVANjsi|Gl*nEZEAQh=H=MmM`5;m-!RRH@qk=p`8$eW>I>4wgQ>}=|TCj!JlKdnX?eG9?(Qu zz$dui4e|&tu1`m+=l>O@-i8Yf4hH>Ba6oBm=b!&h%2}RRsDbFmk^dg21{90$uSt4d z7ZtT<c!epoYz;EXYoq<(Cla@F)%A|cHsvSIDu7n?82SJt-Wq86{uh0$d zXW5@TCvI)@hW*G4oAwiDi3}d6lVfvS*qT2V-p9gUq$(bB-GpsOaqPu$a^o;AXVUCP z&nCOAx)-dhuUPtASp8xR3{%>SC5u44TRgj2kh}dHR~{HHx6jeY05r;|4*%kJU0l8w zlIVb)iUi7Ast*s_*V;Y6{>X-&MTuQq-$Si1z_CCjPy)gLPWFkKhVbM@pJ!Z#NgJf2uHs#WA7we+;3S2@Dyzu%}2b zK^~Ubv{T<+*q*~dX7;ss?BHyAjccNDOA;<;-r5bC@1f(Rg5;lgaW0V`xe$Bx>V+Ri z@GU$OOc4){4_c$}BO(<+kG?<~1<(p8>9E81Fbm{RMK}zJoV2v`_CqE7l@4HgA>K?S z-4De2Ac(+6_dN&pvW)SfmUF();z@{gjnGLd} zZW)E9vy^+MLgisa9c!vNxO*^jP&&qPZ}{E^;UmtXvV*VH2ZsCqV-M!f@-Lk5rN@#UX922(lJLcrD{>UlX#Uj7#!>v#+mafu<>PZ`heZ zoJhXEybG)E!tHyGipz}wcIp&;y_Z1>aEmL>&Q1H9Faibej;tv7m;rrf(g*P`V;Y8> zv`BtXCE$f30@qu6|lRW;MWG7EpRQ?pqirAb&{4x6GAfoFjr8JC|-yntmvmZ)_^-QCvh`~ z%sBsu)tGET*Px4a28cK9fZ!F7H0|@heTH%23SfES6qT5?NxKLl-7Ie}n}B2M@&C8y zt5XH+URhk!Z}u3c-MaJ=NC?V8RT^cK^vZM#gPDB#R8NbtL+=pei|5AhXI+I|$;4PW zXytBUR}fwcc>S)ho&%RIx~>WCxtu!&qtw5}Z(m}@7KOOTI>hDa#fgq~Zi&*1u3fTW z4|7E}`&nj_P)|ui=OW}}j%q@Uh)*@U+8{R1pwy=umv9??UpADCE?|D)Jj^XYPmDd_ zI3jSEdh3~n`pT}mhS3Y%S)Yq~t9sUO;Z?`)?KL}mBFNU^O;E-54P#V3&o?%Lm-_F= z&@rw;h3z7j;g6;t08*GeCz!tjiiHGp9b&>xMGW%<;0j&NJVM!#nR!ej@SW9lcK+VZ zvMX0l5M8c~J)E|Am91iQZ0wCd0?7fckb!)|eX`R1l>OWtJ;+bq8oXsd%wA3uiLEti z?@39o>UAfkz@`DoadrF07ae!`4wfqVk)rmP)hZB!4RLmehj3d8SjI1$!RF?XAjpik z5Uqa#zS$h^EgU|&dVPHu6@D81q@%9c%--Orb{;9Y@$H{V_d)1|^vWAO?!bXMp78Ob z>(w)({t*yTe8Lk#q1uL2$))4Z_05Ug01qc(8_+e_>T5XB^ocZD9o_Q))MC#lB=A}3 zC2Z5mQeTf|r(!sG;5FCGBZESN^mH=goBG0+FMIP}8Zm(11Btk$M^(SW7lXi{&HK+n zJh#+YO00BGAJ5wgA}h}My$}_8=3O56PD3f|CB~D<0=)yJK1k#I8I9Qv!8gdmE&?$n zPIuk*G8<@VfF(8CH~JJ0Ex%&2zKNl0E#OypFry1s;1SX5{6XtYoYq3n<|Y@ z8zG}=+#2(!{sy)F-BPH@Tb*Eng0eL_#jzf+a!{bVkLiF|6)a#OA+QT1uuFrlp5^NS zQa+n?-uqQOGe?7Y_4#hTrqfG#7E%_^R7R=*PhT#*80gSsP&uRHv@2XVzvNg}MD1JM z)*zSivXf}(<>YYH#_@nRr9gFc|5Zr|!lVw|xHAgQdR8+HU@2^$haws%j(MSfz#6Gq zb>$z@Xl^@A|0T|5__0ekUvPLfL*KbLtF?7(a$1?H5ua34ca0y&dd{Q%w_rCEL{SQZ zE=;M*^zk2eeOru}$o{WSb%etx%*ETYkjrlCpFBaLKO^C0Z@tyP>l-ur+>v2612GXl z6n)Z2Z?jFiN#`EtDR~UxY&zyw6<8cRLEsKIq%T`fgZh$YE=&8FmS{w53yq~t$xD04KjGHXplnlH5xjODCw84!A zEWCBFI8S5#bz|MYXJ*dsI1T>CkQa-F=Ro9hWGpy zyr21^XM3T;5AtluL+5{80P>cvvbEKeEhDYNl}d3!GU7{@gV$ihA9jw4nO!6j!2w}m zJ7pL#q{L2xFG%lkQ(wLl0pYyt2z-JALRbI6Z`&CsG{=DY=qGUq=uH>(1-$`!5U#_Y55*NQrQEV>T*%?UK6@@YWe z$wK$jcpk0{>UyHX`GDU0JC}VVo@=J?P}#tO>=#iVg6(fPKLG?%z^ii!%*Q z=mjx6^27rzezmn&m}&D~*a_>d#S&{A0;i)_7gG}nX|boGSv_aba6DchXP35iX$b+i z2H~mx)vF8OFd6*;TP@VH;jm_1nrI-L>MMWzyU=1toRXgTqc_z>k|5*QcknEL5HOs7 z%oRRBAS!p5>t8;myN46-7*Br%Fkk9d2y<#XY%j_76rK=ka3B(-(w?D93wql;}2OY8Rm2y8!&LjRz*^)53tIfi6LPrU$G5#T zP`#1$p*`<6o8>Z*7b;4y5AeawAx;edjL6J8-e=)WaVkbE@)oU1dt5*$$TW){yS4!g zvkJ^+Jp`VF+mYB4v`sJFbFj{YA+)Ebe%($7!yTmh0z05wYi4GiMvw)3DiVFscQx0z z65<5@|M-<|;NKdxC-b}jqmmfxKM!2K5+Q{_U}-M!$v3B=0r~<+3?3dnE7>Zb=UytP zt6`ct+7=n|S8=iKrSTgQ{Pxw$Zs0d(eDOB{Zo!yF#nWzx{*aXi7MmoNn(kN-XMcd- zi|%T#>BRvmSw~&xHoEQun4kd960_6R13x%8 zBaJ8N-cB`k%2t^Ogq+p5Piqmpf2p8VXZ!#1Bv}uv>Vh%wm7=n;_odRT%+Zt%E|FSb z_CYu6lD?oIyi%Wfrus0r27nkhRqnt;2?s;YlPBcaK5w#>3|=P0MAE7>4?BcmxSlvv z=|D|fVc=h_9f3MVE)Xnn*Q$~P6w)To>#o7N*@Jp^9r;BjrhFh_*bpZsl7CtzG75Zp zqoxWg8{onfA6rWyyu6<|OKXqXvP!;UryM56FT(Oe=amezkGxtyEe4sd@KWep_P^es z%c{AjptFJsf_g5QcF>xg{1g48Lq8jBxR4gu7ZH9#1l&rnR*dW7gai4|(3d-+&bC5L zW^~s?{-^4$1+E)00^z*>7=c`V`_+En_cchCad&TJsmQIHfSaJ~fRSAywUh6rU)*;3 z3ULvKV98Xh@2Q#}3h?mwQqiyZ#n?wR&{;papE8T}Nj;R%M4IAmT>i`#(vf9Y9v_7B zVky0BS&hHR5L(~S-)>z@mvQ!sVb0IHQ6RGO0u0mB^@sD@*TD6QP?9tpRciK@F}t~R zRMexoyYRLOi8?n}2&QUn{=*^?q4phlH-HTSx|A)4gKrM51VfQ?)B2WG=+RG#KB`e( z<~HrZoCN)_HFP)MN#9t}NNP6E6GKZ>mh@JKR0aoLgE%bg1vh$7*iKyxgb^S=ZG|(X z^*~hR?sqWOST=^9@>=&sL16-P|Hq2QJiWfwY>*pkYqN4tmk26boD*KZV{cb*QFZ}P zh=pkN(SN-$ZHJD5LVn%S@TeYaTx?8COeFiB^3)mDnmJ zUDS>9G(zEk_G?W)^6cx|Y>-?by|ipk?+OK(OYGrwWBqOlgt@bxMzc0~2S4Cc zn#R)9W@vrk&(DZzssRa`B*9cUl@`Ve)IpT`g~BI`UXTc{(093p?Xvztn(vS?KvM|q zINkn$OZ!1#k_!tE6={uMHC!Ty0{wgO%4n^hr1P%lY?F;w14yj{K;aTlx`Ol~VY3Ip zMYVq_iV=(#!1q!7Pyx)e&BABknXafv51(rxNq*oLE%3E~sfWIV8DXYsaFocg;mqlz zK$LpAC$ALvL79hcEDk({GL0eP)QBd00$G0!l(ZGCyi+72y}BPu{9g9v)~oS;CDH^_ zakw4?D0pkbL?LT>U}z|V1!cz`ZblYF?2{)iag{7mFZe2-B`m@^5#OfV6g7~;KHKyr zWXG*1tGVto&G!ElX@2YX+8?Q{Ob{%4(r{k z7w9&4cbU~Q(Z3~%)-gdZp*Qiu%CX!J1>?etXZ(A+-+$T9?P(USKGC3kLZ~Z6n6blovdnXew>R+LWnjJCrJFO^!eY7lYF9cI5{D!Xpte0a!>Nk z(vw_KS#FQs?xhdbK~31^Y1m$!V`*G9<@{)mGZYLdhFs_^Oix^}n!v|rg~azh15VkQ zmV`;YGrCF0t#~J?y!P7#v;6rpc1&=fiSht3!R&?dF#g8`BOi&&QLH$SIP?Xo@HjzW zL#!CeAL$Lfa9H&FgM9hB5!yCM@qGF*N8qj1itG=*R$kyi--d7zK3p)Xxq|EDu0Q;L zUoZG$JRPudQoMi^2VyPAzCsBMa{fz}0TgsF-{uNvDpMv&VxQgg(zk5*79+s5zj5}} zutZDJRc`-|ljlD@j^^e>rD4`;A6XKq2ymI33fuKpp?QKE7Z`<142VZ?PWS-tt|t? z!&N{{Y<_X>-Q}eWQt{XKuU98Camofs(mk)cF_-*S`Bs z-spkq4X1uoht>FU9m1#vgF zdiBbU@0622PC=~-oVGO=<*I*|rjmCz2Kf2wjybY=CLe@9EQ*gkAtd*a>SH<^$)96z zKydF!_x)?on5Mu*LI1}vT~y%HkZy?j0#1f%D z7M>+@d*83bPAAzaVT~f(qN4Smr4>KRj)=tX?w1`)`-4w2ehC$>2aTrxmrB@ad0wf5 zK15kbsYZXT8m0#Ta01$?{k$h>laA&EgK**)!E$t~ErH_#gjrNic05msb5qMvMWKkvkkz>38KZZe2c6~vP#dgtq^<;(n-`CIpn zhAtcqsKo{wiAJ`uM0JoKdINB>Qf%&IqzNd$va=mC?nVJfLK;xc>2P2-jx*p=k=&0tXTt zx{n&6TqD*VB8Q5+HPugtmbHVuE_GU_n<*;YxKns`nSC2}bg-A&6AIeUoo1Te2v`>7 zH}BI~rEXbPC<_trX;)#rvwsf5%FfMr20COM!gmU6bGeJN^OF;*%yMG?+E>chujb&X z$(7APSDg!9E2>0ln@r|ypX8S1ko;_`OcN9V&i#rKHhhu${<5^jRX9fw1IzlBYRFGT zeYoDlwK;^u?te$q@n5bM&wtj1lfm@7MYK&WRz*d{;mxfeNC-13Nd@P1C5Y!891dW} z$&^e@#O!ORq1=KVUAb=ZuYwDYXAV?M3gbj}WIW2*znnS9xl$rKGEXaZ3O=A>H2hK?`%kv7 zH>YdBWN~Af4Ytw|*i=EGd?Nw?j#C$IUb=L|@WiJx(8#S4m)?vw(O{-ify&VS(p9ZG zDH#VO)6bN)adWzIrm6}rT!f@OYcTCB*~sY7cGVYl!9#)OH9*7A25tju1D3qN3er9h zQz9vm8|7pJ=AP$!0QDeAhU?Iq)~9;A9FD>9S&BO+;#n1SLPR0ze-o-J5p|4YtI-U#qw zq=Op|Mn18MHL5azaA-g!mch90;V-qN;K@LDxJb??EOqOUJu)DO|1~(omPg9)AcQ=< z^dM|MsJQaQi*9O8xIaf&$O3}VS2E#{ zP&r;E$+|8{Y`;IXQ}I8{0A8B8;KfI#PJqqCw&CXM^ty1U^44`c)cr3Wqu=Y6YQRb! zz?g1>2p0+fU+Sv#9%$Rb@2iL3mwldQno6dmp`ih`+cLg@*-PL4i`-3V=sXw8xA*z% zl3I*O&pudrmY{m5RHYzW@4bD%4|fry!Rg}#zYAaY4&4$A*&4l?zE^XRjk&KM&|X`A zwU!(cPr<@_LeviLiGt$~A@8n+{XA;`b|UMqbkCne|5x@8Tx{Uhya8N)aHVVbi4#^( zBlVg11lAlDvi_3i&qu3o2pOqWb%J8W{LHV9{tL?uupXcv69``_#m`^j&})SwBxt$l zL*OhUFWuePjyYb(>^~eWcChprw*bERA+!z({Aig${PE{=ZeyRV!rnJtA2g^`&}3ho zhuQe~?9Ece^&=XW?bo+w?n3*KFaP*2nhD%Wng{ae9WdHeebwHDxp=yL(gk`g(&uPQ z6M?=8x_)oqSjV3rrUEUt_4&{{haGnMp03PH3Q=2{Kg#~7DG8%8kP1o*-q}C`0Tq8I zw7a{b#Db};A**LtFro8{iDzo^FYJ}?qt+kDEFj3apaUQfI7C&6{AEG(ufLS@cdB$^ zLFaBmv{fdPDw`%soF)LC+M6Sb3}LMl&Q4izx8fMos$!W;_u-+G6h7pv$?F2&TtSrj z+%L&q^qmU*{kI^zw(wppW^e~Pf{iYv7!oZR$>(#{{M7}fGx*QH^?iEHP`!Z3hI?Ck zolpz{PeoU1185UHp)mCWg1lYM-B$4hQ{gV=mEW>&sNq8~#}6F#c^IoRun}CkbRT{m zrGtGrj2XDvTjLEjPNtjNgU z7y584D^AUEQ1JkJ(hME_l3TeR~-1Esi39FH!Wlki*`GCBRvP3@3 zqSfKzo4ZHVsyJofM`SU5#L$?Ad#|J+Ks03r_ za2fxVHQTLYkpJUySBA9Uv#tk$N~{U?BEW)IW03Kk((#G+GZT5#FS`nhoO48MK?zdt zMYP#6Z;jw?zzjm@7B0Y-BDleRD2N2T$@2jFSD`q_QJ;P$LJ(iy(oM?yJh-PVCjUk% z6D%7XCd(%bHUiJ!0ye0hS~hL?fn^;MTll*u@p=rm$O5sxey8z#6@6@f+?7lcbE!i< zPP6e63oW2nqN}^F$E4^NokbRwFe;_=^|i<>jFECLtW|2CUs$8Rm&_UT$X9j?J?| zc&>AXWoUxz!v5Y@n53oO%7Q3=bTl1oTRIZCbWlc3^#I|Yo#W#$VWx!Hhcb z9)t%Dd;eSku>NC$81pZtt;@!(OP4Pr(?RYXC5Ym}>93w-T~oDPOpmv_2)b9TN7M@> z6B^e)`~~$lDEbD2_7*__@dR+bYhk3WZs&1YA>vt9kg|Jy)&Za$YPWa>qH`T3TJqh; zZ^Mn7(>F#FnreWiIsPCn3;v-EY^+a!jQ{FYf6nxR2fWZop*o4lw(Tn*q!P*wrF&(7 z=~eIvfe_Rta~#aQAEB4>=GyxL0DXD_%N#0T+V|p487LTLv_eyWf?gEQdM<{a^BYeZ zgQAr+Rf<tbRLdl3R(a(M1;!=Zmo zuIx0cfT%VEe70Q%Kv08A@VRHZ#bVfZW$1^b>dp!or!}jDG7|D4~l#d(~N%MbToHW zNpZ2_G%&=~#WXW)=S7IAtT@bSOA@t?Wi@G1(eR3Xa6i?BtM%EJXrvw*R`=qX>Apj0 zor(<^n|8TU_4vZ!124mnkWwAZr(+vH+jrkgtWiM^ekceTs*)`xX*%^24D(?6N zpP!CNRae0=Z2jn^3-IyrQGIx-zBE7OFhbV3;xJr&;oIpL$x6S`e7Dx0P@E2fWkXwk zh_PD6NPnPh;M_S*uXIZelR630p1}5Z+{uw$F{F1dwn(P|2P~qB<)O9J&!YUf*xoir zC^Zhz?M;^ZE@a^Re)vg?y6g81a>%H(6p+h;JF9BXrIUP1V@&nKKY-Q($tI+<`-lx3sloq3p{RXH+L9!w}U z;3z$`ZBO&+6W3#(+)FmmX|z=#tcUMV=2qU?S{?J}XgW>n5EpT_H#(1Gjne6h$fG!> z!OgZu&3`}tGGpmV|bs;)kZ~#U9*+{Xs^(l>@$7l8YnqLv2L=0OZWT~#yAG2?PK zs&4f&3x9!=HX+7`w1inzvG<ojf@`xAsFbWboSE9l<4}AM{hsU*)75R^)Y^vMiq=%~&sAz1jx55{u8G!E< z>I4&XC+eqoAB)5DGC!6Q12NbCZ6}IuMRRk{DD;<(Kr<3vY_q1cku)Jk$8Zn&J5mAR z9hPhh^S-mi2cCf({B=l!#uJdr^IC^?@8mT*8vhhafg3DD-2ZwRW1PY221kH!jP4k2 z4f@huHFPjIkd0K5U+w1RUlmJrkZ~#^d|XYg49qK_FgfErtp{mqmWf?vrSS|YYP29#yprMKW zy}ThMr&d@hP4X1qCVBjLr-DtOAKAL&v>Pv-Wxfa=9oi^hlOI&h;VEA4bCt#Of<2Hc zxDFqEYt*AtG2qYY@wG#{JumC2mHayomA(sZP~r@yOk86fW8rKcikLwMZ3JiNOhjg4 z6DUw(?b8_}u8dv__squhroT5HH00H#z~i0%=?_-RuKw zg!aqz>&=>BhYgFW^s`n8y`_m5$+&BkL+{9fNbV8W}ebH^kT zhVXWytTjFlA@9?3$DZ`IY2ZE_J-+mPTgR~@{b^X@eoB!G|1E>X*Fx#u+MMVP1~e77 zGiwZ^DEX2W(+H{4PN{B6`3rJ5@I_2&F~JdAyooh8gI*s<9jcRRPdo)sayw0}M_9r$ z;=Pj9izpMqU(y%&xh4e57@WOxwRqG5nqkG%<85l897U(>J)`mOcnHwhM$Nhr!o9PD z9la(#cIMVjf1+@6lfoR)&-v>;Wk#hVDyMV6O&MPy*Cbow@0$%?c1a3ci7VP}Qc0@7 z&r{O^k&D35Si&QijwKKFypzz8{A~>X*zkebiG7+#9tzE}tdl#5B3$|d$pYE;$er%I zSGsL~VlI?b%4%F0gMa6#+&r5ou==ow3Q4EP7AgS-HcS%|uwQCWhPkMumk>ekl@5*D z4gCsHQZBOg4gHz?GsfX* zWq_aI+2Tu;rDl7q;TBb+EK2j#Bdk!XZ)#QWK2w(4JkbgIG`f~hd`NtH~+K86=t72m+cj#a&*R^XxN2)IR3X;$p>9 zbgXLqAu+zKhfRf(?M#*|G}mHAVMu9TZ`}wF8?zFefEx(cccap{_vVy;2V^(0jZ(!pcYHvkuPwH7y10ykk=Z>(D#Q;*4Wcl?M>{K}(R3 zH&~f_1~8(3epeQj6&hki4GrT_xmmZ~P4d~Kv`keAfKGZkd~D1?HHCKN@Z`tJ>o1~7 z+21Ab?a;s5-0-FH3uqm3O<9ZN8XOvO3u75veh~yvg(Yk-wUC$ zA;MTDr7p%k;4Fy#cAouGa4A32_%pWz4Rm7s?YrW()-A8=Tc)yrFbXsFUVwEN0{oRg zhKG>6^l3N(O1Ch~i^>_kfrb8@%Ml#RTdjHp^s6ll z_SImOk#VqLDFP?lx-^mhw+)Wrhs-Z6Tue7qdM-&Vz*Sm{bnG6?A=7-z1Ng(BLONVc&b3~W zO08dQ4r$w=kqwZ!N_%>Fb(BhbqS3moA%hocadxuU6)`|F!75)#`8EB88D?bw)u6_T z{-V9SZM~1WQEe_r2XD>^9;U_>s)*d(w;p_?Hbqi7a98+i6-(bv7xgjsca+7hy{jZP zDjJ#^L5JRtZ2`*zbU}5{#o0XY<~m=Ez}xxGzy!v!WsQ;j9nmPh+jAJTDIMo;tIYg| z&0Gr;Nj-k1QsBq_g1fwS7MCvq-Hdb}#Oskr+w#BJqb={_450c-;I;*mq( zeU%mn7s`%lg=*Mo^EwFYpk^?_9?Y_|LRMoWP8q;zIhk(BG;zqpN&n{V@y!8OAxvVg zS1!4{1i8VODGTb(bkdd?kDxVys$?=d6z#1&?REzr+@^^Ppb;88Xu8eg)^(xj3IC%^ z>K2lN!grf@Ke);6zJHWilvEZuQ%1X&wV4F1x6ycXwAjtgq?=Grn(xHjp`<=O<}fRb zj06b)b^73h6R08B?`OL7hxa9!2GKzzl=BxSDT`Z){u|)gkwPC>qb=PO0W4mppAwMw$dlC3Z721Q3n7E52{b!>83~Lb2`DzpUkjy!O zRl+{^(+N+E_PO?sP{|?~hP5l3(bF%B|3~IlW?MGZ$bQt-4K^@FBexVftg}ZW%zVV_ z?a70aKM1x`2wSbMAFlHX%xViaBfKvqEjC>WIogsd8^?GRtsHFe`89^2#vEeG)(j|~ zy>aO0pEv$=)T$}i#d0Rdr$(IrY4$1nxTo@O<6}(H{ZwqUCJ>p5SX6#X0GGJl)c|g= z^irLpF>C7Fl9@K^5uw06Qy!Y2L93jM%G|;EH@Nuk<5+s!eOTr4apR)YgufW*CYzLZ zcNYVy85%vO2;?*Y(-Ke##Lq}Omr-tg978#) zT0PxuQ2nZuvB>Rhu6<4+q;8tjm#gHIF<2YnLt^gyMx-w(gS}NaxTP;Cp7a@7vWNe3 zqt(%i7i|XRrR1FDZI9fFDOxO7!caMx{-kKqi>?m}3Y2td@rb@smRsx+Ui9$cN9lC= z@TV`ixBVNjJ4q}YZOC(>YTS@oL&-aS7rFZ(jq$F4j`6Di2W~gHca@R&KnS4E-=Q=5 z{F-!$q<1rE?6|TUw9j=sKR`KBPI%#5n(ws!bz0RI?_39#a;4nYCrESZt$G6DfcI}Q z@J239gKJ8#E$ww@@WIxI?2a^djs$_yCR2Drmfy7w+@m8=iL^!ilxp8;AZ~Pot!F#? zUu=7MYaXZu2tgO9R3o2VV;8%@(Zq!w2vKEd`L7crR1Ixt=F)6_&cD~KZ;&^x2-&^nv97;P4MEwsd2 z!z9PQz_)wo#mhJ;BYws#nV`b#k#%K$jR$gh!kGGTyTb`58tA!KJE(VMwWLQhaCUA& zZk)ZF%T)LSIZX}ztGfqG-uw4_7GM-|1n9JW>r+onnT;Jzus{Zi@U20-&0saLAfpDw zTUnOrU$yNAv-8A}zY_*lj&a+&NOSHUu3~2QL}j+vtQFD(y!F3vZMB&nS?dq=ZLBG~ zQ$U-jqI%Ulz(sEem_{@$rvg@pV6Ga$*!_?3h{uNJ-n}(|q9RenDezV8XU|`h`T84R zdV&p3XFad(GmZf!PR1wa6JzFxjz>iGZA>t6&DYD_$9ml!85*~zB~~3_kXLLld@W z1swaegd`YYTke~kB6g7rbqw1H(YX*{t25>QfAReE03jSc*g4p5Jn?3`VDKZ%n=ySR z+rqHa1ReiN_VU8nc{}KEk0{Qtg)hqkT&s0b^gHp_G|6hVcru{keO@+=Ye`bmA~p`A zBiv%gF?AI02(wl|JK&R~fxJc?>fgV4x4kKsFmfS)nAr4RO{~>NjH@v8m*<%gDR9WjBJHh2)vs*8tDzuA9(P1v_kVvV%jLvB6+vB0f8)-9e=26`0Dh);MBZr%ixy8#O zI%zA!9Wt7Cw#mFk-99Mkrkg=~mgqo6)5VtiUm4$6=8!nAGzQsRIjeo8(UZCbV^bzv zXtwgW`?8#qD}m`Twvm0x;z|jVkFgY3K`gOsUudK%z_W1~-r6LNmDRxYM3-oT_SwY{T?WKqC}f$z6Q(x0%LTD>!stdV4so+MS{B z&sBT+4^$~KlH|_+)z@e0AczBbV|xlk4!s-KuK70-2WJ<#5m^+coWh9k;){`siXm45 zcjJ#pN_jaK50jBUyb#niRj3+(YN5k=W&rbk6hLCV&(un#U{{IhxpDEvXH=k8HcYby zBMW;)Sz*^j4J+T#%h$@;Z5}5G+qnS4u3JBncAwOlbI~iK1Nbk4%wOL!-{zX>1%&n2 zB7ngex90?Iz~m&IoMY_>N|`<=XMPk{N#DI1m_uRg_jA)dClyk^r!CM%hxIPDz{@o1 zubG0SJB!$Px%Q=HG(HAnw{7l_JbC@im2JX^ zwKz;Eri}>7TA?{*&K3_Y7vr}AF*pVYYJ;|beIsTY1RWu%xR+gNGWPuxpb|bICyz1Y zPSJcIa=R}aIagSmunY6MGX;(o=H`xCt=s}Qg>ljhIq!)f{=25Qd{C}`0=7ndZ{8g$ z{^zk$IUaLdZk1GC-f8qrDJ><5ndx#y%4e%;58GtQWb7p$`+UMsU;apQcngE?XkhN( zD<^{MB+ch?_McwEvJ6wT+qv{SI2J4v9}K51o&EdS;}y; zlCSE|gaWeom?XUF%5EuZq1sVU8gg&05lQ#)>AUe>cDkp1u1iU<#lJ|RNnXE?@OJaF z3u4l>TPeH;W5%4`?r=$Mr8Hl=Fnh$HRvhbT^FEEe;#}bTrlAE+Pa#i0`F@q~iyi*Y z0bCwqhqq-KwHBxRVbi4WS%C~|sF;Jvw!#H?S9RP;7B&N%h23;je9os2mgv|m*JzD4 z+^KBZiw-#-;!Y$BG&6K6hTB-qKx0``OVD)K`-DVYLdK0#h89GgCu-7(m z(jl$$d&jcDq4GY0oEiurf$Ot|-Ig%HxOYgHM#T~Qu?TH3bL!q&QMY!FER_v!ZC#x- zBZVTy4*0%H^hRl@?0Xn42@mGd4VPV=QqejW)}p4@mbiRqS-P6sgn?62(duU)p;1-+ z9tK0R=B@zR4r~S~Q=nMueW=AZ+5q$_lZoMmg)95lY~Yf@<6V`f*@eE=u1U!V#<`7Z(4QBOUziwUy>~i zE4+Ia{F3(s=ZRn*#X3kF4NrrtuzUlfer%mysCWhLnyIe>_p_QCF>0WH9sI7KgyROWtX`i4^96=`$c}0D0<{6*7YZipClwv%T-}wb*qiYAT2Z>$R|ij zq{X-8Sh8CB<@sfgdv`T8hIluvQG}Ab2yf$USDIsN*4~Pv-`NbG!IL-TRRiemb)@msv=TgC>l_@I0S2pF z<>;&7du_($9j7xe95JnjsyOptr1%7iJi{|nEsyIe2)Umxfbs43MAWF*yFQsw81kbx z%QT>Zmo^WD)IG2X8S4yU{C?y+G<^#@ZJIc4-4wWf8JDW5iS9~tjKdJ(d8tM?wE~;e zi9_^W3G^=3bZE{iDs>^IQ*|x`N_dUl0J=)rzCJ6!i}}U5%f*(db3WUGU z@vj-Ig*%dSx0@vJWSag$z3hyK@tjLX1NK|FTe?!Q5cz<^rT*KHjsFBGo5QmQr^QQr zGE*>-Q8uwGx4R1(`}0w7QTnhq0A!7u(5^I$Oifd`vXpSAUi(>=Z?)P#eH=j1-}$O0 zpCuMz$NBE%kE{pss??Cl<_Cb!dKA^iGyRp#jn+o$@#xfvl~g+O@+qP%`*Qa_Yx^kY z!S&O(Q?Ya%FwTt0Q&W+ZzuA9yl#HTQV*A_y*xa8CE2oLbHr2kn(NxoLNoce?@~h-) zQmK$kN3JiuV5QYPBamOmh9Nlb*vh_h;5b75Ju6z=m1U7wzU&0)?*S@mPQ?C6@88W) zxgseP#W}am8PZ13Rx#jYjB^tCm{#OFWuX^z_pO0op=_Vs0FGg)aXH9Qya2mlFz(D= z&>94we^@TlppyhkFHD9sXv@s>4G5^a{%T44iL`dHw03F$@khRNH)m)e1HUB`%E8;k zPN{h~wOqz1$6t$3OK*B?|4T{?<$ny*m9vBzqj4M^W6QFjKr4;qk*P#K)e*m>aH9vV z`ZU8s{hB*HN+tHM9Tr4tK<<=zlx@@>0g53LBq12|&B&pI!MNe9x%#Y;qZMsCk$i_~ z=u{%^dk{!+63ayAF00*blwWRT&Ma(DoX^LBs{jy%fzOijM3APHPx2&ExDEOVQVSE$=yn3klpv5# zAac-LE28@j3hU3Wg`Lry6ULgKzw$Memz4A|MuzNcz0Xs|SjS_zi%ln#9Pw7RH4bsN*~(Wc+Gq?JH~hAQy(9NjX2*Oggqp17y==6=MmeK2N! z_s6fGf|SW}FarxNZUDSfQ{XD=hPSJv4F!qCooOXtQRjA>akoaxhL74h^Pl^o(vk+V zrRkGEmkI+v4%?i(1sHBnPH6}Qz;{4ULI>978=g^tU^Y};n`*&aHVL;-;C0s@jFaw4 z6HU*usi>^?vl@te0zPCHLKz?p%3;ZXk7=k2U4Ob5hTZDwe}d(i3JHXu8Hi3BHu}^> zUpt<^E~D~!J-j8!vO=&uQA8o82dWJ>uKgdjzB?Z4wvF2?d&`Wn_a-YTBU_}9k(tW6 zk{Q{1WbcuXvNAIgaoIvj85y|{$(BUcd;HXWKhN{N@BPR9`P}yhzjdC+d5rH!km_7c z7Yg*9baJ%ycf)*@5>yvsnRnG=tXe_LFKC@r6Fg>N7pD zO|~N%8p9AI*`KD}h+S6;{<|4zqS$5#-k1ZoJ199Sj{6o?^_T^@7u{rxgIp-Q+4aA$ z(l87#ROR^Kqt7K{XUmZRRTu__bAcdj45n4Qk87g_U3hsb+X3*L)`60gE@ z!-@_c)LPE*;zouC%Y^`5mZlH(`BNasD!*It`6Fw*qxljppa>TL6(qGE4}VRDop0Pe zJ-9^Eb}KCk2mCO;jWB{N2|!t_qd*)&E*mWkxdFmq_5nM)(7m@)>3;y5Ss)WMr(Rj# z+?=Bs*pWW1-%mROk^--E=5XQ7GNho`dnw%QC4QHbHxv1c19f*M3jAS0Lo!Jc97Sj2 zoxe!XPv^-|zv?VAX}5es9;>z3%R>0e-*ds;u>ARZ;ms5_7@Qz$19$)vUTl91gc8z% zP=?p9Q$ZjDg~XnOpfl`!{Iah@!mJM;4o9<`{|!Qh0%eH516BU?x1TUQx4_NunrBF{ zafoj18N`8PE+wV%S_KM3ZzR$ag%VDUTf7=2N=F$U(X=U}vm^Fp;AugXJ8B;&;0QEy z;8fefADdHkkz0NADYt(?=bcc**=jD+pG(~;(It*@p7!(T1j26bR!k>J6O7DW}T*1e-eAa7ofxdj=a#wZ%hL# z1HokBK}-b5ZOuOx2Up#9H*Kl!y;y3uDK%70Gj#PN*4;+Oupw|e)l$JTvt=0tU1Fe1 z@9RDiho8 zRh#x2eA>I>6-4(6MIN}UBlTYQL6;L)wccRcF?chV7{dkNr%XZzn=}{= zG<=)a=BGScam~p0;OB_cS?%%bB3Ca!SEX(P(o8`slM=sIm@}Ax6EAMFyFv)Wfz^A- z`VW0v+CI(X+2Fq@S|0OyL9p*D92*@~OZ*D02(aC7^#Av{<xG8ar-s&&+Q_D7@=6AW;b10P=ITi8Ok%R)(rC?;LRGSD{f5;~hHNbX$M)!J;t5%v^n zxcMKhw7$aMdt)%IU-AA8A_2ia$)0yF+RS!o5xlr}GRyblb3I>vUlR4Vz{{7jYlfYO zot+uP3nN&Flf)b+ZNqc6Ugx!$Kzw`1(Bfa|IPz_&EzgKW-jjkHCS?^I*lpbv1raWo zE`bP8s&F=IZ8?nhr4M-LEwEU3TN{U~M$2=k-NH*MiHhV=us276+_}s8wb_7YU^}Ho z)=P1I@pvf#2^Nw~IACk-@hoZxMDMEj0(8};!V>50kPWTaZP8(sTfoCqD6|J41KuqR zh1M>)!-?yniQCp%^;f0(4Wc>T)zq>vj6VekL@dSqRba<~=NJZ3olFNg#LZTKUY{&Q zz<4HadMV3{LxbV(n++nUGc+fEWbaYlDnI(gSBgoe=S(XeL5+;)beELipbf1_lqlCA zwDoT7`bKM~HQ&ihls~gk+A&X|aKVm?V}iXa%Nsf%RVy}d)>T@l$pE#U%fYd)|M>dH86dh#dS>=b$Il&-@*(yjn&3| zRe5<<0MaR~VrxmT*^f+*LtVxhICuvK$$u_2%`5>C4#xy%{`{Q}9Kv6Kql+mFzaWM_ z{$M94ofO^mi~u77%)wF7!fdwc;qVVP;X z%0`Mr2r&;7qEc#&Z?Bd~s9Gt2;dA8~CgicQ5Q>^)t+oSpIvD#>Z)hz%q*Su`Clq9d zktv&)FtOQFB(|otX$M+Z_)UqWRsQ@WZq#Cg;DmTztfk=FXAX=$ zS}iwHn5kG?rpJr8r-1o7{PAObXgAKIIe1er?`9Zt*N4z<6c@vc(s=`TVgZ1mH5=<> zNJKZHJdz6H)eCb#oHT)Q? z3J6;c884|t{L|zF*qEfPgs2DsoMv?~Wr0z6qTbEc_9-E9(!k8JFCnoxSnd$ALj2U( z#+El`b>Re$ildaDOsC0_3o?0jXL){sbG+g%6T%wlQnA6l+^6C}aP-)*sX4P`>-d3v zUogb26A~;G61v~-u&Nmc89|U^A`t32=2iWkE$+s&yi%hf`g?EtAQJxbO$@OLuq@;z zdIH1d^W?rYHX#1i?SWhOW7I^z62ug>=qdsDRWi!c%tmw(ms&+VSZ+wI$3X|=0#u5J z9-EVMi)81-8+nWiE(jg@)gJme0h$3&?ZM$D)Tc*Sg_FU4osu}=0U0gs2M`VOyGW+? z{h&Cnt1Ig+CwxpXYx8%(Jl2_D6dXwc;3XjGi&5Oi`_H@aawh$>eW$VK#-Az| zd^>*);?RK7Nkl;n1o-x#s}nwdtLQz+37FDHMiTwlDa6tYWstm^6t&vox!na$9AOS< z$IE9*C%t~Rk=%0EtD;}OchBPBrqGjr36F>%Nnk;ak_uNwW~SE%dwY8`6XlqxtB|Cu z%1gx1G_I_!sz#|lc&yshcFL(Am}%H2D}=-cfnzJKZN+7W)f#}6AYhzoQ_*=Lc>G=4 zoYaRlhf=Hizvria&3biPVx+VhVHwVRj!*bV=U&)FdkWDZ`D}>4gFE$nZJzJ+-&h#) zFIVeF*gsb%t+pE>P>cueW$gP0W~%vRok8)SduCH%L%o`WT`Q!GQ$tVRbEB}kmPmYg zqG6~o?+#kqA!9)GPN?WbJK9^fOf1AjMq}s+SX^GAm~MM0G%tx42`_8Kz9l|)lA%rt z183>5$NgmHKha*a)$jpFv{)(4f?gxCx^`cZ2!NUgmZ=vsits?3o_mVY0^7yG+M#R< zyLJng;DdNeBskI(Mvcz5zd8GjnV!oMOFV7jvGhH(n~i@M^Nu^FH$3q-dSI&_t-?<4 z?su^#du^5oY)X-k9LL;_mlHm&UO;lw;(;#h@lKPVxVisH66ZBrb$h6G5?D9raH-S} zowQ+}aCn5}O-6h!EpT>dc*|m;Hd3%>-OJw-b#dXWW9Nq_t3EXe5o( zP9ZgZm+^^zmkrL>mnNS<->>2>#Z{X#3ZSLuW;~<{9l^A{LUMZ3`iP!Et%f$ol4>^- z{^&9_3y*ro`vO-LxC<=S1PFh1zh{!#KyG`mfs1QhSz1N=RLDqRUj8owIQr$tPZKnT zI8d+;36#D4e$Jmm+jT7CTh-(T9fs+Vr|0BbI!!`F{m)kys0(Xmv`QSvDTEyJI zN{R0XQq@!9>g{-3*S|UXpp6Uif^dA<5qLEO&z|?>t>3ufx|PEx&-|mrRu%KbSpA{p z(%(q-&6Jwjlg)`U9&LZ|{d@0X$jFe$7vg8fxflXI*e2JmeVUkf4Ka&_PBYio%q%-r z*pm_g)mjD2o%#PfgCG;p(SXJ%@m|A1&Mf6qV8X7wQVB#hV+(JmO5XE7Pc8of=l{9h za>(T8&%NwK;xNpaocfbJNWZODqoK_SUrQv2Eou4pG5C)kqsQA4)Cbc~etp04=aBKQ zj*e2+!$hDWGXy+M@rVEOW@9p-PYm3pKFLr3-7moq#*Fz3=7dJXU#zh*dJKXufCB%tdREEsj#P^U@;7MnfvXZRl#lxT>ye}t>G#J^1K%Xw3#v9?zcjl#`Sm5xKeMd4bf_s<0AIA{k1B(7Zw{L|d5U?2MK3CKX42QOFqa5S2FV=_1Qjff>0 z&m^I>xXfOU;O&l35LXFSYi}|?m~P$Yy7#cP&qjdDB3ipcO|yBUlPP+v!cs|k>Xlmw zX|-rN54`lqph47u{V7M@YZHJKpux4G8E=%Q4W`BKSs&3*<`9T~ z#Kr_&F^cbh)??9x2bISH1BVnPH}n&ykkTxw>29?=fO4?KhwPmkKd-{&?bV8sz=70BCXAm%sh&KbG`0<2|5+bx{QE~CiGQ%y%Mw$nd zX;^Y_Adm)jU9%A6k(IakH}ygOTr^#6ay&DTiH)}>0Sf(L518MqS&b7!c}OB=W%VV5 zrVwT#KO?MKuC|=anQNWptAKsQQuS{>2Z4z1C!N%@d=!H^mw|Z#)L?ntABTrsa3qV1 zi$TzlFb7}!WAOj};!Oc@zKA}~_27Z8 zMG8A8`~YuWK^OqA&;Jjk8X>QCJOk1(v{`=-97(ry^zq&er{%wnDjEf-r~TztFpzNv zT`5KnXen7Ly|`ybLQ!w<;`l1O57dj=M9+~5vce7aej)b$%f4jaPyUHXIhH_s3SB)K z)`80)ES!Ss!WO!vqql|vfk5_FBWD#Ae9hE;trXwBn#F3%?YXA0ar!eAS_8*_`|!20 zjTJcAF>hDRfN!3_x?EJuE1tXH;tB*4!itp&pV;7uKblm>MA_)xk6JaZI+*Q`dRVTTe-*3{^#zqB^XnU{kNiw5 zG&4myUUN>cjU~Ms${xAg%R2)aVAb?1wR>Y0Jpz!?*VI^y6TJE_IY*=~8tG3PM{>w+ zQeV#`NTe2tnr)tD#GZPxQs_1s1iI_{qjIibR;_vFoyDuZ9LEz(xTlP$(7$S-hkKbi zq6=-DT2O}WYc&G>M-C=J4};(k#4O$$XTK=FvhqD$Rzc#kV~}t`4dg9>3HquDge0J3 zr4kS|_LBlghwVYYeRHEpDIs_Z_yz=Hiq-W}e%L;Up=Yxt0C&Difm#PalX_n09EA*D zyis-VZJ{H{C|_Ua_u+z6Ep2U5(ZuRkD|QvzSP*OAfV><+=cGed|B?`ReByNZKre#B zuK-LjoK69p3P6Je9BPkHKW>dOwQx3^4d&F%mi^ipF%u64YSbVLCC`f{eeQTPxfnv+ zG{82|JT^J0J+#RoSb>cpnQoV8HBA>SwKr1~uJ8Q0^g8POZKm0Cm|yL&HPIak^k9>r zCPlUSoPaLIV)Ou?Q!|BPkfe(j^b+IQFK6M?0o*kS@2TK~+BOU#5P4^)b3ruk6CM~r z$c>Oy%z^ZxXDSuyY^o0(K4^ljT&{&xAmW*cWViA{@YhAY{&4MDiT^E@0K8MVBf%4( zmiG{}4?)h;PD8(bV;NL+K#rIL(bIyor19QtXd|yI&rRQ5PT5C3hS#oPX2jOe)9)_R zU0!E5fyX7GT5Ugr5rS5^$`hkMIKC|C$Ei+EfthbKL%<%Go`K2GW9Zd}v65JR!>NhTAR51&cIHUnon{GGE;&rSfx8G`qX zSFU%!#YOz5 z@I8g~7SOaXw*9jG^|kc7QnSYGbi17QlSAbp)Ix!<@sS+KZA^FN?!AEu6{&@wy|p)Aq+-0_z`$1u1vViJ*5tk@o2XKC z2Jgk3n?gv?nwqL1y_z#hJ|UTOzYyMW$3-FyWS(B0=WCsH}g;J?oap1={4my?$LBd4aj;cPC&VcfB4)L3TU)Phyf5}b)G zaWEeWNgHwtS5LLUnvF)AE|Xft5bhRHuO=$Hn<4$M<8fOXX=SzTH7F_>T-)Av&6K#4Xc zq(jO#qJn*4r#jm20^n|+LtJ9vaPyLt27r_brm8@KK4OJ2N~ z*I#khB*9y=9JnmH8!xC61qhY1LB!`Sy#@^4Nd0X8B05HPcEVHK*(L-2cdeuNYmhba zRg`{&$C3(xH*M4F74f#(j~g%vMUD+C?g@~vMY7!c0J8@J5d~tZ`fO^C+5d6|vI$0s ziYuL_z+)uuY}l|)Vyxu4^U49YuRzc7Rh)xD^m}I0`d=&Kmk;3!xQAu>@Aji7!v!qN z68E(J##zngw`!45y_%7iUI!yx*9!-Wx?tY<9gpwC{@$CTJ;aO@@hLQ;@!)Yf{>EaL z>eI|vk^osZikI;##C9jx2i|>DPxU@jJ>iFk;8UMRY=lp*1U}D}d}7g+GJAI98N->R z4o>SEFwb(}n9vJ|y}_WatNSiy&gEZS#1IZubbrY{=uOQxczmZ}g2e@3B+a_ir&tg4 z{zn+sU4ah}P>>jkQH9d1I@H57Te}__e;U(ODo}_ciO`nfbBC#Wq8cq3R6 zt>N(~ASok??dn&vpfLvCm69V4b@`QHr<H z&%b;821@=^K153z9)SJX)2AUNollg)vSL!mz^|15dMPT#61|v#XjS4+(EZ~WZ+%UR zYJTCu(QX*4uMwZ{lnk9X3lG&dv%x>En?e%yIyP!{(8VJH`x}KBkbG(4z|I&V`-m76Y zus*)Jz-SZnAvQq=h!Z7u8iomkozVFUtiJ^MGVgvu zEoLgWsJw?}c^w^W-vv(z$hpzML5h^m2p5Lq0FNN#Ua=8xeSbXhyT` zs;R$yLBJKE8#UdMVkpp>zs%xQ0QmeB*XRwjtMoi~3$tdtU8YE+41@=`hx6(%m=D79G{HS!y~)ZTa5Hg(iVYPMaW zC(EMp;?OdK5vTkGuM!zcbvliHG5Vd@{xPfzg6UcSRUb&<6Yim|q*#Rk*kgr|8 z&Vg{))+m%ewSW5IIp+KUQ}h*^q1&2_81=L-Lhcnn5V{cbSVkU8@)zzt2PTFL4FMY( zXAgk_m7J=qwh^)i7Mt*Bt~yEBuSuiCR;u?>Yx{Cq>h>HR6w@YDg3y^iz8oEV86&M* zx`+P+z+H_Uz{aJo?SSb_c)c?jSeJQf>V*BOj`}ct-WmJksXJU1c5&pI?X_%`7nXPK zN!Qv%(yD(>d}KF%!*y5Y;D$hdMS3DBX_fUK{b3TEk+a9uEGOnvgtD%y6RNU>_ayJ+ zw#5t6>&u~)lG>$uMv2yb_UW$|1-xEdonA@Dmg?Jcl0K1@L&(s^;m*V~dONpQr{1{3 zBfv4_%mG;)<{XAwee9dRk?1+YYdUEgbU9RcJI5k`h%d?5l@s7;nsx_c>5iLRILXf` zLdQLi%c(t7-cpy{8IO$vM)$i+&x5J~=wu%98jt!YT{u&WO0I(Wyj%_@R?+==*;}y| z?7G?IM0q)`Uh{0h+D`{p?+nh_D$?oJkZ#*o)_ApKjn8O3e{Dm-F%fZ*CB;DhFjhy` zbpR#f37IAcDaLk~vxdX4HulnkPRS*1AnGDEYeAfMl74Py4>~5~s*YEjE`WNp5O_#Imj*m?BTgoR<~?W^dO;|w+dyg7JGg>EiqR>vG}!a_1qi|-qeTJ!Cq$7 zq9>Z>(ydlzoei zTZ(1hFJ>3H?VK}j=}9VtQif}Yq?SUU2~bcfhS-e5(L|*8>2hZb^j`*Fu&v%p!Ir;E z7ts!%*#wsf6dQR%&?q=Ju$XvCA!+#KDmf*l?%0 zmE6=oae`d{SNgRORYd-;Bj+`IjTvETd{cnCm_|jjK1@+WQXk4O}A_ToSt){w1JW5ACTgG z#+OX2%Od=JiX+pUDl|4JAo~;@O{>b&K=+1I*Rfs8qoSn~_7ld8Z$~gmm>5 zNO(zf*w{Dgl!>jki5lZ6Nt|AyiwK z_dHKvR5Y8#B$G3hxy$&{$|cORRMS{F=!8kd6eJ}X*8`*NVC2o=Ctlr)@A=oaP?rNV zZBEq91d%IrfARvn&7&jebwCR0`*G+^g#pEw4tvBosiuN<)Z6-Q9-QH(O77!lqocUc zQ~)}TJ$G*TfLfUlk*IJBCs8jjE1uzlI{yb-+fIZ!;Ja%Jk8cH!4^jK3&qm0P?7)PD zy#Wc4ntK7OAm<3W?IUUnMY%GB4chDx=iy38|6Zx<&0Ousc!%PUCZf47Ac+eZ#cUo$ z0G00jh-PMK6c({d#4WJ;WPe!%z!(R9!tAO)qK%t#jS; zKMi873%sGK_@De zQv)s&s>?`FbeA!h3M z4rU?=M)mw;S59|e^xvFz#2)XQCN%o}JMU}@r@r-&TAt$W(Va~sAN``6C~8vcfN=rV zM3Uw&+o{)HLW~4bGkMW64}aGj-udA7#t3FmV67^70xT*nrzfM4e$R~?TYiZzLgItP zeb=WirV`wCa6qiOH4pq94>^3~s78tXt=a@<+42dDex|CsScwRtuhXN zYDw>BkS0oI0jRUwbK3N}h41FAM&XnYNe!6i>t!n(JaRI=Wd;tPYkCUfl>@|xe{I5X z2ym7pZtG4wE%F02_2x=QpS|lF)n@wZQ;`U$+6b9oH~R4i`O^ojV)>cYaw2 zV`=y^Sn5@-3ZB2lIT4+RS+J0tewGUDa@q8iDCMz|YMcm{D^s}XLK=b-?V}Cmk@m!} z75QXAgA}I{#14uZl&R#q9T?SJd8$!$wY38ol5dT5J5hu#kE;z_K;p6;wi(q1qIKaD zs65%7ROw~L9_Fvld7okx>ZRDs7~*qYRoF5hv@fK02V~ms02|Khu=`Y#205-B-p6+j zaLb0C(0%CL$kuJvq285^C@t~1&K>4fL_&jazfUcM`zCurZ6{ukRDByKS0at_6fG>9 ztBz>W7hgXqXDo8ccNjTMy-v@qt*w3X#1bgB^l%7hpCHYxuQ5wSxEzR~cX?;GU~}IsrHclXQ_idQxOO17#o1d$L@n*U);wx9uyo|Tz)qK*a%-G z#Gy{$|2&Y4V}cO)`tLnkqs=&PKOijJQ!=Q$!$DclhX#Sf^H%Kyzb@3I2UuG=`z&8o zB=ap~!z9vYHamv(DxOhcXI0}WO(TSX=nOOg8v>D&_%j7Nz3Pv2bE;vh-zhIn!HR#< z_7lUsJM<{h=zTexeQJh%P-`f4NG7wR_&NA+W1{Q=e8Wvo!xZdDdJ;h|ui<&>@q{)0 zNpA>~MAIB-*&&j%%;(rl!N>^KXrdVLxihVe9&7FjaukPAZo$#%S-`==5o}V;cT%Ip zZd9x<($!8-SeF!^1N{Rq0A*!$YzS{CH3O}c$L1fBzs5T3l1okNMz%ajJ#kau`OevJRtwcb;=U1L!LHA6M}c#)y6ajEjkp(e*S$s&UCPk9HP0jSsr78%2HE z{eE`hgWow92>^*B4wn@_=*i8f8HrKKPE@a zmI&Q%H20y&hHc_w0F?IwPc8o> zY;5t7LUZu5RB1y&yu)|IwrBU*P0;38NtEK~JgZ5Q2US!~(Ys)XDv;d%8aIk1&|I@# z)Fna9D-gBxN&06|F7r)ld^@uazU1Mc+McUgJ;Bl@TJ>#74UMk z+5*q5LP3ZzZ}!;*HTCRIs>v`|USg^bh^<~7EWx_aWYfYr7AYVV!;!JxClz^FTRUdy zcD}*jijLYz9CBZ=Baz$$gH5lq71OBjUr(`Wg|4$y_@o*fl2UMG5sbHia`gf3qA2O06bKxY1;jOK898YOA< z(^lWP+sCXx;V#3x3V6}{A@d-LR(FRZScXO4)mk6Y9LPDpWFvE7&>p^L zgR)wEfZaM^A+i@#gTSvUhEekS?e(u5?cWOJhQF&0Y33cVn=mGHdvrM}`vdTKy++61 z`wK0TY6m)bIap$KP#=bYUsdl)G}*v8F_3W|pPcY$Ybjr}j zOB1uzK)S8YKWasy8#2tP##qO2ys;@RD*XPI54AuKm@R6qN*X%Q(-PB(+s8oZjuX4b1t{O zC8xl5IH^bD4*T%l>bQ<&lcOK_%f=srfiZyCrDlN2v**5K3K|~Ods_M9n_DhgO}b3 zn`EHqDkN-Hs>&fukfwhHZ<=4{l0TrYnM2q0X6nqB)wJl35D>p9X%Z3~b*I3tOhyAh z!1ES6tB@moGI{D&*nie4vV%QtH=$b#evq*1tW;e?m_T{8gZt;DhmUna1#Z-Ymt;NR zrq_CaH*h*89^3bUdW4@1F~oY?R`va_@VP;zuc5}$K%pp@30G&(c%KObo3tp^4buFX zQ{YX>`|OCMkjz^MsN|M0beph+u>WvK|F5azZTbRK8J(qMSsYf!TZj>r%OE5o}N~R5i@LJgDGRAxLqU?9F=h2 z-@hQ%SfeoBp$W(E(J*IA!Nb$GTx*PBs~@L&&O_C{!ED7e?%#R~w&T*E`AhM8xnG>eyOTg}TKoOS=~CKjyU)0PZ-OpHyNAJCm`OGaJVH*5K;?Vh3s?!0R-F^?1@p;#(Tm zEa4pH(-^?6RkFniRj_3nx)>3p|Gk2U!n)6<_>olFYkM3QgG9~}KiG5^y<|TS*G&$L zRl!LI-S};}7_bx7#!^joiDI${{_RB38Us6gL6NrddS|b{#dQ z6Q87_i8x|VUC}lG+6KjI%T0hDo2A5)x@JdRIx_*5rAnbBdLM{AOiadM{w2b|855bC z5NrYs);-`5nT7yI{eh$73ix1Wfywo7(*uU$+un=Qf35MYftUt@qPR<>O%xV=1I-iO z5U1T#Nf9=%+J#N)MfOt+Do$rA>EpPhA=}nUvG@*0QTWCd)d@DbYowosZ9Xp@pED=s zz$i5InhEM{6hRlV12zn4-Kil%^B|fSyNRs_n&}=fqBa>mCunVgdQc7uw7#kpV zWDU0DRRcqn2_}XbeTAtaJTl~EjJKB28jghO_f;R~z2HQm(=>51&>M2N* z&|M=tV5z2aYANHKNUIa$JuPabeC4(QmMG<_i2NexruDph8`3?L!ST-7@RoAW`WBUX zEjI?XII`e;86%cu?5w1kf-m-X-TtJQ=tkEl6fRej`Y!T-%IE5r7gwL@G`JFMn$hI( zw{S)(=E(iDJJkfala%^9SY)OiNkH*dNmhE(H8DI~d;2|GhrC3SISj@rm3B07W5TJG zFjIg4M2-AXTy}{GZ*7zI`g4jyhe%x|3m%e9U8&?`Mk&*n+r^0|O*(2yDAF%5FNfL} zP^6~(;s6ZKpSz1FrWg0^G*(HgFL5Im#FF!m>6=9Yk z?+=1VVNRwU#m&{liGyT9sF2M|iEfvVKgI~*9Rh}02(PQHPR3+RQY1q^iF77LN4s&O z*zSnN>GJlz*tftX6B{Edp}2B0^JxY@S%GnSv64=xoGq;eA%)ZTlT4Z%{pl)=Zx6@P z+V1Opy%N#oV53qREQk47Tah~Po_qh9UBw*^+Abh#RIVVUO;-nIt|xK%smrreRJ^8= zxFv`PLa@0PL1E*&(LpD)@#9@P=I(QMX&KLT3x%#%$};P}UM>aJD|wf52fTj8gZ1(l zKmZK{W0_+A&k4GUlyC1~H`CPQS>2m%34nfL!n-ocsoZrYJV%bU?CWN#sORpC))P82_{Vzkum~U4LAT~_16-_L1n$8UU?~z4+!ZR>{}EazBHdU9M+e? z^FI%TH~j(Ff_{U(nw)JfO-in8OBc&gPuRZmn}h3`37BhYcZ1eJfW7!ObVwZ@ryWqm znL?|%R;e%YJX1f_pZl&smITcy+Uuq2jCeh(nhNB?V&u%yiu$WdnrB}}dRR4Ku5$%j zGv6@Db-6D0lj$V&YJ%SJH}?q!0W-P%5B!+VWm|rAk>{m`;n+C_%1jEKOqOm6j+*fL zra0Z|DjYCpG{u~>1y~Y;!3E}&W}fJq_>WKEydercN6@TRto^Wj#N}RxjACMbsVzPd zpH_%ZMSTuD=ZqsG6j#p7D~3%Koe?B_4gU>M93D_eS125s7qaX4$#T_pZ^YYh$vq|O zLA|tL^S#Bt5uks3cN5S{v>tJ?wOl9(Tpv;sGJpMTzmD0o(cKY-?)8_R`eeY-8SGyM-VS8m?fyLQG>&?>k2`q?l}8OvwfCwH%(4zy?r zc+#UcTotI~-5_w^dNB7QOyb$6=W%RLWoCIWZit&ikS85~SgraPHH(E3eYkwqc1k83 zAr<N13tSdOdd+2Jq@oESxapVzJsyde zIjGGC?*8Z`uA3L9xl)g5kD%Vw6^^&NFSD_zBZCL}=lUe=%{=$(yR*ZtlS;@`h*7T! z&`UXtLfK%(#$--{ieRJO5SN?A<?F`J@IRn5pa zenG{M8ant{5=HLhvStrX!on8S3khB~pdO~4GGyejNDp$vrE8F@hYOv&eq9CK-}PD^ zP$dG!E`T()e%EVU=XAOU(ro_SUjA|(M4jU+1^?v|j*r}fd>W^M!yk`nvGBy>yqizM z-gP(rdo!U6#H9NETYzrwCyKKZ--1(r@pM5vcZRA9jBHF)rU5$NRZY0=kODH6T--do zB45A^6~<_D!*~J>-(8>X8gsTAbPr1*FTQXZrUyQZ_abmMzU9YO8K)9ZPQ`!63V6#$ z@%M`;)Q3;l7I5hK^3_&Gw2+_bQWPnwu1oOi# z7+4sjO3j2rCx}o?{-$|-+jiTxa;(8KbPO^9&+am+dwA5$@khIV*j3H^J)*dSV@KjQM)J&JNqrTF~wr`7xi_X0?`% zz-!JZr&hhS_Uk#^C9{+o{sAHSCiyZB&0fBSZu`s9=|OYu0-I;jC&P?U4i*95kOk(U@qkswuUpAiw+PUZKCxxOmAPw|Sg z->Q5*VdJd4usfJ4i)ZiPzNvEKZt~U$$=^5#WeRz{#>O2SW=0yd=44awGqnoLp?&a) znQpC4;o_B&FjN*fEYXmMxxs!|{NoWexM;f(KsbDO3YwOnqf{6=;}WCyHKbaVyPe!m zG6TgF-!fp>faH!qy@6H0uv8KN(~*UvS?PvWl~ppf2*^?T9$)yviG#Y0|F;qcvU`WV z4BUUfB6j3h;rELD^)~W{lrsKG_C?I!cX|baNGFu>_xV2K{QTm(=U7JT;4Hg)aq{q= zQ}SzmB_w0D{^T?V<28O8%SW+1eie5OG8_B6jD-QxY#nW76U z>EjE2=1&ER7-+V01sj#uB8z9wsd1H1=Pt{hz>(6#pYxX`d`$Wwms{KB?;^eT_tt_N zL6+x8u9?p1VDocvR`XdJ`Ve?QU|s9PZTrxYC)X8P7BAf0T8!_n#L^w!`nzJO?w5El zMtm$@UiWWZu*pjIm$nKJia$@^#tNQvR{M+YP~9SQllOE&E_Hi{gPGP>8gE-%`cRju z#mciSyf#rimJA@w#@#KL zyRX;JMIpg!*(W){MVq~K;kTP))462cTRYny-#j`UXCN11r6t^2+mHtR0gTNG*O!(0 zyX(4)|CWrf)R=RkHUXK*egEP4=fU2}>vCPiai2J-n-)C*7`cAOZuk!35)7wgiOmB- zyM9SI^PSA&Bh|fGqZ1B)wD85BDKy<%>Ke%gWU@>-Z@t00J)wtIq9?$o>FO`jTM*uI1GkDeT|N~X0%&Me7i&o|It1h^(t zIVyf!!{O4-*!pZ=E<>wITT7kt5@>2pi>KU4$80dXfY4O78cxwYtiU2@Ro{GAC-dJj;E53D}z2Uvw> zp2x<*gYEH6L)%+C6PAtFLV}rD_nIiaeXZ#-wC>q{X{*gh zNXM&l?P5A6OTq!#DuBC0Q0?I33;(tox!Hj3$Z7%L3FuO5PH!6?->d0}!O6QWJ?q(~ zr%W^1PduE-bID&jgb8bUV)?$}Dl~(3z>TuLtc!Rc!`|-oy2tasi#`;NGa%@Q_m?IN zwI0#Uw2l@(HY`U`LJ7LAWYw^TBVCv#iVDi64`jL;X9?S7 z4a#QfV$H86nYNZ|N`4}77G&t>_5$xN5cOb+iGY>)qS}Q`K8&g0Gas85yCCS0&0R5L z!`Zv+VpG@^)D#iKCoEJ?9G%-Li?Y>#-V2n05 z;v;PDDe))6N@~YXPQ21uzVz_@T9R}C()oiA16{WiRi&0m=t*84xhnI-nKz)~F0t6; zqrD*Z1?k;5Mee=%XO|yy!hFG@|MBTSX!r1LS5QiQrD{USyJl<75ourVdL<+hiIu&~ zmY>SAMcB3s`SY8-Rk_fKz-pLH0lSMcS|s^6Slu1`)*5uOQ8_%g4;idad9S%en0KqM zZ+@5#yZCpZal$FrT6?b_nyusMdbc>3SkNnZ8g07l@2?Qxr|Sm3=PIe4X7kw734Dk9 zyJh<9Nxfma6Ex{(_R}w_`^i%AQN#r`zAbB$Y*KO_s|q0!h-gEXtyEZ{H?Vd~XD*P$ zeZ)L%y>Q2rj$WnZ#5`>!;r`Vk|9rved)zw&JAd8qeAZvTlw zPZwZN57<{9=!(31d|_?>_L==X>vt7XhLg1I(4?X+j330ku)iL(lqz2j?f(o99jNLE z5^|ZoHnSWoDt?G2y&5_BT}4veD)QJMv$uz~M#xDphAWZzyS>?U3`YXTV56V#gn)4r z^9)`$-Zr|hV0QVDjiF&`$$QpA_1RY~JUCLGTvt04Eb-F*wwD}A(oDhI6&UM?_RvH# zapcjI5&YwWNaX9|3aO&r8?dk zr$^#aw9rY;I?f4$X&QGWdAY}6$8zv* zilTyG`Qq8M*?TC2To#_Fe+#M9#PhtP8XiW<)JaugDcp7Dm-chTL(r^yX-_NIMyp2k zSl&xto!n&p46p6%M?*5na}J}qL-{14RUeDi-b3UJYT=#Wf*L!?mQeo|=cXz=_X%066l)BwmnVzhlh6hM;VYWk& ztOSky@B=P3-q{PhzLz^_YI-7ketuQ&z+C5Awg63^Suel&@NxVKzP@J}+%uo%f!ta! zV|_^Ml%+Oi@qIue{NkDaq_VG|2?(I3!Q?r7(g1)5#|Xxu;6nRl7}d#%UDebhDdmT? ze@V@thFf!VauWTHyV13sI!PCwg>Z4^y^PQ-X@fpzV%F2otabThx|CC&ytQZl_iF5` zi6CcTnRU1!Efp%>JwW}_ZTl6T<~!@m-ruMIs=PmNC-GF$C)8ZmYprT%iisqlyJ_XV z0X`<|U?w#6@ninLLAQ}k!D09hX|TwSUKkNJp-^M7D#mG5l3A#B(^=2<`KndEUQK)s zurJS6UzP6d*t#aDPzQ^aFxKZDrvHr|1(MMA34jBZZ+I_2tA{`{RY2wS`qyQ4glg2B zgnfLd|KxqqZgrN$i*B5c?S|^Pki(fk_5#T#?h4uM!s7m?L`h_AV=H=3SEZsdI1N~q zjIQ{)R}ypYiY*DDEmuAOb4`6 z_s=c%=6yQyoM8j(+Bg2HV zErLrc26bYA17hn5=9PexgGIxaI28&kHToC_U98Z}H+1nJ>!TaL%x=~QZ121#_VaB8 zu$JfYU(J@|t3P1~q!peO9q0#^%XJ+KMQE(xuSCZQ0VtPI{9a$F4l(V55!Aa6;C#gM z-Gu(?{d@O63*%s{%an6xtOKFz+*AIRGKN-HC!M$7)y__{qd1hJ7a$y$%fRG(nM&T8 z9!<;V>f;8fr~YEgzlW~nUR2v3S11E{SUxrK6$lGw7_w8@70oC#a45a{^&PiL?M~56 zFMlU-!|}yx;y>#KjSp{E(su$!V{&x#-tLN?&yW~-U`R#kv`mNkx|KoJwJZ@4R@;9T z6(=OgdkSr?uA@6n6fRvk@(R+tITnW-BovaT%{6un_ zkgZ8#$FAFG#G;o(ngnjbHnz~2%i_uAEb)mvTndg8L@ zUvFm56Y(xQle(W^;r_%hOoZtr$ON^KP!R5k(!*E(WaMUQPK7N#si4UJdX@4B1fHX` z7Hm79-vbTpTQXAV0L*1oQUG$V(qj-DiDP_z?qOy$?Kj=HLu>8GeZc&1oC5f#@KL}g z_C)HJe0Epr70J_`q)k};zRyAG3--Q-U_Z3)GCE_!*)kXFY?gQy!fk)Rl zRm0jFdL9wN)wu5o96Avf0bQ4lisqdKtC+0fj7=;}n;}U``?^omfeJRYFQF~T{N-dv zM>Jm?r%9u4OWqBZN~POkUU^4vsI>mBV_sbQbfR*bf=-h{4`Q3cjIToaa%&SnI`ilhVaNeTMZvna=NO)#8~4KnLDx zZWfn1m5Ag%59P&MZadhr7uctfd2?%`+XP6-a9FJdzrC~b!Wz~O^{VD+ETJzoXNIl@ z(94Dr*_FRlKN}bT*Vt5L_4cm!Z89~dEIl8??wi}?#l-fF{8JeZyZ_-4eTLaKfUR;z z2cPA4!s&iV0-o<7=3HYns99LPlYFz$yvjG9X%5s5uIn7?GUvYB6tUElA$KI#(S0>O zHS?cS6k;qfq@t8}0RBT+(v?u*+zkw`MFRf8tAcOw?79lkiQrWIlV1N^rCIC1v5CHif+!{Lrn-)hY8pAf5_-u!hdC^KD-m z9v*fFs42fuZ5UQ02O{yBgq8_6|Irr%@x0~qZvba zVVdl^bFrae&*~OQ8^6hXoA3F4$V?EOP4{g6JmconaEwEaocc^>2w&pJ)jRVc1cUyF5^2>*?g4;KX+CdcmtJ$Oblce?-NRA8}^aNv4tVQ6te zn1v=L;zAU{V%NBgZIeGms9G$;`2o9>IS=#D_Q z6PoA;&V)jKL0J0$$P5qPs*tL~VSmAS>*rlauVHTMrESg8{pr$OoN0;ViNBAVXax`aiGV%(r zZ6D&>t%rrc>`TizQ4epxa{W%Ae-z$xyZ|WrQVSzn@nf>(HkdTGZFZgAdE(Y&P#I5@ z#GP2=(d88#_U~$ljtgq)a9d>L#*G)CBK!E=#Nz)B?<&jU}SKh+tx1}7nY z)Yj-QEx1C9w9b**o~8~bX%bd2lMAd8?7GrBQx|?rCx-(vb$FPSlaB%t6EL4IE&scM zv!LPhzp=)aOxy98g~P?0J{j-R*srptJsZzjw_l?Ly8eq}SE^q=J?zVmJ)ih%ELa;; zo@rJ(;&=?|3{WbAH2e7@ zv#r?vz&<_4c>R2%`z$c>4cTw85q0OzOV<5UIsBXH_HVYoni?DPxJiVTDxH{WG-2-DmqQmQazk~c(t$wz32-U(^(&%b z8yhYeGxU^#jhHX)y=VWX6Ggb514H%UVm_}Si9>|U#S8+oq=VY>pCNl>&a#sGe_Pkm zR}`Q^w@Pu`79e#spI8nz=C2^q4T1s~j6i4Fu%iuyqXH(K@S3rCCLj5nM$n-SC?7ym z&5I0&jhoAe5^G&|1Jj~}j%j(@}$N?Zp`jo|(UN`k4vizH8?RdOIhmqpXx zzVUC&t>i^%@XI~|akwbHJ)Y@(VOuqAfAw3yDpF|N1~jyhLOO40E_HuuHf6z=(y+{R z%@KAS=+uZF!j0U77wCS_ad3~8ZoB0bN}2Nwc|6L&w6e|cei8TZg9!=TwL=* z`hiG#{SYHu=l{QZPZJ=9&oPPLTmLLlPIKi=T8Oo$=dUJM4Fg*E?BD?+t6f7DOpFR4 zMuFa~#=DPIX;_(G+Jo9vOvMlM`~Uk;bX)v1(59W)DGPvNM*{0L zNe`S4cYMkdB6%iX7D`u}1?-JzNGr7bgfa-;;+u%>9C67~sUquhzw_g&eSW{yw*9cR z)6#4G_eMgMdgN@J=4o(AOpQMZ=nn$nhfT}Gh<48i`W})k9zwR54#OZWA(3sJoCSMq zHgvXX61<|Cy;f5sdQ^gWCPg@ip0e~+s^anQZV_i?78zcYAy+MJGLxp{qO|AG|JZHY zpVxLm+<7K>RKiL2+*lv$ooW#W+kJlqyVOBwQfkQ)+e~$3wNMag@DrK&7qbP9C}Z47 zc~}D*`PnMB{%%YvwIbB;eng)TFkQcVMQjOx@@3_$HWG_3mg zl7?oLlM;Bk=IHX&x%tTK`v8@;rqCQvM2&E(D2i`TOqzE&Q)PF3Mq;bx&Y2}JW54q7 z@w9kIWo0E4;d@=_(Lzj2Ob+J#TrFUKHSP;#nr^;J`>AJ@8LSNKf!s0;S~O)DzyrdOPeL` zRR(a~BpL>i?{V4R2q~VLbzQBg26V=cnqFq(Mz_2O^`*77*pqQ5GD3q|M+;&zzXj^j zUm#?I_P%#7z93({6;Xsw#Olci;Qpf>y2;()T_SU?Or2bn-PGOIjs}3nqR}|x`b191 z>gDst6~4Em37CEyCxO^&-hpf}IcA~n`dhc}LOrQKF^)Lsy(rsV$rIG|TsJbpO5Rz; zZ1B$wOf`0a!RhJ_*Ax1?<=#T%JQBbI(33OOufhp(tTlDAL36>Mk;ViIOaxMiZZ;#uXoPO^i)6c%+JiowFGxPInn!4zPQbY_9z zklREbZ!~=8#SIpdeuw+-;9-y}<6{`t(zZ6U`qnElpCnkG06q}Z1*txy33m75cV@M$ zu}pT%7^2{SDB-F>Me0QbBhI z4g#8FNW3`7KCid)B zsQ0oi+~ti5(oul&1Tx%-;7kz~&e}l;0ke7O5wPmxbrTHF=hFUq)9&uV9>Tbih<>Lm zuXYX_{fXE$I(Qy@lA#Rm0k)2w8y>7<&8}lTK^Tuw1pQSAw#7sZ2~4%lu@^cO%8fz;_5dy|WWK#+C2u4IDsjwq zZkMRr1ZDtpeY|}dO|cLjA80`m#Kll-7Zth z@*HG~t1Nm8v|2->2SC5c>9R*{w)bTaFXy!Hy`XRX%+sOE**c^Fet7Hk6L-sazr?5$ z|5J|iO-I)E_E=b2r)YUu-8nH5`ZxzlI}A~0uI$554D*ET{`+e+-klg{m0Fv{nZ3{; zN-S)&=SD5d?3w99n)ZEs|0@R^k*mGMNlDCqSLn4fz(tBwO0FYmZfSY`JJrXOhKg>3 zo+kM`EJ~fOdQ#sjx7V!V#UqayhUTip@0XyZDoM20ce>UWzS`^#a%-*aVKjTQT6Sh8 ziD~ZuhNlOpfR`dtQZ6Ki{=0bNNPoX=ks-tLzHOqyKV1h1fWAj~Q)dFkpF2Y#W^e~E z3%w7~4iQBCf#DM6<9*lOyu0lb;#|v@ULM~00zV~5Wg&#|y2m`Bsy@1%Mqu$V@4pLa zT%fjy*C&psK~SK-p46_=)`jZ~&ik5_3yEvPo2h2nj1knO%M_jRRPU$sA7BP_g-SmD zI%kZY{!C;4wSI2C)%RUfLDatwDiZ4?ILn6;Jwr|GbRPsNl%6oR>h-%F{g~F8fpQ%j zanaq^mLdKhWc{E}l>O&S_}@Q;`69QE$2f%7_H3bV|KPz5SxD_ba`$xi&S`SmC4+u0 zu73`xh=eOPO^#}f-_gd#nMmL`Zm70V%|c?cQV75_-0pJ(2enuh3=OsN3C%sqzSG^8 zQacSYS%P@oU>{I@Do$vLZ$vJQ1YsM*vSaQy$uH32!K}WgW(bd9_D&#IT05dwtWR2~ z6r6p`iU{FkpLSXCPlJ6g%+T$Z4#^mMt}1ImeC;4%^?7rui+V0)_W0yi%aLo>Ec# z3``{zN{3UU8|6DfXYNk1vbJlYF1`};@q-GkpijwuYR~~qsv{KvQd48B`Q=DEsh!gD zCJb@j*|H_cEDF2d^N4)@`OOS8hs9JI&>*!eLcxE=Wx7#NVC)`JjD?14Df)s7rd&06 zl9P%W@3G1hnbn}F*`||=b^#ukYB%1xHb?kuLLtJ!TJfJ}zckBX4mD0x>!;QA+*h3X z@xe}i07eFhE`oNs=sSar^8splJIwT;38_AKV{iJZ-?sG?G)5XSq?G7>l;6@&9`xuE z3qJ#wg)4(uW`?!@4N*y{_-%YbW$l5L&%+>q+|O!DG-^RZj3*fRENv{LJGV`gIwB>* zGc^V}mX@6AH>=ZBeEI9^j-eOWY6tQN zSCcOQdC22vMJn;7;WfXvC_#If_E!VX9|7CxGr93l^O`u<&r%uk)q!USGG~lciMpfe zX{?w}(~SRjj$=T_{lC9ACV2*!Fkrl@?5?UBGrkICF-yWA`&>{_Qg+kde0(+yDom^R zp>F47uTwJEzQB?F>4(qsDu*MAPp2%SyemadMFzi}twJ@v+A&4uLCT?kjM~huDS_PM z?fakBUT3xNLfZ*C3_Gyifh?wY3#MZ8)m3h$OdA800iof+u&Jfqde-tLH;@C$kvALG z5B=xou&1`t8@FKqTZQi{mkC8{gl}?N%FO=ol{(s#Tkt(9rwtM)g(WHo>SR?_RpTFn zf{H9?6~`<;QruT-e4ZWXY`pY9AXGJ&z^WS$wPi5r9J>2d>Olr0k^6*ul#u}s2BpbF zhSvMEMvn9Qx!#WG?H1eRhALuf2PH-_*$>_iQx{lXq`EnNv1UUS5W!td`pGgEN#wIDl$x$P zM}$HUZ!gx`HvQ0i8x9Qczt}|WI2?#+Ky)0fMVD!jZoaeJhnB4nGu_1~F`rI!nj(f+ z{9ADqwP^LA7-XOs`*7rTCAk9NF0h< z@Tr*)l+%6w=n>S7znm6`9X$fp)V)2m;MT8?d1|O#*lrNMhw^0faeX62p2qne&_f1N z&s3I#hA=|aihKGl3s3{EJ^YEDXw&EsjD0fe?u!FjSw|fcsJc|9xf$@k_S_)DHL+w7 z=9I8V4zvC-#CS|)AY)U>BPFA?;owBaskTXI|U&juo7(bbury({F}|1f0{VM0oZ$@az&_n_PDYPRf2ZdoMFwdlVAD{s2qrPICaajDid!^vOXv z`n<~=RK_mr6n^f46U21x4e6X4#RQ8RtxCkIqeK#SdhDckUY|1%0@qlHGfQae0^8CaS}{phR}kVC!atlT5*HTuTNnt-P zkwOb4c%`61j{JW8I3PW|HceDGi6d*6@v>Q(Hs$!!-Tqt2iNTZJvj1OF3rp@s>jN*i zUDDEZ?TbPEhfV2mV;#AYS|h1a`{0#X4jID)D*DBe=k| zKS&;m?3ssj19-|n)GxRm>8UFN-_8~bh?74B=FtC!!{886x17-6x9D+muC#_7#2A(6 znd!VJ*CkAm<`1K4TzrTjMc6KnQqJK-B^6s#17@o9{y zgh%%wX^-}lP{KzBG3=tW=ZWYuu=X?uLJ{?`93C#LG+l|;-W!0u{f>46Kj2h7ym$4# zWJi<{w2-*^E2`j?%jjWf5sK&Lg9-|S9+fr8O=BUN_5r|;c{>sP#CA}ZyGbtIS<^C3}DMB%1L zGKF$!X$PjTP8*G0gv&_v%KYJH7KNq259b1p5*#m|F0*et)!heR%K)okon3+@%WsAB z#3m>f(bEQ+s;h2U_3R<{ldcM>Q3_p7?1dNjboJaXDvNW^t2TyQDl2`I#t1cK(fW#) zeLpAdL}7JDy!mkRm}>&G55b*ce^f_9Z;p(dgb-HlU5ZpbQ%gqAsPiNy{}+x<_19JM zF65@4FW$g~aYNqfghU*)fvaBN^ecK230&oGgKH*7#{*e0UO_9C^F(7@H`n^GlWu!e zJLK%7m#(~!Ju>!^1eBKC(-Boo=cLc=UpZu|0H_Pdqdb(mOV|22cb2XkYeNbi&2#Uc z;izJGA84h`d(In#Y@UI)&>Fx5OlL6O!c#brd+eay>uL>4HO_(K6d|g)i$eDltV5e_ zP=^B5rMbk_MCBvlHToOCoJ%E4h(CamiGW#?JpVk(*G~&}3bD*qR*SDI#0JkZBN-Eb zRVE0rBJA!r^xsO3?2pw``pw}=Ni-H5$CWE|urJJmSu<#4&1?H+E(G)v3&vFKhLnSGT4_!k6M4*bkj-`UL>UWbVDSwB;PH~d67hpL?Z@dLB6h#;XNMIjg4fzOLBMTDw>F{6Ds}vg~BBZ$|Y~2)3Ty{ zrE09-MpJ9%h~w$5Hoz}qTYHKyJA6|Q}e(PTrq&{TQ9*NI$vzibx-LzGU z%x$X6K&5Y6fmjhSQAwL+Hk*E)5ZeBlV`&$311FNAgVh#ZSS#4vY`>3w0s!|cY zSfB3D5sB;J@xn=~YcQK=bZ2W;Ja>TF64fMU8-)%dbh{}b&=l~V^X0DB7r)`#7595;8=HOp)esEr_ z^ETe?@Vi7c-*W2}AFykAuJCZkY*9O&HG){7%MdFz>*~pk4Y;L%`2;1z*5EM8 zy`56?MT6{ezpYG$j4kc(3 zT0s-5o4m>ymJK5NWRw^@+N;>qJDk_Kb@HA$V2vFuUUXLaMS=dP6`+|{TRf5QGe)-z zt4?;I8h=dVA*@SZ;??Q!E_fcdf*laIkcH03KhHtNw;S00{xWu~Kw8$W2EJN&Lx+=? z`yAe_tPhU({L>QNYnwqmXTNpH5g(4=5kdD;*=?Ijq9--D$Y4I#k@DF@!uR~IK_?tT z&D{Ap)K!EhXTRmJ;jH{gyM&`p<|;`IL!nt@PD&#ksNgLyAV>dEV&`Pd6a6z*%GBQbmnWnlKE6jWG|_qGM#*yG-37(!ahW zKImMZjf>=UQV3(jsqVjw5rOk9Y$YH|73Z6HApAO+QK`cj4;IVQRjPr*WMx@q(%n6rWL!)PSOY*(@l7jrgNtH{Kv$5P^OTZ@~fq2 zEU()v{f@MQqeiP=;?-~{Js{+J-Un}26ShT}`u=Dt>yX+-?J#5}wPJpHh8X9$Ej7*p zrOp-K&M!LB$255yo4`3x&k(-s#~aN7Gn7E?;UcAX=gO6}VJ8Wm?JPwPF04GfRIgt@ zAbETA#t#6QXrW}(9W?)RIYZ!IdEa~SYOOBgE3@mkVgn>qY1Q8tx!&whJWYsgf-JWST1z7Dcg8e5b7OQ-k0`~j7&ZFcZsF<}*X`h=5Z6Jcs2nDi zSl?U-L`GG~<@J z#husG`M`h~)y3cZvwLvkKIyu_m`Co+i0XCaHzpqdMY&tY=H0UPItXWLNYU#Yc3i&K zXx_T@M>YRg_IO?L8!}nb_9_mUp)dQey(G}TyP5%cyF%$UkC)^iF|QM+mMxE2Q!dK= zD11ZXzL!;*F-bvw|J|d}?AKR{C1MIUcx_frF6AowzkmPw_B>f{mi~U)N#2>XJdsF)f_AvR`kRjn01E2;2HVt8aj1ago$}>aGOQA3d$X za+9WQ5DFEsvPt-BPSEhetcx^k^LcqSF^^L6LzktI)A(uo6%A$-ub%wlPp@B~w@C_$ z>!B`mxnhY-EfU2#6fI+4tvCK_l=W7i)Uer?%#FjVvC-_O_P!oUn%X|3Av>l;%~3}c z@(C)eh9dmCz}dc{PI&shg14@&E|yUY^Q=2KapQ=9;?1=;a;cdY9GbaA?iF0zz-uvA z`}Linno0VMt!f5xuiz3Wa(8*L&S+jzG_sx5{H4GN-BqC$pWfdc%&I0^y&`?A%~oob zN{n$5VC2%r>YqYSR7HDOL2DCO7>gdwzFTupqF6iRF@WB1M;b$MP0bb*1@T;hT8F}_ zpGD;0-6LNoUMv}N;FoRWY8pwR+&G<#$t&kkok7F1 zRhsS+HOpVlw}kU5D_syunZ-hIq?Z8KIf3QDCz3ZWO444TbD(8NM$3rA-9(AF*`wxX z^Ye4_8E!pzxtS7jX|t-e^uh&~isvDpe~KS%ss!s7((gfR>(X&r6rCDO{}0C{$B3Oi zlklke^+ciIEzOlLF|9KJwZb^07WBKc)pbm)AJ{glo}(QUWQ)JuYX9?|^nXm2AhD*w zfn;c05|AY^GXoI}@;2_A!YN07;ejXH^aIP6vgx<&OMhBZ*H}}t5Yn*wWc<|7XuKMt zH^Fv)R$fzwjN7}S71{ut@oEH)JMnrSM$EiQGhAt@m1K)&6^IpIL2t!f=NHN=j#msM zF~r3Wv+woxb<-|TV8`mxf9e~r?l&(YSNl%!JYH_(y3xFV+Ckc-8`pgg;o#cSypmPw z?0~Y4$Gh>$dNVkHljNC&okH2)$tHg736m`e?!Mkoy)>Jc?tAKojR*OMwYj!Hd1_@P zpGTj82c!ika5aiDDr>`ZM5A6K{1+qWnbq96VG@$9T^4Xg$pf>Oc?HU5yHKY5=D&Ky zRJCI%>vW;#*F(XE;^GL!rg8uPK9&;*cus_e8Y7chmZ0mi_FEafj3d;o5Ibhr}s`0L2yhybpDg*;0zJx!jEQ(U&&1 zGapEpS^2cCf0XnkDY!HwEcu3*Q>SJI`UC(WzA{SE%!uH~=oq_D8qM7gD8F91swk*^ zkY?i#1(q|{Y--i{rwu)x3=>p!BuB|5YB0UZ8u(1rFW5Pb8$WtRjJrM69QX8SgF}`_ zJMKQ5_~e(%;l!s&fg%!Ok_7h@>mK)LT_$kQ5VN2sx&cLY>BhkRC$RB6^P1)Fz%!D| z$A+aYgkO69uvdUP92T~qzJTQ>iHC|F;n#CNMg_N(e;%?wC?z0II(v>#-LTqRR!2t% z`|i*J7TZHki;vk15;&W;kK=YAHZzDi$qXzDVuZa7Hy5S2cSc*><@~?6o{L{T-5EI3 z|4dgQ^nIC7=B}~kOhZ9%w>cXU0?b2iAItEJHaO_gBIhsF0Gz)uvT&>Whwl*<){9<4 z|C$kuB@T7pmOlL@4Uq4i*K2$mM;$EeZY&3KyA(fyL-==RVu;2B+QHM_{^`@VDxJ_o zwC@7kla~Pw{q4hEyZiSg*lsuusH=w(g-Uw_nP&?;Q|nS>jZKiQ^_(1e{=#uqQTwTb@*r4&b~ni zH?j00(M1gEnD`Kjeih=X2Qx9R(0^E*T>Hn46g3r%tb~Xho_0VL8?0HuqtNOL2Gq>L zYu|AiwFUUSR&Rbw4DbIABw1~z*g?H<_It}nk*_Q+SP(7zCB9I)beGt{zxQ_Af&S!R z#bN07ieAYy%~g(I8*KmXvb|zn0#f>(k31r(maPZ2XOjjIc7)z9S3~s8uCuqzr#Zf{ zWA;aKPDY1@W+J7ywuWemU%pJf+pM|++))8yITz&hgpPJFkTpwKMW2yWHlgb1KI2Qs zblt7Ph7^2}jhg_ANOX1&#?|!RvAD*k!NFUpeORAQChz7n$)5?6h^IFC!d{D-l_x<6 z{L)as7Xc`yR5NFL=FFMlxL8I{9gR%UxC9jiyL*j~rs5efQs%&I$#p8heDA| zF?rsHGO!SH;p~ED&lUbg_Ez+tV0OWeVCu|Ew*(h6kCJ5yR_~u*Z#ayWYzk96&9k99 z6hbOa#6*L~``@28@vsg3Cgv)9)MXv)hz1nUe+E2 z*Plm3$i%Pk>S3d%%Wqz0(6v20h7z$EB!07pYcZGdwBZg+ZuOY?#sH_^yD8FZA(W~C zbe~fAOb1ADxaW$`o>ITftRpj|(8-}D(w$7bNk3bh5p$Cl0K^1#zVztrtmP7<^n}kr zxmY~v)DjEmHi%^&KrAq%5mgNSIy=;Z$Gc!A$xT%~&l>VI;o(u99p3C1cBGC?sGAMu z>n};`TDGTZd|xh-rz=u$@J`q9`QH02gS#~o#+jR`D zB{M6fdEIbhlGsX6YTp^sJm8MQVyaXAIX$20oXRBK8m1J^pp2QJ%ax6rMawaS)RFiK z2zZe9t?TM2ED5DcqDrbPl8EgXsSO<*+iJh%MiLvgsP6O@t7l#kibJXGyWgZ-!p?iF zvV^^oTa6lHD9iPN8aNX*yl2;x*743CYT4bB)yQjU!7xqtcj7`cB)58i+M6n=Z4=|d zg(Ee$V*4%yP*PW$K{`ggfmuF)9-SuK^`0fsAq%bGyEYIlY;~Ih1}A|n*)!3qo{0(e zDeS&$Sz$BS-4m#!Zn;pkNdc>rZ6k`vNa<6P;rU2dPu#gtHyDVwyOR?yT9wbiU1n7o z^z6H3-X#$FcPL07xdDp&=X6uz0-(CkSk~Zh^VpW+`^z0^Z0e7rkqAT^dnmxGTy-y> zn)-R*;c;}X|LLyLAt?uPxyL4L3$ENBh|)HuP&SBj@+NAqyfrENMF=(A;2aY6K*`E!4V4l5l|xMO<^XK^uFup$n&LZ|M4{)oq-r&E9tT+ zS5^^s{`N0Sr=AS;u^~$aK>Kl^7`eFQT;S@99R}<6^5?Y3vCU>HfVI7O)9;J9?rne) zf&?yjCP|#jKXOKu5pRQGpoxeIcMiZ>lF1*p3!au>c+ zt9l669ysvovF1sst|we4wSRL1VtrOvc4pC}w=nuDh`AAS+M?BFuF@3>*b0A61^zk3 zP6XEMxzsX`o0nBvy9wpmVs6TN!lKyx9(Fe(CN$*f#wC`-NIZc7o*Y9nRm>tWS_u8y zI478WlZCao7-L~Ly!hvEdGC2e%}y_4<;AZoO0-{>PlceYPjng=(tJbp4XDQ|#nHe1 zQZ_}(Z3ol>MUwHtwmzR_J^WZ{i&j{GEyqWs>%C31eK6;#1qvQO%oM+N;V$8HrmAHuyy?s5guGM`Vh z)C?s2G-suYpdOdli}w5(GnJ1R$o)f6I@hsu`0nZlkhN9vp@Z@|0D(sUE%*_B{s*`rXl!BFRS}6~AjVZEX(R$o-hCy2 zB3R}*$3F4APDn62wQ?;J##eNef)Wtqmu)6pAFPLCfv8E~C%r6WV|yKmIKW82&V*8U zOGtZwd=LyPa&A-b?FCxRK%-&WPEYgq#uqqB{{Y!~`NBhBEE&wfKpj7h$JmnHRy(IU zO~arV&_6GGequKUkXk(|04vnY!BWr{ArdZ%PR4VdX7IlGKxI-_vqs~epKVm!a+0de zbT+^Pp_$9qlOZI5lCp&^8EnuGSz_N^qHt>bcUKM%4{-_3EuG*TeO-Tv>30j0uVtU+ zh4aS5#TxRP{Bz?aF|ov~&p{Wa2q7K7HjFLn3bL93M8>pf!bJex@W0yZ0wf`jBD_37 z%o0#^-Ro6d7Cns^I@J{3yo-q3bn0UY2(F{h@0ITwQJu!k+ZX85AA(m*m%cR@0?*aa zG{b-k-Jp$#Y7;TH87#SPAf(}CEDI<_fWyvv>m2l0g#mFde=xYK6)eDLcH`n|zduVY z%ox$<@@2sFIFD8Xz zha+$nD5|j-zI4z72#!$Pqugb7@SH(J(V;z=0fj(4WFnyKqV*Ei3>|9k=eM65@$bxY z@6U$Q*bZhbKTC-BJU;Yl_yP@mXWAljnb|l0aD|hg#cBIKgZ(UlP&c}uhsw(zjVx>{ z_>|t`Ho;9uXnd~q%V@TPP@iNR-V_Ki)=!zdGVf_LIe?|CbysHgjP6wXR~LJG)+Tsk zOpnPlbe34r(@#hv zkZv9osISAD(gHfE(yIR&8@Z}(rdkTL$BE%opFCL*rfVfFTl(k2-B=H-W)S^c@RRdt z*a1}-M_|7}ZI~A%DP$<7A$^JvHP&$_yiEY54=f*xp^|%`e<77EGG2BbN$O^VytHn=a{E4s zD0>9}{}DgEEyH(`@YiSCi>=#a~S(ygmzXvWArt)%z#&9r{;suK>wxyWl3E zEfJL@-(fcZ%NtZVEG;c*{))l=j){pTsc>5-g2|T0alxgb+qwk$)2-cT^`qhh)OULQ zn<~F!(cz9w7PQBCt|Y)Iwd1`bEFZm{5dpz~2ry-F#i>WX8`>9S=jNOFf$iPz>SGC5 z{q+2{_T|)VVxM+bf0AxxmVlgyJHlY)dpWrEcY*5xvbZ>=w>aU)jJ)~%@m*YfqQkzJ zM36sd)Nq|{>?9c7j5+geZU%ZO&KbI7`)H`YcQ|n(bJ|)|9yd#fv{o!8%HZ^bvPlFx^W$l2!8s|}Byp|jC zpV2G_`nz4EjW5HD+iTTimI?p3FE~M@BJm8}?_djA44K=^OtW4qOg*!DxBC&tYz{gi z%y<{TrU0!wxt0vwz*lB}zCBPq)$UgAfQ0=W(Ht&@^pr4gqvz~3G&93VKkL8$3@jhO zw!$Ss$TjFWGsqPWQ_SjrOlLD8{ zTE1smp%e2c3=JEnRFuuNGSx*2;mVp{r#iS;w4GC6H^#gDn%^8b-@lir_TS&+h2Mmt z>;ea=+_{6w2(9Z&#x%*ei0Veto~QuvBY>)M#JgEdnKc6UO07{fcK3yae0o^ww0TuS z4Bms)d=JRN9b30GXTPm^()}ttkPC1+IyDmnYEtfS!&ZxB174XigUTn2gA| znft0CL`&v~Kj--S#nX-#yWbN32NccT0U)juE%|?JhRB-76&7IR9^oytlE&7WbG8DG zn?bQ>i@#}o*Bj}4!c;i)-0}zfvS{p5LF)8x|CW+}CRN1pKcC)N-q@%tEv*52ALO3- zj;M_MKLeMqasU1ws2d=SgpZ2k|IF;aU-xJPykzd&0X)LLFp+{G`0lYjz}oLYk(uIw zE3VYi)U%^T5Jp@Ns=6NVv48PZOPH1>QhTm7Y4!K=?f^p`DAstwtqY26*T4{Y8e@qh zuWW|neZgRGdj*`6r-Io5vj6!+UE+V8Mc;`4D@w2EOS!q)7Ho zPLd!jEwR(~KVV42X<}}I^g^p7EHMwv(JdRWsuFGM$%L9tOX8Q9?4A6ZWG#IH+TC7r zCj3OPetq$*B`;L>ZvFsqj3myx7szgJJqbE00R69*+!&zus1`#|+48)tUuAZ~{v#^W z#ei!-f)?=J3)Z0BFpXt_Fm?uF_qRbiwdEHREP|4uZa$&dQMc^t_Ubq_CxCVh73VpC z#ETp>b^K=a?P14<8V3P0X*6BTvpA@K-I2RrQ5mxw)2Brq_%m_#`i(lA*-vECiit?s z1hfiZ90yAY=0KPHFi54LErDQxi3Ef)gt`eHY+!2{jr@$wA_^L+O*C^_vZkR^bZESl zHMhF}sytAt;FQTumfM6<7fa`o2sU+JQ^0|czWU=8LXQ~`SsH0><5;b=egV2V7Y<75 zznx{!?IyM55zBX{8vh$eCgAeE;|LHY$q+>jt5UD(m4O~ER;{p zgCmOtc&;%}9eOuepE2l|3AXbf?-9b6x zJlywm`&Cz%=m^ zk;wxp`k{V|9(Z91gkstw#B^0+aR3^5fJSLVkSeBZs*RE+f5Y7NKjdr8 zKQm@}OZQpV3sZyP**qS;#Z{Uu@CEd>6_()BS&kL`b6}Cs592(B_tK_2f)vriUXP_P z;k>na)9u2LP3r#gGlaMRzyyV)ZvefAtxGY}2FP#r9CJuY<{QhTkHciz0hkbN6eAAP zO7X&m+fG4`<&5Yc_h=KSIKhJGC+hud&b>|UBJ=Wi)mb!ju3-GS4~TtZ?(c0lBVBN{ z8I>O&v!x!0_x?O5C=ErFHYmeAd+N_Um-8(@3N8$B0)|T!WKVJcW_)_G*FJ6_#B()+ ztZ*eH67Oyv6wIz_M^#b7Q&qerGR@lzW6piRcV6W+mMw8@(#ycI8klqbB zS&Ri|-a`P(4bpIjL7Q{c z_div+88G@0Uz_Dc(h&}819T57^cK2(!4vrK5qEW62At#A+ zz)-KR+Y;PG;Ewd{uncIjs+na-OOzuR2-f zA{x2y4XVtt#l1kJ0C-ZhZ;D;O=0;+z3eV6p&Ni#-&jaque+M-)adt%gZPmr zC`vPndGt7sb1?>_b;`p{))&lb7I6ge)BgZ@=);(yo@Z|X|1=@-wSA<0>{rGCY{^Cz zC|Ej<(=2p{8He35UuC~`zFPfa%iPHf>uw+0leNz;-!f@#0h^!Yew~kYIvhbgl+5`6 z;d?W$>AX02umzOpPvhgi%NuNCSC~W)?Jd}x6T`z>R`yLWV;~0c2X3+xB=3;hvfw;)SpcwO z*6P0-ENz;31@GD@q9jE0z zINgA#^W`KqtD_!>=_}0pdi~(1TLVJS<4jxZ1Ajf`p{%p0GWH(o5pWrW%jS>f;(0h+ z+~3lki3igbdk}h6e)^Uh@2{cC?<9prU@;!Rph69evhCv11w@Y+mrOD@3p{}>!(#yE zwy}_yCc4BrqJXWdIS5jTCy2L0(81d~(+PABAZ@XH4RHu!yxAtrRDb!M_COFzH&|~` z_OyhNN5*2#=(QxQGv$Y?Nof>Dvl|;5flGOjDw|JilDl9nMcWHDHl5?XGD&<@#2G{d(QR99tTEPBPq{ht>@yc2&=vL zDwpb3Kv`F{9S(S@Ge(uWISNHdP%3UJxFQfpfX1)pe#BI`u?4{bc8dx|$YJZaye8ryUCp`+ti zgTD<{3B<&~8GOxNF;}XYUhl->kn?Rnw;PA#7HRvhIi8__49@bV(GN>z@q0_sC*P%k zEHZnpio)5)8yQvke%m+D}jDk}btqv;jke2fRkLY$f zH9O@7aGjj~KqYfHaLhWRzn3VKd6AUJ=O)9Lfl66TQbc&Bwlj7~e6kzm67BxHsQX+G zoR%$Yq6lVX==0~pKi^Kj8oAjY%seCmQ5mz!_HKPfXlc#lZ7}~->_GMtlx*CuG)YC% z3{xSIsD1a7@dh4kD+?OZki}n{P;fBONRE~aH})=}{aG0LVJo?n0yIwkP*E0grpB3`*2-)8?5>Pwitv;qeCU#*PWDe0IRut+@PueS(509T*WUI$ zy>lYiIwZtbizd3>cLS~11@?nW5cX2-o+;BS`o675W4u|PsR^;&T^HkY=55o_@iNLo zNfRnF_7EhOCHVtJg6_^GmN2XX5!eaXC2KlV0w;(`CR1WUD6sp#9dG#Dx*B)BmJw%M zNAOZ8vi2ThUuRlUePaFK#BbmE4u=47(Y33s^7o>!cnOqdmFud)3}VxB<@B-?edK>I z1c7ld7~Rlmncm$k^8I%4gO|ruhNTWbGeVeMg%&63`n{zg1eLRm2><+_rmj1l%KvX0 zhpe(PLdeRfjBMGQLS{t5K}Zy$?AvWkoxBP-dC!+GBK_xZh^ z=hr{}>YV%B_vik6-tX(W-q)4gwnaiF^@6|o63B|gP3B?1rLY|>EO@Nc)c6Dd#0E9b zJni8agRu`=ez)7$$MhDZM&#|s4W0Qjz$B=n#2~o87i%=7E$3cj&T@9^YOQdiuGE$v zfFW?>>sqXCeXRroZnlrKaQaq)na&P!GQY10geX?2PH_?1k5IX(9;{qc9*vc7Z`v0! z%(bvlWjyKDF$d4p>%ssV7umH|?yt03WLLOrwL*Ni87noz;WXxtLrq#sO^9zN2#gGS ze^ttv(i1`s_t$ie4i;XAEQEW}2Am3>IPhY}Rnq8Jw;i86cZXL;K7EKzJW4ZaJl>&RKVIxW&U*gELHkg-Y1|ykoS7 zjIP;nCVwI1iLxNj%`wW+A_0P9=U2yne_=TIA^8ISlU%d1NQ`}c*2e>lSxkHRl0)?m zL)*K>O1^QQFzRwe%l1vfal5qjo%?FOP9C^(QEVvTW{i}=54ZzPFbsaF;Nb8!iRc?Y zUF}y?G`l(4&wO{3XcwRDT5DZr{dHcyLaLQhHmv`$Qn~U{-+m~Ij>%=Gb8qL5-yAn) zGP*tYXk|Mn@O+Rpmyxa^PTHu!7l-6-;J~t3h_pb zn02SwH7e0lmW+Ge@g82|;4%Rx?lI-vX3wM97SQZ-`{5D*`f6LvO&m=A2kbKHs<#t8 z(*BaRvvknDod37q#(jjwkO*V<`XR~UN&tz%Zp*7#sz1NR#?1-EuG@sZzqTB&Y0A~| zPXrj;@MKz&-Av0Y@g`(V5U#1l@cFh~pGeE;l3KjBPb!lNCHU86mz`jR^V${Fydvuro#t3wgsC2^{@@#q2 zWj(eYdIG)LPm6Lgs=)(_{|j08fbw*IrK(?XJeCq%zq1xTrCm!AAC6W5L?xz*!z+Qj zQN2PfRfA)2tSE^)8Z?@L1-( z^LyZad?;#QGnz`-mA-N97jc((7(#@?q0IGNa_vuZsrU9^VdY|Ql${CYj~?l9Z18Rb zfh`Mw*9$K#PbcqI0aBKrwAhUR%1dZ+(*W3`&l+aFPo3z=J_oBlA3ct`_534WzKv@+ zDOsX_P~^ArXk6zwxx!41zY0eCTp>NKFV++%_vFYV%cL|?@fg$a=2y!uO?`Wp%9Ndt z342_IZLbqGTVxB-Y9;|XWhrXct}!PbVqmDX=TB$1PliR92jN0A-JQqjw4s6pHlzhH zo89|lQav+Ug~?*1Z`JZe85Y~fGaj7R5xvd`Hy}{InS$&dYb|g4&Fsix^NK5D;(1(W z#9Y!EQ*Dbyo*Y0@XHM+#ZHuK2lD4yMyD#WUrwg}!Jrk$I4o7g+mtnCPT@!E% zevZtxfj6ru5F38$Q$3LQoV=WEQ|>VuoSO~xrvgAUu|E|1W0^pWbt`aa>oPwb_wOvO zNqW?;Upz!%GK1+Syx&;@4Z6(_57yNvhXby8k1c`fHs!fEAzQ;b#wKGh9d1I(fa9{d|A(_&0@ZzHb9-=&)d9c+yQt`q` zN{V))s~P=P?EKCTONY%I)~F|cr)U0m?<>Fz_umwIP{|Ef6*ap@e87bm88f@L1I_>-OSQLsu>F}GtqkoQ`z%au@So-xRpWzLY z)~Gx+eu6nM&-n-M>PiyT#Bd2NrWOreM+H65Cu}yRgGD|th^yX7;ekK;7#4Ew4mfri zJmh~9q3#QF-ymc%Vv$=9KE$8g#=+;ft?1NSAwqQSmv*I1hs&JkhoWN$#35=2(qv zy}$K?JTPfJGD^nxtgJbFwCD+Jb?vMKHMxxDQC-SXs;ia#qhz`b zeol1{b^wdKj2Yd7O0rIQ`?AXFXZ>1=*|E@rxm=DeBIL~#hfG6JL5=CfiC5JIpSSgc zZpAZfJTX9uK;f03F%Kw*pieDY&2`-{$!!bHnuR3lcXWq+3L7GGVM|_DDp842=E@Il z6p^JV-=rG|B$-(jBN^WPLlMP%B0e-PyD2(Ue&6UuxGG)B;q}~b&kks#DuHs!)U?ug zV7~X$!po})KOjsM$emx4VapM8qr#qIlylT>!+i>{LUGIY-GVg7N9!7xJ;`Qfzij-t z%)si_d3^1V6r}9hP`jpbiM7CoZK)yZAFI@D zmtD}3I?kY9gO2tA%=j<+^P}VnZ8YcaM0^h~s^{;{M8?AgP|i~TZ?Ocjs%1MEVwXn? ztEEW>O;sp$S~fs%qW+@s3jYMf3{1-e%CakY-)%M*U;UV_L{8QH_q0a9orjPT0byBF z@A<;sb=nATa0*0R$Wc)zQ$C?`mtDrXUK{`mW%Hrscg{KeV*wB^LKTFZO}51OigmTb>;mH;N1VP7Qp_DT6q-eYqTZ zIgy#w?YtRt{1%IZ@e(s{auKf&-LpH@CvVn<{hfjL31DRRd-om&tO`z;P)qB{L7j!2$JKRsOgV(G;eV!@vj(5H=5-jBEM5%QxvO%nu62w` z669jsn^qThA`-r~JR-sY1bWJxi~9U6mS zFOXjW>N3Am)?HWD zDJhD}bM#PeRg-R1I_Lxi1^IGBDKqpzdP4%O?>N}^n-|mssn>4%k2lw(pvfg^3@C!K zE)28Vo2IGdd2W4c?Jqn>AH1%SXK6h3fYGJ`1GM;dQj%<}QW$iC8_zxZz4=ia5v*e} z*K}E7+#jGao){aj>c2^OA?OUR%0hjiX?D??Q#aF_Iw(a2%d9mSfOGEO_(nqJC@U>( z8?CG1<#n-r4+LOuHA5XmRSk88COn|}A`<$EuO#fk!A$4eg!B#fLq=CLJDw)bNk>Ks zfHxf(GAXmHU?WDO^Ns4CW$j$4pMH*h`Qg|8(ZL^3Pa;b8hZG(P-=_llAe}pbc4EIU zxTcL(uKv>iuRAhhh#}X)Qv8=Aw;;undX1zvq{>EKA$tGP{IO<$zJ=^w~u!*^1r$B&D@YJya#`h~z@VBcQC zHa6B?XobYPZ!Zm-7pF6yH2}L({O@YH-oKn=Z;KiPMbj#&|TQ;<@+$rdbT+) zud2!mpewwFj@=ZRNd{VOtA7CP1hL>YMHaQ~C;1A!t-~`U(evBEtCF`!^%`&JP=vYf zmXy^AGhCpc_K`2Z0GL83#vJNCic_-Z*8)QBR=jd&0psrbclK>LUTYHy0a$B)4>B#p z-h3_Dln05g2JeR{i)q+z%3O7yQTt>PfEs%FrS;=VT)ydlyuP6BcTpOXPNV|98P^82 z(Y3>AR^8J_mcjNb-vz^`l-OCbG0MC~O=KfauC>y$>NdUjQ(RFmJ2`f++1G104WvB2 zK*!_q-K{76dH^20512L=3&gRj1~&$;Q2M&nO-kz0E`%4~|6b{6UKD?Lu$${VSq*;v zm@>m4p$Q80Z+b^iwAGV~sz702$5nMlU=T)byPp8855B1#zwfylgV-RDTNe z?2Ae-P901za_WFrk_*f)oAisK#?c!Dr85V_9~Eoq+I?y2{)(tyRXEUL+EV&k1MEX(vLF2v6 z!7oPyOK*9KR|&?sXc;*=$sIExg$Ats_Ii=}?Z0_TI^yW}PDh*j9fG_^xu z!p0#D^yFhsX{?s2;_OQ806Zwj$4!aI_(5x>AseTSAdYNsz{vp#xSNqRc&jH-5Kr?J zB2jDkWlhRmM=zjPVeo07(3vHBG&)#_x*A4g?~R^D*Qs-W&=e1_*%62xKI$r{8yMee zy`2@**+p#`QN9!~MZs_O-Av^6%n4d)9X4w$l5^p~bVZ{G-;(t4SV@_@7$4t6PHt}Q ze+E~Nnai_xEG;j$r!ttQju$f`w~8xjBmVJ$yI+;0Ff)X-4-O4&fw77`c^FFvE9%53 z*lWds6;IVFl_&UaBNle>$Kod-0lU7)K>D6Tq=6ah#Pz^Eb7p7S#E!10^0Tl>p9t|h zFD}Ma>fokUj&qo?sdVT8t&U-njc@ZtuNZ7CXgRZJHD=+$SC8>1Q4cr$fR}e0=Es;fgCY+1$wzMY1qVV9MmqmvVZe)J2M67 zw0xFLq!{gK1>Wxhkp4XIVLjw8OlE#ji7`Wg!4Hm~*-do+DHJ~xPr_+4pS`&vn?;w> z6YC98i*JL*a+zPfKY_wW@*G%@t+H?-Lb45(q<4zvz75@e6r7GwX=#(qRpHm)W?2{W zk!LN!`q)gfsSG~EuUpAakCeMZ=s9w^bq|+E-hpv&C9(Xv6!L7Ur_JyANi^J&k(vkd z-<7qslnLG`|BL2tQbRtr^Y~d*Qs~bX-W9JFJ7I9@)(MaJh7yeyN?`e;@+t(p>z)Yh zVb(bBs`hTCZk%)@c3S$jUk_DAoR=LoINcao-6Y|4xp$_x=4S$n+ceo8Xe#!Ay?V-- zNev}kffu41i?H|H`!rweYDxvr?HU5=%g0K~@gbB7phwwg!A=pMY;+2*9E=sVgGf-i zwjBUsE7P>;?;w+_PK!g6A)^(8sEWQ)-Nmp>YKv3r&??M^s5F7sGc~M{XPU>vOa2hw z5%J=p!a_-kxVI-aoj{-1tiMs%Sy@_&q;VEyoq4ZO)97l}Ve35iS^?DT2wa=^n^eJn z%NZ0iz8uum_lSNbQP7OmdBH0RoQ~pfEF~xucSAUi;0u3-aqQoRRAJEw7=n&!uRfcU zKy!f}JISgY)N?v$k@RdSpaTPYe=J#apcLwg=gI03SH>Q!HH=m3^G6}-2pb5OcLpFy zRqGL)ep0e`Oe^;Vz~baA!T+Y|rk>kN@!O@&&QG{W%gl`DfR=>s4uLve!PI9lXogW1 zd|RQD7&PQR(*;?`(#QuwQ=+;xpox$iUv6H{^qW6L6eE%KYmtbN7HFJg9VR3;AA(8G&)% zlx#Wu3M`I71ezyV%~XCPb zIt+w$m=e6v8{}iG0D>~A5KQ9dNm)AnGep&>666oll>L`BV~D*wa4#@oi=!Ltjxc;6Oak;dfscyDfL{yP zbH*C&c&)+R1n;jPa7ezucopMqEb;PtR(A~3CK#*!rQ!r)=B7I~@7O8Z@x!J|;Ns{t z7((0y=FQqciyPo;2_%ceLY!W-pheID*gr@ms^@vVB9?Nzy*s+xgA;wD)@$L!=xc5V z53nr`&T?em0YNV)t&#<<+iLN^C)hT^1Hb6>$%EBO^azZJEvy?8Yf-A-c76K^1UHRWuUS*%vy!gv>;|Hcb!Y# zBfGkK$35gAy=qp&3!D{!c#jBKO1WC&-YyYy4tnz(wfX;6wfPyAv_x{il^M%JCj*{y zU>i~wLJSV7aaNZP!ao2L-nYiN1fS$e{)4~g+a)#-#+2*^HBjkHZl7+ZCC9N#@(D}x zMSu|DoZD0r7IrgWm%+D;x7Q%e3-p=GkT-w%1K4Va4pSb8>%d1qSrbW?W)%VNFe0Fw z`10@btgG!gaYBj$7D` z;gY~M!`M$2q}4yz62aF>`09fgVcSu=Mz~M99l-bZXDCHnXoflmGD}Jz*u*0YAj7S{ z484duc!d|2GC`MfOyi}vunb*$H)$sqNPYIpmn9})EeKMqHa+-l0=(wM#Sp?QRO?f$ zZF4(wP+7t`)Cf=j9On~H>Dnb|9!JIf-^KgY!lgpV2N}wOQ+5=)3dts2PVsPUrvG(q ze67%b#Dvoxm|tm^(2e^4{|*ZTZ3bedN6YMJzZt~`=5PKrL~4hnEJ2s+OxO(}693$j VKDgrEutEa=bhHh$$~0`l{|B@zSO5S3 literal 0 HcmV?d00001 diff --git a/webapps/docs/images/design.gif b/webapps/docs/images/design.gif new file mode 100644 index 0000000000000000000000000000000000000000..f5db0a9fc783924486176ce157924fba53df73fc GIT binary patch literal 608 zcmZ?wbhEHb6k!lyIL5&6|NsAgK=A*+y}iAiot?eCt%HMuEs*J8ujLw+y7Ptq{{K!J zf7q}4X0-UF?)+zRlW&T4U*v2)!e6yXwRo0{SJEe-5&!>cxrG=mdLiwZn4h0tkeBaZ zXTN^^`uFeOe|Y!)!-o&&&!6AFfB%LJ8x|~BuzKPA|Ns7#x-xkKyruV1%sHz7ZzCM7sKSh-eh--RQa&hK1ucJ=HF%O;;& z(71m>$AyWdcbfMc+`Vw!lBw%v)-7nyom8CNS*TU6fAIXF^#?X=1X{Rn{hEX8E?l^< zY2Dfj=YX!b$S}x&4#+*AxL{yE+Q87%#Ms=-#MIW++}hp8(Az(WfqCjw7G_pfW;S+? zK29!f9$r3v0YM>Q5mB)&afwAsB$uyTEw!$TO?uDXeKN9r%yRNakI5@2C^8>9BCn*Z zBB!dVrgo-TU7hjpQ4LKkZEYR#ysImZEe>aVg^r Vos(-!G%qcgJt^mrj0gvVH2@9_)ye<> literal 0 HcmV?d00001 diff --git a/webapps/docs/images/docs-stylesheet.css b/webapps/docs/images/docs-stylesheet.css new file mode 100644 index 0000000..768e2ed --- /dev/null +++ b/webapps/docs/images/docs-stylesheet.css @@ -0,0 +1,303 @@ +@charset "utf-8"; +/* + 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. +*/ + +/* Fonts */ +@import url("fonts/fonts.css"); + +/* General style */ + +h1, h2, h3, h4, h5, h6, th { + font-weight: 600; +} + +body { + margin: 0; +} + +body, input { + font-family: 'Open Sans', sans-serif; + font-size: 10.5pt; +} + +code, pre { + font-family: Consolas, monospace; +} + +img { + border: 0; +} + +table { + border-collapse: collapse; + text-align: left; +} +table *:not(table) { + /* Prevent border-collapsing for table child elements like

    */ + border-collapse: separate; +} + +th { + text-align: left; +} + +main { + /* Remove this once all IEs support
    element */ + display: block; +} + + +/* Layout */ + +#wrapper { + min-width: 400px; +} + +#header { + border-bottom: 1px solid #bbb; +} + +@media not print { + #header { + box-shadow: 0 0 7px #aaa; + } +} + +#header > div { + padding-left: 15px; + padding-right: 15px; + + /* Work-around for old browsers: */ + background-color: #F8F3E4; + background: linear-gradient(to bottom, #ffffff -10%, #F8F3E4 100%); + position: relative; +} + +#header .logo { + float: left; + padding-top: 10px; + min-width: 190px; +} + +#header .logo img{ + /* To avoid that the Font Descender being added to the parent div's height */ + vertical-align: middle; +} + +#header .asfLogo { + float: right; + position: relative; + top: 8px; +} + +#header h1 { + margin-top: 0.6em; + margin-bottom: 0; +} + +#header .versionInfo { + font-size: 13pt; + margin-bottom: 1em; +} + +#middle { + display: table; + table-layout: fixed; + margin: 0; + width: 100%; +} +#middle > div { display: table-row; } +#middle > div > div { display: table-cell; vertical-align: top; } + + + +#mainLeft { + width: 190px; +} + +#mainLeft > div { + margin-top: -1px; /* to overwrite border of element above */ + padding-left: 16px; + padding-right: 14px; + padding-top: 6px; + padding-bottom: 15px; + background-color: #F8F3E4; + border-right: 1px solid #bbb; + border-bottom: 1px solid #bbb; + font-size: 10pt; + border-bottom-right-radius: 20px; + box-shadow: 0 0 5px #aaa; +} + +#mainLeft h2 { + margin-bottom: 0.2em; + font-size: 1.2em; +} + +#mainLeft ul { + padding: 0; + margin: 0; + list-style-type: none; +} + +#mainLeft ul a { + text-indent: -0.6em; + padding-left: 1.4em; + display: block; + text-decoration: none; + color: #444; +} +#mainLeft ul a:hover { + color: #000; + background-color: #D1c9b9; +} + +#mainRight { + padding-left: 14px; + padding-right: 20px; + +} + +#footer { + margin-top: 30px; + padding-top: 20px; + padding-bottom: 20px; + padding-left: 20px; + padding-right: 20px; + border-top: 1px solid #ccc; + color: #444; + text-align: center; + /* font-style: italic; */ + font-size: 9pt; +} + + +/* Content */ + +#content div.text { + padding-left: 1em; + padding-left: 1em; +} + +#content h3, #content h4, #content h5, #content h6 { + padding-left: 5px; + padding-right: 5px; + background-color: #eaeaea; +} + +@media not print { + #content h3, #content h4, #content h5, #content h6 { + border: 1px solid #ccc; + border-radius: 4px; + } +} + +#content h4, #content h5, #content h6 { + background-color: #f6f6f6; +} + +code { + background-color: rgb(224,255,255); +} + +div.codeBox pre code, code.attributeName, code.propertyName, code.noHighlight, .noHighlight code { + background-color: transparent; +} +div.codeBox { + overflow: auto; + margin: 1em 0; +} +div.codeBox pre { + margin: 0; + padding: 4px; + border: 1px solid #999; + border-radius: 5px; + background-color: #eff8ff; + display: table; /* To prevent
    s from taking the complete available width. */
    +  /*
    +  When it is officially supported, use the following CSS instead of display: table
    +  to prevent big 
    s from exceeding the browser window:
    +  max-width: available;
    +  width: min-content;
    +  */
    +}
    +
    +div.codeBox pre.wrap {
    +  white-space: pre-wrap;
    +}
    +
    +
    +table.defaultTable tr, table.detail-table tr {
    +    border: 1px solid #CCC;
    +}
    +
    +table.defaultTable tr:nth-child(even), table.detail-table tr:nth-child(even) {
    +    background-color: #FAFBFF;
    +}
    +
    +table.defaultTable tr:nth-child(odd), table.detail-table tr:nth-child(odd) {
    +    background-color: #EEEFFF;
    +}
    +
    +table.defaultTable th, table.detail-table th {
    +  background-color: #88b;
    +  color: #fff;
    +}
    +
    +table.defaultTable th, table.defaultTable td, table.detail-table th, table.detail-table td {
    +  padding: 5px 8px;
    +}
    +
    +
    +p.notice {
    +  border: 1px solid rgb(255, 0, 0);
    +  background-color: rgb(238, 238, 238);
    +  color: rgb(0, 51, 102);
    +  padding: 0.5em;
    +  margin: 1em 2em 1em 1em;
    +}
    +
    +
    +/* Changelog-Styles */
    +
    +ul.changelog {
    +  padding-left: 1em;
    +  list-style-type: none;
    +}
    +
    +ul.changelog  li{
    +  padding-top: 5px;
    +  padding-bottom: 5px;
    +}
    +
    +ul.changelog img {
    +  vertical-align: middle
    +}
    +
    +
    +/* Printer-only Styles */
    +@media print {
    +    .noPrint { display: none; }
    +    #middle > div > div#mainLeft { display: none; }
    +    a { color: inherit; text-decoration: none; }
    +}
    +
    +/* Fix for Comments section which contains a 

    */ +#comments_thread h1, #comments_thread h2, #comments_thread h3, #comments_thread h4, #comments_thread h5, #comments_thread h6 { + border: none; + background-color: transparent; +} \ No newline at end of file diff --git a/webapps/docs/images/docs.gif b/webapps/docs/images/docs.gif new file mode 100644 index 0000000000000000000000000000000000000000..d64a4a18c405e2c10ab791363f9f7acd16f3d433 GIT binary patch literal 261 zcmZ?wbhEHb6k!lyXklRZ|NsBLfBy^&3_g7L@ZiA%eSLjHL&F0H4xB%K{{H>@`}gm^ zaN)w=|Nrz24bGiAckjXd0|)l~`~Sb8p~273FFrp0;lqa;Hf-?s_kVc*!TptFK^W zYAveoFcA_K5zXln5UZ$U78jG4CN@JuQj&p5NQ}L2!9raIDJdi4wVO8!3NGHP$Y2cs DYrJfE literal 0 HcmV?d00001 diff --git a/webapps/docs/images/fix.gif b/webapps/docs/images/fix.gif new file mode 100644 index 0000000000000000000000000000000000000000..d59ad642ba46c0bc1af17ebd2485722e92d6c70e GIT binary patch literal 345 zcmZ?wbhEHb6k!lySjxcg|NsAg|NhCz$;r#h*Vot2pFe-Xgb57|4F?V!*sx)Pf`Wp- zzyHC52YafE7nQ{H_?deNa@Yt7|Ns5_|Np=BjScHJZqShyb`oKj>}~Xa{jy+g9z6-E ze;_me7)uCF^D_FsZh9ae>;K=s{^BeQRBRf^&21nd@qhh{Kt8trzduj* zHV)$93F2e56B6$9w|uc@qp`Ti|35%m2ml=hAOQJ=fvwJ=K|nx4s{h22qR1Gdx!wm8 zv|72?TbvF$Sny2pWe}U_A+XHvXu`zQIrE#HnF2E`LX!mA98j7>H; OF){5sba63^6 literal 0 HcmV?d00001 diff --git a/webapps/docs/images/fonts/OpenSans400.woff b/webapps/docs/images/fonts/OpenSans400.woff new file mode 100644 index 0000000000000000000000000000000000000000..55b25f867099eb26c436b1c9fecff14a51290d46 GIT binary patch literal 21956 zcmYg%V~{946YlD)ZJTFp>#S|twr$(CZQHhO+uptVe)s;ksd_5uXS&l}>8Y7aGI5a+ z76t$W_$^9<0Pz3S(UHIM|Cs-z|Gx+eD@p+X09yWX*#AMZ(hWyOftKNy3;mU~exaMk z-2-c2rDyxgo&Lh~7xj^+n`s8lj`#ooz_PzOYQM;blR_^vwl%T(C5CDLO{8xwZ7c6fq z$m^!RcHozD&eZ>Fj zt5sdsOaooxJA=&L-tpc(j)cj{P9%neLq$h0E&dX2QiA-05zC7W!%e)U?3n{5tuse_dT; zUtJ7PVNiHg;6r-#B!+&Ca@ZNdveD-#Je0Dy)zH~1Fq~KV1R9|$51HQo^ z;F$875B@}2LfaZeO7i(bXaUYrQdL@BVrOb@a&>xs0{Q>zC(PI1M~s)BXQ;QohYS}V z|66W;j+w5$&e87v4jL>xOj>MwjGC;x%+l=q3>z&yO;c@sjhn5#&C~7u4d}<;N06tt zhY%+>M_*@m2N4Esb(OW1U^1!EX*yP4G@(+h-DG_A?u5l+z1?oK5q;(1e6uwon;4(& zmZz`B&my3PDlkG7tPdLeXkU9_Z;q~fX)ZYpT<2hc5udJJg2!XOf4JsQB7I}XHfsBJ zJ|w}HHqLm0q3)jU(eX|ud6~fxr8l10)&PgYtv%XnTM0=H15hI25&b*PNP6%lw+36F z#1d*Pb=gLIfK<59gwRMB=-)knckqY22=EY#B%dbZKP^A%KRxd)h~$KKHgLddzQF&S z7?~J0jZ*t&XJ$v}67mA_;^K1hpa7(}@qYf7Lnoyfg$-fkZ4^*wbV7x3x+Q`N>&t%K z6vy~xJ_<{#u~nx9eP~9?Q+`5H1NZ$zf#R&Ysz}kF6f%2y_M>%ypAhVI4R+F%$F3wP ze8weki=7wm>z8dB^N(Bn*$tMMUY_^7H?M+jt%ol2BhIF#x2s{+3yTUJpQj_V z7Zo1b6jWYH)ax;stqv#$w~y(`$HhZIXHQEeMtwVF#9jeBVMs!qh(g)|_cr3splTJG zw|Z3nP>@|Rg~X?JKSK~gWZ6L(M_^d~yrTp-Dkbx>YU(y>mQ~QASfKCw!qbO<b{$w%aO~ z;D1h=9RET&A8OJy)nogj#2+uoKHTw`xeH7+d63t&Of^RnD404H#zf|b%yRahy$4|u zS*5yNv@&E*MKmS#u0mFp&#@!WsSC!>UK9gQ!?X=HBN%TN=TL$nAwM+eOX;JBA4u?G zcgl1?q4VL-Jsmm1KQocTq5Z{loRqn)$rVA?ACTr+kW`H;p(CSQ*($34OlW4I0cK6D z0D{7U>(m`$JrRGkJ1ruSrCOU$x$Ns$857v+bnV5h?EP3ww5zKtQ4fhY{N^Es1-}a>Dq6uLc%U3mj~OkUeS@Uki>6~xaf)@qz5%*;y#+7$f`+pvCCoE(Gr2BS4Jc z2mYF8YcbZ*f9a_a=XrdF|E$*9#AqzB-qb67a;>442&c!5Ig5Ag;bZlVV04-K&6#gf zJA7dq2Rl?&T??oDqLGSD>0DMQLMexfGEEXKl9}BcYUFA5mXyVac`Zo7 zbDH2Di>6Z9HWtuOTgvrFM6dItbs&cm`KkXw5c9|K#W~B3VtSm&(=RiwM*>EEw$7yW z)eA&6jCfHFT{6kd5z9~hlOkOz&2s{+vkZTNHqH)2FfzASG+GKAS6fIl{TLffJ#z!ZT{hT`u6jVGdMN&rk4xC#ZC#LVZr*Tdd(LI zxVM;%8t{e1${1PRo$#BGvP{?<{5Nb*M-f9wM?x8BgV3F{Zx9%B5PI(xea(IO1#6q` zD{!?Rhn(|yHu#L<^tV@nr1^*92aMu{fa1TG8Xy2L05||701N=w&kq2cYi5uqn69GG z`)i^~^Gm$c4%JvZ2>6(>f~lH1c-|0ttdM&`{-bYbs4pO)l=>-nC<1T@b}(TG_+NZN z!hFIJ3ZJ2YgG06_E-1s0pO@_oDHigViTX7Xo9)S!4bKO7eynS)k)=-p!8kx8P)o%s zQfw0@y-FgEJRR8+d}I_9qjc(0I?@Ttz98eAnac_52<}A@2QPNm4yoJ1!oB^wZYUk@ zwQ6$N)D4)LJua8?6l52ONAiE6xqsCqo+m6q$tiM06c5Qux$>Dq2QqnT(Wsb(a& z7h!BP7wtVc#w77lrtu)XWF+S`(kDXTUl=D*LgHtK`Fxb*JF^629cMs|8?dZ8^S3#mOhB?e4NlDkL_8r}|c67Ox$p{kG7Fkd0Gd|Ajco~;7o{YE6 ziZF4F-oibT`}->^2?PMPBpKsvVSUDPIrbESSNEwBckWvnvJD(^RLl6QFi z_FSQVOYmO#AFRHGpF=v=d5U3l1SGWA-Pjtx&D&>c)s+HOV17q8wAV&B5^QkhBprCo zd+=1JhFqxGoN>oo4WaPO1-+EBXX)~fk;60z8vuLPurv3K*L{1+=s51g4vgziUE17FA96lIx*X9iBN!2+O4#BMo(q z;U$^6kQwB0z`C57nt1uRge^%uW+p?VN8`D#o!Voz5S%XbMbl=zx|vz(N6O* zl(}w~(r8Is{*%*%_^v;DC}(gtM&jfAeUMIOCSi`sGw*g2AJjn|a{hJ=wPB&Qld0!0 zoCu;(#~!A5$<2mC%LKz8DsphWg{b~;5{tPOhebCdyah}h8@9rxK3r1RXjW!sfoSik zn3Gr*qOxI`XBA1gM4~PP&*q9Ub}DYlyk9bk7aBu5VtHQ(-buCUiviA_{GEO>Bh_(p zV}9Ako6OT`u`*Ef`W0o7Vx`^UzJ#i&p)MzvuWMhezK(U-FR}8@}k+}_Azn6+FdZ_BF^8r7m{E0VttqL`XCC$ z_C6=z*Q^XC4$p;=X2qc40c}Qpor8F@%0QQyv#I+>R* zi40t7%nrp*MM7;ITrrp{t=c!9o)7~wXzfr4M7aU~hkMov!KMF=hfqIe#g{~ANBMsA zlRgFpTzdO+Afj^Ru&;`xfi-#g_Vj#ns|wTP`ztDqcS-Xbt)=*5 z)3588d(&j_wnKw;Z)?lnfKx?zN9}fdRgZ`adHK@YXlpP)VD(bECQ@Ig#z+S{mF^WO zu-1^#HqMgKG6bOrTZUM}PP>jTJS|o#P#=fbT~Bd38i94QK;#ZEE0hU)A6mFdrM-@P zd{9*?*)s>ARXsDCNxs|dX&OzrN~_&#iZniI>2<)1Ol?|br=gSe;?~S)JDRSBhJm)k zFDyupNZZ1k zM`o;CvXd~G`qSy=`|88TFaqgzU$SrbFBzV=zDUSEM5Qq~jWh=n25r(ZPDr)G*(N8x z3N^6upq+X4%2`<{;)MTbY5(6uX5#$!sAJsAv^4`zPUGjeBUdP_uEeOJjAZ$f2TOoz z58Rs|w}{o6R|rR}ve-BLm_=Ptx#UTD0JbHH(Z#V5fv7s7FNXt;3mZ<62A$%l0rr5s zZAi}`SJ>~wBd8sVr;aO>S58l(0njw-`k8;cK%ex0Rlc#(HN{{4=3wz8 zn#nPrp`ZSv)~Bkg)eb5DbXFj=I$Unn+ge4_2SBF}%c*ph2SDt&iUtw&Oq?~uYQ~mk zo7Ba!`ys_2}KlS)@KRsOKSpUr5Dos_dJ6)K&EXC5QqIlrW3@qF-x?*|+R~eJ7p{+)23{WO% z0k_5g;-3KE!k8n_l_4nWRYfaA5Gq5lfr>f9M7p}Vscv1-pB!V=o(}QYTbGPye~j^U z+G3cf(G6@H-!NrTEI2T|E-Ka*(S!nI(yKNSQl?(5vLzI&%o6XXZu6U+gX>Sz>7T8} za3T!3hiV6H>XKbr(=_FQ&+PCP=?*c*bThf{ezfp@o+Qt#Lfn6eci#2}qv2LpUeDT` zUrT@`*Wd8!n<$1y!~9tv287mLWS%92RTDyx6A9NgO_R=oEh$i$JzFd+CVhr14##w; z$t@0>m)9lq5$q%7EETkDvAeEK5NFBQ#PumaKB zsm-IU+;4}A56m;TWY^p)BT!MveaB@nHkMYxk8)%s%U+ehzr+6 zH14;R*jb2d{_Nu2p*;LVrQI&_r;KOe1@4JuyM|WMlx2v3Ku_3LWHv~RfU8xrS zn5Y&@(v(=+D;vn*KhfF^zX)gk*(XTI9wIHIHG8%W7&m#taKJLjYvehLq-_+?PXuZ-@4$w&#ugCcFqNhlUn9wZd4$qM5nu>qu!&jl(`;7@g^}FIYN6f%T z5znC{vdI42=}lw$o`tm{z6_8kaK2zXZPjpW6>gMN|2&U5Ju`k=4=pcMfG&YdEXS6; zGIx%&&kt4CFL>Da+`@E3eDu7$S5%lwpds@*>t+%r^sY$emI26BKs;~}>Jn3&!T2GE z%jN51(8)4FC(lLO0b#7$W@W=I-9cbgTx$Yc0NFvN&>ZCWujlgkIhN2*Er*kqE-BJ& zME+CO=*!H`%-*C-&8`+`U*+M?f^vHYiF@?`r0v}Cp~O{>cOW4aOCqAjgi!7^I6&^h zy}K_w?DC;ecgPL)x|;#beE3Y*nQ}M!YG0?esnH1Bn9`zu7)okTzB`G&cK_s?`SwY3 zI30g8a7lUMW;+^Uy;f?L_8HVBcHk!*@jL$eF1h$tsH}G{px^FyPBW7 zHxYMh8ql^l+lsL@KJ5LYQ$_PL+t|{>|H7m0awTT0(RLKX9Ak~G+8vC_y11@9>|gzk z>wSoa=M|uYwWhTBJX(#WP2<(<`sr84a9NMHCp>Y7vg&|T3ic1Sq;H+pl#aPQF_at5 zqu+E!KN#ec2anM~y0IkBN3B!Uo=xyMN~ty((ozsoG(mf&Vu__{uI~SPU_$cs9@~UdiZ@X%2JK$a-Dt;&4Q&tWkUKrx=0v4kYw_`k zqODkVs=JslY=9yr_1E!n67E!)3pK`qAFRBd7f3cYCc}~4#NfD6HGYe%B(P`A_FDpc5e7X-$%{*}Z-3#2o%+Kod5iPnsbd%#%+kdUp> ze;ZJ>)5O@Bs2eEu0zoFxvJ#nu&+A!SiNpc*0tdvdSkz|3pkyFLF5|T(TcENHLHdND zTwqQ5Y`I}2P9a4=t?^iOiYy%1eaj&`5o3n1tJ|DoyQZD#HACGry7Y;5Ls;)d`0}B9dj$1o%jsyv(2oSC4VhxI z`KfK__~3)tA`)}PVC%&?6EoMTm;ThsMD;a-+g{uXS&CA=sMvVH#z@{$alG2&fM~gt)f5az>GFq|nn(_|XbQlgKou7(!O&GXD7`tB8 z&C4vdBr}hR-0u9Pjz)aM*o7e;Cx6dIiqcV8v z1g=i#5+yTK;aWP*UyiCu!Y9b7NgkWXlqpr<@8sE|f4hk+JRO1O41ONEBaQ|x`iLkW zUpHaTSq%)DnX)M?2Ijh0`K!Q%n!D5~Xt=gSls!;gtcY%TzhXg`4TXfl7o|H)FKs7i zwlS<&YlN?S@bH&e5aZv(7Vw^Fb<1#VS@{5TwGwtvuH|*6+$J zAT!FPm2-gCG|%I13(DpjaoTjZuLKN>){q_7{u2kv6^3G&YZM=WeNqHTZj@a6YA9D= zInV3WLBctvsum83lc7&3^!?qEA!7I}noNoV)dPWO9i31dGG#{<)7%;dMO*yHHrDj< zo}GPk7W}M*(%%3%DFixze$q6KxWVYZcl_CS*DVn3d2ZbAk?xh(AQ@;aVUu&}5>`lavu(`=% zJTOYk2{^d=0>Eslu=-A+uYkG|NWBb$h{e_)YaC*)*PabxZ7Kk=aVUp0!b1;0gT3zj zHVp<*X^^q(=&|^IWPK-8ojH%I2elGrK)t7o5@%Up3k=5c7|r^Dw57Ks$|kb(I5z8# zVgs_@vs?18p^`6GT;3-mbUQ`u;Sfy_t4lz9R!shSEE9=(l{11VVa%EQ$<@UJ2vM#VUi*vTf$qiFg(QZ% zYpYI%>mn|Pw>h{%8o|w2y}!Qi8b;W+Qhs0oN&eYYxKyem6a#ONEcAh+60ic4H39qP zVb+2x(w=K^2e?Z&`qTR!cAPpEu!lDPAuR(B8R9LTSi#%x>#4nXgDaeBu~b7-4v-l8 z$(3Eb4wzvNbc9Dr0g-<~N6K2KGH1q=lVS@|r~*TwhBG6>9Rj6H!;de zu!jL+X$lhT6Ql&WpjXb8kXj&-4*|X`4=6Y{1`2}AK>NccV^7=^#MDn|A`z~YbRQvj zDLII(_w$;~OojD0*4o~Mqf-67%j|qt6Y>T2LbWX`tK@Yc(|Ue*qUBQ5l+N4r5_-Rx zeRJjd)K%ZqWx}=WF(|NhG=kh=0*DCOpWbPgt9W-2NN4U$?BeT>c>ta+PucUp=s24k>N&{!YyqrelqMZ~C6o8iI!mJ(RYeSpk37 zi&=s4_J;g!G5A}@U<~>CuFjy9pq@nty|PCSZHOPvA~!tl~2iaEk&FBIhZ!e&BC24$f4Wq4L zx%}6`M}7go>n~jc%0F0xq++ES_(BJcm{IK@N>9N#E}?_w1t&!A84he!d1wkbXA#Ptuesv z;XC@h16NAE##5kJnoYM;wIp`lo;Mg*XuO~62$2^2h9%ni#Jvqcd z9wQQZ3D4%GhW>pW_m)=CN&>c3nEm}S@$>!NVA1eMsM3*YRk6|9X8m+5Wue_?whCUO z%=z)uH{#R5d=Ul5%{rsmb9-kJWvlMXql_G}4i>M^Bu!nU8+34xxxSyf5C+mmiZn4B zsXC_?N}Fz(404&=Y0|yrYZi{*?&tCFJGh(;wI4S=xwjPz3xvYGAS*ia zcs`3z^Q1^J<<%Xch;++*Z^#`jjrn2vm!c=lIxiNxu!p=xzJAu{o`P%))gV#T#! z=ad$7cKf0Cf4s#8g2&DxQiV+VUZjDGt zqh@gT*`7dUy*yT2aopOSn8j?RSFdQi8 zHX-YKk@Th1sMTzy^k(q#8zc(b=yk%4I`yi?LZMT7Q*arS?N?=geBA4rm5auB*bXk$ z?9JyKMtGpQfYr0NdvbRQY`l=~CGI9M+1JwUuD_1Gk$hYa^ z6vyWh<+2dQ@*p3ZTQ&$)%0ZM;xHnDeU3IuzZ8Vq(89PunKlub&v|8wo3=+f*p`?=n zNJ){yF#!I4HY}BcM61{tLk%{PCWs3ORb^o&p%X$W)5r^!(n%^)4T;X<7NL_;RgYqa zie&3=-7Ym)UT9E^>|)E+oLEUzb11z|B(6#u_;gb5xe#E zYNJA{)tSHg{-rzf*Qj%=lsxuw-afxnyuOr*0#`aBQrN?8)>Ix#$&S2xY!!`}L@Y|2 za>K)RIhve76jzhsRZd#NezkplNLne^dS2(}Y2a#$*-&9C9Ea2LYdi!_7bj>lo!{t^ z@fmN-Z{b2@rpVjlc=+R_=C3z|?o`TI(I9#;2v}5X=?GGvU zU)b}X6ksJTQ|W9(_^}MFsBR`+O*{nAopqScWEB;gZMK($jUAuoB}=Ete)THv_e$Ut zd7Dn3ul^uP2?B`ae>CgG)yFMxwDJgJ0V z%v0@rgl~p{o`xzj&ePUyN;b>N8cH}X_5n^AkvTWuOAaI_q|>2?O^zIu3qY6ro$)L8 z%fp?`Y^QV(id*DDYCef;kWv}VZD!lHKhh0@$xuN6@Z9OEY| zAJN+#Fs^QBw4lO`WUX!pP(JN*bz!^^O^mnyBuv3gVKDRO&(X}*{i_B!kS@gWb8RR9`O?0=n#=*i$Ua~)8TT(&ETLRm^-68C8#)2tV6Zs{_EiwefhwA zJchg|4__n|t4bw?YB)NdpG8MCZj{w@T&_;4&DO`W#i~~?UNqI5S>@BxTfEO@b7h1! z*8hyAgN2$tl2j{KS*fze&szpfy`{01AN(l=3blI=l>HZDCec^z@K0!e;WB3B{5m^d zt6O`e;kfW@$N^>pJNraqyUR3wQ$X&Pzl2Bis&K?|j5heW8n)cMCT+ zCs3SA3L*G{@#rt-zm@= zi=!`enmE{G&WZDC?Vj8DFFgZOG$U3aD=zh;Fib2qJOnRp+oi0Zn48 zE?ui@daB_IBopRw@sx;cl4gyLeK6u1-lUunU$?yf_Z>ln~(PPDO$S}j59`B(u?KxDhkO= z;v=TuwDXk4kD#Zzr)F-gaXcqGyYTjni3C9n9_E=7DK?l}`Wy&ieu77q6w?u$M;1Rk zx3Y%8!iiGZlACh~tWZ$5e*McebN9XJ0w&G+m3l(;>VVZ6Q1 zCxc@8hH!7a#s*7DEPd?G8?!oy-$?+!3c0bsFyT!N=tI@3#6~K@($6hHJ5D3#(4q6? zYE{YW*Q1t+De2XtlyRD&+R;fOQ`*M}2$MYqe@r8E%HhQwEiD@pwO2*NZ!^6t*-%$G zTZueO&^*vwd|CAkU}G%~?SV5?SYy5doo@Ld2<2G#%Xu?rmTY75}`~{SZlIH&UUTpmQY*ggUzL1fcnvs!u zdQfoKXdIgwo1TX75qAomd$EStO62xV1Y=KLxhNd2EF{iYKRtYa0>Pk%p2e3<{ji z>Tuoe2XxqorttnKpxt(NHtra~TJ8x6>89=4G3MlL?{)5G^v1L9a>1K+wZiM@^uhbM zWS(xYS-8%s%4r_@xqTbT;hcP|Bf96dJozY5y%IsY&0X-(Px)?yE4hK&AsthuZ;C7X z#OUROI0D-5;Kvh+?1Q1B*o5YKW?z7MX$fOT6gegTH^>yo6ei% z!q(E!i)Do3smoqb?Bdjp7@u1lPPGg`IAfuFDN}nFS{SD$-aVng+r=J$b4hiuY+QsL zmrmku&Wyg3D+R9VSa-izk|pci{?pc)baMfiHr~o3^CeOpq-Q?T9XU-dQR;yIsDlx| z_Fo4P`H@@W^c;yAYu6YK0p8v6lesRBxNoxv-$G;8YPo7fy)jJ0TemFmdlzY#`687I zDrtfqJ`?{r(2BnNdQ56DRG{Y_aVq7ACqH>o`aJgIa0G~zQrB6-ijeV)LiXq%iQUnn zkqKdFbOu1Y)18y&Yp947{ch)nEsLL$(Kt>xFn1?yc^0xbrTsWdtgHs#toNQk*qsg{ zzE6ruH{Gusr0eg&Gms6gSU)#Mx;B`{1bcUv-dwPa5D=A0F^7JBlKMgK*o8?9q4hC7 z{DQ8T&HyM>?7uo%b-TNd<00e4GSoh<94*Fuc>z$A%F2thR5>h9d*NjZ#A#R|wVMgF zOYO4V*zPP&n=u7sYp2D<9-p5NiwH|DIa1<2Y+w6a6qVRS?x{Au+M@cC4V0r&%ji9_rJ*V^GD3 zRX*QFstZz(N6g%%bg}w}Zi3Wdn0Z3wMxjp-lxJ2$>K2(vs#WExF7et&(M<8s7z~AG z2S_4mUTw8$VLmH)QpG7Q8$O0F!59|Dn(xi_%LWDG?apcPvt6cZvm_dAHWRoG##f%s zMtzyqoGMb6D^&(9k~6u>ZVlHPJ#qq_Gu#!?+dXlxO}=)>+^y|bhlej3S5T72-j|I% z4P6g+nnl-t+QeB$v9N!C)j49uOoC9WVo9PNrdW+?X5)@KM?4qp$7qUKS{++Yv=#>! zb|(X^<)&Sx30so<_L$;}DsD!dF>=}d6qIJy!O*PB%7RaPDyavxW_z_Gx(B7gh7%*N zDI#qUA5tKSAre`v&wu+%ku^eO72Z-;L1Oc89cxU(bKZ%DSG0ck znw`E>bFJO8cLeX@xcS-_Fv81{n$dZ2JBLGy!`1je@tVfyP~2&Z@tUxu=L3XUuH@hw zD#~QO7tq?$Pnas(m@1SqTXbi2qH$tg*G(^ z4V>-S9_5wBh93$gF+SH2AP>Wi)j3<;3MrlxY7g8^&HDdpc>WZW6gO#ceVS_Bz3RFx z_hJad+aB*hpFfoq>w!A z4ogdzaZ0D+)d#LVeI1f>V{r&0{#y}i>G++}OSUnyZhD6Cl7f+R9{Wdi_2vMlE*Pff z5DE^fTvsKWEi*{|3@}L5c88I^#H59VC(=KI#jrcwR5Or1f76#m8Ia;fNNV_RMKvDv ztyaaJm=*O7WGf%v4K$0Q?hBaYHX`fiA_RkMMonK2Y5F`ju>1SgxY6D_zWhuH*YFq zDlN|JQaiC$UAg8w-B=u_EV{Snv}xT~N|{Ue#~P93%L#U>c1ndqXWAY-YlYdB&;)EMEmnt{FCo@*;` zQ@lvkcEa6Q7{@9_8 z5NerpeTUbWv^_U6%;Ppy!SpxzZ~r&>xiETK_re=e*3n!tA-C#lQ%($({r_5mfZ-rXV^hMBo0zj1r2LL&r5Ob?Gvj(I4~~G zKEIo5FTA~%!CKpA5tlb#-`J6ebD`t(>?Va!{MGHFoFZ~1hVe(->SHzb)k(lm`|gig zEYDXfF;Z0P^7e0gVh_PCH*=J=hPWL{A1$=JpWz>NcaQtk*N2h+c515(ns3g!$1N z`B!ATXmnnfJEnc+T)!qWd~A7r%I@9+YvL-mt|r5sS^HFf9{`WEo*~h8Rb5pP0o9f?9U`^4K`WDX!N@fOH4LTU z0W1L!LYSI|Ti>^Hvma9RMRbTsXnUu}v?ZPV8NOSnE`~8vSV_U-LGYDK7tmixIBE13 zK`HhUGm)CN_U%41g-Ac(PD*>uG1gV+wbsxbEH8Nq5!&tsJA-}|&NiIw5#8(_Wyzs4 z`;|B~6^X%hjz2^Lps*%CU-pC*kFG1lZ8p;xvMt`Kw@Xq5f=fumCC(ekTjAL7EvkxCWM55S-wB?3X(}y?x z>BShh{!`ZrME0?cetjsD{{c8GpMPtf=PTBj9U37L6~_i`SRyX%qFxdk;-_{>)dD3N zv@}0&)LYLltKvh%s=espl?>ye5ar=No0NID*S+p~gI!zIC@tI>&n9P$-}*X|a7K#o z`EYM6zvcSKJDKVe=&-@?=y(Z&p}ncz>DTtKI;!VSS}b+m5Lj0qFy=8F_4ptlPg znCFRJ3K#0al{g`&OQa9D7B{M^EQN``jT|3!&eR}!%RcYDq)8h0ZT1`RAG2i%NG*j8@izr<@PlPA_kLHmAc?G-6e7khAeaS8z_2b5XE-d} z_R=^Z(fIrfnCS2n9Vj+60nw~=`B|0^s9BhK)#7~weyI>%MJMtIP1gLYl)BDsB{Es) z;SPAw>0S+tiEQwn2@HUoPK&z7zW&%3$NcDarP<_n@!3I(gvi-I=subu!V>=n2ev*4 zE?mJ3cs2zAlyk|Vj(^NVScQ5oNDY&76n248h-zrX0k~92#PF`05zhq~H()tk49#Hg zJ(o%9iS^F_tbuhk!j%p-5=Rh*VD@$vWxgbD!{!oqxJT0M5CN-t80nU+bF_>&c~jZh zPb*HkskLok+O|S{@^GATHggWu&CM6d{i#&uKF=I#rYy}gZ6>2A;yrh}tpVd&iAl8nl; z<<_&rnJk;#gY!^nZ$6I`7o&Br+GBu{)@t+9{)(e|q*I9_{xkEc1Eax~MVUW(&V0F% zqG63zC&r|q7tA9;Y_F&yNBeVsVTO07$!de9>?^h7--{nFy=d1xAp=I!W&^0%#b1_A z7Ze)be*mU0q9JD7s;oAZ4a;|fn`*!n$y{7nAKqgPXBbLQnQu^NN8=|cw}z<=O|mF! zNDbf|Mb^d53aPIFU?`J=o5NZcTN^sKQYiLzh~<77gW)>!#c)J@TfxQYUGJ%?$L?NYFTC6k>gU!9U`ex7f6z_ z9ijUmPqd4@zPP1It{`|y~l_JH>HOCX6@ta@u0lnFZz2tkoBCMg7s z$@=HhdWP0LN9NcFV=@6gEks;IW%T9*Vg*99A5X^$R?TqM)d*Kj~J^4g+d7!M81dn*sWBaoFUYr-4(HFZ$%%G%uH=Zn73;`Ea>s1eg0tcqr_b58p&>dzkV1t##G;hCLhzLq`H_}iad)_mNanTwf_ z9n?bXgct*|4?WcPFnCWtCuwe#7Ndi6?jQY?^+juZ!vPvv>${o;+LzL!ugkEvAiw2@ z3mK=fGb$Y2%$&J$OP*u92m}eRPoF_3tg!!`A2$3wL7bRrRQRk;pS^QT8NdYw44&!f zK?ErLQ}IdG+a2A7K6<393ls(fV6y__{{L)tPro{VL>WFSK2@f=qXEhNfvN!k5@$er zf3I7`%NT0PArJSs+_?EDqACd#%F*Z7gGbTVhtda^{=Fg;Hc~3kUqsiBltVFalxrAe z_-B|TB-qp;Qd&M*tUsA=K1g0vL6fhjq=ZIv|DLrWCPfsJ<8agVviULnvIXwLD+VZ> zHr$*gvXLXC%yP%I`funbwB04JnIcd0UVeyS(>--qfLN@_#Q%Uww>e5!qgm-z9d9$UJ29j<*oxF(b13=Gbc>`6E_SuaV4 zAa!59pauf!rl~8f1eB?JS#=P#7XF3;_14%SPJ`(w-5YooQC(B-xF1}@$Sco-0DVBi zBm<0u=pLc@QKbe$AJeWXAG|Bs2Lk!)Ok;`HdOuR%8=kj_Mhhc2oP2XjvK~_q@qSuE zj42oR0`t`=I-y@&=jAOc15(E~8oF^$68yL?qty0U>n2&(De#%3Cg(v9dLZw+30?^j z930P_shlr+gzunCMMEl%>fdV_99+KoNGSJVGQ8#75Bsb%0n9R~+ zSjgW1r72T6zxM;lT89m>iSwV^Z9XyfRI6S;gA76`Knb8K&{B%{XeLBzz$grP>ahRQ z%XvmMm34ib-V>z+2$0Z0KspE`1XPL;sz3lSAjJ|zDT9O(I!ZHug+Y)ef*?&m5tJ4n z2ucZ{l!&xYL;)f6$b|3)XJ*Ya^FE`W=DGWD{?B z+F)LA7G;o>QQRKS6jR7H7%OAi>Vp`$Hg#;T-4rMmy0tjTrhh1`9Me}4Q06mIQE7XK zT4YNpj#}C6GZf%DwSl{_%@wdhlb(Wms81OQt zj7g`$_a|DL|Iq3^vbS^RP?Qxh<|;9obD~&7A5Rl&j#4ElQ2^B8OnI_EZfig=XXTVO z71|u9LuMb1xNg{*M^Yu>cq64~#?(X#|Em17O?zH~;lBbRohYuPK+1J^yV!8uC?nk%uNSI#M*Zt2)o#yQ>CV?hFE*UGeMd^x z1fG;8prBx9S6FCf{#~Hq6s+pk3zE_mr|Ai8^pJyaoZI%c+IlK@(7ZlR*L+Man6S~q z3VXJ<O-8=R}??GzG^JX2m-A?@ti>Pyu%$^XX@qF zQtfa2f!GsU{xBIoPdV=AtFA^Mi{mGyCQN=3nnR`qoFaS^Jnt`@dKYCO<~-fP$DXcR z)gCPiJnZkD$owY~Th1Pj;?1sGV?5ldfs7Wvq2vN2^rU#b5Nvemc!Bi?&uDa<$94;vr^VhnIqNj|PwA?e-H0I8Cb*J4^P_=hUr!Vlp z7JorSjg}#qYoMS(IH@k>X3E9wp`^*%AZ^!E?|ySQRVC8MIh`qa0#(4LrEhA+&vhTf zWjWT0MUq}DP%N-#^(N9o*oUW7s5+wy4@Igt&r|{Q{QADai1Ez2*QIsiY;yLnw0SaX z80Xnm->Eu+esnRJCl_xK%vwp>CTllF8@{L)C)toM23x!lrQFoqn}zT3$Y*E5D#UMy z?9%^%oj<{9$mK)dGWuJ-Af&MC!y-ww|*pS$$ zJ(1#gdB1l%=`*TgPLN1WTwEX$K)kL@jDx(G?}0Ai8O7Ro#qh<>xvhUb8kr@xO*0s% zN&+>;P2xw$Y_zD=lN5d`xH%f9d$DckQcFgkdY+d-XIRQtvGt!m2K3DTZ-Lv;J;%*Q zi115@vo1r7j)74Kh&zJa`42f1K;LCq@fqX2@LX^~TQ7He@_;*MI;k@{TzSwtF0Tzh z$=oAMOm zU_L8tzpeFtk5>LLu~FOf`RbzK2aPKjqYvp9KVU{v9z#y)JwzLSNf_mQE`9mrV9H~W zdzX=={v{oGB9WJ=B`>$`dsqIGn|$PVug#S!NMYzee~i{ zKYFhdJ#6)W9&(K@U%V9vdHTd?IPi5n@brk}qFjGH6!9$%5E0SRLZSNmALJi5ij89f zd>`D6lLJ_Z+}F~&gnHXwm~U?Meef{f#OP+6AV68fMXLsd8tDH%xP*GwUzBfUlofXb za8+bftLD1^B63vg8tPMje}2A^QG6T_@Lhn9yuY(uwc;^}Z-@`Fp~1dx&raTr{6l=j zo%_|7!cI9!+^j*vDqPnuj5NO2_C@z*eNS!<2EkTf$RArc^2f%E{IMA!e{9moADb8Q z$M)-mg>cQy=FHGZ%O>BV2_5^Dtdi~C!9VcMu{dTh@SN@~(%(fI$u`ai3_P#{(L?F20LRp6>9U6hun zL*@6S$WM@M8cBHxFkXz}mcvpB(Z0Ia)8+g$OZ#A+ zz6BxoCQJyUvV~lFNeuV~+!C4&VkEatEZrb-R||QnwtoW(TXsv2h!)lNJXK%0B{d0P z_Q5YirD`Y7%67NJCO(Yp7W^9+@vMC1cC$$kqrBy^^qlBjy$_m!)i^e+JKH2% zh0ee_FxYM374-?P#!gB{*4wvF zmkO6L*A=d8u6iyCw;DH^yM~8@$C0O$hX7y#C<1%{xquG98c-0Z26OSE%7*xzw zELiNR*aCzBA_zGQNrGfUo>6VWo1V zs-)Va6{U|#!=+uMFH46?W2N89=*pPMILn}9LS>Ra; z-%ZA?;max?>~Yh{jt58L@1ox;15udaVDxP6=^Q1%%&TLj&WAL1o z>D+J`yHWvvx!{e#_EOAXA;?*Wl+#*y0BX9}A~Jv$N--=*(;bL|r=9B5xKOPYV{+PH zSn)F$-R9z@2YMYxEX{?d_=t3YmJ*fhKFuc&KMs+gB zsCbZ6ea&A=7r{!n#}et)t72xEOy2k*GP}MwO3cvLo7#T&3}TcEVl6y5fa5_uN!#2Z!NhcmHhq~ z_I>j?7r9b3J1-`OE~%2_mUX|!2;Q>Sa947fLqq680_c7Q=x6-psYW;;?BsGcFZ#CD zTH07XqkhRn%jQuO)>K+WKY+y(dOo&e@0~~uEC`V9BgRKbFKC6#Go0Afeh!}3NWTHM zvjnbWGEy6?IX^obdM;{R(;Y=D9W(W0#I0RZ@1R|Y%o`Adw^^b=iiaKy^lOi7$av37 zwc&B6J&KLSrb&5v0lC=?Ph^5W;B1W|Hh&hR6_N*kb6VOXa$XJ31#xCBYGv{<3Kf z@Jr{@5Y{0e2M6=>2nU2!p!T4H#nr$;giSa=YtYQgN^8h6K)LWz$uGmBsIr``qn{q% zCDeZ^%~}pM8?jdfYitgmJiEK0({e19=SaIPo=2z6tN?qY!+8d)(`tOz^2jTPcVdD4 zE{n+OZoS2;&t5^}AzZRVF?daf_2h2VkdLVA_OK2Hvf6EfgZTA8>cn(h)4yDJAwON) z0DZX<9X}y7hFPeVx6b%n24S7d4e?1&HTZC@RxF@L)9Fe0!;jtk35jPUCQpbTe%kJ7 zpa&L#=Y^$3M*7#!wO@bP&A3As?R3)@P*ZuB) zao#ajuxyJ7&bU2o3^%dyus4cLlS@diB-eMJB})o~#rQwgeKiEDuP^9Z9*~*lI8l^% zj*AiR;T*R%%JZoUrD1Lv8<6T1-nHvy1*t2lE&u#;>7~&tu%>S1TT2Tcl^N%3)9u`3 z!gs8vjK$aU4cyPJtO6x4BH&dvNSo@lgWlIhfgEhg6!XReJ1WNrKY783`w00ItPq}Q zRotvoi)pB;dg@=_J1{<0>*PodQ!itfcviRRf@SvVdz+aY(?jem3O+t}(?5M4uDm{d V`l^D1+v7dzXWE*Ozus}S{{vS5Z1n&D literal 0 HcmV?d00001 diff --git a/webapps/docs/images/fonts/OpenSans400italic.woff b/webapps/docs/images/fonts/OpenSans400italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..cedefb8f6ff406c6b77e75ae55ee88e2bd88dc17 GIT binary patch literal 21092 zcmcG#V{k7)_b&R2ZQFLT;~m?!ZQI(hZ5unbZSL5%?d0Tr|EKDl`{mwGcdDLQYdzJ^ zbkFLZny%_vZgQfc03g86rDzO5{ErQ>|B3(0{3rUqFHuot82|vt`iH~)546|fq~(+t zn18sipIGMy`uWn$F-A59_CMUo56pj1A9b>kZsh7r2mpY{|Flv2K^}ZB29~M4navMZ z1^|GT0{{?>z6Q-mW(H0_T1e%eHvbt7A^;6QVrK1O`okFm0L+2_fCQjtLIuOz#Na39 z`q2Vde!%upi8^imL;i3ZKQYk{Na6WFzszl%-2njZpAZ=9r!RHmO+jWW6GvMBfam8O z;86hp5>VI_1So4eqaUpl(^}VhQ;MZExq~3;@X8{OCM>a2IHOx@hlc@}pJwPp)2mI4I?Sl*At&=m&tYa66{| zS|jn^|JutHP4_G#ebZgDtlr-7-af9##k7C`1$;pkJ!5erLw$YyJyRniQxgM2Lyz9x zhphksB5cYsph4J>n7(P)C|GD{tCfTAaK=Xa2WAmUM#gD~u{!r!TM$KldVriV9K}y3 z|9QT?zNx=H7Pu%lqB_U{Q{XEtEQ)_4Fk(I6lq=*rDkQ2;dL+%3X;T4FAx<$)NnWv5 z(O!X4LH+)lPyG-$FM_ZP+5ij+3@Z;>DlOtW_M701@78a3``d5(!{<}~JV*wCjaU6y ze>mt10t(?*e)B$9lr^lqNt85yAe0W!EDddy^#yL0_Qvl{?+@TW!G7Wb!#(5##edB8 zj(5-zlH=4BmS@-*nrmF`o^RkGqQm4Rrbp;0s!MFmu21kWveWd{wpaK$x?8;6zF$BA z0)0gJ2D?b{3Uf?#4!2O@ur`<3n~7$VTAk)&^~Dov)w)flmv1gOY_?nNChIYmUar@h zBl1ZJ86NqD1_EqC8t6hJv>}GzArFqV=Z=<`suz~h(;#(DR#*ucnxzE1j(Z2IPNlNf z#vG&euV+J2{ORK?$5@)48D1T46w()&T+s&OS#1pncs#nJz4lemlyHDjDX*BHJfj#O zn%x*}f|E*VwAAI8@B`7}!xF=yU}1js0No-U@F5{W{d+VU|6cyq{2uz$y8UJiTIu0| zH3ou*T%4L6wNEtw7vd8l7?2Ku4-pmh1#HmbAN|aqi^`1Bx+v-v8aON_vC=r>B2lI7 zMZbQkb3!vem9@>-ip#tqEDOzv05Q3d=U$RfN%rmEDDm%93P&c+!!@DrP~0^wPV(i4 zu4EZPmPJUb?PuSs=Pi26_Z!05cdyIMrD-`n-naZ$pTcgP`!35v?xv>K%VG9&t4ck; z$3u)~HC~2PbUqsNt1-FF4j3no_vy)pg#%$%Z|h$yh7PL8y+Q<{(8PL?MGS?WZKNN; z)oS#w_2_|NpgR;wNssLT#-PNg@`G~DAn*eDhlvQZDwgHdbZvBOE8xX(z+ZPoC-*@q z!;b_xu%O!Mk0cG#sL_nkf#(IG&#Q${Ph7pYP2>D{A6hTPMeChUP69`Fo^3p0T99_BO9K9#uio!Ib z-r|I;$E$sBi537ctJZEaY6hx+bywXu>K#p1Mi~SXXQ_UxDM?6q;)CqO5C~YK|dNGIuVDjmj09}y>Bp-F~p46 zm*T_ilT0Op$k%DGm^8&}*1cXowHNU8>p1vel)nP<~_!t|WK1|C`h4V7N$h7XdYDKVrE=#MoK zP2*t{sJS@_p=E1aGTcY-tswfhHR(p)uhwL^{I-FRC3SFGsP7e=m3VKO6Uu0%Cf+DE zQR3n9B|LWf`BLqAc{=d$ITER&rBLe568ZR{*QzBrVQu=^Ss1!l6T%MQg_x2qB%Y8% zAgqvkftn{<3HDLJjI_wJe12m=o7FZ6dTX3l%__e2Xu;k{w6HIDawuhXDoN>*!i9|9i}jcZgXC9(`x!`f9LKJ{=UyL+n&CM&*L|LwxLeC1cj9c zX3vBX8Ng;2~t8R6VzJ!x>b4GJ|G z4Axqm9%0W*_q;K;dLy+`_TFLy{3y4iMhrH7Ap$MK#~{!B3S-raf+T>pNE{C9*~v~d zB~!XuHzjMxj)6!@@CM@F!hr#B1NSVWyZ|>$!M#u?UvVW%4uqL7pB$c1vznXBRt&!w zPNj)9tXB1ZKYI(wGDbvAD@Cc2e4CWKG9V3p8ykbouA}Lc*_MfuE=&8Ik?J{CO*6F?+#`nu&s3;UBB}Z@0aGdz8mG25-@H)#Y=9& zhQATXdB*41jq5HNwhN$q)Qctt0Yg^-A_$u8*L6AhNhW<;>t@1pvY5MG+=*ypm=uT@lsZ<^B)(s;1U#sDkG}EScwZWKLsfGiwX}AV(Oc)IzK74szp_BJ*5yeB5KM9p{0b@_T+0m z(ioJnA=`@5MdmMO*dwkGczcf-vY!Py6A zTA*@*w}LuD&9^b?3r$DJHdf$Sh%(g8Pw$y0<4I+G_ga75ha)5l#sIUVE@!!?J`qU( z3Stuw4l=hZ6iptz77?V5C<}RNQ!45%wt`^Xzn^*X7)MtNo2v!V<&K+X+N-Ak_F3AZN;gBbm65C4d;Kf*81L1aubqnJ8A}>x^Gm* zyCTSR%{ofaN?KT0`egHfr#$9$%Xy=G<|~*wWQ9CTB^Z1Jfe{4HAB+2>$0gVsgk%SB zt+EwEPMj%Ci-Sw!j}aeT3JG{r=LQf50ab#%1Irp0@TJ1p&B4Y;cIW>3MG7OPu$ryL zo+tJmTeCI)wwpTTA_U{8D5ta@9iwMO{%GYs~O ziH=iU4nMF&ds98xp-QLN(MZE0BO9&FqhxjSD+ybs2m)6>rk8X60Kwb|_8>Zh7=J`G z4%YJSxX|@;PcDCE*Mq9$(^L7vEI?49D?7O<{p4NKY3g3^!R^w}SsP+(f172n- z$F*Ijv@NHafVm*N$X(&46M=(@+k^!gm$=o-+znTglb1r>C`m?1(KAS zSx7r1OrIMLcendNQ4V4No1OJ{dms-F!F@*b=L+63!Y*O|QTeW*bQBHdLA)u!uTeCB z_HRHuO|$$mpQH7OihtS2ZwdPPgfk*==%&CtRJbu6PY^v}dABt=TRu&t_LHS0BRxEB zc9)MVmVeK>o({ePTgbP87|i5dZ{D0Q|+zR5YYWPV%R^X*r$ z-XN(MIH{<%CA?*snN75o#`6zZid7B3lVaw%(+5?V3am@Ezv^u}n}s`%zWO_gYDawC z?=NZKfZZ#CD~RHtvcKuiEWPp{USE$1R*R07p|^i)zkjyJG?bY-9cQhc(p(TFM?HXM zgjN5&-9`fE*~x1RFOBU2ZO^=t{uuc(nGs2)A1n8Qx@ltgJ%)Pg0*J|mzU1f-T*WC1 zURmW+w;Fwx8@kjDEjFHU)rE%?gf5(G!{)(%>=)hxqWl55Sib7JwGrJpDOz`w)3RGzLl4XDV6QDi6 z5u2K&8gcBBwA^)*%UW1TvATD9ip5&KxwGqbayUPWAt|@r`9;SS)XcF1bn-^DQ0-Q9 zbU7Z4^ZC^fUia`P=;Yc9o?z?lgT_32-9J=Eh&rc7%ZVV6VXoqOR>+yPgB&8IZ!Ru) z#BVe3N+#ROvMQ4g5fP*P7Y`d(V4c|tFsiP4Hxi2(BKBHo_yp#S(UkiEDzGNi$D&{v(lpZ{6 zZ>%uXl3f0cR1*zqwPPF6B7LdOrs8Gw8DBKAZ$%F6WLlK`d}5%5(F{B4#&UO|H`QAw zPOy;FtlfI-0=tecXmO~lwWd?-rSLnpoV(PgZupS?t<>%d(CR_t;2TBm)v{(7c^ z9pMn$sVH$EdeKT#R3+umRmEv#N!V9dK@}Hrp*z@Jd5Eo@*!TU}SPS;n)E4hEQM(%! z`X*Nw*{;ZvH}K!?+pnn;nQmQ1R7K&lXJ#A}cbtKG{U&vEA5QM#p#ON zvTZ@aUvEH|O#F2AW0v0`DS2Y%7_Rl!Wx?G#cDbQQH9nqCy(FISo>51B!5Urv$b2zO zwYxm`Z@v3*;=QQ!o*~v6vq*=g9pVaBP`O2fF6?fo0uskz5^@V?JNwF2oxiFQizH2JrCaF*Uk*Xr{#2sSd(qgG9xJ)Lyn2`huF@#I*Tw?C{>XAn{$s zqv?(hj|`m9=BZrk(vls28GDT&{5Hbg!WxYVzErzJw)p4&O@iV@yGIZI4CKK_YyRN_#h0e-E#e|sofe}qC( z^mgo(0y@djafi(T4+>xhe*f7#phJed*dC7tWuCt;QP7;o1U7{z{D`zdpLF&B$U#cv za+L->m^$_{Bqf(!qRf<1By}y7?|O`LvWofD!4l&t1;3O9k7L?cUd4#ulk$a`H`b=_ zmcF;e^sqB}o)eVSfux5=I26hw zV8sltdQscqSkRN6(=NT;H|6xA%~Ki~^uel~7-Y5G=j3M5*8l4k3HP}rH$QX#UCmd^ zr^4jiZwa>;XI9R9PY0LcKpvL+DqpTSS4^vi*Xy1%%V+;$DkrV4YR`)S?v@RMeDP|J zcyECimrYpC9cKZU#1k-HEfMeR# zcju+TN$>G@KgHVq!8}HqHML%Gi=uHa(OfU3gSay9Zpb*xhkg-3s{F!8k0g|ED<9S2 z>c(yD7bRD*;u8ws?U|>nds-7zl`n*)Cb#SLgr_1G)qaU9>^Ji)8^w2}hu^|3WIhr| z8*WhoC|U7BBkhnZw@I&835yLP(Mx#}?U+i^=xToNnkF$A&hL<=O+H79BWI6^+Zi>R z;yb(!`-ctxCsb~9rPl?ABK*yEr(0Y@C4ts%*4-6U$Pa5tz4VwRkKz><*k}^`O+7+y zfyjx~-Ys9sFy(!pTPU?g-q{_Yx%0W_&EgvJOWxY|mB|u1Gpd?0DaXte!+h)QH=hK+q1N!Y3-V~JY`zOc1w);G3Hq2%cbwR+c=9y= zB(gS(5kqE`g)1fWT9Fw%zq6GI6^p)QAW$3lv{)l~n;yf6RHh{bHgB2tPJYpxf$l>lO%>l`EOwe zy3%D#WYTKbXBykUw&UBfTb=b2OgRF?GO8=kWG>J!Z~5j(e}*Evpn9gzjE z%5*pe_TM>TH~EDNAo*Y1gM0n>;J2Oeza#g#pi&RZuvG`Rw%1PvZJd+57G6ffURwi# zl#b?|Xv+Hjm9;ltYR)V`yQUg`6w7|B$Oe=_^gw3%qva~Nz^vK6_j*3yYA%g!S$|~j zKicLCVn*=9yu*7yzyJyDh6Ei;4y?mhlh+^fpT0SnGolH;p`Ss7#$;i{&RAW@9+|ns z?U}gV0v*6c;8L+pLx9+>&f{sINpnDhkcsxV%lt`W*7n`}}L z6Ft*)A6Pv;VrkyMUTNv+K;*U(X--2H=;R{B*s#CsuU?r30d3zj9DFE{8zcQc&y8BdNbMu{>< z6=g@Y{7GQ;-6j_t|LR9EbCFK3c`mpm6y(jS<6{^=xG1^C7FX-m{ec z)(BdI8EOwt;uzI048-sa4~epOiGr2_6E8D@))}S@VOft+qNOMnuRgEP@8;9xXa*Z0ypzCq4NV*^ zy|fm3$Km|^`fFdwIbWX-%0@qBy+sG6hH8)AS-2qShrfk(%E=nYuNo~B|NgmTF*I)l zuysue?fo58Eda&el5j-;(`S;&@u@!x$8U6MH(N|Iq{^>5d@S}ijSo*5NcdyWoPVlu z)e_s`Yl(TcR=pGKn}1qJE_>_xJ@@JNh1Q5;;S&93ayWUz&{Yx*BCd79m^D;)|?_YL1>R(!Rs5mB1s9B~7*(gw`$~ z;kt>El(2Cg!tIy^CYUW#0Sp}!K$y_#O|;F89So&$odz4Z2Jk8|HfX)r`R&nF#Nla;(ts?W%ZV5Y5tMe*#YD}1_xZinvpZ}g?Tgc)C_tADFnub6(%;Io!k=VnZrZ-OO&Df+{7Vz)ig5w!RIlRB5DbWONzRpbIzV^u<8Pv?7iiG#h6eYML?lSTXl0UZa6K zX~RhKUR7bF?fQBUornMSv;Bq?BaGH1ns24{S15tm`=wp5DHMcehEC1guquIE)5Tx< z_RF;N4z`PHknaK{58h6Bq)29^4fFhd8_x4|u*GR2r8TTU4t43*$#RqJ0(nu)w!$HI zWEmTi(cF}pJRs3Kzry&i|7?h%Dk}GUw3JH{e7K0NvmR)SKQ`LHb2B^|>R{aYkLpzJ z$_hal>45>vJh=%Zxv2O!Uz*;$wR!(B=tv2}+dF(tAlVSy;(i3)>$Lle|E+1g*BeD1 z@vX)|2u~LSN$*wwPk0bxy`HALlFLZ9#`F0Eflt>iySf^W)%3cORfXh}tU_h7V~bB7+7Z#}=A#xhUNtXjY_N1g;yZ(L$Zr&O+hjiE8{u6t$g z5vxF;!#id;D9;1}J^aZW@2071G&(g%kEiq<4>NTzbTq4!elS5AnGjYJ)ZG_u`R80iOTPL*2@eGORrCh#OqB{s5>LSd0hm@UGMwlj^RlR41z<7$UHp9G@seOdxsi zZ0xD$JCj{lk5VEz#Zqj%@b4hoEQHNMbSDETFLXMg?c~^jgvaIg#(>Ma|`R(at zVn!-0w~z|Qn${gHNJIXgjKTT2=uDW9eUCmLEC@=ZLi>DYA0I5g;AuA#Y?RKZr<^~_ z*DcKh3n-NBm0Kz@UTCxeLv%oq^o&(;#Tc zsvvp29<}|3`vX&y%R^X_TT#-<m%LY)XX7UPIDv2*~bmv)FWoQG!J5+y_ zNDC9t>l7FYt%;KA;p1CF=|y@Jb;c)HIvmMQR+r9_WT=pjcjO1mf3-xOAU#`+c(n7_ zFf(9alq*+4fi{L9;^#9z%j3@p<*Z8BQ#L}IBAIlx%q9ZpNij`^}9Oz-+3;T_nf(1Zkbj)HEw zULvAyH6?PMkV;W{D1VGh+%kC!fAbL0V4Gw0aPNe2H!rjRAYk_Rm_}>n00=ksjJn`j zAiufZ61uv-44N!w`Q!@Rz6=Va+!lt4D%ix@)#snK#VbfCO&U``jwB*J0p9@ivEdz( zB#J8=UGmymxDtdT(8meS>Zq?n!oXbC(Jc+C4W4avHBfLdBPFZjy=f~=@hxYjfGeZ@ znM#jbuRo@C&}x2Qy;S+0Y3_nBnm*IiBJHyxk#JM(B3h}W3L$#(!FWGrd%~T5+hudZ z2+AZHpm%jr0_jcYu!$D@E8bGM=_#r@Z8~~EyB7>gLd*_v(2dZe&J;jS6Qk&Sw)@us zC#M5B9)RJVH_Z51Bz+c9M>BR`Kz3mi>vU>y(RT6X@EsJhpv|C;*Z$XGRF2(JA3saf zG9KOBIBAkvS_u(Tf4(*;RG=_PVvx8gQ+E923|Lrws+5>FTHY7C4}w{yh;`JG&|}0B zF&-H5m>?ll_~*dPN)9A|%$1^>t;KXW5NxQattmX5m9q&9O$CgVll8VqhDDOinlimR zrd?a1_q(30w7E$7g$%rEtR`blSbx+SVnIKBbD%p9qR?8jT$_l%3WJKHNt9bFW+>-^Kn;ah9Mh}Z|Ev{Be&rb%u;>?3bq_%4TAmd5vkNmq{6zj*N8rFc4< z%Q4;Fp-fG7UiXW$WV&nb+umOjqax_eX`RQQYoD(aC(-x0eOWAag%fry193jnEw`?- zl2#U2Ug*myyrlo6YI!V-{cmr3lR@bPlk|kM&a&s!-=)6rEd>5kroSbY}K3zoNjzmQmlt)ThRnxmTqh1hTB;K56fO|iY%8RzljsQYC z5DR6ls1qFhQ~p(9j;m8nqTQ}iAG6hgW9NwqH}p@Cwo-%W*wDP=v-Ntco=nHoS1L1C z;l{!jp4qz#!?Tm+nKp9cvO%-{>PZ#v06`Z=kcW+g3*vx$LMRMosycm3cXitcb>ZM~ zb$h-*tKh5$0#DP$k=IX$1xL3AYxj>s9A>u9i(&!;gTo@eH^lraJ>LQOH%9f*X5aD) z!3Dy6EQZ>KxO-d=>6Tp>?z^=Abh>6$-f+xKawQwOI?NIGh`>U3uAZYNsaAPi6^HWkW7d3#f%FY2e5%-zo$qClD?t&Td=w<_YZ>{i0)eMe$PwYQ#apPM(l z%^8gn?zQ|?#@>)QrWD}^vn)gQ9nOeu++qHb4gRYtO@p>#$=8P^FfRuaAfT#kq_d?% zz|1jQ%41~ga9YOYT$87$I5>reOns$|3rSd13&(o<+|??!Z_bBHZ)D^#mNsTT8Zm?` z+@^-F!MeS9f?qc}>FR1zbik6*8X7aPYlW*RwK?RFGP$G8{j0HZA(fV1ghT5rhpd>; zY`j-gU~{vl$$Ie*T`<~&U%GC5PMlg_*2hiX+F8WBQ|_~m*=57bm0pY5=pnegZ0hXD zE616Y6BsU?V=~u1<#q9AK^Z8xz*+?qqV5T@ax#$6+W1ugbR#{E=LKa4_;p^BhCQq{ zT7c-pT&J))FbwgmHtlwCtK_61ce20^bFi1H38A_tewpwNjAi@24k4$wQjue$dKmp4 z;{`r!qyZb?3y;XZm%eC?sprAweBYKm6TSRBYKIvE6$a18xIov<{|d<3)&RO0-{*CK7df!#Gyax5<&?o0}PYBg+f2%F^Cgx z4-_-|3M!3UL7ibzBksG@T~Gw5i?R4O{wh(%-7|(i?dM5}=kBzAX2sBFMPSKQM_;C` zeGZOts{ngmM&LKfLoGJo3cP1IABe=pS8*A&Lm8SA?#Un;+_6ft;Anx$H74Ya!j6A) zw2`{%+c$9-QpSf{)|kGxnZp}Kp1R#P!?su}ItE50>EDDyPgaEE!Y?^~tbWmTDN5&t z{ag;UYbWD7*G4Rhu`SFa5=5<1NZNeqkD1plO5L13*{=(o40M9v<+UpXVJ;(1Wn81{A6e%j93d}-rX7++DgT&)C? zr&o|bqSQXXs=-N~=KiJrLn<&{4O0X5o%AIs#G`rH*{rHc6ygry4oA(WFRehF%M*6UXmECiq+-lQ^?_MriljF@;D^m`gi z2XgxbP~>5hpn4xYbk)}KrVDMnhs-0SQP+P*b#TCoV@gt!x15n$auYM?A$_j#LI74 zUt#ruH&Jb=T)_rFdE5F&7;5h2%5HcXhuQ{1JFEyOtx>V`^T?SY@Ah2{+|KroEstyH z$tHw?xf9RPt+BEDYUG3ETO8@J@Jhz4TE5Jpof4`2)NIdT_-?d>73RF05Jqr=>x`$& zRp`4+kjKljR#Z|vk*CRh`>uO$?g)++_DKbnc}m_W7?U}CZy0&#pD2Fv00uNXaZZ)y zubZZ5KX{5kMD*#CybZVO5bZo10wwp>L6Z{B#Kb1ne>|R7i@}sk7yYStai1G&U%FYImfsY#@ilyw%QBr zCENuH=}xxGQaDE`5EY5a(f7B_|K_OcFxq*+IF^OWwCQ$z$)-9|VKQMj2^o};gbYhh zz}n}V*M027cwYPY|eG}{XudYz%hxu0Gw(jhtu9_nKnsDg45 z)2vTK4jGDpsx4Noad&t`*b<&^oC3G*6jN^SlNgH>^H{I#|20ME2 z^GM6fD>Ycb>0&fn^a}{>+JI3y19X~+i9++IY&BSV8BBZA^}=HA{ORM3Fld#KrpFAs zWPl%B1wYv^kBP4k#YVX#5v%tEtBj117a?8aQ6Be~x1dxlR^E2=pz3_UOz!hgAV2F7 zHbA#66ZTU#+yTx}_YCO5fs@|9SPt~Ta1T4~Uh#Ps-Zr&<~$r0KciXnm>sCfPj+i>G`vce zlc!c$>$0NpZ2zXkwHlcEyzaZeH(o3pUtb2U;U$hJRABJt)>}O7BOLj4aPI-2n<6&G<(u+qk(Y!H{R5$W(A4=ZP z++a7viL@7Ul%CJ#zf9{z|M*>P)L{hIV$|ti2{ca0mG{9lBaa(Lpms#1c<#l-=08%o zYQinbXBr;2`4~b?uI@bYU>Kg*+^;}ZmQcLc@^RlWGkdAdweU)1KOJiE>Wu_Bj{j?- zAUa!qaI9~xzSI$Ahn&il9jID@fjOV{UJJP|M;hrj(El?Pn$S=WS(O|HsfvEYa6j@E zPt_&3f>=fTR4yH{c;MX|Za%?K#t{03m9=)3%jflcu~;$M>)-w0fx|N?+uHhYqn6pB z8{+7-HdGOEd7|^+0ol;VW+QWC3&C2-;PJj>jv~87--@ zpD=)i-4iBm@U2!rl-x3zC- zeP#q?Pl@c(KqU@Jnuo_sy7Hy}vttvv3mD}*qX5pcqot;;)@EyJtsgXnkHuk{v^IK1 zqo-vUtyE{6LPjwP$7Ypu7v)K88~#(rUJF;wtr0x|O%~^0tAvElK7E5hDnzNMn6YpK zNe}_KBs~}2XCYzq0iOOSGO&&Ri=JxODQ>lpGN#QInpFfLbiscXNTCiAX!q66lSk!t zJXQ7cf0zJqs#gF_~1BLStTOfiFt1tQW%k zFzniGRylmb?CYlK=ud{=6-Q`E7^K>iY&h6wMX|`6+v|Ep;kG;wfYJLsGsBt($@8F? zgRi!;1|;0r_{^dOS`pO`^P!RTsxWnhI8v-jjnEfR<}1Nahnk;Uy=$SOug$C~aKf4! zep0w%zGJSW%r{=%_pdW1;{>`!7yTi(;C;e?Kjr2(T~qe)0Gd{AB+X1DXF)0sz7~O! za+&d2_Zn-R>=7)+T&(>867zTPbJ^pAj2?OElAHYV0@RySF~a{u-F2< z-sjo{USyZ2(VQ06mgXJ9+yAV7M_nB+;Gg>^ksIwKgx`|~;zG~}g|4CKj0Lphd+(O&hY=-=TwSUVQ z#&RBLHrp}rUmm~iH0=FW$bCidq)+M%i=jPl*JK_DV1W#}qGS5p`@1J>tI{sCt?sR) zKd$$#_o_J2gqyagmyIhsbR>5G4^i(r)8r<7a=MrAbU=5ifsQ^u7rPM9C^1ZTxi zbnsUmBWxbRI1x7vo+{38#m?GjDYjinM|A!{As)|*6a%s-2*ocFTO{ut@`L$!9dZF$ zBgZ#RX9)sQ8bixB(R(nw(>QnDs2XMP5pL_1EK353=N@aD)K+dNkHX?ziib$ZyEw%J zE7oPhR$Qd`5FlbfT&x$Wxv>1P<`Zfz&Hecg+@CX^aHVUWWZ%9*BeWC!2k;Ff>YMtcO+pl|vBmw)Z1A2!i{$T&kA z`ptO1(SvKd?_FguWx^CY(Qk6JaA_dQ!K8Lk<6cu#zPBL5BY39^A;Az!T^{`XL7^-m zb8Iby^VShS8}11+FZRGYbN|l|V8?Vi``@)ew%TDwYUT=dv&ApLnP$@)V3bKL10)6f z7rcxrA?6n{9Z)ouB3-!k+IP_Y)SJaxheV;CId4Q->0l^uU=jF25UFUNt;(|V4*&O{ zC_Jv`)A=^|Z%XefubFbEO55eXHS&}nCGsz&{@0%~e5lY04?V%p&`#H1Bs-D@OgloD z15N$g5Fam_Za{n<%g`@W8~(XnUMM$A-oWgC?Kr>s9Tsm>Z>-cu7X4*b0&P|Tpteex ze-C`&cKb8z(!9WZETvQ2pyu>BxkJ_Luzuiohp^v3toi=o>&x9`*zG~`BmNTp2!BDj zrPz}j%njrF|Ap{Vd(qi-*%dJO+T_@Qs0E#{Co}hze>vH#0U`*wCqOl91%J+(5Axyg z7vcysYGoJz`u#_YG5%PF_g{!{Tiwu#%Q9Bqw22bmt9}e<=DBP>8O@E}UmOCKY zK4$Fzf?yg=`02?QqgM1z&EeLlpYuQFVUwr{%v2`&P;QTuT>ySRN!sW=I}a9_9Y~6D zX%QD0ee!?JB2~Gpgo|W_(w=d6BAILohsJ-M|Dkzlf4_Wx!>Ipp+D(6xKSDVCU_8kB z`%53CSs%xBNBH8Ge)=8rp~?6}QYsaLl5R?mvzhNn|(s}+81Y>_<*Fqi(g7HDyPS*bK`KFl8 z1^32ejsij7t?EV)%DD!f4rL(-d7J)O$~Q&|xxI3`;2)EI1OpWI^KU8p47h4|eo#Fj zF4}M_f9O_1@LG>2@h=8|aQ{~?=N;9=w*7H>4??J^R1x&jq(}=95tOcUkq%PD2uQg^ z5CKCkHbg*>azl|`M1cTOm6p&Wy^0b90#d|?FZ!PAzIWg6%3tp{d#`=I^Vw(4%$mJs zt(mpvoK|jVTY+`ft3NMogCVO`A(&Hkj;|3j(>AujbP!HHx5q;PHQjDjac>>*Y)Qk* zXS-;2-Awun-BNw$-@E+(3+zW8S#JMf5$b8-U^7B%84zv(bF^T#{wYWN>#$!+KcKx= zYWY``)U$SHjySTW&UM`ml^*uSWVZtL9)Ihf&K_$-mfs2K8?F0i341QwQCi1)=T!IQ zt_VVY*6;D3_47qU;^)}+fK3rIEvPwGjXUleQ2W*s!GPxI@;LzIl?8?6Rs^!G`S&mJ z{t}a$7@}1D&p1 zB#K_M>frUhShG|z1*@@{vH2R+oi|HaZLUjF=&B}~wDQ&p%0+-~LauA~15{5fPNw+( zhxn$nNC|CWaog}|!7ov4`|(e{z1%ucucdRU@I4bBKCHvcfoyK6;mc=Y+reW+ZnyZq zq#*)3Cl5&W3^r`zA(@mQD-YWDvD-$ZjGM&8HVGl+&lPW?jWn;uP4PAy4K!FyB}!Lb zcCKr`7CQy}4z30$moq2KmbhEKG+#1(XkdY;?tXT>ytuJ=#nA#*QXFX!P*KcjIr6HQ zQnFdyjX#cj_TM6>N#PEPK5WT;zZ>`IA^O9K-NEPHZv&+#rawK$eefY6Cr(~PYfq%Q zPoT$>3Sk%3a|6$Qj2Y*y74teboKy&Y!W&BER4+kOE z5w?e0sO{lKZ+p0D+a7LWwuhUG?cw&tZ6#D;yXkSrj3Ew%ojzr@o>8>hH~bUtpT^LG zI9*jz=Kd-k%w51}gE+6K{6!?Ws@$79j}Zg?RkSlaNDBux567}f4)_BR$9@EP+M^Or z|C!;r;7q4)c$%6%4B})4NWgy&haK?uLw}n1*U1T$>rq`zJn%2Z!K-1Kfyact3-!5w zqd#{vw{^b`L$=LOJtjD5Wc5Msz5)qff~-53-1jHpOPebWTKCyWvL($m2ZQ?tB-zsD zs)O$R6QuHzY-?m?*gW7{JLDkt6om)1cWipYq;W1g2EmO!zGJ>xL^P``a5m}4ibGK6 zTT;PeqC%y+Grm157Qus-*fIP8mYwk(S@8$~w9Jm}>Pw=3<#%8ogX2Pr?3`J>Lu9WM zaF%WV0i0Cj-eDh)<3~&H z*sRtPy(_v$^OIkDnvz6d zn7>l@h@@4XZKB`*USPcxE%J)MMk5}@x@Jo0#RKMvhoGTLi3ea8 zJ}n1r2<FaBb-O%j<_93JyOU;!==U*$koct z#eJGPhWj;;1}F-21114GfonV}Jo-Ejcsi-J!K=(0&P(6}^QrOW^Y!y<^55jI;(yIg z5I8MhDiAJEAuul}DyShCBA72&CO9a#2@(dGgSx2*#>;luEE@I4_mA-GV4&;uc?&MY^1S*HtIFXA$^@C$mq%qT=wYvOEt*HExLxpM(D zS=O?kI$6FNqLEy&`DTGJYQzdIJMX)#uD~>MX*!|Dex9ao~=pbqX zmf6MD?SuN)!2Ro0-3Jh!Ui zwBVJaL!Q5-Cw2ztB>6A+$yKcDpSYRa9hGAc7kY)Qson$A$Nf$O_3=1L?=oc2-YPZ_ zt!hhRG7J2pZD=;_IW*deNy=_LYHMXnW8m|$@(-Q}(jr;c#7^$k}CsHBs5F8ec%kvs)%DVL-8ysQD8k~~uh zo!}`#xz$3)`Exgzj@>X=<{SQ;`Qn%>QN?pnN;_KZ)7gpfHGN!|pC3>k5523Buxb4D zn{ubb;qyh*&6@; zF!tzIraE%k=CNCsUiADHduD31Z! zj3mB`gsQ&mS86uIoZic8$TxiQl?iqQvKbCHf_S-+RLWtmo7z0_OUT+^8Md!v6#{a!?&C^7?T?CmRmK>TegI+6gH-7xF z!H$fQZB*=Xckq$7YS&=zeMAZ`ytB!$+3}mHW2H%|wOi4WrOjMbq~+x+iM368@8ZUc z)fW9k*b_L9}=SL=2at1o#}Nh^16+&BL8C7W~-Js zSo}&y#Cv4Ow9mO8_ueSJ!MN=-mrhWlajv;bOhrr!eCvApN9+BwQiiP&!D)Br&+6-# zIGJfjK9Gn>E#Ii`ao7;yy&2(KsM0ZlsISk%uMLRLvz)=ky0X!>Iay;i$2s12dz{oW zjPy@-3GM!BZv?BuJ}>#;SL``{6@lxKPFY=fCr!I_A=T7gJapf9cF}q$N7K<^oy;kW z27|~qPiuX-j)|cLzzvLrjZeRF~^5I}pz^MK- F;UD<^W={YB literal 0 HcmV?d00001 diff --git a/webapps/docs/images/fonts/OpenSans600.woff b/webapps/docs/images/fonts/OpenSans600.woff new file mode 100644 index 0000000000000000000000000000000000000000..e83bb333d2ea4655120d1791947d19d28ac710bc GIT binary patch literal 22604 zcmYg%V~l7`ur z00R6Lb!h;E|LTOkU-^HT|D^xFh>9x900015|8hA0L3@Z1QBH}T@s|t#m34oiUjW;N zVq{}r|I3~J!u%HvQKy^fMy}2T006-9ziqUBQGyVU#cXPCX7kHc0RVuM0|0O!dhwt@LyKmcHyS$mlNa>f7v>_7kj%55l&h5+U!2EQ`@uYCaKU$DNhpzNFf zlD}NmuZ;f-61ZlNcXJzO_g}6H000Q(w=ebjA^B)46GvMB0Kr)R0JvX{7z8E-9@5&* z=vP<%@OQj5|6%r_Yu(1c{dX*iNx!xT{sWLAAfK&)jma<9^lJ+p003+T1P&?8-ps+60|jNdZu|$?zz=sThZ!|E-{!Ks#4FG4Hp+8ZfQT@`RX|@bo3J3~uig8Nvigk+i3KRM!~uLEqqz@XYxw z2cS{bF!m-<()@vtI)Jm(G*#A@I9b}8++E(EK!Jh-Lc< zPcWTeYIIg6fAwrQKl5+-z6UaYGgr`xlEU%$$r)6CQD~<#DzArj`2uc}w!q0&L z(N2FNZk$Gm{u3Q|Q4m%hQGv?4nB!{1H-zQE$fA+FbTIDtmo!nSEo%UkW?~_j-qx|P z4(;n{$Z*X4u{QJ4p&QEj~w~z5)``NCkt9a2PUqdRj!$6P52~@Tx#Z9w=WxCNWznsMrrZ*+9l`sBMC3MpPR|$orYH^ux0@9Otwygh|Uhvn#K}n#Z&?(*WK@c}5hV}ynTN6?@9Yukd zn-ddQw#6mGeumr$qWxHtZ00eyCBx>o4~8zQgV8|#tYWXmdsClML@PD(MzM+#jg&9r zve_?`YB$K!g8iK%mMU5fqv|S=j~{-kT80(YrkkCGrj0cr=mc7fDd|S!2|WVD41Ex& zeYTZg8xzb(i#*TgHx{&6YnPz2#(vYR^2@V@St6RAFy$)Qbwr3WID*w@9pW*_1QL)-rpb; z)i~-yJ$%WmFh`;|DJVm>RF>}oR&O2g1Y?>LjA&x%sBE$nJfX3Wl4u`Dhu|@enYnu@ z5+mnCE>Noj{Nh2ME-!02Cj_0W>fZ9h`}7^)8*g-K>dT-If`^kDLdS~zQ~g>X6m)Mj z8$IX`hn+dPy!$WUpNw_l=8)is;~&ac>OZ8^!FGr}$@@mZu?OMz9x>NEmtS!98U8|7 z`|&8bpJzkQs4i4}Qlu?Elt18;KmZT`P(MEa@a|d3-r(NKD$hT(wbD1F zJkn%|fPxU?B$z_~W(EBL2#Nj?;KGCh0Ra(-hUbgd&eKMQBWef>^9#q35j2FW`-Y#s zE7c(kiyVfZK3t!jc;E265C$G^e}UZ~ybwf8mWhnj6atYB zS@y4rmnmGMv#u)$a7Iwp9hqs`uT)(6c1nc8m~*STSWj22R_$ zwIL-M3phe!3_~##P;iiBhmEvbMA4O`ixFeMK(3}m(WI!CE3LA*HZ(4|$+nC_8gF!eCSTNh?V{!Zemm|1c>7R>%Uo)Y z*Cc3>az)r3yn4Gre@MEfE+$=4l$VRNKG-cVKHH#TN3cDpp~{vGXsr=^R~YW>gMiFu zy1NDJrdpR_N(y0`OH=IZ+LOO)N(K^m#_dE1N3*C-TGL9_#I30wI)))dmp7S6jbAZ| zhxQmy6_c}XUI+IWuuVv}67ABFG39yJIlvtl(wNQDn)w@q@VzQ&IEgnbWM&@E$PCz4 z_OeV*jYA)UWzn@}9k+HQ)@1?Tc&zR@YRRbI`guEESD%1uxA*>}H0NGrdU?vlnz5nb zYin%Tt;!zX@D>&E@%HTdUkrHUpL+lEjw&TZ?^3O|PwhS@ZC}}r0c+u`A3f%<@$y*P zbHA7It%nDPdIh;xKH^S#c zlY6a(?@KYV8>4f>HHWFxNz41NxVP2|6P^15s%Ndn$;9Nu^>u9dWJP6fHdJlRj3pN* z{|v~}loV`TwWi~KtC{|;JlB%f&y>YBz~^6XyW>%5?QOZ`dHWXfXYhsR>z3sm_^ulK zr)lAL{&MxajjW&3c+mbr_Fc}gp=_VoggIr?{lX%KDk8_I82UurQ!Q=K!fJ?APSKmRh``&6K z1uE#k!4M`3SUI5}B)p}uyr0qpT%6%+G$K*dUntLt#CSLX@m;>DD(FA9ukBlg&|oC{ z&a0c-%!(ZjN9iJ!YV_>gmcZi!nx8Yc;OZmM*fclQHm^KwcHnJxb>BUlJgCMbeLkQ% z3m=Bi@!cLRn>hRMZPp}h4o7Dpm8$fdgLad?W;T`EBjk3Up)=hNSpO6)t|dkw;l=}` zDR_;+SnI79EuJ(RMT`W?cGC*}OxMiNuAA^YZ}v6_VX;d0lPNPQ^?KyrYSEVQ2whi9^w#i5e>XHisEr^WF(hfo zq|F_K=|XMYM|bfjo<8GZ(W;u@7(XHz$Z>HJKO9B4q{wD>dHJ)R9#U+Zn2U5E z*mgDA{V>y?UThY44(l-j2?|!xi%Z<)iN#GWX(Qqu^z22Np-iCkCA6^?gCo7l`{=kI zt*PjYo$F6kC95nBKVOfx&o`!V5!LaOeeydJ?Z4y=Yu1z>hjar#!ZFM=Okj_80PnE; zF>oG&Xe8!MKqFw?1V+`tMhLNXZxJ+YDxO`-S$UBEd~xzZE3w>Cg%)mOjGRxX3k}z8 zy}_vOF+ez2@C{|SC!mJMW7q|yW84WD`YSGQTP8^04%&DPe9d+tk1DI$iL09X+S`Z~ zg2FFv!coUF>oB=^1bw5yXWTu!pC7?L__$1$57ctrcTHbZY8e~M&~x6-8^|L?Wv*B4 z4$=T~FY%f$vU_(HDc^Ac?xz4W=jBu7D|BMO=Zy=wL@cjMuhTHxcI>62=vY|F-Pl!@ zH9W&yVCTUm7?WEm&XYEiK^$ami*7$r$EJZ57aVN!G;qf~5XL|oJeH=jFO{AbYU$zM z>|a|dzQ|2gcJpK9dff(tlw=LIinRpS3R*KPv}>!`{n~HZm)GQ`(>eEMpYl!8by}-< z%NE&v37osy@SP2XL@vLJ;TuHhdK0TK6Ymiao?$X zoa8t6`s0)a;t=u|fGn~mS(iA-Ryd46Nc?9eV<)Zl#2!`GNhn5Am zNWd8xCN()Nl^v7z*cA-fIac%2(zYWOl(RG8YC<>Zaf|0z`OFmi0@$%X`r>EY`|?%f z@;nc!-hj=S@G?`+k^pMVf?`gMAQu2$Tu?)3*To_NK7n4%JXPwHCN1GJNR}9c&~BGD z7L&GZFzTn+paOZ8W8Io~kqAaa7T)*vx2$XD^_{#ker%nCGO*pzkGmkHa<8hYjuX=4R9W_)#`lvzDX!-<7-rIC(G0 zG4uurh{*78W#U2%$#;I2VkAsbUijDo*(k&_?aKV#Wjr*x+%3lsbkg2RheH%cLVC-f zzwYEjGj4z`x%!A+K4?sV%E-7 zbbO0;4hUtd2M1r}Z%re@v6i|ir;SkXug}8m+3O>*81KJ#shK@~3Gt)384OPE$F_2_ zf~o^x>M^5+!8SpXHMzY}Qx~ghY`J>6J=SM+w<7{nqI(`97~l?D!v`J7VJ;{KtQ_wl zGAZvUO6V)Z8jPF^>Q3Kj&Z;lxnAu*ZPF5TgR7VeYo9(#AiE*eVVf(O*DI@|Vu9^NM zkxkkKqEr@Bf2u{&S)EpA32#KwQ)a4*l-K`I>KdvxRI)Li&DgU1OZXQr;X!K;X&GRd z#zcFFB!C0ds>?JC>P(~~S_?=O)WlP6iDD+|A&M`8H@EmdJ|nPcoVc0l#J%4SnM*z%mt>F(X*)p zm`4(N+dW8Ty}6PyTAic-gjw<@K(sRj`EOXFRI{#U4H>dss0KKm2?LWl<*a%82P|&w zVGHvj<>4L45b;`!^fO3)yPkb#K8kp=x^wpc8X-%$9{bYv7?lmwbR?_fwV?|O(_xHw zOQH-rqSysdkMZN9Z7estwbRXaX)m8^v5Na=Twty$D(cJib~gq-gpB%{+39M*chLR3 ztE}fsvaZtT){2pPFN~|Z%}&Af%d^s@!~PcFyucm7ga~LEdlFjgI>-7%4R>NI3nPd2 zl$KPsAeIk!B@5Saju%KbFAqm%NpYbx=%DHSCEzU>uc(5a-V^ooVeJEDnW(nyxibZv zh+;P-kVD{w33zrwD`sE@DN7vf>L2Y^aE_JvegS&k#TSJzLaVnt^qn4;n%8QgmDL_0 zG^Xc!^2=A%tS}|Iulu8kE)Mg509`PA{vut^ey)vdtN-})cUUf1MaXXX>Py`L zs-|ENnIw>4#Y=$50;?F(Oe%+^BM2wL#p^JIvHBkr-e80Q7}9I$^kdN$mRITfXCl>> zbF@^2y3`4DQ=0p-DkEpogI4Ms_5(I_#?8B$-ky({)EXq~9L0eKK*!2W!yWM!iIa{I z5*}09*ch05SDwYg&d z%eWspgxr;5BB09!{fGthbChpfzASR6cU8t|fd(sQw+@f6tl--d&(A<-@W-(Qux)24 zN&l((K5iuB3bp;o!ZlQ(KX}glyx8^6p@)p}>A!B!)A)VVN|SS=$9|x}uQELbv!!0Z z7?Y(0OfnO&gjb3RK;MoVq5>92DE~nV1Kr47`d0?p;+a8c8Su_BD@|jByvofAhCe8D zn}v1E-5ISu25${piSSZax)wR)eo_!ouFtCp{BWiaCeIXJ%!xb10q0I)1q>wXx0)AWAWiB)2uwLWJcDJItOYq>s`bP54ChdlGvQ*^7Uu1wyJ``B` zurZyqsx(2kPA0q2mjTw$!#Nq0Mzmyr%eUa|oZCBP*TMyEHa`uNGwPsO8>ZeWoNStc zp9J^;=M zjR+Al3QH4b*<3hV*x_qm(=;dnTRjX3-M@r|!1cS>^|aZ__<`P7g|_c!-Nu-|JL*mr zv{vKAK3H4h^dKc^n7Q60>f(9!fDa0YXi>i)KO06(Zr>4%~ z04z>|sqx7-ZtT}RBsR9!gol*2z1MQDZ$+k-bnr*Iye)-*U0?VEgiCHeiC`Clg1Fl| z1=^O3PbcU|!+?P#_3Swkg_+jz4TvDmK2>Ht+s0*jI*Jz#4{tjy4f#;DMFmLT^e zC6yK2Etymw(-`s^mb-iwOP>DY;L4cLeh$|))45&o)U6iyIfi7*nG9JWu*F|d6jBMR zB0BPVi}IpWN=AvSLo()-V#s3mr}Zox-(7%X&WUFiV~s)qS8NvZyoP~{_u>`L zgR(-v;1iclBIP1P`DFERx@A)f;RVoeB{LGkHVT@vCO@HSFtXgeksRM(LQ*yRT#g<8 z1=zmoU_-;^>HRv@yyN{p%|8~KcV&3b4LFbG8FaExQW-`b_zFeNfP=qnGC$RE7W31zp|*OAn#RM-f>|^&M6b^;#NMvjaCw z7Km)^f$N3x5RcFnOFi*?@*!U0zE;%BXQ6fZvi2B^*F{=3;%z*8`-6d02c{e>DGQF3sYy z6mDfG~gGnQC=1hB!m}b;H(BkaPkhEBfz3zfHYJr5Q)m~ za1R5t1Y$vA`MV$geZesYkb*$jNMU_7>naGXulF)=A@u<~ zMilq~pvbR~Vc?=Da%?LqOSyPyPW*);iucsO&gNPnk5yL-rZTCx!Ig7c>w}Mx3_m9diI#ojLCsl` z0L0-y7_&Aki>ru{)uX*K#lahb7j1Ca#R~YTW0PShj9vLPuv&u?f2K2cDu1YK&Hwpi z1zHfOy@}48X~;hByIxWcbn-;jjcmp!`v^SUkM)1Sr?a49)w$r+n4$Y ztcnvaS`#K)uEAg0IG9JzE!)N~MFM~NuSZPiiPM2qFx|n7sPYa#@UtE7mWO<8>57}( zSJQ-gPjLFKWqN=oKpn3aTp$Z2m=V+tSdek|@bFbRdIV7}^W*O+EAs^=D~2$eV+XB> zL92)~1yp2B=oVFF#+N)Y9?z3K%M7}8*Q)iC$%)G@#O9g(Tz`5-!c?Q~=D#|7^TpK< zR#a5@cjwi&8B4pF?KbC&^#E+V)B1;-+Ur^Is}Y{GY{lgC<{S6wj}Q~oK~f84jTzsp zZ>l`M`CHK))C2QiN&%NB{`;Ol|c6bHzF|u42`1KfjSfs&j6N=V1?FTBFy=l zE-uj^5GKF`I3o`;IiQ#=n>3y$>ef-XUdpv!MNh}@H9RY|y( zreV_+shb|}$@}ljo9?N9wsnaOolDMaV{79M+3||e(H%Moc;XEN&~Yc05f?*Aq4T|% z@C*L8GHFR$25tnvi7RMsex@KFo@)bU=;$;~`uItxwwo0TTs9g<->n~6cPs}NJOr8F z1lXBW*pXUNuwY6=ECJR{sw9|kYm~#3B&&tuuTE>>h*@WFMR#3$PhO;a{<13V1Oy50 z&mWcezPxMt)!QqAd=>{09EWG9a+6*Y0a^yXC;nuw+)b$3mu~ldaS;9WQ`Uxu9hkD1i)uz1LLogAwjq*Xc*w{QfrKNPgQUz>VaTw860v(pxGbe- z-loJ59%+!%xWa5A1P0LWS@Lfb{_*lg}Hh+ z%}YrPmI`r=FjuCqS@6T;m}>!@*+cm-=lu(c;}4kewP)3O@upF-k0M(gCFYE09>AIQ z4D!SDDUT3?#MH`aMg# zQW^_ryy2ClrOWp(bG90-71>eBgzO*uCGB9|o1tRm+$kCQYu?M-?Vw8^*E>JI8h7SK zA%j*xc%KJ_<1}`(*4@8u@KG9F7XfGR+|2Jm;3G*q6iKS?-6n6fcnij8Ma4FnF1tbK zs;RUQZo!S5aOP)TZ}D3VpcA={>c(m>Y;lUJ} zP-9Jd)Wa5ZUJ8;eE4CquakTL0{c8zqEG49ER zHukt|8P(C{e9GYprz_aV{rj1bs|h&Rf-^YHz};qv@@-Nb}n~ z-PaZxYpx$KG<%;3I=pI0tyYp=w!1hBtovyiY*gPaeyX|&l4?%5^u^A06U{7{-rQ}q;-6w)IQksXyau8#(QRHyulUAPy2Q?-<_6 zC^;bl=)!b1c+yFPWxeki(tH5}m|3}IY?-3Eqvd!=kAEP5t5I^Z34!LDGI$@!$~H`> z%0Vp=CqgRrKw4|Xh~2@KSLXqqE=dsN6|wfJ%?bA%!5xU9yI8Zlb$RA=LjQPu8u_?`4E@*Ptrc>V~cem&Kj z0BT>pKt2l=#_Qs6ScM!lej!5WR>FeDfNX3RS;QXdF}kr>x>5&Rq?$%7$9yjx=E!V|E6~g^VK=iZdD!=#Vy9!m%*QGH$%)gHg#N1GN?_R$V>FL}H zcx+qW@O5P2E37@_KALhWOKE*tZZ3F6k5Et#;j#)wz<;(Z?<3VgQZ@;btXWsz0;{K~ z+H^#n#I2aprdo)`a*-k`@RM!1xBbz#f)A7=ndx^e>}&zy*g32N2Ffn{0l(9;*J_$G zrBruaFJFB&_Vrpj?(FCNF8%TCt}C`Y^!SovPWnnmpo`_9n<3llm(FLcaIjJn!&$2_ zIYg8Ue9mwM+!SoJ)$A+d@T$;wXSeW#mNR2>Ja0S!oUHExutxtI0C&0 zW|GJyzYF(?nej5qd`{|rw6w;N^0ErJR^|*5VB(1m(YW1fc1*foUWm?*xTsb<@$QaeOct+? zn*?4!azVP?S%hAvQRwy!S`Y?;st13TYa~^dA8LTB6?M{>d5HxOoJo>O(%`7A@gZ|% zfx)0S2RIa-6R~X|8zwtzzQ-kCg{Cdrzqd%x*bh}tn2-+)8Ze^%?_A*sP>~=W_Z5hL zK@YqN%O;aL>WzB!jhDx%)AnK+2t$w2PV-iy;ZBy@Ypr9)-kT=3m-_XaD6JRo0r+rb z*}%8Y%M|nhJ;Y=BvoKTamiFVG5w@E2yDmf%b4^bTwSU zl@nQ98c0U63CXae@s#l$i${1y5%Leebd_!?k;Aqla(7O=oeK3CjvQU9GX&_an{3%s z7QzT24m%<5E3Ha?yVGV>n~6j1Fm41FHu>=IT7Tb_->_UC)SVDWU-|~3Q)poO?gWR$_oj77p+B-A zEw9)9ex1XCscw(ofSQy37CwL`Km*kU&yO_VuaE>ld zWJF|SglA$q3)Wpuj?eq^>2fdrZ71seq|6=js``vPH1ZtsTrna`3J4*k0trcr8wMgY zps+-+dSbVmdM{2wzJMsGS*Wqo4QGx2_~|rVGrrG#`a$HYLgks6(DPuDA!obaXQz3D8u-mNnB0v_`7E(#o6*z0QEfXV2E_dkT7Se(`Th?@X~^5^GL|&|Lw23WV@=>6eqou0 z1+$byz(x6VwsO3Gu41DQUb3_J&JRtv&j0+0j9V{HG<2T97Xn?1(Z*c-%&(&+=9EgJ zf?ERvBhH+}WPBO5Il38@pB`rs<@k6E$ABDtKUdoqsG&GjWV5+ECm!Qb7pOU%(^$F< z7Ym6`rg0&S7TtjdkSF^2bWXqJ0mjiPRa1C@>y#K5T|SnYH!JsT7d`)Z88xxz9ech$9D645y@sOf$+E2$*{!;NG_GQ2=o*(+Mc+AAu*J%xcetAu#;hvVO)1&QB<2&7U`DZ5E8B1MC|vHorFG8{nd z_NwZ7UH;M%bMbkshZ7^?{#wrviLBuJbl1Ic`_QrB@El4g<-D|F^1XW{&zZRp{US|1 zd#R~ZPoH;ma@~CG>ItZsiSgsnZm>R++S)P4E0do2Cj||c&Ph~QcfpfA@`#ma)sBC7 zf()rctV;b6CP5g~PZscQWHh;0E|M-2Qze|QuE$*S>|&9qg)m{U07~yKmp>iAsxx$k z8-yFwp5Joy>ZL27)dR-#Zri+wY8VAwoX_1CK2^@aw05(cfqQLyhX>)x3T=lOojMt6 zJoo@+`NtjjPOxJ|x3!x05Izhu=@bi+V)|Vl+&0Q-F%+*f=r%IUCTi;p`Jyt_48r@|A}OSgQWr=t22+E(o? zDNbb{GOiB%C=BUo^}u2DU&E42Hzi*8cFx#8v`BuCL>&NCK0HU7yjQtISVF(8dHaAs z6CxW8)gx{pe`1_{8Ec;Ek0J>b6?m5m*3y%+eB62$&4@X_cHX%%JX znWTYx$7RS?^8BiR;^MQc5k+Rz+u7Rq;Hb|ilw@a7qSI#0ehNskMuA2BKVe2|GF+Z@ zT9GD~N;{Hwr&O+>qbl_>uMoa@5g)dw8{!SO)A`m+8;n4`Xe+l0(JX@uf!vuf!ZEpj z+>9WhfoNgPpwz>g7H}(Xfcqw7bK;sSkF7l6h`Ei>JG@$JTJfY)@e>fvucB_M-{tAB zkf0zmyT{G>(IT5&6f>X&)0dJ-Kp*n(>M^I5jj8K3510`@q{d^AHA|RTI>?~j5~J1T z2p^mjyurCPo^~!{mEA5hHVf_0yL`XoPIhn~;Zj_!dmfhjSokHOxKNK$d)MP-`h!+g zIe#o%KYKL^cCg!NIY1=VOE04V6yFB7!wI(92{A}U(dt6(m~;Qc;p;(5!C_t{AWjm| zEf=^b*yfHF1O;{zA~cQ6t(kxJi$KHod_49&0k5P!U23qll$lq_GyI%(LZf#-w7TpK z)Ejl6r$(z>WbWlP(d=1;3t0AI^K~2iF^bl0(u5hAuj@zS7{2pfq>Kq=`QgJm+y!-6 zG=O7G&mq2W-D|$=*RjSi>U1q{K?kT@t9T^)5ON(qCm8}*QK%D0 zFRr0&sG(M5G=g8#>^z)Ul+Evp3k#!Le|}u=heq2YEjszE#m8KsBe&a)I&&i1>2y^M zQT#~oZooW#6eJOMC5{Xx>E`(>*`qvGG!6&z!-2ETF4rJ&QQq1oG1ob0+3L5Y*#LFr zQR?Sf_(jCns<)Fr`21t^bU;0^!_IKj@3VD1jvv3%J2(;lkb~ND#xi-6HS4uJ5Hs_f zt;gF+bdq!2GhV$hW!*qeR={@|DZqpB2w)y}*Y=62YdyuWq0@$-)z%%6!UOVW zO}S5)p-hKDfjk0>+L2R-PDZ;*#V%pfw0Z&6jwr-H>1%!-pUaRv43Xkm<*t1P`cpG` zX=Fx?u4+5e(7Mt3a=!;CxmI^r%Su_j=Y0D+2+6g$!L2x^!k4@r%b?>BpmG9&gK#=r zd{JmCN+f!OB*VP64$@>9bvn9f(r495Wv2HClzZS2Gz`u8@`taK?JhuXZ) z^#cwq%~Lh(@BlaIMY&Um^Qa1L=^1)qY%hmQcYc;dW?C$J36LhzVMdQ-<*`xaHDwtY{M_(JJcZG(L>K&aTa+^@+Z2YgZuKd##lMW4O-Gd{^J$ zcpJUz1^>w(UNyfuudw;j#DSTm+ieo-*?*dw(z9d^g_T+)4wVBGeV_W#^!zv<<>pga ztO%n$Fs=MX*R9P^e2%hrdvI|F(}FPvF2VH12PYwMATA&U@`(Zr`5-Zb(G!0u?1@A{ z;T)fv%~meGeZD%fC+E#}`*=S|qqZGyXCrGb{md?gAd$&rvG{b;8%e0l`m;Q%`4N); z%2KCK`gg$+$!VmoDa_9s_M(B;m-Rs6we&=!rmDEZT=cOj@3FfUL!~0JU9bVcj|hb| zB(#ObhM#0SLr7@s<3-|UlWQS16y>My5LZU=mIPV75M5y>spo%BO&u9&4Xzf9txCFF zo_JptIx|HjyCFj3Mi=Ot3t|vYR&ct)KzVxzrr<~Im7M> z#85{FT0l}#=(cPeD49nd2W~{~)7}Zo9URc|>N)n-$=Q6!T68?Ugr(TUxF>Cy=t{M# zZ27l30T{J`0bA;bSSQXuc1FoAugp=-Af;!p)MbZjNPCJD4Z88yW-AbHyLxr=O+D=$Iq2nCuG zWUIZGT%88U=NPwYRdXgnS`j>kLtxXe&YjI-MhWOV*1%#%xcn1SEuQME2}-?*mprlJHOK&F8&@T4d}eo8R84es3w&))muz$^ zDo3v+M}@j!BGAo6l>^qZ(05_<%t}kTN&YqzX_+GCT(2C1<-$H({+-R@$?t4@U))@z zr56}*FGiz;0*U+rd0g&$y|95+dXqky6weI)Eq!2MUq66R82BuvS-5#I<2t>_U^KE> zPC7itbB@g0d5an^v}pq& z+6&Gq{M^4iCz&sQYK>wFzs}PZON=7UN3;0378$1g-;QzO-VH~EUoun-k@b^Q1!*TD zFd9)!X!eX`{SCcqM6OR4A_}u37_vgu2U5V#HT?MJfOxGi{k(6>w5IEtmz6ad(zxoY z&7OE=@G;>)2&3Q$YPz{an}crE7kQ8`dx1OwP?bZ&j)X%Y5GEWwP4lj}B~fGCk8IUq zAsm1ejkm2QX@u=2%E}X4!)waVaWlMPyU2>#QzGQ_nT8KA2*)N{c{BKB{4`5S41Y_v zfQ$`Ji|dkB=Tg2^GZ0Y34L7DJ!!%Iqy=d-@u(e!!m@4vHQbMt83h&ubda&J&G z*c_I-DlzmvumB#9a-4v-*2zdf=LK=Yc;>%~ud-!d8--qu_`#;`auP+l%{Dl|$Q|}k#Jt{S3 ztUbCqem-Kw>lWaSI83tnau$imXc+CMy`;U9CT`; zH4E^A*$$Y7-#n&r6$)dgm}tSuMlqFQ?BwK2=2KKq*e3EBa|tL5JXz}n6j@2<`#ew+ z&kCN-rE-VWXNtLj!dKn_WNcx?!ZAay1GIK!4IYa+(@D%Oxa4IuYhx4rDr<>}tfwnG z@L8G-%_n_Mh2K2q^*>)H(d%B$tDSZq{%YhjJ+mgKd#Lo?K1LVF|4!_*Wm~xw%ZJfR z_Te^u^5Y|7lWeCZ*W2R1-mhp{8V}_olkSiw!mhX59EPj*5|{(k(P=#`n~8cEk9WJB z33>%tRu1OQU%FDI=XByLK2}h+UeBEu9^aexe%{_4inQ&vPdf^zLGli3!8astON6ImH#_x8Z%?s|BZ5^vmCQ~N#Pny_$T_~$sY-3ZN&9SF zxb5#$Mhbd=bvhKf6y*X_o_KHdGm~Pz2GNnCuA0jZ;{E+~!ge?%f6=G(ygu4x#L#cy zz4rV2exb{K*zN|n%=|R8I9_!vL95OX3p@jV0BF(qg+(KWxUp8Jb9L*yPXzixQ$ zaMnrJ>-OERapWn#dpdu=IIym$E)sitwO6J!hCF}B+E!q@)2yW6E8 zWQ64}R)R&@gr@OYJcFmyP%;NV*6R%5F%r`gK=+IzX$8iACxMm2* zf|aFdDU#xT4uq-I^f$!dN{{8d0@o`|?4pDB*tqHvDZO5OWyW|)Vz834i~Txx=g0nd zUpP^;!&9Y%-PL0F64jcFhdYn{Bm8SZD?OJIYcddPND_Ty)Nbv_>qkc0;r0s57bQo< zccgnK_@pC;E1$XJWcss|$4$^;>y4g6mpcfD0uvU(1S@{cRxT<)+(ZV2duo!^R}tgl z34r!VnFl2l(AtnII!tp3ZE5uV1YB4pj&dot)S5pdp0y&6MJA^9Ezg?UE44cqd_Ry< z$YNvNO~U)i1gT??_w(@pp87qNJ?TK+=#IM;iZ4X_nOOWn?YUcxpLZ#&;>d7p<8K+L z@%7()IMIZRJ^P}T7V_~19y*n=X+|W0)IAC)Y(^x;@DNjD2u`Pe1$7#w4-dy2hu3(b zfdF)*ZHrgioCI5CSk@WM$yv0l4^lZO%^z#SOso;M0V4{PO1vx@ku&u-Xj2Ak6>Bf6 z&l^$joNC*@fm@q|?{rv`jnEgrE7_NcWGSkXDs@%DA=$Lr(I4M=heL>I;NHgF$N0h6 zx}BcfW|Pd{n4=RtrTfWPKJX15=WBm~$U}+~_N!-<1Ee2m9uyAY^41v`!Q>D_GFcKr%8ocsvX!WBwHtzOVap)t%AF zjLi;bux${I{@%tFP~K8^K8_*0J%v%ud>--`}SPlouNGa&suZknq@)Q;Jic&*JCV zdNyy9?E5pXvm4Awf84e;U4E0SbMv^yw4bCQkU)oC>I-t(l_kLskip@XqmzFQ)Y}cL zX?UVaDa`aY3H{R=%9aT7ug`SOGiv!}Msg{Snb;@pTSB}a>n?P0*xu;gY2RNG?!6?C zm>UTCZ+5I!FBI{80>m<%%xo`GXs#WyjaKSUjFM}~W{UQ~&A$0^B}CX^7xQQJ_o@3E z;`uft?NDe;$Az7rVri=METK9Y_@y*W28i9Z0GQnE2W(<~1 z8tgiq!UV1r6`n6ZZ(A{kNmgSJdPWSqySbZPPhRnNy#`VNi69+Qe}11`qS@q+1>9(L zVD0cp5=3jm+u^8+n7GQ%gKPaVM&EjSuHUT;wFdp<^JZ61>jn_4~*iw zDipfO?%|O}UjuGFoMh-TyyL}#+A==Qs5PSMt~$S-F&$#z2WP8M)sDIJzoNNa)De-fVJ2I+yb_S`D4nd!VCq zhIJU!xZ~Vz70H_cFPUqPGwUvD80{k~9Uc2pGEml&NKZ+OOQcJLC4kV%I9eHPGLRYo zyBLAHbY4+XmbtNuv}AgQUDeB|4FktJ_1`jd8U*}0H5(`;<>D?6EoMu2IY~g@y4wN$ zY!SYvgd{HjWpwXFk$A^75dv50F$p&<*D@w>e@subYP~YzN>_^ZE-%P)FM0*^RW?|S zLB1~5#$!sk^rx<#c=bwQ-R$T4S^>3x_;^S1ely_z%Re{%%|K7eayg9&m*ei8QUmjV z14Cecdlp5C`d0gv3--bFVgM!v1_wqKM*)cPfW!Vjo!!%~P9Sl{PwF4Fsh$`>ia?+m zK!E@0<-DWeTE9N7_d)bNjNU?&5DXHccSegAM2#LT6H&qt!IcmtT9D{9dL4`wZA7$a zV?sg@MhO#PypntGy1#qh_a=Y+-m{;x_xJhibDne7+Gm}$PFYWCK1t{Km|iKD+}Mmc z`r*)>@ZfysWNsqTPqch$HPj~8v>wn(#mN(05N3^!(-Jz5-T9`;v_D`%P7TkmOb99-_o#>c)pW9eiWp|1Ylh( z`wTHkb5dUfhDbPdsq}#$t5L3AH*W;Kk&-q|d2-#%qL{wLH&-{k&X9K9KHKx!TuBL! z`HQ4matW$l1urib5BR(65T0>FmJWAS(Ts4}uHi&e?@*O*1@Zwx1j&Y#K|TAQs%PH} z#|?J|l-@T@OqbpX4L6vOMx~9g8J7e!W-vdmsH*L@@(-fvkdy7cQ$geF;Hk!^y^tNF zOtj79(eJayi&9aylaPWd1a(}@P1bbIS*pH)+UZ6b_~eZNXXuhu?9+b)#qiG7S=FD#vfe8=^D4V3gfjY?j<6jGzFc!UrX)a_ z#X9Bkl?`wvJ`>)rs$lR~fsVULHQOSN#f(arQ?qvh@yuM_1dL+_Zz<4tQ-AV{3n<3n zjYy8hhh+1=YxCiA4I;)YscK)z@al?m$^z-*>k?Fwln6gblA#mFPh>R_e|UTD?7_qSa(qT{Z!AI73zEqPd>Y-}keNHns%O0#)EsvYH&G-?LiZ}5$kKpv@0rSK zBcYQbBW!WR&M8ubfx`Fz9&wb?vd+y*%W5o$h8`5@33f3UUnO*g0xP@_k=FsG z$6UHcAXn*g1=r&@wVRIdv>cd<(4UR+nvEo1Dwa}0;5a%XB+#N*z{zx`Aci5gJ0OI- zdRg{_s}m-NA)UJ)t0%PGT=TFUET}PBO*BXx~ugf#zzTIEGOOHXf!HE*kWnW|m zHfy|J;%C(m&({#ryBav>l4J)b?pOIOO^vp-hFVDI z7^Rev7;pcDIOEu!N5!sTulo92k{rVT5h&2o^()LDY*GV^$9@Q^cP_?nBXl_|-*?fG zrpedzM)FgO`8hl!{0nhxXD>!v$Zp!h7weX?i3G-oT2qgWBsn!RwOjW$C-||hNcG=J z7-mVobTL*jG!2c%)x(=HxGx80<@X^>Gb?_fz<=@Sk1!=%YM>pcQ)c?!`C$sbL7KeP z5Ai!z7E{8Wol#mw2cR>LjXu(RIFVeA#MS5{jBIg^ZHRFV(S6O1eI$Lhs&hsqn4Jl( z1jewPvel5ST_aYZ@E)o8quzpJc_jB(q!I|3&=s9DYPmZEW3kde*zwr;rHpYh&g(Kn zlbno%0}+D0Z;#VfBseTM!^z>WKpVhpwD>HHvw3ZVYYTzT(Q|NXN?_BTY^fIWeR=S;HQN1{TRE)_d%}YGZ&aa;po;0e=%`va2UyU1Tm8uP)?=6Pqz)&RNU8 zXzSJMawY2|jL7?aruMsL=1P8yhBkfl7Q4NW|4ZtDi_n(T5uWkq_qEi09MSiDrj1S@c4|c~x z9S2XLpG1!)8a>|yiO(+`GnAseg`}yU@Vvp*fjU z!j|q~Q|4FNpT`#NaZ=iH_{_8V-yG_tZ+ed{MX>O~Ujl;y4+qk_EfQJJKsWFDe2fQ+H~MxE49@O&!)h5D;MNP9>B&XBje^aKK?xas@ek> z3E<}-1ttK{XL}|is0VkV+%pJG&@$vk8HMLk6HQ=WJ5&rD>sAlIT3egrDY={GYJz7Pjg#RhN z!kxeQU`er>vtR0iz?HW9E;H@#WPO50vwkMOOa_B@z&5|OFq>Z+q0O&N&F0s}Yx8S! zviY@rbJ+-!{?eIw7p2$XQ?w*!x|3CUG&=bwK30VhvQj(CC!_xN9NEA+Az~ z8#$KVL{_LY;TDbqHj$O^8eGpY<)%ahydF1kth*^u34etfIu_lmuE?`gslERmaMsI? zdnk88?}HUw!Wg!r^Wwl4pdyE6Tcxw6wZ)ExeYx>q#vdSaR=U>J5!svj5KIphIMmzv z4%QrzeYuao%+QO6Hd`&T0Y89UTn8I5TqR%eA zg8}*-Je?J-wQ#KNwR_k>2jxH1`2l7etNZL8budB253RQvW<6`agHGyNI7xy735uls z@xI0w`xT$nzl$5Dh{{s5`5Aob%;!tN_g-h1r$_Tt$ByLE&2~gc>@M{RXTM@XFv|}x z5c1nD*9y2FnsCIjb7{$`eE{2^HIcXV?v%BtGr71kx;V(%RI#!&XL{1W)1z3n;q9|I zcm2=xMaRx%hDGPOFsXC2k#fF)S)N|lslh+m`Ii4Oz)$gZ`o{gzA1-G)L4`G33g>+C z8k@2!xMJ9!_g?`Ib5MUZcSpyy;6GYX#_ZRepSKKIfuQ>VvO`ls7KJv6{{MYZzB#w_s{=&e;Ejv`U3BjWpXM1L$Yc-DBc_~!Uy1bPH61WyRM2=)lM z39k`a6W$|yMz}ykM5IGhM6^w;M4Um~Ng_Z3Bk3f;l5&&kkouF3ky(;uknNF6k$aFA zkWW&8D9k84D4tM^P;5|2QF>F>Q|?jeP^D3=1E>J10Dr(BH4(KGwF`ATbqVzl^#Kh9 zjR;LV%@QpOtp{z_1%eCN7v|{b=xpe+>FVjJ=r!nJ^m7as8SEG;7%+??jMj{=7`K?j zm>igDmtgT3G*t85=#clHY)>b5^FD;D%%sbF*XbvmOY9+gMFNXibI1VjiZs{ zgj15!gfouw73UV00GAS%36~q!AlDq%78edk3j_gWfJwk-zzSd|a2)s^c)(4<&B86h zeUCed`#JY3?q2R`?sXm(o+zGFo+6%Ro*|wEkO1fjr~vc|)B~CTt%44Dqj{5gpYzu6 zcJWT|uJRu8k@2zd!T8eniufA&`uV2$*7+s*Gx$sRTlt6h7x=dX)CJ51yagr%Ed^nM zU4l3v1|bn49U%{)1ffo$ZDEk`HDPDrIN>7Uk0LhbmBK$?r}5PBi1AeLh|cGcqQ#TI zJLmX>c=33I=aVdEzf67OL4TD2cIHJlKOy3^drjRrH3~E;APfT8Sa#$D{jH0-jA^q+`Kh#$%!>ZRJ24Gk(&H0bQii!fAH^F%70K zfN}roKmcGZ$|Bbg?T=a7m$xHDfen{8W-XAW%~mbwP&PvmBsi}>Wg`^nim5s7->3TM zVR1fYQ4o#jxR$)w02hNJ8TD&d?f7J#0la>zz-EyeWNn;)bvx6bXZ>qzH~zXV?w_d|!+gVwhgGQ8BT%=v=>16ysK&yEN2lGylRkN*Tb%+g+q{Y^!ZqpuWoIGzRkz zOoIFI`ljEV3wXrpW>OVr#wN|RD?x7e|&X3JtY~WaEmF=UmS5*`%E`oA6-Gx?$ZKU5iu&WI%0-( zyv%7DtV{)8zPEJKC9y8?il)l9A|XZBR7P7e;flw#8`}G3&hw=Dyl*WJR#k5c-!v}EF9w

    nsHyYcSU)4K<4Nq|7a^_H!~~jqH1Jh;&zOxMAi3Ee5?buk%WicJD-=dV^dLe}|inZ|I= z7oN73NBcuqrmMn*Y}0yfJxyP?obblpxnuY>5Ze&O%t|*OXe%8RCA-nKI+sMJ+maBn z@59u4HCzHVigI;XRk~xPjctVT#YKs3HY1O#_6p}e+NhAd_tnVWmz35nRHS1v% z4~o};==t%dn(4UTN`)ANGMbrb--4Jy^aEul&2+s2Cn1Jm0GUZGCw-YIy#VnS?xjzs z=iJJ34i(l*QpQ@>%d)obYR#BRuu6TI*0eu8kn2)?Ky|s-2tg&+qg9x2x!-anL9Sap zMelN-*;|glaqCT++F`{_uev_2SWXK5Sq`20ego8L&6GF0?a{Oxlyi635YFj4!r8WVjwT>n;C-y>2Uw%5lmk#lEA)TcdYj&;q}7h$!wP~Q!iv7wGp(|*0X zkX>JYkdW$jpD*s#^#@uWK{8yu??WGR3$jH^uUtKlXs+-y3(1y^RWv}TGF)G~lvHTu zM0XP(r?gL=sHm4G6yv89ofm47tyQ`%EC80#a*vuNY8bFROpYQps==d3bKs!WY}H&l zL|Kz+tKws$wZXt>)IMdNecwg9Bo2U=M&1w`41)qxv>*jJ$@dHFQgi0XiVCNoJlBK;(^#hK7> uM%ugil^DG-^@kO)A-Q)h_24+a+4Dk1p)^8DN0~KNdMJg+CTFDnE#~z?;DEI(j-8ACfV{~>{ZxJiCw!XJ+c>-baNjuur;tT`QfC0 zd@=sSOoNWo*l%y=(fPhN0T30g^wQ@{0CU>)!C#!K7fA!G#+91 zWBXGZiKqUrzFaYO&og%IGEXbf|DrRJ4U|?`)YGQP%XJBmL z(bxC9?#oAlM_T|ggb*6lH-iv?01siczWWx&(&X^WAVR~!Iu1He4^(dhrX;`wlAeR- z`|0FA|N8o-{`%Muq7X>xpa+bBuQYI|{>>mrjX9&lU3P=j^it$SF ziuH>23KRE&=({$B1=K*K6tb>oV`i3G=Ctp4)82BO||s}UbglIPnY)xNTA>VQK8`;N}}RF zrbfp*m`KS9s!Gc$O#H_j13OA&=GJpmpPkBW>Z>S=HrbelWMiPEvA=mF1T#ATOB6rv6o)1 z*PEmA$%&aB1%?I!Y(g3sLZdXHh7h3-j`in`mRPD6meSLp4Ng|riJ6*Z_W=ld6#r}*l zssWPOjnO6qiIhfLL#_!wFpU5l5u6Pi-1oM>JJeksR5;if$|v*T_qkunckhSRtsQvK zOD`{+F)#v@i@yQnAVlaeoRWWa6Py_KH1maF`mp&}Jv7i{B2m(R&(TF?MrmCXZ3`U& z4vR=>f?<)c%JyPFKg~I@m7mhuW_-nE-VlzN`b2<;%*b;uS*SGUwkBHqJB{3tk@Ij( z=sOH=O^cIk`Jp>ShJbky%4+-B_v(3z&hq_+VD{bXa&u`~4#4+T@aj|4qjTSFdC1+; z@_ISKer{Ez=l6Js`K-oApN0XT#<&`n+w6pO@_3(~dRRCRcJ;PqVK#J7Md=g57lk3x ziz=os^87>c5mKv0_u7aN7!I~Wu9W=P5nv2PgeE^E=M0J)2F}@%cDpxN0F8#r;@>aM9d+%~^|^ECrvQkgL#82@P$eg2hfa z44pDPEu!d&4!B;wpwOOnF(!Z07!(igxedo3?G^IHPA!I%n)%Z0%fT0t@Z_UWS~$9L z$vt{c#uJ5YMZ3ifT~AQ^-V!YYW>T%+X3z{&0q?2O@yy0?{58r-Y3YC^_UYD5Olgfx z{zu#UjCj*p%qaY;Gwm2*As13VOsD5JT&wQ7&45^q5WH#uSqC!Fi>-v)qQm_%8~1ma zsW@z65L_qSoc)&C1r+#6i!&9h>wz{yOCz2?dg9Td{QWJTg{RQp7B7m1w!f{hgi7Yl z#c|PjVzb->r*9!R#5U<3=j}|nf1_Gb`c`17E9W?o88n3xXU|JPr{Vt$wIZ8tmF7`H zqN3e58Oj)9Mea)h@VewWVX^oL<{l57k)Bv65HYE6oTucj>hi_V3X+3Fr&R>TBBY}=ar*s2= zB5p|v?FSCFCZujYj0Q6|Cnm6Lk57UB2)Px+__ik5$Y*I!fiL(o7`mhmK?D80g1eI7 zO?^TUqtwC|%_d4TQnB=l-G07IyHTDNB4UnMs(2}ks;g8!VfeLr30_#6Zgv)yHqL~g z6J#N_v>TZ>^bi<3^j@Iu$yS1WOfWM&>a2j@SkPwmj|81H?yF|CU%oBeBGL4ODR=3P zBT~GV{sdQrF&wXB zMaCuOwP@hzkM^02{zjqbrcod2;R_apITFPwK^d~e@&Xr#2J6U2IMduO-*B#&|Itep#ySUD$hfjS+~XAk-ed0ERjA=ngE_ttN|$FBh21fvsEUq*$H z-*{;ubZofaH7|uiL3dWOF@ydHxLKo1JCgyEGS*2OLxLlY43u%y45ZY-cE~*`dq%-= z`{8#Uu~)nop9uDu{z8{~321pAr$bNZE>wL|q^;kS-;k8ggp~ii*nojRfS`b2zQ2JG z-Lpfqq4|}>-tsb@?;MdF!~C^Fu?mSqp^++JfRT}ZF6gvy_g$SxceFXhp{&Y9%@$P+ ztD2TnqCJJG$G##-9jiDO9j`yOEt>0>t+gUdA7)#yNH*`=E}#{@8Lvm&zT1r3u>5^1 z4!DQmnZTgQeapMx1?+}h!$#R#nn;)s-uo~+pbFEPL-LcFe!2{8u-SUSW z>Fji54GgTV96Zy_EtiuN5%^pk?pOrhLOaZ+h)Uyi)DT-Tcqq(p;Y}@~@xxe+I39#g zDYWC|=95*Fg{I4L;v#wjKTDbLi-V>w0<0Ch9s>1}yZI`O2yFz=uEj0O41q~|CU_0{ z>C%XZ22C5btjuqK&)lzQ!;YYco(s{Mgf8pon4Y28Qr3n$nsVj6D%dOxfTnwXmkhj5 z(+x3tGmW+OO3zK-fUC&+W$P51DbANGJzenQLklLvWr&wOozLa)_5gunW5F-#RbyG9 zc9tLpq-&@-?&Qn^peA-yj8YR$F*#0h{7Ykw?xkJ)N_ZKkH}il+j8cf!-#wK}S8kbD zE&UjxFG~)R=8a*Rg@O43>-eG~qN4&?E29JmEerxsBZ<8Y3^3)(*y*@{FWxS;E)$ct zl8@@k7J#>PE$gLbX1BNN$iF**b_y@YjDgc^c>IfRVYkc7=lUuiNJ4eqlbAbp`pWOS zP^w+?K(M-&L!PMKTPoSC4Mb8;^kR%>LPJO^?kgvWLa9F3mjM1*$d!QM0{F;1WrYJd zegf2dUOFnx@9OeOfinC&&cTrh;?dFQh^c~arn<_~hn?Le3SdsGd4@%qed;}w6yIERh<@Cy|uTq&^Ci@nXHIh6t1f%N0AzS2xonQaA&Yc zZyCuYoE53LGJISd9IR4ku%hgM>P4&M`LkB9{wLN@ELM-XK?mWp#QB2veOM!QS-Fwre@n$XVyvjgdpdS{Q?ExS)5MVU+gT~z zZd(be-gWg4Nyl>9s1~M2Qxo_qx#+sGeYQrWOp7T zr2SAlyUu&z>d9wzC54Z$g?gCc4UM95EU08Ynjqwaob?_JF6rnzdMz4q% zO9UyxzzkW0Kfj`AY@?Y_f>_RCEKxt8m-~ig>b>&I8W^l*>!B^69YT!Ahv8-NxB~B* zaDk4X>f}}hLIrSirxo`&rV^fJu9yr8O)d98_>SB*%##=iRLwgw z{@fUutO{oh$)`vu#eD2TLO>2`5fQd3RJJ}t@IF2{P4dbwc>SDrzZ&9Y{^umMl|J+l z2pH+_4UBzf0Q$P$MeOb!agtrO=>ccg3v%>q&4Z4U>7p&q9HvQjoW;CBVzBq06Ryt7 zPXk|*KMXnuiE=RH{WCMtA7AjvUFRfbOM#^kkNX8u{X z4|DEIRKGTq~1YW*ca%dcP9)cJagV8cKkuBRt-PF7E^ZgzHu z#jJcqN=A5vk|!6D`a5dWxPdR+om*^3A|PXof$E=m<9#nLhw=XOWtC<(h#QM{6Z5&^ zt`+>tJC4)o74=U5?1$1?_BOk_ajPcWeDE0WSGw9xhpTgXaM6`jT@LO_hbKJiJ32;t z+xsr-bF#+C1YDxV)xbV@5ETEy|79zdkf{eTvB(!0Bpb#S7?LNnpb9P7Gu}SgFGDaJ4PesmF-G(clD5*I^o8y*+nd8ORvBZp>}{`-rZ3x|CCDM* zI|=>b%E!J0yXcVnK=L`}9ST@TWNU;Zag_xrp#)=K>tB8$Al@U6r4;VRa^`cw&<#0i zI$SjvZ_>Oia<9GHdva6r{XRRbx}i79m9uW<3`2wxOxlT1Z3l+1zTv(M(s^m~Io_%m z-Cq7&@Z0;M#1zcKR$#Dxa_ldqil}Wx&2V6G0_hvIBC)r%H&(hQ5^X>^c8#ShP@rz~c^z*>MrmG3M z5L2n@Ku9KeVu~DAlaC5|V)mc^A+a!x^Zt&WTUU$z+n!&K>nU4y7CR2PwI-jR;_6uh zFHEjX9DNlHN(-B<@2#}pTjvZN59jBA?n)V(#r;bV`&ykyU7jj~+v!3V&co8iM*}mh z!HgVM(sG1=tFBssvKmZ^fJt!&3bs3Kgh$M0gxI)#+N!xsu7_awD6smw6aLON!qSww zy6eZ6zU@|+9iHabek-EZ?a)-^&`0*&8&PQuI_6Gi3vXR8Z*XHyTAWQ-V+Z~_`W0m+ z-}_5w#-w{zq5H;2Dhdyqe6?}0QFgRs9p3)JyR$*b5@`V0)p~0iq zSG*_T;|;GHHdP4Q#;*$>K3U|XhFs#Pev0xm!KGf_u+?*%fY_~INQ?n)vm0IS6o3;a zPagym;Qo(Wz0luCrW~4Jg87Qz8&dHt|qu8E9pXHK%K9RZ%Qic>c) zr-3a^%1yk~0#h}ZPq6AR)pRF_ceL&Z$su@0Hu%0nc| z7pP8PW>v&&e2784d0u=J)N47P$ZB>$E-Jtrxc}UM0PAB4JdeY z?`>^0N78{MI9;6vyV~@$>`RGEzBsgJV4I9N4r_{uBi6|+L4x>+PL5|IOc*U)t|?TP zi0Nd>VA(|(y9Xt3`iZMzj+!msLd_KVP@=*b65(=4p00vh(Q zdwkq2gH?THbkjb|aGX#{$Yrgw?Q-9n;bp$9Nzs3kEU5fy0jv8bA{MSf!H*c$22#p} z4x>6{-laaGBYit2Ye{ZSvWN;3vnRgEhV`yD9F1c6brNgiSu@ zGw0hYEl=+Ar(!RiR`nj??{|WV4#?T5+OdqC|`Vg14g`EL78$!WJ0Y$dgWkLXL>W)O&W68|xFdWf+M z@@`Ca02H`6mq(EpV`S18%nRWi`Qez65cQ`t5zvOs9X9Qa+t?zM1SdTsC>oxL$WF); zL^v8JV;q$+y0{fMs&I=*cJp@v?$Aou-#vN4seUzER;}vnszhGK@5!gX#&MRdjkF;d zuGa7SUK<;8HxJPxMjL;*oppf+>;v6sH8kQlQGOx66`mtWp4 zuGn*I(!D;An|?ln)rnDYH$s>Rn<8J??(BZD=ujYJVVWeW<&&L3SQQb$5Z&gQMyB#5UL26JKa4Zo) zG-5HF5%R_Hc9TA@GHz=qv*UGo(UE_Y81DLvyuL?jh+7`->>Iu%wzoaq_jz^v<8+Ef ze)dl>S_!q9$=za)O4c=}N%UgCUE!;*o+Pp_Y`Hcme``s_O-_dWL zhOf=L%p(X{I=r5ze==Ul=O|qP1W}zS%l{eWa4&gbuqMwwYJ(K#iQjxM=|E=axbhWhw)l9t1-Cx-UluM8W0{L4Y342;96%1CKWo6}`{3{lS2 zLm?Pyu0!Hy9Lh(cEdIj51}zD+ax;VyV1dimh`T9bg|sgI24zFUJYpJqSS`_B z2`ap~@)50 zfX9^YH=A5$*{%WKE&w)uQhngjDLhLDfm zAEd67rXn;ENpOk$5!|)+O9U)H=99s7EHT1z8lq(LE%ebErk2}rmQRfiYEJ+@p{354 zIA*G?TC_0nh6w>!KRz5Q=+-2CVD>#}b!bF&+r29GTZh`30vjsMQJKB$7P;qA0tuwu zFI=@cH`h}AQ66tR8<#pcYm9L2Wz6;baoHz|ZeYl*=04DN95WlV?zJ1-(aG`X5A~cA zunJ~086q%suxi1r8d91BovHG2n95kZ(bKUwk0M?~)(-a^(!@_TR}$17l52rA;9~Ly zTm_H!6arHMz8(mEv@u;XHYd?SPz~a?=@I#Iz?P7^a)rw~UY~#=yEyUkKf7ipxt0GxfF4#td zX%>n4iX?!f)Z1L2&-B=&wT%*n**5K#oVZKPuF@6Lxon-Tur^DG@%syxx^B6DD<*bwdii{wrG9XLe@Nx&(MDxQlpv%|WGgCXXd_U+HW= zD~dVfaeM4-W0YEBOIC5$N|nYwWZ8_FYbI^QYZ!;%Q#9l`WGj{kZj)kvbE!G4qrUw- zt{(G0f8ZtEuDQYy_a+U@w>A!3P@`NKW8RmiDO!h{0r!WmPWBrOkF0-d`U9L&gP;6* z^D?fTAdP-G`NNVW^u!H{RY*Bf1q^LZE-wRCPpiO)hydTRJwTKm9x;r9!A({&4&~e( zlm#-@L`%t;+$+cpJQr^QpB6s7LS7S*p`}9z_~%7`YPRdsKcXa2vCNCjK%D}x4O}X3`2`KR&y0at*>aWXqU2RDON2M zGK)mH{!;mTC&+g9hN-@CTv)aIv&vv_yH|n^p$iCsXwn}`ix;I*-1Qvs5i|1ty-6C? z+O#Zu?$%25GUyM9N#r#XfvdP!5AGM(+xxJ1R#O2^+S02#HP%<*ggNE$L@(xeyf}m@ z7AGE_hcs+j%fiR}wm>UkCuZ(1e5llYa&eqhRc0FtH-pEw%5`+v1UUWeS57GtCzr*! z`V^gQAyI|ROGN84obK{K7qH_A({T8kpszrF+?lHoB4+{?C?gldG@HAP(XzPwF443mLmWtnnc0-E zHU+7Vu(F`2w{qg+*QZzE-cWV<335h)OjH-r8kqWA^ zOXaePnzCL+F^njw$G}xJDhwEuPS*NjS6$C|S2V3v=#0y{j=?DC(sZixl8Uc%;=Ei*LV`?Z5m>SM#$n=9Ev_L9P178E+<~e*<=TA4q5Rr0)@`@iGJ>ucmP-bB#LlWM5%a5n! z4=p~^V`D;fHtDH;&_65texENlCLe2@M=R&;HQb7Kjs!e&U4n>$=6Gq$l6{J{AQos! zZ%Ec(K6quV2IK`&Qo(m-b5>isJSfArU!6q<4u%p-(;JK2>>npN4Mv7r=@)4^B;J=V zmn<|`58}McZ7r{L_7fFPL!J~G8XQKrzRg#PX)+Ad&~{Xs%;8-o{G{_<3Q93bqrx}V zHv;*SXz6_`U#%y=Qqvle;KVC~vI*Pr-dS+GDRG2QjFIno+3vTC%j9%ed_|6D$A8V(%W5^4nCJ~A9e&}{jQSZV&N4SKU-MWy=d@%ES{vJWmZ?b1&c!XTZAr#V#r08N+vOaQv!ru-jW?5= z%PNU4fvl&3tC7}a{bD)2+AQ*vFx_7`#PvRmT9saoZAoCA(ba+kczNeP{?X7{8m5gE z8SkQ#3imUM=RIVZNfRK=r!XPbEEZzm^DHn7+*a9cpdJF&fH1dnx+I3Z^72uUbiz;m zd;!uhBJ8ghY=r2pdL%2Q!mLvE1^YAXSwNxW=Hg1@8o1m{w$Ee9By0$tR2dSjhsGDO zUd`T}kSqli#Fzf@t8BXd>mE}LmrAMwsdI=m{i56aBQ}(XWw3?&>^=Ov3AvER`GWX( zjELuk6?-Q>UB#2o$4!RpvU04M%pFs&A25i!{rQFh7m8!;^?;}qo0 ztC_}Stt>nN5*AbcEp)*?*h?YW5Z8}eXBk&DGV>Qw@N;A#sk=BoO-SgB9Xaa)jZy*_ zG6HgTc5c-a`KGlqR7g#`k-r%$t~p=HblLNzE+*P-pD!)?L`2Y%L(^8HT<%q>PbG}HRug04 z6%Ju+v&&sPukbdLWSey6QaQw9xNLX-IgCs%n!VP?Z^O7;N0<9R+>oq?rAhH`$TjRQ zFHe5NtVfDC!W%Ob8&@YPU$T%bNOg-0bET}EnV6XDvxLM6WBQ8KT9hv#%5PrdMz<33 z5^*Z{Ma!vz-u>;r#ee_0VnrAJ7SvTJxGD3TDs(UVHhyJ99g#famD^YY>hUnk1c8lTt6%wY`V4r}CgB_-s05vw1N z04e$$Klc~Z>ju=AehMpFJ^MERY_Zv?@FF^~oiKycITS3LP@P%cIpW}LXiHu`-Y%0F z>P_O%@zK0YdnBM;mGIIW`OY+9T-HH#p4E5>-T$;;h{C|@CCaMO){e!79RP4}KOB;H zlnv-Ix7#Tr;_<<7w5*ORtGJs~A{H6e$~ykIMIBRhV%%I;_^@-foa zgtBEgBaaq|*}Y6Cz$B73^J;;E|4chkeA-P`{rLQ%9iDvGnlwQgW(GW61)-e3r@|p%; zT)BhGSwmzo`IMPlg;tx**<3|rG58Pch+2vo{H9h$-tz#g$7{;T+68p5srg3Zok@}@ zA9q1xq2BwU&LxJ7d&964ySX9=5n zRI+A?D#By-2JbVnQ40prM+_2CDgiqsFg2fa0)VVD;b-spx5+@LSZ(V$*@u6AA@Vnx z%Y3LGi=YeA*SPcL$FDU9M~T+%uFEvNF~o=PiSfa>VScZ418h~eF)eBd@Uf*pxvEWH z8Q#uMJ$Lta064Z5m#?Ysu5>;=mT;pUFU<0)=fiFwDax>T85qPf&MDjz)TeELTgdxY zJ@eLl1a3oT^}RT3FpeyiRe7&L%;%Z;{?gFOTJuE&@v*9kbROqVE@59qYs>rU&jTg> zRM_j_D2DQIY-aFUoZ16Tgx;g*TWnwbuFycJ2wWxg#5N;K%STJ7!8K+yHZq%IUpyDq zb`gu~$xFI!Qz)E+BIgAv;Sxc|%nZU9A{BObvN|+i;7lPmo!747nRFOF9Q!t!6+_jf z2Q>vwq6Hybu3>GXH>C65S?s$u<1I2TLlA|&%S%*OvTna_ofjiN`~ns0ouXHT5RqrS zk5KbwP%XHnRse(5_^FD{&U^;70H2O2D+#IbDQ&Y(nSss6CDEQ;dB&SZ3Su?=1GV19 z-D%BzRVrC(>$0wY2!2i&c$((TRb1W98*|iipRQdm=-DEhx zbw9x8x*as`j0_}nUG`Sw<2xCWlkq|(E zDM4_(+gxVuugPeH;cL>j*z$6&hFdpCY`-e$ZDusDf(Gd}Gb+jGNy(5r@iF)(~r#g!q85=wOM*v0Qgp_B!#EmpT z08r?WMLfG9wo2ZP7ZoeFwPNhN?^mwH_4KMsGK>?uZDzV(G44PN zsi2)Tc&g6qc5%8zGry&4YH-G}`Ma=ae@dS$XDsju_5e~v5x-PPY?E&h-!t&)cAi?* z`(-yWyM|M%w1h-1i;Kwwr_*})^oRG2Z{j4kSZgj$wonSe#^N^q{Tn6fl}kJ!hQtoXhd zJasVpm4F>pbO}oam&eQ<_Y_rHfaQz7_W5Y=;8`M9kGSXe@-hYz-7lHZaQ`neTl{`# z!I0fMRKUPA;ZA6HvfNZgN{+MFZgGP&%8}eA9b0}UTOn63x&0vKSgj$>@%UMvbr^cG z6~WU-c;d!`HR6`^7S`$b#zU9eE9cMURRyyY=~b?0^QO2ZSp#&doHh*UoZqj8oKKG< zY@2~`@l4cNPjBM!5U<%TIpVYkcu9P}4Bc^OwQh&2=HDH5#T`fmO*Cd49HfM{TNO8y z+nd~b#JDX!AJSFAILZ8Hw_a8)vlw|FFG}v8w*jb5WHHzpadJ)#wRY2Ut>@S~R-$=j_=Y%=RcV}^S( zSqgAGgUOXcV5O5Go!_y-)u7Mg-0(T1H>#XnXya5ViUTjy5>p5C)D`otr-7nOoicb} z*Vnqo;jIYT87y7yLc)!p04km0Y8A9cgrqClyGfL@J0%Km)_dl*NFl{iaXPta%IGFo;Nri-H606Y*kt)Y8ru|pUn?XjU<5C&ZthDT`&H~T5S5B8k zx3n6c=V29)r3rpMr;tDWMm2>I$?T5EF2+`1GEy}04OSup^hYpsS_naUHXX4^4vRP^ zZM!?H;SL7i(g8E2dsxBDNB!RF$&t(yuC_$jMp5M&Ccw+HqCm<8`nSjYv=DiPO0|Rh zp78>fiO3=%3YwaA5PvMF!PbhBpEyMY%!gydMI^wbb0y4c3Zs%!8BsK0Uy}8o3|Jv2 zH29Z9;!&4KJ>F!rV;Qn0XPQE|y@&hc4#T>z0DzQ;JWHipB^yGyhbMA040&+mMGCGu zZn!HBo3fTRG0C{r_rIRDqM4jl(q|)cMVj>Z9Dg4E1`^?$Q%>NqRO>qn;#F?d+g+SI zL<19#tP5>ty+1vHA5Z)`MAhGIm$()BS4NU}59Oq@$0S2>0)CPa=F+ZD7SBqUB8dhw zu%KX@`T3jWzTYEYMBhEIbR?)0`~Zf<^AnXNZD?GFD{l%K@_sjMw)kiI!=HWX0Xxl;962E zCF1xl$<~?R3?w#iUB0C%aZQePY0iUbz>Okb%^^x2%TDcs27|tL%a->eD-HhP=_>i} zwfRwyWPoVdyHX|w68Wq-i`?9h>s)dmh3*IxPRwtnqvOP*d#guO_W?avs|ev-D@JM_ z^CPzpk^F?oy!?zSf%$*P)U#v8aQ%^c=5&F`*#t95F3QE z&sb)GX)+KM=!x&1u~NJmxem&XnwxRF4IN`Go2@);S3_^oRQXu>)u2EJZ!fitXrxUOE>F?zIn1CO2>K z<;On{LI=Y_NGLZ);x?BJxeSie^h%kzLYJvHyZ6)*0NQDS&@YNL1=5;&AjF4&4az|o zWsKrEFUw5_(UW1CCK!YkNolM%gs(Cnz)%X)#{X{U{aa5xxK_Z6vnq_oXRa?#Z5`e zO+JjC6YRVQ1Xg8CGk?z`9h_=TWwu_EHPtWrH+>5c8JU9fk+nL#8NZIBay=%+})&m(iOoYR(F2L!PZ{BRc@#gajEm;TM2>@LWG$EA2F} zr<3v4jpe?emmd}Fp^Sw#Un3tH$~}_ozv;-*?n6v7HLv%Yf$UpDXzQI$R9&J9Ykb97 zfnx9!H|-|%SRrf2wn+Jy>gjYPfY+l5x1UFe1~Fu9h+@-W3ET>XKZO5RIv+KlHKOh6 zPcEgUd4+m=JoXFz2fnw+!be^lgZJg3#_-1!5!oLt=3_)den8kYe@+H;@+7es#~}d& z$tw2A{Sa-Vn89_=TvY4ebQbZ~+%UmIXQeJwv|z3Dfa&YaK|#W;kq-fT{^j|;8sap5 zCKEB|!}On{aj8`4JiEW+(r!oSag=z^5+uG0&lF48vZteRMu69=yXmzyT`YXNGt8tE zUW>6W*jvmdCnktu63&UxI881RLI4xr>*#1lDyZ`GM>M1viKFa+(M)|2{|#RY!6Be9 zBX4s4;q?A{wz=IMC+g4wXKs)#^ByO+)NXrwZsWR;@)D>EQ%nq2SV70j=CY8}4YIwc zGAs0jxM25eh4LdQ>y}{J0LNO|72)?3VNO6w<->#ks0wIOHm@?3s@)a!r4$P}>1kUh z+}VdrcTyfR8uIZ&cBXw?TlgkSNfXRfg~ff+Ae~MTy!B_A5q#YV$-XN(ouA2KoRw4R zQ_@KHh1t4BqbcSu6DTvxR28=F^&any*1~3meYO}G1Kqs0R?e4CIaC+wNnA-M0G~55 zhTiYIwjv(7!^MfCCBj44Mm{VbLrmqabapRtTP@!1^n?pN?xK?naot#Tv15hs=)~X^ zxHOlEG>iSmWSWzAbOM4?C9a#>{0u;R+3E5%B~AsT%w(Ehxm0P)atXiEo11_fBjaXA z@$*>E2Uppq!z(yYaC`OD5S%AD=7|7vuTeZdaT-8?_%Yw2Uk`?bDT5r|bLyUWHra5k z8g;L6jhN0XzKQl|iU27%z+B8<9OOfTj==v`>E!VUS+-kOV7@vZS;vV@Cnz1~jRrFA zq`3KZS3&zPJ9$S9S#n?3?BeMKbJ3cwN6949T4Jr;y8k*t%SO3xeZxr!U$(V_mZRv` z*@MNaN0m~f$ga|7$X8!UjvDw6S zZVT2oBtEw=2G(VGSnwP!BdT-LH%a$n=t2sDefyaok%JNjegjx&zh8;rJo=9AKA>2R z=kQm5X@o7u{fr8+z1WRyFCFCbD!;0`>^43a$AO_8pr$R=uR4q`O7dgrNGj;hd&2Ly z+Tyi+AI_#_7XmsqHs2TK8MyP07^=)n2G?luSf^;bfVEA#V7`(XBgCvpTapB_c;ilz z9>|9~_9lVNTc2R13d0DQdOkm-?N=*r!X7ctAveM<<}TKFq#4ofi%LBW9v=sdf12T2 znvATSjU))i)x2~8qXYC@dcQ|^7x+$I^c)5Q`WHp4%DP>FOYv~In##LKerK_b4tb8k^KiJix;5zR0J6(n~%7@z?y zv-WoU!7`1PIccsn?YQwRMHB#TFC*7dIy-G$!rv6YdYc~41BXQe7lOQB*LQoTS_Q^F z9G8t#d%uIeFF}i8(iuee$ElZf5A3Y+nMB zmms;&le_z@UaMEE((ZSItp%A$1ijx(W)T)UkOtl9M?BJzX^lqG{oR4JL&U`jmKwz{IE;~_aI?kA&4 zN>u2jb6UGR!&$xUlmBuFJ6o^qi71uP>2mNAk&zkZZ_;hBk~iqpa~6gz#NrJJ2Ae^R32DFSGg3Q>Jp2VG8L`oP%K?y2Pvke)C#7RbE~C(QCdRv9Qr=>hO90F#!nJ1eU6vxNeIUnh)JK&D za)vWv7uel6@@}T$TK|#1#Cli}oIwn2DFd6?r(v zi+=s-2(eE8i|Hs;aRxaSrT z`r-PMLonpADCj7U2Z%B;t#4=lleS!3(aQ&DXKqLVI2iu8$1La<&ja6R)DCgIs657I z*&gP?3O8a{Wy$L`8x7VaU-7Qd2k}FvL|%-4jX66t;S!ODoFv@8w}bs&tu1XO#U8KE zWDoQ2Cq%QKgv}t+T?U+dVB~8j$+%aulXvSmM#cFX6nu!+(Y|OGAA0WIBN^x`Mf`Py z;T7xR@a12?8_XtYK^&m81;n>_kF+LO# z-J48y4ZTU3ip>Vz%WDb@NFyCuwzN$r+X+|lV}{z#3#E#C@XzZVfdhcCWy#{V&doST zdnWpJ_({NfoO}{6QBQDBs#tLZGUq9lsQ%?>&XD(=u3QBV1P*}gPIufNWiG*hb-&{4 z#OW7OByD2YkONr!{od*T#!E8*bi}F{_f}Ders$W2fi>`|)b8gtza?BX>O*R;o@_+1 z57y(wXv(<~B4_>UC4=#GR~tM>3ku`)Hkp$)^v2oVyl%>3e9nL0z=O_cF#kITY5a2% zGC5lX6Zz-2lt%5IB4&-HAH@yVLmg@H{8mU0F3{4*6`m_Jk{z+xv)=t;?%(0lY z!kQPKmRmE43C6_Fn^T4KTVLCs69&HW3xQ1NEu zZ0w5D&va|usCN7Fwoc%8C+}PGx8fa_DIzQD5OoAG?wj1Tj?A)$D6-w)8YeTCSin2^ z-4+57wYyAjmhSpnHU8ZO+dJkImu@_3QwX`|e5~L$eLq~q6w#DHEJGM)y2_Cq0_Lup zjY1S%l*a%~dPa2F{Oq$7gU{P))*-_TP1yGh@2VfC_RwGTr28qFBL|K4sq9bgfyEFDK_sI6wkpfcJN(~6q6m1NPv_eZJQUv5UNaR=Rkq7Db@CJ+rSdOj z{@0%~05lkdhu)B97^mwm;vGo?#vLK7!Ipt-$d8vzH(-FrGRzC*hJRkS7wQe8Hwb%; z9p~48!{Tk)jg|W7qQA^apv_7E^i~-Y&)^qck3an`^$YyRQU>`AT5i9SJ9OO+>&Ne& zQ1%Rj;aG;_xGmtkQ!pWsh*U%} zG8v7MQcc*`X`dLYym78u5{g?-C;y5rDd-`Dt zoVV%wNgp3!2Pt+3i)`9&pCjqgrtcXRQLyzHR)hod>)m(96ZK}dvllFSxAKv7^t|4- z1URc6e7znN)lAkn7b4p&=DyZpYw>Sqhld*DHPw;Ma935j8~S~%;g-tOd@uh!$X{3% z`q{4khsHPF)%teW@r!`H9V5f^4gTOZ(Uu4&=Am}7lfv}>8gQ$o3iJL_c z^w;qXxHq(3zuivTG*3X;4P5S^lskCc?(+?6R{)^B1AkZU#>`!69QP%P`TIcf8;(3X zgSQ4h1m_c{9_m;S8~|fGRsVlKZ}RCp2yZN==>MyiGmnR=?fwcd5`Q7!` z^E=<~>wA5!_xC#2Ij`$`&L8Leaj^tTlmy$D%Np!NIP zJrh|Ybvh+cp|6IY{j-EGPe-XP^m=8Nzsi{xh5e}C{XgsHhq>8H!ZihFc|65c36=$rXM>r@7(EMqGgOaxa5Yo9{esS^D{)?V4cWbD5FxJG9T8 zfon6hc>i%s&1a&0unsdWc>Q+s^L8hozS2z`tM=2a4hE{>;B97}*4Ol}9k{OCDJH$) zS)xjNImN6=uu@d%hS*h@uWk=WpYNv`&| z-jOFCID~qywOTrTv9i|k6Ed@J6|nizg8jdwF(!|y47z%WERw{KJY5}yD<6KH?7HGPw>I9KgHrC=4GIO$2gna2%Z*-``%us9rfL5 zk6a%%dAO5uxK&6?3Pe0D)*TM)s0E)H*+-M^t3_gW^B^b`jrQ>9>wB1eT=yn{1N3un zhae3yfj&T^FM160J<2|*`*V0)fqbB& z=!%~L1oSBSvd4$MzU*vW-CG1O=%=7L`~hRFbm81&^YdH5W?PtVYcrE~!heWw4Xzuw zbg?SE}3`(N96`(N9J z+kBYHO8x!NNn; zsh8jW>*9T73z6;h0>};15c)OkAQ|!RLX-M$^hZZ~MSpV;wql8UX+}PzQ@+*BP@(k} z>O9*TV+7E8iyBI|nixDZ#lnV)tpSD+O|huq=~fp*j#g5bWvf$m?Hy>l1-2D)geibq zmzh{JtD|N`WBBp1t5);^ie*{8i&-l%79+F^?o(9C++BKGh%p!ey!5Iu{X3X*>1`#( zVMOo>tM>F7O295~j3)Ep_pP3wM^SjnL|hbGcEQ0_3;JViDgK-`f309QWDdvch=m{Ca%Fa|-}xdlZPu3c zKLIhBlYja}7nixkn}B8 zACUU35`KYRpGnx-lVC`W*!e8{amcfrVj%*!0RDh6z#@M_Gkg z)mSgHcCfLr>9Y~p>e=D!zU(>deeClbV2%SEK^&``(wzRBB+e-=Fc+HZD%T^fYOXPE z4sHo=8NACMl%%SZz99+HHtL-ru2kYB|)#684A z#pA_^;-%tE;)4==5}p!QB@!hbN<5Wl*(bP9dY}5fWJwOm1CsWV$&!yGt0em*=_nvd z7G;GBK_#OKQEjL(DTvh0&S1N{cMY@Xv9Pmfv9RszZNkT*$YQ^Pp)8gxtSo?#j7%%c zZ9xORuyS$EXjayzY^i+njn^-X_gqV2i3x;zMP3GWy7SwnE%%_yLC;lkiH@16jlT;( zw)e zfM=yqI>1Y)#Co3Uo4q+l0KsL5P2{3*an-ws-l_Cq93ZPT-v_pM#U9rH4uSd2xiyMJ zPjX!4cM56;I5pLL-V1PQlNq<^mYUC)?mB6$nz*Q8y()Yr98{g4%M|nF%>AI(=wKgr zjXNZ6jv+UtvR<$f*y#R6Z{v@M-f8QNjfiJ{lr4=Z9(B4gus#~6xxiWScBR zCjXlA?|vbPb4rHU>gbUN2aaEd7!jYWJ{TY^CZCT45a8qp0wsGzo6N&_)sb6jK(BsI ztXvf7;AdU)b|Z&fvPkhP!CKrkaxX8iQ5^D6f-kFsf(JKqI^6U>2^Gs{tX6f@tnh#1 zJtGM2xStDgRg&3=WU9PRGe&QD}EU#QawfYnz1=V(S0JvxNO^LMemB5GE?hPYOH@3VSm(A7?ag+J=IBYsknS1 zW+b^c#uhPWOsRvD`k$nD#MsXBrmtA8zKFreij`Q|4Ap8Xq!rz@p7=P}U5U?&m65Cb zsD%w6TM6W5bb8Mwq&d*c040w{5tk z{H*AX^!3BS=zN*HiL_wGW^Qh-E$92l7C(#bjqR-Aa{6OIN_5a+hT75CsDK{*O)rlI zc@t0B^x3;-v{nKb3q~^Wm$N^Pzx%C5D~6`9aJV$jsoZYTGW@Kz;Bxdq^Tv%-F)Moi ziC9{`{-@NVR8-v?bGg>r?_TKGyjKsA$9Ap_fos08QB(Mc>nh7w9Ltj!f3YJ zJZUTLCO~ZX_KS&i>WgX_Ztsb^=N;ZPC@^o0Fma?PT=dQ>we#P!&jyzOV2Pm4U4CML zUm)9j!kqHjX)^ueiH!L{$1?7HkmBe-T{=Qpfe+tWKQ)?Yen~^8FkQ0UMo7kW!z-O_1`SQgedR zz=7NZCcG+M-ysIP2aTjedB%gj=3-3n%{?Wj+0TA#g;P+7nJK6b=Zb2=e-;wUX6PN8PD zj_1dUMK&xmM>P=Kxz;OWX;X1a0#@)0D`pD;61s1s@J(Afb=Y$GvZiPAlLI zUjPr-valP8!ubqp4dzxS?Ku680oEh^FyfJuOGa5vdJ2w-aJb$^h`*cpn(efs!THyH z9{vd;1#uT=oDHf902#cU+!|aH)BERWGpKK;)H0s`ts2Yr;d4EC zd;xemf-ma?&!tNMR8l;Wf|7f4L`9y%ObzYKUNYn$O!!DDz*!E}5@l_lT&nWhcFXCG zazWoul#57XUa4_qRmA4Mv1NB8k~WUYmppwVp%|-_A+&U*Iy=Z*FnqIjVLWq_MRC%g IY-b(*0efB24gdfE literal 0 HcmV?d00001 diff --git a/webapps/docs/images/fonts/OpenSans700.woff b/webapps/docs/images/fonts/OpenSans700.woff new file mode 100644 index 0000000000000000000000000000000000000000..27619e7cea1c29f9d71567b78470fdd705c174dc GIT binary patch literal 22748 zcmYhBb8sim)5l|6Y}>Z2%f+^B+qw7?=VIHoZQHhOKl%RtdZy}Cv;D5_>6xjmo!#1Y zll%P}2pH(6sc!%w{?{jL{Mi3f{W*vQ7f{zp6c!Tg81$dip!BUfiaARtirA0M3`%24RA6HM*RY<{#FARw>?ARx${ zcBcXtGXtleSSaHkAK3o^1%zW}?P2<({pg(gKtO87`tQNf<|YO|K0?YrKCC~mzlxxC zng57C+JAE)_<GKIsfY~SLEF@ zjPy-U4c7X5#(H|WBNkKs{#C#iX3;bLWAxGAJ1{WWJ2WxYH!yJL|M#>LG*Flo6f_V5 zkp+_*f`^5Yk&?2)ApV=8Z>Dc{95uJ6hm(MG7&QQ}k7~SNK`FTl`(VU%-EbdWmukc9G%~ zW|?XoZlM9NHkX;3@n#cR9p(Q59uGgEx@(KUaJaP>U zgxEzi&_#x6LkuB89vo}V9W60cFD#{}Kx>_>u>PfK78CG0?j5W;70X^5bB@@*o()O~ zrjD^3V`+M(d9}Y$NMEFLM;VM|wALfw@#&8A*jN0bgaay;@{0c1XA}cOGkyI{2vRAH z=GrV1L10=3!COm+kZ(IdUa(g=Fi{W}z*p+s_ha|hH^SE?a5&T_j1eJ>y^3GA zW+N6oi?yLKNZsF*UAlgs5E)q5NFtzLBSiB*H;a2ll*+UmjyP(T1}r9{ab^+H89wtm zKnKxPBC!En(!kOSuiaKNQYej#?;=2`s5rwhOmmbE4er6uBVAisw)JV|n<(^2HF}iI z$8L$7RI#vXsr|m|*=g(Dwc<)(1LpBc)~vnrvHuyN%gSfndZQY$(ee9UJ{0Q`jFtB)wZSPLSXv@oSB;1w|cAN41K27fB-U4~=-k zp64~}NhL^p%-%D}YacZ1f2ATEu1YSl&}gS=ny>;;@fbE~C) zBpD`FdraPE6{~34Z)32B3?JBx{xWq7`9h`dM(A)J{JwJ5|?2lrPlH*4Jpz2`y{%VTt6B6V;ScJIBuN~KqRE3#O_utV}( zyf1{z|22H46zr~4(x~ydrcxT0BH~1{(2ZmzxcXNlFsI!nj%>DmW)7MvTu%df@nQk{ z>0`Vs$BKE{w_!D%eB`NHK_9xLs;@G6RqKCC%nsdI#Z728GT5S;;+5G&&nqoIp@&h9 z+%KOY2r+x(yOBN&1cI}vE(xf~7h1H>-P;`qIGAGg96*AG45 zAbHKq*%$4!G(xor5RFu_arZqbR%<=T(S$|G7f*ti!7RP`BO-?0s%@hl_Qo%#gQkf; zCjSIhLMP*u|Fi;tO*c4Z$vB_lnM*V^NKc75ixZp-di&?r*lNE$mTb7s=i*d=PEMnR zC(?V!NZ3D?-V=6ZNp`*J6o^T3n}JU6IK(_eDv6{OhTAt{x;zrC+q~rd^s&d6tG%od z(dRbYD0&y~n)f1MCYRkF35du0T-cRmD+?O6SvYm-O{pspq5Mk9h z%878>#Iya%3kGT!J<{QEjagnSKf5c6cpwKPJ%_!tBPdMjNN)pgMNeb}Vwc2=X}BCB zKLGWRgRAY?;Yq6p0F2NWlU?v~f-v2J!CH!lK)C;<^Mye|!rc9H>VW`30sZ>^210Pp zNc4vGR#JKT?!4|IV|dSUwo9XLC`@3N;W4qIRJXK{(z3Dvq7fE)QW5P54+|>^3nR?i zClnS7aY!(*$I=FZAtnxl6$sHYiFMExvPZ=WI(qWE@{&j+{yMtnD1!57zRvPmy5@S` z2<2q$es}WLUWtNY&Ykd2G;hS6PN!36{u3n>7)4T2LAxB_jZQ<~_~MPOC**fQhOe!B zLF3GQk;g%YV%K)ipMQEZE5i|mwLZg%?k5|q!?n1wu)%04jYtGfM{!x8ohl*>4UES? zB+N=ofz;A^{U;Z^-$sBo2nVHwrQ^1K6>oJz>qcY+?=708o}*n3UziuoH5V))U=5_7 z4L}9IML({IpU=5P2bH&Iyl|_TK6xcA`_Fy$(6Rk8FG=pv+&z;X2}db*6!o|Ud$-VK zHLRo&1lWY%%EH2zpLVL4R{qmukY2rd(6cjJu=KzIsF0n(>z$A6rs*{-rV}rqdO)gc zVi=cC<6h#j>nvUcqlTm##>Gk!;Mz#f zCwNC7Agx{Pu3wBsQY^KE!IRNmq%DG!Y!%K>HkyKm3&zAkS~p@CC6t!3T2EFHT{y%! zV)$PoPWE9Oi>OZ?jf3*c74oBtkPvAejIIZK&{&@p8-aoFWTSdqTgjZ_A!K|VvFVGp zdWm=cS7{x1k_`q~=yGD zZujA^LqELr=f%y~^tQQ&>-bkC+r!JvF2ZbTU?{_2sBiPK>p`r13|s3VF@1EkkIi6O z_e!>IvCRv)enIi?Des-=3}7mO_Lk=NB=YQfU_CJ9B_E*=3C>7kV{zLA2yO+mKW=T> z0Xb|W_3OJ~5#vwfUXxqCucfVkR0rGHLvn~w>c0isG_*8E0{!Kz-CE`*5i#ES=T2|G zKE<|J735jh?VeD%S!yuWtK5JQCTMUusu&TzO^%~0+wE@(1)Qp#Ghd<)=)0f1@aZhh z+v-{_BN)0rQ3>9juGPwdHgz)IHjnjuy&$@bnC(vZ8Hm-jkViUzCZQ9KusSp|4n#GJ zdril~BVwkR5ur&Aq>>7RlPR`JW#p!(IfjStO`T+AW+v{l&Is-LRZSa4bcPPJ&Zf*F zl!CO%Qn0;B+i;Uw?*wQCaLju5=2GR+X3Nm&clVGH)U6=)g`n9DwG*Ht?N@X}YQj#! zJ+43Akw1_78c9O^I=?4b5bOp3B;hx)4_@bOTRPmgTL9-}e}k9ku{i^_X11uz`j2@x z)hiq8x64YLt&jdRa6BPT+plvP^XDKFnRtl(vabCW$j?Af(DdS`ZZv} zKBox-T!p0^ZkR!Z5`K{07l^h{EvS7sN;drki^ibiIMP(-(~Ik|gzo!bpOi^!x?4a- zzpaZ0OKXT(R!iO4Z6kt?ZhIU}5A)Z_VQ0h5Tuj43)a46My?7)2UI&lC_?YUpK3iG8BG9JJXoOE^XupQ%m~9W z3V^Gz=$F6N(kYlQtopwRa^3PoQftOAY?72Kuiy!Ev_zFmChw4MEE;C*0GYX6TCLvz z!4|c;TW^F-hogEkwJZS{y{SHyIx!v}Z^z{HEbTC<(_C;)p06=uUYsM_-`Tf?-jH*D zpE;~VHOO=HJOgEa6U)i-ODn2lrcUL(0L5&BqWXW=yE{lvJ!IP& zVsl6rx=Ir0*Soz@>0+RDP2b+foEY4-QLCafN_+{H`s-^ZNCR@pYymsQ4%SxvB_ttT zkVXKlP@X>v*aS_H3Zd~#!kjV6NfO~5@i#I>eL}mCWq#6U=5-Fq+UM=FHSK4tDSPMD zqr(0Bs#7A@Qw5n!N9>kyfr*wtW{8bO>Og52G0XEe#CR}m# zO)~+dW|UVLJJQ7yt^|I1oY=LW**gid6FA>&$d;-<58BLtptLX)ARpghgU2r+aY=$> zxk0lv0E4=-{S{x^Z#zJKadYFMgPZjzY_+|9Znxz;-gFvcliqQ+&m&&3ULR`3 z132z%aX#ufDq0&0G%Byxa@*v(UF2}5L~LIUwglAa4{ zdm!fmOb!gY?}RjDYl~x#buQ2KHf03zzT0lzCJCDOKL?tY%_djB*1J`!aO&TDE?!`z zJ}MHgiz24BtGRq_QIi}aD5l%okdRA|hU*&u^Z)jUacVA;Rs%rB>fgRFrBsBzx-hgj`A*S1?9c3P zC}uqTl9@-A`Hj>Wbbrc};rFj&j_vR+_P!}ZlUpXvkLG3tft4($NRU2A4FgcW9)(RZ z8#_SSF4J^MbP@e)h=`OM_i8Oi1obgLz%XX69;g5)Y7RfKMwA%29^5p8a$-%{FD{g6 z0YWtXpP}vCcGX#&X%v%dM#f7jyp^8WW(gNWXg zk4*JUJ#q=eO&#({6$(7a??2)qp!l<8%f*GcqBM1;#%YRY0p#(4oO{2)@f<`!Er}oV znuk>rS4R54?@-%tb2{{a`4@8u?I|;ICvsT%O-N&!t8^=PRzu&h6P7dwZ zzrC=`vKU>2t;X-zZ6US*&8c5T1kJ|?>wesiaGfpq#a;E@w+(|4XBgHBJ_-OsyY?Qy z0}N+xwgVU)Q z&G_TV%=&`?0r5>;76)&ea{H6a$@*`1k$WB|Hy7WR$G%dT>-v>}-i0`_ii z-e-f)8=+>jCmc0b5j;g>-KvUv!f+9+657}_(v=6#YK%xKmhlWt4AykOJeC0UNkUi= z{Sw1)xHb~!xY&d=#i!sGqV^t}A;eoaBGgt|F0H5zx}I2eLHRYo66aIubFt+EDEoxZ zF-pa|;XT50D1CG~e^=vSFT0}9xq19;ozcYCcC+&LOZRb&2`FFv^Ixx`t@@T|3BHWL z+xrB}e^@mdy+MeM>9|os@m$zbsEQ6?Mgzau#vh4s-+ionI;qMD~21FCk7-3Qdoh15EW=!B=%xgfHz4_V($pg)IW0~~U5{0Kiuv_s$Z?6_# z+OJQ1-wLVEO*^pcVH6$59{j-_C7`^qbE64{ML^04A@sf~VcCLC&3b_Q+fy@4ILNRv zfpocfyylZ}Y8;`=K>|hcpyd6bVzsGJe+vsI{7#<;8<^aPmM3Ya@I@Kk?cG+mx6~}0 zqjdF?Wf$@`eBoWjbk0ZO9KG=O;FWlBT5|#7+;# zNim>8KadG5K^Z#!oe|`9xc?M_r4$lnrTKjo$)d*pmisvC!0%Z{!Bvg@tiCyt1S}F7 zb8XF2<8oBAgrpJ(F?-4M`c`lyu_ttcAyxtl4%(_{QMze+{|*Og<6{l62e>~N6xu?= zJX=UexLNB(RA@ZPC;;K-gKWWmQSBS{=x4{xJ)weE;(HTnCXn-e z&(p(pRqA~EJyZSw>^V-=0RfY}Gwanh6Fp0(8OSj61ogbRk3oRVq<;$Fh&Gzq&#Myu zx?8yaZtc#*=M|owS9dYNWaexzxhY6?cGNVrjbch5%#kzCKKTW1q)KA#s;{(%OAu)pgAl;QJ3Yn5A z&v3)Z*_QC;5&U*09f$*Lwq3*n4$~>DTht_khNzX@5j5e zv5r8?kjD(+nnjmGpH2GvPMD%F52``Rs2)_o_vsCLc0+-k5zSyA#ZShUy||)*c%f?&yhr&rjF;%x418NI-xW8?FPQ1&(a$_mwP4K)SD|cReOFbF;}}f z??>grw^@G-Z0ftg^ym?QzT4*o_-oaz5-M=CT#N*i1)}RBHwe4PTSWk|foF=1fL=;~ ze%_!asf$*Z;dKcoFCZ}I3Uo!7*<5TqBD5uUZA42&XL7Et%}f#G31sJ2VXv_vBR(wn z?P}?^=hIjLjUc;IPC(*Y`bYS0n*`B9WO2Z}o#hC+gBHv$sUPF?UPJA$l#sD|b)f5& zF=opaJkQ*|FlGf26xz)@XeIBv0mtx)U8A_pL!PR7*Oe>Y21}dbP85r9fX%V(^uXq+ zM%3yilm~hu9Sm&1L(&{WEvZ7*J|;V!#l;1bCn`&O+-on}Dxz`6Fet*9g#u1_5~>8_ zJfMR;Oq5Z>mJ#znt^d|0$>6Z$vjxy?)Q+uLVSljkNm2_RGrDuL4~EA&`o$K;g4acg zFaE7?Z%${yhQ&3LYOr91{#`!>OAR>&Ik9wWTf7sr3Wiw8@T%b}s$o`0w7ySmA z&;ZL@jEU2k@Gbc6=|=#Z;i z5!{$Ms3bI|O9q5Idl2ysHduJwa~`Z=E#yueh|mtaDZPoLjm~R<;@M7@r!xMZS*Hxu;y^YWHc>>_8@arB0N4_)PF>@$0u2g!NTx^2pCB@m zU9wns7#C+0owi6iUnE~t4yEYy4dgdyOhyC9FE?`oXWuT^BaD?ccj%4+C~qL${2JHL z)^bPDU1&9ii!RMYyic#V#ga>NLBiQ+<4o8B9HHKffRh?WW{NXXLs4Q9-6(-1^BuK* z_>Hx|PF2WuKGj%efAVw;6oyUGb)fo&{aNCkXIIaV6O*<8J!y_<1U68(^w%UBm5r)^ z_nObMa7G3MD*k3p#9}dKbU26`EKx+U(Fzh|do-!px;ja*h6PiVyDNNQ&e&}Guq`xC zK_Wx&W_4x*jBP=lbz#Dqa=V>^_7?1}4nslppQm7lELJw`66 zS5gdPe^`AlS)%NIDBSp0I(>Z5%A;Q6+6JkvZ&MC#SOfFniawCv5cJ}Xa0lhem!g?%-41!m_h!7BsEy}*D;m96 zYtOw6dYv~=3&yBPei~Z;<`4_XY)!$W3|Xqg>`G>|B_@!+ec^dGG8u3B(CzXVh_GaE zTHY#t+%V(Ut|_--E)rcQI1udgt5GI0FT4maWNI;cJVIm(A)iP#-3n?KwpWy z-Fq?(k!j4P2e{M~k|x2lG6vl2=)C;lJ#X|vbJ*PQcmvKHUheh6IHC;p>V#!q0(q{> zi}W81sMRT+U8!Jx!Ja9)ne#G0jL`;*CxbWyvkrM%9e*Uo@ClG2$)UT`Ru>f&Y1f zrfEq?I~iYyl&YOnAv^=IRH(BunvCN&>Xtu_ed@uLs7NbFlyOb>k6Wip3FBvCqM{P3 zo+qPN1H*GjmsVee2`n8`ubK~3(kYDVYwF|qH)?`aJFrt0MRrfd9s@U`z#o?Q;0EdJ zt9xO0;$L$EMgJo8S81X#vj>YJl8f6$?>xQ5AIP!KCxYQk*gv4{%fr*@bkK3{oLlj3 z`faV6M#3?OL*|!V2&AG52$--$lBfoarA7&Phi>sM*xzI#{aYk8D}_;2Z(j?!55>|X ztQOO6qg(*LxLc1E##aV>kBmDZsYBz!v`MI}{eU44XRFpM5l=m(KB|ML-bTj%Av86o=Y3Vka& zsO1l1R>s(ZU8XqF7uE83ed;U&d3aDlXwcr3J4?A-W^EmnF#&wvsYSQeW?>m`Z*xw! z2gFzO{O9(HvdZcruNIH+N>WHzV$5GL#(znOWn;|K_e@b24jq7M4v%IEM_;|=6krgw=dJMwZw46yO1z;B2`%!HBRb?qFH9`qtd#uEcV2T zQh6R8acIotcrTrHm_ z$szkib$B18U3iDlfNP2prb_6KYeC!+NoPTlTPT(#Zy>pxm_>*{awJ+JFfZQPqykB2!il)pzZ zg7@^Z@V?C8do>gQ#2D)(EFJnxdX*Sw_*GhBCSOr`XX5oRLgrxzI5%aPirLLrVH$t+ zFl=3b0Z97+jccJ$9~M;|(*9q$Zr&B9_1$-DjFZQC)9KU+nf7{&-SaXA|pZ5684@$d55-PW9Ara4LY(Occ~FQ)7^^jAcRc zAub}e467P@6DCk-ve*FocwvEEQQGTu&bGTOA`E)gY66YNgiM^sczfN)*8M_Knk1Z#)*Oh~4QAfyZ1!&pvZt$8 z-}*%uL{sul@bjHI6d|Skp?1ksvtsJi|5a$(-MTu0euJ)vn}AT{L;2* zyuZiggzm-$#fumzVnon~|Ba_-^N%Iijw$OyAYj)O2-3zi2f}4XuGUnK|iN1rq2sCUak2$KmCOsm#I)YHiqs4!46wsKjyPK zi)wUy`|0W2>lEi9-|oxlI`1RAEc}RvK4VJHvQPu&rc%tp}sHpA9#(SK_ zgT!vrB@F7+XFS{Mt+Ttk#&u6b;)$uNXL2=l{iy4yE4X_sStYuP?pz>zAu^TQWdf40 zU1le}`7a-5tN1(%$?0_2M%{HX`sP+3WdqZhR zEj#(EV+{-V&zY5_aofIZUdOr?*y%b^`;plKRG%Om5FNY&^3jM2h!ejndvj*Z8_Dqu zw=E-o6I{jqcu1hKV7aTnyU#Y{_)TP1?Qvi>3c1AMwOhm0m<5&or zgI}cxH-|mRqeaaG#sKqyt?CBOj5tB@5ZeoaZ@gf(1H%F$lgE0aJ4(o7Et*#oB$W*f zg@r?ek{Z{X(|h$L&}e%ElLsqU$P6n*MlZ&RaWeV-5NXOqpGcUhD|pjyBTjUXzn>a! zmn6R=KA3;f-51a0>zQfYHBEP5OOL$oPc%Qz$DG-v@Xkb=Uku+-LR1K}pl~LI(LNDqUjJ%d}diLuf zfBysy_J2!{Cb+nr$4U6iK2HUwasgC_?$`E7_j?eG2@C3hHC;*IIM;gQ3^^62__6+b(s52e_8QxA-Yq zVzkW8w<|Cndze)}h=r{}LIWbvzlncbXx@~}m=`33%I4CFAd)B0)+TY@waZ&=F~Lo= z)hrW;j27jCQvg!r*S|WV7b!OE^ZCh6QSU|c#fc++#(6%E`Lf*d6`J$10(sk? z0A1gd3Kb9m+pr<_9GB?6dgTy>3{t?r@bF;aD0w^uf3Jz8>sh5L;1!<_8)@O&6IRu& zl!cfG1DQ{albA=R3%T&$z!PMvhMO#gDIGsol)%mDF=R16$Dep0jYDw=k=~=J!%M98 ztU?Amt8S=Ne=K!P<>i*Qnw3wS$T4^x4t#0581H>yfXiEQC*$Ekm71s_Qh1J#9Wyi`H<5Ieon7@; z6ja7IC9MM`jx^%qc+GTg*$Pk^`od(^`qx^7oVC>!g2h8G&a}J0edjGia*gxcD@=AM zKLK~pYz@+dElZ0R4>fEHSHz&L+Zii%s0zD7fC6LmN$;MdF8X*eCfJq|?v47$`RY-C z&OP_mF!;okL+{yIdJMsiZ153N{c)rkN9ettFWN-8h!3bM9x_<8MG!DEU1y}hETfr$ zwL=efseN6>h8k=s=g_(Z3jMoR8|^P2**)g9f##qjx(t3>uH~XB;41YCf3AP(-vvEZ zy)mRj$r0v&gxJ`W6k>p3om4qlz-R(tT5Qe>TO-rLUHKz$jO;0#>L_J@1-oF1FLU_I zzapMOgwu0pf~#&;GL-|R355MNla4{_p1cjYcfzScyM445;bjtUm`2*~BG2>544#L? z`qL*Z6q@R%L5#olBaweO&e67%A?L*Y+TqiN=1yrKd?2F)mWO1r=i2la&psQ-KaFpRvgLEjYH|YQV(>w44v`JzzCmtNuH5yH zF>rJQp~K>C-@I_X<24-_vN2_Z?5ewy4PCXY`T6?Q*ODgWL_>TCs!g%4NNX+>kCYBn zu8=y&!?BzsBsJ?6!R0A5Xf5L(!DV>m)Vi znW138Ax56o6nDW6Ym{^VR+K=2{GL+=(`dO;(GD{Vve$mBhg|^{^zd4ymMw57$gt$R zq_@3Zh7^7r)251D&-UNo4_=fC)A{)e;LxrF2LRw_tyn|MlsX6UXB?HkuOj&ksrPLB za#!;+xYeIn!y?qOS0_Z|jAwLefoCMisH?c#dVkhQV^sWD=1{3jelpmnGh$%pFu#c{ zUZ46GzWXqc#oGC-lA497EA+M6q%b*;Fz0PsZw_W{=m)2d%;0aDQ|HYlX4a@2fkAL_ zff|*}N4X&)$yyW5+7yinbURWpTd?l2mHy9~bq%~d5X$@QyB7Yhjn#Xoez%xK^wdOv zVA7%4rY$=9GVV?jmrTDFgDj@Ua>w*Q*%HQfm0m}OjmFkR>zAKnzxa-}&OB=bzYW4v zC8q4u+wo+DGBaaLBAmsm^#>2}eA=4_FWN1!Dnxy#GqP2)+NVKKY z{>IMEGX~6B-n5(UGUgob0#}Qn5HOIE+S4Z|&6AT4uKosR8&VJ=HQ(DIBOd@DOP4<2 z_QY&#Sf!Dxms@ZLo!h6kX$G>F!zAPCPKgiW^K{kwa7mqGY`xm_gH}r6Z@rEQdV|Ao zG8KMud7QKsJYe|AZT}g~4?N^6i}puID|--ShkQ$`;Fe{_+{=`mvT@a_JUT$_-`bg}Js zZg;%Mz6b7nsI(!j*4>tGdOS59+z+P*7Qk_NsomL`v=+(ARGrManjvv%1!1^50%Q z%h{RPB$f(K1P}jd6PZ==i&RpTE_Mc&jrDpOd$!$eT9X#V@E1>7ZPi5#z*ApL6o6hI!M$+J&Hh9BS#RatMc$j3)k!Vq!@FeF4tS-FWC_P8HKo_W`P)`y7a6#6|p!sB98v+jVXGmi2WYvZg9ABXy$6Sz0T;Z zV&k(I)XHFOjicy#JF`o1hM;^gj;Zs!)^B#zUGKVg2P-g&*;pa>^EjSn>&iSkU+&JK zX1JM)RI@u{DLfk$h~`ey;T987~kyNc?J9@b4`% z)7*__LE4LrJ3CUwQ-MbsbQU%la20F)4KXIJiU1cmV08`&S6K=NTZXX~qN)ss?`$Nd zF*q|;*PAG^SkFFRTKwI|Gc6o?~hmgCcY6Fl8S@P0Wx z4Lr}POq*YP;Fc&A97t^OCkMb+u} z7EfT+440!v<0r!5HzTzE?TH%0-xD(Uky@try5Y8Q-Y2wE0|^I5+D>-iNS02PNv|O( zd!Ggyf|@xMf`Z(GYeQ9X(K+`a%yv#Q1rw>sW8N+;79Ehw}#yJk9jX1JyIWe21! z*jB@zXU~hD+9x=fNHv)Vco1@BDN&RW9U4It2$*wD?G$|ZYFCP>GiO@yO8HzU^q>nTQs>KA^Gblq+q7yyQ-b0IeB)q z#y5?WF5d<&bhj}cW!4&`DOitfu@+TfS{1|6~7)p*yMRH$^rS!JnZW>v#j zFLxhOIDWj{;rnI7cq~|zuug{yvWbiQr|z5+NPtpBdNEZjGbL5*!ZlPx_Mr&VM6RIp z40|);U^2%c+Pp_Wc2jIxGiW-RQLWErV~hDUkwb0!`(STzvax=6OIhz^+>SPr^PrR` z$@*xzik@l3EL!vB&)|A~@#|-LxFpNp4V71stM|h#yj@W2AY;1fi%mS}>rFhvxB)%w zy}#0;eTzI}_DZvhNFSv^&lKF@(WoKkDY4%+ptKjJY-pL3V~!n*09^QD7FoyCj2tEM zyrkoXS!Q4w`d81z=0!1aYOpQ)RYDrEQ>C8aG_9-52$-%3w!@80USr2H!Wv0x{wDif zf-R%RfJct)jq2;k?!xPENd&Akk@lCxJiG=vft{IDBD$Yi4mxZ&c5Dfm8A?Wi6oRc@ z=unv@ojhDL1BOR;quS~IYTquO``Dpwg0JoBmf8lXiuYHvy?Wi&aE~P#rtI;dVIqWC zB5^^}Kdo$C%-#U^h2Ko-NqyiW15lW*qfKn1IMlYxWg5_SF&i zd@@Fnr^9kH)N6o08QTa{Z4PPq03;{4o$p!e+Ag%w9`(A_Gr2OGUaMV|QMxg%qteDu z?-f8257s)L*-{efB&NJWFevD&k;Q(CM;p!8>&fPnD=;}j z=|hrgV5S~X1WQ%|8k5+pR~kr3QGTLVX|eW+R8%<9Zg;Bdsl9mnWgZ4+t6(cPG10^% zF;V2TwtIX1c2DY^drgZ%*4ieMaW@T*mwz=pjiSE*6}!q2XD^ zC$wF}$jt1#NV20FpKaJ}4!KT-8%su=Ke7RtbsNRRu~|teDLvSYm##(xM(mP|6!tmh z&{$YhQN6J7Vm#F5x4P?qn0`s5;4{np&NgwKz3`k&@r}t*$qAfvfrWchxArilV00iUv!;snhL^n&-mPX&K-@+=v`vw1w?jmmJAQ8Y~KJ zbwdV!2H^hOQ%q4A=V=SHZKbr&)P8dpfwGkvAy?tC&~qQ@8%I8_G0Ds_DJ3$k=>*+V z2Y$>lt=(WE;iYU>{27!|E)9A=DTFOdo(nMrhXAk}HX*^-^=jrLv5 zhMeW-j=!xmn$n@$)XlX@yVI~gIZK4nAnX|5C#s%iea4QMDzq4uUP;y}niRNU2c9P~ zw`Ju;KQ;tvO;#h|?MCKab8Rn@c9;X^dDe$8ejL zF^A0NBVU@e{%$$eMTo0sj)l*n-m-P!epD}yayp6Y;x-2#9<<0Lbfqyf$UI%fr-*D2 z7C$Sl7&BJz(h;iLf`!1Q&7nN`&??H{LXq`i74La;cgi}bMwku|^LVjHC`tRf1=&BFx+eI33tvdQvm&a7w zd7^hD<;80FzeTDHF{V$z-Q(pKs!ylCMj>YZ1L2M%)L6nvQ1sCDHJGNdJ9>~ZCOZVa zj&Yko!Li4ucgMCD-{SU6z}K6`8}`6Ch0bWl|MGyX>O)N8U^ubG=nCT9gvertON2ny zp#r$Jvm*sma%`qv09XcUkF@t*#V9PS;gPw#hJQ&~(!kTO8njDsJ(8fB7!)(8TpC=t z8$TS)LaKXR{iVR>V7qSj+~atW+)vGEV2B`?hOI0-*Rxa=QSPq!6YKQMVe z3B2AI)fsz#9oD%QNumw*le{h_4&4qqOf4Vd9pcdCIik1zs5auB`RyWo)@YQLXH-OO zloIs-c+BQixniNbAV_*@YE1F9PC`M4MAndN5kd!3aJ~yk%eg3lzGG&HKsLy{wFI!9 zA?LRb?EbcfJaC%*h~AbJ$XClUwfW?sS_%zmuJW)R6?sOyx*u;)TKRkh=Qbb9vxrB7 zg|$khEfV~_hB?wGg)fJGQ?J)Ct3Jmd(7l3r9Vf4vgetee)s=xHL@?&2zM&!%!E#p9 zFGqOVIcW}X?O2QIXe&H<8Ei7Tu;4UdXb8hY+MhlGD+A&Bx}zw1>H4zn+YkKE0$o?c zDZ9ecQ&3sbi+*XA2|Rvg*h_BY$@q+TucBEgq$7Ss`|YM{}8fzTAticX$*uH9`l^jKB=yV~ue zI9=y@`@N0H^D{^F^}S+i`Ss+n)W0(|@p~vw(*q%jC*KI(xZH@D#HdHrAo9=aD))+6{>te6>j z^NL8$X>rZOVWLdk0gvxgNSsaLw3hoz=dO{Z0jgOn{cpg`^@i=}B%>t}h2C+h%u5nW z^8;b?>g}oWHFwsBFVV+Y+uxtv+t5qAZ7zLUdVcKH9|&&QLzD>=yuROF)iIuYtj<$> z&%E5Xwo_hX0eU|bofNXE1m0FO=Z@ALxgAG=fN>C!(`G#Zk8hO8mqteVr6c9a{1^1Zb((QVFZn2}pGTDxT5n4XtdaLSfw(IY%tz3mFv*)fU z@oKrovOC(7IFDx*{m5*}=M{|T+X~mQy=JmAwnXGrNQ#9eH4sUTe>Ead~G zUx_=Hs`qeTF$BZ&*6Lal-foXxqv;TGxo*Ehloa`lVU488eUJgoLTlvRB53wzaP~SAsO+pqzRa2EPuwB&Yyp6(?nX-M`kpl z`yK{)vT)&Os;cPv8ZFM`l9Z2ys?=?3TbUgXp2anCsIo2~`x?u9hK0Xr28Yycw!grA zV1p~Udl`jCG8XCci4U;stqmo9>q#r9PuS^S%0J5zG&#ILEFnGiWRl@P@GiM5hCm)C z(WOPloSg1X?1{~{fGpuBayUqy*RM=*7ldOMaqx>&H!1JQE-zu6L(U4%OZ6BP`iaIV zI7RC9NFZT7bWI&>L>vC5bnJS<#{(Y=YuI|;$zLu$Do;v5K|x612S3C&Qu$_4Zxs*c zmTIY%I-P!~aXO>QJ8?Ogyk(CcwU(_XbPCuaLgBIym3Kv^xOr8>ZZqO;OYM(lbtw;~ zjeu@9)NH@0?VD7B_x3#60gV2ZQ1s&LhIey|BYEVYiQ4QrNEFdL5roTwxvr|XJiQ68 z0>o5VoKc*$z5jmX*hnt9PzilUmRY`*GRDi?WpP!^vQL+^OQwg5*gH}}}Ne10)L^0&4U zppYQxtSxKIn>OaEuzDx`Cbk#BUBZf$%cs+4IGRob!^AUrocn{3aM!{;RY4U+#qs3~ zA=%eeeUyaVY$a(*D&p0H68unjAsY`fc6)n-3pX`2?dvoC;weu#YAE?6Y}`#5{Qf>} zb)%WuoZaa+e#T>1&Q#Dmrt-X|%bYyY;YP z$eK8P!@OU$mW#>#;UOomQ~fr?nnV*qt`gnYthC1L8_fYQ6a~6sP4Sa8c9C-)YVffVMPhGE7 zk_RA_WqUaP;|0MkYO|u@WpDBtqb!O@*JM7>2>F(Yaw(=iR8bP{+`0UE3|~blvC3Q5 ziQ=6$;XPv6`qsD*ovNGHQlbP}_jqx*~)M?;8$ zACWg8@}{+PugqM*e*jj^fxflwArfA*K5B{U6J6rBeJ{=8CU~SC(G1jzHWov)yG&pO z@dbWF0h7R%BLBOmV%{n;2m_c$ixdSYKP%LrukuT^WG0_}Bp&H2YPy^T^d%1YRa7&986HlAa zGAQ+s!0X7^qa5GE4foZ@5JnSkXKts?<-En;ZZo#KFJN(ZFZG$QFc;W6LLT^Xt=ig5 zZT==;+|Adh)w`(8qt2(T-8P;i(L+&O$ZYrBM{?|q-PzD}aINZ;c)pbcf7wh_`72y|g&JX*7n`Y0&^1j&)< zycG-3k*3EP`hsRgrHwf`S@e?zwK6N^o#WK%OZ|$b@+QdwpGjYPV3nvDFc=FYM33P) zfKfLdR)k>9a|C{ey=@Az%WowMVtt9Py51Cr@W#|slapNzZNYW+Owi4KT~FL?#-;de z&1MhC;PHI>4>*E5=LoqFx|UBv2RK4*H5Z_3+FRM?w_}TzUN3;%YyT5Ij_kplv$OV& zr>E_cv-YYG9wsWSLlzVPv=sD;U-z|$;KvrnMiGH@&`YF3q?Dvg+RQ{xS7^aM&gVPY zFRXoUB+-^8NSs)_Z;2un4I?QdCQ1pV=s#W4ElJ(k%!Iug*?7>n8Oiuci}gUe51vWUcPR|+o9&NOlMimyU9-6Q;4(@!d$)+N?ZXXVaJ36M z`dV35N!qoa)(|V$Tf3x0AJm!Ll{PXR)SK4~|ILMxa=hU{dI#Z?<6N$BO?{P~E}(V? z<64aqj(wP1ntL$K^s0BC0a>iJVl1q%XSJq*(_Je>x5qu|3mP+Agq0<3*JpS=pMT!8 z=|ummOPiPqRvmSl+vfVtid)eE|e7XmK9~(V{oqWmh^H+3~(%jlY?_>%Z5bKeY zQWT2)o*>I8NgU5%DjD-&`XqdD=*VTdQRk(dvs!FgL01$>*eicgWPEg)1jr zTD07c_pxXS711XpP-Kz!8~3Twa$`|KzVTr}$mVyXIy1G&ZfoQLwfE++(1PymOA8w8 zoi!b3O2XlsAF+DC6!oFlY)DQslnH<735i0fhthF}*TmLyysKsJM2G<@%ncPvHD4gD z(s|6?5(36ZoPIq`ri1Xzh<0y(s`3Rn0&fdU0Z%y{6rtXpg8^e&8G;3muM=VpnYKWm zjJk57)&DCX*y7LzZz!T^kWPG?bS5C|a+u>t&sHZr{8Ve6p4AD>w zyHgMDBvy&Iaq-!zf#PaiD7Nw1v)=`Qn!2&c%wl4urmtRU>--c*Tlj-Lhgbv^`Hq#@ zb_el(tyBkQ#&XJ1oh+*==lfT~5FEq1gXF4JC;Rq%vl zm2x&rh!q*?_~;g!eR7OR0ve_&{&pt*z-kv%4o^;KEF-)z=|{#ZbjyT`OIf9|_Dr?* zT)qp}akBR5+tSN-`v;Pri-FB-Q(N@|IQ0GvxX)9JLUP*0LsY)DT2cEdLCy5 zl~n~m*%SFy6%|FQ7$KwxGs-@JAFtxcpHSJgdo0uqtArNd&m%hZ{K?BP2Y9)rQ1$u- zK8z9G$zSh1*I}H@$&&8Lc~DlmYAIhN=XcJEbF0_L)v4VFEB?$kXgG)B3A;3$loeCb zLZ^Y)BpHs9;e#CwpKfLR9h00kBMb#$Fqp3sesk|NB+vz7cWZ+9rcVfj#~wmy`InsVuFL;c`WfoF zUcpO7M+{0k?#ot58NA%r)#6!#JLBd1?#;loXa`pL+kB zIFVZ-1h$>Khj(j3E)%SWk=ZBjb|f0EYAO`2QLrF`y3Opi%x|iAu^3r*Gg)kPLH;kP zubPj!1W0u8*Y9M)8kMM59(Nt(?-^h-pP-gHMfl{uR=kfgPbl%x<`{o~2R`!Ul<Z3nKY(h?q&m$& zrxOZ!OqBC{uYZZ2W~dRme`7qMkn{O{>te5>o*d5L`-Gx*`x&m~|H+|Wdp_^5q4pD- z{afJCsslbu&U0k9rl8oz9zVuV>}Ai>Q6Y5F;KgakCaMJc)Q9J7nZ~3~UjyxLQvz6# zkp^DKSscL0DJ$#jJTmek_o_xXi~{g;kOUI}7;t9D%DOm@j=aj%(fB#Y%hl9~gRuf6 zId92UJ3D_E`8jZL9vjKeHPFa}$phRtr)8^u3V1j#%RX{m8yU&X)zFB7(E@%7I)gKg z4k|WpqdOa;{EUuOzaA{1lY;*eUt1l&c^pz6-azeD`>K}O?A)7b`Y7k&JCyk|xi{{o zx~XdYYYVjgwUJu?+BB?xZ9>++wma6pwr}^=17-GF(jTMsn?3U970fm>iw=gy|H4OC zV5A(hj*3q){}jm>W0(d9?Onxxhy+K)-!a-SA&!5FF0$mb5XRQ=7%I^Z-i)fCGk1v? zDDvWu2$Qh`iLU;Y)9|lgv(yJH=w^DT!2UH{B#Nez z%>mqX2E_JFaj#K&mCqf(-L|pK%+Mbo1tn2w;ZWXX8`I1P1@7yffhmXbZriwKR;c9u zEnE%CwekmOp{;DCfZJ1`DVm<`Xbp2Ic`W_Aurli`EyCP9X`MLn*vtEI)e`7>zkZ^5 z^WyyNrcVE;E!st%9%Z;6`x)rS^! zPw~d{^`N3Nmputr=LnWQb|kz%Oxgl18ujG-S3pcQ`tR=I;IQiVXU|VVea-%PPm>ez z4f2-jpAfZpX&wLYzc2Fd%R*ijaT-iG7i*cxWfXrfkN*W4`z`*MI$pau!$@C{ej=kMvnA^xrzd|v-bcPoagU;d;)qh3 z(wZ`ivg{o7xr}r3RBBWqRP|K5)WXz{sVk^^sJCgPX-sL-XvWX8p1*m%1V99^2fzSL zfDKw*S~zVR?LHklojjc-T?AbgT^l_oy*qs|{V{_%Ln@;*qbFk)<1~{X(`BYqrcq{A z<_F9j%)2c5ED0?AECg0dRs`z?n=qRvTQnPr{YTSf&tTu>Fy<)Z80R$QjO8NZy2Vw- zHOMv2?ZEw*yMlY2N0{d^4}xcnmz!6O*Pl0^cb4xWpBA4r-$TApz9qhW{&W1C{1^Du z_!0cY{7wAB{0scs0%QWL0-^$!1!4uV1j+>31V#mx1oj0*1S14f1q%fm1P2A@h4_Wi zgbIb~h5Ce0LhC{V;Z)%~;kUva!lS}V!u!B;Ku+KVU@@=(*bkfq;(!DZS`lNBGLaUM z5s?LvZ4e2_8srXwf%Zj%L@PvB#6-o^#LUHf#Ztv;#c<-h;)dex;#uN@;_DJp5^qm4 z^`iphB+*qOaw1hCveUX)^h97H>oW`|B0Vi2nU$T~=+1PN8Fo^Wo%v9Vp8T4amTtIl zQ;X~sYGWypSV!`h>qX8YjY)eV`(<*D<}6cpj5f;z1aP*CC&RhNXfSS&35G?WSg$nRk2JMr2&ub z^t8Sv5V7!dR?jqqIS|es;68yVYGb*<?CLi!zOc_(L6E5A>6}Qd}Z}ZM5 zf%7WK`@BaMi@n;bn4RjVMcncuYB;@AYYH;F9Vg&`M(1a!$22&ddGYS!tdC=p@=M&a z%&G$eYQxvEsKnr@x$8QC0;5$}hXG+`RVBs(U1yf=bdwAlMv$QKw07AlDUZ`_?xinJ zzgFq0G-7)+v(`W;n7r)#wRm*y_oF6xNlrx_v}t1M8v7W!cmmd$3(kIc<+i#uwC3cw zoIVsSV)=0++S%$+tcM)*!nnFkriopoc-+f~_$5F6#CqJ5y1qGM(qE;te3AT2ccE@!=PQj(Ho};UXLMS zqP8uhs~x1M`C$IdKJV+A)4{grgGKA=gl#320Ha}713mNxoYbKlMLFz?lZdY}+Wt=2 z!bsQwS=F8dm>6q^UB9hEt#i{dRxnlxeGCe82HGIo9nsx zj6;k90kY%TcMN1F^t~ltxfG>LPCLKJ-oLz7m^9q5R-C!77NeewMgMsYEM9U^7LPC zhn~FLFgPKXq=%4;{fgX?;lRsCbIL@l$c)RBIQb*n1l!tB{5^gB_2FI}Jx}f3+%|b| zC*e&Bo)p(T>~W|UoaY63NM6cU_&l&b7ayB|>gz+XeVm>dSP;fGw~Hz|goizNbpT`L l*SO;Ql0Dqa(W_FPKJ<#0wHH-11(PQ~10@ko{|Xh+{{Wv=7+?SZ literal 0 HcmV?d00001 diff --git a/webapps/docs/images/fonts/OpenSans700italic.woff b/webapps/docs/images/fonts/OpenSans700italic.woff new file mode 100644 index 0000000000000000000000000000000000000000..e12c3a9f273c2f0e4952293bb852406c2ab1eb0a GIT binary patch literal 21184 zcmcG#V{k7)_wO6qwr$(Vj-Bk-wr$(lF@7<2xMSP4ZQDF~p8u)3_rAI3)tRdAtnN?E zn(kgbQ&ZK`%UwZS90(Zb=TPneLi&#l@A;|!m;9&p|31XURpo$yfNg#l-hZUCmQbLe z!pQQ&B7W++KQburXazI2HFWr4CqJ_IQC-Z*MuxGQ3lR_ysN&BxMn9@VHOICzb1=94 zVGTe)U_XM~X?HGiH8*tr@rC;F1A_anpn!1AZ9L6>mFL8ARrL5pSCnIzL-F*O`Yt3fcSsL zf$+mf!QfI6p>6Dqe|%*Ee)?PeA328@ZrK`o{PabR>E~WV{}G59Fod0z1NQvUU69S`qJxv^kFV0ikKe-&1Kz7a1^+1n{|IO_(w_Oh z)>w+*zxHxP*(2N7!0gt1t-oiirBA1cmOUeu6GJ93JwOwdgb6dlBvPrf#nw!6Vnvr=)Zw~cA&~a3_#vb@c-$= zf9`K!U>0D24IvJJqyc)s9P~;DhZ@iTf>Z}|$`ke-6Bg4eJDhIEys3nwl%SlTqNrS} z?4U%Yq;dZ(sBs9A7e!PCV+alnj-3Z1lOFXQ|4sNNc-?R@NV*}@48Y3$d1g#4^LrYg_bAgwwv%%Nl^8peh(nnHYw1<+UJjYV! zbO#e9Jx24_>I^4SYmKMP>kT4Ie2AjN>3ZCyx7+pR zu;QPjOwW8HLtze4O$^atx-cV%um`8wb0;e-wF@iRDbRn;*4RmzTBU>nPJ0Kd&ZY9# zCR`&9uV;fYLK$PM$Jkn4ncnShl(H9DJh6sj*{$`6`22b!Jr0#HRPaEhGTw1NaYi*n zGQTn2gdmgAZ2p&PDg;dD2v-2N2nYAQ9pC|V2Y`wIJ45+oJ^ViRfBgo0uYZ4&2TyeK z!(k6Parl#3+$g)R{SpP4a&h%i`+e85^VYjt<(1$D?BkWZd3)z$|1)BjwePykMh#@6 z)gf+U>+R#4x^)GuzTfSuxqGT0eHw-UHHO`I{HOLk=iOCf>uSbn`NhG2huOq#C0T$3 za16Rw7s0sDP^^;_*nOu~E82(`7zsu|jjR#}4n`7lgrpF%{w+EeNNB3WwdUUiWnC>N z^~!ozX=3mX&19pV=QW(=?qw6IoxWVUoGZAP}T>d6E45i z5KG6&Q)Q=joFLR|i3rJk41?E0>yawS5yq#KwKgn~+zl(Lf-{fb4OHiaNhN}FI&Bgt=Idt` zpcx|dw4fI+mT;fGCd&$JSf_m(*3)T6UV4=b;Y(@;zbCK$1>BNwKzCO25ZR9mwy33h z=X5df$tq6hV^*N_D`pEp%-;BIWDNs>;BIP2N7fb!FFNG!?G6MTOtE+kAR}C~n1w@+ z-yv@#q>P^`YxX)yXs6}lZj^yUBZTSI;;mdugN3C(`Ae)0D}8xkJS{QB+MGMPJx`7w zTatTYtHbtK zn$bSLt8)@<^Os z^ODEY#~y#a&a!57vm4DJ3PEs+*j=()!Hbl+0-!x6G8zAKVOO60Z^)?a!l`p_dR>_) zRUjD@3e_A3jHdN<4(-sJTGqdS6hw$@O*-B_E?z7y>Z6j#D2_xU1S^(u@z_e%2ve0x zduDI)7_E|$;ooX<<=L#~9?+C0)Fji~#wywTG(@t=k7pSPePej@#o*hI2~l2Kw&dRdK>sE zdtxe)x};vrq7;w>BGC>xx!ax{pR{`-fe{Y$>`2(XfN zGmtaO%ga3pQ!+C%GqAC(G7fb!lJr^c%}dnWN=*-!|KNYI-d})deReZ$iUK^%1n#-F z0LB8-AP8VhddEkwazMcJ5iU3lOn6SxVEK&UKE)_$HG-1y;J~M03opq1w5V;O6|*48 z$`_GeeiXAf*zm{SPb74Xi~uH&UB?0oNm4vR1Ut-(W0&ms`iSIF z;8`>V`h{IVlm^aYu|cG)Mu?>ZPGs6~7L^PQtDjxEwdZ~oGu9=UB%VmCy*X*%9hf$d z^7={zZ5kdfY9_Gm!6tX7I~85^72!ghq-`1nq*`K@W@q~t-pK`P^X0n2q5Jtm#0OiS zo=-fR@8a;~c1`(x^Aok#H#6*1h@nD(DiL8kIExQLR1|v5GV+WtTI7xIU|x~Fp|ygQ zPW_>ckbEHZZ_5OdG)FtXY@7f0&r4{;^cupMCV!30Dt{5#cK$spcGJK~S(>Lo=F5zF zEv%0#UHmKDXI@A!IYWK3-PF73eD#oW>cGYmSpOeGolTbR=fl7QcRh=PotI(IVnXU~ zF#k<|VgEZ(R{>Qy#oibJZ-B>Thub91_r&!(&8_GA$m&YfgxvX(O#fpdi#K8n5et%# z?m4_;xdRMU;Lqa+ktCeWN>tR2zjoOx(gtD0CfsYJ2y~N7(hk9bU8G`K-f(`3yZVdl z*u0JsCn*_iuSw!r8*>oAr1CFQ3`DwPte$jX~Wm z_Yd{046E_WqlnSi@B74H)?95h-;+r2ikJ2l*Wc)9fQrAXVKoffU03K>ZyP1Q+#`7! z8~D6z4n`s=CDdZK;ux!ZZd*C?To0oI#%Svw%?%qJGB3(ePZ02?6dBU54=}Nj&gABcL3r?Y6pw}_9OU*6PjQ$ScIG@`v*-7=<)HiS zb+W%k=-93Nv-b<*RNrK{8&=#k5{{cA_JLa`z{%aK732R#I5&8Zb3G%fHcva z7FygKa+Cz_toI2_TO=6R9xWxZkYv$_u~gH^rB^iImwjm3jT6@`O6A}zR`cmUA zmCdI3pC^uF#uQw013q;TSg~ephMj0CaffCryY@U}mLS#RMnHQX^b5 zp&564a0}G1x;)Sj7;PseF%F%; zwb=MG)~ZSL@GMJ-=wO1dT1!b7u#DHbirFduEGF(s{e917(gw8=cD7pyj(6i?uTvff zSL{d1B_!~?E&F+a=eB?pKC6!XDQ@Zm@-!2{3A zqcUN%0wqiw<%wW2Lv9B$oce7kf*YU7Vu|2u@RCN1H|L>MyKR@s+{X5+$Um-bw)na0 zPNuIpPHtQy*f5O$I-kDS_H*~+qq3Nz$%jLCi{))Yf*`4%h!n{F264<4BSN7&sBXo46q;nV!{3H_C4F8P zXu~)0Hy-(#=z-Z23Q@sC+J_Y?6=ooM4TE=jb=$x}w|?K1a|SF2+lRjmhR6YRjQDo85<#ht18| zJmK$8zALkW_0KB;+$WG9{22aGL^r%=llZuqSxv(CLF+oB&cR`AcR1pIRLB33#X6(D z@B~GgMMX+#?uqaS+D48~cf+H`;tOH9zHI66@;NKYd0y>MFtQZtqN75XqzEJ(KUv1R zmUkOiNAv)FWJ|NjD4t5|(BNwm37o(=5=^*j@W>;WvkHQUx_ha&_VB^DkJN`$DzNXM z5t0M3G;a$KAnWes0lf{V#ETb*v#`pB`ALsRG#(UuUU}I~QLi+heZA2NM0FRw*92xo z#)HuzAiA9a9rS}^F|t9ahh%{;GF^KFy@(sv4I(C~!>=iri!?=J-cHD*5#$k%l!FvN zN#zmWC{l<{eyF@?u8WLw)wLA+Sb~~GwJYPnh&p*l; z5k}kSQ(B9nP8rX2VT%npNcR^G?p&oqv4$8}O@iSL$9Qsq@@-NsyP6sBSzMyQ!)IwZdYs~PV1QBvCw1c6^)QfpQJ^gs zJGuVr{hr##fKYWi)5&YCSx|Q%-0cfuJ5+oRu42=05Z)8oG1^&$H-BUK=6D^#1aV}m z#S2`_8n30mdOKo56ClUse@V1;J9zz)Hnq9M;E(>q1wQF;?SC7%ZmjS6I2n#}hS<8) z3f7JR4H;R2bj#IfHDGNRC{uArZn7g~F&eVpXcBacHsxdBLP(W#!9cj~w)@NtS6bPF zX(Rl%+8418rsFr42WN6cQ2#OCmgO7YMvu{rN!`?P$g-AE^6tWO)Gm+i3s9Ptginop z>8#bHiDnTrm4hYR_mOu7#K=`zeBjIo6C(!6b?NC_{Q6RCZrJpW_SHI*=k{N0R=4U; zi|aqF?|K-SO*VSIY$9-4e7A4-D5TxIPAfsF8>7aYsJZ~DJ5b1ejvx$i#T}MAtMZ;= zjwT7kt+LU4${?~tM(AP`DX%5?0&oRDR-?h=-LryU#BDR6C#vQwenGxUZv?Ow6W`tF zdk?h10yHE7inbeg?$~+#6qV{4eA-hFTrg7B6O&Mt>5HNdXTW7$7*2JFFc~? z-DjrUYfWSNb%in&nPsG32sj6?>tis_a?ryDJci9^6Nn+0-Uhfiwq5I_@i-*$iWvSO z7zr<;iWSh|3mr(3TqYmdABMzc=CL;;p;3ct6ikJo?SreF)9nJV?LV>TExrBj)KBYn zI%(U+pwrf6;sgDTOxiT!gpHwSDh{*%c$n#&+HBsPx4-bB1{(5urL-~qfOS4YE9?24 z7XTBQhk}gO(HP;8p)YPm!UnZ(eVgIQWk#?`CYue+I1vE7!S)PN;4RTAu;tkbpUXu^ zLNxqJWZ898B3Il-Oi)_AWIcv0(UhGgcQA zZWh)<<@CK;^u4!d&}=z`XZWxw<*(Oi&uC@fdbWTU?&BjS#xdW7ne?FSQcCC|DrtmU zFn_OdbLITVeiyc67jVAg@oZO3wJzs`?mlGslEqsuw~e&O88{>J^a~3D4u6lBMVBR~ zASGQmYQ}}J8@*wZGGoM79O*>$ohn8W@=ta>-^<*AaFu_!CpJ&lpd_1;lL=$_B_X#Frfs~$2&HL6+J%$(60%~#s(6?YIl{L=>o3Dr*cMm$- z{rbCGprdl1FOr1>HcJsgfTL9?C2{zL9SNjRgqqAzrTKcJ(qVx`#?5r~8ep6;2;&_j^R_@8dtFu6nGLIn~W z1Kw~7OtbSHrtXl9?=WSJ`LN!{V{I9QfUj-E9pmMw8Qi8FIAP@o3Ab+=`#XbV3>tw` zlK~{+(nekYrh{ujot?X5WK9fg!iZD3t(A1TP53p7%lM;srz4RTBZS~GWdEx!zVDmy zcmeF}mzh2ZKug!z-c}}NE`d&t5~nfGhV#?2lZA(gq@T?;!7C!Zr^{2h6yj%se(tup z#&<#gM70}|Ubd;%&b6v=aEOOd(n9OFQ=(%u3Fa~^r*X$0FUWN40LKm67HODWJ*rMtMv8N=6Ff296C3om8$jiF)Cbi9 z`Q(=6U<}%@>KPaQFKvFNuoCJ;M`v}#W7=oV-K2{tiu^AV>E5IkF!*hq=ePz)h7Y|s zi8(JcA9PZ>WHl9|nT%5&?q=PMa0f3!STu~%1LJa^l;VCVn(?@SfUMhe&-w>9!KQB4 zRE|*U7tN_!-F>*CiM?k1$LOkl9uf$TIcAFK_+#8J6MxopL}-V<=mH?a_tHVF1YTI& zU^bSq!$w6Kyg1#BunI%G+{f@II*^n-EYIi+8@{pDyIXGChhpxPF&XyBp7R;GSvfRG zG(f=8k~cSTzXww8ip1Jn9cbuWYkoc+^6%JBLdGoxsn}QbI((Uo^B#XrMhTAkdAZ?H z7@_zan<7+%%5K1*CidMbCu$Ftf|i;F#7T?awOH1W=bfTyUy(jRdL`&7Jhxy*!w&K)QT@3DK@|+uNfrE?Jll`Vp6U%c4V#M zz5W{1y5b0oxgL>RAGTVdaPucB)mS1Ucy>Az(S{^yr^Uw9M$`Q7pjNzBa60)soUm{j=Tr z2tLGic#KldeZO5zjJAzhxVG0>zPW!hout7_$mM3O0i1rkQ|_$nlmC*rw$*#OyS}ex z9545|IvzDO{gX52nrkF`d0Bk(14$ipJg<7Cpbz{SZ_O_ zo(4F34Lf~}Bz79JNpFB*Fih^4mUWxMg$mcj=EnKah^D&P@7S2;Edr>HyA~Ob9fg^V zBevJ{fFf{ZEbV#OWi}VIh_7oDVhsyoEy*M?35jyWe7a9Z$J+)Ia7r9l13WvDq*2eU zBqjZsZbszT)B675oqo#uaZw6{K;y4B&()c}ZA>55(z2)sb3}_m`rsR3;R8_;Z<8y9 z4Ib5KC~8-pgzLG}Kg6p}i?6*S1aoy*kK8lk)FdohMk#ApgO`ln#6(xG%Qx5GuV1EE z3ok>^8xewa&c+pC44xjCTHWP#2Rq30gZep^VWL|bbq6^rekF$ch3)>jH0wTS9QiUv zT)o>2^8Sr9WQr0l;*cQF!hu>uNK@d=)^W?YP2lz7);!Bs5REeon#h=(4DAZi0GRz@ z?Y<{$qqwUK)@rKpjLa%Eed(9IJ|G*Ijze|TxHv{yYVHTGzQvn@F3V+YHV@`v$VP@C z3P0A}-{uKmb3#e1VrQ${r zpXKx}%Vz;XKCCpgWjxLLuIraqQ1O^c{ZVzBXsI!-w?>*30_+l|7j{`ech|{zV>Op} z#z|~kz>Uf4DB;92hlMoj*;ur#F55}4tLyxdT!x4vebkNz{Oqa_(%!;}k1PfAu<2Ym z@WX9k&r00(5L)Yk2&n*kk=odUkcjzhD|nj(joNTr-*K7mTHC|#qAL7Ohu945Y)ioF z^oTc_*6JaEc7U^=U(HmHN1!uoFj1Xd)a@=+_&&Ens3*IKUT&Hzzk_;h9?3q4`Y zK^%O6XV@52aYWbRM>?;Q+zi4g7u@Y$iZ~6y+q5sDk@DHM!`22NN5IjHB|na+ZOpmi z94+PFq>j8~T(|yje8nTpD9aIfzvE7<4h@_ztz80M+!}dzwfF5E!j2iW_t@c*04jZ z$4t!n{K5O{2a|~QUqcnzAp-t?wQ&eLb5BzBN|(O;U80e15Tgc{_|}z5PjF=OO>Afl z*>%G1%ov7Emrl{ZMAwuEC^XmQQwn7p#x4d~&>9=Q8@?*u6|N!oG56Qs*PpxIdn<1o8P1AurxD&a}^kgAn&eycU*`vzIL!duf8f_ypo!+v5_w zfna~F$)nq-nTfd+9zlL?1mVg+Dm2qB?6!){6mOrUzvG!iI16<7VCK;sScx z2R?GHKbgPRSP29rJcKOPdOS8q_r1}&v~+8OJP1RF-}!cY%dhjP7BCQmV5?dMcf3D# zKkCdO5)$%U>=aN?60)Cke3TV_T_p)jh4CHv+{uz=n$7m)bz7h6%YD1w_uK?+13sMy zoCVl847>u|P}UmQC~tm43zgp8=bvTdmZdGC{BmFD{?Jpxc8nXXk@!6xq~E*zy2I%D zt~Y2^qUj>fdV8KVlAXwAuN+Py*qvvS20);$*B$xp?TGxxzv_HpcrRi<+)zHPuP?un z{1&+AdUcovW|vvJUHULu3j8X z1a|llU-SLrZ1AkDF@HE$APFcK^|sk{5BeA0qpEz`vt(YRzVy-^F?Sl`Xlp~kw&0=8O%W-~P>e^^l7+2*46AUq@k%o?D=fD9J9R`0*8( zu%TZlH_byyJ*q3Xm4=yY`WvO8-F}BcDU=2Wv%Q-Fsbc%;5y+{lv1mVv({*P5&T!EFYsVmD; zb!y%Wsh21XbeQ9AO3$~5t?9u>MjO-g%T+o&)%DJtMw^7azTU0-eZ$O?u6H3TCT(3mfVuw9%Z+_GKWe+Pp9<=`B1{HbX;Q3v*H=q!fApi(A_4j z;FwEYzWy4DTOEDTC6VTkF__ht8Ftb^dAx~Uc+v8Co+g#G8XMk`y)LnfxU&^&7sDZN zi2Xu6gAcLm?4+L#MJB+xL>a_~j|m+WLuwGE)GSE{$pZz^5E&^PcrX6dc`vSGn!!PO zv$}@AM#ap?C~naNoqCl=zaBGwm1pcGR1`7Ku~rt8KHCW#~tpQ}gehd1^=^ z-^&0ZN9v%81FL~ZCq#=`M;22e0)9{V)>R3GKbGq8HbzK9jRn+fiJhnrT$n*4QyFk~ zb3b*ae0+2DH2fZ;yKMa+sSeys-G8eqCt13aAkf)e!R#K#p(wZ*g6XUz#%O^E7%Ybz zp#R6puHPD}KnoD`LR^S(+e?wnZ6dF8ny-TQz*Lt}AS9%o#+`t-kHlo5r9d-5EFVp~ z^q}np==D)!G+Qr*k)$bKs!ACGgX-$6d@Vh1&tW_F8xSF@*pU2w%bSm6+pOa;so6i( zD&)poPL-0>dFu`Fggx)pw&u3T;mIfQ)EU~aJskW5jCq+wp9&6~SO@?AqkhaWfm)tdJo>g%&*Y;24v^vaqjk zu25WzU96K}5zm!X8u=G3C?(1vg`ja#{}Gc4;~~?$sLv?fY#d9LiL)4Q2jol0fhcvo zIvZRCfncTK(1a*>B5+Orx}-#lQK0 zGK3qvN9HH+lQ`r@Vw2Lbi6U~cq#&t>U)nU65218A=#{|;bh&knAq`@U1N-B*2c!*y zZWq2-!9$LrcHKjvv1yFSoL7`RuC=bvCs|1G9+6r;9i>)6m3r6ze5~H-%^gL@H8Zu- zBjoRl;7&l2EEy7ztl21CFhtpx^rOp3gHZWgEN( zLKyh{_?oQMZ=+tP;c>jq9bA;hi{k-Xk87g+EMW>6;1cWeVR7=3M^`OsX5s=rKO7h9 z`pFmnVK<%aL!MV6KKq;seOK()tJr?F6<3Fh2KFl}hq=HK&US{)z+>LKzt|*FV%dRY zc6LEwqZ4gS$xmtW42(osO>6D!5rZCKU`&UP%fP&wA>gs;W2tC7NNy>T%n?X=imq~7 z`JPW3f&WwR4+^6u8qR^d?yQx1fO|Ngc*>+Ih zUc5P#F}InC@1S!*P_TU|slA=@D{#vh#Qx)kZ0pUDl}_*iH|x)K`Fp13UDQNGu13CQ2|fRP~}s-l43G=BYrb|KS|v1 zwtOIc?Pjc(wy3Ur@YxMy4foB5J=JXaCGZX`7l8tY@P0;o-tGGE#P7Yvet3VNe^D^r zOibgEGMD|fmuk9EO50Ft_(t$KKmn?%+S2<4I!ZL^eWECh#f~JRj33lUB#j^ z`dSenUAp&&rufV+C6Sj%<6X3)a)?5yplWHri9Yhn7Lw{)#{T8rw0QWo&u_c0Xc=J# z#M{nsD$m4y@OvqnyBHp$17m0hw#-YQD{-k*o$+J%TbbZU;wp+_i$RY91MOtNSqlqs z-jLoVpr$`d@l3JjaM0U-;MQvPAz#OMJqkn#dp9MOsH=N^_@xXdPEpjAD3GDRBPFFF zHmn@)7xo&|9K74WT~$g}`0p-NrGJ(lhb3d8{cmP}5YN}$u>>POwr}LnQ?kNR>~V4h zDoN;lU!XksLGXOg{DTLowUVA`d=~|zp?>9pI&H(lvRt2A7W4DrziHh%H2~HxPHlAq z75=#q6o_`V_@K!=Axlp4v;vjQg+lf~+3yTL5u^;cUKkXu6K^H+p5@wwzO8NLyIG&6 z&%SC?PU*}xTHV@k#f)#eS!!s-Cotqha~3`MIhnXU_8W`OG}5%FqEY%tI*Db?$9#M? z9y#25ADP{w*Bfxq_E+v|6&J&Y@Kv#qjXX3wH|)C5UCgLT38M_gmTP}&8h;N)KGwsE zlNYm#rKZKmM$zJ{bQ~;}F5NbV%R}$e%N%zg5xn@>0dkd|d|twe4_gIBt&#*KfHF7xoX#si_KrPO)($%5lWSE^4SAL$(CRWUdx255+}62 z4dBNdEkAy5xlA~#!kIqUOb=P8C+N@x@y@lUF-8|+uet_vnWp##NU4IE?KznfaI2a&cg4Psz;oo-UWsaM;B?ibP zyJcWPuic%?k(dQQ2v$WG3cvP}mwQ0kS#b$xFjvi_FX3Jw;7;Cj&_dgufe*7HSh0k>oa)>!U~Mi;*hV{R zZ0XujC1V3}=gG(Ka%G05Fa?V4f0o9Qrxlc1y#ab)Dn>vVX~9W+j1~TqLVWX&7E7+E zVtZQTFfem&Mmb(u#T-eQHt)nBz6)VbN2A4d;hJ%{8sB<67wOIp>uf`95`@!6mtJW# zU5*s4dmUpoIUs8PC?qlEL}7+&7_fBC(rKE{(Wk4Ni(BXDYU4^MisKKOo5p;tlyiu2 zTtHm9KZUsHAfhn1=ctaq?Zpr+TdKTb zv@Fn^M61$J;V1>ln5tsuQ(^sqX#E9e#q(q}D^lxo_9V2Ox)FYGxfvu*i>9KTdGGkM z0fn&YIJ!euClQo;2f>P}k5+R(w-f-Y=4g6_FB@)VgnAiE>K#FWLl!v9t!b{QfW2>Q zu1Qd}%|sx)%|Y;KJE5w`7|;Cqp1x1?o50w^LDq6T-3nz^QO}uaW8AtmEoyT9cM0S- z^)%@QCnC?H;l#p{`vLRW`*6pc;l%9{pJ&Y37qc}M|6Tgok}!eS1PlM2C)D|MVW%LO zGkg-7a^SdZ`;h9F*k4JgtC=%*|D&P5;Me{OF^NZJ?EJ%;8u( z1ymySpw`(--vAH(p>~tDU#RovVDWm}iO(1dU7Snx15bdHfAyAiSK(jp>E$5^93}JJ z@!SXqW5y(ecq;#iJQsa@q}Trpg9EX#-UG2FvL?_y_8#o{4@I9kc?}x>q##T6tTTbV zdZx)U1@^xLK$!j@AgyHdG9q$G+kK-DKe~xoNZLqr~^k+F6 z4w7G3-CU@F1uDZ>NH#9oWctV^t+(6TDX!ujlydIqDb=`D=LRtp%zRHVg&i{CGbFjC&dZ@wz0f(o|&W|+gccQEcwqqfM(tsvcNX2w-_Fxoz;=u;I% zotME_y*G>g+!-)3Ju;S^yNM~6#7iwp-5JhEi+kgL?E@r{jl<+(=_#`I6ndS_f>SZ% zcK+dddOiIyqo~?=6aJP2bA`X8+v^c7*RULV!=U}t?cj&2z?!b(Y}(pAc3^Z#l@_;L zI`pY)2g;)(_yya(_4@Vx6Ou)B(+qO=1X}+2tKlQAz1!9YPwg}3M_Ui!Y3OL4n2fcv zKf`6~!wDx)@Fz@1TwlKk3fue%APXf3RAT*&GsS(Xx6YlNp(mYx;R;_=_w2&Kf)WRb zpooeN5><8kyEH|c6g0fc;^tn?c_G}$lb!K@0xk^ojc&*~Z_Rfu%k>(pVP*-kZUq{5 z{*sQDPT@Yv^u7mYN8b=$`+Y55T&u(D&B(FEepK5S4Hb2P*4-1xFrtZ&ws&rpI!vvd zL01_}>U*q?GEQ~CxmwT9O~+@~7Yr)DKb-*g|9zInVuSN|9=kT;;FN4?)fkG6h^wPE z{GdrS?amoxXxV>s#IIJMf9IuuJ9YSc0bIQK-cK8yk1s1Ued#t!r_&vrFA{jiof}NE z@30UK0!)t4dV8)e&{}4pV!MQBlKvJ+kn4dw97E0#M}_0=df&iwK!a1I{i8vR|Lv74 zkHW+E^s}Y36XMdnW^{UFWc+%)+22pRTjy;ww#((zO!&L6<6=)_;F*w#iqcz=xR|`j zm0vJSU)aZ!YM1`@P&mAqK0JBhb@JcWW)WxAtF2+Z z-luEys4QW5#zmulos>8eicpg=drXoLXATV1e+eGz4RJHz`4J>4_bLN2i>yFt<17u+ zuQBi6+&tMWMa(!cF zuPpT6{KzAHEBZ?!^+YVaLDFvo+f97jwY7TYf1j&=nzChyghd*TnbDcotIF`sY(4xr zUnx=h#s!kpl6q=O?K6!91s$8-L)vxoEPy*y=KiX2%V(R zPHGmcWglIQzPA@&r@u}ehAP_d@!EX?Dl=~W_YK{~y!Oj3p z>K(Ak6vZDv{sR#F6JHk_m5vXhH=LPJ&yn=E@aZ3X=#;8m@z){oXKt;oq+)Wl7azlI z4cM3c0M{eus56&{l`l?3A4u_wkbQPEEoitRkC823^JX03(HKmt&12&1<*(HmWFw4qU*Zy4%_f;@xc4`&6q*Q2GkZ@)P zHhq7t!m;;wFoUonLS2*+{a^WsPtLG2Rj()~SveW=AJ(AGhceo)1QAfjn|y#j$M| zRZ&lUOavoi8p@hZk#eTmS+DAx9#XA=BU$;h+4P6FTq!0J&Y$*o?5ZQN0|)3-M=Z+hXZMl_MI3WMk$(YbiY`pbE=NJt5Dufh%AsKL z-pxTYj=I06LeWX?JB>MCADRHIL{3r1cS_j~2}{YA+n}pq$`!E7jyyRbBtFGVue0o} zVluIM1%hAwevriYa}*eZ4H38)9aMOFK_4-eFQK_#U7g!|)m57HS<%jbFNSSZ2MiCu zW~*$iAkG0VZ*(tl?;t}FJT&6&AF6?ihg;5f%yu-hdS((3IG10N;&S=pSTD~k@`?dh z#$F*;)TcX1v3=X^LsPn+2g6*( zwyME%&08G)R1@5sUZR$tGb~>pe?z@VAlc;s8F7w0ANBwApZV#TTC~qnLKK0%2%r2B z*8X>1q{&ZaHUKHEVgSw4L4pUK~y!_Xje1MS_wvtbBoI`pi#I zq4}T+iM`1DYqVgnwLpv6wJ|7`)f%^>nk-$@WX_RHP_WbwS!X*K! z3#?pjimP!6jX&~;g6al{Yj6=#%kQsc4FQ_DmZj@#nY%A) zymouVWQMjakTxf~ClgFUGGqDQ^FH(3aAn$cK`nYL*2+%R2dEr}Xk7$~moMlXiz&B< zk>#W6FJ~F1KGfc`D>8k}u9oWp&PLuXH8tKX74svG)nxY`*xJKQMHBgS>2-GLjDPJJ z$gi`N5Jee9ui-mIgS$TSQ%Se8S^34X-75y;{#AiP-r=0qhHtHB_Qbf%KFtneS0>Rd zBOZoxJ0`D`Znw+#^O^Wcd=BwG^|uEfFRQvnpP#(jdO#ob()Hn$Ksl!eX}%`o+4Xr} zD(bJ$l|a|mW>m$JZrc-c6aa#$!YvE%M6R?`NWEA_cPXA?8~$p z>*zbB9!hFXtGg0H*Tja*|H?qS$XeE>N)95p{X|Suhm=I+dU=i}5Zv*M{KmIAJ@m*X zUv0N1Gj{{O*%FfCPPgp|G|nQC1C{})f+(X-iep2e2ad&7rjN8)`wrfpe6w6@mnzh^ z5Qs`I9S8>rDuP%DCKK*Z+mr6~MSl`vU*5ltFoemfP#>0bR4h{z1?c z#(4v|<`*K^3)p4c?MC(|`4anxd_ldX+*23;LP)YYSFTxJ?%uG`^g#FtjTfew54|V=HD^2~k0JmzUG#?;?r?J3HtvaysPZZ0OaiEJ7BtA7M(!CP+Bd7sfjq_7K{$9p_Uj&?0uFY)0lCr zG-ifyUeDBBAc8*9^pSf`er$4k&{WmZA|7&vl>eGV>T-E0SLwehdnS>|@eKGeB)Jz;)XZyI?cOxMMjq9UK3XM$M?Am)b4s zi7oU0K_wlso*8%O?hDep&2iCj=Ff{!+llDxx=Qo^bLwWK-CZ`}+@YNMK(p(a-r0B1 z{SOGsQftEV-)%X2k%`gg-AeU@T-ssy;9NT^z^R4Su7&4yn11D72?ym0A>|5F$oY=> zI=%t-f!6P}-)WxW3oN~X%k7u(0I%77zG3eO6l`fD+*P&+OTIt=VgTm_73#^H-SqZ)=ESDgE?UdG*5Sf;LMM8?`r5oFEr4 zLdkfm3Uz=*a<*cFPypgBWg-?KH4fveuqXVuPRBn2|Gua zSY@L{tsUPHq=H&*wCZ?vjCr?Z4XD$%NA_G#{tEqa_u5VSVFu{~W?sRxw&D1HOgTeML*6kRRih{pa{yMnn&gxOadpH|N^WBu@Pp&sC^P zhf*k@^>!5rKyA09azx%VB(z@JO$e0T7@!avid|=Ho_tnt+!iWtl$)5jNB;ar(Dp(D z=E@A>`R9C8h#@s0WG98t*mI7_L@gT4Z19x!f&y<Q0YDLv!j`BmL5HeF-m|&1o8J<&^$>b8)X}zN0W1nIt1#00w$48S(z?nX%vdg92 z1>i^@YU%T>9N((Hb9283WOg`ng(yt_DOTqYuYv@}miFyt`w_xc7|H-KIrx-oA4 zfF5$bT~1C7f<3K7jt9MH;5{=TNtPXK5VPBl1HfQSO)sy(!3Tv$kkME+z|X-wtSrDB zoTI7f?KM33sPF{x=b)(27@2?-0Vso=G;6%Ph6aBQyuC&SafRl{T&y<04?L+^^HTr= z>uO&0`YDh_9{gyXYPEo#VvK znqWkQ^OuVgO|P}k!F{e?|nY+?Y<98PUVPg3T&JWZ=KMUtRF`U2jbN>*j;_ zm0LY6f?~TCq012l!E)l?g&yhO=#LYFO_Oh%&+y@hGRb4I#%_11(EvMyLqA&hCXyL`TvL(8*&9ODz`C`%!IgDQTPjS|UP*F;< z-)kM)LvI#+KWhp1^{F3gUQwGp-$V$#y~g_%)~%NqBDxQHLQ~en-Xgj&iQA$p>a!P` zdUm>VhYOZpe*@uKQj1$|6$d-n=v9>FjgMOR`V?!`j^s>wn}4doQQXTcar9XDZ*;VD zkKVwxz%2Zm#a{}&<==GhQ~Wt|DWY`W<&7sAf+ByTxl{kxxwj_w*0Q_ce*)t2=Kd_+ zZf=Vq|0=jF;@7;Nmn<#W;D|u2mt*n{4^c^1{`UgsqiOfB6l^}`Rc2zNl~XokoAe7b zawdt!m1No+MV~78ai}p^Fg#-9XS~k1z_`N%X3}MHV@hViG1W7TGA%JnGn+8qWG1o* zu{g4nv%FvxW4+3XV;yDPU=v}}VT)zwVmDwyz^lLqz;Qta!IOe0L7dbPLoAdIQ=4 z^MYN#0pOe9G%yZa4}J*&Lu4U3kdqJ;#2*p`xeIv&c?M;G0-@4SZKx^K33>^d0bPdD zVEiyCm?q2w76Ds=QN*~!z+#GG`eGJhlVb11w#3=QMa1RBb;ZrZUBm;#N5vP!HzZgj zge0UTv?Z=f^h?Z2e3qa|@=Hoc`b$Phrb(_#(O3LSbx7^Nx!_Q^Cfp3}4v&XFg-^mM z(oktrX&33c(p&VQ`kujSoWYoZm7aWzg`U%dpFx!YMaS$6whYV+jG_1GzkdBkjD(t# zM=~i!Oiq%WZuT5@Nnad?R8l83ZPUx7hRPoM4~TjHF_U0W5vB^O z9DcezOHy!JjnL6I=Tj8E?fHS8eu7ZFF?cQ{G(Ud_4Rd+$U`elugW6A$amsVT7CGb* zPM!&hMl@I!Kr08i!b6C~NXn-G;h>hwq48qZ3SjxT;#o_2KS+-*(wVULbAj}Jc6WJI z2l0DDBxBvmXHb_S!ji+J>CsV!C>z--WMm5S8O5erAI(BM#p3mS*Pi)L_ero^lb;)_3N_^hI z1i|)6dbX{u+Ft8R;j9LXA|~wfW~D~*2Nd+?73M{RfV(V>&uA<;k{xG&c;`OBxGveP zTN&_<^3=10`o346T{YGWtgiKJJYXD_67rT7dW?U3g}zN}YU$L$oTTnY$LmkU%U%(H zz3$+iu(CPk%3TOASldsxW8L?GQZ^GIX(yD#aNH4 zVXqjcgR@_kOIFHhm#Ot+m`|}QAml2Xyx_QoE8$odrCi8spW`Zui7Ay+aPIDGdwn3H zvj0r5?iD^u`1o67uv8Whf3U{tNuJpm`Ly@-`J3qnr9j|F;dW_^*|W9HJ1L;FWk}JT zrXxXA_APPODf8jM(`+J}sVAH(z9fZ(tBJBqtKXL3?I?HHi*!nQw)m{QgCb+%|1q!a z%2t5YUfMVTb*w1$GM6u_^9+)fkKjCSS*YJgb8OU%?NdM>&4O2Ap|QH#L(dvfR@v zqCP92B<8MIDd)jx9FDG=d2RS%)QmBbA=LN8_5FFzK?igaBSREv@oI@RW~JHT6XK*V zy~?|im`_<6b-{B=0xM&KkjpT26@E-t%lu^WY5&89ZMpi9^XYAK9re(|w28;{sVs4K zrIn8X$^>LlI z=4wOI_srGDG)MXZrq7f80{X1nk6S&yXJmYw(U6Bc@JlR{>7$@ddfp1*72_Mcj4fK+1d z9{ocBKW!QoW^&P|MgqFvc|1D6$wE_Pp=Z70h%{K*qwD4e!?0Pu5Q~&e9Ks z{AFz~po%N)P{khVIfJ(2X0vmLjAsYrj9vPUN!X4#%Gg@(wCd}={T^Of5SNH2L4Fmo%GA>Z*L_@wogvZ>Jj(Y zuc7C8VrSck_#;}P$8=dt{9MVzHh!=$41ebiUiE2hU1s}yYYA{ndHhDb^Md=smN zQ?WW_7!m4O&3Da=BIf4iyng+9d;3eHNHiky*&2;#;p+kZBly1||1Wg^^}kO5RD#20 z`vn!Buc`O3*9z6WO@Gd3)SFXI{AetRNN|8>!?2NvYP|guFVQi7j|M{LmbEp@UwR`S zK-e07+VI0>itQD`FWXSiihOTqGW@#|)W`Q&k{Tiw8kU-1re^Pa<%S^$f{wMyh)iL^ zp&(BU=WJfi9Str7EX)^p6*AEtFwN4;ie?WexSkpB#?5bSY3{TT!oWB4K~QNgR6j`S z@j#cD71n!q6bvQ5&g4YtPx+liJJPvF51Cy^wQ4c( z$fbHW--q9)&#u-k#1*T6^NlK(>W|%YtOoz$iW+DNh)#N@Gq-FEg~f230$6j%@Ve=^ zn#XR94uy?DGa3p#L!%e}ULl-^=Td^@JEwcpYWzc^o~cU@Px~aNe8SY8)YH!qhE_ya z5D9GCZNONSLem%xV}7jbYp&hGEhco(^_r(3NNq;q3@JL6#VGBYyMB?lv+(Ae)LVjO z3K^|67-k7fI0f20B;qh2Du-F_N()VcX=l*s**a>KM+(`xN;s_asvYTo`{GW5m=o$9 zJ*>}@+Y*@Wz*EXI!g9dAu07q|0&>UR8xvd@osMUbXb& zn-5I~oefi`WhkJFQ-j#)BldF{7O@(WRf05EUY1ykQWg!xZBhWprv??wfe0P-_&k=9 zj=^a@mjW0(8sY@5;}SX=I#eIaN$kdfoF%!ko(Ggt_TuA^#>JXbhp6;Y(1%LxJ`t4{ zxdZ%!c^7Q+@#)}S!5hx7@x@Q=l3)W}C*c!EqD#_vn*;d!+Yl!aed8#MDHpGN?a3vr zoe>~9Z)U9{Ms0NICF*YOLT#L&KTPG3>O6-8jiG<+6ztvceF?J|$Z4)rU87sIzF#}g z`ORU8Q(Y&WX1z`hQl|GV$(vnrMFt{~fS*;s?@>SdX9vgX@D~!PbxRdQ>8%ayz~>+6 zQR<`8*hcuGF*m?R@zJ29P#+YeshDuWlU#8*#;LV8A%3uFwrKh8GEVUWxc$S zSjir`eS~hr?i9A~e%&IObwFzu7kTXGL=(p51(|%1inzV?Ve9(U^jB*i&(O$KH=YhN zGq4fau2S<0z50zh;NKKvzfpdsv}=dQe0;bA2*vIn_7}kB4kbdp4@| zbm9QXjy0&7nza3Edu0RNM9V`x3={PfhTr^&Kdv>fEbh#oM){s9CKynX8P0%od8+mg zxViNcHH5;ZrbJflBQWf+uVQ=iwPx%l!bcZ)W!&hI=V&DFe)M!hM{~PT?iO117d6%H zbW1(;00-l&a`>(hLLI`uL$H23sAW>}EG@}>T9z=cXr^aFy3At2Mt0v9pj&UqyfZJ- zG^4%5cw@BMaUEO8Rq~d1>P-M-W&pm~&na0+5smMob7_NuSxmv_KRuR^mNt1Ud#*m# z%^^fBVgxLiThV!Q&~CU2FnKfWTprda>I)|HXx+gXdYO(X);#F)qSl>T0k$%SEljyl zm?05YdwYfS{BVcx6VuOhB`5D@%bj98zoaFLL-E=T4=Gnq`N#C($uEQDj9p_Joxl~& zSSfPc7Y5jOGD1w zPX1PQ#KdzS(|tquOD5(vFGfoT1k(ujh(d!)&t1+D893W*Ct16Ngc{0+tGkV`Iuk4I z{E=Dv81f0Chp(_`?E`9eHJgj+2^(Mq%T9ZA!Mj)C{k~e>S;5pc8N6f5%=zyxF5a*1 z5jWV>1fPx?w-qO^p)-WaJQq#C`_T``ge2<&4av8|RhBB23U$;k1I^1wz$#U_+FZTN zD}xImjf$E3Tdn=4aE9q+Io2+OuxqcZjYhoL&fs(Qe=v!Bg~mX`jMK}=sYDv1`MSk@ z@$qKnG=8dI)QqO!+W^AV%1*5`$9rmlAs(j{hRvW;a`J8iJ^Xu$(t=Y}iGAd@^41hH z^Ou;<*(0<6^ix`_XtffJ(E%??UI#22%=xC=6>r?<&->hvbnpomksf}<FHYidn+`U2X=qZRq`VR#7O*Z%?iw2vkL literal 0 HcmV?d00001 diff --git a/webapps/docs/images/tomcat.png b/webapps/docs/images/tomcat.png new file mode 100644 index 0000000000000000000000000000000000000000..209b07fd8a8d2429b1a0c9740dc184f803ba3c67 GIT binary patch literal 5103 zcmW+)cRbYpA3sXSJ}V;~&R*GB@x`4Zdy|=YIAmsZly&yVIx9P5M@B?;2w5lLqBBn< zgv9UD?~l)Td>-%5=kt2JUeDL_`Fg%FhWc7GH`#B3Kp+}zgr+fY%>WJ?iW|T=CXc8C zTqu1JR{kIm7305y1cWWP1p+aaXlwpw`sB-=t*4iX8}{iqv=x3YfZ5qPIDwM3P`k8U z*kdYap+9wCnOpuQ4b_=sq157|R$ER8cb<_^1sReIdm|$wS6>N9^-5Zmlo8R#9fkUk zDz}`@K3mg9VRC~piN%B`jfEr7UC<4EZ}pqf@aB*8b*K3T=d7W({vj9dCd1BF=&1W4 zS5FQs%}fSg*Hk}JFQLy&97apsayFkcl@xgXU*9@;Z@kLZX3Prjad*gpVXC3v0Fz}M z(JB9mP(Z>&LLWi&+h-uv;zB4Yx1HH$G@a{pgR zc|;d~|4KskD*iCxCP?jjzCSV|$5`&;HH?(s^k_rgS8AcVG5wAjeX|P-B9uHpG{I-; zCs$RUiY;iEBo9WSgz2CwFaZ$Q-{(!B7bBL#ib*;t?2Qu(hpM8%tP`%FRHzMBTr zem8;~V>yK-JSan*e`Gc_H{XbIjusjP74I|*O{W_ZaY|9{ zUN`jPZcE6_|I3k}NM341BI|*-yUOO@E|$qP&8Z^Nf3?GT#dzT^A>jh`JCaav3n((( zz^R>`ay_%*>A1%|#jlgn#woaSMS&f2Mc=a>6bT-YKVcrv%I;}wsjZET3O6t&SD3C} z%T501z1fe>g5xx3;s?~$18{o%f*}w>TE`fx-^~v|6v{H2t{w|4oLsI$@Km7VRU=sD z=45@lJQHqhz||-Bg{+zf8vV}$Q6!{)N6|A1A}V#SoiF?8pczO$N+r-UJVH#L|7uUT zX&9)(T9$>9k(Cuvl4?uCME!Z z*`vY4-TBd;BIxROKiN0QdeM#VYHmKYm;M#x02RyG48XZ$qSej!AMlHGbIA*E(&?!r z^Ix(3D_3V0Ev-9{<>s}gem4;;V?0Z8XxoBGLFoS4`F4xYdnRxxyhkCf`q!pe6s9q! zc7Tg6t{;oTIC?`px<%-dC&mLwlq&&d;8#ERgKcWqYhq%BfA~9Q;p1N_t?2UtDXnwG zyfvX-4;C2~JBye&nD_488Okm#>glKCQ-gZLlNo|T!hO?_UhX69K{xf5HD97Z0D!u} z2QxmLe>f_96P1Y=Upt&z`UggIhr^)1OE7HLQ(*$p_s!9@_DXgtV)6~dHJ2k{u>?r2 zp7#xeTmh2A1OPV>Q~Rxs6wOEG=}&)#esxC*XGPSpEDy5&4390&5S;ZpG#yprmycuu z(BXc2cKMja`r%u5*BI1ZVVYZT1 zqnVN;j!qw;gadV;Ms@weoC* zCs@4TExoHYzcgz(RGN1tc&5^q8wCfoIyI<>#Tqm22!LB~O~LB^#YXNs{9&|EbvfBI z(QKHU1t#lwb=#dMtC#duimFNs)VA<<=+}4lN47?LnqQO$-hBK#NTq4t{}yQ3_#s#P zZ`poV5#5kt`ZPw2RZ=*6f1gv%z3R*zk9Cib><&UZ78g&kCLti8XI77ha~xIBqrEgv z7gtaO52kc8mI_g>h0aw<4XpGBx0Y1rpQ3&*2zAI;^{0#+*)H#9bV`AXll|B<%Z;vx zi;t=4A_gx10yISwu{dFK7Ar{8=RVh~ZsUd+pBDi&8Sm(I&fz1;N@vJ-g%nbAC1v`q zuJ%X^^l9v%>%p}SWj&v%f1fK>y}YFg{QkjGoGB(yY`?^|uq&LK3Lo40UJ5-6w^k~k zRWg@vyOnY9nL}CqSQTi{<_|bcisK&VD6_B=$FN7b^E zfhWXv$dh=gkjuP%e9&JC1E$r!?NP0tLs8!+Ak5*d9R*XfdM6R^jS|}A6{pED_YR|% zgdDx2bl~lwXZ`)UFT<){Qt;ue%mVL#kR9!GAWPuxeev#fNUhB5iCZSeIUq2y6_hWs zO%Z?hfGp@YDRC6$U~g%Awk=Zpo?H6k8vbOfj0pr#IPI?yj9aK)9y>iBxaA%L=fZ62 zp13_lRe!x@F$4Y2UEh^2G#ljLZsECCl|mIDzbs2K&j7nr4%__pabA~yQar>ztL1Z{ zZ0NpIxnVO8kh@hM@P+sYLa+GhE^g|C4d^akMdIDkKmW19r8ur(NaAU0_a#orHkL1a zV8su*-ZJ(*CLaE+Vv6gSU8yY1uTtj=oDtnaHD~_9W@0B}n6|WcAN#mp(5|Pi&OtZ+Q*{>l90S zdx?H>dNx(wIjfKZ9A5y+_)}$R@>@C1clR(?FsFV6vk7nNr{bhl#uy$Ki@G3+dR?o*ui=4LP7) zN|s@z)ctM(-ba<=`UaoYzxoQ@$VdtbA@U9xdnCOu1>sNe5CGLolz$JBK^c{NOHc!b zEm-YRFx=)2Rq>XQld%LO_1V?kIyjGFb#JH=t(rxlW~rn#Lg{wU zyOpC?<+K@}?_d1Bc)V(zqV)YeeB5PfSVKTOE8uQyrFZElI@YBmyVAHV7$F3duy&-i zTA2{aS>|MKsXK2|;_tW)$&jVmR&W|*8r|I`m27R_kx*dSKN$qg4G*#$rz z&9BvQ+PaN8K#JVnC(d*G)xE|Hh_?L>Ppk<G}ri%yR$UjeG1ri7nqK;r}>vy|i(acx=Mho7|rH}vEFXsOy z3?FLilscAM3Z&-}oGS?TbFM<0He?fB(kG7UlZ$Tvawr><_o&v7B)$_;%~8j~>7iG5 zz%SOQlaZ&TRq9nP;Id4v-!$wS?t52+K)-Cm@z|&QBj?r~L$S_HbHdPIEG+G*TSYdK zo`F@{CbXH0B`dsp!u(bHIQKkcn4E*gxg}Ci3_{W?(s2LPZuo&+j<5xMpSXV9u7D*U z`SeAUZ@zV#HmpDbb?38b?i}|1%d$6bukpfMJH@fK9I932rXm3J_7>kIm7x?@izoS4 zUhF{2k{fc$r9OvR@<=8UJTJ#j5E0>i0|60-$kfX)!kqpI>zAfhm@(a4sKwnQPVW2h zBN9c8CW430g{-zQYgWM2nv`6Gy>OG+%y|*%uCe4+0DI+XeQLb))A!}m{G;4kW`T0*~>n=NlfB7qm#NEk6wh)kk)F|>D+djHXLS=6E}I&DZJ5m+mnTk342!7 zYeox|O33AK;}XDnR8AR$yIS{bOd_ccG#|-Tsg-l2fnGM1^r>UpN&jAR_gogF;OW-g zCjHq^pjX4Frg2H$#d$*VTA#Wi{JVwe z)3G+UH$=A>t-TSY3EEXd0Ob;{1Wa+nQp#V!oGwj^A+?<3sw zQ^|EW5Jz@Lz&r*ltTI%#{jN){yO^8&DyNor;z_tG5An_171qHFe--0dHq4XR9G4bU z(jOGLB9GGamtcTK2NMGsFwa;K%!+l;!Cf|7L!OZ#jV*Q76+}QDv5|hT%~;P zxBWY{0m!zzURW^Wcb;4s`K!z?SeyE-FLmnCJgRuW7rYKlWvFAg9r;2pVJ_eyJ1SsT zxBi(nV;aC+xTcXukWSzyWvoSg7<~H=ZO!KYQf7%LA(pUuqWC3=NhN}t0v6ao!9S-4 zPMC@oPnlX90Q6oxFp#FQ!G@&U2V-5<(sKlVHe*qb_xld+8q44up@5(Pd!$E2dK7>> zy5KJrNG{U#yp`tuSnN6v4>aq$)+zggWI$|TyR9Pw)})Q!N>ZVHFrPl}Xc1z#szOOJ z%gP0QVE6I*(JAxHb8X4AEe;8$lApZNfGPi$^&vn$E7o>7&f6Xy~)WJ}4Db!S+p%Rsoq&C)J5@ z@O*ZVPhLUv2RD%tlB)06gW=sRw!!Iqv3xe&47H3In9T9cAiet7+rjaztV-jTIu@^H zM4?Q2hZbj8ntv-kvbr1&oGxKq)dP0PgiFr!tZi1eN1Yc_vfu5RFFZjY|$Ah z0~IbiuENaU!s$6wl%qgsuOmc);iRS_H=`ZI>7R$8&Z`hBPfR*@LD1ykpJSkqC~^XW z&)y9U+v2aiPrBbQ(eSrWXV6~`JH?y~1)?HuQI{k~$H(j|n2N?GtHxo44Y--MXCc(lVJQ#V>Ki4Inm6k2h0hJ3G;z_WEwMwsg@F%Py4cxlysBLV2jn)F~`H(_zj zTdoe^8Y#t-7{b`7_G!dK0T~z|XZ6iLzs#hWw$+k{TpL{jZQ-%~HIJZ*KLzL5)obP! zQ*I$n>jttrb@UX%{}YW-YfCfyXc!O(m;wJ2UaK8p*k}eHo0~9I|6L&s$bhTGjGEL8 zuERziyhlm06EnmPLk(USXQhP+pp^4KSuu{c77Nl1aaWDR*C;u)e>3v0z!%19>BM`h zb-(%uzh&0}j~m#HX6gOZoc=Xbk{$8;hxiC4w$?_L+p#{jcxrG=mdLiwZn4h0tkeBaZ zXTN^^`uFeOe|Y!)!-o&&&!6AFfB%LJ8x|~BprD}e|KI zixKbaBa2L3t=6qucmCpq-3N9pT)$}Y+DUZ_>hmV#r_`iKsYvtb2q@R8MedK$FEQlQ z=a5vD+;`!~rt>?OoLxQp!m`Qd7Budkz+leWabaTVo#u#BStWN`r`%g8s3p4R;O>R% zmP}nYvu;6q?xf=E&O#{_89^-(t#bYP8*L_qrri462hSf`e_+E#pd0tCUvqFBkFL=A zvs*7*xUgy6+SLo^E6dAWI0y8^MUY>q3UnBN02CJt>^B-1nwl7!o0*u}x|& + + +]> + + + &project; + + + Craig R. McClanahan + Remy Maucherat + Yoav Shapira + Documentation Index + + + + +

    + +

    This is the top-level entry point of the documentation bundle for the +Apache Tomcat Servlet/JSP container. Apache Tomcat version + implements the Servlet 6.0 and JavaServer Pages 3.1 +specifications from +Jakarta EE, and includes many +additional features that make it a useful platform for developing and deploying +web applications and web services.

    + +

    Select one of the links from the navigation menu (to the left) to drill +down to the more detailed documentation that is available. Each available +manual is described in more detail below.

    + +
    + + +
    + +

    The following documents will assist you in downloading and installing +Apache Tomcat, and using many of the Apache Tomcat features.

    + +
      +
    1. Introduction - A + brief, high level, overview of Apache Tomcat.
    2. +
    3. Setup - How to install and run + Apache Tomcat on a variety of platforms.
    4. +
    5. First web application + - An introduction to the concepts of a web application as defined + in the Servlet Specification. Covers basic organization of your web application + source tree, the structure of a web application archive, and an + introduction to the web application deployment descriptor + (/WEB-INF/web.xml).
    6. +
    7. Deployer - + Operating the Apache Tomcat Deployer to deploy, precompile, and validate web + applications.
    8. +
    9. Manager - + Operating the Manager web app to deploy, undeploy, and + redeploy applications while Apache Tomcat is running.
    10. +
    11. Host Manager - + Operating the Host Manager web app to add and remove + virtual hosts while Apache Tomcat is running.
    12. +
    13. Realms and Access Control + - Description of how to configure Realms (databases of users, + passwords, and their associated roles) for use in web applications that + utilize Container Managed Security.
    14. +
    15. Security Manager + - Configuring and using a Java Security Manager to + support fine-grained control over the behavior of your web applications. +
    16. +
    17. JNDI Resources + - Configuring standard and custom resources in the JNDI naming context + that is provided to each web application.
    18. +
    19. + JDBC DataSource + - Configuring a JNDI DataSource with a DB connection pool. + Examples for many popular databases.
    20. +
    21. Classloading + - Information about class loading in Apache Tomcat, including where to place + your application classes so that they are visible.
    22. +
    23. JSPs + - Information about Jasper configuration, as well as the JSP compiler + usage.
    24. +
    25. SSL/TLS - + Installing and configuring SSL/TLS support so that your Apache Tomcat will + serve requests using the https protocol.
    26. +
    27. SSI - + Using Server Side Includes in Apache Tomcat.
    28. +
    29. CGI - + Using CGIs with Apache Tomcat.
    30. +
    31. Proxy Support - + Configuring Apache Tomcat to run behind a proxy server (or a web server + functioning as a proxy server).
    32. +
    33. MBeans Descriptors - + Configuring MBean descriptors files for custom components.
    34. +
    35. Default Servlet - + Configuring the default servlet and customizing directory listings.
    36. +
    37. Apache Tomcat Clustering - + Enable session replication in a Apache Tomcat environment.
    38. +
    39. Balancer - + Configuring, using, and extending the load balancer application.
    40. +
    41. Connectors - + Connectors available in Apache Tomcat, and native web server integration.
    42. +
    43. Monitoring and Management - + Enabling JMX Remote support, and using tools to monitor and manage Apache Tomcat.
    44. +
    45. Logging - + Configuring logging in Apache Tomcat.
    46. +
    47. Apache Portable Runtime - + Using APR to provide access to OpenSSL for TLS support.
    48. +
    49. Virtual Hosting - + Configuring virtual hosting in Apache Tomcat.
    50. +
    51. Advanced IO - + Extensions available over regular, blocking IO.
    52. +
    53. Using Tomcat libraries with Maven - + Obtaining Tomcat jars through Maven.
    54. +
    55. Security Considerations - + Options to consider when securing an Apache Tomcat installation.
    56. +
    57. Windows Service - + Running Tomcat as a service on Microsoft Windows.
    58. +
    59. Windows Authentication - + Configuring Tomcat to use integrated Windows authentication.
    60. +
    61. High Concurrency JDBC Pool - + Configuring Tomcat to use an alternative JDBC pool.
    62. +
    63. WebSocket support - + Developing WebSocket applications for Apache Tomcat.
    64. +
    65. URL rewrite - + Using the regexp based rewrite valve for conditional URL and host rewrite.
    66. +
    67. CDI and JAX-RS support - + Configuring CDI,JAX-RS and Eclipse Microprofile support.
    68. +
    69. AOT compilation support - + Ahead of Time compilation support with GraalVM/Native Image.
    70. +
    + +
    + + +
    + +

    The following documents are aimed at System Administrators who +are responsible for installing, configuring, and operating an Apache Tomcat server. +

    + + +
    + + +
    + +

    The following documents are for Java developers who wish to contribute to +the development of the Apache Tomcat project.

    +
      +
    • Building from Source - + Details the steps necessary to download Apache Tomcat source code (and the + other packages that it depends on), and build a binary distribution from + those sources. +
    • +
    • Changelog - Details the + changes made to Apache Tomcat. +
    • +
    • Status - + Apache Tomcat development status. +
    • +
    • Developers - List of active + Apache Tomcat contributors. +
    • +
    • Javadocs + - Javadoc API documentation for Apache Tomcat's internals.
    • +
    • Apache Tomcat Architecture + - Documentation of the Apache Tomcat Server Architecture.
    • +
    + +
    + + + + diff --git a/webapps/docs/introduction.xml b/webapps/docs/introduction.xml new file mode 100644 index 0000000..5b06ffc --- /dev/null +++ b/webapps/docs/introduction.xml @@ -0,0 +1,304 @@ + + + +]> + + + &project; + + + Robert Slifka + Introduction + + + + +
    + +
    + +
    + +

    For administrators and web developers alike, there are some important bits +of information you should familiarize yourself with before starting out. This +document serves as a brief introduction to some of the concepts and +terminology behind the Tomcat container. As well, where to go when you need +help.

    + +
    + + +
    + +

    In the course of reading these documents, you will run across a number of +terms; some specific to Tomcat, and others defined by the +Servlet and +JSP specifications.

    + +
      +
    • Context - In a nutshell, a Context is a + web application.
    • +
    +

    That is it. If you find any more terms we need to add to this section, please +do let us know.

    + +
    + + +
    + +

    These are some of the key tomcat directories:

    + +
      +
    • /bin - Startup, shutdown, and other scripts. The + *.sh files (for Unix systems) are functional duplicates of + the *.bat files (for Windows systems). Since the Win32 + command-line lacks certain functionality, there are some additional + files in here.
    • +
    • /conf - Configuration files and related DTDs. The most + important file in here is server.xml. It is the main configuration file + for the container.
    • +
    • /logs - Log files are here by default.
    • +
    • /webapps - This is where your webapps go.
    • +
    + +
    + +
    +

    Throughout the documentation, there are references to the two following + properties: +

      +
    • + CATALINA_HOME: Represents the root of your Tomcat + installation, for example /home/tomcat/apache-tomcat-9.0.10 + or C:\Program Files\apache-tomcat-9.0.10. +
    • +
    • + CATALINA_BASE: Represents the root of a runtime + configuration of a specific Tomcat instance. If you want to have + multiple Tomcat instances on one machine, use the CATALINA_BASE + property. +
    • +
    +

    +

    + If you set the properties to different locations, the CATALINA_HOME location + contains static sources, such as .jar files, or binary files. + The CATALINA_BASE location contains configuration files, log files, deployed + applications, and other runtime requirements. +

    + +

    + By default, CATALINA_HOME and CATALINA_BASE point to the same directory. + Set CATALINA_BASE manually when you require running multiple Tomcat + instances on one machine. Doing so provides the following benefits: +

    +
      +
    • + Easier management of upgrading to a newer version of Tomcat. Because all + instances with single CATALINA_HOME location share one set of + .jar files and binary files, you can easily upgrade the files + to newer version and have the change propagated to all Tomcat instances + using the same CATALIA_HOME directory. +
    • +
    • + Avoiding duplication of the same static .jar files. +
    • +
    • + The possibility to share certain settings, for example the setenv shell + or bat script file (depending on your operating system). +
    • +
    +
    + +

    + Before you start using CATALINA_BASE, first consider and create the + directory tree used by CATALINA_BASE. Note that if you do not create + all the recommended directories, Tomcat creates the directories + automatically. If it fails to create the necessary directory, for example + due to permission issues, Tomcat will either fail to start, or may not + function correctly. +

    +

    + Consider the following list of directories: +

      +
    • +

      + The bin directory with the setenv.sh, + setenv.bat, and tomcat-juli.jar files. +

      +

      + Recommended: No. +

      +

      + Order of lookup: CATALINA_BASE is checked first; fallback is provided + to CATALINA_HOME. +

      +
    • +
    • +

      + The lib directory with further resources to be added on + classpath. +

      +

      + Recommended: Yes, if your application depends on external libraries. +

      +

      + Order of lookup: CATALINA_BASE is checked first; CATALINA_HOME is + loaded second. +

      +
    • +
    • +

      + The logs directory for instance-specific log files. +

      +

      + Recommended: Yes. +

      +
    • +
    • +

      + The webapps directory for automatically loaded web + applications. +

      +

      + Recommended: Yes, if you want to deploy applications. +

      +

      + Order of lookup: CATALINA_BASE only. +

      +
    • +
    • +

      + The work directory that contains temporary working + directories for the deployed web applications. +

      +

      + Recommended: Yes. +

      +
    • +
    • +

      + The temp directory used by the JVM for temporary files. +

      +

      + Recommended: Yes. +

      +
    • +
    +

    +

    + We recommend you not to change the tomcat-juli.jar file. + However, in case you require your own logging implementation, you can + replace the tomcat-juli.jar file in a CATALINA_BASE location + for the specific Tomcat instance. +

    +

    + We also recommend you copy all configuration files from the + CATALINA_HOME/conf directory into the + CATALINA_BASE/conf/ directory. In case a configuration file + is missing in CATALINA_BASE, there is no fallback to CATALINA_HOME. + Consequently, this may cause failure. +

    +

    + At minimum, CATALINA_BASE must contain: +

      +
    • conf/server.xml
    • +
    • conf/web.xml
    • +
    + That includes the conf directory. Otherwise, Tomcat fails + to start, or fails to function properly. +

    +

    + For advanced configuration information, see the + + RUNNING.txt + file. +

    +
    + +

    + The CATALINA_BASE property is an environment variable. You can set it + before you execute the Tomcat start script, for example: +

      +
    • On Unix: CATALINA_BASE=/tmp/tomcat_base1 bin/catalina.sh start
    • +
    • On Windows: CATALINA_BASE=C:\tomcat_base1 bin/catalina.bat start
    • +
    +

    +
    +
    + + +
    + +

    This section will acquaint you with the basic information used during +the configuration of the container.

    + +

    All of the information in the configuration files is read at startup, +meaning that any change to the files necessitates a restart of the container. +

    + +
    + + +
    + +

    While we've done our best to ensure that these documents are clearly +written and easy to understand, we may have missed something. Provided +below are various web sites and mailing lists in case you get stuck.

    + +

    Keep in mind that some of the issues and solutions vary between the +major versions of Tomcat. As you search around the web, there will be +some documentation that is not relevant to Tomcat , but +only to earlier versions.

    + +
      +
    • Current document - most documents will list potential hangups. Be sure + to fully read the relevant documentation as it will save you much time + and effort. There's nothing like scouring the web only to find out that + the answer was right in front of you all along!
    • +
    • Tomcat FAQ
    • +
    • Tomcat WIKI
    • +
    • Tomcat FAQ at jGuru
    • +
    • Tomcat mailing list archives - numerous sites archive the Tomcat mailing + lists. Since the links change over time, clicking here will search + Google. +
    • +
    • The TOMCAT-USER mailing list, which you can subscribe to + here. If you don't + get a reply, then there's a good chance that your question was probably + answered in the list archives or one of the FAQs. Although questions + about web application development in general are sometimes asked and + answered, please focus your questions on Tomcat-specific issues.
    • +
    • The TOMCAT-DEV mailing list, which you can subscribe to + here. This list is + reserved for discussions about the development of Tomcat + itself. Questions about Tomcat configuration, and the problems you run + into while developing and running applications, will normally be more + appropriate on the TOMCAT-USER list instead.
    • +
    + +

    And, if you think something should be in the docs, by all means let us know +on the TOMCAT-DEV list.

    + +
    + + + +
    diff --git a/webapps/docs/jasper-howto.xml b/webapps/docs/jasper-howto.xml new file mode 100644 index 0000000..d027ffb --- /dev/null +++ b/webapps/docs/jasper-howto.xml @@ -0,0 +1,520 @@ + + + +]> + + + &project; + + + Glenn L. Nielsen + Peter Rossbach + Jasper 2 JSP Engine How To + + + + +
    + +
    + +
    + +

    Tomcat uses the Jasper 2 JSP Engine to implement +the JavaServer Pages 2.3 +specification.

    + +

    Jasper 2 has been redesigned to significantly improve performance over +the original Jasper. In addition to general code improvements the following +changes were made:

    +
      +
    • JSP Custom Tag Pooling - The java objects instantiated +for JSP Custom Tags can now be pooled and reused. This significantly boosts +the performance of JSP pages which use custom tags.
    • +
    • Background JSP compilation - If you make a change to +a JSP page which had already been compiled Jasper 2 can recompile that +page in the background. The previously compiled JSP page will still be +available to serve requests. Once the new page has been compiled +successfully it will replace the old page. This helps improve availability +of your JSP pages on a production server.
    • +
    • Recompile JSP when included page changes - Jasper 2 +can now detect when a page included at compile time from a JSP has changed +and then recompile the parent JSP.
    • +
    • JDT used to compile JSP pages - The +Eclipse JDT Java compiler is now used to perform JSP java source code +compilation. This compiler loads source dependencies from the container +classloader. Ant and javac can still be used.
    • +
    + + +

    Jasper is implemented using the servlet class +org.apache.jasper.servlet.JspServlet.

    + +
    + +
    + +

    By default Jasper is configured for use when doing web application +development. See the section +Production Configuration for information on configuring Jasper +for use on a production Tomcat server.

    + +

    The servlet which implements Jasper is configured using init parameters +in your global $CATALINA_BASE/conf/web.xml. +

    +
      +
    • checkInterval - If development is false and checkInterval +is greater than zero, background compiles are enabled. checkInterval is the time +in seconds between checks to see if a JSP page (and its dependent files) needs +to be recompiled. Default 0 seconds.
    • + +
    • classdebuginfo - Should the class file be compiled with +debugging information? true or false, default +true. +
    • + +
    • classpath - Defines the class path to be used to compile +the generated servlets. This parameter only has an effect if the ServletContext +attribute org.apache.jasper.Constants.SERVLET_CLASSPATH is not set. This +attribute is always set when Jasper is used within Tomcat. By default the +classpath is created dynamically based on the current web application.
    • + +
    • compiler - Which compiler Ant should use to compile JSP +pages. The valid values for this are the same as for the compiler attribute of +Ant's +javac +task. If the value is not set, then the default Eclipse JDT Java compiler will +be used instead of using Ant. There is no default value. If this attribute is +set then setenv.[sh|bat] should be used to add +ant.jar, ant-launcher.jar and tools.jar +to the CLASSPATH environment variable.
    • + +
    • compilerSourceVM - What JDK version are the source files +compatible with? (Default value: 11)
    • + +
    • compilerTargetVM - What JDK version are the generated files +compatible with? (Default value: 11)
    • + +
    • development - Is Jasper used in development mode? If true, +the frequency at which JSPs are checked for modification may be specified via +the modificationTestInterval parameter.true or false, +default true.
    • + +
    • displaySourceFragment - Should a source fragment be +included in exception messages? true or false, +default true.
    • + +
    • dumpSmap - Should the SMAP info for JSR45 debugging be +dumped to a file? true or false, default +false. false if suppressSmap is true.
    • + +
    • enablePooling - Determines whether tag handler pooling is +enabled. This is a compilation option. It will not alter the behaviour of JSPs +that have already been compiled. true or false, +default true. +
    • + +
    • engineOptionsClass - Allows specifying the Options class +used to configure Jasper. If not present, the default EmbeddedServletOptions +will be used. This option is ignored if running under a SecurityManager. +
    • + +
    • errorOnUseBeanInvalidClassAttribute - Should Jasper issue +an error when the value of the class attribute in an useBean action is not a +valid bean class? true or false, default +true.
    • + +
    • fork - Have Ant fork JSP page compiles so they are +performed in a separate JVM from Tomcat? true or +false, default true.
    • + +
    • genStringAsCharArray - Should text strings be generated as char +arrays, to improve performance in some cases? Default false.
    • + +
    • javaEncoding - Java file encoding to use for generating +java source files. Default UTF8.
    • + +
    • keepgenerated - Should we keep the generated Java source +code for each page instead of deleting it? true or +false, default true.
    • + +
    • mappedfile - Should we generate static content with one +print statement per input line, to ease debugging? +true or false, default true.
    • + +
    • maxLoadedJsps - The maximum number of JSPs that will be +loaded for a web application. If more than this number of JSPs are loaded, the +least recently used JSPs will be unloaded so that the number of JSPs loaded at +any one time does not exceed this limit. A value of zero or less indicates no +limit. Default -1
    • + +
    • jspIdleTimeout - The amount of time in seconds a JSP can be +idle before it is unloaded. A value of zero or less indicates never unload. +Default -1
    • + +
    • modificationTestInterval - Causes a JSP (and its dependent +files) to not be checked for modification during the specified time interval +(in seconds) from the last time the JSP was checked for modification. A value of +0 will cause the JSP to be checked on every access. Used in development mode +only. Default is 4 seconds.
    • + +
    • recompileOnFail - If a JSP compilation fails should the +modificationTestInterval be ignored and the next access trigger a re-compilation +attempt? Used in development mode only and is disabled by default as compilation +may be expensive and could lead to excessive resource usage.
    • + +
    • scratchdir - What scratch directory should we use when +compiling JSP pages? Default is the work directory for the current web +application. This option is ignored if running under a SecurityManager.
    • + +
    • suppressSmap - Should the generation of SMAP info for JSR45 +debugging be suppressed? true or false, default +false.
    • + +
    • trimSpaces - Should template text that consists entirely of +whitespace be removed from the output (true), replaced with a +single space (single) or left unchanged (false)? +Alternatively, the extended option will remove leading and trailing +whitespace from template text and collapse sequences of whitespace and newlines +within the template text to a single new line. Note that if a JSP page or tag +file specifies a trimDirectiveWhitespaces value of +true, that will take precedence over this configuration setting for +that page/tag. Default false.
    • + +
    • xpoweredBy - Determines whether X-Powered-By response +header is added by generated servlet. true or false, +default false.
    • + +
    • strictQuoteEscaping - When scriptlet expressions are used +for attribute values, should the rules in JSP.1.6 for the escaping of quote +characters be strictly applied? true or false, default +true.
    • + +
    • quoteAttributeEL - When EL is used in an attribute value +on a JSP page, should the rules for quoting of attributes described in JSP.1.6 +be applied to the expression? true or false, default +true.
    • + +
    • variableForExpressionFactory - The name of the variable + to use for the expression language expression factory. If not specified, the + default value of _el_expressionfactory will be used.
    • + +
    • variableForInstanceManager - The name of the variable + to use for the instance manager factory. If not specified, the default value + of _jsp_instancemanager will be used.
    • + +
    • poolTagsWithExtends - By default, JSPs that use + their own base class via the extends + attribute of the page directive, will have Tag pooling disabled since + Jasper cannot guarantee that the necessary initialisation will have taken + place. This can have a negative impact on performance. Providing the + alternative base class calls _jspInit() from Servlet.init(), setting this + property to true will enable pooling with an alternative base + class. If the alternative base class does not call _jspInit() and this + property is true, NPEs will occur when attempting to use + tags. true or false, default + false.
    • + +
    • strictGetProperty - If true, the requirement + to have the object referenced in + jsp:getProperty action to be previously "introduced" + to the JSP processor, as specified in the chapter JSP.5.3 of JSP 2.0 and + later specifications, is enforced. true or false, default + true.
    • + +
    • strictWhitespace - If false the requirements + for whitespace before an + attribute name will be relaxed so that the lack of whitespace will not + cause an error. true or false, default + true.
    • + +
    • jspServletBase - The base class of the Servlets generated + from the JSPs.If not specified, the default value of + org.apache.jasper.runtime.HttpJspBase will be used.
    • + +
    • serviceMethodName - The name of the service method + called by the base class. If not specified, the default value of + _jspService will be used.
    • + +
    • servletClasspathAttribute - The name of the ServletContext + attribute that provides the classpath for the JSP. If not specified, the default + value of org.apache.catalina.jsp_classpath will be used.
    • + +
    • jspPrecompilationQueryParameter - The name of the query + parameter that causes the JSP engine to just pregenerate the servlet but + not invoke it. If not specified, the default value of + jsp_precompile will be used, as defined by JSP specification + (JSP.11.4.2).
    • + +
    • generatedJspPackageName - The default package name + for compiled JSPs. If not specified, the default value of + org.apache.jsp will be used.
    • + +
    • generatedTagFilePackageName - The default package name + for tag handlers generated from tag files. If not specified, the default + value of org.apache.jsp.tag will be used.
    • + +
    • tempVariableNamePrefix - Prefix to use for generated + temporary variable names. If not specified, the default value of + _jspx_temp will be used.
    • + +
    • useInstanceManagerForTags - If true, + the instance manager is used to obtain tag handler instances. + true or false, default false.
    • + +
    • limitBodyContentBuffer - If true, any + tag buffer that expands beyond the value of the + bodyContentTagBufferSize init parameter will be + destroyed and a new buffer created. + true or false, default false.
    • + +
    • bodyContentTagBufferSize - The size (in characters) + to use when creating a tag buffer. If not specified, the default value of + org.apache.jasper.Constants.DEFAULT_TAG_BUFFER_SIZE (512) + will be used.
    • + +
    + +

    The Java compiler from Eclipse JDT in included as the default compiler. It is +an advanced Java compiler which will load all dependencies from the Tomcat class +loader, which will help tremendously when compiling on large installations with +tens of JARs. On fast servers, this will allow sub-second recompilation cycles +for even large JSP pages.

    + +

    Apache Ant, which was used in previous Tomcat releases, can be used instead +of the new compiler by configuring the compiler attribute as explained above. +

    + +

    If you need to change the JSP Servlet settings for an application you can +override the default configuration by re-defining the JSP Servlet in +/WEB-INF/web.xml. However, this may cause problems if you attempt +to deploy the application on another container as the JSP Servlet class may +not be recognised. You can work-around this problem by using the Tomcat specific +/WEB-INF/tomcat-web.xml deployment descriptor. The format is +identical to /WEB-INF/web.xml. It will override any default +settings but not those in /WEB-INF/web.xml. Since it is Tomcat +specific, it will only be processed when the application is deployed on +Tomcat.

    + +
    + +
    + +

    As described in + +bug 39089, a known JVM issue, + +bug 6294277, may cause a +java.lang.InternalError: name is too long to represent exception +when compiling very large JSPs. If this is observed then it may be worked around +by using one of the following: +

    +
      +
    • reduce the size of the JSP
    • +
    • disable SMAP generation and JSR-045 support by setting +suppressSmap to true.
    • +
    + +
    + +
    + +

    The main JSP optimization which can be done is precompilation of JSPs. +However, this might not be possible (for example, when using the +jsp-property-group feature) or practical, in which case the configuration of the +Jasper servlet becomes critical.

    + +

    When using Jasper 2 in a production Tomcat server you should consider making +the following changes from the default configuration.

    +
      +
    • development - To disable on access checks for JSP +pages compilation set this to false.
    • +
    • genStringAsCharArray - To generate slightly more efficient +char arrays, set this to true.
    • +
    • modificationTestInterval - If development has to be set to +true for any reason (such as dynamic generation of JSPs), setting +this to a high value will improve performance a lot.
    • +
    • trimSpaces - To remove unnecessary bytes from the response, +consider setting this to single or extended.
    • +
    + +
    + +
    + +

    Using Ant is the preferred way to compile web applications using JSPC. Note +that when pre-compiling JSPs, SMAP information will only be included in the +final classes if suppressSmap is false and compile is true. +Use the script given below (a similar script is included in the "deployer" +download) to precompile a webapp: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + +

    +The following command line can be used to run the script +(replacing the tokens with the Tomcat base path and the path to the webapp +which should be precompiled): +

    +$ANT_HOME/bin/ant -Dtomcat.home=<$TOMCAT_HOME> -Dwebapp.path=<$WEBAPP_PATH> + + +

    +Then, the declarations and mappings for the servlets which were generated +during the precompilation must be added to the web application deployment +descriptor. Insert the ${webapp.path}/WEB-INF/generated_web.xml +at the right place inside the ${webapp.path}/WEB-INF/web.xml file. +Restart the web application (using the manager) and test it to verify it is +running fine with precompiled servlets. An appropriate token placed in the +web application deployment descriptor may also be used to automatically +insert the generated servlet declarations and mappings using Ant filtering +capabilities. This is actually how all the webapps distributed with Tomcat +are automatically compiled as part of the build process. +

    + +

    +At the jasper task you can use the option addWebXmlMappings for +automatic merge the ${webapp.path}/WEB-INF/generated_web.xml +with the current web application deployment descriptor at +${webapp.path}/WEB-INF/web.xml. +

    + +

    +When you want to use a specific version of Java for your JSP's, add the +javac compiler task attributes source and target with +appropriate values. For example, 16 to compile JSPs for Java 16. +

    + +

    +For production you may wish to disable debug info with +debug="off". +

    + +

    +When you don't want to stop the JSP generation at first JSP syntax error, use +failOnError="false" and with +showSuccess="true" all successful JSP to Java +generation are printed out. Sometimes it is very helpful, when you cleanup the +generate java source files at ${webapp.path}/WEB-INF/src +and the compile JSP servlet classes at +${webapp.path}/WEB-INF/classes/org/apache/jsp. +

    + +

    Hints:

    +
      +
    • When you switch to another Tomcat release, then regenerate and recompile +your JSP's with the new Tomcat version.
    • +
    • Use a Servlet context parameter to disable PageContext pooling +org.apache.jasper.runtime.JspFactoryImpl.POOL_SIZE=-1 +and limit the buffering with the JSP Servlet init-param +limitBodyContentBuffer=true. Note +that changing from the defaults may affect performance, but it will vary +depending on the application.
    • +
    +
    + +
    +

    +There are a number of extension points provided within Jasper that enable the +user to optimise the behaviour for their environment. +

    + +

    +The first of these extension points is the tag plug-in mechanism. This allows +alternative implementations of tag handlers to be provided for a web application +to use. Tag plug-ins are registered via a tagPlugins.xml file +located under WEB-INF. A sample plug-in for the JSTL is included +with Jasper. +

    + +

    +The second extension point is the Expression Language interpreter. Alternative +interpreters may be configured through the ServletContext. See the +ELInterpreterFactory javadoc for details of how to configure an +alternative EL interpreter. A alternative interpreter primarily targeting tag +settings is provided at +org.apache.jasper.optimizations.ELInterpreterTagSetters. See the +javadoc for details of the optimisations and the impact they have on +specification compliance. +

    + +

    +An extension point is also provided for coercion of String values to Enums. It +is provided at +org.apache.jasper.optimizations.StringInterpreterEnum. See the +javadoc for details of the optimisations and the impact they have on +specification compliance. +

    +
    + + + +
    diff --git a/webapps/docs/jaspicapi/index.html b/webapps/docs/jaspicapi/index.html new file mode 100644 index 0000000..e4f31a2 --- /dev/null +++ b/webapps/docs/jaspicapi/index.html @@ -0,0 +1,34 @@ + + + + + + API docs + + + + +The JASPIC Javadoc is not installed by default. Download and install +the "fulldocs" package to get it. + +You can also access the javadoc online in the Tomcat + + documentation bundle. + + + diff --git a/webapps/docs/jndi-datasource-examples-howto.xml b/webapps/docs/jndi-datasource-examples-howto.xml new file mode 100644 index 0000000..9815435 --- /dev/null +++ b/webapps/docs/jndi-datasource-examples-howto.xml @@ -0,0 +1,682 @@ + + + +]> + + + &project; + + + Les Hughes + David Haraburda + Glenn Nielsen + Yoav Shapira + JNDI Datasource How-To + + + + +
    + +
    + +
    + +

    JNDI Datasource configuration is covered extensively in the +JNDI-Resources-HOWTO. However, feedback from tomcat-user has +shown that specifics for individual configurations can be rather tricky.

    + +

    Here then are some example configurations that have been posted to +tomcat-user for popular databases and some general tips for db usage.

    + +

    You should be aware that since these notes are derived from configuration +and/or feedback posted to tomcat-user YMMV :-). Please let us +know if you have any other tested configurations that you feel may be of use +to the wider audience, or if you feel we can improve this section in anyway.

    + +

    +Please note that JNDI resource configuration changed somewhat between +Tomcat 7.x and Tomcat 8.x as they are using different versions of +Apache Commons DBCP library. You will most likely need to modify older +JNDI resource configurations to match the syntax in the example below in order +to make them work in Tomcat . +See Tomcat Migration Guide +for details. +

    + +

    +Also, please note that JNDI DataSource configuration in general, and this +tutorial in particular, assumes that you have read and understood the +Context and +Host configuration references, including +the section about Automatic Application Deployment in the latter reference. +

    +
    + +
    + +

    java.sql.DriverManager supports the +service +provider mechanism. This feature is that all the available JDBC drivers +that announce themselves by providing a META-INF/services/java.sql.Driver +file are automatically discovered, loaded and registered, +relieving you from the need to load the database driver explicitly before +you create a JDBC connection. +However, the implementation is fundamentally broken in all Java versions for +a servlet container environment. The problem is that +java.sql.DriverManager will scan for the drivers only once.

    + +

    The JRE Memory Leak Prevention Listener +that is included with Apache Tomcat solves this by triggering the driver scan +during Tomcat startup. This is enabled by default. It means that only +libraries visible to the common class loader and its parents will be scanned for +database drivers. This include drivers in $CATALINA_HOME/lib, +$CATALINA_BASE/lib, the class path and the module path. Drivers +packaged in web applications (in WEB-INF/lib) and in the shared +class loader (where configured) will not be visible and will not be loaded +automatically. If you are considering disabling this feature, note that the scan +would be triggered by the first web application that is using JDBC, leading to +failures when this web application is reloaded and for other web applications +that rely on this feature. +

    + +

    Thus, the web applications that have database drivers in their +WEB-INF/lib directory cannot rely on the service provider +mechanism and should register the drivers explicitly.

    + +

    The list of drivers in java.sql.DriverManager is also +a known source of memory leaks. Any Drivers registered +by a web application must be deregistered when the web application stops. +Tomcat will attempt to automatically discover and deregister any +JDBC drivers loaded by the web application class loader when the web +application stops. +However, it is expected that applications do this for themselves via +a ServletContextListener. +

    + +
    + +
    + +

    The default database connection pool implementation in Apache Tomcat +relies on the libraries from the +Apache Commons project. +The following libraries are used: +

    + +
      +
    • Commons DBCP 2
    • +
    • Commons Pool 2
    • +
    + +

    +These libraries are located in a single JAR at +$CATALINA_HOME/lib/tomcat-dbcp.jar. However, +only the classes needed for connection pooling have been included, and the +packages have been renamed to avoid interfering with applications. +

    + +

    DBCP 2 provides support for JDBC 4.1.

    + + + +

    See the +DBCP 2 documentation for a complete list of configuration parameters. +

    + +
    + + + +

    +A database connection pool creates and manages a pool of connections +to a database. Recycling and reusing already existing connections +to a database is more efficient than opening a new connection. +

    + +

    +There is one problem with connection pooling. A web application has +to explicitly close ResultSet's, Statement's, and Connection's. +Failure of a web application to close these resources can result in +them never being available again for reuse, a database connection pool "leak". +This can eventually result in your web application database connections failing +if there are no more available connections.

    + +

    +There is a solution to this problem. The Apache Commons DBCP 2 can be +configured to track and recover these abandoned database connections. Not +only can it recover them, but also generate a stack trace for the code +which opened these resources and never closed them.

    + +

    +To configure a DBCP 2 DataSource so that abandoned database connections are +removed and recycled, add one or both of the following attributes to the +Resource configuration for your DBCP 2 DataSource: +

    +removeAbandonedOnBorrow=true +removeAbandonedOnMaintenance=true +

    The default for both of these attributes is false. Note that +removeAbandonedOnMaintenance has no effect unless pool +maintenance is enabled by setting timeBetweenEvictionRunsMillis +to a positive value. See the + +DBCP 2 documentation for full documentation on these attributes. +

    + +

    +Use the removeAbandonedTimeout attribute to set the number +of seconds a database connection has been idle before it is considered abandoned. +

    + +removeAbandonedTimeout="60" + +

    +The default timeout for removing abandoned connections is 300 seconds. +

    + +

    +The logAbandoned attribute can be set to true +if you want DBCP 2 to log a stack trace of the code which abandoned the +database connection resources. +

    +logAbandoned="true" +

    +The default is false. +

    + +
    + + + +
    0. Introduction
    +

    Versions of MySQL and JDBC +drivers that have been reported to work: +

    +
      +
    • MySQL 3.23.47, MySQL 3.23.47 using InnoDB,, MySQL 3.23.58, MySQL 4.0.1alpha
    • +
    • Connector/J 3.0.11-stable (the official JDBC Driver)
    • +
    • mm.mysql 2.0.14 (an old 3rd party JDBC Driver)
    • +
    + +

    Before you proceed, don't forget to copy the JDBC Driver's jar into $CATALINA_HOME/lib.

    + +
    1. MySQL configuration
    +

    +Ensure that you follow these instructions as variations can cause problems. +

    + +

    Create a new test user, a new database and a single test table. +Your MySQL user must have a password assigned. The driver +will fail if you try to connect with an empty password. +

    + GRANT ALL PRIVILEGES ON *.* TO javauser@localhost + -> IDENTIFIED BY 'javadude' WITH GRANT OPTION; +mysql> create database javatest; +mysql> use javatest; +mysql> create table testdata ( + -> id int not null auto_increment primary key, + -> foo varchar(25), + -> bar int);]]> +
    +Note: the above user should be removed once testing is +complete! +
    + +

    Next insert some test data into the testdata table. +

    + insert into testdata values(null, 'hello', 12345); +Query OK, 1 row affected (0.00 sec) + +mysql> select * from testdata; ++----+-------+-------+ +| ID | FOO | BAR | ++----+-------+-------+ +| 1 | hello | 12345 | ++----+-------+-------+ +1 row in set (0.00 sec) + +mysql>]]> + +
    2. Context configuration
    +

    Configure the JNDI DataSource in Tomcat by adding a declaration for your +resource to your Context.

    +

    For example:

    + + + + + + + + + + + + + + + + +]]> + +
    3. web.xml configuration
    + +

    Now create a WEB-INF/web.xml for this test application.

    + + + MySQL Test App + + DB Connection + jdbc/TestDB + javax.sql.DataSource + Container + +]]> + +
    4. Test code
    +

    Now create a simple test.jsp page for use later.

    + +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + +select id, foo, bar from testdata + + + + + DB Test + + + +

    Results

    + + + Foo ${row.foo}
    + Bar ${row.bar}
    +
    + + +]]> + +

    That JSP page makes use of +JSTL's +SQL and Core taglibs. You can get it from +Apache Tomcat Taglibs - Standard Tag Library +project — just make sure you get a 1.1.x or later release. Once you have +JSTL, copy jstl.jar and standard.jar to your web app's +WEB-INF/lib directory. + +

    + +

    Finally deploy your web app into $CATALINA_BASE/webapps either +as a warfile called DBTest.war or into a sub-directory called +DBTest

    +

    Once deployed, point a browser at +http://localhost:8080/DBTest/test.jsp to view the fruits of +your hard work.

    + +
    + + +
    0. Introduction
    + +

    Oracle requires minimal changes from the MySQL configuration except for the +usual gotchas :-)

    +

    Drivers for older Oracle versions may be distributed as *.zip files rather +than *.jar files. Tomcat will only use *.jar files installed in +$CATALINA_HOME/lib. Therefore classes111.zip +or classes12.zip will need to be renamed with a .jar +extension. Since jarfiles are zipfiles, there is no need to unzip and jar these +files - a simple rename will suffice.

    + +

    For Oracle 9i onwards you should use oracle.jdbc.OracleDriver +rather than oracle.jdbc.driver.OracleDriver as Oracle have stated +that oracle.jdbc.driver.OracleDriver is deprecated and support +for this driver class will be discontinued in the next major release. +

    + +
    1. Context configuration
    +

    In a similar manner to the mysql config above, you will need to define your +Datasource in your Context. Here we define a +Datasource called myoracle using the thin driver to connect as user scott, +password tiger to the sid called mysid. (Note: with the thin driver this sid is +not the same as the tnsname). The schema used will be the default schema for the +user scott.

    + +

    Use of the OCI driver should simply involve a changing thin to oci in the URL string. +

    +]]> + +
    2. web.xml configuration
    +

    You should ensure that you respect the element ordering defined by the DTD when you +create you applications web.xml file.

    + + Oracle Datasource example + jdbc/myoracle + javax.sql.DataSource + Container +]]> +
    3. Code example
    +

    You can use the same example application as above (assuming you create the required DB +instance, tables etc.) replacing the Datasource code with something like

    + +
    + + + +
    0. Introduction
    +

    PostgreSQL is configured in a similar manner to Oracle.

    + +
    1. Required files
    +

    +Copy the Postgres JDBC jar to $CATALINA_HOME/lib. As with Oracle, the +jars need to be in this directory in order for DBCP 2's Classloader to find +them. This has to be done regardless of which configuration step you take next. +

    + +
    2. Resource configuration
    + +

    +You have two choices here: define a datasource that is shared across all Tomcat +applications, or define a datasource specifically for one application. +

    + +
    2a. Shared resource configuration
    +

    +Use this option if you wish to define a datasource that is shared across +multiple Tomcat applications, or if you just prefer defining your datasource +in this file. +

    +

    This author has not had success here, although others have reported so. +Clarification would be appreciated here.

    + +]]> +
    2b. Application-specific resource configuration
    + +

    +Use this option if you wish to define a datasource specific to your application, +not visible to other Tomcat applications. This method is less invasive to your +Tomcat installation. +

    + +

    +Create a resource definition for your Context. +The Context element should look something like the following. +

    + + + + +]]> + +
    3. web.xml configuration
    + + postgreSQL Datasource example + jdbc/postgres + javax.sql.DataSource + Container +]]> + +
    4. Accessing the datasource
    +

    +When accessing the datasource programmatically, remember to prepend +java:/comp/env to your JNDI lookup, as in the following snippet of +code. Note also that "jdbc/postgres" can be replaced with any value you prefer, provided +you change it in the above resource definition file as well. +

    + + + +
    +
    + +
    +

    +These solutions either utilise a single connection to the database (not recommended for anything other +than testing!) or some other pooling technology. +

    +
    + +
    + +

    Whilst not strictly addressing the creation of a JNDI DataSource using the OCI client, these notes can be combined with the +Oracle and DBCP 2 solution above.

    +

    +In order to use OCI driver, you should have an Oracle client installed. You should have installed +Oracle8i(8.1.7) client from cd, and download the suitable JDBC/OCI +driver(Oracle8i 8.1.7.1 JDBC/OCI Driver) from otn.oracle.com. +

    +

    +After renaming classes12.zip file to classes12.jar +for Tomcat, copy it into $CATALINA_HOME/lib. +You may also have to remove the javax.sql.* classes +from this file depending upon the version of Tomcat and JDK you are using. +

    +
    + + +

    +Ensure that you have the ocijdbc8.dll or .so in your $PATH or LD_LIBRARY_PATH + (possibly in $ORAHOME\bin) and also confirm that the native library can be loaded by a simple test program +using System.loadLibrary("ocijdbc8"); +

    +

    +You should next create a simple test servlet or JSP that has these +critical lines: +

    + +

    +where database is of the form host:port:SID Now if you try to access the URL of your +test servlet/JSP and what you get is a +ServletException with a root cause of java.lang.UnsatisfiedLinkError:get_env_handle. +

    +

    +First, the UnsatisfiedLinkError indicates that you have +

    +
      +
    • a mismatch between your JDBC classes file and +your Oracle client version. The giveaway here is the message stating that a needed library file cannot be +found. For example, you may be using a classes12.zip file from Oracle Version 8.1.6 with a Version 8.1.5 +Oracle client. The classesXXX.zip file and Oracle client software versions must match. +
    • +
    • A $PATH, LD_LIBRARY_PATH problem.
    • +
    • It has been reported that ignoring the driver you have downloaded from otn and using +the classes12.zip file from the directory $ORAHOME\jdbc\lib will also work. +
    • +
    +

    +Next you may experience the error ORA-06401 NETCMN: invalid driver designator +

    +

    +The Oracle documentation says : "Cause: The login (connect) string contains an invalid +driver designator. Action: Correct the string and re-submit." + +Change the database connect string (of the form host:port:SID) with this one: +(description=(address=(host=myhost)(protocol=tcp)(port=1521))(connect_data=(sid=orcl))) +

    +

    +Ed. Hmm, I don't think this is really needed if you sort out your TNSNames - but I'm not an Oracle DBA :-) +

    +
    +
    + +
    +

    Here are some common problems encountered with a web application which +uses a database and tips for how to solve them.

    + + +

    +Tomcat runs within a JVM. The JVM periodically performs garbage collection +(GC) to remove java objects which are no longer being used. When the JVM +performs GC execution of code within Tomcat freezes. If the maximum time +configured for establishment of a database connection is less than the amount +of time garbage collection took you can get a database connection failure. +

    + +

    To collect data on how long garbage collection is taking add the +-verbose:gc argument to your CATALINA_OPTS +environment variable when starting Tomcat. When verbose gc is enabled +your $CATALINA_BASE/logs/catalina.out log file will include +data for every garbage collection including how long it took.

    + +

    When your JVM is tuned correctly 99% of the time a GC will take less +than one second. The remainder will only take a few seconds. Rarely, +if ever should a GC take more than 10 seconds.

    + +

    Make sure that the db connection timeout is set to 10-15 seconds. +For DBCP 2 you set this using the parameter maxWaitMillis.

    + +
    + + +

    +These can occur when one request gets a db connection from the connection +pool and closes it twice. When using a connection pool, closing the +connection just returns it to the pool for reuse by another request, +it doesn't close the connection. And Tomcat uses multiple threads to +handle concurrent requests. Here is an example of the sequence +of events which could cause this error in Tomcat: +

    +
    +  Request 1 running in Thread 1 gets a db connection.
    +
    +  Request 1 closes the db connection.
    +
    +  The JVM switches the running thread to Thread 2
    +
    +  Request 2 running in Thread 2 gets a db connection
    +  (the same db connection just closed by Request 1).
    +
    +  The JVM switches the running thread back to Thread 1
    +
    +  Request 1 closes the db connection a second time in a finally block.
    +
    +  The JVM switches the running thread back to Thread 2
    +
    +  Request 2 Thread 2 tries to use the db connection but fails
    +  because Request 1 closed it.
    +
    +

    +Here is an example of properly written code to use a database connection +obtained from a connection pool: +

    + + +
    + + +

    + Please note that although the above instructions place the JNDI declarations in a Context + element, it is possible and sometimes desirable to place these declarations in the + GlobalNamingResources section of the server + configuration file. A resource placed in the GlobalNamingResources section will be shared + among the Contexts of the server. +

    +
    + + +

    + In order to get Realms to work, the realm must refer to the datasource as + defined in the <GlobalNamingResources> or <Context> section, not a datasource as renamed + using <ResourceLink>. +

    +
    + +
    + + +
    diff --git a/webapps/docs/jndi-resources-howto.xml b/webapps/docs/jndi-resources-howto.xml new file mode 100644 index 0000000..12b238d --- /dev/null +++ b/webapps/docs/jndi-resources-howto.xml @@ -0,0 +1,1204 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Yoav Shapira + JNDI Resources How-To + + + + +
    + +
    + +
    + +

    Tomcat provides a JNDI InitialContext implementation +instance for each web application running under it, in a manner that is +compatible with those provided by a +Jakarta EE application server. The Jakarta EE +standard provides a standard set of elements in the +/WEB-INF/web.xml file to reference/define resources.

    + +

    See the following Specifications for more information about programming APIs +for JNDI, and for the features supported by Jakarta EE servers, which Tomcat +emulates for the services that it provides:

    + + +
    + +
    + +

    The following elements may be used in the web application deployment +descriptor (/WEB-INF/web.xml) of your web application to define +resources:

    +
      +
    • <env-entry> - Environment entry, a + single-value parameter that can be used to configure how the application + will operate.
    • +
    • <resource-ref> - Resource reference, + which is typically to an object factory for resources such as a JDBC + DataSource, a Jakarta Mail Session, or custom + object factories configured into Tomcat.
    • +
    • <resource-env-ref> - Resource + environment reference, a new variation of resource-ref + added in Servlet 2.4 that is simpler to configure for resources + that do not require authentication information.
    • +
    + +

    Providing that Tomcat is able to identify an appropriate resource factory to +use to create the resource and that no further configuration information is +required, Tomcat will use the information in /WEB-INF/web.xml to +create the resource.

    + +

    Tomcat provides a number of Tomcat specific options for JNDI resources that +cannot be specified in web.xml. These include closeMethod that +enables faster cleaning-up of JNDI resources when a web application stops and +singleton that controls whether or not a new instance of the +resource is created for every JNDI lookup. To use these configuration options +the resource must be specified in a web application's +<Context> element or in the + +<GlobalNamingResources> element of +$CATALINA_BASE/conf/server.xml.

    + +
    + +
    + +

    If Tomcat is unable to identify the appropriate resource factory and/or +additional configuration information is required, additional Tomcat specific +configuration must be specified before Tomcat can create the resource. +Tomcat specific resource configuration is entered in +the <Context> elements that +can be specified in either $CATALINA_BASE/conf/server.xml or, +preferably, the per-web-application context XML file +(META-INF/context.xml).

    + +

    Tomcat specific resource configuration is performed using the following +elements in the <Context> +element:

    + +
      +
    • <Environment> - + Configure names and values for scalar environment entries that will be + exposed to the web application through the JNDI + InitialContext (equivalent to the inclusion of an + <env-entry> element in the web application + deployment descriptor).
    • +
    • <Resource> - + Configure the name and data type of a resource made available to the + application (equivalent to the inclusion of a + <resource-ref> element in the web application + deployment descriptor).
    • +
    • <ResourceLink> - + Add a link to a resource defined in the global JNDI context. Use resource + links to give a web application access to a resource defined in + the <GlobalNamingResources> + child element of the <Server> + element.
    • +
    • <Transaction> - + Add a resource factory for instantiating the UserTransaction object + instance that is available at java:comp/UserTransaction.
    • + +
    + +

    Any number of these elements may be nested inside a +<Context> element and will +be associated only with that particular web application.

    + +

    If a resource has been defined in a +<Context> element it is not +necessary for that resource to be defined in /WEB-INF/web.xml. +However, it is recommended to keep the entry in /WEB-INF/web.xml +to document the resource requirements for the web application.

    + +

    Where the same resource name has been defined for a +<env-entry> element included in the web application +deployment descriptor (/WEB-INF/web.xml) and in an +<Environment> element as part of the +<Context> element for the +web application, the values in the deployment descriptor will take precedence +only if allowed by the corresponding +<Environment> element (by setting the override +attribute to "true").

    + +
    + +
    + +

    Tomcat maintains a separate namespace of global resources for the +entire server. These are configured in the + +<GlobalNamingResources> element of +$CATALINA_BASE/conf/server.xml. You may expose these resources to +web applications by using a +<ResourceLink> to +include it in the per-web-application context.

    + +

    If a resource has been defined using a +<ResourceLink>, it is not +necessary for that resource to be defined in /WEB-INF/web.xml. +However, it is recommended to keep the entry in /WEB-INF/web.xml +to document the resource requirements for the web application.

    + +
    + +
    + +

    The InitialContext is configured as a web application is +initially deployed, and is made available to web application components (for +read-only access). All configured entries and resources are placed in +the java:comp/env portion of the JNDI namespace, so a typical +access to a resource - in this case, to a JDBC DataSource - +would look something like this:

    + + + +
    + +
    + +

    Tomcat includes a series of standard resource factories that can + provide services to your web applications, but give you configuration + flexibility (via the + <Context> element) + without modifying the web application or the deployment descriptor. Each + subsection below details the configuration and usage of the standard resource + factories.

    + +

    See Adding Custom + Resource Factories for information about how to create, install, + configure, and use your own custom resource factory classes with + Tomcat.

    + +

    NOTE - Of the standard resource factories, only the + "JDBC Data Source" and "User Transaction" factories are mandated to + be available on other platforms, and then they are required only if + the platform implements the Jakarta EE specs. + All other standard resource factories, plus custom resource factories + that you write yourself, are specific to Tomcat and cannot be assumed + to be available on other containers.

    + + + +
    0. Introduction
    + +

    This resource factory can be used to create objects of any + Java class that conforms to standard JavaBeans naming conventions (i.e. + it has a zero-arguments constructor, and has property setters that + conform to the setFoo() naming pattern. The resource factory will + only create a new instance of the appropriate bean class every time a + lookup() for this entry is made if the singleton + attribute of the factory is set to false.

    + +

    The steps required to use this facility are described below.

    + +
    1. Create Your JavaBean Class
    + +

    Create the JavaBean class which will be instantiated each time + that the resource factory is looked up. For this example, assume + you create a class com.mycompany.MyBean, which looks + like this:

    + + + +
    2. Declare Your Resource Requirements
    + +

    Next, modify your web application deployment descriptor + (/WEB-INF/web.xml) to declare the JNDI name under which + you will request new instances of this bean. The simplest approach is + to use a <resource-env-ref> element, like this:

    + + + + Object factory for MyBean instances. + + + bean/MyBeanFactory + + + com.mycompany.MyBean + +]]> + +

    WARNING - Be sure you respect the element ordering + that is required by the DTD for web application deployment descriptors! + See the + Servlet + Specification for details.

    + +
    3. Code Your Application's Use Of This Resource
    + +

    A typical use of this resource environment reference might look + like this:

    + + + +
    4. Configure Tomcat's Resource Factory
    + +

    To configure Tomcat's resource factory, add an element like this to the + <Context> element for + this web application.

    + + + ... + + ... +]]> + +

    Note that the resource name (here, bean/MyBeanFactory + must match the value specified in the web application deployment + descriptor. We are also initializing the value of the bar + property, which will cause setBar(23) to be called before + the new bean is returned. Because we are not initializing the + foo property (although we could have), the bean will + contain whatever default value is set up by its constructor.

    + +

    If the bean property is of type String, the BeanFactory + will call the property setter using the provided property value. If the + bean property type is a primitive or a primitive wrapper, the the + BeanFactory will convert the value to the appropriate primitive or + primitive wrapper and then use that value when calling the setter. Some + beans have properties with types that cannot automatically be converted + from String. If the bean provides an alternative setter with + the same name that does take a String, the BeanFactory will + attempt to use that setter. If the BeanFactory cannot use the value or + perform an appropriate conversion, setting the property will fail with a + NamingException.

    + +

    The forceString property available in earlier Tomcat + releases has been removed as a security hardening measure.

    + +
    + + + + +
    0. Introduction
    + +

    UserDatabase resources are typically configured as global resources for + use by a UserDatabase realm. Tomcat includes a UserDatabaseFactory that + creates UserDatabase resources backed by an XML file - usually + tomcat-users.xml.

    + +

    The steps required to set up a global UserDatabase resource are described + below.

    + +
    1. Create/edit the XML file
    + +

    The XML file is typically located at + $CATALINA_BASE/conf/tomcat-users.xml however, you are free to + locate the file anywhere on the file system. It is recommended that the XML + files are placed in $CATALINA_BASE/conf. A typical XML would + look like:

    + + + + + + + + +]]> + +
    2. Declare Your Resource
    + +

    Next, modify $CATALINA_BASE/conf/server.xml to create the + UserDatabase resource based on your XML file. It should look something like + this:

    + +]]> + +

    The pathname attribute can be a URL, an absolute path or a + relative path. If relative, it is relative to $CATALINA_BASE. +

    + +

    The readonly attribute is optional and defaults to + true if not supplied. If the XML is writable then it will be + written to when Tomcat starts. WARNING: When the file is + written it will inherit the default file permissions for the user Tomcat + is running as. Ensure that these are appropriate to maintain the security + of your installation.

    + +

    If referenced in a Realm, the UserDatabase will, by default, monitor + pathname for changes and reload the file if a change in the + last modified time is observed. This can be disabled by setting the + watchSource attribute to false. +

    + +
    3. Configure the Realm
    + +

    Configure a UserDatabase Realm to use this resource as described in the + Realm configuration documentation.

    + +
    + + + + +
    0. Introduction
    + +

    Tomcat also include a UserDatabase that uses a + DataSource resource as the backend. The backend resource + must be declared in the same JNDI context as the user database that will use + it.

    + +

    The steps required to set up a global UserDatabase resource are described + below.

    + +
    1. Database schema
    + +

    The database shema for the user database is flexible. It can be the same + as the schema used for the DataSourceRealm, with only a table + for users (user name, password), and another one listing the roles + associated with each user. To support the full UserDatabase + features, it must include additional tables for groups, and is + compatible with referential integrity between users, groups and roles.

    + +

    The full featured schema with groups and referential integrity + could be:

    + + + +

    The minimal schema without the ability to use groups will be + (it is the same as for the DataSourceRealm):

    + + + +
    2. Declare Your Resource
    + +

    Next, modify $CATALINA_BASE/conf/server.xml to create the + UserDatabase resource based on your DataSource and its schema. + It should look something like this:

    + +]]> + +

    The dataSourceName attribute is the JNDI name of the + DataSource that will be the backend for the + UserDatabase. It must be declared in the same JNDI + Context as the UserDatabase. Please refer to the + DataSource resources + documentation for further instructions.

    + +

    The readonly attribute is optional and defaults to + true if not supplied. If the database is writable then changes + made through the Tomcat management to the UserDatabase can + be persisted to the database using the save operation.

    + +

    Alternately, changes can also be made directly to the backend database. +

    + +
    3. Resource configuration
    + + + + +

    The name of the JNDI JDBC DataSource for this UserDatabase.

    +
    + + +

    Name of the column, in the "groups", "group roles" and "user groups" + tables, that contains the group's name.

    +
    + + +

    Name of the "group roles" table, which must contain columns + named by the groupNameCol and roleNameCol + attributes.

    +
    + + +

    Name of the "groups" table, which must contain columns named + by the groupNameCol attribute.

    +
    + + +

    If this is set to true, then changes to the + UserDatabase can be persisted to the + DataSource by using the save method. + The default value is true.

    +
    + + +

    Name of the column, in the "roles" and "groups" tables, that contains + the description for the roles and groups.

    +
    + + +

    Name of the column, in the "roles", "user roles" and "group roles" + tables, which contains a role name assigned to the corresponding + user.

    +

    This attribute is required in majority of + configurations. See allRolesMode attribute of the + associated realm for a rare case when it can be omitted.

    +
    + + +

    Name of the "roles" table, which must contain columns named + by the roleNameCol attribute.

    +
    + + +

    Name of the column, in the "users" table, which contains + the user's credentials (i.e. password). If a + CredentialHandler is specified, this component + will assume that the passwords have been encoded with the + specified algorithm. Otherwise, they will be assumed to be + in clear text.

    +
    + + +

    Name of the "user groups" table, which must contain columns + named by the userNameCol and groupNameCol + attributes.

    +
    + + +

    Name of the column, in the "users", "user groups" and "user roles" + tables, that contains the user's username.

    +
    + + +

    Name of the column, in the "users" table, that contains the user's + full name.

    +
    + + +

    Name of the "user roles" table, which must contain columns + named by the userNameCol and roleNameCol + attributes.

    +

    This attribute is required in majority of + configurations. See allRolesMode attribute of the + associated realm for a rare case when it can be omitted.

    +
    + + +

    Name of the "users" table, which must contain columns named + by the userNameCol and userCredCol + attributes.

    +
    + +
    + +
    4. Configure the Realm
    + +

    Configure a UserDatabase Realm to use this resource as described in the + Realm configuration documentation.

    + +
    + + + +
    0. Introduction
    + +

    In many web applications, sending electronic mail messages is a + required part of the system's functionality. The + Jakarta Mail API + makes this process relatively straightforward, but requires many + configuration details that the client application must be aware of + (including the name of the SMTP host to be used for message sending).

    + +

    Tomcat includes a standard resource factory that will create + jakarta.mail.Session session instances for you, already + configured to connect to an SMTP server. + In this way, the application is totally insulated from changes in the + email server configuration environment - it simply asks for, and receives, + a preconfigured session whenever needed.

    + +

    The steps required for this are outlined below.

    + +
    1. Declare Your Resource Requirements
    + +

    The first thing you should do is modify the web application deployment + descriptor (/WEB-INF/web.xml) to declare the JNDI name under + which you will look up preconfigured sessions. By convention, all such + names should resolve to the mail subcontext (relative to the + standard java:comp/env naming context that is the root of + all provided resource factories. A typical web.xml entry + might look like this:

    + + + Resource reference to a factory for jakarta.mail.Session + instances that may be used for sending electronic mail + messages, preconfigured to connect to the appropriate + SMTP server. + + + mail/Session + + + jakarta.mail.Session + + + Container + +]]> + +

    WARNING - Be sure you respect the element ordering + that is required by the DTD for web application deployment descriptors! + See the + Servlet + Specification for details.

    + +
    2. Code Your Application's Use Of This Resource
    + +

    A typical use of this resource reference might look like this:

    + + +

    Note that the application uses the same resource reference name + that was declared in the web application deployment descriptor. This + is matched up against the resource factory that is configured in the + <Context> element + for the web application as described below.

    + +
    3. Configure Tomcat's Resource Factory
    + +

    To configure Tomcat's resource factory, add an elements like this to the + <Context> element for + this web application.

    + + + ... + + ... +]]> + +

    Note that the resource name (here, mail/Session) must + match the value specified in the web application deployment descriptor. + Customize the value of the mail.smtp.host parameter to + point at the server that provides SMTP service for your network.

    + +

    Additional resource attributes and values will be converted to properties + and values and passed to + jakarta.mail.Session.getInstance(java.util.Properties) as part of + the java.util.Properties collection. In addition to the + properties defined in Appendix A of the Jakarta Mail specification, individual + providers may also support additional properties. +

    + +

    If the resource is configured with a password attribute and + either a mail.smtp.user or mail.user attribute + then Tomcat's resource factory will configure and add a + jakarta.mail.Authenticator to the mail session.

    + +
    4. Install the Jakarta Mail API
    + +

    + Download the Jakarta Mail API.

    + +

    Unpackage the distribution and place jakarta.mail-api-2.1.0.jar into + $CATALINA_HOME/lib so that it is available to Tomcat during the + initialization of the mail Session Resource. + Note: placing this jar in both $CATALINA_HOME/lib and a web + application's lib folder will cause an error, so ensure you have it in the + $CATALINA_HOME/lib location only. +

    + +
    5. Install a compatible implementaion
    + +

    Select and + download a compatible implementation.

    + +

    Unpackage the implementation and place the jar file(s) into + $CATALINA_HOME/lib.

    + +

    Note: Other implementations may be available

    + +
    6. Restart Tomcat
    + +

    For the additional JAR to be visible to Tomcat, it is necessary for the + Tomcat instance to be restarted.

    + + +
    Example Application
    + +

    The /examples application included with Tomcat contains + an example of utilizing this resource factory. It is accessed via the + "JSP Examples" link. The source code for the servlet that actually + sends the mail message is in + /WEB-INF/classes/SendMailServlet.java.

    + +

    WARNING - The default configuration assumes that there + is an SMTP server listing on port 25 on localhost. If this is + not the case, edit the + <Context> element for + this web application and modify the parameter value for the + mail.smtp.host parameter to be the host name of an SMTP server + on your network.

    + +
    + + + +
    0. Introduction
    + +

    Many web applications need to access a database via a JDBC driver, + to support the functionality required by that application. The Jakarta EE + Platform Specification requires Jakarta EE Application Servers to make + available a DataSource implementation (that is, a connection + pool for JDBC connections) for this purpose. Tomcat offers exactly + the same support, so that database-based applications you develop on + Tomcat using this service will run unchanged on any Jakarta EE server.

    + +

    For information about JDBC, you should consult the following:

    + + +

    NOTE - The default data source support in Tomcat + is based on the DBCP 2 connection pool from the + Commons + project. However, it is possible to use any other connection pool + that implements javax.sql.DataSource, by writing your + own custom resource factory, as described + below.

    + +
    1. Install Your JDBC Driver
    + +

    Use of the JDBC Data Sources JNDI Resource Factory requires + that you make an appropriate JDBC driver available to both Tomcat internal + classes and to your web application. This is most easily accomplished by + installing the driver's JAR file(s) into the + $CATALINA_HOME/lib directory, which makes the driver + available both to the resource factory and to your application.

    + +
    2. Declare Your Resource Requirements
    + +

    Next, modify the web application deployment descriptor + (/WEB-INF/web.xml) to declare the JNDI name under + which you will look up preconfigured data source. By convention, all such + names should resolve to the jdbc subcontext (relative to the + standard java:comp/env naming context that is the root of + all provided resource factories. A typical web.xml entry + might look like this:

    + + + Resource reference to a factory for java.sql.Connection + instances that may be used for talking to a particular + database that is configured in the + configuration for the web application. + + + jdbc/EmployeeDB + + + javax.sql.DataSource + + + Container + +]]> + +

    WARNING - Be sure you respect the element ordering + that is required by the DTD for web application deployment descriptors! + See the + Servlet + Specification for details.

    + +
    3. Code Your Application's Use Of This Resource
    + +

    A typical use of this resource reference might look like this:

    + + +

    Note that the application uses the same resource reference name that was + declared in the web application deployment descriptor. This is matched up + against the resource factory that is configured in the + <Context> element for + the web application as described below.

    + +
    4. Configure Tomcat's Resource Factory
    + +

    To configure Tomcat's resource factory, add an element like this to the + <Context> element for + the web application.

    + + + ... + + ... +]]> + +

    Note that the resource name (here, jdbc/EmployeeDB) must + match the value specified in the web application deployment descriptor.

    + +

    This example assumes that you are using the HypersonicSQL database + JDBC driver. Customize the driverClassName and + driverName parameters to match your actual database's + JDBC driver and connection URL.

    + +

    The configuration properties for Tomcat's standard data source + resource factory + (org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory) are + as follows:

    +
      +
    • driverClassName - Fully qualified Java class name + of the JDBC driver to be used.
    • +
    • username - Database username to be passed to our + JDBC driver.
    • +
    • password - Database password to be passed to our + JDBC driver.
    • +
    • url - Connection URL to be passed to our JDBC driver. + (For backwards compatibility, the property driverName + is also recognized.)
    • +
    • initialSize - The initial number of connections + that will be created in the pool during pool initialization. Default: 0
    • +
    • maxTotal - The maximum number of connections + that can be allocated from this pool at the same time. Default: 8
    • +
    • minIdle - The minimum number of connections that + will sit idle in this pool at the same time. Default: 0
    • +
    • maxIdle - The maximum number of connections that + can sit idle in this pool at the same time. Default: 8
    • +
    • maxWaitMillis - The maximum number of milliseconds that the + pool will wait (when there are no available connections) for a + connection to be returned before throwing an exception. Default: -1 (infinite)
    • +
    +

    Some additional properties handle connection validation:

    +
      +
    • validationQuery - SQL query that can be used by the + pool to validate connections before they are returned to the + application. If specified, this query MUST be an SQL SELECT + statement that returns at least one row.
    • +
    • validationQueryTimeout - Timeout in seconds + for the validation query to return. Default: -1 (infinite)
    • +
    • testOnBorrow - true or false: whether a connection + should be validated using the validation query each time it is + borrowed from the pool. Default: true
    • +
    • testOnReturn - true or false: whether a connection + should be validated using the validation query each time it is + returned to the pool. Default: false
    • +
    +

    The optional evictor thread is responsible for shrinking the pool + by removing any connections which are idle for a long time. The evictor + does not respect minIdle. Note that you do not need to + activate the evictor thread if you only want the pool to shrink according + to the configured maxIdle property.

    +

    The evictor is disabled by default and can be configured using + the following properties:

    +
      +
    • timeBetweenEvictionRunsMillis - The number of + milliseconds between consecutive runs of the evictor. + Default: -1 (disabled)
    • +
    • numTestsPerEvictionRun - The number of connections + that will be checked for idleness by the evictor during each + run of the evictor. Default: 3
    • +
    • minEvictableIdleTimeMillis - The idle time in + milliseconds after which a connection can be removed from the pool + by the evictor. Default: 30*60*1000 (30 minutes)
    • +
    • testWhileIdle - true or false: whether a connection + should be validated by the evictor thread using the validation query + while sitting idle in the pool. Default: false
    • +
    +

    Another optional feature is the removal of abandoned connections. + A connection is called abandoned if the application does not return it + to the pool for a long time. The pool can close such connections + automatically and remove them from the pool. This is a workaround + for applications leaking connections.

    +

    The abandoning feature is disabled by default and can be configured + using the following properties:

    +
      +
    • removeAbandonedOnBorrow - true or false: whether to + remove abandoned connections from the pool when a connection is + borrowed. Default: false
    • +
    • removeAbandonedOnMaintenance - true or false: whether + to remove abandoned connections from the pool during pool maintenance. + Default: false
    • +
    • removeAbandonedTimeout - The number of + seconds after which a borrowed connection is assumed to be abandoned. + Default: 300
    • +
    • logAbandoned - true or false: whether to log + stack traces for application code which abandoned a statement + or connection. This adds serious overhead. Default: false
    • +
    +

    Finally there are various properties that allow further fine tuning + of the pool behaviour:

    +
      +
    • defaultAutoCommit - true or false: default + auto-commit state of the connections created by this pool. + Default: true
    • +
    • defaultReadOnly - true or false: default + read-only state of the connections created by this pool. + Default: false
    • +
    • defaultTransactionIsolation - This sets the + default transaction isolation level. Can be one of + NONE, READ_COMMITTED, + READ_UNCOMMITTED, REPEATABLE_READ, + SERIALIZABLE. Default: no default set
    • +
    • poolPreparedStatements - true or false: whether to + pool PreparedStatements and CallableStatements. Default: false
    • +
    • maxOpenPreparedStatements - The maximum number of open + statements that can be allocated from the statement pool at the same time. + Default: -1 (unlimited)
    • +
    • defaultCatalog - The name of the default catalog. + Default: not set
    • +
    • connectionInitSqls - A list of SQL statements + run once after a Connection is created. Separate multiple statements + by semicolons (;). Default: no statement
    • +
    • connectionProperties - A list of driver specific + properties passed to the driver for creating connections. Each + property is given as name=value, multiple properties + are separated by semicolons (;). Default: no properties
    • +
    • accessToUnderlyingConnectionAllowed - true or false: whether + accessing the underlying connections is allowed. Default: false
    • +
    +

    For more details, please refer to the Commons DBCP 2 documentation.

    + +
    + +
    + + +
    + +

    If none of the standard resource factories meet your needs, you can write + your own factory and integrate it into Tomcat, and then configure the use + of this factory in the + <Context> element for + the web application. In the example below, we will create a factory that only + knows how to create com.mycompany.MyBean beans from the + Generic JavaBean Resources example + above.

    + +

    1. Write A Resource Factory Class

    + +

    You must write a class that implements the JNDI service provider + javax.naming.spi.ObjectFactory interface. Every time your + web application calls lookup() on a context entry that is + bound to this factory (assuming that the factory is configured with + singleton="false"), the + getObjectInstance() method is called, with the following + arguments:

    +
      +
    • Object obj - The (possibly null) object containing + location or reference information that can be used in creating an object. + For Tomcat, this will always be an object of type + javax.naming.Reference, which contains the class name of + this factory class, as well as the configuration properties (from the + <Context> for the + web application) to use in creating objects to be returned.
    • +
    • Name name - The name to which this factory is bound + relative to nameCtx, or null if no name + is specified.
    • +
    • Context nameCtx - The context relative to which the + name parameter is specified, or null if + name is relative to the default initial context.
    • +
    • Hashtable environment - The (possibly null) + environment that is used in creating this object. This is generally + ignored in Tomcat object factories.
    • +
    + +

    To create a resource factory that knows how to produce MyBean + instances, you might create a class like this:

    + + + +

    In this example, we are unconditionally creating a new instance of + the com.mycompany.MyBean class, and populating its properties + based on the parameters included in the <ResourceParams> + element that configures this factory (see below). You should note that any + parameter named factory should be skipped - that parameter is + used to specify the name of the factory class itself (in this case, + com.mycompany.MyBeanFactory) rather than a property of the + bean being configured.

    + +

    For more information about ObjectFactory, see the + + JNDI Service Provider Interface (SPI) Specification.

    + +

    You will need to compile this class against a class path that includes + all of the JAR files in the $CATALINA_HOME/lib directory. When you are through, + place the factory class (and the corresponding bean class) unpacked under + $CATALINA_HOME/lib, or in a JAR file inside + $CATALINA_HOME/lib. In this way, the required class + files are visible to both Catalina internal resources and your web + application.

    + +

    2. Declare Your Resource Requirements

    + +

    Next, modify your web application deployment descriptor + (/WEB-INF/web.xml) to declare the JNDI name under which + you will request new instances of this bean. The simplest approach is + to use a <resource-env-ref> element, like this:

    + + + + Object factory for MyBean instances. + + + bean/MyBeanFactory + + + com.mycompany.MyBean + +]]> + +

    WARNING - Be sure you respect the element ordering + that is required by the DTD for web application deployment descriptors! + See the + Servlet + Specification for details.

    + +

    3. Code Your Application's Use Of This Resource

    + +

    A typical use of this resource environment reference might look + like this:

    + + + +

    4. Configure Tomcat's Resource Factory

    + +

    To configure Tomcat's resource factory, add an elements like this to the + <Context> element for + this web application.

    + + + ... + + ... +]]> + +

    Note that the resource name (here, bean/MyBeanFactory + must match the value specified in the web application deployment + descriptor. We are also initializing the value of the bar + property, which will cause setBar(23) to be called before + the new bean is returned. Because we are not initializing the + foo property (although we could have), the bean will + contain whatever default value is set up by its constructor.

    + +

    You will also note that, from the application developer's perspective, + the declaration of the resource environment reference, and the programming + used to request new instances, is identical to the approach used for the + Generic JavaBean Resources example. This illustrates one of the + advantages of using JNDI resources to encapsulate functionality - you can + change the underlying implementation without necessarily having to + modify applications using the resources, as long as you maintain + compatible APIs.

    + +
    + + + +
    diff --git a/webapps/docs/jspapi/index.html b/webapps/docs/jspapi/index.html new file mode 100644 index 0000000..4d0d127 --- /dev/null +++ b/webapps/docs/jspapi/index.html @@ -0,0 +1,34 @@ + + + + + + API docs + + + + +The JSP Javadoc is not installed by default. Download and install +the "fulldocs" package to get it. + +You can also access the javadoc online in the Tomcat + +documentation bundle. + + + diff --git a/webapps/docs/logging.xml b/webapps/docs/logging.xml new file mode 100644 index 0000000..8659b14 --- /dev/null +++ b/webapps/docs/logging.xml @@ -0,0 +1,430 @@ + + + + +]> + + + &project; + + + Logging in Tomcat + Allistair Crossley + Yoav Shapira + + + + +
    + +
    + +
    +

    + The internal logging for Apache Tomcat uses JULI, a packaged renamed fork + of Apache Commons Logging + that is hard-coded to use the java.util.logging framework. + This ensures that Tomcat's internal logging and any web application + logging will remain independent, even if a web application uses Apache + Commons Logging. +

    + +

    + To configure Tomcat to use an alternative logging framework for its + internal logging, follow the instructions provided by the alternative + logging framework for redirecting logging for applications that use + java.util.logging. Keep in mind that the alternative logging + framework will need to be capable of working in an environment where + different loggers with the same name may exist in different class loaders. +

    + +

    + A web application running on Apache Tomcat can: +

    +
      +
    • + Use any logging framework of its choice. +
    • +
    • + Use system logging API, java.util.logging. +
    • +
    • + Use the logging API provided by the Java Servlets specification, + jakarta.servlet.ServletContext.log(...) +
    • +
    + +

    + The logging frameworks used by different web applications are independent. + See class loading for more details. + The exception to this rule is java.util.logging. If it is + used directly or indirectly by your logging library then elements of it + will be shared across web applications because it is loaded by the system + class loader. +

    + + + +

    + Apache Tomcat has its own implementation of several key elements of + java.util.logging API. This implementation is called JULI. + The key component there is a custom LogManager implementation, + that is aware of different web applications running on Tomcat (and + their different class loaders). It supports private per-application + logging configurations. It is also notified by Tomcat when a web application + is unloaded from memory, so that the references to its classes can be + cleared, preventing memory leaks. +

    + +

    + This java.util.logging implementation is enabled by providing + certain system properties when starting Java. The Apache Tomcat startup + scripts do this for you, but if you are using different tools to run + Tomcat (such as jsvc, or running Tomcat from within an IDE), you should + take care of them by yourself. +

    + +

    + More details about java.util.logging may be found in the documentation + for your JDK and on its Javadoc pages for the java.util.logging + package. +

    + +

    + More details about Tomcat JULI may be found below. +

    + +
    + + + +

    + The calls to jakarta.servlet.ServletContext.log(...) to write + log messages are handled by internal Tomcat logging. Such messages are + logged to the category named +

    + org.apache.catalina.core.ContainerBase.[${engine}].[${host}].[${context}] +

    + This logging is performed according to the Tomcat logging configuration. You + cannot overwrite it in a web application. +

    + +

    + The Servlets logging API predates the java.util.logging API + that is now provided by Java. As such, it does not offer you much options. + E.g., you cannot control the log levels. It can be noted, though, that + in Apache Tomcat implementation the calls to ServletContext.log(String) + or GenericServlet.log(String) are logged at the INFO level. + The calls to ServletContext.log(String, Throwable) or + GenericServlet.log(String, Throwable) + are logged at the SEVERE level. +

    + +
    + + + +

    + When running Tomcat on unixes, the console output is usually redirected + to the file named catalina.out. The name is configurable + using an environment variable. (See the startup scripts). + Whatever is written to System.err/out will be caught into + that file. That may include: +

    + +
      +
    • Uncaught exceptions printed by java.lang.ThreadGroup.uncaughtException(..)
    • +
    • Thread dumps, if you requested them via a system signal
    • +
    + +

    + When running as a service on Windows, the console output is also caught + and redirected, but the file names are different. +

    + +

    + The default logging configuration in Apache Tomcat writes the same + messages to the console and to a log file. This is great when using + Tomcat for development, but usually is not needed in production. +

    + +

    + Old applications that still use System.out or System.err + can be tricked by setting swallowOutput attribute on a + Context. If the attribute is set to + true, the calls to System.out/err during request + processing will be intercepted, and their output will be fed to the + logging subsystem using the + jakarta.servlet.ServletContext.log(...) calls.
    + Note, that the swallowOutput feature is + actually a trick, and it has its limitations. + It works only with direct calls to System.out/err, + and only during request processing cycle. It may not work in other + threads that might be created by the application. It cannot be used to + intercept logging frameworks that themselves write to the system streams, + as those start early and may obtain a direct reference to the streams + before the redirection takes place. +

    + +
    + + + +

    + Access logging is a related but different feature, which is + implemented as a Valve. It uses self-contained + logic to write its log files. The essential requirement for + access logging is to handle a large continuous stream of data + with low overhead, so it only uses Apache Commons Logging for + its own debug messages. This implementation approach avoids + additional overhead and potentially complex configuration. + Please refer to the Valves + documentation for more details on its configuration, including + the various report formats. +

    + +
    + +
    + +
    + +

    + The default implementation of java.util.logging provided in the JDK is too + limited to be useful. The key limitation is the inability to have per-web + application logging, as the configuration is per-VM. As a result, Tomcat + will, in the default configuration, replace the default LogManager + implementation with a container friendly implementation called JULI, which + addresses these shortcomings. +

    +

    + JULI supports the same configuration mechanisms as the standard JDK + java.util.logging, using either a programmatic approach, or + properties files. The main difference is that per-classloader properties + files can be set (which enables easy redeployment friendly webapp + configuration), and the properties files support extended constructs which + allows more freedom for defining handlers and assigning them to loggers. +

    +

    + JULI is enabled by default, and supports per classloader configuration, in + addition to the regular global java.util.logging configuration. This means + that logging can be configured at the following layers: +

    +
      +
    • Globally. That is usually done in the + ${catalina.base}/conf/logging.properties file. + The file is specified by the java.util.logging.config.file + System property which is set by the startup scripts. + If it is not readable or is not configured, the default is to use the + ${java.home}/lib/logging.properties file in the JRE. +
    • +
    • In the web application. The file will be + WEB-INF/classes/logging.properties +
    • +
    +

    + The default logging.properties in the JRE specifies a + ConsoleHandler that routes logging to System.err. + The default conf/logging.properties in Apache Tomcat also + adds several AsyncFileHandlers that write to files. +

    +

    + A handler's log level threshold is INFO by default and can be set using + SEVERE, WARNING, INFO, CONFIG, + FINE, FINER, FINEST or ALL. + You can also target specific packages to collect logging from and specify + a level. +

    +

    + To enable debug logging for part of Tomcat's internals, you should + configure both the appropriate logger(s) and the appropriate handler(s) to + use the FINEST or ALL level. e.g.: +

    + org.apache.catalina.session.level=ALL +java.util.logging.ConsoleHandler.level=ALL +

    + When enabling debug logging it is recommended that it is enabled for the + narrowest possible scope as debug logging can generate large amounts of + information. +

    +

    + The configuration used by JULI is the same as the one supported by plain + java.util.logging, but uses a few extensions to allow better + flexibility in configuring loggers and handlers. The main differences are: +

    +
      +
    • A prefix may be added to handler names, so that multiple handlers of a + single class may be instantiated. A prefix is a String which starts with a + digit, and ends with '.'. For example, 22foobar. is a valid + prefix.
    • +
    • System property replacement is performed for property values which + contain ${systemPropertyName}.
    • +
    • If using a class loader that implements the + org.apache.juli.WebappProperties interface (Tomcat's + web application class loader does) then property replacement is also + performed for ${classloader.webappName}, + ${classloader.hostName} and + ${classloader.serviceName} which are replaced with the + web application name, the host name and the service name respectively. +
    • +
    • By default, loggers will not delegate to their parent if they have + associated handlers. This may be changed per logger using the + loggerName.useParentHandlers property, which accepts a + boolean value.
    • +
    • The root logger can define its set of handlers using the + .handlers property.
    • +
    • By default the log files will be kept on the file system for + 90 days. This may be changed per handler using the + handlerName.maxDays property. If the specified value for the + property is ≤0 then the log files will be kept on the + file system forever, otherwise they will be kept the specified maximum + days.
    • +
    +

    + There are several additional implementation classes, that can be used + together with the ones provided by Java. The notable ones are + org.apache.juli.FileHandler and org.apache.juli.AsyncFileHandler. +

    +

    + org.apache.juli.FileHandler supports buffering of the + logs. The buffering is not enabled by default. To configure it, use the + bufferSize property of a handler. The value of 0 + uses system default buffering (typically an 8K buffer will be used). A + value of <0 forces a writer flush upon each log write. A + value >0 uses a BufferedOutputStream with the defined + value but note that the system default buffering will also be + applied. +

    +

    + org.apache.juli.AsyncFileHandler is a subclass of FileHandler + that queues the log messages and writes them asynchronously to the log files. + Its additional behaviour can be configured by setting some + system properties. +

    +

    + Example logging.properties file to be placed in $CATALINA_BASE/conf: +

    + + +

    + Example logging.properties for the servlet-examples web application to be + placed in WEB-INF/classes inside the web application: +

    + + + + +

    See the following resources for additional information:

    + +
    + + +

    You may want to take note of the following:

    +
      +
    • Consider removing ConsoleHandler from configuration. By + default (thanks to the .handlers setting) logging goes both + to a FileHandler and to a ConsoleHandler. The + output of the latter one is usually captured into a file, such as + catalina.out. Thus you end up with two copies of the same + messages.
    • +
    • Consider removing FileHandlers for the applications + that you do not use. E.g., the one for host-manager.
    • +
    • The handlers by default use the system default encoding to write + the log files. It can be configured with encoding property. + See Javadoc for details.
    • +
    • Consider configuring an + Access log.
    • +
    +
    + +
    + + +
    diff --git a/webapps/docs/manager-howto.xml b/webapps/docs/manager-howto.xml new file mode 100644 index 0000000..f21ac58 --- /dev/null +++ b/webapps/docs/manager-howto.xml @@ -0,0 +1,1483 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Manager App How-To + + + + +
    + +
    + +
    + +

    In many production environments, it is very useful to have the capability +to deploy a new web application, or undeploy an existing one, without having +to shut down and restart the entire container. In addition, you can request +an existing application to reload itself, even if you have not declared it +to be reloadable in the Tomcat server +configuration file.

    + +

    To support these capabilities, Tomcat includes a web application +(installed by default on context path /manager) that supports +the following functions:

    +
      +
    • Deploy a new web application from the uploaded contents of a WAR file.
    • +
    • Deploy a new web application, on a specified context path, from the + server file system.
    • +
    • List the currently deployed web applications, as well as the + sessions that are currently active for those web apps.
    • +
    • Reload an existing web application, to reflect changes in the + contents of /WEB-INF/classes or /WEB-INF/lib. +
    • +
    • List the OS and JVM property values.
    • +
    • List the available global JNDI resources, for use in deployment + tools that are preparing <ResourceLink> elements + nested in a <Context> deployment description.
    • +
    • Start a stopped application (thus making it available again).
    • +
    • Stop an existing application (so that it becomes unavailable), but + do not undeploy it.
    • +
    • Undeploy a deployed web application and delete its document base + directory (unless it was deployed from file system).
    • +
    + +

    A default Tomcat installation includes an instance of the Manager application +configured for the default virtual host. If you create additional virtual hosts, +you may wish to add an instance of the Manager application to one or more of +those Hosts. To add an instance of the Manager web application +Context to a new host install the manager.xml context +configuration file in the +$CATALINA_BASE/conf/[enginename]/[hostname] folder. Here is an +example:

    + + + + +]]> + +

    There are three ways to use the Manager web application.

    +
      +
    • As an application with a user interface you use in your browser. +Here is an example URL where you can replace localhost with +your website host name: http://localhost:8080/manager/html .
    • +
    • A minimal version using HTTP requests only which is suitable for use +by scripts setup by system administrators. Commands are given as part of the +request URI, and responses are in the form of simple text that can be easily +parsed and processed. See +Supported Manager Commands for more information.
    • +
    • A convenient set of task definitions for the Ant +(version 1.4 or later) build tool. See +Executing Manager Commands +With Ant for more information.
    • +
    + +
    + +
    + + +

    The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + + +

    It would be quite unsafe to ship Tomcat with default settings that allowed +anyone on the Internet to execute the Manager application on your server. +Therefore, the Manager application is shipped with the requirement that anyone +who attempts to use it must authenticate themselves, using a username and +password that have one of manager-xxx roles associated with +them (the role name depends on what functionality is required). +Further, there is no username in the default users file +($CATALINA_BASE/conf/tomcat-users.xml) that is assigned to those +roles. Therefore, access to the Manager application is completely disabled +by default.

    + +

    You can find the role names in the web.xml file of the Manager +web application. The available roles are:

    + +
      +
    • manager-gui — Access to the HTML interface.
    • +
    • manager-status — Access to the "Server Status" + page only.
    • +
    • manager-script — Access to the tools-friendly + plain text interface that is described in this document, + and to the "Server Status" page.
    • +
    • manager-jmx — Access to JMX proxy interface + and to the "Server Status" page.
    • +
    + +

    The HTML interface is protected against CSRF (Cross-Site Request Forgery) +attacks, but the text and JMX interfaces cannot be protected. It means that +users who are allowed access to the text and JMX interfaces have to be cautious +when accessing the Manager application with a web browser. +To maintain the CSRF protection:

    + +
      +
    • If you use web browser to access the Manager application using + a user that has either manager-script or + manager-jmx roles (for example for testing + the plain text or JMX interfaces), you MUST close all windows + of the browser afterwards to terminate the session. + If you do not close the browser and visit other sites, you may become + victim of a CSRF attack.
    • +
    • It is recommended to never grant + the manager-script or manager-jmx + roles to users that have the manager-gui role.
    • +
    + +

    Note that JMX proxy interface is effectively low-level root-like +administrative interface of Tomcat. One can do a lot, if one knows +what commands to call. You should be cautious when enabling the +manager-jmx role.

    + +

    To enable access to the Manager web application, you must either create +a new username/password combination and associate one of the +manager-xxx roles with it, or add a +manager-xxx role +to some existing username/password combination. +As the majority of this document describes the using the text interface, this +example will use the role name manager-script. +Exactly how the usernames/passwords are configured depends on which +Realm implementation you are using:

    +
      +
    • UserDatabaseRealm plus MemoryUserDatabase, or MemoryRealm + — The UserDatabaseRealm and MemoryUserDatabase are + configured in the default $CATALINA_BASE/conf/server.xml. + Both MemoryUserDatabase and MemoryRealm read an + XML-format file by default stored at + $CATALINA_BASE/conf/tomcat-users.xml, which can be + edited with any text editor. This file contains an XML + <user> for each individual user, which might + look something like this: +]]> + which defines the username and password used by this individual to + log on, and the role names they are associated with. You can + add the manager-script role to the comma-delimited + roles attribute for one or more existing users, and/or + create new users with that assigned role.
    • +
    • DataSourceRealm + — Your user and role information is stored in + a database accessed via JDBC. Add the manager-script role + to one or more existing users, and/or create one or more new users + with this role assigned, following the standard procedures for your + environment.
    • +
    • JNDIRealm — Your user and role information is stored in + a directory server accessed via LDAP. Add the + manager-script role to one or more existing users, + and/or create one or more new users with this role assigned, following + the standard procedures for your environment.
    • +
    + +

    The first time you attempt to issue one of the Manager commands +described in the next section, you will be challenged to log on using +BASIC authentication. The username and password you enter do not matter, +as long as they identify a valid user in the users database who possesses +the role manager-script.

    + +

    In addition to the password restrictions, access to the Manager web +application can be restricted by the remote IP address or host +by adding a RemoteAddrValve or RemoteHostValve. +See valves documentation +for details. Here is +an example of restricting access to the localhost by IP address:

    + + +]]> + +
    + +
    + +

    The user-friendly HTML interface of Manager web application is located at

    + +http://{host}:{port}/manager/html + +

    As has already been mentioned above, you need manager-gui +role to be allowed to access it. There is a separate document that provides +help on this interface. See:

    + + + +

    The HTML interface is protected against CSRF (Cross-Site Request Forgery) +attacks. Each access to the HTML pages generates a random token, which is +stored in your session and is included in all links on the page. If your next +action does not have correct value of the token, the action will be denied. +If the token has expired you can start again from the main page or +List Applications page of Manager.

    + +
    + +
    + +

    All commands that the Manager application knows how to process are +specified in a single request URI like this:

    +http://{host}:{port}/manager/text/{command}?{parameters} +

    where {host} and {port} represent the hostname +and port number on which Tomcat is running, {command} +represents the Manager command you wish to execute, and +{parameters} represents the query parameters +that are specific to that command. In the illustrations below, customize +the host and port appropriately for your installation.

    + +

    The commands are usually executed by HTTP GET requests. The +/deploy command has a form that is executed by an HTTP PUT request.

    + + + +

    Most commands accept one or more of the following query parameters:

    +
      +
    • path - The context path (including the leading slash) + of the web application you are dealing with. To select the ROOT web + application, specify "/". +
      + NOTE: It is not possible to perform administrative commands + on the Manager application itself. +
      + NOTE: If the path parameter is not explicitly specified + then the path and the version will be derived using the standard + Context naming rules from the + config parameter or, if the config parameter is not present, the war + parameter.
    • +
    • version - The version of this web application as used by + the parallel deployment feature. If you + use parallel deployment wherever a path is required you must specify a + version in addition to the path and it is the combination of path and + version that must be unique rather than just the path. +
      + NOTE: If the path is not explicitly specified, the version + parameter is ignored.
    • +
    • war - URL of a web application archive (WAR) file, or + pathname of a directory which contains the web application, or a + Context configuration ".xml" file. You can use URLs in any of the + following formats: +
        +
      • file:/absolute/path/to/a/directory - The absolute + path of a directory that contains the unpacked version of a web + application. This directory will be attached to the context path + you specify without any changes.
      • +
      • file:/absolute/path/to/a/webapp.war - The absolute + path of a web application archive (WAR) file. This is valid + only for the /deploy command, and is + the only acceptable format to that command.
      • +
      • file:/absolute/path/to/a/context.xml - The + absolute path of a web application Context configuration ".xml" + file which contains the Context configuration element.
      • +
      • directory - The directory name for the web + application context in the Host's application base directory.
      • +
      • webapp.war - The name of a web application war file + located in the Host's application base directory.
      • +
    • +
    + +

    Each command will return a response in text/plain format +(i.e. plain ASCII with no HTML markup), making it easy for both humans and +programs to read). The first line of the response will begin with either +OK or FAIL, indicating whether the requested +command was successful or not. In the case of failure, the rest of the first +line will contain a description of the problem that was encountered. Some +commands include additional lines of information as described below.

    + +

    Internationalization Note - The Manager application looks up +its message strings in resource bundles, so it is possible that the strings +have been translated for your platform. The examples below show the English +version of the messages.

    + +
    + + + +http://localhost:8080/manager/text/deploy?path=/foo + +

    Upload the web application archive (WAR) file that is specified as the +request data in this HTTP PUT request, install it into the appBase +directory of our corresponding virtual host, and start, deriving the name for +the WAR file added to the appBase from the specified path. The +application can later be undeployed (and the corresponding WAR file removed) by +use of the /undeploy command.

    + +

    This command is executed by an HTTP PUT request.

    + +

    The .WAR file may include Tomcat specific deployment configuration, by +including a Context configuration XML file in +/META-INF/context.xml.

    + +

    URL parameters include:

    +
      +
    • update: When set to true, any existing update will be + undeployed first. The default value is set to false.
    • +
    • tag: Specifying a tag name, this allows associating the + deployed webapp with a tag or label. If the web application is undeployed, + it can be later redeployed when needed using only the tag.
    • +
    • config : URL of a Context configuration ".xml" file in the + format file:/absolute/path/to/a/context.xml. This must be + the absolute path of a web application Context configuration ".xml" file + which contains the Context configuration element.
    • +
    + +

    NOTE - This command is the logical +opposite of the /undeploy command.

    + +

    If installation and startup is successful, you will receive a response +like this:

    +OK - Deployed application at context path /foo + +

    Otherwise, the response will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Application already exists at path /foo +

      The context paths for all currently running web applications must be + unique. Therefore, you must undeploy the existing web + application using this context path, or choose a different context path + for the new one. The update parameter may be specified as + a parameter on the URL, with a value of true to avoid this + error. In that case, an undeploy will be performed on an existing + application before performing the deployment.

      +
    • +
    • Encountered exception +

      An exception was encountered trying to start the new web application. + Check the Tomcat logs for the details, but likely explanations include + problems parsing your /WEB-INF/web.xml file, or missing + classes encountered when initializing application event listeners and + filters.

      +
    • +
    + +
    + + + +

    Deploy and start a new web application, attached to the specified context +path (which must not be in use by any other web application). +This command is the logical opposite of the /undeploy command.

    + +

    This command is executed by an HTTP GET request. +There are a number of different ways the deploy command can be used.

    + + + +http://localhost:8080/manager/text/deploy?path=/footoo&tag=footag + +

    This can be used to deploy a previously deployed web application, which +has been deployed using the tag attribute. Note that the work +directory of the Manager webapp will contain the previously deployed WARs; +removing it would make the deployment fail.

    + +
    + + + +

    Deploy a web application directory or ".war" file located on the Tomcat +server. If no path is specified, the path and version are derived +from the directory name or the war file name. The war parameter +specifies a URL (including the file: scheme) for either +a directory or a web application archive (WAR) file. The supported syntax for +a URL referring to a WAR file is described on the Javadocs page for the +java.net.JarURLConnection class. Use only URLs that refer to +the entire WAR file.

    + +

    In this example the web application located in the directory +/path/to/foo on the Tomcat server is deployed as the +web application context named /footoo.

    +http://localhost:8080/manager/text/deploy?path=/footoo&war=file:/path/to/foo + + +

    In this example the ".war" file /path/to/bar.war on the +Tomcat server is deployed as the web application context named +/bar. Notice that there is no path parameter +so the context path defaults to the name of the web application archive +file without the ".war" extension.

    +http://localhost:8080/manager/text/deploy?war=file:/path/to/bar.war + +
    + + + +

    Deploy a web application directory or ".war" file located in your Host +appBase directory. The path and optional version are derived from the directory +or war file name.

    + +

    In this example the web application located in a sub directory named +foo in the Host appBase directory of the Tomcat server is +deployed as the web application context named /foo. Notice +that the context path used is the name of the web application directory.

    +http://localhost:8080/manager/text/deploy?war=foo + + +

    In this example the ".war" file bar.war located in your +Host appBase directory on the Tomcat server is deployed as the web +application context named /bar.

    +http://localhost:8080/manager/text/deploy?war=bar.war + +
    + + + +

    If the Host deployXML flag is set to true you can deploy a web +application using a Context configuration ".xml" file and an optional +".war" file or web application directory. The context path +is not used when deploying a web application using a context ".xml" +configuration file.

    + +

    A Context configuration ".xml" file can contain valid XML for a +web application Context just as if it were configured in your +Tomcat server.xml configuration file. Here is an +example:

    + +]]> + + +

    When the optional war parameter is set to the URL +for a web application ".war" file or directory it overrides any +docBase configured in the context configuration ".xml" file.

    + +

    Here is an example of deploying an application using a Context +configuration ".xml" file.

    +http://localhost:8080/manager/text/deploy?config=file:/path/context.xml + + +

    Here is an example of deploying an application using a Context +configuration ".xml" file and a web application ".war" file located +on the server.

    +http://localhost:8080/manager/text/deploy + ?config=file:/path/context.xml&war=file:/path/bar.war + +
    + + + +

    If the Host is configured with unpackWARs=true and you deploy a war +file, the war will be unpacked into a directory in your Host appBase +directory.

    + +

    If the application war or directory is installed in your Host appBase +directory and either the Host is configured with autoDeploy=true or the +Context path must match the directory name or war file name without the +".war" extension.

    + +

    For security when untrusted users can manage web applications, the +Host deployXML flag can be set to false. This prevents untrusted users +from deploying web applications using a configuration XML file and +also prevents them from deploying application directories or ".war" +files located outside of their Host appBase.

    + +
    + + + +

    If installation and startup is successful, you will receive a response +like this:

    +OK - Deployed application at context path /foo + +

    Otherwise, the response will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Application already exists at path /foo +

      The context paths for all currently running web applications must be + unique. Therefore, you must undeploy the existing web + application using this context path, or choose a different context path + for the new one. The update parameter may be specified as + a parameter on the URL, with a value of true to avoid this + error. In that case, an undeploy will be performed on an existing + application before performing the deployment.

      +
    • +
    • Document base does not exist or is not a readable directory +

      The URL specified by the war parameter must identify a + directory on this server that contains the "unpacked" version of a + web application, or the absolute URL of a web application archive (WAR) + file that contains this application. Correct the value specified by + the war parameter.

      +
    • +
    • Encountered exception +

      An exception was encountered trying to start the new web application. + Check the Tomcat logs for the details, but likely explanations include + problems parsing your /WEB-INF/web.xml file, or missing + classes encountered when initializing application event listeners and + filters.

      +
    • +
    • Invalid application URL was specified +

      The URL for the directory or web application that you specified + was not valid. Such URLs must start with file:, and URLs + for a WAR file must end in ".war".

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character. To reference the + ROOT web application use "/".

      +
    • +
    • Context path must match the directory or WAR file name: +

      If the application war or directory is installed in your Host appBase + directory and either the Host is configured with autoDeploy=true the + Context path must match the directory name or war file name without + the ".war" extension.

      +
    • +
    • Only web applications in the Host web application directory can + be installed +

      + If the Host deployXML flag is set to false this error will happen + if an attempt is made to deploy a web application directory or + ".war" file outside of the Host appBase directory. +

    • +
    + +
    +
    + + + +http://localhost:8080/manager/text/list + +

    List the context paths, current status (running or +stopped), and number of active sessions for all currently +deployed web applications. A typical response immediately +after starting Tomcat might look like this:

    +OK - Listed applications for virtual host localhost +/webdav:running:0:webdav +/examples:running:0:examples +/manager:running:0:manager +/:running:0:ROOT +/test:running:0:test##2 +/test:running:0:test##1 + +
    + + + +http://localhost:8080/manager/text/reload?path=/examples + +

    Signal an existing application to shut itself down and reload. This can +be useful when the web application context is not reloadable and you have +updated classes or property files in the /WEB-INF/classes +directory or when you have added or updated jar files in the +/WEB-INF/lib directory. +

    + +

    If this command succeeds, you will see a response like this:

    +OK - Reloaded application at context path /examples + +

    Otherwise, the response will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to restart the web application. + Check the Tomcat logs for the details.

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character. To reference the + ROOT web application use "/".

      +
    • +
    • No context exists for path /foo +

      There is no deployed application on the context path + that you specified.

      +
    • +
    • No context path was specified +

      + The path parameter is required. +

    • +
    • Reload not supported on WAR deployed at path /foo +

      + Currently, application reloading (to pick up changes to the classes or + web.xml file) is not supported when a web application is + deployed directly from a WAR file. It only works when the web application + is deployed from an unpacked directory. If you are using a WAR file, + you should undeploy and then deploy or + deploy with the update parameter the + application again to pick up your changes. +

    • +
    + +
    + + + +http://localhost:8080/manager/text/serverinfo + +

    Lists information about the Tomcat version, OS, and JVM properties.

    + +

    If an error occurs, the response will start with FAIL and +include an error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to enumerate the system properties. + Check the Tomcat logs for the details.

      +
    • +
    + +
    + + + +http://localhost:8080/manager/text/resources[?type=xxxxx] + +

    List the global JNDI resources that are available for use in resource +links for context configuration files. If you specify the type +request parameter, the value must be the fully qualified Java class name of +the resource type you are interested in (for example, you would specify +javax.sql.DataSource to acquire the names of all available +JDBC data sources). If you do not specify the type request +parameter, resources of all types will be returned.

    + +

    Depending on whether the type request parameter is specified +or not, the first line of a normal response will be:

    +OK - Listed global resources of all types +

    or

    +OK - Listed global resources of type xxxxx +

    followed by one line for each resource. Each line is composed of fields +delimited by colon characters (":"), as follows:

    +
      +
    • Global Resource Name - The name of this global JNDI resource, + which would be used in the global attribute of a + <ResourceLink> element.
    • +
    • Global Resource Type - The fully qualified Java class name of + this global JNDI resource.
    • +
    + +

    If an error occurs, the response will start with FAIL and +include an error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to enumerate the global JNDI + resources. Check the Tomcat logs for the details.

      +
    • +
    • No global JNDI resources are available +

      The Tomcat server you are running has been configured without + global JNDI resources.

      +
    • +
    + + +
    + + + +http://localhost:8080/manager/text/sessions?path=/examples + +

    Display the default session timeout for a web application, and the +number of currently active sessions that fall within one-minute ranges of +their actual timeout times. For example, after restarting Tomcat and then +executing one of the JSP samples in the /examples web app, +you might get something like this:

    + + + +
    + + + +http://localhost:8080/manager/text/expire?path=/examples&idle=num + +

    Display the session statistics (like the above /sessions +command) and expire sessions that are idle for longer than num +minutes. To expire all sessions, use &idle=0 .

    + +0 minutes: 2 sessions were expired]]> + +

    Actually /sessions and /expire are synonyms for +the same command. The difference is in the presence of idle +parameter.

    + +
    + + + +http://localhost:8080/manager/text/start?path=/examples + +

    Signal a stopped application to restart, and make itself available again. +Stopping and starting is useful, for example, if the database required by +your application becomes temporarily unavailable. It is usually better to +stop the web application that relies on this database rather than letting +users continuously encounter database exceptions.

    + +

    If this command succeeds, you will see a response like this:

    +OK - Started application at context path /examples + +

    Otherwise, the response will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to start the web application. + Check the Tomcat logs for the details.

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character. To reference the + ROOT web application use "/".

      +
    • +
    • No context exists for path /foo +

      There is no deployed application on the context path + that you specified.

      +
    • +
    • No context path was specified +

      + The path parameter is required. +

    • +
    + +
    + + + +http://localhost:8080/manager/text/stop?path=/examples + +

    Signal an existing application to make itself unavailable, but leave it +deployed. Any request that comes in while an application is +stopped will see an HTTP error 404, and this application will show as +"stopped" on a list applications command.

    + +

    If this command succeeds, you will see a response like this:

    +OK - Stopped application at context path /examples + +

    Otherwise, the response will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to stop the web application. + Check the Tomcat logs for the details.

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character. To reference the + ROOT web application use "/".

      +
    • +
    • No context exists for path /foo +

      There is no deployed application on the context path + that you specified.

      +
    • +
    • No context path was specified + The path parameter is required. +
    • +
    + +
    + + + + +http://localhost:8080/manager/text/undeploy?path=/examples + +

    WARNING - This command will delete any web +application artifacts that exist within appBase directory +(typically "webapps") for this virtual host. +This will delete the application .WAR, if present, +the application directory resulting either from a deploy in unpacked form +or from .WAR expansion as well as the XML Context definition from +$CATALINA_BASE/conf/[enginename]/[hostname]/ directory. +If you simply want to take an application +out of service, you should use the /stop command instead.

    + +

    Signal an existing application to gracefully shut itself down, and +remove it from Tomcat (which also makes this context path available for +reuse later). In addition, the document root directory is removed, if it +exists in the appBase directory (typically "webapps") for +this virtual host. This command is the logical opposite of the +/deploy command.

    + +

    If this command succeeds, you will see a response like this:

    +OK - Undeployed application at context path /examples + +

    Otherwise, the response will start with FAIL and include an +error message. Possible causes for problems include:

    +
      +
    • Encountered exception +

      An exception was encountered trying to undeploy the web application. + Check the Tomcat logs for the details.

      +
    • +
    • Invalid context path was specified +

      The context path must start with a slash character. To reference the + ROOT web application use "/".

      +
    • +
    • No context exists named /foo +

      There is no deployed application with the name that you specified.

      +
    • +
    • No context path was specified + The path parameter is required. +
    • +
    + +
    + + + +http://localhost:8080/manager/text/findleaks[?statusLine=[true|false]] + +

    The find leaks diagnostic triggers a full garbage collection. It +should be used with extreme caution on production systems.

    + +

    The find leaks diagnostic attempts to identify web applications that have +caused memory leaks when they were stopped, reloaded or undeployed. Results +should always be confirmed +with a profiler. The diagnostic uses additional functionality provided by the +StandardHost implementation. It will not work if a custom host is used that +does not extend StandardHost.

    + +

    Explicitly triggering a full garbage collection from Java code is documented +to be unreliable. Furthermore, depending on the JVM used, there are options to +disable explicit GC triggering, like -XX:+DisableExplicitGC. +If you want to make sure, that the diagnostics were successfully running a full +GC, you will need to check using tools like GC logging, JConsole or similar.

    + +

    If this command succeeds, you will see a response like this:

    +/leaking-webapp + +

    If you wish to see a status line included in the response then include the +statusLine query parameter in the request with a value of +true.

    + +

    Each context path for a web application that was stopped, reloaded or +undeployed, but which classes from the previous runs are still loaded in memory, +thus causing a memory leak, will be listed on a new line. If an application +has been reloaded several times, it may be listed several times.

    + +

    If the command does not succeed, the response will start with +FAIL and include an error message.

    + +
    + + + +http://localhost:8080/manager/text/sslConnectorCiphers + +

    The SSL Connector/Ciphers diagnostic lists the SSL/TLS ciphers that are currently +configured for each connector.

    + +

    The response will look something like this:

    +OK - Connector / SSL Cipher information +Connector[HTTP/1.1-8080] + SSL is not enabled for this connector +Connector[HTTP/1.1-8443] + TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA + TLS_DHE_RSA_WITH_AES_128_CBC_SHA + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA + ... + +
    + + + +http://localhost:8080/manager/text/sslConnectorCerts + +

    The SSL Connector/Certs diagnostic lists the certificate chain that is +currently configured for each virtual host.

    + +

    The response will look something like this:

    +OK - Connector / Certificate Chain information +Connector[HTTP/1.1-8080] +SSL is not enabled for this connector +Connector[HTTP/1.1-8443]-_default_-RSA +[ +[ + Version: V3 + Subject: CN=localhost, OU=Apache Tomcat PMC, O=The Apache Software Foundation, L=Wakefield, ST=MA, C=US + Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11 + ... + +
    + + + +http://localhost:8080/manager/text/sslConnectorTrustedCerts + +

    The SSL Connector/Certs diagnostic lists the trusted certificates that are +currently configured for each virtual host.

    + +

    The response will look something like this:

    +OK - Connector / Trusted Certificate information +Connector[HTTP/1.1-8080] +SSL is not enabled for this connector +Connector[HTTP/1.1-8443]-_default_ +[ +[ + Version: V3 + Subject: CN=Apache Tomcat Test CA, OU=Apache Tomcat PMC, O=The Apache Software Foundation, L=Wakefield, ST=MA, C=US + ... + +
    + + + +http://localhost:8080/manager/text/sslReload?tlsHostName=name + +

    Reload the TLS configuration files (the certificate and key files, this does +not trigger a re-parsing of server.xml). To reload the files for all hosts don't +specify the tlsHostName parameter.

    + + + +
    + + + +http://localhost:8080/manager/text/threaddump + +

    Write a JVM thread dump.

    + +

    The response will look something like this:

    + + +
    + + + +http://localhost:8080/manager/text/vminfo + +

    Write some diagnostic information about Java Virtual Machine.

    + +

    The response will look something like this:

    + + +
    + + + +http://localhost:8080/manager/text/save + +

    If specified without any parameters, this command saves the current +configuration of the server to server.xml. The existing file will be renamed as +a backup if required.

    + +

    If specified with a path parameter that matches the path of +a deployed web application then the configuration for that web application will +be saved to an appropriately named context.xml file in the xmlBase +for the current Host.

    + +

    To use the command a StoreConfig MBean must be present. Typically this is +configured using the StoreConfigLifecycleListener. +

    + +

    If the command does not succeed, the response will start with +FAIL and include an error message.

    + +
    + +
    + +
    + +

    From the following links you can view Status information about the server. +Any one of manager-xxx roles allows access to this page.

    + +http://localhost:8080/manager/status +http://localhost:8080/manager/status/all + +

    Displays server status information in HTML format.

    + +http://localhost:8080/manager/status?XML=true +http://localhost:8080/manager/status/all?XML=true + +

    Displays server status information in XML format.

    + +http://localhost:8080/manager/status?JSON=true +http://localhost:8080/manager/status/all?JSON=true + +

    Displays server status information in JSON format.

    + +

    First, you have the server and JVM version number, JVM provider, OS name +and number followed by the architecture type.

    + +

    Second, there is information about the memory usage of the JVM.

    + +

    Then, there is information about the Tomcat AJP and HTTP connectors. +The same information is available for both of them : +

    +
      +
    • Threads information : Max threads, min and max spare threads, + current thread count and current thread busy.

    • +
    • Request information : Max processing time and processing time, + request and error count, bytes received and sent.

    • +
    • A table showing Stage, Time, Bytes Sent, Bytes Receive, Client, + VHost and Request. All existing threads are listed in the table. + Here is the list of the possible thread stages :

      +
        +
      • "Parse and Prepare Request" : The request headers are + being parsed or the necessary preparation to read the request body (if + a transfer encoding has been specified) is taking place.

      • +
      • "Service" : The thread is processing a request and + generating the response. This stage follows the "Parse and Prepare + Request" stage and precedes the "Finishing" stage. There is always at + least one thread in this stage (the server-status page).

      • +
      • "Finishing" : The end of the request processing. Any + remainder of the response still in the output buffers is sent to the + client. This stage is followed by "Keep-Alive" if it is appropriate to + keep the connection alive or "Ready" if "Keep-Alive" is not + appropriate.

      • +
      • "Keep-Alive" : The thread keeps the connection open to + the client in case the client sends another request. If another request + is received, the next stage will be "Parse and Prepare Request". If no + request is received before the keep alive times out, the connection will + be closed and the next stage will be "Ready".

      • +
      • "Ready" : The thread is at rest and ready to be + used.

      • +
      +
    • +
    + +

    If you are using /status/all command, additional information +on each of deployed web applications will be available.

    + +
    + +
    + + + The JMX Proxy Servlet is a lightweight proxy to get and set the + tomcat internals. (Or any class that has been exposed via an MBean) + Its usage is not very user friendly but the UI is + extremely helpful for integrating command line scripts for monitoring + and changing the internals of tomcat. You can do two things with the proxy: + get information and set information. For you to really understand the + JMX Proxy Servlet, you should have a general understanding of JMX. + If you don't know what JMX is, then prepare to be confused. + + + +

    This takes the form:

    +http://webserver/manager/jmxproxy/?qry=STUFF +

    Where STUFF is the JMX query you wish to perform. For example, + here are some queries you might wish to run:

    +
      +
    • + qry=*%3Atype%3DRequestProcessor%2C* --> + type=RequestProcessor which will locate all + workers which can process requests and report + their state. +
    • +
    • + qry=*%3Aj2eeType=Servlet%2c* --> + j2eeType=Servlet which return all loaded servlets. +
    • +
    • + qry=Catalina%3Atype%3DEnvironment%2Cresourcetype%3DGlobal%2Cname%3DsimpleValue --> + Catalina:type=Environment,resourcetype=Global,name=simpleValue + which look for a specific MBean by the given name. +
    • +
    +

    + You'll need to experiment with this to really understand its capabilities + If you provide no qry parameter, then all of the MBeans will + be displayed. We really recommend looking at the tomcat source code and + understand the JMX spec to get a better understanding of all the queries + you may run. +

    +
    + + +

    + The JMXProxyServlet also supports a "get" command that you can use to + fetch the value of a specific MBean's attribute. The general form of + the get command is: +

    + +http://webserver/manager/jmxproxy/?get=BEANNAME&att=MYATTRIBUTE&key=MYKEY + +

    You must provide the following parameters:

    +
      +
    1. get: The full bean name
    2. +
    3. att: The attribute you wish to fetch
    4. +
    5. key: (optional) The key into a CompositeData MBean attribute
    6. +
    +

    + If all goes well, then it will say OK, otherwise an error message will + be shown. For example, let's say we wish to fetch the current heap memory + data: +

    + +http://webserver/manager/jmxproxy/?get=java.lang:type=Memory&att=HeapMemoryUsage + +

    Or, if you only want the "used" key:

    + +http://webserver/manager/jmxproxy/ + ?get=java.lang:type=Memory&att=HeapMemoryUsage&key=used +
    + + +

    + Now that you can query an MBean, its time to muck with Tomcat's internals! + The general form of the set command is : +

    +http://webserver/manager/jmxproxy/?set=BEANNAME&att=MYATTRIBUTE&val=NEWVALUE +

    So you need to provide 3 request parameters:

    +
      +
    1. set: The full bean name
    2. +
    3. att: The attribute you wish to alter
    4. +
    5. val: The new value
    6. +
    +

    + If all goes ok, then it will say OK, otherwise an error message will be + shown. For example, lets say we wish to turn up debugging on the fly for the + ErrorReportValve. The following will set debugging to 10. +

    +http://localhost:8080/manager/jmxproxy/ + ?set=Catalina%3Atype%3DValve%2Cname%3DErrorReportValve%2Chost%3Dlocalhost + &att=debug&val=10 +

    and my result is (YMMV):

    +Result: ok + +

    Here is what I see if I pass in a bad value. Here is the URL I used, + I try set debugging equal to 'cow':

    +http://localhost:8080/manager/jmxproxy/ + ?set=Catalina%3Atype%3DValve%2Cname%3DErrorReportValve%2Chost%3Dlocalhost + &att=debug&val=cow +

    When I try that, my result is

    +Error: java.lang.NumberFormatException: For input string: "cow" +
    + + +

    The invoke command enables methods to be called on MBeans. The + general form of the command is:

    +http://webserver/manager/jmxproxy/ + ?invoke=BEANNAME&op=METHODNAME&ps=COMMASEPARATEDPARAMETERS +

    For example, to call the findConnectors() method of the + Service use:

    +http://localhost:8080/manager/jmxproxy/ + ?invoke=Catalina%3Atype%3DService&op=findConnectors&ps= +
    +
    + +
    + +

    In addition to the ability to execute Manager commands via HTTP requests, +as documented above, Tomcat includes a convenient set of Task definitions +for the Ant (version 1.4 or later) build tool. In order to use these +commands, you must perform the following setup operations:

    +
      +
    • Download the binary distribution of Ant from + https://ant.apache.org. + You must use version 1.4 or later.
    • +
    • Install the Ant distribution in a convenient directory (called + ANT_HOME in the remainder of these instructions).
    • +
    • Add the $ANT_HOME/bin directory to your PATH + environment variable.
    • +
    • Configure at least one username/password combination in your Tomcat + user database that includes the manager-script role.
    • +
    + +

    To use custom tasks within Ant, you must declare them first with an +<import> element. Therefore, your build.xml +file might look something like this:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + +

    Note: The definition of the resources task via the import above will override +the resources datatype added in Ant 1.7. If you wish to use the resources +datatype you will need to use Ant's namespace support to modify +catalina-tasks.xml to assign the Tomcat tasks to their own +namespace.

    + +

    Now, you can execute commands like ant deploy to deploy the +application to a running instance of Tomcat, or ant reload to +tell Tomcat to reload it. Note also that most of the interesting values in +this build.xml file are defined as replaceable properties, so +you can override their values from the command line. For example, you might +consider it a security risk to include the real manager password in your +build.xml file's source code. To avoid this, omit the password +property, and specify it from the command line:

    +ant -Dpassword=secret deploy + + + +

    Using Ant version 1.6.2 or later, +the Catalina tasks offer the option to capture their output in +properties or external files. They support directly the following subset of the +<redirector> type attributes: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionRequired
    outputName of a file to which to write the output. If +the error stream is not also redirected to a file or property, it will +appear in this output.No
    errorThe file to which the standard error of the +command should be redirected.No
    logErrorThis attribute is used when you wish to see +error output in Ant's log and you are redirecting output to a +file/property. The error output will not be included in the output +file/property. If you redirect error with the error or errorProperty +attributes, this will have no effect.No
    appendWhether output and error files should be +appended to or overwritten. Defaults to false.No
    createemptyfilesWhether output and error files should be created +even when empty. Defaults to true.No
    outputpropertyThe name of a property in which the output of +the command should be stored. Unless the error stream is redirected to +a separate file or stream, this property will include the error output.No
    errorpropertyThe name of a property in which the standard +error of the command should be stored.No
    + +

    A couple of additional attributes can also be specified: +

    + + + + + + + + + + + + + + + + +
    AttributeDescriptionRequired
    alwaysLogThis attribute is used when you wish to see the +output you are capturing, appearing also in the Ant's log. It must not be +used unless you are capturing task output. +Defaults to false. +This attribute will be supported directly by <redirector> +in Ant 1.6.3No
    failonerrorThis attribute is used when you wish to avoid that +any manager command processing error terminates the ant execution. Defaults to true. +It must be set to false, if you want to capture error output, +otherwise execution will terminate before anything can be captured. +
    +This attribute acts only on manager command execution, +any wrong or missing command attribute will still cause Ant execution termination. +
    No
    + +

    They also support the embedded <redirector> element +in which you can specify +its full set of attributes, but input, inputstring and +inputencoding that, even if accepted, are not used because they have +no meaning in this context. +Refer to ant manual for details on +<redirector> element attributes. +

    + +

    +Here is a sample build file extract that shows how this output redirection support +can be used: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]]> + +

    WARNING: even if it doesn't make many sense, and is always a bad idea, +calling a Catalina task more than once, +badly set Ant tasks depends chains may cause that a task be called +more than once in the same Ant run, even if not intended to. A bit of caution should be exercised when you are +capturing output from that task, because this could lead to something unexpected:

    +
      +
    • when capturing in a property you will find in it only the output from the first call, because +Ant properties are immutable and once set they cannot be changed, +
    • +
    • when capturing in a file, each run will overwrite it and you will find in it only the last call +output, unless you are using the append="true" attribute, in which case you will +see the output of each task call appended to the file. +
    • +
    + +
    + +
    + + + + +
    diff --git a/webapps/docs/maven-jars.xml b/webapps/docs/maven-jars.xml new file mode 100644 index 0000000..7320452 --- /dev/null +++ b/webapps/docs/maven-jars.xml @@ -0,0 +1,59 @@ + + + +]> + + + &project; + + + Filip Hanik + Apache Tomcat - Using Tomcat libraries with Maven + + + + +
    + +
    + +
    + + Tomcat snapshots are located in the + Apache Snapshot Repository. + The official URL is https://repository.apache.org/content/repositories/snapshots/org/apache/tomcat/ +

    + Snapshots are done periodically, not on a regular basis, but when changes happen and the Tomcat team deems a new snapshot might + useful. +

    +
    + +

    + Stable releases are published to the + Maven + Central Repository. The URL for this is +

    + https://repo.maven.apache.org/maven2/org/apache/tomcat/ +
    + +
    + + + +
    diff --git a/webapps/docs/mbeans-descriptors-howto.xml b/webapps/docs/mbeans-descriptors-howto.xml new file mode 100644 index 0000000..d9a7be2 --- /dev/null +++ b/webapps/docs/mbeans-descriptors-howto.xml @@ -0,0 +1,86 @@ + + + +]> + + + &project; + + + Amy Roh + MBeans Descriptors How To + + + + +
    + +
    + +
    + +

    Tomcat uses JMX MBeans as the technology for implementing +manageability of Tomcat.

    + +

    The descriptions of JMX MBeans for Catalina are in the mbeans-descriptors.xml +file in each package.

    + +

    You will need to add MBean descriptions for your custom components +in order to avoid a "ManagedBean is not found" exception.

    + +
    + +
    + +

    You may also add MBean descriptions for custom components in +an mbeans-descriptors.xml file, located in the same package as the class files +it describes.

    + +

    The permitted syntax for the mbeans-descriptors.xml is defined by +the DTD file.

    + +

    The entries for a custom LDAP authentication Realm may look like this:

    + + + + + + + . + . + . + + ]]> + + +
    + + + +
    diff --git a/webapps/docs/monitoring.xml b/webapps/docs/monitoring.xml new file mode 100644 index 0000000..42c8911 --- /dev/null +++ b/webapps/docs/monitoring.xml @@ -0,0 +1,1198 @@ + + + +]> + + + &project; + + + Monitoring and Managing Tomcat + + + + +
    + +
    + +
    + +

    Monitoring is a key aspect of system administration. Looking inside a + running server, obtaining some statistics or reconfiguring some aspects of + an application are all daily administration tasks.

    + +
    + +
    + +

    Note: This configuration is needed only if you are + going to monitor Tomcat remotely. It is not needed if you are going + to monitor it locally, using the same user that Tomcat runs with.

    + +

    The Oracle website includes the list of options and how to configure + JMX Remote on Java 11: + + http://docs.oracle.com/javase/6/docs/technotes/guides/management/agent.html. +

    +

    The following is a quick configuration guide for Java 11:

    +

    Add the following parameters to setenv.bat script of your + Tomcat (see RUNNING.txt for details).
    + Note: This syntax is for Microsoft Windows. The command has + to be on the same line. It is wrapped to be more readable. If Tomcat is + running as a Windows service, use its configuration dialog to set + java options for the service. + For Linux, MacOS, etc, remove "set " from beginning of the + line. +

    + +

    If you don't set com.sun.management.jmxremote.rmi.port then the +JSR 160 JMX-Adaptor will select a port at random which will may it difficult to +configure a firewall to allow access.

    + +

    If you require TLS:

    +
      +
    1. change and add this: +
    2. +
    3. to configure the protocols and/or cipher suites use: +
    4. +
    5. to client certificate authentication use: +
    6. +
    +

    If you require authorization (it is strongly recommended that TLS is always +used with authentication):

    +
      +
    1. change and add this: + +
    2. +
    3. edit the access authorization file $CATALINA_BASE/conf/jmxremote.access: + +
    4. +
    5. edit the password file $CATALINA_BASE/conf/jmxremote.password: + + Tip: The password file should be read-only and only accessible by the + operating system user Tomcat is running as. +
    6. +
    7. Alternatively, you can configure a JAAS login module with: +
    8. +
    + +

    If you need to specify a host name to be used in the RMI stubs sent to the +client (e.g. because the public host name that must be used to connect is not +the same as the local host name) then you can set:

    + + +

    If you need to specify a specific interface for the JMX service to bind to +then you can set:

    + + +
    + +
    +

    To simplify JMX usage with Ant, a set of tasks is provided that may + be used with antlib.

    +

    antlib: Copy your catalina-ant.jar from $CATALINA_HOME/lib to $ANT_HOME/lib.

    +

    The following example shows the JMX Accessor usage:
    + Note: The name attribute value was wrapped here to be + more readable. It has to be all on the same line, without spaces.

    + + + + + + + + + + + + + + + + + + + + + + + + + +senderObjectNames: ${senderObjectNames.0} +IDataSender.backup.connected: ${IDataSender.backup.connected} +session: ${sessions.0} +manager.length: ${manager.length} +manager.0.name: ${manager.0.name} +manager.1.name: ${manager.1.name} +hello: ${Hello} +manager.ClusterTest.0.name: ${manager.ClusterTest.0.name} +manager.ClusterTest.0.activeSessions: ${manager.ClusterTest.0.activeSessions} +manager.ClusterTest.0.counterSend_EVT_SESSION_EXPIRED: + ${manager.ClusterTest.0.counterSend_EVT_SESSION_EXPIRED} +manager.ClusterTest.0.counterSend_EVT_GET_ALL_SESSIONS: + ${manager.ClusterTest.0.counterSend_EVT_GET_ALL_SESSIONS} + + + + +]]> +

    import: Import the JMX Accessor Project with + <import file="${CATALINA.HOME}/bin/catalina-tasks.xml" /> and + reference the tasks with jmxOpen, jmxSet, jmxGet, + jmxQuery, jmxInvoke, jmxEquals and jmxCondition.

    + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    urlSet JMX connection URL - service:jmx:rmi:///jndi/rmi://localhost:8050/jmxrmi +
    hostSet the host, shortcut the very long URL syntax. + localhost
    portSet the remote connection port + 8050
    usernameremote JMX connection user name. +
    passwordremote JMX connection password. +
    refName of the internal connection reference. With this attribute you can + configure more the one connection inside the same Ant project. + jmx.server
    echoEcho the command usage (for access analysis or debugging) + false
    ifOnly execute if a property of the given name exists in the current project. +
    unlessOnly execute if a property of the given name not exists in the current project. +
    + +

    +Example to open a new JMX connection +

    +]]> + +

    +Example to open a JMX connection from URL, with authorization and +store at other reference +

    +]]> + +

    +Example to open a JMX connection from URL, with authorization and +store at other reference, but only when property jmx.if exists and +jmx.unless not exists +

    +]]> + +

    Note: All properties from jmxOpen task also exists at all +other tasks and conditions. +

    + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    nameFull qualified JMX ObjectName -- Catalina:type=Server +
    attributeExisting MBean attribute (see Tomcat MBean description above) +
    refJMX Connection reference + jmx.server
    echoEcho command usage (access and result) + false
    resultpropertySave result at this project property +
    delimiterSplit result with delimiter (java.util.StringTokenizer) + and use resultproperty as prefix to store tokens. +
    separatearrayresultsWhen return value is an array, save result as property list + ($resultproperty.[0..N] and $resultproperty.length) + true
    + +

    +Example to get remote MBean attribute from default JMX connection +

    +]]> + +

    +Example to get and result array and split it at separate properties +

    +]]> +

    +Access the senderObjectNames properties with: +

    + + + +

    +Example to get IDataSender attribute connected only when cluster is configured.
    +Note: The name attribute value was wrapped here to be +more readable. It has to be all on the same line, without spaces. +

    + + ]]> + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    nameFull qualified JMX ObjectName -- Catalina:type=Server +
    attributeExisting MBean attribute (see Tomcat MBean description above) +
    valuevalue that set to attribute +
    typetype of the attribute. + java.lang.String
    refJMX Connection reference + jmx.server
    echoEcho command usage (access and result) + false
    + +

    +Example to set remote MBean attribute value +

    +]]> + + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    nameFull qualified JMX ObjectName -- Catalina:type=Server +
    operationExisting MBean operation +
    refJMX Connection reference + jmx.server
    echoEcho command usage (access and result) + false
    resultpropertySave result at this project property +
    delimiterSplit result with delimiter (java.util.StringTokenizer) + and use resultproperty as prefix to store tokens. +
    separatearrayresultsWhen return value is an array, save result as property list + ($resultproperty.[0..N] and $resultproperty.length) + true
    + +

    +stop an application +

    +]]> +

    +Now you can find the sessionid at ${sessions.[0..N} properties and access the count +with ${sessions.length} property. +

    +

    +Example to get all sessionids +

    +]]> +

    +Now you can find the sessionid at ${sessions.[0..N} properties and access the count +with ${sessions.length} property. +

    +

    +Example to get remote MBean session attribute from session ${sessionid.0} +

    + + + + ]]> + +

    +Example to create a new access logger valve at vhost localhost +

    + + + ]]> +

    +Now you can find new MBean with name stored at ${accessLoggerObjectName} +property. +

    + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    nameJMX ObjectName query string -- Catalina:type=Manager,* +
    refJMX Connection reference + jmx.server
    echoEcho command usage (access and result) + false
    resultpropertyPrefix project property name to all founded MBeans (mbeans.[0..N].objectname) +
    attributebindingbind ALL MBean attributes in addition to name + false
    delimiterSplit result with delimiter (java.util.StringTokenizer) + and use resultproperty as prefix to store tokens. +
    separatearrayresultsWhen return value is an array, save result as property list + ($resultproperty.[0..N] and $resultproperty.length) + true
    + +

    +Get all Manager ObjectNames from all services and Hosts +

    +]]> +

    +Now you can find the Session Manager at ${manager.[0..N].name} +properties and access the result object counter with ${manager.length} property. +

    +

    +Example to get the Manager from servlet-examples application an bind all MBean properties +

    +]]> +

    +Now you can find the manager at ${manager.servletExamples.0.name} property +and can access all properties from this manager with ${manager.servletExamples.0.[manager attribute names]}. +The result object counter from MBeans is stored ad ${manager.length} property. +

    + +

    +Example to get all MBeans from a server and store inside an external XML property file +

    + + + + + + + + + + + + + + + + + + +]]> +

    +Now you can find all MBeans inside the file mbeans.properties. +

    + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    nameFull qualified JMX ObjectName -- Catalina:type=MBeanFactory +
    classNameExisting MBean full qualified class name (see Tomcat MBean description above) +
    classLoaderObjectName of server or web application classloader
    + ( Catalina:type=ServerClassLoader,name=[server,common,shared] or
    + Catalina:type=WebappClassLoader,context=/myapps,host=localhost) +
    refJMX Connection reference + jmx.server
    echoEcho command usage (access and result) + false
    + +

    +Example to create remote MBean +

    + + + ]]> + +

    + Warning: Many Tomcat MBeans can't be linked to their parent once
    + created. The Valve, Cluster and Realm MBeans are not automatically
    + connected with their parent. Use the MBeanFactory create
    + operation instead. +

    + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    nameFull qualified JMX ObjectName -- Catalina:type=MBeanFactory +
    refJMX Connection reference + jmx.server
    echoEcho command usage (access and result) + false
    + +

    +Example to unregister remote MBean +

    +]]> + +

    + Warning: A lot of Tomcat MBeans can't be unregister.
    + The MBeans are not unlinked from their parent. Use MBeanFactory
    + remove operation instead. +

    + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    urlSet JMX connection URL - service:jmx:rmi:///jndi/rmi://localhost:8050/jmxrmi +
    hostSet the host, shortcut the very long URL syntax. + localhost
    portSet the remote connection port + 8050
    usernameremote JMX connection user name. +
    passwordremote JMX connection password. +
    refName of the internal connection reference. With this attribute you can + configure more the one connection inside the same Ant project. + jmx.server
    nameFull qualified JMX ObjectName -- Catalina:type=Server +
    echoEcho condition usage (access and result) + false
    ifOnly execute if a property of the given name exists in the current project. +
    unlessOnly execute if a property of the given name not exists in the current project. +
    value (required)Second arg for operation +
    typeValue type to express operation (support long and double) + long
    operation express one +
      +
    • == equals
    • +
    • != not equals
    • +
    • > greater than (&gt;)
    • +
    • >= greater than or equals (&gt;=)
    • +
    • < lesser than (&lt;)
    • +
    • <= lesser than or equals (&lt;=)
    • +
    +
    ==
    + +

    +Wait for server connection and that cluster backup node is accessible +

    + + + + + + + + + + +]]> + +
    + + + +
    +

    +List of Attributes +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AttributeDescriptionDefault value
    urlSet JMX connection URL - service:jmx:rmi:///jndi/rmi://localhost:8050/jmxrmi +
    hostSet the host, shortcut the very long URL syntax. + localhost
    portSet the remote connection port + 8050
    usernameremote JMX connection user name. +
    passwordremote JMX connection password. +
    refName of the internal connection reference. With this attribute you can + configure more the one connection inside the same Ant project. + jmx.server
    nameFull qualified JMX ObjectName -- Catalina:type=Server +
    echoEcho condition usage (access and result) + false
    + +

    +Wait for server connection and that cluster backup node is accessible +

    + + + + + + + + + + +]]> + +
    + +
    + +

    + Tomcat offers an alternative to using remote (or even local) JMX + connections while still giving you access to everything JMX has to offer: + Tomcat's + JMXProxyServlet. +

    + +

    + The JMXProxyServlet allows a client to issue JMX queries via an HTTP + interface. This technique offers the following advantages over using + JMX directly from a client program: +

    + +
      +
    • You don't have to launch a full JVM and make a remote JMX connection + just to ask for one small piece of data from a running server
    • +
    • You don't have to know how to work with JMX connections
    • +
    • You don't need any of the complex configuration covered in the rest + of this page
    • +
    • Your client program does not have to be written in Java
    • +
    + +

    + A perfect example of JMX overkill can be seen in the case of popular + server-monitoring software such as Nagios or Icinga: if you want to + monitor 10 items via JMX, you will have to launch 10 JVMs, make 10 JMX + connections, and then shut them all down every few minutes. With the + JMXProxyServlet, you can make 10 HTTP connections and be done with it. +

    + +

    + You can find out more information about the JMXProxyServlet in the + documentation for the + Tomcat + manager. +

    +
    + +
    diff --git a/webapps/docs/project.xml b/webapps/docs/project.xml new file mode 100644 index 0000000..167f7d3 --- /dev/null +++ b/webapps/docs/project.xml @@ -0,0 +1,109 @@ + + + + + Apache Tomcat 10 + + + The Apache Tomcat Servlet/JSP Container + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/docs/proxy-howto.xml b/webapps/docs/proxy-howto.xml new file mode 100644 index 0000000..704dcd8 --- /dev/null +++ b/webapps/docs/proxy-howto.xml @@ -0,0 +1,145 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Proxy Support How-To + + + + +
    + +
    + +
    + +

    Using standard configurations of Tomcat, web applications can ask for +the server name and port number to which the request was directed for +processing. When Tomcat is running standalone with the +HTTP/1.1 Connector, it will generally +report the server name specified in the request, and the port number on +which the Connector is listening. The servlet API +calls of interest, for this purpose, are:

    +
      +
    • ServletRequest.getServerName(): Returns the host name of the server to which the request was sent.
    • +
    • ServletRequest.getServerPort(): Returns the port number of the server to which the request was sent.
    • +
    • ServletRequest.getLocalName(): Returns the host name of the Internet Protocol (IP) interface on which the request was received.
    • +
    • ServletRequest.getLocalPort(): Returns the Internet Protocol (IP) port number of the interface on which the request was received.
    • +
    + +

    When you are running behind a proxy server (or a web server that is +configured to behave like a proxy server), you will sometimes prefer to +manage the values returned by these calls. In particular, you will +generally want the port number to reflect that specified in the original +request, not the one on which the Connector itself is +listening. You can use the proxyName and proxyPort +attributes on the <Connector> element to configure +these values.

    + +

    Proxy support can take many forms. The following sections describe +proxy configurations for several common cases.

    + +
    + +
    + +

    Apache httpd 1.3 and later versions support an optional module +(mod_proxy) that configures the web server to act as a proxy +server. This can be used to forward requests for a particular web application +to a Tomcat instance, without having to configure a web connector such as +mod_jk. To accomplish this, you need to perform the following +tasks:

    +
      +
    1. Configure your copy of Apache so that it includes the + mod_proxy module. If you are building from source, + the easiest way to do this is to include the + --enable-module=proxy directive on the + ./configure command line.

    2. +
    3. If not already added for you, make sure that you are loading the + mod_proxy module at Apache startup time, by using the + following directives in your httpd.conf file:

      +
    4. +
    5. Include two directives in your httpd.conf file for + each web application that you wish to forward to Tomcat. For + example, to forward an application at context path /myapp:

      + +

      which tells Apache to forward URLs of the form + http://localhost/myapp/* to the Tomcat connector + listening on port 8081.

    6. +
    7. Configure your copy of Tomcat to include a special + <Connector> element, with appropriate + proxy settings, for example:

      +]]> +

      which will cause servlets inside this web application to think that + all proxied requests were directed to www.mycompany.com + on port 80.

    8. +
    9. It is legal to omit the proxyName attribute from the + <Connector> element. If you do so, the value + returned by request.getServerName() will by the host + name on which Tomcat is running. In the example above, it would be + localhost.

    10. +
    11. If you also have a <Connector> listening on port + 8080 (nested within the same Service + element), the requests to either port will share the same set of + virtual hosts and web applications.

    12. +
    13. You might wish to use the IP filtering features of your operating + system to restrict connections to port 8081 (in this example) to + be allowed only from the server that is running + Apache.

    14. +
    15. Alternatively, you can set up a series of web applications that are + only available via proxying, as follows:

      +
        +
      • Configure another <Service> that contains + only a <Connector> for the proxy port.
      • +
      • Configure appropriate Engine, + Host, and + Context elements for the virtual hosts + and web applications accessible via proxying.
      • +
      • Optionally, protect port 8081 with IP filters as described + earlier.
      • +
    16. +
    17. When requests are proxied by Apache, the web server will be recording + these requests in its access log. Therefore, you will generally want to + disable any access logging performed by Tomcat itself.

    18. +
    + +

    When requests are proxied in this manner, all requests +for the configured web applications will be processed by Tomcat (including +requests for static content). You can improve performance by using the +mod_jk web connector instead of mod_proxy. +mod_jk can be configured so that the web server serves static +content that is not processed by filters or security constraints defined +within the web application's deployment descriptor +(/WEB-INF/web.xml).

    + +
    + + + +
    diff --git a/webapps/docs/realm-howto.xml b/webapps/docs/realm-howto.xml new file mode 100644 index 0000000..c517b34 --- /dev/null +++ b/webapps/docs/realm-howto.xml @@ -0,0 +1,1127 @@ + + + +]> + + + &project; + + + Craig R. McClanahan + Yoav Shapira + Andrew R. Jaquith + Realm Configuration How-To + + + + +
    + +
    + +
    + +

    This document describes how to configure Tomcat to support container +managed security, by connecting to an existing "database" of usernames, +passwords, and user roles. You only need to care about this if you are using +a web application that includes one or more +<security-constraint> elements, and a +<login-config> element defining how users are required +to authenticate themselves. If you are not utilizing these features, you can +safely skip this document.

    + +

    For fundamental background information about container managed security, +see the Servlet +Specification (Version 2.4), Section 12.

    + +

    For information about utilizing the Single Sign On feature of +Tomcat (allowing a user to authenticate themselves once across the entire +set of web applications associated with a virtual host), see +here.

    + +
    + + +
    + + + + +

    A Realm is a "database" of usernames and passwords that +identify valid users of a web application (or set of web applications), plus +an enumeration of the list of roles associated with each valid user. +You can think of roles as similar to groups in Unix-like operating +systems, because access to specific web application resources is granted to +all users possessing a particular role (rather than enumerating the list of +associated usernames). A particular user can have any number of roles +associated with their username.

    + +

    Although the Servlet Specification describes a portable mechanism for +applications to declare their security requirements (in the +web.xml deployment descriptor), there is no portable API +defining the interface between a servlet container and the associated user +and role information. In many cases, however, it is desirable to "connect" +a servlet container to some existing authentication database or mechanism +that already exists in the production environment. Therefore, Tomcat +defines a Java interface (org.apache.catalina.Realm) that +can be implemented by "plug in" components to establish this connection. +Six standard plug-ins are provided, supporting connections to various +sources of authentication information:

    +
      +
    • DataSourceRealm - Accesses authentication + information stored in a relational database, accessed via a named JNDI + JDBC DataSource.
    • +
    • JNDIRealm - Accesses authentication information + stored in an LDAP based directory server, accessed via a JNDI provider. +
    • +
    • UserDatabaseRealm - Accesses authentication + information stored in an UserDatabase JNDI resource, which is typically + backed by an XML document (conf/tomcat-users.xml).
    • +
    • MemoryRealm - Accesses authentication + information stored in an in-memory object collection, which is initialized + from an XML document (conf/tomcat-users.xml).
    • +
    • JAASRealm - Accesses authentication information + through the Java Authentication & Authorization Service (JAAS) + framework.
    • +
    + +

    It is also possible to write your own Realm implementation, +and integrate it with Tomcat. To do so, you need to:

    +
      +
    • Implement org.apache.catalina.Realm,
    • +
    • Place your compiled realm in $CATALINA_HOME/lib,
    • +
    • Declare your realm as described in the "Configuring a Realm" section below,
    • +
    • Declare your realm to the MBeans Descriptors.
    • +
    + +
    + + + + +

    Before getting into the details of the standard Realm implementations, it is +important to understand, in general terms, how a Realm is configured. In +general, you will be adding an XML element to your conf/server.xml +configuration file, that looks something like this:

    + +]]> + +

    The <Realm> element can be nested inside any one of +of the following Container elements. The location of the +Realm element has a direct impact on the "scope" of that Realm +(i.e. which web applications will share the same authentication information): +

    +
      +
    • Inside an <Engine> element - This Realm will be shared + across ALL web applications on ALL virtual hosts, UNLESS it is overridden + by a Realm element nested inside a subordinate <Host> + or <Context> element.
    • +
    • Inside a <Host> element - This Realm will be shared across + ALL web applications for THIS virtual host, UNLESS it is overridden + by a Realm element nested inside a subordinate <Context> + element.
    • +
    • Inside a <Context> element - This Realm will be used ONLY + for THIS web application.
    • +
    + + +
    + + +
    + + +
    + + + + +

    For each of the standard Realm implementations, the +user's password (by default) is stored in clear text. In many +environments, this is undesirable because casual observers of the +authentication data can collect enough information to log on +successfully, and impersonate other users. To avoid this problem, the +standard implementations support the concept of digesting +user passwords. This allows the stored version of the passwords to be +encoded (in a form that is not easily reversible), but that the +Realm implementation can still utilize for +authentication.

    + +

    When a standard realm authenticates by retrieving the stored +password and comparing it with the value presented by the user, you +can select digested passwords by placing a +CredentialHandler element inside your <Realm> +element. An easy choice to support one of the algorithms SSHA, SHA or MD5 +would be the usage of the MessageDigestCredentialHandler. +This element must be configured to one of the digest algorithms supported +by the java.security.MessageDigest class (SSHA, SHA or MD5). +When you select this option, the contents of the password that is stored +in the Realm must be the cleartext version of the password, +as digested by the specified algorithm.

    + +

    When the authenticate() method of the Realm is called, the +(cleartext) password specified by the user is itself digested by the same +algorithm, and the result is compared with the value returned by the +Realm. An equal match implies that the cleartext version of the +original password is the same as the one presented by the user, so that this +user should be authorized.

    + +

    To calculate the digested value of a cleartext password, two convenience +techniques are supported:

    +
      +
    • If you are writing an application that needs to calculate digested + passwords dynamically, call the static Digest() method of the + org.apache.catalina.realm.RealmBase class, passing the + cleartext password, the digest algorithm name and the encoding as arguments. + This method will return the digested password.
    • +
    • If you want to execute a command line utility to calculate the digested + password, simply execute +CATALINA_HOME/bin/digest.[bat|sh] -a {algorithm} {cleartext-password} + and the digested version of this cleartext password will be returned to + standard output.
    • +
    + +

    If using digested passwords with DIGEST authentication, the cleartext used + to generate the digest is different and the digest must use one iteration of + the MD5 algorithm with no salt. In the examples above + {cleartext-password} must be replaced with + {username}:{realm}:{cleartext-password}. For example, in a + development environment this might take the form + testUser:Authentication required:testPassword. The value for + {realm} is taken from the <realm-name> + element of the web application's <login-config>. If + not specified in web.xml, the default value of Authentication + required is used.

    + +

    Usernames and/or passwords using encodings other than the platform default +are supported using

    +CATALINA_HOME/bin/digest.[bat|sh] -a {algorithm} -e {encoding} {input} +

    but care is required to ensure that the input is correctly passed to the +digester. The digester returns {input}:{digest}. If the input +appears corrupted in the return, the digest will be invalid.

    + +

    The output format of the digest is {salt}${iterations}${digest}. +If the salt length is zero and the iteration count is one, the output is +simplified to {digest}.

    + +

    The full syntax of CATALINA_HOME/bin/digest.[bat|sh] is:

    +CATALINA_HOME/bin/digest.[bat|sh] [-a <algorithm>] [-e <encoding>] + [-i <iterations>] [-s <salt-length>] [-k <key-length>] + [-h <handler-class-name>] [-f <password-file> | <credentials>] + +
      +
    • -a - The algorithm to use to generate the stored + credential. If not specified, the default for the handler will + be used. If neither handler nor algorithm is specified then a + default of SHA-512 will be used
    • +
    • -e - The encoding to use for any byte to/from character + conversion that may be necessary. If not specified, the + system encoding (Charset#defaultCharset()) will + be used.
    • +
    • -i - The number of iterations to use when generating the + stored credential. If not specified, the default for the + CredentialHandler will be used.
    • +
    • -s - The length (in bytes) of salt to generate and store as + part of the credential. If not specified, the default for + the CredentialHandler will be used.
    • +
    • -k - The length (in bits) of the key(s), if any, created while + generating the credential. If not specified, the default + for the CredentialHandler will be used.
    • +
    • -h - The fully qualified class name of the CredentialHandler + to use. If not specified, the built-in handlers will be + tested in turn (MessageDigestCredentialHandler then + SecretKeyCredentialHandler) and the first one to accept the + specified algorithm will be used.
    • +
    • -f - The name of the file that contains passwords to encode. Each + line in the file should contain only one password. Using this + option ignores other password input.
    • +
    +
    + + + + + +

    The example application shipped with Tomcat includes an area that is +protected by a security constraint, utilizing form-based login. To access it, +point your browser at +http://localhost:8080/examples/jsp/security/protected/ +and log on with one of the usernames and passwords described for the default +UserDatabaseRealm.

    + +
    + + + + +

    If you wish to use the Manager Application +to deploy and undeploy applications in a running Tomcat installation, you +MUST add the "manager-gui" role to at least one username in your selected +Realm implementation. This is because the manager web application itself uses a +security constraint that requires role "manager-gui" to access ANY request URI +within the HTML interface of that application.

    + +

    For security reasons, no username in the default Realm (i.e. using +conf/tomcat-users.xml is assigned the "manager-gui" role. +Therefore, no one will be able to utilize the features of this application +until the Tomcat administrator specifically assigns this role to one or more +users.

    + +
    + + + +

    Debugging and exception messages logged by a Realm will + be recorded by the logging configuration associated with the container + for the realm: its surrounding Context, + Host, or + Engine.

    + +
    + +
    + + +
    + + + +
    Introduction
    + +

    DataSourceRealm is an implementation of the Tomcat +Realm interface that looks up users in a relational database +accessed via a JNDI named JDBC DataSource. There is substantial configuration +flexibility that lets you adapt to existing table and column names, as long +as your database structure conforms to the following requirements:

    +
      +
    • There must be a table, referenced below as the users table, + that contains one row for every valid user that this Realm + should recognize.
    • +
    • The users table must contain at least two columns (it may + contain more if your existing applications required it): +
        +
      • Username to be recognized by Tomcat when the user logs in.
      • +
      • Password to be recognized by Tomcat when the user logs in. + This value may in cleartext or digested - see below for more + information.
      • +
    • +
    • There must be a table, referenced below as the user roles table, + that contains one row for every valid role that is assigned to a + particular user. It is legal for a user to have zero, one, or more than + one valid role.
    • +
    • The user roles table must contain at least two columns (it may + contain more if your existing applications required it): +
        +
      • Username to be recognized by Tomcat (same value as is specified + in the users table).
      • +
      • Role name of a valid role associated with this user.
      • +
    • +
    + +
    Quick Start
    + +

    To set up Tomcat to use DataSourceRealm, you will need to follow these steps:

    +
      +
    1. If you have not yet done so, create tables and columns in your database + that conform to the requirements described above.
    2. +
    3. Configure a database username and password for use by Tomcat, that has + at least read only access to the tables described above. (Tomcat will + never attempt to write to these tables.)
    4. +
    5. Configure a JNDI named JDBC DataSource for your database. Refer to the + JNDI DataSource Example + How-To for information on how to configure a JNDI named JDBC DataSource. + Be sure to set the Realm's localDataSource + attribute appropriately, depending on where the JNDI DataSource is + defined.
    6. +
    7. Set up a <Realm> element, as described below, in your + $CATALINA_BASE/conf/server.xml file.
    8. +
    9. Restart Tomcat if it is already running.
    10. +
    + +
    Realm Element Attributes
    + +

    To configure DataSourceRealm, you will create a <Realm> +element and nest it in your $CATALINA_BASE/conf/server.xml file, +as described above. The attributes for the +DataSourceRealm are defined in the Realm +configuration documentation.

    + +
    Example
    + +

    An example SQL script to create the needed tables might look something +like this (adapt the syntax as required for your particular database):

    +create table users ( + user_name varchar(15) not null primary key, + user_pass varchar(15) not null +); + +create table user_roles ( + user_name varchar(15) not null, + role_name varchar(15) not null, + primary key (user_name, role_name) +); + +

    Here is an example for using a MySQL database called "authority", configured +with the tables described above, and accessed with the JNDI JDBC DataSource with +name "java:/comp/env/jdbc/authority".

    +]]> + +
    Additional Notes
    + +

    DataSourceRealm operates according to the following rules:

    +
      +
    • When a user attempts to access a protected resource for the first time, + Tomcat will call the authenticate() method of this + Realm. Thus, any changes you have made to the database + directly (new users, changed passwords or roles, etc.) will be immediately + reflected.
    • +
    • Once a user has been authenticated, the user (and their associated + roles) are cached within Tomcat for the duration of the user's login. + (For FORM-based authentication, that means until the session times out or + is invalidated; for BASIC authentication, that means until the user + closes their browser). The cached user is not saved and + restored across sessions serialisations. Any changes to the database + information for an already authenticated user will not be + reflected until the next time that user logs on again.
    • +
    • Administering the information in the users and user roles + table is the responsibility of your own applications. Tomcat does not + provide any built-in capabilities to maintain users and roles.
    • +
    + +
    + + + + +
    Introduction
    + +

    JNDIRealm is an implementation of the Tomcat +Realm interface that looks up users in an LDAP directory +server accessed by a JNDI provider (typically, the standard LDAP +provider that is available with the JNDI API classes). The realm +supports a variety of approaches to using a directory for +authentication.

    + +
    Connecting to the directory
    + +

    The realm's connection to the directory is defined by the +connectionURL configuration attribute. This is a URL +whose format is defined by the JNDI provider. It is usually an LDAP +URL that specifies the domain name of the directory server to connect +to, and optionally the port number and distinguished name (DN) of the +required root naming context.

    + +

    If you have more than one provider you can configure an +alternateURL. If a socket connection cannot be +made to the provider at the connectionURL an +attempt will be made to use the alternateURL.

    + +

    When making a connection in order to search the directory and +retrieve user and role information, the realm authenticates itself to +the directory with the username and password specified by the +connectionName and +connectionPassword properties. If these properties +are not specified the connection is anonymous. This is sufficient in +many cases. +

    + + +
    Selecting the user's directory entry
    + +

    Each user that can be authenticated must be represented in the +directory by an individual entry that corresponds to an element in the +initial DirContext defined by the +connectionURL attribute. This user entry must have an +attribute containing the username that is presented for +authentication.

    + +

    Often the distinguished name of the user's entry contains the +username presented for authentication but is otherwise the same for +all users. In this case the userPattern attribute may +be used to specify the DN, with "{0}" marking where +the username should be substituted.

    + +

    Otherwise the realm must search the directory to find a unique entry +containing the username. The following attributes configure this +search:

    + +
      +
    • userBase - the entry that is the base of + the subtree containing users. If not specified, the search + base is the top-level context.
    • + +
    • userSubtree - the search scope. Set to + true if you wish to search the entire subtree + rooted at the userBase entry. The default value + of false requests a single-level search + including only the top level.
    • + +
    • userSearch - pattern specifying the LDAP + search filter to use after substitution of the username.
    • + +
    + + +
    Authenticating the user
    + +
      +
    • +

      Bind mode

      + +

      By default the realm authenticates a user by binding to +the directory with the DN of the entry for that user and the password +presented by the user. If this simple bind succeeds the user is considered to +be authenticated.

      + +

      For security reasons a directory may store a digest of the user's +password rather than the clear text version (see +Digested Passwords for more information). In that case, +as part of the simple bind operation the directory automatically +computes the correct digest of the plaintext password presented by the +user before validating it against the stored value. In bind mode, +therefore, the realm is not involved in digest processing. The +digest attribute is not used, and will be ignored if +set.

      +
    • + +
    • +

      Comparison mode

      +

      Alternatively, the realm may retrieve the stored +password from the directory and compare it explicitly with the value +presented by the user. This mode is configured by setting the +userPassword attribute to the name of a directory +attribute in the user's entry that contains the password.

      + +

      Comparison mode has some disadvantages. First, the +connectionName and +connectionPassword attributes must be configured to +allow the realm to read users' passwords in the directory. For +security reasons this is generally undesirable; indeed many directory +implementations will not allow even the directory manager to read +these passwords. In addition, the realm must handle password digests +itself, including variations in the algorithms used and ways of +representing password hashes in the directory. However, the realm may +sometimes need access to the stored password, for example to support +HTTP Digest Access Authentication (RFC 2069). (Note that HTTP digest +authentication is different from the storage of password digests in +the repository for user information as discussed above). +

      +
    • +
    + +
    Assigning roles to the user
    + +

    The directory realm supports two approaches to the representation +of roles in the directory:

    + +
      +
    • +

      Roles as explicit directory entries

      + +

      Roles may be represented by explicit directory entries. A role +entry is usually an LDAP group entry with one attribute +containing the name of the role and another whose values are the +distinguished names or usernames of the users in that role. The +following attributes configure a directory search to +find the names of roles associated with the authenticated user:

      + +
        +
      • roleBase - the base entry for the role search. + If not specified, the search base is the top-level directory + context.
      • + +
      • roleSubtree - the search + scope. Set to true if you wish to search the entire + subtree rooted at the roleBase entry. The default + value of false requests a single-level search + including the top level only.
      • + +
      • roleSearch - the LDAP search filter for + selecting role entries. It optionally includes pattern + replacements "{0}" for the distinguished name and/or "{1}" for the + username and/or "{2}" for an attribute from user's directory entry, + of the authenticated user. Use userRoleAttribute to + specify the name of the attribute that provides the value for "{2}".
      • + +
      • roleName - the attribute in a role entry + containing the name of that role.
      • + +
      • roleNested - enable nested roles. Set to + true if you want to nest roles in roles. If configured, then + every newly found roleName and distinguished + Name will be recursively tried for a new role search. + The default value is false.
      • + +
      + +
    • +
    + +
      +
    • +

      Roles as an attribute of the user entry

      + +

      Role names may also be held as the values of an attribute in the +user's directory entry. Use userRoleName to specify +the name of this attribute.

      + +
    • +
    +

    A combination of both approaches to role representation may be used.

    + +
    Quick Start
    + +

    To set up Tomcat to use JNDIRealm, you will need to follow these steps:

    +
      +
    1. Make sure your directory server is configured with a schema that matches + the requirements listed above.
    2. +
    3. If required, configure a username and password for use by Tomcat, that has + read only access to the information described above. (Tomcat will + never attempt to modify this information.)
    4. +
    5. Set up a <Realm> element, as described below, in your + $CATALINA_BASE/conf/server.xml file.
    6. +
    7. Restart Tomcat if it is already running.
    8. +
    + +
    Realm Element Attributes
    + +

    To configure JNDIRealm, you will create a <Realm> +element and nest it in your $CATALINA_BASE/conf/server.xml file, +as described above. The attributes for the +JNDIRealm are defined in the Realm configuration +documentation.

    + +
    Example
    + +

    Creation of the appropriate schema in your directory server is beyond the +scope of this document, because it is unique to each directory server +implementation. In the examples below, we will assume that you are using a +distribution of the OpenLDAP directory server (version 2.0.11 or later), which +can be downloaded from +https://www.openldap.org. Assume that +your slapd.conf file contains the following settings +(among others):

    +database ldbm +suffix dc="mycompany",dc="com" +rootdn "cn=Manager,dc=mycompany,dc=com" +rootpw secret + +

    We will assume for connectionURL that the directory +server runs on the same machine as Tomcat. See +http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/index.html +for more information about configuring and using the JNDI LDAP +provider.

    + +

    Next, assume that this directory server has been populated with elements +as shown below (in LDIF format):

    + +# Define top-level entry +dn: dc=mycompany,dc=com +objectClass: dcObject +dc:mycompany + +# Define an entry to contain people +# searches for users are based on this entry +dn: ou=people,dc=mycompany,dc=com +objectClass: organizationalUnit +ou: people + +# Define a user entry for Janet Jones +dn: uid=jjones,ou=people,dc=mycompany,dc=com +objectClass: inetOrgPerson +uid: jjones +sn: jones +cn: janet jones +mail: j.jones@mycompany.com +userPassword: janet + +# Define a user entry for Fred Bloggs +dn: uid=fbloggs,ou=people,dc=mycompany,dc=com +objectClass: inetOrgPerson +uid: fbloggs +sn: bloggs +cn: fred bloggs +mail: f.bloggs@mycompany.com +userPassword: fred + +# Define an entry to contain LDAP groups +# searches for roles are based on this entry +dn: ou=groups,dc=mycompany,dc=com +objectClass: organizationalUnit +ou: groups + +# Define an entry for the "tomcat" role +dn: cn=tomcat,ou=groups,dc=mycompany,dc=com +objectClass: groupOfUniqueNames +cn: tomcat +uniqueMember: uid=jjones,ou=people,dc=mycompany,dc=com +uniqueMember: uid=fbloggs,ou=people,dc=mycompany,dc=com + +# Define an entry for the "role1" role +dn: cn=role1,ou=groups,dc=mycompany,dc=com +objectClass: groupOfUniqueNames +cn: role1 +uniqueMember: uid=fbloggs,ou=people,dc=mycompany,dc=com + +

    An example Realm element for the OpenLDAP directory +server configured as described above might look like this, assuming +that users use their uid (e.g. jjones) to login to the +application and that an anonymous connection is sufficient to search +the directory and retrieve role information:

    + +]]> + +

    With this configuration, the realm will determine the user's +distinguished name by substituting the username into the +userPattern, authenticate by binding to the directory +with this DN and the password received from the user, and search the +directory to find the user's roles.

    + +

    Now suppose that users are expected to enter their email address +rather than their userid when logging in. In this case the realm must +search the directory for the user's entry. (A search is also necessary +when user entries are held in multiple subtrees corresponding perhaps +to different organizational units or company locations).

    + +

    Further, suppose that in addition to the group entries you want to +use an attribute of the user's entry to hold roles. Now the entry for +Janet Jones might read as follows:

    + +dn: uid=jjones,ou=people,dc=mycompany,dc=com +objectClass: inetOrgPerson +uid: jjones +sn: jones +cn: janet jones +mail: j.jones@mycompany.com +memberOf: role2 +memberOf: role3 +userPassword: janet + +

    This realm configuration would satisfy the new requirements:

    + +]]> + +

    Now when Janet Jones logs in as "j.jones@mycompany.com", the realm +searches the directory for a unique entry with that value as its mail +attribute and attempts to bind to the directory as +uid=jjones,ou=people,dc=mycompany,dc=com with the given +password. If authentication succeeds, they are assigned three roles: +"role2" and "role3", the values of the "memberOf" attribute in their +directory entry, and "tomcat", the value of the "cn" attribute in the +only group entry of which they are a member.

    + +

    Finally, to authenticate the user by retrieving +the password from the directory and making a local comparison in the +realm, you might use a realm configuration like this:

    + +]]> + +

    However, as discussed above, the default bind mode for +authentication is usually to be preferred.

    + +
    Additional Notes
    + +

    JNDIRealm operates according to the following rules:

    +
      +
    • When a user attempts to access a protected resource for the first time, + Tomcat will call the authenticate() method of this + Realm. Thus, any changes you have made to the directory + (new users, changed passwords or roles, etc.) will be immediately + reflected.
    • +
    • Once a user has been authenticated, the user (and their associated + roles) are cached within Tomcat for the duration of the user's login. + (For FORM-based authentication, that means until the session times out or + is invalidated; for BASIC authentication, that means until the user + closes their browser). The cached user is not saved and + restored across sessions serialisations. Any changes to the directory + information for an already authenticated user will not be + reflected until the next time that user logs on again.
    • +
    • Administering the information in the directory server + is the responsibility of your own applications. Tomcat does not + provide any built-in capabilities to maintain users and roles.
    • +
    + +
    + + + + +
    Introduction
    + +

    UserDatabaseRealm is an implementation of the Tomcat +Realm interface that uses a JNDI resource to store user +information. By default, the JNDI resource is backed by an XML file. It is not +designed for large-scale production use. At startup time, the UserDatabaseRealm +loads information about all users, and their corresponding roles, from an XML +document (by default, this document is loaded from +$CATALINA_BASE/conf/tomcat-users.xml). The users, their passwords +and their roles may all be editing dynamically, typically via JMX. Changes may +be saved and will be reflected in the XML file.

    + +
    Realm Element Attributes
    + +

    To configure UserDatabaseRealm, you will create a <Realm> +element and nest it in your $CATALINA_BASE/conf/server.xml file, +as described above. The attributes for the +UserDatabaseRealm are defined in the Realm +configuration documentation.

    + +
    User File Format
    + +

    For the XML file based UserDatabase, the users file uses the +same format as the MemoryRealm.

    + +
    Example
    + +

    The default installation of Tomcat is configured with a UserDatabaseRealm +nested inside the <Engine> element, so that it applies +to all virtual hosts and web applications. The default contents of the +conf/tomcat-users.xml file is:

    + + + + +]]> + +
    Additional Notes
    + +

    UserDatabaseRealm operates according to the following rules:

    +
      +
    • When Tomcat first starts up, it loads all defined users and their + associated information from the users file. Changes made to the data in + this file will not be recognized until Tomcat is + restarted. Changes may be made via the UserDatabase resource. Tomcat + provides MBeans that may be accessed via JMX for this purpose.
    • +
    • When a user attempts to access a protected resource for the first time, + Tomcat will call the authenticate() method of this + Realm.
    • +
    • Once a user has been authenticated, the user becomes associated within + Tomcat for the duration of the user's login. + (For FORM-based authentication, that means until the session times out or + is invalidated; for BASIC authentication, that means until the user + closes their browser). However, the user roles will still reflect the + UserDatabase contents, unlike for the other realms. If a user + is removed from the database, it will be considered to have no roles. + The useStaticPrincipal attribute of the + UserDatabaseRealm can be used to instead cache the user along + with all its roles. The cached user is not saved and + restored across sessions serialisations. When the user's principal object + is serialized for any reason, it will also be replaced by a static + equivalent object with roles that will no longer reflect the database + contents.
    • +
    + + +
    + + + + +
    Introduction
    + +

    MemoryRealm is a simple demonstration implementation of the +Tomcat Realm interface. It is not designed for production use. +At startup time, MemoryRealm loads information about all users, and their +corresponding roles, from an XML document (by default, this document is loaded +from $CATALINA_BASE/conf/tomcat-users.xml). Changes to the data +in this file are not recognized until Tomcat is restarted.

    + +
    Realm Element Attributes
    + +

    To configure MemoryRealm, you will create a <Realm> +element and nest it in your $CATALINA_BASE/conf/server.xml file, +as described above. The attributes for the +MemoryRealm are defined in the Realm +configuration documentation.

    + +
    User File Format
    + +

    The users file (by default, conf/tomcat-users.xml must be an +XML document, with a root element <tomcat-users>. Nested +inside the root element will be a <user> element for each +valid user, consisting of the following attributes:

    +
      +
    • name - Username this user must log on with.
    • +
    • password - Password this user must log on with (in + clear text if the digest attribute was not set on the + <Realm> element, or digested appropriately as + described here otherwise).
    • +
    • roles - Comma-delimited list of the role names + associated with this user.
    • +
    + +
    Additional Notes
    + +

    MemoryRealm operates according to the following rules:

    +
      +
    • When Tomcat first starts up, it loads all defined users and their + associated information from the users file. Changes to the data in + this file will not be recognized until Tomcat is + restarted.
    • +
    • When a user attempts to access a protected resource for the first time, + Tomcat will call the authenticate() method of this + Realm.
    • +
    • Once a user has been authenticated, the user (and their associated + roles) are cached within Tomcat for the duration of the user's login. + (For FORM-based authentication, that means until the session times out or + is invalidated; for BASIC authentication, that means until the user + closes their browser). The cached user is not saved and + restored across sessions serialisations.
    • +
    • Administering the information in the users file is the responsibility + of your application. Tomcat does not + provide any built-in capabilities to maintain users and roles.
    • +
    + + +
    + + + + +
    Introduction
    + +

    JAASRealm is an implementation of the Tomcat +Realm interface that authenticates users through the Java +Authentication & Authorization Service (JAAS) framework which is now +provided as part of the standard Java SE API.

    +

    Using JAASRealm gives the developer the ability to combine +practically any conceivable security realm with Tomcat's CMA.

    +

    JAASRealm is prototype for Tomcat of the JAAS-based +J2EE authentication framework for J2EE v1.4, based on the JCP Specification +Request 196 to enhance container-managed security and promote +'pluggable' authentication mechanisms whose implementations would be +container-independent. +

    +

    Based on the JAAS login module and principal (see javax.security.auth.spi.LoginModule +and javax.security.Principal), you can develop your own +security mechanism or wrap another third-party mechanism for +integration with the CMA as implemented by Tomcat. +

    + +
    Quick Start
    +

    To set up Tomcat to use JAASRealm with your own JAAS login module, + you will need to follow these steps:

    +
      +
    1. Write your own LoginModule, User and Role classes based +on JAAS (see + +the JAAS Authentication Tutorial and + +the JAAS Login Module Developer's Guide) to be managed by the JAAS Login +Context (javax.security.auth.login.LoginContext) +When developing your LoginModule, note that JAASRealm's built-in CallbackHandler +only recognizes the NameCallback and PasswordCallback at present. +
    2. +
    3. Although not specified in JAAS, you should create +separate classes to distinguish between users and roles, extending javax.security.Principal, +so that Tomcat can tell which Principals returned from your login +module are users and which are roles (see org.apache.catalina.realm.JAASRealm). +Regardless, the first Principal returned is always treated as the user Principal. +
    4. +
    5. Place the compiled classes on Tomcat's classpath +
    6. +
    7. Set up a login.config file for Java (see +JAAS LoginConfig file) and tell Tomcat where to find it by specifying +its location to the JVM, for instance by setting the environment +variable: JAVA_OPTS=$JAVA_OPTS -Djava.security.auth.login.config==$CATALINA_BASE/conf/jaas.config
    8. + +
    9. Configure your security-constraints in your web.xml for +the resources you want to protect
    10. +
    11. Configure the JAASRealm module in your server.xml
    12. +
    13. Restart Tomcat if it is already running.
    14. +
    +
    Realm Element Attributes
    +

    To configure JAASRealm as for step 6 above, you create +a <Realm> element and nest it in your +$CATALINA_BASE/conf/server.xml +file within your <Engine> node. The attributes for the +JAASRealm are defined in the Realm +configuration documentation.

    + +
    Example
    + +

    Here is an example of how your server.xml snippet should look.

    + +]]> + +

    It is the responsibility of your login module to create and save User and +Role objects representing Principals for the user +(javax.security.auth.Subject). If your login module doesn't +create a user object but also doesn't throw a login exception, then the +Tomcat CMA will break and you will be left at the +http://localhost:8080/myapp/j_security_check URI or at some other +unspecified location.

    + +

    The flexibility of the JAAS approach is two-fold:

    +
      +
    • you can carry out whatever processing you require behind +the scenes in your own login module.
    • +
    • you can plug in a completely different LoginModule by changing the configuration +and restarting the server, without any code changes to your application.
    • +
    + +
    Additional Notes
    +
      +
    • When a user attempts to access a protected resource for + the first time, Tomcat will call the authenticate() + method of this Realm. Thus, any changes you have made in + the security mechanism directly (new users, changed passwords or + roles, etc.) will be immediately reflected.
    • +
    • Once a user has been authenticated, the user (and their + associated roles) are cached within Tomcat for the duration of + the user's login. For FORM-based authentication, that means until + the session times out or is invalidated; for BASIC authentication, + that means until the user closes their browser. Any changes to the + security information for an already authenticated user will not + be reflected until the next time that user logs on again.
    • +
    • As with other Realm implementations, digested passwords + are supported if the <Realm> element in server.xml + contains a digest attribute; JAASRealm's CallbackHandler + will digest the password prior to passing it back to the LoginModule
    • +
    + +
    + + + + +
    Introduction
    + +

    CombinedRealm is an implementation of the Tomcat + Realm interface that authenticates users through one or more + sub-Realms.

    + +

    Using CombinedRealm gives the developer the ability to combine multiple + Realms of the same or different types. This can be used to authenticate + against different sources, provide fall back in case one Realm fails or for + any other purpose that requires multiple Realms.

    + +

    Sub-realms are defined by nesting Realm elements inside the + Realm element that defines the CombinedRealm. Authentication + will be attempted against each Realm in the order they are + listed. Authentication against any Realm will be sufficient to authenticate + the user.

    + +
    Realm Element Attributes
    +

    To configure a CombinedRealm, you create a <Realm> + element and nest it in your $CATALINA_BASE/conf/server.xml + file within your <Engine> or <Host>. + You can also nest inside a <Context> node in a + context.xml file.

    + +
    Example
    + +

    Here is an example of how your server.xml snippet should look to use a +UserDatabase Realm and a DataSource Realm.

    + + + + +]]> + +
    + + + +
    Introduction
    + +

    LockOutRealm is an implementation of the Tomcat + Realm interface that extends the CombinedRealm to provide lock + out functionality to provide a user lock out mechanism if there are too many + failed authentication attempts in a given period of time.

    + +

    To ensure correct operation, there is a reasonable degree of + synchronisation in this Realm.

    + +

    This Realm does not require modification to the underlying Realms or the + associated user storage mechanisms. It achieves this by recording all failed + logins, including those for users that do not exist. To prevent a DOS by + deliberating making requests with invalid users (and hence causing this + cache to grow) the size of the list of users that have failed authentication + is limited.

    + +

    Sub-realms are defined by nesting Realm elements inside the + Realm element that defines the LockOutRealm. Authentication + will be attempted against each Realm in the order they are + listed. Authentication against any Realm will be sufficient to authenticate + the user.

    + +
    Realm Element Attributes
    +

    To configure a LockOutRealm, you create a <Realm> + element and nest it in your $CATALINA_BASE/conf/server.xml + file within your <Engine> or <Host>. + You can also nest inside a <Context> node in a + context.xml file. The attributes for the + LockOutRealm are defined in the Realm + configuration documentation.

    + +
    Example
    + +

    Here is an example of how your server.xml snippet should look to add lock out +functionality to a UserDatabase Realm.

    + + + +]]> + +
    + +
    + + + +
    diff --git a/webapps/docs/rewrite.xml b/webapps/docs/rewrite.xml new file mode 100644 index 0000000..72fc813 --- /dev/null +++ b/webapps/docs/rewrite.xml @@ -0,0 +1,823 @@ + + + + +]> + + + &project; + + + Remy Maucherat + The rewrite Valve + + + + +
    + +

    The rewrite valve implements URL rewrite functionality in a way that is + very similar to mod_rewrite from Apache HTTP Server.

    + +
    + +
    + +

    The rewrite valve is configured as a valve using the org.apache.catalina.valves.rewrite.RewriteValve + class name.

    + +

    The rewrite valve can be configured as a valve added in a Host. + See virtual-server documentation for + information on how to configure it. It will use a rewrite.config file + containing the rewrite directives, it must be placed in the Host configuration + folder. +

    + +

    It can also be in the context.xml of a webapp. + The valve will then use a rewrite.config file containing the + rewrite directives, it must be placed in the WEB-INF folder of the web application +

    + +
    + +
    + +

    The rewrite.config file contains a list of directives which closely + resemble the directives used by mod_rewrite, in particular the central + RewriteRule and RewriteCond directives. Lines that start with a + # character are treated as comments and will be ignored.

    + +

    Note: This section is a modified version of the mod_rewrite documentation, + which is Copyright 1995-2006 The Apache Software Foundation, and licensed under the + under the Apache License, Version 2.0.

    + + + +

    Syntax: RewriteCond TestString CondPattern

    + +

    The RewriteCond directive defines a rule condition. One or more RewriteCond + can precede a RewriteRule directive. The following rule is then only used if both + the current state of the URI matches its pattern, and if these conditions are met.

    + +

    TestString is a string which can contain the + following expanded constructs in addition to plain text:

    + +
      +
    • + RewriteRule backreferences: These are + backreferences of the form $N + (0 <= N <= 9), which provide access to the grouped + parts (in parentheses) of the pattern, from the + RewriteRule which is subject to the current + set of RewriteCond conditions.. +
    • +
    • + RewriteCond backreferences: These are + backreferences of the form %N + (1 <= N <= 9), which provide access to the grouped + parts (again, in parentheses) of the pattern, from the last matched + RewriteCond in the current set + of conditions. +
    • +
    • + RewriteMap expansions: These are + expansions of the form ${mapname:key|default}. + See the documentation for + RewriteMap for more details. +
    • +
    • + Server-Variables: These are variables of + the form + %{ NAME_OF_VARIABLE + } + where NAME_OF_VARIABLE can be a string taken + from the following list: + +
        +
      • +

        HTTP headers:

        +

        + HTTP_USER_AGENT
        + HTTP_REFERER
        + HTTP_COOKIE
        + HTTP_FORWARDED
        + HTTP_HOST
        + HTTP_PROXY_CONNECTION
        + HTTP_ACCEPT
        +

        +
      • +
      • +

        connection & request:

        +

        + REMOTE_ADDR
        + REMOTE_HOST
        + REMOTE_PORT
        + REMOTE_USER
        + REMOTE_IDENT
        + REQUEST_METHOD
        + SCRIPT_FILENAME
        + REQUEST_PATH
        + CONTEXT_PATH
        + SERVLET_PATH
        + PATH_INFO
        + QUERY_STRING
        + AUTH_TYPE
        +

        +
      • +
      • +

        server internals:

        +

        + DOCUMENT_ROOT
        + SERVER_NAME
        + SERVER_ADDR
        + SERVER_PORT
        + SERVER_PROTOCOL
        + SERVER_SOFTWARE
        +

        +
      • +
      • +

        date and time:

        +

        + TIME_YEAR
        + TIME_MON
        + TIME_DAY
        + TIME_HOUR
        + TIME_MIN
        + TIME_SEC
        + TIME_WDAY
        + TIME
        +

        +
      • +
      • +

        specials:

        +

        + THE_REQUEST
        + REQUEST_URI
        + REQUEST_FILENAME
        + HTTPS
        +

        +
      • +
      + +

      These variables all + correspond to the similarly named HTTP + MIME-headers and Servlet API methods. + Most are documented elsewhere in the Manual or in + the CGI specification. Those that are special to + the rewrite valve include those below.

      + +
      + +
      REQUEST_PATH
      + +
      Corresponds to the full path that is used for mapping.
      + +
      CONTEXT_PATH
      + +
      Corresponds to the path of the mapped context.
      + +
      SERVLET_PATH
      + +
      Corresponds to the servlet path.
      + +
      THE_REQUEST
      + +
      The full HTTP request line sent by the + browser to the server (e.g., "GET + /index.html HTTP/1.1"). This does not + include any additional headers sent by the + browser.
      + +
      REQUEST_URI
      + +
      The resource requested in the HTTP request + line. (In the example above, this would be + "/index.html".)
      + +
      REQUEST_FILENAME
      + +
      The full local file system path to the file or + script matching the request.
      + +
      HTTPS
      + +
      Will contain the text "on" if the connection is + using SSL/TLS, or "off" otherwise.
      + +
      + +
    • +
    + +

    Other things you should be aware of:

    + +
      +
    1. The variables SCRIPT_FILENAME and REQUEST_FILENAME + contain the same value - the value of the + filename field of the internal + request_rec structure of the Apache server. + The first name is the commonly known CGI variable name + while the second is the appropriate counterpart of + REQUEST_URI (which contains the value of the + uri field of request_rec).
    2. + +
    3. + %{ENV:variable}, where variable can be + any Java system property, is also available.
    4. + +
    5. + %{SSL:variable}, where variable is the + name of an SSL environment + variable, are implemented, except + SSL_SESSION_RESUMED, SSL_SECURE_RENEG, + SSL_COMPRESS_METHOD, SSL_TLS_SNI, + SSL_SRP_USER, SSL_SRP_USERINFO, + SSL_CLIENT_VERIFY, + SSL_CLIENT_SAN_OTHER_msUPN_n, + SSL_CLIENT_CERT_RFC4523_CEA, + SSL_SERVER_SAN_OTHER_dnsSRV_n. + When OpenSSL is used, the variables related to the server + certificate, prefixed by SSL_SERVER_ are not available. + Example: + %{SSL:SSL_CIPHER_USEKEYSIZE} may expand to + 128.
    6. + +
    7. + %{HTTP:header}, where header can be + any HTTP MIME-header name, can always be used to obtain the + value of a header sent in the HTTP request. + Example: %{HTTP:Proxy-Connection} is + the value of the HTTP header + 'Proxy-Connection:'.
    8. + +
    + +

    CondPattern is the condition pattern, + a regular expression which is applied to the + current instance of the TestString. + TestString is first evaluated, before being matched against + CondPattern.

    + +

    Remember: CondPattern is a + perl compatible regular expression with some + additions:

    + +
      +
    1. You can prefix the pattern string with a + '!' character (exclamation mark) to specify a + non-matching pattern.
    2. + +
    3. + There are some special variants of CondPatterns. + Instead of real regular expression strings you can also + use one of the following: + +
        +
      • '<CondPattern' (lexicographically + precedes)
        + Treats the CondPattern as a plain string and + compares it lexicographically to TestString. True if + TestString lexicographically precedes + CondPattern.
      • + +
      • '>CondPattern' (lexicographically + follows)
        + Treats the CondPattern as a plain string and + compares it lexicographically to TestString. True if + TestString lexicographically follows + CondPattern.
      • + +
      • '=CondPattern' (lexicographically + equal)
        + Treats the CondPattern as a plain string and + compares it lexicographically to TestString. True if + TestString is lexicographically equal to + CondPattern (the two strings are exactly + equal, character for character). If CondPattern + is "" (two quotation marks) this + compares TestString to the empty string.
      • + +
      • '-d' (is + directory)
        + Treats the TestString as a pathname and tests + whether or not it exists, and is a directory.
      • + +
      • '-f' (is regular + file)
        + Treats the TestString as a pathname and tests + whether or not it exists, and is a regular file.
      • + +
      • '-s' (is regular file, with + size)
        + Treats the TestString as a pathname and tests + whether or not it exists, and is a regular file with size greater + than zero.
      • + +
      + +Note: + All of these tests can + also be prefixed by an exclamation mark ('!') to + negate their meaning. + +
    4. + +
    5. You can also set special flags for + CondPattern by appending + [flags] + as the third argument to the RewriteCond + directive, where flags is a comma-separated list of any of the + following flags: + +
        +
      • 'nocase|NC' + (no case)
        + This makes the test case-insensitive - differences + between 'A-Z' and 'a-z' are ignored, both in the + expanded TestString and the CondPattern. + This flag is effective only for comparisons between + TestString and CondPattern. It has no + effect on file system and subrequest checks.
      • + +
      • + 'ornext|OR' + (or next condition)
        + Use this to combine rule conditions with a local OR + instead of the implicit AND. Typical example: + +RewriteCond %{REMOTE_HOST} ^host1.* [OR] +RewriteCond %{REMOTE_HOST} ^host2.* [OR] +RewriteCond %{REMOTE_HOST} ^host3.* +RewriteRule ...some special stuff for any of these hosts... + + Without this flag you would have to write the condition/rule + pair three times. +
      • +
      +
    6. +
    + +

    Example:

    + +

    To rewrite the Homepage of a site according to the + 'User-Agent:' header of the request, you can + use the following:

    + +RewriteCond %{HTTP_USER_AGENT} ^Mozilla.* +RewriteRule ^/$ /homepage.max.html [L] + +RewriteCond %{HTTP_USER_AGENT} ^Lynx.* +RewriteRule ^/$ /homepage.min.html [L] + +RewriteRule ^/$ /homepage.std.html [L] + +

    Explanation: If you use a browser which identifies itself + as 'Mozilla' (including Netscape Navigator, Mozilla etc), then you + get the max homepage (which could include frames, or other special + features). + If you use the Lynx browser (which is terminal-based), then + you get the min homepage (which could be a version designed for + easy, text-only browsing). + If neither of these conditions apply (you use any other browser, + or your browser identifies itself as something non-standard), you get + the std (standard) homepage.

    + +
    + + + +

    Syntax: RewriteMap name rewriteMapClassName optionalParameters

    + +

    The rewriteMapClassName value also allows special values: +

      +
    • int:toupper: Special map converting passed values to upper case
    • +
    • int:tolower: Special map converting passed values to lower case
    • +
    • int:escape: URL escape the passed value
    • +
    • int:unescape: URL unescape the passed value
    • +
    +

    + +

    The maps are implemented using an interface that users must implement. Its class + name is org.apache.catalina.valves.rewrite.RewriteMap, and its code is:

    + + + +

    The referenced implementation of such a class – in our example rewriteMapClassName – + will be instantiated and initialized with the optional parameter – optionalParameters from above + (be careful with whitespace) – by calling setParameters(String). That instance + will then be registered under the name given as the first parameter of RewriteMap rule.

    + +

    Note: You can use more than one parameter. These have to be separated by spaces. Parameters + can be quoted with ". This enables space characters inside parameters.

    + +

    That map instance will be given the the lookup value that is configured in the corresponding RewriteRule by + calling lookup(String). Your implementation is free to return null to indicate, + that the given default should be used, or to return a replacement value.

    + +

    Say, you want to implement a rewrite map function that converts all lookup keys to uppercase. You + would start by implementing a class that implements the RewriteMap interface.

    + + + +

    Compile this class, put it into a jar and place that jar in ${CATALINA_BASE}/lib.

    + +

    Having done that, you can now define a map with the RewriteMap directive + and further on use that map in a RewriteRule.

    + +RewriteMap uc example.maps.UpperCaseMap + +RewriteRule ^/(.*)$ ${uc:$1} + + +

    With this setup a request to the url path /index.html would get routed + to /INDEX.HTML.

    +
    + + + +

    Syntax: RewriteRule Pattern Substitution

    + +

    The RewriteRule directive is the real + rewriting workhorse. The directive can occur more than once, + with each instance defining a single rewrite rule. The + order in which these rules are defined is important - this is the order + in which they will be applied at run-time.

    + +

    Pattern is a perl compatible regular + expression, which is applied to the current URL. + 'Current' means the value of the URL when this rule is + applied. This may not be the originally requested URL, + which may already have matched a previous rule, and have been + altered.

    + +

    Security warning: Due to the way Java's + regex matching is done, poorly formed regex patterns are vulnerable + to "catastrophic backtracking", also known as "regular expression + denial of service" or ReDoS. Therefore, extra caution should be used + for RewriteRule patterns. In general it is difficult to automatically + detect such vulnerable regex, and so a good defense is to read a bit + on the subject of catastrophic backtracking. A good reference is the + + OWASP ReDoS guide.

    + +

    Some hints on the syntax of regular + expressions:

    + + +
    +Text:
    +  .           Any single character
    +  [chars]     Character class: Any character of the class 'chars'
    +  [^chars]    Character class: Not a character of the class 'chars'
    +  text1|text2 Alternative: text1 or text2
    +
    +Quantifiers:
    +  ?           0 or 1 occurrences of the preceding text
    +  *           0 or N occurrences of the preceding text (N > 0)
    +  +           1 or N occurrences of the preceding text (N > 1)
    +
    +Grouping:
    +  (text)      Grouping of text
    +              (used either to set the borders of an alternative as above, or
    +              to make backreferences, where the Nth group can
    +              be referred to on the RHS of a RewriteRule as $N)
    +
    +Anchors:
    +  ^           Start-of-line anchor
    +  $           End-of-line anchor
    +
    +Escaping:
    +  \char       escape the given char
    +              (for instance, to specify the chars ".[]()" etc.)
    +
    + +

    For more information about regular expressions, have a look at the + perl regular expression manpage ("perldoc + perlre"). If you are interested in more detailed + information about regular expressions and their variants + (POSIX regex etc.) the following book is dedicated to this topic:

    + +

    + Mastering Regular Expressions, 2nd Edition
    + Jeffrey E.F. Friedl
    + O'Reilly & Associates, Inc. 2002
    + ISBN 978-0-596-00289-3
    +

    + +

    In the rules, the NOT character + ('!') is also available as a possible pattern + prefix. This enables you to negate a pattern; to say, for instance: + 'if the current URL does NOT match this + pattern'. This can be used for exceptional cases, where + it is easier to match the negative pattern, or as a last + default rule.

    + +

    +Note: When using the NOT character to negate a pattern, you cannot include +grouped wildcard parts in that pattern. This is because, when the +pattern does NOT match (i.e., the negation matches), there are no +contents for the groups. Thus, if negated patterns are used, you +cannot use $N in the substitution string! +

    + +

    The substitution of a + rewrite rule is the string which is substituted for (or + replaces) the original URL which Pattern + matched. In addition to plain text, it can include

    + +
      +
    1. back-references ($N) to the RewriteRule + pattern
    2. + +
    3. back-references (%N) to the last matched + RewriteCond pattern
    4. + +
    5. server-variables as in rule condition test-strings + (%{VARNAME})
    6. + +
    7. mapping-function calls + (${mapname:key|default})
    8. +
    +

    Back-references are identifiers of the form + $N + (N=0..9), which will be replaced + by the contents of the Nth group of the + matched Pattern. The server-variables are the same + as for the TestString of a RewriteCond + directive. The mapping-functions come from the + RewriteMap directive and are explained there. + These three types of variables are expanded in the order above.

    + +

    As already mentioned, all rewrite rules are + applied to the Substitution (in the order in which + they are defined + in the config file). The URL is completely + replaced by the Substitution and the + rewriting process continues until all rules have been applied, + or it is explicitly terminated by a + L flag.

    + +

    The special characters $ and % can + be quoted by prepending them with a backslash character + \.

    + +

    There is a special substitution string named + '-' which means: NO + substitution! This is useful in providing + rewriting rules which only match + URLs but do not substitute anything for them. It is commonly used + in conjunction with the C (chain) flag, in order + to apply more than one pattern before substitution occurs.

    + +

    Unlike newer mod_rewrite versions, the Tomcat rewrite valve does + not automatically support absolute URLs (the specific redirect flag + must be used to be able to specify an absolute URLs, see below) + or direct file serving.

    + +

    Additionally you can set special flags for Substitution by + appending [flags] + as the third argument to the RewriteRule + directive. Flags is a comma-separated list of any of the + following flags:

    + +
      +
    • 'chain|C' + (chained with next rule)
      + This flag chains the current rule with the next rule + (which itself can be chained with the following rule, + and so on). This has the following effect: if a rule + matches, then processing continues as usual - + the flag has no effect. If the rule does + not match, then all following chained + rules are skipped. For instance, it can be used to remove the + '.www' part, inside a per-directory rule set, + when you let an external redirect happen (where the + '.www' part should not occur!).
    • + +
    • + 'cookie|CO=NAME:VAL:domain[:lifetime[:path]]' + (set cookie)
      + This sets a cookie in the client's browser. The cookie's name + is specified by NAME and the value is + VAL. The domain field is the domain of the + cookie, such as '.apache.org', the optional lifetime + is the lifetime of the cookie in minutes, and the optional + path is the path of the cookie
    • + +
    • + 'env|E=VAR:VAL' + (set environment variable)
      + This forces a request attribute named VAR to + be set to the value VAL, where VAL can + contain regexp backreferences ($N and + %N) which will be expanded. You can use this + flag more than once, to set more than one variable.
    • + +
    • 'forbidden|F' (force URL + to be forbidden)
      + This forces the current URL to be forbidden - it immediately + sends back an HTTP response of 403 (FORBIDDEN). + Use this flag in conjunction with + appropriate RewriteConds to conditionally block some + URLs.
    • + +
    • 'gone|G' (force URL to be + gone)
      + This forces the current URL to be gone - it + immediately sends back an HTTP response of 410 (GONE). Use + this flag to mark pages which no longer exist as gone.
    • + +
    • + 'host|H=Host' + (apply rewriting to host)
      + Rather that rewrite the URL, the virtual host will be + rewritten.
    • + +
    • 'last|L' + (last rule)
      + Stop the rewriting process here and don't apply any more + rewrite rules. This corresponds to the Perl + last command or the break command + in C. Use this flag to prevent the currently + rewritten URL from being rewritten further by following + rules. For example, use it to rewrite the root-path URL + ('/') to a real one, e.g., + '/e/www/'.
    • + +
    • 'next|N' + (next round)
      + Re-run the rewriting process (starting again with the + first rewriting rule). This time, the URL to match is no longer + the original URL, but rather the URL returned by the last rewriting rule. + This corresponds to the Perl next command or + the continue command in C. Use + this flag to restart the rewriting process - + to immediately go to the top of the loop.
      + Be careful not to create an infinite + loop!
    • + +
    • 'nocase|NC' + (no case)
      + This makes the Pattern case-insensitive, + ignoring difference between 'A-Z' and + 'a-z' when Pattern is matched against the current + URL.
    • + +
    • + 'noescape|NE' + (no URI escaping of + output)
      + This flag prevents the rewrite valve from applying the usual URI + escaping rules to the result of a rewrite. Ordinarily, + special characters (such as '%', '$', ';', and so on) + will be escaped into their hexcode equivalents ('%25', + '%24', and '%3B', respectively); this flag prevents this + from happening. This allows percent symbols to appear in + the output, as in + RewriteRule /foo/(.*) /bar?arg=P1\%3d$1 [R,NE] + which would turn '/foo/zed' into a safe + request for '/bar?arg=P1=zed'. +
    • + + + +
    • 'qsappend|QSA' + (query string + append)
      + This flag forces the rewrite engine to append a query + string part of the substitution string to the existing string, + instead of replacing it. Use this when you want to add more + data to the query string via a rewrite rule.
    • + +
    • 'redirect|R + [=code]' (force + redirect)
      + Prefix Substitution with + http://thishost[:thisport]/ (which makes the + new URL a URI) to force an external redirection. If no + code is given, an HTTP response of 302 (FOUND, previously MOVED + TEMPORARILY) will be returned. If you want to use other response + codes in the range 300-399, simply specify the appropriate number + or use one of the following symbolic names: + temp (default), permanent, + seeother. Use this for rules to + canonicalize the URL and return it to the client - to + translate '/~' into + '/u/', or to always append a slash to + /u/user, etc.
      + Note: When you use this flag, make + sure that the substitution field is a valid URL! Otherwise, + you will be redirecting to an invalid location. Remember + that this flag on its own will only prepend + http://thishost[:thisport]/ to the URL, and rewriting + will continue. Usually, you will want to stop rewriting at this point, + and redirect immediately. To stop rewriting, you should add + the 'L' flag. +
    • + +
    • 'skip|S=num' + (skip next rule(s))
      + This flag forces the rewriting engine to skip the next + num rules in sequence, if the current rule + matches. Use this to make pseudo if-then-else constructs: + The last rule of the then-clause becomes + skip=N, where N is the number of rules in the + else-clause. (This is not the same as the + 'chain|C' flag!)
    • + +
    • + 'type|T=MIME-type' + (force MIME type)
      + Force the MIME-type of the target file to be + MIME-type. This can be used to + set up the content-type based on some conditions. + For example, the following snippet allows .php files to + be displayed by mod_php if they are called with + the .phps extension: + RewriteRule ^(.+\.php)s$ $1 [T=application/x-httpd-php-source] +
    • + +
    • 'valveSkip|VS' + (skip valve)
      + This flag can be used to setup conditional execution of valves. + When the flag is set and the rule matches, the rewrite valve will skip + the next valve in the Catalina pipeline. If the rewrite valve is the + last of the pipeline, then the flag will be ignored and the container + basic valve will be invoked. If rewrite occurred, then the flag will + not have any effect. +
    • + +
    + +
    + +
    + + +
    diff --git a/webapps/docs/security-howto.xml b/webapps/docs/security-howto.xml new file mode 100644 index 0000000..65ef1c0 --- /dev/null +++ b/webapps/docs/security-howto.xml @@ -0,0 +1,598 @@ + + + +]> + + + &project; + + + Security Considerations + + + + +
    + +
    + +
    +

    Tomcat is configured to be reasonably secure for most use cases by + default. Some environments may require more, or less, secure configurations. + This page is to provide a single point of reference for configuration + options that may impact security and to offer some commentary on the + expected impact of changing those options. The intention is to provide a + list of configuration options that should be considered when assessing the + security of a Tomcat installation.

    + +

    Note: Reading this page is not a substitute for reading + and understanding the detailed configuration documentation. Fuller + descriptions of these attributes may be found in the relevant documentation + pages.

    +
    + +
    +

    Tomcat configuration should not be the only line of defense. The other + components in the system (operating system, network, database, etc.) should + also be secured.

    +

    Tomcat should not be run under the root user. Create a dedicated user for + the Tomcat process and provide that user with the minimum necessary + permissions for the operating system. For example, it should not be possible + to log on remotely using the Tomcat user.

    +

    File permissions should also be suitably restricted. In the + .tar.gz distribution, files and directories are not world + readable and the group does not have write access. On Unix like operating + systems, Tomcat runs with a default umask of 0027 to maintain + these permissions for files created while Tomcat is running (e.g. log files, + expanded WARs, etc.).

    +

    Taking the Tomcat instances at the ASF as an example (where + auto-deployment is disabled and web applications are deployed as exploded + directories), the standard configuration is to have all Tomcat files owned + by root with group Tomcat and whilst owner has read/write privileges, group + only has read and world has no permissions. The exceptions are the logs, + temp and work directory that are owned by the Tomcat user rather than root. + This means that even if an attacker compromises the Tomcat process, they + can't change the Tomcat configuration, deploy new web applications or + modify existing web applications. The Tomcat process runs with a umask of + 007 to maintain these permissions.

    +

    At the network level, consider using a firewall to limit both incoming + and outgoing connections to only those connections you expect to be + present.

    + + +

    The security of the JMX connection is dependent on the implementation + provided by the JRE and therefore falls outside the control of Tomcat.

    + +

    Typically, access control is very limited (either read-only to + everything or read-write to everything). Tomcat exposes a large amount + of internal information and control via JMX to aid debugging, monitoring + and management. Given the limited access control available, JMX access + should be treated as equivalent to local root/admin access and restricted + accordingly.

    + +

    The JMX access control provided by most (all?) JRE vendors does not + log failed authentication attempts, nor does it provide an account + lock-out feature after repeated failed authentications. This makes a + brute force attack easy to mount and difficult to detect.

    + +

    Given all of the above, care should be taken to ensure that, if used, + the JMX interface is appropriately secured. Options you may wish to + consider to secure the JMX interface include:

    + +
      +
    • configuring a strong password for all JMX users;
    • +
    • binding the JMX listener only to an internal network;
    • +
    • limiting network access to the JMX port to trusted clients; and
    • +
    • providing an application specific health page for use by external + monitoring systems.
    • +
    +
    + +
    + +
    + + +

    Tomcat ships with a number of web applications that are enabled by + default. Vulnerabilities have been discovered in these applications in the + past. Applications that are not required should be removed so the system + will not be at risk if another vulnerability is discovered.

    +
    + + +

    The ROOT web application presents a very low security risk but it does + include the version of Tomcat that is being used. The ROOT web application + should normally be removed from a publicly accessible Tomcat instance, not + for security reasons, but so that a more appropriate default page is shown + to users.

    +
    + + +

    The documentation web application presents a very low security risk but + it does identify the version of Tomcat that is being used. It should + normally be removed from a publicly accessible Tomcat instance.

    +
    + + +

    The examples web application should always be removed from any security + sensitive installation. While the examples web application does not + contain any known vulnerabilities, it is known to contain features + (particularly the cookie examples that display the contents of all + received and allow new cookies to be set) that may be used by an attacker + in conjunction with a vulnerability in another application deployed on the + Tomcat instance to obtain additional information that would otherwise be + unavailable.

    +
    + + +

    The Manager application allows the remote deployment of web + applications and is frequently targeted by attackers due to the widespread + use of weak passwords and publicly accessible Tomcat instances with the + Manager application enabled. The Manager application is not accessible by + default as no users are configured with the necessary access. If the + Manager application is enabled then guidance in the section + Securing Management Applications section should be + followed.

    +
    + + +

    The Host Manager application allows the creation and management of + virtual hosts - including the enabling of the Manager application for a + virtual host. The Host Manager application is not accessible by default + as no users are configured with the necessary access. If the Host Manager + application is enabled then guidance in the section Securing + Management Applications section should be followed.

    +
    + + +

    When deploying a web application that provides management functions for + the Tomcat instance, the following guidelines should be followed:

    +
      +
    • Ensure that any users permitted to access the management application + have strong passwords.
    • +
    • Do not remove the use of the LockOutRealm + which prevents brute force attacks against user passwords.
    • +
    • Configure the RemoteAddrValve + in the context.xml file for the + management application which limits access to localhost by default. + If remote access is required, limit it to specific IP addresses using + this valve.
    • +
    +
    +
    + +
    +

    Enabling the security manager causes web applications to be run in a + sandbox, significantly limiting a web application's ability to perform + malicious actions such as calling System.exit(), establishing network + connections or accessing the file system outside of the web application's + root and temporary directories. However, it should be noted that there are + some malicious actions, such as triggering high CPU consumption via an + infinite loop, that the security manager cannot prevent.

    + +

    Enabling the security manager is usually done to limit the potential + impact, should an attacker find a way to compromise a trusted web + application . A security manager may also be used to reduce the risks of + running untrusted web applications (e.g. in hosting environments) but it + should be noted that the security manager only reduces the risks of + running untrusted web applications, it does not eliminate them. If running + multiple untrusted web applications, it is recommended that each web + application is deployed to a separate Tomcat instance (and ideally separate + hosts) to reduce the ability of a malicious web application impacting the + availability of other applications.

    + +

    Tomcat is tested with the security manager enabled; but the majority of + Tomcat users do not run with a security manager, so Tomcat is not as well + user-tested in this configuration. There have been, and continue to be, + bugs reported that are triggered by running under a security manager.

    + +

    The restrictions imposed by a security manager are likely to break most + applications if the security manager is enabled. The security manager should + not be used without extensive testing. Ideally, the use of a security + manager should be introduced at the start of the development cycle as it can + be time-consuming to track down and fix issues caused by enabling a security + manager for a mature application.

    + +

    Enabling the security manager changes the defaults for the following + settings:

    +
      +
    • The default value for the deployXML attribute of the + Host element is changed to false.
    • +
    +
    + +
    + +

    The default server.xml contains a large number of comments, including + some example component definitions that are commented out. Removing these + comments makes it considerably easier to read and comprehend + server.xml.

    +

    If a component type is not listed, then there are no settings for that + type that directly impact security.

    +
    + + +

    Setting the port attribute to -1 disables + the shutdown port.

    +

    If the shutdown port is not disabled, a strong password should be + configured for shutdown.

    +
    + + +

    The APR Lifecycle Listener is not stable if compiled on Solaris using + gcc. If using the APR/native connector on Solaris, compile it with the + Sun Studio compiler.

    +

    The JNI Library Loading Listener may be used to load native code. It should + only be used to load trusted libraries.

    +

    The Security Lifecycle Listener should be enabled and configured as appropriate. +

    +
    + + +

    By default, a non-TLS, HTTP/1.1 connector is configured on port 8080. + Connectors that will not be used should be removed from server.xml.

    + +

    AJP Connectors should only be used on trusted networks or be + appropriately secured with a suitable secret attribute.

    + +

    AJP Connectors block forwarded requests with unknown request + attributes. Known safe and/or expected attributes may be allowed by + configuration an appropriate regular expression for the + allowedRequestAttributesPattern attribute.

    + +

    The address attribute may be used to control which IP + address a connector listens on for connections. By default, a connector + listens on all configured IP addresses.

    + +

    The allowBackslash attribute allows non-standard + parsing of the request URI. Setting this attribute to a non-default value + when behind a reverse proxy may enable an attacker to bypass any security + constraints enforced by the proxy.

    + +

    The allowTrace attribute may be used to enable TRACE + requests which can be useful for debugging. Due to the way some browsers + handle the response from a TRACE request (which exposes the browser to an + XSS attack), support for TRACE requests is disabled by default.

    + +

    The discardFacades attribute set to true + will cause a new facade object to be created for each request. This is + the default value, and this reduces the chances of a bug in an + application exposing data from one request to another.

    + +

    The encodedSolidusHandling attribute allows + non-standard parsing of the request URI. Setting this attribute to a + non-default value when behind a reverse proxy may enable an attacker to + bypass any security constraints enforced by the proxy.

    + +

    The enforceEncodingInGetWriter attribute has security + implications if set to false. Many user agents, in breach of + RFC 7230, try to guess the character encoding of text media types when the + specification-mandated default of ISO-8859-1 should be used. Some browsers + will interpret as UTF-7 a response containing characters that are safe for + ISO-8859-1 but trigger an XSS vulnerability if interpreted as UTF-7.

    + +

    The maxPostSize attribute controls the maximum size + of a POST request that will be parsed for parameters. The parameters are + cached for the duration of the request so this is limited to 2 MiB by + default to reduce exposure to a DOS attack.

    + +

    The maxSavePostSize attribute controls the saving of + the request body during FORM and CLIENT-CERT authentication and HTTP/1.1 + upgrade. For FORM authentication, the request body is cached in the HTTP + session for the duration of the authentication so the cached request body + is limited to 4 KiB by default to reduce exposure to a DOS attack. To + further reduce exposure to a DoS attack by limiting the permitted duration + of the FORM authentication, a reduced session timeout is used if the + session is created by the FORM authentication. This reduced timeout is + controlled by the authenticationSessionTimeout attribute of + the FORM + authenticator.

    + +

    The maxParameterCount attribute controls the maximum + total number of request parameters (including uploaded files) obtained + from the query string and, for POST requests, the request body if the + content type is application/x-www-form-urlencoded or + multipart/form-data. Excessive parameters are ignored. If you + want to reject such requests, configure a + FailedRequestFilter.

    + +

    The xpoweredBy attribute controls whether or not the + X-Powered-By HTTP header is sent with each request. If sent, the value of + the header contains the Servlet and JSP specification versions, the full + Tomcat version (e.g. Apache Tomcat/), the name of + the JVM vendor and + the version of the JVM. This header is disabled by default. This header + can provide useful information to both legitimate clients and attackers. +

    + +

    The server attribute controls the value of the Server + HTTP header. The default value of this header for Tomcat 4.1.x to + 8.0.x is Apache-Coyote/1.1. From 8.5.x onwards this header is not set by + default. This header can provide limited information to both legitimate + clients and attackers.

    + +

    The SSLEnabled, scheme and + secure attributes may all be independently set. These are + normally used when Tomcat is located behind a reverse proxy and the proxy + is connecting to Tomcat via HTTP or HTTPS. They allow Tomcat to see the + SSL attributes of the connections between the client and the proxy rather + than the proxy and Tomcat. For example, the client may connect to the + proxy over HTTPS but the proxy connects to Tomcat using HTTP. If it is + necessary for Tomcat to be able to distinguish between secure and + non-secure connections received by a proxy, the proxy must use separate + connectors to pass secure and non-secure requests to Tomcat. If the + proxy uses AJP then the SSL attributes of the client connection are + passed via the AJP protocol and separate connectors are not needed.

    + +

    The tomcatAuthentication and + tomcatAuthorization attributes are used with the + AJP connectors to determine if Tomcat should handle all authentication and + authorisation or if authentication should be delegated to the reverse + proxy (the authenticated user name is passed to Tomcat as part of the AJP + protocol) with the option for Tomcat to still perform authorization.

    + +

    The requiredSecret attribute in AJP connectors + configures shared secret between Tomcat and reverse proxy in front of + Tomcat. It is used to prevent unauthorized connections over AJP protocol.

    +
    + + +

    The host element controls deployment. Automatic deployment allows for + simpler management but also makes it easier for an attacker to deploy a + malicious application. Automatic deployment is controlled by the + autoDeploy and deployOnStartup + attributes. If both are false, only Contexts defined in + server.xml will be deployed and any changes will require a Tomcat restart. +

    + +

    In a hosted environment where web applications may not be trusted, set + the deployXML attribute to false to ignore + any context.xml packaged with the web application that may try to assign + increased privileges to the web application. Note that if the security + manager is enabled that the deployXML attribute will + default to false.

    +
    + + +

    This applies to Context + elements in all places where they can be defined: + server.xml file, + default context.xml file, + per-host context.xml.default file, + web application context file in per-host configuration directory + or inside the web application.

    + +

    The crossContext attribute controls if a context is + allowed to access the resources of another context. It is + false by default and should only be changed for trusted web + applications.

    + +

    The privileged attribute controls if a context is + allowed to use container provided servlets like the Manager servlet. It is + false by default and should only be changed for trusted web + applications.

    + +

    The allowLinking attribute of a nested + Resources element controls if a context + is allowed to use linked files. If enabled and the context is undeployed, + the links will be followed when deleting the context resources. Changing + this setting from the default of false on case insensitive + operating systems (this includes Windows) will disable a number of + security measures and allow, among other things, direct access to the + WEB-INF directory.

    + +

    The sessionCookiePathUsesTrailingSlash can be used to + work around a bug in a number of browsers (Internet Explorer, Safari and + Edge) to prevent session cookies being exposed across applications when + applications share a common path prefix. However, enabling this option + can create problems for applications with Servlets mapped to + /*. It should also be noted the RFC6265 section 8.5 makes it + clear that different paths should not be considered sufficient to isolate + cookies from other applications.

    + +

    When antiResourceLocking is enabled, Tomcat will copy + the unpacked web application to the directory defined by the + java.io.tmpdir system property + ($CATALINA_BASE/temp by default). This location should be + secured with appropriate file permissions - typically read/write for the + Tomcat user and no access for other users.

    +
    + + +

    It is strongly recommended that an AccessLogValve is configured. The + default Tomcat configuration includes an AccessLogValve. These are + normally configured per host but may also be configured per engine or per + context as required.

    + +

    Any administrative application should be protected by a + RemoteAddrValve (this Valve is also available as a Filter). + The allow attribute should be used to limit access to a + set of known trusted hosts.

    + +

    The default ErrorReportValve includes the Tomcat version number in the + response sent to clients. To avoid this, custom error handling can be + configured within each web application. Alternatively, you can explicitly + configure an ErrorReportValve and set its + showServerInfo attribute to false. + Alternatively, the version number can be changed by creating the file + CATALINA_BASE/lib/org/apache/catalina/util/ServerInfo.properties with + content as follows:

    + server.info=Apache Tomcat/.x +

    Modify the values as required. Note that this will also change the version + number reported in some of the management tools and may make it harder to + determine the real version installed. The CATALINA_HOME/bin/version.bat|sh + script will still report the correct version number.

    + +

    The default ErrorReportValve can display stack traces and/or JSP + source code to clients when an error occurs. To avoid this, custom error + handling can be configured within each web application. Alternatively, you + can explicitly configure an ErrorReportValve + and set its showReport attribute to false.

    + +

    The RewriteValve uses regular expressions and poorly formed regex + patterns may be vulnerable to "catastrophic backtracking" or "ReDoS". See + Rewrite docs for more details.

    +
    + + +

    The MemoryRealm is not intended for production use as any changes to + tomcat-users.xml require a restart of Tomcat to take effect.

    + +

    The UserDatabaseRealm is not intended for large-scale installations. It + is intended for small-scale, relatively static environments.

    + +

    The JAASRealm is not widely used and therefore the code is not as + mature as the other realms. Additional testing is recommended before using + this realm.

    + +

    By default, the realms do not implement any form of account lock-out. + This means that brute force attacks can be successful. To prevent a brute + force attack, the chosen realm should be wrapped in a LockOutRealm.

    +
    + + +

    The manager component is used to generate session IDs.

    + +

    The class used to generate random session IDs may be changed with + the randomClass attribute.

    + +

    The length of the session ID may be changed with the + sessionIdLength attribute.

    + +

    The persistAuthentication controls whether the + authenticated Principal associated with the session (if any) is included + when the session is persisted during a restart or to a Store.

    + +

    When using the JDBCStore, the session store should be + secured (dedicated credentials, appropriate permissions) such that only + the JDBCStore is able to access the persisted session + data. In particular, the JDBCStore should not be + accessible via any credentials available to a web application.

    +
    + + +

    The cluster implementation is written on the basis that a secure, + trusted network is used for all of the cluster related network traffic. It + is not safe to run a cluster on a insecure, untrusted network.

    + +

    If you require confidentiality and/or integrity protection then you can + use the + EncryptInterceptor + to encrypt traffic between nodes. This interceptor does not protect + against all the risks of running on an untrusted network, particularly + DoS attacks.

    +
    +
    + +
    +

    This applies to the default conf/web.xml file, the + /WEB-INF/tomcat-web.xml and the /WEB-INF/web.xml + files in web applications if they define the components mentioned here.

    + +

    The DefaultServlet is configured + with readonly set to + true. Changing this to false allows clients to + delete or modify static resources on the server and to upload new + resources. This should not normally be changed without requiring + authentication.

    + +

    The DefaultServlet is configured with listings set to + false. This isn't because allowing directory listings is + considered unsafe but because generating listings of directories with + thousands of files can consume significant CPU leading to a DOS attack. +

    + +

    The DefaultServlet is configured with showServerInfo + set to true. When the directory listings is enabled the Tomcat + version number is included in the response sent to clients. To avoid this, + you can explicitly configure a DefaultServlet and set its + showServerInfo attribute to false. + Alternatively, the version number can be changed by creating the file + CATALINA_BASE/lib/org/apache/catalina/util/ServerInfo.properties with + content as follows:

    + server.info=Apache Tomcat/.x +

    Modify the values as required. Note that this will also change the version + number reported in some of the management tools and may make it harder to + determine the real version installed. The CATALINA_HOME/bin/version.bat|sh + script will still report the correct version number. +

    + +

    The CGI Servlet is disabled by default. If enabled, the debug + initialisation parameter should not be set to 10 or higher on a + production system because the debug page is not secure.

    + +

    When using the CGI Servlet on Windows with + enableCmdLineArguments enabled, review the setting of + cmdLineArgumentsDecoded carefully and ensure that it is + appropriate for your environment. The default value is secure. Insecure + configurations may expose the server to remote code execution. Further + information on the potential risks and mitigations may be found by + following the links in the CGI How To.

    + +

    FailedRequestFilter + can be configured and used to reject requests that had errors during + request parameter parsing. Without the filter the default behaviour is + to ignore invalid or excessive parameters.

    + +

    HttpHeaderSecurityFilter can be + used to add headers to responses to improve security. If clients access + Tomcat directly, then you probably want to enable this filter and all the + headers it sets unless your application is already setting them. If Tomcat + is accessed via a reverse proxy, then the configuration of this filter needs + to be co-ordinated with any headers that the reverse proxy sets.

    +
    + +
    +

    When using embedded Tomcat, the typical defaults provided by the scripts, + server.xml and other configuration are not set. Users of embedded Tomcat may + wish to consider the following:

    +
      +
    • The listeners normally configured in server.xml, including + org.apache.catalina.security.SecurityListener, will not be + configured by default. They must be explicitly enabled if required.
    • +
    • The java.io.tmpdir will not be set (it is normally set to + $CATALINA_BASE/temp). This directory is used for various + temporary files that may be security sensitive including file uploads and + a copy of the web application if anti-resource locking is enabled. + Consider setting the java.io.tmpdir system property to an + appropriately secured directory.
    • +
    +
    + +
    +

    BASIC and FORM authentication pass user names and passwords in clear + text. Web applications using these authentication mechanisms with clients + connecting over untrusted networks should use SSL.

    + +

    The session cookie for a session with an authenticated user is nearly as + useful as the user's password to an attacker and should be afforded the same + level of protection as the password itself. This usually means + authenticating over SSL and continuing to use SSL until the session + ends.

    + +

    Tomcat's implementation of the Servlet API's file upload support may use + the directory defined by the java.io.tmpdir system property + ($CATALINA_BASE/temp by default) to store temporary files. This + location should be secured with appropriate file permissions - typically + read/write for the Tomcat user and no access for other users.

    +
    + + +
    diff --git a/webapps/docs/security-manager-howto.xml b/webapps/docs/security-manager-howto.xml new file mode 100644 index 0000000..b5d534c --- /dev/null +++ b/webapps/docs/security-manager-howto.xml @@ -0,0 +1,256 @@ + + + + +]> + + + &project; + + + Glenn Nielsen + Jean-Francois Arcand + Security Manager How-To + + + + +
    + +
    + +
    + +

    The Java SecurityManager is what allows a web browser + to run an applet in its own sandbox to prevent untrusted code from + accessing files on the local file system, connecting to a host other + than the one the applet was loaded from, and so on. In the same way + the SecurityManager protects you from an untrusted applet running in + your browser, use of a SecurityManager while running Tomcat can protect + your server from trojan servlets, JSPs, JSP beans, and tag libraries. + Or even inadvertent mistakes.

    + +

    Imagine if someone who is authorized to publish JSPs on your site + inadvertently included the following in their JSP:

    +]]> + +

    Every time this JSP was executed by Tomcat, Tomcat would exit. + Using the Java SecurityManager is just one more line of defense a + system administrator can use to keep the server secure and reliable.

    + +

    WARNING - A security audit + have been conducted using the Tomcat codebase. Most of the critical + package have been protected and a new security package protection mechanism + has been implemented. Still, make sure that you are satisfied with your SecurityManager + configuration before allowing untrusted users to publish web applications, + JSPs, servlets, beans, or tag libraries. However, running with a + SecurityManager is definitely better than running without one.

    + +
    + + +
    + +

    As of Java 17, the SecurityManager has been deprecated with the expectation + that it will be removed in a future Java version. Users currently using a + SecurityManager are recommended to start planning for its removal.

    + +
    + + +
    + +

    Permission classes are used to define what Permissions a class loaded + by Tomcat will have. There are a number of Permission classes that are + a standard part of the JDK, and you can create your own Permission class + for use in your own web applications. Both techniques are used in + Tomcat.

    + + + + +

    This is just a short summary of the standard system SecurityManager + Permission classes applicable to Tomcat. See + + http://docs.oracle.com/javase/7/docs/technotes/guides/security/ + for more information.

    + +
      +
    • java.util.PropertyPermission - Controls read/write + access to JVM properties such as java.home.
    • +
    • java.lang.RuntimePermission - Controls use of + some System/Runtime functions like exit() and + exec(). Also control the package access/definition.
    • +
    • java.io.FilePermission - Controls read/write/execute + access to files and directories.
    • +
    • java.net.SocketPermission - Controls use of + network sockets.
    • +
    • java.net.NetPermission - Controls use of + multicast network connections.
    • +
    • java.lang.reflect.ReflectPermission - Controls + use of reflection to do class introspection.
    • +
    • java.security.SecurityPermission - Controls access + to Security methods.
    • +
    • java.security.AllPermission - Allows access to all + permissions, just as if you were running Tomcat without a + SecurityManager.
    • +
    + +
    + +
    + + +
    + +

    Policy File Format

    + +

    The security policies implemented by the Java SecurityManager are + configured in the $CATALINA_BASE/conf/catalina.policy file. + This file completely replaces the java.policy file present + in your JDK system directories.

    + +

    Entries in the catalina.policy file use the standard + java.policy file format, as follows:

    +,] [codeBase ] { + permission [ [, ]]; +};]]> + +

    The signedBy and codeBase entries are + optional when granting permissions. Comment lines begin with "//" and + end at the end of the current line. The codeBase is in the + form of a URL, and for a file URL can use the ${java.home} + and ${catalina.home} properties (which are expanded out to + the directory paths defined for them by the JAVA_HOME, + CATALINA_HOME and CATALINA_BASE environment + variables).

    + +

    The Default Policy File

    + +

    The default $CATALINA_BASE/conf/catalina.policy file + looks like this:

    + + +&defaultpolicy; + +

    Starting Tomcat With A SecurityManager

    + +

    Once you have configured the catalina.policy file for use + with a SecurityManager, Tomcat can be started with a SecurityManager in + place by using the "-security" option:

    +$CATALINA_HOME/bin/catalina.sh start -security (Unix) +%CATALINA_HOME%\bin\catalina start -security (Windows) + + + +

    When using packed WAR files, it is necessary to use Tomcat's custom war + URL protocol to assign permissions to web application code.

    + +

    To assign permissions to the entire web application the entry in the + policy file would look like this:

    + + + +

    To assign permissions to a single JAR within the web application the + entry in the policy file would look like this:

    + + + +
    + +
    + +
    +

    Starting with Tomcat 5, it is now possible to configure which Tomcat + internal package are protected against package definition and access. See + + http://www.oracle.com/technetwork/java/seccodeguide-139067.html + for more information.

    + + +

    WARNING: Be aware that removing the default package protection + could possibly open a security hole

    + +

    The Default Properties File

    + +

    The default $CATALINA_BASE/conf/catalina.properties file + looks like this:

    + +

    Once you have configured the catalina.properties file for use + with a SecurityManager, remember to re-start Tomcat.

    +
    + +
    + +

    If your web application attempts to execute an operation that is + prohibited by lack of a required Permission, it will throw an + AccessControLException or a SecurityException + when the SecurityManager detects the violation. Debugging the permission + that is missing can be challenging, and one option is to turn on debug + output of all security decisions that are made during execution. This + is done by setting a system property before starting Tomcat. The easiest + way to do this is via the CATALINA_OPTS environment variable. + Execute this command:

    +export CATALINA_OPTS=-Djava.security.debug=all (Unix) +set CATALINA_OPTS=-Djava.security.debug=all (Windows) + +

    before starting Tomcat.

    + +

    WARNING - This will generate many megabytes + of output! However, it can help you track down problems by searching + for the word "FAILED" and determining which permission was being checked + for. See the Java security documentation for more options that you can + specify here as well.

    + +
    + + + + +
    diff --git a/webapps/docs/servletapi/index.html b/webapps/docs/servletapi/index.html new file mode 100644 index 0000000..b8a7a66 --- /dev/null +++ b/webapps/docs/servletapi/index.html @@ -0,0 +1,34 @@ + + + + + + API docs + + + + +The Servlet Javadoc is not installed by default. Download and install +the "fulldocs" package to get it. + +You can also access the javadoc online in the Tomcat + +documentation bundle. + + + diff --git a/webapps/docs/setup.xml b/webapps/docs/setup.xml new file mode 100644 index 0000000..b3e8028 --- /dev/null +++ b/webapps/docs/setup.xml @@ -0,0 +1,195 @@ + + + +]> + + + &project; + + + Remy Maucherat + Tomcat Setup + + + + +
    + +
    + +
    +

    + There are several ways to set up Tomcat for running on different + platforms. The main documentation for this is a file called + RUNNING.txt. We encourage you to refer to that + file if the information below does not answer some of your questions. +

    +
    + +
    + +

    + Installing Tomcat on Windows can be done easily using the Windows + installer. Its interface and functionality is similar to other wizard + based installers, with only a few items of interest. +

    + + +
      +
    • Installation as a service: Tomcat will be + installed as a Windows service no matter what setting is selected. + Using the checkbox on the component page sets the service as "auto" + startup, so that Tomcat is automatically started when Windows + starts. For optimal security, the service should be run as a + separate user, with reduced permissions (see the Windows Services + administration tool and its documentation).
    • +
    • Java location: The installer will provide a default + JRE to use to run the service. The installer uses the registry to + determine the base path of a Java or later JRE, + including the JRE installed as part of the full JDK. When running on + a 64-bit operating system, the installer will first look for a + 64-bit JRE and only look for a 32-bit JRE if a 64-bit JRE is not + found. If a JRE cannot be found when running on a 64-bit operating + system, the installer will look for a 64-bit JDK. Finally, if a JRE + or JDK has not been found, the installer will try to use the + JAVA_HOME environment variable. It is not mandatory to + use the default JRE detected by the installer. Any installed Java + or later JRE (32-bit or 64-bit) may be + used.
    • +
    • Tray icon: When Tomcat is run as a service, there + will not be any tray icon present when Tomcat is running. Note that + when choosing to run Tomcat at the end of installation, the tray + icon will be used even if Tomcat was installed as a service.
    • +
    • Defaults: The defaults used by the installer may be + overridden by use of the /C=<config file> command + line argument. The configuration file uses the format + name=value with each pair on a separate line. The names + of the available configuration options are: +
        +
      • JavaHome
      • +
      • TomcatPortShutdown
      • +
      • TomcatPortHttp
      • +
      • TomcatMenuEntriesEnable
      • +
      • TomcatShortcutAllUsers
      • +
      • TomcatServiceDefaultName
      • +
      • TomcatServiceName
      • +
      • TomcatServiceFileName
      • +
      • TomcatServiceManagerFileName
      • +
      • TomcatAdminEnable
      • +
      • TomcatAdminUsername
      • +
      • TomcatAdminPassword
      • +
      • TomcatAdminRoles
      • +
      + By using /C=... along with /S and + /D= it is possible to perform fully configured + unattended installs of Apache Tomcat. +
    • +
    • Refer to the + Windows Service How-To + for information on how to manage Tomcat as a Windows service. +
    • +
    + + +

    The installer will create shortcuts allowing starting and configuring + Tomcat. It is important to note that the Tomcat administration web + application can only be used when Tomcat is running.

    + +
    + +
    + +

    Tomcat can be run as a daemon using the jsvc tool from the + commons-daemon project. Source tarballs for jsvc are included with the + Tomcat binaries, and need to be compiled. Building jsvc requires + a C ANSI compiler (such as GCC), GNU Autoconf, and a JDK.

    + +

    Before running the script, the JAVA_HOME environment + variable should be set to the base path of the JDK. Alternately, when + calling the ./configure script, the path of the JDK may + be specified using the --with-java parameter, such as + ./configure --with-java=/usr/java.

    + +

    Using the following commands should result in a compiled jsvc binary, + located in the $CATALINA_HOME/bin folder. This assumes + that GNU TAR is used, and that CATALINA_HOME is an + environment variable pointing to the base path of the Tomcat + installation.

    + +

    Please note that you should use the GNU make (gmake) instead of + the native BSD make on FreeBSD systems.

    + +cd $CATALINA_HOME/bin +tar xvfz commons-daemon-native.tar.gz +cd commons-daemon-1.1.x-native-src/unix +./configure +make +cp jsvc ../.. +cd ../.. + +

    Tomcat can then be run as a daemon using the following commands.

    + +CATALINA_BASE=$CATALINA_HOME +cd $CATALINA_HOME +./bin/jsvc \ + -classpath $CATALINA_HOME/bin/bootstrap.jar:$CATALINA_HOME/bin/tomcat-juli.jar \ + -outfile $CATALINA_BASE/logs/catalina.out \ + -errfile $CATALINA_BASE/logs/catalina.err \ + --add-opens=java.base/java.lang=ALL-UNNAMED \ + --add-opens=java.base/java.io=ALL-UNNAMED \ + --add-opens=java.base/java.util=ALL-UNNAMED \ + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED \ + --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED \ + -Dcatalina.home=$CATALINA_HOME \ + -Dcatalina.base=$CATALINA_BASE \ + -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \ + -Djava.util.logging.config.file=$CATALINA_BASE/conf/logging.properties \ + org.apache.catalina.startup.Bootstrap + +

    You may also need to specify -jvm server if the JVM defaults + to using a server VM rather than a client VM. This has been observed on + OSX.

    + +

    jsvc has other useful parameters, such as -user which + causes it to switch to another user after the daemon initialization is + complete. This allows, for example, running Tomcat as a non privileged + user while still being able to use privileged ports. Note that if you + use this option and start Tomcat as root, you'll need to disable the + org.apache.catalina.security.SecurityListener check that + prevents Tomcat starting when running as root.

    + +

    jsvc --help will return the full jsvc usage + information. In particular, the -debug option is useful + to debug issues running jsvc.

    + +

    The file $CATALINA_HOME/bin/daemon.sh can be used as a + template for starting Tomcat automatically at boot time from + /etc/init.d with jsvc.

    + +

    Note that the Commons-Daemon JAR file must be on your runtime classpath + to run Tomcat in this manner. The Commons-Daemon JAR file is in the + Class-Path entry of the bootstrap.jar manifest, but if you get a + ClassNotFoundException or a NoClassDefFoundError for a Commons-Daemon + class, add the Commons-Daemon JAR to the -cp argument when launching + jsvc.

    + +
    + + +
    diff --git a/webapps/docs/ssi-howto.xml b/webapps/docs/ssi-howto.xml new file mode 100644 index 0000000..234ae2c --- /dev/null +++ b/webapps/docs/ssi-howto.xml @@ -0,0 +1,440 @@ + + + +]> + + +&project; + + +Glenn L. Nielsen +SSI How To + + + + +
    + +
    + +
    + +

    SSI (Server Side Includes) are directives that are placed in HTML pages, +and evaluated on the server while the pages are being served. They let you +add dynamically generated content to an existing HTML page, without having +to serve the entire page via a CGI program, or other dynamic technology. +

    + +

    Within Tomcat SSI support can be added when using Tomcat as your +HTTP server and you require SSI support. Typically this is done +during development when you don't want to run a web server like Apache.

    + +

    Tomcat SSI support implements the same SSI directives as Apache. See the + +Apache Introduction to SSI for information on using SSI directives.

    + +

    SSI support is available as a servlet and as a filter. You should use one +or the other to provide SSI support but not both.

    + +

    Servlet based SSI support is implemented using the class +org.apache.catalina.ssi.SSIServlet. Traditionally, this servlet +is mapped to the URL pattern "*.shtml".

    + +

    Filter based SSI support is implemented using the class +org.apache.catalina.ssi.SSIFilter. Traditionally, this filter +is mapped to the URL pattern "*.shtml", though it can be mapped to "*" as +it will selectively enable/disable SSI processing based on mime types. The +contentType init param allows you to apply SSI processing to JSP pages, +JavaScript, or any other content you wish.

    +

    By default SSI support is disabled in Tomcat.

    +
    + +
    + +

    CAUTION - SSI directives can be used to execute programs +external to the Tomcat JVM. If you are using the Java SecurityManager this +will bypass your security policy configuration in catalina.policy. +

    + +

    To use the SSI servlet, remove the XML comments from around the SSI servlet +and servlet-mapping configuration in +$CATALINA_BASE/conf/web.xml.

    + +

    To use the SSI filter, remove the XML comments from around the SSI filter +and filter-mapping configuration in +$CATALINA_BASE/conf/web.xml.

    + +

    Only Contexts which are marked as privileged may use SSI features (see the +privileged property of the Context element).

    + +
    + +
    + +

    There are several servlet init parameters which can be used to +configure the behaviour of the SSI servlet.

    +
      +
    • buffered - Should output from this servlet be buffered? +(0=false, 1=true) Default 0 (false).
    • +
    • debug - Debugging detail level for messages logged +by this servlet. Default 0.
    • +
    • expires - The number of seconds before a page with SSI +directives will expire. Default behaviour is for all SSI directives to be +evaluated for every request.
    • +
    • isVirtualWebappRelative - Should "virtual" SSI directive +paths be interpreted as relative to the context root, instead of the server +root? Default false.
    • +
    • inputEncoding - The encoding to be assumed for SSI +resources if one cannot be determined from the resource itself. Default is +the default platform encoding.
    • +
    • outputEncoding - The encoding to be used for the result +of the SSI processing. Default is UTF-8.
    • +
    • allowExec - Is the exec command enabled? Default is +false.
    • +
    + + +
    + +
    + +

    There are several filter init parameters which can be used to +configure the behaviour of the SSI filter.

    +
      +
    • contentType - A regex pattern that must be matched before +SSI processing is applied. When crafting your own pattern, don't forget that a +mime content type may be followed by an optional character set in the form +"mime/type; charset=set" that you must take into account. Default is +"text/x-server-parsed-html(;.*)?".
    • +
    • debug - Debugging detail level for messages logged +by this servlet. Default 0.
    • +
    • expires - The number of seconds before a page with SSI +directives will expire. Default behaviour is for all SSI directives to be +evaluated for every request.
    • +
    • isVirtualWebappRelative - Should "virtual" SSI directive +paths be interpreted as relative to the context root, instead of the server +root? Default false.
    • +
    • allowExec - Is the exec command enabled? Default is +false.
    • +
    + + +
    + +
    +

    Server Side Includes are invoked by embedding SSI directives in an HTML document + whose type will be processed by the SSI servlet. The directives take the form of an HTML + comment. The directive is replaced by the results of interpreting it before sending the + page to the client. The general form of a directive is:

    +

    <!--#directive [param=value] -->

    +

    The directives are:

    +
      +
    • +config - <!--#config errmsg="Error occurred" sizefmt="abbrev" +timefmt="%B %Y" --> +Used to set SSI error message, the format of dates and file sizes processed by SSI.
      +All are optional but at least one must be used. The available options are as follows: +
      +errmsg - error message used for SSI errors
      +sizefmt - format used for sizes in the fsize directive
      +timefmt - format used for timestamps in the flastmod directive
      +
    • +
    • +echo - <!--#echo var="VARIABLE_NAME" encoding="entity" --> +will be replaced by the value of the variable. +
      +The optional encoding parameter specifies the type of encoding to use. +Valid values are entity (default), url or none. +NOTE: Using an encoding other than entity can lead to security issues. +
    • +
    • +exec - <!--#exec cmd="file-name" --> +Used to run commands on the host system. +
    • +
    • +exec - <!--#exec cgi="file-name" --> +This acts the same as the include virtual directive, and doesn't actually execute any commands. +
    • +
    • +include - <!--#include file="file-name" --> +inserts the contents. The path is interpreted relative to the document where this directive +is being used, and IS NOT a "virtual" path relative to either the context root or the server root. +
    • +
    • +include - <!--#include virtual="file-name" --> +inserts the contents. The path is interpreted as a "virtual" path which is +relative to either the context root or the server root (depending on the isVirtualWebappRelative +parameter). +
    • +
    • +flastmod - <!--#flastmod file="filename.shtml" --> +Returns the time that a file was last modified. The path is interpreted relative to the document where this directive +is being used, and IS NOT a "virtual" path relative to either the context root or the server root. +
    • +
    • +flastmod - <!--#flastmod virtual="filename.shtml" --> +Returns the time that a file was last modified. The path is interpreted as a "virtual" path which is +relative to either the context root or the server root (depending on the isVirtualWebappRelative +parameter). +
    • +
    • +fsize - <!--#fsize file="filename.shtml" --> +Returns the size of a file. The path is interpreted relative to the document where this directive +is being used, and IS NOT a "virtual" path relative to either the context root or the server root. +
    • +
    • +fsize - <!--#fsize virtual="filename.shtml" --> +Returns the size of a file. The path is interpreted as a "virtual" path which is +relative to either the context root or the server root (depending on the isVirtualWebappRelative +parameter). +
    • +
    • +printenv - <!--#printenv --> +Returns the list of all the defined variables. +
    • +
    • +set - <!--#set var="foo" value="Bar" --> +is used to assign a value to a user-defined variable. +
    • +
    • +if elif endif else - Used to create conditional sections. For example: + + +

      Meeting at 10:00 on Mondays

      + +

      Turn in your time card

      + +

      Yoga class at noon.

      +]]> +
    • +
    +

    +See the + +Apache Introduction to SSI for more information on using SSI directives.

    +
    + +
    +

    +SSI variables are implemented via request attributes on the jakarta.servlet.ServletRequest object +and are not limited to the SSI servlet. Variables starting with the names +"java.", "javax.", "sun" or "org.apache.catalina.ssi.SSIMediator." are reserved +and cannot be used. +

    +

    The SSI servlet currently implements the following variables: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Variable NameDescription
    AUTH_TYPE + The type of authentication used for this user: BASIC, FORM, etc.
    CONTENT_LENGTH + The length of the data (in bytes or the number of + characters) passed from a form.
    CONTENT_TYPE + The MIME type of the query data, such as "text/html".
    DATE_GMT +Current date and time in GMT
    DATE_LOCAL +Current date and time in the local time zone
    DOCUMENT_NAME +The current file
    DOCUMENT_URI +Virtual path to the file
    GATEWAY_INTERFACE + The revision of the Common Gateway Interface that the + server uses if enabled: "CGI/1.1".
    HTTP_ACCEPT + A list of the MIME types that the client can accept.
    HTTP_ACCEPT_ENCODING + A list of the compression types that the client can accept.
    HTTP_ACCEPT_LANGUAGE + A list of the languages that the client can accept.
    HTTP_CONNECTION + The way that the connection from the client is being managed: + "Close" or "Keep-Alive".
    HTTP_HOST + The web site that the client requested.
    HTTP_REFERER + The URL of the document that the client linked from.
    HTTP_USER_AGENT + The browser the client is using to issue the request.
    LAST_MODIFIED +Last modification date and time for current file
    PATH_INFO + Extra path information passed to a servlet.
    PATH_TRANSLATED + The translated version of the path given by the + variable PATH_INFO.
    QUERY_STRING +The query string that follows the "?" in the URL. +
    QUERY_STRING_UNESCAPED +Undecoded query string with all shell metacharacters escaped +with "\"
    REMOTE_ADDR + The remote IP address of the user making the request.
    REMOTE_HOST + The remote hostname of the user making the request.
    REMOTE_PORT + The port number at remote IP address of the user making the request.
    REMOTE_USER + The authenticated name of the user.
    REQUEST_METHOD + The method with which the information request was + issued: "GET", "POST" etc.
    REQUEST_URI + The web page originally requested by the client.
    SCRIPT_FILENAME + The location of the current web page on the server.
    SCRIPT_NAME + The name of the web page.
    SERVER_ADDR + The server's IP address.
    SERVER_NAME + The server's hostname or IP address.
    SERVER_PORT + The port on which the server received the request.
    SERVER_PROTOCOL + The protocol used by the server. E.g. "HTTP/1.1".
    SERVER_SOFTWARE + The name and version of the server software that is + answering the client request.
    UNIQUE_ID + A token used to identify the current session if one + has been established.
    +
    + + + +
    diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml new file mode 100644 index 0000000..ec274dc --- /dev/null +++ b/webapps/docs/ssl-howto.xml @@ -0,0 +1,672 @@ + + + +]> + + + &project; + + + Christopher Cain + Yoav Shapira + SSL/TLS Configuration How-To + + + + +
    + +
    + +
    + +

    The description below uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.

    + +

    To install and configure SSL/TLS support on Tomcat, you need to follow +these simple steps. For more information, read the rest of this How-To.

    +
      +
    1. Create a keystore file to store the server's private key and +self-signed certificate by executing the following command:

      +

      Windows:

      +"%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA +

      Unix:

      +$JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA + +

      and specify a password value of "changeit".

    2. +
    3. Uncomment the "SSL HTTP/1.1 Connector" entry in + $CATALINA_BASE/conf/server.xml and modify as described in + the Configuration section below.

    4. + +
    + + +
    + + +
    + +

    Transport Layer Security (TLS) and its predecessor, Secure Sockets Layer +(SSL), are technologies which allow web browsers and web servers to communicate +over a secured connection. This means that the data being sent is encrypted by +one side, transmitted, then decrypted by the other side before processing. +This is a two-way process, meaning that both the server AND the browser encrypt +all traffic before sending out data.

    + +

    Another important aspect of the SSL/TLS protocol is Authentication. This means +that during your initial attempt to communicate with a web server over a secure +connection, that server will present your web browser with a set of +credentials, in the form of a "Certificate", as proof the site is who and what +it claims to be. In certain cases, the server may also request a Certificate +from your web browser, asking for proof that you are who you claim +to be. This is known as "Client Authentication," although in practice this is +used more for business-to-business (B2B) transactions than with individual +users. Most SSL-enabled web servers do not request Client Authentication.

    + +
    + +
    + +

    It is important to note that configuring Tomcat to take advantage of +secure sockets is usually only necessary when running it as a stand-alone +web server. Details can be found in the +Security Considerations Document. +When running Tomcat primarily as a Servlet/JSP container behind +another web server, such as Apache or Microsoft IIS, it is usually necessary +to configure the primary web server to handle the SSL connections from users. +Typically, this server will negotiate all SSL-related functionality, then +pass on any requests destined for the Tomcat container only after decrypting +those requests. Likewise, Tomcat will return cleartext responses, that will +be encrypted before being returned to the user's browser. In this environment, +Tomcat knows that communications between the primary web server and the +client are taking place over a secure connection (because your application +needs to be able to ask about this), but it does not participate in the +encryption or decryption itself.

    + +

    Tomcat is able to use any of the the cryptographic protocols that are +provided by the underlying environment. Java itself provides cryptographic +capabilities through JCE/JCA +and encrypted communications capabilities through JSSE. +Any compliant cryptographic "provider" can provide cryptographic algorithms +to Tomcat. The built-in provider (SunJCE) includes support for various +SSL/TLS versions like SSLv3, TLSv1, TLSv1.1, and so on. Check the documentation +for your version of Java for details on protocol and algorithm support.

    + +

    If you use the optional tcnative library, you can use +the OpenSSL cryptographic provider +through JCA/JCE/JSSE which may provide a different selection of cryptographic +algorithms and/or performance benefits relative to the SunJCE provider. +Check the documentation for your version of OpenSSL for details on protocol and +algorithm support.

    + +
    + +
    + +

    In order to implement SSL, a web server must have an associated Certificate +for each external interface (IP address) that accepts secure connections. +The theory behind this design is that a server should provide some kind of +reasonable assurance that its owner is who you think it is, particularly +before receiving any sensitive information. While a broader explanation of +Certificates is beyond the scope of this document, think of a Certificate as a +"digital passport" for an Internet address. It states which organisation the +site is associated with, along with some basic contact information about the +site owner or administrator.

    + +

    This certificate is cryptographically signed by its owner, and is +therefore extremely difficult for anyone else to forge. For the certificate to +work in the visitors browsers without warnings, it needs to be signed by a +trusted third party. These are called Certificate Authorities (CAs). To +obtain a signed certificate, you need to choose a CA and follow the instructions +your chosen CA provides to obtain your certificate. A range of CAs is available +including some that offer certificates at no cost.

    + +

    Java provides a relatively simple command-line tool, called +keytool, which can easily create a "self-signed" Certificate. +Self-signed Certificates are simply user generated Certificates which have not +been signed by a well-known CA and are, therefore, not really guaranteed to be +authentic at all. While self-signed certificates can be useful for some testing +scenarios, they are not suitable for any form of production use.

    + +
    + +
    + +

    When securing a website with SSL it's important to make sure that all assets +that the site uses are served over SSL, so that an attacker can't bypass +the security by injecting malicious content in a JavaScript file or similar. To +further enhance the security of your website, you should evaluate to use the +HSTS header. It allows you to communicate to the browser that your site should +always be accessed over https.

    + +

    Using name-based virtual hosts on a secured connection requires careful +configuration of the names specified in a single certificate or Tomcat 8.5 +onwards where Server Name Indication (SNI) support is available. SNI allows +multiple certificates with different names to be associated with a single TLS +connector.

    + +
    + +
    + + + +

    Tomcat currently operates only on JKS, PKCS11 or +PKCS12 format keystores. The JKS format +is Java's standard "Java KeyStore" format, and is the format created by the +keytool command-line utility. This tool is included in the JDK. +The PKCS12 format is an internet standard, and can be manipulated +via (among other things) OpenSSL and Microsoft's Key-Manager. +

    + +

    Each entry in a keystore is identified by an alias string. Whilst many +keystore implementations treat aliases in a case insensitive manner, case +sensitive implementations are available. The PKCS11 specification, +for example, requires that aliases are case sensitive. To avoid issues related +to the case sensitivity of aliases, it is not recommended to use aliases that +differ only in case. +

    + +

    To import an existing certificate into a JKS keystore, please read the +documentation (in your JDK documentation package) about keytool. +Note that OpenSSL often adds readable comments before the key, but +keytool does not support that. So if your certificate has +comments before the key data, remove them before importing the certificate with +keytool. +

    +

    To import an existing certificate signed by your own CA into a PKCS12 +keystore using OpenSSL you would execute a command like:

    +openssl pkcs12 -export -in mycert.crt -inkey mykey.key + -out mycert.p12 -name tomcat -CAfile myCA.crt + -caname root -chain +

    For more advanced cases, consult the +OpenSSL documentation.

    + +

    To create a new JKS keystore from scratch, containing a single +self-signed Certificate, execute the following from a terminal command line:

    +

    Windows:

    +"%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA +

    Unix:

    +$JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA + +

    (The RSA algorithm should be preferred as a secure algorithm, and this +also ensures general compatibility with other servers and components.)

    + +

    This command will create a new file, in the home directory of the user +under which you run it, named ".keystore". To specify a +different location or filename, add the -keystore parameter, +followed by the complete pathname to your keystore file, +to the keytool command shown above. You will also need to +reflect this new location in the server.xml configuration file, +as described later. For example:

    +

    Windows:

    +"%JAVA_HOME%\bin\keytool" -genkey -alias tomcat -keyalg RSA + -keystore \path\to\my\keystore +

    Unix:

    +$JAVA_HOME/bin/keytool -genkey -alias tomcat -keyalg RSA + -keystore /path/to/my/keystore + +

    After executing this command, you will first be prompted for the keystore +password. The default password used by Tomcat is "changeit" +(all lower case), although you can specify a custom password if you like. +You will also need to specify the custom password in the +server.xml configuration file, as described later.

    + +

    Next, you will be prompted for general information about this Certificate, +such as company, contact name, and so on. This information will be displayed +to users who attempt to access a secure page in your application, so make +sure that the information provided here matches what they will expect.

    + +

    Finally, you will be prompted for the key password, which is the +password specifically for this Certificate (as opposed to any other +Certificates stored in the same keystore file). The keytool prompt +will tell you that pressing the ENTER key automatically uses the same password +for the key as the keystore. You are free to use the same password or to select +a custom one. If you select a different password to the keystore password, you +will also need to specify the custom password in the server.xml +configuration file.

    + +

    If everything was successful, you now have a keystore file with a +Certificate that can be used by your server.

    + +
    + + +

    +Tomcat can use two different implementations of SSL: +

    +
      +
    • JSSE implementation provided as part of the Java runtime
    • +
    • JSSE implementation that uses OpenSSL
    • +
    +

    +The exact configuration details depend on which implementation is being used. +If you configured Connector by specifying generic +protocol="HTTP/1.1" then the implementation used by Tomcat is +chosen automatically. If the installation uses APR +- i.e. you have installed the Tomcat native library - +then it will use the JSSE OpenSSL implementation, otherwise it will use the Java +JSSE implementation. +

    + +

    +Auto-selection of implementation can be avoided if needed. It is done by specifying a classname +in the protocol attribute of the Connector.

    + +

    To define a Java (JSSE) connector, regardless of whether the APR library is +loaded or not, use one of the following:

    + + + + +]]> + +

    The OpenSSL JSSE implementation can also be configured explicitly if needed. +If the APR library is installed, using the sslImplementationName attribute +allows enabling it. When using the OpenSSL JSSE implementation, the +configuration can use either the JSSE attributes or the OpenSSL attributes, but +must not mix attributes from both types in the same SSLHostConfig or Connector +element.

    + +]]> + +

    If you are using JSSE OpenSSL, you have the option of configuring an alternative engine to OpenSSL.

    +]]> +

    The default value is

    +]]> +

    +So to enable OpenSSL, make sure the SSLEngine attribute is set to something other than off. +The default value is on and if you specify another value, +it has to be a valid OpenSSL engine name. +

    + +

    +SSLRandomSeed allows to specify a source of entropy. Productive system needs a reliable source of entropy +but entropy may need a lot of time to be collected therefore test systems could use no blocking entropy +sources like "/dev/urandom" that will allow quicker starts of Tomcat. +

    + +

    The final step is to configure the Connector in the +$CATALINA_BASE/conf/server.xml file, where +$CATALINA_BASE represents the base directory for the +Tomcat instance. An example <Connector> element +for an SSL connector is included in the default server.xml +file installed with Tomcat. To configure an SSL connector that uses JSSE with +the JSSE configuration style, you will need to remove the comments and edit it +so it looks something like this:

    + + + + + +]]> +

    + Note: If tomcat-native is installed, the configuration will use JSSE with + an OpenSSL implementation.

    +

    + The APR configuration style uses different attributes for many SSL settings, + particularly keys and certificates. An example of an APR configuration style + is:

    + + + + + +]]> + + +

    The configuration options and information on which attributes +are mandatory, are documented in the SSL Support section of the +HTTP connector configuration +reference. Tomcat supports either configuration style (JSSE or OpenSSL) with all +TLS connectors.

    + +

    The port attribute is the TCP/IP +port number on which Tomcat will listen for secure connections. You can +change this to any port number you wish (such as to the default port for +https communications, which is 443). However, special setup +(outside the scope of this document) is necessary to run Tomcat on port +numbers lower than 1024 on many operating systems.

    + +

    If you change the port number here, you should also change the + value specified for the redirectPort attribute on the + non-SSL connector. This allows Tomcat to automatically redirect + users who attempt to access a page with a security constraint specifying + that SSL is required, as required by the Servlet Specification.

    + +

    After completing these configuration changes, you must restart Tomcat as +you normally do, and you should be in business. You should be able to access +any web application supported by Tomcat via SSL. For example, try:

    +https://localhost:8443/ +

    and you should see the usual Tomcat splash page (unless you have modified +the ROOT web application). If this does not work, the following section +contains some troubleshooting tips.

    + +
    + +
    + +
    +

    To obtain and install a Certificate from a Certificate Authority (like verisign.com, thawte.com +or trustcenter.de), read the previous section and then follow these instructions:

    + + +

    In order to obtain a Certificate from the Certificate Authority of your choice +you have to create a so called Certificate Signing Request (CSR). That CSR will be used +by the Certificate Authority to create a Certificate that will identify your website +as "secure". To create a CSR follow these steps:

    +
      +
    • Create a local self-signed Certificate (as described in the previous section): + keytool -genkey -alias tomcat -keyalg RSA + -keystore <your_keystore_filename> + Note: In some cases you will have to enter the domain of your website (i.e. www.myside.org) + in the field "first- and lastname" in order to create a working Certificate. +
    • +
    • The CSR is then created with: + keytool -certreq -keyalg RSA -alias tomcat -file certreq.csr + -keystore <your_keystore_filename> +
    • +
    +

    Now you have a file called certreq.csr that you can submit to the Certificate Authority (look at the +documentation of the Certificate Authority website on how to do this). In return you get a Certificate.

    +
    + + +

    Now that you have your Certificate you can import it into you local keystore. +First of all you have to import a so called Chain Certificate or Root Certificate into your keystore. +After that you can proceed with importing your Certificate.

    + +
      +
    • Download a Chain Certificate from the Certificate Authority you obtained the Certificate from.
      + For Verisign.com commercial certificates go to: + http://www.verisign.com/support/install/intermediate.html
      + For Verisign.com trial certificates go to: + http://www.verisign.com/support/verisign-intermediate-ca/Trial_Secure_Server_Root/index.html
      + For Trustcenter.de go to: + http://www.trustcenter.de/certservices/cacerts/en/en.htm#server
      + For Thawte.com go to: + http://www.thawte.com/certs/trustmap.html
      +
    • +
    • Import the Chain Certificate into your keystore + keytool -import -alias root -keystore <your_keystore_filename> + -trustcacerts -file <filename_of_the_chain_certificate> +
    • +
    • And finally import your new Certificate + keytool -import -alias tomcat -keystore <your_keystore_filename> + -file <your_certificate_filename> +
    • +
    + +

    Each Certificate Authority tends to differ slightly from the others. They may +require slightly different information and/or provide the certificate and +associated certificate chain in different formats. Additionally, the rules that +the Certificate Authorities use for issuing certificates change over time. As a +result you may find that the commands given above may need to be modified. If +you require assitance then help is available via the +Apache Tomcat users +mailing list.

    + +
    +
    + +
    +

    To use Online Certificate Status Protocol (OCSP) with Apache Tomcat, ensure + you have downloaded, installed, and configured the + + Tomcat Native Connector. +Furthermore, if you use the Windows platform, ensure you download the +ocsp-enabled connector.

    +

    To use OCSP, you require the following:

    + +
      +
    • OCSP-enabled certificates
    • +
    • Tomcat with SSL APR connector
    • +
    • Configured OCSP responder
    • +
    + + +

    Apache Tomcat requires the OCSP-enabled certificate to have the OCSP + responder location encoded in the certificate. The basic OCSP-related + certificate authority settings in the openssl.cnf file could look + as follows:

    + + +#... omitted for brevity + +[x509] +x509_extensions = v3_issued + +[v3_issued] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer +# The address of your responder +authorityInfoAccess = OCSP;URI:http://127.0.0.1:8088 +keyUsage = critical,digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment,keyAgreement,keyCertSign,cRLSign,encipherOnly,decipherOnly +basicConstraints=critical,CA:FALSE +nsComment="Testing OCSP Certificate" + +#... omitted for brevity + + +

    The settings above encode the OCSP responder address + 127.0.0.1:8088 into the certificate. Note that for the following + steps, you must have openssl.cnf and other configuration of + your CA ready. To generate an OCSP-enabled certificate:

    + +
      +
    • + Create a private key: + openssl genrsa -aes256 -out ocsp-cert.key 4096 +
    • +
    • + Create a signing request (CSR): + openssl req -config openssl.cnf -new -sha256 \ + -key ocsp-cert.key -out ocsp-cert.csr
    • +
    • + Sign the CSR: + openssl ca -openssl.cnf -extensions ocsp -days 375 -notext \ + -md sha256 -in ocsp-cert.csr -out ocsp-cert.crt +
    • +
    • + You may verify the certificate: + openssl x509 -noout -text -in ocsp-cert.crt +
    • +
    +
    + +
    + +
    + +

    Additional information may be obtained about TLS handshake failures by +configuring the dedicated TLS handshake logger to log debug level messages by +adding the following to $CATALINA_BASE/conf/logging.properties: +org.apache.tomcat.util.net.NioEndpoint.handshake.level=FINE +or +org.apache.tomcat.util.net.Nio2Endpoint.handshake.level=FINE +depending on the Connector being used. +

    + +

    Here is a list of common problems that you may encounter when setting up +SSL communications, and what to do about them.

    + +
      + +
    • When Tomcat starts up, I get an exception like + "java.io.FileNotFoundException: {some-directory}/{some-file} not found". + +

      A likely explanation is that Tomcat cannot find the keystore file + where it is looking. By default, Tomcat expects the keystore file to + be named .keystore in the user home directory under which + Tomcat is running (which may or may not be the same as yours :-). If + the keystore file is anywhere else, you will need to add a + certificateKeystoreFile attribute to the + <Certificate> + element in the Tomcat + configuration file.

      +
    • + +
    • When Tomcat starts up, I get an exception like + "java.io.FileNotFoundException: Keystore was tampered with, or + password was incorrect". + +

      Assuming that someone has not actually tampered with + your keystore file, the most likely cause is that Tomcat is using + a different password than the one you used when you created the + keystore file. To fix this, you can either go back and + recreate the keystore + file, or you can add or update the keystorePass + attribute on the <Connector> element in the + Tomcat configuration + file. REMINDER - Passwords are case sensitive!

      +
    • + +
    • When Tomcat starts up, I get an exception like + "java.net.SocketException: SSL handshake error javax.net.ssl.SSLException: No + available certificate or key corresponds to the SSL cipher suites which are + enabled." + +

      A likely explanation is that Tomcat cannot find the alias for the server + key within the specified keystore. Check that the correct + certificateKeystoreFile and certificateKeyAlias + are specified in the <Certificate> element in the + Tomcat configuration file. + REMINDER - keyAlias values may be case + sensitive!

      +
    • + +
    + +

    If you are still having problems, a good source of information is the +TOMCAT-USER mailing list. You can find pointers to archives +of previous messages on this list, as well as subscription and unsubscription +information, at +https://tomcat.apache.org/lists.html.

    + +
    + +
    +

    This is a new feature in the Servlet 3.0 specification. Because it uses the + SSL session ID associated with the physical client-server connection there + are some limitations. They are:

    +
      +
    • Tomcat must have a connector with the attribute + isSecure set to true.
    • +
    • If SSL connections are managed by a proxy or a hardware accelerator + they must populate the SSL request headers (see the + SSLValve) so that + the SSL session ID is visible to Tomcat.
    • +
    • If Tomcat terminates the SSL connection, it will not be possible to use + session replication as the SSL session IDs will be different on each + node.
    • +
    + +

    + To enable SSL session tracking you need to use a context listener to set the + tracking mode for the context to be just SSL (if any other tracking mode is + enabled, it will be used in preference). It might look something like:

    + modes = + EnumSet.of(SessionTrackingMode.SSL); + + context.setSessionTrackingModes(modes); + } + +}]]> + +
    + +
    + +

    To access the SSL session ID from the request, use:

    + + +

    +For additional discussion on this area, please see +Bugzilla. +

    + +

    To terminate an SSL session, use:

    + +
    + + + +
    diff --git a/webapps/docs/tomcat-docs.xsl b/webapps/docs/tomcat-docs.xsl new file mode 100644 index 0000000..83d2393 --- /dev/null +++ b/webapps/docs/tomcat-docs.xsl @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + project.xml + + + + + + + + + + + + + + <xsl:value-of select="$project/title"/> (<xsl:value-of select="$version"/>) - <xsl:value-of select="properties/title"/> + + + + + + + + + + +
    + +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + +

    + +
    +
    +
    +
    + + +
    + +
    +
    + + +
    + + + +
    +

    +
      + +
    +
    +
    + + + + + + +
  • +
    + + + + + + + + + + + + + + + + + + +

    + + + + + + + +

    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + +

    + +

    + +
    + +
    +
    +
    + + + +
    +
    + + + + + + + + + + + + + + + + + + +
  • + +
    +
    +
  • +
    + + + +
    +
    +        
    +          wrap
    +        
    +        
    +      
    +
    +
    + + + + + + + + + + + + + + +
    + Attribute + + Description +
    + + + + + + + + +
    +
    + + + + + + + + + + + + + + +
    + Property + + Description +
    + + + +
    +
    + + + +
      + +
    +
    + + + /images/add.gif +
  • + Add: +
  • +
    + + + /images/update.gif +
  • + Update: +
  • +
    + + + /images/design.gif +
  • + Design: +
  • +
    + + + /images/docs.gif +
  • + Docs: +
  • +
    + + + /images/fix.gif +
  • + Fix: +
  • +
    + + + /images/code.gif +
  • + Code: +
  • +
    + + + + + + + + + + + # + + + + + + r + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/webapps/docs/tribes/developers.xml b/webapps/docs/tribes/developers.xml new file mode 100644 index 0000000..dc5a994 --- /dev/null +++ b/webapps/docs/tribes/developers.xml @@ -0,0 +1,37 @@ + + + +]> + + + &project; + + + Apache Tribes - Developers + + + + + +
    +

    TODO

    +
    + + +
    diff --git a/webapps/docs/tribes/faq.xml b/webapps/docs/tribes/faq.xml new file mode 100644 index 0000000..44f581f --- /dev/null +++ b/webapps/docs/tribes/faq.xml @@ -0,0 +1,37 @@ + + + +]> + + + &project; + + + Apache Tribes - Frequently Asked Questions + + + + + +
    +

    TODO

    +
    + + +
    diff --git a/webapps/docs/tribes/interceptors.xml b/webapps/docs/tribes/interceptors.xml new file mode 100644 index 0000000..e7949f0 --- /dev/null +++ b/webapps/docs/tribes/interceptors.xml @@ -0,0 +1,37 @@ + + + +]> + + + &project; + + + Apache Tribes - Interceptors + + + + + +
    +

    TODO

    +
    + + +
    diff --git a/webapps/docs/tribes/introduction.xml b/webapps/docs/tribes/introduction.xml new file mode 100644 index 0000000..499b3ff --- /dev/null +++ b/webapps/docs/tribes/introduction.xml @@ -0,0 +1,273 @@ + + + +]> + + + &project; + + + Filip Hanik + Apache Tribes - Introduction + + + + +
    + +
    + +
    + +

    Apache Tribes is a group or peer-to-peer communication framework that enables you to easily connect + your remote objects to communicate with each other. +

    +
      +
    • Import: org.apache.catalina.tribes.Channel
    • +
    • Import: org.apache.catalina.tribes.Member
    • +
    • Import: org.apache.catalina.tribes.MembershipListener
    • +
    • Import: org.apache.catalina.tribes.ChannelListener
    • +
    • Import: org.apache.catalina.tribes.group.GroupChannel
    • +
    • Create a class that implements: org.apache.catalina.tribes.ChannelListener
    • +
    • Create a class that implements: org.apache.catalina.tribes.MembershipListener
    • +
    • Simple class to demonstrate how to send a message: + +
    • +
    +

    + Simple yeah? There is a lot more to Tribes than we have shown, hopefully the docs will be able + to explain more to you. Remember, that we are always interested in suggestions, improvements, bug fixes + and anything that you think would help this project. +

    +
    + + +
    +

    + Tribes is a messaging framework with group communication abilities. Tribes allows you to send and receive + messages over a network, it also allows for dynamic discovery of other nodes in the network.
    + And that is the short story, it really is as simple as that. What makes Tribes useful and unique will be + described in the section below.
    +

    +

    + The Tribes module was started early 2006 and a small part of the code base comes from the clustering module + that has been existing since 2003 or 2004. + The current cluster implementation has several short comings and many workarounds were created due + to the complexity in group communication. Long story short, what should have been two modules a long time + ago, will be now. Tribes takes out the complexity of messaging from the replication module and becomes + a fully independent and highly flexible group communication module.
    +

    +

    + In Tomcat the old modules/cluster has now become modules/groupcom(Tribes) and + modules/ha (replication). This will allow development to proceed and let the developers + focus on the issues they are actually working on rather than getting boggled down in details of a module + they are not interested in. The understanding is that both communication and replication are complex enough, + and when trying to develop them in the same module, well you know, it becomes a cluster :)
    +

    +

    + Tribes allows for guaranteed messaging, and can be customized in many ways. Why is this important?
    + Well, you as a developer want to know that the messages you are sending are reaching their destination. + More than that, if a message doesn't reach its destination, the application on top of Tribes will be notified + that the message was never sent, and what node it failed. +

    + +
    + +
    +

    + I am a big fan of reusing code and would never dream of developing something if someone else has already + done it and it was available to me and the community I try to serve.
    + When I did my research to improve the clustering module I was constantly faced with a few obstacles:
    + 1. The framework wasn't flexible enough
    + 2. The framework was licensed in a way that neither I nor the community could use it
    + 3. Several features that I needed were missing
    + 4. Messaging was guaranteed, but no feedback was reported to me
    + 5. The semantics of my message delivery had to be configured before runtime
    + And the list continues... +

    +

    + So I came up with Tribes, to address these issues and other issues that came along. + When designing Tribes I wanted to make sure I didn't lose any of the flexibility and + delivery semantics that the existing frameworks already delivered. The goal was to create a framework + that could do everything that the others already did, but to provide more flexibility for the application + developer. In the next section will give you the high level overview of what features tribes offers or will offer. +

    +
    + +
    +

    + To give you an idea of the feature set I will list it out here. + Some of the features are not yet completed, if that is the case they are marked accordingly. +

    +

    + Pluggable modules
    + Tribes is built using interfaces. Any of the modules or components that are part of Tribes can be swapped out + to customize your own Tribes implementation. +

    +

    + Guaranteed Messaging
    + In the default implementation of Tribes uses TCP or UDP for messaging. TCP already has guaranteed message delivery + and flow control built in. I believe that the performance of Java TCP, will outperform an implementation of + Java/UDP/flow-control/message guarantee since the logic happens further down the stack. UDP messaging has been added in for + sending messages over UDP instead of TCP when desired. The same guarantee scenarios as described below are still available + over UDP, however, when a UDP message is lost, it's considered failed.
    + Tribes supports both non-blocking and blocking IO operations. The recommended setting is to use non blocking + as it promotes better parallelism when sending and receiving messages. The blocking implementation is available + for those platforms where NIO is still a trouble child. +

    +

    + Different Guarantee Levels
    + There are three different levels of delivery guarantee when a message is sent. +

    +
      +
    1. IO Based send guarantee. - fastest, least reliable
      + This means that Tribes considers the message transfer to be successful + if the message was sent to the socket send buffer and accepted.
      + On blocking IO, this would be socket.getOutputStream().write(msg)
      + On non blocking IO, this would be socketChannel.write(), and the buffer byte buffer gets emptied + followed by a socketChannel.read() to ensure the channel still open. + The read() has been added since write() will succeed if the connection has been "closed" + when using NIO. +
    2. +
    3. ACK based. - recommended, guaranteed delivery
      + When the message has been received on a remote node, an ACK is sent back to the sender, + indicating that the message was received successfully. +
    4. +
    5. SYNC_ACK based. - guaranteed delivery, guaranteed processed, slowest
      + When the message has been received on a remote node, the node will process + the message and if the message was processed successfully, an ACK is sent back to the sender + indicating that the message was received and processed successfully. + If the message was received, but processing it failed, an ACK_FAIL will be sent back + to the sender. This is a unique feature that adds an incredible amount value to the application + developer. Most frameworks here will tell you that the message was delivered, and the application + developer has to build in logic on whether the message was actually processed properly by the application + on the remote node. If configured, Tribes will throw an exception when it receives an ACK_FAIL + and associate that exception with the member that didn't process the message. +
    6. +
    +

    + You can of course write even more sophisticated guarantee levels, and some of them will be mentioned later on + in the documentation. One mentionable level would be a 2-Phase-Commit, where the remote applications don't receive + the message until all nodes have received the message. Sort of like a all-or-nothing protocol. +

    +

    + Per Message Delivery Attributes
    + Perhaps the feature that makes Tribes stand out from the crowd of group communication frameworks. + Tribes enables you to send to decide what delivery semantics a message transfer should have on a per + message basis. Meaning, that your messages are not delivered based on some static configuration + that remains fixed after the message framework has been started.
    + To give you an example of how powerful this feature is, I'll try to illustrate it with a simple example. + Imagine you need to send 10 different messages, you could send them the following way: +

    + +

    + As you can imagine by now, these are just examples. The number of different semantics you can apply on a + per-message-basis is almost limitless. Tribes allows you to set up to 28 different on a message + and then configure Tribes to what flag results in what action on the message.
    + Imagine a shared transactional cache, probably >90% are reads, and the dirty reads should be completely + unordered and delivered as fast as possible. But transactional writes on the other hand, have to + be ordered so that no cache gets corrupted. With tribes you would send the write messages totally ordered, + while the read messages you simple fire to achieve highest throughput.
    + There are probably better examples on how this powerful feature can be used, so use your imagination and + your experience to think of how this could benefit you in your application. +

    +

    + Interceptor based message processing
    + Tribes uses a customizable interceptor stack to process messages that are sent and received.
    + So what, all frameworks have this!
    + Yes, but in Tribes interceptors can react to a message based on the per-message-attributes + that are sent runtime. Meaning, that if you add a encryption interceptor that encrypts message + you can decide if this interceptor will encrypt all messages, or only certain messages that are decided + by the applications running on top of Tribes.
    + This is how Tribes is able to send some messages totally ordered and others fire and forget style + like the example above.
    + The number of interceptors that are available will keep growing, and we would appreciate any contributions + that you might have. +

    +

    + Threadless Interceptor stack + The interceptor don't require any separate threads to perform their message manipulation.
    + Messages that are sent will piggy back on the thread that is sending them all the way through transmission. + The exception is the MessageDispatchInterceptor that will queue up the message + and send it on a separate thread for asynchronous message delivery. + Messages received are controlled by a thread pool in the receiver component.
    + The channel object can send a heartbeat() through the interceptor stack to allow + for timeouts, cleanup and other events.
    + The MessageDispatchInterceptor is the only interceptor that is configured by default. +

    +

    + Parallel Delivery
    + Tribes support parallel delivery of messages. Meaning that node_A could send three messages to node_B in + parallel. This feature becomes useful when sending messages with different delivery semantics. + Otherwise if Message_1 was sent totally ordered, Message_2 would have to wait for that message to complete.
    + Through NIO, Tribes is also able to send a message to several receivers at the same time on the same thread. +

    +

    + Silent Member Messaging
    + With Tribes you are able to send messages to members that are not in your group. + So by default, you can already send messages over a wide area network, even though the dynamic discover + module today is limited to local area networks by using multicast for dynamic node discovery. + Of course, the membership component will be expanded to support WAN memberships in the future. + But this is very useful, when you want to hide members from the rest of the group and only communicate with them +

    +
    + +
    +

    + Tribes ships as a module with Tomcat, and is released as part of the Apache Tomcat release. +

    + + +
    + + + +
    diff --git a/webapps/docs/tribes/leader-election-initiate-election.dia b/webapps/docs/tribes/leader-election-initiate-election.dia new file mode 100644 index 0000000000000000000000000000000000000000..f64c8430204930a68679389ac9dbeaca83aa309b GIT binary patch literal 2732 zcmV;d3RCqTiwFP!000001MOXHbK5o&{+?gKQNF4$-e0OZnQO0|>rC(3x#Z^N`o%*@ zjLa)VYDn6Szua#>0Hh;JB4wH+7&n1toLDl5@Kj2@7{jgCc$l#7jc$e&JcjJ zAWB!+I!-s2vwyw+=^HtF`{vbm>p1*Y|7`Mb8|W+2!o0hjU6+<^Qqoc=F^?vWD_uIIL zmr2xg_i`#O4S{cC7X(XtYUKE{_G|T8A**B+*`o zk+DD$@Hy=#DN3U4)hf%=Q>$P^eXJCV9j7`y;06a3HGO&wk#!x#o9nV&7zXfA81E;n zxX$jb8mz^qY4ABxcHw$`NP$P{K+yAHmC?tOr*sSlM{U}kgf#>Y+8x=A+i1Jg({>#f z;cgd&d9-e^Au=F{e zZlanty1dvkMSto*L#cxTE-R>Y$sJk>u~HygAOw(pLji=$C5a5y%(IARk=I3(d3l5wcyuW`7|tOIjQCwQj2D zM+i`=mP8p}V5)rvtUVM36k^D!doqTSUDyRd02xJGQ7)8L5^{&^qLtEuBti^wDk;I} zPtGoc5yl8nkST%%5$ff$&0n^`&%w3+o}!`*UhVQY%j5F?t>+u(j*9|NXlqeuLlqKF zIn#JAWH8+*Kpz|nmt3O&vA%?0Y=K#An<(bge;KZucVN5&^AHBpy7&N?dmh2qMmmVL z+q~m43X8ncK)ep~wI-3@vgH1kIE@;ZgpR-@)&NjI^1j|fH!hKYDrNl?i6~m={~3g8 zjtP*gFZ%BElex0m^wF`YrM;h?RFJN(!aUFJj&Ef@m@ppL^;Ht3n{&L@8=Lnco?y<- z5>{E7>M@|lYjJ}44}S@+^(e1#vdpX}9_;^mIg=;fR>QTZzE&Mxi%-`=k8bFh^YFc) zrqemd2G2nW&>TCSd|CnobWzLFPU6d_a8KLL-M{0J_a!hk{QQjFf)so(QV1Z->~*{0 zp%#LfK?}`@7~q!=7XSJ2oyQC3c+oKaJY4Y3wigf~& zS?<>MJr{D05TKCsj6r94dHu9~->6($GqbipF+1-C0BCRh8BL8vHAu@Wj}M3GIY0E= z@NK{DIrkLalv$qLlnsHPbDMU(zcjJGgbsZkM4sa?>9KuqH5}VxItyxYD5!l3Iw`kN za0wWKVr~KzY+;0I7~y@#O*cquR;{%i>jYFriS8{qpv>c$$Fphij10mv=mO84Ue8k_ zY#-5>(m(Xr4ac+)BSUBqBLL4a)4w|2FEU80F6(#_kV79xI9$g=8jcTM$gktM_UnJp z_p6)#kY)M$@5RRZ`0lra%rOPZFXK(Ml^u@29tdk=kpkwHKFlhbmO?FoC?m4Jk=U6+ zhN;$tL)0)H^sMpnSmT{GXFqXTqWw|NBNOllVSsxcnQ%J@Lc{li zSiit^I7LY#hEpYNpcq3uClF95klT$QN6IxvKkU5_#(WsyvxNcFpfCV&u@U64CHGsH zhe;A8naP=NnumCJej>EnvR&>9p+p6h@G9vm8Y+!A^HUAzn)&g5X6}90t?gi+de6LAB_2rYSuLe6}v& z^=jrf40v3cWCIi%FJnZE9w=xpIf2;l5CIa*WOWcJ+!ir1OE5+b5jI$W6(<&FL|+{3 z8ZH>DV7HI~6o_C*C}B)-N*qQE2!Mof4v|1YD2e;VTYRyJ{$Bt4cpGV8_rAV+U%$39 zu&HO{-TjHOjL`5jBZKwUqN7gp1s>#7J8KaFtTvP(^9e^DLfS{7&pC1mAYz2V%#hfn z9=G*1_j0fS5L{*WNPzzg0P>GtJ$^XHkLH2s@xv*849@H!088unF{hb5l^9bw5wZof z<$1Nt-jL{lkYwDBxg8i)VoYj> zgoMsb(KLd%9JpG%3~A$`{V?R`Qo&CBv6jIOFL^ zA;x1=!q*ir zlJVKOe1fXvz^ZS+8Q~GIzLF`RKw=d9~Aid37_#8jX`x3 z5p-QZ;mn%J@Z&U029y|BlDXqioJ3TDAW;jO0A==*n3do`Sp-j8bt9-?~RwuvsrnCNy?n|*MUIG*T&3L}YdYUjCN*c`Ynsl*e^ z^F}q>PgERo5WJt}FLs&_NvX%5X(dsPFv~f%V;T|1ONmwv1q^X)RuZWq!pLRVFC}`P m;LKR`Yl+=Fg6e$|-beYHSJfN+V`_E2dG&vx$SH^in*ac>WkY`e literal 0 HcmV?d00001 diff --git a/webapps/docs/tribes/leader-election-initiate-election.jpg b/webapps/docs/tribes/leader-election-initiate-election.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3e8c11cd6462ed73f0853dfe508e8e69c029afc8 GIT binary patch literal 33664 zcmeFZbzEG_mNvX`hv4oI2o~I(U?Btv!7aGEG!SScXpjI21cF0|V8N|%hu{Pfym1NA z5J=&-H7>%NM*3V?zF04U%O za5D!e0oa(BSeO{tSXfv%IM}%OWCZwlc=*&Lq(o%&G>i=NG<0-KY&k zDJJJvcHpoI=^m0>d5z;zunEK25x+$HO|rixSonWRvVRfm|Hw59;G>~{%0nXt& zoCTq{V3W>rNYCLzzFp;(fIV7~k(|-Ip!^1(^hLuZ62z|`RG>GIw*F9-gnXQPB8%L- z0k9aaA{j_-fRq*l(J@^s`Gpf><+_4T_Yx*K45^#=G< zcuj79rPV5N19bG=08h-rl!1U7;F4?fJol;h4G@uZ-TB911!DXUj-;)t-(7f=9yz1* z#`ue`h6&N9tLzi*LaB-j(q=CRHOD7ih=y zQYcv>ckuyD^A}obUp2mR_7MXi22`=WD;2WSy8wH$q4fy62-qM*AxWxN@EqV6C zxc7D#Go8p{VAxO{Gc0v!%Ysk{`-007VA zFAk|9P1?xXy%;iN`l&i;sJ3>Bk2z9|w3b2zS1>>v@U3XvT_K_eWl}=iy^3^e`{AsO zHTOeD86X4OzqX@)eFd=Dy=JOCJZY(^7tmmC%rfb&tl~2*5##y(QT)+Vd=^TIvzQ)Y zBe`(txl|?B;0LnQO9T;hjqtD`&YW3tFmf%&Y*?TcV zNnN$J3d=)McX*c`wzO37&nY`~3elcbcJsf1b-T&4*;r;>-~(2UIjqyZ^g+s1_SsJ| zU3E57$et6{{WRBQUg*~@vUxwi?h$e->D)#;hYzmZ~nirR?^2dxuxjlq4xW8hZ9h?Xv;;%Y~l0S)5I^;}@eo1$icJ4yDhD+A2N>hNtoQ1`LL zGdOWuplqi#1%ve+#&#AIDxyQO@FH-1|5e!;+eM*krhI)^5pVqP1799` zuf{#KpS@N`LzwKRL}@mb{+1Cl;+U}9TRdO#n$*nABsZj84TTDP zI?nR=#^lOtzSQUEUoh-av!J-){C?leI$rrGfCPIDf9~{ZgNo@z5JQI1Q;cGnPxWy- zr`VC%WqH_J^Cqpx*ZYss(33|86(q3Y4JV#5+7?;x&inhP_Lq3&Js3#(5lf;jyOXr~ zAW8GHq%}bKRIH@61zsj(kn*L>`1Bjckx_)}JPKW6VOC-^x((V35w67NZ+i+H>H9svrX@7DKKUTSh2XkFoSLt7q;gfX-SLR0G1^d zS7bGigKPE8Sm<|h-y5JHGYJ(sAbmJ<6{-<9VPd>FJ$WrG-b<8B}1>Fe(AGF`1q^h#XW5kHS=@N!O`=>{M+wcPLJ+Ng!U z;q~1J`rvkJ=$+~I@H#(ZbFSTyAkk;(%81Ewh4=fH?C%!eOs8l>rtGM(FP5Uj!qKcb z=pmaDSd(al=j3)xCx5Zd^UmdvQ-sLzJSub zyG#wWQ!aUF)CIan%EFcL@6oT;tV2E8Pk=AG6_~S2U4rI2lru!Uks~=1jCY7$$qsj; z#}jl7QHk$+=S(aj?~nZ4b3H9-uxZShUWU13sMg!sa5t*$6iQ&pPN8IYBDhs+_0{+G zSDIr6Ick#g)tQe@_Xh+qq$OXFzs@?8oE}M9F){NyJ;LJ-DkS*u*tz&Iv4q2fGTyP9 z98c(vRVVw#o$d{a7;ctWG~#11 z^kOfGAKSbzRagHNsTLC2fLZ4U0TUGQT|?vbN>kiN$^9eJQ_2$NyvP5mC~f=IGZ+l`m^; zIs$3qtT8ZigET{dW6`T`$x&3{gp$j27fDgO?zZ^G`X!L6LL(I#Z^{9Jmd?Sqo{W5j zA$a!etu7}NMTf$L=21sW`mjib#cg-K$A!n6Df$uApKTTQq8T}LMa!ke(5G{WYw)gA zW~XNjlKE^BIXCgcG@7t%gJ_)`Vn5=g=6mp%L1YE8-M4XRBXSO$Zo$-3S0Go6TVe#i$tX=Zy*-;kA^ z9d1|oRDXvYqc|%)R=O$*)A!V~T{GoQW|Uq0TQig935w@O0j~MAygpkV$&_5cW7FW{ zT=Nz$HT01GtleWNm_rY%uk9@^Xb##=@BhI4bA*MJr=v`Ul2GAnAgOn5R+}AeZt6tI z>lSx>cE3oOErY|^^8)5`u#~^K7s7Hd!9H~$oEE-x!6ow$L%lnRMWA<@R{l|zb!x{z z$4WEa8$6TdH&U@HOT1adhP0lvbcM{)3?T+jMk~s+qdRALC_Xr)VOwf2zl&MOnEAe1 za7%r3>i9sON_{m!(WtD9bqojA(0m~rVguS)$cnl@7UWf;bXAe-ss#0xcg3Z5&p2kk=V%{9f+=4fQa z8dse^^*s!hf8Pct9=idm6QaLD$2Fr*-9|z0xw>?bv5?zW(E>a%gIurl>AK$DRX#cq zVsVMJ4`4m|M&BjG=F*-Jj~nUcIY2j^7`f8i;;m2NT-Z|TXixs7YySS@RYBp8x0oN~ zSnYVe@a_W^+;O%>X}RY>l~ghOo&HH-u6^Ma~kB;)g}ew(aGf+LUkQAdr4WC7ZKFMKK?mGV-*K$EsfRw znd5*X$IdT2{l-b($Wj#)ajbBZkX9zd(&bAzFr)sezk36y)B?e6%4FIoV8hBqYFyv{*==zV1>Kf1 z$TN7e0CDfjv`@TLDu*~}5sH*ooVpYjO{QCL@n+w$v`O0!@+o0Nfa&3m3K&N>0Y7lL z!8C2<;0BmV0CNW)XwMRugwx&tx3|#7I4i&e?Rnb`peuH*Ier6BLa&S5FW=-5$rr%D z{E+VkxKdj>6H~o3y8+IV+m75qWl-aef~C51$xh^mca7-;kRon1NvwN>yFSFQDZXYk z330TnFywUKAHMM`k!-#JPFSI93;)k!L+i9SGH7MF0p^8n0QK1x)CMRz#JxMyKIT%r z9FnGar@R(g1s9%vQE;X0yqxw^=JX^COryo38aUFQd)*6znDV5_2(wJ<* zvWRq4Qa?O3n)Y#~*Ijr@_Q`y%&y%K~F9O3CAEkp;T(Lym!u33KQTfSukgcYfyvIFqlx0chmlB*zXZ0>uXZ@k=}?q8!@!>8TPz z>~!hURI3_l=TyDjc7#z66&sp6KJE4Q*`(TWK>PZewf(U-#y|G+yWtVvje-7nbORiP zxnB!I{~+2g3;eax{vaS|x_^`nG<)PPb!lIM>Uv^QaYge5%v=5_nsap9Ro1?UjLOFK zaj&`Um#GTj(W^AfO)CvL91lVDZFvU7-<{AeNA!>FUpb{eIjKL~{EuJ!{ckL}e~P!L zCf)!bo|S&xVPJ87=lvlEXt)H>uUs>rws--bUtqvLYrpjKr==u^G`azVDy|azmoC^r z75?(Wf1xoz;djrtmx!VJ)HguUo_U!0pD+Bo-H@HQiqhMwv`*~PD2JaeY6Q>-qG9-Q zsr7?48r1-iB~fyACvRy>xOB!RW>Q;C(#L(DX9=KM0^&V^?EiyU=D&-ouG|2Gmy7`` z2dIus>mZyYZA<@U=1mLJml%aQ1BFn%oH(hOKLd@0%juUX^mx~caz6^d=%md%lh)5?%@#4lo0f+KT_GY>WHuQejM!}QBn2Qh6w z@o#_$KhRhG`NCiB>R@@`k*rPV2iN{}fyjYDp*1m1 z6l#%O@;>BhHv_I2k={5=qs*|2E;+z}VnsOx^3&RxM_$N{S|7`?7tMzZWjUAdQWex1 zNj7Oa&Ep@Le(8A43#Q*N)h}~}K6}}Akujxwl-Je>{WctIU?KYyZ%UdHUNp6M19TfZ z?NSmBGMJ6C%oSR?2IvI%NyTllq4#5`G28&?hYKb>p6)!}3S3wqY%SQ7{6c~5ma0S^ z4rf6e@Ska*JHvqvm?NLx03M@P98SN5`G30d*E%l0tBb5yZjpq$k{Tsx!;(>=<}_6r z9bc!AljU*4l0$LmRXhltaxZT)2+Xwk!z(aGt!bYYUvz7o+Q#;=QBev?CWlfn!h*@b zC&ptL4gEMf<>?hf6Tsy?=%v!duRE9fMIDd=q!bUnyC}hqlKrC+B*bnvHfG z5D+L={BS(6ATpChaBuxgAbL#|Q4>zZ6)yg`{dC#-S7AUZR}=Z6U~Y&ncZHs|+fBRj zxz48x?mIf2ee3-ACgM{4XlOZ_>YyKI)tR`qAcHWPP6}M~HPk$At&3tfQs;rp#S3tU z%LB)mzX}m=#-yQeEi%cT!ripgkr5qBE2~%iW_>PhXvB$Ln4qI=y1#kSh6XX|4heFa zB}nL=+&kuz&Px@YxBgiYO$Ug}h;N&M9*|ojf${p@5`|L}8PwNhy=Sk;A;2d+_lvd=$=ikdr$5xF2dP`>7UH!W8<3mDk< ze*8C6iX91Ch1Sn?T^IM6*ODBJE*|rdlF*TS=9l97k%#s@m&~{A3)BUTgb3->Ta@iO;cqe3+^!adUAt0O0PKK>ps~Q2h@s7w#vDk zX=BZ+((xtJSq-0%ktAugTklyRI=L``_V0u^+SERqB z&GwgTo+B!faYIS%RdYt)I&4d2YF-#F8d2-gkyGMVyguw2`r6;DDz1~Hr1EOnvF6H{ zc3XssPW9>Cc^Zl-RbE^p8qWQ7MO*h4D zNDIIRehdpKUOb6eFq|Eti%7FP60Gb(#iCV{yEAG9iQZbK|60StbSgD_?BI1N1c#B89B5 zxY4AJRV-5LMwD|B{#3g&_?tsvWRB!yOv0A>mRkipIm1`GD z3L(*hP!h;c?=>UD8i5tIHpR451&01apY8GQryX>D{pQMNv&Q+HHjZByG zAT&9n5lwfriX^7%x~2buv-7~MTt$mVeeHbu?LUq1|8hC-pQQ=EJ|oxCo$y((TW~$m zl$|)o@YtoF;^vC#phgL!?qHSODA$pvqXTm*7+*cf!d9Q|;niCCgLFyd-OkTNYsENR zq~hX$wL>q^V-TT;%*{9H!OUIBrZv+H!KaTQ34QyeM^EIw>8ifBXABDIY?dq+n#sLCH7>N)CYb+$OoxpPkL7uecd5_!lE@E-)uL4}Zy7`_1%D@2uM z{5)tXZ?s+lbo*{zT3Em>qI0Qr{P84bU zLeGr&iVa`Z;k5I}kD!R3iu8>l{5$tNd5hyk?%p!Lx2En|WF-ChBO%^e=v$$%7Z3gM zZRp0Z0+a*3A+WkzoZy5Ba2YYxYR(Cs^YsD0SL(DViL6Z=9c(4`^n%zMT1bOymMQ3( z9D>#~cL-#gT)m8cJBSSDJq?f88 zcD-6+w5`*{n^YqL8)X`_F04%r!O1ilO=WN>ls`uUOge6B~bkjwg3qngH%j z7(h_8B`SKX&^**|6!E#aI(M$Ld=!L;iR3(%;kH|AG|v_pbis-TQnAj3Za% zWNKY8dsiG!&(ayR)!(=YTzu(D1n3z47Dpo5BCmnNq8SFRoK>ZorXU%={Z14g{J|H@ ziXX`bs2LY~_q9a6|C=%G@-+qY^X?5`ZT9nezznl-<-O0{TMp;0J=Ihq7%yAKTzDJx z6DU3UR2(0B=io5FK-j>5KZhr0Yi`ca@q=i-r)Lm}GJ9|Tw3Op#O;&MTm4}~!cjP<|=}` zDv*_OAt?uU1hL#mkXc1~1DyK)P9d2DQC&QcTEziUtF}O%$vY6#r39%}AO!Yp=I_)h zJgw`ZyP!W?24Sn}!fT4$D>ZWokXi*IVD`T=NOnCJFn#d{B3^4tI!GuOS%2UjUAsG$-BVjTv?4|9v3n>n<;v-+vJmrilfz3qAD z)$~1loK466Cxe^$EOkFL>9%ziU^8-#u$BffN+Fl8)TbXqOHykU3ql10eQ($PHSptK z5R1PEmi%AuZ-B;{3rtJny))d^H@2aPdR&;iX-)QvJ-{4_Au)8$fSbWbXNp z_!_`f+K!HXhi9|_<&(7pykx7Hs19mx0(CfJbURAjW3(Sl->`K1mgBc6J}O?3BTING z468>zf4|`D_cf{O=yj@(y-hvVe$~EKhT`S&p~}{q{EEUYpdgO(zqzfQw8ggK9iH@k zJ7K(K%yY3oBkuojyef;|lgjEDX}qW;qmn$mE%~N(Uk>+lIm1#{8_@}8s?~?)89JlV zhuxvQvJYY!(7#LUg>|qkQX!*P$neS_os+7eO{Hl~7}aPjm;?=4%!G*nbT*cRglc(& ztGZum7HAgSL^KsAdNDnovri3?q;-g6#89_$F!VN@UGpK$%Gqniqu#FI^=HNM);#CP zc%{(s^aWY5hak=fJU${br4N4hnd<>-TU&Lm@l?9hfrxW-V*rH7UDr>P5_k%;^rtSL zz-F$kk6T>igom@?(0RUptc(T#;sBul$oj-=M#!1f#|6uE&Wb*JgO*&nC_T*iB)QfK z$fsHpAN?$8IhA`@c0&JX1EHEIGOQ)n92BHtLhG1+K$onV{cQ;K8+tev)$g=Mvc3q{ z8z87?&ccy$=(_y8^V&A^uG!m+XjU#SBJ?RQh(X^uZRb;oRq+#AwI$S{?pp`!_p{O) z6gDLAy)eO-2Y)ch-{_B;Ftb9$3L~(jfg7^BXUw)8u&E{&`vg8Ah<3|x?P=bT(?%@9 zx`SrD?F~^3FRz&njatRLNfia7|7N=tzrw5`k-19H{n4o(yo@GCZM8Kev>>TZ)?#*V z!YrfLvw_YY?Ok3T!EYtHU-7j6Fg)1X^zd|Pk-g>Zu1w`?pA@yCg|>q5Mj#TkFi|cp zhcO_dcpPS6m(=(;`m5U-hWu;;gKnftj+Yu%)(ZfuK*!vlqbJ74Ac~O+;%qcVpKFuO zd#j;FpUX(`bMDhu^yCl;tbCDahehtg8z9DyfYrFNCAok!fPbV|St-#-s4+0q3lpbf zBUJQ1gXI0|?-b@hTwT{28q&lqWAO&q;>mU72iS4|w5&1omj*<^2AE^p=(a}U z@!0!P5iE&RowSiSw9?fdpJeM!^x#V6Wonl(>7;#aF^x|oBkeyamqrrx1b*o>g*;!u zd`seDN0CW`CldQLTu*%(K+G!Llps@P$~Ei_NiT^PZENXZ$LIqiai$ zwX}U+9)4phudx$0)-z!O`|ia3%F@%*3WAUQhNwy0NiQOqtLv#)*}U%MB7A(8&QXw7 z6Yf7N+<6gaiWf1L8EP@d#nncK8LfVk+Foy%xBoT2DFvo z9$0`M&A*o%fq;&oEdO-zm@rYjf=Cg;X$i#Gx%M_^ih45jS4?v<0Xc|IX*Nf10y8YA z#%Xp4HV&`f@i%i#@T9 z5;E(!neZ%bg2><8AZR_(2>AaCql)x${CF%TY1eS>dUq~&7Ala9YK6?_Xu~!t8_q|32VHDmeL19~*Xq5$x zLwBi-oXVV<%(C~6wNU5fQdm*Z?QAfH#2EwVp7=Aqoc7CJoocXuqg|8wur7{3F#tWT z%9hvOa0t^te8;Q#;X@A(6idR1xOE%Emr?`mZyH5AvF|mx?xls8+G*uTu&KZYBUw18 zN722*QXlWbG(RkCkL}RhslO|vP9|wKTtXYm9y_R3teoxybYXnb^YHTGM5`*dFhY7S z??D5mo=RCANf+B^Qe9@yk9Zla&JJg#cm+tGl!Ji!af2-FZYDiU|C$OJQTna@J#pdG zlenwLdQZ@wgvYc~qZ7gcwWSphca$F#39`{%ItAJf4_ROCzdA(0Z5Vx! z0{N9WL{}m!;2+#stG=`@z<0L=t?Q@vGQU;<53l+-$zHVEM%_UOvSizp5jzPLD3Qs?tsJeh|h8d4W(e0w&%Nc(uTgmCP^971fY!(Z6D5?rJC zWXfmD?FYSrDSy-WJJIi$oso)1aoEy(+FBJHO!Vu&6;G3%Dvt+DNLyH!bhb6c&6rfr zt@nM)bZ8A=0|0eE)G)_Wrvs!clR2g#?#5m-?B0>e!#x+!SocHQzZ*QjBdL;wuLD@L23g4i9k;cRCNzH8N612@8EKjNpYERiHlr?f^wx=nKi#*w zMFD2@UtJ0P81`44zFhjAt=|i>Jo++DHJ+v0dB^W@PN)cwdNAgXL_fMxKx?a}P3*}X8S!XWM5+2+SDazE2!=tg) zIWo(vmu2|6lxQ9pP$szXxw`pFeQ=%L#myU;w~Qf`_II(4_Z8O05?~DT>Hp!}!lM}nnOx-rX~2BBvhKj7|a?|d2+j_q!nMe(XiP5&`* z5=@P@QAVDsd^!$jhvN4)73T-rS2+1;Lb-;8wac1suP3mgyT`_y=>(k=n?fXe12!5> zN*dhOS}vUCnH9$e)=JqpZMpOc@T0I&w2CFUIMMK~iC@%@Eh&{a-C8K+MxW?qW^`1x zvn4E+Da3e#p_u#n`0-@>xpq*Js76M94tM_p62b3!P02Q|RLB@%jnr%jeRK4@WGEwZ z5XlL8{W-nN^Yi`l#m6r+rEVMY=-zv;m`5w0snm|e!Is%rj0~L7NTCQfG8uG!hbsRp z)$?xV4IsMEj=2oT_nCF0io6Fan|;$y4d5WS=uXpGYi-NYZFgjseJ?&j`lG$K50fQb za@epJ$dITL-cwyaY>h65MEmbgtV|4~e0>n8%2wt2?Tfn0Ei|;>_!jeCPxTOJpUXfP zM?)<5(YOKa8-AC2>MT+D6Y9+% zx{3>NGxA;{L#ZsiR5|nugE`59&p47tcwP88J~68j_{saWz1R|T43g7wmploodS_4X zE(d!*8a*Ic+0*ZxbqG?YtFrg=D&j+7O6}z?8k44nkK(bEKQEj$7FW#iD&98IaAPiV zX~tg-*HYNx_6n8${`INe3?GS6Z5eHs=o{*3ZYgNpLp_+)!T|2Y)`t?|)YcFKL-?M!a4gHWG<9E8k8%VS2YPT#( zYZ}?r%xsPN#PP@rC`ngWkqstOW&fGN+H_O~;!*o-+nZB&Qp3J=Yya7=bM_Lw(qqj{ zek0yJjrtI0@APmZEh4+(Enb42khO#CKs;7cQBOk33n&Rjc4d1KXtMY}?eG7W4q@mB& zOdi7vi4ZcBEi!}jZF3pKXk5sgYav9ichFg*k*65e_67Y@hcJY%G$wy8BGe%MlSI2n zK8OUrOZ068IZCuw;fwAk(~<=5pWDeEq8ud2`mYIyUsKBoZmgw4;sTMdzp3m;@>Lv0>{TIM6qD3OHbPw=$2%sQ)F~9 zl(NT_B8o<*>Z28|u0JAaL;N}Su;Nnr{SD9$T@h`ROTT`1!6p9V(*D5xD=k#HH_&_c z*8=ql^q{)cc<>rbX#CuM|EV?YHq($JGQa#>oIp;eU-IV@*euuq7 z8~2vU{Oka(lm;0?`~A$*=OwfWP=!j3eh@d?kFQJjR%*~#m)7kQQf;tg&@P9cbZpC0 zA43)gbT^zK#yc3*)s1zKEIW9)Z3^cY+StDGTJFl0!0;S_6QVY`Xs79^e!8wn?PpB9 zRADccs*?DZid2*j1>*TTc8!$aS5;=aDaye-9y=UZ6Pnb|namgkc%O!@7(0^wh~=o~(mZeq-*v6E)4;_^%WooV_XfPw!7yZw)T#~NAE zHGR!}*jkvsAlkpb!pVq!=0>PzU&j(PIrl5dM)c8P3|EgFOQE^&?WPN%@BC zB7CyqF^{s;@fZ$t{N%Wy6!%yLZiww<&%0;BRTmKpDtzwQqA8_TtX61^3W`d19t~{C zOM5TRgzk$F#Xf3%huMEA#dq;+k&y3kOoCw9w^wgina&{6BEeebZZ`mdNHzm^Nd+F{ z!1Vc}T%OIx$$aqFdw`p6F89;qBq4moa(*YJ)e2IG6PyW=6lo(GMqM;5tT{umgMwb& ztAZSVoTx#fu2e%jhBnSw+Cti$c{gV7@`_fkrd2*>pZTSb3YD#Yg&qf16mS^Kt^1c@ z;J*qN|M9+ZcMHU3w6yr1d%*iXzI>QUrf-Jy{QzjE2;g&fWD#upfLWeTn9+$Mx_^4k>c!V@vGs(soxlTbzC z+9#;0XR!6f*<-VvHAR7NS>qL`i=hv)9FH2BN1QK{uqY|03HCHRZ$*3ln7#=c=$G> zcI-z#PoaCOHp!7?ikQTZs2H71mOWbB*S`nsAYv9ROE>K%J4auWssEmS!I$^W^{@x^ z=!gbq7X>x?Cut#T>ftAE+qw^Ml${A+b+SU0rrc-UMvEvlp<-#AdgQuB^%6WGg%g~4 zgpBIQO1P`yQ4sCf=9v5P`kqKbPf29w3$KTZ{`mO&;v7^P&2gjCJ~DT^CB45oo!&B^ zt$R3-Dy5F@_UMZVIy?${6tkiFa22c-f@lL{_e`J-F{|suT@5oMG3L6^KA$JANj{5- zjd&(L1D95XRP+jFExel>fz~zQc*tFK12}Kz%^TmM3LPm)kD=?O^wXe_=EJ5hPAGQ_ zVV+F60o2;UPrrN;>pYuP!*VA4_?4B*LnVL=CI3$x&3_CNu&`DM9$B;?5|iL^_)g>7 z^(1pc!SBszA7|u;g{bu#FdGV`6cN3)WWe-h{3A!yNiG`I{g$&VGoq(Ec^I?M!&SKW zYlw-s(M$4q4NkxHRJ`Nc-@OF3UjDZI`wyJh!s~xzv5Nd&r}!r{-QUX;f26YhnWp=D zq2iy@bbl|M{I?8g<5Cl3CSi5WzKj0R-^#2W6i{mZ+$UC;N+FT?y2?!uTVC&lX9tHk zW=jeTqyQ;%5=3NNWGKkp)C`u6bG; zCZ%c1Exrw`FBoqh5994&IhVTVEgyQw@0z}B6`lHS>`?v+q^#ec5W%S^dn|8c_iXO< z8hxi=f4B8TH{Gjcv~h=WO^z=$!6u4{yN0g!WpoX~^&r*?wAkj_i0|5d*Wcp>++h-{ zIaPmiTiiqzh@@y~p7`16N=FAc+8NKqTU*{5@&vE%LOJp>MET{KmpZFUTi1_9Qk#QMN#mZW*kelB5rxm^pwgIs79hHCt*s;GGHI zMCMyVJ$a)eYIfU5cZeI+DW(GS5x1c0Mf;*ayX=G*CU@fUl*c7k^^^#0uvRZk&(GM3 z`~ky#Px1m$0}Q5zl+Uyk*iBLtM(KMJB6BkA>9Zq?DAFo7ZTMAKl?T}1s%UJfAk91X zUF2oZ%KI`qgj}55r+g}~gf5Nm_HZ(MGL4O&H@wh`zL4%nGKm1m*kw+9Oc-6H;K-Ii zAZo0REk1bQW#T?aIKRp&KJ4?i$QuJM+x=n9G*8W&=S3P9>wfjmA~I0%>&4qwXUYjB zY1+(vE0FfxvuP#oozp7pQs+z+&x1=p?WZY6uwlGZBbV+xig+c>KR}-b#ncF1v7d6Q zDe`$^di>Ur9>-bh6ZT+oj@}@^y#&0F9|ru>e@>s#s>{`aYO6oL|*^sXFS1hiGJyLyHH8;2GypNc^v8f zakJQjaCj9aEfvSQ1hD3rWn+gmRkk{9IWgxyYhRxN7hkm!}K$mhW$DcpWClET=@r)G!^kt01`18ZUS5X>?4uVne!k3%2N zuYEcvNT6M@kE#t;7H)Lon5gw@m-H(^GkH+-f;`0iWwo0wnE)GcpQ#^tdKuDP8A0Ec zT)OR--cLM^KaT0pg)iE>^K99xhym*@-~^)gL|s?TooXiSt;{LnV%1O^XY^6W@a{+z ziyWrrEJR8--|AF~R%|uI7G8^3jPx&RmP@~S>r}kbYkH>((;{1g%tb}-6BS1zz!3YF z8HT?(@8%Hjc!4f(PmBTMrCVX{s#uqENr3CVNW(_B-W@hC!-s=AD%ebY;n=I>936Mh z*d!^hV9@P*uNJig=S+sAVsTX>zpBzo-NJ-CMz1^tQfEVD9jcLEK0uVZDfKA=vvRl( zcVu0Q+X%>eC-AI4vwAYWL9fbscBU9%c3q2rsv#J|`SK3mt|cSm&Q>dHsu~`Vt*BdA za<%WF^KIbM;`*(H%S(7aWt|COh7_!Jhln*@4@Q}?N!n(0I)w&cU+)_h5xYFd5##p~%ArKOdzY}>oVmd3`pzh1fjz5kzp`1Vtok9~n{L^U zH)1jtHf0LhoX~xu6$;E{`T$Wgs|PN$xY=1x8|R0Y(&X+(^>t9_2R`#J3l8?=GJRfp z-wJmkIJ@llnR2_pr)5i96@d|XPA7BjDK}Y~?emjYuX^4t7Pxd4P-Pd8s?(&I^*O?? zO@DGXpkx^~BKEV3jYdU=Tk}p&cRj*)Z6FOP7}i{z__9wb&m(p9mZx8aP}FGQ8#!RE zZ11_u&Ub$w+OVRgP@8(uz~u6HI|a-1Snuv7O0koGnaQYs@Tt-D#DH6Q7*jwO4!x}D zzSPnI3FdK_JmraNpa~Hn!F?$liWNtza6IRfTW2YQmBm*@%eG-M?Rz5J-{ z78bIn7}-2~tnCnL6V1=`Xy00Xvjc(`VF=^E@(kzF?P^1FZefzJ?9#_C!*Q#U`V#bdA|LQ2($o8?G+GY6_AI zDC9($sT9{+gB!XllcOXS1#&#e7-T2!QeO`^LOmI?$!-nCgxRO4r99RfB3cga69?{$ zh<|?9AsQBs*fEYb*q^AW?;k&_ibcgDICAY}iO1*c?TGhIkwbIzJ5`w-aW2uW_cWJB z2&UCT;A(oe*I8+Aq5dMAe?l2mUR}_~yP|h1%ct-+n0+P|rQDr{=<%sLTrP2izm@Mx zp4x0NkM7&x)#y$!pL;)1b5MKr4gw`NP@UFgbqqGAqB_)z&xL{^X?aKT8D3Z~|Bb_^Rr#*Fp-|E|F zaUQ85`c_ZVpSCK6RmA2vR z@uc&dXy@7(cS)Q*kltO!Y1MLRx+4qg*hkpi@mPazk`#v{=@`_OXTlxYsC^EwkUR>u z?pGx%W-3PRCpaTp%OJRo8$^spZ!jDa7TAX^kF~n6nti zY}(x%=H}6j+%(V4_7#t}j)j*17U?dO#9^=w;14ISp zWP3jeC_hW9*AW)t_{OKQW~qe#tiPY*twHKaQfG#5YRSOI-kFUrrL&K0y^j?Q1r$Xs z^`Tc^#FrThEfOuS42m>s1CNP$ z0u~9lldi>nkDd9Jz++hQ9R2QcU_RTOVU<&8OGMpfhezzAjFt+@~@zfEe1UI(9uEKbhQ~g_UgI~o%(mU-v7^q!b%aB;29ozb zo{F6YW@q`QQF@ro4rHe)FeTHXM?}$q8(84_zcV{FuE8TC1|$$1R}p-2KaBDSQwr)E zG+xIi$D&5tT&=l8bMelPFRuFIwsu8uyJTj+3FiTUh56zu3;~mDEEx;!#B}pLmNIL4rpWL ze5`fK0qDQE5k(m3q{^0WWUrmpsBq& z&GxY3Y;`TIg00xjjp*|0*`rhRJi=R;--w@+kfYtD8zmIje~=nRH=q;m?$k zP|r}&b#*FXlr8eEoh1rs-gk=%)=#9*PcARt7RORGc{Nx)RG90XOwR6NTBZ8}MH-0Z zFkOMRc&Qxx;VgGoxcLr2ZKwr|)ar7yc7S;R->*E-`>Vq&Lpppa7UVR7noNR!&`tOJu1N2biTAeWR25@IQ*I^ID z0LudZ7(anWr#Q%0|BUt30-XlRQRrCd5p0M%lt;;VIQ zo?bt1=pkxYo3+AZ>7BxxrcoLy3Wj&*wW_#f^J^FBI!rGE?v4)e@`6Tidmsc% zhWU<08jBSC*&ulTUwh{r*HpIc@nC2wgx-dp%0%UYoE*zGwq=z-dOTImW=Hkf%?T{5~H&@{9d$iSR_yXbsU%HSo2|@ zv0hRGw9X-Yv4{DDh1he`xhETA%|9CTPZmYiPC9G02jC0`^1T~90y!d%!IAbnaX9ZEwp z@aP%p2bM%v_)*``FXX!Rz)wYzg+|OYoF13N$tpE{;=Y?&jK`2@sDW8zC6VPN-J2TO zym|p+By2AdNbWP8u28A1Em=)QzVGBrk2#s}7B`3&#%@jyrgEM0APP`D)spb+LzP&y z;Ro+K=@CS8!_s=!Ir;rWl>E;4;ok`|N`=UyCy0Fcr#@))>HPw_Wuq;a{5klUaubHd zykVORkaa!Bk;I*i7&QBHCfMPg4YzI<@!oU`XihpgfQCrH9_E3adkP|$P%AA1){@ghK;=r>o1=5`3~U7~OfpiqpA7~b@icy-xnWwW@nn_zh8hGP5 z<}fj17sJvwV$}&JeGD-gDiMTR7=)kVhR~}siDuDMOVr}j-mpY>cWQ)zL2DACfi{wc zBPNKE2&p{-B%GXeXo{v}y<@0TvYMM9E%N4oSyu*31qp+cSC6#P|H5Wq-YM?+03OQ?CF8A<^!#I4nZBLUhE|y%agiT(S(4x(<7&D(Ym|@= z35n+mr8VOxRj)ru3R;-fk|Ne-5fZFu0rx~pq8awE*M$W@v}LBapyV*xvJ_0%bgPmh zk!R8p38$X{c$DQzgMth?1=gFguJ)GwC9T48^Tf!>8{(SC)?ir)9EDTOR11!a1$}u?=vcZ`}(6t{>O$5kiDuZvpPJ|>Ey)c z*g#GLAUAo_{E_N1AOav=oL6cg`0co1_MW(EXf3|KLNAqF+bV>vSm82tjX5b?A*_5&-R-`_1&g>S`_{m zoI%$H5t@J!-H4^c;rjQSi0XapUq7+ed5*pOYp3);SZhLaEkk*gk3Lp0UD+|>^0d#p zfdT&pTndCs3-rp#P=PcN!!E|&V9PNEe1=D11#QcOEKyBl3gn0Rzq*2e`uLU!YYlyP zt6a*9%+UeSA+47kvv~57dlNh(8(=>*+AcRrzhF=Rp)6r41iYu)Dn~k_UjVp^@-Hy2 z@24xV*Jy8JHcHoDQQnr}jkQ^nkpD|`xU6qMA#$yV=)+{TnX4%fL^#GzE#xEjI zV?70g6vbr1ORk`ud>jc~Hq4YP?s0~8t13=uGkLnSxKC}`sYr1juhLYPQ=ge~c_@Q8 ziBkb{sps-LtMN05G^bPkcrQwU$GBShisA~ax?`f2{+XZJ^0Hdkz4aTn_+YX%ii7ZtO-XWxHM zCq)O&lzU`_9*)YUPPew6hGH(B(r|W|qckKd;dV_{QWOvK#kJJDM{v=UxUaI?le4`GW=VQ!>0SIJL>yli{0+NnFSt$H*m zWYYt_`cp4_i)v#;ni&|8>Q9-hcKCWRgVe*q0y6KotiX;fo(y&?1Ur{2n#D{mN^_Tt1f{r!dnG?{2O zNAWD$r=uBnDBxI5Z(S_MSy7*aJyZ1vzTT`21`%{UBVId$j_R6lD#<0lZ2QqL^OM?3 z@EVz>e&WoE7|D3#!Jt8jxo&M2dd2&gQX%-=XI(0|-L(E@dxv_SDxdh-(OyCm9e$B9 zunk0RyK|eSYE`6?YO&-JLDC}iI9o}{+Z%-mS3QFU5))>vVTdNB-2;7Gi98*`R#rzx zUh?>7?q%U_UU_5Ian;hf{CQJJoO1d>dgL$|xU0cjSK8&ZZaU!XH@$Jd6TjSZae#dw zvMD}H-Gk3u`QCZgt{mL(^`MPyMlc%W$zheZda8J6hd{A378-Xmpo3>FYF5(q!8HQf zyN)P-7e)^8UYmSP z;AFx*)1}$K#PjybAo{b;c=dF%D^j}anBqyLbIKt;h!Z!6Q-vWExG)m2el0+?Z?I;A zzcv_Zi6mZ}7^0UFGPohcX`Ha|P`qDeGMm3-lf9(3L{<a z`}0gVjcyP}vKrxxFkW543a7stS9T*eswr4#-&3A_lcP^7O*I-M$zh4pz*cs+EBZaS z=gZ#t$fEVlW%E<-;D6fy&%Xq;?S})r0#1(Qe_h(yGH{#z4v8_t6ES|_SK5_0Q~@-p$Xp5UgIKwdiA!qiwkXfpZqIU>2)%XYc4{Qw&4} zcT!VtqllKBENvF)hyvDm>@9Ezuntk^t*Nfa_~hXNia|rr;imk=?Oo)0)(nT; z4JU+c#dj>J!tb-5{%qM8$w%Df)Zd|LI_nl&n%P|i!=7)=QkvRoW5mB6y<(>WGe>hQ z;Al&N@{dg9yTC1gQ;WVNH+Jv5P@JOM|0$n*w9Cvsq+M0rKuoE7W^SxoQr=^JBysTd z_jnqA`5n&8f%K|;a4B=x0VH@v%}@3De$51-rt-qW=vUY6?)i?yA;+|x6{tRbXLDdA z9}Wjd@&IAHBIO_^7a++WX}^wUu0~(Sa`*7OgMeYHaQ%ryX*~XNcOf7>gQ6S4`)DxC zHC&XKV?$EnX8U>SqE{w2(mS%0ISZ(YR1|m>rEo9dH&aC&aJ^wG(yY{)Rp%H#@6>K~ zwv!z*78IJXHA#UWocv5*X9mYlw_6yw-)IlLKk2DQwwMPd>G0$0Ju+W-)wMu@NdCpu zq@dZmu-w7>x2K*+kX6#E(ONo-90%B955BP0zQ-#+)I=Oo+rEoXfB4Oh^j(DdH)z}U z5bA&&*$CdWLI{il>q*-&an7+jG@rE|OYX0OgZAou>x(QCiH9)yfUx=dXEEE8_Eg2h zH&28u>u!rnm6a^yysDFSoM?=UAXVHDkuvx!2Z%zBqS+6*Sx2bsU(t+5Xz>r>jYk^! z747&3T0Ha7%ln3lJ3@i&aL36Pn-jEb+QO4M(V%5V6-Z>Y-X-(ZYi@BTlanDYO?VdC_8 zqBRqASCi9it?7K$z?!hq0FhCiF)|{x1_({!zGn`cY{#uTW~y3~SZ>|tj@*u>=ylz@ z=Nn}1$2cO|LZru>JuGKG&1VAvor!N?&EH)ue#B?*o>Vm0)58SLw&1HNkP+#|U?*Rr zeqnYS8ZZ-}B0PAtacyzUdLu9AP0UvCo>1kjVOYpXf7a+Z{%Q}FWMSk*TR?*U&q6u@ zrq*xm|5@Z_R$q???&d9F(i60!xS@=X$xsd;3rUN?c+loq zXph;TTk~4@hYg#u2b?dbmH6%FBRMJqJ59%V3Ad$@>Kz3}k<}{1E*m?wD>_kdXcLW) z{%$;M)_KJr79d`ITBNogM=7tdW29^rpM#tS{4OgG-1zyRJj*k>y|(}MWAaf|xg?JF zToF(*I<+RV&meWa`rx)!N@Hfg*w)X@kEm^^eignay{GN)XbXP&%$&oF$CG*cR#|Bv z>FzVMXJL(amLBLvz7-|Hm0bRu&8PI{)4hEn8_p8g>Swd01djrsr+f|bwe2!-mdL#@k`*yiXWx@FhnbFA`+ebFYei-k^Bi z<{7xdV!?Tv{B#5u1ib;;!F;I=cBn=`)UwmfFmFgrf(4z^P}Nhfy0V+kODqZkFQ zxeFQmM67KTHNNIX12F;5NbCjaxtgTFiEPqWHCjoc^2|zrg^6D=j z`;UO*;TO>S6ny#GFQAbs0227;B+^4S@IUvOU6NzmaqejT)(cdacn%W6|H{>!baLIvP~} zHrpTVJ|H9LNY8=bU5x+_7?^|0-QH8p9x$w3jGSJHDl&cVkz!jKjaH$CpPoU7Bh=7- z-a^=qZ+~jH@ORVv0n!V=E@XXOhXGUp8XSjg$Q@4L?u94;mlwUya|moWD)0VzipbwR z4j4g4Nu0p)5Ma8l<|?@72>>kb1G&k6x4D1!xc~2_pXu`atvz`R^U_Lb!`2G4XHOzd zrbkfP1M{(5#wr8YT>Wui)l5v`?I%B3)m8a52S8rl_e~qSa#xcdEE>$=zccvkhwiE0 z>&AcnZ>q=B#u~rYU$QU!T7Bu*wIbFV0Hvet6D3ZzvRCr=an*++lyCOaSL^q`!sdAm zZ99YLcryzm<3DSs>dvhzsJHeTO(BqhD&9t;=s#=H(uO=7d9CB-`p& zCR`QFjUAWNlO+lOS3&)ShB_^sZAt!>zqst z2g7&R>rKh9;7p^T58Mq!`x&O<>UmmdHAm3NN}aox+d-Rr z1wgGo-HDYw4Y1#frZxu<>|R+t1Kk%vCg?hY<)0bXfBl<0_z}wZz-!$417Nn^k@@$ziz=!M`f{n&`i53+&$(6OT|;s~vO=6-!-_Ii6{(QM zu(_BP&$|BGbA9&Mkvr$Dw>l|QT0Xpj3b*LtH6mM`z#x~ztjN2pANT}}Mojs{W9w|H zo}exj5cjdF>=xJg@>PH~A}lTaAZ20AuYI*Grpes-AN3ik=){V9jz*$c0vSXSH(oCU zUR&~YFoUC%U#Qx;mc zS!_AJT0)235Dl#^YjMq;zPsx)^5U-{MPYM%Sa`-Fjl6kDX;sRlQPm+fPvf#6B2!01 zYdJ!0%h@R`5S=U$wSpEuvv~ZB4Y4(hBPuYfo>p25vbH+nkHg$!*EIeB<0a!l_C*P7W<{WxP>4X z3_7~ZcmlL|D^L2uTF*#+p50+49>=43MJrqK=dUcp3tqKobyJewY&Ds5d0QsKo;7OA z1nDh>mR+x`DhjjKh&cAjFA*{t4;uPP@B#N=fmNB)XIgL{Ex(!c0x(1W@Q=S9qdx-y z`Ztc_#(^3Uv6K?D+lxiE#`kvd%|s?w1whakI!A~U8j_iA->bm5-+C{X>t$t4)cX3k zS6<&ICbpEyC+6##Tyyf?@(+VYWkF(8<^Cn%K3Yoe_S5w8G1rGD)mb{t=A3syq;nb0 z+Id3w(DW~40THdUbnJIj7B}35-sC9c$;y-%9Z2Q4uZlek_wHMy>GAO3tci3By{>$V zV}qTA&+IsHe-YA~33N!Sm=uc!^=z?PqcH9d1Y$L-U+l6Yj?aonvWc?kjJgC|TN{i) zc%GOlt-fSY9<32D#T`R~tgL{AcjBT;*a~r+N20+Q>S7az4N3{BZOAN*EGy2+t%y)2 zf8-y*Ls*a7r0FQUu{<*o|6Gn4kZ_xqmxvhCng`5}J$8;3<91wzP}=Pd=-nO(05Lil z>rmo!l?T36w_AuY9H5_{aBh`cCu@|%c{3M!3_`iU;D1SxQh$eCqbNlwUZV15uU%~y z{sQd-CCpV8@I17gw&f&~ED)Rm$1|NQx`*aW$6#`sv>h8gv1K>)=R~SV-*>3Yw9?Uj z927Fe%JzN|#9VdZpSp8FaiOa1`L!kxo>0Y=1_cKrcOXX=v>+fuXCd%HJd=11lRH{W zcL8yhI2Fx!A&}QG-Td4lVH;a%iJbs3g>qqEZnA1XH*SD9BB1#i=r{{bvMev=H@D)y zM*IEkG=FmC9|ju_Uphb?hYs|oc)}b#>@A51 z@N{pgafH48!v@+ddOH7p3K3R!>W=EUsx|5_Y1~V?+?|fj(SCu_dZ$~wT1bFe6wD+C zY@V?T7l61BdW8s$7m3znQ_q&vPrB3wv$}ShXEwhV)fOK!nI{P6t1Ng9HW~Lc-#Nydp&-wq4Z-FX! zhp4M3!2k;+heHxult^xTV)T=;xrRCEk zIN2JxgHjIB?pqn(RW8fsyEG)JB4^vV4FXwOP0+4{fdrZKc_^GH$n?b(y&&y#~s?mAS7Uru^j!QqQ0 zlA@z)TOB)%C4%_H zWcoGyT>M8|#iyFr(@R#nh?sARv~;lux1EoRhR1^V?vW0SG>GZytO~qvdG_&)zTw%A z&Wr9y0MRExXAx;prW)fYd39aAnz2kevUCwJ2&g78)E~76^93f9=(TF%?b0iZ*MyOJ z!{$rw6g}NF$*5E7cuA~oaFWvbl>HFdr7IKKF2u7Gjg&b>B`GzK#rnK;Nn!*+UsVSW zxJv+gIZ$(3C?zqdqUraqn5lVmB|MYE;xO%AJcZq$s`6S zKb*7`n57p@QCg#(sXsO{Ds4kEPleFvI|hms7cgh3n*gPbM8(ng62(>})^a8fw=|wQ z`(ZkWyf;>jnas=&hGUp7*@lS;TX~;K!WGgGUt+>kR7_86_^cPRgn6dV3f8Vxfs4!p zABbGu=F(egI95&Ug9a*^8mgtKHQoHA_lJ5E!<23pm^A zN}yW;SK(vBqPnW;`}`AK{un(_0h^3K_qiF?h7%fz3)y0#cTWslRZ8v#e3H?ZT&oMJ z{OHIUM_PG{4FuNZ2lC@^B#D^wIY_Ri`AM3d@-+BplB`-@VCc^j0>`U0*0p;RcR{k9 zR!W6s6Bmqdy7q~g&^SFK*UmbfrX=Oko4o6UFlCX@@iWrL^n9C6w{wttwdbn$$hQf<52kWzP9#qKVjZ>Y0%!^H3wHTDO`Aak@_w%Fd%77#9YfG}i+# zL1RfDO3CZq)i%9N&v{^<;d;VSO5rk!-K<;g3O*>`5ZKTFj?&+k^!}oCfB3yU|L)tfKTgX05Aff#&gU6?VzI=l*XLKO)%71g-cKdwu@RNB`&i)%rHCHtw_M-q-oGxTuT#@9npy@W;7R zZhc+U+qa!xSIZKXtnRKKEor$A{{M?L>s3oww3vST%^Uem^Oe>=_M_ZSs+tw~ zqM^E~Z;I3DEHCpJY_WZ81#t1PT&=1D-~VGiTXtcf`SHVPJND#G>vFRH;BKsFxyx-i zSzUejEz;v^?tT~P?w`tKc`++?efx5;>h1P-{oH>0?Re?^`Qw4Rp}AYDdYITk73z5P zFuf@!#qz*5+g#Ifr>pg{SBIYZ_PVYQruos9vi2lq`CU=3Z~v#yYC2oL;I+j0;we-Y z|5IG9*6jT-t8Oo^@_Lp1KKn6W+`z_tx46Dpoqxt3fIiB}>+_%K=7ZX14Q>lE@X?dH zt{%1%7le-(k=xuqmK|;Pzqr`GeGRPSi|MR*Ai)&S5W;aa7L=HgBJ4DR#jKcrxUA~M zV|>sXQ=W#GBaa!omm!8o>Aw6U0%%h@n8Z zdGw~XQ6#0Ok@#57W*tauV&LOQEU&8D4?BSNVH9!nvs$k1X2mvvVsSHHznDjIY^O#$ zhU}{NZFPP?XKbx$drTf=Z>-zG<8P~~p8U9+W^d}c{8TJk(D@#f*0u?%#cCH}{CV~< zpO>>c_}6;3(SIvupNds^nSc3ckN*abj_=N6Ww(x%3?sTJ)prU(O+em` zBnV#>Mr%{kcRa|a5CR-MUCfK!FZA&LK)Z<5rm|1!K;i{_3|Ap<8&-R}GEfjt< z8Bp%H8N-P*KWrdH7mV@eGM`r-o@~Pl77Q;KQO4*I=b$j7f(#(U8$!a#vEYPhV@BK& z(9Jq3*yw@9E|7y!q9mop7-!%}TWumj4!}3gX=%9;S_?_7d(QgvI-g87_0J>mE0j(|+=0vVCbFY4@{^7-}H|1W9N0ko-= zc*+$D6q^+4A%)sS3H=HZe3I7_&_RDIr+>?*e*qx8Lk;m@HH275xDj*27%pP!Ax#AC z6Qsns7<1EHqjqqoD!_Vm^}%y<5|8amL3M$W*&HBq-rA1rE{x zn5)O!5{1;1dj8nvzrX)AVV3BaI|OOq^Oh5iLG;(0WhN3NJ?T;xN0E0s)H4dCJf%0C5CbvQ{CLtVL)dL zS5A+lBUGJAH9^)FCO&p=#DrPnJLgTX5V;rqhFEK4TrmQO#1jITItIdcsWj0io7fLRknazo?%H_xya_l`~uG7Re7CY@(@`!2o4Ouh?I3REL0Xf6ty+=R*pC+bljpg z$*^7w3@Rry7Pa|n!HCr=a#7oE9^01+N|@t^D%G6-{zohYR$0Q&2|o{wpF?8jGDh88 z$IcPqwJ@A$5q9o6$u>@uMKVUKG5p1)flEV_a5ie&h+y_o7JJHKe*~(Ve04<68648Z zwD>Ne=pi~Sd5nO$>s%%rJxumGQ$5sYOEnVq^wP(!CE7^FcooAjaELSwOLf{wezb4H zl%F+#5guFt0|1UWM2r%SHIJFRj+Eq@(insZ*Qjz_K&f~q3%r%V155WV)F28Qh1qeT zB`!s;lJz~f^}S_5%VW%MPGg(jEx{|(gX!(;NP;mO+w^t<$uRBvLR+9I@FKF^9mw0W z3tpv!uHk|41l|lRU*1fo#d1|lK9$AoyGg>zL)h9_^u}KvSboNqc5pS6@F@d;_k^HP zL2^S!5Ce3^o}NdU_Ef5=Q2b@dkCu&?jO_u8ZP!N|SMO*w@DiPk9Py^!XD5ZTKH!a7 z2T)`5c=v7&_bhcpD0yflGShWo5b#gdcCxm6vbK*4cAMC#qV2(M9$X?NxK~RdZmv%7 zh-11al3{8L$DE?OP9i>v8|)5eF9o|(u>0|ZgojkovlQ$e3P7)2@*q7lO~LMg2O+SY zT)X^qH*42%JWX>@EK3lC3g4k5DstY**q(L!(>@t79Tx?3W=L2$VdYb?@)q1R$6#gE z8r6#0Q;7#xRSAG8#<%(4lE$2>a8C$O9uUI3P}R^??o_%dZRe#Sp_a{u2MM`BJ)_rJtNZ47wJZDvWBOB?UMiI@GB$Uyp;nWuX z+81(SdR-^ruOJqajh$@lQ*3M<(y)@-r)gNpZBGC>=-3^Bkbw?JuzQHC{M7&$IfT6g zk`qY&=FQu;-~aOd1H$BlkcUFZim(mwkRWpGQRLk9(Y{TNa2$s-vHQ0Bo?%c~FGwG$H>8UPK2kCeeWHypR333DQ5Q49(V zgCk{tk`xwamuZgETLkuyjSG$1nB2RRb8QdISrluAx-LatrAZ(ufuyG$Ds$W%a-n0i zwe2k9WDk=WDV#k;oRu!3gu~KdPew>LW_Z-G0~eQZgg#%8{?%+(5dTW_hFC~$)FG167Bo{oDN<(b`1;WtxJGa zr3{^y)tS4luCsZuT;|gvnftNLeXsQ;CE~@!KAv}ij?g{f;2;~c<<>?HTrxRQ$ z<%sltkjNHFLq>v~0$PUAwev+ulLAvofVA(!6qgzxE)`sy^dS6gKw5X#xRhg>`2nqf zPsc#nUt=#=&2?1QZ5xyAOVRLRTpd^{Jp)H91zuSYugQsx0Q|E1C_FU}){RDU-_0Er97uvIkYMkl}Cke0a&uAg~$eTHZg)iU?_R7eJFP@1`vJ*1g46=;fd%rkGA#JVQY z+%Vtm*2T{>4`r`YsyWDNx6vLl<(|K+7V0ZJhVV4*9f%1k50ZcmB7&s^^udw>Wq8?= z^vx3uF*>F;xu?SP*vGv`%o4`F1{?c*_9lD(^S^)pQ}*Mp|C*#o^sW`V_G2=*qX)i& zt60zyl-l5{)5^uH=ayJbvZh3x8!JI1Q+C7!aYmb%@U#~uy8u*131^NfZUj|=YQZ7` zR86?nmU3kcPSuvyq{#P^M85Oeye_ipW0q%USMbh?I-3{si=s~9@92t0TEdm)*k2TO zjYtT#X9W98HM?u`^@a}tVuWKcBPNZDsLe!x5>pK&kd`=WS1H4=GrnZ>M8enbXgUl6 zYU6R|uw6yrTQT2MvdcYy2o%R>PlS+8g!{g@nviO_zKL31rb zd)RgvWw4XS_LVqX^5E11{PLX_28}UG`9{g=J~ap11x=%MPg4`Eg3C(KEowP7(^fR9 zT)QWvhyy}n>4@AqQrgQPkQxyK02aY{@;VNe^`c}SfEwSOChXL-$IftCYV8Rztv#NW zrkSEcHH(?_(!%2Ih0^#T6^gk80iO&4Zlk#4AfS1gW(6v^jzT(wMA+7rQx-M*c}+-S zw=XyTOjn(}?}t?MNux*C=q)=jImu#HVkzB&HrJ@Z4B;xS1X&Z(M5{)E*oLmAUxNfm zp7vqPf}NW&36YrDZ7MhsUdl%Jhrr-&LPVnT$rC~tqBxOoiL>ZV79t&eP(5(it@Wtb zJaZQO^I>q)7*${?f8#EJHr!=X{ zNXY9Hfw|Ko#r$RDxJEFCU33cGKWUK{rA2-%7Lx}ZlC7$20XNR(7j;%HvRS!ElTU}z zD~iJQPkLq05Qgg`gwfayG_r2Njw0H6N|YHN!_X-6706^4JA|=|#oa4$ssel^s#6T( z31b*0B-vMs*fZ>Z$?JSJD`wTS&gVPMr4BA|zSlGr5XQ;I~FFf=bYd9r->YE}~ z#RYwRu-FCkX=0hfxT#W3Dxr1ET$X1&vt)TyEM@cGFYT zFu|#i0yLGDB5FZ56>#PNh%;lH;#z22e1ZnziHGq#VnEGBR{< z!II*{T8^Pct#ni)=z$OgLSwXv;1V09km`ipi(4xP3QCx@3rm83r1# zxq?m}j{zfQx|gCKe0P|xtK9%qKhpchR=bO<|&GlGWA~{u_vOSq{r{)%| zOzv6A31eLEU22H)3>>jEC@PuSTO=XZQ*EO>DlLFUmT_&;P4^UJk(96v|78q6n2<_& zCkJH~9buevhLklukuW~h53qj@i?*MCAJ!?utN z52;|b!y=jB0wN4-N(^kqKkR_n1$s2qI`xM(*GO5Js7R)j$@ZlJte->cJU3m}sf1yY zczXuoEeOLrj@!oqG2!z=4En=$;KA!8*fcQ|+oO2F1}2(;Ym1UW%Ts$xbw@F6Y_`Xx zgEC7Jj(N5RodzW_v#04;tbofmCXU+FSFHEzRJ#S zJ$vN@>+9DOWSZYx<)4bPNx96guZz4cCZh~K>3;(snN*8^h7Z@p@AJtdyR7r&RWW(^ zdmKX4ZS5p3J(-g|jGeWon3Jld iG5qo7WOF^s?~3}{v*rT+!vffE&;B1YmyW0ucmV+U%Uh)Y literal 0 HcmV?d00001 diff --git a/webapps/docs/tribes/leader-election-message-arrives.jpg b/webapps/docs/tribes/leader-election-message-arrives.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4733d2d1b3ca93cb702fb0d7aa7537582397655e GIT binary patch literal 111284 zcmeFZ1yo$!nk~9oCs0FV$r zz&#w01u)UkG0@R4F)%Q&urRT4N$_xSaBwM!hzUq&sp#owsc2{zn0eS39&$0!(69?X z;(Ed>ARs``CMqGqC(gq!!1wziNLW}{xHz~Jcz6_ij5LgV|I4rYR)7!_wH-AZ1?eGx zOo)U+h;-ix&?EYZhV;h^`0EP^83h#$9Rm{!8wYVh)dK(-2?Yfi6$K3q6%}!|FXA|W zN{B|p$omYPSltxkp%V#TK-_0cCYiD}QjL)VW_~m0KrC!BatcZ+7FIU)M;rozLc$`V zPi3FW$tx&=lwQ5o)Y8_`)ibxSw6cC_*(<|tGa7bv_hj3{8$ArYBPsu4? zvU763=6%aAD6gojs;;T6t8eezyA{0-^4|Thzl7N6$KUJcU(xw?udp$h>FI@i%#@R9mCX#_#s~a zCW%bk=dv~|CVq_rQZwffY%*qnMV75`ifx)CxKPjnAUf#mmD#0=Vne45^HDKpOHFUF zFn5VG7d;AsY|m4!)8YYcB-)wxK>M$nmU|%1yZw%7B0Fidsxh#$$A8*%I5_YmXW3Mb!N2zADO(=DhQRMCs6Q|< zK%Jdk(k4~`&wJo(y>nOfT<9JCXr(Usx-eD7R~~cIF%up6g&u~LLx1l$g&QXN2UW&% zw#}kJFQXk2mwFV(CT-bd9%jg{aS{$KNot8ZqIsc_Ag#Wm9a%d#@9xO0t}EL5?$<7I zjtR@1PS3WglQU@qkO)ide{ChK-vh^vE$1A5yR%+s0ExQXKY7!A$27U)!@YPt2-dJc z%Pm0n<96pKk|_w-(g_&C5RVnj!c*cLz6Tb~wx5RXRZ4}vV@lcZ`B9(3^4|0*5wr6% zySSs}xOiBM*iuRHaSwQL5PiZgHs=WB>Tdg#9LdFY^I%@lB(X3LAtZATL?VZI=v#H$6)cZ_KDHA+l?>QS2Y?-v@_6= z1gC6L$YQ>DwoE_Oj0#PU;onKzB)hSugrPgdPmWIn|MA$ybQ!9iPk0f83g}F(2wkPG zB=PZXHwrMlc@_J_vsWT4`|g1%sV#l|q$IO#gi$FX^;%9QWuLccMheXfrG%0Lu(C-q_=RKkJF#19$f1cdM$m4W-h*r-Rhsd!X*sJz&56lO6aH{?FY? zr7qz20CT9}Jy7F!%O2f?+;|TxJKR8N-S1}afs-#!5l`x$){(DrTrQo^u7`cp{{0)y z6m;+DPJn;3+Wi0H{_j0o|KV|8ZFE$Jyv_D(xu)OUzRj%>^U35s+wVE>%rM%U+U^GD zfAJR)OIMa2D~6~BmkJC@sn#~@RN>r;njS3}^T=zw#5GBL{UX+>WmD(qqQig;Paq{G zPTcLCeL6if=vq%z@-+&Y%ke`Q$4^qt!!NW8)TQYl4olqXDmf{MO)SL;t7qASwK}GB zYJt{@UkpB@eM0s`P4~xlRu%j_EE#=8@PT_FV@2lz{337(lcYkNpE|CURx$3Olr$Lr zEsKD8Ec4aDT1<(#R&{0l-8_*|V?_^Pa>fvOoSCO)EbTf2;rTO4THCvdrCk;7!A^3$ zsE-cTmJ;DrDa4|2EuL%8_m9*oA7C#_E;aF$m->i)wG}>`o9z-WXJWz}9REVp@0`;~ zZOX8)lD{cU)6@C7b|Sbm1Y);u|9<0KTc&SWA&SI|DVY}IYP5KgZ9x2$KqjFrY<53% zY}dh}N2NYVy+&u`LtUVbxdn4jrM)$cE5?R`pB&)n8+;ySzf;Ni?G(wSaL&WZ`y_7H z4CBXOHfA?@uqeY-e8#|sk{jbfBb!98uJqoDQ9RJ;AVp{vus9d1G<@KOtqG->oE}9|*kI%z!B`(+E7`xowqZb}eY1m+}5SBHynRwF|4IN2d zv^ViRVAl%o)>aYN)-r0~({H=d)PF&gMfX7dd5_eE$UU&bj=*cW_rR z%!Ov=spJLXZeh^h1_NGbq=-Kc3Z=Gu4~OsYrLM817Pb7wua`4#X#CDdkT(9lC!9$K z#zFhy3ikjO0bJZ^nfY#TV(;yRPr6RjNKcK8UH*Wxvphx>;4SyReChv}f~5Z!xaf38 zec@@nIQ*cm#1FEal6wrJmbglv%CxiWSfI^{Rwxk_cQFhGNKv&XlE~f3zRx~m8LXa{ zB#9-DaO~ehN$6d%{+7S}qx8hrjevZ+J~0mb1oCpn=2$tZ;UQSxHwi_`LwRIMVziT)8M-^>##PV-UbbIgLF-_zyFVwsbhM-`1v=8X-MBGTiP#_VjI zTAVH6w(0XcAe88;s9gPRVWJ>sSXE=J4XmkrUZQA_*|dc54ffuE=+VJzi!zuz2m_$Y zmGhi!tgqP6iW`O$Dp-@{mmld+?F>6%Ko#F=v%WPJ$l?%fFGCGVsblt9nL`;{vL1&X zp0bmalab@tI*_CFuF$hlSL|n;AkC;7x##GD&yvoD;%$ezAun-_lsUW0aw54S(3i7N zMK3?m<(TDfYJjV~xI52dii`{dD|u(8Q@uMaKK6cI)fE>8!7w%g^(?T=J>zPlwl=zfQ8?9MJ=_xLup1YfbNs%#Vh99evzH!$! z;%QE#aOIl02h1;Bo~`k*M;|1szC|O+b{3%yTXuatNp4(GIdL*T;kt=E>^gFlZcs?=sNKN4~>M!y7flDC1h~#)xpU2bbFTZY-U6_SOs#bTjc~gTEG?&31 zbx0SePaLIJOppO=&f+ZOaQ1($)`&=otZ#-|&IITEb`myz&sR87Yp(Y|S;_T1V39>2 z{g3g`F}Im9`P4R5LRX|-LRWLbTdSHdd&_41V6_K+9?!dmo+8C40Vl9x-@Iwt#A5I_ zUHB}#4DCzp2MgKgWK==}^knlTYE@zd2j7A%b`+XD$x5V=-YfUe6HgXSyu@^C zVlm8RSlq+NG>+(WFYM@}4*AJnyZgmlf~3_+;2v?CC*dkt3qk3{RRO1MbX9B3%)a(_ zst;uGGmE%{pW5fzLL%LXvilo+5)D?-Px0kjF&IHy(!~*Zzs5RizU*;Zo>-0NE{zT0 z`i($tvT~oaOWe6L1kaA2X5nwYtgCDsgOu3O(hcb(EWGI^`ApA)@s(E#36~F(3D%g% zHgR$f;9Y1<&`RmlWOVnugvy7A1yXp1vqfdACUH_hWdmO>kXTqHlO-2Vb#g5y>6DjW zzsznLX>4wr(uiLxZ7Aj0m%XV{xLb()^hVw@1dicI%KiPi*Q{1(;Jnv@~1B8ds()eC00)m+oH~}haXLUj@hZOpky&y zRepcRh2H%{u>ifJlkb8_Eqv8AqA+3~+dhRehPSVQqqQLax-=*-3)vA1^;nv&O3o9q zBFUh4mSPnlec!1|0Mrpn;l zz_)Z@>ewMh@y76GV+`h-ypmBsf~c^^7b3V*?ewGA3D<@z>}i3hN#5bqe1gjy>9yf} zr~|}-dvA%8sS`TD-D)%bnVZF!92|RxR>QK zguH9-M(Uh?omB%bwedw5iUIFOjL(?;F*Ix`_aVD^)qyseEcLNH*qO^FMJogs*kw1I z{WnS}bEP~@VA7EC*jbyasG8|}U{p^i0s9jL@e$ruiS!GCR2$|rZQW9vsQkf2ie0}- ztzVBRR}%Cz6GaAdS{>Xje?n!Tigq<>Z%|6VG-l{$K7c>7CYY|;7wSLun(@n3vZ^89 zD=I2Hs%Fp>_5INn@>5kXm)VF1Y-rp6t2EJ&18ccFed-?gT3KArhb7_`&0E73HMk`L zoY*3eikTs^`&Vyx-;%X_G-sjyr$_~R3iMi*Pc$fP4D`jWe?ZsY@b&-rrk#1ibq^#@ z-gP(Z-$MULng3<#`-s4K|A0m#HCnrZ4Jp5^O*RK7i+_{#qu#adxjl;X!!9!di|eIN zh8Iz-O=>~6<_&87`0ZM(qTxHoTylP^8825TY4(_j_>h2;H*6tWbDR;L<|$Ze;?DAI z3FDE0m*b{W}TkSO4C)ydA_XkTzd4_wdQ-(B=?V$ciG4a6m?GPpN$zunwJtaotuq-UJYdv zt&lSFVz@`>&Y`Vd4jBlSb!gGFu6|(Oob?{yE{pHEYSp$uate~By`F>j_~OO)e_4Ao zr?|Y=A1q7XT7Ty%;TaO=;%Qwp1*YoyTCuKcqrICyDDX-E%$AG;38PPM+~IWguYL8&!30!J)05f_jkr;N69TXubF_kemqZ#=Mp-Q zAMM{AQI`kZaQaVJPOg7ojh@u0s`F)?t%Q5fY3r2Y2!E<5KWKfoLCl6~zn>z>yh$!x z<2>wOM36PTx|q^)f2+HH#=Q zvYEuOxr<*nXh~qaR`|5g>2AIfMj}&ylP^MY4tZwvb^`cjKHK^dy$a}+c=JIHI0B7fPQ7G!JuGkM(t|W%Y z;n(BRM9aiQs~T_pkdWtGKIP?PPrd;>LjK-#F4DAJUAs+1)g!|k*)szx?F5I-6F!J( ziuZ=Y>o_z*qwers%A{6(QIgix&YlJ5<7xBiYw-a>`lI34UOv%2hPYUK=bk0c;>wFJ z@tuS(x8Kw^My*BFpl>}`qn+nz&3NbLlj^qSFCo4~R`@MS;w$n-*E(jel#8RmdY%TF zZYlpS0j&9#zqG)~WYkiK|y>zoc9V9aa6I|LCat$#D?<~xcD^Tk1;A*o$s zLIb4{*{@w}Nk5YK(B>J@Ue#b^Ccgy4{4pBU)mvHhJrb7`SJD`W#NVjvmi1wS7_6&b zN~nPT0V$!KVLOq*AaxKo3meQ2x-CR?%JNi-km5mM3?>PD2f1W-cWhH+UP1Y}#hLo% zF*ib|2ocRnPy&&JQyQwGKJZ|e6kPl!m{S?mQuY1}ED)UCI95XBoEKX#+Dn0yQA90S z)uV6h5vU?9o|wTdpJYfk_W;{~59H|1?=-{6?KtxK zCx(igV}DKDR!Z|{2w9zzrA@ZXgb-9k{GCtUE_LW|Upx}VAe1jBa<@ovsDd@=o4mbi ztp>M}=x)MpDSRlnxZ8YA%BOG*Lv$DwH}VN8%IMvgjf=Ze9yy3Ln%VO+lZXNrNyd~I z^--bV>ebiGqB(_f%3Whu_ElmZLfE0t4N&QVAcc_L&fwzm?q8-pxCcC zJo21{4%8B+EtAGs=XpT6A3jQ1?^JP4kxX`}2CXsACYe5AIAxRxk?j#>ViQ2x(mJm` z*O=)+E!c-Vj(sNkL=68M9 z^vSEfh2Hze)jkbQtA0h7P7O3`{$d53LlzNTgWzIay{e;u+-Kr$uNvr2CK77opRwVO zbPKH<)-@>*PV3rEYS$YuX-v$u8!EirVr;B_V@_6K!QbE2pM*7!6xNMJg0%TsoAi(j z^6FYd-$o1eq0U?X14CVfO*1G#A@Til1ujsVUo1ovL!^k?ik?{Y>$X>O0&CUn7eQs? z9k?4`&bwC%4vn9pkqjzRYmi9_eMhPMCAb|Rb9c(52`c02%)|7KkvflVl!RAbP2wy} zSxB1wPk2}4g{>ZbT7s}5$&a5NZT${B{DWraez1#!nYtUURDVy znpHaT$B^&$K;afcukoj-#fG04xg$nk_s1VxY59h=pb|>`IPeMYRO$F+$Y-5?d7b)q0;N=L}^KoLybuyh^nWoka>}ZfsMlv$=fU$yf;@ucd1Ly>0T5qy{_2&vAF~X zfp^ERF7&gFjAw`4UD-05ZC3ci%shbsx|aaTLCKOocInx+YK?E`u9|PDb@Ga5&<}Xe zU`>fA&GoPFm8wTjsj#c(h1Onuv{B<4!?cmb6rNNwS$t^lUio!#^E5RKeKnXN+!)s5 zYVAbY)fn`Mi-(rNo3O3uG4ag&yoW@*LUrbDN(wU}7U7pSsF|KQm!KHiL^h^CpxdKF zzJJp2C->Tj@r&BZG2&O@>r}4>M3h0YNzuZuwR;nuF-kKUkEH-7{6#j)D)r+kqrTBU z``MDx6jq6thp%(>rIhM(_(CukBEt{8Qzf+T2z~9l4TX)bxx2o8wc}m_4OH&C@Y30j zCH7PF&MBJcQ-4?=@loeGd~^1s2kgy|VHvK=joX%G80j8|g)c8{fQ+s~Bu#>p?^L{5 zour|!`Ziy{3g~^d@MBG+!H@V6eW>7PBV}`&;yF0nz*uJs&&~AUYDRowkD?E6CB)2G z6n_3;Gm!c-dObZYe4Ct{Pbii3TLgZnpMY84irS=->64(Eec`$G^A%@4c9gm=jr(fV zhore*E6^fgshvDUEAvdsTps-?JIJ zHR@oHCyLADx4((>T|^L!X0fqZP-&EW!Syda+Nmmsq+LV#MRKC`fnqjV)PY}l@fvs) z)k2Wwy`YUH`lz(O9Mo%CCYm20II+ChE^qFrPA7O}c_WmtmT}zURgbx~n3;UYusr7O z*UKr{m}wbkclOIHQ5Ui1E>7_wqELurui}1Yj(#^7}T*o zby)n(K>aN>O0UCe}r_98d*&Zr%Phf*=HRx3EK4>Us;4am}OYg6>~*ngGiaVxx(f)R0#J2-|c z`pk4ae)Jx-c{ny?2MLAf9#EQ~?x?S-iliD-d3@GeUk!cqo}0R{n~*B5DmghW;02Vj zt{VW(yk11c#l^vDNn;EpMUzWPOS7UJ5{gH-?O8(fA^wn=;?>7u<+|?3{Ap&%rBk*K zE(-OV$|E%JoGkNV_@;9izbJ>2V}H7Q9w>ySFn9UE2$V%2^QBhH(DDXtDWUx7r`U*? zofwf`o@!57hsW1HE>q9e(;#OB!pB>%NWCT)s95jWZ80^h{jA1(=)Imiwiy?QihO38 zmcldygb=RcXP()GpA#z(RTSBdK2g&R(^St`i3Ro)NseR!5kr+T$(!GoKjWb)pGd7g zpkLE4mg4Q|=J`-nl8K`NUlFI4S3+8J-#{VkMD>Bug@$2NiGksqq3!np`!VIpXVn|+ z4gCJOiL^Pc5z{ve zu*rGbNj`Y+l=Ct1R0JFhPM29Us~jWK4_Fh8CmK8pi9scrf+@;S+XHu0c`PyEtPgZ_ zY*Q-Q@^gNsR);L1n3qk+Nj0(DnOI1#tlVTHmVyFx1BB|BH@^Q?YuxpXJ$FedtBp$0 zB{~|wphh4qW8YIqJXenmuzv~vZ`%-%B{=i=DMFE*^hKm!+F5@~wEfl6Si-*~kW!$# z5hP!iLEW-T%%GT7nz;&BMX*l4bh?o+!hxNQ(nm?%ld2^%7a9G!B?5js%6S!)JX)f- z8v1Kmv`Dke;i{IZUNgpo9)-JJt{(4?UKjd%!01NJQf8REP;yd-QC!@cza+<@r! zE-Bql=bHS!PLVc*_6udu)rHRnttbn2Q}ZQSG3>N)*TyP1Dg)kVq@jz0FYhi-!I0;0 ziSA6_Gh@Cvp(qP<_7w~;{{uhI=)A9B=l@~;6i3L}NeDX+&fT2U$rq&)Y{Vc38XqaX zeSGbKpqD>zB1}*PHib6jw%mEFpWjUO@=XlzB};EZ>xP|RCE9f79|}BR6!u(II3AVx zv%4{8K5!luUcQRU&qAo!J6ZMv!aprg3Tx{Ehvt+clCuLbyVs#w*aZ?GaqJ+~UzM|Z z!tpBO+>gy=t5RT=v5!tzKwZ}0eCxuC*AnAT!=F8le;UR;HS@eJ;pe3{Xr>5>E2VC@(OyhpwzU-%*Luc=j%DnGoo$3#OGU<4`L)*jhD#eP;g_MN z?UCQ365voBpD6|eAN$(n-nbJULGCd-rC&=H+ zwMgH5DWZu_be~u~d~M<$Q2oyP^>Cfb9cz8Y#8m5iDm@fGHNhYdNVv;Hi2&SMkF3rNaghP~vAwB?8_IXKDo>BGQ;EmjDZ!f+lMcl@3(euJ)Da=a{CGLxseR)7o(p8XGg+>h z$lXz_^{)CJiW7KWwgw+2XV>AJxqbIR9$gLp5U?17R}+wc$fPT^KX&AS4>n-J$@w0E zkL)W2p;x+SgKhf3(5d(Aq0U5KHN}Lgg^56@lL}5k#Zv82atoEsRlm>@`rD{L{6hu~ znEl4*$#&?#O$UQ33DWe4kvCm>k?uvHcKEt}BBo%i`FmyYk|_JLUQ1>=N}u+bv|uKj znmFB*{TpV)ApMf;x~;n{ya$Ns#RiTWGnp(ht&W_I9ERhfGKJi&{3W!)kJHV1^s5gE zrkBR66uF=jD(USVF0M&)-70tb=4>&i{w1Tx%jhK+d-FQAb#ax`g3oo{id|-tsXh#; ze}(m)w#BpgjJ$;@E>4@^XMz@a%3=B#33a zkVrfqJ07uYv)}0Y-`IK`8=AiypXhmAX*I!CZK(PpLzI_!l>OMuCPNbI(orA#Nv#M< zu6o9NXS27$S(6nWVNM;HbEUS+%gn7aO5`UWBQI0kSIqbEKO$|Dybnf&0<@q~39_e+ zFo&=b9#<8!M}y;paHa1vq`YB}>2R6%Pw=nD6IPw481@RiqMGii4uW1+K_vG$r|s;S zqGxjxR$`;t@Qu$y-H>22i`9sMh-%txnyKM!L(J*QD?cy}B!BtPU*P2b?z+g^!V4Zj z+Ls;I5Ml5cD`xVVG2cvXfzn>oz)eF3`Os|WXY_nosjz*cx6$11x*pT3#4?g|O$?qj z8nAovj|#n5#*}78MiM~U7SLwN!&6Wiy?LD>pX*g!!$*zzHA6np;#xqZirHBwh|v-M zCBF1#__{!CRpG1>tXo%bY6rh;ja zStizC{e9o{?a$&}c6UB2C;{6G8PtUyCETmgZ&;)c`>4^iH4tiZAm+ND5w}*mckQle zLx^sUt|6JwY#jCb__&__A0*K|;R|;7H4X@3@Z)im-k#goj#+(qoPGAu%pvoigeRL} z%Wnhj))H6S8-3{I5iDO+*X+SvPaauNYO-gQC~A=S5ul=s6_-LvR_nvDWSfebU$mke zA#Q`;{BI_&4<#R^MsSrE=J6HBCS}!r@z_;oUfqK+Ir$#=$za&e$gz`Q8>>W@{gY~D z;_Y@rODN1jo7k582tKbn$Wl)pNcd4Jj8_w`aK< zKMp>fhx^P6@3dH4-b5we&h4K{xvyPcH;h@-P9$|pSsb03+q=$-4ZeTO-vdq|NMe$*wdNG9J8%BG z*^N<#@5uaqY$L2QhUa&EMgc~C2NieL#@AH)EoTTQ@dwc45M>ClVe~YMnj@bl<_lD> zPUyd)E$Uz@V`>7)0oqi)v2UGh>?xVvd<^T>qzLR6Ss==a`?=ty>8*kx|J?%2&DuVd zeHm_A{Tv2Y+M}RByjSvl^l8sUjecWo#!>g9;@#pMS3@2$Ee*@`oD6a?eMjskPLV=x zHA>2*G+S{tdL43TpIb)k_STxU>2h+H3ZdhGQW}np$1I4dur{2MvV(5?aoV zF5hZ67@dcm&GsFoO8H;p`roco)RvXl4db)JdmxwS5BQBpVWG00{op-!A3=Y0pArr` zp_OyasL&`4l@%(sJ+orel=nt37j+DpU|OwV4d^B}vkhL+S4#Z>OSOds|4v|vA*eEq zFlbQiP_5}HByUQtE$Lcmz%MR+&KSmY%pbEj-5^nxcBqrW6YNb8c-kPQ``B(jx1|LN z16jp8I#eo}*_92ABF9e$Ft0l?qttw9?Amv|b$C8jwUKg#L#wniC_spnZ5S5Z5*l^z67$Ut`8JxwQ!HaVf`t> z#+BnmxP|AupR@J7q%{A{x=!?BRRfbXP3hCZ_7PZiW7teB{I4Vm!W{Fal&zxXkSoQ2 z1D04_b`Ca)X%1qUR^orWc;`;N*y55ih(fq)mvrzN+cAI zHg)1|Y-lF2U~0o-g@esV5_(bn9l`RAk(()d$%xb@Pp+tY*rue>)X0j}{B4FT59qRg zC7&j(jIvL#w0KkRhh8ykY7RW7Qf~vc?{nfrf9%RoYd+3Pn3lG}-qwobh_q2otK7b|_-F;I?+%b`4uK3H&9(Ee%!1J&M{K0~c0(9xbMBC}?I6d4# zx}IVmPyoL$y7JaJh3msradlZ5-;8w+yVJvjd7d^;v|{xy_Mnnx5m+*_Gsgm3Dojta z2A-hIHKumv7jGSHPn79^AK^`8euMjF-m+xdsO1L)NHNS6tf01DRzpEuu5?Ue6I;fZ zGi~^}ELBa5T$*{ugnBT|R}2X7+N?%2%O~ro=l>KgR{MA-tFh{%vV#-V07=BiNXRpK zH8~X{3A%(O2&eSH4?UM{j`BAnv`Rw!!XGtvL%O8pyD=@PUx8zbe}#bMbp>~yQj=DX zm+rMxS*E^-847=+H=T(OMf;Q_srMmz87e;l^OukbUl)IUtiP${_EUfU2XyFrpJ2t1 z5$RNqx2M7ndugNWMAS4CgLj$a5s%-38%p`LRWt|)V9dNfETZ)G&$oGDQ(E!CD~J7= zdnv%_(^5}gh$)e>D|-~3&Z5RMQG`)a>967+DunP5 zq!KKvA*5~pcU3(yS995p&+Ffn(GbtrXv@qki6x^Fv25^19a-;{v@0ztq6YY@0A5Ap z4-B7M)TD@JHN8Tl{(Uc_@sjG};odM(&Pub0?O8YOhg(Ni5|-S^w$O*g9nZKPpM^ZB z4E2hmh}k=Ojr8-T5^0O<-_R!ugqzt8?o!<*l3;tuVCULd+;gYw$jHb*m$8(ElUaV9 zXwGzXeQo``iOMBHBWdXK5k2Oievw%7A84rzb|P)4ljf!HVc{@wZZ5Un!?tfqN9Fks z1%=|jae3o@r}pJ<(z{{%8#g4LdM!gb ztd0SwZbsO>@@Cnf%o=+60#9WknaS zK;B~M?-qHuPBtG~*XbHW#8l}&fiiN$%4B^^RD128ThYfr_Zi`8cL=%rVehxdFxu$9 zBqh9CwmbKX(Iw3qNb{!pS>94HUYxkry@&ISI)IA^T z#*%#XHv6Rua*cR)aj+R>-0C|WYIi12<)NKK!`8@k3Iw0}je8%mms=N&r8x&PW;Kxt zI4fMZ2U-nPmyPuQ=Q_2Z^V)z;Oy^Z+z1pvb<17as!zp;t<~FLnyuE)Zx*2l)q`F#A&z()R2QDUVUO>OV`f4@xA^m zQ}?`0%#drsbP~1lio%h3n#t>{v*aZ#*FM*4Jhq{T=ZPfc{04nzN#P7Ww_)b2s!zmt zlmp=;$1^A+=D#X0?Q^R2Ya1`tuX&?VZTK$D4E^cyO!2xshCdf3tPfb`D=+DGI&G1i zC3)RMam8W8JW1zbca|3+0+4nV+)34AB6)5koSfL31D7V0y|L6a&sAs~uCL!~GDhDV zyxxQ5+4QZTjIkHdW3l}3;^!V*?V8-5>7FDzme~l6t^Jf_BHG4$m(cVN{3HK+7P`>= z83X?vXQ_rHM}laiT^KsM*2EECKD5dd5Of?`)+l$9*z`300oAK1ZLZiG={2E zwvBsIguCn4>0##vnL#e>A))Hk4#EryWC;R6yygINl9Y-;su_f%V4!)^TF%mSs_Z=z z1qZ29(3USaDXE)kTuon(yQvZOzCPy+{Y{pnE@9!5he8SO%z)(|JE|gG+O>x<;f!&K z%6O76Wsj44KyuFHN7tUc5i$yAY!*c8j>}mT(GdrCL`U|9J-<%RRM^c}1sJ3!KT3;P zoS8bAahgaa3(aYxjHc!37hu79T_cU(!^Z~r;gp$ffSBS9G$#s+ZH2vX!Q3gf55SW` zR_erI1}q5Ohd1V(mrISi`nX%f%ftDwQyZT`?uV3?)lpQ7gz~IE%9;516&uJ2q4@No zJ@(QPfX$imL&a*h#Zj$vo#%DB6r|P|^d5oFBotYbZE|d?$@yodH_sB!$uanqaXF$G zw~rt2`BANmbsDi{K9AR^sjf)yt1RKJ?ywsbSCiu^OJcV)=Kb(|QL!7ez8BfBPh_iR z3~pu^<>4yfW;nP_xTgFPZ#R&d*v_+9;YbS-X-ahfu77!QQ$k18r^)OyWIyYc~WLiQuE8N>D%z93|2z zJbFy)d$XlJxHiX)FIZ*Dd{rum5!;i|c$Mv|;wfo|N2o@F=i52~F~b*nvb5=)uXKw$ zdYI)+SYj;vShW~H_*i28>TxN71Im**F*|PEcb$z?W8L?F_?wEYqiHOw;Nk_na?jK)9^R0w)XaIryuez*AD-j)856Idr`f+bUGYU9&b>+MLa z4B;RPKZZQ>0SOh`kIik7Ii-qe)j+A0KYUc;v&1iZP{#&bhRYLBv=t!Mv$eS+gNuL{ zKezF>YKYl!lbCisJH9i>XOZq+;O&o8!^q9uZA==|WiGHcjbesMZk7DShx8IUft45H z$*7Qs{Nm*R^nE0amxYs&Wr5x{5u(b#k*9(zpC>LaLOxJbi>+4s)Ax_x;K4I|B~BsY z))b|#^x^kF#c2(wGGq$HO&sA&wax!pXv&(&ebEqX zx5&p8-vD`jG*!5g@rk1~({`nb(|h-E7nSie%P&_jsvcGsSM$U2)oK}1xyH+0 z(`^M*915qr>}nY+&kwf?UfHIYZa#q8=uQJ;?{mhIIK;i5J8aprK1)Wk&)5A z*6G|nyDZbHbMu^j;MqNezf~L`I8)s9`9x(he>9xHizoD3my&uiqKlMufQ>Er|Us z%LKpxkYG7~UzPvon)3hsk0YS)zgYYc)lI8@1U|}H8P8cIOQMa~fB)75@G_ zQa!_`{hoB7qTLlw@ur=_N8GE`8%K)kV4{wnY2m#Hab+K&E}aN&_+9XZ-UH#;cf>6i z2r6_Efw>pD>F+rF1=&Ug;Dt7j5`k@yNusKdVotEG;nf~PB=NpVjNIs#n1PrV<x<_erApa3;*qvVlH0Z(BA{HMu^V|qVT&Y07hy@1X6o%8q#_C-yXlX zwycq-GesqDLoC&v+k-6KLD{pnnY&L_BIm8tzI!Ha%&DCi#C&vos^#~$anc{8Ur61B zG$957{mDIW_!=qO>5Gl9Tj1!ra(T*>;%JyfjlA>@$OAOAmCtX z+b7QH!13wzhliByPkK0ng^-rlB&!nsBZCWaU!*0vepS^}H@2HgR`w$f-iSGZp73k- zEbjcA&G;-;bkH6ES~m?4)xLjo1OL9mKYy$8|BszEeDi0%4Z*km8AbfDJBz}$ka!O;i8Z!VDy&)4 zcT~7}n~N^hUDHzC!51e#?CdNzMBgp>s=Vf_I4KR#AI#|Up)eXeQ# z=2fHmprsRO`=`;jOeH;(T5ap7))-S)RL8zgmvs$QvYH|~`QEhHc#`)=?^=juMGUC@ z@R@Bk$rNQksS$fdTDVXM{={-niH958XT4ZPdgJ^^OTnDrBO)7)ig)yK+5V%nd#%`J z2?g=^(9RaYK1sp8D^Ov4{8n+az(=93$hI~Q4RyyQ34H0iUvwkOnkhA1MeF7osx+Qg z!z<@%Kj-n}6BFamIm14@7TWP!S)(hJbL59_C%So^kR5H6cdbv?H;6TUWqov($)4d- zgP1w#>ycWEj>J_^q0Q;HC1=9tM4M+VZ}^hA%}ptlLkN!9wXPMo`_M3Q0Wa6*JjH{Z ziAx@lhXobgH1=wp!OX5ef{BEqFK1B5yn!l}Vh#D#dtL>vOlx%q7Xb~pr>#(u)=>bP zn{M}lyi25pM|=C-wdT02Y`(H7&c^W51 zy+91n<`oI?MpPl|XUW_GP8q|S%J->xvfmfwb>#bZ`?w_S9&mm?a5iKX?gqoBm#pNY z#ZvV@r4ZNPBGqyYrlE}czpF(0rEX+y$x{dZVjz!R;d-V&E2-7;$gO;)XT3B{JejZc zy@rlF_IEp^ZjP$cy#}&4S5IHM^27i^5TVv$RwDVeEQOwIlVYU8xhDs(@lFpoWiQSO zSo1KA7EZtE(%-!S~P26d?0EwcmhcX-0TcytDRx(xt*m>%wP^!oWTTYp zO``&dy7Je#t#eK%scD3%$M8Aic^T<3C@s+>`rGsnId^o`*aeGVCeO(>`i@{BYbF5< z=iR00@P4;f68Cw-E(y}a{~?u%NTE`JH=M3c++95(l{R?^1AWYig#2Q}P!J~+v@S@0 z!;tYCguMCOwrDUlc`u zof8z(UaBrlVm>19sY-Z!d5kMw%_*c_-<7Dw-k4G<-RNI^=6`2n;Qy@eoWplmetnET zTXvTbmRsiE`+WV)1A{1t_$S+s;XfHKfz(A=sYg<~7j0?S36=@iTU5zdPZhgd32Lg| z*K1mJ@FXw&Qv6XV#X)A*th@3-NWwx3zReRqo62Gbyp_?AnbF==x6eqLK~EPdNSEV= z=~d^a;9Fj+_(v5H@N|e}ivZ^lnD*Q!+ix6rnkQXp6yps%&7=fdkuu*bIy*Z)wJQ22 zs{j1Y*07Q$m0rXs9#sEd?7ekdRO{M4J}{KhNJ_&X-7SsMlG2^h-2zh5jf6BR2uMkH zHwZ{bNzG7-)BwUr`CION&U?0=?LPZ`&pE$0KHoq14D;ZuwOq5F=eh6ezOL&|TS%)5 zpe)RH$(kc(8_3sEQ=%#YQ6Y&TRXFRx&xgt+B3fz9F6+u6FELASHfpEFChsWI064`( zw3}D;Iu|IH67ew6jvEotrxW;onSooj-1Ro+69>&bWR&a3rfZx13aH^rZ!pjd77&#% z{o5vmD`BmyEJ|2u*&NS&1kIH8q-mvZS3;n{i)9$o)8fQF0ncBjOT}E_U#cn()pSfv z+zD;4AqbK1FNzHp7LC0a&4mFDT5S>o`sv|Lv=djf@8zs-4Ng*eE%NFa4fe)zFQBLf zC*z|MN}<|Fsy&cexUN%TZ4As_Otd`{8m?J1zT{5!;hj^N=Ar zDwTJlxW?B^)q*^!nBZP9tGn#`pJV2rNMz50xOm27(dMRvEz(TPvXjyI2ihv5uNYnw zo*5wwx~mIb57~BalZ1K^wy)C>(!V-XA*CGdjKjse5D4%7%)ftQS)me;#qU2&^N!N0`B{A9VvZ9T8V_ z1;jdTck`@gXc)>l4QK=&sc&!%?~Rd^e9^23pexyK){1W`T(r&1sr44{zE-W9_X2kl zLx?6`3KWYkNy1dg8UXGK#9G=qNFJ*6vG=bQ2xUc0ngcK7)5cv< z#x-Tk{$_V$n}$cuskN$@B?`0;$dFR5fag(Oh!HL5_Ww3m?caL6J#e|0S`&0z=fE=K z0St^$LZsj{H!gwxj1h|JbFB45;R$t9aHPe+%Jpdn40cG*4n7= zsk<}wWEF|Zaj_7{D{|Aa4W%pN6^O_$q34R9=tpEBJJF)4ak8qa`+zvR7GyU599Zxq zQ|#|AAuM{V#-EruNG~tnomk_omzNqpYo#hH<9YRN*=B(%9l9&b#utOZR<46?$sE@U z66D?<4dNMqtUb~HRyzEhA48Mqc8a-A3kCWo8J@RdV7iny*)^8D2}tPe2NEl;C$}C2 zAu-5KEdcI~fiEwl8z((UJyau!6K$GP+U=$M<59yVX;({Y2;vf;{$7lz-Lv^xVmgXd zvwSUay!Y$lH^-m?N;i(pF1pZB@CRw|Ls&zvhT*C`^B1ES<1lKh+LnUJ4f9zqyjK>) zd!y%lq%hv`>*#Ewl2l1DW(bqM1HLzX~>;QNW$cn_cm0t+TwYxQ2@rq zKijip#NM@4rk_N?B1RKssGmZ;n?Ae{Q#dZ>`0Oaq#ppLGJvncxX&XDmg3*)n%78IQ zp2oW5<)@ok?F|KswzCG0-2pFw9(r)9r&xwO3bh(*SmRV3IzkI|^o{pd(0iR(Vv>e^ z$}0QEf#SsG^b3LSUafLW1!aehg1qXl=-%$LS8zDi<%s*R2l0*&P4e{Oxafw4eRz1L zdG@IM<@Q8trqeT9znhe0GgEBAs?S(dSksdn`8jcx7nWBLX_v&hg#_b;eE07TLlgU- zqu_?C^*TCb$GYlM5w}h6Rd7xojpoj9a_*cynQJSkxzXZQ&4setebEW>wxGBP`uLAx z+W+l;|HrdW^_;a&0c&hgjl%k@SR=S5`4R1H>O>aFN5PdEBs?LLo?LZ(0 zmeUZTcf9Mgd@Fujt0w71Ce~L_v+TxtTv3g{^(UD9mV?)7uWAqO+HD$pKZIm^JE$=%~oo&^zm5s!I7g$cPMNOrO+Bw0nFG?n5; zkEb&rw5=LNDW(#pHyz%Vl|pfmqS!3RN6y@>B5On9+jfX)2RPj>^n6G$MLwmf9SJHj z-hNr{<(;|>?ywa{F?~@82(?QlNA2E157y@3PpC#jk0_;K$@yU=VE12D-@2ASx~-Xp zF44W58cGse5H5k+pZOTWBgW50e2TJ^y|P#$u8dW03BRi|X&mt>9d zP;5E%CPqV;UK&64bif3O%9m|!o!#19eR-?8ca_KIEyA=mnL=#F#KNQ-v18n^@Pz&# zo;`nGd&hEnZEU+YnT|=#dZBQa;aB4~V?B}Oo3;?7K=HaRTBa{ov$E$ znBx_>3~_4y!pJKHzB8+d#@yAuP_M#JLbF^XR_aO&^oJiHjd0HQ`uvl8;WbCsqsTQA zg%iXS1MJ8)l_^eR)_G9a3FFzU!I~o||DL=Nc z<;kf=VXS(6ZhWE7=@@9(79Pd?9r~9gAk0uQD?W#>OG&nz3wXH1N!{VKC zQ3+6^aFcPi$y7bz&px>d%-LKvTymOkpmG?$wOsV_!n&PzKoJD2lHR8`cQUn3@|W?H#MsgV$&6y;q)LoMcdeD#68W)w@Y|{0+keb z_m?%>_3G9EYI{|2jD|wC1B^2M*drZiGPX-TRD%C;aJ>7>=tAO~yW?oq6UH7^xUV z$5pLUSW>B=$)ZERqkT|LJfCf?7WjIe`4sxoDCh|D96fgDi$%q_^L|nkq#%J3(h+9R zwgglJKzpzsQeR=20ccP0?-a!FRKFF(O1>4unyIgDCHx2lf&Skyz5~$y-~IR=Q2f7q zKVJ1E#E1%(EX!G&!LV8Ow(sqPW6rI6bwZ$0L!BsBL?(b+|97*e@zP#;;#f6i1%QgH zJxa-OMb72koHI1bbP%uor4axiNCL7&q=Yg=sr13;jkt=ck}eB|(~J{$VV1bp$n)-p zmFlOFV=g>)9Y5Af-L6D{r2UH7U+mYkU3HiJV4O6G9FLjoepeUhifB5p`x?NtJR{xKco3{l5hMW)WTmd>sw^B3}T$js;1B&j4RX zJgLC$qpO|o*XNy|-s0aA7H zb(n41^SchXQTY5%O+?ff9?RdMg8L3O$=PkD%G09?P2;o<)M9wkCt z6my2;IU^*-r{<)rr}y+&`xW*xJ66e+dF2qEFHB_Y?QI~FXv?0zFBlhCM>Po?BUrKlD( zs=_1Y+D6kfL}A&$@ly|=5S}%U7X;`Sfb-Q|7d$(+WFYQylZG^G>(H7B?drR;;?|=hg)@*O|S(|)Z zRyf8)#fAUjXfZ+?^TCAU&85#VcN^vIvk#n=4F_Ra2#vYj)++6u5$8%bnOGLy_1Gou zRX}}8rFjy^0BCX8rRZ6R^jvcZi_hPE)Ko*yT$+=4b%A-Xm&0vFw0Kh#M{_mIwtRZ+ z=#{q4?c7ROBy)KIFoy|CDW+GuLGAsVg}PkSoQqkvuow`T!Ei|+>B0*wd0wR^Z?2V{0_ z2cNEBfoCdpfOqx}13$6U)qV7j5Y(Z?k5?RssAkH$s?b18** z-`e%Zg_d$+jzdcW=ujsa7p)o?vV0juVs13P?(DIqw_AT?qffs~9QW81 zQIe&b;L5NI<%nBto8a5DiAqBss>wIB4_2CSBHx4&q*GX42!o}Pv%Vx|g zmTALHd<|_8>DIHgCHp>|BbyZ*NBXQ`U%IB*O1#Txx?-WIaPRg_($#q0x|BWg+#3k* zLWH^3w9~CT-C3&zt9PE@P36QauE*=o*F-I|DF`Gk>q0L%45p zC@@|ZB7@}hm;FB0`g{26k7LGxu$3>YD>xl{j9){QDFb*G-_Q2HoBp%; zKU@LP0HOwVD;*j&tI`l2U}2Q*5~DaZkRc%LJQAE76YC!0;)VoT1(*HoTbRvTLagb) z4OTwp;W}6}9vYQz%vP*p+M;Ahy|KW0ASMR}r7*fRa0A`N&|mp12i5~n@o47A=KKfZ z`{txb_a2?eAQdE97Ym^X;LI$D5n#~PHAF8jGeJWj*Cav@{;z@v}yb2JG?X0!FZuwT?+sV9!5;+H<;96Y`kpRrJy?qZB9MMh!ETFsV z4+Wm_K5MMXW4Ca7Hi9=4qIMK?pA=M#*_hb`-H+0K7+>Zg7O0xnllkGiK7C9;HRC$& zEftgu8V|7W7jKX<$opr&_5Z|iB8r1Mp3dxTz3KpZsUg3v2B4Q1GzU@?<^ca%bT1Iq z?h3ynUGo47US4~O20QOLB9{_Q-TPQ6fs~9d5LYoti8$DibKla+0hp;nwG>n> zB&5Sz_P11@>;9aq19E?K^8hC+e`UPPXyjHU3^ScT^_4MQ2H0WYt|Zporw~-i_{BuD z+r$4PsigHQsITKtr=2ozQS~cGVdfVv`R_))S$Q($<4VAtFH-j-OiphArcIvT2rfSi z{H@tY&F36z^SvRLE!jwmF7b)yVJY))^{g4R@|rCb zVM7kzoF(s)Q1afMFV>UAp?G6C(PR}BG&H?P@U4d5xs%nOzk+aVshB6b{cabd|rEK>3Eh}#pc~e8~CX8n30}E zTuWo1?4ScE1(JTW4ScaYz{OO~h8mQH>?SanF`QlMM`1}ITt2)GoD4~Kd*OwBX28;N z3=BJ1_Juiu2lc#^g*r`h&@hBYnrRFbczQrh-m1gx}91sicURO+qKbx zh(AHY=yAnKgWtY_hhsoivCD9H$|P?m5co_D0fssuSwrn>3I&H}W*<3FrsrF_%RYI`*m$8jnoZ;i?4 zxyg}>%BmSH(#7S6C_Dg@Q3CRviRZ0jfmEN?6zwCQO{Lk}vukXxD%^mMW{;3wN6F(@ zRrTr;9_q(6fL`jTwN3LYvES9tV82Nh=M?+GA(Gs7QAJ*u42bUwLCzC~6=9|1&euQP zjJt$hvkfdE&U722j~9uOTr1Ji0wK+rYTxiV*Qq&|HZss}8Spv5hY?2(i5NiMBDcJE zg6YW*T~S2<%6lrnQpn%2*p8UJcEMn&ENO1%oOh)8v-+|J_>D=bbjUTcSOEoC>7BJp zV@J9*Z36?&8Uq`F=Iq2U^Ib@{Ns7P@3L2LJO*Z&bBH(GJO%maW{Q%DK6;!wLSeobM zHFad)VI9VG0)<$r^QOTHTq8JDc?H6)d%>cp>~^o0cQ7%!GmAh=2rpKi#CRSb@*~5? zo&&-lpofKnl`<;y%xyBAGi;) zLHdRc$cf@D8H_l)61q#V5GD%~@-!!fV2I+$kyeTXq98)MW@EfpmF3<{-bYi#F;N`7 z0it*vuDxjS07Bs+NKr+DS}6~(SS2toBqv2>Kbi?<6n^R=94|0oj`o==abPkPjLWar zLC8ZmT#c;E5b53ea?!i1%8FztCl!{pEg^R(P9`lP;s$6!Lqn5NHKaN##If)90ZVEx zumh~a86!r*u4e0_!unF|so6y(35F6?E_;JWlNGe`aqm5^Id*hcg5zYTpUGU1)Rzah zlZ;JDis*W3cahx>;TZJrL%(-0QiaBRisS9-AaveYKin(#Y}2l@NyW+mcCU~}_--u@(Cp!-$#`Fu&}7JrI`>KWKc)5o zo=`Xro|rQ;eB>pfA~0lV{4?`Lg1GZ}t4O@+aMA2TdBbEhY!PSb6-1!A5ibn&@l6Zy zF0@MPO|h_^CL!fFyf;7+u|SJ3ZWiuDpf%Ww5Fe*dK*4obkuClba3kcM6tk$WyE(QR)|W zw2cND)PodGXvpD+K-nk;`QsQA1@*Ng?Rk)g%d=_tplLM5eyw$WY0`TstZF4f{DS1Q zH=dH1tEQHxov_xmJMfLA-E0y9awX1P^110n!1>5NdV5mp3SoGb@3sNcHP;)kw(sC$Qn3w4*ZUIc!IsK!9e#eLAos+KHz*#sN$A0rBA{+xz-Hrq}e%k ztzcRBUC`ZwJq(H{w=YamNSd*q#q5h_`Na(64nEhe?PzTCEE^b5^uuU3$C7ddt%EMz zr>%j`i7Jja6dHr`v+h+VGiL2#lfN&~PCsAAKW#gkD)-0{=reNUJ6G~d5)g_ek{n4B z5~7H0$$nV%nSanWmwjeo4?mD7_H}FKV^nr?PcJ_m)<&yHr24${Y}X^IRE?GXM~qpr zb<;RmHdQA4i9ioU{E}RcxF;K_F;ElQ*KNYa(?~!kE0wH*7cX;)7W7K9o8Na=g^Qm{ zj`E1%%16Gjm@HEpryF55@Sw6Nb<%bcrfotw7M*1+K;#*;v^O+#5TlW8bkyBiW|UoV zq*cCX0tw)bj7hXU#PL&rr?or-m{v2%J@#I+54E1WAB=qeY;j`Xd}$1vOo2b_hA|FT zLoZsf)%SX7E_g!R@PO{#10QYWjMX*z*y)2FqFVee6a_-=;@?2OUKQd4hH&=fmZFb> zX=(M3JOuBn?2|3S2Ounk$b-_v1W6&IA-p@zHn%-{AC8W$-?zj2_@UYW)Bb%)k!_9% zmGeI74mB(4z%ureg`nGZ|A8r0;A$XZ8%{sHK+w%d;MqAqZ#Bv~SLvZiYb#i(-N8N4 zT;DZI>_kS&TdTyHL--r{RNjR{9uZ8hdnd!R9p0~Wq|NC(Ugy1pT`#3b6VZ#L&p*J8 zuHz*7Fln@Xn=kE_pl0;O9Slw#8*vAz?#4UWaXrtdxkZA2$~;B+<+(kTU~@w6%`9Ob z$0dUuZK*`Gc&4X`J(n-ZL^M~qAgBK;c&tdqS7$5trYfBGA&0w;=Dsfj3U*~ssyf$K z!IP~kQ0o)#s8+ExlP|tg{1SL3fxe=1mUqv%h8wELb`3lFN9Fp&Z%l8sR82ExNzU$4 zjcv~O4-xYCIy`2iZW*qJhmY`7(F8}oLFmCGNDy1SH#w4>3#tyjf+BXIwljl##@;Cc z{S^p?m~1C$>FO7FNQ6lHZZ;1sn}^=5*Thwhmg?0ON$@d*64E~-0~de(%#W4>jbl`5 zQi?I~MMV!&uK((Lv=N(UeT=su)nfPua0L)kcLkgN_!r$j`Vk67)RATP1I^#w4=ZrDP;sqr7^aCbkyDW% zf(v}&CcB+^m1aYj^W@v_6T+)B=F~c!$)y}B4-(}KVa>x^7YeIwn0?}g&2cZ^e*lU& zyK{Z+V60tcnyn2sU2y8S5FgT8bz^JdCcp_QVjndxirsSdLX3be=h2M7(W+XRUIiU$ z3~J{{0+fv5DXa~w_|Cy|lw;-o6o*AqO`|egF^4peOKOnj5QAnl8q}oDU)Qq8-qS^{ zQan)>hGZHmWrbdGO$*}Pd0b+Xk7cleH9u}%pGF|?1kdvUF_$!WSFJP=UVZQ?pTZIq zM8#syiStq5ruB)xm)a0ggMqF8I?j|VZ&+f|5pG62?rB1~ow_7hHNsCOchfJ*BS2hX zkFa2}t(rJVz6;AW8Dlk6$GRb8gNTDRUQ0)h$WpD7Cvmt(tP2!67i6c;gk1YDAVmX` zH2?m^i)}YjJCgi` zG9Yc`+x>z%O01q%wv>v!`Qh&A^zmg1kkfH~_QJCI`URQC0#i&1X7TexlyZ`t zSoojIZ!#gL<^odcKJ54`Yddv4wJzkBZ+nTs;2^~k@fNv4-H>HWOUs6Eq12oLt2pV9 zH%hz|-jHNfHwEB+AxE*miMEH- z-0`KX256k#r3Zvzw|LOeb4lM5pMK<<`Lb30?pfOrnRQY~>TnvP>4G9O@G%IG8X|>f zO2mlM^x3<@1OXP1pQOM10((K_Nmhy#&44TCNS&%tv@p&cvlRTr{~Go7N9gyz{XGB@ zSHe*9BKFyj;{;aD?R#}JBA-o`zeE~9ijiOZVn?{eCj99wF9_p`WzMEKy=PZ;)}Xz% zN;+)Ui9?rTewtXhl|W%Z>N+M78@m4174n=-y)hWKXhq-1PKR@A<;y+HZUvU4*PW)I z`$29zh9xwc^?WACCP4Fqc6xaG3#yVbebMmA8Kf9i8Uq#!jr^L}fRn8~K+mczs=g{a z%`XRJ7W`y<&V&pt(&^k`+=e(@HP19iO-(5*{fn@xBtg4O71WZ5;10vA9&y%&P}y17 z{03OSv_rc)A~=g|#2UkGsfjk^^e08HzZq8kL-oZE)waLG-~aRFHy!j6e_;d6&oiR< zp41Va`CX0-o;S=@N zI!LY%Y4TdQsrkvP)|~m~f{^EWFYTb+^=VCxf>(QFcR2>$M80Y3zZ0 zXr2;0u&0V``KE%X0`zdBqng9Y%+>1VKzA22q}mdx#+C*ZM4(sv&0|Xf+owSuZq&N} za+D_bU9Y#-lYWcbg?nzvwCi|4HI1hXNr=Dc`+esh%YrBr)&e7~U|dTG!5-+l#|25n zOQ?w(a`c0!l8iLjYpCzyKu$hc11D$V&pnjsx&yp>zk*9ND!jpc4r%}?_a~+MBYNa?@ZLzs@{VlPaw3W_9_|;1abiZ<%M6cXRNlso=mxDkNPRkQqsL$ z%!s+(5^c)s)rp#)$xvM6KPbB#tRS+WrBC@^c*8g`+j>YpYduKY>t-*EYE&y|2Ipl{ zA?ImIeN0h96=T)J6Cz)J8F`Fm!lJxL*- zVgX>43j<|~L*k~rF^+G!zn>NTob0(N95t-T1|pg7ltY*nH9|yzMQjf0-V={fA4d_? zK779@1MqahAn`H)PscO_7X`nc3G~yvJ9YCk7xYe)-4{5#c*nWO(5BL@V9z7nu^o~< zkj}7kI?YxE2}=f1Q@+p1fc86*JJakO$0m3VY#yK1yAnBv zkKVjcS_`1>A;HsAY4C0?942X)@<3pn5)_Ljz6b&>y7gMJS_shv-Bvt0-;Wf`T(7qM z3c{^j*^Mbkk>e=5oierB1N!oWX0Ve;BBlje#({I>g}=tufS{Xgrd|t>H4t}Ii?uTn1~3bJUhF^*q~{wY)rC3cRZQ`BRn5RqGtu$ z;doc~Q|8q(iOA1Wr%wBc4xt1jRQLQN59B|na3AiXMDP7aBKJQNqW@bt0N70ozscOh zt||ioncKQ^2D~vynw4@BDasH!dtzjX3*> zeNzGwv?J{Gwi7uR8$2P;u(%XM6yE24Ar+!8{#Mh(y}1q4G-H(D$M`@^6M1<6=;ZXR zrYW4`eFO8!ZUaMAnDoE|OWWtL+#+j+oXY|duWAc_-hPN|7mAfCItmzQOdba|K{&cP zbB3ku>mJm9(chQx($1acW;33T$3%+77=k1}OlQld%J`>*PJe{?{Q&9x9sTDg7~kI~ zdk;~6S7=2P0ZYL6(9S)%+@o6^Z>pZg+#Sq@vbq-sPZicW0c>Pl4N}z56S7QhE+8_U zBLu|RSM0@e&m!OL)7WyBKeJWDco!P9o9(}af@aQLC*z$<1XO>agsD5`$*?PL@5X7T z!Q^eX9n!rgpfws7E*>6VNVde6Mw9bYiIcrH@fs_FG(EG31MiEC&TgWdZA@z^dTe4PQiNlofo(A#!rq$2iR)N2*QbHYsKKOOm z>K6*oQCVg3zH3fz3*VUC(*eh_eb5XiB}FHrX6|uNqkk(-<2-xoU>qnQOw=LjeI-Yq za=v^0Z1q9E+k7`ggOj{zTc6dZD~d_c`-Syc<+=mjHiqK;42rKmXr+V{6%W#f$VuCP zHKLD%oG+9Q-o3dw(E=w4TZYCyd{v|5ZALuVyU3(rgfu>ei=_IB<{0~rc6cV_jlZ~P zegiN5)X%>X3Hix6sRF-(1TnvY+={N2zGV_$L8IMYL5n4TC5h*+Zl3=zANgk@jep{^ zKwe^@dbM<$aY55gdd|of1@E3r^d9=Nb^KX+IFa&&-V28Exb8(|9ur9p$C>3BJxiZR z6yduX!>#f1l&-o9TM_)2<2Ov~wV6tM9j$FVzGMX9vN^k1Mzw@lC28j7anYy38X}I% zt$Wly>aZ7#JlvA5dEFRt?Ky$L2!^?@w-$?k_cbhE^;uCscj=wB5SFdu^#n3=#ALI6 zC$YPz%o)?R;kNeUM5D(BQH;5p@@o!d5A=+cgHAC!$WF>LL9DpF?d+4H%+BEL`|{S! z?v#ZE_!*2%PbeTZ!jNpNY^>@KSsC}U+4%iR{;WzIA9|Gp%^7woZf{TFzSKnaZ~`t< zy?$}n(P?vvRcS6EBG4LoJ`!i5(Rj50Cz^h#S7-24(zQ#is#07(OsG5ep~v-nFwS_- z&C^~;s0Msw$gH4QA{E+V?bE|-s$O9T?2(||&MqwpX12PIct6iW{-X_B`1avKKd$&5 zjNGSz7kVUa#HYz;aE_!~4KKTF zsZ$S4^cRKFMfB)%AZBk5wdBDs16awG4<4U9j4M05OdO!xD|F?laQo;=Yf0^$Oc><; zvEK$?9S#LYdcE~z$5p8ycBwNUPjv~Kxk%I2xD_liu8QUKIXkNM#X#T zM?;(76iOE)LLO(JT-y|~u%=cQ?icIJt-w>Nkc;+KMHQ;7Y^{{$_mqCRY}sB-AI6`f zB#o}ICT!?8OY6Ss*U(1L+~bQf<5hpuz3_OZ>n6pa05i@tVNh_$?W4ZNR?cGfnW}5y z(>Akj7g=pb{UA}I)dJo}q+DB^jm&20gA{H^fU2Wg>UoaPHoea4a}3AutzG7g@Z+Uy z>xHy6qe*s@TU%OHk5!(*jyUlB`^2uVAL7UXbIUN&Cg z+@&%;^uCy1z!5h*tx$Bv`E*+)4<`)6cCUh+&0+Jf>}9gpYhk1re-t&%jnpO-JV9%q z^)z<5ZbpS;xf5+`dxOTfwKLxXClNW@9qui$H+`h6md+OAM~A1 zj1!z4woyAJov7a&VI^B^J_vYa_~b%HHUM1_H2M5532&vyv?ykY&jXmF1$5($mzzV< z=GzKP{?x@-so}WJy9iPgQ~y^TEPLb6)g>u99$&Z@`4o9(L=z3c@%E_3YP{y1U-G$#h6 z9f0*V@mJ80F~Sq*vLkWHsQ*iIyB~-CV;cT=4Zz3$7NxXq`<42cED%f7suGGjhoh)} zi6`;PYA3WV5l;oI=ocJAR8BL)3adMdr^sGv;zx*R)~kXNCCBdrDwDF2KztkTv{@2? zg|R$Q;!d)S=#OOP=t68#-N#}9ECCXL+{I>DSx~LEEe>bK-Zo9L=xr*HdAj|TRr}xl z+S<{Z>A4ko#Ulr(CA;rb*m^NT;l4HZ+PIt&4GD)xX$}H-JGY`gf9oCi*IO2Z`}6M7 z$Jd#_+-o;(un0YEqZ3wZGQg;1;U1MBZ3xS2Y z)LMInDu-L|QzS7>ncg%)vOU>|0;Kz|#_!j}A9n9yw&?Lz+dv5DO%q*uLPIbvSo(Yy zudctlpbz9IStVODu-(J%JB`>gWCvJDx46NMw@4UQoD{dN2g{>BS2yekZS^cfbdTdx zS8c!DGZ^LY>OI*jKu1-%rY28bwStZgbczFMdtA{>I66TZ8sF{E<+At7uQ$n9c83e2 zteffH*pb@T=N}M!%t77Ds2*6&L+N8@xZaS$)*2+fzf9oY=`kQYPKY$2T=9|kt%0g| zsRg0n9f3o!ZkEw2C?oc?$LAuYGTTHGhS-I;Z0~sK!j`e;vW)rHRaEQ~)4R@kZ{wso z7Y#Y$0H2#EJTz*3q;i5Cm#arm(w$tM(99*LuDIEwF3x4Z%9m^4J)@(ZvHm(0*)fv& z5`OufbC`>SeUK8(2Hc*fHC66xR^8Z97rqzxSq<0-hQ`FT7#RIn0 ztZ8aeuA=O78IWLJEO1}MU#Q~rPte!WDTvhrAYfUVREYpJ>#`+mOvcBG1 ziaoy@itM9uHLF;4E-_coRx{(|DZwFY741qtTFZ+H;Zo4tvhAVS2ELU0u%j`5d?7YKz{;lWxmK9Rq&VE{W^}(2G+S5AnO}+M{A!(0kqZT9C?&QmW z`>F|Q%Hm3&G}{uLlpbWzq@62S> zc$U(S`1Mb_7sC4MJSK+h*edEo9A&nw%|vC1VtwseT%-;oM8d%hXhDLrCKFGWP~VkZ z19I?$M2`EXqH9}Q8(TBaB6&33ldT{HVyn zxv~;AjnP&cDIN>kD!!d!gCI(ZKYlPZ_bv}| z(%RXpoCzsGcify%!f$YpJb>e+#5yg-J@$X^wZ0|{%RdMVha9}Q{a9m!qTlv8_b_i; zvWSg2IpR~ZpwJMtU{i&i1lay$%Al^S9)C0#_43R}zQzu3A4{n+&1+;9i}W-7Gjhq> zeQ?JZpZpKV#)pA3xX+b)?BtR;r6opV@a@%bY-l!m*r_cr7hnq7OghX-oo`ITBGA=~P@i zHdj=sbZP9!3nt5h!}9>u_=$~>m=_0k>e$S^Joyj|M@LvgLo}f3K$fGh15_QIARG`l zRt-(!aVx9E>NsxS3)?B95ohw6@aA+st9X^$s8qwC?T5)K8^7aL^Rp7!49e|hup+QxISwm|YM4Qx+o;N}MRvFcuQ0Qo?tcZ|71SpcKd5t?Dw)+q!C0-=Yn^%*YY*% zsk#=QOc62dG1+(<-m8z~>B$1QZ{}E3>~U4$GlkHsNRPlsuZj7gYQ6}g2CNw!H*R_? zuizW2mZ8CiHU6ay;!&EPjaUW@C-?Y9RTf0DL>|wxsZK;3p-|w-(FDn2}@ zoWm;^CuCM4TRBdzLnbry4O%ej!;qI^2tMG5OAB2WGa7Ks_eqe4tE4rj#RO0;J#)*N zCSvI+wlck*8-ap@A%>BUt@tCr_}lj^xuvh>K0Gw)FDh9aaI|*H+DJWPy0Ux{1X@o9 z4e@Vn9`&ve>~YXd?r~fQL-~ASA4d01EN_EpqhcVwt~`l%R-Xh<6KjE{XZ#->+T%j^ zF?0hR?G>0qw(m4K@*UMCPRXO;U`Wtq=ae;Jfp~nBI+Y3*WhxLMd&<^*Y_X7zjQKRY zty@Bx2a!CTct~He@*Q8~zZDfO@G(#_5|pWmpL{DE5UoUPF#`OV2B5l{_x zdn;s4YHAs~?%%^+6}iib+ed{z;crpc7G7mV(fNYyei*XuwXP-dH5Rwh9oG;HSmRM8 za8B$B4m!A!(I+b66@mqO`Bzg5;k43*#a*V{PiRn*a3h6`u&r20*!UtG2yWDIo2PM&V8t&)SAN|Ba)9I7$U=0i_&X|*Ka)KHVflBe zntz4P=7;ls%V)BHem_k>MB?-b$XUOFHlcxgng9&k{Fhd#KW3o+UGIOmw%=xTf66Bp zS0t}XAF$incqa-L1?;xE8ru&pN5TNRt$rHE0>Ex-cgPT`#=Lmo9bel)st2P#5!0s} z@hu#i%UzldS}o~fxd!$USzTSndEOLA>_e^sS9#yXcIMUKd_&Iq)%@VZC|ckWz1yMe z)O;4I!YIg-P$%c()$Qq4URRt1IL?BIuz+mhr+R4S@SZHhEdDK>KUSiPxVCeu*Ve>e zA9CpDJnGiW9)w>vG4*@>Nk!A<#61IL(On4NBvNPy?qCt4J;xmo2HIA%sL^wTh?3u> z`H7IEEIa(4bl-UzP~s{zPJb48x?8q8}BayoB>`S2`c56)xD@5))b564Qa>qfA9l^}LGDyE$mk+};PknIst009yac+(WRL^U2UPc2_%__9;ru%JN7=kG)@9%R z2S<+DbTNFcx%t9A@DPDLx7*hX>L1pZta0MA$T9TgwC!*+J3iW*8oKX@iC1;EP?gO9 z5}OQkRrsK(bFDHahgEahGe>IQ>G){|1lyuy2vd*cdARmtp0ZG3hxMwZxFWFF(tkSO z`R#-D@2;_aJJ)X>weREU|2x6YZ*S?h-*q~w8m5mwWnm6pj2IN>l09)HPYTB35hg=9 zuAp8`zeuLUyjqqxeh%~s2R4}ylS6qA&T}6?!5YN`Zn5{F+7w9(7s%zeR8R~VE)VIX z%BL(%S^-biR9tXEFQxaCT9z>HH8lZEN;4+~y*fcwkwdFU$3*2apa&z+T>`(2_AI%n z^;V7Ri(=JeNR;B{y#1_-|0MtG?}W8K4CDi1m1qFo{6`qI{oB9pM1S}C4IBMUXz-iu z_kUWo{JTp7VyG)8lB*5X%a&hn!Vlr(-~B)T`gd>yT%uPA5WGE~8R+Hua$Dv?m7)Fx z9{WZ!=q6*VRf6F@DB!bED=$E%!h|`cLleTqDJIy~uHM0kddmrKJ81a&54qDA9pY(; zxcGiaSp_#fl<#{R0}2{vaDCnC1bG-P!feEVgd*ba5B5-p8gTjUJ}un`<*{>%GaGiM!Rv6;OfXFYA8IOn?1DY=M-Ej>N2Z6rkh*^QrT_Znx4!7Ry;uHRFuwBU+15FK&wRJm zpaAb>IAc^|LNn!JXnfr3(_78V-P@T1Q7SJ=mvuRwsyjX=alYPzA7(N}E03GbA#N(S z2am5sTA0v#MBdsmEy`K2yfUz{Y^c}A_A%CM9?40>O_iwR&+!EQEH?79fA^a;-l*^M zWp0Kw!^Y(zaaz@y|I{G8%n-;b0SpO7%;J8JrfBUt@9YE5L)B83>$>7w<@G`8k)%h5 z0=Kya2SuZ+{@k?_jU#IMGf6^fH(F;ZE-D3GLI^bu?+x=v7hj;M5EAgn$^c*XyfHH( zOd_Kq<3bFYn(pz?@SgVG74bcD=(`TDBGG=~jo=NfV&wip`1LjI=?4?y)C*F8dcjv& z?5I(IxOgjhtPHbWB8+!2HicCWzUb2eO(9SMC=Pw9(DCD#k(L$_%kaG!xqu!2wsT{i zH=_ge?gY}s=o5eJtDAz92dL^zB0bhSCOKEz>Kw2S+P#vI7J6RQV+?2G=gQasQ$Ge; znNg+yQ$@Fxv5zMHu{60i2x;-3U`zahd;QyZoxss`+B<5d*w$M+T0$mgM4dBz zQEdLwfLr1ws7l7g{X#kX3}&E;@j*X5icyaH^7;H-ZD|<^9{!$-{ID;ooUG27A7;AA);{pK|!0r03RAj3Rz29wr#G@DtlkopI)qQB3Yqc zP?uq)HiwQlS#o%M6=ji2nu2FfJD&Kf*PH}#k5JfiVp$;(N zFdLv-msk>C7+k-C`ak8vS1z866MO~Du>jzS3_leLV6o*3tvpB72l^fr=HmojkvR8^ zpOFS0DgsSo;s9Cq_XqfEiSqry(N#G0MRMa;(3(eJ_bvih1Mc8nDHsOAfxMmxJo*69 za8M5;FSdTiUKqWz*C;pj!0PEPW;&WgR+##@6}V2$53ULoFBHAMVxqUCjC%rFR>V*0SBqxYWWt0{ptGnzXF{YpXIZc!296cEOX{u z*An&IAEdbTfp6WiwNK9$+KxU_|WjNUAbjY^uyB7;e-!6dDq(k1{c#N8~TMyzjI2=H~UB-Yh+q@*KO|B)7=sh)Ov zL}HZd8u(TM^rHr|`CInF%bnQryyFak(9l`)H8R6p%b`E!IIDJd;5c|tY8d(5Z+@Z= zXWQH5C~;|##}2NeAJRmUG#2in!7H-a{p)lapYagm?7i(iXLo{Uyil()^h87f#p(V# z0~BaD#yDCnP`l?r?>rX2A@<9TvT+ zD7bHxMk75;dgwkx>n2YMw5cZAI&+N>oRT-7zAqq^S0%`|sK zGGOjv$o@}25&xD9QPX`Kkf}6~5DZ}9)A~Y^LS8YEL7sGi+%s4A+gU%(5&#V;DS6KO zNIo)!&Gg^Wr8Do6N_G=?mG?B9JY`py#vQ;c3_GH({y*m40xGJuZ5tj+q`O;6X^;*9 zkrq%yIt8S=LxwKNK|pDxq$P*$?(Q5)qy~_Z@Zb2nKF{razjwUfdcS{t%Y_58*07n` zdtcXi9_JBsK>i80(p6r81jmQvnTM_NeMIw_hU<>p{?qz0asx&^E9n$xr7P7__43Ri zg{S-E`ho2VBo=dxYlphTZ6I_i-za5S`IRQ5Gpp7IM#Eo2*%{*Ka{A=yP|5W66m8BW z=Tc0}$TG04x39$y#9EmGGGteFIUX6#23Os;7s7Z0Rqw4$T|@2t1cKZTV2o~!6ozT; zi|XYqxqrA~yiaeS(1-k3vlOoxd*a!lf39O{CJ6h6;_>%bmo9YyJY6DnkCT_A4nCR3 zVsoJ%d;cLf6ydSu!<}w*x`z;rk84vNKyy7m3yr>{1oV<7J{)6h9|dYHJxI*6gJCDj zxSJcTfnB#1@lqm(aJ(PFr^4*8g5tk};`4IxElwRO2r`!wlnQTc=oQu3?iiOSd||~g zfV+i=sz%gKMq9Y9BYN~Fn?A>?Mv1lmaz71$N+mA?tCw|f8E*IblF^Qk8s~t`ZPL=8 z+X%)rTBvL6?AVC}Ld*|6r~o<_wV2E2 zBWT3PbQHqpK~&s(+^cd`G8pG0)s2U3P3IU2NBD&36sVU#p>k8!9x4=N(T~~h%bZ=k75Wv zp-52^#>mkc`a1ah&USM7=<+#yD)iE3S;=Ovv9$`%65kR}nc|AHf)vEhb$4`a!0gGw*9-E>9he@>x>JWj zi^~fK%Uw|po?X9{(?*xJ)t>0))NPWs@!0WDXbs{B(B1nb$aoR@wysge>y;d^OAoL& zM0)sT;Gfmk6M9Fb8P+S{!w?1~Q>=S8&4bR?4Q}EUAH7fNOSCCS2=FF?E~^5@A#Hj9-Y|2_Qk<7W*4SPA9lMm#ylH!WbmB z@N~#bA7b<1FeA|;K@e7#;U!^>45THOcDzaI!QIqYh@I*(F&@bwvteIc^Wj`Fj-pk_ zxL8oU)bwpR>re+T?-j$RtTcN7JJAu9Op-ZgeR zuit6vYRQzsKPkwwy#ra#${|P~Cs=;%y^E9%Pmko~B;OkF_^@!^s6HafkqxshopkHYGP@dq z9*hRl-QWTpFJ4KuouPAWmka}>Dup7NH4Fe`->9Wa`JkSQ||?@&ycxEBxY1(OOR2t858v%(t-Am>lK(g{f1TvE(DWZX8|sNgb=dJP62j48h9B0~9`W4+(8+9l5Ivz=O*(uAnrtDu9J){B_ALzR1=5 z5!6RGVzmz0Z zMva+FD_>ex;!r?l6~vdnXaWlvi>T&M;1R>dv8 zu9`JbofGJ=P({)x)!m1@%`yM5-7pZNIt3!%MVf(Ka!p`^)aQ1g zb}r+AL5P*q$u^&Y4QeUR;F8izz-f+>&=5$l#k= zn7M%JF{H*JKm1qc~V>@nuJ5p@GXF!uC zzS2bLZq>19IDG8Ru@=qvzD<@EiOePn$>!X5A;M%;sMx;-OmHa*hra%heDO@)cR{hI zA?T4)Fh-^FL7$;9LRmd#KzN%lYV;+yeRRh5!C8o6BxzN8QHn0Llo1D!<6xIN3OxTd zGNlQ~IqfeBtje~tM3_&a%St+~J6OB4ahR`$w;)K-=AkIV;ENbM!T^@-8aYhYz3w@P zgI<@PI8en|UAL$=eY0I4dqdw%kIzNJVk?HhPiXH! zAE58HVi2HDS`Tkb^@7>(crDoO(K&gEwI6lRL8R&niob*E>#E|$<9pVIQ462m@K?|k z0lWia>0AWBJG$-ypaCO*R0Qo`rrsbGZvYm{jYg>za)VbqJiS()2Oso`ugH7ffCgTG zc+*z=Y#p$Nxfz$jvl^+v!wug-aZs8|$LKRv103q6E{Y~OG1h+@2$KNk*c&gn09PlrEWPc zvR`vm)C?(n9pHoidT^Rw4|`(^3BVJ-9$fbF9%o4US5ENSF2Me#yxAlG81`Da9Z(|vyU!yYD6en%P-x!}Dn9^z|3EhexpukH+%r7}D3uw&?%Aj| zdW{CenpROc(I9-o-<#!U>`5?jfS7&H(n zL(DDt5`kCX4ln`LGCWQ`wOT<8lyQ;M+jGMv8C_&4uCpGBV^katN= z+!=Q6)8Z?^AN*Wu;ksMlJ0p_zm`H+U%{!-@kC>fswT zH7KFBpE#ORGpIe8Jk^vd)~tCvS0gUSCP;O3mQxQD!^?N9{x2mUa=4_zC15oA%!%em35Fc9ZxigiI}oOo^xQ z@9GHgPmOduF0c6}F7?11iOZz)R^}X74X2?TPx$I-n4A#zZkeIZ**@H4SqV%h@FpvC z&lwc@I=BBo#5~z2#BncSY9K~tBXT4F)5Mn1vEWn&<_s7iyrNM{t$%RRH0?PMJm)sD zhfdEyAYRkb9_x#Z4y3`Og79K6s0o0iy%n3OV69^+TNq>1(ewJ)tR!!6z4F8|R%ThT zB3{y$8;~g1IIrcSu0hM~S0@5AFO%(?m)5gVJR{3ESCeY=oeIx%H0m0gQglQo9j#=x zieg$jVf1E%lKy>%RK>aL-p<`?n7*ET&k|LkAsUrqG;XsTMhWI{!8tOC8F4L=;}X$v zZ=s^wyl>fgt-{8XiOc5ZA5Sh>s3uaZMW%y$ML!`1F0TdCIy_Hoxu#1Z3KtX9`ht~*d|hWK3??I=FZWxx`h)jIeiOP%+!1ydl5LR zUm0Ad$1D51R%uo9JY0n;ak}0r~RK5h3g;^g+C%Zx? zoDKSLUWsAPF(1NeYd)wG#-)LNKMI7JrRYG((?Ss@@OYiT+PWDxyYa;>LPH?f%w9N# zMf<3yp3cE&wa#lnL`AdWZNF;wmoGf9#bc^&W+!f*hZvM`u~VqHmpmhD;X_grv|}_X zRW|5%bKRVoPSo+rdV*dGJ|V@LrWGUwz3>%T&`^brRJ=5tCzQis(_{vJxp`@jCp*zv-m;DMew()c(w6kG+@*+>sfp=3 z(yL-lQQ~LSzK+H$fGGS6_KMa}?))}+=9aeFH|Ny`1nXRERJZZw`p-D zk#Rkc$=951(2rC`Iae#7<2wwsX_)QmBM&*&wJ@y+P;_$~=LN_>F$g=>JG);mC5SRKeX`KH`| zwK~V#&ww!LuF)mT`5K~DnxN=)Mm4&sT!$#~5uNKhiOrpi0jIM*`kDVGY_Z%l@9br-(w)u`reJZxzDAK?y3(ozd}+3j z4Kg3DLlBW4Cc@5}ANzLUcTnDGm-w0BchJtw=C1V}^lU#03y~kdD*vG*K4ADD1tFt0>{fYV>UiNWB^FIyMa~G@ zrdOYExm~=M3AHjuChjC%l`%CFZ_~$aQyFYjG-@f^q z?f$p#`)5t?i%d)xa3S;N^H|=MZG|TIa3APx&els>nDGkCzJu^bhOwdh-cQYN>O_P2 ziuwO`X8-ehpvd??;fofxX$^mE!rdA&Dwvk^vYZiNdc4bkSEKUWnjn~zUX*931)F z=HkAqlAHhsbBPy7!o~WuPNaG>Y?|{6kl?{H!Aa)iVzn*&z98~gR_)Q zJaj~$W*~nZk3M6EtrQ0>JBI_2)~9+(4n)xrI~r_ha>Q=FHpr=d3PdQHD0+j6I2oX? z;jGakjZE%V8spCt6JW1cHLQ_xEkE&9^a7~o#@e6i6+!5ylKiFS|NwO;2jvGNUUx-0bWp z+XkR6@><4P*i)TjhtS8HNP?YcUC}G>X!$AY;2cHE6B<*fx4UG)NUPnR0%vNE358CDX>y!buOWE;yN&AFrRQSMR zF7n4Y7q8*TD#dQS=1&W>>I^)QMTeATA8WDz3;C*a53%D*g6*jz_^Ic-|5opc1S!28 zg$%X;W-JgQ(gB!Ho;l>X`>*VYI>4+Nl+m8LSrAJXStml~!GKh6hIizPhbtw=bcB%|74&or&uIf=yPvfClF9JMQitigEemQG z97Km*T5rh8+hWJ(#LwQ#`VeCEsDws3B%7rG}^$Rcum&|sBWdvGgzrO)P0 zOEG<2U4#}#R{5jBrKY!!4Eh%lFX!O=(1jg9(|8!ZTrXbuWvY>Jtf*t$!uXEm02^jB zy-@I1nJf_@+Ld5b2_8`ALZwk%Q`tdd^>8wh+p9vUTq!5Ma+*8bCgsa3Ek>P){!RCZ z_k96VbPci<147G1y2%`9EZd99N~rqV4?-R$_jRS;hN+MLHCp&!j*vwU4?kTYPOL7P9)r(C*>fTpDAXk^R zOO3|r*Dc@~epL^R+2eU(>H3I_5AUKVe4K*rQko#j$cV{0=-XWYL~)?oPJnf_)`Bk( zd?p;5Uh}+-t|EUha}Kn(t(|~>ZBZfH>WzP`oRL!dt~BUw&)PD=WO%9cf9_I%SNY!p z?ZERyaj;0n<0bSjwYvIG(!NZ}tynk`F|fFD)8cbY3n9_pP$M1FnBX;5OU9a`3iJ!A>~#{CnO zVkf=lga~0PuUJmy#x{cwpSaEQ#x$4LZ`+WeBfpSEcxyt?O}gOMOa%R9==AI_$;`eS z!0Ka1Gxzyh*a+`ywBsc_8n*ACm$Pdg=IvOAJ^rO1ySJ~_89;8B;9Ud4%ZKTG!-3b- z@ZP>ECDrGwZeR>1C)iDY@3 zMrkz!0}SdXr~)~cV$~Csq%KXNB=%O?Z}cr&UW8%=|nb-Uk=5%@mn}4)U613CkOKhc3v(E zBrq`W+jG~&hzF*D003jPo(2~AOgCxh;-Ls{)Tu(q(=eL}zi5XRb%xZ1C}1Qpq+OG( zyU>?^*2?YJ-r^Z%w_!TTHY+iW5!<=Jj6}xpZ5UNi+5vDk+{b~UOYyn@>GABt1TPW^HjY>vx>&V0)?j?&YElmE3eo$k z7q5(;k~5VUJqQB4#A$Zs3e&j>V;k>r5lah-iPyFx-ddejrwJFOznSfOVZG=T@{`fb zMF(d40ks01YU>))tH^jAAls+?=%m$(p99u(=rsOH!;8SKf3k&E<~Y{SOBO%E@)NgDEV;l!L*D)bhui5gUb zh|UtVyezAW4HbG?eLEcq6oC`3v931*`l`Sh2cL?rSn#}lMZI^AHyT$*FzOY@!ML2x zOV|rqm|8Of*N%W8;{D101bxXW{8qyRc$57pi*Ohvd=2o-+ZB*(3XrV`S2AcP;V=$R zEO+6#HB(I{FBrxp$>v2hO-t#t590(RDzZ?x`wxiH5a&<=3*+P#RIi zUuJmn5A3AOkrKx)0UMDd7?m-Ms^1M_9l0`G)W;s56)xY0)vj|egre~_~Ri{~%))qnIj`{#z% z|3%>>^IQu4YJfL)PCl4u;q6C-JAnH#f~(Ei_MTj^So25j7O59U=*Oeh{VCH7s=dhyxrdGZE*|%n5KnxUUg|A zx_wLHrfLTTUOd|^v^`meoG-Js<+fAg`tL1XA=b!1CQaj|3@P!mB9;T99W+=2CVU7! z6Et*e_@DZaPcSMqS&cH8%kVGXiRi*#>hvrkWavLNR;xnCr72Fr+92rL9& zc7M)_jlo9i05pQ(SqDFM0s)s)>TAX)tYI9X3{aJFL43{=dX*trBQxb{`s-m^ux zEi*|Pw|`6Z_7^ry?|UQQO7SXDX$Lo$-jj@5dQSt|LpJ8=1F`Z0Yv{0->y4@8kiqbB zK#zTGfOO`0(Vpy)uqtj`_1Hy)|H*yYUaAad>&<|t>4;~^?NBuF2=C{#6?glXqCeO| zp@woO`VZZ@Bk~5p$Q8W z7tfGK&ks9|^h%hP=ej#FY(Z;Bu}P+TAz5FX&`Q46vDQ5FFxrI5lX5sg8JveKo1{T% zN}MY4bg^vg&zC(If&y7EZRi+#Q|vY4Ai`@KkX89H+tuo*Rh7ylkwkpuvWJC?h9J!5 z=(oQ4JYEP}8h10$PfmzkZE9ynZ&UQil@l5%cMMLfewv@ADtl$w`2M{Gc|~kbo`t$V z327_eK&_m{XH81=AVP%4TjUmv;a|5-g7+9_D6bXHLmRj#$dKZ?8g9}Zg4W$D3J5Js zw6aX05_%I*R%#)``G9e1%PT^_k&Hs|dKmaOP<~;%4BSSCB>nS*#cswzvvNc4@1!{w z3C>)_Olv3G(eEs(Kv&n3VRY-0RdbGfOdXV<^ zGC$$<%3EHqxz}SJE*>sCxBH=pe6zlBzVwguwD%8xkD2dEpb)YlX0c05a(MRzC z8ODYnR1iYv-9L#&2CllJ^9O7I)M5UG*cAvy)qj72QNy;8)TM&owL#YgYPk+cp~UO> zu_m?-ou`6#tQrBr_gxTx4gpK6DhYrNsQ^!m0DMxF>nzS2U5q!V%*#f3@)GNXE z$nHT+TY4aU2JP!=(KEPR$d0#9j61Zwb|5z_H*zx=L(aG(GK2l}k@EZ3tCZb5lKR*a zmd6S!m(uWK=5dn)oFH2OaPwBU6RQqGHegySoqzOMEu?2?Ai`gYYEBJ=4GUlSYDZJ= z+3N1qaY8d)JJw5E8DnduB(aNJ!Gq@qk?roHcPv)KM|Jo&^&d0g?q<+R5+8(FwK$50 z6iAG%&gZYa6={4&-Yjccr{09n+$I)1rZB@Fl~lv%>kE;jbv_jmD_0WXqF$*N-+a;w z=C-b%MD~xd72L9SGm^XS&@%KG(`3BrV6l!k-lFJ>=c{^cwhxP1whuw{@)QxfIHA*1gR0Q|@JOK{ET~?!IXSbg z5|4(5EGTn+2YuWoC0FuAdSt)us)^@)eE+v*VveCJ7>mr9iwdEkqm<%L<Jqo$zxW|A`1x>nI=|Qc~UnRM9|iRqlzUXj(e8Bf8o`hb9bVcS^Cf> zJhfTEPvSA9_jC{Kr@0zG&^$b$<`s++r{m)nVWP#?_ItfH98sDeds2IBoC}5A5@f^F z-v3mX*x(Ky?9odjGdrU1pmdau!VgwDbFzn~1K zZA=C4^N|A#O2bk!sb7wqWOz~))@#8c`ci^UY!js zzw%}r$`cfnnoVSU#MCw#g)^sDFp)XHQeAA^GZn_PZNo@nQxwKoUDE4tulc!9H!ZHB zZH0XH2*iFcfnN}E3PPM7%Aa=lKWnuAhbriQSLw!Q zA+5C};X570H93-A1cw)8`0_gsGNBjVU1 z8M7d#!yT=VeSAK>IyJNSwad=TXjuLhC$By~d@p$FP(*i2mYG!p1SZl1JQBtW-*5#T<22T`NE{_N`CZ>|VFF+sU=G`UP` z6aH}3yhYbW6oSUo;Mmy;uuY=|TSo!5=}{ReJ-{}F*k=N6Oqb?JLu0MiVh3vKFtP@q!y;Sl(@M>_f&M##_N=5O9SAeGcE z+5jLe*{5TL%uS8K^e0~CbUfwV$`if*fD0dclxu(lHSTPpzcqHvlfyjKxg^#1&+OH^S9#X(38OQ&*O1_uodVZP$%=JAu=@Jg5}@4JbE^*+xaYsh zV)Ak`p-c3W5Ym=-h`_gr&48li1ER=<;v&OcQ+r#r>&qNatY%z25j|%LF;*0}1eNJ; z*RZKTDfL(ZSCQYw8U8azgIhl3@3(yD#?sm}KKY+al({~BH!DZ)n3g}yjL9YA z&exJ%5`Im7htXf}EVs3k;YpW8n_e6QR;-QVyS>Gbk=-@?5Cpck`R9WUFUa!q3h=iu1 z$D9kDm&V8$*ID|h$eqRUr{)8!=5K>y1kaZ4hF_XGUu5gz8pjTyT4CelGH~ApiQxmo zIP`&B&8By?gY)Nc>}?y2=h=F7x(9t&VFBYd)c*M;A-ud7G7A*S20*1+(vgoNka9Wi zJ%2RhIrhPqE^2a-BCPn1KSPo?nTZ-gR&(>bXA1T$%c9PpEWJ7Q%E5SJUJQDLQhD2m z_tTJm1X952-3wy%FdQ^shx56_5z3jlwJW^So->1imF|>cw%**8Ye@?~d2NF{ESb4= zAjNZgJxavnd8{M@D@cg!rQn0y$%>B|=<1#i5_1Xpg>6#vq)nxe(UYZcH8HUfShBwD zQpdagkf#GzHJ-P^;c=L#!1Av61-RW}1ISRe~MFlp27h=cTuju(`#^cLK&CD5mZ4JQLW6aF{rUz(5%CbK7ShRj232-HB zT)RU^pMR)KO4ZvOse*keCdWs5DFrKN+TF*0o2g!%O43us;)Z~Tg23f)WX#~-X1jq} zoq>5p0ffp#J9s0LMyme#6zN+dh zHGe+&7Wp1;LE`CatTMwClQe7@Wm#H0!1#>Xj^wqsJCvHJ5X4>Ju*hjsXa`V({fr0_hfuw=arAM~(?TSP&CzH{N-mbv;noeK-ll$HNOjk~cx1_so)avtk)>snSoHg%x00jhyinMJZ^GleU#;1n9Cr+3~W24GcSh%ORjA&F30j@<=y%T{|;OV3^k`_ zHB*#1n)dS@Y=v9EQut3;Y2hGX5xMsr)F1yHWULo`4fxUPgAY`$O-e4vcfco}AYc~& zdePs10hjFWm4knH>R&Gv{oC33U)US~FaBvVP&~PI-nJuAhXfT6e2*#jK{S)Lw{d=Qq z!@P0*%L~bjxY~W(9{!0e?+Fu{)qBW5dB|&cQfJAJ!+SW^1!5WEJn*_01Gm|tp{~St zlIMJtCFlb_Tj8|(P&s9t*^aiU)K^2t_5;hMP2JWW6Lh~5K*J1mYM5enRaVir)cSgy zE*X3azE^)OJ$RO&r$qjRe-`WxdDpY2fzpK>Vf+zQ(FlAi+h%hvY=TFFpc(#&enR&a zO}uuY3Px8!NA6&4xw6VE#ZK#brbPlJ2Xe6Zo2uP;fA>y#p`zNqB^?5%f0V14~RG_Cq|8@joG;N{n51{dV}O(zO}asmDRg1Y(nA1Do3 zl>|DzgD|PE-%R=3g}-%=-^pg@cyC_kO2Z7gB^f9J1`*nN?ul~3c1b#gwGBR>miD8w zDk2y%`ldvdF*)Y4EOwD0M_D;mH-J|LqTYMXb_TdGhj>fqa*wlVAU8f6h%w{YRlXToH<;Ef^#{wr3$vG*u! z00TBZK^0Zwl?9&x!<@jyX2389G2dVFSP%q9`;6t}bLT8D#*)X~tF)KLte}w%C}!C&IhVIXo2I=Q6!^iHf!QSU-b;rgI?<2*);;iV6ORxE&I+1m z0mc(^WB3i@iF)a2#T|h0#BEgdm>?pqrMstl4N(vrl154WV1&1X%zgAk$#o~|U`}gZ zihQ=Yu9sk)#dpv(uKI+v^SP4eI#9^8aGo;=lU&tiRuo2{2#>*!d;Qr9GA--XJ@gFr zw`pUrgTq^E4(^pzRzbF9$M4kC)?Dy4yyGHcvVB?~3;Q}WnJ=;< zZY=<-tn5$xw|-e2e;C<>gO3=&(^JL(xb`Prng2Ka1Ad&ln?~zD z23)|W`yVUMe?va{`u3E{6UAM;BRWjo%@ZfQp5)`! z(X{1oA1PF%y>woNF>C^gQ``T}Cd&{Zat2lxi3;mXgbk5PXSh2KdTqOeZPboap5Ma< zRkK70Jkax}4C;CsJMfz^579;%@j*hVhs}y2ySj^T{CpViPU%My*HNt@9xl@55Q(A) z$F#`s28+UN`nFl-u=NRYwej3ae2Yc37YvFaYUqI+S6lV?iny*&BvH`)pPR(ZVCfg% zy+76`Kb1+q1WM~W=zhsnq}$R3qsH05%Ef?GtCapI2Xp1bSN2n`0puJ3-1zohQm(FE zBr}Ms4UK0#Qk!to>w1Gi9CYpN`)$}YALX6A)&iAwzIbn1+sxM?5l}x&F~o%VKIr;T zFs(TU*b`I+rT75$1oD{ossVcf6f)5iknqGm+n9e+%5(F#)P*+^9dayPN9JednR;IM zM2NqoUG^73M&y#V2j7@{c!|GzPj)G_l91?}q+6)H*GlyG#llRJdnTof$BM-IyPJ6x zrZ~n~o%BIeLgF{E9Vf)_(Pm9FLuPvGu#`L?7q?TAK571@lmS5}`3Q=(NRB@1-=WsL z9y(Nx2%ie;&9Nqa)|gP{nAX{ ziXFBrYHHmMhRAL7RS`d2tdhE`xb2h)0K4DPsFg_<6@UaZu4DzHq;Be%cR!)W7sPC% zO_S){!3Gu8{`ocj^I8WU5rtMQiq^xJAgInPWu1g&_z&Rox4XBy>Bq^>k2(wp7B`=c z@NyDqUIl23h$^BCtW6$3XOeFB`5Ykm9K|&I1;loX?$St2`CSj|qa=NSwYASaPl;1u z&5ob9f25Tf)~*ty3sLEo7e5-1$iQB0z7}gd3xQ$u6$y_ja$-*3`3P~ox*FfXR!e_} zcvpx{c_(zxzQbySH5pD5L{sz-X<)AIhBH z2SEJah=gEC%LZ(KPqv*t&Uaba>e8hdB=9oe3p71KB}EBEjuDQ*>iugs3lKof{kbH& zCU_P_y?1ayN$q#TvAovZ;CyH79rNVVs(Tk}O0kT@{QZNDfu)H0`>=vkB0eg47xO0; z@@w_MQn)TAa`F_d+~{f?JUE%SJ|%zmX#sh8|6OJEhv$lE+M5U~*s%{P3Q*E@rG$Tgl~OiTOhDASHZlV46aG&c-3Wa({%t=~dG1i9vf)y)~N9k$6B{ zO*8^9to95d>Nzup3519~w?9#b@@QMe)-fd6eDe^v-K#@yA@~9j|6!cp6cuaXzY-|l z*5A);{U(wP2^q=c*5p57BIG_dF%##S>l#ScuOFj@7f0H7Cpu_XyRTVfMhwvf3kdbG zNmsEtdZ6PlPga@#Ps-t;DHrh_6&y=XXx!$j;8C4)R@a3K+^YHIRl!bstB_9zOHs#S z!n4;yE4lBiT=11JfL*p(fF7i_Tr49=W%cpz?P>6rLcw1jiyt=GT@!*^bq;W@!$Ph# zb^%1_=eSPMhweLQgzGz~_p0gaDKO5mYZw2SQ;lc~>BVb5h>FjGP%*6jKb_$Jz;5n$)xa-KxBr92?fACj*$owJ@g`*)DqfztZMYxuXeT^yQsgh^LZ zea1*14XP#V_!_MdqN=TVOomal{z)wC*I`kO11$Wb_C%-;`N+bZv@!Xy9KZKVc00!+g8OV7%XO7;B2500clgmzF$D!_qg%7mZvV(*2^Y$UN0+AzC)1N_K?dfV z5gNdIM^b}Vu5VPrzX9Xpedc#;NhnhT#v-2#su2wCHR=YWRqN)PJVDKRs+%5M|0)Nd z2!B3xkk4}WpJ{j6a*O)#a87^#4;4QLfiA?T4uqFh_A7Al2ZZ8B`Ti@8@jnDDMAKUr z8P0NH``Fr633zAxB05DE_%5~B{B?BQ$Wi%TlU>*e#~hJpbl&3GiB zv~&h{01bH_1&|j5fZ;z@p4Uyl6kp(42^ip$fUmRcE|Y)WJsiM1UT*1JK=$9i`{xn> z6z|9M;I##s0{~b5A_KJgKTN6G_phlgCC9xz%vRA>;Tk7F`5<}>hjqDLQr2o#$aZW% z8b5QkOZu-b0<;+aKmB<d`v`%FKrE;9Y9CZ zxdil!`&Ym_+6>JUa)J*M*K&qoB`^a>lo``id z5XHHkHZj3O{LQAlH=?^3!Q^4hY_I2OzS}j`g-W#ZfR;^9m7;~uXbZduNUB6h6z4|R zY-#CQ*TTtM&^fEqZAzA&DpuXtyH~U}cKj6iae*B^)yUNcF6A|&qkG+5PbH{$uNlYK zkI~o^=X*pS=W)iG@%9mLA)`7!%(s_Zi@MEMqAOFE`z3u=!%Z9eUEZvv>YF=zA|HMF z?xTRnKA811Q^>0=_;Ps~LxD|;HROP^gB?Sv-iD>cY>IEJ=9$tMZtuk#eyP})>bp|d znl7`_AUC2L`Ef$f`$hhE!}9HIjroXf7T}~=dY!Hhw{eev=>qTF}}>E z(96zMV9LAfAVDdbMshF|bqZ`V8u`F~Vwv(a;4T(oAt%Nnx3|)ORAM2!c30J%vrq}i8Lps zndefr2qwD15u3iU&%RhCU-upi1qzLn+>{JssF&4W%5U!zT=hlRc$X84bfLdjzq#1g zFU3}`Fxmw|l;xSnj-4;Q>1X3XYjpkF*2KNmDH>JnZ!?n>e<%eyB0Y=_*B4W|W3&yC zJ}dT8T@bX{Rm_^%`~LSMs96*L_Fy+U)-{V@dV{;u;AgvwHGKw@(-eJY``**^ z(NASE~udoL#Ap4U&P!oa)tKME?^u`AnPRka+YAe@%>tWtkP@Z9Xw#!6l&>Nt?JEUPFs zjLF^WG(n%U-^rlNhsfYil0$Q>zS$OYV8yoyLA#Pl2s$O`S(>?!^C*MKP)qr`-2vHn zqV}y^8w*zs`sa<+rR8|Y;0G%V$h<+HGPncT_K_7|}6HJ1_DJ5Dv;k4b;H8qDR)7ADvr74?UVNSoLaCvcm26Z8* zP*DY0-o`j@jv`VB)`%Bf1~!Av(6h8B;J#DopCM!EeehhcdjnA4w_PLy(Gqi8_*TWk zStL&s7=yCZ2`K$DdSZ-eSco}&UwLF;2ZIUCPflA06?2Zf*P(2mio$NcTtfk z3FbY=Z(KG9UaCiRt}Fyv-NnJ^YP{daSn1nkNI87;_Be34Jk^R;9NV>~4e6s~$S&XMv-2^`Y z5h8360s?{s@Usz7=`695ectiP+S+l$+hl1B?j(*WX7@G{n!g}WIXvgRa(gz#JhnqS z-jCt-An#s7u;5ID`l-y_5rKOuwc9 zag>A{DTQyGMKcoXuyfJuA3D*W5Y2BN0W|!#bz$+L(8{|Fx$MYAk{Ol)#HUrkZ2*ZT z7qt|%!5)m#Gq#`&~GR6zYeSEmn1X7^i#@B_*0Cb+W5c!QRI?1F!L z%)5VHQVt`>1K=$nR}EBUXiQLieZGLdq+EfhnAcbOv`RD}BA}sq zkF%LeW^!1U!fPK_H^a*P!Q%~-dCWnas`;S7=WSR+XlE44@T%=Lwr^2T2mCIN{6)D@ zr3d1R%$D$5BQqL|p#vZ%vzA8sg9F8M%7vaBi~Jpuwpr&T){ikf$zhUr9zI;fh_F|C z&&E3PSAI%7~s9)1k_@sJDz|(uyr6@wfaZZ-< zWQYxNu1dP_8jX-iO9u(76USRaMU!>K#s3gyMP;B`8VoZQo&3Zr!|y8eLLif8Pc*eq zh~bzQK}IE%+(iA_X;%ME3uM2H9<*Z%_MKw1R{<@KEhdO-~ytv4k{L!?kF6|KG&0Q1OcMJ30y=o=< zq=tE`RGpT<(&ty?a&OY(SnrY2x)g<6njFWAH+>Ywea&=(_8CZiF%A-B2bYga!}|ut zc`uSJ`V%FjHl^Yy@!$5XXMqm6E4*h=5qx{*CXTo6RE&bl0(piC21~Vj_`NHiQ7Gv7 zw$R?8=HapD5uR{?E2Z2I+jYU%wwTmfU{JXiUO4IzO-;>%zTb!G-^Pqesh*bme(uX8 z@yQf{a+ywTA6EZkV$HRuzRRm*fB@9cUxr(Ph;e-XtK0`Yt(9?Gj$GVXsoXhIeVN&Q z8HP7eM4D@#NN?pqqbo{n%@nO>?u64kIEW{?!V>L@c*oNWAL+iWw`8C8cA&?b%a-o- zuJB^6*f>Gh?%q-l{=*h6pC_e(EgFW7xkBHb^TV+dZ2HlhyeL)M*7R9wzk_m^-yY&I zsikSlqp?EwW5-BAkfqKlC?wRTqCr7RALdS{RC@1i>kgzW!Ja=42_oYL66z+Urasun zbjo-4nR=#ZSNesVy|N!lr|_xvjY2cEYalDq$##@Sap)O;k8vgODRlC1PQ`^qg&@OQ zS8h};ghgglCjulhGU$=N1TW#09hFJuu1%WP($>Nn^=)CFnIRTtgC( zKMNh*nsTenPMfgN*-__isl<>OW*B)#F8dtwd6c89AjqtEF?$t5i7!W^wBhZXEM}tq zlZbP~XF*trWTG^6b;o27JM=kG@<1+q-8sgq?&_2~50|gE?o1P{8bR^$wo*>Jh=wd1 z5t2M?3M)Rt0#xK|Y}QjtvX7_*by&!0>-2oPF*RM*DA7UtpZ{wd{`;9|`axpXE{61x z^`#ieNyFi~_fC9OWTR2)f=LUyeYHAjNO8cb9S~jMcD(?^&wdaVHHCY%E^;2(`V3qp z=QZl=CWMoCQFZY!FL;-mq>(OTwfc(guWWX7GfldPIdxI90Ep{_SXweB&s#hUyO-0U zGP3A}npFZwX;_}jmmEugpA7)nti^YI2T=ej{~&$r15RH)Dh;W|Lrz{`Fh2@pXu9Z? zZ5pXR6tFcxS7Fq6iNKI_jDC}Md#2X}Sn~kbN)K!FwZQZJ2mJ2OVR-J~-P!A0rWKT+ z_>}kaLF^DM8ImYDOc|$u0R33qrSMC@fQ)SJ&7||^wG^i5z14H(=-^IDM+r9NvG^ed za?slUPVbDx)Map&@=to?sFlbgNozYe0YcJpKVCz z>g_g$+<~>OJl5di>RdiZ^HFV)C(}KMlc2h(7burra~?NvP*|n%Y^um**H%@U@7YkR z_m{Jo2Cx-0V`FpAm554-hZ{)O)>TcQ)h8^l`4Oup3y1+rR5cc>iKzyC6r2QdFhZ9!s+ip~6RkmGMqPTbyv{SN+^y>X%=v_aeWt+11ESvEyYo^NBX!JzdV=$R8FXXaE zP>KY>>~Jn)vlKZPFFMg6+RLYvC$ZHg*bdpKfnU#C^UXqI#c4_9etn|9|Yg zbwHKr+V;ID>6T6jK~g}v4ML<5kd~0{79^#+r9nWD?(Pl|q`O-hBqaPUoN;e)?>#g7 z+0XlZ&-cFXA2SS#HTPQUzVCI#c^=2_;L-CXowk;FG}ohiSn`fUMg4R7n1eSJt^uTTNd^qB}{cSs<50X zhUU9X^-oX`(%Sn4w$X&icMK=^c=WbYtzic-Ur|U>NhxGkw-;?|uyw!HrwZok;D&qx zZ3P&zRsrCnQF_5>+slePHoA=m%jZa*n18n?F^!9ot7)Oq0R*2VLz{s4SpF&amybw@ z(Km>$(EwZ=JE_XTok|$DF?ge_OI-F5hrh5&;o1Vt^8?+OW3pXugOIZNUW2opWzFew zlmtiDj?Rge(9UKzmUH$+Cw}Wik7><`zSL=S^t^xxBi)CRDpPdY}e2Es(hOK z4HAj$y7vQafyoSN!SdfA`|jqK_gtH)K{43h^->1#TibtdvmVx86@#TgF&M7lWK?io zS>V=5{F(VB6U}%-3H!6d*;DArhWO>J(=#33YU@DTdr+Id5yE~xSjfq>!(4T@uRtRR zF`%#vhNe>?P+2AvoX5JVDL)eY2B8JDv_&98ym}HRC~;e5UQUDZ)%E8W%{Rar^XLbr z?=e*^ByZ)s8|?K6**q+tNYV;@)!x(uUjZaK@Ron27k~8}4cB?*Q~Fs<*KLAtko2vq zUfs0^gh*g8cJWb0GO_e8wI|0^<#wLmN)^sfS&u#vK~@zfRkeiO z7uhW-VKcGS>Iv^{&o-ZslWPVa$n$fflHtR9C3`zQ zOiQIh4A`6YW|dk;+p|o|I;CFLgN9Uhqr2lnBxh% z2S~O;JFGk3yMgSr$*5i@{%??0<5$vBRfV-DuNGq&&E7h^klqaEq=d%T1!sVtKHx<2 zWdr)XniIWU{UEa)W+r65^|!^ISW6<6te6S7I>Fl_Vlraz_22Kg|MbKCj<)-6IoI}r zZIxi1QCYbw8ua{oSfb&rr+{H1?hM#*`HSL1sUtSVBy!Wb%FL#Vl|j0YyGk;R#8_;e z8wSwJKeoa+#0M?L=8SEvMJ=#mz>ubu?SDfY1Zokl)XuE7Yf$c$wc2Swm4;7B*a!A3 z7*|>n=Bqk*-+{1VD497Cd*Iz#gUQ-c3bxTiNEd&_5QBG6Ob65@v7kN1N^{xshJt9JW{a?P+w&3ZeuIeZ z8u7OZ=&cl4y{Q}8#xJcvQ7$1BpFk#_vu)mH5HZgELx%TjH$)nU?QJ()|L&01udtJR zdDc%}ZHK*^to*oQ8713C6)d!)sH!+9FGJ|Gft&ME$)cQ%HZ)M5*E&Hb4pQ|%^d+ zxZH#)d-S5f*G9?T5KnnM}e!#?>qSCF;nk1(#ZmMmiX*CkL5|?DkDaj2>h$s5% zNR<7{5xa@R$kE66K#`j}M~`EY#Xav1-ybLgx6!gE3Ur(J>*mjMpNZb3?YTKG6b)@* zFLKhIkbOq1<-v93NAKCT4Z%O>OpI3+KbM|=I$OLy!#H~6n}u}R;-yEVjK}Y&-SS$% z{m(62AIPDxC-eGW;OPh>_jL8a-j?+||_euu(D~;&aM+^Xe2`r+)y7PL= zwj3XG=xJAsE?U*SYovWoEnMp6?Hl4WdpXysQ~&FT zP@?B_eP5HEt-nD$LoSLPr0U5EFQFlB&pw8(B{;n;_cWq5V28;FOo~#MedY>CJm(zh@vHo!& z9L+_p&5c{_z*8NBj;vT|fDUG31)P3qMB62a5;AfBH%O-l!oELe7UYT0bU#)H!v|0Q z4Zb^zeP|9Jrv!qsI;iG3#SLk?_?(%ne8Qo$I8pK)U`NH(lKn1;PpsRN5S?sIso%XP z%ISEoEEr)Cm|piX7Ha@$<&MbM zD_9?H;+S~J^@amw^|5~`GtPK?9=g285&PLZB08x!j1@8Ks$sj(M^>&BRR60+FMaYJ z4PbR6J6alXIu*QYtSGFIXZ)~fT}q{s{N}xk6ds?h5?Jft^a01{hRi3jb*2m+vU5As z&YA7t#5RZxDb?OPuX%GOMv)!c!!g_=+zq{|#9+JEfz}5&bcoHXf&SnQTd3MgzIaSX zc|kpt2^M(4O;tqGp9U-QJzHGBNoHJ;$W$?_kH5WeIUiWTR@A<*PwJwb>Wi;-;11kk z)|W)UQ8sVRs?IZLdpORCcZ_oN5}bIU2M0JPLd~lQNNfL~`1nP{4ZfOQ$p<_DA2<5y z8>Cn%1!$fazCmI~FWYN&F2b+A5r4N0{?-3R)9etqQ@Q&h1h?vltbD!banUTr-Deb{ zTCmyTEn0ge#4+$zUfc(-&=-7O+w>7R9JI1-#BZ7lG%`Xzywq!dY^nwix z3)A@Is$>3seUt+)>@>PUmXGcB!uPd<6)WPpCar_{yjR<3I{_c13tx}AfIR!;E?{5`0{+1Iw9j_F`D_^hEMxN{a*3G`mAaQPD13yLrZxCFR0iFJqD z|Ep64P7x^NYMcaogIM%k&=`Fm=dL|+)htfg1>ccE({uIosg0PUB8`c%x~N{9`r2dIrs{d#MD(u02v!i-swvar6YW zs`FpM+in!;(M)D?9BmPIZ|uWODUr_+>l9d}y(g&C&t9Jh=#J(*s*pr%eTx{?O(xQ` znEH8L2zM>_Ab)K^qVi*oPJOS$RKW!HUD^#b{biV$^kl_B)94qKLyQ#4x|ol3V--Y} zZXk{GVIgqw``&}H^8et9xn|i0TadfnL%e9iG%5z`$<>{3&_`_lUg_Du#LB|BEj=55 zaZJ)Jo^3x-xfX62{x#??IDwZ8{W!Uh5rzcKkEUBm+)=|zNXq%b9m1!fJYkC1nxE4B z7Zip!33;txw<~BLK1_gg@L6})NZlmJB~g&IvQkP1R$H&+Old|-dvrQEn3Iz2vjg~{^`~2Z@P1 z@GOvMS972dNcLHty3XGo@J@4laV(os>V7+Ven}Q+8yfwA`zSfY_|vmE zw2B)InfzZ0NyVy?;I!sFeU}Z6p}EMnI*-^hg^2&-@C}fd-ABAWgFE?xB3X81o5GOc zFo$bGJhS60WlIBb%86c!1wa37ZN51?*bSqz$IoIpNR9BDjo8GgZV7C9zTEskbj(SF zTsTJ(EM=Em`vC#c?+HSd0}2*~`Cjpvn!+_!7VTb?cvpBXwbPiUe3(1i>NL?)%Q{3L z_a2#{cXCMcMhK;{Y)=L2QV0aNM%r{vjN7V3{kK+MaTvlL0lrvi5*zW+fKFr>I=N?9 zSXh)6VpCrC_YTLM@b?ah@QQLb3=d`C6DQ9F>O93%Wk#T%zg_?8vdkF5fk?8%J(-wS zW$`kc8@hng(@d#V$=)PvT4hQTc}c$^?zYzv-8V=!lI&iEhuGZQ;c9s{Cr^94p+mX6 z=0wKl{I#tlJuI%(w-FKj{qWf4yI+q`Z<#(&M)VHITtAB}=NqH44_!60D_?Hf5L-ej zdo@>cb|Ec~W-?z^H~hTeOwEdO0!PRD9%97{fqhQiYL>;gsZZycX+{%#c#AS6?nwjm zRdY^}vu63J-j@z%jk*hYT?_nZM9SM4BQ{GFQ7B>Eqhgpt7#Z##5#S=RH-vVCh}8>P z&V~jzx0r5~8ZxSMvssKDJf`+0SPYBl)@|0k)d(AypiewKflo;_?K6up zi(0X2pE+ah%8UUhd=W#&x=ZfN`;aF~rT7p0&Gdm%=L*7o{cXwzZFBHJB(cv}D~KdU zDh^fnLI7(awkBy|H{!x_q6gh!roX4}O)-8`G3!l3@%`0pH}r(;bQE2?#z*oRscNo% zwXWuA8#E6G7-&YFA3B#@#2be#r(tmR+(v3oqbxX%427D4aNhe9%V5_@#pUDUmVz@s zpz`1uWuCi$Q5Y29>AhXYAF!cBNLBH%T36#CoyMx!pxZkQR%>z^Dt+n)O0o;HpaOu_ zuuTIh0H1%eLBc0rKNgtZetF>U45nX74Oi)Ey?_xt_5K^A!glTFmgZkfl)~vb@o6N) z^EJQmcCRmUWBr(2l0)9ystQW_qrh=D$9dx%)(tlGM>nCC_Z`wY1Yl>uOrrq|Scqsr zq$`)B13Jj}jQH9=6z;-9S&0nGo9fh#*rJ;h^kCk@Xo!;#-d6d|rFkrK?FB=d7u2KaWFvVs?w(FY@m5kjNW-RGSk5UmW1Q zZC&vk8k9+ImW}9plUU1yuN-$F#E%c1ejJ;uvb%Ac&x@;(pbhdWFb=Dm?tBLgb8`%peH@2R#)*@Z1S&-{GX2x0aU>l}2 zak$jHR8xA@G5jK1ce-3(wi%Re;h}5Le?j#By)r(s{JafdFXy$du%cn+?EDh&InN-L!U4%t>ziHV$ z$s+Q-+dQ zA1av5|8rIf8rDJ6*SZg+w~8v_W_qNC@|e;jT+8^n^&^)gyoHx-t-3SZ8Mk_QFX)v9 zzTl1Kf6zXXl-`nkpl@jDgfDLpilgO^k69Wz207CY5d|Oe1wXn<>A>5;H1D+;BfBF% zYwY#?!Vr@W-yolTw)>xdoRboUC+M{~N`j4>Qd3o*P~E^isln*t$l%$2)`OLYaX%+< zi~(`IhG3!QD=mzVCer6YRm0ErPQ>1$GT{oMRgQ%8_7x6~{T@Saq698l1$Rro{8+M! z`~F3>!h4+f8AB0F;&3DRVl@hAj!0dKTL)(N(;krZ-j|Z#3U@SCmzFE`P!A_q_c%JR zlQEQ!n{3mtRx^p%Tcjo3P)&aJ1s!e!+enG{xQ&QYBi(-dZp zESYF|@iAPiv!s`3>Rx(iCL}(naV{ZP=(C2`BZ&5iszmL;JwL4>(i>IL`y0zaRn&q` zY@Lu!_%!s(I#VDgx4z{4gA6r?9U!{$bKS(T1?Bg+5NOvfzTMFak382yxqf)z-v>%~ zngvbBdi9rc!nX8cLsIf`YbwHwINH$i-jv4(t?%8S>J=y>N>(hCD0c4HbGn}H`z6jf6#{-wcXAoN@NzTkY>@-qEYW-t3k3%PC7lBo zIa#wCgRI7jOA_#1v~dc7k7W-*e;D4wKEYFbn88^%yARe*e%vTL zeL$SyL&SYW>JS5&*F5KLA+Z+Esz*gZ;@-Ii@<{0xvrvdcQaf@Uujm;{n{rn81L_)q zkueMWN3^qn9*v6wrtnQMM?4vIy(h&s6_O7{%ax)$)%u9Dt-84(V(F_ip63tIHvA!b zTWl?adH(B3c#3gii&Y-RRU_?s%HxdD9Q3@9z$)PiGEQVqT&Y)0-=r>)A_JyT%SmKEyyXHJ zjscLb;;F1;UddNs!Zn^?pbZhcXrE7j30&IK0CS<-ZipwJYL*da`Pcj~y#y0ILw$_E$7^1Rmn@EZixnX$i=w{6&b;+3^J@3SwAk{=4H?$4!gBYU{L zU-pq~ReJvFIp7*~=>x*m_hQ+eA7-I2fqYXF2%&+Z!fvrO$7!oA>V?fB<)+l?-VhOn zwGbv=Ne2PQ-lLb;wZ=O>6Afcei*-Mh(S#clh$GZg@_uqtpRCuGx^W+b$%eG%@H%xK z?%)|7oM_59Z{mF%sDf2A-(&#bve8a8jrSjK4dl~+P& zp0kkQS%^?}N;JO|gb(^9j1XZ|%jdsBq9S#@q}vI=aP4tLKH*tlJ|f+b3j+7Ztvip7 z$vDqT6&F9<`(xYPP2e83pWhJ9>HwEZ9a!ibBHIYdTG#3&i{vX2FlZ4yL*fajaEa{i z^0)ncVG5tTp^0vGFHywQeEwpJkST*XvSh3p>bZ@tBZ_}|n$AfCNn?}Mu(i-hkWQ9H zen^2z*ZeF79^JjOmBvVO5G0lk7s)cG6aGL8gVpKrpH}CuLO#1^<@Q`#;4QQBX8rkY zlPXG#c}irdFikIh?1Bn?-oDcsMI={;Ei!r!F#Wq+(>0r%jK+xOowul9%6y)hU`BCK z^7%vhL09~W)dO=~2KXI$N)6JPeeQ!@MaTth_!aixz^MTBRsH*H!epP)>Bb z)8H{J9}cTfnZ-Dha8AP{#?eWDAX1Ujp}zxj%j2Tado@99^5wxRALltFun(V~H^IYV zc9LUy(0^RmJ@#`UoMEUZH2hM47^~}8djDqMi$EA$_py~H7-LTkYVaA_8sW2Km0j@m z>;e`^7?}^9##Z2PGTaKW8l^sHK1?RcpJ?{Z7KBT)>OKiE#XIyTkRy}bfIwx_8y(HY>SqV#B81tEn?a( zt~sT|cNq&H6l2@-v1$YI$&=bA$Hm?OGBxToeGwk_Q@c@TeHl2Z4GhUp-woMHZ|#t2 zh~{lLEwd=2lqVbIxxrdiE^WzneObbV>U%45gY3_zwcjA-SGVL8^F?>^nf>)SjO>R~ zlJw^c+EUQ{1`9Rte_fmK{t*NG2ix+WUh+yB@ONFpXv)7V06_h^5B}5N^*;lH_WTz2 z&KIW>hCC%o=f0p;XuGV9UN?(0szrA-*K=ypJA1C%=eIIkU#y389IX@(sBgFi&!E?y zDk_OM-)WMAgV_DaH6sHB0#Scw&9epsL(TDOoxO{yoOY#`$%_@9I|5ImJ|!brD7dQ- za<UQpXoGACdyXj3tg+A=|B~O?nVLdki%C!W1+?krVrjv5h;qMhDUb z#%b_dU;Sp6iM?A{F2*9c7*bI37ogezBlx$Q@6U2`z;z{$>j3s>$2msx&GN-dt%>(e z0c@9x`X2(K?YU^IF%zH^8C1 zw8u%#U3&>s>#K#so8)z6CW#TZ?ZOeJGPBDOTTn2QP(im=LzBjxPkyfMm<+wFfuq&T zb(2In7N}jKgM5?8Ga2rJpr?|gsMHK+<$+0A9#Maj)!q7hR_8>a%M~Hype_#Rs8fNx z2}CZrq2KPWACSFkKNVd{@(o=`2a~=5MyyBouodTrlPMt@qu|^zcqCRSE}W)}Q9xRH z%kMe;owOt`YX6P5rCz5NgJtR~)rMi*oK&!zB-ri4~ea8#+j z#KHUA12qogcWdP=pvJ*mBo1mE(J!cq)>y^op!$)9{#Mw7_RlkQHMX~QG>5HOE{S}^ zvqh;BsRR?&*S+EJEm55rRCwsblV z=Wb8~ytfdsppVNfs;_}Aq@uW(+nC8{4kU*Wuj-CHnDxlXM^#bs-ZF{czY8s>W` z+jvwBtA_*a?YvqxwH0#Eyq<~Cy$tl=qO!SNY5(W(!_ZLck%}%s;2PW-3@i9@3lT@0kOI(ayuaJwYjPN5B)(Zcju#8JSwvRCW`bI1Up&odL|dWFzm4%wnt0|*@E`zYDxeg| z6%1X|W;*WD<}tvzMMGP0bjEfkBwo9QQknw{2wJ{V# zasor!g$6%^5Upbu+}+&wH$P49R(s7&Mgdn}uuxhU7C7T3R3 zcx@bhE;?U{sX$?N_44P5s0cA^A@bZ@_~zlikcGQ%5VO@W?rbL zSo`{}Dl>oxm6bjtd}NNH^-(=yw>##I`yBc9dNB-lS%OQUCWf8PKEGm=qW4n zdl!2YQFarn117=^{qe!x++@%Lb@j(*qGSytijJ)Vl$_&INtAjfEU=D^&}rNgXh?3x z&ukdQoGI)2>w%;K8-9?9@vY|v&D!pbfEq3m1>43Hv^Hf{Z~0J>M~v1Q;lAaR4p0_xWH}+{b^jcP2;_Tr7n3Q&$c=On2ibl_p;Ev}h(th3%+~>F^9-hBD<+ZncT-P%p&YcJm9-(;rt7p55R)V> zZR3$r1Ko=L!xL(`Vo_5<)}&TiSfFG23lJ_rpySnS6TrAwea)`9#mK^a*)qJuSR}fu z+IJj*lMS6uOX9?mx0c=APBOFyk z8r?y0@CxR9hro2s_6p{l{&Csu3GsPV7x|sZD)E9nvsDF2^oN6oiHfgSO_fy33&S_i zNHC9W$UV4`94G-wLQ?O9vlUkWBF0C(piX#2nLg-+n__)&MVW3^(r(%drs9S$Q(EOg z%>F7i=XZuS<*WR6sX3m#|m*@oTm;A|V(#R%I) zl1ahdsUZz%nn{2n*5n$?q>+Gn#s`bECgR&0+`3rqWqvK9cc9{_ z-0XGSHIm;MDSj=BtuCKl_RDy^jnA7FmS%qLh^m%?&RV(~T#>G7#1S_BC~IOuB-HRT zFJO=bWcNc4<6_T0`Ap`3V*W6k;cgI7j<-UWlJs` zYiE^+rPSJ-7e3Wl@)(;g3wPr?m_zq;Ew7cO@io6St&`#Uex~Xf6T+!w6!|8B83J89 zo2K+=$mPe0yu?xW{KHP~$r}3NgZzF@22a{?BsSnUW0<9psZntHb29d|MLXCm{EZHp3H(C>!P$rc#gUsoXVs*j zl?d&wTvgI+rg7UrC2jfW4ydHbeQ1s5zah>C;ek?#{Kwh+=bGxL{}-ybiev$~U!3|- zLBU5$y~{ZFzPBo;8pZ&`MMUuy{#|Mp6E`svJ<#)T{jt%03Fq9Tzt=&^DL_}(f2_{r zTWwI`?MxN!*evp@6)PZU3}2^_V3l(6lBhR-!v!xuV-Ke<_EolPx+oH)XmT?AiC8uw zyl0K&X{T1MRFe>i*N?5@fAJ?NS^hcy-VMJ6zglU_=kOt0E_P=esg#T#5dEZvp1g?_ znQwg1H}&YVhTnV$Si0{$Z5daY>N3n0*$g+VSYMi=ykioSKJSYljwA)IW|LoB)qrz? z-jc*taVK~EQx}(H(R9bYG#P3j*OHrZVkUi(*gdELSq*)X?xqq<&N^XlUvOR-{vjCO^BYyOoTCgGC#J(H zinNye$apRAj668mA@;-4l0$i4R2}upr;B<0@+K`h`iO&wT!oF!J+}QJ4Eu z1y4$eed*DS<{xF^OAu*$13&Q~ETR;yk&>J=yrX5J_?&at##c*{^>W2{)PpL_uDw{A z@~yW9gs#zYke{m!VjB~eSQoUCY)tFQdilbGB#qJ{Xo>rfNXNVb$EtRsrTOJ+&N%0C zJNps3n1Xvbg5$E}y$O5p4_`0t@W8Z7!;?rV6z;yrGf$Og25U#ihQ7&V93v*~bPRn@J1Nf_Rs7D4*KZXlv{VE9bbvyuHe9UfyK?}* zgnPkW){jrra8F#Y*qM3m9aiNmnMc= zl`eUPbw^f3u{ttEP!w@D(L^YTI~8Ko`x3jyo~yTFJTNxLsjHO*1_LavMHyf)fIvVL zp0*p;H;9i5>hL*Pny6v6L-daPEr{w1NE3Xwrx7mPZjBgm_S{9IV}88yK;D65+~8V%Td- zer2nF^4EMG1MSfHr03aFs*$4w;-8@>IINbJC!?9$d zZTTW;-wCfYTHD@CQ3R4$srd$iwyEQ(PJLAa{3GKF*|h;bfAHx*u5B;V;*+7Rw5pOy z$?(<-eAm0eO|Vu7uv-564Ol192kkr|UM8F8Bnvfy#U~yE`-TOjEyTuz;rP+8i$w0L zfiS&b?s9B6Qu22p?OQPr0h6&_#`NtNdfb{qFM0e5JM$UZ4$LG68SWQGpZ@}`7VJ|T zpl6kbq$<)i`%)cz9d*tgkJ5a2M7|l#=M^ReueqH?HI1&)7_5)BF|By48 zt>vp6qdS@Ovt0!$Y7-xH)qBixOp2qZ&Z`hlISCcOsEG-r+G!{C3zMVUa>tX z^Bz(4G7b=@Bp0I$gsH^-xXg7bB?T!k1zLa{5E7EZ6oza#l4G1Jo@FG>KN^?%8tWpx zy{1@9kr}U>2e*shM&7ss<8s}=`Sk5A26@@DN5f^a`rghM^V=j!nH;pD_NEe zXH3~W(h(+t-fa(BUbSnKunr7GED#6p%)h5OiQ@7@NyX4THfyJwahfsqRJ%oG!PAQe zLeze%It?Tb4mu~Lt9L~Tzq(XmtISHi?=yWxs75)-z^P$i>OM&!KWstdMd|6TQT7NT ztgmmHIU#Cni1Ywf2g&y?Jhz}9AkRn0e*yuYJ_{ju2K8tIGv6S}b-@^zakMZn3`<(3 zL2S|7dPf9ZG1 zwXt>Z(DFbVdxfQs0i7;SZMLm6js#o$L~pM5wy4wVXDj99fng-ol?cjVZiJd|s=kZO*SNgEsmG}8ZF9Vb{F_8^6wT7sl zs%bn{xJf7I^_aEuC2_HPBX{Z?;{(m%!!3H5q4d!=(l1K*Vvw~M^j(h=bw|~t^zUshBW5`W>9LU`O%A3;6YC`_z7{lwr74G zwd13Cj18>fAi4-p-$Co~&#XLvR}B*~a(Tyrvn8(et5ja@MbcN)dRE)evj<1JLmfbN%aAZ&3wU=X|mlK z7gE>$79gxqB0D$xfhD_UQy2Zk^!|fthG*W=_Z8oD|M4O)z5>@Ma>e}W1&ZJ)wI#5% z{(z!+P3|6CdXt@pR|2Og$Q!%%27fRP|MWebQnm)oNh&R(*zXw=><(|f-9EkS73@5Z zrR+enFTPlApXGGl9|bMb|Kjn%&g!}~|9`+|p5GK;KEakBG6fQ&S=NmW1@D_@REyR{ zP$G}Dbi+n!{K1?4iDdonw#U@27B6_I4w*eMvZa%+&`|3xYVh;5o+oBd5uaxSK&+}( zZ-7*rtGjxkYFGb4@+-wFilG!?ptSER!yBKTExhu+{0R*}9nSia3Rqtjl9|*+22BsS zIF64twX58{?IJ_1_oEr0x>pkjA#2}_)c&hY$zM3v{0;-;+~F^I1%@>B?OmFy(NRwZ z@9#;+;wIXce$5q+q6wH0_+9VnQ$9I3)QvS|sZA{rsqaR+sx z3k63(OJAkkN*&L|--|{$y}p=EK=o-dl!97jy>Y)#kbWgCWIj4$!O7im4_iG#9;!z) zeT?ma9v&0?6Sh#|=k<#CO@EBcapS5KAeGEhd*r&9rzmM=W6AK+Myu_)S>uv6%LgxU z+o{v3Ja3$-f0aW>_xNvCufH^We2*jgz8(f_;iQ}l<@%|Cy`H5!R z1aqKGX~U--oi{fXqoErmpty;2pDar~tqW#FBpz9|aG^ahMZNg^c?Q2WLTHa74X3iK z`H@vy%fdY5wCQ=*PVyTrh-`|((F%#IGykHo*kO?6HVQ|0d&rTbknIfuG!Y90(8Mne zsePOU?|7}Pi?i~zBIJuSMh{cO?n=wqW1Z?dWmN(ayqqZ679UvXA(MqwbMK+rA3ghKcC&HGpAt}tnbD;?G zrFM}}(vJsnZTOi$S~z3cRj66m!^Eplvo0W(rCN%s`b~Ou)pq!qCUs@#0^?Ws)c}6~ z_)==hb+21+{(8U?P>y{!pr_Y^F}n^>C@aX$twOHrd4Cwk?=S;U3A5|cR>wC8aoXjz zfF+>N{qE65k^mz%^6V)u7_mEOfx+xry3`LV`~SIb$=vk0v7xo2C9EoSZKMRR8I99x z1^9@LZhx-BFS*lf%`X*$83$T7Fx$-HDZI+)WBioxVRXclAa)%%y}x90{llfS#dX0S z*fgi8_G8jewqCk;yKgwc5J=*`eLn$<6*3p5`oHm&2G&J+U|pmK)}E}HNDAOEVFSmUsDrNk$A^9ck7T^x7*To1BJN0; z6)jCi6-*PgFGr49($a|SYuws^lYg$A|N&(qx6>#aHg@nL#KMR z{eU!$!i`DgGY6+-V-J0+e%M=o-ZvWX_DITZ!7%zp5rZkW3q~RL7+-uTX;?}eSb&b zf$lt(d~}P;0YnBqS60Ib{&jxdx$d!*s`?wbz1yXgrMTT;_bC-HIX1NoFU_zGeo-ai&exT`%}rG$MvAbAh1Dj(8B6TOMw~*Pm~z+&*~}`St?Re#G|i7T zh>)x(FCV#Sgf;axl@m{#L4TP#>aho6yC|Q{M>Qpm8($ZdG1e5na!xLi=@^$vsD?Af zx;*39v{TgMf&^D~sX&gZ{a03y>7SY9zY{Z^zrQa*38x^#Do&Gvv&2L5+LkvSm33ob z;7i`j0Je!qTXkJLL+~o89Rbu-=AEuJQRuQVbOa+#7Ki-qL2*q=xsqyn_Kl{MvKEfD zX0bp(%N9F;(7$e4guo0M#ThB?8)L5<*KFNY?XWnvEE*qn{PMx{t5SlR1E*s27|K|ur+#kO@<%ZLl%FJ4d3r9!Vc)n9$a?V$6+KQ`)Zb%l7t6$|I z{;UN@TR^Lc6T8z)S_Z;TJ4G=~U4-Qr97ImRLGLESZDI<)f)BfN5bT3BnxNqzs-85A z0{h^VW!fy`hXonvX=w&56Zm`qK%>Bb4l#N+gEG`AD-f&WWN;vTo zL*TRKuoVA|fvr4~)q|L0yD$~C4p^UrRp*xAG(nZwUAq9=%+I4S=BcQ+XCyfWrkGCN zbTOE+*zqE+u4+ij2A@J;%6?y904*iJ&HSr~s^4J)|E@UW%IEEI$b9qEYJ(%*JhiE! zp+X1e0}5Mdx@U4^krtW~-`uD$P@hc=Isl9nN$VBHYFI$58(^%o(!7WO#wwzPjfDmY zG2gQ}iEno`aI0l)BG5_-+a@~PIijO^mDZ6Q7sjG{)lJeSs_P^fq$2`weN2Vo007rl z*S?JMJO$u-C3p*bZD-Q9t%n&Wxo9}Q{gT2|vvlHi&q>U&aRixw z{@yMp%Th_!d%<}dL&CSJswdKqhsrKg=5gxVZi?J8j}~%X$Hz!9YoD*O;wHy1fElFs zsXvL*w{*OnI8P$rdecZeXrZln@=++hV+{O~TK=Yv-Qy@O_88;25NwCKkv_fOFrZEi4MYhS5aA|@vQiGc z0&$?X)-w+9-7$yRkQdXhwkF9)$Bd<^iXf&k;HFAINkA#~5P4+v)hD=&8Cy*h`o`re?d@mmt{7^S;jiTOZgvvA z7@&Y?Q#Fy(ZrUxm-G%DQ^l=v5yM?vbenXP1WguOyEk#x0Y@9$aB+QPNg(Xcy>NPRIk|h3nSMGWJ()*ceqgsfUWc^ZE@GLmNa4mJ z-=QcngB+{nkiug-y*09Ox)nG6_8A3zurdL{=X3r8MMh`gET+`V$&HEo&u+hcW}3uX zaL)%pQ`b{_%QU;gqh*vNHV><_`H!pa|JRDea)RYJr3AEiwE<+syti7Oh| zFn{Z^5o|=jh6G!%_*F&c<&@8b)J1%S>Ev-`yY-gJFZmSz6F&%8ivGcQMZZ?FQHuDi z-{HbE>h4;+Wx`&gdYUg-mXeG}aI0R{^ON5qKaaIW(SN2y19#THA4B_eXS=>7 zQ*Gu|l2Zr;O-vUE=N2!;F2k!K)!vkOZZ-4)X0^MT5v>OmWPXOG|8|%f+FB@5IJX-7*u`;Do-x%Hx6R z{Ao|WvBbf60eNzL#ELyB`_lHcMXU6O4pbyB8CLu`x>cmEsnLN;nB8;%lwDizKo2&a z+h^~w$!cS(tP`L`Tx7&P4$Hihffv74BlzJ?vEV5PVYaE+!Bv$oHp|IuqF;}=t6d^N zk}b8@)u%ZF%@UYpzP(aTJ$wM8QvuDVb26ri1%CU+xDekm)exi; zr%e$eJY95w!vkn>x-1>SIk$%Gxgd`^<3~e_BZu^;iZ^wG_h)#$6gN_bIM7$s%&!dt z5PRG|8pMCW#Qd@^`iC6QKlnRe@*8FKZ<>97})Kg77BdJ+4>Ci+|4s()!rj-4-ffZec&K8r;iCvGt zESw7(=Eog^C!puQN&oxJ>-Fp#M1CjObr;NwW3s<|nD5^w%>^3I9X`rANeQ*Wy^g^2 z>azWEs;@<~_K$t7pgli@yga(<7hDgQ{d)m9VZ<3b!+7FFkzo+v{)?w4e^5D>biSnEpAK>A!I#18Wlf^N$Tj?6a<$QQsSQzYJ2) ze`UoBsyx?fN#E-}*B<$ex`i%yt*3Y89Q@^hf8J>H2b_pOW5p!Gn)=<7V9CdyQ1&K! z2y7}LB%ealEU9#}_rhWzR9S#itMXI@@w#tg^=mHhC3ad?`yLRvGp5@CB>gu*=>Q!X=g@c$Q|}hhAC& zJJ{d#r2m~iuV;0BSJnTt0HKAN_-~tzHFHmB9*>sxYPWyM=c|*OH>0w#Af>^i5^A^X z=)wP-ux-|X!+z{FUsGFM- zw&c%rs};63y%*Yto6Ot6VDdD~0}FvNR5Z_e<|?AHZV&HrKKZNE@br{wlRa?zeqiZ^ z&6X2OmjOl}a?d9C0XM#}&lLh>L$(1~n_r9-iUw5X)+h2)%n0Xuf{Je)P(j_if$DvE zE0l{J%y5W!0RqFBfBp_Nrql|GO5lwc2hclIbU!Ss&C>XUZZx8u`Q!Mp17PNCdUwj zp^@Vc2i63}5*OaGBMw_Ta#(Y_<~0=Y$~1Bc3B*KKJJxl68vSV%9#3#k=8V)$^5}(OD$_J7ltLQfB>pv+ zsKHwOoTmAx*U}dYH_<%P%BCvx#fv!E?mWBAv0m4NPhQR{Ol&R7>~${S zi+PWcMFB_a?Z90qwJm;L-NnJ%Zd~ZdCz+a;EuTs+*rd*$l@BoJ$2-8i!(#qmPk{FV zI?1kH>ZMN7Z1Y4?TJAG-4b4|kI(RX-yHp5`UjB}hJy;!7n(a5 ze^wR$heG*(jZ^sBaJ_skzK7jhk2$1MILfYBtS*ibE5Etl6-ni0UK zV}JUG`hp)j-9OYk{Mcvyq2^%?rXQRl2ICp(-6S951%7B{!kLe9F(VPpx#8(=6s)Z9 zO`TEhthz`e0YS($VT7#)D9(cfA^z|5q+1^A=#C>__OZxDS)SK|tCyBAZdMDFbtJTH+p1?ORRT`!R0$S#pn z!L4Bc0eh;QCZHu^UvoWS0h79a+9h@a{5MGdDERwaJJ}^&$cZo?8R#9q1Ap}Z&x-rM z$N1y(0kaZ<<)GAJ?fg7Ms%W${>zqI9F;m{&r-ah5Z5ZvlbWM0+-XZ*wuV~MvfDr!o>;-GZgp6UK|T+{i%l*rFHhoOx@vz2WQdX0fnf2%cM;-O>QW%yQ#iex z_yo~bTiU4m>^q*bp{KD%JyVLh@TJC#hF}Cs&v#fk9vEDzB%U%>sIF+(XqeyVFbg!T zDs12o@dp-eh0+=r;Y;Z zF;07yP5K7IoKpv8H$v#rI#BMNhop{{OXcroEj&}9InUEl^j&0w{NUNy9v!zmfB&9i z3~X>N=Tz87m-pOg05Z7X9~324t6A(Pep7}@K-R(x3+`rLWeF~`i!FS^O&VP5-Qc3n z!J06oEVemdd~b;X%plNE=C8f}tTg5|(Mo0g=IO<2%(kghN5ZM%bXW|$L}@_v3inE5 zA}?0TdA)Jvk;ph!*ka=Yie8o2dp9cOAZjJDwlwY?jD)WxZ(oZScQn5t+{fGgGMNj} zYH~#W#(A^7P#ry^V-vm3eD71eqsjE!t+Q3THP~cpY}u|oqP%WVc6NwrTwI8|!TgoR zeN^w@_(S}{W8=q)dQ^tzFL#FGXljp?dh0^+Afhlxp&&#mjsi6=b4c!W zzF6v;VM`zR1Ivk$2)~{vsh}l%9Y4y+5j$&FmulGAajYRI0px8=c5r35VAv_$XBw3) zhMv&SGdi9w5EZIIb_qhC_uO`iGNYnyT9=CYdqor?pdOZrniuCab5Uzb#yLtmisvJ{%V2dR zFh$TaVLYEdgR(v?uUtajVTy(P4st4eUFt~W(`)bN)q9T{G#IDP@V4hhofN^+rU{`q zt7wjM6p~ao>cKXd=aBO1#O}7m9BsbX$8P(3x^lTchTcpOtSnS8ix|c7D8uoBEJ)RN zIpE_gT~3vwBISDZaa7X_PN4~>mg-s);(OEG*HdN$VWiRvIPAK}LOvWQk|6`+x1XvQ zEs<8Fb@`q)i;P`^+B}~L_}G3(KBDAOkB`7eQR)7;&=Qtrm|bc*$0Ymp0^cemJC7*q ziUaeTKe7gNRGlUI-b+JF-;DDqM`jkb&ahASL+`R9Qw|T`qQddY0gk_R?Cb3|BqHhO z<8t|hHFFSARGmXhcjRxS**KE_)2 z^-TASF0SO{fL0wL>iOxtdyma<#FMCWRkzeLS)G#$l4-)0cG+kcA92v<=noz2GmRvb z*jrVgYeSS0SlcM4F6E@11`UK-^M+}i+y?;x^#Ke@xZZ}=3 zl_WZtW`cUbE&;0wjS{TC@!-BE2d(OH?RTX3Z^#)OMwso6BnGc(Z;*r3wWQ`_a_K>a zx59dlQE5@KUA}$D#z&@%sUTQ`&3O>hb%v|}>gys6MDlY50 zOtTYN6iT(&tj}@Uni4$@wdJ~Sv$b2|@fuFOrP5#m>)s*hF>!gCZVQaNjly?Z@ z?vSlaGv=Rn@Qn4m)($hEJjzyiJgLz_0%8Ltn5UY|TT`QvqdwCSTilb%+r0Y`6G0=7 z$gt0D4%OIQkF*Ou_+a%-w1-*Sle?8fGB0EwiPtTz^oRN~bZVcV5VD@?i7K-YAj4{Y zEwE@Ttdh(gI?XA3a^t1;jn0cDN@1FK$!|o>qj=sM`LwX*J-kInjWSqtx6AWAdpc<( zRFqof#&FdG_)*o=^GZJR5CO)^@%bslUWv9~rs_4_UkCN^C{yR0eE!5wfOX_3WT6`} zeiXYT2I)D8yk-z*Yd4h+t79%~(GFJNLAIPI2! zf-XqG4y8#ud6wa;smeC;sJq+4V{cN>PaV*{b%GCqXLRK6aMNnhaq_U z`QTek5;l3RKNdRtUN7X`CkQF5`1EGA9XrLagF!FLbxr(bn6_|FQ;Z)OsU4#ktLO&C z@_)0ivMEr7(f0sYc9=_6G>aAkn1BypLK0|VFGe$lKW0k;n%Il!Kp6Y)9MkrxWm~rh zPzIi`xAE8j%D@UOCc@3yaG(s#Q7zr2bxV*7G~W%&0=={!U;jVljQ-;DO?LFR&Sx+V zH0k22C=0EPpOw+83o3il;FpZLx~>M-Ne1mEja1EJCUvS_7s=e@JW=f*6?sLejeKZ=nz3pxo(p0C#esfmP}ZvTzpVMz#UXLsFN-0%s{nJw5g}VwAFb+>{6zMH*FUt$R;e?h zZCc5nqN%N5jyOgL9VIlAzm=X2JcaURg%+DyFHhVgtuak%U~zMG^cyis((~Z7L>Lj_ z;$J52ExQpoz^*WFOsZzx>7eLg*rMj>^>N@Wbjbj!$$KePK7Gh9g0O$EnBEKGhp*7t zdvt&8WnTOQNvb!_-1Xed{)Nz}MdmNr?*Fn33)C|N+Ci~xPO4fl*jA;|-ZwoS#hLRe zVQfT4Se4TBVZf<;fksgkhS^-;ICFLm2ulK8hf`=Y(%=)M%?*_M4t#>30HW23(IESU zyMtjD*9=08%tjn1Er1fDKS6TPSR{sDoH$0qEE?0q@QeF?II;Ae@#m9E66qi5guD8{ zTDp=#iY%7v)+c;H6ieLVSoiN6!e{=_|CTEvyp6O9AJic2wXKnxO^b|nMJno{H1;d2 zOT$nPwSBcOi;_N%&^8mCbGFOS{EPbDPcr5JNWP_9`DCb(p2hiktEs6~;dE~OBXz22 z#z33$H5*3!H+@C>y0uMjJKrv?(4Z8A4M?A^vNVZW9bch;U`2S89U8w zNcv(s_A4+7dfyLDq{_=F7nc(mJgwk_fbgK4@4-gtSi)+YG5UzOZ@3?;t5p4X4F=y0 zT_fL#?}nzF{53BNUEtr5?}o0|@5FceJRHuJ5~yvx%9<(4ro#X9B)`QqRj0~%^0z0n zE#_kdYKL_kwv+ajL4}_uw z{-8$~Rx|+WPXOOW=i8K~H3uQcN?N=knus@&MFFuUv_Nn3h+@&PD8ZUGnQ>O6zeDQ> z^ZYg<%BEB`236%>W~TgI7V&GJt2@mhHYABa7pV!r>*eSeFntivG4-+ruy6+Tnz}Ck zjg#W*7=2dg(2UV-1pqh9MypL9NcXgL#Q{fg(PzeiJ(aS;d*-`oWBW-WQ^|$|hw!{s zD*8K|r(wjNS4t9bYu8V6tljG+;<)PJdm`;^$YUnP6$&ETAmis<0qm1gp_!$p`K^Np!Hd{-6a8LYb8>W54W+U7-JBc5C>G!r>3BH=; zF7JLZd_*9FMS8Z1<;4x`lW(JFo#Qf9LvRi_i=7cD-MWGRgNa<{i2~e4I#pFETCVeh z-b+D}l*eP7?*+*~uo{ezStW?J)S;r>;8%#?BPVwS(C!j@*LYQ+R;ywcK6JnSuBsm~ zoVITRkNe-y=Kp2U_#I^mw~yy<{-)pdq zX`SklW4In&=cCjmc`!=Hjv!3GM~e4$#!(U3)JqN)Ho`2W%}AD8WXe-R;}e!ok$23U zeVbl(X&Ki_vZea$*9@rHEl&usUnC!)PDo2OtBCzIE{upDEgMH+LaPuIrc z-6ciRtl=T5266J7IpRv2Q$*p(+77V1HRW#AGv*X^%FH;2deaI$weG1N_+Y_(hAxkM zRJ@Y9a*01Yq$KBAHJwFu!(Q6~-`F6k_AYCU-5wu5kTd$(2XW!_LOT|VWGE!Hai3@> zo(U9T&O}BYmWsKCEDQDZ)`W2R#OWM}@Pf=74nNj<`vlapAbnUJcP&Js zy`iD&Mb#U9a-Srfvkm+?cM*`ARJ2Wamgnsby63$T>#G2+rz-^!`knA-2dFtRU?yBA z@{!9sWG0P!13|=aXhZz{oOHugkYeg0JfIYN$tS9u$oA*-KyP#zVdv=WWAh9sI3z<= z2B8ne5Lv!DV0_k5rTi2}td)GX+8YtUcrh$JU!{KQ;SSbsTFhS}_fLga%!m1(e7Mz} z24zTXD>f)-?;M;(+B2-X-2Y?V(1?G1kvo7;?_)3b#4Cnxvgu#5??1)Ut(?BerhjG3 zqgC2Lw)<6}9UafnGVS7j3bcdSpaAYIO!m04gvh~HK;l{Z1CXv{2GM%jaI$&HQ`BKI za-0~Cm;=H8+j+hT!8hSG(&M`VpWhDl^`o~=4<)Fp6;H=eh*e-`)!Xu<0sH|Jm#)5gcgEFnQ?G7qoUM9n7I9x+7&7Pva^oo=U+^i z=kM-T(NU2nzZh68)2(q~;+tH&)mJFu$K&~eWqc=0k47E77pDJ;S$w;~+s?!ZoNU9Q zIA)JLPS>cxu~AevS-2}9lCLP@EGa~0ll}v?AD~lVHfeqabTBu4si_J6Ei-RZtbYft z^FQ?&7|4+6dJvt>Mh__e#+>>*;;pQi|7&;<8i&~~kdX?g%86ogHZ|8c7fv_kb=C~T z?u9>?8>p&)BB@g!vIeL4vM^%0&hAvRS}{G2FCvYJ2CdQvFvvoOc`mS?;ib@hSlS$x_# z>Svn!QBfn$^>VYq(4`R4R;kc~4k_Jbv=3D;5J{bsJ-eT~!R`;@b1{WdDKn%Q-f6iH zO>^TAg?Uj^Qe*Qx)me3<_n?=sNtxH)FxVstniyTt=+Z|7j|Gau7UXy;$ecq0of~@b z1&0`}oH_VDo&V}wT=ZLhdq1yC>D?u69I3BP1lL{GRL7slKI^bvoD-hf%lbND?nq+d zc|kLABSEezgh&x`UkPO#bG&ak=LwR}!`?*n`$2yIo#821Ni6lXGT2n;A9Aq-Z}^P% zN5)m#d(0P(6(J1AtsLbRRnjDb>-nsgK<0FDK7RZjg!oMHOF`e<{J2ttI5 z#=L|T=b9~$&+%k!VlCfbSeKtobQLBWqpqhiOTU?>&3S}+@S@JFm-z>+hqRhY zRKew?6oOK73^k{$L*lNL#g1iOAODz*AQhCjqg%flODUW4MbX{UIX+&HrSD?He_&kt z?l=5SECjzBgWmc%xM6I;WH|iFNkQP;LTZL@5lEV7`oc?32WzYWpnI1ml2qrQ6Rh5M zGN2EsZ*oWg>o%e*q?62e)iNStkxUksyym{BA&tVoxA>o1@eu6jiU-mdX-IVxrs=qjmCXC)GWV=X?+{HEbS;F)vc;~EOWx^5kjZpYYh%y{;a6QOOZw3>zt04Rbh5ZNC?CD|3^ zLwg?X%RUwB>qT?Y_+^m@3rBW{!Lpo5Py+(vR}Q33AQJ3d-Aat^p^jMW((0eZSoY z@RK;<&-5((L;DcP4C3ac+3EqpV%Mr5NiVba{o?EZhTi8A-7n-x(+e6TJIM+UWpWRP z%T)UlkKH*w7wu6MzYyIStxcieEMxG8U07u`O_pW*UL*6DNW^(b zpGYzVin1nC0=henxz|c;g$90t|Io!%*f6P7%$N!1#h@p;QKXgxq*sh6;H-oY>kC+e z&O!jb;xSUZL!DbP2k{fSFoiR(QblzRfwM2mxx$Q5GxNH{_2;yj`(FIs#Uz4PMiaBG ziw{yRHck~=N|i{7=#^e9+Hc}>^7et;;JU>cY(G}`21bhz+O+9-<7)J6OF--M-v9{P z))pZ&OqT*5cLZesBKR7B*S>Z8{E5+R?fC&vA4aaxyYHwa&rxbt?%pNa)#g0M`i7#^=Q@e?YN(!Hb-9Kwl1A_Z9+vxVNxGVNh}m|>*lmJ7KR1Xgq)NkfBv1wY zh7QPZ@^b;aOOlQKU8_mD zuKp9>?rcFUGc^f@3F7Nq01%{;6rd71mQ_dF49z-)GWwy-!#?;+DH{j75kDp<=Pn)zkLFPA!iO#l`duypyxF#{%58#h*6~xw)!Xf1t2VD_ z4M-fsBH=BMEh6wPOKLnTQOB?Dcf9@xnpOTJK2Vl|U@xssu(NPnz)`G)uPgXX^)t+# zP0XIcFL?OxfZFt1J<`9U3@M_4fS=>p;$c>_zPlfPRh9Es`A@&QKfq4=R#^LpN~c*q zLEL zF5P*Oq$YX91F<^#wzIWTQK3Ta(RBrRidJWt7fd~_xpV~$0zyQIE{l#f*V^fB(TS{k z8{spP;FFv$von#ExYX@jN6LqbD^ZOrG6NnBK}XRm!bb{PCruS<)ogv!;)71x)>Br8 zXD_L%Som<)^<#(77}lVAGkbUE7pR@_Mzx2@@QaZ#2WWhZW;RDK;|X8~SF;uYHqMMh zd){Jc{sBYIJJ82h^7adICwwqvV!L@E>2yOkt(M(@xN?2=oQ%h`RjuwuPd1OLlooAZ zN);55^vUgr>#k;$74jr4C7Y?z8tF7Y-S?Y!vHiWI?*1j}l)Wa$-Uig9I z)vBAjjk?hpJ#0owrj3=5HpjHWBxcg!**AS(-qqfl5Y+sH#wFsmTMm+sfP#ruF}L{) zyBSwmE049^PR3&vodC2VE zEv7qrU`>Xn9P?8Wy2qBXRzeZnygWw+ivsWC*AAOGTn+z>$6(HFJN7^PB08{z5%Jp% z++VZA|KyAik!WiPGQwNN%0rVVE+_q!ph>;1XJm5aE;3M}-S%#u&0#S`oVW1?(dpNY zyupsI#%eT%0We z1OPVP*0=Rb`bzYebLDrF=RaW__%{{!-${J_JHGS($<@#0jlZ-0+xngVUkm=ya(8>^ z{n@{ + + +]> + + + &project; + + + Apache Tribes - Membership + + + + + +
    +

    TODO

    +
    + + +
    diff --git a/webapps/docs/tribes/project.xml b/webapps/docs/tribes/project.xml new file mode 100644 index 0000000..e3c427a --- /dev/null +++ b/webapps/docs/tribes/project.xml @@ -0,0 +1,58 @@ + + + + + Apache Tribes - The Tomcat Cluster Communication Module + + Apache Tomcat + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/docs/tribes/setup.xml b/webapps/docs/tribes/setup.xml new file mode 100644 index 0000000..55daad8 --- /dev/null +++ b/webapps/docs/tribes/setup.xml @@ -0,0 +1,37 @@ + + + +]> + + + &project; + + + Apache Tribes - Configuration + + + + + +
    +

    TODO

    +
    + + +
    diff --git a/webapps/docs/tribes/status.xml b/webapps/docs/tribes/status.xml new file mode 100644 index 0000000..a890607 --- /dev/null +++ b/webapps/docs/tribes/status.xml @@ -0,0 +1,37 @@ + + + +]> + + + &project; + + + Apache Tribes - Status + + + + + +
    +

    TODO

    +
    + + +
    diff --git a/webapps/docs/tribes/transport.xml b/webapps/docs/tribes/transport.xml new file mode 100644 index 0000000..3b03671 --- /dev/null +++ b/webapps/docs/tribes/transport.xml @@ -0,0 +1,37 @@ + + + +]> + + + &project; + + + Apache Tribes - Transport + + + + + +
    +

    TODO

    +
    + + +
    diff --git a/webapps/docs/virtual-hosting-howto.xml b/webapps/docs/virtual-hosting-howto.xml new file mode 100644 index 0000000..5f90580 --- /dev/null +++ b/webapps/docs/virtual-hosting-howto.xml @@ -0,0 +1,145 @@ + + + +]> + + + &project; + + + Virtual Hosting and Tomcat + + + + +
    + +
    + +
    +

    + For the sake of this how-to, assume you have a development host with two + host names, ren and stimpy. Let's also assume + one instance of Tomcat running, so $CATALINA_HOME refers to + wherever it's installed, perhaps /usr/local/tomcat. +

    +

    + Also, this how-to uses Unix-style path separators and commands; if you're + on Windows modify accordingly. +

    +
    + +
    + +
    +

    + Create directories for each of the virtual hosts: +

    + mkdir $CATALINA_HOME/renapps +mkdir $CATALINA_HOME/stimpyapps +
    + +
    + +

    Contexts are normally located underneath the appBase directory. For + example, to deploy the foobar context as a war file in + the ren host, use + $CATALINA_HOME/renapps/foobar.war. Note that the + default or ROOT context for ren would be deployed as + $CATALINA_HOME/renapps/ROOT.war (WAR) or + $CATALINA_HOME/renapps/ROOT (directory). +

    +

    NOTE: The docBase for a context should never be + the same as the appBase for a host. +

    +
    + +

    + Within your Context, create a META-INF directory and then + place your Context definition in it in a file named + context.xml. i.e. + $CATALINA_HOME/renapps/ROOT/META-INF/context.xml + This makes deployment easier, particularly if you're distributing a WAR + file. +

    +
    + +

    + Create a structure under $CATALINA_HOME/conf/Catalina + corresponding to your virtual hosts, e.g.: +

    + mkdir $CATALINA_HOME/conf/Catalina/ren +mkdir $CATALINA_HOME/conf/Catalina/stimpy +

    + Note that the ending directory name "Catalina" represents the + name attribute of the + Engine element as shown above. +

    +

    + Now, for your default webapps, add: +

    + $CATALINA_HOME/conf/Catalina/ren/ROOT.xml +$CATALINA_HOME/conf/Catalina/stimpy/ROOT.xml +

    + If you want to use the Tomcat manager webapp for each host, you'll also + need to add it here: +

    + cd $CATALINA_HOME/conf/Catalina +cp localhost/manager.xml ren/ +cp localhost/manager.xml stimpy/ +
    + +

    + You can override the default values found in conf/context.xml + and conf/web.xml by specifying the new values in files + named context.xml.default and web.xml.default + from the host specific xml directory.

    +

    Following our previous example, you could use + $CATALINA_HOME/conf/Catalina/ren/web.xml.default + to customize the defaults for all webapps that are deployed in the virtual + host named ren. +

    +
    + +

    + Consult the configuration documentation for other attributes of the + Context element. +

    +
    +
    + + diff --git a/webapps/docs/web-socket-howto.xml b/webapps/docs/web-socket-howto.xml new file mode 100644 index 0000000..2aaa808 --- /dev/null +++ b/webapps/docs/web-socket-howto.xml @@ -0,0 +1,189 @@ + + + +]> + + + &project; + + + WebSocket How-To + + + + +
    + +
    + +
    +

    Tomcat provides support for WebSocket as defined by + RFC 6455.

    +
    + +
    +

    Tomcat implements the Jakarta WebSocket 2.1 API defined by the Jakarta + WebSocket project.

    + +

    There are several example applications that demonstrate how the WebSocket API + can be used. You will need to look at both the client side + HTML and the server side + code.

    +
    + +
    + +

    Tomcat provides a number of Tomcat specific configuration options for + WebSocket. It is anticipated that these will be absorbed into the WebSocket + specification over time.

    + +

    The write timeout used when sending WebSocket messages in blocking mode + defaults to 20000 milliseconds (20 seconds). This may be changed by setting + the property org.apache.tomcat.websocket.BLOCKING_SEND_TIMEOUT + in the user properties collection attached to the WebSocket session. The + value assigned to this property should be a Long and represents + the timeout to use in milliseconds. For an infinite timeout, use + -1.

    + +

    The session close timeout defaults to 30000 milliseconds (30 seconds). This + may be changed by setting the property + org.apache.tomcat.websocket.SESSION_CLOSE_TIMEOUT in the user + properties collection attached to the WebSocket session. The value assigned + to this property should be a Long and represents the timeout to + use in milliseconds. Values less than or equal to zero will be ignored.

    + +

    In addition to the Session.setMaxIdleTimeout(long) method which + is part of the Jakarta WebSocket API, Tomcat provides greater control of the + timing out the session due to lack of activity. Setting the property + org.apache.tomcat.websocket.READ_IDLE_TIMEOUT_MS in the user + properties collection attached to the WebSocket session will trigger a + session timeout if no WebSocket message is received for the specified number + of milliseconds. Setting the property + org.apache.tomcat.websocket.WRITE_IDLE_TIMEOUT_MS will trigger a + session timeout if no WebSocket message is sent for the specified number of + milliseconds. These can be used separately or together, with or without + Session.setMaxIdleTimeout(long). If the associated property is + not specified, the read and/or write idle timeout will be applied.

    + +

    If the application does not define a MessageHandler.Partial for + incoming binary messages, any incoming binary messages must be buffered so + the entire message can be delivered in a single call to the registered + MessageHandler.Whole for binary messages. The default buffer + size for binary messages is 8192 bytes. This may be changed for a web + application by setting the servlet context initialization parameter + org.apache.tomcat.websocket.binaryBufferSize to the desired + value in bytes.

    + +

    If the application does not define a MessageHandler.Partial for + incoming text messages, any incoming text messages must be buffered so the + entire message can be delivered in a single call to the registered + MessageHandler.Whole for text messages. The default buffer size + for text messages is 8192 bytes. This may be changed for a web application by + setting the servlet context initialization parameter + org.apache.tomcat.websocket.textBufferSize to the desired value + in bytes.

    + +

    When using the WebSocket client to connect to server endpoints, the timeout + for IO operations while establishing the connection is controlled by the + userProperties of the provided + jakarta.websocket.ClientEndpointConfig. The property is + org.apache.tomcat.websocket.IO_TIMEOUT_MS and is the + timeout as a String in milliseconds. The default is 5000 (5 + seconds).

    + +

    When using the WebSocket client to connect to secure server endpoints, the + client SSL configuration should be configured via + jakarta.websocket.ClientEndpointConfig.getSSLContext(). Tomcat + 10.1.x still supports the pre-WebSocket 2.1 configuration method where TLS + configuration was via the userProperties of the provided + jakarta.websocket.ClientEndpointConfig. However, this approach + is deprecated and will be removed in Tomcat 11. The following user properties + are supported:

    +
      +
    • org.apache.tomcat.websocket.SSL_CONTEXT
    • +
    • org.apache.tomcat.websocket.SSL_PROTOCOLS
    • +
    • org.apache.tomcat.websocket.SSL_TRUSTSTORE
    • +
    • org.apache.tomcat.websocket.SSL_TRUSTSTORE_PWD
    • +
    +

    The default truststore password is changeit.

    + +

    If the org.apache.tomcat.websocket.SSL_CONTEXT property is + set then the org.apache.tomcat.websocket.SSL_TRUSTSTORE and + org.apache.tomcat.websocket.SSL_TRUSTSTORE_PWD properties + will be ignored.

    + +

    For secure server end points, host name verification is enabled by default. + To bypass this verification (not recommended), it is necessary to provide a + custom SSLContext via the + org.apache.tomcat.websocket.SSL_CONTEXT user property. The + custom SSLContext must be configured with a custom + TrustManager that extends + javax.net.ssl.X509ExtendedTrustManager. The desired verification + (or lack of verification) can then be controlled by appropriate + implementations of the individual abstract methods.

    + +

    When using the WebSocket client to connect to server endpoints, the number of + HTTP redirects that the client will follow is controlled by the + userProperties of the provided + jakarta.websocket.ClientEndpointConfig. The property is + org.apache.tomcat.websocket.MAX_REDIRECTIONS. The default value + is 20. Redirection support can be disabled by configuring a value of zero. +

    + +

    When using the WebSocket client to connect to a server endpoint that requires + BASIC or DIGEST authentication, the following user properties must be set: +

    +
      +
    • org.apache.tomcat.websocket.WS_AUTHENTICATION_USER_NAME +
    • +
    • org.apache.tomcat.websocket.WS_AUTHENTICATION_PASSWORD +
    • +
    +

    Optionally, the WebSocket client can be configured only to send + credentials if the server authentication challenge includes a specific realm + by defining that realm in the optional user property:

    +
      +
    • org.apache.tomcat.websocket.WS_AUTHENTICATION_REALM
    • +
    + +

    When using the WebSocket client to connect to a server endpoint via a forward + proxy (also known as a gateway) that requires BASIC or DIGEST authentication, + the following user properties must be set: +

    +
      +
    • org.apache.tomcat.websocket.WS_PROXY_AUTHENTICATION_USER_NAME +
    • +
    • org.apache.tomcat.websocket.WS_PROXY_AUTHENTICATION_PASSWORD +
    • +
    +

    Optionally, the WebSocket client can be configured only to send + credentials if the server authentication challenge includes a specific realm + by defining that realm in the optional user property:

    +
      +
    • org.apache.tomcat.websocket.WS_PROXY_AUTHENTICATION_REALM +
    • +
    + +
    + + +
    diff --git a/webapps/docs/websocketapi/index.html b/webapps/docs/websocketapi/index.html new file mode 100644 index 0000000..19cd778 --- /dev/null +++ b/webapps/docs/websocketapi/index.html @@ -0,0 +1,34 @@ + + + + + + API docs + + + + +The WebSocket Javadoc is not installed by default. Download and install +the "fulldocs" package to get it. + +You can also access the javadoc online in the Tomcat + +documentation bundle. + + + diff --git a/webapps/docs/windows-auth-howto.xml b/webapps/docs/windows-auth-howto.xml new file mode 100644 index 0000000..4b9ec05 --- /dev/null +++ b/webapps/docs/windows-auth-howto.xml @@ -0,0 +1,342 @@ + + + +]> + + + &project; + + + Windows Authentication How-To + + + + +
    + +
    + +
    +

    Integrated Windows authentication is most frequently used within intranet +environments since it requires that the server performing the authentication and +the user being authenticated are part of the same domain. For the user to be +authenticated automatically, the client machine used by the user must also be +part of the domain.

    +

    There are several options for implementing integrated Windows authentication +with Apache Tomcat. They are:

    +
      +
    • Built-in Tomcat support.
    • +
    • Use a third party library such as Waffle.
    • +
    • Use a reverse proxy that supports Windows authentication to perform the +authentication step such as IIS or httpd.
    • +
    +

    The configuration of each of these options is discussed in the following +sections.

    +
    + +
    +

    Kerberos (the basis for integrated Windows authentication) requires careful +configuration. If the steps in this guide are followed exactly, then a working +configuration will result. It is important that the steps below are followed +exactly. There is very little scope for flexibility in the configuration. From +the testing to date it is known that:

    +
      +
    • The host name used to access the Tomcat server must match the host name in +the SPN exactly else authentication will fail. A checksum error may be reported +in the debug logs in this case.
    • +
    • The client must be of the view that the server is part of the local trusted +intranet.
    • +
    • The SPN must be HTTP/<hostname> and it must be exactly the same in all +the places it is used.
    • +
    • The port number must not be included in the SPN.
    • +
    • No more than one SPN may be mapped to a domain user.
    • +
    • Tomcat must run as the domain account with which the SPN has been associated +or as domain admin. It is NOT recommended to run Tomcat under a +domain admin user.
    • +
    • Convention is that the domain name (dev.local) is always used in +lower case. The domain name is typically not case sensitive.
    • +
    • Convention is that the Kerberos realm name (DEV.LOCAL) is always +used in upper case. The realm name is case sensitive.
    • +
    • The domain must be specified when using the ktpass command.
    • +
    +

    There are four components to the configuration of the built-in Tomcat +support for Windows authentication. The domain controller, the server hosting +Tomcat, the web application wishing to use Windows authentication and the client +machine. The following sections describe the configuration required for each +component.

    +

    The names of the three machines used in the configuration examples below are +win-dc01.dev.local (the domain controller), win-tc01.dev.local (the Tomcat +instance) and win-pc01.dev.local (client). All are members of the +dev.local domain.

    +

    Note: In order to use the passwords in the steps below, the domain password +policy had to be relaxed. This is not recommended for production environments. +

    + + +

    These steps assume that the server has already been configured to act as a + domain controller. Configuration of a Windows server as a domain controller is + outside the scope of this how-to. The steps to configure the domain controller + to enable Tomcat to support Windows authentication are as follows: +

    +
      +
    • Create a domain user that will be mapped to the service name used by the + Tomcat server. In this how-to, this user is called tc01 and has a + password of tc01pass.
    • +
    • Map the service principal name (SPN) to the user account. SPNs take the + form + <service class>/<host>:<port>/<service name>. + The SPN used in this how-to is HTTP/win-tc01.dev.local. To + map the user to the SPN, run the following: + setspn -A HTTP/win-tc01.dev.local tc01 +
    • +
    • Generate the keytab file that the Tomcat server will use to authenticate + itself to the domain controller. This file contains the Tomcat private key for + the service provider account and should be protected accordingly. To generate + the file, run the following command (all on a single line): + ktpass /out c:\tomcat.keytab /mapuser tc01@DEV.LOCAL + /princ HTTP/win-tc01.dev.local@DEV.LOCAL + /pass tc01pass /kvno 0
    • +
    • Create a domain user to be used on the client. In this how-to the domain + user is test with a password of testpass.
    • +
    +

    The above steps have been tested on a domain controller running Windows + Server 2019 Standard using the Windows Server 2016 functional level + for both the forest and the domain. +

    +
    + + +

    These steps assume that Tomcat and a Java 11 JDK/JRE have already been + installed and configured and that Tomcat is running as the tc01@dev.local + user. The steps to configure the Tomcat instance for Windows authentication + are as follows: +

    +
      +
    • Copy the tomcat.keytab file created on the domain controller + to $CATALINA_BASE/conf/tomcat.keytab.
    • +
    • Create the kerberos configuration file + $CATALINA_BASE/conf/krb5.ini. The file used in this how-to + contained:[libdefaults] +default_realm = DEV.LOCAL +default_keytab_name = FILE:c:\apache-tomcat-.x\conf\tomcat.keytab +default_tkt_enctypes = rc4-hmac,aes256-cts-hmac-sha1-96,aes128-cts-hmac-sha1-96 +default_tgs_enctypes = rc4-hmac,aes256-cts-hmac-sha1-96,aes128-cts-hmac-sha1-96 +forwardable=true + +[realms] +DEV.LOCAL = { + kdc = win-dc01.dev.local:88 +} + +[domain_realm] +dev.local= DEV.LOCAL +.dev.local= DEV.LOCAL + The location of this file can be changed by setting the + java.security.krb5.conf system property.
    • +
    • Create the JAAS login configuration file + $CATALINA_BASE/conf/jaas.conf. The file used in this how-to + contained:com.sun.security.jgss.krb5.initiate { + com.sun.security.auth.module.Krb5LoginModule required + doNotPrompt=true + principal="HTTP/win-tc01.dev.local@DEV.LOCAL" + useKeyTab=true + keyTab="c:/apache-tomcat-.x/conf/tomcat.keytab" + storeKey=true; +}; + +com.sun.security.jgss.krb5.accept { + com.sun.security.auth.module.Krb5LoginModule required + doNotPrompt=true + principal="HTTP/win-tc01.dev.local@DEV.LOCAL" + useKeyTab=true + keyTab="c:/apache-tomcat-.x/conf/tomcat.keytab" + storeKey=true; +}; + The location of this file can be changed by setting the + java.security.auth.login.config system property. The LoginModule + used is a JVM specific one so ensure that the LoginModule specified matches + the JVM being used. The name of the login configuration must match the + value used by the authentication + valve.
    • +
    +

    The SPNEGO authenticator will work with any + Realm but if used with the JNDI Realm, by default the JNDI Realm will use + the user's delegated credentials to connect to the Active Directory. If + only the authenticated user name is required then the AuthenticatedUserRealm + may be used that will simply return a Principal based on the authenticated + user name that does not have any roles.

    +

    The above steps have been tested on a Tomcat server running Windows Server + 2019 Standard with AdoptOpenJDK 8u232-b09 (64-bit).

    +
    + + +

    This was tested with:

    +
      +
    • Java 1.7.0, update 45, 64-bit
    • +
    • Ubuntu Server 12.04.3 LTS 64-bit
    • +
    • Tomcat 8.0.x (r1546570)
    • +
    +

    It should work with any Tomcat release although it is recommended that + the latest stable release is used.

    +

    The configuration is the same as for Windows but with the following + changes:

    +
      +
    • The Linux server does not have to be part of the Windows domain.
    • +
    • The path to the keytab file in krb5.ini and jaas.conf should be updated + to reflect the path to the keytab file on the Linux server using Linux + style file paths (e.g. /usr/local/tomcat/...).
    • +
    +
    + + +

    The web application needs to be configured to the use Tomcat specific + authentication method of SPNEGO (rather than BASIC etc.) in + web.xml. As with the other authenticators, behaviour can be customised by + explicitly configuring the + authentication valve and setting attributes on the Valve.

    +
    + + +

    The client must be configured to use Kerberos authentication. For Internet + Explorer this means making sure that the Tomcat instance is in the "Local + intranet" security domain and that it is configured (Tools > Internet + Options > Advanced) with integrated Windows authentication enabled. Note that + this will not work if you use the same machine for the client + and the Tomcat instance as Internet Explorer will use the unsupported NTLM + protocol.

    +
    + + +

    Correctly configuring Kerberos authentication can be tricky. The following + references may prove helpful. Advice is also always available from the + Tomcat users + mailing list.

    +
      +
    1. + IIS and Kerberos
    2. +
    3. + SPNEGO project at SourceForge
    4. +
    5. + Oracle Java GSS-API tutorial (Java 7)
    6. +
    7. + Oracle Java GSS-API tutorial - Troubleshooting (Java 7)
    8. +
    9. + Geronimo configuration for Windows authentication
    10. +
    11. + Encryption Selection in Kerberos Exchanges
    12. +
    13. Supported Kerberos Cipher + Suites
    14. +
    +
    + +
    + +
    + + +

    Full details of this solution can be found through the + Waffle web site. The + key features are:

    +
      +
    • Drop-in solution
    • +
    • Simple configuration (no JAAS or Kerberos keytab configuration required) +
    • +
    • Uses a native library
    • +
    +
    + + +

    Full details of this solution can be found through the + Kerberos extension web site. The key features are:

    +
      +
    • Extension to Spring Security
    • +
    • Requires a Kerberos keytab file to be generated
    • +
    • Pure Java solution
    • +
    +
    + + +

    Full details of this solution can be found through the + project web site. The key + features are:

    +
      +
    • Pure Java solution
    • +
    • Advanced Active Directory integration
    • +
    +
    + + +

    Full details of this solution can be found through the + project + site. The key features are:

    +
      +
    • Pure Java solution
    • +
    • SPNEGO/Kerberos Authenticator
    • +
    • Active Directory Realm
    • +
    +
    +
    + +
    + + +

    There are three steps to configuring IIS to provide Windows authentication. + They are:

    +
      +
    1. Configure IIS as a reverse proxy for Tomcat (see the + + IIS Web Server How-To).
    2. +
    3. Configure IIS to use Windows authentication
    4. +
    5. Configure Tomcat to use the authentication user information from IIS by + setting the tomcatAuthentication attribute on the + AJP connector to false. Alternatively, set the + tomcatAuthorization attribute to true to allow IIS to + authenticate, while Tomcat performs the authorization.
    6. +
    +
    + + +

    Apache httpd does not support Windows authentication out of the box but + there are a number of third-party modules that can be used. These include:

    +
      +
    1. mod_auth_sspi for use on Windows platforms.
    2. +
    3. mod_auth_ntlm_winbind for non-Windows platforms. Known to + work with httpd 2.0.x on 32-bit platforms. Some users have reported stability + issues with both httpd 2.2.x builds and 64-bit Linux builds.
    4. +
    +

    There are three steps to configuring httpd to provide Windows + authentication. They are:

    +
      +
    1. Configure httpd as a reverse proxy for Tomcat (see the + + Apache httpd Web Server How-To).
    2. +
    3. Configure httpd to use Windows authentication
    4. +
    5. Configure Tomcat to use the authentication user information from httpd by + setting the tomcatAuthentication attribute on the + AJP connector to false.
    6. +
    +
    + +
    + + +
    diff --git a/webapps/docs/windows-service-howto.xml b/webapps/docs/windows-service-howto.xml new file mode 100644 index 0000000..96ddb1b --- /dev/null +++ b/webapps/docs/windows-service-howto.xml @@ -0,0 +1,585 @@ + + + +]> + + + &project; + + + Mladen Turk + Windows Service How-To + + + + +
    + +
    + +
    + +

    Tomcatw is a GUI application for monitoring and + configuring Tomcat services.

    + + + +

    Each command line directive is in the form of + //XX[//ServiceName]

    + +

    If the //ServiceName component is omitted, then the service + name is assumed to be the name of the file less the w suffix. So the default + service name is Tomcat.

    + +

    The available command line directives are:

    + + + + + + + + + + + + + + + + + + + + + + +
    //ESEdit service configurationThis is the default operation. It is called if the no option is + provided. Starts the GUI application which allows the service + configuration to be modified, started and stopped.
    //MSMonitor serviceStarts the GUI application and minimizes it to the system tray.
    //MRMonitor & run serviceStarts the GUI application and minimizes it to the system tray. Start + the service if it is not currently running.
    //MQMonitor quitStop any running monitor for the service.
    +
    + +
    +
    + +

    Tomcat is a service application for running Tomcat + as a Windows service.

    + + + +

    Each command line directive is in the form of + //XX[//ServiceName]

    + +

    The available command line directives are:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    //TSRun the service as console applicationThis is the default operation. It is called if the no option is + provided. The ServiceName is the name of the executable without + exe suffix, meaning Tomcat
    //RSRun the serviceCalled only from ServiceManager
    //ESStart (execute) the service
    //SSStop the service
    //USUpdate service parameters
    //ISInstall service
    //DSDelete serviceStops the service if running
    //PSPrint servicePrints the command to (re-)create the current configuration
    //PP[//seconds]Pause serviceDefault is 60 seconds
    //VSVersionPrint version and exit
    //?HelpPrint usage and exit
    +
    + + + +

    Each command line parameter is prefixed with --. If the + command line parameter is prefixed with ++, and the parameter + supports multiple values, then it's value will be appended to the existing + option. In the table below, parameters that support multiple values are + prefixed with ++.

    + +

    If the environment variable with the same name as command line parameter + but prefixed with PR_ exists it will take precedence. For + example:

    + + set PR_CLASSPATH=xx.jar + +

    is equivalent to providing

    + + --Classpath=xx.jar + +

    as command line parameter.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ParameterNameDefaultDescription
    --DescriptionService name description (maximum 1024 characters)
    --DisplayNameServiceNameService display name
    --Installprocrun.exe //RS//ServiceNameInstall image
    --StartupmanualService startup mode can be either auto or manual
    ++DependsOnList of services that this service depend on. Dependent services are + separated using either # or ; characters
    ++EnvironmentList of environment variables that will be provided to the service + in the form key=value. They are separated using either + # or ; characters. If you need to use either the + # or ; character within a value then the entire value + must be enclosed inside single quotes.
    --UserUser account used for running executable. It is used only for + StartMode java or exe and enables running applications + as service under account without LogonAsService privilege.
    --PasswordPassword for user account set by --User parameter
    --ServiceUserSpecifies the name of the account under which the service should + run. Use an account name in the form + DomainName\UserName. The service process will be logged + on as this user. if the account belongs to the built-in domain, you + can specify .\UserName. Note that the Service Control + Manager does not accept localised forms of the standard names so to + use them you need to specify NT Authority\LocalService, + NT Authority\NetworkService or LocalSystem + as appropriate.
    --ServicePasswordPassword for user account set by --ServiceUser parameter
    --LibraryPathDirectory added to the search path used to locate the DLLs for the + JVM. This directory is added both in front of the PATH + environment variable and as a parameter to the + SetDLLDirectory function.
    --JavaHomeJAVA_HOMESet a different JAVA_HOME than defined by JAVA_HOME environment + variable
    --JvmautoUse either auto (i.e. find the JVM from the Windows registry) + or specify the full path to the jvm.dll. + You can use the environment variable expansion here.
    ++JvmOptions-XrsList of options in the form of -D or -X that will be + passed to the JVM. The options are separated using either + # or ; characters. If you need to embed either # or + ; characters, put them inside single quotes. (Not used in + exe mode.)
    ++JvmOptions9List of options in the form of -D or -X that will be + passed to the JVM when running on Java 9 or later. The options are + separated using either # or ; characters. If you need + to embed either # or ; characters, put them inside + single quotes. (Not used in exe mode.)
    --ClasspathSet the Java classpath. (Not used in exe mode.)
    --JvmMsInitial memory pool size in MiB. (Not used in exe mode.)
    --JvmMxMaximum memory pool size in MiB. (Not used in exe mode.)
    --JvmSsThread stack size in KiB. (Not used in exe mode.)
    --StartModeOne of jvm, Java or exe. The modes are: +
      +
    • jvm - start Java in-process. Depends on jvm.dll, see + --Jvm.
    • +
    • Java - same as exe, but automatically uses the default Java + executable, i.e. %JAVA_HOME%\bin\java.exe. Make sure JAVA_HOME + is set correctly, or use --JavaHome to provide the correct + location. If neither is set, procrun will try to find the + default JDK (not JRE) from the Windows registry.
    • +
    • exe - run the image as a separate process
    • +
    +
    --StartImageExecutable that will be run. Only applies to exe mode.
    --StartPathWorking path for the start image executable.
    --StartClassMainClass that contains the startup method. Applies to the jvm and + Java modes. (Not used in exe mode.)
    --StartMethodmainMethod name if differs then main
    ++StartParamsList of parameters that will be passed to either StartImage or + StartClass. Parameters are separated using either # or + ; character.
    --StopModeOne of jvm, Java or exe. See --StartMode + for further details.
    --StopImageExecutable that will be run on Stop service signal. Only applies to + exe mode.
    --StopPathWorking path for the stop image executable. Does not apply to + jvm mode.
    --StopClassMainClass that will be used on Stop service signal. Applies to the + jvm and Java modes.
    --StopMethodmainMethod name if differs then main
    --StopParamsList of parameters that will be passed to either StopImage or + StopClass. Parameters are separated using either # or + ; character.
    ++StopTimeoutNo TimeoutDefines the timeout in seconds that procrun waits for service to + exit gracefully.
    --LogPath%SystemRoot%\System32\LogFiles\ApacheDefines the path for logging. Creates the directory if + necessary.
    --LogPrefixcommons-daemonDefines the service log filename prefix. The log file is created in + the LogPath directory with .YEAR-MONTH-DAY.log + suffix
    --LogLevelInfoDefines the logging level and can be either Error, + Info, Warn or Debug. (Case insensitive).
    --LogJniMessages0Set this non-zero (e.g. 1) to capture JVM jni debug messages in the + procrun log file. Is not needed if stdout/stderr redirection is + being used. Only applies to jvm mode.
    --StdOutputRedirected stdout filename. + If named auto then file is created inside LogPath with + the name service-stdout.YEAR-MONTH-DAY.log.
    --StdErrorRedirected stderr filename. + If named auto then file is created inside LogPath with + the name service-stderr.YEAR-MONTH-DAY.log.
    --PidFileDefines the file name for storing the running process id. Actual + file is created in the LogPath directory
    +
    +
    + +
    +

    +The safest way to manually install the service is to use the provided +service.bat script. Administrator privileges are required to run this +script. If necessary, you can use the /user switch to specify +a user to use for the installation of the service. +

    +

    +NOTE: If User Account Control (UAC) is enabled you will be +asked for additional privileges when 'Tomcat.exe' is launched by +the script.
    +If you want to pass additional options to service installer as +PR_* environment variables, you have to either configure them +globally in OS, or launch the program that sets them with elevated privileges +(e.g. right-click on cmd.exe and select "Run as administrator"; on Windows 8 +(or later) or Windows Server 2012 (or later), you can open an elevated command +prompt for the current directory from the Explorer +by clicking on the "File" menu bar). See issue 56143 for details. +

    + +Install the service named 'Tomcat' +C:\> service.bat install + +

    There is a 2nd optional parameter that lets you specify the name of the +service, as displayed in Windows services.

    + +Install the service named 'MyService' +C:\> service.bat install MyService + +

    When installing the service with a non-default name, +tomcat.exe and tomcatw.exe may be renamed to +match the chosen service name. To do this, use the --rename +option.

    + +Install the service named 'MyService' with renaming +C:\> service.bat install MyService --rename + +

    +If using tomcat.exe, you need to use the //IS parameter.

    + +Install the service named 'Tomcat' +C:\> tomcat //IS//Tomcat --DisplayName="Apache Tomcat " ^ + --Install="C:\Program Files\Tomcat\bin\tomcat.exe" --Jvm=auto ^ + --StartMode=jvm --StopMode=jvm ^ + --StartClass=org.apache.catalina.startup.Bootstrap --StartParams=start ^ + --StopClass=org.apache.catalina.startup.Bootstrap --StopParams=stop + +
    +
    +

    +To update the service parameters, you need to use the //US parameter. +

    + +Update the service named 'Tomcat' +C:\> tomcat //US//Tomcat --Description="Apache Tomcat Server - https://tomcat.apache.org/ " ^ + --Startup=auto --Classpath=%JAVA_HOME%\lib\tools.jar;%CATALINA_HOME%\bin\bootstrap.jar + +

    If you gave the service an optional name, you need to specify it like this: +

    + +Update the service named 'MyService' +C:\> tomcat //US//MyService --Description="Apache Tomcat Server - https://tomcat.apache.org/ " ^ + --Startup=auto --Classpath=%JAVA_HOME%\lib\tools.jar;%CATALINA_HOME%\bin\bootstrap.jar + +
    +
    +

    +To remove the service, you need to use the //DS parameter.
    +If the service is running it will be stopped and then deleted.

    + +Remove the service named 'Tomcat' +C:\> tomcat //DS//Tomcat + +

    If you gave the service an optional name, you need to specify it like this: +

    + +Remove the service named 'MyService' +C:\> tomcat //DS//MyService + +
    +
    +

    +To run the service in console mode, you need to use the //TS parameter. +The service shutdown can be initiated by pressing CTRL+C or +CTRL+BREAK. +If you rename the tomcat.exe to testservice.exe then you can just execute the +testservice.exe and this command mode will be executed by default.

    + +Run the service named 'Tomcat' in console mode +C:\> tomcat //TS//Tomcat [additional arguments] +Or simply execute: +C:\> tomcat + +
    +
    +

    +Tomcat supports installation of multiple instances. You can have a single +installation of Tomcat with multiple instances running on different IP/port +combinations, or multiple Tomcat versions, each running one or more instances on +different IP/ports.

    +

    +Each instance folder will need the following structure: +

    +
      +
    • conf
    • +
    • logs
    • +
    • temp
    • +
    • webapps
    • +
    • work
    • +
    +

    +At a minimum, conf should contain a copy of the following files from +CATALINA_HOME\conf\. Any files not copied and edited, will be picked up by +default from CATALINA_HOME\conf, i.e. CATALINA_BASE\conf files override defaults +from CATALINA_HOME\conf.

    +
      +
    • server.xml
    • +
    • web.xml
    • +
    +

    +You must edit CATALINA_BASE\conf\server.xml to specify a unique IP/port for the +instance to listen on. Find the line that contains +<Connector port="8080" ... and add an address attribute and/or +update the port number so as to specify a unique IP/port combination.

    +

    +To install an instance, first set the CATALINA_HOME environment variable to the +name of the Tomcat installation directory. Then create a second environment +variable CATALINA_BASE and point this to the instance folder. Then run +"service.bat install" command specifying a service name.

    + +set CATALINA_HOME=c:\tomcat_ +set CATALINA_BASE=c:\tomcat_\instances\instance1 +service.bat install instance1 + +

    +To modify the service settings, you can run tomcatw //ES//instance1. +

    +

    +For additional instances, create additional instance folder, update the +CATALINA_BASE environment variable, and run the "service.bat install" again.

    + +set CATALINA_BASE=c:\tomcat_\instances\instance2 +service.bat install instance2 + +
    + +
    diff --git a/webapps/examples/META-INF/context.xml b/webapps/examples/META-INF/context.xml new file mode 100644 index 0000000..2ae7e66 --- /dev/null +++ b/webapps/examples/META-INF/context.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/webapps/examples/WEB-INF/classes/CookieExample.java b/webapps/examples/WEB-INF/classes/CookieExample.java new file mode 100644 index 0000000..2ed975d --- /dev/null +++ b/webapps/examples/WEB-INF/classes/CookieExample.java @@ -0,0 +1,140 @@ +/* + * 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. + */ + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ResourceBundle; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import util.CookieFilter; +import util.HTMLFilter; + +/** + * Example servlet showing request headers + * + * @author James Duncan Davidson <duncan@eng.sun.com> + */ + +public class CookieExample extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + ResourceBundle rb = ResourceBundle.getBundle("LocalStrings",request.getLocale()); + + String cookieName = request.getParameter("cookiename"); + String cookieValue = request.getParameter("cookievalue"); + Cookie aCookie = null; + if (cookieName != null && cookieValue != null) { + aCookie = new Cookie(cookieName, cookieValue); + aCookie.setPath(request.getContextPath() + "/"); + response.addCookie(aCookie); + } + + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + PrintWriter out = response.getWriter(); + out.println(""); + out.println(""); + out.println(""); + + String title = rb.getString("cookies.title"); + out.println("" + title + ""); + out.println(""); + out.println(""); + + // relative links + + // XXX + // making these absolute till we work out the + // addition of a PathInfo issue + + out.println(""); + out.println("\"view"); + out.println(""); + out.println("\"return\""); + + out.println("

    " + title + "

    "); + + Cookie[] cookies = request.getCookies(); + if ((cookies != null) && (cookies.length > 0)) { + HttpSession session = request.getSession(false); + String sessionId = null; + if (session != null) { + sessionId = session.getId(); + } + out.println(rb.getString("cookies.cookies") + "
    "); + for (Cookie cookie : cookies) { + String cName = cookie.getName(); + String cValue = cookie.getValue(); + out.print("Cookie Name: " + HTMLFilter.filter(cName) + "
    "); + out.println(" Cookie Value: " + + HTMLFilter.filter(CookieFilter.filter(cName, cValue, sessionId)) + + "

    "); + } + } else { + out.println(rb.getString("cookies.no-cookies")); + } + + if (aCookie != null) { + out.println("

    "); + out.println(rb.getString("cookies.set") + "
    "); + out.print(rb.getString("cookies.name") + " " + + HTMLFilter.filter(cookieName) + "
    "); + out.print(rb.getString("cookies.value") + " " + + HTMLFilter.filter(cookieValue)); + } + + out.println("

    "); + out.println(rb.getString("cookies.make-cookie") + "
    "); + out.print("

    "); + out.print(rb.getString("cookies.name") + " "); + out.println("
    "); + out.print(rb.getString("cookies.value") + " "); + out.println("
    "); + out.println("
    "); + + + out.println(""); + out.println(""); + } + + @Override + public void doPost(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + doGet(request, response); + } + +} + + diff --git a/webapps/examples/WEB-INF/classes/HelloWorldExample.java b/webapps/examples/WEB-INF/classes/HelloWorldExample.java new file mode 100644 index 0000000..19daf6c --- /dev/null +++ b/webapps/examples/WEB-INF/classes/HelloWorldExample.java @@ -0,0 +1,79 @@ +/* + * 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. + */ +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ResourceBundle; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * The simplest possible servlet. + * + * @author James Duncan Davidson + */ + +public class HelloWorldExample extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + ResourceBundle rb = + ResourceBundle.getBundle("LocalStrings",request.getLocale()); + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + PrintWriter out = response.getWriter(); + + out.println(""); + out.println(""); + out.println(""); + + String title = rb.getString("helloworld.title"); + + out.println("" + title + ""); + out.println(""); + out.println(""); + + // note that all links are created to be relative. this + // ensures that we can move the web application that this + // servlet belongs to a different place in the url + // tree and not have any harmful side effects. + + // XXX + // making these absolute till we work out the + // addition of a PathInfo issue + + out.println(""); + out.println("\"view"); + out.println(""); + out.println("\"return\""); + out.println("

    " + title + "

    "); + out.println(""); + out.println(""); + } +} + + + diff --git a/webapps/examples/WEB-INF/classes/LocalStrings.properties b/webapps/examples/WEB-INF/classes/LocalStrings.properties new file mode 100644 index 0000000..2791a66 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings.properties @@ -0,0 +1,48 @@ +# 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. + +cookies.cookies=Your browser is sending the following cookies: +cookies.make-cookie=Create a cookie to send to your browser +cookies.name=Name: +cookies.no-cookies=Your browser isn't sending any cookies +cookies.set=You just sent the following cookie to your browser: +cookies.title=Cookies Example +cookies.value=Value: + +helloworld.title=Hello World! + +requestheader.title=Request Header Example + +requestinfo.label.method=Method: +requestinfo.label.pathinfo=Path Info: +requestinfo.label.protocol=Protocol: +requestinfo.label.remoteaddr=Remote Address: +requestinfo.label.requesturi=Request URI: +requestinfo.title=Request Information Example + +requestparams.firstname=First Name: +requestparams.lastname=Last Name: +requestparams.no-params=No Parameters, Please enter some +requestparams.params-in-req=Parameters in this request: +requestparams.title=Request Parameters Example + +sessions.adddata=Add data to your session +sessions.created=Created: +sessions.data=The following data is in your session: +sessions.dataname=Name of Session Attribute: +sessions.datavalue=Value of Session Attribute: +sessions.id=Session ID: +sessions.lastaccessed=Last Accessed: +sessions.title=Sessions Example diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_cs.properties b/webapps/examples/WEB-INF/classes/LocalStrings_cs.properties new file mode 100644 index 0000000..c06a261 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_cs.properties @@ -0,0 +1,22 @@ +# 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. + +cookies.make-cookie=VytvoÅ™te cookie pro zaslání do VaÅ¡eho prohlížeÄe + +requestheader.title=Příklad hlaviÄky dotazu + +requestparams.firstname=KÅ™estní jméno: +requestparams.no-params=Žádný parametr, vložte nÄ›jaký prosím +requestparams.title=Příklad parametrů dotazu diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_de.properties b/webapps/examples/WEB-INF/classes/LocalStrings_de.properties new file mode 100644 index 0000000..9f94299 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_de.properties @@ -0,0 +1,28 @@ +# 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. + +cookies.make-cookie=Erzeuge ein Cookie um es an deinen Browser zu senden +cookies.name=Name: + +requestheader.title=Request-Header Beispiel + +requestinfo.label.protocol=Protokoll: +requestinfo.label.requesturi=Anfrage-URI: + +requestparams.firstname=Vorname: +requestparams.no-params=Keine Parameter, bitte geben Sie welche ein +requestparams.title=Beispiel für Anfrageparameter + +sessions.title=Sessions-Beispiel diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_es.properties b/webapps/examples/WEB-INF/classes/LocalStrings_es.properties new file mode 100644 index 0000000..eaac59a --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_es.properties @@ -0,0 +1,48 @@ +# 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. + +cookies.cookies=Tu navegador está enviando los siguientes cookies: +cookies.make-cookie=Crea un cookie para enviarlo a tu navegador +cookies.name=Nombre: +cookies.no-cookies=Tu navegador no está enviando cookies +cookies.set=Acabas de enviar a tu navegador estos cookies: +cookies.title=Ejemplo de Cookies +cookies.value=Valor: + +helloworld.title=Hola Mundo! + +requestheader.title=Ejemplo de Cabecera de Requerimiento: + +requestinfo.label.method=Método: +requestinfo.label.pathinfo=Info de Ruta: +requestinfo.label.protocol=Protocolo: +requestinfo.label.remoteaddr=Direccion Remota: +requestinfo.label.requesturi=URI de Requerimiento: +requestinfo.title=Ejemplo de Informacion de Requerimiento: + +requestparams.firstname=Nombre: +requestparams.lastname=Apellidos: +requestparams.no-params=No hay párametro. Por favor, usa alguno +requestparams.params-in-req=Parámetros en este Request: +requestparams.title=Ejemplo de solicitud con parámetros: + +sessions.adddata=Añade datos a tu sesión: +sessions.created=Creado: +sessions.data=Lo siguientes datos están en tu sesión: +sessions.dataname=Nombre del atributo de sesión: +sessions.datavalue=Valor del atributo de sesión: +sessions.id=ID de Sesión: +sessions.lastaccessed=Ultimo Acceso: +sessions.title=Ejemplo de Sesiones diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_fr.properties b/webapps/examples/WEB-INF/classes/LocalStrings_fr.properties new file mode 100644 index 0000000..e0c0060 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_fr.properties @@ -0,0 +1,48 @@ +# 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. + +cookies.cookies=Votre navigateur retourne les cookies suivant : +cookies.make-cookie=Création d'un cookie à retourner à votre navigateur +cookies.name=Nom : +cookies.no-cookies=Votre navigateur ne retourne aucun cookie +cookies.set=Vous venez d'envoyer le cookie suivant à votre navigateur : +cookies.title=Exemple d'utilisation de Cookies +cookies.value=Valeur : + +helloworld.title=Salut le Monde ! + +requestheader.title=Exemple d'information sur les entêtes de requête + +requestinfo.label.method=Méthode : +requestinfo.label.pathinfo=Info de chemin : +requestinfo.label.protocol=Protocole : +requestinfo.label.remoteaddr=Adresse distante : +requestinfo.label.requesturi=URI de requête : +requestinfo.title=Exemple d'information sur la requête + +requestparams.firstname=Prénom : +requestparams.lastname=Nom : +requestparams.no-params=Pas de paramêtre, merci d'en saisir quelques-uns +requestparams.params-in-req=Paramêtres dans la requête : +requestparams.title=Exemple de Requête avec Paramètres + +sessions.adddata=Ajouter des données à votre session +sessions.created=Crée le : +sessions.data=Les données existantes dans votre session : +sessions.dataname=Nom de l'Attribut de Session : +sessions.datavalue=Valeur de l'Attribut de Session : +sessions.id=ID de Session : +sessions.lastaccessed=Dernier accès : +sessions.title=Exemple de Sessions diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_ja.properties b/webapps/examples/WEB-INF/classes/LocalStrings_ja.properties new file mode 100644 index 0000000..4e57162 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_ja.properties @@ -0,0 +1,48 @@ +# 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. + +cookies.cookies=ã‚ãªãŸã®ã®ãƒ–ラウザã‹ã‚‰æ¬¡ã®CookieãŒé€ä¿¡ã•ã‚Œã¦ã„ã¾ã™ï¼š +cookies.make-cookie=ブラウザã¸é€ä¿¡ã™ã‚‹ cookie を作æˆã—ã¾ã™ã€‚ +cookies.name=Name: +cookies.no-cookies=ã‚ãªãŸã®ãƒ–ラウザã¯ã‚¯ãƒƒã‚­ãƒ¼ã‚’é€ä¿¡ã—ã¦ã„ã¾ã›ã‚“。 +cookies.set=ブラウザ㫠cookie ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚ +cookies.title=Cookie 例 +cookies.value=値: + +helloworld.title=Hello World! + +requestheader.title=リクエストヘッダ例 + +requestinfo.label.method=メソッド: +requestinfo.label.pathinfo=パス情報: +requestinfo.label.protocol=プロトコル: +requestinfo.label.remoteaddr=リモートアドレス: +requestinfo.label.requesturi=Request URI: +requestinfo.title=リクエスト情報例 + +requestparams.firstname=First Name: +requestparams.lastname=Last Name: +requestparams.no-params=パラメータãŒã‚ã‚Šã¾ã›ã‚“。何ã‹å…¥åŠ›ã—ã¦ãã ã•ã„。 +requestparams.params-in-req=ã“ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®ãƒ‘ラメータ: +requestparams.title=リクエストパラメータ例 + +sessions.adddata=セッションã«ãƒ‡ãƒ¼ã‚¿ã‚’追加ã—ã¾ã™ +sessions.created=作æˆï¼š +sessions.data=ã‚ãªãŸã®ã‚»ãƒƒã‚·ãƒ§ãƒ³ã«ã¯æ¬¡ã®ãƒ‡ãƒ¼ã‚¿ãŒã‚ã‚Šã¾ã™ï¼š +sessions.dataname=セッション属性å: +sessions.datavalue=セッション属性ã®å€¤ï¼š +sessions.id=セッションID +sessions.lastaccessed=最終アクセス: +sessions.title=セッション例 diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_ko.properties b/webapps/examples/WEB-INF/classes/LocalStrings_ko.properties new file mode 100644 index 0000000..5999f05 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_ko.properties @@ -0,0 +1,48 @@ +# 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. + +cookies.cookies=ê·€í•˜ì˜ ë¸Œë¼ìš°ì €ê°€ ë‹¤ìŒ ì¿ í‚¤ë“¤ì„ ë³´ëƒ…ë‹ˆë‹¤. +cookies.make-cookie=ê·€í•˜ì˜ ë¸Œë¼ìš°ì €ì— 전송하기 위한 쿠키 ìƒì„± +cookies.name=ì´ë¦„: +cookies.no-cookies=ê·€í•˜ì˜ ë¸Œë¼ìš°ì €ëŠ” ì–´ë–¤ ì¿ í‚¤ë„ ì „ì†¡í•˜ì§€ 않습니다. +cookies.set=귀하는 ë‹¤ìŒ ì¿ í‚¤ë¥¼, ê·€í•˜ì˜ ë¸Œë¼ìš°ì €ì— 전송했습니다. +cookies.title=ì¿ í‚¤ë“¤ì˜ ì˜ˆì œ +cookies.value=ê°’: + +helloworld.title=안녕 세계여! + +requestheader.title=ìš”ì²­ì˜ í—¤ë” ì˜ˆì œ + +requestinfo.label.method=메소드: +requestinfo.label.pathinfo=경로 ì •ë³´: +requestinfo.label.protocol=프로토콜: +requestinfo.label.remoteaddr=ì›ê²© 주소: +requestinfo.label.requesturi=요청 URI: +requestinfo.title=요청 ì •ë³´ 예제 + +requestparams.firstname=ì´ë¦„: +requestparams.lastname=성 +requestparams.no-params=파ë¼ë¯¸í„°ë“¤ì´ 없습니다. 파ë¼ë¯¸í„°ë“¤ì„ 입력하십시오. +requestparams.params-in-req=ì´ ìš”ì²­ì˜ íŒŒë¼ë¯¸í„°ë“¤: +requestparams.title=요청 파ë¼ë¯¸í„°ë“¤ì˜ 예제 + +sessions.adddata=ê·€í•˜ì˜ ì„¸ì…˜ì— ë°ì´í„°ë¥¼ 추가 +sessions.created=ìƒì„±ì‹œê°„: +sessions.data=ê·€í•˜ì˜ ì„¸ì…˜ì— ë‹¤ìŒ ë°ì´í„°ê°€ 있습니다: +sessions.dataname=세션 ì†ì„± ì´ë¦„: +sessions.datavalue=세션 ì†ì„± ê°’: +sessions.id=세션 ID: +sessions.lastaccessed=최종 ì ‘ê·¼ 시간: +sessions.title=ì„¸ì…˜ë“¤ì˜ ì˜ˆì œ diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_pt.properties b/webapps/examples/WEB-INF/classes/LocalStrings_pt.properties new file mode 100644 index 0000000..b432741 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_pt.properties @@ -0,0 +1,48 @@ +# 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. + +cookies.cookies=O se browser esta a enviar os seguintes cookies: +cookies.make-cookie=Crie um cookie para enviar para o seu browser +cookies.name=Nome: +cookies.no-cookies=O seu browser nao esta a enviar nenhuns cookies +cookies.set=Acabou de enviar o seguinte cookie para o seu browser: +cookies.title=CExamplo de Cookies +cookies.value=Valor: + +helloworld.title=Ola Mundo! + +requestheader.title=Exemplo da Cebeceira do Pedido + +requestinfo.label.method=Metodo: +requestinfo.label.pathinfo=Informacao do Caminho: +requestinfo.label.protocol=Protocolo: +requestinfo.label.remoteaddr=Endereco Remoto: +requestinfo.label.requesturi=URI do Pedido: +requestinfo.title=Exemplo da Informacao do Pedido + +requestparams.firstname=Primeiro Nome: +requestparams.lastname=Apelido: +requestparams.no-params=Sem Parametros, Por favor entre alguns +requestparams.params-in-req=Parametros neste pedido: +requestparams.title=Examplo de Parametros do Pedido + +sessions.adddata=Adicione data a sua sessao +sessions.created=Criada: +sessions.data=Os seguintes dados fazem parte da sua sessao: +sessions.dataname=Nome do atributo da sessao: +sessions.datavalue=Valor do atributo da Sessao: +sessions.id=Identificador da Sessao: +sessions.lastaccessed=Ultima vez acedida: +sessions.title=Examplo de sessoes diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_pt_BR.properties b/webapps/examples/WEB-INF/classes/LocalStrings_pt_BR.properties new file mode 100644 index 0000000..83bdac6 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_pt_BR.properties @@ -0,0 +1,16 @@ +# 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. + +requestparams.title=Exemplo de Parâmetros de Requisição diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_ru.properties b/webapps/examples/WEB-INF/classes/LocalStrings_ru.properties new file mode 100644 index 0000000..b674ee5 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_ru.properties @@ -0,0 +1,47 @@ +# 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. + +cookies.cookies=Ваш браузер отправлÑет Ñледующие куки: +cookies.make-cookie=Создайте куку Ð´Ð»Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸ в ваш браузер +cookies.name=ИмÑ: +cookies.no-cookies=Ваш браузер не отправлÑет никаких кук. +cookies.set=Ð’Ñ‹ только что отправили Ñледующую куку в ваш браузер: +cookies.value=Значение: + +helloworld.title=Hello World! + +requestheader.title=Пример заголовка запроÑа + +requestinfo.label.method=Метод: +requestinfo.label.pathinfo=Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ пути: +requestinfo.label.protocol=Протокол: +requestinfo.label.remoteaddr=Удаленный адреÑ: +requestinfo.label.requesturi=URI запроÑа: +requestinfo.title=Пример информации о запроÑе + +requestparams.firstname=ИмÑ: +requestparams.lastname=ФамилиÑ: +requestparams.no-params=Ðет параметров, ПожалуйÑта добавьте неÑколько +requestparams.params-in-req=Параметры в Ñтом запроÑе: +requestparams.title=Пример параметров запроÑа + +sessions.adddata=Добавьте данные в вашу ÑеÑÑию +sessions.created=Создано: +sessions.data=Ð’ вашей ÑеÑÑии находÑÑ‚ÑÑ Ñледующие данные: +sessions.dataname=Ð˜Ð¼Ñ Ð°Ñ‚Ñ€Ð¸Ð±ÑƒÑ‚Ð° ÑеÑÑии: +sessions.datavalue=Значение атрибута ÑеÑÑии: +sessions.id=ID СеÑÑии: +sessions.lastaccessed=ПоÑледнее обращение: +sessions.title=Пример ÑеÑÑий diff --git a/webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties b/webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties new file mode 100644 index 0000000..fa9f447 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties @@ -0,0 +1,48 @@ +# 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. + +cookies.cookies=ä½ çš„æµè§ˆå™¨æ­£åœ¨å‘é€ä¸‹é¢çš„cookie: +cookies.make-cookie=创建一个å‘é€åˆ°ä½ çš„æµè§ˆå™¨çš„cookie +cookies.name=å.称: +cookies.no-cookies=ä½ çš„æµè§ˆå™¨æœªå‘é€ä»»ä½•cookie +cookies.set=你刚刚将以下cookieå‘é€åˆ°ä½ çš„æµè§ˆå™¨ï¼š +cookies.title=cookie示例 +cookies.value=值: + +helloworld.title=你好,世界. + +requestheader.title=请求 Header 示例 + +requestinfo.label.method=方法: +requestinfo.label.pathinfo=路径信æ¯ï¼š +requestinfo.label.protocol=å议: +requestinfo.label.remoteaddr=远程地å€ï¼š +requestinfo.label.requesturi=请求 URI (: +requestinfo.title=请求信æ¯èŒƒä¾‹ + +requestparams.firstname=姓: +requestparams.lastname=姓æ°ï¼š +requestparams.no-params=没有å‚数,请输入一些 +requestparams.params-in-req=å‚数在请求中. +requestparams.title=请求å‚数示例 + +sessions.adddata=将数æ®æ·»åŠ åˆ°ä½ çš„会è¯ä¸­ +sessions.created=已创建的: +sessions.data=以下数æ®åœ¨æ‚¨çš„会è¯ä¸­ï¼š +sessions.dataname=会è¯å±žæ€§å: +sessions.datavalue=会è¯å±žæ€§å€¼ï¼š +sessions.id=会è¯ID: +sessions.lastaccessed=最åŽè®¿é—®ï¼š +sessions.title=会è¯.示例 diff --git a/webapps/examples/WEB-INF/classes/RequestHeaderExample.java b/webapps/examples/WEB-INF/classes/RequestHeaderExample.java new file mode 100644 index 0000000..eaa2f1a --- /dev/null +++ b/webapps/examples/WEB-INF/classes/RequestHeaderExample.java @@ -0,0 +1,184 @@ +/* + * 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. + */ + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.apache.tomcat.util.json.JSONFilter; + +import util.CookieFilter; +import util.HTMLFilter; + +/** + * Example servlet showing request headers + * + * @author James Duncan Davidson <duncan@eng.sun.com> + */ +public class RequestHeaderExample extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + if (prefersJSON(request.getHeader("Accept"))) { + renderJSON(request, response); + } else { + renderHTML(request, response); + } + } + + /** + * Returns true if the client appears to prefer a JSON response, + * false otherwise. + * + * Note that this method is not very pedantic and uses only a very lazy + * algorithm for checking whether JSON is "preferred". + * + * @param acceptHeader The value of the HTTP "Accept" header from the client. + * + * @return true if the client appears to prefer a JSON response, + * false otherwise. + */ + protected boolean prefersJSON(String acceptHeader) { + if (null == acceptHeader) { + return false; + } + // mime/type, mime/type;q=n, ... + + // Don't bother with the q-factor. + // This is not expected to be 100% accurate or spec-compliant + String[] accepts = acceptHeader.split(","); + for (String accept : accepts) { + if (accept.contains("application/json")) { + return true; + } + + // text/html, application/html, etc. + if (accept.contains("html")) { + return false; + } + } + return false; + } + + protected void renderHTML(HttpServletRequest request, + HttpServletResponse response) + throws IOException + { + ResourceBundle rb = ResourceBundle.getBundle("LocalStrings",request.getLocale()); + + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + PrintWriter out = response.getWriter(); + out.println(""); + out.println(""); + out.println(""); + + String title = rb.getString("requestheader.title"); + out.println("" + title + ""); + out.println(""); + out.println(""); + + // all links relative + + // XXX + // making these absolute till we work out the + // addition of a PathInfo issue + + out.println(""); + out.println("\"view"); + out.println(""); + out.println("\"return\""); + + out.println("

    " + title + "

    "); + out.println(""); + Enumeration e = request.getHeaderNames(); + while (e.hasMoreElements()) { + String headerName = e.nextElement(); + String headerValue = request.getHeader(headerName); + out.println(""); + } + out.println("
    "); + out.println(HTMLFilter.filter(headerName)); + out.println(""); + if (headerName.toLowerCase(Locale.ENGLISH).contains("cookie")) { + HttpSession session = request.getSession(false); + String sessionId = null; + if (session != null) { + sessionId = session.getId(); + } + out.println(HTMLFilter.filter(CookieFilter.filter(headerValue, sessionId))); + } else { + out.println(HTMLFilter.filter(headerValue)); + } + out.println("
    "); + } + + protected void renderJSON(HttpServletRequest request, HttpServletResponse response) + throws IOException + { + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + PrintWriter out = response.getWriter(); + + out.append('['); + Enumeration e = request.getHeaderNames(); + while (e.hasMoreElements()) { + String headerName = e.nextElement(); + String headerValue = request.getHeader(headerName); + + out.append("{\"") + .append(JSONFilter.escape(headerName)) + .append("\":\"") + .append(JSONFilter.escape(headerValue)) + .append("\"}") + ; + + if(e.hasMoreElements()) { + out.append(','); + } + } + + out.print("]"); + } + + @Override + public void doPost(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + doGet(request, response); + } + +} + diff --git a/webapps/examples/WEB-INF/classes/RequestInfoExample.java b/webapps/examples/WEB-INF/classes/RequestInfoExample.java new file mode 100644 index 0000000..7b5e18a --- /dev/null +++ b/webapps/examples/WEB-INF/classes/RequestInfoExample.java @@ -0,0 +1,118 @@ +/* + * 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. + */ + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ResourceBundle; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import util.HTMLFilter; + +/** + * Example servlet showing request information. + * + * @author James Duncan Davidson <duncan@eng.sun.com> + */ + +public class RequestInfoExample extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + ResourceBundle rb = ResourceBundle.getBundle("LocalStrings",request.getLocale()); + + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + PrintWriter out = response.getWriter(); + out.println(""); + out.println(""); + out.println(""); + + String title = rb.getString("requestinfo.title"); + out.println("" + title + ""); + out.println(""); + out.println(""); + + // img stuff not req'd for source code HTML showing + // all links relative! + + // XXX + // making these absolute till we work out the + // addition of a PathInfo issue + + out.println(""); + out.println("\"view"); + out.println(""); + out.println("\"return\""); + + out.println("

    " + title + "

    "); + out.println(""); + + String cipherSuite= + (String)request.getAttribute("jakarta.servlet.request.cipher_suite"); + if(cipherSuite!=null){ + out.println(""); + } + + out.println("
    "); + out.println(rb.getString("requestinfo.label.method")); + out.println(""); + out.println(HTMLFilter.filter(request.getMethod())); + out.println("
    "); + out.println(rb.getString("requestinfo.label.requesturi")); + out.println(""); + out.println(HTMLFilter.filter(request.getRequestURI())); + out.println("
    "); + out.println(rb.getString("requestinfo.label.protocol")); + out.println(""); + out.println(HTMLFilter.filter(request.getProtocol())); + out.println("
    "); + out.println(rb.getString("requestinfo.label.pathinfo")); + out.println(""); + out.println(HTMLFilter.filter(request.getPathInfo())); + out.println("
    "); + out.println(rb.getString("requestinfo.label.remoteaddr")); + out.println(""); + out.println(HTMLFilter.filter(request.getRemoteAddr())); + out.println("
    "); + out.println("SSLCipherSuite:"); + out.println(""); + out.println(HTMLFilter.filter(cipherSuite)); + out.println("
    "); + } + + @Override + public void doPost(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + doGet(request, response); + } + +} + diff --git a/webapps/examples/WEB-INF/classes/RequestParamExample.java b/webapps/examples/WEB-INF/classes/RequestParamExample.java new file mode 100644 index 0000000..4c4e668 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/RequestParamExample.java @@ -0,0 +1,111 @@ +/* + * 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. + */ + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ResourceBundle; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import util.HTMLFilter; + +/** + * Example servlet showing request headers + * + * @author James Duncan Davidson <duncan@eng.sun.com> + */ + +public class RequestParamExample extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + ResourceBundle rb = ResourceBundle.getBundle("LocalStrings",request.getLocale()); + + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + PrintWriter out = response.getWriter(); + out.println(""); + out.println(""); + out.println(""); + + String title = rb.getString("requestparams.title"); + out.println("" + title + ""); + out.println(""); + out.println(""); + + // img stuff not req'd for source code HTML showing + + // all links relative + + // XXX + // making these absolute till we work out the + // addition of a PathInfo issue + + out.println(""); + out.println("\"view"); + out.println(""); + out.println("\"return\""); + + out.println("

    " + title + "

    "); + String firstName = request.getParameter("firstname"); + String lastName = request.getParameter("lastname"); + out.println(rb.getString("requestparams.params-in-req") + "
    "); + if (firstName != null || lastName != null) { + out.println(rb.getString("requestparams.firstname")); + out.println(" = " + HTMLFilter.filter(firstName) + "
    "); + out.println(rb.getString("requestparams.lastname")); + out.println(" = " + HTMLFilter.filter(lastName)); + } else { + out.println(rb.getString("requestparams.no-params")); + } + out.println("

    "); + out.print("

    "); + out.println(rb.getString("requestparams.firstname")); + out.println(""); + out.println("
    "); + out.println(rb.getString("requestparams.lastname")); + out.println(""); + out.println("
    "); + out.println(""); + out.println("
    "); + + out.println(""); + out.println(""); + } + + @Override + public void doPost(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + doGet(request, response); + } + +} diff --git a/webapps/examples/WEB-INF/classes/ServletToJsp.java b/webapps/examples/WEB-INF/classes/ServletToJsp.java new file mode 100644 index 0000000..7d31b71 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/ServletToJsp.java @@ -0,0 +1,39 @@ +/* + * 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. + */ + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class ServletToJsp extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet (HttpServletRequest request, + HttpServletResponse response) { + + try { + // Set the attribute and Forward to hello.jsp + request.setAttribute ("servletName", "servletToJsp"); + getServletConfig().getServletContext().getRequestDispatcher( + "/jsp/jsptoserv/hello.jsp").forward(request, response); + } catch (Exception ex) { + ex.printStackTrace (); + } + } +} diff --git a/webapps/examples/WEB-INF/classes/SessionExample.java b/webapps/examples/WEB-INF/classes/SessionExample.java new file mode 100644 index 0000000..b7210fd --- /dev/null +++ b/webapps/examples/WEB-INF/classes/SessionExample.java @@ -0,0 +1,147 @@ +/* + * 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. + */ + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; +import java.util.Enumeration; +import java.util.ResourceBundle; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import util.HTMLFilter; + +/** + * Example servlet showing request headers + * + * @author James Duncan Davidson <duncan@eng.sun.com> + */ + +public class SessionExample extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + ResourceBundle rb = ResourceBundle.getBundle("LocalStrings",request.getLocale()); + + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + PrintWriter out = response.getWriter(); + out.println(""); + out.println(""); + out.println(""); + + + String title = rb.getString("sessions.title"); + out.println("" + title + ""); + out.println(""); + out.println(""); + + // img stuff not req'd for source code HTML showing + // relative links everywhere! + + // XXX + // making these absolute till we work out the + // addition of a PathInfo issue + + out.println(""); + out.println("\"view"); + out.println(""); + out.println("\"return\""); + + out.println("

    " + title + "

    "); + + HttpSession session = request.getSession(true); + out.println(rb.getString("sessions.id") + " " + session.getId()); + out.println("
    "); + out.println(rb.getString("sessions.created") + " "); + out.println(new Date(session.getCreationTime()) + "
    "); + out.println(rb.getString("sessions.lastaccessed") + " "); + out.println(new Date(session.getLastAccessedTime())); + + String dataName = request.getParameter("dataname"); + String dataValue = request.getParameter("datavalue"); + if (dataName != null && dataValue != null) { + session.setAttribute(dataName, dataValue); + } + + out.println("

    "); + out.println(rb.getString("sessions.data") + "
    "); + Enumeration names = session.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + String value = session.getAttribute(name).toString(); + out.println(HTMLFilter.filter(name) + " = " + + HTMLFilter.filter(value) + "
    "); + } + + out.println("

    "); + out.print("

    "); + out.println(rb.getString("sessions.dataname")); + out.println(""); + out.println("
    "); + out.println(rb.getString("sessions.datavalue")); + out.println(""); + out.println("
    "); + out.println(""); + out.println("
    "); + + out.println("

    GET based form:
    "); + out.print("

    "); + out.println(rb.getString("sessions.dataname")); + out.println(""); + out.println("
    "); + out.println(rb.getString("sessions.datavalue")); + out.println(""); + out.println("
    "); + out.println(""); + out.println("
    "); + + out.print("

    URL encoded "); + + out.println(""); + out.println(""); + } + + @Override + public void doPost(HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + doGet(request, response); + } + +} diff --git a/webapps/examples/WEB-INF/classes/async/Async0.java b/webapps/examples/WEB-INF/classes/async/Async0.java new file mode 100644 index 0000000..a8d21a0 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/async/Async0.java @@ -0,0 +1,69 @@ +/* + * 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 async; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class Async0 extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(Async0.class); + + @Override + protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { + if (Boolean.TRUE.equals(req.getAttribute("dispatch"))) { + log.info("Received dispatch, completing on the worker thread."); + log.info("After complete called started:"+req.isAsyncStarted()); + Date date = new Date(System.currentTimeMillis()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); + resp.getWriter().write("Async dispatch worked: " + sdf.format(date) + "\n"); + } else { + resp.setContentType("text/plain"); + final AsyncContext actx = req.startAsync(); + actx.setTimeout(Long.MAX_VALUE); + Runnable run = new Runnable() { + @Override + public void run() { + try { + req.setAttribute("dispatch", Boolean.TRUE); + Thread.currentThread().setName("Async0-Thread"); + log.info("Putting AsyncThread to sleep"); + Thread.sleep(2*1000); + log.info("Dispatching"); + actx.dispatch(); + }catch (InterruptedException | IllegalStateException x) { + log.error("Async1",x); + } + } + }; + Thread t = new Thread(run); + t.start(); + } + } +} diff --git a/webapps/examples/WEB-INF/classes/async/Async1.java b/webapps/examples/WEB-INF/classes/async/Async1.java new file mode 100644 index 0000000..746fec0 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/async/Async1.java @@ -0,0 +1,60 @@ +/* + * 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 async; + +import java.io.IOException; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class Async1 extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(Async1.class); + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + final AsyncContext actx = req.startAsync(); + actx.setTimeout(30*1000); + Runnable run = new Runnable() { + @Override + public void run() { + try { + String path = "/jsp/async/async1.jsp"; + Thread.currentThread().setName("Async1-Thread"); + log.info("Putting AsyncThread to sleep"); + Thread.sleep(2*1000); + log.info("Dispatching to "+path); + actx.dispatch(path); + }catch (InterruptedException | IllegalStateException x) { + log.error("Async1",x); + } + } + }; + Thread t = new Thread(run); + t.start(); + } + + +} diff --git a/webapps/examples/WEB-INF/classes/async/Async2.java b/webapps/examples/WEB-INF/classes/async/Async2.java new file mode 100644 index 0000000..efcc6bb --- /dev/null +++ b/webapps/examples/WEB-INF/classes/async/Async2.java @@ -0,0 +1,66 @@ +/* + * 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 async; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public class Async2 extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(Async2.class); + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + final AsyncContext actx = req.startAsync(); + actx.setTimeout(30*1000); + Runnable run = new Runnable() { + @Override + public void run() { + try { + Thread.currentThread().setName("Async2-Thread"); + log.info("Putting AsyncThread to sleep"); + Thread.sleep(2*1000); + log.info("Writing data."); + Date date = new Date(System.currentTimeMillis()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); + actx.getResponse().getWriter().write( + "Output from background thread. Time: " + sdf.format(date) + "\n"); + actx.complete(); + }catch (InterruptedException | IllegalStateException | IOException x) { + log.error("Async2",x); + } + } + }; + Thread t = new Thread(run); + t.start(); + } + + +} diff --git a/webapps/examples/WEB-INF/classes/async/Async3.java b/webapps/examples/WEB-INF/classes/async/Async3.java new file mode 100644 index 0000000..2dcd2bf --- /dev/null +++ b/webapps/examples/WEB-INF/classes/async/Async3.java @@ -0,0 +1,39 @@ +/* + * 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 async; + +import java.io.IOException; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class Async3 extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + final AsyncContext actx = req.startAsync(); + actx.setTimeout(30*1000); + actx.dispatch("/jsp/async/async3.jsp"); + } + + +} diff --git a/webapps/examples/WEB-INF/classes/async/AsyncStockContextListener.java b/webapps/examples/WEB-INF/classes/async/AsyncStockContextListener.java new file mode 100644 index 0000000..032fcad --- /dev/null +++ b/webapps/examples/WEB-INF/classes/async/AsyncStockContextListener.java @@ -0,0 +1,44 @@ +/* + * 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 async; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +/* + * Ensures the Stockticker is shut down cleanly when the context stops. This + * also covers the case when the server shuts down. + */ +public class AsyncStockContextListener implements ServletContextListener { + + public static final String STOCK_TICKER_KEY = "StockTicker"; + + @Override + public void contextInitialized(ServletContextEvent sce) { + Stockticker stockticker = new Stockticker(); + ServletContext sc = sce.getServletContext(); + sc.setAttribute(STOCK_TICKER_KEY, stockticker); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + ServletContext sc = sce.getServletContext(); + Stockticker stockticker = (Stockticker) sc.getAttribute(STOCK_TICKER_KEY); + stockticker.shutdown(); + } +} diff --git a/webapps/examples/WEB-INF/classes/async/AsyncStockServlet.java b/webapps/examples/WEB-INF/classes/async/AsyncStockServlet.java new file mode 100644 index 0000000..f8e1def --- /dev/null +++ b/webapps/examples/WEB-INF/classes/async/AsyncStockServlet.java @@ -0,0 +1,144 @@ +/* + * 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 async; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +import async.Stockticker.Stock; +import async.Stockticker.TickListener; + +public class AsyncStockServlet extends HttpServlet implements TickListener, AsyncListener{ + + private static final long serialVersionUID = 1L; + + private static final Log log = LogFactory.getLog(AsyncStockServlet.class); + + private static final ConcurrentLinkedQueue clients = + new ConcurrentLinkedQueue<>(); + private static final AtomicInteger clientcount = new AtomicInteger(0); + + public AsyncStockServlet() { + log.info("AsyncStockServlet created"); + } + + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (req.isAsyncStarted()) { + req.getAsyncContext().complete(); + } else if (req.isAsyncSupported()) { + AsyncContext actx = req.startAsync(); + actx.addListener(this); + resp.setContentType("text/plain"); + clients.add(actx); + if (clientcount.incrementAndGet()==1) { + Stockticker ticker = (Stockticker) req.getServletContext().getAttribute( + AsyncStockContextListener.STOCK_TICKER_KEY); + ticker.addTickListener(this); + } + } else { + new Exception("Async Not Supported").printStackTrace(); + resp.sendError(400,"Async is not supported."); + } + } + + + @Override + public void tick(Stock stock) { + for (AsyncContext actx : clients) { + try { + writeStock(actx, stock); + } catch (Exception e) { + // Ignore. The async error handling will deal with this. + } + } + } + + + public void writeStock(AsyncContext actx, Stock stock) throws IOException { + HttpServletResponse response = (HttpServletResponse)actx.getResponse(); + PrintWriter writer = response.getWriter(); + writer.write("STOCK#");//make client parsing easier + writer.write(stock.getSymbol()); + writer.write("#"); + writer.write(stock.getValueAsString()); + writer.write("#"); + writer.write(stock.getLastChangeAsString()); + writer.write("#"); + writer.write(String.valueOf(stock.getCnt())); + writer.write("\n"); + writer.flush(); + response.flushBuffer(); + } + + + @Override + public void shutdown() { + // The web application is shutting down. Complete any AsyncContexts + // associated with an active client. + for (AsyncContext actx : clients) { + try { + actx.complete(); + } catch (Exception e) { + // Ignore. The async error handling will deal with this. + } + } + } + + + @Override + public void onComplete(AsyncEvent event) throws IOException { + if (clients.remove(event.getAsyncContext()) && clientcount.decrementAndGet()==0) { + ServletContext sc = event.getAsyncContext().getRequest().getServletContext(); + Stockticker ticker = (Stockticker) sc.getAttribute( + AsyncStockContextListener.STOCK_TICKER_KEY); + ticker.removeTickListener(this); + } + } + + @Override + public void onError(AsyncEvent event) throws IOException { + event.getAsyncContext().complete(); + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + event.getAsyncContext().complete(); + } + + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + // NOOP + } +} diff --git a/webapps/examples/WEB-INF/classes/async/Stockticker.java b/webapps/examples/WEB-INF/classes/async/Stockticker.java new file mode 100644 index 0000000..d3488f6 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/async/Stockticker.java @@ -0,0 +1,212 @@ +/* + * 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 async; + +import java.text.DecimalFormat; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +public class Stockticker implements Runnable { + public volatile boolean run = true; + protected final AtomicInteger counter = new AtomicInteger(0); + final List listeners = new CopyOnWriteArrayList<>(); + protected volatile Thread ticker = null; + protected volatile int ticknr = 0; + + public synchronized void start() { + run = true; + ticker = new Thread(this); + ticker.setName("Ticker Thread"); + ticker.start(); + } + + public synchronized void stop() { + // On context stop this can be called multiple times. + // NO-OP is the ticker thread is not set + // (i.e. stop() has already completed) + if (ticker == null) { + return; + } + run = false; + try { + ticker.join(); + }catch (InterruptedException x) { + Thread.interrupted(); + } + + ticker = null; + } + + public void shutdown() { + // Notify each listener of the shutdown. This enables them to + // trigger any necessary clean-up. + for (TickListener l : listeners) { + l.shutdown(); + } + // Wait for the thread to stop. This prevents warnings in the logs + // that the thread is still active when the context stops. + stop(); + } + + public void addTickListener(TickListener listener) { + if (listeners.add(listener)) { + if (counter.incrementAndGet()==1) { + start(); + } + } + + } + + public void removeTickListener(TickListener listener) { + if (listeners.remove(listener)) { + if (counter.decrementAndGet()==0) { + stop(); + } + } + } + + @Override + public void run() { + try { + + Stock[] stocks = new Stock[] { new Stock("GOOG", 435.43), + new Stock("YHOO", 27.88), new Stock("ASF", 1015.55), }; + Random r = new Random(System.currentTimeMillis()); + while (run) { + for (int j = 0; j < 1; j++) { + int i = r.nextInt() % 3; + if (i < 0) { + i = i * (-1); + } + Stock stock = stocks[i]; + double change = r.nextDouble(); + boolean plus = r.nextBoolean(); + if (plus) { + stock.setValue(stock.getValue() + change); + } else { + stock.setValue(stock.getValue() - change); + } + stock.setCnt(++ticknr); + for (TickListener l : listeners) { + l.tick(stock); + } + + } + Thread.sleep(850); + } + } catch (InterruptedException ix) { + // Ignore + } catch (Exception x) { + x.printStackTrace(); + } + } + + + public interface TickListener { + void tick(Stock stock); + void shutdown(); + } + + public static final class Stock implements Cloneable { + protected static final DecimalFormat df = new DecimalFormat("0.00"); + protected final String symbol; + protected double value = 0.0d; + protected double lastchange = 0.0d; + protected int cnt = 0; + + public Stock(String symbol, double initvalue) { + this.symbol = symbol; + this.value = initvalue; + } + + public void setCnt(int c) { + this.cnt = c; + } + + public int getCnt() { + return cnt; + } + + public String getSymbol() { + return symbol; + } + + public double getValue() { + return value; + } + + public void setValue(double value) { + double old = this.value; + this.value = value; + this.lastchange = value - old; + } + + public String getValueAsString() { + return df.format(value); + } + + public double getLastChange() { + return this.lastchange; + } + + public void setLastChange(double lastchange) { + this.lastchange = lastchange; + } + + public String getLastChangeAsString() { + return df.format(lastchange); + } + + @Override + public int hashCode() { + return symbol.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof Stock) { + return this.symbol.equals(((Stock) other).symbol); + } + + return false; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("STOCK#"); + buf.append(getSymbol()); + buf.append('#'); + buf.append(getValueAsString()); + buf.append('#'); + buf.append(getLastChangeAsString()); + buf.append('#'); + buf.append(String.valueOf(getCnt())); + return buf.toString(); + + } + + @Override + public Object clone() { + Stock s = new Stock(this.getSymbol(), this.getValue()); + s.setLastChange(this.getLastChange()); + s.setCnt(this.cnt); + return s; + } + } +} diff --git a/webapps/examples/WEB-INF/classes/cal/Entries.java b/webapps/examples/WEB-INF/classes/cal/Entries.java new file mode 100644 index 0000000..5c6cd49 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/cal/Entries.java @@ -0,0 +1,63 @@ +/* + * 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 cal; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.http.HttpServletRequest; + +public class Entries { + + private final Map entries; + private static final String[] time = { "8am", "9am", "10am", "11am", + "12pm", "1pm", "2pm", "3pm", "4pm", "5pm", "6pm", "7pm", "8pm" }; + public static final int rows = 12; + + public Entries() { + entries = new ConcurrentHashMap<>(rows); + for (int i = 0; i < rows; i++) { + entries.put(time[i], new Entry(time[i])); + } + } + + public int getRows() { + return rows; + } + + public Entry getEntry(int index) { + return this.entries.get(time[index]); + } + + public int getIndex(String tm) { + for (int i = 0; i < rows; i++) { + if (tm.equals(time[i])) { + return i; + } + } + return -1; + } + + public void processRequest(HttpServletRequest request, String tm) { + int index = getIndex(tm); + if (index >= 0) { + String descr = request.getParameter("description"); + entries.get(time[index]).setDescription(descr); + } + } + +} diff --git a/webapps/examples/WEB-INF/classes/cal/Entry.java b/webapps/examples/WEB-INF/classes/cal/Entry.java new file mode 100644 index 0000000..ac248bf --- /dev/null +++ b/webapps/examples/WEB-INF/classes/cal/Entry.java @@ -0,0 +1,52 @@ +/* + * 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 cal; + +public class Entry { + + final String hour; + String description; + + public Entry(String hour) { + this.hour = hour; + this.description = ""; + + } + + public String getHour() { + return this.hour; + } + + public String getColor() { + if (description.equals("")) { + return "lightblue"; + } + return "red"; + } + + public String getDescription() { + if (description.equals("")) { + return "None"; + } + return this.description; + } + + public void setDescription(String descr) { + description = descr; + } + +} diff --git a/webapps/examples/WEB-INF/classes/cal/JspCalendar.java b/webapps/examples/WEB-INF/classes/cal/JspCalendar.java new file mode 100644 index 0000000..29541cc --- /dev/null +++ b/webapps/examples/WEB-INF/classes/cal/JspCalendar.java @@ -0,0 +1,152 @@ +/* + * 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 cal; + +import java.util.Calendar; +import java.util.Date; + +public class JspCalendar { + final Calendar calendar; + + public JspCalendar() { + calendar = Calendar.getInstance(); + Date trialTime = new Date(); + calendar.setTime(trialTime); + } + + + public int getYear() { + return calendar.get(Calendar.YEAR); + } + + public String getMonth() { + int m = getMonthInt(); + String[] months = new String [] { "January", "February", "March", + "April", "May", "June", + "July", "August", "September", + "October", "November", "December" }; + if (m > 12) { + return "Unknown to Man"; + } + + return months[m - 1]; + + } + + public String getDay() { + int x = getDayOfWeek(); + String[] days = new String[] {"Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday"}; + + if (x > 7) { + return "Unknown to Man"; + } + + return days[x - 1]; + + } + + public int getMonthInt() { + return 1 + calendar.get(Calendar.MONTH); + } + + public String getDate() { + return getMonthInt() + "/" + getDayOfMonth() + "/" + getYear(); + } + + public String getCurrentDate() { + Date dt = new Date (); + calendar.setTime (dt); + return getMonthInt() + "/" + getDayOfMonth() + "/" + getYear(); + + } + + public String getNextDate() { + calendar.set (Calendar.DAY_OF_MONTH, getDayOfMonth() + 1); + return getDate (); + } + + public String getPrevDate() { + calendar.set (Calendar.DAY_OF_MONTH, getDayOfMonth() - 1); + return getDate (); + } + + public String getTime() { + return getHour() + ":" + getMinute() + ":" + getSecond(); + } + + public int getDayOfMonth() { + return calendar.get(Calendar.DAY_OF_MONTH); + } + + public int getDayOfYear() { + return calendar.get(Calendar.DAY_OF_YEAR); + } + + public int getWeekOfYear() { + return calendar.get(Calendar.WEEK_OF_YEAR); + } + + public int getWeekOfMonth() { + return calendar.get(Calendar.WEEK_OF_MONTH); + } + + public int getDayOfWeek() { + return calendar.get(Calendar.DAY_OF_WEEK); + } + + public int getHour() { + return calendar.get(Calendar.HOUR_OF_DAY); + } + + public int getMinute() { + return calendar.get(Calendar.MINUTE); + } + + + public int getSecond() { + return calendar.get(Calendar.SECOND); + } + + + public int getEra() { + return calendar.get(Calendar.ERA); + } + + public String getUSTimeZone() { + String[] zones = new String[] {"Hawaii", "Alaskan", "Pacific", + "Mountain", "Central", "Eastern"}; + + return zones[10 + getZoneOffset()]; + } + + public int getZoneOffset() { + return calendar.get(Calendar.ZONE_OFFSET)/(60*60*1000); + } + + + public int getDSTOffset() { + return calendar.get(Calendar.DST_OFFSET)/(60*60*1000); + } + + + public int getAMPM() { + return calendar.get(Calendar.AM_PM); + } +} + + diff --git a/webapps/examples/WEB-INF/classes/cal/TableBean.java b/webapps/examples/WEB-INF/classes/cal/TableBean.java new file mode 100644 index 0000000..e782125 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/cal/TableBean.java @@ -0,0 +1,106 @@ +/* + * 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 cal; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.servlet.http.HttpServletRequest; + +public class TableBean { + + private final Map table; + private final JspCalendar JspCal; + private Entries entries; + private String date; + private String name = null; + private String email = null; + private boolean processError = false; + + public TableBean() { + this.table = new ConcurrentHashMap<>(10); + this.JspCal = new JspCalendar(); + this.date = JspCal.getCurrentDate(); + } + + public void setName(String nm) { + this.name = nm; + } + + public String getName() { + return this.name; + } + + public void setEmail(String mail) { + this.email = mail; + } + + public String getEmail() { + return this.email; + } + + public String getDate() { + return this.date; + } + + public Entries getEntries() { + return this.entries; + } + + public void processRequest(HttpServletRequest request) { + + // Get the name and e-mail. + this.processError = false; + if (name == null || name.equals("")) { + setName(request.getParameter("name")); + } + if (email == null || email.equals("")) { + setEmail(request.getParameter("email")); + } + if (name == null || email == null || name.equals("") + || email.equals("")) { + this.processError = true; + return; + } + + // Get the date. + String dateR = request.getParameter("date"); + if (dateR == null) { + date = JspCal.getCurrentDate(); + } else if (dateR.equalsIgnoreCase("next")) { + date = JspCal.getNextDate(); + } else if (dateR.equalsIgnoreCase("prev")) { + date = JspCal.getPrevDate(); + } + + entries = table.get(date); + if (entries == null) { + entries = new Entries(); + table.put(date, entries); + } + + // If time is provided add the event. + String time = request.getParameter("time"); + if (time != null) { + entries.processRequest(request, time); + } + } + + public boolean getProcessError() { + return this.processError; + } +} diff --git a/webapps/examples/WEB-INF/classes/checkbox/CheckTest.java b/webapps/examples/WEB-INF/classes/checkbox/CheckTest.java new file mode 100644 index 0000000..a19caf0 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/checkbox/CheckTest.java @@ -0,0 +1,30 @@ +/* + * 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 checkbox; + +public class CheckTest { + + String b[] = new String[] { "1", "2", "3", "4" }; + + public String[] getFruit() { + return b; + } + + public void setFruit(String [] b) { + this.b = b; + } +} diff --git a/webapps/examples/WEB-INF/classes/colors/ColorGameBean.java b/webapps/examples/WEB-INF/classes/colors/ColorGameBean.java new file mode 100644 index 0000000..37df405 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/colors/ColorGameBean.java @@ -0,0 +1,114 @@ +/* + * 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 colors; + +public class ColorGameBean { + + private String background = "yellow"; + private String foreground = "red"; + private String color1 = foreground; + private String color2 = background; + private String hint = "no"; + private int attempts = 0; + private int intval = 0; + private boolean tookHints = false; + + public void processRequest() { + + // background = "yellow"; + // foreground = "red"; + + if (! color1.equals(foreground)) { + if (color1.equalsIgnoreCase("black") || + color1.equalsIgnoreCase("cyan")) { + background = color1; + } + } + + if (! color2.equals(background)) { + if (color2.equalsIgnoreCase("black") || + color2.equalsIgnoreCase("cyan")) { + foreground = color2; + } + } + + attempts++; + } + + public void setColor2(String x) { + color2 = x; + } + + public void setColor1(String x) { + color1 = x; + } + + public void setAction(String x) { + if (!tookHints) { + tookHints = x.equalsIgnoreCase("Hint"); + } + hint = x; + } + + public String getColor2() { + return background; + } + + public String getColor1() { + return foreground; + } + + public int getAttempts() { + return attempts; + } + + public boolean getHint() { + return hint.equalsIgnoreCase("Hint"); + } + + public boolean getSuccess() { + if (background.equalsIgnoreCase("black") || + background.equalsIgnoreCase("cyan")) { + + if (foreground.equalsIgnoreCase("black") || + foreground.equalsIgnoreCase("cyan")) { + return true; + } + return false; + } + + return false; + } + + public boolean getHintTaken() { + return tookHints; + } + + public void reset() { + foreground = "red"; + background = "yellow"; + } + + public void setIntval(int value) { + intval = value; + } + + public int getIntval() { + return intval; + } +} + diff --git a/webapps/examples/WEB-INF/classes/compressionFilters/CompressionFilter.java b/webapps/examples/WEB-INF/classes/compressionFilters/CompressionFilter.java new file mode 100644 index 0000000..5ac0054 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/compressionFilters/CompressionFilter.java @@ -0,0 +1,224 @@ +/* + * 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 compressionFilters; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.StringTokenizer; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Implementation of jakarta.servlet.Filter used to compress + * the ServletResponse if it is bigger than a threshold. + * + * @author Amy Roh + * @author Dmitri Valdin + */ +public class CompressionFilter extends GenericFilter { + + private static final long serialVersionUID = 1L; + + /** + * Minimal reasonable threshold. + */ + private static final int MIN_THRESHOLD = 128; + + /** + * Minimal reasonable buffer. + */ + // 8 KiB is what tomcat would use by default anyway + private static final int MIN_BUFFER = 8192; + + /** + * The threshold number to compress. + */ + protected int compressionThreshold = 0; + + /** + * The compression buffer size to avoid chunking. + */ + protected int compressionBuffer = 0; + + /** + * The mime types to compress. + */ + protected String[] compressionMimeTypes = {"text/html", "text/xml", "text/plain"}; + + /** + * Debug level for this filter. + */ + private int debug = 0; + + @Override + public void init() { + String str = getInitParameter("debug"); + if (str != null) { + debug = Integer.parseInt(str); + } + + str = getInitParameter("compressionThreshold"); + if (str != null) { + compressionThreshold = Integer.parseInt(str); + if (compressionThreshold != 0 && compressionThreshold < MIN_THRESHOLD) { + if (debug > 0) { + System.out.println("compressionThreshold should be either 0 - no compression or >= " + MIN_THRESHOLD); + System.out.println("compressionThreshold set to " + MIN_THRESHOLD); + } + compressionThreshold = MIN_THRESHOLD; + } + } + + str = getInitParameter("compressionBuffer"); + if (str != null) { + compressionBuffer = Integer.parseInt(str); + if (compressionBuffer < MIN_BUFFER) { + if (debug > 0) { + System.out.println("compressionBuffer should be >= " + MIN_BUFFER); + System.out.println("compressionBuffer set to " + MIN_BUFFER); + } + compressionBuffer = MIN_BUFFER; + } + } + + str = getInitParameter("compressionMimeTypes"); + if (str != null) { + List values = new ArrayList<>(); + StringTokenizer st = new StringTokenizer(str, ","); + + while (st.hasMoreTokens()) { + String token = st.nextToken().trim(); + if (token.length() > 0) { + values.add(token); + } + } + + if (values.size() > 0) { + compressionMimeTypes = values.toArray(new String[0]); + } else { + compressionMimeTypes = null; + } + + if (debug > 0) { + System.out.println("compressionMimeTypes set to " + + Arrays.toString(compressionMimeTypes)); + } + } + } + + /** + * The doFilter method of the Filter is called by the container + * each time a request/response pair is passed through the chain due + * to a client request for a resource at the end of the chain. + * The FilterChain passed into this method allows the Filter to pass on the + * request and response to the next entity in the chain.

    + * This method first examines the request to check whether the client support + * compression.
    + * It simply just pass the request and response if there is no support for + * compression.
    + * If the compression support is available, it creates a + * CompressionServletResponseWrapper object which compresses the content and + * modifies the header if the content length is big enough. + * It then invokes the next entity in the chain using the FilterChain object + * (chain.doFilter()),
    + **/ + @Override + public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain ) + throws IOException, ServletException { + + if (debug > 0) { + System.out.println("@doFilter"); + } + + if (compressionThreshold == 0) { + if (debug > 0) { + System.out.println("doFilter got called, but compressionThreshold is set to 0 - no compression"); + } + chain.doFilter(request, response); + return; + } + + boolean supportCompression = false; + if (request instanceof HttpServletRequest) { + if (debug > 1) { + System.out.println("requestURI = " + ((HttpServletRequest)request).getRequestURI()); + } + + // Are we allowed to compress ? + String s = ((HttpServletRequest)request).getParameter("gzip"); + if ("false".equals(s)) { + if (debug > 0) { + System.out.println("got parameter gzip=false --> don't compress, just chain filter"); + } + chain.doFilter(request, response); + return; + } + + Enumeration e = + ((HttpServletRequest)request).getHeaders("Accept-Encoding"); + while (e.hasMoreElements()) { + String name = e.nextElement(); + if (name.indexOf("gzip") != -1) { + if (debug > 0) { + System.out.println("supports compression"); + } + supportCompression = true; + } else { + if (debug > 0) { + System.out.println("no support for compression"); + } + } + } + } + + if (supportCompression) { + if (response instanceof HttpServletResponse) { + CompressionServletResponseWrapper wrappedResponse = + new CompressionServletResponseWrapper((HttpServletResponse)response); + wrappedResponse.setDebugLevel(debug); + wrappedResponse.setCompressionThreshold(compressionThreshold); + wrappedResponse.setCompressionBuffer(compressionBuffer); + wrappedResponse.setCompressionMimeTypes(compressionMimeTypes); + if (debug > 0) { + System.out.println("doFilter gets called with compression"); + } + try { + chain.doFilter(request, wrappedResponse); + } finally { + wrappedResponse.finishResponse(); + } + return; + } + } else { + if (debug > 0) { + System.out.println("doFilter gets called w/o compression"); + } + chain.doFilter(request, response); + return; + } + } +} + diff --git a/webapps/examples/WEB-INF/classes/compressionFilters/CompressionFilterTestServlet.java b/webapps/examples/WEB-INF/classes/compressionFilters/CompressionFilterTestServlet.java new file mode 100644 index 0000000..330aa2b --- /dev/null +++ b/webapps/examples/WEB-INF/classes/compressionFilters/CompressionFilterTestServlet.java @@ -0,0 +1,65 @@ +/* + * 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 compressionFilters; + +import java.io.IOException; +import java.util.Enumeration; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Very Simple test servlet to test compression filter + * @author Amy Roh + */ +public class CompressionFilterTestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + ServletOutputStream out = response.getOutputStream(); + response.setContentType("text/plain"); + + Enumeration e = request.getHeaders("Accept-Encoding"); + while (e.hasMoreElements()) { + String name = e.nextElement(); + out.println(name); + if (name.indexOf("gzip") != -1) { + out.println("gzip supported -- able to compress"); + } else { + out.println("gzip not supported"); + } + } + + + out.println("Compression Filter Test Servlet"); + out.println("Minimum content length for compression is 128 bytes"); + out.println("********** 32 bytes **********"); + out.println("********** 32 bytes **********"); + out.println("********** 32 bytes **********"); + out.println("********** 32 bytes **********"); + out.close(); + } + +} + diff --git a/webapps/examples/WEB-INF/classes/compressionFilters/CompressionResponseStream.java b/webapps/examples/WEB-INF/classes/compressionFilters/CompressionResponseStream.java new file mode 100644 index 0000000..c6ee9e4 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/compressionFilters/CompressionResponseStream.java @@ -0,0 +1,448 @@ +/* + * 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 compressionFilters; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.zip.GZIPOutputStream; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; + +/** + * Implementation of ServletOutputStream that works with + * the CompressionServletResponseWrapper implementation. + * + * @author Amy Roh + * @author Dmitri Valdin + */ +public class CompressionResponseStream extends ServletOutputStream { + + // ----------------------------------------------------------- Constructors + + /** + * Construct a servlet output stream associated with the specified Response. + * + * @param responseWrapper The associated response wrapper + * @param originalOutput the output stream + */ + public CompressionResponseStream( + CompressionServletResponseWrapper responseWrapper, + ServletOutputStream originalOutput) { + + super(); + closed = false; + this.response = responseWrapper; + this.output = originalOutput; + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * The threshold number which decides to compress or not. + * Users can configure in web.xml to set it to fit their needs. + */ + protected int compressionThreshold = 0; + + /** + * The compression buffer size to avoid chunking + */ + protected int compressionBuffer = 0; + + /** + * The mime types to compress + */ + protected String[] compressionMimeTypes = {"text/html", "text/xml", "text/plain"}; + + /** + * Debug level + */ + private int debug = 0; + + /** + * The buffer through which all of our output bytes are passed. + */ + protected byte[] buffer = null; + + /** + * The number of data bytes currently in the buffer. + */ + protected int bufferCount = 0; + + /** + * The underlying gzip output stream to which we should write data. + */ + protected OutputStream gzipstream = null; + + /** + * Has this stream been closed? + */ + protected boolean closed = false; + + /** + * The response with which this servlet output stream is associated. + */ + protected final CompressionServletResponseWrapper response; + + /** + * The underlying servlet output stream to which we should write data. + */ + protected final ServletOutputStream output; + + + // --------------------------------------------------------- Public Methods + + /** + * Set debug level. + * + * @param debug The higher the number, the more detail shown. Currently the + * range 0 (none) to 3 (everything) is used. + */ + public void setDebugLevel(int debug) { + this.debug = debug; + } + + + /** + * Set the compressionThreshold number and create buffer for this size + * + * @param compressionThreshold Responses above this size in bytes will be + * compressed + */ + protected void setCompressionThreshold(int compressionThreshold) { + this.compressionThreshold = compressionThreshold; + buffer = new byte[this.compressionThreshold]; + if (debug > 1) { + System.out.println("compressionThreshold is set to "+ this.compressionThreshold); + } + } + + /** + * The compression buffer size to avoid chunking + * + * @param compressionBuffer The compression buffer size in bytes + */ + protected void setCompressionBuffer(int compressionBuffer) { + this.compressionBuffer = compressionBuffer; + if (debug > 1) { + System.out.println("compressionBuffer is set to "+ this.compressionBuffer); + } + } + + /** + * Set supported mime types. + * + * @param compressionMimeTypes The mimetypes that will be compressed. + */ + public void setCompressionMimeTypes(String[] compressionMimeTypes) { + this.compressionMimeTypes = compressionMimeTypes; + if (debug > 1) { + System.out.println("compressionMimeTypes is set to " + + Arrays.toString(this.compressionMimeTypes)); + } + } + + /** + * Close this output stream, causing any buffered data to be flushed and + * any further output data to throw an IOException. + */ + @Override + public void close() throws IOException { + + if (debug > 1) { + System.out.println("close() @ CompressionResponseStream"); + } + if (closed) { + throw new IOException("This output stream has already been closed"); + } + + if (gzipstream != null) { + flushToGZip(); + gzipstream.close(); + gzipstream = null; + } else { + if (bufferCount > 0) { + if (debug > 2) { + System.out.print("output.write("); + System.out.write(buffer, 0, bufferCount); + System.out.println(")"); + } + output.write(buffer, 0, bufferCount); + bufferCount = 0; + } + } + + output.close(); + closed = true; + + } + + + /** + * Flush any buffered data for this output stream, which also causes the + * response to be committed. + */ + @Override + public void flush() throws IOException { + + if (debug > 1) { + System.out.println("flush() @ CompressionResponseStream"); + } + if (closed) { + throw new IOException("Cannot flush a closed output stream"); + } + + if (gzipstream != null) { + gzipstream.flush(); + } + + } + + public void flushToGZip() throws IOException { + + if (debug > 1) { + System.out.println("flushToGZip() @ CompressionResponseStream"); + } + if (bufferCount > 0) { + if (debug > 1) { + System.out.println("flushing out to GZipStream, bufferCount = " + bufferCount); + } + writeToGZip(buffer, 0, bufferCount); + bufferCount = 0; + } + + } + + /** + * Write the specified byte to our output stream. + * + * @param b The byte to be written + * + * @exception IOException if an input/output error occurs + */ + @Override + public void write(int b) throws IOException { + + if (debug > 1) { + System.out.println("write "+b+" in CompressionResponseStream "); + } + if (closed) { + throw new IOException("Cannot write to a closed output stream"); + } + + if (bufferCount >= buffer.length) { + flushToGZip(); + } + + buffer[bufferCount++] = (byte) b; + + } + + + /** + * Write b.length bytes from the specified byte array + * to our output stream. + * + * @param b The byte array to be written + * + * @exception IOException if an input/output error occurs + */ + @Override + public void write(byte b[]) throws IOException { + + write(b, 0, b.length); + + } + + + + /** + * TODO SERVLET 3.1 + */ + @Override + public boolean isReady() { + // TODO Auto-generated method stub + return false; + } + + + /** + * TODO SERVLET 3.1 + */ + @Override + public void setWriteListener(WriteListener listener) { + // TODO Auto-generated method stub + + } + + + /** + * Write len bytes from the specified byte array, starting + * at the specified offset, to our output stream. + * + * @param b The byte array containing the bytes to be written + * @param off Zero-relative starting offset of the bytes to be written + * @param len The number of bytes to be written + * + * @exception IOException if an input/output error occurs + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + + if (debug > 1) { + System.out.println("write, bufferCount = " + bufferCount + " len = " + len + " off = " + off); + } + if (debug > 2) { + System.out.print("write("); + System.out.write(b, off, len); + System.out.println(")"); + } + + if (closed) { + throw new IOException("Cannot write to a closed output stream"); + } + + if (len == 0) { + return; + } + + // Can we write into buffer ? + if (len <= (buffer.length - bufferCount)) { + System.arraycopy(b, off, buffer, bufferCount, len); + bufferCount += len; + return; + } + + // There is not enough space in buffer. Flush it ... + flushToGZip(); + + // ... and try again. Note, that bufferCount = 0 here ! + if (len <= (buffer.length - bufferCount)) { + System.arraycopy(b, off, buffer, bufferCount, len); + bufferCount += len; + return; + } + + // write direct to gzip + writeToGZip(b, off, len); + } + + public void writeToGZip(byte b[], int off, int len) throws IOException { + + if (debug > 1) { + System.out.println("writeToGZip, len = " + len); + } + if (debug > 2) { + System.out.print("writeToGZip("); + System.out.write(b, off, len); + System.out.println(")"); + } + if (gzipstream == null) { + if (debug > 1) { + System.out.println("new GZIPOutputStream"); + } + + boolean alreadyCompressed = false; + String contentEncoding = response.getHeader("Content-Encoding"); + if (contentEncoding != null) { + if (contentEncoding.contains("gzip")) { + alreadyCompressed = true; + if (debug > 0) { + System.out.println("content is already compressed"); + } + } else { + if (debug > 0) { + System.out.println("content is not compressed yet"); + } + } + } + + boolean compressibleMimeType = false; + // Check for compatible MIME-TYPE + if (compressionMimeTypes != null) { + if (startsWithStringArray(compressionMimeTypes, response.getContentType())) { + compressibleMimeType = true; + if (debug > 0) { + System.out.println("mime type " + response.getContentType() + " is compressible"); + } + } else { + if (debug > 0) { + System.out.println("mime type " + response.getContentType() + " is not compressible"); + } + } + } + + if (response.isCommitted()) { + if (debug > 1) { + System.out.print("Response already committed. Using original output stream"); + } + gzipstream = output; + } else if (alreadyCompressed) { + if (debug > 1) { + System.out.print("Response already compressed. Using original output stream"); + } + gzipstream = output; + } else if (!compressibleMimeType) { + if (debug > 1) { + System.out.print("Response mime type is not compressible. Using original output stream"); + } + gzipstream = output; + } else { + response.addHeader("Content-Encoding", "gzip"); + response.setContentLength(-1); // don't use any preset content-length as it will be wrong after gzipping + response.setBufferSize(compressionBuffer); + gzipstream = new GZIPOutputStream(output); + } + } + gzipstream.write(b, off, len); + + } + + + // -------------------------------------------------------- Package Methods + + /** + * Has this response stream been closed? + * + * @return true if the stream has been closed, otherwise false. + */ + public boolean closed() { + return closed; + } + + + /** + * Checks if any entry in the string array starts with the specified value + * + * @param sArray the StringArray + * @param value string + */ + private boolean startsWithStringArray(String sArray[], String value) { + if (value == null) { + return false; + } + for (String s : sArray) { + if (value.startsWith(s)) { + return true; + } + } + return false; + } +} diff --git a/webapps/examples/WEB-INF/classes/compressionFilters/CompressionServletResponseWrapper.java b/webapps/examples/WEB-INF/classes/compressionFilters/CompressionServletResponseWrapper.java new file mode 100644 index 0000000..0c6a65f --- /dev/null +++ b/webapps/examples/WEB-INF/classes/compressionFilters/CompressionServletResponseWrapper.java @@ -0,0 +1,295 @@ +/* + * 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 compressionFilters; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +/** + * Implementation of HttpServletResponseWrapper that works with + * the CompressionServletResponseStream implementation.. + * + * @author Amy Roh + * @author Dmitri Valdin + */ +public class CompressionServletResponseWrapper + extends HttpServletResponseWrapper { + + // ----------------------------------------------------- Constructor + + /** + * Calls the parent constructor which creates a ServletResponse adaptor + * wrapping the given response object. + * + * @param response The response object to be wrapped. + */ + public CompressionServletResponseWrapper(HttpServletResponse response) { + super(response); + origResponse = response; + if (debug > 1) { + System.out.println("CompressionServletResponseWrapper constructor gets called"); + } + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Original response + */ + protected final HttpServletResponse origResponse; + + /** + * The ServletOutputStream that has been returned by + * getOutputStream(), if any. + */ + protected ServletOutputStream stream = null; + + + /** + * The PrintWriter that has been returned by + * getWriter(), if any. + */ + protected PrintWriter writer = null; + + /** + * The threshold number to compress + */ + protected int compressionThreshold = 0; + + /** + * The compression buffer size + */ + protected int compressionBuffer = 8192; // 8 KiB default + + /** + * The mime types to compress + */ + protected String[] compressionMimeTypes = {"text/html", "text/xml", "text/plain"}; + + /** + * Debug level + */ + protected int debug = 0; + + /** + * keeps a copy of all headers set + */ + private final Map headerCopies = new HashMap<>(); + + + // --------------------------------------------------------- Public Methods + + + /** + * Set threshold number. + * + * @param threshold The new compression threshold + */ + public void setCompressionThreshold(int threshold) { + if (debug > 1) { + System.out.println("setCompressionThreshold to " + threshold); + } + this.compressionThreshold = threshold; + } + + /** + * Set compression buffer. + * + * @param buffer New size of buffer to use for compressed output + */ + public void setCompressionBuffer(int buffer) { + if (debug > 1) { + System.out.println("setCompressionBuffer to " + buffer); + } + this.compressionBuffer = buffer; + } + + /** + * Set compressible mime types. + * + * @param mimeTypes The new list of mime types that will be considered for + * compression + */ + public void setCompressionMimeTypes(String[] mimeTypes) { + if (debug > 1) { + System.out.println("setCompressionMimeTypes to " + + Arrays.toString(mimeTypes)); + } + this.compressionMimeTypes = mimeTypes; + } + + /** + * Set debug level. + * + * @param debug The new debug level + */ + public void setDebugLevel(int debug) { + this.debug = debug; + } + + + /** + * Create and return a ServletOutputStream to write the content + * associated with this Response. + * + * @exception IOException if an input/output error occurs + * + * @return A new servlet output stream that compressed any data written to + * it + */ + protected ServletOutputStream createOutputStream() throws IOException { + if (debug > 1) { + System.out.println("createOutputStream gets called"); + } + + CompressionResponseStream stream = new CompressionResponseStream( + this, origResponse.getOutputStream()); + stream.setDebugLevel(debug); + stream.setCompressionThreshold(compressionThreshold); + stream.setCompressionBuffer(compressionBuffer); + stream.setCompressionMimeTypes(compressionMimeTypes); + + return stream; + } + + + /** + * Finish a response. + */ + public void finishResponse() { + try { + if (writer != null) { + writer.close(); + } else { + if (stream != null) { + stream.close(); + } + } + } catch (IOException e) { + // Ignore + } + } + + + // ------------------------------------------------ ServletResponse Methods + + + /** + * Flush the buffer and commit this response. + * + * @exception IOException if an input/output error occurs + */ + @Override + public void flushBuffer() throws IOException { + if (debug > 1) { + System.out.println("flush buffer @ GZipServletResponseWrapper"); + } + ((CompressionResponseStream)stream).flush(); + + } + + /** + * Return the servlet output stream associated with this Response. + * + * @exception IllegalStateException if getWriter has + * already been called for this response + * @exception IOException if an input/output error occurs + */ + @Override + public ServletOutputStream getOutputStream() throws IOException { + + if (writer != null) { + throw new IllegalStateException("getWriter() has already been called for this response"); + } + + if (stream == null) { + stream = createOutputStream(); + } + if (debug > 1) { + System.out.println("stream is set to "+stream+" in getOutputStream"); + } + + return stream; + } + + /** + * Return the writer associated with this Response. + * + * @exception IllegalStateException if getOutputStream has + * already been called for this response + * @exception IOException if an input/output error occurs + */ + @Override + public PrintWriter getWriter() throws IOException { + + if (writer != null) { + return writer; + } + + if (stream != null) { + throw new IllegalStateException("getOutputStream() has already been called for this response"); + } + + stream = createOutputStream(); + if (debug > 1) { + System.out.println("stream is set to "+stream+" in getWriter"); + } + String charEnc = origResponse.getCharacterEncoding(); + if (debug > 1) { + System.out.println("character encoding is " + charEnc); + } + writer = new PrintWriter(new OutputStreamWriter(stream, charEnc)); + + return writer; + } + + @Override + public String getHeader(String name) { + return headerCopies.get(name); + } + + @Override + public void addHeader(String name, String value) { + if (headerCopies.containsKey(name)) { + String existingValue = headerCopies.get(name); + if ((existingValue != null) && (existingValue.length() > 0)) { + headerCopies.put(name, existingValue + "," + value); + } else { + headerCopies.put(name, value); + } + } else { + headerCopies.put(name, value); + } + super.addHeader(name, value); + } + + + @Override + public void setHeader(String name, String value) { + headerCopies.put(name, value); + super.setHeader(name, value); + } +} diff --git a/webapps/examples/WEB-INF/classes/dates/JspCalendar.java b/webapps/examples/WEB-INF/classes/dates/JspCalendar.java new file mode 100644 index 0000000..24466d9 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/dates/JspCalendar.java @@ -0,0 +1,155 @@ +/* + * 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 dates; + +import java.util.Calendar; +import java.util.Date; + +public class JspCalendar { + final Calendar calendar; + + public JspCalendar() { + calendar = Calendar.getInstance(); + Date trialTime = new Date(); + calendar.setTime(trialTime); + } + + public int getYear() { + return calendar.get(Calendar.YEAR); + } + + public String getMonth() { + int m = getMonthInt(); + String[] months = new String [] { "January", "February", "March", + "April", "May", "June", + "July", "August", "September", + "October", "November", "December" }; + if (m > 12) { + return "Unknown to Man"; + } + + return months[m - 1]; + + } + + public String getDay() { + int x = getDayOfWeek(); + String[] days = new String[] {"Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday"}; + + if (x > 7) { + return "Unknown to Man"; + } + + return days[x - 1]; + + } + + public int getMonthInt() { + return 1 + calendar.get(Calendar.MONTH); + } + + public String getDate() { + return getMonthInt() + "/" + getDayOfMonth() + "/" + getYear(); + + } + + public String getTime() { + return getHour() + ":" + getMinute() + ":" + getSecond(); + } + + public int getDayOfMonth() { + return calendar.get(Calendar.DAY_OF_MONTH); + } + + public int getDayOfYear() { + return calendar.get(Calendar.DAY_OF_YEAR); + } + + public int getWeekOfYear() { + return calendar.get(Calendar.WEEK_OF_YEAR); + } + + public int getWeekOfMonth() { + return calendar.get(Calendar.WEEK_OF_MONTH); + } + + public int getDayOfWeek() { + return calendar.get(Calendar.DAY_OF_WEEK); + } + + public int getHour() { + return calendar.get(Calendar.HOUR_OF_DAY); + } + + public int getMinute() { + return calendar.get(Calendar.MINUTE); + } + + + public int getSecond() { + return calendar.get(Calendar.SECOND); + } + + public static void main(String args[]) { + JspCalendar db = new JspCalendar(); + p("date: " + db.getDayOfMonth()); + p("year: " + db.getYear()); + p("month: " + db.getMonth()); + p("time: " + db.getTime()); + p("date: " + db.getDate()); + p("Day: " + db.getDay()); + p("DayOfYear: " + db.getDayOfYear()); + p("WeekOfYear: " + db.getWeekOfYear()); + p("era: " + db.getEra()); + p("ampm: " + db.getAMPM()); + p("DST: " + db.getDSTOffset()); + p("ZONE Offset: " + db.getZoneOffset()); + p("TIMEZONE: " + db.getUSTimeZone()); + } + + private static void p(String x) { + System.out.println(x); + } + + + public int getEra() { + return calendar.get(Calendar.ERA); + } + + public String getUSTimeZone() { + String[] zones = new String[] {"Hawaii", "Alaskan", "Pacific", + "Mountain", "Central", "Eastern"}; + + return zones[10 + getZoneOffset()]; + } + + public int getZoneOffset() { + return calendar.get(Calendar.ZONE_OFFSET)/(60*60*1000); + } + + + public int getDSTOffset() { + return calendar.get(Calendar.DST_OFFSET)/(60*60*1000); + } + + + public int getAMPM() { + return calendar.get(Calendar.AM_PM); + } +} + diff --git a/webapps/examples/WEB-INF/classes/error/Smart.java b/webapps/examples/WEB-INF/classes/error/Smart.java new file mode 100644 index 0000000..82c22f6 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/error/Smart.java @@ -0,0 +1,30 @@ +/* + * 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 error; + +public class Smart { + + String name = "JSP"; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/webapps/examples/WEB-INF/classes/examples/ExampleTagBase.java b/webapps/examples/WEB-INF/classes/examples/ExampleTagBase.java new file mode 100644 index 0000000..30d0bb6 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/examples/ExampleTagBase.java @@ -0,0 +1,74 @@ +/* + * 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 examples; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyContent; +import jakarta.servlet.jsp.tagext.BodyTagSupport; +import jakarta.servlet.jsp.tagext.Tag; + +public abstract class ExampleTagBase extends BodyTagSupport { + + private static final long serialVersionUID = 1L; + + @Override + public void setParent(Tag parent) { + this.parent = parent; + } + + @Override + public void setBodyContent(BodyContent bodyOut) { + this.bodyOut = bodyOut; + } + + @Override + public Tag getParent() { + return this.parent; + } + + @Override + public int doStartTag() throws JspException { + return SKIP_BODY; + } + + @Override + public int doEndTag() throws JspException { + return EVAL_PAGE; + } + + + @Override + public void doInitBody() throws JspException { + // Default implementations for BodyTag methods as well + // just in case a tag decides to implement BodyTag. + } + + @Override + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + @Override + public void release() { + bodyOut = null; + pageContext = null; + parent = null; + } + + protected BodyContent bodyOut; + protected Tag parent; +} diff --git a/webapps/examples/WEB-INF/classes/examples/FooTag.java b/webapps/examples/WEB-INF/classes/examples/FooTag.java new file mode 100644 index 0000000..8f2af8c --- /dev/null +++ b/webapps/examples/WEB-INF/classes/examples/FooTag.java @@ -0,0 +1,87 @@ +/* + * 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 examples; + +import java.io.IOException; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; + +/** + * Example1: the simplest tag + * Collect attributes and call into some actions + * + * + */ + +public class FooTag extends ExampleTagBase { + + private static final long serialVersionUID = 1L; + + private final String atts[] = new String[3]; + int i = 0; + + private void setAtt(int index, String value) { + atts[index] = value; + } + + public void setAtt1(String value) { + setAtt(0, value); + } + + public void setAtt2(String value) { + setAtt(1, value); + } + + public void setAtt3(String value) { + setAtt(2, value); + } + + /** + * Process start tag + * + * @return EVAL_BODY_INCLUDE + */ + @Override + public int doStartTag() throws JspException { + i = 0; + return EVAL_BODY_BUFFERED; + } + + @Override + public void doInitBody() throws JspException { + pageContext.setAttribute("member", atts[i]); + i++; + } + + @Override + public int doAfterBody() throws JspException { + try { + if (i == 3) { + bodyOut.writeOut(bodyOut.getEnclosingWriter()); + return SKIP_BODY; + } + + pageContext.setAttribute("member", atts[i]); + i++; + return EVAL_BODY_BUFFERED; + } catch (IOException ex) { + throw new JspTagException(ex.toString()); + } + } +} + diff --git a/webapps/examples/WEB-INF/classes/examples/FooTagExtraInfo.java b/webapps/examples/WEB-INF/classes/examples/FooTagExtraInfo.java new file mode 100644 index 0000000..feb345a --- /dev/null +++ b/webapps/examples/WEB-INF/classes/examples/FooTagExtraInfo.java @@ -0,0 +1,36 @@ +/* + * 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 examples; + +import jakarta.servlet.jsp.tagext.TagData; +import jakarta.servlet.jsp.tagext.TagExtraInfo; +import jakarta.servlet.jsp.tagext.VariableInfo; + +public class FooTagExtraInfo extends TagExtraInfo { + @Override + public VariableInfo[] getVariableInfo(TagData data) { + return new VariableInfo[] + { + new VariableInfo("member", + "String", + true, + VariableInfo.NESTED) + }; + } +} + + diff --git a/webapps/examples/WEB-INF/classes/examples/LogTag.java b/webapps/examples/WEB-INF/classes/examples/LogTag.java new file mode 100644 index 0000000..cc368c4 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/examples/LogTag.java @@ -0,0 +1,63 @@ +/* + * 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 examples; + +import java.io.IOException; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; + +/** + * Log the contents of the body. Could be used to handle errors etc. + */ +public class LogTag extends ExampleTagBase { + + private static final long serialVersionUID = 1L; + + boolean toBrowser = false; + + public void setToBrowser(String value) { + if (value == null) { + toBrowser = false; + } else if (value.equalsIgnoreCase("true")) { + toBrowser = true; + } else { + toBrowser = false; + } + } + + @Override + public int doStartTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + @Override + public int doAfterBody() throws JspException { + try { + String s = bodyOut.getString(); + System.err.println(s); + if (toBrowser) { + bodyOut.writeOut(bodyOut.getEnclosingWriter()); + } + return SKIP_BODY; + } catch (IOException ex) { + throw new JspTagException(ex.toString()); + } + } +} + + diff --git a/webapps/examples/WEB-INF/classes/examples/ValuesTag.java b/webapps/examples/WEB-INF/classes/examples/ValuesTag.java new file mode 100644 index 0000000..7f63dee --- /dev/null +++ b/webapps/examples/WEB-INF/classes/examples/ValuesTag.java @@ -0,0 +1,79 @@ +/* + * 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 examples; + +import java.io.IOException; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspTagException; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.tagext.TagSupport; + +/** + * Accept and display a value. + */ +public class ValuesTag extends TagSupport { + + private static final long serialVersionUID = 1L; + + // Using "-1" as the default value, + // in the assumption that it won't be used as the value. + // Cannot use null here, because null is an important case + // that should be present in the tests. + private Object objectValue = "-1"; + private String stringValue = "-1"; + private long longValue = -1; + private double doubleValue = -1; + + public void setObject(Object objectValue) { + this.objectValue = objectValue; + } + + public void setString(String stringValue) { + this.stringValue = stringValue; + } + + public void setLong(long longValue) { + this.longValue = longValue; + } + + public void setDouble(double doubleValue) { + this.doubleValue = doubleValue; + } + + @Override + public int doEndTag() throws JspException { + JspWriter out = pageContext.getOut(); + + try { + if (!"-1".equals(objectValue)) { + out.print(objectValue); + } else if (!"-1".equals(stringValue)) { + out.print(stringValue); + } else if (longValue != -1) { + out.print(longValue); + } else if (doubleValue != -1) { + out.print(doubleValue); + } else { + out.print("-1"); + } + } catch (IOException ex) { + throw new JspTagException("IOException: " + ex.toString(), ex); + } + return super.doEndTag(); + } +} diff --git a/webapps/examples/WEB-INF/classes/filters/ExampleFilter.java b/webapps/examples/WEB-INF/classes/filters/ExampleFilter.java new file mode 100644 index 0000000..eced646 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/filters/ExampleFilter.java @@ -0,0 +1,99 @@ +/* + * 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 filters; + + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.GenericFilter; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + + +/** + * Example filter that can be attached to either an individual servlet + * or to a URL pattern. This filter performs the following functions: + *

      + *
    • Attaches itself as a request attribute, under the attribute name + * defined by the value of the attribute initialization + * parameter.
    • + *
    • Calculates the number of milliseconds required to perform the + * servlet processing required by this request, including any + * subsequently defined filters, and logs the result to the servlet + * context log for this application. + *
    + * + * @author Craig McClanahan + */ +public final class ExampleFilter extends GenericFilter { + + + private static final long serialVersionUID = 1L; + + + /** + * The request attribute name under which we store a reference to ourself. + */ + private String attribute = null; + + + /** + * Time the processing that is performed by all subsequent filters in the + * current filter stack, including the ultimately invoked servlet. + * + * @param request The servlet request we are processing + * @param response The servlet response we are creating + * @param chain The filter chain we are processing + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + // Store ourselves as a request attribute (if requested) + if (attribute != null) { + request.setAttribute(attribute, this); + } + + // Time and log the subsequent processing + long startTime = System.currentTimeMillis(); + chain.doFilter(request, response); + long stopTime = System.currentTimeMillis(); + getServletContext().log(this.toString() + ": " + (stopTime - startTime) + + " milliseconds"); + } + + + @Override + public void init() throws ServletException { + this.attribute = getInitParameter("attribute"); + } + + + /** + * Return a String representation of this object. + */ + @Override + public String toString() { + return "ExampleFilter(" + getFilterConfig() + ")"; + } +} + diff --git a/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java b/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java new file mode 100644 index 0000000..3da6895 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java @@ -0,0 +1,59 @@ +/* + * 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 http2; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.PushBuilder; + +public class SimpleImagePush extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/html"); + PrintWriter pw = resp.getWriter(); + + PushBuilder pb = req.newPushBuilder(); + if (pb != null) { + pb.path("servlets/images/code.gif"); + pb.push(); + pw.println(""); + pw.println(""); + pw.println("

    The following image was provided via a push request.

    "); + pw.println(""); + pw.println(""); + pw.println(""); + pw.flush(); + } else { + pw.println(""); + pw.println(""); + pw.println("

    Server push requests are not supported by this protocol.

    "); + pw.println(""); + pw.println(""); + } + } +} diff --git a/webapps/examples/WEB-INF/classes/jsp2/examples/BookBean.java b/webapps/examples/WEB-INF/classes/jsp2/examples/BookBean.java new file mode 100644 index 0000000..30a8450 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/jsp2/examples/BookBean.java @@ -0,0 +1,42 @@ +/* + * 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 jsp2.examples; + +public class BookBean { + private final String title; + private final String author; + private final String isbn; + + public BookBean( String title, String author, String isbn ) { + this.title = title; + this.author = author; + this.isbn = isbn; + } + + public String getTitle() { + return this.title; + } + + public String getAuthor() { + return this.author; + } + + public String getIsbn() { + return this.isbn; + } + +} diff --git a/webapps/examples/WEB-INF/classes/jsp2/examples/FooBean.java b/webapps/examples/WEB-INF/classes/jsp2/examples/FooBean.java new file mode 100644 index 0000000..4334856 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/jsp2/examples/FooBean.java @@ -0,0 +1,34 @@ +/* + * 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 jsp2.examples; + +public class FooBean { + private String bar; + + public FooBean() { + bar = "Initial value"; + } + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + +} diff --git a/webapps/examples/WEB-INF/classes/jsp2/examples/ValuesBean.java b/webapps/examples/WEB-INF/classes/jsp2/examples/ValuesBean.java new file mode 100644 index 0000000..aac414f --- /dev/null +++ b/webapps/examples/WEB-INF/classes/jsp2/examples/ValuesBean.java @@ -0,0 +1,50 @@ +/* + * 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 jsp2.examples; + +/** + * Accept and display a value. + */ +public class ValuesBean { + private String string; + private double doubleValue; + private long longValue; + + public String getStringValue() { + return this.string; + } + + public void setStringValue(String string) { + this.string = string; + } + + public double getDoubleValue() { + return doubleValue; + } + + public void setDoubleValue(double doubleValue) { + this.doubleValue = doubleValue; + } + + public long getLongValue() { + return longValue; + } + + public void setLongValue(long longValue) { + this.longValue = longValue; + } +} diff --git a/webapps/examples/WEB-INF/classes/jsp2/examples/el/Functions.java b/webapps/examples/WEB-INF/classes/jsp2/examples/el/Functions.java new file mode 100644 index 0000000..79f9eac --- /dev/null +++ b/webapps/examples/WEB-INF/classes/jsp2/examples/el/Functions.java @@ -0,0 +1,45 @@ +/* + * 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 jsp2.examples.el; + +import java.util.Locale; + +/** + * Defines the functions for the jsp2 example tag library. + * + *

    Each function is defined as a static method.

    + */ +public class Functions { + public static String reverse( String text ) { + return new StringBuilder( text ).reverse().toString(); + } + + public static int numVowels( String text ) { + String vowels = "aeiouAEIOU"; + int result = 0; + for( int i = 0; i < text.length(); i++ ) { + if( vowels.indexOf( text.charAt( i ) ) != -1 ) { + result++; + } + } + return result; + } + + public static String caps( String text ) { + return text.toUpperCase(Locale.ENGLISH); + } +} diff --git a/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/EchoAttributesTag.java b/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/EchoAttributesTag.java new file mode 100644 index 0000000..8ff624b --- /dev/null +++ b/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/EchoAttributesTag.java @@ -0,0 +1,56 @@ +/* + * 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 jsp2.examples.simpletag; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.tagext.DynamicAttributes; +import jakarta.servlet.jsp.tagext.SimpleTagSupport; + +/** + * SimpleTag handler that echoes all its attributes + */ +public class EchoAttributesTag + extends SimpleTagSupport + implements DynamicAttributes +{ + private final List keys = new ArrayList<>(); + private final List values = new ArrayList<>(); + + @Override + public void doTag() throws JspException, IOException { + JspWriter out = getJspContext().getOut(); + for( int i = 0; i < keys.size(); i++ ) { + String key = keys.get( i ); + Object value = values.get( i ); + out.println( "
  • " + key + " = " + value + "
  • " ); + } + } + + @Override + public void setDynamicAttribute( String uri, String localName, + Object value ) + throws JspException + { + keys.add( localName ); + values.add( value ); + } +} diff --git a/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/FindBookSimpleTag.java b/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/FindBookSimpleTag.java new file mode 100644 index 0000000..3d79a2e --- /dev/null +++ b/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/FindBookSimpleTag.java @@ -0,0 +1,44 @@ +/* + * 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 jsp2.examples.simpletag; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.SimpleTagSupport; + +import jsp2.examples.BookBean; + +/** + * SimpleTag handler that pretends to search for a book, and stores + * the result in a scoped variable. + */ +public class FindBookSimpleTag extends SimpleTagSupport { + private String var; + + private static final String BOOK_TITLE = "The Lord of the Rings"; + private static final String BOOK_AUTHOR = "J. R. R. Tolkien"; + private static final String BOOK_ISBN = "0618002251"; + + @Override + public void doTag() throws JspException { + BookBean book = new BookBean( BOOK_TITLE, BOOK_AUTHOR, BOOK_ISBN ); + getJspContext().setAttribute( this.var, book ); + } + + public void setVar( String var ) { + this.var = var; + } +} diff --git a/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/HelloWorldSimpleTag.java b/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/HelloWorldSimpleTag.java new file mode 100644 index 0000000..b56b35c --- /dev/null +++ b/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/HelloWorldSimpleTag.java @@ -0,0 +1,32 @@ +/* + * 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 jsp2.examples.simpletag; + +import java.io.IOException; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.SimpleTagSupport; + +/** + * SimpleTag handler that prints "Hello, world!" + */ +public class HelloWorldSimpleTag extends SimpleTagSupport { + @Override + public void doTag() throws JspException, IOException { + getJspContext().getOut().write( "Hello, world!" ); + } +} diff --git a/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/RepeatSimpleTag.java b/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/RepeatSimpleTag.java new file mode 100644 index 0000000..2dc6c9a --- /dev/null +++ b/webapps/examples/WEB-INF/classes/jsp2/examples/simpletag/RepeatSimpleTag.java @@ -0,0 +1,42 @@ +/* + * 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 jsp2.examples.simpletag; + +import java.io.IOException; + +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.SimpleTagSupport; + +/** + * SimpleTag handler that accepts a num attribute and + * invokes its body 'num' times. + */ +public class RepeatSimpleTag extends SimpleTagSupport { + private int num; + + @Override + public void doTag() throws JspException, IOException { + for (int i=0; i
    " + this.label + + "
    " ); + } + + public void setColor( String color ) { + this.color = color; + } + + public void setLabel( String label ) { + this.label = label; + } +} diff --git a/webapps/examples/WEB-INF/classes/listeners/ContextListener.java b/webapps/examples/WEB-INF/classes/listeners/ContextListener.java new file mode 100644 index 0000000..c565566 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/listeners/ContextListener.java @@ -0,0 +1,139 @@ +/* + * 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 listeners; + + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextAttributeEvent; +import jakarta.servlet.ServletContextAttributeListener; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + + +/** + * Example listener for context-related application events, which were + * introduced in the 2.3 version of the Servlet API. This listener + * merely documents the occurrence of such events in the application log + * associated with our servlet context. + * + * @author Craig R. McClanahan + */ +public final class ContextListener + implements ServletContextAttributeListener, ServletContextListener { + + + // ----------------------------------------------------- Instance Variables + + + /** + * The servlet context with which we are associated. + */ + private ServletContext context = null; + + + // --------------------------------------------------------- Public Methods + + + /** + * Record the fact that a servlet context attribute was added. + * + * @param event The servlet context attribute event + */ + @Override + public void attributeAdded(ServletContextAttributeEvent event) { + + log("attributeAdded('" + event.getName() + "', '" + + event.getValue() + "')"); + + } + + + /** + * Record the fact that a servlet context attribute was removed. + * + * @param event The servlet context attribute event + */ + @Override + public void attributeRemoved(ServletContextAttributeEvent event) { + + log("attributeRemoved('" + event.getName() + "', '" + + event.getValue() + "')"); + + } + + + /** + * Record the fact that a servlet context attribute was replaced. + * + * @param event The servlet context attribute event + */ + @Override + public void attributeReplaced(ServletContextAttributeEvent event) { + + log("attributeReplaced('" + event.getName() + "', '" + + event.getValue() + "')"); + + } + + + /** + * Record the fact that this web application has been destroyed. + * + * @param event The servlet context event + */ + @Override + public void contextDestroyed(ServletContextEvent event) { + + log("contextDestroyed()"); + this.context = null; + + } + + + /** + * Record the fact that this web application has been initialized. + * + * @param event The servlet context event + */ + @Override + public void contextInitialized(ServletContextEvent event) { + + this.context = event.getServletContext(); + log("contextInitialized()"); + + } + + + // -------------------------------------------------------- Private Methods + + + /** + * Log a message to the servlet context application log. + * + * @param message Message to be logged + */ + private void log(String message) { + + if (context != null) { + context.log("ContextListener: " + message); + } else { + System.out.println("ContextListener: " + message); + } + + } + +} diff --git a/webapps/examples/WEB-INF/classes/listeners/SessionListener.java b/webapps/examples/WEB-INF/classes/listeners/SessionListener.java new file mode 100644 index 0000000..573a769 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/listeners/SessionListener.java @@ -0,0 +1,161 @@ +/* + * 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 listeners; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.http.HttpSessionAttributeListener; +import jakarta.servlet.http.HttpSessionBindingEvent; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; + +/** + * Example listener for context-related application events, which were + * introduced in the 2.3 version of the Servlet API. This listener merely + * documents the occurrence of such events in the application log associated + * with our servlet context. + * + * @author Craig R. McClanahan + */ +public final class SessionListener implements ServletContextListener, + HttpSessionAttributeListener, HttpSessionListener { + + // ----------------------------------------------------- Instance Variables + + /** + * The servlet context with which we are associated. + */ + private ServletContext context = null; + + // --------------------------------------------------------- Public Methods + + /** + * Record the fact that a servlet context attribute was added. + * + * @param event + * The session attribute event + */ + @Override + public void attributeAdded(HttpSessionBindingEvent event) { + + log("attributeAdded('" + event.getSession().getId() + "', '" + + event.getName() + "', '" + event.getValue() + "')"); + + } + + /** + * Record the fact that a servlet context attribute was removed. + * + * @param event + * The session attribute event + */ + @Override + public void attributeRemoved(HttpSessionBindingEvent event) { + + log("attributeRemoved('" + event.getSession().getId() + "', '" + + event.getName() + "', '" + event.getValue() + "')"); + + } + + /** + * Record the fact that a servlet context attribute was replaced. + * + * @param event + * The session attribute event + */ + @Override + public void attributeReplaced(HttpSessionBindingEvent event) { + + log("attributeReplaced('" + event.getSession().getId() + "', '" + + event.getName() + "', '" + event.getValue() + "')"); + + } + + /** + * Record the fact that this web application has been destroyed. + * + * @param event + * The servlet context event + */ + @Override + public void contextDestroyed(ServletContextEvent event) { + + log("contextDestroyed()"); + this.context = null; + + } + + /** + * Record the fact that this web application has been initialized. + * + * @param event + * The servlet context event + */ + @Override + public void contextInitialized(ServletContextEvent event) { + + this.context = event.getServletContext(); + log("contextInitialized()"); + + } + + /** + * Record the fact that a session has been created. + * + * @param event + * The session event + */ + @Override + public void sessionCreated(HttpSessionEvent event) { + + log("sessionCreated('" + event.getSession().getId() + "')"); + + } + + /** + * Record the fact that a session has been destroyed. + * + * @param event + * The session event + */ + @Override + public void sessionDestroyed(HttpSessionEvent event) { + + log("sessionDestroyed('" + event.getSession().getId() + "')"); + + } + + // -------------------------------------------------------- Private Methods + + /** + * Log a message to the servlet context application log. + * + * @param message + * Message to be logged + */ + private void log(String message) { + + if (context != null) { + context.log("SessionListener: " + message); + } else { + System.out.println("SessionListener: " + message); + } + + } + +} diff --git a/webapps/examples/WEB-INF/classes/nonblocking/ByteCounter.java b/webapps/examples/WEB-INF/classes/nonblocking/ByteCounter.java new file mode 100644 index 0000000..7b3d959 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/nonblocking/ByteCounter.java @@ -0,0 +1,142 @@ +/* + * 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 nonblocking; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * This doesn't do anything particularly useful - it just counts the total + * number of bytes in a request body while demonstrating how to perform + * non-blocking reads. + */ +public class ByteCounter extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + resp.getWriter().println("Try again using a POST request."); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + // Non-blocking IO requires async + AsyncContext ac = req.startAsync(); + + // Use a single listener for read and write. Listeners often need to + // share state to coordinate reads and writes and this is much easier as + // a single object. + @SuppressWarnings("unused") + CounterListener listener = new CounterListener( + ac, req.getInputStream(), resp.getOutputStream()); + } + + + /** + * Keep in mind that each call may well be on a different thread to the + * previous call. Ensure that changes in values will be visible across + * threads. There should only ever be one container thread at a time calling + * the listener. + */ + private static class CounterListener implements ReadListener, WriteListener { + + private final AsyncContext ac; + private final ServletInputStream sis; + private final ServletOutputStream sos; + + private volatile boolean readFinished = false; + private volatile long totalBytesRead = 0; + private byte[] buffer = new byte[8192]; + + private CounterListener(AsyncContext ac, ServletInputStream sis, + ServletOutputStream sos) { + this.ac = ac; + this.sis = sis; + this.sos = sos; + + // In Tomcat, the order the listeners are set controls the order + // that the first calls are made. In this case, the read listener + // will be called before the write listener. + sis.setReadListener(this); + sos.setWriteListener(this); + } + + @Override + public void onDataAvailable() throws IOException { + int read = 0; + // Loop as long as there is data to read. If isReady() returns false + // the socket will be added to the poller and onDataAvailable() will + // be called again as soon as there is more data to read. + while (sis.isReady() && read > -1) { + read = sis.read(buffer); + if (read > 0) { + totalBytesRead += read; + } + } + } + + @Override + public void onAllDataRead() throws IOException { + readFinished = true; + + // If sos is not ready to write data, the call to isReady() will + // register the socket with the poller which will trigger a call to + // onWritePossible() when the socket is ready to have data written + // to it. + if (sos.isReady()) { + onWritePossible(); + } + } + + @Override + public void onWritePossible() throws IOException { + if (readFinished) { + // Must be ready to write data if onWritePossible was called + String msg = "Total bytes written = [" + totalBytesRead + "]"; + sos.write(msg.getBytes(StandardCharsets.UTF_8)); + ac.complete(); + } + } + + @Override + public void onError(Throwable throwable) { + // Should probably log the throwable + ac.complete(); + } + } +} diff --git a/webapps/examples/WEB-INF/classes/nonblocking/NumberWriter.java b/webapps/examples/WEB-INF/classes/nonblocking/NumberWriter.java new file mode 100644 index 0000000..0c9ed29 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/nonblocking/NumberWriter.java @@ -0,0 +1,148 @@ +/* + * 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 nonblocking; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * This doesn't do anything particularly useful - it just writes a series of + * numbers to the response body while demonstrating how to perform non-blocking + * writes. + */ +public class NumberWriter extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + // Non-blocking IO requires async + AsyncContext ac = req.startAsync(); + + // Use a single listener for read and write. Listeners often need to + // share state to coordinate reads and writes and this is much easier as + // a single object. + @SuppressWarnings("unused") + NumberWriterListener listener = new NumberWriterListener( + ac, req.getInputStream(), resp.getOutputStream()); + + } + + + /** + * Keep in mind that each call may well be on a different thread to the + * previous call. Ensure that changes in values will be visible across + * threads. There should only ever be one container thread at a time calling + * the listener. + */ + private static class NumberWriterListener implements ReadListener, + WriteListener { + + private static final int LIMIT = 10000; + + private final AsyncContext ac; + private final ServletInputStream sis; + private final ServletOutputStream sos; + private final AtomicInteger counter = new AtomicInteger(0); + + private volatile boolean readFinished = false; + private byte[] buffer = new byte[8192]; + + private NumberWriterListener(AsyncContext ac, ServletInputStream sis, + ServletOutputStream sos) { + this.ac = ac; + this.sis = sis; + this.sos = sos; + + // In Tomcat, the order the listeners are set controls the order + // that the first calls are made. In this case, the read listener + // will be called before the write listener. + sis.setReadListener(this); + sos.setWriteListener(this); + } + + @Override + public void onDataAvailable() throws IOException { + + // There should be no data to read + + int read = 0; + // Loop as long as there is data to read. If isReady() returns false + // the socket will be added to the poller and onDataAvailable() will + // be called again as soon as there is more data to read. + while (sis.isReady() && read > -1) { + read = sis.read(buffer); + if (read > 0) { + throw new IOException("Data was present in input stream"); + } + } + } + + @Override + public void onAllDataRead() throws IOException { + readFinished = true; + + // If sos is not ready to write data, the call to isReady() will + // register the socket with the poller which will trigger a call to + // onWritePossible() when the socket is ready to have data written + // to it. + if (sos.isReady()) { + onWritePossible(); + } + } + + @Override + public void onWritePossible() throws IOException { + if (readFinished) { + int i = counter.get(); + boolean ready = true; + while (i < LIMIT && ready) { + i = counter.incrementAndGet(); + String msg = String.format("%1$020d\n", Integer.valueOf(i)); + sos.write(msg.getBytes(StandardCharsets.UTF_8)); + ready = sos.isReady(); + } + + if (i == LIMIT) { + ac.complete(); + } + } + } + + @Override + public void onError(Throwable throwable) { + // Should probably log the throwable + ac.complete(); + } + } +} diff --git a/webapps/examples/WEB-INF/classes/num/NumberGuessBean.java b/webapps/examples/WEB-INF/classes/num/NumberGuessBean.java new file mode 100644 index 0000000..c47c211 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/num/NumberGuessBean.java @@ -0,0 +1,98 @@ +/* + * 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. + */ + +/* + * Originally written by Jason Hunter, http://www.servlets.com. + */ +package num; + +import java.io.Serializable; +import java.util.Random; + +public class NumberGuessBean implements Serializable { + + private static final long serialVersionUID = 1L; + + private int answer; + private String hint; + private int numGuesses; + private boolean success; + private final Random random = new Random(); + + public NumberGuessBean() { + reset(); + } + + public int getAnswer() { + return answer; + } + + public void setAnswer(int answer) { + this.answer = answer; + } + + public String getHint() { + return "" + hint; + } + + public void setHint(String hint) { + this.hint = hint; + } + + public void setNumGuesses(int numGuesses) { + this.numGuesses = numGuesses; + } + + public int getNumGuesses() { + return numGuesses; + } + + public boolean getSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public void setGuess(String guess) { + numGuesses++; + + int g; + try { + g = Integer.parseInt(guess); + } catch (NumberFormatException e) { + g = -1; + } + + if (g == answer) { + success = true; + } else if (g == -1) { + hint = "a number next time"; + } else if (g < answer) { + hint = "higher"; + } else if (g > answer) { + hint = "lower"; + } + } + + public void reset() { + answer = Math.abs(random.nextInt() % 100) + 1; + success = false; + numGuesses = 0; + } +} diff --git a/webapps/examples/WEB-INF/classes/sessions/DummyCart.java b/webapps/examples/WEB-INF/classes/sessions/DummyCart.java new file mode 100644 index 0000000..44decc9 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/sessions/DummyCart.java @@ -0,0 +1,66 @@ +/* + * 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 sessions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DummyCart { + final List items = Collections.synchronizedList(new ArrayList<>()); + String submit = null; + String item = null; + + private void addItem(String name) { + items.add(name); + } + + private void removeItem(String name) { + items.remove(name); + } + + public void setItem(String name) { + item = name; + } + + public void setSubmit(String s) { + submit = s; + } + + public String[] getItems() { + return items.toArray(new String[0]); + } + + public void processRequest() { + // null value for submit - user hit enter instead of clicking on + // "add" or "remove" + if (submit == null || submit.equals("add")) { + addItem(item); + } else if (submit.equals("remove")) { + removeItem(item); + } + + // reset at the end of the request + reset(); + } + + // reset + private void reset() { + submit = null; + item = null; + } +} diff --git a/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java b/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java new file mode 100644 index 0000000..31818ac --- /dev/null +++ b/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java @@ -0,0 +1,68 @@ +/* + * 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 trailers; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * This example writes some trailer fields to the HTTP response. + */ +public class ResponseTrailers extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final Supplier> TRAILER_FIELD_SUPPLIER = + new TrailerFieldSupplier(); + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setTrailerFields(TRAILER_FIELD_SUPPLIER); + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + PrintWriter pw = resp.getWriter(); + + pw.print("This response should include trailer fields."); + } + + + private static class TrailerFieldSupplier implements Supplier> { + + private static final Map trailerFields = new HashMap<>(); + + static { + trailerFields.put("x-trailer-1", "Trailer value one"); + trailerFields.put("x-trailer-2", "Trailer value two"); + } + + @Override + public Map get() { + return trailerFields; + } + } +} diff --git a/webapps/examples/WEB-INF/classes/util/CookieFilter.java b/webapps/examples/WEB-INF/classes/util/CookieFilter.java new file mode 100644 index 0000000..d7c8550 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/util/CookieFilter.java @@ -0,0 +1,85 @@ +/* + * 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 util; + +import java.util.Locale; +import java.util.StringTokenizer; + +/** + * Processes a cookie header and attempts to obfuscate any cookie values that + * represent session IDs from other web applications. Since session cookie names + * are configurable, as are session ID lengths, this filter is not expected to + * be 100% effective. + * + * It is required that the examples web application is removed in security + * conscious environments as documented in the Security How-To. This filter is + * intended to reduce the impact of failing to follow that advice. A failure by + * this filter to obfuscate a session ID or similar value is not a security + * vulnerability. In such instances the vulnerability is the failure to remove + * the examples web application. + */ +public class CookieFilter { + + private static final String OBFUSCATED = "[obfuscated]"; + + private CookieFilter() { + // Hide default constructor + } + + public static String filter(String cookieHeader, String sessionId) { + + StringBuilder sb = new StringBuilder(cookieHeader.length()); + + // Cookie name value pairs are ';' separated. + // Session IDs don't use ; in the value so don't worry about quoted + // values that contain ; + StringTokenizer st = new StringTokenizer(cookieHeader, ";"); + + boolean first = true; + while (st.hasMoreTokens()) { + if (first) { + first = false; + } else { + sb.append(';'); + } + sb.append(filterNameValuePair(st.nextToken(), sessionId)); + } + + + return sb.toString(); + } + + private static String filterNameValuePair(String input, String sessionId) { + int i = input.indexOf('='); + if (i == -1) { + return input; + } + String name = input.substring(0, i); + String value = input.substring(i + 1); + + return name + "=" + filter(name, value, sessionId); + } + + public static String filter(String cookieName, String cookieValue, String sessionId) { + if (cookieName.toLowerCase(Locale.ENGLISH).contains("jsessionid") && + (sessionId == null || !cookieValue.contains(sessionId))) { + cookieValue = OBFUSCATED; + } + + return cookieValue; + } +} diff --git a/webapps/examples/WEB-INF/classes/util/HTMLFilter.java b/webapps/examples/WEB-INF/classes/util/HTMLFilter.java new file mode 100644 index 0000000..3d18d33 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/util/HTMLFilter.java @@ -0,0 +1,69 @@ +/* + * 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 util; + +/** + * HTML filter utility. + * + * @author Craig R. McClanahan + * @author Tim Tye + */ +public final class HTMLFilter { + + + /** + * Filter the specified message string for characters that are sensitive + * in HTML. This avoids potential attacks caused by including JavaScript + * codes in the request URL that is often reported in error messages. + * + * @param message The message string to be filtered + * + * @return the filtered version of the message + */ + public static String filter(String message) { + + if (message == null) { + return null; + } + + char content[] = new char[message.length()]; + message.getChars(0, message.length(), content, 0); + StringBuilder result = new StringBuilder(content.length + 50); + for (char c : content) { + switch (c) { + case '<': + result.append("<"); + break; + case '>': + result.append(">"); + break; + case '&': + result.append("&"); + break; + case '"': + result.append("""); + break; + default: + result.append(c); + } + } + return result.toString(); + } + + +} + diff --git a/webapps/examples/WEB-INF/classes/validators/DebugValidator.java b/webapps/examples/WEB-INF/classes/validators/DebugValidator.java new file mode 100644 index 0000000..90b799b --- /dev/null +++ b/webapps/examples/WEB-INF/classes/validators/DebugValidator.java @@ -0,0 +1,83 @@ +/* + * 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 validators; + + +import java.io.IOException; +import java.io.InputStream; + +import jakarta.servlet.jsp.tagext.PageData; +import jakarta.servlet.jsp.tagext.TagLibraryValidator; +import jakarta.servlet.jsp.tagext.ValidationMessage; + + +/** + * Example tag library validator that simply dumps the XML version of each + * page to standard output (which will typically be sent to the file + * $CATALINA_HOME/logs/catalina.out). To utilize it, simply + * include a taglib directive for this tag library at the top + * of your JSP page. + * + * @author Craig McClanahan + */ +public class DebugValidator extends TagLibraryValidator { + + + // ----------------------------------------------------- Instance Variables + + + // --------------------------------------------------------- Public Methods + + + /** + * Validate a JSP page. This will get invoked once per directive in the + * JSP page. This method will return null if the page is + * valid; otherwise the method should return an array of + * ValidationMessage objects. An array of length zero is + * also interpreted as no errors. + * + * @param prefix The value of the prefix argument in this directive + * @param uri The value of the URI argument in this directive + * @param page The page data for this page + */ + @Override + public ValidationMessage[] validate(String prefix, String uri, + PageData page) { + + System.out.println("---------- Prefix=" + prefix + " URI=" + uri + + "----------"); + + InputStream is = page.getInputStream(); + while (true) { + try { + int ch = is.read(); + if (ch < 0) { + break; + } + System.out.print((char) ch); + } catch (IOException e) { + break; + } + } + System.out.println(); + System.out.println("-----------------------------------------------"); + return null; + + } + + +} diff --git a/webapps/examples/WEB-INF/classes/websocket/ExamplesConfig.java b/webapps/examples/WEB-INF/classes/websocket/ExamplesConfig.java new file mode 100644 index 0000000..5222a17 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/ExamplesConfig.java @@ -0,0 +1,66 @@ +/* + * 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 websocket; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.websocket.Endpoint; +import jakarta.websocket.server.ServerApplicationConfig; +import jakarta.websocket.server.ServerEndpointConfig; + +import websocket.drawboard.DrawboardEndpoint; +import websocket.echo.EchoEndpoint; + +public class ExamplesConfig implements ServerApplicationConfig { + + @Override + public Set getEndpointConfigs( + Set> scanned) { + + Set result = new HashSet<>(); + + if (scanned.contains(EchoEndpoint.class)) { + result.add(ServerEndpointConfig.Builder.create( + EchoEndpoint.class, + "/websocket/echoProgrammatic").build()); + } + + if (scanned.contains(DrawboardEndpoint.class)) { + result.add(ServerEndpointConfig.Builder.create( + DrawboardEndpoint.class, + "/websocket/drawboard").build()); + } + + return result; + } + + + @Override + public Set> getAnnotatedEndpointClasses(Set> scanned) { + // Deploy all WebSocket endpoints defined by annotations in the examples + // web application. Filter out all others to avoid issues when running + // tests on Gump + Set> results = new HashSet<>(); + for (Class clazz : scanned) { + if (clazz.getPackage().getName().startsWith("websocket.")) { + results.add(clazz); + } + } + return results; + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/chat/ChatAnnotation.java b/webapps/examples/WEB-INF/classes/websocket/chat/ChatAnnotation.java new file mode 100644 index 0000000..c0a2f2b --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/chat/ChatAnnotation.java @@ -0,0 +1,109 @@ +/* + * 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 websocket.chat; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +import util.HTMLFilter; + +@ServerEndpoint(value = "/websocket/chat") +public class ChatAnnotation { + + private static final Log log = LogFactory.getLog(ChatAnnotation.class); + + private static final String GUEST_PREFIX = "Guest"; + private static final AtomicInteger connectionIds = new AtomicInteger(0); + private static final Set connections = + new CopyOnWriteArraySet<>(); + + private final String nickname; + private Session session; + + public ChatAnnotation() { + nickname = GUEST_PREFIX + connectionIds.getAndIncrement(); + } + + + @OnOpen + public void start(Session session) { + this.session = session; + connections.add(this); + String message = String.format("* %s %s", nickname, "has joined."); + broadcast(message); + } + + + @OnClose + public void end() { + connections.remove(this); + String message = String.format("* %s %s", + nickname, "has disconnected."); + broadcast(message); + } + + + @OnMessage + public void incoming(String message) { + // Never trust the client + String filteredMessage = String.format("%s: %s", + nickname, HTMLFilter.filter(message.toString())); + broadcast(filteredMessage); + } + + + + + @OnError + public void onError(Throwable t) throws Throwable { + log.error("Chat Error: " + t.toString(), t); + } + + + private static void broadcast(String msg) { + for (ChatAnnotation client : connections) { + try { + synchronized (client) { + client.session.getBasicRemote().sendText(msg); + } + } catch (IOException e) { + log.debug("Chat Error: Failed to send message to client", e); + connections.remove(client); + try { + client.session.close(); + } catch (IOException e1) { + // Ignore + } + String message = String.format("* %s %s", + client.nickname, "has been disconnected."); + broadcast(message); + } + } + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java new file mode 100644 index 0000000..a99aad0 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/Client.java @@ -0,0 +1,231 @@ +/* + * 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 websocket.drawboard; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.RemoteEndpoint.Async; +import jakarta.websocket.SendHandler; +import jakarta.websocket.SendResult; +import jakarta.websocket.Session; + +import websocket.drawboard.wsmessages.AbstractWebsocketMessage; +import websocket.drawboard.wsmessages.BinaryWebsocketMessage; +import websocket.drawboard.wsmessages.CloseWebsocketMessage; +import websocket.drawboard.wsmessages.StringWebsocketMessage; + +/** + * Represents a client with methods to send messages asynchronously. + */ +public class Client { + + private final Session session; + private final Async async; + + /** + * Contains the messages which are buffered until the previous + * send operation has finished. + */ + private final Deque messagesToSend = new ArrayDeque<>(); + /** + * If this client is currently sending a messages asynchronously. + */ + private volatile boolean isSendingMessage = false; + /** + * If this client is closing. If true, new messages to + * send will be ignored. + */ + private volatile boolean isClosing = false; + /** + * The length of all current buffered messages, to avoid iterating + * over a linked list. + */ + private volatile long messagesToSendLength = 0; + + public Client(Session session) { + this.session = session; + this.async = session.getAsyncRemote(); + } + + /** + * Asynchronously closes the Websocket session. This will wait until all + * remaining messages have been sent to the Client and then close + * the Websocket session. + */ + public void close() { + sendMessage(new CloseWebsocketMessage()); + } + + /** + * Sends the given message asynchronously to the client. + * If there is already a async sending in progress, then the message + * will be buffered and sent when possible.

    + * + * This method can be called from multiple threads. + * + * @param msg The message to send + */ + public void sendMessage(AbstractWebsocketMessage msg) { + synchronized (messagesToSend) { + if (!isClosing) { + // Check if we have a Close message + if (msg instanceof CloseWebsocketMessage) { + isClosing = true; + } + + if (isSendingMessage) { + // Check if the buffered messages exceed + // a specific amount - in that case, disconnect the client + // to prevent DoS. + // In this case we check if there are >= 1000 messages + // or length(of all messages) >= 1000000 bytes. + if (messagesToSend.size() >= 1000 + || messagesToSendLength >= 1000000) { + isClosing = true; + + // Discard the new message and close the session immediately. + CloseReason cr = new CloseReason( + CloseCodes.VIOLATED_POLICY, + "Send Buffer exceeded"); + try { + // TODO: close() may block if the remote endpoint doesn't read the data + // (eventually there will be a TimeoutException). However, this method + // (sendMessage) is intended to run asynchronous code and shouldn't + // block. Otherwise it would temporarily stop processing of messages + // from other clients. + // Maybe call this method on another thread. + // Note that when this method is called, the RemoteEndpoint.Async + // is still in the process of sending data, so there probably should + // be another way to cancel the Websocket connection. + // Ideally, there should be some method that cancels the connection + // immediately... + session.close(cr); + } catch (IOException e) { + // Ignore + } + + } else { + + // Check if the last message and the new message are + // String messages - in that case we concatenate them + // to reduce TCP overhead (using ";" as separator). + if (msg instanceof StringWebsocketMessage + && !messagesToSend.isEmpty() + && messagesToSend.getLast() + instanceof StringWebsocketMessage) { + + StringWebsocketMessage ms = + (StringWebsocketMessage) messagesToSend.removeLast(); + messagesToSendLength -= calculateMessageLength(ms); + + String concatenated = ms.getString() + ";" + + ((StringWebsocketMessage) msg).getString(); + msg = new StringWebsocketMessage(concatenated); + } + + messagesToSend.add(msg); + messagesToSendLength += calculateMessageLength(msg); + } + } else { + isSendingMessage = true; + internalSendMessageAsync(msg); + } + } + + } + } + + private long calculateMessageLength(AbstractWebsocketMessage msg) { + if (msg instanceof BinaryWebsocketMessage) { + return ((BinaryWebsocketMessage) msg).getBytes().capacity(); + } else if (msg instanceof StringWebsocketMessage) { + return ((StringWebsocketMessage) msg).getString().length() * 2; + } + + return 0; + } + + /** + * Internally sends the messages asynchronously. + * + * @param msg Message to send + */ + private void internalSendMessageAsync(AbstractWebsocketMessage msg) { + try { + if (msg instanceof StringWebsocketMessage) { + StringWebsocketMessage sMsg = (StringWebsocketMessage) msg; + async.sendText(sMsg.getString(), sendHandler); + + } else if (msg instanceof BinaryWebsocketMessage) { + BinaryWebsocketMessage bMsg = (BinaryWebsocketMessage) msg; + async.sendBinary(bMsg.getBytes(), sendHandler); + + } else if (msg instanceof CloseWebsocketMessage) { + // Close the session. + session.close(); + } + } catch (IllegalStateException|IOException ex) { + // Trying to write to the client when the session has + // already been closed. + // Ignore + } + } + + + + /** + * SendHandler that will continue to send buffered messages. + */ + private final SendHandler sendHandler = new SendHandler() { + @Override + public void onResult(SendResult result) { + if (!result.isOK()) { + // Message could not be sent. In this case, we don't + // set isSendingMessage to false because we must assume the connection + // broke (and onClose will be called), so we don't try to send + // other messages. + // As a precaution, we close the session (e.g. if a send timeout occurred). + // TODO: session.close() blocks, while this handler shouldn't block. + // Ideally, there should be some method that cancels the connection + // immediately... + try { + session.close(); + } catch (IOException ex) { + // Ignore + } + } + synchronized (messagesToSend) { + + if (!messagesToSend.isEmpty()) { + AbstractWebsocketMessage msg = messagesToSend.remove(); + messagesToSendLength -= calculateMessageLength(msg); + + internalSendMessageAsync(msg); + + } else { + isSendingMessage = false; + } + + } + } + }; + +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java new file mode 100644 index 0000000..33de557 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawMessage.java @@ -0,0 +1,253 @@ +/* + * 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 websocket.drawboard; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.Arc2D; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; + +/** + * A message that represents a drawing action. + * Note that we use primitive types instead of Point, Color etc. + * to reduce object allocation.

    + * + * TODO: But a Color objects needs to be created anyway for drawing this + * onto a Graphics2D object, so this probably does not save much. + */ +public final class DrawMessage { + + private int type; + private byte colorR, colorG, colorB, colorA; + private double thickness; + private double x1, y1, x2, y2; + + /** + * The type. + * + * @return 1: Brush
    2: Line
    3: Rectangle
    4: Ellipse + */ + public int getType() { + return type; + } + public void setType(int type) { + this.type = type; + } + + public double getThickness() { + return thickness; + } + public void setThickness(double thickness) { + this.thickness = thickness; + } + + public byte getColorR() { + return colorR; + } + public void setColorR(byte colorR) { + this.colorR = colorR; + } + public byte getColorG() { + return colorG; + } + public void setColorG(byte colorG) { + this.colorG = colorG; + } + public byte getColorB() { + return colorB; + } + public void setColorB(byte colorB) { + this.colorB = colorB; + } + public byte getColorA() { + return colorA; + } + public void setColorA(byte colorA) { + this.colorA = colorA; + } + + public double getX1() { + return x1; + } + public void setX1(double x1) { + this.x1 = x1; + } + public double getX2() { + return x2; + } + public void setX2(double x2) { + this.x2 = x2; + } + public double getY1() { + return y1; + } + public void setY1(double y1) { + this.y1 = y1; + } + public double getY2() { + return y2; + } + public void setY2(double y2) { + this.y2 = y2; + } + + + public DrawMessage(int type, byte colorR, byte colorG, byte colorB, + byte colorA, double thickness, double x1, double x2, double y1, + double y2) { + + this.type = type; + this.colorR = colorR; + this.colorG = colorG; + this.colorB = colorB; + this.colorA = colorA; + this.thickness = thickness; + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + } + + + /** + * Draws this DrawMessage onto the given Graphics2D. + * + * @param g The target for the DrawMessage + */ + public void draw(Graphics2D g) { + + g.setStroke(new BasicStroke((float) thickness, + BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)); + g.setColor(new Color(colorR & 0xFF, colorG & 0xFF, colorB & 0xFF, + colorA & 0xFF)); + + if (x1 == x2 && y1 == y2) { + // Always draw as arc to meet the behavior in the HTML5 Canvas. + Arc2D arc = new Arc2D.Double(x1, y1, 0, 0, + 0d, 360d, Arc2D.OPEN); + g.draw(arc); + + } else if (type == 1 || type == 2) { + // Draw a line. + Line2D line = new Line2D.Double(x1, y1, x2, y2); + g.draw(line); + + } else if (type == 3 || type == 4) { + double x1 = this.x1, x2 = this.x2, + y1 = this.y1, y2 = this.y2; + if (x1 > x2) { + x1 = this.x2; + x2 = this.x1; + } + if (y1 > y2) { + y1 = this.y2; + y2 = this.y1; + } + + // TODO: If (x1 == x2 || y1 == y2) draw as line. + + if (type == 3) { + // Draw a rectangle. + Rectangle2D rect = new Rectangle2D.Double(x1, y1, + x2 - x1, y2 - y1); + g.draw(rect); + + } else if (type == 4) { + // Draw an ellipse. + Arc2D arc = new Arc2D.Double(x1, y1, x2 - x1, y2 - y1, + 0d, 360d, Arc2D.OPEN); + g.draw(arc); + + } + } + } + + /** + * Converts this message into a String representation that + * can be sent over WebSocket.
    + * Since a DrawMessage consists only of numbers, + * we concatenate those numbers with a ",". + */ + @Override + public String toString() { + + return type + "," + (colorR & 0xFF) + "," + (colorG & 0xFF) + "," + + (colorB & 0xFF) + "," + (colorA & 0xFF) + "," + thickness + + "," + x1 + "," + y1 + "," + x2 + "," + y2; + } + + public static DrawMessage parseFromString(String str) + throws ParseException { + + int type; + byte[] colors = new byte[4]; + double thickness; + double[] coords = new double[4]; + + try { + String[] elements = str.split(","); + + type = Integer.parseInt(elements[0]); + if (!(type >= 1 && type <= 4)) { + throw new ParseException("Invalid type: " + type); + } + + for (int i = 0; i < colors.length; i++) { + colors[i] = (byte) Integer.parseInt(elements[1 + i]); + } + + thickness = Double.parseDouble(elements[5]); + if (Double.isNaN(thickness) || thickness < 0 || thickness > 100) { + throw new ParseException("Invalid thickness: " + thickness); + } + + for (int i = 0; i < coords.length; i++) { + coords[i] = Double.parseDouble(elements[6 + i]); + if (Double.isNaN(coords[i])) { + throw new ParseException("Invalid coordinate: " + + coords[i]); + } + } + + } catch (RuntimeException ex) { + throw new ParseException(ex); + } + + DrawMessage m = new DrawMessage(type, colors[0], colors[1], + colors[2], colors[3], thickness, coords[0], coords[2], + coords[1], coords[3]); + + return m; + } + + public static class ParseException extends Exception { + private static final long serialVersionUID = -6651972769789842960L; + + public ParseException(Throwable root) { + super(root); + } + + public ParseException(String message) { + super(message); + } + } + + + +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardContextListener.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardContextListener.java new file mode 100644 index 0000000..dfa2a97 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardContextListener.java @@ -0,0 +1,32 @@ +/* + * 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 websocket.drawboard; + +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +public final class DrawboardContextListener implements ServletContextListener { + + @Override + public void contextDestroyed(ServletContextEvent sce) { + // Shutdown our room. + Room room = DrawboardEndpoint.getRoom(false); + if (room != null) { + room.shutdown(); + } + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java new file mode 100644 index 0000000..10ffd2c --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/DrawboardEndpoint.java @@ -0,0 +1,236 @@ +/* + * 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 websocket.drawboard; + +import java.io.EOFException; +import java.io.IOException; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.Session; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +import websocket.drawboard.DrawMessage.ParseException; +import websocket.drawboard.wsmessages.StringWebsocketMessage; + + +public final class DrawboardEndpoint extends Endpoint { + + private static final Log log = + LogFactory.getLog(DrawboardEndpoint.class); + + + /** + * Our room where players can join. + */ + private static volatile Room room = null; + private static final Object roomLock = new Object(); + + public static Room getRoom(boolean create) { + if (create) { + if (room == null) { + synchronized (roomLock) { + if (room == null) { + room = new Room(); + } + } + } + return room; + } else { + return room; + } + } + + /** + * The player that is associated with this Endpoint and the current room. + * Note that this variable is only accessed from the Room Thread.

    + * + * TODO: Currently, Tomcat uses an Endpoint instance once - however + * the java doc of endpoint says: + * "Each instance of a websocket endpoint is guaranteed not to be called by + * more than one thread at a time per active connection." + * This could mean that after calling onClose(), the instance + * could be reused for another connection so onOpen() will get called + * (possibly from another thread).
    + * If this is the case, we would need a variable holder for the variables + * that are accessed by the Room thread, and read the reference to the holder + * at the beginning of onOpen, onMessage, onClose methods to ensure the room + * thread always gets the correct instance of the variable holder. + */ + private Room.Player player; + + + @Override + public void onOpen(Session session, EndpointConfig config) { + // Set maximum messages size to 10.000 bytes. + session.setMaxTextMessageBufferSize(10000); + session.addMessageHandler(stringHandler); + final Client client = new Client(session); + + final Room room = getRoom(true); + room.invokeAndWait(new Runnable() { + @Override + public void run() { + try { + + // Create a new Player and add it to the room. + try { + player = room.createAndAddPlayer(client); + } catch (IllegalStateException ex) { + // Probably the max. number of players has been + // reached. + client.sendMessage(new StringWebsocketMessage( + "0" + ex.getLocalizedMessage())); + // Close the connection. + client.close(); + } + + } catch (RuntimeException ex) { + log.error("Unexpected exception: " + ex.toString(), ex); + } + } + }); + + } + + + @Override + public void onClose(Session session, CloseReason closeReason) { + Room room = getRoom(false); + if (room != null) { + room.invokeAndWait(new Runnable() { + @Override + public void run() { + try { + // Player can be null if it couldn't enter the room + if (player != null) { + // Remove this player from the room. + player.removeFromRoom(); + + // Set player to null to prevent NPEs when onMessage events + // are processed (from other threads) after onClose has been + // called from different thread which closed the Websocket session. + player = null; + } + } catch (RuntimeException ex) { + log.error("Unexpected exception: " + ex.toString(), ex); + } + } + }); + } + } + + + + @Override + public void onError(Session session, Throwable t) { + // Most likely cause is a user closing their browser. Check to see if + // the root cause is EOF and if it is ignore it. + // Protect against infinite loops. + int count = 0; + Throwable root = t; + while (root.getCause() != null && count < 20) { + root = root.getCause(); + count ++; + } + if (root instanceof EOFException) { + // Assume this is triggered by the user closing their browser and + // ignore it. + } else if (!session.isOpen() && root instanceof IOException) { + // IOException after close. Assume this is a variation of the user + // closing their browser (or refreshing very quickly) and ignore it. + } else { + log.error("onError: " + t.toString(), t); + } + } + + + + private final MessageHandler.Whole stringHandler = + new MessageHandler.Whole<>() { + + @Override + public void onMessage(final String message) { + // Invoke handling of the message in the room. + room.invokeAndWait(new Runnable() { + @Override + public void run() { + try { + + // Currently, the only types of messages the client will send + // are draw messages prefixed by a Message ID + // (starting with char '1'), and pong messages (starting + // with char '0'). + // Draw messages should look like this: + // ID|type,colR,colB,colG,colA,thickness,x1,y1,x2,y2,lastInChain + + boolean dontSwallowException = false; + try { + char messageType = message.charAt(0); + String messageContent = message.substring(1); + switch (messageType) { + case '0': + // Pong message. + // Do nothing. + break; + + case '1': + // Draw message + int indexOfChar = messageContent.indexOf('|'); + long msgId = Long.parseLong( + messageContent.substring(0, indexOfChar)); + + DrawMessage msg = DrawMessage.parseFromString( + messageContent.substring(indexOfChar + 1)); + + // Don't ignore RuntimeExceptions thrown by + // this method + // TODO: Find a better solution than this variable + dontSwallowException = true; + if (player != null) { + player.handleDrawMessage(msg, msgId); + } + dontSwallowException = false; + + break; + } + } catch (ParseException e) { + // Client sent invalid data + // Ignore, TODO: maybe close connection + } catch (RuntimeException e) { + // Client sent invalid data. + // Ignore, TODO: maybe close connection + if (dontSwallowException) { + throw e; + } + } + + } catch (RuntimeException ex) { + log.error("Unexpected exception: " + ex.toString(), ex); + } + } + }); + + } + }; + + +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java new file mode 100644 index 0000000..7fe0267 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/Room.java @@ -0,0 +1,497 @@ +/* + * 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 websocket.drawboard; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.locks.ReentrantLock; + +import javax.imageio.ImageIO; + +import websocket.drawboard.wsmessages.BinaryWebsocketMessage; +import websocket.drawboard.wsmessages.StringWebsocketMessage; + +/** + * A Room represents a drawboard where a number of + * users participate.

    + * + * Note: Instance methods should only be invoked by calling + * {@link #invokeAndWait(Runnable)} to ensure access is correctly synchronized. + */ +public final class Room { + + /** + * Specifies the type of a room message that is sent to a client.
    + * Note: Currently we are sending simple string messages - for production + * apps, a JSON lib should be used for object-level messages.

    + * + * The number (single char) will be prefixed to the string when sending + * the message. + */ + public enum MessageType { + /** + * '0': Error: contains error message. + */ + ERROR('0'), + /** + * '1': DrawMessage: contains serialized DrawMessage(s) prefixed + * with the current Player's {@link Player#lastReceivedMessageId} + * and ",".
    + * Multiple draw messages are concatenated with "|" as separator. + */ + DRAW_MESSAGE('1'), + /** + * '2': ImageMessage: Contains number of current players in this room. + * After this message a Binary Websocket message will follow, + * containing the current Room image as PNG.
    + * This is the first message that a Room sends to a new Player. + */ + IMAGE_MESSAGE('2'), + /** + * '3': PlayerChanged: contains "+" or "-" which indicate a player + * was added or removed to this Room. + */ + PLAYER_CHANGED('3'); + + private final char flag; + + MessageType(char flag) { + this.flag = flag; + } + + } + + + /** + * The lock used to synchronize access to this Room. + */ + private final ReentrantLock roomLock = new ReentrantLock(); + + /** + * Indicates if this room has already been shutdown. + */ + private volatile boolean closed = false; + + /** + * If true, outgoing DrawMessages will be buffered until the + * drawmessageBroadcastTimer ticks. Otherwise they will be sent + * immediately. + */ + private static final boolean BUFFER_DRAW_MESSAGES = true; + + /** + * A timer which sends buffered drawmessages to the client at once + * at a regular interval, to avoid sending a lot of very small + * messages which would cause TCP overhead and high CPU usage. + */ + private final Timer drawmessageBroadcastTimer = new Timer(); + + private static final int TIMER_DELAY = 30; + + /** + * The current active broadcast timer task. If null, then no Broadcast task is scheduled. + * The Task will be scheduled if the first player enters the Room, and + * cancelled if the last player exits the Room, to avoid unnecessary timer executions. + */ + private TimerTask activeBroadcastTimerTask; + + + /** + * The current image of the room drawboard. DrawMessages that are + * received from Players will be drawn onto this image. + */ + private final BufferedImage roomImage = + new BufferedImage(900, 600, BufferedImage.TYPE_INT_RGB); + private final Graphics2D roomGraphics = roomImage.createGraphics(); + + + /** + * The maximum number of players that can join this room. + */ + private static final int MAX_PLAYER_COUNT = 100; + + /** + * List of all currently joined players. + */ + private final List players = new ArrayList<>(); + + + + public Room() { + roomGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + // Clear the image with white background. + roomGraphics.setBackground(Color.WHITE); + roomGraphics.clearRect(0, 0, roomImage.getWidth(), + roomImage.getHeight()); + } + + private TimerTask createBroadcastTimerTask() { + return new TimerTask() { + @Override + public void run() { + invokeAndWait(new Runnable() { + @Override + public void run() { + broadcastTimerTick(); + } + }); + } + }; + } + + /** + * Creates a Player from the given Client and adds it to this room. + * + * @param client the client + * + * @return The newly created player + */ + public Player createAndAddPlayer(Client client) { + if (players.size() >= MAX_PLAYER_COUNT) { + throw new IllegalStateException("Maximum player count (" + + MAX_PLAYER_COUNT + ") has been reached."); + } + + Player p = new Player(this, client); + + // Broadcast to the other players that one player joined. + broadcastRoomMessage(MessageType.PLAYER_CHANGED, "+"); + + // Add the new player to the list. + players.add(p); + + // If currently no Broadcast Timer Task is scheduled, then we need to create one. + if (activeBroadcastTimerTask == null) { + activeBroadcastTimerTask = createBroadcastTimerTask(); + drawmessageBroadcastTimer.schedule(activeBroadcastTimerTask, + TIMER_DELAY, TIMER_DELAY); + } + + // Send the current number of players and the current room image. + String content = String.valueOf(players.size()); + p.sendRoomMessage(MessageType.IMAGE_MESSAGE, content); + + // Store image as PNG + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try { + ImageIO.write(roomImage, "PNG", bout); + } catch (IOException e) { /* Should never happen */ } + + + // Send the image as binary message. + BinaryWebsocketMessage msg = new BinaryWebsocketMessage( + ByteBuffer.wrap(bout.toByteArray())); + p.getClient().sendMessage(msg); + + return p; + + } + + /** + * @see Player#removeFromRoom() + * @param p player to remove + */ + private void internalRemovePlayer(Player p) { + boolean removed = players.remove(p); + assert removed; + + // If the last player left the Room, we need to cancel the Broadcast Timer Task. + if (players.size() == 0) { + // Cancel the task. + // Note that it can happen that the TimerTask is just about to execute (from + // the Timer thread) but waits until all players are gone (or even until a new + // player is added to the list), and then executes. This is OK. To prevent it, + // a TimerTask subclass would need to have some boolean "cancel" instance variable and + // query it in the invocation of Room#invokeAndWait. + activeBroadcastTimerTask.cancel(); + activeBroadcastTimerTask = null; + } + + // Broadcast that one player is removed. + broadcastRoomMessage(MessageType.PLAYER_CHANGED, "-"); + } + + /** + * @see Player#handleDrawMessage(DrawMessage, long) + * @param p player + * @param msg message containing details of new shapes to draw + * @param msgId message ID + */ + private void internalHandleDrawMessage(Player p, DrawMessage msg, + long msgId) { + p.setLastReceivedMessageId(msgId); + + // Draw the RoomMessage onto our Room Image. + msg.draw(roomGraphics); + + // Broadcast the Draw Message. + broadcastDrawMessage(msg); + } + + + /** + * Broadcasts the given drawboard message to all connected players.
    + * Note: For DrawMessages, please use + * {@link #broadcastDrawMessage(DrawMessage)} + * as this method will buffer them and prefix them with the correct + * last received Message ID. + * @param type message type + * @param content message content + */ + private void broadcastRoomMessage(MessageType type, String content) { + for (Player p : players) { + p.sendRoomMessage(type, content); + } + } + + + /** + * Broadcast the given DrawMessage. This will buffer the message + * and the {@link #drawmessageBroadcastTimer} will broadcast them + * at a regular interval, prefixing them with the player's current + * {@link Player#lastReceivedMessageId}. + * @param msg message to broadcast + */ + private void broadcastDrawMessage(DrawMessage msg) { + if (!BUFFER_DRAW_MESSAGES) { + String msgStr = msg.toString(); + + for (Player p : players) { + String s = String.valueOf(p.getLastReceivedMessageId()) + + "," + msgStr; + p.sendRoomMessage(MessageType.DRAW_MESSAGE, s); + } + } else { + for (Player p : players) { + p.getBufferedDrawMessages().add(msg); + } + } + } + + + /** + * Tick handler for the broadcastTimer. + */ + private void broadcastTimerTick() { + // For each Player, send all per Player buffered + // DrawMessages, prefixing each DrawMessage with the player's + // lastReceivedMessageId. + // Multiple messages are concatenated with "|". + + for (Player p : players) { + + StringBuilder sb = new StringBuilder(); + List drawMessages = p.getBufferedDrawMessages(); + + if (drawMessages.size() > 0) { + for (int i = 0; i < drawMessages.size(); i++) { + DrawMessage msg = drawMessages.get(i); + + String s = String.valueOf(p.getLastReceivedMessageId()) + + "," + msg.toString(); + if (i > 0) { + sb.append('|'); + } + + sb.append(s); + } + drawMessages.clear(); + + p.sendRoomMessage(MessageType.DRAW_MESSAGE, sb.toString()); + } + } + } + + /** + * A list of cached {@link Runnable}s to prevent recursive invocation of Runnables + * by one thread. This variable is only used by one thread at a time and then + * set to null. + */ + private List cachedRunnables = null; + + /** + * Submits the given Runnable to the Room Executor and waits until it + * has been executed. Currently, this simply means that the Runnable + * will be run directly inside of a synchronized() block.
    + * Note that if a runnable recursively calls invokeAndWait() with another + * runnable on this Room, it will not be executed recursively, but instead + * cached until the original runnable is finished, to keep the behavior of + * using an Executor. + * + * @param task The task to be executed + */ + public void invokeAndWait(Runnable task) { + + // Check if the current thread already holds a lock on this room. + // If yes, then we must not directly execute the Runnable but instead + // cache it until the original invokeAndWait() has finished. + if (roomLock.isHeldByCurrentThread()) { + + if (cachedRunnables == null) { + cachedRunnables = new ArrayList<>(); + } + cachedRunnables.add(task); + + } else { + + roomLock.lock(); + try { + // Explicitly overwrite value to ensure data consistency in + // current thread + cachedRunnables = null; + + if (!closed) { + task.run(); + } + + // Run the cached runnables. + if (cachedRunnables != null) { + for (Runnable cachedRunnable : cachedRunnables) { + if (!closed) { + cachedRunnable.run(); + } + } + cachedRunnables = null; + } + + } finally { + roomLock.unlock(); + } + + } + + } + + /** + * Shuts down the roomExecutor and the drawmessageBroadcastTimer. + */ + public void shutdown() { + invokeAndWait(new Runnable() { + @Override + public void run() { + closed = true; + drawmessageBroadcastTimer.cancel(); + roomGraphics.dispose(); + } + }); + } + + + /** + * A Player participates in a Room. It is the interface between the + * {@link Room} and the {@link Client}.

    + * + * Note: This means a player object is actually a join between Room and + * Client. + */ + public static final class Player { + + /** + * The room to which this player belongs. + */ + private Room room; + + /** + * The room buffers the last draw message ID that was received from + * this player. + */ + private long lastReceivedMessageId = 0; + + private final Client client; + + /** + * Buffered DrawMessages that will be sent by a Timer. + */ + private final List bufferedDrawMessages = + new ArrayList<>(); + + private List getBufferedDrawMessages() { + return bufferedDrawMessages; + } + + private Player(Room room, Client client) { + this.room = room; + this.client = client; + } + + public Room getRoom() { + return room; + } + + public Client getClient() { + return client; + } + + /** + * Removes this player from its room, e.g. when + * the client disconnects. + */ + public void removeFromRoom() { + if (room != null) { + room.internalRemovePlayer(this); + room = null; + } + } + + + private long getLastReceivedMessageId() { + return lastReceivedMessageId; + } + private void setLastReceivedMessageId(long value) { + lastReceivedMessageId = value; + } + + + /** + * Handles the given DrawMessage by drawing it onto this Room's + * image and by broadcasting it to the connected players. + * + * @param msg The draw message received + * @param msgId The ID for the draw message received + */ + public void handleDrawMessage(DrawMessage msg, long msgId) { + room.internalHandleDrawMessage(this, msg, msgId); + } + + + /** + * Sends the given room message. + * @param type message type + * @param content message content + */ + private void sendRoomMessage(MessageType type, String content) { + Objects.requireNonNull(content); + Objects.requireNonNull(type); + + String completeMsg = String.valueOf(type.flag) + content; + + client.sendMessage(new StringWebsocketMessage(completeMsg)); + } + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java new file mode 100644 index 0000000..d425393 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/AbstractWebsocketMessage.java @@ -0,0 +1,25 @@ +/* + * 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 websocket.drawboard.wsmessages; + +/** + * Abstract base class for Websocket Messages (binary or string) + * that can be buffered. + */ +public abstract class AbstractWebsocketMessage { + +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java new file mode 100644 index 0000000..b16e1ae --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/BinaryWebsocketMessage.java @@ -0,0 +1,34 @@ +/* + * 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 websocket.drawboard.wsmessages; + +import java.nio.ByteBuffer; + +/** + * Represents a binary websocket message. + */ +public final class BinaryWebsocketMessage extends AbstractWebsocketMessage { + private final ByteBuffer bytes; + + public BinaryWebsocketMessage(ByteBuffer bytes) { + this.bytes = bytes; + } + + public ByteBuffer getBytes() { + return bytes; + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/CloseWebsocketMessage.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/CloseWebsocketMessage.java new file mode 100644 index 0000000..44f48ad --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/CloseWebsocketMessage.java @@ -0,0 +1,24 @@ +/* + * 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 websocket.drawboard.wsmessages; + +/** + * Represents a "close" message that closes the session. + */ +public class CloseWebsocketMessage extends AbstractWebsocketMessage { + +} diff --git a/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java b/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java new file mode 100644 index 0000000..49be369 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/drawboard/wsmessages/StringWebsocketMessage.java @@ -0,0 +1,34 @@ +/* + * 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 websocket.drawboard.wsmessages; + +/** + * Represents a string websocket message. + * + */ +public final class StringWebsocketMessage extends AbstractWebsocketMessage { + private final String string; + + public StringWebsocketMessage(String string) { + this.string = string; + } + + public String getString() { + return string; + } + +} diff --git a/webapps/examples/WEB-INF/classes/websocket/echo/EchoAnnotation.java b/webapps/examples/WEB-INF/classes/websocket/echo/EchoAnnotation.java new file mode 100644 index 0000000..7cb3aa6 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/echo/EchoAnnotation.java @@ -0,0 +1,75 @@ +/* + * 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 websocket.echo; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jakarta.websocket.OnMessage; +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +/** + * The three annotated echo endpoints can be used to test with Autobahn and + * the following command "wstest -m fuzzingclient -s servers.json". See the + * Autobahn documentation for setup and general information. + */ +@ServerEndpoint("/websocket/echoAnnotation") +public class EchoAnnotation { + + @OnMessage + public void echoTextMessage(Session session, String msg, boolean last) { + try { + if (session.isOpen()) { + session.getBasicRemote().sendText(msg, last); + } + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + + @OnMessage + public void echoBinaryMessage(Session session, ByteBuffer bb, + boolean last) { + try { + if (session.isOpen()) { + session.getBasicRemote().sendBinary(bb, last); + } + } catch (IOException e) { + try { + session.close(); + } catch (IOException e1) { + // Ignore + } + } + } + + /** + * Process a received pong. This is a NO-OP. + * + * @param pm Ignored. + */ + @OnMessage + public void echoPongMessage(PongMessage pm) { + // NO-OP + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/echo/EchoAsyncAnnotation.java b/webapps/examples/WEB-INF/classes/websocket/echo/EchoAsyncAnnotation.java new file mode 100644 index 0000000..1d92fd4 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/echo/EchoAsyncAnnotation.java @@ -0,0 +1,128 @@ +/* + * 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 websocket.echo; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import jakarta.websocket.OnMessage; +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; + +/** + * The three annotated echo endpoints can be used to test with Autobahn and + * the following command "wstest -m fuzzingclient -s servers.json". See the + * Autobahn documentation for setup and general information. + * + * Note: This one is disabled by default since it allocates memory, and needs + * to be enabled back. + */ +//@jakarta.websocket.server.ServerEndpoint("/websocket/echoAsyncAnnotation") +public class EchoAsyncAnnotation { + + private static final Future COMPLETED = new CompletedFuture(); + + Future f = COMPLETED; + StringBuilder sb = null; + ByteArrayOutputStream bytes = null; + + @OnMessage + public void echoTextMessage(Session session, String msg, boolean last) { + if (sb == null) { + sb = new StringBuilder(); + } + sb.append(msg); + if (last) { + // Before we send the next message, have to wait for the previous + // message to complete + try { + f.get(); + } catch (InterruptedException | ExecutionException e) { + // Let the container deal with it + throw new RuntimeException(e); + } + f = session.getAsyncRemote().sendText(sb.toString()); + sb = null; + } + } + + @OnMessage + public void echoBinaryMessage(byte[] msg, Session session, boolean last) + throws IOException { + if (bytes == null) { + bytes = new ByteArrayOutputStream(); + } + bytes.write(msg); + if (last) { + // Before we send the next message, have to wait for the previous + // message to complete + try { + f.get(); + } catch (InterruptedException | ExecutionException e) { + // Let the container deal with it + throw new RuntimeException(e); + } + f = session.getAsyncRemote().sendBinary(ByteBuffer.wrap(bytes.toByteArray())); + bytes = null; + } + } + + /** + * Process a received pong. This is a NO-OP. + * + * @param pm Ignored. + */ + @OnMessage + public void echoPongMessage(PongMessage pm) { + // NO-OP + } + + private static class CompletedFuture implements Future { + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + return null; + } + + @Override + public Void get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, + TimeoutException { + return null; + } + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java b/webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java new file mode 100644 index 0000000..8652861 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java @@ -0,0 +1,80 @@ +/* + * 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 websocket.echo; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.RemoteEndpoint; +import jakarta.websocket.Session; + +public class EchoEndpoint extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig endpointConfig) { + RemoteEndpoint.Basic remoteEndpointBasic = session.getBasicRemote(); + session.addMessageHandler(new EchoMessageHandlerText(remoteEndpointBasic)); + session.addMessageHandler(new EchoMessageHandlerBinary(remoteEndpointBasic)); + } + + private static class EchoMessageHandlerText + implements MessageHandler.Partial { + + private final RemoteEndpoint.Basic remoteEndpointBasic; + + private EchoMessageHandlerText(RemoteEndpoint.Basic remoteEndpointBasic) { + this.remoteEndpointBasic = remoteEndpointBasic; + } + + @Override + public void onMessage(String message, boolean last) { + try { + if (remoteEndpointBasic != null) { + remoteEndpointBasic.sendText(message, last); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + private static class EchoMessageHandlerBinary + implements MessageHandler.Partial { + + private final RemoteEndpoint.Basic remoteEndpointBasic; + + private EchoMessageHandlerBinary(RemoteEndpoint.Basic remoteEndpointBasic) { + this.remoteEndpointBasic = remoteEndpointBasic; + } + + @Override + public void onMessage(ByteBuffer message, boolean last) { + try { + if (remoteEndpointBasic != null) { + remoteEndpointBasic.sendBinary(message, last); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/echo/EchoStreamAnnotation.java b/webapps/examples/WEB-INF/classes/websocket/echo/EchoStreamAnnotation.java new file mode 100644 index 0000000..95f4c1a --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/echo/EchoStreamAnnotation.java @@ -0,0 +1,75 @@ +/* + * 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 websocket.echo; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; + +import jakarta.websocket.OnMessage; +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +/** + * The three annotated echo endpoints can be used to test with Autobahn and + * the following command "wstest -m fuzzingclient -s servers.json". See the + * Autobahn documentation for setup and general information. + */ +@ServerEndpoint("/websocket/echoStreamAnnotation") +public class EchoStreamAnnotation { + + Writer writer; + OutputStream stream; + + @OnMessage + public void echoTextMessage(Session session, String msg, boolean last) + throws IOException { + if (writer == null) { + writer = session.getBasicRemote().getSendWriter(); + } + writer.write(msg); + if (last) { + writer.close(); + writer = null; + } + } + + @OnMessage + public void echoBinaryMessage(byte[] msg, Session session, boolean last) + throws IOException { + if (stream == null) { + stream = session.getBasicRemote().getSendStream(); + } + stream.write(msg); + stream.flush(); + if (last) { + stream.close(); + stream = null; + } + } + + /** + * Process a received pong. This is a NO-OP. + * + * @param pm Ignored. + */ + @OnMessage + public void echoPongMessage(PongMessage pm) { + // NO-OP + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/echo/servers.json b/webapps/examples/WEB-INF/classes/websocket/echo/servers.json new file mode 100644 index 0000000..c816a7d --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/echo/servers.json @@ -0,0 +1,20 @@ +{ + "options": {"failByDrop": false}, + "outdir": "./reports/servers", + + "servers": [ + {"agent": "Basic", + "url": "ws://localhost:8080/examples/websocket/echoAnnotation", + "options": {"version": 18}}, + {"agent": "Stream", + "url": "ws://localhost:8080/examples/websocket/echoStreamAnnotation", + "options": {"version": 18}}, + {"agent": "Async", + "url": "ws://localhost:8080/examples/websocket/echoAsyncAnnotation", + "options": {"version": 18}} + ], + + "cases": ["*"], + "exclude-cases": [], + "exclude-agent-cases": {} +} diff --git a/webapps/examples/WEB-INF/classes/websocket/snake/Direction.java b/webapps/examples/WEB-INF/classes/websocket/snake/Direction.java new file mode 100644 index 0000000..4440c9d --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/snake/Direction.java @@ -0,0 +1,21 @@ +/* + * 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 websocket.snake; + +public enum Direction { + NONE, NORTH, SOUTH, EAST, WEST +} diff --git a/webapps/examples/WEB-INF/classes/websocket/snake/Location.java b/webapps/examples/WEB-INF/classes/websocket/snake/Location.java new file mode 100644 index 0000000..420be8a --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/snake/Location.java @@ -0,0 +1,73 @@ +/* + * 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 websocket.snake; + +public class Location { + + public int x; + public int y; + + public Location(int x, int y) { + this.x = x; + this.y = y; + } + + public Location getAdjacentLocation(Direction direction) { + switch (direction) { + case NORTH: + return new Location(x, y - SnakeAnnotation.GRID_SIZE); + case SOUTH: + return new Location(x, y + SnakeAnnotation.GRID_SIZE); + case EAST: + return new Location(x + SnakeAnnotation.GRID_SIZE, y); + case WEST: + return new Location(x - SnakeAnnotation.GRID_SIZE, y); + case NONE: + // fall through + default: + return this; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Location location = (Location) o; + + if (x != location.x) { + return false; + } + if (y != location.y) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = x; + result = 31 * result + y; + return result; + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/snake/Snake.java b/webapps/examples/WEB-INF/classes/websocket/snake/Snake.java new file mode 100644 index 0000000..619a879 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/snake/Snake.java @@ -0,0 +1,150 @@ +/* + * 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 websocket.snake; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; + +import jakarta.websocket.CloseReason; +import jakarta.websocket.CloseReason.CloseCodes; +import jakarta.websocket.Session; + +public class Snake { + + private static final int DEFAULT_LENGTH = 5; + + private final int id; + private final Session session; + + private Direction direction; + private int length = DEFAULT_LENGTH; + private Location head; + private final Deque tail = new ArrayDeque<>(); + private final String hexColor; + + public Snake(int id, Session session) { + this.id = id; + this.session = session; + this.hexColor = SnakeAnnotation.getRandomHexColor(); + resetState(); + } + + private void resetState() { + this.direction = Direction.NONE; + this.head = SnakeAnnotation.getRandomLocation(); + this.tail.clear(); + this.length = DEFAULT_LENGTH; + } + + private synchronized void kill() { + resetState(); + sendMessage("{\"type\": \"dead\"}"); + } + + private synchronized void reward() { + length++; + sendMessage("{\"type\": \"kill\"}"); + } + + + protected void sendMessage(String msg) { + try { + session.getBasicRemote().sendText(msg); + } catch (IOException ioe) { + CloseReason cr = + new CloseReason(CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage()); + try { + session.close(cr); + } catch (IOException ioe2) { + // Ignore + } + } + } + + public synchronized void update(Collection snakes) { + Location nextLocation = head.getAdjacentLocation(direction); + if (nextLocation.x >= SnakeAnnotation.PLAYFIELD_WIDTH) { + nextLocation.x = 0; + } + if (nextLocation.y >= SnakeAnnotation.PLAYFIELD_HEIGHT) { + nextLocation.y = 0; + } + if (nextLocation.x < 0) { + nextLocation.x = SnakeAnnotation.PLAYFIELD_WIDTH; + } + if (nextLocation.y < 0) { + nextLocation.y = SnakeAnnotation.PLAYFIELD_HEIGHT; + } + if (direction != Direction.NONE) { + tail.addFirst(head); + if (tail.size() > length) { + tail.removeLast(); + } + head = nextLocation; + } + + handleCollisions(snakes); + } + + private void handleCollisions(Collection snakes) { + for (Snake snake : snakes) { + boolean headCollision = id != snake.id && snake.getHead().equals(head); + boolean tailCollision = snake.getTail().contains(head); + if (headCollision || tailCollision) { + kill(); + if (id != snake.id) { + snake.reward(); + } + } + } + } + + public synchronized Location getHead() { + return head; + } + + public synchronized Collection getTail() { + return tail; + } + + public synchronized void setDirection(Direction direction) { + this.direction = direction; + } + + public synchronized String getLocationsJson() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("{\"x\": %d, \"y\": %d}", + Integer.valueOf(head.x), Integer.valueOf(head.y))); + for (Location location : tail) { + sb.append(','); + sb.append(String.format("{\"x\": %d, \"y\": %d}", + Integer.valueOf(location.x), Integer.valueOf(location.y))); + } + return String.format("{\"id\":%d,\"body\":[%s]}", + Integer.valueOf(id), sb.toString()); + } + + public int getId() { + return id; + } + + public String getHexColor() { + return hexColor; + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/snake/SnakeAnnotation.java b/webapps/examples/WEB-INF/classes/websocket/snake/SnakeAnnotation.java new file mode 100644 index 0000000..7f14ab8 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/snake/SnakeAnnotation.java @@ -0,0 +1,142 @@ +/* + * 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 websocket.snake; + +import java.awt.Color; +import java.io.IOException; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +@ServerEndpoint(value = "/websocket/snake") +public class SnakeAnnotation { + + public static final int PLAYFIELD_WIDTH = 640; + public static final int PLAYFIELD_HEIGHT = 480; + public static final int GRID_SIZE = 10; + + private static final AtomicInteger snakeIds = new AtomicInteger(0); + private static final Random random = new Random(); + + + private final int id; + private Snake snake; + + public static String getRandomHexColor() { + float hue = random.nextFloat(); + // sat between 0.1 and 0.3 + float saturation = (random.nextInt(2000) + 1000) / 10000f; + float luminance = 0.9f; + Color color = Color.getHSBColor(hue, saturation, luminance); + return '#' + Integer.toHexString( + (color.getRGB() & 0xffffff) | 0x1000000).substring(1); + } + + + public static Location getRandomLocation() { + int x = roundByGridSize(random.nextInt(PLAYFIELD_WIDTH)); + int y = roundByGridSize(random.nextInt(PLAYFIELD_HEIGHT)); + return new Location(x, y); + } + + + private static int roundByGridSize(int value) { + value = value + (GRID_SIZE / 2); + value = value / GRID_SIZE; + value = value * GRID_SIZE; + return value; + } + + public SnakeAnnotation() { + this.id = snakeIds.getAndIncrement(); + } + + + @OnOpen + public void onOpen(Session session) { + this.snake = new Snake(id, session); + SnakeTimer.addSnake(snake); + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = SnakeTimer.getSnakes().iterator(); + iterator.hasNext();) { + Snake snake = iterator.next(); + sb.append(String.format("{\"id\": %d, \"color\": \"%s\"}", + Integer.valueOf(snake.getId()), snake.getHexColor())); + if (iterator.hasNext()) { + sb.append(','); + } + } + SnakeTimer.broadcast(String.format("{\"type\": \"join\",\"data\":[%s]}", + sb.toString())); + } + + + @OnMessage + public void onTextMessage(String message) { + if ("west".equals(message)) { + snake.setDirection(Direction.WEST); + } else if ("north".equals(message)) { + snake.setDirection(Direction.NORTH); + } else if ("east".equals(message)) { + snake.setDirection(Direction.EAST); + } else if ("south".equals(message)) { + snake.setDirection(Direction.SOUTH); + } + } + + + @OnClose + public void onClose() { + SnakeTimer.removeSnake(snake); + SnakeTimer.broadcast(String.format("{\"type\": \"leave\", \"id\": %d}", + Integer.valueOf(id))); + } + + + @OnError + public void onError(Throwable t, Session session) throws Throwable { + /* + * Assume all errors are fatal. Close the session and remove the snake from the game. + */ + session.close(); + onClose(); + /* + * Correct action depends on root cause. Protect against infinite loops while looking for root cause. + */ + int count = 0; + Throwable root = t; + while (root.getCause() != null && count < 20) { + root = root.getCause(); + count ++; + } + if (root instanceof IOException) { + /* + * User going away can present in different ways depending on their platform and exactly what went wrong. + * Assume that any IO issue is some form of the user going away and ignore it. + */ + } else { + throw t; + } + } +} diff --git a/webapps/examples/WEB-INF/classes/websocket/snake/SnakeTimer.java b/webapps/examples/WEB-INF/classes/websocket/snake/SnakeTimer.java new file mode 100644 index 0000000..6b777e9 --- /dev/null +++ b/webapps/examples/WEB-INF/classes/websocket/snake/SnakeTimer.java @@ -0,0 +1,115 @@ +/* + * 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 websocket.snake; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Sets up the timer for the multi-player snake game WebSocket example. + */ +public class SnakeTimer { + + private static final Log log = + LogFactory.getLog(SnakeTimer.class); + + private static Timer gameTimer = null; + + private static final long TICK_DELAY = 100; + + private static final ConcurrentHashMap snakes = + new ConcurrentHashMap<>(); + + protected static synchronized void addSnake(Snake snake) { + if (snakes.size() == 0) { + startTimer(); + } + snakes.put(Integer.valueOf(snake.getId()), snake); + } + + + protected static Collection getSnakes() { + return Collections.unmodifiableCollection(snakes.values()); + } + + + protected static synchronized void removeSnake(Snake snake) { + snakes.remove(Integer.valueOf(snake.getId())); + if (snakes.size() == 0) { + stopTimer(); + } + } + + + protected static void tick() { + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = getSnakes().iterator(); + iterator.hasNext();) { + Snake snake = iterator.next(); + snake.update(getSnakes()); + sb.append(snake.getLocationsJson()); + if (iterator.hasNext()) { + sb.append(','); + } + } + broadcast(String.format("{\"type\": \"update\", \"data\" : [%s]}", + sb.toString())); + } + + protected static void broadcast(String message) { + for (Snake snake : getSnakes()) { + try { + snake.sendMessage(message); + } catch (IllegalStateException ise) { + // An ISE can occur if an attempt is made to write to a + // WebSocket connection after it has been closed. The + // alternative to catching this exception is to synchronise + // the writes to the clients along with the addSnake() and + // removeSnake() methods that are already synchronised. + } + } + } + + + public static void startTimer() { + gameTimer = new Timer(SnakeTimer.class.getSimpleName() + " Timer"); + gameTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + tick(); + } catch (RuntimeException e) { + log.error("Caught to prevent timer from shutting down", e); + } + } + }, TICK_DELAY, TICK_DELAY); + } + + + public static void stopTimer() { + if (gameTimer != null) { + gameTimer.cancel(); + } + } +} diff --git a/webapps/examples/WEB-INF/jsp/403.jsp b/webapps/examples/WEB-INF/jsp/403.jsp new file mode 100644 index 0000000..406f754 --- /dev/null +++ b/webapps/examples/WEB-INF/jsp/403.jsp @@ -0,0 +1,44 @@ +<%-- + 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. +--%> +<%@ page session="false" trimDirectiveWhitespaces="true" %> + + + + 403 Access Denied + + + +

    403 Access Denied

    +

    + You are not authorized to view this page. +

    +

    + By default the examples web application is only accessible from a browser + running on the same machine as Tomcat. If you wish to modify this + restriction, you'll need to edit the example web application's + context.xml file. +

    + + + diff --git a/webapps/examples/WEB-INF/jsp/debug-taglib.tld b/webapps/examples/WEB-INF/jsp/debug-taglib.tld new file mode 100644 index 0000000..424a3df --- /dev/null +++ b/webapps/examples/WEB-INF/jsp/debug-taglib.tld @@ -0,0 +1,54 @@ + + + + + + + + 1.0 + 1.2 + debug + http://tomcat.apache.org/debug-taglib + + This tag library defines no tags. Instead, its purpose is encapsulated + in the TagLibraryValidator implementation that simply outputs the XML + version of a JSP page to standard output, whenever this tag library is + referenced in a "taglib" directive in a JSP page. + + + validators.DebugValidator + + + + + log + examples.LogTag + TAGDEPENDENT + + Perform a server side action; Log the message. + + + toBrowser + false + + + + + diff --git a/webapps/examples/WEB-INF/jsp/example-taglib.tld b/webapps/examples/WEB-INF/jsp/example-taglib.tld new file mode 100644 index 0000000..f60e353 --- /dev/null +++ b/webapps/examples/WEB-INF/jsp/example-taglib.tld @@ -0,0 +1,107 @@ + + + + + + + 1.0 + 1.2 + simple + http://tomcat.apache.org/example-taglib + + A simple tab library for the examples + + + + + + foo + examples.FooTag + examples.FooTagExtraInfo + JSP + + Perform a server side action; uses 3 mandatory attributes + + + + att1 + true + + + att2 + true + + + att3 + true + + + + + + + log + examples.LogTag + TAGDEPENDENT + + Perform a server side action; Log the message. + + + toBrowser + false + + + + + + + values + examples.ValuesTag + empty + + Accept and return values of different types. This tag is used + to illustrate type coercions. + + + object + false + true + java.lang.Object + + + string + false + true + java.lang.String + + + long + false + true + long + + + double + false + true + double + + + diff --git a/webapps/examples/WEB-INF/jsp/jsp2-example-taglib.tld b/webapps/examples/WEB-INF/jsp/jsp2-example-taglib.tld new file mode 100644 index 0000000..73173bd --- /dev/null +++ b/webapps/examples/WEB-INF/jsp/jsp2-example-taglib.tld @@ -0,0 +1,124 @@ + + + + + A tag library exercising SimpleTag handlers. + 1.0 + SimpleTagLibrary + http://tomcat.apache.org/jsp2-example-taglib + + Outputs Hello, World + helloWorld + jsp2.examples.simpletag.HelloWorldSimpleTag + empty + + + Repeats the body of the tag 'num' times + repeat + jsp2.examples.simpletag.RepeatSimpleTag + scriptless + + Current invocation count (1 to num) + count + + + num + true + true + + + + Populates the page context with a BookBean + findBook + jsp2.examples.simpletag.FindBookSimpleTag + empty + + var + true + true + + + + + Takes 3 fragments and invokes them in a random order + + shuffle + jsp2.examples.simpletag.ShuffleSimpleTag + empty + + fragment1 + true + true + + + fragment2 + true + true + + + fragment3 + true + true + + + + Outputs a colored tile + tile + jsp2.examples.simpletag.TileSimpleTag + empty + + color + true + + + label + true + + + + + Tag that echoes all its attributes and body content + + echoAttributes + jsp2.examples.simpletag.EchoAttributesTag + empty + true + + + Reverses the characters in the given String + reverse + jsp2.examples.el.Functions + java.lang.String reverse( java.lang.String ) + + + Counts the number of vowels (a,e,i,o,u) in the given String + countVowels + jsp2.examples.el.Functions + java.lang.String numVowels( java.lang.String ) + + + Converts the string to all caps + caps + jsp2.examples.el.Functions + java.lang.String caps( java.lang.String ) + + + diff --git a/webapps/examples/WEB-INF/tags/displayProducts.tag b/webapps/examples/WEB-INF/tags/displayProducts.tag new file mode 100644 index 0000000..41e8c35 --- /dev/null +++ b/webapps/examples/WEB-INF/tags/displayProducts.tag @@ -0,0 +1,55 @@ + +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ attribute name="normalPrice" fragment="true" %> +<%@ attribute name="onSale" fragment="true" %> +<%@ variable name-given="name" %> +<%@ variable name-given="price" %> +<%@ variable name-given="origPrice" %> +<%@ variable name-given="salePrice" %> + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/webapps/examples/WEB-INF/tags/helloWorld.tag b/webapps/examples/WEB-INF/tags/helloWorld.tag new file mode 100644 index 0000000..192bf53 --- /dev/null +++ b/webapps/examples/WEB-INF/tags/helloWorld.tag @@ -0,0 +1,17 @@ + +Hello, world! diff --git a/webapps/examples/WEB-INF/tags/panel.tag b/webapps/examples/WEB-INF/tags/panel.tag new file mode 100644 index 0000000..f4f30d0 --- /dev/null +++ b/webapps/examples/WEB-INF/tags/panel.tag @@ -0,0 +1,29 @@ + +<%@ attribute name="color" %> +<%@ attribute name="bgcolor" %> +<%@ attribute name="title" %> + + + + + + + +
    ${title}
    + +
    diff --git a/webapps/examples/WEB-INF/web.xml b/webapps/examples/WEB-INF/web.xml new file mode 100644 index 0000000..12d865e --- /dev/null +++ b/webapps/examples/WEB-INF/web.xml @@ -0,0 +1,423 @@ + + + + + + Servlet and JSP Examples. + + Servlet and JSP Examples + + UTF-8 + + + + Timing Filter + filters.ExampleFilter + + attribute + filters.ExampleFilter + + + + + Request Dumper Filter + org.apache.catalina.filters.RequestDumperFilter + + + + Compression Filter + compressionFilters.CompressionFilter + + compressionThreshold + 128 + + + compressionBuffer + 8192 + + + compressionMimeTypes + text/html,text/plain,text/xml + + + debug + 0 + + + + + + + + HTTP header security filter + org.apache.catalina.filters.HttpHeaderSecurityFilter + true + + hstsEnabled + false + + + + + + + + + + + + + HTTP header security filter + /* + + + + + listeners.ContextListener + + + listeners.SessionListener + + + + + async.AsyncStockContextListener + + + + + + ServletToJsp + ServletToJsp + + + CompressionFilterTestServlet + compressionFilters.CompressionFilterTestServlet + + + HelloWorldExample + HelloWorldExample + + + RequestInfoExample + RequestInfoExample + + + RequestHeaderExample + RequestHeaderExample + + + RequestParamExample + RequestParamExample + + + CookieExample + CookieExample + + + SessionExample + SessionExample + + + + CompressionFilterTestServlet + /CompressionTest + + + HelloWorldExample + /servlets/servlet/HelloWorldExample + + + RequestInfoExample + /servlets/servlet/RequestInfoExample/* + + + RequestHeaderExample + /servlets/servlet/RequestHeaderExample + + + RequestParamExample + /servlets/servlet/RequestParamExample + + + CookieExample + /servlets/servlet/CookieExample + + + SessionExample + /servlets/servlet/SessionExample + + + ServletToJsp + /servletToJsp + + + + + + http://tomcat.apache.org/debug-taglib + + + /WEB-INF/jsp/debug-taglib.tld + + + + + + http://tomcat.apache.org/example-taglib + + + /WEB-INF/jsp/example-taglib.tld + + + + + + http://tomcat.apache.org/jsp2-example-taglib + + + /WEB-INF/jsp/jsp2-example-taglib.tld + + + + + + Special property group for JSP Configuration JSP example. + + JSPConfiguration + /jsp/jsp2/misc/config.jsp + true + ISO-8859-1 + true + /jsp/jsp2/misc/prelude.jspf + /jsp/jsp2/misc/coda.jspf + + + + + Example Security Constraint - part 1 + + Protected Area - Allow methods + + /jsp/security/protected/* + + + DELETE + GET + POST + PUT + + + + tomcat + role1 + + + + Example Security Constraint - part 2 + + Protected Area - Deny methods + + /jsp/security/protected/* + DELETE + GET + POST + PUT + + + + + + + + FORM + Example Form-Based Authentication Area + + /jsp/security/protected/login.jsp + /jsp/security/protected/error.jsp + + + + + + role1 + + + tomcat + + + + + + minExemptions + java.lang.Integer + 1 + + + foo/name1 + java.lang.String + value1 + + + foo/bar/name2 + java.lang.Boolean + true + + + name3 + java.lang.Integer + 1 + + + foo/name4 + java.lang.Integer + 10 + + + + + async0 + async.Async0 + true + + + async0 + /async/async0 + + + async1 + async.Async1 + true + + + async1 + /async/async1 + + + async2 + async.Async2 + true + + + async2 + /async/async2 + + + async3 + async.Async3 + true + + + async3 + /async/async3 + + + stock + async.AsyncStockServlet + true + + + stock + /async/stockticker + + + + + bytecounter + nonblocking.ByteCounter + true + + + bytecounter + /servlets/nonblocking/bytecounter + + + numberwriter + nonblocking.NumberWriter + true + + + numberwriter + /servlets/nonblocking/numberwriter + + + + + simpleimagepush + http2.SimpleImagePush + + + simpleimagepush + /servlets/serverpush/simpleimage + + + + + responsetrailer + trailers.ResponseTrailers + + + responsetrailer + /servlets/trailers/response + + + + index.html + index.xhtml + index.htm + index.jsp + + + + + websocket.drawboard.DrawboardContextListener + + + + 403 + /WEB-INF/jsp/403.jsp + + + diff --git a/webapps/examples/index.html b/webapps/examples/index.html new file mode 100644 index 0000000..0799e10 --- /dev/null +++ b/webapps/examples/index.html @@ -0,0 +1,30 @@ + + + +Apache Tomcat Examples + + +

    +

    Apache Tomcat Examples

    +

    + + diff --git a/webapps/examples/jsp/async/async1.jsp b/webapps/examples/jsp/async/async1.jsp new file mode 100644 index 0000000..af88869 --- /dev/null +++ b/webapps/examples/jsp/async/async1.jsp @@ -0,0 +1,28 @@ +<%-- + 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. +--%> +<%@page session="false" import="java.util.Date,java.text.SimpleDateFormat"%> +Output from async1.jsp +Type is <%=request.getDispatcherType()%> +<% + System.out.println("Inside Async 1"); + if (request.isAsyncStarted()) { + request.getAsyncContext().complete(); + } + Date date = new Date(System.currentTimeMillis()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); +%> +Completed async request at <%=sdf.format(date)%> \ No newline at end of file diff --git a/webapps/examples/jsp/async/async3.jsp b/webapps/examples/jsp/async/async3.jsp new file mode 100644 index 0000000..9d24e60 --- /dev/null +++ b/webapps/examples/jsp/async/async3.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> +<%@page session="false" import="java.util.Date,java.text.SimpleDateFormat"%> +Output from async3.jsp +Type is <%=request.getDispatcherType()%> +<% + Date date = new Date(System.currentTimeMillis()); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); +%> + +Completed async 3 request at <%=sdf.format(date)%> \ No newline at end of file diff --git a/webapps/examples/jsp/async/index.jsp b/webapps/examples/jsp/async/index.jsp new file mode 100644 index 0000000..be2d713 --- /dev/null +++ b/webapps/examples/jsp/async/index.jsp @@ -0,0 +1,69 @@ +<%-- + 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. +--%> +<%@page session="false"%> + +
    +Use cases:
    +
    +1. Simple dispatch
    + - servlet does startAsync()
    + - background thread calls ctx.dispatch()
    +   "> Async 0 
    +
    +2. Simple dispatch
    + - servlet does startAsync()
    + - background thread calls dispatch(/path/to/jsp)
    +   "> Async 1 
    +
    +3. Simple dispatch
    + - servlet does startAsync()
    + - background thread calls writes and calls complete()
    +   "> Async 2 
    +
    +4. Simple dispatch
    + - servlet does a startAsync()
    + - servlet calls dispatch(/path/to/jsp)
    + - servlet calls complete()
    +   "> Async 3 
    +
    +3. Timeout s1
    + - servlet does a startAsync()
    + - servlet does a setAsyncTimeout
    + - returns - waits for timeout to happen should return error page
    +
    +4. Timeout s2
    + - servlet does a startAsync()
    + - servlet does a setAsyncTimeout
    + - servlet does a addAsyncListener
    + - returns - waits for timeout to happen and listener invoked
    +
    +5. Dispatch to asyncSupported=false servlet
    + - servlet1 does a startAsync()
    + - servlet1 dispatches to dispatch(/servlet2)
    + - the container calls complete() after servlet2 is complete
    + - TODO
    +
    +6. Chained dispatch
    + - servlet1 does a startAsync
    + - servlet1 does a dispatch to servlet2 (asyncsupported=true)
    + - servlet2 does a dispatch to servlet3 (asyncsupported=true)
    + - servlet3 does a dispatch to servlet4 (asyncsupported=false)
    +
    +
    +7. Stock ticker
    +   "> StockTicker 
    +
    \ No newline at end of file diff --git a/webapps/examples/jsp/cal/cal1.jsp b/webapps/examples/jsp/cal/cal1.jsp new file mode 100644 index 0000000..ce29c13 --- /dev/null +++ b/webapps/examples/jsp/cal/cal1.jsp @@ -0,0 +1,94 @@ +<%-- + 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. +--%> +<%@page contentType="text/html; charset=UTF-8" %> + + + Calendar: A JSP APPLICATION + + + + + +<%@ page language="java" import="cal.*" %> + + +<% + table.processRequest(request); + if (table.getProcessError() == false) { +%> + + +
    + + + + +
    prev + Calendar:<%= table.getDate() %> next +
    + + + + + + + + +<% + for(int i=0; i + + + + +<% + } +%> + +
    Time Appointment
    + > + <%= entr.getHour() %> + > + <% out.print(util.HTMLFilter.filter(entr.getDescription())); %> +
    +
    + + + + + + +
    <% out.print(util.HTMLFilter.filter(table.getName())); %> : + <% out.print(util.HTMLFilter.filter(table.getEmail())); %>
    +
    + +<% + } else { +%> + + You must enter your name and email address correctly. + +<% + } +%> + + + + + + diff --git a/webapps/examples/jsp/cal/cal2.jsp b/webapps/examples/jsp/cal/cal2.jsp new file mode 100644 index 0000000..e7e14d8 --- /dev/null +++ b/webapps/examples/jsp/cal/cal2.jsp @@ -0,0 +1,45 @@ +<%-- + 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. +--%> +<%@page contentType="text/html; charset=UTF-8" %> + + + Calendar: A JSP APPLICATION + + + + + + +<% + String time = request.getParameter ("time"); +%> + + Please add the following event: +

    Date <%= table.getDate() %> +
    Time <%= util.HTMLFilter.filter(time) %>

    +
    +
    +
    +
    +
    +

    Description of the event

    +
    +
    + + + + diff --git a/webapps/examples/jsp/cal/calendar.html b/webapps/examples/jsp/cal/calendar.html new file mode 100644 index 0000000..a0a3ea1 --- /dev/null +++ b/webapps/examples/jsp/cal/calendar.html @@ -0,0 +1,43 @@ + + + + +Untitled Document + + + + +

    + +

    Source Code for Calendar Example.
    +

    cal1.jsp +

    +

    cal2.jsp +

    + +
    +

    Beans. +

    TableBean +

    +

    Entries +

    +

    Entry +

    + + + diff --git a/webapps/examples/jsp/cal/login.html b/webapps/examples/jsp/cal/login.html new file mode 100644 index 0000000..8a62eca --- /dev/null +++ b/webapps/examples/jsp/cal/login.html @@ -0,0 +1,47 @@ + + + + + Login page for the calendar. + + + +
    + + Please Enter the following information: + +
    +
    + + Name + +
    + Email + +
    + + +
    +
    + Note: This application does not implement the complete +functionality of a typical calendar application. It demonstrates a way JSP can +be used with HTML tables and forms. + +
    + + diff --git a/webapps/examples/jsp/checkbox/CheckTest.html b/webapps/examples/jsp/checkbox/CheckTest.html new file mode 100644 index 0000000..284d9ec --- /dev/null +++ b/webapps/examples/jsp/checkbox/CheckTest.html @@ -0,0 +1,56 @@ + + + + + +checkbox.CheckTest Bean Properties + + +

    +checkbox.CheckTest Bean Properties +

    +
    +
    +
    public class CheckTest
    extends Object
    + +

    +


    + +

    + + + + + + + + + +
    +Properties Summary
    + +String +CheckTest:fruit +
    +
    + +Multi +
    +


    + + diff --git a/webapps/examples/jsp/checkbox/check.html b/webapps/examples/jsp/checkbox/check.html new file mode 100644 index 0000000..b6d6b3b --- /dev/null +++ b/webapps/examples/jsp/checkbox/check.html @@ -0,0 +1,38 @@ + + + + + + +
    +
    + +Check all Favorite fruits:
    + + Apples
    + Grapes
    + Oranges
    + Melons
    + + +
    + +
    +
    + + diff --git a/webapps/examples/jsp/checkbox/checkresult.jsp b/webapps/examples/jsp/checkbox/checkresult.jsp new file mode 100644 index 0000000..4e64739 --- /dev/null +++ b/webapps/examples/jsp/checkbox/checkresult.jsp @@ -0,0 +1,65 @@ +<%-- + 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. +--%> + + + +<%! String[] fruits; %> + + + +
    +The checked fruits (got using request) are:
    +<% + fruits = request.getParameterValues("fruit"); +%> +
      +<% + if (fruits != null) { + for (String fruit : fruits) { +%> +
    • +<% + out.println (util.HTMLFilter.filter(fruit)); + } + } else out.println ("none selected"); +%> +
    +
    +
    + +The checked fruits (got using beans) are
    + +<% + fruits = foo.getFruit(); +%> +
      +<% + if (!fruits[0].equals("1")) { + for (String fruit : fruits) { +%> +
    • +<% + out.println (util.HTMLFilter.filter(fruit)); + } + } else { + out.println ("none selected"); + } +%> +
    +
    + + diff --git a/webapps/examples/jsp/checkbox/cresult.html b/webapps/examples/jsp/checkbox/cresult.html new file mode 100644 index 0000000..b6a28d6 --- /dev/null +++ b/webapps/examples/jsp/checkbox/cresult.html @@ -0,0 +1,34 @@ + + + + +Untitled Document + + + + +

    + +

    Source Code for Checkbox Example +

    + +

    Property Sheet for CheckTest +

    + + + diff --git a/webapps/examples/jsp/colors/ColorGameBean.html b/webapps/examples/jsp/colors/ColorGameBean.html new file mode 100644 index 0000000..172bc66 --- /dev/null +++ b/webapps/examples/jsp/colors/ColorGameBean.html @@ -0,0 +1,116 @@ + + + + + +colors.ColorGameBean Bean Properties + + +

    +colors.ColorGameBean Bean Properties +

    +
    +
    +
    public class ColorGameBean
    extends Object
    + +

    +


    + +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +Properties Summary
    + +String +ColorGameBean:color2 +
    +
    + +Single +
    + +String +ColorGameBean:color1 +
    +
    + +Single +
    + +int +ColorGameBean:attempts +
    +
    + +Single +
    + +boolean +ColorGameBean:hint +
    +
    + +Single +
    + +boolean +ColorGameBean:success +
    +
    + +Single +
    + +boolean +ColorGameBean:hintTaken +
    +
    + +Single +
    +


    + + diff --git a/webapps/examples/jsp/colors/clr.html b/webapps/examples/jsp/colors/clr.html new file mode 100644 index 0000000..e411f59 --- /dev/null +++ b/webapps/examples/jsp/colors/clr.html @@ -0,0 +1,34 @@ + + + + +Untitled Document + + + + +

    + +

    Source Code for Color Example +

    + +

    Property Sheet for ColorGameBean +

    + + + diff --git a/webapps/examples/jsp/colors/colors.html b/webapps/examples/jsp/colors/colors.html new file mode 100644 index 0000000..900651e --- /dev/null +++ b/webapps/examples/jsp/colors/colors.html @@ -0,0 +1,47 @@ + + + + + + +
    +This web page is an example using JSP and BEANs. +

    +Guess my favorite two colors + +

    If you fail to guess both of them - you get yellow on red. + +

    If you guess one of them right, either your foreground or + your background will change to the color that was guessed right. + +

    Guess them both right and your browser foreground/background + will change to my two favorite colors to display this page. + +


    +
    +Color #1: +
    +Color #2: +

    + + +

    + +
    + + diff --git a/webapps/examples/jsp/colors/colrs.jsp b/webapps/examples/jsp/colors/colrs.jsp new file mode 100644 index 0000000..ec3af88 --- /dev/null +++ b/webapps/examples/jsp/colors/colrs.jsp @@ -0,0 +1,70 @@ +<%-- + 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. +--%> + + + + + +<% + cb.processRequest(); +%> + +> +> +

    + +<% if (cb.getHint()==true) { %> + +

    Hint #1: Vampires prey at night! +

    Hint #2: Nancy without the n. + +<% } %> + +<% if (cb.getSuccess()==true) { %> + +

    CONGRATULATIONS!! + <% if (cb.getHintTaken()==true) { %> + +

    ( although I know you cheated and peeked into the hints) + + <% } %> + +<% } %> + +

    Total attempts so far: <%= cb.getAttempts() %> +

    + +

    + +

    + +Color #1: + +
    + +Color #2: + +

    + + + + +

    + +
    + + diff --git a/webapps/examples/jsp/dates/date.html b/webapps/examples/jsp/dates/date.html new file mode 100644 index 0000000..683ab4d --- /dev/null +++ b/webapps/examples/jsp/dates/date.html @@ -0,0 +1,31 @@ + + + + +Untitled Document + + + + +

    + +

    Source Code for Date Example +

    + + + diff --git a/webapps/examples/jsp/dates/date.jsp b/webapps/examples/jsp/dates/date.jsp new file mode 100644 index 0000000..d6c6b86 --- /dev/null +++ b/webapps/examples/jsp/dates/date.jsp @@ -0,0 +1,41 @@ +<%-- + 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. +--%> + + +<%@ page session="false"%> + + + + + +
      +
    • Day of month: is +
    • Year: is +
    • Month: is +
    • Time: is +
    • Date: is +
    • Day: is +
    • Day Of Year: is +
    • Week Of Year: is +
    • era: is +
    • DST Offset: is +
    • Zone Offset: is +
    +
    + + + diff --git a/webapps/examples/jsp/error/er.html b/webapps/examples/jsp/error/er.html new file mode 100644 index 0000000..af78159 --- /dev/null +++ b/webapps/examples/jsp/error/er.html @@ -0,0 +1,31 @@ + + + + +Untitled Document + + + + +

    + +

    Source Code for Error Example +

    + + + diff --git a/webapps/examples/jsp/error/err.jsp b/webapps/examples/jsp/error/err.jsp new file mode 100644 index 0000000..d188456 --- /dev/null +++ b/webapps/examples/jsp/error/err.jsp @@ -0,0 +1,44 @@ +<%-- + 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. +--%> + + + + <%@ page errorPage="errorpge.jsp" %> + + <% + String name = null; + + if (request.getParameter("name") == null) { + %> + <%@ include file="error.html" %> + <% + } else { + foo.setName(request.getParameter("name")); + if (foo.getName().equalsIgnoreCase("integra")) + name = "acura"; + if (name.equalsIgnoreCase("acura")) { + %> + +

    Yes!!! Acura is my favorite car. + + <% + } + } + %> + + + diff --git a/webapps/examples/jsp/error/error.html b/webapps/examples/jsp/error/error.html new file mode 100644 index 0000000..b1b029c --- /dev/null +++ b/webapps/examples/jsp/error/error.html @@ -0,0 +1,37 @@ + + + + + +

    This example uses errorpage directive

    +
    +

    Select my favourite car.

    +
    + + +
    +
    + + + diff --git a/webapps/examples/jsp/error/errorpge.jsp b/webapps/examples/jsp/error/errorpge.jsp new file mode 100644 index 0000000..5c6eb0a --- /dev/null +++ b/webapps/examples/jsp/error/errorpge.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> + + + + + <%@ page isErrorPage="true" %> +

    The exception <%= exception.getMessage() %> tells me you + made a wrong choice. + + diff --git a/webapps/examples/jsp/forward/forward.jsp b/webapps/examples/jsp/forward/forward.jsp new file mode 100644 index 0000000..092d9b4 --- /dev/null +++ b/webapps/examples/jsp/forward/forward.jsp @@ -0,0 +1,33 @@ +<%-- + 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. +--%> + +<% + double freeMem = Runtime.getRuntime().freeMemory(); + double totlMem = Runtime.getRuntime().totalMemory(); + double percent = freeMem/totlMem; + if (percent < 0.5) { +%> + + + +<% } else { %> + + + +<% } %> + + diff --git a/webapps/examples/jsp/forward/fwd.html b/webapps/examples/jsp/forward/fwd.html new file mode 100644 index 0000000..b3b0219 --- /dev/null +++ b/webapps/examples/jsp/forward/fwd.html @@ -0,0 +1,30 @@ + + + +Untitled Document + + + + +

    + +

    Source Code for Forward Example +

    + + + diff --git a/webapps/examples/jsp/forward/one.jsp b/webapps/examples/jsp/forward/one.jsp new file mode 100644 index 0000000..c7f0004 --- /dev/null +++ b/webapps/examples/jsp/forward/one.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> + + + + + +VM Memory usage < 50%. + \ No newline at end of file diff --git a/webapps/examples/jsp/forward/two.html b/webapps/examples/jsp/forward/two.html new file mode 100644 index 0000000..24f4c08 --- /dev/null +++ b/webapps/examples/jsp/forward/two.html @@ -0,0 +1,23 @@ + + + + + + +VM Memory usage > 50%. + \ No newline at end of file diff --git a/webapps/examples/jsp/images/code.gif b/webapps/examples/jsp/images/code.gif new file mode 100644 index 0000000000000000000000000000000000000000..93af2cd130aa61cb2f235cdd6f0e75ab444819ef GIT binary patch literal 292 zcmZ?wbhEHblwgoxIP#z2z<~q*(`NopOZ#sOM8;>*(#{wIk?|P@o$6#>Qz3K*V4S1Pnk5hzS72pDc_ZB|0DiWTyk`LIs7s6iv0@q;(ETk{*X8*p&Sz zS?%Jy|7uNwX5dT_rVVU*FIJccbhvMP#4t}Kw7uYTuBh_VnCTjt97)}qgEke3iU+j# zdTyN*!`HsV>FFn-s95XDrdAynk)$>kVJ;@##Ikl#0e5yDZsrM%j-vc*EJY@UJi>F- w^aL|Jc?Bj;=4WLO5adbda@4El6|}Iev#dyLst@fxxV)*dq0M4 zuU27uS$av`))UvZFOHHZ*ItST2O0$7;7H? zm&+9jg_V_+QmIrfmut0JNgWD>N~y#dv!$ejV3mcCNeF_Fs91zF06g0g%rptkdaf-6 zF_0xVM$i_FQ|#InC0H;@)orLVf()pw6mqsDEh%itRj8`R2I3~Mg(*yIbHb?sbqyn3 zQYZyCsY->?M4l}qr>- z0I$VBAEL|c3!Aa23>>`6zgZI-x?7(TmI_v%;048`BL-aEhiAV!!lj_8tNy{ zuHVlON6V>(=ELvD+RJkuyyRyVcEVSxb@v=^_xJ8_AXukA`|R1H)q9Ptr@mW4Q_jP; zvV-p8YH%TPuI2Q*17~mjF+W7_4*dGfOe`P2(>42bIlAt3wjaL+tnW9`gI6Q#4vn(x+N>Q)$&RbyXeH}5khoAZ zgHYWn8m8H>&V#9A@nKri+6f7CAZX^n(t)JHP^8RL7z)bJ*uqp1CeXrcK^P~G{tuqV z^BsQQ$2pwh@zEn}1p^F%5QJd}Aw&paj0qu>(zUfUmSs7P<9S{Xgle@~tJOqNtk>(3 zB+0U@D2l48nx^TxZWsoa&$A3e34s7$jDj$XWIqfUmW{ZO5C9=SDAKGTH2r`GRm|~r z5L$j98>Zp~kYOR42b6JwQ1b%S^%chx9UoE#D+ZEvK&b8ovKvT_U$;Hc_5{o0Esv@u zV|vtZNi@a@L^X--U`fN84f8w{M0eXUG$N)K%8oDDURAev)u`Ei)$*#g&zml%+l*qc z5u_%u5lz+^!=({{P?YDX?hsigszo##*Hy@JNK{Z=MY4eu6Y@37k1~gfg@L4*NB(TMHD`z5Mi$4{3$K3Fd0=2+ z2&SJ^4mHd1^M91~z5jb@w)hR4@9nxce6>IZv*odP-zSex6`o(pw(oi7X69J;zL6Ew zmMCYwj%&NFbnm5LWxe~Y@k3x5DnB+pXsm90nHrr#zm +To get the current time in ms diff --git a/webapps/examples/jsp/include/foo.jsp b/webapps/examples/jsp/include/foo.jsp new file mode 100644 index 0000000..bb476c7 --- /dev/null +++ b/webapps/examples/jsp/include/foo.jsp @@ -0,0 +1,17 @@ +<%-- + 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. + +--%><%= System.currentTimeMillis() %> diff --git a/webapps/examples/jsp/include/inc.html b/webapps/examples/jsp/include/inc.html new file mode 100644 index 0000000..fedaed0 --- /dev/null +++ b/webapps/examples/jsp/include/inc.html @@ -0,0 +1,30 @@ + + + +Untitled Document + + + + +

    + +

    Source Code for Include Example +

    + + + diff --git a/webapps/examples/jsp/include/include.jsp b/webapps/examples/jsp/include/include.jsp new file mode 100644 index 0000000..62a8c22 --- /dev/null +++ b/webapps/examples/jsp/include/include.jsp @@ -0,0 +1,30 @@ +<%-- + 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. +--%> + + + + + + +<%@ page buffer="5kb" autoFlush="false" %> + +

    In place evaluation of another JSP which gives you the current time: <%@ include file="foo.jsp" %> + +

    by including the output of another JSP: +:-) + + diff --git a/webapps/examples/jsp/index.html b/webapps/examples/jsp/index.html new file mode 100644 index 0000000..b237120 --- /dev/null +++ b/webapps/examples/jsp/index.html @@ -0,0 +1,361 @@ + + + + + + JSP Examples + + + +

    JSP +Samples

    +

    This is a collection of samples demonstrating the usage of different +parts of the Java Server Pages (JSP) specification. Both JSP 2.0 and +JSP 1.2 examples are presented below. +

    These examples will only work when these pages are being served by a +servlet engine; of course, we recommend +Tomcat. +They will not work if you are viewing these pages via a +"file://..." URL. +

    To navigate your way through the examples, the following icons will +help:

    +
      +
    • Execute the example
    • +
    • Look at the source code for the example
    • +
    • Return to this screen
    • +
    + +

    Tip: For session scoped beans to work, the cookies must be enabled. +This can be done using browser options.

    +

    JSP 2.0 Examples

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Expression Language
    Basic ArithmeticExecuteSource
    Basic ComparisonsExecuteSource
    Implicit ObjectsExecuteSource
    FunctionsExecuteSource
    Composite ExpressionsExecuteSource

    SimpleTag Handlers and JSP Fragments
    Hello World TagExecuteSource
    Repeat TagExecuteSource
    Book ExampleExecuteSource

    Tag Files
    Hello World Tag FileExecuteSource
    Panel Tag FileExecuteSource
    Display Products ExampleExecuteSource

    New JSP XML Syntax (.jspx)
    XHTML Basic ExampleExecuteSource
    SVG (Scalable Vector Graphics)ExecuteSource

    Other JSP 2.0 Features
    <jsp:attribute> and <jsp:body>ExecuteSource
    Shuffle ExampleExecuteSource
    Attributes With Dynamic NamesExecuteSource
    JSP ConfigurationExecuteSource
    + +

    JSP 1.2 Examples

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NumberguessExecuteSource
    DateExecuteSource
    SnoopExecuteSource
    ErrorPageExecuteSource
    CartsExecuteSource
    CheckboxExecuteSource
    ColorExecuteSource
    CalendarExecuteSource
    IncludeExecuteSource
    ForwardExecuteSource
    JSP-Servlet-JSPExecuteSource
    Custom tag exampleExecuteSource
    XML syntax exampleExecuteSource
    + +

    Tag Plugins

    + + + + + + + + + + + + + + + + + + + + +
    If + + Execute + + + Source +
    ForEach + + Execute + + + Source +
    Choose + + Execute + + + Source +
    + +

    Other Examples

    + + + + + + + + + + + +
    FORM Authentication + Execute +
    Example that demonstrates protecting a resource and + using Form-Based authentication. To access the page the user must + have role of either "tomcat" or "role1". By default no user + is configured to have these roles.
    + + diff --git a/webapps/examples/jsp/jsp2/el/basic-arithmetic.html b/webapps/examples/jsp/jsp2/el/basic-arithmetic.html new file mode 100644 index 0000000..8a2f0a6 --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/basic-arithmetic.html @@ -0,0 +1,30 @@ + + + +View Source Code + + + + +

    + +

    Source Code for Basic Arithmetic Example +

    + + + diff --git a/webapps/examples/jsp/jsp2/el/basic-arithmetic.jsp b/webapps/examples/jsp/jsp2/el/basic-arithmetic.jsp new file mode 100644 index 0000000..757e809 --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/basic-arithmetic.jsp @@ -0,0 +1,88 @@ +<%-- + 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. +--%> + + + JSP 2.0 Expression Language - Basic Arithmetic + + +

    JSP 2.0 Expression Language - Basic Arithmetic

    +
    + This example illustrates basic Expression Language arithmetic. + Addition (+), subtraction (-), multiplication (*), division (/ or div), + and modulus (% or mod) are all supported. Error conditions, like + division by zero, are handled gracefully. +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    EL ExpressionResult
    \${1}${1}
    \${1 + 2}${1 + 2}
    \${1.2 + 2.3}${1.2 + 2.3}
    \${1.2E4 + 1.4}${1.2E4 + 1.4}
    \${-4 - 2}${-4 - 2}
    \${21 * 2}${21 * 2}
    \${3/4}${3/4}
    \${3 div 4}${3 div 4}
    \${3/0}${3/0}
    \${10%4}${10%4}
    \${10 mod 4}${10 mod 4}
    \${(1==2) ? 3 : 4}${(1==2) ? 3 : 4}
    +
    +
    + + diff --git a/webapps/examples/jsp/jsp2/el/basic-comparisons.html b/webapps/examples/jsp/jsp2/el/basic-comparisons.html new file mode 100644 index 0000000..60fb40a --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/basic-comparisons.html @@ -0,0 +1,30 @@ + + + +View Source Code + + + + +

    + +

    Source Code for Basic Comparisons Example +

    + + + diff --git a/webapps/examples/jsp/jsp2/el/basic-comparisons.jsp b/webapps/examples/jsp/jsp2/el/basic-comparisons.jsp new file mode 100644 index 0000000..d72f724 --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/basic-comparisons.jsp @@ -0,0 +1,116 @@ +<%-- + 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. +--%> + + + JSP 2.0 Expression Language - Basic Comparisons + + +

    JSP 2.0 Expression Language - Basic Comparisons

    +
    + This example illustrates basic Expression Language comparisons. + The following comparison operators are supported: +
      +
    • Less-than (< or lt)
    • +
    • Greater-than (> or gt)
    • +
    • Less-than-or-equal (<= or le)
    • +
    • Greater-than-or-equal (>= or ge)
    • +
    • Equal (== or eq)
    • +
    • Not Equal (!= or ne)
    • +
    +
    + Numeric + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    EL ExpressionResult
    \${1 < 2}${1 < 2}
    \${1 lt 2}${1 lt 2}
    \${1 > (4/2)}${1 > (4/2)}
    \${1 gt (4/2)}${1 gt (4/2)}
    \${4.0 >= 3}${4.0 >= 3}
    \${4.0 ge 3}${4.0 ge 3}
    \${4 <= 3}${4 <= 3}
    \${4 le 3}${4 le 3}
    \${100.0 == 100}${100.0 == 100}
    \${100.0 eq 100}${100.0 eq 100}
    \${(10*10) != 100}${(10*10) != 100}
    \${(10*10) ne 100}${(10*10) ne 100}
    +
    +
    + Alphabetic + + + + + + + + + + + + + + + + + + +
    EL ExpressionResult
    \${'a' < 'b'}${'a' < 'b'}
    \${'hip' > 'hit'}${'hip' > 'hit'}
    \${'4' > 3}${'4' > 3}
    +
    +
    + + diff --git a/webapps/examples/jsp/jsp2/el/composite.html b/webapps/examples/jsp/jsp2/el/composite.html new file mode 100644 index 0000000..5900008 --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/composite.html @@ -0,0 +1,31 @@ + + + +View Source Code + + + + +

    + +

    Source Code for composite.jsp

    +

    Source Code for ValuesTag.java

    +

    Source Code for ValuesBean.java

    + + + diff --git a/webapps/examples/jsp/jsp2/el/composite.jsp b/webapps/examples/jsp/jsp2/el/composite.jsp new file mode 100644 index 0000000..ae671d4 --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/composite.jsp @@ -0,0 +1,110 @@ +<%-- + 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. +--%> +<%@ taglib prefix="my" uri="http://tomcat.apache.org/example-taglib" %> + + + + JSP 2.0 Expression Language - Composite Expressions + + +

    JSP 2.0 Expression Language - Composite Expressions

    +
    + This example illustrates EL composite expressions. Composite expressions + are formed by grouping together multiple EL expressions. Each of them is + evaluated from left to right, coerced to String, all those strings are + concatenated, and the result is coerced to the expected type. + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    EL ExpressionTypeResult
    \${'hello'} wo\${'rld'}String${values.stringValue}
    \${'hello'} wo\${'rld'}String
    \${1+2}.\${220}Double${values.doubleValue}
    \${1+2}.\${220}Double
    000\${1}\${7}Long${values.longValue}
    000\${1}\${7}Long
    \${undefinedFoo}hello world\${undefinedBar}String${values.stringValue}
    \${undefinedFoo}hello world\${undefinedBar}String
    \${undefinedFoo}\${undefinedBar}Double${values.doubleValue}
    \${undefinedFoo}\${undefinedBar}Double
    \${undefinedFoo}\${undefinedBar}Long${values.longValue}
    \${undefinedFoo}\${undefinedBar}Long
    +
    +
    + + + diff --git a/webapps/examples/jsp/jsp2/el/functions.html b/webapps/examples/jsp/jsp2/el/functions.html new file mode 100644 index 0000000..726dda3 --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/functions.html @@ -0,0 +1,32 @@ + + + +View Source Code + + + + +

    + +

    Source Code for functions.jsp +

    +

    Source Code for Functions.java +

    + + + diff --git a/webapps/examples/jsp/jsp2/el/functions.jsp b/webapps/examples/jsp/jsp2/el/functions.jsp new file mode 100644 index 0000000..12b3fa9 --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/functions.jsp @@ -0,0 +1,67 @@ +<%-- + 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. +--%> +<%@page contentType="text/html; charset=UTF-8" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="my" uri="http://tomcat.apache.org/jsp2-example-taglib"%> + + + + JSP 2.0 Expression Language - Functions + + +

    JSP 2.0 Expression Language - Functions

    +
    + An upgrade from the JSTL expression language, the JSP 2.0 EL also + allows for simple function invocation. Functions are defined + by tag libraries and are implemented by a Java programmer as + static methods. + +
    + Change Parameter +
    + foo = + +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    EL ExpressionResult
    \${param["foo"]}${fn:escapeXml(param["foo"])} 
    \${my:reverse(param["foo"])}${my:reverse(fn:escapeXml(param["foo"]))} 
    \${my:reverse(my:reverse(param["foo"]))}${my:reverse(my:reverse(fn:escapeXml(param["foo"])))} 
    \${my:countVowels(param["foo"])}${my:countVowels(fn:escapeXml(param["foo"]))} 
    +
    +
    + + + diff --git a/webapps/examples/jsp/jsp2/el/implicit-objects.html b/webapps/examples/jsp/jsp2/el/implicit-objects.html new file mode 100644 index 0000000..15268db --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/implicit-objects.html @@ -0,0 +1,31 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for Implicit Objects Example +

    + + + diff --git a/webapps/examples/jsp/jsp2/el/implicit-objects.jsp b/webapps/examples/jsp/jsp2/el/implicit-objects.jsp new file mode 100644 index 0000000..b557714 --- /dev/null +++ b/webapps/examples/jsp/jsp2/el/implicit-objects.jsp @@ -0,0 +1,90 @@ +<%-- + 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. +--%> +<%@page contentType="text/html; charset=UTF-8" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + JSP 2.0 Expression Language - Implicit Objects + + +

    JSP 2.0 Expression Language - Implicit Objects

    +
    + This example illustrates some of the implicit objects available + in the Expression Language. The following implicit objects are + available (not all illustrated here): +
      +
    • pageContext - the PageContext object
    • +
    • pageScope - a Map that maps page-scoped attribute names to + their values
    • +
    • requestScope - a Map that maps request-scoped attribute names + to their values
    • +
    • sessionScope - a Map that maps session-scoped attribute names + to their values
    • +
    • applicationScope - a Map that maps application-scoped attribute + names to their values
    • +
    • param - a Map that maps parameter names to a single String + parameter value
    • +
    • paramValues - a Map that maps parameter names to a String[] of + all values for that parameter
    • +
    • header - a Map that maps header names to a single String + header value
    • +
    • headerValues - a Map that maps header names to a String[] of + all values for that header
    • +
    • initParam - a Map that maps context initialization parameter + names to their String parameter value
    • +
    • cookie - a Map that maps cookie names to a single Cookie object.
    • +
    + +
    + Change Parameter +
    + foo = + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    EL ExpressionResult
    \${param.foo}${fn:escapeXml(param["foo"])} 
    \${param["foo"]}${fn:escapeXml(param["foo"])} 
    \${header["host"]}${fn:escapeXml(header["host"])} 
    \${header["accept"]}${fn:escapeXml(header["accept"])} 
    \${header["user-agent"]}${fn:escapeXml(header["user-agent"])} 
    +
    +
    + + diff --git a/webapps/examples/jsp/jsp2/jspattribute/jspattribute.html b/webapps/examples/jsp/jsp2/jspattribute/jspattribute.html new file mode 100644 index 0000000..df1b6e6 --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspattribute/jspattribute.html @@ -0,0 +1,37 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for jspattribute.jsp +

    + +

    Source Code for HelloWorldSimpleTag.java +

    + +

    Source Code for FooBean.java +

    + + + diff --git a/webapps/examples/jsp/jsp2/jspattribute/jspattribute.jsp b/webapps/examples/jsp/jsp2/jspattribute/jspattribute.jsp new file mode 100644 index 0000000..8050b34 --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspattribute/jspattribute.jsp @@ -0,0 +1,46 @@ +<%-- + 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. +--%> +<%@ taglib prefix="my" uri="http://tomcat.apache.org/jsp2-example-taglib"%> + + + + JSP 2.0 Examples - jsp:attribute and jsp:body + + +

    JSP 2.0 Examples - jsp:attribute and jsp:body

    +
    +

    The new <jsp:attribute> and <jsp:body> + standard actions can be used to specify the value of any standard + action or custom action attribute.

    +

    This example uses the <jsp:attribute> + standard action to use the output of a custom action invocation + (one that simply outputs "Hello, World!") to set the value of a + bean property. This would normally require an intermediary + step, such as using JSTL's <c:set> action.

    +
    + + Bean created! Setting foo.bar...
    + + + + + +
    +
    + Result: ${foo.bar} + + diff --git a/webapps/examples/jsp/jsp2/jspattribute/shuffle.html b/webapps/examples/jsp/jsp2/jspattribute/shuffle.html new file mode 100644 index 0000000..5711860 --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspattribute/shuffle.html @@ -0,0 +1,37 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for shuffle.jsp +

    + +

    Source Code for ShuffleSimpleTag.java +

    + +

    Source Code for TileSimpleTag.java +

    + + + diff --git a/webapps/examples/jsp/jsp2/jspattribute/shuffle.jsp b/webapps/examples/jsp/jsp2/jspattribute/shuffle.jsp new file mode 100644 index 0000000..737ff65 --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspattribute/shuffle.jsp @@ -0,0 +1,90 @@ +<%-- + 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. +--%> +<%@ taglib prefix="my" uri="http://tomcat.apache.org/jsp2-example-taglib"%> + + + + JSP 2.0 Examples - Shuffle Example + + +

    JSP 2.0 Examples - Shuffle Example

    +
    +

    Try reloading the page a few times. Both the rows and the columns + are shuffled and appear different each time.

    +

    Here's how the code works. The SimpleTag handler called + <my:shuffle> accepts three attributes. Each attribute is a + JSP Fragment, meaning it is a fragment of JSP code that can be + dynamically executed by the shuffle tag handler on demand. The + shuffle tag handler executes the three fragments in a random order. + To shuffle both the rows and the columns, the shuffle tag is used + with itself as a parameter.

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + diff --git a/webapps/examples/jsp/jsp2/jspx/basic.html b/webapps/examples/jsp/jsp2/jspx/basic.html new file mode 100644 index 0000000..f9df6a4 --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspx/basic.html @@ -0,0 +1,31 @@ + + + + + +View Source Code + + + +

    ExecuteReturn

    + +

    Source Code for XHTML Basic Example

    + + + diff --git a/webapps/examples/jsp/jsp2/jspx/basic.jspx b/webapps/examples/jsp/jsp2/jspx/basic.jspx new file mode 100644 index 0000000..fc1e45f --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspx/basic.jspx @@ -0,0 +1,48 @@ + + + + + + + JSPX - XHTML Basic Example + + +

    JSPX - XHTML Basic Example

    + This example illustrates how to use JSPX to produce an XHTML basic + document suitable for use with mobile phones, televisions, + PDAs, vending machines, pagers, car navigation systems, + mobile game machines, digital book readers, smart watches, etc. +

    + JSPX lets you create dynamic documents in a pure XML syntax compatible + with existing XML tools. The XML syntax in JSP 1.2 was awkward and + required &lt;jsp:root&gt; to be the root element of the document. + This is no longer the case in JSP 2.0. +

    + This particular example uses + namespace declarations to make the output of this page a valid XHTML + document. +

    + Just to prove this is live, here's some dynamic content: + + + + diff --git a/webapps/examples/jsp/jsp2/jspx/svgexample.html b/webapps/examples/jsp/jsp2/jspx/svgexample.html new file mode 100644 index 0000000..f7d591a --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspx/svgexample.html @@ -0,0 +1,46 @@ + + + + + JSP 2.0 SVG Example + + +

    JSP 2.0 SVG Example

    + This example uses JSP 2.0's new, simplified JSPX syntax to render a + Scalable Vector Graphics (SVG) document. When you view the source, + notice the lack of a <jsp:root> element! The text to be rendered + can be modified by changing the value of the name parameter. +

    + SVG has many potential uses, such as searchable images, or images + customized with the name of your site's visitor (e.g. a "Susan's Store" + tab image). JSPX is a natural fit for generating dynamic XML content + such as SVG. +

    + To execute this example you will need a browser with basic SVG support. Any + remotely recent browser should have this. +

      +
    1. Use this URL: + textRotate.jspx?name=JSPX
    2. +
    3. Customize by changing the name=JSPX parameter
    4. +
    +

    + The following is a screenshot of the resulting image, for those using a + browser without SVG support:
    + [Screenshot image] + + diff --git a/webapps/examples/jsp/jsp2/jspx/textRotate.html b/webapps/examples/jsp/jsp2/jspx/textRotate.html new file mode 100644 index 0000000..5b3befe --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspx/textRotate.html @@ -0,0 +1,32 @@ + + + + + +View Source Code + + + +

    Execute Return

    + +

    Source Code for SVG (Scalable Vector Graphics) +Example

    + + + diff --git a/webapps/examples/jsp/jsp2/jspx/textRotate.jpg b/webapps/examples/jsp/jsp2/jspx/textRotate.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9e987367bc83c25aedf1c7afd76f34de4a229f82 GIT binary patch literal 26729 zcmdS9bx>X35-xafhd>~~J;5QkTL>Q95AMPB;O-Dy6Wsma?(QxJI7o1J3(oMnck13Z zQ!`WVz5izK>Z-2VYp=b&TD^96f8DQ(uj_z!G7{1f02mk;fWg}hcwGjF0gw?9kq{A( zk&uv3P>@m4aWT-*(9j8R-ecjC5R#FS5E2uUQ!~?$Q!-K!6Vr0jF@9m?;NT#m;T7az z6JTcNVEbnh7!(u~bTo8)3=DiW3StVj|K;no3xJIba}Co22SW*f#fE{yhI#D+kO2TN z2yd(XXTksH1p^BQkAR4TjDm{xHlY3;02T%g4i+8`0RbNVZM65>a{xRx0uBYMDB^o1 zLnKNETsHr>9Av7m)!lf?)92LeMveg}s2@J!6A*s-OhZdY&%w#X&BMzl_Dx(uQc7Az zMO95*LsJWAY+`C={=>r3$rgMhd7!({58WtWApOBc8obu~;YHnVBL19sGNoh@O zU427iQ*%pCZ(skw;Lz~M%y5iBtwVylx+UUxL@OPs=HCB*p<)m zj2x#?KTvaQe7g7t+P{$f&jAbg-$M2uVE@Gh1)#&hyloyFHb5Bg_klLo5B2{KLSHq< z6t%4Fo-Na6@K-3HeA?hY%QmT%gbhiW$4xg$wdwD3srqedP5bO!mfX~wrPihjSJFdc zsD{6S*%A=~!K&4k#JqfzV6(K0{m=Wfc~m^vuo1NXdy>=C+E}M*l@^wx0g6J8#$~VW zQp(KF3@EH%+0=LnaMpjsT=d=F5Fx>v46BnBlow<>(x?h~`3kEoZU2+%{S*R?!0k=0 zKK75~htOLu=s*_9zCowCXCjWtCE|=zLY$6mc4G(p+zpAcrxYg(rsc;`kke;CQ%zh% zu{9~Rh|3l}!&yZ49LKn<-UU`422QVJL-WVR4;P##W2I$QY0mcz>(x}ngI-P|q}|lv zxHi<$C4{hJWA?b1`Kft@R#1sLQ$oEGQ!hiFSZ(xUB>JhpC(j%23O43iKZ7nxau z5XGiXwTscs724mz+eA07FXZYd|44`QmmjAQ=^e9|5wJp88+VW@q}Q=yF=To?$`PT88YS>u=b zIZKiaOtjL>ph`t7u*kzi3bxYc0<++vN4C=p3A0x~rg2v1%Or69g?8AfQ0JhfF|;qV zZa4Of_l5RGvhhhWyJ>ULzH2%iPN47>_?gL9q;^e(M)gSv?8; zsc3E~y&6Deo##wx89pf$j}S=>qLh~SZ0vnG_m1jKUCT|3uXNTYU`{Y6Ri=K;#%606 z>#(xK@w;+>K=eQjEzQ8Cz~rb8=vQS5z=m$sw$Z1lHrzK0)UnH6* z;f@y8=o;H0wrJYg0&Cy+tV-{b`+0o7Z1FEe+qarFC}uRg~J1!Vv4stAY8vo^%mVOap##&Prx)y@0P-HB^ndi))^XRx<}2M zNLYcZJgvw#mJFk(H~P481#Y8Sp?wl-f|!@v+S+JR5ku9K9fL;AxS%i<0>Hv1QXOMe40S9#V2JikfNkBUUK7JD>Mcq5e$J~lyH zE`n^V-`hKDXKkX!pI1Iq!NMfF!_v|eQExW0VE+WO=_9UiF6$hkl}MU1b&C`_$8+PD zssdo3L;!~>57jIjT(0nfGJxulIw0`R-y;!{&spUM9#u|-hw85WLKh7gCd)TaIUO_9 zf?s|}%AOjTcaz6_cHv&Eo&|%rz^XZc-|*yg$%}aec-3?0bbC1!j)VO|)C;HJN|&|z z@|xB=lQ|UDrqBjy#viy`H{a*JN9&j2+AFUbSV(CIYpj zfa^-Owt3KY6@-(gUZl*pgviZi3|VbqOszNF9N%_PXs=O;tG@h>?W|8d;ZKGd<2C0z z1&&DEaf(-jsx{#F9%eh?OpQVO2U4Mu4u@;%S}6F=l7HOcMiUg`W*#j?DM}QUDso}1 z#_yx^uD^*Y_0)>|zfR$Q!7uJ{c4Dp_K{I}}%#rJUh8!~VH^>@&wxHrwH2D+NTIEnH zSgT8d3o|*q-cIOw+wz;`6NWGpqb{YE15xL6b?3vmZ%skWVm3&~M4Png zuG=IupX#+hXyLIe#PF{4e3_tzqH7fK^G`a6wbjZoD!g+eBaWI)Haw zLt3XYZ10K%LhcsD=b7o-Q-OMnq02S`u}Y}mtf|OxK<&9aH4dAL#}nmIN=S$_xfz$V z*pT}$c$^Cz9o3dCVLq9Yc{EBE=^lw(hb8II3lxA+*;grtnpk3uld6SYg6V%Vd|^xL z^m#DqXIg|vK_@olwx@YP<4*R!)LVWp z4vA2>vJNjVFK04TX08$ax|PER{XLPn6tl3^la$Jda?}b_Qxufmr%x;|_q(EPBSVbY zSZovY2OLSBW%0uSvG~??3OZu+LOvju=)hTio)G7)x z-usN!e+A25>3dNsMAEv<0IlBWFnRd<3J{vNTZ`%vU01-WNW%!wUMXmAdj{M{j;$08!_-Ro*SRzmfqE28hW3PcXr9ueX^Mw;|F2BlI z?5!-A3+IRzb=oa$JDn~I{2TDTj;yC0vsutgm_;RJx~lgQOm}9$u;?XJ!|gE#ZAlC6 zoXb2hir7^P-=g3uuMWT)qIV;Yw#xHH`kwr7c&Ys@*|W^Sid{-J@ZIDP438QwF97}T zxK2aDczuMz8M$zQtd@T`R)`IG`^+lI%W%P0<%y-=S;ny%?q<~0$SyG=+7%K^M|__` zIt>`>Y4tSW!)Z|%`bG{N%L$Ol1K7L4sI+j&)#sm+3`+!8=qRXdG*Zn|McNlU<3%P5 zq#f2zKCD)-fU}zdl)sO~u8zbB_<8R}a`m|FQDxrD%zIu%39Y{ZX1T};G}x^%Km6%^ zFdgeE7@EoJySk4(8A7Tn6Z z+Uzop8`bH?lxaU@*7b8E=tcfD#Xr;hf5^xJ2L_F{zkErNwjMs-n1l&NEx9?F4%Wz< zMfTj@3gO>zVntj(vyE9{TObGB_Yl%K#oXAl#a|LFZz@RMYT{!ku? zv1!m-AI*xJlzi5FE`w5A+SWuH$8Tp{rCztjol)$|01ty|n?tr-g&VyM&IlFtOdabW zg?%A0UlzB3WA=Oh;Q{^yF+)?k!h>4~S9RFQ$);wW=#r`d){r6+d!7~F->`bwgX8Vl z6OJ^Jud-CQkTdGpdz%Icbds-5>0aP7o#>46yTGs27eYm5slK;1m0=a|C2Xy-Lih8U zbnx(9wkQ}0i>{{|=jkpUeSe`o;Nm55Cr9QA>m1f!W(koA&1lTWUdx(h@d}6*Ge24i z6U|yKIf)?czXO?fTw-}qUDlaZLGWAc03)|Q0~#Gf@td9uCtRj=nrpfO&{68KauNP~ z_2rL9RF3J8&jlq`sYO3;Fob2IXRYm-2GiKs-=P>r!m|IBEaW}XWeF2Nvnu{vPun>+ z!=%Wdv($A^BaQd9>@wINM#FEB-yN~wyfHK$dU>UiEWNh-17~{d7XeB_D*zow```p9 zy2OPAOOH zgvAz}PA*qhf-Pv#1y(>#%?yXO&F5bA<`ns$Wl1M<2h{IW0Cb_t9RESX0X}t& z5NF}q#cqpo_uCCl!xH3pk#XXPd&M!))nyl6p}N(=ibgmU)eGMUO5YDUT4DvCLPOK5 z*{P#G^@PQLP5*-GF3c?`Ioi!x`?(ct+3_h$W*Q^7MN6yO$?4O!0)p zmLH`zs8hV7${VV4$ji#p*yExVP6zFIIR}A1PE4kI&AmOJvuufmY8)fu zXoP8PT5luI(HyS9bSoITt^0ERLOUtq#S6`AErrs}d6+|T@$mC9e18zl{ZYk2kxd?@IKSoedEq^4o-hBWXa5e)DnVH)S&kL$G7&RW+m%W00XuK~5% z(;XNv3J8+IZTWVPeD&x)dHe0W0yd2G$uQk~q;b~%D9zI}he#(ww8L`Wu`@JTFy?5UfF$n|zmTtAA0((Q_cnAcxu~-$IDK5S z{nS*Sq2;xhDrx59vL4tLi5+QF$3*@pn8&zqxiUdkUYZ^E3V=}*mVta}D3`El=-eD; z;XFIeDSA$mf`i>5Y3trW;X3bv8zyQ#nyCX-60c%mXju`Oo@UulKN!p*MD>?9;xsX| zlC>IEd)kplKGPS5Zpeb=Ujco&-ZC!(6Azlbl9m^nZco|ZjFEnMQ34`)VRUD1AGq1Q z7I&sfyK;l%evbPS8(nzEuC#!Xm4XzQu#s4ME8bJKq(9;HN2^ZXW}Sar-$@h{)IY_q z<_&MJ4^A;KpLIq$9A$nnyg-Vqtc!WKEN16fxT?*SgOCn!fR4=cQBye1A}*Gr!PONH zfls)5lA)l}G&eiuQ)Z6Gk5%D?bXO8%MZ2hZan&MJ#Gv8b+DacXpdRNU;XoPu`?V-h zgQ(3Vj4p%a1zPECb1R&-Og;}diZ+hiUOriN_pEToN&4oM^ z7cp8|_^*sbqk|*t6Yfu?$+dYFQ4Z#AH15nyOoSsmf9cetB5*W~D~I^~hd0yJvmHB7 zFXZz|tz76SC1fAn30#)G=T0}+>Yn~#^xGn~*~33lIml#LaE=MJC_ijeZ~Xg_yVJOC z*@~QYcVp|bTzP*N9EB=e@F4YWrJJk5DWg zMsnST=f~ev+0`}KqDf(>%_ej9((o<;_qPo(xVCLurby#Lr?+q-D`l{yzcE-q*2g9% zb4v!Bvwn1O(gttH%f#K0q^o3z$B2*2a%L`XexiPTciyCqn?~(dVx79G_#<^6vu1ps z!PS5_f~B6D%A<;C=M1Num2yYGg_`UTg=8e%Nvno(XD4 zg%FdaBA(N2+85uMs_RGJy0w8IQ_+<_qQC(S4I@tH6MBv)vnJgMcT^8l2mr?K3vF(- zO?5D?lGOh!U5Bm?q0x0c7B{+V44%L|m4q~uML_c24;qD$g$^IoxRa%tqn1$MNs0(a6WN207G|=|m>sQhC_a>*5<)iV7UJ`XJ)&BFc4%0|M(Z~4iw!EVrE7em)w))Vbc-i2y zIojNu6gRJM6kEYh0C8r$h}92QrR7z=kqx|s01tEFmzI6g*2W;8OA~$Tt9uPxUv8P zVHla_%fYtJwPK!`nS>_88hj)de~%K;NysnGIc1L-ts@X&G~*skWyh3Kq%Bi%nUkXjI) zUi`u|@lEulKgsuT10sZf5-lxObFfuIdU+Yp#0CtsNMY@%eL@9-EXlR zm8nQ|!>Q~O#=xUMH6jpggg?v8_$x} z6Q=#S+2$1>u=5mpH~n7~f`EJfF6tHVpAW_SM=OEuUn<}XT_X3Q`$wj$<>ZJxsqj7+ zsMf=?e_aDL`K=$`Hr3%fLq*+f2 z6w+3=b?OA0zX0^PESna)d$*KxV|C^y&c`UorJ2&a0^nJYfr~`uQC^x!+VmuZIOn!R z-52>o{|z+j?k?eiw?sp|P5<7K=rFggPP>G~`u8cd7BMS+*AVHAr8b&Y2q7NQyad*P z@^(V47(3O13E2^a`YXVBv!1E+#NL6DsmK67hk2)X})c6A$(wV zuK$$MTf$SYEZY!^QG}V8aWDr^636|QlybjAke2|!<|;*7ED6>5L1sCHt8=2onFx4^ zJOY8j%gKMPCiI7Vqns!%pqVwOq3}0wc`qvsZ^W|Wi`ZNI@gLPH3!XG2IfW9=_tA0r z#eMW1>!O&2^Vd$j+om)e&wwUo#v_E`i;_ah$jH6CjtzV~Yv6$-C9F1qalnabl4j{8 z?Awu5{}dgA3khcglbi3MO3JU6+2-1CeR<2<0Q3cGIe2NMLlv^&R@P(WUi}Zqo*q=gxUT^DudBRK1xJT%*)0v$c@;=SJF%&oNItW zp;B?>M1Cw2QdEK3&d?u-Lr28cr}m|6a*hkGEFgZVRX*BlylU{6@)9M|Z!BTa7jXmi z#05pv1CBB!J4@o2H7PcQMnj!r>0O!d=Uv#AhNb{!!XFDHKVifrr%26WmD{iM+(OoYUHF z$*bApJ!~UA{{4%)TI!p(@E}mZgM|1$}%K z0{u-k`g9~Az*~^Y9iN`gOb|fuIkV~^)r+mYzDFj@9hqt^77i$f0xOlu=G(!lG?E6` zZT(@|FmD}*haOs*S!$I&&(Ui^3!m`bd*CG`m%X(XHbSBRcdMR?d3I1H79Ai+M{H{8 zci_BlQ(2d&5hV?0^^O;@y3z(XViwhmKa!=Syw9B}z85>Vo#KDIT4t`%aCJQDf7O45 zRpfhhqNSg#PB8qHo0q2k`_LD8x-OV0C7{l?R8X6S#!vM8U;}&At8}xXIp@L}Q~Jj( z+o0&snbIZc?LQ(f(d^gbpp+@^+zF|RgviP{RT;T2Jdm&1@jS&{!zD#D0OJTp30Q+@ zD-p`v%;7-MsDO}es2dWoA{=cVVg!J$d(o8xs>t>OMuge-ciD$2g1uBu zpiSt-C&MISvYh-|xH2`=9907tKROE-cvwu)eG98!hT^!2Z@>KoP!z$+hN?*_prl0i zIeG3xxXtI|@>=sgg=cs&HG!FMlC-rHTHbYgeTFTMq0MR(Djs+vU#e4OiC_FcZfN6C`(}|!L99Ce9UI*l` zN*DG56E3<`@Y%{`la`Z#Qp4J>gm$aCJiXv!F|kJgF5Chx+bQm@a$$Tqiu2<=$79CU zCoIg&+(yoD|Z z1oFtd@Gcb-13Pxs@p?jrA*9bo(GE#q1*|o94JPZsorI)Y%kZ)?e`U8wZk%tv!C}Qm zVp?*kKeNtg?GBz9gi>8azO8V&OiAx{6KAa{{`plwH^@1^f9alTB2<|at+HR0oY zxfdtg(&tl8qTv1tAgg@^EJ(b75R@0c*+E(aDRC2|)#Bh7s;RNhbhe%k59{9G}|nQGn((y zYGOpcQJ)m!0>=5kdW*xR%#a%gy%*Nq_OmbadXg zw~aEI@2aHkm*4zi{!-&}sBjyDbWv_jT5OhttD^TU9Brsu6-2#R3o{>Fu6O2JE8(N| zxa}&GZko)%_KjiBC>r$(S>ZyM28FG<42J?32x zQfvB4k_?l7vks)(chmHrLnOvr;L5%B+wWRWI7xjcrUXny1ZXYQ_OYdW1$g=Lh1$s3 z_4nzY#q6n5Rpdr6{4z7Ecc!fW!ty8aop(yRov^0!kql z>R&m8x+D@CMZEfJd{+-t%gP$Wxv(h!C6Ta#ZWS&Mb4x3@!uFR%bIOqw$B2w0xh`99 zf~74ou21=8aDxK#zwdvSrmrL6A4M#|7-@$W*vv{@zGF?HPW4MqpFu&RRe?mf07~*= zTuEAIsnFU!POMx-9GBZTTNSH{gzXWR7&m!anlmf%;$q+~i%=6*>wY@*M~XkQX*PkQ zaEkRVOE+bJ6iCY&;16Lz3Tr!?W_MQKdlj8g?9Y?H!}6A{pfL%!;_%a-tVk~(D8&V; zMZ9yYr<{xneZpXX`NX;)5M8gWH1}S<;eAUALVzFE{dIZ$1vU43$#;$RS^?W}YbE0C zibNN({?#omCG?+v{*^g;o{rPGZ7j#$wdf|%-{GH>&};L_0%OOpPhlpeqo>+#5QkGU zx^*LwU^twQmvy~kfx>cqA(18ndkA4{-u&B3BT3k5veV4?kP- z(U!t;KMu4U;Vm253Lm7t0_eAi&InH^bgLvfB9IQW=9bXi3GfB!;>3yRYN(Oh?Gc0` zhEVxhb1lxAU^V@kZz^ngNF94P8P!~gJ=6 zwZ#gJv#t@X+i8@{BQI~shRib7(P#bk1{@JSnv4JDHU6Dkz9pCheEt1%ScE-0O5Kln z=@8A@jwYJ{I?abpvWb>b`nhAjZS&fudcZh=q8rki6QU1jIch=}FOMV5cR2Rcpp?MG z0?x2C1*F$O)s9r*N~MIiHd+1-UKQ_Owrx_bs8$b1S^|#65DAo#Z*X~`k!yjR0|zZ_ zP>vd)&H(U}5o&a)cY7JgOT18~$zq9(HAL*0;Ui**f%5x^$VqG-9F{%Vb}1Trat}gi z4+oJ(^CO=a0d{GzPIO_}bLOdU)=292SHOoEC-rNzG}k6)6QXD?c1+QV)LO?K7b)}1 z6$a|l@&Zi|yg@`B-B{>y1O&3JQl^tc?zcE}3tJuYxiNYHQ1b4S!gChaWNuB=WQ9jC zSn+*W2D`lVgk^Kc8p!Y~ za@}Nr*R8G{`E$%d_8aVJpQ0W&5=xXpzJQ%AZtCv9=<4-7osrok-t*Z(7o68`g%-j< ztW@C*>#w0q&qvOig;y|ME6$o&-^~AAGPhA622xk|$i#bMn7R5v9tMY0T6m^82K%T; zqv>*Es6KK>^lGwRgs_CzF)T$~7;JS>r#E5Q_IvMY(_ip<*B=N^ z>*Snd=xm;IKs!N7yI4$V)Gq6!^-l71j{Z0A zS3~e80w`E-_LX^)F9nYU+_9~b25(+bIWP;IqXh^Lv|wRs>izjzCiKl1-*}EmHBq_6 zi1F7@`amx_kp<~}#Z1$kB8dbCL3bjiPyM2XpnM?eSCKcyfX*@+EF5QWC(Qm7cyaWg z`pE@JP$0_H?G)y-#JVqqt`0Ubxm2h)eubxG;6w+ep=+)wE8_0K@( z@o3D{D?nkSin3md0&DlFFIE*ZY5yY{k+w`N`hemo>0$*Y@)})+y^BYx>JG?&=$808 z;W~S<(;WC`)>{2?z$ZYi*W$YCjpvz1p>oUrv8`O*_Riw!$bgMYz*b^C-*tjs>V|*J zkcEbH&G3yt@rG_%-x{iZ7HKl<2iC$$GS%$~%&BG?zk}f|K;jf*)cTsRk}FN$7j$_t zrQVku*X581Iv#oSFW21q`JODNRGZjY7|e1edBMFW)1^f( z2?Tq)C4Y!P9%7rg~&G4Y)LRFQ48WW^W!QXYe= z>8~(W_SOL8ZB=PuY-N@-gj_27ewoB-0t_mP!JK4@I>|O}buh7ZS&vatJ|bm^xcbi3 z;A4cMaON8M4#{rtL478Fdp_CE$Wp`TN;UJ4bFpG45wo{(OJ%&w^5cha8-WvQu|}sG z!NVY{gv_2gYv-f1`+&DxfXp`V&{IRK2I#N*ULZ4xEyp&4!O}{OO#GgcZf*Cd?ZflO zL*L1n{KgCQlpp!a`^%bGaX(?5ZUi7+b*G@;kDk{<@q}-M^42DNA8#$W z7H+`m`_%78+ha!YI!9whvnIeK#(L8xU<>_atccY{@}J*M{^hsYMMdeQvpLeJ0?_u;nia+q=e7p5s1 zmoy7ft|RJ{YiSYwZkPZwUSc`%6_6!UqWp8_!T%^}@~HN#SxR)1HQ|O+?@u51$3m1X zxwsqFXs4s`uRCWf(qtYQTg#k52D5V*;~%i-N5`mkWB1NU7-Z52k+)R!Te4x~%a>c5 z5OCFP!JgaV+d0o$(cZx*x{K(~>Q^W1_dk5!%Bw36^GM+1S^>np$b2enro~ z%Qq`qeNJ70T8qQXCJtVLZS%6Dfe}tDjU3P_GTch^pVZcBDLHsEia~a=}Zg!$c*Me>lqkbSZZK0 z*4^FD=ZmL9Y)E0?i_pKLe!xD+oUZV@=(bV`K`5K%Q{wyozhJ??Or?zTwlMAiHX{Yuyp+6-2+2+2j@64pw;H?Z~vpj|l7@lcI241#Ul zDzwPY`>(5V+_7O$`JZ~)`tCxyfOTsw#B}_~ypsjEbA=V`DHqr&suw!v+ElU>v7&^u z!_*G`ce|} zb)MbfE;y?{bm z4QkKVgU&s46#M_SM))1FzAr*nYOlgD+p# zOq;A{-XX*z$bwPP_R+y!p*fO@NX6f}!ll0SV z1bB19rX~8)#>GSEalkrWCQ_kWrx-oH3tgX|>)~|_JI!?vlfjDtLn0H>Alpigx%%a! zvN9LXA`2U}JT0eCzzxBn=L_h746A;iV|H0CBRqN0j7xCq`iak*Roqlb;xDYO&ulOE z!%yJK>YzpOMG6_SG_ma@Qlr-b9@x#}N<7v(2XDja@_TxjuYX1DpbtTo600TGI|6g1 z8dbgW_OKhOqMZZHjt}!oJ9d~>^piH+r&~DN914q0L$}u)B@H>_AAR=Gb=f8rS~4In zcS{2w={MQ#h0---ys!!}1oRu48|Fhc7`_3hE;!%cQ67M{>xQ{(uI|_E-+?Cc)Pz4+ zG5E4+E=4tb$TP8Q$A2RekKa0L<>i%iA~7)724;poak)wy5)d&k_cV813*Cc9y#tq&+lsnS-M8#UUs=iscn_?rTRvo0e zRSLU$%=KWKI*baq^h%3S=AW0t!D2fppb8r#Q+bh7z0j+~D@$5IlW0<%R2QBt5Y4-* z8JU&Wual%}a!S@rA@=}L)+!|gPmvVh;nf|0Z1DQr-}q;p2W({sK-`H>TV$qFbjl-3 zW@ojawVCEjn#!IYQsVjM&nTW}>;)&mdeg^kb&uZzcMUJ$7Cj(ks~WH;8KqF|a^o|Q z@u6iuo|t;P8(3}KvB(7ziH9@u82#C3b~tTP^ismt-v37aF^DKW|~+ zX8L0r-;{^5&J#I0Nhu>BH`#a*Jyi!ao{o?}{Tq)dFP?~Is8m}@De_=hy4qbGF7Y_4 z&=4!oNA39YLM$T1p>aEe7M4B_hy}a8l z@h8sku^U;Gp^mPOo{)*rS_B_fPsJB&4DE4e^n&8aXAZ-sV54D%v7K$lG0%kWx%Eo2 zhNyhAC4qqog)r$(3bmQM6^c}_VljK)kF*xJz$jNa)U>=?rL!{Y9=I1M>+w|6a}k?` z&!aWGs3Z1{mVT|B%|iN6Tk7?60+e+hj-TdK#H}z{eKV1~uEvb@V=`D1D93#BQQUnv znzra6-)Q)qB8nTZW7@`iWuiA%4e3XO^nKh*>+}LRPusKws`*51t8L+Jnc{^q7}6bO zuH@X7q(OFGnDp!n*n#7!M#;S-8jN-ojI#Iyq7ES=Awy4=VGimBrptdryy@z*-;cV2 z_-)Mwd|PrP4-^w!c+H;1WyLA5!$O^uE&e(H-9qIfM8~e+MR=clC&74ecu8Asf2CW5 z+Zx_y>vneEkm+5|w4WlC98vMfb={JUX+*nC%#8d|U=LHX&5xxu<(YJpI-9+z)V|;t zw%b@9Hn9&YMKNC9#akDmgpjGXDWhB(;fr`aq-shvoQ8OO>>ig9uC?UTCYB-AA%8h1 zr+cdE%y1>#O~0yH=_&ZFVMTis1H7_>S?0toBDM2VdA5#ds0#uSl}O?P3n482^#B(96b6ab^^Jv2E8L%+!iqWxC zJ;{T2of58X7hLahUGwV|uoco_TWi2V71tTu_@V>BX9=z{8z}52Tkz|0-wAsBs9?7#*QvZ#Vpht{)*)RM z>XQ_Nux#Iz*=N#OI#jru_T-w}$G~Hud~H#Tqr|IWTGrc+=%#zj%cE z>^OUR4b`$XM8GB%kOSs&xN%R|sPhD$w=d~U-w~9th3VdZ;x66xthXVWp~HCyLJp4GR$!|e_K2}_H=fq2Mvx=posX!Du{BL%6~dj>Q`mHaEQJw{euav7aCVk$7u1lSPBX zsaMtWjN{tN{LFQ+4R{8E5Y7)0DuNW2?=bvBc;~~`fYRhHekPK!ov&>{>PwK(k*Ubf zMGG@By0(hTxFLlzJ9#hfN3txz+C>h89)f?8Sp6r5DYh0N&kot^+n|aph1N&iDkC4z zMn^||`qrFj3vF)gKSfeFLP1zoc8>V>q&{l;3QoYPZoC$o2u{&sdKtQA?k0Q>NG9M> zRW{~+AL*)`?_^1=L>*9qPyV+;RWT_x zPR}W9>EkedR3+P61H+nFnbh1~E9?VpXi^}s6;_PRfQwI^x_569Z1>S2k~p=Nq-G?K zV8xprY`rvUjFEg#{#}chWWP!W*Zj_zZ zLGeKf?Ard*D5F=|v*i7gRn<8Oq=fNf12mns@Wa=52{*4jVTBf9zJWTbi$3$XyV2f1 z=aF~O+B~B~F0h*TH;Yb#bMGWK)HoE>oy}MuLx>S+FW2qw_>XDkSb9y7(9H`abBHNPtGyd*7=qN>mXp$ElztpuL z!@$N}tj!reWmy%HO>H;-( zVz-IcsL=U!$5KWN3!7 z{4j_^nl^WBx9^jAvy;hn0p}K~f-`ujrTuaWWi+;Y*qYnA$D9}IB|Ls1U0^!PK}eGS zB;wGHrS#(IuPGB)g1B?x3q-&64JUMNajF2DzUPK&N|NhE?Bxgz-alKVpF20C3e_|| zWVEadt+?=E-y{HVZAwX)c7 zt?Xlvy(j$euEIX@+;Rhyw~w&;Ub}gk1-_bO`mI8$fGIq<)~=v12L{P`NN!ar7dV1NB!mKXT2sc@8W9&U@Di3qA>|uX`X?mJCvl5Q<9$+I&y9=r^M7VrR?Hyt}(a+^z!%hKj;IwoDTB!_9ngxZf}?6Pq!=YyB(n$dkW zHbU#wa_cNN`QM2FM27?dWwn5uyLT^Rd%JoXD<0}#gZYeqGotj3%O}En8^V0aberk# zaU<@I0_De0=%Y)_Z(GIcd@h7cUvevzzPij|l19uz%3mg(_Gq71(x0(BMh>lV-dI_W z%y~-QsQHD`Sn+Q>MS*c5Btm&)%N#{?6RLc#0KVyJ9l2}FlkxR z^e|UVAWebEKmUc<53m`b?N$&cWSdt~_QpEwB5PHdg5-Gx(#>+J@$|QK$GeKJz1bjJ zeGM8}X5#U@fk;Y$SzaJ8lc(g%cqZ8&jxx%Gp#fIt?~TRN0*Fl_SsdnV7cq<^cMFtY z{;m+&){EB_{-}i)HlTUH$52PSO-U5B7u|}Gyr(o0_29|0_@3*0sqas_%UW**uJhC6 zWrd7z-3Ro+owJ$TcrQN}D6z1``O{Qjz|I_*Jc1)MGd>>9rWhCkR~s`&c1Yi!S>$Tg z6kt@Ao+J=61o6AzT+*%Sg%4a+xSNqta{bEVzfr=ZlR>4_)WGOFkKHc);QMeiCBS0p zp7UdN#mY78a%eex5tq`xtBb_r1*GwUqh&mqU|~b#5nOJgqSV$wKRcd8)?*KNKyBuo zlz=Bpys#wPhQ{d(bx8!blw$gMcw~W9BDR#8E|{~7Eo_97_6E$7jCs&v6$C@**+26` z{4ZQ8;bp>IgB1a!KzpPOpcWX1U_W)dR_}*La4GX%DpYFDBR=03c>m{-?RJ^cQ+ku3 z`-+MoW}-!VlH{&}S9^3mOy^u^@*~;ZiH1JmU5ZkGmF&2bG}0!hYpAv}7*yL3NhZqBiYJQ~Y#ZC$#^;vu=rfuubttyRh%pAITGypI6tV2!ZD zaC4dMg(!XRQM;;S#>V_){k-yE;(U#!$+gA{arVSN23OotyQAQTV<}4?q8!R8V8uKs zy?&eKZp=Nl557lLRqE7m)0f9jDU4S9iel!o}LV9cd423I=Z2=D9(* z33bQVk#nB6+1TCzXp$xu;%+#n-3Ex7ISAI~2-H;XtKe7Ly+w}R-l}As6|mb}l{bw7 zfg+6=2iB-gW^F(ow*{LRi1+0B@4U-fY-w$B)fJqAD}@T^JY^4jkA}=Y7T$SkU6Aba zc^B7xxm!3R>F=QZ zAMKrGP+R}nu7gubkpe9gcX!u7DZw3r1d0TAr;tAoq?A&ELkq=+H7FfxS z`$b;3h?sb>4vu}42cSq4iplp-pcvoXhM1eKK*>lDSqijelfattj-9v%xz!EjOT&+M zr|60K(w%dfp{fYB-;0E{v^+uV?&C3UQ;OG*xncJFgfQb6vZgHHi)!N*3;DlSFmyVQ zn#oaN47ZWq8HP7uFlkuo6A@SxB~8LzNtuO?P;3jk!}Ie}asR{>2}c3UUjPqo*e%!a z!yELK(b0oQh^(a&=H4Ws!b^YxP>fXTg0=zFfduH_) zzdla8yO9LV{G!N!dcM8*)i~|u9YOCaO;Fj!S(mfkpw>;RYep4e(R918U6A_;&7enK z_OT$C$VPV`Rl-bmIl3b?00kz@TBn$kCgW3$F}=|tb?^to+v+E zs`aI7Ur+b!_%z^JZj?weFeLP546l+a&@cPVY$K_et4z?=rReNu2=#76yMbG!aFd7j zIu|Rq>?;p(u?G`Ww$J6SQen0&lvMOCAG10_DYq_;3*yA*ogM6ic*we}+^C#Vgf{*G znAXKaS{r`QXg$r@A0ZDGCI3+0h7h5lna%NeJ|eP3>V_!nM}fyyE4jsH!Rqss`NQPC zf7w?OIZ-0TN3@(wBg*mgz1Mm99!P1IlgIEFJi9I>DKvh^&S^HDQ}0zH z86i{T&dCO5CTrf(;I;-~wV&shTs<@phXyVU{z%I6Y}RkJqd7!!Iy2ukgzpakVY5H? z*#EEd;`fTs$4}qt2+jj=UOVP}e>C2}E#?DCi7wXNmB{6WeC2Tz=XYoOJw~OaJk`eE zo!*d*v)P#bDbb`77yIb^EncPa>!5%t{;(DK&Q5hndVe=(KQ^Sx=ff&{CKb?@N%9vg zy2#ox99B!Qi_JLo8TMyAqm4#HZ>fQdU6dD{q7%Q%bn0jLc#g}rOsrrYLp{rJcoU~n z&AifDGrowI+=k6z3;7a+-h019oo;*lAD(r@qO@s|UOD~*Vn>6_GUx&Z@#0qKnE5Prdi0^Ss{0S%0zEKKt8i-tmi#z+PI^`a?~Kz;-}cG#OTt1rlxq_m z2=7k}@AQ^VTDreRWL(~HyESj7`&s_Cboqa4VkC{eev3Pi{{w)JpB2-Z3VsAz6jy$) z^);7L-vY&I2%Xl)(VNa_H_`NuGFxJ5TXG(`N-_Gq?Ixy}%QrBvs6#poU&m9cP%k{%Qr0*RmsVla=O)9# zWy-9G-60_)`>gWyH(H|e(TF9<>-fK#k2~(2#)3Mx_D9?9`||aasqbR(xnTB9S7lP| z8MFR4liKk+TXOHFY8LCjiCEB{6KBFRb(!Kss1xqSzPF)_dH}Y+7YkpF$osMB_*Q6o z&$n!!=xRZhz`$EWM@CD6CdP4ZO~sXMDCK+?s*`SpXieMM3)f&1DO!W+u$gpRC|U7X zg{uFA$P)!NZJ35|dtyteB#q*(4L*KxLAPBf4{lHK4-mY(?O&bBHC${e1aeyu`rZ~=#iq2geIk*%gezb-}3znYFFaQ@u;{_!I zA^8+E8=@k|3>wwcaSEE1aFgZ!HnGx9h}~64+W=kixl|+pBxyqLE0GM6{Y!~>bj;U6 zY!@M}XMk9zmm3*|peEdnVP+Z|deeNkP3$8M`~fPanq$Uva-r`2VRoK2k07UIl=fE? z$YZdpzd(_E4&p^Y1-V^5P|6`;(PqLtZ`NPXrr?4s@v|}YFBhu^Ona(CJ{rUF;CwKg z-#M&QZWK9Y8gi=MJ4U#_=;C*L9uv&eJY15szOS0^I?ntIy06Vvcar#q`RtfC|LrGC z>}Zg3m1x@La|x~X6dhG3+^fi<=#_1;Om$GtHi2ZPByhGRQ)OR{$Amw(MunFbYF!@q zl_nu;CpHr&pgo%dB(Ly<>N@n-XdozjoqZa)XqLqKxdcG};|2yZlC_!MhQ#zvo8%&o zXeDgZ@bCJ3+Qt3=;zxiN#~n9hM#A|hCHHk%iDfHSjUMkC@t-?O68FyTea}Oj=j{5I zqwwFY&0Vn7CSFZY@+Ndvtm5xo8ZCY>Th)2h&V=bCr|yeuX$#W5>^T1xk%hvUGDkoSsC#cg&%(yiO9nJ=MlyC!haL z0z+04HUx2g<$4rBrq*k7hBqgQUB(>F^Db$zP|L>qMB8?yK-tsqD>efqR&{siY^f4Q z;$bsw>;z2jqkpKSjt3;^NF$Y>qpZlSZVD69Z`;V5Ve>-|w6reY`cv{kWO$aWRq)HT z=eNAKqvc%|$+E-+&nEEobji=ZYl%zloEGkru5%R7dMN;hJ`S1KXJnsbkX{rHC04Y6 zf*smIT8xmmc_^j$Ar}2E3T#^_PecmU4}m@KWH?9HsA{rjg8`vz-*ZwE$hYiX{j3LP zhclt!tT%h$?`f`rPo8vrkX1GdF&#nbsjF_*JLZ0Lyge1$Vm^4#Bp98y)qHsa>DN4~ z&9%2=5B)`3Jbl#HyN`&c3fnaFi#Z#TpbaWh)-nr_*f$H2n%u3!s%rYlTOLg9DvzoY zXK_QYcwq5wWn5MI`s}@gH+q%yOR!U*-nc7N&-4uXdvgy{ZIgKRL$mUO4ae2OB=ePa z%jGMLN0-Y&+3y(U=1lP}qn(OR4}~c62_?cnBPgUqc&_P^qU7k40F}CYe`Xg)-4`oj+Yr*Jh=fV|qk)gl7ye961DuHk)(-uK z(H(DyYvHmQA8l7Q)(HC+y~TKi*HB`ur=@?g9wt6EP54B#X(yU7`j^@;f{*DjVz0Q% zYDweC4Jqqud6AA=*oe>ANlDssR?Dt5d8J~+6SFwZl!fpkhtj~HGxN@7OI9qxj@ie<`VhF9d>%MeZ zs2QZHgi|hmWX8io=hOS&)b z;fe&qGosRn*E^$W@cAZrJj}#nJHIpL&7?74ib?DkYZNyJ6gB&vwMzF>yQ09bxF{Jj z<0cr@pn|I#b#tymO_lbDfGVgI2_Gy8o;Z9@-9oN$g#EMmAlk%Auvw4b>;4n@9eZqV z!=`78KCkd912IgW!2>iIX=R-sQ9B>YDmm}XUG6454i967QesJG(-6B`bQh{?0=^KR zzNmh_TFiJ`bfCXjqw3ix*^m1fGZ&Ghk`V z|Fsc=eEUqH{)T?4umhgk@6q<#eNQJm9=`~-kK;ngBG^RNKU)A^^uCU6-elcs;apeR z(k{^H>0^wv<=Uvuy}|@d_YT-LNSlQaLk-1+Bx7yS2m}RV3(Pql%S!F?qWIzodTDR{ zc1q@z)-2X`*HO0C?<-MNLGta)+_cMTuAVcw8I8n#QMn%WA*m^dQd;cV6%01}L7s2# z#;h^TcmR)Pr&$(9J0S1%@4#c0)meVSax`hj&k?aD*TsHP+vpl}J4N02dCg5A3s?y&^A2{f` zmQrREJAD9rO!(LX_`yPC<5Sxr6pX{PIxf)oWSw z)FHiIN9^(&ypQfmw3_}mv1zU)-ONq;XZN%EVf7ZEN$3>V44B69?qq}Vb$2pb9=(7_ z9)jsTGvZ|~pcK!N6RB$q{|EB-f7xQZzTuY}a=1+yxQSKrGF+T>zoac(S4~RFuz_A1 zc7stw-qHbMg1TIz{$k`Zu`nVn?i{3#r7OG^;g6L%Op{&YRGaUJmMJw1$E~Uk!(fKz zWo978iK@S{1(o#DD<=lUg~)dfz4lM%vCE!iJ_qq~)cab)}8)WtVA z(9{6s<9BAnCqdk4%_f_MBm*Q@l70FQBTXq?0eOQGJR@2_wGW@4dZ~OVkH^!!|d?% ztBkwc^WvCTHETFoh_rPF?;O8rW{9T)Oa$_!N&WzqMaSySDK$VQGPN`QG{J`8ZO6ok zh~(1n%;xn1g~d5iWpIj-PBLaCX#-{(HSK|9UP|nG{&t;ZU7~R#<`?6uqWa`$Nt?S- zAGXwR!Jz~ssVc;|aFhjB^Vu7&71CXM%o`(o?FL|;N>8*j5E3Gclq^~@3!r2m+>snr zV8j`dB1&E?!=FLAR~)wwO>%SRze)0-$?QEn3ggPSVqiF1+GZ;=a0_-XAmAk2<=Lsr z2uj_zePwABaMX{^+9J|)D4^3*rO(()^1pyOwV$j-s!~4HI*f(st?qkSTuV<<Oq>XQUL;Vt&vftsr3PkBVoP8=XM|tY$j{#%a%-E-1<#^|1=PIHu|g5Q z_kPjMj5n{>M!TL1P@<)@YS4>M;ox)-sL2t^ubWwx|Ion2L zg&p*5tbuhH39us#SE{KW{%?Xe_kYuVqz!pPR(SUUC40g7`v<=Ll^^Kn-Sf?K_ ztFKl#LadQnHvduV*|sX!=xoj>+KUpM!BPzUBS82MwtcDMeoDSP9dEFv;PfUpsoBn+ zgh?tbwB!vJ2mMtQ4q&@BU{b-io$dGt`Vll@;o0^JD}mM#IV2ptPjVw1?Y3_g4Op1G zaIX~z^SdA1T}8*KMxwL?+#};oLy6)N56`YHE8Qv<_Klut-Zl>TgEkE$T2>g0nx?CG zRnJ{f7Y5ot-G9AG zPW>tB_`sy$l(EC-`B~^>a}~pv%Dus>kSGyyy{9s^w24&qbHsj9qCp$5I}V z5{5R`MKxhV3%qA%*_58Z6S6#{XS#!r=El~jQytAs@Fd#+OL6u@xV6M}=D zIidU`p6E24(MZs)oJG}a6AaJz_~mH)d^ef7Ts=;N_z_)}aqj&(ggaZ|$aoHHw}ly+ zhPMoyD$M$TB&zNC7W2X!anyT+EPh367V{G*Xm+tY^6{e=O3mv`Y~$$2q|E2%qns98 z>8KikADanF2j=hIiNL5`>6Y|o>`+rr4ydyPvn2LWmdHVl*V@I;T6qhCGN}!|b@NSc zxou#z81b)ry9QMC1VCM@ABQq?-JcP!FxR^s1Rgl?ZYm_#zg~Wv9pdiP10Ca3kUkfn z)(Eb!T^AE$}{sE$0l;<=JvfLdrBD)vT!dTUMM4MLg=uu|RE~;%dG0GZ|Rq}n_P!`~HN@HJ|t!Ng`&75KtiH8lIs0Cxy zhOKjd>P?Wa1r~Teux0!F>?B^+>qigQFTmEdoI}-k^GHuc)~)s4jFt?W=@_14CIart z#tE18cP=H{&HKFx*ZQnKw?1#?sZw1^(pLZ-NhH;{okE_ViqNh^V#Txcv8?XnRW?Wf zInnQdyFR@{7>1F|dS;StcAIu7X2EEAqtH3ud`FA^{QP{Efb%EQHR%%Tj-mYA4=u(- z$&ON>0;earEIh4l2oGhXe|cHllhs$+$xm$$4bZ~U5+#v%6*_hst7R>2Eniy93FLTXEDXV3AH_Dn%)s`S<4O)oO>&%%TC2GwITlvsQECXVR^D9n&*)vXQ=`F(D+rJffthcdm!Mlk)uh?PJltpr z^d!V-$ydouGRJP;khj{2c{G07#q|%VMX!?(X0}vimsl@nS71}Qi&QM^8C<_^Zj8+H zeh14+JYH6q9?^Q4bXL?XJIa#98OgHXXb+ciW5D=L(A9=?^L-?aQjM45SW}(=yQiDF zz>78lT&gap1Ij0^E0Pm6EP)!HM+d+1?d=TpdLwkok|xl&dL_lcP0j-ly`6 z5){;!fiFBIMRxN73RLpU$2Pe_QR$Xaj~@>gXZ~nRG|-~V8RM#DW~T7v>M3T%Z3Ak2 z)s*U%_UU4$h(!f#p8G;dq8~%dNF6bl{u;zX5_)QDJidY&J zX14Np8-y+l9zYEAcm6g-R>2{MM^a&v9;wUIfa(A<#{f#|cV`sJo)=|{t9Oxna(UMT zU(9tE@Xfy?z9&ZKJomiF^DJysQ!p(Bl<4wBFPVb(a~ z?%EXVw{$uF@KhSz*X}rp?niMJ><8%Y;?_NIwue1SXssdld1Tg}?ehA^Ycr;f z(I#fXNV)t#@cZjXV&;|T;sS~sYUlM2Gk^dO(uX}Xj}#d~>O|&I!=w0+R`q+k=6#<) zJ)g=Y#+AON7h+rEaFc}Uy^Eo`x~DThBdkA_xlIFKq#ni%(5I^X_2n~A%@vEfDSQ;C zMxtVr_@%2-?{rv0Y4F?eoXe}S7ugY8AAA}JZME7V^q+!=1GC-~SxTnw%P<$dOE}ql zO|}^)*EasE(snY>E4ILxh6IFPr8q&yKFQmf12A0dxvxYwYk&w%BZpnw6np0vY16!5 zrKlFkMPkdK15i#|UF_wQdVrL~j~lx0%_&vIbTDskUeuAun8{L`PZniOYh$WveoX#T zJ^|EI7)@|Uro-MMML{GzolDPel+O*(Oo{1OuG0W|qEY@IX(IpIF~V$u%$mn%!ZD9j z-^jVF2&@XQr#Qrw!~=Hsl%{(+%F1u^_;Qw8gnttXwqd_Jhm=(=P+mRr2>E8PbH|<8 zQkOdBO`f+4BR$Xor~(L+AQB}`3`n-;fiX3?@wAZ`2h$Yw_G`uIIIyC4m4L;3!r+vT zwpfMD*m``FD*53{ZGLq{Y~}GIp=-k!i~1LV7QdCzx}jtArESn`Jw>)mOK1|>N4cGS zzW_Y>wz2d6Z$M=GnV|k}qdcSM>(U|YV0)~s6vYQwVzjb!u+a`zOOxEN%vJ2{1ee<1 z7T62I+jh1)WU3H-7r2}Av;1h}JRdp<+A#I_?XDg3*Zx_CuU-FHu`d&Ry@$|S1u|9c z^i10d#@e#8C8@tJ{i4j2SDvG&hp_Q!)EXjlTuC>5&*$WEC_0&DuSzbu&i+}Foa~SF z3F4R~o-UD@r`_7=QBTS;SCp^YPx7AGFow_-E5?Yv%7Ja9ex@|ZEx?w(3(Z=WAPkm2 zjRiOO@RHVZ{$yP9#CE*>tBavjJ?auK1?mWd9DZyl%t5ci@AVL~s^F`sj zGM>~W=PK?5c;HAy_s{uqXG$`iQ3$dr*im&+jHN8YtJaHMO$W^CH={!Uq zBisMU65!uG2D5D|!|3a$P5NW>V94m})R&s;@e|gEQ(C!{FG|ua#<rw}_vRGLIWvZi}aBfVpuC;+wI&iFWl2$}!fNWQm*L&N6#Y;TEu6VggC`R-J2a zUpc2)QQe@;zm=dV)UtUWKw_oKd(Yfjy zh4(3bU!~21o_I;w1iKZvMA!tt6AmRS*D0AAqm&4pqNKQD2udqluv4^kr#Fxlvz(O0 zU=;p=5l4B_SE|$-+-zLpq-}u{#__$WIz17D-}mSbyLp(IN>3WmeA|ESYx{Te)W7db zNHJIYN>~(zf1s`eu(o&;EGcyP4jSU|3#hHuSm4;*P1ry5 zt6RWSp<6A~&1EHDkk}=rM(P=~1*0k0x+RcnLrzEL@*6F+YS?=3urGqnFF0TgpICDgZd-D$qNSryzyOM}08_p}AZFQ?y2=%GcICu(Xl% zguqMA8m+r{=$a1MS^5KD68}AmAfI+10N-dL%7;Y+ToVFmM?A07Ta?JfR{q~f&i{me V=J@~b5&mDv+y9Wy0{Ca)e*sFkE8PG9 literal 0 HcmV?d00001 diff --git a/webapps/examples/jsp/jsp2/jspx/textRotate.jspx b/webapps/examples/jsp/jsp2/jspx/textRotate.jspx new file mode 100644 index 0000000..c543887 --- /dev/null +++ b/webapps/examples/jsp/jsp2/jspx/textRotate.jspx @@ -0,0 +1,53 @@ + + + + + + JSP 2.0 JSPX + + + + + JSP 2.0 XML Syntax (.jspx) Demo + + Try changing the name parameter! + + + + <g opacity="0.95" transform="scale(1.05) rotate(15)"> + + ${name} + + + </g> + + ${name} + + + diff --git a/webapps/examples/jsp/jsp2/misc/coda.jspf b/webapps/examples/jsp/jsp2/misc/coda.jspf new file mode 100644 index 0000000..d767de5 --- /dev/null +++ b/webapps/examples/jsp/jsp2/misc/coda.jspf @@ -0,0 +1,21 @@ + +
    +
    +This banner included with <include-coda> +
    +
    diff --git a/webapps/examples/jsp/jsp2/misc/config.html b/webapps/examples/jsp/jsp2/misc/config.html new file mode 100644 index 0000000..707d68f --- /dev/null +++ b/webapps/examples/jsp/jsp2/misc/config.html @@ -0,0 +1,35 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for config.jsp +

    +

    Source Code for prelude.jspf +

    +

    Source Code for coda.jspf +

    + + + diff --git a/webapps/examples/jsp/jsp2/misc/config.jsp b/webapps/examples/jsp/jsp2/misc/config.jsp new file mode 100644 index 0000000..0372889 --- /dev/null +++ b/webapps/examples/jsp/jsp2/misc/config.jsp @@ -0,0 +1,32 @@ +<%-- + 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. +--%> +<%@ taglib prefix="my" uri="http://tomcat.apache.org/jsp2-example-taglib"%> +

    JSP 2.0 Examples - JSP Configuration

    +
    +

    Using a <jsp-property-group> element in the web.xml + deployment descriptor, this JSP page has been configured in the + following ways:

    +
      +
    • Uses <include-prelude> to include the top banner.
    • +
    • Uses <include-coda> to include the bottom banner.
    • +
    • Uses <scripting-invalid> true to disable + <% scripting %> elements
    • +
    • Uses <el-ignored> true to disable ${EL} elements
    • +
    • Uses <page-encoding> ISO-8859-1 to set the page encoding (though this is the default anyway)
    • +
    + There are various other configuration options that can be used. + diff --git a/webapps/examples/jsp/jsp2/misc/dynamicattrs.html b/webapps/examples/jsp/jsp2/misc/dynamicattrs.html new file mode 100644 index 0000000..4fa1bf1 --- /dev/null +++ b/webapps/examples/jsp/jsp2/misc/dynamicattrs.html @@ -0,0 +1,33 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for dynamicattrs.jsp +

    +

    Source Code for EchoAttributesTag.java +

    + + + diff --git a/webapps/examples/jsp/jsp2/misc/dynamicattrs.jsp b/webapps/examples/jsp/jsp2/misc/dynamicattrs.jsp new file mode 100644 index 0000000..251c49d --- /dev/null +++ b/webapps/examples/jsp/jsp2/misc/dynamicattrs.jsp @@ -0,0 +1,44 @@ +<%-- + 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. +--%> +<%@ taglib prefix="my" uri="http://tomcat.apache.org/jsp2-example-taglib"%> + + + JSP 2.0 Examples - Dynamic Attributes + + +

    JSP 2.0 Examples - Dynamic Attributes

    +
    +

    This JSP page invokes a custom tag that accepts a dynamic set + of attributes. The tag echoes the name and value of all attributes + passed to it.

    +
    +

    Invocation 1 (six attributes)

    +
      + +
    +

    Invocation 2 (zero attributes)

    +
      + +
    +

    Invocation 3 (three attributes)

    +
      + +
    + + diff --git a/webapps/examples/jsp/jsp2/misc/prelude.jspf b/webapps/examples/jsp/jsp2/misc/prelude.jspf new file mode 100644 index 0000000..05f7c84 --- /dev/null +++ b/webapps/examples/jsp/jsp2/misc/prelude.jspf @@ -0,0 +1,21 @@ + +
    +
    +This banner included with <include-prelude> +
    +
    diff --git a/webapps/examples/jsp/jsp2/simpletag/book.html b/webapps/examples/jsp/jsp2/simpletag/book.html new file mode 100644 index 0000000..2841acf --- /dev/null +++ b/webapps/examples/jsp/jsp2/simpletag/book.html @@ -0,0 +1,37 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for the Book Example JSP +

    +

    Source Code for the FindBook SimpleTag Handler +

    +

    Source Code for BookBean +

    +

    Source Code for the EL Functions +

    + + + diff --git a/webapps/examples/jsp/jsp2/simpletag/book.jsp b/webapps/examples/jsp/jsp2/simpletag/book.jsp new file mode 100644 index 0000000..4cce650 --- /dev/null +++ b/webapps/examples/jsp/jsp2/simpletag/book.jsp @@ -0,0 +1,55 @@ +<%-- + 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. +--%> +<%@ taglib prefix="my" uri="/WEB-INF/jsp/jsp2-example-taglib.tld" %> + + + JSP 2.0 Examples - Book SimpleTag Handler + + +

    JSP 2.0 Examples - Book SimpleTag Handler

    +
    +

    Illustrates a semi-realistic use of SimpleTag and the Expression + Language. First, a <my:findBook> tag is invoked to populate + the page context with a BookBean. Then, the books fields are printed + in all caps.

    +
    + Result:
    + + + + + + + + + + + + + + + + + + + + + + +
    FieldValueCapitalized
    Title${book.title}${my:caps(book.title)}
    Author${book.author}${my:caps(book.author)}
    ISBN${book.isbn}${my:caps(book.isbn)}
    + + diff --git a/webapps/examples/jsp/jsp2/simpletag/hello.html b/webapps/examples/jsp/jsp2/simpletag/hello.html new file mode 100644 index 0000000..20cadf8 --- /dev/null +++ b/webapps/examples/jsp/jsp2/simpletag/hello.html @@ -0,0 +1,33 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for the Hello World Tag Example JSP +

    +

    Source Code for the Hello World SimpleTag Handler +

    + + + diff --git a/webapps/examples/jsp/jsp2/simpletag/hello.jsp b/webapps/examples/jsp/jsp2/simpletag/hello.jsp new file mode 100644 index 0000000..408c216 --- /dev/null +++ b/webapps/examples/jsp/jsp2/simpletag/hello.jsp @@ -0,0 +1,31 @@ +<%-- + 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. +--%> +<%@ taglib prefix="mytag" uri="/WEB-INF/jsp/jsp2-example-taglib.tld" %> + + + JSP 2.0 Examples - Hello World SimpleTag Handler + + +

    JSP 2.0 Examples - Hello World SimpleTag Handler

    +
    +

    This tag handler simply echos "Hello, World!" It's an example of + a very basic SimpleTag handler with no body.

    +
    + Result: + + + diff --git a/webapps/examples/jsp/jsp2/simpletag/repeat.html b/webapps/examples/jsp/jsp2/simpletag/repeat.html new file mode 100644 index 0000000..a56bfcd --- /dev/null +++ b/webapps/examples/jsp/jsp2/simpletag/repeat.html @@ -0,0 +1,33 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for the Repeat Tag Example JSP +

    +

    Source Code for the Repeat SimpleTag Handler +

    + + + diff --git a/webapps/examples/jsp/jsp2/simpletag/repeat.jsp b/webapps/examples/jsp/jsp2/simpletag/repeat.jsp new file mode 100644 index 0000000..234360f --- /dev/null +++ b/webapps/examples/jsp/jsp2/simpletag/repeat.jsp @@ -0,0 +1,39 @@ +<%-- + 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. +--%> +<%@ taglib prefix="mytag" uri="/WEB-INF/jsp/jsp2-example-taglib.tld" %> + + + JSP 2.0 Examples - Repeat SimpleTag Handler + + +

    JSP 2.0 Examples - Repeat SimpleTag Handler

    +
    +

    This tag handler accepts a "num" parameter and repeats the body of the + tag "num" times. It's a simple example, but the implementation of + such a tag in JSP 2.0 is substantially simpler than the equivalent + JSP 1.2-style classic tag handler.

    +

    The body of the tag is encapsulated in a "JSP Fragment" and passed + to the tag handler, which then executes it five times, inside a + for loop. The tag handler passes in the current invocation in a + scoped variable called count, which can be accessed using the EL.

    +
    + Result:
    + + Invocation ${count} of 5
    +
    + + diff --git a/webapps/examples/jsp/jsp2/tagfiles/hello.html b/webapps/examples/jsp/jsp2/tagfiles/hello.html new file mode 100644 index 0000000..f29a379 --- /dev/null +++ b/webapps/examples/jsp/jsp2/tagfiles/hello.html @@ -0,0 +1,33 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for hello.jsp +

    +

    Source Code for helloWorld.tag +

    + + + diff --git a/webapps/examples/jsp/jsp2/tagfiles/hello.jsp b/webapps/examples/jsp/jsp2/tagfiles/hello.jsp new file mode 100644 index 0000000..9b260f5 --- /dev/null +++ b/webapps/examples/jsp/jsp2/tagfiles/hello.jsp @@ -0,0 +1,35 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + JSP 2.0 Examples - Hello World Using a Tag File + + +

    JSP 2.0 Examples - Hello World Using a Tag File

    +
    +

    This JSP page invokes a custom tag that simply echos "Hello, World!" + The custom tag is generated from a tag file in the /WEB-INF/tags + directory.

    +

    Notice that we did not need to write a TLD for this tag. We just + created /WEB-INF/tags/helloWorld.tag, imported it using the taglib + directive, and used it!

    +
    + Result: + + + diff --git a/webapps/examples/jsp/jsp2/tagfiles/panel.html b/webapps/examples/jsp/jsp2/tagfiles/panel.html new file mode 100644 index 0000000..1f03b9c --- /dev/null +++ b/webapps/examples/jsp/jsp2/tagfiles/panel.html @@ -0,0 +1,33 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for panel.jsp +

    +

    Source Code for panel.tag +

    + + + diff --git a/webapps/examples/jsp/jsp2/tagfiles/panel.jsp b/webapps/examples/jsp/jsp2/tagfiles/panel.jsp new file mode 100644 index 0000000..d963877 --- /dev/null +++ b/webapps/examples/jsp/jsp2/tagfiles/panel.jsp @@ -0,0 +1,58 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + JSP 2.0 Examples - Panels using Tag Files + + +

    JSP 2.0 Examples - Panels using Tag Files

    +
    +

    This JSP page invokes a custom tag that draws a + panel around the contents of the tag body. Normally, such a tag + implementation would require a Java class with many println() statements, + outputting HTML. Instead, we can use a .tag file as a template, + and we don't need to write a single line of Java or even a TLD!

    +
    + + + + + + +
    + + First panel.
    +
    +
    + + Second panel.
    + Second panel.
    + Second panel.
    + Second panel.
    +
    +
    + + Third panel.
    + + A panel in a panel. + + Third panel.
    +
    +
    + + diff --git a/webapps/examples/jsp/jsp2/tagfiles/products.html b/webapps/examples/jsp/jsp2/tagfiles/products.html new file mode 100644 index 0000000..72ae49f --- /dev/null +++ b/webapps/examples/jsp/jsp2/tagfiles/products.html @@ -0,0 +1,33 @@ + + + +View Source Code + + + + +

    +

    + +

    Source Code for products.jsp +

    +

    Source Code for displayProducts.tag +

    + + + diff --git a/webapps/examples/jsp/jsp2/tagfiles/products.jsp b/webapps/examples/jsp/jsp2/tagfiles/products.jsp new file mode 100644 index 0000000..7f32ffb --- /dev/null +++ b/webapps/examples/jsp/jsp2/tagfiles/products.jsp @@ -0,0 +1,54 @@ +<%-- + 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. +--%> +<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> + + + JSP 2.0 Examples - Display Products Tag File + + +

    JSP 2.0 Examples - Display Products Tag File

    +
    +

    This JSP page invokes a tag file that displays a listing of + products. The custom tag accepts two fragments that enable + customization of appearance. One for when the product is on sale + and one for normal price.

    +

    The tag is invoked twice, using different styles

    +
    +

    Products

    + + + Item: ${name}
    + Price: ${price} +
    + + Item: ${name}
    + Was: ${origPrice}
    + Now: ${salePrice} +
    +
    +
    +

    Products (Same tag, alternate style)

    + + + ${name} @ ${price} ea. + + + ${name} @ ${salePrice} ea. (was: ${origPrice}) + + + + diff --git a/webapps/examples/jsp/jsptoserv/hello.jsp b/webapps/examples/jsp/jsptoserv/hello.jsp new file mode 100644 index 0000000..8b2a43f --- /dev/null +++ b/webapps/examples/jsp/jsptoserv/hello.jsp @@ -0,0 +1,26 @@ +<%-- + 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. +--%> + + + +

    +I have been invoked by +<% out.print (request.getAttribute("servletName").toString()); %> +Servlet. +

    + + \ No newline at end of file diff --git a/webapps/examples/jsp/jsptoserv/jsptoservlet.jsp b/webapps/examples/jsp/jsptoserv/jsptoservlet.jsp new file mode 100644 index 0000000..db68a6f --- /dev/null +++ b/webapps/examples/jsp/jsptoserv/jsptoservlet.jsp @@ -0,0 +1,23 @@ +<%-- + 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. +--%> + + + + + + + \ No newline at end of file diff --git a/webapps/examples/jsp/jsptoserv/jts.html b/webapps/examples/jsp/jsptoserv/jts.html new file mode 100644 index 0000000..a4e1679 --- /dev/null +++ b/webapps/examples/jsp/jsptoserv/jts.html @@ -0,0 +1,36 @@ + + + + + +Untitled Document + + + + +

    Execute
    + Return

    + +

    Source Code for JSP calling servlet

    + +

    Source Code for Servlet calling JSP

    + + + diff --git a/webapps/examples/jsp/num/numguess.html b/webapps/examples/jsp/num/numguess.html new file mode 100644 index 0000000..1c5a484 --- /dev/null +++ b/webapps/examples/jsp/num/numguess.html @@ -0,0 +1,34 @@ + + + +Untitled Document + + + + +

    + +

    Source Code for Numguess Example +

    + + + diff --git a/webapps/examples/jsp/num/numguess.jsp b/webapps/examples/jsp/num/numguess.jsp new file mode 100644 index 0000000..d9c61b9 --- /dev/null +++ b/webapps/examples/jsp/num/numguess.jsp @@ -0,0 +1,69 @@ +<%-- + 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. + + Number Guess Game + Written by Jason Hunter, CTO, K&A Software + http://www.servlets.com +--%> + +<%@ page import = "num.NumberGuessBean" %> + + + + + +Number Guess + + + +<% if (numguess.getSuccess()) { %> + + Congratulations! You got it. + And after just <%= numguess.getNumGuesses() %> tries.

    + + <% numguess.reset(); %> + + Care to try again? + +<% } else if (numguess.getNumGuesses() == 0) { %> + + Welcome to the Number Guess game.

    + + I'm thinking of a number between 1 and 100.

    + +

    + What's your guess? + +
    + +<% } else { %> + + Good guess, but nope. Try <%= numguess.getHint() %>. + + You have made <%= numguess.getNumGuesses() %> guesses.

    + + I'm thinking of a number between 1 and 100.

    + +

    + What's your guess? + +
    + +<% } %> + +
    + + diff --git a/webapps/examples/jsp/security/protected/error.jsp b/webapps/examples/jsp/security/protected/error.jsp new file mode 100644 index 0000000..29616af --- /dev/null +++ b/webapps/examples/jsp/security/protected/error.jsp @@ -0,0 +1,25 @@ +<%-- + 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. +--%> + + +Error Page For Examples + + +Invalid user name and/or password, please try +again. + + diff --git a/webapps/examples/jsp/security/protected/index.jsp b/webapps/examples/jsp/security/protected/index.jsp new file mode 100644 index 0000000..09c23e7 --- /dev/null +++ b/webapps/examples/jsp/security/protected/index.jsp @@ -0,0 +1,163 @@ +<%-- + 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. +--%> +<%@ page import="java.util.Enumeration" %> +<%@ page import="java.security.Principal" %> +<%@ page import="org.apache.catalina.TomcatPrincipal" %> +<% + if (request.getParameter("logoff") != null) { + session.invalidate(); + response.sendRedirect("index.jsp"); + return; + } +%> + + +Protected Page for Examples + + + +You are logged in as remote user +<%= util.HTMLFilter.filter(request.getRemoteUser()) %> +in session <%= session.getId() %>

    + +<% + if (request.getUserPrincipal() != null) { +%> + Your user principal name is + <%= util.HTMLFilter.filter(request.getUserPrincipal().getName()) %> +

    +<% + } else { +%> + No user principal could be identified.

    +<% + } +%> + +<% + String role = request.getParameter("role"); + if (role == null) + role = ""; + if (role.length() > 0) { + if (request.isUserInRole(role)) { +%> + You have been granted role + <%= util.HTMLFilter.filter(role) %>

    +<% + } else { +%> + You have not been granted role + <%= util.HTMLFilter.filter(role) %>

    +<% + } + } +%> + +To check whether your user name has been granted a particular role, +enter it here: +
    + + +
    +

    + +<% + Principal p = request.getUserPrincipal(); + if (!(p instanceof TomcatPrincipal)) { +%> +

    The principal does not support attributes.

    +<% + } else { + TomcatPrincipal principal = (TomcatPrincipal) p; +%> +

    The principal contains the following attributes:

    + + +<% + Enumeration names = principal.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + Object value = principal.getAttribute(name); + String type = value != null ? value.getClass().getName() : "unknown"; + if (value instanceof Object[]) { + Object[] values = (Object[]) value; + value = ""; + for (int i = 0; i < values.length; i++) { + value += values[i] + "
    "; + } + if (values.length > 0) { + type = values[0].getClass().getName() + "[]"; + } else { + type = "unknown"; + } + } + type = type.replaceFirst("^java\\.lang\\.", ""); +%> +
    + + + + +<% + } +%> +
    NameValueType
    <%= util.HTMLFilter.filter(name) %><%= util.HTMLFilter.filter(String.valueOf(value)) %><%= util.HTMLFilter.filter(type) %>
    +<% + } +%> +

    + +To add some data to the authenticated session, enter it here: +
    + + + +
    +

    + +<% + String dataName = request.getParameter("dataName"); + if (dataName != null) { + session.setAttribute(dataName, request.getParameter("dataValue")); + } +%> +

    The authenticated session contains the following attributes:

    + + +<% + Enumeration names = session.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); +%> + + + + +<% + } +%> +
    NameValue
    <%= util.HTMLFilter.filter(name) %><%= util.HTMLFilter.filter(String.valueOf(session.getAttribute(name))) %>
    +

    + +If you have configured this application for form-based authentication, you can +log off by clicking +here. +This should cause you to be returned to the login page after the redirect +that is performed. + + + diff --git a/webapps/examples/jsp/security/protected/login.jsp b/webapps/examples/jsp/security/protected/login.jsp new file mode 100644 index 0000000..e11a898 --- /dev/null +++ b/webapps/examples/jsp/security/protected/login.jsp @@ -0,0 +1,38 @@ +<%-- + 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. +--%> + + +Login Page for Examples + +
    + + + + + + + + + + + + + +
    Username:
    Password:
    +
    + + diff --git a/webapps/examples/jsp/sessions/DummyCart.html b/webapps/examples/jsp/sessions/DummyCart.html new file mode 100644 index 0000000..d953fa9 --- /dev/null +++ b/webapps/examples/jsp/sessions/DummyCart.html @@ -0,0 +1,56 @@ + + + + + +sessions.DummyCart Bean Properties + + +

    +sessions.DummyCart Bean Properties +

    +
    +
    +
    public class DummyCart
    extends Object
    + +

    +


    + +

    + + + + + + + + + +
    +Properties Summary
    + +String +DummyCart:items +
    +
    + +Multi +
    +


    + + diff --git a/webapps/examples/jsp/sessions/carts.html b/webapps/examples/jsp/sessions/carts.html new file mode 100644 index 0000000..834ee0a --- /dev/null +++ b/webapps/examples/jsp/sessions/carts.html @@ -0,0 +1,53 @@ + + + + + carts + + + + + +
    +
    +Please enter item to add or remove: +
    +Add Item: + + + + +

    + + + +
    + +
    + + diff --git a/webapps/examples/jsp/sessions/carts.jsp b/webapps/examples/jsp/sessions/carts.jsp new file mode 100644 index 0000000..dc51495 --- /dev/null +++ b/webapps/examples/jsp/sessions/carts.jsp @@ -0,0 +1,43 @@ +<%-- + 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. +--%> + + + + +<% + cart.processRequest(); +%> + + + +
    You have the following items in your cart: +
      +<% + String[] items = cart.getItems(); + for (String item : items) { +%> +
    1. <% out.print(util.HTMLFilter.filter(item)); %> +<% + } +%> +
    + +
    + +
    +<%@ include file ="carts.html" %> + diff --git a/webapps/examples/jsp/sessions/crt.html b/webapps/examples/jsp/sessions/crt.html new file mode 100644 index 0000000..11e6eda --- /dev/null +++ b/webapps/examples/jsp/sessions/crt.html @@ -0,0 +1,34 @@ + + + + +Untitled Document + + + + +

    + +

    Source Code for Cart Example +

    + +

    Property Sheet for DummyCart +

    + + + diff --git a/webapps/examples/jsp/simpletag/foo.html b/webapps/examples/jsp/simpletag/foo.html new file mode 100644 index 0000000..e20f840 --- /dev/null +++ b/webapps/examples/jsp/simpletag/foo.html @@ -0,0 +1,30 @@ + + + +Untitled Document + + + + +

    + +

    Source Code for the Simple Tag Example +

    + + + diff --git a/webapps/examples/jsp/simpletag/foo.jsp b/webapps/examples/jsp/simpletag/foo.jsp new file mode 100644 index 0000000..2489146 --- /dev/null +++ b/webapps/examples/jsp/simpletag/foo.jsp @@ -0,0 +1,38 @@ +<%-- + 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. +--%> + + +<%@ taglib uri="http://tomcat.apache.org/example-taglib" prefix="eg"%> + +Radio stations that rock: + +
      + +
    • <%= member %>
    • +
      +
    + + +Did you see me on the stderr window? + + + +Did you see me on the browser window as well? + + + + diff --git a/webapps/examples/jsp/snp/snoop.html b/webapps/examples/jsp/snp/snoop.html new file mode 100644 index 0000000..e48355b --- /dev/null +++ b/webapps/examples/jsp/snp/snoop.html @@ -0,0 +1,31 @@ + + + + +Untitled Document + + + + +

    + +

    Source Code for Request Parameters Example +

    + + + diff --git a/webapps/examples/jsp/snp/snoop.jsp b/webapps/examples/jsp/snp/snoop.jsp new file mode 100644 index 0000000..9bb57a8 --- /dev/null +++ b/webapps/examples/jsp/snp/snoop.jsp @@ -0,0 +1,56 @@ +<%-- + 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. +--%> + + +

    Request Information

    + +JSP Request Method: <%= util.HTMLFilter.filter(request.getMethod()) %> +
    +Request URI: <%= util.HTMLFilter.filter(request.getRequestURI()) %> +
    +Request Protocol: <%= util.HTMLFilter.filter(request.getProtocol()) %> +
    +Servlet path: <%= util.HTMLFilter.filter(request.getServletPath()) %> +
    +Path info: <%= util.HTMLFilter.filter(request.getPathInfo()) %> +
    +Query string: <%= util.HTMLFilter.filter(request.getQueryString()) %> +
    +Content length: <%= request.getContentLength() %> +
    +Content type: <%= util.HTMLFilter.filter(request.getContentType()) %> +
    +Server name: <%= util.HTMLFilter.filter(request.getServerName()) %> +
    +Server port: <%= request.getServerPort() %> +
    +Remote user: <%= util.HTMLFilter.filter(request.getRemoteUser()) %> +
    +Remote address: <%= util.HTMLFilter.filter(request.getRemoteAddr()) %> +
    +Remote host: <%= util.HTMLFilter.filter(request.getRemoteHost()) %> +
    +Authorization scheme: <%= util.HTMLFilter.filter(request.getAuthType()) %> +
    +Locale: <%= request.getLocale() %> +
    +The browser you are using is +<%= util.HTMLFilter.filter(request.getHeader("User-Agent")) %> +
    +
    + + diff --git a/webapps/examples/jsp/tagplugin/choose.html b/webapps/examples/jsp/tagplugin/choose.html new file mode 100644 index 0000000..afe90b2 --- /dev/null +++ b/webapps/examples/jsp/tagplugin/choose.html @@ -0,0 +1,36 @@ + + + +View Source Code + + + +

    + + + + + +

    + +

    + Source Code for choose.jsp +

    + + + diff --git a/webapps/examples/jsp/tagplugin/choose.jsp b/webapps/examples/jsp/tagplugin/choose.jsp new file mode 100644 index 0000000..745e5f5 --- /dev/null +++ b/webapps/examples/jsp/tagplugin/choose.jsp @@ -0,0 +1,54 @@ +<%-- + 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. +--%> + + + Tag Examples - choose + + +

    Tag Plugin Examples - <c:choose>

    + +
    +
    + Plugin Introductory Notes +
    + Brief Instructions for Writing Plugins +

    +
    + +
    + + <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + # ${index}: + + + One!
    +
    + + Four!
    +
    + + Three!
    +
    + + Huh?
    +
    +
    +
    + + diff --git a/webapps/examples/jsp/tagplugin/foreach.html b/webapps/examples/jsp/tagplugin/foreach.html new file mode 100644 index 0000000..3d2e608 --- /dev/null +++ b/webapps/examples/jsp/tagplugin/foreach.html @@ -0,0 +1,36 @@ + + + +View Source Code + + + +

    + + + + + +

    + +

    + Source Code for foreach.jsp +

    + + + diff --git a/webapps/examples/jsp/tagplugin/foreach.jsp b/webapps/examples/jsp/tagplugin/foreach.jsp new file mode 100644 index 0000000..4803506 --- /dev/null +++ b/webapps/examples/jsp/tagplugin/foreach.jsp @@ -0,0 +1,54 @@ +<%-- + 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. +--%> + + + Tag Plugin Examples: forEach + + +

    Tag Plugin Examples - <c:forEach>

    + +
    +
    + Plugin Introductory Notes +
    + Brief Instructions for Writing Plugins +

    +
    + +
    + + <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + <%@ page import="java.util.Vector" %> + +

    Iterating over a range

    + + ${item} + + + <% Vector v = new Vector<>(); + v.add("One"); v.add("Two"); v.add("Three"); v.add("Four"); + + pageContext.setAttribute("vector", v); + %> + +

    Iterating over a Vector

    + + + ${item} + + + diff --git a/webapps/examples/jsp/tagplugin/howto.html b/webapps/examples/jsp/tagplugin/howto.html new file mode 100644 index 0000000..5f1d223 --- /dev/null +++ b/webapps/examples/jsp/tagplugin/howto.html @@ -0,0 +1,45 @@ + + + + Tag Plugin Implementation + + +

    How to write tag plugins

    +

    + To write a plugin, you'll need to download the source for Tomcat. + There are two steps: +

      +
    1. + Implement the plugin class.

      + This class, which implements + org.apache.jasper.compiler.tagplugin.TagPlugin + instructs Jasper what Java codes to generate in place of the tag + handler calls. + See Javadoc for org.apache.jasper.compiler.tagplugin.TagPlugin + for details. +

    2. + +
    3. + Create the plugin descriptor file WEB-INF/tagPlugins.xml

      + This file + specifies the plugin classes and their corresponding tag handler + classes. +

    4. +
    + + diff --git a/webapps/examples/jsp/tagplugin/if.html b/webapps/examples/jsp/tagplugin/if.html new file mode 100644 index 0000000..b04ac59 --- /dev/null +++ b/webapps/examples/jsp/tagplugin/if.html @@ -0,0 +1,36 @@ + + + +View Source Code + + + +

    + + + + + +

    + +

    + Source Code for if.jsp +

    + + + diff --git a/webapps/examples/jsp/tagplugin/if.jsp b/webapps/examples/jsp/tagplugin/if.jsp new file mode 100644 index 0000000..af627bf --- /dev/null +++ b/webapps/examples/jsp/tagplugin/if.jsp @@ -0,0 +1,47 @@ +<%-- + 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. +--%> + + + Tag Plugin Examples: if + + +

    Tag Plugin Examples - <c:if>

    + +
    +
    + Plugin Introductory Notes +
    + Brief Instructions for Writing Plugins +

    +
    + +
    + <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + +

    Set the test result to a variable

    + + The result of testing for (1==1) is: ${theTruth} + +

    Conditionally execute the body

    + +

    It's true that (2>0)! Working.

    +
    + +

    It's not true that (0>2)! Failed.

    +
    + + diff --git a/webapps/examples/jsp/tagplugin/notes.html b/webapps/examples/jsp/tagplugin/notes.html new file mode 100644 index 0000000..cd326fc --- /dev/null +++ b/webapps/examples/jsp/tagplugin/notes.html @@ -0,0 +1,41 @@ + + + + Tag Plugin Introduction + + +

    Tag Plugins: Introductory Notes

    +

    + Tomcat provides a framework for implementing tag plugins. The + plugins instruct Jasper, at translation time, to replace tag handler + calls with Java scriptlets. + The framework allows tag library authors to implement plugins for + their tags. +

    +

    + Tomcat is released with plugins for several JSTL tags. Note + that these plugins work with JSTL 1.1 as well as JSTL 1.0, though + the examples uses JSTL 1.1 and JSP 2.0. + These plugins are not complete (for instance, some item types are not + handled in <c:if>). + They do serve as examples to show plugins in action (just + examine the generated Java files), and how they can be implemented. +

    + + + diff --git a/webapps/examples/jsp/xml/xml.html b/webapps/examples/jsp/xml/xml.html new file mode 100644 index 0000000..0012142 --- /dev/null +++ b/webapps/examples/jsp/xml/xml.html @@ -0,0 +1,31 @@ + + + + +Untitled Document + + + + +

    + +

    Source Code for XML syntax Example +

    + + + diff --git a/webapps/examples/jsp/xml/xml.jsp b/webapps/examples/jsp/xml/xml.jsp new file mode 100644 index 0000000..840b21f --- /dev/null +++ b/webapps/examples/jsp/xml/xml.jsp @@ -0,0 +1,70 @@ + + + + + + + + + String getDateTimeStr(Locale l) { + DateFormat df = SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, l); + return df.format(new Date()); + } + + + + + Example JSP in XML format + + + +This is the output of a simple JSP using XML format. +
    + +
    Use a jsp:scriptlet to loop from 1 to 10:
    + +// Note we need to declare CDATA because we don't escape the less than symbol + + + + +
    +]]> + +
    + Use a jsp:expression to write the date and time in the browser's locale: + getDateTimeStr(request.getLocale()) +
    + + + + <p>This sentence is enclosed in a jsp:text element.</p> + + + + +
    diff --git a/webapps/examples/servlets/cookies.html b/webapps/examples/servlets/cookies.html new file mode 100644 index 0000000..35d2340 --- /dev/null +++ b/webapps/examples/servlets/cookies.html @@ -0,0 +1,61 @@ + + + +Untitled Document + + + + +

    +

    Source Code for Cookie Example
    +

    + +
    import java.io.*;
    +import jakarta.servlet.*;
    +import jakarta.servlet.http.*;
    +
    +public class CookieExample extends HttpServlet {
    +
    +    public void doGet(HttpServletRequest request, HttpServletResponse response)
    +    throws IOException, ServletException
    +    {
    +        response.setContentType("text/html");
    +        PrintWriter out = response.getWriter();
    +
    +        // print out cookies
    +
    +        Cookie[] cookies = request.getCookies();
    +        for (int i = 0; i < cookies.length; i++) {
    +            Cookie c = cookies[i];
    +            String name = c.getName();
    +            String value = c.getValue();
    +            out.println(name + " = " + value);
    +        }
    +
    +        // set a cookie
    +
    +        String name = request.getParameter("cookieName");
    +        if (name != null && name.length() > 0) {
    +            String value = request.getParameter("cookieValue");
    +            Cookie c = new Cookie(name, value);
    +            response.addCookie(c);
    +        }
    +    }
    +}
    + + diff --git a/webapps/examples/servlets/helloworld.html b/webapps/examples/servlets/helloworld.html new file mode 100644 index 0000000..c1fb705 --- /dev/null +++ b/webapps/examples/servlets/helloworld.html @@ -0,0 +1,50 @@ + + + +Untitled Document + + + + +

    +

    Source Code for HelloWorld Example
    +

    + +
    import java.io.*;
    +import jakarta.servlet.*;
    +import jakarta.servlet.http.*;
    +
    +public class HelloWorld extends HttpServlet {
    +
    +    public void doGet(HttpServletRequest request, HttpServletResponse response)
    +    throws IOException, ServletException
    +    {
    +        response.setContentType("text/html");
    +        PrintWriter out = response.getWriter();
    +        out.println("<html>");
    +        out.println("<head>");
    +        out.println("<title>Hello World!</title>");
    +        out.println("</head>");
    +        out.println("<body>");
    +        out.println("<h1>Hello World!</h1>");
    +        out.println("</body>");
    +        out.println("</html>");
    +    }
    +}
    + + diff --git a/webapps/examples/servlets/images/code.gif b/webapps/examples/servlets/images/code.gif new file mode 100644 index 0000000000000000000000000000000000000000..93af2cd130aa61cb2f235cdd6f0e75ab444819ef GIT binary patch literal 292 zcmZ?wbhEHblwgoxIP#z2z<~q*(`NopOZ#sOM8;>*(#{wIk?|P@o$6#>Qz3K*V4S1Pnk5hzS72pDc_ZB|0DiWTyk`LIs7s6iv0@q;(ETk{*X8*p&Sz zS?%Jy|7uNwX5dT_rVVU*FIJccbhvMP#4t}Kw7uYTuBh_VnCTjt97)}qgEke3iU+j# zdTyN*!`HsV>FFn-s95XDrdAynk)$>kVJ;@##Ikl#0e5yDZsrM%j-vc*EJY@UJi>F- w^aL|Jc?Bj;=4WLO5adbda@4El6|}Iev#dyLst@fxxV)*dq0M4 zuU27uS$av`))UvZFOHHZ*ItST2O0$7;7H? zm&+9jg_V_+QmIrfmut0JNgWD>N~y#dv!$ejV3mcCNeF_Fs91zF06g0g%rptkdaf-6 zF_0xVM$i_FQ|#InC0H;@)orLVf()pw6mqsDEh%itRj8`R2I3~Mg(*yIbHb?sbqyn3 zQYZyCsY->?M4l}qr>- z0I$VBAEL|c3!Aa23>>`6zgZI-x?7(TmI_v%;048`BL-aEhiAV!!lj_8tNy{ zuHVlON6V>(=ELvD+RJkuyyRyVcEVSxb@v=^_xJ8_AXukA`|R1H)q9Ptr@mW4Q_jP; zvV-p8YH%TPuI2Q*17~mjF+W7_4*dGfOe`P2(>42bIlAt3wjaL+tnW9`gI6Q#4vn(x+N>Q)$&RbyXeH}5khoAZ zgHYWn8m8H>&V#9A@nKri+6f7CAZX^n(t)JHP^8RL7z)bJ*uqp1CeXrcK^P~G{tuqV z^BsQQ$2pwh@zEn}1p^F%5QJd}Aw&paj0qu>(zUfUmSs7P<9S{Xgle@~tJOqNtk>(3 zB+0U@D2l48nx^TxZWsoa&$A3e34s7$jDj$XWIqfUmW{ZO5C9=SDAKGTH2r`GRm|~r z5L$j98>Zp~kYOR42b6JwQ1b%S^%chx9UoE#D+ZEvK&b8ovKvT_U$;Hc_5{o0Esv@u zV|vtZNi@a@L^X--U`fN84f8w{M0eXUG$N)K%8oDDURAev)u`Ei)$*#g&zml%+l*qc z5u_%u5lz+^!=({{P?YDX?hsigszo##*Hy@JNK{Z=MY4eu6Y@37k1~gfg@L4*NB(TMHD`z5Mi$4{3$K3Fd0=2+ z2&SJ^4mHd1^M91~z5jb@w)hR4@9nxce6>IZv*odP-zSex6`o(pw(oi7X69J;zL6Ew zmMCYwj%&NFbnm5LWxe~Y@k3x5DnB+pXsm90nHrr#zm + + + + + Servlet Examples + + + +

    Servlet +Examples with Code

    +

    This is a collection of examples which demonstrate some of the more +frequently used parts of the Servlet API. Familiarity with the Java(tm) +Programming Language is assumed. +

    These examples will only work when viewed via an http URL. They will +not work if you are viewing these pages via a "file://..." URL. Please +refer to the README file provide with this Tomcat release regarding +how to configure and start the provided web server. +

    Wherever you see a form, enter some data and see how the servlet reacts. +When playing with the Cookie and Session Examples, jump back to the Headers +Example to see exactly what your browser is sending the server. +

    To navigate your way through the examples, the following icons will +help:

    +
      +
    • Execute the example
    • +
    • Look at the source code for the example
    • +
    • Return to this screen
    • +
    + +

    Tip: To see the cookie interactions with your browser, try turning on +the "notify when setting a cookie" option in your browser preferences. +This will let you see when a session is created and give some feedback +when looking at the cookie demo.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Hello WorldExecuteSource
    Request InfoExecuteSource
    Request HeadersExecuteSource
    Request ParametersExecuteSource
    CookiesExecuteSource
    SessionsExecuteSource
    + +

    Note: The source code for these examples does not contain all of the +source code that is actually in the example, only the important sections +of code. Code not important to understand the example has been removed +for clarity.

    + +

    Other Examples

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Servlet 3.0 Asynchronous processing examples:
    async0 + Execute +
    async1 + Execute +
    async2 + Execute +
    async3 + Execute +
    stockticker + Execute +
    Servlet 3.1 Non-blocking IO examples
    Byte counter + Execute +
    Number Writer + Execute +
    Servlet 4.0 Server Push examples
    Simple image push + Execute +
    Servlet 4.0 Trailer Field examples
    Response trailer fields + Execute +
    + + + diff --git a/webapps/examples/servlets/nonblocking/bytecounter.html b/webapps/examples/servlets/nonblocking/bytecounter.html new file mode 100644 index 0000000..55d31a2 --- /dev/null +++ b/webapps/examples/servlets/nonblocking/bytecounter.html @@ -0,0 +1,32 @@ + + + + Servlet 3.1 non-blocking IO examples: Byte counter + + +

    Byte counter

    +

    Select a file and/or enter some data using the form below and then submit + it. The server will read the request body using non-blocking IO and then + respond with the total length of the request body in bytes.

    +
    +

    +

    +

    +
    + + \ No newline at end of file diff --git a/webapps/examples/servlets/reqheaders.html b/webapps/examples/servlets/reqheaders.html new file mode 100644 index 0000000..66d3794 --- /dev/null +++ b/webapps/examples/servlets/reqheaders.html @@ -0,0 +1,49 @@ + + + +Untitled Document + + + + +

    +

    Source Code for RequestHeader Example
    +

    + +
    import java.io.*;
    +import java.util.*;
    +import jakarta.servlet.*;
    +import jakarta.servlet.http.*;
    +
    +public class RequestHeaderExample extends HttpServlet {
    +
    +    public void doGet(HttpServletRequest request, HttpServletResponse response)
    +    throws IOException, ServletException
    +    {
    +        response.setContentType("text/html");
    +        PrintWriter out = response.getWriter();
    +        Enumeration e = request.getHeaderNames();
    +        while (e.hasMoreElements()) {
    +            String name = (String)e.nextElement();
    +            String value = request.getHeader(name);
    +            out.println(name + " = " + value);
    +        }
    +    }
    +}
    + + diff --git a/webapps/examples/servlets/reqinfo.html b/webapps/examples/servlets/reqinfo.html new file mode 100644 index 0000000..cb2581d --- /dev/null +++ b/webapps/examples/servlets/reqinfo.html @@ -0,0 +1,68 @@ + + + +Untitled Document + + + + +

    +

    Source Code for Request Info Example
    +

    + +
    import java.io.*;
    +import jakarta.servlet.*;
    +import jakarta.servlet.http.*;
    +
    +public class RequestInfo extends HttpServlet {
    +
    +    public void doGet(HttpServletRequest request, HttpServletResponse response)
    +    throws IOException, ServletException
    +    {
    +        response.setContentType("text/html");
    +        PrintWriter out = response.getWriter();
    +        out.println("<html>");
    +        out.println("<body>");
    +        out.println("<head>");
    +        out.println("<title>Request Information Example</title>");
    +        out.println("</head>");
    +        out.println("<body>");
    +        out.println("<h3>Request Information Example</h3>");
    +        out.println("Method: " + request.getMethod());
    +        out.println("Request URI: " + request.getRequestURI());
    +        out.println("Protocol: " + request.getProtocol());
    +        out.println("PathInfo: " + request.getPathInfo());
    +        out.println("Remote Address: " + request.getRemoteAddr());
    +        out.println("</body>");
    +        out.println("</html>");
    +    }
    +
    +    /**
    +     * We are going to perform the same operations for POST requests
    +     * as for GET methods, so this method just sends the request to
    +     * the doGet method.
    +     */
    +
    +    public void doPost(HttpServletRequest request, HttpServletResponse response)
    +    throws IOException, ServletException
    +    {
    +        doGet(request, response);
    +    }
    +}
    + + diff --git a/webapps/examples/servlets/reqparams.html b/webapps/examples/servlets/reqparams.html new file mode 100644 index 0000000..f20901b --- /dev/null +++ b/webapps/examples/servlets/reqparams.html @@ -0,0 +1,82 @@ + + + +Untitled Document + + + + +

    +

    Source Code for Request Parameter Example
    +

    + +
    import java.io.*;
    +import java.util.*;
    +import jakarta.servlet.*;
    +import jakarta.servlet.http.*;
    +
    +public class RequestParamExample extends HttpServlet {
    +
    +    public void doGet(HttpServletRequest request, HttpServletResponse response)
    +    throws IOException, ServletException
    +    {
    +        response.setContentType("text/html");
    +        PrintWriter out = response.getWriter();
    +        out.println("<html>");
    +        out.println("<head>");
    +        out.println("<title>Request Parameters Example</title>");
    +        out.println("</head>");
    +        out.println("<body>");
    +        out.println("<h3>Request Parameters Example</h3>");
    +        out.println("Parameters in this request:<br>");
    +
    +        String firstName = request.getParameter("firstname");
    +        String lastName = request.getParameter("lastname");
    +
    +        if (firstName != null || lastName != null) {
    +            out.println("First Name:");
    +            out.println(" = " + HTMLFilter.filter(firstName) + "<br>");
    +            out.println("Last Name:");
    +            out.println(" = " + HTMLFilter.filter(lastName));
    +        } else {
    +            out.println("No Parameters, Please enter some");
    +        }
    +        out.println("<P>");
    +        out.print("<form action=\"");
    +        out.print("RequestParamExample\" ");
    +        out.println("method=POST>");
    +        out.println("First Name:");
    +        out.println("<input type=text size=20 name=firstname>");
    +        out.println("<br>");
    +        out.println("Last Name:");
    +        out.println("<input type=text size=20 name=lastname>");
    +        out.println("<br>");
    +        out.println("<input type=submit>");
    +        out.println("</form>");
    +        out.println("</body>");
    +        out.println("</html>");
    +    }
    +
    +    public void doPost(HttpServletRequest request, HttpServletResponse res)
    +    throws IOException, ServletException
    +    {
    +        doGet(request, response);
    +    }
    +}
    + + diff --git a/webapps/examples/servlets/sessions.html b/webapps/examples/servlets/sessions.html new file mode 100644 index 0000000..c855367 --- /dev/null +++ b/webapps/examples/servlets/sessions.html @@ -0,0 +1,70 @@ + + + +Untitled Document + + + + +

    +

    Source Code for Session Example
    +

    + +
    import java.io.*;
    +import java.util.*;
    +import jakarta.servlet.*;
    +import jakarta.servlet.http.*;
    +
    +public class SessionExample extends HttpServlet {
    +
    +    public void doGet(HttpServletRequest request, HttpServletResponse response)
    +    throws IOException, ServletException
    +    {
    +        response.setContentType("text/html");
    +        PrintWriter out = response.getWriter();
    +
    +        HttpSession session = request.getSession(true);
    +
    +        // print session info
    +
    +        Date created = new Date(session.getCreationTime());
    +        Date accessed = new Date(session.getLastAccessedTime());
    +        out.println("ID " + session.getId());
    +        out.println("Created: " + created);
    +        out.println("Last Accessed: " + accessed);
    +
    +        // set session info if needed
    +
    +        String dataName = request.getParameter("dataName");
    +        if (dataName != null && dataName.length() > 0) {
    +            String dataValue = request.getParameter("dataValue");
    +            session.setAttribute(dataName, dataValue);
    +        }
    +
    +        // print session contents
    +
    +        Enumeration e = session.getAttributeNames();
    +        while (e.hasMoreElements()) {
    +            String name = (String)e.nextElement();
    +            String value = session.getAttribute(name).toString();
    +            out.println(name + " = " + value);
    +        }
    +    }
    +}
    + + diff --git a/webapps/examples/websocket/chat.xhtml b/webapps/examples/websocket/chat.xhtml new file mode 100644 index 0000000..6c863fe --- /dev/null +++ b/webapps/examples/websocket/chat.xhtml @@ -0,0 +1,136 @@ + + + + + Apache Tomcat WebSocket Examples: Chat + + + + +

    Seems your browser doesn't support JavaScript! Websockets rely on JavaScript being enabled. Please enable + JavaScript and reload this page!

    +
    +

    + +

    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/webapps/examples/websocket/drawboard.xhtml b/webapps/examples/websocket/drawboard.xhtml new file mode 100644 index 0000000..7702ef9 --- /dev/null +++ b/webapps/examples/websocket/drawboard.xhtml @@ -0,0 +1,899 @@ + + + + + Apache Tomcat WebSocket Examples: Drawboard + + + + +
    Seems your browser doesn't support JavaScript! Websockets rely on JavaScript being enabled. Please enable + JavaScript and reload this page!
    +
    +
    +
    +
    + +

    About Drawboard WebSocket Example

    +
    +

    + This drawboard is a page where you can draw with your mouse or touch input + (using different colors) and everybody else which has the page open will + immediately see what you are drawing.
    + If someone opens the page later, they will get the current room image (so they + can see what was already drawn by other people). +

    +

    + It uses asynchronous sending of messages so that it doesn't need separate threads + for each client to send messages.
    + Each "Room" (where the drawing happens) uses a ReentrantLock to synchronize access + (currently, only a single Room is implemented). +

    +

    + When you open the page, first you will receive a binary websocket message containing + the current room image as PNG image. After that, you will receive string messages + that contain the drawing actions (line from x1,y1 to x2,y2).
    + Note that it currently only uses simple string messages instead of JSON because + I did not want to introduce a dependency on a JSON lib. +

    +

    + It uses synchronization mechanisms to ensure that the final image will look the same + for every user, regardless of what their network latency/speed is – e.g. if two user + draw at the same time on the same place, the server will decide which line was the + first one, and that will be reflected on every client. +

    +
    + + \ No newline at end of file diff --git a/webapps/examples/websocket/echo.xhtml b/webapps/examples/websocket/echo.xhtml new file mode 100644 index 0000000..db3ce71 --- /dev/null +++ b/webapps/examples/websocket/echo.xhtml @@ -0,0 +1,184 @@ + + + + + Apache Tomcat WebSocket Examples: Echo + + + + +

    Seems your browser doesn't support JavaScript! Websockets rely on JavaScript being enabled. Please enable + JavaScript and reload this page!

    +
    +
    +
    + Connect to service implemented using: +
    + + +
    + + +
    + + +
    + + + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/webapps/examples/websocket/index.xhtml b/webapps/examples/websocket/index.xhtml new file mode 100644 index 0000000..97ee945 --- /dev/null +++ b/webapps/examples/websocket/index.xhtml @@ -0,0 +1,32 @@ + + + + + Apache Tomcat WebSocket Examples + + +

    Apache Tomcat WebSocket Examples

    + + + \ No newline at end of file diff --git a/webapps/examples/websocket/snake.xhtml b/webapps/examples/websocket/snake.xhtml new file mode 100644 index 0000000..a376a83 --- /dev/null +++ b/webapps/examples/websocket/snake.xhtml @@ -0,0 +1,266 @@ + + + + + Apache Tomcat WebSocket Examples: Multiplayer Snake + + + +

    Seems your browser doesn't support JavaScript! Websockets rely on JavaScript being enabled. Please enable + JavaScript and reload this page!

    +
    + +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/webapps/host-manager/META-INF/context.xml b/webapps/host-manager/META-INF/context.xml new file mode 100644 index 0000000..1fa3a5a --- /dev/null +++ b/webapps/host-manager/META-INF/context.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/webapps/host-manager/WEB-INF/jsp/401.jsp b/webapps/host-manager/WEB-INF/jsp/401.jsp new file mode 100644 index 0000000..047766b --- /dev/null +++ b/webapps/host-manager/WEB-INF/jsp/401.jsp @@ -0,0 +1,71 @@ +<%-- + 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. +--%> +<%@ page session="false" trimDirectiveWhitespaces="true" %> + + + + 401 Unauthorized + + + +

    401 Unauthorized

    +

    + You are not authorized to view this page. If you have not changed + any configuration files, please examine the file + conf/tomcat-users.xml in your installation. That + file must contain the credentials to let you use this webapp. +

    +

    + For example, to add the admin-gui role to a user named + tomcat with a password of s3cret, add the following to the + config file listed above. +

    +
    +<role rolename="admin-gui"/>
    +<user username="tomcat" password="s3cret" roles="admin-gui"/>
    +
    +

    + Note that for Tomcat 7 onwards, the roles required to use the host manager + application were changed from the single admin role to the + following two roles. You will need to assign the role(s) required for + the functionality you wish to access. +

    +
      +
    • admin-gui - allows access to the HTML GUI
    • +
    • admin-script - allows access to the text interface
    • +
    +

    + The HTML interface is protected against CSRF but the text interface is not. + To maintain the CSRF protection: +

    +
      +
    • Users with the admin-gui role should not be granted the + admin-script role.
    • +
    • If the text interface is accessed through a browser (e.g. for testing + since this interface is intended for tools not humans) then the browser + must be closed afterwards to terminate the session.
    • +
    + + + diff --git a/webapps/host-manager/WEB-INF/jsp/403.jsp b/webapps/host-manager/WEB-INF/jsp/403.jsp new file mode 100644 index 0000000..74e1e2d --- /dev/null +++ b/webapps/host-manager/WEB-INF/jsp/403.jsp @@ -0,0 +1,90 @@ +<%-- + 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. +--%> +<%@ page session="false" trimDirectiveWhitespaces="true" %> + + + + 403 Access Denied + + + +

    403 Access Denied

    +

    + You are not authorized to view this page. +

    +

    + By default the Host Manager is only accessible from a browser running on the + same machine as Tomcat. If you wish to modify this restriction, you'll need + to edit the Host Manager's context.xml file. +

    +

    + If you have already configured the Host Manager application to allow access + and you have used your browsers back button, used a saved book-mark or + similar then you may have triggered the cross-site request forgery (CSRF) + protection that has been enabled for the HTML interface of the Host Manager + application. You will need to reset this protection by returning to the + main Host Manager page. + Once you return to this page, you will be able to continue using the Host + Manager application's HTML interface normally. If you continue to see this + access denied message, check that you have the necessary permissions to + access this application. +

    +

    If you have not changed + any configuration files, please examine the file + conf/tomcat-users.xml in your installation. That + file must contain the credentials to let you use this webapp. +

    +

    + For example, to add the admin-gui role to a user named + tomcat with a password of s3cret, add the following to the + config file listed above. +

    +
    +<role rolename="admin-gui"/>
    +<user username="tomcat" password="s3cret" roles="admin-gui"/>
    +
    +

    + Note that for Tomcat 7 onwards, the roles required to use the host manager + application were changed from the single admin role to the + following two roles. You will need to assign the role(s) required for + the functionality you wish to access. +

    +
      +
    • admin-gui - allows access to the HTML GUI
    • +
    • admin-script - allows access to the text interface
    • +
    +

    + The HTML interface is protected against CSRF but the text interface is not. + To maintain the CSRF protection: +

    +
      +
    • Users with the admin-gui role should not be granted the + admin-script role.
    • +
    • If the text interface is accessed through a browser (e.g. for testing + since this interface is intended for tools not humans) then the browser + must be closed afterwards to terminate the session.
    • +
    + + + diff --git a/webapps/host-manager/WEB-INF/jsp/404.jsp b/webapps/host-manager/WEB-INF/jsp/404.jsp new file mode 100644 index 0000000..956b9a9 --- /dev/null +++ b/webapps/host-manager/WEB-INF/jsp/404.jsp @@ -0,0 +1,62 @@ +<%-- + 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. +--%> +<%@ page import="org.apache.tomcat.util.security.Escape" session="false" + trimDirectiveWhitespaces="true" %> + + + + 404 Not found + + + +

    404 Not found

    +

    + The page you tried to access + (<%=Escape.htmlElementContent((String) request.getAttribute( + "jakarta.servlet.error.request_uri"))%>) + does not exist. +

    +

    + The Host Manager application has been re-structured for Tomcat 7 onwards and + some URLs have changed. All URLs used to access the Manager application + should now start with one of the following options: +

    +
      +
    • <%=request.getContextPath()%>/html for the HTML GUI
    • +
    • <%=request.getContextPath()%>/text for the text interface
    • +
    +

    + Note that the URL for the text interface has changed from + "<%=request.getContextPath()%>" to + "<%=request.getContextPath()%>/text". +

    +

    + You probably need to adjust the URL you are using to access the Host Manager + application. However, there is always a chance you have found a bug in the + Host Manager application. If you are sure you have found a bug, and that the + bug has not already been reported, please report it to the Apache Tomcat + team. +

    + + diff --git a/webapps/host-manager/WEB-INF/manager.xml b/webapps/host-manager/WEB-INF/manager.xml new file mode 100644 index 0000000..a26dca6 --- /dev/null +++ b/webapps/host-manager/WEB-INF/manager.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/webapps/host-manager/WEB-INF/web.xml b/webapps/host-manager/WEB-INF/web.xml new file mode 100644 index 0000000..6c6d501 --- /dev/null +++ b/webapps/host-manager/WEB-INF/web.xml @@ -0,0 +1,148 @@ + + + + + Tomcat Host Manager Application + + A scriptable host management web application for the Tomcat Web Server; + Manager lets you view, create and remove virtual hosts. + + + UTF-8 + + + HostManager + org.apache.catalina.manager.host.HostManagerServlet + + debug + 2 + + + + HTMLHostManager + org.apache.catalina.manager.host.HTMLHostManagerServlet + + debug + 2 + + + + + CSRF + org.apache.catalina.filters.CsrfPreventionFilter + + entryPoints + /html,/html/,/html/list,/index.jsp + + + + + + + + HTTP header security filter + org.apache.catalina.filters.HttpHeaderSecurityFilter + + hstsEnabled + false + + + + + CSRF + HTMLHostManager + + + + HTTP header security filter + /* + + + + + HostManager + /text/* + + + HTMLHostManager + /html/* + + + + + + HostManager commands + /text/* + + + + admin-script + + + + + HTMLHostManager commands + /html/* + + + + admin-gui + + + + + + BASIC + Tomcat Host Manager Application + + + + + + The role that is required to log in to the Host Manager Application HTML + interface + + admin-gui + + + + The role that is required to log in to the Host Manager Application text + interface + + admin-script + + + + 401 + /WEB-INF/jsp/401.jsp + + + 403 + /WEB-INF/jsp/403.jsp + + + 404 + /WEB-INF/jsp/404.jsp + + + diff --git a/webapps/host-manager/css/manager.css b/webapps/host-manager/css/manager.css new file mode 100644 index 0000000..5b50738 --- /dev/null +++ b/webapps/host-manager/css/manager.css @@ -0,0 +1,141 @@ +/* + * 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. + */ + +body { + font-family: Tahoma, Arial, sans-serif; +} + +h1, +h2, +h3, +b { + color : white; + background-color: #525D76; +} + +h1 { + font-size: 22px; +} + +h2 { + font-size: 16px; +} + +h3 { + font-size: 14px; +} + +p { + font-size: 12px; +} + +a { + color: black; +} + +.line { + height : 1px; + background-color: #525D76; + border : none; +} + +table { + width: 100%; +} + +td.page-title { + text-align : center; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-weight : bold; + background : white; + color : black; +} + +td.title { + text-align : left; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-style : italic; + font-weight : bold; + background : #D2A41C; +} + +td.header-left { + text-align : left; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-weight : bold; + background : #FFDC75; +} + +td.header-center { + text-align : center; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-weight : bold; + background : #FFDC75; +} + +td.row-left { + text-align : left; + vertical-align: middle; + font-family : sans-serif, Tahoma, Arial; + color : black; +} + +td.row-center { + text-align : center; + vertical-align: middle; + font-family : sans-serif, Tahoma, Arial; + color : black; +} + +td.row-right { + text-align : right; + vertical-align: middle; + font-family : sans-serif, Tahoma, Arial; + color : black; +} + +TH { + text-align : center; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-weight : bold; + background : #FFDC75; +} + +TD { + text-align : center; + vertical-align: middle; + font-family : sans-serif, Tahoma, Arial; + color : black; +} + +form { + margin: 1; +} + +form.inline { + display: inline; +} + +img.tomcat-logo { + height: 92px; + float : left; +} \ No newline at end of file diff --git a/webapps/host-manager/images/asf-logo.svg b/webapps/host-manager/images/asf-logo.svg new file mode 100644 index 0000000..e24cbe5 --- /dev/null +++ b/webapps/host-manager/images/asf-logo.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/host-manager/images/tomcat.svg b/webapps/host-manager/images/tomcat.svg new file mode 100644 index 0000000..8823f79 --- /dev/null +++ b/webapps/host-manager/images/tomcat.svg @@ -0,0 +1,967 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 2006-05-09T08:17:21Z + 2006-05-09T08:37:38Z + Illustrator + + + + JPEG + 256 + 184 + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAuAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXhH/OYHnWfQ/wAurfRLSUxXXmK49GQqaN9VtwJJqH3cxqfYnFXhP5Y/ +85O+f/JU0enaw769okbBJLS8ZvrUKg0IhnarDj/I9R2HHFX2F+Xn5neT/P8ApP6R8u3glKAfW7KS +iXNuzdFljqaezCqnsTirK8VdirsVdirsVdirsVdirC/zM/Nvyd+XemC71255Xcqk2WmQUa5nI2+F +CRxUd3ag+nbFXx1+Zf8Azkn+YvneaW1tLh9C0NgwXTrB2V3Sm/rzji8m3UDitP2cVfV//OOfmabz +D+T3l+6uHMl1aRPYTsxqSbVzEhJ7kxKhxV6VirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd +irsVfHn/ADlxdSa7+bvlvyvGx4RW0EVARtNfXJVqf7BY+uRlKgT3JAt5r/zkD5ZGgfmfqSRR+nZ6 +gsd9agdOMq0f/ksj5h9nZvEwgnmNi2Z4cMiw/wAqebPMHlTXLfW9BvHstQtjVZEPwstQWjkXo6NT +4lOxzOan3v8Akl+cel/mX5a+tAJa69ZcU1fTlJojGvGWLluYpKbV6GqmtKlV6NirsVdirsVdirsV +eWfnr+eGl/lroywwBLzzPfox02wJqqL0+sT03EanoOrnYdyFXwh5i8x655j1i41jW7yS+1K6blNc +SmpPgABQKo6BVFB2xVnf5Q+SjrWh+d9Yli5w6XolylsadbqSNnTj8kiYf7IZg6zUeHKERzlIfL8U +3YoWCe4Pff8AnCfVTN5D1zTCamz1P11HcLcQIAPlWE5nNL6KxV2KuxV2KuxV2KuxV2KuxV2KuxV2 +KuxV2KuxV2KuxV2KvjD8wm/Sv/OX8UTGsdrqGnCMNUU+rW0Mp6f5ammY2sNYZ/1T9zZi+oe9m/8A +zkx+Xc/mPytFrunRepqehc3ljUVeS0cAyAU6mMqHA8OXfNB2PqhCfAeUvv8A2uZqcdix0fIedQ69 +m35OefrryN+YOla2kpjsjKttqqDo9nMwEoI78ftr/lKMVfaeqf8AOSH5KaaSs3meCZx0W1inuanf +YNDG69vHFWM3v/OYn5QW5YQ/pK8ArQwWqitPD1pIuvviqVT/APObH5cKR6GjaxIP2i8dqhB9qTvi +qmP+c2fIFd9C1Wnfa2/6q4qmFv8A85n/AJUSvxksdZtx/NJb25H/ACTuHOKp3bf85XfkpPBI7avN +BIisywS2lwGcqCeIZUdKmm1WGKvijzz5x1bzl5q1HzFqjlrm+lLrHWqxRDaOFP8AJjSij7+uKpNb +W1xdXMVtbRtNcTuscMKAszu54qqgbkkmgwE1uVfbHkL8uk8o/lTPoMiK+o3drPNqZHRrieIhlr4I +tEB9q5yWo1fi6gS/hBFfN2UMfDAjqwT/AJwdvyt/5usC20sVlOq77em0yMR2/wB2Cudc619ZYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXxZKTJ/zmFc+oedNTmA5b/ZtG49fCgpmH2h/ +cS9zbh+sPqDrsc4t2r57/Nf/AJxkGo3c+teSTFb3ExMlxo0hEcTMdybd/spU/sN8PgQNs3+i7Xoc +OX5/rcLLpusWIaF/zif56vFWTVr6y0pG6xgtczL81QLH90mZWTtnFH6bk1x0sjz2Z1pf/OIvlOIL ++lNbvrthSv1dYrZSe+zC4ND88wp9uTP0xA9+/wCptGkHUsms/wDnGf8AKS3AEunT3dOpmupxXam/ +pNFmPPtjOeRA+H67bBpoPDv+ch/yt03yXrdjeaFbG30HUouCQ8pJBFcQ0DqXkZ2+NSrCrfzeGbns +vWHNAiX1BxdRi4TtySH8jfJdn5u/MOy07UIfrGl28ct3fw1IDRxrxUEqQaGV0By7X6g4sRkOfRhh +hxSp9N3X/OO/5P3FSdBETGnxRXN0nT/JEvH8M50dq6gfxfYHOOnh3JDqP/OKn5a3NTazajYt+yIp +0dfpEsbn/hsvj21lHMRP497A6SPmwzW/+cQr9A76H5himO/CG9haL5AyxGT/AIhmXj7cifqiR7t/ +1NUtIehZh+S3/OP8Xk+5GveYXivNfTkLSKIloLYGqlwzBecjL3p8P45i9odqeIOCH09fNtw6fh3P +N7DfIz2VwijkzRuFA6klTmpxmpD3uRLk+bf+cJrrj+Yet2tT+90hpeP7J9O5hWp9/wB5tneunfZm +KuxV2KuxV2KuxV2KuxVZLNFDG0srrHGu7O5CqB7k4qks3nzyNC5jm8xaZHIOqPeW6nf2L4qmFhrW +j6iK6ff294KVrbypLt1r8BPjirAvzb/Pnyf+WrW9rqKS6hq90vqRaba8eaxVp6krMQEUkEL1JPbq +cVYFof8Azmp5BupVj1fR9Q0wNsZo/SuY1/1qGN6fJDir2Xyf+Yfkrzjam48taxb6iqgGSKNisyA9 +PUhcLKn+yXFWRYq7FXYq7FXxRrBNj/zl/NVwC+rL8XtcWw+Hf/jJTMXXC8M/6pbMP1h9SZxLtnYq +7FWG+afzg/LnyvdNZ6vrUSXqGj2sKvcSofB1hV+B/wBamZmHs/NkFxjt8mqWaMeZRPk78zvI/nF5 +ItA1RLm5hHKS1dXhmC1pyEcoRmXputRkdRosuLeQ2TDLGXJCfm/5JXzj5D1HSo05X8a/WtNPcXMI +JUD/AFxVP9lk+z9R4WUE8jsWOaHFGnl3/OI/lpodN1zzFMlGuJUsLcsKELCPUlpXsWkQfNc2Xbmb +eMPj+r9LRpI8y+hc0DmuxV2KuxV2Kvl//nClHP5oas4B4Lok6luwLXdqQPpoc9AdK+08VdirsVdi +rsVdiqXeYPMOi+XtIudY1q7jsdNtF5z3EpooHQAd2ZjsqjcnYYq+VfPf/OV3nXzNqp0D8stPlto5 +mMcF0IfrGoT+8UIDrGD8mbvVcVSqz/5xn/Pjzs66h5t1RbUueX+5W7kurgA/yxx+sq/6pZaeGKsj +h/5wanMYM3nNUk7qmml1/wCCN0n6sVQt7/zhDr8B56Z5stppEIMZntZLfcb1qkk9KHFXzr5mtdUs +tfv9O1S5a7vtOuJbKaZndwWt3MZ4mSjcartUDFUsxVFabqeo6XfQ3+m3UtlfW7c4Lq3dopUbxV1I +IxV9Sfkr/wA5aNcT2+gfmG6K8hWO18wqAi1OwF2q0Vf+Mi0H8w6tir6lVlZQykMrCqsNwQe4xVvF +XYq+Kfzzro3/ADlLa6oxKJLdaReFiaApGsMLeG1ISMqzw4sco94LKBogvqPOEdw7FXkf55/mBrlj +Jp3kbykX/wAVeYSFE0Zo8FuzFOSt+wzlW+P9lQx2NDm27N0sZXlyfRFxs+Qj0jmUd5B/IHyP5bsI +31Oyh1zWnAa6vb1BMnqHciKKSqKAehI5e+Q1XamTIfSeGPlzTj08YjfcsJ/PDy5pXkHX/LH5geW7 +WPTGhvlt9Rt7RBFHKpBk+wgCjnGkiPQbg5m9m5jnhLFM3s1Z4iBEg+hOu4zn3NQOkaLpuj20ltp8 +IghlnnunRe8tzK0sh/4JzQdhtlmXLKZuXdXyYxiByR2VsnYqxjV/zO/L3SJWh1DzDYQzoaPD66PI +p/ykQsw+kZlY9Dmnyifu+9qOWI6pvoOvaRr+kwato9yt3p1zz9C4UMob03MbbMFOzoR0ynLiljkY +yFEM4yBFhV1WVYdLvJWJCxwSOxHWioTjhFzA8wsuRfPn/OEVoX83eZLzekOnxQnpSsswb/mVneOn +fYOKuxV2KuxV2KqF9e2lhZT315KsFpaxtNcTuaKkcYLMzHwAFcVfFHnPzR50/wCchPzJi8veXlaH +y7aO5sYnqsUUCkK97dU/bYdB2qFXcklV9U/lj+UnlH8u9IWz0a2WS+dQL7VpVBuLhh1q37KV+yg2 +Huakqs1xV2KuxV8v/nf/AM4patrnmG+80eSp4Xn1GR7m/wBIuW9ImdyWd4JSOH7xjUq9KGvxb0Cr +5/1j8mPzX0iRkvfKepgL9qSC3e5jG9P7yASJ1PjiqRjyb5vMvpDQ9QMtePpi1m5culKca1xVPtG/ +JT82dYdUsvKepUf7MlxA1rGe395cekn44q+zf+cffKv5m+VvJ50bzvPbzRwFf0RFHK01xbxU+KCV +6cCqmnDizU3FaUAVeo4q7FXx5/zmxpD2vnTy7rcdUN5YPbh12POzmL1qO4FyuKsl/Lz/AJyc8ra2 +sNj5mUaHqZAU3TGtnI3Qnn1ir1o/wj+bOY1XY8474/UO7r+1z8epB2Oz2iKWKaJJYnWSKQBkkQhl +ZTuCCNiDmnIINFygVGXTNOmvYb6W1hkvbbkLe6eNWljDgq3ByOS1UkGhwjJIDhs0ei0LtE5FLxD/ +AJyycP5F0ezQcp59WjaNdt+NvMp/GQZuuxI/vJH+j+lxNWfSPe9rgiEMEcQNRGoQE9+IpmmlKyS5 +QCpgSsllihieWVxHFGpeR2NFVVFSST0AGEAk0EEvn2fVfOv5269e6foN9Jof5e6fIYbm9QMst2af +ZIBUtyG4QkKqkFqmgzfiGLRQBkOLKfx+C4ZMspobRZzof/OOv5U6VCiyaUdSnUUa4vZZJGb5opSL +7kzBydrZ5HY8PuDbHTQDP9G0XStE02HTNJtks9Pt+Xo20Qoi83LtQe7MTmBkyynLikbJboxAFBJv +zO1Aaf8Al35lu60ZNNuljP8AlvEyJ/wzDL9FDizQH9IfYxymol59/wA4P6S0eg+adXI+G6ura0Vv +e2jeRgP+kkZ2zqX01irsVdirsVdir50/5zJ/MGbSfK1j5PspOFxrrGa/KmhFpAwon/PWWn0KR3xV +mf8Azjd+WEPkj8vrae5iA17XES91KQijorrWG333HpI24/mLYq9YxV2KuxV2KuxV2KuxV2KuxV2K +obUdT03TbR7zUbuGytI/7y4uJFijX5u5VRir5U/5yz/MX8tfNfl7S7DQtZh1LW9NvS5W2V3iFvJG +yyUnC+kfjVPsscVSv8i/yi/LTzn5Ij1XVLSafU4J5rW9C3EkaFlIdCFQrT926980XaOuy4cnDGqI +vk5eDDGQsvdvKXkby35StXtdBgmtrZ6Vge6uZ4wf5ljmkkRCe5UCuaPPqp5Tc9/gHLhjEeSN8x3+ +o6foGoX2m2hv9QtoJJbWyFazSKpKxjjv8R22yOCEZTAkaBZTJAsPHv8AlcP53/8Altpv+BuP+ac3 +H8n6X/VPti4vjZP5rzz8wfPP5i+bfNvluw1Dyq1rqWjzG+g0ROZmuRVZDVGHPjxgbcDpXNhpdNiw +wkYy9Mutj8dWnJOUiAQ9D/5XD+d//ltpv+BuP+ac1/8AJ+l/1T7Yt3jZP5rv+Vw/nf8A+W2m/wCB +uP8AmnH+T9L/AKp9sV8bJ/NYp+ZX5v8A5qXnli40LVfKbaCutAWkdyxlWRwWXnHGrheRdfhI8DmV +pNBgE+KMuLh9zXkzTIoirR/kbzf+bvlHy1Y+XtO/LedobYENM6zK0kjtyeRzxoOTH6BtkNTp9Plm +ZyyfaEwnOIoRej+RPO35o6xr62fmPyf+hdNMTub71C1HWnFaV/azX6rS4IQuE+KXds348kyaIZ7q +jaqthKdKSCS/pSBbp3jhr4uY1kbbwA38Rmux8PF6r4fJuldbPlv8+YvzstdPS483apafoO7nEEVh +pcjJbl6NIA0bKkjgenWsnKhpnTdnHTH+7HqHfz+f6nAz8f8AFyfQ3/OLHl06N+TWkyOnCfVpJ9Rm +Hj6r+nEfphiQ5t3GeuYq7FXYq7FXYq+MfzQhXzz/AM5YWmgz1lsLe7sbB4zvW3gRbi5TvSrNLir7 +OxV2KuxV2KuxV2KuxV2KuxV5j59/5yM/K7yb6kFxqQ1TU0qP0dpvG4cMO0kgIij36hn5e2KvAvMv +/OWP5p+arl9P8laWukxtXiYIzfXvHpUuy+mg+UdR/NkJ5IwFyIA80xiSaDF/+VT/AJo+b7sah5w1 +h1kavx3sz3k617KgYoo9uYp4ZptR7QYIbRuZ8uXzP7XMx6GcuezJYf8AnH3yrBptwjXFxd6g8LrB +NIwSNJSpCOEQA7NvRmOak+0eQzGwjCxfU11/FOT/ACfEDnZYH+S+sfmZZeajoHlC8htrq6ZnubC/ +K/VnMAPLkrAtyUdfT+Kg8BnSa7HhMOLINg6/CZA1F9k6KdbOmw/pxbZdTp/pH1IyNAW8U9UK9Pnn +I5eDi9F8PnzdlG63R2VsmndUUu5CooJZiaAAdSTiBaHhP5N8/On5r+bPzEkBbT7dv0do7EGhWgUM +tRswgjUsP+LM3vaH7nBDCOZ5/j3/AHOJh9UzJ7vmicx2KvEf+clQLS78i63cEjT9O1cC6O3H4mjl +FR/qwPm77G3GSPUj9f63E1XQvbQQQCDUHoc0jlN4pSXzN5z8q+V7ZLjX9Tg0+OSvpLK37x+PXhGv +J3pXfiMuw6bJlNQFsJ5BHmXzJ+dn5haf+Z/mby75e8qtLPbLN6EbyI0YluruRI0oh+KigChIHU50 +/ZmilhieL6i4GoyiZ2fbWh6Ra6Noun6PaClpp1tFaW4/4rgQRr+C5s3HR2KuxV2KuxV2KvjfymCP ++c0p/rdK/pTU+POlKfUp/S/4144q+yMVdirsVdirsVdirsVeQfmX/wA5Ofl55MaaxtZv0/rcdVNl +ZMDEj+E1x8SL4ELyYdxir5W/Mf8A5yD/ADJ88GSC6vjpmjyVC6VYFoYmQ1FJXr6kte/I8fADFXme +Kvpj8jdTtb3yJBFFGkdxYyyW9zwVU5MDzRzTqSjipPU1zhvaDHKOosk8Mht5d/6/i7rQSBh5h6Fm +ic12Kvnvz6l35B/Nqz8z2CEQyzLqMSqeIY143UVf8upr7Pnedl5RqdLwS5gcJ/R9n2uj1MPDyWPe ++wdL1Ky1TTbXUrGQTWd5Ek9vKOjJIoZT9xznMkDCRieYc2JsWisgyYZ+b1p5vvfIGqWPlSFZ9Tu0 +9F1LiN/q77TelXYuV+EAkddt6A5vZ8sccoMzsPv6NOYSMdnzl+Wn5m/mVoKR+RtEtNLsrmGWSsOp +q1vM87t8Su8ssS+p0UKaGgAGdDqtHhyfvJ2fd3fBwseWUfSHq36V/wCcqf8AqzaN/wAGn/ZRms4N +B/OP2/qci83c79K/85U/9WbRv+DT/sox4NB/OP2/qW83c8o/Mj8z/wAy/MAm8i6zaaZfXU0sY9HT +Ea4lSdGqqxvFLKvqbFSBXqQc2el0eHH+8jY2693xcfJllL0l9KflXb+bbXyJpVp5riWLV7aIQsqu +JGMSbRGUio9ThQNQnx70znNccZyk4+R+9zsIkIi2W5iNqB1xdH/RF2+sxQy6XFE8t4tyiyRelGpZ +i6uCpAAyzFxcQ4D6ixlVb8nzj/zjB5UtfNn5xal5tisltNE0Rpbu1tEUCOOa6ZktYgBt+7j5tt3U +Z3UIkRAJt1BO77PySHYq7FXYq7FXYq+M/wAyX/wb/wA5b2WsP+7s7q90+7Zz8NILlEt7htqV3EmK +vszFXYq7FXYq7FWGfmR+bnkn8vrD6xr16PrkilrXS4KPdTdacY6jitRTmxC++Kvjz80/+clPPvnk +TWVq50Py45KfULRj6kqntcTjiz1H7K8V8QeuKsQ/KyLyvP5wtbTzFbC4trn91bc2IjW4JBj9QAjk +G+zQ7VIrmB2mcowE4jUh93Vv0wiZgS5Po7zD5J8ta/pa6bf2UfoQrxtWiAjeDbb0io+Hp06eIzht +N2jmwz4oyu+d7373dZNPCYoh8/effyj17yuZLu3B1DRgSRdRr8cS9f3yD7P+sPh+XTOz7P7Wxajb +6Z936u90+fSyx78wnP8Azj5r4s/M11o8jUi1OHlED/v63qwA+cbP92YvtDp+PCJjnA/Ydv1NugyV +Ou99C5xDuWDeefKvnzV9WiufL+v/AKKskt1jkt+Ui8pQ7sX+AEbqyj6M3XZ2t02LGRlhxyvnQO23 +e4eow5JSuJoe8sD81/lL+ZF9pj3Go65Hq7WKPLBbMZGc7VZY+S9WC9O+bnSdsaQTEYQ4OLyAHxou +Jl0mWrJuvel/5Q/8rK80ySeXdA85S6P9Qh9W2spZ51RouXx+kEDD4CwqPfbvmz1pw4xxzhxX5Bxc +XFLYGnv35Y+RfzR0DXri881+af03p0lq8MVp6s0nGZpI2WSkiqNkRh9OaLW6rBkgBjjwm+4D7nMx +Y5g7m3p2axyGGfmF+U3k/wA82pGq23paii8bfVIAFuEpWgLU+NN/st9FDvmZpddkwnbePc1ZMMZ+ +95R/iv8AMz8lbm20/wAzMPMvk2Z/Ssr5XpcIBvxXmSwKr/ut6r2Vxm28HDrAZQ9OTr+P0uNxzxbH +cNSeb/zJ/Om9uNM8pk+XPJ0Lelf6g7D13DD7L8DyJZf91oafzNTEYMOjAlP1ZOn7P1qZyymhsHrH +5d/lN5R8i2gXS7f1tRdaXGqTgNcPXqAeiJ/kr9NTvmq1euyZjvtHucjHhEPezPMJuePedvy3/OXV +fNF/qGg+c/0ZpM7KbWx9a4X0wI1VhxRSoqwJ2zc6fWaaMAJQuXuDizxZCbB2eNfm7F+Z3lQQaDr3 +nKXV21SJmm0+GedgIQwCmVXC7OwIUd6HNtopYcvrhDhrrQcbKJR2JeieSv8AnHD8+9H0SJtG83Q+ +XlvlS5udPinuonSR0Hwy+nHxLqPhO5zYtD2r8mvJH5m+V/0x/jjzN/iL659W/R/76eb0PS9X1f75 +Vpz5p08MVel4q7FXYq7FXYq+Xv8AnNjya81joXnG3Sv1Vm0y/YCp4SEy25PgquJB82GKva/yY87J +5z/LXRNbaTneNALfUfEXVv8Au5SR25leY9mGKs2xV2KrZJI4o2kkYJGgLO7EBVUCpJJ6AYq+aPzm +/wCctrTTWn0L8vmjvL1ax3GvOA9vEehFsh2lYH9tvg8A1cVeMfl95AvPzCvLrzP5l1SW6iNwUueT +tJdTyqqsQ7tXgvFgPGmwp1zS9rdrflqjEXMj4OZpdL4m5Oz3O18seXrXSP0PDp0C6ZSjWhjVkb3c +NXk3ud842etzSyeIZHi73bDDAR4a2eaeb/yBsLlmvPK9x9QuQeX1OYs0JPX4JN3j/EfLN9ovaIj0 +5hfmP0j9XycLNoBzh8noHku+1y50OKLXrV7XWLT9xeB6FZGUCkyOvwsHG549DUds03aOLHHJxYiD +jluPLy8v1OXp5SMakPUE9IBBBFQdiDmCDTe841/8pLaHW7bzL5U42OqWkyzvYfZt5+JqyrT+6LrV +f5fl1zoNL21xQOLPvGQri6j39/3+9wMujo8UOY6PSB06U9s54uewnzt5H8z69qsV5pXme60W3jgW +F7WAyhWcO7GQ+nLGKkMB07Zt9BrsGGBjkxiZvnt5d7iZ8M5m4ypj/wDyqbz9/wBT/f8A/BXP/ZRm +d/K+k/1CPyj+pp/K5f55+15z518keZ/y91G01W01SZ2nLiPVrYyW8qTMDzQurFgXQnfl8Qrm90Pa +GLVxIrl/CXCz4JYiHv8A+Qeia/NDH5tufO155k0u+s3gGm3Tzt9XufUjZuQkmlUPHwZdh0NQaHfV +9qTgP3YgIyB57bhv04PO7eyZp3KYZ+afm/zN5Z0KGby5okmtanezC1gVAXSF3UlXkRPjYbdqDxYd +83Q6eGWR45cIG7TmmYjYMC8p/kVrGu6ovmj81b1tV1Njyi0YODBEOoWQp8FB/vuP4fEtXM7P2nGE +eDAKHf8Aj7y1QwEm5orzX+Rd9pepP5n/ACuvm0HWlq0mlhqWc46lFBqqV/kYFP8AVyODtMSHBnHF +Hv8Ax9/NM8BBuGxZB+VP5j+ZPMs9/ovmbQJ9J13R1Q3s3ErbPzNEoGPJWehIA5KQKhu2Ua7RwxgT +hK4yZYcplsRuHo2a1yHh35u+SvN1nNrXnD/lYl/omiIFli0yB7gBSEVFiiC3EacpHGwAG5zd6HPi +lw4/DEpd+3z5OJmhIXLi2eW/lJ+UXnn829Svtdl1ue0XTjGo127MtzM9ytDHHG5dXrGg5E8vh+Hx +zo4QERQFBwSSeb2z/oXX86P/AC8Gq/8AI2+/7Kskh6L+UP5dedPJv6W/xN5wu/Nf1/6v9U+tvO/1 +f0fV9Th68s3956i1pT7OKvRcVdirsVdirsVY/wCf/J9l5x8nar5bvKLFqMDRpKRX05R8UUlP8iRV +b6MVfLf/ADiz50vvJX5han+XXmGtsmoztDHE/SLU4Dw4jt++Qca9yEpir7ExVK/MnmbQvLOjXGs6 +5eR2Om2q8pZ5TT5KoG7M3RVUVJ6Yq+M/zS/PHzr+bWrnyv5Vt5rPy67fDZoaS3CqaerduDRU/wAi +vEd+RplWbNDFEymaiGUIGRoc0Nc/846uugI1vqXPX1BaRGFLVtv7tTTmtP5z18BnOw9pInLRj+77 ++vv/AB9rsD2eeHY+pV/Io6rofmDWPK2rwSWlzJEl3FBIKCsbem5UjZuYddxUHjke34xy4YZYGwDW +3n/YuhJjMxL2rOSdq7FXYq7FXYq7FXYq7FUt8w6Bp2v6Pc6VqCc7a5XiSPtIw3V0J6Mp3GZGl1M8 +GQTjzH2+TXlxicaLxryB5w1r8nPPM+i63yl8v3rKbrgCVKE0ju4V8R0ZR13HUDO3ywx67CJw59P1 +H8ebpgZYZ0X1xZXlpfWkN5ZyrPa3CLLBNGQyOjiqspHUEZzE4mJo8w54N7q2RS7FXYq73xVTuLi3 +treS4uJFht4VMk00hCoiKKszMdgAOpwxiSaHNBNPlfzv5j8wfnh+Yll5O8qBhoVtKTFKwIQqvwzX +047IgNEB33p9p6Z13Z2iGGNn6zz/AFOtz5eM+T7B8j+TdG8m+V7Hy7o8fCzso+Jc/blkO8ksh7s7 +bn7htTNi0J9irsVdirsVdirsVdirsVfLP/OXf5WXENxb/mXoKNHNCY4tbMNVdWQhbe7BG9RtGx/1 +PfFWefl3/wA5I+VdQ/KqTzN5mu0ttV0YLbavarT1Z7gqfSaCPbl9YCkgdFIb9la4q+cvNPm3z/8A +nr5uCUNnolo1YLRSxtrOIkgSSdPUmYd+p7cV6Yms1mPTw4pn3DqW3FhlkNB695O8l6J5U00Wemx/ +vHAN1duB6szDux8B2XoM4LXdoZNTK5cug7vx3u7w4I4xQT/MFvUJbGzluYbqSFGubfl6ExA5oHFG +AbrQjqMsjmkImIPplzDEwBIPUNahew2Nhc3s54wWsTzSt4JGpZj9wxw4zOYiP4iB81nLhBPc8w/J +Tzn5v8y3mqHV7oXFlaIhjHpojLJKxIAZQtQFQ9a50XbujwYYRMI8MifsH4DgaLNOZNmwHq+cy7F2 +KuxV2KuxV2KuxVjXnzyLpnm/SDZ3P7m7hq9leAVaJyO/ijftL/EDNj2d2jLTTsbxPMfjq4+o04yD +zeb/AJZ/mj5g/KrXZPKnmyKSTQS9QFq5t+Z/v7c/txP1ZR8x8VQet1Gmx6vGMmM+r8bF1UJyxS4Z +PqrTNT0/VLCDUNOuI7qyuVDwXETBkZT3BGczkxygeGQohzgQRYRWRZOxVSurq2tLaW6upUgtoVLz +TSMEREUVLMxoABhjEyNDcoJp8v8A5n/mrr/5n65D5E8hQTTadcy+kxQcZL1lNeTV+xbpTl8VNvia +nTOp7O7OGL1S+v7v2uvz5+LYcn0j+SX5N6V+Wvlv6uCl1r96FfV9RUGjMKlYoq7iKOu38x+I+A2z +jPR8VdirsVdirsVdirsVdirsVSDz3rvlfQ/KWp6h5oaMaGsDx3kUgDCZJFK+iqEjm0leIXvir81d +SfTpdTupdPhkt9MedzawyMJJI4WYmNGeihmCbV74q+q/y8tfLEHlOyPlsV06VefqGnqvJ0czH/fl +RQ+HQbUzzrtWeY5z4v1D5V5eTv8ATCAgOFkma5yHYq7FWIfm3qBsfy81mRftSxLbge08ixN/wrHN +r2Jj4tVHys/Z+txdZKsZSD/nH3TRb+S5rwj4767kYH/IjVYwP+CDZm+0mQnNGPQR+/8AAauz4+gn +zenZzrnuxV2KuxV2KuxV2KuxVjnnbyLovm3Tfqt+np3MYJtL1APUiY+Feqn9pe/zocz9B2jk00rj +vHqPx1aM+njkG/N4/ovmf8xfyX1w2rr9b0W4fkbVyxtLgDq8T0Jikp12r4gimdkPA12PiHP7R7/x +7nUETwyovpX8vvzc8m+eLZf0ZdCDUgKzaVcEJcKR1KitJF/ykr70O2aHVaDJhO4uPf8Ajk5ePNGX +vTXzl578seTtMOoa9eLboa+hAPimmYfsxRjdj+A7kZVp9LPMaiP1Mp5BEbvmXzJ54/Mb87vMcflj +y1ZyQ6SzhksENFCKf96L2YbcV60+yDQAM1Cep0eghgF85d/6nX5cxn7n1H+S35IaB+Wmkkxlb3zD +eIo1LVGHyJhgrukQbfxbqewGe0vSsVdirsVdirsVdirsVdirsVQup6np+l6fc6jqNwlrY2kbTXNx +KeKJGgqzMfYYq+HfzQ/MTzL+dvnmHSNFR4PLtm7fo+2eoUIKh7y5pX42BoB+yPhG5JajU6mGGBnM +7BnjxmZoPQ4Pyv8AK8fk1vK5i5W8g5yXVAJjcU2nr/MO3am3TOGl2xmOfxfs6V3ft73dDSQ4OH7X +kehaz5g/KfzbLpWqK0+jXLB5VQfDJGaqlxDU7MKfEv0HsR0uowYu0MAlA+ocvI9x/HmHXY5ywTo8 +n0Fp2o2OpWMN9YzLcWlwoeGZDUEH/Pcds4jNhljkYyFSDuYTEhY5KzTQoaPIqnwJAOCOOR3AKmQH +VyzQueKyKx8AQTiccgLIKiQPV5t/zkDctD5FijHS5voYm37BJJP1x5vPZwf4Qf6h+8OH2h/dj3p3 ++UNt9X/LnRkoQXjklNRQ/vJnf9TbZjdtyvVT+H3Bs0Y/dBmOalynYq7FXYq7FXYq7FXYq7FUHq+j +6ZrFhLYanbJdWkwo8Tjb2II3Vh2I3GXYNRPFLigaLCeMSFF4R50/JTXdCnOq+VpJby1ib1FjjJF5 +ARuCvGhenYr8Xt3zstB25jzenJ6Z/Yf1fF1OfRShvHcJFJ5F/M7zRY3PmTUI7m8eKMFHvZHa6mRe +0SvV2CjcdK/s1OZsu0NNimMVgHy5D39zQMGSQ4qfTP8AziV518hXnlX/AA3p1lBpPmi0XnqUIr6l +6F2+sq7lnfr8SV+A9AFIzYtD6BxV2KuxV2KuxV2KuxV2KuxV2KvjX/nI7847/wA+eYk/L/ye7XGj +QTiO4kgNRfXSnswNDBEeh6Egt0CnIZMkYRMpGgExiSaDJvy88h2PlDRRbJxl1G4o9/dAfbcDZVPX +gn7P3988/wC0+0Zamd8oDkP0+93um04xx82vOP5meVvKoMV7OZ7+lVsLejy+3PcKg/1j8q4dF2Tm +1G4HDDvP6O9c2qhj25l47r/mfzt+ak6aXovlxrmO3f1I47SF7meOuxLzAURT32UZ1/Z/ZcNNdEkn +n3fJ1OfUnJzDFvNXl7z35Lu/8P8AmCG60uQoLhbNpaxMsg+2nps0TVpQkHqKHcZseEXdbtFsbySH +Yqu9ST0/T5H068uFTx5UpWnjir2HyZ+T/wCfGr+U9O1/yreSS6VdKzWkEOo+iQI5HRlMcjxoPjjI +pXKMmmxT+qMT7wGcckhyJCOudA/5yq0IfvtM1G4VDuscNvqFadqwidj07HMXJ2Tpp84D4bfc2x1W +QdUvl/Oj8y9CmEPmHQ0iPQpc209pKT1/aNP+FzCyezunly4o/H9bbHX5Bzop1pv/ADkboslBqWkX +FsfG3dJx8/j9HNfl9mZfwTB94r9bkR7RHUMv0r82/wAvtSoserx28ndLoNb0/wBlIFT7mzWZuxdT +D+HiHlv9nP7HIhrMcutMst7i3uIlmt5Umib7MkbBlPyIqM1s8coGpAg+bkxkDuFTIJdirsVdirsV +dirH/PXm608q+XZ9Umo8391ZwH/dk7A8V+Qpyb2GZ/Z2iOoyiP8AD19zRqMwxxvq+cfL9n+Yf19/ +Omi29ytzYytfnU41CgPyLOyhqCTqeSqDt1FM7+WoxYyIGQBOwDoxjlIE0+1/yK/O7S/zJ0IpP6dp +5nsVA1LT1OzrsPrEAO5jYncdVOx/ZJyGt6jirsVdirsVdirsVdirsVfO/wDzlT+dh8vaa/kfQJ6a +7qUf+5S4jPxWtrINoxTpJMD8wm/7SnFWA/k3+W48v6eNZ1OL/c1ep8EbDe3hbfhQ9Hbq3h08a8V2 +52n4svCgfRHn5n9Q/HR3Gi03COI8yl/5qfm5LYTt5d8sP6mqM3pXd3GOZiY7elFStZa9T+z0+10v +7I7G4gMmUbdI/pP6mGr1demPzZX+UH/OJcl6I/MP5lNKZJj6sehB2EjV35XkoPKp68FNfFuq51wF +OqfT2j6Jo+i2Een6RZQafYxf3dtbRrFGPfigAqe5xVj35mflh5Y/MLy++k61CBKgLWGoIB69tKf2 +o2PY0HJejD6CFXwV+Z35WeaPy715tL1qHlbyFmsNRjB9C4jBoGU/st/Mh3X5UJVYdirsVfb3/OHX +mKPUfyrfSS9Z9EvpovTrUiK4/wBIRvYM7yD6MVe7YqsmhhniaKaNZYnFHjcBlI8CDtirDde/JX8q +Ne5HUvK1g0j15zQRC1lJPcyW/pOT9OKvMfMn/OF/5eXwZ9D1K+0aY/ZRit3AP9g/CT/krirzTVv+ +cTvzh8tSPdeVNVh1EDoLS4exuWp4rIVj/wCSpyGTHGYqQBHmmMiNwxq58/fnT5ImW382aVMYgeIO +oWzRch0pHcRhUfp1+LNVn7C02TcDhPl+rk5UNbkj1tlGgf8AOQHlS94x6rBNpUx6uR68P/BIOf8A +wmaPUezmWO+MiX2H9X2uZj7QifqFPRNK1vR9Wg9fTL2G9iHVoHV6V7NQ7H2OaTPpsmI1OJi5sMkZ +cjaNyhm7FXYqlGq+VNC1fULe91S2F69opW2hn+OFCxqzekfhLGg3avTbMzDrsuKBhA8N8yOfz/U0 +zwRlKzumyqqqFUAKBQKNgAO2YhJJttp84edta0nyl+Y0Gu+Qr/0NQtH9W4WAfuI5wfiRSDxdJBUO +lOPUd6D0PsqWc4R4w36d5Hm6HUiAn6H2P+TH5xaN+ZXlwXcIW11u0ATVdM5VMbnpJHXcxP8Asnt0 +PTNk470PFXYq7FXYq7FXYqwf84fzP078uvJtxrU/GXUJawaTZMf765YbVA34IPic+G3UjFXyR+U/ +lPUvNnmK589+ZXa65XDzRPKB/pF2Wq0h7cIz0AFK7D7NM5/tztLwo+HA+uXPyH6z+OjnaLT8R4jy +DOPzf89t5Y8v+hZScdX1HlHbEdY0A/eS/MVovufbNJ2J2f4+TikPRD7T3fr/AGubrM/BGhzKf/8A +OK/5HQWtjb/mF5ltxLqV3+90K2mBPoxHpdMD1kk6x+C/F1O3dukfTGKuxV2KpL5v8neXfN+hz6J5 +gs0vLCffi2zxuPsyROPiR17EfqxV8N/nR/zj/wCZfy5umvYeep+VpXpb6mq/FFyPwx3Kj7Ddg32W +7UO2KvKcVeu/84z/AJoQeRvPwi1KX0tC11Vs7+RjRIpA1YJ29kZipJ6KxPbFX3sCCKjcHocVbxV2 +KuxV2Kqc9vBcQvBcRrNDIOMkUihlYHsVNQcVeX+cP+cZ/wAovM3OQ6QNIvH/AOPrSmFsQf8AjDRo +D/yLrirw/wA0f84fef8AQZ21DyRrKal6dTHEWNhejwVH5GJvmXT5ZGURIURYSCRyYf8A8rL/ADW8 +jXo03zjpUslK8Y7+JreVlXasU6rxdf8AKo3zzT6rsHBk3j6D5cvl+qnLx62cee7P/LX5zeSdbKxS +XJ0y7bb0byiKT/kygmP5VIPtnO6rsLPi3iOOPlz+X6rc/HrYS57FnSsrKGUhlIqCNwRmmIINFywW +8CWLebfLnmTzCG0+PVV0jRm2n+rK0lzOpG6s7FFjXtRa17nembXRavBp/VwmeTz2A93P5uLmxTnt +dRSjR/yO8g6cVea2l1GVTUPdyEiv+pH6aEfMHL83tBqJ/TUfcP12whocY57sS80+XfMH5YeaLfz3 +5JdorSKStxbAExxBz8UUigjlbydP8n58Tm97H7WGccE/7wf7L9vf8/dhavS8BsfT9z6x/Kf81NB/ +MbyzHq2nEQXsVI9U0xmDSW03genJHpVHpuPAggb1wmbYq7FXYq7FVK6ure0tprq5lWG2gRpZ5nIV +ERByZmJ2AAFTir4W89eZtV/PD81xHas8Xlyw5RWXb0bJGHqTsDt6s7U/4Vei1zE12rjp8Rmfh5lt +w4jOVB7Zp2n2enWMFjZxiG1tkWKGMdAqig655xmyyyTM5G5F6CEREUOTxPS9Gb81/wA/YNJlLNo1 +tMUuKbUsrEky0I6es9QD25jPQ+zNL4OCMevM+8/inQ6nJxzJfdcUUUUSRRIscUahY41AVVVRQAAb +AAZntC/FXYq7FXYqo3dnaXtrLaXkKXFrOpjnglUOjowoVZWqCD74q+T/AM7f+cTri0a48wfl7E09 +pvJdeX6lpY+5NqTu6/8AFZ+Ifs16BV8xyRyRSNHIpSRCVdGBDBgaEEHoRiqLv9b1nUEjS/v7m7SF +VjhWeV5QiIOKqocmgUbADFU/8k/mp588l38N1oOrzwxREcrCR2ktJFH7MkDHgRTaoow7EYq/Qb8v +POFv5y8laR5mt4/RXUoBI8NeXpyqxjlQNtULIjCuKsixV2KuxV2KuxVB6rpGlavZSWGq2cF/ZS7S +W1zGssbfNHBGKvD/AD5/zh75B1r1Lny1PL5cvmqREtbizY/8YnYOlT/K9B/LirxDWPy7/Pr8pmea +GKW90OI8nuLOt5ZcQakvERzhHixVfnmJqdDhzj1xvz6/Ntx5pw5FNvKv/OQWi3fCDzDbNp0/Q3UI +aWAmnUqKyJv2+L55zWr9nJDfEeLyPP58vudhi7QB2kKepWGo6fqNst1YXMd1bP8AZmhcOp+lSc57 +LhnjPDMGJ83YRmJCwbROVMlk0MU8LwzIJIZVKSRsKqysKEEHqCMlCZiQRsQggEUXiepWHmf8m/OM +PnDyiS+jSH07i3erxhHYFrafuY2oOD9QadwCe77J7UGojwy2yD7fN0mq0xxmx9L7C/Lr8wvL/n3y +zBr+iyExSfBc2z/3tvOAC8Ug8RXY9CNxm5cRk+KuxV2Kvm7/AJzA/NOTTNHg8haVKRf6ugn1ZkJ5 +JacqJDt3mdTyH8op0bFUg/KjyOvlfy2n1iMDVr8LNfsaVXb4Ia/8Vg7/AOVXOB7Z1/j5aH0R5fpL +vNJg4I2eZZRr1/8Ao/Q9Rv8A/lktZp/+RUZf+Ga7SwE8sInkZAfa35ZVEnyYp/zg/o0Ump+atccV +mghtbKJu/Gd3ll/GBM9PecfWeKuxV2KuxV2KuxV2KvOfPf5Aflj521UatrGmtHqRFJ7m0kMDTdKG +Xjs7CmzUr+GKsb/6FD/Jv/lmvv8ApLb+mKu/6FD/ACb/AOWa+/6S2/pir0/yZ5Q0byf5as/LmirI +mmWPqfV1lcyOPWleZ6sevxyHFU7xV2KuxV2KuxV2KuxV2KvMfzC/5x1/LLzr6lzcaf8AovVn3/Se +ncYJGbrWSOhikr3LLy9xir5080f846/nH+XVzJqnlK6k1nT1NTLpwYXHFenrWR58/kvMZTmwQyx4 +ZgSDKEzE2DSH8r/85ABZRZea7IwSoeD3lup+FgaH1YT8Qp34/wDA5zes9nBzwn4H9B/X83Y4u0Ok +w9b0nWdK1e0W80y7iu7ZukkTBgD4Hup9jvnM59PkxS4ZgxLsYZIyFg2q31jaX9pNZ3kKz2s6lJoX +FVZT2ORxZZY5CUTUgmURIUeTxy2svzN/KLzbcaj5Eil1DS9RRkNuIZLqMqDVUnij35Rk/A+3z3YZ +3Wg7YxZYXOQhMc7NfK/wHS59JKMthYZVB/zlL+eWlMZNc8owTWiEmRzaXlsaClaS83jp/sTmxx6r +FM1GUZe4guPLHIcwQ9C8jf8AOYH5ea7NFaa9bzeW7uUhRLMwns+RNADOgVl+bxhR3OXsHulvcW9z +BHcW0qTW8yh4Zo2Do6MKqysKggjoRir849U/MZtX/M6688azZnUTNdNcxWTSekFVPhtk5cZPhhVV +FKb0yjU4pZMZjE8JPVnjkIyBItnP/Qyn/fuf9Pv/AF4zm/8AQx/tn+x/487D+Uv6P2/sQWuf85A/ +pXRNQ0z9A+j9etprb1vrfLh60ZTlx9Fa05VpXLcHs74eSM+O+Eg/T3f5zGev4okcPPz/AGPU/wDn +B7UUbTvNmmkgPFNaXCjuRIsqH7vTH350zrn1DirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV +dirsVdirsVdirBPzB/JP8uvPivJremKmpFaJqtofQul2oKuopJTsJFYYq+afOP8AzjN+afkK7fWP +JF7LrNjGeX+iVjvVUb0ktqlZh/qcq/yjK8uKGSPDIAjzZRkYmwl/lf8AP1opf0f5vsmgnjb05LyB +CCrA0PqwH4lI78f+BzmtZ7OA74T8D+g/r+bsMPaHSfzet6TrOlavZreaZdR3ds3SSJgwB8D3B9jv +nMZ9PkxS4ZgxLsoZIyFg2jMpZsJ87flR5Z8zxSTLCthqxBKX0Kgcm/4uQUEg9/te+bjQds5cBAke +KHcf0H8BxM+kjPlsWPfkJ+aPmL8t/PS+QfNEjHQbycWyo7FktbiZh6U8LH/dMpYcxsN+WxBr3OHN +HLATibiXSzgYmjzfWP8AyrzyB/1LOlf9INt/zRlrF3/KvPIH/Us6V/0g23/NGKu/5V55A/6lnSv+ +kG2/5oxVHaV5Z8uaRJJJpOlWenySgLK9rbxQMyg1AYxqtRiqZYq7FXYq7FXYq7FXYq7FXYq7FXYq +7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqwT8xvyU/L/AM/xFtbsBHqQXjFq1pSG6XsKuARIB2EisB2x +V856t/ziZ+bHl/VpT5M1qO4sZhtcpcPYT0B2SVFJBp4hj8hleTFCYqQEh5i2UZGPI0of9C+f85Nf +9XeT/uLS/wDNWUfkNP8A6nD/AEo/Uz8ef84/N3/Qvn/OTX/V3k/7i0v/ADVj+Q0/+pw/0o/Uvjz/ +AJx+aX3n/OK/576ldpcalLBdTgKguLi/MzqoNQAzVagqTTMjHijAVECI8tmEpEmybf/Z + + + + + + + image/svg+xml + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eJzdffle8sqy6H0B3gFUFGQwEyEBB2YHUEFwwJlJRJlkWGuv88d59lvVSUgICWmQ75x1716/7aed +Tnd1dXXN1fF6iuVQsjmot0J8mHG7vN70qFWbDEYxN2l1n3e70/FkhE2+G7+bZcMMdEqeS29qx7vW +aNwZ9GPkEXmYw7d951e565vTrN/t80NbpTPptqB1Mug1apPw+K+2X5sLXs7UJvAwciAfMKKbZWJ8 +1J28hOepwbTf7PTbqcF/YPyo6OYZzi3AU0GKwuOzzk1rbO4TjrK8jB3DnAy/CLwYluBNQYInDL6V +GTSmvVZ/UhwNGq3xOD3oDkbjmDv9T63vvqy14UnNXW11u4O/3alurfHtgtVG3nKdbgsW1qtN3FFc +ZfKcfyOv3o7hHXgdf8fm6Nt5D1rKrckEoIKBESXpy2reOB9Aqv7ne7pptTsEw4CIF78ycqXVG3YB +KWRRPCCFl0XtX7UHwEOehqJsmJdlGfAmhiMy9BMlPiwwjAC/RMgj5Q193a2/Oq2/Y+6rQb+lLC45 +mpQ7/9XCqRg3xzBK68202xrd9jsTWASHTbKy4stBs9VVm8i7uW6NLJT8x+o/lQ6V2qjdmsBODbrT +CaEUSZvhator1P5pjfQJroetfmVwR+ALiUJYFMWIWxQY5Rc2HHFLouyOMoA6ScEgC8tUp2TJtKwy +No6E42gTRHHvi7Az16NOu9OPsYLoDnHYint2Ouo09S2Lcm5J+UHWEZYM/5e1/ysAw9onk1Zf2eZs +v5ke9BDJY6Re2Ng+7Hp30FaezX4nT2C66VCBlfz9BvtRHHX6CIPrijyR3ordKTw6HQ2mw/P+x8Dl +U05lEScd9a/78MunOzWajj/dlcGgC6dtroP6SBkFH44mxt5L54C+9uPrA601drrW7Xbao9rws9Ow +Gt7i+Wweu3eXTgjbNGrpY5A/Z/8ufbPcIKi0gnL+0WxwizeWz/BPrz7odsY9fWBDi/67E0XARnVb +/eZ4Nozypw5YofOX1rh8sEzrA1idYWtJa7b/V6s7GBrQOGup9Zvu+9poaDcsQvfR6TcBK+VpZ9LS +N3rQGyIDd5c/a0NsXuipnBA4PcbzEQotPzgrvyArT5ARTv7ptsaug3x/8Hef/OGOuXxPgJLatDt5 +8bsPrmq9ljvoOih3gEm3tC6M+9rFqDzwG367cWn8MO/SuCLjfvgH/riAX76g6W+34L50P70w7ia0 +Pty4kIE9NF0HxRoA54673AcwLfxLAIQV6eA5rrFY6wI7axEginWXnbhBkMauhdZiY/bGt+XTYmoG +gjbTKvgtwHBGpC6skHRYZyNZRnmkHBsc5v+ozTCQqdFmcBVWTV6CclJzed8OtL9hr/GvTgOxURv9 +o/z9cFm4ArlI/vBtN9W+QC3lCQzedvv+0+v2oUMIf/SBgvxAQt436+d/1bpTtYPsPjiHOeceT/4Z +qk8PkqNRzQqCXmtSawLgvweAXQ+Av2qjTq3eRT1o/G8A4n8dhv9JLMT1Po3PTrc5avXVPiayNXQE +mTXq1KcTBDRIHgUX1xIb15Dn4ZH4H95Y6iXNQ4zvOIPp2+2P3xpg5wx6cZvOBpi5/9lt0NawuB3k +QewvuuUBHY7/rYvDNQRpyHFNKoC1A7leEYQ44areIeYk++9DlXEVi8TQHTS+W03n9fXB6vv3rU2D +/k9SwQq84N98WCiRNL/28cff/2sScNztNP6/EH9kIeXBdNRoEa/Tv3JN8yD/4wjizFN2cNOqdf81 +pP6PpcBzXM3MAfjvWs1/rFbzd6c5+XRcEScyYVbk2H/ZilTgF1f12eq0P53VbVYSwgLL/9uWpUG/ +uK76YALqYaH1MVEciM4rdB+kBoN/z9IWF/AvEbYgm/4fl7WbEzgbAt7ggMAWRsVd8pxl3TM/BnFA +uwu1fntaa7fcxcFwOjSRLnmhOGqNW6O/Wu5K6z8Td7bZmdTqnW5norJoMRLhI7MJZHdtNKkPaqOm +u4HBAjfrHmmKnWPP9qilrdexb31GGRFO4CT7rpwOgGNPAwCOfesLQnyx2zzp4vPJqNYfD2uwr41/ +YLpO0z3u/Fdrtk0a2mX3sDZsjeBhb9olfjdNWjMax8RO19PJcDpx39TGk9ao81+ko1sPtajgRebe +uWyNPx3eYOb2X6Mldwd61SYtWHmL2EhLO3/3QaUfAHBtdAOrx/3pstXsTHuGCV8MJ9+KPNX4CqCC +kOHEbbB/TEdCIxfAvIr4qIb55rATNkFb63bGpqZebfytolnUMDasNXWzJHnuTk4ngxn2tP1nDAeM +cX/MQB6RfqG/Wo0JkEy91q31G4t7PfcKYKzb6bfcEzhrdD3Hk9HgWzv7rE3nRrczBJJE581/4Dy0 +AW0Obwy1Uz/4qzUaooN0xl4ANY3BqNlqLm6D++BqMJl7vCrvcRhOp5YDne8djJqjcVhx4JgV74Vu +tX5/MJmtXdnlhU4aHsbjeQ662HHabzh0AXkHJ6ZJdQSML/9nGNYlpdXo0GEwbE4dOoydRmgM5tmY +qQOSzvIOgz6QyEShw6VzqT112iasyaonMOJ5lsQzNj1H5p7RiHXHueNnufNDZd+X7zp0AjY038/A +lc1dP2vN1qi1fLwuiyezNlnaCXA3Ia6bpX16eGzHRkZu1a/fagPj/2v5YPUOnsF5CWYGvPVXq2s/ +yEd/Eh5P6+MlC8Muze5w9DGY8RcrKlO69UDbUbUDS3S3e9/hXm30PR58fIQVdZe6+0jX+yl6TwZD +6r5d0LhnCLDpDPyh1TRDTdHdADVF7xnUFH3noF7ce+xLNJx6bbSMuLHfyBA9dOg6BGHQ6X8MnGYe +GVZi3YUsRO0T5iK2C262PlCKGsxZa2ZMOn8N6hNMZHLsqIiij0532RHDjmMMdjr0mZMfVr0ao2Z4 +Ahq5ppFZnSDsM240+ssOo9Jn2G38Y9BrFvGmdKt1W+G/KPt9LiE77DUYtbWxlvZRx7Fi8NhlOBh3 +lhMZ9oL9Hn4ORv+lcraoXb/BqIO5YA4DdkfhmYJUx3Sx5X01WTkcTJYcG+ypMztrOgNadFAPsEe9 +M+nVhmYRadebrKI2Vl6i6DpYTuGzfnXVW7qsY7M17rT7TugeDkdhYkItoxbs9AlMbNxaxhtJt7/p +uhndQksGc2Qi0Enfs2iUDwuWjAm6dTCJcE4cROSIU3eDOGClsLVsmnWeSQNWdOqqC4OozNl1NeJI +ZG27GZBkxaewS1NJC1nCFqGTs7Y/nnTVXsNh035G7KbOOOtnPyB0wZPZtfLxL/RF2m+N5lyCS6dX ++muGgiHlyGoGEL/dFjGVdJM4PnPZYAJRUuvsRpuKyryyO504WW3icNZHoA6Oxi0cbWS/YOw5/u4M +gVv2v504HCoEcNzbluu7GNQxvcywOt0TA52yxbL72mS8zvlP1D4FtKIxexGz2IiPa6kHRX3rdFRr +ooAgbyk+FTtDZPaO4jc4uFP8ASk7f4AKumrfV3RrybZP2c4HoHRLo/WfVq3/G6P1T+ORwRGWuGFY +o9eqP9D9Be5On7gcUCpbuWwWqc/3ZEg3d69B/1Z2Cq6hmMm9pYmN1TG6Lq3IU+uueT0NEKHrE8BI +14aKA7TTWmKyaOOcItbg6FQ+p716v9bpLpGD2juYtwz/5pZKV61zDojqvlXHd5yhIQncmcHffSWR +J9/pNw0kTvuamdI5zkols3mZpMcn64O/dFtu+atp3arV4V2+0/NvlaY1fc+5iOOEmFtf1r17yzZ3 +VPtndWzOv7UaMuffXQWX+ObKqDS9tAIm8U16RF4O+oPG52jQa1mh09r5s+xdM1KFpRuCI9gjVaCa +2xK1y4+i8gJIHudDXhl1epfoUXDuCvydsich9tRSA37GDQEl50sNc51vEiUGQajMwnN2Jrh5efct +BzeM9sI1UdtzgHhA39+D0XdhpqKu9l7KyU1k++bNuqBWlrphtNdS6MAoLPcdzfW9cTBR5jqvAIMR +Q8voWQG4019iAWtds716q3meThdHxILUpOjSU16e1hGNg/7kBo1EZ3hmqh+FCFW0m4ohNkelHi0Z +C54rmtKVIdNmKbLNL17W/rNED6UaodO31Ulp3lf01JTJb079OmqdqtKp6JyrD6Hqt2WH0ILD6xVj +LM1R4Us2RoN6baLUjc3MDuihrmqmdppNDtkc3hrW+pp7XJOx5btTJGGFmCcLHjv1cWHQqC3OAA/J +wVGsCJWm9GcAXqOju/4NM2b7jYEerxX0B6TUQufSM00eHpHyHKRdOBANi+daheLik2L7Y7HxoWZO +LcDpu53GDKz4ojmgF77M12Lgjik1Griz2jMX2UljC5oYyXL6/FyKZGDcJlbteAPHYmgnMfY/bGXy +F42PnL/EJRM/qVefcHL9fhy955lmvBXz9smf8fPx4CP3Xpju5TyBJ8bUFji5qx8wXHcSSd5UcpVE +bPgii49i79HlPQy95wZkMJgvPk6Wp7e+ZL/eHqvvHP/0kvn77PZodFzrn3bvvuqp98tSMhnssy/x +E/ZOymw3p9lM+uz5hQwVOD4aeoUxv1MKnHxOeAKIy0sBygqAHNWTweHVRSIvj4+ls8P7cG7wKNy5 +vNnR8yOTecxVK7mj5FHDCp7jof9wCBOchdLcztF7JjxN3Cajz29VsTpki7nd0kNXna+R3M18DP1s +snIxmeptLq/Smn/wT2Cci2kmfP15OBoJmQ7DiVvDxN1eeUfpzjLFWs4/2a1lgy9XBykxyG2p47wP +EqNRfFwBeIPnDBv6iunIiqdu0i2XdyzlJnfc6+B7Vyy19gMRT9p/LRyWYpXA0Y34OXphxodhviBz +geNTz64w5saXAM2dFD4YS6eC9BP/gj/9fqa5W83MT/o8erl8LpFJgcbmp4V3o6+R2Plr2HLS152r +gu2kYid/6rWa1OUdjQ49vtGY9Y6s1jqWiuyzsMXF9q0mHe8FL0M2k0Y+fbW9apZM6vIurFXwPwcO +uXbJctKt3KuwfTvsFqwmZXKpfMJqUpcXphW3d/oj/5E1goXqK5P7uCpbT3rqOdxlL94qlpOennEV +Mime/UUEc4/HlXcyKbufrGfnd/V+9Dw9LuCk8cU99VX5py7rh0lDQX1SmEUhpQKTUtda3NszTRqJ +9N6GdpO+jV4++xWbSRM1MZrbYV1e07QqKZ2839hNerbD++LP1pMeel7G25+tG9OkwGGUaUtp//HP +Tq9gNWkg3o0d20wa+dw/eUxcW08qVKtMTmaugMas1rqVa0d3bnrctdWkTO7lJWczqbjt/e5fpk2T +wizatDXmNPh+Zz3pKZPca/miVUv0TraDJ+qk1ZDPhN6TK+Ho2aWcVTb7/J2bW+vjIVOIhlic9HBh +0rPWQLyphTiYlAmZV1p4eqyZJiWzqGuNfjdzr3aTZpjL/RfZetLzn1jia3R1YzlpOb7Hw6m0Xqu4 +nW+VecZm0qcQU37zb1lPmj9rXT09+n36pC6vYdq7vX7bdtLyZ+m9bjfpKXOXHx5aTw== + + + WuC9Lu9tLnF4ZLnWu+HFlu2kd2+nWxO7Sa+Z+5N8Rp8U1mKY9vI4+/ZaenmxnPTl+vvcdtKvaqSZ +s5n0Gbgl8zLuhqzXevU17F3LEm856dt5qG876chbDnj0SVGKGc/qLZPt9C4sJ5WuQluexEsoC5Py +YfOhmb5F39RJ67zfdGhcXv9jobZPpuX2jn1n82stMO/7sSROemyaFIb9+tGYvnhgnnQ82D1SJ52c +BGBf5tfqea49+ZVJU1X2fJ4VBkfj22MPTppY5EnnIW2lh6xpUsBY/GxLVKY9YfMhEysMDoUrRdLw +O7F0fn7SndGoVu/jpCnzSkfJ1kCj3hTQmFmUR75iqqQ5iZXCJgRvDVrvFUWmtmpv4jxIZ7e7r4OY +1VMikSNn1RLbu7N7+5M5e/dObZ8C683s2jyFHdgNpL0qt2RaX62o6bkosW8a3ONvyfy0/7n1YPs0 +WjyPPetPF3Zf4vZv3m3flj5rr3u2T5Pc7mPD6qmqwxQC/RPO9u1C/fojbvv0eqtRP7N5Kp3tnh3e +jjWMfez9yKa3bwMdTT39YLdi5qf1i3Lf9uldJvA90p8uYOzeWz/w2L59/5yJHdk+ffe+RnesnqoY ++5oUh2e2b3/fcamS7dPed+741e4poKoUj8wwtvj8ghOOH2yfNvr1csHu6a5n9/x53x5ju9nkZb1l ++/YFd7LF2j1Nergdf8wWY5EzJnu0r6065oubznSgUhqfqE/T4UPT08r76X7S+FQI3iBDKSnGXDq0 +nwbdcjJ8fUm3Pyvo1EseHctnO0hZ9z7VWj5pxGzMvvFD4u7jtpysVLz3hEUlK5dNIVsbPXkDqcH4 +Sm8Du7I2etwjfC7GSp4rwsw8+/k46wlmbu49wbvXsif41qx4fE/+Kf5WBBL8TntC+bfIolFYbSdL +fFkCqNMBsE4H3+JOVP5AS3yf82h25YuUe5s81xLxIbuVuQhsR7Sl7faSg8wrkOm2vMXtHRWPM639 +rJecOzRnnjQsWvdzKT3R2pKX9yT9jmPpp6pjPzDD6js333o/l9e257730DNwHFHcpl0L2GLRG/8L +xYg7fT7+RtHPe925rFGsRdxGod6gGHHvvB5ua/22e7n0x4V0cHnRisKf+9vJ6GOXV2xkPwjHj0OF +Tpgx101Wkv0ccxER9hWyQfcHWMsRThe84lZVuMw+Nn4+DjpHdb/4KBbOVLs5ujuaCeB0cvBz60cO +s7glft/JU3c5eGhLv9AAt5WrhY1eBVvwmFz+sGgCz3I3hKvMuxVwhFvq4FXfqMA73RFpgDstbT8a +dH478KSzOWKxxV31ZjlwQGPK1l7l72jAy2ZvczPcZZLl4PcODFCqHnS2Y8G5CQKHZhqLGUBh9yKv +mY9KhkeQBVzaob5SNnjLhvRJR1M+zVBMCjr//LREO15z0kBsMMnipEOCFoabJj7Tn8Kbui+gah4P +M9lGsSJqbsX2NNuoth6UNo2P5zPnzSPQlHLTbjReui6ib5GbPb3B38AI/5bPAergdy59EiuTbTdY +FuPA8XF2D6At7yOMYbLq46GvOVZdNfMORmWlbW83ebt9hFoBs5Usdz2jXFa6OVAHvWr8BI6LuwOY +BYWZOPGxp+qLO82MojYDZKmDz1bGq/wAOriHwYqiam3BfLMtIcvIoJMhN7+MjMGrQJbhNfzAmWPv +P8WYQbTOgfezEnDkVC4Fr86fWYFnAdy+LXC4FhW8MQ14hEIJVaojXkh2y53q42m7b7tg+HGjLFfx +3VgsF4yrwvLlulbZjb2tNUlF5ckLu3Fa7CERt/EgbStcR7wgauyddCyf3hbBctr1kh/c3glzjoCc +z4YqaZyvKELnpwzsCxhId5T7S0F8A3Y/9ZVjWDnyleATj6jB7fpmvosK04Rd9Xq1H8K+eiCJy2Au +AhF7H43rsE3xEC0CXXSn7fT55zcI1LVxFYWoJz/++oDoCORSj/IF+i3nULgSAi042o0VR5udympw +aMYyM3xNr8fRsgjNqY4RVSJb4+Q0v4sz31jufvb5emLaq8jwQC6a9oqwd5fXlsHPjXjnoRhR/VF7 +yCCCzmx3/zXL78Tzhbm92t6z3KtWMbyr7osFxk5ipcvNYCwToNzJXZfKD615w2sWHQX3Jvm6Okgu +LwIVpgXKASSwWatWIFnISic8MU4gDQJHugpBWIFyXi6WgJcOPy3F2K6uihhPL3FeamC6vBbnt7xE +I6lzCyLf+fSSfbE8vzkrxcpi43Xd6omMqAbW5sZzeZURT3zZPBUpGYTMpWzNI2G5CmOenTqiw5jO +nU+yVv3mUG2giNrWJbcci3he5mhCXzq8PTmdLX2ojy1VdvcuTyvPX02GTT23M+Gb26Ae7iczw1C3 +I50nqbLSSiYtV2PnRnwYL5dxLu8cITrrWd/SZHW9zeVdOuJ0M5rgTIp9yx6qEY/q+/o5sKJa7HyK +3v0LM082SXYa82JuXz63N70v8s6m90Wmsm5W2RdppMhSJ5UGjVCCVFXtOrXhtM1TXWt1eZeqXTRM +St3u07uB7eYAT17nGN4tCJmlqHR5nY/hiK3t7J39BpUmHQaQSafBroLK+hilmKOWvbJhfmsSgzN7 +n2BnckxlXNKpsWe6GutAY7pqb6lscKmHT7PSaYUMl8HosN79yQmVNbn0aJowdkLFPuiM5zPdeP4t +xqpbu5vB2PGYjvXMrKlFDV3RYYAcTsv9lSxHW5BWtpGtzQYEqTpcCSQlwmsNFBVfoQDpbUR19uct +bDulun1moVQv8Y/NLOxyfD70dKMNe+hLRl89Ye5lXE+lP6Nnw0w+/5PSgjVk0q9zprlXyxJkuLz0 +RjjJFrIg55dx34EBuLwWODmzwcnX+Yp7pQHnMqd5auBNHNSSleSey8u9TLzUJGBlZpuWu2hk0/iU +bHdjEtijBc5FsxuYg3C7qgfIaN3M8eQTX2ZixSDWJ75PbhZ7XUUu2nD58+UuNKOmREvOq7vQiAZr +YyAiAokT7TcIJAxu5k9WtY97eyP8hL1YMGcoXWiWnt4LkxNtTe8LvxPz7ZC9Aj7m7ESjON0wYtgs +m/XxFnbf8XT3LlZ3odlgbN6JtjbG5B9m8bys46/qXVC40Fy0QPG/caGpUR4FKDsn2sp4iphAAilG +QfNzvpI5igezgcI561qmOqgpJ9eGIOJJrDixCyLmkc6zlB5FZ/89UOD2SttkoOR52hnmTT4um2NB +ZTKTUwkjvkxTeZqDhj+WSxX+5DbmM+0V6JbWrnT/LuECdhjzbwpjFnJ4HcI+ufXyixHedRgAgPSx +9/NgC9JcJNERKPNpowXJZO8jUAvi1tYba61Pz+2fxypiZUtZ1j5vC1MfyWc7btLQdT72ULY9uusE +3k6LPTb7Mj416fxrBHPSALdg1o+s+RitzEXCPqDWLubtdEtZiTAe0YTWHE4voo0/uatc0u2+E9r8 +PmcPPM25I7Sx4M2jXd+8hwRWuPwQ0x5h3ES/brj9msb8C4FxC4pw0UpfMiJtlM48noHGLGIfrz9L +Ylen5T6toHN5KUQd7n7lN+GmmY08B+MqLNPrwDJxPDgvjrFCpxEtnBqob/p1Xcflndd20sARYpTH +giJ95OGWGCmEJ//2bIy/HRjcjJJpIyPAAFZXeHAtNioPwugQIaTkTrd4XjZhqyBIgUX/prpIrLBb +gaVgrk1w9fXNPIomwlj0TK4lX+4GxFzZEI0FFnmN0S9AMiHnA8eOfBPR5hjlmQsbu+hNF8SibeAY +xZL9hilnf6WIRxoenI9W2jU7fzLAvWn75eFuo1kEAKHJ8WCVRUB3crLPz2YHqi3aXN5l5A7bvREj +BuULUMeiH3HN9Vkn8Gj1lSvwvjsr7+HaNLZW7p21WCYV3DiiTbThN7EGZGEBIpZdXqtMZmAuBUqc +0IhlktO7Ce8hws3ScRh6sfz8s5JYdjl4IhBG4ddiWeEw9xsSywBSyEos28Qslh++2tblatlzth4S +WN+mxPLzD3KYTfgRCbJsxbKuw6wqlq3yuezEMkpkLcePTiy/jZaI5ZiPnlvSiGUA7snJWG9Nbg8s +rSS7wOrDg0Vm/9JsRf1sl+O2PPlttHG5/7BZuf82WkHu2/mr5rdb3KgljnlDNLa9YTw7Xx9usrMj +ZEY7NA4/Jb7vfPZpMl2tvH5c6qGaJF4/l3cxMv9Q3azXD/OUdL+fDcGumEqKKoSZ9VhERqhSSY1k +kXr43lq+k2pkhNqtCIDSOpUNbkXAmE1oGGGcmAP/zoqMJYtSzn6VXiLTkG59bFKvl2baL0tRQtwd +OKrXKuXMPL3OZz/18OboZFhMTCdYnJf7qtjSdYpUrPMafEiWgw+D5E2/nk+FpMH1Ap5md2iZb8vi +xm1PqF96c3mxejrrCSX5V0/oQs6qhdMXctUT/Pyq4I8wtuWw1DpN6q3xBonDZPm795Ft3J80cC1Z +rMisZmrl40LOPzpD9+rOXi7zHdrCKmpB1ZSUAuviWaWYDYz5XV3Sikdf9fNsPZRYLLa9OrnR92pJ +v+IBM1f3at+zFCtS9BtN38Mhq34u72LPGEsz81bu1XLExX7NA7q1bOW+KGce8lRrwQuyIlQjnoZk +Qz+7StnROBYYG+4hsS/kFR4+C7P7CewKeQ+3fLaFvBizMGwJG6YAL3AY7904Aidub/+I02c98m5f +Z/xEV6PdcyggV28GY3KDSoUCvN1C67PpXATdMgEH1qtlfftV6YECd0z26umUYmNd3rnCf3vwSmsX +Ru8fHRhojLoeG6ux4ytN6vIa1o/3Da0zqeAvKpOqroXCQyKW89ZUGJRYktgflhOHnckDjn2bDew3 +w8lr9uQe2qZbJhVrSTgmNx052vu6OWCvwVsZTcXdnl0aRkbX6hwyh/cpyygX6hnnbT9DJNEYMLWv +Gl1wp9AAZ2clFXcXsOxcqGILHHKYhYKt3yTwZywMroy5sn4Fk6u4R12XQ1fDu6gTrkYq8xHe4u6A +LkccmB5dyXJghXxLGuKjrUyZVwjtq74tAtoOCCT5lsuqvqkSkp0QiLOoKFyIBv7igJgSy5ZU2C1B +oBVHqwb7dhwtq9wISmGp0nG0atCSMa/ugX/Nru6gUSq57OLtMOJGEsu03c8+Vx3sXCofZTVkmYq3 +DsZWj8lYZ9pjIIXWs+NYCQ1HeANRUQSJMvJOA5RTloS9V8geY/YRiOX2sNXpVXZfO791bmB3fnO/ +Ob0WOSR1jio9nDqMUue3qHMUKUq3csSvZ3Xq1PtgLTyFzj7mWo62It5lsXTHCh7QtxM2FTzNU72C +Z3nNCH3NqWNRhjqUGk2gKpSkUist9TbLvL5Tytt6qEs5qXXL+XNgX1tsWcA4x5NpzIu5fVmUPr/e +F+dLgFbdFzqVdaV9obsJaJVqZXPYCj0kv7mcCauVnSuZXKsdQ0rl1YzK2W3zVoeG7k4gClSqOgwi +0zH6uCoq9Zqm5TWJKxvmk+MfKzGo172aBeH6hvnk2CwG7Whsptpb00H7bCWTY3anyg== + + + wu6fUTK4oKJdvHOv77ZHU8OYhfm8tvE8OTGL+bUxRjK1N4IxOtaj62N2xDDGU7kop5dYjvYgrWwj +2wK1GBCkAsngUZwBRcVXKEDSbbblZ3/ewrYJUGKQaSFlxto/ZrKwna65GX7YX3PzMm6bkxhcXmcj +fEkRNOU2uSjLZH/MVudKwJki71/ny1NNV1U/YTxUc2jvhXOq3+JeJj77GxbsfUpLarRXYS5OudZY +tLyOB8jCulF48vlyBrH6ck1aiOXtQBZycUnJMoU8cy0e8SUIdDjitgi01GDPraozV0TgnO7h8qrV +mezF2M4Iv/i1C22+sp7NL5gf69X1Utw/ttLphhGdXWgu+urMnRhrvuJgreonwJhDSRZ9JTR93oVT +2TF1FcbyGw6xEnoThV0uLwL1+xxUFSQbF9oKt50Y9+9iungVwpJct1m2rVOe1oN9EBFLQilreaj8 +9/k1k5mNGRFaES1Lk51EfRXCyW10b+Gum5XuEzVV5tpehbBaLU9+zTxmK4x5Hd2vlJXQs6sQDBHe +Ncuzac6ai648+5cpzKq9j0DRJIFSpoDmrSNWK2bSWSYuF3u2GjreBV17o6rGpzu4WFMaNitda90S +gHCvFLYy1FjZlomWf51XvFApvJHbgBFtiSnt7juhjTZ25aIqX/5tPZGijZNiY+p7ih0rvReM7LVp +bKWbEVzOhb44In2Zr6U0V2ZZLPT9avlsv4zANHePaOr4dEE3k/u2NxB8tRwvTHYMNyla3wxGjr7e +XB/Rtn4dxjugvrfHdkSr+vWRla4zu90US1QXfFhWx4IqfQTFpCFErHGY9eqMaS9ypo6MYLVrYbqm +wmNTKbwYa1mzuJffzK1NpHh5wb9pnWlPU77sfAWU7fpUzq+XLzuWKlBXeoc3R2M2gWNT2NgQ5XHO +08eqb6c6TMtUApRijiUmoK8cH9sEjkmBqpPPwbVCxOO0Olxz18z+5PRK30Ogqv/Y++E2mUWQts3d +mcsioDo51eGiR3Gte+HSK30UwbF4+W5Aa/E5rW9Td0Mpld4L3sO1acz5ywj0lgwZj13re0mOYnkS +WHYXNGiFmxTLYCjSaxfLqeieQizPqjgpxXJt640yZ84olpdVCm9GLAPaIpu5S40ULy8Ry6abJ53K +l9cUywseElK+vCGxXNu63NDNkwRZTmIZ7f3VKj8BbSuLZWqPYnFZPhcpZF0ill1GK4pKLL+NVq/7 +tF7LfC3watmKVnWfCzwZetZpvlpEL/djPmGjch8g/N6hlfsUt35jzfcKdZ8UlvjbiD6J16buU/X1 +kfp+2spP6rrPvaM71vnsr5DpOuf1q49NXr85ywJLWTfo9eNSD82p453DK6aSYuG3zXUFhsiI/YUF +NmRRH9O6Falvn6tu+va5+pjarUhzXUE6HF3h7NOVLK9834X9Jq96XQHF2Qfq/+11BarcV8SWzhuO +xRKfvBkkaqluNN87DDCZV4tjMftQ9eInqX03O1GsmC5jxXTB5fWEpNCb5Yeq9ytJcc0PVZs+U+3y +/pEPVZs+U004/+Y/VG3q5/L+kQ9Vmz5TvXwta3+o2nYtG/1Qtekz1XgTxR/4ULXVl743/6FqE3Au +7x/5UHVg/jPVsJY/8aFqE3DEB7v5D1WbPlOt1b5t+EPVps9UY5baH/hQtWlS/Dr2H/hQNcJg+Ez1 +zKuw2Q9Vz0sfowd+kx+qXjVz2KGM0uZD1RaRRINm88sPVdsBZ7aSaD9UTVtAPqar4V3vQ9Wm8Rbv +6nQ0uWg+VL1SDS/dh6qdSUVZC/WHqqlLluc/U22fb0lLfJYfqnbOt/zth6otEGj7pe8Vk8eXIxDv +7KK6LJP2gAysvrFjVWFHjcAF14nTLQEOZu+y8uwNR3lsyrMp/HYLX5Te4P15enE27dcZnDJLlxdn +r+KDtSnPXgdjm7ky11Sc/ZuoqFqevYGsG4rybAqQyL78rjybojjbDmNrOq1sirO1L0pv8nKFxeJs +my8ZrVKeTZ2j+KvybAqPmm2Uh748m6I4Wz+Va5dnU3A527XQl2dTf7X8V+XZFptoLs6212FWGNGx +dtFKiq1Ynm2gWrvi7CX7QluevcK+/KI82xKL87GGjeyLo/a74l2dVuXZv/8CO015NoWyoX+Vae3y +bIribGM04beV7vbF2evdaj5Xnk2BSrv85BXKsymKs1eOii6WZ1OUGlvl9a1Ynk1RnO1wLxxNeTZF +cfZ81s1a5dlrY2zFCkrH4myn80JRdkwhpV2UQK1YfWkCaZZtu3559iJIFnnjvy7Ppqys/2V5tnNx +tjHXGtfaMicOUq/U6uad2bezre7oNn49m75Wfdm3s6m+J05dNm7HmEw1VrTgrfjtbKdM+818O1uX +yNZfz157N+a+nU19k96vvp1tpcEuq6OmUmkWlmv7bfRPuotcHAvRSRU1sffX8Out9u1sy7s6qRFI +p8jQ36vwuVAZR319CahDBj9//s9VjNvc1LrhinEKL9wGKsYXa0X/RMW4PcY2WTG+gRsOKSrGaW84 +/F3FuDHn6s9VjNN80eD3FeMuh4SjzVSML2YQ/YmK8VmFnWWx8aYqxvVK4SjlQVunYnzdb9itVjG+ +HGObqhjH/OT1a6doK8bNsdc/UzFuWVm/8Ypx+rsIflMxPl+V9qcqxpfljm6uYnyluwfXrhi3/VrW +RivGN1OX5FQxvkJd0i8qxhdy4P9IxfgGaIyiYtxFL31/UTE+R2N/rGJ8lW/Wr18xbvPN+g1XjJMb +QTna6Oq6FeMu7+I3zzdfMb6hGiuHinEDJdOXpq1cMa74+uxUnk1VjCvaBb8ptNmUpq1e97pOxbi1 +72LTFeObo7HFWPbi3YMrlqatWDHuWsl0Wbdi3Hxn15+pGLe/qXWTFeOz6qc96vvX1qgY/81dN/QV +4y6KD17/XtnQvlpOUdLyi4pxu+/ybLZifDmNLVaMr1rfPXc7kOVHHzZXMY7f4LbKl95sxbjyjdTf +524trxg3c5g/UzHucvZEbKBifMZh2C3ar5KuUTG+5t1QK1aML/GQbLBiHLTxWc34H6kYJ2LZ/gsg +m6wY178AssK3gleuGKeIjGygYtzCSvoDFeNk923LiTdVMW6oeqZ1WK9RMb7eDYerVoyvZImvXTFu +cUfEH6gYJxVDd5v8NtKc10+tGHd5rT9xv9mKcVjLrGb8z1WMW0ZGNl4xrkdGqN2Ka1SMk7w+20/c +b6pifHb20+Hon6sYd8i031DF+Er3j61dMW5z/9jyivFFPC2pGMfacPwG95+qDtdrw+Hs/7HqcL0f +YuxPVYfr/VzeP1cdvnwtm6oO1/u5vH+uOlwvtl380vfmqsP12nC9amDz1eE6cHNfYd5wdThdZf1v +q8NNlfV/qDrcsbJ+I9Xhepk26Px/rDpcrw3X5MufqA5XRUK3PQWM/bHqcF0xVK2kP1IdbpcDv9nq +cJMO84eqw80+pT9THb65L+Utqw5fo15sjerwpV8v3Vh1uF4bvkZOL3V1uL5cy69mbKg63CKj+w9U +h1tmdG+8OlwPYzvUWfyqOtzKStpYdbiGO6s6iz9QHa4jw+VIT+tXhy/U8P6R6nC6/LHfVocbswj+ +XHX4YtXzn6gOX5ajuLnqcKcI72aqw/XacIds219Vh9thbLPV4XptOE3m8LrV4TbZgxuuDtdlvVrN +8Ueqw3VEm+5V2Gh1uJ6M5PL+uepw27VstDpcrw0309gmq8PtdZhNVodbSbHNV4cv2ZcNVofrteGb +3Re7T3f/Yl9W+HT3Eovv19Xh+mZb6Pwbqw7XNxtj4n+qOtypinMz1eFzttgfqw6nuleB+SgIz0tR +afAbqAs3xpENCRIur5Yi0WvZf8A39fC6+gdAz23PfvtsU4W8lLdq6NLeUsOD9X1TfQH4nXtFz1Wn ++MA6kFLq4cd8K/ZKpZwLGFuxlNMOY7T3XThjbPHm3xXu7Jpbmq0JvxJIJJL4RpWO5Py9dFtmZZ/X +Z4unFYtI56xXE1Br3OJmF+giX2Cnrfek0PlxpQu5O7jSOZ3fwlK31/RPfJmRnaZ/brLTf/0V5uxC +GZoN56er9l3L6Wh7EziAt2AU/8bpCON5V/gyi6PT8dzW6bg8D9Z+N/ZWOjQOVtL5eldSLqust0gJ ++90nwGcC0eXdxM0Jnwvp7fMItPkO7xIELknctkWgrQ6DxetOyWHUCMRZsHjdyZ5QcWchIS0yRsft +JbVv/I48pKlsoPPB9i6sdn+NrMCLjX/172KzXzQALrFwsNcrfdbzvX+LMccP/tH5LbF6ekPfSL0g +Gd+/zxsnIFmkelvd1EqBJ0c/03zKnlLLY5eihcXrv/w86sw7Olfsxp9UJhkb79Iwv2aWt7UPlj+5 +DZhvgFlnm2IlLSrqlFNHK95jJftjtsaN0/nVE7xtMXbk3wjGPFq92C8TqfMUSWR0X/xEoH6T222I +8eWtfBtr4skUNVoe5XFS8rF0nSYd0LV6gcZwScIClsZaHNy1b5zGuBB1kY/L61RTTCdzl0vcue8j +n73HzO6W9S+KwNJ17fQaIrxrfWm39kZzwYOLshSbSvDakrj+FWasW9/EbRFK1fr8EV73Vg1StR7c +HI2tKn2tZK9uv5AR69Q0tkLyeLmPPNnGj4iOHqoybmdBp+9+uW97HdLqYWyEcF9nmY66pWFEuzIP +MuLSa3VcXooRLWtT95ORI7simFtLD8n6sVAQkxTizUVzNgDuHA3x2TruF+ssgAHQ3j1DVZu6nywu +3j24VvnY3WB5berc7juWYu+vaT6Z/MmEOjZTm4rl15qz9LfyBavWnWtTaWksvpAgYXYiLctAsPlm +vQXfNDtCKArlZoFxwpO1ezoAj/u2yaJs9jlNYxRa+Rws13K30lVZSyuKKb9dSx01A/N3o3fcIYyr +3fdg/33ku5Xuy1peiK1V1/76kga7FCVLi8/5S+freB+svl+JdetrXZplhSyrexStXKSUH8PEEe3C +H1Y0tsKVEbWt0xM7sXyvi2UHbkkplp9/lovlFeRLbeuBxqtpJZZt7iGB3ac9dzRimdS91jfgc0G0 +2Yrlxd13/Hg3taavHz1Lnny/MbGM5dcgljdwi6ZStb5ULK9EY5eOFzzYiOVl36zfnFhe8Chi2DVo +K5YBxlsa08wklu3X8uAslqkrV3Ur6W3kqFgtfPPcuX6d27TcfxttWO7HfLQXSnjnIolLyuv7v7xq +cd4Sf1jBEaKOaIs7kxuEourZWUxi/brz2aepYLfw+r15lnn96oaPECy3xOm8fh/psIONiH4YmysX +lpU0fzpduDCfgeuiuNOg+mu34sI9ilY5Sb9wK1Yt8y3Xvn2YlK7TS2S6z50fUKvXWi2PjdiyTC1a +48KF+bNfpfHzr2JN6kIL8y0LQthebL2M2w7Xg6nLWPZdiHnr9TZ2YDsfRcTcdr7ZjTqqxxHxWKhk +6weNXrLfCA2Ske700iQDyFSpbktkEoff18+5/d1rjbk0kruZj6GfTebvHwuzfQYrSQ== + + + r7xXT5G5+/uV3l3vrNRqqTgpXKe6kodNX92XWrnD7HMY1nfvy/lLXDLxk3r1YWWKYg7MWk8aORyx +mIhXSlOsjz6TQp7dafY+de+ZP1zFTCNWa2Yb79niaMSNk5799qh0EPLGdvn7y0gyIgbGn+cHg2nN +5d07a0ny/snTzuOW53zi9yRzNwdb1VcxtlO+3854v/vlwl7rcxgSy4Wft+h3s9BOfL9ffeZbZVm+ +ej77uSuzw/xH+bPU6d6eHlxO795O/b63t3TA/1WNfF33ioH4h8s7DDwmJiNv2bc3GvE7nq1Ba3Dg +ZXY+4/7HQuMuIAcufcc/O71CjHu/OUoyh4dbo9FJrOjZfzm99HCp624g3hASTI45OWZylfscc7o9 +uGJOr68+R6POSWg0/TwGbjneC17Vw3xBjiXL2+09UkIO63vOhqKF3S8pepLOp7rRUoAUb8NKKxWA +odUMWFy4YJHnoOyky2t55YK/05U9ga5Qwul5nXRjXG2vlDgMnQlKQfte6ufGGsddVih/3u78jBJ9 +8crl9dxf5QMOKJq+h3f2d70PxR0p3k15i+XTi7338vmRIO9eirCqlCdZOaifYIF8JXB0ELpH4KKJ ++MtrN3Ph852Qr2NXS0z2Lvo2Grcvthmu9LydjL4kWpnz3slDIjZ8OTR58oFHXv5kg9I4LBcvSp3Z +6TXQOciFYlo/FC6vdixeLnXxznDdyWH6U7yuwl6NX5OwF/dz4zx5rcY2433/JTf1BD+/HvC+i7vX +rCd4ef/g8YVqW3jLxQX+iHtCSb7oCR3svuI2HXtC/dKzJ9AIy4iOSPKm8ryXrTPxKBk7es8zTXLz +Bfkt3notfgL3vffBqrYP8Tbgs4+XTGs0CiZ7g8IYVnX/g23NXDrl2Up8bSe76U7tgGWY8ftupn79 +dpWUgsNBIHv3cAadH7ZgvsA7ov+ceztrBhn2ddub6l2/SLl0clpLDMdsXfvi57HYCGYbje39RPzi +G459Pj8hRhPDTSfbidF2c5QKSd0rht3zt5Plxt40WdqX7pKDQbiAiuqOWM3384nYdfor+x5qTxKH +J4I3kyrcNXAtUZc39/52PUxWMo8ckHvwKPPBVPFaD5kla8U/3xOHomeEx8I/a+sd+hOFcqrb3wvh +YLe4k5HU90/Mmyold8SUMAk85XyDdzbzEXq6Jpz/XQCiak5wnGrm4+U+muomujyOSOJBnvTnU2Ma +uwzwT0gHeQI8kFfoIpe9vJNynuBNLvfmf8qLk+FrEfbqJAiAeHynk7dhHVb1ICBPvpgGEvGCB+/V +3d/O5PNnUXh7Ozw3jm9P25zMCBc+zqVboaeUEOn7CQloe/rgTX+Gzptn22FvVN9nPPvv51sAfI8N +ZILeac53dn4eu0pNnzLn54ldvS3e5qrfqbPb/o8yHzethmCRzS29i/gofj0lbz7ao8zHwc5btrH9 +fUKmgrUAAP5c9uc5ltv3BwPiduyhnG0ED0Nzi7ziYNJUn7iOdBhT35c3B0AHzyO8iyBNeFvmI3Uv +5TKRRw4IqVrIZTk/6DAGdHDbb98JPnXwpfzJtqMDgDa9bVhQ/vF0YAD+KPdzRnYNtuSplBhNc3tA +DNU7hvdsSZnw9WeGbCLoYx+9+E6y5Km2YRnRi8zF+3gX5N3RC8MFW92UmLj/IfKVYQfVLrCmh0ny +mg2V1c1pDktkf9UuqXorcfdxew8Mda+QKlVPGYIMQmMfsIM3vXLu+azQSl6nU9VM/txzt/CgzVyQ +033kCwXfxWrhNpF7be1UM+fdwFXmYzA6VhgXHy/vptqPpbv4x7SYRsJ9hrOPh1PRrggLm3U68k25 +99PDoa+We/fHMhhuu1ZhfNr/zgY6jxNxO339tgCS2JzAvpzswMjnfTwvd2Qtzy9HL/nsQaYd8tWT +khgN5zLVShDQFn1iuEk8KRe97Yf4RapxnzsZnxZz73fVJChTFQGPVCpZDu1cpi+P+mmVWzQK41Sp +dH2bavSnr+q+SP32M5+tlx5QF8yHxRPx4TSdvREmKgnkq9108z54n/kojd+QCd/msuxoJxOs/eAK +HkOZg9rhbqzd37sFuCK+TGt8Iqi48wgyUYJAt+S5wiT3FryLoRpbMpoNVr4prnAPxBmXcP+KtDOD +fLGYe+WZc+nPaF710CJHAz6dq6AElMXbI7wVMHm7G38nnfAukXt85yLV9VYq6XZ77AG4LyUET8o2 +Iu/+6F23cKqfl+hd6akCP3q13OHp0ylhZqQtdtWOd2KdV+kDHrCwFvVAAgCZVmfwdOo9ff7K1ivZ +iD4YcLTUrfhQG96R7UbiLGdaD35+9ud76ps78mhC5GwPr35pprrhmwBYQS9+QmPZXaB5IZvztU5e +koPP633VAtMfNL+esrVaRD6Wz7mBgtnLs7vvxOi0B/qD/7WUCd80OPJ0XudPI59+gbW0PytvoFsn +jyx02ZjBLQ5MagtY6+1OIj7oVVPXk9o43op5+4oFdj4efOTeC9M9PUtAb5vd1nj80yNX1YDiGN2L +7Q44ARTjn7vEwWQ/E5AjwiH8dniROBgPT+DB5UVmWitew4Ojk5y8c9NMv1xsnxEFK3oX2rrMvV/u +RlWn4zAJVtL8Mb0GkLlptvF4RITjDUixwxGezw7uwTnoAjxPfBd44ivkyEXjjYeQqV82sC9+iEdf +9fNsPZQ4V7I7duIfZweV5DUzvERCO1dE+bTH12HPfyrkphhyKxdwgYc0UWjhnAs+YKg/DaJsHIbe +cwP99i5JHhx8JMuT4TSZf/RLsBYkP0PrTf/7OVvf3X87O3rm7xV1+PrH30hdT/d8yMzOMh+vW99n +2zenYeDyz/d4MdBPznfx5QO0DBh1qGmJUCAcXCZ44PKysOXHMJjMAQNsbwMf37uA/f3ZBUWm6kve +dIf72cb3jwwaUDKEU3WIiILDEHwE9p/YIlRCrhc68t8HOgQtZLnECsTPI4XVbFv29DWMBS/ZrVw2 +44+aepqdFWyl4wOQmE9QjbY+U6FYZkdhlLMfYBS2PDDAzVaydNX6grMvtKeNeRLA+wKkQeZi/2kH +hmgNDVpfIwVSG9EvC29PN4fBx8xzLhMUAwb5eXJzeJ/z7b50svVyfpoNFMJKpXDKm8mn3vncc3On +TXYf21hgAPEJKMHXHrmbazzHW7f+TqrxszVSucXO1jjnL3ZigePo3Vn6/Tu1n2m9Tx9zb5PPbiKW +89ZmD7ZcXmApL8cKD3kU28+gj+UN2gxYP2I0l+kGFUGIBzslTvKTo9xb98CTiGXjxaQcz4e03Q0O +QPdoT3PJXiyo3X7Uvp73Wq9iNhivwQvtJMb+h61M/uLxPc3e9sY5z345pD/AL0tefLRhzyPRlBhk +H3DEi9Q3W2tkcs+X7Wzg8/URT+A78TilO1/iAehM99PoiD/rpAqPgwc4i7FnEOSfO8Ajr4RE7Opb +Tl0Xhx1QaWK78tfI8+TyqqSksLAPJtXtDUPK0dUV7MdGixmNjg6wyzZoLh95llC8PphisOxGBvr1 +WFWQcjdbOX+1OAEau06HjomCiRufRMM9hBYKo8O9ON8ooHuzjnzdeAfw2fYkfpLbicRhsNkkl34R +X8H2jzi9dHkNyuTifEXDDVx2OFHuvkrvnGRtVwq7r6z1kbuwWqvFzNuOKx3sm1ZK/DBWawXb3pva +ySxdaYl2pWBX8udPPaPF0GyXTLhlcw+n7MztlIpx9YMO2mw1NCC+iDwj/EB7UM+bpB2efXgeb3u8 +X9lG9Z1LdS8OemAWHkfQ45jCjwsUcu+xgZDzyaI/GwoMhrqNddAJxvAsPsqJ+5J3y/Ld1w+s5AJG +ko/GOunMJ+G1KPJ9yDyD8Y/g6DHdvLu/VC3e42wQDTcZTIlTwGKd2cl8fJT4mR3rz4TD58E4c/Lh +gS4Hr7psdnnl67v2de71eaAIodgV030Tpzf8Pdj7500Ypxgh3hC8bxF08OHTa/K6cPaIqn0p984l +IwqNHcZ7Nwx3dj3NnE6irVQkE7rTnwLG4o3rS5BExQFIIrkGnLgxVuAhasD0nTXc9mcECeSnPUhP +I5CzM5BA5zcDlRmXbYEKxLu3V6uDhLtf3O3ZAKXcI3lli6f52wqvwIZ6207ExqEBbvxZNnA1qIBh +cwUWX7LYK7dz6cZNGv0+j3jkuHTnKBWwnPSmx71qzuLMOHMR2N9OCfdcCk1PJlke98tAVAfTufUl +eCLF/FOQKhcPZNq5Sf3PhnNnNAA7JQWpuWSmYYlyBanntQcyKdiVxmljrPVOT45/aHd6YVKuXTJE +RgxoxS8bF3S0Lq7PFqkwaeJ+v0ZAgmMWfCVP0T+mPce7Sp8VoEr78Wcr8rICCXSh70s7kF4JSGhZ +2AGFmDABpT/FOzhfaEGC86IDdVKZXC0Hak2QXN6VgNJBwrinn5YdWZz9ZQxpTXZEfH3V4UrUbzpy +6oGc5J4tJ8XjiDT2ctFWVNq5jajaHMiX2rJzDgfO4pRjnpI6LF7IKh/j7jcsJn3Qd9+O5LoWrOcE +bwS6ACm2ucNne/RA66Mlqo90OLQmm4Hd/xMCBUjqc6qTlI0UW0JUR5z1sMVne0rV1rJkWDs+XXz7 +oT0AQGP00LbGtsMyuZR4bQuryzveyrUZm2EHnvWPK8A6O654e7bNgV31uL63bY8rruX0LLVMLFf7 +NsM23myHBQR9GbgAcpiFYV/smMuLDdWW+9YnrCAc6XFkI0fgT24LBQt0vNpsXXVgqSkFC4G4znDI +rebKBAkuVWXPLSZozaLHX9n6wQfGNkqvROlW3kD/fOr7MjQBdf/5PtvYFncUV/j1q3hMwhHogVfM +WX6XuInU3y4+vtEPc8MGvqN7DJtP9k4fTo7qSeb4oqc5Z6Gtuu9ppD+fUt6Z77+L8ZwfLb7kF4l3 +BTO7NL8QrL8bRhfNceajUsJgzuN++uPos4iGu+KwP6yL8X7q23fzEW/tiUXcqyd84wIMlrO+4myC +P6VZXOgZzDrf9uyrGaL/Z2YbfQEXzDJKqIfkTighsVz1VQkVsPnvSS7BXNwCHsKP6p97mXsllKV4 +/LEtefotkKgRWuJq3OgcgxhcsnJ3GwC6ewkYbRowOQ5Pu8fVbH1wFkoWm/GmIVjF+/vbYMfd7mTO +u7e3JscnCQShlcTEo5pDNzhGk8urhE8OQ/et1Hfha6T4B+eCObXk7e7hMWz33fViQOm+q9u2JCKF +33ycj0lxZ+mhQiL5armv7fTkSwmu5E9+BD3yg9vZSXde3zit7TEOW7LvnwWw9mHNzD5Sss/vM8TF +Lr62f8haovf8Yc04PYnHPo2F1PfeaU4z631eY6ArffyilmvNXmPzVZT775feLkasRMXi67xe9IHG +RiLG6bbk72QW0ObnZzHTBw8cvvxohp2JEj0ddRp+DdZ7KfHA+MPan7tg7+d8ecaLzulLOHK1XYUY +mrvBfG6fO+a0bXyU0Oru5N738i9wAo+2SWQE3ZgZA0sBGkqA/f3QS1Yus3l9c1xezQ== + + + /+uVsVMcWj1fydIVaJkY6tHRMYsgyXgOCkrwCO24+QfFRLx3nDYEfV74bBdsZOKA04JCxEFV1UJG +4ihRTudvQA/xn5MoHjpBJQw4PGldUl9JqfH4kDl4KEWVcNosGAXblPlIxJ+TF4Qnm4GKYrToKNmv +196V84K35xzujVvtrG/rrp3JeT8u5gNKF+J9zz9IDobDekqYpkqmiJTCYT5zJ+Pnu8Dx9uE2vnMG +mA/siNssc39wWf5GHfVkB/Sj/SfVEfJ+fgyc6mRsiFx2vZWSIZQzH4QBi6/16v8A7SqHmTPbEvqg +B7MY0d3Z9s1RHInhEihmb0LcEYpD15wHqmVuzYIs1/qkuJa5aTc+Kbt3gRbfee27uBjb8l9IJVk6 +BS7YqpDAuPoOCcaNIoVUKB6PEe89MArvNF0YTK4RzKF2Srhp+nN6U8HARA45jMLCSGvvK1lJ336Q +ZcyOoV8JtxWfLp6zb8PmF9DiVkQbjASC2Ex2J/R5z3Ah3yRZDHUraswf/yxdle8wMuJvXish++/W +C4xTYGup7vd5WQvjA51PfOHLxfi++kDc/vGUT4c7hyL63bcQqYVovHF7Q4QWSfZAWQm41aJFC6I1 +1u6nSing0lupwmMpqbJWRbqeMBhFD6NQ68/kx2Gq+yzHCAx6nGYWf+metnOJduJZOQJkG9lU/dMU +0VHjNPL4hHwyQOmH9/wnfhKjQ+IdVN+VO57sXn+3BCCxTy6vOTfk+DTvI9GLROzB10lxX2E1Jq70 +C4Z7Sngk3SjHzCGTdGtag+mTLVX3eKgWsvVws4VWUqe9rTiWQDiO1SDN6O5UTZBIpTygMBwntFCP +b5cczePdcqwTOK4OFE1BCfoQl+xD7eeC8Omzo9c7hhAX0cf0AI/gzZxWE/Vk/8QzyJw/idtKyi1/ +6BOSkebpbeLno91XL/h6mISVQodOMBZAPWqQrNzv7GffxWHD0M/iTntxB4DK/aAH/gsTaUZqoHP2 +A0OQmon31MzWvc26iQQQTxktEUqVj2QtO3u5zHfw9Oj+I51guO3vj6z/tdYGPaMyzj62Jruq0Nv7 +8CYOI18dZMcvChcAZCW1lL5sH/NdSMpQEiQJxyditUI/9fl+iXZlJ/n8nq2XqjPZpgV97t+PK6nu +Vj9HojyyeOpvYDgGtbCHDHkQPbwX3zDtpYMh8lI2WPk4MMCFn6AA6v0Za7qlwj7k5Hvi8CwdxsHi +amhY3d9YshxMgLLRqUxIsgtqzJdaduT4v49dAsMKbiHKiu6Dm2m3NboeddqdvjvoirsOkucse9tv +DnKjVqvS+s8kM2hMe63+xB1zHyTL6fNzCfhrY9BsQXfvXDyn8aFnOuppV7QxX8sg/aPYe3R5SehT +zeES42PMMPWBrGuPF6LEx7X+affuq556vywlk8E++xI/Ye+kzHZzms2kz55ftCjq0FyxBKLIGZQV +AAHdPji8ukjk5fGxdHZ4H84NHgVgrNnR8yOTecxVK7mj5FHDCh6NISo6z9dKkfOZwFseORfqB6ks +d5YgQfPEx23lKnkmvl9RRc5Re1dVBEWT7/a30KR4UyLit6+5GvD8pytDogS2SZXn1H3qe1eaaCcE +j+VXp6bwx5kQ1APgbLDdPlaYkS9br3QOM/n84d68CVMAtb8XxByeoprIo6RHvEQTsWtfCznJKVE0 +NTWz9pT6zpwxKg9nMi8khIRhp/RTCxihvK9EX0h8ngTkhffQi/IlF9x4NVyvpRLqOVN6WyzPHhzo +gRwlf6ReOdxGsRp8ieTejpM1+4w5e6PncXAJyziVVaEU8vWUPKrKxVRWRYh8+ePyJkaNMFGkJ9lm +7LhnYbj00UwMn748+2LpVrl4OxtnlohmCAKWrYwjlzd2WSudKOaBvtP3l8NzAvycDXSHcfeTH2Nb +z7eX8zWeZ+zyBZTP4uOBIeqO6QioVPWOMDmvWwWB+CIbktfQqkp1Wyk20bseltQw3selrKf7YZxw +V8lHNybtFB7A0mC+CWtE5OfwEz6RgYRZmSJIxZNbEA3j4JzhkhxlznOPW9qfhwB3/uHDuKDGjxfU +ndeQJky07M3rglgiGYGouIeeMOVd+diaoOkWJGW20Mrkdv2lVOSuNNEyygw+EYwEHqN0vP/J+c4f +bmabc6mmuoYS6J5gdmD3MTdAswwq9Rk6QpqZ4XlDFfBmMauNPDg6U3TDxWS5C80weQ7PJeeh7UOU +Lu1oPj5ye8e+M2OXe/kj0w7kaxa2y2G2cfyyDasPPphAArFaehx2zGL+ZVwvgfWSDYPpIbfErdxb +CbpIrOZveX45er5KH+WSjXYDU+Su0vxR5ZjYuEoCByba6VYMWct9kFhBhgQ7dvBzBiRXJCSSydQu +Lm4T8ZPpZE4rP79Pd46OYwoTBu39OxEvT4qpkPQVSHXD7f1ksfFRQLjK5FNkzcfsY/MCY7Avx5ge +gihq+NKd8SFL6iTmy2FN18KkNPPg+9p2UnSmzE3760nF41vhKskcTscqHzt4BEpW0unZg+3xYeA2 +PlI6oXZE3lET4zBZbhZZVlMAX/NZ4xFXbKDgI1ZonKksDNtaA6EBp3LBelET8UwJduoJROvlOtXL +qZuMVsDgIvui/4kZna3UWWX7Ffc0jSkfQWK0n2DKSLo3S7W7RLfLQ7J/tv8zy+vSH1zMvvU10JBa +64FmXPlBsVVNxCs3Y+Xp3AUcqyfMcIUK+t0+QUQNt+OtyvsNCNtaz1yaUrmYTGdyXy9YUT7seDHF +xN3D0Yib7Hr8h1eCJ3Swd4cp9BnPTkY6nGXUn+CDC0/wZ3iND04wwb7pCV4+nHn2f7ZHsC+XLyYX +J70RtroJhqkzqxphq5tgcF5WNsJWN8HURJCVjLDVTTDtoodVjLDVTTDislnRCFvdBMOSjFWNsNVN +MCUpfzUjbHUTzOVdNMK0mljiifZXR6TqoZ87zDVZs0Kv2AbG4rezTMszDOIZaiTuKue1XFYqtZRy +39zsURv3PEw+5KoU3UrcbS5ZnmyVcZObWPNxrBTKGzKq2qXXWXrdrVVaHGoXmL71Vpm8ZANjfnfG +wpZngiUcM8Eau0ZbbHnWG36is7Q0E+yOIhNMS2jzTHzj5ZlgzPlzMKxFkpalCk7rxR/TSkHuO2e9 +2a50a+v45HTpSm/nPnW5NOuNyR5eZx1W2tqKHNiuVNt9ZRmpg0untMiz3TNjcdjJaSATDouPxGhQ +5JlS/aA+uCfSjhTkaPJOed7bAuMk86Vkus1y2fb53fgTO59e92Wwqy4i4bieJ2d61+VV3gbl9gvs +pdhuLlnqjWcxpQZ7+tpmQOa8jlDNO8/6b8/HimoL6ucNBu5bWAHYIlFivZgJU+7ec8kn1gPyI/WK +7lq9sH2+NsY+2sy1S7TRZhJrNiVOhUNrRZtfLpcHwNHe/2XGCgVIJPBgCMlerRnsXxLqrwZBVm48 +L6+2dXpuymExpM44pUYZP1JtMPvSy/PyDGlAq+TldW9WT3UgsnJZvgdGnH6dFApr2VwWFl6BPbYC +yeWlShb8ZQ6P8ZpCI1Cx4mTt/MVFkDBM+/vEorvBcna0/OwvMKQ12ZHp7L+8U1C/He2/2uaqkQJp +Mu062WpW59wqVw3k/m+z1Shy1VAik1yamC/4RzKyydGb7f5qRFW3T9SiSP/dlECZJ6kW8fasS1R2 +qU/vZkq1Wsvq+dPvS9M1HdOybaFtLMv/ah6YBjWnmD/ZDfvxG8wCT16WXLrmcU2Y8pXNa+ktT5dH +dvxjMWwSTbysDReoPitrsR8W7Zd31SF06x0ky4+eSYp7v/wislmpNrlKH7wSp/l83cmLfrOBegH6 +8rsNfn2zAWDM+W6DX99soAbpl99t8OubDVxeirsNAr+92YAU4TvdbfDrmw3UhLbldxv8+mYDl5fi +boNf32wANOZ8t8GvbzZweSnuNvj1zQbouXK82yDw25sNMC7meLfBr282wFJMx7sNAr+92QB23/lu +g1/fbABrcbrbAAxgh/sFHC81QHt/tQsVVptUuc9gVh6/eKPBBu8zUEvKrW402OB9Bvq1OAs3GgQ2 +d58BXryl3mhgIVoNlZsF081B0jDIjVPfmb3PmTC6RWob5fZ9/g7AkL4HWVHdJnC5lExV9Au9zF8K +EM+cN47u9J7Yj/jopGQ5+L1DHEuGywO2qsJl9rHpbRNXMuxVXxV0IPck5YqfWUq47+ygs1XcnaVN +vM2c5kRg9vQwgybU78d6W1LknzvJSJNrq36B0vM2iWjgqVRiGiUmt783muphDxQE7yAIvv2pXmC3 +qgyWPxmWofMgljn/+jnjMqGngDGW0mxfZAPj7G7685kPJpnjaZQQl8ur31iAQraRhR0ZAPqDUzWO +fFS8IUEYY1TiOBuaRSXUT3ZwwZEHyed7vp93ti/aB8qUKvGr3e/qLBSSnt3Nu6uFQuDgoslBrihY +JIHPJ4Z9ufxMRfziyJgRQfJFMOuWI/EJvK/sKdPKPuwBEvQ06fte7r0w4MFw2/pMdfKJseoO1Or9 +UUDf4+5Xoe00DIpKqJ3zfUivWP5fx5i4d7hvOGiRA7meaX1dHDKRev4ED1Ioze0cvWfC0/EDSJ/K +VHvABJOV7G5HzWzeO+NzvnxSVyH2E7HsAXDQVP1E0S0vSbRBi2Tc1xOVG6mMmaEldX+1AIia4Y4R +R/2GgMF/H7viQEbnLPuW7TeNuWQurxdayq3JdIgdIm+pVrvTL9T+aY1crFv5j4H/WDfnjspuLhLB +fxg3D/8v1F2+xqA7GI397kLf5X07SI4mmU5j0hn0a6N/3DFsergs3J5n3DH3rGvc7QNImDfoDE/8 +mLz2BtC9uRh3Ev7/8LdrfwpzZ+D3axcTZjg+4mbCLCcL8E+EkSWY+tvFqIDBC//AHxfwyxc0/e0W +3JfupxfG3cSxblwCK4R5GcAWInyYkeDtntIWjUKTFGZFRnRjQ1SGhQnRaDgiwUwCy4Yl8prEhwWZ +4dxpl8AIAI0IDyUAxM3LXDjKiCym74UlkHxuXpLDgihIbkFkw5gCBC/xUT4s8xzMIQphjmOjbj4i +hAWOg8lEeCREeTfPywCaQF7jZFguvMaL4QjHRsjgcoQX3TwnhMWIDFBH5TDLsvAaQC1HRAXGKBPF +11gmLIk8QBAVwxLDYCc2zETgF5wtKokiaYmwLC6fA0AkjrzHwioFVmljYd2kJRpRWxgYkrREohHS +wsusSN7jw1wEloC4EWVWgLXwAGaUcwOCw7LMwS+48IgIA0RkpQXekyPhKC/xSi+JgUHZCKBHgl8Y +QIYgSWRT+EhUIDsHC5ZwC8jOMazSxghKJwkXjNsr46YubHja9QETQn+YXMGxBLvWI22wgIjSJsJa +sUXgcBBsEQSlJSLwSgP8z91wKZ0krRMfdSsDCbOBRPfidA0AggGiBsTBS6zyROQEAgduiCiSpoiE +hAQtUVmQlRYetxGIjYmyCiCw3wIBZLEXjiSpI0UlZaS56RCO/Vvrw0pOoXb4wiyMzA== + + + IPBw1sNSVMLjx8Ay4fBxoFlGOaQlwDEflWRcBeCdY4AKeUEGJCBa4HiJER7pmwHqwpPCSHBSgKp5 +IBegdDwgAg/HgQFUzdoKpC0aZZR+oN5ESFtEwSTPiGExipvEc2FJ4pAykT/g8YWJGFw/0iXP424J +XAQQF4XXWJhbJCQjKQTCs3Bco4h3eJ8nYEELJ/HaYQfCBrA4EagPVyHKYZ6JwuBwklgBtrvqQtqO +EMKOIow8TAa/8BKLrUDRPJwVNxx2RAgH/Tl8EoFfkUphRs7NCZEwnGGBkIyMZMVxeKwFmCfKAb9D +YIBvhCWYi8DAsTATxwHIwK2gJRoWETwOliUzEraIsE+AKGA2wEMAoQilIMEpbbhYOHiiRHYJUCVE +ZDcL7AQISybwwiRzLSpXSLv0Njie5GQADAycDQ4OmKmN4QSln8wqsDLALqMRmBMQIUUA8xwTBUaJ +x59H/gHUzOGGMQJH4OJkgKLhQvgJqQMWYbGMRFCj8KIIo5x2DqkCuQ3wpGiEENQicRacaL6QUoQf +iEIi+kKhtYThZNBr1CZUwlDrSiUMiSB0t1eQeVGNBQq6zIuqMk/WZF5ElXkozRSZx2kyT9RlXkST +eTyReYwm83hN5kU1mcfrMo/VZJ64IPMiZpnHW8g8XpN5vCrzJE6Tebwm80RN5gGlqTJPVmUesBmT +zIOWBZkHbSaZhy3zMg9bFmQeYy/zhAWZJ1rIPEGTeZIq81hGk3lRTeYJusyLajJPUGVeVJN5gibz +zBuuyDxWE0K8LvNYTebxmsxjNZnHazKPVWUer8s8VpN5vCbzWE3m8ZrMM083k3mSrAkhXpN50KTK +PF6VedCiSjNek2aMJvP4mcxb7EVGktSRiMwzT4dwIHELMuKMCYtAKeRI8ApDR/TLEYmwYCaCHB4o +CTghS/YxCoyHsHxRIrsv4mmRCOETKQBDANkCBxeEqCInkZNGeZacLAGJFV8DkmR5RCD8AhySMEMg +OlwdSC+ZQTICBHKIUiAaEJ4CvofgImkRlVKAo4AaUwQXIIiEJHG/kGgiynuyQtx4bkVCLNBL5nBX +NNkrgFgBquHIsiMRWSRwskyUSGNAU1RAARVl8OiwbgVxiE0CCse7F1CZduKvU3K2EYG8DP/KcHh6 +5NzKHApWra1gbOPIlhQMby62zN77mDXCsQ3LUZCehglmbQVjG6yPFwTDeJZNszfxPAE9inxkBgqS +kKjqBDPoDE36svQ3rdq0Vw1TzGAxzGGAz9CmL01/16pt9u4HbgfsuETYAHBYVuIJa4jgaRH1poJC +fUwkMtfGAwELeAIt20D3kGUydQQYRASp2dgGS5WRunhU6YncRtUZGR7IA4kTUFSwirAXBGQ/ArIR +hANoH4hRAJYbxbMCSiacF2xRtSzQhsKiGNUb4DXUcfA8651gX4Fjw2t8FMSOzJOWqERAEhXJogii +qBTR2mSi2oFKysC5I2/KyIyUNo5lUO+UUF7gaHjM4QVBANYuskaw1AZlNaAyCoZOEdT0WMJBQNuV +CVok5ABkwShj8DUQpiIyOgHPLbK+CAhTCTkPoJOXREXOwmAGpKM0jhJJqTaR/cLtZ/Dko/rECgo3 +4ySJJfsQAWUayBKMYxaPu9aCImzWBlwQeQiOBaQYlWW9DWQTTITyiVdsDpwO6JHVQUgrVpQEG2Ak +D2xTCVCWOA3QOTolupxiovCgFER5lKPA2qISCKqe0oaESJoiZAxg5qh5ALeXJNRCo7AIGeUbil8B +dWHoIQqMYWbEGlgZojxP5lFgqjzqOmAnSBLuN/RiCC9EWxwsFTJ6RJYUoSICTnAs2GdgOpLephyt +KIeUgnZTBAWKonQocImipFDB3IlMW59INLhA8qmWTZTQHEgunqwZ9CeOaNDA9QRiXcvAEQhUAop5 +BVeirFC0iDJc5MnuE9JWDrLEsaoNilZVgfAK2DhJkb6oguC7qABGFH4eYSKyijGOVfuBbCNNomrb +oZohs4Sjo56IG4jTMqgN4NaDwBNV8ECdB4YqabIJNxctzDuVBiReaSPDkhY8FKQFjaYFOklrigmA +LEWJwQ1oifJEakTQRANyB8qAHZIi2sZzgtrEaaudf1W1Mm5dstvndz/cu5ZJRK0Ph4pVhKiAcGii +HApGMKlQj5VUHQtW0HVxUcQpSm+wfjng5Ni28G4XQLCfkxMRUlRv0XCLAP5gLmAOIOUVNUqG0wHD +ossCSJNsBuEqXYtXHaYCWgazQVaEG2jvOFUE7UtBUa4icNpgWCBB0CZEhf3wUVzB4qvLp0IHGyIB +2WOEiaJURnKKsGhBC7hvcIi7hAswHE6vyZfu4qsOM0mCIkN5kEwysapwBKJO88hIQVTgqBJaNsjE +gXEC5ZAm05sOExGjAfmliLiAXcc1RZQTh2IRrG6ewI/ePpRT0I8YUV2Ldx3mQncZw0pEFBDeDHNx +ILPwBHBwGkEq4LCg0ooiylt4xrEcWejCq05TgeYqEi0BmJhEDhs6TDhUldHW59RhkVsSLhuBbZTJ +qsyvLp9J0WKRXDkZLVFclMAAGxNR7YU5QQ1EaJEfS0TX4HhFu+5avOswF4NMl0eGDxKDEYkvDHkC +4V3o5kEGDOPCwY2KaIKybDjKsjJpM7+7fC5V8KG4n3Nzwepwq2zcXBxrdnNxrIWbS1LdXGC8L7q5 +ZDRaoIlRLCvQQBn0I6C1CFsDz4jbDu0oBjUAaGPRVIA2dF2xqGihBSSJskpVHDrIOFSyZYJpURkL +IOTRT4WePEmUOGKBy+iDRGUClKYosbeiRNxBi4xuCvQIRlVvInE7RfQmlFUo/1nF18QSrzlxcxG5 +xqMfh7jQ0JeG+pQiHBUnEqpmcIyEKJxiOHthiZhbyAtZ0c0BouDoKwoFx0RY8hpOi2tDJicR4cYR +7xu6xMhxBV7LEZNZVjGM/jD8BRBEOBF6z2CkCApgVlaYUxQ9hRFeUxRgEjTUOaI/otNMa2kQlspw +6GDRezEKmnliVgLj5UBTg8MkkwWjcgrT8cAtRBRnuMXoSOOQIEgngB+1EHS+SRySmIiCNYLmKScg +USP5yKjqwUpA1spIh3iMiJwiXEIhSPTTRKNq2EB1jQqKaxTQBBhTXaOi5hrlFlyj7IJrFOYBjosi +JIKyBNaLvh5CKKCHMKiHKO5GpBiM4PACKuEcg5qbdkRBhSDvCSIwaWQGQjQCK4ZHgog8F6hRBnMD +34OHircJoBPRaObQlGAjCmcGUgYwQUkDBRNb0KJnRLKfwLii2AtMBOLcIr1ERlGcJVDHoQU1LZkl +DJUokQ0iNVEnBgg4Hl7n0HkUVZkzUfU5UMBFDq0HHmMVMiHfqKS4YdHyAWQAMmFfRRZ1woga2MGR +0Y0gYDhJIjhBB5PMysQw4XmwylAYK6cPNlpGjxi2sCKgXiAqJfH3gdmGDjTSiZFYluxcFIQPaYmi +3sfxsuo0gxYRthIxGVUIDJvIatHhSxyR8DqPh59DhiKyioEDFEe8wgxhqVGVj+PSgLMD61HCeBLL +oH8ZGST6LOAXYLKc+l6EHAwZNw5sKw4dh1FB8dezHJIOMk+Dnz1NfNBmP3tkwc8uWPjZuQU/u6h5 +1XnNzy7qPvWZn33WFtV96hHNzz7Xtuhn51H9jxBWSyw2JHJRwBYOYzLYMvOzEwPY7GfnzH52IAST +n52Z+dmB04DOAfwAqJwHBgK0gfChps6rMoSLomcN0QAnV0RhAu/JnGKdzNoKxjYeScDUJoaBAGA0 +9DAyRKRhZAnIEpQTVbrA+WAjxE7mkBGAugRtOABuGViRsEkCacHzzEWRVyBeAFCGJxYBgioJguIj +YDmF4cObPEGLwn70yAKrRBbSxrBX1Bz24qSFsBenh71ELezF6WEv0SLsFV0Ie0UWwl68Oeyl2DeE +wnlZIFSIZlhPDdDwircRaE5ws6CskOOD+8FEyYFCZiMpwWMiQ4AwgbEz6GuYtRWgDf2caCkAaxcx +ukDeRCUHcSZE8HDAoMSYAP01wiiEg5FscoCA3eHZQk4gS0TWgcxB1zOHngMGhS0GDnji58eRMPSA +yFdpGTkB8E0SH4oi3zGv1zHaaRX58RZr7VZlVOt0WyNXe1z7q+Wu9fuDSW3SGsITd3vUGk8Go5Z7 +/Dn4G1vgFa2715u9zrn+L1T7Dxc= + + + TM + \ No newline at end of file diff --git a/webapps/host-manager/index.jsp b/webapps/host-manager/index.jsp new file mode 100644 index 0000000..2806b76 --- /dev/null +++ b/webapps/host-manager/index.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ page session="false" trimDirectiveWhitespaces="true" %> +<% response.sendRedirect(request.getContextPath() + "/html"); %> \ No newline at end of file diff --git a/webapps/manager/META-INF/context.xml b/webapps/manager/META-INF/context.xml new file mode 100644 index 0000000..120b7ab --- /dev/null +++ b/webapps/manager/META-INF/context.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/webapps/manager/WEB-INF/jsp/401.jsp b/webapps/manager/WEB-INF/jsp/401.jsp new file mode 100644 index 0000000..84c9a97 --- /dev/null +++ b/webapps/manager/WEB-INF/jsp/401.jsp @@ -0,0 +1,80 @@ +<%-- + 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. +--%> +<%@ page session="false" trimDirectiveWhitespaces="true" %> + + + + 401 Unauthorized + + + +

    401 Unauthorized

    +

    + You are not authorized to view this page. If you have not changed + any configuration files, please examine the file + conf/tomcat-users.xml in your installation. That + file must contain the credentials to let you use this webapp. +

    +

    + For example, to add the manager-gui role to a user named + tomcat with a password of s3cret, add the following to the + config file listed above. +

    +
    +<role rolename="manager-gui"/>
    +<user username="tomcat" password="s3cret" roles="manager-gui"/>
    +
    +

    + Note that for Tomcat 7 onwards, the roles required to use the manager + application were changed from the single manager role to the + following four roles. You will need to assign the role(s) required for + the functionality you wish to access. +

    +
      +
    • manager-gui - allows access to the HTML GUI and the status + pages
    • +
    • manager-script - allows access to the text interface and the + status pages
    • +
    • manager-jmx - allows access to the JMX proxy and the status + pages
    • +
    • manager-status - allows access to the status pages only
    • +
    +

    + The HTML interface is protected against CSRF but the text and JMX interfaces + are not. To maintain the CSRF protection: +

    +
      +
    • Users with the manager-gui role should not be granted either + the manager-script or manager-jmx roles.
    • +
    • If the text or jmx interfaces are accessed through a browser (e.g. for + testing since these interfaces are intended for tools not humans) then + the browser must be closed afterwards to terminate the session.
    • +
    +

    + For more information - please see the + Manager App How-To. +

    + + + diff --git a/webapps/manager/WEB-INF/jsp/403.jsp b/webapps/manager/WEB-INF/jsp/403.jsp new file mode 100644 index 0000000..4baa2f4 --- /dev/null +++ b/webapps/manager/WEB-INF/jsp/403.jsp @@ -0,0 +1,100 @@ +<%-- + 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. +--%> +<%@ page session="false" trimDirectiveWhitespaces="true" %> + + + + 403 Access Denied + + + +

    403 Access Denied

    +

    + You are not authorized to view this page. +

    +

    + By default the Manager is only accessible from a browser running on the + same machine as Tomcat. If you wish to modify this restriction, you'll need + to edit the Manager's context.xml file. +

    +

    + If you have already configured the Manager application to allow access and + you have used your browsers back button, used a saved book-mark or similar + then you may have triggered the cross-site request forgery (CSRF) protection + that has been enabled for the HTML interface of the Manager application. You + will need to reset this protection by returning to the + main Manager page. Once you + return to this page, you will be able to continue using the Manager + application's HTML interface normally. If you continue to see this access + denied message, check that you have the necessary permissions to access this + application. +

    +

    + If you have not changed + any configuration files, please examine the file + conf/tomcat-users.xml in your installation. That + file must contain the credentials to let you use this webapp. +

    +

    + For example, to add the manager-gui role to a user named + tomcat with a password of s3cret, add the following to the + config file listed above. +

    +
    +<role rolename="manager-gui"/>
    +<user username="tomcat" password="s3cret" roles="manager-gui"/>
    +
    +

    + Note that for Tomcat 7 onwards, the roles required to use the manager + application were changed from the single manager role to the + following four roles. You will need to assign the role(s) required for + the functionality you wish to access. +

    +
      +
    • manager-gui - allows access to the HTML GUI and the status + pages
    • +
    • manager-script - allows access to the text interface and the + status pages
    • +
    • manager-jmx - allows access to the JMX proxy and the status + pages
    • +
    • manager-status - allows access to the status pages only
    • +
    +

    + The HTML interface is protected against CSRF but the text and JMX interfaces + are not. To maintain the CSRF protection: +

    +
      +
    • Users with the manager-gui role should not be granted either + the manager-script or manager-jmx roles.
    • +
    • If the text or jmx interfaces are accessed through a browser (e.g. for + testing since these interfaces are intended for tools not humans) then + the browser must be closed afterwards to terminate the session.
    • +
    +

    + For more information - please see the + Manager App How-To. +

    + + + diff --git a/webapps/manager/WEB-INF/jsp/404.jsp b/webapps/manager/WEB-INF/jsp/404.jsp new file mode 100644 index 0000000..d08b659 --- /dev/null +++ b/webapps/manager/WEB-INF/jsp/404.jsp @@ -0,0 +1,63 @@ +<%-- + 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. +--%> +<%@ page import="org.apache.tomcat.util.security.Escape" session="false" + trimDirectiveWhitespaces="true" %> + + + + 404 Not found + + + +

    404 Not found

    +

    + The page you tried to access + (<%=Escape.htmlElementContent((String) request.getAttribute( + "jakarta.servlet.error.request_uri"))%>) + does not exist. +

    +

    + The Manager application has been re-structured for Tomcat 7 onwards and some + of URLs have changed. All URLs used to access the Manager application should + now start with one of the following options: +

    +
      +
    • <%=request.getContextPath()%>/html for the HTML GUI
    • +
    • <%=request.getContextPath()%>/text for the text interface
    • +
    • <%=request.getContextPath()%>/jmxproxy for the JMX proxy
    • +
    • <%=request.getContextPath()%>/status for the status pages
    • +
    +

    + Note that the URL for the text interface has changed from + "<%=request.getContextPath()%>" to + "<%=request.getContextPath()%>/text". +

    +

    + You probably need to adjust the URL you are using to access the Manager + application. However, there is always a chance you have found a bug in the + Manager application. If you are sure you have found a bug, and that the bug + has not already been reported, please report it to the Apache Tomcat team. +

    + + diff --git a/webapps/manager/WEB-INF/jsp/connectorCerts.jsp b/webapps/manager/WEB-INF/jsp/connectorCerts.jsp new file mode 100644 index 0000000..80bfc7f --- /dev/null +++ b/webapps/manager/WEB-INF/jsp/connectorCerts.jsp @@ -0,0 +1,92 @@ + +<%-- + 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. +--%> +<%@page session="false" contentType="text/html; charset=UTF-8" %> +<%@page import="java.util.Map" %> +<%@page import="java.util.Map.Entry" %> +<%@page import="java.util.List" %> + + + +<% Map> certList = (Map>) request.getAttribute("certList"); +%> + + + + + + + + + Configured certificate chains per Connector + + +

    Configured certificate chains per Connector

    + + + + + + + + + + <% + for (Map.Entry> entry : certList.entrySet()) { + %> + + + + + <% + } + %> + +
    Connector / TLS Virtual Host / Certificate typeCertificate chain
    <%=entry.getKey()%> + <% + for (String cert : entry.getValue()) { + %> +
    <%=cert%>
    + <% + } + %> +
    + +
    +

    + +

    +
    + +<%--div style="display: none;"> +

    + Valid HTML 4.01! + Valid XHTML 1.0! + Valid XHTML 1.1! +

    + + + + diff --git a/webapps/manager/WEB-INF/jsp/connectorCiphers.jsp b/webapps/manager/WEB-INF/jsp/connectorCiphers.jsp new file mode 100644 index 0000000..f59e878 --- /dev/null +++ b/webapps/manager/WEB-INF/jsp/connectorCiphers.jsp @@ -0,0 +1,92 @@ + +<%-- + 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. +--%> +<%@page session="false" contentType="text/html; charset=UTF-8" %> +<%@page import="java.util.Map" %> +<%@page import="java.util.Map.Entry" %> +<%@page import="java.util.List" %> + + + +<% Map> cipherList = (Map>) request.getAttribute("cipherList"); +%> + + + + + + + + + Configured ciphers per Connector + + +

    Configured ciphers per Connector

    + + + + + + + + + + <% + for (Map.Entry> entry : cipherList.entrySet()) { + %> + + + + + <% + } + %> + +
    Connector / TLS Virtual HostEnabled Ciphers
    <%=entry.getKey()%> + <% + for (String cipher : entry.getValue()) { + %> +

    <%=cipher%>

    + <% + } + %> +
    + +
    +

    + +

    +
    + +<%--div style="display: none;"> +

    + Valid HTML 4.01! + Valid XHTML 1.0! + Valid XHTML 1.1! +

    + + + + diff --git a/webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp b/webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp new file mode 100644 index 0000000..f969e9a --- /dev/null +++ b/webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp @@ -0,0 +1,92 @@ + +<%-- + 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. +--%> +<%@page session="false" contentType="text/html; charset=UTF-8" %> +<%@page import="java.util.Map" %> +<%@page import="java.util.Map.Entry" %> +<%@page import="java.util.List" %> + + + +<% Map> trustedCertList = (Map>) request.getAttribute("trustedCertList"); +%> + + + + + + + + + Trusted certificates per Connector + + +

    Trusted certificates per Connector

    + + + + + + + + + + <% + for (Map.Entry> entry : trustedCertList.entrySet()) { + %> + + + + + <% + } + %> + +
    Connector / TLS Virtual HostTrusted Certificates
    <%=entry.getKey()%> + <% + for (String cert : entry.getValue()) { + %> +
    <%=cert%>
    + <% + } + %> +
    + +
    +

    + +

    +
    + +<%--div style="display: none;"> +

    + Valid HTML 4.01! + Valid XHTML 1.0! + Valid XHTML 1.1! +

    + + + + diff --git a/webapps/manager/WEB-INF/jsp/sessionDetail.jsp b/webapps/manager/WEB-INF/jsp/sessionDetail.jsp new file mode 100644 index 0000000..b76e6f0 --- /dev/null +++ b/webapps/manager/WEB-INF/jsp/sessionDetail.jsp @@ -0,0 +1,197 @@ + +<%-- + 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. +--%> +<%@page session="false" contentType="text/html; charset=UTF-8" %> +<%@page import="java.util.Enumeration" %> +<%@page import="jakarta.servlet.http.HttpSession" %> +<%@page import="org.apache.catalina.Session" %> +<%@page import="org.apache.catalina.manager.JspHelper" %> +<%@page import="org.apache.catalina.util.ContextName" %> + +<%--!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" + "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"--%> + + +<% String path = (String) request.getAttribute("path"); + String version = (String) request.getAttribute("version"); + ContextName cn = new ContextName(path, version); + Session currentSession = (Session)request.getAttribute("currentSession"); + String currentSessionId = null; + HttpSession currentHttpSession = null; + if (currentSession != null) { + currentHttpSession = currentSession.getSession(); + currentSessionId = JspHelper.escapeXml(currentSession.getId()); + } else { + currentSessionId = "Session invalidated"; + } + String submitUrl = JspHelper.escapeXml(response.encodeURL( + ((HttpServletRequest) pageContext.getRequest()).getRequestURI() + + "?path=" + path + "&version=" + version)); +%> + + + + + + + + + + Sessions Administration: details for <%= currentSessionId %> + + +<% if (currentHttpSession == null) { %> +

    <%=currentSessionId%>

    +<% } else { %> +

    Details for Session <%= currentSessionId %>

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Session Id<%= currentSessionId %>
    Guessed Locale<%= JspHelper.guessDisplayLocaleFromSession(currentSession) %>
    Guessed User<%= JspHelper.guessDisplayUserFromSession(currentSession) %>
    Creation Time<%= JspHelper.getDisplayCreationTimeForSession(currentSession) %>
    Last Accessed Time<%= JspHelper.getDisplayLastAccessedTimeForSession(currentSession) %>
    Session Max Inactive Interval<%= JspHelper.secondsToTimeString(currentSession.getMaxInactiveInterval()) %>
    Used Time<%= JspHelper.getDisplayUsedTimeForSession(currentSession) %>
    Inactive Time<%= JspHelper.getDisplayInactiveTimeForSession(currentSession) %>
    TTL<%= JspHelper.getDisplayTTLForSession(currentSession) %>
    + +
    +
    + + + <% + if ("Primary".equals(request.getParameter("sessionType"))) { + %> + + <% + } + %> +
    +
    + +
    <%= JspHelper.escapeXml(request.getAttribute("error")) %>
    +
    <%= JspHelper.escapeXml(request.getAttribute("message")) %>
    + + + <% int nAttributes = 0; + Enumeration attributeNamesEnumeration = currentHttpSession.getAttributeNames(); + while (attributeNamesEnumeration.hasMoreElements()) { + attributeNamesEnumeration.nextElement(); + ++nAttributes; + } + %> + + + + + + + + + <%--tfoot> + + + + + + <% attributeNamesEnumeration = currentHttpSession.getAttributeNames(); + while (attributeNamesEnumeration.hasMoreElements()) { + String attributeName = attributeNamesEnumeration.nextElement(); + %> + + + + + + <% } // end while %> + +
    <%= JspHelper.formatNumber(nAttributes) %> attributes
    Remove AttributeAttribute nameAttribute value
    + TODO: set Max Inactive Interval on sessions +
    +
    +
    + + + + <% + if ("Primary".equals(request.getParameter("sessionType"))) { + %> + + + <% + } else { + out.print("Primary sessions only"); + } + %> +
    +
    +
    <%= JspHelper.escapeXml(attributeName) %><% Object attributeValue = currentHttpSession.getAttribute(attributeName); %>"><%= JspHelper.escapeXml(attributeValue) %>
    +<% } // endif%> + +
    +

    + +

    +
    + +<%--div style="display: none;"> +

    + Valid HTML 4.01! + Valid XHTML 1.0! + Valid XHTML 1.1! +

    + + + + diff --git a/webapps/manager/WEB-INF/jsp/sessionsList.jsp b/webapps/manager/WEB-INF/jsp/sessionsList.jsp new file mode 100644 index 0000000..03346a8 --- /dev/null +++ b/webapps/manager/WEB-INF/jsp/sessionsList.jsp @@ -0,0 +1,170 @@ + +<%-- + 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. +--%> +<%@page session="false" contentType="text/html; charset=UTF-8" %> +<%@page import="java.util.Collection" %> +<%@page import="org.apache.catalina.manager.JspHelper" %> +<%@page import="org.apache.catalina.Session" %> +<%@page import="org.apache.catalina.ha.session.DeltaSession" %> +<%@page import="org.apache.catalina.util.ContextName" %> + + + +<%@page import="org.apache.catalina.manager.DummyProxySession"%> +<% String path = (String) request.getAttribute("path"); + String version = (String) request.getAttribute("version"); + ContextName cn = new ContextName(path, version); + String submitUrl = JspHelper.escapeXml(response.encodeURL( + ((HttpServletRequest) pageContext.getRequest()).getRequestURI() + + "?path=" + path + "&version=" + version)); + Collection activeSessions = (Collection) request.getAttribute("activeSessions"); +%> + + + + + + + + + + Sessions Administration for <%= JspHelper.escapeXml(cn.getDisplayName()) %> + + +

    Sessions Administration for <%= JspHelper.escapeXml(cn.getDisplayName()) %>

    + +

    Tips:

    +
      +
    • Click on a column to sort.
    • +
    • To view a session details and/or remove a session attributes, click on its id.
    • +
    + +
    <%= JspHelper.escapeXml(request.getAttribute("error")) %>
    +
    <%= JspHelper.escapeXml(request.getAttribute("message")) %>
    + +
    +
    Active HttpSessions information + + "/> + <% String order = (String) request.getAttribute("order"); + if (order == null || "".equals(order)) { + order = "ASC"; + } + %> + + + <%= JspHelper.formatNumber(activeSessions.size()) %> active Sessions
    + + + + + + + + + + + + + + + <% if (activeSessions.size() > 10) { %> + <%-- is the same as --%> + + + + + + + + + + + + + <% } // end if %> + +<% + for (Session currentSession : activeSessions) { + String currentSessionId = JspHelper.escapeXml(currentSession.getId()); + String type; + if (currentSession instanceof DeltaSession) { + if (((DeltaSession) currentSession).isPrimarySession()) { + type = "Primary"; + } else { + type = "Backup"; + } + } else if (currentSession instanceof DummyProxySession) { + type = "Proxy"; + } else { + type = "Primary"; + } +%> + + + + + + + + + + + +<% } // end while %> + +
    Session IdTypeGuessed LocaleGuessed User nameCreation TimeLast Accessed TimeUsed TimeInactive TimeTTL
    Session IdTypeGuessed LocaleGuessed User nameCreation TimeLast Accessed TimeUsed TimeInactive TimeTTL
    + <% + if ("Proxy".equals(type)) { + out.print(currentSessionId); + } else { + %> + <%= currentSessionId %> + <% + } + %> + <%= type %><%= JspHelper.guessDisplayLocaleFromSession(currentSession) %><%= JspHelper.guessDisplayUserFromSession(currentSession) %><%= JspHelper.getDisplayCreationTimeForSession(currentSession) %><%= JspHelper.getDisplayLastAccessedTimeForSession(currentSession) %><%= JspHelper.getDisplayUsedTimeForSession(currentSession) %><%= JspHelper.getDisplayInactiveTimeForSession(currentSession) %><%= JspHelper.getDisplayTTLForSession(currentSession) %>
    +

    + +

    +
    +
    + +
    +

    + +

    +
    + +<%--div style="display: none;"> +

    + Valid HTML 4.01! + Valid XHTML 1.0! + Valid XHTML 1.1! +

    + + + + diff --git a/webapps/manager/WEB-INF/web.xml b/webapps/manager/WEB-INF/web.xml new file mode 100644 index 0000000..d9e83df --- /dev/null +++ b/webapps/manager/WEB-INF/web.xml @@ -0,0 +1,212 @@ + + + + + Tomcat Manager Application + + A scriptable management web application for the Tomcat Web Server; + Manager lets you view, load/unload/etc particular web applications. + + + UTF-8 + + + Manager + org.apache.catalina.manager.ManagerServlet + + debug + 2 + + + + HTMLManager + org.apache.catalina.manager.HTMLManagerServlet + + debug + 2 + + + + + 52428800 + 52428800 + 0 + + + + Status + org.apache.catalina.manager.StatusManagerServlet + + debug + 0 + + + + + JMXProxy + org.apache.catalina.manager.JMXProxyServlet + + + + + Manager + /text/* + + + Status + /status/* + + + JMXProxy + /jmxproxy/* + + + HTMLManager + /html/* + + + + CSRF + org.apache.catalina.filters.CsrfPreventionFilter + + entryPoints + /html,/html/,/html/list,/index.jsp + + + + + + + + HTTP header security filter + org.apache.catalina.filters.HttpHeaderSecurityFilter + + hstsEnabled + false + + + + + CSRF + HTMLManager + + + + HTTP header security filter + /* + + + + + + + HTML Manager interface (for humans) + /html/* + + + manager-gui + + + + + Text Manager interface (for scripts) + /text/* + + + manager-script + + + + + JMX Proxy interface + /jmxproxy/* + + + manager-jmx + + + + + Status interface + /status/* + + + manager-gui + manager-script + manager-jmx + manager-status + + + + + + BASIC + Tomcat Manager Application + + + + + + The role that is required to access the HTML Manager pages + + manager-gui + + + + The role that is required to access the text Manager pages + + manager-script + + + + The role that is required to access the HTML JMX Proxy + + manager-jmx + + + + The role that is required to access to the Manager Status pages + + manager-status + + + + 401 + /WEB-INF/jsp/401.jsp + + + 403 + /WEB-INF/jsp/403.jsp + + + 404 + /WEB-INF/jsp/404.jsp + + + diff --git a/webapps/manager/css/manager.css b/webapps/manager/css/manager.css new file mode 100644 index 0000000..5b50738 --- /dev/null +++ b/webapps/manager/css/manager.css @@ -0,0 +1,141 @@ +/* + * 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. + */ + +body { + font-family: Tahoma, Arial, sans-serif; +} + +h1, +h2, +h3, +b { + color : white; + background-color: #525D76; +} + +h1 { + font-size: 22px; +} + +h2 { + font-size: 16px; +} + +h3 { + font-size: 14px; +} + +p { + font-size: 12px; +} + +a { + color: black; +} + +.line { + height : 1px; + background-color: #525D76; + border : none; +} + +table { + width: 100%; +} + +td.page-title { + text-align : center; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-weight : bold; + background : white; + color : black; +} + +td.title { + text-align : left; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-style : italic; + font-weight : bold; + background : #D2A41C; +} + +td.header-left { + text-align : left; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-weight : bold; + background : #FFDC75; +} + +td.header-center { + text-align : center; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-weight : bold; + background : #FFDC75; +} + +td.row-left { + text-align : left; + vertical-align: middle; + font-family : sans-serif, Tahoma, Arial; + color : black; +} + +td.row-center { + text-align : center; + vertical-align: middle; + font-family : sans-serif, Tahoma, Arial; + color : black; +} + +td.row-right { + text-align : right; + vertical-align: middle; + font-family : sans-serif, Tahoma, Arial; + color : black; +} + +TH { + text-align : center; + vertical-align: top; + font-family : sans-serif, Tahoma, Arial; + font-weight : bold; + background : #FFDC75; +} + +TD { + text-align : center; + vertical-align: middle; + font-family : sans-serif, Tahoma, Arial; + color : black; +} + +form { + margin: 1; +} + +form.inline { + display: inline; +} + +img.tomcat-logo { + height: 92px; + float : left; +} \ No newline at end of file diff --git a/webapps/manager/images/asf-logo.svg b/webapps/manager/images/asf-logo.svg new file mode 100644 index 0000000..e24cbe5 --- /dev/null +++ b/webapps/manager/images/asf-logo.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/manager/images/tomcat.svg b/webapps/manager/images/tomcat.svg new file mode 100644 index 0000000..8823f79 --- /dev/null +++ b/webapps/manager/images/tomcat.svg @@ -0,0 +1,967 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 2006-05-09T08:17:21Z + 2006-05-09T08:37:38Z + Illustrator + + + + JPEG + 256 + 184 + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAuAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXhH/OYHnWfQ/wAurfRLSUxXXmK49GQqaN9VtwJJqH3cxqfYnFXhP5Y/ +85O+f/JU0enaw769okbBJLS8ZvrUKg0IhnarDj/I9R2HHFX2F+Xn5neT/P8ApP6R8u3glKAfW7KS +iXNuzdFljqaezCqnsTirK8VdirsVdirsVdirsVdirC/zM/Nvyd+XemC71255Xcqk2WmQUa5nI2+F +CRxUd3ag+nbFXx1+Zf8Azkn+YvneaW1tLh9C0NgwXTrB2V3Sm/rzji8m3UDitP2cVfV//OOfmabz +D+T3l+6uHMl1aRPYTsxqSbVzEhJ7kxKhxV6VirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd +irsVfHn/ADlxdSa7+bvlvyvGx4RW0EVARtNfXJVqf7BY+uRlKgT3JAt5r/zkD5ZGgfmfqSRR+nZ6 +gsd9agdOMq0f/ksj5h9nZvEwgnmNi2Z4cMiw/wAqebPMHlTXLfW9BvHstQtjVZEPwstQWjkXo6NT +4lOxzOan3v8Akl+cel/mX5a+tAJa69ZcU1fTlJojGvGWLluYpKbV6GqmtKlV6NirsVdirsVdirsV +eWfnr+eGl/lroywwBLzzPfox02wJqqL0+sT03EanoOrnYdyFXwh5i8x655j1i41jW7yS+1K6blNc +SmpPgABQKo6BVFB2xVnf5Q+SjrWh+d9Yli5w6XolylsadbqSNnTj8kiYf7IZg6zUeHKERzlIfL8U +3YoWCe4Pff8AnCfVTN5D1zTCamz1P11HcLcQIAPlWE5nNL6KxV2KuxV2KuxV2KuxV2KuxV2KuxV2 +KuxV2KuxV2KuxV2KvjD8wm/Sv/OX8UTGsdrqGnCMNUU+rW0Mp6f5ammY2sNYZ/1T9zZi+oe9m/8A +zkx+Xc/mPytFrunRepqehc3ljUVeS0cAyAU6mMqHA8OXfNB2PqhCfAeUvv8A2uZqcdix0fIedQ69 +m35OefrryN+YOla2kpjsjKttqqDo9nMwEoI78ftr/lKMVfaeqf8AOSH5KaaSs3meCZx0W1inuanf +YNDG69vHFWM3v/OYn5QW5YQ/pK8ArQwWqitPD1pIuvviqVT/APObH5cKR6GjaxIP2i8dqhB9qTvi +qmP+c2fIFd9C1Wnfa2/6q4qmFv8A85n/AJUSvxksdZtx/NJb25H/ACTuHOKp3bf85XfkpPBI7avN +BIisywS2lwGcqCeIZUdKmm1WGKvijzz5x1bzl5q1HzFqjlrm+lLrHWqxRDaOFP8AJjSij7+uKpNb +W1xdXMVtbRtNcTuscMKAszu54qqgbkkmgwE1uVfbHkL8uk8o/lTPoMiK+o3drPNqZHRrieIhlr4I +tEB9q5yWo1fi6gS/hBFfN2UMfDAjqwT/AJwdvyt/5usC20sVlOq77em0yMR2/wB2Cudc619ZYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXxZKTJ/zmFc+oedNTmA5b/ZtG49fCgpmH2h/ +cS9zbh+sPqDrsc4t2r57/Nf/AJxkGo3c+teSTFb3ExMlxo0hEcTMdybd/spU/sN8PgQNs3+i7Xoc +OX5/rcLLpusWIaF/zif56vFWTVr6y0pG6xgtczL81QLH90mZWTtnFH6bk1x0sjz2Z1pf/OIvlOIL ++lNbvrthSv1dYrZSe+zC4ND88wp9uTP0xA9+/wCptGkHUsms/wDnGf8AKS3AEunT3dOpmupxXam/ +pNFmPPtjOeRA+H67bBpoPDv+ch/yt03yXrdjeaFbG30HUouCQ8pJBFcQ0DqXkZ2+NSrCrfzeGbns +vWHNAiX1BxdRi4TtySH8jfJdn5u/MOy07UIfrGl28ct3fw1IDRxrxUEqQaGV0By7X6g4sRkOfRhh +hxSp9N3X/OO/5P3FSdBETGnxRXN0nT/JEvH8M50dq6gfxfYHOOnh3JDqP/OKn5a3NTazajYt+yIp +0dfpEsbn/hsvj21lHMRP497A6SPmwzW/+cQr9A76H5himO/CG9haL5AyxGT/AIhmXj7cifqiR7t/ +1NUtIehZh+S3/OP8Xk+5GveYXivNfTkLSKIloLYGqlwzBecjL3p8P45i9odqeIOCH09fNtw6fh3P +N7DfIz2VwijkzRuFA6klTmpxmpD3uRLk+bf+cJrrj+Yet2tT+90hpeP7J9O5hWp9/wB5tneunfZm +KuxV2KuxV2KuxV2KuxVZLNFDG0srrHGu7O5CqB7k4qks3nzyNC5jm8xaZHIOqPeW6nf2L4qmFhrW +j6iK6ff294KVrbypLt1r8BPjirAvzb/Pnyf+WrW9rqKS6hq90vqRaba8eaxVp6krMQEUkEL1JPbq +cVYFof8Azmp5BupVj1fR9Q0wNsZo/SuY1/1qGN6fJDir2Xyf+Yfkrzjam48taxb6iqgGSKNisyA9 +PUhcLKn+yXFWRYq7FXYq7FXxRrBNj/zl/NVwC+rL8XtcWw+Hf/jJTMXXC8M/6pbMP1h9SZxLtnYq +7FWG+afzg/LnyvdNZ6vrUSXqGj2sKvcSofB1hV+B/wBamZmHs/NkFxjt8mqWaMeZRPk78zvI/nF5 +ItA1RLm5hHKS1dXhmC1pyEcoRmXputRkdRosuLeQ2TDLGXJCfm/5JXzj5D1HSo05X8a/WtNPcXMI +JUD/AFxVP9lk+z9R4WUE8jsWOaHFGnl3/OI/lpodN1zzFMlGuJUsLcsKELCPUlpXsWkQfNc2Xbmb +eMPj+r9LRpI8y+hc0DmuxV2KuxV2Kvl//nClHP5oas4B4Lok6luwLXdqQPpoc9AdK+08VdirsVdi +rsVdiqXeYPMOi+XtIudY1q7jsdNtF5z3EpooHQAd2ZjsqjcnYYq+VfPf/OV3nXzNqp0D8stPlto5 +mMcF0IfrGoT+8UIDrGD8mbvVcVSqz/5xn/Pjzs66h5t1RbUueX+5W7kurgA/yxx+sq/6pZaeGKsj +h/5wanMYM3nNUk7qmml1/wCCN0n6sVQt7/zhDr8B56Z5stppEIMZntZLfcb1qkk9KHFXzr5mtdUs +tfv9O1S5a7vtOuJbKaZndwWt3MZ4mSjcartUDFUsxVFabqeo6XfQ3+m3UtlfW7c4Lq3dopUbxV1I +IxV9Sfkr/wA5aNcT2+gfmG6K8hWO18wqAi1OwF2q0Vf+Mi0H8w6tir6lVlZQykMrCqsNwQe4xVvF +XYq+Kfzzro3/ADlLa6oxKJLdaReFiaApGsMLeG1ISMqzw4sco94LKBogvqPOEdw7FXkf55/mBrlj +Jp3kbykX/wAVeYSFE0Zo8FuzFOSt+wzlW+P9lQx2NDm27N0sZXlyfRFxs+Qj0jmUd5B/IHyP5bsI +31Oyh1zWnAa6vb1BMnqHciKKSqKAehI5e+Q1XamTIfSeGPlzTj08YjfcsJ/PDy5pXkHX/LH5geW7 +WPTGhvlt9Rt7RBFHKpBk+wgCjnGkiPQbg5m9m5jnhLFM3s1Z4iBEg+hOu4zn3NQOkaLpuj20ltp8 +IghlnnunRe8tzK0sh/4JzQdhtlmXLKZuXdXyYxiByR2VsnYqxjV/zO/L3SJWh1DzDYQzoaPD66PI +p/ykQsw+kZlY9Dmnyifu+9qOWI6pvoOvaRr+kwato9yt3p1zz9C4UMob03MbbMFOzoR0ynLiljkY +yFEM4yBFhV1WVYdLvJWJCxwSOxHWioTjhFzA8wsuRfPn/OEVoX83eZLzekOnxQnpSsswb/mVneOn +fYOKuxV2KuxV2KqF9e2lhZT315KsFpaxtNcTuaKkcYLMzHwAFcVfFHnPzR50/wCchPzJi8veXlaH +y7aO5sYnqsUUCkK97dU/bYdB2qFXcklV9U/lj+UnlH8u9IWz0a2WS+dQL7VpVBuLhh1q37KV+yg2 +Huakqs1xV2KuxV8v/nf/AM4patrnmG+80eSp4Xn1GR7m/wBIuW9ImdyWd4JSOH7xjUq9KGvxb0Cr +5/1j8mPzX0iRkvfKepgL9qSC3e5jG9P7yASJ1PjiqRjyb5vMvpDQ9QMtePpi1m5culKca1xVPtG/ +JT82dYdUsvKepUf7MlxA1rGe395cekn44q+zf+cffKv5m+VvJ50bzvPbzRwFf0RFHK01xbxU+KCV +6cCqmnDizU3FaUAVeo4q7FXx5/zmxpD2vnTy7rcdUN5YPbh12POzmL1qO4FyuKsl/Lz/AJyc8ra2 +sNj5mUaHqZAU3TGtnI3Qnn1ir1o/wj+bOY1XY8474/UO7r+1z8epB2Oz2iKWKaJJYnWSKQBkkQhl +ZTuCCNiDmnIINFygVGXTNOmvYb6W1hkvbbkLe6eNWljDgq3ByOS1UkGhwjJIDhs0ei0LtE5FLxD/ +AJyycP5F0ezQcp59WjaNdt+NvMp/GQZuuxI/vJH+j+lxNWfSPe9rgiEMEcQNRGoQE9+IpmmlKyS5 +QCpgSsllihieWVxHFGpeR2NFVVFSST0AGEAk0EEvn2fVfOv5269e6foN9Jof5e6fIYbm9QMst2af +ZIBUtyG4QkKqkFqmgzfiGLRQBkOLKfx+C4ZMspobRZzof/OOv5U6VCiyaUdSnUUa4vZZJGb5opSL +7kzBydrZ5HY8PuDbHTQDP9G0XStE02HTNJtks9Pt+Xo20Qoi83LtQe7MTmBkyynLikbJboxAFBJv +zO1Aaf8Al35lu60ZNNuljP8AlvEyJ/wzDL9FDizQH9IfYxymol59/wA4P6S0eg+adXI+G6ura0Vv +e2jeRgP+kkZ2zqX01irsVdirsVdir50/5zJ/MGbSfK1j5PspOFxrrGa/KmhFpAwon/PWWn0KR3xV +mf8Azjd+WEPkj8vrae5iA17XES91KQijorrWG333HpI24/mLYq9YxV2KuxV2KuxV2KuxV2KuxV2K +obUdT03TbR7zUbuGytI/7y4uJFijX5u5VRir5U/5yz/MX8tfNfl7S7DQtZh1LW9NvS5W2V3iFvJG +yyUnC+kfjVPsscVSv8i/yi/LTzn5Ij1XVLSafU4J5rW9C3EkaFlIdCFQrT926980XaOuy4cnDGqI +vk5eDDGQsvdvKXkby35StXtdBgmtrZ6Vge6uZ4wf5ljmkkRCe5UCuaPPqp5Tc9/gHLhjEeSN8x3+ +o6foGoX2m2hv9QtoJJbWyFazSKpKxjjv8R22yOCEZTAkaBZTJAsPHv8AlcP53/8Altpv+BuP+ac3 +H8n6X/VPti4vjZP5rzz8wfPP5i+bfNvluw1Dyq1rqWjzG+g0ROZmuRVZDVGHPjxgbcDpXNhpdNiw +wkYy9Mutj8dWnJOUiAQ9D/5XD+d//ltpv+BuP+ac1/8AJ+l/1T7Yt3jZP5rv+Vw/nf8A+W2m/wCB +uP8AmnH+T9L/AKp9sV8bJ/NYp+ZX5v8A5qXnli40LVfKbaCutAWkdyxlWRwWXnHGrheRdfhI8DmV +pNBgE+KMuLh9zXkzTIoirR/kbzf+bvlHy1Y+XtO/LedobYENM6zK0kjtyeRzxoOTH6BtkNTp9Plm +ZyyfaEwnOIoRej+RPO35o6xr62fmPyf+hdNMTub71C1HWnFaV/azX6rS4IQuE+KXds348kyaIZ7q +jaqthKdKSCS/pSBbp3jhr4uY1kbbwA38Rmux8PF6r4fJuldbPlv8+YvzstdPS483apafoO7nEEVh +pcjJbl6NIA0bKkjgenWsnKhpnTdnHTH+7HqHfz+f6nAz8f8AFyfQ3/OLHl06N+TWkyOnCfVpJ9Rm +Hj6r+nEfphiQ5t3GeuYq7FXYq7FXYq+MfzQhXzz/AM5YWmgz1lsLe7sbB4zvW3gRbi5TvSrNLir7 +OxV2KuxV2KuxV2KuxV2KuxV5j59/5yM/K7yb6kFxqQ1TU0qP0dpvG4cMO0kgIij36hn5e2KvAvMv +/OWP5p+arl9P8laWukxtXiYIzfXvHpUuy+mg+UdR/NkJ5IwFyIA80xiSaDF/+VT/AJo+b7sah5w1 +h1kavx3sz3k617KgYoo9uYp4ZptR7QYIbRuZ8uXzP7XMx6GcuezJYf8AnH3yrBptwjXFxd6g8LrB +NIwSNJSpCOEQA7NvRmOak+0eQzGwjCxfU11/FOT/ACfEDnZYH+S+sfmZZeajoHlC8htrq6ZnubC/ +K/VnMAPLkrAtyUdfT+Kg8BnSa7HhMOLINg6/CZA1F9k6KdbOmw/pxbZdTp/pH1IyNAW8U9UK9Pnn +I5eDi9F8PnzdlG63R2VsmndUUu5CooJZiaAAdSTiBaHhP5N8/On5r+bPzEkBbT7dv0do7EGhWgUM +tRswgjUsP+LM3vaH7nBDCOZ5/j3/AHOJh9UzJ7vmicx2KvEf+clQLS78i63cEjT9O1cC6O3H4mjl +FR/qwPm77G3GSPUj9f63E1XQvbQQQCDUHoc0jlN4pSXzN5z8q+V7ZLjX9Tg0+OSvpLK37x+PXhGv +J3pXfiMuw6bJlNQFsJ5BHmXzJ+dn5haf+Z/mby75e8qtLPbLN6EbyI0YluruRI0oh+KigChIHU50 +/ZmilhieL6i4GoyiZ2fbWh6Ra6Noun6PaClpp1tFaW4/4rgQRr+C5s3HR2KuxV2KuxV2KvjfymCP ++c0p/rdK/pTU+POlKfUp/S/4144q+yMVdirsVdirsVdirsVeQfmX/wA5Ofl55MaaxtZv0/rcdVNl +ZMDEj+E1x8SL4ELyYdxir5W/Mf8A5yD/ADJ88GSC6vjpmjyVC6VYFoYmQ1FJXr6kte/I8fADFXme +Kvpj8jdTtb3yJBFFGkdxYyyW9zwVU5MDzRzTqSjipPU1zhvaDHKOosk8Mht5d/6/i7rQSBh5h6Fm +ic12Kvnvz6l35B/Nqz8z2CEQyzLqMSqeIY143UVf8upr7Pnedl5RqdLwS5gcJ/R9n2uj1MPDyWPe ++wdL1Ky1TTbXUrGQTWd5Ek9vKOjJIoZT9xznMkDCRieYc2JsWisgyYZ+b1p5vvfIGqWPlSFZ9Tu0 +9F1LiN/q77TelXYuV+EAkddt6A5vZ8sccoMzsPv6NOYSMdnzl+Wn5m/mVoKR+RtEtNLsrmGWSsOp +q1vM87t8Su8ssS+p0UKaGgAGdDqtHhyfvJ2fd3fBwseWUfSHq36V/wCcqf8AqzaN/wAGn/ZRms4N +B/OP2/qci83c79K/85U/9WbRv+DT/sox4NB/OP2/qW83c8o/Mj8z/wAy/MAm8i6zaaZfXU0sY9HT +Ea4lSdGqqxvFLKvqbFSBXqQc2el0eHH+8jY2693xcfJllL0l9KflXb+bbXyJpVp5riWLV7aIQsqu +JGMSbRGUio9ThQNQnx70znNccZyk4+R+9zsIkIi2W5iNqB1xdH/RF2+sxQy6XFE8t4tyiyRelGpZ +i6uCpAAyzFxcQ4D6ixlVb8nzj/zjB5UtfNn5xal5tisltNE0Rpbu1tEUCOOa6ZktYgBt+7j5tt3U +Z3UIkRAJt1BO77PySHYq7FXYq7FXYq+M/wAyX/wb/wA5b2WsP+7s7q90+7Zz8NILlEt7htqV3EmK +vszFXYq7FXYq7FWGfmR+bnkn8vrD6xr16PrkilrXS4KPdTdacY6jitRTmxC++Kvjz80/+clPPvnk +TWVq50Py45KfULRj6kqntcTjiz1H7K8V8QeuKsQ/KyLyvP5wtbTzFbC4trn91bc2IjW4JBj9QAjk +G+zQ7VIrmB2mcowE4jUh93Vv0wiZgS5Po7zD5J8ta/pa6bf2UfoQrxtWiAjeDbb0io+Hp06eIzht +N2jmwz4oyu+d7373dZNPCYoh8/effyj17yuZLu3B1DRgSRdRr8cS9f3yD7P+sPh+XTOz7P7Wxajb +6Z936u90+fSyx78wnP8Azj5r4s/M11o8jUi1OHlED/v63qwA+cbP92YvtDp+PCJjnA/Ydv1NugyV +Ou99C5xDuWDeefKvnzV9WiufL+v/AKKskt1jkt+Ui8pQ7sX+AEbqyj6M3XZ2t02LGRlhxyvnQO23 +e4eow5JSuJoe8sD81/lL+ZF9pj3Go65Hq7WKPLBbMZGc7VZY+S9WC9O+bnSdsaQTEYQ4OLyAHxou +Jl0mWrJuvel/5Q/8rK80ySeXdA85S6P9Qh9W2spZ51RouXx+kEDD4CwqPfbvmz1pw4xxzhxX5Bxc +XFLYGnv35Y+RfzR0DXri881+af03p0lq8MVp6s0nGZpI2WSkiqNkRh9OaLW6rBkgBjjwm+4D7nMx +Y5g7m3p2axyGGfmF+U3k/wA82pGq23paii8bfVIAFuEpWgLU+NN/st9FDvmZpddkwnbePc1ZMMZ+ +95R/iv8AMz8lbm20/wAzMPMvk2Z/Ssr5XpcIBvxXmSwKr/ut6r2Vxm28HDrAZQ9OTr+P0uNxzxbH +cNSeb/zJ/Om9uNM8pk+XPJ0Lelf6g7D13DD7L8DyJZf91oafzNTEYMOjAlP1ZOn7P1qZyymhsHrH +5d/lN5R8i2gXS7f1tRdaXGqTgNcPXqAeiJ/kr9NTvmq1euyZjvtHucjHhEPezPMJuePedvy3/OXV +fNF/qGg+c/0ZpM7KbWx9a4X0wI1VhxRSoqwJ2zc6fWaaMAJQuXuDizxZCbB2eNfm7F+Z3lQQaDr3 +nKXV21SJmm0+GedgIQwCmVXC7OwIUd6HNtopYcvrhDhrrQcbKJR2JeieSv8AnHD8+9H0SJtG83Q+ +XlvlS5udPinuonSR0Hwy+nHxLqPhO5zYtD2r8mvJH5m+V/0x/jjzN/iL659W/R/76eb0PS9X1f75 +Vpz5p08MVel4q7FXYq7FXYq+Xv8AnNjya81joXnG3Sv1Vm0y/YCp4SEy25PgquJB82GKva/yY87J +5z/LXRNbaTneNALfUfEXVv8Au5SR25leY9mGKs2xV2KrZJI4o2kkYJGgLO7EBVUCpJJ6AYq+aPzm +/wCctrTTWn0L8vmjvL1ax3GvOA9vEehFsh2lYH9tvg8A1cVeMfl95AvPzCvLrzP5l1SW6iNwUueT +tJdTyqqsQ7tXgvFgPGmwp1zS9rdrflqjEXMj4OZpdL4m5Oz3O18seXrXSP0PDp0C6ZSjWhjVkb3c +NXk3ud842etzSyeIZHi73bDDAR4a2eaeb/yBsLlmvPK9x9QuQeX1OYs0JPX4JN3j/EfLN9ovaIj0 +5hfmP0j9XycLNoBzh8noHku+1y50OKLXrV7XWLT9xeB6FZGUCkyOvwsHG549DUds03aOLHHJxYiD +jluPLy8v1OXp5SMakPUE9IBBBFQdiDmCDTe841/8pLaHW7bzL5U42OqWkyzvYfZt5+JqyrT+6LrV +f5fl1zoNL21xQOLPvGQri6j39/3+9wMujo8UOY6PSB06U9s54uewnzt5H8z69qsV5pXme60W3jgW +F7WAyhWcO7GQ+nLGKkMB07Zt9BrsGGBjkxiZvnt5d7iZ8M5m4ypj/wDyqbz9/wBT/f8A/BXP/ZRm +d/K+k/1CPyj+pp/K5f55+15z518keZ/y91G01W01SZ2nLiPVrYyW8qTMDzQurFgXQnfl8Qrm90Pa +GLVxIrl/CXCz4JYiHv8A+Qeia/NDH5tufO155k0u+s3gGm3Tzt9XufUjZuQkmlUPHwZdh0NQaHfV +9qTgP3YgIyB57bhv04PO7eyZp3KYZ+afm/zN5Z0KGby5okmtanezC1gVAXSF3UlXkRPjYbdqDxYd +83Q6eGWR45cIG7TmmYjYMC8p/kVrGu6ovmj81b1tV1Njyi0YODBEOoWQp8FB/vuP4fEtXM7P2nGE +eDAKHf8Aj7y1QwEm5orzX+Rd9pepP5n/ACuvm0HWlq0mlhqWc46lFBqqV/kYFP8AVyODtMSHBnHF +Hv8Ax9/NM8BBuGxZB+VP5j+ZPMs9/ovmbQJ9J13R1Q3s3ErbPzNEoGPJWehIA5KQKhu2Ua7RwxgT +hK4yZYcplsRuHo2a1yHh35u+SvN1nNrXnD/lYl/omiIFli0yB7gBSEVFiiC3EacpHGwAG5zd6HPi +lw4/DEpd+3z5OJmhIXLi2eW/lJ+UXnn829Svtdl1ue0XTjGo127MtzM9ytDHHG5dXrGg5E8vh+Hx +zo4QERQFBwSSeb2z/oXX86P/AC8Gq/8AI2+/7Kskh6L+UP5dedPJv6W/xN5wu/Nf1/6v9U+tvO/1 +f0fV9Th68s3956i1pT7OKvRcVdirsVdirsVY/wCf/J9l5x8nar5bvKLFqMDRpKRX05R8UUlP8iRV +b6MVfLf/ADiz50vvJX5han+XXmGtsmoztDHE/SLU4Dw4jt++Qca9yEpir7ExVK/MnmbQvLOjXGs6 +5eR2Om2q8pZ5TT5KoG7M3RVUVJ6Yq+M/zS/PHzr+bWrnyv5Vt5rPy67fDZoaS3CqaerduDRU/wAi +vEd+RplWbNDFEymaiGUIGRoc0Nc/846uugI1vqXPX1BaRGFLVtv7tTTmtP5z18BnOw9pInLRj+77 ++vv/AB9rsD2eeHY+pV/Io6rofmDWPK2rwSWlzJEl3FBIKCsbem5UjZuYddxUHjke34xy4YZYGwDW +3n/YuhJjMxL2rOSdq7FXYq7FXYq7FXYq7FUt8w6Bp2v6Pc6VqCc7a5XiSPtIw3V0J6Mp3GZGl1M8 +GQTjzH2+TXlxicaLxryB5w1r8nPPM+i63yl8v3rKbrgCVKE0ju4V8R0ZR13HUDO3ywx67CJw59P1 +H8ebpgZYZ0X1xZXlpfWkN5ZyrPa3CLLBNGQyOjiqspHUEZzE4mJo8w54N7q2RS7FXYq73xVTuLi3 +treS4uJFht4VMk00hCoiKKszMdgAOpwxiSaHNBNPlfzv5j8wfnh+Yll5O8qBhoVtKTFKwIQqvwzX +047IgNEB33p9p6Z13Z2iGGNn6zz/AFOtz5eM+T7B8j+TdG8m+V7Hy7o8fCzso+Jc/blkO8ksh7s7 +bn7htTNi0J9irsVdirsVdirsVdirsVfLP/OXf5WXENxb/mXoKNHNCY4tbMNVdWQhbe7BG9RtGx/1 +PfFWefl3/wA5I+VdQ/KqTzN5mu0ttV0YLbavarT1Z7gqfSaCPbl9YCkgdFIb9la4q+cvNPm3z/8A +nr5uCUNnolo1YLRSxtrOIkgSSdPUmYd+p7cV6Yms1mPTw4pn3DqW3FhlkNB695O8l6J5U00Wemx/ +vHAN1duB6szDux8B2XoM4LXdoZNTK5cug7vx3u7w4I4xQT/MFvUJbGzluYbqSFGubfl6ExA5oHFG +AbrQjqMsjmkImIPplzDEwBIPUNahew2Nhc3s54wWsTzSt4JGpZj9wxw4zOYiP4iB81nLhBPc8w/J +Tzn5v8y3mqHV7oXFlaIhjHpojLJKxIAZQtQFQ9a50XbujwYYRMI8MifsH4DgaLNOZNmwHq+cy7F2 +KuxV2KuxV2KuxVjXnzyLpnm/SDZ3P7m7hq9leAVaJyO/ijftL/EDNj2d2jLTTsbxPMfjq4+o04yD +zeb/AJZ/mj5g/KrXZPKnmyKSTQS9QFq5t+Z/v7c/txP1ZR8x8VQet1Gmx6vGMmM+r8bF1UJyxS4Z +PqrTNT0/VLCDUNOuI7qyuVDwXETBkZT3BGczkxygeGQohzgQRYRWRZOxVSurq2tLaW6upUgtoVLz +TSMEREUVLMxoABhjEyNDcoJp8v8A5n/mrr/5n65D5E8hQTTadcy+kxQcZL1lNeTV+xbpTl8VNvia +nTOp7O7OGL1S+v7v2uvz5+LYcn0j+SX5N6V+Wvlv6uCl1r96FfV9RUGjMKlYoq7iKOu38x+I+A2z +jPR8VdirsVdirsVdirsVdirsVSDz3rvlfQ/KWp6h5oaMaGsDx3kUgDCZJFK+iqEjm0leIXvir81d +SfTpdTupdPhkt9MedzawyMJJI4WYmNGeihmCbV74q+q/y8tfLEHlOyPlsV06VefqGnqvJ0czH/fl +RQ+HQbUzzrtWeY5z4v1D5V5eTv8ATCAgOFkma5yHYq7FWIfm3qBsfy81mRftSxLbge08ixN/wrHN +r2Jj4tVHys/Z+txdZKsZSD/nH3TRb+S5rwj4767kYH/IjVYwP+CDZm+0mQnNGPQR+/8AAauz4+gn +zenZzrnuxV2KuxV2KuxV2KuxVjnnbyLovm3Tfqt+np3MYJtL1APUiY+Feqn9pe/zocz9B2jk00rj +vHqPx1aM+njkG/N4/ovmf8xfyX1w2rr9b0W4fkbVyxtLgDq8T0Jikp12r4gimdkPA12PiHP7R7/x +7nUETwyovpX8vvzc8m+eLZf0ZdCDUgKzaVcEJcKR1KitJF/ykr70O2aHVaDJhO4uPf8Ajk5ePNGX +vTXzl578seTtMOoa9eLboa+hAPimmYfsxRjdj+A7kZVp9LPMaiP1Mp5BEbvmXzJ54/Mb87vMcflj +y1ZyQ6SzhksENFCKf96L2YbcV60+yDQAM1Cep0eghgF85d/6nX5cxn7n1H+S35IaB+Wmkkxlb3zD +eIo1LVGHyJhgrukQbfxbqewGe0vSsVdirsVdirsVdirsVdirsVQup6np+l6fc6jqNwlrY2kbTXNx +KeKJGgqzMfYYq+HfzQ/MTzL+dvnmHSNFR4PLtm7fo+2eoUIKh7y5pX42BoB+yPhG5JajU6mGGBnM +7BnjxmZoPQ4Pyv8AK8fk1vK5i5W8g5yXVAJjcU2nr/MO3am3TOGl2xmOfxfs6V3ft73dDSQ4OH7X +kehaz5g/KfzbLpWqK0+jXLB5VQfDJGaqlxDU7MKfEv0HsR0uowYu0MAlA+ocvI9x/HmHXY5ywTo8 +n0Fp2o2OpWMN9YzLcWlwoeGZDUEH/Pcds4jNhljkYyFSDuYTEhY5KzTQoaPIqnwJAOCOOR3AKmQH +VyzQueKyKx8AQTiccgLIKiQPV5t/zkDctD5FijHS5voYm37BJJP1x5vPZwf4Qf6h+8OH2h/dj3p3 ++UNt9X/LnRkoQXjklNRQ/vJnf9TbZjdtyvVT+H3Bs0Y/dBmOalynYq7FXYq7FXYq7FXYq7FUHq+j +6ZrFhLYanbJdWkwo8Tjb2II3Vh2I3GXYNRPFLigaLCeMSFF4R50/JTXdCnOq+VpJby1ib1FjjJF5 +ARuCvGhenYr8Xt3zstB25jzenJ6Z/Yf1fF1OfRShvHcJFJ5F/M7zRY3PmTUI7m8eKMFHvZHa6mRe +0SvV2CjcdK/s1OZsu0NNimMVgHy5D39zQMGSQ4qfTP8AziV518hXnlX/AA3p1lBpPmi0XnqUIr6l +6F2+sq7lnfr8SV+A9AFIzYtD6BxV2KuxV2KuxV2KuxV2KuxV2KvjX/nI7847/wA+eYk/L/ye7XGj +QTiO4kgNRfXSnswNDBEeh6Egt0CnIZMkYRMpGgExiSaDJvy88h2PlDRRbJxl1G4o9/dAfbcDZVPX +gn7P3988/wC0+0Zamd8oDkP0+93um04xx82vOP5meVvKoMV7OZ7+lVsLejy+3PcKg/1j8q4dF2Tm +1G4HDDvP6O9c2qhj25l47r/mfzt+ak6aXovlxrmO3f1I47SF7meOuxLzAURT32UZ1/Z/ZcNNdEkn +n3fJ1OfUnJzDFvNXl7z35Lu/8P8AmCG60uQoLhbNpaxMsg+2nps0TVpQkHqKHcZseEXdbtFsbySH +Yqu9ST0/T5H068uFTx5UpWnjir2HyZ+T/wCfGr+U9O1/yreSS6VdKzWkEOo+iQI5HRlMcjxoPjjI +pXKMmmxT+qMT7wGcckhyJCOudA/5yq0IfvtM1G4VDuscNvqFadqwidj07HMXJ2Tpp84D4bfc2x1W +QdUvl/Oj8y9CmEPmHQ0iPQpc209pKT1/aNP+FzCyezunly4o/H9bbHX5Bzop1pv/ADkboslBqWkX +FsfG3dJx8/j9HNfl9mZfwTB94r9bkR7RHUMv0r82/wAvtSoserx28ndLoNb0/wBlIFT7mzWZuxdT +D+HiHlv9nP7HIhrMcutMst7i3uIlmt5Umib7MkbBlPyIqM1s8coGpAg+bkxkDuFTIJdirsVdirsV +dirH/PXm608q+XZ9Umo8391ZwH/dk7A8V+Qpyb2GZ/Z2iOoyiP8AD19zRqMwxxvq+cfL9n+Yf19/ +Omi29ytzYytfnU41CgPyLOyhqCTqeSqDt1FM7+WoxYyIGQBOwDoxjlIE0+1/yK/O7S/zJ0IpP6dp +5nsVA1LT1OzrsPrEAO5jYncdVOx/ZJyGt6jirsVdirsVdirsVdirsVfO/wDzlT+dh8vaa/kfQJ6a +7qUf+5S4jPxWtrINoxTpJMD8wm/7SnFWA/k3+W48v6eNZ1OL/c1ep8EbDe3hbfhQ9Hbq3h08a8V2 +52n4svCgfRHn5n9Q/HR3Gi03COI8yl/5qfm5LYTt5d8sP6mqM3pXd3GOZiY7elFStZa9T+z0+10v +7I7G4gMmUbdI/pP6mGr1demPzZX+UH/OJcl6I/MP5lNKZJj6sehB2EjV35XkoPKp68FNfFuq51wF +OqfT2j6Jo+i2Een6RZQafYxf3dtbRrFGPfigAqe5xVj35mflh5Y/MLy++k61CBKgLWGoIB69tKf2 +o2PY0HJejD6CFXwV+Z35WeaPy715tL1qHlbyFmsNRjB9C4jBoGU/st/Mh3X5UJVYdirsVfb3/OHX +mKPUfyrfSS9Z9EvpovTrUiK4/wBIRvYM7yD6MVe7YqsmhhniaKaNZYnFHjcBlI8CDtirDde/JX8q +Ne5HUvK1g0j15zQRC1lJPcyW/pOT9OKvMfMn/OF/5eXwZ9D1K+0aY/ZRit3AP9g/CT/krirzTVv+ +cTvzh8tSPdeVNVh1EDoLS4exuWp4rIVj/wCSpyGTHGYqQBHmmMiNwxq58/fnT5ImW382aVMYgeIO +oWzRch0pHcRhUfp1+LNVn7C02TcDhPl+rk5UNbkj1tlGgf8AOQHlS94x6rBNpUx6uR68P/BIOf8A +wmaPUezmWO+MiX2H9X2uZj7QifqFPRNK1vR9Wg9fTL2G9iHVoHV6V7NQ7H2OaTPpsmI1OJi5sMkZ +cjaNyhm7FXYqlGq+VNC1fULe91S2F69opW2hn+OFCxqzekfhLGg3avTbMzDrsuKBhA8N8yOfz/U0 +zwRlKzumyqqqFUAKBQKNgAO2YhJJttp84edta0nyl+Y0Gu+Qr/0NQtH9W4WAfuI5wfiRSDxdJBUO +lOPUd6D0PsqWc4R4w36d5Hm6HUiAn6H2P+TH5xaN+ZXlwXcIW11u0ATVdM5VMbnpJHXcxP8Asnt0 +PTNk470PFXYq7FXYq7FXYqwf84fzP078uvJtxrU/GXUJawaTZMf765YbVA34IPic+G3UjFXyR+U/ +lPUvNnmK589+ZXa65XDzRPKB/pF2Wq0h7cIz0AFK7D7NM5/tztLwo+HA+uXPyH6z+OjnaLT8R4jy +DOPzf89t5Y8v+hZScdX1HlHbEdY0A/eS/MVovufbNJ2J2f4+TikPRD7T3fr/AGubrM/BGhzKf/8A +OK/5HQWtjb/mF5ltxLqV3+90K2mBPoxHpdMD1kk6x+C/F1O3dukfTGKuxV2KpL5v8neXfN+hz6J5 +gs0vLCffi2zxuPsyROPiR17EfqxV8N/nR/zj/wCZfy5umvYeep+VpXpb6mq/FFyPwx3Kj7Ddg32W +7UO2KvKcVeu/84z/AJoQeRvPwi1KX0tC11Vs7+RjRIpA1YJ29kZipJ6KxPbFX3sCCKjcHocVbxV2 +KuxV2Kqc9vBcQvBcRrNDIOMkUihlYHsVNQcVeX+cP+cZ/wAovM3OQ6QNIvH/AOPrSmFsQf8AjDRo +D/yLrirw/wA0f84fef8AQZ21DyRrKal6dTHEWNhejwVH5GJvmXT5ZGURIURYSCRyYf8A8rL/ADW8 +jXo03zjpUslK8Y7+JreVlXasU6rxdf8AKo3zzT6rsHBk3j6D5cvl+qnLx62cee7P/LX5zeSdbKxS +XJ0y7bb0byiKT/kygmP5VIPtnO6rsLPi3iOOPlz+X6rc/HrYS57FnSsrKGUhlIqCNwRmmIINFywW +8CWLebfLnmTzCG0+PVV0jRm2n+rK0lzOpG6s7FFjXtRa17nembXRavBp/VwmeTz2A93P5uLmxTnt +dRSjR/yO8g6cVea2l1GVTUPdyEiv+pH6aEfMHL83tBqJ/TUfcP12whocY57sS80+XfMH5YeaLfz3 +5JdorSKStxbAExxBz8UUigjlbydP8n58Tm97H7WGccE/7wf7L9vf8/dhavS8BsfT9z6x/Kf81NB/ +MbyzHq2nEQXsVI9U0xmDSW03genJHpVHpuPAggb1wmbYq7FXYq7FVK6ure0tprq5lWG2gRpZ5nIV +ERByZmJ2AAFTir4W89eZtV/PD81xHas8Xlyw5RWXb0bJGHqTsDt6s7U/4Vei1zE12rjp8Rmfh5lt +w4jOVB7Zp2n2enWMFjZxiG1tkWKGMdAqig655xmyyyTM5G5F6CEREUOTxPS9Gb81/wA/YNJlLNo1 +tMUuKbUsrEky0I6es9QD25jPQ+zNL4OCMevM+8/inQ6nJxzJfdcUUUUSRRIscUahY41AVVVRQAAb +AAZntC/FXYq7FXYqo3dnaXtrLaXkKXFrOpjnglUOjowoVZWqCD74q+T/AM7f+cTri0a48wfl7E09 +pvJdeX6lpY+5NqTu6/8AFZ+Ifs16BV8xyRyRSNHIpSRCVdGBDBgaEEHoRiqLv9b1nUEjS/v7m7SF +VjhWeV5QiIOKqocmgUbADFU/8k/mp588l38N1oOrzwxREcrCR2ktJFH7MkDHgRTaoow7EYq/Qb8v +POFv5y8laR5mt4/RXUoBI8NeXpyqxjlQNtULIjCuKsixV2KuxV2KuxVB6rpGlavZSWGq2cF/ZS7S +W1zGssbfNHBGKvD/AD5/zh75B1r1Lny1PL5cvmqREtbizY/8YnYOlT/K9B/LirxDWPy7/Pr8pmea +GKW90OI8nuLOt5ZcQakvERzhHixVfnmJqdDhzj1xvz6/Ntx5pw5FNvKv/OQWi3fCDzDbNp0/Q3UI +aWAmnUqKyJv2+L55zWr9nJDfEeLyPP58vudhi7QB2kKepWGo6fqNst1YXMd1bP8AZmhcOp+lSc57 +LhnjPDMGJ83YRmJCwbROVMlk0MU8LwzIJIZVKSRsKqysKEEHqCMlCZiQRsQggEUXiepWHmf8m/OM +PnDyiS+jSH07i3erxhHYFrafuY2oOD9QadwCe77J7UGojwy2yD7fN0mq0xxmx9L7C/Lr8wvL/n3y +zBr+iyExSfBc2z/3tvOAC8Ug8RXY9CNxm5cRk+KuxV2Kvm7/AJzA/NOTTNHg8haVKRf6ugn1ZkJ5 +JacqJDt3mdTyH8op0bFUg/KjyOvlfy2n1iMDVr8LNfsaVXb4Ia/8Vg7/AOVXOB7Z1/j5aH0R5fpL +vNJg4I2eZZRr1/8Ao/Q9Rv8A/lktZp/+RUZf+Ga7SwE8sInkZAfa35ZVEnyYp/zg/o0Ump+atccV +mghtbKJu/Gd3ll/GBM9PecfWeKuxV2KuxV2KuxV2KvOfPf5Aflj521UatrGmtHqRFJ7m0kMDTdKG +Xjs7CmzUr+GKsb/6FD/Jv/lmvv8ApLb+mKu/6FD/ACb/AOWa+/6S2/pir0/yZ5Q0byf5as/LmirI +mmWPqfV1lcyOPWleZ6sevxyHFU7xV2KuxV2KuxV2KuxV2KvMfzC/5x1/LLzr6lzcaf8AovVn3/Se +ncYJGbrWSOhikr3LLy9xir5080f846/nH+XVzJqnlK6k1nT1NTLpwYXHFenrWR58/kvMZTmwQyx4 +ZgSDKEzE2DSH8r/85ABZRZea7IwSoeD3lup+FgaH1YT8Qp34/wDA5zes9nBzwn4H9B/X83Y4u0Ok +w9b0nWdK1e0W80y7iu7ZukkTBgD4Hup9jvnM59PkxS4ZgxLsYZIyFg2q31jaX9pNZ3kKz2s6lJoX +FVZT2ORxZZY5CUTUgmURIUeTxy2svzN/KLzbcaj5Eil1DS9RRkNuIZLqMqDVUnij35Rk/A+3z3YZ +3Wg7YxZYXOQhMc7NfK/wHS59JKMthYZVB/zlL+eWlMZNc8owTWiEmRzaXlsaClaS83jp/sTmxx6r +FM1GUZe4guPLHIcwQ9C8jf8AOYH5ea7NFaa9bzeW7uUhRLMwns+RNADOgVl+bxhR3OXsHulvcW9z +BHcW0qTW8yh4Zo2Do6MKqysKggjoRir849U/MZtX/M6688azZnUTNdNcxWTSekFVPhtk5cZPhhVV +FKb0yjU4pZMZjE8JPVnjkIyBItnP/Qyn/fuf9Pv/AF4zm/8AQx/tn+x/487D+Uv6P2/sQWuf85A/ +pXRNQ0z9A+j9etprb1vrfLh60ZTlx9Fa05VpXLcHs74eSM+O+Eg/T3f5zGev4okcPPz/AGPU/wDn +B7UUbTvNmmkgPFNaXCjuRIsqH7vTH350zrn1DirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV +dirsVdirsVdirBPzB/JP8uvPivJremKmpFaJqtofQul2oKuopJTsJFYYq+afOP8AzjN+afkK7fWP +JF7LrNjGeX+iVjvVUb0ktqlZh/qcq/yjK8uKGSPDIAjzZRkYmwl/lf8AP1opf0f5vsmgnjb05LyB +CCrA0PqwH4lI78f+BzmtZ7OA74T8D+g/r+bsMPaHSfzet6TrOlavZreaZdR3ds3SSJgwB8D3B9jv +nMZ9PkxS4ZgxLsoZIyFg2jMpZsJ87flR5Z8zxSTLCthqxBKX0Kgcm/4uQUEg9/te+bjQds5cBAke +KHcf0H8BxM+kjPlsWPfkJ+aPmL8t/PS+QfNEjHQbycWyo7FktbiZh6U8LH/dMpYcxsN+WxBr3OHN +HLATibiXSzgYmjzfWP8AyrzyB/1LOlf9INt/zRlrF3/KvPIH/Us6V/0g23/NGKu/5V55A/6lnSv+ +kG2/5oxVHaV5Z8uaRJJJpOlWenySgLK9rbxQMyg1AYxqtRiqZYq7FXYq7FXYq7FXYq7FXYq7FXYq +7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqwT8xvyU/L/AM/xFtbsBHqQXjFq1pSG6XsKuARIB2EisB2x +V856t/ziZ+bHl/VpT5M1qO4sZhtcpcPYT0B2SVFJBp4hj8hleTFCYqQEh5i2UZGPI0of9C+f85Nf +9XeT/uLS/wDNWUfkNP8A6nD/AEo/Uz8ef84/N3/Qvn/OTX/V3k/7i0v/ADVj+Q0/+pw/0o/Uvjz/ +AJx+aX3n/OK/576ldpcalLBdTgKguLi/MzqoNQAzVagqTTMjHijAVECI8tmEpEmybf/Z + + + + + + + image/svg+xml + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eJzdffle8sqy6H0B3gFUFGQwEyEBB2YHUEFwwJlJRJlkWGuv88d59lvVSUgICWmQ75x1716/7aed +Tnd1dXXN1fF6iuVQsjmot0J8mHG7vN70qFWbDEYxN2l1n3e70/FkhE2+G7+bZcMMdEqeS29qx7vW +aNwZ9GPkEXmYw7d951e565vTrN/t80NbpTPptqB1Mug1apPw+K+2X5sLXs7UJvAwciAfMKKbZWJ8 +1J28hOepwbTf7PTbqcF/YPyo6OYZzi3AU0GKwuOzzk1rbO4TjrK8jB3DnAy/CLwYluBNQYInDL6V +GTSmvVZ/UhwNGq3xOD3oDkbjmDv9T63vvqy14UnNXW11u4O/3alurfHtgtVG3nKdbgsW1qtN3FFc +ZfKcfyOv3o7hHXgdf8fm6Nt5D1rKrckEoIKBESXpy2reOB9Aqv7ne7pptTsEw4CIF78ycqXVG3YB +KWRRPCCFl0XtX7UHwEOehqJsmJdlGfAmhiMy9BMlPiwwjAC/RMgj5Q193a2/Oq2/Y+6rQb+lLC45 +mpQ7/9XCqRg3xzBK68202xrd9jsTWASHTbKy4stBs9VVm8i7uW6NLJT8x+o/lQ6V2qjdmsBODbrT +CaEUSZvhator1P5pjfQJroetfmVwR+ALiUJYFMWIWxQY5Rc2HHFLouyOMoA6ScEgC8tUp2TJtKwy +No6E42gTRHHvi7Az16NOu9OPsYLoDnHYint2Ouo09S2Lcm5J+UHWEZYM/5e1/ysAw9onk1Zf2eZs +v5ke9BDJY6Re2Ng+7Hp30FaezX4nT2C66VCBlfz9BvtRHHX6CIPrijyR3ordKTw6HQ2mw/P+x8Dl +U05lEScd9a/78MunOzWajj/dlcGgC6dtroP6SBkFH44mxt5L54C+9uPrA601drrW7Xbao9rws9Ow +Gt7i+Wweu3eXTgjbNGrpY5A/Z/8ufbPcIKi0gnL+0WxwizeWz/BPrz7odsY9fWBDi/67E0XARnVb +/eZ4Nozypw5YofOX1rh8sEzrA1idYWtJa7b/V6s7GBrQOGup9Zvu+9poaDcsQvfR6TcBK+VpZ9LS +N3rQGyIDd5c/a0NsXuipnBA4PcbzEQotPzgrvyArT5ARTv7ptsaug3x/8Hef/OGOuXxPgJLatDt5 +8bsPrmq9ljvoOih3gEm3tC6M+9rFqDzwG367cWn8MO/SuCLjfvgH/riAX76g6W+34L50P70w7ia0 +Pty4kIE9NF0HxRoA54673AcwLfxLAIQV6eA5rrFY6wI7axEginWXnbhBkMauhdZiY/bGt+XTYmoG +gjbTKvgtwHBGpC6skHRYZyNZRnmkHBsc5v+ozTCQqdFmcBVWTV6CclJzed8OtL9hr/GvTgOxURv9 +o/z9cFm4ArlI/vBtN9W+QC3lCQzedvv+0+v2oUMIf/SBgvxAQt436+d/1bpTtYPsPjiHOeceT/4Z +qk8PkqNRzQqCXmtSawLgvweAXQ+Av2qjTq3eRT1o/G8A4n8dhv9JLMT1Po3PTrc5avXVPiayNXQE +mTXq1KcTBDRIHgUX1xIb15Dn4ZH4H95Y6iXNQ4zvOIPp2+2P3xpg5wx6cZvOBpi5/9lt0NawuB3k +QewvuuUBHY7/rYvDNQRpyHFNKoC1A7leEYQ44areIeYk++9DlXEVi8TQHTS+W03n9fXB6vv3rU2D +/k9SwQq84N98WCiRNL/28cff/2sScNztNP6/EH9kIeXBdNRoEa/Tv3JN8yD/4wjizFN2cNOqdf81 +pP6PpcBzXM3MAfjvWs1/rFbzd6c5+XRcEScyYVbk2H/ZilTgF1f12eq0P53VbVYSwgLL/9uWpUG/ +uK76YALqYaH1MVEciM4rdB+kBoN/z9IWF/AvEbYgm/4fl7WbEzgbAt7ggMAWRsVd8pxl3TM/BnFA +uwu1fntaa7fcxcFwOjSRLnmhOGqNW6O/Wu5K6z8Td7bZmdTqnW5norJoMRLhI7MJZHdtNKkPaqOm +u4HBAjfrHmmKnWPP9qilrdexb31GGRFO4CT7rpwOgGNPAwCOfesLQnyx2zzp4vPJqNYfD2uwr41/ +YLpO0z3u/Fdrtk0a2mX3sDZsjeBhb9olfjdNWjMax8RO19PJcDpx39TGk9ao81+ko1sPtajgRebe +uWyNPx3eYOb2X6Mldwd61SYtWHmL2EhLO3/3QaUfAHBtdAOrx/3pstXsTHuGCV8MJ9+KPNX4CqCC +kOHEbbB/TEdCIxfAvIr4qIb55rATNkFb63bGpqZebfytolnUMDasNXWzJHnuTk4ngxn2tP1nDAeM +cX/MQB6RfqG/Wo0JkEy91q31G4t7PfcKYKzb6bfcEzhrdD3Hk9HgWzv7rE3nRrczBJJE581/4Dy0 +AW0Obwy1Uz/4qzUaooN0xl4ANY3BqNlqLm6D++BqMJl7vCrvcRhOp5YDne8djJqjcVhx4JgV74Vu +tX5/MJmtXdnlhU4aHsbjeQ662HHabzh0AXkHJ6ZJdQSML/9nGNYlpdXo0GEwbE4dOoydRmgM5tmY +qQOSzvIOgz6QyEShw6VzqT112iasyaonMOJ5lsQzNj1H5p7RiHXHueNnufNDZd+X7zp0AjY038/A +lc1dP2vN1qi1fLwuiyezNlnaCXA3Ia6bpX16eGzHRkZu1a/fagPj/2v5YPUOnsF5CWYGvPVXq2s/ +yEd/Eh5P6+MlC8Muze5w9DGY8RcrKlO69UDbUbUDS3S3e9/hXm30PR58fIQVdZe6+0jX+yl6TwZD +6r5d0LhnCLDpDPyh1TRDTdHdADVF7xnUFH3noF7ce+xLNJx6bbSMuLHfyBA9dOg6BGHQ6X8MnGYe +GVZi3YUsRO0T5iK2C262PlCKGsxZa2ZMOn8N6hNMZHLsqIiij0532RHDjmMMdjr0mZMfVr0ao2Z4 +Ahq5ppFZnSDsM240+ssOo9Jn2G38Y9BrFvGmdKt1W+G/KPt9LiE77DUYtbWxlvZRx7Fi8NhlOBh3 +lhMZ9oL9Hn4ORv+lcraoXb/BqIO5YA4DdkfhmYJUx3Sx5X01WTkcTJYcG+ypMztrOgNadFAPsEe9 +M+nVhmYRadebrKI2Vl6i6DpYTuGzfnXVW7qsY7M17rT7TugeDkdhYkItoxbs9AlMbNxaxhtJt7/p +uhndQksGc2Qi0Enfs2iUDwuWjAm6dTCJcE4cROSIU3eDOGClsLVsmnWeSQNWdOqqC4OozNl1NeJI +ZG27GZBkxaewS1NJC1nCFqGTs7Y/nnTVXsNh035G7KbOOOtnPyB0wZPZtfLxL/RF2m+N5lyCS6dX ++muGgiHlyGoGEL/dFjGVdJM4PnPZYAJRUuvsRpuKyryyO504WW3icNZHoA6Oxi0cbWS/YOw5/u4M +gVv2v504HCoEcNzbluu7GNQxvcywOt0TA52yxbL72mS8zvlP1D4FtKIxexGz2IiPa6kHRX3rdFRr +ooAgbyk+FTtDZPaO4jc4uFP8ASk7f4AKumrfV3RrybZP2c4HoHRLo/WfVq3/G6P1T+ORwRGWuGFY +o9eqP9D9Be5On7gcUCpbuWwWqc/3ZEg3d69B/1Z2Cq6hmMm9pYmN1TG6Lq3IU+uueT0NEKHrE8BI +14aKA7TTWmKyaOOcItbg6FQ+p716v9bpLpGD2juYtwz/5pZKV61zDojqvlXHd5yhIQncmcHffSWR +J9/pNw0kTvuamdI5zkols3mZpMcn64O/dFtu+atp3arV4V2+0/NvlaY1fc+5iOOEmFtf1r17yzZ3 +VPtndWzOv7UaMuffXQWX+ObKqDS9tAIm8U16RF4O+oPG52jQa1mh09r5s+xdM1KFpRuCI9gjVaCa +2xK1y4+i8gJIHudDXhl1epfoUXDuCvydsich9tRSA37GDQEl50sNc51vEiUGQajMwnN2Jrh5efct +BzeM9sI1UdtzgHhA39+D0XdhpqKu9l7KyU1k++bNuqBWlrphtNdS6MAoLPcdzfW9cTBR5jqvAIMR +Q8voWQG4019iAWtds716q3meThdHxILUpOjSU16e1hGNg/7kBo1EZ3hmqh+FCFW0m4ohNkelHi0Z +C54rmtKVIdNmKbLNL17W/rNED6UaodO31Ulp3lf01JTJb079OmqdqtKp6JyrD6Hqt2WH0ILD6xVj +LM1R4Us2RoN6baLUjc3MDuihrmqmdppNDtkc3hrW+pp7XJOx5btTJGGFmCcLHjv1cWHQqC3OAA/J +wVGsCJWm9GcAXqOju/4NM2b7jYEerxX0B6TUQufSM00eHpHyHKRdOBANi+daheLik2L7Y7HxoWZO +LcDpu53GDKz4ojmgF77M12Lgjik1Griz2jMX2UljC5oYyXL6/FyKZGDcJlbteAPHYmgnMfY/bGXy +F42PnL/EJRM/qVefcHL9fhy955lmvBXz9smf8fPx4CP3Xpju5TyBJ8bUFji5qx8wXHcSSd5UcpVE +bPgii49i79HlPQy95wZkMJgvPk6Wp7e+ZL/eHqvvHP/0kvn77PZodFzrn3bvvuqp98tSMhnssy/x +E/ZOymw3p9lM+uz5hQwVOD4aeoUxv1MKnHxOeAKIy0sBygqAHNWTweHVRSIvj4+ls8P7cG7wKNy5 +vNnR8yOTecxVK7mj5FHDCp7jof9wCBOchdLcztF7JjxN3Cajz29VsTpki7nd0kNXna+R3M18DP1s +snIxmeptLq/Smn/wT2Cci2kmfP15OBoJmQ7DiVvDxN1eeUfpzjLFWs4/2a1lgy9XBykxyG2p47wP +EqNRfFwBeIPnDBv6iunIiqdu0i2XdyzlJnfc6+B7Vyy19gMRT9p/LRyWYpXA0Y34OXphxodhviBz +geNTz64w5saXAM2dFD4YS6eC9BP/gj/9fqa5W83MT/o8erl8LpFJgcbmp4V3o6+R2Plr2HLS152r +gu2kYid/6rWa1OUdjQ49vtGY9Y6s1jqWiuyzsMXF9q0mHe8FL0M2k0Y+fbW9apZM6vIurFXwPwcO +uXbJctKt3KuwfTvsFqwmZXKpfMJqUpcXphW3d/oj/5E1goXqK5P7uCpbT3rqOdxlL94qlpOennEV +Mime/UUEc4/HlXcyKbufrGfnd/V+9Dw9LuCk8cU99VX5py7rh0lDQX1SmEUhpQKTUtda3NszTRqJ +9N6GdpO+jV4++xWbSRM1MZrbYV1e07QqKZ2839hNerbD++LP1pMeel7G25+tG9OkwGGUaUtp//HP +Tq9gNWkg3o0d20wa+dw/eUxcW08qVKtMTmaugMas1rqVa0d3bnrctdWkTO7lJWczqbjt/e5fpk2T +wizatDXmNPh+Zz3pKZPca/miVUv0TraDJ+qk1ZDPhN6TK+Ho2aWcVTb7/J2bW+vjIVOIhlic9HBh +0rPWQLyphTiYlAmZV1p4eqyZJiWzqGuNfjdzr3aTZpjL/RfZetLzn1jia3R1YzlpOb7Hw6m0Xqu4 +nW+VecZm0qcQU37zb1lPmj9rXT09+n36pC6vYdq7vX7bdtLyZ+m9bjfpKXOXHx5aTw== + + + WuC9Lu9tLnF4ZLnWu+HFlu2kd2+nWxO7Sa+Z+5N8Rp8U1mKY9vI4+/ZaenmxnPTl+vvcdtKvaqSZ +s5n0Gbgl8zLuhqzXevU17F3LEm856dt5qG876chbDnj0SVGKGc/qLZPt9C4sJ5WuQluexEsoC5Py +YfOhmb5F39RJ67zfdGhcXv9jobZPpuX2jn1n82stMO/7sSROemyaFIb9+tGYvnhgnnQ82D1SJ52c +BGBf5tfqea49+ZVJU1X2fJ4VBkfj22MPTppY5EnnIW2lh6xpUsBY/GxLVKY9YfMhEysMDoUrRdLw +O7F0fn7SndGoVu/jpCnzSkfJ1kCj3hTQmFmUR75iqqQ5iZXCJgRvDVrvFUWmtmpv4jxIZ7e7r4OY +1VMikSNn1RLbu7N7+5M5e/dObZ8C683s2jyFHdgNpL0qt2RaX62o6bkosW8a3ONvyfy0/7n1YPs0 +WjyPPetPF3Zf4vZv3m3flj5rr3u2T5Pc7mPD6qmqwxQC/RPO9u1C/fojbvv0eqtRP7N5Kp3tnh3e +jjWMfez9yKa3bwMdTT39YLdi5qf1i3Lf9uldJvA90p8uYOzeWz/w2L59/5yJHdk+ffe+RnesnqoY ++5oUh2e2b3/fcamS7dPed+741e4poKoUj8wwtvj8ghOOH2yfNvr1csHu6a5n9/x53x5ju9nkZb1l ++/YFd7LF2j1Nergdf8wWY5EzJnu0r6065oubznSgUhqfqE/T4UPT08r76X7S+FQI3iBDKSnGXDq0 +nwbdcjJ8fUm3Pyvo1EseHctnO0hZ9z7VWj5pxGzMvvFD4u7jtpysVLz3hEUlK5dNIVsbPXkDqcH4 +Sm8Du7I2etwjfC7GSp4rwsw8+/k46wlmbu49wbvXsif41qx4fE/+Kf5WBBL8TntC+bfIolFYbSdL +fFkCqNMBsE4H3+JOVP5AS3yf82h25YuUe5s81xLxIbuVuQhsR7Sl7faSg8wrkOm2vMXtHRWPM639 +rJecOzRnnjQsWvdzKT3R2pKX9yT9jmPpp6pjPzDD6js333o/l9e257730DNwHFHcpl0L2GLRG/8L +xYg7fT7+RtHPe925rFGsRdxGod6gGHHvvB5ua/22e7n0x4V0cHnRisKf+9vJ6GOXV2xkPwjHj0OF +Tpgx101Wkv0ccxER9hWyQfcHWMsRThe84lZVuMw+Nn4+DjpHdb/4KBbOVLs5ujuaCeB0cvBz60cO +s7glft/JU3c5eGhLv9AAt5WrhY1eBVvwmFz+sGgCz3I3hKvMuxVwhFvq4FXfqMA73RFpgDstbT8a +dH478KSzOWKxxV31ZjlwQGPK1l7l72jAy2ZvczPcZZLl4PcODFCqHnS2Y8G5CQKHZhqLGUBh9yKv +mY9KhkeQBVzaob5SNnjLhvRJR1M+zVBMCjr//LREO15z0kBsMMnipEOCFoabJj7Tn8Kbui+gah4P +M9lGsSJqbsX2NNuoth6UNo2P5zPnzSPQlHLTbjReui6ib5GbPb3B38AI/5bPAergdy59EiuTbTdY +FuPA8XF2D6At7yOMYbLq46GvOVZdNfMORmWlbW83ebt9hFoBs5Usdz2jXFa6OVAHvWr8BI6LuwOY +BYWZOPGxp+qLO82MojYDZKmDz1bGq/wAOriHwYqiam3BfLMtIcvIoJMhN7+MjMGrQJbhNfzAmWPv +P8WYQbTOgfezEnDkVC4Fr86fWYFnAdy+LXC4FhW8MQ14hEIJVaojXkh2y53q42m7b7tg+HGjLFfx +3VgsF4yrwvLlulbZjb2tNUlF5ckLu3Fa7CERt/EgbStcR7wgauyddCyf3hbBctr1kh/c3glzjoCc +z4YqaZyvKELnpwzsCxhId5T7S0F8A3Y/9ZVjWDnyleATj6jB7fpmvosK04Rd9Xq1H8K+eiCJy2Au +AhF7H43rsE3xEC0CXXSn7fT55zcI1LVxFYWoJz/++oDoCORSj/IF+i3nULgSAi042o0VR5udympw +aMYyM3xNr8fRsgjNqY4RVSJb4+Q0v4sz31jufvb5emLaq8jwQC6a9oqwd5fXlsHPjXjnoRhR/VF7 +yCCCzmx3/zXL78Tzhbm92t6z3KtWMbyr7osFxk5ipcvNYCwToNzJXZfKD615w2sWHQX3Jvm6Okgu +LwIVpgXKASSwWatWIFnISic8MU4gDQJHugpBWIFyXi6WgJcOPy3F2K6uihhPL3FeamC6vBbnt7xE +I6lzCyLf+fSSfbE8vzkrxcpi43Xd6omMqAbW5sZzeZURT3zZPBUpGYTMpWzNI2G5CmOenTqiw5jO +nU+yVv3mUG2giNrWJbcci3he5mhCXzq8PTmdLX2ojy1VdvcuTyvPX02GTT23M+Gb26Ae7iczw1C3 +I50nqbLSSiYtV2PnRnwYL5dxLu8cITrrWd/SZHW9zeVdOuJ0M5rgTIp9yx6qEY/q+/o5sKJa7HyK +3v0LM082SXYa82JuXz63N70v8s6m90Wmsm5W2RdppMhSJ5UGjVCCVFXtOrXhtM1TXWt1eZeqXTRM +St3u07uB7eYAT17nGN4tCJmlqHR5nY/hiK3t7J39BpUmHQaQSafBroLK+hilmKOWvbJhfmsSgzN7 +n2BnckxlXNKpsWe6GutAY7pqb6lscKmHT7PSaYUMl8HosN79yQmVNbn0aJowdkLFPuiM5zPdeP4t +xqpbu5vB2PGYjvXMrKlFDV3RYYAcTsv9lSxHW5BWtpGtzQYEqTpcCSQlwmsNFBVfoQDpbUR19uct +bDulun1moVQv8Y/NLOxyfD70dKMNe+hLRl89Ye5lXE+lP6Nnw0w+/5PSgjVk0q9zprlXyxJkuLz0 +RjjJFrIg55dx34EBuLwWODmzwcnX+Yp7pQHnMqd5auBNHNSSleSey8u9TLzUJGBlZpuWu2hk0/iU +bHdjEtijBc5FsxuYg3C7qgfIaN3M8eQTX2ZixSDWJ75PbhZ7XUUu2nD58+UuNKOmREvOq7vQiAZr +YyAiAokT7TcIJAxu5k9WtY97eyP8hL1YMGcoXWiWnt4LkxNtTe8LvxPz7ZC9Aj7m7ESjON0wYtgs +m/XxFnbf8XT3LlZ3odlgbN6JtjbG5B9m8bys46/qXVC40Fy0QPG/caGpUR4FKDsn2sp4iphAAilG +QfNzvpI5igezgcI561qmOqgpJ9eGIOJJrDixCyLmkc6zlB5FZ/89UOD2SttkoOR52hnmTT4um2NB +ZTKTUwkjvkxTeZqDhj+WSxX+5DbmM+0V6JbWrnT/LuECdhjzbwpjFnJ4HcI+ufXyixHedRgAgPSx +9/NgC9JcJNERKPNpowXJZO8jUAvi1tYba61Pz+2fxypiZUtZ1j5vC1MfyWc7btLQdT72ULY9uusE +3k6LPTb7Mj416fxrBHPSALdg1o+s+RitzEXCPqDWLubtdEtZiTAe0YTWHE4voo0/uatc0u2+E9r8 +PmcPPM25I7Sx4M2jXd+8hwRWuPwQ0x5h3ES/brj9msb8C4FxC4pw0UpfMiJtlM48noHGLGIfrz9L +Ylen5T6toHN5KUQd7n7lN+GmmY08B+MqLNPrwDJxPDgvjrFCpxEtnBqob/p1Xcflndd20sARYpTH +giJ95OGWGCmEJ//2bIy/HRjcjJJpIyPAAFZXeHAtNioPwugQIaTkTrd4XjZhqyBIgUX/prpIrLBb +gaVgrk1w9fXNPIomwlj0TK4lX+4GxFzZEI0FFnmN0S9AMiHnA8eOfBPR5hjlmQsbu+hNF8SibeAY +xZL9hilnf6WIRxoenI9W2jU7fzLAvWn75eFuo1kEAKHJ8WCVRUB3crLPz2YHqi3aXN5l5A7bvREj +BuULUMeiH3HN9Vkn8Gj1lSvwvjsr7+HaNLZW7p21WCYV3DiiTbThN7EGZGEBIpZdXqtMZmAuBUqc +0IhlktO7Ce8hws3ScRh6sfz8s5JYdjl4IhBG4ddiWeEw9xsSywBSyEos28Qslh++2tblatlzth4S +WN+mxPLzD3KYTfgRCbJsxbKuw6wqlq3yuezEMkpkLcePTiy/jZaI5ZiPnlvSiGUA7snJWG9Nbg8s +rSS7wOrDg0Vm/9JsRf1sl+O2PPlttHG5/7BZuf82WkHu2/mr5rdb3KgljnlDNLa9YTw7Xx9usrMj +ZEY7NA4/Jb7vfPZpMl2tvH5c6qGaJF4/l3cxMv9Q3azXD/OUdL+fDcGumEqKKoSZ9VhERqhSSY1k +kXr43lq+k2pkhNqtCIDSOpUNbkXAmE1oGGGcmAP/zoqMJYtSzn6VXiLTkG59bFKvl2baL0tRQtwd +OKrXKuXMPL3OZz/18OboZFhMTCdYnJf7qtjSdYpUrPMafEiWgw+D5E2/nk+FpMH1Ap5md2iZb8vi +xm1PqF96c3mxejrrCSX5V0/oQs6qhdMXctUT/Pyq4I8wtuWw1DpN6q3xBonDZPm795Ft3J80cC1Z +rMisZmrl40LOPzpD9+rOXi7zHdrCKmpB1ZSUAuviWaWYDYz5XV3Sikdf9fNsPZRYLLa9OrnR92pJ +v+IBM1f3at+zFCtS9BtN38Mhq34u72LPGEsz81bu1XLExX7NA7q1bOW+KGce8lRrwQuyIlQjnoZk +Qz+7StnROBYYG+4hsS/kFR4+C7P7CewKeQ+3fLaFvBizMGwJG6YAL3AY7904Aidub/+I02c98m5f +Z/xEV6PdcyggV28GY3KDSoUCvN1C67PpXATdMgEH1qtlfftV6YECd0z26umUYmNd3rnCf3vwSmsX +Ru8fHRhojLoeG6ux4ytN6vIa1o/3Da0zqeAvKpOqroXCQyKW89ZUGJRYktgflhOHnckDjn2bDew3 +w8lr9uQe2qZbJhVrSTgmNx052vu6OWCvwVsZTcXdnl0aRkbX6hwyh/cpyygX6hnnbT9DJNEYMLWv +Gl1wp9AAZ2clFXcXsOxcqGILHHKYhYKt3yTwZywMroy5sn4Fk6u4R12XQ1fDu6gTrkYq8xHe4u6A +LkccmB5dyXJghXxLGuKjrUyZVwjtq74tAtoOCCT5lsuqvqkSkp0QiLOoKFyIBv7igJgSy5ZU2C1B +oBVHqwb7dhwtq9wISmGp0nG0atCSMa/ugX/Nru6gUSq57OLtMOJGEsu03c8+Vx3sXCofZTVkmYq3 +DsZWj8lYZ9pjIIXWs+NYCQ1HeANRUQSJMvJOA5RTloS9V8geY/YRiOX2sNXpVXZfO791bmB3fnO/ +Ob0WOSR1jio9nDqMUue3qHMUKUq3csSvZ3Xq1PtgLTyFzj7mWo62It5lsXTHCh7QtxM2FTzNU72C +Z3nNCH3NqWNRhjqUGk2gKpSkUist9TbLvL5Tytt6qEs5qXXL+XNgX1tsWcA4x5NpzIu5fVmUPr/e +F+dLgFbdFzqVdaV9obsJaJVqZXPYCj0kv7mcCauVnSuZXKsdQ0rl1YzK2W3zVoeG7k4gClSqOgwi +0zH6uCoq9Zqm5TWJKxvmk+MfKzGo172aBeH6hvnk2CwG7Whsptpb00H7bCWTY3anyg== + + + wu6fUTK4oKJdvHOv77ZHU8OYhfm8tvE8OTGL+bUxRjK1N4IxOtaj62N2xDDGU7kop5dYjvYgrWwj +2wK1GBCkAsngUZwBRcVXKEDSbbblZ3/ewrYJUGKQaSFlxto/ZrKwna65GX7YX3PzMm6bkxhcXmcj +fEkRNOU2uSjLZH/MVudKwJki71/ny1NNV1U/YTxUc2jvhXOq3+JeJj77GxbsfUpLarRXYS5OudZY +tLyOB8jCulF48vlyBrH6ck1aiOXtQBZycUnJMoU8cy0e8SUIdDjitgi01GDPraozV0TgnO7h8qrV +mezF2M4Iv/i1C22+sp7NL5gf69X1Utw/ttLphhGdXWgu+urMnRhrvuJgreonwJhDSRZ9JTR93oVT +2TF1FcbyGw6xEnoThV0uLwL1+xxUFSQbF9oKt50Y9+9iungVwpJct1m2rVOe1oN9EBFLQilreaj8 +9/k1k5mNGRFaES1Lk51EfRXCyW10b+Gum5XuEzVV5tpehbBaLU9+zTxmK4x5Hd2vlJXQs6sQDBHe +Ncuzac6ai648+5cpzKq9j0DRJIFSpoDmrSNWK2bSWSYuF3u2GjreBV17o6rGpzu4WFMaNitda90S +gHCvFLYy1FjZlomWf51XvFApvJHbgBFtiSnt7juhjTZ25aIqX/5tPZGijZNiY+p7ih0rvReM7LVp +bKWbEVzOhb44In2Zr6U0V2ZZLPT9avlsv4zANHePaOr4dEE3k/u2NxB8tRwvTHYMNyla3wxGjr7e +XB/Rtn4dxjugvrfHdkSr+vWRla4zu90US1QXfFhWx4IqfQTFpCFErHGY9eqMaS9ypo6MYLVrYbqm +wmNTKbwYa1mzuJffzK1NpHh5wb9pnWlPU77sfAWU7fpUzq+XLzuWKlBXeoc3R2M2gWNT2NgQ5XHO +08eqb6c6TMtUApRijiUmoK8cH9sEjkmBqpPPwbVCxOO0Olxz18z+5PRK30Ogqv/Y++E2mUWQts3d +mcsioDo51eGiR3Gte+HSK30UwbF4+W5Aa/E5rW9Td0Mpld4L3sO1acz5ywj0lgwZj13re0mOYnkS +WHYXNGiFmxTLYCjSaxfLqeieQizPqjgpxXJt640yZ84olpdVCm9GLAPaIpu5S40ULy8Ry6abJ53K +l9cUywseElK+vCGxXNu63NDNkwRZTmIZ7f3VKj8BbSuLZWqPYnFZPhcpZF0ill1GK4pKLL+NVq/7 +tF7LfC3watmKVnWfCzwZetZpvlpEL/djPmGjch8g/N6hlfsUt35jzfcKdZ8UlvjbiD6J16buU/X1 +kfp+2spP6rrPvaM71vnsr5DpOuf1q49NXr85ywJLWTfo9eNSD82p453DK6aSYuG3zXUFhsiI/YUF +NmRRH9O6Falvn6tu+va5+pjarUhzXUE6HF3h7NOVLK9834X9Jq96XQHF2Qfq/+11BarcV8SWzhuO +xRKfvBkkaqluNN87DDCZV4tjMftQ9eInqX03O1GsmC5jxXTB5fWEpNCb5Yeq9ytJcc0PVZs+U+3y +/pEPVZs+U004/+Y/VG3q5/L+kQ9Vmz5TvXwta3+o2nYtG/1Qtekz1XgTxR/4ULXVl743/6FqE3Au +7x/5UHVg/jPVsJY/8aFqE3DEB7v5D1WbPlOt1b5t+EPVps9UY5baH/hQtWlS/Dr2H/hQNcJg+Ez1 +zKuw2Q9Vz0sfowd+kx+qXjVz2KGM0uZD1RaRRINm88sPVdsBZ7aSaD9UTVtAPqar4V3vQ9Wm8Rbv +6nQ0uWg+VL1SDS/dh6qdSUVZC/WHqqlLluc/U22fb0lLfJYfqnbOt/zth6otEGj7pe8Vk8eXIxDv +7KK6LJP2gAysvrFjVWFHjcAF14nTLQEOZu+y8uwNR3lsyrMp/HYLX5Te4P15enE27dcZnDJLlxdn +r+KDtSnPXgdjm7ky11Sc/ZuoqFqevYGsG4rybAqQyL78rjybojjbDmNrOq1sirO1L0pv8nKFxeJs +my8ZrVKeTZ2j+KvybAqPmm2Uh748m6I4Wz+Va5dnU3A527XQl2dTf7X8V+XZFptoLs6212FWGNGx +dtFKiq1Ynm2gWrvi7CX7QluevcK+/KI82xKL87GGjeyLo/a74l2dVuXZv/8CO015NoWyoX+Vae3y +bIribGM04beV7vbF2evdaj5Xnk2BSrv85BXKsymKs1eOii6WZ1OUGlvl9a1Ynk1RnO1wLxxNeTZF +cfZ81s1a5dlrY2zFCkrH4myn80JRdkwhpV2UQK1YfWkCaZZtu3559iJIFnnjvy7Ppqys/2V5tnNx +tjHXGtfaMicOUq/U6uad2bezre7oNn49m75Wfdm3s6m+J05dNm7HmEw1VrTgrfjtbKdM+818O1uX +yNZfz157N+a+nU19k96vvp1tpcEuq6OmUmkWlmv7bfRPuotcHAvRSRU1sffX8Out9u1sy7s6qRFI +p8jQ36vwuVAZR319CahDBj9//s9VjNvc1LrhinEKL9wGKsYXa0X/RMW4PcY2WTG+gRsOKSrGaW84 +/F3FuDHn6s9VjNN80eD3FeMuh4SjzVSML2YQ/YmK8VmFnWWx8aYqxvVK4SjlQVunYnzdb9itVjG+ +HGObqhjH/OT1a6doK8bNsdc/UzFuWVm/8Ypx+rsIflMxPl+V9qcqxpfljm6uYnyluwfXrhi3/VrW +RivGN1OX5FQxvkJd0i8qxhdy4P9IxfgGaIyiYtxFL31/UTE+R2N/rGJ8lW/Wr18xbvPN+g1XjJMb +QTna6Oq6FeMu7+I3zzdfMb6hGiuHinEDJdOXpq1cMa74+uxUnk1VjCvaBb8ptNmUpq1e97pOxbi1 +72LTFeObo7HFWPbi3YMrlqatWDHuWsl0Wbdi3Hxn15+pGLe/qXWTFeOz6qc96vvX1qgY/81dN/QV +4y6KD17/XtnQvlpOUdLyi4pxu+/ybLZifDmNLVaMr1rfPXc7kOVHHzZXMY7f4LbKl95sxbjyjdTf +524trxg3c5g/UzHucvZEbKBifMZh2C3ar5KuUTG+5t1QK1aML/GQbLBiHLTxWc34H6kYJ2LZ/gsg +m6wY178AssK3gleuGKeIjGygYtzCSvoDFeNk923LiTdVMW6oeqZ1WK9RMb7eDYerVoyvZImvXTFu +cUfEH6gYJxVDd5v8NtKc10+tGHd5rT9xv9mKcVjLrGb8z1WMW0ZGNl4xrkdGqN2Ka1SMk7w+20/c +b6pifHb20+Hon6sYd8i031DF+Er3j61dMW5z/9jyivFFPC2pGMfacPwG95+qDtdrw+Hs/7HqcL0f +YuxPVYfr/VzeP1cdvnwtm6oO1/u5vH+uOlwvtl380vfmqsP12nC9amDz1eE6cHNfYd5wdThdZf1v +q8NNlfV/qDrcsbJ+I9Xhepk26Px/rDpcrw3X5MufqA5XRUK3PQWM/bHqcF0xVK2kP1IdbpcDv9nq +cJMO84eqw80+pT9THb65L+Utqw5fo15sjerwpV8v3Vh1uF4bvkZOL3V1uL5cy69mbKg63CKj+w9U +h1tmdG+8OlwPYzvUWfyqOtzKStpYdbiGO6s6iz9QHa4jw+VIT+tXhy/U8P6R6nC6/LHfVocbswj+ +XHX4YtXzn6gOX5ajuLnqcKcI72aqw/XacIds219Vh9thbLPV4XptOE3m8LrV4TbZgxuuDtdlvVrN +8Ueqw3VEm+5V2Gh1uJ6M5PL+uepw27VstDpcrw0309gmq8PtdZhNVodbSbHNV4cv2ZcNVofrteGb +3Re7T3f/Yl9W+HT3Eovv19Xh+mZb6Pwbqw7XNxtj4n+qOtypinMz1eFzttgfqw6nuleB+SgIz0tR +afAbqAs3xpENCRIur5Yi0WvZf8A39fC6+gdAz23PfvtsU4W8lLdq6NLeUsOD9X1TfQH4nXtFz1Wn ++MA6kFLq4cd8K/ZKpZwLGFuxlNMOY7T3XThjbPHm3xXu7Jpbmq0JvxJIJJL4RpWO5Py9dFtmZZ/X +Z4unFYtI56xXE1Br3OJmF+giX2Cnrfek0PlxpQu5O7jSOZ3fwlK31/RPfJmRnaZ/brLTf/0V5uxC +GZoN56er9l3L6Wh7EziAt2AU/8bpCON5V/gyi6PT8dzW6bg8D9Z+N/ZWOjQOVtL5eldSLqust0gJ ++90nwGcC0eXdxM0Jnwvp7fMItPkO7xIELknctkWgrQ6DxetOyWHUCMRZsHjdyZ5QcWchIS0yRsft +JbVv/I48pKlsoPPB9i6sdn+NrMCLjX/172KzXzQALrFwsNcrfdbzvX+LMccP/tH5LbF6ekPfSL0g +Gd+/zxsnIFmkelvd1EqBJ0c/03zKnlLLY5eihcXrv/w86sw7Olfsxp9UJhkb79Iwv2aWt7UPlj+5 +DZhvgFlnm2IlLSrqlFNHK95jJftjtsaN0/nVE7xtMXbk3wjGPFq92C8TqfMUSWR0X/xEoH6T222I +8eWtfBtr4skUNVoe5XFS8rF0nSYd0LV6gcZwScIClsZaHNy1b5zGuBB1kY/L61RTTCdzl0vcue8j +n73HzO6W9S+KwNJ17fQaIrxrfWm39kZzwYOLshSbSvDakrj+FWasW9/EbRFK1fr8EV73Vg1StR7c +HI2tKn2tZK9uv5AR69Q0tkLyeLmPPNnGj4iOHqoybmdBp+9+uW97HdLqYWyEcF9nmY66pWFEuzIP +MuLSa3VcXooRLWtT95ORI7simFtLD8n6sVAQkxTizUVzNgDuHA3x2TruF+ssgAHQ3j1DVZu6nywu +3j24VvnY3WB5berc7juWYu+vaT6Z/MmEOjZTm4rl15qz9LfyBavWnWtTaWksvpAgYXYiLctAsPlm +vQXfNDtCKArlZoFxwpO1ezoAj/u2yaJs9jlNYxRa+Rws13K30lVZSyuKKb9dSx01A/N3o3fcIYyr +3fdg/33ku5Xuy1peiK1V1/76kga7FCVLi8/5S+freB+svl+JdetrXZplhSyrexStXKSUH8PEEe3C +H1Y0tsKVEbWt0xM7sXyvi2UHbkkplp9/lovlFeRLbeuBxqtpJZZt7iGB3ac9dzRimdS91jfgc0G0 +2Yrlxd13/Hg3taavHz1Lnny/MbGM5dcgljdwi6ZStb5ULK9EY5eOFzzYiOVl36zfnFhe8Chi2DVo +K5YBxlsa08wklu3X8uAslqkrV3Ur6W3kqFgtfPPcuX6d27TcfxttWO7HfLQXSnjnIolLyuv7v7xq +cd4Sf1jBEaKOaIs7kxuEourZWUxi/brz2aepYLfw+r15lnn96oaPECy3xOm8fh/psIONiH4YmysX +lpU0fzpduDCfgeuiuNOg+mu34sI9ilY5Sb9wK1Yt8y3Xvn2YlK7TS2S6z50fUKvXWi2PjdiyTC1a +48KF+bNfpfHzr2JN6kIL8y0LQthebL2M2w7Xg6nLWPZdiHnr9TZ2YDsfRcTcdr7ZjTqqxxHxWKhk +6weNXrLfCA2Ske700iQDyFSpbktkEoff18+5/d1rjbk0kruZj6GfTebvHwuzfQYrSQ== + + + r7xXT5G5+/uV3l3vrNRqqTgpXKe6kodNX92XWrnD7HMY1nfvy/lLXDLxk3r1YWWKYg7MWk8aORyx +mIhXSlOsjz6TQp7dafY+de+ZP1zFTCNWa2Yb79niaMSNk5799qh0EPLGdvn7y0gyIgbGn+cHg2nN +5d07a0ny/snTzuOW53zi9yRzNwdb1VcxtlO+3854v/vlwl7rcxgSy4Wft+h3s9BOfL9ffeZbZVm+ +ej77uSuzw/xH+bPU6d6eHlxO795O/b63t3TA/1WNfF33ioH4h8s7DDwmJiNv2bc3GvE7nq1Ba3Dg +ZXY+4/7HQuMuIAcufcc/O71CjHu/OUoyh4dbo9FJrOjZfzm99HCp624g3hASTI45OWZylfscc7o9 +uGJOr68+R6POSWg0/TwGbjneC17Vw3xBjiXL2+09UkIO63vOhqKF3S8pepLOp7rRUoAUb8NKKxWA +odUMWFy4YJHnoOyky2t55YK/05U9ga5Qwul5nXRjXG2vlDgMnQlKQfte6ufGGsddVih/3u78jBJ9 +8crl9dxf5QMOKJq+h3f2d70PxR0p3k15i+XTi7338vmRIO9eirCqlCdZOaifYIF8JXB0ELpH4KKJ ++MtrN3Ph852Qr2NXS0z2Lvo2Grcvthmu9LydjL4kWpnz3slDIjZ8OTR58oFHXv5kg9I4LBcvSp3Z +6TXQOciFYlo/FC6vdixeLnXxznDdyWH6U7yuwl6NX5OwF/dz4zx5rcY2433/JTf1BD+/HvC+i7vX +rCd4ef/g8YVqW3jLxQX+iHtCSb7oCR3svuI2HXtC/dKzJ9AIy4iOSPKm8ryXrTPxKBk7es8zTXLz +Bfkt3notfgL3vffBqrYP8Tbgs4+XTGs0CiZ7g8IYVnX/g23NXDrl2Up8bSe76U7tgGWY8ftupn79 +dpWUgsNBIHv3cAadH7ZgvsA7ov+ceztrBhn2ddub6l2/SLl0clpLDMdsXfvi57HYCGYbje39RPzi +G459Pj8hRhPDTSfbidF2c5QKSd0rht3zt5Plxt40WdqX7pKDQbiAiuqOWM3384nYdfor+x5qTxKH +J4I3kyrcNXAtUZc39/52PUxWMo8ckHvwKPPBVPFaD5kla8U/3xOHomeEx8I/a+sd+hOFcqrb3wvh +YLe4k5HU90/Mmyold8SUMAk85XyDdzbzEXq6Jpz/XQCiak5wnGrm4+U+muomujyOSOJBnvTnU2Ma +uwzwT0gHeQI8kFfoIpe9vJNynuBNLvfmf8qLk+FrEfbqJAiAeHynk7dhHVb1ICBPvpgGEvGCB+/V +3d/O5PNnUXh7Ozw3jm9P25zMCBc+zqVboaeUEOn7CQloe/rgTX+Gzptn22FvVN9nPPvv51sAfI8N +ZILeac53dn4eu0pNnzLn54ldvS3e5qrfqbPb/o8yHzethmCRzS29i/gofj0lbz7ao8zHwc5btrH9 +fUKmgrUAAP5c9uc5ltv3BwPiduyhnG0ED0Nzi7ziYNJUn7iOdBhT35c3B0AHzyO8iyBNeFvmI3Uv +5TKRRw4IqVrIZTk/6DAGdHDbb98JPnXwpfzJtqMDgDa9bVhQ/vF0YAD+KPdzRnYNtuSplBhNc3tA +DNU7hvdsSZnw9WeGbCLoYx+9+E6y5Km2YRnRi8zF+3gX5N3RC8MFW92UmLj/IfKVYQfVLrCmh0ny +mg2V1c1pDktkf9UuqXorcfdxew8Mda+QKlVPGYIMQmMfsIM3vXLu+azQSl6nU9VM/txzt/CgzVyQ +033kCwXfxWrhNpF7be1UM+fdwFXmYzA6VhgXHy/vptqPpbv4x7SYRsJ9hrOPh1PRrggLm3U68k25 +99PDoa+We/fHMhhuu1ZhfNr/zgY6jxNxO339tgCS2JzAvpzswMjnfTwvd2Qtzy9HL/nsQaYd8tWT +khgN5zLVShDQFn1iuEk8KRe97Yf4RapxnzsZnxZz73fVJChTFQGPVCpZDu1cpi+P+mmVWzQK41Sp +dH2bavSnr+q+SP32M5+tlx5QF8yHxRPx4TSdvREmKgnkq9108z54n/kojd+QCd/msuxoJxOs/eAK +HkOZg9rhbqzd37sFuCK+TGt8Iqi48wgyUYJAt+S5wiT3FryLoRpbMpoNVr4prnAPxBmXcP+KtDOD +fLGYe+WZc+nPaF710CJHAz6dq6AElMXbI7wVMHm7G38nnfAukXt85yLV9VYq6XZ77AG4LyUET8o2 +Iu/+6F23cKqfl+hd6akCP3q13OHp0ylhZqQtdtWOd2KdV+kDHrCwFvVAAgCZVmfwdOo9ff7K1ivZ +iD4YcLTUrfhQG96R7UbiLGdaD35+9ud76ps78mhC5GwPr35pprrhmwBYQS9+QmPZXaB5IZvztU5e +koPP633VAtMfNL+esrVaRD6Wz7mBgtnLs7vvxOi0B/qD/7WUCd80OPJ0XudPI59+gbW0PytvoFsn +jyx02ZjBLQ5MagtY6+1OIj7oVVPXk9o43op5+4oFdj4efOTeC9M9PUtAb5vd1nj80yNX1YDiGN2L +7Q44ARTjn7vEwWQ/E5AjwiH8dniROBgPT+DB5UVmWitew4Ojk5y8c9NMv1xsnxEFK3oX2rrMvV/u +RlWn4zAJVtL8Mb0GkLlptvF4RITjDUixwxGezw7uwTnoAjxPfBd44ivkyEXjjYeQqV82sC9+iEdf +9fNsPZQ4V7I7duIfZweV5DUzvERCO1dE+bTH12HPfyrkphhyKxdwgYc0UWjhnAs+YKg/DaJsHIbe +cwP99i5JHhx8JMuT4TSZf/RLsBYkP0PrTf/7OVvf3X87O3rm7xV1+PrH30hdT/d8yMzOMh+vW99n +2zenYeDyz/d4MdBPznfx5QO0DBh1qGmJUCAcXCZ44PKysOXHMJjMAQNsbwMf37uA/f3ZBUWm6kve +dIf72cb3jwwaUDKEU3WIiILDEHwE9p/YIlRCrhc68t8HOgQtZLnECsTPI4XVbFv29DWMBS/ZrVw2 +44+aepqdFWyl4wOQmE9QjbY+U6FYZkdhlLMfYBS2PDDAzVaydNX6grMvtKeNeRLA+wKkQeZi/2kH +hmgNDVpfIwVSG9EvC29PN4fBx8xzLhMUAwb5eXJzeJ/z7b50svVyfpoNFMJKpXDKm8mn3vncc3On +TXYf21hgAPEJKMHXHrmbazzHW7f+TqrxszVSucXO1jjnL3ZigePo3Vn6/Tu1n2m9Tx9zb5PPbiKW +89ZmD7ZcXmApL8cKD3kU28+gj+UN2gxYP2I0l+kGFUGIBzslTvKTo9xb98CTiGXjxaQcz4e03Q0O +QPdoT3PJXiyo3X7Uvp73Wq9iNhivwQvtJMb+h61M/uLxPc3e9sY5z345pD/AL0tefLRhzyPRlBhk +H3DEi9Q3W2tkcs+X7Wzg8/URT+A78TilO1/iAehM99PoiD/rpAqPgwc4i7FnEOSfO8Ajr4RE7Opb +Tl0Xhx1QaWK78tfI8+TyqqSksLAPJtXtDUPK0dUV7MdGixmNjg6wyzZoLh95llC8PphisOxGBvr1 +WFWQcjdbOX+1OAEau06HjomCiRufRMM9hBYKo8O9ON8ooHuzjnzdeAfw2fYkfpLbicRhsNkkl34R +X8H2jzi9dHkNyuTifEXDDVx2OFHuvkrvnGRtVwq7r6z1kbuwWqvFzNuOKx3sm1ZK/DBWawXb3pva +ySxdaYl2pWBX8udPPaPF0GyXTLhlcw+n7MztlIpx9YMO2mw1NCC+iDwj/EB7UM+bpB2efXgeb3u8 +X9lG9Z1LdS8OemAWHkfQ45jCjwsUcu+xgZDzyaI/GwoMhrqNddAJxvAsPsqJ+5J3y/Ld1w+s5AJG +ko/GOunMJ+G1KPJ9yDyD8Y/g6DHdvLu/VC3e42wQDTcZTIlTwGKd2cl8fJT4mR3rz4TD58E4c/Lh +gS4Hr7psdnnl67v2de71eaAIodgV030Tpzf8Pdj7500Ypxgh3hC8bxF08OHTa/K6cPaIqn0p984l +IwqNHcZ7Nwx3dj3NnE6irVQkE7rTnwLG4o3rS5BExQFIIrkGnLgxVuAhasD0nTXc9mcECeSnPUhP +I5CzM5BA5zcDlRmXbYEKxLu3V6uDhLtf3O3ZAKXcI3lli6f52wqvwIZ6207ExqEBbvxZNnA1qIBh +cwUWX7LYK7dz6cZNGv0+j3jkuHTnKBWwnPSmx71qzuLMOHMR2N9OCfdcCk1PJlke98tAVAfTufUl +eCLF/FOQKhcPZNq5Sf3PhnNnNAA7JQWpuWSmYYlyBanntQcyKdiVxmljrPVOT45/aHd6YVKuXTJE +RgxoxS8bF3S0Lq7PFqkwaeJ+v0ZAgmMWfCVP0T+mPce7Sp8VoEr78Wcr8rICCXSh70s7kF4JSGhZ +2AGFmDABpT/FOzhfaEGC86IDdVKZXC0Hak2QXN6VgNJBwrinn5YdWZz9ZQxpTXZEfH3V4UrUbzpy +6oGc5J4tJ8XjiDT2ctFWVNq5jajaHMiX2rJzDgfO4pRjnpI6LF7IKh/j7jcsJn3Qd9+O5LoWrOcE +bwS6ACm2ucNne/RA66Mlqo90OLQmm4Hd/xMCBUjqc6qTlI0UW0JUR5z1sMVne0rV1rJkWDs+XXz7 +oT0AQGP00LbGtsMyuZR4bQuryzveyrUZm2EHnvWPK8A6O654e7bNgV31uL63bY8rruX0LLVMLFf7 +NsM23myHBQR9GbgAcpiFYV/smMuLDdWW+9YnrCAc6XFkI0fgT24LBQt0vNpsXXVgqSkFC4G4znDI +rebKBAkuVWXPLSZozaLHX9n6wQfGNkqvROlW3kD/fOr7MjQBdf/5PtvYFncUV/j1q3hMwhHogVfM +WX6XuInU3y4+vtEPc8MGvqN7DJtP9k4fTo7qSeb4oqc5Z6Gtuu9ppD+fUt6Z77+L8ZwfLb7kF4l3 +BTO7NL8QrL8bRhfNceajUsJgzuN++uPos4iGu+KwP6yL8X7q23fzEW/tiUXcqyd84wIMlrO+4myC +P6VZXOgZzDrf9uyrGaL/Z2YbfQEXzDJKqIfkTighsVz1VQkVsPnvSS7BXNwCHsKP6p97mXsllKV4 +/LEtefotkKgRWuJq3OgcgxhcsnJ3GwC6ewkYbRowOQ5Pu8fVbH1wFkoWm/GmIVjF+/vbYMfd7mTO +u7e3JscnCQShlcTEo5pDNzhGk8urhE8OQ/et1Hfha6T4B+eCObXk7e7hMWz33fViQOm+q9u2JCKF +33ycj0lxZ+mhQiL5armv7fTkSwmu5E9+BD3yg9vZSXde3zit7TEOW7LvnwWw9mHNzD5Sss/vM8TF +Lr62f8haovf8Yc04PYnHPo2F1PfeaU4z631eY6ArffyilmvNXmPzVZT775feLkasRMXi67xe9IHG +RiLG6bbk72QW0ObnZzHTBw8cvvxohp2JEj0ddRp+DdZ7KfHA+MPan7tg7+d8ecaLzulLOHK1XYUY +mrvBfG6fO+a0bXyU0Oru5N738i9wAo+2SWQE3ZgZA0sBGkqA/f3QS1Yus3l9c1xezQ== + + + /+uVsVMcWj1fydIVaJkY6tHRMYsgyXgOCkrwCO24+QfFRLx3nDYEfV74bBdsZOKA04JCxEFV1UJG +4ihRTudvQA/xn5MoHjpBJQw4PGldUl9JqfH4kDl4KEWVcNosGAXblPlIxJ+TF4Qnm4GKYrToKNmv +196V84K35xzujVvtrG/rrp3JeT8u5gNKF+J9zz9IDobDekqYpkqmiJTCYT5zJ+Pnu8Dx9uE2vnMG +mA/siNssc39wWf5GHfVkB/Sj/SfVEfJ+fgyc6mRsiFx2vZWSIZQzH4QBi6/16v8A7SqHmTPbEvqg +B7MY0d3Z9s1RHInhEihmb0LcEYpD15wHqmVuzYIs1/qkuJa5aTc+Kbt3gRbfee27uBjb8l9IJVk6 +BS7YqpDAuPoOCcaNIoVUKB6PEe89MArvNF0YTK4RzKF2Srhp+nN6U8HARA45jMLCSGvvK1lJ336Q +ZcyOoV8JtxWfLp6zb8PmF9DiVkQbjASC2Ex2J/R5z3Ah3yRZDHUraswf/yxdle8wMuJvXish++/W +C4xTYGup7vd5WQvjA51PfOHLxfi++kDc/vGUT4c7hyL63bcQqYVovHF7Q4QWSfZAWQm41aJFC6I1 +1u6nSing0lupwmMpqbJWRbqeMBhFD6NQ68/kx2Gq+yzHCAx6nGYWf+metnOJduJZOQJkG9lU/dMU +0VHjNPL4hHwyQOmH9/wnfhKjQ+IdVN+VO57sXn+3BCCxTy6vOTfk+DTvI9GLROzB10lxX2E1Jq70 +C4Z7Sngk3SjHzCGTdGtag+mTLVX3eKgWsvVws4VWUqe9rTiWQDiO1SDN6O5UTZBIpTygMBwntFCP +b5cczePdcqwTOK4OFE1BCfoQl+xD7eeC8Omzo9c7hhAX0cf0AI/gzZxWE/Vk/8QzyJw/idtKyi1/ +6BOSkebpbeLno91XL/h6mISVQodOMBZAPWqQrNzv7GffxWHD0M/iTntxB4DK/aAH/gsTaUZqoHP2 +A0OQmon31MzWvc26iQQQTxktEUqVj2QtO3u5zHfw9Oj+I51guO3vj6z/tdYGPaMyzj62Jruq0Nv7 +8CYOI18dZMcvChcAZCW1lL5sH/NdSMpQEiQJxyditUI/9fl+iXZlJ/n8nq2XqjPZpgV97t+PK6nu +Vj9HojyyeOpvYDgGtbCHDHkQPbwX3zDtpYMh8lI2WPk4MMCFn6AA6v0Za7qlwj7k5Hvi8CwdxsHi +amhY3d9YshxMgLLRqUxIsgtqzJdaduT4v49dAsMKbiHKiu6Dm2m3NboeddqdvjvoirsOkucse9tv +DnKjVqvS+s8kM2hMe63+xB1zHyTL6fNzCfhrY9BsQXfvXDyn8aFnOuppV7QxX8sg/aPYe3R5SehT +zeES42PMMPWBrGuPF6LEx7X+affuq556vywlk8E++xI/Ye+kzHZzms2kz55ftCjq0FyxBKLIGZQV +AAHdPji8ukjk5fGxdHZ4H84NHgVgrNnR8yOTecxVK7mj5FHDCh6NISo6z9dKkfOZwFseORfqB6ks +d5YgQfPEx23lKnkmvl9RRc5Re1dVBEWT7/a30KR4UyLit6+5GvD8pytDogS2SZXn1H3qe1eaaCcE +j+VXp6bwx5kQ1APgbLDdPlaYkS9br3QOM/n84d68CVMAtb8XxByeoprIo6RHvEQTsWtfCznJKVE0 +NTWz9pT6zpwxKg9nMi8khIRhp/RTCxihvK9EX0h8ngTkhffQi/IlF9x4NVyvpRLqOVN6WyzPHhzo +gRwlf6ReOdxGsRp8ieTejpM1+4w5e6PncXAJyziVVaEU8vWUPKrKxVRWRYh8+ePyJkaNMFGkJ9lm +7LhnYbj00UwMn748+2LpVrl4OxtnlohmCAKWrYwjlzd2WSudKOaBvtP3l8NzAvycDXSHcfeTH2Nb +z7eX8zWeZ+zyBZTP4uOBIeqO6QioVPWOMDmvWwWB+CIbktfQqkp1Wyk20bseltQw3selrKf7YZxw +V8lHNybtFB7A0mC+CWtE5OfwEz6RgYRZmSJIxZNbEA3j4JzhkhxlznOPW9qfhwB3/uHDuKDGjxfU +ndeQJky07M3rglgiGYGouIeeMOVd+diaoOkWJGW20Mrkdv2lVOSuNNEyygw+EYwEHqN0vP/J+c4f +bmabc6mmuoYS6J5gdmD3MTdAswwq9Rk6QpqZ4XlDFfBmMauNPDg6U3TDxWS5C80weQ7PJeeh7UOU +Lu1oPj5ye8e+M2OXe/kj0w7kaxa2y2G2cfyyDasPPphAArFaehx2zGL+ZVwvgfWSDYPpIbfErdxb +CbpIrOZveX45er5KH+WSjXYDU+Su0vxR5ZjYuEoCByba6VYMWct9kFhBhgQ7dvBzBiRXJCSSydQu +Lm4T8ZPpZE4rP79Pd46OYwoTBu39OxEvT4qpkPQVSHXD7f1ksfFRQLjK5FNkzcfsY/MCY7Avx5ge +gihq+NKd8SFL6iTmy2FN18KkNPPg+9p2UnSmzE3760nF41vhKskcTscqHzt4BEpW0unZg+3xYeA2 +PlI6oXZE3lET4zBZbhZZVlMAX/NZ4xFXbKDgI1ZonKksDNtaA6EBp3LBelET8UwJduoJROvlOtXL +qZuMVsDgIvui/4kZna3UWWX7Ffc0jSkfQWK0n2DKSLo3S7W7RLfLQ7J/tv8zy+vSH1zMvvU10JBa +64FmXPlBsVVNxCs3Y+Xp3AUcqyfMcIUK+t0+QUQNt+OtyvsNCNtaz1yaUrmYTGdyXy9YUT7seDHF +xN3D0Yib7Hr8h1eCJ3Swd4cp9BnPTkY6nGXUn+CDC0/wZ3iND04wwb7pCV4+nHn2f7ZHsC+XLyYX +J70RtroJhqkzqxphq5tgcF5WNsJWN8HURJCVjLDVTTDtoodVjLDVTTDislnRCFvdBMOSjFWNsNVN +MCUpfzUjbHUTzOVdNMK0mljiifZXR6TqoZ87zDVZs0Kv2AbG4rezTMszDOIZaiTuKue1XFYqtZRy +39zsURv3PEw+5KoU3UrcbS5ZnmyVcZObWPNxrBTKGzKq2qXXWXrdrVVaHGoXmL71Vpm8ZANjfnfG +wpZngiUcM8Eau0ZbbHnWG36is7Q0E+yOIhNMS2jzTHzj5ZlgzPlzMKxFkpalCk7rxR/TSkHuO2e9 +2a50a+v45HTpSm/nPnW5NOuNyR5eZx1W2tqKHNiuVNt9ZRmpg0untMiz3TNjcdjJaSATDouPxGhQ +5JlS/aA+uCfSjhTkaPJOed7bAuMk86Vkus1y2fb53fgTO59e92Wwqy4i4bieJ2d61+VV3gbl9gvs +pdhuLlnqjWcxpQZ7+tpmQOa8jlDNO8/6b8/HimoL6ucNBu5bWAHYIlFivZgJU+7ec8kn1gPyI/WK +7lq9sH2+NsY+2sy1S7TRZhJrNiVOhUNrRZtfLpcHwNHe/2XGCgVIJPBgCMlerRnsXxLqrwZBVm48 +L6+2dXpuymExpM44pUYZP1JtMPvSy/PyDGlAq+TldW9WT3UgsnJZvgdGnH6dFApr2VwWFl6BPbYC +yeWlShb8ZQ6P8ZpCI1Cx4mTt/MVFkDBM+/vEorvBcna0/OwvMKQ12ZHp7L+8U1C/He2/2uaqkQJp +Mu062WpW59wqVw3k/m+z1Shy1VAik1yamC/4RzKyydGb7f5qRFW3T9SiSP/dlECZJ6kW8fasS1R2 +qU/vZkq1Wsvq+dPvS9M1HdOybaFtLMv/ah6YBjWnmD/ZDfvxG8wCT16WXLrmcU2Y8pXNa+ktT5dH +dvxjMWwSTbysDReoPitrsR8W7Zd31SF06x0ky4+eSYp7v/wislmpNrlKH7wSp/l83cmLfrOBegH6 +8rsNfn2zAWDM+W6DX99soAbpl99t8OubDVxeirsNAr+92YAU4TvdbfDrmw3UhLbldxv8+mYDl5fi +boNf32wANOZ8t8GvbzZweSnuNvj1zQbouXK82yDw25sNMC7meLfBr282wFJMx7sNAr+92QB23/lu +g1/fbABrcbrbAAxgh/sFHC81QHt/tQsVVptUuc9gVh6/eKPBBu8zUEvKrW402OB9Bvq1OAs3GgQ2 +d58BXryl3mhgIVoNlZsF081B0jDIjVPfmb3PmTC6RWob5fZ9/g7AkL4HWVHdJnC5lExV9Au9zF8K +EM+cN47u9J7Yj/jopGQ5+L1DHEuGywO2qsJl9rHpbRNXMuxVXxV0IPck5YqfWUq47+ygs1XcnaVN +vM2c5kRg9vQwgybU78d6W1LknzvJSJNrq36B0vM2iWjgqVRiGiUmt783muphDxQE7yAIvv2pXmC3 +qgyWPxmWofMgljn/+jnjMqGngDGW0mxfZAPj7G7685kPJpnjaZQQl8ur31iAQraRhR0ZAPqDUzWO +fFS8IUEYY1TiOBuaRSXUT3ZwwZEHyed7vp93ti/aB8qUKvGr3e/qLBSSnt3Nu6uFQuDgoslBrihY +JIHPJ4Z9ufxMRfziyJgRQfJFMOuWI/EJvK/sKdPKPuwBEvQ06fte7r0w4MFw2/pMdfKJseoO1Or9 +UUDf4+5Xoe00DIpKqJ3zfUivWP5fx5i4d7hvOGiRA7meaX1dHDKRev4ED1Ioze0cvWfC0/EDSJ/K +VHvABJOV7G5HzWzeO+NzvnxSVyH2E7HsAXDQVP1E0S0vSbRBi2Tc1xOVG6mMmaEldX+1AIia4Y4R +R/2GgMF/H7viQEbnLPuW7TeNuWQurxdayq3JdIgdIm+pVrvTL9T+aY1crFv5j4H/WDfnjspuLhLB +fxg3D/8v1F2+xqA7GI397kLf5X07SI4mmU5j0hn0a6N/3DFsergs3J5n3DH3rGvc7QNImDfoDE/8 +mLz2BtC9uRh3Ev7/8LdrfwpzZ+D3axcTZjg+4mbCLCcL8E+EkSWY+tvFqIDBC//AHxfwyxc0/e0W +3JfupxfG3cSxblwCK4R5GcAWInyYkeDtntIWjUKTFGZFRnRjQ1SGhQnRaDgiwUwCy4Yl8prEhwWZ +4dxpl8AIAI0IDyUAxM3LXDjKiCym74UlkHxuXpLDgihIbkFkw5gCBC/xUT4s8xzMIQphjmOjbj4i +hAWOg8lEeCREeTfPywCaQF7jZFguvMaL4QjHRsjgcoQX3TwnhMWIDFBH5TDLsvAaQC1HRAXGKBPF +11gmLIk8QBAVwxLDYCc2zETgF5wtKokiaYmwLC6fA0AkjrzHwioFVmljYd2kJRpRWxgYkrREohHS +wsusSN7jw1wEloC4EWVWgLXwAGaUcwOCw7LMwS+48IgIA0RkpQXekyPhKC/xSi+JgUHZCKBHgl8Y +QIYgSWRT+EhUIDsHC5ZwC8jOMazSxghKJwkXjNsr46YubHja9QETQn+YXMGxBLvWI22wgIjSJsJa +sUXgcBBsEQSlJSLwSgP8z91wKZ0krRMfdSsDCbOBRPfidA0AggGiBsTBS6zyROQEAgduiCiSpoiE +hAQtUVmQlRYetxGIjYmyCiCw3wIBZLEXjiSpI0UlZaS56RCO/Vvrw0pOoXb4wiyMzA== + + + IPBw1sNSVMLjx8Ay4fBxoFlGOaQlwDEflWRcBeCdY4AKeUEGJCBa4HiJER7pmwHqwpPCSHBSgKp5 +IBegdDwgAg/HgQFUzdoKpC0aZZR+oN5ESFtEwSTPiGExipvEc2FJ4pAykT/g8YWJGFw/0iXP424J +XAQQF4XXWJhbJCQjKQTCs3Bco4h3eJ8nYEELJ/HaYQfCBrA4EagPVyHKYZ6JwuBwklgBtrvqQtqO +EMKOIow8TAa/8BKLrUDRPJwVNxx2RAgH/Tl8EoFfkUphRs7NCZEwnGGBkIyMZMVxeKwFmCfKAb9D +YIBvhCWYi8DAsTATxwHIwK2gJRoWETwOliUzEraIsE+AKGA2wEMAoQilIMEpbbhYOHiiRHYJUCVE +ZDcL7AQISybwwiRzLSpXSLv0Njie5GQADAycDQ4OmKmN4QSln8wqsDLALqMRmBMQIUUA8xwTBUaJ +x59H/gHUzOGGMQJH4OJkgKLhQvgJqQMWYbGMRFCj8KIIo5x2DqkCuQ3wpGiEENQicRacaL6QUoQf +iEIi+kKhtYThZNBr1CZUwlDrSiUMiSB0t1eQeVGNBQq6zIuqMk/WZF5ElXkozRSZx2kyT9RlXkST +eTyReYwm83hN5kU1mcfrMo/VZJ64IPMiZpnHW8g8XpN5vCrzJE6Tebwm80RN5gGlqTJPVmUesBmT +zIOWBZkHbSaZhy3zMg9bFmQeYy/zhAWZJ1rIPEGTeZIq81hGk3lRTeYJusyLajJPUGVeVJN5gibz +zBuuyDxWE0K8LvNYTebxmsxjNZnHazKPVWUer8s8VpN5vCbzWE3m8ZrMM083k3mSrAkhXpN50KTK +PF6VedCiSjNek2aMJvP4mcxb7EVGktSRiMwzT4dwIHELMuKMCYtAKeRI8ApDR/TLEYmwYCaCHB4o +CTghS/YxCoyHsHxRIrsv4mmRCOETKQBDANkCBxeEqCInkZNGeZacLAGJFV8DkmR5RCD8AhySMEMg +OlwdSC+ZQTICBHKIUiAaEJ4CvofgImkRlVKAo4AaUwQXIIiEJHG/kGgiynuyQtx4bkVCLNBL5nBX +NNkrgFgBquHIsiMRWSRwskyUSGNAU1RAARVl8OiwbgVxiE0CCse7F1CZduKvU3K2EYG8DP/KcHh6 +5NzKHApWra1gbOPIlhQMby62zN77mDXCsQ3LUZCehglmbQVjG6yPFwTDeJZNszfxPAE9inxkBgqS +kKjqBDPoDE36svQ3rdq0Vw1TzGAxzGGAz9CmL01/16pt9u4HbgfsuETYAHBYVuIJa4jgaRH1poJC +fUwkMtfGAwELeAIt20D3kGUydQQYRASp2dgGS5WRunhU6YncRtUZGR7IA4kTUFSwirAXBGQ/ArIR +hANoH4hRAJYbxbMCSiacF2xRtSzQhsKiGNUb4DXUcfA8651gX4Fjw2t8FMSOzJOWqERAEhXJogii +qBTR2mSi2oFKysC5I2/KyIyUNo5lUO+UUF7gaHjM4QVBANYuskaw1AZlNaAyCoZOEdT0WMJBQNuV +CVok5ABkwShj8DUQpiIyOgHPLbK+CAhTCTkPoJOXREXOwmAGpKM0jhJJqTaR/cLtZ/Dko/rECgo3 +4ySJJfsQAWUayBKMYxaPu9aCImzWBlwQeQiOBaQYlWW9DWQTTITyiVdsDpwO6JHVQUgrVpQEG2Ak +D2xTCVCWOA3QOTolupxiovCgFER5lKPA2qISCKqe0oaESJoiZAxg5qh5ALeXJNRCo7AIGeUbil8B +dWHoIQqMYWbEGlgZojxP5lFgqjzqOmAnSBLuN/RiCC9EWxwsFTJ6RJYUoSICTnAs2GdgOpLephyt +KIeUgnZTBAWKonQocImipFDB3IlMW59INLhA8qmWTZTQHEgunqwZ9CeOaNDA9QRiXcvAEQhUAop5 +BVeirFC0iDJc5MnuE9JWDrLEsaoNilZVgfAK2DhJkb6oguC7qABGFH4eYSKyijGOVfuBbCNNomrb +oZohs4Sjo56IG4jTMqgN4NaDwBNV8ECdB4YqabIJNxctzDuVBiReaSPDkhY8FKQFjaYFOklrigmA +LEWJwQ1oifJEakTQRANyB8qAHZIi2sZzgtrEaaudf1W1Mm5dstvndz/cu5ZJRK0Ph4pVhKiAcGii +HApGMKlQj5VUHQtW0HVxUcQpSm+wfjng5Ni28G4XQLCfkxMRUlRv0XCLAP5gLmAOIOUVNUqG0wHD +ossCSJNsBuEqXYtXHaYCWgazQVaEG2jvOFUE7UtBUa4icNpgWCBB0CZEhf3wUVzB4qvLp0IHGyIB +2WOEiaJURnKKsGhBC7hvcIi7hAswHE6vyZfu4qsOM0mCIkN5kEwysapwBKJO88hIQVTgqBJaNsjE +gXEC5ZAm05sOExGjAfmliLiAXcc1RZQTh2IRrG6ewI/ePpRT0I8YUV2Ldx3mQncZw0pEFBDeDHNx +ILPwBHBwGkEq4LCg0ooiylt4xrEcWejCq05TgeYqEi0BmJhEDhs6TDhUldHW59RhkVsSLhuBbZTJ +qsyvLp9J0WKRXDkZLVFclMAAGxNR7YU5QQ1EaJEfS0TX4HhFu+5avOswF4NMl0eGDxKDEYkvDHkC +4V3o5kEGDOPCwY2KaIKybDjKsjJpM7+7fC5V8KG4n3Nzwepwq2zcXBxrdnNxrIWbS1LdXGC8L7q5 +ZDRaoIlRLCvQQBn0I6C1CFsDz4jbDu0oBjUAaGPRVIA2dF2xqGihBSSJskpVHDrIOFSyZYJpURkL +IOTRT4WePEmUOGKBy+iDRGUClKYosbeiRNxBi4xuCvQIRlVvInE7RfQmlFUo/1nF18QSrzlxcxG5 +xqMfh7jQ0JeG+pQiHBUnEqpmcIyEKJxiOHthiZhbyAtZ0c0BouDoKwoFx0RY8hpOi2tDJicR4cYR +7xu6xMhxBV7LEZNZVjGM/jD8BRBEOBF6z2CkCApgVlaYUxQ9hRFeUxRgEjTUOaI/otNMa2kQlspw +6GDRezEKmnliVgLj5UBTg8MkkwWjcgrT8cAtRBRnuMXoSOOQIEgngB+1EHS+SRySmIiCNYLmKScg +USP5yKjqwUpA1spIh3iMiJwiXEIhSPTTRKNq2EB1jQqKaxTQBBhTXaOi5hrlFlyj7IJrFOYBjosi +JIKyBNaLvh5CKKCHMKiHKO5GpBiM4PACKuEcg5qbdkRBhSDvCSIwaWQGQjQCK4ZHgog8F6hRBnMD +34OHircJoBPRaObQlGAjCmcGUgYwQUkDBRNb0KJnRLKfwLii2AtMBOLcIr1ERlGcJVDHoQU1LZkl +DJUokQ0iNVEnBgg4Hl7n0HkUVZkzUfU5UMBFDq0HHmMVMiHfqKS4YdHyAWQAMmFfRRZ1woga2MGR +0Y0gYDhJIjhBB5PMysQw4XmwylAYK6cPNlpGjxi2sCKgXiAqJfH3gdmGDjTSiZFYluxcFIQPaYmi +3sfxsuo0gxYRthIxGVUIDJvIatHhSxyR8DqPh59DhiKyioEDFEe8wgxhqVGVj+PSgLMD61HCeBLL +oH8ZGST6LOAXYLKc+l6EHAwZNw5sKw4dh1FB8dezHJIOMk+Dnz1NfNBmP3tkwc8uWPjZuQU/u6h5 +1XnNzy7qPvWZn33WFtV96hHNzz7Xtuhn51H9jxBWSyw2JHJRwBYOYzLYMvOzEwPY7GfnzH52IAST +n52Z+dmB04DOAfwAqJwHBgK0gfChps6rMoSLomcN0QAnV0RhAu/JnGKdzNoKxjYeScDUJoaBAGA0 +9DAyRKRhZAnIEpQTVbrA+WAjxE7mkBGAugRtOABuGViRsEkCacHzzEWRVyBeAFCGJxYBgioJguIj +YDmF4cObPEGLwn70yAKrRBbSxrBX1Bz24qSFsBenh71ELezF6WEv0SLsFV0Ie0UWwl68Oeyl2DeE +wnlZIFSIZlhPDdDwircRaE5ws6CskOOD+8FEyYFCZiMpwWMiQ4AwgbEz6GuYtRWgDf2caCkAaxcx +ukDeRCUHcSZE8HDAoMSYAP01wiiEg5FscoCA3eHZQk4gS0TWgcxB1zOHngMGhS0GDnji58eRMPSA +yFdpGTkB8E0SH4oi3zGv1zHaaRX58RZr7VZlVOt0WyNXe1z7q+Wu9fuDSW3SGsITd3vUGk8Go5Z7 +/Dn4G1vgFa2715u9zrn+L1T7Dxc= + + + TM + \ No newline at end of file diff --git a/webapps/manager/index.jsp b/webapps/manager/index.jsp new file mode 100644 index 0000000..2806b76 --- /dev/null +++ b/webapps/manager/index.jsp @@ -0,0 +1,18 @@ +<%-- + 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. +--%> +<%@ page session="false" trimDirectiveWhitespaces="true" %> +<% response.sendRedirect(request.getContextPath() + "/html"); %> \ No newline at end of file diff --git a/webapps/manager/status.xsd b/webapps/manager/status.xsd new file mode 100644 index 0000000..5af979d --- /dev/null +++ b/webapps/manager/status.xsd @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/manager/xform.xsl b/webapps/manager/xform.xsl new file mode 100644 index 0000000..0f0753f --- /dev/null +++ b/webapps/manager/xform.xsl @@ -0,0 +1,140 @@ + + + + + + + + + + + + Tomcat Status + + + +
    Tomcat Status
    + + + + + +
    + + + + Memory Pools
    + +
    +
    + + + + + + + + + +
    JVM:free: total: max:
    +
    +
    + + + + + + + + + + + +
    Name: Type: Initial: Committed: Maximum: Used:
    +
    + + + Connector --
    + + + + +
    + + + + + + + + + +
    threadInfomaxThreads: currentThreadCount: currentThreadsBusy:
    +
    +
    + + + + + + + + + + + + +
    requestInfo maxTime: processingTime: requestCount: errorCount: bytesReceived: bytesSent:
    +
    +
    + + + + + + + + + + + + + +
    StageTimeB SentB RecvClientVHostRequest
    +
    +
    + + + + + + + + + + ? + + + +

    +

    + At the simplest, edit the Engine portion + of your server.xml file to look like this: +

    + + + +]]> +

    + Note that the directory structures under the appBase for each host should + not overlap each other. +

    +

    + Consult the configuration documentation for other attributes of the + Engine and + Host elements. +

    +

    + +

    +Building Apache Tomcat from source is very easy, and is the first step to +contributing to Tomcat. The complete and comprehensive instructions are +provided in the file BUILDING.txt. +The following is a quick step by step guide. +

    + +